From 8ba70bf8f29afcb5ba736f3ad796b15326c94634 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 19:23:07 -0300 Subject: [PATCH 1/9] docs: add spec for #13 Co-Authored-By: Claude Opus 5 --- docs/specs/issue-13.md | 712 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 712 insertions(+) create mode 100644 docs/specs/issue-13.md 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. From c01c0ee49834f1e07b44fb2c8f9971bdfab4fac7 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 19:25:50 -0300 Subject: [PATCH 2/9] docs: lead the README with the process opinion, not the stack (#13) Co-Authored-By: Claude Opus 5 --- README.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a49349a..beca894 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 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 | |---|---| @@ -170,7 +174,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 From be9571f4ad966482be639f83cc3a986b3d9e61d5 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 19:39:19 -0300 Subject: [PATCH 3/9] docs: reconcile the README merge rule with P8 and wire in the principles doc (#13) Co-Authored-By: Claude Opus 5 --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index beca894..223d975 100644 --- a/README.md +++ b/README.md @@ -143,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, so nothing in the harness merges. Both +are stated in full in `docs/loopwright/principles.md` (P2 and P8). ## Running it @@ -163,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/` | From ab9ee438ebb90c15e9386cfa6eaa5444a3b74dae Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 19:51:17 -0300 Subject: [PATCH 4/9] docs: mark the missing merge opt-in as a present-state fact (#13) Co-Authored-By: Claude Opus 5 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 223d975..2ca2ccf 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ the repo alone. 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 +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 From da0bf75b729e4b8d2e31e8b85eff08cd3ab5e988 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 19:58:36 -0300 Subject: [PATCH 5/9] docs: introduce loopwright to host repos by its opinions first (#13) Co-Authored-By: Claude Opus 5 --- .loopwright/claude-md-section.md | 10 +++++++--- CLAUDE.md | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) 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..64e9ccf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,9 +7,13 @@ lives in `.loopwright/scripts/`, its tests in `.loopwright/tests/` ## 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). From abf29b01c17000ccf7f909a3480943e4209f5e96 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 20:04:41 -0300 Subject: [PATCH 6/9] chore: describe loopwright by its process opinions (#13) Co-Authored-By: Claude Opus 5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From 27edb2bfd5717bdc343bbbac52d33b0cc480b4b7 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 20:08:46 -0300 Subject: [PATCH 7/9] docs: define this repo by the opinions it is held to, not its directories (#13) Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 64e9ccf..c0e0c23 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,9 @@ # 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`). The engine that enforces them lives in +`.loopwright/scripts/`, its tests in `.loopwright/tests/` (`cd .loopwright && npx vitest run` runs them). From c60d8d42a2a5d7e0ea4dba705e976f6cf2372b90 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 20:27:55 -0300 Subject: [PATCH 8/9] docs: stop attributing every opinion's enforcement to the engine dir (#13) The preamble said "The engine that enforces them lives in `.loopwright/scripts/`", attributing enforcement of all twelve opinions to that directory one line after citing the doc that denies it: `docs/loopwright/principles.md:175-176` states the engine has zero coupling to Claude, agents, skills or sessions. Six of the twelve `enforced by:` pointers live under `.claude/`, so an agent following this line to find what enforces test-first would find nothing there. "Its engine lives in" drops the false attribution and also resolves the sentence's double "them" (the opinions, then the tests). Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index c0e0c23..189a0a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ 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`). The engine that enforces them lives in +(`docs/loopwright/principles.md`). Its engine lives in `.loopwright/scripts/`, its tests in `.loopwright/tests/` (`cd .loopwright && npx vitest run` runs them). From 611bc5f427de9aa58b639b9c89e9d10cc83c8951 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 20:28:36 -0300 Subject: [PATCH 9/9] docs: name both halves of loopwright in the README lead (#13) The old lead's "a CI quality gate ... plus the loop that feeds it" was its only statement that the product has two halves, and roughly half the README is about the loop half. Dropping it left `README.md:131` ("The gate is the enforcement half. The other half is how work reaches it:") introducing that duality cold. Sentence 2 now carries the category fact, so sentence 1 stays the opinion and the lead still carries the committed baseline, the earned-vs-bought question, and the verdict as a comment an agent can act on. Co-Authored-By: Claude Opus 5 --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2ca2ccf..5914573 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # loopwright -**"Done" is a mechanical verdict, not a judgement call.** loopwright 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. +**"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.*