@skill-map/cli 0.40.0 → 0.41.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.
Files changed (38) hide show
  1. package/dist/cli/tutorial/sm-tutorial/SKILL.md +10 -1
  2. package/dist/cli.js +275 -53
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +22 -5
  5. package/dist/index.js.map +1 -1
  6. package/dist/kernel/index.d.ts +30 -0
  7. package/dist/kernel/index.js +22 -5
  8. package/dist/kernel/index.js.map +1 -1
  9. package/dist/migrations/001_initial.sql +8 -0
  10. package/dist/ui/chunk-5GD2GBPS.js +2190 -0
  11. package/dist/ui/chunk-5WJRN3LD.js +809 -0
  12. package/dist/ui/{chunk-3SI3TVER.js → chunk-C2YUQODZ.js} +2 -2
  13. package/dist/ui/{chunk-NGIFGXW7.js → chunk-CFJBTDAA.js} +1 -1
  14. package/dist/ui/chunk-DQ45L4EV.js +123 -0
  15. package/dist/ui/{chunk-3FPS5OIY.js → chunk-HEJCH7BA.js} +1 -1
  16. package/dist/ui/chunk-HFPA56IM.js +1 -0
  17. package/dist/ui/chunk-HP375T2O.js +2 -0
  18. package/dist/ui/{chunk-WQ42JTZB.js → chunk-IUDL3NDH.js} +1 -1
  19. package/dist/ui/{chunk-ASCTZSBZ.js → chunk-JPYAASHN.js} +1 -1
  20. package/dist/ui/chunk-JWI3M5RV.js +1 -0
  21. package/dist/ui/chunk-XJL4DZ4M.js +1 -0
  22. package/dist/ui/{chunk-GSTYLDCL.js → chunk-XOHD5XWA.js} +1 -1
  23. package/dist/ui/chunk-YL6SWAFJ.js +1024 -0
  24. package/dist/ui/chunk-ZVGHYOKP.js +315 -0
  25. package/dist/ui/index.html +2 -2
  26. package/dist/ui/main-JCF6OBGZ.js +3 -0
  27. package/dist/ui/{styles-2WO3KNOY.css → styles-6H4GSOHY.css} +1 -1
  28. package/migrations/001_initial.sql +8 -0
  29. package/package.json +2 -2
  30. package/dist/ui/chunk-2W6BCESO.js +0 -2589
  31. package/dist/ui/chunk-4KQGLZV2.js +0 -123
  32. package/dist/ui/chunk-MHEKCBIR.js +0 -317
  33. package/dist/ui/chunk-PFA2CVPN.js +0 -61
  34. package/dist/ui/chunk-PURF7B6R.js +0 -411
  35. package/dist/ui/chunk-UFCOGNZX.js +0 -1
  36. package/dist/ui/chunk-XEWWXGNE.js +0 -1
  37. package/dist/ui/chunk-YXFKPYK3.js +0 -965
  38. package/dist/ui/main-WLCA4YLM.js +0 -3
@@ -919,6 +919,11 @@ Tell the tester:
919
919
  > the field you'll edit next, so leave the card open and the
920
920
  > change will be obvious.
921
921
  >
922
+ > ⚠ Heads-up: the inspector header shows a few action buttons
923
+ > (Bump, Close, etc). **Don't click any of them yet** , some of
924
+ > them write files to your project and we cover that flow
925
+ > deliberately in step 13. For now, just look.
926
+ >
922
927
  > Now open `.claude/agents/demo-agent.md` in your editor of
923
928
  > choice. In the **frontmatter** at the top of the file, change
924
929
  > the `description:` field to any text you want, the actual
@@ -1409,9 +1414,13 @@ permission. After you say yes, the choice persists per-checkout
1409
1414
  (gitignored) and the prompt never appears again.
1410
1415
 
1411
1416
  We'll demonstrate by creating an empty annotation scaffold for
1412
- `notes/todo.md`.
1417
+ `notes/todo.md`. **Reset any prior consent state first** so the
1418
+ prompt actually appears (an earlier step may have flipped the flag
1419
+ without you noticing, in which case `sm sidecar annotate` would
1420
+ skip straight past the prompt and the lesson would not land):
1413
1421
 
1414
1422
  ```bash
1423
+ rm -f notes/todo.sm .skill-map/settings.local.json
1415
1424
  sm sidecar annotate notes/todo.md
1416
1425
  ```
1417
1426
 
