@shrkcrft/cli 0.1.0-alpha.22 → 0.1.0-alpha.24

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 (108) 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 +139 -7
  14. package/dist/commands/command-catalog.d.ts +11 -0
  15. package/dist/commands/command-catalog.d.ts.map +1 -1
  16. package/dist/commands/command-catalog.js +291 -0
  17. package/dist/commands/commands.command.d.ts +15 -0
  18. package/dist/commands/commands.command.d.ts.map +1 -1
  19. package/dist/commands/commands.command.js +162 -3
  20. package/dist/commands/compress.command.d.ts.map +1 -1
  21. package/dist/commands/compress.command.js +39 -4
  22. package/dist/commands/context.command.d.ts.map +1 -1
  23. package/dist/commands/context.command.js +3 -1
  24. package/dist/commands/daily.commands.d.ts.map +1 -1
  25. package/dist/commands/daily.commands.js +48 -5
  26. package/dist/commands/diff-check.command.d.ts.map +1 -1
  27. package/dist/commands/diff-check.command.js +6 -2
  28. package/dist/commands/feedback.command.d.ts.map +1 -1
  29. package/dist/commands/feedback.command.js +4 -0
  30. package/dist/commands/finish.command.d.ts +3 -0
  31. package/dist/commands/finish.command.d.ts.map +1 -0
  32. package/dist/commands/finish.command.js +70 -0
  33. package/dist/commands/framework.command.d.ts.map +1 -1
  34. package/dist/commands/framework.command.js +20 -3
  35. package/dist/commands/gate.command.d.ts.map +1 -1
  36. package/dist/commands/gate.command.js +98 -13
  37. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  38. package/dist/commands/graph-code-subverbs.js +215 -81
  39. package/dist/commands/graph.command.d.ts.map +1 -1
  40. package/dist/commands/graph.command.js +6 -0
  41. package/dist/commands/help.command.d.ts.map +1 -1
  42. package/dist/commands/help.command.js +20 -2
  43. package/dist/commands/impact.command.d.ts.map +1 -1
  44. package/dist/commands/impact.command.js +86 -1
  45. package/dist/commands/knowledge.command.d.ts.map +1 -1
  46. package/dist/commands/knowledge.command.js +21 -1
  47. package/dist/commands/onboard.command.d.ts.map +1 -1
  48. package/dist/commands/onboard.command.js +11 -2
  49. package/dist/commands/orchestrate.command.d.ts.map +1 -1
  50. package/dist/commands/orchestrate.command.js +6 -1
  51. package/dist/commands/pack-author.command.d.ts.map +1 -1
  52. package/dist/commands/pack-author.command.js +13 -1
  53. package/dist/commands/packs.command.d.ts.map +1 -1
  54. package/dist/commands/packs.command.js +95 -17
  55. package/dist/commands/paths.command.d.ts.map +1 -1
  56. package/dist/commands/paths.command.js +8 -1
  57. package/dist/commands/pipelines.command.d.ts.map +1 -1
  58. package/dist/commands/pipelines.command.js +2 -1
  59. package/dist/commands/plan-context.command.d.ts.map +1 -1
  60. package/dist/commands/plan-context.command.js +6 -3
  61. package/dist/commands/plan-simulate.command.js +1 -1
  62. package/dist/commands/policy-lint.command.d.ts.map +1 -1
  63. package/dist/commands/policy-lint.command.js +20 -7
  64. package/dist/commands/recommend.command.d.ts.map +1 -1
  65. package/dist/commands/recommend.command.js +10 -2
  66. package/dist/commands/registry.command.d.ts.map +1 -1
  67. package/dist/commands/registry.command.js +117 -9
  68. package/dist/commands/report.command.js +1 -1
  69. package/dist/commands/reuse.command.d.ts.map +1 -1
  70. package/dist/commands/reuse.command.js +17 -5
  71. package/dist/commands/review.command.d.ts.map +1 -1
  72. package/dist/commands/review.command.js +20 -2
  73. package/dist/commands/rule-graph-subverbs.d.ts.map +1 -1
  74. package/dist/commands/rule-graph-subverbs.js +31 -4
  75. package/dist/commands/search-structural.command.d.ts.map +1 -1
  76. package/dist/commands/search-structural.command.js +2 -2
  77. package/dist/commands/search.command.d.ts.map +1 -1
  78. package/dist/commands/search.command.js +19 -1
  79. package/dist/commands/simulate.command.d.ts.map +1 -1
  80. package/dist/commands/simulate.command.js +6 -1
  81. package/dist/commands/smart-context.command.d.ts.map +1 -1
  82. package/dist/commands/smart-context.command.js +101 -41
  83. package/dist/commands/spec.command.d.ts.map +1 -1
  84. package/dist/commands/spec.command.js +73 -13
  85. package/dist/commands/task-context.command.d.ts.map +1 -1
  86. package/dist/commands/task-context.command.js +26 -2
  87. package/dist/commands/test.command.d.ts.map +1 -1
  88. package/dist/commands/test.command.js +17 -0
  89. package/dist/commands/trace.command.d.ts.map +1 -1
  90. package/dist/commands/trace.command.js +87 -4
  91. package/dist/commands/wiring.command.d.ts +12 -0
  92. package/dist/commands/wiring.command.d.ts.map +1 -0
  93. package/dist/commands/wiring.command.js +384 -0
  94. package/dist/diff/collect-changed-paths.d.ts +5 -3
  95. package/dist/diff/collect-changed-paths.d.ts.map +1 -1
  96. package/dist/diff/collect-changed-paths.js +73 -36
  97. package/dist/diff/deleted-orphans.d.ts +42 -0
  98. package/dist/diff/deleted-orphans.d.ts.map +1 -0
  99. package/dist/diff/deleted-orphans.js +46 -0
  100. package/dist/finish/run-finish.d.ts +62 -0
  101. package/dist/finish/run-finish.d.ts.map +1 -0
  102. package/dist/finish/run-finish.js +239 -0
  103. package/dist/main.d.ts.map +1 -1
  104. package/dist/main.js +19 -1
  105. package/dist/status/freshness.d.ts +30 -0
  106. package/dist/status/freshness.d.ts.map +1 -0
  107. package/dist/status/freshness.js +81 -0
  108. package/package.json +33 -33
