task-pipeline-skill 1.6.0 → 1.7.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 (28) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +68 -8
  3. package/cursor/rules/task-pipeline.mdc +82 -0
  4. package/package.json +5 -2
  5. package/plugins/task-pipeline/.claude-plugin/plugin.json +4 -2
  6. package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +41 -10
  7. package/plugins/task-pipeline/skills/task-pipeline/pipeline.example.json +6 -3
  8. package/plugins/task-pipeline/skills/task-pipeline/references/artifacts.md +10 -2
  9. package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +5 -1
  10. package/plugins/task-pipeline/skills/task-pipeline/references/companion-skills.md +15 -0
  11. package/plugins/task-pipeline/skills/task-pipeline/references/conventions.md +19 -0
  12. package/plugins/task-pipeline/skills/task-pipeline/references/documentation.md +285 -0
  13. package/plugins/task-pipeline/skills/task-pipeline/references/gates.md +236 -0
  14. package/plugins/task-pipeline/skills/task-pipeline/references/hooks.md +164 -0
  15. package/plugins/task-pipeline/skills/task-pipeline/references/knowledge-sources.md +26 -5
  16. package/plugins/task-pipeline/skills/task-pipeline/references/learned.md +17 -2
  17. package/plugins/task-pipeline/skills/task-pipeline/references/retrospective.md +55 -14
  18. package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +71 -9
  19. package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +14 -1
  20. package/plugins/task-pipeline/skills/task-pipeline/templates/adr.md +28 -3
  21. package/plugins/task-pipeline/skills/task-pipeline/templates/brief.md +26 -8
  22. package/plugins/task-pipeline/skills/task-pipeline/templates/decisions.md +50 -0
  23. package/plugins/task-pipeline/skills/task-pipeline/templates/docgate.sh +391 -0
  24. package/plugins/task-pipeline/skills/task-pipeline/templates/docmap.md +86 -0
  25. package/plugins/task-pipeline/skills/task-pipeline/templates/hooks.example.json +21 -0
  26. package/plugins/task-pipeline/skills/task-pipeline/templates/open-questions.md +21 -0
  27. package/plugins/task-pipeline/skills/task-pipeline/templates/retro-archive.md +41 -0
  28. package/plugins/task-pipeline/skills/task-pipeline/templates/retro.md +27 -13
