diff --git a/CLAUDE.md b/CLAUDE.md index e80cc2d..d95e743 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,3 +1,10 @@ # Guidelines +Guidelines for developing **this** repo (the strapped plugin), loaded only when working in the strapped repo. Rules for how the *harness itself* should plan/review/implement live in `plugins/strapped/conventions.md` and the stage prompts under `src/workflows/strapped-run/stages/` — those reach every run against any repo, so put harness-behavior guidance there, not here. + - **Always bump the plugin version when building new changes.** Any PR that changes the plugin's behavior (skills, workflows, scripts, hooks, conventions) must bump `version` in `plugins/strapped/.claude-plugin/plugin.json`. `claude plugin update` compares versions only — with an unbumped version, installed copies silently stay pinned to a stale commit even after the change merges. + +## Design + +- **This is a new repo — breaking changes are fine.** Don't preserve backward compatibility or carry compat weight when it complicates the design. +- **Don't add machinery for cases that carry no actionable signal.** Scope each mechanism to the state it can actually act on. diff --git a/plugins/strapped/.claude-plugin/plugin.json b/plugins/strapped/.claude-plugin/plugin.json index 6f45a9f..7ad307d 100644 --- a/plugins/strapped/.claude-plugin/plugin.json +++ b/plugins/strapped/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "strapped", - "version": "0.6.0", + "version": "0.7.0", "description": "Adversarial plan → implement → stacked-PR coding harness: rule-partitioned reviewers, refute passes, DAG deliverables in persistent worktrees, CLAUDE.md learning loop", "author": { "name": "Christian Schuetz", diff --git a/plugins/strapped/conventions.md b/plugins/strapped/conventions.md index 8d0243e..7dd6e50 100644 --- a/plugins/strapped/conventions.md +++ b/plugins/strapped/conventions.md @@ -273,6 +273,7 @@ Deterministic executables under `$PLUGIN_ROOT/scripts/`, invocable via Bash by s Node CLI that bundles `js-yaml` directly for frontmatter parsing and writing (a small `---` fence split does the rest — no `gray-matter` wrapper, so the artifact carries a single js-yaml). A write re-serializes the whole frontmatter block through the js-yaml engine, so it is not a byte-for-byte preservation of every line. The engine is pinned (`flowLevel: 1`, `condenseFlow`, `lineWidth: -1`) so the two shapes grep-based consumers depend on survive: the deliverable `deps: [...]` flow array (`sync-prs.sh` parses `[...]`) and the single-space `key: value` scalar block lines (`sync-prs.sh`/`preamble.sh` read `^status:`/`^pr:`/`^id:`). js-yaml quotes a scalar only when it contains a colon-SPACE `: ` (e.g. an agent-composed `parked_reason: 'typecheck failed: TS2322'`); a `pr:` URL stays unquoted (`pr: https://…`) because its `://` is colon-slash, a valid plain scalar. Either way the `key: value` line shape holds, and `sync-prs.sh` tolerates an optional surrounding quote. The manifest's `repos:`/`deliverables:`/`budgets:` maps are read only by state.ts (never grepped by bash), so their reflow under this engine is inconsequential. - **`resolve `** — resolves `stateRoot` per [Config resolution](#config-resolution) (`$STRAPPED_STATE_ROOT` → `~/.claude/strapped.json` → default `~/.claude/strapped`; leading `~` expanded; a value still relative after expansion is invalid input → exit 1) and probes `/runs//manifest.md` — the cwd-independent direct path, no glob. Output: `{ slug, stateRoot, runRoot, runDir, manifest, exists, status, seed, budgets, repos: [{ name, root, config, configExists, validations, worktreeRoot, provisioning }] }`, where `repos` comes from the manifest `repos:` map joined with each repo's config at `/repos//config.json`. A missing manifest is NOT an error: `exists: false`, exit 0 (the plan skill treats a miss as "no existing run"; slug-addressed downstream skills stop themselves). +- **`runroot`** — slug-less resolution for callers that scan across ALL runs (e.g. `/strapped:learn` globbing every `runs/*/critiques/user-critiques.md`). Resolves `stateRoot` per [Config resolution](#config-resolution) (same chain and hard-fail-on-invalid rules as `resolve`) and returns `{ stateRoot, runRoot }` where `runRoot` = `/runs`. An unresolvable or non-absolute anchor → exit 1, so a subsequent empty cross-run glob is unambiguously "no runs" rather than a silently-wrong root. Never hand-roll `stateRoot`/`runRoot` from the cwd or an ad-hoc `~/.claude/strapped.json` read. - **`dag [--only ]`** — reads the manifest `deliverables` list and every deliverable file's frontmatter. Output: `{ manifest: {status, seed, budgets}, nodes: [{id, file, title, status, deps, repo, branch, base, worktree, pr, review_rounds_used, feedback_rounds_used, parked_reason, estimated_diff_lines}], ready, topo, blocked: [{id, blockedOn}], remaining }`. `ready` = `status: pending` nodes whose deps are all `done`/`pr-open`/`merged`; with `--only`, a `parked`/`in-progress` node is additionally admitted (implement's `--only` resume semantics) and `ready` is intersected with the named node. `topo` = stable topological order, parents before children, ties broken by id. `remaining` = count of nodes NOT yet `done`/`pr-open`/`merged` — done-or-later counts as complete, so a partially-shipped run reports the true remaining work; consumers read this field verbatim and never recompute it. Unknown dep id or dependency cycle → exit 1 naming the offender. - **`set `** — idempotent single-field frontmatter write; `value` is written verbatim after `: ` (`null` writes literal `null`). `value` must be a single line: a value containing `\n` or `\r` → exit 1, no write (a multi-line value would inject extra frontmatter lines). A field not already present in the file's frontmatter → exit 1 (no silent field invention). Output: `{file, field, old, new}`. - **`transition [--from ]`** — guarded deliverable status flip over the on-disk edge table below. `--from` adds an exact-current-status guard. Transitioning to the current status is an idempotent no-op: exit 0, `{changed: false}`. An illegal edge → exit 1 naming the current status and requested edge, no write. Output: `{file, from, to, changed}`. diff --git a/plugins/strapped/scripts/state.mjs b/plugins/strapped/scripts/state.mjs index baa4096..2aa089f 100755 --- a/plugins/strapped/scripts/state.mjs +++ b/plugins/strapped/scripts/state.mjs @@ -3277,6 +3277,10 @@ function cmdResolve(slug) { repos }); } +function cmdRunRoot() { + const stateRoot = resolveStateRoot(die2); + out({ stateRoot, runRoot: join2(stateRoot, "runs") }); +} var COMPLETE_STATUSES = new Set(["done", "pr-open", "merged"]); function cmdDag(runDir, only) { const manifestFile = join2(runDir, "manifest.md"); @@ -3567,7 +3571,7 @@ function cmdFeedbackIndexSet(runDir, externalId, status, commit) { writeFeedbackIndex(path, index); out({ externalId, from, to: status, commit: comment.commit, changed: true }); } -var USAGE = "usage: state.mjs ..."; +var USAGE = "usage: state.mjs ..."; var [cmd, ...rest] = process.argv.slice(2); function takeFlag(args, flag) { const i = args.indexOf(flag); @@ -3587,6 +3591,10 @@ switch (cmd) { cmdResolve(slug); break; } + case "runroot": { + cmdRunRoot(); + break; + } case "dag": { const only = takeFlag(rest, "--only"); const runDirArg = rest[0]; diff --git a/plugins/strapped/skills/implement/SKILL.md b/plugins/strapped/skills/implement/SKILL.md index 5812c56..1764149 100644 --- a/plugins/strapped/skills/implement/SKILL.md +++ b/plugins/strapped/skills/implement/SKILL.md @@ -43,6 +43,8 @@ The `resolve` output already carries the manifest `status`, `seed`, `budgets`, a Each deliverable's `repo:` field is **required** and names one of the `repos:` entries — a deliverable with no `repo:` is invalid input. +**Baseline-freshness pre-flight.** The plan may have been written before `main` moved. Before dispatching, in each target repo `git -C fetch` and diff the plan's architectural assumptions against current `origin/main` (recent commits touching the conventions/specs/modules the deliverables cite). If main has moved under the plan, surface the conflicts to the user and let them choose to amend the affected deliverables to match current main (or re-plan) — never implement a spec you know is stale. + ## Step 2 — Rule assignments As in /strapped:plan: read `reviews/rules-snapshot.md` (re-extract if missing — discover every applicable CLAUDE.md AND recurse into any skills/files it loads for additional rules, per the conventions' **Rule extraction**), compute the per-round rule splits (full rule objects) for rounds `1..code_rounds` using `random.Random(seed + round)` from the manifest seed. diff --git a/plugins/strapped/skills/learn/SKILL.md b/plugins/strapped/skills/learn/SKILL.md index bfb2bee..e9ade5e 100644 --- a/plugins/strapped/skills/learn/SKILL.md +++ b/plugins/strapped/skills/learn/SKILL.md @@ -1,6 +1,6 @@ --- name: learn -description: Synthesize captured user critiques from strapped runs into proposed CLAUDE.md guideline additions — presented as a diff for approval, never auto-applied +description: Synthesize captured user critiques from strapped runs into proposed guidelines, routed by scope to the harness (stage prompts / SKILLs / conventions) or the pertaining repo's CLAUDE.md — presented as a diff for approval, never auto-applied allowed-tools: - Read - Write @@ -15,24 +15,37 @@ Turn the user's recurring corrections into durable guidelines. Source format is ## Step 1 — Collect -Collect every critique entry with `synthesized: false` across all runs, per the conventions' Config resolution: glob `/runs/*/critiques/user-critiques.md` — every run under the global state root, so critiques from **every** run are collected. The `runs/` tier never touches `repos/` (its sibling dir). +Resolve the run root the SAME way every other strapped skill does — via the canonical resolver, never by hand-rolling the config chain or reading `~/.claude/strapped.json` yourself (a mis-resolved root silently globs to zero and is indistinguishable from "no critiques"): -If there are none, say so and stop. +```bash +node $PLUGIN_ROOT/scripts/state.mjs runroot # → { "stateRoot": "", "runRoot": "/runs" } +``` + +If that command exits non-zero (unresolvable or non-absolute anchor), **stop and report the resolution error** — do NOT treat it as "no critiques." + +Then collect every critique entry with `synthesized: false` across all runs: glob `/*/critiques/user-critiques.md` — every run under the global state root, so critiques from **every** run are collected. The `runs/` tier never touches `repos/` (its sibling dir). State the resolved `runRoot` and how many critique files matched, so a zero is legibly "root X held no unsynthesized critiques" and not a swallowed resolution failure. + +If the root resolved cleanly but there are genuinely no unsynthesized entries, say so and stop. ## Step 2 — Cluster and filter 1. Group entries expressing the same underlying rule (across runs). -2. Drop clusters already covered by an existing rule — read every applicable CLAUDE.md first and compare meaning, not wording. +2. Drop clusters already covered by an existing rule — compare **meaning**, not wording, against wherever a rule of that scope would already live (per Step 3's routing): every applicable `CLAUDE.md`, `$PLUGIN_ROOT/conventions.md`, and the stage prompts under `$PLUGIN_ROOT/../../src/workflows/strapped-run/`. A critique whose lesson is already encoded in a stage prompt is covered even if no `CLAUDE.md` mentions it. 3. Drop entries marked `generalizable: no` or that are plan-specific one-offs; flip those to `synthesized: no` with a short reason appended to the entry. -## Step 3 — Draft +## Step 3 — Classify scope and route + +Critiques captured during runs are usually corrections about **how the harness behaves**, not about how to develop the plugin repo — and a rule only fires where it is actually loaded. Classify each surviving cluster and pick its target file accordingly (a cluster may be **both**, landing in more than one place): + +- **harness-behavior** — how the strapped harness itself plans, reviews, implements, creates PRs, or otherwise operates; it must shape FUTURE runs against ANY repo. Route to the harness, most specific first: the exact agent prompt under `src/workflows/strapped-run/stages/*.ts` (or `review-loop.ts`) when the rule governs one agent's behavior; the relevant skill's `SKILL.md` step when it is an orchestrator/interactive concern; `conventions.md` when it is a cross-cutting format/procedure rule seeded to every subagent. Editing any `src/**` stage prompt requires a rebuild (`bun run build`) of the generated `plugins/strapped/workflows/strapped-run.js` and a plugin-version bump (per the repo's own CLAUDE.md). +- **repo-development** — how to develop a specific repo's code (its naming, testing style, build, architecture). Route to **that repo's** `CLAUDE.md`, NOT the plugin's. Determine the pertaining repo from the cluster's source critiques: each lives under `//critiques/`, so resolve that run via `node $PLUGIN_ROOT/scripts/state.mjs resolve ` and use its `repos[].root` — the guideline lands in that repo root's `CLAUDE.md`. Only when the pertaining repo genuinely IS the strapped plugin does it land in this repo's `CLAUDE.md`. -For each surviving cluster, draft one guideline line in the existing CLAUDE.md voice (terse imperative bullets, no explanations) and pick the section it belongs in (or propose a new section only when nothing fits). Prefer editing an existing rule over adding a near-duplicate. +Draft one guideline line per cluster in the target file's existing voice (terse imperative, no explanations), and pick the section it belongs in (or a new section only when nothing fits). Prefer editing an existing rule over a near-duplicate. ## Step 4 — Propose (the gate) -Present a **unified diff** of the proposed CLAUDE.md changes plus, per hunk, the source critiques that motivated it. Ask the user to approve/reject each proposed guideline (AskUserQuestion with one question per guideline when few, or a single multi-select). **Apply nothing without explicit approval.** +Present a **unified diff per target file**, and for each proposed guideline state its scope (harness-behavior/repo-development), its destination file, and the source critiques that motivated it. Ask the user to approve/reject each proposed guideline (AskUserQuestion with one question per guideline when few, or a single multi-select). **Apply nothing without explicit approval.** ## Step 5 — Apply approved changes only -Edit CLAUDE.md with the approved hunks only. Flip each consumed entry to `synthesized: true` (rejected clusters: `synthesized: no`). Report what was applied and what was rejected. +Edit each approved guideline into its routed target file (rebuild + bump the plugin version if any `src/**` stage prompt changed). Flip each consumed entry to `synthesized: true` (rejected clusters: `synthesized: no`). Report what was applied, where each guideline landed, and what was rejected. diff --git a/plugins/strapped/workflows/strapped-run.js b/plugins/strapped/workflows/strapped-run.js index b4654b1..6aff2ba 100644 --- a/plugins/strapped/workflows/strapped-run.js +++ b/plugins/strapped/workflows/strapped-run.js @@ -725,7 +725,7 @@ var PR_SCHEMA = { // src/workflows/strapped-run/review-loop.ts var PLAN_LENSES = { - a: "completeness: is every element of the original ask covered by some deliverable? Hunt for missing requirements, unhandled edge cases, acceptance criteria without tests, and parts of the ask that silently disappeared", + a: "completeness AND fidelity to intent: is every element of the original ask covered by some deliverable? Hunt for missing requirements, unhandled edge cases, acceptance criteria without tests, and parts of the ask that silently disappeared. Beyond coverage, test the plan's KEY DESIGN DECISIONS against the ask's UNDERLYING INTENT, not just its own stated framing/ACs — a plan can satisfy every AC it wrote for itself while a core decision quietly contradicts what the user actually wanted. Re-open such decisions as findings", b: "soundness: wrong assumptions about the codebase, DAG dependency errors (missing or backwards deps, undeclared cross-deliverable coupling), deliverables that mix unrelated themes or whose estimated meaningful diff (excluding generated code, dependency bumps, and fixtures) exceeds ~1,000 lines and should be split, deliverables/chains that should be CONSOLIDATED (fragments of one theme, or a linear chain whose combined meaningful diff — excluding generated code, dependency bumps, and fixtures — is under the ~1,000-line threshold and could be a single deliverable/PR), planned work that is dead, duplicated, or superseded within the plan (steps or files a later step obviates, two deliverables doing the same work, or acceptance criteria/tests no remaining step produces), and steps that cannot work as written" }; function ruleBlock(rules) { @@ -1056,7 +1056,7 @@ ${item.resumeNote ? ` This deliverable is being RESUMED. Prior state: ${item.resumeNote} ` : ""} -Implement exactly what the plan specifies — its acceptance criteria are the contract. Write the tests the plan names (integration-style, public interfaces). Stay in scope: anything under "Out of scope" is off limits; note side-discoveries in your summary instead of fixing them. +Implement exactly what the plan specifies — its acceptance criteria are the contract. Write the tests the plan names (integration-style, public interfaces). Do NOT write grep-guard regression tests that police prose/spec files for the reintroduction of a removed concept — a deleted concept that won't plausibly recur does not need a permanent test pinning its absence. Stay in scope: anything under "Out of scope" is off limits; note side-discoveries in your summary instead of fixing them. Before finishing, ALL validations must pass inside the worktree: ${item.validations.map((v) => `- ${v}`).join(` @@ -1294,9 +1294,9 @@ Output directory (already scaffolded): ${cfg.dir} Conventions you MUST follow for every file format: ${cfg.conventionsFile} Procedure: -1. Read the source plan in full, then research each target repo's codebase thoroughly: architecture, the modules the ask touches, existing utilities to reuse, test patterns. +1. Read the source plan in full, then research each target repo's codebase thoroughly: architecture, the modules the ask touches, existing utilities to reuse, test patterns. Verify every repo claim against \`origin/main\` — \`git -C fetch\` first, then read the fetched \`origin/main\`, NOT the local working tree, which may be behind. A plan written against a stale local checkout is a defect. 2. Write ${cfg.dir}/research.md — a distilled digest (~300 lines max): architecture notes, key files with one-line roles, library/API findings, decisions with rationale, known pitfalls. This is the only research context implementers will ever see. -3. Split the work into deliverables by discrete theme, forming a DAG: independent work has no deps, dependent work lists its parent deliverable ids. Keep one coherent theme in a single deliverable so a reviewer can grasp the whole change in one PR — split a theme into multiple deliverables only when its estimated meaningful diff (excluding generated code, dependency/lockfile bumps, generated clients/schemas, vendored code, and large fixtures) exceeds ~1,000 changed lines. Prefer a few cohesive, independently-shippable nodes over many fragments that scatter one theme across PRs. Assign each deliverable to exactly one target repo. +3. Split the work into deliverables by discrete theme, forming a DAG: independent work has no deps, dependent work lists its parent deliverable ids. Keep one coherent theme in a single deliverable so a reviewer can grasp the whole change in one PR — split a theme into multiple deliverables only when its estimated meaningful diff (excluding generated code, dependency/lockfile bumps, generated clients/schemas, vendored code, and large fixtures) exceeds ~1,000 changed lines. Prefer a few cohesive, independently-shippable nodes over many fragments that scatter one theme across PRs. Assign each deliverable to exactly one target repo. If a target repo has no real test suite and its only validations are heuristic syntax checks, make a proper test suite (per that project type's current standard) the FIRST deliverable and wire it in as the validation gate for the rest of the run — do not settle for the heuristics. 4. Write one self-contained file per deliverable at ${cfg.dir}/deliverables/-.md per the conventions (frontmatter: id, title, deps, repo: , status: pending, branch: strapped/${cfg.slug}/-, base, worktree: null, pr: null, review_rounds_used: 0, feedback_rounds_used: 0, parked_reason: null, estimated_diff_lines; body: Context slice from your research, Files to touch, Implementation steps, Acceptance criteria, Tests, Out of scope). Set base per the cross-repo base rule: a deliverable's base is a parent branch WITHIN THE SAME repo, otherwise that repo's main (roots, and any cross-repo child, base on their own repo's main — you can never branch across repos). A fresh implementer seeded with ONLY this file plus research.md must be able to do the work. 5. Cross-repo deps are ordering-only, NEVER a code dependency: a cross-repo child bases on its own repo's main and does not have its parent's unmerged code. Reject or restructure any plan where a cross-repo child has a true code dependency on its parent — either require the shared change to merge to the parent repo's main first, or keep both sides in the same repo/chain. 6. Write ${cfg.dir}/manifest.md per the conventions (status: in-review, seed: ${cfg.seed}, budgets — record the EFFECTIVE budgets of this run: plan_rounds: ${cfg.planRounds}, code_rounds: ${cfg.codeRounds}, confidence_min: ${cfg.confidenceMin} — the repos: map listing every target repo above per the conventions — name, root, config path (repos: is an unordered set, no repo is special); the deliverables list with ids/files/repos/deps, theme summary, ASCII DAG sketch). diff --git a/src/scripts/state.ts b/src/scripts/state.ts index f6fbbb2..25349a4 100644 --- a/src/scripts/state.ts +++ b/src/scripts/state.ts @@ -5,6 +5,7 @@ // // Commands (all print one JSON object on stdout; misuse = one-line stderr + exit 1): // resolve config + run-root resolution +// runroot slug-less stateRoot/runRoot resolution (for cross-run globs) // dag [--only ] nodes, ready set, topo order, blocked, remaining // set single-field frontmatter write // transition [--from ] guarded deliverable status flip @@ -83,6 +84,18 @@ function cmdResolve(slug: string): void { }) } +// --- runroot --------------------------------------------------------------- + +// Slug-less resolution: the single anchor value plus its `runs/` tier, for +// callers that scan across ALL runs (e.g. /strapped:learn globbing every +// `runs/*/critiques/user-critiques.md`) rather than one slug. Errors loudly +// via resolveStateRoot's die on an unresolvable/relative anchor, so an empty +// cross-run glob is unambiguously "no matches" — never a silently-wrong root. +function cmdRunRoot(): void { + const stateRoot = resolveStateRoot(die) + out({ stateRoot, runRoot: join(stateRoot, 'runs') }) +} + // --- dag ------------------------------------------------------------------- const COMPLETE_STATUSES: ReadonlySet = new Set(['done', 'pr-open', 'merged']) @@ -459,7 +472,7 @@ function cmdFeedbackIndexSet(runDir: string, externalId: string, status: string, // --- dispatch --------------------------------------------------------------- -const USAGE = 'usage: state.mjs ...' +const USAGE = 'usage: state.mjs ...' const [cmd, ...rest] = process.argv.slice(2) function takeFlag(args: string[], flag: string): string | null { @@ -478,6 +491,10 @@ switch (cmd) { cmdResolve(slug) break } + case 'runroot': { + cmdRunRoot() + break + } case 'dag': { const only = takeFlag(rest, '--only') const runDirArg = rest[0] diff --git a/src/workflows/strapped-run/review-loop.ts b/src/workflows/strapped-run/review-loop.ts index 7e3f9e3..741780f 100644 --- a/src/workflows/strapped-run/review-loop.ts +++ b/src/workflows/strapped-run/review-loop.ts @@ -19,7 +19,7 @@ import type { } from './types.ts' export const PLAN_LENSES: Record = { - a: 'completeness: is every element of the original ask covered by some deliverable? Hunt for missing requirements, unhandled edge cases, acceptance criteria without tests, and parts of the ask that silently disappeared', + a: "completeness AND fidelity to intent: is every element of the original ask covered by some deliverable? Hunt for missing requirements, unhandled edge cases, acceptance criteria without tests, and parts of the ask that silently disappeared. Beyond coverage, test the plan's KEY DESIGN DECISIONS against the ask's UNDERLYING INTENT, not just its own stated framing/ACs — a plan can satisfy every AC it wrote for itself while a core decision quietly contradicts what the user actually wanted. Re-open such decisions as findings", b: 'soundness: wrong assumptions about the codebase, DAG dependency errors (missing or backwards deps, undeclared cross-deliverable coupling), deliverables that mix unrelated themes or whose estimated meaningful diff (excluding generated code, dependency bumps, and fixtures) exceeds ~1,000 lines and should be split, deliverables/chains that should be CONSOLIDATED (fragments of one theme, or a linear chain whose combined meaningful diff — excluding generated code, dependency bumps, and fixtures — is under the ~1,000-line threshold and could be a single deliverable/PR), planned work that is dead, duplicated, or superseded within the plan (steps or files a later step obviates, two deliverables doing the same work, or acceptance criteria/tests no remaining step produces), and steps that cannot work as written', } diff --git a/src/workflows/strapped-run/stages/implement.ts b/src/workflows/strapped-run/stages/implement.ts index c92f510..838f3f6 100644 --- a/src/workflows/strapped-run/stages/implement.ts +++ b/src/workflows/strapped-run/stages/implement.ts @@ -48,7 +48,7 @@ Work EXCLUSIVELY inside the worktree: ${item.worktree} (branch ${item.branch}, b 2. Read the shared research digest: ${cfg.dir}/research.md 3. Read the project guidelines: every CLAUDE.md that applies (repo root at minimum). ${item.resumeNote ? `\nThis deliverable is being RESUMED. Prior state:\n${item.resumeNote}\n` : ''} -Implement exactly what the plan specifies — its acceptance criteria are the contract. Write the tests the plan names (integration-style, public interfaces). Stay in scope: anything under "Out of scope" is off limits; note side-discoveries in your summary instead of fixing them. +Implement exactly what the plan specifies — its acceptance criteria are the contract. Write the tests the plan names (integration-style, public interfaces). Do NOT write grep-guard regression tests that police prose/spec files for the reintroduction of a removed concept — a deleted concept that won't plausibly recur does not need a permanent test pinning its absence. Stay in scope: anything under "Out of scope" is off limits; note side-discoveries in your summary instead of fixing them. Before finishing, ALL validations must pass inside the worktree: ${item.validations.map(v => `- ${v}`).join('\n')} diff --git a/src/workflows/strapped-run/stages/plan.ts b/src/workflows/strapped-run/stages/plan.ts index f986078..e6e0e8b 100644 --- a/src/workflows/strapped-run/stages/plan.ts +++ b/src/workflows/strapped-run/stages/plan.ts @@ -21,9 +21,9 @@ Output directory (already scaffolded): ${cfg.dir} Conventions you MUST follow for every file format: ${cfg.conventionsFile} Procedure: -1. Read the source plan in full, then research each target repo's codebase thoroughly: architecture, the modules the ask touches, existing utilities to reuse, test patterns. +1. Read the source plan in full, then research each target repo's codebase thoroughly: architecture, the modules the ask touches, existing utilities to reuse, test patterns. Verify every repo claim against \`origin/main\` — \`git -C fetch\` first, then read the fetched \`origin/main\`, NOT the local working tree, which may be behind. A plan written against a stale local checkout is a defect. 2. Write ${cfg.dir}/research.md — a distilled digest (~300 lines max): architecture notes, key files with one-line roles, library/API findings, decisions with rationale, known pitfalls. This is the only research context implementers will ever see. -3. Split the work into deliverables by discrete theme, forming a DAG: independent work has no deps, dependent work lists its parent deliverable ids. Keep one coherent theme in a single deliverable so a reviewer can grasp the whole change in one PR — split a theme into multiple deliverables only when its estimated meaningful diff (excluding generated code, dependency/lockfile bumps, generated clients/schemas, vendored code, and large fixtures) exceeds ~1,000 changed lines. Prefer a few cohesive, independently-shippable nodes over many fragments that scatter one theme across PRs. Assign each deliverable to exactly one target repo. +3. Split the work into deliverables by discrete theme, forming a DAG: independent work has no deps, dependent work lists its parent deliverable ids. Keep one coherent theme in a single deliverable so a reviewer can grasp the whole change in one PR — split a theme into multiple deliverables only when its estimated meaningful diff (excluding generated code, dependency/lockfile bumps, generated clients/schemas, vendored code, and large fixtures) exceeds ~1,000 changed lines. Prefer a few cohesive, independently-shippable nodes over many fragments that scatter one theme across PRs. Assign each deliverable to exactly one target repo. If a target repo has no real test suite and its only validations are heuristic syntax checks, make a proper test suite (per that project type's current standard) the FIRST deliverable and wire it in as the validation gate for the rest of the run — do not settle for the heuristics. 4. Write one self-contained file per deliverable at ${cfg.dir}/deliverables/-.md per the conventions (frontmatter: id, title, deps, repo: , status: pending, branch: strapped/${cfg.slug}/-, base, worktree: null, pr: null, review_rounds_used: 0, feedback_rounds_used: 0, parked_reason: null, estimated_diff_lines; body: Context slice from your research, Files to touch, Implementation steps, Acceptance criteria, Tests, Out of scope). Set base per the cross-repo base rule: a deliverable's base is a parent branch WITHIN THE SAME repo, otherwise that repo's main (roots, and any cross-repo child, base on their own repo's main — you can never branch across repos). A fresh implementer seeded with ONLY this file plus research.md must be able to do the work. 5. Cross-repo deps are ordering-only, NEVER a code dependency: a cross-repo child bases on its own repo's main and does not have its parent's unmerged code. Reject or restructure any plan where a cross-repo child has a true code dependency on its parent — either require the shared change to merge to the parent repo's main first, or keep both sides in the same repo/chain. 6. Write ${cfg.dir}/manifest.md per the conventions (status: in-review, seed: ${cfg.seed}, budgets — record the EFFECTIVE budgets of this run: plan_rounds: ${cfg.planRounds}, code_rounds: ${cfg.codeRounds}, confidence_min: ${cfg.confidenceMin} — the repos: map listing every target repo above per the conventions — name, root, config path (repos: is an unordered set, no repo is special); the deliverables list with ids/files/repos/deps, theme summary, ASCII DAG sketch). diff --git a/tests/state-cli.test.ts b/tests/state-cli.test.ts index e4511ce..ab70663 100644 --- a/tests/state-cli.test.ts +++ b/tests/state-cli.test.ts @@ -181,6 +181,40 @@ test('resolve: missing slug argument → usage on stderr, exit 1', () => { assert.match(res.stderr, /usage/) }) +// --- runroot ----------------------------------------------------------------- + +interface RunRootJson { + stateRoot: string + runRoot: string +} + +test('runroot: env stateRoot → { stateRoot, runRoot } with no slug, exit 0', () => { + const env = makeStateEnv() + const res = env.runState(['runroot']) + assert.equal(res.status, 0, res.stderr) + const json = parse(res) + assert.equal(json.stateRoot, env.stateRoot) + assert.equal(json.runRoot, join(env.stateRoot, 'runs')) +}) + +test('runroot: no env, no anchor → default ~/.claude/strapped under isolated HOME', () => { + const env = makeStateEnv() + const res = env.runState(['runroot'], { env: { STRAPPED_STATE_ROOT: undefined } }) + assert.equal(res.status, 0, res.stderr) + const json = parse(res) + assert.equal(json.stateRoot, join(env.home, '.claude', 'strapped')) + assert.equal(json.runRoot, join(env.home, '.claude', 'strapped', 'runs')) +}) + +test('runroot: relative stateRoot → exit 1 with one-line stderr, no silent fallback', () => { + const env = makeStateEnv() + const res = env.runState(['runroot'], { env: { STRAPPED_STATE_ROOT: 'plans/strapped' } }) + assert.equal(res.status, 1) + assert.equal(res.stdout, '') + assert.match(res.stderr, /not absolute/) + assert.equal(res.stderr.trim().split('\n').length, 1) +}) + // --- dag ----------------------------------------------------------------- test('dag: roots ready, child blocked until parent done/pr-open/merged', () => {