@thebassclef/lite 1.9.7 → 1.9.9
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 +6 -2
- package/dist/cli.cjs +89 -7
- package/dist/cli.js +89 -7
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/hooks/git-stash-u-guard.sh +4 -1
- package/dist/lite/.claude/hooks/post-skill-friction-check.sh +6 -1
- package/dist/lite/.claude/hooks/pre-commit-gate.sh +18 -6
- package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +10 -6
- package/dist/lite/.claude/hooks/pre-commit-manifest-autoregen.sh +4 -2
- package/dist/lite/.claude/hooks/save-state.sh +24 -0
- package/dist/lite/.claude/hooks/trace-helper.sh +29 -0
- package/dist/lite/.claude/rules/lite-manifest-schema-change-discipline.md +30 -0
- package/dist/lite/.claude/settings.json +1 -1
- package/dist/lite/.claude/skills/build/SKILL.md +19 -7
- package/dist/lite/.claude/skills/onboard-repo/SKILL.md +41 -0
- package/dist/lite/.claude/skills/release-notes/SKILL.md +24 -0
- package/dist/lite/bassclef-version.json +5 -5
- package/dist/lite/gitignore +8 -0
- package/dist/lite/lib/deferred-actions.sh +4 -4
- package/dist/lite/lib/fixture-builder.sh +9 -9
- package/dist/lite/lib/identifier-leak-check.sh +85 -0
- package/dist/lite/lib/preview-state.sh +20 -20
- package/dist/lite/lib/skip-env-inline-check.sh +61 -0
- package/dist/lite/lib/state.sh +31 -28
- package/dist/lite/lib/tier-check.sh +9 -9
- package/dist/lite/lib/tier-dependency-audit.sh +23 -23
- package/dist/lite/presence/install/schedule-auto-save.taskscheduler.md +1 -1
- package/dist/lite/standards/lite-manifest-schema-changes.md +38 -0
- package/dist/lite/standards/lite-manifest.json +38 -38
- package/dist/lite/templates/spec-template.md +10 -6
- package/package.json +1 -1
- package/dist/lite/.claude/hooks/longrun-prep-compounding-axis-check.sh +0 -492
- package/dist/lite/.claude/rules/compounding-axis-fresh-analysis.md +0 -173
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# @thebassclef/lite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Security posture.** @thebassclef/lite ships with zero install-time scripts, SLSA v1 build provenance, Apache-2.0 license, and zero runtime dependencies. The CLI writes to `~/.claude` and `$CLAUDE_PROJECT_DIR` at runtime, not at install. See [SECURITY.md](SECURITY.md) for disclosure.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Two commands.
|
|
4
8
|
|
|
5
9
|
```bash
|
|
6
10
|
npm install -g @thebassclef/lite
|
|
@@ -63,7 +67,7 @@ your browser.
|
|
|
63
67
|
|
|
64
68
|
## Current release
|
|
65
69
|
|
|
66
|
-
<!-- version-start -->1.9.
|
|
70
|
+
<!-- version-start -->1.9.9<!-- version-end -->
|
|
67
71
|
|
|
68
72
|
See [CHANGELOG.md](CHANGELOG.md) for the full changelog. Adopter-facing
|
|
69
73
|
release notes live at [docs.bassclef.dev/docs/whats-new/releases](https://docs.bassclef.dev/docs/whats-new/releases).
|
package/dist/cli.cjs
CHANGED
|
@@ -810,15 +810,19 @@ function dirExists(p) {
|
|
|
810
810
|
}
|
|
811
811
|
const STATUSLINE_FIELD = {
|
|
812
812
|
type: "command",
|
|
813
|
-
command: "bash ~/.claude/bassclef-statusline.sh"
|
|
813
|
+
command: "bash ~/.claude/bassclef-statusline-dispatcher.sh"
|
|
814
814
|
};
|
|
815
|
+
const LEGACY_STATUSLINE_COMMAND = "bash ~/.claude/bassclef-statusline.sh";
|
|
815
816
|
const DISPATCHER_REL = ["dist", "lite", "presence", "cli", "bassclef-statusline.dispatcher.sh"];
|
|
816
|
-
const
|
|
817
|
+
const RICH_IMPL_REL = ["dist", "lite", "presence", "cli", "bassclef-statusline.sh"];
|
|
818
|
+
const USER_DISPATCHER_REL = [".claude", "bassclef-statusline-dispatcher.sh"];
|
|
819
|
+
const USER_RICH_IMPL_REL = [".claude", "bassclef-statusline.sh"];
|
|
817
820
|
const PROJECT_SETTINGS_REL = [".claude", "settings.json"];
|
|
818
821
|
function installStatusline(opts) {
|
|
819
822
|
if (opts.skip) {
|
|
820
823
|
return {
|
|
821
824
|
dispatcher: skipped(node_path.join(opts.home, ...USER_DISPATCHER_REL)),
|
|
825
|
+
rich: skipped(node_path.join(opts.home, ...USER_RICH_IMPL_REL)),
|
|
822
826
|
settings: skipped(node_path.join(opts.projectDir, ...PROJECT_SETTINGS_REL))
|
|
823
827
|
};
|
|
824
828
|
}
|
|
@@ -828,10 +832,18 @@ function installStatusline(opts) {
|
|
|
828
832
|
`bassclef init: cannot install statusline — bundled dispatcher missing at ${dispatcherSource}. Reinstall @thebassclef/lite or file a bug.`
|
|
829
833
|
);
|
|
830
834
|
}
|
|
831
|
-
const
|
|
832
|
-
|
|
835
|
+
const richImplSource = node_path.join(opts.packageDir, ...RICH_IMPL_REL);
|
|
836
|
+
if (!node_fs.existsSync(richImplSource)) {
|
|
837
|
+
throw new Error(
|
|
838
|
+
`bassclef init: cannot install statusline — bundled bassclef-statusline.sh missing at ${richImplSource}. Reinstall @thebassclef/lite or file a bug.`
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
const dispatcherBody = node_fs.readFileSync(dispatcherSource, "utf8");
|
|
842
|
+
const richBody = node_fs.readFileSync(richImplSource, "utf8");
|
|
843
|
+
const dispatcher = handleDispatcher(opts, dispatcherSource, dispatcherBody);
|
|
844
|
+
const rich = handleRichImpl(opts, richImplSource, richBody);
|
|
833
845
|
const settings = handleSettings(opts);
|
|
834
|
-
return { dispatcher, settings };
|
|
846
|
+
return { dispatcher, rich, settings };
|
|
835
847
|
}
|
|
836
848
|
function handleDispatcher(opts, sourcePath, sourceBody) {
|
|
837
849
|
const target = node_path.join(opts.home, ...USER_DISPATCHER_REL);
|
|
@@ -873,6 +885,57 @@ function handleDispatcher(opts, sourcePath, sourceBody) {
|
|
|
873
885
|
writeExec(target, sourceBody);
|
|
874
886
|
return { kind: "replaced", path: target, detail: `Overwrote ${target} (--force) with bundled dispatcher.` };
|
|
875
887
|
}
|
|
888
|
+
function handleRichImpl(opts, sourcePath, sourceBody) {
|
|
889
|
+
const target = node_path.join(opts.home, ...USER_RICH_IMPL_REL);
|
|
890
|
+
if (opts.dryRun) {
|
|
891
|
+
return {
|
|
892
|
+
kind: "would-install",
|
|
893
|
+
path: target,
|
|
894
|
+
detail: `Dry-run: would copy ${sourcePath} → ${target} (0755).`
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
if (!node_fs.existsSync(target)) {
|
|
898
|
+
writeExec(target, sourceBody);
|
|
899
|
+
return { kind: "installed", path: target, detail: `Wrote ${target} (0755) from bundled rich impl.` };
|
|
900
|
+
}
|
|
901
|
+
const st = node_fs.lstatSync(target);
|
|
902
|
+
if (st.isSymbolicLink()) {
|
|
903
|
+
if (!opts.force) {
|
|
904
|
+
return {
|
|
905
|
+
kind: "preserved",
|
|
906
|
+
path: target,
|
|
907
|
+
detail: `${target} is a symlink; refusing to follow. Pass --force to replace.`
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
node_fs.unlinkSync(target);
|
|
911
|
+
writeExec(target, sourceBody);
|
|
912
|
+
return { kind: "replaced", path: target, detail: `Replaced symlink at ${target} with bundled rich impl.` };
|
|
913
|
+
}
|
|
914
|
+
const existing = node_fs.readFileSync(target, "utf8");
|
|
915
|
+
if (existing === sourceBody) {
|
|
916
|
+
return { kind: "unchanged", path: target, detail: `${target} already matches bundled rich impl.` };
|
|
917
|
+
}
|
|
918
|
+
if (isLegacyDispatcher(existing)) {
|
|
919
|
+
writeExec(target, sourceBody);
|
|
920
|
+
return {
|
|
921
|
+
kind: "replaced",
|
|
922
|
+
path: target,
|
|
923
|
+
detail: `Migrated ${target} from legacy dispatcher to rich impl (cli#281 upgrade path).`
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
if (!opts.force) {
|
|
927
|
+
return {
|
|
928
|
+
kind: "preserved",
|
|
929
|
+
path: target,
|
|
930
|
+
detail: `${target} differs from bundled rich impl. Pass --force to overwrite.`
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
writeExec(target, sourceBody);
|
|
934
|
+
return { kind: "replaced", path: target, detail: `Overwrote ${target} (--force) with bundled rich impl.` };
|
|
935
|
+
}
|
|
936
|
+
function isLegacyDispatcher(body) {
|
|
937
|
+
return body.includes("BASSCLEF_SYNC_VERSION=thin-pointer-statusline-");
|
|
938
|
+
}
|
|
876
939
|
function handleSettings(opts) {
|
|
877
940
|
const target = node_path.join(opts.projectDir, ...PROJECT_SETTINGS_REL);
|
|
878
941
|
if (opts.settingsPreserved) {
|
|
@@ -900,6 +963,15 @@ function handleSettings(opts) {
|
|
|
900
963
|
if (matchesDesired(current)) {
|
|
901
964
|
return { kind: "unchanged", path: target, detail: `${target} already carries the bassclef statusLine.` };
|
|
902
965
|
}
|
|
966
|
+
if (isLegacyStatuslineCommand(current)) {
|
|
967
|
+
const next2 = { ...existing, statusLine: desired };
|
|
968
|
+
writeSettings(target, next2);
|
|
969
|
+
return {
|
|
970
|
+
kind: "replaced",
|
|
971
|
+
path: target,
|
|
972
|
+
detail: `Migrated statusLine in ${target} from legacy dispatcher path to new dispatcher path (cli#281 upgrade).`
|
|
973
|
+
};
|
|
974
|
+
}
|
|
903
975
|
if (!opts.force) {
|
|
904
976
|
return {
|
|
905
977
|
kind: "preserved",
|
|
@@ -911,6 +983,11 @@ function handleSettings(opts) {
|
|
|
911
983
|
writeSettings(target, next);
|
|
912
984
|
return { kind: "replaced", path: target, detail: `Overwrote statusLine in ${target} (--force).` };
|
|
913
985
|
}
|
|
986
|
+
function isLegacyStatuslineCommand(current) {
|
|
987
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return false;
|
|
988
|
+
const c = current;
|
|
989
|
+
return c.type === "command" && c.command === LEGACY_STATUSLINE_COMMAND;
|
|
990
|
+
}
|
|
914
991
|
function skipped(path) {
|
|
915
992
|
return {
|
|
916
993
|
kind: "skipped",
|
|
@@ -1647,15 +1724,16 @@ function maybeEmitStatuslinePlan(opts) {
|
|
|
1647
1724
|
return;
|
|
1648
1725
|
}
|
|
1649
1726
|
const dv = report.dispatcher.kind;
|
|
1727
|
+
const rv = report.rich.kind;
|
|
1650
1728
|
const sv = report.settings.kind;
|
|
1651
|
-
if (dv === "skipped" && sv === "skipped") {
|
|
1729
|
+
if (dv === "skipped" && rv === "skipped" && sv === "skipped") {
|
|
1652
1730
|
opts.say(`bassclef init: statusline install skipped (--skip-statusline).
|
|
1653
1731
|
`);
|
|
1654
1732
|
return;
|
|
1655
1733
|
}
|
|
1656
1734
|
if (opts.dryRun) {
|
|
1657
1735
|
opts.say(
|
|
1658
|
-
`bassclef init: would install statusline dispatcher at ${report.dispatcher.path} and set statusLine in ${report.settings.path}.
|
|
1736
|
+
`bassclef init: would install statusline dispatcher at ${report.dispatcher.path}, rich impl at ${report.rich.path}, and set statusLine in ${report.settings.path}.
|
|
1659
1737
|
`
|
|
1660
1738
|
);
|
|
1661
1739
|
return;
|
|
@@ -1665,6 +1743,10 @@ function maybeEmitStatuslinePlan(opts) {
|
|
|
1665
1743
|
else if (dv === "replaced") parts.push(`replaced ${report.dispatcher.path} (--force)`);
|
|
1666
1744
|
else if (dv === "preserved") parts.push(`preserved ${report.dispatcher.path} (pass --force to overwrite)`);
|
|
1667
1745
|
else if (dv === "unchanged") parts.push(`${report.dispatcher.path} already matches bundle`);
|
|
1746
|
+
if (rv === "installed") parts.push(`wrote ${report.rich.path}`);
|
|
1747
|
+
else if (rv === "replaced") parts.push(`replaced ${report.rich.path} (migration or --force)`);
|
|
1748
|
+
else if (rv === "preserved") parts.push(`preserved ${report.rich.path} (pass --force to overwrite)`);
|
|
1749
|
+
else if (rv === "unchanged") parts.push(`${report.rich.path} already matches bundle`);
|
|
1668
1750
|
if (sv === "installed") parts.push(`set statusLine in ${report.settings.path}`);
|
|
1669
1751
|
else if (sv === "replaced") parts.push(`replaced statusLine in ${report.settings.path} (--force)`);
|
|
1670
1752
|
else if (sv === "preserved") parts.push(`preserved statusLine in ${report.settings.path} (pass --force to overwrite)`);
|
package/dist/cli.js
CHANGED
|
@@ -787,15 +787,19 @@ function dirExists(p) {
|
|
|
787
787
|
}
|
|
788
788
|
const STATUSLINE_FIELD = {
|
|
789
789
|
type: "command",
|
|
790
|
-
command: "bash ~/.claude/bassclef-statusline.sh"
|
|
790
|
+
command: "bash ~/.claude/bassclef-statusline-dispatcher.sh"
|
|
791
791
|
};
|
|
792
|
+
const LEGACY_STATUSLINE_COMMAND = "bash ~/.claude/bassclef-statusline.sh";
|
|
792
793
|
const DISPATCHER_REL = ["dist", "lite", "presence", "cli", "bassclef-statusline.dispatcher.sh"];
|
|
793
|
-
const
|
|
794
|
+
const RICH_IMPL_REL = ["dist", "lite", "presence", "cli", "bassclef-statusline.sh"];
|
|
795
|
+
const USER_DISPATCHER_REL = [".claude", "bassclef-statusline-dispatcher.sh"];
|
|
796
|
+
const USER_RICH_IMPL_REL = [".claude", "bassclef-statusline.sh"];
|
|
794
797
|
const PROJECT_SETTINGS_REL = [".claude", "settings.json"];
|
|
795
798
|
function installStatusline(opts) {
|
|
796
799
|
if (opts.skip) {
|
|
797
800
|
return {
|
|
798
801
|
dispatcher: skipped(join(opts.home, ...USER_DISPATCHER_REL)),
|
|
802
|
+
rich: skipped(join(opts.home, ...USER_RICH_IMPL_REL)),
|
|
799
803
|
settings: skipped(join(opts.projectDir, ...PROJECT_SETTINGS_REL))
|
|
800
804
|
};
|
|
801
805
|
}
|
|
@@ -805,10 +809,18 @@ function installStatusline(opts) {
|
|
|
805
809
|
`bassclef init: cannot install statusline — bundled dispatcher missing at ${dispatcherSource}. Reinstall @thebassclef/lite or file a bug.`
|
|
806
810
|
);
|
|
807
811
|
}
|
|
808
|
-
const
|
|
809
|
-
|
|
812
|
+
const richImplSource = join(opts.packageDir, ...RICH_IMPL_REL);
|
|
813
|
+
if (!existsSync(richImplSource)) {
|
|
814
|
+
throw new Error(
|
|
815
|
+
`bassclef init: cannot install statusline — bundled bassclef-statusline.sh missing at ${richImplSource}. Reinstall @thebassclef/lite or file a bug.`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
const dispatcherBody = readFileSync(dispatcherSource, "utf8");
|
|
819
|
+
const richBody = readFileSync(richImplSource, "utf8");
|
|
820
|
+
const dispatcher = handleDispatcher(opts, dispatcherSource, dispatcherBody);
|
|
821
|
+
const rich = handleRichImpl(opts, richImplSource, richBody);
|
|
810
822
|
const settings = handleSettings(opts);
|
|
811
|
-
return { dispatcher, settings };
|
|
823
|
+
return { dispatcher, rich, settings };
|
|
812
824
|
}
|
|
813
825
|
function handleDispatcher(opts, sourcePath, sourceBody) {
|
|
814
826
|
const target = join(opts.home, ...USER_DISPATCHER_REL);
|
|
@@ -850,6 +862,57 @@ function handleDispatcher(opts, sourcePath, sourceBody) {
|
|
|
850
862
|
writeExec(target, sourceBody);
|
|
851
863
|
return { kind: "replaced", path: target, detail: `Overwrote ${target} (--force) with bundled dispatcher.` };
|
|
852
864
|
}
|
|
865
|
+
function handleRichImpl(opts, sourcePath, sourceBody) {
|
|
866
|
+
const target = join(opts.home, ...USER_RICH_IMPL_REL);
|
|
867
|
+
if (opts.dryRun) {
|
|
868
|
+
return {
|
|
869
|
+
kind: "would-install",
|
|
870
|
+
path: target,
|
|
871
|
+
detail: `Dry-run: would copy ${sourcePath} → ${target} (0755).`
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
if (!existsSync(target)) {
|
|
875
|
+
writeExec(target, sourceBody);
|
|
876
|
+
return { kind: "installed", path: target, detail: `Wrote ${target} (0755) from bundled rich impl.` };
|
|
877
|
+
}
|
|
878
|
+
const st = lstatSync(target);
|
|
879
|
+
if (st.isSymbolicLink()) {
|
|
880
|
+
if (!opts.force) {
|
|
881
|
+
return {
|
|
882
|
+
kind: "preserved",
|
|
883
|
+
path: target,
|
|
884
|
+
detail: `${target} is a symlink; refusing to follow. Pass --force to replace.`
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
unlinkSync(target);
|
|
888
|
+
writeExec(target, sourceBody);
|
|
889
|
+
return { kind: "replaced", path: target, detail: `Replaced symlink at ${target} with bundled rich impl.` };
|
|
890
|
+
}
|
|
891
|
+
const existing = readFileSync(target, "utf8");
|
|
892
|
+
if (existing === sourceBody) {
|
|
893
|
+
return { kind: "unchanged", path: target, detail: `${target} already matches bundled rich impl.` };
|
|
894
|
+
}
|
|
895
|
+
if (isLegacyDispatcher(existing)) {
|
|
896
|
+
writeExec(target, sourceBody);
|
|
897
|
+
return {
|
|
898
|
+
kind: "replaced",
|
|
899
|
+
path: target,
|
|
900
|
+
detail: `Migrated ${target} from legacy dispatcher to rich impl (cli#281 upgrade path).`
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
if (!opts.force) {
|
|
904
|
+
return {
|
|
905
|
+
kind: "preserved",
|
|
906
|
+
path: target,
|
|
907
|
+
detail: `${target} differs from bundled rich impl. Pass --force to overwrite.`
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
writeExec(target, sourceBody);
|
|
911
|
+
return { kind: "replaced", path: target, detail: `Overwrote ${target} (--force) with bundled rich impl.` };
|
|
912
|
+
}
|
|
913
|
+
function isLegacyDispatcher(body) {
|
|
914
|
+
return body.includes("BASSCLEF_SYNC_VERSION=thin-pointer-statusline-");
|
|
915
|
+
}
|
|
853
916
|
function handleSettings(opts) {
|
|
854
917
|
const target = join(opts.projectDir, ...PROJECT_SETTINGS_REL);
|
|
855
918
|
if (opts.settingsPreserved) {
|
|
@@ -877,6 +940,15 @@ function handleSettings(opts) {
|
|
|
877
940
|
if (matchesDesired(current)) {
|
|
878
941
|
return { kind: "unchanged", path: target, detail: `${target} already carries the bassclef statusLine.` };
|
|
879
942
|
}
|
|
943
|
+
if (isLegacyStatuslineCommand(current)) {
|
|
944
|
+
const next2 = { ...existing, statusLine: desired };
|
|
945
|
+
writeSettings(target, next2);
|
|
946
|
+
return {
|
|
947
|
+
kind: "replaced",
|
|
948
|
+
path: target,
|
|
949
|
+
detail: `Migrated statusLine in ${target} from legacy dispatcher path to new dispatcher path (cli#281 upgrade).`
|
|
950
|
+
};
|
|
951
|
+
}
|
|
880
952
|
if (!opts.force) {
|
|
881
953
|
return {
|
|
882
954
|
kind: "preserved",
|
|
@@ -888,6 +960,11 @@ function handleSettings(opts) {
|
|
|
888
960
|
writeSettings(target, next);
|
|
889
961
|
return { kind: "replaced", path: target, detail: `Overwrote statusLine in ${target} (--force).` };
|
|
890
962
|
}
|
|
963
|
+
function isLegacyStatuslineCommand(current) {
|
|
964
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return false;
|
|
965
|
+
const c = current;
|
|
966
|
+
return c.type === "command" && c.command === LEGACY_STATUSLINE_COMMAND;
|
|
967
|
+
}
|
|
891
968
|
function skipped(path) {
|
|
892
969
|
return {
|
|
893
970
|
kind: "skipped",
|
|
@@ -1624,15 +1701,16 @@ function maybeEmitStatuslinePlan(opts) {
|
|
|
1624
1701
|
return;
|
|
1625
1702
|
}
|
|
1626
1703
|
const dv = report.dispatcher.kind;
|
|
1704
|
+
const rv = report.rich.kind;
|
|
1627
1705
|
const sv = report.settings.kind;
|
|
1628
|
-
if (dv === "skipped" && sv === "skipped") {
|
|
1706
|
+
if (dv === "skipped" && rv === "skipped" && sv === "skipped") {
|
|
1629
1707
|
opts.say(`bassclef init: statusline install skipped (--skip-statusline).
|
|
1630
1708
|
`);
|
|
1631
1709
|
return;
|
|
1632
1710
|
}
|
|
1633
1711
|
if (opts.dryRun) {
|
|
1634
1712
|
opts.say(
|
|
1635
|
-
`bassclef init: would install statusline dispatcher at ${report.dispatcher.path} and set statusLine in ${report.settings.path}.
|
|
1713
|
+
`bassclef init: would install statusline dispatcher at ${report.dispatcher.path}, rich impl at ${report.rich.path}, and set statusLine in ${report.settings.path}.
|
|
1636
1714
|
`
|
|
1637
1715
|
);
|
|
1638
1716
|
return;
|
|
@@ -1642,6 +1720,10 @@ function maybeEmitStatuslinePlan(opts) {
|
|
|
1642
1720
|
else if (dv === "replaced") parts.push(`replaced ${report.dispatcher.path} (--force)`);
|
|
1643
1721
|
else if (dv === "preserved") parts.push(`preserved ${report.dispatcher.path} (pass --force to overwrite)`);
|
|
1644
1722
|
else if (dv === "unchanged") parts.push(`${report.dispatcher.path} already matches bundle`);
|
|
1723
|
+
if (rv === "installed") parts.push(`wrote ${report.rich.path}`);
|
|
1724
|
+
else if (rv === "replaced") parts.push(`replaced ${report.rich.path} (migration or --force)`);
|
|
1725
|
+
else if (rv === "preserved") parts.push(`preserved ${report.rich.path} (pass --force to overwrite)`);
|
|
1726
|
+
else if (rv === "unchanged") parts.push(`${report.rich.path} already matches bundle`);
|
|
1645
1727
|
if (sv === "installed") parts.push(`set statusLine in ${report.settings.path}`);
|
|
1646
1728
|
else if (sv === "replaced") parts.push(`replaced statusLine in ${report.settings.path} (--force)`);
|
|
1647
1729
|
else if (sv === "preserved") parts.push(`preserved statusLine in ${report.settings.path} (pass --force to overwrite)`);
|
package/dist/index.cjs
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const version: "1.9.
|
|
1
|
+
export declare const version: "1.9.9";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -50,7 +50,10 @@ if ! echo "$CMD" | grep -qE '(^|[[:space:]])git[[:space:]]+stash([[:space:]]+pus
|
|
|
50
50
|
fi
|
|
51
51
|
|
|
52
52
|
# --- Override — SKIP_GIT_STASH_U_GUARD=1 bypasses (logged) ---
|
|
53
|
-
|
|
53
|
+
# Honors inline SKIP per #2036 Finding #4 (recipe must reach the hook).
|
|
54
|
+
# shellcheck source=/dev/null
|
|
55
|
+
source "${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}/lib/skip-env-inline-check.sh" 2>/dev/null || true
|
|
56
|
+
if declare -F skip_var_set >/dev/null && skip_var_set "SKIP_GIT_STASH_U_GUARD" "$CMD"; then
|
|
54
57
|
TRACE_LOG="${BASSCLEF_TRACE_LOG:-${BASSCLEF_TRACE_FILE:-$HOME/.claude/state/trace.log}}"
|
|
55
58
|
mkdir -p "$(dirname "$TRACE_LOG")" 2>/dev/null || true
|
|
56
59
|
echo "$(date -u +%FT%TZ) SKIP_GIT_STASH_U_GUARD $CMD" >> "$TRACE_LOG" 2>/dev/null || true
|
|
@@ -45,7 +45,12 @@ SKILL_NAME=$(jq <<< "$TOOL_INPUT" -r '.skill // ""' 2>/dev/null)
|
|
|
45
45
|
|
|
46
46
|
# Major composer skills — these chain other skills or do heavy work.
|
|
47
47
|
# Friction surfaces best at their boundaries.
|
|
48
|
-
|
|
48
|
+
# Per ADR-040 D1 vocabulary rename (grace window through 2026-10-31 per
|
|
49
|
+
# .claude/rules/we-dont-break-adopters.md): new names stage/launch added;
|
|
50
|
+
# old names preview/preview-build preserved for grace-window compat.
|
|
51
|
+
# Fix for bassclef-upstream#1777 (friction log never created because current
|
|
52
|
+
# vocabulary skipped the gate).
|
|
53
|
+
MAJOR_SKILLS_PATTERN="^(preview|preview-build|stage|launch|build|shape|longrun|decompose|spec|architect-review|frontend-design|prototype-variants|prototype-gallery|interpret-input|objectory-decompose|ux-migration|ia-model|interaction-design|use-case|user-stories|jtbd-tasks|task-scenarios)$"
|
|
49
54
|
|
|
50
55
|
if ! echo "$SKILL_NAME" | grep -qE "$MAJOR_SKILLS_PATTERN"; then
|
|
51
56
|
exit 0
|
|
@@ -41,7 +41,16 @@ trace_log "pre-commit" "git commit"
|
|
|
41
41
|
# Exempt: rules/hooks/skills/standards/docs/strategy/chronicle/scripts/tests
|
|
42
42
|
# (places where the policy itself is documented or test fixtures live).
|
|
43
43
|
# Override: SKIP_OPERATOR_PATHS=1 git commit (logged via trace-helper)
|
|
44
|
-
|
|
44
|
+
#
|
|
45
|
+
# Env-reach fix per #2036 Finding #4: the inline recipe sets the var in
|
|
46
|
+
# the git subshell env, not the hook env. Source the helper lib so the
|
|
47
|
+
# recipe reaches the check reliably across harness setups.
|
|
48
|
+
# shellcheck source=/dev/null
|
|
49
|
+
source "${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}/lib/skip-env-inline-check.sh" 2>/dev/null || true
|
|
50
|
+
if declare -F skip_var_set >/dev/null && skip_var_set "SKIP_OPERATOR_PATHS" "$COMMAND"; then
|
|
51
|
+
# Override honored; skip the check.
|
|
52
|
+
:
|
|
53
|
+
else
|
|
45
54
|
STAGED_NONEXEMPT=$(git diff --cached --name-only --diff-filter=AM 2>/dev/null | \
|
|
46
55
|
grep -vE '^\.claude/(rules|hooks|skills|luminaries|agents)/' | \
|
|
47
56
|
grep -vE '^standards/' | \
|
|
@@ -50,7 +59,8 @@ if [ "${SKIP_OPERATOR_PATHS:-0}" != "1" ]; then
|
|
|
50
59
|
grep -vE '^docs/' | \
|
|
51
60
|
grep -vE '^chronicle/' | \
|
|
52
61
|
grep -vE '^strategy/' | \
|
|
53
|
-
grep -vE '^scripts/tests/'
|
|
62
|
+
grep -vE '^scripts/tests/' | \
|
|
63
|
+
grep -vE '^lib/tests/' || true)
|
|
54
64
|
if [ -n "$STAGED_NONEXEMPT" ]; then
|
|
55
65
|
LEAKED=""
|
|
56
66
|
while IFS= read -r f; do
|
|
@@ -110,7 +120,8 @@ fi
|
|
|
110
120
|
# fast-fail layer per Saltzer-Schroeder complete mediation.
|
|
111
121
|
#
|
|
112
122
|
# Override: SKIP_COLD_ADOPTER_HARNESS=1 git commit (logged via trace-helper)
|
|
113
|
-
|
|
123
|
+
# Env-reach via skip_var_set helper per #2036 Finding #4.
|
|
124
|
+
if ! { declare -F skip_var_set >/dev/null && skip_var_set "SKIP_COLD_ADOPTER_HARNESS" "$COMMAND"; }; then
|
|
114
125
|
# The 12-pattern matcher mirrors .claude/rules/cold-adopter-harness-discipline.md
|
|
115
126
|
# § "When this rule fires". Keep these patterns in sync; a future WU may
|
|
116
127
|
# centralize the list via a sourced library function.
|
|
@@ -173,7 +184,8 @@ fi
|
|
|
173
184
|
# state/markers/lead-lens-signoff/<branch-slug>.marker
|
|
174
185
|
# Discipline lives in .claude/rules/loop-discipline.md Step 0.5 + 2a + 5.5.
|
|
175
186
|
# Override: SKIP_LEAD_LENS_SIGNOFF=1 git commit (logged via trace-helper)
|
|
176
|
-
|
|
187
|
+
# Env-reach via skip_var_set helper per #2036 Finding #4.
|
|
188
|
+
if ! { declare -F skip_var_set >/dev/null && skip_var_set "SKIP_LEAD_LENS_SIGNOFF" "$COMMAND"; }; then
|
|
177
189
|
# Reuse the substrate-affecting matcher shape from cold-adopter fast-path.
|
|
178
190
|
STAGED_SUBSTRATE_LL=$(git diff --cached --name-only --diff-filter=AM 2>/dev/null | grep -E \
|
|
179
191
|
-e '^\.claude/hooks/[^/]+\.sh$' \
|
|
@@ -305,7 +317,7 @@ fi
|
|
|
305
317
|
|
|
306
318
|
# --- Reserved skill names check (collision with Claude Code built-ins) ---
|
|
307
319
|
# See .claude/rules/reserved-skill-names.md + standards/reserved-skill-names.md
|
|
308
|
-
if
|
|
320
|
+
if ! { declare -F skip_var_set >/dev/null && skip_var_set "SKIP_RESERVED_NAMES" "$COMMAND"; }; then
|
|
309
321
|
STAGED_NEW_SKILLS=$(git diff --cached --name-only --diff-filter=A -- '.claude/skills/*/SKILL.md' 2>/dev/null || true)
|
|
310
322
|
if [ -n "$STAGED_NEW_SKILLS" ]; then
|
|
311
323
|
# Extract reserved names from the standards table (Name column values inside backticks)
|
|
@@ -351,7 +363,7 @@ fi
|
|
|
351
363
|
# counter, silencing the compliance signal (the failure mode that motivated
|
|
352
364
|
# this gate). Block commits to chronicle/*.md or docs/chronicle/*.md that
|
|
353
365
|
# lack the section. Override: SKIP_CHRONICLE_GATE_EVIDENCE=1 git commit.
|
|
354
|
-
if
|
|
366
|
+
if ! { declare -F skip_var_set >/dev/null && skip_var_set "SKIP_CHRONICLE_GATE_EVIDENCE" "$COMMAND"; }; then
|
|
355
367
|
STAGED_CHRONICLES=$(git diff --cached --name-only --diff-filter=AM -- 'chronicle/*.md' 'docs/chronicle/*.md' 2>/dev/null || true)
|
|
356
368
|
# Source spine accessor library when available — preferred frontmatter
|
|
357
369
|
# check route per state-spine-contract.md. Graceful fallback for repos
|
|
@@ -33,12 +33,6 @@
|
|
|
33
33
|
|
|
34
34
|
set -u
|
|
35
35
|
|
|
36
|
-
# Override path — logged via trace-helper per bassclef discipline
|
|
37
|
-
if [ "${SKIP_IDENTIFIER_LEAK_SCRUB:-0}" = "1" ]; then
|
|
38
|
-
echo " identifier-leak-scrub: SKIP_IDENTIFIER_LEAK_SCRUB=1 — bypassing scrub" >&2
|
|
39
|
-
exit 0
|
|
40
|
-
fi
|
|
41
|
-
|
|
42
36
|
# R-N1 fail-soft: if grep is unavailable, warn to stderr and exit 0
|
|
43
37
|
if ! command -v grep >/dev/null 2>&1; then
|
|
44
38
|
echo " identifier-leak-scrub: warn — grep unavailable; skipping scrub" >&2
|
|
@@ -59,6 +53,16 @@ else
|
|
|
59
53
|
COMMAND=$(echo "$INPUT" | grep -oE '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/^"command"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
|
|
60
54
|
fi
|
|
61
55
|
|
|
56
|
+
# Override path — honors inline SKIP_FOO=1 cmd per #2036 Finding #4.
|
|
57
|
+
# PreToolUse hooks run in Claude Code's env, not the git subshell env.
|
|
58
|
+
# The helper lib parses $COMMAND for the inline prefix + falls back to env.
|
|
59
|
+
# shellcheck source=/dev/null
|
|
60
|
+
source "${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}/lib/skip-env-inline-check.sh" 2>/dev/null || true
|
|
61
|
+
if declare -F skip_var_set >/dev/null && skip_var_set "SKIP_IDENTIFIER_LEAK_SCRUB" "$COMMAND"; then
|
|
62
|
+
echo " identifier-leak-scrub: SKIP_IDENTIFIER_LEAK_SCRUB=1 — bypassing scrub" >&2
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
62
66
|
# Pass through non-git-commit commands
|
|
63
67
|
if ! echo "$COMMAND" | grep -qE '^[[:space:]]*git[[:space:]]+commit'; then
|
|
64
68
|
exit 0
|
|
@@ -38,8 +38,10 @@ if ! echo "$COMMAND" | grep -q 'git commit'; then
|
|
|
38
38
|
exit 0
|
|
39
39
|
fi
|
|
40
40
|
|
|
41
|
-
# Override path
|
|
42
|
-
|
|
41
|
+
# Override path — honors inline SKIP_FOO=1 cmd per #2036 Finding #4.
|
|
42
|
+
# shellcheck source=/dev/null
|
|
43
|
+
source "${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}/lib/skip-env-inline-check.sh" 2>/dev/null || true
|
|
44
|
+
if declare -F skip_var_set >/dev/null && skip_var_set "SKIP_MANIFEST_AUTOREGEN" "$COMMAND"; then
|
|
43
45
|
# Best-effort trace log; ignore failure so hook stays exit 0
|
|
44
46
|
if command -v bash >/dev/null 2>&1 && [ -f ".claude/hooks/trace-helper.sh" ]; then
|
|
45
47
|
bash .claude/hooks/trace-helper.sh "skip" "manifest-autoregen" "SKIP_MANIFEST_AUTOREGEN=1" 2>/dev/null || true
|
|
@@ -216,6 +216,30 @@ if [ -n "$DIRTY" ]; then
|
|
|
216
216
|
git add 'state/' ':!state/markers' ':!state/markers/**' 2>/dev/null || true
|
|
217
217
|
git add .claude/projects/*/memory/ 2>/dev/null || true
|
|
218
218
|
|
|
219
|
+
STAGED=$(git diff --cached --name-only 2>/dev/null)
|
|
220
|
+
if [ -n "$STAGED" ]; then
|
|
221
|
+
# Operator-identifier leak check per #2036 Finding #1.
|
|
222
|
+
# save-state runs `git commit` directly, bypassing the PreToolUse
|
|
223
|
+
# pre-commit-gate.sh CCF-3 check. Even if CCF-3 fired, its grep
|
|
224
|
+
# exempts docs/ paths — so docs/sdlc-traces/*.log carrying absolute
|
|
225
|
+
# /Users/<name>/ paths would still get committed.
|
|
226
|
+
# Shared lib so this check matches pre-commit-gate CCF-3 shape.
|
|
227
|
+
REPO_ROOT_FOR_LIB="$(git rev-parse --show-toplevel 2>/dev/null)"
|
|
228
|
+
LEAK_LIB="${REPO_ROOT_FOR_LIB}/lib/identifier-leak-check.sh"
|
|
229
|
+
if [ -f "$LEAK_LIB" ]; then
|
|
230
|
+
# shellcheck source=/dev/null
|
|
231
|
+
source "$LEAK_LIB"
|
|
232
|
+
if ! check_staged_for_leaks; then
|
|
233
|
+
log "BLOCKED: auto-save detected operator paths in staged content; resetting stage"
|
|
234
|
+
git reset HEAD -- . 2>/dev/null || true
|
|
235
|
+
# Fall through — SESSION_LOCK cleanup below still fires, push is skipped
|
|
236
|
+
# because STAGED check below sees empty stage.
|
|
237
|
+
STAGED=""
|
|
238
|
+
fi
|
|
239
|
+
fi
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
# Re-check STAGED after possible reset
|
|
219
243
|
STAGED=$(git diff --cached --name-only 2>/dev/null)
|
|
220
244
|
if [ -n "$STAGED" ]; then
|
|
221
245
|
TIMESTAMP=$(date +%Y-%m-%dT%H:%M:%S)
|
|
@@ -5,9 +5,28 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Usage: source this file, then call:
|
|
7
7
|
# trace_log "hook-name" "trigger-description"
|
|
8
|
+
#
|
|
9
|
+
# Privacy — relative paths (#2036 Finding #1 — adopter report 2026-10-02):
|
|
10
|
+
# The ${trigger} field can carry filesystem paths from caller args
|
|
11
|
+
# (e.g., $FILE_PATH from PreToolUse hooks). We strip repo-root prefix
|
|
12
|
+
# so adopter traces never leak /Users/<name>/ or /home/<name>/ paths
|
|
13
|
+
# to adopter git. Fallback chain: $CLAUDE_PROJECT_DIR → git rev-parse
|
|
14
|
+
# --show-toplevel → no-strip (silent — trigger lands as-is).
|
|
8
15
|
|
|
9
16
|
TRACE_DIR="docs/sdlc-traces"
|
|
10
17
|
|
|
18
|
+
# _trace_repo_root — resolve repo root for prefix-strip.
|
|
19
|
+
# Precondition fallback per @luminary tony-hoare — Hoare R1 risk ledger.
|
|
20
|
+
_trace_repo_root() {
|
|
21
|
+
if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
|
|
22
|
+
echo "$CLAUDE_PROJECT_DIR"
|
|
23
|
+
return 0
|
|
24
|
+
fi
|
|
25
|
+
local root
|
|
26
|
+
root=$(git rev-parse --show-toplevel 2>/dev/null) || return 1
|
|
27
|
+
echo "$root"
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
trace_log() {
|
|
12
31
|
local hook="$1"
|
|
13
32
|
local trigger="$2"
|
|
@@ -17,6 +36,16 @@ trace_log() {
|
|
|
17
36
|
today=$(date +%Y-%m-%d)
|
|
18
37
|
local log_file="${TRACE_DIR}/${today}.log"
|
|
19
38
|
|
|
39
|
+
# Strip repo-root prefix from trigger before writing.
|
|
40
|
+
# Only strips when root is a non-empty prefix of trigger — leaves
|
|
41
|
+
# unrelated absolute paths (/opt/anthropic/..., /usr/bin/...) intact.
|
|
42
|
+
local root
|
|
43
|
+
root=$(_trace_repo_root 2>/dev/null) || root=""
|
|
44
|
+
if [ -n "$root" ] && [ "$root" != "/" ]; then
|
|
45
|
+
trigger="${trigger//$root\//}"
|
|
46
|
+
trigger="${trigger//$root/}"
|
|
47
|
+
fi
|
|
48
|
+
|
|
20
49
|
mkdir -p "$TRACE_DIR" 2>/dev/null || true
|
|
21
50
|
echo "${timestamp} | ${hook} | ${trigger} | — | —" >> "$log_file" 2>/dev/null || true
|
|
22
51
|
}
|
|
@@ -36,11 +36,41 @@ When a PR changes the manifest schema OR output shape:
|
|
|
36
36
|
3. **Downstream cure named** — the entry names what adopters need to do. If nothing (backward-compatible add), say so explicitly. If code change (like the v0.36.0 `upstream_commit` removal), name the pattern.
|
|
37
37
|
4. **Release script pre-flight** — before every `/release`, the script reads the ledger and confirms an entry exists for the outgoing `manifest_version`. Missing entry BLOCKs the cascade.
|
|
38
38
|
|
|
39
|
+
## Retirements
|
|
40
|
+
|
|
41
|
+
Retirement-class changes (content-remove: a file previously shipped in the lite manifest gets deleted intentionally) ship with the same discipline shape as additions. Three paired artifacts, same PR:
|
|
42
|
+
|
|
43
|
+
1. **Ledger entry** under this ledger's `## Entries` section naming the removed slugs, the deletion SHA, the version bump (MINOR per content-remove policy), and the downstream cure (what adopters at an exact pin do).
|
|
44
|
+
2. **Allowlist row** in `.claude/hooks/tests/fixtures/2026-09-16-lite-catalog-closure/retired-entries.txt` naming each removed `<type>/<slug>` + `retired-in=<SHA>` + `reason=<one-line>`. The Feathers v1.6.1 snapshot guard (Test 16 in `.claude/hooks/tests/generate-lite-manifest.test.sh`) subtracts allowlist rows before asserting zero drops. Without the allowlist row the test fails and main CI goes red.
|
|
45
|
+
3. **Discipline amendment** when a new retirement class surfaces that this section does not cover (e.g., tier retirement, field retirement). Extend this `## Retirements` section rather than author a new rule, per Ousterhout's complexity-is-incremental principle.
|
|
46
|
+
|
|
47
|
+
When this rule fires on retirement:
|
|
48
|
+
|
|
49
|
+
- Any PR that deletes a file listed in the current `lite-manifest.json` under `.entries[].path`
|
|
50
|
+
- Any PR that retires a schema field that previously shipped as REQUIRED
|
|
51
|
+
|
|
52
|
+
Skips (same as additions):
|
|
53
|
+
|
|
54
|
+
- Content-only changes where the file stays but its content_hash shifts
|
|
55
|
+
- Comment-only edits
|
|
56
|
+
|
|
57
|
+
**Anchor luminaries for retirement:**
|
|
58
|
+
|
|
59
|
+
- `@luminary linus-torvalds` — retirement of a tier-tagged adopter-observable file is a userspace break; the ledger + allowlist + release-step6 check ship in one PR
|
|
60
|
+
- `@luminary hyrum-wright` — observable behavior becomes dependency with enough adopters; the 2 removed slugs were cited in rule bodies, sister docs, and the fixture snapshot; every retirement needs observable-surface enumeration first
|
|
61
|
+
- `@luminary michael-nygard` — current ledger discipline for additions is methodology at author time; retirement needs the same mechanical floor plus a stability-pattern gate (release-step6 pre-flight)
|
|
62
|
+
- `@luminary michael-feathers` — the fixture snapshot is the characterization test; allowlist is the author-time hook that keeps it green
|
|
63
|
+
- `@luminary vaughn-vernon` — `retired-entries.txt` is the anticorruption layer between the frozen snapshot and the current manifest
|
|
64
|
+
- `@luminary john-ousterhout` — contrarian: extend the existing rule with this section, do not author a new rule; the mechanical pre-commit hook (`pre-retire-discipline-check.sh`) is deferred to a follow-on PR so this one stays scope-tight
|
|
65
|
+
|
|
39
66
|
## Anti-patterns
|
|
40
67
|
|
|
41
68
|
**Schema field added but no ledger entry.** Release script pre-flight BLOCKs.
|
|
42
69
|
INSTEAD: add the entry in the same PR.
|
|
43
70
|
|
|
71
|
+
**File removed from the lite manifest without an allowlist row + ledger entry.** Test 16 (Feathers v1.6.1 snapshot guard) sees the drop and main CI goes red at the next run.
|
|
72
|
+
INSTEAD: append a row to `retired-entries.txt` naming `<type>/<slug> retired-in=<SHA> reason=<one-line>` AND add a `## Retirements`-aware entry to `standards/lite-manifest-schema-changes.md` in the same PR.
|
|
73
|
+
|
|
44
74
|
**Ledger entry written but wrong version bump.** Adopters plan their pin update against the wrong version.
|
|
45
75
|
INSTEAD: correct the bump before merge. AJV validation of the entry format catches inconsistency.
|
|
46
76
|
|
|
@@ -236,6 +236,6 @@
|
|
|
236
236
|
"$schema": "https://bassclef.sunj-labs/state-spine/v0/schemas/bassclef-wiring-manifest.schema.json",
|
|
237
237
|
"statusLine": {
|
|
238
238
|
"type": "command",
|
|
239
|
-
"command": "bash ~/.claude/bassclef-statusline.sh"
|
|
239
|
+
"command": "bash ~/.claude/bassclef-statusline-dispatcher.sh"
|
|
240
240
|
}
|
|
241
241
|
}
|