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.
- package/CHANGELOG.md +799 -0
- package/README.md +25 -0
- package/SKILL-CARD.md +1 -1
- package/bin/task-pipeline.js +30 -0
- package/package.json +4 -3
- package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
- package/plugins/task-pipeline/agents/verifier.md +88 -0
- package/plugins/task-pipeline/commands/task-pipeline.md +22 -0
- package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +2 -1
- package/plugins/task-pipeline/skills/task-pipeline/graph.example.json +73 -0
- package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +253 -0
- package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +46 -3
- package/plugins/task-pipeline/skills/task-pipeline/references/acceptance.md +36 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/continuity.md +9 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/documentation.md +17 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/gates.md +46 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/progress.md +41 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +11 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/verification.md +52 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/work-graph.md +121 -0
- package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +1113 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/carryover.md +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/templates/convergence.sh +146 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/exposure.sh +104 -1
- package/plugins/task-pipeline/skills/task-pipeline/templates/hooks.example.json +13 -1
- package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +32 -0
- 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.
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|