task-pipeline-skill 1.68.0 → 1.69.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +799 -0
  2. package/README.md +25 -0
  3. package/SKILL-CARD.md +1 -1
  4. package/bin/task-pipeline.js +30 -0
  5. package/package.json +4 -3
  6. package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
  7. package/plugins/task-pipeline/agents/verifier.md +88 -0
  8. package/plugins/task-pipeline/commands/task-pipeline.md +22 -0
  9. package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +2 -1
  10. package/plugins/task-pipeline/skills/task-pipeline/graph.example.json +73 -0
  11. package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +253 -0
  12. package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +46 -3
  13. package/plugins/task-pipeline/skills/task-pipeline/references/acceptance.md +36 -0
  14. package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +1 -1
  15. package/plugins/task-pipeline/skills/task-pipeline/references/continuity.md +9 -1
  16. package/plugins/task-pipeline/skills/task-pipeline/references/documentation.md +17 -0
  17. package/plugins/task-pipeline/skills/task-pipeline/references/gates.md +46 -1
  18. package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
  19. package/plugins/task-pipeline/skills/task-pipeline/references/progress.md +41 -0
  20. package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +11 -1
  21. package/plugins/task-pipeline/skills/task-pipeline/references/verification.md +52 -0
  22. package/plugins/task-pipeline/skills/task-pipeline/references/work-graph.md +121 -0
  23. package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +1113 -0
  24. package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +1 -0
  25. package/plugins/task-pipeline/skills/task-pipeline/templates/carryover.md +1 -1
  26. package/plugins/task-pipeline/skills/task-pipeline/templates/convergence.sh +146 -0
  27. package/plugins/task-pipeline/skills/task-pipeline/templates/exposure.sh +104 -1
  28. package/plugins/task-pipeline/skills/task-pipeline/templates/hooks.example.json +13 -1
  29. package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +32 -0
  30. package/plugins/task-pipeline/skills/task-pipeline/templates/verification.md +67 -5
@@ -21,6 +21,7 @@ from `super-ux`.
21
21
  | `open-questions.md` | `docs/OPEN_QUESTIONS.md` | 0 seeds it, the Doc Loop resolves rows |
22
22
  | `docgate.sh` | `scripts/check-docs.sh` | 0 seeds it · 9 runs it · 10 proves it |
23
23
  | `exposure.sh` | `scripts/exposure.sh` | 0 reads it · 10 recomputes it · `checkup` prints it |
24
+ | `convergence.sh` | `scripts/check-convergence.sh` | 0 seeds it · **10 runs it before the release closes** — only where the project pins components |
24
25
  | `hygiene.sh` | `scripts/check-hygiene.sh` | 0 seeds it · **5 runs it after every task** · 6 and 9 run it · 10 proves it |
25
26
  | `stage-coverage.sh` | `scripts/stage-coverage.sh` | 0 seeds it · **10 runs it before the coverage table** — every stage the flow declares must carry a verdict, or the flow stops declaring one it merges |
26
27
  | `hooks.example.json` | the project's `.claude/settings.json` | 0 — offered, never installed silently |
@@ -41,7 +41,7 @@ set that may only shrink, and it is printed beside every gate verdict:**
41
41
  ```
42
42
  GATE 6 tests: PASS — full suite green (247 tests)
43
43
  carry-over: 4 open (was 6) · unresolved: 0 · audit findings deferred: 2
44
- abstained: 1 (1 cannot-verify) · unlooked: 2 dormant
44
+ abstained: 1 (1 cannot-verify) · unlooked: 2 dormant · holds: 1 (container: pg-test, this run)
45
45
  ```
46
46
 
