@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.
- 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/bassclef-sync.sh +98 -16
- package/dist/lite/.claude/hooks/longrun-prep-compounding-sequence-check.sh +3 -3
- 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,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
|