@windyroad/architect 0.15.5-preview.617 → 0.15.6
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.
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
7
|
source "$SCRIPT_DIR/lib/architect-gate.sh"
|
|
8
|
+
source "$SCRIPT_DIR/lib/marker-only-diff.sh"
|
|
8
9
|
|
|
9
10
|
# P191 Phase 2: resolve the project root from the session signal, not the
|
|
10
11
|
# hook's runtime CWD. Claude Code can launch the hook with a working directory
|
|
@@ -22,6 +23,7 @@ INPUT=$(cat)
|
|
|
22
23
|
|
|
23
24
|
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') || true
|
|
24
25
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty') || true
|
|
26
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty') || true
|
|
25
27
|
|
|
26
28
|
if [ -z "$SESSION_ID" ]; then
|
|
27
29
|
architect_gate_parse_error
|
|
@@ -114,6 +116,41 @@ case "$FILE_PATH" in
|
|
|
114
116
|
exit 0 ;;
|
|
115
117
|
esac
|
|
116
118
|
|
|
119
|
+
# P301: marker-only-diff exemption for docs/decisions/*.md ADRs. An
|
|
120
|
+
# Edit/Write that adds or updates ONLY the narrow oversight-marker
|
|
121
|
+
# frontmatter grammar (`human-oversight:`, `oversight-date:`,
|
|
122
|
+
# `decision-makers:`, `supersede-ticket:`) is the mechanical output of a
|
|
123
|
+
# decision the user already substance-confirmed via AskUserQuestion
|
|
124
|
+
# (ADR-066 contract); the architect review has nothing substantive to
|
|
125
|
+
# assess. The architect-oversight-marker-discipline.sh hook remains the
|
|
126
|
+
# safety net: marker-only diffs that introduce `human-oversight:
|
|
127
|
+
# confirmed` still require the per-ADR session evidence marker (P348 /
|
|
128
|
+
# ADR-066 amendment 2026-06-02). Mixed marker+body diffs, status:/date:
|
|
129
|
+
# changes, and pure body changes fall through to the normal gate.
|
|
130
|
+
case "$FILE_PATH" in
|
|
131
|
+
*/docs/decisions/*.md|docs/decisions/*.md)
|
|
132
|
+
case "$TOOL_NAME" in
|
|
133
|
+
Edit)
|
|
134
|
+
_OLD=$(echo "$INPUT" | jq -r '.tool_input.old_string // empty') || _OLD=""
|
|
135
|
+
_NEW=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty') || _NEW=""
|
|
136
|
+
if [ -n "$_OLD$_NEW" ] && is_marker_only_diff "$_OLD" "$_NEW"; then
|
|
137
|
+
exit 0
|
|
138
|
+
fi
|
|
139
|
+
;;
|
|
140
|
+
Write)
|
|
141
|
+
_NEW=$(echo "$INPUT" | jq -r '.tool_input.content // empty') || _NEW=""
|
|
142
|
+
_OLD=""
|
|
143
|
+
if [ -f "$FILE_PATH" ]; then
|
|
144
|
+
_OLD=$(cat "$FILE_PATH" 2>/dev/null) || _OLD=""
|
|
145
|
+
fi
|
|
146
|
+
if [ -n "$_OLD$_NEW" ] && is_marker_only_diff "$_OLD" "$_NEW"; then
|
|
147
|
+
exit 0
|
|
148
|
+
fi
|
|
149
|
+
;;
|
|
150
|
+
esac
|
|
151
|
+
;;
|
|
152
|
+
esac
|
|
153
|
+
|
|
117
154
|
# Check gate
|
|
118
155
|
if check_architect_gate "$SESSION_ID"; then
|
|
119
156
|
exit 0
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared helper: detect "oversight-marker-only" frontmatter diffs.
|
|
3
|
+
#
|
|
4
|
+
# P301: ADR-066/068 oversight-marker writes to docs/decisions/ ADRs trip the
|
|
5
|
+
# full architect+JTBD edit gate each batch of an `/wr-architect:review-decisions`
|
|
6
|
+
# drain, producing 2-3 no-op-PASS review round-trips per batch. The marker
|
|
7
|
+
# add/update is the mechanical output of a decision the user already
|
|
8
|
+
# substance-confirmed via AskUserQuestion (ADR-066 contract); the gate has
|
|
9
|
+
# nothing substantive to assess.
|
|
10
|
+
#
|
|
11
|
+
# This helper exposes `is_marker_only_diff OLD NEW` returning 0 when every
|
|
12
|
+
# added/removed non-empty line matches one of the oversight-marker frontmatter
|
|
13
|
+
# patterns:
|
|
14
|
+
#
|
|
15
|
+
# human-oversight: confirmed | unconfirmed | rejected-pending-supersede
|
|
16
|
+
# oversight-date: <date>
|
|
17
|
+
# decision-makers: <name|email>
|
|
18
|
+
# supersede-ticket: <ticket>
|
|
19
|
+
#
|
|
20
|
+
# When the predicate fires PASS, the calling gate short-circuits to exit 0
|
|
21
|
+
# without requiring a fresh architect / JTBD review marker. The
|
|
22
|
+
# architect-oversight-marker-discipline.sh (and JTBD sibling) hooks remain
|
|
23
|
+
# active as the safety net: a marker-only diff that introduces
|
|
24
|
+
# `human-oversight: confirmed` still requires the per-ADR session evidence
|
|
25
|
+
# marker (P348 / ADR-066 amendment 2026-06-02).
|
|
26
|
+
#
|
|
27
|
+
# Conservative boundary: any non-empty line outside the marker grammar
|
|
28
|
+
# (frontmatter delimiters that shift position, status:/date: changes, body
|
|
29
|
+
# edits) fails the predicate. The exemption is exact so it cannot be used
|
|
30
|
+
# to slip decision-content edits past the gate.
|
|
31
|
+
#
|
|
32
|
+
# Fail-safe: if python3 is unavailable or the diff parse errors, the function
|
|
33
|
+
# returns 1 (NOT marker-only) and the gate proceeds with its normal review
|
|
34
|
+
# requirement.
|
|
35
|
+
|
|
36
|
+
is_marker_only_diff() {
|
|
37
|
+
local old="$1"
|
|
38
|
+
local new="$2"
|
|
39
|
+
|
|
40
|
+
command -v python3 >/dev/null 2>&1 || return 1
|
|
41
|
+
|
|
42
|
+
OLD_CONTENT="$old" NEW_CONTENT="$new" python3 - <<'PYEOF'
|
|
43
|
+
import os, sys, re
|
|
44
|
+
try:
|
|
45
|
+
import difflib
|
|
46
|
+
except Exception:
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
|
|
49
|
+
old = os.environ.get('OLD_CONTENT', '')
|
|
50
|
+
new = os.environ.get('NEW_CONTENT', '')
|
|
51
|
+
|
|
52
|
+
if old == new:
|
|
53
|
+
sys.exit(0)
|
|
54
|
+
|
|
55
|
+
old_lines = old.splitlines()
|
|
56
|
+
new_lines = new.splitlines()
|
|
57
|
+
|
|
58
|
+
marker_pat = re.compile(
|
|
59
|
+
r'^[ \t]*(human-oversight|oversight-date|decision-makers|supersede-ticket)[ \t]*:.*$'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def allowed(line: str) -> bool:
|
|
63
|
+
s = line.strip()
|
|
64
|
+
if s == '':
|
|
65
|
+
return True
|
|
66
|
+
if marker_pat.match(line):
|
|
67
|
+
return True
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
sm = difflib.SequenceMatcher(a=old_lines, b=new_lines, autojunk=False)
|
|
71
|
+
for tag, i1, i2, j1, j2 in sm.get_opcodes():
|
|
72
|
+
if tag == 'equal':
|
|
73
|
+
continue
|
|
74
|
+
for line in old_lines[i1:i2]:
|
|
75
|
+
if not allowed(line):
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
for line in new_lines[j1:j2]:
|
|
78
|
+
if not allowed(line):
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
PYEOF
|
|
83
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# P301: architect-enforce-edit.sh exempts marker-only frontmatter diffs to
|
|
4
|
+
# docs/decisions/*.md ADRs from the full architect review gate. The
|
|
5
|
+
# architect-oversight-marker-discipline.sh hook remains the safety net for
|
|
6
|
+
# `human-oversight: confirmed` introductions; this exemption only short-
|
|
7
|
+
# circuits the enforce-edit drift/TTL gate when the diff adds/modifies
|
|
8
|
+
# nothing but the narrow oversight-marker grammar.
|
|
9
|
+
#
|
|
10
|
+
# Behavioural — exercises the full hook with constructed PreToolUse stdin
|
|
11
|
+
# JSON payloads under a sandbox project dir; asserts on stdout+exit.
|
|
12
|
+
|
|
13
|
+
setup() {
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
15
|
+
HOOK="$SCRIPT_DIR/architect-enforce-edit.sh"
|
|
16
|
+
ORIG_DIR="$PWD"
|
|
17
|
+
TEST_DIR=$(mktemp -d)
|
|
18
|
+
cd "$TEST_DIR"
|
|
19
|
+
# Engage the architect gate — only runs when docs/decisions/ exists.
|
|
20
|
+
mkdir -p docs/decisions
|
|
21
|
+
echo "# stub" > docs/decisions/001-stub.proposed.md
|
|
22
|
+
SID="marker-only-exempt-$$-$BATS_TEST_NUMBER"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
teardown() {
|
|
26
|
+
cd "$ORIG_DIR"
|
|
27
|
+
rm -rf "$TEST_DIR"
|
|
28
|
+
rm -f "/tmp/architect-reviewed-${SID}" "/tmp/architect-reviewed-${SID}.hash"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Helper: run the hook with constructed JSON. Echo to a file then pipe to
|
|
32
|
+
# avoid heredoc / quote-escaping headaches with multi-line content.
|
|
33
|
+
run_hook_json() {
|
|
34
|
+
local json_file="$1"
|
|
35
|
+
bash "$HOOK" < "$json_file"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# ── Marker-only ADD: should exempt ───────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
@test "P301: Edit adding only human-oversight + oversight-date is exempt (no architect marker required)" {
|
|
41
|
+
adr="$PWD/docs/decisions/100-some-adr.proposed.md"
|
|
42
|
+
cat > "$adr" <<'EOF'
|
|
43
|
+
---
|
|
44
|
+
status: "proposed"
|
|
45
|
+
date: 2026-06-08
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
# 100 some adr
|
|
49
|
+
|
|
50
|
+
Body content unchanged.
|
|
51
|
+
EOF
|
|
52
|
+
old=$'---\nstatus: "proposed"\ndate: 2026-06-08\n---'
|
|
53
|
+
new=$'---\nstatus: "proposed"\ndate: 2026-06-08\nhuman-oversight: unconfirmed\noversight-date: 2026-06-08\n---'
|
|
54
|
+
json_file=$(mktemp)
|
|
55
|
+
jq -nc --arg p "$adr" --arg s "$SID" --arg o "$old" --arg n "$new" \
|
|
56
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:$o,new_string:$n}}' > "$json_file"
|
|
57
|
+
run run_hook_json "$json_file"
|
|
58
|
+
rm -f "$json_file"
|
|
59
|
+
[ "$status" -eq 0 ]
|
|
60
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@test "P301: Edit updating human-oversight value (unconfirmed → confirmed) is exempt" {
|
|
64
|
+
adr="$PWD/docs/decisions/101-promote.proposed.md"
|
|
65
|
+
cat > "$adr" <<'EOF'
|
|
66
|
+
---
|
|
67
|
+
status: "proposed"
|
|
68
|
+
human-oversight: unconfirmed
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
# 101 promote
|
|
72
|
+
EOF
|
|
73
|
+
old=$'human-oversight: unconfirmed'
|
|
74
|
+
new=$'human-oversight: confirmed\noversight-date: 2026-06-08'
|
|
75
|
+
json_file=$(mktemp)
|
|
76
|
+
jq -nc --arg p "$adr" --arg s "$SID" --arg o "$old" --arg n "$new" \
|
|
77
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:$o,new_string:$n}}' > "$json_file"
|
|
78
|
+
run run_hook_json "$json_file"
|
|
79
|
+
rm -f "$json_file"
|
|
80
|
+
[ "$status" -eq 0 ]
|
|
81
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@test "P301: Edit adding rejected-pending-supersede + supersede-ticket is exempt" {
|
|
85
|
+
adr="$PWD/docs/decisions/102-rejected.proposed.md"
|
|
86
|
+
cat > "$adr" <<'EOF'
|
|
87
|
+
---
|
|
88
|
+
status: "proposed"
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
# 102 rejected
|
|
92
|
+
EOF
|
|
93
|
+
old=$'status: "proposed"'
|
|
94
|
+
new=$'status: "proposed"\nhuman-oversight: rejected-pending-supersede\nsupersede-ticket: P999'
|
|
95
|
+
json_file=$(mktemp)
|
|
96
|
+
jq -nc --arg p "$adr" --arg s "$SID" --arg o "$old" --arg n "$new" \
|
|
97
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:$o,new_string:$n}}' > "$json_file"
|
|
98
|
+
run run_hook_json "$json_file"
|
|
99
|
+
rm -f "$json_file"
|
|
100
|
+
[ "$status" -eq 0 ]
|
|
101
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# ── Mixed marker + body: must NOT exempt ─────────────────────────────────
|
|
105
|
+
|
|
106
|
+
@test "P301: Edit changing body content along with markers still gates" {
|
|
107
|
+
adr="$PWD/docs/decisions/110-mixed.proposed.md"
|
|
108
|
+
cat > "$adr" <<'EOF'
|
|
109
|
+
---
|
|
110
|
+
status: "proposed"
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
# 110 mixed
|
|
114
|
+
|
|
115
|
+
Original body.
|
|
116
|
+
EOF
|
|
117
|
+
old=$'---\nstatus: "proposed"\n---\n\n# 110 mixed\n\nOriginal body.'
|
|
118
|
+
new=$'---\nstatus: "proposed"\nhuman-oversight: confirmed\noversight-date: 2026-06-08\n---\n\n# 110 mixed\n\nRewritten body with new policy claim.'
|
|
119
|
+
json_file=$(mktemp)
|
|
120
|
+
jq -nc --arg p "$adr" --arg s "$SID" --arg o "$old" --arg n "$new" \
|
|
121
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:$o,new_string:$n}}' > "$json_file"
|
|
122
|
+
run run_hook_json "$json_file"
|
|
123
|
+
rm -f "$json_file"
|
|
124
|
+
[[ "$output" == *"BLOCKED"* ]]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@test "P301: Edit changing status field (not a marker line) still gates" {
|
|
128
|
+
adr="$PWD/docs/decisions/111-status-change.proposed.md"
|
|
129
|
+
cat > "$adr" <<'EOF'
|
|
130
|
+
---
|
|
131
|
+
status: "proposed"
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
# 111
|
|
135
|
+
EOF
|
|
136
|
+
old=$'status: "proposed"'
|
|
137
|
+
new=$'status: "accepted"\nhuman-oversight: confirmed\noversight-date: 2026-06-08'
|
|
138
|
+
json_file=$(mktemp)
|
|
139
|
+
jq -nc --arg p "$adr" --arg s "$SID" --arg o "$old" --arg n "$new" \
|
|
140
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:$o,new_string:$n}}' > "$json_file"
|
|
141
|
+
run run_hook_json "$json_file"
|
|
142
|
+
rm -f "$json_file"
|
|
143
|
+
[[ "$output" == *"BLOCKED"* ]]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# ── Pure body change: must still gate (no marker involvement) ────────────
|
|
147
|
+
|
|
148
|
+
@test "P301: Edit changing only body content with no marker lines still gates" {
|
|
149
|
+
adr="$PWD/docs/decisions/120-body.proposed.md"
|
|
150
|
+
cat > "$adr" <<'EOF'
|
|
151
|
+
---
|
|
152
|
+
status: "proposed"
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
# 120 body
|
|
156
|
+
|
|
157
|
+
Some text.
|
|
158
|
+
EOF
|
|
159
|
+
old='Some text.'
|
|
160
|
+
new='Some text. And new text.'
|
|
161
|
+
json_file=$(mktemp)
|
|
162
|
+
jq -nc --arg p "$adr" --arg s "$SID" --arg o "$old" --arg n "$new" \
|
|
163
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:$o,new_string:$n}}' > "$json_file"
|
|
164
|
+
run run_hook_json "$json_file"
|
|
165
|
+
rm -f "$json_file"
|
|
166
|
+
[[ "$output" == *"BLOCKED"* ]]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# ── Scope: exemption is narrow to docs/decisions/*.md ────────────────────
|
|
170
|
+
|
|
171
|
+
@test "P301: marker-only diff to a NON docs/decisions/ path still gates (no path exemption)" {
|
|
172
|
+
mkdir -p "$PWD/src"
|
|
173
|
+
echo "// stub" > "$PWD/src/x.ts"
|
|
174
|
+
src="$PWD/src/x.ts"
|
|
175
|
+
old='// stub'
|
|
176
|
+
new='// stub'$'\n''human-oversight: confirmed'
|
|
177
|
+
json_file=$(mktemp)
|
|
178
|
+
jq -nc --arg p "$src" --arg s "$SID" --arg o "$old" --arg n "$new" \
|
|
179
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:$o,new_string:$n}}' > "$json_file"
|
|
180
|
+
run run_hook_json "$json_file"
|
|
181
|
+
rm -f "$json_file"
|
|
182
|
+
[[ "$output" == *"BLOCKED"* ]]
|
|
183
|
+
}
|