@@ -1,11 +1,12 @@
1
- import { spawn, spawnSync } from 'node:child_process';
1
+ import { spawn } from 'node:child_process';
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import * as nodePath from 'node:path';
4
4
  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
+ import { runGitLines } from '@shrkcrft/shared';
7
8
  import { EdgeKind, GraphQueryApi, GraphStore, NodeKind } from '@shrkcrft/graph';
8
- import { buildProjectOverview, buildTaskPacket, inspectSharkcraft, renderOverviewText, } from '@shrkcrft/inspector';
9
+ import { buildProjectOverview, buildTaskPacket, contextTuningBoostFor, inspectSharkcraft, renderOverviewText, } from '@shrkcrft/inspector';
9
10
  import { flagBool, flagList, flagNumber, flagString, resolveCwd, } from "../command-registry.js";
10
11
  import { DeclarationKind, PLAN_CACHE_SCHEMA, PlanCache, SemanticIndex, TaskType, buildFocusedContext, classifyTask, encodeEmbedding, getDefaultSourceRoots, listIndexableFiles, parseTaskTypeOverride, pruneDeletedHits, renderFocusedContextForPrompt, } from '@shrkcrft/embeddings';
11
12
  import { SmartContextDetailedPlanSchema, SmartContextExpansionRequestSchema, } from "../schemas/json-schemas.js";
@@ -150,6 +151,42 @@ export const smartContextCommand = {
150
151
  }
151
152
  }
152
153
  const enh = enhanced.value.enhancement;
