❯ go-clikit
v0.10.0 pkg.go.dev ↗

reference › releases

Releases

Each release of the kit, newest first, with its notes. A breaking change in a v0 minor release is named here.

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 now Strict(root, opts ...Option). Change Strict(root, m) to Strict(root, cobratree.Moved(m)). Strict(root, nil) still compiles and works; you can write Strict(root).
  • A missing required flag and an error of a flag group (required together, one required, mutually exclusive) are now usage errors: IsUsage returns true. A program that exits with 2 for a usage error now exits with 2 for these too.
  • Strict checks required flags and flag groups with the arguments, before the PersistentPreRun and PreRun hooks. 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 before Execute.
  • A runnable root with subcommands and no Args now refuses a word that names no command, with the text of Strict and a usage error. This includes a word after --, which cobra gave to the root. Give the root an Args check if it must take words.
  • Coded(0, err) and Coded(-1, err) no longer give a status, so Status returns its default. Use a status of 1 or more.
  • Strict panics on a tree whose help already shows topics, from HelpTopics or from an earlier Strict with Topics. HelpTopics panics on a tree that has no help command. Topics(nil, ...) panics. Pass Topics to one call of Strict.

localweb:

  • Hosts(answers, refuse) is now Hosts(answers, HostsOptions{Refuse: refuse}). Change Hosts(a, nil) to Hosts(a, localweb.HostsOptions{}). Hosts(nil, ...) now panics when the middleware is made, not on the first request.
  • PeerListener returns an error for a listener that is not TCP. Before, it returned a listener that closed every connection.

localweb/session:

  • New returns an error for a negative MaxBody, for a CookiePrefix that does not make a valid cookie name, and for a CSRFHeader that is not a valid header name. Use 0 for the default MaxBody.

localweb/sse:

  • With Options.Done nil, Serve ends the stream when localweb.Serve starts to stop. Before, the stream held up the stop until its grace period ended. A program that sets Done keeps its own signal, and the stream does not watch the server.
  • A negative Retry sends no retry field; before, it sent the negative value. A negative KeepAlive turns keep-alives off; before, it panicked. A negative Coalesce renders each change at once.
  • Format drops CR and LF from Event.Name. Serve still refuses such a name.

localweb/localwebtest:

  • Login follows the redirect and fails the test when the page after it does not answer 2xx.

localweb/launch:

  • Acquire(runtimeDir, program) is now Acquire(ctx, runtimeDir, program), and (*Instance).Retire() is now Retire(ctx). Pass the context of the command. They wait for the lock until ctx ends and then return an error that wraps ctx.Err(); before, they waited with no limit. A free lock is still taken after ctx ended, so Retire can use the context that stopped the server.

localweb/page and localweb/htmx:

  • htmx.Config has the new key "historyRestoreAsHxRequest": false. A test that pins the configuration must add it.
  • IsHTMX returns false for a request with HX-History-Restore-Request: true. So Redirect answers a history restore with 303, not with 204 and HX-Redirect, which the htmx history loader cannot follow.
  • Set.Render and Set.Respond return an error when include nests more than MaxIncludeDepth (100) deep. Before, such a template stopped the process.

termtext and cliout:

  • EscapeLines and 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, and Escape escapes every CR. This changes the output of text with CRLF line ends, for example a cliout.Table.Caption or a doctor remediation.
  • NewEscapeWriter returns *termtext.EscapeWriter, not io.WriteCloser. A variable that holds the result still compiles. A func value of type func(io.Writer) io.WriteCloser does not; wrap the call in a func literal.
  • cliout.Table.Add copies its cells. To change a row after Add, change Table.Rows.

fanout:

  • Batches returns ([]Batch, error), the error its step ends with. Change b := fanout.Batches(...) to b, err := fanout.Batches(...).
  • MapOptions.Noun is now func(n int) string, not string. The function returns the count with the noun, such as “480 hosts”, as display.PlainOptions.Noun and cliprogress.Options.Noun do. Change Noun: "hosts" to a function such as Noun: func(n int) string { return fmt.Sprintf("%d hosts", n) }.
  • A negative BatchOptions.ReportLimit (or Limit) records no limit on the batch span. Before, the negative value was recorded.

