@nurix/etna 0.20.10 → 0.20.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/data/catalog/rules/cloudflare.md +8 -0
  2. package/data/catalog/rules/database.md +10 -0
  3. package/data/catalog/rules/frontend.md +10 -0
  4. package/data/catalog/rules/pnpm.md +9 -0
  5. package/data/catalog/rules/react-native.md +10 -0
  6. package/data/catalog/rules/rust-desktop.md +8 -0
  7. package/data/catalog/rules/temporal.md +9 -0
  8. package/data/catalog/rules/testing.md +1 -1
  9. package/data/harness-index.json +1 -1
  10. package/data/harnesses/code/CLAUDE.md +7 -5
  11. package/data/harnesses/code/agents/polygraph-applier.md +24 -0
  12. package/data/harnesses/code/agents/polygraph-finder.md +31 -0
  13. package/data/harnesses/code/agents/polygraph-verifier.md +20 -0
  14. package/data/harnesses/code/codex/agents/polygraph-applier.toml +21 -0
  15. package/data/harnesses/code/codex/agents/polygraph-finder.toml +28 -0
  16. package/data/harnesses/code/codex/agents/polygraph-verifier.toml +17 -0
  17. package/data/harnesses/code/harness.json +2 -2
  18. package/data/harnesses/code/hooks/awareness.mjs +16 -2
  19. package/data/harnesses/code/hooks/lib/plans.mjs +11 -1
  20. package/data/harnesses/code/rules/cloudflare.md +8 -0
  21. package/data/harnesses/code/rules/database.md +10 -0
  22. package/data/harnesses/code/rules/frontend.md +10 -0
  23. package/data/harnesses/code/rules/pnpm.md +9 -0
  24. package/data/harnesses/code/rules/testing.md +1 -1
  25. package/data/harnesses/code/skills/documentation/references/instruction-style.md +1 -1
  26. package/data/harnesses/code/skills/documentation/templates/plan.md +2 -2
  27. package/data/harnesses/code/skills/polygraph/SKILL.md +71 -0
  28. package/data/harnesses/code/skills/polygraph/references/dispatch.md +80 -0
  29. package/data/harnesses/code/skills/polygraph/references/rows.md +57 -0
  30. package/data/harnesses/code/skills/polygraph/scripts/audit.mjs +113 -0
  31. package/data/harnesses/code/skills/testing/SKILL.md +18 -17
  32. package/data/harnesses/delivery/harness.json +2 -2
  33. package/data/harnesses/delivery/skills/documentation/references/instruction-style.md +1 -1
  34. package/data/harnesses/delivery/skills/documentation/templates/plan.md +2 -2
  35. package/data/harnesses/etna/harness.json +2 -2
  36. package/data/harnesses/etna/rules/testing.md +1 -1
  37. package/data/harnesses/etna/skills/documentation/references/instruction-style.md +1 -1
  38. package/data/harnesses/etna/skills/documentation/templates/plan.md +2 -2
  39. package/package.json +2 -2
@@ -1,3 +1,11 @@
1
+ ---
2
+ paths:
3
+ - "**/wrangler.*"
4
+ - "**/.dev.vars*"
5
+ - "**/worker/**"
6
+ - "**/workers/**"
7
+ - "**/src/**/*.ts"
8
+ ---
1
9
  # Cloudflare Worker
2
10
 
3
11
  Conventions for Cloudflare Workers — `wrangler.jsonc` setup, vars and secrets, and code shape (dispatch, errors, modules, WebSockets). Schema design doctrine is the `schema-design` skill.
@@ -1,3 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "**/*.sql"
4
+ - "**/migrations/**"
5
+ - "**/drizzle/**"
6
+ - "**/drizzle.config.*"
7
+ - "**/schema.ts"
8
+ - "**/schema/**"
9
+ - "**/db/**"
10
+ ---
1
11
  # Database
2
12
 
3
13
  Naming and the fixed column vocabulary for relational schemas in **every** Nurix project — one concept, one name, everywhere. Written for SQLite / Cloudflare D1 + Drizzle; a Postgres project keeps the same column names and semantics with native types (`uuid`, `timestamptz`, `boolean`). **Design doctrine — what earns a table, state vs. logs, junctions, FK policy, uniqueness traps — is the `schema-design` skill; consult it before any schema change.**
@@ -1,3 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "**/*.tsx"
4
+ - "**/*.jsx"
5
+ - "**/*.ts"
6
+ - "**/*.css"
7
+ - "**/*.html"
8
+ - "**/tailwind.config.*"
9
+ - "**/vite.config.*"
10
+ ---
1
11
  # Frontend
2
12
 
3
13
  Tailwind v4 + shadcn-based UI conventions for **web** apps — styling, the `@nurix/components` library, the `design.md` token artifact, and design-skill routing — plus the TypeScript conventions every package compiles under.
@@ -1,3 +1,12 @@
1
+ ---
2
+ paths:
3
+ - "**/package.json"
4
+ - "**/pnpm-workspace.yaml"
5
+ - "**/pnpm-lock.yaml"
6
+ - "**/.npmrc"
7
+ - "**/tsconfig*.json"
8
+ - "**/turbo.json"
9
+ ---
1
10
  # pnpm — `@nurix/*` Workspaces: naming, boundaries, and consuming
2
11
 
3
12
  How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and CLI design live in the `nustack` skill, not here — no `nustack` skill in the runtime ⇒ ask before crossing the publish boundary.
@@ -1,3 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "**/app.json"
4
+ - "**/app.config.*"
5
+ - "**/eas.json"
6
+ - "**/metro.config.*"
7
+ - "**/babel.config.*"
8
+ - "**/app/**"
9
+ - "**/*.tsx"
10
+ ---
1
11
  # React Native + Expo
2
12
 
3
13
  How mobile apps are built — distilled from a hand-crafted reference app, the gold standard for *style*. The rule is **scale-aware**: a few-screen app takes the lean form, a feature/auth-heavy app the full form; where a section offers both, the graduation trigger is stated.
@@ -1,3 +1,11 @@
1
+ ---
2
+ paths:
3
+ - "**/*.rs"
4
+ - "**/Cargo.toml"
5
+ - "**/Cargo.lock"
6
+ - "**/tauri.conf.json"
7
+ - "**/src-tauri/**"
8
+ ---
1
9
  # Rust Desktop Apps (Tauri 2)
2
10
 
3
11
  How native desktop apps are built: a **Rust core + web-frontend** shell — the cargo/JS dual workspace, the Rust↔TS contract seam, native-capability handling, and signed distribution. This rule governs everything on the **Rust and OS side of the webview boundary**; the webview UI is a normal web app, unchanged by being hosted in a webview. Scale-aware: a single-window tool with no native capabilities skips §2's workspace and §4's actor (graduation trigger in §2); a capability-rich app (audio, permissions, background work, sibling processes) takes the full shape.
@@ -1,3 +1,12 @@
1
+ ---
2
+ paths:
3
+ - "**/workflows/**"
4
+ - "**/activities/**"
5
+ - "**/*.workflow.ts"
6
+ - "**/*.activity.ts"
7
+ - "**/worker.ts"
8
+ - "**/temporal/**"
9
+ ---
1
10
  # Temporal
2
11
 
3
12
  Workflow and activity conventions for Temporal-backed services — the determinism boundary, file layout, registration, retry defaults, and starting workflows.
@@ -60,7 +60,7 @@ Test packages install at `latest` — outside the approved-packages pin set. Fak
60
60
 
