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.
- package/CHANGELOG.md +137 -0
- package/README.md +68 -8
- package/cursor/rules/task-pipeline.mdc +82 -0
- package/package.json +5 -2
- package/plugins/task-pipeline/.claude-plugin/plugin.json +4 -2
- package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +41 -10
- package/plugins/task-pipeline/skills/task-pipeline/pipeline.example.json +6 -3
- package/plugins/task-pipeline/skills/task-pipeline/references/artifacts.md +10 -2
- package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +5 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/companion-skills.md +15 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/conventions.md +19 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/documentation.md +285 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/gates.md +236 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/hooks.md +164 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/knowledge-sources.md +26 -5
- package/plugins/task-pipeline/skills/task-pipeline/references/learned.md +17 -2
- package/plugins/task-pipeline/skills/task-pipeline/references/retrospective.md +55 -14
- package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +71 -9
- package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +14 -1
- package/plugins/task-pipeline/skills/task-pipeline/templates/adr.md +28 -3
- package/plugins/task-pipeline/skills/task-pipeline/templates/brief.md +26 -8
- package/plugins/task-pipeline/skills/task-pipeline/templates/decisions.md +50 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/docgate.sh +391 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/docmap.md +86 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/hooks.example.json +21 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/open-questions.md +21 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/retro-archive.md +41 -0
- 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>` |
|