@@ -0,0 +1,50 @@
1
+ # Decisions — <project>
2
+
3
+ **Append-only.** Every settled thing that shapes the product, the architecture, the
4
+ scope, security, data, pricing or process. Doctrine:
5
+ `references/documentation.md`.
6
+
7
+ **Next free ID:** `DEC-0002`
8
+
9
+ Reading *"Next free ID"* is **not** reserving it — a second agent reading it in the
10
+ same minute gets the same answer. Reserve it, then write.
11
+
12
+ ## Format
13
+
14
+ ```markdown
15
+ ### DEC-0007 — <one line, in the present tense>
16
+
17
+ - **Date:** 2026-08-03
18
+ - **Status:** Accepted
19
+ - **Context:** what forced the choice
20
+ - **Decision:** what was chosen, stated so it can be obeyed
21
+ - **Consequences / affects:** `docs/SECURITY.md`, `docs/DATA_MODEL.md`
22
+ - **Source:** run `2026-08-03-<topic>` · commit `a1b2c3d`
23
+ - **Supersedes:** DEC-0004
24
+ ```
25
+
26
+ | Field | Rule |
27
+ |---|---|
28
+ | `Status` | `Accepted` · `Superseded by DEC-####` · `Reversed` · `Accepted · **Partially superseded by DEC-####** — <one line>` · `Accepted · **Refined by DEC-####**` |
29
+ | `Consequences / affects` | every document that must change. **Each one must cite this id** — the gate checks it |
30
+ | `Source` | the run that produced it **and the commit**; the commit is what survives a rename |
31
+ | edge markers | `Refines:` additive, target needs no annotation · `Contradicts:` a named clause falls, target **must** be annotated · `Supersedes:` the whole target retires, target **must** be annotated |
32
+
33
+ **To change your mind:** add a new entry, edit **only the status line** of the old
34
+ one, leave its body intact. Never renumber. Never delete.
35
+
36
+ ---
37
+
38
+ ### DEC-0001 — Documentation is governed: registers, a doc map and a gate
39
+
40
+ - **Date:** <YYYY-MM-DD>
41
+ - **Status:** Accepted
42
+ - **Context:** this repository had no addressable home for settled things, so
43
+ decisions lived in chat and in per-run specs and were re-litigated every time
44
+ somebody new arrived.
45
+ - **Decision:** decisions live here with stable `DEC-####` ids, append-only; open
46
+ questions live in `docs/OPEN_QUESTIONS.md`; `docs/DOCMAP.md` holds the single
47
+ homes, the propagation matrix and the gate; `scripts/check-docs.sh` enforces the
48
+ mechanical half and runs before every commit.
49
+ - **Consequences / affects:** `docs/DOCMAP.md`, `docs/OPEN_QUESTIONS.md`
50
+ - **Source:** run `<topic>` · commit `<sha>`
@@ -0,0 +1,391 @@
1
+ #!/usr/bin/env bash
2
+ # check-docs.sh — the documentation gate for <project>.
3
+ #
4
+ # Seeded by task-pipeline (references/gates.md). IT IS YOURS NOW: extend it here,
5
+ # section by section. Each section is independent and removable.
6
+ #
7
+ # SCOPE: walks the markdown under docs/ (and this repository's own root .md files).
8
+ # It does NOT check: prose meaning, whether a citation is the RIGHT one, code,
9
+ # another repository's documents, or anything inside a fenced code block.
10
+ # Read this header before quoting a green from here as evidence.
11
+ #
12
+ # EXIT CODE IS THE OUTPUT: non-zero on any failure. Nothing may run after the
13
+ # VERDICT block at the bottom — a gate that appended a check after its verdict
14
+ # printed FAIL and returned 0, and CI was green over it for an unknown period.
15
+ #
16
+ # PORTABLE to macOS bash 3.2: no grep -P, no sed -i, no readarray, no mapfile.
17
+ #
18
+ # PROGRESSIVE ARMING: a section whose input does not exist yet prints
19
+ # "dormant: … — no <artefact> yet" and does NOT fail. Dormant is visible so it is
20
+ # not forgotten, and green so a freshly seeded project does not start red.
21
+
22
+ set -u
23
+
24
+ FAIL=0
25
+ DOCS_DIR=${DOCS_DIR:-docs}
26
+ DEC_FILE=${DEC_FILE:-$DOCS_DIR/DECISIONS.md}
27
+ OQ_FILE=${OQ_FILE:-$DOCS_DIR/OPEN_QUESTIONS.md}
28
+ MAP_FILE=${MAP_FILE:-$DOCS_DIR/DOCMAP.md}
29
+ RETRO_GLOB=${RETRO_GLOB:-$DOCS_DIR/superpowers}
30
+
31
+ # ---------- ratchets: a floor may only fall. Raising one is a decision. ----------
32
+ PROP_FLOOR=${PROP_FLOOR:-0} # allowed missing propagation citations
33
+ RESIDUE_FLOOR=${RESIDUE_FLOOR:-0} # allowed unmarked citations of retired ids
34
+
35
+ TMP=$(mktemp -d 2>/dev/null || mktemp -d -t docgate)
36
+ trap 'rm -rf "$TMP"' EXIT
37
+
38
+ err() { echo "ERR: $*"; FAIL=1; }
39
+ ok() { echo "ok: $*"; }
40
+ skipmsg() { echo "skip: $*"; }
41
+ dormant() { echo "dormant: $*"; }
42
+
43
+ # Strip fenced code blocks: sample content is not a claim about this repository.
44
+ # awk, because sed -i is not portable and this must run identically everywhere.
45
+ strip_fences() {
46
+ awk '
47
+ /^[ \t]*(```|~~~)/ { infence = !infence; print ""; next }
48
+ { if (infence) print ""; else print }
49
+ ' "$1"
50
+ }
51
+
52
+ # Every markdown file in scope, one per line.
53
+ find "$DOCS_DIR" -type f -name '*.md' 2>/dev/null | sort > "$TMP/files" || true
54
+ find . -maxdepth 1 -type f -name '*.md' 2>/dev/null | sort >> "$TMP/files" || true
55
+ FILE_COUNT=$(wc -l < "$TMP/files" | tr -d ' ')
56
+
57
+ if [ "$FILE_COUNT" = "0" ]; then
58
+ echo "FAIL: documentation gate — no markdown found under $DOCS_DIR"
59
+ exit 1
60
+ fi
61
+
62
+ # Fence-stripped copies, addressed by a flattened path.
63
+ while IFS= read -r f; do
64
+ [ -f "$f" ] || continue
65
+ flat=$(echo "$f" | tr '/' '_')
66
+ strip_fences "$f" > "$TMP/s_$flat"
67
+ done < "$TMP/files"
68
+
69
+ flat_of() { echo "$TMP/s_$(echo "$1" | tr '/' '_')"; }
70
+
71
+ # ---------- 1. relative links resolve ----------
72
+ _bad=0
73
+ while IFS= read -r f; do
74
+ [ -f "$f" ] || continue
75
+ dir=$(dirname "$f")
76
+ # grep -o on the link target; keep the line number for the message.
77
+ grep -n -o '](\([^) ]*\))' "$(flat_of "$f")" 2>/dev/null |
78
+ sed 's/](\(.*\))/\1/' |
79
+ while IFS=: read -r ln target; do
80
+ case "$target" in
81
+ http://*|https://*|mailto:*|'#'*|'') continue ;;
82
+ esac
83
+ base=${target%%#*}
84
+ [ -n "$base" ] || continue
85
+ if [ ! -e "$dir/$base" ]; then
86
+ echo "$f:$ln: dangling link -> $target" >> "$TMP/badlinks"
87
+ fi
88
+ done
89
+ done < "$TMP/files"
90
+ if [ -s "$TMP/badlinks" ] 2>/dev/null; then
91
+ _bad=$(wc -l < "$TMP/badlinks" | tr -d ' ')
92
+ err "$_bad dangling relative link(s):"; sed 's/^/ /' "$TMP/badlinks"
93
+ else
94
+ ok "relative links resolve ($FILE_COUNT files)"
95
+ fi
96
+
97
+ # ---------- 2. every id referenced is defined ----------
98
+ if [ ! -f "$DEC_FILE" ]; then
99
+ dormant "id integrity — no $DEC_FILE yet"
100
+ DECS=0; OQS=0
101
+ else
102
+ # Read the FENCE-STRIPPED copy: a heading inside a ``` block is sample content,
103
+ # not a definition. Reading the raw file here counts the format example as an
104
+ # entry, and then "Next free ID" is wrong against a decision that never existed.
105
+ grep -o '^### DEC-[0-9][0-9]*' "$(flat_of "$DEC_FILE")" | sed 's/^### //' | sort -u > "$TMP/dec_def"
106
+ DECS=$(wc -l < "$TMP/dec_def" | tr -d ' ')
107
+ : > "$TMP/oq_def"
108
+ [ -f "$OQ_FILE" ] && grep -o '^| *OQ-[0-9][0-9]*' "$(flat_of "$OQ_FILE")" | sed 's/^| *//' | sort -u > "$TMP/oq_def"
109
+ OQS=$(wc -l < "$TMP/oq_def" | tr -d ' ')
110
+ cat "$TMP/dec_def" "$TMP/oq_def" | sort -u > "$TMP/defined"
111
+
112
+ : > "$TMP/refs"
113
+ while IFS= read -r f; do
114
+ [ -f "$f" ] || continue
115
+ grep -v 'Next free ID' "$(flat_of "$f")" 2>/dev/null |
116
+ grep -o '\(DEC\|OQ\)-[0-9][0-9]*' | sed "s|^|$f |" >> "$TMP/refs"
117
+ done < "$TMP/files"
118
+
119
+ : > "$TMP/undef"
120
+ sort -u "$TMP/refs" | while read -r f id; do
121
+ grep -qx "$id" "$TMP/defined" || echo "$f: $id referenced, never defined" >> "$TMP/undef"
122
+ done
123
+ if [ -s "$TMP/undef" ] 2>/dev/null; then
124
+ err "undefined id(s):"; sed 's/^/ /' "$TMP/undef"
125
+ else
126
+ ok "every referenced id is defined ($DECS decisions, $OQS open questions)"
127
+ fi
128
+ fi
129
+
130
+ # ---------- 3. "Next free ID" == max defined + 1 ----------
131
+ check_next_free() {
132
+ _file=$1; _prefix=$2; _deffile=$3
133
+ [ -f "$_file" ] || { dormant "next-free-$_prefix — no $_file yet"; return; }
134
+ _claim=$(grep -o "Next free ID:\** *\`\?$_prefix-[0-9][0-9]*" "$_file" | head -1 |
135
+ grep -o '[0-9][0-9]*$')
136
+ if [ -z "${_claim:-}" ]; then
137
+ err "$_file: no parsable 'Next free ID: \`$_prefix-NNNN\`' line"
138
+ return
139
+ fi
140
+ # Strip leading zeros BEFORE any arithmetic: bash reads 0009 as octal, 9 is not
141
+ # an octal digit, the expansion errors, the `if` takes its else branch and the
142
+ # check prints ok. It passed for every id ending 0-7 and was silent for 8 and 9.
143
+ # Found by the probe; the check was wrong, not the probe.
144
+ _claim=$(echo "$_claim" | sed 's/^0*//'); [ -n "$_claim" ] || _claim=0
145
+ if [ ! -s "$_deffile" ]; then _max=0; else
146
+ _max=$(sed "s/^$_prefix-//" "$_deffile" | sed 's/^0*//' | sort -n | tail -1)
147
+ [ -n "${_max:-}" ] || _max=0
148
+ fi
149
+ _want=$((_max + 1))
150
+ if [ "$((_claim))" -ne "$_want" ]; then
151
+ err "$_file: 'Next free ID' claims $_prefix-$_claim, highest defined is $_max (expected $_want)"
152
+ else
153
+ ok "next free $_prefix id is correct ($_prefix-$_claim)"
154
+ fi
155
+ }
156
+ check_next_free "$DEC_FILE" DEC "$TMP/dec_def"
157
+ check_next_free "$OQ_FILE" OQ "$TMP/oq_def"
158
+
159
+ # ---------- 4. a stated register size equals the computed one ----------
160
+ # Compute, never restate: a number written in prose is a number that goes stale.
161
+ if [ -f "$DEC_FILE" ] && grep -q 'Register size:' "$DEC_FILE" 2>/dev/null; then
162
+ _stated=$(grep -o 'Register size:\** *\**[0-9][0-9]*' "$DEC_FILE" | head -1 | grep -o '[0-9][0-9]*')
163
+ if [ "${_stated:-x}" != "$DECS" ]; then
164
+ err "$DEC_FILE: states 'Register size: $_stated', computed $DECS"
165
+ else
166
+ ok "stated register size matches the computed one ($DECS)"
167
+ fi
168
+ else
169
+ dormant "register-size cross-check — no 'Register size:' line stated"
170
+ fi
171
+
172
+ # ---------- 5. consequences propagation (RATCHETED) ----------
173
+ # A document named in an entry's "Consequences / affects:" line must cite that
174
+ # entry. Writing down where a decision must propagate and then not propagating is
175
+ # the exact failure the loop exists to prevent.
176
+ if [ ! -f "$DEC_FILE" ]; then
177
+ dormant "propagation — no $DEC_FILE yet"
178
+ PROP_MISSING=0
179
+ else
180
+ : > "$TMP/prop"
181
+ _cur=""
182
+ while IFS= read -r line; do
183
+ case "$line" in
184
+ '### DEC-'*) _cur=$(echo "$line" | grep -o 'DEC-[0-9][0-9]*') ;;
185
+ *'Consequences / affects:'*)
186
+ [ -n "$_cur" ] || continue
187
+ echo "$line" | grep -o '`[^`]*`' | tr -d '`' | while IFS= read -r doc; do
188
+ case "$doc" in *.md) ;; *) continue ;; esac
189
+ [ -f "$doc" ] || { echo "$_cur $doc MISSINGFILE" >> "$TMP/prop"; continue; }
190
+ if grep -q "$_cur" "$doc"; then :; else echo "$_cur $doc NOCITE" >> "$TMP/prop"; fi
191
+ done ;;
192
+ esac
193
+ done < "$(flat_of "$DEC_FILE")"
194
+ PROP_MISSING=0
195
+ [ -f "$TMP/prop" ] && PROP_MISSING=$(wc -l < "$TMP/prop" | tr -d ' ')
196
+ : > "$TMP/prop_new"
197
+ if [ "$PROP_MISSING" -gt 0 ]; then
198
+ while read -r id doc why; do
199
+ _n=$(echo "$id" | sed 's/^DEC-//' | sed 's/^0*//'); [ -n "$_n" ] || _n=0
200
+ [ "$_n" -ge "$PROP_FLOOR" ] && echo "$id -> $doc ($why)" >> "$TMP/prop_new"
201
+ done < "$TMP/prop"
202
+ fi
203
+ if [ -s "$TMP/prop_new" ] 2>/dev/null; then
204
+ err "decision(s) naming a document that does not cite them (floor DEC-$PROP_FLOOR):"
205
+ sed 's/^/ /' "$TMP/prop_new"
206
+ else
207
+ ok "consequences propagate (backlog below the floor: $PROP_MISSING)"
208
+ fi
209
+ fi
210
+
211
+ # ---------- 6. supersede / contradict annotates the target ----------
212
+ # One word for "adds to" and "replaces a clause of" is unenforceable, so the
213
+ # markers are distinct and only two of them oblige the target to say so.
214
+ if [ ! -f "$DEC_FILE" ]; then
215
+ dormant "supersede annotations — no $DEC_FILE yet"
216
+ else
217
+ : > "$TMP/ann"
218
+ _cur=""
219
+ while IFS= read -r line; do
220
+ case "$line" in
221
+ '### DEC-'*) _cur=$(echo "$line" | grep -o 'DEC-[0-9][0-9]*') ;;
222
+ *'Supersedes:'*|*'Contradicts:'*)
223
+ [ -n "$_cur" ] || continue
224
+ echo "$line" | grep -o 'DEC-[0-9][0-9]*' | while IFS= read -r target; do
225
+ [ "$target" = "$_cur" ] && continue
226
+ _status=$(awk -v t="### $target " '
227
+ index($0, t) == 1 { found = 1; next }
228
+ found && /^### DEC-/ { exit }
229
+ found && /Status:/ { print; exit }
230
+ ' "$(flat_of "$DEC_FILE")")
231
+ case "$_status" in
232
+ *"$_cur"*) ;;
233
+ *) echo "$target: status line does not record that $_cur retires or contradicts it" >> "$TMP/ann" ;;
234
+ esac
235
+ done ;;
236
+ esac
237
+ done < "$(flat_of "$DEC_FILE")"
238
+ if [ -s "$TMP/ann" ] 2>/dev/null; then
239
+ err "unannotated supersede/contradict target(s):"; sed 's/^/ /' "$TMP/ann"
240
+ else
241
+ ok "supersede and contradict targets are annotated"
242
+ fi
243
+ fi
244
+
245
+ # ---------- 7. retired-decision residue (RATCHETED) ----------
246
+ # A document citing a WHOLLY RETIRED id must say so in the same breath. The unit
247
+ # is a line: one marker on it exempts every id on it. That is a real blind spot,
248
+ # measured and accepted — a tighter window produced mostly noise, and a gate that
249
+ # is mostly noise is a gate people switch off.
250
+ if [ ! -f "$DEC_FILE" ]; then
251
+ dormant "retired residue — no $DEC_FILE yet"
252
+ RESIDUE=0
253
+ else
254
+ : > "$TMP/retired"
255
+ _cur=""
256
+ while IFS= read -r line; do
257
+ case "$line" in
258
+ '### DEC-'*) _cur=$(echo "$line" | grep -o 'DEC-[0-9][0-9]*') ;;
259
+ *'Status:'*)
260
+ [ -n "$_cur" ] || continue
261
+ case "$line" in
262
+ *Superseded\ by*|*Reversed*) echo "$_cur" >> "$TMP/retired" ;;
263
+ esac
264
+ _cur="" ;;
265
+ esac
266
+ done < "$(flat_of "$DEC_FILE")"
267
+ RESIDUE=0
268
+ if [ -s "$TMP/retired" ] 2>/dev/null; then
269
+ : > "$TMP/res"
270
+ while IFS= read -r f; do
271
+ [ -f "$f" ] || continue
272
+ [ "$f" = "$DEC_FILE" ] && continue
273
+ while IFS= read -r rid; do
274
+ grep -n "$rid" "$(flat_of "$f")" 2>/dev/null | while IFS=: read -r ln text; do
275
+ case "$text" in
276
+ *supersed*|*Supersed*|*retired*|*Retired*|*reversed*|*Reversed*) ;;
277
+ *) echo "$f:$ln cites $rid (retired) without saying so" >> "$TMP/res" ;;
278
+ esac
279
+ done
280
+ done < "$TMP/retired"
281
+ done < "$TMP/files"
282
+ [ -f "$TMP/res" ] && RESIDUE=$(wc -l < "$TMP/res" | tr -d ' ')
283
+ if [ "$RESIDUE" -gt "$RESIDUE_FLOOR" ]; then
284
+ err "$RESIDUE unmarked citation(s) of retired decisions (floor $RESIDUE_FLOOR):"
285
+ sed 's/^/ /' "$TMP/res"
286
+ else
287
+ ok "no unmarked citation of a retired decision (residue $RESIDUE, floor $RESIDUE_FLOOR)"
288
+ fi
289
+ else
290
+ ok "no retired decisions yet"
291
+ fi
292
+ fi
293
+
294
+ # ---------- 8. status vocabularies are closed ----------
295
+ # An unrecognised status is worse than a missing one: it looks answered, and every
296
+ # check on that row skips in silence.
297
+ if [ -f "$DEC_FILE" ]; then
298
+ : > "$TMP/vocab"
299
+ grep -n 'Status:' "$(flat_of "$DEC_FILE")" 2>/dev/null | while IFS= read -r hit; do
300
+ case "$hit" in
301
+ *Accepted*|*Superseded\ by*|*Reversed*|*'`Status`'*) ;;
302
+ *) echo "$DEC_FILE:${hit%%:*}: unknown decision status -> ${hit#*Status:}" >> "$TMP/vocab" ;;
303
+ esac
304
+ done
305
+ if [ -s "$TMP/vocab" ] 2>/dev/null; then
306
+ err "decision status vocabulary:"; sed 's/^/ /' "$TMP/vocab"
307
+ else
308
+ ok "decision statuses are inside the closed vocabulary"
309
+ fi
310
+ else
311
+ dormant "decision status vocabulary — no $DEC_FILE yet"
312
+ fi
313
+
314
+ if [ -f "$OQ_FILE" ]; then
315
+ : > "$TMP/oqvocab"
316
+ grep -n '^| *OQ-[0-9]' "$(flat_of "$OQ_FILE")" 2>/dev/null | while IFS= read -r row; do
317
+ case "$row" in
318
+ *'| Open '*|*'| Open|'*|*Open\ \|*|*Resolved→DEC-*|*Dropped*) ;;
319
+ *) echo "$OQ_FILE:${row%%:*}: unknown question status" >> "$TMP/oqvocab" ;;
320
+ esac
321
+ done
322
+ if [ -s "$TMP/oqvocab" ] 2>/dev/null; then
323
+ err "open-question status vocabulary:"; sed 's/^/ /' "$TMP/oqvocab"
324
+ else
325
+ ok "open-question statuses are inside the closed vocabulary"
326
+ fi
327
+ else
328
+ dormant "open-question status vocabulary — no $OQ_FILE yet"
329
+ fi
330
+
331
+ # ---------- 9. every commit SHA named in the retro resolves ----------
332
+ # A file:line rots at the next edit; a SHA carries the diff, the message and the
333
+ # parent forever. A document may not send a reader to something absent.
334
+ if [ ! -d .git ]; then
335
+ skipmsg "commit-SHA resolution — not a git working tree"
336
+ elif [ ! -d "$RETRO_GLOB" ]; then
337
+ dormant "commit-SHA resolution — no $RETRO_GLOB yet"
338
+ else
339
+ : > "$TMP/sha"
340
+ find "$RETRO_GLOB" -type f -name '*.md' 2>/dev/null | sort | while IFS= read -r f; do
341
+ grep -n -o '`[0-9a-f][0-9a-f]*`' "$(flat_of "$f")" 2>/dev/null |
342
+ while IFS=: read -r ln tok; do
343
+ s=$(echo "$tok" | tr -d '`')
344
+ case ${#s} in 7|8|9|10|11|12|40) ;; *) continue ;; esac
345
+ git rev-parse --verify --quiet "$s^{commit}" >/dev/null 2>&1 ||
346
+ echo "$f:$ln: commit \`$s\` does not resolve" >> "$TMP/sha"
347
+ done
348
+ done
349
+ if [ -s "$TMP/sha" ] 2>/dev/null; then
350
+ err "unresolvable commit reference(s):"; sed 's/^/ /' "$TMP/sha"
351
+ else
352
+ ok "every commit reference in $RETRO_GLOB resolves"
353
+ fi
354
+ fi
355
+
356
+ # ---------- 10. the doc map and the registers agree, BOTH directions ----------
357
+ # The direction that feels redundant is the one that finds things: a register the
358
+ # map never names is a register nobody is told about.
359
+ if [ ! -f "$MAP_FILE" ]; then
360
+ dormant "doc-map coverage — no $MAP_FILE yet"
361
+ else
362
+ # Forward direction is scoped to the "## Registers" table: that table is a CLAIM
363
+ # about what exists. The SSOT table below it legitimately names documents a young
364
+ # project has not written yet, and failing on those would make the gate seed red.
365
+ : > "$TMP/map"
366
+ awk '/^## Registers/ { on = 1; next } on && /^## / { exit } on { print }' \
367
+ "$(flat_of "$MAP_FILE")" | grep -o '`[^`]*\.md`' | tr -d '`' | sort -u > "$TMP/map"
368
+ : > "$TMP/mapmiss"
369
+ while IFS= read -r doc; do
370
+ case "$doc" in *'<'*|*'>'*) continue ;; esac
371
+ [ -e "$doc" ] || echo "$MAP_FILE names $doc, which does not exist" >> "$TMP/mapmiss"
372
+ done < "$TMP/map"
373
+ for reg in "$DEC_FILE" "$OQ_FILE"; do
374
+ [ -f "$reg" ] || continue
375
+ grep -q "$(basename "$reg")" "$TMP/map" ||
376
+ echo "$reg exists but $MAP_FILE never names it" >> "$TMP/mapmiss"
377
+ done
378
+ if [ -s "$TMP/mapmiss" ] 2>/dev/null; then
379
+ err "doc map / register disagreement:"; sed 's/^/ /' "$TMP/mapmiss"
380
+ else
381
+ ok "doc map and registers agree in both directions"
382
+ fi
383
+ fi
384
+
385
+ # ---------- VERDICT — nothing may run after this block ----------
386
+ if [ "$FAIL" -ne 0 ]; then
387
+ echo "FAIL: documentation gate"
388
+ exit 1
389
+ fi
390
+ echo "OK: documentation gate — ${DECS:-0} decisions · ${OQS:-0} open questions · propagation backlog ${PROP_MISSING:-0} (floor $PROP_FLOOR) · retired residue ${RESIDUE:-0} (floor $RESIDUE_FLOOR)"
391
+ exit 0
@@ -0,0 +1,86 @@
1
+ # Doc map — <project>
2
+
3
+ **One per project, not per run.** The four questions of
4
+ `references/documentation.md`, answered for this repository: where settled things
5
+ live, what each fact's single home is, what a change obliges, and what proves it.
6
+
7
+ Seeded by task-pipeline at stage 0 **only when absent**. Extend it whenever a new
8
+ document class appears; never let it grow a second copy of something stated
9
+ elsewhere — where another file already says it, this one holds a **pointer line**,
10
+ not a copy. A doc map that duplicates `AGENTS.md` is the first violation of the
11
+ rule it publishes.
12
+
13
+ ## Regime
14
+
15
+ `governed` — established <YYYY-MM-DD> by run `<topic>`, recorded as `DEC-0001`.
16
+
17
+ Governed scales by **volume**, never by dropping rules: a register with three
18
+ entries is a register. Nothing here is authored twice — every entry is transcribed
19
+ from an artefact the run already produced (the brief's *Decisions locked*, the
20
+ spec's contracts, an ADR).
21
+
22
+ ## Registers
23
+
24
+ | Register | File | ID scheme | Append-only? | Guarded? |
25
+ |---|---|---|---|---|
26
+ | Decisions | `docs/DECISIONS.md` | `DEC-####` | yes | lease before write, where a mechanism exists |
27
+ | Open questions | `docs/OPEN_QUESTIONS.md` | `OQ-####` | yes (never delete a resolved row) | same |
28
+ | `<fill me>` | `<path>` | `<PREFIX-####>` | … | … |
29
+
30
+ > One decision home per project. If this repository already had `docs/adr/`, that
31
+ > is the register and the first row names it instead — never both.
32
+
33
+ ## Single source of truth
34
+
35
+ Every fact has exactly one home; everything else links to it by id. A fact stated
36
+ in two places is a bug: collapse it to one home and link from the other.
37
+
38
+ | Fact | Home | Everything else |
39
+ |---|---|---|
40
+ | A settled decision | `docs/DECISIONS.md` | cites the `DEC-####` |
41
+ | `<the domain glossary>` | `CONTEXT.md` | links to the term |
42
+ | `<user-facing behaviour>` | `docs/ux/scenarios.md` | links to the scenario id |
43
+ | `<fill me>` | `<path>` | … |
44
+
45
+ ## Propagation matrix
46
+
47
+ **The harvest ledger names what you read. This names what you owe.** A row's third
48
+ column names the check that notices when the row is not honoured — or the word
49
+ `review` **with a one-line reason why no check can decide it**. An empty third
50
+ column is a finding, not a blank.
51
+
52
+ | Change type | Update these | Checked by |
53
+ |---|---|---|
54
+ | New/changed **decision** | `docs/DECISIONS.md` + every doc in its `Consequences / affects:` line | gate §5 propagation |
55
+ | Question **resolved** | `docs/OPEN_QUESTIONS.md` → `Resolved→DEC-####`, the owning topic doc | gate §2 ids · gate §8 status vocabulary |
56
+ | **Scope** change | `<roadmap>`, `<mvp>`, the register | review — scope is a judgement, not a shape |
57
+ | `<new/changed entity or field>` | `<data model>` (canonical), `<glossary>` | `<fill me>` |
58
+ | `<user-facing behaviour>` | `<scenarios>` + `<flows>` + `<screens>`, same change | `<ux linter>` |
59
+ | `<fill me>` | … | … |
60
+
61
+ ## Gates
62
+
63
+ | Gate | Command | When | Blocking? |
64
+ |---|---|---|---|
65
+ | Documentation | `bash scripts/check-docs.sh` | before the commit | yes |
66
+ | `<fill me>` | … | … | … |
67
+
68
+ Each gate states in its own header what it does **not** cover. Read that before
69
+ quoting a green as evidence.
70
+
71
+ ## Ratchets
72
+
73
+ A named, counted set that may only shrink, printed beside the verdict on every run.
74
+ Raising a floor is a decision and belongs in the register.
75
+
76
+ | Ratchet | Floor variable | Current | Set on |
77
+ |---|---|---|---|
78
+ | Propagation backlog | `PROP_FLOOR` | 0 | <YYYY-MM-DD> |
79
+ | `<fill me>` | … | … | … |
80
+
81
+ ## Navigation
82
+
83
+ - One definition per entity, with an explicit anchor.
84
+ - A mention links to the **anchor**, not to the file.
85
+ - Indexes and summaries link; they never restate a rule. An index that falls behind
86
+ lies with authority — a reader concludes the entry does not exist.
@@ -0,0 +1,21 @@
1
+ {
2
+ "_note": "EXAMPLE — copy this block into the project's .claude/settings.json. Doctrine: references/hooks.md. Hooks exist ONLY in Claude Code; on any other agent the same rule runs as a self-check and the run is recorded 'ungated'. Never describe a project as protected when its agents run elsewhere.",
3
+ "_contract": "A PreToolUse hook blocks in one of two ways: exit 2 with the reason on stderr (stdout is ignored), or exit 0 with {\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"...\"}} on stdout. ANY OTHER EXIT CODE IS NON-BLOCKING, so a crashing guard fails open and stops guarding without announcing it. That is why the command below ends in '|| exit 2' — the gate's own 'exit 1' would otherwise land in the non-blocking branch and the commit would proceed.",
4
+ "hooks": {
5
+ "PreToolUse": [
6
+ {
7
+ "_note": "Refuse a commit while the documentation gate is red. Matched narrowly: the matcher is free, the script is not.",
8
+ "matcher": "Bash",
9
+ "if": "Bash(git commit *)",
10
+ "hooks": [
11
+ {
12
+ "type": "command",
13
+ "shell": "bash",
14
+ "timeout": 60,
15
+ "command": "bash scripts/check-docs.sh >&2 || exit 2"
16
+ }
17
+ ]
18
+ }
19
+ ]
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ # Open questions — <project>
2
+
3
+ Everything undecided, with an owner and what it blocks. This register was
4
+ established by DEC-0001. Doctrine: `references/documentation.md`. A question is
5
+ **never deleted** — the question is the history of the answer, and deleting it is
6
+ how the same thing gets re-argued a quarter later.
7
+
8
+ **Next free ID:** `OQ-0002`
9
+
10
+ Reserve the id before you write it; reading this line is not reserving it.
11
+
12
+ Status is a closed vocabulary: `Open` · `Resolved→DEC-####` · `Dropped (<why>)`.
13
+ Anything else reads as answered when it is not, and every check on that row skips
14
+ in silence.
15
+
16
+ | ID | Question | Owner | Blocks | Status |
17
+ |---|---|---|---|---|
18
+ | OQ-0001 | Does the propagation matrix cover `<the class this project keeps forgetting>`? | `<owner>` | the first change of that type | Open |
19
+
20
+ **When one resolves:** flip the status to `Resolved→DEC-####` in the **same
21
+ change** as the decision that answers it, and leave the row where it is.
@@ -0,0 +1,41 @@
1
+ # Retro archive — <project> · <YYYY>-Q<N>
2
+
3
+ **Append-only. Queried, never read in full.** The in-force list
4
+ (`docs/superpowers/retro.md`) is capped at ten and read whole at stage 0; this file
5
+ is where entries and retirements go when they age out, so pruning stops losing
6
+ things. Doctrine: `references/retrospective.md`.
7
+
8
+ Stage 0's harvest **queries** this file by the task's nouns — it is the source that
9
+ answers *"have we been bitten by this class before?"*. It is never read end to end;
10
+ that is what the cap on the in-force list is for.
11
+
12
+ **Every entry carries a commit.** A `file:line` rots at the next edit; a SHA carries
13
+ the diff, the message and the parent forever, and `git show <sha>` reconstructs the
14
+ whole incident two months later when the class comes back. Every SHA here must
15
+ resolve — the documentation gate checks it.
16
+
17
+ ## Entries
18
+
19
+ ### <YYYY-MM-DD> · `<topic>` · <one-line symptom>
20
+
21
+ - **Symptom:** … (with evidence — a command, a `file:line`, the gate that reopened)
22
+ - **Surfaced at:** stage <N> · **Owned by:** stage <M> (the stage that let it
23
+ through — usually an earlier one)
24
+ - **Root cause:** … ("the agent was careless" is not a cause; it is the absence of
25
+ one, and it produces no fix)
26
+ - **Fix:** grade 1 (mechanical) — … / grade 2 → `R-00N` / grade 3, expires in 2 runs
27
+ - **The check:** … (what catches this the first time, from now on)
28
+ - **Commit:** `<sha>` — the change that fixed it (a real short SHA, and it must
29
+ resolve: the gate runs `git rev-parse --verify` over every one)
30
+ - **Upstream?** … (a lesson true in any repository belongs in the skill's own
31
+ `references/learned.md` — open an issue and say so here)
32
+
33
+ ## Retirements
34
+
35
+ One line each. Silent deletion is forbidden: the record stays, the instruction
36
+ leaves. A retired rule that returns as a real failure is a grade-1 fix — **with its
37
+ history attached**, which is what this section is for.
38
+
39
+ | Date | id | Instruction | Trigger that retired it | Commit |
40
+ |---|---|---|---|---|
41
+ | <YYYY-MM-DD> | R-000 | … | became a check (`npm run lint:paths`) | `<sha>` |