progress:

  • TraceContext.Trace is now TraceContext.ID. A struct field cannot keep an alias. Change tc.Trace to tc.ID.
  • Fields.Exit is now int, not *int, and the new HasExit says that there is a status. Change if f.Exit != nil { use(*f.Exit) } to if f.HasExit { use(f.Exit) }.
  • Span.Update applies every field option. Before, it applied only Total, Message and Work. Total never goes down, and the unit does not change.

progress/cliprogress:

  • The probes IsTerminal, IsPipe, TerminalSize and InForeground take any. A call still compiles. A variable of the old func type does not; change its type.

progress/eventlog:

  • The method Feed.Line is now Feed.Take. A Feed that a program writes must rename its method. Reducer.Line stays as a deprecated wrapper.
  • Decode returns a *VersionError, which matches ErrVersion, for a line of another version of the log. Use errors.Is(err, eventlog.ErrVersion) to tell it from other errors.
  • The deprecated Forward gives the sink only the first run that begins. Before, it gave every run. Use NewForwarder(sink, trace), which forwards the run of the trace and counts the others in Dropped.

progress/mcpprogress:

  • Options.Send takes a context first: func(ctx, progress, total, message) error. Notifier.Close() is now Close(ctx) error. Pass the context of the tool call, and give ctx on to the server’s send.
  • New panics on a nil Send.

progress/display:

  • Colors.String writes “Colors(n)”, not “Colours(n)”, for a value that is not a constant.
  • An assignment to Classic, Aurora, Ember, Neon or Tide changes only that variable. Themes and ParseTheme no 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.

PackageOld nameNew name
cobratreeResolve(version, commit)ReadBuildInfo().Resolve(version, commit)
cobratreeHelpTopics(root, lookup)Strict(root, Topics(lookup, names))
cobratree/cobratreetestCheckCommandLineValidateCommandLine
localwebOriginOriginPolicy
localweb/sessionSessionsManager
localweb/sessionOptions.CookieOptions.CookiePrefix
localweb/sessionOptions.HeaderOptions.CSRFHeader
localweb/sessionOptions.FieldOptions.CSRFField
localweb/sessionDefaultFieldDefaultCSRFField
localweb/assetsAssetsSet
localweb/htmxVersionScriptVersion
localweb/htmxIdiomorphVersionMorphVersion
localweb/pageOptions.ReloadOptions.Dev
fanoutBatchOptions.LimitBatchOptions.ReportLimit
fanoutFailedFailedItem
fanoutItemItemInfo
fanoutFailure(noun, s)&FailureError{Summary: s, Noun: f}, only when an item failed
progressItems, Bytes, PercentUnitItems, UnitBytes, UnitPercent
progressStdout, StderrStreamStdout, StreamStderr
progressHidden, Fold, ShowLines, DryRunFlagHidden, FlagFold, FlagShowLines, FlagDryRun
progressUnbounded, BySize, ByTargets, ByBelowBoundNone, BoundSize, BoundTargets, BoundChildren
progressWithFlagsFlagged
progressHTTPHTTPRequest
progressRunRunInfo
progressTraceSinkRunSink
progressBus.TraceBus.TraceID
progressLogOptions.RunLogOptions.RunID
progressCountWriter, CountReaderAdvanceWriter, AdvanceReader
progress/cliprogressRunSession
progress/eventlogLine.RunLine.RunID
progress/eventlogOptionsReducerOptions
progress/eventlogReducerOptions.KeepReducerOptions.KeepRuns
progress/eventlogReducer.LineReducer.Take
progress/eventlogForward(sink)NewForwarder(sink, trace)
progress/displayColoursColors
progress/displayColours256, Colours16, NoColoursColors256, Colors16, ColorsNone
progress/displayTheme.Colours()Theme.Colors()
progress/display&Summary{Theme: t, ASCII: a}NewSummary(SummaryOptions{Theme: t, ASCII: a})

Changes#

