Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/memory/wt-cli/create-output-phases.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ This file documents the phase-separator output contract that `wt create` and `wt
- `wt create`'s stdout remains **solely** the final worktree-path line (`fmt.Println(wtPath)`), byte-identical to before this change, **on the success path**. This preserves the launcher-contract guarantee that stdout is the machine-readable result. No separator, summary, banner, prompt, or init output leaks to stdout.
- **On any init-failure path the path line is NOT printed** (`260626-n6ma`). The end-of-function guard `if initFailed { os.Exit(wt.ExitInitFailed) }` (`create.go:476–478`) sits **before** `fmt.Println(wtPath)` (`create.go:482`), so a non-zero init exit — including after a successful interactive open-anyway open — exits 7 with **no** stdout path line. This is the correct stdout discipline: stdout is the success result, and an init failure is not a success. Operators detect the kept worktree via exit code 7 (and the stderr banner's absolute `Worktree:` path / `Go:` hint), not via a stdout line. The init-failure banner and the open-anyway prompt are stderr writes, consistent with stdout=machine-result / stderr=human.
- `wt init` was **realigned** so ALL its init diagnostics go to stderr: the `Running worktree init...` / `Worktree init complete.` banner (now `fmt.Fprintln(os.Stderr, ...)`), the Init separator, AND the init script's own stdout (the init child is wired with `cmd.Stdout = os.Stderr` at `init.go:83`, joining `cmd.Stderr = os.Stderr`). `wt init` has **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 = 7` on script non-zero exit), graceful-skip behavior, and all output text are unchanged. Existing `wt init` tests assert on the combined `r.Stdout + r.Stderr`, so they stay green.
- The realignment changed only the stream (stdout → stderr) for `wt init`: exit-code behavior, graceful-skip behavior, and all output text were unchanged by *this* change. `wt init` exits `ExitInitFailed = 7` on a script non-zero exit, with one carve-out added later by `260705-irnt`: the **built-in default** `fab sync` exiting fab-kit's `ExitNotManaged = 3` in a non-fab repo is classified (`DefaultNotApplicable`) as "default does not apply" — `wt init` writes the skip warning and exits 0 instead of 7 (an explicit `WORKTREE_INIT_SCRIPT` value, or any other exit code, still exits 7; see `init-failure-contract.md`). Existing `wt init` tests assert on the combined `r.Stdout + r.Stderr`, so they stay green.

### `wt.Warn` is the one-line warning helper; warnings are stderr-only (`260622-log5`)

