go-clikit uses semantic versions, and is at v0: a minor release can break
the API, and its notes name the break. Each release is a signed tag. A
release that is broken is retracted in go.mod, and its tag stays.
v0.10.0#
2026-10-26on GitHubon pkg.go.dev
go-clikit v0.10.0
The kit now writes a file atomically, locks it, and checks that a
directory is private. A doctor command writes its checks in fixed words.
clusterctl, sind and brig each carried the file code. Two packages are
new: fsutil and doctor. The release also fixes the findings of a
review of every package, and gives some names a clearer form.
Some changes break code that uses the kit. Before v1.0.0, a minor release
can do this. Each renamed name keeps a deprecated alias, and v0.11.0
removes the aliases (decision 36). A changed signature cannot keep an
alias, so code that calls it must change now. “Breaking changes” below
says what to change. Decisions 36 to 46 in doc/decisions.md record what
was decided, and the decisions they refine.
Breaking changes#
Change these before you update. Each item gives the old form, the new form and what to do.
cobratree:
Strict(root, moved)is nowStrict(root, opts ...Option). ChangeStrict(root, m)toStrict(root, cobratree.Moved(m)).Strict(root, nil)still compiles and works; you can writeStrict(root).- A missing required flag and an error of a flag group (required
together, one required, mutually exclusive) are now usage errors:
IsUsagereturns true. A program that exits with 2 for a usage error now exits with 2 for these too. Strictchecks required flags and flag groups with the arguments, before thePersistentPreRunandPreRunhooks. cobra checks them after. A hook that sets a required flag, for example from a config file, is now too late. Set such a flag beforeExecute.- A runnable root with subcommands and no
Argsnow refuses a word that names no command, with the text ofStrictand a usage error. This includes a word after--, which cobra gave to the root. Give the root anArgscheck if it must take words. Coded(0, err)andCoded(-1, err)no longer give a status, soStatusreturns its default. Use a status of 1 or more.Strictpanics on a tree whose help already shows topics, fromHelpTopicsor from an earlierStrictwithTopics.HelpTopicspanics on a tree that has no help command.Topics(nil, ...)panics. PassTopicsto one call ofStrict.
localweb:
Hosts(answers, refuse)is nowHosts(answers, HostsOptions{Refuse: refuse}). ChangeHosts(a, nil)toHosts(a, localweb.HostsOptions{}).Hosts(nil, ...)now panics when the middleware is made, not on the first request.PeerListenerreturns an error for a listener that is not TCP. Before, it returned a listener that closed every connection.
localweb/session:
Newreturns an error for a negativeMaxBody, for aCookiePrefixthat does not make a valid cookie name, and for aCSRFHeaderthat is not a valid header name. Use 0 for the defaultMaxBody.
localweb/sse:
- With
Options.Donenil,Serveends the stream whenlocalweb.Servestarts to stop. Before, the stream held up the stop until its grace period ended. A program that setsDonekeeps its own signal, and the stream does not watch the server. - A negative
Retrysends no retry field; before, it sent the negative value. A negativeKeepAliveturns keep-alives off; before, it panicked. A negativeCoalescerenders each change at once. Formatdrops CR and LF fromEvent.Name.Servestill refuses such a name.
localweb/localwebtest:
Loginfollows the redirect and fails the test when the page after it does not answer 2xx.
localweb/launch:
Acquire(runtimeDir, program)is nowAcquire(ctx, runtimeDir, program), and(*Instance).Retire()is nowRetire(ctx). Pass the context of the command. They wait for the lock untilctxends and then return an error that wrapsctx.Err(); before, they waited with no limit. A free lock is still taken afterctxended, soRetirecan use the context that stopped the server.
localweb/page and localweb/htmx:
htmx.Confighas the new key"historyRestoreAsHxRequest": false. A test that pins the configuration must add it.IsHTMXreturns false for a request withHX-History-Restore-Request: true. SoRedirectanswers a history restore with 303, not with 204 andHX-Redirect, which the htmx history loader cannot follow.Set.RenderandSet.Respondreturn an error whenincludenests more thanMaxIncludeDepth(100) deep. Before, such a template stopped the process.
termtext and cliout:
EscapeLinesand the escape writer now write a CR immediately before an LF as it is (\r\n), not as a visible\r(decision 42). A lone CR is still escaped, andEscapeescapes every CR. This changes the output of text with CRLF line ends, for example acliout.Table.Captionor adoctorremediation.NewEscapeWriterreturns*termtext.EscapeWriter, notio.WriteCloser. A variable that holds the result still compiles. A func value of typefunc(io.Writer) io.WriteCloserdoes not; wrap the call in a func literal.cliout.Table.Addcopies its cells. To change a row afterAdd, changeTable.Rows.
fanout:
Batchesreturns([]Batch, error), the error its step ends with. Changeb := fanout.Batches(...)tob, err := fanout.Batches(...).MapOptions.Nounis nowfunc(n int) string, notstring. The function returns the count with the noun, such as “480 hosts”, asdisplay.PlainOptions.Nounandcliprogress.Options.Noundo. ChangeNoun: "hosts"to a function such asNoun: func(n int) string { return fmt.Sprintf("%d hosts", n) }.- A negative
BatchOptions.ReportLimit(orLimit) records no limit on the batch span. Before, the negative value was recorded.
progress:
TraceContext.Traceis nowTraceContext.ID. A struct field cannot keep an alias. Changetc.Tracetotc.ID.Fields.Exitis nowint, not*int, and the newHasExitsays that there is a status. Changeif f.Exit != nil { use(*f.Exit) }toif f.HasExit { use(f.Exit) }.Span.Updateapplies every field option. Before, it applied onlyTotal,MessageandWork.Totalnever goes down, and the unit does not change.
progress/cliprogress:
- The probes
IsTerminal,IsPipe,TerminalSizeandInForegroundtakeany. A call still compiles. A variable of the old func type does not; change its type.
progress/eventlog:
- The method
Feed.Lineis nowFeed.Take. AFeedthat a program writes must rename its method.Reducer.Linestays as a deprecated wrapper. Decodereturns a*VersionError, which matchesErrVersion, for a line of another version of the log. Useerrors.Is(err, eventlog.ErrVersion)to tell it from other errors.- The deprecated
Forwardgives the sink only the first run that begins. Before, it gave every run. UseNewForwarder(sink, trace), which forwards the run of the trace and counts the others inDropped.
progress/mcpprogress:
Options.Sendtakes a context first:func(ctx, progress, total, message) error.Notifier.Close()is nowClose(ctx) error. Pass the context of the tool call, and givectxon to the server’s send.Newpanics on a nilSend.
progress/display:
Colors.Stringwrites “Colors(n)”, not “Colours(n)”, for a value that is not a constant.- An assignment to
Classic,Aurora,Ember,NeonorTidechanges only that variable.ThemesandParseThemeno longer see it.
Renamed names#
Each old name below still compiles in v0.10.0 as a deprecated alias,
field or wrapper, and v0.11.0 removes it. Each Deprecated: paragraph
ends with “(decision 36)”. Change the old name to the new name before
v0.11.0. staticcheck (SA1019) shows each use.
| Package | Old name | New name |
|---|---|---|
cobratree | Resolve(version, commit) | ReadBuildInfo().Resolve(version, commit) |
cobratree | HelpTopics(root, lookup) | Strict(root, Topics(lookup, names)) |
cobratree/cobratreetest | CheckCommandLine | ValidateCommandLine |
localweb | Origin | OriginPolicy |
localweb/session | Sessions | Manager |
localweb/session | Options.Cookie | Options.CookiePrefix |
localweb/session | Options.Header | Options.CSRFHeader |
localweb/session | Options.Field | Options.CSRFField |
localweb/session | DefaultField | DefaultCSRFField |
localweb/assets | Assets | Set |
localweb/htmx | Version | ScriptVersion |
localweb/htmx | IdiomorphVersion | MorphVersion |
localweb/page | Options.Reload | Options.Dev |
fanout | BatchOptions.Limit | BatchOptions.ReportLimit |
fanout | Failed | FailedItem |
fanout | Item | ItemInfo |
fanout | Failure(noun, s) | &FailureError{Summary: s, Noun: f}, only when an item failed |
progress | Items, Bytes, Percent | UnitItems, UnitBytes, UnitPercent |
progress | Stdout, Stderr | StreamStdout, StreamStderr |
progress | Hidden, Fold, ShowLines, DryRun | FlagHidden, FlagFold, FlagShowLines, FlagDryRun |
progress | Unbounded, BySize, ByTargets, ByBelow | BoundNone, BoundSize, BoundTargets, BoundChildren |
progress | WithFlags | Flagged |
progress | HTTP | HTTPRequest |
progress | Run | RunInfo |
progress | TraceSink | RunSink |
progress | Bus.Trace | Bus.TraceID |
progress | LogOptions.Run | LogOptions.RunID |
progress | CountWriter, CountReader | AdvanceWriter, AdvanceReader |
progress/cliprogress | Run | Session |
progress/eventlog | Line.Run | Line.RunID |
progress/eventlog | Options | ReducerOptions |
progress/eventlog | ReducerOptions.Keep | ReducerOptions.KeepRuns |
progress/eventlog | Reducer.Line | Reducer.Take |
progress/eventlog | Forward(sink) | NewForwarder(sink, trace) |
progress/display | Colours | Colors |
progress/display | Colours256, Colours16, NoColours | Colors256, Colors16, ColorsNone |
progress/display | Theme.Colours() | Theme.Colors() |
progress/display | &Summary{Theme: t, ASCII: a} | NewSummary(SummaryOptions{Theme: t, ASCII: a}) |
Changes#
fsutil (new):
WriteAtomicwrites a file through a temporary file in the same directory and a rename, so a reader sees the old file or the new one, never a part of it. It keeps the mode and group of a file that exists, and leaves no temporary file after a failure.Locktakes an exclusive lock of a file through the lock file.NAME.lockbeside it, the name clusterctl uses, so that an older and a newer build exclude each other. It waits until it holds the lock, untilctxends, or until the wait passes.DefaultLockWaitis 30 seconds. On Linux, the error after the wait names the process that holds the lock.Updatereads a file, changes it with a function and writes it back atomically, under the lock.EnsureDirmakes a directory for the user alone, and refuses one that the group or others can write. It does not change the mode.CheckTrustedrefuses a path that a user other than this one or root could have written, with an error that matchesErrUntrusted.LockandEnsureDirreturn an error that matcheserrors.ErrUnsupportedon a system that is not Unix. On Windows,CheckTrustedrefuses every path.
doctor (new):
Checkholds the name, the status, a detail and a remediation of one check.Statusis “ok”, “warning” or “failed” (StatusOK,StatusWarning,StatusFailed). The words, the JSON keys and the text ofWriteare interfaces (decision 38).Worstgives the worst status, andCountcounts the failed and the warned checks. A status that is not one of the three counts as failed.Writewrites one line for each check,✓,!or✗with the name and the detail, escaped on one line. Then it writes each different remediation once, after a blank line, in the order it first came.
cobratree:
Stricttakes the optionsMovedandTopics.Topicsadds help topics after the checks ofStrict, and their names complete afterhelp.Strictchecks required flags and flag groups on every runnable command, and a runnable root with subcommands is as strict as a group. The doc says thatStrictmakes a group runnable, which adds a line to its usage.BuildInfo.Resolveis a method of the build info.- A help topic on a tree with no help command panics with a clear message, before the tree changes.
cobratree/cobratreetest:
ValidateCommandLinegives the words under a runnable group to the group’s argument check, so that it agrees withExecute.
localweb:
HostsOptionscarries theRefuserofHosts.CrossSiteignores it, and the doc says so.MisdirectedRefusalgives the fixed text of the answer to a request for another Host.Hostsandsessionuse it.Serveputs its stop signal in the context of each request, andStoppingreturns it.
localweb/session:
Newchecks the cookie name and the CSRF header name, and the error names the field.
localweb/localwebtest:
Client.CSRFreads the CSRF token of a page, from an<input>or a<meta>element.Client.CSRFFieldnames the element when the program sets its ownsession.Options.CSRFField.
localweb/page:
MaxIncludeDepthis the deepest thatincludenests in one render.
termtext:
EscapeWriteris an exported type, with its own docs forWriteandClose. The escape writer holds back a CR at the end of a write until the next byte shows whether an LF follows it.- The parameter of
Wrapis nowcols, and the package doc gives both rules for zero. The behaviour does not change.
fanout:
FailureErroris the error of aMapstep in which items failed.errors.Asfinds it under a wrapping error, with itsSummary.Keyed.Acquirerefuses a context that has ended before it looks at a key, also with no keys.- The package doc gives the rule for zero of every limit.
progress:
- Every method of a nil
*Busis safe, and the doc says so. Span.TraceContextgives a child the trace context of a span, with the span as its parent.NewTraceContextis for a program with noBus.RunInfohas the fieldVersion, whicheventlogpasses on.DefaultAdvanceEveryandDefaultFlushEveryare exported.- The docs of
TimeoutandExitsay that they only record a value.
progress/cliprogress:
Choosereturns(mode Mode, note string, err error), and its doc gives the rules for the note. A wrapper of a file is never a terminal.
progress/eventlog:
NewForwarderreturns a*Forwarder, which forwards one run, counts the other runs inDroppedand the refused lines inSkipped.Reducer.Rungives one run by its ID, andReducer.Skippedcounts the lines it refused.DefaultKeepRunsandDefaultMaxSpansare exported.
progress/mcpprogress:
- A
Notifieris aRunSink, and takesProgramandPanicLogfrom theBus. NoSendstarts afterClosereturns, also whenClosestops because its context ended.
progress/display:
NewSummarymakes aSummaryfromSummaryOptions, asNewTree,NewCounterandNewPlaindo. The zeroSummarystill works.ThemesandParseThemeread their own copies of the themes.
Requirements:
- None change.
fsutilusesgolang.org/x/sys, which the kit already requires. The x/sys rule of the lint now namescliprogress,fsutilandlocalweb/launch.
v0.9.0#
2026-10-23on GitHubon pkg.go.dev
go-clikit v0.9.0
The kit now starts a program’s local web server once per user, opens the
browser on it, and serves its static files, its pages and htmx. sind and
brig each carried this code, and sdash carried the opener. Four packages
are new: localweb/launch, localweb/assets, localweb/page and
localweb/htmx. localweb gets ListenerOwner.
Nothing that exists changes. This is a minor release because it adds
packages and API. Decision 35 in doc/decisions.md records what was
decided, and it refines decisions 25 and 34.
Changes#
localweb:
ListenerOwnerreturns the user of the TCP socket that listens at an address. It works on Linux only. On other systems it returns an error that matcheserrors.ErrUnsupported.
localweb/launch (new):
AcquiremakesRUNTIME_DIR/PROGRAMfor the user alone and takes the lockui.lockin it.Instance.Runningfinds a server that an earlier start recorded inui.json, and accepts it only if its process lives, its address answers and the user owns the socket.Publishrecords the server and releases the lock;Retireremoves the record.Opener.Openwrites a private page that sends the browser on to the address, and gives the opener that file, so the token is never on a command line.Opener.Skipsays why no browser can open: no opener for the system, an SSH session, or no display.Detachedstarts the opener in a session of its own.ui.lock,ui.jsonand its keysurlandpidare those of sind and brig, so a new version finds a server that an old version started.
localweb/assets (new):
Newserves the files of anfs.FSatPREFIX/VERSION/NAME, whereVERSIONis a hash of all the files, cached for a year as immutable.URLandURLsgive the addresses for a template.Options.Devreads each file anew and turns the cache off.ContentTypetakes the type from a fixed list, never from the host.
localweb/page (new):
Parsereads a program’s templates into one set and addsinclude, which renders a block by a name that is a value. A fragment renders the same in its page, in an answer to htmx and in an event of the stream.Set.Respondrenders the whole block before it writes, and writes nothing on a failure.Options.Reloadparses anew for each render.IsHTMX,RedirectandRetargetread and set the htmx headers.
localweb/htmx (new):
- Embeds htmx 2.0.11, its SSE extension 2.2.4 and idiomorph 0.7.4, the
files their projects publish on npm, under the 0BSD licence. A test
holds each file to its SHA-256.
Filesserves them, for example withassets. Configis the htmx configuration that keeps htmx inside the Content Security Policy oflocalweb.Headers: noeval, no swapped-in scripts, no inline styles, and requests to the page’s own origin only.
Requirements:
- None change.
v0.8.0#
2026-10-23on GitHubon pkg.go.dev
go-clikit v0.8.0
The kit guards the web server that a command starts for its own user.
sind, brig and sdash each carried this code. Four packages are new:
localweb, localweb/session, localweb/sse and
localweb/localwebtest.
Nothing that exists changes. This is a minor release because it adds
packages. Decision 34 in doc/decisions.md records what was decided, and
it refines decision 25.
Changes#
localweb (new):
Listenbinds a loopback address,localhoston both families with one port, or a Unix socket (unix:PATH) that only its owner can reach.Bound.Answersis the Host allow-list that stops DNS rebinding.Hosts,SameOriginandCrossSiterefuse a request with another Host, or one that came from another site, bySec-Fetch-SiteandOrigin. ARefuserlets the program answer with its own page.Headerssets the Content Security Policy and the other headers that every page carries.NoStorekeeps a response out of the cache.PeerListenercloses a TCP connection that a process of another user opened. It works on Linux only. On other systems it returns an error that matcheserrors.ErrUnsupported.Serveserves the listeners until the context ends, with timeouts and a grace period to stop.
localweb/session (new):
Sessions.Middlewareexchanges the launch token in a link for a session cookie, and gives each session a CSRF token. A write without the token, or with a form larger than the limit, is refused.
localweb/sse (new):
NotifierandSubscriptiontell a stream which topics changed.Takereturns them in the order they first changed.Servestreams the fragments of a page: a baseline on connect, then only the fragments that changed, with keep-alives.Formatwrites one event.
localweb/localwebtest (new):
Loginsigns a test client in through the launch link.Clientsends what a browser sends from the same page.Eventsreads a stream event by event.
Requirements:
- None change.
net/httpandhtml/templateare allowed only inlocalweband its packages.
v0.7.1#
2026-10-22on GitHubon pkg.go.dev
go-clikit v0.7.1
eventlog.Decode read a wrong-typed "exit" as exit 0, which reads as
success. sind found it when it adopted v0.7.0.
Nothing else changes. This is a patch release because it only fixes a bug.
Changes#
progress/eventlog:
Decodekeeps"exit"only when it is a whole number. A string, a fraction, a boolean, an object or null gives no exit value, as a missing key does, not exit 0.
Requirements:
- None change.
v0.7.0#
2026-10-22on GitHubon pkg.go.dev
go-clikit v0.7.0
The kit reads back the event log it writes. sind and brig each parsed it
again for the view of their runs; sind’s MCP server needs it to send the
progress of a command it runs as a child. One package is new:
progress/eventlog.
Nothing that exists changes. This is a minor release because it adds a
package. Decision 33 in doc/decisions.md records what was decided.
Changes#
progress/eventlog (new):
Decodereads one line of the event log, version 1, back into the first line of a run (its trace context, program and version) or one of its events, as aprogress.Event. It leaves out the keys and values it does not know, refuses lines it cannot read, and sanitises every text as a Bus does.Tailreads a log as it grows, aPollat a time, whole lines into aFeed. It follows the rotation ofcliprogressto the “.1” copy, and reads a log cut short or made again from its start.Forwardgives the events of a log to aprogress.Sink, each once: run a child with its event log in a file of its own, poll it withTail, and forward its events to anmcpprogress.Notifier.Reducerfolds a log into runs, each the tree of the spans of its command, for a view of what a program did.Options.KeepandOptions.MaxSpansbound what it holds;Changedtells which runs changed.
Requirements:
- None change.
v0.6.0#
2026-10-22on GitHubon pkg.go.dev
go-clikit v0.6.0
The code that clusterctl, sind, brig and sdash each kept beside the kit
moves into it: words for refused flag values, exit statuses carried by
errors, help topics, a command’s result as a table or as JSON, a bound per
host, a trace context for a child, a default event log that is moved
aside, the colours a terminal shows, a question asked with the display
off, and progress over MCP. Four packages are new: cliout,
cobratree/cobratreetest, fanout/fanouttest and progress/mcpprogress.
Nothing that exists changes. This is a minor release because it adds API
and packages. Decisions 29 to 32 in doc/decisions.md record what was
decided, and which new texts are interfaces.
Changes#
cobratree:
FlagError, set as a tree’sFlagErrorFunc, words a flag value pflag refused by the flag’s type, “invalid –count value "x": want a whole number”, and marks it a usage error.InvalidValuewords a value the program refuses itself the same way.Codedgives an error an exit status, andStatusreads it in main, with a default for an error that carries none; the first status given holds.HelpTopicsletshelp NAMEshow a topic that is no command, such ashelp selectors.Resolvetakes the version and commit a release build set with-ldflagsover what the build info records;BuildInfo.Shortcuts the commit to a number of digits, with “-dirty” for a modified tree.
cobratree/cobratreetest (new):
Runruns a command tree in a test, with standard output and standard error apart.CheckCommandLineandCheckCommandLinescheck that the command lines of a manual or a script name commands and flags the tree has.
termtext:
Wrapfills text into lines of a number of columns, by display width, with an indent for the first line and the rest.NewEscapeWriterescapes a stream, such as a child’s output, line by line as it arrives.
cliout (new):
Tablewrites a command’s result in columns aligned by display width, every cell escaped, with wide columns shown only when asked.WriteJSONwrites indented JSON with<,>and&as they are, and every runetermtextescapes as a JSON escape, so that strings decode to what they were and cannot act on a terminal.
fanout:
Keyedbounds the work under way on each key, such as a host, across pools.
fanout/fanouttest (new):
InFlightcounts the calls a test’s fake is in the middle of, and holds them, to prove a limit.
progress:
NewTraceContextmakes the trace context of a trace that begins here, andTraceContext.Traceparentwrites it for a child’sTRACEPARENT.
progress/cliprogress:
Options.DefaultLognames an event log the program keeps unasked; it fails no command and writes no note.Options.RotateAtmoves it to its name with “.1” after it once it is larger than that.IsTerminaltells whether a file is a terminal.ThemeFordraws a theme in the colours the terminal shows, from NO_COLOR, TERM and COLORTERM as the program read them.Askasks a question with the displays off the terminal until the answer comes;Yesreads “y” or “yes”.
progress/mcpprogress (new):
Notifier, a sink, sends a tool call’s progress as MCP progress notifications through aSendthe server gives it: the targets ended, of how many, and a line, “probe the nodes: 3/6 done”, at most every 500 ms, never going back, and nothing afterClose. The package imports no MCP SDK.
Requirements:
- None change.
v0.5.0#
2026-10-22on GitHubon pkg.go.dev
go-clikit v0.5.0
progress.LogOptions has a new field, FlushEvery, that sets how long a
line of the event log waits before the log writes it. A program whose log a
live view follows, such as a web page of the run, can now keep that view a
few frames behind instead of a second.
Nothing that exists changes: the default stays a second. This is a minor release because it adds API.
Changes#
progress:
LogOptions.FlushEveryis the most time a line waits before the log writes it. 0 is a second, as before; a negative value writes each line as it comes.- The wait runs from the first line kept, not from a free-running ticker, so
a line waits
FlushEveryat most, and an idle log wakes no goroutine. doc/event-log.mddescribes it.
Requirements:
- None change.
v0.4.0#
2026-10-21on GitHubon pkg.go.dev
go-clikit v0.4.0
A new package, cobratree, holds the plumbing around cobra that sind and
clusterctl each carried a copy of. It makes a command tree strict for
scripts, marks the errors of a command line so a program can exit with its
own code, and reads the build info the toolchain recorded.
Nothing that exists changes, so this is a minor release because it adds a
package. The module now requires cobra and pflag, which only cobratree
imports; a program that imports none of it does not build them.
Changes#
cobratree (new):
Strictadds help and completion to a built tree. A command that only holds subcommands prints its help bare and refuses a word that names none of them, help refuses a topic that names no command, and every argument check returns a usage error that ends with the command’s usage line. An unknown command’s error suggests the closest subcommand, or names the command that replaced one the program dropped (decision 24).Usage,UsagefandIsUsagemark and tell the errors of a command line, pflag’s typed errors among them. The package names no exit code.BuildInfoandReadBuildInfogive the module version, the revision, its time and whether the checkout had changes, as data the program words itself.- The texts of the errors
Strict’s checks return are an interface beyond the Go API: changing one is a breaking change.
Requirements:
- Requires
github.com/spf13/cobrav1.10.2 andgithub.com/spf13/pflagv1.0.9, withgithub.com/inconshreveable/mousetrapindirect. golang.org/x/sysgoes from v0.48.0 to v0.49.0 andgolang.org/x/textfrom v0.42.0 to v0.43.0.
v0.3.0#
2026-10-10on GitHubon pkg.go.dev
go-clikit v0.3.0
A span can now say how far its work has got, and the displays draw it.
Work in items, bytes or a percentage, reported by a library, a copy or a
pool of targets, is rolled up the tree and drawn as an amount, a share,
a rate and the time left. The displays can draw in five themes the
program picks. The live clocks tick in tenths of a second. A new package,
progress/cliprogress, holds the glue between a command line and the
progress packages, which clusterctl and sind each carried a copy of until
now.
The text the live tree and the counter draw with the zero Theme
changes for every program, including one that reports no work. That is a
breaking change, which v0 allows in a minor release, so the minor version
goes up. The new API and the new package progress/cliprogress would
make this a minor release in any case.
Changes a program or its tests may notice#
progress/display:
- The live clocks read tenths of a second, cut rather than rounded
(decision 18). The command’s time on the counter’s line, on the tree’s
command row and on the counter line the tree falls back to on a small
terminal reads
0:41.3or1:02:03.4, where it read0:41or1:02:03. How long a running target, step, call or wait has run reads4.2sor3m12.4s, against a request’s bound1.0s/10m, and from an hour on1h02m, as before. A test that compares frames sees0:41.0and4.0swhere it saw0:41and4s. What is not a live clock stays as it was: the[0:03]in front of a plain line, how long a span that has ended took (✓ powering on 2.0s,done in 25s, the summary), a pause’s countdown (20s left, rounded up) and request bounds (/10m). - The tree lists running targets by the tenth of a second each started in, counted from when the tree was made on the monotonic clock, those that started in the same tenth in the order they were queued (decision 18). It used to sort them by the whole seconds each had run at the time of the frame. Targets that start within the same second can now be listed in another order: four queued exe1 to exe4 that a pool starts in the order exe4, exe2, exe1, exe3, 0.20 s, 0.41 s, 0.45 s and 0.55 s in, are listed exe4, exe1, exe2, exe3 (exe1 and exe2 started in the same tenth, so they keep their queue order), where v0.2.0, at a frame 2.0 s in, listed them exe1, exe2, exe3, exe4. A row no longer changes places from one frame to the next.
- While anything runs, every frame differs from the one before, so the
tree and the counter write to the terminal up to ten times a second
rather than about once: about 12 KB a second for a region of 12 rows
of 100 columns. A test that drives a display from a ticker on the real
clock sees a line at every tick,
0:01.0and then0:01.1, where v0.2.0 drew0:01once. A frame drawn again within the same tenth still writes nothing. - A clock that steps back reads
0:00.0rather than a negative time such as-60:00. - Nothing else changes in what a program that reports no work draws with
the zero
Theme: plain lines and the summary are byte for byte as in v0.2.0. TheTerminalcuts a row that holds colour sequences by the columns it shows, but cuts a row without one, which is every row drawn with the zeroTheme, withtermtext.Truncateas before.
progress:
- The event log stays at version 1 and gains the type
advanceand the keysamount,size,unitandleftOut(decision 23). It writes an advance of a span at most once a second by default, soseqis no longer gapless: the advances it leaves out leave gaps, whichleftOutcounts. A reader of the log that checksseqfor gaps, or refuses an unknown type, key or unit, must allow these before the programs it reads report work; a program can set a negativeLogOptions.AdvanceEveryto write every advance. The log of a program that reports no work is byte for byte what v0.2.0 wrote.Event.Seqon the Bus still has no gaps. BusOptions.Nowmust be safe for concurrent use once a span reports work:AdvanceandSetAmountread it on the caller’s goroutine, outside the Bus’s lock. A test clock moved by hand without a mutex of its own races under-raceas soon as a span callsAdvance,SetAmount,CountWriterorCountReader.- An advance that comes too soon after the one before is sent later from
a timer’s goroutine, still under the Bus’s lock and in
Seqorder, so a sink’sHandlemay run on a goroutine the program did not start, up to 100 ms after the call. Spans that report no work start no timers.
progress/progresstest:
Checkenforces a new rule 7, the work of a span: an advance names a span that is running and changes itsAmount; two advances of a span come at least 100 ms apart by their times;AmountandSizeare never negative; a span inPercenthas aSizeof 100; and a span’sUnitnever changes once given. Decision 15 makes a new rule a breaking change. This one applies only to the event and fields this release adds, which no source of events sent before, so a stream that passedCheckunder v0.2.0 passes it still (decision 23). A hand-made event of type 8, nowTypeAdvance, is no longer reported as having no valid type; rule 7 checks it instead, and refuses one that does not change its span’sAmountor names a span that is not running.- A
Capture, and the one behindWatch, keeps every advance the Bus sends, up to one a span every 100 ms; a test that counts events or compares their types sees them once its spans report work. Treedraws a span’s work after its message and before its flags, asamount=2147483648 size=2147483648 unit=bytes, each left out when zero, from the span’s newest event, advances included. A tree of spans without work is unchanged, byte for byte.
fanout, progress and progress/display:
fanout.Itemgains the fieldsUnitandSize,progress.FieldsgainsAmount,SizeandUnit,progress.LogOptionsgainsAdvanceEvery, anddisplay.TreeOptions,CounterOptionsandPlainOptionsgainTheme, inserted among their fields. Code that builds any of these with an unkeyed composite literal no longer compiles.
New#
progress (decision 23):
Work(unit, size), an option ofStart,UpdateandEnd, gives a span’s work in aUnit,Items,BytesorPercent, out of a size; a size of 0 or less is not known, and the size of work inPercentis always 100. A unit once given stays: a laterWorkchanges the size alone.Span.Advanceadds to the amount andSpan.SetAmountsets it, for work that reports how far it has got; the amount may go down. Both do nothing on a nil span or one that is not running, and cost an atomic operation and a clock read; the Bus’s lock is taken at most once every 100 ms a span. A span given noWorkthat advances countsItems.TypeAdvance: the Bus sends a span’s newest amount at most every 100 ms, by its clock. The first goes out at once, the newest is never lost, and the span’sEndcarries the final amount.Fields, and soEvent, gainAmount,SizeandUnit: the span’s own work, never that of the spans below.CountWriterandCountReaderadvance the innermost span in a context by the bytes each write or read moved. They return the writer or reader itself, at no cost, when the context carries no span. They hide theio.ReaderFromof the writer and theio.WriterToof the reader they wrap, soio.Copycannot use those fast paths through them; the other side’s still apply.Meter,ReadingandBoundroll the work of a Bus’s spans up the tree for a sink. Amounts add within one unit. A span’s share done comes from its own size, else from the targets of a Fold step, or of a batch whose Total is known, once one of them is bounded, short of done until their count is complete, else from the sizes of the work below. AReadingalso gives the rate over the last five seconds and, once a span has run two seconds, the time left over the same window, and says when the work has stalled for five seconds. The zeroMeteris ready to use, and not safe for concurrent use.Readingmay gain fields, andUnitvalues, in a minor release.LogOptions.AdvanceEvery: the least time between two advances of a span the log writes. 0 means a second, and a negative value writes them all.- Without a Bus, the new calls allocate nothing, which tests hold them to.
progress/display:
- Themes (decisions 19 and 20).
Themeis a comparable value whose zero value is no theme and draws what v0.2.0 drew. There are five themes,Classic,Aurora,Ember,NeonandTide.Themeslists them,ParseThemereads a name in any case,nonefor the zeroTheme, andString,MarshalTextandUnmarshalTextlet aThemeback a--themeflag throughflag.TextVar.Theme.Indraws a theme in its 256 colours, the terminal’s own 16, or none, its art alone, forNO_COLOR;Theme.Colourssays which. The kit reads no environment: the program decides from its flags,NO_COLOR,TERM,COLORTERMand whether standard error is a terminal. TreeOptions,CounterOptionsandPlainOptionstake aTheme, andSummaryaThemeandASCII. The tree and the counter draw a theme’s marks, and, where the theme has them, a spinner on each running target and in front of the counter’s line, a bar on counted steps that gives way when the row would not fit, and a guide at the innermost indent;Classic’s running mark stands still, and it draws no bar or guide. Plain lines take a theme’s colours and one mark after the time, and the summary one mark in front, and both keep their words, so a log reads the same once the colours and marks are taken out. With a theme,ASCIIkeeps its colours and draws ASCII marks, a[###...]bar, and no spinner or guide. Every painted piece sets its colour back, and a frame drawn in colour starts with ESC[0m.- The
Terminalcuts a row that sets colours by the columns it shows: a sequence takes no column and is never split, and a row cut short that held one ends with ESC[0m. - Every display draws the work spans report, rolled up by a
Meter(decision 23). The tree draws it on a step’s or batch’s row after its counts, or after its name, and on a running target’s or call’s row after its name, as amount out of size, share, rate and time left:1.1/2.0 GiB · 56% · 80.0 MiB/s · ~12s left. Work inPercentreads as its share and time left,40% · ~30s left, and work of no known size as its amount and rate,48.2k · 6.6k/s. Once work has not grown for five seconds,stalled 6.1stakes the rate’s place. The line a step with work leaves adds what it came to and its mean rate,✓ fetching the base image 25s 2.0 GiB at 80.0 MiB/s, but for work inPercent. A row too wide for the terminal gives up the time left, then the rate, then the amount, then a theme’s bar. - The counter draws the work of a counted step after its counts, or that
of the newest named step after its name, as in
fetching the base image · 1.1/2.0 GiB · 56% · 80.0 MiB/s · ~12s left; the command’s own work is never drawn. - Plain lines say every ten seconds how far each step or batch with work
has got, unless a counted span is above it, as in
fetching the base image: 0.8/2.0 GiB, 39%, 80.0 MiB/s, ~16s left, and a step’s end line adds what its work came to,done in 25s: 2.0 GiB at 80.0 MiB/s. - The summary adds the command’s amount and mean rate,
fetch: done in 25s: 2.0 GiB at 80.0 MiB/s, when all its work is in one unit other thanPercent. - The forms of work are part of the text drawn with the zero
Theme: bytes in IEC units (512 B,85.3 MiB), items whole below 10,000 and then48.2k,1.2M, a share cut to a whole percentage that never reads 100% short of done, and a time left rounded up, behind~so it is not read as a pause’s countdown. Spans without work draw as before, and nothing in the kit reports work on a program’s behalf: a program sees these only once it, or a library it imports, passesWork, callsAdvanceorSetAmount, copies throughCountWriterorCountReader, or setsfanout.Item.Unit.
progress/progresstest (decision 19):
Screen.Stylesmakes aScreenapply the colours and attributes a theme draws with, as a terminal does: bold, faint, underline, reverse, their resets, colours30–37,90–97and38;5;n, and0. They take no column, andStringshows the text alone. Any other sequence, such as italic, a background or 24-bit colour, still shows as text. WithoutStyles, aScreenbehaves as before.Screen.Styledshows each run of text with its attributes, as in«1;31»✗«» exe0007.
fanout:
Item.UnitandItem.Size:Mapstarts the target of an item that sets aUnitwithprogress.Work(Unit, Size), so a display shows its share before it runs, and the work moves it withAdvance,SetAmountorCountWriter. A zeroUnitis no work, even with aSize, unlikeprogress.Work, where it isItems;Mapthen does what it did.
progress/cliprogress, released for the first time (decisions 21 and 22):
- The command line’s side of progress, which clusterctl and sind each
carried as glue of their own: the words of
--progress,auto,tty,counter,plainandnone(Mode,Modes);Choose, which picks a display from the program’s flag and the variable that stands in for it, given together in aSetting(Options.Mode, andOptions.Logfor the event log), and what the program found out about its streams;Start, which makes the Bus, the display, the summary and the event log of a command, andRun.Finish, which takes them down in order and writes the summary. - What the flag asks for and cannot have is an error, which the program
reports as a usage error; what the variable asks for fails no command
and gives a note where it names no word or no log that can be used.
With neither a display nor a log,
Startmakes no Bus, so standard error holds what it would without the kit. - The event log is appended to a file this user alone may read, created
with mode 0600. An existing regular file must be this user’s, under no
other name, and readable or writable by no one else, its group included
(mode 0600 or narrower); a symbolic link on the way to it, a
directory’s included, and a named pipe must be this user’s or root’s.
/dev/stderr,/dev/fd/Nand the like are written through a duplicate of the process’s descriptor, and a log on the display’s own standard error is written above the display. IsPipe,TerminalSizeandInForegroundask the system, andUTF8Localereads the getenv the program passes. The package reads no environment, sets nothing process-wide, ends no process, imports no package of flags and exports no error.
Stability#
The interfaces beyond the Go API grow, and changing any of these is a breaking change:
- the words
cliprogress.Modesnames, in their order, and the texts of the errors and notescliprogressreturns and writes, the refusals of the log’s file included (decision 21); adding a word is not breaking; - the forms in which the displays draw work with the zero
Theme, as part of the text consumers compare (decisions 5 and 23); - a theme’s name and the words and numbers it draws: removing or renaming a theme is breaking, and adding one is not (decision 20).
A theme’s colours, marks, separators, spinner, bar and guides may change
in a minor release, which names the change (decision 20). A test of a
display drawn in a theme should check its words and numbers, through
progresstest.Screen, rather than its frames byte for byte, or pin the
release.
Known limitations#
progress/cliprogresson systems that are not Unix builds but checks less: a display takes the terminal to be 80 columns wide, the process is taken to be in the foreground, and the log’s file is not checked for its owner, mode or links.- On AIX, a descriptor
cliprogressduplicates for the event log is inherited by a child started while the log is open.
Requirements#
- Go 1.26 or newer, as before.
golang.org/x/sysv0.48.0 is new (decision 22), the version sind and clusterctl already require. Onlyprogress/cliprogressimports it, in its files for Unix, and depguard keeps it there; a program that builds no package ofcliprogresslinks none of it, but has it in its module graph.github.com/GSI-HPC/go-nodesetgoes from v1.0.0 to v1.1.0, which programs that take this release take too; see its own notes. The kit parses, folds, splits and expands node sets with it, andfanout.Batchestakes a*nodeset.NodeSet. v1.1.0’sParserefuses a range with no last bound, such asexe[1-], which v1.0.0 read asexe1.golang.org/x/textstays at v0.42.0.
Upgrading#
- A test that compares the frames of the tree or the counter needs new expectations for the tenths, and for the order of running targets that start within the same second.
- A program that adopts work, or imports a library that does: give the
Bus a clock safe for concurrent use, expect
advanceevents and the work fields in what its tests capture and in the frames they compare, let its own sinks takeTypeAdvance, which may come from a timer’s goroutine, and make sure the readers of its event log takeadvance, the new keys and the gaps inseq. - A program that moves its glue to
cliprogresskeeps the lines that fill inOptions. Finding out whether a stream is a terminal, reading the environment, the theme and its colours, signals, exit codes and whether a command that succeeded leaves a summary stay the program’s. Where clusterctl and sind differed, the kit takes sind’s way, so clusterctl’s log refuses another user’s symbolic link on the way to it and is written above the display on its own terminal. Once moved, both refuse a log’s file that has other hard links, which their own glue appended to.
API reference: https://pkg.go.dev/github.com/GSI-HPC/go-clikit
v0.2.0#
2026-10-04on GitHubon pkg.go.dev
go-clikit v0.2.0
The live tree costs a fraction of what it did on steps of tens of thousands of targets. It handles every event and draws every frame under a lock that the Bus waits for, and every worker that reports an event waits for the Bus, so on such steps the tree slowed the work itself.
Changes a program or its tests may notice#
progress/display:
- The live tree folds a set of more than 256 targets again at most once a second, rather than at every frame, so such a row may leave out the targets that ended in the last second. The count in the step’s row, and the line a step leaves when it ends, stay exact. A test that draws frames less than a second apart over more than 256 targets sees the earlier fold (decision 16). This changes the text the tree draws, so the minor version goes up.
Faster#
On one machine, best of three runs:
| v0.1.0 | v0.2.0 | |
|---|---|---|
| Ending 16,000 targets | 489 ms | 98 ms |
| A run that draws a frame every 100 events, 16,000 targets | 1.64 s | 0.29 s |
| The same, 30,000 targets | 6.0 s | 0.83 s |
| A frame while 16,000 targets run | 5.8 ms | 0.72 ms |
| A frame while targets end, after 16,000 ended well | 3.8 ms | 13 µs |
- A target that ends leaves its step in constant time, rather than with a pass over the step’s other targets.
- A failure finds its row by its text, and a batch that ends adds its targets to its step’s in place rather than copying the step’s set.
- A frame builds only the rows of a list that fit the region, rather than one for every running target.
- The tree counts each event once, rather than a second time for the counter line it draws on a terminal too small for it.
The Go API, the counter, the plain lines and the event log are unchanged.
v0.1.0#
2026-10-03on GitHubon pkg.go.dev
go-clikit v0.1.0
The first release of go-clikit, the runtime that GSI-HPC’s command-line
tools share. A library reports what it does as spans carried in a
context.Context, at no cost when no command is listening, and runs work
on many targets in bounded pools; the command decides how that is shown:
a live tree or a counter on a terminal, plain lines for a CI log, or a
JSONL event log for programs.
The packages were written for clusterctl, where they shipped in v0.4.0
under internal/, and moved here with their history.
Packages#
termtext: escaping untrusted text for a terminal, and the columns text takes there.progress: spans in acontext.Context, theBusthat orders their events, the JSONL event log and the W3C trace context.progress/display: the live tree, the counter, plain lines, the summary line, and theTerminalthey share with the command’s own output.progress/progresstest: capturing the events a command reports, checking them against the contract every source of events keeps, andScreen, a terminal for tests.fanout:Each,MapandBatches, bounded pools that report their items as progress.
API reference: https://pkg.go.dev/github.com/GSI-HPC/go-clikit
Requirements#
- Go 1.26 or newer. The module requires the oldest Go release the Go project still supports, and follows it up after each Go release.
github.com/GSI-HPC/go-nodesetv1.0.0 andgolang.org/x/textv0.42.0, and nothing else.- Licensed under Apache-2.0.
Stability#
The module is at v0: a minor release may change the API, and its notes say how. Beyond the Go API, four things are interfaces, and changing any of them is a breaking change:
- the six span kinds: command, step, batch, target, call and wait;
- the JSONL event log, version 1, described in
doc/event-log.md: keys may be added, never renamed or removed; - the text the displays draw, which programs compare in their tests,
but for the characters
termtextescapes, which may grow in a minor release (decision 14); - the rules
progresstest.Checkenforces: a new or stricter rule is a breaking change (decision 15).
Coming from clusterctl v0.4.0#
Rewrite the imports:
| clusterctl v0.4.0 | go-clikit v0.1.0 |
|---|---|
github.com/GSI-HPC/clusterctl/internal/termtext | github.com/GSI-HPC/go-clikit/termtext |
github.com/GSI-HPC/clusterctl/internal/progress | github.com/GSI-HPC/go-clikit/progress |
github.com/GSI-HPC/clusterctl/internal/progress/display | github.com/GSI-HPC/go-clikit/progress/display |
github.com/GSI-HPC/clusterctl/internal/progress/progresstest | github.com/GSI-HPC/go-clikit/progress/progresstest |
github.com/GSI-HPC/clusterctl/internal/clikit/fanout | github.com/GSI-HPC/go-clikit/fanout |
github.com/GSI-HPC/clusterctl/nodeset | github.com/GSI-HPC/go-nodeset |
fanout.Batches and fanout.Batch take go-nodeset’s *nodeset.NodeSet,
so a program that passes node sets to them moves to go-nodeset as well.
The event log writes the same version 1 lines, and the span kinds are the same. The API was reviewed before this release for what an internal package can afford and a public one cannot, and reshaped:
| clusterctl v0.4.0 | go-clikit v0.1.0 |
|---|---|
termtext.EscapeCell, termtext.EscapeText | termtext.Escape, termtext.EscapeLines; the escaped bytes are the same |
termtext.RuneWidth(r) | unexported; termtext.Width(string(r)) |
progress.Options | progress.BusOptions |
Options.Trace, Parent, TraceFlags, TraceState | BusOptions.Trace, the TraceContext that ParseTraceContext returns |
TraceSink.Begin(TraceContext) | TraceSink.Begin(Run); the log takes the program’s name from the Bus |
LogOptions.FlushEvery, LogOptions.CloseWait, Log.WantsLines | removed |
Count.Targets | Count.Started |
fanout.Skip, fanout.IsSkipped(err) | progress.Skip, errors.Is(err, progress.ErrSkipped); Span.End ends a span skipped for it |
fanout.Options | fanout.MapOptions |
fanout.DefaultMax | fanout.DefaultLimit |
outcomes := fanout.Map(…) | outcomes, err := fanout.Map(…), the error the step ended with |
Describe func(T) (node, host, role string) | Describe func(T) fanout.Item |
Summarize func(n int, names []string, errs []error, interrupted bool) error, fanout.Failure(noun, n, names, errs, interrupted) | Summarize func(fanout.Summary) error, fanout.Failure(noun, s); Summary.Canceled |
BatchOptions.After | removed; test the pause in a testing/synctest bubble |
Batch.Ran | Batch.Started |
PanicError.Value string | PanicError.Value any, and PanicError.Target |
display.NewTerminal(w, size) and the fields PanicLog, Program, Foreground | display.NewTerminal(w, display.TerminalOptions{…}) |
progresstest.Checked | removed; progresstest.Watch |
ctx, tree := progresstest.Watch(…); tree() | ctx, w := progresstest.Watch(…); w.Finish(), with w.Events() and w.Tree() |
progresstest.Option, a func(*progress.Options) | opaque; progresstest.Classify and the new progresstest.Sinks |
Added: Bus.Program, Bus.PanicLog and Bus.Classify, which answer for
a nil Bus too; progress.Run; progress.ErrSkipped and progress.Skip;
fanout.Item, fanout.Summary and fanout.Failed; MapOptions.Noun;
display.TerminalOptions; progresstest.Watcher and progresstest.Sinks;
progress.Log.Done.
The behaviour differs where the review asked for it or found bugs, as listed below.
Changes a program or its tests may notice#
termtext:
WidthandTruncatecount a character of one column followed by the emoji variation selector U+FE0F as two columns, as terminals draw it, so a red heart or a keycap measures 2, not 1.- The soft hyphen U+00AD and the prepended concatenation marks, such as U+0600, take one column, not none.
- Characters made wide after Unicode 15.0, such as U+1FAE9, take two
columns: the widths follow Unicode 18.0, where
golang.org/x/textknows 15.0. U+1171E, a spacing mark since Unicode 16.0, takes one. - Text holding any of these measures wider, and
Truncatecuts it shorter.
progress:
Classifytakes a fallback’sClassNonefor an error asClassTarget, so a span no longer ends “failed (none)”.- The event log names the program from
BusOptions.ProgramwhenLogOptions.Programis empty.
progress/display:
- Plain counts “1 target” and “480 targets” when
PlainOptions.Nounis nil, not hosts. A program that names its targets is unchanged. Startstarts a display once, and never afterClose. A Terminal carries one display, and a second panics.- Plain lines and the counter no longer print an empty part for a step
with no name, as
fanout.Mapgiven noStepreports: its lines name the span above it, and a plain line with nothing to name it reads “start, 2 hosts”, not “: start, 2 hosts”. - In the tree, a target’s name is read as
{}in an error only where it stands alone. A name inside a word or an address is left as it is, so a target named “e” no longer turns “timeout” into “tim{}out”; names beside “-” and “_” are read as before. - The summary counts the nodes of a batch left out once, not again on top of the targets already counted.
- A time from 9.95 s up to 10 s reads “10s”, not “10.0s”.
- A write of the command’s that ends a line and then resets the colours no longer leaves the line open, which used to keep the display off the terminal and hold the lines written beside it.
- A display that panics takes its region off at once, and writes the lines it held before its stack.
fanout:
Maptakes the program’s name, its panic log and its class rule from the Bus in its context whenMapOptionsleaves them empty.- A batch whose work returns a skip ends skipped, and
Batchesgoes on with the next one rather than stopping. Eachgiven a limit below one runsDefaultLimitat a time, asMapdoes, not one.Mapends the pool under a deadline as it does under an interrupt: the items it left out, and those whose work failed once the context had ended, end their targets canceled rather than failed as timeouts, and the step ends canceled when every item that failed ended so. The outcomes keep the context’s error; tell a deadline from an interrupt byctx.Err().- An item
Options.Acquirerefused ends its target at once, rather than once the whole pool has finished: skipped for an error ofSkip, canceled when the context had ended, and with the refusal otherwise. Its outcome keeps the refusal even when the context ends later. - The release function
Acquirereturned runs before the item’s target ends, so the next item can be reported running before that target’s end. - A panic in the release fails the item, with its stack in
Options.PanicLog, instead of ending the process. - A panic in the work or in
Acquirefails its item even after the context has ended, rather than passing for a cancellation. - Work,
Acquireor a release that callsruntime.Goexit, ast.FailNowdoes, fails its item with an error that names which of the three it was, instead of leaving it reported as done. Failurelists names that repeat as given, so that the list names as many items as the count, and takes time linear in the number of names: an interrupt of a pool over 16000 hosts no longer holds the command for seconds.Recoveredescapes the target it names.
progress/progresstest:
Watchasks for the lines of output, soCheckalso checks their texts, and it checks the events at the test’s cleanup whenFinishis not called.Treewrites a target’s name as{}only where it stands alone, not inside other words.Treelists the names of targets that name a node twice, alone or in another target’s node set, rather than folding them into one, so a target that ran twice shows.Treeorders siblings that differ only in leading zeros, “x01” and “x1”, the same way on every run.Checkholds the batches of every step to being announced queued before the first runs, not only those under a Fold step.Screenwraps a full row at the next rune, as a terminal does; blanks the other half of a wide rune written over; and shows an escape or sequence it cannot complete as text, rather than hiding it and all that follows.
Other fixes#
- A sink of a type that cannot be compared, such as a func type, that panicked made the Bus panic in the caller, and could leave it locked for good. Such a sink is now taken off like any other.
- A display taken off the Bus for panicking while suspended is resumed, so its terminal no longer holds the command’s lines.
Log.Closestarts no write once it has stopped waiting, and the newLog.Donesays when the last write has returned, so that the writer can be closed safely.- A log that falls 8 MiB behind keeps the lines it had already taken.
- A
PanicLogthat is one of the Terminal’s own writers no longer deadlocks the program once a display panics. Tallycounts a nested step left out once, so a count no longer passes its total.- At the end of a span, the unfinished last line of each stream is sent, not only one of them.
Teeand theLineswriter take linear time in a long line, and a span that runs many commands no longer keeps a tee for each.- The Bus asks each sink for lines once, and takes off one that panics when asked.
- A tracestate with tabs around its commas, as W3C allows, is kept.
Known limitations#
- Escapes are for a reader, not for decoding: a literal backslash is
written as it is, so text holding
\x1breads the same as an escaped ESC (doc/decisions.md, decision 7). - Widths are counted a rune at a time, save U+FE0F, and err wide: a sequence joined with U+200D, a skin tone modifier or a flag is counted as the sum of its runes. They can fall short where a terminal draws U+FE0F narrow, and for a wide character assigned after Unicode 18.0 (decision 8).
- On a terminal that reflows its rows and is made narrower under the tree, the first lines of the rows that no longer fit stay above it until they scroll away (decision 9).
- The lines of runs that append to one event log stay whole only on a
local file opened with
O_APPEND.
Documentation#
How the kit is built and why is in doc/: architecture.md,
event-log.md, testing.md and decisions.md.