fsutil (new):

  • WriteAtomic writes 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.
  • Lock takes an exclusive lock of a file through the lock file .NAME.lock beside it, the name clusterctl uses, so that an older and a newer build exclude each other. It waits until it holds the lock, until ctx ends, or until the wait passes. DefaultLockWait is 30 seconds. On Linux, the error after the wait names the process that holds the lock.
  • Update reads a file, changes it with a function and writes it back atomically, under the lock.
  • EnsureDir makes a directory for the user alone, and refuses one that the group or others can write. It does not change the mode.
  • CheckTrusted refuses a path that a user other than this one or root could have written, with an error that matches ErrUntrusted.
  • Lock and EnsureDir return an error that matches errors.ErrUnsupported on a system that is not Unix. On Windows, CheckTrusted refuses every path.

doctor (new):

  • Check holds the name, the status, a detail and a remediation of one check. Status is “ok”, “warning” or “failed” (StatusOK, StatusWarning, StatusFailed). The words, the JSON keys and the text of Write are interfaces (decision 38).
  • Worst gives the worst status, and Count counts the failed and the warned checks. A status that is not one of the three counts as failed.
  • Write writes 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:

  • Strict takes the options Moved and Topics. Topics adds help topics after the checks of Strict, and their names complete after help.
  • Strict checks required flags and flag groups on every runnable command, and a runnable root with subcommands is as strict as a group. The doc says that Strict makes a group runnable, which adds a line to its usage.
  • BuildInfo.Resolve is 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:

  • ValidateCommandLine gives the words under a runnable group to the group’s argument check, so that it agrees with Execute.

localweb:

  • HostsOptions carries the Refuser of Hosts. CrossSite ignores it, and the doc says so.
  • MisdirectedRefusal gives the fixed text of the answer to a request for another Host. Hosts and session use it.
  • Serve puts its stop signal in the context of each request, and Stopping returns it.

localweb/session:

  • New checks the cookie name and the CSRF header name, and the error names the field.

localweb/localwebtest:

  • Client.CSRF reads the CSRF token of a page, from an <input> or a <meta> element. Client.CSRFField names the element when the program sets its own session.Options.CSRFField.

localweb/page:

  • MaxIncludeDepth is the deepest that include nests in one render.

termtext:

  • EscapeWriter is an exported type, with its own docs for Write and Close. 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 Wrap is now cols, and the package doc gives both rules for zero. The behaviour does not change.

fanout:

  • FailureError is the error of a Map step in which items failed. errors.As finds it under a wrapping error, with its Summary.
  • Keyed.Acquire refuses 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 *Bus is safe, and the doc says so.
  • Span.TraceContext gives a child the trace context of a span, with the span as its parent. NewTraceContext is for a program with no Bus.
  • RunInfo has the field Version, which eventlog passes on.
  • DefaultAdvanceEvery and DefaultFlushEvery are exported.
  • The docs of Timeout and Exit say that they only record a value.

