@windyroad/jtbd 0.11.0-preview.521 → 0.12.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-jtbd-mark-oversight-confirmed +51 -0
- package/hooks/hooks.json +2 -1
- package/hooks/jtbd-oversight-marker-discipline.sh +139 -0
- package/hooks/test/jtbd-oversight-marker-discipline.bats +184 -0
- package/package.json +1 -1
- package/scripts/mark-oversight-confirmed.sh +100 -0
- package/skills/update-guide/SKILL.md +12 -4
|
@@ -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" "$@"
|
package/hooks/hooks.json
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/jtbd-eval.sh" }] }
|
|
8
8
|
],
|
|
9
9
|
"PreToolUse": [
|
|
10
|
-
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/jtbd-enforce-edit.sh" }] }
|
|
10
|
+
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/jtbd-enforce-edit.sh" }] },
|
|
11
|
+
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/jtbd-oversight-marker-discipline.sh" }] }
|
|
11
12
|
],
|
|
12
13
|
"PostToolUse": [
|
|
13
14
|
{ "matcher": "Agent", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/jtbd-mark-reviewed.sh" }] },
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# jtbd-oversight-marker-discipline.sh — PreToolUse:Edit|Write hook
|
|
3
|
+
# (P348 / ADR-068 amendment 2026-06-02). JTBD-side sibling of
|
|
4
|
+
# architect-oversight-marker-discipline.sh. Denies Edit/Write operations
|
|
5
|
+
# that introduce `human-oversight: confirmed` into a docs/jtbd/ artefact's
|
|
6
|
+
# (persona.md or JTBD-NNN-*.md) frontmatter unless a session-scoped
|
|
7
|
+
# evidence marker proves the user has substance-confirmed THAT specific
|
|
8
|
+
# artefact via AskUserQuestion.
|
|
9
|
+
#
|
|
10
|
+
# P348 captured AFK iter subprocesses silently writing `human-oversight:
|
|
11
|
+
# confirmed` without any user confirmation event. ADR-068 mirrors ADR-066's
|
|
12
|
+
# marker contract on the JTBD/persona surface, and JTBD-006's audit-trail
|
|
13
|
+
# outcome + JTBD-201/202's auditability constraints depend on the marker
|
|
14
|
+
# being honest. This hook is the structural guard.
|
|
15
|
+
#
|
|
16
|
+
# Allow paths (exit 0 silently per ADR-045 Pattern 1):
|
|
17
|
+
# - tool_name not Edit|Write
|
|
18
|
+
# - file outside the project root (P004)
|
|
19
|
+
# - file not under docs/jtbd/
|
|
20
|
+
# - file not a `.md` (README, etc.)
|
|
21
|
+
# - basename README.md
|
|
22
|
+
# - the Edit/Write does NOT introduce `human-oversight: confirmed`
|
|
23
|
+
# - the session-scoped marker `/tmp/oversight-confirmed-<sha>-<sid>`
|
|
24
|
+
# exists for THIS specific JTBD/persona path under THIS session
|
|
25
|
+
#
|
|
26
|
+
# Deny path (PreToolUse deny JSON, hook exit 0):
|
|
27
|
+
# - all of: docs/jtbd/**/*.md, the change introduces
|
|
28
|
+
# `human-oversight: confirmed`, AND no matching marker for this
|
|
29
|
+
# artefact under this session
|
|
30
|
+
#
|
|
31
|
+
# Recovery (mechanical per ADR-013 Rule 1):
|
|
32
|
+
# The SKILL flow that hosts the substance-confirm AskUserQuestion calls
|
|
33
|
+
# `wr-jtbd-mark-oversight-confirmed <jtbd-or-persona-path>` immediately
|
|
34
|
+
# after the user's answer lands. AFK iter subprocesses MUST instead write
|
|
35
|
+
# `human-oversight: unconfirmed` (new enum value per ADR-068 amendment
|
|
36
|
+
# 2026-06-02), which the drain (/wr-jtbd:confirm-jobs-and-personas)
|
|
37
|
+
# later promotes interactively.
|
|
38
|
+
#
|
|
39
|
+
# @adr ADR-068 (JTBD/persona human-oversight marker)
|
|
40
|
+
# @adr ADR-066 (parent contract — architect surface)
|
|
41
|
+
# @adr ADR-049 (PATH shim resolution for invocation)
|
|
42
|
+
# @adr ADR-050 (multi-SID candidate enumeration for marker write)
|
|
43
|
+
# @adr ADR-045 (Pattern 1 silent-on-pass PreToolUse)
|
|
44
|
+
# @adr ADR-013 (Rule 6 fail-safe-defer in non-interactive contexts)
|
|
45
|
+
# @problem P348 (iter subprocesses set human-oversight: confirmed without user event)
|
|
46
|
+
|
|
47
|
+
set -uo pipefail
|
|
48
|
+
|
|
49
|
+
INPUT=$(cat)
|
|
50
|
+
|
|
51
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) || TOOL_NAME=""
|
|
52
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null) || FILE_PATH=""
|
|
53
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) || SESSION_ID=""
|
|
54
|
+
|
|
55
|
+
case "$TOOL_NAME" in
|
|
56
|
+
Edit|Write) ;;
|
|
57
|
+
*) exit 0 ;;
|
|
58
|
+
esac
|
|
59
|
+
|
|
60
|
+
[ -n "$FILE_PATH" ] || exit 0
|
|
61
|
+
[ -n "$SESSION_ID" ] || exit 0
|
|
62
|
+
|
|
63
|
+
case "$FILE_PATH" in
|
|
64
|
+
/*)
|
|
65
|
+
case "$FILE_PATH" in
|
|
66
|
+
"$PWD"/*) ;;
|
|
67
|
+
*) exit 0 ;;
|
|
68
|
+
esac
|
|
69
|
+
;;
|
|
70
|
+
esac
|
|
71
|
+
|
|
72
|
+
# Scope: docs/jtbd/**/*.md. Personas live at docs/jtbd/<persona>/persona.md;
|
|
73
|
+
# jobs live at docs/jtbd/<persona>/JTBD-NNN-*.md. Both are valid targets.
|
|
74
|
+
case "$FILE_PATH" in
|
|
75
|
+
*/docs/jtbd/*.md|docs/jtbd/*.md) ;;
|
|
76
|
+
*) exit 0 ;;
|
|
77
|
+
esac
|
|
78
|
+
|
|
79
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
80
|
+
case "$BASENAME" in
|
|
81
|
+
README.md) exit 0 ;;
|
|
82
|
+
esac
|
|
83
|
+
|
|
84
|
+
case "$TOOL_NAME" in
|
|
85
|
+
Write)
|
|
86
|
+
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null) || NEW_CONTENT=""
|
|
87
|
+
OLD_CONTENT=""
|
|
88
|
+
if [ -f "$FILE_PATH" ]; then
|
|
89
|
+
OLD_CONTENT=$(cat "$FILE_PATH" 2>/dev/null) || OLD_CONTENT=""
|
|
90
|
+
fi
|
|
91
|
+
;;
|
|
92
|
+
Edit)
|
|
93
|
+
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty' 2>/dev/null) || NEW_CONTENT=""
|
|
94
|
+
OLD_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.old_string // empty' 2>/dev/null) || OLD_CONTENT=""
|
|
95
|
+
;;
|
|
96
|
+
esac
|
|
97
|
+
|
|
98
|
+
MARKER_RE='^[[:space:]]*human-oversight:[[:space:]]*confirmed[[:space:]]*$'
|
|
99
|
+
|
|
100
|
+
if ! echo "$NEW_CONTENT" | grep -qiE "$MARKER_RE"; then
|
|
101
|
+
exit 0
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
if [ -n "$OLD_CONTENT" ] && echo "$OLD_CONTENT" | grep -qiE "$MARKER_RE"; then
|
|
105
|
+
exit 0
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
abs_dir=$(cd "$(dirname "$FILE_PATH")" 2>/dev/null && pwd) || abs_dir=""
|
|
109
|
+
if [ -n "$abs_dir" ]; then
|
|
110
|
+
ABS_PATH="$abs_dir/$(basename "$FILE_PATH")"
|
|
111
|
+
else
|
|
112
|
+
ABS_PATH="$FILE_PATH"
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
116
|
+
PATH_HASH=$(printf '%s' "$ABS_PATH" | sha256sum | cut -d' ' -f1 | cut -c1-16)
|
|
117
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
118
|
+
PATH_HASH=$(printf '%s' "$ABS_PATH" | shasum -a 256 | cut -d' ' -f1 | cut -c1-16)
|
|
119
|
+
else
|
|
120
|
+
exit 0
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
MARKER_DIR="${SESSION_MARKER_DIR:-/tmp}"
|
|
124
|
+
MARKER="$MARKER_DIR/oversight-confirmed-${PATH_HASH}-${SESSION_ID}"
|
|
125
|
+
|
|
126
|
+
if [ -f "$MARKER" ]; then
|
|
127
|
+
exit 0
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
cat <<EOF
|
|
131
|
+
{
|
|
132
|
+
"hookSpecificOutput": {
|
|
133
|
+
"hookEventName": "PreToolUse",
|
|
134
|
+
"permissionDecision": "deny",
|
|
135
|
+
"permissionDecisionReason": "BLOCKED: '${BASENAME}' is about to receive 'human-oversight: confirmed' but no substance-confirm evidence marker exists for this JTBD/persona in this session (P348 / ADR-068). The marker '/tmp/oversight-confirmed-<sha>-<sid>' is written by 'wr-jtbd-mark-oversight-confirmed <artefact-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-jtbd:confirm-jobs-and-personas) will promote it interactively. To recover this Edit/Write: surface the substance-confirm AskUserQuestion to the user, call wr-jtbd-mark-oversight-confirmed with this artefact's path, then retry."
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
EOF
|
|
139
|
+
exit 0
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# P348 / ADR-068 amendment 2026-06-02: jtbd-oversight-marker-discipline.sh
|
|
4
|
+
# is the JTBD-side sibling of architect-oversight-marker-discipline.sh.
|
|
5
|
+
# Denies Edit/Write that introduces `human-oversight: confirmed` into a
|
|
6
|
+
# docs/jtbd/ artefact's frontmatter unless a session-scoped evidence marker
|
|
7
|
+
# proves the user has substance-confirmed THAT artefact.
|
|
8
|
+
#
|
|
9
|
+
# Behavioural — exercises the hook with constructed PreToolUse stdin JSON
|
|
10
|
+
# payloads under SESSION_MARKER_DIR sandboxing.
|
|
11
|
+
|
|
12
|
+
setup() {
|
|
13
|
+
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
|
|
14
|
+
HOOK="$REPO_ROOT/packages/jtbd/hooks/jtbd-oversight-marker-discipline.sh"
|
|
15
|
+
MARK_SCRIPT="$REPO_ROOT/packages/jtbd/scripts/mark-oversight-confirmed.sh"
|
|
16
|
+
|
|
17
|
+
DIR="$(mktemp -d)"
|
|
18
|
+
mkdir -p "$DIR/docs/jtbd/developer"
|
|
19
|
+
MARK_DIR="$(mktemp -d)"
|
|
20
|
+
export SESSION_MARKER_DIR="$MARK_DIR"
|
|
21
|
+
|
|
22
|
+
ORIG_DIR="$PWD"
|
|
23
|
+
cd "$DIR"
|
|
24
|
+
|
|
25
|
+
SID="jtbd-discipline-test-$$"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
teardown() {
|
|
29
|
+
cd "$ORIG_DIR"
|
|
30
|
+
rm -rf "$DIR" "$MARK_DIR"
|
|
31
|
+
unset SESSION_MARKER_DIR
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
expected_marker() {
|
|
35
|
+
local f="$1"
|
|
36
|
+
local abs_dir abs path_hash
|
|
37
|
+
abs_dir="$(cd "$(dirname "$f")" && pwd)"
|
|
38
|
+
abs="$abs_dir/$(basename "$f")"
|
|
39
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
40
|
+
path_hash=$(printf '%s' "$abs" | sha256sum | cut -d' ' -f1 | cut -c1-16)
|
|
41
|
+
else
|
|
42
|
+
path_hash=$(printf '%s' "$abs" | shasum -a 256 | cut -d' ' -f1 | cut -c1-16)
|
|
43
|
+
fi
|
|
44
|
+
printf '%s/oversight-confirmed-%s-%s\n' "$MARK_DIR" "$path_hash" "$SID"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
mk_existing_artefact() {
|
|
48
|
+
local subpath="$1"; shift
|
|
49
|
+
local dir
|
|
50
|
+
dir="$(dirname "$DIR/docs/jtbd/$subpath")"
|
|
51
|
+
mkdir -p "$dir"
|
|
52
|
+
{
|
|
53
|
+
echo "---"
|
|
54
|
+
echo "status: \"proposed\""
|
|
55
|
+
echo "date: 2026-06-02"
|
|
56
|
+
for line in "$@"; do echo "$line"; done
|
|
57
|
+
echo "---"
|
|
58
|
+
echo "# $(basename "$subpath" .md)"
|
|
59
|
+
} > "$DIR/docs/jtbd/$subpath"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# ── Positive paths ───────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
@test "Write introducing 'human-oversight: confirmed' to a JTBD with marker present is allowed" {
|
|
65
|
+
mk_existing_artefact "developer/JTBD-300-test.proposed.md"
|
|
66
|
+
art="$DIR/docs/jtbd/developer/JTBD-300-test.proposed.md"
|
|
67
|
+
: > "$(expected_marker "$art")"
|
|
68
|
+
new_content=$'---\nstatus: "proposed"\ndate: 2026-06-02\nhuman-oversight: confirmed\noversight-date: 2026-06-02\n---\n\n# JTBD-300\n'
|
|
69
|
+
json=$(jq -nc --arg p "$art" --arg s "$SID" --arg c "$new_content" \
|
|
70
|
+
'{tool_name:"Write",session_id:$s,tool_input:{file_path:$p,content:$c}}')
|
|
71
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
72
|
+
[ "$status" -eq 0 ]
|
|
73
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@test "Edit to a persona introducing the marker with evidence is allowed" {
|
|
77
|
+
mk_existing_artefact "developer/persona.md"
|
|
78
|
+
art="$DIR/docs/jtbd/developer/persona.md"
|
|
79
|
+
: > "$(expected_marker "$art")"
|
|
80
|
+
old='date: 2026-06-02'
|
|
81
|
+
new=$'date: 2026-06-02\nhuman-oversight: confirmed\noversight-date: 2026-06-02'
|
|
82
|
+
json=$(jq -nc --arg p "$art" --arg s "$SID" --arg o "$old" --arg n "$new" \
|
|
83
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:$o,new_string:$n}}')
|
|
84
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
85
|
+
[ "$status" -eq 0 ]
|
|
86
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# ── Negative paths ───────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
@test "Write introducing 'human-oversight: confirmed' to a JTBD WITHOUT marker is denied" {
|
|
92
|
+
mk_existing_artefact "developer/JTBD-310-orphan.proposed.md"
|
|
93
|
+
art="$DIR/docs/jtbd/developer/JTBD-310-orphan.proposed.md"
|
|
94
|
+
new_content=$'---\nstatus: "proposed"\ndate: 2026-06-02\nhuman-oversight: confirmed\noversight-date: 2026-06-02\n---\n\n# JTBD-310\n'
|
|
95
|
+
json=$(jq -nc --arg p "$art" --arg s "$SID" --arg c "$new_content" \
|
|
96
|
+
'{tool_name:"Write",session_id:$s,tool_input:{file_path:$p,content:$c}}')
|
|
97
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
98
|
+
[ "$status" -eq 0 ]
|
|
99
|
+
[[ "$output" == *'"permissionDecision": "deny"'* ]]
|
|
100
|
+
[[ "$output" == *"BLOCKED"* ]]
|
|
101
|
+
[[ "$output" == *"wr-jtbd-mark-oversight-confirmed"* ]]
|
|
102
|
+
[[ "$output" == *"/wr-jtbd:confirm-jobs-and-personas"* ]]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@test "Edit introducing the marker to persona WITHOUT marker is denied" {
|
|
106
|
+
mk_existing_artefact "tech-lead/persona.md"
|
|
107
|
+
art="$DIR/docs/jtbd/tech-lead/persona.md"
|
|
108
|
+
old='date: 2026-06-02'
|
|
109
|
+
new=$'date: 2026-06-02\nhuman-oversight: confirmed'
|
|
110
|
+
json=$(jq -nc --arg p "$art" --arg s "$SID" --arg o "$old" --arg n "$new" \
|
|
111
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:$o,new_string:$n}}')
|
|
112
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
113
|
+
[[ "$output" == *'"permissionDecision": "deny"'* ]]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# ── AFK-unconfirmed-write ────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
@test "Write introducing 'human-oversight: unconfirmed' is allowed without marker (AFK path)" {
|
|
119
|
+
mk_existing_artefact "developer/JTBD-320-afk.proposed.md"
|
|
120
|
+
art="$DIR/docs/jtbd/developer/JTBD-320-afk.proposed.md"
|
|
121
|
+
new_content=$'---\nstatus: "proposed"\ndate: 2026-06-02\nhuman-oversight: unconfirmed\n---\n\n# JTBD-320-afk\n'
|
|
122
|
+
json=$(jq -nc --arg p "$art" --arg s "$SID" --arg c "$new_content" \
|
|
123
|
+
'{tool_name:"Write",session_id:$s,tool_input:{file_path:$p,content:$c}}')
|
|
124
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
125
|
+
[ "$status" -eq 0 ]
|
|
126
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# ── Scope / non-fire paths ───────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
@test "Edit to a non-JTBD path exits 0 silently even with the marker line" {
|
|
132
|
+
mkdir -p "$DIR/src"
|
|
133
|
+
echo "// stub" > "$DIR/src/foo.ts"
|
|
134
|
+
json=$(jq -nc --arg p "$DIR/src/foo.ts" --arg s "$SID" \
|
|
135
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:"// stub",new_string:"// human-oversight: confirmed"}}')
|
|
136
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
137
|
+
[ "$status" -eq 0 ]
|
|
138
|
+
[ -z "$output" ]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@test "Edit to docs/jtbd/README.md exits 0 silently" {
|
|
142
|
+
echo "# index" > "$DIR/docs/jtbd/README.md"
|
|
143
|
+
art="$DIR/docs/jtbd/README.md"
|
|
144
|
+
new=$'# index\nhuman-oversight: confirmed'
|
|
145
|
+
json=$(jq -nc --arg p "$art" --arg s "$SID" --arg n "$new" \
|
|
146
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:"# index",new_string:$n}}')
|
|
147
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
148
|
+
[ "$status" -eq 0 ]
|
|
149
|
+
[ -z "$output" ]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@test "Edit whose new content lacks the marker exits 0 silently" {
|
|
153
|
+
mk_existing_artefact "developer/JTBD-330-rename.proposed.md"
|
|
154
|
+
art="$DIR/docs/jtbd/developer/JTBD-330-rename.proposed.md"
|
|
155
|
+
json=$(jq -nc --arg p "$art" --arg s "$SID" \
|
|
156
|
+
'{tool_name:"Edit",session_id:$s,tool_input:{file_path:$p,old_string:"# JTBD-330-rename",new_string:"# JTBD-330 renamed"}}')
|
|
157
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
158
|
+
[ "$status" -eq 0 ]
|
|
159
|
+
[ -z "$output" ]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# ── End-to-end ───────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
@test "mark-oversight-confirmed.sh writes a marker that satisfies the JTBD hook" {
|
|
165
|
+
mk_existing_artefact "developer/JTBD-340-e2e.proposed.md"
|
|
166
|
+
art="$DIR/docs/jtbd/developer/JTBD-340-e2e.proposed.md"
|
|
167
|
+
: > "$MARK_DIR/jtbd-announced-$SID"
|
|
168
|
+
bash "$MARK_SCRIPT" "$art"
|
|
169
|
+
[ -f "$(expected_marker "$art")" ]
|
|
170
|
+
new_content=$'---\nstatus: "proposed"\ndate: 2026-06-02\nhuman-oversight: confirmed\noversight-date: 2026-06-02\n---\n\n# JTBD-340-e2e\n'
|
|
171
|
+
json=$(jq -nc --arg p "$art" --arg s "$SID" --arg c "$new_content" \
|
|
172
|
+
'{tool_name:"Write",session_id:$s,tool_input:{file_path:$p,content:$c}}')
|
|
173
|
+
run bash -c "echo '$(echo "$json" | sed "s/'/'\\\\''/g")' | bash '$HOOK'"
|
|
174
|
+
[ "$status" -eq 0 ]
|
|
175
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@test "tool_name=Bash exits 0 silently regardless of file path" {
|
|
179
|
+
json=$(jq -nc --arg s "$SID" \
|
|
180
|
+
'{tool_name:"Bash",session_id:$s,tool_input:{command:"echo human-oversight: confirmed"}}')
|
|
181
|
+
run bash -c "echo '$json' | bash '$HOOK'"
|
|
182
|
+
[ "$status" -eq 0 ]
|
|
183
|
+
[ -z "$output" ]
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# wr-jtbd — mark a JTBD/persona's human-oversight: confirmed marker write as
|
|
3
|
+
# user-substance-confirmed (P348 / ADR-068 amendment 2026-06-02).
|
|
4
|
+
#
|
|
5
|
+
# JTBD-side sibling of packages/architect/scripts/mark-oversight-confirmed.sh.
|
|
6
|
+
# Companion to jtbd-oversight-marker-discipline.sh (also P348). SKILLs invoke
|
|
7
|
+
# this script AFTER an AskUserQuestion lands the user's substance-confirm
|
|
8
|
+
# answer for a specific job or persona; the script writes the evidence marker
|
|
9
|
+
# that the hook reads to permit the subsequent Edit/Write that introduces
|
|
10
|
+
# `human-oversight: confirmed` into that artefact's frontmatter.
|
|
11
|
+
#
|
|
12
|
+
# Why the marker is required:
|
|
13
|
+
# ADR-068 mirrors ADR-066's `human-oversight: confirmed` marker contract on
|
|
14
|
+
# the JTBD/persona surface. P348 captured iter subprocesses silently
|
|
15
|
+
# writing the `confirmed` value without a user confirmation event,
|
|
16
|
+
# contradicting JTBD-006's audit-trail outcome and JTBD-201/202's
|
|
17
|
+
# auditability persona constraints. The hook enforces the boundary
|
|
18
|
+
# structurally; this script is the evidence-write side that legitimate
|
|
19
|
+
# substance-confirm flows use.
|
|
20
|
+
#
|
|
21
|
+
# AFK iter subprocesses with no AskUserQuestion access MUST write
|
|
22
|
+
# `human-oversight: unconfirmed` instead (the new enum value codified in
|
|
23
|
+
# ADR-068 amendment 2026-06-02), which the drain
|
|
24
|
+
# (/wr-jtbd:confirm-jobs-and-personas) later promotes.
|
|
25
|
+
#
|
|
26
|
+
# Marker convention:
|
|
27
|
+
# /tmp/oversight-confirmed-<sha256-of-path>-<session-id>
|
|
28
|
+
# Shared marker namespace with the architect hook — both hooks consume the
|
|
29
|
+
# same marker file shape. Written under EVERY recent candidate session SID
|
|
30
|
+
# per ADR-050 Option C.
|
|
31
|
+
#
|
|
32
|
+
# Usage:
|
|
33
|
+
# wr-jtbd-mark-oversight-confirmed <artefact-path>
|
|
34
|
+
# <artefact-path> — the JTBD or persona file path the user just
|
|
35
|
+
# substance-confirmed.
|
|
36
|
+
#
|
|
37
|
+
# Exit codes:
|
|
38
|
+
# 0 — marker(s) written for at least one candidate SID, OR no candidate
|
|
39
|
+
# SID was discoverable (cold-path: no announce markers yet).
|
|
40
|
+
# 2 — bad argument (missing or empty artefact-path).
|
|
41
|
+
#
|
|
42
|
+
# @adr ADR-068 (JTBD/persona 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-jtbd-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
|
+
abs_dir="$(cd "$(dirname "$ARTEFACT_PATH")" 2>/dev/null && pwd)" || abs_dir=""
|
|
59
|
+
if [ -n "$abs_dir" ]; then
|
|
60
|
+
ABS_PATH="$abs_dir/$(basename "$ARTEFACT_PATH")"
|
|
61
|
+
else
|
|
62
|
+
ABS_PATH="$ARTEFACT_PATH"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Path hash — sha256, first 16 hex chars. Portable across macOS (shasum) and
|
|
66
|
+
# Linux (sha256sum / shasum).
|
|
67
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
68
|
+
PATH_HASH=$(printf '%s' "$ABS_PATH" | sha256sum | cut -d' ' -f1 | cut -c1-16)
|
|
69
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
70
|
+
PATH_HASH=$(printf '%s' "$ABS_PATH" | shasum -a 256 | cut -d' ' -f1 | cut -c1-16)
|
|
71
|
+
else
|
|
72
|
+
echo "wr-jtbd-mark-oversight-confirmed: no sha256 tool available" >&2
|
|
73
|
+
exit 2
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
MARKER_DIR="${SESSION_MARKER_DIR:-/tmp}"
|
|
77
|
+
WINDOW_MINS="${SESSION_CANDIDATE_WINDOW_MINS:-1440}"
|
|
78
|
+
|
|
79
|
+
# Candidate SID enumeration — recent announce markers across all systems
|
|
80
|
+
# within the mtime window. Inlined for plugin self-containment (no cross-
|
|
81
|
+
# plugin lib source — jtbd must not depend on architect-internal helpers
|
|
82
|
+
# per ADR-002 plugin packaging).
|
|
83
|
+
candidates=$(
|
|
84
|
+
{
|
|
85
|
+
if [ -n "${CLAUDE_SESSION_ID:-}" ]; then
|
|
86
|
+
echo "$CLAUDE_SESSION_ID"
|
|
87
|
+
fi
|
|
88
|
+
find "$MARKER_DIR" -maxdepth 1 -name '*-announced-*' -mmin "-${WINDOW_MINS}" 2>/dev/null \
|
|
89
|
+
| sed 's|.*/||; s/.*-announced-//'
|
|
90
|
+
} | awk 'NF && !seen[$0]++'
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
[ -n "$candidates" ] || exit 0
|
|
94
|
+
|
|
95
|
+
while IFS= read -r sid; do
|
|
96
|
+
[ -n "$sid" ] || continue
|
|
97
|
+
: > "$MARKER_DIR/oversight-confirmed-${PATH_HASH}-${sid}"
|
|
98
|
+
done <<< "$candidates"
|
|
99
|
+
|
|
100
|
+
exit 0
|
|
@@ -89,14 +89,18 @@ Use AskUserQuestion to present the drafted personas and ask:
|
|
|
89
89
|
- Any missing user segments?
|
|
90
90
|
- Any constraints or pain points to add?
|
|
91
91
|
|
|
92
|
-
**Born-confirmed write (ADR-068).** Once the user confirms a persona via this AskUserQuestion pass,
|
|
92
|
+
**Born-confirmed write (ADR-068 — structurally gated by P348 amendment 2026-06-02).** Once the user confirms a persona via this AskUserQuestion pass, IMMEDIATELY call the marker-evidence helper THEN insert the two lines into that persona's frontmatter — after the `description:` line:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
wr-jtbd-mark-oversight-confirmed docs/jtbd/<persona-name>/persona.md
|
|
96
|
+
```
|
|
93
97
|
|
|
94
98
|
```yaml
|
|
95
99
|
human-oversight: confirmed
|
|
96
100
|
oversight-date: YYYY-MM-DD # today
|
|
97
101
|
```
|
|
98
102
|
|
|
99
|
-
This is the born-confirmed gate: a persona authored through update-guide enters the world
|
|
103
|
+
The `wr-jtbd-mark-oversight-confirmed` call writes the session-scoped evidence marker (`/tmp/oversight-confirmed-<sha>-<sid>`) that the `jtbd-oversight-marker-discipline.sh` PreToolUse hook reads to authorise the subsequent Edit/Write — without the helper call, the hook will DENY the marker write. This is the load-bearing born-confirmed gate: a persona authored through update-guide enters the world human-oversighted ONLY because the helper above paired the user's substance-confirm answer to the marker write. Do NOT write the marker for a persona the user has not confirmed. AFK iter subprocesses spawned via `claude -p` have no `AskUserQuestion` access; they MUST write `human-oversight: unconfirmed` (the AFK fallback enum value codified in ADR-068 amendment 2026-06-02), which the drain (`/wr-jtbd:confirm-jobs-and-personas`) later promotes interactively. The marker is orthogonal to status.
|
|
100
104
|
|
|
101
105
|
### 5. Draft jobs
|
|
102
106
|
|
|
@@ -144,14 +148,18 @@ Use AskUserQuestion to present the drafted jobs and ask:
|
|
|
144
148
|
- Do the job statements ring true?
|
|
145
149
|
- Any missing jobs or user flows?
|
|
146
150
|
|
|
147
|
-
**Born-confirmed write (ADR-068).** Once the user confirms a job via this AskUserQuestion pass,
|
|
151
|
+
**Born-confirmed write (ADR-068 — structurally gated by P348 amendment 2026-06-02).** Once the user confirms a job via this AskUserQuestion pass, IMMEDIATELY call the marker-evidence helper THEN insert the two lines into that job's frontmatter — after the `date-created:` line:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
wr-jtbd-mark-oversight-confirmed docs/jtbd/<persona-name>/JTBD-NNN-<kebab-title>.proposed.md
|
|
155
|
+
```
|
|
148
156
|
|
|
149
157
|
```yaml
|
|
150
158
|
human-oversight: confirmed
|
|
151
159
|
oversight-date: YYYY-MM-DD # today
|
|
152
160
|
```
|
|
153
161
|
|
|
154
|
-
A job authored through update-guide is born human-oversighted
|
|
162
|
+
Without the `wr-jtbd-mark-oversight-confirmed` call, the `jtbd-oversight-marker-discipline.sh` PreToolUse hook will DENY the marker write. A job authored through update-guide is born human-oversighted ONLY because the helper above paired the user's substance-confirm answer to the marker write. Do NOT write the marker for a job the user has not confirmed. AFK iter subprocesses MUST write `human-oversight: unconfirmed` instead (the AFK fallback enum value codified in ADR-068 amendment 2026-06-02); the drain (`/wr-jtbd:confirm-jobs-and-personas`) later promotes it interactively. The marker is orthogonal to `status:`.
|
|
155
163
|
|
|
156
164
|
### 7. Generate README.md index
|
|
157
165
|
|