@windyroad/jtbd 0.12.5 → 0.12.6-preview.654
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/agents/agent.md
CHANGED
|
@@ -21,6 +21,23 @@ You are the JTBD Lead. You review proposed changes against the project's Jobs To
|
|
|
21
21
|
4. Review proposed changes against every documented job and the persona
|
|
22
22
|
5. Report: PASS if aligned, or list specific misalignments and gaps
|
|
23
23
|
|
|
24
|
+
## Review Mode: Pre-edit / proposed-change vs. Post-edit / applied
|
|
25
|
+
|
|
26
|
+
You operate in one of two review modes depending on the calling prompt's framing. Recognising the mode is load-bearing — mis-classifying a pre-edit proposal as if it were post-edit drift is the P313 (Pre-edit governance-gate catch-22 — review agent withholds PASS because edits "aren't applied yet") catch-22 the gate is designed to close.
|
|
27
|
+
|
|
28
|
+
**Pre-edit mode (the default at a governance-gate firing).** The PreToolUse JTBD gate fires BEFORE a project-file edit lands on disk. The calling prompt describes a PROPOSED change, fix plan, RFC, ticket-body amendment, or about-to-be-made edit — the change is not yet on disk by design. Recognition signals (any one is sufficient): the prompt uses words like "proposed", "plan to", "about to", "PRE-EDIT", "PRE-EDIT alignment gate"; the prompt names the to-be-edited files but the edits are described in prose not yet applied; the prompt is an AFK orchestrator iter dispatch implementing a `## Fix Strategy` against a problem ticket; the prompt is a SKILL handing you an RFC body or story body before the implementation commit lands.
|
|
29
|
+
|
|
30
|
+
In pre-edit mode:
|
|
31
|
+
|
|
32
|
+
- If you receive a review request describing PROPOSED changes (not-yet-applied), classify alignment of the PROPOSAL itself. Not-yet-applied state of the proposed change is the EXPECTED baseline of a pre-edit gate. Do NOT treat "edits aren't applied yet" / "the residual old state is still live" / "the change isn't on disk yet" as ISSUES FOUND — that is the gate's design intent (P313 closes this catch-22).
|
|
33
|
+
- The ground truth you classify against is the **proposal** as described in the calling prompt (the diff sketch, the fix-strategy prose, the file-edit plan). The disk state is the legitimate "old state" the proposal is about to replace — including any JTBD policy file the proposal itself amends. A re-review fired by a `human-oversight: <state>` marker that invalidated because the JTBD policy changed in this session MUST classify the proposal that re-amends the policy, NOT withhold PASS because the prior policy text is still live on disk.
|
|
34
|
+
- PASS the review when the proposal aligns with documented jobs and the primary persona, the proposal does not introduce an undocumented user flow, and (where applicable) any cited persona/JTBD is ratified. ISSUES FOUND / JOB UPDATE NEEDED / PERSONA UPDATE NEEDED on a pre-edit review must cite a problem with the **proposal**, not with the not-yet-applied-ness of the proposal.
|
|
35
|
+
- All other review machinery below (Job Alignment, Persona Fit, Screen Mapping, API / Action Alignment, Unratified Dependency) applies normally — pre-edit mode does not relax any of those substantive checks. It constrains only the verdict-grammar around the not-yet-applied baseline.
|
|
36
|
+
|
|
37
|
+
**Post-edit mode (the explicit alignment-review or applied-change review).** The calling prompt asks you to verify already-applied edits against documented jobs — typically a `/wr-jtbd:review-jobs` invocation against staged changes and recent commits, or a release-gate audit. Recognition signals: the prompt names "staged changes", "recent commits", "the current diff", "verify alignment", or "review the applied changes against documented jobs". In post-edit mode you may flag drift between disk state and JTBD docs exactly as the original verdict grammar describes — the change is on disk by construction; the not-yet-applied carve-out does not apply.
|
|
38
|
+
|
|
39
|
+
**Default when ambiguous.** When the calling prompt does not name the mode explicitly, default to **pre-edit mode** if a PreToolUse gate context is plausible (the prompt was likely fired by `jtbd-detect.sh` or an AFK iter dispatch). The pre-edit default is the safer fail-mode: a true post-edit drift will still surface as ISSUES FOUND / JOB UPDATE NEEDED on the substance; a true pre-edit proposal mis-classified as post-edit fires the P313 catch-22.
|
|
40
|
+
|
|
24
41
|
## What You Check
|
|
25
42
|
|
|
26
43
|
All review criteria come from the JTBD documentation. Read the docs first and apply them. Typical checks include:
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
# Doc-lint guard: jtbd agent.md must carry an explicit pre-edit /
|
|
3
|
+
# proposed-change review-mode carve-out so the agent classifies alignment
|
|
4
|
+
# of the PROPOSAL when the calling prompt describes a not-yet-applied
|
|
5
|
+
# change, instead of mis-classifying not-yet-applied state as ISSUES FOUND
|
|
6
|
+
# (P313 catch-22: gate blocks edits; reviewer wants edits done first).
|
|
7
|
+
#
|
|
8
|
+
# tdd-review: structural-permitted (justification: P176 — agent behaviour is
|
|
9
|
+
# prompt-driven with no skill-invocation harness to exercise the verdict
|
|
10
|
+
# behaviourally; ADR-052 Surface 2 structural-justified case, NOT an ADR-005
|
|
11
|
+
# Permitted Exception — ADR-052 narrows ADR-005 to exclude prose-doc greps).
|
|
12
|
+
# When P176 lands, upgrade to a behavioural test that feeds the agent a
|
|
13
|
+
# pre-edit proposal and asserts the PASS verdict.
|
|
14
|
+
#
|
|
15
|
+
# Cross-reference:
|
|
16
|
+
# P313 (Pre-edit governance-gate catch-22 — pass withheld pending edits)
|
|
17
|
+
# ADR-052 Surface 2 (structural-justified verdict) + P176 (harness gap)
|
|
18
|
+
# @jtbd JTBD-001 (Enforce Governance Without Slowing Down)
|
|
19
|
+
|
|
20
|
+
setup() {
|
|
21
|
+
AGENT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
22
|
+
AGENT_FILE="${AGENT_DIR}/agent.md"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@test "agent.md carries a Review Mode section distinguishing pre-edit from post-edit (P313)" {
|
|
26
|
+
run grep -nE "^## Review Mode: Pre-edit / proposed-change vs\. Post-edit / applied" "$AGENT_FILE"
|
|
27
|
+
[ "$status" -eq 0 ]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@test "agent.md classifies alignment of the PROPOSAL in pre-edit mode (P313 verbatim core sentence)" {
|
|
31
|
+
run grep -nE "classify alignment of the PROPOSAL itself" "$AGENT_FILE"
|
|
32
|
+
[ "$status" -eq 0 ]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@test "agent.md treats not-yet-applied state as the EXPECTED baseline of a pre-edit gate (P313)" {
|
|
36
|
+
run grep -nE "EXPECTED baseline of a pre-edit gate" "$AGENT_FILE"
|
|
37
|
+
[ "$status" -eq 0 ]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@test "agent.md explicitly rejects 'edits aren't applied yet' as a valid ISSUES FOUND substance (P313)" {
|
|
41
|
+
run grep -nE "Do NOT treat .edits aren't applied yet" "$AGENT_FILE"
|
|
42
|
+
[ "$status" -eq 0 ]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@test "agent.md cites P313 as the catch-22 closure (audit-trail)" {
|
|
46
|
+
run grep -nE "P313" "$AGENT_FILE"
|
|
47
|
+
[ "$status" -eq 0 ]
|
|
48
|
+
}
|
|
@@ -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
|
+
}
|