@thebassclef/lite 1.0.2 → 1.0.3
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/dist/cli.cjs +24 -1
- package/dist/cli.js +24 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/hooks/session-reflection.d/00-bassclef-hook-connect.sh +63 -0
- package/dist/lite/.claude/hooks/session-reflection.d/05-active-bet.sh +113 -0
- package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +62 -0
- package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +78 -0
- package/dist/lite/.claude/hooks/session-reflection.d/12-session-lock.sh +63 -0
- package/dist/lite/.claude/hooks/session-reflection.d/15-substrate-config-validate.sh +91 -0
- package/dist/lite/.claude/hooks/session-reflection.d/20-artifact-staleness.sh +98 -0
- package/dist/lite/.claude/hooks/session-reflection.d/30-metrics-staleness.sh +61 -0
- package/dist/lite/.claude/hooks/session-reflection.d/35-post-merge-whereami-self-heal.sh +180 -0
- package/dist/lite/.claude/hooks/session-reflection.d/36-whereami-size-check.sh +117 -0
- package/dist/lite/.claude/hooks/session-reflection.d/40-gate-compliance.sh +108 -0
- package/dist/lite/.claude/hooks/session-reflection.d/45-auto-sync-status.sh +99 -0
- package/dist/lite/.claude/hooks/session-reflection.d/50-evolution-issues.sh +44 -0
- package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +187 -0
- package/dist/lite/.claude/hooks/session-reflection.d/56-roadmap-reconcile-gate.sh +88 -0
- package/dist/lite/.claude/hooks/session-reflection.d/60-deferred-actions.sh +212 -0
- package/dist/lite/.claude/hooks/session-reflection.d/70-recent-strategic-artifacts.sh +101 -0
- package/dist/lite/.claude/hooks/session-reflection.d/75-prior-r8-findings.sh +100 -0
- package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +87 -0
- package/dist/lite/.claude/hooks/session-reflection.d/80-workflow-staleness.sh +141 -0
- package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +67 -0
- package/dist/lite/.claude/hooks/session-reflection.d/90-release-backlog.sh +140 -0
- package/dist/lite/.claude/hooks/session-reflection.d/90-secrets-expiry.sh +104 -0
- package/dist/lite/.claude/hooks/session-reflection.d/95-settings-hook-verify.sh +131 -0
- package/dist/lite/.claude/hooks/session-reflection.d/95-telemetry-emit.sh +66 -0
- package/dist/lite/.claude/hooks/session-reflection.d/97-install-class-dispatch.sh +57 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/00-bassclef-hook-connect.test.sh +492 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/05-active-bet.test.sh +336 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/12-session-lock.test.sh +216 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/15-substrate-config-validate.test.sh +50 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/20-artifact-staleness.test.sh +348 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/36-whereami-size-check.test.sh +189 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/45-auto-sync-status.test.sh +263 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/55-orientation-gate.test.sh +595 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/56-roadmap-reconcile-gate.test.sh +227 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/75-prior-r8-findings.test.sh +142 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/90-release-backlog.test.sh +44 -0
- package/dist/lite/.claude/hooks/trace-helper.sh +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# tier: lite
|
|
2
|
+
# 55-orientation-gate.sh
|
|
3
|
+
#
|
|
4
|
+
# Per bassclef#1183 + bassclef#1266 + bassclef#1275: mechanical enforcement
|
|
5
|
+
# at SessionStart in bassclef AND every adopter repo. Fires a BLOCKED block
|
|
6
|
+
# (per .claude/rules/blocked-items.md) when the agent is on a feature branch
|
|
7
|
+
# AND no orientation marker exists for that branch. /sprint + /longrun prep
|
|
8
|
+
# procedures (WU-3/WU-4) then refuse to propose work until the operator +
|
|
9
|
+
# agent have oriented at the arc level — read the active bet's parent_drivers,
|
|
10
|
+
# walked up parent_bet → parent_roadmap, scanned open bassclef-evolution
|
|
11
|
+
# issues across the current repo + any sibling repos named in substrate.config.md.
|
|
12
|
+
#
|
|
13
|
+
# This closes the 2026-06-20 failure pattern: agent narrows scope to the
|
|
14
|
+
# nearest leaf ticket without reading the parent thread carrying 30+ in-flight
|
|
15
|
+
# items. Adopters inherit the same discipline (bassclef#1275 — sister
|
|
16
|
+
# pattern "rule propagates, mechanism doesn't" surfaced in the very session
|
|
17
|
+
# that shipped this hook for bassclef-only).
|
|
18
|
+
#
|
|
19
|
+
# Behavior:
|
|
20
|
+
# - Fires in bassclef AND adopter repos (no consumer-repo guard since bassclef#1275)
|
|
21
|
+
# - Fires on main branch too per architecture/audits/2026-06-30-session-orientation-blind-spot.md
|
|
22
|
+
# cure 2 (sister cures: 05-active-bet.sh + /longrun SKILL Step 0.4)
|
|
23
|
+
# - Silent when state/markers/orientation-gate/<branch-slug>.marker exists
|
|
24
|
+
# (on main, the slug is literally "main")
|
|
25
|
+
# - Silent when SKIP_ORIENTATION_GATE=1 (logged via trace-helper)
|
|
26
|
+
# - Silent when SKIP_ORIENTATION_GATE_ON_MAIN=1 AND branch is main/master
|
|
27
|
+
# (adopter compat shim per ADR-031 — adopters can opt out of main-firing
|
|
28
|
+
# per session if needed; bassclef-upstream itself does not set this)
|
|
29
|
+
# - Otherwise fires BLOCKED block naming active bet + resolution path
|
|
30
|
+
#
|
|
31
|
+
# Config (read from substrate.config.md when present):
|
|
32
|
+
# - bassclef_evolution_label — issue label to scan (default: bassclef-evolution)
|
|
33
|
+
# - bassclef_evolution_child_repos — comma-separated owner/repo list to also scan
|
|
34
|
+
#
|
|
35
|
+
# Resolution:
|
|
36
|
+
# 1. Read active bet's parent_drivers + acceptance + out_of_scope
|
|
37
|
+
# 2. Walk parent_bet → parent_roadmap → parent_drivers chain
|
|
38
|
+
# 3. Scan open bassclef-evolution issues across current repo + child repos
|
|
39
|
+
# 4. Touch marker: mkdir -p state/markers/orientation-gate/ && \
|
|
40
|
+
# touch state/markers/orientation-gate/<branch-slug>.marker
|
|
41
|
+
# OR explicitly defer: "skip orientation-gate this session"
|
|
42
|
+
#
|
|
43
|
+
# Override (logged):
|
|
44
|
+
# SKIP_ORIENTATION_GATE=1 <command>
|
|
45
|
+
|
|
46
|
+
# Override path — silent, log via trace-helper if present
|
|
47
|
+
if [ "${SKIP_ORIENTATION_GATE:-0}" = "1" ]; then
|
|
48
|
+
if [ -x ".claude/hooks/trace-helper.sh" ]; then
|
|
49
|
+
bash .claude/hooks/trace-helper.sh "orientation-gate" "skipped via SKIP_ORIENTATION_GATE=1" 2>/dev/null || true
|
|
50
|
+
fi
|
|
51
|
+
return 0 2>/dev/null || exit 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Branch detection. Silent only when git is missing.
|
|
55
|
+
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
|
|
56
|
+
if [ -z "$BRANCH" ]; then
|
|
57
|
+
return 0 2>/dev/null || exit 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Adopter opt-out for main-firing (per ADR-031 we-don't-break-adopters
|
|
61
|
+
# compat shim; bassclef-upstream itself does not set this).
|
|
62
|
+
if [ "${SKIP_ORIENTATION_GATE_ON_MAIN:-0}" = "1" ] && { [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; }; then
|
|
63
|
+
if [ -x ".claude/hooks/trace-helper.sh" ]; then
|
|
64
|
+
bash .claude/hooks/trace-helper.sh "orientation-gate" "skipped on main via SKIP_ORIENTATION_GATE_ON_MAIN=1" 2>/dev/null || true
|
|
65
|
+
fi
|
|
66
|
+
return 0 2>/dev/null || exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Branch slug: slash → dash for safe filename
|
|
70
|
+
BRANCH_SLUG=$(echo "$BRANCH" | tr '/' '-')
|
|
71
|
+
|
|
72
|
+
# Marker check: silent if orientation marker exists for this branch
|
|
73
|
+
MARKER_PATH="state/markers/orientation-gate/${BRANCH_SLUG}.marker"
|
|
74
|
+
if [ -f "$MARKER_PATH" ]; then
|
|
75
|
+
return 0 2>/dev/null || exit 0
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# Read active iteration_bet from whereami (best-effort)
|
|
79
|
+
ACTIVE_BET="(none in whereami)"
|
|
80
|
+
if [ -f "docs/whereami.md" ]; then
|
|
81
|
+
BET_LINE=$(grep -E "^iteration_bet:" docs/whereami.md 2>/dev/null | head -1 | sed 's/^iteration_bet:[[:space:]]*//')
|
|
82
|
+
if [ -n "$BET_LINE" ]; then
|
|
83
|
+
# Trim parenthetical status notes (some whereami entries carry CLOSED/CLOSED+MERGED suffix)
|
|
84
|
+
ACTIVE_BET=$(echo "$BET_LINE" | sed 's/[[:space:]]*(.*//')
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# Walk parent_bet chain recursively (bassclef#1347).
|
|
89
|
+
# Reads each bet's frontmatter at docs/iteration-bets/<slug>.md, extracts
|
|
90
|
+
# parent_bet field, recurses until null or max depth (cycle protection).
|
|
91
|
+
# Builds ANCESTOR_CHAIN newline-separated for display in BLOCKED message.
|
|
92
|
+
ANCESTOR_CHAIN=""
|
|
93
|
+
walk_chain() {
|
|
94
|
+
local current="$1"
|
|
95
|
+
local depth=0
|
|
96
|
+
local max_depth=20
|
|
97
|
+
local visited=" "
|
|
98
|
+
|
|
99
|
+
while [ -n "$current" ] && [ "$depth" -lt "$max_depth" ]; do
|
|
100
|
+
# Cycle break — if we've seen this bet already, stop
|
|
101
|
+
case "$visited" in *" $current "*) break ;; esac
|
|
102
|
+
visited="$visited$current "
|
|
103
|
+
|
|
104
|
+
if [ "$depth" -gt 0 ]; then
|
|
105
|
+
ANCESTOR_CHAIN="${ANCESTOR_CHAIN} ↳ ${current}
|
|
106
|
+
"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
local bet_file="docs/iteration-bets/${current}.md"
|
|
110
|
+
[ -f "$bet_file" ] || break
|
|
111
|
+
|
|
112
|
+
local parent
|
|
113
|
+
parent=$(grep -E "^parent_bet:" "$bet_file" 2>/dev/null | head -1 | sed 's/^parent_bet:[[:space:]]*//')
|
|
114
|
+
current="$parent"
|
|
115
|
+
depth=$((depth + 1))
|
|
116
|
+
done
|
|
117
|
+
}
|
|
118
|
+
if [ -n "$ACTIVE_BET" ] && [ "$ACTIVE_BET" != "(none in whereami)" ]; then
|
|
119
|
+
walk_chain "$ACTIVE_BET"
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# Read scan config from substrate.config.md (best-effort defaults)
|
|
123
|
+
EVOLUTION_LABEL="bassclef-evolution"
|
|
124
|
+
CHILD_REPOS=""
|
|
125
|
+
if [ -f "substrate.config.md" ]; then
|
|
126
|
+
CFG_LABEL=$(grep -E "^bassclef_evolution_label:" substrate.config.md 2>/dev/null | head -1 | sed 's/^bassclef_evolution_label:[[:space:]]*//')
|
|
127
|
+
if [ -n "$CFG_LABEL" ]; then
|
|
128
|
+
EVOLUTION_LABEL="$CFG_LABEL"
|
|
129
|
+
fi
|
|
130
|
+
CFG_CHILDREN=$(grep -E "^bassclef_evolution_child_repos:" substrate.config.md 2>/dev/null | head -1 | sed 's/^bassclef_evolution_child_repos:[[:space:]]*//')
|
|
131
|
+
if [ -n "$CFG_CHILDREN" ]; then
|
|
132
|
+
CHILD_REPOS="$CFG_CHILDREN"
|
|
133
|
+
fi
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# Emit BLOCKED block
|
|
137
|
+
echo ""
|
|
138
|
+
echo "============================================"
|
|
139
|
+
echo "🛑 BLOCKED: orientation-gate"
|
|
140
|
+
echo "============================================"
|
|
141
|
+
echo ""
|
|
142
|
+
echo "Before /sprint or /longrun prep proposes work, read the bigger picture first."
|
|
143
|
+
echo ""
|
|
144
|
+
echo "Active iteration_bet: ${ACTIVE_BET}"
|
|
145
|
+
echo ""
|
|
146
|
+
echo "Read these BEFORE picking what to do next:"
|
|
147
|
+
echo " - The active bet's parent_drivers (why this exists), acceptance (what counts as done), and out_of_scope (what NOT to do)"
|
|
148
|
+
echo " - The FULL ancestor chain (per bassclef#1347 — recursive walk, not just one level):"
|
|
149
|
+
echo " ${ACTIVE_BET}"
|
|
150
|
+
if [ -n "$ANCESTOR_CHAIN" ]; then
|
|
151
|
+
printf "%s" "$ANCESTOR_CHAIN"
|
|
152
|
+
else
|
|
153
|
+
echo " (no ancestors — single bet, or bet file missing)"
|
|
154
|
+
fi
|
|
155
|
+
echo " Read each ancestor bet's parent_drivers + acceptance + out_of_scope. Union them, dedupe, surface in /longrun prep."
|
|
156
|
+
echo " - The parent_roadmap (if any bet in the chain has one) and its current pending work"
|
|
157
|
+
echo " - Open bassclef-evolution issues — run these:"
|
|
158
|
+
echo " gh issue list --label \"${EVOLUTION_LABEL}\" --state open # current repo"
|
|
159
|
+
if [ -n "$CHILD_REPOS" ]; then
|
|
160
|
+
# Split on comma; trim whitespace per item
|
|
161
|
+
OLD_IFS="$IFS"
|
|
162
|
+
IFS=','
|
|
163
|
+
for repo in $CHILD_REPOS; do
|
|
164
|
+
# Trim leading/trailing whitespace
|
|
165
|
+
repo_trimmed=$(echo "$repo" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
166
|
+
if [ -n "$repo_trimmed" ]; then
|
|
167
|
+
echo " gh issue list -R ${repo_trimmed} --label \"${EVOLUTION_LABEL}\" --state open"
|
|
168
|
+
fi
|
|
169
|
+
done
|
|
170
|
+
IFS="$OLD_IFS"
|
|
171
|
+
fi
|
|
172
|
+
echo " - Paused steps and open audit-drop tickets in the active arc"
|
|
173
|
+
echo ""
|
|
174
|
+
echo "When you've read enough to know what work makes sense, touch this marker:"
|
|
175
|
+
echo " mkdir -p state/markers/orientation-gate/"
|
|
176
|
+
echo " touch ${MARKER_PATH}"
|
|
177
|
+
echo ""
|
|
178
|
+
echo "OR explicitly defer per .claude/rules/blocked-items.md:"
|
|
179
|
+
echo " \"skip orientation-gate this session\""
|
|
180
|
+
echo ""
|
|
181
|
+
echo "Silence is not deferral. Default is read first."
|
|
182
|
+
echo ""
|
|
183
|
+
echo "Override (logged): SKIP_ORIENTATION_GATE=1"
|
|
184
|
+
echo "Per bassclef#1183 + bassclef#1266 + bassclef#1275 + bassclef#1347."
|
|
185
|
+
echo ""
|
|
186
|
+
|
|
187
|
+
return 0 2>/dev/null || exit 0
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# tier: lite
|
|
2
|
+
# 56-roadmap-reconcile-gate.sh
|
|
3
|
+
#
|
|
4
|
+
# Per bassclef#1183 + bassclef#504 + bassclef#1275: mechanical enforcement
|
|
5
|
+
# at SessionStart in bassclef AND every adopter repo. Fires BLOCKED on
|
|
6
|
+
# feature branches when no roadmap-reconcile marker exists. /sprint + /longrun
|
|
7
|
+
# prep procedures (WU-3/WU-4) refuse to propose work until /roadmap-reconcile
|
|
8
|
+
# --dry-run has run AND either showed no drift OR drift was resolved or
|
|
9
|
+
# explicitly deferred.
|
|
10
|
+
#
|
|
11
|
+
# This closes the methodology-vs-mechanism gap that bassclef#504
|
|
12
|
+
# left open: /roadmap-reconcile auto-fire was declared methodology
|
|
13
|
+
# at /longrun prep Step 0.5, but no hook fired it. Agents skipped the
|
|
14
|
+
# check across multiple sessions; canvas + roadmap drift accumulated
|
|
15
|
+
# silently. Adopters inherit the same discipline (bassclef#1275 — sister
|
|
16
|
+
# pattern "rule propagates, mechanism doesn't" surfaced in the same session
|
|
17
|
+
# that shipped this hook for bassclef-only).
|
|
18
|
+
#
|
|
19
|
+
# Behavior:
|
|
20
|
+
# - Fires in bassclef AND adopter repos (no consumer-repo guard since bassclef#1275)
|
|
21
|
+
# - Silent on main branch (gate is per-feature-branch scope decision)
|
|
22
|
+
# - Silent when state/markers/roadmap-reconcile-gate/<branch-slug>.marker exists
|
|
23
|
+
# - Silent when SKIP_ROADMAP_RECONCILE_GATE=1 (logged via trace-helper)
|
|
24
|
+
# - Otherwise emits BLOCKED block naming the dispatch command + resolution path
|
|
25
|
+
#
|
|
26
|
+
# Resolution:
|
|
27
|
+
# 1. Run /roadmap-reconcile --dry-run
|
|
28
|
+
# 2. If diff shows no drift: touch the marker
|
|
29
|
+
# mkdir -p state/markers/roadmap-reconcile-gate/
|
|
30
|
+
# touch state/markers/roadmap-reconcile-gate/<branch-slug>.marker
|
|
31
|
+
# 3. If diff shows drift: amend the canvas LR-N tables OR explicitly defer
|
|
32
|
+
# per .claude/rules/blocked-items.md ("skip roadmap-reconcile this session")
|
|
33
|
+
#
|
|
34
|
+
# Override (logged):
|
|
35
|
+
# SKIP_ROADMAP_RECONCILE_GATE=1 <command>
|
|
36
|
+
|
|
37
|
+
# Override path — silent, log via trace-helper if present
|
|
38
|
+
if [ "${SKIP_ROADMAP_RECONCILE_GATE:-0}" = "1" ]; then
|
|
39
|
+
if [ -x ".claude/hooks/trace-helper.sh" ]; then
|
|
40
|
+
bash .claude/hooks/trace-helper.sh "roadmap-reconcile-gate" "skipped via SKIP_ROADMAP_RECONCILE_GATE=1" 2>/dev/null || true
|
|
41
|
+
fi
|
|
42
|
+
return 0 2>/dev/null || exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Branch guard: silent on main
|
|
46
|
+
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
|
|
47
|
+
if [ -z "$BRANCH" ] || [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
|
|
48
|
+
return 0 2>/dev/null || exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Branch slug: slash → dash
|
|
52
|
+
BRANCH_SLUG=$(echo "$BRANCH" | tr '/' '-')
|
|
53
|
+
|
|
54
|
+
# Marker check
|
|
55
|
+
MARKER_PATH="state/markers/roadmap-reconcile-gate/${BRANCH_SLUG}.marker"
|
|
56
|
+
if [ -f "$MARKER_PATH" ]; then
|
|
57
|
+
return 0 2>/dev/null || exit 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Emit BLOCKED block
|
|
61
|
+
echo ""
|
|
62
|
+
echo "============================================"
|
|
63
|
+
echo "🛑 BLOCKED: roadmap-reconcile-gate"
|
|
64
|
+
echo "============================================"
|
|
65
|
+
echo ""
|
|
66
|
+
echo "Before /sprint or /longrun prep proposes work this session, run /roadmap-reconcile --dry-run."
|
|
67
|
+
echo ""
|
|
68
|
+
echo "Why: canvases hold LR-N tables that drift from shipping reality across long sessions."
|
|
69
|
+
echo "Agents proposing work against stale canvas state pick the wrong scope. The dry-run diff"
|
|
70
|
+
echo "names the drift (status mismatch, sequencing mismatch, reference rot) before scope-commit."
|
|
71
|
+
echo ""
|
|
72
|
+
echo "Resolve:"
|
|
73
|
+
echo " 1. Dispatch: /roadmap-reconcile --dry-run"
|
|
74
|
+
echo " 2. If diff shows NO drift, mark this branch oriented:"
|
|
75
|
+
echo " mkdir -p state/markers/roadmap-reconcile-gate/"
|
|
76
|
+
echo " touch ${MARKER_PATH}"
|
|
77
|
+
echo " 3. If diff shows drift, either:"
|
|
78
|
+
echo " - Amend the canvas LR-N tables to match reality, then mark"
|
|
79
|
+
echo " - OR explicitly defer per .claude/rules/blocked-items.md:"
|
|
80
|
+
echo " \"skip roadmap-reconcile this session\""
|
|
81
|
+
echo ""
|
|
82
|
+
echo "Silence is not deferral. Default is run the dry-run."
|
|
83
|
+
echo ""
|
|
84
|
+
echo "Override (logged): SKIP_ROADMAP_RECONCILE_GATE=1"
|
|
85
|
+
echo "Per bassclef#1183 + bassclef#504 + bassclef#1275."
|
|
86
|
+
echo ""
|
|
87
|
+
|
|
88
|
+
return 0 2>/dev/null || exit 0
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# tier: lite
|
|
2
|
+
# Deferred-actions scan: enumerate docs/deferred-actions/*.md, detect current
|
|
3
|
+
# modality capabilities, emit BLOCKED: for capability matches (this session
|
|
4
|
+
# can resolve), INFORM: for capability misses (visible, not actionable).
|
|
5
|
+
#
|
|
6
|
+
# Schema: standards/deferred-actions-schema.md
|
|
7
|
+
# Rule: .claude/rules/deferred-actions.md
|
|
8
|
+
#
|
|
9
|
+
# Migration note (Phase A WS-3 + spine v0 WU-6): this hook reads
|
|
10
|
+
# docs/deferred-actions/ entries via lib/deferred-actions.sh (typed
|
|
11
|
+
# accessor; pre-spine, still load-bearing for capability-routed scanning).
|
|
12
|
+
# Capability detection stays inline because it is session-environment-
|
|
13
|
+
# specific, not artifact-specific. Falls back to inline scanning if lib
|
|
14
|
+
# unavailable (consumer repos pre-distribution).
|
|
15
|
+
#
|
|
16
|
+
# Spine v0 (WU-6) integration: lib/state.sh provides
|
|
17
|
+
# state_deferred_action_pending [requires-capability...] and
|
|
18
|
+
# state_deferred_action_list [pending|completed|all] as the unified
|
|
19
|
+
# accessor surface. This hook continues using lib/deferred-actions.sh
|
|
20
|
+
# for now because (a) capability-routed filtering is more deeply wired
|
|
21
|
+
# into the existing accessor + (b) full migration would risk regressing
|
|
22
|
+
# capability-detection logic that's uniquely tested here. Future
|
|
23
|
+
# unification (consolidate into lib/state.sh) is tracked as a /promote.
|
|
24
|
+
|
|
25
|
+
DEFERRED_DIR="docs/deferred-actions"
|
|
26
|
+
|
|
27
|
+
# No dir → nothing to scan.
|
|
28
|
+
if [ ! -d "$DEFERRED_DIR" ]; then
|
|
29
|
+
return 0 2>/dev/null || true
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Source typed accessor when available. Probe bundled path first
|
|
33
|
+
# (dist/lite/ ship path per #1631 goal 2026-09-14b), then $BASSCLEF_DIR
|
|
34
|
+
# fallback for synced adopters. Pattern matches session-reflection.sh:13.
|
|
35
|
+
# Graceful fallback to inline scanning if lib not present.
|
|
36
|
+
_sd="$(dirname "${BASH_SOURCE[0]:-$0}")"
|
|
37
|
+
DA_LIB=""
|
|
38
|
+
for _c in "${_sd}/../../../lib/deferred-actions.sh" \
|
|
39
|
+
"${BASSCLEF_DIR:-}/lib/deferred-actions.sh"; do
|
|
40
|
+
[ -f "$_c" ] && DA_LIB="$_c" && break
|
|
41
|
+
done
|
|
42
|
+
USE_LIB=0
|
|
43
|
+
if [ -n "$DA_LIB" ]; then
|
|
44
|
+
# shellcheck source=/dev/null
|
|
45
|
+
source "$DA_LIB"
|
|
46
|
+
# Point lib at consumer-local docs/deferred-actions, not bassclef's
|
|
47
|
+
DEFERRED_ACTIONS_DIR="$DEFERRED_DIR"
|
|
48
|
+
export DEFERRED_ACTIONS_DIR
|
|
49
|
+
USE_LIB=1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# === Modality detection — synthesize current capability set ===
|
|
53
|
+
#
|
|
54
|
+
# Detection order matches .claude/rules/mobile-ephemeral-session.md:
|
|
55
|
+
# primary signal $CLAUDE_EPHEMERAL, then heuristics. Capabilities are
|
|
56
|
+
# additive — start empty, set flags for each positive check.
|
|
57
|
+
|
|
58
|
+
CAP_GCLOUD=0
|
|
59
|
+
CAP_GDOCS=0
|
|
60
|
+
CAP_GH_AUTH=0
|
|
61
|
+
CAP_GIT_PUSH=0
|
|
62
|
+
CAP_GIT_TAG=0
|
|
63
|
+
CAP_NETWORK=0
|
|
64
|
+
CAP_MEMORY_WRITE=0
|
|
65
|
+
CAP_NPM_INSTALL=0
|
|
66
|
+
CAP_PLAYWRIGHT_MCP=0
|
|
67
|
+
|
|
68
|
+
# gcloud-auth + google-docs-write: presence of SA key
|
|
69
|
+
if [ -f "$HOME/.config/gcloud/sunjay-google-ops.json" ]; then
|
|
70
|
+
CAP_GCLOUD=1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# gh-auth: CLI installed AND authenticated
|
|
74
|
+
if command -v gh &>/dev/null && gh auth status &>/dev/null 2>&1; then
|
|
75
|
+
CAP_GH_AUTH=1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# git-push: in a git repo with a remote configured
|
|
79
|
+
if git remote get-url origin &>/dev/null 2>&1; then
|
|
80
|
+
CAP_GIT_PUSH=1
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# git-tag: superset of git-push (same creds); set if git-push set
|
|
84
|
+
if [ "$CAP_GIT_PUSH" = "1" ]; then
|
|
85
|
+
CAP_GIT_TAG=1
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# network: reachable via curl with short timeout (heuristic — some
|
|
89
|
+
# ephemeral sandboxes block outbound but claim no $CLAUDE_EPHEMERAL).
|
|
90
|
+
# Skip the check in known-no-network envs to avoid flakiness.
|
|
91
|
+
if [ "${CLAUDE_EPHEMERAL:-}" = "1" ]; then
|
|
92
|
+
CAP_NETWORK=1
|
|
93
|
+
else
|
|
94
|
+
CAP_NETWORK=1
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# memory-write: path exists + writable. Only desktop has this.
|
|
98
|
+
MEMORY_DIR="$HOME/.claude/projects"
|
|
99
|
+
if [ -d "$MEMORY_DIR" ] && [ -w "$MEMORY_DIR" ] && [ "${CLAUDE_EPHEMERAL:-}" != "1" ]; then
|
|
100
|
+
CAP_MEMORY_WRITE=1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# npm-install: npm on PATH + network
|
|
104
|
+
if command -v npm &>/dev/null && [ "$CAP_NETWORK" = "1" ]; then
|
|
105
|
+
CAP_NPM_INSTALL=1
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# playwright-mcp: settings.json mentions mcp__playwright, or env hint
|
|
109
|
+
if [ -f "$HOME/.claude/settings.json" ] && grep -q "playwright" "$HOME/.claude/settings.json" 2>/dev/null; then
|
|
110
|
+
CAP_PLAYWRIGHT_MCP=1
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# google-docs-write composite: gcloud-auth + network
|
|
114
|
+
if [ "$CAP_GCLOUD" = "1" ] && [ "$CAP_NETWORK" = "1" ]; then
|
|
115
|
+
CAP_GDOCS=1
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# === Helper: does current session satisfy a required-capability list? ===
|
|
119
|
+
capability_match() {
|
|
120
|
+
local reqs="$1"
|
|
121
|
+
local cap
|
|
122
|
+
reqs=$(echo "$reqs" | tr -d '[]"' | tr ',' ' ')
|
|
123
|
+
for cap in $reqs; do
|
|
124
|
+
cap=$(echo "$cap" | xargs)
|
|
125
|
+
[ -z "$cap" ] && continue
|
|
126
|
+
case "$cap" in
|
|
127
|
+
gcloud-auth) [ "$CAP_GCLOUD" = "1" ] || return 1 ;;
|
|
128
|
+
google-docs-write) [ "$CAP_GDOCS" = "1" ] || return 1 ;;
|
|
129
|
+
gh-auth) [ "$CAP_GH_AUTH" = "1" ] || return 1 ;;
|
|
130
|
+
git-push) [ "$CAP_GIT_PUSH" = "1" ] || return 1 ;;
|
|
131
|
+
git-tag) [ "$CAP_GIT_TAG" = "1" ] || return 1 ;;
|
|
132
|
+
network) [ "$CAP_NETWORK" = "1" ] || return 1 ;;
|
|
133
|
+
memory-write) [ "$CAP_MEMORY_WRITE" = "1" ] || return 1 ;;
|
|
134
|
+
npm-install) [ "$CAP_NPM_INSTALL" = "1" ] || return 1 ;;
|
|
135
|
+
playwright-mcp) [ "$CAP_PLAYWRIGHT_MCP" = "1" ] || return 1 ;;
|
|
136
|
+
*)
|
|
137
|
+
return 1
|
|
138
|
+
;;
|
|
139
|
+
esac
|
|
140
|
+
done
|
|
141
|
+
return 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# === Scan entries ===
|
|
145
|
+
|
|
146
|
+
BLOCKED_ENTRIES=""
|
|
147
|
+
INFORM_ENTRIES=""
|
|
148
|
+
MALFORMED_COUNT=0
|
|
149
|
+
|
|
150
|
+
# Lib-backed enumeration when available; fall back to direct glob.
|
|
151
|
+
if [ "$USE_LIB" = "1" ]; then
|
|
152
|
+
ENTRIES="$(da_list_open)"
|
|
153
|
+
else
|
|
154
|
+
ENTRIES=""
|
|
155
|
+
for entry in "$DEFERRED_DIR"/*.md; do
|
|
156
|
+
[ -f "$entry" ] || continue
|
|
157
|
+
[ "$(basename "$entry")" = "README.md" ] && continue
|
|
158
|
+
[ "$(basename "$entry")" = "template.md" ] && continue
|
|
159
|
+
ENTRIES="$ENTRIES $entry"
|
|
160
|
+
done
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
for entry in $ENTRIES; do
|
|
164
|
+
[ -f "$entry" ] || continue
|
|
165
|
+
# Lib-backed field reads when available; fall back to inline awk.
|
|
166
|
+
if [ "$USE_LIB" = "1" ]; then
|
|
167
|
+
ID="$(da_get_field "$entry" id 2>/dev/null)"
|
|
168
|
+
ACTION="$(da_get_field "$entry" pending_action 2>/dev/null)"
|
|
169
|
+
REQS="$(da_get_field "$entry" requires_capability 2>/dev/null)"
|
|
170
|
+
PRIORITY="$(da_get_field "$entry" priority 2>/dev/null)"
|
|
171
|
+
else
|
|
172
|
+
ID=$(awk '/^id:/ {print $2; exit}' "$entry" 2>/dev/null)
|
|
173
|
+
ACTION=$(awk '/^pending_action:/ {sub(/^pending_action: */, ""); print; exit}' "$entry" 2>/dev/null)
|
|
174
|
+
REQS=$(awk '/^requires_capability:/ {sub(/^requires_capability: */, ""); print; exit}' "$entry" 2>/dev/null)
|
|
175
|
+
PRIORITY=$(awk '/^priority:/ {print $2; exit}' "$entry" 2>/dev/null)
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
if [ -z "$ID" ] || [ -z "$ACTION" ] || [ -z "$REQS" ]; then
|
|
179
|
+
MALFORMED_COUNT=$((MALFORMED_COUNT + 1))
|
|
180
|
+
continue
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
SUMMARY_LINE="${PRIORITY:-medium}: ${ID} — ${ACTION} (requires: ${REQS})"
|
|
184
|
+
|
|
185
|
+
if capability_match "$REQS"; then
|
|
186
|
+
BLOCKED_ENTRIES="${BLOCKED_ENTRIES}\n - ${SUMMARY_LINE}"
|
|
187
|
+
else
|
|
188
|
+
INFORM_ENTRIES="${INFORM_ENTRIES}\n - ${SUMMARY_LINE}"
|
|
189
|
+
fi
|
|
190
|
+
done
|
|
191
|
+
|
|
192
|
+
# === Surface findings ===
|
|
193
|
+
|
|
194
|
+
if [ -n "$BLOCKED_ENTRIES" ]; then
|
|
195
|
+
MSG="deferred-actions — $(echo -e "$BLOCKED_ENTRIES" | grep -c '^ - ') entry(ies) resolvable in this session:$(echo -e "$BLOCKED_ENTRIES")\nResolve each per .claude/rules/deferred-actions.md, or explicitly defer."
|
|
196
|
+
blocked_banner "$MSG"
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
if [ -n "$INFORM_ENTRIES" ]; then
|
|
200
|
+
echo "### DEFERRED ACTIONS — INFORM (not resolvable this session)"
|
|
201
|
+
echo ""
|
|
202
|
+
echo -e "The following deferred-action entries require capabilities"
|
|
203
|
+
echo "this session doesn't have. They wait for a capable session."
|
|
204
|
+
echo -e "$INFORM_ENTRIES"
|
|
205
|
+
echo ""
|
|
206
|
+
echo "Do NOT attempt to resolve them here. See .claude/rules/deferred-actions.md."
|
|
207
|
+
echo ""
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
if [ "$MALFORMED_COUNT" -gt 0 ]; then
|
|
211
|
+
blocked_banner "deferred-actions — ${MALFORMED_COUNT} malformed entry(ies) in $DEFERRED_DIR/. Missing required frontmatter (id / pending_action / requires_capability). See standards/deferred-actions-schema.md."
|
|
212
|
+
fi
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# tier: lite
|
|
2
|
+
# Recent Strategic Artifacts — session-start branch of artifact-ingestion gate.
|
|
3
|
+
#
|
|
4
|
+
# Surfaces canvases / specs / ADRs / iteration-bets / decompositions / roadmaps
|
|
5
|
+
# authored OR modified within the last N days (default 7) so the agent reads
|
|
6
|
+
# them before substantive work this session.
|
|
7
|
+
#
|
|
8
|
+
# Closes the gap that PreToolUse `artifact-ingestion-gate.sh` cannot cover:
|
|
9
|
+
# that hook fires on Write/Edit (production-time); this module fires on
|
|
10
|
+
# SessionStart (orientation-time). Failure mode covered:
|
|
11
|
+
# "agent does substantive work without reading recent strategic artifacts"
|
|
12
|
+
# — explicitly named in canvas v3 §"Failure modes spine does NOT prevent".
|
|
13
|
+
#
|
|
14
|
+
# Inherits from session-reflection.sh: blocked_banner, $BASSCLEF_DIR, trace_log.
|
|
15
|
+
#
|
|
16
|
+
# Env overrides (logged via trace_log when used):
|
|
17
|
+
# SKIP_RECENT_ARTIFACTS=1 — disable for this session
|
|
18
|
+
# RECENT_STRATEGIC_DAYS=N — window size in days (default 7); 0 disables
|
|
19
|
+
#
|
|
20
|
+
# Issue: bassclef#308
|
|
21
|
+
|
|
22
|
+
# Skip if env override
|
|
23
|
+
if [ "${SKIP_RECENT_ARTIFACTS:-0}" = "1" ]; then
|
|
24
|
+
type trace_log >/dev/null 2>&1 && trace_log "70-recent-strategic-artifacts" "SKIP via SKIP_RECENT_ARTIFACTS=1"
|
|
25
|
+
return 0 2>/dev/null || exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
DAYS="${RECENT_STRATEGIC_DAYS:-7}"
|
|
29
|
+
|
|
30
|
+
# Zero-day window = disabled this session
|
|
31
|
+
if [ "$DAYS" = "0" ]; then
|
|
32
|
+
type trace_log >/dev/null 2>&1 && trace_log "70-recent-strategic-artifacts" "DISABLED via RECENT_STRATEGIC_DAYS=0"
|
|
33
|
+
return 0 2>/dev/null || exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Bail if not in a git repo (defensive — most consumer repos are git, but mobile
|
|
37
|
+
# / fresh-clone edges may not be when this fires).
|
|
38
|
+
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
|
39
|
+
return 0 2>/dev/null || exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Strategic artifact path matcher — mirrors artifact-ingestion-gate.sh's set
|
|
43
|
+
# minus prototypes (those have their own approval workflow per
|
|
44
|
+
# prototype-workflow.md), plus roadmaps (added per canvas v3 §Failure modes).
|
|
45
|
+
STRATEGIC_PATHS=(
|
|
46
|
+
'strategy/canvases/*.md'
|
|
47
|
+
'docs/specs/*.md'
|
|
48
|
+
'docs/iteration-bets/*.md'
|
|
49
|
+
'architecture/decisions/ADR-*.md'
|
|
50
|
+
'docs/decompositions/*.md'
|
|
51
|
+
'docs/roadmaps/*.md'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# git log --since=N days ago, A=added M=modified, restricted to strategic paths.
|
|
55
|
+
# Output: <ISO-date>\t<path> per line, deduplicated, most recent commit per path.
|
|
56
|
+
RECENT=$(git log \
|
|
57
|
+
--since="${DAYS} days ago" \
|
|
58
|
+
--diff-filter=AM \
|
|
59
|
+
--name-only \
|
|
60
|
+
--pretty=format:'%nCOMMIT %ai' \
|
|
61
|
+
-- "${STRATEGIC_PATHS[@]}" 2>/dev/null \
|
|
62
|
+
| awk '
|
|
63
|
+
/^COMMIT / { date=$2; next }
|
|
64
|
+
NF && !seen[$0]++ { print date "\t" $0 }
|
|
65
|
+
' \
|
|
66
|
+
| sort -u
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if [ -z "$RECENT" ]; then
|
|
70
|
+
return 0 2>/dev/null || exit 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Build the block message.
|
|
74
|
+
COUNT=$(echo "$RECENT" | wc -l | tr -d ' ')
|
|
75
|
+
MSG="RECENT STRATEGIC ARTIFACTS ($COUNT in last ${DAYS} days) — Read before substantive work this session"
|
|
76
|
+
MSG="${MSG}"$'\n'
|
|
77
|
+
while IFS=$'\t' read -r DATE PATHNAME; do
|
|
78
|
+
[ -z "$PATHNAME" ] && continue
|
|
79
|
+
MSG="${MSG} - ${PATHNAME} (${DATE})"$'\n'
|
|
80
|
+
done <<< "$RECENT"
|
|
81
|
+
MSG="${MSG}"
|
|
82
|
+
MSG="${MSG}Per .claude/rules/artifact-ingestion.md §Session-start branch:"$'\n'
|
|
83
|
+
MSG="${MSG} Strategic artifacts authored or modified within the last ${DAYS} days"$'\n'
|
|
84
|
+
MSG="${MSG} MUST be Read before substantive work. Resolve via Read; defer via"$'\n'
|
|
85
|
+
MSG="${MSG} explicit operator statement (\"skip recent-artifacts this session\")."$'\n'
|
|
86
|
+
MSG="${MSG} Override: SKIP_RECENT_ARTIFACTS=1 <command> (logged)."
|
|
87
|
+
|
|
88
|
+
# Emit via inherited blocked_banner (defined in session-reflection.sh)
|
|
89
|
+
if type blocked_banner >/dev/null 2>&1; then
|
|
90
|
+
blocked_banner "$MSG"
|
|
91
|
+
else
|
|
92
|
+
# Defensive fallback if module is invoked outside the coordinator
|
|
93
|
+
echo ""
|
|
94
|
+
echo "🛑🛑🛑 BLOCKED 🛑🛑🛑"
|
|
95
|
+
echo "$MSG"
|
|
96
|
+
echo ""
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
type trace_log >/dev/null 2>&1 && trace_log "70-recent-strategic-artifacts" "BLOCK count=$COUNT days=$DAYS"
|
|
100
|
+
|
|
101
|
+
return 0 2>/dev/null || exit 0
|