task-pipeline-skill 1.14.0 → 1.15.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,144 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.15.0 — 2026-08-06
4
+
5
+ ### Added — the ten canons: what makes a document evidence
6
+
7
+ The doctrine carried the mechanisms and never stated the standard they serve.
8
+ `references/documentation.md` now opens with ten laws — a claim carries its address,
9
+ numbers are computed rather than restated, one home per fact, a reference resolves from
10
+ where the document is *read*, green nobody watched turn red is not evidence, a check
11
+ proves its scope and nothing beyond it, silence is not a pass, an estimate is never
12
+ announced as a measurement, what was not checked is printed beside what was, and the
13
+ document ships in the change that made it true.
14
+
15
+ Each canon **names where it is enforced** instead of restating the mechanism, and the
16
+ boundary against `learned.md` is written down: the canons are epistemic (what makes a
17
+ claim documentation), `learned.md` is operational (what to do at a trigger). Two
18
+ undifferentiated rule lists would be the duplication canon 3 forbids.
19
+
20
+ Guarded four ways: the list exists, all ten laws are present, every canon names an
21
+ enforcement, and the boundary is stated. The enforcement check counts **per canon**
22
+ rather than in total — a total threshold only fires once most of them are gone, which
23
+ is a check proving less than it claims, canon 6 applied to itself.
24
+
25
+ **Found while writing them, by this repository's own guard:** canon 9 cited
26
+ `audit.md → Ratchets`, a section that lives in `gates.md`. The file resolved and the
27
+ section did not — the shape a link checker cannot catch and a reader believes.
28
+
29
+ ### Added — a build date is the graph's reply about itself, not a measurement of it
30
+
31
+ Stage 0 reads the code graph before it reads anything else, and until now it recorded
32
+ that graph's freshness as `built YYYY-MM-DD`. That is true, self-reported, and silent
33
+ about the only thing the harvest needs to know: whether the graph describes the tree
34
+ this run is about to change. Twelve commits later the row still reads `built
35
+ 2026-08-05` — and still reads *fresh*, because a date with nothing subtracted from it
36
+ is a fact with no scale. It is the class `references/gates.md` named one release ago:
37
+ an actor's own reply, standing in for evidence about the world.
38
+
39
+ The occasion was this repository's own. `docs/superpowers/specs/` holds a brief, a
40
+ carry-over ledger and an acceptance document for v1.11.0, v1.12.0 and v1.13.0 — and
41
+ none for v1.14.0 or v1.14.1. Those two releases never opened stage 9, so nothing
42
+ refreshed the graph, and the next harvest would have read a two-release-old index
43
+ behind a date that looked current.
44
+
45
+ - **`references/knowledge-graph.md` gains *Measure the lag***: three commands
46
+ (`git rev-parse --verify`, `git rev-list --count`, `git log -1 --format=%ct`) so
47
+ the number comes from git rather than from judgement, and **three states** —
48
+ `built_at_commit` exact, file `mtime` approximate, and `unresolvable` — because
49
+ `graph.json` carries the commit stamp **only when the caller passed it** and
50
+ `graphify update` from the CLI does not. With one state, "could not measure" would
51
+ print like "fresh", which is the failure the section cites `gates.md` for. Zero is
52
+ stated out loud for the same reason.
53
+ - **No threshold, deliberately.** `continuity.md` refused a context-budget number on
54
+ the same grounds: an unmeasurable threshold becomes unconditional doctrine, not
55
+ config. One commit that moved the function this task is about outweighs fifty that
56
+ touched a README, so anything but `current` carries `⚠ not trusted for reach until
57
+ refreshed` — a marker, not a block. The graph is recommended everywhere else in this
58
+ bundle; a blocking staleness check would make *no graph* cheaper than a week-old one.
59
+ - **The cadence is untouched.** Stage 9 already required the refresh unconditionally,
60
+ and an `always | major | manual` mode was rejected rather than deferred: `major`
61
+ schedules exactly the state the doctrine warns about — a graph confidently wrong
62
+ between releases, read first by the next run.
63
+
64
+ ### Added — hygiene check 7: a blank line inside a table
65
+
66
+ A GFM table ends at the first blank line, so a blank line left mid-table silently
67
+ demotes every row below it to pipe-delimited prose. The file stays well-formed, every
68
+ row is still present, and a diff showing only added lines shows nothing wrong. It
69
+ happened **twice inside this run** — a carry-over ledger and the brief's decision
70
+ table — which is `audit.md`'s threshold for a mechanism rather than a third ledger
71
+ row. On its first armed pass the check found **three more** in the carry-over ledgers
72
+ of v1.12.0 and v1.13.0, which had been rendering broken since the day they were
73
+ written. All three are fixed rather than baselined behind a floor.
74
+
75
+ The check shipped with `HYGIENE_FLOOR_7` undeclared, so `judge()` compared against an
76
+ empty string and printed `ok: check 7 … 3 (floor )` over three real hits — the gate
77
+ reporting a pass it never computed, on the release about exactly that. An undeclared
78
+ floor is now a failure, not a zero, and the reason is written beside it.
79
+
80
+ ### Guards — 95 → 104, by extending a sibling rather than copying it
81
+
82
+ `test/validate.py` already enforced that the code-graph doctrine be named in **both**
83
+ halves of stage 9 — the config gate the orchestrator verifies and the section an agent
84
+ reads — because a file that "reads as law while the run never does it" is an inert
85
+ gate. Standing instruction R-003 requires running a fixed defect's definition against
86
+ its siblings, and stage 0 is the sibling: same doctrine file, one duty reading the
87
+ graph where the other refreshes it. So the existing guard was **extended**, not
88
+ duplicated.
89
+
90
+ At stage 0 the word *graph* is not the test — it was already there. The guard requires
91
+ the **measured lag**, and three more checks keep the cited section honest: the commands
92
+ must survive, all three states must survive, and `templates/brief.md` may not ship the
93
+ superseded bare-date row to every project scaffolded from it.
94
+
95
+ The repository's own drift guard caught the release mid-flight **three times**:
96
+ `SKILL-CARD.md` and `evals/RESULTS.md` said *95*, then *100*, then *102*. Its message
97
+ has always offered two fixes — *derive the number or delete it* — and three
98
+ hand-corrections in one run is the answer to which one was right. **The prose no
99
+ longer states a count at all**; it names the command that prints one. The guard's own
100
+ negative self-test was rewritten in the same move: its plant used to edit the
101
+ restated number in place, so it broke the moment the number went away — a test
102
+ coupled to the defect it was written against rather than to the rule. It now
103
+ *introduces* a count into a document that has none, which is what the guard actually
104
+ forbids.
105
+
106
+ ### Fixed — one marker, one spelling
107
+
108
+ The distrust marker this release introduces was written **four different ways inside
109
+ the release that introduced it**: the doctrine's own three-state table omitted it
110
+ entirely, its `unresolvable` row invented *"treat as stale until refreshed"*, and the
111
+ Cursor rule and the config both dropped the sigil. Review caught the first; standing
112
+ instruction R-003 — run a fixed defect's definition against its siblings — found the
113
+ other three. `audit.md` says a class seen twice becomes a mechanism rather than a
114
+ third ledger row, so a guard now requires the canonical string and rejects the second
115
+ spelling. Greppability is the marker's only property: a ledger row is prose, and the
116
+ marker is the one string a later reader can search for.
117
+
118
+ That guard shipped green for the wrong reason and was caught by probing it rather
119
+ than by reading it. It compared **per line**, and this doctrine wraps at ~80 columns —
120
+ so in `README.md` and `stages.md`, where the marker is split across two lines, it
121
+ matched nothing and reported a pass. It now normalises whitespace before counting.
122
+ Two releases running, the defect found inside the release was an instance of the
123
+ class the release was about.
124
+
125
+ ## v1.14.1 — 2026-08-05
126
+
127
+ ### Fixed — a guard below the verdict block is dead code shaped like a guard
128
+
129
+ The fourteen guards v1.14.0 added were first appended to the *end* of
130
+ `test/validate.py`, below `if errors: … sys.exit(1)`. On a clean run they executed
131
+ after `PASS` was printed; on a corrupted one `sys.exit()` fired first and they never
132
+ executed at all. Every one was green for the single reason that cannot be argued
133
+ with: it never ran. The negatives runner caught it because it requires positive
134
+ evidence (`OK:` in stdout) rather than a non-zero exit — this release's own subject,
135
+ committed by the release that names it.
136
+
137
+ A guard now reads the validator's own source and rejects any `fail(` after the
138
+ verdict. It shipped with the same defect it checks for — `find()` matched the literal
139
+ inside the guard's own body — and its negative self-test caught that within one run;
140
+ `rfind`, with the reason written beside it. Guards 94 → 95.
141
+
3
142
  ## v1.14.0 — 2026-08-05
