@shrkcrft/cli 0.1.0-alpha.21 → 0.1.0-alpha.23

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 (94) hide show
  1. package/dist/command-registry.d.ts +9 -0
  2. package/dist/command-registry.d.ts.map +1 -1
  3. package/dist/command-registry.js +15 -0
  4. package/dist/commands/ai-status.command.d.ts.map +1 -1
  5. package/dist/commands/ai-status.command.js +10 -0
  6. package/dist/commands/apply.command.d.ts.map +1 -1
  7. package/dist/commands/apply.command.js +9 -2
  8. package/dist/commands/ask.command.d.ts.map +1 -1
  9. package/dist/commands/ask.command.js +2 -1
  10. package/dist/commands/changes.command.d.ts.map +1 -1
  11. package/dist/commands/changes.command.js +14 -4
  12. package/dist/commands/check.command.d.ts.map +1 -1
  13. package/dist/commands/check.command.js +119 -3
  14. package/dist/commands/command-catalog.d.ts.map +1 -1
  15. package/dist/commands/command-catalog.js +224 -0
  16. package/dist/commands/commands.command.d.ts +15 -0
  17. package/dist/commands/commands.command.d.ts.map +1 -1
  18. package/dist/commands/commands.command.js +162 -3
  19. package/dist/commands/compress.command.d.ts.map +1 -1
  20. package/dist/commands/compress.command.js +39 -4
  21. package/dist/commands/constructs.command.d.ts.map +1 -1
  22. package/dist/commands/constructs.command.js +40 -9
  23. package/dist/commands/context.command.d.ts.map +1 -1
  24. package/dist/commands/context.command.js +3 -1
  25. package/dist/commands/daily.commands.d.ts.map +1 -1
  26. package/dist/commands/daily.commands.js +48 -5
  27. package/dist/commands/diff-check.command.d.ts.map +1 -1
  28. package/dist/commands/diff-check.command.js +6 -2
  29. package/dist/commands/feedback.command.d.ts.map +1 -1
  30. package/dist/commands/feedback.command.js +4 -0
  31. package/dist/commands/framework.command.d.ts.map +1 -1
  32. package/dist/commands/framework.command.js +20 -3
  33. package/dist/commands/gate.command.d.ts.map +1 -1
  34. package/dist/commands/gate.command.js +99 -2
  35. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  36. package/dist/commands/graph-code-subverbs.js +183 -68
  37. package/dist/commands/graph.command.d.ts.map +1 -1
  38. package/dist/commands/graph.command.js +6 -0
  39. package/dist/commands/impact.command.d.ts.map +1 -1
  40. package/dist/commands/impact.command.js +91 -1
  41. package/dist/commands/knowledge.command.d.ts.map +1 -1
  42. package/dist/commands/knowledge.command.js +21 -1
  43. package/dist/commands/onboard.command.d.ts.map +1 -1
  44. package/dist/commands/onboard.command.js +11 -2
  45. package/dist/commands/orchestrate.command.d.ts.map +1 -1
  46. package/dist/commands/orchestrate.command.js +6 -1
  47. package/dist/commands/pack-author.command.d.ts.map +1 -1
  48. package/dist/commands/pack-author.command.js +13 -1
  49. package/dist/commands/packs.command.d.ts.map +1 -1
  50. package/dist/commands/packs.command.js +95 -17
  51. package/dist/commands/paths.command.d.ts.map +1 -1
  52. package/dist/commands/paths.command.js +8 -1
  53. package/dist/commands/pipelines.command.d.ts.map +1 -1
  54. package/dist/commands/pipelines.command.js +2 -1
  55. package/dist/commands/plan-context.command.d.ts.map +1 -1
  56. package/dist/commands/plan-context.command.js +6 -3
  57. package/dist/commands/plan-simulate.command.js +1 -1
  58. package/dist/commands/policy-lint.command.d.ts +3 -0
  59. package/dist/commands/policy-lint.command.d.ts.map +1 -0
  60. package/dist/commands/policy-lint.command.js +135 -0
  61. package/dist/commands/recommend.command.d.ts.map +1 -1
  62. package/dist/commands/recommend.command.js +10 -2
  63. package/dist/commands/registry.command.d.ts.map +1 -1
  64. package/dist/commands/registry.command.js +117 -9
  65. package/dist/commands/report.command.js +1 -1
  66. package/dist/commands/reuse.command.d.ts +3 -0
  67. package/dist/commands/reuse.command.d.ts.map +1 -0
  68. package/dist/commands/reuse.command.js +202 -0
  69. package/dist/commands/review.command.d.ts.map +1 -1
  70. package/dist/commands/review.command.js +20 -2
  71. package/dist/commands/rule-graph-subverbs.d.ts.map +1 -1
  72. package/dist/commands/rule-graph-subverbs.js +31 -4
  73. package/dist/commands/search-structural.command.d.ts.map +1 -1
  74. package/dist/commands/search-structural.command.js +2 -2
  75. package/dist/commands/search.command.d.ts.map +1 -1
  76. package/dist/commands/search.command.js +19 -1
  77. package/dist/commands/simulate.command.d.ts.map +1 -1
  78. package/dist/commands/simulate.command.js +6 -1
  79. package/dist/commands/smart-context.command.d.ts.map +1 -1
  80. package/dist/commands/smart-context.command.js +87 -4
  81. package/dist/commands/spec.command.d.ts.map +1 -1
  82. package/dist/commands/spec.command.js +73 -13
  83. package/dist/commands/task-context.command.d.ts.map +1 -1
  84. package/dist/commands/task-context.command.js +26 -2
  85. package/dist/commands/test.command.d.ts.map +1 -1
  86. package/dist/commands/test.command.js +17 -0
  87. package/dist/commands/trace.command.d.ts.map +1 -1
  88. package/dist/commands/trace.command.js +14 -1
  89. package/dist/main.d.ts.map +1 -1
  90. package/dist/main.js +19 -1
  91. package/dist/status/freshness.d.ts +30 -0
  92. package/dist/status/freshness.d.ts.map +1 -0
  93. package/dist/status/freshness.js +81 -0
  94. package/package.json +33 -33