progress/cliprogress:

  • Choose returns (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:

  • NewForwarder returns a *Forwarder, which forwards one run, counts the other runs in Dropped and the refused lines in Skipped.
  • Reducer.Run gives one run by its ID, and Reducer.Skipped counts the lines it refused.
  • DefaultKeepRuns and DefaultMaxSpans are exported.

progress/mcpprogress:

  • A Notifier is a RunSink, and takes Program and PanicLog from the Bus. No Send starts after Close returns, also when Close stops because its context ended.

progress/display:

  • NewSummary makes a Summary from SummaryOptions, as NewTree, NewCounter and NewPlain do. The zero Summary still works.
  • Themes and ParseTheme read their own copies of the themes.

Requirements:

  • None change. fsutil uses golang.org/x/sys, which the kit already requires. The x/sys rule of the lint now names cliprogress, fsutil and localweb/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:

  • ListenerOwner returns the user of the TCP socket that listens at an address. It works on Linux only. On other systems it returns an error that matches errors.ErrUnsupported.

localweb/launch (new):

  • Acquire makes RUNTIME_DIR/PROGRAM for the user alone and takes the lock ui.lock in it. Instance.Running finds a server that an earlier start recorded in ui.json, and accepts it only if its process lives, its address answers and the user owns the socket. Publish records the server and releases the lock; Retire removes the record.
  • Opener.Open writes 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.Skip says why no browser can open: no opener for the system, an SSH session, or no display. Detached starts the opener in a session of its own.
  • ui.lock, ui.json and its keys url and pid are those of sind and brig, so a new version finds a server that an old version started.

localweb/assets (new):

  • New serves the files of an fs.FS at PREFIX/VERSION/NAME, where VERSION is a hash of all the files, cached for a year as immutable. URL and URLs give the addresses for a template. Options.Dev reads each file anew and turns the cache off.
  • ContentType takes the type from a fixed list, never from the host.

localweb/page (new):

  • Parse reads a program’s templates into one set and adds include, 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.Respond renders the whole block before it writes, and writes nothing on a failure. Options.Reload parses anew for each render.
  • IsHTMX, Redirect and Retarget read 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. Files serves them, for example with assets.
  • Config is the htmx configuration that keeps htmx inside the Content Security Policy of localweb.Headers: no eval, 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):

  • Listen binds a loopback address, localhost on both families with one port, or a Unix socket (unix:PATH) that only its owner can reach. Bound.Answers is the Host allow-list that stops DNS rebinding.
  • Hosts, SameOrigin and CrossSite refuse a request with another Host, or one that came from another site, by Sec-Fetch-Site and Origin. A Refuser lets the program answer with its own page.
  • Headers sets the Content Security Policy and the other headers that every page carries. NoStore keeps a response out of the cache.
  • PeerListener closes a TCP connection that a process of another user opened. It works on Linux only. On other systems it returns an error that matches errors.ErrUnsupported.
  • Serve serves the listeners until the context ends, with timeouts and a grace period to stop.

localweb/session (new):

  • Sessions.Middleware exchanges 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):

  • Notifier and Subscription tell a stream which topics changed. Take returns them in the order they first changed.
  • Serve streams the fragments of a page: a baseline on connect, then only the fragments that changed, with keep-alives. Format writes one event.

localweb/localwebtest (new):

  • Login signs a test client in through the launch link. Client sends what a browser sends from the same page. Events reads a stream event by event.

Requirements:

  • None change. net/http and html/template are allowed only in localweb and 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:

  • Decode keeps "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):

  • Decode reads 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 a progress.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.
  • Tail reads a log as it grows, a Poll at a time, whole lines into a Feed. It follows the rotation of cliprogress to the “.1” copy, and reads a log cut short or made again from its start.
  • Forward gives the events of a log to a progress.Sink, each once: run a child with its event log in a file of its own, poll it with Tail, and forward its events to an mcpprogress.Notifier.
  • Reducer folds a log into runs, each the tree of the spans of its command, for a view of what a program did. Options.Keep and Options.MaxSpans bound what it holds; Changed tells 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’s FlagErrorFunc, 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. InvalidValue words a value the program refuses itself the same way.
  • Coded gives an error an exit status, and Status reads it in main, with a default for an error that carries none; the first status given holds.
  • HelpTopics lets help NAME show a topic that is no command, such as help selectors.
  • Resolve takes the version and commit a release build set with -ldflags over what the build info records; BuildInfo.Short cuts the commit to a number of digits, with “-dirty” for a modified tree.

cobratree/cobratreetest (new):

  • Run runs a command tree in a test, with standard output and standard error apart.
  • CheckCommandLine and CheckCommandLines check that the command lines of a manual or a script name commands and flags the tree has.

termtext:

  • Wrap fills text into lines of a number of columns, by display width, with an indent for the first line and the rest.
  • NewEscapeWriter escapes a stream, such as a child’s output, line by line as it arrives.

cliout (new):

  • Table writes a command’s result in columns aligned by display width, every cell escaped, with wide columns shown only when asked.
  • WriteJSON writes indented JSON with <, > and & as they are, and every rune termtext escapes as a JSON escape, so that strings decode to what they were and cannot act on a terminal.

