From c1e727f9225a33932a721207a0526853fcfa8433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:08:11 +0200 Subject: [PATCH 1/4] docs(specs): add the claude-init command design and its story MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate-A spec cycle closed. Floor 3 reached with four valid passes; the final pass found 0 Blockers and 0 Majors. Every Blocker and Major raised in the cycle was resolved; 15 Minor and Nit findings are collected and, per §5, were not iterated. Clean in the Blocker/Major sense §5 defines — not error-free, and not implemented: no product file is written by this commit. Two decisions are recorded in the story's §5. Item 4: the create branch uses an exclusive-create operation and accepts an already-present Python 3 runtime as a precondition, stopping without writing where none is available. Item 5: the command never writes over an existing CLAUDE.md — it shows a focused diff, asks, hands back the proposed result and stops. Docs-only change (docs/**.md) — Gate B is N/A per CLAUDE.md §5, and no plugin path is touched, so invariant 12 does not apply. cycle 02quh945yj; floor 3 per {docs/superpowers/stories/2026-09-17-claude-init-command-story.md (level 1)}; hook reminder threshold absent cycle 02quh945yj; Gate-A spec (passes 1-4, pass 1 undetermined; pass 2 undetermined; pass 3 gpt-6-astra; pass 4 gpt-6-astra): Findings 12,8,13,15. Blockers 0,0,0,0. Majors 6,3,1,0. --- .../2026-09-17-claude-init-command-design.md | 553 ++++++++++++++++++ .../2026-09-17-claude-init-command-story.md | 197 +++++++ 2 files changed, 750 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-17-claude-init-command-design.md create mode 100644 docs/superpowers/stories/2026-09-17-claude-init-command-story.md diff --git a/docs/superpowers/specs/2026-09-17-claude-init-command-design.md b/docs/superpowers/specs/2026-09-17-claude-init-command-design.md new file mode 100644 index 0000000..9e2257a --- /dev/null +++ b/docs/superpowers/specs/2026-09-17-claude-init-command-design.md @@ -0,0 +1,553 @@ +# `/dev-workflow:claude-init` — design and target text + +**Story:** `docs/superpowers/stories/2026-09-17-claude-init-command-story.md` — read the profile from +its header at every gate call; it is the only writable copy. + +This spec carries **every string the change installs**, in final form. The plan cites sections here +by name and does not restate them: a second copy of normative text is the defect the loop-rule cycle +spent most of its findings on. + +--- + +## §1 What is being added + +**One command file**, `plugins/dev-workflow/commands/claude-init.md`, loaded by convention from +`commands/` — **no manifest key** (invariant 6). It carries its template inline (invariant 8). + +**One rule**, `### Don't guess`, added at the end of section 1 in **three** copies: + +| # | File | Position | +|---|---|---| +| 1 | `plugins/dev-workflow/commands/claude-init.md` | inside the new inline template, end of §1 | +| 2 | `plugins/dev-workflow/commands/workflow-init.md` | inside the existing inline template, end of §1 | +| 3 | `CLAUDE.md` (this repository's own) | end of §1 | + +**Byte-identical in all three**, including line wrapping, so parity is decidable by `diff` and needs +no new mechanism. On `main` the three §1 bodies are already byte-identical, which is what makes this +possible. + +**Three inventory sites**, and no others: `README.md`'s component table, `AGENTS.md`'s layout tree, +`docs/architecture.md`'s layout tree. `MANIFEST.md` is **not** among them — it inventories +`source-files/`, the frozen extraction seed, and a command with no seed origin has no row there. + +**One version bump and one changelog entry** (invariant 12): **`0.11.0` → `0.12.0`**, decided by +Daniel on 2026-09-17, with `claude-init` planned as **the first of the two in-flight changes to +merge**. That is release sequencing and **not** permission to merge. `0.13.0` was explicitly rejected +as a way to reserve `0.12.0` for the unfinished `loop-rule-consolidation` branch. + +**The integration base is rechecked before the version is finalized**, never taken from this +sentence. Verified when it was written: `origin/main` and local `main` are both +`7c0d475b9a4a1897e8b03dfa20ec058b9ce09ba6`, its manifest is `0.11.0`, `CHANGELOG.md`'s newest entry +is `0.11.0`, and no pull requests are open — so `0.12.0` is free. `main` can move, and +`scripts/check-version-bump.sh` is blind to exactly this case ("two PRs branched from the same +version each bumping to the same new one"), so the recheck is the only thing standing in for it. + +**The `loop-rule-consolidation` branch is not edited by this change.** Its plan pins `0.12.0` for +itself; reconciling that belongs to that cycle, on that branch. + +## §2 What the command does, and what it refuses to do + +**Its only write to a target project is `CLAUDE.md`.** Not a marker, not a README note, not an +`AGENTS.md`. One file. + +**It requires nothing that initializing a file does not require, with one named exception.** No +superpowers plugin, no Codex MCP server, no `gh`, no package manager, and **no git repository**. It +does not invoke `/workflow-init`, does not run `git init`, installs nothing, writes no `.context/` +marker, and **does not commit**. **The exception is the create branch**, which needs an +already-present Python 3 runtime for its exclusive-create operation (§2, *Writing*); a directory +alone is enough to read and to propose, and where that runtime is absent the create branch stops +without writing rather than falling back to a weaker write. + +**Existing-file behaviour**, matching invariant 9's shape without inheriting its scaffolding. **Two +questions are asked, in order, and the first has nothing to do with content:** what *kind* of path is +this, and only then what does it *contain*. Pass 1's Major 1 was that the earlier table asked only +the second, so a path that could not safely be classified fell into a content branch anyway. + +### Step one — the path must be one of exactly two kinds + +Anything else **stops without writing**. This is a partition over what the filesystem can present, +not over what the file says. + +| Path observed | Action | +|---|---| +| **Absent** — nothing at that path, and not a dangling symlink | Eligible for the create branch. | +| **An existing, readable, regular file** | Eligible for the content branch. | +| A symlink — resolved or dangling | **Stop. Write nothing.** Report the path and where it points. | +| A directory, or any other non-regular file | **Stop. Write nothing.** Report what was observed. | +| Present but unreadable | **Stop. Write nothing.** Report the failure. | + +**Why each stop, rather than one "cannot write".** A **live symlink redirects the write into another +file**, which breaks the one-file contract silently. A **dangling symlink reads as absent** to an +ordinary existence test and would be written through. A **directory** cannot hold the template. An +**unreadable file cannot be classified at all**, and "cannot classify" is not "different" — treating +it as different would offer a diff computed from nothing. + +**The kind test must not follow the link.** `test -e` and `test -f` both resolve symlinks, so a +dangling link answers *absent* — the exact misread that would overwrite through it. Test with +`test -L` **before** any existence test, or read the kind with `ls -ld`. **Report which row was +observed**, not a single generic failure: each of the five has a different fix, and a reader who is +told only "cannot write" retries the thing that already failed. + +**A failed write is an outcome, not silence.** If the create write is attempted and fails, report +the failure and the path's observed state afterwards. `written` and `unchanged` are claims about an +**observed** result; neither may be reported from a write whose result was not seen. + +### Step two — content, for a target that passed step one + +| State found | Action | +|---|---| +| Absent | Create the file with the template, and **only through a qualifying create operation** — see *Writing* below. Report `written`. Where no such operation is available, stop without writing and report that. | +| Present, and the rules it carries are equivalent | Report `unchanged`. **Add nothing, duplicate nothing.** | +| Present, with different or partly overlapping rules | Show a **focused proposed diff** — the sections that differ, not the whole file — and **ask**. On approval, hand back the proposed result and **stop**. The command never writes over an existing `CLAUDE.md`; see *Writing* below. | + +### Writing — which branch writes, through what, and what that actually gives + +**Exactly one branch writes: the create branch.** A target that already exists is never written +over by this command. Where the rules differ or overlap, the command produces the proposed result, +asks, and — approval or not — **stops without writing**. Decided 2026-09-21, recorded in the story's +§5 item 5. + +**Why the merge write was removed rather than qualified.** Pass 1's Major 2 was that the earlier +text computed a diff, waited for a human, and merged with no second look; **that wait is +human-length** — minutes — and the likeliest concurrent writer is the person being asked, in their +editor, looking at the file the command just showed them. The repair named `Edit` and claimed its +exact-string check as write-time preimage rejection. **Gate-A spec pass 3 found that claim +unsupported**, and it could not be rescued: an operation was measured to publish by **replacing the +file**, and an atomic replacement of a file is not an atomic check-and-replace of content read +earlier — POSIX `rename()` guarantees only that the destination name keeps referring to one file or +the other throughout, and imposes **no condition on the destination's content** +([XSH rename](https://pubs.opengroup.org/onlinepubs/9799919799/functions/rename.html)). The +alternative on offer was an **operating assumption** that no other writer touches the file during +the run. An assumption is not a guarantee, and **D2** — *"Where §5 and its gates are present, they +stay. There is no remove, disable, or downgrade path"* — is stated absolutely. A command that can +discard a live gate under an assumption it does not enforce breaches D2 whatever the assumption +says. So the write went, not the obligation. + +**What that costs, stated plainly:** where a project already has a `CLAUDE.md`, the user applies the +proposal themselves. The command's convenience shrinks; nothing it claims becomes conditional. + +**Two limits on the proposal itself, because not writing is not a licence.** + +- **A proposal must preserve what a write would have had to preserve.** Unrelated project content + and existing mandatory rules stay in it, and it never removes, disables, weakens or renumbers a + gate. **A harmful proposal is not made acceptable by the user being the one who applies it** — + D2 and the preservation rule bind the content of the proposal, not only the act of writing. +- **Approval opens no write path inside this command.** "Proposal produced and approved" is **not** + a merge that was applied, must never be reported as one, and must not be followed by a write + through any other operation. The command has no route from approval to an existing file. + +**On the create branch, re-read immediately before attempting the create.** Compare the target +against the state observed at step one; changed → report what changed and stop rather than +attempting. **This is not the guard** — the guard is the exclusive-create operation below, which +refuses the target outright. What the re-read buys is a better report: the command can name what +appeared, instead of only relaying an `EEXIST` from the attempt. **It narrows a window; it does not +close one.** + +**The create branch has one admission rule, and it admits no exceptions.** Between the final +absence check and the write, another writer can create something at that path. **A write that +overwrites it and discloses the overwrite afterwards is not preservation** — the file it destroyed +could hold unrelated project rules, or a full §5 with live gates, which D2 forbids removing. So: + +> **Create only through an operation whose verified semantics refuse an existing destination of +> every kind** — regular file, symbolic link resolved or dangling, FIFO, device, directory. **If no +> qualifying operation is available, stop without writing and report the limitation.** + +That is the shape step one already uses for every unsafe path kind, now applied to the window after +step one has run. **Whether a route qualifies is decided by reading its specification, never by +assuming a capability**, and a route that qualifies for some destination kinds and not others does +not qualify. + +**Three routes have been checked against that rule. One qualifies.** + +**The agent's ordinary file-writing tool does not qualify** — it offers no create-exclusive mode and +refuses nothing, so it is forbidden here — as is every other write into an existing path, on every +branch. + +**`set -C` plus a `> CLAUDE.md` redirect does not qualify either, and POSIX is quoted rather than +paraphrased.** Redirecting Output (XCU 2.7.2) says the redirection *"shall fail if the noclobber +option is set … and the file named by the expansion of word exists and is either a regular file or a +symbolic link that resolves to a regular file"*, and that it *"may also fail if the file is a +symbolic link that does not resolve to an existing file"*. The rationale adds only that *"the +restriction on regular files is historical practice"* +([XCU 2.7.2](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_07_02), +[rationale](https://pubs.opengroup.org/onlinepubs/9799919799/xrat/V4_xcu_chap01.html)). So **three +gaps, read off that text**: a dangling symlink is *"may also fail"*, which is permission and not a +requirement; a FIFO, a device or a directory is not covered at all; and nothing outside regular +files is required to fail. **Narrowing the description of noclobber does not turn it into a +qualifying operation** — it is the same operation, described more accurately. **Measured here, its +worst case is not a wrong answer but a hang**: against a FIFO the redirect neither refused nor +returned, blocking past a five-second limit, because the open waits for a reader. + +**`os.open(path, O_WRONLY | O_CREAT | O_EXCL)` qualifies, and POSIX is quoted for it too.** `open()` +says *"If O_CREAT and O_EXCL are set, open() shall fail if the file exists"* — `shall`, not `may` — +and, in the sentence that closes exactly the gap noclobber leaves, *"If O_CREAT and O_EXCL are set, +and path names a symbolic link, open() shall fail and set errno to [EEXIST], **regardless of the +contents of the symbolic link**"* +([XSH open](https://pubs.opengroup.org/onlinepubs/9799919799/functions/open.html)). A dangling +symlink is therefore required to fail, not permitted to; and an existing FIFO, device or directory +is an existing file, so it fails too. **This is the admission rule's requirement met by +specification, not by observation.** + +**Observation was taken as well, and it is reported as what it is.** A probe in one environment — +Darwin 25.6.0, Python 3.12.1, Apple's `/bin/sh` — ran both routes against six destination kinds in +throwaway directories: + +| Destination | `os.open` `O_CREAT\|O_EXCL` | `sh`: `set -C` then `>` | +|---|---|---| +| absent | created | created | +| existing regular file | refused, `EEXIST` | refused | +| directory | refused, `EEXIST` | refused | +| FIFO | refused, `EEXIST` | **not refused — blocked past 5 s** | +| symlink → regular file | refused, `EEXIST` | refused | +| dangling symlink | refused, `EEXIST` | refused | + +**Read this table the right way round.** It is evidence about one implementation on one machine, and +one row shows why that is not enough on its own: this `sh` refuses a **dangling symlink**, which +POSIX only permits it to do. A conforming implementation that declined that +permission would create straight through a dangling symlink, and this column would read +differently on the same machine tomorrow if the shell changed. **The guarantee is the specification; the probe only +shows the two routes are not interchangeable in practice either**, and it is not an end-to-end test +of the command, which has none. + +**The price, accepted rather than hidden: creating requires a Python 3 runtime that is already +present.** The command installs nothing, adds no package manager, and introduces no lock, marker or +helper subsystem. **Where no qualifying operation is available it stops without writing and reports +the limitation** — the admission rule's own second sentence, and there is no weaker fallback. The +runtime was observed on the machine this was written on; **that says nothing about a user's project**, +which is exactly why the stop is the other half of the rule and not an error path nobody expects. + +**Why this needed a decision, and why D3 was not the reason.** D3 forbids a new **synchronization** +mechanism — the parity checker for the three template copies — so a lock directory would breach it +and a single standard-library call would not. What made this the maintainer's call is narrower and +real: it is a **new precondition** on a command whose story said it demands nothing an initializer +does not need — a sentence this decision amends rather than quietly outgrows. Recorded as settled on +2026-09-21, in the story's §5, and the story's own wording was corrected in the same round. + +**What the qualifying operation does not give, so that nothing later reads it as more.** It makes +the creation of the **directory entry** exclusive. It does **not** make writing the template's bytes +atomic, and it does **not** protect the file from any change made after the write returns. A write +that begins and then fails leaves a partial file, and that is an outcome to report, never a +`written` claim — the rule above already says a result may only be reported from a result that was +observed. + +**No lock, no marker file and no synchronization mechanism is introduced.** There is no state here +worth a subsystem. This change carries exactly one new precondition — the runtime above — and +inventing a synchronization mechanism would be a second one of a different kind, which it does not +carry. +**Unresolved guarantee, named — and now only one branch has a sequence to be unresolved about.** +`O_EXCL` gives an exclusive directory entry at creation; it does **not** make the whole +observe-then-create sequence atomic, and nothing available to this command does. The other branch +has no such gap because it performs no write. + +**Unrelated project content and existing mandatory rules are preserved in every branch.** + +**Where the full workflow is already installed** — §5 present, gates in force — the command **says so +and changes nothing about them.** It never removes, disables, weakens or renumbers a gate, and it +offers no downgrade path. This is the lighter entry point, not an exit. + +**Equivalence is judged by a reader, and the command says so.** No fingerprint, no checksum, no +normalizer. A rule set that says the same thing in different words is equivalent; the command reports +`unchanged` and explains what it matched. This is a stated limit, not a guard. + +## §3 What the template omits, and why each omission is required + +| Omitted | Reason | +|---|---| +| §4's closing sentence — *"The work loop includes the review gates: **spec ready → Gate A (spec) → plan ready → Gate A (plan) → execute → tests green → Gate B → commit** (see §5)."* | It routes into a §5 this template does not write. Keeping it would ship a dangling reference. | +| §5 entirely | The command installs no gates. | +| §6 | This repository's own §6 is a personal context canary. It is in no template today and joins none. | +| The closing pointer — *"Project architecture, stack-specific patterns, and invariants live in @AGENTS.md … The Cross-Model Review gates (§5) check against the invariants there."* | Two dangling references in one sentence: `AGENTS.md`, which this command does not write, and §5, which does not exist here. | + +**Kept, and deliberately:** the `# ` heading, the `**Tradeoff:**` line, all of §1–§3, §4 +minus its last sentence (**including** the ground-your-progress-claims paragraph, which depends on no +gate), and the closing `**These guidelines are working if:**` line, which references nothing. + +## §4 Target text — the `Don't guess` rule + +Installed verbatim at the end of section 1 in all three copies. **This block is normative in its +bytes**, wrapping included, because parity is checked by comparing the three copies. + +``` +### Don't guess + +Applies to factual claims in every answer, not only implementation. Confidence is not evidence. + +**Leave gaps visible.** Do not invent missing or ambiguous facts. State what is unknown and why. In extraction +tasks, leave unsupported fields blank where the format permits; otherwise use the format's defined missing-value +handling. + +**Separate evidence from inference.** Cite the relevant source for factual conclusions. Identify deductions and +assumptions as such, with their basis. For extraction tasks, label populated fields EXTRACTED or INFERRED and +explain each inference where the required output format permits. If neither annotations nor accompanying +explanations are permitted, preserve the required format. This does not permit inventing unsupported values. + +**Keep decisions distinct from facts.** Make reasonable design and implementation choices within the authorized +scope, describing them as choices rather than source facts. Ask when missing information changes correctness or +scope. + +**Verify before claiming.** Report a test or action as completed only when its result was observed. Preserve +required output formats; put explanations outside structured artifacts where permitted. +``` + +**Placement in each copy:** immediately after §1's last bullet (`- If something is unclear, stop. +Name what's confusing. Ask.`), separated by one blank line, and immediately before `## 2. Simplicity +First`, separated by one blank line. + +## §5 Target text — the minimal `CLAUDE.md` template + +**The complete content the command writes.** In the command file this is wrapped in a ````markdown +fence (four backticks), because the block below contains a three-backtick fence of its own. +`` is the target directory's name unless the user says otherwise. + +```` +# + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +### Don't guess + +Applies to factual claims in every answer, not only implementation. Confidence is not evidence. + +**Leave gaps visible.** Do not invent missing or ambiguous facts. State what is unknown and why. In extraction +tasks, leave unsupported fields blank where the format permits; otherwise use the format's defined missing-value +handling. + +**Separate evidence from inference.** Cite the relevant source for factual conclusions. Identify deductions and +assumptions as such, with their basis. For extraction tasks, label populated fields EXTRACTED or INFERRED and +explain each inference where the required output format permits. If neither annotations nor accompanying +explanations are permitted, preserve the required format. This does not permit inventing unsupported values. + +**Keep decisions distinct from facts.** Make reasonable design and implementation choices within the authorized +scope, describing them as choices rather than source facts. Ask when missing information changes correctness or +scope. + +**Verify before claiming.** Report a test or action as completed only when its result was observed. Preserve +required output formats; put explanations outside structured artifacts where permitted. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +Ground progress claims: before reporting a step as done, audit the claim against a tool result from this session ("tests green" needs a test run to point to). Report unverified work as unverified — this keeps status reports factual on long runs. + +--- + +**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. +```` + +**Every line above except the `### Don't guess` block is taken unchanged from `workflow-init`'s +template as it stands BEFORE this change.** The only edits are the four omissions in §3 and the one +insertion in §4. **That is the derivation, and it is the pre-change comparison only** — §7 row 4a. + +**After this change the two templates differ by three omissions and no insertion**, because +`workflow-init`'s template gains the same `Don't guess` rule and has no §6 of its own to omit. §7 +row 4b is that comparison. **Conflating the two was pass 1's Major 4**: a verifier holding the shipped +pair to the derivation's transformation rejects a correct change. (Pass 1's row stated that as a +hunk list; neither row requires a hunk shape now — see 4b.) + +## §6 Prompt-standards conformance + +**Invariant 11 owes all twelve items of `docs/prompt-standards.md`, and the scripts reach three of +them.** Pass 1's Major 6 was that this section addressed two items and §7 then claimed the scripts +established conformance. They do not: `AGENTS.md` invariant 11 says so itself — the checks in +`scripts/check-invariants.sh` "are a floor, not coverage. Every other item is judged by a reader." + +### The three items a script touches, and the discovery that gates them + +- **Item 1 — `Target model:`.** A `Target model: Claude via Claude Code.` line goes in the command + body, matching `workflow-init.md` and `process-pr-review.md`. +- **The declaration check only runs on files the scan selects, and the selector is a content + marker, not a path list.** `scripts/check-invariants.sh:313` builds its file set with + `grep -rl 'prompt artifact and follows'`. **The command body must therefore carry that exact + phrase** — the spelling `workflow-init.md:9` uses, *"This command is a prompt artifact and follows + …"* — or the file is outside the scan and its declaration is never counted. + **Verified, not assumed:** `process-pr-review.md` carries a `Target model:` line and **not** the + marker, so it sits outside the scan today. A `claude-init.md` written without the marker would too, + and §7's check would be green about a file nothing looked at. + **So the check on the check:** after the command file exists, confirm its path **appears in the + scan's own output** (`grep -rl 'prompt artifact and follows' --include='*.md' .`) before treating a + green `check-invariants.sh` as evidence about it. +- **The template must not contain a `Target model:` line.** `workflow-init.md` records this as a + reasoned `n/a` in a note **outside** its fence, because the scaffolded file's executing model is + whichever model the target project runs. `claude-init.md` carries the same note, in the same place, + for the same reason — and a second `^Target model:` line in the file would make its declaration + count 2 and fail the check. +- **The checklist-count check does not cover this file.** `check-invariants.sh` parses the count + claim in `docs/prompt-standards.md` and in `workflow-init.md` **by name**. `claude-init.md` is not + in that pair, so it must simply **make no numeric checklist-count claim**; if it ever does, the + checker will not verify it. + +### The other nine items are owed to a reader, and the plan assigns the review + +**The plan carries an explicit twelve-item review of `claude-init.md`** — every item of +`docs/prompt-standards.md` answered, in writing, with either the answer or a **reasoned `n/a`** in +the shape `workflow-init.md` already uses for item 1 of its scaffolded template. Items 2 (success +criteria), 3 (stop conditions), 4 (output format **with an example**), 5 (structured sections), 6 +(rules carry their why), 7 (no contradictions with `CLAUDE.md`/`AGENTS.md`), 8 (token-lean), 9 +(positive instructions), 10 (diagnostic states name their causes), 11 (enforcement claims name their +mechanism) and 12 (calibrated emphasis) **have no mechanical check at all** and are satisfied only by +that review being done and recorded. + +**The same review covers the changed prompt text elsewhere in this change** — the `Don't guess` rule +as it lands in `workflow-init.md`'s template and in this repository's own `CLAUDE.md`. + +**Item 11 applies to this spec too**, and pass 1 found it violated four times. Any sentence here +saying something is enforced must name the mechanism and stop at what that mechanism compares. + +## §7 Verification — what is checked, and how + +**Every row states only what its own command compares.** Where a claim is broader than its check, the +check's scope is the claim and the remainder is assigned to the walkthrough below. Pass 1's Majors 4 +and 6 and Minor 7 were all rows that did not do this. + +| # | Claim — exactly as wide as the check | Check | +|---|---|---| +| 1 | The `Don't guess` block is byte-identical in all three copies | Extract each copy's block and `diff` them pairwise; expect empty. | +| 2 | The template contains none of the spellings `Gate A`, `Gate B`, `§5`, `Cross-Model` | `grep -c` for each inside the template fence; expect 0. **This is absence of those spellings, not absence of gate obligations** — a sentence demanding approval before every commit passes it. | +| 3 | The template contains neither the spelling `AGENTS.md` nor `(see §5)` | `grep -c` for each inside the fence; expect 0. **Absence of those spellings, not absence of dangling references** — a pointer to `@POLICY.md` passes it. | +| 4a | **Pre-change derivation.** The proposed template equals `workflow-init`'s template **as it stands before this change**, with exactly the four §3 omissions applied and the `Don't guess` rule inserted | Build that expectation mechanically from the pre-change fence and compare for **exact equality**; expect byte-identical. | +| 4b | **Post-change comparison.** The shipped `claude-init` template equals the shipped `workflow-init` template with exactly **three** omissions applied and **no insertion** | Build the expectation mechanically from the shipped `workflow-init` template by removing §4's closing gate-chain sentence, §5, and the closing `@AGENTS.md` pointer, then compare it with the shipped `claude-init` template for **exact equality**; expect byte-identical. The transformation applies **no insertion** — both templates carry `Don't guess` after this change — and **removes no §6**, because `workflow-init`'s template has none. **No number of diff hunks is required**: how many hunks a rendering shows is a property of the renderer's context window, not of the change. | +| 5 | No `commands` key was added to the plugin manifest | `grep -c '"commands"' plugins/dev-workflow/.claude-plugin/plugin.json`; expect 0. **This is one key.** Invariant 6 as a whole — no `skills`, `agents` or `hooks` key either — is checked separately by `scripts/check-invariants.sh`. | +| 6 | The version was bumped and logged | `scripts/check-version-bump.sh` against the PR's own base ref, plus a `CHANGELOG.md` entry for the new version. **Verifies a bump is present, not that it is correct** — the script's own documented limit. | +| 7 | The three mechanical prompt-conformance spellings hold, and the pinning and manifest invariants hold | `sh scripts/check-invariants.test.sh && sh scripts/check-invariants.sh`. **This is not prompt conformance** — `AGENTS.md` invariant 11 calls these three "a floor, not coverage"; the other nine checklist items are judged by the §6 review. | +| 8 | The commands in `AGENTS.md` § Commands exit 0 | The full quality battery. **Tested coverage, not "nothing broke"** — the battery covers shell lint, the hook suites and the two checkers, and nothing in this change is executable. | +| 9 | `claude-init.md` is inside the conformance scan | `grep -rl 'prompt artifact and follows' --include='*.md' .` lists it. **Without this, row 7 says nothing about the new file** (§6). | + +**Rows 2, 3, 4a and 9 are the `battery+check` counterfactual, and the counterfactual is +demonstrated rather than asserted.** Run against `workflow-init`'s template, rows 2 and 3 go red: +the six greps return **6, 12, 3, 2, 7, 10** where the new template returns **0** for each. Row 4a's +expectation is built from a source that must be transformed to match, so an untransformed template +fails it. Row 9 goes red on any file lacking the marker — `process-pr-review.md` is a live example. + +**Row 4b has no counterfactual and is not offered as one.** It compares two files that will both be +correct or both be wrong, so it detects divergence between them and not an error common to both. + +**What is not checked mechanically, stated rather than implied.** Nothing verifies the command's +*behaviour* in a target project — that a new target receives only `CLAUDE.md`, that a second run adds +no duplicate, that an overlapping file triggers the ask, that each of step one's five path kinds +stops where it should. Those are **prompt instructions read by a model**, and this repo has no +harness that executes a command against a fixture project. They are verified by a **walkthrough** — a +reading of the command text against each case — and a walkthrough is evidence about the text, **not** +an execution. Any report saying otherwise is false. + +**The walkthrough also carries what rows 2 and 3 cannot reach.** Those rows establish the absence of +four and two *spellings*. The **semantic** questions — does the template impose any gate obligation +in any wording, does it point at any file this command does not write — are read by a human against +the template text, and the report says which rows were mechanical and which were read. + +### Accounting — what the pass-1 repair kept, moved and dropped + +`AGENTS.md`: *"Never replace a decision procedure without accounting for its old conditions."* The +pass-1 §2 table and §7 table were both replaced; every condition they carried is listed. + +| Old condition | Fate | +|---|---| +| Absent → write, report `written` | **Kept**, now in step two, reachable only after step one. | +| Equivalent → report `unchanged`, add nothing | **Kept**, unchanged, now in step two. | +| Different/overlapping → focused diff, ask before merging | **The diff and the ask are kept**; the merge that followed them is **dropped** — the branch now proposes and stops. `AGENTS.md` invariant 9 and the story both require only *show the diff and ask*; *before merging* was this spec's own addition, so what is dropped is the spec's, not theirs. Pass 3's Major. | +| "Unrelated project content and existing mandatory rules are preserved in every branch" | **Kept**, moved below step two so it governs both steps. | +| "Equivalence is judged by a reader … a stated limit, not a guard" | **Kept**, verbatim, in place. | +| Full workflow present → say so, change nothing | **Kept**, verbatim, in place. | +| §7 row 1 (three-copy parity) | **Kept**; claim narrowed to the `Don't guess` block, which is what the check extracts. | +| §7 rows 2–3 (gate obligations, dangling references) | **Kept as checks**; their claims **narrowed to spellings**, with the semantic remainder **moved** to the walkthrough. | +| §7 row 4 (template derivation) | **Split** into 4a (pre-change derivation, exact equality) and 4b (post-change comparison, three omissions, no insertion). The original single row asserted a post-change diff that cannot exist. | +| §7 row 5 (manifest) | **Kept**; claim narrowed to the `commands` key, with invariant 6 as a whole named as a separate check. | +| §7 row 6 (version) | **Kept**; the script's own "present, not correct" limit added. | +| §7 row 7 ("invariants and prompt conformance hold") | **Kept as a check**; the claim **dropped** to the three spellings it actually tests, with the nine reader-judged items moved to §6. | +| §7 row 8 ("nothing else broke") | **Kept as a check**; the claim **dropped** to "the battery's commands exit 0". | +| The counterfactual paragraph | **Kept and corrected**: it named rows 2–4, and row 4b has no counterfactual, so the set is now 2, 3, 4a and 9. | +| **Nothing was dropped without a replacement** | The only outright drops are two overclaims (rows 7 and 8), and in both cases the check survives and only the sentence about it shrank. | + +### Accounting — what the pass-2 and pass-3 repairs kept and dropped + +Three decision procedures were replaced across these rounds: the create branch's write rule (§2), +the **merge** branch's write rule (§2), and row 4b's check (§7). The merge branch's rule was +replaced once, by removal, after Gate-A spec pass 3; two new conditions came with that removal — the +proposal must preserve what a write would have had to preserve, and approval opens no write path — +and they are stated in §2 rather than here, because this table accounts for **old** conditions. +**The create branch's rule was replaced three times** — first to stop the overwrite-and-disclose +fallback; then again once POSIX was read against the remaining route and `set -C` turned out not to +satisfy the rule either, which left the branch unable to write at all; then a third time when a +qualifying operation was admitted and the maintainer accepted its precondition. All three are +accounted for below, and where a later one undid part of an earlier one the row says so. Every condition each procedure carried is listed, kept or dropped on +purpose. + +| Old condition | Fate | +|---|---| +| Create branch: re-read immediately before writing | **Kept**, now stated for the create branch alone, since it is the only branch that writes. Its value is **re-described**, not re-claimed: it improves the report, it is not the guard — the exclusive-create operation is. | +| Create branch: `set -C` is the named operation | **Dropped.** The first replacement kept it and narrowed its claimed guarantee; the second removed it as a route, because a narrower description does not make an operation qualify. XCU 2.7.2 requires failure only for an existing regular file or a symlink resolving to one, makes a dangling symlink *"may also fail"*, and says nothing about FIFOs, devices or directories. Pass 2's Major 2, and the contradiction that survived its first repair. | +| Merge branch: apply the merge as `Edit` operations, never a whole-file `Write` | **Dropped.** The branch no longer writes, so there is no operation to constrain. The sentence was not wrong about `Write`; it is simply moot. | +| Merge branch: "`Edit` rejects a changed preimage" and "the outside-region limit costs nothing here" | **Dropped as unsupported.** Pass 3's Major. `Edit` was measured to publish by replacing the file, and an atomic file replacement is not an atomic check-and-replace of previously read content; POSIX `rename()` imposes no condition on the destination's content. Neither the concrete loss nor its absence was proven — what failed is the **support for the guarantee**, which is enough to stop claiming it. | +| "Only preimage rejection is a guarantee, and it is available on one branch and not the other" | **Dropped.** It was the frame that made an unsupported guarantee look like the strong half of a pair. With one writing branch there is no pair, and the create branch's guarantee is named where it is used. | +| Create branch: a residual remains and is named | **Dropped.** A named residual presupposes a route used in spite of the rule. The qualifying route has no such residual at creation; what it does *not* cover — atomicity of the content write, and any later change — is stated as a scope limit, which is a different claim. | +| Create branch: the write happens, subject to a caveat | **Dropped**, then **restored on a different footing.** The second replacement removed the write entirely, because no route qualified. The third admits `O_EXCL` and the write returns, so the branch is functional again — not by relaxing the rule, which is unchanged, but by finding an operation that meets it. | +| Step two: "Absent → create the file, report `written`" | **Kept as the intended outcome**, now conditional on a qualifying create operation and naming the stop when none is available. The condition is normally satisfied; the stop is the honest other half, not an afterthought. | +| Create branch: the maintainer's open decision | **Closed on 2026-09-21** in favour of admitting `O_EXCL` and accepting its precondition, recorded in the story's §5. The alternative — weakening the preservation guarantee — was rejected while a qualifying standard operation existed. | +| "every candidate route breaches D3" | **Dropped as an overclaim, and it was mine.** D3 forbids a new *synchronization* mechanism; a lock directory breaches it, a single standard-library call does not. What made this the maintainer's call was a **new precondition** on a command whose story says it demands nothing an initializer does not need. | +| §2: "no lock, no marker file, no synchronization mechanism" | **Kept**, verbatim through all three replacements. The second introduced no mechanism because it removed a write; the third adds none either — `O_EXCL` is a standard-library call, not a lock, a marker or a subsystem. What it *does* add is a runtime precondition, a different kind of cost, priced separately in the row above. | +| Create branch: where the ordinary file tool is used, write anyway and disclose the race | **Dropped deliberately**, replaced by *stop without writing and report* — the shape step one already uses. Disclosure after an overwriting write is not preservation, and D2 forbids the gate removal it permits. Pass 2's Major 1. | +| §2: "preserved in every branch" | **Kept**, verbatim, and the reason it holds on the create branch has now changed twice. An earlier wording of this row said it held because the branch could not write — true then, and an overstatement dressed as a guarantee. It now holds because the create operation **refuses every existing destination by specification**, so no existing entry is written through. That is a real guarantee about the directory entry, and **only** about the directory entry. | +| Row 4b claim: three omissions applied, no insertion | **Kept**, verbatim. | +| Row 4b check: expect exactly the three omission hunks | **Dropped**, replaced by exact equality against a mechanically transformed source — the shape 4a already uses. A hunk count is a property of the renderer, not of the change, and the measured count for this pair is not three. Pass 2's Major 3. | +| Row 4b: no `Don't guess` hunk, no §6 hunk | **Kept as statements about the transformation** — no insertion, no §6 removal — rather than as expectations about a rendered diff. | +| Row 4b has no counterfactual | **Kept**, unchanged; exact equality does not give it one. | + +## §8 Deliberately out of scope + +- **A parity checker for the three template copies.** Named as a cost in the story (D1/D3), not + solved here. If drift is observed it earns a ledger row. +- **Any hook change.** The gate hook is untouched. +- **Any change to `/workflow-init`'s behaviour** beyond inserting the one rule into its template. +- **Any change to the active `loop-rule-consolidation` work.** Different branch, different cycle. diff --git a/docs/superpowers/stories/2026-09-17-claude-init-command-story.md b/docs/superpowers/stories/2026-09-17-claude-init-command-story.md new file mode 100644 index 0000000..1c73373 --- /dev/null +++ b/docs/superpowers/stories/2026-09-17-claude-init-command-story.md @@ -0,0 +1,197 @@ +# `/dev-workflow:claude-init` — general working rules without the review workflow — Story + +**Date:** 2026-09-17 · **Size:** story +**Risk:** standard · **Security:** none · **Validation:** battery+check + +**Profile log:** +- 2026-09-17 · adoption · proposed at intake as `standard` / `none` / `battery+check`; **confirmed by + Daniel on 2026-09-17, exactly as proposed.** Gates read this header, which is the only writable + copy. Derived floor **3** — max(risk `standard` = 1, security `none` = 0) = 1, and only 0 gives a + floor of 1. + +## 1. Problem statement + +**The kit ships exactly one way to get its discipline rules into a project, and it is the whole +workflow.** `/dev-workflow:workflow-init` scaffolds `CLAUDE.md`, `docs/hardening-log.md`, +`docs/hardening-taxonomy.md`, `docs/prompt-standards.md`, `docs/pr-review-bots.md`, `.gitattributes`, +`.mcp.json` and CI, then interviews the user through `AGENTS.md`. Its preflight requires the +superpowers plugin, a Codex MCP server exposing `exec` and `review`, and a project whose quality +battery can be named and verified. + +**A project that wants only the working rules has no route.** The §1–§4 rules — think before coding, +simplicity, surgical changes, goal-driven execution — are useful on their own and carry none of that +machinery. Today the only ways to get them are to run the full initializer, which installs gates the +project has not agreed to, or to copy the text by hand, which is how a copy drifts. + +**Taking §5 out is not a subtraction.** `workflow-init`'s §4 ends with the sentence *"The work loop +includes the review gates: **spec ready → Gate A (spec) → plan ready → Gate A (plan) → execute → +tests green → Gate B → commit** (see §5)."* Dropping §5 while keeping that sentence leaves a +`CLAUDE.md` pointing at a section that does not exist — the dangling-reference defect this repo has +shipped twice (`ci.yml` pointing at a deleted README section; `MANIFEST.md` listing a `CLAUDE.md` +that did not exist). The same holds for the closing pointer to `@AGENTS.md`, which `claude-init` does +not write, and for §6, which in this repo's own `CLAUDE.md` is a personal context canary and belongs +in no template at all. + +**A second problem travels with it.** The §1 rules say "don't assume" but never say what to do with a +fact that is missing rather than ambiguous. A model that cannot find a fact fills it in, and nothing +in the current text names that as the failure it is. The revised **Don't guess** rule closes that, +and it is wanted in all three copies — the new template, `workflow-init`'s template, and this repo's +own `CLAUDE.md` — because a rule that reads differently in three places is three rules. + +## 2. Desired outcome + +**One new command, `/dev-workflow:claude-init`, whose only write to a target project is `CLAUDE.md`.** +It carries an inline template of the general rules — §1–§4 with the gate-chain sentence removed, no +§5, no §6, no pointer to files it does not create — plus the revised **Don't guess** rule under §1. + +**It initializes a file, so it demands nothing an initializer does not need — with one named +exception.** No superpowers, no Codex MCP server, no `gh`, no package manager, and no git +repository. It does not invoke `workflow-init`, does not `git init`, does not install anything, does +not write a workflow marker, and does not commit. **The exception is creating the file**, which +needs an already-present Python 3 runtime, because that is the only route checked that refuses every +existing destination. See §5 item 4. + +**It never destroys what it finds.** Missing → write, through an operation that refuses any +existing destination; where no such operation is available, stop without writing and say so. +Equivalent rules already present → report +unchanged and add nothing. Different or partly overlapping → show a focused proposed diff and ask, +then hand the result back — **the command does not write over an existing file** (§5 item 5). +Unrelated project content and existing mandatory rules are preserved either way. **Where the full +workflow is already installed, the command says so and leaves every gate exactly as it is** — it is +the lighter entry point, never a downgrade path. + +**The `Don't guess` rule reads identically in all three copies.** + +## 3. Acceptance criteria + +1. **`plugins/dev-workflow/commands/claude-init.md` exists**, carries its template inline per + invariant 8, and is loaded by convention — **no `commands` key is added to the plugin manifest** + (invariant 6). +2. **A new target project receives `CLAUDE.md` and nothing else**, where the exclusive-create + operation of §5 item 4 is available. No ledger, no taxonomy, no `.mcp.json`, no CI, no + `.context/` marker, no `AGENTS.md`. Where it is not available, the command writes nothing and + reports the limitation — that is the criterion met, not waived. +3. **A repeated invocation adds no duplicate rule.** Second run on its own output reports unchanged + and writes nothing. +4. **The minimal template contains no gate obligation and no dangling reference.** No §5, no §6, no + gate-chain sentence, and no pointer to a file the command does not write. +5. **The `Don't guess` rule is byte-identical** in the new template, in `workflow-init`'s template, + and in this repo's own `CLAUDE.md`. +6. **The command inventory and layout documentation name the new command** — `README.md`'s component + table, `AGENTS.md`'s layout tree, `docs/architecture.md`'s tree — checked by the reference-grep + `AGENTS.md` requires before editing layout documentation. +7. **The plugin version is bumped and `CHANGELOG.md` carries the entry** (invariant 12). +8. **The quality battery is green** and `docs/prompt-standards.md`'s twelve items are answered for + the new command file. + +## 4. Settled inputs — decided, paid for, and not to be reopened + +- **D1. Inline template, no template engine and no cache-path discovery.** Invariant 8, and the + assignment restates it. The cost is named rather than hidden: §1–§4 now exist in **three** copies + and nothing mechanical compares them. That is accepted here, not solved. +- **D2. `claude-init` never touches the full workflow.** Where §5 and its gates are present, they + stay. There is no remove, disable, or downgrade path, and none is to be proposed. +- **D3. No new synchronization mechanism.** A parity checker for the three copies is out of scope for + this story. If drift becomes a real defect it earns its own ledger row and its own rung. +- **D4. The command writes exactly one file.** Not "one file plus a marker", not "one file plus a + README note". One. +- **D5. `CLAUDE.md` in a target project is a general working-rules file, not a gate file.** The + scaffolded §4 keeps its goal-driven-execution rules, including the ground-your-progress-claims + sentence, and loses only the clause that routes into §5. + +## 5. Settled by Daniel — no longer open + +Items 1–3 were settled on 2026-09-17, items 4 and 5 on 2026-09-21. + +1. **The profile is confirmed** as `standard` / `none` / `battery+check`, exactly as proposed, and is + recorded in the profile log above. The reasoning it was accepted on: the command writes over a + file a project may have authored by hand, so the failure mode is **content loss** rather than + inconvenience, and `trivial` would read that write path as harmless — the lenient direction on the + one axis that matters here. + + **Floor 3, read as §5 actually states it.** Three *valid* passes and a **clean final pass** — not + three clean passes. The zero-finding early exit below the floor stands, as does every other duty: + the Blocker/Major resolve rule, the clean-final-pass rule, the mandatory tells, and the stop + conditions. Nothing here waives any of them. + +2. **Version 0.12.0, and `claude-init` is planned as the first of the two changes to merge.** That is + **release sequencing, not permission to merge.** `0.13.0` was explicitly rejected as a way to + reserve `0.12.0` for an unfinished branch. + + **Verified at the time of recording:** `origin/main` and local `main` are both + `7c0d475b9a4a1897e8b03dfa20ec058b9ce09ba6`, the manifest there is `0.11.0`, `CHANGELOG.md`'s newest + entry is `0.11.0`, and there are **no open pull requests**. So `0.12.0` is free. + + **The integration base is rechecked before the version is finalized**, not taken from this note — + `main` can move, and a version chosen against a stale base is the defect + `scripts/check-version-bump.sh` is blind to ("two PRs branched from the same version each bumping + to the same new one"). + + **The `loop-rule-consolidation` branch is not edited by this change.** Its plan text pins `0.12.0` + for itself; reconciling that is its own cycle's work, on its own branch, and is deliberately not + started here. + +3. **The `Don't guess` rule's extraction clause is made format-conditional** — Daniel's choice of + 2026-09-17, answering Gate-A spec pass 1's Major 3. The earlier wording made the + EXTRACTED/INFERRED label **unconditional** while its sibling clauses were conditional, so a schema + admitting an integer and no extra keys could satisfy neither. The sentence now reads, in both + normative spec blocks and eventually in all three product copies: + + > For extraction tasks, label populated fields EXTRACTED or INFERRED and explain each inference + > where the required output format permits. If neither annotations nor accompanying explanations + > are permitted, preserve the required format. This does not permit inventing unsupported values. + + **The last sentence is the point of the change**: relaxing the labelling duty must not read as + permission to fill a field in. **The three product copies are not written in this round** — the + spec cycle is not closed, and implementation is not authorized. + +4. **The create branch may require an already-present Python 3 runtime** — Daniel's choice of + 2026-09-21, answering the contradiction Gate-A spec pass 2's Majors 1 and 2 left behind. The + create branch must refuse an existing destination of every kind. Two routes failed that test: the + ordinary file-writing tool refuses nothing, and POSIX requires noclobber to refuse only a regular + file or a symlink resolving to one. `os.open` with `O_WRONLY | O_CREAT | O_EXCL` meets it by + specification, including a dangling symlink, which POSIX requires to fail *"regardless of the + contents of the symbolic link"*. + + **The rejected alternative** was weakening the preservation guarantee so a lesser operation would + do. It was rejected because a qualifying standard operation exists; accepting a runtime + precondition is the smaller product decision than accepting content loss. + + **What is accepted with it**, stated so it is not discovered later: a project without that + runtime gets **no** created file, and the command says so instead of writing one a weaker way. + Nothing is installed and no fallback is offered. **This is not a D3 breach** — D3 is about a new + *synchronization* mechanism, and this is neither a lock, a marker nor a subsystem; it is a + precondition, which is why it was Daniel's to accept and not an agent's. + + **The guarantee stays narrow.** Exclusive creation of the directory entry is not an atomic write + of the template's bytes and is no protection against a later edit. A write that fails part-way + leaves a partial file and is reported as a failure, never as `written`. + +5. **The command never writes over an existing `CLAUDE.md`. It proposes and stops** — Daniel's + choice of 2026-09-21, answering Gate-A spec pass 3's Major. Where the rules differ or overlap, + the command shows the focused diff, asks, and hands back the proposed result; the user applies + it. Only the create branch writes. + + **Why.** The spec had claimed the merge write rejected a changed preimage. That claim was found + unsupported: the operation publishes by replacing the file, and an atomic file replacement is not + an atomic check-and-replace of content read earlier. The remaining option was an **operating + assumption** that nobody else writes during the run — and an assumption cannot carry **D2**, + which is absolute. Removing the write removes the conflict; qualifying it would not have. + + **Nothing in §2 above or in `AGENTS.md` invariant 9 required that write.** Both ask only for a + focused diff and a question. *"Before merging"* was the spec's own addition, and it is what was + dropped. + + **Two limits come with it, so that "we do not write" is not read as "anything goes".** The + proposal itself must preserve unrelated content and existing mandatory rules and must never + remove, disable, weaken or renumber a gate — D2 binds its content, not only the act of writing. + And approval opens no write path: *proposal produced and approved* is never reported as a merge + that was applied, and is never followed by a write through some other operation. + + **The cost, accepted:** on a project that already has a `CLAUDE.md`, the user does the merge by + hand. + +## 6. Suggested size + +**Small.** One new command file, one rule added in three places, three inventory lines, one version +bump, one changelog entry. No executable code, no hook change, no change to any gate. From 8b3867af79ff4017fb5556aaeabc89d483d784dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:21:01 +0200 Subject: [PATCH 2/4] docs(plans): add the claude-init implementation plan The plan behind /dev-workflow:claude-init: add the Don't guess rule to the two existing section-1 copies, create the command with its inline template, name it in the three inventory sites, bump dev-workflow to 0.12.0, and record the review, battery and counterfactual evidence. Gate-A plan cycle closed at pass 14. cycle d0wzih7gr2; floor 3 per {docs/superpowers/stories/2026-09-17-claude-init-command-story.md (level 1)}; hook reminder threshold absent cycle d0wzih7gr2; Gate-A plan (passes 1-14, pass 1 gpt-6-astra; pass 2 undetermined; pass 3 gpt-6-astra; pass 4 gpt-6-astra; pass 5 gpt-6-astra; pass 6 gpt-6-astra; pass 7 gpt-6-astra; pass 8 gpt-6-astra; pass 9 gpt-6-astra; pass 10 gpt-6-astra; pass 11 gpt-6-astra; pass 12 gpt-6-astra; pass 13 gpt-6-astra; pass 14 gpt-6-astra): Findings 22,27,31,30,39,37,42,42,42,41,38,37,41,40. Blockers 0,0,0,0,0,0,0,0,0,0,0,0,0,0. Majors 7,3,4,2,2,2,2,1,1,9,3,2,3,0. Pass 8 returned no Major; the recorded 1 is a Minor upgraded under CLAUDE.md's severity test (the plan's success token contradicted the approved spec). Pass 2 used its single recovery attempt after an unclassified failure; it is one pass. From pass 9 to 13 the gate prompt carried an added severity sentence, withdrawn before pass 14. Pass 2's reviewer model is undetermined: the Codex config was last modified between passes 2 and 3. Findings and dispositions for every pass are in the cycle's working files, which are not committed. --- .../plans/2026-09-21-claude-init-command.md | 1485 +++++++++++++++++ 1 file changed, 1485 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-21-claude-init-command.md diff --git a/docs/superpowers/plans/2026-09-21-claude-init-command.md b/docs/superpowers/plans/2026-09-21-claude-init-command.md new file mode 100644 index 0000000..ca50a5b --- /dev/null +++ b/docs/superpowers/plans/2026-09-21-claude-init-command.md @@ -0,0 +1,1485 @@ +# `/dev-workflow:claude-init` Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Story:** `docs/superpowers/stories/2026-09-17-claude-init-command-story.md` — read the profile from its header at every gate call; it is the only writable copy. + +**Spec:** `docs/superpowers/specs/2026-09-17-claude-init-command-design.md` + +**Goal:** Ship one new slash command, `/dev-workflow:claude-init`, that writes a single +`CLAUDE.md` of general working rules into a target project — without the review workflow — and add +the `Don't guess` rule to the two existing copies of section 1. + +**Architecture:** No executable code. Five text artifacts change: one new command file carrying its +template inline, two existing copies of section 1, three inventory sites, and the manifest plus +changelog. Every check in this plan is a shell command comparing files; there is no test framework +and no harness that executes a command against a fixture project. + +**Tech Stack:** Markdown prompts, POSIX shell for checks, `shellcheck` and `claude plugin validate` +via the repository's quality battery. + +## Global Constraints + +Exact values, copied from the spec and `AGENTS.md`. Every task's requirements implicitly include +this section. + +- **Invariant 6 — no manifest key.** `commands/` is convention-loaded. `plugins/dev-workflow/.claude-plugin/plugin.json` gains **no** `commands` key. +- **Invariant 8 — the template stays inline** in the command body. `${CLAUDE_PLUGIN_ROOT}` is not expanded in command markdown. +- **Invariant 11 — all twelve items** of `docs/prompt-standards.md` are owed. Three are mechanically checked; nine are judged by a reader and must be answered in writing. +- **Invariant 12 — version bump.** `0.11.0` → `0.12.0`, plus a `CHANGELOG.md` entry. +- **Invariant 5 — exact pinning.** Nothing in this change installs or references a floating version. +- **Story §5 item 4** — the create branch uses `os.open(path, O_WRONLY | O_CREAT | O_EXCL)` and an **already-present** Python 3 runtime. Where no qualifying operation is available it **stops without writing**. No weaker fallback, nothing installed. +- **Story §5 item 5** — the command **never writes over an existing `CLAUDE.md`**. It shows a focused diff, asks, hands back the proposed result and stops. Approval opens no write path. +- **Story D2** — existing gates stay. No remove, disable, weaken or renumber path, and the *proposal's content* is bound by this too, not only the act of writing. +- **Story D3/D4** — no new synchronization mechanism; the command writes exactly one file. +- **The `Don't guess` block is normative in its bytes**, wrapping included (spec §4). +- **No normative text is copied into this plan.** Spec sections are cited by name. A second copy of normative text is the defect this project spends most of its findings on. + +## Gate and commit handling + +**Every commit in this plan is a `WIP:` commit.** Spec §4's rule applies: a `wip`-prefixed message +is cycle-internal, so the gate hook neither fires a Gate-B STOP nor resets the pass counters. The +plan touches `plugins/`, so the change is **not** prose-exempt and owes a full Gate B. + +**Sequence, none of it performed by this plan:** this plan itself owes a **Gate-A plan cycle** +before execution begins. After the tasks are done and the battery is green, one **Gate-B cycle** +runs at the derived floor, and the WIP commits are collapsed with `git reset --soft` and replaced by +one real commit carrying the provenance line, the curve and the evidence entry. See `CLAUDE.md` §5 +Mechanics. + +**That closing commit must carry exactly the reviewed content, and that is not automatic.** `git reset --soft` moves `HEAD` and **leaves the index alone** — including every +unrelated staged entry the five tasks deliberately kept outside their own commits. A plain +`git commit` afterwards consumes all of it, so the single commit that reaches `main` would be the +only one in the whole cycle carrying somebody else's work, and no later inspection can undo a +commit that has already happened. **The close is `CLAUDE.md` §5 Mechanics' own procedure** — soft +reset to the parent of the first WIP commit, then one commit — **with two guards in front of it**: + +```sh +git diff --quiet '' HEAD \ + || { echo 'NOT CLOSING: HEAD differs from the reviewed commit' >&2; exit 1; } +git diff --cached --quiet '' \ + || { echo 'NOT CLOSING: the index differs from the reviewed commit' >&2; exit 1; } +git reset --soft '' \ + || { echo 'NOT CLOSING: reset failed' >&2; exit 1; } +git commit -m '' -m '' \ + || { echo 'CLOSING COMMIT FAILED: stop and surface' >&2; exit 1; } +git diff --quiet '' HEAD \ + || { echo 'CLOSING TREE DIFFERS from the reviewed commit: stop and surface' >&2; exit 1; } +git rev-parse --verify 'HEAD^' +git show --format=full --stat HEAD +``` + +**Two values go in, and where each comes from matters.** + +- **``** is the full 40-character `headSha` kept with the final clean Gate-B pass, + per `CLAUDE.md` §5 Mechanics — never whatever `HEAD` is at closing time. +- **``** is resolved **before** the first task commit and noted in + the Gate-B working record. **It is not the review's `baseSha`**: Gate B may be run against the + merge-base with `main`, and on this branch the approved spec and story commit sits above that + merge-base. Resetting to the merge-base would fold that already-closed Gate-A commit, and the + provenance line and curve in its body, into the closing commit. + +**The two guards are the whole mechanism, and neither covers the other.** `git commit` with no +paths commits the **index**, and the soft reset discards every commit above the parent — so both +must match the reviewed commit before anything moves. **`HEAD`'s tree** against it refuses a WIP +commit landed after the final pass, even one whose index was afterwards put back to the reviewed +tree. **The index** against it refuses unrelated work staged in the index and new content staged on +a reviewed path. An **unstaged** worktree edit is not committed — `git commit` does not read the +worktree — and stays where it was. + +**Unrelated staged work stops the close rather than being carried around it.** The tasks keep such +entries out of their own commits; at closing, `CLAUDE.md`'s procedure would take them. Stop and +surface them to the user — **do not unstage someone else's work yourself**, because an entry's staged +content can differ from its worktree content and unstaging loses it. + +**On any stop after the guards, do not retry and do not repair.** A repository's own commit hooks +can change the index or the worktree during `git commit`, whether it then succeeds or fails, so no +fixed recovery command is promised here. Inspect `HEAD`, the index and the worktree, surface what +you found, and re-establish a reviewed snapshot before closing again. +**`git rev-parse --verify 'HEAD^'` must print ``.** + +**What the comparisons establish, and what they do not.** They compare **trees**: `HEAD`'s and the +index's against the reviewed commit's before the close, and `HEAD`'s against it after. A late commit +whose tree equals the reviewed one — an empty WIP — passes and is folded into the close, which loses +nothing but its message. They do not +establish what the reviewer actually read; `CLAUDE.md` §5 says the same of `headSha` itself. + +The block begins with `git`, contains no `"` and carries no `-m` starting with `wip`, so the hook +reads it as the cycle closing and clears the Gate-B state — correct on success. **It does so whether +or not the close succeeded**, because the hook cannot see an exit status (`codex-gate.sh`'s own +comment at the reset). A failed close therefore costs a further Gate-B pass after restoring, which +is the safe direction. + +**Measured under `sh` and `dash`**, running the block above verbatim with its placeholders filled, +in a throwaway repository shaped like this branch — an approved document commit above the base, +then two `WIP:` task commits: + +| scenario | exit | where it stopped | state afterwards | +|---|---|---|---| +| ordinary close | 0 | — | one closing commit on the parent of the first WIP; the approved document commit **kept**; tree equal to the reviewed commit's | +| unrelated work staged in the index | 1 | the index guard | untouched; the unrelated entry still staged | +| a WIP commit landed after the final pass | 1 | the `HEAD` guard | untouched; the late commit kept | +| the same, with the index afterwards put back to the reviewed tree | 1 | the `HEAD` guard | untouched; the late commit kept | +| new content staged on a reviewed path after the pass | 1 | the index guard | untouched | +| an unstaged worktree edit after the pass | 0 | — | closed with the reviewed content; the edit still unstaged in the worktree | +| unresolvable parent | 1 | the reset | untouched | +| a pre-commit hook that stages a change and then fails | 1 | the commit | `HEAD` at the parent, index changed by the hook — the case the no-fixed-recovery rule exists for | + +For comparison, the canonical procedure **without** the guard, with an unrelated path staged: +the closing tree differs from the reviewed tree and the unrelated path is inside the closing commit. + +The block run through `codex-gate.sh`, with and without `jq`, under `sh` and `dash`: read as the +cycle closing in all four. + +**This is the link the previous round's end-to-end probe stopped short of.** That probe ran the +tasks and the evidence amend and reported green; the cycle closing was never exercised, and the +defect lived there. + +**Profile:** Risk `standard` · Security `none` · Validation `battery+check` — read fresh from the +story header at each gate call, never from this line. `battery+check` owes **the quality battery +green plus a check that fails without the change**, with its counterfactual observed against the +prior state. Every task below is written so its check *is* that counterfactual: the check is run and +seen to fail before the change exists. + +**Lens sets: none.** Risk is not `high` and security is neither `standard` nor `high`. + +## The commit procedure — one procedure, used by every task + +Every task in this plan ends with a commit, and **all of them use the block below**. Three +independent defects found in Gate-A plan passes 1–3 lived in per-task commit spellings that drifted +apart; unifying them is the repair. Substitute only the `set --` line and `SUBJECT`. + +```sh +git status --porcelain +git --literal-pathspecs add -- '' '' \ + || { echo 'STAGING FAILED — not committing' >&2; exit 1; } +git --literal-pathspecs commit --only -m 'WIP: ' -- '' '' \ + || { echo 'COMMIT FAILED' >&2; exit 1; } +git show --stat --format='%H %s' HEAD +git status --porcelain +``` + +**Substitute the path literals and the subject; nothing else changes.** The block carries no +comments, and three properties of its *shape* are load-bearing rather than stylistic — they are +what the gate hook's `jq`-free parser needs, and they are stated as rules here because the block no +longer explains itself inline: + +1. **The Bash call begins with `git`.** No leading comment, no blank line, no variable assignment. +2. **No `"` appears before the end of the `-m 'WIP: …'` argument.** After that argument, double + quotes are harmless. +3. **Every path is a single-quoted literal, spelled out on both the `add` and the `commit`** — + repeated rather than held in `"$@"`, because `"` is what the parser truncates on. +4. **No Bash call that is *not* a commit may contain the substring `commit`** — not in a comment, + not in a `^{commit}` peel, not in a word like "uncommitted". `is_commit` is + `git[[:space:]].*commit` over the whole call, so any call starting with `git` and containing + that substring is classified as a commit; with no `-m …wip` in it, the hook deletes `gateB`, + `passCount` and `freshCount`. **Two such calls were found in this plan by running every block + through the hook** — a `refs/remotes/origin/main^{commit}` peel in the integration-base block, + which cleared the state whenever `jq` was installed, and a `# … is uncommitted` comment on a + bare `git status`, which cleared it in all four configurations. Both are repaired; the rule is + here because neither was visible by reading. + +A subject containing an apostrophe is written `'\''` inside the single-quoted argument — for +example `-m 'WIP: add the Don'\''t guess rule'`. Verified to parse under `sh` and `dash` and to be +recognised by the hook with and without `jq`. + +**Why those three rules, measured rather than asserted:** *The `jq`-free parser* below. + +**Why each part is there. Every clause answers a finding, not a preference.** + +- **`||` after `add`, and `exit 1`.** `git add` and `git commit` as separate unguarded lines let a + failed stage be followed by a commit that records an older index or unrelated staged content. + Reproduced under **both `sh` and `dash`** with a stubbed `git`: with `add` returning non-zero, + **`commit` still ran**. The guard is what stops that; `&&` would do as well, `;` would not. +- **`--only -- '' …`.** It commits the named paths' working-tree content and **excludes anything + else that happens to be staged**, leaving those entries staged and untouched. Without it, every + already-staged unrelated path enters this task's commit, and no later inspection can undo a commit + that already happened. +- **Single-quoted path literals, and `--literal-pathspecs` on top.** The two guard different + stages and **neither substitutes for the other**: quoting stops the **shell** from word-splitting + and glob-expanding the names, and `--literal-pathspecs` stops **git** from reading a leading `:` + as pathspec magic. An earlier version of this procedure used an unquoted `$PATHS` and credited + `--literal-pathspecs` with protection it cannot give — measured, `*.txt` reached git as two + arguments under both `sh` and `dash`. The version before this one used `set -- "…"` with `"$@"`, + which gives the **same** shell-level protection and was replaced for an unrelated reason: its + double quotes are what the hook's `jq`-free parser truncates on. Single-quoted literals carry + that protection unchanged; the price is repeating each path on the `add` and on the `commit`, + and that price is the entire reason for the change. +- **The subject is passed with a literal, single-quoted `-m` beginning with `WIP:`, and none of + that is cosmetic.** The gate hook recognises a cycle-internal commit only from that spelling: + `is_wip_commit() { printf '%s' "$1" | grep -Eiq -- "-m[[:space:]]*['\"]?[[:space:]]*wip"; }`. + **A message supplied through `-F` does not match** — the hook then takes its real-commit path and + runs `rm -f "$state_file" "$count_file" "$fresh_file"` (`codex-gate.sh:886-888`), **clearing the + Gate-B fingerprint and the pass counters**. That is the failure `CLAUDE.md` §5 warns about by + name. Where a body is needed, add a **second `-m`**, which **may** be double-quoted because it + comes after the recognised argument — `-m 'WIP: …' -m ""`, verified recognised with and + without `jq`. + + **Testing that predicate on its own is not enough, and that is how this survived a round.** The + predicate is fed by a parser, and the earlier version of this plan checked only the predicate. + The parser is what the next section measures. +- **The two inspections afterwards.** `git show --stat` confirms the commit contains only the + intended paths; the second `git status --porcelain` confirms nothing this task owns is still + outstanding and that unrelated staged entries survived. + +### The `jq`-free parser — the constraint this plan works around, and what it is not + +`AGENTS.md` invariant 4 makes `jq` **optional**, so the hook has to be correct without it. Without +`jq` it lifts the command out of the JSON payload with + +```sh +printf '%s' "$payload" | sed 's/.*"tool_input"[[:space:]]*:[[:space:]]*{//' \ + | grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -n1 | sed 's/^.*:[[:space:]]*"\(.*\)"$/\1/' +``` + +(`codex-gate.sh:27-36`). Two consequences follow, and both were measured **through the real hook**, +not against the predicate alone: + +1. **`[^"]*` stops at the first `"` in the command.** Every double quote in the shell command is + `\"` in the payload, so the hook sees only the prefix in front of it. +2. **`is_commit` is anchored `(^|[^[:alnum:]])git[[:space:]]`, and a newline inside the payload is + the two characters `\n`.** The character preceding a `git` on a continuation line is therefore + the letter `n`, which is alphanumeric — so only a `git` at the very **start** of the command can + satisfy the anchor. + +Measured in a throwaway repository carrying the adoption marker, running +`plugins/dev-workflow/hooks/codex-gate.sh` under **`sh` and `dash`**, **with `jq` present and with +`jq` removed from `PATH`** — four runs per row, both shells agreeing in every one: + +| command shape | with `jq` | without `jq` | +|---|---|---| +| starts with `git`, `-m "WIP: …"` **double**-quoted | WIP, state kept | **read as a real commit — `gateB`, `passCount`, `freshCount` deleted** | +| `git status …` first, then `commit … -m "WIP: …"` | WIP, state kept | **same: state deleted** | +| a **leading comment**, then that same commit | WIP, state kept | silent — the commit is never seen | +| **this plan's block**: starts with `git`, `-m 'WIP: …'`, no `"` before it | WIP note shown | **WIP note shown — identical** | +| the closing real commit, starting with `git` | state cleared | state cleared | + +**Row three is why "just single-quote the `-m`" was not the fix.** The previous version of this +block opened with a comment containing `"$@"`, so the parse truncated inside that comment: the hook +saw no commit and therefore deleted nothing. The block was protected **by accident**, and any +reordering would have removed the protection without changing a visible behaviour. Row four is the +fix, and it is the only shape in the table that behaves the same with and without `jq`. + +**Stated narrowly, because the distinction is the point.** This is a **plan-level workaround for a +hook defect**. It makes the commands *this plan* issues survive the `jq`-free parser. It does +**not** repair the parser: any other command that opens with a comment, or carries a `"` ahead of +its `-m WIP`, is still mis-read. Repairing `codex-gate.sh` is a separate change with its own scope +decision — **this plan does not touch the hook**, and nothing here should be read as a claim that +the parser defect is closed. + +**The one case this block cannot handle: a path carrying both task-owned and unrelated edits.** +`--only` selects **whole paths**, so it cannot split one. **Stop and surface that overlap rather +than committing a mixture** — no flag resolves it and inventing a split here would be a mechanism +the story does not carry. + +**This procedure was executed in a throwaway repository, not reasoned about.** A fresh `git init` +with one owned file, one **unrelated file staged in the index**, and one untracked file: + +| case | observed | +|---|---| +| `--only -- owned.txt` with `foreign.txt` staged | the commit contained **only** `owned.txt`; `foreign.txt` stayed `M ` — staged and outside | +| `add` stubbed to fail, under **`sh`** and **`dash`** | `STAGING FAILED — not committing`, the commit never ran, in both | +| new untracked file, `--only` **without** `add` | `error: pathspec 'new.txt' did not match any file(s) known to git` — so the `add` is required, which is why Task 2 must not drop it | +| `-m 'WIP: …' -m ""` | subject and body land separately, verified with `git log --format='%s' / '%b'` — **re-run in the single-quoted spelling this plan now uses** | +| the hook predicate against all three planned spellings | all three **recognised**; the old `-F` spelling **not**. **This row is the shallow test** — it exercised the predicate and not the parser feeding it; see *The `jq`-free parser* | + +**And then the whole sequence end to end, including the evidence amend**, in a repository holding +an owned file, **a path containing a space**, and an unrelated staged file — run under **`sh` and +`dash`**: + +| checked after the full run | `sh` | `dash` | +|---|---|---| +| the space-containing path arrived as **one** path | yes | yes | +| the unrelated staged path stayed staged and out | `M foreign.txt` | `M foreign.txt` | +| the tree was **unchanged** by the amend | yes | yes | +| the evidence body landed in the commit | yes | yes | + +**Extended to the cycle-closing commit, and repeated with `jq` removed from `PATH`.** The run +executes the Task 1–4 commit blocks, then **one of the two Task 5 routes**, then the closing block, +each **verbatim as this plan prints it** with only its placeholders filled, in a repository shaped +like this branch — an approved document commit above the base — and checks every exit status. It +runs once with the index otherwise clean and once with an unrelated path staged before the first +task commit: + +| checked after the full run, `sh` and `dash` agreeing in every cell | empty route | owned route | +|---|---|---| +| every task block, the evidence commit and, on the owned route, the amend exited 0 | yes | yes | +| `WIP:` commits before the close | 5 | 5 | +| **index otherwise clean:** close exited 0; tree equal to the reviewed commit's; parent is the first WIP's parent; approved document commit kept; provenance line in the body | yes | yes | +| **unrelated path staged:** every task commit kept it out; the close **stopped at the guard**; nothing moved; the path still staged | yes | yes | + +Every commit-bearing block and the evidence amend, run through `codex-gate.sh` under `sh` and `dash` +with and without `jq`: every WIP call recognised, the closing call read as the cycle closing. + +**A correction to the previous version of this table.** It reported a single run that included "the +Task 5 empty evidence commit, the evidence amend" and marked it green. The amend in that run had +**failed** — `git commit --amend --only` on an empty commit refuses with *"would make it empty"* — +and the harness did not check its exit status. The plan never prescribes that sequence: the amend +belongs to the owned route only. The table above runs the two routes separately and checks every +exit status. + +**That end-to-end run is the one that mattered**, and its absence is why an earlier version of this +table could be entirely green while two defects were still present: each operation was correct in +isolation, and the **transition** to the amend was never exercised. + +**It happened again one link further along, and that is worth stating plainly.** An earlier version +of that run stopped at the amend, so the **cycle closing** was never exercised — and the closing commit was +where the ownership scoping was lost. It also ran only with `jq` installed, so the supported +`jq`-free configuration was never exercised either. Both gaps are closed above, and the rule this +plan now follows is: **the sequence is run to its last commit, in every supported configuration.** + +Earlier rounds checked these sequences by reading them and missed a defect each time. This is the +method change: the stateful part is executed against a real index, **as a whole sequence**. + +**Worktree checks run before the commit; committed-tree checks run after it.** That split is not +stylistic: `scripts/check-version-bump.sh` reads `HEAD` and the merge-base +(`git ls-tree -d --name-only HEAD plugins/`, `git diff --quiet "$mb" HEAD`, `tree_has HEAD`), so it +cannot see a working-tree edit and **cannot pass before the commit that carries it**. Any check that +reads committed trees belongs after the commit, in every task, including on a conditional route. + +--- + +## File Structure + +| File | Responsibility | Task | +|---|---|---| +| `CLAUDE.md` (repo root) | copy 3 of the `Don't guess` rule | 1 | +| `plugins/dev-workflow/commands/workflow-init.md` | copy 2, inside its inline template | 1 | +| `plugins/dev-workflow/commands/claude-init.md` | **new** — the command body and copy 1 inside its inline template | 2 | +| `README.md` | component-table row | 3 | +| `AGENTS.md` | layout-tree entry | 3 | +| `docs/architecture.md` | layout-tree entry | 3 | +| `plugins/dev-workflow/.claude-plugin/plugin.json` | version `0.12.0` — **no** new key | 4 | +| `plugins/dev-workflow/CHANGELOG.md` | the `0.12.0` entry | 4 | + +Task 5 writes no product file; it records the twelve-item review in the execution report. + +--- + +### Task 1: Add `Don't guess` to the two existing copies + +Copy 3 in this repository's own `CLAUDE.md` and copy 2 inside `workflow-init.md`'s inline template. +Doing these first means Task 2's template can be derived and checked against a settled source. + +**Files:** +- Modify: `CLAUDE.md` — end of `## 1. Think Before Coding`, after the bullet `- If something is unclear, stop. Name what's confusing. Ask.`, before `## 2. Simplicity First` +- Modify: `plugins/dev-workflow/commands/workflow-init.md` — the same position inside the `````markdown` fence that opens at line 199 + +**Interfaces:** +- Consumes: nothing. +- Produces: two byte-identical `### Don't guess` blocks. Task 2 derives copy 1 from the + `workflow-init.md` one and compares all three. + +**Placement, exactly as spec §4 states it:** immediately after §1's last bullet, separated by one +blank line, and immediately before `## 2. Simplicity First`, separated by one blank line. + +- [ ] **Step 1: Write the parity check and watch it fail** + +Create `/tmp/dg-parity.py` (a scratch check, not a shipped file). **This script was run against +the current tree while the plan was written**: it reports `OK` on the spec, `MISSING in CLAUDE.md` +on the unmodified root file, and exits 1 in the second case. + +```python +"""Extract each `### Don't guess` block and compare them. Files are read with newline="", +so line endings are not translated: for valid UTF-8, equal decoded text is equal bytes. +A block runs from its heading to the blank line after the `**Verify before claiming.**` +paragraph. Exit 0 only when every named file has one and all are identical.""" +import io, sys, difflib + +def block(path): + lines = io.open(path, encoding="utf-8", newline="").read().split("\n") + heads = [i for i, l in enumerate(lines) if l.strip() == "### Don't guess"] + if len(heads) != 1: + return "%d" % len(heads) + s = heads[0] + v = next(i for i in range(s, len(lines)) if lines[i].startswith("**Verify before claiming.**")) + e = next((i for i in range(v, len(lines)) if not lines[i].strip()), len(lines)) + return lines[s:e] + +blocks = {} +for p in sys.argv[1:]: + b = block(p) + if isinstance(b, str): + print("FAIL %s: expected exactly 1 '### Don't guess' heading, found %s" % (p, b)); sys.exit(1) + blocks[p] = b + +ref = sys.argv[1] +ok = True +for p in sys.argv[2:]: + if blocks[p] != blocks[ref]: + ok = False + print("DIFFERS: %s vs %s" % (ref, p)) + print("\n".join(list(difflib.unified_diff(blocks[ref], blocks[p], ref, p, lineterm="", n=1))[:20])) +print("OK: the one block in each of %d files is identical, %d lines each" % (len(blocks), len(blocks[ref])) if ok else "FAIL") +sys.exit(0 if ok else 1) +``` + +- [ ] **Step 2: Run it to make sure it fails** + +Run: `python3 /tmp/dg-parity.py CLAUDE.md plugins/dev-workflow/commands/workflow-init.md` +Expected: `MISSING in CLAUDE.md`, exit 1. **This is the counterfactual for this task.** Record the +output; `battery+check` owes the observation against the prior state, not the claim. + +- [ ] **Step 3: Insert the block into `CLAUDE.md`** + +Copy the block verbatim from spec §4's fence, wrapping included. Do not retype it, do not reflow +it, and do not assert a line count anywhere — the count is whatever the fence holds, and a stated +count that drifts is this project's most common defect. Insert one blank line, the block, one blank line, before `## 2. Simplicity First`. + +- [ ] **Step 4: Insert the block into `workflow-init.md`'s template** + +Same block, same placement, inside the `````markdown` fence. The fence is four backticks because the +template contains three-backtick fences of its own; do not close it early. + +- [ ] **Step 5: Run the parity check again** + +Run: `python3 /tmp/dg-parity.py CLAUDE.md plugins/dev-workflow/commands/workflow-init.md` +Expected: `OK: the one block in each of 2 files is identical, lines each`, exit 0. + +- [ ] **Step 6: Confirm the insertion is the ONLY change, by reconstruction** + +A file count plus "no line was removed" is **not** that check: it is satisfied by any number of +extra insertions anywhere in either file. Instead, remove the authorized block from the result and +compare the remainder with the file's own pre-task bytes, read from the committed tree — no fixture +is saved. + +Create `/tmp/only-insert.py`: + +```python +"""For each file: build the ONE expected result from its pre-task bytes -- the authorized block +inserted immediately before `## 2. Simplicity First`, after the blank line already there, followed +by exactly one blank line -- and require the working-tree file to equal it. Any other difference +-- an extra insertion, a missing or doubled separator, a different position -- fails. + +Files are read with newline="" and git output is decoded from raw bytes, so no line ending is +translated: for valid UTF-8, equal decoded text is equal bytes.""" +import io, subprocess, sys, difflib + +PRE_REV = "REPLACE_WITH_THE_COMMIT_BEFORE_TASK_1" + +def pre(path): + return subprocess.run(["git", "show", "%s:%s" % (PRE_REV, path)], + capture_output=True, check=True).stdout.decode("utf-8").split("\n") + +def now(path): + return io.open(path, encoding="utf-8", newline="").read().split("\n") + +def block_from_spec(): + S = now("docs/superpowers/specs/2026-09-17-claude-init-command-design.md") + i4 = next(i for i, l in enumerate(S) if l.startswith("## \u00a74 Target text")) + o = next(i for i in range(i4, len(S)) if S[i].strip() == "```") + c = next(i for i in range(o + 1, len(S)) if S[i].strip() == "```") + b = S[o + 1:c] + while b and not b[-1].strip(): + b.pop() + return b + +def expected_after(before, block): + hits = [k for k, l in enumerate(before) if l.startswith("## 2. Simplicity First")] + if len(hits) != 1: + return None, "expected exactly 1 '## 2. Simplicity First' in the pre-task file, found %d" % len(hits) + i = hits[0] + if i == 0 or before[i - 1] != "": + return None, "the pre-task file has no blank line before '## 2. Simplicity First'" + return before[:i] + block + [""] + before[i:], None + +block = block_from_spec() +ok = True +for path in ("CLAUDE.md", "plugins/dev-workflow/commands/workflow-init.md"): + before = pre(path) + after = now(path) + expected, err = expected_after(before, block) + if err: + print("FAIL %-52s %s" % (path, err)); ok = False; continue + if after == expected: + print("OK %-52s equals its pre-task bytes plus exactly the authorized insertion" % path) + else: + ok = False + print("FAIL %-52s differs from its pre-task bytes plus exactly the authorized insertion" % path) + print("\n".join(list(difflib.unified_diff(expected, after, "expected", "working tree", + lineterm="", n=1))[:20])) +sys.exit(0 if ok else 1) +``` + +Replace `PRE_REV` with the same commit id Task 2 Step 4a pins. + +Run: `python3 /tmp/only-insert.py` +Expected: two `OK` lines, exit 0. + +**This procedure was exercised in memory while the plan was written**, against a stand-in block: the +authorized insertion alone leaves a remainder identical to the original, and adding one unrelated +line elsewhere makes the remainder differ. The check detects what its title claims. + +- [ ] **Step 7: Commit** + +```sh +git status --porcelain +git --literal-pathspecs add -- 'CLAUDE.md' 'plugins/dev-workflow/commands/workflow-init.md' \ + || { echo 'STAGING FAILED — not committing' >&2; exit 1; } +git --literal-pathspecs commit --only -m 'WIP: add the Don'\''t guess rule to the two existing section-1 copies' -- 'CLAUDE.md' 'plugins/dev-workflow/commands/workflow-init.md' \ + || { echo 'COMMIT FAILED' >&2; exit 1; } +git show --stat --format='%H %s' HEAD +git status --porcelain +``` + +This **is** the commit procedure above with its path literals and subject filled in — issued as **one** Bash call beginning with `git`. + +--- + +### Task 2: Create `claude-init.md` + +The new command file: frontmatter, the conformance declaration, the operational body, the reasoned +`n/a` note, and the inline template. + +**Files:** +- Create: `plugins/dev-workflow/commands/claude-init.md` +- Read (do not modify): `plugins/dev-workflow/commands/workflow-init.md` — the model for frontmatter, the declaration line at `:9`, and the `n/a` note that sits outside the fence + +**Interfaces:** +- Consumes: Task 1's `workflow-init.md` template, which now carries `Don't guess`. +- Produces: the shipped template that Task 5's row-4b check compares, and the file path Task 3 + writes into three inventories. + +**Spec sections this task installs, cited not copied:** §2 (behaviour), §3 (what the template omits +and why), §4 (the `Don't guess` rule), §5 (the minimal template), §6 (the declaration and the +marker). + +**The template transformation is deterministic and was verified in memory before this plan was +written.** Applying the operations below to `workflow-init.md`'s template reproduces spec §5's +normative template **byte for byte** — 85 lines, 3708 bytes, counting the payload as the lines +between the fences joined with LF, without the LF before the closing fence. The boundaries below are the exact ones +that produce that result; the spec's §3 table names the four omissions, and this is where their +edges are pinned. + +- [ ] **Step 1: Confirm the target file does not exist yet** + +Run: `test ! -e plugins/dev-workflow/commands/claude-init.md && test ! -L plugins/dev-workflow/commands/claude-init.md && echo 'absent, as expected'` + +`test -e` follows symlinks, so a dangling link at the path reads as absent; `test ! -L` is what stops on it. +Expected: `absent, as expected`. If it exists, **stop** and report the collision; another task may +own it. + +- [ ] **Step 2: Write the command file's head** + +Frontmatter and body opening, modelled on `workflow-init.md:1-12`. Three things are load-bearing and +each has a stated reason: + +1. A `Target model: Claude via Claude Code.` line — prompt-standards item 1. +2. The exact phrase **`This command is a prompt artifact and follows`** — `scripts/check-invariants.sh:313` + builds its file set with `grep -rl 'prompt artifact and follows'`. Without the phrase the file is + outside the scan and its declaration is never counted, so a green checker would be green about a + file nothing looked at. +3. **Exactly one** `^Target model:` line in the whole file. A second one makes the declaration count + 2 and fails the check — which is why the `n/a` note in Step 6 must not be phrased as one. + +- [ ] **Step 3: Write the operational body** + +Spec §2 is the source; do not restate it, implement it. The body must instruct the agent through: + +- **Step one — the path kind**, five outcomes, with the rule that `test -e`/`test -f` resolve + symlinks so `test -L` comes first. Report **which row** was observed, not a generic failure. +- **Step two — content**, three outcomes: absent → create; equivalent → report `unchanged`, add + nothing; different or overlapping → focused diff, ask, hand back, stop. +- **Writing** — the create branch only, through `os.open(path, O_WRONLY | O_CREAT | O_EXCL, 0o644)` + in an already-present Python 3. **Pass the mode explicitly**: the default is `0o777`, which under a + conventional umask creates the file executable. Where no qualifying operation is available, stop + without writing and report the limitation. +- **Report forms** — prompt-standards item 4 owes an output format **with an example**. **Eight** + states need one, and the last three cover spec §2's requirement *"A failed write + is an outcome, not silence"*: + `written` · `unchanged` · `proposal produced` (approved and declined are the same state for this + command — it writes in neither) · `stopped: path kind` · `stopped: no qualifying create operation` + · **`stopped: create refused`**, where the exclusive create returned `EEXIST` because something + appeared in the window · **`failed: partial write`**, where the exclusive open succeeded and the + template was then written only in part · **`failed: create write`** for every other failed + create attempt, including failure before an entry was created or after open but before any + template bytes were written. `EACCES`, `EROFS` and `ENOSPC` are examples, not a closed error list. + For each unsuccessful attempt, report the observed error, whether exclusive open succeeded, and + the path's observed state afterwards; if that state cannot be inspected, report that limitation. + **`written` may be reported only from an observed completed write.** An `os.open` that returned is + not that observation — the guarantee is over the **directory entry**, not over the bytes, and a + failed write may leave an empty or partial file. Describe only the state actually observed, with + the failure's cause per item 10. Spec §2's qualifying-operation rule still applies; a failure + opens no fallback write path. +- **The two limits on a proposal** — it preserves unrelated content and existing gates (D2 binds its + content), and approval opens no write path. "Proposal produced" is never reported as an applied + merge. + +- [ ] **Step 4: Run the negative case, then write the template, then run both derivations** + +Insert spec §5's template inside a FENCE-markdown fence (four backticks — the template contains +three-backtick fences). Produce it by applying these operations to `workflow-init.md`'s template, +**not** by retyping. The boundaries below were executed in memory against the real files while this +plan was written and reproduce spec §5's template **byte for byte**: 85 lines, 3708 bytes (the LF +before the closing fence not counted). + +1. **Delete** from the line beginning `The work loop includes the review gates:` up to, but **not + including**, the first line that is exactly `---` after it. That removes §4's closing sentence, + the blank line after it, all of `## 5.`, and the blank line before the separator. +2. **Delete** everything after the line beginning `**These guidelines are working if:**`. That + removes the blank line, the second `---`, its blank line, and the three-line `@AGENTS.md` + pointer. +3. **No §6 removal is performed.** Neither the pre-change nor the post-change `workflow-init.md` + template contains a `## 6.` — measured, `False` in both. Spec §3 lists it among the omissions; + here it is an **already-satisfied absence**, asserted rather than executed. +4. **Insertion applies to row 4a only.** Row 4a derives from the **pre-change** source, which has no + `Don't guess` yet, so its derivation inserts the block. Row 4b derives from the **post-change** + source, where Task 1 already placed it, so its derivation inserts nothing. + +Create `/tmp/tmpl-derive.py`: + +```python +"""Rows 4a and 4b as two SEPARATE exact-equality comparisons, per spec §7. + +Row 4a's source is the template **as it stands before this change**, read from the commit +this branch sat on before Task 1 — an identified revision, not a saved fixture. +Row 4b's source is the working-tree template after Task 1. + +`actual` is passed in explicitly, so the negative case reaches the comparison instead of +throwing while looking for a file. No hunk count is asserted anywhere: how many hunks a diff +renders is a property of the renderer, not of the change. + +A payload is the lines strictly between its opening and closing fence, joined with LF; the LF +before the closing fence is not part of it. Files are read with newline="" and git output is +decoded from raw bytes, so no line ending is translated and no trailing line is stripped: for +valid UTF-8, equal payload text is equal payload bytes. +""" +import io, subprocess, sys, difflib + +FENCE = "`" * 4 +PRE_REV = "REPLACE_WITH_THE_COMMIT_BEFORE_TASK_1" # see Step 4a below + +def fence_after(lines, i): + o = next(k for k in range(i, len(lines)) if lines[k].startswith(FENCE)) + c = next(k for k in range(o + 1, len(lines)) if lines[k].strip() == FENCE) + return lines[o + 1:c] + +def from_rev(rev, path): + return subprocess.run(["git", "show", "%s:%s" % (rev, path)], + capture_output=True, check=True).stdout.decode("utf-8").split("\n") + +def from_file(path): + return io.open(path, encoding="utf-8", newline="").read().split("\n") + +def idx(t, pred, start=0): + return next(i for i in range(start, len(t)) if pred(t[i])) + +SPEC = "docs/superpowers/specs/2026-09-17-claude-init-command-design.md" +S = from_file(SPEC) +target = fence_after(S, next(i for i, l in enumerate(S) if l.startswith("## §5 Target text"))) +i4 = next(i for i, l in enumerate(S) if l.startswith("## §4 Target text")) +o4 = next(i for i in range(i4, len(S)) if S[i].strip() == "```") +c4 = next(i for i in range(o4 + 1, len(S)) if S[i].strip() == "```") +dg = S[o4 + 1:c4] +while dg and not dg[-1].strip(): + dg.pop() + +def omissions(t): + t = list(t) + a = idx(t, lambda l: l.startswith("The work loop includes the review gates:")) + b = idx(t, lambda l: l.strip() == "---", a) + del t[a:b] + c = idx(t, lambda l: l.startswith("**These guidelines are working if:**")) + del t[c + 1:] + return t + +def insert_dg(t): + t = list(t) + i = idx(t, lambda l: l.startswith("## 2. Simplicity First")) + return t[:i] + dg + [""] + t[i:] + +def norm(t): + return "\n".join(t) + +def compare(label, expected, actual): + if norm(expected) == norm(actual): + print("OK %s" % label) + return True + print("FAIL %s" % label) + print("\n".join(list(difflib.unified_diff(norm(expected).split("\n"), norm(actual).split("\n"), + "expected", "actual", lineterm="", n=2))[:40])) + return False + +WI = "plugins/dev-workflow/commands/workflow-init.md" +pre_lines = from_rev(PRE_REV, WI) +pre_src = fence_after(pre_lines, next(i for i, l in enumerate(pre_lines) if l.startswith(FENCE + "markdown"))) +post_lines = from_file(WI) +post_src = fence_after(post_lines, next(i for i, l in enumerate(post_lines) if l.startswith(FENCE + "markdown"))) + +mode = sys.argv[1] if len(sys.argv) > 1 else "all" +if mode not in ("negative", "4a", "4b", "all"): + print("unknown mode %r: use negative, 4a, 4b or all" % mode); sys.exit(2) +ok = True + +if mode in ("negative", "all"): + print("-- negative case: an explicitly UNTRANSFORMED source must not equal the target") + ok &= not compare("negative: untransformed pre-change source vs spec §5 target", target, pre_src) + ok &= not compare("negative: deletions without the insertion vs spec §5 target", + target, omissions(pre_src)) + +if mode in ("4a", "all"): + print("-- row 4a: PRE-change source + the insertion + the deletions") + ok &= compare("row 4a", omissions(insert_dg(pre_src)), target) + +if mode in ("4b", "all"): + print("-- row 4b: POST-change source + the deletions, no insertion") + ci = from_file("plugins/dev-workflow/commands/claude-init.md") + shipped = fence_after(ci, next(i for i, l in enumerate(ci) if l.startswith(FENCE + "markdown"))) + ok &= compare("row 4b", omissions(post_src), shipped) + ok &= compare("row 4b, shipped template vs spec \u00a75 target", target, shipped) + +no6 = not any(l.startswith("## 6.") for l in pre_src) and not any(l.startswith("## 6.") for l in post_src) +print("no §6 in either source:", no6) +ok &= no6 +sys.exit(0 if ok else 1) +``` + +- [ ] **Step 4a: Pin the pre-change revision and run the negative case — before the template exists** + +Replace `PRE_REV` with the commit this branch sat on **before Task 1** — `git rev-parse HEAD` +taken before Task 1's commit, or equivalently `git log --format=%H -1 ^`. It must be +a real commit id, not `HEAD`, which moves. + +Run: `python3 /tmp/tmpl-derive.py negative` + +Expected: **two `FAIL` lines and exit 0.** Read that pairing carefully — in `negative` mode a +*differing* comparison is the assertion passing, so the `FAIL` labels are the desired outcome and +the exit status reports that the negative case held. **Exit 1 here would mean an untransformed +source unexpectedly equalled the target**, which is the real failure. + +**This is the counterfactual, and it reaches the comparison**: both inputs exist, so no file lookup +or fence search can throw instead. A traceback here is a setup defect, not evidence. + +**Both expectations were observed while this plan was written** — run against the tree at +`c1e727f`, `negative` produced exactly two `FAIL` lines and exit 0, and `4a` produced `OK row 4a` +with exit 0. + +- [ ] **Step 4b: Run row 4a — still before the template exists** + +Run: `python3 /tmp/tmpl-derive.py 4a` +Expected: `OK row 4a`, exit 0. **This is the check spec §7 row 4a actually specifies** — the +proposed template against the template *as it stood before this change*, with the insertion applied. +It reads no working-tree command file, so it runs before Task 2 writes anything. + +- [ ] **Step 4c: Write the template, then run row 4b** + +Now install the template in `claude-init.md` and run: `python3 /tmp/tmpl-derive.py 4b` +Expected: `OK row 4b`, `OK row 4b, shipped template vs spec §5 target`, `no §6 in either source: +True`, exit 0. The second comparison is what ties the shipped template to the normative target +directly, rather than only through the post-change `workflow-init` template. + +**Row 4b has no counterfactual and is not offered as one** — it compares two files that will both be +correct or both be wrong. Row 4a's negative case, above, is the one that carries evidence. + +**What this pair checks and what it does not.** Both comparisons read without line-ending +translation and without stripping, so for valid UTF-8 they establish that the compared **payloads** +are byte-identical — the fenced templates, not the whole files around them — and they say nothing +about either source being correct on its own. **Row 4a** ties the pre-change `workflow-init` template +to spec §5; **row 4b** ties the shipped template to both the post-change `workflow-init` template and +spec §5. The post-change `workflow-init` template itself is tied to its pre-task bytes by Task 1's +`only-insert.py`, which is only as current as its last run — rerun it if either file changes. + +- [ ] **Step 5: Add the reasoned `n/a` note, outside the fence** + +`workflow-init.md:190-197` is the model. Same reason, same place: the scaffolded file's executing +model is whatever the target project runs, so a `Target model:` line inside the template would be +false in every repo it lands in. **Outside the fence so it never scaffolds, and deliberately not +phrased as a `Target model:` line.** + +- [ ] **Step 6: Run the spelling and manifest checks** + +```sh +# Rows 2 and 3 — absence of four and two spellings inside the fence. Absence of SPELLINGS, +# not absence of gate obligations or dangling references; the semantic question is the walkthrough's. +python3 - <<'PY' +import io +L = io.open("plugins/dev-workflow/commands/claude-init.md", encoding="utf-8").read().split("\n") +o = next(i for i,l in enumerate(L) if l.startswith("````markdown")) +c = next(i for i in range(o+1,len(L)) if L[i].strip()=="````") +body = "\n".join(L[o+1:c]) +for s in ("Gate A","Gate B","§5","Cross-Model","AGENTS.md","(see §5)"): + print("%-14s %d" % (s, body.count(s))) +PY +# Row 5 — one key. Invariant 6 as a whole is checked separately by check-invariants.sh. +grep -c '"commands"' plugins/dev-workflow/.claude-plugin/plugin.json +# Row 9 — the file is inside the conformance scan at all. +grep -rl 'prompt artifact and follows' --include='*.md' . | grep -Fx './plugins/dev-workflow/commands/claude-init.md' +# Exactly one declaration line. +grep -c '^Target model:' plugins/dev-workflow/commands/claude-init.md +``` + +Expected: all six spelling counts `0`; `"commands"` count `0`; the scan prints exactly +`./plugins/dev-workflow/commands/claude-init.md` and that pipeline exits 0 — `grep -Fx` matches the +product path only, so this plan's and the spec's own mentions of `claude-init` cannot satisfy it; +declaration count `1`. + +- [ ] **Step 7: Run the three-copy parity check** + +Run: `python3 /tmp/dg-parity.py CLAUDE.md plugins/dev-workflow/commands/workflow-init.md plugins/dev-workflow/commands/claude-init.md` +Expected: `OK: the one block in each of 3 files is identical, lines each`, exit 0. + +**What this establishes, and what it does not.** It finds the single `### Don't guess` heading in +each file and compares the blocks. It does **not** establish **where** the block sits — in +`claude-init.md` a block moved out of the template into the command's prose would still compare +equal here. Position inside the scaffolded template is row 4b's job: it compares the whole shipped +template with spec §5's target, which carries the block at the end of section 1. **Story criterion 5 +is discharged by this check and row 4b together**, not by either alone. + +- [ ] **Step 8: Commit** + +```sh +git status --porcelain +git --literal-pathspecs add -- 'plugins/dev-workflow/commands/claude-init.md' \ + || { echo 'STAGING FAILED — not committing' >&2; exit 1; } +git --literal-pathspecs commit --only -m 'WIP: add the claude-init command with its inline template' -- 'plugins/dev-workflow/commands/claude-init.md' \ + || { echo 'COMMIT FAILED' >&2; exit 1; } +git show --stat --format='%H %s' HEAD +git status --porcelain +``` + +This **is** the commit procedure above with its path literals and subject filled in — issued as **one** Bash call beginning with `git`. + +**`claude-init.md` is new and untracked**, so the `git add` in that procedure is what makes +`--only` able to name it. Do not drop the `add` for this task. + +--- + +### Task 3: Name the command in the three inventory sites + +**Files:** +- Modify: `README.md` — the component table, rows at `:23` and `:25` show the shape +- Modify: `AGENTS.md` — the layout tree, `commands/{workflow-init,process-pr-review}.md` at `:46` +- Modify: `docs/architecture.md` — the layout tree, same line shape at `:25` + +**Interfaces:** +- Consumes: the file path created in Task 2. +- Produces: nothing later tasks depend on. + +**`MANIFEST.md` is deliberately not among them** (spec §1): it inventories `source-files/`, the +frozen extraction seed, and a command with no seed origin has no row there. + +- [ ] **Step 1: Run the reference grep `AGENTS.md` requires before touching layout documentation** + +```sh +grep -rn 'commands/{workflow-init' --include='*.md' . | grep -v source-files/ +grep -rniE 'declare[sd]?|convention[- ]load' --include='*.md' . | grep -v source-files/ +``` + +Expected: the first names exactly the two tree sites. **Read the second's hits rather than counting +them** — it is there because a sentence about what the manifest declares must be checked against the +manifest in the same change, and this change adds a convention-loaded component. + +- [ ] **Step 2: Write the check and watch it fail** + +```sh +grep -c '^| `/dev-workflow:claude-init` | command' README.md +grep '^| `/dev-workflow:claude-init` |' README.md | grep -c 'Python 3' +``` + +```sh +got=$(git diff HEAD -U0 -- AGENTS.md docs/architecture.md | grep '^[-+@]' | grep -v '^--- \|^+++ ' | sed 's/^\(@@ [^@]*@@\).*/\1/') +want=$(printf '%s\n' '@@ -46 +46 @@' '- commands/{workflow-init,process-pr-review}.md' '+ commands/{workflow-init,process-pr-review,claude-init}.md' '@@ -25 +25 @@' '- commands/{workflow-init,process-pr-review}.md' '+ commands/{workflow-init,process-pr-review,claude-init}.md') +if [ "$got" = "$want" ]; then echo 'OK trees: the text diff is exactly one in-place line replacement in each'; else echo 'FAIL trees: unexpected diff'; printf '%s\n' "$got"; exit 1; fi +``` + +**What each line compares, and nothing more** — spec §7's rule: a row claims only what its command +compares, and the rest goes to a named reading check. + +- The two README lines match the row's opening and the Python 3 disclosure **anywhere in + `README.md`**. They do **not** establish that the row sits in the component table — Step 5's + reading check does. +- The tree block compares the **text** diff of the two layout files with the one expected text diff: + the tree line replaced **in place**, at `AGENTS.md:46` and `docs/architecture.md:25`, and no other + content line changed. A line moved out of the tree, an extra content edit, or a misspelling all + produce a different diff. It reads the text diff only, so a file-mode or other metadata change is + **outside** it; the commit's own `git show --stat` is where such a change would show. The `sed` + only drops the context label git appends to hunk headers. + +Expected before: `0`, `0`, then `FAIL trees: unexpected diff` with an empty diff and exit 1 — +**the counterfactual**. + +- [ ] **Step 3: Add the README component-table row** + +One row in the same voice as its neighbours, **starting ``| `/dev-workflow:claude-init` | command —``** +like the `workflow-init` row above it — the check in Step 2 matches that opening: what the command is, +and what it writes. Say that it +writes `CLAUDE.md` only and installs no gates, so a reader can tell it from `/dev-workflow:workflow-init` +in the row above. **Disclose the create-branch Python 3 requirement here** — this is the only +inventory site a user reads before running it. + +- [ ] **Step 4: Update the two layout trees** + +`commands/{workflow-init,process-pr-review}.md` → `commands/{workflow-init,process-pr-review,claude-init}.md` +in both `AGENTS.md:46` and `docs/architecture.md:25`. Same spelling in both; they are two copies of +one tree. + +- [ ] **Step 5: Run the check again** + +Same commands as Step 2. Expected: `1`, `1`, then `OK trees: the text diff is exactly one in-place +line replacement in each`, exit 0. + +**Then the reading check for placement**, which the greps do not carry: open `README.md` at its +component table and confirm the new row is **inside that table, directly below the +`/dev-workflow:workflow-init` row**, and that no second `/dev-workflow:claude-init` row exists +elsewhere. Record the observed line number in the Gate-B working record. + +- [ ] **Step 6: Commit** + +```sh +git status --porcelain +git --literal-pathspecs add -- 'README.md' 'AGENTS.md' 'docs/architecture.md' \ + || { echo 'STAGING FAILED — not committing' >&2; exit 1; } +git --literal-pathspecs commit --only -m 'WIP: name claude-init in the component table and both layout trees' -- 'README.md' 'AGENTS.md' 'docs/architecture.md' \ + || { echo 'COMMIT FAILED' >&2; exit 1; } +git show --stat --format='%H %s' HEAD +git status --porcelain +``` + +This **is** the commit procedure above with its path literals and subject filled in — issued as **one** Bash call beginning with `git`. + +--- + +### Task 4: Version bump and changelog entry + +**Files:** +- Modify: `plugins/dev-workflow/.claude-plugin/plugin.json` — `"version": "0.11.0"` → `"0.12.0"` +- Modify: `plugins/dev-workflow/CHANGELOG.md` — a `0.12.0` entry at the top, newest first + +**Interfaces:** +- Consumes: nothing. +- Produces: the version `check-version-bump.sh` reads in Task 5. + +**Recheck the integration base before finalizing the number.** Spec §1 requires it and says why: +`scripts/check-version-bump.sh` is blind to exactly this case — "two PRs branched from the same +version each bumping to the same new one". The recheck is the only thing standing in for it. +`loop-rule-consolidation` pins `0.12.0` for itself in its own plan text; **that branch is not edited +here**, and reconciling it belongs to that cycle. + +- [ ] **Step 1: Recheck the integration base — a live check, not a cached one** + +**Resolve the integration revision once and read everything from it**, so the manifest and the +changelog cannot come from two different states: + +```sh +git fetch origin '+refs/heads/main:refs/remotes/origin/main' \ + || { echo "FETCH FAILED: integration-base check stopped" >&2; exit 1; } +BASE=$(git rev-parse --verify 'refs/remotes/origin/main') \ + || { echo "cannot resolve fetched origin/main" >&2; exit 1; } +echo "integration revision: $BASE" +git show "$BASE:plugins/dev-workflow/.claude-plugin/plugin.json" | grep '"version"' \ + || echo "READ FAILED or no version key in the manifest at $BASE" +# First release heading, no line cutoff. The file opens with a convention preamble, so the +# first `## ` is NOT in the first twenty lines — measured: it is at line 25. +git show "$BASE:plugins/dev-workflow/CHANGELOG.md" > /tmp/cl.$$ \ + || { echo "READ FAILED: changelog not readable at $BASE"; } +if [ -s /tmp/cl.$$ ]; then + grep -m1 -E '^## ' /tmp/cl.$$ || echo "NO RELEASE HEADING found in the changelog at $BASE" +fi +rm -f /tmp/cl.$$ +gh pr list --state open +``` + +The explicit refspec requests remote `main` and updates `origin/main`; the checked fetch status +stops execution before `BASE` is read on failure. Git's error output supplies the cause. Successful +fetch establishes the fetched snapshot, not that the remote cannot move afterwards. + +**Three outcomes are deliberately distinguishable** — a read that failed, a file with no release +heading, and a heading whose version conflicts with the manifest. A single silent empty result +would conflate them, which is what the earlier fixed-prefix form did. + +Expected at the spec's writing: `origin/main` at `7c0d475…`, manifest `0.11.0`, first changelog +heading `## 0.11.0`, no open PRs — so `0.12.0` is free. **If any of these has moved, stop and +report**; choosing a version against a stale base is the defect the checker cannot see. + +- [ ] **Step 2: Run the check and watch it fail — after Tasks 1–3, before the bump** + +Run: `sh scripts/check-version-bump.sh main` +Expected: it reports the plugin changed without a version change, exit non-zero — **the +counterfactual**. + +**The checker reads committed trees, not the working tree**, and that governs where both runs go. +Its own usage line says *"compares HEAD against the merge-base with <base-ref>"*, and it works +through `git ls-tree -d --name-only HEAD plugins/`, `git diff --quiet "$mb" HEAD -- "$dir/"` and +`tree_has HEAD "$manifest"`. So this negative run needs Tasks 1–3's WIP commits to exist — against a +clean tree it passes uselessly — and **the positive run cannot be made until the bump is +committed**. That is why Step 5 below comes after the commit, not before it. + +- [ ] **Step 3: Bump the manifest** + +`"version": "0.11.0"` → `"version": "0.12.0"`. **Change nothing else in that file** — invariant 6 +means no `commands`, `skills`, `agents` or `hooks` key is added. + +- [ ] **Step 4: Write the changelog entry** + +Newest first. The file's own header states the convention: entries are written from `git log` over +`plugins/`, not from memory, and carry **no dates**. Cover the plugin-touching commits of this +change: the new command, and the `Don't guess` rule added to `workflow-init.md`'s template. + +- [ ] **Step 5: Check the working tree — the two things that are readable before a commit** + +```sh +grep '"version"' plugins/dev-workflow/.claude-plugin/plugin.json +grep -c '"commands"\|"skills"\|"agents"\|"hooks"' plugins/dev-workflow/.claude-plugin/plugin.json +grep -m1 -E '^## ' plugins/dev-workflow/CHANGELOG.md +grep -n '^## ' plugins/dev-workflow/CHANGELOG.md +awk '/^## /{n++; next} n==1' plugins/dev-workflow/CHANGELOG.md | grep -c 'claude-init' +awk '/^## /{n++; next} n==1' plugins/dev-workflow/CHANGELOG.md | grep -c "Don't guess" +``` + +Expected: the manifest reads `0.12.0`; the key count is `0`; the newest changelog heading is +`## 0.12.0`, **the same version as the manifest**; and the **body of the newest entry** — the lines +after the first release heading and before the next, which is what the `awk` prints — mentions both +plugin-touching changes: the last two counts are non-zero. Before Step 4 the first line is +`## 0.11.0` and both counts are `0`, which is this check's counterfactual. + +**Uniqueness of the release heading is a reading check, not a count.** The `grep -n '^## '` line +prints every release heading with its line number: **read it** and confirm that `0.12.0` heads the +list and appears in no other heading, in any spelling — trailing spaces, a `v` prefix or a second +entry included. Record what you saw in the Gate-B working record. It reads the entry's text for the two change +names; whether the prose describing them is right is the reviewer's judgement, not this grep's. **The version checker is deliberately +not run here** — it would still read `0.11.0` from `HEAD` and fail a correct change. + +- [ ] **Step 6: Commit** + +```sh +git status --porcelain +git --literal-pathspecs add -- 'plugins/dev-workflow/.claude-plugin/plugin.json' 'plugins/dev-workflow/CHANGELOG.md' \ + || { echo 'STAGING FAILED — not committing' >&2; exit 1; } +git --literal-pathspecs commit --only -m 'WIP: bump dev-workflow to 0.12.0 with its changelog entry' -- 'plugins/dev-workflow/.claude-plugin/plugin.json' 'plugins/dev-workflow/CHANGELOG.md' \ + || { echo 'COMMIT FAILED' >&2; exit 1; } +git show --stat --format='%H %s' HEAD +git status --porcelain +``` + +This **is** the commit procedure above with its path literals and subject filled in — issued as **one** Bash call beginning with `git`. + +- [ ] **Step 7: Now run the positive comparison** + +Run: `sh scripts/check-version-bump.sh main` +Expected: exit 0. **This is the first point at which it can pass**, because the bump is now in +`HEAD`'s tree. + +**Use the same base the release decision used.** Step 1 resolved an integration revision and read +the manifest and changelog from it; if `main` and that revision have diverged since, pass the +resolved revision here instead of the bare `main` and say which was used. **Verifies a bump is +present, not that it is correct** — the script's own documented limit. + +--- + +### Task 5: Record the twelve-item review, run the battery, and demonstrate the counterfactual + +No product file changes. This task produces the written evidence the profile owes and the green +battery Gate B expects — and **names where each piece is retained**, because "the execution report" +is not a destination. + +**Files:** +- Write: `.context/codex-reviews/gate-b--resume.md` — the Gate-B cycle's working record, + which `CLAUDE.md` §5 already sanctions as an advisory companion. **The twelve-item review, the + walkthrough and the measured counterfactual values are retained there**, in the artifact the + Gate-B cycle already owns. No new artifact kind is introduced, and nothing about that record is + load-bearing: it is advisory, and the findings file plus its terminator remain the only hard + requirement. +- Commit body: **the story's named evidence entry**, which `CLAUDE.md` requires to live in the + commit body and to be restated by the closing amend. It is not a file. + +**Interfaces:** +- Consumes: everything Tasks 1–4 produced. +- Produces: the evidence entry the Gate-B closing commit carries. + +- [ ] **Step 1: Answer all twelve prompt-standards items for `claude-init.md`, in writing** + +Every item of `docs/prompt-standards.md`, answered with either the answer or a **reasoned `n/a`** in +the shape `workflow-init.md:190-197` uses. The three with a mechanical check (item 1 plus the two +narrow spellings) still get an answer; the other nine have **no mechanical check at all** and are +satisfied only by this review being done and recorded. Item 4 needs the output-format example from +Task 2 Step 3; item 11 needs every enforcement claim to name its mechanism and stop at what that +mechanism compares. + +**The same review covers the changed prompt text elsewhere in this change** — `Don't guess` as it +lands in `workflow-init.md`'s template and in this repository's own `CLAUDE.md`. + +- [ ] **Step 2: Run the semantic walkthrough and label it correctly** + +Read the command text against each case: a new target receives only `CLAUDE.md`; a second run adds +no duplicate; an overlapping file triggers the ask and no write; each of step one's five path kinds +stops where it should; the create branch stops where no qualifying operation exists. + +Also trace the attempted-create failures from spec §2: `EEXIST` refusal; an open error before +creation (`EACCES`, `EROFS`, `ENOSPC`, and an error outside those examples); successful open followed +by failure with zero bytes written; and a partial write. In each case, locate the instruction and +report example that name the failure and the observed path state afterwards, or the inability to +inspect it. Confirm none reports completed creation or takes a weaker fallback. These cases check +the general failure rule as well as its refusal/partial-write examples; they do not execute writes. + +**A walkthrough is evidence about the text, not an execution.** This repository has no harness that +runs a command against a fixture project. Any report saying otherwise is false. Also read the +**semantic** questions rows 2 and 3 cannot reach: does the template impose any gate obligation in +any wording, and does it point at any file this command does not write. + +- [ ] **Step 3: Demonstrate the counterfactual, measured rather than asserted** + +Run the six spelling greps from Task 2 Step 6 against **`workflow-init.md`'s template** instead. + +Expected: `7, 12, 3, 2, 7, 1` where the new template returns `0` for each. These are +**occurrence** counts — the probe uses `body.count` — measured against the current source; the first +is `7` because one line carries `Gate A` twice. **Measure these; do not +copy the numbers from the spec** — §7 records the sixth as `10`, which is a standing collected Minor +(pass 4, finding 11), and the measured value is `1`. Row 4a's expectation is built from a source +that must be transformed to match, so an untransformed template fails it. Row 9 goes red on any file +lacking the marker; `process-pr-review.md` is a live example. + +**Row 4b has no counterfactual and is not offered as one** — it compares two files that will both be +correct or both be wrong. + +- [ ] **Step 4: Run the full quality battery** + +Run the whole chained command from `AGENTS.md` § Commands, verbatim. It needs `shellcheck`, the +`claude` CLI and **`dash`** — the hook suite runs twice, once with the hook under `sh` and once +under `dash`, because Ubuntu's `/bin/sh` is dash and a dash-only defect shipped once already. + +Expected: exit 0. Nothing in this change is executable, so this is **tested coverage, not "nothing +broke"**. + +- [ ] **Step 5: Retain the records and carry the evidence in a commit body** + +Write the twelve-item review, the walkthrough and the measured values into the Gate-B cycle's +working record. Then account for what actually changed, and pick the route that matches: + +```sh +git status --porcelain # what, if anything, is outstanding +``` + +Classify outstanding changes as **task-owned or unrelated**, inspecting both index and worktree +diffs as well as untracked paths. Record their paths and why any task-owned changes remain. Leave +unrelated changes, including their staged state, untouched. If ownership is uncertain or a path +mixes task-owned and unrelated edits, stop and surface that overlap before staging or committing. +The routes below select whole paths; they cannot isolate ownership within one path. + +**Both routes carry the evidence through a second `-m`, never `-F`.** `-F` is what broke the +hook's WIP recognition; the commit procedure above states the predicate and the consequence. The +subject stays `WIP: record the battery run and the evidence entry`; `$BODY` is the story's named +evidence entry. + +- **No task-owned changes remain** — the expected case, since Tasks 1–4 committed their own files + and the working record is under `.context/`, which is git-ignored. Record the evidence with an + empty commit, which changes no content and therefore raises no review obligation: + + ```sh + git commit --only --allow-empty -m 'WIP: record the battery run and the evidence entry' -m "$BODY" \ + || { echo "COMMIT FAILED" >&2; exit 1; } + git show --stat --format='%H %s' HEAD + ``` + + **The `SUBJECT=` assignment is gone on purpose.** A Bash call that opens with a variable + assignment does not begin with `git`, and the `jq`-free parser then fails the `is_commit` anchor + and never sees the commit at all — the earlier spelling was protected only by that blindness. The + subject is inlined and single-quoted; `-m "$BODY"` stays double-quoted because it follows the + recognised argument. `$BODY` must be exported into this call, not assigned ahead of the `git`. + + `--only --allow-empty` **without paths** creates an empty commit even when unrelated changes are + staged, so this route cannot capture them. It is the only route that skips the shared procedure's + `add`, because there is nothing to stage. + +- **Task-owned changes remain** — finish and validate those changes first, then run **the commit + procedure** above with the reviewed task-owned paths as single-quoted literals and the same + subject, adding the `-m "$BODY"` argument after the subject. This route commits content, so it owes a content + review under the existing Gate-B rules; the empty-commit rationale does not apply. + + **Then rerun the committed-tree checks — this is the ordering the earlier text got wrong.** + Reruning affected checks *before* this commit does not cover the tree it creates: + `scripts/check-version-bump.sh` reads `HEAD`, so a task-owned manifest edit could pass a + pre-commit battery and land in `HEAD` with no version observation of the resulting tree. After the + commit: + + ```sh + sh scripts/check-version-bump.sh main # or the resolved integration revision from Task 4 + ``` + + and rerun any other check that reads committed trees. **Then refresh the evidence entry** to + describe the tree that now exists, and amend it into this commit with + `git commit --amend --only -m 'WIP: record the battery run and the evidence entry' -m "$BODY"` — + the call begins with `git`, the WIP argument is single-quoted, and the body follows it, per the + three shape rules in the commit procedure — before Gate B is called. An evidence body describing a pre-commit state is not evidence + about the reviewed range. + + **`--only` is load-bearing on the amend, and its absence was a real hole.** A plain + `git commit --amend` commits the **current index**, which still holds every unrelated staged path. + Measured in a throwaway repository: after a correctly scoped first commit containing only + `owned.txt`, a plain amend produced a commit containing `foreign.txt` **as well**, and the tree + changed from `8071d7d8…` to `b6bc185a…`. With `--amend --only` and no paths, the tree was + **byte-identical** and the unrelated path stayed staged. Amending without it would re-cross the + ownership boundary **and** invalidate the committed-tree checks just run against that tree. + +On either route, a failed commit stops this step; do not call Gate B with a missing evidence body +or task changes outside the committed range. After success, inspect `git show --format=full --stat +HEAD` and `git status --porcelain`: confirm the evidence body is present, the commit contains only +the intended task changes (none on the empty route), and no task-owned change remains uncommitted. +The stateful sequence is pending execution; this plan does not claim it has been run. + +**The closing amend restates the evidence entry.** The amend replaces the WIP message wholesale, so +an entry written only into a WIP body is destroyed exactly when the cycle closes. The final commit +body is the durable record. + +**None of this is executed by the planning assignment** — these commands describe the execution +stage. + +--- + +## What this plan does not do + +- **It does not run either gate.** This plan owes a Gate-A plan cycle before execution; Gate B runs + after Task 5, at the floor derived from the story's profile. +- **It does not repair the 15 collected Minor and Nit findings** from the spec cycle. They are + reference material. Two of them are load-bearing here and are handled inside tasks rather than as + repairs: the `os.open` mode argument (Task 2 Step 3) and the measured sixth spelling count (Task 5 + Step 3). Everything else stays collected. +- **It does not edit the spec or the story.** If a task cannot proceed without changing an approved + decision, that is a stop-and-surface, not an edit. +- **It does not touch `loop-rule-consolidation`.** +- **It adds no new checker, synchronization layer or gate machinery.** The two scratch scripts live + in `/tmp` and ship nothing. + +## Accounting — what the pass-1 repair kept, moved and dropped + +`AGENTS.md`: *"Never replace a decision procedure without accounting for its old conditions."* Seven +procedures were replaced. Every condition each carried is listed. + +| Old condition | Fate | +|---|---| +| Task 4: run the version checker once, after editing the manifest | **Split.** The negative run is **kept**, moved to after Tasks 1–3's commits; the positive run is **moved** to after the bump commit, because the checker reads committed trees. Nothing about what it verifies changed. | +| Task 4: "it compares *commits*, so run it with the WIP commits already made" | **Kept and made load-bearing** — it was already true and is now the stated reason for the ordering, with the checker's own lines cited. | +| Task 4: `head -20 CHANGELOG.md \| grep '^## '` | **Dropped.** Measured: no match, the first heading is at line 25. Replaced by `grep -m1 -E '^## '` with no cutoff, read from the resolved integration revision, with read failure, absent heading and version conflict kept distinguishable. | +| Task 4: read the manifest from `origin/main`, the changelog from the working tree | **Dropped.** Both now come from one resolved revision, so they cannot describe two states. | +| Task 2: one check, run after the template is written, expected to FAIL beforehand | **Dropped as unreachable.** The script raised `FileNotFoundError` on the prescribed pre-template state. Replaced by an explicit `actual` input and three ordered runs — negative, 4a, then 4b — so the negative case reaches the comparison. | +| Task 2: "both comparisons use the post-change source, and that is deliberate" | **Dropped, and the reasoning with it.** Spec §7 row 4a requires the template *"as it stands before this change"*. Row 4a now derives from an identified pre-Task-1 commit and runs separately from 4b. The earlier collapse changed an approved verification contract. | +| Task 2: the transformation's two deletions and the §6 no-op | **Kept, verbatim**, and still reproduce spec §5's template byte for byte. What changed is which source each row applies them to, not the operations. | +| Task 2: five report states | **Kept**, plus **two added** — `stopped: create refused` and `failed: partial write` — which spec §2 requires with *"A failed write is an outcome, not silence"*. | +| Task 1 Step 6: two changed paths and no removed lines | **Dropped as an overclaim.** It is satisfied by arbitrary extra insertions, while its title claimed "nothing else changed". Replaced by reconstruction: remove the authorized block, compare the remainder with the file's pre-task bytes, and check the block's placement. | +| Task 5: "Modify: none", results in "the execution report" | **Dropped.** The review and measured values now go to the Gate-B cycle's working record — an artifact `CLAUDE.md` §5 already sanctions — and the named evidence to the commit body, where `CLAUDE.md` puts it. | +| Task 5: `git add -A` | **Dropped.** Replaced by two named cases: an empty records commit when nothing is outstanding, or staging by path. Broad staging in a worktree that may hold unrelated work is the hazard. | +| The five-task structure, the task boundaries, the criterion coverage, the no-duplicated-normative-text rule | **Kept, untouched.** Pass 1 raised no Major against any of them. | + +**Scope of this repair, stated so it is not read as more.** Only the seven Majors. The eleven Minor +and four Nit findings stay collected and were not swept. **Two of them touch mechanisms these +repairs necessarily rewrote**, and the dependency is named rather than used to claim the class is +solved: the comparison scripts' text-mode reads and trailing-newline stripping are now **described +accurately** where they run — the checks establish agreement of decoded text, not byte identity of +files — and the parity extractor's scoping is unchanged and still collected. + +## Accounting — what the pass-2 repair kept, moved and dropped + +The pass-1 accounting above records that earlier repair. This round replaces three procedures: + +| Previous condition | Fate | +|---|---| +| Task 2 Step 3: examples for five original states, `EEXIST` refusal and partial write; success observed; directory-entry limit; failure cause and observed state | **Kept.** Added a catch-all failed-create state, covering failures before creation and after open with zero bytes; unsuccessful attempts report open outcome and any inability to inspect the path. The closed seven-state coverage and the claim that a failed write necessarily leaves a partial file are **dropped**. The qualifying-operation requirement stays. | +| Task 5 Step 2: new target, repeat, overlap, five path kinds, unavailable operation, semantic gate/reference questions; text-only evidence | **Kept.** Added refusal, generic open error, zero-byte and partial-write cases and their reports. No execution claim or fallback is added. | +| Task 5 Step 5: working-record destination, evidence body, WIP subject, empty route, staging by name, path report, closing-amend carry | **Kept.** The branch condition changes from any outstanding work to task ownership, and the owned branch now commits before Gate B. Broad staging remains excluded; `--only` excludes unrelated staged paths. Mixed/uncertain ownership stops. Evidence is refreshed when its inputs change; commit failure and post-commit verification are explicit. | +| Task 4 Step 1: fetch origin, resolve once, read manifest and changelog at that revision, distinguish read/heading/conflict outcomes, inspect open PRs and stop on movement | **Kept.** The refspec explicitly refreshes `origin/main`, fetch failure stops before resolution, and the ref must resolve to a commit. Acceptance of a cached ref after fetch failure is **dropped**; the later checks and release-decision conditions stay. | + +Only the three Majors from pass 2 are repaired here. Its 17 Minor and 7 Nit findings remain +collected. The failure reports and walkthrough overlap the reporting/coverage mechanisms named +by Minors, but this round adds only the Major's failed-create cases; it does not claim those +broader findings resolved. + +## Accounting — what the pass-3 repair kept and dropped + +`AGENTS.md`: *"Never replace a decision procedure without accounting for its old conditions."* + +| Old condition | Fate | +|---|---| +| Five per-task commit spellings, each written separately | **Replaced by one shared procedure.** The drift between them was where three of the cycle's Majors lived. Each task now supplies only its `set --` line and `SUBJECT`. | +| Tasks 1–4: `git add ` then `git commit -m …` on separate unguarded lines | **Dropped.** Reproduced under `sh` and `dash`: a failed `add` was still followed by the commit. Replaced by `\|\| { …; exit 1; }` after the stage. This defect predates every repair round — it was in the plan's first version. | +| Tasks 1–4: plain `git commit`, which includes everything already staged | **Dropped.** Replaced by `--only -- "$@"`, tested with an unrelated path staged: it stayed staged and out of the commit. The pass-2 repair applied ownership handling to Task 5 only; this extends it to every path. | +| Task 5: `-F /tmp/evidence-msg.txt` on both routes | **Dropped.** `is_wip_commit()` matches only a literal `-m …wip`, so `-F` read as a real commit and the hook would clear the Gate-B fingerprint and counters. Replaced by a second `-m` carrying the body, which keeps the recognised first argument. **This spelling entered in the pass-1 repair round, not the pass-2 one** — the later round propagated it to the second route. | +| Task 5 owned route: rerun affected checks, then commit, then `show --stat` and `status` | **Re-ordered.** `check-version-bump.sh` reads `HEAD`, so committed-tree checks now run **after** the commit, followed by refreshing the evidence body and amending it in. A pre-commit pass is not an observation of the tree the commit creates. | +| Task 5 empty route: `--only --allow-empty` without paths | **Kept**, and re-tested: it commits nothing even with unrelated paths staged. Only its message spelling changed. | +| "Quote each path and use literal pathspecs" | **Kept**, moved into the shared procedure as `--literal-pathspecs`. | +| The mixed-ownership stop | **Kept**, and its reason sharpened: `--only` selects whole paths and cannot split one, so no flag resolves the case. | +| Shared procedure: unquoted `$PATHS` with `--literal-pathspecs` credited for protection | **Dropped as an overclaim.** The shell splits and globs before git runs; measured, `*.txt` reached git as two arguments under `sh` and `dash`. Replaced by `set -- "…"` and `"$@"`, with `--literal-pathspecs` kept for the separate job it does do. | +| Task 5 owned route: `git commit --amend` for the refreshed evidence | **Dropped.** A plain amend commits the current index, so unrelated staged paths re-enter. Measured: the tree moved from `8071d7d8…` to `b6bc185a…`. Replaced by `--amend --only`, which left the tree byte-identical. | +| The five-task structure, task boundaries, criterion coverage, the transformation, the derivation checks | **Kept, untouched.** Pass 3 raised no Major against any of them. | + +**Scope:** the four Majors of pass 3 only. The twenty Minor and seven Nit findings stay collected +and were not swept. + +## Accounting — what the pass-4 repair kept, moved and dropped + +`AGENTS.md`: *"Never replace a decision procedure without accounting for its old conditions."* + +| Old condition | Fate | +|---|---| +| `set -- "" …` with `"$@"` on the `add` and the `commit` | **Moved.** The shell-level protection it gave — no word-splitting, no globbing — is unchanged, now carried by single-quoted path literals. It was replaced only because its double quotes truncate the hook's `jq`-free parse. The cost is repeating each path twice per task, and it is paid deliberately. | +| `SUBJECT=""` as a separate assignment | **Dropped.** A call opening with an assignment does not begin with `git`, so `is_commit`'s anchor fails without `jq` and the commit is never seen. The subject is inlined into the `-m` argument. | +| The explanatory comments inside the procedure block | **Moved** into the prose below it, unchanged in substance. A leading comment breaks the `^git` anchor, and a comment containing `"` truncates the parse — the block's old first line did both. | +| `-m "WIP: …"` (double-quoted) | **Dropped.** Measured end to end through the hook, without `jq`: the parse truncates at that quote, the remainder is still read as a commit but no longer as a WIP commit, and `gateB`, `passCount` and `freshCount` are deleted. Replaced by `-m 'WIP: …'`, which is recognised identically with and without `jq`. | +| "The `-m` beginning with `WIP:` is what the hook recognises", evidenced by testing `is_wip_commit` directly | **Kept as a claim, replaced as evidence.** The predicate test was one layer too shallow: a parser stands in front of it. The claim now rests on runs of the real hook under `sh` and `dash`, with and without `jq`. | +| A second `-m` for the body, double-quoted | **Kept.** Double quotes *after* the recognised argument change nothing — verified in both configurations. | +| `--only -- `, the `\|\|` guard, `--literal-pathspecs`, the two `git status --porcelain` inspections, the mixed-ownership stop | **Kept, untouched.** Pass 4 raised no Major against any of them, and none depends on the quoting change. | +| Cycle closing stated as "collapsed with `git reset --soft` and replaced by one real commit" | **Replaced by an explicit, scoped procedure.** A soft reset leaves the index intact, so a plain closing commit consumed every unrelated staged entry the five tasks had preserved — measured under `sh` and `dash`. The closing commit now names its paths with `--only` and is checked by comparing the committed tree against the reviewed tree. | +| The end-to-end probe as sufficient evidence for the commit handling | **Kept and extended.** It ran the tasks and the amend with `jq` present, and stopped there. It now runs to the **cycle-closing commit** and in the **`jq`-free** configuration, which is where both of pass 4's Majors lived. | +| The five-task structure, task boundaries, criterion coverage, the transformation, the derivation checks | **Kept, untouched.** Pass 4 raised no Major against any of them. | + +**Scope:** the two Majors of pass 4 only. The twenty-one Minor and seven Nit findings stay +collected and were not swept. **The hook itself was not touched** — the parser defect is worked +around here and remains open. + +## Accounting — what the pass-5 repair kept, moved and dropped + +`AGENTS.md`: *"Never replace a decision procedure without accounting for its old conditions."* + +| Old condition | Fate | +|---|---| +| Closing block: first reading `git rev-parse 'HEAD^{tree}'` taken at closing time, described as "the tree Gate B reviewed" | **Dropped as an overclaim.** It read whatever `HEAD` was at closing, so a WIP commit landing after the final pass became the "reviewed" tree. Replaced by `git diff --quiet '' HEAD`, pinned to the `headSha` kept with the final clean pass. | +| Closing block: `git reset --soft ''`, resolved before the cycle | **Moved.** The base is now the `baseSha` kept with the final clean pass — the same value, taken from the record the pass already owes rather than a second note. | +| Closing block: reset, commit and inspections as unguarded lines | **Dropped.** Each mutation now stops the block on failure and says what state it left. Measured under `sh` and `dash`: late WIP commit, unresolvable base and incomplete path list each exit 1 at the intended step. | +| Closing block: the second tree reading compared by eye with the first | **Replaced** by `git diff --quiet '' HEAD` after the commit, which fails the block rather than relying on a reader. | +| Closing block: `--only -- ''`, no `"` anywhere, begins with `git`, no `wip` subject | **Kept.** Re-verified through the hook in all four configurations. | +| The prose "they diverge exactly when the path list is incomplete or a worktree edit arrived" | **Dropped as an overclaim.** Tree inequality does not identify its cause. The failure list now names the usual cause without claiming equivalence. | +| End-to-end table: one run including the empty evidence commit **and** the amend, reported green | **Dropped as false.** The amend in that run had failed and its exit status was not checked. Replaced by one run per Task 5 route, every exit status checked. | + +**Scope:** the two Majors of pass 5, plus the end-to-end table, whose "verbatim" claim the closing +block's rewrite made false and which turned out to be false already. The twenty-seven Minor and ten +Nit findings of pass 5 stay collected. + +## Accounting — what the pass-6 repair kept, moved and dropped + +`AGENTS.md`: *"Never replace a decision procedure without accounting for its old conditions."* +**This round deletes a mechanism rather than repairing it a third time.** The closing block written +in the pass-4 round and rewritten in the pass-5 round drew both Majors of pass 5 and both of pass 6. +It is replaced by `CLAUDE.md` §5 Mechanics' own closing procedure plus one guard. + +| Old condition | Fate | +|---|---| +| Closing commit scoped with `--only -- ''` | **Dropped.** It committed the named paths' **worktree** content rather than the reviewed content, rewrote their index entries so a soft reset no longer restored them, and made a path-list error indistinguishable from new content. Replaced by a plain `git commit`, which commits the index, behind a guard that the index equals the reviewed tree. | +| Reset target: the final pass's `baseSha` (pass-5 round), described as "the same value" as the first WIP's parent | **Dropped as false.** Gate B may run against the merge-base with `main`; on this branch the approved spec and story commit sits above it, and resetting there would fold that closed Gate-A commit and its records into the closing commit. Reverted to the parent of the first WIP commit, resolved before the first task commit — `CLAUDE.md`'s own wording. | +| Pre-close check `git diff --quiet '' HEAD` (committed trees only) | **Replaced** by `git diff --cached --quiet ''` — the index against the reviewed tree — because the index is what `git commit` records. It covers everything the old check did and also staged changes after the pass and unrelated staged work. | +| Unrelated staged work preserved **through** the close | **Dropped, deliberately.** The close now stops on it and hands it to the user; unstaging it is not done by the agent, because staged and worktree content can differ. The tasks still keep such work out of their own commits, unchanged. | +| Post-close `git diff --quiet '' HEAD`; every mutation guarded; `git rev-parse --verify 'HEAD^'`; no `"` in the block; begins with `git`; no `wip` subject | **Kept.** Re-verified: six failure and success scenarios under `sh` and `dash`, and the hook in all four configurations. | +| Recovery `git reset --soft ` after a failed commit | **Kept, and now true.** With no `--only`, a failed commit leaves the index untouched, so the soft reset restores the pre-close state exactly. | +| End-to-end table asserting the unrelated path stayed staged **through** a successful close | **Replaced** by two runs per route — index clean, and unrelated path staged — matching the new behaviour. | + +**Scope:** the two Majors of pass 6. The twenty-six Minor and nine Nit findings stay collected. + +## Accounting — what the pass-7 repair kept, moved and dropped + +| Old condition | Fate | +|---|---| +| Pass-6 claim: the index guard "covers everything the old check did" | **Dropped as false.** A late WIP followed by restoring only the index passed it. The `HEAD`-tree guard from the pass-5 round is **restored** beside the index guard; each covers a state the other does not. | +| Pass-6 claim: after a failed commit `git reset --soft ` "restores the pre-close state exactly" | **Dropped as false.** A pre-commit hook can stage changes and then fail. No fixed recovery command is promised; the instruction is to stop, inspect and surface. | +| The "final comparison should be unreachable" sentence | **Dropped** with the claim it supported; the post-close comparison itself is **kept**. | +| Canonical close, index guard, parent of the first WIP as reset target, no `"`, begins with `git` | **Kept.** Re-verified in eight scenarios under `sh` and `dash`, and through the hook in all four configurations. | + +**Scope:** the two Majors of pass 7. The thirty Minor and ten Nit findings stay collected. + +## Accounting — what the pass-8 repair changed + +| Old condition | Fate | +|---|---| +| Task 2 Step 3: the success report token `created` | **Dropped — it contradicted the approved spec.** Spec §2 reports `written` throughout (`:92`, `:99`, `:230`). Reported by Codex as a Minor across passes; **upgraded to Major** under `CLAUDE.md`'s severity test, because the executor consumes this list and would author the command with the wrong output. Both occurrences now read `written`. | + +**Scope:** that one finding. Pass 8 raised no Blocker and no Major of its own; the other thirty-one +Minor and ten Nit findings stay collected. + +## Accounting — what the pass-9 repair kept, moved and dropped + +| Old condition | Fate | +|---|---| +| `dg-parity.py`, `only-insert.py`, `tmpl-derive.py`: text-mode reads (`io.open` without `newline=""`, `git show` with `text=True`) | **Dropped.** They translated CRLF to LF, so a copy with different line endings compared equal while story criterion 5 and spec §7 rows 1, 4a and 4b require byte equality. Measured: each old script reported **OK** on a CRLF copy; each patched script reports **FAIL**. | +| `tmpl-derive.py` `norm()`: `.rstrip("\n")` on both sides | **Dropped.** It hid a differing number of trailing blank lines inside a template. Measured: an extra blank line before the §5 closing fence — old **OK**, new **FAIL**. | +| The prose describing these checks as "decoded text, not byte identity" | **Replaced.** That prose was accurate about the old scripts and is the reason pass 9 upgraded this: describing a weaker check accurately does not discharge a byte-equality requirement. The prose now states what the patched reads establish. | +| `85 lines, 3708 bytes` | **Kept, and its unit defined**: the payload between the fences joined with LF, without the LF before the closing fence (3709 with it). | +| Block definitions — the `Don't guess` block ends at the blank line after its last paragraph; trailing blank lines are popped from the §4 source block before insertion | **Kept.** They define the unit being compared, applied identically to every copy; they are not a loosening of the comparison. | +| Positive results on the real tree | **Re-run with the patched scripts:** `tmpl-derive.py negative` two `FAIL` and exit 0; `4a` `OK` exit 0; `only-insert.py` and `dg-parity.py` `OK` on a correct LF insertion. | + +**Scope:** the one Major of pass 9. Its thirty Minor and eleven Nit findings stay collected. + +## Accounting — what the pass-10 repair kept, moved and dropped + +Pass 10 returned **nine** Majors, all of them findings collected as Minors in earlier passes and +re-rated after the gate prompt, from pass 9 on, stated `CLAUDE.md`'s instrument carve-out explicitly +("a check that can report success against a requirement it does not meet is not a Minor"). **None is +in text a previous repair wrote.** Each was reproduced before repair and re-run after it. + +| Old condition | Fate | +|---|---| +| Task 5 Step 3 expected `6, 12, 3, 2, 7, 1` | **Corrected to `7, …`**: the probe counts occurrences, and one line carries `Gate A` twice. | +| `dg-parity.py`: first `### Don't guess` heading anywhere | **Dropped.** Exactly one heading per file is now required; an explanatory copy ahead of the shipped block makes the check fail instead of selecting it. | +| `tmpl-derive.py`: the no-§6 predicate printed but not part of the result | **Dropped.** It now contributes to the exit status. | +| Task 3: `grep -c 'claude-init'` per file | **Dropped.** Replaced by three matches on the required entries — table row, Python 3 disclosure in it, exact tree line. | +| `only-insert.py`: `remove_once` plus a placement scan that skipped blank lines | **Deleted.** Replaced by one equality against the single expected file — pre-task bytes with the block inserted after the existing blank line and followed by exactly one. Missing or doubled separators now fail. | +| Rows 4a and 4b as the only link between the three templates, with prose claiming they establish agreement | **Extended and re-described.** Row 4b also compares the shipped template directly with spec §5; the prose names which comparison ties which pair, and that Task 1's check must be current. | +| Task 2 Step 1: `test ! -e` | **Extended** with `test ! -L`; a dangling symlink now stops it. | +| `tmpl-derive.py`: any unrecognised mode ran nothing and exited 0 | **Dropped.** Unknown modes exit 2 before any comparison. | +| Task 2 Step 6: `… \| grep claude-init` | **Dropped.** `grep -Fx` on the exact product path; the plan's and the spec's own mentions no longer satisfy it. | + +Verified: the three scripts as the plan now prints them — every positive case exit 0 on the real +tree or a correct construction, every counterfactual above non-zero; the three shell checks under +`sh` and `dash`, before, after and with an incidental mention. + +**Scope:** the nine Majors of pass 10. Its twenty-one Minor and eleven Nit findings stay collected. + +## Accounting — what the pass-11 repair kept, moved and dropped + +| Old condition | Fate | +|---|---| +| `dg-parity.py`'s result described as "N copies identical", and Step 7 as discharging criterion 5 | **Narrowed, not extended.** The check establishes that the single block in each file is identical, and says so; **position** is row 4b's. Adding template-scoped extraction was the alternative and was declined: each extraction rule added in this cycle drew the next counterexample. Criterion 5 is now stated as discharged by the two checks together. | +| Task 3 tree check `grep -c 'commands/{…,claude-init}.md'` (regex, unanchored) | **Dropped.** Replaced by `grep -cxF` over the whole line, plus a count of the old tree line, which must reach `0`. Measured under `sh` and `dash`: the new line only in prose, and a `}Xmd` typo, both now fail. | +| Task 4: no post-edit read of the changelog | **Added** to Step 5: newest heading equals the manifest version, and the entry names both changes. Measured: before `## 0.11.0 0 0`; complete entry `## 0.12.0 1 1`; incomplete entry `## 0.12.0 0 0`. | + +**Scope:** the three Majors of pass 11. Its twenty-two Minor and thirteen Nit findings stay collected. + +## Accounting — what the pass-12 repair kept, moved and dropped + +Both Majors of pass 12 were counterexamples to checks the pass-11 repair wrote. **The rule applied: +no further counting checks; compare against the one expected result**, the principle behind +`only-insert.py`, which has drawn no Major since it was introduced. + +| Old condition | Fate | +|---|---| +| Task 3: whole-line fixed-string counts for the new and old tree lines | **Dropped.** Replaced by a comparison of the whole working-tree diff of the two layout files with the single expected diff — one line replaced in place in each. Measured under `sh` and `dash`: in place OK; line moved after the fence, an extra edit, a misspelling, and no change all fail. | +| Task 4: `sed` range over `## 0.12.0` headings | **Dropped.** It included the next heading and restarted on duplicates. Replaced by an `awk` that prints only the newest entry's body, plus a count requiring the `## 0.12.0` heading exactly once. Measured: empty entry with names in the next heading → body counts `0`; names only in a later duplicate → heading count `2`, body counts `0`. | +| Task 3: README row and Python 3 disclosure matches | **Kept.** Pass 12 raised nothing against them. | + +**Scope:** the two Majors of pass 12. Its twenty-two Minor and thirteen Nit findings stay collected. + +## Accounting — what the pass-13 repair kept, moved and dropped + +**Method change, decided by Daniel on 2026-09-23 after pass 13:** each check claims only what its +command compares, and the remainder is carried by a **named reading check** — spec §7's own rule. +Byte comparisons the spec prescribes (rows 1, 4a, 4b) stay mechanical. The severity sentence the gate +prompt carried from pass 9 on is withdrawn: `CLAUDE.md`'s severity test sets a ceiling, not a floor. + +| Old condition | Fate | +|---|---| +| Task 3 tree block's claim "nothing else changed" | **Narrowed.** It compares the text diff only; the claim now says so, and names file-mode and metadata changes as outside it. | +| Task 3 README greps presented as matching "the required entry" | **Narrowed**, and placement in the component table **moved to a named reading check** in Step 5. | +| Task 4 `grep -c '^## 0\.12\.0$'` presented as "occurs exactly once" | **Dropped.** Replaced by printing every release heading with `grep -n '^## '` and a **named reading check** for uniqueness in any spelling. | +| Task 4 newest-heading and newest-entry-body checks | **Kept.** Pass 13 raised nothing against them. | + +**Scope:** the three Majors of pass 13, each disposed of individually. Its twenty-four Minor and +fourteen Nit findings stay collected. + +## Self-review against the spec + +**Story acceptance criteria, each to a task:** + +| # | Criterion | Task | +|---|---|---| +| 1 | command file exists, inline, no manifest key | 2 (Steps 2–4, 6) | +| 2 | a new target receives `CLAUDE.md` and nothing else, where the operation is available | 2 (Step 3) · walkthrough in 5 (Step 2) | +| 3 | a repeated invocation adds no duplicate | 2 (Step 3) · walkthrough in 5 (Step 2) | +| 4 | no gate obligation, no dangling reference in the template | 2 (Step 6 spellings) · 5 (Step 2 semantics) | +| 5 | `Don't guess` byte-identical in all three copies | 1 (Steps 1–5) · 2 (Step 7, with row 4b for position) | +| 6 | the three inventory sites name the command | 3 | +| 7 | version bumped, changelog entry | 4 | +| 8 | battery green, twelve items answered | 5 | + +**Spec sections, each to a task:** §1 → 2, 3, 4 · §2 → 2 · §3 → 2 (Step 4) · §4 → 1 · §5 → 2 +(Step 4) · §6 → 2 (Step 2), 5 (Step 1) · §7 → distributed, rows named where they run · §8 → the +out-of-scope list above. + +**Known gap, named rather than left to be discovered.** Spec §7 says nothing verifies the command's +**behaviour** in a target project, and this plan does not change that. Criteria 2 and 3 are answered +by a walkthrough of the text. Building a fixture harness would be a new mechanism and is out of +scope; if that trade is ever unacceptable, it is a scope decision to surface, not a step to add. From c60f52e5ab4433d6a1bc25f06a2b31522dcdb848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:32:15 +0200 Subject: [PATCH 3/4] feat(commands): add /dev-workflow:claude-init and the Don't guess rule (0.12.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /dev-workflow:claude-init writes one CLAUDE.md of general working rules into a project and nothing else: no review gates, no marker, no AGENTS.md, no commit, no git repository required. It classifies the path before reading content, never writes over an existing CLAUDE.md (equivalent rules report unchanged; different ones get a proposal and a stop), and creates only through an exclusive O_CREAT|O_EXCL open in an already-present Python 3, reporting eight observed states. Its inline template is spec §5, derived from workflow-init's template. The Don't guess rule is added at the end of section 1 in three byte-identical copies: this repo's CLAUDE.md, workflow-init's template and claude-init's template. README's component table and both layout trees name the command; dev-workflow moves to 0.12.0 with a changelog entry. Gate B: 3 valid logical passes, final clean. Pass 1 raised 3 Majors, two distinct issues (the first was reported by both branches) — the create snippet let close and read-back errors escape without a report state (repaired; a failure before the script prints anything is now covered too), and the prompt-standards item-1 check of the model's prompting page had not been done (done and recorded). Passes 2 and 3: no Blocker or Major. Each pass was one full-mode call carrying both branches, so the hook's call count equals the logical pass count (3). Collected, not iterated on: claude-init does not tell the user when the full workflow is already installed (it preserves it); only the written state has a rendered report example; the create-refused remedy promises the content branch for kinds step one refuses; Done means promises byte-for-byte preservation it does not verify; the dangling-symlink rationale overstates what the precheck guards; the new "failure before the script prints" rule labels an interrupted run as zero bytes written; MANIFEST.md's three-template enumeration and README.md's checklist-in-every-project sentence are now stale. Plan deviations, stated: this session was worktree-isolated and refused the plan's multi-line commit blocks, so each commit procedure ran as separate calls with every exit status checked before the next; the session shell's grep is a harness function, so plan shell checks were run under sh; Task 1's stated parity outputs were stale (both calls exit 1 with the current script), and the actual outputs were recorded instead. The closing commit takes its message from a file; that is safe here because it is the cycle-closing commit, not a WIP snapshot. cycle x7c71e162z; floor 3 per {docs/superpowers/stories/2026-09-17-claude-init-command-story.md (level 1)}; hook reminder threshold absent cycle x7c71e162z; Gate B (passes 1-3, gpt-6-astra): Findings 16,16,16. Blockers 0,0,0. Majors 3,0,0. Evidence: docs/superpowers/stories/2026-09-17-claude-init-command-story.md Battery: the full AGENTS.md § Commands chain green on the tree of this commit — shellcheck clean on six shell files; hook suite passed under sh and under dash; check-invariants.test.sh 148 assertions; check-invariants.sh ok; check-version-bump.test.sh 36 assertions; check-version-bump.sh main ok (main = origin/main = 7c0d475); claude plugin validate . --strict passed. Check — automated, each observed failing against the prior state before its change: - Spellings (spec §7 rows 2, 3): workflow-init template 7, 12, 3, 2, 7, 1; the new template 0 for each. - Derivation (row 4a) from the pre-change source 8b3867a: the untransformed source and the deletions-only source both differ from spec §5 (negative mode, exit 0); deletions plus the insertion are byte-identical to it. - Parity (row 1, criterion 5): before Task 1 the parity check failed on CLAUDE.md (no heading, exit 1); after, the block is identical in all three copies. Its position in the shipped template is carried by row 4b: shipped template byte-identical to spec §5 and to the post-change workflow-init template minus three omissions. - Scan membership (row 9): process-pr-review.md absent from the scan; claude-init.md present. - Version (row 6): check-version-bump.sh failed before the bump (still 0.11.0, exit 1), ok after it was committed. - Inventory (criterion 6): README row and tree lines absent before (0, 0, tree diff FAIL); after, the row is present and each tree line replaced in place; row placement read at README.md:26, inside the component table. Also run, labelled as what it is: the create snippet in claude-init.md executed as printed in throwaway directories — absent: written, 3705 bytes, mode 644; existing file, directory, FIFO, symlink, dangling symlink: refused with EEXIST, target untouched, no hang; read-only directory: failed: create write; umask 0444 applied to the Python process: all bytes written, then failed: partial write (read-back failed), exit 5, instead of an unreported crash; umask 0444 in the shell: the here-document cannot be created and the script never runs. This exercises the snippet, not the command; the walkthrough of the command text is recorded in the cycle working record. --- AGENTS.md | 2 +- CLAUDE.md | 20 ++ README.md | 1 + docs/architecture.md | 2 +- .../dev-workflow/.claude-plugin/plugin.json | 2 +- plugins/dev-workflow/CHANGELOG.md | 20 ++ plugins/dev-workflow/commands/claude-init.md | 268 ++++++++++++++++++ .../dev-workflow/commands/workflow-init.md | 20 ++ 8 files changed, 332 insertions(+), 3 deletions(-) create mode 100644 plugins/dev-workflow/commands/claude-init.md diff --git a/AGENTS.md b/AGENTS.md index 9702f1a..52bf462 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,7 +43,7 @@ plugins/dev-workflow/ CHANGELOG.md # every manifest version, newest first skills/{intake,harden-finding}/SKILL.md agents/finding-triage.md # read-only PR-comment checker (convention-loaded) - commands/{workflow-init,process-pr-review}.md + commands/{workflow-init,process-pr-review,claude-init}.md hooks/{hooks.json,codex-gate.sh,codex-gate.test.sh} hooks/fixtures/ # captured PostToolUse payloads the suite replays examples/ # ships, but never scaffolded — one stack's answers diff --git a/CLAUDE.md b/CLAUDE.md index 4458020..0a43981 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,6 +12,26 @@ Before implementing: - If a simpler approach exists, say so. Push back when warranted. - If something is unclear, stop. Name what's confusing. Ask. +### Don't guess + +Applies to factual claims in every answer, not only implementation. Confidence is not evidence. + +**Leave gaps visible.** Do not invent missing or ambiguous facts. State what is unknown and why. In extraction +tasks, leave unsupported fields blank where the format permits; otherwise use the format's defined missing-value +handling. + +**Separate evidence from inference.** Cite the relevant source for factual conclusions. Identify deductions and +assumptions as such, with their basis. For extraction tasks, label populated fields EXTRACTED or INFERRED and +explain each inference where the required output format permits. If neither annotations nor accompanying +explanations are permitted, preserve the required format. This does not permit inventing unsupported values. + +**Keep decisions distinct from facts.** Make reasonable design and implementation choices within the authorized +scope, describing them as choices rather than source facts. Ask when missing information changes correctness or +scope. + +**Verify before claiming.** Report a test or action as completed only when its result was observed. Preserve +required output formats; put explanations outside structured artifacts where permitted. + ## 2. Simplicity First **Minimum code that solves the problem. Nothing speculative.** diff --git a/README.md b/README.md index f4e3103..ef38e69 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Why each of these, and how to adapt them: [`docs/coding-workflow.md`](docs/codin | `/dev-workflow:process-pr-review` | command — validates PR bot comments against the code and your invariants, replies to each, fixes regressions, tracks pre-existing issues. | | `dev-workflow:finding-triage` | agent — read-only, fresh context, judges whether one PR-bot claim is actually true of the code. Used by the PR processor; never counts as a review gate. | | `/dev-workflow:workflow-init` | command — scaffolds the per-project files, then interviews you to write `AGENTS.md`. | +| `/dev-workflow:claude-init` | command — writes one `CLAUDE.md` of general working rules and nothing else: no review gates, no other files. An existing `CLAUDE.md` is never overwritten; you get a proposal instead. Creating the file needs an already-installed Python 3. | | codex-gate hook | non-blocking reminders that count Gate A and Gate B passes, and verify a Gate-B review against a fingerprint of the effective index plus the included worktree content, as of the hook's invocation — a deliberate superset of any one commit's payload, so the gate errs toward firing. Always exits 0. | ## Setup diff --git a/docs/architecture.md b/docs/architecture.md index d896fbb..6d6e23b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -22,7 +22,7 @@ plugins/dev-workflow/ CHANGELOG.md # every manifest version, newest first skills/{intake,harden-finding}/SKILL.md agents/finding-triage.md - commands/{workflow-init,process-pr-review}.md + commands/{workflow-init,process-pr-review,claude-init}.md hooks/{hooks.json,codex-gate.sh,codex-gate.test.sh} hooks/fixtures/ # captured PostToolUse payloads the suite replays examples/ # ships, but never scaffolded — one stack's answers diff --git a/plugins/dev-workflow/.claude-plugin/plugin.json b/plugins/dev-workflow/.claude-plugin/plugin.json index 5599cbd..7251d72 100644 --- a/plugins/dev-workflow/.claude-plugin/plugin.json +++ b/plugins/dev-workflow/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "dev-workflow", "displayName": "Cross-Model Review Workflow", - "version": "0.11.0", + "version": "0.12.0", "description": "Spec-driven workflow with two independent cross-model review gates, an append-only hardening ledger with an escalation ladder, and repo-enforced quality. Requires the superpowers plugin.", "author": { "name": "Daniel Sänger", diff --git a/plugins/dev-workflow/CHANGELOG.md b/plugins/dev-workflow/CHANGELOG.md index 526f194..20734d9 100644 --- a/plugins/dev-workflow/CHANGELOG.md +++ b/plugins/dev-workflow/CHANGELOG.md @@ -22,6 +22,26 @@ unambiguously, still fails. Deleting only a plugin's *manifest* while the direct keeps shipping fails too. AGENTS.md invariant 12 carries the complete list. +## 0.12.0 + +- **New command `/dev-workflow:claude-init`** writes one `CLAUDE.md` of general working rules + into a project and nothing else — no review gates, no marker, no `AGENTS.md`, no commit, no git + repository required. The lighter entry point beside `/dev-workflow:workflow-init`, never an exit + from it: gates already present in a project's `CLAUDE.md` stay untouched. + - It classifies the path before reading content and stops on a symlink (working or dangling), a + directory or other non-regular file, or an unreadable file, reporting which one it saw. + - It never writes over an existing `CLAUDE.md`. Equivalent rules report `unchanged`; different or + overlapping rules get a focused proposed diff, and the command stops whether or not the proposal + is approved. + - It creates the file only through an exclusive open (`O_CREAT | O_EXCL`, mode `0o644`), which + refuses any existing destination. That needs an already-installed Python 3; without one it stops + instead of writing some weaker way. A failed or partial write is reported as such, never as + `written`. +- **`Don't guess`**, a new rule at the end of section 1, now ships in `/dev-workflow:workflow-init`'s + `CLAUDE.md` template as well as in `claude-init`'s: leave gaps visible, separate evidence from + inference, keep decisions distinct from facts, and report an action as done only when its result + was observed. + ## 0.11.0 - **The mandatory pass floor is now a function of the cited story's profile**, not the constant 3. diff --git a/plugins/dev-workflow/commands/claude-init.md b/plugins/dev-workflow/commands/claude-init.md new file mode 100644 index 0000000..95b0d14 --- /dev/null +++ b/plugins/dev-workflow/commands/claude-init.md @@ -0,0 +1,268 @@ +--- +description: Write one CLAUDE.md of general working rules into a project — no review gates, no other files +--- + +Write a `CLAUDE.md` of general working rules into the current project: think before coding, +simplicity, surgical changes, goal-driven execution, and not guessing. This is the lighter entry +point beside `/dev-workflow:workflow-init` — it installs no review gates and writes no other file. + +Target model: Claude via Claude Code. This command is a prompt artifact and follows +the dev-workflow prompt checklist (`docs/prompt-standards.md` in the dev-workflow-kit repository). + +## Context + +- **The only file this command writes is `CLAUDE.md`**, in the current directory. No marker, no + README note, no `AGENTS.md`, no `.context/`, no commit — the user asked for one file, and every + extra file is something they did not ask to own. +- **It needs nothing a directory does not have, with one exception.** No git repository, no plugin, + no MCP server, no package manager. Reading and proposing work anywhere. **Creating** the file needs + a Python 3 that is already installed, because that is where the one safe create operation lives + (see *Writing*). Where it is missing, the command stops instead of writing some weaker way. +- **It is an entry point, never an exit.** Where a `CLAUDE.md` already carries review gates — a + `Cross-Model Review` section, Gate A / Gate B — they stay exactly as they are. This command never + removes, disables, weakens or renumbers a gate, because a project that has gates chose them. + +## Done means + +- Exactly **one** report state from *Report* below was printed, and it describes what you + **observed**, not what you expected. +- At most one file changed on disk: `CLAUDE.md`, and only when it did not exist before. +- An existing `CLAUDE.md` is byte-for-byte what it was before the command ran. + +## Step one — what kind of path is `CLAUDE.md`? + +Ask this before looking at any content: a path that cannot be classified safely must never reach a +content decision. Run: + +```sh +ls -ld CLAUDE.md +test -L CLAUDE.md && echo symlink +test -r CLAUDE.md && echo readable +``` + +Test for a symlink **first**. `test -e` and `test -f` follow links, so a dangling link would read as +"absent" and the create would write through it into another place. + +| What you observe | Do this | +|---|---| +| Nothing at the path — `ls` reports no such file, and it is not a symlink | Go to step two, **absent**. | +| A regular file you can read | Go to step two, **present**. | +| A symlink, working or dangling | Stop. Write nothing. Report the path and where it points. | +| A directory, or any other non-regular file | Stop. Write nothing. Report what `ls -ld` showed. | +| A file you cannot read | Stop. Write nothing. Report the read error. | + +Each stop is reported as its own row, because each has a different fix (see *Report*). + +## Step two — what does it contain? + +| Found | Do this | +|---|---| +| **Absent** | Create it — see *Writing*. | +| **Present, and its rules say the same as the template** | Report `unchanged`. Add nothing, so a second run never duplicates a rule. | +| **Present, with different or partly overlapping rules** | Show a **focused proposed diff** — only the sections that differ — and ask. Whatever the answer, hand the proposed result back and **stop without writing**. | + +**Judge equivalence by reading.** Rules that say the same thing in other words are equivalent; say +which sections you matched with what. There is no checksum behind this judgement, so state it as +yours. + +**A proposal keeps everything a careful edit would keep.** Unrelated project content stays. Existing +mandatory rules and any review gates stay, unchanged and in place — the proposal only adds or aligns +the general rules. The user applying it themselves does not make a harmful proposal acceptable. + +**Approval opens no write path.** This command never writes over an existing `CLAUDE.md`, because +the person being asked may be editing that same file while they read the proposal, and nothing +available here can detect that in time. After approval, report `proposal produced` and stop; never +describe the proposal as applied, and never write it through any other tool. + +## Writing — the create branch only + +1. **Fill in ``** in the template's first line with the current directory's name, unless the + user named the project. +2. **Re-run step one immediately before creating.** If anything is now at the path, report what + appeared as `stopped: create refused` and stop. This gives a better report; it is not the guard. +3. **Check for Python 3:** `command -v python3 && python3 --version`. No `python3`, or a version + below 3 → report `stopped: no qualifying create operation` and stop. Do not fall back to a shell + redirect or your file-writing tool: neither refuses an existing destination of every kind, and a + `set -C` redirect can hang on a FIFO. +4. **Create through an exclusive open**, with the command below. +5. **Report only what the script printed and what `ls -ld CLAUDE.md` shows afterwards.** A partial + file is left as it is — do not delete it and do not retry into it. If the command fails before the + script prints anything — for example the shell cannot create the here-document — report + `failed: create write` with the shell's error text and the path's state afterwards. + +The create command passes the filled-in template on standard input. Run it exactly as shown, with +no added indentation: the script and the closing `CLAUDE_INIT_TEMPLATE` line must start in the +first column. The `O_CREAT | O_EXCL` open refuses any existing destination — file, directory, FIFO, +device, and a symlink whether or not it resolves — and that refusal is the guard. Mode `0o644` is +passed explicitly because the default `0o777` makes the file executable. + +```sh +python3 -c ' +import os, sys +path = sys.argv[1] +data = sys.stdin.buffer.read() +try: + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) +except FileExistsError: + print("stopped: create refused (EEXIST)"); sys.exit(3) +except OSError as e: + print("failed: create write (open: %s)" % e.strerror); sys.exit(4) +done = 0 +err = None +try: + while done < len(data): + done += os.write(fd, data[done:]) +except OSError as e: + err = e +try: + os.close(fd) +except OSError as e: + err = err or e +if err is not None: + print("%s (%d of %d bytes: %s)" % ("failed: partial write" if done else "failed: create write", done, len(data), err.strerror)); sys.exit(5) +try: + with open(path, "rb") as f: + same = f.read() == data +except OSError as e: + print("failed: partial write (read-back failed after %d of %d bytes: %s)" % (done, len(data), e.strerror)); sys.exit(5) +print("written (%d bytes)" % len(data) if same else "failed: partial write (read-back differs)") +sys.exit(0 if same else 5) +' CLAUDE.md <<'CLAUDE_INIT_TEMPLATE' + filled in> +CLAUDE_INIT_TEMPLATE +``` + +The exclusive open makes creating the **directory entry** exclusive. It does not make writing the +bytes atomic, so a write can fail part-way; the read-back at the end is what observes a complete +write. + +## Report + +Print one of these eight states, then the details. Example of the ordinary case: + +``` +claude-init: written + path: /home/ana/projects/atlas/CLAUDE.md + created: 3705 bytes, -rw-r--r-- + project: atlas (directory name) +``` + +| State | Meaning | Causes, the check that tells them apart, and the fix | +|---|---|---| +| `written` | The file was created and read back identical. | — | +| `unchanged` | An existing `CLAUDE.md` already carries equivalent rules. | Name the sections matched. | +| `proposal produced` | An existing file differs; the proposal was shown. Approved or declined, nothing was written. | The user applies it themselves if they want it. | +| `stopped: path kind` | Step one found an unsafe path. | **Symlink** (`ls -ld` starts with `l`): replace or remove the link, then rerun. **Directory or other type** (`d`, `p`, `c`, `b`, `s`): move it aside. **Unreadable** (`test -r` fails): fix its permissions. | +| `stopped: no qualifying create operation` | No usable Python 3. | `command -v python3` prints nothing → install Python 3, or create the file by hand from the template. It prints a path but `python3 --version` fails or is below 3 → fix that installation. | +| `stopped: create refused` | Something appeared at the path after step one. | Seen at the re-read, or `EEXIST` from the exclusive open. Run the command again; it will take the content branch. | +| `failed: create write` | The create failed and no template bytes were written. | Report the error text. `EACCES` → the directory is not writable (`test -w .`). `EROFS` → read-only filesystem. `ENOSPC` → disk full (`df .`). Other errors are possible; report them as printed. Report whether the open succeeded, and `ls -ld CLAUDE.md` afterwards — an empty file may exist. | +| `failed: partial write` | The open succeeded, but a complete write was not observed: only part of the template was written, closing the file failed, or reading it back failed or differed. | Report the byte counts and the error. `ENOSPC` → disk full (`df .`), the common cause of a short write. A read-back `Permission denied` → the file's mode leaves it unreadable, usually a restrictive `umask` (`ls -ld CLAUDE.md`). The file stays; the user checks or removes it before rerunning. | + +If the path's state afterwards cannot be inspected, say so in the report rather than guessing it. + +## Stop and ask + +- Before writing anything, if the user wants more than the general rules — gates, an `AGENTS.md`, + or a merge into an existing file. Point them to `/dev-workflow:workflow-init` for gates. +- After a proposal: always, whatever the answer. +- On every `stopped:` and `failed:` state: report and stop; the fix is the user's. + +## The template + +The complete file content. `` is filled in at write time; everything else is written as it +stands. + +> **Prompt-standards item 1 for the written `CLAUDE.md`: n/a, and why.** The file this +> template writes is model-agnostic by design — its executing model is whatever the reader of +> that project runs — so a `Target model:` line inside it would be false in every project it +> lands in. Recorded as a reasoned n/a rather than skipped: the item is answered. **This note sits +> outside the fence** so it is never written, and is deliberately **not** a `Target model:` line, +> which would make this file's declaration count 2 and fail `scripts/check-invariants.sh`. + +````markdown +# + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +### Don't guess + +Applies to factual claims in every answer, not only implementation. Confidence is not evidence. + +**Leave gaps visible.** Do not invent missing or ambiguous facts. State what is unknown and why. In extraction +tasks, leave unsupported fields blank where the format permits; otherwise use the format's defined missing-value +handling. + +**Separate evidence from inference.** Cite the relevant source for factual conclusions. Identify deductions and +assumptions as such, with their basis. For extraction tasks, label populated fields EXTRACTED or INFERRED and +explain each inference where the required output format permits. If neither annotations nor accompanying +explanations are permitted, preserve the required format. This does not permit inventing unsupported values. + +**Keep decisions distinct from facts.** Make reasonable design and implementation choices within the authorized +scope, describing them as choices rather than source facts. Ask when missing information changes correctness or +scope. + +**Verify before claiming.** Report a test or action as completed only when its result was observed. Preserve +required output formats; put explanations outside structured artifacts where permitted. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +Ground progress claims: before reporting a step as done, audit the claim against a tool result from this session ("tests green" needs a test run to point to). Report unverified work as unverified — this keeps status reports factual on long runs. + +--- + +**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. +```` diff --git a/plugins/dev-workflow/commands/workflow-init.md b/plugins/dev-workflow/commands/workflow-init.md index d72087e..0d789c6 100644 --- a/plugins/dev-workflow/commands/workflow-init.md +++ b/plugins/dev-workflow/commands/workflow-init.md @@ -211,6 +211,26 @@ Before implementing: - If a simpler approach exists, say so. Push back when warranted. - If something is unclear, stop. Name what's confusing. Ask. +### Don't guess + +Applies to factual claims in every answer, not only implementation. Confidence is not evidence. + +**Leave gaps visible.** Do not invent missing or ambiguous facts. State what is unknown and why. In extraction +tasks, leave unsupported fields blank where the format permits; otherwise use the format's defined missing-value +handling. + +**Separate evidence from inference.** Cite the relevant source for factual conclusions. Identify deductions and +assumptions as such, with their basis. For extraction tasks, label populated fields EXTRACTED or INFERRED and +explain each inference where the required output format permits. If neither annotations nor accompanying +explanations are permitted, preserve the required format. This does not permit inventing unsupported values. + +**Keep decisions distinct from facts.** Make reasonable design and implementation choices within the authorized +scope, describing them as choices rather than source facts. Ask when missing information changes correctness or +scope. + +**Verify before claiming.** Report a test or action as completed only when its result was observed. Preserve +required output formats; put explanations outside structured artifacts where permitted. + ## 2. Simplicity First **Minimum code that solves the problem. Nothing speculative.** From 00be5f8b9716cfe18a1c644ef34d3c64a60ded7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Fri, 25 Sep 2026 15:29:25 +0200 Subject: [PATCH 4/4] fix(commands): bound claude-init's project name before the create here-document MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #27, Greptile P1 (thread 4091660211): the project name was filled into the create command's shell here-document with no rule, so a name holding a line break could put the fixed delimiter on its own line, end the document early and run the rest of the name and the template as shell commands. - claude-init.md *Writing*: the Python check moves to step 1; step 2 requires the name to be one line with no Unicode Cc character (LF, CR/CRLF, tab, ...), from every source. A directory-derived name is obtained and checked by one Python command that prints `name ok: `, and exactly that value is filled in (Gate-B pass 1: a symlink alias could otherwise be checked under one name and filled in under another). A user-given name is checked by reading; a failure asks for a name before the create command. Nothing is sanitized. This is an agent-followed rule; the create script does not check the name. *Stop and ask* gains the name question as a non-state; the eight report states and the template bytes are unchanged. - Spec §2 carries the rule and its limits, §5 references it, and a new accounting table marks each prior condition kept or moved. - Story profile: security none -> standard, confirmed by Daniel on 2026-09-25 by releasing the repair brief; floor stays 3. - CHANGELOG 0.12.0 gains one sub-bullet (0.12.0 is unreleased; no further bump). AGENTS.md Don'ts gains the class-level rule; hardening-log gains a missing-input-validation row (rung 1 prose, no deterministic rung). Evidence — docs/superpowers/stories/2026-09-17-claude-init-command-story.md (battery+check): quality battery (AGENTS.md § Commands) green, exit 0. Check that fails without the change: .context/pr27-name-check.py runs the create snippet and the name check exactly as printed in claude-init.md, in throwaway directories under sh and dash. Against c60f52e's claude-init.md it fails (no name check exists to run — IndexError). Against the repaired file all 28 assertions pass: a control LF-collision name breaks out of the here-document under sh and dash (marker printed), so the harness can observe a breakout; five valid names (plain, space, Unicode, the delimiter as a single-line name, shell metacharacters) are written byte-exact with only CLAUDE.md present and no marker; an existing target is refused and unchanged; the printed Python name check flags LF, CRLF, CR, tab, ESC and U+0085 and passes the valid names; through a symlink alias whose logical name carries an LF delimiter collision, $PWD shows that name while the check prints `name ok: safe`, and filling in the printed value writes byte-exact under sh and dash. Limits: the agent's decision to stop and ask, to fill in the printed value, and the reading check of a user-given name, are verified by walkthrough only; no end-to-end command run exists. cycle 7vnga7znhk; floor 3 per {docs/superpowers/stories/2026-09-17-claude-init-command-story.md (level 1)}; hook reminder threshold absent cycle 7vnga7znhk; Gate B (passes 1-3, gpt-6-astra): Findings 4,3,5. Blockers 0,0,0. Majors 2,0,0. Three full-mode calls, one per logical pass, each branch pair issued with equal baseSha c60f52e5ab4433d6a1bc25f06a2b31522dcdb848 and headSha 4cf39f0ae8ab98d79a87231807bef2d9760dc1b7 (pass 1) and cc53b1ad5c48d002092e0063825b4ac0b05321d4 (passes 2-3). Reviewer model read from each Codex session log's turn_context. Pass 1's two Majors are one issue from both reviewers (checked vs filled name), fixed. The security lens set was carried on every pass. Collected Minors, not fixed: the plan (closed Gate-A artifact) still copies the old profile and "Lens sets: none"; spec §2's "exactly one new precondition" sentence now understates the name condition; the CHANGELOG header's per-version commit interval does not cover this later repair to the unreleased 0.12.0. --- AGENTS.md | 6 +++ docs/hardening-log.md | 1 + .../2026-09-17-claude-init-command-design.md | 34 ++++++++++++++++- .../2026-09-17-claude-init-command-story.md | 10 ++++- plugins/dev-workflow/CHANGELOG.md | 3 ++ plugins/dev-workflow/commands/claude-init.md | 37 +++++++++++++++---- 6 files changed, 81 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 52bf462..8da829a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -237,6 +237,12 @@ reader can judge whether it still holds. its most frequent site. Before any rename or delete: `grep -rn ""` across the repo *and* the inline templates. The same applies to adding files: the layout tree above is part of the surface that drifts. +- **Never splice data into shell source a prompt tells the agent to run without bounding it.** + A value filled into a command or here-document — a directory name, a user's answer — is shell + text once it is there, and a quoted here-document delimiter stops expansion but not a line that + ends the document. State the rule the value must meet, the check, and what happens when it fails. + `claude-init`'s project name did not, and one containing a line break could run the rest of the + template as commands (PR #27). Nothing mechanical finds this; it is a reading check. - **No dependency-freshness policy applies by ecosystem here** — this repo has no package manager and no runtime dependencies. Invariant 5 (exact pinning) is its equivalent, and it covers the tools CI installs. diff --git a/docs/hardening-log.md b/docs/hardening-log.md index 45b182c..8151183 100644 --- a/docs/hardening-log.md +++ b/docs/hardening-log.md @@ -125,3 +125,4 @@ escape `\|`, one line), `source` (gate-a|gate-b|bot|manual), | 2026-08-17 | prompt-vague-criteria | first row of this base class here: §5 named "clearly stuck" as the loop's terminal state and never said how to recognize one, so a plateauing artifact could burn passes with nobody entitled to call it — in the field one 2848-line spec ran 34 Gate-A spec passes, and over the 19 that were measured the count fell from 43 to an oscillation between 2 and 7 while Blockers fell from 8 to 0-1, i.e. the substance converged and the number never did | manual | major | P std | CLAUDE.md §5 loop paragraph + the same block in the workflow-init inline template. Guard: the recognition rule — after about six passes read the Blocker curve rather than the total, and Blockers at 0-1 with every remaining finding individually fixable IS the stuck state — plus the smaller-specs guidance that follows from the same measurement. Does NOT guard a size threshold (deliberately unmeasured and shipped as guidance), and does not say where a plateau begins. | | 2026-09-02 | docs-drift | seventh occurrence: PR #26 (CodeRabbit) — the dark-factory vision document, sitting on the same branch, still said the kit "has a fixed 3-pass floor", listed the review-economics story as "in flight" twice, and made both claims about the very change the branch ships. Every sentence was true when written and false the moment the branch merged. NEW SUB-SHAPE, and it is why this row exists rather than a note: the standing lens was carried on this cycle and the falsified file is one the diff never touches — it is not in the changed-path set, so nothing scoped to the diff could reach it, and the lens's own recipe (grep for where each changed value is described elsewhere) was run against `CLAUDE.md`'s vocabulary and not against the branch's other documents | bot | major | 1 prose | The lens now names the branch, not the diff, as its search surface: a document added or edited **anywhere on the same branch** can be falsified by a change it does not contain, and a co-shipped design document describing the current state of the thing being changed is the likeliest instance. PRIOR ROW: 2026-08-16 docs-drift (1 prose), and 2026-08-04 (P std) whose guard asks what the diff changes the size, value or position of — this finding is INSIDE that guard and outside its reach at once: the value did change and was described elsewhere, but "elsewhere" was scoped to the changed paths by everyone who ran it, including me. Repaired at the same rung, not escalated. NO DETERMINISTIC RUNG EXISTS: nothing can tell that a design document's description of current state went stale, and widening the grep to the whole branch is a recipe a human runs. It raises the floor and does not close the class | | 2026-09-02 | verification-masks-failure | fifth occurrence: PR #26 (CodeRabbit) — Plan C task 25's completion assert tested only that the provisional wording was ABSENT (`grep -c … -eq 0`). A replacement that deleted the provisional passage and wrote no closed record at all would have passed it, which is precisely the outcome the task exists to prevent. The task did run correctly this cycle, so the mask never fired; it was found by reading, not by failing | bot | major | 4 test | The assert gained a positive arm beside the negative one: the closed curves must be PRESENT, with a cardinality floor (`grep -cE '^Findings( +[0-9]+,?)+' … -ge 3`). PRIOR ROWS under this fingerprint all share one shape — a check whose only assertion is that something is gone. WHAT GENERALIZES: an absence assert is half a check whenever the edit it guards is a replacement rather than a deletion, and the missing half is always the same one. This row's remedy is specific to task 25; the general form belongs to the loop-rule consolidation story, which owns the plan-assert conventions | +| 2026-09-25 | missing-input-validation | first row of this base class here: PR #27 (Greptile P1, thread 4091660211) — `/dev-workflow:claude-init` filled an unconstrained project name into the create command's shell here-document; a name with a line break could put the fixed delimiter on its own line, end the document early and run the rest of the name and the template as shell. Confirmed as transport under sh and dash with a harmless marker, not as an end-to-end command run | bot | blocker | 1 prose | claude-init.md *Writing* step 2 now states the rule (one line, no Unicode `Cc` character, every name source, ask on failure, nothing sanitized) and gives a Python check for a directory-derived name; spec §2 carries it. AGENTS.md Don'ts gains the class-level rule: data spliced into shell source a prompt tells the agent to run must be bounded, with the rule, the check and the failure path stated. Does NOT guard: the rule is agent-followed — the create script does not validate the name, and a user-given name is checked by reading. NO DETERMINISTIC RUNG EXISTS: nothing mechanical can tell which prompt text becomes shell source; it is a reading check | diff --git a/docs/superpowers/specs/2026-09-17-claude-init-command-design.md b/docs/superpowers/specs/2026-09-17-claude-init-command-design.md index 9e2257a..7483cc0 100644 --- a/docs/superpowers/specs/2026-09-17-claude-init-command-design.md +++ b/docs/superpowers/specs/2026-09-17-claude-init-command-design.md @@ -143,6 +143,26 @@ refuses the target outright. What the re-read buys is a better report: the comma appeared, instead of only relaying an `EEXIST` from the attempt. **It narrows a window; it does not close one.** +**The project name must be one line with no control character before it reaches the create +command.** Added after PR #27's review (Greptile, thread 4091660211). The filled-in template travels +to the create operation as a shell here-document with a fixed, quoted delimiter; a name containing a +line break can put the delimiter on a line of its own, end the here-document early, and make the +rest of the name and the template shell commands. The name is substituted only into `# `, +so a one-line name always starts its line with `# ` and cannot form the delimiter line. The rule +applies to every source of the name — the directory's name, a name the user gives, and a +replacement answer — and rejects line feed, carriage return (alone or in CRLF), tab and every other +Unicode `Cc` character (U+0000–U+001F, U+007F–U+009F). Nothing is trimmed, replaced or stripped; +spaces, punctuation and non-ASCII letters stay valid. An invalid name stops the create branch +**before** the create command and asks for a name; this question is not a report state, so the eight +states stay as they are, and a run the user ends there has written nothing and printed no state. +**This is an instruction the executing agent follows, not a technical guard:** the directory name +comes from a one-line Python check that prints the physical directory name it checked, and that +printed value is the one the agent is told to fill in, so the checked name and the filled name are +one value even where a symlink makes the logical directory name differ (Gate-B cycle 7vnga7znhk, +pass 1). Python is already required on this branch, so the Python check moves ahead of the name. A +user-given name is checked by reading, and the create script itself does not validate the name and +could not undo a shell breakout that happened before it started. + **The create branch has one admission rule, and it admits no exceptions.** Between the final absence check and the write, another writer can create something at that path. **A write that overwrites it and discloses the overwrite afterwards is not preservation** — the file it destroyed @@ -297,7 +317,8 @@ First`, separated by one blank line. **The complete content the command writes.** In the command file this is wrapped in a ````markdown fence (four backticks), because the block below contains a three-backtick fence of its own. -`` is the target directory's name unless the user says otherwise. +`` is the target directory's name unless the user says otherwise, and in either case one +line with no control character (§2, *Writing*). ```` # @@ -544,6 +565,17 @@ purpose. | Row 4b: no `Don't guess` hunk, no §6 hunk | **Kept as statements about the transformation** — no insertion, no §6 removal — rather than as expectations about a rendered diff. | | Row 4b has no counterfactual | **Kept**, unchanged; exact equality does not give it one. | +### Accounting — what the PR #27 name rule kept, moved and changed + +| Prior condition | Now | +|---|---| +| `` is the directory's name unless the user says otherwise | **Kept**; every source of the name now passes the one-line rule (§2, *Writing*). | +| Python 3 is required only on the create branch | **Kept.** The directory-name check runs on that branch only; reading and proposing still need no Python. | +| The Python check came after the re-read | **Moved** ahead of the name check, so the name can be checked with the runtime already required; the re-read still runs immediately before the create. | +| Eight terminal report states | **Kept.** The name question is not a state; no ninth state was added. | +| *Done means*: exactly one report state printed | **Kept**; a run ended at the name question is stated as not done, with nothing written. | +| The exclusive create, the quoted delimiter, the inline template and every path and error branch | **Kept** unchanged. | + ## §8 Deliberately out of scope - **A parity checker for the three template copies.** Named as a cost in the story (D1/D3), not diff --git a/docs/superpowers/stories/2026-09-17-claude-init-command-story.md b/docs/superpowers/stories/2026-09-17-claude-init-command-story.md index 1c73373..1409c4f 100644 --- a/docs/superpowers/stories/2026-09-17-claude-init-command-story.md +++ b/docs/superpowers/stories/2026-09-17-claude-init-command-story.md @@ -1,13 +1,19 @@ # `/dev-workflow:claude-init` — general working rules without the review workflow — Story **Date:** 2026-09-17 · **Size:** story -**Risk:** standard · **Security:** none · **Validation:** battery+check +**Risk:** standard · **Security:** standard · **Validation:** battery+check **Profile log:** - 2026-09-17 · adoption · proposed at intake as `standard` / `none` / `battery+check`; **confirmed by Daniel on 2026-09-17, exactly as proposed.** Gates read this header, which is the only writable copy. Derived floor **3** — max(risk `standard` = 1, security `none` = 0) = 1, and only 0 gives a floor of 1. +- 2026-09-25 · axis change · security `none` → `standard`, after PR #27's review found that the + project name could break out of the create command's shell here-document (Greptile, thread + 4091660211). Complete header: `standard` / `standard` / `battery+check`; no prior override to void. + Proposed by the coding agent and the sparring reviewer; **confirmed by Daniel on 2026-09-25 by + releasing the repair brief that names this header.** Floor stays **3** — max(1, 1) = 1; the security + lens set now applies. ## 1. Problem statement @@ -104,7 +110,7 @@ the lighter entry point, never a downgrade path. Items 1–3 were settled on 2026-09-17, items 4 and 5 on 2026-09-21. 1. **The profile is confirmed** as `standard` / `none` / `battery+check`, exactly as proposed, and is - recorded in the profile log above. The reasoning it was accepted on: the command writes over a + recorded in the profile log above (security raised to `standard` on 2026-09-25 — see that log). The reasoning it was accepted on: the command writes over a file a project may have authored by hand, so the failure mode is **content loss** rather than inconvenience, and `trivial` would read that write path as harmless — the lenient direction on the one axis that matters here. diff --git a/plugins/dev-workflow/CHANGELOG.md b/plugins/dev-workflow/CHANGELOG.md index 20734d9..b6e0b9a 100644 --- a/plugins/dev-workflow/CHANGELOG.md +++ b/plugins/dev-workflow/CHANGELOG.md @@ -37,6 +37,9 @@ AGENTS.md invariant 12 carries the complete list. refuses any existing destination. That needs an already-installed Python 3; without one it stops instead of writing some weaker way. A failed or partial write is reported as such, never as `written`. + - The project name travels to that open through a shell here-document, so before creating it + must be one line with no control character; otherwise the command asks for a name rather than + running the create. The agent follows this rule; the create script does not check the name. - **`Don't guess`**, a new rule at the end of section 1, now ships in `/dev-workflow:workflow-init`'s `CLAUDE.md` template as well as in `claude-init`'s: leave gaps visible, separate evidence from inference, keep decisions distinct from facts, and report an action as done only when its result diff --git a/plugins/dev-workflow/commands/claude-init.md b/plugins/dev-workflow/commands/claude-init.md index 95b0d14..b65bf5e 100644 --- a/plugins/dev-workflow/commands/claude-init.md +++ b/plugins/dev-workflow/commands/claude-init.md @@ -76,14 +76,33 @@ describe the proposal as applied, and never write it through any other tool. ## Writing — the create branch only -1. **Fill in ``** in the template's first line with the current directory's name, unless the - user named the project. -2. **Re-run step one immediately before creating.** If anything is now at the path, report what - appeared as `stopped: create refused` and stop. This gives a better report; it is not the guard. -3. **Check for Python 3:** `command -v python3 && python3 --version`. No `python3`, or a version +1. **Check for Python 3:** `command -v python3 && python3 --version`. No `python3`, or a version below 3 → report `stopped: no qualifying create operation` and stop. Do not fall back to a shell redirect or your file-writing tool: neither refuses an existing destination of every kind, and a `set -C` redirect can hang on a FIFO. +2. **Settle the project name, then fill in ``** in the template's first line. The name is + the current directory's name, unless the user named the project. It goes into shell source — the + here-document below — so it must be **one line with no control character**: no line feed, no + carriage return (alone or as CRLF), no tab, and nothing else in Unicode category `Cc` + (U+0000–U+001F, U+007F–U+009F). A line break in the name could end the here-document early and + turn the rest of the name and the template into shell commands. Spaces, punctuation and non-ASCII + letters are fine; keep every character exactly as given. Get and check a directory-derived name + with one command: + + ```sh + python3 -c 'import os, unicodedata; n = os.path.basename(os.getcwd()); print("name ok: " + n if n and not any(unicodedata.category(c) == "Cc" for c in n) else "name invalid: %r" % n)' + ``` + + On `name ok:`, the project name is exactly the text after `name ok: ` — fill in that value and + no other. It is the directory's physical name, which differs from `$PWD` or what the prompt shows + when you arrived through a symlink; checking one name and filling in another would leave the + filled one unchecked. Check a name the user gave by reading it: it must be a single line with no + control character. If + the name is invalid, or you cannot tell, **stop before the create command**: say which character + is not allowed and ask the user for a project name. Check the answer by the same rule; ask again + until a name passes. This rule is followed by you — the create script does not check the name. +3. **Re-run step one immediately before creating.** If anything is now at the path, report what + appeared as `stopped: create refused` and stop. This gives a better report; it is not the guard. 4. **Create through an exclusive open**, with the command below. 5. **Report only what the script printed and what `ls -ld CLAUDE.md` shows afterwards.** A partial file is left as it is — do not delete it and do not retry into it. If the command fails before the @@ -165,12 +184,16 @@ If the path's state afterwards cannot be inspected, say so in the report rather - Before writing anything, if the user wants more than the general rules — gates, an `AGENTS.md`, or a merge into an existing file. Point them to `/dev-workflow:workflow-init` for gates. - After a proposal: always, whatever the answer. +- Before creating, when the project name fails the name rule in *Writing*. This question is not a + report state: once a valid name exists the create branch continues and ends in one of the states in + *Report*. If the user ends the run at this question, nothing was written, no report state is printed, + and the run is not done in the sense of *Done means*. - On every `stopped:` and `failed:` state: report and stop; the fix is the user's. ## The template -The complete file content. `` is filled in at write time; everything else is written as it -stands. +The complete file content. `` is filled in at write time, under the name rule in +*Writing*; everything else is written as it stands. > **Prompt-standards item 1 for the written `CLAUDE.md`: n/a, and why.** The file this > template writes is model-agnostic by design — its executing model is whatever the reader of