| type | memory |
|---|---|
| description | Phase-separator output contract for `wt create` / `wt init` — Git/Init/Open separators on stderr, stdout reserved for the machine result. |
Post-implementation behavior capture for the
wt create/wt initphase-separator output. Source change:260531-pnmi-add-phase-separators(singlewt.Warnwarning emitter +wt deletestdout→stderr warning realignment added by260622-log5-dx-copy-polish).
This file documents the phase-separator output contract that wt create and wt init honor, and — as the canonical stdout/stderr stream-discipline file — the single wt.Warn warning emitter that all Warning: call sites route through. Future changes touching src/cmd/wt/create.go, src/cmd/wt/init.go, src/cmd/wt/delete.go, or src/internal/worktree/{crud.go,errors.go} should preserve these invariants unless an explicit spec amendment supersedes them.
src/internal/worktree/errors.goexposesfunc PhaseSeparator(label string) string, alongside the other output helpers (WtError,PrintError,PrintInitFailureBanner). It is the single source of phase-separator strings — call sites MUST NOT reinline the glyph/width/color logic.- The returned string has no trailing newline; callers add it via the
fmt.Fprintln(os.Stderr, ...)they already use. - Fixed total visible width of
phaseSeparatorWidth = 40columns (label + glyphs + spaces). It MUST NOT query the terminal size — fixed width keeps output deterministic for tests and matches the single-binary / no-hidden-state posture (Constitution I). - Visible width is rune-counted (
utf8.RuneCountInString(label)), not byte-counted, so a label with multi-byte runes (e.g. a non-ASCIIWORKTREE_INIT_SCRIPTpath) still renders at the fixed visible width. - Layout: two leading glyphs, a space, the label, a space, then enough trailing glyphs to fill 40 visible columns. ANSI escapes around the label are NOT counted toward the width. Trailing glyph count is clamped at 0 for labels at or over the budget.
- Color detection uses the blanked package-level color vars (
colorEnabled := ColorReset != ""), reusing the existinginit()that blanksColorBold/ColorReset/etc. whenNO_COLORis non-empty. It MUST NOT callos.Getenvafresh.- Color enabled: unicode rule
── Label ──…(glyph─, U+2500 BOX DRAWINGS LIGHT HORIZONTAL) framing aColorBold-wrapped label. NO_COLORset (color vars blanked): plain-ASCII rule-- Label --…(glyph-) with no ANSI escape sequences.
- Color enabled: unicode rule
wt createemits up to three phase separators to stderr (never stdout), in order, each immediately preceding the output of its phase:PhaseSeparator("Git")— emitted increate.goimmediately before the deferred summary block (Created worktree:/Path:/Branch:), joining the existing deferred-summary emission under the rollback handler.- The Init separator — emitted by the init runner, NOT by
create.go(see next requirement). PhaseSeparator("Open")— emitted immediately before the open phase. As of260602-z4p7, when the init phase ran, the Open separator + menu render are now preceded by an unconditional foreground reclaim (reclaimTerminalForeground(ttyFd, wtPgid)increate.go, just before the Open separator, gated on the sameterm.IsTerminalcheck as capture) so the Open phase can never SIGTTOU on a shared-TTY init child that stranded terminal foreground. The separator output contract itself is unchanged — this is a job-control ordering guarantee, not an output change. Seeinit-failure-contract.md"Terminal foreground is reclaimed after the init child".
- A separator is emitted only when its phase produces output — a separator never precedes a phase that emits nothing:
- Git is emitted on every successful create, because the deferred summary block always prints (it is the Git phase's output).
- Init is emitted only when the init phase actually runs an init command — NOT when init is disabled and NOT on the
*InitNotFoundpath. - Open is emitted only when the open phase runs, gated on
worktreeOpen != "skip".--worktree-open=skip(and--non-interactive, which defaults open toskip) suppress it.
- The Git separator MUST stay inside the existing deferred-summary emission, before the init-phase
signal.Reset(syscall.SIGINT, syscall.SIGTERM)call. It MUST NOT introduce new I/O or a prompt inside the tight reinstall window betweengit worktree addreturning and thatsignal.Reset(seeinit-failure-contract.md"SIGINT during init"). - Existing summary/warning/prompt substrings are preserved verbatim — in particular
Created worktree:andOpen in:.
- The Init separator is emitted by the code that owns the
Running worktree init...line, so it is produced exactly once regardless of caller — a single emission point per path:RunWorktreeSetupWithObserverinsrc/internal/worktree/crud.go(thewt createinit step) —crud.go:157.runInitScriptinsrc/cmd/wt/init.go(the standalonewt initpath) —init.go:78.
- The label is
Init (<cmd>), where<cmd>is the resolved init command as the user would recognize it (e.g.Init (fab sync), orInit (scripts/setup.sh)for a file-path invocation). It is derived from the same init-script value resolution uses (InitScriptPath()/ the resolved*exec.Cmd) and is never hardcoded. - On the
*InitNotFoundpath, the existingRenderWarning()is used unchanged and no Init separator is emitted — there is no command to label. The graceful-skip exit-0 behavior is unchanged (seeinit-failure-contract.md). - The substrings
Running worktree initandWorktree init completeremain present — the separator augments these markers, it does not replace them (preserves existing test assertions).
- All phase separators are written to stderr. None ever appears on stdout.
wt create's stdout remains solely the final worktree-path line (fmt.Println(wtPath)), byte-identical to before this change. This preserves the launcher-contract guarantee that stdout is the machine-readable result. No separator, summary, or init output leaks to stdout.wt initwas realigned so ALL its init diagnostics go to stderr: theRunning worktree init.../Worktree init complete.banner (nowfmt.Fprintln(os.Stderr, ...)), the Init separator, AND the init script's own stdout (the init child is wired withcmd.Stdout = os.Stderratinit.go:83, joiningcmd.Stderr = os.Stderr).wt inithas no machine-readable stdout result — it is a side-effecting command whose outcome is its exit code — so all of its output is diagnostic.- The realignment changed only the stream (stdout → stderr) for
wt init: exit-code behavior (ExitInitFailed = 7on script non-zero exit), graceful-skip behavior, and all output text are unchanged. Existingwt inittests assert on the combinedr.Stdout + r.Stderr, so they stay green.
src/internal/worktree/errors.goexposesfunc Warn(format string, args ...any)alongside the other output helpers (WtError,PrintError,PhaseSeparator,PrintInitFailureBanner). It is the shared one-lineWarning:-diagnostic helper used bycreate/delete— it writes"%sWarning:%s %s\n"(ColorYellowprefix,ColorReset, then the formatted message) to stderr. (Exception: the verbose init not-found warning,InitNotFound.RenderWarningininternal/worktree/init.go, builds its own multi-lineWarning:text and does not route throughWarn— soWarnis not the CLI's soleWarning:producer.)- It is the warnings analogue of
PhaseSeparator: color detection reuses the blanked package-level color vars (theNO_COLOR-blankinginit()zeroesColorYellow/ColorReset), so underNO_COLORit emits a plainWarning: <msg>\nwith no ANSI escapes. It MUST NOT callos.Getenvafresh. - All one-line
Warning:call sites route throughWarn—create.go(the uncommitted-changes warning and the open-phase "could not open in …" / reused-worktree-init warnings) anddelete.go(the twofailed to remove …warnings and the two pre-menu warnings). (The init not-found renderer noted above is the lone multi-line exception that stays out of band.) Call sites pass the message text only, never the literalWarning:prefix — the helper owns the prefix, so re-including it would double it. wt delete's two pre-menu warnings were realigned stdout → stderr. The uncommitted-changes warning (handleUncommittedChanges) and the unpushed-commits warning (handleUnpushedCommits) previously printed viafmt.Printf(stdout); they now route throughwt.Warn(→ stderr), matching the sibling warnings already on stderr in the same file. This is the warnings counterpart of thewt initstdout→stderr realignment above — same stream-discipline fix, different command.wt deletehas no stdout machine contract, but warnings are diagnostics and belong on stderr.- Menu-layout spacing is preserved at the call site, not in the helper.
Warnappends exactly one trailing\n. The two pre-menu warnings need a blank line before and after for menu spacing, so each call site frames thewt.Warn(...)with a barefmt.Fprintln(os.Stderr)immediately before and after (reproducing the old\n…\n\nframing now that the whole block is on stderr). - Each warning's existing wording is preserved — the consolidation standardizes only the prefix, stream, and color, never the message text.
errors.go already centralizes user-facing output formatting (WtError, PrintInitFailureBanner) and owns the NO_COLOR-blanking init(). Co-locating PhaseSeparator reuses that mechanism and keeps the canonical glyph/width/color wording in one place — mirroring how RenderWarning is the single source for not-found warnings. Inlining rule construction at each call site (create.go, crud.go, init.go) was rejected: it would duplicate the logic and invite drift.
The runner already prints Running worktree init... and already holds the resolved *exec.Cmd, so it can label the separator with the resolved command and guarantee exactly one emission for both wt create and wt init. Emitting from create.go was rejected — it would either duplicate the resolution logic or double-label when wt init runs the same runner.
The convention captured by this change: stdout = machine-readable result; stderr = diagnostics (progress lines, banners, separators, prompts). wt create already followed it (stdout = the path line only). wt init used stdout for identical init diagnostics, an asymmetry that made separator placement inconsistent. Since wt init has no stdout contract (its tests assert on combined streams and no spec pins it to stdout), realigning it to stderr resolves the inconsistency and makes separator placement uniform across both commands. Keeping each command on its own stream was rejected — it enshrines the inconsistency.
Decision: a single Warn(format, args...) lives in internal/worktree/errors.go next to PhaseSeparator/WtError/PrintInitFailureBanner, and every Warning: call site in create.go/delete.go routes through it; the documentation home is this stream-discipline file (a note here), not a new dedicated memory file.
Why: errors.go already centralizes user-facing output formatting and owns the NO_COLOR-blanking init(), so Warn reuses that mechanism and collapses ~10 divergent call sites to one idiom (DRY, and keeps cmd/ thin per Constitution V) — exactly the rationale that put PhaseSeparator here. The warning realignment is the same stdout=machine / stderr=human convention this file already canonicalizes (see the wt init realignment above), so it belongs as a note here rather than fragmenting the stream-discipline story across two files. A standalone warning-output-contract.md would be the smallest file in the bundle for one helper + a two-call-site realignment, against the per-command / per-shared-concern granularity of the sibling files (none of errors.go's other emitters got their own file).
Rejected: a cmd/-local warning helper (pushes formatting into the thin command layer, can't reuse the package color vars); a dedicated warning-output-contract.md memory file (over-granular for a copy-polish change; splits the one stream-discipline contract in two).
Introduced by: 260622-log5-dx-copy-polish.
Deterministic output for unit tests; no dependency on terminal/ioctl; consistent with the single-binary, no-hidden-state posture (Constitution I). Dynamic width via terminal detection was rejected — non-deterministic in tests and adds a platform-specific code path for a cosmetic gain. (This mirrors wt list's flag-based, no-isatty posture; see list-status-contract.md.)
- Spec doc: none — phase-separator output is per-command diagnostic structure, not part of
docs/specs/cli-surface.md's per-subcommand flag surface. This contract lives in memory only. - Source:
src/internal/worktree/errors.go(PhaseSeparator,phaseSeparatorWidth,Warn, theNO_COLOR-blankinginit()),src/internal/worktree/crud.go(RunWorktreeSetupWithObserverInit separator emission),src/cmd/wt/create.go(Git + Open separators, stdout path line,wt.Warncall sites),src/cmd/wt/init.go(runInitScriptInit separator + stderr realignment),src/cmd/wt/delete.go(wt.Warncall sites — incl. the two pre-menu warnings realigned stdout→stderr withfmt.Fprintln(os.Stderr)spacing framing). - Tests:
src/internal/worktree/errors_test.go(PhaseSeparatorunit test: label presence, colored ANSI +─, NO_COLOR ASCII-with no\033[, 40-column visible width, no trailing newline;260622-log5:Warnunit test — color-wrappedWarning:to stderr when colored, plainWarning:with no ANSI under blanked color vars);src/cmd/wt/create_test.go(Created worktree:stderr + one-line-stdout guard);src/cmd/wt/init_test.go(combined-streamRunning worktree init/Worktree init complete);src/cmd/wt/delete_test.go(260622-log5: the uncommitted-changes / unpushed-commits warnings appear on stderr, not stdout). - Constitution: Principle I (Single-Binary CLI, No Hidden State — motivated the fixed width, no terminal query), Principle V (Internal Package Boundary —
Warnlives ininternal/worktree, keepingcmd/thin), Principle VI (Interactive by Default, Scriptable on Demand — stdout=machine-result keepswt create's path line deterministic for launchers/operators). - Sibling memory:
wt-cli/init-failure-contract.md— the init runner / resolver /*InitNotFoundcontract this change builds on (the Init separator sits next to theRunning worktree initline and respects the not-found and reinstall-window invariants)./wt-cli/go-command-contract.md—wt go's stderr navigation confirmation (260622-log5) honors this same stdout=machine / stderr=human stream discipline.wt-cli/list-status-contract.mdandwt-cli/menu-navigation-contract.md— same post-change invariant-capture pattern for siblingwtsubcommands.