@windyroad/architect 0.5.1 → 0.5.2
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/README.md +1 -1
- package/hooks/architect-slide-marker.sh +29 -0
- package/hooks/hooks.json +2 -1
- package/hooks/lib/architect-gate.sh +1 -1
- package/hooks/lib/gate-helpers.sh +50 -0
- package/hooks/test/architect-slide-marker.bats +78 -0
- package/hooks/test/slide-marker-on-subprocess-return.bats +100 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ This walks you through creating an ADR in [MADR 4.0](https://adr.github.io/madr/
|
|
|
42
42
|
| `architect-detect.sh` | Every prompt | Checks for `docs/decisions/` and injects the review instruction |
|
|
43
43
|
| `architect-enforce-edit.sh` | Edit or Write | Blocks the edit if the architect hasn't reviewed yet |
|
|
44
44
|
| `architect-plan-enforce.sh` | ExitPlanMode | Ensures plans are reviewed before execution |
|
|
45
|
-
| `architect-mark-reviewed.sh` | Agent completes | Marks the review as done (TTL:
|
|
45
|
+
| `architect-mark-reviewed.sh` | Agent completes | Marks the review as done (TTL: 3600s) |
|
|
46
46
|
| `architect-refresh-hash.sh` | After edit | Refreshes the content hash so the next edit triggers a fresh review |
|
|
47
47
|
|
|
48
48
|
## Agent
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Architecture - PostToolUse:Agent|Bash slide-marker hook (P111).
|
|
3
|
+
# Slides the parent session's existing architect-reviewed marker forward on
|
|
4
|
+
# subprocess return, treating subprocess wall-clock as continuous parent-
|
|
5
|
+
# session work for TTL purposes. Only TOUCHES an existing marker — never
|
|
6
|
+
# creates one (creation requires a real architect review parsed from the
|
|
7
|
+
# agent's verdict text in architect-mark-reviewed.sh).
|
|
8
|
+
#
|
|
9
|
+
# This addresses P111 / ADR-009 "Subprocess-boundary refresh": Agent and Bash
|
|
10
|
+
# tool calls that wrap long-running subprocesses (other subagents, `claude
|
|
11
|
+
# -p` iteration subprocesses, run_in_background completions) would otherwise
|
|
12
|
+
# let the parent's marker age past TTL even though the parent is still
|
|
13
|
+
# actively working through the subprocess.
|
|
14
|
+
#
|
|
15
|
+
# Failed subprocesses (tool_response.is_error=true) do NOT extend the trust
|
|
16
|
+
# window — see slide_marker_on_subprocess_return in lib/gate-helpers.sh.
|
|
17
|
+
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
19
|
+
source "$SCRIPT_DIR/lib/gate-helpers.sh"
|
|
20
|
+
|
|
21
|
+
_parse_input
|
|
22
|
+
|
|
23
|
+
SESSION_ID=$(_get_session_id)
|
|
24
|
+
[ -n "$SESSION_ID" ] || exit 0
|
|
25
|
+
|
|
26
|
+
slide_marker_on_subprocess_return "/tmp/architect-reviewed-${SESSION_ID}"
|
|
27
|
+
slide_marker_on_subprocess_return "/tmp/architect-plan-reviewed-${SESSION_ID}"
|
|
28
|
+
|
|
29
|
+
exit 0
|
package/hooks/hooks.json
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
],
|
|
10
10
|
"PostToolUse": [
|
|
11
11
|
{ "matcher": "Agent", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-mark-reviewed.sh" }] },
|
|
12
|
-
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-refresh-hash.sh" }] }
|
|
12
|
+
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-refresh-hash.sh" }] },
|
|
13
|
+
{ "matcher": "Agent|Bash", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-slide-marker.sh" }] }
|
|
13
14
|
]
|
|
14
15
|
}
|
|
15
16
|
}
|
|
@@ -12,7 +12,7 @@ source "$_ARCHITECT_GATE_DIR/gate-helpers.sh"
|
|
|
12
12
|
check_architect_gate() {
|
|
13
13
|
local SESSION_ID="$1"
|
|
14
14
|
local MARKER="/tmp/architect-reviewed-${SESSION_ID}"
|
|
15
|
-
local TTL_SECONDS="${ARCHITECT_TTL:-
|
|
15
|
+
local TTL_SECONDS="${ARCHITECT_TTL:-3600}"
|
|
16
16
|
|
|
17
17
|
if [ -n "$SESSION_ID" ] && [ -f "$MARKER" ]; then
|
|
18
18
|
local NOW=$(date +%s)
|
|
@@ -153,6 +153,56 @@ _risk_dir() {
|
|
|
153
153
|
echo "$dir"
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Subprocess-completion marker slide (P111, ADR-009 amendment)
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
# Slides an existing session-review marker forward on subprocess return,
|
|
161
|
+
# treating subprocess wall-clock as continuous parent-session work for TTL
|
|
162
|
+
# purposes. Intended for PostToolUse hooks on Agent / Bash that may have
|
|
163
|
+
# been long-running subprocesses (Agent-tool delegations, `claude -p`
|
|
164
|
+
# iteration subprocesses, run_in_background completions).
|
|
165
|
+
#
|
|
166
|
+
# Contract:
|
|
167
|
+
# - Touches the marker ONLY if it already exists. NEVER creates a marker
|
|
168
|
+
# (creating requires a real gate review with verdict parsing).
|
|
169
|
+
# - Skips the touch if tool_response.is_error == true. A failed
|
|
170
|
+
# subprocess MUST NOT extend the parent's trust window.
|
|
171
|
+
# - Fail-safe on parse error: if _HOOK_INPUT cannot be parsed, treat as
|
|
172
|
+
# error and skip the touch.
|
|
173
|
+
# - No-op when marker path is empty or marker file does not exist.
|
|
174
|
+
#
|
|
175
|
+
# Why this is NOT cross-process marker sharing (ADR-032 line 123 invariant):
|
|
176
|
+
# the parent's PostToolUse hook touches the parent's OWN marker. The
|
|
177
|
+
# subprocess's session id, marker, and gate state are never read or shared.
|
|
178
|
+
# This is identical in shape to the existing PreToolUse:Edit slide; only
|
|
179
|
+
# the trigger expands to subprocess return.
|
|
180
|
+
#
|
|
181
|
+
# Usage: slide_marker_on_subprocess_return "/tmp/architect-reviewed-${SESSION_ID}"
|
|
182
|
+
slide_marker_on_subprocess_return() {
|
|
183
|
+
local MARKER="$1"
|
|
184
|
+
[ -n "$MARKER" ] || return 0
|
|
185
|
+
[ -f "$MARKER" ] || return 0
|
|
186
|
+
|
|
187
|
+
local IS_ERROR
|
|
188
|
+
IS_ERROR=$(echo "$_HOOK_INPUT" | python3 -c "
|
|
189
|
+
import sys, json
|
|
190
|
+
try:
|
|
191
|
+
data = json.load(sys.stdin)
|
|
192
|
+
tr = data.get('tool_response', {})
|
|
193
|
+
if isinstance(tr, dict):
|
|
194
|
+
print('true' if tr.get('is_error') is True else 'false')
|
|
195
|
+
else:
|
|
196
|
+
print('false')
|
|
197
|
+
except Exception:
|
|
198
|
+
print('true')
|
|
199
|
+
" 2>/dev/null || echo "true")
|
|
200
|
+
|
|
201
|
+
if [ "$IS_ERROR" = "false" ]; then
|
|
202
|
+
touch "$MARKER"
|
|
203
|
+
fi
|
|
204
|
+
}
|
|
205
|
+
|
|
156
206
|
# ---------------------------------------------------------------------------
|
|
157
207
|
# Non-doc file detection for WIP gating
|
|
158
208
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Hook-level integration tests for architect-slide-marker.sh (P111).
|
|
4
|
+
# Verifies that the PostToolUse:Agent|Bash hook correctly wires session_id
|
|
5
|
+
# extraction + slide_marker_on_subprocess_return for the architect markers.
|
|
6
|
+
|
|
7
|
+
setup() {
|
|
8
|
+
HOOKS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
9
|
+
HOOK="$HOOKS_DIR/architect-slide-marker.sh"
|
|
10
|
+
TEST_SESSION="bats-arch-slide-$$-${BATS_TEST_NUMBER}"
|
|
11
|
+
REVIEW_MARKER="/tmp/architect-reviewed-${TEST_SESSION}"
|
|
12
|
+
PLAN_MARKER="/tmp/architect-plan-reviewed-${TEST_SESSION}"
|
|
13
|
+
rm -f "$REVIEW_MARKER" "$PLAN_MARKER"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
teardown() {
|
|
17
|
+
rm -f "$REVIEW_MARKER" "$PLAN_MARKER"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_backdate() {
|
|
21
|
+
local file="$1" seconds="$2"
|
|
22
|
+
local stamp
|
|
23
|
+
stamp=$(date -v-${seconds}S +%Y%m%d%H%M.%S 2>/dev/null \
|
|
24
|
+
|| date -d "${seconds} seconds ago" +%Y%m%d%H%M.%S 2>/dev/null)
|
|
25
|
+
touch -t "$stamp" "$file"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@test "hook: slides architect-reviewed marker on subprocess return" {
|
|
29
|
+
touch "$REVIEW_MARKER"
|
|
30
|
+
_backdate "$REVIEW_MARKER" 60
|
|
31
|
+
BEFORE=$(stat -c%Y "$REVIEW_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$REVIEW_MARKER")
|
|
32
|
+
echo '{"session_id":"'"$TEST_SESSION"'","tool_name":"Agent","tool_response":{"content":[]}}' | "$HOOK"
|
|
33
|
+
AFTER=$(stat -c%Y "$REVIEW_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$REVIEW_MARKER")
|
|
34
|
+
[ "$AFTER" -gt "$BEFORE" ]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@test "hook: slides architect-plan-reviewed marker too" {
|
|
38
|
+
touch "$PLAN_MARKER"
|
|
39
|
+
_backdate "$PLAN_MARKER" 60
|
|
40
|
+
BEFORE=$(stat -c%Y "$PLAN_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$PLAN_MARKER")
|
|
41
|
+
echo '{"session_id":"'"$TEST_SESSION"'","tool_name":"Agent","tool_response":{"content":[]}}' | "$HOOK"
|
|
42
|
+
AFTER=$(stat -c%Y "$PLAN_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$PLAN_MARKER")
|
|
43
|
+
[ "$AFTER" -gt "$BEFORE" ]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@test "hook: skips slide when tool_response.is_error=true" {
|
|
47
|
+
touch "$REVIEW_MARKER"
|
|
48
|
+
_backdate "$REVIEW_MARKER" 60
|
|
49
|
+
BEFORE=$(stat -c%Y "$REVIEW_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$REVIEW_MARKER")
|
|
50
|
+
echo '{"session_id":"'"$TEST_SESSION"'","tool_name":"Bash","tool_response":{"is_error":true,"content":[]}}' | "$HOOK"
|
|
51
|
+
AFTER=$(stat -c%Y "$REVIEW_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$REVIEW_MARKER")
|
|
52
|
+
[ "$BEFORE" = "$AFTER" ]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@test "hook: no-op when no marker exists (never creates)" {
|
|
56
|
+
[ ! -f "$REVIEW_MARKER" ]
|
|
57
|
+
echo '{"session_id":"'"$TEST_SESSION"'","tool_name":"Agent","tool_response":{"content":[]}}' | "$HOOK"
|
|
58
|
+
[ ! -f "$REVIEW_MARKER" ]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@test "hook: exits 0 when session_id is missing" {
|
|
62
|
+
run bash -c 'echo "{}" | '"$HOOK"
|
|
63
|
+
[ "$status" -eq 0 ]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@test "hook: P111 reproduction — long subprocess does not cause parent marker expiry on return" {
|
|
67
|
+
touch "$REVIEW_MARKER"
|
|
68
|
+
# Marker is 50 minutes old, well within default 60-min TTL but close.
|
|
69
|
+
# Without the slide on subprocess return, a 15-min subprocess would push
|
|
70
|
+
# the next PreToolUse check past TTL. With the slide, the next check sees
|
|
71
|
+
# a fresh marker.
|
|
72
|
+
_backdate "$REVIEW_MARKER" 3000
|
|
73
|
+
echo '{"session_id":"'"$TEST_SESSION"'","tool_name":"Bash","tool_input":{"command":"claude -p ..."},"tool_response":{"content":[{"type":"text","text":"OK"}]}}' | "$HOOK"
|
|
74
|
+
NOW=$(date +%s)
|
|
75
|
+
AFTER=$(stat -c%Y "$REVIEW_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$REVIEW_MARKER")
|
|
76
|
+
AGE=$((NOW - AFTER))
|
|
77
|
+
[ "$AGE" -lt 5 ]
|
|
78
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Tests for slide_marker_on_subprocess_return helper (P111).
|
|
4
|
+
#
|
|
5
|
+
# Behavioural contract:
|
|
6
|
+
# - Slides an existing marker forward (touch) on PostToolUse:Agent|Bash
|
|
7
|
+
# completion, treating subprocess wall-clock as continuous parent-session
|
|
8
|
+
# work for TTL purposes.
|
|
9
|
+
# - Never CREATES a marker (creating requires a real gate review).
|
|
10
|
+
# - Skips slide on subprocess error (tool_response.is_error=true) so a failed
|
|
11
|
+
# subprocess does NOT extend the parent's trust window (ADR-009 amendment).
|
|
12
|
+
# - No-op when no marker exists or session_id is empty (fail-safe).
|
|
13
|
+
|
|
14
|
+
setup() {
|
|
15
|
+
HOOKS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
16
|
+
source "$HOOKS_DIR/lib/gate-helpers.sh"
|
|
17
|
+
|
|
18
|
+
TEST_SESSION="bats-slide-$$-${BATS_TEST_NUMBER}"
|
|
19
|
+
MARKER="/tmp/architect-reviewed-${TEST_SESSION}"
|
|
20
|
+
rm -f "$MARKER"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
teardown() {
|
|
24
|
+
rm -f "$MARKER"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Helper: backdate file mtime by N seconds (portable between macOS and Linux)
|
|
28
|
+
_backdate() {
|
|
29
|
+
local file="$1" seconds="$2"
|
|
30
|
+
local stamp
|
|
31
|
+
stamp=$(date -v-${seconds}S +%Y%m%d%H%M.%S 2>/dev/null \
|
|
32
|
+
|| date -d "${seconds} seconds ago" +%Y%m%d%H%M.%S 2>/dev/null)
|
|
33
|
+
touch -t "$stamp" "$file"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@test "slide: existing marker is touched on success response" {
|
|
37
|
+
touch "$MARKER"
|
|
38
|
+
_backdate "$MARKER" 60
|
|
39
|
+
BEFORE=$(_mtime "$MARKER")
|
|
40
|
+
_HOOK_INPUT='{"tool_response":{"content":[]}}'
|
|
41
|
+
slide_marker_on_subprocess_return "$MARKER"
|
|
42
|
+
AFTER=$(_mtime "$MARKER")
|
|
43
|
+
[ "$AFTER" -gt "$BEFORE" ]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@test "slide: long-running subprocess does NOT cause parent marker expiry on return (P111 reproduction)" {
|
|
47
|
+
# Simulate the P111 failure mode: parent's marker is set, then a long
|
|
48
|
+
# subprocess runs (we backdate the marker to simulate elapsed wall-clock),
|
|
49
|
+
# then PostToolUse fires with a successful tool_response. The marker mtime
|
|
50
|
+
# must be refreshed so the parent's NEXT PreToolUse gate check (which
|
|
51
|
+
# compares NOW - mtime against TTL) sees a fresh marker.
|
|
52
|
+
touch "$MARKER"
|
|
53
|
+
# Marker is 50 minutes old — under default 60-min TTL but close to expiry.
|
|
54
|
+
# Without the slide on subprocess return, a subsequent 15-min subprocess
|
|
55
|
+
# would push the mtime past TTL and the next PreToolUse would deny.
|
|
56
|
+
_backdate "$MARKER" 3000
|
|
57
|
+
BEFORE=$(_mtime "$MARKER")
|
|
58
|
+
_HOOK_INPUT='{"tool_response":{"content":[{"type":"text","text":"OK"}]}}'
|
|
59
|
+
slide_marker_on_subprocess_return "$MARKER"
|
|
60
|
+
AFTER=$(_mtime "$MARKER")
|
|
61
|
+
NOW=$(date +%s)
|
|
62
|
+
[ "$AFTER" -gt "$BEFORE" ]
|
|
63
|
+
# And the new mtime is approximately NOW (within 5 seconds of slide call)
|
|
64
|
+
AGE=$((NOW - AFTER))
|
|
65
|
+
[ "$AGE" -lt 5 ]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@test "slide: does NOT touch marker when tool_response.is_error=true" {
|
|
69
|
+
touch "$MARKER"
|
|
70
|
+
_backdate "$MARKER" 60
|
|
71
|
+
BEFORE=$(_mtime "$MARKER")
|
|
72
|
+
_HOOK_INPUT='{"tool_response":{"is_error":true,"content":[]}}'
|
|
73
|
+
slide_marker_on_subprocess_return "$MARKER"
|
|
74
|
+
AFTER=$(_mtime "$MARKER")
|
|
75
|
+
[ "$BEFORE" = "$AFTER" ]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@test "slide: no-op when marker does not exist (never creates)" {
|
|
79
|
+
[ ! -f "$MARKER" ]
|
|
80
|
+
_HOOK_INPUT='{"tool_response":{"content":[]}}'
|
|
81
|
+
slide_marker_on_subprocess_return "$MARKER"
|
|
82
|
+
[ ! -f "$MARKER" ]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@test "slide: no-op when marker path argument is empty" {
|
|
86
|
+
_HOOK_INPUT='{"tool_response":{"content":[]}}'
|
|
87
|
+
run slide_marker_on_subprocess_return ""
|
|
88
|
+
[ "$status" -eq 0 ]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@test "slide: malformed hook input is fail-safe (no slide)" {
|
|
92
|
+
touch "$MARKER"
|
|
93
|
+
_backdate "$MARKER" 60
|
|
94
|
+
BEFORE=$(_mtime "$MARKER")
|
|
95
|
+
_HOOK_INPUT='not valid json'
|
|
96
|
+
slide_marker_on_subprocess_return "$MARKER"
|
|
97
|
+
AFTER=$(_mtime "$MARKER")
|
|
98
|
+
# Fail-safe: when the hook input cannot be parsed, treat as error and skip
|
|
99
|
+
[ "$BEFORE" = "$AFTER" ]
|
|
100
|
+
}
|