@windyroad/jtbd 0.12.5-preview.617 → 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.
|
@@ -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
|
+
}
|