From f981739990ed26902c45019d8bd6b5ba9deda41e Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Mon, 5 Oct 2026 19:55:45 -0300 Subject: [PATCH 01/24] docs: add spec for #11 Co-Authored-By: Claude Opus 5 --- docs/specs/issue-11.md | 607 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 607 insertions(+) create mode 100644 docs/specs/issue-11.md diff --git a/docs/specs/issue-11.md b/docs/specs/issue-11.md new file mode 100644 index 0000000..313582e --- /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 tool | +| 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`. From cbf6eb89c626215fdbaabeb817d080f75bc9f086 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Mon, 5 Oct 2026 20:11:15 -0300 Subject: [PATCH 02/24] docs: add principles doc skeleton and context Co-Authored-By: Claude Opus 5 --- docs/loopwright/principles.md | 39 +++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docs/loopwright/principles.md diff --git a/docs/loopwright/principles.md b/docs/loopwright/principles.md new file mode 100644 index 0000000..a29e3a2 --- /dev/null +++ b/docs/loopwright/principles.md @@ -0,0 +1,39 @@ +# 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, and every core opinion exists to keep that verdict +mechanical, measured and out of the agent's reach. + +This document is descriptive. No skill consults it, no gate checks it, and +no RFC carries 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. Everything that has a knob and a +current default is detail territory. + +How far autonomy may eventually reach is deliberately not declared here. +Declaring a limit would turn a precondition into a ceiling, and the limit +is a judgement that belongs to whoever relies on the loop, not to this +document. + +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 + +## Detail territory + +## Classification rule + +## Amendments From 2280f704f2c7552c2ae9e4f654ae9855490b3b9f Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Mon, 5 Oct 2026 20:43:10 -0300 Subject: [PATCH 03/24] fix: tighten principles Context claims (#11) Co-Authored-By: Claude Opus 5 --- docs/loopwright/principles.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/loopwright/principles.md b/docs/loopwright/principles.md index a29e3a2..8ff9ecc 100644 --- a/docs/loopwright/principles.md +++ b/docs/loopwright/principles.md @@ -7,24 +7,24 @@ 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, and every core opinion exists to keep that verdict -mechanical, measured and out of the agent's reach. +cannot be bought. The gate as sole authority, the integrity signals and the +ratchet are preconditions of autonomy, not constraints on it. This document is descriptive. No skill consults it, no gate checks it, and -no RFC carries 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. +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. Everything that has a knob and a -current default is detail territory. +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, and the limit -is a judgement that belongs to whoever relies on the loop, not to this -document. +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 From 8348fc530985ce4449a5b6cbfb7990ff84dae8d1 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 16:16:29 -0300 Subject: [PATCH 04/24] docs: add the twelve core opinions with rejects lines (#11) Co-Authored-By: Claude Opus 5 --- docs/loopwright/principles.md | 68 +++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/docs/loopwright/principles.md b/docs/loopwright/principles.md index 8ff9ecc..27586ea 100644 --- a/docs/loopwright/principles.md +++ b/docs/loopwright/principles.md @@ -32,6 +32,74 @@ For how work flows from an issue to a merged change, see ## 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), or `failed` (always blocks). + - `rejects:` a connector that reports a metric key core does not define. + - `enforced by:` `.loopwright/scripts/lib/collect-metrics.mjs` + ## Detail territory ## Classification rule From 07547ec62588d33683d98650b0f14371cb77c267 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 16:24:28 -0300 Subject: [PATCH 05/24] docs: add the rationale prose under the core opinions (#11) Co-Authored-By: Claude Opus 5 --- docs/loopwright/principles.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/loopwright/principles.md b/docs/loopwright/principles.md index 27586ea..debfb7e 100644 --- a/docs/loopwright/principles.md +++ b/docs/loopwright/principles.md @@ -100,6 +100,32 @@ For how work flows from an issue to a merged change, see - `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 and is not the policy file P7 names. + +**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 is not a completed entry; it names a gap to +close. + ## Detail territory ## Classification rule From 835971a78eddb455997d2faceb7511f8925bac60 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 16:31:32 -0300 Subject: [PATCH 06/24] docs: add the detail territory and the agent-host contract (#11) Co-Authored-By: Claude Opus 5 --- docs/loopwright/principles.md | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/loopwright/principles.md b/docs/loopwright/principles.md index debfb7e..874a786 100644 --- a/docs/loopwright/principles.md +++ b/docs/loopwright/principles.md @@ -128,6 +128,46 @@ close. ## 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. + +Each territory below is a named seam and the default behind it today. + +| id | Territory | Seam | Default today | +|---|---|---|---| +| 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; container | +| 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 | + +This doc 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/` +references Claude, agents, skills or sessions. + ## Classification rule ## Amendments From d6011074014970ed0690e205de0cf6f165496569 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 16:33:01 -0300 Subject: [PATCH 07/24] fix: correct detail territory claims (#11) Co-Authored-By: Claude Opus 5 --- docs/loopwright/principles.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/loopwright/principles.md b/docs/loopwright/principles.md index 874a786..8b84daa 100644 --- a/docs/loopwright/principles.md +++ b/docs/loopwright/principles.md @@ -132,13 +132,11 @@ close. 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. -Each territory below is a named seam and the default behind it today. - | id | Territory | Seam | Default today | |---|---|---|---| | 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; container | +| 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 | @@ -166,7 +164,7 @@ 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/` -references Claude, agents, skills or sessions. +depends on Claude, agents, skills or sessions. ## Classification rule From f38fa5338ccc5316e3ff09735c723578b3cb868c Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 16:44:37 -0300 Subject: [PATCH 08/24] fix: rename the territory default column and define a default (#11) Co-Authored-By: Claude Opus 5 --- docs/loopwright/principles.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/loopwright/principles.md b/docs/loopwright/principles.md index 8b84daa..cf65b69 100644 --- a/docs/loopwright/principles.md +++ b/docs/loopwright/principles.md @@ -132,7 +132,7 @@ close. 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. -| id | Territory | Seam | Default today | +| 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 | @@ -143,6 +143,10 @@ default is never a requirement: there is always a documented way to swap it. | 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, wired out of the box +where the territory is built. Where a territory has no implementation yet, the +default names what it will be built with. + This doc defines no contract for these territories. Each belongs to its own RFC. T5 is the one exception. From 5870e6316b0c3ae4e2f3a0051ea4f518dda1d05c Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 16:45:52 -0300 Subject: [PATCH 09/24] docs: add the classification rule and the amendment mechanism (#11) Co-Authored-By: Claude Opus 5 --- docs/loopwright/principles.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/loopwright/principles.md b/docs/loopwright/principles.md index cf65b69..06cdfec 100644 --- a/docs/loopwright/principles.md +++ b/docs/loopwright/principles.md @@ -172,4 +172,18 @@ 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. From b7b01bed7d531fd80fbe720227ce4a8781b16b5d Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 16:50:38 -0300 Subject: [PATCH 10/24] docs: reconcile the principles doc with RFCs #6-#10 Co-Authored-By: Claude Opus 5 --- docs/loopwright/principles.md | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/loopwright/principles.md b/docs/loopwright/principles.md index 06cdfec..adca72d 100644 --- a/docs/loopwright/principles.md +++ b/docs/loopwright/principles.md @@ -8,7 +8,7 @@ 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 preconditions of autonomy, not constraints on it. +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 @@ -52,8 +52,8 @@ For how work flows from an issue to a merged change, see - `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. +- **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. @@ -96,7 +96,7 @@ For how work flows from an issue to a merged change, see - **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), or `failed` (always blocks). + 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` @@ -115,7 +115,9 @@ 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 and is not the policy file P7 names. +P3. What separates it from the policy file is the author: the host writes the +policy file, and the engine writes the baseline. The host commits the baseline +and never edits it. **An artifact exists before production.** An issue and a PR exist before anything reaches production, because P1 requires the artifact. Without it @@ -123,20 +125,23 @@ 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 is not a completed entry; it names a gap to -close. +`enforced by:` line is empty is complete as documentation. The empty line +records that nothing enforces the opinion yet, so what is incomplete is the +engine, not the entry. 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. +default is never a requirement: where a territory has a seam, there is a +documented way to swap it. T4 and T6 have no seam and promise 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 | +| T3 | Where and how agents execute | `runtime` | `docker` container | | T4 | Thresholds and tolerances | — | already config-driven | | T5 | Agent host | `agent-host` | Claude Code | | T6 | Engine implementation language | — | Node | @@ -147,7 +152,7 @@ A default is the recommended choice for that territory, wired out of the box where the territory is built. Where a territory has no implementation yet, the default names what it will be built with. -This doc defines no contract for these territories. Each belongs to its own +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 @@ -164,8 +169,9 @@ agent host: 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. +Capability 2 is load-bearing for P10. A reviewer is meant to be read-only by +the tools it is handed, not by being told to behave. The guarantee is only as +strong as that tool list, and a write-capable tool in it weakens the guarantee. The engine has zero coupling here: nothing under `.loopwright/scripts/` depends on Claude, agents, skills or sessions. From 7a4274d032f7b1cbf9033636e097cba1c34965c2 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 16:52:13 -0300 Subject: [PATCH 11/24] fix: restore local session as T3's default and tighten the default rule (#11) Co-Authored-By: Claude Opus 5 --- docs/loopwright/principles.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/loopwright/principles.md b/docs/loopwright/principles.md index adca72d..91080b7 100644 --- a/docs/loopwright/principles.md +++ b/docs/loopwright/principles.md @@ -141,16 +141,16 @@ documented way to swap it. T4 and T6 have no seam and promise no swap route. |---|---|---|---| | 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` | `docker` container | +| 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, wired out of the box -where the territory is built. Where a territory has no implementation yet, the -default names what it will be built with. +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. From 2ece483a0e0b958a7f8162e4c200b8c7c58d32e4 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 17:10:52 -0300 Subject: [PATCH 12/24] fix: correct the swap rule, empty-pointer, baseline and capability wording (#11) Co-Authored-By: Claude Opus 5 --- docs/loopwright/principles.md | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/loopwright/principles.md b/docs/loopwright/principles.md index 91080b7..f9f61ae 100644 --- a/docs/loopwright/principles.md +++ b/docs/loopwright/principles.md @@ -115,9 +115,9 @@ 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 the author: the host writes the -policy file, and the engine writes the baseline. The host commits the baseline -and never edits it. +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 @@ -125,17 +125,17 @@ 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 is complete as documentation. The empty line -records that nothing enforces the opinion yet, so what is incomplete is the -engine, not the entry. The gap is closed by building the enforcement, never -by writing a pointer to a file that does not enforce the opinion. +`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: where a territory has a seam, there is a -documented way to swap it. T4 and T6 have no seam and promise no swap route. +default is never a requirement: there is always a documented way to swap it. +The one exception is T6, the engine's implementation language, which has no +swap route. | id | Territory | Seam | Default | |---|---|---|---| @@ -152,8 +152,8 @@ 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. +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. @@ -169,9 +169,8 @@ agent host: 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 meant to be read-only by -the tools it is handed, not by being told to behave. The guarantee is only as -strong as that tool list, and a write-capable tool in it weakens the guarantee. +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. From 8c45702b39dc48361ed437cbac6dc0f5f5622706 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 17:26:20 -0300 Subject: [PATCH 13/24] docs: fix P10 pointer claim, backtick changesets, name swap-rule exception (#11) Co-Authored-By: Claude Opus 5 --- docs/loopwright/principles.md | 6 +++--- docs/specs/issue-11.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/loopwright/principles.md b/docs/loopwright/principles.md index f9f61ae..bc89b05 100644 --- a/docs/loopwright/principles.md +++ b/docs/loopwright/principles.md @@ -134,8 +134,8 @@ opinion. **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 is T6, the engine's implementation language, which has no -swap route. +The one exception to the swap rule is T6, the engine's implementation +language, which has no swap route. | id | Territory | Seam | Default | |---|---|---|---| @@ -145,7 +145,7 @@ swap route. | 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 | +| 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 diff --git a/docs/specs/issue-11.md b/docs/specs/issue-11.md index 313582e..a6af092 100644 --- a/docs/specs/issue-11.md +++ b/docs/specs/issue-11.md @@ -151,7 +151,7 @@ are file paths with **no line number**. | 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 tool | +| 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 | From 17d981ae66245ba0e0a90f0e2a53a51bd1e8f870 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 19:19:07 -0300 Subject: [PATCH 14/24] docs: add spec for #12 Co-Authored-By: Claude Opus 5 --- docs/specs/issue-12.md | 576 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 576 insertions(+) create mode 100644 docs/specs/issue-12.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. From 8ba70bf8f29afcb5ba736f3ad796b15326c94634 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Wed, 7 Oct 2026 19:23:07 -0300 Subject: [PATCH 15/24] 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 16/24] 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 17/24] 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 18/24] 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 19/24] 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 20/24] 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 21/24] 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 22/24] 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 23/24] 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.* From 40861c28daefcee069564c0d7a6cbbee2e8304d5 Mon Sep 17 00:00:00 2001 From: Samuel Denani Date: Thu, 8 Oct 2026 11:45:24 -0300 Subject: [PATCH 24/24] docs: strip the opinions from loop-harness, leaving mechanics (#12) The three rules that are core opinions now live in principles.md, so stating them here too made two sources of truth for one rule. Replaced in place by a reference: the section is renamed to Mechanics, a new Opinions section names P1, P2 and P8 with the question each settles, and the opening paragraph points at the doc. The grill-phase fact that was an exception to P1 is re-homed as a descriptive mechanic. The Flow terminal drops the actor, since P8 owns it. Co-Authored-By: Claude Opus 5 --- docs/loopwright/loop-harness.md | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) 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 |