@@ -1 +1 @@
1
- {"version":3,"file":"search-structural.command.d.ts","sourceRoot":"","sources":["../../src/commands/search-structural.command.ts"],"names":[],"mappings":"AAgBA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,uBAAuB,EAAE,eAkErC,CAAC"}
1
+ {"version":3,"file":"search-structural.command.d.ts","sourceRoot":"","sources":["../../src/commands/search-structural.command.ts"],"names":[],"mappings":"AAgBA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,uBAAuB,EAAE,eAkErC,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import * as nodePath from 'node:path';
3
3
  import { applyRewritePlan, PatternRegistryStore, planRewrite, runSearch, signRewritePlan, STARTER_PATTERNS, verifySignedRewritePlan, } from '@shrkcrft/structural-search';
4
- import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
4
+ import { flagBool, flagPositiveInt, flagString, resolveCwd, } from "../command-registry.js";
5
5
  import { asJson, header, kv } from "../output/format-output.js";
6
6
  /**
7
7
  * `shrk search structural` — declarative AST pattern matching with
@@ -49,7 +49,7 @@ export const searchStructuralCommand = {
49
49
  process.stderr.write(`Pattern parse error: ${e.message}\n`);
50
50
  return 2;
51
51
  }
52
- const limit = Number(flagString(args, 'limit') ?? '200');
52
+ const limit = flagPositiveInt(args, 'limit', 200);
53
53
  // Rewrite path?
54
54
  const recipeInline = flagString(args, 'rewrite');
55
55
  const recipeFile = flagString(args, 'rewrite-file');
@@ -1 +1 @@
1
- {"version":3,"file":"search.command.d.ts","sourceRoot":"","sources":["../../src/commands/search.command.ts"],"names":[],"mappings":"AAgBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA0BhC,eAAO,MAAM,aAAa,EAAE,eAuG3B,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eA+FrC,CAAC;AAwEF;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,EAAE,eASzC,CAAC"}
1
+ {"version":3,"file":"search.command.d.ts","sourceRoot":"","sources":["../../src/commands/search.command.ts"],"names":[],"mappings":"AAiBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAkChC,eAAO,MAAM,aAAa,EAAE,eAsH3B,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eA+FrC,CAAC;AAwEF;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,EAAE,eASzC,CAAC"}
@@ -1,8 +1,15 @@
1
- import { buildSearchIndex, entrypointBanner, explainSearchTuning, inspectSharkcraft, loadConstructs, loadPlaybooks, loadSearchTuning, renderSearchText, renderTuningExplainHtml, renderTuningExplainMarkdown, searchIndex, SearchKind, SearchSource, } from '@shrkcrft/inspector';
1
+ import { buildSearchIndex, entrypointBanner, explainSearchTuning, inspectSharkcraft, loadConstructs, loadPlaybooks, loadSearchTuning, renderSearchText, renderTuningExplainHtml, renderTuningExplainMarkdown, searchIndex, SearchKind, SearchResultKind, SearchSource, } from '@shrkcrft/inspector';
2
2
  import { flagBool, flagNumber, flagString, flagList, resolveCwd, } from "../command-registry.js";
3
3
  import { asJson } from "../output/format-output.js";
4
4
  const VALID_KINDS = new Set(Object.values(SearchKind));
5
5
  const VALID_SOURCES = new Set(Object.values(SearchSource));
6
+ // Universal-search (v2) filter vocabularies. `--kind` is one of the
7
+ // `SearchResultKind` enum values; `--source` is the local/pack origin the
8
+ // engine actually filters hits by (`makeHit` only ever emits these two). Both
9
+ // are validated up front so a typo'd value rejects loudly instead of silently
10
+ // filtering every hit out and returning an empty result with exit 0.
11
+ const VALID_RESULT_KINDS = Object.values(SearchResultKind);
12
+ const VALID_RESULT_SOURCES = ['local', 'pack'];
6
13
  function parseKinds(args) {
7
14
  const list = flagList(args, 'type').concat(flagList(args, 'kind'));
8
15
  if (list.length === 0)
@@ -48,6 +55,17 @@ export const searchCommand = {
48
55
  const { buildUniversalSearch, renderUniversalSearchText, renderUniversalSearchMarkdown, } = await import('@shrkcrft/inspector');
49
56
  const kindFilter = flagString(args, 'kind');
50
57
  const sourceFilter = flagString(args, 'source');
58
+ // Validate the filter vocabularies BEFORE building opts — universal-search
59
+ // silently drops every non-matching hit, so an unknown `--kind`/`--source`
60
+ // would otherwise return an empty result with exit 0.
61
+ if (kindFilter && !VALID_RESULT_KINDS.includes(kindFilter)) {
62
+ process.stderr.write(`Unknown --kind ${kindFilter}. Valid: ${VALID_RESULT_KINDS.join(', ')}\n`);
63
+ return 2;
64
+ }
65
+ if (sourceFilter && !VALID_RESULT_SOURCES.includes(sourceFilter)) {
66
+ process.stderr.write(`Unknown --source ${sourceFilter}. Valid: ${VALID_RESULT_SOURCES.join(', ')}\n`);
67
+ return 2;
68
+ }
51
69
  const limit = flagNumber(args, 'limit');
52
70
  const opts = {};
53
71
  if (kindFilter)
@@ -1 +1 @@
1
- {"version":3,"file":"simulate.command.d.ts","sourceRoot":"","sources":["../../src/commands/simulate.command.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC,eAAO,MAAM,eAAe,EAAE,eAmD7B,CAAC"}
1
+ {"version":3,"file":"simulate.command.d.ts","sourceRoot":"","sources":["../../src/commands/simulate.command.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAKhC,eAAO,MAAM,eAAe,EAAE,eAyD7B,CAAC"}
@@ -3,10 +3,11 @@ import * as nodePath from 'node:path';
3
3
  import { inspectSharkcraft, OrchestrationMode, renderWorkflowSimulationText, simulateWorkflow, } from '@shrkcrft/inspector';
4
4
  import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
5
5
  import { asJson } from "../output/format-output.js";
6
+ const VALID_SIMULATION_MODES = ['conservative', 'balanced', 'aggressive'];
6
7
  export const simulateCommand = {
7
8
  name: 'simulate',
8
9
  description: 'Predict what a workflow would do without executing anything. Read-only. Optionally takes a playbook or pipeline id.',
9
- usage: 'shrk simulate "<task>" [--playbook <id>] [--pipeline <id>] [--bundle] [--mode conservative|balanced|aggressive] [--json] [--output <file>]',
10
+ usage: 'shrk simulate "<task>" [--playbook <id>] [--pipeline <id>] [--mode conservative|balanced|aggressive] [--json] [--output <file>]',
10
11
  async run(args) {
11
12
  const task = args.positional.join(' ').trim();
12
13
  if (!task) {
@@ -18,6 +19,10 @@ export const simulateCommand = {
18
19
  const playbookId = flagString(args, 'playbook');
19
20
  const pipelineId = flagString(args, 'pipeline');
20
21
  const modeRaw = (flagString(args, 'mode') ?? '').toLowerCase();
22
+ // Warn on a typo'd --mode rather than silently treating it as balanced.
23
+ if (modeRaw && !VALID_SIMULATION_MODES.includes(modeRaw)) {
24
+ process.stderr.write(`Unknown --mode ${modeRaw}. Valid: ${VALID_SIMULATION_MODES.join(', ')} (defaulting to balanced).\n`);
25
+ }
21
26
  const mode = modeRaw === 'conservative'
22
27
  ? OrchestrationMode.Conservative
23
28
  : modeRaw === 'aggressive'
@@ -1 +1 @@
1
- {"version":3,"file":"smart-context.command.d.ts","sourceRoot":"","sources":["../../src/commands/smart-context.command.ts"],"names":[],"mappings":"AAyBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAmFhC,eAAO,MAAM,mBAAmB,EAAE,eAuNjC,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,EAAE,eAmF1C,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,uBAAuB,EAAE,eAwBrC,CAAC;AAEF,8DAA8D;AAC9D,eAAO,MAAM,uBAAuB,EAAE,eAkCrC,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAmH/C,CAAC;AA2JF;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAoH/C,CAAC;AA2JF;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAgG/C,CAAC;AAyPF,4EAA4E;AAC5E,eAAO,MAAM,kCAAkC,EAAE,eAuHhD,CAAC;AAMF,iFAAiF;AACjF,eAAO,MAAM,mCAAmC,EAAE,eAsCjD,CAAC;AAqJF,UAAU,eAAe;IACvB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,SAAS,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,eAAe,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAWzF"}
1
+ {"version":3,"file":"smart-context.command.d.ts","sourceRoot":"","sources":["../../src/commands/smart-context.command.ts"],"names":[],"mappings":"AA0BA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAmFhC,eAAO,MAAM,mBAAmB,EAAE,eAmQjC,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,EAAE,eAsF1C,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,uBAAuB,EAAE,eAwBrC,CAAC;AAEF,8DAA8D;AAC9D,eAAO,MAAM,uBAAuB,EAAE,eAkCrC,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAmH/C,CAAC;AA2JF;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAoH/C,CAAC;AA2JF;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAgG/C,CAAC;AAyPF,4EAA4E;AAC5E,eAAO,MAAM,kCAAkC,EAAE,eAuHhD,CAAC;AAMF,iFAAiF;AACjF,eAAO,MAAM,mCAAmC,EAAE,eAsCjD,CAAC;AAqJF,UAAU,eAAe;IACvB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,SAAS,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,eAAe,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAWzF"}
@@ -5,7 +5,7 @@ import * as os from 'node:os';
5
5
  import { AiMessageRole, buildPromptMessages, EnhancementPipeline, EnhancementStageKind, OllamaProvider, buildDefaultEnhancementStages, buildFastEnhancementStages, selectAiProvider, } from '@shrkcrft/ai';
6
6
  import { buildContext } from '@shrkcrft/context';
7
7
  import { EdgeKind, GraphQueryApi, GraphStore, NodeKind } from '@shrkcrft/graph';
8
- import { buildProjectOverview, buildTaskPacket, inspectSharkcraft, renderOverviewText, } from '@shrkcrft/inspector';
8
+ import { buildProjectOverview, buildTaskPacket, contextTuningBoostFor, inspectSharkcraft, renderOverviewText, } from '@shrkcrft/inspector';
9
9
  import { flagBool, flagList, flagNumber, flagString, resolveCwd, } from "../command-registry.js";
10
10
  import { DeclarationKind, PLAN_CACHE_SCHEMA, PlanCache, SemanticIndex, TaskType, buildFocusedContext, classifyTask, encodeEmbedding, getDefaultSourceRoots, listIndexableFiles, parseTaskTypeOverride, pruneDeletedHits, renderFocusedContextForPrompt, } from '@shrkcrft/embeddings';
11
11
  import { SmartContextDetailedPlanSchema, SmartContextExpansionRequestSchema, } from "../schemas/json-schemas.js";
@@ -150,6 +150,42 @@ export const smartContextCommand = {
150
150
  }
151
151
  }
152
152
  const enh = enhanced.value.enhancement;
153
+ // Deterministic-always contract (CLAUDE.md): when the enhancement
154
+ // pipeline fully degrades — every stage failed (or none ran), so the
155
+ // pipeline's finalOutput is just the echoed system message (preamble +
156
+ // "## Repository context" + the assembled deterministic seed) — fall
157
+ // back to the clean deterministic seed instead of leaking the raw
158
+ // internal prompt to stdout. This mirrors the single-shot
159
+ // provider-failure fallback below: clean renderSeed(seed) content,
160
+ // finishReason 'deterministic-fallback', no LLM/system-prompt wrapper.
161
+ const systemMsg = messages.find((m) => m.role === AiMessageRole.System);
162
+ const originalContext = (systemMsg?.content ?? '').trim();
163
+ const fullyDegraded = (enh.stages.length > 0 && enh.stages.every((s) => s.degraded)) ||
164
+ (originalContext.length > 0 && enhanced.value.content.trim() === originalContext);
165
+ if (fullyDegraded) {
166
+ if (!opts.json) {
167
+ process.stderr.write('[smart-context] enhancement fully degraded — returning deterministic context only.\n');
168
+ }
169
+ const fallbackEnvelope = buildEnvelope({
170
+ task,
171
+ seed,
172
+ ai: {
173
+ content: renderSeed(seed),
174
+ model: 'deterministic',
175
+ finishReason: 'deterministic-fallback',
176
+ usage: null,
177
+ providerId: 'deterministic',
178
+ },
179
+ mode: opts.mode,
180
+ });
181
+ if (opts.save) {
182
+ const saved = saveEnvelope(cwd, fallbackEnvelope);
183
+ writeSavedNotice(saved, opts.json, fallbackEnvelope);
184
+ return 0;
185
+ }
186
+ writeEnvelope(fallbackEnvelope, opts.json, opts.debug);
187
+ return 0;
188
+ }
153
189
  if (!opts.json && !enh.deterministicFallback) {
154
190
  if (enh.budgetExhausted) {
155
191
  process.stderr.write(`[smart-context] budget reached before all ${enh.plannedPasses} passes finished — output is the best so far. Try a smaller --model or raise --budget.\n`);
@@ -179,6 +215,9 @@ export const smartContextCommand = {
179
215
  messages,
180
216
  maxTokens: opts.maxTokens,
181
217
  model: opts.model,
218
+ // Bound the single shot so a hung local model degrades to the
219
+ // deterministic seed below instead of hanging the command.
220
+ timeoutMs: perCallTimeoutMs(opts.budgetMs),
182
221
  });
183
222
  if (!aiResult.ok) {
184
223
  // Deterministic-always contract (CLAUDE.md): a provider failure must
@@ -293,6 +332,9 @@ export const smartContextPlanAheadCommand = {
293
332
  messages,
294
333
  maxTokens: opts.maxTokens,
295
334
  model: opts.model,
335
+ // Bound each task's call so one hung model can't stall the queue;
336
+ // a timeout is reported as that task's error and the loop continues.
337
+ timeoutMs: perCallTimeoutMs(opts.budgetMs),
296
338
  });
297
339
  if (!aiResult.ok) {
298
340
  results.push({ task, status: 'error', error: aiResult.error.message, slug: slug(task) });
@@ -1418,6 +1460,7 @@ async function buildSmartContextSeed(input) {
1418
1460
  task,
1419
1461
  maxTokens: options.seedTokens,
1420
1462
  projectOverview: overviewText,
1463
+ boostFor: contextTuningBoostFor(inspection, task),
1421
1464
  });
1422
1465
  const graphGrounding = buildInitialGraphGrounding(cwd, task);
1423
1466
  const semantic = await tryLoadSemanticHits(cwd, task, 10, options);
@@ -2693,6 +2736,23 @@ function truncateLine(text, max) {
2693
2736
  const PER_STAGE_TIMEOUT_MS = 90_000;
2694
2737
  const FAST_ENHANCE_BUDGET_MS = 150_000;
2695
2738
  const PLUS_ENHANCE_BUDGET_MS = 360_000;
2739
+ /**
2740
+ * Per-call wall-clock bounds for the paths that DON'T run through
2741
+ * `runEnhancementPipeline` (single-shot brief, plan-ahead, and the two
2742
+ * `--ai-plan` stages). Without these, `--budget` only constrained the
2743
+ * enhancement pipeline, so a hung local model could still hang those modes.
2744
+ *
2745
+ * - `SINGLE_CALL_DEFAULT_BUDGET_MS` — the cap applied when no `--budget` is
2746
+ * given (mirrors the fast-enhance ceiling).
2747
+ * - `PER_CALL_TIMEOUT_CAP_MS` — an absolute ceiling, so even a generous
2748
+ * `--budget` can't let a single call run unbounded.
2749
+ */
2750
+ const SINGLE_CALL_DEFAULT_BUDGET_MS = 150_000;
2751
+ const PER_CALL_TIMEOUT_CAP_MS = 360_000;
2752
+ /** Derive a per-call timeout (ms) from the optional `--budget` (budgetMs). */
2753
+ function perCallTimeoutMs(budgetMs) {
2754
+ return Math.min(budgetMs ?? SINGLE_CALL_DEFAULT_BUDGET_MS, PER_CALL_TIMEOUT_CAP_MS);
2755
+ }
2696
2756
  async function runEnhancementPipeline(input) {
2697
2757
  const provider = input.provider;
2698
2758
  const systemMsg = input.messages.find((m) => m.role === AiMessageRole.System);
@@ -2830,6 +2890,7 @@ async function callProvider(input) {
2830
2890
  ...(input.model ? { model: input.model } : {}),
2831
2891
  ...(input.responseFormat ? { responseFormat: input.responseFormat } : {}),
2832
2892
  ...(input.onTokenStream ? { onTokenStream: input.onTokenStream } : {}),
2893
+ ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
2833
2894
  });
2834
2895
  if (!res.ok || !res.value)
2835
2896
  return { ok: false, error: res.error };
@@ -2927,10 +2988,23 @@ function writeDryRun(messages, mode, provider) {
2927
2988
  }
2928
2989
  }
