@thebassclef/lite 1.1.0 → 1.2.0-alpha.1

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.
Files changed (135) hide show
  1. package/dist/cli.cjs +258 -63
  2. package/dist/cli.js +258 -63
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/lite/.claude/bassclef-configs.jsonc +56 -0
  7. package/dist/lite/.claude/bassclef-orientation.md +120 -0
  8. package/dist/lite/.claude/hooks/adr-discipline-check.sh +132 -0
  9. package/dist/lite/.claude/hooks/atomic-pr-check.sh +169 -0
  10. package/dist/lite/.claude/hooks/auto-save-idle.sh +51 -0
  11. package/dist/lite/.claude/hooks/bassclef-sync.sh +51 -13
  12. package/dist/lite/.claude/hooks/bet-doc-gate.sh +283 -0
  13. package/dist/lite/.claude/hooks/bug-diagnosis.sh +64 -0
  14. package/dist/lite/.claude/hooks/destructive-command-guard.sh +136 -0
  15. package/dist/lite/.claude/hooks/file-rename-discipline-check.sh +147 -0
  16. package/dist/lite/.claude/hooks/hook-classifier-canary.sh +158 -0
  17. package/dist/lite/.claude/hooks/kiss-default-check.sh +129 -0
  18. package/dist/lite/.claude/hooks/multi-tenancy-check.sh +83 -0
  19. package/dist/lite/.claude/hooks/oo-ad-entry-point-check.sh +216 -0
  20. package/dist/lite/.claude/hooks/operator-private-path-check.sh +203 -0
  21. package/dist/lite/.claude/hooks/pattern-annotation-validate.sh +142 -0
  22. package/dist/lite/.claude/hooks/plain-english-steering.sh +15 -1
  23. package/dist/lite/.claude/hooks/post-write-trace.sh +66 -0
  24. package/dist/lite/.claude/hooks/pr-body-scrub-check.sh +446 -0
  25. package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +137 -0
  26. package/dist/lite/.claude/hooks/pre-gh-pr-body-scrub.sh +192 -0
  27. package/dist/lite/.claude/hooks/pre-gh-pr-title-scrub.sh +172 -0
  28. package/dist/lite/.claude/hooks/role-entry-gate.sh +115 -0
  29. package/dist/lite/.claude/hooks/save-state.sh +268 -0
  30. package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +8 -1
  31. package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +52 -0
  32. package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +40 -4
  33. package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +12 -5
  34. package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +18 -2
  35. package/dist/lite/.claude/hooks/session-reflection.d/91-npm-version-drift.sh +54 -0
  36. package/dist/lite/.claude/hooks/session-reflection.d/tests/10-abrupt-stop-recovery.test.sh +95 -0
  37. package/dist/lite/.claude/hooks/session-reflection.sh +62 -15
  38. package/dist/lite/.claude/hooks/tool-failure-diagnosis.sh +50 -0
  39. package/dist/lite/.claude/luminaries/andrej-karpathy.md +165 -0
  40. package/dist/lite/.claude/luminaries/dave-gray.md +155 -0
  41. package/dist/lite/.claude/luminaries/gary-klein.md +166 -0
  42. package/dist/lite/.claude/luminaries/kaoru-ishikawa.md +162 -0
  43. package/dist/lite/.claude/luminaries/lean-product-canvas.md +33 -0
  44. package/dist/lite/.claude/luminaries/luke-wroblewski.md +224 -0
  45. package/dist/lite/.claude/luminaries/ryan-singer.md +113 -0
  46. package/dist/lite/.claude/luminaries/steve-krug.md +168 -0
  47. package/dist/lite/.claude/luminaries/tony-ulwick.md +134 -0
  48. package/dist/lite/.claude/rules/arc-level-orientation-before-leaf.md +60 -0
  49. package/dist/lite/.claude/rules/architect-review-discipline.md +71 -0
  50. package/dist/lite/.claude/rules/auto-fix-criteria.md +126 -0
  51. package/dist/lite/.claude/rules/bassclef-hook-drift-blocker.md +144 -0
  52. package/dist/lite/.claude/rules/bassclef-models-discovery.md +107 -0
  53. package/dist/lite/.claude/rules/bassclef-workflow-parity.md +104 -0
  54. package/dist/lite/.claude/rules/cost-governance.md +154 -0
  55. package/dist/lite/.claude/rules/file-rename-discipline.md +95 -0
  56. package/dist/lite/.claude/rules/mechanism-fidelity.md +88 -0
  57. package/dist/lite/.claude/rules/prototype-production-coupling.md +125 -0
  58. package/dist/lite/.claude/rules/quick-fix-criteria.md +90 -0
  59. package/dist/lite/.claude/rules/session-artifacts.md +30 -0
  60. package/dist/lite/.claude/rules/sibling-smoke-after-substrate-change.md +107 -0
  61. package/dist/lite/.claude/rules/substrate-as-system.md +67 -0
  62. package/dist/lite/.claude/rules/usability.md +22 -0
  63. package/dist/lite/.claude/settings.json +20 -0
  64. package/dist/lite/.claude/skills/agent-research-spawn/SKILL.md +100 -0
  65. package/dist/lite/.claude/skills/architect-review/SKILL.md +201 -0
  66. package/dist/lite/.claude/skills/auto-fix/SKILL.md +297 -0
  67. package/dist/lite/.claude/skills/autonomous/SKILL.md +300 -0
  68. package/dist/lite/.claude/skills/bind-subdomain/SKILL.md +232 -0
  69. package/dist/lite/.claude/skills/canvas/SKILL.md +41 -0
  70. package/dist/lite/.claude/skills/deploy-prod/SKILL.md +106 -0
  71. package/dist/lite/.claude/skills/docs-sync/SKILL.md +188 -0
  72. package/dist/lite/.claude/skills/eisenhower/SKILL.md +273 -0
  73. package/dist/lite/.claude/skills/empathy-map/SKILL.md +168 -0
  74. package/dist/lite/.claude/skills/frontend-design/SKILL.md +53 -0
  75. package/dist/lite/.claude/skills/interview-me/SKILL.md +171 -0
  76. package/dist/lite/.claude/skills/journal-export/SKILL.md +293 -0
  77. package/dist/lite/.claude/skills/jtbd-tasks/SKILL.md +203 -0
  78. package/dist/lite/.claude/skills/objectory-decompose/SKILL.md +256 -0
  79. package/dist/lite/.claude/skills/pick-luminaries/SKILL.md +100 -0
  80. package/dist/lite/.claude/skills/pre-mortem/SKILL.md +180 -0
  81. package/dist/lite/.claude/skills/prototype-gallery/SKILL.md +264 -0
  82. package/dist/lite/.claude/skills/provision-deploy-host/SKILL.md +257 -0
  83. package/dist/lite/.claude/skills/quick-fix/SKILL.md +273 -0
  84. package/dist/lite/.claude/skills/release/SKILL.md +311 -0
  85. package/dist/lite/.claude/skills/release-notes/SKILL.md +202 -0
  86. package/dist/lite/.claude/skills/rfc/SKILL.md +222 -0
  87. package/dist/lite/.claude/skills/riff-prototypes/SKILL.md +637 -0
  88. package/dist/lite/.claude/skills/secrets-bootstrap/SKILL.md +209 -0
  89. package/dist/lite/.claude/skills/skill/SKILL.md +304 -0
  90. package/dist/lite/.claude/skills/smoke-test/SKILL.md +100 -0
  91. package/dist/lite/.claude/skills/state/SKILL.md +204 -0
  92. package/dist/lite/.claude/skills/substrate-check/SKILL.md +218 -0
  93. package/dist/lite/.claude/skills/synthetic-user/SKILL.md +381 -0
  94. package/dist/lite/.claude/skills/task-scenarios/SKILL.md +105 -0
  95. package/dist/lite/.claude/skills/tier-dependency-audit/SKILL.md +139 -0
  96. package/dist/lite/.claude/skills/ux-migration/SKILL.md +424 -0
  97. package/dist/lite/.claude/skills/value-prop-canvas/SKILL.md +297 -0
  98. package/dist/lite/README.md +5 -1
  99. package/dist/lite/gitignore +6 -0
  100. package/dist/lite/lib/bassclef-dir-resolver.sh +85 -0
  101. package/dist/lite/lib/clone-failure-classifier.sh +59 -0
  102. package/dist/lite/lib/fixture-builder.sh +190 -0
  103. package/dist/lite/lib/fresh-install-check.sh +49 -0
  104. package/dist/lite/lib/hook-heartbeat.sh +0 -1
  105. package/dist/lite/lib/mechanism-fidelity.sh +569 -0
  106. package/dist/lite/lib/npm-version-check.sh +112 -0
  107. package/dist/lite/lib/preview-state.sh +229 -0
  108. package/dist/lite/lib/session-rescue.sh +0 -1
  109. package/dist/lite/scripts/generate-lite-manifest.sh +145 -4
  110. package/dist/lite/scripts/lite-manifest-drift-check.sh +5 -1
  111. package/dist/lite/standards/architect-review-discipline.md +222 -0
  112. package/dist/lite/standards/bassclef-platform.md +249 -0
  113. package/dist/lite/standards/bassclef-test-repos.md +97 -0
  114. package/dist/lite/standards/bassclef-wiring-manifest.json +25 -1
  115. package/dist/lite/standards/cold-adopter-harness.md +205 -0
  116. package/dist/lite/standards/data-management.md +135 -0
  117. package/dist/lite/standards/frontend-stack/nextjs-prisma.md +124 -0
  118. package/dist/lite/standards/lite-manifest-schema-changes.md +27 -0
  119. package/dist/lite/standards/lite-manifest.json +1456 -14
  120. package/dist/lite/standards/mechanism-fidelity.md +205 -0
  121. package/dist/lite/standards/operator-private-discipline.md +200 -0
  122. package/dist/lite/standards/operator-to-public-flow.md +195 -0
  123. package/dist/lite/standards/prototype-gallery.md +251 -0
  124. package/dist/lite/standards/pseudonym-key-schema.md +109 -0
  125. package/dist/lite/standards/sibling-validation-checklist.md +181 -0
  126. package/dist/lite/standards/slack-notifications.md +161 -0
  127. package/dist/lite/standards/synthetic-user-testing.md +173 -0
  128. package/dist/lite/standards/vendor-bindings.md +165 -0
  129. package/dist/lite/standards/vendor-plugins/frontend-design.md +66 -0
  130. package/dist/lite/standards/vpc-fit-validation.md +167 -0
  131. package/dist/lite/templates/gallery-yml.md +113 -0
  132. package/dist/lite/templates/product-canvas.md +169 -0
  133. package/dist/lite/templates/value-prop-canvas.md +180 -0
  134. package/package.json +12 -5
  135. /package/dist/lite/{whereami.md → docs/whereami.md} +0 -0
