@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 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.4.1<!-- version-end -->
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
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const version = "1.4.1";
3
+ const version = "1.6.0";
4
4
  exports.version = version;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const version: "1.4.1";
1
+ export declare const version: "1.6.0";
2
2
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- const version = "1.4.1";
1
+ const version = "1.6.0";
2
2
  export {
3
3
  version
4
4
  };
@@ -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
- # Path class matches; check frontmatter for tier: lite
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
  }
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": "v1.1.0",
3
+ "marker_tag": "release-2026-09-21-0e54fa7c",
4
+ "release_date": "2026-09-21T22:20:31Z",
5
+ "release_sha": "0e54fa7c",
6
+ "release_tag": "release-2026-09-21-0e54fa7c"
7
+ }
@@ -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 (bassclef-upstream#1163)algorithmic label derivation:
546
- # "standard" DEAD-LETTER; anything else NOT-WIRED-BY-DESIGN-<UPPER>.
547
- # New patterns land in standards/hook-invocation-patterns.json; their
548
- # classification label flows through this branch without cascade edits.
582
+ # Layer 2 + bassclef-upstream#1880read 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.1",
4
- "generated_at": "2026-09-21T00:38:33Z",
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:34dc76e61307d79da11bb38460823a8945a144f322ea2cf74b0cbfac75118ec2",
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:7ccd6751d3ff3c31b142aac3c1739cd1cb1863f5e13db29ce1eb8206def1b7ca",
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:abae8c156ac538321b90326946eade3b70068ee1d4bd8ad605fdb374849400ba",
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:b212287354832a3613bf9bcdae7da6071df64d8c08f227e8d4233666ccea4d55",
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:94931a2f1f0e08f4381ddbc464ecd1578170f832a41d263a42f67ac0dfafee48",
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 /journal-export]
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 `.claude/skills/journal-export/SKILL.md`):
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.4.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",