@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 +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/bassclef-orientation.md +27 -0
- package/dist/lite/.claude/hooks/session-reflection.d/70-recent-strategic-artifacts.sh +4 -2
- package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +4 -2
- package/dist/lite/.claude/hooks/session-reflection.d/80-workflow-staleness.sh +3 -1
- package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +21 -0
- package/dist/lite/.claude/hooks/session-reflection.d/95-settings-hook-verify.sh +14 -11
- package/dist/lite/.claude/hooks/session-reflection.sh +23 -14
- package/dist/lite/.claude/rules/longrun-prep-plan-doc-compression.md +25 -6
- package/dist/lite/.claude/settings.json +5 -1
- package/dist/lite/.claude/skills/longrun/SKILL.md +23 -6
- package/dist/lite/bassclef-version.json +7 -0
- package/dist/lite/gitignore +8 -0
- package/dist/lite/lib/mechanism-fidelity.sh +34 -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-schema-changes.md +41 -0
- package/dist/lite/standards/lite-manifest.json +22 -14
- package/dist/lite/standards/session-start-banner-discipline.md +99 -0
- 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.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
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const version: "1.
|
|
1
|
+
export declare const version: "1.7.0";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -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 "
|
|
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 "
|
|
112
|
+
printf "ADVISORY: %b\n" "$MSG" >&2
|
|
111
113
|
fi
|
|
112
114
|
fi
|
|
113
115
|
|