package/dist/cli.js CHANGED
@@ -3855,7 +3855,7 @@ var UPDATE_CHECK_TEXTS = {
3855
3855
  // package.json
3856
3856
  var package_default = {
3857
3857
  name: "@skill-map/cli",
3858
- version: "0.40.0",
3858
+ version: "0.41.0",
3859
3859
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
3860
3860
  license: "MIT",
3861
3861
  type: "module",
@@ -4287,40 +4287,40 @@ var updateCheckHook = {
4287
4287
  };
4288
4288
 
4289
4289
  // plugins/built-ins.ts
4290
- var claudeProvider2 = { ...claudeProvider, pluginId: "claude", version: "0.40.0" };
4291
- var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "claude", version: "0.40.0" };
4292
- var slashCommandExtractor2 = { ...slashCommandExtractor, pluginId: "claude", version: "0.40.0" };
4293
- var antigravityProvider2 = { ...antigravityProvider, pluginId: "antigravity", version: "0.40.0" };
4294
- var openaiProvider2 = { ...openaiProvider, pluginId: "openai", version: "0.40.0" };
4295
- var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills", version: "0.40.0" };
4296
- var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core", version: "0.40.0" };
4297
- var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core", version: "0.40.0" };
4298
- var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core", version: "0.40.0" };
4299
- var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core", version: "0.40.0" };
4300
- var mcpToolsExtractor2 = { ...mcpToolsExtractor, pluginId: "core", version: "0.40.0" };
4301
- var toolsCounterExtractor2 = { ...toolsCounterExtractor, pluginId: "core", version: "0.40.0" };
4302
- var annotationFieldUnknownAnalyzer2 = { ...annotationFieldUnknownAnalyzer, pluginId: "core", version: "0.40.0" };
4303
- var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core", version: "0.40.0" };
4304
- var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core", version: "0.40.0" };
4305
- var contributionOrphanAnalyzer2 = { ...contributionOrphanAnalyzer, pluginId: "core", version: "0.40.0" };
4306
- var issueCounterAnalyzer2 = { ...issueCounterAnalyzer, pluginId: "core", version: "0.40.0" };
4307
- var jobFileOrphanAnalyzer2 = { ...jobFileOrphanAnalyzer, pluginId: "core", version: "0.40.0" };
4308
- var linkConflictAnalyzer2 = { ...linkConflictAnalyzer, pluginId: "core", version: "0.40.0" };
4309
- var linkCounterAnalyzer2 = { ...linkCounterAnalyzer, pluginId: "core", version: "0.40.0" };
4310
- var linkSelfLoopAnalyzer2 = { ...linkSelfLoopAnalyzer, pluginId: "core", version: "0.40.0" };
4311
- var nameReservedAnalyzer2 = { ...nameReservedAnalyzer, pluginId: "core", version: "0.40.0" };
4312
- var nodeStabilityAnalyzer2 = { ...nodeStabilityAnalyzer, pluginId: "core", version: "0.40.0" };
4313
- var nodeSupersededAnalyzer2 = { ...nodeSupersededAnalyzer, pluginId: "core", version: "0.40.0" };
4314
- var referenceBrokenAnalyzer2 = { ...referenceBrokenAnalyzer, pluginId: "core", version: "0.40.0" };
4315
- var referenceRedundantAnalyzer2 = { ...referenceRedundantAnalyzer, pluginId: "core", version: "0.40.0" };
4316
- var schemaViolationAnalyzer2 = { ...schemaViolationAnalyzer, pluginId: "core", version: "0.40.0" };
4317
- var signalCollisionAnalyzer2 = { ...signalCollisionAnalyzer, pluginId: "core", version: "0.40.0" };
4318
- var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core", version: "0.40.0" };
4319
- var asciiFormatter2 = { ...asciiFormatter, pluginId: "core", version: "0.40.0" };
4320
- var jsonFormatter2 = { ...jsonFormatter, pluginId: "core", version: "0.40.0" };
4321
- var nodeBumpAction2 = { ...nodeBumpAction, pluginId: "core", version: "0.40.0" };
4322
- var nodeSupersedeAction2 = { ...nodeSupersedeAction, pluginId: "core", version: "0.40.0" };
4323
- var updateCheckHook2 = { ...updateCheckHook, pluginId: "core", version: "0.40.0" };
4290
+ var claudeProvider2 = { ...claudeProvider, pluginId: "claude", version: "0.41.0" };
4291
+ var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "claude", version: "0.41.0" };
4292
+ var slashCommandExtractor2 = { ...slashCommandExtractor, pluginId: "claude", version: "0.41.0" };
4293
+ var antigravityProvider2 = { ...antigravityProvider, pluginId: "antigravity", version: "0.41.0" };
4294
+ var openaiProvider2 = { ...openaiProvider, pluginId: "openai", version: "0.41.0" };
4295
+ var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills", version: "0.41.0" };
4296
+ var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core", version: "0.41.0" };
4297
+ var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core", version: "0.41.0" };
4298
+ var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core", version: "0.41.0" };
4299
+ var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core", version: "0.41.0" };
4300
+ var mcpToolsExtractor2 = { ...mcpToolsExtractor, pluginId: "core", version: "0.41.0" };
4301
+ var toolsCounterExtractor2 = { ...toolsCounterExtractor, pluginId: "core", version: "0.41.0" };
4302
+ var annotationFieldUnknownAnalyzer2 = { ...annotationFieldUnknownAnalyzer, pluginId: "core", version: "0.41.0" };
4303
+ var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core", version: "0.41.0" };
4304
+ var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core", version: "0.41.0" };
4305
+ var contributionOrphanAnalyzer2 = { ...contributionOrphanAnalyzer, pluginId: "core", version: "0.41.0" };
4306
+ var issueCounterAnalyzer2 = { ...issueCounterAnalyzer, pluginId: "core", version: "0.41.0" };
4307
+ var jobFileOrphanAnalyzer2 = { ...jobFileOrphanAnalyzer, pluginId: "core", version: "0.41.0" };
4308
+ var linkConflictAnalyzer2 = { ...linkConflictAnalyzer, pluginId: "core", version: "0.41.0" };
4309
+ var linkCounterAnalyzer2 = { ...linkCounterAnalyzer, pluginId: "core", version: "0.41.0" };
4310
+ var linkSelfLoopAnalyzer2 = { ...linkSelfLoopAnalyzer, pluginId: "core", version: "0.41.0" };
4311
+ var nameReservedAnalyzer2 = { ...nameReservedAnalyzer, pluginId: "core", version: "0.41.0" };
4312
+ var nodeStabilityAnalyzer2 = { ...nodeStabilityAnalyzer, pluginId: "core", version: "0.41.0" };
4313
+ var nodeSupersededAnalyzer2 = { ...nodeSupersededAnalyzer, pluginId: "core", version: "0.41.0" };
4314
+ var referenceBrokenAnalyzer2 = { ...referenceBrokenAnalyzer, pluginId: "core", version: "0.41.0" };
4315
+ var referenceRedundantAnalyzer2 = { ...referenceRedundantAnalyzer, pluginId: "core", version: "0.41.0" };
4316
+ var schemaViolationAnalyzer2 = { ...schemaViolationAnalyzer, pluginId: "core", version: "0.41.0" };
4317
+ var signalCollisionAnalyzer2 = { ...signalCollisionAnalyzer, pluginId: "core", version: "0.41.0" };
4318
+ var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core", version: "0.41.0" };
4319
+ var asciiFormatter2 = { ...asciiFormatter, pluginId: "core", version: "0.41.0" };
4320
+ var jsonFormatter2 = { ...jsonFormatter, pluginId: "core", version: "0.41.0" };
4321
+ var nodeBumpAction2 = { ...nodeBumpAction, pluginId: "core", version: "0.41.0" };
4322
+ var nodeSupersedeAction2 = { ...nodeSupersedeAction, pluginId: "core", version: "0.41.0" };
4323
+ var updateCheckHook2 = { ...updateCheckHook, pluginId: "core", version: "0.41.0" };
4324
4324
  var builtInBundles = [
4325
4325
  {
4326
4326
  id: "claude",
@@ -4898,6 +4898,7 @@ var defaults_default = {
4898
4898
  strict: false,
4899
4899
  followSymlinks: false,
4900
4900
  maxFileSizeBytes: 1048576,
4901
+ maxNodes: 256,
4901
4902
  watch: {
4902
4903
  debounceMs: 300
4903
4904
  },
@@ -7037,6 +7038,8 @@ async function loadScanResult(db) {
7037
7038
  roots: parseJsonArray(metaRow.rootsJson),
7038
7039
  providers: parseJsonArray(metaRow.providersJson),
7039
7040
  scannedBy,
7041
+ recommendedNodeLimit: metaRow.recommendedNodeLimit,
7042
+ overrideMaxNodes: metaRow.overrideMaxNodes,
7040
7043
  nodes,
7041
7044
  links,
7042
7045
  issues,
@@ -7060,6 +7063,11 @@ async function loadScanResult(db) {
7060
7063
  scannedAt,
7061
7064
  roots: ["."],
7062
7065
  providers: [],
7066
+ // Synthetic envelope, default to the design cap (256) so SPA reads
7067
+ // the same shape across cold-boot and pre-cap-aware DBs. A real
7068
+ // scan overwrites scan_meta with the live values on next run.
7069
+ recommendedNodeLimit: 256,
7070
+ overrideMaxNodes: null,
7063
7071
  nodes,
7064
7072
  links,
7065
7073
  issues,
@@ -7645,7 +7653,14 @@ function metaToRow(result) {
7645
7653
  providersJson: JSON.stringify(result.providers),
7646
7654
  statsFilesWalked: result.stats.filesWalked,
7647
7655
  statsFilesSkipped: result.stats.filesSkipped,
7648
- statsDurationMs: result.stats.durationMs
7656
+ statsDurationMs: result.stats.durationMs,
7657
+ ...projectNodeLimitColumns(result)
7658
+ };
7659
+ }
7660
+ function projectNodeLimitColumns(result) {
7661
+ return {
7662
+ recommendedNodeLimit: result.recommendedNodeLimit ?? 256,
7663
+ overrideMaxNodes: result.overrideMaxNodes ?? null
7649
7664
  };
7650
7665
  }
7651
7666
  function extractorRunToRow(record) {
@@ -15673,17 +15688,26 @@ async function walkAndExtract(opts) {
15673
15688
  const walkOptions = opts.ignoreFilter ? { ignoreFilter: opts.ignoreFilter } : {};
15674
15689
  let filesWalked = 0;
15675
15690
  let index = 0;
15691
+ const effectiveMaxNodes = opts.overrideMaxNodes ?? opts.recommendedNodeLimit;
15692
+ let capReached = false;
15676
15693
  const activeProviders = opts.providers.filter((provider) => {
15677
15694
  if (!provider.gatedByActiveLens) return true;
15678
15695
  if (opts.activeProvider === null) return true;
15679
15696
  return provider.id === opts.activeProvider;
15680
15697
  });
15681
- for (const provider of activeProviders) {
15698
+ const advance = async (raw, provider) => {
15699
+ const advanced = await processRawNode(raw, provider, wctx, accum, claimedPaths, index + 1);
15700
+ if (advanced) index += 1;
15701
+ };
15702
+ outer: for (const provider of activeProviders) {
15682
15703
  for await (const raw of resolveProviderWalk(provider)(opts.roots, walkOptions)) {
15683
15704
  filesWalked += 1;
15684
15705
  if (claimedPaths.has(raw.path)) continue;
15685
- const advanced = await processRawNode(raw, provider, wctx, accum, claimedPaths, index + 1);
15686
- if (advanced) index += 1;
15706
+ if (accum.nodes.length >= effectiveMaxNodes) {
15707
+ capReached = true;
15708
+ break outer;
15709
+ }
15710
+ await advance(raw, provider);
15687
15711
  }
15688
15712
  }
15689
15713
  const orphanSidecars = discoverOrphanSidecars(opts.roots);
@@ -15694,6 +15718,9 @@ async function walkAndExtract(opts) {
15694
15718
  cachedPaths: accum.cachedPaths,
15695
15719
  frontmatterIssues: accum.frontmatterIssues,
15696
15720
  filesWalked,
15721
+ recommendedNodeLimit: opts.recommendedNodeLimit,
15722
+ overrideMaxNodes: opts.overrideMaxNodes,
15723
+ capReached,
15697
15724
  enrichments: [...accum.enrichmentBuffer.values()],
15698
15725
  extractorRuns: accum.extractorRuns,
15699
15726
  contributions: accum.contributionsBuffer,
@@ -15972,7 +15999,9 @@ async function runScanInternal(_kernel, options) {
15972
15999
  priorExtractorRuns: setup.priorExtractorRuns,
15973
16000
  providerFrontmatter: setup.providerFrontmatter,
15974
16001
  pluginStores: options.pluginStores,
15975
- activeProvider: activeProviderId
16002
+ activeProvider: activeProviderId,
16003
+ recommendedNodeLimit: options.recommendedNodeLimit ?? 256,
16004
+ overrideMaxNodes: options.overrideMaxNodes ?? null
15976
16005
  });
15977
16006
  const activeProvider = activeProviderId ? exts.providers.find((p) => p.id === activeProviderId) ?? null : null;
15978
16007
  const resolved = resolveSignals({
@@ -16140,6 +16169,8 @@ function buildScanReturn(walked, issues, renameOps, stats, options, setup) {
16140
16169
  roots: options.roots,
16141
16170
  providers: setup.exts.providers.map((a) => a.id),
16142
16171
  scannedBy: SCANNED_BY,
16172
+ recommendedNodeLimit: walked.recommendedNodeLimit,
16173
+ overrideMaxNodes: walked.overrideMaxNodes,
16143
16174
  nodes: walked.nodes,
16144
16175
  links: walked.internalLinks,
16145
16176
  issues,
@@ -16830,7 +16861,8 @@ async function runScanForCommand(opts) {
16830
16861
  extensions,
16831
16862
  referenceablePaths,
16832
16863
  ctx.cwd,
16833
- activeProvider
16864
+ activeProvider,
16865
+ cfg.scan.maxNodes
16834
16866
  );
16835
16867
  const willPersist = !opts.noBuiltIns && !opts.dryRun;
16836
16868
  return willPersist ? runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanWith, extensions) : runEphemeralPath(opts, dbPath, strict, loadPrior, runScanWith);
@@ -16931,7 +16963,7 @@ function makePriorLoader(noBuiltIns, strict) {
16931
16963
  return loaded;
16932
16964
  };
16933
16965
  }
16934
- function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, extensions, referenceablePaths, scanCwd, activeProvider) {
16966
+ function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, extensions, referenceablePaths, scanCwd, activeProvider, recommendedNodeLimit) {
16935
16967
  return async (prior, priorExtractorRuns, orphanJobFiles) => {
16936
16968
  if (opts.changed && prior === null) {
16937
16969
  opts.stderr.write(SCAN_RUNNER_TEXTS.changedNoPriorWarning);
@@ -16946,6 +16978,7 @@ function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, exte
16946
16978
  cwd: scanCwd,
16947
16979
  prior,
16948
16980
  activeProvider,
16981
+ recommendedNodeLimit,
16949
16982
  ...priorExtractorRuns ? { priorExtractorRuns } : {},
16950
16983
  ...orphanJobFiles ? { orphanJobFiles } : {}
16951
16984
  });
@@ -16959,15 +16992,15 @@ function buildRunScanOptions(args2) {
16959
16992
  tokenize: !opts.noTokens,
16960
16993
  ignoreFilter: args2.ignoreFilter,
16961
16994
  strict: args2.strict,
16962
- emitter: opts.emitterFactory ? opts.emitterFactory() : createStderrProgressEmitter(opts.stderr, {
16963
- colorEnabled: opts.colorEnabled === true
16964
- }),
16995
+ emitter: buildRunScanEmitter(opts),
16965
16996
  // Orphan job-file detection, empty list means "no orphans
16966
16997
  // visible from this caller" (legacy behaviour). The orchestrator
16967
16998
  // defaults to `[]` when the field is absent; we always pass the
16968
16999
  // array (possibly empty) to keep the wiring uniform.
16969
17000
  orphanJobFiles: orphanJobFiles ?? [],
16970
- activeProvider: args2.activeProvider
17001
+ activeProvider: args2.activeProvider,
17002
+ recommendedNodeLimit: args2.recommendedNodeLimit,
17003
+ overrideMaxNodes: opts.maxNodes ?? null
16971
17004
  };
16972
17005
  if (args2.extensions) runOptions.extensions = args2.extensions;
16973
17006
  if (prior) {
@@ -16979,6 +17012,12 @@ function buildRunScanOptions(args2) {
16979
17012
  runOptions.cwd = args2.cwd;
16980
17013
  return runOptions;
16981
17014
  }
17015
+ function buildRunScanEmitter(opts) {
17016
+ if (opts.emitterFactory) return opts.emitterFactory();
17017
+ return createStderrProgressEmitter(opts.stderr, {
17018
+ colorEnabled: opts.colorEnabled === true
17019
+ });
17020
+ }
16982
17021
  async function runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanWith, extensions) {
16983
17022
  let outcome;
16984
17023
  try {
@@ -21004,6 +21043,22 @@ var SCAN_TEXTS = {
21004
21043
  persistedTo: " {{dbPath}}\n",
21005
21044
  /** Body line for dry-run mode, same indent, marker tail. */
21006
21045
  wouldPersist: " would persist to {{dbPath}} (dry-run)\n",
21046
+ /**
21047
+ * Cap-hit notice, printed when the walker stopped accepting nodes
21048
+ * because `--max-nodes` (or the `scan.maxNodes` setting) was reached.
21049
+ * `{{glyph}}` is the yellow warning glyph, `{{limit}}` the effective
21050
+ * cap, `{{source}}` either `--max-nodes` or `scan.maxNodes`. The hint
21051
+ * names both escape routes the user has: trimming `.skillmapignore`
21052
+ * (preferred) or raising the cap with `--max-nodes <N>`.
21053
+ */
21054
+ scanCappedNotice: "{{glyph}} Scan capped at {{limit}} nodes ({{source}}).\n {{hint}}\n",
21055
+ scanCappedNoticeHint: "Trim .skillmapignore to exclude noisy paths (preferred), or re-run with --max-nodes <N> to raise the cap. Past the recommended limit the graph is hard to read and analyzer signal drops.",
21056
+ /**
21057
+ * Validation message for an invalid `--max-nodes` value. Surfaced as a
21058
+ * §3.1b two-line block.
21059
+ */
21060
+ maxNodesInvalid: "{{glyph}} --max-nodes must be an integer >= 1 (got `{{value}}`).\n {{hint}}\n",
21061
+ maxNodesInvalidHint: "Pass a positive integer, e.g. --max-nodes 256.",
21007
21062
  // --- scan compare-with sub-verb --------------------------------------
21008
21063
  compareErrorPrefix: "sm scan compare-with: {{message}}\n",
21009
21064
  compareDumpNotFound: "dump file not found: {{path}}",
@@ -21167,7 +21222,9 @@ function createWatcherRuntime(opts) {
21167
21222
  tokenize,
21168
21223
  ignoreFilter,
21169
21224
  strict,
21170
- emitter
21225
+ emitter,
21226
+ recommendedNodeLimit: cfg.scan.maxNodes,
21227
+ overrideMaxNodes: opts.maxNodesOverride ?? null
21171
21228
  };
21172
21229
  if (cfg.scan.referencePaths.length > 0) {
21173
21230
  const walk2 = walkReferencePaths(cfg.scan.referencePaths, cwd);
@@ -21380,7 +21437,12 @@ var WATCH_TEXTS = {
21380
21437
  * accepted shape so the operator can re-run without `--help`.
21381
21438
  */
21382
21439
  maxConsecutiveFailuresInvalid: "{{glyph}} sm watch: --max-consecutive-failures must be a non-negative integer (got {{raw}}).\n {{hint}}\n",
21383
- maxConsecutiveFailuresInvalidHint: "Pass an integer >= 0 (0 disables the circuit-breaker; the default is 5)."
21440
+ maxConsecutiveFailuresInvalidHint: "Pass an integer >= 0 (0 disables the circuit-breaker; the default is 5).",
21441
+ /**
21442
+ * §3.1b two-line block. Validation rejection for `--max-nodes`.
21443
+ */
21444
+ maxNodesInvalid: "{{glyph}} sm watch: --max-nodes must be an integer >= 1 (got {{raw}}).\n {{hint}}\n",
21445
+ maxNodesInvalidHint: "Pass a positive integer, e.g. --max-nodes 256."
21384
21446
  };
21385
21447
 
21386
21448
  // cli/commands/watch.ts
@@ -21443,6 +21505,7 @@ async function runWatchLoop(opts) {
21443
21505
  circuitBreaker: { maxConsecutiveFailures: breakerLimit },
21444
21506
  killSwitches: readConformanceKillSwitches(),
21445
21507
  ...opts.maxBatches !== void 0 ? { maxBatches: opts.maxBatches } : {},
21508
+ ...opts.maxNodes !== void 0 ? { maxNodesOverride: opts.maxNodes } : {},
21446
21509
  events: {
21447
21510
  onBatch: (outcome) => {
21448
21511
  if (outcome.kind === "ok") {
@@ -21556,6 +21619,10 @@ var WatchCommand = class extends SmCommand {
21556
21619
  required: false,
21557
21620
  description: "Shut down with exit 2 after N consecutive batch failures (default 5; 0 disables the breaker)."
21558
21621
  });
21622
+ maxNodes = Option28.String("--max-nodes", {
21623
+ required: false,
21624
+ description: "Per-batch override of scan.maxNodes (default 256). Bidirectional: raises OR lowers the recommended cap on classified nodes. When a batch hits the cap, additional files are dropped and the UI surfaces the persistent oversized banner. Validation: integer >= 1."
21625
+ });
21559
21626
  // Long-running verb, the watcher prints its own "stopped" line on
21560
21627
  // SIGINT / SIGTERM. Adding `done in <…>` after that would be noise.
21561
21628
  emitElapsed = false;
@@ -21563,6 +21630,8 @@ var WatchCommand = class extends SmCommand {
21563
21630
  const roots = this.roots.length > 0 ? this.roots : ["."];
21564
21631
  const breaker = parseBreakerLimit(this.maxConsecutiveFailures, this.context.stderr, this.noColor);
21565
21632
  if (breaker === null) return ExitCode.Error;
21633
+ const maxNodes = parseMaxNodesLimit(this.maxNodes, this.context.stderr, this.noColor);
21634
+ if (maxNodes === null) return ExitCode.Error;
21566
21635
  const watchOpts = {
21567
21636
  roots,
21568
21637
  json: this.json,
@@ -21575,6 +21644,7 @@ var WatchCommand = class extends SmCommand {
21575
21644
  printer: this.printer
21576
21645
  };
21577
21646
  if (breaker !== void 0) watchOpts.maxConsecutiveFailures = breaker;
21647
+ if (maxNodes !== void 0) watchOpts.maxNodes = maxNodes;
21578
21648
  return runWatchLoop(watchOpts);
21579
21649
  }
21580
21650
  };
@@ -21595,6 +21665,23 @@ function parseBreakerLimit(raw, stderr, noColor) {
21595
21665
  }
21596
21666
  return parsed;
21597
21667
  }
21668
+ function parseMaxNodesLimit(raw, stderr, noColor) {
21669
+ if (raw === void 0) return void 0;
21670
+ const n = Number(raw);
21671
+ if (!Number.isInteger(n) || n < 1) {
21672
+ const stderrTty = stderr;
21673
+ const ansi = ansiFor({ isTTY: stderrTty.isTTY === true, noColorFlag: noColor });
21674
+ stderr.write(
21675
+ tx(WATCH_TEXTS.maxNodesInvalid, {
21676
+ glyph: ansi.red("\u2715"),
21677
+ raw,
21678
+ hint: ansi.dim(WATCH_TEXTS.maxNodesInvalidHint)
21679
+ })
21680
+ );
21681
+ return null;
21682
+ }
21683
+ return n;
21684
+ }
21598
21685
 
21599
21686
  // cli/commands/scan.ts
21600
21687
  var ScanCommand = class extends SmCommand {
@@ -21662,10 +21749,16 @@ var ScanCommand = class extends SmCommand {
21662
21749
  yes = Option29.Boolean("--yes", false, {
21663
21750
  description: "Non-interactive mode for ambiguous activeProvider auto-detect. With `--yes`, multiple provider markers (.claude/, .codex/, AGENTS.md, .cursor/) under the scan tree exit non-zero instead of prompting the operator. Set the lens manually via `sm config set activeProvider <id>` and re-run."
21664
21751
  });
21752
+ maxNodes = Option29.String("--max-nodes", {
21753
+ required: false,
21754
+ description: "Per-invocation override of `scan.maxNodes` (default 256). Bidirectional: raises OR lowers the recommended cap on classified nodes. When the walker hits the cap, additional files are dropped and the scan is marked oversized in scan_meta (the UI raises a persistent banner pointing at the .skillmapignore editor in Settings \u2192 Project). Validation: integer >= 1."
21755
+ });
21665
21756
  // Each branch in the orchestrator maps to one validation gate
21666
21757
  // (--watch alias / --changed mutex / -g mutex / dispatch).
21667
21758
  // Splitting per branch scatters the gate from the value it gates.
21668
21759
  async run() {
21760
+ const parsedMaxNodes = this.parseMaxNodesFlag();
21761
+ if (parsedMaxNodes.kind === "error") return parsedMaxNodes.exit;
21669
21762
  if (this.watch) return this.runWatchAlias();
21670
21763
  if (this.changed && this.noBuiltIns) {
21671
21764
  const ansi = this.ansiFor("stderr");
@@ -21701,10 +21794,33 @@ var ScanCommand = class extends SmCommand {
21701
21794
  killSwitches: readConformanceKillSwitches(),
21702
21795
  colorEnabled,
21703
21796
  yes: this.yes,
21704
- style
21797
+ style,
21798
+ ...parsedMaxNodes.value !== void 0 ? { maxNodes: parsedMaxNodes.value } : {}
21705
21799
  });
21706
21800
  return outcome.kind === "ok" ? this.renderOutcome(outcome.result, outcome.persistedTo, outcome.dbPath, outcome.strict) : this.renderFailure(outcome);
21707
21801
  }
21802
+ /**
21803
+ * Parse `--max-nodes <N>`. Returns either the integer value (or
21804
+ * `undefined` when the flag was omitted) or an error sentinel after
21805
+ * printing the validation block. Invalid (non-integer, < 1) exits 2
21806
+ * per spec/cli-contract.md §Node cap.
21807
+ */
21808
+ parseMaxNodesFlag() {
21809
+ if (this.maxNodes === void 0) return { kind: "ok", value: void 0 };
21810
+ const n = Number(this.maxNodes);
21811
+ if (!Number.isInteger(n) || n < 1) {
21812
+ const ansi = this.ansiFor("stderr");
21813
+ this.printer.info(
21814
+ tx(SCAN_TEXTS.maxNodesInvalid, {
21815
+ glyph: ansi.red("\u2715"),
21816
+ value: this.maxNodes,
21817
+ hint: ansi.dim(SCAN_TEXTS.maxNodesInvalidHint)
21818
+ })
21819
+ );
21820
+ return { kind: "error", exit: ExitCode.Error };
21821
+ }
21822
+ return { kind: "ok", value: n };
21823
+ }
21708
21824
  /**
21709
21825
  * `--watch` is a thin alias for the `sm watch` verb. Combining
21710
21826
  * `--watch` with one-shot-only flags is incoherent, the watcher
@@ -21724,6 +21840,7 @@ var ScanCommand = class extends SmCommand {
21724
21840
  }
21725
21841
  this.emitElapsed = false;
21726
21842
  const roots = this.roots.length > 0 ? this.roots : ["."];
21843
+ const parsedMaxNodes = this.parseMaxNodesFlag();
21727
21844
  return runWatchLoop({
21728
21845
  roots,
21729
21846
  json: this.json,
@@ -21733,7 +21850,8 @@ var ScanCommand = class extends SmCommand {
21733
21850
  db: this.db,
21734
21851
  noPlugins: this.noPlugins,
21735
21852
  context: this.context,
21736
- printer: this.printer
21853
+ printer: this.printer,
21854
+ ...parsedMaxNodes.kind === "ok" && parsedMaxNodes.value !== void 0 ? { maxNodes: parsedMaxNodes.value } : {}
21737
21855
  });
21738
21856
  }
21739
21857
  /**
@@ -21820,8 +21938,32 @@ var ScanCommand = class extends SmCommand {
21820
21938
  })
21821
21939
  );
21822
21940
  }
21941
+ this.maybePrintCapNotice(result, ansi);
21823
21942
  return exitCode2;
21824
21943
  }
21944
+ /**
21945
+ * Surface the §Node cap notice when the walker actually stopped
21946
+ * accepting files because of the cap. Derivation: `filesWalked >
21947
+ * effectiveLimit` means the walker incremented past the cap at least
21948
+ * once (i.e. classified the (limit+1)-th raw before breaking). When
21949
+ * the project has EXACTLY the cap many files the loop ends naturally
21950
+ * without ever firing the break, so the notice stays silent.
21951
+ */
21952
+ maybePrintCapNotice(result, ansi) {
21953
+ const recommended = result.recommendedNodeLimit;
21954
+ if (recommended === void 0) return;
21955
+ const override = result.overrideMaxNodes ?? null;
21956
+ const effectiveLimit = override ?? recommended;
21957
+ if (result.stats.filesWalked <= effectiveLimit) return;
21958
+ this.printer.info(
21959
+ tx(SCAN_TEXTS.scanCappedNotice, {
21960
+ glyph: ansi.yellow("\u26A0"),
21961
+ limit: String(effectiveLimit),
21962
+ source: override !== null ? "--max-nodes" : "scan.maxNodes",
21963
+ hint: ansi.dim(SCAN_TEXTS.scanCappedNoticeHint)
21964
+ })
21965
+ );
21966
+ }
21825
21967
  /**
21826
21968
  * `--json` output path. Under `--strict` (H4) self-validates the
21827
21969
  * ScanResult against `scan-result.schema.json` before emitting it,
@@ -24285,6 +24427,9 @@ function createWatcherService(opts) {
24285
24427
  if (opts.debounceMsOverride !== void 0) {
24286
24428
  runtimeOpts.debounceMsOverride = opts.debounceMsOverride;
24287
24429
  }
24430
+ if (opts.options.maxNodes !== void 0) {
24431
+ runtimeOpts.maxNodesOverride = opts.options.maxNodes;
24432
+ }
24288
24433
  return runtimeOpts;
24289
24434
  };
24290
24435
  return {
@@ -24362,7 +24507,11 @@ async function runPersistedScan(c, deps) {
24362
24507
  // BFF has no TTY; ambiguous activeProvider must be resolved by
24363
24508
  // the operator via the Settings UI (PATCH /api/active-provider)
24364
24509
  // before the scan, not via interactive prompt here.
24365
- yes: true
24510
+ yes: true,
24511
+ // `--max-nodes` from the `sm serve` invocation (or the bare
24512
+ // `sm --max-nodes <N>` shortcut) flows through to every scan
24513
+ // the BFF runs so the override is honoured end-to-end.
24514
+ ...deps.options.maxNodes !== void 0 ? { maxNodes: deps.options.maxNodes } : {}
24366
24515
  });
24367
24516
  if (outcome.kind !== "ok") {
24368
24517
  throw new HTTPException13(500, {
@@ -24466,7 +24615,10 @@ async function runFreshScan(deps) {
24466
24615
  printer: bffScanRunnerPrinter,
24467
24616
  // BFF has no TTY; ambiguous activeProvider is the operator's
24468
24617
  // problem to resolve via the Settings UI, not via prompt here.
24469
- yes: true
24618
+ yes: true,
24619
+ // Carry `--max-nodes` from `sm serve` into the fresh-scan path
24620
+ // too so a UI-driven refresh honours the same cap as the watcher.
24621
+ ...deps.options.maxNodes !== void 0 ? { maxNodes: deps.options.maxNodes } : {}
24470
24622
  });
24471
24623
  if (outcome.kind !== "ok") {
24472
24624
  throw new HTTPException13(500, {
@@ -24488,6 +24640,12 @@ function emptyScanResult() {
24488
24640
  scannedAt: Date.now(),
24489
24641
  roots: ["."],
24490
24642
  providers: [],
24643
+ // Surface the design default so the SPA reads the same field shape
24644
+ // on cold boot as on populated DBs. 256 mirrors `scan.maxNodes`
24645
+ // from `src/config/defaults.json`; the temporary testing default
24646
+ // (2) only applies after a real scan walks through the kernel.
24647
+ recommendedNodeLimit: 256,
24648
+ overrideMaxNodes: null,
24491
24649
  nodes: [],
24492
24650
  links: [],
24493
24651
  issues: [],
@@ -25200,6 +25358,8 @@ function validateServerOptions(input) {
25200
25358
  if (watcherError !== null) return { ok: false, error: watcherError };
25201
25359
  const debounceError = validateWatcherDebounce(input.watcherDebounceMs);
25202
25360
  if (debounceError !== null) return { ok: false, error: debounceError };
25361
+ const maxNodesError = validateMaxNodes(input.maxNodes);
25362
+ if (maxNodesError !== null) return { ok: false, error: maxNodesError };
25203
25363
  const noUiError = validateNoUi(filled.noUi, filled.uiDist);
25204
25364
  if (noUiError !== null) return { ok: false, error: noUiError };
25205
25365
  const options = {
@@ -25217,6 +25377,9 @@ function validateServerOptions(input) {
25217
25377
  if (input.watcherDebounceMs !== void 0) {
25218
25378
  options.watcherDebounceMs = input.watcherDebounceMs;
25219
25379
  }
25380
+ if (input.maxNodes !== void 0) {
25381
+ options.maxNodes = input.maxNodes;
25382
+ }
25220
25383
  return { ok: true, options };
25221
25384
  }
25222
25385
  function applyDefaults(input) {
@@ -25277,6 +25440,17 @@ function validateWatcherDebounce(value) {
25277
25440
  }
25278
25441
  return null;
25279
25442
  }
25443
+ function validateMaxNodes(value) {
25444
+ if (value === void 0) return null;
25445
+ if (!Number.isInteger(value) || value < 1) {
25446
+ return {
25447
+ code: "max-nodes-invalid",
25448
+ message: `--max-nodes must be an integer >= 1 (got ${value})`,
25449
+ value: String(value)
25450
+ };
25451
+ }
25452
+ return null;
25453
+ }
25280
25454
  function validateNoUi(noUi, uiDist) {
25281
25455
  if (noUi && uiDist !== null) {
25282
25456
  return {
@@ -25556,6 +25730,12 @@ var SERVE_TEXTS = {
25556
25730
  */
25557
25731
  watcherDebounceInvalid: "{{glyph}} sm serve: --watcher-debounce-ms must be a non-negative integer (got {{value}}).\n {{hint}}\n",
25558
25732
  watcherDebounceInvalidHint: "Pass an integer >= 0 (e.g. 250).",
25733
+ /**
25734
+ * §3.1b error block for an invalid `--max-nodes <N>`. Same shape as
25735
+ * the watcher-debounce template family.
25736
+ */
25737
+ maxNodesInvalid: "{{glyph}} sm serve: --max-nodes must be an integer >= 1 (got {{value}}).\n {{hint}}\n",
25738
+ maxNodesInvalidHint: "Pass a positive integer, e.g. --max-nodes 256.",
25559
25739
  // --- --no-ui flag-validation failures (ExitCode.Error) ------------------
25560
25740
  /**
25561
25741
  * §3.1b error block when `--no-ui` is paired with an explicit
@@ -25825,6 +26005,10 @@ var ServeCommand = class extends SmCommand {
25825
26005
  // who want to tighten / relax the watcher's batching window without
25826
26006
  // editing settings.json. Hidden flag, the Usage block omits it.
25827
26007
  watcherDebounceMs = Option31.String("--watcher-debounce-ms", { required: false, hidden: true });
26008
+ maxNodes = Option31.String("--max-nodes", {
26009
+ required: false,
26010
+ description: "Per-invocation override of scan.maxNodes (default 256). Bidirectional: raises OR lowers the recommended cap on classified nodes. Applies to every scan the server runs (initial watcher pass, debounced batches, POST /api/scan, GET /api/scan?fresh=1). Same flag is honoured on the bare `sm` invocation, which routes to `sm serve`."
26011
+ });
25828
26012
  // Long-running daemon, `done in <…>` after a graceful shutdown is
25829
26013
  // noise. Mirrors `sm watch`'s opt-out.
25830
26014
  emitElapsed = false;
@@ -25903,6 +26087,17 @@ var ServeCommand = class extends SmCommand {
25903
26087
  );
25904
26088
  return ExitCode.Error;
25905
26089
  }
26090
+ const maxNodesResult = parseMaxNodes(this.maxNodes);
26091
+ if (!maxNodesResult.ok) {
26092
+ this.printer.info(
26093
+ tx(SERVE_TEXTS.maxNodesInvalid, {
26094
+ glyph: errGlyph,
26095
+ value: sanitizeForTerminal(maxNodesResult.value),
26096
+ hint: stderrAnsi.dim(SERVE_TEXTS.maxNodesInvalidHint)
26097
+ })
26098
+ );
26099
+ return ExitCode.Error;
26100
+ }
25906
26101
  const input = {
25907
26102
  dbPath,
25908
26103
  uiDist: resolvedUiDist,
@@ -25916,6 +26111,7 @@ var ServeCommand = class extends SmCommand {
25916
26111
  if (portResult.port !== void 0) input.port = portResult.port;
25917
26112
  if (this.host !== void 0) input.host = this.host;
25918
26113
  if (debounceResult.value !== void 0) input.watcherDebounceMs = debounceResult.value;
26114
+ if (maxNodesResult.value !== void 0) input.maxNodes = maxNodesResult.value;
25919
26115
  const validation = validateServerOptions(input);
25920
26116
  if (!validation.ok) {
25921
26117
  this.printer.info(formatValidationError(validation.error, stderrAnsi));
@@ -25985,6 +26181,12 @@ function parseDebounce(raw) {
25985
26181
  if (parsed === null) return { ok: false, value: raw };
25986
26182
  return { ok: true, value: parsed };
25987
26183
  }
26184
+ function parseMaxNodes(raw) {
26185
+ if (raw === void 0) return { ok: true, value: void 0 };
26186
+ const n = Number(raw);
26187
+ if (!Number.isInteger(n) || n < 1) return { ok: false, value: raw };
26188
+ return { ok: true, value: n };
26189
+ }
25988
26190
  function resolveUiDist(ctx, raw) {
25989
26191
  if (raw === void 0) {
25990
26192
  return { ok: true, uiDist: resolveDefaultUiDist(ctx) };
@@ -26031,6 +26233,12 @@ function formatValidationError(err, ansi) {
26031
26233
  value: sanitizeForTerminal(err.value),
26032
26234
  hint: ansi.dim(SERVE_TEXTS.watcherDebounceInvalidHint)
26033
26235
  });
26236
+ case "max-nodes-invalid":
26237
+ return tx(SERVE_TEXTS.maxNodesInvalid, {
26238
+ glyph: errGlyph,
26239
+ value: sanitizeForTerminal(err.value),
26240
+ hint: ansi.dim(SERVE_TEXTS.maxNodesInvalidHint)
26241
+ });
26034
26242
  case "no-ui-conflicts-ui-dist":
26035
26243
  return tx(SERVE_TEXTS.noUiConflictsUiDist, {
26036
26244
  glyph: errGlyph,
@@ -27409,7 +27617,7 @@ var logLevel = resolveLogLevel({
27409
27617
  errStream: process.stderr
27410
27618
  });
27411
27619
  configureLogger(new Logger({ level: logLevel, stream: process.stderr }));
27412
- var bareArgs = args.length === 0 ? resolveBareDefault() : null;
27620
+ var bareArgs = resolveBareInvocation(args);
27413
27621
  var routedArgs = routeHelpArgs(bareArgs ?? args, cli);
27414
27622
  var lifecycleDispatcher = makeHookDispatcher(
27415
27623
  builtIns().hooks ?? [],
@@ -27452,6 +27660,20 @@ await lifecycleDispatcher.dispatch(
27452
27660
  makeEvent("shutdown", { exitCode })
27453
27661
  );
27454
27662
  process.exit(exitCode);
27663
+ function resolveBareInvocation(rawArgs) {
27664
+ if (rawArgs.length === 0) return resolveBareDefault();
27665
+ const first = rawArgs[0];
27666
+ const passthrough = /* @__PURE__ */ new Set(["--help", "-h", "--version", "-V", "-v"]);
27667
+ if (first !== void 0 && first.startsWith("-") && !passthrough.has(first)) {
27668
+ const isSingleDashLong = !first.startsWith("--") && first.length > 2;
27669
+ if (isSingleDashLong) return null;
27670
+ if (existsSync30(defaultProjectDbPath(defaultRuntimeContext()))) {
27671
+ return ["serve", ...rawArgs];
27672
+ }
27673
+ return resolveBareDefault();
27674
+ }
27675
+ return null;
27676
+ }
27455
27677
  function resolveBareDefault() {
27456
27678
  const ctx = defaultRuntimeContext();
27457
27679
  if (existsSync30(defaultProjectDbPath(ctx))) {