@windyroad/architect 0.16.0 → 0.17.0-preview.692
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/.claude-plugin/plugin.json +1 -1
- package/hooks/architect-compendium-update-entry.sh +226 -0
- package/hooks/architect-readme-pairing-check.sh +93 -0
- package/hooks/hooks.json +2 -1
- package/hooks/test/architect-compendium-update-entry.bats +241 -0
- package/hooks/test/architect-readme-pairing-check.bats +90 -0
- package/package.json +1 -1
- package/scripts/generate-decisions-compendium.sh +16 -2
- package/scripts/test/generate-decisions-compendium.bats +9 -4
- package/hooks/architect-compendium-refresh-discipline.sh +0 -113
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# architect-compendium-update-entry.sh — PostToolUse:Edit|Write hook.
|
|
3
|
+
#
|
|
4
|
+
# ADR-078 Phase 1, Option 9 (architect-on-edit writes README entry directly).
|
|
5
|
+
# RFC-014 Story A. Closes P337 structurally: every edit to a per-ADR body
|
|
6
|
+
# triggers a same-hook re-authoring of that ADR's entry in the decisions
|
|
7
|
+
# compendium (docs/decisions/README.md), so body↔compendium drift becomes
|
|
8
|
+
# impossible by construction rather than detectable-and-fixable.
|
|
9
|
+
#
|
|
10
|
+
# Mechanism:
|
|
11
|
+
# 1. Fires on Edit/Write events whose file_path is docs/decisions/<NNN>-*.md
|
|
12
|
+
# (excludes README.md and any -history.md / -summary.md sibling).
|
|
13
|
+
# 2. Spawns a `claude -p` subprocess invoking wr-architect:agent with the
|
|
14
|
+
# just-edited ADR body + the current README entry for that ADR-ID (or an
|
|
15
|
+
# empty string when the ADR is new). The architect emits the updated
|
|
16
|
+
# compendium entry shape (### ADR-NNN header + Status/Oversight/Supersedes
|
|
17
|
+
# badges + **Decides:** + **Confirmation:** + **Related:**).
|
|
18
|
+
# 3. Captures the architect's emit from the subprocess JSON `.result` field;
|
|
19
|
+
# replaces the existing entry block for that ADR-ID in-place, or inserts a
|
|
20
|
+
# new one in numeric-sort order under the correct section (in-force for
|
|
21
|
+
# proposed/accepted; historical for superseded/rejected/deprecated).
|
|
22
|
+
# 4. Stages docs/decisions/README.md so it lands in the same commit as the
|
|
23
|
+
# ADR body change (paired by architect-readme-pairing-check.sh — Story B).
|
|
24
|
+
#
|
|
25
|
+
# Failure mode (ADR-078 Confirmation criterion l): if the subprocess fails
|
|
26
|
+
# (network, quota, model error) or emits nothing usable, the hook logs a
|
|
27
|
+
# warning to stderr and leaves README unchanged (degraded mode). It does NOT
|
|
28
|
+
# block the body edit (exit 0). The stale README is then caught by Story B's
|
|
29
|
+
# pre-commit pairing check on the next `git commit`, surfacing the failure for
|
|
30
|
+
# manual recovery via `wr-architect-generate-decisions-compendium`.
|
|
31
|
+
#
|
|
32
|
+
# Opt-out (ADR-078 Confirmation criterion k): set
|
|
33
|
+
# ARCHITECT_AUTO_UPDATE_COMPENDIUM=0 to suppress the hook entirely (for
|
|
34
|
+
# API-cost-sensitive adopter setups). The hook self-suppresses with a stderr
|
|
35
|
+
# message directing the user to the manual generator.
|
|
36
|
+
#
|
|
37
|
+
# The compendium is no longer generator-derived (ADR-077 criterion b/g/h
|
|
38
|
+
# retired by ADR-078); the generator script is kept as a one-release-cycle
|
|
39
|
+
# backstop only (RFC-014 Story C). ADR-031 authoritative-state is preserved:
|
|
40
|
+
# the per-ADR body remains the source of truth; this entry is a derived view.
|
|
41
|
+
|
|
42
|
+
set -uo pipefail
|
|
43
|
+
|
|
44
|
+
# --- Opt-out (criterion k) -------------------------------------------------
|
|
45
|
+
if [ "${ARCHITECT_AUTO_UPDATE_COMPENDIUM:-1}" = "0" ]; then
|
|
46
|
+
echo "architect-compendium-update-entry: ARCHITECT_AUTO_UPDATE_COMPENDIUM=0 — hook suppressed. Refresh the compendium manually with: wr-architect-generate-decisions-compendium && git add docs/decisions/README.md" >&2
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# PostToolUse input arrives on stdin as JSON.
|
|
51
|
+
input=$(cat)
|
|
52
|
+
|
|
53
|
+
tool_name=$(printf '%s' "$input" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
54
|
+
case "$tool_name" in
|
|
55
|
+
Edit|Write|MultiEdit) ;;
|
|
56
|
+
*) exit 0 ;;
|
|
57
|
+
esac
|
|
58
|
+
|
|
59
|
+
file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
60
|
+
[ -n "$file_path" ] || exit 0
|
|
61
|
+
|
|
62
|
+
# Path gate: only docs/decisions/<NNN>-*.md ADR bodies. Exclude README and the
|
|
63
|
+
# non-ADR sibling files the generator also excludes.
|
|
64
|
+
base=$(basename "$file_path")
|
|
65
|
+
case "$file_path" in
|
|
66
|
+
*/docs/decisions/*|docs/decisions/*) ;;
|
|
67
|
+
*) exit 0 ;;
|
|
68
|
+
esac
|
|
69
|
+
case "$base" in
|
|
70
|
+
README.md|*-history.md|*-summary.md) exit 0 ;;
|
|
71
|
+
esac
|
|
72
|
+
# Must be a numbered ADR file (NNN-...).
|
|
73
|
+
echo "$base" | grep -qE '^[0-9]+-' || exit 0
|
|
74
|
+
|
|
75
|
+
adr_id_padded=$(echo "$base" | grep -oE '^[0-9]+') # zero-padded form for display (ADR-049)
|
|
76
|
+
adr_id=$((10#$adr_id_padded)) # numeric form for sort comparison (49)
|
|
77
|
+
|
|
78
|
+
# Resolve repo root so the README path + git staging are stable regardless of
|
|
79
|
+
# the hook's runtime CWD (P191 project-root anchoring).
|
|
80
|
+
project_dir="${CLAUDE_PROJECT_DIR:-}"
|
|
81
|
+
if [ -z "$project_dir" ]; then
|
|
82
|
+
project_dir=$(git rev-parse --show-toplevel 2>/dev/null) || project_dir="$PWD"
|
|
83
|
+
fi
|
|
84
|
+
readme="$project_dir/docs/decisions/README.md"
|
|
85
|
+
|
|
86
|
+
# The edited ADR body must exist on disk (the Write/Edit already landed —
|
|
87
|
+
# PostToolUse fires after the tool succeeds).
|
|
88
|
+
if [ ! -f "$file_path" ]; then
|
|
89
|
+
# Try the project-root-relative path if file_path was relative.
|
|
90
|
+
if [ -f "$project_dir/$file_path" ]; then
|
|
91
|
+
file_path="$project_dir/$file_path"
|
|
92
|
+
else
|
|
93
|
+
echo "architect-compendium-update-entry: edited ADR body not found ($file_path) — leaving compendium unchanged" >&2
|
|
94
|
+
exit 0
|
|
95
|
+
fi
|
|
96
|
+
fi
|
|
97
|
+
[ -f "$readme" ] || {
|
|
98
|
+
echo "architect-compendium-update-entry: compendium not found ($readme) — leaving unchanged (run wr-architect-generate-decisions-compendium to bootstrap)" >&2
|
|
99
|
+
exit 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# --- Determine target section from the ADR's status ------------------------
|
|
103
|
+
adr_status=$(awk '
|
|
104
|
+
/^---$/ { fm = !fm; if (!fm) exit; next }
|
|
105
|
+
fm && /^status:/ {
|
|
106
|
+
sub(/^status: */, ""); gsub(/^["'"'"']|["'"'"']$/, "")
|
|
107
|
+
sub(/^ +/, ""); sub(/ +$/, ""); print; exit
|
|
108
|
+
}
|
|
109
|
+
' "$file_path")
|
|
110
|
+
case "$adr_status" in
|
|
111
|
+
superseded|rejected|deprecated) target_section="historical" ;;
|
|
112
|
+
*) target_section="inforce" ;;
|
|
113
|
+
esac
|
|
114
|
+
|
|
115
|
+
# --- Extract the current README entry block for this ADR-ID -----------------
|
|
116
|
+
# Block = the `### ADR-<id> —` line and following lines up to the next
|
|
117
|
+
# `### ` / `## ` / `---` / EOF. Empty when the ADR is new.
|
|
118
|
+
current_entry=$(awk -v id="$adr_id" '
|
|
119
|
+
function bid(l, s){ s=l; sub(/^### ADR-/,"",s); return s+0 }
|
|
120
|
+
{
|
|
121
|
+
if ($0 ~ /^### ADR-[0-9]+/) { cap = (bid($0)==id) ? 1 : 0; if (cap) { print; next } }
|
|
122
|
+
else if ($0 ~ /^### / || $0 ~ /^## / || $0 ~ /^---[[:space:]]*$/) { cap = 0 }
|
|
123
|
+
if (cap) print
|
|
124
|
+
}
|
|
125
|
+
' "$readme")
|
|
126
|
+
|
|
127
|
+
# --- Spawn the architect subprocess (claude -p) ----------------------------
|
|
128
|
+
adr_body=$(cat "$file_path")
|
|
129
|
+
prompt=$(cat <<PROMPT
|
|
130
|
+
You are the wr-architect compendium-entry author. Re-author the single
|
|
131
|
+
docs/decisions/README.md compendium entry for ADR-${adr_id_padded} from its
|
|
132
|
+
current body. Emit ONLY the entry block — no preamble, no code fence, no
|
|
133
|
+
trailing commentary. The entry shape is exactly:
|
|
134
|
+
|
|
135
|
+
### ADR-${adr_id_padded} — <title>
|
|
136
|
+
**Status:** <status> | **Oversight:** <human-oversight> [| **Supersedes:** <list>]
|
|
137
|
+
**Decides:** <one or two sentence semantic TL;DR of the Decision Outcome — what was decided and why, in plain prose>
|
|
138
|
+
**Confirmation:** <short "; "-joined digest of the Confirmation criteria>
|
|
139
|
+
**Related:** <deduped ADR-NNN list from the Related section and inline mentions>
|
|
140
|
+
|
|
141
|
+
Omit the Supersedes badge when the body has no supersedes. Omit any of the
|
|
142
|
+
Decides / Confirmation / Related lines only when the body genuinely has no such
|
|
143
|
+
content. Keep the whole entry compact (a few lines) — it is a routine-load
|
|
144
|
+
index surface, not the full body.
|
|
145
|
+
|
|
146
|
+
--- CURRENT COMPENDIUM ENTRY (may be empty if the ADR is new) ---
|
|
147
|
+
${current_entry}
|
|
148
|
+
|
|
149
|
+
--- ADR BODY ---
|
|
150
|
+
${adr_body}
|
|
151
|
+
PROMPT
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Capture the architect's emit. `claude -p --output-format json` returns a JSON
|
|
155
|
+
# envelope with a `.result` string. PATH-resolved `claude` (so bats fixtures can
|
|
156
|
+
# stub it with a fixed-response shim placed first on PATH — RFC-014 SQ-014-1).
|
|
157
|
+
subprocess_out=$(printf '%s' "$prompt" | claude -p --output-format json 2>/dev/null)
|
|
158
|
+
subprocess_rc=$?
|
|
159
|
+
|
|
160
|
+
new_entry=""
|
|
161
|
+
if [ "$subprocess_rc" -eq 0 ] && [ -n "$subprocess_out" ]; then
|
|
162
|
+
new_entry=$(printf '%s' "$subprocess_out" | jq -r '.result // empty' 2>/dev/null)
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
# Degraded mode (criterion l): no usable emit → warn + leave README unchanged,
|
|
166
|
+
# do NOT block the edit.
|
|
167
|
+
if [ -z "$new_entry" ] || ! printf '%s' "$new_entry" | grep -qE '^### ADR-[0-9]+'; then
|
|
168
|
+
echo "architect-compendium-update-entry: architect subprocess produced no usable entry for ADR-${adr_id} (degraded mode) — compendium left unchanged. The pre-commit pairing check will surface this; recover with wr-architect-generate-decisions-compendium && git add docs/decisions/README.md" >&2
|
|
169
|
+
exit 0
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# --- Apply the entry: delete any existing block, then insert sorted ---------
|
|
173
|
+
tmp_entry=$(mktemp -t architect-entry.XXXXXX)
|
|
174
|
+
tmp_readme=$(mktemp -t architect-readme.XXXXXX)
|
|
175
|
+
trap 'rm -f "$tmp_entry" "$tmp_readme"' EXIT
|
|
176
|
+
printf '%s\n' "$new_entry" > "$tmp_entry"
|
|
177
|
+
|
|
178
|
+
# Pass 1 — remove any existing block for this ADR-ID (and the single blank line
|
|
179
|
+
# that precedes it), collapsing blank runs so deletion leaves no double-gap.
|
|
180
|
+
awk -v id="$adr_id" '
|
|
181
|
+
function bid(l, s){ s=l; sub(/^### ADR-/,"",s); return s+0 }
|
|
182
|
+
{
|
|
183
|
+
if ($0 ~ /^### ADR-[0-9]+/) {
|
|
184
|
+
if (bid($0)==id) { skipping=1; pendingblank=0; next }
|
|
185
|
+
skipping=0
|
|
186
|
+
if (pendingblank) { print ""; pendingblank=0 }
|
|
187
|
+
print; next
|
|
188
|
+
}
|
|
189
|
+
if (skipping) {
|
|
190
|
+
if ($0 ~ /^### / || $0 ~ /^## / || $0 ~ /^---[[:space:]]*$/) { skipping=0 }
|
|
191
|
+
else next
|
|
192
|
+
}
|
|
193
|
+
if ($0 ~ /^[[:space:]]*$/) { pendingblank=1; next }
|
|
194
|
+
if (pendingblank) { print ""; pendingblank=0 }
|
|
195
|
+
print
|
|
196
|
+
}
|
|
197
|
+
END { if (pendingblank) print "" }
|
|
198
|
+
' "$readme" > "$tmp_readme"
|
|
199
|
+
|
|
200
|
+
# Pass 2 — insert the new block in numeric-sort order within the target section.
|
|
201
|
+
awk -v id="$adr_id" -v section="$target_section" -v entryfile="$tmp_entry" '
|
|
202
|
+
function bid(l, s){ s=l; sub(/^### ADR-/,"",s); return s+0 }
|
|
203
|
+
BEGIN {
|
|
204
|
+
while ((getline l < entryfile) > 0) entry = (entry=="" ? l : entry "\n" l)
|
|
205
|
+
insec=0; done=0
|
|
206
|
+
}
|
|
207
|
+
/^## In-force decisions/ { insec=(section=="inforce") }
|
|
208
|
+
/^## Historical decisions/ { insec=(section=="historical") }
|
|
209
|
+
{
|
|
210
|
+
if (!done && insec && $0 ~ /^### ADR-[0-9]+/ && bid($0) > id) {
|
|
211
|
+
print entry; print ""; done=1
|
|
212
|
+
}
|
|
213
|
+
else if (!done && insec && ($0 ~ /^---[[:space:]]*$/ || ($0 ~ /^## / && $0 !~ /In-force decisions|Historical decisions/))) {
|
|
214
|
+
print entry; print ""; done=1; insec=0
|
|
215
|
+
}
|
|
216
|
+
print
|
|
217
|
+
}
|
|
218
|
+
END { if (!done) { print ""; print entry } }
|
|
219
|
+
' "$tmp_readme" > "$readme"
|
|
220
|
+
|
|
221
|
+
# Stage the compendium so it lands in the same commit as the ADR body change.
|
|
222
|
+
( cd "$project_dir" && git add docs/decisions/README.md 2>/dev/null ) || \
|
|
223
|
+
echo "architect-compendium-update-entry: git add docs/decisions/README.md failed (not a git repo or staging error) — stage it manually before commit" >&2
|
|
224
|
+
|
|
225
|
+
echo "architect-compendium-update-entry: refreshed compendium entry for ADR-${adr_id} (${target_section})" >&2
|
|
226
|
+
exit 0
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# architect-readme-pairing-check.sh — PreToolUse:Bash hook.
|
|
3
|
+
#
|
|
4
|
+
# ADR-078 Phase 1, Option 9. RFC-014 Story B. The structural replacement for
|
|
5
|
+
# the retired architect-compendium-refresh-discipline.sh: instead of comparing
|
|
6
|
+
# the staged compendium against PROGRAMMATIC generator output (which no longer
|
|
7
|
+
# holds under architect-authored entries), this hook asserts a simpler, robust
|
|
8
|
+
# invariant — every commit that stages a `docs/decisions/<NNN>-*.md` ADR body
|
|
9
|
+
# change MUST also stage `docs/decisions/README.md`.
|
|
10
|
+
#
|
|
11
|
+
# Under Option 9 the PostToolUse architect-compendium-update-entry.sh hook
|
|
12
|
+
# (Story A) re-authors + stages the README entry on every ADR body edit, so a
|
|
13
|
+
# commit that touches a body but NOT the README means Story A did not run (or
|
|
14
|
+
# ran in degraded mode after a subprocess failure). Denying surfaces that for
|
|
15
|
+
# manual recovery: re-run the edit (re-triggers Story A) or regenerate via
|
|
16
|
+
# `wr-architect-generate-decisions-compendium && git add docs/decisions/README.md`.
|
|
17
|
+
#
|
|
18
|
+
# Replaces ADR-077 Confirmation criterion (g) (the generator-output drift gate /
|
|
19
|
+
# bats test 2145). See ADR-078 § "Drift safety under Option 9".
|
|
20
|
+
#
|
|
21
|
+
# Allow paths (exit 0 silently per ADR-045 Pattern 1):
|
|
22
|
+
# - tool_name != "Bash"
|
|
23
|
+
# - command's leading effective executable is not `git commit`
|
|
24
|
+
# - `RISK_BYPASS: architect-compendium-deferred` token present in command
|
|
25
|
+
# - BYPASS_COMPENDIUM_REFRESH_GATE=1 (batch/migration parity)
|
|
26
|
+
# - staged set has no `docs/decisions/<NNN>-*.md` ADR body change
|
|
27
|
+
# - staged set already includes `docs/decisions/README.md`
|
|
28
|
+
#
|
|
29
|
+
# Deny path (exit 2 with PreToolUse deny JSON on stderr):
|
|
30
|
+
# - ADR body staged but README not staged
|
|
31
|
+
|
|
32
|
+
set -uo pipefail
|
|
33
|
+
|
|
34
|
+
# PreToolUse input arrives on stdin as JSON.
|
|
35
|
+
input=$(cat)
|
|
36
|
+
|
|
37
|
+
tool_name=$(printf '%s' "$input" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
38
|
+
[ "$tool_name" = "Bash" ] || exit 0
|
|
39
|
+
|
|
40
|
+
command=$(printf '%s' "$input" | jq -r '.tool_input.command // ""' 2>/dev/null)
|
|
41
|
+
[ -n "$command" ] || exit 0
|
|
42
|
+
|
|
43
|
+
# Only fire on `git commit` invocations. Leading-executable check (P268
|
|
44
|
+
# pattern): a bare substring match would catch unrelated commands that mention
|
|
45
|
+
# "git commit" (grep/sed/cat). Strip leading whitespace + env assignments, then
|
|
46
|
+
# require the first effective tokens to be `git commit`.
|
|
47
|
+
echo "$command" | awk '
|
|
48
|
+
{
|
|
49
|
+
sub(/^[[:space:]]+/, "")
|
|
50
|
+
while ($0 ~ /^[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+/) {
|
|
51
|
+
sub(/^[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+/, "")
|
|
52
|
+
}
|
|
53
|
+
if ($0 ~ /^git[[:space:]]+commit([[:space:]]|$)/) exit 0
|
|
54
|
+
exit 1
|
|
55
|
+
}
|
|
56
|
+
' || exit 0
|
|
57
|
+
|
|
58
|
+
# Allow-list bypass token (parity with the retired refresh-discipline hook and
|
|
59
|
+
# ADR-014 commit-message bypass shape).
|
|
60
|
+
if echo "$command" | grep -qF 'RISK_BYPASS: architect-compendium-deferred'; then
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Env-var bypass for batch/migration cases.
|
|
65
|
+
if [ "${BYPASS_COMPENDIUM_REFRESH_GATE:-0}" = "1" ]; then
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Resolve repo root so the git plumbing is path-stable.
|
|
70
|
+
repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
|
|
71
|
+
cd "$repo_root" || exit 0
|
|
72
|
+
|
|
73
|
+
# Inspect the staged set. ADR bodies are docs/decisions/<NNN>-*.md (exclude the
|
|
74
|
+
# README itself and the -history / -summary siblings).
|
|
75
|
+
staged_adrs=$(git diff --cached --name-only 2>/dev/null \
|
|
76
|
+
| awk '/^docs\/decisions\/[0-9]+-.*\.md$/ { print }' \
|
|
77
|
+
| head -20)
|
|
78
|
+
[ -n "$staged_adrs" ] || exit 0
|
|
79
|
+
|
|
80
|
+
staged_compendium=$(git diff --cached --name-only 2>/dev/null \
|
|
81
|
+
| awk '/^docs\/decisions\/README\.md$/ { print }')
|
|
82
|
+
|
|
83
|
+
if [ -z "$staged_compendium" ]; then
|
|
84
|
+
first_adr=$(echo "$staged_adrs" | head -1)
|
|
85
|
+
cat >&2 <<EOF
|
|
86
|
+
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "architect-readme-pairing-check: '${first_adr}' is staged for commit but 'docs/decisions/README.md' is NOT. Under ADR-078 Option 9 every ADR body change must be paired with its compendium entry refresh (the architect-compendium-update-entry PostToolUse hook does this automatically — if README is unstaged the hook did not run or hit degraded mode). Recover: re-run the ADR edit to re-trigger the hook, OR run 'wr-architect-generate-decisions-compendium && git add docs/decisions/README.md'. Intentional follow-up split: append 'RISK_BYPASS: architect-compendium-deferred' to the commit message. Batch/migration: set BYPASS_COMPENDIUM_REFRESH_GATE=1."}}
|
|
87
|
+
EOF
|
|
88
|
+
exit 2
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Both staged — pairing satisfied. (No generator-output comparison: the README
|
|
92
|
+
# is architect-authored under Option 9, not generator-derived.)
|
|
93
|
+
exit 0
|
package/hooks/hooks.json
CHANGED
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-enforce-edit.sh" }] },
|
|
11
11
|
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-oversight-marker-discipline.sh" }] },
|
|
12
12
|
{ "matcher": "ExitPlanMode", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-plan-enforce.sh" }] },
|
|
13
|
-
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-
|
|
13
|
+
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-readme-pairing-check.sh" }] }
|
|
14
14
|
],
|
|
15
15
|
"PostToolUse": [
|
|
16
16
|
{ "matcher": "Agent", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-mark-reviewed.sh" }] },
|
|
17
17
|
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-refresh-hash.sh" }] },
|
|
18
|
+
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-compendium-update-entry.sh" }] },
|
|
18
19
|
{ "matcher": "Agent|Bash|Skill", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-slide-marker.sh" }] }
|
|
19
20
|
]
|
|
20
21
|
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Behavioural tests for architect-compendium-update-entry.sh (RFC-014 Story A,
|
|
4
|
+
# ADR-078 Phase 1 / Option 9). Exercises the hook against fixture compendium
|
|
5
|
+
# trees; asserts on its side-effects (README mutation, staging, exit code) and
|
|
6
|
+
# stderr signals — never on hook source content (feedback_behavioural_tests).
|
|
7
|
+
#
|
|
8
|
+
# The `claude -p` subprocess is stubbed with a PATH-priority fake-claude shim
|
|
9
|
+
# (RFC-014 SQ-014-1) that emits a fixed-shape `{"result": "<entry>"}` envelope
|
|
10
|
+
# for whichever ADR-ID appears in the prompt. Real-subprocess integration is
|
|
11
|
+
# out of scope for Phase 1.
|
|
12
|
+
|
|
13
|
+
setup() {
|
|
14
|
+
HOOK="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/architect-compendium-update-entry.sh"
|
|
15
|
+
PROJ="$(mktemp -d)"
|
|
16
|
+
mkdir -p "$PROJ/docs/decisions"
|
|
17
|
+
( cd "$PROJ" && git init -q && git config user.email t@e.x && git config user.name t )
|
|
18
|
+
|
|
19
|
+
# PATH-priority fake-claude shim: emits a {"result": "<entry>"} envelope with
|
|
20
|
+
# an entry block for the ADR-ID found in the prompt. Marker text STUBBED-<id>
|
|
21
|
+
# makes the emitted Decides line assertable.
|
|
22
|
+
SHIMDIR="$(mktemp -d)"
|
|
23
|
+
cat > "$SHIMDIR/claude" <<'SHIM'
|
|
24
|
+
#!/usr/bin/env bash
|
|
25
|
+
prompt=$(cat)
|
|
26
|
+
id=$(printf '%s' "$prompt" | grep -oE 'ADR-[0-9]+' | head -1 | sed 's/ADR-//')
|
|
27
|
+
entry="### ADR-${id} — Stub Title
|
|
28
|
+
**Status:** proposed | **Oversight:** confirmed
|
|
29
|
+
**Decides:** STUBBED-${id} decision body.
|
|
30
|
+
**Confirmation:** stub crit a; stub crit b
|
|
31
|
+
**Related:** ADR-001"
|
|
32
|
+
jq -cn --arg r "$entry" '{result:$r}'
|
|
33
|
+
SHIM
|
|
34
|
+
chmod +x "$SHIMDIR/claude"
|
|
35
|
+
|
|
36
|
+
# Failing shim variant: exits non-zero, emits nothing (degraded-mode tests).
|
|
37
|
+
FAILSHIMDIR="$(mktemp -d)"
|
|
38
|
+
cat > "$FAILSHIMDIR/claude" <<'SHIM'
|
|
39
|
+
#!/usr/bin/env bash
|
|
40
|
+
cat >/dev/null
|
|
41
|
+
exit 7
|
|
42
|
+
SHIM
|
|
43
|
+
chmod +x "$FAILSHIMDIR/claude"
|
|
44
|
+
|
|
45
|
+
ORIG_PATH="$PATH"
|
|
46
|
+
export PATH="$SHIMDIR:$PATH"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
teardown() {
|
|
50
|
+
export PATH="$ORIG_PATH"
|
|
51
|
+
rm -rf "$PROJ" "$SHIMDIR" "$FAILSHIMDIR"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Writes a minimal compendium README with one in-force section (ADRs 003, 049,
|
|
55
|
+
# 051) and one historical section (ADR 010). Stable fixture for placement tests.
|
|
56
|
+
mk_readme() {
|
|
57
|
+
cat > "$PROJ/docs/decisions/README.md" <<'EOF'
|
|
58
|
+
# Decisions Compendium
|
|
59
|
+
|
|
60
|
+
Intro prose.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## In-force decisions
|
|
65
|
+
|
|
66
|
+
_3 ADRs._
|
|
67
|
+
|
|
68
|
+
### ADR-003 — Three
|
|
69
|
+
**Status:** proposed | **Oversight:** confirmed
|
|
70
|
+
**Decides:** Decides three.
|
|
71
|
+
|
|
72
|
+
### ADR-049 — FortyNine
|
|
73
|
+
**Status:** accepted | **Oversight:** confirmed
|
|
74
|
+
**Decides:** Decides forty-nine.
|
|
75
|
+
|
|
76
|
+
### ADR-051 — FiftyOne
|
|
77
|
+
**Status:** proposed | **Oversight:** confirmed
|
|
78
|
+
**Decides:** Decides fifty-one.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Historical decisions
|
|
83
|
+
|
|
84
|
+
_1 ADR._
|
|
85
|
+
|
|
86
|
+
### ADR-010 — Ten
|
|
87
|
+
**Status:** superseded | **Oversight:** confirmed
|
|
88
|
+
**Decides:** Decides ten.
|
|
89
|
+
EOF
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# mk_adr <nnn> <status> <title>
|
|
93
|
+
mk_adr() {
|
|
94
|
+
local nnn="$1" status="$2" title="$3"
|
|
95
|
+
cat > "$PROJ/docs/decisions/${nnn}-slug.${status}.md" <<EOF
|
|
96
|
+
---
|
|
97
|
+
status: "$status"
|
|
98
|
+
date: 2026-06-09
|
|
99
|
+
human-oversight: confirmed
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
# $title
|
|
103
|
+
|
|
104
|
+
## Decision Outcome
|
|
105
|
+
|
|
106
|
+
Chosen option: **"$title impl"**, because reasons.
|
|
107
|
+
|
|
108
|
+
## Confirmation
|
|
109
|
+
|
|
110
|
+
- (a) first
|
|
111
|
+
- (b) second
|
|
112
|
+
|
|
113
|
+
## Related
|
|
114
|
+
|
|
115
|
+
- Relates to [ADR-001](001-foo.proposed.md)
|
|
116
|
+
EOF
|
|
117
|
+
echo "$PROJ/docs/decisions/${nnn}-slug.${status}.md"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
run_hook() {
|
|
121
|
+
local fp="$1"
|
|
122
|
+
echo "{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"$fp\"},\"session_id\":\"s1\"}" \
|
|
123
|
+
| CLAUDE_PROJECT_DIR="$PROJ" bash "$HOOK"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@test "fires on Edit to a numbered ADR body and refreshes its entry in-place (criterion 1+4)" {
|
|
127
|
+
mk_readme
|
|
128
|
+
fp=$(mk_adr "049" "accepted" "FortyNine")
|
|
129
|
+
run run_hook "$fp"
|
|
130
|
+
[ "$status" -eq 0 ]
|
|
131
|
+
# In-place replacement: the entry now carries the stubbed Decides marker...
|
|
132
|
+
grep -q 'STUBBED-049 decision body' "$PROJ/docs/decisions/README.md"
|
|
133
|
+
# ...and there is exactly ONE ADR-049 header (no duplicate).
|
|
134
|
+
[ "$(grep -c '^### ADR-049 ' "$PROJ/docs/decisions/README.md")" -eq 1 ]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@test "produces an observable stderr signal on every invocation (criterion 2)" {
|
|
138
|
+
mk_readme
|
|
139
|
+
fp=$(mk_adr "049" "accepted" "FortyNine")
|
|
140
|
+
run run_hook "$fp"
|
|
141
|
+
[[ "$output" == *"architect-compendium-update-entry"* ]]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@test "emits the expected entry shape with a Decides line (criterion 3)" {
|
|
145
|
+
mk_readme
|
|
146
|
+
fp=$(mk_adr "049" "accepted" "FortyNine")
|
|
147
|
+
run_hook "$fp"
|
|
148
|
+
grep -qE '^\*\*Decides:\*\* STUBBED-049' "$PROJ/docs/decisions/README.md"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@test "inserts a new ADR entry in numeric-sort order within in-force (criterion 5)" {
|
|
152
|
+
mk_readme
|
|
153
|
+
fp=$(mk_adr "050" "proposed" "Fifty")
|
|
154
|
+
run run_hook "$fp"
|
|
155
|
+
[ "$status" -eq 0 ]
|
|
156
|
+
# ADR-050 must appear between ADR-049 and ADR-051 in the in-force section.
|
|
157
|
+
line049=$(grep -n '^### ADR-049 ' "$PROJ/docs/decisions/README.md" | cut -d: -f1)
|
|
158
|
+
line050=$(grep -n '^### ADR-050 ' "$PROJ/docs/decisions/README.md" | cut -d: -f1)
|
|
159
|
+
line051=$(grep -n '^### ADR-051 ' "$PROJ/docs/decisions/README.md" | cut -d: -f1)
|
|
160
|
+
[ -n "$line050" ]
|
|
161
|
+
[ "$line049" -lt "$line050" ]
|
|
162
|
+
[ "$line050" -lt "$line051" ]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@test "routes a superseded ADR's entry into the historical section (criterion 6)" {
|
|
166
|
+
mk_readme
|
|
167
|
+
fp=$(mk_adr "012" "superseded" "Twelve")
|
|
168
|
+
run run_hook "$fp"
|
|
169
|
+
[ "$status" -eq 0 ]
|
|
170
|
+
# ADR-012 must land AFTER the Historical-decisions header, not in In-force.
|
|
171
|
+
hist=$(grep -n '^## Historical decisions' "$PROJ/docs/decisions/README.md" | cut -d: -f1)
|
|
172
|
+
line012=$(grep -n '^### ADR-012 ' "$PROJ/docs/decisions/README.md" | cut -d: -f1)
|
|
173
|
+
[ -n "$line012" ]
|
|
174
|
+
[ "$line012" -gt "$hist" ]
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@test "migrates an entry from in-force to historical when status flips (criterion 6)" {
|
|
178
|
+
mk_readme
|
|
179
|
+
# ADR-049 currently renders in the in-force fixture section; re-author it as
|
|
180
|
+
# superseded — the hook must remove the in-force block and place it historical.
|
|
181
|
+
fp=$(mk_adr "049" "superseded" "FortyNine")
|
|
182
|
+
run run_hook "$fp"
|
|
183
|
+
[ "$status" -eq 0 ]
|
|
184
|
+
# Exactly one ADR-049 entry, and it is now below the Historical header.
|
|
185
|
+
[ "$(grep -c '^### ADR-049 ' "$PROJ/docs/decisions/README.md")" -eq 1 ]
|
|
186
|
+
hist=$(grep -n '^## Historical decisions' "$PROJ/docs/decisions/README.md" | cut -d: -f1)
|
|
187
|
+
line049=$(grep -n '^### ADR-049 ' "$PROJ/docs/decisions/README.md" | cut -d: -f1)
|
|
188
|
+
[ "$line049" -gt "$hist" ]
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@test "stages docs/decisions/README.md after refresh (criterion: same-commit pairing)" {
|
|
192
|
+
mk_readme
|
|
193
|
+
( cd "$PROJ" && git add -A && git commit -q -m init )
|
|
194
|
+
fp=$(mk_adr "049" "accepted" "FortyNine")
|
|
195
|
+
run_hook "$fp"
|
|
196
|
+
# README must be in the staged set.
|
|
197
|
+
( cd "$PROJ" && git diff --cached --name-only ) | grep -q 'docs/decisions/README.md'
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@test "subprocess failure leaves README unchanged + does NOT block the edit (criterion 7)" {
|
|
201
|
+
mk_readme
|
|
202
|
+
before=$(cat "$PROJ/docs/decisions/README.md")
|
|
203
|
+
fp=$(mk_adr "049" "accepted" "FortyNine")
|
|
204
|
+
export PATH="$FAILSHIMDIR:$ORIG_PATH"
|
|
205
|
+
run run_hook "$fp"
|
|
206
|
+
[ "$status" -eq 0 ] # does not block the body edit
|
|
207
|
+
after=$(cat "$PROJ/docs/decisions/README.md")
|
|
208
|
+
[ "$before" = "$after" ] # README unchanged (degraded mode)
|
|
209
|
+
[[ "$output" == *"degraded mode"* ]]
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@test "opt-out ARCHITECT_AUTO_UPDATE_COMPENDIUM=0 self-suppresses (criterion 8)" {
|
|
213
|
+
mk_readme
|
|
214
|
+
before=$(cat "$PROJ/docs/decisions/README.md")
|
|
215
|
+
fp=$(mk_adr "049" "accepted" "FortyNine")
|
|
216
|
+
run bash -c "echo '{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"$fp\"}}' | ARCHITECT_AUTO_UPDATE_COMPENDIUM=0 CLAUDE_PROJECT_DIR='$PROJ' bash '$HOOK'"
|
|
217
|
+
[ "$status" -eq 0 ]
|
|
218
|
+
[ "$before" = "$(cat "$PROJ/docs/decisions/README.md")" ]
|
|
219
|
+
[[ "$output" == *"ARCHITECT_AUTO_UPDATE_COMPENDIUM=0"* ]]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@test "ignores README.md edits (no self-recursion)" {
|
|
223
|
+
mk_readme
|
|
224
|
+
before=$(cat "$PROJ/docs/decisions/README.md")
|
|
225
|
+
run run_hook "$PROJ/docs/decisions/README.md"
|
|
226
|
+
[ "$status" -eq 0 ]
|
|
227
|
+
[ "$before" = "$(cat "$PROJ/docs/decisions/README.md")" ]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@test "ignores non-decisions file edits" {
|
|
231
|
+
mk_readme
|
|
232
|
+
echo "x" > "$PROJ/other.ts"
|
|
233
|
+
run run_hook "$PROJ/other.ts"
|
|
234
|
+
[ "$status" -eq 0 ]
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@test "registered in hooks.json on PostToolUse Edit|Write (criterion 9)" {
|
|
238
|
+
HOOKS_JSON="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/hooks.json"
|
|
239
|
+
run jq -e '.hooks.PostToolUse[] | select(.matcher | test("Edit")) | .hooks[] | select(.command | test("architect-compendium-update-entry"))' "$HOOKS_JSON"
|
|
240
|
+
[ "$status" -eq 0 ]
|
|
241
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Behavioural tests for architect-readme-pairing-check.sh (RFC-014 Story B,
|
|
4
|
+
# ADR-078 Phase 1 / Option 9). Exercises the hook against a real staged git
|
|
5
|
+
# index; asserts on its PreToolUse allow/deny decision (exit code + deny JSON).
|
|
6
|
+
# Behavioural — no grep on hook source (feedback_behavioural_tests).
|
|
7
|
+
|
|
8
|
+
setup() {
|
|
9
|
+
HOOK="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/architect-readme-pairing-check.sh"
|
|
10
|
+
REPO="$(mktemp -d)"
|
|
11
|
+
cd "$REPO"
|
|
12
|
+
git init -q
|
|
13
|
+
git config user.email t@e.x
|
|
14
|
+
git config user.name t
|
|
15
|
+
mkdir -p docs/decisions
|
|
16
|
+
echo "# compendium" > docs/decisions/README.md
|
|
17
|
+
echo "# adr 049" > docs/decisions/049-x.proposed.md
|
|
18
|
+
git add -A && git commit -q -m init
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
teardown() {
|
|
22
|
+
cd /
|
|
23
|
+
rm -rf "$REPO"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Run the hook with a synthetic `git commit` Bash PreToolUse payload.
|
|
27
|
+
run_commit_hook() {
|
|
28
|
+
local cmd="${1:-git commit -m wip}"
|
|
29
|
+
echo "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"$cmd\"}}" | bash "$HOOK"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@test "denies commit when an ADR body is staged without README (criterion 1)" {
|
|
33
|
+
echo "# adr 049 edited" > docs/decisions/049-x.proposed.md
|
|
34
|
+
git add docs/decisions/049-x.proposed.md
|
|
35
|
+
run run_commit_hook
|
|
36
|
+
[ "$status" -eq 2 ]
|
|
37
|
+
[[ "$output" == *"deny"* ]]
|
|
38
|
+
[[ "$output" == *"049-x.proposed.md"* ]]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@test "permits commit when ADR body AND README are both staged (criterion 2)" {
|
|
42
|
+
echo "# adr 049 edited" > docs/decisions/049-x.proposed.md
|
|
43
|
+
echo "# compendium refreshed" > docs/decisions/README.md
|
|
44
|
+
git add docs/decisions/049-x.proposed.md docs/decisions/README.md
|
|
45
|
+
run run_commit_hook
|
|
46
|
+
[ "$status" -eq 0 ]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@test "permits commit when only README is staged (compendium-only edit) (criterion 3)" {
|
|
50
|
+
echo "# compendium refreshed" > docs/decisions/README.md
|
|
51
|
+
git add docs/decisions/README.md
|
|
52
|
+
run run_commit_hook
|
|
53
|
+
[ "$status" -eq 0 ]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@test "permits commit when no ADR-touching change is staged (criterion 4)" {
|
|
57
|
+
echo "x" > unrelated.txt
|
|
58
|
+
git add unrelated.txt
|
|
59
|
+
run run_commit_hook
|
|
60
|
+
[ "$status" -eq 0 ]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@test "deny message names the unpaired ADR file + recovery directive (criterion 5)" {
|
|
64
|
+
echo "# adr 049 edited" > docs/decisions/049-x.proposed.md
|
|
65
|
+
git add docs/decisions/049-x.proposed.md
|
|
66
|
+
run run_commit_hook
|
|
67
|
+
[ "$status" -eq 2 ]
|
|
68
|
+
[[ "$output" == *"049-x.proposed.md"* ]]
|
|
69
|
+
[[ "$output" == *"wr-architect-generate-decisions-compendium"* ]]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@test "allows non-commit Bash commands silently" {
|
|
73
|
+
echo "# adr 049 edited" > docs/decisions/049-x.proposed.md
|
|
74
|
+
git add docs/decisions/049-x.proposed.md
|
|
75
|
+
run run_commit_hook "git status"
|
|
76
|
+
[ "$status" -eq 0 ]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@test "RISK_BYPASS token permits an intentional split" {
|
|
80
|
+
echo "# adr 049 edited" > docs/decisions/049-x.proposed.md
|
|
81
|
+
git add docs/decisions/049-x.proposed.md
|
|
82
|
+
run run_commit_hook "git commit -m 'wip RISK_BYPASS: architect-compendium-deferred'"
|
|
83
|
+
[ "$status" -eq 0 ]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@test "registered in hooks.json as PreToolUse Bash (criterion 6)" {
|
|
87
|
+
HOOKS_JSON="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/hooks.json"
|
|
88
|
+
run jq -e '.hooks.PreToolUse[] | select(.matcher=="Bash") | .hooks[] | select(.command | test("architect-readme-pairing-check"))' "$HOOKS_JSON"
|
|
89
|
+
[ "$status" -eq 0 ]
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -34,11 +34,25 @@ set -uo pipefail
|
|
|
34
34
|
# truncation — both halves are needed to close P334.
|
|
35
35
|
export LC_ALL=C
|
|
36
36
|
|
|
37
|
+
# --- ADR-078 (Option 9) deprecation notice (Confirmation criterion j) -------
|
|
38
|
+
# ADR-078 retires this generator as the load-bearing primary path. The
|
|
39
|
+
# compendium is now architect-authored per-edit by the PostToolUse hook
|
|
40
|
+
# `architect-compendium-update-entry.sh` (RFC-014 Story A); body↔compendium
|
|
41
|
+
# pairing is enforced at commit by `architect-readme-pairing-check.sh`
|
|
42
|
+
# (Story B). This script remains a one-release-cycle backstop / bootstrap +
|
|
43
|
+
# offline-reproducibility tool only, to be removed after one
|
|
44
|
+
# @windyroad/architect minor-version cycle (RFC-014 Story C). The notice fires
|
|
45
|
+
# on every invocation per ADR-078 Confirmation criterion (j).
|
|
46
|
+
echo "generate-decisions-compendium: DEPRECATED per ADR-078 (Option 9) — the compendium is now architect-authored on every ADR edit (architect-compendium-update-entry.sh PostToolUse hook). This script is a backstop/bootstrap tool only and will be removed after one @windyroad/architect minor-version cycle (RFC-014 Story C)." >&2
|
|
47
|
+
|
|
37
48
|
# --- Flag parsing ----------------------------------------------------------
|
|
38
49
|
# `--check` (no write): generate to a temp file and diff against the on-disk
|
|
39
50
|
# compendium. Exit 0 if byte-identical, 1 if drift, 2 if directory missing.
|
|
40
|
-
#
|
|
41
|
-
# (
|
|
51
|
+
# Formerly used by the retired architect-compendium-refresh-discipline.sh
|
|
52
|
+
# enforcement hook (Story D, ADR-078) to verify the staged compendium matched
|
|
53
|
+
# the working-tree ADRs. Under Option 9 the compendium is architect-authored,
|
|
54
|
+
# so --check no longer has a live caller; it is retained for the backstop /
|
|
55
|
+
# offline-reproducibility window only.
|
|
42
56
|
CHECK_MODE=0
|
|
43
57
|
case "${1:-}" in
|
|
44
58
|
--check)
|
|
@@ -53,10 +53,15 @@ mk_adr() {
|
|
|
53
53
|
# --- ADR-077 (g) drift-detection contract on the live committed state -------
|
|
54
54
|
|
|
55
55
|
@test "committed compendium matches generator output (CI drift gate)" {
|
|
56
|
-
#
|
|
57
|
-
# docs/decisions/README.md
|
|
58
|
-
#
|
|
59
|
-
#
|
|
56
|
+
# RETIRED per ADR-078 (Option 9) / RFC-014 Story C (test 2145). Under
|
|
57
|
+
# architect-on-edit authoring the committed docs/decisions/README.md is
|
|
58
|
+
# LLM-authored and intentionally no longer byte-matches programmatic
|
|
59
|
+
# generator output, so this idempotency/drift assertion no longer holds.
|
|
60
|
+
# Replacement enforcement: the architect-readme-pairing-check.sh pre-commit
|
|
61
|
+
# hook (Story B) asserts body↔README pairing at commit time. Removed entirely
|
|
62
|
+
# with the generator script after the backstop window (ADR-078 reassessment
|
|
63
|
+
# 2026-08-30).
|
|
64
|
+
skip "test 2145 retired per ADR-078 Option 9 — compendium is architect-authored, not generator-derived (RFC-014 Story C; pairing enforced by architect-readme-pairing-check.sh)"
|
|
60
65
|
cd "$REPO_ROOT"
|
|
61
66
|
run bash "$SCRIPT" --check docs/decisions
|
|
62
67
|
[ "$status" -eq 0 ]
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# architect-compendium-refresh-discipline.sh — PreToolUse:Bash hook
|
|
3
|
-
# (safety net per ADR-077). Denies `git commit` invocations whose staged
|
|
4
|
-
# set includes a `docs/decisions/<NNN>-*.md` ADR change but does NOT also
|
|
5
|
-
# stage a refreshed `docs/decisions/README.md` compendium that matches
|
|
6
|
-
# the current ADR bodies.
|
|
7
|
-
#
|
|
8
|
-
# Per ADR-077: the architect agent and the architect skills
|
|
9
|
-
# (`/wr-architect:create-adr`, `/wr-architect:capture-adr`,
|
|
10
|
-
# `/wr-architect:review-decisions`) are the PRIMARY mechanism for keeping
|
|
11
|
-
# the compendium fresh — they invoke `wr-architect-generate-decisions-compendium`
|
|
12
|
-
# at the right point in their flows. This hook is the SAFETY NET: it
|
|
13
|
-
# catches edits that bypass those flows (hand-edits via Edit/Write tools,
|
|
14
|
-
# off-skill bulk renames, direct file modifications). Mirrors the
|
|
15
|
-
# P165 `itil-readme-refresh-discipline.sh` pattern at the decisions surface.
|
|
16
|
-
#
|
|
17
|
-
# Allow paths (exit 0 silently per ADR-045 Pattern 1):
|
|
18
|
-
# - tool_name != "Bash"
|
|
19
|
-
# - command's leading effective executable is not `git commit`
|
|
20
|
-
# - `RISK_BYPASS: architect-compendium-deferred` token present in command
|
|
21
|
-
# (intentional follow-up refresh; same allow-list shape as the P165 +
|
|
22
|
-
# ADR-014 commit-message bypass-token pattern)
|
|
23
|
-
# - staged set has no `docs/decisions/<NNN>-*.md` ADR change
|
|
24
|
-
#
|
|
25
|
-
# Deny paths (exit 2 with PreToolUse deny JSON on stderr):
|
|
26
|
-
# - ADR staged but compendium not staged
|
|
27
|
-
# - both staged but staged compendium does not match generator output
|
|
28
|
-
# against current working-tree ADR bodies
|
|
29
|
-
#
|
|
30
|
-
# Recovery is mechanical per ADR-013 Rule 1:
|
|
31
|
-
# wr-architect-generate-decisions-compendium && git add docs/decisions/README.md
|
|
32
|
-
#
|
|
33
|
-
# Override (legitimate intentional split):
|
|
34
|
-
# append "RISK_BYPASS: architect-compendium-deferred" to the commit message
|
|
35
|
-
#
|
|
36
|
-
# Cross-ref: ADR-077 Confirmation item (h). See also packages/itil/hooks/itil-readme-refresh-discipline.sh
|
|
37
|
-
# for the P165 sibling pattern.
|
|
38
|
-
|
|
39
|
-
set -uo pipefail
|
|
40
|
-
|
|
41
|
-
# PreToolUse input arrives on stdin as JSON.
|
|
42
|
-
input=$(cat)
|
|
43
|
-
|
|
44
|
-
# Tool gate: only Bash.
|
|
45
|
-
tool_name=$(printf '%s' "$input" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
46
|
-
[ "$tool_name" = "Bash" ] || exit 0
|
|
47
|
-
|
|
48
|
-
# Extract the command.
|
|
49
|
-
command=$(printf '%s' "$input" | jq -r '.tool_input.command // ""' 2>/dev/null)
|
|
50
|
-
[ -n "$command" ] || exit 0
|
|
51
|
-
|
|
52
|
-
# Only fire on `git commit` invocations. Leading-executable check (P268
|
|
53
|
-
# pattern): substring "git commit" anywhere can match unrelated commands
|
|
54
|
-
# (e.g. grep / sed / cat with that literal). We check the first effective
|
|
55
|
-
# token sequence: optional env-var assignments + optional `git`-aliasing
|
|
56
|
-
# wrappers (none in this codebase) + the literal `git commit`.
|
|
57
|
-
echo "$command" | awk '
|
|
58
|
-
{
|
|
59
|
-
# Strip leading whitespace and env assignments (FOO=bar).
|
|
60
|
-
sub(/^[[:space:]]+/, "")
|
|
61
|
-
while ($0 ~ /^[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+/) {
|
|
62
|
-
sub(/^[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+/, "")
|
|
63
|
-
}
|
|
64
|
-
# Match git followed by commit.
|
|
65
|
-
if ($0 ~ /^git[[:space:]]+commit\b/) exit 0
|
|
66
|
-
exit 1
|
|
67
|
-
}
|
|
68
|
-
' || exit 0
|
|
69
|
-
|
|
70
|
-
# Allow-list bypass token. Same shape as P165 and ADR-014.
|
|
71
|
-
if echo "$command" | grep -qF 'RISK_BYPASS: architect-compendium-deferred'; then
|
|
72
|
-
exit 0
|
|
73
|
-
fi
|
|
74
|
-
|
|
75
|
-
# Env-var bypass for batch/migration cases (parity with BYPASS_README_REFRESH_GATE).
|
|
76
|
-
if [ "${BYPASS_COMPENDIUM_REFRESH_GATE:-0}" = "1" ]; then
|
|
77
|
-
exit 0
|
|
78
|
-
fi
|
|
79
|
-
|
|
80
|
-
# Resolve repo root so subsequent git plumbing is path-stable.
|
|
81
|
-
repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
|
|
82
|
-
cd "$repo_root" || exit 0
|
|
83
|
-
|
|
84
|
-
# Inspect the staged set.
|
|
85
|
-
staged_adrs=$(git diff --cached --name-only 2>/dev/null \
|
|
86
|
-
| awk '/^docs\/decisions\/[0-9]+-.*\.md$/ { print }' \
|
|
87
|
-
| head -20)
|
|
88
|
-
[ -n "$staged_adrs" ] || exit 0
|
|
89
|
-
|
|
90
|
-
staged_compendium=$(git diff --cached --name-only 2>/dev/null \
|
|
91
|
-
| awk '/^docs\/decisions\/README\.md$/ { print }')
|
|
92
|
-
|
|
93
|
-
if [ -z "$staged_compendium" ]; then
|
|
94
|
-
# ADR staged but compendium not staged. Deny.
|
|
95
|
-
first_adr=$(echo "$staged_adrs" | head -1)
|
|
96
|
-
cat >&2 <<EOF
|
|
97
|
-
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "architect-compendium-refresh-discipline: '${first_adr}' is staged for commit but 'docs/decisions/README.md' is NOT. The compendium is the architect agent's routine load surface (ADR-077). Run: wr-architect-generate-decisions-compendium && git add docs/decisions/README.md. Intentional follow-up split: append 'RISK_BYPASS: architect-compendium-deferred' to the commit message. Batch/migration: set BYPASS_COMPENDIUM_REFRESH_GATE=1."}}
|
|
98
|
-
EOF
|
|
99
|
-
exit 2
|
|
100
|
-
fi
|
|
101
|
-
|
|
102
|
-
# Both staged. Verify the staged compendium matches generator output for the
|
|
103
|
-
# current ADR bodies (working tree). The --check mode generates to temp, no
|
|
104
|
-
# mutation. Exit 0 => match; exit 1 => stale.
|
|
105
|
-
if ! wr-architect-generate-decisions-compendium --check >/dev/null 2>&1; then
|
|
106
|
-
cat >&2 <<EOF
|
|
107
|
-
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "architect-compendium-refresh-discipline: 'docs/decisions/README.md' is staged but does NOT match the current ADR bodies (stale compendium). Run: wr-architect-generate-decisions-compendium && git add docs/decisions/README.md to refresh, then re-commit. Intentional follow-up split: append 'RISK_BYPASS: architect-compendium-deferred' to the commit message."}}
|
|
108
|
-
EOF
|
|
109
|
-
exit 2
|
|
110
|
-
fi
|
|
111
|
-
|
|
112
|
-
# All clear.
|
|
113
|
-
exit 0
|