@windyroad/architect 0.15.1 → 0.15.2-preview.581
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-mark-reviewed.sh +19 -16
- package/hooks/architect-refresh-hash.sh +15 -5
- package/hooks/lib/architect-gate.sh +6 -2
- package/hooks/lib/gate-helpers.sh +105 -0
- package/hooks/test/substance-aware-drift.bats +243 -0
- package/package.json +1 -1
- package/skills/create-adr/SKILL.md +4 -0
- package/skills/review-decisions/SKILL.md +2 -0
|
@@ -41,29 +41,32 @@ case "$SUBAGENT" in
|
|
|
41
41
|
VERDICT="FAIL"
|
|
42
42
|
fi
|
|
43
43
|
|
|
44
|
+
# Substance-aware drift hash + atomic verdict-write (ADR-009 amendment
|
|
45
|
+
# 2026-06-06). The marker + hash file are written as an atomic pair via
|
|
46
|
+
# `_atomic_mark_with_hash` so a PASS never silently fails to persist
|
|
47
|
+
# (closes the "marker doesn't land after PASS" failure mode P353
|
|
48
|
+
# measured as ~12 subagent invocations + 3 BYPASS_RISK_GATE=1 uses
|
|
49
|
+
# per 3-filing session).
|
|
50
|
+
MARKER="/tmp/architect-reviewed-${SESSION_ID}"
|
|
44
51
|
case "$VERDICT" in
|
|
45
|
-
PASS)
|
|
46
|
-
|
|
52
|
+
PASS|"")
|
|
53
|
+
# PASS or unparseable verdict — allow with marker (the empty case
|
|
54
|
+
# preserves the pre-amendment "could not parse verdict" backward-
|
|
55
|
+
# compat allow-with-marker behaviour to avoid lockout).
|
|
56
|
+
if [ -d "$PROJECT_DIR/docs/decisions" ]; then
|
|
57
|
+
HASH=$(_substance_hash_path "$PROJECT_DIR/docs/decisions")
|
|
58
|
+
else
|
|
59
|
+
HASH="none"
|
|
60
|
+
fi
|
|
61
|
+
if ! _atomic_mark_with_hash "$MARKER" "$HASH"; then
|
|
62
|
+
echo "WARN: architect-mark-reviewed atomic marker-write failed for ${MARKER}" >&2
|
|
63
|
+
fi
|
|
47
64
|
;;
|
|
48
65
|
FAIL)
|
|
49
66
|
# Do NOT create marker — review found issues
|
|
50
67
|
;;
|
|
51
|
-
*)
|
|
52
|
-
# Could not parse verdict — allow with marker to avoid lockout
|
|
53
|
-
touch "/tmp/architect-reviewed-${SESSION_ID}"
|
|
54
|
-
;;
|
|
55
68
|
esac
|
|
56
69
|
|
|
57
|
-
# Store decision hash for drift detection
|
|
58
|
-
if [ -f "/tmp/architect-reviewed-${SESSION_ID}" ]; then
|
|
59
|
-
if [ -d "$PROJECT_DIR/docs/decisions" ]; then
|
|
60
|
-
HASH=$(find "$PROJECT_DIR/docs/decisions" -name '*.md' -not -name 'README.md' -print0 | sort -z | xargs -0 cat 2>/dev/null | _hashcmd | cut -d' ' -f1)
|
|
61
|
-
else
|
|
62
|
-
HASH="none"
|
|
63
|
-
fi
|
|
64
|
-
echo "$HASH" > "/tmp/architect-reviewed-${SESSION_ID}.hash"
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
70
|
# Plan review marker
|
|
68
71
|
touch "/tmp/architect-plan-reviewed-${SESSION_ID}"
|
|
69
72
|
;;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
# This prevents drift detection from invalidating the marker when creating
|
|
5
5
|
# new decision files that the architect just approved.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
source "$SCRIPT_DIR/lib/gate-helpers.sh"
|
|
9
9
|
|
|
10
10
|
# P191 Phase 2: anchor docs/decisions on the project root, not the hook's
|
|
11
11
|
# runtime CWD (see architect-enforce-edit.sh). The refreshed hash must match
|
|
@@ -33,14 +33,24 @@ esac
|
|
|
33
33
|
MARKER="/tmp/architect-reviewed-${SESSION_ID}"
|
|
34
34
|
HASH_FILE="/tmp/architect-reviewed-${SESSION_ID}.hash"
|
|
35
35
|
|
|
36
|
-
# Only refresh if a valid marker exists
|
|
36
|
+
# Only refresh if a valid marker exists. Uses substance-aware hash
|
|
37
|
+
# (ADR-009 amendment 2026-06-06) and atomic-rename write (mktemp + mv).
|
|
37
38
|
if [ -f "$MARKER" ] && [ -f "$HASH_FILE" ]; then
|
|
38
39
|
if [ -d "$PROJECT_DIR/docs/decisions" ]; then
|
|
39
|
-
HASH=$(
|
|
40
|
+
HASH=$(_substance_hash_path "$PROJECT_DIR/docs/decisions")
|
|
40
41
|
else
|
|
41
42
|
HASH="none"
|
|
42
43
|
fi
|
|
43
|
-
|
|
44
|
+
htmp="${HASH_FILE}.tmp.$$.${RANDOM:-0}"
|
|
45
|
+
if printf '%s\n' "$HASH" > "$htmp" 2>/dev/null; then
|
|
46
|
+
if ! mv -f "$htmp" "$HASH_FILE" 2>/dev/null; then
|
|
47
|
+
rm -f "$htmp"
|
|
48
|
+
echo "WARN: architect-refresh-hash atomic rename failed for ${HASH_FILE}" >&2
|
|
49
|
+
fi
|
|
50
|
+
else
|
|
51
|
+
rm -f "$htmp"
|
|
52
|
+
echo "WARN: architect-refresh-hash tempfile write failed for ${HASH_FILE}" >&2
|
|
53
|
+
fi
|
|
44
54
|
fi
|
|
45
55
|
|
|
46
56
|
exit 0
|
|
@@ -22,13 +22,17 @@ check_architect_gate() {
|
|
|
22
22
|
local MARKER_TIME=$(_mtime "$MARKER")
|
|
23
23
|
local AGE=$(( NOW - MARKER_TIME ))
|
|
24
24
|
if [ "$AGE" -lt "$TTL_SECONDS" ]; then
|
|
25
|
-
# TTL still valid -- check for decision drift
|
|
25
|
+
# TTL still valid -- check for decision drift via substance-aware hash
|
|
26
|
+
# (ADR-009 amendment 2026-06-06: trivial whitespace / line-ending /
|
|
27
|
+
# trailing-newline edits do NOT trigger drift; substantive policy
|
|
28
|
+
# changes DO. Conservative boundary — ambiguous edits stay
|
|
29
|
+
# substantive. See gate-helpers.sh::_substance_hash_path).
|
|
26
30
|
local HASH_FILE="/tmp/architect-reviewed-${SESSION_ID}.hash"
|
|
27
31
|
if [ -f "$HASH_FILE" ]; then
|
|
28
32
|
local STORED=$(cat "$HASH_FILE")
|
|
29
33
|
local CURRENT
|
|
30
34
|
if [ -d "$PROJECT_DIR/docs/decisions" ]; then
|
|
31
|
-
CURRENT=$(
|
|
35
|
+
CURRENT=$(_substance_hash_path "$PROJECT_DIR/docs/decisions")
|
|
32
36
|
else
|
|
33
37
|
CURRENT="none"
|
|
34
38
|
fi
|
|
@@ -13,6 +13,111 @@ _mtime() { stat -c%Y "$1" 2>/dev/null || /usr/bin/stat -f%m "$1" 2>/dev/null ||
|
|
|
13
13
|
# Portable hash: tries md5sum, falls back to md5 -r, then shasum
|
|
14
14
|
_hashcmd() { md5sum 2>/dev/null || md5 -r 2>/dev/null || shasum 2>/dev/null; }
|
|
15
15
|
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Substance-aware drift hash + atomic verdict-write (ADR-009 amendment
|
|
18
|
+
# 2026-06-06, P353 + P303 close).
|
|
19
|
+
#
|
|
20
|
+
# `_substance_hash_path` normalises trivial/no-op edits BEFORE hashing so a
|
|
21
|
+
# PASS marker survives whitespace / CRLF / trailing-newline edits while still
|
|
22
|
+
# detecting substantive policy changes. Conservative boundary: when in doubt
|
|
23
|
+
# whether an edit is trivial vs substantive, this helper treats it as
|
|
24
|
+
# substantive (re-review fires). Only whitespace + line-ending + trailing-
|
|
25
|
+
# newline are normalised in this iteration — single-numeral edits and
|
|
26
|
+
# frontmatter-key changes are intentionally NOT normalised. See ADR-009
|
|
27
|
+
# 2026-06-06 amendment for the ratified contract.
|
|
28
|
+
#
|
|
29
|
+
# `_atomic_mark_with_hash` writes the marker + hash file as an atomic pair
|
|
30
|
+
# (mktemp + mv) so a PASS NEVER silently fails to persist (the empirically-
|
|
31
|
+
# measured P353 failure mode that forced BYPASS_RISK_GATE=1 on every
|
|
32
|
+
# external-comms gate clearance). Either both files land, or neither does.
|
|
33
|
+
# Non-zero exit on failure so callers can emit a diagnostic.
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
# Substance-aware hash of a file or directory path.
|
|
37
|
+
# For directories: hashes the concatenated content of all *.md files
|
|
38
|
+
# (excluding README.md) in sorted order.
|
|
39
|
+
# For files: hashes the file content.
|
|
40
|
+
# Normalisation BEFORE hashing: CRLF → LF, strip trailing whitespace per
|
|
41
|
+
# line, normalise trailing whitespace to a single \n.
|
|
42
|
+
# Echoes "missing" for paths that do not exist (drop-in equivalence with the
|
|
43
|
+
# pre-amendment `cat | _hashcmd | cut -d' ' -f1` site behaviour).
|
|
44
|
+
# Echoes a hex sha256 of the normalised content on success.
|
|
45
|
+
_substance_hash_path() {
|
|
46
|
+
local path="$1"
|
|
47
|
+
if [ -z "$path" ]; then
|
|
48
|
+
echo "missing"
|
|
49
|
+
return 0
|
|
50
|
+
fi
|
|
51
|
+
if [ -f "$path" ]; then
|
|
52
|
+
cat "$path" 2>/dev/null | _substance_normalize_then_hash
|
|
53
|
+
elif [ -d "$path" ]; then
|
|
54
|
+
find "$path" -name '*.md' -not -name 'README.md' -print0 \
|
|
55
|
+
| sort -z \
|
|
56
|
+
| xargs -0 cat 2>/dev/null \
|
|
57
|
+
| _substance_normalize_then_hash
|
|
58
|
+
else
|
|
59
|
+
echo "missing"
|
|
60
|
+
fi
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Internal: reads from stdin, normalises whitespace + line endings, emits a
|
|
64
|
+
# hex sha256 of the normalised content. Conservative boundary documented in
|
|
65
|
+
# ADR-009 2026-06-06 amendment: ambiguous edits stay substantive.
|
|
66
|
+
_substance_normalize_then_hash() {
|
|
67
|
+
python3 -c "
|
|
68
|
+
import sys, hashlib
|
|
69
|
+
data = sys.stdin.buffer.read().decode('utf-8', errors='replace')
|
|
70
|
+
# CRLF / CR -> LF
|
|
71
|
+
data = data.replace('\r\n', '\n').replace('\r', '\n')
|
|
72
|
+
# Strip trailing whitespace per line.
|
|
73
|
+
lines = [line.rstrip() for line in data.split('\n')]
|
|
74
|
+
# Re-join and normalise trailing whitespace to a single \n.
|
|
75
|
+
normalised = '\n'.join(lines).rstrip() + '\n'
|
|
76
|
+
print(hashlib.sha256(normalised.encode('utf-8')).hexdigest())
|
|
77
|
+
" 2>/dev/null || echo "missing"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Atomically write a presence marker + its paired hash file. Either both
|
|
81
|
+
# files land or neither does. Returns 0 on success, 1 on failure. On failure
|
|
82
|
+
# any partial state is rolled back.
|
|
83
|
+
# Usage: _atomic_mark_with_hash "/tmp/architect-reviewed-${SID}" "$HASH"
|
|
84
|
+
_atomic_mark_with_hash() {
|
|
85
|
+
local marker="$1"
|
|
86
|
+
local hash="$2"
|
|
87
|
+
local hash_file="${marker}.hash"
|
|
88
|
+
|
|
89
|
+
if [ -z "$marker" ]; then
|
|
90
|
+
return 1
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
local htmp="${hash_file}.tmp.$$.${RANDOM:-0}"
|
|
94
|
+
local mtmp="${marker}.tmp.$$.${RANDOM:-0}"
|
|
95
|
+
|
|
96
|
+
# Write hash to tempfile.
|
|
97
|
+
if ! printf '%s\n' "$hash" > "$htmp" 2>/dev/null; then
|
|
98
|
+
rm -f "$htmp"
|
|
99
|
+
return 1
|
|
100
|
+
fi
|
|
101
|
+
# Write empty marker to tempfile.
|
|
102
|
+
if ! : > "$mtmp" 2>/dev/null; then
|
|
103
|
+
rm -f "$htmp" "$mtmp"
|
|
104
|
+
return 1
|
|
105
|
+
fi
|
|
106
|
+
# Atomic rename: hash file first.
|
|
107
|
+
if ! mv -f "$htmp" "$hash_file" 2>/dev/null; then
|
|
108
|
+
rm -f "$htmp" "$mtmp"
|
|
109
|
+
return 1
|
|
110
|
+
fi
|
|
111
|
+
# Atomic rename: marker second. If this fails, roll back the hash file
|
|
112
|
+
# so we never observe a hash-without-marker half-state.
|
|
113
|
+
if ! mv -f "$mtmp" "$marker" 2>/dev/null; then
|
|
114
|
+
rm -f "$mtmp"
|
|
115
|
+
rm -f "$hash_file"
|
|
116
|
+
return 1
|
|
117
|
+
fi
|
|
118
|
+
return 0
|
|
119
|
+
}
|
|
120
|
+
|
|
16
121
|
# Paths excluded from pipeline state hashing and docs-only detection.
|
|
17
122
|
_doc_exclusions() {
|
|
18
123
|
echo ':!docs/' ':!.risk-reports/' ':!.changeset/' ':!governance/' ':!.claude/plans/' ':!CLAUDE.md' ':!AGENTS.md' ':!PRINCIPLES.md' ':!DECISION-MANAGEMENT.md' ':!AGENTIC_RISK_REGISTER.md' ':!PROBLEM-MANAGEMENT.md'
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Behavioural tests for ADR-009 amendment 2026-06-06: substance-aware drift +
|
|
4
|
+
# atomic verdict-write. Closes P353 (hash-marker brittleness umbrella) +
|
|
5
|
+
# P303 (architect-gate multi-ADR deadlock drift-relock facet).
|
|
6
|
+
#
|
|
7
|
+
# Cases ratified by the user 2026-06-06:
|
|
8
|
+
# (a) Trivial edit (whitespace, CRLF, trailing newline) does NOT re-trigger.
|
|
9
|
+
# (b) Substantive edit (new content, changed line) DOES re-trigger.
|
|
10
|
+
# (c) Atomic write — marker + hash file land together on PASS, with the
|
|
11
|
+
# marker's mtime current and the hash file's content equal to the
|
|
12
|
+
# substance hash of the policy content.
|
|
13
|
+
# (d) Conservative fallback — semantic edits beyond the documented
|
|
14
|
+
# whitespace normalisation (e.g. single-numeral change) ARE treated as
|
|
15
|
+
# substantive (re-review fires).
|
|
16
|
+
|
|
17
|
+
setup() {
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
19
|
+
source "$SCRIPT_DIR/lib/gate-helpers.sh"
|
|
20
|
+
source "$SCRIPT_DIR/lib/architect-gate.sh"
|
|
21
|
+
|
|
22
|
+
TEST_SESSION="bats-arch-substance-$$-${BATS_TEST_NUMBER}"
|
|
23
|
+
TEST_DIR=$(mktemp -d -t substance-aware-drift.XXXXXX)
|
|
24
|
+
mkdir -p "$TEST_DIR/docs/decisions"
|
|
25
|
+
CLAUDE_PROJECT_DIR="$TEST_DIR"
|
|
26
|
+
export CLAUDE_PROJECT_DIR
|
|
27
|
+
|
|
28
|
+
MARKER="/tmp/architect-reviewed-${TEST_SESSION}"
|
|
29
|
+
HASH_FILE="${MARKER}.hash"
|
|
30
|
+
rm -f "$MARKER" "$HASH_FILE"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
teardown() {
|
|
34
|
+
rm -f "$MARKER" "$HASH_FILE"
|
|
35
|
+
rm -rf "$TEST_DIR"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Helper: install ADR content, then store the substance hash + marker as the
|
|
39
|
+
# architect-mark-reviewed.sh PASS path would.
|
|
40
|
+
_install_and_mark() {
|
|
41
|
+
local body="$1"
|
|
42
|
+
printf '%s' "$body" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
43
|
+
local hash
|
|
44
|
+
hash=$(_substance_hash_path "$TEST_DIR/docs/decisions")
|
|
45
|
+
_atomic_mark_with_hash "$MARKER" "$hash"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Case (a) — trivial whitespace / line-ending edits do NOT re-trigger
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
@test "substance-aware: trailing-whitespace edit does NOT re-trigger drift" {
|
|
53
|
+
_install_and_mark "# ADR-123
|
|
54
|
+
|
|
55
|
+
A line of policy content.
|
|
56
|
+
"
|
|
57
|
+
# Add trailing spaces to the same line.
|
|
58
|
+
printf '%s' "# ADR-123
|
|
59
|
+
|
|
60
|
+
A line of policy content.
|
|
61
|
+
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
62
|
+
|
|
63
|
+
run check_architect_gate "$TEST_SESSION"
|
|
64
|
+
[ "$status" -eq 0 ]
|
|
65
|
+
[ -f "$MARKER" ]
|
|
66
|
+
[ -f "$HASH_FILE" ]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@test "substance-aware: CRLF→LF edit does NOT re-trigger drift" {
|
|
70
|
+
_install_and_mark "# ADR-123
|
|
71
|
+
|
|
72
|
+
A line of policy content.
|
|
73
|
+
"
|
|
74
|
+
# Convert the same content to CRLF line endings.
|
|
75
|
+
printf '# ADR-123\r\n\r\nA line of policy content.\r\n' \
|
|
76
|
+
> "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
77
|
+
|
|
78
|
+
run check_architect_gate "$TEST_SESSION"
|
|
79
|
+
[ "$status" -eq 0 ]
|
|
80
|
+
[ -f "$MARKER" ]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@test "substance-aware: extra trailing newlines do NOT re-trigger drift" {
|
|
84
|
+
_install_and_mark "# ADR-123
|
|
85
|
+
|
|
86
|
+
A line of policy content.
|
|
87
|
+
"
|
|
88
|
+
# Add multiple trailing newlines.
|
|
89
|
+
printf '%s' "# ADR-123
|
|
90
|
+
|
|
91
|
+
A line of policy content.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
96
|
+
|
|
97
|
+
run check_architect_gate "$TEST_SESSION"
|
|
98
|
+
[ "$status" -eq 0 ]
|
|
99
|
+
[ -f "$MARKER" ]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# Case (b) — substantive content edits DO re-trigger
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
@test "substance-aware: new-word edit DOES re-trigger drift" {
|
|
107
|
+
_install_and_mark "# ADR-123
|
|
108
|
+
|
|
109
|
+
A line of policy content.
|
|
110
|
+
"
|
|
111
|
+
# Real content change.
|
|
112
|
+
printf '%s' "# ADR-123
|
|
113
|
+
|
|
114
|
+
A different line of policy content.
|
|
115
|
+
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
116
|
+
|
|
117
|
+
run check_architect_gate "$TEST_SESSION"
|
|
118
|
+
[ "$status" -ne 0 ]
|
|
119
|
+
[ ! -f "$MARKER" ]
|
|
120
|
+
[ ! -f "$HASH_FILE" ]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@test "substance-aware: added-paragraph edit DOES re-trigger drift" {
|
|
124
|
+
_install_and_mark "# ADR-123
|
|
125
|
+
|
|
126
|
+
A line of policy content.
|
|
127
|
+
"
|
|
128
|
+
printf '%s' "# ADR-123
|
|
129
|
+
|
|
130
|
+
A line of policy content.
|
|
131
|
+
|
|
132
|
+
A whole new paragraph of substantive change.
|
|
133
|
+
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
134
|
+
|
|
135
|
+
run check_architect_gate "$TEST_SESSION"
|
|
136
|
+
[ "$status" -ne 0 ]
|
|
137
|
+
[ ! -f "$MARKER" ]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@test "substance-aware: new file in docs/decisions DOES re-trigger drift" {
|
|
141
|
+
_install_and_mark "# ADR-123
|
|
142
|
+
|
|
143
|
+
Content.
|
|
144
|
+
"
|
|
145
|
+
# Adding a NEW ADR file is a substantive change.
|
|
146
|
+
printf '%s' "# ADR-124
|
|
147
|
+
|
|
148
|
+
New ADR content.
|
|
149
|
+
" > "$TEST_DIR/docs/decisions/124-new.proposed.md"
|
|
150
|
+
|
|
151
|
+
run check_architect_gate "$TEST_SESSION"
|
|
152
|
+
[ "$status" -ne 0 ]
|
|
153
|
+
[ ! -f "$MARKER" ]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Case (c) — atomic write persists the PASS marker reliably
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
@test "atomic-write: PASS write lands marker + hash together" {
|
|
161
|
+
printf '%s' "# ADR-123
|
|
162
|
+
|
|
163
|
+
Policy content.
|
|
164
|
+
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
165
|
+
local hash
|
|
166
|
+
hash=$(_substance_hash_path "$TEST_DIR/docs/decisions")
|
|
167
|
+
|
|
168
|
+
run _atomic_mark_with_hash "$MARKER" "$hash"
|
|
169
|
+
[ "$status" -eq 0 ]
|
|
170
|
+
[ -f "$MARKER" ]
|
|
171
|
+
[ -f "$HASH_FILE" ]
|
|
172
|
+
# The stored hash equals the substance hash of the policy content.
|
|
173
|
+
[ "$(cat "$HASH_FILE")" = "$hash" ]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@test "atomic-write: failed hash-write leaves neither file (no half-state)" {
|
|
177
|
+
# Point the marker into a non-existent directory to force mv failure.
|
|
178
|
+
local bad_marker="/tmp/does-not-exist-bats-$$/architect-reviewed-${TEST_SESSION}"
|
|
179
|
+
run _atomic_mark_with_hash "$bad_marker" "deadbeef"
|
|
180
|
+
[ "$status" -ne 0 ]
|
|
181
|
+
[ ! -f "$bad_marker" ]
|
|
182
|
+
[ ! -f "${bad_marker}.hash" ]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@test "atomic-write: PASS write enables a subsequent gate check to allow" {
|
|
186
|
+
printf '%s' "# ADR-123
|
|
187
|
+
|
|
188
|
+
Policy content.
|
|
189
|
+
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
190
|
+
local hash
|
|
191
|
+
hash=$(_substance_hash_path "$TEST_DIR/docs/decisions")
|
|
192
|
+
_atomic_mark_with_hash "$MARKER" "$hash"
|
|
193
|
+
|
|
194
|
+
run check_architect_gate "$TEST_SESSION"
|
|
195
|
+
[ "$status" -eq 0 ]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
# Case (d) — conservative boundary: semantic edits beyond whitespace
|
|
200
|
+
# normalisation ARE treated as substantive
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
@test "conservative: single-numeral change DOES re-trigger drift" {
|
|
204
|
+
_install_and_mark "# ADR-123
|
|
205
|
+
|
|
206
|
+
Threshold is 5 minutes.
|
|
207
|
+
"
|
|
208
|
+
# A single-numeral change is semantic — conservative boundary re-reviews.
|
|
209
|
+
printf '%s' "# ADR-123
|
|
210
|
+
|
|
211
|
+
Threshold is 6 minutes.
|
|
212
|
+
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
213
|
+
|
|
214
|
+
run check_architect_gate "$TEST_SESSION"
|
|
215
|
+
[ "$status" -ne 0 ]
|
|
216
|
+
[ ! -f "$MARKER" ]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@test "conservative: frontmatter-key change DOES re-trigger drift" {
|
|
220
|
+
_install_and_mark "---
|
|
221
|
+
status: proposed
|
|
222
|
+
date: 2026-06-06
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
# ADR-123
|
|
226
|
+
|
|
227
|
+
Body.
|
|
228
|
+
"
|
|
229
|
+
# Frontmatter date bump — conservative boundary re-reviews.
|
|
230
|
+
printf '%s' "---
|
|
231
|
+
status: proposed
|
|
232
|
+
date: 2026-06-07
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
# ADR-123
|
|
236
|
+
|
|
237
|
+
Body.
|
|
238
|
+
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
239
|
+
|
|
240
|
+
run check_architect_gate "$TEST_SESSION"
|
|
241
|
+
[ "$status" -ne 0 ]
|
|
242
|
+
[ ! -f "$MARKER" ]
|
|
243
|
+
}
|
package/package.json
CHANGED
|
@@ -85,6 +85,8 @@ Before writing the ADR file, perform a decision-boundary analysis on the gathere
|
|
|
85
85
|
|
|
86
86
|
**Non-interactive fallback**: When `AskUserQuestion` is unavailable (e.g., non-interactive/AFK mode), automatically split into separate ADRs with consecutive IDs and note the auto-split in output. Do not block creation.
|
|
87
87
|
|
|
88
|
+
**ADR-013 Rule 6 carve-out audit (P352, 2026-06-06 amendment)**: the universal AFK default is **queue-and-continue**; this site is a documented **AUTO-DEFAULT** carve-out. Authorising principle: policy-authorised safe default per ADR-044 category 4 (silent framework). Splitting is fully reversible (manual combine via supersession), the framework's WSJF / lifecycle model rewards explicit per-decision ranking, and "split when in doubt" is the persona-correct safe heuristic for JTBD-006 (the loop progresses; over-splits are cheap to combine; halt would cost more loop throughput than the over-split risk). Note: the Step 5 substance-confirm HALT below is a separate carve-out authorised by ADR-074 — substance-confirm cannot AUTO-DEFAULT because the dependent work (Decision Outcome / Consequences / Confirmation / Pros and Cons drafting) is built ON the chosen option.
|
|
89
|
+
|
|
88
90
|
**Split implementation**: When splitting, assign consecutive IDs. Cross-reference each ADR in the other's Related section or as a linked decision in the consequences.
|
|
89
91
|
|
|
90
92
|
**Scope**: Scoped to new ADR creation only (steps 2–5). Does not apply to supersession handling (step 6), where the scope of the new decision is already known and bounded.
|
|
@@ -238,6 +240,8 @@ oversight-date: YYYY-MM-DD # today
|
|
|
238
240
|
|
|
239
241
|
The `wr-architect-mark-oversight-confirmed` call writes the session-scoped evidence marker (`/tmp/oversight-confirmed-<sha>-<sid>`) that the `architect-oversight-marker-discipline.sh` PreToolUse hook reads to authorise the subsequent Edit/Write — without the helper call, the hook will DENY the marker write. AFK iter subprocesses spawned via `claude -p` have no `AskUserQuestion` access; they MUST write `human-oversight: unconfirmed` instead (the AFK fallback enum value codified in ADR-066 amendment 2026-06-02), which the drain (`/wr-architect:review-decisions`) later promotes interactively. Calling the helper without a real user substance-confirm event is the P348 hollow-marker bug — every legitimate marker write traces back to an `AskUserQuestion` answer in the same turn.
|
|
240
242
|
|
|
243
|
+
**ADR-013 Rule 6 carve-out audit (P352, 2026-06-06 amendment)**: the universal AFK default is queue-and-continue. This Step 5 substance-confirm HALT-and-write-`human-oversight: unconfirmed` shape is a documented carve-out, authorised by **ADR-074** (Confirm decision substance before building dependent work). Rationale: an ADR with `human-oversight: confirmed` enters the world born-confirmed (it does not appear in `/wr-architect:review-decisions`' unoversighted set), so dependent work — every implementation that cites this ADR as authority — would be built on substance that was never user-affirmed. AFK writing `human-oversight: unconfirmed` IS the queue-and-continue shape: the loop continues; the substance-confirm decision is queued to the next interactive drain. Persona-correct for JTBD-006 ("queued for my return, not guessed at"); the carve-out is from the auto-confirm shape, not from queue-and-continue itself.
|
|
244
|
+
|
|
241
245
|
**Mismatch handling.** If the substance-confirm answer selects a DIFFERENT option than the draft was authored against:
|
|
242
246
|
|
|
243
247
|
- DO NOT write the marker.
|
|
@@ -59,6 +59,8 @@ For each ADR in the ordered queue, surface the decision as an `AskUserQuestion`
|
|
|
59
59
|
|
|
60
60
|
The trailing clause exists for cross-reference value — it is optional, NOT a re-entry point for meta-leading. When the meta does not add load-bearing context for the confirm decision, omit it entirely.
|
|
61
61
|
|
|
62
|
+
**Brief-before-ID discipline (P350).** The `question` and `options` text MUST inline what each referenced artefact is and what is at stake before naming it by ID. `ADR-NNN` / `P-NNN` / `JTBD-NNN` / `RFC-NNN` references are audit-trail annotations, NEVER carriers of meaning — the user reads this prompt without project filesystem access (mobile clients, accessibility tooling, notification surfaces) and cannot follow links. Acceptable: *"This ADR decides: five per-tool-call hook patterns + their budget bands (sibling of the UserPromptSubmit-pattern decision and the SessionStart-pattern decision)."* Unacceptable: *"This ADR decides: 5 patterns; sibling to ADR-038 and ADR-040."* The trailing parenthetical IDs are permitted ONLY after a self-contained explanation, never as the explanation itself. Mirrors the canonical `/wr-architect:create-adr` Step 5 § 5a Rule 3 ("No IDs as explainers"). See also session memory `feedback_brief_before_id.md`.
|
|
63
|
+
|
|
62
64
|
This is a genuine human-decision surface (the whole point of P283) — `AskUserQuestion` is correct here and is NOT over-asking. Do not auto-confirm; do not prose-ask.
|
|
63
65
|
|
|
64
66
|
### Step 4: Apply the outcome
|