From 4902cb76c55e41e8dcc2f6c8120cb980c6735aab Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:28:24 +0100 Subject: [PATCH 1/7] feat: show the release cut in the launch preview and retire changelog abcd launch --dry-run now renders the cut the next launch ship would make beside its bundle report: the derived version, the deciding impact, the records, the guard and findings verdicts, in text under its own line and in --json under a new cut key (cut_error when it cannot be read). It is the same deterministic emit and render the changelog verb used, with no prose and no agent. A refused cut is reported and changes nothing about the dry run's exit. The changelog verb goes with no alias (adr-40). abcd changelog is an unknown command whose note names abcd launch --dry-run. Its placement, sentence, parity note and DOGFOODING recall entry go with it, and its tests move onto the preview: the tree-digest check, the refused cut still exiting 0, the JSON, the page set, the uncommitted-records refusal and the subdirectory read. The dry run now reads the checkout root wherever it is run, as the cut always has. Before, from a subdirectory it read the subdirectory as a repository that declares no artefact kind and refused before reaching the cut, which would have lost the subdirectory read the changelog verb had. The dry run's own targeted lines are dropped when the cut is read, because the cut lists the same intents and says which ones it moves. The pages, the composer agent, the brief and the principle that named the verb now name the preview. The launch chapter no longer says the preview always exits 0 (a peer finding): a --baseline that names no release tag exits 2, and an undeclared kind or a missing payload is refused. Step 3 of spc-2610100613109045. Assisted-by: Claude:claude-opus-5-5 --- .../development/brief/01-product/04-scope.md | 2 +- .../brief/02-constraints/04-naming.md | 2 +- .../brief/04-surfaces/04-launch.md | 54 +++-- .abcd/development/brief/04-surfaces/README.md | 8 +- .../05-internals/04-universal-patterns.md | 2 +- .../brief/05-internals/08-skills.md | 2 +- .../pre-existing-is-not-a-defence.md | 4 +- .abcd/development/release/surface.json | 10 +- .abcd/rules.json | 1 - agents/release-changelog-composer.md | 2 +- commands/intent.md | 4 +- commands/launch.md | 47 +++-- docs/reference/cli/commands.md | 14 +- internal/core/release/emit.go | 3 +- internal/core/surface/manifest_test.go | 4 +- internal/core/surface/sentences.go | 5 +- internal/surface/cli/cli.go | 37 +++- internal/surface/cli/helpgroups.go | 4 +- internal/surface/cli/helpgroups_test.go | 2 +- .../surface/cli/intent_target_cli_test.go | 15 +- internal/surface/cli/launch_preflight.go | 37 ++++ internal/surface/cli/launch_preview_test.go | 190 ++++++++++++++++++ internal/surface/cli/ship.go | 42 +--- internal/surface/cli/ship_test.go | 106 ++++++---- internal/surface/cli/ship_uncommitted_test.go | 29 ++- internal/surface/cli/staleusage.go | 3 +- internal/surface/cli/surfaceparity_test.go | 1 - 27 files changed, 454 insertions(+), 176 deletions(-) create mode 100644 internal/surface/cli/launch_preview_test.go diff --git a/.abcd/development/brief/01-product/04-scope.md b/.abcd/development/brief/01-product/04-scope.md index 2ab37eeb1..22b47ac56 100644 --- a/.abcd/development/brief/01-product/04-scope.md +++ b/.abcd/development/brief/01-product/04-scope.md @@ -16,7 +16,7 @@ The scope this brief covers is the intents on the lifecycle shelves: what has sh - `/abcd` — top-level where-am-i status board (per itd-20) — see [`04-surfaces/08-abcd.md`](../04-surfaces/08-abcd.md) - `/abcd:reflect` — release retrospective, a design target (itd-24, planned) — see [`04-surfaces/09-reflect.md`](../04-surfaces/09-reflect.md) -**Operator-internal commands** (wiring rather than user-facing surface): `/abcd:run` — the itd-29 autonomous-run operator surface (`status`/`pause`/`resume`/`preflight`; read-mostly over the pluggable autonomous-run seam ([adr-27](../../decisions/adrs/0027-autonomous-run-pluggable-seam.md)), v1 never starts or kills the loop) — is not built: itd-29 is superseded (by itd-2609201916151817, planned), and no `run` verb or `commands/run.md` is on any shipped surface. The operator-internal verbs the binary does register are `changelog`, `completion`, `hook`, `rules`, `spec` and `statusline`, and the class is defined by the absence of a `commands/` file rather than by the presence of one. See [`04-surfaces/README.md`](../04-surfaces/README.md) for the user-facing-vs-operator-internal boundary and the record that delivered each. +**Operator-internal commands** (wiring rather than user-facing surface): `/abcd:run` — the itd-29 autonomous-run operator surface (`status`/`pause`/`resume`/`preflight`; read-mostly over the pluggable autonomous-run seam ([adr-27](../../decisions/adrs/0027-autonomous-run-pluggable-seam.md)), v1 never starts or kills the loop) — is not built: itd-29 is superseded (by itd-2609201916151817, planned), and no `run` verb or `commands/run.md` is on any shipped surface. The operator-internal verbs the binary does register are `completion`, `hook`, `rules`, `spec` and `statusline`, and the class is defined by the absence of a `commands/` file rather than by the presence of one. See [`04-surfaces/README.md`](../04-surfaces/README.md) for the user-facing-vs-operator-internal boundary and the record that delivered each. **Intents: derived, never hand-counted here.** The shelves are the single source of what is shipped, committed and benched, and this page keeps no static copy of diff --git a/.abcd/development/brief/02-constraints/04-naming.md b/.abcd/development/brief/02-constraints/04-naming.md index 4dd929642..8866157b7 100644 --- a/.abcd/development/brief/02-constraints/04-naming.md +++ b/.abcd/development/brief/02-constraints/04-naming.md @@ -52,7 +52,7 @@ The exemptions carrying a rationale of their own: board. The remaining surfaces are exempt for the plain reason that no maritime cognate -adds meaning: `banlist`, `changelog`, `consult`, `decide`, `docs`, `guard`, +adds meaning: `banlist`, `consult`, `decide`, `docs`, `guard`, `history`, `ideate`, `identity`, `ingest`, `lint`, `memory`, `prepare-this-repo`, `reading`, `rules`, `site`, `spec`, and `update`. They are registered here so the exemption is on the record. diff --git a/.abcd/development/brief/04-surfaces/04-launch.md b/.abcd/development/brief/04-surfaces/04-launch.md index b32a4aab8..f143de5cd 100644 --- a/.abcd/development/brief/04-surfaces/04-launch.md +++ b/.abcd/development/brief/04-surfaces/04-launch.md @@ -12,9 +12,16 @@ a dated changelog heading and the release page and stops, and CI and a human tak And it never ships the design record: the payload is default-deny with the whole `.abcd/` namespace excluded structurally, so no include line can put it back. -The preview always exits 0, because a preview never blocks, and its one write is -its pre-flight report in the gitignored local tier. Bare `abcd launch` refuses -with a hint to ask for it. A repository that declares `kind: plugin` with no +The preview never blocks on what it finds: every finding a cut would refuse +on, and a refused cut, is reported and the preview exits 0. It exits non-zero +only when it cannot run at all: an undeclared or unknown artefact kind and a +plugin with no launch payload are refused, and a named baseline that is not a +release tag exits 2. Its one write is its pre-flight +report in the gitignored local tier. Beside the bundle report it renders the +release cut the next ship would make, the deterministic emit with no prose +(spc-2610100613109045, decision 5): the preview is the one way to see the cut +without starting one, and there is no separate changelog verb. Bare +`abcd launch` refuses with a hint to ask for the preview. A repository that declares `kind: plugin` with no launch payload (`.abcd/config/launch-payload.json`) has nothing to preview, and the preview says so and names the release path a repository that ships no plugin has: declaring its kind as `binary` or `application`, the scaffolded release @@ -143,7 +150,9 @@ admit (or nothing is armed), 1 when it would refuse, and 2 on a structural fault `commands/launch.md` carries the emit, compose and ingest orchestration over the `release-changelog-composer` agent, including the release page's retry loop. The -deterministic emit alone is `abcd changelog`, read-only and prose-free. +deterministic emit alone is the cut the preview renders, read-only and +prose-free; it is exactly what the ship's emit step +derives, through the same derivation and the same render. **Commit, tag and publish stay a design target** (itd-72's publishing). The verb neither commits, tags, nor publishes, so every step past the changelog @@ -226,9 +235,13 @@ kind exits 2 with nothing written. **The preview is spelled `dry-run`, and it is a flag, not a sub-verb.** The binary registers no `dry-run` subcommand under launch, and `commands/launch.md` names it as a flag. Its report is -preview-only and always exits 0. It is **not** "ship minus publish": it runs the -same gate suite the cut's render path runs and reports what that path would -refuse on, but refusing is the cut's. +preview-only, and what it finds changes no exit code (the opening of this +chapter names the cases where it cannot run). It is **not** "ship minus +publish": it runs the same gate suite the cut's render path runs and reports +what that path would refuse on, and it renders the cut the ship's emit step +would derive, but refusing is the cut's. The cut is read from the checkout +root wherever the preview is run, and so is the whole preview: run from a +subdirectory, it reads the same repository as from the root. ## 1. Pre-flight gates @@ -312,7 +325,12 @@ hard-fails, citations, a line for every gate row that did not run (its name, its status — `host-run`, `not_armed`, `not_implemented` — and why; the semantic-receipts row is always one) and whether it would publish, then a would-refuse-on line per refusal, a warning line per warn-tier concern, and -where its report landed. The JSON carries the gate detail. +where its report landed. After the bundle report it renders the cut, under its +own header line, exactly as the ship renders its emit: +the base and derived tags or `REFUSED`, the deciding records, the guard and +findings lines, the records added and removed, the release page's intents, +the targeted intents and every refusal. The JSON carries the gate detail, and +the cut under `cut` (or `cut_error` when the cut could not be read). The **manifest lockstep check** also runs for real on the preview, at its `dev` polarity over the working tree — the polarity adr-19 requires the committed @@ -511,8 +529,8 @@ binary whose placement or sentence disagrees with the snapshot committed at `HEAD` is refused as stale, and the refusal names each moved verb and each reworded sentence. -**Unfixed-findings guardrail.** The cut and the read-only `changelog` preview -both ask one further question of the cut: of the findings **this cycle** +**Unfixed-findings guardrail.** The cut and its read-only preview in +the launch preview both ask one further question of the cut: of the findings **this cycle** produced, is any of them consequential, still open, and unanswered? A cut that carries one is refused, and a refused cut carries no derived version at all. The gate fired on this release's own first cut, so it is live behaviour rather @@ -543,9 +561,8 @@ than a design target. single-use: the anchor moves at the next release and every waiver written against the old one lapses, so a deferred finding is re-asked rather than forgotten. Half a waiver does not stand. -- **What the render shows.** Every render of `abcd changelog` and of the launch - cut carries a findings line - (the verdict, the count of unfixed findings and +- **What the render shows.** Every render of the cut, in the launch preview and + in the ship, carries a findings line (the verdict, the count of unfixed findings and the anchor they were measured from, and the count deferred), then one line per waiver naming the record, its severity and its stated reason. A deferral an operator cannot see in the report they actually read is indistinguishable from @@ -764,13 +781,13 @@ performed by a human and by CI. nothing is published. - **Given** an issue record captured since the anchor tag, graded major or critical (or carrying no readable grade), still open and carrying no standing - waiver, **when** the cut or `changelog` runs, **then** the cut is refused under + waiver, **when** the cut or the preview runs, **then** the cut is refused under `unfixed-finding`, the refusal names every such record with its grade and path, no version is derived, and the findings line says how many were counted and from which anchor. - **Given** an issue record the anchor tag held in `open/`, graded major or critical (or carrying no readable grade), and present in no status directory at - HEAD, **when** the cut or `changelog` runs, **then** the cut is refused under + HEAD, **when** the cut or the preview runs, **then** the cut is refused under `deleted-finding`, the refusal names the record with the grade and path the anchor held and says it is in no status directory, and the three dispositions — a move to `resolved/`, a move to `wontfix/`, a re-slug inside `open/` — each go @@ -794,6 +811,13 @@ performed by a human and by CI. - **Given** findings in several gates at once, **when** the preview or the cut runs, **then** every one of them is reported in the one pass and in the pre-flight report. +- **Given** shipped records since the anchor tag, **when** the preview runs, + **then** it renders, beside the bundle report, the cut the ship would + emit: the derived version and deciding impact, the records, and the guard and + findings verdicts, in text and in its JSON; **given** a cut that + refuses, the preview renders the refusal and its exit is unchanged. **Given** + the retired changelog verb, the binary refuses it as an unknown command, and + the note names the preview. - **Given** a previous release tag, **when** the preview or a cut that renders a payload runs, **then** the report lists every payload path added, changed or removed since that tag's payload, each with its SHA-256; **given** no previous diff --git a/.abcd/development/brief/04-surfaces/README.md b/.abcd/development/brief/04-surfaces/README.md index 1848b1c8d..b4656736b 100644 --- a/.abcd/development/brief/04-surfaces/README.md +++ b/.abcd/development/brief/04-surfaces/README.md @@ -141,13 +141,16 @@ the command's sentence (the section below). | people | checks | `lab`, `reading` | | people | portability | `disembark`, `embark` | | people | release | `launch` | -| agents and hosts | — | `banlist`, `changelog`, `docs`, `drain`, `guard`, `guard hook`, `history`, `ideate record`, `identity`, `implement`, `inbox`, `intent audit ingest`, `intent consistency ingest`, `intent prepass`, `lint`, `mode`, `peers`, `reflect`, `report`, `rules`, `scribe`, `site`, `spec`, `statusline` | +| agents and hosts | — | `banlist`, `docs`, `drain`, `guard`, `guard hook`, `history`, `ideate record`, `identity`, `implement`, `inbox`, `intent audit ingest`, `intent consistency ingest`, `intent prepass`, `lint`, `mode`, `peers`, `reflect`, `report`, `rules`, `scribe`, `site`, `spec`, `statusline` | The placement is presentation. No verb is hidden, renamed, moved or nested by it, every verb runs the same whichever block lists it, and the group titles carry no adr-40 bucket meaning. `version` is the root's `--version` flag (itd-2609212130136102), not a verb, so it is in neither block, and bare `abcd` -shows the installed version as the board's last line (itd-2610090831227812). The product +shows the installed version as the board's last line (itd-2610090831227812). +`changelog` is in neither block either: it merged into `launch` as its preview, +and `abcd launch --dry-run` renders the cut it showed beside the bundle report +(itd-2610090831227812). The product thinker placed every verb on 2026-10-09 by who types it: a command a person needs to do their job is a person's, and only a command no person types is listed with the agents (itd-2610090831227812; its spec's verb audit cites each @@ -240,7 +243,6 @@ verb the binary registers apart from the framework's own `help`. | Verb | What it is | Delivered by | |---|---|---| -| `changelog` | The deterministic, read-only emit of the next release cut — derived version, record set, guardrail, no prose. Nothing on the plugin surface runs it: `commands/launch.md`'s emit → compose → ingest orchestration runs `launch ship --json`, and names this verb only as the read-only preview of the same cut. `launch ship` is the write half. | itd-73 (derived versioning) and itd-67's changelog slice, both in `intents/planned/`; documented in [`04-launch.md`](04-launch.md) | | `rules` | Renders the active rule set; a positional `DOMAIN` scopes to one. Read-only diagnostics over the hook-driven rule injection. | itd-3 (the modular rules loader); the loader it reports on is documented in [`05-internals/03-configuration.md`](../05-internals/03-configuration.md), and the verb in [The rules verb](#the-rules-verb) below | | `spec` | The native spec store: bare invocation is a read-only status board, and `spec close` closes a spec and ships its linked intent (`planned/` → `shipped/`) only when no open spec is left naming it — an intent owns one or more specs, and `--remainder ` mints the follow-on for a partial delivery in the same operation, carrying the closing spec's steps not marked landed. | itd-80 / spc-2 (intent lifecycle automation), adr-2609151513118583 (the 1:n relation); its lifecycle is documented in [`05-intent.md`](05-intent.md), and the verb in [The spec verb](#the-spec-verb) below | | `hook` | Hidden from `--help`: five host hook entrypoints, live-wired from `hooks/hooks.json`. `prompt-router` injects the rules a prompt matches and `prompt-router-reset` clears the per-session ledger so they inject again; `session-end` stages the session's own transcript, `subagent-stop` stages a finished sub-agent's transcript with its lineage, and `session-start` files both away and says how many reports wait in the inbox ([`29-report.md`](29-report.md)). The pre-tool-use adapter is `guard hook`, under `guard`. | itd-3 (the prompt router), itd-89 / spc-4 (the transcript clock), itd-103 / spc-16 (the guard hook); the transcript entrypoints are documented in [`11-history.md`](11-history.md), and the rule injection the router drives, with `prompt-router`'s two outputs — the injected block and the `--json` envelope that names the active-domain set for a client that snapshots rules — in [`05-internals/03-configuration.md`](../05-internals/03-configuration.md#the-prompt-routers-output); the generated CLI reference omits both router entrypoints by design | diff --git a/.abcd/development/brief/05-internals/04-universal-patterns.md b/.abcd/development/brief/05-internals/04-universal-patterns.md index cbd295362..c43faf5ba 100644 --- a/.abcd/development/brief/05-internals/04-universal-patterns.md +++ b/.abcd/development/brief/05-internals/04-universal-patterns.md @@ -87,7 +87,7 @@ All inter-agent data is JSON; markdown is a render step at the end of each pass. **A design target: no shipped verb writes such a pair yet.** Every command emits `-report.json` (full structured detail) and `-report.md` (human skim summary, rendered from JSON). Both stored in `.abcd/.work.local/logs///`. -What ships today is narrower. Most verbs write no report at all: `lint` and its targets, `capture`, `intent`, `guard`, `--version`, `changelog`, `rules`, `spec`, `identity` and `banlist` are read-only renders, and `abcd lint` in particular is documented as performing zero writes. Where run output exists it lands in the local ephemeral tier under `.abcd/.work.local/logs///`, which is the shape the pattern generalises. +What ships today is narrower. Most verbs write no report at all: `lint` and its targets, `capture`, `intent`, `guard`, `--version`, `rules`, `spec`, `identity` and `banlist` are read-only renders, and `abcd lint` in particular is documented as performing zero writes. Where run output exists it lands in the local ephemeral tier under `.abcd/.work.local/logs///`, which is the shape the pattern generalises. ## 6. `.abcd/.work.local/logs/` layout diff --git a/.abcd/development/brief/05-internals/08-skills.md b/.abcd/development/brief/05-internals/08-skills.md index 4817d245b..77c04b680 100644 --- a/.abcd/development/brief/05-internals/08-skills.md +++ b/.abcd/development/brief/05-internals/08-skills.md @@ -72,7 +72,7 @@ the transport-agnostic core. The mapping between command pages and binary verbs is one-to-one in neither direction, and both exceptions are deliberate. Five verbs have a Go verb and no -command page: `changelog`, `completion`, `hook`, `rules`, and `spec`. Three +command page: `completion`, `hook`, `rules`, `spec`, and `statusline`. Three command pages carry no Go verb of their own name: the host-delegated three above. Two of those three call no part of the binary at all; `/abcd:prepare-this-repo` is the exception, running the binary's audit, install and identity verbs as steps diff --git a/.abcd/development/principles/pre-existing-is-not-a-defence.md b/.abcd/development/principles/pre-existing-is-not-a-defence.md index 8dbd0bc6f..cfe774d7f 100644 --- a/.abcd/development/principles/pre-existing-is-not-a-defence.md +++ b/.abcd/development/principles/pre-existing-is-not-a-defence.md @@ -91,8 +91,8 @@ a release cut it takes the set-difference of issue-ledger membership between the anchor tag's tree and HEAD, keyed on record id, and refuses the cut when any record that difference names is still in `open/` and graded `major` or `critical`. The refusal names every blocking record and all three ways out, and it -reaches `abcd changelog` and `abcd launch ship` through the same `refusals` -channel as the surface guardrail. Membership is read from git rather than from +reaches the cut `abcd launch --dry-run` previews and `abcd launch ship` through +the same `refusals` channel as the surface guardrail. Membership is read from git rather than from the timestamp inside a record id, so the gate's verdict is not decided by a field the record it is judging can edit. diff --git a/.abcd/development/release/surface.json b/.abcd/development/release/surface.json index 226877295..384fb5a7e 100644 --- a/.abcd/development/release/surface.json +++ b/.abcd/development/release/surface.json @@ -942,14 +942,6 @@ } ] }, - { - "path": "abcd changelog", - "hidden": false, - "group": "agents", - "block": "agents", - "sentence": "Preview the next release cut's version, records, and guardrail verdict: Writes nothing; refuses outside a checkout, exiting 0 on a cut the gates would stop.", - "flags": [] - }, { "path": "abcd dashboard", "hidden": false, @@ -2479,7 +2471,7 @@ "hidden": false, "group": "release", "block": "people", - "sentence": "Preview the public launch bundle, its secret scan, and the release gates: Writes only its pre-flight report, to the local tier; refuses without --dry-run.", + "sentence": "Preview the release cut, the launch bundle, its secret scan and the gates: Writes only its pre-flight report, to the local tier; refuses without --dry-run.", "flags": [ { "name": "baseline", diff --git a/.abcd/rules.json b/.abcd/rules.json index 2b7bffa9d..5b9afc63f 100644 --- a/.abcd/rules.json +++ b/.abcd/rules.json @@ -58,7 +58,6 @@ "banlist", "build", "capture", - "changelog", "completion", "dashboard", "decide", diff --git a/agents/release-changelog-composer.md b/agents/release-changelog-composer.md index 55add0802..6025da74a 100644 --- a/agents/release-changelog-composer.md +++ b/agents/release-changelog-composer.md @@ -25,7 +25,7 @@ talk your way past — so write for the reader, and cite everything. ## What you read The **cut** — the emit step's JSON, produced by `abcd launch ship --json` (or the -read-only preview `abcd changelog --json`): +`cut` of the read-only preview `abcd launch --dry-run --json`): - `next_tag` — the derived version (e.g. `v0.4.1`). Copy it verbatim into your payload; never compute, guess, or "correct" it. diff --git a/commands/intent.md b/commands/intent.md index 659989c6d..0137c8bea 100644 --- a/commands/intent.md +++ b/commands/intent.md @@ -789,8 +789,8 @@ intent only: a bundle is planned without one and each member targeted after); Refused with nothing written: a draft, a shipped, superseded or discipline record, and a value that is neither shape. A target is a report, never a gate: -`launch --dry-run` and the release cut (`launch ship`, `abcd changelog`) list -every targeted intent still planned, and neither refuses on one. The cut moves +`launch --dry-run`, which previews the cut, and the release cut (`launch ship`) +list every targeted intent still planned, and neither refuses on one. The cut moves every target it passes — `next`, or a tag at or below the release it cuts — to `next`, whatever the following release is numbered, in the same write as the changelog, and the dated section names the move. Closing the diff --git a/commands/launch.md b/commands/launch.md index eadad383d..0d153c5da 100644 --- a/commands/launch.md +++ b/commands/launch.md @@ -1,21 +1,22 @@ --- name: launch -description: "Preview the public launch bundle, its secret scan, and the release gates: Writes only its pre-flight report, to the local tier; refuses without --dry-run." +description: "Preview the release cut, the launch bundle, its secret scan and the gates: Writes only its pre-flight report, to the local tier; refuses without --dry-run." argument-hint: "[--dry-run [--deep-smoke] [--baseline ] [--fetch-baseline]] | ship [--changelog-json ] [--payload-dir ] [--allow-dirty] [--fetch-baseline] | archive --out [--tag ] [--verify] [--repository ] | manifests --tree public|dev [--root ] | receipts | scaffold [--confirm] [--dependency-reauthor]" block: people --- # `/abcd:launch` release preview and release cut -`abcd --help` lists `launch` in the person's release group. `changelog`, the -read-only preview of the same cut, is in the agents-and-hosts block of -`abcd --help --agent`, and its line there names this page. +`abcd --help` lists `launch` in the person's release group. The preview +(`--dry-run`) also renders the release cut the next ship would make; there is +no separate changelog command. Two flows over the abcd binary, kept apart on purpose: -- **preview** (`dry-run`) — the bundle, the scan, and the pre-flight gates. Its - one write is its pre-flight report, under the gitignored - `.abcd/.work.local/logs/launch/`. +- **preview** (`dry-run`) — the bundle, the scan, the pre-flight gates, and the + release cut: the version the release would carry, the records it is made of, + and the guard verdict. Its one write is its pre-flight report, under the + gitignored `.abcd/.work.local/logs/launch/`. - **ship** — the release cut: derive the version from what shipped, compose the changelog prose and the release page, write them. It writes the dated section of `CHANGELOG.md`, the release page `RELEASE.md`, the outgoing page's copy @@ -370,10 +371,22 @@ Then summarise the JSON for the user: - `targets` — every planned intent that names a release it must land by (`target_release`, written by `abcd intent target`): its `id`, `path`, the `target_release` it names, and `invalid` when the value is not a legal - target. The plain preview prints each on a `targeted:` line and the - pre-flight report lists them under *Targeted, not shipped*. A target is a - report and never a refusal: it adds nothing to `would_refuse_on`. Relay the - list, so the user sees what was meant to land and has not. + target. The plain preview prints each on a `targeted:` line, in the cut + below when the cut was read, and the pre-flight report lists them under + *Targeted, not shipped*. A target is a report and never a refusal: it adds + nothing to `would_refuse_on`. Relay the list, so the user sees what was + meant to land and has not. +- `cut` — the release cut the next `launch ship` would make, read from HEAD at + the checkout root: exactly what its emit step derives (*1. Emit the cut* + below), with no prose, so no agent runs in a preview. Relay the derived + `next_tag` and `impact`, the records under `added` and `removed`, which of + them the release page would cite (`in_press_release`), the guard verdict + (`guard`) and the findings verdict (`findings`). A refused cut (`ready` + false) carries its `refusals`, each naming what blocks it; relay them as + information. A refused cut changes nothing about the preview's exit, which + stays the bundle's. When the cut could not be read at all, `cut_error` says + why in its place. The plain preview renders the cut after the bundle report, + under its own `abcd launch --dry-run —` line, as `launch ship` renders it. This is preview-only: publishing is not driven from this command. @@ -415,7 +428,8 @@ the second marks the intents the release page must cite (the user-facing intents that entered `shipped/` since the base tag; never an issue, an `impact: internal` intent, a removed intent or anything still planned). The human render lists them under `release page:`, or says `release page: none` for a cut that ships fixes -alone. Read-only preview of the same thing: `abcd changelog --json`. +alone. Read-only preview of the same thing: the `cut` in +`abcd launch --dry-run --json`. **The cut is read from HEAD, so an uncommitted record move refuses it.** The record set comes out of git at HEAD, never the working tree, so a spec close or @@ -428,8 +442,8 @@ the same reader as the pre-flight's dirty-tree gate, narrowed to those folders: dirt anywhere else is not the derivation's concern (the ingest's pre-flight judges the whole tree), and `--allow-dirty` does not waive it, because the cut would still be read from HEAD. Commit the records (or discard the change) and -run again. `abcd changelog` carries the same refusal, rendered `REFUSED` with -the paths; as the preview it still exits 0. +run again. The preview (`launch --dry-run`) carries the same refusal in its +cut, rendered `REFUSED` with the paths, and its exit stays the bundle's. The cut also lists every planned intent that names a release it must land by (`targets`, one `targeted:` line each in the render, and `targets_error` when @@ -484,8 +498,9 @@ that no brief chapter names. ### The findings gate -Both renders — `abcd changelog` and `abcd launch ship` — carry two lines about -the issue ledger, and they are the two most easily skipped lines in the report: +Both renders of the cut — in `abcd launch --dry-run` and in `abcd launch ship` +— carry two lines about the issue ledger, and they are the two most easily +skipped lines in the report: ``` findings: failed (2 unfixed finding(s) captured since v0.7.0) (1 record(s) deleted from the ledger since v0.7.0) diff --git a/docs/reference/cli/commands.md b/docs/reference/cli/commands.md index b7cc4577b..b976e81f9 100644 --- a/docs/reference/cli/commands.md +++ b/docs/reference/cli/commands.md @@ -633,12 +633,6 @@ Move an open issue to wontfix/ with the reason it is not acted on: Writes the mo abcd capture wontfix iss-2609010000000001 "the behaviour is the documented one" ``` -### `abcd changelog` - -Preview the next release cut's version, records, and guardrail verdict: Writes nothing; refuses outside a checkout, exiting 0 on a cut the gates would stop. - -**Usage:** `abcd changelog` - ### `abcd completion` Generate the autocompletion script for the specified shell @@ -2759,12 +2753,14 @@ abcd lab sweep lab-260901000000-0123abc ### `abcd launch` -Preview the public launch bundle, its secret scan, and the release gates: Writes only its pre-flight report, to the local tier; refuses without --dry-run. +Preview the release cut, the launch bundle, its secret scan and the gates: Writes only its pre-flight report, to the local tier; refuses without --dry-run. **Usage:** `abcd launch [flags]` Preview the release bundle and run the release gates with --dry-run; nothing is -published. +published. The preview also renders the release cut the next `launch ship` would +make: the derived version, the deciding impact, the records, and the guard verdict. +A refused cut is reported, and changes nothing about the preview's exit. The dirty-tree gate compares the working tree with HEAD byte for byte. The repository's content filters (filter..clean, .smudge and .process) are switched off for that @@ -2783,7 +2779,7 @@ configuration names runs; a submodule moved to a different commit still does. ``` --baseline string the release tag the payload parity diff measures against (default: the newest release tag) --deep-smoke also run the installability smoke's deep tier: render every command, skill and agent page's help in an isolated subprocess (always on in the cut) - --dry-run preview the launch bundle and gates without publishing + --dry-run preview the launch bundle, its gates and the release cut without publishing --fetch-baseline read the parity baseline from the tag's published plugin archive, verified against the release's checksums.txt (a network fetch; default: a fresh render at the tag) ``` diff --git a/internal/core/release/emit.go b/internal/core/release/emit.go index 3eac61c6a..c6e0dfeb3 100644 --- a/internal/core/release/emit.go +++ b/internal/core/release/emit.go @@ -1,5 +1,6 @@ // Package release is the transport-agnostic composition of a release cut: the -// deterministic half of `abcd launch ship` and the whole of `abcd changelog`. +// deterministic half of `abcd launch ship`, and the cut `abcd launch --dry-run` +// previews. // // It is a composition, not a new domain. The version arithmetic, the record // set-difference, the tag anchor and the surface guardrail all live in diff --git a/internal/core/surface/manifest_test.go b/internal/core/surface/manifest_test.go index 63557ecc3..51ac1adff 100644 --- a/internal/core/surface/manifest_test.go +++ b/internal/core/surface/manifest_test.go @@ -219,8 +219,8 @@ func TestManifestEntriesUsesRepoRelativePaths(t *testing.T) { // per-repo fact — adr-19's version-location contract is abcd's own precedent for // declaring such a fact rather than assuming it. A repo whose artefact is a // binary, an application bundle or a library has no plugin manifest, and reading -// its absence as a broken payload is what stops `abcd changelog` before anything -// else runs. +// its absence as a broken payload is what stops the release cut and its preview +// before anything else runs. // // Nothing about the guardrail is weakened: the removal case // (TestManifestEntriesAbsenceStillReportsTheRemoval) is caught as a BREAK diff --git a/internal/core/surface/sentences.go b/internal/core/surface/sentences.go index 5ec8bf913..da8e9aaa7 100644 --- a/internal/core/surface/sentences.go +++ b/internal/core/surface/sentences.go @@ -90,9 +90,6 @@ var sentences = map[string]string{ "abcd capture wontfix": "Move an open issue to wontfix/ with the reason it is not acted on: " + "Writes the moved record; refuses an id this ledger does not hold.", - "abcd changelog": "Preview the next release cut's version, records, and guardrail verdict: " + - "Writes nothing; refuses outside a checkout, exiting 0 on a cut the gates would stop.", - "abcd decide": "Mint an ADR id and lay the record's empty skeleton: " + "Writes one proposed record into the decisions store; refuses a missing or unusable title.", @@ -283,7 +280,7 @@ var sentences = map[string]string{ "abcd lab sweep": "Sweep a lab's documents for every retracted pattern: " + "Writes the sweep artefact, and a finding and halt on an unapplied correction; refuses an unknown lab.", - "abcd launch": "Preview the public launch bundle, its secret scan, and the release gates: " + + "abcd launch": "Preview the release cut, the launch bundle, its secret scan and the gates: " + "Writes only its pre-flight report, to the local tier; refuses without --dry-run.", "abcd launch archive": "Render the release's plugin archive: " + "Writes the archive into --out; refuses a dirty tree without --verify, and exits 1 when --verify finds it unpinned.", diff --git a/internal/surface/cli/cli.go b/internal/surface/cli/cli.go index 0c266bc51..b4be2a1eb 100644 --- a/internal/surface/cli/cli.go +++ b/internal/surface/cli/cli.go @@ -416,7 +416,9 @@ func NewRootCommand() *cobra.Command { launchCmd := &cobra.Command{ Use: "launch", Long: "Preview the release bundle and run the release gates with --dry-run; nothing is\n" + - "published.\n\n" + + "published. The preview also renders the release cut the next `launch ship` would\n" + + "make: the derived version, the deciding impact, the records, and the guard verdict.\n" + + "A refused cut is reported, and changes nothing about the preview's exit.\n\n" + "The dirty-tree gate compares the working tree with HEAD byte for byte. The repository's\n" + "content filters (filter..clean, .smudge and .process) are switched off for that\n" + "comparison, so no program a filter names runs. A filter the repository marks required\n" + @@ -436,6 +438,15 @@ func NewRootCommand() *cobra.Command { if !launchDryRun { return fmt.Errorf("abcd launch: pass --dry-run to preview the bundle (publishing is not wired at this stage)") } + // The preview carries the release cut, which is a fact about the + // repository, so the whole preview reads the checkout root wherever + // it is run, as the cut always has (iss-2609251713073532): from a + // subdirectory it no longer reads the subdirectory as a repository + // that declares nothing. Outside a checkout it reads the working + // directory, as it did, and refuses there as it did. + if root, rerr := gitutil.CheckoutRoot(cwd, "the release record"); rerr == nil { + cwd = root + } // The parity diff measures the payload against the previous // release's, rendered fresh at its tag unless --fetch-baseline asks // for the published archive (adr-38: the network answers only an @@ -473,7 +484,12 @@ func NewRootCommand() *cobra.Command { // Every preview leaves its pre-flight report in the local logs tier // (itd-65); the preview still refuses nothing and exits 0. rep.ReportPath, rep.ReportError = writePreflight(cwd, rep.PreflightReport(time.Now())) - return render(cmd.OutOrStdout(), asJSON, rep, func(w io.Writer) { + // Beside the bundle, the cut the release would make + // (spc-2610100613109045, decision 5). It is read after the bundle + // report, so a dry run that refuses still refuses as it did. + preview := launchPreview{DryRunReport: rep} + preview.Cut, preview.CutError = previewCut(cwd) + return render(cmd.OutOrStdout(), asJSON, preview, func(w io.Writer) { fmt.Fprintf(w, "abcd launch (dry-run) — version %s\n", rep.Version) fmt.Fprintf(w, " artefact kind: %s\n", rep.Kind) fmt.Fprintf(w, " scanned tree: %s\n", rep.ScannedTree) @@ -498,8 +514,12 @@ func NewRootCommand() *cobra.Command { } renderDeepSmoke(w, rep.DeepSmoke) renderParity(w, rep.Parity) - for _, tg := range rep.Targets { - fmt.Fprintf(w, " targeted: %s\n", targetLine(tg)) + // The cut below lists the same targeted intents and says which + // it moves, so they are listed here only when it was not read. + if preview.Cut == nil { + for _, tg := range rep.Targets { + fmt.Fprintf(w, " targeted: %s\n", targetLine(tg)) + } } fmt.Fprintf(w, " would publish: %v\n", rep.WouldPublish) for _, reason := range rep.WouldRefuseOn { @@ -520,10 +540,16 @@ func NewRootCommand() *cobra.Command { } else { fmt.Fprintf(w, " report: not written — %s\n", termsafe.Sanitize(rep.ReportError)) } + if preview.Cut == nil { + fmt.Fprintf(w, " cut: not read — %s\n", termsafe.Sanitize(preview.CutError)) + return + } + fmt.Fprintln(w) + renderCut(w, "abcd launch --dry-run", *preview.Cut) }) }, } - launchCmd.Flags().BoolVar(&launchDryRun, "dry-run", false, "preview the launch bundle and gates without publishing") + launchCmd.Flags().BoolVar(&launchDryRun, "dry-run", false, "preview the launch bundle, its gates and the release cut without publishing") launchCmd.Flags().BoolVar(&launchDeepSmoke, "deep-smoke", false, "also run the installability smoke's deep tier: render every command, skill and agent page's help in an isolated subprocess (always on in the cut)") launchCmd.Flags().StringVar(&launchBaseline, "baseline", "", @@ -554,7 +580,6 @@ func NewRootCommand() *cobra.Command { launchCmd.AddCommand(newLaunchSmokePagesCommand()) root.AddCommand(launchCmd) - root.AddCommand(newChangelogCommand(&asJSON)) // `reflect` is the release retrospective (itd-24): the seed a cut release's // interview opens from, and the one write that files it. root.AddCommand(newReflectCommand(&asJSON)) diff --git a/internal/surface/cli/helpgroups.go b/internal/surface/cli/helpgroups.go index e07142c88..fd1f0cd8d 100644 --- a/internal/surface/cli/helpgroups.go +++ b/internal/surface/cli/helpgroups.go @@ -98,7 +98,8 @@ type helpPlacement struct { // statusline with agents. P2 keeps drain with agents until itd-82 ships. P3 // makes peers and mode agents only, P4 report, spec and rules, and P6 banlist. // memory and source stay person's verbs until they merge into library (P4); -// changelog stays with agents until it merges into launch. Neither surface caps +// changelog has merged into launch as its preview (`launch --dry-run`, P4), so +// it has no placement. Neither surface caps // the person's list (the 2026-10-09 menu ruling), and the plugin menu lists the // same commands (TestPluginPersonListEqualsTheCLIs). The groups are the spec's // decision 1: ideate under records, reading and lab under checks, dashboard @@ -126,7 +127,6 @@ var helpPlacements = map[string]helpPlacement{ // loader, and spec the user-facing half of the spec lifecycle (the spec's // decision 2). "banlist": {group: groupAgents, page: "commands/banlist.md"}, - "changelog": {group: groupAgents, page: "commands/launch.md"}, "docs": {group: groupAgents, page: "commands/docs.md"}, "guard": {group: groupAgents, page: "commands/guard.md"}, "guard hook": {page: "commands/guard.md"}, diff --git a/internal/surface/cli/helpgroups_test.go b/internal/surface/cli/helpgroups_test.go index 4b3252a06..f2c46edc0 100644 --- a/internal/surface/cli/helpgroups_test.go +++ b/internal/surface/cli/helpgroups_test.go @@ -96,7 +96,7 @@ func TestRootHelpListsThePersonsGroups(t *testing.T) { t.Errorf("%s lists %v, want %v", group, entries[group], want) } } - for _, agentVerb := range []string{"implement", "statusline", "changelog", "mode", "history", "lint", "reflect", "rules", "spec"} { + for _, agentVerb := range []string{"implement", "statusline", "mode", "history", "lint", "reflect", "rules", "spec"} { for _, group := range peopleGroupTitles { for _, got := range entries[group] { if got == agentVerb { diff --git a/internal/surface/cli/intent_target_cli_test.go b/internal/surface/cli/intent_target_cli_test.go index 7ddf79fbc..fc41e13f7 100644 --- a/internal/surface/cli/intent_target_cli_test.go +++ b/internal/surface/cli/intent_target_cli_test.go @@ -105,9 +105,15 @@ func TestLaunchPreviewAndCutListTheTargetedIntent(t *testing.T) { if err != nil { t.Fatalf("dry-run: %v\n%s", err, plain) } - if !strings.Contains(string(plain), "targeted: itd-91 targets v0.4.1, not shipped") { + // The plain preview lists it once, in the cut it renders beside the + // bundle report, which also says the cut moves it (spc-2610100613109045 + // step 3); the bundle half does not repeat the line. + if !strings.Contains(cutSection(t, plain), "targeted: itd-91 targets v0.4.1, not shipped") { t.Errorf("the plain preview must list the targeted intent:\n%s", plain) } + if n := strings.Count(string(plain), "itd-91 targets v0.4.1"); n != 1 { + t.Errorf("the plain preview lists the targeted intent %d times, want once:\n%s", n, plain) + } // The cut: the emit step lists it and stays ready (exit 0). emitted, err := shipIn(t, r, "launch", "ship") @@ -170,7 +176,7 @@ func targetedLines(out []byte) map[string]string { // TestTheCutsDryRunNamesTheTargetsItMoves is iss-2610020718369838 at the // front door: the cut's dry run — the emit step of the ship verb and the -// read-only changelog preview, which share one render — marks the target the +// cut `launch --dry-run` previews, which share one render — marks the target the // cut passes as moving to next, and leaves the target past the cut unmarked, // so a reader no longer compares each target with the derived tag by hand. // The JSON carries the same list. @@ -184,12 +190,15 @@ func TestTheCutsDryRunNamesTheTargetsItMoves(t *testing.T) { const moved = "itd-91 targets v0.4.1, not shipped (.abcd/development/intents/planned/itd-91-due.md); the cut moves it to next" const stands = "itd-93 targets v0.5.0, not shipped (.abcd/development/intents/planned/itd-93-later.md)" - for _, args := range [][]string{{"launch", "ship"}, {"changelog"}} { + for _, args := range [][]string{{"launch", "ship"}, {"launch", "--dry-run"}} { out, err := shipIn(t, r, args...) if code := exitCodeOf(err); code != 0 { t.Fatalf("%v: a target must not refuse the cut: exit = %d\n%s", args, code, out) } lines := targetedLines(out) + if args[1] == "--dry-run" { + lines = targetedLines([]byte(cutSection(t, out))) + } if lines["itd-91"] != moved { t.Errorf("%v: the target the cut reaches must be named as moving:\n got %q\nwant %q", args, lines["itd-91"], moved) } diff --git a/internal/surface/cli/launch_preflight.go b/internal/surface/cli/launch_preflight.go index 2366e3cb0..791d36702 100644 --- a/internal/surface/cli/launch_preflight.go +++ b/internal/surface/cli/launch_preflight.go @@ -7,8 +7,45 @@ import ( "github.com/intentdriven/abcd/internal/core/launch" "github.com/intentdriven/abcd/internal/core/lint" + "github.com/intentdriven/abcd/internal/core/release" + "github.com/intentdriven/abcd/internal/gitutil" ) +// launchPreview is what `abcd launch --dry-run` reports: the bundle report and, +// beside it, the release cut the next `launch ship` would make — its derived +// version, deciding impact, records, guard verdict and findings +// (spc-2610100613109045, decision 5; the preview that was `abcd changelog`). +// The report is embedded so its JSON shape is unchanged; the cut is an +// addition to it. The cut is the deterministic emit and nothing more: no prose +// is composed and no agent runs in a preview. +type launchPreview struct { + launch.DryRunReport + // Cut is the cut the release would make, present whenever it could be + // read. A refused cut is information here, as it was in the changelog + // verb, and changes no exit code the dry run gives. + Cut *release.Cut `json:"cut,omitempty"` + // CutError says why the cut could not be read, in place of Cut. It + // refuses nothing either: the dry run's verdict is the bundle's. + CutError string `json:"cut_error,omitempty"` +} + +// previewCut reads the cut the dry run renders. It is read from the checkout +// root, as `launch ship` reads it, so a dry run from a subdirectory reports +// the same records, baseline and anchor tag as one from the root +// (iss-2609251713073532). A cut that cannot be read is returned as the reason, +// already path-scrubbed, never as an error. +func previewCut(cwd string) (*release.Cut, string) { + root, err := gitutil.CheckoutRoot(cwd, "the release record") + if err != nil { + return nil, scrubPaths(err) + } + cut, err := emitCut(root) + if err != nil { + return nil, scrubPaths(err) + } + return &cut, "" +} + // noLaunchPayloadGuidance is what a repository that declares kind plugin but // no launch payload is told (iss-2608270559313719). A plugin's bundle is its // include set, so there is nothing to preview, gate or stage until it declares diff --git a/internal/surface/cli/launch_preview_test.go b/internal/surface/cli/launch_preview_test.go new file mode 100644 index 000000000..99d8ec048 --- /dev/null +++ b/internal/surface/cli/launch_preview_test.go @@ -0,0 +1,190 @@ +package cli + +import ( + "encoding/json" + "path/filepath" + "strings" + "testing" + + "github.com/intentdriven/abcd/internal/gittest" +) + +// launch_preview_test.go — step 3 of spc-2610100613109045: `abcd changelog` +// merges into launch as a preview (decision 5). `abcd launch --dry-run` renders +// the deterministic cut beside its bundle report, a refused cut is information +// that changes no exit code the dry run gives, and the changelog verb is gone +// with no alias (adr-40). + +// previewFixture is shipFixture with the launch configuration a plugin's dry +// run reads, and no record shipped since the tag: each test adds the records +// its cut is made of. +func previewFixture(t *testing.T) *gittest.Repo { + t.Helper() + r := shipFixture(t) + r.Write(".abcd/config/artefact.json", `{"kind": "plugin"}`+"\n") + r.Write(".abcd/config/version-location.json", + `{"manifest_path": ".claude-plugin/plugin.json", "json_pointer": "/version"}`+"\n") + r.Write(".abcd/config/launch-payload.json", + `{"includes": [".claude-plugin", "CHANGELOG.md"]}`+"\n") + r.Commit("the release configuration") + refreshSurface(t, r) + return r +} + +// cutHeader is the first line of the cut the dry run renders. +const cutHeader = "abcd launch --dry-run — " + +// cutSection is the cut half of a plain dry run: everything from the cut's own +// header line on, so an assertion about the cut cannot be met by a line of the +// bundle report above it. +func cutSection(t *testing.T, out []byte) string { + t.Helper() + i := strings.Index(string(out), cutHeader) + if i < 0 { + t.Fatalf("the dry run renders no cut (no %q line):\n%s", cutHeader, out) + } + return string(out)[i:] +} + +// dryRunCut is the slice of the dry run's JSON these tests read: the bundle +// report's version beside the cut. +type dryRunCut struct { + Version string `json:"version"` + Cut *struct { + Ready bool `json:"ready"` + NextTag string `json:"next_tag"` + Impact string `json:"impact"` + Guard any `json:"guard"` + Refusals []struct { + Kind string `json:"kind"` + Reason string `json:"reason"` + } `json:"refusals"` + Added []struct { + ID string `json:"id"` + Title string `json:"title"` + } `json:"added"` + } `json:"cut"` +} + +func decodeDryRunCut(t *testing.T, out []byte) dryRunCut { + t.Helper() + var got dryRunCut + if err := json.Unmarshal(out, &got); err != nil { + t.Fatalf("launch --dry-run --json is not JSON: %v\n%s", err, out) + } + if got.Cut == nil { + t.Fatalf("launch --dry-run --json carries no cut:\n%s", out) + } + return got +} + +// TestLaunchDryRunRendersTheCut is A10's preview half: the dry run renders the +// cut the release would make, its derived version, its records and its guard +// verdict, in text and in --json, beside the bundle report; and a refused cut +// is reported as information, exit 0, as the dry run's exit is today. +func TestLaunchDryRunRendersTheCut(t *testing.T) { + t.Run("ready", func(t *testing.T) { + r := previewFixture(t) + r.Write(".abcd/development/intents/shipped/itd-73-derived-versioning.md", + "---\nid: itd-73\nimpact: additive\n---\n\n# A Version Is A Fact\n\nthe version is derived.\n") + r.Commit("ship an intent") + + out, err := shipIn(t, r, "launch", "--dry-run") + if code := exitCodeOf(err); code != 0 { + t.Fatalf("exit = %d, want 0\n%s\n%v", code, out, err) + } + if !strings.HasPrefix(string(out), "abcd launch (dry-run) — version ") { + t.Errorf("the bundle report no longer leads the dry run:\n%s", out) + } + cut := cutSection(t, out) + for _, want := range []string{ + cutHeader + "v0.4.0 -> v0.4.1 (additive)", + "guard: ", + "itd-73", "A Version Is A Fact", + } { + if !strings.Contains(cut, want) { + t.Errorf("the dry run's cut does not mention %q:\n%s", want, out) + } + } + + js, err := shipIn(t, r, "launch", "--dry-run", "--json") + if code := exitCodeOf(err); code != 0 { + t.Fatalf("--json exit = %d, want 0\n%s", code, js) + } + got := decodeDryRunCut(t, js) + if got.Version == "" { + t.Errorf("the bundle report's fields left the JSON:\n%s", js) + } + if !got.Cut.Ready || got.Cut.NextTag != "v0.4.1" || got.Cut.Impact != "additive" || got.Cut.Guard == nil { + t.Errorf("cut = %+v, want a ready v0.4.1 additive cut with its guard", *got.Cut) + } + if len(got.Cut.Added) != 1 || got.Cut.Added[0].ID != "itd-73" { + t.Errorf("cut.added = %+v, want itd-73", got.Cut.Added) + } + }) + + t.Run("refused", func(t *testing.T) { + r := previewFixture(t) + + out, err := shipIn(t, r, "launch", "--dry-run") + if code := exitCodeOf(err); code != 0 { + t.Fatalf("a refused cut changed the dry run's exit: %d, want 0\n%s\n%v", code, out, err) + } + cut := cutSection(t, out) + for _, want := range []string{cutHeader + "REFUSED", "empty-cut"} { + if !strings.Contains(cut, want) { + t.Errorf("the dry run's cut does not mention %q:\n%s", want, out) + } + } + + js, err := shipIn(t, r, "launch", "--dry-run", "--json") + if code := exitCodeOf(err); code != 0 { + t.Fatalf("--json exit = %d, want 0\n%s", code, js) + } + if got := decodeDryRunCut(t, js); got.Cut.Ready || len(got.Cut.Refusals) == 0 { + t.Errorf("cut = %+v, want a refused cut with its refusal", *got.Cut) + } + }) +} + +// TestChangelogIsAnUnknownCommand is A10's removal half: with the preview in +// launch, `abcd changelog` is an unknown command (no alias, adr-40), and its +// note names what answers instead rather than calling the binary stale for not +// knowing it. +func TestChangelogIsAnUnknownCommand(t *testing.T) { + root := stalePluginRoot(t) + setExecutable(t, filepath.Join(root, "abcd")) + code, stdout, stderr := runMain(t, "changelog") + if code != 2 || stdout != "" { + t.Fatalf("`abcd changelog` exit = %d, stdout %q; want exit 2 and an empty stdout", code, stdout) + } + if !strings.Contains(stderr, "unknown command") { + t.Errorf("`abcd changelog` is not refused as an unknown command:\n%s", stderr) + } + if !strings.Contains(stderr, "abcd launch --dry-run") { + t.Errorf("the refusal does not name the preview that replaced it:\n%s", stderr) + } + for _, not := range []string{"predates", "make build", "abcd update"} { + if strings.Contains(stderr, not) { + t.Errorf("the refusal names %q:\n%s", not, stderr) + } + } +} + +// TestPreviewCutOutsideACheckoutNamesTheCheckout carries the retired changelog +// verb's outside-a-checkout refusal onto the preview: the cut the dry run +// would render is not read, and the reason names the missing checkout rather +// than git's bare exit status or the caller's path. +func TestPreviewCutOutsideACheckoutNamesTheCheckout(t *testing.T) { + dir := t.TempDir() + cut, reason := previewCut(dir) + if cut != nil { + t.Fatalf("a cut was read outside a checkout: %+v", *cut) + } + if !strings.Contains(reason, "not inside a git repository") || strings.Contains(reason, "exit status") { + t.Errorf("reason = %q, want it to name the missing checkout", reason) + } + if strings.Contains(reason, dir) { + t.Errorf("reason leaks an absolute path: %q", reason) + } +} diff --git a/internal/surface/cli/ship.go b/internal/surface/cli/ship.go index b761285d0..519eada3b 100644 --- a/internal/surface/cli/ship.go +++ b/internal/surface/cli/ship.go @@ -699,45 +699,9 @@ func runShipIngest(cmd *cobra.Command, cwd string, raw []byte, payloadDir string return nil } -// newChangelogCommand builds `abcd changelog`, the DETERMINISTIC-ONLY preview of -// the next release cut. -// -// It renders exactly what `abcd launch ship` would emit — the derived version, -// the deciding impact, the record list, and the guardrail status — and no prose: -// the changelog text is composed once, at the reviewed ship, never twice with a -// chance of disagreeing. It performs ZERO writes. -// -// It always exits 0 (2 only on a structural fault), which is the deliberate -// difference from `launch ship`. This is a status render in the shape of `abcd -// capture` bare and `abcd launch --dry-run`: a refused cut is information a -// reader asked for, not a gate they tripped. The gate is the ship verb. -func newChangelogCommand(asJSON *bool) *cobra.Command { - return &cobra.Command{ - Use: "changelog", - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { - cwd, err := os.Getwd() - if err != nil { - return err - } - // The same root the ship verb reads, from wherever it is run. - root, err := gitutil.CheckoutRoot(cwd, "the release record") - if err != nil { - return &exitError{Code: 2, Msg: "abcd changelog: " + scrubPaths(err)} - } - cut, err := emitCut(root) - if err != nil { - return &exitError{Code: 2, Msg: "abcd changelog: " + scrubPaths(err)} - } - return render(cmd.OutOrStdout(), *asJSON, cut, func(w io.Writer) { - renderCut(w, "abcd changelog", cut) - }) - }, - } -} - -// renderCut writes the human rendering of a cut, shared by the preview and the -// ship verb so the two can never describe the same repository differently. +// renderCut writes the human rendering of a cut, shared by the preview (`launch +// --dry-run`) and the ship verb so the two can never describe the same +// repository differently. // // Every value that came out of a record — a title, a path, a refusal quoting // either — is sanitised: record frontmatter and prose are author-supplied text diff --git a/internal/surface/cli/ship_test.go b/internal/surface/cli/ship_test.go index 344421d06..bebcb0840 100644 --- a/internal/surface/cli/ship_test.go +++ b/internal/surface/cli/ship_test.go @@ -268,10 +268,12 @@ func TestLaunchShipIngestOnARefusedCutExits1(t *testing.T) { } } -// TestChangelogPreviewWritesNothing is deliverable 4's contract, asserted the -// only way that means anything: hash the whole tree before and after. -func TestChangelogPreviewWritesNothing(t *testing.T) { - r := shipFixture(t) +// TestLaunchDryRunCutWritesNothing is deliverable 4's contract, carried from +// the retired changelog verb onto the preview that replaced it, and asserted +// the only way that means anything: hash the whole tree before and after (the +// local tier, where the dry run keeps its pre-flight report, aside). +func TestLaunchDryRunCutWritesNothing(t *testing.T) { + r := previewFixture(t) r.Write(".abcd/development/intents/shipped/itd-73-x.md", "---\nid: itd-73\nimpact: additive\n---\n# x\n") // A standing release page, so the digest proves the preview neither // rewrites it nor archives it. @@ -279,27 +281,30 @@ func TestChangelogPreviewWritesNothing(t *testing.T) { r.Commit("ship an intent") before := cliTreeDigest(t, r.Root()) - out, err := shipIn(t, r, "changelog") + out, err := shipIn(t, r, "launch", "--dry-run") if code := exitCodeOf(err); code != 0 { t.Fatalf("exit = %d, want 0 (a preview always reports)\n%s", code, out) } + if !strings.Contains(cutSection(t, out), "v0.4.1") { + t.Errorf("the preview does not render the cut it read:\n%s", out) + } if after := cliTreeDigest(t, r.Root()); after != before { t.Errorf("the preview changed the working tree:\nbefore %s\nafter %s", before, after) } } -// TestChangelogPreviewRefusalStillExitsZero separates the preview from the gate: -// `abcd changelog` REPORTS a refused cut (like `launch --dry-run`), while -// `launch ship` exits non-zero on the same repository. -func TestChangelogPreviewRefusalStillExitsZero(t *testing.T) { - r := shipFixture(t) +// TestLaunchDryRunRefusedCutStillExitsZero separates the preview from the +// gate: `launch --dry-run` REPORTS a refused cut, as the retired changelog verb +// did, while `launch ship` exits non-zero on the same repository. +func TestLaunchDryRunRefusedCutStillExitsZero(t *testing.T) { + r := previewFixture(t) r.Commit("nothing shipped at all") - out, err := shipIn(t, r, "changelog") + out, err := shipIn(t, r, "launch", "--dry-run") if code := exitCodeOf(err); code != 0 { t.Fatalf("exit = %d, want 0\n%s", code, out) } - if !strings.Contains(string(out), "empty-cut") { + if !strings.Contains(cutSection(t, out), "empty-cut") { t.Errorf("preview does not name the refusal:\n%s", out) } @@ -308,28 +313,36 @@ func TestChangelogPreviewRefusalStillExitsZero(t *testing.T) { } } -// TestChangelogPreviewJSON pins the machine surface the next stages read. -func TestChangelogPreviewJSON(t *testing.T) { - r := shipFixture(t) +// TestLaunchDryRunCutJSON pins the machine surface the next stages read: the +// cut sits under the dry run's `cut` key, in the shape the retired changelog +// verb emitted at the top level. +func TestLaunchDryRunCutJSON(t *testing.T) { + r := previewFixture(t) r.Write(".abcd/development/intents/shipped/itd-73-x.md", "---\nid: itd-73\nimpact: additive\n---\n\n# Title\n\nsummary.\n") r.Commit("ship an intent") - out, err := shipIn(t, r, "changelog", "--json") + out, err := shipIn(t, r, "launch", "--dry-run", "--json") if code := exitCodeOf(err); code != 0 { t.Fatalf("exit = %d, want 0\n%s", code, out) } - var got struct { - Ready bool `json:"ready"` - NextTag string `json:"next_tag"` - Added []struct { - ID string `json:"id"` - Title string `json:"title"` - } `json:"added"` + var preview struct { + Cut *struct { + Ready bool `json:"ready"` + NextTag string `json:"next_tag"` + Added []struct { + ID string `json:"id"` + Title string `json:"title"` + } `json:"added"` + } `json:"cut"` } - if err := json.Unmarshal(out, &got); err != nil { - t.Fatalf("changelog --json is not JSON: %v\n%s", err, out) + if err := json.Unmarshal(out, &preview); err != nil { + t.Fatalf("launch --dry-run --json is not JSON: %v\n%s", err, out) + } + if preview.Cut == nil { + t.Fatalf("launch --dry-run --json carries no cut:\n%s", out) } + got := *preview.Cut if !got.Ready || got.NextTag != "v0.4.1" { t.Errorf("ready=%v next_tag=%q, want true v0.4.1", got.Ready, got.NextTag) } @@ -360,7 +373,7 @@ func TestLaunchShipWritesNothingYet(t *testing.T) { // refusal — and the diagnostic must be path-scrubbed, because it names files // under the caller's working directory. func TestShipStructuralFaultExits2(t *testing.T) { - for _, args := range [][]string{{"launch", "ship"}, {"changelog"}} { + for _, args := range [][]string{{"launch", "ship"}} { t.Run(strings.Join(args, " "), func(t *testing.T) { t.Chdir(t.TempDir()) out, err := runCLIErr(t, args...) @@ -379,14 +392,17 @@ func TestShipStructuralFaultExits2(t *testing.T) { // checkout read the same records, baseline and anchor tag as from its root. // Handing the working directory over as the root made the preview report an // empty cut and a missing baseline there with exit 0 (iss-2609251713073532). +// The preview is `launch --dry-run` now, whose bundle half read the +// subdirectory as a repository declaring no artefact kind and refused before +// any cut was read, so it resolves the checkout root as the cut does. func TestCutReadsTheWholeCheckoutFromASubdirectory(t *testing.T) { - r := shipFixture(t) + r := previewFixture(t) r.Write(".abcd/development/intents/shipped/itd-73-derived-versioning.md", "---\nid: itd-73\nimpact: additive\n---\n\n# A Version Is A Fact\n\nthe version is derived.\n") r.Commit("ship an intent") sub := filepath.Join(r.Root(), ".abcd", "development") - for _, args := range [][]string{{"changelog"}, {"launch", "ship"}} { + for _, args := range [][]string{{"launch", "--dry-run"}, {"launch", "ship"}} { t.Run(strings.Join(args, " "), func(t *testing.T) { t.Chdir(sub) out, err := runCLIErr(t, args...) @@ -402,10 +418,12 @@ func TestCutReadsTheWholeCheckoutFromASubdirectory(t *testing.T) { } } -// TestCutOutsideACheckoutNamesTheCheckout: outside any repository both verbs -// refuse naming what is missing, not with git's bare exit status. +// TestCutOutsideACheckoutNamesTheCheckout: outside any repository the cut +// refuses naming what is missing, not with git's bare exit status. The dry +// run's bundle half refuses there before the cut is read, as it always has; +// TestPreviewCutOutsideACheckoutNamesTheCheckout holds the cut's own reason. func TestCutOutsideACheckoutNamesTheCheckout(t *testing.T) { - for _, args := range [][]string{{"changelog"}, {"launch", "ship"}} { + for _, args := range [][]string{{"launch", "ship"}} { t.Run(strings.Join(args, " "), func(t *testing.T) { t.Chdir(t.TempDir()) out, err := runCLIErr(t, args...) @@ -508,21 +526,21 @@ func TestRenderEntriesShowsAnUnreadableShippedIn(t *testing.T) { } } -// TestChangelogPreviewListsThePageSet: the read-only preview names the intents +// TestLaunchDryRunCutListsThePageSet: the read-only preview names the intents // the release page will be composed from, and marks them in the entry list. -func TestChangelogPreviewListsThePageSet(t *testing.T) { - r := shipFixture(t) +func TestLaunchDryRunCutListsThePageSet(t *testing.T) { + r := previewFixture(t) r.Write(".abcd/development/intents/shipped/itd-73-x.md", "---\nid: itd-73\nimpact: additive\n---\n\n# A Version Is A Fact\n") r.Write(".abcd/development/intents/shipped/itd-97-y.md", "---\nid: itd-97\nimpact: internal\n---\n# Plumbing\n") r.Write(".abcd/work/issues/resolved/iss-51-crash.md", "---\nid: iss-51\nimpact: fix\n---\n# x\n") r.Commit("ship a mixed cut") - out, err := shipIn(t, r, "changelog") + out, err := shipIn(t, r, "launch", "--dry-run") if code := exitCodeOf(err); code != 0 { t.Fatalf("exit = %d, want 0\n%s", code, out) } - got := string(out) + got := cutSection(t, out) if !strings.Contains(got, "release page: 1 intent(s)") { t.Errorf("the preview does not count the page set:\n%s", got) } @@ -534,24 +552,24 @@ func TestChangelogPreviewListsThePageSet(t *testing.T) { t.Errorf("the entry list does not mark the page's intent:\n%s", got) } - jsonOut, err := shipIn(t, r, "changelog", "--json") + jsonOut, err := shipIn(t, r, "launch", "--dry-run", "--json") if exitCodeOf(err) != 0 || !strings.Contains(string(jsonOut), `"in_press_release": true`) { - t.Errorf("changelog --json does not carry in_press_release:\n%s", jsonOut) + t.Errorf("launch --dry-run --json does not carry in_press_release:\n%s", jsonOut) } } -// TestChangelogPreviewSaysNoPageForAFixesOnlyCut: with no user-facing intent the -// preview says no page will be written, and why. -func TestChangelogPreviewSaysNoPageForAFixesOnlyCut(t *testing.T) { - r := shipFixture(t) +// TestLaunchDryRunCutSaysNoPageForAFixesOnlyCut: with no user-facing intent +// the preview says no page will be written, and why. +func TestLaunchDryRunCutSaysNoPageForAFixesOnlyCut(t *testing.T) { + r := previewFixture(t) r.Write(".abcd/work/issues/resolved/iss-51-crash.md", "---\nid: iss-51\nimpact: fix\n---\n# x\n") r.Commit("a fix alone") - out, err := shipIn(t, r, "changelog") + out, err := shipIn(t, r, "launch", "--dry-run") if code := exitCodeOf(err); code != 0 { t.Fatalf("exit = %d, want 0\n%s", code, out) } - if !strings.Contains(string(out), "release page: none (no user-facing intent shipped; RELEASE.md stays as it is)") { + if !strings.Contains(cutSection(t, out), "release page: none (no user-facing intent shipped; RELEASE.md stays as it is)") { t.Errorf("the preview does not say no page will be written:\n%s", out) } } diff --git a/internal/surface/cli/ship_uncommitted_test.go b/internal/surface/cli/ship_uncommitted_test.go index 2504f8e07..0b7b00f45 100644 --- a/internal/surface/cli/ship_uncommitted_test.go +++ b/internal/surface/cli/ship_uncommitted_test.go @@ -98,28 +98,37 @@ func TestLaunchShipIgnoresDirtOutsideTheRecords(t *testing.T) { } } -// TestChangelogPreviewReportsUncommittedRecords: the preview is a status render -// that always exits 0 and reports a refused cut as information, so it carries -// the same refusal — loudly, as REFUSED with the paths — rather than a cut that -// silently leaves the record out. -func TestChangelogPreviewReportsUncommittedRecords(t *testing.T) { +// TestLaunchDryRunReportsUncommittedRecords: the preview is a status render +// that reports a refused cut as information and keeps its own exit, so it +// carries the same refusal — loudly, as REFUSED with the paths — rather than a +// cut that silently leaves the record out. +func TestLaunchDryRunReportsUncommittedRecords(t *testing.T) { r := shipWithUncommittedClose(t) + r.Write(".abcd/config/artefact.json", `{"kind": "plugin"}`+"\n") + r.Write(".abcd/config/launch-payload.json", `{"includes": [".claude-plugin", "CHANGELOG.md"]}`+"\n") - out, err := shipIn(t, r, "changelog") + out, err := shipIn(t, r, "launch", "--dry-run") if code := exitCodeOf(err); code != 0 { - t.Fatalf("exit = %d, want 0\n%s", code, out) + t.Fatalf("exit = %d, want 0\n%s\n%v", code, out, err) } + cut := cutSection(t, out) for _, want := range []string{"REFUSED", "uncommitted-records", uncommittedIntent} { - if !strings.Contains(string(out), want) { + if !strings.Contains(cut, want) { t.Errorf("preview does not mention %q:\n%s", want, out) } } - out, err = shipIn(t, r, "changelog", "--json") + out, err = shipIn(t, r, "launch", "--dry-run", "--json") if code := exitCodeOf(err); code != 0 { t.Fatalf("--json exit = %d, want 0\n%s", code, out) } - if got := decodeCut(t, out); got.Ready || len(got.Refusals) != 1 || got.Refusals[0].Kind != "uncommitted-records" { + var preview struct { + Cut *cutJSON `json:"cut"` + } + if err := json.Unmarshal(out, &preview); err != nil || preview.Cut == nil { + t.Fatalf("launch --dry-run --json carries no cut (%v):\n%s", err, out) + } + if got := *preview.Cut; got.Ready || len(got.Refusals) != 1 || got.Refusals[0].Kind != "uncommitted-records" { t.Errorf("ready=%v refusals=%+v, want the uncommitted-records refusal", got.Ready, got.Refusals) } } diff --git a/internal/surface/cli/staleusage.go b/internal/surface/cli/staleusage.go index 2f2a3f9df..c717f48f3 100644 --- a/internal/surface/cli/staleusage.go +++ b/internal/surface/cli/staleusage.go @@ -93,7 +93,8 @@ var pagesWithNoVerb = map[string]string{ // does not know a retired name is never called stale for it. A token here has // no page, so it is not in pagesWithNoVerb. var retiredTokens = map[string]string{ - "version": "`version` is a root flag, not a verb: run `abcd --version`, and bare `abcd` shows the installed version as the board's last line (asking for a newer release is the update verb's --check)", + "changelog": "`changelog` merged into launch as its preview: run `abcd launch --dry-run`, which renders the release cut (derived version, records, guard verdict) beside the bundle report", + "version": "`version` is a root flag, not a verb: run `abcd --version`, and bare `abcd` shows the installed version as the board's last line (asking for a newer release is the update verb's --check)", } // maxCommandPageBytes caps a command-page read; the pages are a few KiB. diff --git a/internal/surface/cli/surfaceparity_test.go b/internal/surface/cli/surfaceparity_test.go index 7eec9d010..37c9e1d0b 100644 --- a/internal/surface/cli/surfaceparity_test.go +++ b/internal/surface/cli/surfaceparity_test.go @@ -37,7 +37,6 @@ const bareCommandFile = "abcd" // gap. A verb added here must be a verb a plugin user has no business invoking — // never a verb whose command file merely has not been written yet. var cliOnlyVerbs = map[string]string{ - "changelog": "deterministic release-cut input consumed by `launch ship`; the plugin surface orchestrates the cut through commands/launch.md", "hook": "operator-internal host adapter set, live-wired from hooks/hooks.json and invoked by the harness, never by a user", "rules": "operator-internal rule injection driven by the prompt hook; its bare render is read-only diagnostics", "spec": "internal spec-store tooling for the intent lifecycle; the user-facing half is commands/intent.md", From f5268bbcf89709b5a9099b07440d857d15f4176a Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:37:52 +0100 Subject: [PATCH 2/7] docs: stop the manifest comment naming the retired changelog verb Refs: itd-2610090831227812 Assisted-by: Claude:claude-opus-5-5 --- internal/core/surface/manifest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/surface/manifest.go b/internal/core/surface/manifest.go index 1f8d08df3..4aeb878bb 100644 --- a/internal/core/surface/manifest.go +++ b/internal/core/surface/manifest.go @@ -71,7 +71,7 @@ const nameKey = "name" // An ABSENT manifest is not that case. It contributes no entries, because a repo // whose artefact is not a plugin — a binary, an application bundle, a library — // declares no plugin surface, and treating the absence as an unreadable payload -// stopped every caller of the snapshot before it ran, `abcd changelog` included +// stopped every caller of the snapshot before it ran, the cut preview included // (iss-2609100506255436). Where a plugin manifest lives is fixed; whether this // artefact has one is a per-repo fact, the same distinction adr-19 already drew // for the version location. Nor does absence hide a removal: a manifest that WAS From 2f90a032ce962c001dae6125df86bc71953d89f6 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:43:03 +0100 Subject: [PATCH 3/7] fix: point the loop and the docs lint at ahoy install, not prepare-this-repo prepare-this-repo folds into the ahoy page's install section in the next commit, so its name leaves the two strings that send a reader to it. The implement loop's brief refusal for a base with no AGENTS.md now names /abcd:ahoy install, the plugin command whose preparation merges the conventions into AGENTS.md. It names the plugin command, not the bare binary call, because a default `abcd ahoy install` writes no AGENTS.md. The repo lint's warning for docs/ with no .abcd/docs-lint.json now says to run abcd ahoy install, the binary step that seeds that file. Step 4 of spc-2610100613109045. Assisted-by: Claude:claude-opus-5-5 --- internal/core/implement/loop/brief.go | 10 +++++---- .../implement/loop/conventionsremedy_test.go | 17 ++++++++------- internal/core/repolint/rule_docs.go | 6 +++--- internal/core/repolint/rule_refusal_test.go | 21 +++++++++++++++++++ 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/internal/core/implement/loop/brief.go b/internal/core/implement/loop/brief.go index 067ebc470..41837dbf1 100644 --- a/internal/core/implement/loop/brief.go +++ b/internal/core/implement/loop/brief.go @@ -42,16 +42,18 @@ const ( const ConventionsFile = "AGENTS.md" // conventionsRemedy is the remedy a brief refused for a base without -// ConventionsFile carries. prepare-this-repo runs in the host agent and has no -// binary verb, so the remedy names the plugin command. +// ConventionsFile carries. The preparation that merges the conventions into +// ConventionsFile runs in the host agent from the ahoy page's install section, +// around the binary's install, which by default writes no ConventionsFile, so +// the remedy names the plugin command. const conventionsRemedy = "write the repository's conventions into " + ConventionsFile + - " on the default branch (`/abcd:prepare-this-repo` sets one up)" + " on the default branch (`/abcd:ahoy install` sets one up)" // DecisionsLogRel is the append-only decision log the brief quotes entries of. const DecisionsLogRel = ".abcd/work/DECISIONS.md" // The markers the working-conventions section sits between in a managed -// repository's AGENTS.md (commands/prepare-this-repo.md). +// repository's AGENTS.md (commands/ahoy.md, the install section). const ( conventionsBegin = "" diff --git a/internal/core/implement/loop/conventionsremedy_test.go b/internal/core/implement/loop/conventionsremedy_test.go index b716f0c2e..7a8aa3772 100644 --- a/internal/core/implement/loop/conventionsremedy_test.go +++ b/internal/core/implement/loop/conventionsremedy_test.go @@ -6,20 +6,23 @@ import ( ) // assertConventionsRemedy checks a brief refusal for a base without AGENTS.md -// names the plugin command that writes one. prepare-this-repo has no binary -// verb, so a remedy telling the person to run `abcd prepare-this-repo` sends -// them to a command that exits 2 as unknown. +// names the command that writes one. The preparation workflow that merges the +// conventions into AGENTS.md runs in the host agent from the ahoy page's +// install section (spc-2610100613109045, step 4), so the remedy names +// /abcd:ahoy install. The retired prepare-this-repo page is named nowhere: its +// slash form is a page that no longer exists, and `abcd prepare-this-repo` was +// never a verb, so either sends the person to a command that refuses. func assertConventionsRemedy(t *testing.T, err error) { t.Helper() r := mustRefusal(t, err) if r.Stage != string(StageBrief) || !strings.Contains(r.Reason, ConventionsFile) { t.Fatalf("want the missing %s refused at the brief: %+v", ConventionsFile, r) } - if !strings.Contains(r.Remedy, "/abcd:prepare-this-repo") { - t.Errorf("the remedy names the plugin command /abcd:prepare-this-repo: %q", r.Remedy) + if !strings.Contains(r.Remedy, "/abcd:ahoy install") { + t.Errorf("the remedy names the plugin command /abcd:ahoy install: %q", r.Remedy) } - if strings.Contains(r.Remedy, "`abcd prepare-this-repo`") { - t.Errorf("the remedy names a binary verb that does not exist: %q", r.Remedy) + if strings.Contains(r.Remedy, "prepare-this-repo") { + t.Errorf("the remedy names the retired prepare-this-repo: %q", r.Remedy) } } diff --git a/internal/core/repolint/rule_docs.go b/internal/core/repolint/rule_docs.go index 2dbe312a3..4fe74f026 100644 --- a/internal/core/repolint/rule_docs.go +++ b/internal/core/repolint/rule_docs.go @@ -52,8 +52,8 @@ func (docsCurrency) Eval(ctx Context) ([]Finding, error) { cfgPath := filepath.Join(ctx.RepoRoot, ".abcd", "docs-lint.json") // The engine reuse needs the repo's docs-lint config. A repo with docs/ but // no committed config cannot be linted — but the rule must not then read as a - // silent pass. Surface a warn that the check could not run (a - // prepare-this-repo gap), not an absence of drift. + // silent pass. Surface a warn that the check could not run (a gap the + // install closes, since it seeds the config), not an absence of drift. if present, err := fsutil.Exists(cfgPath); err != nil { return nil, err } else if !present { @@ -61,7 +61,7 @@ func (docsCurrency) Eval(ctx Context) ([]Finding, error) { RuleID: "docs-currency", Severity: SeverityWarn, File: ".abcd/docs-lint.json", - Message: "docs/ is present but .abcd/docs-lint.json is missing — docs currency cannot be checked; run prepare-this-repo", + Message: "docs/ is present but .abcd/docs-lint.json is missing — docs currency cannot be checked; run abcd ahoy install, which seeds it", }}, nil } diff --git a/internal/core/repolint/rule_refusal_test.go b/internal/core/repolint/rule_refusal_test.go index a4c3a6b66..6bc632338 100644 --- a/internal/core/repolint/rule_refusal_test.go +++ b/internal/core/repolint/rule_refusal_test.go @@ -108,3 +108,24 @@ func TestPositioningMissingBlockIsAnError(t *testing.T) { file(".abcd/development/brief.md", "# no identity block here\n").commit().run() requireRefusal(t, res, "identity-positioning", "identity") } + +// A docs/ folder with no .abcd/docs-lint.json cannot be checked, and the +// warning names the command that seeds the config. prepare-this-repo folded +// into `ahoy install` (spc-2610100613109045, step 4), and the binary's install +// is what writes .abcd/docs-lint.json, so the message names it and not the +// retired page. +func TestDocsWithoutConfigNamesAhoyInstall(t *testing.T) { + res := newFixtureRepo(t).conforming(). + file("docs/index.md", "# docs\n"). + commit().run() + f := findingFor(res, "docs-currency") + if f == nil { + t.Fatalf("no docs-currency finding for docs/ without a config: findings=%+v skipped=%v", res.Findings, res.Skipped) + } + if !strings.Contains(f.Message, "abcd ahoy install") { + t.Errorf("the message does not name `abcd ahoy install`: %s", f.Message) + } + if strings.Contains(f.Message, "prepare-this-repo") { + t.Errorf("the message names the retired prepare-this-repo: %s", f.Message) + } +} From aeb442d2635b20dc8bf45b1ab3fc2a2fa6404c31 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:44:08 +0100 Subject: [PATCH 4/7] feat: fold prepare-this-repo into ahoy install and retire its page The product thinker's placement of 2026-10-09 folds prepare-this-repo into ahoy install (itd-2610090831227812, decision 6 of its spec). The fold is page-only: the binary and its refusals do not change. commands/ahoy.md's install section now carries the whole preparation workflow around the binary call it already made: the ownership refusal, the authority ordering, orient, audit, adopt, the working-conventions block, the downstream rule and the definition of done. The account of what the binary install does follows under its own heading. A repository already prepared goes straight to the binary half. commands/prepare-this-repo.md is deleted with no alias (adr-40). prepare-this-repo leaves pagesWithNoVerb and record-lint's host_delegated list. `abcd prepare-this-repo`, never a verb, is still an unknown command; it moves to retiredTokens, whose note names /abcd:ahoy install. The onboarding, tool-conventions-file, persona and hooks-arming tests now read commands/ahoy.md; a new page test holds each phase to the install section and the old page absent. The brief folds 15-prepare-this-repo.md into a section of 01-ahoy.md, drops register row 15 and the index entry, and repoints the host- delegated paragraphs, 08-skills.md, 04-naming.md's exemption list, 16-lint.md (whose opening named the page as live), 08-abcd.md, 14-ingest.md and 19-identity.md. The release-gate manifest drops the chapter and the page, with its checker count, prompt hash and the receipt example rehashed. The two review flags on the deleted chapter are removed: the sentences they named left with it. README and commands/lint.md name /abcd:ahoy install. The bootstrap test that refuses an unrunnable `ahoy install` in README no longer counts the slash command, which the host runs and whose page resolves the binary through the plugin root before PATH. Step 4 of spc-2610100613109045. Assisted-by: Claude:claude-opus-5-5 --- .../brief/02-constraints/04-naming.md | 2 +- .../development/brief/04-surfaces/01-ahoy.md | 147 ++++++++- .../development/brief/04-surfaces/08-abcd.md | 7 +- .../brief/04-surfaces/14-ingest.md | 4 +- .../brief/04-surfaces/15-prepare-this-repo.md | 186 ----------- .../development/brief/04-surfaces/16-lint.md | 5 +- .../brief/04-surfaces/19-identity.md | 2 +- .abcd/development/brief/04-surfaces/README.md | 26 +- .../brief/05-internals/08-skills.md | 28 +- .abcd/development/release-gate/manifest.json | 7 +- .../release-gate/receipt.example.json | 2 +- .abcd/record-lint.json | 3 +- .abcd/work/brief-review-flags.json | 16 - README.md | 2 +- commands/ahoy.md | 301 +++++++++++++++++- commands/lint.md | 3 +- commands/prepare-this-repo.md | 291 ----------------- internal/core/ahoy/attribution_hook_test.go | 2 +- .../core/ahoy/guard_template_naming_test.go | 2 +- internal/core/ahoy/onboarding_test.go | 44 +-- .../core/ahoy/persona_conventions_test.go | 17 +- internal/core/ahoy/preparation_page_test.go | 82 +++++ internal/core/ahoy/prepare_hookspath_test.go | 21 +- internal/core/lint/config.go | 2 +- internal/surface/cli/bootstrap_test.go | 8 +- internal/surface/cli/staleusage.go | 12 +- .../surface/cli/staleusage_pageverb_test.go | 32 +- 27 files changed, 650 insertions(+), 604 deletions(-) delete mode 100644 .abcd/development/brief/04-surfaces/15-prepare-this-repo.md delete mode 100644 commands/prepare-this-repo.md create mode 100644 internal/core/ahoy/preparation_page_test.go diff --git a/.abcd/development/brief/02-constraints/04-naming.md b/.abcd/development/brief/02-constraints/04-naming.md index 8866157b7..b1b1e02fe 100644 --- a/.abcd/development/brief/02-constraints/04-naming.md +++ b/.abcd/development/brief/02-constraints/04-naming.md @@ -54,7 +54,7 @@ The exemptions carrying a rationale of their own: The remaining surfaces are exempt for the plain reason that no maritime cognate adds meaning: `banlist`, `consult`, `decide`, `docs`, `guard`, `history`, `ideate`, `identity`, `ingest`, `lint`, `memory`, -`prepare-this-repo`, `reading`, `rules`, `site`, `spec`, and `update`. They +`reading`, `rules`, `site`, `spec`, and `update`. They are registered here so the exemption is on the record. **Reserved meta-development commands** (later phases; named now to prevent diff --git a/.abcd/development/brief/04-surfaces/01-ahoy.md b/.abcd/development/brief/04-surfaces/01-ahoy.md index 3a4a2feee..63a233f0f 100644 --- a/.abcd/development/brief/04-surfaces/01-ahoy.md +++ b/.abcd/development/brief/04-surfaces/01-ahoy.md @@ -50,7 +50,9 @@ the table above is the sub-verb set, and the modes are the bare verb's flags. - **Install** installs or updates abcd in this repo, covering first install and upgrade alike. It runs the detection pass, then an apply pass over the - resulting gaps. + resulting gaps. Through the plugin page it is also how a repository is + prepared to abcd's working conventions, a workflow the host agent runs around + that binary step (below). - **Uninstall** is reversible removal: the marker block, abcd's own `PATH` entry where abcd owns it, and the provenance record that proves that ownership. It leaves `.abcd/` entirely intact, never mutates the hook @@ -239,6 +241,126 @@ before they are written, refusing any finding. A value is read from stdin only, and never printed, logged or written to a record; a call's record names the credential it used. +## Preparing a repository: the host-run half of the install + +Take a repository you own and bring it up to abcd's working conventions in one +sitting: the install's plugin page audits first, shows you the gap report, and +only then adopts the three-tier `.abcd/` layout, a working-conventions section +in `AGENTS.md`, an identity block, and the commit gates. What you get back is a +repo a fresh agent session can build and test from `AGENTS.md` alone. What it +costs you is a sign-off at each phase, which is the point: nothing is adopted +before you have seen what it would change. + +This half was its own plugin page until the product thinker's ruling of +2026-10-09 folded it into the install (itd-2610090831227812): the retired +page's name is gone with no alias, and typing it at a shell gets a note naming +the install instead. The fold is page-only. The binary's install is +unchanged and gains no refusal: it checks no ownership, so the workflow's first +phase does, as before, and a refusal in the binary would break installs that +work today. A repository already prepared, its three tiers in place and its +`AGENTS.md` carrying the working-conventions markers, needs only the binary's +apply pass. + +The workflow is an **interim bridge**. abcd cannot yet manage repositories +directly, so the page does by hand what the CLI will later take over, in a +shape the CLI can adopt without unpicking. It runs in the host agent from the +install section of [`commands/ahoy.md`](../../../../commands/ahoy.md), always +on the current repository. + +### What the preparation does + +- **Refuses on repos the user does not own.** The first phase checks the origin + remote and stops entirely: no audit, no writes. Imposing these conventions on + a third-party repo would interfere with its own development principles. +- **Audits before it touches anything.** It produces a gap report covering + existing structure, documentation shape, decision and working-state hygiene, + principles followed or violated, and privacy, and presents it before adopting + anything. A tool's own conventions file it finds, one an agent tool reads in + place of `AGENTS.md`, is named as setup names it, as holding the owner's words + or as only repeating `AGENTS.md`, and the report points at setup's offer to + retire the second kind rather than acting on it. +- **Adopts the conventions.** The three-tier layout; a merged, never + overwritten, `AGENTS.md` carrying verified repo facts plus a marked + working-conventions block; a registered identity block; and the commit gates. + `AGENTS.md` is the one conventions file it writes: it makes no other tool's + conventions file, as a link or as a copy + ([adr-2610030814023326](../../decisions/adrs/2610030814023326-agents-md-is-the-one-conventions-file.md)). + +### Where the binary does the work + +The workflow is markdown, but three of its steps are the binary's, so the +result does not depend on an agent's memory of what a convention looks like. + +The JSON form of `abcd lint` supplies the engine-backed conformance core, +read-only, which the workflow then supplements with the structural and +principles judgement the binary does not make. The identity verb's initialiser +records the identity block: the markdown itself, which stays the source of +truth, plus `.abcd/positioning.json`, the pointer recording where that block +lives and which surfaces render from it. The install's apply pass is the adopt +phase's workhorse, and it does more than write the commit gates: everything +this chapter describes, from the repo's settings file to the copy of the binary +on `PATH`, lands in that one run. It runs a second time, with the attribution +flag, where the user opts in: that run installs the committed +`prepare-commit-msg` prompt asking every commit to declare whether a tool +assisted it, and the choice is recorded, so a later install without the flag +keeps the hook. The identity verb's render is the follow-on surface and writes +nothing: it proposes a correction as a diff, and adopting it is always the +product thinker's move. + +### The flow + +Four phases, each gated on the one before. + +0. **Refuse unless owned.** Origin-remote ownership check; stop if it fails. +1. **Orient.** Read the abcd record from the plugin root: the three-tier + README, the brief, principles, ADRs, intents, the `docs/` Diátaxis rules, and + the lint configs as patterns. +2. **Conformance lint.** Run `abcd lint` in its JSON form, supplement it, write + the gap report to the target's `.abcd/.work.local/scratch/`, and present it + before any change. +3. **Adopt.** Create the three tiers with a repo-specific `CONTEXT.md`; migrate + a historical `.work/` layout at the repo root if one is found, proposing the + mapping and waiting for sign-off, and never leaving a repo with both homes; + merge into `AGENTS.md`; scaffold the commit gates through the apply pass; + register the identity block, adopting one the repo already carries rather + than re-interviewing; and, only where the user says the repo requires AI + disclosure, install the attribution hook. + +A committed hook is not a running hook until git is pointed at it, once per +clone, and the page keys that step on the hooks-path state detection reports: +it arms an unarmed clone, leaves a foreign hooks dispatcher alone, and does +nothing in an armed one. A declined config change is reported as +scaffolded-but-unarmed rather than passed over silently. + +When abcd's own record has conflicting sources, the workflow trusts a fixed +authority order: `AGENTS.md`, then `work/CONTEXT.md`'s live-constraints section, +then ratified ADRs, then everything else read for understanding only. + +### The preparation's boundaries + +- **Nameless, self-contained output.** The working-conventions block written + into `AGENTS.md` never mentions abcd, the page, or any private repository: + the conventions read as the repo's own, between dated markers so later tooling + can find and replace them. The paths it states into the `.abcd/` layout are + the one trace of the tool, and the adopter accepts that namespace by adopting + the layout. The apply pass leaves the conventions files nameless as well + (below). +- **Never commit downstream assets.** Anything tooling will later provide + (persona data, lint-config JSON copied in by hand, content copied from the + abcd record) is applied, not copied. Only content about the target repository + is committed. The config files the apply pass seeds, `.abcd/docs-lint.json` + among them, are not downstream assets: each is the repository's own once it is + written, and the gates read it on every commit, so it is committed. +- **Privacy.** A `private-names.txt`, if present, is read-only context for the + audit and never reproduced in any committed or published artefact. +- **No secret-pattern hooks.** No hook the workflow scaffolds carries a + secret-pattern set or an absolute-path check. Absolute-path detection is the + binary's own `privacy-hygiene` lint rule, which the audit phase already runs. +- **Self-contained.** Every asset the adopt phase applies resolves from the + abcd record or from the binary, never from a path only one machine has: a + step reaching for one would silently do nothing on any other machine + (itd-162). + ## What abcd manages — repos and `~/.abcd.noindex/` abcd manages exactly one kind of folder, a **repository**, and keeps one @@ -827,7 +949,7 @@ both values, each with the one explanation `RetiredDocsTarget` holds, naming setting to `agents_md` or `skip` runs as any target change does and takes the block out of CLAUDE.md. The name-guard hooks and the ignore fence are the one sanctioned mention of abcd outside -`.abcd/` (ruled 2026-09-23; see prepare-this-repo). Every name-guard +`.abcd/` (ruled 2026-09-23; see the preparation's boundaries above). Every name-guard write is create-if-absent **and** contained: paths resolve through an `os.Root` opened at the repo, so a symlink committed at the hooks directory or at the local tier cannot land an artefact outside it. The private stub is written only where git @@ -983,6 +1105,27 @@ byte-identical to a fresh install save for the setup date. divergence is reported as its own gap, and with no terminal the install asks nothing, writes nothing and says so. +- **Given** a repo the user does not own, **when** they ask the plugin page + to install, **then** the preparation stops at the ownership phase with no + audit and no writes. +- **Given** an owned repo, **when** the preparation runs, **then** a gap report + exists under `.abcd/.work.local/scratch/` and was presented before anything + was adopted. +- **Given** sign-off, **when** it adopts, **then** the three-tier layout exists + with a repo-specific `CONTEXT.md`, `AGENTS.md` carries verified repo facts and + the marked nameless working-conventions section, the done-test passes (a fresh + agent can build and test from `AGENTS.md` alone), and any historical `.work/` + layout is fully migrated or fully left alone. +- **Given** sign-off, **when** it adopts, **then** one identity block is + recorded and registered, adopted where the repo already had one and + interviewed only where it did not, and the lint's identity report shows every + rendered surface against it. +- **Given** the adoption completes, **then** nothing from `private-names.txt` + and no abcd-internal content appears in any committed artefact, with one + sanctioned exception (ruled 2026-09-23): the name-guard hooks + (`.githooks/pre-commit`, `.githooks/pre-merge-commit`) and the `.gitignore` + fence name abcd, and cite none of its record ids. + ## Appendix: the shipped surface diff --git a/.abcd/development/brief/04-surfaces/08-abcd.md b/.abcd/development/brief/04-surfaces/08-abcd.md index b6b5f4f60..0d49e2a2f 100644 --- a/.abcd/development/brief/04-surfaces/08-abcd.md +++ b/.abcd/development/brief/04-surfaces/08-abcd.md @@ -128,7 +128,7 @@ is the front door today; an MCP server follows later, per [adr-23](../../decisions/adrs/0023-transport-agnostic-core.md)). Not every verb does: `consult` and `ingest` run as host-side markdown over the sources corpus, and reach the binary only through the `source` verb, which makes every write to -the corpus. `prepare-this-repo` is the mixed +the corpus. The install's preparation workflow, on the ahoy page, is the mixed case: its audit half runs `abcd lint`, and its adoption half is binary-backed too and writes — the identity verb's initialiser records the repo's identity block and registers the surfaces held to it, and the ahoy installer lays the hooks, the @@ -367,13 +367,14 @@ command surface beside the resolved plugin root documents the very verb or flag that was refused, the line says the binary predates it and names the remedy for where the binary sits. A page that documents no verb is not that evidence: the dispatcher page `abcd.md` documents the bare call, and the host-delegated pages -(`consult`, `ingest`, `prepare-this-repo`) run in the host agent, so the line +(`consult`, `ingest`) run in the host agent, so the line for one of those tokens says what it is instead — `abcd ` for the first, the `/abcd:` invocation for the rest — and never sends the reader to rebuild or update. Neither is a `status` or `show` sub-verb under a record verb (`capture`, `intent`, `spec`): the refusal names the record dispatcher, `abcd `, which answers that question. A name retired with no -alias (`version`, whose page retired when the board took the version) is +alias (`version`, whose page retired when the board took the version, and +`prepare-this-repo`, whose page retired into the install) is answered with what replaced it, and never as a stale binary. Failing that evidence, the disk-only vintage the version flag renders stands in. When neither says anything, the framework's line stands byte-for-byte. The exit code, the stream and the JSON envelope are the diff --git a/.abcd/development/brief/04-surfaces/14-ingest.md b/.abcd/development/brief/04-surfaces/14-ingest.md index abba2de67..9ac14354a 100644 --- a/.abcd/development/brief/04-surfaces/14-ingest.md +++ b/.abcd/development/brief/04-surfaces/14-ingest.md @@ -22,8 +22,8 @@ on an unknown command, and because a command page of that name exists, the binar adds a line saying the command has no binary verb and runs in the host agent, naming the `/abcd:ingest` invocation, never a rebuild or an update. For a host-delegated command that advice could never come true, because there is no Go verb for a rebuild to -bring in. Every host-delegated page has the same shape, `/abcd:consult` and -`/abcd:prepare-this-repo` alongside this one. What the note should say is that the +bring in. Every host-delegated page has the same shape, `/abcd:consult` +alongside this one. What the note should say is that the command runs in the host agent rather than at the CLI, and that is what the line says. ## Sub-verbs diff --git a/.abcd/development/brief/04-surfaces/15-prepare-this-repo.md b/.abcd/development/brief/04-surfaces/15-prepare-this-repo.md deleted file mode 100644 index adfe195a4..000000000 --- a/.abcd/development/brief/04-surfaces/15-prepare-this-repo.md +++ /dev/null @@ -1,186 +0,0 @@ -# `/abcd:prepare-this-repo` — Repo Onboarding Bridge - -Take a repository you own and bring it up to abcd's working conventions in one -sitting: it audits first, shows you the gap report, and only then adopts the -three-tier `.abcd/` layout, a working-conventions section in `AGENTS.md`, an -identity block, and the commit gates. What you get back is a repo a fresh agent -session can build and test from `AGENTS.md` alone. What it costs you is a -sign-off at each phase, which is the point: nothing is adopted before you have -seen what it would change. - -It is an **interim bridge**. abcd cannot yet manage repositories directly, so -the command does by hand what the CLI will later take over, in a shape the CLI -can adopt without unpicking. - -It is **host-delegated**: no dedicated Go verb backs it, and there is no -bare-status render. The workflow runs in the host agent from -[`commands/prepare-this-repo.md`](../../../../commands/prepare-this-repo.md). -It takes no argument, always operating on the current repository. - -Because the binary carries no verb of this name, typing `abcd prepare-this-repo` -at a shell is an unknown command rather than a route in. That refusal -says the command has no binary verb and runs in the host agent, naming `/abcd:prepare-this-repo`: the right reading for a -surface that is host-delegated by design, because what the reader needs is the plugin -command above. - -## Sub-verbs - -> _Machine-checked (`surface_coverage`, spc-27): each row records the verb's -> adr-40 bucket (`lint` / `review` / `audit` / `gate`, or `—` for a -> non-assessment verb) and its existence (`shipped` / `staged`). The existence -> fact is verified against the committed command-tree snapshot in both -> directions. The bucket cell is checked for membership of the closed adr-40 -> vocabulary only: the snapshot carries no bucket field, so a bucket that is -> wrong but legal passes, and that cell stays a review-grain claim._ - -| Verb | Bucket | Status | -|---|---|---| - -The table is empty: the command takes no argument and defines no sub-verb, and -its four phases run within one invocation. Because the command is -host-delegated, the rule's configuration exempts this table from the comparison -with the command tree, and from nothing else: the table is still required, and -any row it carries is still format-checked. - -## What it does - -- **Refuses on repos the user does not own.** The first phase checks the origin - remote and stops entirely: no audit, no writes. Imposing these conventions on - a third-party repo would interfere with its own development principles. -- **Audits before it touches anything.** It produces a gap report covering - existing structure, documentation shape, decision and working-state hygiene, - principles followed or violated, and privacy, and presents it before adopting - anything. A tool's own conventions file it finds, one an agent tool reads in - place of `AGENTS.md`, is named as setup names it, as holding the owner's words - or as only repeating `AGENTS.md`, and the report points at setup's offer to - retire the second kind rather than acting on it. -- **Adopts the conventions.** The three-tier layout; a merged, never - overwritten, `AGENTS.md` carrying verified repo facts plus a marked - working-conventions block; a registered identity block; and the commit gates. - `AGENTS.md` is the one conventions file it writes: it makes no other tool's - conventions file, as a link or as a copy - ([adr-2610030814023326](../../decisions/adrs/2610030814023326-agents-md-is-the-one-conventions-file.md)). - -## Where the binary does the work - -The command is markdown, but three of its steps are the binary's, so the result -does not depend on an agent's memory of what a convention looks like. - -The JSON form of `abcd lint` supplies the engine-backed conformance core, read-only, -which the command then supplements with the structural and principles judgement -the binary does not make. The identity verb's initialiser records the identity block: the -markdown itself, which stays the source of truth, plus `.abcd/positioning.json`, -the pointer recording where that block lives and which surfaces render from it. -The ahoy installer is the adopt phase's workhorse, and it does more than write -the commit gates. In one run it writes the repo's settings file with its -visibility, oracle backend and scan depth, writes its rule-loader overrides file, -installs a copy of the binary on `PATH`, records the repo in the machine's own -store, and offers to pin the git commit identity. abcd's own managed -rule-loader block names the tool, so it lands in `AGENTS.md`, and only there, -where the project chooses that docs target, and the repo classifies as managed on -its registry entry without it. A tool's own conventions file at the root, such as -`CLAUDE.md`, the installer never edits: one holding the repo's own words is named -in a warning, and one that only repeats `AGENTS.md` is offered for retirement at -a terminal and removed only on the answer `retire`. It runs a second time, with -the installer's attribution flag, where the user opts in: that run installs the committed `prepare-commit-msg` prompt asking every -commit to declare whether a tool assisted it, and the choice is recorded, so a -later install without the flag keeps the hook. The flag's spelling, like the docs -target's, is ahoy's shape, so it lives in the generated appendix of -[`01-ahoy.md`](01-ahoy.md#appendix-the-shipped-surface) and is not repeated here. - -The identity verb's render is the follow-on surface and writes nothing: it proposes -a correction as a diff, and adopting it is always the product thinker's move. - -## Flow - -Four phases, each gated on the one before. - -0. **Refuse unless owned.** Origin-remote ownership check; stop if it fails. -1. **Orient.** Read the abcd record from the plugin root: the three-tier - README, the brief, principles, ADRs, intents, the `docs/` Diátaxis rules, and - the lint configs as patterns. -2. **Conformance lint.** Run `abcd lint` in its JSON form, supplement it, write the gap - report to the target's `.abcd/.work.local/scratch/`, and present it before - any change. -3. **Adopt.** Create the three tiers with a repo-specific `CONTEXT.md`; migrate - a historical `.work/` layout at the repo root if one is found, proposing the - mapping and waiting for sign-off, and never leaving a repo with both homes; - merge into `AGENTS.md`; scaffold the commit gates; register the identity - block, adopting one the repo already carries rather than re-interviewing; and, - only where the user says the repo requires AI disclosure, install the - attribution hook. - -A committed hook is not a running hook until `git config core.hooksPath -.githooks` points git at it, once per clone. A declined config change is -reported as scaffolded-but-unarmed rather than passed over silently. - -When abcd's own record has conflicting sources, the command trusts a fixed -authority order: `AGENTS.md`, then `work/CONTEXT.md`'s live-constraints section, -then ratified ADRs, then everything else read for understanding only. - -## Boundaries - -- **Nameless, self-contained output.** The working-conventions block written - into `AGENTS.md` never mentions abcd, this command, or any private repository: - the conventions read as the repo's own, between dated markers so later tooling - can find and replace them. The paths it states into the `.abcd/` layout are - the one trace of the tool, and the adopter accepts that namespace by adopting - the layout. The install leaves the conventions files nameless as well. The - name-guard hooks and the `.gitignore` fence it commits are the one sanctioned - mention outside that namespace: the hooks run the binary, the fence warns - against hand-editing, and both keep the markers detection finds an adopted - repository by (the product thinker's ruling of 2026-09-23). -- **Never commit downstream assets.** Anything tooling will later provide - (persona data, lint-config JSON copied in by hand, content copied from the - abcd record) is applied, not copied. Only content about the target repository - is committed. The config files the install seeds, `.abcd/docs-lint.json` - among them, are not downstream assets: each is the repository's own once it is - written (the docs-lint config carries its banned-names family and its - house-style answer), and the gates read it on every commit, so it is committed. -- **Privacy.** A `private-names.txt`, if present, is read-only context for the - audit and never reproduced in any committed or published artefact. -- **No secret-pattern hooks.** No hook this command scaffolds carries a - secret-pattern set or an absolute-path check. Absolute-path detection is the - binary's own `privacy-hygiene` lint rule, which the audit phase already runs. - -## Acceptance - -- **Given** a repo the user does not own, **when** they run - `/abcd:prepare-this-repo`, **then** it stops at the ownership phase with no - audit and no writes. -- **Given** an owned repo, **when** the command runs, **then** a gap report - exists under `.abcd/.work.local/scratch/` and was presented before anything - was adopted. -- **Given** sign-off, **when** it adopts, **then** the three-tier layout exists - with a repo-specific `CONTEXT.md`, `AGENTS.md` carries verified repo facts and - the marked nameless working-conventions section, the done-test passes (a fresh - agent can build and test from `AGENTS.md` alone), and any historical `.work/` - layout is fully migrated or fully left alone. -- **Given** sign-off, **when** it adopts, **then** one identity block is - recorded and registered, adopted where the repo already had one and - interviewed only where it did not, and the lint's identity report shows every rendered - surface against it. -- **Given** the adoption completes, **then** nothing from `private-names.txt` - and no abcd-internal content appears in any committed artefact, with one - sanctioned exception (ruled 2026-09-23): the name-guard hooks - (`.githooks/pre-commit`, `.githooks/pre-merge-commit`) and the `.gitignore` - fence name abcd, and cite none of its record ids. - -## Composition - -`/abcd:prepare-this-repo` is one of the three host-delegated user-facing -commands that carry no Go verb; [`/abcd:consult`](13-consult.md) and -[`/abcd:ingest`](14-ingest.md) are the others. Its end state is the `.abcd/` -layout the shipped abcd surfaces then operate over. - -## References - -- Plugin command: [`commands/prepare-this-repo.md`](../../../../commands/prepare-this-repo.md) -- The three-tier layout it adopts: [`../02-constraints/01-platform.md`](../02-constraints/01-platform.md) and the abcd `.abcd/README.md` -- The invariants the working-conventions block encodes: [`../02-constraints`](../02-constraints) - - - -It ships as a host-delegated command page: the command tree registers no `abcd prepare-this-repo` verb, so there are no flags and no sub-verbs to list. - - diff --git a/.abcd/development/brief/04-surfaces/16-lint.md b/.abcd/development/brief/04-surfaces/16-lint.md index 574b70c9f..e5869cf6f 100644 --- a/.abcd/development/brief/04-surfaces/16-lint.md +++ b/.abcd/development/brief/04-surfaces/16-lint.md @@ -9,7 +9,8 @@ order. It is **read-only** but for one target: bare and every other target perform zero writes, and the site target renders the site into its output directory when that directory holds no `index.html`, and gates what it rendered -([`22-site.md`](22-site.md)). Remediation stays with `/abcd:prepare-this-repo` +([`22-site.md`](22-site.md)). Remediation stays with the install's preparation +workflow ([`01-ahoy.md`](01-ahoy.md#preparing-a-repository-the-host-run-half-of-the-install)) and the technical facilitator. It answers a different question from `/abcd:ahoy`: `ahoy` reports whether the *tool* is installed and configured for a repo; `lint` reports whether the *repo* conforms. Two @@ -173,7 +174,7 @@ new dependency. - Plugin command: [`commands/lint.md`](../../../../commands/lint.md) - Design record: [`plans/2026-07-13-abcd-audit-verb.md`](../../plans/2026-07-13-abcd-audit-verb.md) - Intent: [`itd-85`](../../intents/drafts/itd-85-audit-verb.md) -- Onboarding consumer: [`15-prepare-this-repo.md`](15-prepare-this-repo.md) +- Onboarding consumer: [`01-ahoy.md`](01-ahoy.md#preparing-a-repository-the-host-run-half-of-the-install) diff --git a/.abcd/development/brief/04-surfaces/19-identity.md b/.abcd/development/brief/04-surfaces/19-identity.md index f035e2ed1..7a929566e 100644 --- a/.abcd/development/brief/04-surfaces/19-identity.md +++ b/.abcd/development/brief/04-surfaces/19-identity.md @@ -128,7 +128,7 @@ the recorded three-variant tagline drift this check exists to catch. - Plugin command: [`commands/identity.md`](../../../../commands/identity.md) - Spec: [`spc-19`](../../specs/closed/spc-19-your-repo-says-the-same-thing-about.md) - Intent: [`itd-102`](../../intents/shipped/itd-102-your-repo-says-the-same-thing-about.md) -- Onboarding consumer: [`15-prepare-this-repo.md`](15-prepare-this-repo.md) +- Onboarding consumer: [`01-ahoy.md`](01-ahoy.md#preparing-a-repository-the-host-run-half-of-the-install) - Conformance surface: [`16-lint.md`](16-lint.md) diff --git a/.abcd/development/brief/04-surfaces/README.md b/.abcd/development/brief/04-surfaces/README.md index b4656736b..adaa7ee30 100644 --- a/.abcd/development/brief/04-surfaces/README.md +++ b/.abcd/development/brief/04-surfaces/README.md @@ -26,7 +26,6 @@ are wiring rather than user-facing surface are listed separately under | 11 | `/abcd:history` | shipped | Keep session transcripts as a local, redacted corpus this project can study | [`11-history.md`](11-history.md) | | 13 | `/abcd:consult` | shipped | Ask the local sources corpus what prior work says, and record what it changed | [`13-consult.md`](13-consult.md) | | 14 | `/abcd:ingest` | shipped | Put a document or URL into the sources corpus with its reference metadata | [`14-ingest.md`](14-ingest.md) | -| 15 | `/abcd:prepare-this-repo` | shipped | Bring an owned repo up to abcd's conventions (interim bridge until abcd manages repos directly) | [`15-prepare-this-repo.md`](15-prepare-this-repo.md) | | 16 | `/abcd:lint` | shipped | Check whether this repo still conforms to the working conventions | [`16-lint.md`](16-lint.md) | | 17 | `/abcd:guard` | shipped | Find out whether a shell command is safe to run, and what to run instead | [`17-guard.md`](17-guard.md) | | 18 | `/abcd:ideate` | shipped | Put a big, unproven idea through an admission gauntlet and record the verdict either way | [`18-ideate.md`](18-ideate.md) | @@ -309,11 +308,12 @@ present the result: the markdown is the surface, the binary is the engine. Those commands stay thin — they call `abcd --json` and format the result, and never reimplement behaviour that belongs in the core. -The three host-delegated commands named below are the exception, and are -configured as such rather than being silently different: `/abcd:consult`, -`/abcd:ingest` and `/abcd:prepare-this-repo` back onto no verb of their own, so -`consult.md` and `ingest.md` invoke the binary nowhere at all and carry the -workflow itself, and `prepare-this-repo.md` calls other verbs on the way through. +The two host-delegated commands named below are the exception, and are +configured as such rather than being silently different: `/abcd:consult` and +`/abcd:ingest` back onto no verb of their own, and carry the workflow itself. +The preparation workflow that brings an owned repository up to the conventions +runs in the host agent too, but from `ahoy.md`'s install section, around the +binary's install, so it backs a verb. The directory is **flat**, and that is load-bearing rather than tidiness. A harness maps each `commands/` subdirectory to an extra namespace segment, so a @@ -326,7 +326,7 @@ documents is then an unknown command (iss-161). One file per verb, directly unde `abcd`, `ahoy`, `banlist`, `build`, `capture`, `consult`, `dashboard`, `decide`, `disembark`, `docs`, `drain`, `embark`, `guard`, `history`, `ideate`, `identity`, `implement`, `inbox`, `ingest`, `intent`, `lab`, `launch`, `lint`, `memory`, `mode`, `peers`, -`prepare-this-repo`, `reading`, `reflect`, `report`, `scribe`, `site`, `source`, `update`. +`reading`, `reflect`, `report`, `scribe`, `site`, `source`, `update`. `abcd.md` is the bare `/abcd` status board; every other file is `/abcd:`. @@ -344,11 +344,13 @@ the only reliable fix is a home outside the auto-discovery root. ## No skills **abcd ships zero skills** — the `/abcd:` surface is commands only, and there is -no `skills/` directory in the tree. `/abcd:consult`, `/abcd:ingest` and -`/abcd:prepare-this-repo` were once shipped as skills and are commands: each -mutates state — the sources corpus, its ledger, the target repo — which the -boundary rule makes command-shaped. They are **host-delegated** commands, with a -command page and no Go verb, so the workflow runs in the host agent. The +no `skills/` directory in the tree. `/abcd:consult` and `/abcd:ingest` were +once shipped as skills and are commands: each mutates state — the sources +corpus and its ledger — which the boundary rule makes command-shaped. They are +**host-delegated** commands, with a command page and no Go verb, so the +workflow runs in the host agent. The third workflow once shipped as a skill, +the preparation of a target repo, mutates that repo; its page retired into the +install section of `ahoy.md` (itd-2610090831227812). The skill/command boundary is documented in [`05-internals/08-skills.md`](../05-internals/08-skills.md). diff --git a/.abcd/development/brief/05-internals/08-skills.md b/.abcd/development/brief/05-internals/08-skills.md index 77c04b680..1cbfb05aa 100644 --- a/.abcd/development/brief/05-internals/08-skills.md +++ b/.abcd/development/brief/05-internals/08-skills.md @@ -7,11 +7,13 @@ makes the call at design time, and records where abcd came out. **abcd ships zero skills.** The `/abcd:` namespace is commands only, and there is no `skills/` directory in the tree. The three workflows once shipped as skills — -`consult`, `ingest`, and `prepare-this-repo` — are commands, because each mutates -state: the sources corpus, its ledger, the target repo. They live at -`commands/.md` with chapters [`13-consult.md`](../04-surfaces/13-consult.md), -[`14-ingest.md`](../04-surfaces/14-ingest.md), and -[`15-prepare-this-repo.md`](../04-surfaces/15-prepare-this-repo.md). +`consult`, `ingest`, and the preparation of a target repo — are commands, +because each mutates state: the sources corpus, its ledger, the target repo. +The first two live at `commands/.md` with chapters +[`13-consult.md`](../04-surfaces/13-consult.md) and +[`14-ingest.md`](../04-surfaces/14-ingest.md); the preparation runs from the +install section of `commands/ahoy.md`, its chapter +[`01-ahoy.md`](../04-surfaces/01-ahoy.md#preparing-a-repository-the-host-run-half-of-the-install). The plugin carries two further surface kinds this choice does not cover, because neither is something a person invokes by name: the agent prompts under `agents/`, @@ -64,19 +66,19 @@ output, any artefact mutation, any state change makes it a command. instead. Most of abcd's commands are **binary-backed**: a Go verb plus a `commands/` file. -Three are **host-delegated** — `/abcd:consult`, `/abcd:ingest` and -`/abcd:prepare-this-repo` — with a command page and no Go verb, so the workflow -runs in the host agent, and they have no bare-status render and no sub-verbs. That +Two are **host-delegated** — `/abcd:consult` and `/abcd:ingest` — with a +command page and no Go verb, so the workflow runs in the host agent, and they +have no bare-status render and no sub-verbs. That is the shape a command takes when its work is host-delegated rather than owned by the transport-agnostic core. The mapping between command pages and binary verbs is one-to-one in neither direction, and both exceptions are deliberate. Five verbs have a Go verb and no -command page: `completion`, `hook`, `rules`, `spec`, and `statusline`. Three -command pages carry no Go verb of their own name: the host-delegated three above. -Two of those three call no part of the binary at all; `/abcd:prepare-this-repo` -is the exception, running the binary's audit, install and identity verbs as steps -inside a workflow the host drives. See +command page: `completion`, `hook`, `rules`, `spec`, and `statusline`. Two +command pages carry no Go verb of their own name: the host-delegated two above. +A page that backs a verb can still carry a workflow the host drives: the ahoy +page's install section prepares a repository by running the binary's audit, +install and identity verbs as steps. See [`04-surfaces/`](../04-surfaces) for per-command detail. ## Skills are not in `04-surfaces/` diff --git a/.abcd/development/release-gate/manifest.json b/.abcd/development/release-gate/manifest.json index e950cebae..165227f48 100644 --- a/.abcd/development/release-gate/manifest.json +++ b/.abcd/development/release-gate/manifest.json @@ -32,7 +32,6 @@ ".abcd/development/brief/04-surfaces/11-history.md", ".abcd/development/brief/04-surfaces/13-consult.md", ".abcd/development/brief/04-surfaces/14-ingest.md", - ".abcd/development/brief/04-surfaces/15-prepare-this-repo.md", ".abcd/development/brief/04-surfaces/16-lint.md", ".abcd/development/brief/04-surfaces/17-guard.md", ".abcd/development/brief/04-surfaces/18-ideate.md", @@ -87,10 +86,10 @@ "probe": "list skills/ (abcd ships zero skills; the directory is empty or absent)" } ], - "checkerCount": 45, - "promptHash": "sha256:a2abc8e72b60541927a0dbd966577f5cdabc717a483a070cdfe1f8bcb8a290c1", + "checkerCount": 44, + "promptHash": "sha256:6fcb0365f73f07072dd3fa47c1eb18eab46c129a0253ffcc33325bf8d9262ea2", "prompt": { - "context": "Repo root: the current working directory — use repo-relative paths\nthroughout, never absolute local paths. Ground truth is the SHIPPED surface,\nverified empirically: build the binary (make build produces bin/abcd--)\nand run it (`abcd --help` and `abcd --help`), and list commands/, agents/\nand skills/. abcd currently ships ZERO skills — the whole /abcd: surface is\ncommands under commands/ (abcd, ahoy, banlist, build, capture, consult, dashboard, decide, disembark, docs, drain, embark, guard,\nhistory, ideate, identity, implement, inbox, ingest, intent, lab, launch, lint, memory, mode, peers,\nprepare-this-repo, reading, reflect, report, scribe, site, source, update)\nand agent prompts under agents/ (cold-reading-comparative, cold-reading-detection, cold-reading-entailment,\ncold-reading-widening, docs-currency-reviewer, graveyard-interpreter,\nintent-auditor, lifeboat-reviewer, planning-interviewer, press-release-composer,\nprinciple-distiller, question-drafter, reflection-composer, release-changelog-composer, ruthless-reviewer, scribe,\nsecurity-reviewer, sota-researcher);\nskills/ is empty or absent. The brief chapters that carry surface claims are\npinned in this manifest's briefDocs: .abcd/development/brief/04-surfaces/*.md\n(the index README included) and the 02-constraints, 05-internals and 06-delivery\nchapters that count or enumerate verbs, sub-verbs, agents, hooks and the plugin\ntree. That list says where to LOOK, not what may be REPORTED: a surface claim\nin any other brief chapter is in scope, and a real surface whose only\ndocumented home lies outside the pinned list is reported with that location.\nReport DISCREPANCIES ONLY — where record and reality disagree, or one side is\nmissing. A brief row explicitly marked staged (its Status column is \"staged\")\n/ probe-only / later-phase is NOT a discrepancy; an unmarked claim about a\nsurface that does not exist IS. Do not fix anything.", + "context": "Repo root: the current working directory — use repo-relative paths\nthroughout, never absolute local paths. Ground truth is the SHIPPED surface,\nverified empirically: build the binary (make build produces bin/abcd--)\nand run it (`abcd --help` and `abcd --help`), and list commands/, agents/\nand skills/. abcd currently ships ZERO skills — the whole /abcd: surface is\ncommands under commands/ (abcd, ahoy, banlist, build, capture, consult, dashboard, decide, disembark, docs, drain, embark, guard,\nhistory, ideate, identity, implement, inbox, ingest, intent, lab, launch, lint, memory, mode, peers,\nreading, reflect, report, scribe, site, source, update)\nand agent prompts under agents/ (cold-reading-comparative, cold-reading-detection, cold-reading-entailment,\ncold-reading-widening, docs-currency-reviewer, graveyard-interpreter,\nintent-auditor, lifeboat-reviewer, planning-interviewer, press-release-composer,\nprinciple-distiller, question-drafter, reflection-composer, release-changelog-composer, ruthless-reviewer, scribe,\nsecurity-reviewer, sota-researcher);\nskills/ is empty or absent. The brief chapters that carry surface claims are\npinned in this manifest's briefDocs: .abcd/development/brief/04-surfaces/*.md\n(the index README included) and the 02-constraints, 05-internals and 06-delivery\nchapters that count or enumerate verbs, sub-verbs, agents, hooks and the plugin\ntree. That list says where to LOOK, not what may be REPORTED: a surface claim\nin any other brief chapter is in scope, and a real surface whose only\ndocumented home lies outside the pinned list is reported with that location.\nReport DISCREPANCIES ONLY — where record and reality disagree, or one side is\nmissing. A brief row explicitly marked staged (its Status column is \"staged\")\n/ probe-only / later-phase is NOT a discrepancy; an unmarked claim about a\nsurface that does not exist IS. Do not fix anything.", "directionA": "Direction A. Read ${doc} fully. Extract every checkable claim\nabout the shipped surface (verbs, sub-verbs, flags, skill names, counts,\nfile layouts, \"abcd ships N ...\" statements) and verify each against\nreality. Return item=\"${doc}\" and the discrepancy list.", "directionB": "Direction B. The real surface \"${s.name}\" (${s.kind}) exists:\ninspect it (${s.probe}). Search the brief's surface chapters for its\ndocumented home (grep .abcd/development/brief/). If no brief row documents\nit — or the brief documents it under a wrong name/shape — that is a\ndiscrepancy. Return item=\"${s.name}\" and the discrepancy list (empty if\nproperly documented)." } diff --git a/.abcd/development/release-gate/receipt.example.json b/.abcd/development/release-gate/receipt.example.json index 9f4993e20..6ec2b824c 100644 --- a/.abcd/development/release-gate/receipt.example.json +++ b/.abcd/development/release-gate/receipt.example.json @@ -8,7 +8,7 @@ "verificationResult": "PROMOTE", "judgeModel": "claude-opus-4-8", "tier": "full", - "manifestHash": "sha256:e74436245455cbfff190c8bf842b7b08912960c822f540ece376373f9bf94c19", + "manifestHash": "sha256:7fc5efa9b0332a6833d93fa130c2ee9aa55b5bf84e1b8aa0fd4f9aa8c21deb64", "policy": { "detector": "iss35-brief-surface-crosscheck", "version": "1", diff --git a/.abcd/record-lint.json b/.abcd/record-lint.json index 9b17dee64..382957cf7 100644 --- a/.abcd/record-lint.json +++ b/.abcd/record-lint.json @@ -396,8 +396,7 @@ "snapshot": ".abcd/development/release/surface.json", "host_delegated": [ "consult", - "ingest", - "prepare-this-repo" + "ingest" ], "operator_internal": [ "spec", diff --git a/.abcd/work/brief-review-flags.json b/.abcd/work/brief-review-flags.json index 4da3d5c96..88dc63e08 100644 --- a/.abcd/work/brief-review-flags.json +++ b/.abcd/work/brief-review-flags.json @@ -121,22 +121,6 @@ "commit": "6538ab4a6ec349b800ef4b7f8b03819d84c055c4", "applied": "2026-10-04T23:45:50Z" }, - { - "chapter": "15-prepare-this-repo.md", - "sentence": "at a shell is an unknown command rather than a route in. Today that refusal also", - "replacement": "at a shell is an unknown command rather than a route in. That refusal", - "evidence": "internal/surface/cli/staleusage.go:86", - "commit": "6538ab4a6ec349b800ef4b7f8b03819d84c055c4", - "applied": "2026-10-04T23:45:50Z" - }, - { - "chapter": "15-prepare-this-repo.md", - "sentence": "blames a stale binary and asks for a rebuild, which is the wrong reading for a", - "replacement": "says the command has no binary verb and runs in the host agent, naming `/abcd:prepare-this-repo`: the right reading for a", - "evidence": "internal/surface/cli/staleusage.go:86", - "commit": "6538ab4a6ec349b800ef4b7f8b03819d84c055c4", - "applied": "2026-10-04T23:45:50Z" - }, { "chapter": "24-decide.md", "sentence": "and only the citation resolver, the `abcd \u003crecord-id\u003e` dispatch and `decide`", diff --git a/README.md b/README.md index b5cbb34a0..2ff909f9f 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ The [install guide](docs/how-to/install.md) covers building from source and what ### Setup -In a plugin session, inside a repository you own, `/abcd:prepare-this-repo` audits the tree and adopts the working conventions: The three-tier `.abcd/` layout, an `AGENTS.md` router, and the commit gates. Bare `/abcd` (or `abcd` from a terminal) then shows where you are; the status board is read-only, so it is safe on any tree: +In a plugin session, inside a repository you own, `/abcd:ahoy install` audits the tree and adopts the working conventions: The three-tier `.abcd/` layout, an `AGENTS.md` router, and the commit gates. Bare `/abcd` (or `abcd` from a terminal) then shows where you are; the status board is read-only, so it is safe on any tree: ```text $ abcd diff --git a/commands/ahoy.md b/commands/ahoy.md index 2e542f2f0..450020d8e 100644 --- a/commands/ahoy.md +++ b/commands/ahoy.md @@ -20,7 +20,8 @@ Bare invocation, its `--dry-run`, `--remote` and `--providers` modes, and the asks before it does — and `credential --home` writes the chosen home only after the credential's verification call succeeds. A mode is a flag on the bare verb, one at a time; a distinct action is a -sub-verb. +sub-verb. `install` is also where a repository is prepared to abcd's working +conventions: that workflow runs in the host agent, around the binary's install. Read `$ARGUMENTS` for the sub-verb or the mode. No argument is the bare read-only detection pass below; the binary refuses any other word, `status` @@ -117,7 +118,303 @@ If there are actionable gaps, tell the user to run `/abcd:ahoy install` to apply them. If `folder_kind` is `unmanaged-folder`, note there is nothing to act on (not a git repo, no abcd markers). -## `install` — apply the outstanding gaps +## `install` — prepare the repository and apply the outstanding gaps + +`install` is how a repository is set up for abcd, the first time and every time +after, and it has two halves. The preparation is yours to run, in the host +agent: refuse a repository the user does not own, orient, audit, present the +gap report, then adopt the working conventions, one of whose steps is the +binary's install. The binary's install, `abcd ahoy install`, applies the gaps +the detection pass finds, and +[What the binary install does](#what-the-binary-install-does) below says how to +run it and relay it. The binary checks no ownership, so Phase 0 is the step +that does. + +A repository the preparation has already brought up to the conventions (the +three tiers in place and `AGENTS.md` carrying the working-conventions markers) +needs only the binary's half: go straight to it. + +The preparation is an interim bridge: abcd cannot yet manage repositories +directly, so it reads the abcd record and brings the current repository up to +its conventions in a way the abcd CLI can later take over without unpicking. It +supersedes the older `scaffold-repo` layout (`.work/` at the repo root); Phase 3 +migrates that layout when found. + +**Locate the record first.** This command file lives at +`/commands/ahoy.md` — the abcd repository root is two levels up +from it, the same root `${CLAUDE_PLUGIN_ROOT}` names (Phase 2 onward uses it +for the binary). Call that `$ABCD` below. Never assume any other checkout +location. + +### Phase 0 — Refuse unless the user owns this repo + +Check the origin remote (`git remote get-url origin`). If the repository is not +owned by the user (their account or an org they control) — or there is no +remote and the user cannot confirm ownership — **stop entirely**. No audit, no +local layer, no exceptions: imposing these conventions on a third-party repo +interferes with its own development principles. + +### Authority ordering + +Parts of the abcd record are still being reconciled. When sources conflict, +trust in this order: + +1. `$ABCD/AGENTS.md` — the binding conventions. +2. `$ABCD/.abcd/work/CONTEXT.md` — current phase and the **Live constraints / + sharp edges** section, which names what is not yet authoritative. +3. `$ABCD/.abcd/development/decisions/adrs/` — ratified decisions. +4. Everything else (brief, intents, research) — read for understanding, not as + gospel. + +### Phase 1 — Orient + +Read from `$ABCD`, delegating heavy reading to subagents where available: + +- `.abcd/README.md` — the three-tier layout (durability × sharing): + `development/` (durable record, committed), `work/` (shared working state, + committed), `.work.local/` (local ephemeral, gitignored). +- `.abcd/development/brief/` — the shape of a design brief (product, + constraints, evidence, surfaces, internals, delivery, glossary). +- `.abcd/development/principles/` — one principle per file, plus the + three-rung promotion ladder in its README. +- `.abcd/development/decisions/adrs/` — MADR, one `-.md` per + decision under a timestamp id (`adr-`). Session + decisions live in `work/DECISIONS.md`; architecture-shaping ones graduate. +- `.abcd/development/intents/` — lifecycle by directory (`drafts/` → + `planned/` → `shipped/` → `superseded/`). +- `docs/README.md` — strict Diátaxis, four directories, present tense only, + user-facing only. +- `.abcd/docs-lint.json` and `.abcd/record-lint.json` — study as patterns + (banned-token rules with severity, fix message, `allow_context` escapes). +- `.abcd/.work.local/private-names.txt` — **if present**: the banlist for the + Phase 2 privacy audit. Its contents are read-only context; never reproduce + them in anything committed or published. Do not read anything else under + `$ABCD/.abcd/.work.local/`. + +### Phase 2 — Audit + +The conformance core is **engine-backed**: run the binary's read-only audit and +build the gap report on its result, rather than hand-producing the whole thing. + +```bash +"${CLAUDE_PLUGIN_ROOT}/abcd" lint --json +``` + +Its `findings` (each with a stable `ruleId`, `severity`, `file`, a `line` where +applicable, `message`, `fix`) cover the conventions the binary checks — the three-tier +`.abcd/` layout, an `AGENTS.md` router, durable decisions, docs currency, +privacy hygiene (absolute local paths in committed files), and identity +positioning (rendered surfaces match the canonical identity block). Its +`skipped` list names rules that did not apply (e.g. `docs-currency` where there +is no `docs/`, or `identity-positioning` where there is no `.abcd/positioning.json`). +The exit code is the tri-state (`0` clean, `1` warnings, `2` any error). + +Resolve the binary by the ladder under **Binary resolution** at the end of +this page. + +Then supplement with the judgement the binary does not make: + +- **Existing structure (mandatory):** any convention layer already present — + a legacy `.work/` + `.work.local/` at the root, an existing `AGENTS.md` + router, pre-commit config — and where each piece maps in the three-tier + layout. +- **Tools' own conventions files:** a file at the root that an agent tool + reads in place of `AGENTS.md` (`CLAUDE.md`, `.claude/CLAUDE.md`, + `GEMINI.md`, `.rules`, `.cursorrules`, `.github/copilot-instructions.md`), + named as setup names it. One that holds the owner's own words hides + `AGENTS.md` from that tool; it is reported, with the way to end it (move + those words into `AGENTS.md` and remove the file), and never edited, moved + or removed. One that only repeats `AGENTS.md` (a link to it, that link saved + as a plain file, an exact copy, a single `@AGENTS.md` line, or nothing once + abcd's block is taken out) is reported as one setup offers to retire: point + the technical facilitator at that offer, which `ahoy install` asks at a + terminal, and leave the file in place here. +- **Principles:** which abcd principles the repo follows, violates, or has no + opinion on — cite files as evidence. +- **Privacy beyond absolute paths:** real hostnames/usernames/emails, private + repo names, or `private-names.txt` matches the `privacy-hygiene` rule's v1 + scope does not yet cover. A finding on a deliberately illustrative line can be + waived with `abcd-lint:allow` on that line (the earlier `abcd-audit:allow` spelling is honoured too). + +Write the report — the `abcd lint` findings plus these supplements — to the +target's `.abcd/.work.local/scratch/` (create the directory via +`.git/info/exclude` if needed) and present it before touching anything. + +### Phase 3 — Adopt + +1. **Three tiers.** Create `.abcd/development/` (subdirectories only where + there is content), `.abcd/work/` with a repo-specific `CONTEXT.md` (what the + repo is, current phase, sharp edges — never copied from abcd) and an empty + `DECISIONS.md`, and `.abcd/.work.local/` (`NEXT.md`, `scratch/`, `logs/`) + excluded via `.git/info/exclude`. +2. **Legacy migration — propose, then wait.** If Phase 2 found a legacy + `.work/` layout, present the mapping (`.work/DECISIONS.md` → + `.abcd/work/DECISIONS.md`, and so on) and get explicit sign-off before + moving anything. Moves are content-preserving (`git mv`, then fold), never + delete-and-recreate. Hard rule: never leave both `.work/` and `.abcd/work/` + behind — complete the migration or do not start it. +3. **AGENTS.md.** Merge — never overwrite — into the repo's `AGENTS.md` + (create it if absent). It is the one conventions file this step writes: no + other tool's conventions file is made, as a link or as a copy, so every + tool's instructions live in `AGENTS.md` alone. A tool that reads + `AGENTS.md` only once its own settings name it is configured by the + technical facilitator in those settings, never by a file this step adds. + - Repo facts: what the repo is, exact build/test/lint commands (verified by + running them, including how to run a single test), boundaries, definition + of done. The done-test: a fresh agent session must be able to build and + test the repo from `AGENTS.md` alone. + - The working-conventions section below, between markers. An existing + attribution policy in the file stays authoritative over the template. +4. **Identity — detect first, interview only if there is nothing to detect.** + A project's positioning fragments silently: the README strapline, the + package or plugin manifest description, and the conventions file's opening + are edited at different moments until three surfaces tell three stories. + One canonical block stops that. + + **Detect before asking.** Look for an identity block the repo already + carries — three bullets (`- **Title:**`, `- **Tagline:**`, `- **Pitch:**`) + under a heading such as "Identity (canonical)", typically in a brief or + product chapter under `.abcd/development/`. If one exists, register it and + ask nothing: + + ```bash + "${CLAUDE_PLUGIN_ROOT}/abcd" identity init --file --heading "" + ``` + + `init` adopts the existing block byte-for-byte and writes only the pointer. + Re-interviewing over an answer the repo already gives is how a project ends + up with two canons. + + **Only if there is no block**, ask the product thinker once, under a + `Product` chip, in their own words: + + - **Title** (required) — what the project is called, as it should read in a + heading. + - **Tagline** (required) — one line saying what it is. This is the line + every surface is held to. + - **Pitch** (optional — offer to skip) — two or three sentences a newcomer + could read and know whether this is for them. + + Then record the answers: + + ```bash + "${CLAUDE_PLUGIN_ROOT}/abcd" identity init --title "…" --tagline "…" [--pitch "…"] + ``` + + This writes the markdown block (markdown stays the source of truth) and + `.abcd/positioning.json`, which records where the block lives and which + surfaces render from it. From then on `abcd lint` reports any surface that + drifts from it, and `abcd identity render` proposes the correction as a + diff. abcd never rewrites a surface itself — adopting a proposal is always + the product thinker's move. + +5. **Commit gates.** Scaffold them from the binary — every hook it writes is + embedded in it, so the step applies the same artefacts on a fresh clone as on + the machine that wrote them: + + ```bash + "${CLAUDE_PLUGIN_ROOT}/abcd" ahoy install + ``` + + Run it, and relay what it asks and reports, as + [What the binary install does](#what-the-binary-install-does) below says. + Among the rest, that writes the committed private name guard + (`.githooks/pre-commit` and its `pre-merge-commit` half), the gitignored + local banlist stub, and the `.gitattributes` line pinning the hooks to LF. A committed hook is not a + running hook until git is pointed at it, so read which hooks directory git + runs in this clone from the detection pass + (`"${CLAUDE_PLUGIN_ROOT}/abcd" ahoy --json`, `banlist.hooks_path`) and act + on it: + + - `unarmed` — point git at the committed hooks, once per clone: + + ```bash + git config core.hooksPath .githooks + ``` + + Report the hooks as scaffolded but unarmed if the user declines that + config change — never silently. + - `foreign` — a hooks path set outside the clone (a global or system hooks + dispatcher) is in force. Leave it alone: never set a local + `core.hooksPath` over it, which would shadow the dispatcher in this clone. + Tell the user the dispatcher has to call the hook in `.githooks/` for the + guard to run, as the bare detection pass above says. + - `armed` — nothing to do. +6. **Attribution (opt-in only).** If the user says this repo requires + AI disclosure, scaffold the prompt hook from the binary and add the + attribution section to `AGENTS.md`: + + ```bash + "${CLAUDE_PLUGIN_ROOT}/abcd" ahoy install --attribution + ``` + + That writes `.githooks/prepare-commit-msg`, which seeds a commented + disclosure prompt into every commit message an editor opens, and records the + opt-in so a later plain `ahoy install` keeps the hook. It never writes a + value: which tool assisted, and which version, is a fact only the committer + has. Otherwise the default is no attribution, and nothing is written. + +### The working-conventions section + +Write this between `` and +`` markers (today's date; the markers let later +tooling find and replace the block). The section is **self-contained and +nameless**: it never mentions abcd, this page, or any private repository — +the conventions read as the repo's own. Adapt wording to the repo; keep the +substance: + +- Three-tier working state: `.abcd/development/` (durable record: ADRs in + `decisions/adrs/` using MADR under timestamp ids, dated plans and research notes), + `.abcd/work/` (committed: `CONTEXT.md` orientation, `DECISIONS.md` + append-only one-line decision log), `.abcd/.work.local/` (gitignored: + `NEXT.md` handover, `scratch/`, `logs/` — runtime artefacts go here, never + in tracked directories). +- Decisions: one dated line in `.abcd/work/DECISIONS.md`; promote + architecture-shaping decisions to ADRs. +- Docs: `docs/` is user-facing only, one Diátaxis type per page, present tense + only; British English prose, US English in code and commits. No stray root + markdown beyond the standard set. +- Privacy: no absolute local paths, hostnames, usernames, emails, tokens, or + private repository names in anything committed; repo-relative paths only. +- Examples and user stories take their personas from an open alphabetical + sequence — Alice, Bob, Carol, Dave, and on — that the repository extends + when it needs another; where the repository declares a persona roster, take + names and roles from it and pick by role. +- Refer to the product thinker and the technical facilitator as they/them in + every artefact. +- Never commit or push without being asked; substantive work goes on a branch + and PR; new dependencies need explicit sign-off first. + +### Never commit downstream + +Assets that will later be provided by tooling are applied, not copied: no +`personas.json`, no lint-config JSON copied in by hand, no content copied from +the abcd record. Commit only content that is about this repository. The config +files `abcd ahoy install` seeds, `.abcd/docs-lint.json` among them, are this +repository's own once written, and the gates read them on every commit, so they +are committed. + +### Definition of done + +- The gap report exists in the target's `.abcd/.work.local/scratch/` and was + presented to the technical facilitator. +- The three-tier layout exists with a repo-specific `CONTEXT.md`; any legacy + `.work/` layout was fully migrated (with sign-off) or fully left alone. +- `AGENTS.md` carries verified repo facts and the marked, nameless + working-conventions section; the done-test passes. +- One identity block is recorded and registered — adopted where the repo + already had one, interviewed only where it did not — and `abcd lint identity` + reports every registered surface as `ok`, or the drift it reports was shown + to the product thinker with the proposed diff. +- Nothing from `private-names.txt` and no abcd-internal content appears in any + committed or published artefact. +- Every asset the adopt phase applied resolved from this record or from the + binary. A step that reaches for a path only one machine has does not fail — + it silently does nothing, and the adoption degrades against loud-staging, so + the preparation above carries no home-relative path and the onboarding + self-containment check refuses one. + +### What the binary install does ```bash "${CLAUDE_PLUGIN_ROOT}/abcd" ahoy install --json diff --git a/commands/lint.md b/commands/lint.md index e080e2da4..0d8eafabf 100644 --- a/commands/lint.md +++ b/commands/lint.md @@ -13,7 +13,8 @@ present the result. Bare `lint` and every target but one perform **zero writes**; `lint site` renders the site into its `--out` directory (default `./site`, under the working directory) when that directory holds no `index.html`, and leaves the render there. It reports gaps, it never fixes -them (remediation stays with `/abcd:prepare-this-repo`). +them (remediation stays with `/abcd:ahoy install`, which prepares a repository +to the conventions). `lint` is the one check, with targets. Bare, it runs every target that judges the repository: the working conventions, the docs (`docs-currency`), the identity diff --git a/commands/prepare-this-repo.md b/commands/prepare-this-repo.md deleted file mode 100644 index 3d5fbf281..000000000 --- a/commands/prepare-this-repo.md +++ /dev/null @@ -1,291 +0,0 @@ ---- -name: prepare-this-repo -description: Prepare the current repository to abcd's working conventions — audit it against the abcd record, then adopt the three-tier .abcd/ layout, an AGENTS.md conventions section, and the commit gates. Interim bridge until abcd manages repos directly. Use when the user asks to prepare, onboard, scaffold, or bootstrap a repo for agent work, or to bring a repo up to current conventions. Owned repos only — refuses everywhere else. -block: people ---- - -# Prepare this repo - -An interim bridge: abcd cannot yet manage repositories directly, so this command -reads the abcd record and brings the current repository up to its conventions -in a way the abcd CLI can later take over without unpicking. It supersedes the -older `scaffold-repo` layout (`.work/` at the repo root); Phase 3 migrates that -layout when found. - -**Locate the record first.** This command file lives at -`/commands/prepare-this-repo.md` — the abcd repository root is two -levels up from it, the same root `${CLAUDE_PLUGIN_ROOT}` names (Phase 2 onward -uses it for the binary). Call that `$ABCD` below. Never assume any other -checkout location. - -## Phase 0 — Refuse unless the user owns this repo - -Check the origin remote (`git remote get-url origin`). If the repository is not -owned by the user (their account or an org they control) — or there is no -remote and the user cannot confirm ownership — **stop entirely**. No audit, no -local layer, no exceptions: imposing these conventions on a third-party repo -interferes with its own development principles. - -## Authority ordering - -Parts of the abcd record are still being reconciled. When sources conflict, -trust in this order: - -1. `$ABCD/AGENTS.md` — the binding conventions. -2. `$ABCD/.abcd/work/CONTEXT.md` — current phase and the **Live constraints / - sharp edges** section, which names what is not yet authoritative. -3. `$ABCD/.abcd/development/decisions/adrs/` — ratified decisions. -4. Everything else (brief, intents, research) — read for understanding, not as - gospel. - -## Phase 1 — Orient - -Read from `$ABCD`, delegating heavy reading to subagents where available: - -- `.abcd/README.md` — the three-tier layout (durability × sharing): - `development/` (durable record, committed), `work/` (shared working state, - committed), `.work.local/` (local ephemeral, gitignored). -- `.abcd/development/brief/` — the shape of a design brief (product, - constraints, evidence, surfaces, internals, delivery, glossary). -- `.abcd/development/principles/` — one principle per file, plus the - three-rung promotion ladder in its README. -- `.abcd/development/decisions/adrs/` — MADR, one `-.md` per - decision under a timestamp id (`adr-`). Session - decisions live in `work/DECISIONS.md`; architecture-shaping ones graduate. -- `.abcd/development/intents/` — lifecycle by directory (`drafts/` → - `planned/` → `shipped/` → `superseded/`). -- `docs/README.md` — strict Diátaxis, four directories, present tense only, - user-facing only. -- `.abcd/docs-lint.json` and `.abcd/record-lint.json` — study as patterns - (banned-token rules with severity, fix message, `allow_context` escapes). -- `.abcd/.work.local/private-names.txt` — **if present**: the banlist for the - Phase 2 privacy audit. Its contents are read-only context; never reproduce - them in anything committed or published. Do not read anything else under - `$ABCD/.abcd/.work.local/`. - -## Phase 2 — Audit - -The conformance core is **engine-backed**: run the binary's read-only audit and -build the gap report on its result, rather than hand-producing the whole thing. - -```bash -"${CLAUDE_PLUGIN_ROOT}/abcd" lint --json -``` - -Its `findings` (each with a stable `ruleId`, `severity`, `file`, a `line` where -applicable, `message`, `fix`) cover the conventions the binary checks — the three-tier -`.abcd/` layout, an `AGENTS.md` router, durable decisions, docs currency, -privacy hygiene (absolute local paths in committed files), and identity -positioning (rendered surfaces match the canonical identity block). Its -`skipped` list names rules that did not apply (e.g. `docs-currency` where there -is no `docs/`, or `identity-positioning` where there is no `.abcd/positioning.json`). -The exit code is the tri-state (`0` clean, `1` warnings, `2` any error). - -**Binary resolution.** Run `"${CLAUDE_PLUGIN_ROOT}/abcd"` — a plugin install -provisions the binary into the plugin root, so this is the rung that fires for a -plugin user. If that path does not exist, try `abcd` on `PATH`; if that fails -too, you are in a source checkout of this repo, where — and only there — -`go run ./cmd/abcd` works, the published payload carrying no `cmd/`. To put a -binary on `PATH`, run `ahoy install` through whichever rung just resolved: -`"${CLAUDE_PLUGIN_ROOT}/abcd" ahoy install`, `abcd ahoy install`, or -`go run ./cmd/abcd ahoy install` in a source checkout. - -Then supplement with the judgement the binary does not make: - -- **Existing structure (mandatory):** any convention layer already present — - a legacy `.work/` + `.work.local/` at the root, an existing `AGENTS.md` - router, pre-commit config — and where each piece maps in the three-tier - layout. -- **Tools' own conventions files:** a file at the root that an agent tool - reads in place of `AGENTS.md` (`CLAUDE.md`, `.claude/CLAUDE.md`, - `GEMINI.md`, `.rules`, `.cursorrules`, `.github/copilot-instructions.md`), - named as setup names it. One that holds the owner's own words hides - `AGENTS.md` from that tool; it is reported, with the way to end it (move - those words into `AGENTS.md` and remove the file), and never edited, moved - or removed. One that only repeats `AGENTS.md` (a link to it, that link saved - as a plain file, an exact copy, a single `@AGENTS.md` line, or nothing once - abcd's block is taken out) is reported as one setup offers to retire: point - the technical facilitator at that offer, which `ahoy install` asks at a - terminal, and leave the file in place here. -- **Principles:** which abcd principles the repo follows, violates, or has no - opinion on — cite files as evidence. -- **Privacy beyond absolute paths:** real hostnames/usernames/emails, private - repo names, or `private-names.txt` matches the `privacy-hygiene` rule's v1 - scope does not yet cover. A finding on a deliberately illustrative line can be - waived with `abcd-lint:allow` on that line (the earlier `abcd-audit:allow` spelling is honoured too). - -Write the report — the `abcd lint` findings plus these supplements — to the -target's `.abcd/.work.local/scratch/` (create the directory via -`.git/info/exclude` if needed) and present it before touching anything. - -## Phase 3 — Adopt - -1. **Three tiers.** Create `.abcd/development/` (subdirectories only where - there is content), `.abcd/work/` with a repo-specific `CONTEXT.md` (what the - repo is, current phase, sharp edges — never copied from abcd) and an empty - `DECISIONS.md`, and `.abcd/.work.local/` (`NEXT.md`, `scratch/`, `logs/`) - excluded via `.git/info/exclude`. -2. **Legacy migration — propose, then wait.** If Phase 2 found a legacy - `.work/` layout, present the mapping (`.work/DECISIONS.md` → - `.abcd/work/DECISIONS.md`, and so on) and get explicit sign-off before - moving anything. Moves are content-preserving (`git mv`, then fold), never - delete-and-recreate. Hard rule: never leave both `.work/` and `.abcd/work/` - behind — complete the migration or do not start it. -3. **AGENTS.md.** Merge — never overwrite — into the repo's `AGENTS.md` - (create it if absent). It is the one conventions file this step writes: no - other tool's conventions file is made, as a link or as a copy, so every - tool's instructions live in `AGENTS.md` alone. A tool that reads - `AGENTS.md` only once its own settings name it is configured by the - technical facilitator in those settings, never by a file this step adds. - - Repo facts: what the repo is, exact build/test/lint commands (verified by - running them, including how to run a single test), boundaries, definition - of done. The done-test: a fresh agent session must be able to build and - test the repo from `AGENTS.md` alone. - - The working-conventions section below, between markers. An existing - attribution policy in the file stays authoritative over the template. -4. **Identity — detect first, interview only if there is nothing to detect.** - A project's positioning fragments silently: the README strapline, the - package or plugin manifest description, and the conventions file's opening - are edited at different moments until three surfaces tell three stories. - One canonical block stops that. - - **Detect before asking.** Look for an identity block the repo already - carries — three bullets (`- **Title:**`, `- **Tagline:**`, `- **Pitch:**`) - under a heading such as "Identity (canonical)", typically in a brief or - product chapter under `.abcd/development/`. If one exists, register it and - ask nothing: - - ```bash - "${CLAUDE_PLUGIN_ROOT}/abcd" identity init --file --heading "" - ``` - - `init` adopts the existing block byte-for-byte and writes only the pointer. - Re-interviewing over an answer the repo already gives is how a project ends - up with two canons. - - **Only if there is no block**, ask the product thinker once, under a - `Product` chip, in their own words: - - - **Title** (required) — what the project is called, as it should read in a - heading. - - **Tagline** (required) — one line saying what it is. This is the line - every surface is held to. - - **Pitch** (optional — offer to skip) — two or three sentences a newcomer - could read and know whether this is for them. - - Then record the answers: - - ```bash - "${CLAUDE_PLUGIN_ROOT}/abcd" identity init --title "…" --tagline "…" [--pitch "…"] - ``` - - This writes the markdown block (markdown stays the source of truth) and - `.abcd/positioning.json`, which records where the block lives and which - surfaces render from it. From then on `abcd lint` reports any surface that - drifts from it, and `abcd identity render` proposes the correction as a - diff. abcd never rewrites a surface itself — adopting a proposal is always - the product thinker's move. - -5. **Commit gates.** Scaffold them from the binary — every hook it writes is - embedded in it, so the step applies the same artefacts on a fresh clone as on - the machine that wrote them: - - ```bash - "${CLAUDE_PLUGIN_ROOT}/abcd" ahoy install - ``` - - That writes the committed private name guard (`.githooks/pre-commit` and its - `pre-merge-commit` half), the gitignored local banlist stub, and the - `.gitattributes` line pinning the hooks to LF. A committed hook is not a - running hook until git is pointed at it, so read which hooks directory git - runs in this clone from the detection pass - (`"${CLAUDE_PLUGIN_ROOT}/abcd" ahoy --json`, `banlist.hooks_path`) and act - on it: - - - `unarmed` — point git at the committed hooks, once per clone: - - ```bash - git config core.hooksPath .githooks - ``` - - Report the hooks as scaffolded but unarmed if the user declines that - config change — never silently. - - `foreign` — a hooks path set outside the clone (a global or system hooks - dispatcher) is in force. Leave it alone: never set a local - `core.hooksPath` over it, which would shadow the dispatcher in this clone. - Tell the user the dispatcher has to call the hook in `.githooks/` for the - guard to run, as `/abcd:ahoy` says. - - `armed` — nothing to do. -6. **Attribution (opt-in only).** If the user says this repo requires - AI disclosure, scaffold the prompt hook from the binary and add the - attribution section to `AGENTS.md`: - - ```bash - "${CLAUDE_PLUGIN_ROOT}/abcd" ahoy install --attribution - ``` - - That writes `.githooks/prepare-commit-msg`, which seeds a commented - disclosure prompt into every commit message an editor opens, and records the - opt-in so a later plain `ahoy install` keeps the hook. It never writes a - value: which tool assisted, and which version, is a fact only the committer - has. Otherwise the default is no attribution, and nothing is written. - -### The working-conventions section - -Write this between `` and -`` markers (today's date; the markers let later -tooling find and replace the block). The section is **self-contained and -nameless**: it never mentions abcd, this command, or any private repository — -the conventions read as the repo's own. Adapt wording to the repo; keep the -substance: - -- Three-tier working state: `.abcd/development/` (durable record: ADRs in - `decisions/adrs/` using MADR under timestamp ids, dated plans and research notes), - `.abcd/work/` (committed: `CONTEXT.md` orientation, `DECISIONS.md` - append-only one-line decision log), `.abcd/.work.local/` (gitignored: - `NEXT.md` handover, `scratch/`, `logs/` — runtime artefacts go here, never - in tracked directories). -- Decisions: one dated line in `.abcd/work/DECISIONS.md`; promote - architecture-shaping decisions to ADRs. -- Docs: `docs/` is user-facing only, one Diátaxis type per page, present tense - only; British English prose, US English in code and commits. No stray root - markdown beyond the standard set. -- Privacy: no absolute local paths, hostnames, usernames, emails, tokens, or - private repository names in anything committed; repo-relative paths only. -- Examples and user stories take their personas from an open alphabetical - sequence — Alice, Bob, Carol, Dave, and on — that the repository extends - when it needs another; where the repository declares a persona roster, take - names and roles from it and pick by role. -- Refer to the product thinker and the technical facilitator as they/them in - every artefact. -- Never commit or push without being asked; substantive work goes on a branch - and PR; new dependencies need explicit sign-off first. - -### Never commit downstream - -Assets that will later be provided by tooling are applied, not copied: no -`personas.json`, no lint-config JSON copied in by hand, no content copied from -the abcd record. Commit only content that is about this repository. The config -files `abcd ahoy install` seeds, `.abcd/docs-lint.json` among them, are this -repository's own once written, and the gates read them on every commit, so they -are committed. - -## Definition of done - -- The gap report exists in the target's `.abcd/.work.local/scratch/` and was - presented to the technical facilitator. -- The three-tier layout exists with a repo-specific `CONTEXT.md`; any legacy - `.work/` layout was fully migrated (with sign-off) or fully left alone. -- `AGENTS.md` carries verified repo facts and the marked, nameless - working-conventions section; the done-test passes. -- One identity block is recorded and registered — adopted where the repo - already had one, interviewed only where it did not — and `abcd lint identity` - reports every registered surface as `ok`, or the drift it reports was shown - to the product thinker with the proposed diff. -- Nothing from `private-names.txt` and no abcd-internal content appears in any - committed or published artefact. -- Every asset the adopt phase applied resolved from this record or from the - binary. A step that reaches for a path only one machine has does not fail — - it silently does nothing, and the adoption degrades against loud-staging, so - this file carries no home-relative path and the onboarding self-containment - check refuses one. diff --git a/internal/core/ahoy/attribution_hook_test.go b/internal/core/ahoy/attribution_hook_test.go index 84f81f7d5..ff1a25096 100644 --- a/internal/core/ahoy/attribution_hook_test.go +++ b/internal/core/ahoy/attribution_hook_test.go @@ -114,7 +114,7 @@ func TestAttributionHookIsOptInAndIdempotent(t *testing.T) { }) t.Run("opting in on an already-installed repo", func(t *testing.T) { - // The exact two-step sequence prepare-this-repo's adopt phase prescribes: + // The exact two-step sequence the ahoy page's adopt phase prescribes: // scaffold the commit gates, then opt into attribution. On the second run // there is no other safe-autocreate gap open, so a step keyed on the // CATEGORY's approval writes nothing — the flag becomes a silent no-op in the diff --git a/internal/core/ahoy/guard_template_naming_test.go b/internal/core/ahoy/guard_template_naming_test.go index 3a0d23f4a..a47fb12f1 100644 --- a/internal/core/ahoy/guard_template_naming_test.go +++ b/internal/core/ahoy/guard_template_naming_test.go @@ -11,7 +11,7 @@ import ( // repository, because they run the binary — but the ruling covers the markers, // not abcd's own record ids. An adopter's repository holds none of abcd's // records, so an id or a path into abcd's design record is abcd-internal content -// that resolves to nothing there, and prepare-this-repo's acceptance keeps it out +// that resolves to nothing there, and the preparation workflow's acceptance keeps it out // of every committed artefact. func TestScaffoldedGuardHooksCiteNoAbcdRecord(t *testing.T) { recordID := regexp.MustCompile(`\b(itd|spc|iss|adr|rcp|rfc)-[0-9]+\b`) diff --git a/internal/core/ahoy/onboarding_test.go b/internal/core/ahoy/onboarding_test.go index ce04aceb7..28b9233b7 100644 --- a/internal/core/ahoy/onboarding_test.go +++ b/internal/core/ahoy/onboarding_test.go @@ -1,20 +1,11 @@ package ahoy import ( - "os" - "path/filepath" "regexp" "strings" "testing" ) -// onboardingArtefacts are the committed files the adopt phase reads and applies -// from. Every asset one of them names must resolve from inside this record or from -// the binary; a path outside both is a template only the machine that wrote it has. -var onboardingArtefacts = []string{ - filepath.Join("..", "..", "..", "commands", "prepare-this-repo.md"), -} - // machineLocalRefs returns the 1-based line numbers of every machine-local path // reference in data: a home-relative `~/` path, or the private templates directory // the adopt phase used to reach into. @@ -43,15 +34,14 @@ func machineLocalRefs(data []byte) []int { // did nothing at all, and the adoption silently degraded against loud-staging. // Every asset it applies now resolves from this record or from the binary. func TestOnboardingIsSelfContained(t *testing.T) { - for _, rel := range onboardingArtefacts { - data, err := os.ReadFile(rel) - if err != nil { - t.Fatalf("cannot read onboarding artefact %s: %v", rel, err) - } - if hits := machineLocalRefs(data); len(hits) > 0 { - t.Errorf("%s references a machine-local path on line(s) %v; every adopt-phase asset must resolve "+ - "from this record or from the binary, never from a path only one machine has", rel, hits) - } + // The preparation workflow on the ahoy page is what the adopt phase reads and + // applies from. Every asset it names must resolve from inside this record or + // from the binary; a path outside both is a template only the machine that + // wrote it has. The page's later account of the binary install names the + // binary's own user-scope paths, which are not assets the workflow applies. + if hits := machineLocalRefs([]byte(preparationWorkflow(t))); len(hits) > 0 { + t.Errorf("%s's preparation workflow references a machine-local path on line(s) %v (counted from the install heading); "+ + "every adopt-phase asset must resolve from this record or from the binary, never from a path only one machine has", ahoyPage, hits) } } @@ -121,13 +111,14 @@ func toolFileInstructions(page string) []string { return hits } -// TestPrepareThisRepoScaffoldsNoToolConventionsFile is the page half of -// itd-2610030814013772's A1 (spc-2610031156364295, step 6): prepare-this-repo -// creates AGENTS.md and no tool's own conventions file, link or copy, since +// TestAhoyPageScaffoldsNoToolConventionsFile is the page half of +// itd-2610030814013772's A1 (spc-2610031156364295, step 6): the preparation +// workflow, which folded into the ahoy page's install section +// (spc-2610100613109045, step 4), creates AGENTS.md and no tool's own conventions file, link or copy, since // AGENTS.md is the one conventions file abcd writes // (adr-2610030814023326). The detector is watched fire first on the shapes the // page once carried, so an emptied scan cannot leave the gate green. -func TestPrepareThisRepoScaffoldsNoToolConventionsFile(t *testing.T) { +func TestAhoyPageScaffoldsNoToolConventionsFile(t *testing.T) { for _, bad := range []string{ "3. **AGENTS.md.** Merge into the repo's `AGENTS.md`\n (create it if absent, with `CLAUDE.md` as a symlink to it):\n", "Then run `ln -s AGENTS.md GEMINI.md` so Gemini CLI reads it.\n", @@ -164,13 +155,8 @@ func TestPrepareThisRepoScaffoldsNoToolConventionsFile(t *testing.T) { } } - rel := filepath.Join("..", "..", "..", "commands", "prepare-this-repo.md") - data, err := os.ReadFile(rel) - if err != nil { - t.Fatalf("cannot read %s: %v", rel, err) - } - for _, hit := range toolFileInstructions(string(data)) { + for _, hit := range toolFileInstructions(readAhoyPage(t)) { t.Errorf("%s instructs making a tool's own conventions file; it creates AGENTS.md and no other, "+ - "link or copy (adr-2610030814023326):\n %s", rel, hit) + "link or copy (adr-2610030814023326):\n %s", ahoyPage, hit) } } diff --git a/internal/core/ahoy/persona_conventions_test.go b/internal/core/ahoy/persona_conventions_test.go index a94089501..11a5ba382 100644 --- a/internal/core/ahoy/persona_conventions_test.go +++ b/internal/core/ahoy/persona_conventions_test.go @@ -1,8 +1,6 @@ package ahoy import ( - "os" - "path/filepath" "regexp" "strings" "testing" @@ -12,19 +10,16 @@ import ( // that are "always" or "only" a fixed set, or a ban on "other names". var conventionsPersonaCap = regexp.MustCompile(`(?i)\balways\b|\bonly\b|other names|\bfixed\b`) -// TestPrepareThisRepoConventionsKeepThePersonaSequenceOpen pins that the -// working-conventions block prepare-this-repo writes into a prepared +// TestAhoyInstallConventionsKeepThePersonaSequenceOpen pins that the +// working-conventions block the ahoy page's preparation workflow writes into a +// prepared // repository's AGENTS.md teaches the same open persona sequence the bundled // INTENTS rule does (iss-2610040758394205). A cap left in that block would // contradict the injected rule from inside the repository's own conventions // file, and forbid the fourth persona the issue was captured for. -func TestPrepareThisRepoConventionsKeepThePersonaSequenceOpen(t *testing.T) { - rel := filepath.Join("..", "..", "..", "commands", "prepare-this-repo.md") - data, err := os.ReadFile(rel) - if err != nil { - t.Fatalf("cannot read %s: %v", rel, err) - } - page := string(data) +func TestAhoyInstallConventionsKeepThePersonaSequenceOpen(t *testing.T) { + rel := ahoyPage + page := preparationWorkflow(t) start := strings.Index(page, "### The working-conventions section") if start < 0 { t.Fatalf("%s carries no working-conventions section", rel) diff --git a/internal/core/ahoy/preparation_page_test.go b/internal/core/ahoy/preparation_page_test.go new file mode 100644 index 000000000..04b3445db --- /dev/null +++ b/internal/core/ahoy/preparation_page_test.go @@ -0,0 +1,82 @@ +package ahoy + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// ahoyPage is the command page that carries the preparation workflow: +// prepare-this-repo folded into its install section (itd-2610090831227812, +// spc-2610100613109045 step 4), around the binary call that section makes. +var ahoyPage = filepath.Join("..", "..", "..", "commands", "ahoy.md") + +// The headings that bound the preparation workflow on the ahoy page: it opens +// the install section and ends where the page turns to what the binary install +// itself does, whose user-scope paths (`~/.local/bin`, the home folder) are +// the binary's own and no asset the workflow applies. +const ( + installHeading = "## `install`" + binaryInstallHeading = "### What the binary install does" +) + +// readAhoyPage returns the ahoy page's text. +func readAhoyPage(t *testing.T) string { + t.Helper() + data, err := os.ReadFile(ahoyPage) + if err != nil { + t.Fatalf("cannot read %s: %v", ahoyPage, err) + } + return string(data) +} + +// preparationWorkflow returns the part of the ahoy page the host follows to +// prepare a repository: from the install heading to the heading on what the +// binary install does. +func preparationWorkflow(t *testing.T) string { + t.Helper() + page := readAhoyPage(t) + start := strings.Index(page, installHeading) + if start < 0 { + t.Fatalf("%s has no %q section", ahoyPage, installHeading) + } + rest := page[start:] + end := strings.Index(rest, binaryInstallHeading) + if end < 0 { + t.Fatalf("%s's install section has no %q heading closing the preparation workflow", ahoyPage, binaryInstallHeading) + } + return rest[:end] +} + +// TestAhoyInstallCarriesThePreparation is A11's page half: `ahoy install` does +// what prepare-this-repo did, and that page is gone. Each phase the retired page +// ran is held to the install section by the words that carry it, so a later +// edit that drops one is named. +func TestAhoyInstallCarriesThePreparation(t *testing.T) { + if _, err := os.Stat(filepath.Join("..", "..", "..", "commands", "prepare-this-repo.md")); err == nil { + t.Errorf("commands/prepare-this-repo.md still exists; it folded into the ahoy page's install section") + } else if !os.IsNotExist(err) { + t.Fatal(err) + } + section := strings.Join(strings.Fields(preparationWorkflow(t)), " ") + for _, want := range []struct{ phase, text string }{ + {"the ownership refusal", "git remote get-url origin"}, + {"the authority ordering", "Authority ordering"}, + {"the orientation", ".abcd/development/principles/"}, + {"the engine-backed audit", `"${CLAUDE_PLUGIN_ROOT}/abcd" lint --json`}, + {"the gap report", ".abcd/.work.local/scratch/"}, + {"the legacy migration", "never leave both `.work/` and `.abcd/work/`"}, + {"the AGENTS.md merge", "Merge — never overwrite"}, + {"the identity block", "identity init"}, + {"the commit gates", `"${CLAUDE_PLUGIN_ROOT}/abcd" ahoy install`}, + {"the attribution opt-in", "ahoy install --attribution"}, + {"the working-conventions section", "### The working-conventions section"}, + {"the downstream rule", "### Never commit downstream"}, + {"the definition of done", "### Definition of done"}, + } { + if !strings.Contains(section, want.text) { + t.Errorf("the ahoy page's install section lost %s (%q)", want.phase, want.text) + } + } +} diff --git a/internal/core/ahoy/prepare_hookspath_test.go b/internal/core/ahoy/prepare_hookspath_test.go index 3af05db35..78bcc1e80 100644 --- a/internal/core/ahoy/prepare_hookspath_test.go +++ b/internal/core/ahoy/prepare_hookspath_test.go @@ -1,8 +1,6 @@ package ahoy import ( - "os" - "path/filepath" "reflect" "regexp" "strconv" @@ -28,7 +26,7 @@ var stateBullet = regexp.MustCompile("^\\s*- `([a-z]+)`") // above it. var numberedStep = regexp.MustCompile(`^\s*\d+\.\s`) -// hooksPathAdviceFaults returns what the prepare-this-repo page gets wrong about +// hooksPathAdviceFaults returns what the ahoy page gets wrong about // arming the committed hooks, one sentence each; none means the page sets a local // core.hooksPath only in the unarmed branch and leaves a foreign one alone. // @@ -81,18 +79,15 @@ func hooksPathAdviceFaults(page string) []string { return faults } -// TestPrepareThisRepoArmsHooksOnlyWhenUnarmed is iss-2610080546210831's detector. -// The page's commit-gates step told every prepared repository to run +// TestAhoyPageArmsHooksOnlyWhenUnarmed is iss-2610080546210831's detector. The +// preparation workflow's commit-gates step, on the prepare-this-repo page until +// it folded into the ahoy page's install section (spc-2610100613109045, step 4), told every prepared repository to run // `git config core.hooksPath .githooks` with no condition, which shadows a global // hooks dispatcher: the exact advice ahoy refuses to give when hooks_path reads // foreign. The page must key the command on the state ahoy reports. -func TestPrepareThisRepoArmsHooksOnlyWhenUnarmed(t *testing.T) { - data, err := os.ReadFile(filepath.Join("..", "..", "..", "commands", "prepare-this-repo.md")) - if err != nil { - t.Fatalf("cannot read the prepare-this-repo page: %v", err) - } - for _, f := range hooksPathAdviceFaults(string(data)) { - t.Errorf("commands/prepare-this-repo.md: %s", f) +func TestAhoyPageArmsHooksOnlyWhenUnarmed(t *testing.T) { + for _, f := range hooksPathAdviceFaults(readAhoyPage(t)) { + t.Errorf("commands/ahoy.md: %s", f) } } @@ -108,7 +103,7 @@ func TestPrepareHooksPathFieldIsWhatAhoyEmits(t *testing.T) { return strings.Split(f.Tag.Get("json"), ",")[0] } if got := tag(DetectionResult{}, "Banlist") + "." + tag(BanlistHealth{}, "HooksPath"); got != "banlist.hooks_path" { - t.Errorf("ahoy --json carries the hooks path state at %q; the prepare-this-repo page reads banlist.hooks_path", got) + t.Errorf("ahoy --json carries the hooks path state at %q; the ahoy page reads banlist.hooks_path", got) } } diff --git a/internal/core/lint/config.go b/internal/core/lint/config.go index 2f34e4dae..0ed6c68fe 100644 --- a/internal/core/lint/config.go +++ b/internal/core/lint/config.go @@ -168,7 +168,7 @@ type RuleConfig struct { // grain unarmed (the pre-spc-27 surface-grain check only). Snapshot string `json:"snapshot"` // HostDelegated lists surfaces whose workflow runs in the host agent with - // no Go verb (consult, ingest, prepare-this-repo): their sub-verb tables + // no Go verb (consult, ingest): their sub-verb tables // are format-checked only, never compared to the cobra tree. Explicit // config, never a hard-coded skip — an unlisted surface gets the full // comparison. diff --git a/internal/surface/cli/bootstrap_test.go b/internal/surface/cli/bootstrap_test.go index 16a83b064..2b19301fe 100644 --- a/internal/surface/cli/bootstrap_test.go +++ b/internal/surface/cli/bootstrap_test.go @@ -511,11 +511,17 @@ func bootstrapShSingleQuote(s string) string { // rather than searching for the good form is deliberate: a text may hold the // runnable instruction AND still leave the unrunnable one standing beside it, // and only an every-occurrence check sees the second one. +// +// The plugin's slash command, `/abcd:ahoy install`, is not a shell invocation: +// the host runs it in a plugin session, and its page resolves the binary +// through the plugin root before PATH, so it runs in the very state this +// check guards. It is the person's route into a repository's preparation +// (spc-2610100613109045, step 4), so it is not counted. func bootstrapEveryInvocationIsPathQualified(body, prefix string) []string { parts := strings.Split(body, "ahoy install") var bad []string for _, before := range parts[:len(parts)-1] { - if strings.HasSuffix(before, prefix) { + if strings.HasSuffix(before, prefix) || strings.HasSuffix(before, "/abcd:") { continue } if len(before) > 80 { diff --git a/internal/surface/cli/staleusage.go b/internal/surface/cli/staleusage.go index c717f48f3..ee3c7dffa 100644 --- a/internal/surface/cli/staleusage.go +++ b/internal/surface/cli/staleusage.go @@ -81,10 +81,9 @@ const dispatcherPage = "abcd" // this set, so a new host-delegated page is added here or the parity check // reads its missing verb as drift. var pagesWithNoVerb = map[string]string{ - dispatcherPage: "`abcd` is the binary itself, not one of its commands — the page /abcd:abcd documents the bare call; did you mean `abcd ` (or bare `abcd` for the status board)?", - "consult": "`consult` has no binary verb — it runs in the host agent; invoke it as /abcd:consult", - "ingest": "`ingest` has no binary verb — it runs in the host agent; invoke it as /abcd:ingest", - "prepare-this-repo": "`prepare-this-repo` has no binary verb — it runs in the host agent; invoke it as /abcd:prepare-this-repo", + dispatcherPage: "`abcd` is the binary itself, not one of its commands — the page /abcd:abcd documents the bare call; did you mean `abcd ` (or bare `abcd` for the status board)?", + "consult": "`consult` has no binary verb — it runs in the host agent; invoke it as /abcd:consult", + "ingest": "`ingest` has no binary verb — it runs in the host agent; invoke it as /abcd:ingest", } // retiredTokens are top-level tokens whose page or verb was retired with no @@ -93,8 +92,9 @@ var pagesWithNoVerb = map[string]string{ // does not know a retired name is never called stale for it. A token here has // no page, so it is not in pagesWithNoVerb. var retiredTokens = map[string]string{ - "changelog": "`changelog` merged into launch as its preview: run `abcd launch --dry-run`, which renders the release cut (derived version, records, guard verdict) beside the bundle report", - "version": "`version` is a root flag, not a verb: run `abcd --version`, and bare `abcd` shows the installed version as the board's last line (asking for a newer release is the update verb's --check)", + "prepare-this-repo": "`prepare-this-repo` folded into ahoy install: run /abcd:ahoy install, which prepares the repository in the host agent around the binary's `abcd ahoy install`", + "changelog": "`changelog` merged into launch as its preview: run `abcd launch --dry-run`, which renders the release cut (derived version, records, guard verdict) beside the bundle report", + "version": "`version` is a root flag, not a verb: run `abcd --version`, and bare `abcd` shows the installed version as the board's last line (asking for a newer release is the update verb's --check)", } // maxCommandPageBytes caps a command-page read; the pages are a few KiB. diff --git a/internal/surface/cli/staleusage_pageverb_test.go b/internal/surface/cli/staleusage_pageverb_test.go index 26e8f926d..d095c8a55 100644 --- a/internal/surface/cli/staleusage_pageverb_test.go +++ b/internal/surface/cli/staleusage_pageverb_test.go @@ -11,7 +11,7 @@ import ( // iss-2609240519471816). // // commands/abcd.md documents the bare dispatcher, and the host-delegated pages -// (consult, ingest, prepare-this-repo) document workflows the host agent runs +// (consult, ingest) document workflows the host agent runs // with no Go verb at all. The stale-usage note reads "a page exists for the // token" as "the surface is newer than this binary", which is false for every // one of them: rebuilding or updating adds no verb, because none was ever @@ -120,3 +120,33 @@ func TestRetiredVersionTokenNamesWhatReplacedIt(t *testing.T) { } } } + +// TestRetiredPrepareThisRepoTokenNamesWhatReplacedIt is the prepare-this-repo +// half of spc-2610100613109045's old-names rule (step 4): the page folded into +// the ahoy page's install section, so `abcd prepare-this-repo`, never a verb, +// is still an unknown command, and its note names `ahoy install` rather than +// the retired page, and never calls the binary stale for not knowing it. +func TestRetiredPrepareThisRepoTokenNamesWhatReplacedIt(t *testing.T) { + if _, ok := pagesWithNoVerb["prepare-this-repo"]; ok { + t.Fatalf("pagesWithNoVerb still names prepare-this-repo, whose page is retired") + } + root := stalePluginRoot(t) + setExecutable(t, filepath.Join(root, "abcd")) + code, stdout, stderr := runMain(t, "prepare-this-repo") + if code != 2 || stdout != "" { + t.Fatalf("`abcd prepare-this-repo` exit = %d, stdout %q; want exit 2 and an empty stdout", code, stdout) + } + if !strings.Contains(stderr, "unknown command") { + t.Errorf("`abcd prepare-this-repo` is not refused as an unknown command:\n%s", stderr) + } + for _, want := range []string{"/abcd:ahoy install", "abcd ahoy install"} { + if !strings.Contains(stderr, want) { + t.Errorf("the refusal does not name %q:\n%s", want, stderr) + } + } + for _, not := range []string{"/abcd:prepare-this-repo", "predates", "make build", "abcd update"} { + if strings.Contains(stderr, not) { + t.Errorf("the refusal names %q:\n%s", not, stderr) + } + } +} From 076b764b30369839690d4138899a0c202014d30a Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:44:20 +0100 Subject: [PATCH 5/7] docs: correct the ahoy chapter's status alias and its user-scope tree Two peer findings on 01-ahoy.md, folded in with step 4's change to that chapter. The chapter called `status` a plugin-page alias for the bare form. It is not: commands/ahoy.md hands every word to the binary, which refuses `status` like any other word it does not register, so `abcd ahoy status` is an unknown command on both surfaces. The user-scope tree, declared the one inventory, left out files the shipped code reads or writes under the home folder: oracle-routing.json, interviews/, cache-attestation and statusline.json, which the symlink rule in 03-configuration.md already lists, and backups/, where the status-line change keeps its copies of the harness settings file. Step 4 of spc-2610100613109045. Assisted-by: Claude:claude-opus-5-5 --- .../development/brief/04-surfaces/01-ahoy.md | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/.abcd/development/brief/04-surfaces/01-ahoy.md b/.abcd/development/brief/04-surfaces/01-ahoy.md index 63a233f0f..0cc766121 100644 --- a/.abcd/development/brief/04-surfaces/01-ahoy.md +++ b/.abcd/development/brief/04-surfaces/01-ahoy.md @@ -43,10 +43,11 @@ commands. The slash command dispatches every sub-verb and mode but the identity check, the write verbs included, and each announces that it writes before it runs. The identity check is a plain command-line entrypoint, because its exit code is the whole point of it and its home is a -pre-commit hook or CI rather than a conversation. `status` is a plugin-page -alias for the bare form and has no CLI sub-command behind it: `abcd ahoy -status` is refused as an unknown command. Every other word ships on the CLI: -the table above is the sub-verb set, and the modes are the bare verb's flags. +pre-commit hook or CI rather than a conversation. `status` is a word neither +surface takes: the plugin page passes it to the binary, which refuses it like +any other word it does not register, so `abcd ahoy status` is an unknown +command. The table above is the sub-verb set, and the modes are the bare verb's +flags. - **Install** installs or updates abcd in this repo, covering first install and upgrade alike. It runs the detection pass, then an apply pass over the @@ -392,6 +393,12 @@ earlier version wrote wider is narrowed when abcd next writes it inbox/ reports managed repositories filed back to abcd, -.md; promoted/ keeps the ones filed as captures (itd-2609221656361680) + interviews/ the setup answers that change the machine (the + status line, the machine's routing table), one + record per install run that asked them + backups/ copies of the harness's settings file, taken + before the status line changes it; the newest 10 + are kept config.json the machine layer of the layered configuration, read-only except for the provider blocks (oracle.api.) the provider setup writes, @@ -421,8 +428,20 @@ earlier version wrote wider is narrowed when abcd next writes it between the bundled domains and each repo's .abcd/rules.json, read-only; abcd never creates it (itd-117) + oracle-routing.json the machine's model-tier routing table, written + owner-only on the install's answered consent and + left by uninstall (itd-2609170822093401) + statusline.json the status line's settings: the elements switched + on and the command the harness ran before abcd + took the row; written on the status-line offer's + consent, left by uninstall path-entry the abcd copy this machine owns, the one PATH binary a hook will run + cache-attestation the bootstrap's record that the plugin data + directory's cached binary matched the published + release manifest, written only after that check + passed; install promotes the cache to the owned + PATH copy only when the record agrees trusted-roots foreign-uid configuration roots the caller vouches for local-transcript-roots checkouts whose transcripts are pulled in to /.abcd/.work.local/transcripts/ instead From c56be93dd05ca58159dc44020948baf92b6b4138 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:14:01 +0100 Subject: [PATCH 6/7] fix: bump the changelog composer's prompt version for the preview rename Step 3 repointed the composer's preview source at the launch preview's cut without the prompt_version bump and agents changelog entry record-lint's agent_contract rule requires. It also adds changelog to the brief's list of names retired with no alias. Refs: itd-2610090831227812 Assisted-by: Claude:claude-opus-5-5 --- .abcd/development/agents/CHANGELOG.md | 10 ++++++++++ .abcd/development/brief/04-surfaces/08-abcd.md | 7 ++++--- agents/release-changelog-composer.md | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.abcd/development/agents/CHANGELOG.md b/.abcd/development/agents/CHANGELOG.md index 2f962c278..4a8b16e61 100644 --- a/.abcd/development/agents/CHANGELOG.md +++ b/.abcd/development/agents/CHANGELOG.md @@ -12,6 +12,16 @@ over the brief's earlier `1.0.0`-at-close expectation). The four M6 synthesis agents below entered at `0.1.0`, wired to their `abcd disembark` verbs and unmeasured; `lifeboat-oracle` has since become `lifeboat-reviewer` at `0.1.1`. +## 2026-10-11 (itd-2610090831227812 — the changelog verb retires into the launch preview) + +### release-changelog-composer 0.4.3 + +PATCH: the read-only preview the definition names as a source of the cut is +now the `cut` member of `abcd launch --dry-run --json`, since `abcd changelog` +retired into the launch preview (spc-2610100613109045, step 3). The cut's +shape and every rule for composing from it are unchanged. Unmeasured, in the +`0.x` band. + ## 2026-10-10 (iss-2610091942156774 — sub-agents are taught the rm the guard refuses) ### cold-reading-comparative 0.1.4 diff --git a/.abcd/development/brief/04-surfaces/08-abcd.md b/.abcd/development/brief/04-surfaces/08-abcd.md index b6b5f4f60..4e172105a 100644 --- a/.abcd/development/brief/04-surfaces/08-abcd.md +++ b/.abcd/development/brief/04-surfaces/08-abcd.md @@ -373,9 +373,10 @@ first, the `/abcd:` invocation for the rest — and never sends the reader to rebuild or update. Neither is a `status` or `show` sub-verb under a record verb (`capture`, `intent`, `spec`): the refusal names the record dispatcher, `abcd `, which answers that question. A name retired with no -alias (`version`, whose page retired when the board took the version) is -answered with what replaced it, and never as a stale binary. Failing that evidence, the disk-only vintage the version flag -renders stands in. When neither says anything, the framework's line stands +alias (`version`, whose page retired when the board took the version, and +`changelog`, which the launch preview replaced) is answered with what replaced +it, and never as a stale binary. Failing that evidence, the disk-only vintage +the version flag renders stands in. When neither says anything, the framework's line stands byte-for-byte. The exit code, the stream and the JSON envelope are the framework's own. diff --git a/agents/release-changelog-composer.md b/agents/release-changelog-composer.md index 6025da74a..70ad5055b 100644 --- a/agents/release-changelog-composer.md +++ b/agents/release-changelog-composer.md @@ -1,7 +1,7 @@ --- name: release-changelog-composer description: Compose the prose of one release cut from the records that shipped in it — the changelog lines and the release page, every line and headline citing the record id it reports, so the binary can prove both documents describe exactly the cut. Host-delegated; feeds `abcd launch ship --changelog-json`. -prompt_version: 0.4.2 +prompt_version: 0.4.3 reads_untrusted_input: true capability_scope: task_classes: [surface_render] From df4c1e302c6e2c6c1104b8fdf8239cc4a2a5ba99 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:33:03 +0100 Subject: [PATCH 7/7] fix: start the ahoy install page from the detection pass, not the markers The shortcut straight to the binary's half keyed on the working-conventions markers, which only the host-run preparation writes, so a repository adopted through the binary install alone, abcd's own checkout among them, would have been walked through the whole preparation again. The page now starts from the detection pass: a managed repository goes to the binary's half unless the person asks to prepare it, and an unmanaged one starts at Phase 0. The brief's mirror sentence follows, and the spec's line on the loop hint now says /abcd:ahoy install, matching what shipped. Refs: itd-2610090831227812 Assisted-by: Claude:claude-opus-5-5 --- .abcd/development/brief/04-surfaces/01-ahoy.md | 6 +++--- ...10100613109045-a-person-who-types-abcd-sees-only-the.md | 4 +++- commands/ahoy.md | 7 ++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.abcd/development/brief/04-surfaces/01-ahoy.md b/.abcd/development/brief/04-surfaces/01-ahoy.md index 0cc766121..ed4eb5ebf 100644 --- a/.abcd/development/brief/04-surfaces/01-ahoy.md +++ b/.abcd/development/brief/04-surfaces/01-ahoy.md @@ -258,9 +258,9 @@ page's name is gone with no alias, and typing it at a shell gets a note naming the install instead. The fold is page-only. The binary's install is unchanged and gains no refusal: it checks no ownership, so the workflow's first phase does, as before, and a refusal in the binary would break installs that -work today. A repository already prepared, its three tiers in place and its -`AGENTS.md` carrying the working-conventions markers, needs only the binary's -apply pass. +work today. The detection pass decides where the page starts: a managed +repository needs only the binary's apply pass unless the person asks to prepare +it, and an unmanaged one starts at the workflow's first phase. The workflow is an **interim bridge**. abcd cannot yet manage repositories directly, so the page does by hand what the CLI will later take over, in a diff --git a/.abcd/development/specs/open/spc-2610100613109045-a-person-who-types-abcd-sees-only-the.md b/.abcd/development/specs/open/spc-2610100613109045-a-person-who-types-abcd-sees-only-the.md index 2c829c1ed..ab42628f3 100644 --- a/.abcd/development/specs/open/spc-2610100613109045-a-person-who-types-abcd-sees-only-the.md +++ b/.abcd/development/specs/open/spc-2610100613109045-a-person-who-types-abcd-sees-only-the.md @@ -303,7 +303,9 @@ binary would break installs that work now. Code that names the page or its verb changes in the same step. The loop's conventions-file hint in `internal/core/implement/loop/brief.go` and `issuebrief.go` tells the reader to run `abcd prepare-this-repo`, which was -never a verb, so it becomes `abcd ahoy install`. Also: +never a verb, so it becomes `/abcd:ahoy install`: the page's preparation is what +writes the conventions file, and a default binary `abcd ahoy install` writes no +`AGENTS.md` (iss-2610101707453618). Also: `internal/core/repolint/rule_docs.go`'s message; `commands/lint.md`'s pointer; `pagesWithNoVerb`; `host_delegated` in `.abcd/record-lint.json` and the comment in `internal/core/lint/config.go`. `onboarding_test.go` and diff --git a/commands/ahoy.md b/commands/ahoy.md index 450020d8e..62fff7be8 100644 --- a/commands/ahoy.md +++ b/commands/ahoy.md @@ -130,9 +130,10 @@ the detection pass finds, and run it and relay it. The binary checks no ownership, so Phase 0 is the step that does. -A repository the preparation has already brought up to the conventions (the -three tiers in place and `AGENTS.md` carrying the working-conventions markers) -needs only the binary's half: go straight to it. +Which half to start with comes from the detection pass above. A +`managed-repo` needs only the binary's half: go straight to it, unless the user +asked to prepare the repository or adopt the conventions. An `unmanaged-repo` +starts at Phase 0. The preparation is an interim bridge: abcd cannot yet manage repositories directly, so it reads the abcd record and brings the current repository up to