@windyroad/architect 0.15.5 → 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.
@@ -123,5 +123,5 @@
123
123
  }
124
124
  },
125
125
  "name": "wr-architect",
126
- "version": "0.15.5"
126
+ "version": "0.15.6"
127
127
  }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/architect",
3
- "version": "0.15.5",
3
+ "version": "0.15.6",
4
4
  "description": "Architecture decision enforcement for AI coding agents",
5
5
  "bin": {
6
6
  "windyroad-architect": "./bin/install.mjs"