fanout:

  • Keyed bounds the work under way on each key, such as a host, across pools.

fanout/fanouttest (new):

  • InFlight counts the calls a test’s fake is in the middle of, and holds them, to prove a limit.

progress:

  • NewTraceContext makes the trace context of a trace that begins here, and TraceContext.Traceparent writes it for a child’s TRACEPARENT.

progress/cliprogress:

  • Options.DefaultLog names an event log the program keeps unasked; it fails no command and writes no note. Options.RotateAt moves it to its name with “.1” after it once it is larger than that.
  • IsTerminal tells whether a file is a terminal.
  • ThemeFor draws a theme in the colours the terminal shows, from NO_COLOR, TERM and COLORTERM as the program read them.
  • Ask asks a question with the displays off the terminal until the answer comes; Yes reads “y” or “yes”.

progress/mcpprogress (new):

  • Notifier, a sink, sends a tool call’s progress as MCP progress notifications through a Send the 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 after Close. 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.FlushEvery is 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 FlushEvery at most, and an idle log wakes no goroutine.
  • doc/event-log.md describes 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):

  • Strict adds 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, Usagef and IsUsage mark and tell the errors of a command line, pflag’s typed errors among them. The package names no exit code.
  • BuildInfo and ReadBuildInfo give 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/cobra v1.10.2 and github.com/spf13/pflag v1.0.9, with github.com/inconshreveable/mousetrap indirect.
  • golang.org/x/sys goes from v0.48.0 to v0.49.0 and golang.org/x/text from 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.3 or 1:02:03.4, where it read 0:41 or 1:02:03. How long a running target, step, call or wait has run reads 4.2s or 3m12.4s, against a request’s bound 1.0s/10m, and from an hour on 1h02m, as before. A test that compares frames sees 0:41.0 and 4.0s where it saw 0:41 and 4s. 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.0 and then 0:01.1, where v0.2.0 drew 0:01 once. A frame drawn again within the same tenth still writes nothing.
  • A clock that steps back reads 0:00.0 rather 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. The Terminal cuts a row that holds colour sequences by the columns it shows, but cuts a row without one, which is every row drawn with the zero Theme, with termtext.Truncate as before.

progress:

  • The event log stays at version 1 and gains the type advance and the keys amount, size, unit and leftOut (decision 23). It writes an advance of a span at most once a second by default, so seq is no longer gapless: the advances it leaves out leave gaps, which leftOut counts. A reader of the log that checks seq for gaps, or refuses an unknown type, key or unit, must allow these before the programs it reads report work; a program can set a negative LogOptions.AdvanceEvery to write every advance. The log of a program that reports no work is byte for byte what v0.2.0 wrote. Event.Seq on the Bus still has no gaps.
  • BusOptions.Now must be safe for concurrent use once a span reports work: Advance and SetAmount read 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 -race as soon as a span calls Advance, SetAmount, CountWriter or CountReader.
  • 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 Seq order, so a sink’s Handle may 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:

  • Check enforces a new rule 7, the work of a span: an advance names a span that is running and changes its Amount; two advances of a span come at least 100 ms apart by their times; Amount and Size are never negative; a span in Percent has a Size of 100; and a span’s Unit never 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 passed Check under v0.2.0 passes it still (decision 23). A hand-made event of type 8, now TypeAdvance, is no longer reported as having no valid type; rule 7 checks it instead, and refuses one that does not change its span’s Amount or names a span that is not running.
  • A Capture, and the one behind Watch, 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.
  • Tree draws a span’s work after its message and before its flags, as amount=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.Item gains the fields Unit and Size, progress.Fields gains Amount, Size and Unit, progress.LogOptions gains AdvanceEvery, and display.TreeOptions, CounterOptions and PlainOptions gain Theme, 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 of Start, Update and End, gives a span’s work in a Unit, Items, Bytes or Percent, out of a size; a size of 0 or less is not known, and the size of work in Percent is always 100. A unit once given stays: a later Work changes the size alone.
  • Span.Advance adds to the amount and Span.SetAmount sets 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 no Work that advances counts Items.
  • 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’s End carries the final amount. Fields, and so Event, gain Amount, Size and Unit: the span’s own work, never that of the spans below.
  • CountWriter and CountReader advance 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 the io.ReaderFrom of the writer and the io.WriterTo of the reader they wrap, so io.Copy cannot use those fast paths through them; the other side’s still apply.
  • Meter, Reading and Bound roll 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. A Reading also 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 zero Meter is ready to use, and not safe for concurrent use. Reading may gain fields, and Unit values, 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). Theme is a comparable value whose zero value is no theme and draws what v0.2.0 drew. There are five themes, Classic, Aurora, Ember, Neon and Tide. Themes lists them, ParseTheme reads a name in any case, none for the zero Theme, and String, MarshalText and UnmarshalText let a Theme back a --theme flag through flag.TextVar. Theme.In draws a theme in its 256 colours, the terminal’s own 16, or none, its art alone, for NO_COLOR; Theme.Colours says which. The kit reads no environment: the program decides from its flags, NO_COLOR, TERM, COLORTERM and whether standard error is a terminal.
  • TreeOptions, CounterOptions and PlainOptions take a Theme, and Summary a Theme and ASCII. 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, ASCII keeps 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 Terminal cuts 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 in Percent reads 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.1s takes 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 in Percent. 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 than Percent.
  • 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 then 48.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, passes Work, calls Advance or SetAmount, copies through CountWriter or CountReader, or sets fanout.Item.Unit.