4
143
 
5
144
  ### Added — false success: the failure mode that removes the reason to look
package/CONTRIBUTING.md CHANGED
@@ -225,7 +225,17 @@ side; `references/review.md` rates an effect asserted without one as **Important
225
225
  not Minor. A finding that never blocks is a finding the fix loop never sees.
226
226
  *(guard: `the report no longer requires verified-by lines`)*
227
227
 
228
- **30. Every invariant above names the guard that enforces it, and that guard exists.**
228
+ **30. The code graph's ledger row states a measured lag, never a build date.**
229
+ Stage 0 reads the graph first, so its freshness is the one claim a whole run rests
230
+ on — and `built 2026-08-05` is the graph's own reply about itself, true and silent
231
+ about whether it describes the tree the run is about to change. `stages.md`'s stage-0
232
+ section and the config's stage-0 gate must both require the measurement, exactly as
233
+ they do for the stage-9 refresh, and `references/knowledge-graph.md` must keep the
234
+ commands and all three signal states — with a state missing, a graph that could not
235
+ be measured prints like a fresh one.
236
+ *(guard: `never requires it — a run passes intake quoting a`)*
237
+
238
+ **31. Every invariant above names the guard that enforces it, and that guard exists.**
229
239
  This list claims to be *what the validator enforces*; it was eight guards behind when
230
240
  an audit measured it. A claim of enforcement is now checked like any other claim.
231
241
  *(guard: `whose message does not appear in`)* — and a cited literal must lie inside
package/README.md CHANGED
@@ -238,8 +238,12 @@ then, in the project root:
238
238
 
239
239
  **Stage 0 asks it what grep can't** — `graphify query "how does session reach the
240
240
  API layer"`, `graphify affected "AuthModule"`, `graphify god-nodes` — and records it
241
- in the source ledger **with its build date**, because a graph goes stale exactly like
242
- a doc. It points; the code decides.
241
+ in the source ledger **with its measured lag**, because a graph goes stale exactly
242
+ like a doc. Not a build date: `built 2026-08-05` is the graph's own reply about
243
+ itself, true and self-reported and silent about whether it describes the tree you
244
+ are about to change. The row carries `N commits / M days behind HEAD`, the signal
245
+ that measured it — and, on anything but `current`, `⚠ not trusted for reach until
246
+ refreshed`. It points; the code decides.
243
247
 
244
248
  **Stage 9 closes three artifacts, not two.** Docs, wiki, **and the graph** — in the
245
249
  agent, so the documents this stage just edited are re-extracted too:
package/SKILL-CARD.md CHANGED
@@ -12,7 +12,7 @@ harmless.
12
12
  |---|---|
13
13
  | **Purpose** | Runs a substantial task through ten gated delivery stages — intake grill, docs study, brainstorm, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs+registers, acceptance — refusing to advance until each gate passes |
