@windyroad/architect 0.14.1-preview.521 → 0.15.0
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/bin/wr-architect-mark-oversight-confirmed +51 -0
- package/hooks/architect-oversight-marker-discipline.sh +163 -0
- package/hooks/hooks.json +1 -0
- package/hooks/test/architect-oversight-marker-discipline.bats +210 -0
- package/package.json +1 -1
- package/scripts/mark-oversight-confirmed.sh +108 -0
- package/skills/capture-adr/SKILL.md +3 -0
- package/skills/create-adr/SKILL.md +7 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Generated by scripts/sync-shim-wrappers.sh from
|
|
3
|
+
# packages/shared/lib/shim-wrapper-template.sh. DO NOT EDIT individual
|
|
4
|
+
# shim files in packages/*/bin/wr-* directly; edit the template + run
|
|
5
|
+
# `npm run sync:shim-wrappers` to regenerate.
|
|
6
|
+
#
|
|
7
|
+
# Resolution (ADR-080):
|
|
8
|
+
# 1. If the wrapper's parent dir is semver-shaped, treat as installed-
|
|
9
|
+
# cache execution and resolve to the highest-version sibling's
|
|
10
|
+
# scripts/ entry below.
|
|
11
|
+
# 2. Otherwise (parent dir is e.g. `architect`), treat as source-
|
|
12
|
+
# monorepo execution and dispatch to own scripts/. The source-repo-
|
|
13
|
+
# guard `exec` is the anchor parsed by
|
|
14
|
+
# packages/retrospective/scripts/check-tarball-shipped-shims.sh.
|
|
15
|
+
# 3. If the cache parent contains zero semver-shaped siblings, exit
|
|
16
|
+
# 127 with a stderr message naming the cache parent (per SQ-080-2).
|
|
17
|
+
#
|
|
18
|
+
# @adr ADR-080 (highest-version-wins shim wrapper plugin scaffold)
|
|
19
|
+
# @adr ADR-049 (plugin-bundled scripts resolve via bin/ on $PATH — amended)
|
|
20
|
+
# @problem P343 (mid-session staleness window)
|
|
21
|
+
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
SHIM_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
25
|
+
OWN_VERSION_DIR="$(dirname "$SHIM_DIR")"
|
|
26
|
+
OWN_VERSION_NAME="$(basename "$OWN_VERSION_DIR")"
|
|
27
|
+
CACHE_PARENT="$(dirname "$OWN_VERSION_DIR")"
|
|
28
|
+
|
|
29
|
+
SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
|
|
30
|
+
|
|
31
|
+
# Source-repo guard: own parent dir is NOT semver → dispatch to own scripts/.
|
|
32
|
+
if ! [[ "$OWN_VERSION_NAME" =~ $SEMVER_RE ]]; then
|
|
33
|
+
exec "$SHIM_DIR/../scripts/mark-oversight-confirmed.sh" "$@"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Cache execution: pick the highest-semver sibling under CACHE_PARENT.
|
|
37
|
+
HIGHEST=""
|
|
38
|
+
while IFS= read -r dir; do
|
|
39
|
+
name="$(basename "$dir")"
|
|
40
|
+
[[ "$name" =~ $SEMVER_RE ]] || continue
|
|
41
|
+
if [[ -z "$HIGHEST" ]] || [[ "$(printf '%s\n%s\n' "$HIGHEST" "$name" | sort -V | tail -1)" == "$name" ]]; then
|
|
42
|
+
HIGHEST="$name"
|
|
43
|
+
fi
|
|
44
|
+
done < <(find "$CACHE_PARENT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
|
|
45
|
+
|
|
46
|
+
if [[ -z "$HIGHEST" ]]; then
|
|
47
|
+
printf 'wr-shim: no cached versions in %s\n' "$CACHE_PARENT" >&2
|
|
48
|
+
exit 127
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
exec "$CACHE_PARENT/$HIGHEST/scripts/mark-oversight-confirmed.sh" "$@"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# architect-oversight-marker-discipline.sh — PreToolUse:Edit|Write hook
|
|
3
|
+
# (P348 / ADR-066 amendment 2026-06-02). Denies Edit/Write operations that
|
|
4
|
+
# introduce `human-oversight: confirmed` into a docs/decisions/ ADR's
|
|
5
|
+
# frontmatter unless a session-scoped evidence marker proves the user has
|
|
6
|
+
# substance-confirmed THAT specific ADR via AskUserQuestion.
|
|
7
|
+
#
|
|
8
|
+
# P348 captured AFK iter subprocesses silently writing `human-oversight:
|
|
9
|
+
# confirmed` without any user confirmation event — contradicting ADR-066's
|
|
10
|
+
# write-once-permanent-on-substance-confirm contract and JTBD-006's audit-
|
|
11
|
+
# trail outcome. This hook is the structural guard. The discipline-prose
|
|
12
|
+
# already in /wr-architect:create-adr (substance-confirm at Step 5,
|
|
13
|
+
# P340-tightened by ADR-066 Amendment 2026-05-31) is the SKILL-level
|
|
14
|
+
# expression; this hook elevates it to a structurally enforced boundary
|
|
15
|
+
# AFK iter subprocesses cannot bypass.
|
|
16
|
+
#
|
|
17
|
+
# Allow paths (exit 0 silently per ADR-045 Pattern 1):
|
|
18
|
+
# - tool_name not Edit|Write
|
|
19
|
+
# - file outside the project root (P004)
|
|
20
|
+
# - file not under docs/decisions/
|
|
21
|
+
# - file not a `.md` (README, changelogs, etc.)
|
|
22
|
+
# - the Edit/Write does NOT introduce the literal frontmatter line
|
|
23
|
+
# `human-oversight: confirmed` (e.g. introduces `unconfirmed` or
|
|
24
|
+
# `rejected-pending-supersede`, leaves the marker untouched, etc.)
|
|
25
|
+
# - the session-scoped marker `/tmp/oversight-confirmed-<sha>-<sid>`
|
|
26
|
+
# exists for THIS specific ADR path under THIS session ID
|
|
27
|
+
#
|
|
28
|
+
# Deny path (PreToolUse deny JSON, hook exit 0):
|
|
29
|
+
# - all of: docs/decisions/*.md, the change introduces
|
|
30
|
+
# `human-oversight: confirmed`, AND no matching marker for this ADR
|
|
31
|
+
# under this session
|
|
32
|
+
#
|
|
33
|
+
# Recovery (mechanical per ADR-013 Rule 1):
|
|
34
|
+
# The SKILL flow that hosts the substance-confirm AskUserQuestion calls
|
|
35
|
+
# `wr-architect-mark-oversight-confirmed <adr-path>` immediately after
|
|
36
|
+
# the user's answer lands. The next Edit/Write of that ADR is then
|
|
37
|
+
# allowed. AFK iter subprocesses MUST instead write
|
|
38
|
+
# `human-oversight: unconfirmed` (new enum value per ADR-066 amendment
|
|
39
|
+
# 2026-06-02), which the drain (/wr-architect:review-decisions) later
|
|
40
|
+
# promotes interactively.
|
|
41
|
+
#
|
|
42
|
+
# Resolution (ADR-049 amended): PATH-shim grammar — the SKILL invokes
|
|
43
|
+
# `wr-architect-mark-oversight-confirmed`, which resolves through the
|
|
44
|
+
# highest-version-wins shim wrapper.
|
|
45
|
+
#
|
|
46
|
+
# @adr ADR-066 (human-oversight marker — write-once-permanent contract)
|
|
47
|
+
# @adr ADR-068 (sibling JTBD/persona contract)
|
|
48
|
+
# @adr ADR-049 (PATH shim resolution for invocation)
|
|
49
|
+
# @adr ADR-050 (multi-SID candidate enumeration for marker write)
|
|
50
|
+
# @adr ADR-045 (Pattern 1 silent-on-pass PreToolUse)
|
|
51
|
+
# @adr ADR-013 (Rule 6 fail-safe-defer in non-interactive contexts)
|
|
52
|
+
# @problem P348 (iter subprocesses set human-oversight: confirmed without user event)
|
|
53
|
+
|
|
54
|
+
set -uo pipefail
|
|
55
|
+
|
|
56
|
+
INPUT=$(cat)
|
|
57
|
+
|
|
58
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) || TOOL_NAME=""
|
|
59
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null) || FILE_PATH=""
|
|
60
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) || SESSION_ID=""
|
|
61
|
+
|
|
62
|
+
# Tool gate: only Edit and Write.
|
|
63
|
+
case "$TOOL_NAME" in
|
|
64
|
+
Edit|Write) ;;
|
|
65
|
+
*) exit 0 ;;
|
|
66
|
+
esac
|
|
67
|
+
|
|
68
|
+
# Missing inputs — allow (fail-open is acceptable here because the
|
|
69
|
+
# architect-enforce-edit gate is the outer perimeter that catches missing
|
|
70
|
+
# session_id with fail-closed deny; this discipline is a refinement).
|
|
71
|
+
[ -n "$FILE_PATH" ] || exit 0
|
|
72
|
+
[ -n "$SESSION_ID" ] || exit 0
|
|
73
|
+
|
|
74
|
+
# P004: only gate files inside the project root.
|
|
75
|
+
case "$FILE_PATH" in
|
|
76
|
+
/*)
|
|
77
|
+
case "$FILE_PATH" in
|
|
78
|
+
"$PWD"/*) ;;
|
|
79
|
+
*) exit 0 ;;
|
|
80
|
+
esac
|
|
81
|
+
;;
|
|
82
|
+
esac
|
|
83
|
+
|
|
84
|
+
# Scope: only docs/decisions/<NNN>-*.md and per-state subdir layouts.
|
|
85
|
+
case "$FILE_PATH" in
|
|
86
|
+
*/docs/decisions/*.md|docs/decisions/*.md) ;;
|
|
87
|
+
*) exit 0 ;;
|
|
88
|
+
esac
|
|
89
|
+
|
|
90
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
91
|
+
# Skip README + non-ADR auxiliary files.
|
|
92
|
+
case "$BASENAME" in
|
|
93
|
+
README.md) exit 0 ;;
|
|
94
|
+
esac
|
|
95
|
+
|
|
96
|
+
# Extract the candidate new-content. For Write, that's the full
|
|
97
|
+
# `tool_input.content`. For Edit, that's `tool_input.new_string`.
|
|
98
|
+
case "$TOOL_NAME" in
|
|
99
|
+
Write)
|
|
100
|
+
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null) || NEW_CONTENT=""
|
|
101
|
+
OLD_CONTENT=""
|
|
102
|
+
if [ -f "$FILE_PATH" ]; then
|
|
103
|
+
OLD_CONTENT=$(cat "$FILE_PATH" 2>/dev/null) || OLD_CONTENT=""
|
|
104
|
+
fi
|
|
105
|
+
;;
|
|
106
|
+
Edit)
|
|
107
|
+
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty' 2>/dev/null) || NEW_CONTENT=""
|
|
108
|
+
OLD_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.old_string // empty' 2>/dev/null) || OLD_CONTENT=""
|
|
109
|
+
;;
|
|
110
|
+
esac
|
|
111
|
+
|
|
112
|
+
# Does the new content introduce `human-oversight: confirmed`?
|
|
113
|
+
# Token-cheap grep — single literal line, case-insensitive, tolerant of
|
|
114
|
+
# trailing whitespace (mirroring detect-unoversighted.sh's match grammar).
|
|
115
|
+
MARKER_RE='^[[:space:]]*human-oversight:[[:space:]]*confirmed[[:space:]]*$'
|
|
116
|
+
|
|
117
|
+
if ! echo "$NEW_CONTENT" | grep -qiE "$MARKER_RE"; then
|
|
118
|
+
exit 0 # new content does not contain the confirmed marker → allow
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# If the OLD content already had the marker, this Edit/Write is not
|
|
122
|
+
# INTRODUCING it (could be reformatting or a no-op on this line). Allow.
|
|
123
|
+
if [ -n "$OLD_CONTENT" ] && echo "$OLD_CONTENT" | grep -qiE "$MARKER_RE"; then
|
|
124
|
+
exit 0
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# Now we know: the change INTRODUCES `human-oversight: confirmed`. Require
|
|
128
|
+
# the session-scoped evidence marker for THIS ADR's absolute path.
|
|
129
|
+
abs_dir=$(cd "$(dirname "$FILE_PATH")" 2>/dev/null && pwd) || abs_dir=""
|
|
130
|
+
if [ -n "$abs_dir" ]; then
|
|
131
|
+
ABS_PATH="$abs_dir/$(basename "$FILE_PATH")"
|
|
132
|
+
else
|
|
133
|
+
ABS_PATH="$FILE_PATH"
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
137
|
+
PATH_HASH=$(printf '%s' "$ABS_PATH" | sha256sum | cut -d' ' -f1 | cut -c1-16)
|
|
138
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
139
|
+
PATH_HASH=$(printf '%s' "$ABS_PATH" | shasum -a 256 | cut -d' ' -f1 | cut -c1-16)
|
|
140
|
+
else
|
|
141
|
+
# No hashing tool available — fail-open. Unrealistic on macOS/Linux but
|
|
142
|
+
# do not block the user over an environment edge case.
|
|
143
|
+
exit 0
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
MARKER_DIR="${SESSION_MARKER_DIR:-/tmp}"
|
|
147
|
+
MARKER="$MARKER_DIR/oversight-confirmed-${PATH_HASH}-${SESSION_ID}"
|
|
148
|
+
|
|
149
|
+
if [ -f "$MARKER" ]; then
|
|
150
|
+
exit 0 # evidence present → allow
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# Deny.
|
|
154
|
+
cat <<EOF
|
|
155
|
+
{
|
|
156
|
+
"hookSpecificOutput": {
|
|
157
|
+
"hookEventName": "PreToolUse",
|
|
158
|
+
"permissionDecision": "deny",
|
|
159
|
+
"permissionDecisionReason": "BLOCKED: '${BASENAME}' is about to receive 'human-oversight: confirmed' but no substance-confirm evidence marker exists for this ADR in this session (P348 / ADR-066). The marker '/tmp/oversight-confirmed-<sha>-<sid>' is written by 'wr-architect-mark-oversight-confirmed <adr-path>' immediately after an AskUserQuestion lands the user's substance-confirm answer. If you are an AFK iter subprocess without AskUserQuestion access, write 'human-oversight: unconfirmed' instead — the drain (/wr-architect:review-decisions) will promote it interactively. To recover this Edit/Write: surface the substance-confirm AskUserQuestion to the user, call wr-architect-mark-oversight-confirmed with this ADR's path, then retry."
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
EOF
|
|
163
|
+
exit 0
|
package/hooks/hooks.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
],
|
|
9
9
|
"PreToolUse": [
|
|
10
10
|
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-enforce-edit.sh" }] },
|
|
11
|
+
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-oversight-marker-discipline.sh" }] },
|
|
11
12
|
{ "matcher": "ExitPlanMode", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-plan-enforce.sh" }] },
|
|
12
13
|
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-compendium-refresh-discipline.sh" }] }
|
|
13
14
|
],
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# P348 / ADR-066 amendment 2026-06-02: architect-oversight-marker-discipline.sh
|
|
4
|
+
# is a PreToolUse:Edit|Write hook that denies any Edit/Write that introduces
|
|
5
|
+
# `human-oversight: confirmed` into a docs/decisions/ ADR's frontmatter unless
|
|
6
|
+
# a session-scoped evidence marker `/tmp/oversight-confirmed-<sha>-<sid>`
|
|
7
|
+
# exists for THAT specific ADR under THIS session.
|
|
8
|
+
#
|
|
9
|
+
# Behavioural — exercises the hook with constructed PreToolUse stdin JSON
|
|
10
|
+
# payloads under SESSION_MARKER_DIR sandboxing, asserts on stdout+exit.
|
|
11
|
+
|
|
12
|
+
setup() {
|
|
13
|
+
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
|
|
14
|
+
HOOK="$REPO_ROOT/packages/architect/hooks/architect-oversight-marker-discipline.sh"
|
|
15
|
+
MARK_SCRIPT="$REPO_ROOT/packages/architect/scripts/mark-oversight-confirmed.sh"
|
|
16
|
+
|
|
17
|
+
DIR="$(mktemp -d)"
|
|
18
|
+
mkdir -p "$DIR/docs/decisions"
|
|
19
|
+
MARK_DIR="$(mktemp -d)"
|
|
20
|
+
export SESSION_MARKER_DIR="$MARK_DIR"
|
|
21
|
+
|
|
22
|
+
ORIG_DIR="$PWD"
|
|
23
|
+
cd "$DIR"
|
|
24
|
+
|
|
25
|
+
SID="discipline-test-$$"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
teardown() {
|
|
29
|
+
cd "$ORIG_DIR"
|
|
30
|
+
rm -rf "$DIR" "$MARK_DIR"
|
|
31
|
+
unset SESSION_MARKER_DIR
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Helper: compute the marker path the hook expects for an ADR file.
|
|
35
|
+
expected_marker() {
|
|
36
|
+
local f="$1"
|
|
37
|
+
local abs_dir abs path_hash
|
|
38
|
+
abs_dir="$(cd "$(dirname "$f")" && pwd)"
|
|
39
|
+
abs="$abs_dir/$(basename "$f")"
|
|
40
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
41
|
+
path_hash=$(printf '%s' "$abs" | sha256sum | cut -d' ' -f1 | cut -c1-16)
|
|
42
|
+
else
|
|
43
|
+
path_hash=$(printf '%s' "$abs" | shasum -a 256 | cut -d' ' -f1 | cut -c1-16)
|
|
44
|
+
fi
|
|
45
|
+
printf '%s/oversight-confirmed-%s-%s\n' "$MARK_DIR" "$path_hash" "$SID"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
mk_existing_adr() {
|
|
49
|
+
local name="$1"; shift
|
|
50
|
+
{
|
|
51
|
+
echo "---"
|
|
52
|
+
echo "status: \"proposed\""
|
|
53
|
+
echo "date: 2026-06-02"
|
|
54
|
+
for line in "$@"; do echo "$line"; done
|
|
55
|
+
echo "---"
|
|
56
|
+
echo "# $name"
|
|
57
|
+
} > "$DIR/docs/decisions/$name"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# ── Positive paths (marker present → allow) ──────────────────────────────
|
|
61
|
+
|
|
62
|
+
@test "Write introducing 'human-oversight: confirmed' with marker present is allowed" {
|
|
63
|
+
mk_existing_adr "200-unconfirmed.proposed.md"
|
|
64
|
+
adr="$DIR/docs/decisions/200-unconfirmed.proposed.md"
|
|
65
|
+
: > "$(expected_marker "$adr")"
|
|
66
|
+
new_content=$'---\nstatus: "proposed"\ndate: 2026-06-02\nhuman-oversight: confirmed\noversight-date: 2026-06-02\n---\n\n# 200-unconfirmed\n'
|
|
67
|
+
json=$(jq -nc --arg p "$adr" --arg s "$SID" --arg c "$new_content" \
|
|
68
|
+
'{tool_name:"Write",session_id:$s,tool_input:{file_path:$p,content:$c}}')
|
|
69
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
70
|
+
[ "$status" -eq 0 ]
|
|
71
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@test "Edit introducing 'human-oversight: confirmed' with marker present is allowed" {
|
|
75
|
+
mk_existing_adr "201-pending.proposed.md"
|
|
76
|
+
adr="$DIR/docs/decisions/201-pending.proposed.md"
|
|
77
|
+
: > "$(expected_marker "$adr")"
|
|
78
|
+
old='date: 2026-06-02'
|
|
79
|
+
new=$'date: 2026-06-02\nhuman-oversight: confirmed\noversight-date: 2026-06-02'
|
|
80
|
+
json=$(jq -nc --arg p "$adr" --arg s "$SID" --arg o "$old" --arg n "$new" \
|
|
81
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:$o,new_string:$n}}')
|
|
82
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
83
|
+
[ "$status" -eq 0 ]
|
|
84
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# ── Negative paths (no marker → deny) ───────────────────────────────────
|
|
88
|
+
|
|
89
|
+
@test "Write introducing 'human-oversight: confirmed' WITHOUT marker is denied" {
|
|
90
|
+
mk_existing_adr "210-orphan.proposed.md"
|
|
91
|
+
adr="$DIR/docs/decisions/210-orphan.proposed.md"
|
|
92
|
+
new_content=$'---\nstatus: "proposed"\ndate: 2026-06-02\nhuman-oversight: confirmed\noversight-date: 2026-06-02\n---\n\n# 210-orphan\n'
|
|
93
|
+
json=$(jq -nc --arg p "$adr" --arg s "$SID" --arg c "$new_content" \
|
|
94
|
+
'{tool_name:"Write",session_id:$s,tool_input:{file_path:$p,content:$c}}')
|
|
95
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
96
|
+
[ "$status" -eq 0 ]
|
|
97
|
+
[[ "$output" == *'"permissionDecision": "deny"'* ]]
|
|
98
|
+
[[ "$output" == *"BLOCKED"* ]]
|
|
99
|
+
[[ "$output" == *"oversight-confirmed-"* ]]
|
|
100
|
+
[[ "$output" == *"wr-architect-mark-oversight-confirmed"* ]]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@test "Edit introducing 'human-oversight: confirmed' WITHOUT marker is denied" {
|
|
104
|
+
mk_existing_adr "211-no-evidence.proposed.md"
|
|
105
|
+
adr="$DIR/docs/decisions/211-no-evidence.proposed.md"
|
|
106
|
+
old='date: 2026-06-02'
|
|
107
|
+
new=$'date: 2026-06-02\nhuman-oversight: confirmed\noversight-date: 2026-06-02'
|
|
108
|
+
json=$(jq -nc --arg p "$adr" --arg s "$SID" --arg o "$old" --arg n "$new" \
|
|
109
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:$o,new_string:$n}}')
|
|
110
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
111
|
+
[[ "$output" == *'"permissionDecision": "deny"'* ]]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# ── AFK-unconfirmed-write path (no marker required) ──────────────────────
|
|
115
|
+
|
|
116
|
+
@test "Write introducing 'human-oversight: unconfirmed' is allowed without marker (AFK path)" {
|
|
117
|
+
mk_existing_adr "220-afk.proposed.md"
|
|
118
|
+
adr="$DIR/docs/decisions/220-afk.proposed.md"
|
|
119
|
+
new_content=$'---\nstatus: "proposed"\ndate: 2026-06-02\nhuman-oversight: unconfirmed\n---\n\n# 220-afk\n'
|
|
120
|
+
json=$(jq -nc --arg p "$adr" --arg s "$SID" --arg c "$new_content" \
|
|
121
|
+
'{tool_name:"Write",session_id:$s,tool_input:{file_path:$p,content:$c}}')
|
|
122
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
123
|
+
[ "$status" -eq 0 ]
|
|
124
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@test "Write introducing rejected-pending-supersede is allowed without marker" {
|
|
128
|
+
mk_existing_adr "221-rejected.proposed.md"
|
|
129
|
+
adr="$DIR/docs/decisions/221-rejected.proposed.md"
|
|
130
|
+
new_content=$'---\nstatus: "proposed"\ndate: 2026-06-02\nhuman-oversight: rejected-pending-supersede\nsupersede-ticket: P999\n---\n\n# 221-rejected\n'
|
|
131
|
+
json=$(jq -nc --arg p "$adr" --arg s "$SID" --arg c "$new_content" \
|
|
132
|
+
'{tool_name:"Write",session_id:$s,tool_input:{file_path:$p,content:$c}}')
|
|
133
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
134
|
+
[ "$status" -eq 0 ]
|
|
135
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# ── Scope / non-fire paths ────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
@test "Edit to a non-ADR path (src/index.ts) exits 0 silently" {
|
|
141
|
+
mkdir -p "$DIR/src"
|
|
142
|
+
echo "// stub" > "$DIR/src/index.ts"
|
|
143
|
+
json=$(jq -nc --arg p "$DIR/src/index.ts" --arg s "$SID" \
|
|
144
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:"// stub",new_string:"// human-oversight: confirmed"}}')
|
|
145
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
146
|
+
[ "$status" -eq 0 ]
|
|
147
|
+
[ -z "$output" ]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@test "Edit to docs/decisions/README.md exits 0 silently" {
|
|
151
|
+
echo "# index" > "$DIR/docs/decisions/README.md"
|
|
152
|
+
adr="$DIR/docs/decisions/README.md"
|
|
153
|
+
new=$'# index\nhuman-oversight: confirmed'
|
|
154
|
+
json=$(jq -nc --arg p "$adr" --arg s "$SID" --arg n "$new" \
|
|
155
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:"# index",new_string:$n}}')
|
|
156
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
157
|
+
[ "$status" -eq 0 ]
|
|
158
|
+
[ -z "$output" ]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@test "Edit whose new content lacks the marker exits 0 silently" {
|
|
162
|
+
mk_existing_adr "230-unrelated.proposed.md"
|
|
163
|
+
adr="$DIR/docs/decisions/230-unrelated.proposed.md"
|
|
164
|
+
json=$(jq -nc --arg p "$adr" --arg s "$SID" \
|
|
165
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:"# 230-unrelated",new_string:"# 230 renamed"}}')
|
|
166
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
167
|
+
[ "$status" -eq 0 ]
|
|
168
|
+
[ -z "$output" ]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@test "Write whose OLD content already had the marker (re-stamp / no-op) is allowed without re-evidencing" {
|
|
172
|
+
# Pre-existing ADR already carries the marker; new content keeps it.
|
|
173
|
+
mk_existing_adr "240-already.proposed.md" "human-oversight: confirmed" "oversight-date: 2026-05-30"
|
|
174
|
+
adr="$DIR/docs/decisions/240-already.proposed.md"
|
|
175
|
+
new_content=$'---\nstatus: "accepted"\ndate: 2026-06-02\nhuman-oversight: confirmed\noversight-date: 2026-05-30\n---\n\n# 240-already\n'
|
|
176
|
+
json=$(jq -nc --arg p "$adr" --arg s "$SID" --arg c "$new_content" \
|
|
177
|
+
'{tool_name:"Write",session_id:$s,tool_input:{file_path:$p,content:$c}}')
|
|
178
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
179
|
+
[ "$status" -eq 0 ]
|
|
180
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# ── End-to-end: mark-oversight-confirmed.sh + the hook ──────────────────
|
|
184
|
+
|
|
185
|
+
@test "mark-oversight-confirmed.sh writes a marker that satisfies the hook" {
|
|
186
|
+
mk_existing_adr "250-e2e.proposed.md"
|
|
187
|
+
adr="$DIR/docs/decisions/250-e2e.proposed.md"
|
|
188
|
+
# Seed an announce marker so candidate enumeration finds the SID.
|
|
189
|
+
: > "$MARK_DIR/architect-announced-$SID"
|
|
190
|
+
bash "$MARK_SCRIPT" "$adr"
|
|
191
|
+
# The mark script writes under every candidate; the hook's expected marker
|
|
192
|
+
# filename for our SID must now exist.
|
|
193
|
+
[ -f "$(expected_marker "$adr")" ]
|
|
194
|
+
new_content=$'---\nstatus: "proposed"\ndate: 2026-06-02\nhuman-oversight: confirmed\noversight-date: 2026-06-02\n---\n\n# 250-e2e\n'
|
|
195
|
+
json=$(jq -nc --arg p "$adr" --arg s "$SID" --arg c "$new_content" \
|
|
196
|
+
'{tool_name:"Write",session_id:$s,tool_input:{file_path:$p,content:$c}}')
|
|
197
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
198
|
+
[ "$status" -eq 0 ]
|
|
199
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# ── Non-Edit/Write tool calls always exit 0 silently ────────────────────
|
|
203
|
+
|
|
204
|
+
@test "tool_name=Bash exits 0 silently regardless of file path" {
|
|
205
|
+
json=$(jq -nc --arg s "$SID" \
|
|
206
|
+
'{tool_name:"Bash",session_id:$s,tool_input:{command:"echo human-oversight: confirmed"}}')
|
|
207
|
+
run bash -c "echo '$json' | bash '$HOOK'"
|
|
208
|
+
[ "$status" -eq 0 ]
|
|
209
|
+
[ -z "$output" ]
|
|
210
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# wr-architect — mark a decision/ADR's human-oversight: confirmed marker write
|
|
3
|
+
# as user-substance-confirmed (P348 / ADR-066 amendment 2026-06-02).
|
|
4
|
+
#
|
|
5
|
+
# Companion to the architect-oversight-marker-discipline.sh PreToolUse hook
|
|
6
|
+
# (also P348). SKILLs invoke this script AFTER an AskUserQuestion lands the
|
|
7
|
+
# user's substance-confirm answer for a specific ADR; the script writes the
|
|
8
|
+
# evidence marker that the hook reads to permit the subsequent Edit/Write
|
|
9
|
+
# that introduces `human-oversight: confirmed` into that ADR's frontmatter.
|
|
10
|
+
#
|
|
11
|
+
# Why the marker is required:
|
|
12
|
+
# ADR-066 establishes that `human-oversight: confirmed` is a write-once-
|
|
13
|
+
# permanent durable record. ADR-074 + P340 tighten the substance-confirm
|
|
14
|
+
# semantics: the marker MAY ONLY land in response to a user answer that
|
|
15
|
+
# selects a specific option. AFK iter subprocesses have no AskUserQuestion
|
|
16
|
+
# access (ADR-013 Rule 6 fail-safe-defer territory), so they MUST NOT write
|
|
17
|
+
# the `confirmed` value — they write `human-oversight: unconfirmed` instead,
|
|
18
|
+
# which the drain (/wr-architect:review-decisions) later promotes. The hook
|
|
19
|
+
# enforces the boundary structurally; this script is the evidence-write
|
|
20
|
+
# side that legitimate substance-confirm flows use.
|
|
21
|
+
#
|
|
22
|
+
# Marker convention:
|
|
23
|
+
# /tmp/oversight-confirmed-<sha256-of-path>-<session-id>
|
|
24
|
+
# Written under EVERY recent candidate session SID per ADR-050 Option C
|
|
25
|
+
# (concurrent orchestrator + subprocess sessions in the same project, the
|
|
26
|
+
# per-machine runtime-sid marker is last-writer-wins). The PreToolUse hook
|
|
27
|
+
# reads the SID from its stdin JSON; marking under every candidate
|
|
28
|
+
# guarantees a matching marker exists whichever SID the hook reads.
|
|
29
|
+
#
|
|
30
|
+
# Usage:
|
|
31
|
+
# wr-architect-mark-oversight-confirmed <artefact-path>
|
|
32
|
+
# <artefact-path> — the ADR file path the user just substance-confirmed.
|
|
33
|
+
# The script computes sha256 of the absolute path.
|
|
34
|
+
#
|
|
35
|
+
# Exit codes:
|
|
36
|
+
# 0 — marker(s) written for at least one candidate SID, OR no candidate
|
|
37
|
+
# SID was discoverable (cold-path: no announce markers yet). The
|
|
38
|
+
# latter is a no-op so SKILL flows do not crash before any hook has
|
|
39
|
+
# fired in the session.
|
|
40
|
+
# 2 — bad argument (missing or empty artefact-path).
|
|
41
|
+
#
|
|
42
|
+
# @adr ADR-066 (human-oversight marker)
|
|
43
|
+
# @adr ADR-049 (PATH shim grammar)
|
|
44
|
+
# @adr ADR-050 (multi-SID candidate enumeration)
|
|
45
|
+
# @adr ADR-013 (Rule 6 fail-safe-defer in non-interactive contexts)
|
|
46
|
+
# @problem P348 (iter subprocesses set human-oversight: confirmed without user event)
|
|
47
|
+
|
|
48
|
+
set -uo pipefail
|
|
49
|
+
|
|
50
|
+
ARTEFACT_PATH="${1:-}"
|
|
51
|
+
|
|
52
|
+
if [ -z "$ARTEFACT_PATH" ]; then
|
|
53
|
+
echo "wr-architect-mark-oversight-confirmed: missing <artefact-path>" >&2
|
|
54
|
+
exit 2
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Normalize to absolute path so the hash is stable regardless of CWD.
|
|
58
|
+
# `cd $(dirname)` works whether the file exists or not (basename + abs dir).
|
|
59
|
+
abs_dir="$(cd "$(dirname "$ARTEFACT_PATH")" 2>/dev/null && pwd)" || abs_dir=""
|
|
60
|
+
if [ -n "$abs_dir" ]; then
|
|
61
|
+
ABS_PATH="$abs_dir/$(basename "$ARTEFACT_PATH")"
|
|
62
|
+
else
|
|
63
|
+
ABS_PATH="$ARTEFACT_PATH"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Path hash — use shasum/sha256sum portably (macOS ships shasum; Linux usually
|
|
67
|
+
# has both). First 16 hex chars are plenty for unique marker filenames.
|
|
68
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
69
|
+
PATH_HASH=$(printf '%s' "$ABS_PATH" | sha256sum | cut -d' ' -f1 | cut -c1-16)
|
|
70
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
71
|
+
PATH_HASH=$(printf '%s' "$ABS_PATH" | shasum -a 256 | cut -d' ' -f1 | cut -c1-16)
|
|
72
|
+
else
|
|
73
|
+
echo "wr-architect-mark-oversight-confirmed: no sha256 tool available" >&2
|
|
74
|
+
exit 2
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
MARKER_DIR="${SESSION_MARKER_DIR:-/tmp}"
|
|
78
|
+
WINDOW_MINS="${SESSION_CANDIDATE_WINDOW_MINS:-1440}"
|
|
79
|
+
|
|
80
|
+
# Candidate SID enumeration — recent announce markers across all systems
|
|
81
|
+
# within the mtime window. Mirrors get_candidate_session_ids in
|
|
82
|
+
# packages/itil/hooks/lib/session-id.sh; inlined here so this script is
|
|
83
|
+
# self-contained (no cross-plugin lib source — architect must not depend on
|
|
84
|
+
# itil-internal helpers per ADR-002 plugin packaging).
|
|
85
|
+
candidates=$(
|
|
86
|
+
{
|
|
87
|
+
# Env-var fast path. Not exported in agent contexts today, but if a
|
|
88
|
+
# future Claude Code release adds it, this branch picks it up for free.
|
|
89
|
+
if [ -n "${CLAUDE_SESSION_ID:-}" ]; then
|
|
90
|
+
echo "$CLAUDE_SESSION_ID"
|
|
91
|
+
fi
|
|
92
|
+
# Recent announce markers.
|
|
93
|
+
find "$MARKER_DIR" -maxdepth 1 -name '*-announced-*' -mmin "-${WINDOW_MINS}" 2>/dev/null \
|
|
94
|
+
| sed 's|.*/||; s/.*-announced-//'
|
|
95
|
+
} | awk 'NF && !seen[$0]++'
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# No candidate SID — cold path. Exit 0 so SKILL flows do not crash before any
|
|
99
|
+
# hook has fired this session. The subsequent Write deny will naturally
|
|
100
|
+
# surface the missing-marker case to the agent.
|
|
101
|
+
[ -n "$candidates" ] || exit 0
|
|
102
|
+
|
|
103
|
+
while IFS= read -r sid; do
|
|
104
|
+
[ -n "$sid" ] || continue
|
|
105
|
+
: > "$MARKER_DIR/oversight-confirmed-${PATH_HASH}-${sid}"
|
|
106
|
+
done <<< "$candidates"
|
|
107
|
+
|
|
108
|
+
exit 0
|
|
@@ -73,6 +73,7 @@ Log the renumber decision in the operation report if origin and local diverged.
|
|
|
73
73
|
---
|
|
74
74
|
status: "proposed"
|
|
75
75
|
date: <YYYY-MM-DD>
|
|
76
|
+
human-oversight: unconfirmed
|
|
76
77
|
decision-makers: [unspecified — fill at canonical review]
|
|
77
78
|
consulted: []
|
|
78
79
|
informed: []
|
|
@@ -180,6 +181,8 @@ The trailing pointer is **not optional** — it is the user-visible signal that
|
|
|
180
181
|
|
|
181
182
|
**Confirm-every-ADR gate (ADR-064):** a capture-adr skeleton is recorded `proposed` with a pre-pinned decision but WITHOUT human review of the options. It must NOT be promoted to `accepted` until it has been through a `/wr-architect:create-adr` (or equivalent) `AskUserQuestion` review-and-confirm pass. Capture records the decision quickly; the confirm — not the capture — is what gives it human oversight. This is prong 1 of P283 (lift auto-/quick-recorded decisions to human-confirmed before they stand).
|
|
182
183
|
|
|
184
|
+
**Oversight marker discipline (ADR-066 amendment 2026-06-02 / P348).** A capture-adr skeleton MUST be born `human-oversight: unconfirmed` — NOT `confirmed`. Capture is the AFK-friendly aside surface; there is no substance-confirm `AskUserQuestion` pass in this flow, so `confirmed` would be a hollow marker (the P348 bug class). The `architect-oversight-marker-discipline.sh` PreToolUse hook will DENY any Edit/Write that introduces `human-oversight: confirmed` without a matching session-scoped evidence marker. The frontmatter skeleton (Step 3 above) MUST include `human-oversight: unconfirmed` so the ADR enters the world honestly self-identified as needing user confirmation. The drain (`/wr-architect:review-decisions`) and the canonical-expansion path (`/wr-architect:create-adr <NNN>`) are the surfaces that legitimately promote it to `confirmed` via `wr-architect-mark-oversight-confirmed` + the gated marker write.
|
|
185
|
+
|
|
183
186
|
## Composition with create-adr
|
|
184
187
|
|
|
185
188
|
| Concern | create-adr | capture-adr |
|
|
@@ -225,13 +225,19 @@ options:
|
|
|
225
225
|
- ...one entry per considered option
|
|
226
226
|
```
|
|
227
227
|
|
|
228
|
-
**Born-confirmed marker write (ADR-066 — tightened by P340 amendment).** The marker write fires ONLY when the substance-confirm answer specifies a substantive option from the considered-options set AND that option matches the option the draft was authored against. On a substantive match,
|
|
228
|
+
**Born-confirmed marker write (ADR-066 — tightened by P340 amendment + structurally gated by P348 amendment 2026-06-02).** The marker write fires ONLY when the substance-confirm answer specifies a substantive option from the considered-options set AND that option matches the option the draft was authored against. On a substantive match, IMMEDIATELY call the marker-evidence helper THEN insert the two lines:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
wr-architect-mark-oversight-confirmed docs/decisions/<NNN>-<slug>.proposed.md
|
|
232
|
+
```
|
|
229
233
|
|
|
230
234
|
```yaml
|
|
231
235
|
human-oversight: confirmed
|
|
232
236
|
oversight-date: YYYY-MM-DD # today
|
|
233
237
|
```
|
|
234
238
|
|
|
239
|
+
The `wr-architect-mark-oversight-confirmed` call writes the session-scoped evidence marker (`/tmp/oversight-confirmed-<sha>-<sid>`) that the `architect-oversight-marker-discipline.sh` PreToolUse hook reads to authorise the subsequent Edit/Write — without the helper call, the hook will DENY the marker write. AFK iter subprocesses spawned via `claude -p` have no `AskUserQuestion` access; they MUST write `human-oversight: unconfirmed` instead (the AFK fallback enum value codified in ADR-066 amendment 2026-06-02), which the drain (`/wr-architect:review-decisions`) later promotes interactively. Calling the helper without a real user substance-confirm event is the P348 hollow-marker bug — every legitimate marker write traces back to an `AskUserQuestion` answer in the same turn.
|
|
240
|
+
|
|
235
241
|
**Mismatch handling.** If the substance-confirm answer selects a DIFFERENT option than the draft was authored against:
|
|
236
242
|
|
|
237
243
|
- DO NOT write the marker.
|