2929
2990
  function displayProviderName(explicit) {
2930
- if (explicit)
2931
- return explicit;
2991
+ // Mirror provider-resolver.ts `normaliseKind` so the --dry-run header names
2992
+ // the provider that will ACTUALLY run, not a stale 'auto'. An explicit
2993
+ // `auto` is a local-first request that AI_PROVIDER never overrides; with no
2994
+ // --provider, AI_PROVIDER (including a hosted claude/gemini) decides what
2995
+ // runs, so the header must say so truthfully.
2996
+ if (explicit !== undefined) {
2997
+ const e = explicit.trim().toLowerCase();
2998
+ if (e === 'auto')
2999
+ return 'auto';
3000
+ if (e === 'claude' || e === 'gemini' || e === 'ollama' || e === 'llamacpp')
3001
+ return e;
3002
+ }
2932
3003
  const envProvider = (process.env.AI_PROVIDER ?? '').trim().toLowerCase();
2933
- if (envProvider === 'ollama' || envProvider === 'llamacpp') {
3004
+ if (envProvider === 'ollama' ||
3005
+ envProvider === 'llamacpp' ||
3006
+ envProvider === 'claude' ||
3007
+ envProvider === 'gemini') {
2934
3008
  return envProvider;
2935
3009
  }
2936
3010
  return 'auto';
@@ -3212,6 +3286,11 @@ async function buildAiPlanEnvelope(input) {
3212
3286
  }
3213
3287
  progressMarker(`preflight ok — host=${preflight.value.host} models=${preflight.value.models.length}`, input.options);
3214
3288
  }
3289
+ // Split the (optional) --budget across the two stages so a hung local
3290
+ // model can't hang the ai-plan path the way it previously could. Each
3291
+ // call is independently bounded; a TIMEOUT surfaces as a call-failure and
3292
+ // is handled exactly like any other provider failure below.
3293
+ const aiPlanStageTimeoutMs = Math.min(Math.floor((input.options.budgetMs ?? SINGLE_CALL_DEFAULT_BUDGET_MS) / 2), PER_CALL_TIMEOUT_CAP_MS);
3215
3294
  progressMarker(`stage 1 calling ${selection.provider.id}${input.options.model ? `:${input.options.model}` : ''}…`, input.options);
3216
3295
  const stage1Messages = buildStage1Messages(input.seed, grounding, cacheLookup.reference);
3217
3296
  logPromptToStderr('stage1', stage1Messages, input.options);
@@ -3229,6 +3308,7 @@ async function buildAiPlanEnvelope(input) {
3229
3308
  repromptInstruction: STAGE1_REPROMPT,
3230
3309
  stageLabel: 'stage 1',
3231
3310
  options: input.options,
3311
+ timeoutMs: aiPlanStageTimeoutMs,
3232
3312
  });
