@thebassclef/lite 1.4.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.cjs +237 -3
- package/dist/cli.js +238 -4
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/hooks/pre-commit-manifest-autoregen.sh +28 -3
- package/dist/lite/.claude/settings.json +5 -1
- package/dist/lite/bassclef-version.json +7 -0
- package/dist/lite/lib/mechanism-fidelity.sh +48 -4
- package/dist/lite/lib/source-graph-walker.sh +29 -2
- package/dist/lite/presence/cli/bassclef-statusline.dispatcher.sh +78 -0
- package/dist/lite/presence/cli/bassclef-statusline.sh +78 -0
- package/dist/lite/standards/lite-manifest.json +7 -7
- package/dist/lite/standards/mechanism-fidelity.md +1 -0
- package/dist/lite/standards/substrate-config-schema.md +2 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ See [`standards/os-support.md`](standards/os-support.md) for the full policy.
|
|
|
35
35
|
|
|
36
36
|
## Current release
|
|
37
37
|
|
|
38
|
-
<!-- version-start -->1.
|
|
38
|
+
<!-- version-start -->1.6.0<!-- version-end -->
|
|
39
39
|
|
|
40
40
|
See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
41
41
|
|
package/dist/cli.cjs
CHANGED
|
@@ -38,7 +38,8 @@ const DEFAULTS$2 = {
|
|
|
38
38
|
allowAnyDir: false,
|
|
39
39
|
dir: void 0,
|
|
40
40
|
yes: false,
|
|
41
|
-
json: false
|
|
41
|
+
json: false,
|
|
42
|
+
skipStatusline: false
|
|
42
43
|
};
|
|
43
44
|
let ArgvError$1 = class ArgvError extends Error {
|
|
44
45
|
name = "ArgvError";
|
|
@@ -83,6 +84,11 @@ function parseInitArgs(argv) {
|
|
|
83
84
|
i += 1;
|
|
84
85
|
continue;
|
|
85
86
|
}
|
|
87
|
+
if (token === "--skip-statusline") {
|
|
88
|
+
out.skipStatusline = true;
|
|
89
|
+
i += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
86
92
|
if (token === "--dir") {
|
|
87
93
|
const value = argv[i + 1];
|
|
88
94
|
if (value === void 0 || value.startsWith("--")) {
|
|
@@ -802,6 +808,149 @@ function dirExists(p) {
|
|
|
802
808
|
return false;
|
|
803
809
|
}
|
|
804
810
|
}
|
|
811
|
+
const STATUSLINE_FIELD = {
|
|
812
|
+
type: "command",
|
|
813
|
+
command: "bash ~/.claude/bassclef-statusline.sh"
|
|
814
|
+
};
|
|
815
|
+
const DISPATCHER_REL = ["dist", "lite", "presence", "cli", "bassclef-statusline.dispatcher.sh"];
|
|
816
|
+
const USER_DISPATCHER_REL = [".claude", "bassclef-statusline.sh"];
|
|
817
|
+
const PROJECT_SETTINGS_REL = [".claude", "settings.json"];
|
|
818
|
+
function installStatusline(opts) {
|
|
819
|
+
if (opts.skip) {
|
|
820
|
+
return {
|
|
821
|
+
dispatcher: skipped(node_path.join(opts.home, ...USER_DISPATCHER_REL)),
|
|
822
|
+
settings: skipped(node_path.join(opts.projectDir, ...PROJECT_SETTINGS_REL))
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
const dispatcherSource = node_path.join(opts.packageDir, ...DISPATCHER_REL);
|
|
826
|
+
if (!node_fs.existsSync(dispatcherSource)) {
|
|
827
|
+
throw new Error(
|
|
828
|
+
`bassclef init: cannot install statusline — bundled dispatcher missing at ${dispatcherSource}. Reinstall @thebassclef/lite or file a bug.`
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
const sourceBody = node_fs.readFileSync(dispatcherSource, "utf8");
|
|
832
|
+
const dispatcher = handleDispatcher(opts, dispatcherSource, sourceBody);
|
|
833
|
+
const settings = handleSettings(opts);
|
|
834
|
+
return { dispatcher, settings };
|
|
835
|
+
}
|
|
836
|
+
function handleDispatcher(opts, sourcePath, sourceBody) {
|
|
837
|
+
const target = node_path.join(opts.home, ...USER_DISPATCHER_REL);
|
|
838
|
+
if (opts.dryRun) {
|
|
839
|
+
return {
|
|
840
|
+
kind: "would-install",
|
|
841
|
+
path: target,
|
|
842
|
+
detail: `Dry-run: would copy ${sourcePath} → ${target} (0755).`
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
if (!node_fs.existsSync(target)) {
|
|
846
|
+
writeExec(target, sourceBody);
|
|
847
|
+
return { kind: "installed", path: target, detail: `Wrote ${target} (0755) from bundled dispatcher.` };
|
|
848
|
+
}
|
|
849
|
+
const st = node_fs.lstatSync(target);
|
|
850
|
+
if (st.isSymbolicLink()) {
|
|
851
|
+
if (!opts.force) {
|
|
852
|
+
return {
|
|
853
|
+
kind: "preserved",
|
|
854
|
+
path: target,
|
|
855
|
+
detail: `${target} is a symlink; refusing to follow. Pass --force to replace.`
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
node_fs.unlinkSync(target);
|
|
859
|
+
writeExec(target, sourceBody);
|
|
860
|
+
return { kind: "replaced", path: target, detail: `Replaced symlink at ${target} with bundled dispatcher.` };
|
|
861
|
+
}
|
|
862
|
+
const existing = node_fs.readFileSync(target, "utf8");
|
|
863
|
+
if (existing === sourceBody) {
|
|
864
|
+
return { kind: "unchanged", path: target, detail: `${target} already matches bundled dispatcher.` };
|
|
865
|
+
}
|
|
866
|
+
if (!opts.force) {
|
|
867
|
+
return {
|
|
868
|
+
kind: "preserved",
|
|
869
|
+
path: target,
|
|
870
|
+
detail: `${target} differs from bundled dispatcher. Pass --force to overwrite.`
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
writeExec(target, sourceBody);
|
|
874
|
+
return { kind: "replaced", path: target, detail: `Overwrote ${target} (--force) with bundled dispatcher.` };
|
|
875
|
+
}
|
|
876
|
+
function handleSettings(opts) {
|
|
877
|
+
const target = node_path.join(opts.projectDir, ...PROJECT_SETTINGS_REL);
|
|
878
|
+
if (opts.settingsPreserved) {
|
|
879
|
+
return {
|
|
880
|
+
kind: "preserved",
|
|
881
|
+
path: target,
|
|
882
|
+
detail: `${target} was preserved by init (--force not passed on existing file). Statusline field not merged. Re-run init with --force to overwrite.`
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
if (opts.dryRun) {
|
|
886
|
+
return {
|
|
887
|
+
kind: "would-install",
|
|
888
|
+
path: target,
|
|
889
|
+
detail: `Dry-run: would set statusLine in ${target}.`
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
const existing = readSettings(target);
|
|
893
|
+
const desired = STATUSLINE_FIELD;
|
|
894
|
+
const current = existing.statusLine;
|
|
895
|
+
if (current === void 0) {
|
|
896
|
+
const next2 = { ...existing, statusLine: desired };
|
|
897
|
+
writeSettings(target, next2);
|
|
898
|
+
return { kind: "installed", path: target, detail: `Wrote statusLine field to ${target}.` };
|
|
899
|
+
}
|
|
900
|
+
if (matchesDesired(current)) {
|
|
901
|
+
return { kind: "unchanged", path: target, detail: `${target} already carries the bassclef statusLine.` };
|
|
902
|
+
}
|
|
903
|
+
if (!opts.force) {
|
|
904
|
+
return {
|
|
905
|
+
kind: "preserved",
|
|
906
|
+
path: target,
|
|
907
|
+
detail: `${target} carries a different statusLine. Pass --force to overwrite.`
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
const next = { ...existing, statusLine: desired };
|
|
911
|
+
writeSettings(target, next);
|
|
912
|
+
return { kind: "replaced", path: target, detail: `Overwrote statusLine in ${target} (--force).` };
|
|
913
|
+
}
|
|
914
|
+
function skipped(path) {
|
|
915
|
+
return {
|
|
916
|
+
kind: "skipped",
|
|
917
|
+
path,
|
|
918
|
+
detail: "--skip-statusline set; no changes to statusline target."
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
function writeExec(target, body) {
|
|
922
|
+
node_fs.mkdirSync(node_path.dirname(target), { recursive: true });
|
|
923
|
+
const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
|
|
924
|
+
node_fs.writeFileSync(tmp, body, { mode: 493 });
|
|
925
|
+
node_fs.renameSync(tmp, target);
|
|
926
|
+
node_fs.chmodSync(target, 493);
|
|
927
|
+
}
|
|
928
|
+
function readSettings(target) {
|
|
929
|
+
if (!node_fs.existsSync(target)) return {};
|
|
930
|
+
const raw = node_fs.readFileSync(target, "utf8");
|
|
931
|
+
if (raw.trim() === "") return {};
|
|
932
|
+
try {
|
|
933
|
+
const parsed = JSON.parse(raw);
|
|
934
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
935
|
+
return parsed;
|
|
936
|
+
}
|
|
937
|
+
return {};
|
|
938
|
+
} catch {
|
|
939
|
+
return {};
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function writeSettings(target, next) {
|
|
943
|
+
node_fs.mkdirSync(node_path.dirname(target), { recursive: true });
|
|
944
|
+
const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
|
|
945
|
+
node_fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}
|
|
946
|
+
`, { mode: 420 });
|
|
947
|
+
node_fs.renameSync(tmp, target);
|
|
948
|
+
}
|
|
949
|
+
function matchesDesired(current) {
|
|
950
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return false;
|
|
951
|
+
const c = current;
|
|
952
|
+
return c.type === STATUSLINE_FIELD.type && c.command === STATUSLINE_FIELD.command;
|
|
953
|
+
}
|
|
805
954
|
const FAMILIES = [
|
|
806
955
|
"skills",
|
|
807
956
|
"rules",
|
|
@@ -962,6 +1111,16 @@ function runInit(argv) {
|
|
|
962
1111
|
args.allowRoot,
|
|
963
1112
|
say
|
|
964
1113
|
);
|
|
1114
|
+
const dryRunSettingsPreserved = !(args.force || upgradeApproved) && node_fs.existsSync(node_path.join(targetDir, ".claude", "settings.json"));
|
|
1115
|
+
maybeEmitStatuslinePlan({
|
|
1116
|
+
dryRun: true,
|
|
1117
|
+
force: args.force || upgradeApproved,
|
|
1118
|
+
skip: args.skipStatusline,
|
|
1119
|
+
allowRoot: args.allowRoot,
|
|
1120
|
+
targetDir,
|
|
1121
|
+
say,
|
|
1122
|
+
settingsPreserved: dryRunSettingsPreserved
|
|
1123
|
+
});
|
|
965
1124
|
if (args.json) {
|
|
966
1125
|
const report = buildInitReport({
|
|
967
1126
|
entries: outcome.result?.wouldCopyEntries ?? [],
|
|
@@ -981,7 +1140,7 @@ function runInit(argv) {
|
|
|
981
1140
|
}
|
|
982
1141
|
return outcome.code;
|
|
983
1142
|
}
|
|
984
|
-
return runReal$1(plans, args.force || upgradeApproved, args.verbose, targetDir, args.allowRoot, args.json, say);
|
|
1143
|
+
return runReal$1(plans, args.force || upgradeApproved, args.verbose, targetDir, args.allowRoot, args.json, args.skipStatusline, say);
|
|
985
1144
|
}
|
|
986
1145
|
function maybeEmitUpgradeAdvisory(targetDir, yes, json) {
|
|
987
1146
|
const prompt = json ? (t) => {
|
|
@@ -1204,7 +1363,7 @@ function runDryRun$1(plans, say) {
|
|
|
1204
1363
|
}
|
|
1205
1364
|
return 0;
|
|
1206
1365
|
}
|
|
1207
|
-
function runReal$1(plans, force, verbose, targetDir, allowRoot, json, say) {
|
|
1366
|
+
function runReal$1(plans, force, verbose, targetDir, allowRoot, json, skipStatusline, say) {
|
|
1208
1367
|
const results = [];
|
|
1209
1368
|
let anyRefused = false;
|
|
1210
1369
|
let anyError = false;
|
|
@@ -1307,6 +1466,20 @@ function runReal$1(plans, force, verbose, targetDir, allowRoot, json, say) {
|
|
|
1307
1466
|
`
|
|
1308
1467
|
);
|
|
1309
1468
|
}
|
|
1469
|
+
const settingsRefused = (walker.result?.refused ?? []).some((r) => {
|
|
1470
|
+
const p = typeof r === "string" ? r : r.path;
|
|
1471
|
+
if (typeof p !== "string") return false;
|
|
1472
|
+
return p === node_path.join(".claude", "settings.json") || p === ".claude/settings.json" || p.endsWith("/.claude/settings.json");
|
|
1473
|
+
});
|
|
1474
|
+
maybeEmitStatuslinePlan({
|
|
1475
|
+
dryRun: false,
|
|
1476
|
+
force,
|
|
1477
|
+
skip: skipStatusline,
|
|
1478
|
+
allowRoot,
|
|
1479
|
+
targetDir,
|
|
1480
|
+
say,
|
|
1481
|
+
settingsPreserved: settingsRefused
|
|
1482
|
+
});
|
|
1310
1483
|
if (json) {
|
|
1311
1484
|
renderJsonReport(report, (t) => {
|
|
1312
1485
|
process.stdout.write(t);
|
|
@@ -1440,6 +1613,67 @@ function readDeclaredCommandLeaves(settingsPath) {
|
|
|
1440
1613
|
return /* @__PURE__ */ new Set();
|
|
1441
1614
|
}
|
|
1442
1615
|
}
|
|
1616
|
+
function maybeEmitStatuslinePlan(opts) {
|
|
1617
|
+
const packageDir = node_path.dirname(node_path.dirname(resolveBundleRoot(void 0)));
|
|
1618
|
+
const dispatcherSource = node_path.join(
|
|
1619
|
+
packageDir,
|
|
1620
|
+
"dist",
|
|
1621
|
+
"lite",
|
|
1622
|
+
"presence",
|
|
1623
|
+
"cli",
|
|
1624
|
+
"bassclef-statusline.dispatcher.sh"
|
|
1625
|
+
);
|
|
1626
|
+
if (!node_fs.existsSync(dispatcherSource)) {
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
let home;
|
|
1630
|
+
try {
|
|
1631
|
+
home = resolveHome({ allowRoot: opts.allowRoot });
|
|
1632
|
+
} catch {
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
let report;
|
|
1636
|
+
try {
|
|
1637
|
+
report = installStatusline({
|
|
1638
|
+
home,
|
|
1639
|
+
projectDir: opts.targetDir,
|
|
1640
|
+
packageDir,
|
|
1641
|
+
force: opts.force,
|
|
1642
|
+
dryRun: opts.dryRun,
|
|
1643
|
+
skip: opts.skip,
|
|
1644
|
+
settingsPreserved: opts.settingsPreserved
|
|
1645
|
+
});
|
|
1646
|
+
} catch {
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
const dv = report.dispatcher.kind;
|
|
1650
|
+
const sv = report.settings.kind;
|
|
1651
|
+
if (dv === "skipped" && sv === "skipped") {
|
|
1652
|
+
opts.say(`bassclef init: statusline install skipped (--skip-statusline).
|
|
1653
|
+
`);
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
if (opts.dryRun) {
|
|
1657
|
+
opts.say(
|
|
1658
|
+
`bassclef init: would install statusline dispatcher at ${report.dispatcher.path} and set statusLine in ${report.settings.path}.
|
|
1659
|
+
`
|
|
1660
|
+
);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const parts = [];
|
|
1664
|
+
if (dv === "installed") parts.push(`wrote ${report.dispatcher.path}`);
|
|
1665
|
+
else if (dv === "replaced") parts.push(`replaced ${report.dispatcher.path} (--force)`);
|
|
1666
|
+
else if (dv === "preserved") parts.push(`preserved ${report.dispatcher.path} (pass --force to overwrite)`);
|
|
1667
|
+
else if (dv === "unchanged") parts.push(`${report.dispatcher.path} already matches bundle`);
|
|
1668
|
+
if (sv === "installed") parts.push(`set statusLine in ${report.settings.path}`);
|
|
1669
|
+
else if (sv === "replaced") parts.push(`replaced statusLine in ${report.settings.path} (--force)`);
|
|
1670
|
+
else if (sv === "preserved") parts.push(`preserved statusLine in ${report.settings.path} (pass --force to overwrite)`);
|
|
1671
|
+
else if (sv === "unchanged") parts.push(`statusLine in ${report.settings.path} already matches`);
|
|
1672
|
+
if (parts.length > 0) {
|
|
1673
|
+
opts.say(`bassclef init: statusline — ${parts.join("; ")}.
|
|
1674
|
+
`);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1443
1677
|
const DEFAULTS$1 = {
|
|
1444
1678
|
force: false,
|
|
1445
1679
|
replaceEdits: false,
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { version } from "./index.js";
|
|
3
|
-
import { realpathSync, statSync, constants, lstatSync, mkdirSync, accessSync, unlinkSync, openSync, writeSync, closeSync, readlinkSync, readFileSync, chmodSync, readdirSync, existsSync } from "node:fs";
|
|
3
|
+
import { realpathSync, statSync, constants, lstatSync, mkdirSync, accessSync, unlinkSync, openSync, writeSync, closeSync, readlinkSync, readFileSync, chmodSync, readdirSync, existsSync, writeFileSync, renameSync } from "node:fs";
|
|
4
4
|
import { isAbsolute, resolve, dirname, join, normalize, relative, basename as basename$1 } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
@@ -15,7 +15,8 @@ const DEFAULTS$2 = {
|
|
|
15
15
|
allowAnyDir: false,
|
|
16
16
|
dir: void 0,
|
|
17
17
|
yes: false,
|
|
18
|
-
json: false
|
|
18
|
+
json: false,
|
|
19
|
+
skipStatusline: false
|
|
19
20
|
};
|
|
20
21
|
let ArgvError$1 = class ArgvError extends Error {
|
|
21
22
|
name = "ArgvError";
|
|
@@ -60,6 +61,11 @@ function parseInitArgs(argv) {
|
|
|
60
61
|
i += 1;
|
|
61
62
|
continue;
|
|
62
63
|
}
|
|
64
|
+
if (token === "--skip-statusline") {
|
|
65
|
+
out.skipStatusline = true;
|
|
66
|
+
i += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
63
69
|
if (token === "--dir") {
|
|
64
70
|
const value = argv[i + 1];
|
|
65
71
|
if (value === void 0 || value.startsWith("--")) {
|
|
@@ -779,6 +785,149 @@ function dirExists(p) {
|
|
|
779
785
|
return false;
|
|
780
786
|
}
|
|
781
787
|
}
|
|
788
|
+
const STATUSLINE_FIELD = {
|
|
789
|
+
type: "command",
|
|
790
|
+
command: "bash ~/.claude/bassclef-statusline.sh"
|
|
791
|
+
};
|
|
792
|
+
const DISPATCHER_REL = ["dist", "lite", "presence", "cli", "bassclef-statusline.dispatcher.sh"];
|
|
793
|
+
const USER_DISPATCHER_REL = [".claude", "bassclef-statusline.sh"];
|
|
794
|
+
const PROJECT_SETTINGS_REL = [".claude", "settings.json"];
|
|
795
|
+
function installStatusline(opts) {
|
|
796
|
+
if (opts.skip) {
|
|
797
|
+
return {
|
|
798
|
+
dispatcher: skipped(join(opts.home, ...USER_DISPATCHER_REL)),
|
|
799
|
+
settings: skipped(join(opts.projectDir, ...PROJECT_SETTINGS_REL))
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
const dispatcherSource = join(opts.packageDir, ...DISPATCHER_REL);
|
|
803
|
+
if (!existsSync(dispatcherSource)) {
|
|
804
|
+
throw new Error(
|
|
805
|
+
`bassclef init: cannot install statusline — bundled dispatcher missing at ${dispatcherSource}. Reinstall @thebassclef/lite or file a bug.`
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
const sourceBody = readFileSync(dispatcherSource, "utf8");
|
|
809
|
+
const dispatcher = handleDispatcher(opts, dispatcherSource, sourceBody);
|
|
810
|
+
const settings = handleSettings(opts);
|
|
811
|
+
return { dispatcher, settings };
|
|
812
|
+
}
|
|
813
|
+
function handleDispatcher(opts, sourcePath, sourceBody) {
|
|
814
|
+
const target = join(opts.home, ...USER_DISPATCHER_REL);
|
|
815
|
+
if (opts.dryRun) {
|
|
816
|
+
return {
|
|
817
|
+
kind: "would-install",
|
|
818
|
+
path: target,
|
|
819
|
+
detail: `Dry-run: would copy ${sourcePath} → ${target} (0755).`
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
if (!existsSync(target)) {
|
|
823
|
+
writeExec(target, sourceBody);
|
|
824
|
+
return { kind: "installed", path: target, detail: `Wrote ${target} (0755) from bundled dispatcher.` };
|
|
825
|
+
}
|
|
826
|
+
const st = lstatSync(target);
|
|
827
|
+
if (st.isSymbolicLink()) {
|
|
828
|
+
if (!opts.force) {
|
|
829
|
+
return {
|
|
830
|
+
kind: "preserved",
|
|
831
|
+
path: target,
|
|
832
|
+
detail: `${target} is a symlink; refusing to follow. Pass --force to replace.`
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
unlinkSync(target);
|
|
836
|
+
writeExec(target, sourceBody);
|
|
837
|
+
return { kind: "replaced", path: target, detail: `Replaced symlink at ${target} with bundled dispatcher.` };
|
|
838
|
+
}
|
|
839
|
+
const existing = readFileSync(target, "utf8");
|
|
840
|
+
if (existing === sourceBody) {
|
|
841
|
+
return { kind: "unchanged", path: target, detail: `${target} already matches bundled dispatcher.` };
|
|
842
|
+
}
|
|
843
|
+
if (!opts.force) {
|
|
844
|
+
return {
|
|
845
|
+
kind: "preserved",
|
|
846
|
+
path: target,
|
|
847
|
+
detail: `${target} differs from bundled dispatcher. Pass --force to overwrite.`
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
writeExec(target, sourceBody);
|
|
851
|
+
return { kind: "replaced", path: target, detail: `Overwrote ${target} (--force) with bundled dispatcher.` };
|
|
852
|
+
}
|
|
853
|
+
function handleSettings(opts) {
|
|
854
|
+
const target = join(opts.projectDir, ...PROJECT_SETTINGS_REL);
|
|
855
|
+
if (opts.settingsPreserved) {
|
|
856
|
+
return {
|
|
857
|
+
kind: "preserved",
|
|
858
|
+
path: target,
|
|
859
|
+
detail: `${target} was preserved by init (--force not passed on existing file). Statusline field not merged. Re-run init with --force to overwrite.`
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
if (opts.dryRun) {
|
|
863
|
+
return {
|
|
864
|
+
kind: "would-install",
|
|
865
|
+
path: target,
|
|
866
|
+
detail: `Dry-run: would set statusLine in ${target}.`
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
const existing = readSettings(target);
|
|
870
|
+
const desired = STATUSLINE_FIELD;
|
|
871
|
+
const current = existing.statusLine;
|
|
872
|
+
if (current === void 0) {
|
|
873
|
+
const next2 = { ...existing, statusLine: desired };
|
|
874
|
+
writeSettings(target, next2);
|
|
875
|
+
return { kind: "installed", path: target, detail: `Wrote statusLine field to ${target}.` };
|
|
876
|
+
}
|
|
877
|
+
if (matchesDesired(current)) {
|
|
878
|
+
return { kind: "unchanged", path: target, detail: `${target} already carries the bassclef statusLine.` };
|
|
879
|
+
}
|
|
880
|
+
if (!opts.force) {
|
|
881
|
+
return {
|
|
882
|
+
kind: "preserved",
|
|
883
|
+
path: target,
|
|
884
|
+
detail: `${target} carries a different statusLine. Pass --force to overwrite.`
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
const next = { ...existing, statusLine: desired };
|
|
888
|
+
writeSettings(target, next);
|
|
889
|
+
return { kind: "replaced", path: target, detail: `Overwrote statusLine in ${target} (--force).` };
|
|
890
|
+
}
|
|
891
|
+
function skipped(path) {
|
|
892
|
+
return {
|
|
893
|
+
kind: "skipped",
|
|
894
|
+
path,
|
|
895
|
+
detail: "--skip-statusline set; no changes to statusline target."
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function writeExec(target, body) {
|
|
899
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
900
|
+
const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
|
|
901
|
+
writeFileSync(tmp, body, { mode: 493 });
|
|
902
|
+
renameSync(tmp, target);
|
|
903
|
+
chmodSync(target, 493);
|
|
904
|
+
}
|
|
905
|
+
function readSettings(target) {
|
|
906
|
+
if (!existsSync(target)) return {};
|
|
907
|
+
const raw = readFileSync(target, "utf8");
|
|
908
|
+
if (raw.trim() === "") return {};
|
|
909
|
+
try {
|
|
910
|
+
const parsed = JSON.parse(raw);
|
|
911
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
912
|
+
return parsed;
|
|
913
|
+
}
|
|
914
|
+
return {};
|
|
915
|
+
} catch {
|
|
916
|
+
return {};
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
function writeSettings(target, next) {
|
|
920
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
921
|
+
const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
|
|
922
|
+
writeFileSync(tmp, `${JSON.stringify(next, null, 2)}
|
|
923
|
+
`, { mode: 420 });
|
|
924
|
+
renameSync(tmp, target);
|
|
925
|
+
}
|
|
926
|
+
function matchesDesired(current) {
|
|
927
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return false;
|
|
928
|
+
const c = current;
|
|
929
|
+
return c.type === STATUSLINE_FIELD.type && c.command === STATUSLINE_FIELD.command;
|
|
930
|
+
}
|
|
782
931
|
const FAMILIES = [
|
|
783
932
|
"skills",
|
|
784
933
|
"rules",
|
|
@@ -939,6 +1088,16 @@ function runInit(argv) {
|
|
|
939
1088
|
args.allowRoot,
|
|
940
1089
|
say
|
|
941
1090
|
);
|
|
1091
|
+
const dryRunSettingsPreserved = !(args.force || upgradeApproved) && existsSync(join(targetDir, ".claude", "settings.json"));
|
|
1092
|
+
maybeEmitStatuslinePlan({
|
|
1093
|
+
dryRun: true,
|
|
1094
|
+
force: args.force || upgradeApproved,
|
|
1095
|
+
skip: args.skipStatusline,
|
|
1096
|
+
allowRoot: args.allowRoot,
|
|
1097
|
+
targetDir,
|
|
1098
|
+
say,
|
|
1099
|
+
settingsPreserved: dryRunSettingsPreserved
|
|
1100
|
+
});
|
|
942
1101
|
if (args.json) {
|
|
943
1102
|
const report = buildInitReport({
|
|
944
1103
|
entries: outcome.result?.wouldCopyEntries ?? [],
|
|
@@ -958,7 +1117,7 @@ function runInit(argv) {
|
|
|
958
1117
|
}
|
|
959
1118
|
return outcome.code;
|
|
960
1119
|
}
|
|
961
|
-
return runReal$1(plans, args.force || upgradeApproved, args.verbose, targetDir, args.allowRoot, args.json, say);
|
|
1120
|
+
return runReal$1(plans, args.force || upgradeApproved, args.verbose, targetDir, args.allowRoot, args.json, args.skipStatusline, say);
|
|
962
1121
|
}
|
|
963
1122
|
function maybeEmitUpgradeAdvisory(targetDir, yes, json) {
|
|
964
1123
|
const prompt = json ? (t) => {
|
|
@@ -1181,7 +1340,7 @@ function runDryRun$1(plans, say) {
|
|
|
1181
1340
|
}
|
|
1182
1341
|
return 0;
|
|
1183
1342
|
}
|
|
1184
|
-
function runReal$1(plans, force, verbose, targetDir, allowRoot, json, say) {
|
|
1343
|
+
function runReal$1(plans, force, verbose, targetDir, allowRoot, json, skipStatusline, say) {
|
|
1185
1344
|
const results = [];
|
|
1186
1345
|
let anyRefused = false;
|
|
1187
1346
|
let anyError = false;
|
|
@@ -1284,6 +1443,20 @@ function runReal$1(plans, force, verbose, targetDir, allowRoot, json, say) {
|
|
|
1284
1443
|
`
|
|
1285
1444
|
);
|
|
1286
1445
|
}
|
|
1446
|
+
const settingsRefused = (walker.result?.refused ?? []).some((r) => {
|
|
1447
|
+
const p = typeof r === "string" ? r : r.path;
|
|
1448
|
+
if (typeof p !== "string") return false;
|
|
1449
|
+
return p === join(".claude", "settings.json") || p === ".claude/settings.json" || p.endsWith("/.claude/settings.json");
|
|
1450
|
+
});
|
|
1451
|
+
maybeEmitStatuslinePlan({
|
|
1452
|
+
dryRun: false,
|
|
1453
|
+
force,
|
|
1454
|
+
skip: skipStatusline,
|
|
1455
|
+
allowRoot,
|
|
1456
|
+
targetDir,
|
|
1457
|
+
say,
|
|
1458
|
+
settingsPreserved: settingsRefused
|
|
1459
|
+
});
|
|
1287
1460
|
if (json) {
|
|
1288
1461
|
renderJsonReport(report, (t) => {
|
|
1289
1462
|
process.stdout.write(t);
|
|
@@ -1417,6 +1590,67 @@ function readDeclaredCommandLeaves(settingsPath) {
|
|
|
1417
1590
|
return /* @__PURE__ */ new Set();
|
|
1418
1591
|
}
|
|
1419
1592
|
}
|
|
1593
|
+
function maybeEmitStatuslinePlan(opts) {
|
|
1594
|
+
const packageDir = dirname(dirname(resolveBundleRoot(void 0)));
|
|
1595
|
+
const dispatcherSource = join(
|
|
1596
|
+
packageDir,
|
|
1597
|
+
"dist",
|
|
1598
|
+
"lite",
|
|
1599
|
+
"presence",
|
|
1600
|
+
"cli",
|
|
1601
|
+
"bassclef-statusline.dispatcher.sh"
|
|
1602
|
+
);
|
|
1603
|
+
if (!existsSync(dispatcherSource)) {
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
let home;
|
|
1607
|
+
try {
|
|
1608
|
+
home = resolveHome({ allowRoot: opts.allowRoot });
|
|
1609
|
+
} catch {
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
let report;
|
|
1613
|
+
try {
|
|
1614
|
+
report = installStatusline({
|
|
1615
|
+
home,
|
|
1616
|
+
projectDir: opts.targetDir,
|
|
1617
|
+
packageDir,
|
|
1618
|
+
force: opts.force,
|
|
1619
|
+
dryRun: opts.dryRun,
|
|
1620
|
+
skip: opts.skip,
|
|
1621
|
+
settingsPreserved: opts.settingsPreserved
|
|
1622
|
+
});
|
|
1623
|
+
} catch {
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
const dv = report.dispatcher.kind;
|
|
1627
|
+
const sv = report.settings.kind;
|
|
1628
|
+
if (dv === "skipped" && sv === "skipped") {
|
|
1629
|
+
opts.say(`bassclef init: statusline install skipped (--skip-statusline).
|
|
1630
|
+
`);
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
if (opts.dryRun) {
|
|
1634
|
+
opts.say(
|
|
1635
|
+
`bassclef init: would install statusline dispatcher at ${report.dispatcher.path} and set statusLine in ${report.settings.path}.
|
|
1636
|
+
`
|
|
1637
|
+
);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
const parts = [];
|
|
1641
|
+
if (dv === "installed") parts.push(`wrote ${report.dispatcher.path}`);
|
|
1642
|
+
else if (dv === "replaced") parts.push(`replaced ${report.dispatcher.path} (--force)`);
|
|
1643
|
+
else if (dv === "preserved") parts.push(`preserved ${report.dispatcher.path} (pass --force to overwrite)`);
|
|
1644
|
+
else if (dv === "unchanged") parts.push(`${report.dispatcher.path} already matches bundle`);
|
|
1645
|
+
if (sv === "installed") parts.push(`set statusLine in ${report.settings.path}`);
|
|
1646
|
+
else if (sv === "replaced") parts.push(`replaced statusLine in ${report.settings.path} (--force)`);
|
|
1647
|
+
else if (sv === "preserved") parts.push(`preserved statusLine in ${report.settings.path} (pass --force to overwrite)`);
|
|
1648
|
+
else if (sv === "unchanged") parts.push(`statusLine in ${report.settings.path} already matches`);
|
|
1649
|
+
if (parts.length > 0) {
|
|
1650
|
+
opts.say(`bassclef init: statusline — ${parts.join("; ")}.
|
|
1651
|
+
`);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1420
1654
|
const DEFAULTS$1 = {
|
|
1421
1655
|
force: false,
|
|
1422
1656
|
replaceEdits: false,
|
package/dist/index.cjs
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const version: "1.
|
|
1
|
+
export declare const version: "1.6.0";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -80,7 +80,14 @@ LITE_STAGED=0
|
|
|
80
80
|
while IFS= read -r file; do
|
|
81
81
|
[ -z "$file" ] && continue
|
|
82
82
|
|
|
83
|
-
# Quick path-class filter (avoids frontmatter reads for obviously non-lite files)
|
|
83
|
+
# Quick path-class filter (avoids frontmatter reads for obviously non-lite files).
|
|
84
|
+
# Covers all 11 manifest types per bassclef-upstream#1872 widen (2026-09-21):
|
|
85
|
+
# skills, rules, hooks, luminaries, agents, standards, ADRs,
|
|
86
|
+
# lib, scripts, templates, presence-templates, root-docs.
|
|
87
|
+
# Note: bash case-pattern `*` matches slashes, so `.claude/hooks/*.sh` already
|
|
88
|
+
# covers subdirs like `.claude/hooks/session-reflection.d/foo.sh`. Same for
|
|
89
|
+
# `standards/*.md`. No separate subdir patterns needed.
|
|
90
|
+
# Sister refactor to path-agnostic marker check tracked at #1873.
|
|
84
91
|
case "$file" in
|
|
85
92
|
.claude/skills/*/SKILL.md \
|
|
86
93
|
| .claude/rules/*.md \
|
|
@@ -88,8 +95,21 @@ while IFS= read -r file; do
|
|
|
88
95
|
| .claude/luminaries/*.md \
|
|
89
96
|
| .claude/agents/*.md \
|
|
90
97
|
| standards/*.md \
|
|
91
|
-
| architecture/decisions/ADR-*.md
|
|
92
|
-
|
|
98
|
+
| architecture/decisions/ADR-*.md \
|
|
99
|
+
| lib/*.sh \
|
|
100
|
+
| scripts/*.sh \
|
|
101
|
+
| templates/*.md \
|
|
102
|
+
| presence/install/*.sh \
|
|
103
|
+
| presence/install/*.md \
|
|
104
|
+
| presence/install/*.jsonc \
|
|
105
|
+
| AGENTS.md \
|
|
106
|
+
| README.md \
|
|
107
|
+
| CLAUDE-lite.md \
|
|
108
|
+
| CODE_OF_CONDUCT.md \
|
|
109
|
+
| CONTRIBUTING.md \
|
|
110
|
+
| SECURITY.md \
|
|
111
|
+
| .claude/bassclef-orientation.md)
|
|
112
|
+
# Path class matches; check frontmatter for tier: lite (markdown YAML)
|
|
93
113
|
if [ -f "$REPO_ROOT/$file" ] && head -20 "$REPO_ROOT/$file" 2>/dev/null | grep -q '^tier: lite'; then
|
|
94
114
|
LITE_STAGED=1
|
|
95
115
|
break
|
|
@@ -99,6 +119,11 @@ while IFS= read -r file; do
|
|
|
99
119
|
LITE_STAGED=1
|
|
100
120
|
break
|
|
101
121
|
fi
|
|
122
|
+
# JSON root-key shape for .jsonc / .json files: `"tier": "lite"` — #1872 addition
|
|
123
|
+
if [ -f "$REPO_ROOT/$file" ] && head -3 "$REPO_ROOT/$file" 2>/dev/null | grep -q '"tier": "lite"'; then
|
|
124
|
+
LITE_STAGED=1
|
|
125
|
+
break
|
|
126
|
+
fi
|
|
102
127
|
;;
|
|
103
128
|
esac
|
|
104
129
|
done <<< "$STAGED"
|
|
@@ -228,5 +228,9 @@
|
|
|
228
228
|
"$HOME/src/sunj-labs/bassclef/.claude/luminaries",
|
|
229
229
|
"$HOME/src/sunj-labs/bassclef/.claude/agents"
|
|
230
230
|
],
|
|
231
|
-
"$schema": "https://bassclef.sunj-labs/state-spine/v0/schemas/bassclef-wiring-manifest.schema.json"
|
|
231
|
+
"$schema": "https://bassclef.sunj-labs/state-spine/v0/schemas/bassclef-wiring-manifest.schema.json",
|
|
232
|
+
"statusLine": {
|
|
233
|
+
"type": "command",
|
|
234
|
+
"command": "bash ~/.claude/bassclef-statusline.sh"
|
|
235
|
+
}
|
|
232
236
|
}
|
|
@@ -444,6 +444,20 @@ _classify_via_patterns_json() {
|
|
|
444
444
|
fi
|
|
445
445
|
fi
|
|
446
446
|
;;
|
|
447
|
+
basename_in_file)
|
|
448
|
+
# bassclef-upstream#1867 — match when hook basename appears as
|
|
449
|
+
# a whole line in the file at $arg. Missing file falls through
|
|
450
|
+
# (fail-safe default per @luminary michael-nygard). Comments
|
|
451
|
+
# (lines starting with #) and blank lines never equal a valid
|
|
452
|
+
# .sh basename, so -qxF is safe without pre-filtering.
|
|
453
|
+
local list_file="$project_root/$arg"
|
|
454
|
+
if [ -f "$list_file" ]; then
|
|
455
|
+
if grep -qxF "$hook_name" "$list_file" 2>/dev/null; then
|
|
456
|
+
echo "$name"
|
|
457
|
+
return 0
|
|
458
|
+
fi
|
|
459
|
+
fi
|
|
460
|
+
;;
|
|
447
461
|
default)
|
|
448
462
|
echo "$name"
|
|
449
463
|
return 0
|
|
@@ -455,6 +469,29 @@ _classify_via_patterns_json() {
|
|
|
455
469
|
return 1
|
|
456
470
|
}
|
|
457
471
|
|
|
472
|
+
# === _install_pattern_classification (private helper for Layer 2) ===
|
|
473
|
+
# Reads classification_when_unwired for a given pattern name from the JSON.
|
|
474
|
+
# Returns 0 + emits label on success; returns 1 if pattern missing or JSON
|
|
475
|
+
# not available. Enables Parnas info-hiding — each label lives as a
|
|
476
|
+
# grep-discoverable string in standards/hook-invocation-patterns.json
|
|
477
|
+
# instead of being algorithmically derived at runtime.
|
|
478
|
+
# Per bassclef-upstream#1880 — closes the doc-code drift class.
|
|
479
|
+
_install_pattern_classification() {
|
|
480
|
+
local candidate="${1:-}"
|
|
481
|
+
[ -z "$candidate" ] && return 1
|
|
482
|
+
local project_root="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
483
|
+
local patterns_json="$project_root/standards/hook-invocation-patterns.json"
|
|
484
|
+
[ -f "$patterns_json" ] || return 1
|
|
485
|
+
command -v jq >/dev/null 2>&1 || return 1
|
|
486
|
+
local label
|
|
487
|
+
label=$(jq -r --arg n "$candidate" '.patterns[] | select(.name == $n) | .classification_when_unwired' "$patterns_json" 2>/dev/null)
|
|
488
|
+
if [ -n "$label" ] && [ "$label" != "null" ]; then
|
|
489
|
+
echo "$label"
|
|
490
|
+
return 0
|
|
491
|
+
fi
|
|
492
|
+
return 1
|
|
493
|
+
}
|
|
494
|
+
|
|
458
495
|
# === _install_pattern_is_valid (private helper for Layer 2) ===
|
|
459
496
|
# Returns 0 if pattern is in the JSON pattern list OR in the hardcoded
|
|
460
497
|
# fallback list (ci|launchd|lib|release|standard). Returns 1 otherwise.
|
|
@@ -542,10 +579,17 @@ classify_finding() {
|
|
|
542
579
|
# Hook lives on disk but settings.json wiring absent.
|
|
543
580
|
# If the hook is meant to live elsewhere (CI / launchd / sourced lib),
|
|
544
581
|
# this is by design, not a failure.
|
|
545
|
-
# Layer 2
|
|
546
|
-
#
|
|
547
|
-
#
|
|
548
|
-
#
|
|
582
|
+
# Layer 2 + bassclef-upstream#1880 — read classification_when_unwired
|
|
583
|
+
# from JSON so each label lives as a grep-discoverable string in
|
|
584
|
+
# standards/hook-invocation-patterns.json. Falls back to algorithmic
|
|
585
|
+
# derivation if JSON missing (defensive per @luminary michael-nygard).
|
|
586
|
+
local label
|
|
587
|
+
label=$(_install_pattern_classification "$install_pattern" 2>/dev/null)
|
|
588
|
+
if [ -n "$label" ]; then
|
|
589
|
+
echo "$label"
|
|
590
|
+
return 0
|
|
591
|
+
fi
|
|
592
|
+
# Fallback — hardcoded/algorithmic derivation when JSON unavailable
|
|
549
593
|
if [ "$install_pattern" = "standard" ]; then
|
|
550
594
|
echo "DEAD-LETTER"
|
|
551
595
|
return 0
|
|
@@ -58,14 +58,41 @@ _sgw_init_cache() {
|
|
|
58
58
|
# Extract every .sh literal ref from a shell file — the walker's resolve
|
|
59
59
|
# step filters to only paths that map to real files, so false-positive
|
|
60
60
|
# extractions are cheap.
|
|
61
|
+
#
|
|
62
|
+
# Post-filter (#1889): reject refs containing regex metacharacters —
|
|
63
|
+
# [^, \., .*, .+ — which appear when hooks grep for `.sh` patterns in
|
|
64
|
+
# other files. The suffix-fallback in _sgw_resolve_ref_in_repo would
|
|
65
|
+
# otherwise map these to phantom paths (e.g., `/[^/]+\.sh` resolves
|
|
66
|
+
# to a real hook via suffix extraction).
|
|
67
|
+
#
|
|
68
|
+
# NOT tightened to invocation-only extraction (`source X`/`bash X`)
|
|
69
|
+
# because many hooks assign paths to vars first, then invoke via the
|
|
70
|
+
# var later — `X_PATH="path/to/x.sh"; source "$X_PATH"`. The extractor
|
|
71
|
+
# would miss X_PATH's target line and the resolver could not follow.
|
|
72
|
+
# See docs/risk-ledgers/2026-09-21-source-graph-walker-tighten.md
|
|
73
|
+
# for the design decision to keep raw extraction + post-filter.
|
|
74
|
+
#
|
|
75
|
+
# Env-var escape: BASSCLEF_SGW_REGEX_FILTER=0 restores prior extractor
|
|
76
|
+
# behavior (no post-filter). For rescue when a real path contains a
|
|
77
|
+
# regex metacharacter (rare — paths never contain [^, \., .*, .+ in
|
|
78
|
+
# bassclef substrate).
|
|
61
79
|
_sgw_extract_all_refs() {
|
|
62
80
|
local file="$1"
|
|
81
|
+
local raw
|
|
63
82
|
|
|
64
83
|
# 1. Every .sh literal in the file (quoted or path-shaped), from non-comment lines
|
|
65
|
-
grep -vE '^[[:space:]]*#' "$file" 2>/dev/null | \
|
|
84
|
+
raw=$(grep -vE '^[[:space:]]*#' "$file" 2>/dev/null | \
|
|
66
85
|
grep -oE '"[^"]+\.sh"|'"'"'[^'"'"']+\.sh'"'"'|\$\{[^}]+\}/[^"'"'"'[:space:]]+\.sh|[$][A-Za-z_][A-Za-z0-9_]*/[^"'"'"'[:space:]]+\.sh|(\./|\.\./|/)[^"'"'"'[:space:]$]+\.sh' 2>/dev/null | \
|
|
67
86
|
tr -d '"'"'"'"' | \
|
|
68
|
-
sort -u
|
|
87
|
+
sort -u)
|
|
88
|
+
|
|
89
|
+
# Post-filter (#1889) — drop refs with regex metacharacters.
|
|
90
|
+
# Escape via BASSCLEF_SGW_REGEX_FILTER=0.
|
|
91
|
+
if [ "${BASSCLEF_SGW_REGEX_FILTER:-1}" = "1" ] && [ -n "$raw" ]; then
|
|
92
|
+
echo "$raw" | grep -vE '\[\^|\\\.|\.\*|\.\+' 2>/dev/null || true
|
|
93
|
+
else
|
|
94
|
+
echo "$raw"
|
|
95
|
+
fi
|
|
69
96
|
|
|
70
97
|
# 2. Glob-source patterns — for loops iterating dir/*.sh
|
|
71
98
|
grep -oE 'for[[:space:]]+[[:alnum:]_]+[[:space:]]+in[[:space:]]+[^;$]+/\*\.sh' "$file" 2>/dev/null | \
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: standard
|
|
3
|
+
# BASSCLEF_SYNC_VERSION=thin-pointer-statusline-2026-06-22
|
|
4
|
+
# ^^ DO NOT REMOVE — used by Surface 4 self-heal (migrate-adopter-references.sh)
|
|
5
|
+
# + bassclef-side drift check. Format: thin-pointer-statusline-YYYY-MM-DD.
|
|
6
|
+
#
|
|
7
|
+
# Thin-pointer bassclef statusline dispatcher.
|
|
8
|
+
#
|
|
9
|
+
# Source of truth for adopters' user-level statusline. Adopters install
|
|
10
|
+
# THIS file at ~/.claude/bassclef-statusline.sh; it finds bassclef's
|
|
11
|
+
# rich impl via sibling fast-path and exec's it on every tick. Substrate
|
|
12
|
+
# updates to the rich impl reach existing adopters because the pointer
|
|
13
|
+
# runs the live file, not a frozen copy.
|
|
14
|
+
#
|
|
15
|
+
# Three paths, tried in order (per #1860):
|
|
16
|
+
#
|
|
17
|
+
# 1. Sibling fast-path: $HOME/src/sunj-labs/bassclef OR /bassclef-upstream
|
|
18
|
+
# — if presence/cli/bassclef-statusline.sh exists and is executable,
|
|
19
|
+
# exec it with stdin piped through.
|
|
20
|
+
#
|
|
21
|
+
# 2. Script-relative: same directory as this dispatcher. Serves adopters
|
|
22
|
+
# who install only @thebassclef/lite via npm — the tarball ships both
|
|
23
|
+
# scripts side by side under node_modules/@thebassclef/lite/dist/lite/
|
|
24
|
+
# presence/cli/. Uses `cd $(dirname); pwd` for macOS + Linux portability
|
|
25
|
+
# (BSD readlink lacks -f per R15 rfc finding).
|
|
26
|
+
#
|
|
27
|
+
# 3. Fallback: minimal render so SessionStart doesn't crash. Adopter
|
|
28
|
+
# sees "bassclef · ?" instead of an empty statusline.
|
|
29
|
+
#
|
|
30
|
+
# Always exits 0 — same SessionStart-safe discipline as bassclef-sync
|
|
31
|
+
# dispatcher (per ADR-032).
|
|
32
|
+
#
|
|
33
|
+
# Versioning:
|
|
34
|
+
# thin-pointer-statusline-2026-06-22 — initial. Bump format when the
|
|
35
|
+
# dispatcher's contract changes (rare; ideally never).
|
|
36
|
+
#
|
|
37
|
+
# Primary lens: Linus — substrate carries the recovery cost. Adopters
|
|
38
|
+
# don't re-install when the rich impl evolves; the dispatcher's
|
|
39
|
+
# stability is what shields them.
|
|
40
|
+
#
|
|
41
|
+
# Anchor: Hyrum — the version string IS the observable surface adopters
|
|
42
|
+
# can grep to detect drift. Surface 4 self-heal in
|
|
43
|
+
# migrate-adopter-references.sh uses it.
|
|
44
|
+
|
|
45
|
+
set -u
|
|
46
|
+
|
|
47
|
+
__bassclef_statusline_version() {
|
|
48
|
+
echo "thin-pointer-statusline-2026-06-22"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Read all of stdin so we can pipe it through to the rich impl
|
|
52
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
53
|
+
|
|
54
|
+
# Resolve this dispatcher's directory for the script-relative fallback.
|
|
55
|
+
# `cd $(dirname); pwd` is portable across macOS (BSD readlink lacks -f) and
|
|
56
|
+
# Linux (GNU readlink -f works but we don't need it).
|
|
57
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
58
|
+
|
|
59
|
+
# Find rich impl via sibling fast-path, then script-relative fallback
|
|
60
|
+
RICH_IMPL=""
|
|
61
|
+
for CANDIDATE in \
|
|
62
|
+
"$HOME/src/sunj-labs/bassclef/presence/cli/bassclef-statusline.sh" \
|
|
63
|
+
"$HOME/src/sunj-labs/bassclef-upstream/presence/cli/bassclef-statusline.sh" \
|
|
64
|
+
"$SCRIPT_DIR/bassclef-statusline.sh"; do
|
|
65
|
+
if [ -x "$CANDIDATE" ]; then
|
|
66
|
+
RICH_IMPL="$CANDIDATE"
|
|
67
|
+
break
|
|
68
|
+
fi
|
|
69
|
+
done
|
|
70
|
+
|
|
71
|
+
if [ -n "$RICH_IMPL" ]; then
|
|
72
|
+
echo "$INPUT" | bash "$RICH_IMPL"
|
|
73
|
+
exit 0
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Fallback — minimal render when no sibling is available
|
|
77
|
+
echo "bassclef · ?"
|
|
78
|
+
exit 0
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: standard
|
|
3
|
+
# bassclef — Claude Code status line.
|
|
4
|
+
# Claude Code pipes session JSON on stdin every tick; whatever we print to stdout
|
|
5
|
+
# becomes the status line (ANSI colors allowed). Requires accepting the workspace
|
|
6
|
+
# trust prompt, same as any shell-executing setting. Keep it ONE short line.
|
|
7
|
+
#
|
|
8
|
+
# Register it in ~/.claude/settings.json — see bassclef-settings.snippet.json.
|
|
9
|
+
# Test: echo '{"model":{"display_name":"Opus"},"workspace":{"current_dir":"/x/bassline"},"context_window":{"used_percentage":23}}' | bash bassclef-statusline.sh
|
|
10
|
+
|
|
11
|
+
# Script-relative dir for tarball-layout fallback (#1860). Portable across
|
|
12
|
+
# macOS + Linux — `cd $(dirname); pwd` avoids BSD readlink -f gap. Python
|
|
13
|
+
# picks this up via os.environ; we cannot use __file__ under python3 -c.
|
|
14
|
+
BASSCLEF_STATUSLINE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
export BASSCLEF_STATUSLINE_SCRIPT_DIR
|
|
16
|
+
|
|
17
|
+
python3 -c '
|
|
18
|
+
import sys, json, os
|
|
19
|
+
try:
|
|
20
|
+
d = json.load(sys.stdin)
|
|
21
|
+
except Exception:
|
|
22
|
+
d = {}
|
|
23
|
+
model = d.get("model", {}).get("display_name", "?")
|
|
24
|
+
cwd = d.get("workspace", {}).get("current_dir") or d.get("cwd", "")
|
|
25
|
+
base = os.path.basename(cwd.rstrip("/")) or "~"
|
|
26
|
+
pct = d.get("context_window", {}).get("used_percentage")
|
|
27
|
+
|
|
28
|
+
# Bassclef version segment (bet 2026-07-09d WU-1 / sunj-labs/bassclef-upstream#686).
|
|
29
|
+
# Reads bassclef-version.json. Fallback chain: env var, workspace root,
|
|
30
|
+
# peer sibling ../bassclef/. Prefers .version, falls back to .release_tag.
|
|
31
|
+
# Silent on any failure.
|
|
32
|
+
def read_bassclef_version(cwd):
|
|
33
|
+
def try_file(path):
|
|
34
|
+
if not path or not os.path.isfile(path):
|
|
35
|
+
return None
|
|
36
|
+
try:
|
|
37
|
+
with open(path) as f:
|
|
38
|
+
data = json.load(f)
|
|
39
|
+
except Exception:
|
|
40
|
+
return None
|
|
41
|
+
v = data.get("version") or data.get("release_tag")
|
|
42
|
+
return v if v else None
|
|
43
|
+
candidates = []
|
|
44
|
+
env_path = os.environ.get("BASSCLEF_VERSION_FILE")
|
|
45
|
+
if env_path:
|
|
46
|
+
candidates.append(env_path)
|
|
47
|
+
if cwd:
|
|
48
|
+
candidates.append(os.path.join(cwd, "bassclef-version.json"))
|
|
49
|
+
candidates.append(os.path.join(cwd, "..", "bassclef", "bassclef-version.json"))
|
|
50
|
+
# Script-relative tarball-layout fallback (#1860). Adopters installing
|
|
51
|
+
# only @thebassclef/lite via npm land under node_modules/@thebassclef/
|
|
52
|
+
# lite/dist/lite/presence/cli/, with bassclef-version.json two dirs up
|
|
53
|
+
# at dist/lite/bassclef-version.json. Runs LAST — cwd + peer sibling
|
|
54
|
+
# still win for adopters with a cloned repo (order per R16 rfc finding).
|
|
55
|
+
script_dir = os.environ.get("BASSCLEF_STATUSLINE_SCRIPT_DIR")
|
|
56
|
+
if script_dir:
|
|
57
|
+
candidates.append(os.path.join(script_dir, "..", "..", "bassclef-version.json"))
|
|
58
|
+
for p in candidates:
|
|
59
|
+
v = try_file(p)
|
|
60
|
+
if v:
|
|
61
|
+
return v
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
version = read_bassclef_version(cwd)
|
|
65
|
+
|
|
66
|
+
O = "\033[38;2;232;93;4m" # burnt orange
|
|
67
|
+
C = "\033[38;2;205;201;189m"# cream
|
|
68
|
+
D = "\033[2m" # dim
|
|
69
|
+
R = "\033[0m"
|
|
70
|
+
sep = " " + D + "\u00b7" + R + " " # dim middot
|
|
71
|
+
|
|
72
|
+
seg = [O + "\U0001D122 bassclef.dev" + R, C + model + R, D + base + R]
|
|
73
|
+
if version:
|
|
74
|
+
seg.append(D + version + R)
|
|
75
|
+
if pct is not None:
|
|
76
|
+
seg.append(D + str(pct) + "%" + R)
|
|
77
|
+
sys.stdout.write(sep.join(seg))
|
|
78
|
+
'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"tier": "lite",
|
|
3
|
-
"manifest_version": "1.9.
|
|
4
|
-
"generated_at": "2026-09-
|
|
3
|
+
"manifest_version": "1.9.6",
|
|
4
|
+
"generated_at": "2026-09-21T21:28:13Z",
|
|
5
5
|
"entries": [
|
|
6
6
|
{
|
|
7
7
|
"slug": "029-release-pipeline",
|
|
@@ -544,7 +544,7 @@
|
|
|
544
544
|
"type": "hook",
|
|
545
545
|
"path": ".claude/hooks/pre-commit-manifest-autoregen.sh",
|
|
546
546
|
"tier": "lite",
|
|
547
|
-
"content_hash": "sha256:
|
|
547
|
+
"content_hash": "sha256:b3641c5c2402fdedb9fb23b812548e94809dcc732e8d5e19af4fd5b488b023e0",
|
|
548
548
|
"description": "PreToolUse: Bash"
|
|
549
549
|
},
|
|
550
550
|
{
|
|
@@ -824,7 +824,7 @@
|
|
|
824
824
|
"type": "lib",
|
|
825
825
|
"path": "lib/mechanism-fidelity.sh",
|
|
826
826
|
"tier": "lite",
|
|
827
|
-
"content_hash": "sha256:
|
|
827
|
+
"content_hash": "sha256:8afeac2e42bd7fb281cda9be94395e272092857f8a8e431350b8c8673578bcf5",
|
|
828
828
|
"description": "lib/mechanism-fidelity.sh — shared scanner for the mechanism-fidelity"
|
|
829
829
|
},
|
|
830
830
|
{
|
|
@@ -880,7 +880,7 @@
|
|
|
880
880
|
"type": "lib",
|
|
881
881
|
"path": "lib/source-graph-walker.sh",
|
|
882
882
|
"tier": "lite",
|
|
883
|
-
"content_hash": "sha256:
|
|
883
|
+
"content_hash": "sha256:350a3356243a2e7943e9f02d9272db58488ee72abac515e56c7690d665b1a0fd",
|
|
884
884
|
"description": "lib/source-graph-walker.sh — walks source-graph closure from a seed hook"
|
|
885
885
|
},
|
|
886
886
|
{
|
|
@@ -3761,7 +3761,7 @@
|
|
|
3761
3761
|
"type": "standard",
|
|
3762
3762
|
"path": "standards/mechanism-fidelity.md",
|
|
3763
3763
|
"tier": "lite",
|
|
3764
|
-
"content_hash": "sha256:
|
|
3764
|
+
"content_hash": "sha256:e0b14abf4a0331c96c488d52c6e3067f827b6ef4d506c19c83616184c53c3642",
|
|
3765
3765
|
"description": "- ADR-035 — names the substrate-as-system foundational tenet that this standard's audit method verifies - architecture/audits/2026-06-24-closeout.md — bet 24c architect-review that missed this class; this standard's audit fills the lens-gap and…"
|
|
3766
3766
|
},
|
|
3767
3767
|
{
|
|
@@ -4025,7 +4025,7 @@
|
|
|
4025
4025
|
"type": "standard",
|
|
4026
4026
|
"path": "standards/substrate-config-schema.md",
|
|
4027
4027
|
"tier": "lite",
|
|
4028
|
-
"content_hash": "sha256:
|
|
4028
|
+
"content_hash": "sha256:dff6bec2527dfa1cb899fd907b4f802bd4ce7a1e4bb9621bdf3cdfcc05601804",
|
|
4029
4029
|
"description": "substrate.config.md is the single source of truth for external resource references in any bassclef-substrate repo."
|
|
4030
4030
|
},
|
|
4031
4031
|
{
|
|
@@ -67,6 +67,7 @@ The class fires when one or more of these hold:
|
|
|
67
67
|
| `NOT-WIRED-BY-DESIGN-CI` | Hook lives on disk and is referenced from `.github/workflows/*.yml`; settings.json is not the right home for it | `pr-body-scrub-check.sh`, `pr-body-loop-discipline-check.sh` |
|
|
68
68
|
| `NOT-WIRED-BY-DESIGN-LAUNCHD` | Hook is scheduled by macOS launchd (or cron); header comment names the scheduler; settings.json is not the right home | `auto-save-idle.sh` |
|
|
69
69
|
| `NOT-WIRED-BY-DESIGN-LIB` | File is sourced by other hooks (library role); header comment says "sourced by"; settings.json is not the right home | `trace-helper.sh` |
|
|
70
|
+
| `NOT-WIRED-BY-DESIGN-EXCLUSIONS` | Hook basename is listed in `presence/install/template-hook-exclusions.txt`; wired in bassclef-upstream settings.json but intentionally absent from the adopter-shipped template (bassclef-team-only or upstream-only substrate) | `lite-skill-key-gate-check.sh`, `user-global-hook-ref-guard-check.sh` |
|
|
70
71
|
| `CLEAN` | All chain steps pass | most existing rules (audit will surface count) |
|
|
71
72
|
|
|
72
73
|
## The 5-step verification chain
|
|
@@ -83,7 +83,7 @@ uncommented defaults shown.
|
|
|
83
83
|
# === Artifacts ===
|
|
84
84
|
journal_doc_id: [per-repo draft Google Doc — pushes from /journal + session-end]
|
|
85
85
|
changelog_doc_id: [per-repo weekly changelog Google Doc — pushes from /release-notes]
|
|
86
|
-
brand_corpus_doc_id: [cross-repo brand team corpus — pushes from
|
|
86
|
+
brand_corpus_doc_id: [cross-repo brand team corpus — pushes from an operator export skill]
|
|
87
87
|
|
|
88
88
|
# === Environments ===
|
|
89
89
|
deploy_targets:
|
|
@@ -213,7 +213,7 @@ reference the raw ID directly.
|
|
|
213
213
|
|
|
214
214
|
### Anti-pattern 2: hardcoded IDs in skills
|
|
215
215
|
|
|
216
|
-
**Before** (in
|
|
216
|
+
**Before** (in a skill body):
|
|
217
217
|
```bash
|
|
218
218
|
npx tsx scripts/push-to-gdoc.ts \
|
|
219
219
|
--file docs/journal-corpus/corpus.md \
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thebassclef/lite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Bassclef CLI — install and upgrade bassclef in your project with two commands.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bassclef",
|
|
@@ -65,7 +65,8 @@
|
|
|
65
65
|
"typecheck": "tsc --noEmit",
|
|
66
66
|
"bump": "node scripts/bump-version.mjs",
|
|
67
67
|
"harness:npm-install": "vitest run --config vitest.harness.config.ts",
|
|
68
|
-
"prepublishOnly": "node scripts/prepublish-bundle-substrate.mjs"
|
|
68
|
+
"prepublishOnly": "node scripts/prepublish-bundle-substrate.mjs",
|
|
69
|
+
"install-hooks": "node scripts/install-git-hooks.mjs"
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|
|
71
72
|
"@types/node": "^20.14.0",
|