@thebassclef/lite 1.0.2 → 1.0.4
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 +51 -7
- package/dist/cli.js +51 -7
- 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
package/dist/cli.cjs
CHANGED
|
@@ -548,8 +548,11 @@ function copySubstrate(targetDir, options = {}) {
|
|
|
548
548
|
for (const [directory, groupFiles] of groups) {
|
|
549
549
|
let completedInGroup = 0;
|
|
550
550
|
for (const relPath of groupFiles) {
|
|
551
|
-
const
|
|
552
|
-
|
|
551
|
+
const decisions = decisionsForFile(relPath, scopeMap, targetDir, options);
|
|
552
|
+
for (const decision of decisions) {
|
|
553
|
+
const outcome = copyOne(relPath, bundleRoot, options, result, decision);
|
|
554
|
+
if (outcome !== "skipped") completedInGroup += 1;
|
|
555
|
+
}
|
|
553
556
|
}
|
|
554
557
|
if (options.onProgress) options.onProgress(directory, completedInGroup);
|
|
555
558
|
}
|
|
@@ -656,12 +659,30 @@ function buildScopeMap(bundleRoot, targetDir, options, bundleHookRelPaths) {
|
|
|
656
659
|
function isHookFile(relPath) {
|
|
657
660
|
return relPath.startsWith(HOOKS_SUBPATH) && relPath.endsWith(".sh");
|
|
658
661
|
}
|
|
659
|
-
function
|
|
662
|
+
function decisionsForFile(relPath, scopeMap, targetDir, options) {
|
|
663
|
+
if (!isHookFile(relPath)) {
|
|
664
|
+
return [{ scope: "project", targetPath: node_path.join(targetDir, mapAdopterPath(relPath)) }];
|
|
665
|
+
}
|
|
666
|
+
const declared = scopeMap.get(relPath);
|
|
667
|
+
if (declared) {
|
|
668
|
+
return [declared];
|
|
669
|
+
}
|
|
670
|
+
const allowRoot = options.allowRoot ?? false;
|
|
671
|
+
const userDecision = classify$1(
|
|
672
|
+
{ command: `$HOME/${relPath}` },
|
|
673
|
+
{ targetDir, allowRoot }
|
|
674
|
+
);
|
|
675
|
+
const projectDecision = classify$1(
|
|
676
|
+
{ command: `$CLAUDE_PROJECT_DIR/${relPath}` },
|
|
677
|
+
{ targetDir, allowRoot }
|
|
678
|
+
);
|
|
679
|
+
return [userDecision, projectDecision];
|
|
680
|
+
}
|
|
681
|
+
function copyOne(relPath, bundleRoot, options, result, scopeDecision) {
|
|
660
682
|
const sourcePath = node_path.join(bundleRoot, relPath);
|
|
661
683
|
const adopterRelPath = mapAdopterPath(relPath);
|
|
662
|
-
const
|
|
663
|
-
const
|
|
664
|
-
const scope = scopeDecision ? scopeDecision.scope : "project";
|
|
684
|
+
const targetPath = scopeDecision.targetPath;
|
|
685
|
+
const scope = scopeDecision.scope;
|
|
665
686
|
let content;
|
|
666
687
|
try {
|
|
667
688
|
content = node_fs.readFileSync(sourcePath, "utf8");
|
|
@@ -919,8 +940,11 @@ function dispatchSubstrateCopy(targetDir, force, verbose, dryRun, allowRoot, jso
|
|
|
919
940
|
`bassclef init: ${grandTotal} files total (1 config + ${result.copied.length} substrate).
|
|
920
941
|
`
|
|
921
942
|
);
|
|
943
|
+
const declaredCommandLeaves = readDeclaredCommandLeaves(
|
|
944
|
+
node_path.join(targetDir, ".claude", "settings.json")
|
|
945
|
+
);
|
|
922
946
|
const copiedHookEntries = result.copiedEntries.filter(
|
|
923
|
-
(e) => e.path.startsWith(HOOKS_SUBPATH) && e.path.endsWith(".sh")
|
|
947
|
+
(e) => e.path.startsWith(HOOKS_SUBPATH) && e.path.endsWith(".sh") && declaredCommandLeaves.has(node_path.basename(e.path))
|
|
924
948
|
);
|
|
925
949
|
const copiedCount = copiedHookEntries.length;
|
|
926
950
|
const declaredCount = result.hookCount;
|
|
@@ -1148,6 +1172,26 @@ function usage$2() {
|
|
|
1148
1172
|
""
|
|
1149
1173
|
].join("\n");
|
|
1150
1174
|
}
|
|
1175
|
+
function readDeclaredCommandLeaves(settingsPath) {
|
|
1176
|
+
try {
|
|
1177
|
+
const parsed = JSON.parse(node_fs.readFileSync(settingsPath, "utf8"));
|
|
1178
|
+
const leaves = /* @__PURE__ */ new Set();
|
|
1179
|
+
for (const matcherBlocks of Object.values(parsed.hooks ?? {})) {
|
|
1180
|
+
for (const block of matcherBlocks) {
|
|
1181
|
+
for (const entry of block.hooks ?? []) {
|
|
1182
|
+
const cmd = entry.command;
|
|
1183
|
+
if (typeof cmd !== "string" || cmd.length === 0) continue;
|
|
1184
|
+
if (!cmd.startsWith("$")) continue;
|
|
1185
|
+
const leaf = cmd.slice(cmd.lastIndexOf("/") + 1);
|
|
1186
|
+
if (leaf.endsWith(".sh")) leaves.add(leaf);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return leaves;
|
|
1191
|
+
} catch {
|
|
1192
|
+
return /* @__PURE__ */ new Set();
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1151
1195
|
const DEFAULTS$1 = {
|
|
1152
1196
|
force: false,
|
|
1153
1197
|
replaceEdits: false,
|
package/dist/cli.js
CHANGED
|
@@ -525,8 +525,11 @@ function copySubstrate(targetDir, options = {}) {
|
|
|
525
525
|
for (const [directory, groupFiles] of groups) {
|
|
526
526
|
let completedInGroup = 0;
|
|
527
527
|
for (const relPath of groupFiles) {
|
|
528
|
-
const
|
|
529
|
-
|
|
528
|
+
const decisions = decisionsForFile(relPath, scopeMap, targetDir, options);
|
|
529
|
+
for (const decision of decisions) {
|
|
530
|
+
const outcome = copyOne(relPath, bundleRoot, options, result, decision);
|
|
531
|
+
if (outcome !== "skipped") completedInGroup += 1;
|
|
532
|
+
}
|
|
530
533
|
}
|
|
531
534
|
if (options.onProgress) options.onProgress(directory, completedInGroup);
|
|
532
535
|
}
|
|
@@ -633,12 +636,30 @@ function buildScopeMap(bundleRoot, targetDir, options, bundleHookRelPaths) {
|
|
|
633
636
|
function isHookFile(relPath) {
|
|
634
637
|
return relPath.startsWith(HOOKS_SUBPATH) && relPath.endsWith(".sh");
|
|
635
638
|
}
|
|
636
|
-
function
|
|
639
|
+
function decisionsForFile(relPath, scopeMap, targetDir, options) {
|
|
640
|
+
if (!isHookFile(relPath)) {
|
|
641
|
+
return [{ scope: "project", targetPath: join(targetDir, mapAdopterPath(relPath)) }];
|
|
642
|
+
}
|
|
643
|
+
const declared = scopeMap.get(relPath);
|
|
644
|
+
if (declared) {
|
|
645
|
+
return [declared];
|
|
646
|
+
}
|
|
647
|
+
const allowRoot = options.allowRoot ?? false;
|
|
648
|
+
const userDecision = classify$1(
|
|
649
|
+
{ command: `$HOME/${relPath}` },
|
|
650
|
+
{ targetDir, allowRoot }
|
|
651
|
+
);
|
|
652
|
+
const projectDecision = classify$1(
|
|
653
|
+
{ command: `$CLAUDE_PROJECT_DIR/${relPath}` },
|
|
654
|
+
{ targetDir, allowRoot }
|
|
655
|
+
);
|
|
656
|
+
return [userDecision, projectDecision];
|
|
657
|
+
}
|
|
658
|
+
function copyOne(relPath, bundleRoot, options, result, scopeDecision) {
|
|
637
659
|
const sourcePath = join(bundleRoot, relPath);
|
|
638
660
|
const adopterRelPath = mapAdopterPath(relPath);
|
|
639
|
-
const
|
|
640
|
-
const
|
|
641
|
-
const scope = scopeDecision ? scopeDecision.scope : "project";
|
|
661
|
+
const targetPath = scopeDecision.targetPath;
|
|
662
|
+
const scope = scopeDecision.scope;
|
|
642
663
|
let content;
|
|
643
664
|
try {
|
|
644
665
|
content = readFileSync(sourcePath, "utf8");
|
|
@@ -896,8 +917,11 @@ function dispatchSubstrateCopy(targetDir, force, verbose, dryRun, allowRoot, jso
|
|
|
896
917
|
`bassclef init: ${grandTotal} files total (1 config + ${result.copied.length} substrate).
|
|
897
918
|
`
|
|
898
919
|
);
|
|
920
|
+
const declaredCommandLeaves = readDeclaredCommandLeaves(
|
|
921
|
+
join(targetDir, ".claude", "settings.json")
|
|
922
|
+
);
|
|
899
923
|
const copiedHookEntries = result.copiedEntries.filter(
|
|
900
|
-
(e) => e.path.startsWith(HOOKS_SUBPATH) && e.path.endsWith(".sh")
|
|
924
|
+
(e) => e.path.startsWith(HOOKS_SUBPATH) && e.path.endsWith(".sh") && declaredCommandLeaves.has(basename$1(e.path))
|
|
901
925
|
);
|
|
902
926
|
const copiedCount = copiedHookEntries.length;
|
|
903
927
|
const declaredCount = result.hookCount;
|
|
@@ -1125,6 +1149,26 @@ function usage$2() {
|
|
|
1125
1149
|
""
|
|
1126
1150
|
].join("\n");
|
|
1127
1151
|
}
|
|
1152
|
+
function readDeclaredCommandLeaves(settingsPath) {
|
|
1153
|
+
try {
|
|
1154
|
+
const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
1155
|
+
const leaves = /* @__PURE__ */ new Set();
|
|
1156
|
+
for (const matcherBlocks of Object.values(parsed.hooks ?? {})) {
|
|
1157
|
+
for (const block of matcherBlocks) {
|
|
1158
|
+
for (const entry of block.hooks ?? []) {
|
|
1159
|
+
const cmd = entry.command;
|
|
1160
|
+
if (typeof cmd !== "string" || cmd.length === 0) continue;
|
|
1161
|
+
if (!cmd.startsWith("$")) continue;
|
|
1162
|
+
const leaf = cmd.slice(cmd.lastIndexOf("/") + 1);
|
|
1163
|
+
if (leaf.endsWith(".sh")) leaves.add(leaf);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return leaves;
|
|
1168
|
+
} catch {
|
|
1169
|
+
return /* @__PURE__ */ new Set();
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1128
1172
|
const DEFAULTS$1 = {
|
|
1129
1173
|
force: false,
|
|
1130
1174
|
replaceEdits: false,
|
package/dist/index.cjs
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const version: "1.0.
|
|
1
|
+
export declare const version: "1.0.4";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# 00-bassclef-hook-connect.sh — Auto-connect bassclef hooks to adopter settings.json.
|
|
4
|
+
#
|
|
5
|
+
# Closes sunj-labs/bassclef-upstream#644 — sync-cure peer path fix.
|
|
6
|
+
#
|
|
7
|
+
# Runs at every session-start via the session-reflection dispatcher.
|
|
8
|
+
# Sources presence/install/bassclef-hook-connect.sh (shared function)
|
|
9
|
+
# and calls bassclef_hook_connect_apply. Fires regardless of which sync
|
|
10
|
+
# path (peer, clone, npm, curl) delivered bassclef to the adopter.
|
|
11
|
+
#
|
|
12
|
+
# Sorts BEFORE 08-settings-drift.sh in the dispatcher's lexicographic
|
|
13
|
+
# scan (session-reflection.sh L135), so hooks are merged before drift
|
|
14
|
+
# is reported. First POA session after release shows 0 missing entries.
|
|
15
|
+
#
|
|
16
|
+
# SOURCED (not exec'd) by session-reflection.sh — uses `return 0` NOT
|
|
17
|
+
# `exit 0` so a skip path does not kill sister fragments. The tail
|
|
18
|
+
# `|| true` on `return` is the standard sourced-fragment idiom (from
|
|
19
|
+
# 08-settings-drift.sh) — lets the file also work when bash-invoked
|
|
20
|
+
# directly (in tests) where `return` at top level is an error.
|
|
21
|
+
#
|
|
22
|
+
# Anchor luminaries — @luminary michael-nygard (steady state — merge
|
|
23
|
+
# self-heals per session-start), @luminary alistair-cockburn (walking
|
|
24
|
+
# skeleton — thinnest end-to-end path), @luminary john-ousterhout
|
|
25
|
+
# (signal visibility — every merge run emits stderr line), @luminary
|
|
26
|
+
# michael-feathers (characterization test with fixture parent path
|
|
27
|
+
# OUTSIDE ~/src/sunj-labs/ closes the specific hole).
|
|
28
|
+
#
|
|
29
|
+
# Bootstrap pair — this fragment closes the peer-path gap. Sister:
|
|
30
|
+
# presence/install/bassclef-sync.template.sh:682 retains the clone-path
|
|
31
|
+
# call as belt + suspenders.
|
|
32
|
+
|
|
33
|
+
# BASSCLEF_DIR is set by the parent dispatcher (session-reflection.sh L33)
|
|
34
|
+
# when this fragment is sourced. CWD is set by the parent dispatcher
|
|
35
|
+
# (session-reflection.sh L14). When executed directly (bash <fragment>),
|
|
36
|
+
# env vars come from the caller.
|
|
37
|
+
|
|
38
|
+
if [ -z "${BASSCLEF_DIR:-}" ]; then
|
|
39
|
+
# No bassclef root resolved. Fragment cannot run. Silent skip.
|
|
40
|
+
return 0 2>/dev/null || exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Ousterhout signal-visibility — emit one stderr line per session-start
|
|
44
|
+
# whenever BASSCLEF_DIR is set, before any conditional skip. Silent
|
|
45
|
+
# success is as dangerous as silent failure.
|
|
46
|
+
echo "[bassclef-hook-connect] session-reflection.d: attempting merge for CWD=${CWD:-$PWD}" >&2
|
|
47
|
+
|
|
48
|
+
__bhc_lib="${BASSCLEF_DIR}/presence/install/bassclef-hook-connect.sh"
|
|
49
|
+
if [ ! -f "$__bhc_lib" ]; then
|
|
50
|
+
echo "[bassclef-hook-connect] skip — shared function not found at $__bhc_lib" >&2
|
|
51
|
+
unset __bhc_lib
|
|
52
|
+
return 0 2>/dev/null || exit 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Source the shared function (defines bassclef_hook_connect_apply).
|
|
56
|
+
# shellcheck disable=SC1090
|
|
57
|
+
source "$__bhc_lib"
|
|
58
|
+
|
|
59
|
+
# Apply the merge. Never break the dispatcher — return 0 on all paths.
|
|
60
|
+
bassclef_hook_connect_apply "${CWD:-$PWD}" "$BASSCLEF_DIR" || true
|
|
61
|
+
|
|
62
|
+
unset __bhc_lib
|
|
63
|
+
return 0 2>/dev/null || exit 0
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# tier: lite
|
|
2
|
+
# install-class: dual
|
|
3
|
+
# 05-active-bet.sh — Surface in_flight_bet from whereami at SessionStart
|
|
4
|
+
#
|
|
5
|
+
# Per architecture/audits/2026-06-30-session-orientation-blind-spot.md cure 1
|
|
6
|
+
# (sister cures: 55-orientation-gate.sh fires on main + /longrun SKILL Step 0.4).
|
|
7
|
+
#
|
|
8
|
+
# The audit's root cause was the agent narrowing scope to "bet 29e wrap" from
|
|
9
|
+
# a stale branch name. Whereami had `in_flight_bet: bet 30a` and the recent
|
|
10
|
+
# commits named bet 30a, but the agent did not read either. This hook puts
|
|
11
|
+
# the in_flight_bet at the TOP of the SessionStart output so the agent sees
|
|
12
|
+
# it before any subsequent reasoning.
|
|
13
|
+
#
|
|
14
|
+
# Behavior:
|
|
15
|
+
# - Fires at SessionStart in every repo with .claude/ wired
|
|
16
|
+
# - Reads docs/whereami.md from CWD (the project's whereami, not $HOME)
|
|
17
|
+
# - When in_flight_bet block present: emits "### ACTIVE BET" header + slug + status + remaining_wus + goal doc pointer + cure source citation
|
|
18
|
+
# - When in_flight_bet absent: emits "### ACTIVE BET — no in-flight bet" one-line note
|
|
19
|
+
# - When whereami absent: emits "### ACTIVE BET — no in-flight bet (whereami absent)" one-line note
|
|
20
|
+
# - Always exit 0 (informational; SessionStart hooks do not block)
|
|
21
|
+
# - SKIP_ACTIVE_BET=1 silences output entirely
|
|
22
|
+
#
|
|
23
|
+
# Override (logged):
|
|
24
|
+
# SKIP_ACTIVE_BET=1 <command>
|
|
25
|
+
#
|
|
26
|
+
# Closes 1 of 5 sub-cures under sunj-labs/bassclef-upstream#540.
|
|
27
|
+
|
|
28
|
+
# Override path — silent, log via trace-helper if present
|
|
29
|
+
if [ "${SKIP_ACTIVE_BET:-0}" = "1" ]; then
|
|
30
|
+
if [ -x ".claude/hooks/trace-helper.sh" ]; then
|
|
31
|
+
bash .claude/hooks/trace-helper.sh "active-bet" "skipped via SKIP_ACTIVE_BET=1" 2>/dev/null || true
|
|
32
|
+
fi
|
|
33
|
+
return 0 2>/dev/null || exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Resolve whereami path. The hook reads from the project's whereami, not
|
|
37
|
+
# from $HOME or the operator-install location. Per criterion 11 (path
|
|
38
|
+
# resolution under both install classes): CWD/docs/whereami.md works under
|
|
39
|
+
# both project install (CLAUDE_PROJECT_DIR=CWD) and operator install
|
|
40
|
+
# (HOME=/some/other; CWD=project root).
|
|
41
|
+
WHEREAMI="docs/whereami.md"
|
|
42
|
+
|
|
43
|
+
# Silent-but-print one-liner when whereami absent
|
|
44
|
+
if [ ! -f "$WHEREAMI" ]; then
|
|
45
|
+
echo ""
|
|
46
|
+
echo "### ACTIVE BET — no in-flight bet (whereami absent)"
|
|
47
|
+
echo ""
|
|
48
|
+
return 0 2>/dev/null || exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Extract the in_flight_bet block. Whereami uses Shape B markdown YAML
|
|
52
|
+
# frontmatter. The in_flight_bet block looks like:
|
|
53
|
+
#
|
|
54
|
+
# in_flight_bet:
|
|
55
|
+
# bet: docs/iteration-bets/<slug>.md
|
|
56
|
+
# status: <status>
|
|
57
|
+
# remaining_wus: [<list>]
|
|
58
|
+
#
|
|
59
|
+
# awk extracts lines from `in_flight_bet:` until the next top-level key
|
|
60
|
+
# (a line starting with a non-space character that ends in `:`).
|
|
61
|
+
BLOCK=$(awk '
|
|
62
|
+
/^in_flight_bet:/ { in_block=1; next }
|
|
63
|
+
in_block && /^[a-zA-Z][^:]*:/ { in_block=0 }
|
|
64
|
+
in_block { print }
|
|
65
|
+
' "$WHEREAMI" 2>/dev/null)
|
|
66
|
+
|
|
67
|
+
# When block is empty, the field is absent
|
|
68
|
+
if [ -z "$BLOCK" ]; then
|
|
69
|
+
echo ""
|
|
70
|
+
echo "### ACTIVE BET — no in-flight bet"
|
|
71
|
+
echo ""
|
|
72
|
+
echo "Whereami present but no in_flight_bet field. If you expect one, check"
|
|
73
|
+
echo " docs/whereami.md for the in_flight_bet: block."
|
|
74
|
+
echo ""
|
|
75
|
+
return 0 2>/dev/null || exit 0
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# Extract goal slug: the goal. line under in_flight_bet, then strip path + .md
|
|
79
|
+
BET_PATH=$(echo "$BLOCK" | grep -E "^[[:space:]]+bet:" | head -1 | sed -E 's/^[[:space:]]+bet:[[:space:]]*//')
|
|
80
|
+
BET_SLUG=$(echo "$BET_PATH" | sed -E 's|^.*/||; s|\.md$||')
|
|
81
|
+
|
|
82
|
+
# Extract status: best-effort, may carry parenthetical suffix
|
|
83
|
+
STATUS=$(echo "$BLOCK" | grep -E "^[[:space:]]+status:" | head -1 | sed -E 's/^[[:space:]]+status:[[:space:]]*//')
|
|
84
|
+
|
|
85
|
+
# Extract remaining_wus: best-effort, the bracketed list
|
|
86
|
+
REMAINING=$(echo "$BLOCK" | grep -E "^[[:space:]]+remaining_wus:" | head -1 | sed -E 's/^[[:space:]]+remaining_wus:[[:space:]]*//')
|
|
87
|
+
|
|
88
|
+
# Emit the ACTIVE BET block — header + tagline + fields + cure source
|
|
89
|
+
echo ""
|
|
90
|
+
echo "### ACTIVE BET — read before scoping work"
|
|
91
|
+
echo ""
|
|
92
|
+
if [ -n "$BET_SLUG" ]; then
|
|
93
|
+
echo " bet: ${BET_SLUG}"
|
|
94
|
+
fi
|
|
95
|
+
if [ -n "$STATUS" ]; then
|
|
96
|
+
echo " status: ${STATUS}"
|
|
97
|
+
fi
|
|
98
|
+
if [ -n "$REMAINING" ]; then
|
|
99
|
+
echo " remaining_wus: ${REMAINING}"
|
|
100
|
+
fi
|
|
101
|
+
if [ -n "$BET_PATH" ]; then
|
|
102
|
+
echo ""
|
|
103
|
+
echo " Read the goal doc before picking scope:"
|
|
104
|
+
echo " ${BET_PATH}"
|
|
105
|
+
fi
|
|
106
|
+
echo ""
|
|
107
|
+
echo " Cure source: architecture/audits/2026-06-30-session-orientation-blind-spot.md"
|
|
108
|
+
echo " Sister cures: 55-orientation-gate.sh fires on main + /longrun SKILL Step 0.4"
|
|
109
|
+
echo " Closes 1 of 5 sub-cures under sunj-labs/bassclef-upstream#540."
|
|
110
|
+
echo ""
|
|
111
|
+
echo "Override (logged): SKIP_ACTIVE_BET=1"
|
|
112
|
+
echo ""
|
|
113
|
+
return 0 2>/dev/null || exit 0
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# tier: lite
|
|
2
|
+
# Drift detector — fires at every SessionStart via session-reflection.sh.
|
|
3
|
+
# Compares required wirings (per project settings.json) against loaded
|
|
4
|
+
# wirings (per scope-stack-merged settings). Emits BLOCKED banner if drift.
|
|
5
|
+
#
|
|
6
|
+
# Closes the bug class diagnosed 2026-06-24: substrate ships new wiring,
|
|
7
|
+
# operator's user-scope settings.json never picks it up, session-reflection.d/*
|
|
8
|
+
# modules silently never fire.
|
|
9
|
+
#
|
|
10
|
+
# Sourced (not exec'd) by session-reflection.sh. Inherits:
|
|
11
|
+
# - blocked_banner() function
|
|
12
|
+
# - trace_log() function (via trace-helper.sh sourced in parent)
|
|
13
|
+
# - BASSCLEF_DIR environment variable
|
|
14
|
+
#
|
|
15
|
+
# Per .claude/rules/blocked-items.md: BLOCKED items must be resolved
|
|
16
|
+
# or explicitly deferred. Per .claude/rules/defensive-bash.md: trace
|
|
17
|
+
# logging at every exit; quote variables; local in functions.
|
|
18
|
+
# Per docs/decompositions/2026-06-24-adopter-update-distribution.md.
|
|
19
|
+
# WU-2 of bet 2026-06-24a-settings-hardening (#313).
|
|
20
|
+
|
|
21
|
+
# Override path — operator can bypass per-session.
|
|
22
|
+
if [ "${SKIP_SETTINGS_DRIFT:-0}" = "1" ]; then
|
|
23
|
+
trace_log "skip" "settings-drift" "SKIP_SETTINGS_DRIFT=1" 2>/dev/null || true
|
|
24
|
+
return 0 2>/dev/null || true
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Source the shared wirings accessor (per decomposition § Interface Registry).
|
|
28
|
+
__sd_wirings_lib="${BASSCLEF_DIR:-${PWD}}/.claude/hooks/_lib/wirings.sh"
|
|
29
|
+
if [ ! -f "$__sd_wirings_lib" ]; then
|
|
30
|
+
# Substrate not fully synced; sister modules surface the larger issue.
|
|
31
|
+
trace_log "skip" "settings-drift" "wirings lib not yet synced" 2>/dev/null || true
|
|
32
|
+
unset __sd_wirings_lib
|
|
33
|
+
return 0 2>/dev/null || true
|
|
34
|
+
fi
|
|
35
|
+
# shellcheck source=/dev/null
|
|
36
|
+
source "$__sd_wirings_lib"
|
|
37
|
+
|
|
38
|
+
__sd_project_settings="${BASSCLEF_DIR:-${PWD}}/.claude/settings.json"
|
|
39
|
+
|
|
40
|
+
__sd_required=$(read_required_wirings "$__sd_project_settings")
|
|
41
|
+
__sd_loaded=$(read_loaded_wirings)
|
|
42
|
+
__sd_diff=$(compare_wirings "$__sd_required" "$__sd_loaded")
|
|
43
|
+
__sd_ok=$(echo "$__sd_diff" | jq -r '.ok')
|
|
44
|
+
|
|
45
|
+
if [ "$__sd_ok" = "true" ]; then
|
|
46
|
+
trace_log "ok" "settings-drift" "no drift" 2>/dev/null || true
|
|
47
|
+
else
|
|
48
|
+
__sd_missing_count=$(echo "$__sd_diff" | jq '.missing | length')
|
|
49
|
+
__sd_missing_list=$(echo "$__sd_diff" | jq -r '.missing[] | " - " + .event + " → " + .command')
|
|
50
|
+
blocked_banner "settings-drift — ${__sd_missing_count} required hook wiring(s) missing from harness-loaded settings:
|
|
51
|
+
|
|
52
|
+
${__sd_missing_list}
|
|
53
|
+
|
|
54
|
+
Root cause class: substrate ships new wiring; operator's settings.json never picked it up.
|
|
55
|
+
Resolve: bassclef-sync.sh self-heal will additively merge on next session-start once it ships (#313 WU-3). For now, run manually: bash ~/.claude/hooks/bassclef-sync.sh
|
|
56
|
+
Override: SKIP_SETTINGS_DRIFT=1 (one-off bypass; logged to trace)"
|
|
57
|
+
trace_log "blocked" "settings-drift" "missing=${__sd_missing_count}" 2>/dev/null || true
|
|
58
|
+
unset __sd_missing_count __sd_missing_list
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
unset __sd_wirings_lib __sd_project_settings __sd_required __sd_loaded __sd_diff __sd_ok
|
|
62
|
+
return 0 2>/dev/null || true
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# tier: lite
|
|
2
|
+
# Abrupt-stop detection: dirty working tree indicates the previous
|
|
3
|
+
# session-end protocol was skipped. Surface for recovery.
|
|
4
|
+
#
|
|
5
|
+
# bassclef#888 fix (Phase B WU-B2): filter routine bookkeeping marker
|
|
6
|
+
# writes that happen every session-start. Without this filter, the
|
|
7
|
+
# session-timing marker bumped at every boot surfaces as a false
|
|
8
|
+
# "abrupt stop" gate, training operators + future Claude sessions to
|
|
9
|
+
# disregard the banner as noise.
|
|
10
|
+
#
|
|
11
|
+
# bassclef-web#103 fix: extend allowlist to cover two more false-positive
|
|
12
|
+
# classes surfaced by adopter sessions:
|
|
13
|
+
# Class A — .playwright-mcp/ transient exhaust from Playwright MCP browser
|
|
14
|
+
# sessions (visual-review, visual-qa, synthetic-user skills)
|
|
15
|
+
# Class C — docs/deferred-actions/*-session-rescue.md files that the
|
|
16
|
+
# session-end hook writes on abrupt stop; the detector then
|
|
17
|
+
# sees them as untracked and fires the banner about itself
|
|
18
|
+
#
|
|
19
|
+
# Class B (sync-lag on new substrate primitives landing before .gitignore
|
|
20
|
+
# settles) is DEFERRED to a follow-on ticket — that class needs a
|
|
21
|
+
# sync-complete marker sequenced from bassclef-sync, wider scope than
|
|
22
|
+
# this cure.
|
|
23
|
+
#
|
|
24
|
+
# Allowlisted path prefixes (substrate runtime bookkeeping that's
|
|
25
|
+
# expected to be dirty at session-start):
|
|
26
|
+
# state/markers/session-timing/ — bumped by session-timing hook
|
|
27
|
+
# state/markers/edit-count/ — budget-tracking, auto-written
|
|
28
|
+
# state/markers/verify/ — /verify writes
|
|
29
|
+
# state/markers/temperance/ — /temperance writes
|
|
30
|
+
# state/markers/diagnose/ — /diagnose writes
|
|
31
|
+
# state/markers/compliance-history/ — gate-compliance counter writes
|
|
32
|
+
# .playwright-mcp/ — Playwright MCP transient (Class A)
|
|
33
|
+
# docs/deferred-actions/*-session-rescue.md — rescue files (Class C)
|
|
34
|
+
#
|
|
35
|
+
# Real work (src/, docs/ outside the rescue pattern, .claude/, etc.)
|
|
36
|
+
# still surfaces the banner. Non-rescue deferred-action files (e.g.,
|
|
37
|
+
# journal-backfill, wiki-refresh) still fire the banner.
|
|
38
|
+
|
|
39
|
+
# -uall expands untracked-directory summaries to individual file paths
|
|
40
|
+
# so the per-file allowlist match below can recognize each path.
|
|
41
|
+
# Without -uall, a new file inside a previously empty marker dir would
|
|
42
|
+
# appear as "?? state/" (dir summary), bypassing the allowlist filter.
|
|
43
|
+
DIRTY_TREE_RAW=$(git status --porcelain -uall 2>/dev/null)
|
|
44
|
+
|
|
45
|
+
# Strip the 3-char porcelain status prefix (XY + space) before matching
|
|
46
|
+
# path prefixes, then re-emit the original line so the user sees the
|
|
47
|
+
# git-status-shaped output if banner fires.
|
|
48
|
+
DIRTY_TREE=$(echo "$DIRTY_TREE_RAW" | awk '
|
|
49
|
+
{
|
|
50
|
+
path = substr($0, 4)
|
|
51
|
+
# Class 0 (bassclef#888): substrate runtime bookkeeping markers
|
|
52
|
+
if (path ~ /^state\/markers\/(session-timing|edit-count|verify|temperance|diagnose|compliance-history)\//) {
|
|
53
|
+
next
|
|
54
|
+
}
|
|
55
|
+
# Class A (bassclef-web#103): Playwright MCP transient exhaust
|
|
56
|
+
if (path ~ /^\.playwright-mcp\//) {
|
|
57
|
+
next
|
|
58
|
+
}
|
|
59
|
+
# Class C (bassclef-web#103): session-rescue files written by session-end.sh
|
|
60
|
+
if (path ~ /^docs\/deferred-actions\/.*-session-rescue\.md$/) {
|
|
61
|
+
next
|
|
62
|
+
}
|
|
63
|
+
print
|
|
64
|
+
}' | head -5)
|
|
65
|
+
|
|
66
|
+
if [ -n "$DIRTY_TREE" ]; then
|
|
67
|
+
echo "### ABRUPT STOP DETECTED — RECOVERY NEEDED"
|
|
68
|
+
echo ""
|
|
69
|
+
echo "Uncommitted changes found:"
|
|
70
|
+
echo "$DIRTY_TREE"
|
|
71
|
+
echo ""
|
|
72
|
+
echo "→ Ask the user: commit these, stash, or discard?"
|
|
73
|
+
echo ""
|
|
74
|
+
echo "Session-end protocol was likely skipped. Check:"
|
|
75
|
+
echo "- Are any issues stuck in 'In Progress' that should be Done?"
|
|
76
|
+
echo "- Was memory saved from the last session?"
|
|
77
|
+
echo ""
|
|
78
|
+
fi
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# tier: lite
|
|
2
|
+
# SESSION_LOCK presence: surface to operator + future agents that
|
|
3
|
+
# auto-save is suppressed and another session may be active. Without
|
|
4
|
+
# this signal, the next session opens, sees "up-to-date," removes the
|
|
5
|
+
# lock thinking it's stale → auto-save resumes silently → atomic-PR
|
|
6
|
+
# bypass returns. Per bassclef#582 (Phase B WU-B4).
|
|
7
|
+
#
|
|
8
|
+
# Self-recognition (bassclef#1155, WU-1 of bet 2026-06-18a):
|
|
9
|
+
# Back-port the pgid match shipped in save-state.sh L123-130 and
|
|
10
|
+
# session-end.sh Guard 4. When the lock's pid is in the SAME process
|
|
11
|
+
# group as the current shell, the lock is ours — emit an informational
|
|
12
|
+
# "is ours" line instead of the (misleading) "another session" warning.
|
|
13
|
+
|
|
14
|
+
LOCK_FILE=".claude/SESSION_LOCK"
|
|
15
|
+
|
|
16
|
+
if [ -f "$LOCK_FILE" ]; then
|
|
17
|
+
LOCK_CONTENT=$(head -3 "$LOCK_FILE" 2>/dev/null)
|
|
18
|
+
LOCK_PID=$(grep -E '^pid:' "$LOCK_FILE" 2>/dev/null | head -1 | sed -E 's/^pid:[[:space:]]*//' | tr -d '[:space:]')
|
|
19
|
+
|
|
20
|
+
# Try pgid self-recognition first. If our pgid matches the lock pid's
|
|
21
|
+
# pgid, the lock was written by our session (Claude harness writes the
|
|
22
|
+
# lock; this hook runs as descendant in the same job/pgid).
|
|
23
|
+
if [ -n "$LOCK_PID" ] \
|
|
24
|
+
&& LOCK_PGID=$(ps -o pgid= -p "$LOCK_PID" 2>/dev/null | tr -d ' ') \
|
|
25
|
+
&& OUR_PGID=$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ') \
|
|
26
|
+
&& [ -n "$LOCK_PGID" ] && [ -n "$OUR_PGID" ] \
|
|
27
|
+
&& [ "$LOCK_PGID" = "$OUR_PGID" ]; then
|
|
28
|
+
echo "### SESSION_LOCK is ours (same pgid $OUR_PGID) — proceeding"
|
|
29
|
+
echo ""
|
|
30
|
+
else
|
|
31
|
+
# Foreign pid, dead pid, or malformed lock — surface the warning.
|
|
32
|
+
# Try to read mtime as the lock-create time.
|
|
33
|
+
if LOCK_MTIME=$(stat -c %Y "$LOCK_FILE" 2>/dev/null) || \
|
|
34
|
+
LOCK_MTIME=$(stat -f %m "$LOCK_FILE" 2>/dev/null); then
|
|
35
|
+
NOW=$(date -u +%s)
|
|
36
|
+
AGE_SEC=$(( NOW - LOCK_MTIME ))
|
|
37
|
+
AGE_MIN=$(( AGE_SEC / 60 ))
|
|
38
|
+
AGE_HOUR=$(( AGE_MIN / 60 ))
|
|
39
|
+
if [ "$AGE_HOUR" -gt 0 ]; then
|
|
40
|
+
AGE_LABEL="${AGE_HOUR}h"
|
|
41
|
+
elif [ "$AGE_MIN" -gt 0 ]; then
|
|
42
|
+
AGE_LABEL="${AGE_MIN}m"
|
|
43
|
+
else
|
|
44
|
+
AGE_LABEL="${AGE_SEC}s"
|
|
45
|
+
fi
|
|
46
|
+
else
|
|
47
|
+
AGE_LABEL="(age unknown)"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
echo "### SESSION_LOCK PRESENT — auto-save suppressed"
|
|
51
|
+
echo ""
|
|
52
|
+
echo " Path: $LOCK_FILE"
|
|
53
|
+
echo " Age: $AGE_LABEL"
|
|
54
|
+
if [ -n "$LOCK_CONTENT" ]; then
|
|
55
|
+
echo " Content:"
|
|
56
|
+
echo "$LOCK_CONTENT" | sed 's/^/ /'
|
|
57
|
+
fi
|
|
58
|
+
echo ""
|
|
59
|
+
echo " → Another session may be active. Confirm before removing the"
|
|
60
|
+
echo " lock; removing it resumes auto-save in this repo."
|
|
61
|
+
echo ""
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# 15-substrate-config-validate.sh
|
|
4
|
+
#
|
|
5
|
+
# bassclef#591 — substrate.config.md presence + basic-coherence check.
|
|
6
|
+
#
|
|
7
|
+
# Today: thin presence check + required-section presence verification.
|
|
8
|
+
# Full JSON Schema validation deferred per bassclef#643 (migration of
|
|
9
|
+
# substrate.config.md to YAML frontmatter; schema already exists at
|
|
10
|
+
# standards/state-spine/schemas/substrate-config.schema.json).
|
|
11
|
+
#
|
|
12
|
+
# Fires at session-start as part of session-reflection.d/* chain.
|
|
13
|
+
# Surfaces BLOCKED if substrate.config.md is missing OR missing key
|
|
14
|
+
# sections that downstream hooks/skills depend on.
|
|
15
|
+
#
|
|
16
|
+
# Override: SKIP_SUBSTRATE_CONFIG_VALIDATE=1
|
|
17
|
+
#
|
|
18
|
+
# Per bassclef#639 — operates ONLY on $CWD; no cross-repo execution.
|
|
19
|
+
|
|
20
|
+
set -u
|
|
21
|
+
|
|
22
|
+
if [ "${SKIP_SUBSTRATE_CONFIG_VALIDATE:-0}" = "1" ]; then
|
|
23
|
+
return 0 2>/dev/null || exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Some sessions provide CWD via stdin JSON; fall back to $PWD if not.
|
|
27
|
+
CWD="${CWD:-$PWD}"
|
|
28
|
+
CONFIG_FILE="$CWD/substrate.config.md"
|
|
29
|
+
|
|
30
|
+
# Not all repos have substrate.config.md (fresh consumer pre-onboarding).
|
|
31
|
+
# Skip silently if absent; bassclef-sync's onboard path prompts when needed.
|
|
32
|
+
if [ ! -f "$CONFIG_FILE" ]; then
|
|
33
|
+
return 0 2>/dev/null || exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Required sections (header style: `## SectionName`).
|
|
37
|
+
# Downstream hooks (session-reflection.sh + others) grep for these section
|
|
38
|
+
# fields; absence is observable behavior, not crash.
|
|
39
|
+
REQUIRED_SECTIONS="Configuration"
|
|
40
|
+
MISSING=""
|
|
41
|
+
|
|
42
|
+
for section in $REQUIRED_SECTIONS; do
|
|
43
|
+
if ! grep -q "^## $section" "$CONFIG_FILE" 2>/dev/null; then
|
|
44
|
+
MISSING="$MISSING $section"
|
|
45
|
+
fi
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
# Required fields by line-prefix (legacy markdown style — pre-YAML-frontmatter).
|
|
49
|
+
REQUIRED_FIELDS="gating execution_mode"
|
|
50
|
+
for field in $REQUIRED_FIELDS; do
|
|
51
|
+
if ! grep -q "^$field:" "$CONFIG_FILE" 2>/dev/null; then
|
|
52
|
+
MISSING="$MISSING $field:"
|
|
53
|
+
fi
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
if [ -n "$MISSING" ]; then
|
|
57
|
+
cat >&2 <<EOF
|
|
58
|
+
|
|
59
|
+
================================================================
|
|
60
|
+
⚠️ SUBSTRATE.CONFIG DRIFT — INFORM (Class B cure per #1157)
|
|
61
|
+
================================================================
|
|
62
|
+
|
|
63
|
+
File: $CONFIG_FILE
|
|
64
|
+
Missing:$MISSING
|
|
65
|
+
|
|
66
|
+
substrate.config.md is the single source of truth for external
|
|
67
|
+
resource references (per standards/substrate-config-schema.md).
|
|
68
|
+
Missing sections/fields above are expected by downstream hooks.
|
|
69
|
+
Session continues — this WARN does not block SessionStart.
|
|
70
|
+
|
|
71
|
+
Resolution (do at your convenience):
|
|
72
|
+
- If field/section was intentionally removed, restore it (set to a
|
|
73
|
+
placeholder value if not yet decided)
|
|
74
|
+
- If file is being bootstrapped fresh, see presence/install/
|
|
75
|
+
for the substrate-config.md template
|
|
76
|
+
|
|
77
|
+
Override (logged): SKIP_SUBSTRATE_CONFIG_VALIDATE=1
|
|
78
|
+
|
|
79
|
+
Full JSON Schema validation (against
|
|
80
|
+
standards/state-spine/schemas/substrate-config.schema.json) is
|
|
81
|
+
deferred — substrate.config.md needs migration to YAML frontmatter
|
|
82
|
+
first; tracked at bassclef#643.
|
|
83
|
+
|
|
84
|
+
Closes bassclef#1157 (BLOCK→WARN severity ladder).
|
|
85
|
+
Prior contract: bassclef#591 (thin-validator half; full-schema half deferred).
|
|
86
|
+
================================================================
|
|
87
|
+
EOF
|
|
88
|
+
return 0 2>/dev/null || exit 0
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
return 0 2>/dev/null || exit 0
|