47
47
  That one line is the whole mechanism. Without it, `PASS` reads as *verified*; with
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env bash
2
+ # check-convergence.sh — release acceptance across components, for <project>.
3
+ #
4
+ # Seeded by task-pipeline (references/acceptance.md → *Release acceptance happens at
5
+ # convergence*). IT IS YOURS NOW: extend it here, section by section.
6
+ #
7
+ # WHY IT EXISTS. Stage 10 already required `git submodule status` with no `+` and every
8
+ # repository clean and pushed. That is a statement about COMMITS: the parent points at
9
+ # the child's newest one. It does not prove anything works at those two versions
10
+ # together. A parent can point at a green submodule whose contract the parent calls
11
+ # with the previous signature, and every check passes — the submodule's suite ran
12
+ # against the submodule, the parent's against the parent, and no check ran across the
13
+ # pointer. Neither repository looks wrong alone.
14
+ #
15
+ # SCOPE: it checks the POINTERS mechanically and the SEAM by record. It does NOT run your
16
+ # cross-component test — it cannot know what that is — it requires that one was run and
17
+ # that its record names the exact versions it observed. Read this before quoting a green
18
+ # from here as evidence of a working composition.
19
+ #
20
+ # EXIT CODE IS THE OUTPUT: non-zero on any failure. Nothing runs after the VERDICT block.
21
+ #
22
+ # PORTABLE to macOS bash 3.2: no grep -P, no readarray, no mapfile, no sed -i.
23
+ #
24
+ # PROGRESSIVE ARMING: a section whose input does not exist prints `dormant:` and does NOT
25
+ # fail. A project with no components, or a range in which no component pointer moved,
26
+ # has no seam to prove — and a gate that starts red teaches its project on day one that
27
+ # the gate is noise.
28
+ #
29
+ # USAGE: check-convergence.sh [<base-ref>] # default: origin/HEAD, else the root commit
30
+ set -u
31
+
32
+ BASE="${1:-}"
33
+ FAILED=0
34
+ LIVE=0
35
+ say() { printf '%s\n' "$*"; }
36
+ ok() { LIVE=$((LIVE+1)); say "ok: $*"; }
37
+ bad() { FAILED=$((FAILED+1)); say "FAIL: $*"; }
38
+ dorm() { say "dormant: $*"; }
39
+
40
+ if ! git rev-parse --git-dir >/dev/null 2>&1; then
41
+ dorm "not a git checkout — nothing to converge"
42
+ say ""; say "VERDICT: PASS (dormant) — components 0 · live checks 0"
43
+ exit 0
44
+ fi
45
+
46
+ if [ -z "$BASE" ]; then
47
+ if git rev-parse --verify -q origin/HEAD >/dev/null 2>&1; then
48
+ BASE="origin/HEAD"
49
+ elif git rev-parse --verify -q origin/main >/dev/null 2>&1; then
50
+ BASE="origin/main"
51
+ else
52
+ BASE="$(git rev-list --max-parents=0 HEAD 2>/dev/null | tail -1)"
53
+ fi
54
+ fi
55
+ say "range: ${BASE}..HEAD"
56
+
57
+ # --- 1. the components this repository pins -----------------------------------------
58
+ if [ ! -f .gitmodules ]; then
59
+ dorm "no .gitmodules — this repository pins no components"
60
+ say ""; say "VERDICT: PASS (dormant) — components 0 · live checks 0"
61
+ exit 0
62
+ fi
63
+
64
+ COMPONENTS="$(git config -f .gitmodules --get-regexp '^submodule\..*\.path$' 2>/dev/null \
65
+ | awk '{print $2}')"
66
+ N=0
67
+ for c in $COMPONENTS; do N=$((N+1)); done
68
+ say "components: $N"
69
+
70
+ # --- 2. the pointer, mechanically ----------------------------------------------------
71
+ # A '+' means the parent points at something other than the child's checked-out commit,
72
+ # so a clone of the parent gets a different tree than the one that was tested.
73
+ DIRTY="$(git submodule status 2>/dev/null | grep '^+' | awk '{print $2}')"
74
+ if [ -n "$DIRTY" ]; then
75
+ for d in $DIRTY; do bad "$d: the parent's pointer and the child's HEAD disagree — a clone gets a different tree than the one tested"; done
76
+ else
77
+ ok "every pointer matches the child's HEAD"
78
+ fi
79
+
80
+ # --- 3. the pin must exist on the child's REMOTE -------------------------------------
81
+ # Measured here, 2026-08-16: a release tag failed CI at checkout because the parent
82
+ # pinned a commit that existed only on one machine. `git submodule status` showed no '+',
83
+ # because the pointer matched the LOCAL head. This is the check that was missing.
84
+ # The PARENT'S POINTER, not the child's HEAD. Those are the same fact only while they
85
+ # agree, and they disagree in exactly the case this section exists for. The first draft
86
+ # read `git -C <c> rev-parse HEAD` and, on its first live run against a real parent,
87
+ # reported about a commit the parent does not pin.
88
+ for c in $COMPONENTS; do
89
+ PIN="$(git rev-parse "HEAD:$c" 2>/dev/null)"
90
+ [ -n "$PIN" ] || { dorm "$c: the parent's tree records no pointer for it"; continue; }
91
+ SHORT="$(printf '%.7s' "$PIN")"
92
+ if [ ! -d "$c/.git" ] && [ ! -f "$c/.git" ]; then
93
+ dorm "$c: pinned at $SHORT but not checked out — cannot ask its remote whether that commit is published"
94
+ continue
95
+ fi
96
+ if git -C "$c" branch -r --contains "$PIN" 2>/dev/null | grep -q .; then
97
+ ok "$c: pinned at $SHORT and that commit is published"
98
+ else
99
+ bad "$c: pinned at $SHORT, which no remote branch of the component contains — every clone of the parent fails at checkout while this machine stays green"
100
+ fi
101
+ done
102
+
103
+ # --- 4. the seam, by record -----------------------------------------------------------
104
+ # Required only for a component whose pointer MOVED in this range: a range that crossed
105
+ # no component boundary has no seam to prove, and demanding a record for it is how a
106
+ # gate becomes noise.
107
+ MOVED=""
108
+ for c in $COMPONENTS; do
109
+ if git diff --quiet "$BASE" HEAD -- "$c" 2>/dev/null; then :; else MOVED="$MOVED $c"; fi
110
+ done
111
+
112
+ if [ -z "$MOVED" ]; then
113
+ dorm "no component pointer moved in this range — no seam was crossed"
114
+ else
115
+ RECORD=""
116
+ for f in docs/evidence/convergence.md docs/superpowers/convergence.md CONVERGENCE.md; do
117
+ [ -f "$f" ] && RECORD="$f" && break
118
+ done
119
+ if [ -z "$RECORD" ]; then
120
+ for c in $MOVED; do
121
+ bad "$c: its pointer moved and no convergence record exists (docs/evidence/convergence.md) — the composition at these versions was never observed"
122
+ done
123
+ else
124
+ ok "convergence record: $RECORD"
125
+ for c in $MOVED; do
126
+ # Again the parent's pointer: the record must name the version the parent will
127
+ # SHIP, which is what a clone gets, not whatever is checked out here.
128
+ PIN="$(git rev-parse "HEAD:$c" 2>/dev/null)"
129
+ SHORT="$(printf '%.7s' "${PIN:-}")"
130
+ if [ -n "$SHORT" ] && grep -q "$SHORT" "$RECORD"; then
131
+ ok "$c: the record names the exact version it observed ($SHORT)"
132
+ else
133
+ bad "$c: pinned at ${SHORT:-?} and $RECORD does not name that version — a record citing no version cannot say which composition it observed"
134
+ fi
135
+ done
136
+ fi
137
+ fi
138
+
139
+ # --- VERDICT — nothing runs after this ------------------------------------------------
140
+ say ""
141
+ if [ "$FAILED" -gt 0 ]; then
142
+ say "VERDICT: FAIL — components $N · live checks $LIVE · failures $FAILED"
143
+ exit 1
144
+ fi
145
+ say "VERDICT: PASS — components $N · live checks $LIVE"
146
+ exit 0
@@ -3,7 +3,10 @@
3
3
  #
