@thebassclef/lite 1.5.1 → 1.7.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.5.1<!-- version-end -->
38
+ <!-- version-start -->1.7.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.5.1";
3
+ const version = "1.7.0";
4
4
  exports.version = version;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const version: "1.5.1";
1
+ export declare const version: "1.7.0";
2
2
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- const version = "1.5.1";
1
+ const version = "1.7.0";
2
2
  export {
3
3
  version
4
4
  };
@@ -66,6 +66,33 @@ JTBD → HTA → Entity Model → State Diagrams → Sequence Diagrams → /deco
66
66
  - **Don't conflate "propose" with "execute".** When operator says "propose," "options," "path forward" — wait for explicit go signal.
67
67
  - **Don't use bassclef-internal jargon** in operator-facing prose. Plain English at grade-10 reading level.
68
68
  - **Don't push to main without PR review** even on solo workflows.
69
+ - **Don't run `git stash -u` on repos with gitignored substrate.** The `-u` flag moves untracked files to the stash. When your adopter repo gitignores `.claude/hooks/`, `.claude/skills/`, `.claude/rules/`, `.claude/agents/`, or `.claude/luminaries/`, `git stash -u` grabs the substrate files and Stop hooks fail with "No such file or directory" on every tool call. INSTEAD: use plain `git stash` unless you have vetted every untracked file first.
70
+
71
+ ## Substrate git posture
72
+
73
+ Bassclef ships adopter `.gitignore` (per `presence/dist-templates/.gitignore`) with substrate tracked-by-default. Substrate paths under `.claude/hooks/`, `.claude/skills/`, `.claude/rules/`, `.claude/agents/`, `.claude/luminaries/` are NOT gitignored. Adopters commit substrate so `git stash -u` and similar verbs cannot grab it.
74
+
75
+ **Opt-out recipe** — if you prefer substrate gitignored in your adopter repo:
76
+
77
+ 1. Add these lines to your `.gitignore`:
78
+
79
+ ```
80
+ .claude/hooks/
81
+ .claude/skills/
82
+ .claude/rules/
83
+ .claude/agents/
84
+ .claude/luminaries/
85
+ ```
86
+
87
+ 2. Untrack the files git already indexed:
88
+
89
+ ```
90
+ git rm --cached -r .claude/hooks .claude/skills .claude/rules .claude/agents .claude/luminaries
91
+ ```
92
+
93
+ 3. Commit the change.
94
+
95
+ **Caveat.** `bassclef-sync` may re-add substrate files on the next session-start. If that happens, re-run the untrack step. Long-term cure lives with the sync script.
69
96
 
70
97
  ## Luminaries at design boundaries
71
98
 
@@ -86,12 +86,14 @@ MSG="${MSG} explicit operator statement (\"skip recent-artifacts this session\"
86
86
  MSG="${MSG} Override: SKIP_RECENT_ARTIFACTS=1 <command> (logged)."
87
87
 
88
88
  # Emit via inherited blocked_banner (defined in session-reflection.sh)
89
+ # Per standards/session-start-banner-discipline.md — this check is ADVISORY:
90
+ # recent strategic artifacts are context worth reading; session may proceed.
89
91
  if type blocked_banner >/dev/null 2>&1; then
90
- blocked_banner "$MSG"
92
+ blocked_banner "$MSG" advisory
91
93
  else
92
94
  # Defensive fallback if module is invoked outside the coordinator
93
95
  echo ""
94
- echo "🛑🛑🛑 BLOCKED 🛑🛑🛑"
96
+ echo "⚠ ADVISORY"
95
97
  echo "$MSG"
96
98
  echo ""
97
99
  fi
@@ -104,10 +104,12 @@ done < <(jq -r '.critical_hooks[] | [.name, .max_age_seconds, .rationale] | @tsv
104
104
  if [ -n "$STALE_ENTRIES" ]; then
105
105
  NL=$'\n'
106
106
  MSG="hook liveness — stale hook(s) detected:$(echo -e "$STALE_ENTRIES")${NL}${NL}Cadence per standards/hook-cadence.json. Discipline per ADR-048 + .claude/rules/substrate-as-system.md.${NL}${NL}Resolve — run each named hook manually (check .claude/settings.json for its trigger event) OR file a substrate-defect ticket if the hook cannot be fired."
107
+ # Per standards/session-start-banner-discipline.md — stale hook is ADVISORY:
108
+ # session may proceed; operator informed to check hook trigger.
107
109
  if declare -f blocked_banner >/dev/null 2>&1; then
108
- blocked_banner "$MSG"
110
+ blocked_banner "$MSG" advisory
109
111
  else
110
- printf "BLOCKED: %b\n" "$MSG" >&2
112
+ printf "ADVISORY: %b\n" "$MSG" >&2
111
113
  fi
112
114
  fi
113
115