- `src/internal/worktree/errors.go` exposes `func Warn(format string, args ...any)` alongside the other output helpers (`WtError`, `PrintError`, `PhaseSeparator`, `PrintInitFailureBanner`). It is the **shared one-line** `Warning:`-diagnostic helper used by `create`/`delete` — it writes `"%sWarning:%s %s\n"` (`ColorYellow` prefix, `ColorReset`, then the formatted message) to **stderr**. (Exception: the verbose init not-found warning, `InitNotFound.RenderWarning` in `internal/worktree/init.go`, builds its own multi-line `Warning:` text and does **not** route through `Warn` so `Warn` is not the CLI's sole `Warning:` producer.)
- `src/internal/worktree/errors.go` exposes `func Warn(format string, args ...any)` alongside the other output helpers (`WtError`, `PrintError`, `PhaseSeparator`, `PrintInitFailureBanner`). It is the **shared one-line** `Warning:`-diagnostic helper used by `create`/`delete` — it writes `"%sWarning:%s %s\n"` (`ColorYellow` prefix, `ColorReset`, then the formatted message) to **stderr**. (Exceptions: the two multi-line `Warning:` renderers in `internal/worktree/init.go` — `InitNotFound.RenderWarning` (verbose not-found copy) and `RenderDefaultSkipWarning` (the `260705-irnt` default-not-applicable skip copy) — build their own multi-line text and do **not** route through `Warn`, so `Warn` is not the CLI's sole `Warning:` producer.)
- It is the warnings analogue of `PhaseSeparator`: color detection reuses the **blanked package-level color vars** (the `NO_COLOR`-blanking `init()` zeroes `ColorYellow`/`ColorReset`), so under `NO_COLOR` it emits a plain `Warning: <msg>\n` with **no** ANSI escapes. It MUST NOT call `os.Getenv` afresh.
- **All one-line `Warning:` call sites route through `Warn`** — `create.go` (the uncommitted-changes warning and the open-phase "could not open in …" / reused-worktree-init warnings) and `delete.go` (the two `failed 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 literal `Warning: ` prefix — the helper owns the prefix, so re-including it would double it.
- **All one-line `Warning:` call sites route through `Warn`** — `create.go` (the uncommitted-changes warning and the open-phase "could not open in …" / reused-worktree-init warnings) and `delete.go` (the two `failed to remove …` warnings and the two pre-menu warnings). The two multi-line renderers noted above (`InitNotFound.RenderWarning` and `RenderDefaultSkipWarning`, both in `init.go`) stay out of band — `Warn` owns the *one-line* `Warning:` idiom, not the multi-line ones. Call sites pass the **message text only**, never the literal `Warning: ` 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 via `fmt.Printf` (**stdout**); they now route through `wt.Warn` (→ **stderr**), matching the sibling warnings already on stderr in the same file. This is the warnings counterpart of the `wt init` stdout→stderr realignment above — same stream-discipline fix, different command. `wt delete` has 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.** `Warn` appends exactly one trailing `\n`. The two pre-menu warnings need a blank line before and after for menu spacing, so each call site frames the `wt.Warn(...)` with a bare `fmt.Fprintln(os.Stderr)` immediately before and after (reproducing the old `\n…\n\n` framing 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.
Expand Down Expand Up @@ -91,7 +91,7 @@ Deterministic output for unit tests; no dependency on terminal/ioctl; consistent
## Cross-references

- 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`, the `NO_COLOR`-blanking `init()`), `src/internal/worktree/crud.go` (`RunWorktreeSetupWithObserver` Init separator emission), `src/cmd/wt/create.go` (Git + Open separators, stdout path line gated behind the end-of-function `initFailed` exit-7 guard per `260626-n6ma`, `wt.Warn` call sites), `src/cmd/wt/init.go` (`runInitScript` Init separator + stderr realignment), `src/cmd/wt/delete.go` (`wt.Warn` call sites — incl. the two pre-menu warnings realigned stdout→stderr with `fmt.Fprintln(os.Stderr)` spacing framing).
- Source: `src/internal/worktree/errors.go` (`PhaseSeparator`, `phaseSeparatorWidth`, `Warn`, the `NO_COLOR`-blanking `init()`), `src/internal/worktree/init.go` (`InitNotFound.RenderWarning` / `RenderDefaultSkipWarning` — the two out-of-band multi-line `Warning:` renderers that do not route through `Warn`), `src/internal/worktree/crud.go` (`RunWorktreeSetupWithObserver` Init separator emission), `src/cmd/wt/create.go` (Git + Open separators, stdout path line gated behind the end-of-function `initFailed` exit-7 guard per `260626-n6ma`, `wt.Warn` call sites), `src/cmd/wt/init.go` (`runInitScript` Init separator + stderr realignment; the `260705-irnt` exit-3 default-skip carve-out returns before the `Worktree init complete.` trailer), `src/cmd/wt/delete.go` (`wt.Warn` call sites — incl. the two pre-menu warnings realigned stdout→stderr with `fmt.Fprintln(os.Stderr)` spacing framing).
- Tests: `src/internal/worktree/errors_test.go` (`PhaseSeparator` unit test: label presence, colored ANSI + `─`, NO_COLOR ASCII `-` with no `\033[`, 40-column visible width, no trailing newline; `260622-log5`: `Warn` unit test — color-wrapped `Warning:` to stderr when colored, plain `Warning: ` 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-stream `Running 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 — `Warn` lives in `internal/worktree`, keeping `cmd/` thin), Principle VI (Interactive by Default, Scriptable on Demand — stdout=machine-result keeps `wt create`'s path line deterministic for launchers/operators).
- Sibling memory: `wt-cli/init-failure-contract.md` — the init runner / resolver / `*InitNotFound` contract this change builds on (the Init separator sits next to the `Running worktree init` line and respects the not-found and reinstall-window invariants); as of `260626-n6ma` it also owns the interactive open-anyway prompt + `Go:` banner hint that make the Open phase reachable after an init failure and keep the stdout path line suppressed on exit 7. `/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.md` and `wt-cli/menu-navigation-contract.md` — same post-change invariant-capture pattern for sibling `wt` subcommands.
12 changes: 11 additions & 1 deletion docs/memory/wt-cli/init-failure-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ This file documents the contract that `wt create` and `wt init` honor when the i
- `cmd/wt/init.go` (the `wt init` subcommand) also exits via `os.Exit(wt.ExitInitFailed)` on init-script non-zero exit. Returning the error to Cobra would map to `ExitGeneralError = 1`; the explicit `os.Exit` ensures both command paths emit the typed code. (`wt init` has no open-anyway prompt — it does not call `PrintInitFailureBanner` and is unchanged by `260626-n6ma`.)
- Operators (shell wrappers, fab-kit, `hop`) can distinguish "worktree exists, init didn't complete" from any other generic failure — including when the user chose to open the kept worktree anyway.

### Default-not-applicable run-time skip bypasses the failure contract (`260705-irnt`)

> The one carve-out from "`ExitInitFailed = 7` on EVERY path" above. When the **built-in default** init script `fab sync` runs in a repo that is not fab-managed and exits fab-kit's `ExitNotManaged = 3`, the whole failure contract is bypassed: no `ExitInitFailed`, no kept-worktree banner, no open-anyway prompt, no `Worktree init complete.` line, exit 0, and `wt create`'s Open phase proceeds exactly as on init success.

- **Run-time classification, not resolve-time.** This is a distinct axis from the not-found skip above: resolution *succeeds* and the script genuinely *runs*. Only after `cmd.Run()` returns non-zero is the outcome classified. So it composes with, and is mutually exclusive from, the resolve-time `CommandNotOnPath` / `FileNotFound` skips by construction (post-run vs. pre-run — a script that never ran cannot have exited 3; a script that exited 3 was resolved and ran). `ResolveInitInvocation` and `InitNotFound` are untouched.
- **Single shared helper, keyed on the documented exit code.** `src/internal/worktree/init.go` exposes `func DefaultNotApplicable(err error, isDefault bool) bool` — it returns true only when `isDefault` is true AND `err` unwraps (via `errors.As`) to an `*exec.ExitError` whose code is exactly the documented `exitNotManaged = 3` const. `func RenderDefaultSkipWarning() string` is the single renderer for the two-line skip copy. Both mirror the `RenderWarning()` anti-drift discipline so the two run sites cannot diverge. `wt` embeds the numeric `3` (documented as fab-kit's `ExitNotManaged`) rather than importing from fab-kit — separate repos, no import (Constitution I).
- **Provenance-gated (built-in default only).** The gate is `isDefault`, which `InitScriptPath()` reports as provenance — true only when `WORKTREE_INIT_SCRIPT` is unset/empty, never by string equality. An explicit `WORKTREE_INIT_SCRIPT="fab sync"` (`isDefault=false`) still hard-fails on exit 3 exactly like any other configured script — the user opted into it, so the failure is theirs to see. `DefaultNotApplicable` returns false whenever `isDefault` is false, regardless of exit code.
- **Both run sites route through the helper.** `RunWorktreeSetupWithObserver` (`crud.go:178–181`) and `wt init`'s own `cmd.Run()` failure path (`cmd/wt/init.go:128–131`) apply `DefaultNotApplicable` before the hard-fail path: on the skip they write `RenderDefaultSkipWarning()` to stderr and return `nil` (no `Worktree init complete.`). `wt init` reclaims the terminal foreground **first** (reclaim-before-write ordering preserved, `init.go:119–121`) so the skip-warning write cannot SIGTTOU. `wt create`'s two call sites (normal via `RunWorktreeSetupWithObserver`, `--reuse` via `RunWorktreeSetup`) pass `isDefault` from `InitScriptPath()` and need no other logic — the skip surfaces as a `nil` return, so `initFailed` is never set (no banner, no prompt, exit 0, stdout path line unaffected).
- **Old fab-kit degrades to today's hard-fail.** An installed fab-kit predating PR #471 exits 1 in a non-fab repo, which is not 3, so `DefaultNotApplicable` returns false and behavior is exactly as today (hard fail, exit 7, banner). No version detection, no fallback probe — the skip lights up only when fab-kit updates. Every non-zero exit other than 3 (any provenance) keeps `ExitInitFailed = 7` on every path.

### Interactive open-anyway prompt (`260626-n6ma`)

- On init-script non-zero exit, `wt create` offers to open the kept worktree anyway — but **only when interactive**, gated by `!nonInteractive && reclaimTTY` (`reclaimTTY` is `term.IsTerminal(os.Stdin.Fd())`, already computed earlier in the init block; `create.go:388`). This is the same TTY / `--non-interactive` discipline the rest of `create` uses (Constitution VI).
Expand Down Expand Up @@ -135,7 +145,7 @@ The interactive *Yes* path does NOT build a second open codepath inside the init
## Cross-references

- Spec doc: `docs/specs/init-protocol.md` — wire contract + "Script failure semantics" + "SIGINT during init" sections (rewritten by this change).
- Source: `src/internal/worktree/init.go` (resolver, `InitNotFound`, `RenderWarning`), `src/internal/worktree/errors.go` (`ExitInitFailed`, `PrintInitFailureBanner` + the `bannerLabelGo` `Go:` hint, `260626-n6ma`), `src/internal/worktree/crud.go` (`RunWorktreeSetup`, `RunWorktreeSetupWithObserver`), `cmd/wt/create.go` (rollback disarm + handler swap + unified post-init foreground reclaim/teardown + `initFailed`-flag-driven open-anyway fall-through and end-of-function exit, `260626-n6ma`), `cmd/wt/init.go` (resolver consumer + foreground capture/reclaim around `runInitScript`'s `cmd.Run()`), `src/internal/worktree/menu.go` (`ConfirmYesNo`, the open-anyway prompt), `src/internal/worktree/apps.go` (`shellQuoteSingle`, `OpenInApp` test seam), `src/cmd/wt/tty_unix.go` / `src/cmd/wt/tty_windows.go` (`terminalForeground` / `reclaimTerminalForeground` build-tag split).
- Source: `src/internal/worktree/init.go` (resolver, `InitNotFound`, `RenderWarning`; the `260705-irnt` run-time skip helpers `DefaultNotApplicable` / `RenderDefaultSkipWarning` + the documented `exitNotManaged = 3` const), `src/internal/worktree/context.go` (`InitScriptPath() (script string, isDefault bool)` — provenance report, `260705-irnt`), `src/internal/worktree/errors.go` (`ExitInitFailed`, `PrintInitFailureBanner` + the `bannerLabelGo` `Go:` hint, `260626-n6ma`), `src/internal/worktree/crud.go` (`RunWorktreeSetup`, `RunWorktreeSetupWithObserver` — both gained the `isDefault` param + post-run `DefaultNotApplicable` classification, `260705-irnt`), `cmd/wt/create.go` (rollback disarm + handler swap + unified post-init foreground reclaim/teardown + `initFailed`-flag-driven open-anyway fall-through and end-of-function exit, `260626-n6ma`; both init call sites pass `isDefault`, `260705-irnt`), `cmd/wt/init.go` (resolver consumer + foreground capture/reclaim around `runInitScript`'s `cmd.Run()`; `DefaultNotApplicable` skip before `os.Exit(ExitInitFailed)`, reclaim-before-write preserved, `260705-irnt`), `src/internal/worktree/menu.go` (`ConfirmYesNo`, the open-anyway prompt), `src/internal/worktree/apps.go` (`shellQuoteSingle`, `OpenInApp` test seam), `src/cmd/wt/tty_unix.go` / `src/cmd/wt/tty_windows.go` (`terminalForeground` / `reclaimTerminalForeground` build-tag split).
- Tests: `src/cmd/wt/tty_pty_test.go` — `TestIntegration_ReclaimForegroundAfterInit_NotStopped` (Unix-only, `//go:build !windows`): allocates a real PTY via an in-tree `openpty` helper on `x/sys/unix` (`/dev/ptmx` + `TIOCSPTLCK`/`TIOCGPTN` + `/dev/pts/N`, no `creack/pty`), runs `wt create` under it via a session-leader launcher with a foreground-stranding init script, and asserts the `wt` process is not left `WIFSTOPPED`; self-skips when a PTY / process groups can't be set up. Verified non-vacuous (fails `WIFSTOPPED` without the reclaim).
- Constitution: Principle III (Typed Exit Codes) — `ExitInitFailed` is the canonical example for "worktree exists, init didn't complete". Principle I (Single-Binary CLI, slim deps) — drove the `x/sys/unix` ioctls (no new module) and the in-tree `openpty` helper (no `creack/pty`). Principle VI (Interactive by Default, Scriptable on Demand) — the reclaim restores the broken interactive Open path; the `term.IsTerminal` gate keeps the scriptable / non-interactive path a no-op.
- Sibling memory: `wt-cli/create-output-phases.md` — the Open-phase separator + menu render now sits behind the unconditional pre-Open foreground reclaim.
Loading
Loading