diff --git a/.loopwright/claude-md-section.md b/.loopwright/claude-md-section.md index ceb3754..8338bc9 100644 --- a/.loopwright/claude-md-section.md +++ b/.loopwright/claude-md-section.md @@ -1,8 +1,12 @@ ## loopwright -This repo has the loopwright layer vendored under `.loopwright/`: RFC-driven -issues, an agentic execution loop, and a CI quality gate. See -`docs/loopwright/quality-gate.md` for how the gate works, and +In this repo, "done" is a mechanical verdict from configured checks, never +the judgement of whoever merges: quality is a ratchet against a committed +baseline, and a green build has to be earned rather than bought. Those +opinions are enforced by the loopwright layer vendored under `.loopwright/`: +RFC-driven issues, an agentic execution loop, and a CI quality gate. Read +`docs/loopwright/principles.md` for the opinions in full and what each one +refuses, `docs/loopwright/quality-gate.md` for how a verdict is reached, and `docs/loopwright/loop-harness.md` for how work flows from an RFC issue to a merged PR (the `/grill-rfc`, `/execute-issue`, and `/babysit-pr` skills). diff --git a/CLAUDE.md b/CLAUDE.md index 9076c9f..189a0a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,15 +1,21 @@ # loopwright -Built from the loopwright project — this repo IS the product: the engine -lives in `.loopwright/scripts/`, its tests in `.loopwright/tests/` +This repo is loopwright itself, so nothing in it may make "done" a judgement +call or buy a green build — it is held to the opinions it ships +(`docs/loopwright/principles.md`). Its engine lives in +`.loopwright/scripts/`, its tests in `.loopwright/tests/` (`cd .loopwright && npx vitest run` runs them). ## loopwright -This repo has the loopwright layer vendored under `.loopwright/`: RFC-driven -issues, an agentic execution loop, and a CI quality gate. See -`docs/loopwright/quality-gate.md` for how the gate works, and +In this repo, "done" is a mechanical verdict from configured checks, never +the judgement of whoever merges: quality is a ratchet against a committed +baseline, and a green build has to be earned rather than bought. Those +opinions are enforced by the loopwright layer vendored under `.loopwright/`: +RFC-driven issues, an agentic execution loop, and a CI quality gate. Read +`docs/loopwright/principles.md` for the opinions in full and what each one +refuses, `docs/loopwright/quality-gate.md` for how a verdict is reached, and `docs/loopwright/loop-harness.md` for how work flows from an RFC issue to a merged PR (the `/grill-rfc`, `/execute-issue`, and `/babysit-pr` skills). diff --git a/README.md b/README.md index a49349a..5914573 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # loopwright -**A CI quality gate for repos where agents write the code — plus the loop that -feeds it.** Vendor it into any JS/TS repo: every PR gets scored against a -committed baseline, and the verdict comes back as a comment an agent can act on -without a human translating it first. +**"Done" is a mechanical verdict, not a judgement call.** loopwright is a CI +gate plus the loop that feeds it: it scores every PR against a committed +baseline, asks whether the green was earned or bought, and posts the verdict +as a comment an agent can act on without a human translating it first. +Vendor it into any JS/TS repo: it is opinionated about how work flows and how +"done" is judged, and agnostic about the tools that measure it. > *A wright is a maker — a shipwright builds ships, a playwright builds plays. > A loopwright builds loops.* @@ -86,9 +88,11 @@ stack stays yours: loopwright owns `.loopwright/` and nothing else. get the engine, its 148 tests and its docs as a starting point to modify. Point `sources.roots` in `.loopwright/config.json` at your code and re-baseline. -## Your stack, not loopwright's +## Your toolchain is a detail -Each collector picks an adapter, and the installer detects which one you're on: +loopwright fixes the metric vocabulary and the verdict, never which tool +measures each metric — each collector picks an adapter, and the installer +detects which one you're on: | Collector | Adapters | |---|---| @@ -139,8 +143,11 @@ issue, a committed spec in `docs/specs/`, one commit per plan step, the PR conversation — so any session or human can pick up a half-finished task from the repo alone. -Two rules hold the whole thing together: **the gate is the only source of truth -for "done"**, and **merging is always the human's decision.** +Two rules hold the whole thing together: **"done" is the gate's verdict, +never the judgement of whoever merges**, and **an irreversible action +needs a human unless automating it has been explicitly opted into** — +loopwright ships no such opt-in today, so nothing in the harness merges. Both +are stated in full in `docs/loopwright/principles.md` (P2 and P8). ## Running it @@ -159,6 +166,7 @@ authoritative verdict. | Piece | Where | |---|---| +| Principles — what is fixed, what is swappable | `docs/loopwright/principles.md` | | Flow overview | `docs/loopwright/loop-harness.md` | | Gate design, metric by metric | `docs/loopwright/quality-gate.md` | | Gate engine + adapters | `.loopwright/scripts/` | @@ -170,7 +178,8 @@ authoritative verdict. ## Requirements -- Node ≥ 20.11 and a `package.json` (loopwright targets JS/TS repos) +- Node ≥ 20.11 and a `package.json` — the engine runs on Node, and + today's adapters measure a JS/TS repo - npm, pnpm or yarn — the engine and its CI workflow detect which from your lockfile - [`gh`](https://cli.github.com), authenticated - [Claude Code](https://claude.com/claude-code) locally, and the diff --git a/docs/loopwright/loop-harness.md b/docs/loopwright/loop-harness.md index 31fe2db..c642e72 100644 --- a/docs/loopwright/loop-harness.md +++ b/docs/loopwright/loop-harness.md @@ -2,7 +2,8 @@ How work flows through this repo when coding agents execute it. The quality gate (`docs/loopwright/quality-gate.md`) is the enforcement half; this is the -execution half. +execution half. The opinions both halves serve are stated in +`docs/loopwright/principles.md`. ## Flow @@ -38,10 +39,10 @@ Draft PR │ gate green, draft → promote to ready (triggers Claude review) │ review findings → fix or reply with reasons, never ignore ▼ -Ready PR, gate green, reviews addressed → human merges +Ready PR, gate green, reviews addressed → merge ``` -## Rules of the loop +## Mechanics - **Base branch**: a task with no open `blockedBy` always branches from `origin/main`. A task inside a feature-branch RFC branches from @@ -55,15 +56,10 @@ Ready PR, gate green, reviews addressed → human merges - **Draft until ready**: PRs open as drafts. The quality gate runs on drafts (cheap, mechanical signal for the babysitting loop); the Claude review workflow skips drafts and runs on `ready_for_review`. -- **The gate is the only source of truth for "done".** No agent may override - a red verdict, regenerate `.loopwright/baseline.json`, or take any of the - gated shortcuts listed in `CLAUDE.md`. -- **State lives in artifacts, not sessions**: the refined RFC and its comment - trail on the issue, the committed spec in `docs/specs/`, per-step commits, - and the PR conversation. Any session (or human) can pick up a half-done - task from these alone — except the grill phase itself, whose task spine and - drafts are session-scoped, so a crashed grill restarts. -- **Merging is always the human's decision.** +- **The grill phase is session-scoped.** `/grill-rfc` holds its task spine + and its sub-issue drafts in the session that runs it: the spine is a native + task list, the drafts are `draft-.md` files in a scratch directory, + and neither is committed. A crashed grill restarts. - **Architecture is scaffolding, not a spec.** The architect phase's mermaid lives in an RFC issue comment and never in the issue body, because `planner.md` reads the body via `gh issue view` and comments are not @@ -76,6 +72,18 @@ Ready PR, gate green, reviews addressed → human merges it reports `no change` across three RFCs, delete it and rely on the ledger's mandatory reasoned exclusion list alone. +## Opinions + +The opinions the loop runs on are stated once, in +`docs/loopwright/principles.md`, and this file does not restate them. Three +of them bear directly on the mechanics above: + +| Opinion | The question it settles | +|---|---| +| P1 | Where loop state lives | +| P2 | What counts as "done" | +| P8 | Who may perform an irreversible action, merge included | + ## Pieces | Piece | Where | diff --git a/docs/loopwright/principles.md b/docs/loopwright/principles.md new file mode 100644 index 0000000..bc89b05 --- /dev/null +++ b/docs/loopwright/principles.md @@ -0,0 +1,194 @@ +# Principles + +## Context + +The destination is near-autonomy, done properly rather than for hype: an +agent takes an issue to a merged change with a human stepping in only where +a human is genuinely needed. The opinions in this document are the +preconditions of that destination, not constraints on it. An agent that +merges on its own is only acceptable if "done" is a mechanical verdict that +cannot be bought. The gate as sole authority, the integrity signals and the +ratchet are what make that verdict mechanical. + +This document is descriptive. No skill consults it, no gate checks it, and +no RFC is required to carry a classification line. Its job is to name the +boundary between what is fixed and what is swappable, so the boundary stops +being re-argued case by case. + +An opinion is **core** when the rule has no knob. That is a statement about +the rule only. It does not say the rule is frozen, and it does not say the +action the rule governs has no knob: an action may be configurable when the +rule itself provides for it, as P8 does. + +How far autonomy may eventually reach is deliberately not declared here. +Declaring a limit would turn a precondition into a ceiling. Nothing in this +document may foreclose an alert-driven investigate, fix, deploy sequence, +and the limit is a decision to revisit only if an actual problem appears, +not a gap to fill pre-emptively. + +For how work flows from an issue to a merged change, see +`docs/loopwright/loop-harness.md`. For how a verdict is reached, see +`docs/loopwright/quality-gate.md`. + +## Core opinions + +- **P1 — Flow.** Work flows RFC issue, then task sub-issues, then PRs. State + lives in artifacts, never in session memory. The trigger is detail; the + flow is core. + - `rejects:` a loop that keeps task state in session memory instead of a + committed artifact, so a crashed session loses the work. + - `enforced by:` `.claude/skills/execute-issue/SKILL.md` +- **P2 — Done.** "Done" is determined by configured checks, never by the + judgement of whoever merges. A merge, autonomous or not, requires every + configured check green for the current diff and review findings resolved. + - `rejects:` a merge, autonomous or human, over a red configured check or + an unresolved review finding. + - `enforced by:` `.loopwright/scripts/quality-gate.mjs`, + `.claude/skills/babysit-pr/SKILL.md` +- **P3 — Ratchet.** Quality is a ratchet against a committed baseline, with + per-file debt grandfathered. The baseline is measured state: + machine-written, engine-schema'd, host-committed, never hand-edited, and + never regenerated to hide a regression. It is not the policy file P7 names. + - `rejects:` regenerating `.loopwright/baseline.json` to make a regression + disappear. + - `enforced by:` `.loopwright/scripts/lib/evaluate.mjs` +- **P4 — Integrity.** Green earned versus green bought — the integrity + signals — is a first-class concern, not an add-on. + - `rejects:` a gate that scores correctness and coverage but leaves the + integrity metrics out, so a suite made green with `.only` and empty + catches still passes. + - `enforced by:` `.loopwright/scripts/lib/collect-metrics.mjs` +- **P5 — Failure blocks.** Infrastructure failure blocks. Disabling a + configured check is itself a violation. + - `rejects:` switching a configured collector off, or letting a collector + that failed to run read as "no data" and merely warn. + - `enforced by:` `.loopwright/scripts/lib/evaluate.mjs` +- **P6 — Mediation.** Agents communicate and coordinate through a mediator, + never peer to peer. + - `rejects:` agents that message each other directly instead of through + the mediator that dispatched them. + - `enforced by:` +- **P7 — Policy.** All loopwright policy lives in one versioned file under + `.loopwright/` in the host repo, and the engine's defaults are frozen. + Changing what the gate demands is a PR in the host repo. + - `rejects:` a second policy file, an environment variable that changes a + verdict, or a loopwright update that moves a host's verdict without a + breaking release. + - `enforced by:` `.loopwright/scripts/lib/paths.mjs` +- **P8 — Irreversibles.** Irreversible actions require a human by default. + Automating one is an explicit opt-in in the host's config file. + - `rejects:` a harness that merges, tags, publishes or deploys without an + explicit opt-in entry in the host's config file. + - `enforced by:` `.claude/skills/babysit-pr/SKILL.md` +- **P9 — Test first.** Strict TDD: the test comes before the implementation. + - `rejects:` a step that writes the implementation first and backfills a + test that was never seen to fail. + - `enforced by:` `.claude/agents/coder.md` +- **P10 — Fresh review.** Fresh-context review: every diff is reviewed by an + agent that did not write it. + - `rejects:` the agent that wrote a diff reviewing its own diff. + - `enforced by:` `.claude/agents/reviewer.md` +- **P11 — Vertical slices.** A unit of work is a vertical slice, never a + horizontal layer. + - `rejects:` a unit of work that delivers a horizontal layer, such as + "define all the types", instead of a slice that ships on its own. + - `enforced by:` `.claude/agents/sub-issue-reviewer.md` +- **P12 — Vocabulary.** The metric vocabulary and its semantics are core and + closed. Connectors measure and never invent a key: per metric, a connector + reports a value for a core-owned key, or `unconfigured` (warns forever, + never blocks on its own), or `failed` (always blocks). + - `rejects:` a connector that reports a metric key core does not define. + - `enforced by:` `.loopwright/scripts/lib/collect-metrics.mjs` + +**The P2/P8 division of labour.** P2 decides what "done" is: every +configured check green for the current diff and review findings resolved. +P8 decides who may act on an irreversible action. Nothing may silently +remove the human from an irreversible action; who merges is a default, and +a configurable one. Which CI step may fail the build is mechanics and lives +in `docs/loopwright/quality-gate.md`. + +**The irreversibles.** Enumerated today: merge, publishing a tag or +release, and deploy. The list is not exhaustive. Everything else the +harness automates is reversible and needs no opt-in: a closed issue +reopens, `gh pr ready` can be undone, and comments can be edited. A version +bump is not irreversible: it lives in a file inside the PR, and the human +sees it in the same act as the merge. + +**The baseline is measured state.** `.loopwright/baseline.json` belongs to +P3. What separates it from the policy file is hand-editing: the policy file is +meant to be edited by hand, and the baseline never is. The host commits the +baseline and leaves its content to the engine. + +**An artifact exists before production.** An issue and a PR exist before +anything reaches production, because P1 requires the artifact. Without it +there is nowhere for a diagnosis to live, and a fix deployed without a +trail is green without evidence. + +**An empty enforcement pointer is a finding.** An entry whose +`enforced by:` line is empty names a gap. The gap is closed by building the +enforcement, never by writing a pointer to a file that does not enforce the +opinion. + +## Detail territory + +**Defaults are opinions too.** Every territory ships with one recommended +default, wired out of the box, the way framework starters ship `eslint`. A +default is never a requirement: there is always a documented way to swap it. +The one exception to the swap rule is T6, the engine's implementation +language, which has no swap route. + +| id | Territory | Seam | Default | +|---|---|---|---| +| T1 | Host language/ecosystem and the tools that measure it | `connector` | `js` | +| T2 | Stack detection and toolchain installation | `toolchain provider` | `mise`, never required | +| T3 | Where and how agents execute | `runtime` | local session | +| T4 | Thresholds and tolerances | — | already config-driven | +| T5 | Agent host | `agent-host` | Claude Code | +| T6 | Engine implementation language | — | Node | +| T7 | Release tool | `release` | `changesets`, internal, host-first | +| T8 | How work enters the pipeline | `intake` | hand-written RFC | + +A default is the recommended choice for that territory. Where the territory is +built, the default names what runs today. Where it has no implementation yet, +the default names what it will be built with. + +This document defines no contract for these territories. Each belongs to +its own RFC. T5 is the one exception. + +T6 is independent of T1: CI written in a different language than the project +is ordinary, so a Go host needing Node in CI is not coupling. + +**T5 is a contract of exactly four capabilities** the loop requires of its +agent host: + +1. Dispatch a subagent with fresh context and an explicit payload. +2. Restrict tools per role. +3. Choose a model tier per task. +4. An external reviewer that can write on the PR. + +Anything the loop needs beyond those four is leakage: it either enters the +contract as a core change, or it is a bug. + +Capability 2 is load-bearing for P10. A reviewer is read-only by being handed +no write tools, not by being told to behave. + +The engine has zero coupling here: nothing under `.loopwright/scripts/` +depends on Claude, agents, skills or sessions. + +## Classification rule + +If a change alters what counts as done, or how work flows, it is core. It +must be argued against the opinions above. + +If a change alters how a fact is measured, or where a step runs, it belongs +behind a contract, with a default implementation. + +## Amendments + +Changing a core opinion is an explicit amendment: a dated entry naming the +RFC that changed it. + +Ids are stable. Amendments never renumber. A withdrawn opinion stays in +place, and keeps its id, in the form `P4 (withdrawn, RFC #N)`. + +There are no amendments yet. diff --git a/docs/specs/issue-11.md b/docs/specs/issue-11.md new file mode 100644 index 0000000..a6af092 --- /dev/null +++ b/docs/specs/issue-11.md @@ -0,0 +1,607 @@ +# Issue #11 — Write docs/loopwright/principles.md + +RFC: #5 · Branch: task/11-principles-doc · Base: feat/rfc-5-core-principles + +## Spec + +Create one new file, `docs/loopwright/principles.md`, holding the descriptive +statement of loopwright's core opinions (P1–P12) and its detail territory +(T1–T8), plus the classification rule and the amendment mechanism. The doc is +the artifact RFC #5 exists to produce: #6, #7 and #8 already cite it textually, +and #12 and #13 are blocked on it existing. It gates nothing, no skill consults +it, and no RFC is required to carry a classification line — its job is to name +the boundary so it stops being re-argued. + +The source of truth for content is RFC #5's body (`gh issue view 5`): the +**Decision** section carries the P1–P12 table and the T1–T8 table, and the +**Detail territory** section carries T5's four capabilities and the T6/T7 notes. +Two things are **not** in those tables and must be authored or lifted from +prose: the twelve `rejects:` lines (the RFC mandates them but supplies none — +seeds are in Step 2 below), and the normative residue that lives only in the +RFC's *"Why the contested branches settled where they did"* prose. The rule for +that prose, stated by #11: a rule a reader must obey travels into the doc; the +story of how it was decided stays in the issue. + +This is documentation only. Nothing under `.loopwright/scripts/`, `.claude/` or +`.github/` changes, and no test exists or can exist for a markdown file — see +**How steps are verified** before Step 1. + +**The path and the id scheme are already promised.** RFC #5's close-out act has +been performed: #6, #7, #8, #9 and #10 each end with an identical `## Boundary +document` footer naming `docs/loopwright/principles.md` (task #11) and stating +that core opinions are cited as `P` and detail territory as `T`. The file +must land at exactly that path with exactly that id scheme, or five issues point +at nothing. #9's footer additionally carries a supersession note citing `P8` by +id — which is why the doc itself needs no citation of #9 (Step 3). + +### Acceptance criteria + +- [ ] `docs/loopwright/principles.md` exists and opens with the Context that + states near-autonomy as the destination and the core opinions as its + preconditions, not its constraints. +- [ ] P1–P12 are present as a list with stable ids, each carrying three things: + the assertion, a `rejects:` line naming one concrete change it refuses, + and an enforcement pointer that is a file name with no line number (left + empty where nothing enforces it). +- [ ] T1–T8 are present, each with its territory and its current default. Six + carry a named seam; T4 and T6 carry none. No contract is specified for + T1, T2, T3, T7 or T8, with T5 the one exception, whose four required + capabilities are enumerated in full. +- [ ] The classification rule and an `Amendments` section are present; the + amendment rule states that ids are stable, amendments never renumber, and + a withdrawn opinion stays as `P4 (withdrawn, RFC #N)`. +- [ ] P7 names the policy file only. `baseline.json` is described as measured + state belonging to P3, never as policy. +- [ ] P12's assertion carries the three-valued connector contract — a value for + a core-owned key, `unconfigured`, `failed` — with each state's verdict + stated (`unconfigured` warns and never blocks; `failed` always blocks). +- [ ] Read against #6, #7, #8, #9 and #10, the doc contradicts none of them + except the #9 version-bump contradiction, which the RFC states is + deliberate. +- [ ] Style matches the two siblings in `docs/loopwright/` (see **Style + contract**), and the quality gate is green. + +### Out of scope + +- Any change to `.loopwright/scripts/`, `.loopwright/tests/`, `.claude/` or + `.github/`. RFC #5's non-goals forbid an engine or workflow change. +- Any change to `docs/loopwright/loop-harness.md` (that is #12) or to + `README.md`, `CLAUDE.md`, `.loopwright/claude-md-section.md`, + `package.json` (that is #13). Do not touch them even to fix a reference. +- A **"Known violations" section**, and any mention of the known P5 divergence + at `.loopwright/scripts/quality-gate.mjs:97-99`. RFC #5 considered recording + it and rejected it explicitly. +- Defining any contract for T1, T2, T3, T4, T6, T7 or T8. T5's four + capabilities are the single exception the RFC resolves. +- **History**: which alternative the grill rejected, what the RFC said before + refinement, which RFC a decision contradicts, the names #9/#6/#8 as sources + of a decision. The doc carries rules, not provenance. +- Mermaid or any diagram. The four boundary diagrams on #5 are scaffolding; the + doc names seams and draws nothing. +- Appending the pointer to this doc into the bodies of #6–#10. RFC #5 makes + that a close-out act on the RFC, not part of this task. +- Naming the policy file's eventual filename (`config.yml` vs `settings.yml`) — + that belongs to #8. P7 asserts "one file, under `.loopwright/`" only. + +## Context the coder needs + +### Style contract (measured from the siblings, not guessed) + +Both siblings were measured; these are facts, not preferences. + +| Rule | Evidence | +|---|---| +| Single `#` title, subject in sentence case | `loop-harness.md:1` `# Loop harness`, `quality-gate.md:1` `# Quality gate` | +| Short sentence-case `##` section names | `Flow`, `Rules of the loop`, `Pieces`, `Baselines`, `What is measured`, `Tuning` | +| No front matter, no date, no emoji, no author | neither sibling has any | +| Impersonal declarative voice, no "we"/"you should" | throughout; `quality-gate.md` uses "you" only in the reader-advice sense at `:53`, `:178` | +| Tables for anything enumerable | `loop-harness.md:81-88`, `quality-gate.md:82-87`, `:157-165` | +| Bold lead-in term in list items | `loop-harness.md:46` `- **Base branch**: …`, `quality-gate.md:144` `**Coverage** — …` | +| Every path, filename, tool and metric name backticked | throughout both | +| Prose wraps at ~78 chars | max prose line is 80 chars (`quality-gate.md:59`, `:101`, `:111`); 78 is the target | +| Table rows and fenced code blocks are exempt from the wrap | table rows reach 98 chars (`quality-gate.md:163`) | +| Cross-reference the sibling docs by path | `loop-harness.md:4` cites `docs/loopwright/quality-gate.md` | + +Count characters, not bytes: an em dash is three bytes, so `awk 'length>78'` +over-reports. Use the Python check given in each step's **Verify**. + +### Section spine (decided here so the coder does not dither) + +``` +# Principles +## Context +## Core opinions +## Detail territory +## Classification rule +## Amendments +``` + +`## Context` is the **first** section with no lead paragraph before it, because +AC 1 requires the doc to open with the Context. The sibling convention of an +opening cross-reference is satisfied inside Context, which cites +`docs/loopwright/loop-harness.md` and `docs/loopwright/quality-gate.md`. The +rationale prose for the opinions sits as paragraphs **below** the P1–P12 list, +inside `## Core opinions`, per the RFC's Form rule ("Rationale goes in prose +below the list") — no sixth section for it. + +### Entry form for P1–P12 (fixed, so AC 2 is greppable) + +```markdown +- **P1 — Flow.** + - `rejects:` + - `enforced by:` `` +``` + +The `rejects:` and `enforced by:` tokens are literal and backticked, on their +own nested bullet, exactly twelve times each. Where nothing enforces an +opinion, the line is `- \`enforced by:\`` with nothing after it — the gap stays +visible, because RFC #5 states an empty pointer is itself a finding. Pointers +are file paths with **no line number**. + +### Enforcement pointers (verified by reading each file — do not invent) + +| id | `enforced by:` | What was read | +|---|---|---| +| P1 | `.claude/skills/execute-issue/SKILL.md` | spec committed at `:140`, "Session memory does not" at `:123` | +| P2 | `.loopwright/scripts/quality-gate.mjs`, `.claude/skills/babysit-pr/SKILL.md` | the gate decides the checks half; babysit-pr `:48-56` enforces the review half | +| P3 | `.loopwright/scripts/lib/evaluate.mjs` | the ratchet, `evaluateMetric` at `:87`, grandfathering at `:183-219` | +| P4 | `.loopwright/scripts/lib/collect-metrics.mjs` | `INTEGRITY_METRICS` at `:290-297` | +| P5 | `.loopwright/scripts/lib/evaluate.mjs` | `failed` → BLOCK at `:112-113`; `collectorRegressions` at `:150-162` ("disabling a tool is not a way to pass the gate") | +| P6 | *(empty)* | nothing in `.loopwright/scripts/` or `.claude/` enforces mediation; grep for "mediat"/"peer to peer" returns nothing | +| P7 | `.loopwright/scripts/lib/paths.mjs` | `CONFIG_PATH` is a single constant at `:11`, and `quality-gate.mjs:49` / `run-report.mjs:39` read only it | +| P8 | `.claude/skills/babysit-pr/SKILL.md` | `:56` "**Merging is the user's decision — never merge.**" | +| P9 | `.claude/agents/coder.md` | `:12` "## TDD loop (mandatory for plan steps)", `:15`, `:33` | +| P10 | `.claude/agents/reviewer.md` | frontmatter `:4` `tools: Read, Grep, Glob, Bash` — no Write/Edit, though Bash is write-capable; the three sibling reviewers carry `Read, Grep, Glob` only | +| P11 | `.claude/agents/sub-issue-reviewer.md` | `:27-28` rejects a horizontal layer | +| P12 | `.loopwright/scripts/lib/collect-metrics.mjs` | `COLLECTOR_METRICS` at `:76` is the closed id map | + +Tie-break used: name the file that would **reject** a violation, not the file +that merely states the rule. That is why P11 points at +`sub-issue-reviewer.md` and not `grill-rfc/SKILL.md`, which also carries the +rule at `:273-274`. P2 is the one entry with two paths, because the opinion has +two clauses with two different enforcers; keep both, and see **Open questions**. + +Line numbers in the table above are for the coder's verification only. **None +of them appears in the doc.** + +### How steps are verified + +There is no test, because the deliverable is a markdown file and nothing +executes it. Inventing a vitest case that asserts on the text of a doc would be +a test with no subject, and the repo's collectors cannot see the file anyway: +`.loopwright/config.json` sets `sources.roots` to `.loopwright/scripts` and +`.loopwright/tests` with `extensions: ['.mjs']`, so no metric moves on a +markdown-only diff. + +Each step therefore substitutes a **check first**: a command that fails before +the step's change and passes after, plus a readthrough against the one +acceptance criterion the step owns. Run the check before writing, see it fail, +then make it pass. That is the same discipline as TDD with the only executable +surface available. + +### Commit hygiene — read before the first commit + +`coverage/` and `reports/` exist untracked in the repo root and are **not** in +`.gitignore` (only `.loopwright/reports/` is). They hold 28 generated +artifacts. Every commit in this plan must add explicit paths: + +```bash +git add docs/loopwright/principles.md # or docs/specs/issue-11.md +``` + +Never `git add -A`, never `git add .`, never `git add docs`. Verify with +`git status --short` after each commit that `?? coverage/` and `?? reports/` +are still untracked. + +## Implementation plan + +Steps 1–5 each add one complete section and leave the repo green by +construction (markdown is outside every collector's scope, proven above). Step +6 is the cross-RFC and style pass. Step 7 runs the gate once, because an +unconfigured-to-failed collector transition blocks for reasons unrelated to the +diff and must be caught before the PR opens. + +### Step 1: The file, its spine, and the Context (AC 1) + +- **Check first**: the file does not exist. + ```bash + test -f docs/loopwright/principles.md && echo PRESENT || echo ABSENT + ``` + Expected before: `ABSENT`. +- **Then**: create `docs/loopwright/principles.md` with the title `# Principles` + and the five `##` headings from **Section spine** in order, each followed for + now only by the Context body. Context must carry, in impersonal prose: + - Near-autonomy, done properly rather than for hype, as the **destination**, + and the opinions below as its **preconditions, not its constraints** — an + agent that merges on its own is only acceptable if "done" is a mechanical + verdict that cannot be bought. + - That the doc is **descriptive**: no skill consults it, no gate checks it, + no RFC carries a classification line. + - The definition of **core**: the rule has no knob — not that the rule is + frozen, and not that the action it governs has no knob (an action may be + configurable when the rule itself provides for it, as P8 does). + - That the limit of autonomy is deliberately not declared here. + - The cross-references: `docs/loopwright/loop-harness.md` for how work flows + and `docs/loopwright/quality-gate.md` for how a verdict is reached. +- **Files**: `docs/loopwright/principles.md` +- **Verify**: + ```bash + test -f docs/loopwright/principles.md && \ + grep -n '^#\{1,2\} ' docs/loopwright/principles.md + ``` + Exactly one `# ` line, then `## Context`, `## Core opinions`, + `## Detail territory`, `## Classification rule`, `## Amendments` in that + order. Then read the Context against AC 1: it must state near-autonomy as the + destination and the opinions as preconditions. A Context that only says "this + doc lists principles" fails the step. +- **Commit**: `docs: add principles doc skeleton and context` + +### Step 2: P1–P12 as a list with ids, `rejects:` and pointers (AC 2, AC 5, AC 6) + +- **Check first**: + ```bash + grep -c '`rejects:`' docs/loopwright/principles.md + grep -c '`enforced by:`' docs/loopwright/principles.md + ``` + Expected before: `0` and `0`. After: `12` and `12`. +- **Then**: under `## Core opinions`, write the twelve entries in the fixed + **Entry form**. Take each assertion from RFC #5's Decision table + (`gh issue view 5`) — its wording is authoritative; compress only to fit the + wrap, never change the claim. Three entries carry extra required content: + - **P7** names the policy file and *only* the policy file: all loopwright + policy lives in one versioned file under `.loopwright/` in the host repo, + the engine's defaults are frozen, and changing what the gate demands is a + PR in the host repo. P7 must not mention `baseline.json` and must not name + a filename (#8 owns the name). + - **P3** states that the baseline is **measured state**: machine-written, + engine-schema'd, host-committed, never hand-edited, and never regenerated + to hide a regression. Either P3's assertion or the prose in Step 3 must say + it is not the policy file P7 names; putting it in P3's assertion is the + safer reading of AC 5. + - **P12**'s assertion carries the full three-valued connector contract: a + connector reports, per metric, either a value for a core-owned key, or + `unconfigured`, or `failed`. `unconfigured` warns forever and never blocks; + `failed` always blocks. "Connectors measure, they never invent a key" alone + fails AC 6. + - Use the `enforced by:` column from the table above verbatim. +- **Seed `rejects:` lines** — the RFC mandates the line and supplies none, so + these are authored. Refine the wording to fit the wrap; do not weaken one + into a slogan. Each must name a change somebody could actually propose: + - P1: a loop that keeps task state in session memory instead of a committed + artifact, so a crashed session loses the work. (Keep this narrow: #7 permits + a local mailbox cache in front of GitHub, which stays the source of truth. + A line broad enough to forbid a cache would contradict #7.) + - P2: a merge — autonomous or human — over a red configured check or an + unresolved review finding. + - P3: regenerating `.loopwright/baseline.json` to make a regression + disappear. + - P4: a gate that scores correctness and coverage but leaves the integrity + metrics unconfigured, so a suite made green with `.only` and empty catches + still passes. + - P5: switching a configured collector off, or letting a collector that + failed to run read as "no data" and merely warn. (P5 may assert that this + **blocks**: `collectorRegressions` returns `status: STATUS.BLOCK` at + `.loopwright/scripts/lib/evaluate.mjs:158`. #8's open question 3 describes + today's behaviour as a PR-comment flag, which understates the code.) + - P6: agents that message each other directly instead of through the + mediator that dispatched them. + - P7: a second policy file, an environment variable that changes a verdict, + or a loopwright update that moves a host's verdict without a breaking + release. + - P8: a harness that merges, tags, publishes or deploys without an explicit + opt-in entry in the host's config file. + - P9: a step that writes the implementation first and backfills a test that + was never seen to fail. + - P10: the agent that wrote a diff reviewing its own diff. + - P11: a unit of work that delivers a horizontal layer — "define all the + types" — instead of a slice that ships on its own. + - P12: a connector that reports a metric key core does not define. +- **Files**: `docs/loopwright/principles.md` +- **Verify**: + ```bash + grep -c '^- \*\*P' docs/loopwright/principles.md # 12 + grep -c '`rejects:`' docs/loopwright/principles.md # 12 + grep -c '`enforced by:`' docs/loopwright/principles.md # 12 + grep -n 'baseline' docs/loopwright/principles.md # none inside P7 + grep -n 'unconfigured\|failed' docs/loopwright/principles.md # both inside P12 + grep -nE '\.(mjs|md|json):[0-9]' docs/loopwright/principles.md # must be empty + ``` + The last check is the AC 2 trap: a pointer with a line number fails. Then + read P7, P3 and P12 against AC 5 and AC 6. +- **Commit**: `docs: add the twelve core opinions with rejects lines` + +### Step 3: The rationale prose below the list (AC 5, AC 6 support) + +- **Check first**: + ```bash + grep -c 'division of labour\|decides' docs/loopwright/principles.md + ``` + Expected before: `0` for the P2/P8 paragraph's lead term (adjust the grep to + the lead-in actually chosen; the point is the paragraphs are absent). +- **Then**: below the twelve-entry list, still inside `## Core opinions`, write + the short rationale paragraphs with bold lead-ins. Exactly these, and nothing + that is history: + - **The P2/P8 division of labour.** P2 decides *what* "done" is: every + configured check green for the current diff and review findings resolved. + P8 decides *who may act* on an irreversible action. Nothing may silently + remove the human from an irreversible action; who merges is a default, and + a configurable one. Which CI step may fail the build is mechanics and lives + in `docs/loopwright/quality-gate.md`. + - **The irreversibles.** Enumerated today: merge, publishing a tag or + release, deploy — the list is not exhaustive. Everything else the harness + automates is reversible and needs no opt-in: closing an issue reopens, + `gh pr ready` de-promotes, comments edit. A version bump is not + irreversible: it lives in a file inside the PR and the human sees it in the + same act as the merge. **Do not** name #9 or say that this contradicts a + published RFC — that is the history that stays in the issue. + - **The baseline is measured state.** Reinforce, in one or two sentences, + that `.loopwright/baseline.json` belongs to P3 and is not the policy file + P7 names. + - **Defaults are opinions too.** One recommended default per territory, + wired out of the box, the way framework starters ship `eslint`; a default + is never a requirement, and there is always a documented way to swap it. + (This may instead open `## Detail territory` in Step 4 — pick one place, + not both.) + - **An artifact exists before production.** An issue and a PR exist before + anything reaches production, because P1 requires the artifact: without it + there is nowhere for a diagnosis to live, and a fix deployed without a + trail is green without evidence. + - **An empty enforcement pointer is a finding**, not a completed entry. +- **Files**: `docs/loopwright/principles.md` +- **Verify**: readthrough. The prose contains no sentence naming an issue + number as the source of a decision, no "originally", "was rejected", + "before the grill", or "contradicts". + ```bash + grep -niE 'rejected|originally|before the grill|contradict|alternative' \ + docs/loopwright/principles.md # expect no hits + ``` +- **Commit**: `docs: add the rationale prose under the core opinions` + +### Step 4: Detail territory T1–T8, with T5's four capabilities (AC 3) + +- **Check first**: + ```bash + grep -c '^| T[1-8] ' docs/loopwright/principles.md + ``` + Expected before: `0`. After: `8`. +- **Then**: under `## Detail territory`, reproduce the RFC's table — a table, + because it is enumerable — with the columns `id`, `Territory`, `Seam`, + `Default today`, taking all eight rows from RFC #5's Decision section. Then, + below it: + - **T4 and T6 have `—` in the Seam column.** The RFC omits them + deliberately. Inventing a seam name for either is a defect. + - **Strip the issue numbers** the RFC's table carries in its default column + (`js (#6)`, `mise, never required (#6)`, `container per #7`, + `changesets, internal, host-first`) — the default travels, the citation + does not. Keep the defaults themselves exactly. + - One line stating this doc defines **no** contract for these territories; + each belongs to its own RFC. + - **T5 is the one exception**, and its four capabilities are enumerated in + full, as a list: dispatch a subagent with fresh context and an explicit + payload; restrict tools per role; choose a model tier per task; an external + reviewer that can write on the PR. Add that anything the loop needs beyond + those four is leakage — it either enters the contract as a core change or + it is a bug; that capability 2 is load-bearing for P10, because a reviewer + is read-only by being handed no write tools, not by being told to behave; + and that the engine has zero coupling here — nothing under + `.loopwright/scripts/` references Claude, agents, skills or sessions. + - **T6 is independent of T1**: CI written in a different language than the + project is ordinary, so a Go host needing Node in CI is not coupling. One + line. Do not turn it into a contract. + - **T7: name the seam and the default, and stop there.** Write the seam + (`release`) and the default (changesets, internal to loopwright, + host-first — if the host already uses it, loopwright uses the host's). + **Do not** write the RFC's further claim that T7 therefore works on a + non-JS host because the engine is already Node. #9's own body says the + opposite — that `@changesets/cli` assumes a `package.json` and "conflicts + with the connectors RFC for non-JS hosts", and that the release tool "should + be pluggable" (knope elsewhere). Omitting the claim satisfies AC 3 ("no + contract is specified for T7") and avoids a second contradiction with #9 + that AC 7 does not sanction. See **Open questions**. + - **Do not add a T9.** #10's territory (first-time setup / installer UX) is + not in T1–T8, and AC 3 fixes the list at eight entries. The gap is recorded + in **Open questions**, not filled here. +- **Files**: `docs/loopwright/principles.md` +- **Verify**: + ```bash + grep -c '^| T[1-8] ' docs/loopwright/principles.md # 8 + grep -nE '^\| T(4|6) ' docs/loopwright/principles.md # Seam column is — + grep -n '#[6-9]\|#10' docs/loopwright/principles.md # no issue refs in the table + ``` + Then count the T5 capabilities: exactly four, no fifth. Read AC 3 line by + line against the section. +- **Commit**: `docs: add the detail territory and the agent-host contract` + +### Step 5: Classification rule and Amendments (AC 4) + +- **Check first**: + ```bash + grep -c 'withdrawn' docs/loopwright/principles.md + ``` + Expected before: `0`. After: at least `1`. +- **Then**: + - `## Classification rule`: if a change alters what counts as done, or how + work flows, it is core and must be argued against the opinions above. If it + alters how a fact is measured or where a step runs, it belongs behind a + contract, with a default implementation. + - `## Amendments`: changing a core opinion is an explicit amendment — a dated + entry naming the RFC that changed it. Ids are stable and amendments never + renumber. A withdrawn opinion stays in place as `P4 (withdrawn, RFC #N)`, + written exactly in that form as the example. State that there are no + amendments yet; do not fabricate an entry, and do not add a date anywhere + else in the doc. +- **Files**: `docs/loopwright/principles.md` +- **Verify**: + ```bash + grep -n 'P4 (withdrawn, RFC #N)' docs/loopwright/principles.md + grep -n '^## Classification rule' docs/loopwright/principles.md + grep -n '^## Amendments' docs/loopwright/principles.md + ``` + All three must hit. Read AC 4: stable ids, never renumber, and the withdrawn + form must all be stated, not implied. +- **Commit**: `docs: add the classification rule and the amendment mechanism` + +### Step 6: Cross-RFC consistency and style pass (AC 7, AC 8) + +- **Check first**: run the wrap check; it will report the prose lines the + drafting left long. + Save this as `/tmp/wrapcheck.py` and run `python3 /tmp/wrapcheck.py` — do not + inline it, the fence marker does not survive shell quoting: + + ```python + FENCE = chr(96) * 3 + path = 'docs/loopwright/principles.md' + inside = False + for n, line in enumerate(open(path, encoding='utf-8').read().split('\n'), 1): + if line.startswith(FENCE): + inside = not inside + continue + if inside or line.lstrip().startswith('|'): + continue + if len(line) > 80: + print(n, len(line), line[:50]) + ``` + Expected after the pass: no output. Table rows and fenced blocks are exempt + by the measured sibling convention. +- **Then**: + 1. **Confirm the four in-body citations land.** These were read out of the + five bodies; each one is a phrase an RFC already uses, so the doc has to + make it true: + + | Citation, verbatim from the RFC | What the doc must satisfy it with | + |---|---| + | #7: "The runtime is a detail behind a contract, **per the principles RFC**" | T3 is a territory with a named seam (`runtime`) and a default | + | #7: peer-to-peer messaging "**conflicts with the mediated principle**" | P6 is nameable as the mediated principle and is strong enough to reject peer-to-peer | + | #7: "consistent with the principle that **state never lives in session memory**" | P1's second clause says exactly that | + | #8: "the section boundaries mirror the principles RFC: **each feature is a detail with a default, and its section is where you swap it**" | every T-row carries a default, and "there is always a documented way to swap it" is present | + + #6 carries the defaults rule verbatim but uncited ("mise is the default + provider, the way eslint is the default linter that framework starters + ship with"), so "Defaults are opinions too" must be recognizable as the + same rule. #9 and #10 cite nothing in-body beyond their footers. + 2. **Check the two territory namings against the RFCs that own them.** #7's + T3 default is plain Docker and its non-goal is "Keeping git worktrees as a + supported isolation level" — so the doc must not describe a worktree as the + current default. #8 and #9 both write the config section as `changesets:`, + while #5's T7 row names the seam `release`; if the doc writes both, it must + be unambiguous that `release` is the seam and changesets is today's default + — never that `release` is a config key. + 3. **Read the doc against each of #6–#10 for any further contradiction.** The + RFCs are the facts; the doc adjusts to them. The one permitted + contradiction is #9 on who decides the version bump — leave it. Two known + near-misses that are **not** doc bugs and need no edit: #6's connector + contract is two-valued (a value or `unconfigured`) while P12 is + three-valued — `failed` is real in the engine + (`.loopwright/scripts/lib/evaluate.mjs:112-113`), so P12 adds a state + rather than contradicting #6; and P7's frozen defaults answer a question #8 + leaves open rather than restating it. Anything else found: fix the doc, + never the RFC, and report it. + 4. Fix the wrap, then sweep the style contract: no emoji, no front matter, no + date, no "we"/"I", every path and tool name backticked, bold lead-ins on + list items, section names short and sentence-case. +- **Files**: `docs/loopwright/principles.md` +- **Verify**: + ```bash + python3 -c "print(open('docs/loopwright/principles.md',encoding='utf-8').read().count('---'))" + grep -nE '\b(we|I|our)\b' docs/loopwright/principles.md # expect no hits + grep -n '^## ' docs/loopwright/principles.md # five sections, unchanged + ``` + Plus the wrap check above returning nothing, and a written note of what each + of #6–#10 was checked against. +- **Commit**: `docs: reconcile the principles doc with RFCs #6-#10` + +### Step 7: Gate run and final readthrough (all ACs) + +- **Check first**: the gate has not been run against this branch. + ```bash + node .loopwright/scripts/run-report.mjs --all && \ + node .loopwright/scripts/quality-gate.mjs + ``` +- **Then**: nothing, if the verdict is green. A markdown-only diff cannot move + a metric (`sources.roots` is `.loopwright/scripts` and `.loopwright/tests`, + `extensions: ['.mjs']`), so any blocker is pre-existing or environmental. + If it is red, read `.loopwright/reports/quality-gate.json` and report the + verdict to the orchestrator — do **not** touch `.loopwright/baseline.json`, + `.loopwright/config.json` or any engine file to make it green. Both are out + of scope for this task. +- **Files**: none. `.loopwright/reports/` is gitignored; the untracked + `coverage/` and `reports/` directories the run refreshes must stay + untracked. +- **Verify**: + ```bash + node .loopwright/scripts/quality-gate.mjs ; echo "exit=$?" + git status --short # only docs/ paths; coverage/ and reports/ still ?? + ``` + Exit `0`. Then a final readthrough of the whole file against all eight + acceptance criteria, in order, ticking each. +- **Commit**: nothing to commit unless the readthrough found a fix, in which + case `docs: ...` with `git add docs/loopwright/principles.md` only. + +## Risks and open questions + +- **The twelve `rejects:` lines are authored, not lifted.** RFC #5 mandates the + line and its Decision table supplies none. Step 2's seeds are derived from + the RFC's own prose, but the wording is the coder's, and the RFC's first risk + ("principles written too broadly stop deciding anything") lands here. The + reviewer should read each line against "could somebody actually propose + this change?". +- **P2 carries two enforcement pointers.** AC 2 says "a file name", singular, + but P2 has two clauses with two enforcers: the gate decides the checks half, + `.claude/skills/babysit-pr/SKILL.md:48-56` the review half. Dropping either + misdescribes the opinion. Flagged rather than resolved. +- **P7's "frozen defaults" is not enforced today.** + `.loopwright/scripts/detect-stack.mjs:104-121` *copies* + `config.default.json` into `config.json` once and refuses to overwrite — it + is not an overlay, so there are no live defaults to freeze. #8 owns that + change. P7 describes the decided state; its pointer + (`.loopwright/scripts/lib/paths.mjs`) honestly covers only the "one file" + clause. +- **P8 is the one non-descriptive line in a descriptive doc.** It describes the + decided state, and the opt-in knob it assumes does not exist. The doc should + not hedge it into vagueness, and should not claim the knob exists. +- **The version-bump consequence is resolved, not open.** AC 7 implies the doc + does contradict #9, so the consequence travels; the issue's rule means the + words "#9" and "contradicts" do not. Step 3 resolves it as "a version bump is + not an irreversible action", with no citation — and that is safe because #9's + own `## Boundary document` footer already carries the supersession note, + citing `P8` by id and giving the same reasoning ("a version bump is reversible + inside the PR — the human still sees it in the same act as the merge"). The + cross-reference lives on the issue, where the RFC put it. +- **T7 is the one place the RFC and #9 genuinely conflict, and the plan avoids + it rather than deciding it.** RFC #5's Detail territory prose asserts "T7 + works on a non-JS host because of T6 … changesets can be internal to + loopwright and still serve a Go host". #9's body says the opposite: that + `@changesets/cli` assumes a `package.json`, "conflicts with the connectors + RFC for non-JS hosts", and that "the *release tool* that consumes it should be + pluggable (changesets on JS, a single-binary tool such as knope elsewhere)". + Under #5's own one-way consistency rule the RFCs are the facts, and AC 7 + sanctions only the version-bump contradiction — so Step 4 omits the claim and + writes the seam and default only, which AC 3 independently requires. **This is + the one item the orchestrator may want to rule on before Step 4 runs.** +- **P8 versus #9's release workflow — a second P8/#9 tension, not planned + around.** P8's enumerated irreversibles include "publishing a tag or + release"; #9 has a CI workflow that "creates a git tag and GitHub release" + and never mentions an opt-in. It may not be a real conflict — changesets' + usual shape is a release PR a human merges, which satisfies P8 through the + merge — and #9 is not yet grilled. P8's wording comes from the RFC's + authoritative Decision table and must not be weakened to dodge this, so the + plan states P8 as given and records the tension here. +- **`settings.yml` is a misnomer in the RFC, and the plan sidesteps it.** RFC + #5's open question 3 calls #8's filename alternatives "`config.yml` vs + `settings.yml`"; #8's actual open question 2 is "`config.yml` or + `loopwright.yml`". Out of scope already forbids the doc from naming the file + at all, which is why this cannot leak in. +- **A ninth territory is visible but must not be added.** #10's subject — + first-time setup / installer UX — is not in T1–T8, and #10 claims no seam of + its own (it consumes T2's bundled `mise`). AC 3 fixes the list at eight, so + Step 4 forbids a T9. Whether the RFC's territory list is complete is a + question for #5, not for this task. +- **P7's freeze is a new decision presented as a settled one.** RFC #5 says P7 + "settles #8's open question 1"; #8 leaves that question open and leans the + other way for the gate section ("a middle ground: overlay for feature + sections, full copy for `gate:`"), calling it "the decision that matters most + here". P7's assertion is still the RFC's to make — the doc states it without + claiming #8 agreed. +- **`# Principles` as the title** is chosen to match the siblings' pattern + (subject in sentence case, no subtitle). RFC #5's own title is longer; the + file is `principles.md`. diff --git a/docs/specs/issue-12.md b/docs/specs/issue-12.md new file mode 100644 index 0000000..35cd287 --- /dev/null +++ b/docs/specs/issue-12.md @@ -0,0 +1,576 @@ +# Issue #12 — Strip opinions from loop-harness.md, leaving mechanics + +RFC: #5 · Branch: task/12-strip-harness-opinions · Base: feat/rfc-5-core-principles + +## Spec + +`docs/loopwright/loop-harness.md:44-77` carries eight bolded "Rules of the +loop". Three of them assert what `docs/loopwright/principles.md` now asserts +as P1, P2 and P8. Two lists of rules is two sources of truth, and the one +nobody updates starts lying. This task deletes those three from +`loop-harness.md` and replaces them with a reference to the principle that +owns them. The five that remain are harness mechanics and stay as they are. + +**This task writes no principle.** `principles.md` landed with #11 and is the +sole author of P1–P12; it is off-limits here, and so is every file #13 owns. +The change is one-directional: text leaves `loop-harness.md` and a pointer +arrives. Nothing is copied in the other direction, and no sentence is +authored here that a reader could mistake for a principle. + +It also reconciles the one place where `principles.md` changed a rule rather +than describing it. `loop-harness.md:66` states that merging is always the +human's decision, absolutely; P8 makes it an opt-in-automatable default. The +absolute claim appears twice in this file — at `:66` and at the end of the +`Flow` diagram at `:41` ("human merges") — and both go, because a bare +reference to P8 next to a surviving absolute statement of P8 is still two +sources of truth. + +### Acceptance criteria + +Copied verbatim from the issue body. + +- [ ] All eight rules at `loop-harness.md:44-77` are handled per this fixed + classification, with no rule left undecided: + + | Lines | Rule | Verdict | + |---|---|---| + | 46-49 | Base branch | mechanics — stays | + | 50-54 | Closing keywords | mechanics — stays | + | 55-57 | Draft until ready | mechanics — stays | + | 58-60 | The gate is the only source of truth for "done" | opinion **P2** — deleted, replaced by a reference | + | 61-65 | State lives in artifacts, not sessions | opinion **P1** — deleted, replaced by a reference | + | 66 | Merging is always the human's decision | opinion **P8** — deleted, replaced by a reference | + | 67-72 | Architecture is scaffolding, not a spec | mechanics — stays | + | 73-77 | `boundary-reviewer` is the first thing to cut | mechanics — stays | +- [ ] No rule that is an opinion remains stated in `loop-harness.md` — the file + references the principle by its id instead of repeating it, so no rule + exists in both files and no principle text is authored here. +- [ ] The merge rule at `loop-harness.md:66` is replaced by a **bare reference to + P8** — not by a restatement of it. `loop-harness.md` may still describe that + a merge step exists at the end of the flow, which is mechanics; it must not + state who holds the authority or under what condition, which is P8's. + Concretely: "See P8" and not "merging is the human's by default, automating + it is an opt-in" — the second sentence is principle text, and authoring it + here is what recreates the two-sources-of-truth problem this task exists to + remove. +- [ ] What remains in `loop-harness.md` still reads coherently on its own as the + execution half — base branch, closing keywords, draft-until-ready, the flow + diagram and the `Pieces` table are intact. +- [ ] `docs/loopwright/quality-gate.md:7-9` ("The one rule") is left untouched: + "only `quality-gate.mjs` can fail the build" is CI mechanics, not an + opinion, and P2 does not replace it. + +Short names used below: **AC1** classification table, **AC2** no opinion +stated / referenced by id, **AC3** bare P8 reference, **AC4** coherent +remainder, **AC5** `quality-gate.md` untouched. + +| AC | Completed by | Also touched by | +|---|---|---| +| AC1 | Step 2 | Step 1 | +| AC2 | Step 3 (the "referenced by id" half) | Steps 1, 2 (the "not stated" half) | +| AC3 | Step 3 (the reference) | Step 1 (the two removals) | +| AC4 | Step 3 | — | +| AC5 | Step 3 (verified) | standing constraint on every step | + +### Out of scope + +- **Every file but `docs/loopwright/loop-harness.md`.** #13 is executing in + parallel and owns `README.md`, `CLAUDE.md`, + `.loopwright/claude-md-section.md` and `package.json`. Do not edit them, + not even to fix a cross-reference this change makes stale. A set review + verified there is no file overlap, and that is what makes the parallel run + safe. Also off-limits: `.loopwright/baseline.json`, + `.loopwright/config.json`, `docs/loopwright/quality-gate.md`, + `docs/loopwright/principles.md`. +- **Any test.** See **There is no test** below. Do not add, edit or read + `.loopwright/tests/` for this task. +- **Writing, amending or paraphrasing any principle.** No step may add a + sentence to `loop-harness.md` that states what P1, P2 or P8 decides. Naming + the question a principle settles is the limit. +- **Adjudicating whether the grill phase conforms to P1.** RFC #5 put that + question out of scope for #11, so it is out of scope here. The grill-phase + fact re-homed in Step 2 is neither labelled an exception to P1 nor called a + violation of it. +- **The `Flow` diagram's structure and the `Pieces` table.** AC4 requires both + intact. The only diagram change in this plan is the three-word terminal at + `:41`, ruled on below. +- Rewriting the five surviving mechanics bullets. Their wording is not in + scope; only the three deletions, one re-homing and the pointer are. +- Running `/babysit-pr`, opening the PR, or committing this spec. The + orchestrator owns those. + +## Rulings carried in from the orchestrator + +These were decided before planning and are not re-argued by any step. + +**R1 — the exception clause is mechanics and stays.** `:64-65` today reads +"— except the grill phase itself, whose task spine and drafts are +session-scoped, so a crashed grill restarts". P1 admits no exception, and the +bullet that carried the clause is being deleted, so the clause is re-homed as +a behavioural fact about `/grill-rfc`: purely descriptive, no `except`, no +`may`, no `is allowed to`, asserting nothing about what should be. Step 2 +owns it. + +**R2 — line 66 becomes a bare reference.** `loop-harness.md` may describe +that a merge step exists at the end of the flow. It may not say who holds the +authority or under what condition. "See P8" is correct; "merging is the +human's by default, automating it is an opt-in" is principle text and is +forbidden. + +**R3 — `docs/loopwright/quality-gate.md:7-9` stays exactly as it is.** It is +CI mechanics, not an opinion, and `principles.md:107-108` already says so. + +## Design decisions this plan makes + +Three questions the issue leaves open. Each is settled here so the coder does +not dither, and each is justified from something measured in the repo. + +**The section is renamed `## Mechanics`.** "Rules of the loop" is the wrong +name for a list that is now five mechanics items and no rules of judgement: +the heading is an invitation to add the next rule here, which is how the +duplication this task removes came to exist. "Mechanics" is the word the +issue and RFC #5 use for what stays ("leaving behind only harness +mechanics"), so the vocabulary is the repo's own, not invented. It satisfies +the measured style for `##` names — one short sentence-case word, like +`Flow`, `Pieces`, `Baselines`, `Tuning`. Nothing links to the old anchor: +`rg 'rules-of-the-loop'` returns no hits, and the only live references to the +file are `CLAUDE.md:13` and `README.md:162`, both of which cite the path and +not a section (and both belong to #13 — leave them). + +**The three P-references live collected in one `## Opinions` section, not +inline.** Inline stubs would leave three half-bullets in a list whose point +is that it contains only mechanics, and a bullet reading "**Done** — see P2" +still occupies the slot where a rule used to be, which is what invites the +restatement back. One block is also the form the repo prefers for anything +enumerable: three ids and what each settles is a table +(`loop-harness.md:81-88`, `quality-gate.md:82-87`). The section sits between +the mechanics and `## Pieces` — after the list, because a reader who +remembers the merge rule being "in loop-harness" looks where the rules were. + +**The right-hand column names the question, never the answer.** This is what +keeps the block a reference rather than a restatement under R2. "Who may +perform an irreversible action, merge included" states no authority; "merging +is the human's by default" would. A reviewer can check the whole block with +one test: no cell asserts a rule. + +**The file gains a pointer to `principles.md` in its opening paragraph.** +`principles.md:29-31` already points here, so the link is one-directional +today. One navigational clause at `:3-5`, where the file already cites +`docs/loopwright/quality-gate.md`, makes it bidirectional. It is deliberately +redundant with `## Opinions`: the opening clause is navigation between the +three docs, the section is the specific redirect for the three rules. + +**`:41`'s "human merges" is softened to "merge".** R2 forbids stating who +holds the authority, and a diagram terminal reading "human merges" states it +as flatly as the deleted `:66` did — being inside a code fence does not make +a claim mechanics. Dropping the actor leaves the step, which is mechanics, +and the diagram stays a flow of artifact states: RFC issue, task sub-issue, +draft PR, ready PR, merge. The P8 pointer is **not** put inside the fence — +a cross-reference in an ASCII diagram is noise, and `## Opinions` already +carries it with "merge included" as the hook back to this line. + +**This is the one tension between two acceptance criteria.** AC4 asks for the +flow diagram "intact"; AC3 forbids stating who holds the authority. AC3 wins +for those three words. AC4's "intact" is read as "still present and still +describing the flow" — it is listed alongside "the `Pieces` table are +intact", i.e. neither was deleted — not as "byte-identical". Flagged here +because it is the one place a reviewer could reasonably read the two criteria +against each other. + +## Context the coder needs + +### There is no test + +Markdown is outside every collector's scope. `.loopwright/config.json` sets +`sources.roots` to `.loopwright/scripts` and `.loopwright/tests` with +`extensions: ['.mjs']`, so no metric moves on a markdown-only diff. +**Inventing a vitest case that asserts on a documentation file's text is +itself a defect** and the reviewer will flag it. Do not write one, and do not +touch `.loopwright/tests/`. + +Each step therefore carries a **check first** instead: a one-liner that fails +before the step's change and passes after. Run it, see the pre-state, make +the change, run it again. That is the same discipline as TDD against the only +executable surface this diff has. + +Every check is scoped to `docs/loopwright/loop-harness.md` by path on +purpose. **Never run these greps repo-wide** — this spec file quotes the +strings being deleted, so a repo-wide sweep reports them as still present +and is meaningless. + +### Style, measured from the file and its sibling + +| Rule | Evidence | +|---|---| +| Single `#` title, sentence case | `loop-harness.md:1`, `quality-gate.md:1` | +| Short sentence-case `##` names | `Flow`, `Pieces`, `Baselines`, `Tuning` | +| Tables for anything enumerable | `loop-harness.md:81-88`, `quality-gate.md:82-87` | +| Bold lead-in on list items | `loop-harness.md:46` `- **Base branch**: …` | +| Full-sentence bold lead with a period | `loop-harness.md:67`, `:73` — the form the two surviving grill notes use | +| Prose wraps at ~78 characters | longest prose line in the file is 78 | +| No front matter, no date, no emoji | neither doc has any | +| Impersonal declarative voice | throughout | +| Every path and tool name backticked | throughout | + +Count **characters**, not bytes: `→` and `—` are three bytes each, so +`awk 'length>78'` over-reports. Every line this plan adds was measured at 78 +or fewer characters; keep it that way if you reflow. + +### The grill-phase facts Step 2 asserts, and where they were read + +| Claim | Read at | +|---|---| +| The task spine is a native task list created per run | `.claude/skills/grill-rfc/SKILL.md:22-26` | +| It is session-scoped — "Keep it live for the rest of the session" | `.claude/skills/grill-rfc/SKILL.md:28` | +| Drafts are `draft-.md` files in a scratch directory | `.claude/skills/grill-rfc/SKILL.md:255-256` | +| The scratch directory is never committed | `.claude/skills/grill-rfc/SKILL.md:198-199` | + +Step 2 states only these four facts and their consequence. It invents +nothing, and none of these line numbers appears in the doc. + +### What is knowingly lost, and why that is accepted + +The deleted bullet at `:58-60` has a second sentence — "No agent may override +a red verdict, regenerate `.loopwright/baseline.json`, or take any of the +gated shortcuts listed in `CLAUDE.md`." The fixed classification deletes +`:58-60` whole, and that is settled input, so it goes with the rest. Two +consequences, both accepted: + +- The baseline clause is P3's, not P2's. A reader reaches it through + `## Opinions` ("the opinions the loop runs on are stated once, in + `principles.md`"), and `principles.md:48-54` states it in full. The + `## Opinions` table lists P1, P2 and P8 only, because the fixed + classification names exactly those three — do not add a P3 row. +- `loop-harness.md` loses its only pointer to `CLAUDE.md`'s "Do not do these" + list. `CLAUDE.md` still carries that list, and `CLAUDE.md:13` still points + back at this file. Restoring the pointer would mean editing `CLAUDE.md` or + re-authoring the sentence; the first is #13's file and the second is + principle text. Leave it. + +### Commit hygiene — read before the first commit + +`coverage/` and `reports/` exist untracked in the repo root and are **not** +in `.gitignore` (only `.loopwright/reports/` is). They hold 28 generated +artifacts, and running the gate refreshes them. Every commit in this plan +stages one explicit path: + +```bash +git add docs/loopwright/loop-harness.md +``` + +Never `git add -A`, never `git add .`, never `git add docs`. After each +commit run: + +```bash +git status --short +``` + +and confirm that the only paths it reports are this spec and +`docs/loopwright/loop-harness.md`, and that if `coverage/` or `reports/` +appear they appear as `??` and never staged. In a freshly cut worktree those +two directories do not exist until the gate runs, which is why the Step 3 +gate run comes after the last commit and not before it. Every commit message +ends with: + +``` +Co-Authored-By: Claude Opus 5 +``` + +### Base branch + +Base and PR target are both `feat/rfc-5-core-principles`. RFC #5 runs on a +feature branch because it has three sub-issues with `blocked-by` edges, #12 +is blocked by #11, and #11's work is already merged into that branch — +`principles.md` exists there and nowhere upstream, so basing on `main` would +make every reference this task adds dangle. The PR body says +`Part of #12`; the feature branch's own PR carries `Closes #5`. + +## Implementation plan + +Three steps. Step 1 is two mechanical deletions plus the diagram terminal. +Step 2 is the judgement-heavy re-homing, kept separate per the plan shape, +and owns the whole of the `:61-65` bullet because the clause it re-homes +comes out of that same bullet — splitting the deletion from the re-homing +would leave the repo transiently missing a true fact about the harness. +Step 3 is the pointer architecture and the branch-level readthrough. + +Edit by matching text, not by line number: Step 1 shifts every line below +`:57`. + +### Step 1: Delete the P2 and P8 rules and drop the actor from the diagram terminal (AC1 rows 58-60 and 66, AC3 removal half) + +- **Check first** (run all four; the fourth is a guard): + + ```bash + rg -q 'the only source of truth for "done"' docs/loopwright/loop-harness.md && echo STILL-P2 || echo P2-GONE + rg -q 'Merging is always' docs/loopwright/loop-harness.md && echo STILL-P8 || echo P8-GONE + rg -q 'Ready PR, gate green, reviews addressed → merge' docs/loopwright/loop-harness.md && echo TERMINAL-OK || echo TERMINAL-OLD + rg -c '^- \*\*' docs/loopwright/loop-harness.md + ``` + + Expected **before**: `STILL-P2`, `STILL-P8`, `TERMINAL-OLD`, `8`. + Expected **after**: `P2-GONE`, `P8-GONE`, `TERMINAL-OK`, `6`. + +- **Then**, three edits in `docs/loopwright/loop-harness.md`. + + 1. Delete these three lines (`:58-60`) entirely, leaving no stub bullet: + + ```markdown + - **The gate is the only source of truth for "done".** No agent may override + a red verdict, regenerate `.loopwright/baseline.json`, or take any of the + gated shortcuts listed in `CLAUDE.md`. + ``` + + 2. Delete this line (`:66`) entirely: + + ```markdown + - **Merging is always the human's decision.** + ``` + + 3. In the `Flow` fence, change the last line (`:41`) from: + + ``` + Ready PR, gate green, reviews addressed → human merges + ``` + + to: + + ``` + Ready PR, gate green, reviews addressed → merge + ``` + + Nothing else inside the fence changes — not a character. + +- **Files**: `docs/loopwright/loop-harness.md` +- **Verify**: the four checks above show the after-state. Then confirm the + deletions were clean and the diagram survived: + + ```bash + rg -n '^- \*\*' docs/loopwright/loop-harness.md + rg -n '^```' docs/loopwright/loop-harness.md + ``` + + The first prints six bullets, in this order: `Base branch`, `Closing + keywords only fire on the default branch`, `Draft until ready`, `State + lives in artifacts, not sessions`, `Architecture is scaffolding, not a + spec`, `` `boundary-reviewer` is the first thing to cut ``. The second + prints exactly two fence lines, still bracketing the `Flow` diagram. + +- **Commit**: stage the one path, then + `docs: delete the P2 and P8 rules from loop-harness (#12)` + +### Step 2: Re-home the grill-phase fact and delete the P1 rule (AC1 row 61-65, AC2 "not stated" half) + +This is R1. The clause at `:64-65` is harness mechanics about `/grill-rfc`, +re-stated as a fact and its consequence — not as an exception to anything. + +- **Check first**: + + ```bash + rg -q 'State lives in artifacts, not sessions' docs/loopwright/loop-harness.md && echo STILL-P1 || echo P1-GONE + rg -q 'except' docs/loopwright/loop-harness.md && echo EXCEPTION-LANGUAGE || echo DESCRIPTIVE + rg -q 'The grill phase is session-scoped' docs/loopwright/loop-harness.md && echo REHOMED || echo NOT-REHOMED + ``` + + Expected **before**: `STILL-P1`, `EXCEPTION-LANGUAGE`, `NOT-REHOMED`. + Expected **after**: `P1-GONE`, `DESCRIPTIVE`, `REHOMED`. + + The middle check works because `except` occurs exactly once in the file + today, at `:64`, and this step is the only one that removes it. + +- **Then**, one edit: replace this bullet (`:61-65` as numbered in the + issue) — + + ```markdown + - **State lives in artifacts, not sessions**: the refined RFC and its comment + trail on the issue, the committed spec in `docs/specs/`, per-step commits, + and the PR conversation. Any session (or human) can pick up a half-done + task from these alone — except the grill phase itself, whose task spine and + drafts are session-scoped, so a crashed grill restarts. + ``` + + — with this one, in the same position, which is immediately above + `- **Architecture is scaffolding, not a spec.**`: + + ```markdown + - **The grill phase is session-scoped.** `/grill-rfc` holds its task spine + and its sub-issue drafts in the session that runs it: the spine is a native + task list, the drafts are `draft-.md` files in a scratch directory, + and neither is committed. A crashed grill restarts. + ``` + + Why that position: it keeps all three grill-phase facts adjacent at the end + of the list — session scope, then the architecture note, then the + `boundary-reviewer` note — so the list reads as three loop mechanics + followed by three grill-phase mechanics. The alternative, putting it inside + the `Flow` fence's grill block, was rejected because AC4 requires the + diagram intact and a fence has no room for the three clauses without + reflowing the block. + + Three things the replacement must not do, and a reviewer will check each: + it must not contain `except`, `may`, `is allowed to` or `should`; it must + not name P1 or any principle; and it must not say the grill phase is an + exception to, or in violation of, anything. It asserts four facts read from + `.claude/skills/grill-rfc/SKILL.md` and one consequence, and nothing about + what ought to be. + +- **Files**: `docs/loopwright/loop-harness.md` +- **Verify**: the three checks show the after-state, plus + + ```bash + rg -c '^- \*\*' docs/loopwright/loop-harness.md + rg -n '\bmay\b|allowed|should' docs/loopwright/loop-harness.md + ``` + + First prints `6`. Second prints nothing at all — `may` occurred once in the + file, in the bullet Step 1 deleted, and this step adds no modal. + + Run that second command **at this step, not after Step 3**: Step 3 adds the + row `| P8 | Who may perform an irreversible action, merge included |`, which + is the one legitimate `may` in the finished file because it poses a question + rather than making a claim. Step 3's own sweep uses a narrower pattern for + exactly that reason. + +- **Commit**: stage the one path, then + `docs: re-home the grill-phase session-scope fact as mechanics (#12)` + +### Step 3: Rename the section, add the `## Opinions` reference and the top pointer (AC2, AC3, AC4, AC5) + +- **Check first**: + + ```bash + rg -q '^## Rules of the loop$' docs/loopwright/loop-harness.md && echo OLD-HEADING || echo RENAMED + rg -q '^## Opinions$' docs/loopwright/loop-harness.md && echo POINTER-SECTION || echo NO-POINTER-SECTION + rg -c 'docs/loopwright/principles.md' docs/loopwright/loop-harness.md + ``` + + Expected **before**: `OLD-HEADING`, `NO-POINTER-SECTION`, and the third + command exits non-zero printing `0`. + Expected **after**: `RENAMED`, `POINTER-SECTION`, `2`. + +- **Then**, three edits in `docs/loopwright/loop-harness.md`. + + 1. Extend the opening paragraph (`:3-5`). From: + + ```markdown + How work flows through this repo when coding agents execute it. The quality + gate (`docs/loopwright/quality-gate.md`) is the enforcement half; this is the + execution half. + ``` + + to: + + ```markdown + How work flows through this repo when coding agents execute it. The quality + gate (`docs/loopwright/quality-gate.md`) is the enforcement half; this is the + execution half. The opinions both halves serve are stated in + `docs/loopwright/principles.md`. + ``` + + 2. Rename the heading. From `## Rules of the loop` to `## Mechanics`. + + 3. Insert a new section between the last bullet of `## Mechanics` (the + `boundary-reviewer` one) and `## Pieces`, separated by one blank line on + each side: + + ```markdown + ## Opinions + + The opinions the loop runs on are stated once, in + `docs/loopwright/principles.md`, and this file does not restate them. Three + of them bear directly on the mechanics above: + + | Opinion | The question it settles | + |---|---| + | P1 | Where loop state lives | + | P2 | What counts as "done" | + | P8 | Who may perform an irreversible action, merge included | + ``` + + Verbatim. In particular the right-hand column names the question and + never the answer: a cell that said "the human merges by default" or "the + gate decides" would be principle text and would fail AC2 and AC3. Add no + fourth row, no prose after the table, and no sentence explaining what any + of the three decides. + +- **Files**: `docs/loopwright/loop-harness.md` +- **Verify**, in four parts. + + 1. The after-state of the three checks above. + + 2. The structure holds — the file is six mechanics bullets, five `##` + sections, one fence pair, and the `Pieces` table untouched: + + ```bash + rg -n '^#{1,2} ' docs/loopwright/loop-harness.md + rg -c '^- \*\*' docs/loopwright/loop-harness.md + rg -q '^\| Quality gate \| `.loopwright/scripts/quality-gate.mjs`' docs/loopwright/loop-harness.md && echo PIECES-OK || echo PIECES-BROKEN + ``` + + (`rg` takes `{1,2}` unescaped — `'^#\{1,2\} '` is `grep` syntax and + silently matches nothing here.) + + First prints `# Loop harness`, then `## Flow`, `## Mechanics`, + `## Opinions`, `## Pieces` in that order. Second prints `6`. Third + prints `PIECES-OK`. + + 3. No opinion remains stated anywhere in the file, and the wrap holds: + + ```bash + rg -n 'source of truth|State lives in artifacts|Merging is always|human merges|except' docs/loopwright/loop-harness.md + rg -n '\balways\b' docs/loopwright/loop-harness.md + python3 -c "print([(i,len(l)) for i,l in enumerate(open('docs/loopwright/loop-harness.md',encoding='utf-8').read().splitlines(),1) if len(l)>78 and not l.startswith('|')])" + ``` + + The first prints nothing. The second prints exactly two lines — the + `Base branch` bullet and the `Closing keywords` bullet — both mechanics, + both survivors, and neither an authority claim. The third prints `[]` + before and after; a non-empty list means reflow the line it names. The + `not l.startswith('|')` filter is load-bearing: `Pieces` rows `:84` and + `:87` are 97 and 92 characters today, and the style contract exempts + table rows from the wrap. Every prose and fence line in the file is + already inside 78, and this plan adds none that is not. + + 4. The scope held and AC5 is satisfied: + + ```bash + git diff --name-only origin/feat/rfc-5-core-principles + sed -n '7,9p' docs/loopwright/quality-gate.md + ``` + + The first prints `docs/loopwright/loop-harness.md`, plus + `docs/specs/issue-12.md` once the orchestrator has committed this spec + (an uncommitted spec is untracked and `git diff` will not list it) — + nothing else, and in particular not + `README.md`, `CLAUDE.md`, `.loopwright/claude-md-section.md`, + `package.json`, `docs/loopwright/principles.md`, + `docs/loopwright/quality-gate.md`, `.loopwright/config.json` or + `.loopwright/baseline.json`. The second prints `## The one rule`, a blank + line, and + **`` `.loopwright/scripts/quality-gate.mjs` is the only step allowed to fail the build.``** + unchanged. + + Then read `docs/loopwright/loop-harness.md` top to bottom once against AC4: + it must stand on its own as the execution half — the flow from RFC issue to + merge, the six mechanics, where the opinions live, and what the pieces are. + A reader who never opens `principles.md` must still be able to run the loop + from this file. + +- **Commit**: stage the one path, then + `docs: point loop-harness at the principles that own its opinions (#12)` +- **After the commit**, once per branch, confirm the gate is green for + reasons unrelated to this diff: + + ```bash + node .loopwright/scripts/run-report.mjs --all + node .loopwright/scripts/quality-gate.mjs + ``` + + A markdown-only diff moves no metric, so a red verdict here is + pre-existing — read `.loopwright/reports/quality-gate.json` and report it + to the orchestrator rather than changing `loop-harness.md` to chase it. + This run creates or refreshes the untracked `coverage/` and `reports/` + directories in the worktree root; stage neither, and do not add them to + `.gitignore` (that file belongs to nobody in this RFC's task set). It runs + last so that no commit in this plan is made with those directories + present. diff --git a/docs/specs/issue-13.md b/docs/specs/issue-13.md new file mode 100644 index 0000000..db31e88 --- /dev/null +++ b/docs/specs/issue-13.md @@ -0,0 +1,712 @@ +# Issue #13 — Describe loopwright by its principles, not its stack + +RFC: #5 · Branch: task/13-principles-first-identity · Base: feat/rfc-5-core-principles + +## Spec + +Four files currently write loopwright's identity through its stack and its +directory layout. This task rewrites each of them so the process opinion comes +first and the stack follows as a requirement rather than a definition. The +opinions already exist as prose: `docs/loopwright/principles.md` landed on this +branch from #11, and this task is the identity half of the same RFC — the +source repo would otherwise talk about principles while every adopter still +read a description by directory and test runner. + +The most consequential of the four is **`.loopwright/claude-md-section.md`**. +`install.sh:30` syncs it into every host repo on every run, and `install.sh:53-63` +splices it into that repo's `CLAUDE.md` between `` and +``. It is how loopwright introduces itself to everyone +who adopts it. This repo's own `CLAUDE.md:8-62` **is** that block — verified +byte-identical to `.loopwright/claude-md-section.md:1-55`, and the installer's +awk splice round-trips `CLAUDE.md` unchanged today. So those two edits are one +edit, applied twice, and the pair must stay identical (Step 3). + +Nothing factual is removed, with **exactly one exception**. Every *requirement* +fact survives — Node ≥ 20.11, the need for a `package.json`, npm/pnpm/yarn +support, the engine's test-runner command. They stop being the identity and +stay stated; see **Requirement facts that must survive**. The single exception +is `README.md:142-143`, where *"merging is always the human's decision"* is the +one line in this task whose **truth value** changes rather than its framing: +P8 makes the human's decision a default that a host can opt out of. That is a +correction, not a reframing, and it is the only one. Read together: the +requirement rule and the `:142-143` correction are not in tension, because +`:142-143` states no requirement. + +### Acceptance criteria + +- [ ] `README.md:3-6` leads with a named opinion, and the "vendor it into any + JS/TS repo" clause follows rather than defines. Passes the textual test + in **The test for "leads with the process opinion"**. +- [ ] `README.md:89` (`## Your stack, not loopwright's`) and `README.md:173` + (the parenthetical "loopwright targets JS/TS repos") no longer frame + identity as the stack, while remaining factually accurate about what is + required. `README.md:174` is untouched. +- [ ] `README.md:158-169` ("How it's wired") gains a row for + `docs/loopwright/principles.md`, placed before the `loop-harness.md` row. +- [ ] `README.md:142-143` is reconciled with P8: merge is the human's decision + **by default**, and automating an irreversible action is an explicit + opt-in. The gate half of that sentence survives as P2. No text claims a + config key that does not exist (see **Accuracy traps**). +- [ ] `.loopwright/claude-md-section.md` and the marker region of `CLAUDE.md` + both point at three docs with `principles.md` first, both describe + loopwright by what it is opinionated about before naming `.loopwright/`, + and the two remain byte-identical. +- [ ] `CLAUDE.md:1-6` — the preamble **above** ``, + which is unique to this repo and shared with nothing — no longer defines + the project by the directory its engine lives in. The test-runner fact + (`cd .loopwright && npx vitest run`) survives. +- [ ] `package.json:4` describes loopwright by its process opinions rather than + as a "quality layer for AI-assisted JS/TS repos". No other field of + `package.json` changes. +- [ ] No factual requirement is lost: Node ≥ 20.11, the need for a + `package.json`, and npm/pnpm/yarn support remain stated in `README.md`. +- [ ] The quality gate is green (pass with the two pre-existing warnings, see + Step 6). + +### Out of scope + +- **`docs/loopwright/loop-harness.md`. Do not open it for editing.** #12 is + executing against it in parallel right now; a set review confirmed that no + file is touched by both tasks, and that is the only reason parallel execution + is safe. Do not edit it even to fix a cross-reference — `README.md:162` + cites it by path, and the path does not change. +- `docs/loopwright/principles.md` (#11, already landed) and + `docs/loopwright/quality-gate.md`. Both are read-only input here. +- `install.sh` and `setup.sh`. The marker mechanism is correct as it stands. +- Anything under `.loopwright/scripts/`, `.loopwright/tests/`, `.claude/` or + `.github/`. RFC #5's non-goals forbid an engine or workflow change. +- `.loopwright/config.json` and `.loopwright/baseline.json`. +- Every field of `package.json` except `description`. Touching `scripts`, + `engines` or `dependencies` would be an engine change. +- README sections the acceptance criteria do not name: `:85-87` (template + use), `:111-123` (Package managers), `:125-140` (loop prose and diagram), + `:145-156` (Running it), `:174-178` (the other requirement bullets), + `:180-185` ("This repo runs on itself"), `:187-190` (License). `:174` in + particular carries no identity framing and needs no edit. +- Adding a `principles.md` pointer anywhere beyond the four named here: one + README table row, one README citation at `:142-143`, the three-doc list in + the shared block, one pointer in the `CLAUDE.md` preamble. Nothing more. +- Writing a test. See the next section — this is not an omission. +- Any `audit.ignore` entry, and any regeneration of `.loopwright/baseline.json`. + +## Context the coder needs + +### There is no test to write — every step has a check instead + +The deliverable is four prose/metadata edits. Nothing executes them, and no +collector can see them: `.loopwright/config.json` scopes `sources.roots` to +`.loopwright/scripts` and `.loopwright/tests` with `extensions: ['.mjs']`, so a +diff of markdown and one JSON string moves no metric. A vitest case asserting +on the text of `README.md` would be a test with no subject, and the gate would +be right to flag it. + +So each step substitutes a **check** for the test, with the same discipline: +run the check before writing, see it fail, make the change, see it pass. Every +check below was run against this branch, and the "expected before" lines are +recorded output, not guesses. + +Two environment facts about the checks, learned the hard way: + +- The `node -e` checks hardcode their file path **inside** the script. The + operand form (`node -e '…' FILE`) and any `process.argv` use is refused by + the worktree guard. Keep the path inline. +- `git | grep …` is also refused inside an isolated worktree. Every + check here either uses plain `git` with no pipe, or avoids `git` entirely. + +### The test for "leads with the process opinion" + +Three of the five sites are leads, and "principles first" has to be checkable +or two coders produce two different leads. The literal test, applied to each +rewritten lead: + +1. The lead's **first sentence names a concrete opinion** — something + loopwright requires or refuses about process, which a repo doing the + opposite would falsify. "A CI quality gate for repos where agents write the + code" is a category, not an opinion, and fails. So does "Opinionated about + process, agnostic about stack" — that is a claim *about* having opinions, + not an opinion. +2. The first sentence does **not** contain the stack/mechanism words for that + site before the opinion appears: `JS/TS` or `vendor` for `README.md` and + `package.json`; `vendor` or `.loopwright/` for `CLAUDE.md` and + `.loopwright/claude-md-section.md`. + +The tone to match is `docs/loopwright/quality-gate.md:3-5`, the one place in +the repo that already states purpose process-first: + +> A CI gate designed for agentic development: it blocks the merge, explains +> exactly what is wrong in the PR comment, and gives an agent enough +> structured detail to fix it and push again without a human in the loop. + +The check command below prints the extracted first sentence and scores both +halves. The token list is a **proxy** for part 1: a first sentence that +matches a token but asserts nothing still fails the readthrough, which is the +authoritative test. The printed sentence is the evidence for a reviewer. + +```bash +node -e 'const F="README.md",S=3,BAD=/js\/ts|vendor/i,OPI=/\b(done|verdict|merge|merging|earned|bought|ratchet|ratchets|baseline|review|reviewed|test first|TDD|judgement|judgment|irreversible)\b/i;const L=require("fs").readFileSync(F,"utf8").split("\n");let i=S-1,b=[];while(i` | nothing — unique to this repo | +| `CLAUDE.md:7` | `` | the marker `install.sh:53` matches | +| `CLAUDE.md:8-62` | the injected block | byte-identical to `.loopwright/claude-md-section.md:1-55` | +| `CLAUDE.md:63` | `` | the marker `install.sh:53` matches | + +The preamble is the worst identity-by-mechanism text in the repo: + +> Built from the loopwright project — this repo IS the product: the engine +> lives in `.loopwright/scripts/`, its tests in `.loopwright/tests/` +> (`cd .loopwright && npx vitest run` runs them). + +It gets its own step (Step 4), separate from the shared block (Step 3), because +editing the block does not touch it and a sync check cannot see it. + +### Requirement facts that must survive + +The final review checks this list. Each fact must still be stated somewhere +after the rewrite, with a check command that finds it. + +| Fact | Lives at | Check | +|---|---|---| +| Node ≥ 20.11 | `README.md:173` | `grep -n '20\.11' README.md` | +| A `package.json` is required | `README.md:173` | `grep -n -A8 '^## Requirements' README.md` | +| npm, pnpm and yarn all work | `README.md:174` (untouched), `:111-123` | `grep -n 'npm, pnpm' README.md` | +| The engine's tests run with `cd .loopwright && npx vitest run` | `CLAUDE.md:5` | `grep -n 'npx vitest run' CLAUDE.md` | +| The engine is at `.loopwright/scripts/`, its tests at `.loopwright/tests/` | `CLAUDE.md:4`, `README.md:182-183` | `grep -n '\.loopwright/tests/' CLAUDE.md` | +| JS/TS is what loopwright measures today | `README.md:4` and `:173` (2 hits now) | `grep -cn 'JS/TS' README.md` | +| The layer is vendored under `.loopwright/` | shared block | `grep -n 'loopwright/' .loopwright/claude-md-section.md` | +| What the layer is: RFC-driven issues, an agentic execution loop, a CI quality gate | shared block `:3-4` | readthrough | +| `gh`, Claude Code, the Claude GitHub App | `README.md:175-178` (untouched) | — | + +### Accuracy traps + +Three ways to write a true-sounding sentence that is false. A reviewer will +catch these, so do not write them. + +- **P8's opt-in knob does not exist yet.** RFC #5 records this in its Risks: + P8 "describes the decided state, not the current one, and the knob it assumes + does not exist yet." So `README.md:142-143` may state the rule — an + irreversible action needs a human unless automating it has been explicitly + opted into — but must not send a reader looking for a config key. Say that + loopwright ships no such opt-in today, so merge stays human. That is both P8 + and the truth. +- **Stack agnosticism is a boundary claim, not a shipped capability.** T1's + only connector today is `js`; #6 owns the seam and has not landed. No lead + may imply loopwright measures a non-JS repo today. "Agnostic about stack by + design, JS/TS in practice" is the honest shape. +- **The shared block is read in repos that are not this one.** It is spliced + into arbitrary host repos, so it says "this repo has / this repo is governed + by", never "loopwright itself". It may cite `docs/loopwright/principles.md`: + `install.sh:43` copies `docs/loopwright` file-by-file into the host, so the + pointer resolves there — including for a host that installed before #11 + landed, on its next install run. + +Two facts that remove worries rather than create them: `installer.test.sh:28` +and `:46` assert only that the markers exist and are not duplicated, never the +block's content, so no test depends on this text; and the installer's awk +splice (`install.sh:57-60`) round-trips `CLAUDE.md` byte-identically today, +which is what makes Step 3's propagation mechanical. + +### Style contract + +Measured from the files being edited, not guessed. + +| Rule | Evidence | +|---|---| +| Prose wraps at ~78 chars; 80 is the practical ceiling | `README.md` has 9 lines over 80, all code fences, the flow diagram, or two prose lines at `:108` (82) and `:174` (84); `claude-md-section.md` has 2, both backticked commands | +| Table rows and fenced code are exempt from the wrap | `README.md:130-133` reach 107 | +| Every path, filename, command and metric name is backticked | throughout all four files | +| Bold for the claim a paragraph turns on | `README.md:3`, `:21`, `:28`, `:142` | +| Sentence-case `##` headings | `Why`, `Install`, `The loop`, `Running it`, `How it's wired` | +| The shared block addresses the host repo impersonally | `claude-md-section.md:3` "This repo has…" | + +Do not reflow paragraphs that are not being changed — it inflates the diff and +makes the review harder than the change. + +### Commit hygiene — read before the first commit + +Root `coverage/` and `reports/` are **not** in `.gitignore` — it covers only +`node_modules/`, `.loopwright/reports/` and `.loopwright/node_modules/`. In the +main checkout those two directories exist untracked with 28 generated +artifacts. In a fresh worktree they may be absent, and **their absence is not a +finding**. Either way, a bare `git add -A` would stage any generated artifact a +collector run creates, so every commit stages explicit paths: + +```bash +git add README.md # Steps 1, 2 +git add .loopwright/claude-md-section.md CLAUDE.md # Step 3 +git add CLAUDE.md # Step 4 +git add package.json # Step 5 +``` + +**Never `git add -A`, never `git add .`, never `git add docs`.** After each +commit run plain `git status --short` and confirm it shows **no staged or +modified path other than the ones that step names**, and no generated artifact +staged. + +### Line numbers and anchors + +Every line number in this plan is as of this branch's HEAD before any change. +Step 1 changes the length of the README lead, so `:89`, `:142` and `:158` all +shift. **Locate every site by its quoted anchor text, not by number.** The +anchors: + +| # | Site | Anchor to match | Step | +|---|---|---|---| +| A | `README.md:3-6` | `**A CI quality gate for repos where agents write the code` | 1 | +| B | `README.md:89` | `## Your stack, not loopwright's` | 1 | +| C | `README.md:91` | `Each collector picks an adapter, and the installer detects` | 1 | +| D | `README.md:173` | `(loopwright targets JS/TS repos)` | 1 | +| E | `README.md:142-143` | `Two rules hold the whole thing together` | 2 | +| F | `README.md:162` | `| Flow overview | ` | 2 | +| G | `claude-md-section.md:3-7` + `CLAUDE.md:10-14` | `This repo has the loopwright layer vendored under` | 3 | +| H | `CLAUDE.md:3-5` | `Built from the loopwright project` | 4 | +| I | `package.json:4` | `"description": "Vendorable quality layer` | 5 | + +## Implementation plan + +Six steps. Steps 1–5 each own one file group, are independently checkable, and +leave the repo green by construction (markdown and one JSON string are outside +every collector's scope, proven above). Step 6 is the requirement sweep and the +one gate run. + +Step 3 runs before Step 4 on purpose: the preamble's job is to say what is true +of *this* repo and not already said by the shared block, so it is written +against the rewritten block. + +### Step 1: README identity leads — sites A, B, C, D (AC 1, AC 2) + +- **Check first**: + ```bash + node -e 'const F="README.md",S=3,BAD=/js\/ts|vendor/i,OPI=/\b(done|verdict|merge|merging|earned|bought|ratchet|ratchets|baseline|review|reviewed|test first|TDD|judgement|judgment|irreversible)\b/i;const L=require("fs").readFileSync(F,"utf8").split("\n");let i=S-1,b=[];while(i{const w=[...l].length;if(w>80&&!/^\s*[|>]/.test(l)){n++;console.log((i+1)+" ("+w+"): "+l)}});console.log("count: "+n)' + ``` + Expected: `count: 9` or fewer, and every listed line is one that was already + long before the change (code fences at `:74-78`, `:147-151`, the flow + diagram, `:108`, `:174`). A new prose line over 80 means the wrap slipped. +- **Commit**: `git add README.md` — + `docs: lead the README with the process opinion, not the stack (#13)` + +### Step 2: README loop section — sites E and F (AC 3, AC 4) + +- **Check first**: + ```bash + grep -n "always the human" README.md + grep -n "principles.md" README.md + ``` + Expected before: one hit for the first — + `143:for "done"**, and **merging is always the human's decision.**` — and + **no output** for the second. +- **Then**: + - **E — the "Two rules" sentence.** Rewrite it so the gate half states P2 + (what "done" is: the gate's verdict, not the judgement of whoever merges) + and the merge half states P8 (an irreversible action needs a human unless + automating it has been explicitly opted into). Cite + `docs/loopwright/principles.md` and name P2 and P8 — this is the README's + only prose pointer to the doc, and it is where a reader asks "says who?". + The phrase "always the human's decision" must not survive anywhere. + Per **Accuracy traps**, state that loopwright ships no opt-in today, so + merge stays human; do not name a config key. A shape that satisfies both: + ```markdown + Two rules hold the whole thing together: **"done" is the gate's verdict, + never the judgement of whoever merges**, and **an irreversible action + needs a human unless automating it has been explicitly opted into** — + loopwright ships no such opt-in, so nothing in the harness merges. Both + are stated in full in `docs/loopwright/principles.md` (P2 and P8). + ``` + Do not touch `:153-156`. "Only `quality-gate.mjs` can fail the build" is CI + mechanics, not the authority claim — `docs/loopwright/principles.md:107-108` + draws exactly that line, and it belongs where it is. + - **F — the "How it's wired" table.** Add one row, **first** in the table, + before `| Flow overview | \`docs/loopwright/loop-harness.md\` |`: + ```markdown + | Principles — what is fixed, what is swappable | `docs/loopwright/principles.md` | + ``` + The row's label may be reworded; its position and its path may not. The + other eight rows stay as they are. +- **Files**: `README.md` +- **Verify**: + ```bash + grep -n "always the human" README.md + ``` + Expected: no output (exit 1). + ```bash + grep -n -B2 -A6 "Two rules hold" README.md + ``` + Read the result: the gate half must claim the verdict decides "done", the + merge half must carry both the default and the opt-in, and neither may name + a config key that does not exist. + ```bash + grep -n "docs/loopwright/" README.md + ``` + Expected: the `principles.md` table row appears, and its line number is lower + than the `loop-harness.md` row's. +- **Commit**: `git add README.md` — + `docs: reconcile the README merge rule with P8 and wire in the principles doc (#13)` + +### Step 3: the shared block — site G, in both files at once (AC 5) + +- **Check first**: + ```bash + node -e 'const F=".loopwright/claude-md-section.md",S=3,BAD=/vendor|\.loopwright\//i,OPI=/\b(done|verdict|merge|merging|earned|bought|ratchet|ratchets|baseline|review|reviewed|test first|TDD|judgement|judgment|irreversible)\b/i;const L=require("fs").readFileSync(F,"utf8").split("\n");let i=S-1,b=[];while(i$/{f=1;next} /^$/{f=0} f' CLAUDE.md > "$TMPDIR/lw-block.md" + diff "$TMPDIR/lw-block.md" .loopwright/claude-md-section.md && echo IN-SYNC + ``` + Expected before: `opinion in first sentence: FAIL`, + `mechanism word before it: FAIL`, then no diff output and `IN-SYNC` — the + two files start identical and must end identical. +- **Then**: edit **`.loopwright/claude-md-section.md` only**, then propagate. + - Rewrite `:3-7` so the first sentence names the opinions and the + `.loopwright/` location follows. The paragraph must still carry every fact + it carries today: what the layer is (RFC-driven issues, an agentic + execution loop, a CI quality gate), that it is vendored under + `.loopwright/`, and the doc pointers — now **three**, with + `docs/loopwright/principles.md` first, then `quality-gate.md` ("how the + gate works" / how a verdict is reached), then `loop-harness.md` (how work + flows from an RFC issue to a merged PR, with the `/grill-rfc`, + `/execute-issue` and `/babysit-pr` skills). A shape that passes: + ```markdown + ## loopwright + + In this repo, "done" is a mechanical verdict from configured checks, never + the judgement of whoever merges: quality is a ratchet against a committed + baseline, and a green build has to be earned rather than bought. Read + `docs/loopwright/principles.md` for those opinions and what each one + refuses, `docs/loopwright/quality-gate.md` for how a verdict is reached, + and `docs/loopwright/loop-harness.md` for how work flows from an RFC issue + to a merged PR (the `/grill-rfc`, `/execute-issue`, and `/babysit-pr` + skills). The layer that enforces it — RFC-driven issues, an agentic + execution loop and a CI quality gate — is vendored under `.loopwright/`. + ``` + It addresses a host repo, not this one (see **Accuracy traps**). + - Leave `:9-55` — Commands, Working on a PR, Do not do these — untouched. + They are mechanics and instructions, not identity. + - Propagate into `CLAUDE.md` with the installer's own splice, which is + `install.sh:57-60` verbatim and is the reason the two stay byte-identical: + ```bash + awk -v s='' -v e='' -v f='.loopwright/claude-md-section.md' ' + $0==s {print; while ((getline line < f) > 0) print line; skip=1; next} + $0==e {print; skip=0; next} + !skip {print}' CLAUDE.md > CLAUDE.md.tmp && mv CLAUDE.md.tmp CLAUDE.md + ``` + `CLAUDE.md.tmp` must not survive the command; if it does, the `mv` did not + run and `CLAUDE.md` is unchanged. Do not hand-edit the marker region. +- **Files**: `.loopwright/claude-md-section.md`, `CLAUDE.md` +- **Verify**: + ```bash + node -e 'const F=".loopwright/claude-md-section.md",S=3,BAD=/vendor|\.loopwright\//i,OPI=/\b(done|verdict|merge|merging|earned|bought|ratchet|ratchets|baseline|review|reviewed|test first|TDD|judgement|judgment|irreversible)\b/i;const L=require("fs").readFileSync(F,"utf8").split("\n");let i=S-1,b=[];while(i$/{f=1;next} /^$/{f=0} f' CLAUDE.md > "$TMPDIR/lw-block.md" + diff "$TMPDIR/lw-block.md" .loopwright/claude-md-section.md && echo IN-SYNC + ``` + Expected: no diff output, then `IN-SYNC`. This is the step's load-bearing + check — if it prints a diff, the two copies have drifted and the step is not + done. + ```bash + grep -n "principles.md\|quality-gate.md\|loop-harness.md" .loopwright/claude-md-section.md + grep -c "loopwright:start" CLAUDE.md + ``` + Expected: the three doc paths in that order, `principles.md` first; and + exactly `1` start marker. + ```bash + node -e 'const L=require("fs").readFileSync(".loopwright/claude-md-section.md","utf8").split("\n");let n=0;L.forEach((l,i)=>{const w=[...l].length;if(w>80){n++;console.log((i+1)+" ("+w+"): "+l)}});console.log("count: "+n)' + ``` + Expected: `count: 2`, both in the untouched body (`The \`Quality gate\` + workflow…` and the re-run command). A third entry means the new lead does not + wrap. +- **Commit**: `git add .loopwright/claude-md-section.md CLAUDE.md` — + `docs: introduce loopwright to host repos by its opinions first (#13)` + +### Step 4: the `CLAUDE.md` preamble — site H (AC 6) + +This is a **different site** from Step 3, above the `` +marker, shared with nothing, and the one a review already missed once. + +- **Check first**: + ```bash + sed -n '1,8p' CLAUDE.md + node -e 'const F="CLAUDE.md",S=3,BAD=/vendor|\.loopwright\//i,OPI=/\b(done|verdict|merge|merging|earned|bought|ratchet|ratchets|baseline|review|reviewed|test first|TDD|judgement|judgment|irreversible)\b/i;const L=require("fs").readFileSync(F,"utf8").split("\n");let i=S-1,b=[];while(i$/{f=1;next} /^$/{f=0} f' CLAUDE.md > "$TMPDIR/lw-block.md" + diff "$TMPDIR/lw-block.md" .loopwright/claude-md-section.md && echo IN-SYNC + ``` + Expected: `IN-SYNC` still. This proves the preamble edit did not reach into + the shared block. +- **Commit**: `git add CLAUDE.md` — + `docs: define this repo by the opinions it is held to, not its directories (#13)` + +### Step 5: `package.json` description — site I (AC 7) + +- **Check first**: + ```bash + cp package.json "$TMPDIR/pkg-base.json" + node -e 'const d=JSON.parse(require("fs").readFileSync("package.json","utf8")).description;const OPI=/\b(done|verdict|merge|earned|bought|ratchet|ratchets|baseline|review|judgement|judgment|irreversible)\b/i;const s=(d.match(/^.*?[.!?:](?=\s|$)/)||[d])[0];console.log("DESC : "+d+"\nlength: "+d.length+"\nFIRST : "+s+"\nopinion first: "+(OPI.test(s)?"PASS":"FAIL")+"\nstack word before it: "+(/js\/ts|vendor/i.test(s)?"FAIL":"PASS"))' + ``` + Expected before, exactly: + ``` + DESC : Vendorable quality layer for AI-assisted JS/TS repos: RFC-driven issue flow, agentic execution loop, and a CI quality gate that agents cannot cheat. + length: 148 + FIRST : Vendorable quality layer for AI-assisted JS/TS repos: + opinion first: FAIL + stack word before it: FAIL + ``` + The `cp` is part of the check: it is the baseline the verify step compares + against, and it avoids piping `git` into anything. +- **Then**: replace the value of `description` and nothing else. Constraints: + - the first clause (up to the first `.`, `!`, `?` or `:`) names a concrete + process opinion and contains neither `JS/TS` nor `vendor`; + - the facts that survive: an RFC-driven issue flow, an agentic execution + loop, a CI quality gate, and JS/TS as what it measures today; + - one line, ≤ 220 characters (it is 148 today), valid JSON; + - **no `"` inside the string.** Escaped quotes in a `package.json` + description are a needless hazard; leave `done` unquoted. + + A value that passes: + ``` + Done is a mechanical verdict, not a judgement call: quality ratchets against a committed baseline and green must be earned — RFC-driven issue flow, agentic execution loop and CI quality gate for JS/TS repos. + ``` +- **Files**: `package.json` +- **Verify**: + ```bash + node -e 'const d=JSON.parse(require("fs").readFileSync("package.json","utf8")).description;const OPI=/\b(done|verdict|merge|earned|bought|ratchet|ratchets|baseline|review|judgement|judgment|irreversible)\b/i;const s=(d.match(/^.*?[.!?:](?=\s|$)/)||[d])[0];console.log("DESC : "+d+"\nlength: "+d.length+"\nFIRST : "+s+"\nopinion first: "+(OPI.test(s)?"PASS":"FAIL")+"\nstack word before it: "+(/js\/ts|vendor/i.test(s)?"FAIL":"PASS"))' + ``` + Expected: both `PASS`, `length` ≤ 220. The `JSON.parse` succeeding is also + the JSON-validity check. + ```bash + node -e 'const fs=require("fs"),a=JSON.parse(fs.readFileSync(process.env.TMPDIR+"/pkg-base.json","utf8")),b=JSON.parse(fs.readFileSync("package.json","utf8"));delete a.description;delete b.description;console.log(JSON.stringify(a)===JSON.stringify(b)?"PASS: only description differs":"FAIL: another field changed")' + ``` + Expected: `PASS: only description differs`. Because `JSON.stringify` of a + parsed object preserves key order, this also catches a reordering of + `scripts`, `engines` or `devDependencies`. + ```bash + git diff -U0 -- package.json + ``` + Expected: exactly one `-` line and one `+` line, both the `"description":` + line. Plain `git diff` with no pipe — the piped form is refused in a + worktree. +- **Commit**: `git add package.json` — + `chore: describe loopwright by its process opinions (#13)` + +### Step 6: requirement sweep, sync re-check, and the one gate run + +No file changes unless the sweep finds a dropped fact. + +- **Check first**: nothing to fail — this step verifies the previous five. +- **Then**: run the sweep. If a fact is missing, restore it in the file that + owned it and amend that file's commit (or add a fixup commit); do not move a + fact to a different file to make a grep pass. +- **Files**: none expected. +- **Verify**: + ```bash + grep -n "20\.11" README.md + grep -n -A8 "^## Requirements" README.md + grep -n "npm, pnpm" README.md + grep -cn "JS/TS" README.md + grep -n "npx vitest run" CLAUDE.md + grep -n "\.loopwright/tests/" CLAUDE.md + grep -n "principles.md" README.md .loopwright/claude-md-section.md CLAUDE.md + ``` + Expected: every row of **Requirement facts that must survive** accounted + for; `principles.md` found in all three files. + ```bash + awk '/^$/{f=1;next} /^$/{f=0} f' CLAUDE.md > "$TMPDIR/lw-block.md" + diff "$TMPDIR/lw-block.md" .loopwright/claude-md-section.md && echo IN-SYNC + ``` + Expected: `IN-SYNC`. + ```bash + git status --short + ``` + Expected: no staged or modified path beyond the five this task owns, and no + generated artifact staged. Root `coverage/` and `reports/` may legitimately + not exist here — they are absent in a fresh worktree, and that absence is not + a finding; if they do appear, they must stay untracked. + ```bash + git diff --stat origin/feat/rfc-5-core-principles...HEAD + ``` + Expected: exactly five paths — `README.md`, `CLAUDE.md`, + `.loopwright/claude-md-section.md`, `package.json`, `docs/specs/issue-13.md`. + **`docs/loopwright/loop-harness.md` must not appear.** If it does, Step 1–5 + reached into #12's file and the change must be reverted. + + The `origin/` prefix is **required** and the three-dot form is deliberate. + The local branch of that name is stale in this worktree — it points at + `2b510fa`, which is also `origin/main`'s tip, so + `feat/rfc-5-core-principles...HEAD` and `main...HEAD` both pick the wrong + merge-base and report seven paths, with #11's `docs/loopwright/principles.md` + and `docs/specs/issue-11.md` reappearing. Only + `origin/feat/rfc-5-core-principles` (`26681fe`, the commit carrying #11's + merge) gives the five-path result. Do not run `git fetch` and do not move, + reset or update any branch to make this command agree — the orchestrator owns + the refs. + Dependencies must be installed before the collectors can run — in a fresh + worktree `eslint`, `vitest` and `jscpd` are absent and a collector fails with + "command not found", which is a missing install and not a finding about this + diff. `npm ci` at the repo root and `cd .loopwright && npm ci` fix it. + ```bash + node .loopwright/scripts/run-report.mjs --all + node .loopwright/scripts/quality-gate.mjs + ``` + Expected: exit `0`, verdict pass, with the two pre-existing warnings — + `typecheck` unconfigured and `audit.high` = 2 (`brace-expansion`, + `js-yaml`). Neither is ours and neither blocks. **Do not add an + `audit.ignore` entry**: both advisories have upstream fixes, and `CLAUDE.md` + forbids ignoring an advisory that does. Do not regenerate the baseline — no + metric can move on this diff, so a baseline change would be unexplainable. + The collectors write to `.loopwright/reports/`, which is gitignored, so a + run leaves `git status --short` empty in a worktree. Re-check it after the + run anyway: if any generated path does appear, it is untracked and stays + that way. +- **Commit**: none, unless the sweep found something. If it did: + `git add ` — `docs: restore dropped in the rewrite (#13)` + +## Risks and open questions + +- **P8's opt-in does not exist in the code.** Step 2 states a rule the harness + does not yet implement a knob for. The wording in **Accuracy traps** keeps it + honest, but a reviewer may still read "explicitly opted into" as a promise. + RFC #5 accepted this risk explicitly ("P8 is the only non-descriptive line in + a descriptive document"); the mitigation is the clause saying loopwright + ships no such opt-in today. +- **`README.md:89`'s heading wording is a judgement call.** `## Your toolchain + is a detail` is recommended, not mandated by any acceptance criterion. The + criterion is only that the heading no longer frames identity as the stack. + A reviewer may prefer different wording; that is a comment, not a blocker. +- **The shared block ships to hosts on their next `install.sh` run**, replacing + their marker region. That is the mechanism working as designed, and the only + reason this task matters more than the other three, but it does mean the + wording gets no second chance in an adopter's repo once released. +- **Line-number drift.** Every site is anchored by text for this reason, but + Step 1 moves `:89`, `:142`, `:158` and `:173`. A coder working top-down from + numbers will edit the wrong lines. +- **Open question, not blocking**: `README.md:180-185` ("This repo runs on + itself") also describes the repo through `.loopwright/scripts/` and a test + count. It is a dogfooding claim rather than an identity statement, no + acceptance criterion names it, and RFC #5's scope does not list it — so it + is out of scope here. If a reviewer wants it reframed, it is a follow-up + issue, not a scope creep into this PR. diff --git a/package.json b/package.json index 182ad64..c104cca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "loopwright", "version": "0.2.0", - "description": "Vendorable quality layer for AI-assisted JS/TS repos: RFC-driven issue flow, agentic execution loop, and a CI quality gate that agents cannot cheat.", + "description": "Done is a mechanical verdict, not a judgement call: quality ratchets against a committed baseline and green must be earned — RFC-driven issue flow, agentic execution loop and CI quality gate for JS/TS repos.", "private": true, "license": "MIT", "type": "module",