154
+ // Deterministic-always contract (CLAUDE.md): when the enhancement
155
+ // pipeline fully degrades — every stage failed (or none ran), so the
156
+ // pipeline's finalOutput is just the echoed system message (preamble +
157
+ // "## Repository context" + the assembled deterministic seed) — fall
158
+ // back to the clean deterministic seed instead of leaking the raw
159
+ // internal prompt to stdout. This mirrors the single-shot
160
+ // provider-failure fallback below: clean renderSeed(seed) content,
161
+ // finishReason 'deterministic-fallback', no LLM/system-prompt wrapper.
162
+ const systemMsg = messages.find((m) => m.role === AiMessageRole.System);
163
+ const originalContext = (systemMsg?.content ?? '').trim();
164
+ const fullyDegraded = (enh.stages.length > 0 && enh.stages.every((s) => s.degraded)) ||
165
+ (originalContext.length > 0 && enhanced.value.content.trim() === originalContext);
166
+ if (fullyDegraded) {
167
+ if (!opts.json) {
168
+ process.stderr.write('[smart-context] enhancement fully degraded — returning deterministic context only.\n');
169
+ }
170
+ const fallbackEnvelope = buildEnvelope({
171
+ task,
172
+ seed,
173
+ ai: {
174
+ content: renderSeed(seed),
175
+ model: 'deterministic',
176
+ finishReason: 'deterministic-fallback',
177
+ usage: null,
178
+ providerId: 'deterministic',
179
+ },
180
+ mode: opts.mode,
181
+ });
182
+ if (opts.save) {
183
+ const saved = saveEnvelope(cwd, fallbackEnvelope);
184
+ writeSavedNotice(saved, opts.json, fallbackEnvelope);
185
+ return 0;
186
+ }
187
+ writeEnvelope(fallbackEnvelope, opts.json, opts.debug);
188
+ return 0;
189
+ }
153
190
  if (!opts.json && !enh.deterministicFallback) {
154
191
  if (enh.budgetExhausted) {
155
192
  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 +216,9 @@ export const smartContextCommand = {
179
216
  messages,
180
217
  maxTokens: opts.maxTokens,
181
218
  model: opts.model,
219
+ // Bound the single shot so a hung local model degrades to the
220
+ // deterministic seed below instead of hanging the command.
221
+ timeoutMs: perCallTimeoutMs(opts.budgetMs),
182
222
  });
183
223
  if (!aiResult.ok) {
184
224
  // Deterministic-always contract (CLAUDE.md): a provider failure must
@@ -293,6 +333,9 @@ export const smartContextPlanAheadCommand = {
293
333
  messages,
294
334
  maxTokens: opts.maxTokens,
295
335
  model: opts.model,
336
+ // Bound each task's call so one hung model can't stall the queue;
337
+ // a timeout is reported as that task's error and the loop continues.
338
+ timeoutMs: perCallTimeoutMs(opts.budgetMs),
296
339
  });
297
340
  if (!aiResult.ok) {
298
341
  results.push({ task, status: 'error', error: aiResult.error.message, slug: slug(task) });
@@ -1418,6 +1461,7 @@ async function buildSmartContextSeed(input) {
1418
1461
  task,
1419
1462
  maxTokens: options.seedTokens,
1420
1463
  projectOverview: overviewText,
1464
+ boostFor: contextTuningBoostFor(inspection, task),
1421
1465
  });
1422
1466
  const graphGrounding = buildInitialGraphGrounding(cwd, task);
1423
1467
  const semantic = await tryLoadSemanticHits(cwd, task, 10, options);
@@ -2139,45 +2183,21 @@ function isSemanticAutomationDisabled() {
2139
2183
  * ripples through. Empty array on git failure or no-graph.
2140
2184
  */
2141
2185
  function collectChangedPathsWithNeighbors(cwd, gitRef) {
2142
- // Use `node:child_process` spawnSync (works under both Bun and Node)
2143
- // instead of `Bun.spawnSync` so the CLI runs cleanly on a pure-Node
2144
- // runtime after `npm i -g @shrkcrft/cli`. The compat-node preflight
2145
- // gate flags `Bun.*` direct usages as publish blockers.
2146
- let changed;
2147
- try {
2148
- const out = spawnSync('git', ['-C', cwd, 'diff', '--name-only', `${gitRef}...HEAD`], {
2149
- encoding: 'utf8',
2150
- });
2151
- if (out.status !== 0)
2152
- return [];
2153
- changed = (out.stdout ?? '')
2154
- .split('\n')
2155
- .map((s) => s.trim())
2156
- .filter((s) => s.length > 0);
2157
- }
2158
- catch {
2159
- return [];
2160
- }
2161
- if (changed.length === 0)
2186
+ // `runGitLines` is shell-free + high-maxBuffer, so a large changeset can't
2187
+ // ENOBUFS-crash the feedback bundle. (It also keeps the CLI on
2188
+ // `node:child_process` rather than `Bun.*`, which the compat-node preflight
2189
+ // gate flags as a publish blocker.)
2190
+ const committed = runGitLines(cwd, ['diff', '--name-only', `${gitRef}...HEAD`]);
2191
+ if (!committed.ok || committed.lines.length === 0)
2162
2192
  return [];
2193
+ const changed = [...committed.lines];
2163
2194
  // Also include uncommitted changes — agent feedback should reflect
2164
2195
  // the *current* working tree, not just committed deltas.
2165
- try {
2166
- const out = spawnSync('git', ['-C', cwd, 'diff', '--name-only', 'HEAD'], {
2167
- encoding: 'utf8',
2168
- });
2169
- if (out.status === 0) {
2170
- const uncommitted = (out.stdout ?? '')
2171
- .split('\n')
2172
- .map((s) => s.trim())
2173
- .filter((s) => s.length > 0);
2174
- for (const p of uncommitted)
2175
- if (!changed.includes(p))
2176
- changed.push(p);
2177
- }
2178
- }
2179
- catch {
2180
- // ignore
2196
+ const uncommitted = runGitLines(cwd, ['diff', '--name-only', 'HEAD']);
2197
+ if (uncommitted.ok) {
2198
+ for (const p of uncommitted.lines)
2199
+ if (!changed.includes(p))
2200
+ changed.push(p);
2181
2201
  }
2182
2202
  const set = new Set(changed);
2183
2203
  // One-hop graph expansion if the graph is fresh.
@@ -2693,6 +2713,23 @@ function truncateLine(text, max) {
2693
2713
  const PER_STAGE_TIMEOUT_MS = 90_000;
2694
2714
  const FAST_ENHANCE_BUDGET_MS = 150_000;
2695
2715
  const PLUS_ENHANCE_BUDGET_MS = 360_000;
2716
+ /**
2717
+ * Per-call wall-clock bounds for the paths that DON'T run through
2718
+ * `runEnhancementPipeline` (single-shot brief, plan-ahead, and the two
2719
+ * `--ai-plan` stages). Without these, `--budget` only constrained the
2720
+ * enhancement pipeline, so a hung local model could still hang those modes.
2721
+ *
2722
+ * - `SINGLE_CALL_DEFAULT_BUDGET_MS` — the cap applied when no `--budget` is
2723
+ * given (mirrors the fast-enhance ceiling).
2724
+ * - `PER_CALL_TIMEOUT_CAP_MS` — an absolute ceiling, so even a generous
2725
+ * `--budget` can't let a single call run unbounded.
2726
+ */
2727
+ const SINGLE_CALL_DEFAULT_BUDGET_MS = 150_000;
2728
+ const PER_CALL_TIMEOUT_CAP_MS = 360_000;
2729
+ /** Derive a per-call timeout (ms) from the optional `--budget` (budgetMs). */
2730
+ function perCallTimeoutMs(budgetMs) {
2731
+ return Math.min(budgetMs ?? SINGLE_CALL_DEFAULT_BUDGET_MS, PER_CALL_TIMEOUT_CAP_MS);
2732
+ }
2696
2733
  async function runEnhancementPipeline(input) {
2697
2734
  const provider = input.provider;
2698
2735
  const systemMsg = input.messages.find((m) => m.role === AiMessageRole.System);
@@ -2830,6 +2867,7 @@ async function callProvider(input) {
2830
2867
  ...(input.model ? { model: input.model } : {}),
2831
2868
  ...(input.responseFormat ? { responseFormat: input.responseFormat } : {}),
2832
2869
  ...(input.onTokenStream ? { onTokenStream: input.onTokenStream } : {}),
2870
+ ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
2833
2871
  });
2834
2872
  if (!res.ok || !res.value)
2835
2873
  return { ok: false, error: res.error };
@@ -2927,10 +2965,23 @@ function writeDryRun(messages, mode, provider) {
2927
2965
  }
2928
2966
  }
2929
2967
  function displayProviderName(explicit) {
2930
- if (explicit)
2931
- return explicit;
2968
+ // Mirror provider-resolver.ts `normaliseKind` so the --dry-run header names
2969
+ // the provider that will ACTUALLY run, not a stale 'auto'. An explicit
2970
+ // `auto` is a local-first request that AI_PROVIDER never overrides; with no
2971
+ // --provider, AI_PROVIDER (including a hosted claude/gemini) decides what
2972
+ // runs, so the header must say so truthfully.
2973
+ if (explicit !== undefined) {
2974
+ const e = explicit.trim().toLowerCase();
2975
+ if (e === 'auto')
2976
+ return 'auto';
2977
+ if (e === 'claude' || e === 'gemini' || e === 'ollama' || e === 'llamacpp')
2978
+ return e;
2979
+ }
2932
2980
  const envProvider = (process.env.AI_PROVIDER ?? '').trim().toLowerCase();
2933
- if (envProvider === 'ollama' || envProvider === 'llamacpp') {
2981
+ if (envProvider === 'ollama' ||
2982
+ envProvider === 'llamacpp' ||
2983
+ envProvider === 'claude' ||
2984
+ envProvider === 'gemini') {
2934
2985
  return envProvider;
2935
2986
  }
2936
2987
  return 'auto';
@@ -3212,6 +3263,11 @@ async function buildAiPlanEnvelope(input) {
3212
3263
  }
3213
3264
  progressMarker(`preflight ok — host=${preflight.value.host} models=${preflight.value.models.length}`, input.options);
3214
3265
  }
3266
+ // Split the (optional) --budget across the two stages so a hung local
3267
+ // model can't hang the ai-plan path the way it previously could. Each
3268
+ // call is independently bounded; a TIMEOUT surfaces as a call-failure and
3269
+ // is handled exactly like any other provider failure below.
3270
+ const aiPlanStageTimeoutMs = Math.min(Math.floor((input.options.budgetMs ?? SINGLE_CALL_DEFAULT_BUDGET_MS) / 2), PER_CALL_TIMEOUT_CAP_MS);
3215
3271
  progressMarker(`stage 1 calling ${selection.provider.id}${input.options.model ? `:${input.options.model}` : ''}…`, input.options);
3216
3272
  const stage1Messages = buildStage1Messages(input.seed, grounding, cacheLookup.reference);
3217
3273
  logPromptToStderr('stage1', stage1Messages, input.options);
@@ -3229,6 +3285,7 @@ async function buildAiPlanEnvelope(input) {
3229
3285
  repromptInstruction: STAGE1_REPROMPT,
3230
3286
  stageLabel: 'stage 1',
3231
3287
  options: input.options,
3288
+ timeoutMs: aiPlanStageTimeoutMs,
3232
3289
  });
3233
3290
  let stage1Request;
3234
3291
  let stage1Retried = false;
@@ -3275,6 +3332,7 @@ async function buildAiPlanEnvelope(input) {
3275
3332
  repromptInstruction: STAGE2_REPROMPT,
3276
3333
  stageLabel: 'stage 2',
3277
3334
  options: input.options,
3335
+ timeoutMs: aiPlanStageTimeoutMs,
3278
3336
  });
3279
3337
  const conversationTurns = [];
3280
3338
  if (stage1Call) {
@@ -3424,6 +3482,7 @@ async function callProviderWithRetry(input) {
3424
3482
  maxTokens: input.maxTokens,
3425
3483
  ...(input.model ? { model: input.model } : {}),
3426
3484
  ...(input.responseFormat ? { responseFormat: input.responseFormat } : {}),
3485
+ ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
3427
3486
  });
3428
3487
  if (!first.ok)
3429
3488
  return { kind: 'call-failed', error: first.error };
@@ -3449,6 +3508,7 @@ async function callProviderWithRetry(input) {
3449
3508
  maxTokens: input.maxTokens,
3450
3509
  ...(input.model ? { model: input.model } : {}),
3451
3510
  ...(input.responseFormat ? { responseFormat: input.responseFormat } : {}),
3511
+ ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
3452
3512
  });
3453
3513
  if (!second.ok) {
3454
3514
  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":"AAmBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA6EhC,eAAO,MAAM,YAAY,EAAE,eAwH1B,CAAC"}
@@ -6,16 +6,86 @@
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";
10
- import { asJson, header } from "../output/format-output.js";
9
+ import { traceLiteral, TraceRole, } from '@shrkcrft/boundaries';
10
+ import { flagBool, flagList, flagNumber, flagString, resolveCwd, } from "../command-registry.js";
11
+ import { asJson, header, kv } from "../output/format-output.js";
11
12
  function describeMatch(m) {
12
13
  return `${m.kind.padEnd(12)} ${m.id}${m.label && m.label !== m.id ? ` — ${m.label}` : ''} [${m.score.toFixed(0)}]`;
13
14
  }
15
+ const LITERAL_SITE_CAP = 30;
16
+ const TRACE_ROLE_ORDER = [
17
+ TraceRole.Declare,
18
+ TraceRole.Register,
19
+ TraceRole.Consume,
20
+ TraceRole.Reference,
21
+ ];
22
+ function renderTraceLiteral(report, limit) {
23
+ process.stdout.write(header(`Trace literal: "${report.literal}"`));
24
+ process.stdout.write(kv('found', `${report.total} site(s) across ${report.files} file(s)`) + '\n');
25
+ if (report.aliases.length > 0) {
26
+ process.stdout.write(kv('const aliases', report.aliases.join(', ')) + '\n');
27
+ }
28
+ if (report.total === 0) {
29
+ process.stdout.write('\nNo occurrences of that exact string literal in scope.\n');
30
+ return;
31
+ }
32
+ // Mirror `shrk registry <name> where`'s `<role> file:line` line idiom so the
33
+ // two surfaces read the same — `trace literal` is the same scanner + classifier
34
+ // without a pre-declared registry, just with two extra roles (`registered`,
35
+ // `reference`). The raw source line stays in `--json` (`text`); the human view
36
+ // is classification-first (direction), not a grep-style text dump.
37
+ process.stdout.write('\n');
38
+ for (const role of TRACE_ROLE_ORDER) {
39
+ const sites = report.byRole[role];
40
+ for (const s of sites.slice(0, limit)) {
41
+ const alias = s.viaAlias ? ` [via ${s.viaAlias}]` : '';
42
+ process.stdout.write(` ${role.padEnd(10)} ${s.file}:${s.line}${alias}\n`);
43
+ }
44
+ if (sites.length > limit) {
45
+ process.stdout.write(` ${role.padEnd(10)} … (${sites.length - limit} more — pass --json)\n`);
46
+ }
47
+ }
48
+ }
49
+ /**
50
+ * `shrk trace literal "<string>"`: trace an EXACT string literal across the
51
+ * codebase, classified by direction (declare → register → consume), resolving
52
+ * `const X = "lit"` aliases too. Generalizes `registry … where` to any
53
+ * cross-fence string contract (a kind slug, permission id, route key) with no
54
+ * pre-declared registry — the chain grep can't give (direction + role + layer).
55
+ */
56
+ function runTraceLiteral(args) {
57
+ const cwd = resolveCwd(args);
58
+ const wantJson = flagBool(args, 'json');
59
+ // `trace literal "<lit>"` → the literal is positional[1] (quoted by the user
60
+ // so an internal space stays one token); join the rest defensively.
61
+ const literal = args.positional.slice(1).join(' ');
62
+ if (literal === '') {
63
+ process.stderr.write('Usage: shrk trace literal "<string>" [--glob <g1,g2>] [--no-aliases] [--limit N] [--json]\n');
64
+ return 2;
65
+ }
66
+ const globs = flagList(args, 'glob');
67
+ const report = traceLiteral(cwd, literal, {
68
+ ...(globs.length > 0 ? { globs } : {}),
69
+ resolveAliases: !flagBool(args, 'no-aliases'),
70
+ });
71
+ if (wantJson) {
72
+ process.stdout.write(asJson(report) + '\n');
73
+ return 0;
74
+ }
75
+ const limitRaw = flagNumber(args, 'limit');
76
+ const limit = limitRaw !== undefined && limitRaw > 0 ? Math.floor(limitRaw) : LITERAL_SITE_CAP;
77
+ renderTraceLiteral(report, limit);
78
+ return 0;
79
+ }
14
80
  export const traceCommand = {
15
81
  name: 'trace',
16
- description: 'Fuzzy trace — accept any free-form query (file path, construct id, symbol, plugin key, helper id, template id, knowledge id, command). Read-only.',
17
- usage: 'shrk trace <query> [--limit <n>] [--kind file|construct|knowledge|template|helper|playbook|policy|command] [--deep] [--json]',
82
+ description: 'Fuzzy trace — accept any free-form query (file path, construct id, symbol, plugin key, helper id, template id, knowledge id, command). `trace literal "<string>"` traces an exact string literal\'s declare→register→consume chain across layers (alias-resolved). Read-only.',
83
+ usage: 'shrk trace <query> | shrk trace literal "<string>" [--glob <g>] [--no-aliases] [--limit <n>] [--kind ...] [--deep] [--json]',
84
+ booleanFlags: new Set(['json', 'deep', 'no-aliases']),
18
85
  async run(args) {
86
+ // `trace literal "<lit>"` — cross-fence string-contract tracer (3.3).
87
+ if (args.positional[0] === 'literal')
88
+ return runTraceLiteral(args);
19
89
  // Direct symbol trace via --symbol <Name>
20
90
  const symbol = flagString(args, 'symbol');
21
91
  if (symbol) {
@@ -58,10 +128,23 @@ export const traceCommand = {
58
128
  process.stderr.write('Usage: shrk trace <query> | shrk trace --symbol <Name>\n');
59
129
  return 2;
60
130
  }
131
+ // `--kind` restricts the result set to specific match kinds — the engine
132
+ // (resolveQuery) filters by them. Validate up front so an unknown kind
133
+ // fails fast with the valid list, instead of being silently ignored (which
134
+ // returned a wrong-kind best match, e.g. a knowledge hit for
135
+ // `--kind template`).
136
+ const kinds = flagList(args, 'kind');
137
+ const validKinds = new Set(Object.values(QueryMatchKind));
138
+ const unknownKind = kinds.find((k) => !validKinds.has(k));
139
+ if (unknownKind) {
140
+ process.stderr.write(`trace: unknown --kind "${unknownKind}". Valid kinds: ${[...validKinds].sort().join(', ')}.\n`);
141
+ return 2;
142
+ }
61
143
  const cwd = resolveCwd(args);
62
144
  const inspection = await inspectSharkcraft({ cwd });
63
145
  const limit = flagNumber(args, 'limit');
64
146
  const resolution = resolveQuery(inspection, query, {
147
+ ...(kinds.length > 0 ? { kinds: kinds } : {}),
65
148
  ...(typeof limit === 'number' ? { limit } : {}),
66
149
  });
67
150
  if (flagBool(args, 'json')) {
@@ -0,0 +1,12 @@
1
+ import { type IWiringExplain } from '@shrkcrft/boundaries';
2
+ import { type ICommandHandler } from '../command-registry.js';
3
+ /**
4
+ * Render an {@link IWiringExplain} to stdout. Shared by `wiring explain`,
5
+ * `wiring test`, and `check wiring --explain` so all three speak the same
6
+ * dialect. JSON emits the full payload; text mirrors `search tuning explain`
7
+ * (header → loaded sets → per-site detail → the set-difference → verdict).
8
+ * Always returns 0 — explain is informational, the verdict is in the output.
9
+ */
10
+ export declare function renderWiringExplain(report: IWiringExplain, wantJson: boolean): number;
11
+ export declare const wiringCommand: ICommandHandler;
12
+ //# sourceMappingURL=wiring.command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wiring.command.d.ts","sourceRoot":"","sources":["../../src/commands/wiring.command.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAM9B,OAAO,EAAwB,KAAK,eAAe,EAAmB,MAAM,wBAAwB,CAAC;AAKrG;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAsDrF;AA+TD,eAAO,MAAM,aAAa,EAAE,eAgB3B,CAAC"}