@thebassclef/lite 1.0.0 → 1.0.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 (32) hide show
  1. package/dist/cli.cjs +240 -23
  2. package/dist/cli.js +242 -25
  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/hooks/artifact-ingestion-gate.sh +357 -0
  7. package/dist/lite/.claude/hooks/assert-verify-steering.sh +77 -0
  8. package/dist/lite/.claude/hooks/bassclef-source-config-validate.sh +215 -0
  9. package/dist/lite/.claude/hooks/bassclef-sync.sh +634 -0
  10. package/dist/lite/.claude/hooks/compound-noun-scrub.sh +292 -0
  11. package/dist/lite/.claude/hooks/kiss-expansion-inject.sh +69 -0
  12. package/dist/lite/.claude/hooks/longrun-prep-compounding-sequence-check.sh +492 -0
  13. package/dist/lite/.claude/hooks/plain-english-steering.sh +156 -0
  14. package/dist/lite/.claude/hooks/post-skill-friction-check.sh +177 -0
  15. package/dist/lite/.claude/hooks/post-skill-telemetry.sh +62 -0
  16. package/dist/lite/.claude/hooks/pre-build-gate.sh +511 -0
  17. package/dist/lite/.claude/hooks/pre-commit-gate.sh +451 -0
  18. package/dist/lite/.claude/hooks/session-end.sh +433 -0
  19. package/dist/lite/.claude/hooks/session-reflection.sh +303 -0
  20. package/dist/lite/.claude/hooks/skill-body-grade-gate.sh +219 -0
  21. package/dist/lite/.claude/hooks/skill-body-intent-drift.sh +107 -0
  22. package/dist/lite/.claude/hooks/state-validate.sh +271 -0
  23. package/dist/lite/.claude/hooks/substrate-clarity-gate.sh +1110 -0
  24. package/dist/lite/.claude/hooks/temperance-gate.sh +147 -0
  25. package/dist/lite/.claude/hooks/testing-tier-enforce.sh +233 -0
  26. package/dist/lite/.claude/hooks/turn-prose-grade-measure.sh +219 -0
  27. package/dist/lite/.claude/hooks/turn-prose-kiss-check.sh +463 -0
  28. package/dist/lite/.claude/hooks/vocabulary-migration-check.sh +171 -0
  29. package/dist/lite/.claude/hooks/whereami-utc-gate.sh +142 -0
  30. package/dist/lite/CLAUDE.md +2 -2
  31. package/dist/lite/whereami.md +1 -1
  32. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -36,7 +36,9 @@ const DEFAULTS$2 = {
36
36
  verbose: false,
37
37
  allowRoot: false,
38
38
  allowAnyDir: false,
39
- dir: void 0
39
+ dir: void 0,
40
+ yes: false,
41
+ json: false
40
42
  };