progress/progresstest (decision 19):

  • Screen.Styles makes a Screen apply the colours and attributes a theme draws with, as a terminal does: bold, faint, underline, reverse, their resets, colours 30–37, 90–97 and 38;5;n, and 0. They take no column, and String shows the text alone. Any other sequence, such as italic, a background or 24-bit colour, still shows as text. Without Styles, a Screen behaves as before.
  • Screen.Styled shows each run of text with its attributes, as in «1;31»✗«» exe0007.

fanout:

  • Item.Unit and Item.Size: Map starts the target of an item that sets a Unit with progress.Work(Unit, Size), so a display shows its share before it runs, and the work moves it with Advance, SetAmount or CountWriter. A zero Unit is no work, even with a Size, unlike progress.Work, where it is Items; Map then 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, plain and none (Mode, Modes); Choose, which picks a display from the program’s flag and the variable that stands in for it, given together in a Setting (Options.Mode, and Options.Log for 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, and Run.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, Start makes 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/N and 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, TerminalSize and InForeground ask the system, and UTF8Locale reads 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.Modes names, in their order, and the texts of the errors and notes cliprogress returns 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/cliprogress on 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 cliprogress duplicates 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/sys v0.48.0 is new (decision 22), the version sind and clusterctl already require. Only progress/cliprogress imports it, in its files for Unix, and depguard keeps it there; a program that builds no package of cliprogress links none of it, but has it in its module graph.
  • github.com/GSI-HPC/go-nodeset goes 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, and fanout.Batches takes a *nodeset.NodeSet. v1.1.0’s Parse refuses a range with no last bound, such as exe[1-], which v1.0.0 read as exe1.
  • golang.org/x/text stays 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 advance events and the work fields in what its tests capture and in the frames they compare, let its own sinks take TypeAdvance, which may come from a timer’s goroutine, and make sure the readers of its event log take advance, the new keys and the gaps in seq.
  • A program that moves its glue to cliprogress keeps the lines that fill in Options. 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.0v0.2.0
Ending 16,000 targets489 ms98 ms
A run that draws a frame every 100 events, 16,000 targets1.64 s0.29 s
The same, 30,000 targets6.0 s0.83 s
A frame while 16,000 targets run5.8 ms0.72 ms
A frame while targets end, after 16,000 ended well3.8 ms13 µ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 a context.Context, the Bus that 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 the Terminal they 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, and Screen, a terminal for tests.
  • fanout: Each, Map and Batches, 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-nodeset v1.0.0 and golang.org/x/text v0.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 termtext escapes, which may grow in a minor release (decision 14);
  • the rules progresstest.Check enforces: a new or stricter rule is a breaking change (decision 15).

Coming from clusterctl v0.4.0#

Rewrite the imports:

clusterctl v0.4.0go-clikit v0.1.0
github.com/GSI-HPC/clusterctl/internal/termtextgithub.com/GSI-HPC/go-clikit/termtext
github.com/GSI-HPC/clusterctl/internal/progressgithub.com/GSI-HPC/go-clikit/progress
github.com/GSI-HPC/clusterctl/internal/progress/displaygithub.com/GSI-HPC/go-clikit/progress/display
github.com/GSI-HPC/clusterctl/internal/progress/progresstestgithub.com/GSI-HPC/go-clikit/progress/progresstest
github.com/GSI-HPC/clusterctl/internal/clikit/fanoutgithub.com/GSI-HPC/go-clikit/fanout
github.com/GSI-HPC/clusterctl/nodesetgithub.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.0go-clikit v0.1.0
termtext.EscapeCell, termtext.EscapeTexttermtext.Escape, termtext.EscapeLines; the escaped bytes are the same
termtext.RuneWidth(r)unexported; termtext.Width(string(r))
progress.Optionsprogress.BusOptions
Options.Trace, Parent, TraceFlags, TraceStateBusOptions.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.WantsLinesremoved
Count.TargetsCount.Started
fanout.Skip, fanout.IsSkipped(err)progress.Skip, errors.Is(err, progress.ErrSkipped); Span.End ends a span skipped for it
fanout.Optionsfanout.MapOptions
fanout.DefaultMaxfanout.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.Afterremoved; test the pause in a testing/synctest bubble
Batch.RanBatch.Started
PanicError.Value stringPanicError.Value any, and PanicError.Target
display.NewTerminal(w, size) and the fields PanicLog, Program, Foregrounddisplay.NewTerminal(w, display.TerminalOptions{…})
progresstest.Checkedremoved; 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:

  • Width and Truncate count 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/text knows 15.0. U+1171E, a spacing mark since Unicode 16.0, takes one.
  • Text holding any of these measures wider, and Truncate cuts it shorter.

progress:

  • Classify takes a fallback’s ClassNone for an error as ClassTarget, so a span no longer ends “failed (none)”.
  • The event log names the program from BusOptions.Program when LogOptions.Program is empty.

progress/display:

  • Plain counts “1 target” and “480 targets” when PlainOptions.Noun is nil, not hosts. A program that names its targets is unchanged.
  • Start starts a display once, and never after Close. 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.Map given no Step reports: 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:

  • Map takes the program’s name, its panic log and its class rule from the Bus in its context when MapOptions leaves them empty.
  • A batch whose work returns a skip ends skipped, and Batches goes on with the next one rather than stopping.
  • Each given a limit below one runs DefaultLimit at a time, as Map does, not one.
  • Map ends 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 by ctx.Err().
  • An item Options.Acquire refused ends its target at once, rather than once the whole pool has finished: skipped for an error of Skip, 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 Acquire returned 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 Acquire fails its item even after the context has ended, rather than passing for a cancellation.
  • Work, Acquire or a release that calls runtime.Goexit, as t.FailNow does, fails its item with an error that names which of the three it was, instead of leaving it reported as done.
  • Failure lists 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.
  • Recovered escapes the target it names.

progress/progresstest:

  • Watch asks for the lines of output, so Check also checks their texts, and it checks the events at the test’s cleanup when Finish is not called.
  • Tree writes a target’s name as {} only where it stands alone, not inside other words.
  • Tree lists 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.
  • Tree orders siblings that differ only in leading zeros, “x01” and “x1”, the same way on every run.
  • Check holds the batches of every step to being announced queued before the first runs, not only those under a Fold step.
  • Screen wraps 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.Close starts no write once it has stopped waiting, and the new Log.Done says 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 PanicLog that is one of the Terminal’s own writers no longer deadlocks the program once a display panics.
  • Tally counts 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.
  • Tee and the Lines writer 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 \x1b reads 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.