From a16fea0c654473d2378ccf2dc703221a6caf4aea Mon Sep 17 00:00:00 2001 From: Sahil Ahuja Date: Wed, 3 Jun 2026 20:11:54 +0530 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20wt=20help-dump=20=E2=80=94=20emit?= =?UTF-8?q?=20CLI=20command=20tree=20as=20JSON=20for=20shll.ai=20pull?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/memory/index.md | 2 +- docs/memory/wt-cli/help-dump-contract.md | 153 ++++++++++++ fab/backlog.md | 2 +- .../.history.jsonl | 20 ++ .../.status.yaml | 49 ++++ .../260603-qqkj-help-dump-command/intake.md | 186 ++++++++++++++ .../260603-qqkj-help-dump-command/plan.md | 174 +++++++++++++ src/cmd/wt/help_dump.go | 40 +++ src/cmd/wt/help_dump_test.go | 107 ++++++++ src/cmd/wt/main.go | 1 + src/internal/worktree/helpdump.go | 188 ++++++++++++++ src/internal/worktree/helpdump_test.go | 235 ++++++++++++++++++ 12 files changed, 1155 insertions(+), 2 deletions(-) create mode 100644 docs/memory/wt-cli/help-dump-contract.md create mode 100644 fab/changes/260603-qqkj-help-dump-command/.history.jsonl create mode 100644 fab/changes/260603-qqkj-help-dump-command/.status.yaml create mode 100644 fab/changes/260603-qqkj-help-dump-command/intake.md create mode 100644 fab/changes/260603-qqkj-help-dump-command/plan.md create mode 100644 src/cmd/wt/help_dump.go create mode 100644 src/cmd/wt/help_dump_test.go create mode 100644 src/internal/worktree/helpdump.go create mode 100644 src/internal/worktree/helpdump_test.go diff --git a/docs/memory/index.md b/docs/memory/index.md index b103131..3b01a3e 100644 --- a/docs/memory/index.md +++ b/docs/memory/index.md @@ -5,4 +5,4 @@ | Domain | Description | Memory Files | |--------|-------------|------| -| wt-cli | Behavior contracts for the `wt` CLI binary (commands, exit codes, signal handling, test seams). | [wt-cli/init-failure-contract.md](wt-cli/init-failure-contract.md), [wt-cli/list-status-contract.md](wt-cli/list-status-contract.md), [wt-cli/menu-navigation-contract.md](wt-cli/menu-navigation-contract.md), [wt-cli/update-command-contract.md](wt-cli/update-command-contract.md), [wt-cli/recency-ordering-contract.md](wt-cli/recency-ordering-contract.md), [wt-cli/idle-staleness-contract.md](wt-cli/idle-staleness-contract.md), [wt-cli/create-output-phases.md](wt-cli/create-output-phases.md) | +| wt-cli | Behavior contracts for the `wt` CLI binary (commands, exit codes, signal handling, test seams). | [wt-cli/init-failure-contract.md](wt-cli/init-failure-contract.md), [wt-cli/list-status-contract.md](wt-cli/list-status-contract.md), [wt-cli/menu-navigation-contract.md](wt-cli/menu-navigation-contract.md), [wt-cli/update-command-contract.md](wt-cli/update-command-contract.md), [wt-cli/recency-ordering-contract.md](wt-cli/recency-ordering-contract.md), [wt-cli/idle-staleness-contract.md](wt-cli/idle-staleness-contract.md), [wt-cli/create-output-phases.md](wt-cli/create-output-phases.md), [wt-cli/help-dump-contract.md](wt-cli/help-dump-contract.md) | diff --git a/docs/memory/wt-cli/help-dump-contract.md b/docs/memory/wt-cli/help-dump-contract.md new file mode 100644 index 0000000..b29f381 --- /dev/null +++ b/docs/memory/wt-cli/help-dump-contract.md @@ -0,0 +1,153 @@ +# wt-cli: Help-Dump Contract + +> Post-implementation behavior capture for the Hidden `wt help-dump` command. +> Source change: `260603-qqkj-help-dump-command`. + +This file documents the contract that `wt help-dump` honors. Future changes touching `src/internal/worktree/helpdump.go` or `src/cmd/wt/help_dump.go` should preserve these invariants unless an explicit spec amendment supersedes them. + +`help-dump` is one half of a **cross-repo contract** with shll.ai: shll.ai's command-reference page for `wt` is refreshed by a scheduled puller that runs `wt help-dump`, `brew install`s the tool, and commits the captured JSON. The output shape, field semantics, and `schema_version` are fixed by that contract and are NOT open to local reinterpretation — see "Upstream forward contract" below. `wt`'s sole obligation is to emit valid help-dump output to stdout; everything downstream (capture, `captured_at` stamping, schema validation, commit) is shll.ai's job. The prior **push** model (where `wt` would PR `help/wt.json` into shll.ai — backlog `[pc47]`) is **retired**; this command is the producer half of the inverted pull model and the push wiring was deliberately never built. + +## Requirements + +### Hidden subcommand, self-filtering + +- `help-dump` is declared `Hidden: true` in `helpDumpCmd()`. It NEVER appears in `wt -h`'s Available Commands list. +- Because it is Hidden, it also self-filters from its OWN output: the §4 filter drops every Hidden node, and `help-dump` is Hidden, so no special-case logic is needed to exclude it from the dumped tree. + +- **GIVEN** a built `wt` binary +- **WHEN** the user runs `wt -h` +- **THEN** `help-dump` SHALL NOT appear in the Available Commands list +- **AND** running `wt help-dump` SHALL still execute the command +- **AND** `help-dump` SHALL be absent from the emitted tree + +### Invocation contract: single JSON to stdout, empty stderr, exit 0 + +- On success, `wt help-dump` emits exactly ONE JSON envelope to **stdout** (pretty-printed via `json.MarshalIndent(doc, "", " ")` with a single trailing newline), writes **nothing to stderr**, and **exits 0**. +- On ANY error (builder failure, JSON marshal failure, write failure), the command returns the error via `RunE`; the root handler in `main.go` maps the non-nil error to a typed non-zero exit code (`wt.ExitGeneralError`, per Constitution III). `Args: cobra.NoArgs` — a positional argument is rejected with a non-zero exit. +- The puller treats a non-zero exit as a **failed capture** and MUST NOT clobber its last-good `help/wt.json`. The single-JSON-to-stdout / empty-stderr / exit-0 triad is therefore the load-bearing success signal; any stray stderr write or partial stdout on the success path would be a contract violation. + +- **GIVEN** the command runs successfully +- **WHEN** the JSON envelope is emitted +- **THEN** the process SHALL exit 0 with empty stderr and exactly one JSON document on stdout +- **AND** any internal failure SHALL surface a non-zero exit via the root handler + +### Envelope shape: exactly `{tool, version, schema_version, root}`, no `captured_at` + +- `HelpDoc` marshals to EXACTLY four keys: `tool`, `version`, `schema_version`, `root`. No more, no fewer. +- `tool` = `"wt"` (the `toolName` constant — the invoked binary name, not the file slug). +- `version` = the built binary's version, passed in from `main.version` (ldflags `-X main.version=...`). A plain `go build` reports `"dev"`; release builds inject the real version. It is NEVER hardcoded by the builder package. +- `schema_version` = the integer literal `1` (the `helpDumpSchemaVersion` constant; a Go `int`, not a string). Frozen for this contract revision. +- The envelope **MUST NOT** emit `captured_at`. `HelpDoc` has no `captured_at` field at all — not even an `omitempty` one, since adding it would drift from the contract. `captured_at` is **shll.ai-owned**: the puller stamps it post-capture. This is the deliberate asymmetry — the tool emits the structural help tree; shll.ai owns the timestamp. + +- **GIVEN** the command emits its envelope +- **WHEN** the JSON top-level object is parsed +- **THEN** the keys SHALL be exactly `tool`, `version`, `schema_version`, `root` +- **AND** `captured_at` SHALL be absent +- **AND** `tool` SHALL equal `"wt"` and `schema_version` SHALL equal integer `1` + +### Recursive Node shape: `{name, path, short, usage, text, commands}` + +- Each `HelpNode` marshals to six fields in contract order: + - `name` = `cmd.Name()` + - `path` = `cmd.CommandPath()` (full invocation, e.g. `"wt create"`) + - `short` = `cmd.Short` + - `usage` = `cmd.UseLine()` + - `text` = the raw `-h` render for that command (see below) + - `commands` = the array of child `HelpNode`s. +- `commands` is a **non-nil slice** (`make([]HelpNode, 0, ...)`), so a leaf marshals to `"commands": []`, never `null`. shll.ai's `NodeSchema` requires `z.array(NodeSchema)`; a `null` would fail validation. + +- **GIVEN** the root node +- **WHEN** marshaled +- **THEN** it SHALL carry the six fields in contract order +- **AND** a leaf command's `commands` SHALL serialize as `[]`, not `null` + +### `text` is the raw `-h` render, captured into a buffer, byte-for-byte + +- Each node's `text` is captured by `renderHelpText(cmd)`: it points `cmd.SetOut`/`cmd.SetErr` at a `bytes.Buffer`, invokes `cmd.Help()`, then returns `strings.TrimRight(buf.String(), "\n")` (the reference sample carries no trailing newline; Cobra's help render appends one, so it is trimmed to match byte-for-byte). +- `text` is NEVER produced by regex-parsing `-h` output, nor by manual `Long + UsageString()` composition — the capture-and-render-into-buffer method (verified byte-for-byte against the committed `help/wt.json` reference sample) is the mandated approach. + +- **GIVEN** the `create` node +- **WHEN** its `text` is compared to the reference sample's `create.text` +- **THEN** they SHALL be byte-identical (modulo `version`/`captured_at`) + +### Discovery: recursive walk of `rootCmd.Commands()`, never regex + +- The tree is discovered programmatically by `buildNode` walking `cmd.Commands()` recursively to **full depth**. It NEVER regex-parses `-h` text to discover structure. +- `wt` is currently flat (root + 7 visible leaves), but the walk recurses for correctness under any future nesting. + +- **GIVEN** the root command with its registered children +- **WHEN** the builder walks the tree +- **THEN** every non-filtered descendant SHALL appear at its correct depth + +### Filter rules: drop `completion`, `help`, and any Hidden node + +- `isFilteredCommand(cmd)` drops a node when ANY of: + - `cmd.Hidden == true` (this self-filters `help-dump`), OR + - `cmd.Name()` is `"completion"` or `"help"` (Cobra's auto-generated subcommands). +- Filtering is applied both when deciding which children to recurse into AND, during each node's text render, by temporarily detaching the filtered children so the rendered "Available Commands" listing matches the dumped tree (see "Render-time child detachment" below). + +- **GIVEN** the live command tree (which includes auto-generated `completion`, `help`, and the Hidden `help-dump`) +- **WHEN** the builder walks it +- **THEN** `completion`, `help`, and `help-dump` SHALL be absent from the output tree +- **AND** the remaining 7 visible subcommands (`create`, `delete`, `init`, `list`, `open`, `shell-init`, `update`) SHALL be present + +### Schema is frozen at `schema_version: 1` + +- `schema_version` stays the integer `1` for this contract revision. No new fields are added to the envelope or node shape in this change. +- Future enrichment is a separate, deliberate change and SHALL add new fields as **OPTIONAL** (so a consumer pinned to `schema_version: 1` keeps validating). A breaking shape change would bump `schema_version`. + +### Conformance to the reference sample and upstream schema + +- The emitted output, after shll.ai adds `captured_at` and with `version` normalized, SHALL validate against shll.ai's `HelpDocSchema`/`NodeSchema` and match the committed `help/wt.json` structure: same 7 subcommands, same field names/shapes, same `text`/`short`/`usage`/`path` per node. + +- **GIVEN** `wt help-dump` output with `version` normalized and `captured_at` inserted +- **WHEN** diffed against the committed reference sample +- **THEN** the structures SHALL be equivalent + +## Internal API + +- `worktree.BuildHelpDump(root *cobra.Command, version string) (HelpDoc, error)` is the single entry point. The builder takes the root command and the version as arguments because the root is constructed in package `main` and the internal package cannot reach it otherwise — this keeps the tree-walk/envelope logic in `internal/worktree/` (Constitution V) while `main` retains ownership of command registration. +- The `cmd/wt/help_dump.go` layer is **thin** (Constitution V): `helpDumpCmd()` only wires Cobra to the builder, passing `cmd.Root()` and the package-`main` `version`, marshals the result, writes JSON to `cmd.OutOrStdout()`, and returns any error via `RunE`. +- `HelpDoc` / `HelpNode` are exported structs; `buildNode`, `initHelpTree`, `isFilteredCommand`, and `renderHelpText` are unexported helpers internal to the builder. + +## Design Decisions + +### Builder takes the root `*cobra.Command` as input + +`BuildHelpDump(root *cobra.Command, version string)` receives the root and version rather than constructing its own tree. The root is owned by package `main`; passing it keeps tree-walk/envelope logic in `internal/worktree/` per Constitution V. Building the tree inside `cmd/` was rejected (violates V); having the builder construct its own root was rejected (would duplicate command registration and drift from the live tree). (Source: change qqkj, plan Design Decision 1.) + +### Initialize Cobra's lazy help affordances across the whole tree before rendering + +Cobra adds the `-h, --help` flag, the `-v, --version` flag, and the auto-generated `help`/`completion` subcommands **lazily** — normally during `Execute()`, and only on the command actually being run. When `help-dump` is the executed command, the *root* is initialized but its descendants are not: each leaf's rendered `-h` would omit the `-h, --help` line and drop the `[flags]` suffix from its `UseLine()`, and the root's `-h` would lack `-v, --version`. `initHelpTree(root)` walks the whole tree up front and calls `InitDefaultHelpFlag`, `InitDefaultVersionFlag` (a no-op when `cmd.Version` is empty), `InitDefaultHelpCmd` (on commands with children), and `InitDefaultCompletionCmd` (root only) so every rendered `-h` matches a real `command -h` invocation and the reference sample. All initializers are idempotent. (Source: change qqkj.) + +### Render-time child detachment, not a `Hidden` toggle + +`renderHelpText` temporarily **detaches** filtered children (`completion`/`help`/Hidden) via `cmd.RemoveCommand(...)` during the buffer render, then re-attaches them with a `defer cmd.AddCommand(...)`. This makes each command's rendered "Available Commands" listing reflect the dumped tree (matching the reference sample, which omits those entries). Detachment is required rather than toggling `Hidden`: Cobra's usage template special-cases the `help` command with an explicit `(eq .Name "help")` clause that lists it even when Hidden — only removing it from the children slice keeps it out of the listing. The detached children are re-attached before returning (Cobra re-sorts on `AddCommand`, restoring order), and the `SetOut`/`SetErr` overrides are restored via `defer`, so the **live tree is provably unmutated** — a normal `wt -h` for real users is unaffected after a dump (asserted by `TestBuildHelpDump_RestoresLiveTree`). (Source: change qqkj.) + +### `commands` is a non-nil slice (`[]`, not `null`) + +`HelpNode.Commands` is initialized to `make([]HelpNode, 0, ...)` so a leaf marshals to `"commands": []`. shll.ai's `NodeSchema` is `z.array(NodeSchema)`; a `null` (which a nil slice would produce) fails validation. (Source: change qqkj, plan Design Decision 3.) + +### No `captured_at` field on the struct at all + +`HelpDoc` deliberately has no `captured_at` field — not even an `omitempty` one. The contract §3 asymmetry is that the tool emits the structural tree and shll.ai stamps the timestamp post-capture; adding the field (even unset) would risk emitting it and drift from the contract. The tool's envelope is structurally incapable of carrying `captured_at`. (Source: change qqkj, intake assumption #2.) + +## Upstream forward contract + +- The authoritative cross-repo contract lives in shll.ai: `docs/specs/help-dump-contract.md`. That document (its §1–§8) defines invocation, envelope, node shape, filtering, discovery, version sourcing, and schema-freeze rules; this memory file captures `wt`'s conforming implementation of it. When the two diverge, the shll.ai contract is authoritative. +- The machine-checkable conformance anchor is shll.ai's `sites/astro-starlight-terminal1/src/lib/schemas.ts` — `HelpDocSchema` and `NodeSchema`. After the puller stamps `captured_at`, `wt help-dump` output MUST validate against these Zod schemas. A change to `wt`'s output shape that breaks those schemas is a contract violation on `wt`'s side. + +## Cross-references + +- Source: `src/internal/worktree/helpdump.go` — `HelpDoc`, `HelpNode`, `BuildHelpDump`, `initHelpTree`, `buildNode`, `isFilteredCommand`, `renderHelpText`, the `helpDumpSchemaVersion = 1` and `toolName = "wt"` constants. `src/cmd/wt/help_dump.go` — `helpDumpCmd()` (thin Cobra wiring). `src/cmd/wt/main.go` — registers `helpDumpCmd()` on root; owns `var version = "dev"` (ldflags-injected). +- Tests: `src/internal/worktree/helpdump_test.go` — `TestBuildHelpDump_Envelope`, `TestBuildHelpDump_OmitsCapturedAt`, `TestBuildHelpDump_FiltersCompletionHelpHidden`, `TestBuildHelpDump_RecursiveDiscovery`, `TestBuildHelpDump_NodeShape` (asserts `-h, --help` line present + leaf `commands: []`), `TestBuildHelpDump_RestoresLiveTree` (live tree unmutated). `src/cmd/wt/help_dump_test.go` — `TestHelpDump_EmitsValidEnvelope` (exit 0, empty stderr, exactly the four top-level keys, no `captured_at`, 7 subcommands, banned names absent), `TestHelpDump_HiddenFromRootHelp`, `TestHelpDump_RejectsArgs` (`cobra.NoArgs`). +- Constitution: Principle II (Cobra command surface — `RunE`, `SilenceUsage`/`SilenceErrors` inherited from root), III (Typed exit codes — builder/marshal errors map to `ExitGeneralError`), IV (test what the user sees — builder unit test + command-level test), V (internal package boundary — tree-walk/envelope logic in `internal/worktree`, `cmd/` thin). +- Sibling memory: `wt-cli/init-failure-contract.md`, `wt-cli/list-status-contract.md`, `wt-cli/update-command-contract.md` — same pattern of post-change invariant capture for other `wt` subcommands. `update-command-contract.md` documents another **cross-toolkit** contract (`--skip-brew-update`) whose semantics are likewise fixed externally and not open to local reinterpretation. +- Backlog: `[pc47]` is marked **superseded by qqkj** in `fab/backlog.md` — its producer half is realized here; its push half (build-time CI step, PR-opening into sahil87/shll.ai, auto-merge, `SHLLAI_TOKEN`) was intentionally dropped per the pull-model inversion. +- Upstream: shll.ai `docs/specs/help-dump-contract.md` (authoritative cross-repo contract) and `sites/astro-starlight-terminal1/src/lib/schemas.ts` (`HelpDocSchema`/`NodeSchema`, machine-checkable conformance anchor). + +## Changelog + +| Change | Date | Summary | +|--------|------|---------| +| `260603-qqkj-help-dump-command` | 2026-06-03 | Added the Hidden `wt help-dump` Cobra subcommand for shll.ai's scheduled pull integration. Emits a single JSON envelope to stdout (exactly `{tool, version, schema_version, root}`; `tool=="wt"`, `version` from `main.version` ldflags, `schema_version` integer `1`) with empty stderr and exit 0 on success; non-zero (typed via `RunE` → `ExitGeneralError`) on any error so the puller treats it as a failed capture. The envelope deliberately OMITS `captured_at` (shll.ai stamps it post-capture). Recursive `HelpNode` shape `{name, path, short, usage, text, commands}` with `text` = the raw `-h` render captured into a buffer (trailing newline trimmed, byte-for-byte vs the reference sample) and `commands` a non-nil slice (`[]` for leaves, never `null`). Tree discovered by recursively walking `rootCmd.Commands()` (never regex-parsing `-h`), filtering `completion`, `help`, and any Hidden node (self-filtering `help-dump`). Two Cobra subtleties handled: `initHelpTree` initializes the lazily-added help/version/completion affordances across the whole tree before rendering, and `renderHelpText` temporarily detaches filtered children during each node's render (working around Cobra's `help`-special-casing usage template) then restores them so the live tree is unmutated. Logic lives in `src/internal/worktree/helpdump.go` (`BuildHelpDump`); thin wiring in `src/cmd/wt/help_dump.go`. Superseded the obsolete push half of backlog `[pc47]`. | diff --git a/fab/backlog.md b/fab/backlog.md index f016baf..c9f13a2 100644 --- a/fab/backlog.md +++ b/fab/backlog.md @@ -1,3 +1,3 @@ ## Backlog -- [ ] [pc47] 2026-06-02: Add a build-time 'help-dump' step that emits wt's CLI help tree as help/wt.json and PRs it into sahil87/shll.ai (the shll.ai landing site renders it as an expandable 'Command reference' on the wt tool page). CONTRACT (frozen — copy the reference sample committed at sahil87/shll.ai path help/wt.json, which was generated from THIS binary): JSON shape is {tool, version, captured_at (ISO-8601 UTC), schema_version: 1, root: Node} where Node = {name, path (full invocation e.g. 'wt create'), short (one-line desc), usage, text (the RAW -h output byte-for-byte, newlines preserved), commands: Node[] (recursive; empty array = leaf)}. PRODUCER (wt is Cobra/Go, binary 'wt', main at src/cmd/wt): walk the cobra command tree programmatically (rootCmd.Commands() recursively), NOT regex-parsing -h text; per node capture cmd.Name / cmd.CommandPath() / cmd.Short / cmd.UseLine() and cmd.UsageString() (or Long+UsageString) as 'text'. FILTER OUT cobra's auto-generated 'completion' and 'help' subcommands and any cmd.Hidden==true. VERSION: read from the built binary (rootCmd.Version / ldflags) — the committed sample uses a placeholder '1.4.2' because a plain 'go build' reports 'dev'; the CI producer MUST inject the real version. PUSH: in CI after build, run the dump, write help/wt.json, validate it parses, then open a PR into sahil87/shll.ai using the existing repo secret SHLLAI_TOKEN (contents + pull-request write) with auto-merge enabled (PR, not direct push to main, to avoid the multi-repo push race). wt is the reference tool for this 7-tool rollout (its sample help/wt.json is what the other six copy from); the shll.ai site-side consumer (Astro loader + reference UI) is tracked separately in the shll.ai repo. +- [x] [pc47] 2026-06-02: ~~Add a build-time 'help-dump' step that emits wt's CLI help tree as help/wt.json and PRs it into sahil87/shll.ai (the shll.ai landing site renders it as an expandable 'Command reference' on the wt tool page). CONTRACT (frozen — copy the reference sample committed at sahil87/shll.ai path help/wt.json, which was generated from THIS binary): JSON shape is {tool, version, captured_at (ISO-8601 UTC), schema_version: 1, root: Node} where Node = {name, path (full invocation e.g. 'wt create'), short (one-line desc), usage, text (the RAW -h output byte-for-byte, newlines preserved), commands: Node[] (recursive; empty array = leaf)}. PRODUCER (wt is Cobra/Go, binary 'wt', main at src/cmd/wt): walk the cobra command tree programmatically (rootCmd.Commands() recursively), NOT regex-parsing -h text; per node capture cmd.Name / cmd.CommandPath() / cmd.Short / cmd.UseLine() and cmd.UsageString() (or Long+UsageString) as 'text'. FILTER OUT cobra's auto-generated 'completion' and 'help' subcommands and any cmd.Hidden==true. VERSION: read from the built binary (rootCmd.Version / ldflags) — the committed sample uses a placeholder '1.4.2' because a plain 'go build' reports 'dev'; the CI producer MUST inject the real version. PUSH: in CI after build, run the dump, write help/wt.json, validate it parses, then open a PR into sahil87/shll.ai using the existing repo secret SHLLAI_TOKEN (contents + pull-request write) with auto-merge enabled (PR, not direct push to main, to avoid the multi-repo push race). wt is the reference tool for this 7-tool rollout (its sample help/wt.json is what the other six copy from); the shll.ai site-side consumer (Astro loader + reference UI) is tracked separately in the shll.ai repo.~~ — **SUPERSEDED by qqkj (2026-06-03)**: the `wt help-dump` producer command is implemented in change `260603-qqkj-help-dump-command`. The push half (build-time CI step, PR-opening into sahil87/shll.ai, auto-merge, `SHLLAI_TOKEN`) is intentionally **dropped** — shll.ai's help-dump contract inverted the integration to a scheduled *pull* model (shll.ai runs `wt help-dump` itself), retiring the push wiring. diff --git a/fab/changes/260603-qqkj-help-dump-command/.history.jsonl b/fab/changes/260603-qqkj-help-dump-command/.history.jsonl new file mode 100644 index 0000000..265f9b0 --- /dev/null +++ b/fab/changes/260603-qqkj-help-dump-command/.history.jsonl @@ -0,0 +1,20 @@ +{"action":"enter","driver":"fab-new","event":"stage-transition","stage":"intake","ts":"2026-06-03T07:51:49Z"} +{"args":"There's an update in the way we integrate with shll.ai (help-dump-contract.md teardown directive). shll.ai now PULLS help via 'wt help-dump' instead of the retired push model. Implement the wt help-dump Cobra command per the contract (Hidden, walks tree recursively, emits {tool,version,schema_version:1,root} to stdout, no captured_at). Supersedes backlog pc47 (push producer).","cmd":"fab-new","event":"command","ts":"2026-06-03T07:51:49Z"} +{"delta":"+1.8","event":"confidence","score":1.8,"trigger":"calc-score","ts":"2026-06-03T07:52:54Z"} +{"delta":"+0.0","event":"confidence","score":1.8,"trigger":"calc-score","ts":"2026-06-03T07:53:00Z"} +{"cmd":"fab-clarify","event":"command","ts":"2026-06-03T07:54:44Z"} +{"delta":"+3.2","event":"confidence","score":5,"trigger":"calc-score","ts":"2026-06-03T14:22:14Z"} +{"delta":"+0.0","event":"confidence","score":5,"trigger":"calc-score","ts":"2026-06-03T14:22:21Z"} +{"delta":"+0.0","event":"confidence","score":5,"trigger":"calc-score","ts":"2026-06-03T14:22:28Z"} +{"delta":"+0.0","event":"confidence","score":5,"trigger":"calc-score","ts":"2026-06-03T14:22:39Z"} +{"delta":"+0.0","event":"confidence","score":5,"trigger":"calc-score","ts":"2026-06-03T14:22:42Z"} +{"cmd":"fab-fff","event":"command","ts":"2026-06-03T14:22:53Z"} +{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"apply","ts":"2026-06-03T14:23:17Z"} +{"cmd":"fab-continue","event":"command","ts":"2026-06-03T14:24:28Z"} +{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"review","ts":"2026-06-03T14:34:53Z"} +{"cmd":"fab-continue","event":"command","ts":"2026-06-03T14:35:34Z"} +{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"hydrate","ts":"2026-06-03T14:37:47Z"} +{"event":"review","result":"passed","ts":"2026-06-03T14:37:47Z"} +{"cmd":"fab-continue","event":"command","ts":"2026-06-03T14:38:39Z"} +{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"ship","ts":"2026-06-03T14:40:18Z"} +{"cmd":"git-pr","event":"command","ts":"2026-06-03T14:41:26Z"} diff --git a/fab/changes/260603-qqkj-help-dump-command/.status.yaml b/fab/changes/260603-qqkj-help-dump-command/.status.yaml new file mode 100644 index 0000000..d9e770e --- /dev/null +++ b/fab/changes/260603-qqkj-help-dump-command/.status.yaml @@ -0,0 +1,49 @@ +id: qqkj +name: 260603-qqkj-help-dump-command +created: 2026-06-03T07:51:49Z +created_by: sahil87 +change_type: feat +issues: [] +progress: + intake: done + apply: done + review: done + hydrate: done + ship: active + review-pr: pending +plan: + generated: true + task_count: 7 + acceptance_count: 15 + acceptance_completed: 0 +confidence: + certain: 13 + confident: 0 + tentative: 0 + unresolved: 0 + score: 5.0 + fuzzy: true + dimensions: + signal: 95.9 + reversibility: 74.2 + competence: 88.3 + disambiguation: 86.1 +stage_metrics: + intake: {started_at: "2026-06-03T07:51:49Z", driver: fab-new, iterations: 1, completed_at: "2026-06-03T14:23:17Z"} + apply: {started_at: "2026-06-03T14:23:17Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-03T14:34:53Z"} + review: {started_at: "2026-06-03T14:34:53Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-03T14:37:47Z"} + hydrate: {started_at: "2026-06-03T14:37:47Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-03T14:40:18Z"} + ship: {started_at: "2026-06-03T14:40:18Z", driver: fab-fff, iterations: 1} +prs: [] +true_impact: + added: 0 + deleted: 0 + net: 0 + excluding: + added: 0 + deleted: 0 + net: 0 + computed_at: "2026-06-03T14:40:18Z" + computed_at_stage: hydrate +# true_impact: lazily created on first apply-finish (no placeholder here). +last_updated: 2026-06-03T14:40:18Z diff --git a/fab/changes/260603-qqkj-help-dump-command/intake.md b/fab/changes/260603-qqkj-help-dump-command/intake.md new file mode 100644 index 0000000..a1b5d9a --- /dev/null +++ b/fab/changes/260603-qqkj-help-dump-command/intake.md @@ -0,0 +1,186 @@ +# Intake: Implement `wt help-dump` for shll.ai pull integration + +**Change**: 260603-qqkj-help-dump-command +**Created**: 2026-06-03 +**Status**: Draft + +## Origin + +> /fab-new: "There's an update in the way we integrate with shll.ai. To understand it read +> https://github.com/sahil87/shll.ai/blob/main/docs/specs/help-dump-contract.md#teardown-directive-paste-to-a-tool-repo-agent . +> Implement the change." + +**Mode**: Conversational (one clarifying question asked before folder creation). + +The linked anchor is the **Teardown directive** in shll.ai's `help-dump-contract.md`. It instructs +a tool repo to *remove its now-dead push wiring* (producer CI, PR-opening step, auto-merge, +`SHLLAI_TOKEN`) because shll.ai has inverted the integration: it now **pulls** help by running +` help-dump` on a schedule, rather than each tool *pushing* a `help/.json` PR. + +**Gap analysis (decisive)**: A whole-repo grep showed `wt` has **none** of the push wiring the +directive says to remove — no producer CI, no PR-opening step, no auto-merge, no `SHLLAI_TOKEN` +usage (it appears only inside `fab/backlog.md`), and crucially **no `help-dump` command in +`src/`**. Backlog item `[pc47]` *proposed* building the push producer but it was never +implemented. So the literal teardown is a no-op here. + +Meanwhile the pull model the directive depends on **requires** `wt help-dump` to emit valid JSON, +and shll.ai already commits a reference `help/wt.json` (6514 bytes) described as "generated from +THIS binary". Under the new pull model, shll.ai running `wt help-dump` would fail outright today. + +**Decision** (user-confirmed via clarifying question): scope this change as **implement the +`wt help-dump` command** per the contract — the genuinely missing, load-bearing obligation — and +treat it as **superseding the obsolete push half of backlog `[pc47]`**. The push producer / PR / +token wiring from `[pc47]` is dropped, not built. + +## Why + +1. **Problem**: shll.ai's command-reference page for `wt` is now refreshed by a scheduled puller + that runs `wt help-dump`, `brew install`s the tool, and commits the captured JSON. `wt` has no + `help-dump` command, so that pull fails and the `wt` command reference will silently freeze + (stale-help gap) — the exact failure the contract's precondition warns about, but from the + producer side. +2. **Consequence if unfixed**: the `wt` tool page on shll.ai shows stale help forever; `wt` is + named the *reference tool* for the 7-tool rollout, so its absence is conspicuous. +3. **Why this approach over alternatives**: The contract is explicit and frozen at + `schema_version: 1`. The tool's *single obligation* is "emit valid `help-dump` output to + stdout"; everything downstream (capture, timestamping, validation, commit) is shll.ai's job. + Building the producer-side command (not a transport) is the minimal, correct way to satisfy + the pull contract. The retired push model (backlog `[pc47]`) is deliberately *not* built — it + would add a cross-repo write token, PR-opening, and auto-merge that the contract has retired. + +## What Changes + +### New: `wt help-dump` Cobra subcommand + +A new subcommand registered on the root command in `src/cmd/wt/main.go`, implemented in a new +`src/cmd/wt/help_dump.go` (with `src/cmd/wt/help_dump_test.go`). Per Constitution V, the tree-walk +and envelope-building logic belongs under `src/internal/worktree/` and is exercised through the +thin `cmd/` layer; `cmd/help_dump.go` only wires Cobra → the internal builder and writes JSON to +stdout. + +**Behavior, per `help-dump-contract.md` §1–§8:** + +- **§2 Hidden**: declared `Hidden: true` so it never appears in `wt -h`. +- **§1 Invocation**: emits a single JSON envelope to **stdout**; **stderr empty** on success; + **exit 0** on success; non-zero on any error (so the puller treats it as a failed capture). +- **§3 Envelope** (tool-emitted shape — note the `captured_at` asymmetry): emit exactly + `{tool, version, schema_version, root}` and **MUST NOT emit `captured_at`** (shll.ai stamps it + post-capture). `tool` = `"wt"` (binary name); `version` = the built binary's version + (`rootCmd.Version` / ldflags `main.version`), never hardcoded; `schema_version` = integer `1`. +- **§3 Node** (recursive): each node is `{name, path, short, usage, text, commands: []}` where: + - `name` = `cmd.Name()` + - `path` = `cmd.CommandPath()` (e.g. `"wt create"`) + - `short` = `cmd.Short` + - `usage` = `cmd.UseLine()` + - `text` = the **raw `-h` output byte-for-byte** for that command (Long + Usage + Flags as + Cobra renders `-h`), newlines preserved — see Open Questions on the exact capture method. + - `commands` = child Nodes, `[]` for a leaf. +- **§4 Filter**: drop Cobra's auto-generated `completion` and `help` subcommands, and any + `cmd.Hidden == true` node — which now includes `help-dump` itself (so §2 + §4 make it + self-filtering for free; no special-case logic). +- **§5 Discovery**: walk `rootCmd.Commands()` **recursively to full depth**; NEVER regex-parse + `-h` text to discover structure. (`wt` is currently flat — root + 7 leaves — but the walk must + be recursive to stay correct if nesting is added.) +- **§6 Version**: read from the built binary; the committed sample's `1.4.2` is a placeholder — + real builds inject via ldflags (already wired: `main.version`). +- **§8 Schema**: keep `schema_version: 1`; do not add fields (future enrichment is a separate, + optional-field change). + +**Conformance target**: output MUST validate against `HelpDocSchema`/`NodeSchema` in shll.ai's +`sites/astro-starlight-terminal1/src/lib/schemas.ts` *after the puller stamps `captured_at`*, and +match the structure of the committed `help/wt.json` reference sample (same 7 subcommands, same +field shapes). + +### Test (Constitution IV + contract directive) + +Add a test that exercises `help-dump` directly (the contract explicitly asks for one now that no +push CI implicitly exercises it): asserts exit 0, stdout is valid JSON, `tool == "wt"`, +`schema_version == 1`, **no `captured_at` key present**, `completion`/`help`/`help-dump` absent +from the tree, and the structured fields match the live Cobra tree. Per project Test Strategy +(`test-alongside`) and the file-per-source convention, this lives in `help_dump_test.go` (unit) +with internal-package coverage for the builder; an integration assertion in +`cmd/integration_test.go` may exercise the built binary end-to-end. + +### Out of scope (explicitly NOT built) + +- The retired **push** wiring from backlog `[pc47]`: producer CI that opens a `help/wt.json` PR + into `sahil87/shll.ai`, auto-merge, and `SHLLAI_TOKEN`. The pull model retires all of it. +- The shll.ai-side puller, `captured_at` stamping, JSON validation, and commit — all shll.ai's + responsibility per the contract. +- Schema enrichment (new optional fields) — a separate future change; stay at `schema_version: 1`. + +### Backlog hygiene + +Backlog `[pc47]` SHALL be marked done/superseded by this change (`qqkj`) in `fab/backlog.md` — its +valuable producer half is realized here; its push half (PR-opening, auto-merge, `SHLLAI_TOKEN`) is +intentionally dropped per the contract inversion. + +## Affected Memory + +- `wt-cli/help-dump-contract`: (new) Behavior contract for the `wt help-dump` command — Hidden, + stdout JSON envelope `{tool, version, schema_version:1, root}` (no `captured_at`), recursive + tree walk, `completion`/`help`/Hidden filtering, exit-0/empty-stderr success semantics. Points + at shll.ai's `help-dump-contract.md` as the upstream forward contract. + +## Impact + +- **Code**: + - `src/cmd/wt/main.go` — register `helpDumpCmd()` on the root command. + - `src/cmd/wt/help_dump.go` (new) — thin Cobra wiring; calls the internal builder, writes JSON + to stdout, returns `error` via `RunE`. + - `src/cmd/wt/help_dump_test.go` (new) — command-level test. + - `src/internal/worktree/` (new file, e.g. `helpdump.go` + `helpdump_test.go`) — tree-walk + + envelope builder per Constitution V (logic out of `cmd/`). + - `src/cmd/wt/integration_test.go` — optional end-to-end assertion. +- **Specs**: `docs/specs/cli-surface.md` documents the per-subcommand surface; `help-dump` is + Hidden so its placement there is informational. A short note (or the new memory file) records + the contract. `docs/specs/index.md` may gain a row if a dedicated spec is warranted. +- **Dependencies**: none new — uses existing `spf13/cobra` and stdlib `encoding/json`. +- **Exit codes** (Constitution III): map any build error to a typed exit code from + `internal/worktree/errors.go` (e.g. `ExitGeneralError`) via `RunE` + the root handler. +- **Cross-repo**: producer-side only; no write path into shll.ai. shll.ai consumes via its puller. + +## Open Questions + +- ~~**Exact `text` capture method**~~ — **Resolved** (clarify 2026-06-03): capture each node's + `text` by rendering its `-h` output into a buffer (Cobra's help func / `cmd.SetOut(buf)`), + then verify **byte-for-byte** against the committed `help/wt.json` during planning/apply. + + +## Clarifications + +### Session 2026-06-03 + +| # | Action | Detail | +|---|--------|--------| +| 12 | Changed | Capture `text` by rendering `-h` into a buffer, verified byte-for-byte vs committed `help/wt.json` (over manual Long+UsageString composition) | +| 13 | Confirmed | Mark backlog `[pc47]` superseded by qqkj as part of this change | + +### Session 2026-06-03 (bulk confirm) + +| # | Action | Detail | +|---|--------|--------| +| 8 | Confirmed | — | +| 9 | Confirmed | — | +| 10 | Confirmed | — | +| 11 | Confirmed | — | + +## Assumptions + +| # | Grade | Decision | Rationale | Scores | +|---|-------|----------|-----------|--------| +| 1 | Certain | Scope = implement `wt help-dump` (producer command), not the literal teardown | Gap analysis: wt has zero push wiring to remove and no `help-dump` command; user confirmed this scope via clarifying question | S:95 R:70 A:90 D:90 | +| 2 | Certain | Emit `{tool, version, schema_version, root}` to stdout and OMIT `captured_at` | Contract §3 is explicit: `captured_at` is shll.ai-owned, stamped post-capture; tool must not emit it | S:98 R:80 A:95 D:98 | +| 3 | Certain | `schema_version` is integer literal `1`; add no new fields | Contract §8: frozen at 1 for this revision; enrichment is a separate optional-field change | S:98 R:85 A:95 D:98 | +| 4 | Certain | `Hidden: true`; self-filters via §4 Hidden-drop (no special-case) | Contract §2 + §4 spell this out exactly | S:95 R:85 A:95 D:95 | +| 5 | Certain | Filter `completion`, `help`, and any `Hidden` node from the tree | Contract §4 enumerates the three drop rules | S:98 R:85 A:95 D:98 | +| 6 | Certain | Discover the tree by walking `rootCmd.Commands()` recursively; never regex `-h` | Contract §5 mandates programmatic, full-depth discovery | S:98 R:80 A:95 D:95 | +| 7 | Certain | `version` from the built binary (`main.version` ldflags), never hardcoded | Contract §6; ldflags injection already wired in main.go (`var version = "dev"`) | S:95 R:80 A:98 D:95 | +| 8 | Certain | Tree-walk/envelope logic lives in `internal/worktree/`; `cmd/` stays thin | Clarified — user confirmed | S:95 R:65 A:90 D:80 | +| 9 | Certain | Add a dedicated `help-dump` test (exit 0, valid JSON, tool/schema_version, no captured_at) | Clarified — user confirmed | S:95 R:70 A:90 D:85 | +| 10 | Certain | Do NOT build backlog `[pc47]`'s push producer/PR/auto-merge/`SHLLAI_TOKEN` | Clarified — user confirmed | S:95 R:60 A:90 D:85 | +| 11 | Certain | Conform output to committed `help/wt.json` + `schemas.ts` (validate after captured_at added) | Clarified — user confirmed | S:95 R:70 A:90 D:85 | +| 12 | Certain | Capture `text` by rendering each node's `-h` into a buffer, verified byte-for-byte against the committed `help/wt.json` | Clarified — user chose capture-and-verify over manual Long+UsageString composition | S:95 R:55 A:65 D:55 | +| 13 | Certain | Mark backlog `[pc47]` superseded by qqkj (push half dropped per the inversion) | Clarified — user confirmed backlog hygiene | S:95 R:80 A:60 D:60 | + +13 assumptions (13 certain, 0 confident, 0 tentative, 0 unresolved). diff --git a/fab/changes/260603-qqkj-help-dump-command/plan.md b/fab/changes/260603-qqkj-help-dump-command/plan.md new file mode 100644 index 0000000..f9fa37e --- /dev/null +++ b/fab/changes/260603-qqkj-help-dump-command/plan.md @@ -0,0 +1,174 @@ +# Plan: Implement `wt help-dump` for shll.ai pull integration + +**Change**: 260603-qqkj-help-dump-command +**Status**: In Progress +**Intake**: `intake.md` + +## Requirements + +### help-dump: Command Surface & Wiring + +#### R1: Hidden Cobra subcommand registered on root +A `helpDumpCmd() *cobra.Command` constructor SHALL be added in `src/cmd/wt/help_dump.go` and registered on the root command in `src/cmd/wt/main.go` via `root.AddCommand(...)`. The command SHALL set `Hidden: true`. Per Constitution II it SHALL inherit `SilenceUsage`/`SilenceErrors` (root sets these) and return errors via `RunE`. Per Constitution V the `cmd/` layer SHALL be thin: it only invokes the internal builder and writes JSON to stdout. + +- **GIVEN** a built `wt` binary +- **WHEN** the user runs `wt -h` +- **THEN** `help-dump` SHALL NOT appear in the Available Commands list (it is Hidden) +- **AND** running `wt help-dump` SHALL still execute the command + +#### R2: Typed exit code on error +Any error from the builder or JSON marshal SHALL be returned via `RunE`; the root handler in `main.go` maps a non-nil error to `wt.ExitGeneralError` (exit 1). On success the command SHALL exit 0. + +- **GIVEN** the command runs successfully +- **WHEN** the JSON envelope is emitted +- **THEN** the process SHALL exit 0 with empty stderr +- **AND** any internal failure SHALL surface a non-zero exit via the root handler + +### help-dump: Envelope & Node Shape (contract §3) + +#### R3: Top-level envelope omits captured_at +The builder SHALL produce an envelope marshaling EXACTLY to `{tool, version, schema_version, root}`. It SHALL NOT emit `captured_at` (shll.ai stamps it post-capture). `tool` = `"wt"`; `version` = the built binary's version (passed in from `main.version` / `rootCmd.Version`), never hardcoded; `schema_version` = integer literal `1`. + +- **GIVEN** the command emits its envelope +- **WHEN** the JSON is parsed +- **THEN** keys SHALL be exactly `tool`, `version`, `schema_version`, `root` +- **AND** `captured_at` SHALL be absent +- **AND** `schema_version` SHALL equal integer `1` + +#### R4: Recursive Node shape +Each node SHALL marshal to `{name, path, short, usage, text, commands}` where `name`=`cmd.Name()`, `path`=`cmd.CommandPath()`, `short`=`cmd.Short`, `usage`=`cmd.UseLine()`, `text`= the raw `-h` render for that command, `commands`= the array of child Nodes (`[]`, never `null`, for a leaf). + +- **GIVEN** the root node +- **WHEN** marshaled +- **THEN** it SHALL carry the six fields in contract order +- **AND** a leaf command's `commands` SHALL serialize as `[]`, not `null` + +#### R5: `text` is the raw `-h` render, byte-for-byte +Each node's `text` SHALL be captured by rendering that command's help into a buffer (set the command's output to a `bytes.Buffer` and invoke Cobra's help rendering), trimmed of any trailing newline, matching the committed `help/wt.json` reference sample byte-for-byte (modulo `version`/`captured_at`). It SHALL NOT be produced by regex-parsing or manual Long+UsageString composition. + +- **GIVEN** the `create` node +- **WHEN** its `text` is compared to the reference sample's `create.text` +- **THEN** they SHALL be byte-identical + +### help-dump: Discovery & Filtering (contract §4, §5) + +#### R6: Recursive full-depth discovery +The tree SHALL be discovered by walking `rootCmd.Commands()` recursively to full depth — never by parsing `-h` text. (wt is currently flat: root + 7 leaves, but the walk SHALL recurse for correctness under future nesting.) + +- **GIVEN** the root command with its registered children +- **WHEN** the builder walks the tree +- **THEN** every non-filtered descendant SHALL appear at its correct depth + +#### R7: Filter completion, help, and Hidden nodes +The walk SHALL DROP Cobra's auto-generated `completion` and `help` subcommands and any node with `cmd.Hidden == true`. Because `help-dump` is itself Hidden (R1), this rule self-filters it with no special-case logic. + +- **GIVEN** the live command tree (which includes auto-generated `completion`, `help`, and the Hidden `help-dump`) +- **WHEN** the builder walks it +- **THEN** `completion`, `help`, and `help-dump` SHALL be absent from the output tree +- **AND** the remaining 7 subcommands SHALL be present + +### help-dump: Conformance & Test (contract §8, Constitution IV) + +#### R8: Conformance to reference sample and schema +The emitted output, after shll.ai adds `captured_at`, SHALL validate against `HelpDocSchema`/`NodeSchema` and match the committed `help/wt.json` structure: same 7 subcommands, same field names/shapes, same `text`/`short`/`usage`/`path` per node. `schema_version` SHALL stay `1`; no new fields. + +- **GIVEN** `wt help-dump` output with `version` normalized and `captured_at` inserted +- **WHEN** diffed against the committed reference sample +- **THEN** the structures SHALL be equivalent + +#### R9: Dedicated tests for the command and builder +Tests SHALL be added (test-alongside): a builder test in `src/internal/worktree/helpdump_test.go` and a command test in `src/cmd/wt/help_dump_test.go` asserting exit 0, valid JSON, `tool=="wt"`, `schema_version==1`, no `captured_at` key, and `completion`/`help`/`help-dump` absent from the tree. + +- **GIVEN** the test suite +- **WHEN** `go test ./cmd/... ./internal/...` runs +- **THEN** the new tests SHALL pass + +### Non-Goals + +- Push wiring from backlog `[pc47]` (producer CI, PR-opening, auto-merge, `SHLLAI_TOKEN`) — retired by the pull-model inversion; explicitly NOT built. +- shll.ai-side puller, `captured_at` stamping, JSON validation, commit — shll.ai's responsibility. +- Schema enrichment (new optional fields) — separate future change; stay at `schema_version: 1`. + +### Design Decisions + +1. **Builder takes a `*cobra.Command` (the root) as input**: the root is constructed in package `main`; the internal builder cannot reach it otherwise. — *Why*: keeps tree-walk/envelope logic in `internal/worktree/` (Constitution V) while the root stays owned by `main`. — *Rejected*: building the tree inside `cmd/` (violates V); having the builder construct its own root (would duplicate command registration and drift). +2. **`text` captured via buffer render of `cmd.Help()`**: set `cmd.SetOut(buf)` + `cmd.SetErr(buf)` and invoke the command's help func, then `strings.TrimRight(..., "\n")`. — *Why*: contract §3 + intake assumption #12 mandate the raw `-h` render verified byte-for-byte. — *Rejected*: manual `Long+UsageString` composition (drifts from real `-h`; intake explicitly disfavors it). +3. **`commands` is a non-nil slice**: initialize `[]Node{}` so leaves marshal to `[]` not `null`, matching `NodeSchema`/sample. — *Why*: schema is `z.array(NodeSchema)`; `null` would fail validation. + +## Tasks + +### Phase 1: Core Implementation (internal builder) + +- [x] T001 Add `src/internal/worktree/helpdump.go`: define exported `HelpDoc` envelope struct (`tool`, `version`, `schema_version`, `root` JSON tags; NO `captured_at` field) and `HelpNode` struct (`name`, `path`, `short`, `usage`, `text`, `commands` JSON tags), plus `BuildHelpDump(root *cobra.Command, version string) (HelpDoc, error)` and a recursive `buildNode(cmd *cobra.Command) (HelpNode, error)` helper that walks children, filters `completion`/`help`/Hidden, captures `text` via buffer-rendered help, and returns `[]HelpNode{}` for leaves. + +### Phase 2: Cobra Wiring (thin cmd layer) + +- [x] T002 Add `src/cmd/wt/help_dump.go`: `helpDumpCmd() *cobra.Command` with `Use: "help-dump"`, `Hidden: true`, `Args: cobra.NoArgs`, and a `RunE` that calls `worktree.BuildHelpDump(cmd.Root(), version)`, marshals with `json.MarshalIndent(doc, "", " ")`, writes the JSON (+ trailing newline) to `cmd.OutOrStdout()`, and returns any error. +- [x] T003 Register `helpDumpCmd()` on the root in `src/cmd/wt/main.go` via `root.AddCommand(...)`. + +### Phase 3: Tests + +- [x] T004 [P] Add `src/internal/worktree/helpdump_test.go`: unit-test `BuildHelpDump` against a synthesized root (root + leaves + a Hidden cmd + auto `completion`/`help`): assert envelope fields, no `captured_at` (via JSON marshal key check), `schema_version==1`, Hidden/`completion`/`help` filtered, leaf `commands` is `[]`, and `text`/`path`/`usage` populated. +- [x] T005 [P] Add `src/cmd/wt/help_dump_test.go`: run the built binary `wt help-dump` via `runWt`; assert exit 0, empty stderr, valid JSON, `tool=="wt"`, `schema_version==1`, no `captured_at` key, `help-dump`/`completion`/`help` absent from the tree, 7 subcommands present, and that `help-dump` is absent from `wt -h`. + +### Phase 4: Conformance & Backlog Hygiene + +- [x] T006 Build the binary, run `wt help-dump`, diff against the committed `help/wt.json` (normalize `version`, drop `captured_at`); resolve any byte-level `text` mismatch by adjusting the capture method until identical. +- [x] T007 Edit `fab/backlog.md`: mark `[pc47]` done/superseded by `qqkj`, noting the push half is intentionally dropped per the pull-model inversion. + +## Execution Order + +- T001 blocks T002 (cmd wires to the builder) and T004 (tests the builder). +- T002 blocks T003 and T005. +- T006 requires T001–T003 (needs a buildable binary). +- T004 and T005 are independent of each other once their deps are met. + +## Acceptance + +### Functional Completeness + +- [ ] A-001 R1: `help_dump.go` defines `helpDumpCmd()` with `Hidden: true`, registered on root in `main.go`; `help-dump` absent from `wt -h`. +- [ ] A-002 R2: errors return via `RunE`; success exits 0 with empty stderr; failure exits non-zero. +- [ ] A-003 R3: envelope marshals to exactly `{tool, version, schema_version, root}`, no `captured_at`, `tool=="wt"`, `schema_version==1` (integer), `version` from passed-in binary version. +- [ ] A-004 R4: each node has `{name, path, short, usage, text, commands}`; leaf `commands` is `[]` not `null`. +- [ ] A-005 R5: each node's `text` matches the committed reference sample byte-for-byte (modulo version/captured_at). +- [ ] A-006 R6: discovery walks `rootCmd.Commands()` recursively; no `-h` text parsing. +- [ ] A-007 R7: `completion`, `help`, and `help-dump` filtered from the tree; 7 subcommands remain. + +### Behavioral Correctness + +- [ ] A-008 R8: `wt help-dump` output (version normalized, `captured_at` inserted) is structurally equivalent to committed `help/wt.json`. + +### Scenario Coverage + +- [ ] A-009 R9: builder unit test and command test exist and pass under `go test ./cmd/... ./internal/...`. + +### Edge Cases & Error Handling + +- [ ] A-010 R7: a Hidden node anywhere in the tree (including `help-dump` itself) is excluded without special-case logic. +- [ ] A-011 R4: `commands` serializes as `[]` for leaves (no `null` that would fail `NodeSchema`). + +### Code Quality + +- [ ] A-012 Pattern consistency: new code follows the `xxxCmd() *cobra.Command` constructor pattern and internal-package boundary (Constitution V); naming matches surrounding files. +- [ ] A-013 No unnecessary duplication: reuses existing `version` var and root handler; no reimplemented help rendering. +- [ ] A-014 No god functions (>50 lines without reason); no magic strings — `"wt"`/schema constant are justified literals tied to the contract. +- [ ] A-015 gofmt clean (module root `src/`) and `go vet ./...` clean. + +## Notes + +- Check items as you review: `- [x]` +- All acceptance items must pass before `/fab-continue` (hydrate) +- If an item is not applicable, mark checked and prefix with **N/A**: `- [x] A-NNN **N/A**: {reason}` + +## Assumptions + +| # | Grade | Decision | Rationale | Scores | +|---|-------|----------|-----------|--------| +| 1 | Certain | Builder `BuildHelpDump(root *cobra.Command, version string)` takes the root + version as args; `cmd/` passes `cmd.Root()` and `main.version` | Root lives in package `main`; passing it keeps logic in `internal/` per Constitution V. Intake Impact section prescribes this split | S:95 R:70 A:90 D:85 | +| 2 | Certain | `text` captured via `cmd.SetOut(buf)`+`cmd.Help()` then `TrimRight("\n")` | Reference sample has no trailing newline on `text`; Cobra's help render appends one — trim to match byte-for-byte (intake assumption #12) | S:90 R:60 A:80 D:75 | +| 3 | Certain | JSON emitted with `MarshalIndent(…, "", " ")` + trailing newline to stdout | Reference `help/wt.json` is 2-space-indented pretty JSON; matches sample formatting | S:85 R:75 A:85 D:80 | +| 4 | Certain | `schema_version` modeled as a Go `int` field set to `1` | Contract §8 + `z.literal(1)` require integer `1`, not string | S:98 R:85 A:95 D:95 | +| 5 | Certain | `helpDumpCmd` uses `cobra.NoArgs` | Consistent with `update` cmd; help-dump takes no positional args | S:80 R:85 A:85 D:80 | + +5 assumptions (5 certain, 0 confident, 0 tentative). diff --git a/src/cmd/wt/help_dump.go b/src/cmd/wt/help_dump.go new file mode 100644 index 0000000..cec7f42 --- /dev/null +++ b/src/cmd/wt/help_dump.go @@ -0,0 +1,40 @@ +package main + +import ( + "encoding/json" + + "github.com/spf13/cobra" + + wt "github.com/sahil87/wt/internal/worktree" +) + +// helpDumpCmd builds the Hidden `wt help-dump` subcommand. It emits a single +// JSON help-dump envelope to stdout for shll.ai's scheduled puller to capture +// (see the shll.ai help-dump contract). The command is Hidden so it never +// appears in `wt -h`, and — being Hidden — self-filters from its own dump. +// +// Per Constitution V the tree-walk and envelope-building logic lives in +// internal/worktree (BuildHelpDump); this layer only wires Cobra to the +// builder and writes JSON to stdout, returning errors via RunE so main.go maps +// them to a typed exit code. +func helpDumpCmd() *cobra.Command { + return &cobra.Command{ + Use: "help-dump", + Short: "Emit the CLI help tree as JSON (for shll.ai capture)", + Hidden: true, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + doc, err := wt.BuildHelpDump(cmd.Root(), version) + if err != nil { + return err + } + out, err := json.MarshalIndent(doc, "", " ") + if err != nil { + return err + } + out = append(out, '\n') + _, err = cmd.OutOrStdout().Write(out) + return err + }, + } +} diff --git a/src/cmd/wt/help_dump_test.go b/src/cmd/wt/help_dump_test.go new file mode 100644 index 0000000..a1d398e --- /dev/null +++ b/src/cmd/wt/help_dump_test.go @@ -0,0 +1,107 @@ +package main + +import ( + "encoding/json" + "testing" +) + +// helpDumpDoc mirrors the help-dump envelope for decoding in tests. captured_at +// is deliberately typed so we can assert its ABSENCE separately via a raw map. +type helpDumpDoc struct { + Tool string `json:"tool"` + Version string `json:"version"` + SchemaVersion int `json:"schema_version"` + Root helpDumpNode `json:"root"` +} + +type helpDumpNode struct { + Name string `json:"name"` + Path string `json:"path"` + Short string `json:"short"` + Usage string `json:"usage"` + Text string `json:"text"` + Commands []helpDumpNode `json:"commands"` +} + +// TestHelpDump_EmitsValidEnvelope runs the built binary's `wt help-dump` and +// asserts the contract: exit 0, empty stderr, valid JSON, tool=="wt", +// schema_version==1, no captured_at key, and the auto-generated +// completion/help plus the Hidden help-dump itself are absent from the tree. +func TestHelpDump_EmitsValidEnvelope(t *testing.T) { + repo := createTestRepo(t) + r := runWtSuccess(t, repo, nil, "help-dump") + + if r.Stderr != "" { + t.Errorf("expected empty stderr on success, got: %q", r.Stderr) + } + + // Top-level shape: exactly tool/version/schema_version/root, no captured_at. + var top map[string]json.RawMessage + if err := json.Unmarshal([]byte(r.Stdout), &top); err != nil { + t.Fatalf("stdout is not valid JSON: %v\nstdout: %s", err, r.Stdout) + } + if _, ok := top["captured_at"]; ok { + t.Error("envelope must NOT contain captured_at (shll.ai stamps it post-capture)") + } + allowed := map[string]bool{"tool": true, "version": true, "schema_version": true, "root": true} + for k := range top { + if !allowed[k] { + t.Errorf("unexpected top-level key %q", k) + } + } + + var doc helpDumpDoc + if err := json.Unmarshal([]byte(r.Stdout), &doc); err != nil { + t.Fatalf("decode envelope: %v", err) + } + if doc.Tool != "wt" { + t.Errorf("tool = %q, want %q", doc.Tool, "wt") + } + if doc.SchemaVersion != 1 { + t.Errorf("schema_version = %d, want 1", doc.SchemaVersion) + } + if doc.Version == "" { + t.Error("version must be populated from the built binary, got empty") + } + if doc.Root.Name != "wt" || doc.Root.Path != "wt" { + t.Errorf("root name/path = %q/%q, want wt/wt", doc.Root.Name, doc.Root.Path) + } + + names := map[string]bool{} + for _, c := range doc.Root.Commands { + names[c.Name] = true + } + for _, banned := range []string{"completion", "help", "help-dump"} { + if names[banned] { + t.Errorf("tree must not contain %q", banned) + } + } + // wt currently exposes exactly these 7 visible subcommands. + for _, want := range []string{"create", "delete", "init", "list", "open", "shell-init", "update"} { + if !names[want] { + t.Errorf("tree missing expected subcommand %q, got: %v", want, names) + } + } + if got := len(doc.Root.Commands); got != 7 { + t.Errorf("expected 7 visible subcommands, got %d: %v", got, names) + } +} + +// TestHelpDump_HiddenFromRootHelp asserts help-dump never appears in `wt -h` +// (it is declared Hidden). +func TestHelpDump_HiddenFromRootHelp(t *testing.T) { + repo := createTestRepo(t) + r := runWtSuccess(t, repo, nil, "-h") + assertNotContains(t, r.Stdout, "help-dump") +} + +// TestHelpDump_RejectsArgs asserts cobra.NoArgs enforcement: a positional arg +// surfaces a non-zero exit via main.go's error path. +func TestHelpDump_RejectsArgs(t *testing.T) { + repo := createTestRepo(t) + r := runWt(t, repo, nil, "help-dump", "extra") + if r.ExitCode == 0 { + t.Fatalf("expected non-zero exit from `wt help-dump extra` (cobra.NoArgs)\nstdout: %s\nstderr: %s", + r.Stdout, r.Stderr) + } +} diff --git a/src/cmd/wt/main.go b/src/cmd/wt/main.go index 5206cd8..dd8ffbe 100644 --- a/src/cmd/wt/main.go +++ b/src/cmd/wt/main.go @@ -35,6 +35,7 @@ Shell wrapper (recommended): initCmd(), shellInitCmd(), updateCmd(), + helpDumpCmd(), ) if err := root.Execute(); err != nil { diff --git a/src/internal/worktree/helpdump.go b/src/internal/worktree/helpdump.go new file mode 100644 index 0000000..e0228af --- /dev/null +++ b/src/internal/worktree/helpdump.go @@ -0,0 +1,188 @@ +package worktree + +import ( + "bytes" + "strings" + + "github.com/spf13/cobra" +) + +// helpDumpSchemaVersion is the contract revision emitted in the help-dump +// envelope. It is frozen at 1 for this revision of the shll.ai help-dump +// contract (see docs/specs / shll.ai help-dump-contract.md §8); new fields and +// version bumps are a separate, deliberate change. +const helpDumpSchemaVersion = 1 + +// toolName is the binary name reported in the help-dump envelope's `tool` +// field. It is the invoked binary name, not the file slug. +const toolName = "wt" + +// HelpDoc is the top-level help-dump envelope emitted to stdout by +// `wt help-dump`. It marshals to EXACTLY {tool, version, schema_version, root}. +// +// It deliberately does NOT carry a captured_at field: per the shll.ai +// help-dump contract §3, captured_at is stamped by shll.ai post-capture, and +// the tool MUST NOT emit it. Adding the field here (even omitempty) would +// drift from the contract. +type HelpDoc struct { + Tool string `json:"tool"` + Version string `json:"version"` + SchemaVersion int `json:"schema_version"` + Root HelpNode `json:"root"` +} + +// HelpNode is one command in the recursive help tree. commands holds child +// nodes (an empty, non-nil slice for a leaf so it marshals to [] not null, +// satisfying shll.ai's NodeSchema which requires z.array). +type HelpNode struct { + Name string `json:"name"` + Path string `json:"path"` + Short string `json:"short"` + Usage string `json:"usage"` + Text string `json:"text"` + Commands []HelpNode `json:"commands"` +} + +// BuildHelpDump walks the Cobra command tree rooted at root and builds the +// help-dump envelope. version is the built binary's version (from +// main.version / rootCmd.Version) and is never hardcoded by this package. +// +// The tree is discovered programmatically via root.Commands() recursively to +// full depth — never by parsing -h text. Cobra's auto-generated `completion` +// and `help` subcommands and any Hidden command (which includes `help-dump` +// itself) are dropped from the tree. +func BuildHelpDump(root *cobra.Command, version string) (HelpDoc, error) { + // Cobra adds the `-h, --help` flag and the auto-generated `help` and + // `completion` subcommands lazily — normally during Execute(). When + // help-dump runs, the *root* has been initialized (it is the executed + // command), but descendant commands have not had their help flag added, + // and the `help`/`completion` subcommands may not yet exist. Without this + // each leaf's rendered -h would omit the `-h, --help` line and drop the + // `[flags]` suffix from its usage. Initialize the whole tree up front so + // the render matches a real `command -h` invocation (and the reference + // sample). + initHelpTree(root) + + node, err := buildNode(root) + if err != nil { + return HelpDoc{}, err + } + return HelpDoc{ + Tool: toolName, + Version: version, + SchemaVersion: helpDumpSchemaVersion, + Root: node, + }, nil +} + +// initHelpTree initializes Cobra's lazily-added help affordances across the +// whole command tree so that buildNode's rendered -h matches a real +// `command -h` invocation: the `-h, --help` flag on every command (which also +// makes UseLine() append the `[flags]` suffix) and the auto-generated `help` +// and `completion` subcommands on commands that have children. The added +// `help`/`completion` commands are dropped from the tree by isFilteredCommand; +// initializing them here ensures they exist (and are thus consistently hidden) +// when rendering each parent's -h. All of these initializers are idempotent. +func initHelpTree(cmd *cobra.Command) { + cmd.InitDefaultHelpFlag() + // When help-dump (not the root) is the executed command, Cobra never adds + // the root's `-v, --version` flag — execute() only does so for the command + // being run. Add it here so the root's rendered -h matches a real `wt -h`. + // InitDefaultVersionFlag is a no-op when cmd.Version is empty. + cmd.InitDefaultVersionFlag() + if cmd.HasSubCommands() { + cmd.InitDefaultHelpCmd() + } + if cmd.Root() == cmd { + cmd.InitDefaultCompletionCmd() + } + for _, child := range cmd.Commands() { + initHelpTree(child) + } +} + +// buildNode renders a single command into a HelpNode and recurses into its +// non-filtered children. text is the raw -h render for cmd, captured into a +// buffer (never composed by hand or regex-parsed), with the trailing newline +// trimmed to match the committed help/wt.json reference sample byte-for-byte. +func buildNode(cmd *cobra.Command) (HelpNode, error) { + text, err := renderHelpText(cmd) + if err != nil { + return HelpNode{}, err + } + + children := make([]HelpNode, 0, len(cmd.Commands())) + for _, child := range cmd.Commands() { + if isFilteredCommand(child) { + continue + } + childNode, err := buildNode(child) + if err != nil { + return HelpNode{}, err + } + children = append(children, childNode) + } + + return HelpNode{ + Name: cmd.Name(), + Path: cmd.CommandPath(), + Short: cmd.Short, + Usage: cmd.UseLine(), + Text: text, + Commands: children, + }, nil +} + +// isFilteredCommand reports whether a command must be dropped from the help +// tree: Cobra's auto-generated `completion` and `help` subcommands, plus any +// Hidden command. Because `help-dump` is itself Hidden, this rule self-filters +// it without a special case. +func isFilteredCommand(cmd *cobra.Command) bool { + if cmd.Hidden { + return true + } + switch cmd.Name() { + case "completion", "help": + return true + } + return false +} + +// renderHelpText captures cmd's -h output into a buffer and returns it with the +// trailing newline trimmed. +// +// Filtered children (completion/help/Hidden) are temporarily detached from cmd +// during the render so the command's "Available Commands" listing reflects the +// dumped tree — matching the reference sample, which omits those entries. +// Detachment (rather than toggling Hidden) is required because Cobra's usage +// template special-cases the `help` command, listing it even when Hidden via an +// explicit `(eq .Name "help")` clause; only removing it from the slice keeps it +// out of the listing. The children are re-attached before returning (Cobra +// re-sorts on AddCommand, restoring order) so live `wt -h` for real users is +// unaffected, and the SetOut/SetErr overrides are likewise restored. +func renderHelpText(cmd *cobra.Command) (text string, err error) { + var detached []*cobra.Command + for _, child := range cmd.Commands() { + if isFilteredCommand(child) { + detached = append(detached, child) + } + } + if len(detached) > 0 { + cmd.RemoveCommand(detached...) + defer cmd.AddCommand(detached...) + } + + var buf bytes.Buffer + prevOut, prevErr := cmd.OutOrStdout(), cmd.ErrOrStderr() + cmd.SetOut(&buf) + cmd.SetErr(&buf) + defer func() { + cmd.SetOut(prevOut) + cmd.SetErr(prevErr) + }() + + if err := cmd.Help(); err != nil { + return "", err + } + return strings.TrimRight(buf.String(), "\n"), nil +} diff --git a/src/internal/worktree/helpdump_test.go b/src/internal/worktree/helpdump_test.go new file mode 100644 index 0000000..93d7ac5 --- /dev/null +++ b/src/internal/worktree/helpdump_test.go @@ -0,0 +1,235 @@ +package worktree + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/spf13/cobra" +) + +// newTestRoot builds a synthetic command tree resembling wt's: a root with a +// couple of visible leaves, a Hidden command (which must self-filter, like the +// real help-dump), and a nested child (to exercise recursive discovery). The +// auto-generated completion/help commands are added by BuildHelpDump's tree +// init, so this tree intentionally does not add them itself. +func newTestRoot() *cobra.Command { + root := &cobra.Command{ + Use: "wt", + Short: "Git worktree management", + Long: "Git worktree management — long description.", + Version: "9.9.9", + } + root.AddCommand(&cobra.Command{ + Use: "create [branch]", + Short: "Create a git worktree", + RunE: func(cmd *cobra.Command, args []string) error { return nil }, + }) + + parent := &cobra.Command{ + Use: "remote", + Short: "Manage remotes", + } + parent.AddCommand(&cobra.Command{ + Use: "add", + Short: "Add a remote", + RunE: func(cmd *cobra.Command, args []string) error { return nil }, + }) + root.AddCommand(parent) + + root.AddCommand(&cobra.Command{ + Use: "help-dump", + Short: "Hidden dump command", + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { return nil }, + }) + return root +} + +func TestBuildHelpDump_Envelope(t *testing.T) { + root := newTestRoot() + doc, err := BuildHelpDump(root, "1.2.3") + if err != nil { + t.Fatalf("BuildHelpDump: %v", err) + } + + if doc.Tool != "wt" { + t.Errorf("tool = %q, want %q", doc.Tool, "wt") + } + if doc.Version != "1.2.3" { + t.Errorf("version = %q, want %q (must come from the passed-in binary version)", doc.Version, "1.2.3") + } + if doc.SchemaVersion != 1 { + t.Errorf("schema_version = %d, want 1", doc.SchemaVersion) + } + if doc.Root.Name != "wt" { + t.Errorf("root.name = %q, want %q", doc.Root.Name, "wt") + } +} + +// TestBuildHelpDump_OmitsCapturedAt asserts the marshaled envelope carries +// EXACTLY {tool, version, schema_version, root} and never captured_at — the +// tool must not emit captured_at (shll.ai stamps it post-capture). +func TestBuildHelpDump_OmitsCapturedAt(t *testing.T) { + doc, err := BuildHelpDump(newTestRoot(), "1.2.3") + if err != nil { + t.Fatalf("BuildHelpDump: %v", err) + } + b, err := json.Marshal(doc) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var top map[string]json.RawMessage + if err := json.Unmarshal(b, &top); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if _, ok := top["captured_at"]; ok { + t.Errorf("envelope must NOT contain captured_at, got keys: %v", keysOf(top)) + } + want := map[string]bool{"tool": true, "version": true, "schema_version": true, "root": true} + for k := range top { + if !want[k] { + t.Errorf("unexpected top-level key %q (envelope must be exactly tool/version/schema_version/root)", k) + } + } + for k := range want { + if _, ok := top[k]; !ok { + t.Errorf("missing required top-level key %q", k) + } + } +} + +// TestBuildHelpDump_FiltersCompletionHelpHidden asserts the tree drops the +// auto-generated completion/help subcommands and any Hidden command (the +// Hidden help-dump self-filters), while keeping the visible commands. +func TestBuildHelpDump_FiltersCompletionHelpHidden(t *testing.T) { + doc, err := BuildHelpDump(newTestRoot(), "1.2.3") + if err != nil { + t.Fatalf("BuildHelpDump: %v", err) + } + + names := map[string]bool{} + for _, c := range doc.Root.Commands { + names[c.Name] = true + } + for _, banned := range []string{"completion", "help", "help-dump"} { + if names[banned] { + t.Errorf("tree must not contain %q, got commands: %v", banned, names) + } + } + for _, want := range []string{"create", "remote"} { + if !names[want] { + t.Errorf("tree missing expected command %q, got: %v", want, names) + } + } + if got := len(doc.Root.Commands); got != 2 { + t.Errorf("root should have 2 visible subcommands (create, remote), got %d", got) + } +} + +// TestBuildHelpDump_RecursiveDiscovery asserts the walk recurses to full depth: +// the nested `remote add` child must be discovered under `remote`. +func TestBuildHelpDump_RecursiveDiscovery(t *testing.T) { + doc, err := BuildHelpDump(newTestRoot(), "1.2.3") + if err != nil { + t.Fatalf("BuildHelpDump: %v", err) + } + var remote *HelpNode + for i := range doc.Root.Commands { + if doc.Root.Commands[i].Name == "remote" { + remote = &doc.Root.Commands[i] + } + } + if remote == nil { + t.Fatal("remote command not found") + } + if len(remote.Commands) != 1 || remote.Commands[0].Name != "add" { + t.Errorf("expected nested `remote add`, got %+v", remote.Commands) + } + if got := remote.Commands[0].Path; got != "wt remote add" { + t.Errorf("nested path = %q, want %q", got, "wt remote add") + } +} + +// TestBuildHelpDump_NodeShape asserts per-node fields and that a leaf's +// commands marshals to [] (not null), satisfying shll.ai's NodeSchema. +func TestBuildHelpDump_NodeShape(t *testing.T) { + doc, err := BuildHelpDump(newTestRoot(), "1.2.3") + if err != nil { + t.Fatalf("BuildHelpDump: %v", err) + } + + var create *HelpNode + for i := range doc.Root.Commands { + if doc.Root.Commands[i].Name == "create" { + create = &doc.Root.Commands[i] + } + } + if create == nil { + t.Fatal("create command not found") + } + if create.Path != "wt create" { + t.Errorf("create.path = %q, want %q", create.Path, "wt create") + } + if create.Short != "Create a git worktree" { + t.Errorf("create.short = %q", create.Short) + } + if !strings.HasPrefix(create.Usage, "wt create") { + t.Errorf("create.usage = %q, want prefix %q", create.Usage, "wt create") + } + if create.Text == "" { + t.Error("create.text must be the rendered -h output, got empty") + } + // The rendered -h MUST include the auto-added help flag (proves the help + // flag is initialized across the tree, not just on the executed command). + if !strings.Contains(create.Text, "-h, --help") { + t.Errorf("create.text should contain the -h, --help flag line, got:\n%s", create.Text) + } + + // Leaf commands marshal to "commands": [] (non-nil slice), never null. + b, err := json.Marshal(create) + if err != nil { + t.Fatalf("marshal create: %v", err) + } + if !strings.Contains(string(b), `"commands":[]`) { + t.Errorf("leaf commands must serialize as [] not null, got: %s", b) + } +} + +// TestBuildHelpDump_RestoresLiveTree asserts the render's temporary detachment +// of completion/help (and the SetOut/SetErr overrides) is restored, so a normal +// `wt -h` for real users is unaffected after a dump. +func TestBuildHelpDump_RestoresLiveTree(t *testing.T) { + root := newTestRoot() + if _, err := BuildHelpDump(root, "1.2.3"); err != nil { + t.Fatalf("BuildHelpDump: %v", err) + } + + var found []string + for _, c := range root.Commands() { + found = append(found, c.Name()) + } + hasHelp, hasCompletion := false, false + for _, n := range found { + if n == "help" { + hasHelp = true + } + if n == "completion" { + hasCompletion = true + } + } + if !hasHelp { + t.Errorf("help command should be re-attached after a dump, got: %v", found) + } + if !hasCompletion { + t.Errorf("completion command should be re-attached after a dump, got: %v", found) + } +} + +func keysOf(m map[string]json.RawMessage) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + return out +} From b8dd2f8216067c932055e54a3fe50f6e7411a476 Mon Sep 17 00:00:00 2001 From: Sahil Ahuja Date: Wed, 3 Jun 2026 20:12:37 +0530 Subject: [PATCH 2/3] Update ship status and record PR URL --- .../260603-qqkj-help-dump-command/.history.jsonl | 1 + .../260603-qqkj-help-dump-command/.status.yaml | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/fab/changes/260603-qqkj-help-dump-command/.history.jsonl b/fab/changes/260603-qqkj-help-dump-command/.history.jsonl index 265f9b0..83961dd 100644 --- a/fab/changes/260603-qqkj-help-dump-command/.history.jsonl +++ b/fab/changes/260603-qqkj-help-dump-command/.history.jsonl @@ -18,3 +18,4 @@ {"cmd":"fab-continue","event":"command","ts":"2026-06-03T14:38:39Z"} {"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"ship","ts":"2026-06-03T14:40:18Z"} {"cmd":"git-pr","event":"command","ts":"2026-06-03T14:41:26Z"} +{"action":"enter","driver":"git-pr","event":"stage-transition","stage":"review-pr","ts":"2026-06-03T14:42:33Z"} diff --git a/fab/changes/260603-qqkj-help-dump-command/.status.yaml b/fab/changes/260603-qqkj-help-dump-command/.status.yaml index d9e770e..3e6cf52 100644 --- a/fab/changes/260603-qqkj-help-dump-command/.status.yaml +++ b/fab/changes/260603-qqkj-help-dump-command/.status.yaml @@ -9,8 +9,8 @@ progress: apply: done review: done hydrate: done - ship: active - review-pr: pending + ship: done + review-pr: active plan: generated: true task_count: 7 @@ -33,8 +33,10 @@ stage_metrics: apply: {started_at: "2026-06-03T14:23:17Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-03T14:34:53Z"} review: {started_at: "2026-06-03T14:34:53Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-03T14:37:47Z"} hydrate: {started_at: "2026-06-03T14:37:47Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-03T14:40:18Z"} - ship: {started_at: "2026-06-03T14:40:18Z", driver: fab-fff, iterations: 1} -prs: [] + ship: {started_at: "2026-06-03T14:40:18Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-03T14:42:33Z"} + review-pr: {started_at: "2026-06-03T14:42:33Z", driver: git-pr, iterations: 1} +prs: + - https://github.com/sahil87/wt/pull/21 true_impact: added: 0 deleted: 0 @@ -46,4 +48,4 @@ true_impact: computed_at: "2026-06-03T14:40:18Z" computed_at_stage: hydrate # true_impact: lazily created on first apply-finish (no placeholder here). -last_updated: 2026-06-03T14:40:18Z +last_updated: 2026-06-03T14:42:33Z From 5076af68a8bd5001b4ff65b40a425f7a3ac71c4f Mon Sep 17 00:00:00 2001 From: Sahil Ahuja Date: Thu, 4 Jun 2026 14:17:52 +0530 Subject: [PATCH 3/3] fix: address review feedback from @Copilot --- src/internal/worktree/helpdump.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/internal/worktree/helpdump.go b/src/internal/worktree/helpdump.go index e0228af..4e27ec8 100644 --- a/src/internal/worktree/helpdump.go +++ b/src/internal/worktree/helpdump.go @@ -9,12 +9,14 @@ import ( // helpDumpSchemaVersion is the contract revision emitted in the help-dump // envelope. It is frozen at 1 for this revision of the shll.ai help-dump -// contract (see docs/specs / shll.ai help-dump-contract.md §8); new fields and -// version bumps are a separate, deliberate change. +// contract — the upstream spec is sahil87/shll.ai docs/specs/help-dump-contract.md +// §8 (in-repo behavior contract: docs/memory/wt-cli/help-dump-contract.md). New +// fields and version bumps are a separate, deliberate change. const helpDumpSchemaVersion = 1 -// toolName is the binary name reported in the help-dump envelope's `tool` -// field. It is the invoked binary name, not the file slug. +// toolName is the binary name reported in the help-dump envelope's `tool` field. +// The contract requires the invoked binary name (not the file slug); for this +// repo that is the fixed constant "wt" — it is not derived from argv. const toolName = "wt" // HelpDoc is the top-level help-dump envelope emitted to stdout by