41
43
  let ArgvError$1 = class ArgvError extends Error {
42
44
  name = "ArgvError";
@@ -71,6 +73,16 @@ function parseInitArgs(argv) {
71
73
  i += 1;
72
74
  continue;
73
75
  }
76
+ if (token === "--yes") {
77
+ out.yes = true;
78
+ i += 1;
79
+ continue;
80
+ }
81
+ if (token === "--json") {
82
+ out.json = true;
83
+ i += 1;
84
+ continue;
85
+ }
74
86
  if (token === "--dir") {
75
87
  const value = argv[i + 1];
76
88
  if (value === void 0 || value.startsWith("--")) {
@@ -139,6 +151,13 @@ function isUnder(child, parent) {
139
151
  if (rel === par) return true;
140
152
  return rel.startsWith(par + "/");
141
153
  }
154
+ function readlinkOrNull(path) {
155
+ try {
156
+ return node_fs.readlinkSync(path);
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
142
161
  class WriteError extends Error {
143
162
  name = "WriteError";
144
163
  kind;
@@ -172,7 +191,12 @@ function writeSafely(path, content, opts = {}) {
172
191
  else throw new WriteError("Unknown", `cannot stat ${path}: ${err.code ?? "unknown"}`);
173
192
  }
174
193
  if (existing === "symlink") {
175
- throw new WriteError("SymlinkRefused", `refusing to follow symlink at target path: ${path}`);
194
+ const target = readlinkOrNull(path);
195
+ const targetClause = target ? ` (points to: ${target})` : "";
196
+ throw new WriteError(
197
+ "SymlinkRefused",
198
+ `refusing to follow symlink at target path: ${path}${targetClause}. Delete or move the symlink and rerun bassclef init.`
199
+ );
176
200
  }
177
201
  if (existing !== "none" && !opts.force) {
178
202
  throw new WriteError("AlreadyExists", `file already exists (pass --force to overwrite): ${path}`);
@@ -289,8 +313,10 @@ function substrateConfigMdTemplate(pkgVersion) {
289
313
  ].join("\n");
290
314
  }
291
315
  const MANIFEST_SCHEMA_VERSION = "0.1.0";
316
+ const MANIFEST_SHAPE_VERSION = 2;
292
317
  function manifestTemplate(input) {
293
318
  const value = {
319
+ schema_version: MANIFEST_SHAPE_VERSION,
294
320
  $bassclef: {
295
321
  template: "init.manifest.json",
296
322
  manifest_schema_version: MANIFEST_SCHEMA_VERSION,
@@ -312,6 +338,15 @@ class ManifestReadError extends Error {
312
338
  this.kind = kind;
313
339
  }
314
340
  }
341
+ function readManifestShapeVersion(targetDir) {
342
+ try {
343
+ const parsed = readManifest(targetDir);
344
+ if (typeof parsed.schema_version === "number") return parsed.schema_version;
345
+ return null;
346
+ } catch {
347
+ return null;
348
+ }
349
+ }
315
350
  function readManifest(targetDir) {
316
351
  const path = node_path.join(targetDir, MANIFEST_RELATIVE_PATH);
317
352
  let raw;
@@ -392,8 +427,6 @@ async function computeConfigHashes(targetDir, paths) {
392
427
  }
393
428
  return out;
394
429
  }
395
- const EXPECTED_WIRING_SCHEMA_MAJOR = 2;
396
- const WIRING_MANIFEST_RELATIVE = "standards/bassclef-wiring-manifest.json";
397
430
  class CopyFailure extends Error {
398
431
  constructor(kind, message) {
399
432
  super(message);
@@ -401,11 +434,106 @@ class CopyFailure extends Error {
401
434
  this.name = "CopyFailure";
402
435
  }
403
436
  }
437
+ function resolveHome(opts) {
438
+ const raw = process.env.HOME;
439
+ if (raw === void 0 || raw === "") {
440
+ throw new CopyFailure(
441
+ "EnvironmentIncomplete",
442
+ "HOME environment variable is unset. bassclef init needs HOME set to resolve user-scope hook targets. Set HOME to your home directory and rerun."
443
+ );
444
+ }
445
+ if (raw === "/root" && !opts.allowRoot) {
446
+ throw new CopyFailure(
447
+ "SudoBypassRefused",
448
+ "HOME resolves to /root — sudo bypass detected. Routing user-scope hooks to /root breaks the adopter maintenance model. Run bassclef init without sudo, or pass --allow-root if this is intentional."
449
+ );
450
+ }
451
+ const canonical = raw === node_os.homedir() ? node_os.homedir() : raw;
452
+ try {
453
+ return node_fs.realpathSync(canonical);
454
+ } catch {
455
+ return canonical.endsWith("/") ? canonical.slice(0, -1) : canonical;
456
+ }
457
+ }
458
+ const PREFIX_HOME = "$HOME/";
459
+ const PREFIX_PROJECT = "$CLAUDE_PROJECT_DIR/";
460
+ function classify$1(hook, opts) {
461
+ const cmd = hook.command;
462
+ if (typeof cmd !== "string" || cmd.length === 0) {
463
+ throw new CopyFailure(
464
+ "UnknownScopePrefix",
465
+ "Hook command is empty. settings.json must supply a non-empty command string."
466
+ );
467
+ }
468
+ if (cmd.startsWith(PREFIX_HOME)) {
469
+ return classifyUser(cmd.slice(PREFIX_HOME.length), opts);
470
+ }
471
+ if (cmd.startsWith(PREFIX_PROJECT)) {
472
+ return classifyProject(cmd.slice(PREFIX_PROJECT.length), opts);
473
+ }
474
+ throw new CopyFailure(
475
+ "UnknownScopePrefix",
476
+ `Hook command "${cmd}" uses an unknown scope prefix. cli 1.0.1 handles "$HOME/..." and "$CLAUDE_PROJECT_DIR/..." only. Upgrade cli to a version that supports this prefix.`
477
+ );
478
+ }
479
+ function classifyUser(relPath, opts) {
480
+ const home = resolveHome({ allowRoot: opts.allowRoot });
481
+ const cleaned = normalizeRel(relPath);
482
+ const targetPath = node_path.normalize(node_path.join(home, cleaned));
483
+ assertContained(targetPath, home, opts.allowRoot ? "/root" : null);
484
+ return { scope: "user", targetPath };
485
+ }
486
+ function classifyProject(relPath, opts) {
487
+ const cleaned = normalizeRel(relPath);
488
+ const targetPath = node_path.normalize(node_path.join(opts.targetDir, cleaned));
489
+ assertContained(targetPath, opts.targetDir);
490
+ return { scope: "project", targetPath };
491
+ }
492
+ function normalizeRel(rel) {
493
+ return rel.replace(/\/{2,}/g, "/");
494
+ }
495
+ function assertContained(target, root, altRoot) {
496
+ const resolvedTarget = node_path.resolve(target);
497
+ const resolvedRoot = node_path.resolve(root);
498
+ if (resolvedTarget === resolvedRoot) return;
499
+ if (resolvedTarget.startsWith(resolvedRoot + "/")) return;
500
+ if (altRoot) {
501
+ const resolvedAlt = node_path.resolve(altRoot);
502
+ if (resolvedTarget === resolvedAlt) return;
503
+ if (resolvedTarget.startsWith(resolvedAlt + "/")) return;
504
+ }
505
+ throw new CopyFailure(
506
+ "PathTraversalRefused",
507
+ `Hook target "${target}" escapes ${root}. settings.json commands must resolve inside the declared scope root. If this is a bassclef-upstream bundle defect, file at sunj-labs/bassclef-upstream.`
508
+ );
509
+ }
510
+ const HOOK_EXECUTABLE_MODE = 493;
511
+ function setExecutable(targetPath) {
512
+ if (process.platform === "win32") {
513
+ process.stderr.write(
514
+ `bassclef init: executable bit not applicable on this OS (${targetPath}). Claude Code will still invoke the hook if a POSIX shell layer is present.
515
+ `
516
+ );
517
+ return;
518
+ }
519
+ node_fs.chmodSync(targetPath, HOOK_EXECUTABLE_MODE);
520
+ }
521
+ const HOOKS_SUBPATH = ".claude/hooks/";
522
+ const SETTINGS_SUBPATH = ".claude/settings.json";
523
+ const CONFIG_FILES = [
524
+ ".claude/settings.json",
525
+ "substrate.config.md",
526
+ "substrate.secrets.md"
527
+ ];
528
+ const CURRENT_ENTRY_COUNT = 149;
529
+ const EXPECTED_WIRING_SCHEMA_MAJOR = 2;
530
+ const WIRING_MANIFEST_RELATIVE = "standards/bassclef-wiring-manifest.json";
404
531
  function copySubstrate(targetDir, options = {}) {
405
532
  const bundleRoot = resolveBundleRoot(options.bundleRoot);
406
533
  const manifest = readWiringManifest(bundleRoot);
407
534
  const result = {
408
535
  copied: [],
536
+ copiedEntries: [],
409
537
  refused: [],
410
538
  errored: [],
411
539
  erroredMessages: [],
@@ -415,10 +543,12 @@ function copySubstrate(targetDir, options = {}) {
415
543
  if (options.dryRun) result.wouldCopy = [];
416
544
  const files = walkDistTree(bundleRoot);
417
545
  const groups = groupByTopDirectory(files);
546
+ const bundleHookRelPaths = new Set(files.filter(isHookFile));
547
+ const scopeMap = buildScopeMap(bundleRoot, targetDir, options, bundleHookRelPaths);
418
548
  for (const [directory, groupFiles] of groups) {
419
549
  let completedInGroup = 0;
420
550
  for (const relPath of groupFiles) {
421
- const outcome = copyOne(relPath, bundleRoot, targetDir, options, result);
551
+ const outcome = copyOne(relPath, bundleRoot, targetDir, options, result, scopeMap);
422
552
  if (outcome !== "skipped") completedInGroup += 1;
423
553
  }
424
554
  if (options.onProgress) options.onProgress(directory, completedInGroup);
@@ -497,10 +627,41 @@ function mapAdopterPath(relPath) {
497
627
  if (relPath === GITIGNORE_BUNDLE_NAME) return GITIGNORE_ADOPTER_NAME;
498
628
  return relPath;
499
629
  }
500
- function copyOne(relPath, bundleRoot, targetDir, options, result) {
630
+ function buildScopeMap(bundleRoot, targetDir, options, bundleHookRelPaths) {
631
+ const map = /* @__PURE__ */ new Map();
632
+ const settingsPath = node_path.join(bundleRoot, SETTINGS_SUBPATH);
633
+ if (!fileExists(settingsPath)) return map;
634
+ let parsed;
635
+ try {
636
+ parsed = JSON.parse(node_fs.readFileSync(settingsPath, "utf8"));
637
+ } catch {
638
+ return map;
639
+ }
640
+ const allowRoot = options.allowRoot ?? false;
641
+ for (const eventBlocks of Object.values(parsed.hooks ?? {})) {
642
+ for (const block of eventBlocks) {
643
+ for (const entry of block.hooks ?? []) {
644
+ const cmd = entry.command;
645
+ if (typeof cmd !== "string" || cmd.length === 0) continue;
646
+ if (!cmd.startsWith("$")) continue;
647
+ const decision = classify$1({ command: cmd }, { targetDir, allowRoot });
648
+ const relSource = cmd.replace(/^\$HOME\//, "").replace(/^\$CLAUDE_PROJECT_DIR\//, "").replace(/\/{2,}/g, "/");
649
+ if (!bundleHookRelPaths.has(relSource)) continue;
650
+ map.set(relSource, decision);
651
+ }
652
+ }
653
+ }
654
+ return map;
655
+ }
656
+ function isHookFile(relPath) {
657
+ return relPath.startsWith(HOOKS_SUBPATH) && relPath.endsWith(".sh");
658
+ }
659
+ function copyOne(relPath, bundleRoot, targetDir, options, result, scopeMap) {
501
660
  const sourcePath = node_path.join(bundleRoot, relPath);
502
661
  const adopterRelPath = mapAdopterPath(relPath);
503
- const targetPath = node_path.join(targetDir, adopterRelPath);
662
+ const scopeDecision = isHookFile(relPath) ? scopeMap.get(relPath) : void 0;
663
+ const targetPath = scopeDecision ? scopeDecision.targetPath : node_path.join(targetDir, adopterRelPath);
664
+ const scope = scopeDecision ? scopeDecision.scope : "project";
504
665
  let content;
505
666
  try {
506
667
  content = node_fs.readFileSync(sourcePath, "utf8");
@@ -519,7 +680,11 @@ function copyOne(relPath, bundleRoot, targetDir, options, result) {
519
680
  try {
520
681
  mkdirSafely(node_path.dirname(targetPath));
521
682
  writeSafely(targetPath, outputContent, { force: options.force ?? false });
683
+ if (isHookFile(relPath)) {
684
+ setExecutable(targetPath);
685
+ }
522
686
  result.copied.push(adopterRelPath);
687
+ result.copiedEntries.push({ path: adopterRelPath, scope });
523
688
  return "copied";
524
689
  } catch (e) {
525
690
  if (e instanceof WriteError) {
@@ -529,6 +694,8 @@ function copyOne(relPath, bundleRoot, targetDir, options, result) {
529
694
  }
530
695
  if (e.kind === "SymlinkRefused") {
531
696
  result.refused.push(adopterRelPath);
697
+ process.stderr.write(`bassclef init: ${e.message}
698
+ `);
532
699
  return "refused";
533
700
  }
534
701
  const message = `${adopterRelPath} — write failed (${e.kind}): ${e.message}. Check the target directory exists and is writable, then rerun.`;
@@ -636,7 +803,10 @@ function runInit(argv) {
636
803
  }
637
804
  throw e;
638
805
  }
639
- if (!args.force && !args.dryRun) {
806
+ const advisoryOutcome = maybeEmitUpgradeAdvisory(targetDir, args.yes);
807
+ if (advisoryOutcome === "refused") return 1;
808
+ const upgradeApproved = advisoryOutcome === "upgrade-approved";
809
+ if (!args.force && !args.dryRun && !upgradeApproved) {
640
810
  const manifestPath = node_path.join(targetDir, MANIFEST_RELATIVE_PATH);
641
811
  if (node_fs.existsSync(manifestPath)) {
642
812
  process.stderr.write(
@@ -657,18 +827,44 @@ function runInit(argv) {
657
827
  ];
658
828
  if (args.dryRun) {
659
829
  runDryRun$1(plans);
660
- return dispatchSubstrateCopy(targetDir, args.force, args.verbose, true);
830
+ return dispatchSubstrateCopy(targetDir, args.force || upgradeApproved, args.verbose, true, args.allowRoot, args.json);
831
+ }
832
+ return runReal$1(plans, args.force || upgradeApproved, args.verbose, targetDir, args.allowRoot, args.json);
833
+ }
834
+ function maybeEmitUpgradeAdvisory(targetDir, yes) {
835
+ const manifestPath = node_path.join(targetDir, MANIFEST_RELATIVE_PATH);
836
+ if (!node_fs.existsSync(manifestPath)) return "ok";
837
+ const version = readManifestShapeVersion(targetDir);
838
+ if (version !== null && version >= 2) return "ok";
839
+ process.stdout.write(
840
+ `bassclef init: cli 1.0.1 introduces user-scope hook installation at ~/${HOOKS_SUBPATH}. cli 1.0.0 did not write there.
841
+ `
842
+ );
843
+ if (yes) return "upgrade-approved";
844
+ process.stdout.write("bassclef init: continue? (y/N) ");
845
+ const answer = readOneLineFromStdin();
846
+ if (answer === "" || answer === "y" || answer === "Y") return "upgrade-approved";
847
+ process.stdout.write("bassclef init: aborted by adopter.\n");
848
+ return "refused";
849
+ }
850
+ function readOneLineFromStdin() {
851
+ try {
852
+ const buf = Buffer.alloc(256);
853
+ const n = require("node:fs").readSync(0, buf, 0, 256, null);
854
+ return n > 0 ? buf.slice(0, n).toString("utf8").trim() : "";
855
+ } catch {
856
+ return "";
661
857
  }
662
- return runReal$1(plans, args.force, args.verbose, targetDir);
663
858
  }
664
- function dispatchSubstrateCopy(targetDir, force, verbose, dryRun) {
859
+ function dispatchSubstrateCopy(targetDir, force, verbose, dryRun, allowRoot, json) {
665
860
  const substitute = makePlaceholderTransform(targetDir);
666
861
  let result;
667
862
  try {
668
863
  result = copySubstrate(targetDir, {
669
864
  force,
670
865
  dryRun,
671
- transform: substitute
866
+ transform: substitute,
867
+ allowRoot
672
868
  });
673
869
  } catch (e) {
674
870
  if (e instanceof CopyFailure) {
@@ -723,10 +919,36 @@ function dispatchSubstrateCopy(targetDir, force, verbose, dryRun) {
723
919
  `bassclef init: ${grandTotal} files total (1 config + ${result.copied.length} substrate).
724
920
  `
725
921
  );
726
- process.stdout.write(
727
- `bassclef init: ${result.hookCount} hooks armed (${RESOLVED_TIER} tier).
728
- `
922
+ const copiedHookEntries = result.copiedEntries.filter(
923
+ (e) => e.path.startsWith(HOOKS_SUBPATH) && e.path.endsWith(".sh")
729
924
  );
925
+ const copiedCount = copiedHookEntries.length;
926
+ const declaredCount = result.hookCount;
927
+ const failedCount = result.refused.length + result.errored.length;
928
+ const userScope = copiedHookEntries.filter((e) => e.scope === "user").length;
929
+ const projectScope = copiedHookEntries.filter((e) => e.scope === "project").length;
930
+ const scopeSuffix = userScope + projectScope > 0 ? ` ${projectScope} in <repo>/${HOOKS_SUBPATH.replace(/\/$/, "")}, ${userScope} in ~/${HOOKS_SUBPATH.replace(/\/$/, "")}.` : "";
931
+ if (failedCount > 0) {
932
+ process.stdout.write(
933
+ `bassclef init: Installed ${copiedCount} of ${declaredCount} hooks (${RESOLVED_TIER} tier).${scopeSuffix} ${failedCount} failed — see errors above. Rerun bassclef init to retry.
934
+ `
935
+ );
936
+ } else {
937
+ process.stdout.write(
938
+ `bassclef init: Installed ${copiedCount} of ${declaredCount} hooks (${RESOLVED_TIER} tier).${scopeSuffix}
939
+ `
940
+ );
941
+ }
942
+ if (json) {
943
+ const report = {
944
+ copied: copiedCount,
945
+ declared: declaredCount,
946
+ failed: failedCount,
947
+ scope_counts: { user: userScope, project: projectScope },
948
+ tier: RESOLVED_TIER
949
+ };
950
+ process.stderr.write(JSON.stringify(report) + "\n");
951
+ }
730
952
  if (verbose && result.erroredMessages) {
731
953
  for (const msg of result.erroredMessages) {
732
954
  process.stderr.write(` substrate: ${msg}
@@ -768,7 +990,7 @@ function runDryRun$1(plans) {
768
990
  }
769
991
  return 0;
770
992
  }
771
- function runReal$1(plans, force, verbose, targetDir) {
993
+ function runReal$1(plans, force, verbose, targetDir, allowRoot, json) {
772
994
  const results = [];
773
995
  let anyRefused = false;
774
996
  let anyError = false;
@@ -848,7 +1070,7 @@ function runReal$1(plans, force, verbose, targetDir) {
848
1070
  process.stdout.write(`bassclef init: ${created} config files created, ${unchanged} unchanged.
849
1071
  `);
850
1072
  }
851
- const walkerExit = dispatchSubstrateCopy(targetDir, force, verbose, false);
1073
+ const walkerExit = dispatchSubstrateCopy(targetDir, force, verbose, false, allowRoot, json);
852
1074
  writeManifest(targetDir, results);
853
1075
  if (walkerExit === 0) {
854
1076
  process.stdout.write(
@@ -1452,12 +1674,6 @@ async function confirm(question, opts = {}) {
1452
1674
  rl.close();
1453
1675
  }
1454
1676
  }
1455
- const CONFIG_FILES = [
1456
- ".claude/settings.json",
1457
- "substrate.config.md",
1458
- "substrate.secrets.md"
1459
- ];
1460
- const CURRENT_ENTRY_COUNT = 149;
1461
1677
  async function detectAdopterState(targetDir) {
1462
1678
  let manifest;
1463
1679
  try {
@@ -1507,6 +1723,7 @@ Proceed?`,
1507
1723
  );
1508
1724
  const errored = copyResult.errored;
1509
1725
  const newManifest = {
1726
+ schema_version: MANIFEST_SHAPE_VERSION,
1510
1727
  $bassclef: {
1511
1728
  template: "init.manifest.json",
1512
1729
  manifest_schema_version: "0.1.0",