4
4
  # Seeded by task-pipeline (references/exposure.md). IT IS YOURS NOW.
5
5
  #
6
- # SCOPE: reads the verification ledger and the git tag list. It does NOT know whether a
6
+ # SCOPE: reads the verification ledger and the git tag list. The staleness section reports
7
+ # how far behind HEAD each row's observation is; it does NOT know whether the commits in
8
+ # between touched anything the row covers, so `behind` means *unproven for this tree*,
9
+ # never *wrong*. It does NOT know whether a
7
10
  # confirmation was any good, whether a `pass` in the Auto column was watched failing,
8
11
  # or anything about code. Read this header before quoting a number from here.
9
12
  #
@@ -226,6 +229,106 @@ esac
226
229
 
227
230
  echo "$LINE"
228
231
 
232
+ # ---------- staleness: a row is true about the tree it OBSERVED, not about this one ------
233
+ # B-081. The ledger tracked rows nobody had ever confirmed and had no notion of a row whose
234
+ # confirmation the tree has since overtaken. Those are the same failure from two ends and
235
+ # only one end was instrumented: a row verified at commit A stayed `verified` after commit B
236
+ # forever.
237
+ #
238
+ # This is a PORT, not a design: `references/knowledge-graph.md` already gives the code graph
239
+ # a freshness contract — a stamp, a distance, three states, and every non-current state
240
+ # ending in a marker. The same shape, applied to the ledger.
241
+ #
242
+ # NO THRESHOLD AND NO TARGET, for the same reason the `never` column has none: the honest
243
+ # number is whatever it is, and a floor here would teach a run to re-observe rows for the
244
+ # counter rather than for the question.
245
+ STALE_CURRENT=0; STALE_BEHIND=0; STALE_UNRESOLVABLE=0; STALE_UNANCHORED=0
246
+ STALE_LIST=""
247
+ if [ -f "$LEDGER" ] && git rev-parse --git-dir >/dev/null 2>&1; then
248
+ OBSERVED=$(awk -F'|' '
249
+ function trim(s) { gsub(/^[ \t]+|[ \t]+$/, "", s); return s }
250
+ function lower(s) { return tolower(s) }
251
+ {
252
+ if (NF < 3) next
253
+ c1 = lower(trim($2))
254
+ if (c1 == "req" || c1 == "id" || c1 == "#") {
255
+ ocol = 0
256
+ for (i = 2; i < NF; i++) {
257
+ n = lower(trim($i))
258
+ if (n == "observed at" || n == "observed") { ocol = i }
259
+ }
260
+ next
261
+ }
262
+ if (!ocol) next
263
+ if (trim($2) ~ /^-+$/) next
264
+ id = trim($2); v = trim($(ocol))
265
+ gsub(/[`*]/, "", v)
266
+ if (id == "" || id ~ /^-+$/) next
267
+ print id "\t" v
268
+ }
269
+ ' "$LEDGER" 2>/dev/null)
270
+
271
+ if [ -z "$OBSERVED" ]; then
272
+ STALE_STATE="dormant: the ledger has no \`Observed at\` column — no row can say which tree it saw"
273
+ else
274
+ STALE_STATE=""
275
+ OLDIFS=$IFS; IFS='
276
+ '
277
+ for line in $OBSERVED; do
278
+ IFS=$OLDIFS
279
+ rid=$(printf '%s' "$line" | cut -f1)
280
+ sha=$(printf '%s' "$line" | cut -f2)
281
+ case "$sha" in
282
+ ""|"-"|"—"|"never"|"n/a")
283
+ STALE_UNANCHORED=$((STALE_UNANCHORED+1))
284
+ STALE_LIST="$STALE_LIST$rid unanchored — no commit recorded, so nothing can say what it saw
285
+ " ;;
286
+ *)
287
+ if git rev-parse --verify -q "${sha}^{commit}" >/dev/null 2>&1; then
288
+ behind=$(git rev-list --count "${sha}..HEAD" 2>/dev/null || echo 0)
289
+ if [ "${behind:-0}" -eq 0 ]; then
290
+ STALE_CURRENT=$((STALE_CURRENT+1))
291
+ else
292
+ ts=$(git log -1 --format=%ct "$sha" 2>/dev/null || echo "")
293
+ now=$(git log -1 --format=%ct HEAD 2>/dev/null || echo "")
294
+ days=""
295
+ if [ -n "$ts" ] && [ -n "$now" ]; then days=" / $(( (now - ts) / 86400 ))d"; fi
296
+ STALE_BEHIND=$((STALE_BEHIND+1))
297
+ plural="commits"; [ "$behind" -eq 1 ] && plural="commit"
298
+ STALE_LIST="$STALE_LIST$rid observed \`$sha\` — $behind $plural$days behind HEAD — not trusted for the current tree until re-observed
299
+ "
300
+ fi
301
+ else
302
+ STALE_UNRESOLVABLE=$((STALE_UNRESOLVABLE+1))
303
+ STALE_LIST="$STALE_LIST$rid observed \`$sha\`, which does not resolve in this checkout (rebase, squash or shallow clone) — not trusted until re-observed
304
+ "
305
+ fi ;;
306
+ esac
307
+ IFS='
308
+ '
309
+ done
310
+ IFS=$OLDIFS
311
+ fi
312
+ else
313
+ STALE_STATE="dormant: no ledger or not a git checkout"
314
+ fi
315
+
316
+ echo ""
317
+ if [ -n "${STALE_STATE:-}" ]; then
318
+ echo "staleness — $STALE_STATE"
319
+ else
320
+ # State zero out loud: printing nothing when every row is current is what makes
321
+ # freshness indistinguishable from a check that never looked.
322
+ echo "staleness — current $STALE_CURRENT · behind $STALE_BEHIND · unresolvable $STALE_UNRESOLVABLE · unanchored $STALE_UNANCHORED"
323
+ echo " (a disclosure: no floor, no direction, never a target. Invalidation is"
324
+ echo " not deletion — an overtaken row stays true about the tree it observed.)"
325
+ if [ -n "$STALE_LIST" ]; then
326
+ printf '%s' "$STALE_LIST" | head -8 | while IFS= read -r l; do
327
+ [ -n "$l" ] && echo " $l"
328
+ done
329
+ fi
330
+ fi
331
+
229
332
  # ---------- the check-list: a number without it says there is a problem, not where ----------
230
333
  # A SHRUG IS NOT A CLEAN BILL. A status that is neither a date nor a known word means
231
334
  # somebody wrote something and nobody can act on it; printing "every shipped row is
@@ -16,6 +16,18 @@
16
16
  }
17
17
  ]
18
18
  }
19
+ ],
20
+ "PostToolUse": [
21
+ {
22
+ "matcher": "Read",
23
+ "hooks": [
24
+ {
25
+ "type": "command",
26
+ "command": "f=$(python3 -c 'import json,sys;print(json.load(sys.stdin).get(\"tool_input\",{}).get(\"file_path\",\"\"))'); case \"$f\" in *task-pipeline/references/*.md) r=\"references/${f##*/}\"; l=.task-pipeline/run.md; mkdir -p .task-pipeline; touch \"$l\"; grep -q \"^read: $r$\" \"$l\" || echo \"read: $r\" >> \"$l\";; esac; exit 0"
27
+ }
28
+ ]
29
+ }
19
30
  ]
20
- }
31
+ },
32
+ "_read_note": "The PostToolUse/Read hook writes `read: references/<file>.md` into the run ledger, deduplicated, and ALWAYS exits 0 — a hook that can fail a Read would break every turn in every session. It is what makes `scripts/graph.py doctrine` able to say which doctrine a run opened. Without it that verb reports `unmeasured` rather than 0: a skipped file and a read one are otherwise indistinguishable, and 0 would be the reassuring answer to a question nobody asked. Hooks exist only in Claude Code; on any other agent the count stays `unmeasured` and says so."
21
33
  }
@@ -18,6 +18,33 @@ Run: `<topic>` · started `<YYYY-MM-DD>` · module map: `<path or "none">`
18
18
  - Why it is not the build ledger
19
19
  - What closes it, and what survives it
20
20
 
21
+ ## `read:` — which doctrine this run actually opened
22
+
23
+ The bundle is 34 reference files and nothing recorded which of them a run read, so **a
24
+ skipped file and a read one were indistinguishable** — the class every guard in this
25
+ pipeline exists to catch, left standing over the doctrine itself.
26
+
27
+ `read:` is **hook-written, never agent-written**, for the same reason `gate:` is: a claim
28
+ about what somebody read, written by the party the claim is about, is not evidence. The
29
+ hook is in `hooks.example.json`, matches `Read`, records the path only
30
+ when it is inside the bundle's `references/`, deduplicates, and **always exits 0** — a hook
31
+ that can fail a `Read` would break every turn in every session.
32
+
33
+ `scripts/graph.py doctrine` reads these lines and prints one of three things:
34
+
35
+ | It prints | When | Why not just a number |
36
+ |---|---|---|
37
+ | `unmeasured — no run ledger` | there is no ledger | nothing to read from |
38
+ | `unmeasured — the ledger carries no read: lines` | the hook is absent, **or** the run opened no doctrine | two opposite facts, and the ledger cannot separate them, so neither is claimed |
39
+ | `N of 34 reference files read`, then each unread one | the hook is installed and fired | the count alone says there is a gap, not where |
40
+
41
+ **It is a disclosure: no floor, no direction, never a target.** A run that needs four files
42
+ and reads four is not worse than one that reads thirty — and the moment the number becomes
43
+ something to raise, agents will open files to raise it. There is **no per-file reading
44
+ floor** in this pipeline; stage 0's mandatory items are the floor that exists, and they are
45
+ not per-file. Inventing one inside a measurement would be a doctrine decision smuggled in
46
+ as a count.
47
+
21
48
  ## Lines
22
49
 
23
50
  Append-only. These shapes, and nothing else belongs here (the list is the count — a written one drifts, and this one already had):
@@ -27,9 +54,11 @@ stage: <id> <name> — gate <auto|manual> — verdict <pass|fail|skip> — <ISO-
27
54
  iter: <N> — item <B-NNN or task id> — closed at gate <stage id>
28
55
  touch: <file> — pass <N> (<stage|round|module>) — reason: <finding id / gate item>
29
56
  hand: <N|10> — task "<quoted>" — done <n> — surfaced <n> — decisions <n> — amb <n> (<ids or "— no register">)
57
+ scope <commit>/<env>/<REQ ids> — unverified <n|none-in-scope> (<what, or the literal>)
30
58
  holds: <stage id> — <n> (<class: what, owner>; … or "none") — enumerated <n>/8 classes, <unlooked: classes not enumerable>
31
59
  gate: <stage id> — command "<cmd>" — exit <N> — <ISO-8601>
32
60
  event: <compact|session-end|subagent> — <detail> — <ISO-8601>
61
+ read: references/<file>.md # hook-written, deduplicated, one per file opened
33
62
  ```
34
63
 
35
64
  - **`stage:`** — written when a gate **returns**, not when the stage is entered. The
@@ -77,6 +106,8 @@ never entered look identical from outside, and they mean opposite things.
77
106
  ## Log
78
107
 
79
108
  ```
109
+ read: references/grill.md
110
+ read: references/knowledge-sources.md
80
111
  stage: 0 Intake — gate manual — verdict pass — 2026-08-10T11:14Z
81
112
  iter: 1 — item B-025 — closed at gate 0
82
113
  stage: 1 Docs study — gate auto — verdict pass — 2026-08-10T11:31Z
@@ -88,6 +119,7 @@ event: subagent — general-purpose — 2026-08-10T12:00Z
88
119
  gate: 6 — command "npm test" — exit 0 — 2026-08-10T12:02Z
89
120
  stage: 6 Tests — gate manual — verdict pass — 2026-08-10T12:03Z
90
121
  hand: 3 — task "add CSV export to the orders table" — done 2 — surfaced 1 — decisions 1 — amb 2 (OQ-0007, ledger row 4)
122
+ scope 5f21ac3/node-24-linux/REQ-001,REQ-004 — unverified 1 (XLSX path: no fixture)
91
123
  holds: 5 — 2 (worktree: build-csv-export, this run; container: pg-test, this run) — enumerated 8/8 classes
92
124
  holds: 10 — none — enumerated 7/8 classes, unlooked: containers (no docker on this host)
93
125
  ```
@@ -9,11 +9,73 @@
9
9
  > after it shipped, nor whether it still works N releases later, and it dies with its
10
10
  > run. This file is the column that outlives it.
11
11
 
12
- | REQ | What | Run | Shipped in | Auto | Human | Note |
13
- |---|---|---|---|---|---|---|
14
- | REQ-001 | CSV export from a report | `2026-07-28-export` | v1.4.0 | pass | 2026-07-30 | opened the deployed page, exported, opened the file |
15
- | REQ-004 | XLSX export | `2026-07-28-export` | v1.4.0 | pass | **never** | — |
16
- | REQ-007 | Export respects active filters | `2026-07-28-export` | v1.4.0 | partial | **never** | CSV path only |
12
+ ## Producer — what wrote the rows below
13
+
14
+ Computed, never typed, and every field prints even when it cannot be resolved:
15
+
16
+ ```bash
17
+ python3 scripts/graph.py producer # from the project root
18
+ ```
19
+
20
+ ```text
21
+ actor: unavailable: TASK_PIPELINE_ACTOR is not set by this harness
22
+ model: unavailable: TASK_PIPELINE_MODEL is not set by this harness
23
+ runtime: unavailable: TASK_PIPELINE_RUNTIME is not set by this harness
24
+ skill: task-pipeline@1.69.0
25
+ config: sha256:3bb638e189c9ef46
26
+ commit: fbd8a67e6988a0893f273eb37bd9a075a036c223
27
+ trace: unavailable: TASK_PIPELINE_TRACE is not set by this harness
28
+ ```
29
+
30
+ **Paste one such block per run, above the rows that run wrote.** Without it, two runs six
31
+ months apart under different generations of this doctrine leave rows nobody can tell
32
+ apart — so a defect traced to a doctrine change cannot be scoped to the runs that carried
33
+ it. `skill`, `config` and `commit` resolve from the tree; `actor`, `model`, `runtime` and
34
+ `trace` are the harness's to export, and each says so by name when unset, because a field
35
+ that vanishes when unavailable is indistinguishable from one nobody checked.
36
+
37
+ **A field is never deleted and never guessed.** `model` in particular is not inferred:
38
+ naming a vendor id in a shipped skill is forbidden here, and inferring the wrong one is
39
+ worse than saying nothing.
40
+
41
+ ## Contents
42
+
43
+ - [Staleness — a row is true about the tree it OBSERVED](#staleness--a-row-is-true-about-the-tree-it-observed)
44
+ - the ledger itself — one row per REQ, appended by stage 8
45
+ - [What `Human` means, and what it does not](#what-human-means-and-what-it-does-not)
46
+
47
+ ## Staleness — a row is true about the tree it OBSERVED
48
+
49
+ `Observed at` is the commit the check ran against. Without it a row verified at commit A
50
+ reads `verified` after commit B forever, and the ledger tracks rows nobody ever confirmed
51
+ while having no notion of a row the tree has since **overtaken**. Those are the same
52
+ failure from two ends, and only one end was instrumented.
53
+
54
+ `scripts/exposure.sh` reports four states, and it is a **disclosure**: no floor, no
55
+ direction, never a target — for the same reason the `Human` column has none.
56
+
57
+ | State | Condition | What it means |
58
+ |---|---|---|
59
+ | **current** | 0 commits behind `HEAD` | the row still speaks about this tree |
60
+ | **behind** | N commits / M days behind | *not trusted for the current tree until re-observed* |
61
+ | **unresolvable** | the commit does not resolve here | rebase, squash or shallow clone — same marker |
62
+ | **unanchored** | no commit recorded | nothing can say what it saw |
63
+
64
+ **State zero prints out loud.** `current 12 · behind 0 · unresolvable 0 · unanchored 0` is
65
+ a measurement; printing nothing when everything is fresh is what makes freshness
66
+ indistinguishable from a check that never looked.
67
+
68
+ **Invalidation is not deletion.** An overtaken row is not wrong — it is true about the tree
69
+ it observed, and it stays. Re-observing appends a **new** row; it does not edit the old
70
+ one. Four things overtake a row, and naming which one applies is the note's job: a **code**
71
+ change in what it covers, a **dependency** change, an **environment** change, and a
72
+ **policy** change — the last being the rule under which the evidence was accepted.
73
+
74
+ | REQ | What | Run | Shipped in | Observed at | Auto | Human | Note |
75
+ |---|---|---|---|---|---|---|---|
76
+ | REQ-001 | CSV export from a report | `2026-07-28-export` | v1.4.0 | `5f21ac3` | pass | 2026-07-30 | opened the deployed page, exported, opened the file |
77
+ | REQ-004 | XLSX export | `2026-07-28-export` | v1.4.0 | `5f21ac3` | pass | **never** | — |
78
+ | REQ-007 | Export respects active filters | `2026-07-28-export` | v1.4.0 | `5f21ac3` | partial | **never** | CSV path only |
17
79
 
18
80
  ## Columns
19
81