61
61
  - **Suite, not scripts.** Tests live in the canonical test tree — `tests/` mirroring `src/`, or the convention the repo already uses — named `<subject>.test.ts`; shared fixtures, factories, and helpers in `tests/helpers/` (or the suite's existing shared home).
62
62
  - **Wire the canonical command.** `"test"` in `package.json` runs the whole unit suite (`vitest run`); e2e under `"test:e2e"`. A repo where `pnpm test` / `npm test` doesn't run the suite is misconfigured — fix the wiring, **never** route around it.
63
- - **Unit and E2E trees stay separate.** Vitest/Jest suites in `tests/`; Playwright/Maestro suites in `e2e/` with their own config — **never** mixed into the unit runner's glob.
63
+ - **Unit and E2E trees stay separate.** Vitest/Jest suites in `tests/`; Playwright/Maestro suites in `e2e/` with their own config — **never** mixed into the unit runner's glob. The e2e config **owns its world** — it builds, seeds, and boots the app itself (a `webServer` block or equivalent), so a journey passes from a cold shell like any other test and "the world was not running" is never why one did not run.
64
64
  - **The real-renderer stratum is a third tree** — `tests/rendered/` under the unit runner's *browser mode*, its own config, kept to geometry regressions that shipped and the composition assertions a component library cannot make alone. It is not a second component suite; anything a simulated DOM can prove stays in `tests/component/`.
65
65
  - **No test doubles for what you own.** Prefer real instances — in-memory SQLite for drizzle, the actual Express app via `supertest` — over mocks; mock **only** true externals (network via `msw`, time via the runner's fake timers).
66
66
  - **Determinism.** No real network, no real time, no shared mutable state between tests; every test **must** pass alone, in any order, **and from a cold shell** — never against a hand-started dev server (Workers: `@cloudflare/vitest-pool-workers`; Express: `supertest` against the app instance).
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "0fa183cce9ee",
2
+ "fingerprint": "d525a2adfbe5",
3
3
  "harnesses": {
4
4
  "code": {
5
5
  "plugin": "code",
@@ -33,14 +33,15 @@ Three peer modes, then a verification gate and a Completion step — enter where
33
33
 
34
34
  > Brainstorm entry needs a **basis** — a reference document or a one-shot brain dump; drip-sized asks (a bug fix, one button) are too small and get redirected. A **pillar** is a recorded strategic principle from a past brainstorm: in-progress sessions live under `docs/research/`, harvested ones under `docs/history/brainstorms/`, harvested doctrine under `docs/architecture/`. The LLD is the feature doc itself — `docs/features/{feature}.md`, design intent first, extended in place at Completion.
35
35
 
36
- > **Two stops, and only two.** The work between them runs without asking. The **plan pass** ends at a finished plan — written, boundary-checked, amendments folded, both ledgers in place — shown once; a plan that still owes a gate is not finished and is not shown as finished. The **build pass** ends at a discharged gate — the code written, driven, fixed, pinned, every ledger row in its Outcome cell — and stops before the freeze. What the user decides is the gated plan (stop 1) and the freeze-and-commit (stop 2); running a gate is never a question put to the user.
36
+ > **Two stops, and only two.** The work between them runs without asking. The **plan pass** ends at a finished plan — written, boundary-checked, amendments folded, both ledgers in place — shown once; a plan that still owes a gate is not finished and is not shown as finished. The **build pass** ends at a discharged gate — the code written, driven, fixed, pinned, every ledger row in its Outcome cell, the plan's documents certified (`Polygraph:` written) — and stops before the freeze. What the user decides is the gated plan (stop 1) and the freeze-and-commit (stop 2); running a gate is never a question put to the user.
37
37
 
38
38
  > **The lightweight lane:** a change touching one module, **at most ~10 files, none on a contract-heavy surface** (db schema, wire envelopes, auth, events, env/config — the `boundary` skill's list), and under about **three days'** work skips the modes above entirely — the ad-hoc regime of `.claude/rules/testing.md` applies: make the change, drive it, then pin it in the same unit of work if it alters promised behavior, cover it with a changelog row. The predicate is mechanical; when in doubt, the `boundary` diff scan is the tiebreak, not self-assessment.
39
39
 
40
40
  **On Plan Completion:**
41
41
 
42
- 1. **The verification gate is the tail of the build pass, never a separate ask** **[override]** — the moment the unit's code is written the session opens it: drives the real surface, fixes what driving finds, pins once, discharges. What is the user's call is the freeze and the commit — "commit" / "ship it" triggers step 2, not the gate, which has already run. This keeps the runtime's finish-the-task default inside the build and removes it from the freeze. The gate requires the plan's Test Obligations ledger to exist (proof the plan gates ran) and discharges every row into its **Outcome** cell, driving the real surface before pinning tests — `.claude/rules/testing.md`. A plan with outstanding obligations stays open, whatever the code does.
43
- 2. **Discharge, then freeze — in one edit.** Discharge the Doc Obligations ledger (every row into exactly one outcome: **updated** — the doc now states what the code does; **owes nothing**, with the reason; **blocked** — → a backlog item, so the debt outlives the frozen plan), fill the Completed date, and flip `Status: Complete` **in the same edit**: the flip freezes the file instantly, so anything left for a second edit is unreachable. For each discharged row's doc: flip `status: active` if it was `draft`, extend it in place, and confirm its `owns:` globs against the actual code — the ledger, not memory, says how many docs that is.
42
+ 1. **The verification gate is the tail of the build pass, never a separate ask** **[override]** — the moment the unit's code is written the session opens it: drives the real surface, fixes what driving finds, pins once, discharges. What is the user's call is the freeze and the commit — "commit" / "ship it" triggers step 3, not the gates, which have already run. This keeps the runtime's finish-the-task default inside the build and removes it from the freeze. The gate requires the plan's Test Obligations ledger to exist (proof the plan gates ran) and discharges every row into its **Outcome** cell, driving the real surface before pinning tests — `.claude/rules/testing.md`. A plan with outstanding obligations stays open, whatever the code does.
43
+ 2. **Certify the documents — the polygraph gate, still inside the build pass.** Once every Doc Obligations row has its outcome, run the `polygraph` skill in gate mode over the docs the plan touched (every row discharged **updated**, plus every `Spec:` doc): claim by claim against the code, corrections applied, and the plan's `Gates:` line rewritten from `Polygraph: pending` to `Polygraph: passed YYYY-MM-DD (…)`. A plan that touched no document writes `Polygraph: owes nothing — {reason}` by hand instead. A plan frozen while the segment still reads `pending` raises a `needs-session:` queue row and fails CI — the gate is a judgment obligation, enforced the same way as the doc ledger, never a wall.
44
+ 3. **Discharge, then freeze — in one edit.** Discharge the Doc Obligations ledger (every row into exactly one outcome: **updated** — the doc now states what the code does; **owes nothing**, with the reason; **blocked** — → a backlog item, so the debt outlives the frozen plan), fill the Completed date, and flip `Status: Complete` **in the same edit**: the flip freezes the file instantly, so anything left for a second edit is unreachable. For each discharged row's doc: flip `status: active` if it was `draft`, extend it in place, and confirm its `owns:` globs against the actual code — the ledger, not memory, says how many docs that is.
44
45
  3. Record the gate's results in the changelog entry, and confirm every section of `docs/tests/{feature}.md` carries its `Pinned:` trace — the checklist itself is authored at the gate (step 1), not written up afterwards.
45
46
 
46
47
  **On Brainstorm Close** (when a session ends, not when a plan completes): close + harvest is the skill's `stop` procedure — converge, a user-approved harvest manifest, apply, then a uselessness test; conclusions disperse by scope (architecture docs, decision records, backlog), the record moves from `docs/research/{topic}-brainstorm/` to `docs/history/brainstorms/{topic}/`, and the user decides whether the moved folder is deleted or committed.
@@ -70,7 +71,7 @@ Boundaries are **diagnosed, not documented** — the `boundary` skill derives th
70
71
 
71
72
  ### Test obligations — specify before implementing
72
73
 
73
- **Then, still in the plan pass, invoke the `testing` skill (plan-gate mode)** — after `boundary`'s amendments are folded, ordered second. It derives the tests the plan owes from its Proposed Changes and the owning LLD, and writes the **Test Obligations** ledger into the plan (one row per promised behavior: layer · target file · oracle · fixtures · an empty **Outcome** cell the verification gate fills). It writes no test files — the ledger is what the verification gate discharges, and because it lives in the plan file it survives dispatch to an executor that sees only the plan. On a boundary **BLOCK** it does not run — re-plan first. Layer rules and the e2e altitude gate live in the skill. **The Doc Obligations table is the author's, filled in the same pass** — a lookup against the ownership index, not a skill run; the hook blocks implementation edits while either section is missing.
74
+ **Then, still in the plan pass, invoke the `testing` skill (plan-gate mode)** — after `boundary`'s amendments are folded, ordered second. It derives the tests the plan owes from its Proposed Changes and the owning LLD, and writes the **Test Obligations** ledger into the plan (one row per promised behavior: layer · target file · oracle · fixtures · an empty **Outcome** cell the verification gate fills). It writes no test files — the ledger is what the verification gate discharges, and because it lives in the plan file it survives dispatch to an executor that sees only the plan. On a boundary **BLOCK** it does not run — re-plan first. Layer rules live in the skill — on a rendered surface every user-observable promise is owed as its surface's journey, and component tests are residual. **The Doc Obligations table is the author's, filled in the same pass** — a lookup against the ownership index, not a skill run; the hook blocks implementation edits while either section is missing.
74
75
 
75
76
  ### Verification — prove before completing
76
77
 
@@ -78,6 +79,7 @@ Boundaries are **diagnosed, not documented** — the `boundary` skill derives th
78
79
 
79
80
  ### Documentation — close the loop
80
81
 
82
+ - **When a document's truth is in question — "is this doc still right?", a feature that drifted from its doc, a folder nobody has audited — invoke the `polygraph` skill (audit mode) on that target**: one doc, feature, folder, or code path, never the whole tree by reflex. Docs health (dead globs, lint, placement) is the `maintenance` agent; blast radius is `boundary`.
81
83
  - **Invoke the `documentation` skill when creating a doc, or when an edit changes a doc's structure or claims** — apply its templates and `references/style-guide.md`: truth documents stay present-tense; history lives only in `docs/history/` records. Records written from a template already in context (a changelog entry, a backlog row) skip the invocation.
82
84
  - **[override]** Keep `docs/` updated when code changes affect documented behavior. This file is the standing request for those docs: creating and updating them is part of the work, even if the runtime defaults to never creating documentation files unprompted.
83
85
  - After plan completion **[override]**: write the `docs/history/changelog/` entry as a **draft** immediately, while the context is fresh — no confirmation needed — and append its `docs/history/CHANGELOG.md` row; the commit fills what the draft left open and freezes it. Non-plan work needs no separate prompt: every non-exempt commit carries its own entry (the `changelog` rule).
@@ -130,7 +132,7 @@ The single policy for sub-agent dispatch — when to fan out, at what tier, and
130
132
  - **A dispatch is scoped to minutes and read before anything else starts.** One file, one sweep — never a build. The dispatching agent reads the result before moving on, and never leaves a sub-agent as the only thing running while it does something else: a sub-agent's state is invisible until someone looks, so an hour-long dispatch that dies is discovered an hour late, with nothing to show for the hour.
131
133
  - **Small or judgment work stays inline.** A task finishable in a handful of tool calls is faster done than briefed. **Never delegate judgment by reflex** — a second opinion is for a high-stakes call or an explicit request.
132
134
  - **A delegated result is a report, not verifiable ground truth** — verify load-bearing results directly.
133
- - **Tiers**: mechanical fan-out → the cheapest capable tier; judgment-heavy delegation → the session's standard tier; the strongest tier **only** on explicit user request. The session itself defaults to the standard tier, entering the strongest only by explicit user switch. Model names live in Runtime Notes.
135
+ - **Tiers**: mechanical fan-out → the cheapest capable tier; judgment-heavy delegation → the session's standard tier; the strongest tier **only** on explicit user request. The session itself defaults to the standard tier, entering the strongest only by explicit user switch. Model names live in Runtime Notes. **A rostered agent whose definition pins `model:` and `effort:` is dispatched at that pin** — the pin is the tier decision, made once at authoring; a skill that dispatches such an agent never re-picks per dispatch, and a session running hot cannot drag it up.
134
136
  - **An explicit user directive about tier, cost, or delegation overrides this section.**
135
137
 
136
138
  ---
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: polygraph-applier
3
+ description: Applies one packet of verified polygraph rows to exactly the documents the packet names, re-checking every citation first. Dispatched by the polygraph skill only; never on its own.
4
+ tools: Read, Edit, Write, Grep, Glob, Bash
5
+ model: sonnet
6
+ effort: medium
7
+ maxTurns: 15
8
+ ---
9
+
10
+ # Polygraph applier
11
+
12
+ You are an APPLIER in a truth audit. You receive the path of a **packet** and the path of its **context pack**. The packet names your editable docs (`doc:` lines) and your rows, with any bounce notes — read them; a bounced row's note says exactly what is wrong and where.
13
+
14
+ 1. Read the pack, then the packet. Each row is quoted in full inside the packet; never open the run's findings file to find your rows.
15
+ 2. **Re-check each row's citations against today's tree before editing.** This re-check is the audit's real gate: a claim that does not reproduce resolves `refuted-on-apply`, never as an edit that bends the doc to a stale claim.
16
+ 3. Edit **only** the docs your packet names. A fix that belongs in another doc is `needs-cross-doc`, naming that file precisely — your note is how the row is re-routed. Frozen records (completed plans, changelog entries, archived docs) are `exempt-frozen`. A subject awaiting an owner ruling is `blocked-on-question`, no edit.
17
+ 4. Style: present tense; no history narration; the repo's unbuilt-marker convention is the only sanctioned qualifier; preserve frontmatter and tooling metadata. Run the repo's doc lint over every doc touched, with repo-root-relative paths, and fix blocking findings.
18
+ 5. Write `apply-{doc}.psv` beside the packet — `id|outcome|note`, outcome ∈ `applied` · `needs-cross-doc` · `refuted-on-apply` · `exempt-frozen` · `blocked-on-question` · `skipped`.
19
+
20
+ **The audit never edits code.** A row whose fix is a source change — a comment, a rename, a bug — is `skipped` with a note for the register; markdown anywhere, package and root READMEs included, is yours. Rulings in the pack outrank every recorded decision.
21
+
22
+ Tree state: re-check every citation against today's tree before acting. READ-ONLY outside the packet's docs and your outcome file. Read budget: the pack and the packet are your reading list — grep before you read; never read a file over 20 KB without a line range; read a file once; one shell block per question.
23
+
24
+ Return **at most 120 words**: counts per outcome, docs touched, the lint result, and anything for the register.
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: polygraph-finder
3
+ description: Sweeps one document against the code it describes for the polygraph skill — one row per claim, cited, into a partial file. Dispatched by the skill only; never on its own.
4
+ tools: Read, Grep, Glob, Bash, Write
5
+ model: sonnet
6
+ effort: medium
7
+ maxTurns: 25
8
+ ---
9
+
10
+ # Polygraph finder
11
+
12
+ You are a FINDER in a truth audit. You receive the path of a **context pack** and the path of your **partial file**; the pack is your whole reading list, and the partial is your one write.
13
+
14
+ Docs state the target design; code is presumed correct unless a standing decision **names** it for retirement — the pack's precedent section decides, and a match means the precedent names your subject (quote the naming words into evidence; a resembling thing is not a match; unsure → QUESTION).
15
+
16
+ 1. Read the pack in full. It names the document, its `owns:` globs resolved to today's files, the precedent quotations, the register rows already tracking this subject, and the rulings that bind.
17
+ 2. Read the document once, whole — it is the one file you always read in full. Then check each claim it makes against the code: grep for the mechanism, open the passage with a line range, cite `file:line`.
18
+ 3. Write one row per claim to the partial file, **no header**, exactly ten pipe-separated fields: `id|subject|claim|evidence|counter_evidence|verdict|confidence|depends_on|status|resolution`. A literal `|` becomes `¦`, a newline `; `. Verdicts: `COHERENT` (emit at the unit you audited, so coverage is countable) · `MISDESCRIBED` · `UNDOCUMENTED` · `UNBUILT` · `RESIDUE` · `CONTESTED` · `QUESTION`. `id` is `{doc-stem}-NNN`; `status` is `open`; `resolution` is empty; `confidence` is an honest 0–1.
19
+ 4. Sweep the other direction once: code inside the `owns:` globs that no claim in the document covers is an `UNDOCUMENTED` row — after the precedent test.
20
+
21
+ Rules that bind every row:
22
+
23
+ - **No citation, no row** — `file:line`, or the exact command and its result. Prove or refute; never confirm by plausibility.
24
+ - **A negative claim about a long document quotes the passage you read** — the sentence you believe wrong, or the section where the missing statement should live. Unsupported negative claims are the dominant false-positive class.
25
+ - **An already-tracked finding is skipped** unless your evidence adds something new; cite the register row as `already-registered: path:line` in `counter_evidence`.
26
+ - **A document that should not exist** — its subject gone, its authority absorbed by a sibling — is one `QUESTION` row proposing deletion or merge, with the inbound-reference count.
27
+ - **Rulings in the pack outrank every recorded decision.**
28
+
29
+ Tree state: re-check every citation against today's tree before writing it. **READ-ONLY everywhere except your partial file.** Read budget: the pack is your reading list — read nothing outside it without naming in your return why the pack was insufficient. Grep before you read; never read a file over 20 KB without a line range; read a file once. Compose one shell block per question, not one call per file — your turn cap is your tool budget.
30
+
31
+ Return **at most 120 words**: the partial's path, the row count, up to three findings, and limitations. Findings go in the file, never in the return.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: polygraph-verifier
3
+ description: Blind re-derivation of polygraph claims the primary doubts — claim and subject only, no prior evidence — into a verify file. Dispatched by the polygraph skill only; never on its own.
4
+ tools: Read, Grep, Glob, Bash, Write
5
+ model: sonnet
6
+ effort: low
7
+ maxTurns: 12
8
+ ---
9
+
10
+ # Polygraph verifier
11
+
12
+ You are a VERIFIER in a truth audit. You receive a **queue file** of items — `id`, `subject`, `claim` — deliberately with no prior evidence, reasoning, or verdict, and the path of your **output file**.
13
+
14
+ 1. Read the queue. For each item, re-derive the claim from the tree: find the mechanism, read the passage with a line range, decide.
15
+ 2. Judge each: `CONFIRMED` (true as stated) · `REFUTED` (false) · `ADJUSTED` (true in substance, wrong in a particular — say which). Cite exactly, `file:line`.
16
+ 3. Write the output file, one line per item: `id|check|confidence|citations|note`. A literal `|` becomes `¦`.
17
+
18
+ **A wrong confirmation is worse than an honest REFUTED.** Never infer from the claim's wording what the finder saw; derive from the tree. Read budget: grep before you read; never read a file over 20 KB without a line range; read a file once; one shell block per question. READ-ONLY everywhere except your output file.
19
+
20
+ Return **at most 120 words**: the file path and counts per check.
@@ -0,0 +1,21 @@
1
+ # dropped Claude tools: Read, Edit, Write, Grep, Glob, Bash
2
+ name = "polygraph-applier"
3
+ description = "Applies one packet of verified polygraph rows to exactly the documents the packet names, re-checking every citation first. Dispatched by the polygraph skill only; never on its own."
4
+ developer_instructions = '''
5
+
6
+ # Polygraph applier
7
+
8
+ You are an APPLIER in a truth audit. You receive the path of a **packet** and the path of its **context pack**. The packet names your editable docs (`doc:` lines) and your rows, with any bounce notes — read them; a bounced row's note says exactly what is wrong and where.
9
+
10
+ 1. Read the pack, then the packet. Each row is quoted in full inside the packet; never open the run's findings file to find your rows.
11
+ 2. **Re-check each row's citations against today's tree before editing.** This re-check is the audit's real gate: a claim that does not reproduce resolves `refuted-on-apply`, never as an edit that bends the doc to a stale claim.
12
+ 3. Edit **only** the docs your packet names. A fix that belongs in another doc is `needs-cross-doc`, naming that file precisely — your note is how the row is re-routed. Frozen records (completed plans, changelog entries, archived docs) are `exempt-frozen`. A subject awaiting an owner ruling is `blocked-on-question`, no edit.
13
+ 4. Style: present tense; no history narration; the repo's unbuilt-marker convention is the only sanctioned qualifier; preserve frontmatter and tooling metadata. Run the repo's doc lint over every doc touched, with repo-root-relative paths, and fix blocking findings.
14
+ 5. Write `apply-{doc}.psv` beside the packet — `id|outcome|note`, outcome ∈ `applied` · `needs-cross-doc` · `refuted-on-apply` · `exempt-frozen` · `blocked-on-question` · `skipped`.
15
+
16
+ **The audit never edits code.** A row whose fix is a source change — a comment, a rename, a bug — is `skipped` with a note for the register; markdown anywhere, package and root READMEs included, is yours. Rulings in the pack outrank every recorded decision.
17
+
18
+ Tree state: re-check every citation against today's tree before acting. READ-ONLY outside the packet's docs and your outcome file. Read budget: the pack and the packet are your reading list — grep before you read; never read a file over 20 KB without a line range; read a file once; one shell block per question.
19
+
20
+ Return **at most 120 words**: counts per outcome, docs touched, the lint result, and anything for the register.
21
+ '''
@@ -0,0 +1,28 @@
1
+ # dropped Claude tools: Read, Grep, Glob, Bash, Write
2
+ name = "polygraph-finder"
3
+ description = "Sweeps one document against the code it describes for the polygraph skill — one row per claim, cited, into a partial file. Dispatched by the skill only; never on its own."
4
+ developer_instructions = '''
5
+
6
+ # Polygraph finder
7
+
8
+ You are a FINDER in a truth audit. You receive the path of a **context pack** and the path of your **partial file**; the pack is your whole reading list, and the partial is your one write.
9
+
10
+ Docs state the target design; code is presumed correct unless a standing decision **names** it for retirement — the pack's precedent section decides, and a match means the precedent names your subject (quote the naming words into evidence; a resembling thing is not a match; unsure → QUESTION).
11
+
12
+ 1. Read the pack in full. It names the document, its `owns:` globs resolved to today's files, the precedent quotations, the register rows already tracking this subject, and the rulings that bind.
13
+ 2. Read the document once, whole — it is the one file you always read in full. Then check each claim it makes against the code: grep for the mechanism, open the passage with a line range, cite `file:line`.
14
+ 3. Write one row per claim to the partial file, **no header**, exactly ten pipe-separated fields: `id|subject|claim|evidence|counter_evidence|verdict|confidence|depends_on|status|resolution`. A literal `|` becomes `¦`, a newline `; `. Verdicts: `COHERENT` (emit at the unit you audited, so coverage is countable) · `MISDESCRIBED` · `UNDOCUMENTED` · `UNBUILT` · `RESIDUE` · `CONTESTED` · `QUESTION`. `id` is `{doc-stem}-NNN`; `status` is `open`; `resolution` is empty; `confidence` is an honest 0–1.
15
+ 4. Sweep the other direction once: code inside the `owns:` globs that no claim in the document covers is an `UNDOCUMENTED` row — after the precedent test.
16
+
17
+ Rules that bind every row:
18
+
19
+ - **No citation, no row** — `file:line`, or the exact command and its result. Prove or refute; never confirm by plausibility.
20
+ - **A negative claim about a long document quotes the passage you read** — the sentence you believe wrong, or the section where the missing statement should live. Unsupported negative claims are the dominant false-positive class.
21
+ - **An already-tracked finding is skipped** unless your evidence adds something new; cite the register row as `already-registered: path:line` in `counter_evidence`.
22
+ - **A document that should not exist** — its subject gone, its authority absorbed by a sibling — is one `QUESTION` row proposing deletion or merge, with the inbound-reference count.
23
+ - **Rulings in the pack outrank every recorded decision.**
24
+
25
+ Tree state: re-check every citation against today's tree before writing it. **READ-ONLY everywhere except your partial file.** Read budget: the pack is your reading list — read nothing outside it without naming in your return why the pack was insufficient. Grep before you read; never read a file over 20 KB without a line range; read a file once. Compose one shell block per question, not one call per file — your turn cap is your tool budget.
26
+
27
+ Return **at most 120 words**: the partial's path, the row count, up to three findings, and limitations. Findings go in the file, never in the return.
28
+ '''
@@ -0,0 +1,17 @@
1
+ # dropped Claude tools: Read, Grep, Glob, Bash, Write
2
+ name = "polygraph-verifier"
3
+ description = "Blind re-derivation of polygraph claims the primary doubts — claim and subject only, no prior evidence — into a verify file. Dispatched by the polygraph skill only; never on its own."
4
+ developer_instructions = '''
5
+
6
+ # Polygraph verifier
7
+
8
+ You are a VERIFIER in a truth audit. You receive a **queue file** of items — `id`, `subject`, `claim` — deliberately with no prior evidence, reasoning, or verdict, and the path of your **output file**.
9
+
10
+ 1. Read the queue. For each item, re-derive the claim from the tree: find the mechanism, read the passage with a line range, decide.
11
+ 2. Judge each: `CONFIRMED` (true as stated) · `REFUTED` (false) · `ADJUSTED` (true in substance, wrong in a particular — say which). Cite exactly, `file:line`.
12
+ 3. Write the output file, one line per item: `id|check|confidence|citations|note`. A literal `|` becomes `¦`.
13
+
14
+ **A wrong confirmation is worse than an honest REFUTED.** Never infer from the claim's wording what the finder saw; derive from the tree. Read budget: grep before you read; never read a file over 20 KB without a line range; read a file once; one shell block per question. READ-ONLY everywhere except your output file.
15
+
16
+ Return **at most 120 words**: the file path and counts per check.
17
+ '''
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.14.8",
4
+ "version": "3.15.2",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -25,5 +25,5 @@
25
25
  }
26
26
  ],
27
27
  "docsRoot": "docs",
28
- "digest": "5de54f6b1a71"
28
+ "digest": "05a56f90cade"
29
29
  }
@@ -36,7 +36,7 @@ import {
36
36
  } from "./lib/state.mjs";
37
37
  import { readSafe, relIfInside, today, SOURCE_EXT, commandRoot, docsRoot } from "./lib/repo.mjs";
38
38
  import { sweepResolvedRows, docLint, hasHeaderComment } from "./lib/sweep.mjs";
39
- import { listPlans } from "./lib/plans.mjs";
39
+ import { listPlans, polygraphVerdict } from "./lib/plans.mjs";
40
40
  import { frozenStatusAtHead } from "./lib/frozen.mjs";
41
41
  import { undischargedDocRows } from "./lib/ledger.mjs";
42
42
  import { approvedPackages, rejectedPackages } from "./lib/approved.mjs";
@@ -312,7 +312,8 @@ function detectorForFile(projectDir, rel, meta, rows) {
312
312
  if (rel.startsWith(`${root}/history/plans/`)) {
313
313
  const planText = readSafe(path.join(projectDir, rel));
314
314
  resolveRows(projectDir, (row) => row.kind === "needs-session" && row.body.includes(rel));
315
- if (/^\*\*Status:\*\*\s*Complete\b/m.test(planText) && undischargedDocRows(planText).length > 0) {
315
+ const complete = /^\*\*Status:\*\*\s*Complete\b/m.test(planText);
316
+ if (complete && undischargedDocRows(planText).length > 0) {
316
317
  rows.push(
317
318
  formatNeedsSessionRow({
318
319
  date: meta.date,
@@ -321,6 +322,19 @@ function detectorForFile(projectDir, rel, meta, rows) {
321
322
  })
322
323
  );
323
324
  }
325
+ // Whether the plan's documents are TRUE is the polygraph skill's judgment, not a hook's — so
326
+ // the gate's trace (the `Polygraph:` segment of the Gates line) is raised the same way the doc
327
+ // ledger is: a `needs-session:` row while it reads `pending` on a Complete plan. A plan with no
328
+ // segment predates the gate or runs under a harness that ships no polygraph — out of contract.
329
+ if (complete && polygraphVerdict(planText) === "pending") {
330
+ rows.push(
331
+ formatNeedsSessionRow({
332
+ date: meta.date,
333
+ description: `${rel} reached Status: Complete with its documents uncertified — run the polygraph skill (gate mode) and write its Polygraph: segment`,
334
+ refs: rel,
335
+ })
336
+ );
337
+ }
324
338
  }
325
339
  if (rel.startsWith(`${root}/`)) {
326
340
  const findings = docLint(projectDir, rel);
@@ -28,7 +28,16 @@ export function inPlanScope(rel, scope) {
28
28
  });
29
29
  }
30
30
 
31
- // Every plan in the folder, parsed once: { file, status, gates, scope, hasLedger,
31
+ // The plan's Polygraph gate, read from its Gates line: 'pending' · 'passed' · 'owes nothing' ·
32
+ // null when the line carries no segment (a plan that predates the gate, or a repo whose harness
33
+ // ships no polygraph — both out of contract, both pass).
34
+ export function polygraphVerdict(text) {
35
+ const gates = (/^\*\*Gates:\*\*\s*(.+)$/m.exec(text || "") || [])[1] || "";
36
+ const m = /Polygraph:\s*(pending|passed|owes nothing)\b/i.exec(gates);
37
+ return m ? m[1].toLowerCase() : null;
38
+ }
39
+
40
+ // Every plan in the folder, parsed once: { file, status, gates, polygraph, scope, hasLedger,
32
41
  // hasDocLedger, ledgerRows }. `gates` is null on legacy plans (pre-Gates format).
33
42
  export function listPlans(projectDir) {
34
43
  const dir = path.join(projectDir, docsRoot(projectDir), "history", "plans");
@@ -47,6 +56,7 @@ export function listPlans(projectDir) {
47
56
  file,
48
57
  status,
49
58
  gates: (/^\*\*Gates:\*\*\s*(.+)$/m.exec(text) || [])[1]?.trim() || null,
59
+ polygraph: polygraphVerdict(text),
50
60
  scope: planScopePaths(text),
51
61
  hasLedger: /^##\s+Test Obligations\b/m.test(text),
52
62
  hasDocLedger: /^##\s+Doc Obligations\b/m.test(text),
@@ -1,3 +1,11 @@
1
+ ---
2
+ paths:
3
+ - "**/wrangler.*"
4
+ - "**/.dev.vars*"
5
+ - "**/worker/**"
6
+ - "**/workers/**"
7
+ - "**/src/**/*.ts"
8
+ ---
1
9
  # Cloudflare Worker
2
10
 
3
11
  Conventions for Cloudflare Workers — `wrangler.jsonc` setup, vars and secrets, and code shape (dispatch, errors, modules, WebSockets). Schema design doctrine is the `schema-design` skill.
@@ -1,3 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "**/*.sql"
4
+ - "**/migrations/**"
5
+ - "**/drizzle/**"
6
+ - "**/drizzle.config.*"
7
+ - "**/schema.ts"
8
+ - "**/schema/**"
9
+ - "**/db/**"
10
+ ---
1
11
  # Database
2
12
 
3
13
  Naming and the fixed column vocabulary for relational schemas in **every** Nurix project — one concept, one name, everywhere. Written for SQLite / Cloudflare D1 + Drizzle; a Postgres project keeps the same column names and semantics with native types (`uuid`, `timestamptz`, `boolean`). **Design doctrine — what earns a table, state vs. logs, junctions, FK policy, uniqueness traps — is the `schema-design` skill; consult it before any schema change.**
@@ -1,3 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "**/*.tsx"
4
+ - "**/*.jsx"
5
+ - "**/*.ts"
6
+ - "**/*.css"
7
+ - "**/*.html"
8
+ - "**/tailwind.config.*"
9
+ - "**/vite.config.*"
10
+ ---
1
11
  # Frontend
2
12
 
3
13
  Tailwind v4 + shadcn-based UI conventions for **web** apps — styling, the `@nurix/components` library, the `design.md` token artifact, and design-skill routing — plus the TypeScript conventions every package compiles under.
@@ -1,3 +1,12 @@
1
+ ---
2
+ paths:
3
+ - "**/package.json"
4
+ - "**/pnpm-workspace.yaml"
5
+ - "**/pnpm-lock.yaml"
6
+ - "**/.npmrc"
7
+ - "**/tsconfig*.json"
8
+ - "**/turbo.json"
9
+ ---
1
10
  # pnpm — `@nurix/*` Workspaces: naming, boundaries, and consuming
2
11
 
3
12
  How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and CLI design live in the `nustack` skill, not here — no `nustack` skill in the runtime ⇒ ask before crossing the publish boundary.
@@ -60,7 +60,7 @@ Test packages install at `latest` — outside the approved-packages pin set. Fak
60
60
 
61
61
  - **Suite, not scripts.** Tests live in the canonical test tree — `tests/` mirroring `src/`, or the convention the repo already uses — named `<subject>.test.ts`; shared fixtures, factories, and helpers in `tests/helpers/` (or the suite's existing shared home).
62
62
  - **Wire the canonical command.** `"test"` in `package.json` runs the whole unit suite (`vitest run`); e2e under `"test:e2e"`. A repo where `pnpm test` / `npm test` doesn't run the suite is misconfigured — fix the wiring, **never** route around it.
63
- - **Unit and E2E trees stay separate.** Vitest/Jest suites in `tests/`; Playwright/Maestro suites in `e2e/` with their own config — **never** mixed into the unit runner's glob.
63
+ - **Unit and E2E trees stay separate.** Vitest/Jest suites in `tests/`; Playwright/Maestro suites in `e2e/` with their own config — **never** mixed into the unit runner's glob. The e2e config **owns its world** — it builds, seeds, and boots the app itself (a `webServer` block or equivalent), so a journey passes from a cold shell like any other test and "the world was not running" is never why one did not run.
64
64
  - **The real-renderer stratum is a third tree** — `tests/rendered/` under the unit runner's *browser mode*, its own config, kept to geometry regressions that shipped and the composition assertions a component library cannot make alone. It is not a second component suite; anything a simulated DOM can prove stays in `tests/component/`.
65
65
  - **No test doubles for what you own.** Prefer real instances — in-memory SQLite for drizzle, the actual Express app via `supertest` — over mocks; mock **only** true externals (network via `msw`, time via the runner's fake timers).
66
66
  - **Determinism.** No real network, no real time, no shared mutable state between tests; every test **must** pass alone, in any order, **and from a cold shell** — never against a hand-started dev server (Workers: `@cloudflare/vitest-pool-workers`; Express: `supertest` against the app instance).
@@ -4,7 +4,7 @@ The shared style for every file an agent obeys — a `CLAUDE.md`, a rule, an age
4
4
 
5
5
  ## The test for every line
6
6
 
7
- An instruction file is paid for whenever it loads, scaled by its load class: `CLAUDE.md` and rules load on **every turn of every session** — the most expensive prose in a repo per word; an agent definition loads per dispatch, a skill per invocation. The bar is not "is this true?" but **"must this be in front of the agent whenever this file loads?"** Measure weight in tokens, not lines; trim biggest-first on every edit.
7
+ An instruction file is paid for whenever it loads, scaled by its load class: `CLAUDE.md` and unscoped rules load on **every turn of every session** — the most expensive prose in a repo per word; a `paths:`-scoped rule loads on those same terms from its first matching file read on; an agent definition loads per dispatch, a skill per invocation. The bar is not "is this true?" but **"must this be in front of the agent whenever this file loads?"** Measure weight in tokens, not lines; trim biggest-first on every edit.
8
8
 
9
9
  **Rewriting, the test is armor vs. narration.** A reason clause either blocks a move an agent would otherwise make — the failure is counter-intuitive, the wrong fix looks correct, or the rule reads as arbitrary tidiness and gets "improved away" — or it explains the self-evident. Ask of each: *does the forbidden move look correct to an agent mid-task?* Keep the first kind, cut the second. **A constraint may lose its explanation; it may never lose its existence** — compression is fewer words for the same constraints, so a passage that cannot shrink without dropping one stays long.
10
10
 
@@ -8,13 +8,13 @@ The `boundary` skill parses the **Proposed Changes** sections to check boundary
8
8
 
9
9
  **Doc Obligations is the one ledger the plan's author writes.** Deriving tests from behavior is judgment, which is why `testing` owns that section and it is marked never-hand-author; resolving Proposed Changes' file paths to the feature docs that own them is a **lookup** against the ownership index already injected at session start. Different work, different rule — fill it yourself, while the touch list is fresh.
10
10
 
11
- The **`Gates:`** line carries one machine-read segment. The compliance hook greps **`Boundary:`** — implementation edits inside the plan's scope are blocked while it reads `Boundary: pending`; the `boundary` skill rewrites it to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`. The other two gates are enforced by **section presence**: a plan with no `## Test Obligations` or no `## Doc Obligations` section blocks the same edits — the sections are the artifacts, and the gate derives from them, not from a claim about them. Never hand-edit the line to skip a gate.
11
+ The **`Gates:`** line carries two machine-read segments. The compliance hook greps **`Boundary:`** — implementation edits inside the plan's scope are blocked while it reads `Boundary: pending`; the `boundary` skill rewrites it to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`. The other two plan gates are enforced by **section presence**: a plan with no `## Test Obligations` or no `## Doc Obligations` section blocks the same edits — the sections are the artifacts, and the gate derives from them, not from a claim about them. **`Polygraph:`** is the completion gate's trace: it reads `Polygraph: pending` from the first draft until the `polygraph` skill (gate mode) certifies the plan's documents — every Doc Obligations row discharged **updated** plus every `Spec:` doc — and rewrites it to `Polygraph: passed YYYY-MM-DD (N docs · N claims · N corrected · N registered)`. A plan that touched no document writes `Polygraph: owes nothing — {reason}` by hand; nothing else on this segment is ever hand-written. A Complete plan still reading `pending` raises a `needs-session:` queue row and fails CI. Never hand-edit the line to skip a gate.
12
12
 
13
13
  ```
14
14
  # {Feature Name}
15
15
 
16
16
  **Status:** Draft | In Progress | Complete | Abandoned
17
- **Gates:** Boundary: pending
17
+ **Gates:** Boundary: pending · Polygraph: pending
18
18
  **Spec:** [docs/features/{feature}.md](../../features/{feature}.md) <!-- one line per feature doc this plan implements against; a plan spanning three features names three -->
19
19
  **Started:** YYYY-MM-DD
20
20
  **Completed:** YYYY-MM-DD <!-- fill in the SAME edit that flips Status to Complete — along with every ledger Outcome cell: the flip freezes the file instantly, so anything left for a second edit is unreachable (the frozen-record gate rightly blocks it) -->
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: polygraph
3
+ description: "Use when a document's truth is in question — audit a named doc, feature, folder, or code path against the code — and at every plan completion, where gate mode certifies the plan's docs before the freeze. Never the whole tree by reflex; not docs health, boundary, or the verification gate."
4
+ ---
5
+
6
+ # Polygraph — two witnesses, one verdict
7
+
8
+ Code and docs are two witnesses to the same product, and each lies in its own direction: documents describe designs the code never built, code runs behaviour no document learned. The audit checks a document's claims against the code, presumes the code correct unless a standing decision names it for retirement, corrects the document, registers the code-side findings, and puts to the owner what evidence cannot settle. It runs at two moments and nowhere else:
9
+
10
+ | Moment | Mode | Ends with |
11
+ | --- | --- | --- |
12
+ | Any time, on a named target — a doc, a feature, a folder of docs, or a code path resolved to its owning docs | **Audit** | the target corrected, code-side rows registered, verdict counts reported |
13
+ | Plan completion — after the verification gate has discharged and the Doc Obligations rows read **updated**, before the freeze | **Gate** | the same, over the plan's docs, plus the plan's `Polygraph:` segment |
14
+
15
+ You are the **primary**: you hold the conversation, compose every context pack and packet, judge every contested verdict, and write the segment. Agents sweep and apply; a verdict is never delegated, and neither is a word said to the owner.
16
+
17
+ References — read the routed file in full at the named moment:
18
+
19
+ | Reference | Load when |
20
+ | --- | --- |
21
+ | `references/rows.md` | writing or reading any row; judging CONTESTED, QUESTION, residue, or a document's right to exist |
22
+ | `references/dispatch.md` | composing a context pack, dispatching any role, packeting, recovering a dead or capped agent, pricing a run |
23
+
24
+ ## Standing rules
25
+
26
+ 1. **Code is presumed correct — unless a standing decision names it.** The precedent pack decides by **naming**: a decision condemns code only when it names the file, table, route, command, or surface by the product's own name for it, and the row quotes the naming words. A resembling thing is not a match; arguable → QUESTION. Documenting undocumented code is the mechanically easiest resolution and quietly canonizes a retiring lane; condemning on a resemblance retires code the product still ships. The full test is `references/rows.md` § The residue guard.
27
+ 2. **No citation, no row** — `file:line`, or the exact command and its result. Prove or refute, never confirm by plausibility.
28
+ **A documented behaviour the code lacks is UNBUILT, never MISDESCRIBED.** The document may be stating the target design; rewriting it to match the code silently abandons that design. Without the repo's unbuilt marker on the passage or a precedent naming the subject, the row is a QUESTION for the owner, and the passage stands until the answer.
29
+ 3. **Finders never edit; appliers never judge; the audit never edits code.** A code-side finding leaves as a register row for the planning workflow, however small and however directly it is asked for; a source fix asked for *inside* the audit is refused, registered, and answered with the one thing that would let it happen — a separate ask outside the audit. **Markdown is not source**: every document read may be edited under the applier discipline; frozen records are exempt.
30
+ 4. **The applier re-check is the gate.** Every row is re-derived against today's tree before its doc is touched; a claim that fails to reproduce dies as *did not reproduce* and never becomes an edit. This is what lets a row be applied by an agent that never saw the finder's reasoning.
31
+ 5. **Rows live on disk, never in your context.** Partials, packets, and outcomes land under `.claude/state/polygraph/{run}/`; you re-read files. The folder is machine-local and you delete it when the run closes — in the same turn as the report, **whatever is still open**: the reply carries every open row in full, an owner's answer starts a new run over those rows only, and the changelog entry of the commit is the record. Nothing under `.claude/state/polygraph/` outlives the reply.
32
+ 6. **The owner's answer is applied in the same pass** — verbatim in the doc it settles, quoted in the changelog entry's Ask line. There is no question ledger to park it in.
33
+
34
+ ## The run
35
+
36
+ Both modes run the same sequence; the modes differ only in how the target list and the closing write are derived.
37
+
38
+ 1. **Resolve the targets.** A doc path is a target; a feature name resolves to `docs/features/{feature}.md`; a folder yields every doc in it; a code path resolves through the injected ownership index to the docs whose `owns:` globs match — or, when no index is in context, by scanning the `owns:` frontmatter under the docs root. A code path with no owner is **reported, never refused** — say which paths are unowned and go on with the rest. A target with no document at all is the one refusal: there is nothing to audit.
39
+ 2. **Price it and say so** before dispatching when the target list exceeds ten documents: per-doc cost at the pinned tier, the wave count under the runtime's concurrency ceiling, and the total. An owner directive to run on a stronger tier is priced the same way — state the multiplier and record the directive in the changelog entry.
40
+ 3. **Write one context pack per doc** (`references/dispatch.md` § The context pack) — the doc's `owns:` globs, the code paths in question, the precedent quotations that name this subject, the register rows already tracking it, and the owner rulings that bind. The pack is the agent's whole reading list.
41
+ 4. **Dispatch one `polygraph-finder` per doc**, in waves under the concurrency ceiling. Each writes its partial to the run folder and returns at most 120 words. A finder that returns **partial** at its turn cap is resumed once from its partial on disk; a second partial is accepted as that doc's sweep and reported as such.
42
+ 5. **Merge and check.** Concatenate the partials into `findings.psv`, quarantine malformed lines into `malformed.txt` (kept, never silently repaired), run `scripts/audit.mjs {run}` and fix what it reports by hand.
43
+ 6. **Judge.** Read every non-COHERENT row's evidence yourself. CONTESTED rows you judge with both sets of evidence in hand; a row you doubt goes to a `polygraph-verifier` (claim and subject only) and comes back CONTESTED if the two disagree. QUESTION rows — and every proposed deletion, with its inbound count — go to the owner **now, inline**, batched and ranked by how many rows each answer unblocks; work proceeds on everything else while they are open.
44
+ 7. **Packet and apply.** One doc, one `polygraph-applier`, always — `audit.mjs` asserts no collision. MISDESCRIBED rows and in-scope UNDOCUMENTED rows that passed the residue guard go to the owning doc's packet; RESIDUE, UNBUILT-abandoned, and every code-side row go to the register (the backlog, in its existing grammar, deduped against what is already there); frozen records resolve `exempt-frozen`. Fold each applier's outcome file into row status by hand, then run `audit.mjs` again.
45
+ 8. **Run the repo's doc gates in your own seat** — lint, links, ownership — over every doc touched; a delegated gate that dies reports nothing, which reads exactly like a pass.
46
+ 9. **Report** in prose: docs audited, claims checked, corrected, registered, contested-and-judged, questions answered; the refuted-on-apply count is the quality signal, stated every time. Then delete the run folder — also when rows are still open for the owner; they are in the reply.
47
+
48
+ ## Gate mode — the closing write
49
+
50
+ Gate mode's target list is **every Doc Obligations row discharged as `updated` plus every `Spec:` doc**, and its code scope is the plan's Proposed Changes paths together with those docs' `owns:` globs. It runs after the verification gate and the Doc Obligations discharge, before `Status: Complete` is written — the freeze waits for it, and the harness's completion policy says so. The skill writes the `Polygraph:` segment and nothing else on the plan: the status flip is the author's, in the discharge-then-freeze edit.
51
+
52
+ Ends by rewriting your own segment of the plan's **`Gates:`** line, exactly:
53
+
54
+ - `Polygraph: passed YYYY-MM-DD (N docs · N claims · N corrected · N registered)` — every target swept, every non-QUESTION row resolved.
55
+ - `Polygraph: owes nothing — {reason}` — written by the plan's **author**, not by you, when the plan has no `Spec:` doc and its Doc Obligations row is `None`, or when the repo's harness ships no polygraph.
56
+
57
+ A QUESTION the owner has not answered leaves the segment at `pending`; the plan stays open, and the completion machinery says why. Never write `passed` over an open question.
58
+
59
+ ## Economy
60
+
61
+ The tier is pinned in the three agent definitions — the cheapest capable tier, low to medium effort, a turn cap — never chosen at dispatch. The strong tier's value concentrates in judgment, and every judgment in this skill is yours. Where a role agent is not installed (a runtime whose plugin channel carries skills only), dispatch an independent subagent on the cheapest capable tier with the role's body inline, and say so in the report.
62
+
63
+ ## What this skill is NOT
64
+
65
+ - **Routine docs health** — stale globs, broken links, structural drift: the `maintenance` agent.
66
+ - **A plan's contract check** — both sides of a boundary for a diff or plan: the `boundary` skill.
67
+ - **Test verification** — proving built behaviour: the `testing` skill.
68
+ - **A whole-tree audit by reflex.** The tree is audited one target at a time, and every plan certifies its own docs; a full sweep is an explicit ask, priced before it runs.
69
+ - **A code editor** — code-side findings exit as register rows (standing rule 3).
70
+
71
+ If the request is one of these, say so and redirect — don't adapt.
@@ -0,0 +1,80 @@
1
+ # Dispatch — packs, roles, packets, recovery, price
2
+
3
+ Load when: composing a context pack, dispatching any role, packeting rows, recovering a dead or turn-capped agent, or pricing a run.
4
+
5
+ ## The run folder
6
+
7
+ `.claude/state/polygraph/{YYYY-MM-DD-slug}/` — machine-local, never tracked, deleted when the run closes:
8
+
9
+ ```
10
+ partials/{doc}.psv one per finder — its whole output, no header
11
+ findings.psv the merge, header first — the working copy
12
+ malformed.txt quarantined lines, kept
13
+ packets/{doc}.txt one per applier: `doc: {path}` then row ids and bounce notes
14
+ verify-{group}.psv id|check|confidence|citations|note
15
+ apply-{doc}.psv id|outcome|note
16
+ packs/{doc}.md the context pack each finder and applier reads
17
+ ```
18
+
19
+ Work items pass through files, never through a dispatch call's arguments — a resumed agent may not carry arguments, and one that dies on a missing argument dies before it reads anything.
20
+
21
+ ## The context pack — the agent's whole reading list
22
+
23
+ One page per target document, written by you before the first dispatch. A brief that sends an agent to the charter, the backlog, the ownership index, or the findings file hands it a 500 KB read to extract 2 KB of fact — and hands the same read to every other agent in the wave. The pack carries the 2 KB:
24
+
25
+ | Section | Content |
26
+ | --- | --- |
27
+ | Target | the doc path; in gate mode, the plan path and the Proposed Changes paths that intersect this doc's `owns:` |
28
+ | Code | the doc's `owns:` globs, resolved to the files that exist today |
29
+ | Precedent | the quotations from guardrails, accepted decisions, and doctrine that **name** a subject this doc covers — the naming words only, with `path:line` |
30
+ | Already tracked | backlog and register rows that name a subject this doc covers, as `path:line` |
31
+ | Rulings | owner answers that bind this subject, verbatim |
32
+ | Read budget | the two clauses below, verbatim |
33
+
34
+ The two clauses every pack carries:
35
+
36
+ > Tree state: re-check every citation against today's tree before acting; a claim that fails to reproduce dies as *did not reproduce* — never becomes an edit. READ-ONLY everywhere except your one output file.
37
+
38
+ > Read budget: the pack is your reading list — read nothing outside it without naming in your return why the pack was insufficient. Grep before you read; never read a file over 20 KB without a line range; read a file once. Compose one shell block per question, not one call per file.
39
+
40
+ ## The roles
41
+
42
+ | Role | Agent | Receives | Writes | Returns |
43
+ | --- | --- | --- | --- | --- |
44
+ | Finder | `polygraph-finder` | the pack path, the partial path | `partials/{doc}.psv` — one row per claim, ten fields, no header | ≤ 120 words: the file path, row count, up to three findings, limitations |
45
+ | Verifier | `polygraph-verifier` | a queue of `id`, `subject`, `claim` — no evidence, no verdict | `verify-{group}.psv` | ≤ 120 words: the file path and counts per check |
46
+ | Applier | `polygraph-applier` | the packet path, the pack path | the packet's docs; `apply-{doc}.psv` | ≤ 120 words: counts per outcome, docs touched, lint result, anything for the register |
47
+
48
+ Each agent definition carries its role's doctrine and its turn cap; a dispatch names the target and the paths and nothing else. **Findings go in the file, never in the return** — every word returned is re-billed on every later turn of the session.
49
+
50
+ Where the runtime has no such agent installed, dispatch an independent subagent on the cheapest capable tier with the agent file's body pasted in as its brief, and say so in the report.
51
+
52
+ ## Packets — one doc, one applier
53
+
54
+ Routing is judgment, so you compose packets by hand; `audit.mjs` asserts the one mechanical invariant: **no document appears in two packets** — appliers run in parallel, and two agents in one file silently lose each other's edits.
55
+
56
+ **Packet by the doc the fix belongs in, not the doc a row's evidence cites.** A row's citations say where the *evidence* lives; the fix usually belongs elsewhere, and a bounced row's `needs-cross-doc` note says exactly where. A row with no derivable doc goes to the owner — never into a packet "to see what the agent makes of it".
57
+
58
+ By verdict: QUESTION and `depends_on` rows → the owner, inline. CONTESTED → you, with both sets of evidence. RESIDUE, and any row whose subject is a source file — a stale comment included — → the register. Everything else → the owning doc's packet.
59
+
60
+ ## Verification — budgeted by what it caught
61
+
62
+ - **The applier re-check is the law.** In the proving run it killed more false positives than every other mechanism combined.
63
+ - **Blind re-derivation is discretionary.** Send a row *you* doubt — claim and subject only. As a mandatory stage it refuted nothing: finders' self-doubt is conservative, and the errors live in the confident pile, which is where your own read of the evidence looks. Disagreement makes the row CONTESTED; a verification never overwrites a verdict.
64
+
65
+ ## Fold-back
66
+
67
+ After each applier returns, its outcomes become row state by your own edit, then `audit.mjs` checks the arithmetic:
68
+
69
+ - `applied` · `registered` · `exempt-frozen` → `resolved`, the resolution stamped with how.
70
+ - `refuted-on-apply` → `resolved`, resolution `did not reproduce`. A legitimate death; its count is the quality signal you report.
71
+ - `needs-cross-doc` · `blocked-on-question` · `skipped` → stays open — repacket, or hold for the answer.
72
+ - COHERENT rows close when recorded; they carry no work.
73
+
74
+ ## Recovery
75
+
76
+ **The file on disk is the recovery unit, never the wave.** A dead agent costs its remainder; read what landed and relaunch only that. A finder that returns **partial** at its turn cap is resumed once from its partial — a second partial is accepted as that doc's sweep and named as such in the report. Every dispatch names its output file and a time bound; an agent past its bound with no file is dead — relaunch it, never wait on it, and never let one runaway agent hold the run. Gates — lint, links, ownership — run in your own seat.
77
+
78
+ ## Price
79
+
80
+ State before dispatching, whenever a run exceeds ten documents or an owner directive names a tier: the per-doc cost at the pinned tier, the wave count under the concurrency ceiling, and the total; for a stronger tier, the multiplier and both totals. The directive and the estimate go in the changelog entry.
@@ -0,0 +1,57 @@
1
+ # Rows — the row, the verdicts, the residue guard
2
+
3
+ Load when: writing or reading any row, judging a CONTESTED or QUESTION row, deciding whether a subject is residue, or ruling on a document's right to exist.
4
+
5
+ ## The row
6
+
7
+ Pipe-delimited, one row per claim, **ten fields**, header line first. A literal `|` inside text is written `¦`; a newline becomes `; `.
8
+
9
+ ```
10
+ id|subject|claim|evidence|counter_evidence|verdict|confidence|depends_on|status|resolution
11
+ ```
12
+
13
+ | Field | Content |
14
+ | --- | --- |
15
+ | `id` | `{DOC}-{NNN}` — the target document's stem and a running number, unique within the run |
16
+ | `subject` | the stable key — a file, table, route, command, or surface |
17
+ | `claim` | one sentence: what is asserted true or false |
18
+ | `evidence` | verbose support — `file:line` citations or the exact command and its result. **No citation, no row.** A verdict leaning on a precedent quotes the precedent's naming words here |
19
+ | `counter_evidence` | what cuts the other way, including `already-registered: path:line` for a known item |
20
+ | `verdict` | one of the seven below |
21
+ | `confidence` | 0.0–1.0, honest — a signal that tells the primary where to look, never a routing key |
22
+ | `depends_on` | empty, or the owner question the row waits on, as the question's own words in brief |
23
+ | `status` | `open` · `resolved` · `reopened` |
24
+ | `resolution` | how it closed — `applied` · `registered` · `did not reproduce` · `answered` · `exempt-frozen` — empty while open |
25
+
26
+ **A negative claim about a long document quotes the passage read** — the sentence believed wrong, or the section where the missing statement should live. Unsupported negative claims are the dominant false-positive class, and they concentrate in the longest documents.
27
+
28
+ ## The seven verdicts
29
+
30
+ | Verdict | Meaning | Disposition |
31
+ | --- | --- | --- |
32
+ | `COHERENT` | doc and code agree — emitted at the audited unit so coverage is countable; closed when recorded | none |
33
+ | `MISDESCRIBED` | the doc is wrong about built behaviour | an applier fixes the doc |
34
+ | `UNDOCUMENTED` | code no doc claims — judged against the precedent pack **first**: condemned code is RESIDUE, never a documentation gap | an applier documents it in the owning doc |
35
+ | `UNBUILT` | a documented design with no code behind it — decided-unbuilt (the repo's marker convention), abandoned, or a document that should die; only the owner distinguishes the last two, so suspecting either makes the row a QUESTION | the marker |
36
+ | `RESIDUE` | code a standing decision condemns by name — running, even load-bearing, and still retiring | a register row, never a doc edit |
37
+ | `CONTESTED` | the evidence conflicts — a verifier and a finder disagree, or a claim has no code to arbitrate and a sibling document says otherwise | the primary judges with both sets of evidence in hand |
38
+ | `QUESTION` | only the owner can resolve it — intent, not evidence, is what is missing | asked inline, answered in the same pass |
39
+
40
+ A disposition is the verdict's default; where the primary overrides it, the resolution says so. **A verification never overwrites a verdict** — disagreement makes the row CONTESTED.
41
+
42
+ ## The residue guard
43
+
44
+ Before an UNDOCUMENTED row becomes a doc edit, its subject is tested against the precedent pack — the one place the audit can silently undo the repo's own decisions. The test is **naming**: a precedent matches only when it names the subject — the file, table, route, command, or surface by the product's own name for it — and the row quotes the naming words into `evidence`. A precedent describing a *resembling* thing is not a match; shared adjectives ("legacy", "old export lane") are not a match; a misread pronoun is not a match. Unsure → QUESTION, never a silent RESIDUE.
45
+
46
+ The asymmetry runs both ways. A missed match resurfaces: the register is owner-read, and an UNDOCUMENTED row that should have been RESIDUE is caught when the owner reads the doc it produced. A false match does not: it retires code the product still ships and withholds its documentation — worse than the disease. When in doubt, ask.
47
+
48
+ ## Frozen records and code
49
+
50
+ - **Frozen records are exempt** — a completed plan, a changelog entry, an archived document resolves `exempt-frozen`; its claims were true when frozen and the tree does not correct history.
51
+ - **The audit never edits code.** A code-side finding — a bug, a stale source comment, the unbuilt, residue — exits as a register row for the planning workflow, however small the edit and however directly it is asked for. **Markdown is not source**: every document read may be edited under the applier discipline, package and root READMEs included; the line is doc versus code, not proximity to `src/`.
52
+
53
+ ## A document's right to exist
54
+
55
+ A wholly stale document passes a sentence-by-sentence audit one true sentence at a time, so a finder that reads a document whose subject is gone, whose authority another doc has absorbed, or whose content belongs inside a sibling emits **one QUESTION row proposing deletion or merge**, carrying the inbound-reference count (who links here, which index rows list it, which ownership globs it claims). Deletion is owner-approved, always. An approved deletion removes the file, repoints every inbound reference to the surviving authority, and leaves **no redirect stub** — an annotation, a banner, or a status flag left in the reading path is not a source of truth however carefully the surviving sentences are hedged. A document whose subject survives but whose content is wrong is MISDESCRIBED, not a deletion candidate.
56
+
57
+ **Abandonment has no marker.** A design decided against is not owed: its claims come **out** of every marker that carried them rather than being reworded inside one, and the document survives as the record of a decision not to build, its status saying so. The tell that this went wrong: a reader finishes the paragraph still expecting the feature.
@@ -0,0 +1,113 @@
1
+ // skills/polygraph/scripts/audit.mjs - The audit's one script, and it is READ-ONLY: it validates
2
+ // the run's findings file, cross-checks verifier and applier outcome files against the rows, and
3
+ // asserts the packet-collision invariant. It never writes a byte — the primary holds the pen;
4
+ // this holds the arithmetic. A checker that is wrong emits a wrong warning; a script that acted
5
+ // on its own verdicts could destroy a refutation with no tally, which is the defect class this
6
+ // replaces.
7
+ //
8
+ // Usage: node audit.mjs <run-dir> (the run folder holding findings.psv)
9
+ // Exit: 0 clean · 1 findings to read · 2 unusable input
10
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+
13
+ const HEADER = 'id|subject|claim|evidence|counter_evidence|verdict|confidence|depends_on|status|resolution';
14
+ const FIELDS = HEADER.split('|').length; // 10
15
+ const VERDICTS = new Set(['COHERENT', 'MISDESCRIBED', 'UNDOCUMENTED', 'UNBUILT', 'RESIDUE', 'CONTESTED', 'QUESTION']);
16
+ const STATUSES = new Set(['open', 'resolved', 'reopened']);
17
+ const RESOLVING = new Set(['applied', 'exempt-frozen', 'refuted-on-apply']);
18
+ const STAYING = new Set(['needs-cross-doc', 'blocked-on-question', 'skipped']);
19
+ const CHECKS = new Set(['CONFIRMED', 'REFUTED', 'ADJUSTED']);
20
+
21
+ const run = process.argv[2];
22
+ if (!run || !existsSync(run) || !statSync(run).isDirectory()) { console.error('usage: node audit.mjs <run-dir>'); process.exit(2); }
23
+ const findingsPath = join(run, 'findings.psv');
24
+ if (!existsSync(findingsPath)) { console.error(`no findings file at ${findingsPath}`); process.exit(2); }
25
+
26
+ const problems = []; // schema violations and cross-check disagreements → exit 1
27
+ const notes = []; // informational — never fails the run
28
+
29
+ // ---- parse + validate the working file -------------------------------------------------------
30
+ const lines = readFileSync(findingsPath, 'utf8').split('\n');
31
+ if (lines[0].trim() !== HEADER) { console.error(`bad or missing header line in ${findingsPath}`); process.exit(2); }
32
+
33
+ const rows = new Map(); // id → { verdict, status, resolution, line }
34
+ for (let n = 1; n < lines.length; n++) {
35
+ const line = lines[n];
36
+ if (!line.trim()) continue;
37
+ const p = line.split('|');
38
+ const at = `line ${n + 1}`;
39
+ if (p.length !== FIELDS) { problems.push(`${at}: ${p.length} fields, expected ${FIELDS}`); continue; }
40
+ const [id, , claim, evidence, , verdict, confidence, , status, resolution] = p;
41
+ if (!id.trim()) { problems.push(`${at}: empty id`); continue; }
42
+ if (rows.has(id)) problems.push(`${at} (${id}): duplicate id — first seen at line ${rows.get(id).line}`);
43
+ if (!VERDICTS.has(verdict)) problems.push(`${at} (${id}): unknown verdict "${verdict}"`);
44
+ const conf = Number(confidence);
45
+ if (!(conf >= 0 && conf <= 1)) problems.push(`${at} (${id}): confidence "${confidence}" outside 0..1`);
46
+ if (!STATUSES.has(status)) problems.push(`${at} (${id}): unknown status "${status}"`);
47
+ if (status === 'resolved' && !resolution.trim())
48
+ problems.push(`${at} (${id}): resolved with an empty resolution — how did it close?`);
49
+ if (!claim.trim() || !evidence.trim())
50
+ problems.push(`${at} (${id}): empty ${!claim.trim() ? 'claim' : 'evidence'} — no citation, no row`);
51
+ rows.set(id, { verdict, status, resolution, line: n + 1 });
52
+ }
53
+
54
+ // ---- cross-check outcome files against row status --------------------------------------------
55
+ const files = readdirSync(run).filter((f) => f.endsWith('.psv'));
56
+
57
+ // apply-*.psv → id|outcome|note
58
+ for (const f of files.filter((f) => f.startsWith('apply-'))) {
59
+ for (const line of readFileSync(join(run, f), 'utf8').split('\n')) {
60
+ if (!line.trim()) continue;
61
+ const [id, outcome] = line.split('|').map((s) => (s ?? '').trim());
62
+ const row = rows.get(id);
63
+ if (!row) { problems.push(`${f}: outcome for unknown row "${id}"`); continue; }
64
+ if (!RESOLVING.has(outcome) && !STAYING.has(outcome)) { problems.push(`${f} (${id}): unknown outcome "${outcome}"`); continue; }
65
+ if (RESOLVING.has(outcome) && row.status === 'open')
66
+ problems.push(`${f} (${id}): outcome "${outcome}" but the row is still open — the fold missed it`);
67
+ }
68
+ }
69
+
70
+ // verify-*.psv → id|check|confidence|citations|note ; disagreement across files is a finding
71
+ const seenChecks = new Map(); // id → { check, file }
72
+ for (const f of files.filter((f) => f.startsWith('verify-'))) {
73
+ for (const line of readFileSync(join(run, f), 'utf8').split('\n')) {
74
+ if (!line.trim()) continue;
75
+ const [id, check] = line.split('|').map((s) => (s ?? '').trim());
76
+ const row = rows.get(id);
77
+ if (!row) { problems.push(`${f}: verification of unknown row "${id}"`); continue; }
78
+ if (!CHECKS.has(check)) { problems.push(`${f} (${id}): unknown check "${check}" — CONFIRMED/REFUTED/ADJUSTED only`); continue; }
79
+ const earlier = seenChecks.get(id);
80
+ if (earlier && earlier.check !== check)
81
+ problems.push(`${id}: two verifiers disagree — ${earlier.check} in ${earlier.file}, ${check} in ${f} → the row is CONTESTED, judge it`);
82
+ else seenChecks.set(id, { check, file: f });
83
+ if (check === 'REFUTED' && row.verdict !== 'CONTESTED' && row.status !== 'reopened')
84
+ notes.push(`${id}: a verifier REFUTED this row and it is neither CONTESTED nor reopened — judged already, or missed?`);
85
+ }
86
+ }
87
+
88
+ // packets/*.txt → `doc: path` lines; no doc in two packets
89
+ const packetsDir = join(run, 'packets');
90
+ if (existsSync(packetsDir)) {
91
+ const claims = new Map(); // doc → packet file
92
+ for (const f of readdirSync(packetsDir).filter((f) => f.endsWith('.txt'))) {
93
+ for (const line of readFileSync(join(packetsDir, f), 'utf8').split('\n')) {
94
+ const m = /^doc:\s*(.+)$/.exec(line.trim());
95
+ if (!m) continue;
96
+ const doc = m[1].trim();
97
+ if (claims.has(doc)) problems.push(`packets: "${doc}" claimed by both ${claims.get(doc)} and ${f} — two appliers in one file lose edits`);
98
+ else claims.set(doc, f);
99
+ }
100
+ }
101
+ }
102
+
103
+ // ---- report -----------------------------------------------------------------------------------
104
+ const total = rows.size;
105
+ const resolved = [...rows.values()].filter((r) => r.status === 'resolved').length;
106
+ const hist = {};
107
+ for (const r of rows.values()) hist[r.verdict] = (hist[r.verdict] || 0) + 1;
108
+ console.log(`rows: ${total} · resolved: ${resolved} · open: ${total - resolved}`);
109
+ console.log(`verdicts: ${Object.entries(hist).map(([k, v]) => `${k} ${v}`).join(' · ') || 'none'}`);
110
+ for (const n of notes) console.log(`~ ${n}`);
111
+ for (const p of problems) console.log(`✗ ${p}`);
112
+ console.log(problems.length ? `${problems.length} finding(s) to read` : 'clean');
113
+ process.exit(problems.length ? 1 : 0);
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: testing
3
- description: "The test-lifecycle skill — three modes. Plan-gate mode: after `boundary` passes, derives the tests a plan owes and writes the Test Obligations ledger into the plan (a row per behavior: layer, target file, oracle, fixtures); writes no test files. Placement mode: layered test strategy — which ownership strata the application has, where a test lives and what it may assert; use for any testing-strategy, test-architecture, test-pyramid, or 'unit/integration/e2e?' question, even if 'layer' is never said. Gate mode: executes the verification gate — derives the behavioral checklist, drives the real surface (browser automation, the real CLI, HTTP; the `/test` runner where wired), pins durable behaviors, discharges every ledger row. Triggers on 'test plan', 'testing strategy', 'test architecture', 'test layers', 'where should this test live', 'run the verification gate'."
3
+ description: "The test-lifecycle skill — three modes. Plan-gate mode: after `boundary` passes, derives the tests a plan owes and writes the Test Obligations ledger into the plan (a row per behavior: layer, target file, oracle, fixtures); writes no test files. Placement mode: layered test strategy — which ownership strata the application has, where a test lives and what it may assert (on a rendered surface every user-observable promise lives in the surface's journey; component tests are residual); use for any testing-strategy, test-architecture, test-pyramid, or 'unit/integration/e2e?' question, even if 'layer' is never said. Gate mode: executes the verification gate — derives the behavioral checklist, drives the real surface (browser automation, the real CLI, HTTP; the `/test` runner where wired), pins durable behaviors, discharges every ledger row. Triggers on 'test plan', 'testing strategy', 'test architecture', 'test layers', 'where should this test live', 'run the verification gate'."
4
4
  ---
5
5
 
6
6
  # Testing — Ledger, Layers, Gate
@@ -30,9 +30,9 @@ You derive the tests an implementation plan owes and pin them into the plan as a
30
30
 
31
31
  ### Layer assignment
32
32
 
33
- Assign each behavior the **cheapest layer that can prove it**, then name the oracle. The ledger altitudes are `unit` / `component` / `integration` / `e2e` (strata mapping: § Bridge below); **integration is the default for server behavior** — a route/handler against a real in-memory datastore via the app instance. Error paths, races, and idempotency are obligations too, and belong at **integration** with injected faults (network mock, fake timers, forced constraint) — never e2e.
33
+ Assign each behavior the **cheapest layer that can prove it**, then name the oracle. The ledger altitudes are `unit` / `component` / `integration` / `e2e` (strata mapping: § Bridge below). **Integration is the default for server behavior** — a route/handler against a real in-memory datastore via the app instance; server-side error paths, races, and idempotency belong there with injected faults (network mock, fake timers, forced constraint). **A journey is the default for a rendered surface** — any promise a user could observe in a real browser (what a screen shows over real rows, what a click changes, where navigation lands and what the URL carries, what a refused or failed request shows) is owed at layer 8 as `extend journey <surface>`, never as a component test; the surface's own fault paths (a 500 on load, a 400 on submit) stay in the journey too, injected at the app's own endpoint from the browser. What a rendered surface still owes below the browser is enumerated in § Placement discipline: a pure derivation with real branches (layer 6), a regression pin for a state the journey world cannot reach (layer 7), the wire's error taxonomy (layer 5), and geometry that shipped wrong (7b).
34
34
 
35
- **The e2e altitude gate:** an e2e obligation is earned **only** when the behavior genuinely spans processes no single-process test can exercise together. Density is one scenario per cross-process journey, not per feature — prefer **"extend scenario `<name>`"** over **"new scenario"**. **Refuse to emit any e2e obligation while the project's e2e smoke gate is not wired in CI** — emit a **blocked** row naming the missing gate instead.
35
+ **The journey world is a precondition, not an excuse.** A rendered surface owes its journeys whether or not the repo's e2e config boots today. If it does not — no config, a world pointed at a deleted package, a job parked in CI — the plan's **first** obligation row is wiring it (`e2e · <config> · the world builds, seeds, boots and one journey passes from a cold shell`), and every journey row names it as a dependency. Emitting `blocked` rows in place of journeys and routing the promises down to component tests is the failure this rule exists to stop: a broken world that nothing obliges anyone to fix stays broken, and every UI promise for months lands in a simulated DOM. Density is one spec file per surface and one `test()` per user journey, carrying as many assertions as the journey has observable steps — prefer **"extend journey `<surface>`"** over a new file.
36
36
 
37
37
  ### Run (plan-gate)
38
38
 
@@ -41,7 +41,7 @@ Assign each behavior the **cheapest layer that can prove it**, then name the ora
41
41
  1. **Build the behavior list** from Proposed Changes + the LLD's promises + HIGH/CRITICAL boundary findings. Note compiler-guarded behaviors — they owe **no** test.
42
42
  2. **Assign layer + oracle** per behavior; cheapest proving layer wins.
43
43
  3. **Map to a target file** per `testing.md`; name fixtures/factories and fault injection; use the repo-standard fake-data source.
44
- 4. **Apply the e2e altitude gate**; downgrade everything else to integration.
44
+ 4. **Route by surface**: a user-observable promise on a rendered surface → `e2e` (its surface's journey, with the world-wiring row first if the world does not boot); server behavior → `integration`; only what § Placement discipline keeps below the browser → `component` / `unit`.
45
45
  5. **Emit the Test Obligations section into the plan** (an executor dispatched with only the plan sees nothing else), plus a one-line chat summary:
46
46
 
47
47
  ```
@@ -76,9 +76,9 @@ The maximum form, for a full-stack application. Each layer names what only it ca
76
76
  | 4 | **Contract artifact** | Nothing behavioral. Typed fixtures / interception handlers **compiled from the provider's own types**, exported for every consumer-side test | The typechecker — drift from the provider is a compile error | No runner; a module the provider owns |
77
77
  | 5 | **Client data** | The consumer's half of the wire: error taxonomy, parsing, byte fidelity, stream reassembly, retry/abort | Intercepted network serving layer-4 artifacts; no DOM | `msw`-class interception |
78
78
  | 6 | **View logic** | Pure presentation derivations: grouping, ordering, status vocabularies, formatting | Table-driven equality | Plain runner |
79
- | 7 | **Component / surface** | Interaction orchestration — what a user action changes on screen: buffer lifecycle, optimistic states, error surfaces | Rendered output a user can observe; network answered by layer 4 | DOM testing-library class + interception |
79
+ | 7 | **Component / surface** | **Residual on any stack that has layer 8.** Regression pins for what the journey world cannot reach: a state only a fixture the world lacks can produce, a flow behind an external system (a GitHub installation, an AI reply), an orchestration detail no user step exposes (a debounce, a buffer surviving an abort) | Rendered output a user can observe; network answered by layer 4 | DOM testing-library class + interception — never the default home of a UI promise |
80
80
  | 7b | **Rendered composition** | Defects only a real rendering engine produces: geometry (overlap, clipping, a container that grows when a panel opens), the theme cascade where your tokens meet a component library's, focus order and accessible names through a third-party primitive | Measurement in a real browser engine — `getBoundingClientRect`, computed style, the accessibility tree | Real-renderer component runner (the unit runner's browser mode), **never** a simulated DOM |
81
- | 8 | **System smoke** | Composition only — the layers are actually stacked: the build serves, auth flows end to end, one write round-trips | A real browser or real binary against the really-running app | Browser/process driver class; 3–5 journeys, zero variants |
81
+ | 8 | **Journeys** | Everything a user can observe on a rendered surface: what a screen shows over real rows, what an action changes, where navigation lands and what the URL carries, what a refused or failed request shows, a create-then-delete round trip — and composition: the build serves, auth flows end to end | A real browser (or the real binary/wire, for a stack that renders nothing) against the really-running app over a seeded world; faults injected at the app's own endpoints | Browser/process driver class; **one spec file per surface**, one test per user journey, variants wherever a user can see them |
82
82
 
83
83
  Layer 4 is the hinge. Every layer ≥5 runs on the assumption that layers 1–3 are green — and the artifact makes that assumption *enforceable*: when the provider's response type changes, the fixture stops compiling, and every consumer test fails at typecheck before it runs. A consumer-side test that hand-writes a wire response has smuggled unverified layer-3 knowledge upward; route it through the artifact.
84
84
 
@@ -88,7 +88,7 @@ Layer 4 is the hinge. Every layer ≥5 runs on the assumption that layers 1–3
88
88
 
89
89
  | Application shape | Strata present |
90
90
  | --- | --- |
91
- | Full-stack web app | all eight |
91
+ | Full-stack web app | all eight — 8 is the home of every UI promise, 7 is residual |
92
92
  | Backend-only service | 1, 2, 3, 8 |
93
93
  | CLI | 2, 3 (the CLI's argv/stdout contract), 8 |
94
94
  | Frontend on a third-party API | 4 (derived from the API's published spec), 5, 6, 7, 8 |
@@ -98,16 +98,16 @@ Derive the stack first, then place tests: a placement argument that starts "this
98
98
 
99
99
  ### Placement discipline
100
100
 
101
- - **Two conditions put a test below a real browser; both must hold.** *(a)* Its assertion names a **product promise**, not a construction detail — litmus: would it survive a total visual redesign that changes no behavior? `not.toHaveProperty("taskMode")` survives; `classList.contains("w-24")` does not. *(b)* The defect it catches can **originate below the layout engine** — a wrong wire path, a dropped field, a state-machine bug, a wrong string. Fail either and the honest home is 7b or 8.
101
+ - **Three conditions put a UI test below a real browser; all must hold.** *(a)* Its assertion names a **product promise**, not a construction detail — litmus: would it survive a total visual redesign that changes no behavior? `not.toHaveProperty("taskMode")` survives; `classList.contains("w-24")` does not. *(b)* The defect it catches can **originate below the layout engine** — a wrong wire path, a dropped field, a state-machine bug, a wrong string. *(c)* **The surface's journey cannot reach it** — the state needs a fixture the seeded world lacks, an external system, or an orchestration detail no user step exposes. Fail (a) or (b) and the honest home is 7b; fail (c) and the home is the journey, as one more assertion.
102
102
  - **Geometry may never be asserted in a runner without a layout engine.** There is no valid proxy: a class token is not a width, and a test that pretends otherwise is simultaneously brittle and blind. When the only available runner simulates the DOM, the behavior is driven and pinned at 7b — never approximated.
103
103
  - **An assertion provable at two layers is misplaced — it belongs at the lower one.** The duplication check is mechanical, not judgment.
104
- - **Behavior variants sink to the cheapest layer that owns them** — almost always 2 or 6. A variant at layer 3 pays wire tax on a domain test; a variant at layer 8 is a placement failure below, by definition.
105
- - **One representative path upward.** Layer 3 keeps one happy path per route plus exhaustive coverage of *contract edges only*; layer 8 keeps 3–5 journeys total.
104
+ - **Behavior variants sink to the cheapest layer that owns them** — a derivation's variants to 6, a rule's to 2. A variant a user can *see* (a second sort order, a filtered roster, a refused form) belongs in the journey that shows it, as one more assertion, not as a component case. A variant at layer 3 pays wire tax on a domain test.
105
+ - **One representative path upward for the server; the whole surface for the browser.** Layer 3 keeps one happy path per route plus exhaustive coverage of *contract edges only*. Layer 8 keeps one spec per surface covering everything a user can reach in the seeded world — the count is set by the surface, never by a cap. A cap on journeys is what pushes UI promises into a simulated DOM, and a simulated DOM talks to a hand-written fake of the wire, so a route rename or an envelope change passes it green.
106
106
  - **7b stays small by construction.** It holds geometry regressions that actually shipped and the handful of composition assertions a component library's contract cannot make alone (the theme cascade, the accessible name through its primitive). It is not a second component suite — a behavior that a simulated DOM can prove has no business paying a browser's price.
107
107
  - **External systems enter as modeled fakes, not spies.** A fake implements the port's semantics so tests assert resulting state; a spy only proves methods were called, which passes just as happily with the bytes swapped.
108
108
  - **Failure localization is the payoff to protect.** One defect turns exactly one layer red, at its owner. When a single change reddens several layers, at most one of those tests was correctly placed — treat the rest as findings.
109
109
 
110
- Layering makes cost **additive** — a new API behavior costs one layer-2 test and maybe a layer-3 edge, zero frontend tests; a new UI state costs one layer-7 test, zero backend tests. A flat suite pays N×M, because every top-layer path implicitly re-verifies every behavior beneath it.
110
+ Layering makes cost **additive** — a new API behavior costs one layer-2 test and maybe a layer-3 edge, zero frontend tests; a new UI state costs one more assertion in its surface's journey, zero backend tests. A flat suite pays N×M, because every top-layer path implicitly re-verifies every behavior beneath it.
111
111
 
112
112
  ### The tree
113
113
 
@@ -119,10 +119,10 @@ tests/
119
119
  contract/ L4 — no runner; typed fixtures + interception handlers
120
120
  client/ L5 — plain unit runner
121
121
  view/ L6 — plain unit runner
122
- component/ L7 — unit runner + DOM environment (per-file opt-in)
122
+ component/ L7 — unit runner + DOM environment (per-file opt-in); residual where e2e/ exists
123
123
  rendered/ L7b — the same runner in browser mode; a real engine, kept small
124
124
  helpers/ shared seeds, fakes, and builders — owned by no stratum
125
- e2e/ L8 — its own config, never mixed into the unit runner's glob
125
+ e2e/ L8 — one spec per surface over a seeded world; its own config, never mixed into the unit runner's glob
126
126
  ```
127
127
 
128
128
  - **A stratum the app lacks gets no directory** — an empty `component/` in a backend service is a lie about the app's shape.
@@ -147,7 +147,7 @@ When writing or discharging a ledger row, name the stratum, not just the altitud
147
147
 
148
148
  1. **Derive the stack** from the application's real boundaries (storage? published wire? separate client? DOM?).
149
149
  2. **Place each behavior** by the litmus; name stratum, oracle, technology class; take the concrete framework and file placement from `testing.md`.
150
- 3. **Flag violations, don't just place** — assertions provable lower, variants above 2/6, hand-written wire fixtures above layer 4, spies where fakes should model state, layer-8 variants.
150
+ 3. **Flag violations, don't just place** — assertions provable lower, derivation variants above 6, hand-written wire fixtures above layer 4, spies where fakes should model state, and UI promises pinned at 7 that the surface's journey can reach.
151
151
  4. **Report as a stratum map** — one line per stratum: exists/absent (why), what lives there, what is misplaced and where it moves. For a plan, feed the strata into the ledger's rows.
152
152
 
153
153
  ---
@@ -168,7 +168,7 @@ Runs as the tail of every build pass — the session opens it the moment the uni
168
168
  Cheap oracles have not stopped mattering — they stopped being a gate you pass before looking. Suites, the CLI binary, and HTTP against the running worker run in the same pass and are what the drive's findings get **pinned into** (rung 3). On a surface with no rendering — a CLI, a service, a library — this rung is its own real binary or real wire, and the visual clause simply does not apply.
169
169
 
170
170
  **A failing checklist assertion is never softened to make a run green** — record the defect, send it to the backlog.
171
- 3. **Pin the durable behaviors — once, against the driven code** — as suite tests per `testing.md`, naming each test's stratum (§ Bridge). Pins are written after the drive and the fixes, never beside the first attempt: a pin written earlier is rewritten on every iteration and proves only that its author and the code agree. **Pinning is written inline, by the agent that drove** — it alone knows what the drive found and what each pin must therefore assert; a sub-agent briefed from the ledger row reproduces the row, not the finding. **A pinned test must pass from a cold shell** — `pnpm test` with no dev server hand-started, no seeded state assumed beyond what the suite builds for itself. A test whose oracle is "fetch the server I happen to have running" is a driven check wearing a test's name: it goes red the moment the session ends (on Workers, `@cloudflare/vitest-pool-workers` is the self-contained harness; an Express app is driven via `supertest`, never a port). Before discharging, run the suite once from a clean shell — a suite that needs the session's environment discharges nothing. A test is a permanent maintenance cost, bought deliberately:
171
+ 3. **Pin the durable behaviors — once, against the driven code** — as suite tests per `testing.md`, naming each test's stratum (§ Bridge). **On a rendered surface the pin is the surface's journey first**: the drive just performed *is* the journey, so write it down as the test against the seeded world, and reach for a component test only where § Placement discipline's third condition holds. Pins are written after the drive and the fixes, never beside the first attempt: a pin written earlier is rewritten on every iteration and proves only that its author and the code agree. **Pinning is written inline, by the agent that drove** — it alone knows what the drive found and what each pin must therefore assert; a sub-agent briefed from the ledger row reproduces the row, not the finding. **A pinned test must pass from a cold shell** — `pnpm test` with no dev server hand-started, no seeded state assumed beyond what the suite builds for itself. A test whose oracle is "fetch the server I happen to have running" is a driven check wearing a test's name: it goes red the moment the session ends (on Workers, `@cloudflare/vitest-pool-workers` is the self-contained harness; an Express app is driven via `supertest`, never a port). Before discharging, run the suite once from a clean shell — a suite that needs the session's environment discharges nothing. A test is a permanent maintenance cost, bought deliberately:
172
172
 
173
173
  | Earns a suite test | Verified by driving alone |
174
174
  | --- | --- |
@@ -176,9 +176,10 @@ Runs as the tail of every build pass — the session opens it the moment the uni
176
176
  | A bug that occurred once — its regression test | A one-off migration or backfill's output |
177
177
  | **A layout defect that shipped** — its geometry pin, at 7b, measured in a real engine | Layout a judged drive has looked at and found correct — looking is the proof, and a pin buys nothing until something breaks |
178
178
  | An error path, race, or idempotency guarantee | Behavior a type or the compiler already makes unshippable |
179
+ | A user-observable step of a surface — as an assertion in its journey | A same-branch table row: an `it.each` earns one row per branch or boundary, never one per literal |
179
180
 
180
- A driven check is evidence, not an artifact — it proves the change works today and leaves nothing behind; that is why this rung exists.
181
- 4. **Run the affected suite**, plus the project's e2e smoke gate where one is wired.
181
+ A driven check is evidence, not an artifact — it proves the change works today and leaves nothing behind; that is why this rung exists. Two hygiene rules hold at every pin: **a suite whose subject is no longer reachable from the product's entry points is deleted with the code path**, never repointed to keep it green; and **a case that asserts nothing beyond "it did not throw" is not a pin** — give it an assertion on resulting state or delete it.
182
+ 4. **Run the affected suite and the affected surface's journeys from a cold shell** — the e2e config builds, seeds and boots its own world, so "needs the world running" is never a reason a journey did not run.
182
183
  5. **Discharge every ledger row into exactly one outcome**: **satisfied** (test green, or driven check passed), **owes nothing** (with the reason — compiler-guarded, covered elsewhere), or **blocked** (missing infrastructure named, row converted to a backlog item). **Open by re-reading the ledger from the plan file on disk, never from conversation memory** — a context-held copy is how rows get missed. **Write each row's outcome into its `Outcome` cell in the plan file** — exactly `satisfied`, `owes nothing — {reason}`, or `blocked → backlog` — because the Stop wall and CI read that cell, and a row left unexamined blocks completion.
183
184
  6. **Report results honestly, including failures**; record which test now guards each checklist section (`Pinned:` traces) and each section's `AC:` line where the repo carries a keyed acceptance-criteria source, closing with the coverage count — `AC coverage: {n}/{m}; uncovered: {keys} → backlog`.
184
185
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "delivery",
3
3
  "name": "delivery",
4
- "version": "0.2.29",
4
+ "version": "0.2.30",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -10,5 +10,5 @@
10
10
  "seeds": [],
11
11
  "tools": [],
12
12
  "docsRoot": "docs",
13
- "digest": "e7b3bec5e759"
13
+ "digest": "688776066c84"
14
14
  }
@@ -4,7 +4,7 @@ The shared style for every file an agent obeys — a `CLAUDE.md`, a rule, an age
4
4
 
5
5
  ## The test for every line
6
6
 
7
- An instruction file is paid for whenever it loads, scaled by its load class: `CLAUDE.md` and rules load on **every turn of every session** — the most expensive prose in a repo per word; an agent definition loads per dispatch, a skill per invocation. The bar is not "is this true?" but **"must this be in front of the agent whenever this file loads?"** Measure weight in tokens, not lines; trim biggest-first on every edit.
7
+ An instruction file is paid for whenever it loads, scaled by its load class: `CLAUDE.md` and unscoped rules load on **every turn of every session** — the most expensive prose in a repo per word; a `paths:`-scoped rule loads on those same terms from its first matching file read on; an agent definition loads per dispatch, a skill per invocation. The bar is not "is this true?" but **"must this be in front of the agent whenever this file loads?"** Measure weight in tokens, not lines; trim biggest-first on every edit.
8
8
 
9
9
  **Rewriting, the test is armor vs. narration.** A reason clause either blocks a move an agent would otherwise make — the failure is counter-intuitive, the wrong fix looks correct, or the rule reads as arbitrary tidiness and gets "improved away" — or it explains the self-evident. Ask of each: *does the forbidden move look correct to an agent mid-task?* Keep the first kind, cut the second. **A constraint may lose its explanation; it may never lose its existence** — compression is fewer words for the same constraints, so a passage that cannot shrink without dropping one stays long.
10
10
 
@@ -8,13 +8,13 @@ The `boundary` skill parses the **Proposed Changes** sections to check boundary
8
8
 
9
9
  **Doc Obligations is the one ledger the plan's author writes.** Deriving tests from behavior is judgment, which is why `testing` owns that section and it is marked never-hand-author; resolving Proposed Changes' file paths to the feature docs that own them is a **lookup** against the ownership index already injected at session start. Different work, different rule — fill it yourself, while the touch list is fresh.
10
10
 
11
- The **`Gates:`** line carries one machine-read segment. The compliance hook greps **`Boundary:`** — implementation edits inside the plan's scope are blocked while it reads `Boundary: pending`; the `boundary` skill rewrites it to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`. The other two gates are enforced by **section presence**: a plan with no `## Test Obligations` or no `## Doc Obligations` section blocks the same edits — the sections are the artifacts, and the gate derives from them, not from a claim about them. Never hand-edit the line to skip a gate.
11
+ The **`Gates:`** line carries two machine-read segments. The compliance hook greps **`Boundary:`** — implementation edits inside the plan's scope are blocked while it reads `Boundary: pending`; the `boundary` skill rewrites it to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`. The other two plan gates are enforced by **section presence**: a plan with no `## Test Obligations` or no `## Doc Obligations` section blocks the same edits — the sections are the artifacts, and the gate derives from them, not from a claim about them. **`Polygraph:`** is the completion gate's trace: it reads `Polygraph: pending` from the first draft until the `polygraph` skill (gate mode) certifies the plan's documents — every Doc Obligations row discharged **updated** plus every `Spec:` doc — and rewrites it to `Polygraph: passed YYYY-MM-DD (N docs · N claims · N corrected · N registered)`. A plan that touched no document writes `Polygraph: owes nothing — {reason}` by hand; nothing else on this segment is ever hand-written. A Complete plan still reading `pending` raises a `needs-session:` queue row and fails CI. Never hand-edit the line to skip a gate.
12
12
 
13
13
  ```
14
14
  # {Feature Name}
15
15
 
16
16
  **Status:** Draft | In Progress | Complete | Abandoned
17
- **Gates:** Boundary: pending
17
+ **Gates:** Boundary: pending · Polygraph: pending
18
18
  **Spec:** [docs/features/{feature}.md](../../features/{feature}.md) <!-- one line per feature doc this plan implements against; a plan spanning three features names three -->
19
19
  **Started:** YYYY-MM-DD
20
20
  **Completed:** YYYY-MM-DD <!-- fill in the SAME edit that flips Status to Complete — along with every ledger Outcome cell: the flip freezes the file instantly, so anything left for a second edit is unreachable (the frozen-record gate rightly blocks it) -->
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.5.18",
4
+ "version": "1.5.20",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -10,5 +10,5 @@
10
10
  "seeds": [],
11
11
  "tools": [],
12
12
  "docsRoot": "docs",
13
- "digest": "8627c15bbc62"
13
+ "digest": "4c5fe7575b80"
14
14
  }
@@ -60,7 +60,7 @@ Test packages install at `latest` — outside the approved-packages pin set. Fak
60
60
 
61
61
  - **Suite, not scripts.** Tests live in the canonical test tree — `tests/` mirroring `src/`, or the convention the repo already uses — named `<subject>.test.ts`; shared fixtures, factories, and helpers in `tests/helpers/` (or the suite's existing shared home).
62
62
  - **Wire the canonical command.** `"test"` in `package.json` runs the whole unit suite (`vitest run`); e2e under `"test:e2e"`. A repo where `pnpm test` / `npm test` doesn't run the suite is misconfigured — fix the wiring, **never** route around it.
63
- - **Unit and E2E trees stay separate.** Vitest/Jest suites in `tests/`; Playwright/Maestro suites in `e2e/` with their own config — **never** mixed into the unit runner's glob.
63
+ - **Unit and E2E trees stay separate.** Vitest/Jest suites in `tests/`; Playwright/Maestro suites in `e2e/` with their own config — **never** mixed into the unit runner's glob. The e2e config **owns its world** — it builds, seeds, and boots the app itself (a `webServer` block or equivalent), so a journey passes from a cold shell like any other test and "the world was not running" is never why one did not run.
64
64
  - **The real-renderer stratum is a third tree** — `tests/rendered/` under the unit runner's *browser mode*, its own config, kept to geometry regressions that shipped and the composition assertions a component library cannot make alone. It is not a second component suite; anything a simulated DOM can prove stays in `tests/component/`.
65
65
  - **No test doubles for what you own.** Prefer real instances — in-memory SQLite for drizzle, the actual Express app via `supertest` — over mocks; mock **only** true externals (network via `msw`, time via the runner's fake timers).
66
66
  - **Determinism.** No real network, no real time, no shared mutable state between tests; every test **must** pass alone, in any order, **and from a cold shell** — never against a hand-started dev server (Workers: `@cloudflare/vitest-pool-workers`; Express: `supertest` against the app instance).
@@ -4,7 +4,7 @@ The shared style for every file an agent obeys — a `CLAUDE.md`, a rule, an age
4
4
 
5
5
  ## The test for every line
6
6
 
7
- An instruction file is paid for whenever it loads, scaled by its load class: `CLAUDE.md` and rules load on **every turn of every session** — the most expensive prose in a repo per word; an agent definition loads per dispatch, a skill per invocation. The bar is not "is this true?" but **"must this be in front of the agent whenever this file loads?"** Measure weight in tokens, not lines; trim biggest-first on every edit.
7
+ An instruction file is paid for whenever it loads, scaled by its load class: `CLAUDE.md` and unscoped rules load on **every turn of every session** — the most expensive prose in a repo per word; a `paths:`-scoped rule loads on those same terms from its first matching file read on; an agent definition loads per dispatch, a skill per invocation. The bar is not "is this true?" but **"must this be in front of the agent whenever this file loads?"** Measure weight in tokens, not lines; trim biggest-first on every edit.
8
8
 
9
9
  **Rewriting, the test is armor vs. narration.** A reason clause either blocks a move an agent would otherwise make — the failure is counter-intuitive, the wrong fix looks correct, or the rule reads as arbitrary tidiness and gets "improved away" — or it explains the self-evident. Ask of each: *does the forbidden move look correct to an agent mid-task?* Keep the first kind, cut the second. **A constraint may lose its explanation; it may never lose its existence** — compression is fewer words for the same constraints, so a passage that cannot shrink without dropping one stays long.
10
10
 
@@ -8,13 +8,13 @@ The `boundary` skill parses the **Proposed Changes** sections to check boundary
8
8
 
9
9
  **Doc Obligations is the one ledger the plan's author writes.** Deriving tests from behavior is judgment, which is why `testing` owns that section and it is marked never-hand-author; resolving Proposed Changes' file paths to the feature docs that own them is a **lookup** against the ownership index already injected at session start. Different work, different rule — fill it yourself, while the touch list is fresh.
10
10
 
11
- The **`Gates:`** line carries one machine-read segment. The compliance hook greps **`Boundary:`** — implementation edits inside the plan's scope are blocked while it reads `Boundary: pending`; the `boundary` skill rewrites it to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`. The other two gates are enforced by **section presence**: a plan with no `## Test Obligations` or no `## Doc Obligations` section blocks the same edits — the sections are the artifacts, and the gate derives from them, not from a claim about them. Never hand-edit the line to skip a gate.
11
+ The **`Gates:`** line carries two machine-read segments. The compliance hook greps **`Boundary:`** — implementation edits inside the plan's scope are blocked while it reads `Boundary: pending`; the `boundary` skill rewrites it to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`. The other two plan gates are enforced by **section presence**: a plan with no `## Test Obligations` or no `## Doc Obligations` section blocks the same edits — the sections are the artifacts, and the gate derives from them, not from a claim about them. **`Polygraph:`** is the completion gate's trace: it reads `Polygraph: pending` from the first draft until the `polygraph` skill (gate mode) certifies the plan's documents — every Doc Obligations row discharged **updated** plus every `Spec:` doc — and rewrites it to `Polygraph: passed YYYY-MM-DD (N docs · N claims · N corrected · N registered)`. A plan that touched no document writes `Polygraph: owes nothing — {reason}` by hand; nothing else on this segment is ever hand-written. A Complete plan still reading `pending` raises a `needs-session:` queue row and fails CI. Never hand-edit the line to skip a gate.
12
12
 
13
13
  ```
14
14
  # {Feature Name}
15
15
 
16
16
  **Status:** Draft | In Progress | Complete | Abandoned
17
- **Gates:** Boundary: pending
17
+ **Gates:** Boundary: pending · Polygraph: pending
18
18
  **Spec:** [docs/features/{feature}.md](../../features/{feature}.md) <!-- one line per feature doc this plan implements against; a plan spanning three features names three -->
19
19
  **Started:** YYYY-MM-DD
20
20
  **Completed:** YYYY-MM-DD <!-- fill in the SAME edit that flips Status to Complete — along with every ledger Outcome cell: the flip freezes the file instantly, so anything left for a second edit is unreachable (the frozen-record gate rightly blocks it) -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.20.10",
3
+ "version": "0.20.12",
4
4
  "description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,5 +42,5 @@
42
42
  "agent",
43
43
  "plugin"
44
44
  ],
45
- "etnaFingerprint": "0fa183cce9ee"
45
+ "etnaFingerprint": "d525a2adfbe5"
46
46
  }