package/dist/cli.cjs CHANGED
@@ -313,14 +313,21 @@ function substrateConfigMdTemplate(pkgVersion) {
313
313
  ].join("\n");
314
314
  }
315
315
  const MANIFEST_SCHEMA_VERSION = "0.1.0";
316
- const MANIFEST_SHAPE_VERSION = 2;
316
+ const MANIFEST_SHAPE_VERSION = 3;
317
+ const GENERATED_BY = "@thebassclef/lite";
317
318
  function manifestTemplate(input) {
318
319
  const value = {
319
320
  schema_version: MANIFEST_SHAPE_VERSION,
321
+ // Top-level `version` — read by the v0.45.0 npm-version-drift hook
322
+ // (fragment 91 under session-reflection) via lib/npm-version-check.sh
323
+ // `jq -r '.version // ""' "$manifest"`. Duplicates
324
+ // $bassclef.generated_by_version on purpose — the hook does not walk
325
+ // into $bassclef. Per bassclef-upstream#1749.
326
+ version: input.pkgVersion,
320
327
  $bassclef: {
321
328
  template: "init.manifest.json",
322
329
  manifest_schema_version: MANIFEST_SCHEMA_VERSION,
323
- generated_by: "@thebassclef/core",
330
+ generated_by: GENERATED_BY,
324
331
  generated_by_version: input.pkgVersion
325
332
  },
326
333
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -388,7 +395,7 @@ function readManifest(targetDir) {
388
395
  if (compareSchemaVersion(schema, MANIFEST_SCHEMA_VERSION) > 0) {
389
396
  throw new ManifestReadError(
390
397
  "SchemaTooNew",
391
- `manifest schema version ${schema} is newer than this package understands (${MANIFEST_SCHEMA_VERSION}). Upgrade @thebassclef/core.`
398
+ `manifest schema version ${schema} is newer than this package understands (${MANIFEST_SCHEMA_VERSION}). Upgrade @thebassclef/lite.`
392
399
  );
393
400
  }
394
401
  const obj = parsed;
@@ -541,7 +548,10 @@ function copySubstrate(targetDir, options = {}) {
541
548
  hookCount: 0,
542
549
  wiringVersion: manifest.version
543
550
  };
544
- if (options.dryRun) result.wouldCopy = [];
551
+ if (options.dryRun) {
552
+ result.wouldCopy = [];
553
+ result.wouldCopyEntries = [];
554
+ }
545
555
  const files = walkDistTree(bundleRoot);
546
556
  const groups = groupByTopDirectory(files);
547
557
  const bundleHookRelPaths = new Set(files.filter(isHookFile));
@@ -697,6 +707,11 @@ function copyOne(relPath, bundleRoot, options, result, scopeDecision) {
697
707
  const outputContent = options.transform ? options.transform(adopterRelPath, content) : content;
698
708
  if (options.dryRun) {
699
709
  result.wouldCopy?.push(adopterRelPath);
710
+ result.wouldCopyEntries?.push({
711
+ path: adopterRelPath,
712
+ scope,
713
+ content_hash_sha256: hashContent(outputContent)
714
+ });
700
715
  return "wouldCopy";
701
716
  }
702
717
  try {
@@ -706,7 +721,11 @@ function copyOne(relPath, bundleRoot, options, result, scopeDecision) {
706
721
  setExecutable(targetPath);
707
722
  }
708
723
  result.copied.push(adopterRelPath);
709
- result.copiedEntries.push({ path: adopterRelPath, scope });
724
+ result.copiedEntries.push({
725
+ path: adopterRelPath,
726
+ scope,
727
+ content_hash_sha256: hashContent(outputContent)
728
+ });
710
729
  return "copied";
711
730
  } catch (e) {
712
731
  if (e instanceof WriteError) {
@@ -783,7 +802,92 @@ function dirExists(p) {
783
802
  return false;
784
803
  }
785
804
  }
805
+ const FAMILIES = [
806
+ "skills",
807
+ "rules",
808
+ "agents",
809
+ "luminaries",
810
+ "hooks",
811
+ "libs",
812
+ "adrs",
813
+ "standards",
814
+ "templates",
815
+ "presence-templates",
816
+ "scripts",
817
+ "root-docs",
818
+ "other"
819
+ ];
820
+ const UNDER_CLAUDE = `${CLAUDE_TARGET_ROOT}/`;
821
+ const PREFIX_RULES = [
822
+ [`${UNDER_CLAUDE}skills/`, "skills"],
823
+ [`${UNDER_CLAUDE}rules/`, "rules"],
824
+ [`${UNDER_CLAUDE}agents/`, "agents"],
825
+ [`${UNDER_CLAUDE}luminaries/`, "luminaries"],
826
+ [`${UNDER_CLAUDE}hooks/`, "hooks"],
827
+ ["lib/", "libs"],
828
+ ["architecture/decisions/", "adrs"],
829
+ ["standards/", "standards"],
830
+ ["templates/", "templates"],
831
+ ["presence/install/", "presence-templates"],
832
+ ["scripts/", "scripts"]
833
+ ];
834
+ function classifyEntry(path) {
835
+ for (const [prefix, family] of PREFIX_RULES) {
836
+ if (path.startsWith(prefix)) return family;
837
+ }
838
+ if (!path.includes("/") && /^[A-Z]/.test(path)) return "root-docs";
839
+ return "other";
840
+ }
841
+ function emptyCatalogCounts() {
842
+ const out = {};
843
+ for (const f of FAMILIES) out[f] = 0;
844
+ return out;
845
+ }
846
+ function buildInitReport(input) {
847
+ const catalog = emptyCatalogCounts();
848
+ let project = 0;
849
+ let user = 0;
850
+ for (const e of input.entries) {
851
+ catalog[classifyEntry(e.path)] += 1;
852
+ if (e.scope === "user") user += 1;
853
+ else project += 1;
854
+ }
855
+ const refused = input.refused.length;
856
+ const errored = input.errored.length;
857
+ const written = input.configs.length + input.entries.length;
858
+ return {
859
+ schema_version: 3,
860
+ tier: input.tier,
861
+ totals: {
862
+ files: written + refused + errored,
863
+ written,
864
+ project,
865
+ user,
866
+ configs: input.configs.length
867
+ },
868
+ catalog,
869
+ hooks: {
870
+ declared: input.hookCount,
871
+ copied: input.declaredHooksCopied,
872
+ files: catalog.hooks
873
+ },
874
+ refused,
875
+ errored,
876
+ failed: refused + errored
877
+ };
878
+ }
879
+ function renderJsonReport(report, write) {
880
+ write(JSON.stringify(report) + "\n");
881
+ }
786
882
  const RESOLVED_TIER = "lite";
883
+ const SAY_HUMAN = (s) => {
884
+ process.stdout.write(s);
885
+ };
886
+ const SAY_QUIET = () => {
887
+ };
888
+ function makeSay(json) {
889
+ return json ? SAY_QUIET : SAY_HUMAN;
890
+ }
787
891
  const PLACEHOLDER_FILES = /* @__PURE__ */ new Set([
788
892
  "CLAUDE.md",
789
893
  "docs/whereami.md",
@@ -825,7 +929,7 @@ function runInit(argv) {
825
929
  }
826
930
  throw e;
827
931
  }
828
- const advisoryOutcome = maybeEmitUpgradeAdvisory(targetDir, args.yes);
932
+ const advisoryOutcome = maybeEmitUpgradeAdvisory(targetDir, args.yes, args.json);
829
933
  if (advisoryOutcome === "refused") return 1;
830
934
  const upgradeApproved = advisoryOutcome === "upgrade-approved";
831
935
  if (!args.force && !args.dryRun && !upgradeApproved) {
@@ -847,26 +951,55 @@ function runInit(argv) {
847
951
  templateVersion: SUBSTRATE_CONFIG_TEMPLATE_VERSION
848
952
  }
849
953
  ];
954
+ const say = makeSay(args.json);
850
955
  if (args.dryRun) {
851
- runDryRun$1(plans);
852
- return dispatchSubstrateCopy(targetDir, args.force || upgradeApproved, args.verbose, true, args.allowRoot, args.json);
853
- }
854
- return runReal$1(plans, args.force || upgradeApproved, args.verbose, targetDir, args.allowRoot, args.json);
855
- }
856
- function maybeEmitUpgradeAdvisory(targetDir, yes) {
956
+ runDryRun$1(plans, say);
957
+ const outcome = dispatchSubstrateCopy(
958
+ targetDir,
959
+ args.force || upgradeApproved,
960
+ args.verbose,
961
+ true,
962
+ args.allowRoot,
963
+ say
964
+ );
965
+ if (args.json) {
966
+ const report = buildInitReport({
967
+ entries: outcome.result?.wouldCopyEntries ?? [],
968
+ configs: plans.map((pl) => ({ path: pl.relativePath })),
969
+ refused: outcome.result?.refused ?? [],
970
+ // A dry run still reads every source file, so it can still fail
971
+ // to read one. Hard-coding this empty hid those failures from
972
+ // the JSON (RFC-0005 A-3).
973
+ errored: outcome.result?.errored ?? [],
974
+ hookCount: outcome.result?.hookCount ?? 0,
975
+ declaredHooksCopied: 0,
976
+ tier: RESOLVED_TIER
977
+ });
978
+ renderJsonReport(report, (t) => {
979
+ process.stdout.write(t);
980
+ });
981
+ }
982
+ return outcome.code;
983
+ }
984
+ return runReal$1(plans, args.force || upgradeApproved, args.verbose, targetDir, args.allowRoot, args.json, say);
985
+ }
986
+ function maybeEmitUpgradeAdvisory(targetDir, yes, json) {
987
+ const prompt = json ? (t) => {
988
+ process.stderr.write(t);
989
+ } : SAY_HUMAN;
857
990
  const manifestPath = node_path.join(targetDir, MANIFEST_RELATIVE_PATH);
858
991
  if (!node_fs.existsSync(manifestPath)) return "ok";
859
992
  const version = readManifestShapeVersion(targetDir);
860
993
  if (version !== null && version >= 2) return "ok";
861
- process.stdout.write(
994
+ prompt(
862
995
  `bassclef init: cli 1.0.1 introduces user-scope hook installation at ~/${HOOKS_SUBPATH}. cli 1.0.0 did not write there.
863
996
  `
864
997
  );
865
998
  if (yes) return "upgrade-approved";
866
- process.stdout.write("bassclef init: continue? (y/N) ");
999
+ prompt("bassclef init: continue? (y/N) ");
867
1000
  const answer = readOneLineFromStdin();
868
1001
  if (answer === "" || answer === "y" || answer === "Y") return "upgrade-approved";
869
- process.stdout.write("bassclef init: aborted by adopter.\n");
1002
+ prompt("bassclef init: aborted by adopter.\n");
870
1003
  return "refused";
871
1004
  }
872
1005
  function readOneLineFromStdin() {
@@ -878,7 +1011,7 @@ function readOneLineFromStdin() {
878
1011
  return "";
879
1012
  }
880
1013
  }
881
- function dispatchSubstrateCopy(targetDir, force, verbose, dryRun, allowRoot, json) {
1014
+ function dispatchSubstrateCopy(targetDir, force, verbose, dryRun, allowRoot, say) {
882
1015
  const substitute = makePlaceholderTransform(targetDir);
883
1016
  let result;
884
1017
  try {
@@ -892,9 +1025,9 @@ function dispatchSubstrateCopy(targetDir, force, verbose, dryRun, allowRoot, jso
892
1025
  if (e instanceof CopyFailure) {
893
1026
  process.stderr.write(`bassclef init: ${e.message}
894
1027
  `);
895
- if (e.kind === "ManifestMissing") return 4;
896
- if (e.kind === "SchemaIncompatible") return 5;
897
- return 2;
1028
+ if (e.kind === "ManifestMissing") return { code: 4 };
1029
+ if (e.kind === "SchemaIncompatible") return { code: 5 };
1030
+ return { code: 2 };
898
1031
  }
899
1032
  throw e;
900
1033
  }
@@ -902,23 +1035,23 @@ function dispatchSubstrateCopy(targetDir, force, verbose, dryRun, allowRoot, jso
902
1035
  const wouldCopy = result.wouldCopy ?? [];
903
1036
  for (const relativePath of wouldCopy) {
904
1037
  const targetPath = node_path.join(targetDir, relativePath);
905
- process.stdout.write(` ${"would create".padEnd(14)} ${targetPath}
1038
+ say(` ${"would create".padEnd(14)} ${targetPath}
906
1039
  `);
907
1040
  }
908
1041
  if (wouldCopy.length > 0) {
909
- process.stdout.write(
1042
+ say(
910
1043
  `bassclef init: ${wouldCopy.length} substrate files would be copied.
911
1044
  `
912
1045
  );
913
1046
  }
914
- process.stdout.write(
1047
+ say(
915
1048
  `bassclef init: ${result.hookCount} hooks armed (${RESOLVED_TIER} tier).
916
1049
  `
917
1050
  );
918
- return 0;
1051
+ return { code: 0, result };
919
1052
  }
920
1053
  if (result.copied.length === 0 && result.refused.length === 0 && result.errored.length === 0) {
921
- return 0;
1054
+ return { code: 0, result };
922
1055
  }
923
1056
  const groupCounts = /* @__PURE__ */ new Map();
924
1057
  for (const path of result.copied) {
@@ -927,17 +1060,17 @@ function dispatchSubstrateCopy(targetDir, force, verbose, dryRun, allowRoot, jso
927
1060
  groupCounts.set(top, (groupCounts.get(top) ?? 0) + 1);
928
1061
  }
929
1062
  for (const [directory, count] of groupCounts) {
930
- process.stdout.write(` ${directory}: ${count} files copied
1063
+ say(` ${directory}: ${count} files copied
931
1064
  `);
932
1065
  }
933
1066
  const parts = [];
934
1067
  if (result.copied.length > 0) parts.push(`${result.copied.length} substrate files copied`);
935
1068
  if (result.refused.length > 0) parts.push(`${result.refused.length} refused`);
936
1069
  if (result.errored.length > 0) parts.push(`${result.errored.length} error(s)`);
937
- process.stdout.write(`bassclef init: ${parts.join(", ")}.
1070
+ say(`bassclef init: ${parts.join(", ")}.
938
1071
  `);
939
1072
  const grandTotal = 1 + result.copied.length;
940
- process.stdout.write(
1073
+ say(
941
1074
  `bassclef init: ${grandTotal} files total (1 config + ${result.copied.length} substrate).
942
1075
  `
943
1076
  );
@@ -949,17 +1082,17 @@ function dispatchSubstrateCopy(targetDir, force, verbose, dryRun, allowRoot, jso
949
1082
  );
950
1083
  const copiedCount = copiedHookEntries.length;
951
1084
  const declaredCount = result.hookCount;
952
- const failedCount = result.refused.length + result.errored.length;
1085
+ const erroredCount = result.errored.length;
953
1086
  const userScope = copiedHookEntries.filter((e) => e.scope === "user").length;
954
1087
  const projectScope = copiedHookEntries.filter((e) => e.scope === "project").length;
955
1088
  const scopeSuffix = userScope + projectScope > 0 ? ` ${projectScope} in <repo>/${HOOKS_SUBPATH.replace(/\/$/, "")}, ${userScope} in ~/${HOOKS_SUBPATH.replace(/\/$/, "")}.` : "";
956
- if (failedCount > 0) {
957
- process.stdout.write(
958
- `bassclef init: Installed ${copiedCount} of ${declaredCount} hooks (${RESOLVED_TIER} tier).${scopeSuffix} ${failedCount} failed — see errors above. Rerun bassclef init to retry.
1089
+ if (erroredCount > 0) {
1090
+ say(
1091
+ `bassclef init: Installed ${copiedCount} of ${declaredCount} hooks (${RESOLVED_TIER} tier).${scopeSuffix} ${erroredCount} failed — see errors above. Rerun bassclef init to retry.
959
1092
  `
960
1093
  );
961
1094
  } else {
962
- process.stdout.write(
1095
+ say(
963
1096
  `bassclef init: Installed ${copiedCount} of ${declaredCount} hooks (${RESOLVED_TIER} tier).${scopeSuffix}
964
1097
  `
965
1098
  );
@@ -1004,7 +1137,7 @@ function dispatchSubstrateCopy(targetDir, force, verbose, dryRun, allowRoot, jso
1004
1137
  catalogCounts.luminaries > 0 ? `${catalogCounts.luminaries} luminaries` : null
1005
1138
  ].filter((s) => s !== null);
1006
1139
  if (claudeCounts.length > 0) {
1007
- process.stdout.write(
1140
+ say(
1008
1141
  `bassclef init: Installed ${claudeCounts.join(", ")} under <repo>/.claude/.
1009
1142
  `
1010
1143
  );
@@ -1019,24 +1152,16 @@ function dispatchSubstrateCopy(targetDir, force, verbose, dryRun, allowRoot, jso
1019
1152
  catalogCounts.scripts > 0 ? `${catalogCounts.scripts} scripts` : null
1020
1153
  ].filter((s) => s !== null);
1021
1154
  if (otherCounts.length > 0) {
1022
- process.stdout.write(
1155
+ say(
1023
1156
  `bassclef init: Installed ${otherCounts.join(", ")} under <repo>/.
1024
1157
  `
1025
1158
  );
1026
1159
  }
1027
- process.stdout.write(
1028
- `bassclef init: ${result.refused.length} files refused (path collision).` + (result.refused.length > 0 ? ` Use --force to overwrite existing files.` : ``) + `
1160
+ if (result.refused.length > 0) {
1161
+ say(
1162
+ `bassclef init: ${result.refused.length} files refused (path collision). Use --force to overwrite existing files.
1029
1163
  `
1030
- );
1031
- if (json) {
1032
- const report = {
1033
- copied: copiedCount,
1034
- declared: declaredCount,
1035
- failed: failedCount,
1036
- scope_counts: { user: userScope, project: projectScope },
1037
- tier: RESOLVED_TIER
1038
- };
1039
- process.stderr.write(JSON.stringify(report) + "\n");
1164
+ );
1040
1165
  }
1041
1166
  if (verbose && result.erroredMessages) {
1042
1167
  for (const msg of result.erroredMessages) {
@@ -1044,7 +1169,7 @@ function dispatchSubstrateCopy(targetDir, force, verbose, dryRun, allowRoot, jso
1044
1169
  `);
1045
1170
  }
1046
1171
  }
1047
- return result.errored.length > 0 ? 2 : 0;
1172
+ return { code: result.errored.length > 0 ? 2 : 0, result };
1048
1173
  }
1049
1174
  function makePlaceholderTransform(targetDir) {
1050
1175
  const repoName = node_path.basename(targetDir);
@@ -1054,7 +1179,7 @@ function makePlaceholderTransform(targetDir) {
1054
1179
  return content.replace(/\[REPO_NAME\]/g, repoName).replace(/\[ISO_TIMESTAMP\]/g, timestamp).replace(/\[TIER\]/g, RESOLVED_TIER).replace(/\[Repo name\]/g, repoName).replace(/<tier>/g, RESOLVED_TIER);
1055
1180
  };
1056
1181
  }
1057
- function runDryRun$1(plans) {
1182
+ function runDryRun$1(plans, say) {
1058
1183
  for (const p of plans) {
1059
1184
  let planned;
1060
1185
  let extra = "";
@@ -1074,12 +1199,12 @@ function runDryRun$1(plans) {
1074
1199
  planned = "would skip";
1075
1200
  }
1076
1201
  }
1077
- process.stdout.write(` ${planned.padEnd(14)} ${p.fullPath}${extra}
1202
+ say(` ${planned.padEnd(14)} ${p.fullPath}${extra}
1078
1203
  `);
1079
1204
  }
1080
1205
  return 0;
1081
1206
  }
1082
- function runReal$1(plans, force, verbose, targetDir, allowRoot, json) {
1207
+ function runReal$1(plans, force, verbose, targetDir, allowRoot, json, say) {
1083
1208
  const results = [];
1084
1209
  let anyRefused = false;
1085
1210
  let anyError = false;
@@ -1127,7 +1252,7 @@ function runReal$1(plans, force, verbose, targetDir, allowRoot, json) {
1127
1252
  if (r.outcome === "error") label = `error (${r.message ?? "unknown"})`;
1128
1253
  else if (r.outcome === "refused") label = "refused";
1129
1254
  else label = r.outcome;
1130
- process.stdout.write(` ${label.padEnd(10)} ${r.plan.fullPath}
1255
+ say(` ${label.padEnd(10)} ${r.plan.fullPath}
1131
1256
  `);
1132
1257
  }
1133
1258
  }
@@ -1149,38 +1274,59 @@ function runReal$1(plans, force, verbose, targetDir, allowRoot, json) {
1149
1274
  return 2;
1150
1275
  }
1151
1276
  if (anyRefused && created > 0) {
1152
- process.stdout.write(
1277
+ say(
1153
1278
  `bassclef init: ${created} config files created, ${unchanged} unchanged. Pass --force to overwrite.
1154
1279
  `
1155
1280
  );
1156
1281
  } else if (created === 0 && unchanged === plans.length) {
1157
- process.stdout.write("bassclef init: already initialized. No changes.\n");
1282
+ say("bassclef init: already initialized. No changes.\n");
1158
1283
  } else {
1159
- process.stdout.write(`bassclef init: ${created} config files created, ${unchanged} unchanged.
1284
+ say(`bassclef init: ${created} config files created, ${unchanged} unchanged.
1160
1285
  `);
1161
1286
  }
1162
- const walkerExit = dispatchSubstrateCopy(targetDir, force, verbose, false, allowRoot, json);
1163
- writeManifest(targetDir, results);
1164
- if (walkerExit === 0) {
1165
- process.stdout.write(
1287
+ const walker = dispatchSubstrateCopy(targetDir, force, verbose, false, allowRoot, say);
1288
+ const declaredLeaves = readDeclaredCommandLeaves(
1289
+ node_path.join(targetDir, ".claude", "settings.json")
1290
+ );
1291
+ const declaredHooksCopied = (walker.result?.copiedEntries ?? []).filter(
1292
+ (e) => e.path.startsWith(HOOKS_SUBPATH) && e.path.endsWith(".sh") && declaredLeaves.has(node_path.basename(e.path))
1293
+ ).length;
1294
+ const report = buildInitReport({
1295
+ entries: walker.result?.copiedEntries ?? [],
1296
+ configs: results.map((r) => ({ path: r.plan.relativePath })),
1297
+ refused: walker.result?.refused ?? [],
1298
+ errored: walker.result?.errored ?? [],
1299
+ hookCount: walker.result?.hookCount ?? 0,
1300
+ declaredHooksCopied,
1301
+ tier: RESOLVED_TIER
1302
+ });
1303
+ writeManifest(targetDir, results, walker.result);
1304
+ if (walker.code === 0) {
1305
+ say(
1166
1306
  `bassclef init: your substrate lives under .claude/. Add .claude/ to .gitignore if you have not.
1167
1307
  `
1168
1308
  );
1169
1309
  }
1170
- return walkerExit;
1310
+ if (json) {
1311
+ renderJsonReport(report, (t) => {
1312
+ process.stdout.write(t);
1313
+ });
1314
+ }
1315
+ return walker.code;
1171
1316
  }
1172
1317
  function shouldRefuseRoot(currentUid, allowRoot) {
1173
1318
  if (currentUid === void 0) return false;
1174
1319
  if (currentUid !== 0) return false;
1175
1320
  return !allowRoot;
1176
1321
  }
1177
- function writeManifest(targetDir, results) {
1322
+ function writeManifest(targetDir, results, walkerResult) {
1178
1323
  const entries = results.map((r) => {
1179
1324
  const entry = {
1180
1325
  path: r.plan.relativePath,
1181
1326
  template: r.plan.templateName,
1182
1327
  template_version: r.plan.templateVersion,
1183
- outcome: r.outcome
1328
+ outcome: r.outcome,
1329
+ source: "config-composer"
1184
1330
  };
1185
1331
  if (r.outcome === "created") {
1186
1332
  entry.content_hash_sha256 = hashContent(r.plan.content);
@@ -1188,6 +1334,38 @@ function writeManifest(targetDir, results) {
1188
1334
  }
1189
1335
  return entry;
1190
1336
  });
1337
+ if (walkerResult) {
1338
+ for (const copied of walkerResult.copiedEntries) {
1339
+ entries.push({
1340
+ path: copied.path,
1341
+ template: `bundle:${copied.path}`,
1342
+ template_version: index.version,
1343
+ outcome: "created",
1344
+ source: "bundle",
1345
+ scope: copied.scope,
1346
+ ...copied.content_hash_sha256 ? { content_hash_sha256: copied.content_hash_sha256 } : {},
1347
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1348
+ });
1349
+ }
1350
+ for (const path of walkerResult.refused) {
1351
+ entries.push({
1352
+ path,
1353
+ template: `bundle:${path}`,
1354
+ template_version: index.version,
1355
+ outcome: "refused",
1356
+ source: "bundle"
1357
+ });
1358
+ }
1359
+ for (const path of walkerResult.errored) {
1360
+ entries.push({
1361
+ path,
1362
+ template: `bundle:${path}`,
1363
+ template_version: index.version,
1364
+ outcome: "error",
1365
+ source: "bundle"
1366
+ });
1367
+ }
1368
+ }
1191
1369
  const manifestDir = node_path.join(targetDir, ".bassclef");
1192
1370
  const manifestPath = node_path.join(manifestDir, "init.manifest.json");
1193
1371
  const content = manifestTemplate({
@@ -1198,7 +1376,12 @@ function writeManifest(targetDir, results) {
1198
1376
  try {
1199
1377
  mkdirSafely(manifestDir);
1200
1378
  writeSafely(manifestPath, content, { force: true });
1201
- } catch {
1379
+ } catch (e) {
1380
+ const reason = e instanceof Error ? e.message : String(e);
1381
+ process.stderr.write(
1382
+ `bassclef init: could not write ${manifestPath} (${reason}). The files were written. Run \`bassclef init --force\` to rebuild the record.
1383
+ `
1384
+ );
1202
1385
  }
1203
1386
  }
1204
1387
  function usage$2() {
@@ -1649,7 +1832,10 @@ function emitL2Output(decisions, verbose) {
1649
1832
  }
1650
1833
  }
1651
1834
  function updateManifestEntry(manifest, path, patch) {
1652
- const idx = manifest.files.findIndex((f) => f.path === path);
1835
+ const wantScope = patch.scope;
1836
+ const idx = manifest.files.findIndex(
1837
+ (f) => f.path === path && (wantScope === void 0 || f.scope === wantScope)
1838
+ );
1653
1839
  if (idx < 0) {
1654
1840
  manifest.files.push({
1655
1841
  path,
@@ -1833,6 +2019,12 @@ Proceed?`,
1833
2019
  const errored = copyResult.errored;
1834
2020
  const newManifest = {
1835
2021
  schema_version: MANIFEST_SHAPE_VERSION,
2022
+ // Top-level `.version` — read by the v0.45.0 npm-version-drift hook
2023
+ // (fragment 91 under session-reflection) via lib/npm-version-check.sh
2024
+ // `jq -r '.version // ""' "$manifest"`. Duplicates
2025
+ // $bassclef.generated_by_version on purpose — the hook does not walk
2026
+ // into $bassclef. Per bassclef-upstream#1749; bassclef-cli#129.
2027
+ version: index.version,
1836
2028
  $bassclef: {
1837
2029
  template: "init.manifest.json",
1838
2030
  manifest_schema_version: "0.1.0",
@@ -2000,6 +2192,9 @@ Run \`bassclef migrate --help\` for migrate options + the upgrade paths.
2000
2192
  Version: ${index.version}
2001
2193
  Docs: https://github.com/sunj-labs/bassclef-cli
2002
2194
  `;
2195
+ for (const stream of [process.stdout, process.stderr]) {
2196
+ stream._handle?.setBlocking?.(true);
2197
+ }
2003
2198
  async function main(argv) {
2004
2199
  const first = argv[0];
2005
2200
  if (first === void 0 || first === "--help" || first === "-h" || first === "help") {