14
14
  | **Owner** | ssheleg ([github.com/ssheleg/task-pipeline](https://github.com/ssheleg/task-pipeline)) |
15
- | **Version** | 1.14.0 |
15
+ | **Version** | 1.15.0 |
16
16
  | **Surface** | Claude Code (filesystem skill + plugin) and the vercel `skills` CLI. **Not** uploaded to the Skills API; custom Skills do not sync across surfaces |
17
17
  | **Dependencies** | None required. Optional: `context7` (MCP), `figma` (MCP), super-ux, agent-sync, graphify, obsidian-wiki. Every stage's doctrine ships in-repo; the one conditional requirement is super-ux for the stage-3 UX track on a user-facing task |
18
18
  | **Evaluation status** | Suite authored (15 evals, 5 categories). **Never executed** — see [`evals/RESULTS.md`](evals/RESULTS.md) |
@@ -38,7 +38,7 @@ apply.
38
38
  instruction surface, and every one is linked directly from `SKILL.md`.
39
39
  2. Read `templates/docgate.sh` before seeding it; it is the only shipped script a
40
40
  host project will run on its own repository.
41
- 3. Run `npm run test:all` — 94 guards, each with a negative self-test that plants a
41
+ 3. Run `npm run test:all` — every guard has a negative self-test that plants a
42
42
  defect and requires rejection.
43
43
  4. Run `python3 evals/run.py` for the behavioural protocol, and read
44
44
  `evals/RESULTS.md` for what has actually been observed.
@@ -54,7 +54,7 @@ apply.
54
54
  - **Versions are pinned by git tag** and mirrored into `sshlg-skills`'s catalogue.
55
55
  Rollback is `git checkout v<previous>` or pinning the previous plugin version;
56
56
  the previous version is never deleted.
57
- - **Behavioural evidence is missing, not merely thin.** 94 structural guards prove
57
+ - **Behavioural evidence is missing, not merely thin.** The structural guards prove
58
58
  the skill is well-formed. Until `evals/RESULTS.md` carries a dated run, nothing in
59
59
  this repository proves it *behaves* — triggers correctly, stays quiet on a
60
60
  question, or performs the steps it documents.
@@ -86,8 +86,16 @@ you invented — it counts because the project names it). Query each by *this ta
86
86
  nouns; it is retrieval, not a full read; stop when the terms return nothing new.
87
87
  Write a short **source ledger** into the brief — source, what it says about this
88
88
  task, how fresh, and whether this run makes it stale. `none found` is a valid row;
89
- the graph's row carries its build date, because a graph goes stale like any other
90
- source, and it **points while the code decides** never the tiebreaker.
89
+ the graph's row carries its **measured lag**, not a build date run
90
+ `git rev-list --count <built_at_commit>..HEAD` and record `N commits / M days behind
91
+ HEAD` plus the signal it came from (`built_at_commit` exact, file `mtime`
92
+ approximate when the graph carries no stamp, or `unresolvable` when the stamp does
93
+ not resolve here), ending in `⚠ not trusted for reach until refreshed` on anything
94
+ but `current` — that exact string, so one marker is greppable across every
95
+ ledger. A date is the graph's own reply about itself; the lag is the only part
96
+ that says whether it describes the tree you are about to change. A graph goes stale
97
+ like any other source, and it **points while the code decides** — never the
98
+ tiebreaker.
91
99
  **Read `docs/superpowers/retro.md` in full if it exists** — its *standing
92
100
  instructions* are what previous runs of this pipeline got wrong in this project, and
93
101
  they bind this run (they are capped at ten, so reading them all is cheap). Stamp
package/evals/RESULTS.md CHANGED
@@ -37,7 +37,8 @@ or it belongs in a check.
37
37
  | Dated runs recorded | **0** | 2026-08-03 |
38
38
 
39
39
  The bottom two numbers are the honest state of this skill's behavioural evidence.
40
- Everything else in this repository is proven by 94 structural guards that check the
40
+ Everything else in this repository is proven by structural guards the count is
41
+ whatever `npm run test:all` prints, deliberately not restated here — that check the
41
42
  *form*; these are the only checks that would speak to the *behaviour*, and they have
42
43
  not been run yet. Printed here so "63 of 63 green" is never read as "the skill is
43
44
  known to work".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "task-pipeline-skill",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "Full-cycle delivery pipeline for coding agents: a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine ships inside the skill — no companion plugin required. This package is the installer CLI.",
5
5
  "bin": {
6
6
  "task-pipeline": "bin/task-pipeline.js"
@@ -2,7 +2,7 @@
2
2
  "name": "task-pipeline",
3
3
  "displayName": "Task Pipeline",
4
4
  "description": "Runs a substantial task through a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine is built into the skill — no companion plugin required — with typed auto/manual gates, a frozen requirement spine that must close with evidence, a loop guard that breaks churn, one provider-agnostic model confirmed up front, and an optional super-ux UX track for user-facing work.",
5
- "version": "1.14.0",
5
+ "version": "1.15.0",
6
6
  "author": {
7
7
  "name": "ssheleg",
8
8
  "url": "https://x.com/sshlg93"
@@ -18,7 +18,7 @@
18
18
  ],
19
19
  "gate": {
20
20
  "type": "manual",
21
- "check": "MANDATORY stage — never skipped (only sanctioned bypass: the entry-from-super-ux short-circuit). PHASE 1, before the first question: harvest the knowledge sources (references/knowledge-sources.md) — code, THE CODE GRAPH when one is built (references/knowledge-graph.md: graphify query/affected/god-nodes answer reach, which grep cannot; detect graphify-out/graph.json — recommended, never required), CLAUDE.md/AGENTS.md, CONTEXT.md + docs/adr, docs/ + docs/ux, past pipeline briefs and carry-over ledgers, THE RETRO'S STANDING INSTRUCTIONS (docs/superpowers/retro.md — read IN FULL, not queried: they are capped at ten and they BIND this run; stamp each one the moment it fires, since that date is the only evidence behind stage 10's cold-retirement rule — references/retrospective.md), the knowledge wiki when installed (obsidian-wiki — recommended, never required; detect ~/.obsidian-wiki/config), and any other repo or hosted doc system the project names as its docs — queried by this task's own terms, with the SOURCE LEDGER written into the brief (a row per source consulted, or an explicit 'none found'; the graph's row carries its build date, because a graph goes stale like any other source). PHASE 2, the grill, built into the skill (references/grill.md) — no companion to install. Per its contract: one question at a time, a recommended answer with each, explore the codebase/docs before asking, depth-first, contradictions reconciled; EVERY answer that touches a harvested source is validated against that source — the operator outranks any document, but only out loud, and the losing side is logged for the stage-9 doc update; domain awareness applied (terms challenged against CONTEXT.md, ADRs recorded for hard-to-reverse calls). The autonomy sweep is covered — every stage 1-10 has its blockers pre-resolved (docs sources, branch/tracker policy, test + lint commands, deploy target and authorization, log/health locations, docs+wiki targets, and for UI tasks the design surface: Figma on or text-only, is the Figma MCP connected, and if it is not — ship text-only or stop and connect it, since the UX chain degrades on its own and never blocks; plus, with Figma on, the DESIGN DESTINATION — which team/org by name and which file (the recorded one, a URL the operator gives, or creation in that named team with the creation explicitly authorized), written into the project's canonical record before the first frame, and never created while a recorded file resolves — an unreachable recorded file means stop and ask, never make a replacement) or is explicitly marked 'stop and ask here'. UI verdict recorded (arms super-ux); model decision recorded. All of it locked into a committed task brief the operator confirms before stage 1. The REQ table is written — one row per independently verifiable deliverable, each naming how it is verified — and frozen: adding later is free, removing or narrowing needs the operator's explicit agreement. The carry-over ledger is seeded. PHASE 1b, THE DOCUMENTATION INVENTORY (references/documentation.md): four questions answered into docs/DOCMAP.md before the interview — where settled things live (the DECISION HOME, and there is exactly one per project: an existing docs/adr/ IS the register and is recorded as such, never duplicated), what each fact's single home is, what a change of type X obliges (THE PROPAGATION MATRIX, non-empty, every row naming the check that enforces it or the word 'review' with a one-line reason), and what proves it (the gate command). A project with no answers gets them seeded — registers, matrix and scripts/check-docs.sh from the skill's templates — and the seeding is recorded as the register's first entry; the seeded gate must exit 0 on its own seeds, because a project that starts red teaches everyone on day one that the gate is noise. The regime is recorded. PHASE 1c, RECONCILE: git says how it should be, the run record says how it turned out — read both for the area about to be touched and resolve every divergence (the document is stale, the record is wrong, or they genuinely disagree and that is a decision), because starting on an unresolved divergence means building against a system that does not exist. The retro's in-force sections are read IN FULL and its archive is QUERIED by the task's nouns."
21
+ "check": "MANDATORY stage — never skipped (only sanctioned bypass: the entry-from-super-ux short-circuit). PHASE 1, before the first question: harvest the knowledge sources (references/knowledge-sources.md) — code, THE CODE GRAPH when one is built (references/knowledge-graph.md: graphify query/affected/god-nodes answer reach, which grep cannot; detect graphify-out/graph.json — recommended, never required), CLAUDE.md/AGENTS.md, CONTEXT.md + docs/adr, docs/ + docs/ux, past pipeline briefs and carry-over ledgers, THE RETRO'S STANDING INSTRUCTIONS (docs/superpowers/retro.md — read IN FULL, not queried: they are capped at ten and they BIND this run; stamp each one the moment it fires, since that date is the only evidence behind stage 10's cold-retirement rule — references/retrospective.md), the knowledge wiki when installed (obsidian-wiki — recommended, never required; detect ~/.obsidian-wiki/config), and any other repo or hosted doc system the project names as its docs — queried by this task's own terms, with the SOURCE LEDGER written into the brief (a row per source consulted, or an explicit 'none found'; THE GRAPH'S ROW CARRIES ITS MEASURED LAG — commits and days behind HEAD, the signal it was measured with (built_at_commit exact / mtime approximate / unresolvable), and the marker ' not trusted for reach until refreshed' — that exact string, so one marker is greppable across every ledger — on anything but current. A bare build date does NOT satisfy this: it is the graph's own reply about itself, true and self-reported and silent about whether the graph describes the tree this run is about to change — references/knowledge-graph.md -> Measure the lag, and references/gates.md -> False success for the class). PHASE 2, the grill, built into the skill (references/grill.md) — no companion to install. Per its contract: one question at a time, a recommended answer with each, explore the codebase/docs before asking, depth-first, contradictions reconciled; EVERY answer that touches a harvested source is validated against that source — the operator outranks any document, but only out loud, and the losing side is logged for the stage-9 doc update; domain awareness applied (terms challenged against CONTEXT.md, ADRs recorded for hard-to-reverse calls). The autonomy sweep is covered — every stage 1-10 has its blockers pre-resolved (docs sources, branch/tracker policy, test + lint commands, deploy target and authorization, log/health locations, docs+wiki targets, and for UI tasks the design surface: Figma on or text-only, is the Figma MCP connected, and if it is not — ship text-only or stop and connect it, since the UX chain degrades on its own and never blocks; plus, with Figma on, the DESIGN DESTINATION — which team/org by name and which file (the recorded one, a URL the operator gives, or creation in that named team with the creation explicitly authorized), written into the project's canonical record before the first frame, and never created while a recorded file resolves — an unreachable recorded file means stop and ask, never make a replacement) or is explicitly marked 'stop and ask here'. UI verdict recorded (arms super-ux); model decision recorded. All of it locked into a committed task brief the operator confirms before stage 1. The REQ table is written — one row per independently verifiable deliverable, each naming how it is verified — and frozen: adding later is free, removing or narrowing needs the operator's explicit agreement. The carry-over ledger is seeded. PHASE 1b, THE DOCUMENTATION INVENTORY (references/documentation.md): four questions answered into docs/DOCMAP.md before the interview — where settled things live (the DECISION HOME, and there is exactly one per project: an existing docs/adr/ IS the register and is recorded as such, never duplicated), what each fact's single home is, what a change of type X obliges (THE PROPAGATION MATRIX, non-empty, every row naming the check that enforces it or the word 'review' with a one-line reason), and what proves it (the gate command). A project with no answers gets them seeded — registers, matrix and scripts/check-docs.sh from the skill's templates — and the seeding is recorded as the register's first entry; the seeded gate must exit 0 on its own seeds, because a project that starts red teaches everyone on day one that the gate is noise. The regime is recorded. PHASE 1c, RECONCILE: git says how it should be, the run record says how it turned out — read both for the area about to be touched and resolve every divergence (the document is stale, the record is wrong, or they genuinely disagree and that is a decision), because starting on an unresolved divergence means building against a system that does not exist. The retro's in-force sections are read IN FULL and its archive is QUERIED by the task's nouns."
22
22
  }
23
23
  },
24
24
  {
@@ -18,6 +18,7 @@ being written twice; write it once, here.
18
18
 
19
19
  ## Contents
20
20
 
21
+ - The canons — what makes a document evidence
21
22
  - The inventory — four questions, answered before the first line of work
22
23
  - Registers and ids
23
24
  - Single source of truth
@@ -30,6 +31,71 @@ being written twice; write it once, here.
30
31
  - Where this binds in the pipeline
31
32
  - Rationalizations
32
33
 
34
+ ## The canons — what makes a document evidence
35
+
36
+ Ten laws. Everything else in this file, and the mechanisms in [`gates.md`](gates.md),
37
+ [`learned.md`](learned.md) and [`retrospective.md`](retrospective.md), exist to serve
38
+ them. Where a canon is already enforced by something, the enforcement is **named, not
39
+ restated** — a canon that repeats its own mechanism is the second home this system
40
+ exists to prevent.
41
+
42
+ **1. A claim carries its address.** Every fact that lands in a document names where it
43
+ can be checked: `file:line`, a command with its output, a test name. A lesson names the
44
+ commit that earned it. *"We verified it" is the sentence that passes every review and
45
+ proves nothing.* → the retro's SHA-resolution guard; the finding shape in
46
+ [`setup.md`](setup.md).
47
+
48
+ **2. Numbers are computed, never restated.** A count in prose is a number that was true
49
+ once. Derive it at check time and compare the stated one against the computed one as the
50
+ same object. → [`learned.md`](learned.md) rule 8.
51
+
52
+ **3. Every fact has exactly one home.** Other documents link to it; they never restate
53
+ it. Two homes do not disagree on the day they are written — they disagree on the day one
54
+ of them is updated. → *Single source of truth*, below.
55
+
56
+ **4. A reference resolves from where the document is read.** Not from where it lives. A
57
+ link correct in `templates/` and broken everywhere the template is seeded stays green
58
+ under every link checker, because the checker resolves from the file's home. → invariant
59
+ 27; the seeded-template guard.
60
+
61
+ **5. Green nobody watched turn red is not evidence.** A check must be seen rejecting a
62
+ planted defect before its pass means anything — and the plant must be proven to have
63
+ landed in the text the check actually parses. → [`gates.md`](gates.md) → *Probing*;
64
+ [`learned.md`](learned.md) rules 4 and 5.
65
+
66
+ **6. A check proves its scope and nothing beyond it.** Every gate carries what it does
67
+ **not** cover, and quoting it wider is how "the gate is green" becomes a false statement
68
+ made in good faith. "The docs are in sync" is a command with an exit code, never a
69
+ sentence at the end of a report. → [`gates.md`](gates.md) → *Before you run a check*.
70
+
71
+ **7. Silence is not a pass.** Ask of any mechanism: what does it print when it did not
72
+ look? If that is indistinguishable from what it prints when it looked and found nothing
73
+ wrong, it is not evidence. → [`gates.md`](gates.md) → *False success*.
74
+
75
+ **8. An estimate is never announced as a measurement.** A rule that fires on a judgement
76
+ states its **evidence condition** — the observable signal that licenses it. A false
77
+ alarm does not cost one interruption; it costs the alarm. → [`continuity.md`](continuity.md)
78
+ → *The context budget*.
79
+
80
+ **9. What was not checked is printed beside what was.** Absence is a finding with one
81
+ side, so it never surfaces by comparison. Carry it as a named, counted set next to every
82
+ verdict — a ratchet, never a TODO — so `PASS` reads as *"green, and here is what nobody
83
+ looked at"*. → [`learned.md`](learned.md) rule 7; [`gates.md`](gates.md) → *Ratchets*.
84
+
85
+ **10. The document ships in the change that made it true.** Not in the next ticket —
86
+ documentation deferred is documentation that describes a system nobody is running. A
87
+ correction is **appended**, never edited over: a register that is rewritten loses the
88
+ fact that it was ever wrong, which is usually the useful part. → *The Doc Loop*, below.
89
+
90
+ ### What these are not
91
+
92
+ They are **epistemic**, not operational: they say what makes a claim documentation, not
93
+ what to do at a trigger. The operational layer is [`learned.md`](learned.md) — each rule
94
+ there carries a trigger, a check and an exit criterion. When the two seem to say the same
95
+ thing, the canon is the *why* and the rule is the *how*; edit the rule, cite the canon.
96
+
97
+ ---
98
+
33
99
  ## The inventory — four questions, answered before the first line of work
34
100
 
35
101
  Stage 0 answers these before the interview, and writes the answers to
@@ -19,6 +19,7 @@ harvest simply runs on the sources it has
19
19
 
20
20
  - Detect it, and install it once
21
21
  - Stage 0 — query the graph before you ask the person
22
+ - Measure the lag — a build date is a reply, not a measurement
22
23
  - Stage 9 — the close-out has three artifacts, not two
23
24
  - The divergence check — the graph against the docs
24
25
  - Rationalizations
@@ -78,10 +79,68 @@ Two rules keep it honest:
78
79
  belongs in the retrieval order right after the code, and never as the tiebreaker.
79
80
  Precedence, reflowed so it reads in one direction: code first, then host docs and
80
81
  ADRs, then the graph, then the wiki, then anyone's memory.
81
- - **Record it in the ledger with its build date** — source
82
- `graphify-out/graph.json`, what it said about this task, how fresh, and therefore
83
- whether stage 9 owes it a refresh. A source consulted but not recorded is a source
84
- nobody will update.
82
+ - **Record it in the ledger with its measured lag** — source
83
+ `graphify-out/graph.json`, what it said about this task, **how far behind `HEAD` it
84
+ actually is**, and therefore whether stage 9 owes it a refresh. A source consulted
85
+ but not recorded is a source nobody will update. How to measure it: next section.
86
+
87
+ ## Measure the lag — a build date is a reply, not a measurement
88
+
89
+ [`gates.md`](gates.md) → *False success* gives the law — *an actor's own reply is not
90
+ evidence about the world* — and the test: **what does it print when it did not look?**
91
+
92
+ A build date is that kind of reply. `built 2026-08-05` is true, self-reported, and
93
+ says nothing about whether the graph describes the tree this run is about to change.
94
+ Twelve commits later it still reads `built 2026-08-05`, and it still reads *fresh* —
95
+ a date with nothing subtracted from it is a fact with no scale. So the `Fresh?` cell
96
+ of the graph's ledger row carries a **measured distance from `HEAD`**, the **signal it
97
+ was measured with**, and — while that distance is not zero — a refusal to lean on it.
98
+
99
+ **Compute it; never type it** ([`learned.md`](learned.md) rule 8). Three commands, and
100
+ the first one's failure *is* a state rather than an error to handle:
101
+
102
+ ```bash
103
+ git rev-parse --verify -q "<built_at_commit>^{commit}" # which state applies
104
+ git rev-list --count "<built_at_commit>..HEAD" # commits behind
105
+ git log -1 --format=%ct "<built_at_commit>" # its timestamp → days behind
106
+ ```
107
+
108
+ `<built_at_commit>` is read from the top level of `graph.json`. **It is written only
109
+ when the caller passed it**, and `graphify update .` from the CLI does not — so the
110
+ absence of a stamp is normal, not a defect, and it gets its own state instead of
111
+ silence. Three states, each naming its signal — the same reason
112
+ [`gates.md`](gates.md) → *Progressive arming* gives a dormant check a word of its own:
113
+
114
+ | State | Condition | The `Fresh?` cell reads |
115
+ |---|---|---|
116
+ | **exact** | stamp present and resolves here | ``built `3944593` — 12 commits / 2d behind HEAD, signal: built_at_commit (exact) — ⚠ not trusted for reach until refreshed`` |
117
+ | **approximate** | no stamp in `graph.json` | ``built ≤ 2026-08-05T22:47Z — signal: file mtime (approximate; no commit stamp, so this is a lower bound) — ⚠ not trusted for reach until refreshed`` |
118
+ | **unresolvable** | stamp present, does not resolve here (rebase, squash, shallow clone) | ``built `3944593` — UNRESOLVABLE in this checkout, signal: none — ⚠ not trusted for reach until refreshed`` |
119
+
120
+ Every row above is a **non-current** graph, so every row ends with the marker. The
121
+ cell is quoted whole on purpose: a worked example is what gets copied, and an example
122
+ that omits the thing it exists to demonstrate teaches the omission.
123
+
124
+ **State zero out loud.** A current graph reads ``built `3944593` — current (0 commits
125
+ behind), signal: built_at_commit (exact)``. Printing nothing when the graph is fresh
126
+ is what makes freshness indistinguishable from a harvest that never looked, which is
127
+ the failure this section cites `gates.md` for.
128
+
129
+ **Anything but `current` ends the cell with `⚠ not trusted for reach until
130
+ refreshed`.** The marker does not block — stage 0's gate is `manual` and this is a
131
+ ledger row, not a gate. It tells the harvest what it may lean on; the run continues,
132
+ it just stops quoting the graph as though it were current.
133
+
134
+ **No threshold, deliberately.** [`continuity.md`](continuity.md) refused a
135
+ context-budget number for the same reason: an unmeasurable threshold becomes
136
+ unconditional doctrine, not config. "Ten commits is fine, eleven is not" is a number
137
+ nobody can defend, and a number nobody can defend is one every run argues its way
138
+ under — one commit that moved the function this task is about outweighs fifty that
139
+ touched a README.
140
+
141
+ **Promote it when it breaks, not before** ([`gates.md`](gates.md) → *Axis B*): this
142
+ rule sits at rung 2, a criterion in stage 0's gate. Promote it to a script the first
143
+ time a run is observed passing stage 0 with an unmeasured graph row.
85
144
 
86
145
  ## Stage 9 — the close-out has three artifacts, not two
87
146
 
@@ -160,7 +219,8 @@ sweep on every commit turns an audit axis into noise and it stops being read.
160
219
  | Excuse | Reality |
161
220
  |---|---|
162
221
  | "I'll just grep, it's faster" | Grep answers "where is this name". The questions that stop a run are "what reaches this" and "what breaks if it moves" — that is one query against a graph and an afternoon with grep. |
163
- | "The graph is probably stale" | Then it has a build date and you can say so. Stale-and-dated is a finding; stale-and-unknown is what you get by not building one. |
222
+ | "The graph is probably stale" | Then measure it and say how stale — three commands, above. *Probably* is the word a run uses right before leaning on it anyway; `12 commits / 2d behind HEAD` is a finding, and `built 2026-08-05` on its own is only a date. |
223
+ | "It says built today, that's fresh enough" | Today's date and today's `HEAD` are different claims. The date is the graph's own reply about itself; the lag is the one thing that says whether it describes the tree you are about to change. |
164
224
  | "Docs and wiki are updated, we're done" | The graph is the source the *next* harvest reads first. Leaving it behind is leaving a false premise where a machine will quote it back. |
165
225
  | "The divergence check found nothing, skip it next time" | It found nothing **on this axis, this pass**. Rotate the axis; that is the rule this check belongs to, not an exemption from it. |
166
226
  | "A hub with no doc is fine, everyone knows it" | Everyone currently on the team. The graph found it in one command; the person who joins next month will find it in a postmortem. |
@@ -94,8 +94,11 @@ the directory doesn't (installed, not built).
94
94
 
95
95
  - **Built → query it** during the harvest: `graphify query "<the task, as a
96
96
  question>"`, `graphify affected "<the thing being changed>"`, `graphify god-nodes`.
97
- Record the row **with the graph's build date**, because a graph is a source and
98
- goes stale like one.
97
+ Record the row **with the graph's measured lag** how far behind `HEAD` it is and
98
+ which signal said so — because a graph is a source and goes stale like one, and a
99
+ build date is the graph's own reply about itself rather than a measurement of it.
100
+ The three commands and the three states:
101
+ [`knowledge-graph.md`](knowledge-graph.md) → *Measure the lag*.
99
102
  - **Not installed → recommend it once**, in the preflight block, with the lines:
100
103
 
101
104
  ```bash
@@ -73,6 +73,7 @@ a row pointing outside the bundle is the defect this file exists to catch.
73
73
  | How the host project's own conventions are read | `references/conventions.md` |
74
74
  | Which companions exist, what is required, self-currency | `references/companion-skills.md` |
75
75
  | The code graph: queries, refresh, the graph↔docs divergence | `references/knowledge-graph.md` |
76
+ | How the graph's staleness is measured and stated in the ledger | `references/knowledge-graph.md` |
76
77
  | Model policy — tier not id, ask once at preflight | `references/model-tiering.md` |
77
78
  | This boundary | `references/portability.md` |
78
79
 
@@ -93,7 +93,11 @@ never that the work was skipped quietly.
93
93
  ([obsidian-wiki](https://github.com/ar9av/obsidian-wiki) — recommended,
94
94
  never required), and any **other repo or hosted doc system the project names as
95
95
  its docs**. Write the **source ledger** into the brief (a row per source, or an
96
- explicit "none found"; the graph's row carries its build date). It is retrieval
96
+ explicit "none found"; **the graph's row carries its measured lag commits and
97
+ days behind `HEAD`, the signal that measured it, and `⚠ not trusted for reach
98
+ until refreshed` on anything but `current`**
99
+ ([`knowledge-graph.md`](knowledge-graph.md) → *Measure the lag*; a build date is
100
+ the graph's own reply about itself, not a measurement of it). It is retrieval
97
101
  scoped by the task's own nouns, not a read of everything — and it is what makes
98
102
  phase 2's answers checkable instead of merely confident.
99
103
  - **Phase 1b — the documentation inventory**
@@ -152,7 +156,11 @@ never that the work was skipped quietly.
152
156
  an updated `CONTEXT.md` (terms written as they resolved) and any ADRs under
153
157
  `docs/adr/` — see `grill.md` → *Domain awareness*.
154
158
  - **GATE (manual):** shared understanding reached — **the source ledger is written
155
- (every source consulted, or an explicit "none found")**, **the documentation
159
+ (every source consulted, or an explicit "none found")**, **where a code graph
160
+ exists its row carries the measured lag and the signal it was measured with — a
161
+ bare build date does not satisfy this, because it is the graph's own reply rather
162
+ than a measurement of it** ([`knowledge-graph.md`](knowledge-graph.md) →
163
+ *Measure the lag*), **the documentation
156
164
  inventory is answered into `docs/DOCMAP.md`** with its registers, single homes,
157
165
  a non-empty propagation matrix and the gate command, **the regime is recorded**,
158
166
  **intent and as-built are reconciled with every divergence resolved**, the retro's
@@ -31,7 +31,7 @@ premise if the run leaves it wrong.
31
31
  | Source | What it says about this task | Fresh? | Authority | Stale after this run? |
32
32
  |---|---|---|---|---|
33
33
  | `docs/adr/NNNN-….md` | … | YYYY-MM | decision | no |
34
- | `graphify-out/graph.json` | reach: what calls it, what breaks if it moves | built YYYY-MM-DD | index | **yes — refresh at stage 9** |
34
+ | `graphify-out/graph.json` | reach: what calls it, what breaks if it moves | built `<sha>` — N commits / M days behind HEAD, signal: built_at_commit (exact) — ⚠ not trusted for reach until refreshed | index | **yes — refresh at stage 9** |
35
35
  | wiki: `projects/…/concepts/…` | … | YYYY-MM | context | **yes — update at stage 9** |
36
36
  | `CLAUDE.md` | test/lint/deploy commands, house rules | current | convention | no |
37
37
 
@@ -66,7 +66,8 @@ source is a recorded decision, an unquoted one is an undetected divergence.
66
66
  what it returned: … (or `nothing`)
67
67
  - **Code graph:** built / installed-not-built / not installed
68
68
  ([graphify](https://github.com/Graphify-Labs/graphify); recommended, never a gate —
69
- built → its row above carries the build date and stage 9 refreshes it)
69
+ built → its row above carries the **measured lag and the signal it was measured
70
+ with**, and stage 9 refreshes it)
70
71
 
71
72
  ## Scope
72
73
 
@@ -7,7 +7,8 @@
7
7
  # WHAT IT IS FOR: the defect class an AGENT produces and no other check looks for —
8
8
  # a half-resolved merge, a stub that outlived its task, a generation cut off in the
9
9
  # middle, a file "shortened" while being rewritten, a batch of edits where one
10
- # applied twice, a section opened and abandoned.
10
+ # applied twice, a section opened and abandoned, a blank line left in a table so
11
+ # every row below it silently stops being one.
11
12
  #
12
13
  # SCOPE: walks tracked files. In diff mode it walks only what this run changed; in
13
14
  # tree mode it walks everything, behind per-check floors.
@@ -26,9 +27,11 @@
26
27
  # 5 duplicated block — a legitimately repeated stanza 3+ lines long
27
28
  # 6 empty section — a heading used as a one-line record. If you have those,
28
29
  # they want to be list items: a heading promises a section.
30
+ # 7 split table — a document deliberately showing a broken table as an
31
+ # example of the defect.
29
32
  # Read this header before quoting a green from here as evidence.
30
33
  #
31
- # IT NEVER EDITS. It reports file:line and exits non-zero. None of the six is safely
34
+ # IT NEVER EDITS. It reports file:line and exits non-zero. None of them is safely
32
35
  # machine-fixable: deleting a "duplicated block" sometimes deletes a legitimate
33
36
  # repetition, and deleting a TODO erases a reminder instead of discharging it.
34
37
  # Fixing is the agent's job, and task-pipeline's references/build.md makes it one.
@@ -36,6 +39,8 @@
36
39
  # EXIT CODE IS THE OUTPUT: non-zero on any failure. Nothing may run after the
37
40
  # VERDICT block at the bottom — a gate that appended a check after its verdict
38
41
  # printed FAIL and returned 0, and CI was green over it for an unknown period.
42
+ # For the same reason, an UNDECLARED FLOOR is a failure, not a zero: check 7
43
+ # shipped without HYGIENE_FLOOR_7 and printed "ok … (floor )" over 3 real hits.
39
44
  #
40
45
  # PORTABLE to macOS bash 3.2: no grep -P, no sed -i, no readarray, no mapfile.
41
46
  #
@@ -62,6 +67,7 @@ HYGIENE_FLOOR_3=${HYGIENE_FLOOR_3:-0}
62
67
  HYGIENE_FLOOR_4=${HYGIENE_FLOOR_4:-0}
63
68
  HYGIENE_FLOOR_5=${HYGIENE_FLOOR_5:-0}
64
69
  HYGIENE_FLOOR_6=${HYGIENE_FLOOR_6:-0}
70
+ HYGIENE_FLOOR_7=${HYGIENE_FLOOR_7:-0}
65
71
 
66
72
  TMP=${TMPDIR:-/tmp}/hygiene.$$
67
73
  mkdir -p "$TMP" || exit 2
@@ -117,6 +123,12 @@ floor_for() {
117
123
 
118
124
  judge() { # judge <n> <count> <label>
119
125
  _f=$(floor_for "$1")
126
+ # An undeclared floor made this print "ok … (floor )" while the count was 3 — the
127
+ # gate reporting a pass it never computed. Refuse rather than default to zero: a
128
+ # missing floor is a bug in the gate, and a bug in the gate is not a passing file.
129
+ case "$_f" in
130
+ ''|*[!0-9]*) err "check $1 — $3: no floor declared (HYGIENE_FLOOR_$1) — the gate cannot judge $2 finding(s)"; return ;;
131
+ esac
120
132
  if [ "$2" -gt "$_f" ]; then
121
133
  err "check $1 — $3: $2 finding(s), floor $_f"
122
134
  sed 's/^/ /' "$TMP/hits$1"
@@ -127,7 +139,7 @@ judge() { # judge <n> <count> <label>
127
139
 
128
140
  if [ "$NFILES" -eq 0 ]; then
129
141
  dormant "every check — no files in scope"
130
- C1=0; C2=0; C3=0; C4=0; C5=0; C6=0
142
+ C1=0; C2=0; C3=0; C4=0; C5=0; C6=0; C7=0
131
143
  else
132
144
 
133
145
  # ---------- 1. conflict markers ----------
@@ -225,12 +237,41 @@ else
225
237
  judge 6 "$C6" "empty section"
226
238
  fi
227
239
 
240
+ # ---------- 7. a blank line inside a table ----------
241
+ # A GFM table ends at the first blank line. An agent appending rows to a ledger or a
242
+ # decision register routinely leaves one behind, and the rows after it silently stop
243
+ # being a table: they render as pipe-delimited prose with no header. Nothing else
244
+ # here sees it — the file is well-formed markdown, every row is present, and the
245
+ # defect is invisible in a diff that shows only the added lines.
246
+ # Found twice in one run (a carry-over ledger and a brief's decision table), which is
247
+ # what turns a finding into a check rather than a third ledger row.
248
+ : > "$TMP/hits7"
249
+ if [ "$NMD" -eq 0 ]; then
250
+ dormant "check 7 — blank line inside a table: no markdown in scope"
251
+ C7=0
252
+ else
253
+ while read -r f; do
254
+ awk -v F="$f" '
255
+ /^[[:space:]]*(```|~~~)/ { fence = !fence; next }
256
+ fence { next }
257
+ { line[NR] = $0 }
258
+ END {
259
+ for (i = 2; i < NR; i++)
260
+ if (line[i] == "" && line[i-1] ~ /^\|/ && line[i+1] ~ /^\|/)
261
+ print F ":" i ": blank line inside a table — the rows below it stop being a table"
262
+ }
263
+ ' "$f" >> "$TMP/hits7" 2>/dev/null
264
+ done < "$TMP/md"
265
+ C7=$(wc -l < "$TMP/hits7" | tr -d ' ')
266
+ judge 7 "$C7" "blank line inside a table"
267
+ fi
268
+
228
269
  fi
229
270
 
230
271
  # ---------- VERDICT — nothing may run after this block ----------
231
272
  if [ "$FAIL" -ne 0 ]; then
232
- echo "FAIL: hygiene gate — mode $MODE · conflict ${C1} · placeholder ${C2} · fence ${C3} · truncation ${C4} · duplicate ${C5} · empty-section ${C6}"
273
+ echo "FAIL: hygiene gate — mode $MODE · conflict ${C1} · placeholder ${C2} · fence ${C3} · truncation ${C4} · duplicate ${C5} · empty-section ${C6} · split-table ${C7}"
233
274
  exit 1
234
275
  fi
235
- echo "OK: hygiene gate — mode $MODE · $NFILES file(s) · conflict ${C1} · placeholder ${C2} · fence ${C3} · truncation ${C4} · duplicate ${C5} · empty-section ${C6}"
276
+ echo "OK: hygiene gate — mode $MODE · $NFILES file(s) · conflict ${C1} · placeholder ${C2} · fence ${C3} · truncation ${C4} · duplicate ${C5} · empty-section ${C6} · split-table ${C7}"
236
277
  exit 0