3233
3313
  let stage1Request;
3234
3314
  let stage1Retried = false;
@@ -3275,6 +3355,7 @@ async function buildAiPlanEnvelope(input) {
3275
3355
  repromptInstruction: STAGE2_REPROMPT,
3276
3356
  stageLabel: 'stage 2',
3277
3357
  options: input.options,
3358
+ timeoutMs: aiPlanStageTimeoutMs,
3278
3359
  });
3279
3360
  const conversationTurns = [];
3280
3361
  if (stage1Call) {
@@ -3424,6 +3505,7 @@ async function callProviderWithRetry(input) {
3424
3505
  maxTokens: input.maxTokens,
3425
3506
  ...(input.model ? { model: input.model } : {}),
3426
3507
  ...(input.responseFormat ? { responseFormat: input.responseFormat } : {}),
3508
+ ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
3427
3509
  });
3428
3510
  if (!first.ok)
3429
3511
  return { kind: 'call-failed', error: first.error };
@@ -3449,6 +3531,7 @@ async function callProviderWithRetry(input) {
3449
3531
  maxTokens: input.maxTokens,
3450
3532
  ...(input.model ? { model: input.model } : {}),
3451
3533
  ...(input.responseFormat ? { responseFormat: input.responseFormat } : {}),
3534
+ ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
3452
3535
  });
