@thebassclef/lite 1.0.1 → 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.
Files changed (46) hide show
  1. package/dist/cli.cjs +24 -1
  2. package/dist/cli.js +24 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/lite/.claude/hooks/bassclef-sync.sh +98 -16
  7. package/dist/lite/.claude/hooks/longrun-prep-compounding-sequence-check.sh +3 -3
  8. package/dist/lite/.claude/hooks/session-reflection.d/00-bassclef-hook-connect.sh +63 -0
  9. package/dist/lite/.claude/hooks/session-reflection.d/05-active-bet.sh +113 -0
  10. package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +62 -0
  11. package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +78 -0
  12. package/dist/lite/.claude/hooks/session-reflection.d/12-session-lock.sh +63 -0
  13. package/dist/lite/.claude/hooks/session-reflection.d/15-substrate-config-validate.sh +91 -0
  14. package/dist/lite/.claude/hooks/session-reflection.d/20-artifact-staleness.sh +98 -0
  15. package/dist/lite/.claude/hooks/session-reflection.d/30-metrics-staleness.sh +61 -0
  16. package/dist/lite/.claude/hooks/session-reflection.d/35-post-merge-whereami-self-heal.sh +180 -0
  17. package/dist/lite/.claude/hooks/session-reflection.d/36-whereami-size-check.sh +117 -0
  18. package/dist/lite/.claude/hooks/session-reflection.d/40-gate-compliance.sh +108 -0
  19. package/dist/lite/.claude/hooks/session-reflection.d/45-auto-sync-status.sh +99 -0
  20. package/dist/lite/.claude/hooks/session-reflection.d/50-evolution-issues.sh +44 -0
  21. package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +187 -0
  22. package/dist/lite/.claude/hooks/session-reflection.d/56-roadmap-reconcile-gate.sh +88 -0
  23. package/dist/lite/.claude/hooks/session-reflection.d/60-deferred-actions.sh +212 -0
  24. package/dist/lite/.claude/hooks/session-reflection.d/70-recent-strategic-artifacts.sh +101 -0
  25. package/dist/lite/.claude/hooks/session-reflection.d/75-prior-r8-findings.sh +100 -0
  26. package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +87 -0
  27. package/dist/lite/.claude/hooks/session-reflection.d/80-workflow-staleness.sh +141 -0
  28. package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +67 -0
  29. package/dist/lite/.claude/hooks/session-reflection.d/90-release-backlog.sh +140 -0
  30. package/dist/lite/.claude/hooks/session-reflection.d/90-secrets-expiry.sh +104 -0
  31. package/dist/lite/.claude/hooks/session-reflection.d/95-settings-hook-verify.sh +131 -0
  32. package/dist/lite/.claude/hooks/session-reflection.d/95-telemetry-emit.sh +66 -0
  33. package/dist/lite/.claude/hooks/session-reflection.d/97-install-class-dispatch.sh +57 -0
  34. package/dist/lite/.claude/hooks/session-reflection.d/tests/00-bassclef-hook-connect.test.sh +492 -0
  35. package/dist/lite/.claude/hooks/session-reflection.d/tests/05-active-bet.test.sh +336 -0
  36. package/dist/lite/.claude/hooks/session-reflection.d/tests/12-session-lock.test.sh +216 -0
  37. package/dist/lite/.claude/hooks/session-reflection.d/tests/15-substrate-config-validate.test.sh +50 -0
  38. package/dist/lite/.claude/hooks/session-reflection.d/tests/20-artifact-staleness.test.sh +348 -0
  39. package/dist/lite/.claude/hooks/session-reflection.d/tests/36-whereami-size-check.test.sh +189 -0
  40. package/dist/lite/.claude/hooks/session-reflection.d/tests/45-auto-sync-status.test.sh +263 -0
  41. package/dist/lite/.claude/hooks/session-reflection.d/tests/55-orientation-gate.test.sh +595 -0
  42. package/dist/lite/.claude/hooks/session-reflection.d/tests/56-roadmap-reconcile-gate.test.sh +227 -0
  43. package/dist/lite/.claude/hooks/session-reflection.d/tests/75-prior-r8-findings.test.sh +142 -0
  44. package/dist/lite/.claude/hooks/session-reflection.d/tests/90-release-backlog.test.sh +44 -0
  45. package/dist/lite/.claude/hooks/trace-helper.sh +22 -0
  46. package/package.json +1 -1
