@windyroad/jtbd 0.12.5 → 0.12.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.
@@ -90,5 +90,5 @@
90
90
  }
91
91
  },
92
92
  "name": "wr-jtbd",
93
- "version": "0.12.5"
93
+ "version": "0.12.6"
94
94
  }
@@ -9,6 +9,7 @@
9
9
 
10
10
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
11
11
  source "$SCRIPT_DIR/lib/review-gate.sh"
12
+ source "$SCRIPT_DIR/lib/marker-only-diff.sh"
12
13
 
13
14
  # P191: resolve the project root from the session signal, not the hook's
14
15
  # runtime CWD. Claude Code may launch the hook with an actual working
@@ -40,6 +41,15 @@ except:
40
41
  print('')
41
42
  " 2>/dev/null || echo "")
42
43
 
44
+ TOOL_NAME=$(echo "$INPUT" | python3 -c "
45
+ import sys, json
46
+ try:
47
+ data = json.load(sys.stdin)
48
+ print(data.get('tool_name', ''))
49
+ except:
50
+ print('')
51
+ " 2>/dev/null || echo "")
52
+
43
53
  if [ -z "$SESSION_ID" ]; then
44
54
  review_gate_parse_error
45
55
  exit 0
@@ -120,6 +130,62 @@ case "$FILE_PATH" in
120
130
  exit 0 ;;
121
131
  esac
122
132
 
133
+ # P301: marker-only-diff exemption for docs/decisions/*.md ADRs. The JTBD
134
+ # gate fires on docs/decisions/ writes (not in its exclusion list), so a
135
+ # multi-batch `/wr-architect:review-decisions` drain pays one JTBD review
136
+ # round-trip per batch on top of the architect one — the ticket's
137
+ # observed "Batch 8 (ADR-020): blocked on architect review (`jtbd policy
138
+ # file changed since last review`)" symptom is precisely that. Same
139
+ # rationale + safety-net as the architect-side exemption: oversight
140
+ # marker writes are mechanical output of a substance-confirmed decision
141
+ # (ADR-066 contract); the jtbd-oversight-marker-discipline.sh hook
142
+ # continues to enforce per-ADR session evidence for `confirmed`
143
+ # introductions.
144
+ case "$FILE_PATH" in
145
+ */docs/decisions/*.md|docs/decisions/*.md)
146
+ case "$TOOL_NAME" in
147
+ Edit)
148
+ _OLD=$(echo "$INPUT" | python3 -c "
149
+ import sys, json
150
+ try:
151
+ data = json.load(sys.stdin)
152
+ print(data.get('tool_input', {}).get('old_string', ''))
153
+ except:
154
+ print('')
155
+ " 2>/dev/null || echo "")
156
+ _NEW=$(echo "$INPUT" | python3 -c "
157
+ import sys, json
158
+ try:
159
+ data = json.load(sys.stdin)
160
+ print(data.get('tool_input', {}).get('new_string', ''))
161
+ except:
162
+ print('')
163
+ " 2>/dev/null || echo "")
164
+ if [ -n "$_OLD$_NEW" ] && is_marker_only_diff "$_OLD" "$_NEW"; then
165
+ exit 0
166
+ fi
167
+ ;;
168
+ Write)
169
+ _NEW=$(echo "$INPUT" | python3 -c "
170
+ import sys, json
171
+ try:
172
+ data = json.load(sys.stdin)
173
+ print(data.get('tool_input', {}).get('content', ''))
174
+ except:
175
+ print('')
176
+ " 2>/dev/null || echo "")
177
+ _OLD=""
178
+ if [ -f "$FILE_PATH" ]; then
179
+ _OLD=$(cat "$FILE_PATH" 2>/dev/null) || _OLD=""
180
+ fi
181
+ if [ -n "$_OLD$_NEW" ] && is_marker_only_diff "$_OLD" "$_NEW"; then
182
+ exit 0
183
+ fi
184
+ ;;
185
+ esac
186
+ ;;
187
+ esac
188
+
123
189
  # Determine JTBD path — canonical directory layout only (ADR-008 Option 3).
124
190
  # Legacy docs/JOBS_TO_BE_DONE.md is NOT consulted at runtime; the gate is
125
191
  # inactive on projects that have not run /wr-jtbd:update-guide.
@@ -0,0 +1,87 @@
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
+ # Sibling copy of packages/architect/hooks/lib/marker-only-diff.sh per the
37
+ # existing gate-helpers.sh duplicate-shared pattern (ADR-017). Keep in sync
38
+ # manually until a second call site justifies the cost of a sync script.
39
+
40
+ is_marker_only_diff() {
41
+ local old="$1"
42
+ local new="$2"
43
+
44
+ command -v python3 >/dev/null 2>&1 || return 1
45
+
46
+ OLD_CONTENT="$old" NEW_CONTENT="$new" python3 - <<'PYEOF'
47
+ import os, sys, re
48
+ try:
49
+ import difflib
50
+ except Exception:
51
+ sys.exit(1)
52
+
53
+ old = os.environ.get('OLD_CONTENT', '')
54
+ new = os.environ.get('NEW_CONTENT', '')
55
+
56
+ if old == new:
57
+ sys.exit(0)
58
+
59
+ old_lines = old.splitlines()
60
+ new_lines = new.splitlines()
61
+
62
+ marker_pat = re.compile(
63
+ r'^[ \t]*(human-oversight|oversight-date|decision-makers|supersede-ticket)[ \t]*:.*$'
64
+ )
65
+
66
+ def allowed(line: str) -> bool:
67
+ s = line.strip()
68
+ if s == '':
69
+ return True
70
+ if marker_pat.match(line):
71
+ return True
72
+ return False
73
+
74
+ sm = difflib.SequenceMatcher(a=old_lines, b=new_lines, autojunk=False)
75
+ for tag, i1, i2, j1, j2 in sm.get_opcodes():
76
+ if tag == 'equal':
77
+ continue
78
+ for line in old_lines[i1:i2]:
79
+ if not allowed(line):
80
+ sys.exit(1)
81
+ for line in new_lines[j1:j2]:
82
+ if not allowed(line):
83
+ sys.exit(1)
84
+
85
+ sys.exit(0)
86
+ PYEOF
87
+ }
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # P301: jtbd-enforce-edit.sh exempts marker-only frontmatter diffs to
4
+ # docs/decisions/*.md ADRs from the JTBD review gate. The JTBD gate currently
5
+ # fires on docs/decisions/ writes (decisions are not in its exclusion list);
6
+ # the ticket's symptom "Batch 8 (ADR-020): blocked on architect review
7
+ # (`jtbd policy file changed since last review`)" is that JTBD round-trip.
8
+ #
9
+ # Behavioural — exercises the full hook with constructed PreToolUse stdin
10
+ # JSON payloads under a sandbox project dir; asserts on stdout+exit.
11
+
12
+ setup() {
13
+ SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
14
+ HOOK="$SCRIPT_DIR/jtbd-enforce-edit.sh"
15
+ ORIG_DIR="$PWD"
16
+ TEST_DIR=$(mktemp -d)
17
+ cd "$TEST_DIR"
18
+ # JTBD docs must exist for the gate to engage (otherwise it denies
19
+ # with "no JTBD documentation" — out of scope of this exemption test).
20
+ mkdir -p docs/jtbd/persona docs/decisions
21
+ echo "# stub job" > docs/jtbd/persona/JTBD-001-stub.proposed.md
22
+ echo "# stub adr" > docs/decisions/001-stub.proposed.md
23
+ SID="jtbd-marker-only-$$-$BATS_TEST_NUMBER"
24
+ }
25
+
26
+ teardown() {
27
+ cd "$ORIG_DIR"
28
+ rm -rf "$TEST_DIR"
29
+ rm -f "/tmp/jtbd-reviewed-${SID}" "/tmp/jtbd-reviewed-${SID}.hash"
30
+ }
31
+
32
+ run_hook_json() {
33
+ local json_file="$1"
34
+ bash "$HOOK" < "$json_file"
35
+ }
36
+
37
+ # ── Marker-only ADD: should exempt ───────────────────────────────────────
38
+
39
+ @test "P301: Edit adding only human-oversight + oversight-date to ADR is exempt (no JTBD marker required)" {
40
+ adr="$PWD/docs/decisions/100-some-adr.proposed.md"
41
+ cat > "$adr" <<'EOF'
42
+ ---
43
+ status: "proposed"
44
+ ---
45
+
46
+ # 100 some adr
47
+ EOF
48
+ old=$'---\nstatus: "proposed"\n---'
49
+ new=$'---\nstatus: "proposed"\nhuman-oversight: unconfirmed\noversight-date: 2026-06-08\n---'
50
+ json_file=$(mktemp)
51
+ jq -nc --arg p "$adr" --arg s "$SID" --arg o "$old" --arg n "$new" \
52
+ '{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:$o,new_string:$n}}' > "$json_file"
53
+ run run_hook_json "$json_file"
54
+ rm -f "$json_file"
55
+ [ "$status" -eq 0 ]
56
+ [[ "$output" != *"BLOCKED"* ]]
57
+ }
58
+
59
+ @test "P301: Edit updating human-oversight value (unconfirmed → confirmed) on ADR is exempt" {
60
+ adr="$PWD/docs/decisions/101-promote.proposed.md"
61
+ cat > "$adr" <<'EOF'
62
+ ---
63
+ status: "proposed"
64
+ human-oversight: unconfirmed
65
+ ---
66
+
67
+ # 101
68
+ EOF
69
+ old=$'human-oversight: unconfirmed'
70
+ new=$'human-oversight: confirmed\noversight-date: 2026-06-08'
71
+ json_file=$(mktemp)
72
+ jq -nc --arg p "$adr" --arg s "$SID" --arg o "$old" --arg n "$new" \
73
+ '{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:$o,new_string:$n}}' > "$json_file"
74
+ run run_hook_json "$json_file"
75
+ rm -f "$json_file"
76
+ [ "$status" -eq 0 ]
77
+ [[ "$output" != *"BLOCKED"* ]]
78
+ }
79
+
80
+ # ── Mixed marker + body: must NOT exempt ─────────────────────────────────
81
+
82
+ @test "P301: Edit changing body content along with markers still gates" {
83
+ adr="$PWD/docs/decisions/110-mixed.proposed.md"
84
+ cat > "$adr" <<'EOF'
85
+ ---
86
+ status: "proposed"
87
+ ---
88
+
89
+ # 110
90
+
91
+ Original body.
92
+ EOF
93
+ old=$'---\nstatus: "proposed"\n---\n\n# 110\n\nOriginal body.'
94
+ new=$'---\nstatus: "proposed"\nhuman-oversight: confirmed\noversight-date: 2026-06-08\n---\n\n# 110\n\nNew policy claim added.'
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
+ [[ "$output" == *"BLOCKED"* ]]
101
+ }
102
+
103
+ # ── Pure body change: must still gate ────────────────────────────────────
104
+
105
+ @test "P301: Edit changing only body content with no marker lines still gates" {
106
+ adr="$PWD/docs/decisions/120-body.proposed.md"
107
+ cat > "$adr" <<'EOF'
108
+ ---
109
+ status: "proposed"
110
+ ---
111
+
112
+ # 120
113
+
114
+ Some text.
115
+ EOF
116
+ old='Some text.'
117
+ new='Some text. And more.'
118
+ json_file=$(mktemp)
119
+ jq -nc --arg p "$adr" --arg s "$SID" --arg o "$old" --arg n "$new" \
120
+ '{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:$o,new_string:$n}}' > "$json_file"
121
+ run run_hook_json "$json_file"
122
+ rm -f "$json_file"
123
+ [[ "$output" == *"BLOCKED"* ]]
124
+ }
125
+
126
+ # ── Scope: exemption is narrow to docs/decisions/*.md ────────────────────
127
+
128
+ @test "P301: marker-only diff to a NON docs/decisions/ path still gates (no path exemption)" {
129
+ mkdir -p "$PWD/src"
130
+ echo "// stub" > "$PWD/src/x.ts"
131
+ src="$PWD/src/x.ts"
132
+ old='// stub'
133
+ new=$'// stub\nhuman-oversight: confirmed'
134
+ json_file=$(mktemp)
135
+ jq -nc --arg p "$src" --arg s "$SID" --arg o "$old" --arg n "$new" \
136
+ '{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:$o,new_string:$n}}' > "$json_file"
137
+ run run_hook_json "$json_file"
138
+ rm -f "$json_file"
139
+ [[ "$output" == *"BLOCKED"* ]]
140
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/jtbd",
3
- "version": "0.12.5",
3
+ "version": "0.12.6",
4
4
  "description": "Jobs-to-be-done enforcement for UI changes",
5
5
  "bin": {
6
6
  "windyroad-jtbd": "./bin/install.mjs"