3453
3536
  if (!second.ok) {
3454
3537
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"spec.command.d.ts","sourceRoot":"","sources":["../../src/commands/spec.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA0CH,OAAO,EASL,gBAAgB,EAIjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAShC,eAAO,MAAM,qBAAqB,iCAAiC,CAAC;AACpE,eAAO,MAAM,wBAAwB,oCAAoC,CAAC;AAY1E,eAAO,MAAM,iBAAiB,EAAE,eA2K/B,CAAC;AAcF,eAAO,MAAM,iBAAiB,EAAE,eAiE/B,CAAC;AAqBF,eAAO,MAAM,eAAe,EAAE,eAmC7B,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,eA8B7B,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,eA2C7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,eAoE/B,CAAC;AASF,eAAO,MAAM,oBAAoB,EAAE,eAsRlC,CAAC;AAyDF,eAAO,MAAM,iBAAiB,EAAE,eAsL/B,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,eA4B/B,CAAC;AAkBF,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"spec.command.d.ts","sourceRoot":"","sources":["../../src/commands/spec.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA0CH,OAAO,EAUL,gBAAgB,EAMjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAShC,eAAO,MAAM,qBAAqB,iCAAiC,CAAC;AACpE,eAAO,MAAM,wBAAwB,oCAAoC,CAAC;AAY1E,eAAO,MAAM,iBAAiB,EAAE,eA2K/B,CAAC;AAcF,eAAO,MAAM,iBAAiB,EAAE,eAiE/B,CAAC;AAqBF,eAAO,MAAM,eAAe,EAAE,eAmC7B,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,eA8B7B,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,eA2C7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,eAoE/B,CAAC;AASF,eAAO,MAAM,oBAAoB,EAAE,eAsRlC,CAAC;AAyDF,eAAO,MAAM,iBAAiB,EAAE,eAqP/B,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,eA4B/B,CAAC;AAkBF,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
@@ -17,7 +17,7 @@ import { createHash } from 'node:crypto';
17
17
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
18
18
  import * as nodePath from 'node:path';
19
19
  import { appendSpecEvent, buildSavedPlan, buildSpecId, canonicalJson, deriveSpecJson, FileChangeType, generate, listSpecIds, loadSpec, OverwriteStrategy, persistSpecArtifacts, PLAN_SECRET_ENV, readSpecEvents, renderSpecMd, savePlanToFile, signPlan, specDir, specMdPath, specPlanPath, specVerificationPath, splitSpecMd, SPEC_SCHEMA_V1, SpecStatus, validateSpecStructural, verifyPlan, writeSpecJson, writeSpecMd, } from '@shrkcrft/generator';
20
- import { AssetProvenanceOperation, AssetProvenanceSource, buildSpecList, buildSpecReview, buildTaskPacket, inspectSharkcraft, recordProvenance, SPEC_LIST_SCHEMA, } from '@shrkcrft/inspector';
20
+ import { AssetProvenanceOperation, AssetProvenanceSource, buildSpecList, buildSpecReview, buildTaskPacket, inspectSharkcraft, mapChecklistToEvidence, recordProvenance, SPEC_LIST_SCHEMA, } from '@shrkcrft/inspector';
21
21
  import { flagBool, flagList, flagString, resolveCwd, } from "../command-registry.js";
22
22
  import { collectChangedPaths } from "../diff/collect-changed-paths.js";
23
23
  import { asJson, header } from "../output/format-output.js";
@@ -778,8 +778,8 @@ function shortHash(text) {
778
778
  }
779
779
  export const specVerifyCommand = {
780
780
  name: 'verify',
781
- description: 'Run the spec\'s trusted verification commands, check acceptance-criteria coverage, run diff-aware boundary + drift checks. Returns sharkcraft.spec-verification/v1.',
782
- usage: 'shrk spec verify <id> [--since <ref>] [--strict] [--skip-verification-commands] [--json] [--write]',
781
+ description: 'Run the spec\'s trusted verification commands, check acceptance-criteria coverage, run diff-aware boundary + drift checks. Pass --coverage to map each acceptance criterion to backing changeset evidence and flag UNMET (claimed-but-zero-footprint) criteria. Returns sharkcraft.spec-verification/v1.',
782
+ usage: 'shrk spec verify <id> [--since <ref>] [--coverage] [--strict] [--skip-verification-commands] [--json] [--write]',
783
783
  async run(args) {
784
784
  const ref = args.positional[0];
785
785
  if (!ref) {
@@ -886,18 +886,51 @@ export const specVerifyCommand = {
886
886
  planIntegrity = 'invalid-signature';
887
887
  }
888
888
  }
889
- const acceptanceResults = spec.acceptanceCriteria.map((ac) => ({
890
- id: ac.id,
891
- status: ac.verifiedBy.includes('manual')
892
- ? 'manual'
893
- : commandFailed
894
- ? 'fail'
895
- : 'pass',
896
- evidence: [],
897
- }));
889
+ // --coverage: map each acceptance criterion to backing changeset
890
+ // evidence (new exported symbol / registration / route / companion
891
+ // file / test). UNMET = a claimed criterion with zero footprint.
892
+ const wantCoverage = flagBool(args, 'coverage');
893
+ let coverage = null;
894
+ if (wantCoverage) {
895
+ const fileContents = {};
896
+ if (changed.isAvailable) {
897
+ for (const rel of changed.changed) {
898
+ const abs = nodePath.join(cwd, rel);
899
+ if (existsSync(abs)) {
900
+ try {
901
+ fileContents[rel] = readFileSync(abs, 'utf8');
902
+ }
903
+ catch {
904
+ fileContents[rel] = '';
905
+ }
906
+ }
907
+ }
908
+ }
909
+ coverage = mapChecklistToEvidence({
910
+ criteria: spec.acceptanceCriteria.map((ac) => ({ id: ac.id, text: ac.text })),
911
+ changedFiles: changed.changed,
912
+ fileContents,
913
+ });
914
+ }
915
+ const coverageById = new Map((coverage?.criteria ?? []).map((c) => [c.id, c]));
916
+ const acceptanceResults = spec.acceptanceCriteria.map((ac) => {
917
+ const cov = coverageById.get(ac.id);
918
+ return {
919
+ id: ac.id,
920
+ status: ac.verifiedBy.includes('manual')
921
+ ? 'manual'
922
+ : commandFailed
923
+ ? 'fail'
924
+ : 'pass',
925
+ evidence: cov?.evidence ?? [],
926
+ ...(wantCoverage ? { covered: cov?.covered ?? false } : {}),
927
+ };
928
+ });
898
929
  const verdict = commandFailed
899
930
  ? 'fail'
900
- : scopeDrift.length > 0 || planIntegrity === 'missing-signature'
931
+ : scopeDrift.length > 0 ||
932
+ planIntegrity === 'missing-signature' ||
933
+ (coverage !== null && coverage.unmetCount > 0)
901
934
  ? 'warn'
902
935
  : 'pass';
903
936
  const report = {
@@ -911,6 +944,17 @@ export const specVerifyCommand = {
911
944
  boundaries: { since: changed.ref, violations: [] },
912
945
  scopeDrift: { outsideScope: scopeDrift },
913
946
  planIntegrity: { status: planIntegrity },
947
+ ...(coverage !== null
948
+ ? {
949
+ coverage: {
950
+ checked: true,
951
+ total: coverage.criteria.length,
952
+ covered: coverage.coveredCount,
953
+ unmet: coverage.unmetCount,
954
+ changesetAvailable: changed.isAvailable,
955
+ },
956
+ }
957
+ : {}),
914
958
  };
915
959
  if (flagBool(args, 'write') || flagBool(args, 'apply')) {
916
960
  writeFileSync(specVerificationPath(cwd, spec.id), JSON.stringify(report, null, 2) + '\n', 'utf8');
@@ -935,6 +979,22 @@ export const specVerifyCommand = {
935
979
  }
936
980
  process.stdout.write(` scope drift: ${scopeDrift.length}\n`);
937
981
  process.stdout.write(` plan integrity: ${planIntegrity}\n`);
982
+ if (coverage !== null) {
983
+ process.stdout.write(` coverage: ${coverage.coveredCount}/${coverage.criteria.length} covered, ${coverage.unmetCount} UNMET\n`);
984
+ if (!changed.isAvailable) {
985
+ process.stdout.write(' (no changeset available — pass --since <ref> for evidence)\n');
986
+ }
987
+ for (const c of coverage.criteria) {
988
+ if (c.covered) {
989
+ const ev = c.evidence[0];
990
+ const note = ev ? ` (${ev.kind}: ${ev.detail})` : '';
991
+ process.stdout.write(` [ok] ${c.id}${note}\n`);
992
+ }
993
+ else {
994
+ process.stdout.write(` UNMET ${c.id} — ${c.text.slice(0, 60)}\n`);
995
+ }
996
+ }
997
+ }
938
998
  }
939
999
  if (verdict === 'fail')
940
1000
  return 1;
@@ -1 +1 @@
1
- {"version":3,"file":"task-context.command.d.ts","sourceRoot":"","sources":["../../src/commands/task-context.command.ts"],"names":[],"mappings":"AAuBA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAahC,eAAO,MAAM,qBAAqB,EAAE,eAyEnC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,eAsEnC,CAAC;AAIF,eAAO,MAAM,mBAAmB,EAAE,eA2DjC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,eAqBnC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,eAoBlC,CAAC"}
1
+ {"version":3,"file":"task-context.command.d.ts","sourceRoot":"","sources":["../../src/commands/task-context.command.ts"],"names":[],"mappings":"AAuBA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAchC,eAAO,MAAM,qBAAqB,EAAE,eAyEnC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,eAsEnC,CAAC;AAIF,eAAO,MAAM,mBAAmB,EAAE,eA2DjC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,eAqBnC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,eA8ClC,CAAC"}
@@ -4,6 +4,7 @@ import { cliCommandNameSet } from "./command-catalog.js";
4
4
  import { analyzeImportGraph, buildAgentBrief, buildContradictionReport, buildGeneratedCodeReport, buildRepositoryKnowledgeModel, buildTaskPacket, buildTaskRiskReport, classifyChangeIntent, detectLanguageProfiles, getChangedFiles, getStatusSummary, inspectSharkcraft, isGitRepo, listConstructs, loadRepositoryMemory, } from '@shrkcrft/inspector';
5
5
  import { flagBool, flagString, flagList, resolveCwd, } from "../command-registry.js";
6
6
  import { asJson, header, kv } from "../output/format-output.js";
7
+ import { computeMtimeFreshness } from "../status/freshness.js";
7
8
  const CONTEXT_BASE = nodePath.join('.sharkcraft', 'context');
8
9
  function slug(s) {
9
10
  return s
@@ -244,11 +245,34 @@ export const contextStatusCommand = {
244
245
  }
245
246
  const { readFileSync } = await import('node:fs');
246
247
  const body = readFileSync(file, 'utf8');
248
+ let status;
249
+ try {
250
+ status = JSON.parse(body);
251
+ }
252
+ catch {
253
+ status = {};
254
+ }
255
+ // Honest freshness: the saved context is stale once a source file changed
256
+ // after it was built. Adds `state` + a warn line in both shapes.
257
+ const fresh = computeMtimeFreshness(cwd, status.lastBuilt);
247
258
  if (flagBool(args, 'json')) {
248
- process.stdout.write(body + '\n');
259
+ process.stdout.write(asJson({
260
+ ...status,
261
+ state: fresh.state,
262
+ lastBuiltAt: fresh.lastBuiltAt,
263
+ lastChangedAt: fresh.lastChangedAt,
264
+ behindMs: fresh.behindMs,
265
+ ...(fresh.state === 'stale' ? { nextCommand: 'shrk context refresh' } : {}),
266
+ }) + '\n');
249
267
  return 0;
250
268
  }
251
- process.stdout.write(body + '\n');
269
+ process.stdout.write(header('Context status'));
270
+ process.stdout.write(kv('last task', status.lastTask ?? '-') + '\n');
271
+ process.stdout.write(kv('last built', status.lastBuilt ?? '-') + '\n');
272
+ process.stdout.write(kv('state', fresh.state) + '\n');
273
+ if (fresh.state === 'stale') {
274
+ process.stdout.write(`! stale — files changed since this context was built${fresh.lastChangedAt ? ` (last change ${fresh.lastChangedAt})` : ''}; re-run \`shrk context refresh\`\n`);
275
+ }
252
276
  return 0;
253
277
  },
254
278
  };
@@ -1 +1 @@
1
- {"version":3,"file":"test.command.d.ts","sourceRoot":"","sources":["../../src/commands/test.command.ts"],"names":[],"mappings":"AAUA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA6QhC,eAAO,MAAM,WAAW,EAAE,eA4BzB,CAAC"}
1
+ {"version":3,"file":"test.command.d.ts","sourceRoot":"","sources":["../../src/commands/test.command.ts"],"names":[],"mappings":"AAUA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAiShC,eAAO,MAAM,WAAW,EAAE,eA4BzB,CAAC"}
@@ -3,6 +3,7 @@ import * as nodePath from 'node:path';
3
3
  import { buildTaskPacket, inspectSharkcraft, loadAgentContractTests, loadContextTests, runAgentContractTest, runContextTest, } from '@shrkcrft/inspector';
4
4
  import { flagBool, flagList, flagString, resolveCwd, } from "../command-registry.js";
5
5
  import { asJson, header, kv } from "../output/format-output.js";
6
+ import { agentTestMissingExpectedHints, renderFailureHints } from "../output/failure-hints.js";
6
7
  import { maybeRunInWatchMode } from "../output/watch-loop.js";
7
8
  async function runContextTests(args) {
8
9
  const inspection = await inspectSharkcraft({ cwd: resolveCwd(args) });
@@ -114,6 +115,22 @@ async function runAgentTests(args) {
114
115
  }
115
116
  }
116
117
  process.stdout.write(`\nSummary: ${results.length - failed.length}/${results.length} passed.\n`);
118
+ // Surface explain/why hints when a failure lists an expected construct the
119
+ // packet didn't include — that's the case the `shrk why`/`why-not` family
120
+ // is built to diagnose.
121
+ const anyMissingExpected = failed.some((r) => (r.missingTemplates ?? []).length > 0 ||
122
+ (r.missingRules ?? []).length > 0 ||
123
+ (r.missingForbiddenActions ?? []).length > 0 ||
124
+ (r.missingVerificationCommands ?? []).length > 0 ||
125
+ (r.missingHelpers ?? []).length > 0 ||
126
+ (r.missingPlaybooks ?? []).length > 0 ||
127
+ (r.missingPolicies ?? []).length > 0 ||
128
+ (r.missingConstructs ?? []).length > 0 ||
129
+ (r.missingCommands ?? []).length > 0 ||
130
+ (r.missingKnowledge ?? []).length > 0);
131
+ if (anyMissingExpected) {
132
+ process.stdout.write(renderFailureHints(agentTestMissingExpectedHints()));
133
+ }
117
134
  return failed.length > 0 ? 1 : 0;
118
135
  }
119
136
  async function generateContextTest(args) {
@@ -1 +1 @@
1
- {"version":3,"file":"trace.command.d.ts","sourceRoot":"","sources":["../../src/commands/trace.command.ts"],"names":[],"mappings":"AAcA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAOhC,eAAO,MAAM,YAAY,EAAE,eAsG1B,CAAC"}
1
+ {"version":3,"file":"trace.command.d.ts","sourceRoot":"","sources":["../../src/commands/trace.command.ts"],"names":[],"mappings":"AAcA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAOhC,eAAO,MAAM,YAAY,EAAE,eAqH1B,CAAC"}
@@ -6,7 +6,7 @@
6
6
  * shared query resolver and prints structured trace output.
7
7
  */
8
8
  import { findSymbolInProject, inspectSharkcraft, QueryMatchKind, resolveQuery, } from '@shrkcrft/inspector';
9
- import { flagBool, flagNumber, flagString, resolveCwd, } from "../command-registry.js";
9
+ import { flagBool, flagList, flagNumber, flagString, resolveCwd, } from "../command-registry.js";
10
10
  import { asJson, header } from "../output/format-output.js";
11
11
  function describeMatch(m) {
12
12
  return `${m.kind.padEnd(12)} ${m.id}${m.label && m.label !== m.id ? ` — ${m.label}` : ''} [${m.score.toFixed(0)}]`;
@@ -58,10 +58,23 @@ export const traceCommand = {
58
58
  process.stderr.write('Usage: shrk trace <query> | shrk trace --symbol <Name>\n');
59
59
  return 2;
60
60
  }
61
+ // `--kind` restricts the result set to specific match kinds — the engine
62
+ // (resolveQuery) filters by them. Validate up front so an unknown kind
63
+ // fails fast with the valid list, instead of being silently ignored (which
64
+ // returned a wrong-kind best match, e.g. a knowledge hit for
65
+ // `--kind template`).
66
+ const kinds = flagList(args, 'kind');
67
+ const validKinds = new Set(Object.values(QueryMatchKind));
68
+ const unknownKind = kinds.find((k) => !validKinds.has(k));
69
+ if (unknownKind) {
70
+ process.stderr.write(`trace: unknown --kind "${unknownKind}". Valid kinds: ${[...validKinds].sort().join(', ')}.\n`);
71
+ return 2;
72
+ }
61
73
  const cwd = resolveCwd(args);
62
74
  const inspection = await inspectSharkcraft({ cwd });
63
75
  const limit = flagNumber(args, 'limit');
64
76
  const resolution = resolveQuery(inspection, query, {
77
+ ...(kinds.length > 0 ? { kinds: kinds } : {}),
65
78
  ...(typeof limit === 'number' ? { limit } : {}),
66
79
  });
67
80
  if (flagBool(args, 'json')) {
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EACL,eAAe,EAKhB,MAAM,uBAAuB,CAAC;AAwX/B,wBAAgB,aAAa,IAAI,eAAe,CA4X/C;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrE;AAsHD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAoDxE"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EACL,eAAe,EAKhB,MAAM,uBAAuB,CAAC;AA0X/B,wBAAgB,aAAa,IAAI,eAAe,CA8X/C;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrE;AAqID;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAoDxE"}
package/dist/main.js CHANGED
@@ -32,6 +32,8 @@ import { archCommand } from "./commands/arch.command.js";
32
32
  import { frameworkCommand } from "./commands/framework.command.js";
33
33
  import { apiDiffCommand } from "./commands/api-diff.command.js";
34
34
  import { gateCommand } from "./commands/gate.command.js";
35
+ import { policyLintCommand } from "./commands/policy-lint.command.js";
36
+ import { reuseCommand } from "./commands/reuse.command.js";
35
37
  import { migrateCommand } from "./commands/migrate.command.js";
36
38
  import { coverageCommand } from "./commands/coverage.command.js";
37
39
  import { statsCommand } from "./commands/stats.command.js";
@@ -198,6 +200,8 @@ export function buildRegistry() {
198
200
  registry.register(frameworkCommand);
199
201
  registry.register(apiDiffCommand);
200
202
  registry.register(gateCommand);
203
+ registry.register(policyLintCommand);
204
+ registry.register(reuseCommand);
201
205
  registry.register(migrateCommand);
202
206
  registry.register(coverageCommand);
203
207
  registry.register(statsCommand);
@@ -597,7 +601,21 @@ async function runCliInner(argv) {
597
601
  // (`shrk doctor`, `shrk packs list`, `shrk pack author status`). The
598
602
  // resolver stops when it hits a flag or a token that isn't a child of the
599
603
  // current node, returning the deepest handler + the leftover tokens.
600
- const { handler, matchedPath, rest: leftover, node } = registry.resolve(cleanArgv);
604
+ let { handler, matchedPath, rest: leftover, node } = registry.resolve(cleanArgv);
605
+ // A multi-word command passed as a SINGLE quoted token (`shrk "graph status"`)
606
+ // arrives as one argv element with internal whitespace. The trie has no atomic
607
+ // `graph status` node, so the first descent misses (handler undefined) — yet
608
+ // did-you-mean would still list `graph status` as the #1 closest match, a
609
+ // self-contradiction. Re-split the lone token on whitespace and retry so the
610
+ // quoted form behaves identically to the unquoted `shrk graph status`.
611
+ // Genuinely unknown tokens leave `handler` undefined and fall through to the
612
+ // existing unknown-command path unchanged.
613
+ if (!handler && cleanArgv.length === 1 && /\s/.test(cleanArgv[0])) {
614
+ const retry = registry.resolve(cleanArgv[0].trim().split(/\s+/));
615
+ if (retry.handler) {
616
+ ({ handler, matchedPath, rest: leftover, node } = retry);
617
+ }
618
+ }
601
619
  // `--help` / `-h` immediately after a (sub)group → render help for that
602
620
  // path. This works at any depth.
603
621
  if (leftover[0] === '--help' || leftover[0] === '-h') {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Source-mtime freshness — a small shared CLI helper.
3
+ *
4
+ * Several `… status` commands (`framework status`, `context status`) need to
5
+ * answer the same question: "is this stored artefact stale relative to the
6
+ * source tree it was built from?" They all walk the workspace for the newest
7
+ * source-file mtime and compare it to a stored build timestamp. This module
8
+ * is the single home for that walk so the commands stay byte-identical.
9
+ *
10
+ * NOTE: this is deliberately CLI-local and distinct from
11
+ * `@shrkcrft/graph`'s `detectGraphFreshness` — that one models the graph
12
+ * store's own freshness and graph sits BELOW cli in the layer order, so it
13
+ * cannot be reused here.
14
+ */
15
+ export declare const FRESHNESS_SKIP_DIRS: Set<string>;
16
+ export declare const FRESHNESS_SOURCE_EXTS: Set<string>;
17
+ export interface IStatusFreshness {
18
+ state: 'fresh' | 'stale' | 'unknown';
19
+ lastBuiltAt: string | null;
20
+ lastChangedAt: string | null;
21
+ behindMs: number;
22
+ }
23
+ /** Newest source-file mtime (ms) under `root`, ignoring build/output dirs. 0
24
+ * when nothing matched. */
25
+ export declare function newestSourceMtimeMs(root: string): number;
26
+ /** Compare a stored build timestamp to the newest source mtime in `scope`.
27
+ * `stale` when a source file is newer than the build; `unknown` when the
28
+ * build timestamp can't be parsed. */
29
+ export declare function computeMtimeFreshness(scope: string, builtAtIso: string | undefined | null): IStatusFreshness;
30
+ //# sourceMappingURL=freshness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../src/status/freshness.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AAEH,eAAO,MAAM,mBAAmB,aAG9B,CAAC;AACH,eAAO,MAAM,qBAAqB,aAGhC,CAAC;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;IACrC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;4BAC4B;AAC5B,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA8BxD;AAED;;uCAEuC;AACvC,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,gBAAgB,CAc5G"}