@@ -0,0 +1,99 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # 45-auto-sync-status.sh — Always-on version status line
4
+ #
5
+ # V3 refactor (bassclef bet 2026-06-15b — Phase B Trust Busters):
6
+ # - Always-on session-start version-status line replacing the earned-banner
7
+ # Layer 2 surface (per operator decision — always-on subsumes earned)
8
+ # - Critical-fix entries from adopter-notable-releases.json fire warnings
9
+ # BYPASSING age threshold (no 30-day gate)
10
+ # - Backward-compat: silent when .bassclef-source.json absent (bassclef's
11
+ # own repo) or when V1 installed_sha/installed_at fields not yet populated
12
+ #
13
+ # Behavior:
14
+ # - When .bassclef-source.json present + has installed_sha + installed_at:
15
+ # emit "### bassclef-sync — version status" header + one-line status
16
+ # showing installed SHA + days-behind + auto-sync state
17
+ # - When critical-fix entries published_at > installed_at:
18
+ # emit additional warning section listing the entries
19
+ #
20
+ # Override: SKIP_AUTO_SYNC_STATUS=1 in env.
21
+ #
22
+ # Per bassclef#939 — adopter sync opt-in default.
23
+ # Per bet 2026-06-15b V3 — Layer 2 collapse to always-on.
24
+
25
+ [ "${SKIP_AUTO_SYNC_STATUS:-0}" = "1" ] && { return 0 2>/dev/null || exit 0; }
26
+
27
+ CWD="${CLAUDE_PROJECT_DIR:-$PWD}"
28
+ SOURCE_FILE="$CWD/.bassclef-source.json"
29
+ ADOPTER_NOTABLE_FILE="$CWD/state/adopter-notable-releases.json"
30
+
31
+ # Silent if no .bassclef-source.json (bassclef's own repo path).
32
+ [ -f "$SOURCE_FILE" ] || { return 0 2>/dev/null || exit 0; }
33
+
34
+ # Parse V1 fields. Silent if installed_sha + installed_at not yet populated
35
+ # (adopter hasn't migrated to V1 schema).
36
+ INSTALLED_SHA=$(jq -r '.installed_sha // empty' "$SOURCE_FILE" 2>/dev/null)
37
+ INSTALLED_AT=$(jq -r '.installed_at // empty' "$SOURCE_FILE" 2>/dev/null)
38
+ AUTO_SYNC=$(jq -r '.auto_sync // false' "$SOURCE_FILE" 2>/dev/null)
39
+
40
+ if [ -z "$INSTALLED_SHA" ] || [ -z "$INSTALLED_AT" ]; then
41
+ # Pre-V1 adopter — silent to avoid noise during gradual schema rollout.
42
+ return 0 2>/dev/null || exit 0
43
+ fi
44
+
45
+ # Compute days-behind from installed_at. Cross-platform date parsing.
46
+ NOW_EPOCH=$(date +%s)
47
+ INSTALLED_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$INSTALLED_AT" +%s 2>/dev/null \
48
+ || date -d "$INSTALLED_AT" +%s 2>/dev/null || echo "$NOW_EPOCH")
49
+ AGE_SECONDS=$((NOW_EPOCH - INSTALLED_EPOCH))
50
+ DAYS_BEHIND=$((AGE_SECONDS / 86400))
51
+
52
+ # Auto-sync state surfacing.
53
+ if [ "$AUTO_SYNC" = "true" ]; then
54
+ AUTO_LINE="Auto-update is ON — next session-start fetches latest."
55
+ else
56
+ AUTO_LINE="Auto-update is OFF — say \"enable auto-sync\" to flip the default."
57
+ fi
58
+
59
+ # Days-behind phrasing.
60
+ if [ "$DAYS_BEHIND" -eq 0 ]; then
61
+ AGE_PHRASE="today"
62
+ elif [ "$DAYS_BEHIND" -eq 1 ]; then
63
+ AGE_PHRASE="1 day ago"
64
+ else
65
+ AGE_PHRASE="$DAYS_BEHIND days ago"
66
+ fi
67
+
68
+ # Compose status line + emit. Always fires when reaching this point.
69
+ cat <<HEADER
70
+
71
+ ### bassclef-sync — version status
72
+
73
+ Installed: $INSTALLED_SHA ($AGE_PHRASE)
74
+ $AUTO_LINE
75
+ HEADER
76
+
77
+ # Check for critical-fix entries published_at > installed_at.
78
+ # Critical-fixes BYPASS the age threshold (no 30-day gate).
79
+ if [ -f "$ADOPTER_NOTABLE_FILE" ]; then
80
+ CRITICAL_COUNT=$(jq --arg cutoff "$INSTALLED_AT" \
81
+ '[.releases[]? | select(.published_at > $cutoff) | select(.kind == "critical_fix")] | length' \
82
+ "$ADOPTER_NOTABLE_FILE" 2>/dev/null || echo 0)
83
+ if [ "$CRITICAL_COUNT" -gt 0 ]; then
84
+ CRITICAL_SUMMARY=$(jq --arg cutoff "$INSTALLED_AT" -r \
85
+ '[.releases[]? | select(.published_at > $cutoff) | select(.kind == "critical_fix") | " • " + .summary] | join("\n")' \
86
+ "$ADOPTER_NOTABLE_FILE" 2>/dev/null || echo "")
87
+ cat <<CRITICAL
88
+
89
+ ⚠ $CRITICAL_COUNT critical-fix update(s) released since your install:
90
+ $CRITICAL_SUMMARY
91
+
92
+ Pull now with:
93
+
94
+ bash .claude/hooks/bassclef-sync.sh
95
+ CRITICAL
96
+ fi
97
+ fi
98
+
99
+ return 0 2>/dev/null || exit 0
@@ -0,0 +1,44 @@
1
+ # tier: lite
2
+ # Bassclef-evolution promotion scan: in bassclef sessions only, list
3
+ # open bassclef-evolution issues in bassclef and known consumer repos.
4
+ #
5
+ # Consumer list is read from substrate.config.md field
6
+ # `bassclef_evolution_child_repos` (comma-separated owner/name list).
7
+ # Falls back to sunj-labs/poa only when the field is absent — safe
8
+ # default for old consumer checkouts predating the config field.
9
+ #
10
+ # Keep the field in sync with .github/workflows/bassclef-template-sync.yml
11
+ # matrix.consumer list.
12
+
13
+ REPO_NAME_CHECK=$(gh repo view --json name -q '.name' 2>/dev/null || echo "")
14
+ if [ "$REPO_NAME_CHECK" = "platform-docs" ] || [ "$REPO_NAME_CHECK" = "bassclef" ]; then
15
+ CHILD_REPOS_RAW=$(grep "^bassclef_evolution_child_repos:" substrate.config.md 2>/dev/null | sed 's/^bassclef_evolution_child_repos:[[:space:]]*//' || echo "")
16
+ if [ -n "$CHILD_REPOS_RAW" ]; then
17
+ CHILD_REPOS=$(echo "$CHILD_REPOS_RAW" | tr ',' '\n' | tr -d ' ' | grep -v '^$')
18
+ else
19
+ CHILD_REPOS="sunj-labs/poa"
20
+ fi
21
+
22
+ ALL_EVOLUTION=""
23
+
24
+ BASSCLEF_ISSUES=$(gh issue list -R "sunj-labs/bassclef" --label "bassclef-evolution" --state open --limit 10 2>/dev/null)
25
+ if [ -n "$BASSCLEF_ISSUES" ]; then
26
+ ALL_EVOLUTION="${ALL_EVOLUTION}\n From bassclef:\n${BASSCLEF_ISSUES}\n"
27
+ fi
28
+
29
+ for CHILD in $CHILD_REPOS; do
30
+ CHILD_ISSUES=$(gh issue list -R "$CHILD" --label "bassclef-evolution" --state open --limit 10 2>/dev/null)
31
+ if [ -n "$CHILD_ISSUES" ]; then
32
+ ALL_EVOLUTION="${ALL_EVOLUTION}\n From ${CHILD}:\n${CHILD_ISSUES}\n"
33
+ fi
34
+ done
35
+
36
+ if [ -n "$ALL_EVOLUTION" ]; then
37
+ echo "### BASSCLEF EVOLUTION — PENDING PROMOTIONS"
38
+ echo ""
39
+ echo -e "$ALL_EVOLUTION"
40
+ echo "Review and resolve these. Child repo proposals need ingestion"
41
+ echo "into bassclef if globally applicable."
42
+ echo ""
43
+ fi
44
+ fi
@@ -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