@windyroad/architect 0.15.5 → 0.15.7
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/.claude-plugin/plugin.json +1 -1
- package/agents/agent.md +17 -0
- package/agents/test/architect-pre-edit-review-mode.bats +50 -0
- package/hooks/architect-enforce-edit.sh +37 -0
- package/hooks/lib/marker-only-diff.sh +83 -0
- package/hooks/test/architect-marker-only-exempt.bats +183 -0
- package/package.json +3 -2
- package/skills/capture-adr/SKILL.md +18 -0
- package/skills/create-adr/SKILL.md +39 -1
package/agents/agent.md
CHANGED
|
@@ -23,6 +23,23 @@ You are the Architect. You review proposed changes against the project's archite
|
|
|
23
23
|
4. Determine if the change requires a new decision to be documented
|
|
24
24
|
5. Report: PASS if compliant, or list issues requiring attention
|
|
25
25
|
|
|
26
|
+
## Review Mode: Pre-edit / proposed-change vs. Post-edit / applied
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
**Pre-edit mode (the default at a governance-gate firing).** The PreToolUse architect 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.
|
|
31
|
+
|
|
32
|
+
In pre-edit mode:
|
|
33
|
+
|
|
34
|
+
- 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).
|
|
35
|
+
- 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.
|
|
36
|
+
- PASS the review when the proposal aligns with existing decisions, the proposal does not require a new decision the user hasn't pinned, and the proposal's substance is sound. ISSUES FOUND on a pre-edit review must cite a problem with the **proposal**, not with the not-yet-applied-ness of the proposal.
|
|
37
|
+
- All other review machinery below (Decision Staleness, Existing Decision Compliance, Confirmation Criteria, New Decision Detection, Runtime-Path Performance, Decision Quality, Unratified Dependency, Needs Direction) 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.
|
|
38
|
+
|
|
39
|
+
**Post-edit mode (the explicit drift-detection or applied-change review).** The calling prompt asks you to verify already-applied edits against decisions — typically a `/wr-architect:review-design` 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 compliance", or "review the applied changes against …". In post-edit mode you may flag drift between disk state and decisions exactly as the original verdict grammar describes — the change is on disk by construction; the not-yet-applied carve-out does not apply.
|
|
40
|
+
|
|
41
|
+
**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 `architect-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 on the substance; a true pre-edit proposal mis-classified as post-edit fires the P313 catch-22.
|
|
42
|
+
|
|
26
43
|
## What You Check
|
|
27
44
|
|
|
28
45
|
### Decision Staleness Check
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
# Doc-lint guard: architect 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 — preserves the
|
|
19
|
+
# under-60-second review outcome by removing the redundant
|
|
20
|
+
# re-delegation the catch-22 currently forces)
|
|
21
|
+
|
|
22
|
+
setup() {
|
|
23
|
+
AGENT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
24
|
+
AGENT_FILE="${AGENT_DIR}/agent.md"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@test "agent.md carries a Review Mode section distinguishing pre-edit from post-edit (P313)" {
|
|
28
|
+
run grep -nE "^## Review Mode: Pre-edit / proposed-change vs\. Post-edit / applied" "$AGENT_FILE"
|
|
29
|
+
[ "$status" -eq 0 ]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@test "agent.md classifies alignment of the PROPOSAL in pre-edit mode (P313 verbatim core sentence)" {
|
|
33
|
+
run grep -nE "classify alignment of the PROPOSAL itself" "$AGENT_FILE"
|
|
34
|
+
[ "$status" -eq 0 ]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@test "agent.md treats not-yet-applied state as the EXPECTED baseline of a pre-edit gate (P313)" {
|
|
38
|
+
run grep -nE "EXPECTED baseline of a pre-edit gate" "$AGENT_FILE"
|
|
39
|
+
[ "$status" -eq 0 ]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@test "agent.md explicitly rejects 'edits aren't applied yet' as a valid ISSUES FOUND substance (P313)" {
|
|
43
|
+
run grep -nE "Do NOT treat .edits aren't applied yet" "$AGENT_FILE"
|
|
44
|
+
[ "$status" -eq 0 ]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@test "agent.md cites P313 as the catch-22 closure (audit-trail)" {
|
|
48
|
+
run grep -nE "P313" "$AGENT_FILE"
|
|
49
|
+
[ "$status" -eq 0 ]
|
|
50
|
+
}
|
|
@@ -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.
|
|
3
|
+
"version": "0.15.7",
|
|
4
4
|
"description": "Architecture decision enforcement for AI coding agents",
|
|
5
5
|
"bin": {
|
|
6
6
|
"windyroad-architect": "./bin/install.mjs"
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"skills/",
|
|
26
26
|
"scripts/",
|
|
27
27
|
".claude-plugin/",
|
|
28
|
-
"lib/"
|
|
28
|
+
"lib/",
|
|
29
|
+
"!skills/*/eval/"
|
|
29
30
|
]
|
|
30
31
|
}
|
|
@@ -49,6 +49,24 @@ Empty `$ARGUMENTS` halts per the Rule 6 audit above.
|
|
|
49
49
|
|
|
50
50
|
Derive a kebab-case title slug from the first 8-10 non-stopword tokens of the Title (matching the existing `create-adr` slug derivation pattern).
|
|
51
51
|
|
|
52
|
+
#### Title-as-outcome convention (P354)
|
|
53
|
+
|
|
54
|
+
ADR titles must name the **decision outcome** as a short noun phrase, not the question being decided. The title is the skim-surface for `docs/decisions/` and the ADR-077 compendium — readers should resolve what was decided from the title alone, without opening the file. User direction 2026-06-03 (P354): *"ADR titles are supposed to be the short version of what was decided, so they are skimmable."*
|
|
55
|
+
|
|
56
|
+
**GOOD** (outcome — short noun phrase; drawn from corpus): `marketplace-only-distribution`, `monorepo-per-plugin-packages`, `behavioural-tests-default-for-skill-testing`, `plugin-script-resolution-via-bin-on-path`, `every-fix-goes-through-an-rfc`.
|
|
57
|
+
|
|
58
|
+
**BAD** (question / option-pair / deliberation): `<X>-vs-<Y>` (option-pair), `should-<Z>` (deliberation), `whether-<Z>` (open question), `<X>-or-<Y>` (pure option-set).
|
|
59
|
+
|
|
60
|
+
In `capture-adr` the chosen Decision is pinned in `$ARGUMENTS` at invocation, so the caller SHOULD supply a Title already in outcome shape — the framework does not retitle here (the canonical-outcome short-name is the caller's to author, not the framework's to derive). If the parsed Title slug matches a question-shape pattern (`-vs-`, `should-`, `whether-`, `-or-`), emit an advisory in the I2-isomorphic shape via the shared `emit_stderr_advisory` helper:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
capture-adr: derived title='<slug>' from $ARGUMENTS — slug appears question-shaped (matched <pattern>); the Decision is pinned in $ARGUMENTS so an outcome-shaped Title is recommended; re-invoke with an outcome-shaped Title or rename the file at canonical expansion.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The advisory is **advisory-only** (no halt, no retitle). The caller may proceed with the question-shaped slug if they choose; the subsequent `/wr-architect:create-adr <NNN>` canonical-expansion pass picks up the retitle in its Step 5a mechanical retitle-after-decision check.
|
|
67
|
+
|
|
68
|
+
(Serves JTBD-001 — skimmable titles on the on-disk record; ADR-044 category-4 silent-framework — advisory, not ask.)
|
|
69
|
+
|
|
52
70
|
### 2. Compute the next ADR ID
|
|
53
71
|
|
|
54
72
|
Same P056-safe `local_max + origin_max + 1` formula as `/wr-architect:create-adr` Step 3:
|
|
@@ -34,7 +34,7 @@ Resolve each field via the following dispatch. **The order is load-bearing** —
|
|
|
34
34
|
|
|
35
35
|
| Field | Dispatch | ADR-044 category |
|
|
36
36
|
|-------|----------|------------------|
|
|
37
|
-
| **Title** | Derive silently. Kebab-case the first 8-10 non-stopword tokens of the user's prose problem-statement (same slug derivation as `/wr-itil:capture-problem` Step 1.4, `/wr-itil:manage-incident` Step 4, and `/wr-itil:manage-problem` Step 4 — uses the shared helper's `derive_kebab_slug` function). Emit stderr advisory: `create-adr: derived title='<slug>' from problem-statement; re-invoke with the desired title or rename the file if the slug is wrong`. Do NOT fire AskUserQuestion. | category-4 silent-framework |
|
|
37
|
+
| **Title** | Derive silently. Kebab-case the first 8-10 non-stopword tokens of the user's prose problem-statement (same slug derivation as `/wr-itil:capture-problem` Step 1.4, `/wr-itil:manage-incident` Step 4, and `/wr-itil:manage-problem` Step 4 — uses the shared helper's `derive_kebab_slug` function). At intake the derived slug typically encodes the **question** (the problem-statement is question-shaped); the title-as-outcome convention in Step 2a below names the GOOD/BAD shapes, and Step 5a's mechanical retitle-after-decision check renames the file to the chosen-option's outcome shape after substance-confirm passes. Emit stderr advisory: `create-adr: derived title='<slug>' from problem-statement; re-invoke with the desired title or rename the file if the slug is wrong`. Do NOT fire AskUserQuestion. | category-4 silent-framework |
|
|
38
38
|
| **status** (frontmatter) | Always `proposed` for new ADRs per Step 4 template convention. No ask, no advisory needed — SKILL convention is unambiguous. | category-4 silent-framework |
|
|
39
39
|
| **date** (frontmatter) | Today's date (`date +%Y-%m-%d`) per Step 4 template. No ask, no advisory needed — wall-clock derivation is unambiguous. | category-4 silent-framework |
|
|
40
40
|
| **reassessment-date** (frontmatter) | Today + 3 months (`date -v+3m +%Y-%m-%d` on BSD-date / `date -d '+3 months' +%Y-%m-%d` on GNU-date) per Step 4 template. Emit stderr advisory: `create-adr: derived reassessment-date='<YYYY-MM-DD>' from today+3-months default; re-invoke with --reassessment-date= or edit the frontmatter to override`. | category-4 silent-framework |
|
|
@@ -67,6 +67,30 @@ The advisory text shape is I2-isomorphic — same sentence structure across all
|
|
|
67
67
|
|
|
68
68
|
If the user has already provided context in `$ARGUMENTS` or earlier conversation, use what they've given and only fire AskUserQuestion for the cat-1 fields still missing.
|
|
69
69
|
|
|
70
|
+
### 2a. Title-as-outcome convention (P354)
|
|
71
|
+
|
|
72
|
+
ADR titles must name the **decision outcome** as a short noun phrase, not the question / option-pair being decided. The title is the skim-surface — a reader scanning `docs/decisions/` or the ADR-077 compendium should resolve what was decided from the title alone, without opening the file. User direction 2026-06-03 (P354): *"ADR titles are supposed to be the short version of what was decided, so they are skimmable. Titles like this force the reader to read the document to find the details of what was decided."*
|
|
73
|
+
|
|
74
|
+
**GOOD** (outcome — short noun phrase naming the decided thing; drawn from corpus):
|
|
75
|
+
|
|
76
|
+
- `marketplace-only-distribution`
|
|
77
|
+
- `monorepo-per-plugin-packages`
|
|
78
|
+
- `progressive-disclosure-for-governance-tooling-context`
|
|
79
|
+
- `behavioural-tests-default-for-skill-testing`
|
|
80
|
+
- `plugin-script-resolution-via-bin-on-path`
|
|
81
|
+
- `every-fix-goes-through-an-rfc`
|
|
82
|
+
|
|
83
|
+
**BAD** (question / option-pair / deliberation — reader must open file to learn outcome):
|
|
84
|
+
|
|
85
|
+
- `npm-release-auth-stored-token-vs-oidc` (option-pair pattern `-vs-`)
|
|
86
|
+
- `should-we-adopt-oidc-for-npm-release-auth` (deliberation pattern `should-`)
|
|
87
|
+
- `whether-to-monorepo-or-polyrepo` (open-question pattern `whether-`)
|
|
88
|
+
- `marketplace-or-direct-distribution` (pure option-set pattern `-or-`)
|
|
89
|
+
|
|
90
|
+
**At intake the derived title is acceptable in either shape**: Step 2's `derive_kebab_slug` runs against the problem-statement, which is typically question-shaped. The title-as-outcome convention is enforced at Step 5a's mechanical retitle-after-decision check (post substance-confirm, when the chosen option is locked in). The title need not be outcome-shaped before the decision is made.
|
|
91
|
+
|
|
92
|
+
(Serves JTBD-001 — skimmable titles speed the read path for the governance-enforcement persona.)
|
|
93
|
+
|
|
70
94
|
### 2b. Decision-boundary analysis (multi-decision check)
|
|
71
95
|
|
|
72
96
|
Before writing the ADR file, perform a decision-boundary analysis on the gathered context to prevent conflated ADRs that block independent status transitions and weaken auditability (P017).
|
|
@@ -253,6 +277,20 @@ This is NOT a soft "warn and proceed" path — the marker only ever writes when
|
|
|
253
277
|
|
|
254
278
|
**What the marker means.** This is the load-bearing born-confirmed gate: an ADR recorded through create-adr enters the world already human-oversighted (it does not appear in `/wr-architect:review-decisions`' unoversighted set) ONLY because the substance-confirm fire above explicitly affirmed the chosen option. Do NOT write the marker if the user has not confirmed substance (rejected / still-iterating ADRs stay unmarked). The marker is orthogonal to `status:` — a `proposed` ADR can be `human-oversight: confirmed`.
|
|
255
279
|
|
|
280
|
+
**Retitle-after-decision check (P354 — ADR-044 category-4 silent-framework).** After the marker write lands, check the on-disk filename slug for a question-shape pattern (`-vs-`, `should-`, `whether-`, `-or-`). If matched, the title was derived at intake against a question-shaped problem-statement and must be retitled to the chosen-option's outcome shape now that the substance is locked in. The convention is named in Step 2a above.
|
|
281
|
+
|
|
282
|
+
This step is **mechanical — no AskUserQuestion fires** (per P132 inverse-P078 guard). The chosen option is now known from the substance-confirm answer just above; derive the outcome slug from the chosen-option short name via the same `derive_kebab_slug` helper Step 2's Title derivation uses (`packages/architect/lib/derive-first-dispatch.sh`). Sequence (ordered to preserve marker-discipline hook semantics — the marker-introducing Edit must land BEFORE `git mv`):
|
|
283
|
+
|
|
284
|
+
1. Derive `new_slug = derive_kebab_slug "<chosen option short name>"`.
|
|
285
|
+
2. Edit the H1 in the on-disk file to the new outcome shape (H1 stays human-readable Title Case; the slug is for the filename). The `human-oversight: confirmed` line is already in `OLD_CONTENT` so `architect-oversight-marker-discipline.sh` allows this Edit per its "old content already had the marker" branch.
|
|
286
|
+
3. `git mv docs/decisions/<NNN>-<old-slug>.proposed.md docs/decisions/<NNN>-<new-slug>.proposed.md` (Bash command — no Edit/Write hook fires; rename is captured as a rename in git history).
|
|
287
|
+
4. Emit the I2-isomorphic stderr advisory: `create-adr: retitled <NNN>-<old-slug>.proposed.md -> <NNN>-<new-slug>.proposed.md from chosen-option '<short-name>'; git mv reversible via inverse rename.`
|
|
288
|
+
5. The subsequent compendium regen below picks up the new filename automatically.
|
|
289
|
+
|
|
290
|
+
If the on-disk slug does NOT match a question-shape pattern (already outcome-shaped at intake), skip this step silently — no advisory needed.
|
|
291
|
+
|
|
292
|
+
(Serves JTBD-001 — outcome-shaped on-disk title; category-4 silent-framework per ADR-044.)
|
|
293
|
+
|
|
256
294
|
#### 5b. Draft-quality review fire (optional, after 5a passes)
|
|
257
295
|
|
|
258
296
|
After the substance-confirm fire passes and the marker is written, fire a separate narrow `AskUserQuestion` for draft-quality review:
|