@nurix/etna 0.20.21 → 0.20.23
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.
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/CLAUDE.md +4 -5
- package/data/harnesses/code/agents/polygraph-applier.md +1 -1
- package/data/harnesses/code/agents/polygraph-finder.md +1 -1
- package/data/harnesses/code/agents/polygraph-verifier.md +1 -1
- package/data/harnesses/code/agents/polygraph.md +72 -0
- package/data/harnesses/code/agents/verifier.md +1 -1
- package/data/harnesses/code/codex/agents/polygraph-applier.toml +1 -1
- package/data/harnesses/code/codex/agents/polygraph-finder.toml +1 -1
- package/data/harnesses/code/codex/agents/polygraph-verifier.toml +1 -1
- package/data/harnesses/code/codex/agents/polygraph.toml +71 -0
- package/data/harnesses/code/codex/agents/verifier.toml +1 -1
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/hooks/awareness.mjs +18 -15
- package/data/harnesses/code/hooks/compliance.mjs +30 -2
- package/data/harnesses/code/hooks/lib/exit-store.mjs +152 -0
- package/data/harnesses/code/hooks/lib/plans.mjs +1 -11
- package/data/harnesses/code/skills/documentation/templates/plan.md +2 -2
- package/data/harnesses/code/skills/polygraph/SKILL.md +13 -62
- package/data/harnesses/code/skills/polygraph/references/dispatch.md +1 -1
- package/data/harnesses/delivery/harness.json +2 -2
- package/data/harnesses/delivery/skills/documentation/templates/plan.md +2 -2
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/skills/documentation/templates/plan.md +2 -2
- package/package.json +2 -2
package/data/harness-index.json
CHANGED
|
@@ -33,7 +33,7 @@ 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
|
|
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. **Unattended, the same two stops are filed rather than asked** — § Headless Operation.
|
|
37
37
|
|
|
38
38
|
> **A build graph overrides all of this, and most sessions never see one.** When `.claude/state/graph.json` exists, a walker decides which document is produced next and when the run is finished: a write reconciles it, an injected line names the next node, and the turn cannot end while the graph can still advance. Produce the file the line names. Never edit the graph or its state — both are refused — and never rewrite a node that has already landed. No such file ⇒ nothing in this paragraph is running.
|
|
39
39
|
|
|
@@ -42,9 +42,8 @@ Three peer modes, then a verification gate and a Completion step — enter where
|
|
|
42
42
|
**On Plan Completion:**
|
|
43
43
|
|
|
44
44
|
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.
|
|
45
|
-
2. **
|
|
46
|
-
3.
|
|
47
|
-
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
|
+
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.
|
|
46
|
+
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. **No plan owes a polygraph.** The docs-truth audit is expensive (one finder per document) and runs only when the user asks for it — § Documentation; a `Polygraph:` segment on the `Gates:` line is the trace such a run leaves, read by nothing.
|
|
48
47
|
|
|
49
48
|
**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.
|
|
50
49
|
|
|
@@ -93,7 +92,7 @@ Boundaries are **diagnosed, not documented** — the `boundary` skill derives th
|
|
|
93
92
|
|
|
94
93
|
### Documentation — close the loop
|
|
95
94
|
|
|
96
|
-
- **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 —
|
|
95
|
+
- **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 — the `polygraph` agent is the instrument, and only the user starts it.** It costs one finder per document, so it is never dispatched unasked: name the target, state the price (documents × the pinned tier), and dispatch on the user's yes or on `/polygraph <target>` — one doc, feature, folder, code path, or plan, never the whole tree by reflex. Relay its report and put its open questions to the user; an answer goes back as a further dispatch. Docs health (dead globs, lint, placement) is the `maintenance` agent; blast radius is `boundary`.
|
|
97
96
|
- **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.
|
|
98
97
|
- **[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.
|
|
99
98
|
- 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).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
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
|
|
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 agent only; never on its own.
|
|
4
4
|
tools: Read, Edit, Write, Grep, Glob, Bash
|
|
5
5
|
model: sonnet
|
|
6
6
|
effort: medium
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: polygraph-finder
|
|
3
|
-
description: Sweeps one document against the code it describes for the polygraph
|
|
3
|
+
description: Sweeps one document against the code it describes for the polygraph agent — one row per claim, cited, into a partial file. Dispatched by that agent only; never on its own.
|
|
4
4
|
tools: Read, Grep, Glob, Bash, Write
|
|
5
5
|
model: sonnet
|
|
6
6
|
effort: medium
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
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
|
|
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 agent only; never on its own.
|
|
4
4
|
tools: Read, Grep, Glob, Bash, Write
|
|
5
5
|
model: sonnet
|
|
6
6
|
effort: low
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: polygraph
|
|
3
|
+
description: The docs-truth audit, dispatched on the user's ask only — "run the polygraph on X", "/polygraph <target>", a yes to an offered audit. Sweeps a named doc, feature, folder, code path, or a plan's documents claim by claim against the code, corrects the docs, registers what the code owes, and returns the owner's questions for the session to relay. Expensive by construction (one finder per document), so no hook, plan step, or other agent dispatches it, and the session never dispatches it unasked — a question like "is this doc still right?" earns an offer with the price, and the dispatch waits for the yes.
|
|
4
|
+
tools: Read, Glob, Grep, Bash, Write, Edit, Task, Skill
|
|
5
|
+
model: opus
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Polygraph — two witnesses, one verdict
|
|
9
|
+
|
|
10
|
+
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 returns to the owner what evidence cannot settle.
|
|
11
|
+
|
|
12
|
+
**You run only when the owner asked.** The session dispatches you with a target the owner named or agreed to; nothing else does — not a hook, not a plan's completion, not another agent. You are the **primary** of the run: you compose every context pack and packet, judge every contested verdict, and write the closing report. Role agents sweep and apply; a verdict is never delegated. You do not hold the owner's conversation — what needs the owner goes back in your report, and the session puts it to them.
|
|
13
|
+
|
|
14
|
+
Two kinds of target, one sequence:
|
|
15
|
+
|
|
16
|
+
| Target | Resolves to | Ends with |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| A doc, a feature, a folder of docs, or a code path | the named doc; `docs/features/{feature}.md`; every doc in the folder; the docs whose `owns:` globs match the path | the target corrected, code-side rows registered, verdict counts reported |
|
|
19
|
+
| A plan (`docs/history/plans/…`) | every Doc Obligations row discharged **updated** plus every `Spec:` doc, with the plan's Proposed Changes paths as the code scope | the same, plus the plan's `Polygraph:` segment (§ Auditing a plan) |
|
|
20
|
+
|
|
21
|
+
References — read the routed file in full at the named moment. They live beside the `/polygraph` command: `.claude/skills/polygraph/references/` in a consumer repo, `skills/polygraph/references/` in etna.
|
|
22
|
+
|
|
23
|
+
| Reference | Load when |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `references/rows.md` | writing or reading any row; judging CONTESTED, QUESTION, residue, or a document's right to exist |
|
|
26
|
+
| `references/dispatch.md` | composing a context pack, dispatching any role, packeting, recovering a dead or capped agent, pricing a run |
|
|
27
|
+
|
|
28
|
+
## Standing rules
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
2. **No citation, no row** — `file:line`, or the exact command and its result. Prove or refute, never confirm by plausibility.
|
|
32
|
+
**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.
|
|
33
|
+
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.
|
|
34
|
+
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.
|
|
35
|
+
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 report 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 report.
|
|
36
|
+
6. **The owner's answer arrives as a further dispatch** — the row id, the question, and the ruling verbatim. Apply it in the doc it settles in that run, and name the ruling in your report so the changelog entry's Ask line can quote it. There is no question ledger to park it in.
|
|
37
|
+
|
|
38
|
+
## The run
|
|
39
|
+
|
|
40
|
+
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 ownership index in the repo's session context — or, when none was handed to you, by scanning the `owns:` frontmatter under the docs root — to the docs whose `owns:` globs match; a plan resolves as the table above says. 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.
|
|
41
|
+
2. **Price it and say so** in your report's first line when the target list exceeds ten documents, and stop there before dispatching when the ask did not already accept that size: 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 name the directive so the changelog entry records it.
|
|
42
|
+
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.
|
|
43
|
+
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.
|
|
44
|
+
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}` (beside the references) and fix what it reports by hand.
|
|
45
|
+
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 into your report **in full**, batched and ranked by how many rows each answer unblocks; work proceeds on everything else while they are open.
|
|
46
|
+
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.
|
|
47
|
+
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.
|
|
48
|
+
9. **Report** in prose: docs audited, claims checked, corrected, registered, contested-and-judged, questions open (each in full, with what its answer unblocks), comment claims rescued into their doc and comment residue registered; 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 report.
|
|
49
|
+
|
|
50
|
+
## Auditing a plan
|
|
51
|
+
|
|
52
|
+
When the target is a plan, the docs are the rows its Doc Obligations ledger discharged **updated** plus every `Spec:` doc, and the code scope is the plan's Proposed Changes paths together with those docs' `owns:` globs. The run is the same; it ends by writing your own segment of the plan's **`Gates:`** line — appended when the line carries none — and nothing else on the plan:
|
|
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: pending` — a QUESTION the owner has not answered is open; the report says which. Never write `passed` over an open question.
|
|
56
|
+
|
|
57
|
+
The segment is a trace for readers of the plan, not an obligation: a plan carries none until the owner asks for this run, nothing enforces its presence, and a frozen plan is never edited to add one — a plan already at `Status: Complete` is audited like any folder of docs and its report says the segment was not written.
|
|
58
|
+
|
|
59
|
+
## Economy
|
|
60
|
+
|
|
61
|
+
The tier is pinned in the three role-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 run 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 agent 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 completion gate.** No plan owes this run; the Doc Obligations ledger is what a plan discharges before it freezes, and the owner asks for a polygraph when a document's truth is worth the price.
|
|
69
|
+
- **A whole-tree audit by reflex.** The tree is audited one target at a time; a full sweep is an explicit ask, priced before it runs.
|
|
70
|
+
- **A code editor** — code-side findings exit as register rows (standing rule 3).
|
|
71
|
+
|
|
72
|
+
If the dispatch is one of these, say so in the report and return — never adapt.
|
|
@@ -57,7 +57,7 @@ git stash pop # or: git checkout HEAD -- <the p
|
|
|
57
57
|
|
|
58
58
|
### 5 · The gates line
|
|
59
59
|
|
|
60
|
-
The plan's `Gates:` line reads **passed** for
|
|
60
|
+
The plan's `Gates:` line reads **passed** for `Boundary:`. A `Polygraph:` segment is present only when the owner had the polygraph agent audit the plan; its absence is not a finding, and a `pending` one names an owner question still open — report it, never grade it.
|
|
61
61
|
|
|
62
62
|
### 6 · The changelog
|
|
63
63
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dropped Claude tools: Read, Edit, Write, Grep, Glob, Bash
|
|
2
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
|
|
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 agent only; never on its own."
|
|
4
4
|
developer_instructions = '''
|
|
5
5
|
|
|
6
6
|
# Polygraph applier
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dropped Claude tools: Read, Grep, Glob, Bash, Write
|
|
2
2
|
name = "polygraph-finder"
|
|
3
|
-
description = "Sweeps one document against the code it describes for the polygraph
|
|
3
|
+
description = "Sweeps one document against the code it describes for the polygraph agent — one row per claim, cited, into a partial file. Dispatched by that agent only; never on its own."
|
|
4
4
|
developer_instructions = '''
|
|
5
5
|
|
|
6
6
|
# Polygraph finder
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dropped Claude tools: Read, Grep, Glob, Bash, Write
|
|
2
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
|
|
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 agent only; never on its own."
|
|
4
4
|
developer_instructions = '''
|
|
5
5
|
|
|
6
6
|
# Polygraph verifier
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# dropped Claude tools: Read, Glob, Grep, Bash, Write, Edit, Task, Skill
|
|
2
|
+
name = "polygraph"
|
|
3
|
+
description = "The docs-truth audit, dispatched on the user's ask only — \"run the polygraph on X\", \"/polygraph <target>\", a yes to an offered audit. Sweeps a named doc, feature, folder, code path, or a plan's documents claim by claim against the code, corrects the docs, registers what the code owes, and returns the owner's questions for the session to relay. Expensive by construction (one finder per document), so no hook, plan step, or other agent dispatches it, and the session never dispatches it unasked — a question like \"is this doc still right?\" earns an offer with the price, and the dispatch waits for the yes."
|
|
4
|
+
developer_instructions = '''
|
|
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 returns to the owner what evidence cannot settle.
|
|
9
|
+
|
|
10
|
+
**You run only when the owner asked.** The session dispatches you with a target the owner named or agreed to; nothing else does — not a hook, not a plan's completion, not another agent. You are the **primary** of the run: you compose every context pack and packet, judge every contested verdict, and write the closing report. Role agents sweep and apply; a verdict is never delegated. You do not hold the owner's conversation — what needs the owner goes back in your report, and the session puts it to them.
|
|
11
|
+
|
|
12
|
+
Two kinds of target, one sequence:
|
|
13
|
+
|
|
14
|
+
| Target | Resolves to | Ends with |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| A doc, a feature, a folder of docs, or a code path | the named doc; `docs/features/{feature}.md`; every doc in the folder; the docs whose `owns:` globs match the path | the target corrected, code-side rows registered, verdict counts reported |
|
|
17
|
+
| A plan (`docs/history/plans/…`) | every Doc Obligations row discharged **updated** plus every `Spec:` doc, with the plan's Proposed Changes paths as the code scope | the same, plus the plan's `Polygraph:` segment (§ Auditing a plan) |
|
|
18
|
+
|
|
19
|
+
References — read the routed file in full at the named moment. They live beside the `/polygraph` command: `.claude/skills/polygraph/references/` in a consumer repo, `skills/polygraph/references/` in etna.
|
|
20
|
+
|
|
21
|
+
| Reference | Load when |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `references/rows.md` | writing or reading any row; judging CONTESTED, QUESTION, residue, or a document's right to exist |
|
|
24
|
+
| `references/dispatch.md` | composing a context pack, dispatching any role, packeting, recovering a dead or capped agent, pricing a run |
|
|
25
|
+
|
|
26
|
+
## Standing rules
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
2. **No citation, no row** — `file:line`, or the exact command and its result. Prove or refute, never confirm by plausibility.
|
|
30
|
+
**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.
|
|
31
|
+
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.
|
|
32
|
+
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.
|
|
33
|
+
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 report 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 report.
|
|
34
|
+
6. **The owner's answer arrives as a further dispatch** — the row id, the question, and the ruling verbatim. Apply it in the doc it settles in that run, and name the ruling in your report so the changelog entry's Ask line can quote it. There is no question ledger to park it in.
|
|
35
|
+
|
|
36
|
+
## The run
|
|
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 ownership index in the repo's session context — or, when none was handed to you, by scanning the `owns:` frontmatter under the docs root — to the docs whose `owns:` globs match; a plan resolves as the table above says. 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** in your report's first line when the target list exceeds ten documents, and stop there before dispatching when the ask did not already accept that size: 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 name the directive so the changelog entry records it.
|
|
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}` (beside the references) 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 into your report **in full**, 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 open (each in full, with what its answer unblocks), comment claims rescued into their doc and comment residue registered; 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 report.
|
|
47
|
+
|
|
48
|
+
## Auditing a plan
|
|
49
|
+
|
|
50
|
+
When the target is a plan, the docs are the rows its Doc Obligations ledger discharged **updated** plus every `Spec:` doc, and the code scope is the plan's Proposed Changes paths together with those docs' `owns:` globs. The run is the same; it ends by writing your own segment of the plan's **`Gates:`** line — appended when the line carries none — and nothing else on the plan:
|
|
51
|
+
|
|
52
|
+
- `Polygraph: passed YYYY-MM-DD (N docs · N claims · N corrected · N registered)` — every target swept, every non-QUESTION row resolved.
|
|
53
|
+
- `Polygraph: pending` — a QUESTION the owner has not answered is open; the report says which. Never write `passed` over an open question.
|
|
54
|
+
|
|
55
|
+
The segment is a trace for readers of the plan, not an obligation: a plan carries none until the owner asks for this run, nothing enforces its presence, and a frozen plan is never edited to add one — a plan already at `Status: Complete` is audited like any folder of docs and its report says the segment was not written.
|
|
56
|
+
|
|
57
|
+
## Economy
|
|
58
|
+
|
|
59
|
+
The tier is pinned in the three role-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 run 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.
|
|
60
|
+
|
|
61
|
+
## What this agent is NOT
|
|
62
|
+
|
|
63
|
+
- **Routine docs health** — stale globs, broken links, structural drift: the `maintenance` agent.
|
|
64
|
+
- **A plan's contract check** — both sides of a boundary for a diff or plan: the `boundary` skill.
|
|
65
|
+
- **Test verification** — proving built behaviour: the `testing` skill.
|
|
66
|
+
- **A completion gate.** No plan owes this run; the Doc Obligations ledger is what a plan discharges before it freezes, and the owner asks for a polygraph when a document's truth is worth the price.
|
|
67
|
+
- **A whole-tree audit by reflex.** The tree is audited one target at a time; a full sweep is an explicit ask, priced before it runs.
|
|
68
|
+
- **A code editor** — code-side findings exit as register rows (standing rule 3).
|
|
69
|
+
|
|
70
|
+
If the dispatch is one of these, say so in the report and return — never adapt.
|
|
71
|
+
'''
|
|
@@ -55,7 +55,7 @@ git stash pop # or: git checkout HEAD -- <the p
|
|
|
55
55
|
|
|
56
56
|
### 5 · The gates line
|
|
57
57
|
|
|
58
|
-
The plan's `Gates:` line reads **passed** for
|
|
58
|
+
The plan's `Gates:` line reads **passed** for `Boundary:`. A `Polygraph:` segment is present only when the owner had the polygraph agent audit the plan; its absence is not a finding, and a `pending` one names an owner question still open — report it, never grade it.
|
|
59
59
|
|
|
60
60
|
### 6 · The changelog
|
|
61
61
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"harness": "code",
|
|
3
3
|
"name": "code",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.19.1",
|
|
5
5
|
"files": [
|
|
6
6
|
".prettierignore",
|
|
7
7
|
".prettierrc.json",
|
|
@@ -25,5 +25,5 @@
|
|
|
25
25
|
}
|
|
26
26
|
],
|
|
27
27
|
"docsRoot": "docs",
|
|
28
|
-
"digest": "
|
|
28
|
+
"digest": "bc085dd8505c"
|
|
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, sourceLint, hasHeaderComment } from "./lib/sweep.mjs";
|
|
39
|
-
import { listPlans
|
|
39
|
+
import { listPlans } 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";
|
|
@@ -49,6 +49,22 @@ import {
|
|
|
49
49
|
pruneState,
|
|
50
50
|
} from "./lib/claims.mjs";
|
|
51
51
|
import { appendEvent } from "./lib/session-log.mjs";
|
|
52
|
+
import { wallsAllowlist } from "./lib/graph-store.mjs";
|
|
53
|
+
import { exitWallsAllowlist } from "./lib/exit-store.mjs";
|
|
54
|
+
import { wallEnabled } from "./lib/walls.mjs";
|
|
55
|
+
|
|
56
|
+
// Whether anything will ever read a row: a run whose allowlist switches off both walls the queue feeds —
|
|
57
|
+
// the Stop wall and the commit wall — has nobody to drain it, so its rows would only ride the run's output
|
|
58
|
+
// into the project (docs/harness/code/exit-check.md).
|
|
59
|
+
function queueHasReader(projectDir) {
|
|
60
|
+
let allowlist = null;
|
|
61
|
+
try {
|
|
62
|
+
allowlist = wallsAllowlist(projectDir) ?? exitWallsAllowlist(projectDir);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
allowlist = null;
|
|
65
|
+
}
|
|
66
|
+
return wallEnabled(allowlist, "queue") || wallEnabled(allowlist, "bookkeeping");
|
|
67
|
+
}
|
|
52
68
|
|
|
53
69
|
// ---------------------------------------------------------------------------
|
|
54
70
|
// SessionStart
|
|
@@ -330,19 +346,6 @@ function detectorForFile(projectDir, rel, meta, rows) {
|
|
|
330
346
|
})
|
|
331
347
|
);
|
|
332
348
|
}
|
|
333
|
-
// Whether the plan's documents are TRUE is the polygraph skill's judgment, not a hook's — so
|
|
334
|
-
// the gate's trace (the `Polygraph:` segment of the Gates line) is raised the same way the doc
|
|
335
|
-
// ledger is: a `needs-session:` row while it reads `pending` on a Complete plan. A plan with no
|
|
336
|
-
// segment predates the gate or runs under a harness that ships no polygraph — out of contract.
|
|
337
|
-
if (complete && polygraphVerdict(planText) === "pending") {
|
|
338
|
-
rows.push(
|
|
339
|
-
formatNeedsSessionRow({
|
|
340
|
-
date: meta.date,
|
|
341
|
-
description: `${rel} reached Status: Complete with its documents uncertified — run the polygraph skill (gate mode) and write its Polygraph: segment`,
|
|
342
|
-
refs: rel,
|
|
343
|
-
})
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
349
|
}
|
|
347
350
|
if (rel.startsWith(`${root}/`)) {
|
|
348
351
|
const findings = docLint(projectDir, rel);
|
|
@@ -579,7 +582,7 @@ function postToolUse(projectDir, payload) {
|
|
|
579
582
|
detectOutcomes(projectDir, meta, rows);
|
|
580
583
|
}
|
|
581
584
|
}
|
|
582
|
-
appendRows(projectDir, rows);
|
|
585
|
+
if (rows.length > 0 && queueHasReader(projectDir)) appendRows(projectDir, rows);
|
|
583
586
|
}
|
|
584
587
|
|
|
585
588
|
// ---------------------------------------------------------------------------
|
|
@@ -25,6 +25,7 @@ import { appendEvent, unresolvedDenials, sessionLogPath, malformedEventLines } f
|
|
|
25
25
|
import { approvedPackages, rejectedPackages } from "./lib/approved.mjs";
|
|
26
26
|
import { claimActivity, releaseActivity, recentForeignTouches } from "./lib/claims.mjs";
|
|
27
27
|
import { atStop, graphDenial, wallsAllowlist } from "./lib/graph-store.mjs";
|
|
28
|
+
import { exitAtStop, exitWallsAllowlist, recordUnreadable } from "./lib/exit-store.mjs";
|
|
28
29
|
import { WALLS, wallEnabled } from "./lib/walls.mjs";
|
|
29
30
|
|
|
30
31
|
// --- the build graph's switch ------------------------------------------------------------
|
|
@@ -32,12 +33,13 @@ import { WALLS, wallEnabled } from "./lib/walls.mjs";
|
|
|
32
33
|
// allowlist over lib/walls.mjs. Every wall it does not name is off for that session — because
|
|
33
34
|
// the compliance set assumes a session that can dispatch a bookkeeper and a user who can answer
|
|
34
35
|
// an ask, and a generation box has neither: its walls would be unresolvable blocks, not guard
|
|
35
|
-
// rails. No graph, or a graph with no `mode` block, leaves every wall exactly as it was.
|
|
36
|
+
// rails. No graph, or a graph with no `mode` block, leaves every wall exactly as it was. With no graph
|
|
37
|
+
// declaring one, an exit set (docs/harness/code/exit-check.md) may carry the same block.
|
|
36
38
|
let allowlistCache;
|
|
37
39
|
function wallOn(projectDir, name) {
|
|
38
40
|
if (allowlistCache === undefined) {
|
|
39
41
|
try {
|
|
40
|
-
allowlistCache = wallsAllowlist(projectDir);
|
|
42
|
+
allowlistCache = wallsAllowlist(projectDir) ?? exitWallsAllowlist(projectDir);
|
|
41
43
|
} catch (e) {
|
|
42
44
|
allowlistCache = null; // a broken graph never switches a wall off
|
|
43
45
|
}
|
|
@@ -524,6 +526,29 @@ function graphStop(projectDir, stopHookActive) {
|
|
|
524
526
|
};
|
|
525
527
|
}
|
|
526
528
|
|
|
529
|
+
// The exit check (docs/harness/code/exit-check.md): authority, so a crash blocks rather than passes. It
|
|
530
|
+
// blocks once — the box walls `.claude/state`, so the model cannot repair the file — and the second stop
|
|
531
|
+
// records `blocked`, so the node still ends failed instead of met.
|
|
532
|
+
function exitStop(projectDir, stopHookActive) {
|
|
533
|
+
let step;
|
|
534
|
+
try {
|
|
535
|
+
step = exitAtStop(projectDir);
|
|
536
|
+
} catch (e) {
|
|
537
|
+
const reason = `EXIT CHECK FAILED — ${e?.message || e}`;
|
|
538
|
+
if (!stopHookActive) {
|
|
539
|
+
return { decision: "block", reason: `${reason}. This node's exit set cannot be read, so it is not finished. (Said once — the next Stop ends the node blocked.)` };
|
|
540
|
+
}
|
|
541
|
+
try {
|
|
542
|
+
recordUnreadable(projectDir, reason);
|
|
543
|
+
} catch (e2) {
|
|
544
|
+
/* nowhere to record it; the box seals without a verdict and the run owner's own check decides */
|
|
545
|
+
}
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
if (!step || step.allow) return null;
|
|
549
|
+
return { decision: "block", reason: step.block };
|
|
550
|
+
}
|
|
551
|
+
|
|
527
552
|
function stopGate(projectDir, payload) {
|
|
528
553
|
// The build graph is asked BEFORE the runtime's loop guard, and deliberately ignores it. The
|
|
529
554
|
// guard exists because an unbounded Stop hook is a hang; the walker is bounded instead — every
|
|
@@ -538,6 +563,9 @@ function stopGate(projectDir, payload) {
|
|
|
538
563
|
// hand-written graph should know it.
|
|
539
564
|
const graph = graphStop(projectDir, payload.stop_hook_active);
|
|
540
565
|
if (graph) return graph;
|
|
566
|
+
// The exit check is asked before the loop guard for the walker's reason: it is bounded by its own attempts.
|
|
567
|
+
const exit = exitStop(projectDir, payload.stop_hook_active);
|
|
568
|
+
if (exit) return exit;
|
|
541
569
|
|
|
542
570
|
if (payload.stop_hook_active) return null; // the runtime's infinite-loop guard — never re-block
|
|
543
571
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// harness/code/hooks/lib/exit-store.mjs — the exit check: what a NuStack generation box hands the harness
|
|
2
|
+
// instead of a build graph. The box writes `.claude/state/exit.json` naming the documents a node owes; at
|
|
3
|
+
// every Stop this module checks that each one EXISTS — never its shape — and hands the turn back naming what
|
|
4
|
+
// is missing until the attempts run out, then records `blocked` (docs/harness/code/exit-check.md).
|
|
5
|
+
//
|
|
6
|
+
// This is authority, not hygiene: the caller turns a thrown error into a block, never a pass.
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { atomicWrite, stateDir } from "./state.mjs";
|
|
11
|
+
|
|
12
|
+
export const EXIT_VERSION = 1;
|
|
13
|
+
export const EXIT_FILE = "exit.json";
|
|
14
|
+
export const EXIT_VERDICT_FILE = "exit-verdict.json";
|
|
15
|
+
|
|
16
|
+
const SKIPPED_DIRS = new Set(["node_modules", ".git"]);
|
|
17
|
+
|
|
18
|
+
// Read through a plain join: absence is dormancy, and asking must never create the folder.
|
|
19
|
+
export function exitFile(projectDir) {
|
|
20
|
+
return path.join(projectDir, ".claude", "state", EXIT_FILE);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function exitVerdictFile(projectDir) {
|
|
24
|
+
return path.join(stateDir(projectDir), EXIT_VERDICT_FILE);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Absent → null. Present but not a v1 exit set → throws: a set nobody can read is not a set that is met.
|
|
28
|
+
export function loadExit(projectDir) {
|
|
29
|
+
let text;
|
|
30
|
+
try {
|
|
31
|
+
text = fs.readFileSync(exitFile(projectDir), "utf8");
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const set = JSON.parse(text);
|
|
36
|
+
const attempts = set?.attempts;
|
|
37
|
+
if (!set || set.v !== EXIT_VERSION || !Array.isArray(set.owed) || !Number.isInteger(attempts?.used) || !Number.isInteger(attempts?.max)) {
|
|
38
|
+
throw new Error(`.claude/state/${EXIT_FILE} is not a v${EXIT_VERSION} exit set`);
|
|
39
|
+
}
|
|
40
|
+
return set;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// `*` matches within one path segment; everything else is literal.
|
|
44
|
+
export function globMatches(glob, rel) {
|
|
45
|
+
const pattern = glob
|
|
46
|
+
.split("*")
|
|
47
|
+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
|
|
48
|
+
.join("[^/]+");
|
|
49
|
+
return new RegExp(`^${pattern}$`).test(rel);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isFile(projectDir, rel) {
|
|
53
|
+
try {
|
|
54
|
+
return fs.statSync(path.join(projectDir, rel)).isFile();
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// How many files match a glob, walking only below its literal prefix.
|
|
61
|
+
function globCount(projectDir, glob) {
|
|
62
|
+
const segments = glob.split("/");
|
|
63
|
+
const wild = segments.findIndex((segment) => segment.includes("*"));
|
|
64
|
+
const base = wild === -1 ? path.posix.dirname(glob) : segments.slice(0, wild).join("/");
|
|
65
|
+
let count = 0;
|
|
66
|
+
const walk = (rel) => {
|
|
67
|
+
let entries;
|
|
68
|
+
try {
|
|
69
|
+
entries = fs.readdirSync(path.join(projectDir, rel), { withFileTypes: true });
|
|
70
|
+
} catch (e) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (SKIPPED_DIRS.has(entry.name)) continue;
|
|
75
|
+
const child = rel ? `${rel}/${entry.name}` : entry.name;
|
|
76
|
+
if (entry.isDirectory()) walk(child);
|
|
77
|
+
else if (entry.isFile() && globMatches(glob, child)) count += 1;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
walk(base === "." ? "" : base);
|
|
81
|
+
return count;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The owed entries not on disk: a path that is not a file, or a glob matching fewer files than its floor.
|
|
85
|
+
export function missingOf(projectDir, set) {
|
|
86
|
+
const missing = [];
|
|
87
|
+
for (const entry of set.owed) {
|
|
88
|
+
if (typeof entry?.path === "string") {
|
|
89
|
+
if (!isFile(projectDir, entry.path)) missing.push({ type: entry.type, path: entry.path });
|
|
90
|
+
} else if (typeof entry?.glob === "string") {
|
|
91
|
+
const min = Number.isInteger(entry.min) && entry.min > 0 ? entry.min : 1;
|
|
92
|
+
const found = globCount(projectDir, entry.glob);
|
|
93
|
+
if (found < min) missing.push({ type: entry.type, glob: entry.glob, min, found });
|
|
94
|
+
} else {
|
|
95
|
+
throw new Error(`an owed entry names neither a path nor a glob: ${JSON.stringify(entry)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return missing;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function describeMissing(entry) {
|
|
102
|
+
return entry.path
|
|
103
|
+
? `\`${entry.path}\` (${entry.type})`
|
|
104
|
+
: `at least ${entry.min} file(s) matching \`${entry.glob}\` (${entry.type}, ${entry.found} found)`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function writeVerdict(projectDir, verdict) {
|
|
108
|
+
atomicWrite(exitVerdictFile(projectDir), `${JSON.stringify({ v: EXIT_VERSION, ...verdict }, null, 2)}\n`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Stop: null when dormant; `{ allow, verdict }` when the set is met or its attempts are spent; `{ block }` otherwise, charging one attempt.
|
|
112
|
+
export function exitAtStop(projectDir) {
|
|
113
|
+
const set = loadExit(projectDir);
|
|
114
|
+
if (!set) return null;
|
|
115
|
+
const missing = missingOf(projectDir, set);
|
|
116
|
+
const nodeSlug = set.nodeSlug ?? null;
|
|
117
|
+
if (missing.length === 0) {
|
|
118
|
+
writeVerdict(projectDir, { nodeSlug, verdict: "done", missing, attempts: set.attempts });
|
|
119
|
+
return { allow: true, verdict: "done", missing };
|
|
120
|
+
}
|
|
121
|
+
if (set.attempts.used >= set.attempts.max) {
|
|
122
|
+
writeVerdict(projectDir, { nodeSlug, verdict: "blocked", missing, attempts: set.attempts });
|
|
123
|
+
return { allow: true, verdict: "blocked", missing };
|
|
124
|
+
}
|
|
125
|
+
const attempts = { ...set.attempts, used: set.attempts.used + 1 };
|
|
126
|
+
atomicWrite(exitFile(projectDir), `${JSON.stringify({ ...set, attempts }, null, 2)}\n`);
|
|
127
|
+
const left = attempts.max - attempts.used;
|
|
128
|
+
return {
|
|
129
|
+
block:
|
|
130
|
+
`EXIT CHECK — this node owes documents that do not exist yet: ${missing.map(describeMissing).join("; ")}. Write them, then stop. ` +
|
|
131
|
+
(left > 0
|
|
132
|
+
? `This stop was handed back; ${left} more will be, and after that the node ends blocked.`
|
|
133
|
+
: "The next stop ends the node blocked if any of them is still missing."),
|
|
134
|
+
missing,
|
|
135
|
+
attempts,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A set that cannot be read still ends the node — as blocked, naming why — rather than as met.
|
|
140
|
+
export function recordUnreadable(projectDir, reason) {
|
|
141
|
+
writeVerdict(projectDir, { nodeSlug: null, verdict: "blocked", missing: [{ type: "exit-check", reason }], attempts: { used: 0, max: 0 } });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The set's `mode.walls` allowlist, or null. A broken file switches no wall off: hygiene fails open.
|
|
145
|
+
export function exitWallsAllowlist(projectDir) {
|
|
146
|
+
try {
|
|
147
|
+
const set = loadExit(projectDir);
|
|
148
|
+
return Array.isArray(set?.mode?.walls) ? set.mode.walls : null;
|
|
149
|
+
} catch (e) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -28,16 +28,7 @@ export function inPlanScope(rel, scope) {
|
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
//
|
|
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,
|
|
31
|
+
// Every plan in the folder, parsed once: { file, status, gates, scope, hasLedger,
|
|
41
32
|
// hasDocLedger, ledgerRows }. `gates` is null on legacy plans (pre-Gates format).
|
|
42
33
|
export function listPlans(projectDir) {
|
|
43
34
|
const dir = path.join(projectDir, docsRoot(projectDir), "history", "plans");
|
|
@@ -56,7 +47,6 @@ export function listPlans(projectDir) {
|
|
|
56
47
|
file,
|
|
57
48
|
status,
|
|
58
49
|
gates: (/^\*\*Gates:\*\*\s*(.+)$/m.exec(text) || [])[1]?.trim() || null,
|
|
59
|
-
polygraph: polygraphVerdict(text),
|
|
60
50
|
scope: planScopePaths(text),
|
|
61
51
|
hasLedger: /^##\s+Test Obligations\b/m.test(text),
|
|
62
52
|
hasDocLedger: /^##\s+Doc Obligations\b/m.test(text),
|
|
@@ -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 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:`**
|
|
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. A **`Polygraph:`** segment appears only when the user has the `polygraph` agent audit the plan's documents — it appends `Polygraph: passed YYYY-MM-DD (N docs · N claims · N corrected · N registered)`, or `Polygraph: pending` while an owner question is open. It is a trace for readers, never an obligation: a plan carries none by default, no hook or CI check reads it, and it is never hand-written. 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
|
|
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,71 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: polygraph
|
|
3
|
-
description:
|
|
3
|
+
description: /polygraph <target> — dispatch the polygraph agent on a doc, feature, folder, code path, or plan. User-invoked only; the model never loads this on its own.
|
|
4
|
+
disable-model-invocation: true
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
#
|
|
7
|
+
# /polygraph — the user's door to the audit
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
The docs-truth audit is the **`polygraph` agent** (`.claude/agents/polygraph.md` in a consumer repo, `agents/polygraph.md` in etna), and it runs only when the user asks. This command is that ask, made explicit: `/polygraph docs/features/billing.md`, `/polygraph billing`, `/polygraph docs/architecture/`, `/polygraph src/billing/`, or `/polygraph docs/history/plans/2026-09-01-billing.md`.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 |
|
|
11
|
+
1. **Dispatch the `polygraph` agent** with the argument as its target, word for word. No argument ⇒ ask for one and dispatch nothing.
|
|
12
|
+
2. **Relay its report** to the user as it came — the counts, the corrections, the register rows, and every open question in full. Its questions are the user's to answer; never answer them yourself, and never fold an unanswered question into a doc edit.
|
|
13
|
+
3. **An answer goes back as a further dispatch** to the same agent — the row id, the question, and the user's words verbatim — and its report closes the loop.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
This folder also carries what the agent reads and runs, so it ships wherever the agent does:
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
| Reference | Load when |
|
|
17
|
+
| Path | Read by |
|
|
20
18
|
| --- | --- |
|
|
21
|
-
| `references/rows.md` |
|
|
22
|
-
| `references/dispatch.md` |
|
|
23
|
-
|
|
24
|
-
|
|
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, comment claims rescued into their doc and comment residue registered; 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.
|
|
19
|
+
| `references/rows.md` | the agent — the ten-field row, the seven verdicts, the residue guard, frozen records, a document's right to exist |
|
|
20
|
+
| `references/dispatch.md` | the agent — the run folder, the context pack, the three roles, packets, verification, fold-back, recovery, price |
|
|
21
|
+
| `scripts/audit.mjs` | the agent — the read-only arithmetic over a run folder |
|
|
22
|
+
| `evals/` | authoring time only; never shipped |
|
|
@@ -24,7 +24,7 @@ One page per target document, written by you before the first dispatch. A brief
|
|
|
24
24
|
|
|
25
25
|
| Section | Content |
|
|
26
26
|
| --- | --- |
|
|
27
|
-
| Target | the doc path;
|
|
27
|
+
| Target | the doc path; for a plan target, the plan path and the Proposed Changes paths that intersect this doc's `owns:` |
|
|
28
28
|
| Code | the doc's `owns:` globs, resolved to the files that exist today |
|
|
29
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
30
|
| Already tracked | backlog and register rows that name a subject this doc covers, as `path:line` |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"harness": "delivery",
|
|
3
3
|
"name": "delivery",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.35",
|
|
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": "
|
|
13
|
+
"digest": "f4807d25316f"
|
|
14
14
|
}
|
|
@@ -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 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:`**
|
|
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. A **`Polygraph:`** segment appears only when the user has the `polygraph` agent audit the plan's documents — it appends `Polygraph: passed YYYY-MM-DD (N docs · N claims · N corrected · N registered)`, or `Polygraph: pending` while an owner question is open. It is a trace for readers, never an obligation: a plan carries none by default, no hook or CI check reads it, and it is never hand-written. 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
|
|
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.
|
|
4
|
+
"version": "1.5.25",
|
|
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": "
|
|
13
|
+
"digest": "d84075a6eda5"
|
|
14
14
|
}
|
|
@@ -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 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:`**
|
|
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. A **`Polygraph:`** segment appears only when the user has the `polygraph` agent audit the plan's documents — it appends `Polygraph: passed YYYY-MM-DD (N docs · N claims · N corrected · N registered)`, or `Polygraph: pending` while an owner question is open. It is a trace for readers, never an obligation: a plan carries none by default, no hook or CI check reads it, and it is never hand-written. 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
|
|
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.
|
|
3
|
+
"version": "0.20.23",
|
|
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": "
|
|
45
|
+
"etnaFingerprint": "722680de8132"
|
|
46
46
|
}
|