@gobing-ai/spur 0.3.65 → 0.3.66

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.
package/spur.js CHANGED
@@ -39046,6 +39046,7 @@ var init_finding_codes = __esm(() => {
39046
39046
  "L3.scope-delineation",
39047
39047
  "L3.one-active-goal",
39048
39048
  "L3.children-limit",
39049
+ "L3.review-testing-contradiction",
39049
39050
  "L4.design-placeholder",
39050
39051
  "L4.feature-not-found",
39051
39052
  "L4.feature-terminal",
@@ -39055,6 +39056,7 @@ var init_finding_codes = __esm(() => {
39055
39056
  "L4.rollup-subtasks-open",
39056
39057
  "L4.rollup-parent-open",
39057
39058
  "L4.rollup-missing-roster",
39059
+ "L4.rollup-roster-not-declared-dependency",
39058
39060
  "L4.readiness-blocked",
39059
39061
  "L4.prose-prerequisite-unlisted",
39060
39062
  "L4.prerequisite-cycle",
@@ -39064,6 +39066,7 @@ var init_finding_codes = __esm(() => {
39064
39066
  "L4.orphan-scenarios",
39065
39067
  "L4.uncovered-task-scenario",
39066
39068
  "L4.uncovered-feature-scenario",
39069
+ "L4.verdict-rows-match-no-scenario",
39067
39070
  "L4.verifying-incomplete-tasks",
39068
39071
  "L4.dogfood-missing",
39069
39072
  "L4.stale-line-anchor",
@@ -39098,6 +39101,7 @@ var init_finding_codes = __esm(() => {
39098
39101
  L3_SCOPE_DELINEATION: "L3.scope-delineation",
39099
39102
  L3_ONE_ACTIVE_GOAL: "L3.one-active-goal",
39100
39103
  L3_CHILDREN_LIMIT: "L3.children-limit",
39104
+ L3_REVIEW_TESTING_CONTRADICTION: "L3.review-testing-contradiction",
39101
39105
  L4_DESIGN_PLACEHOLDER: "L4.design-placeholder",
39102
39106
  L4_FEATURE_NOT_FOUND: "L4.feature-not-found",
39103
39107
  L4_FEATURE_TERMINAL: "L4.feature-terminal",
@@ -39107,6 +39111,7 @@ var init_finding_codes = __esm(() => {
39107
39111
  L4_ROLLUP_SUBTASKS_OPEN: "L4.rollup-subtasks-open",
39108
39112
  L4_ROLLUP_PARENT_OPEN: "L4.rollup-parent-open",
39109
39113
  L4_ROLLUP_MISSING_ROSTER: "L4.rollup-missing-roster",
39114
+ L4_ROLLUP_ROSTER_NOT_DECLARED_DEPENDENCY: "L4.rollup-roster-not-declared-dependency",
39110
39115
  L4_READINESS_BLOCKED: "L4.readiness-blocked",
39111
39116
  L4_PROSE_PREREQUISITE_UNLISTED: "L4.prose-prerequisite-unlisted",
39112
39117
  L4_PREREQUISITE_CYCLE: "L4.prerequisite-cycle",
@@ -39116,6 +39121,7 @@ var init_finding_codes = __esm(() => {
39116
39121
  L4_ORPHAN_SCENARIOS: "L4.orphan-scenarios",
39117
39122
  L4_UNCOVERED_TASK_SCENARIO: "L4.uncovered-task-scenario",
39118
39123
  L4_UNCOVERED_FEATURE_SCENARIO: "L4.uncovered-feature-scenario",
39124
+ L4_VERDICT_ROWS_MATCH_NO_SCENARIO: "L4.verdict-rows-match-no-scenario",
39119
39125
  L4_DOGFOOD_MISSING: "L4.dogfood-missing",
39120
39126
  L4_VERIFYING_INCOMPLETE_TASKS: "L4.verifying-incomplete-tasks",
39121
39127
  L4_STALE_LINE_ANCHOR: "L4.stale-line-anchor",
@@ -50346,6 +50352,42 @@ var init_artifact_digest = __esm(() => {
50346
50352
  RANKED_ARTIFACT_KEYS = new Set(Object.entries(ARTIFACT_ARRAY_CLASSIFICATION).filter(([, kind]) => kind === "ranked").map(([key]) => key));
50347
50353
  });
50348
50354
 
50355
+ // ../../packages/domain/src/analytics/assistant-duration.ts
50356
+ async function deriveAssistantDurations(db2) {
50357
+ const candidateSql = `
50358
+ WITH ordered AS (
50359
+ SELECT record_hash,
50360
+ role,
50361
+ duration_ms,
50362
+ ts,
50363
+ LAG(ts) OVER (PARTITION BY source, session_id ORDER BY seq) AS prev_ts
50364
+ FROM history_message
50365
+ WHERE ts IS NOT NULL AND ts LIKE '____-__-__T%'
50366
+ )
50367
+ SELECT record_hash AS recordHash,
50368
+ CAST(ROUND((unixepoch(ts, 'subsec') - unixepoch(prev_ts, 'subsec')) * 1000) AS INTEGER) AS deltaMs
50369
+ FROM ordered
50370
+ WHERE role = 'assistant' AND duration_ms IS NULL AND prev_ts IS NOT NULL`;
50371
+ const rows = await db2.queryAll(candidateSql);
50372
+ let derived = 0;
50373
+ let skippedOverCeiling = 0;
50374
+ for (const row of rows) {
50375
+ if (row.deltaMs === null || row.deltaMs <= 0)
50376
+ continue;
50377
+ if (row.deltaMs > DERIVED_DURATION_CEILING_MS) {
50378
+ skippedOverCeiling += 1;
50379
+ continue;
50380
+ }
50381
+ await db2.run("UPDATE history_message SET duration_ms = ?, duration_source = ? WHERE record_hash = ? AND duration_ms IS NULL", row.deltaMs, DURATION_SOURCE_DERIVED, row.recordHash);
50382
+ derived += 1;
50383
+ }
50384
+ return { derived, skippedOverCeiling };
50385
+ }
50386
+ var DURATION_SOURCE_DERIVED = "derived", DERIVED_DURATION_CEILING_MS;
50387
+ var init_assistant_duration = __esm(() => {
50388
+ DERIVED_DURATION_CEILING_MS = 30 * 60 * 1000;
50389
+ });
50390
+
50349
50391
  // ../../packages/domain/src/analytics/models.ts
50350
50392
  function resolvePricing(model) {
50351
50393
  if (model === undefined || model.length === 0)
@@ -51083,6 +51125,7 @@ async function stepSupport(db2, sel, opts) {
51083
51125
  COUNT(*) AS assistantSteps,
51084
51126
  SUM(m.input_tokens IS NOT NULL OR m.output_tokens IS NOT NULL) AS stepsWithUsage,
51085
51127
  SUM(m.duration_ms IS NOT NULL) AS stepsWithDuration,
51128
+ SUM(m.duration_source IS 'derived') AS stepsWithDerivedDuration,
51086
51129
  SUM(m.cache_read_tokens IS NOT NULL) AS stepsWithCacheRead
51087
51130
  FROM history_message m
51088
51131
  ${withStepPredicates(wm.where, "m.role = 'assistant'")}
@@ -52190,7 +52233,7 @@ async function pairingSummary(db2, spec = {}) {
52190
52233
  failures: d.failures,
52191
52234
  unknownOutcomes: d.dispatches - known,
52192
52235
  escalations: {},
52193
- totalCostUsd: 0,
52236
+ totalCostUsd: null,
52194
52237
  meanDurationMs: d.durationCount > 0 ? d.durationTotal / d.durationCount : 0
52195
52238
  });
52196
52239
  }
@@ -52204,7 +52247,7 @@ async function pairingSummary(db2, spec = {}) {
52204
52247
  const entry = byKey.get(pairingKey(f.executor, f.role));
52205
52248
  if (entry === undefined)
52206
52249
  continue;
52207
- entry.totalCostUsd += f.totalCostUsd;
52250
+ entry.totalCostUsd = entry.totalCostUsd === null ? f.totalCostUsd : f.totalCostUsd === null ? entry.totalCostUsd : entry.totalCostUsd + f.totalCostUsd;
52208
52251
  }
52209
52252
  return [...byKey.values()].sort((a2, b) => a2.executor.localeCompare(b.executor) || a2.role.localeCompare(b.role));
52210
52253
  }
@@ -52346,7 +52389,7 @@ async function loadFolds(db2, spec) {
52346
52389
  WHERE m.session_id IS NOT NULL
52347
52390
  )
52348
52391
  SELECT m.executor AS executor, m.role AS role,
52349
- COALESCE(SUM(h.cost_usd), 0) AS totalCostUsd
52392
+ SUM(h.cost_usd) AS totalCostUsd
52350
52393
  FROM mapped m
52351
52394
  LEFT JOIN history_message h
52352
52395
  ON h.source = m.source AND h.session_id = m.session_id
@@ -52658,7 +52701,7 @@ function renderPerStep(artifact) {
52658
52701
  if ((artifact.stepSupport?.length ?? 0) > 0) {
52659
52702
  lines.push("### Section Support", "", "| Source | Assistant steps | Tokens | Time | Cache |", "| --- | ---: | --- | --- | --- |");
52660
52703
  for (const e of artifact.stepSupport ?? []) {
52661
- lines.push(`| ${e.source} | ${e.assistantSteps.toLocaleString("en-US")} | ${e.stepsWithUsage > 0 ? "yes" : "no"} | ${e.stepsWithDuration > 0 ? "yes" : "no"} | ${e.stepsWithCacheRead > 0 ? "yes" : "no"} |`);
52704
+ lines.push(`| ${e.source} | ${e.assistantSteps.toLocaleString("en-US")} | ${e.stepsWithUsage > 0 ? "yes" : "no"} | ${timeSupport(e)} | ${e.stepsWithCacheRead > 0 ? "yes" : "no"} |`);
52662
52705
  }
52663
52706
  lines.push("");
52664
52707
  }
@@ -52691,6 +52734,15 @@ function renderTopStepsByTokens(artifact) {
52691
52734
  lines.push("");
52692
52735
  return lines;
52693
52736
  }
52737
+ function timeSupport(e) {
52738
+ if (e.stepsWithDuration === 0)
52739
+ return "no";
52740
+ if (e.stepsWithDerivedDuration === 0)
52741
+ return "yes";
52742
+ if (e.stepsWithDerivedDuration >= e.stepsWithDuration)
52743
+ return "derived";
52744
+ return "yes (mixed)";
52745
+ }
52694
52746
  function renderTopStepsByDuration(artifact) {
52695
52747
  const lines = ["### Top Steps by Duration", ""];
52696
52748
  if (artifact.topStepsByDuration === undefined) {
@@ -52852,7 +52904,16 @@ function renderPairings(artifact) {
52852
52904
  `);
52853
52905
  }
52854
52906
  function comparePairings(a2, b) {
52855
- return b.successRate - a2.successRate || totalEscalations(a2) - totalEscalations(b) || a2.totalCostUsd - b.totalCostUsd;
52907
+ return b.successRate - a2.successRate || totalEscalations(a2) - totalEscalations(b) || compareCost(a2.totalCostUsd, b.totalCostUsd);
52908
+ }
52909
+ function compareCost(a2, b) {
52910
+ if (a2 === null && b === null)
52911
+ return 0;
52912
+ if (a2 === null)
52913
+ return 1;
52914
+ if (b === null)
52915
+ return -1;
52916
+ return a2 - b;
52856
52917
  }
52857
52918
  function renderPairingsSection(artifact) {
52858
52919
  const lines = ["## Pairings", ""];
@@ -52895,11 +52956,11 @@ function measureExecutor(entry, pairings) {
52895
52956
  dispatches,
52896
52957
  successRate: dispatches > 0 ? successes / dispatches : 0,
52897
52958
  escalations: owned.reduce((sum, p) => sum + totalEscalations(p), 0),
52898
- costUsd: owned.reduce((sum, p) => sum + p.totalCostUsd, 0)
52959
+ costUsd: owned.reduce((sum, p) => sum === null ? p.totalCostUsd : p.totalCostUsd === null ? sum : sum + p.totalCostUsd, null)
52899
52960
  };
52900
52961
  }
52901
52962
  function compareMeasured(a2, b) {
52902
- return b.successRate - a2.successRate || a2.escalations - b.escalations || a2.costUsd - b.costUsd;
52963
+ return b.successRate - a2.successRate || a2.escalations - b.escalations || compareCost(a2.costUsd, b.costUsd);
52903
52964
  }
52904
52965
  function renderLadderDiffSection(artifact) {
52905
52966
  const lines = ["## Ladder diff", ""];
@@ -52945,7 +53006,7 @@ function pct(rate) {
52945
53006
  return `${(rate * 100).toFixed(1)}%`;
52946
53007
  }
52947
53008
  function usd(cost) {
52948
- return `$${cost.toFixed(2)}`;
53009
+ return cost === null ? "not available" : `$${cost.toFixed(2)}`;
52949
53010
  }
52950
53011
  function fmtDuration(ms) {
52951
53012
  return ms > 0 ? fmtDur(ms) : "n/a";
@@ -53637,6 +53698,7 @@ var init_run_cost = __esm(() => {
53637
53698
  var init_analytics = __esm(() => {
53638
53699
  init_artifact();
53639
53700
  init_artifact_digest();
53701
+ init_assistant_duration();
53640
53702
  init_costs();
53641
53703
  init_forensic_query();
53642
53704
  init_models();
@@ -53669,7 +53731,7 @@ function parseChecklist(content) {
53669
53731
  const checked = (match[1] ?? "").toLowerCase() === "x";
53670
53732
  const rawText = (match[2] ?? "").trim();
53671
53733
  const line = i2 + 1;
53672
- const reqIdMatch = rawText.match(/^(R\d+)\s*[:\-\u2014]?\s*(.*)$/);
53734
+ const reqIdMatch = rawText.match(/^[*_]{0,2}\s*((?:AC|R)\d+)\.?[*_]{0,2}\s*[:\-\u2014]?\s*(.*)$/);
53673
53735
  if (reqIdMatch) {
53674
53736
  items.push({
53675
53737
  text: (reqIdMatch[2] ?? "").trim(),
@@ -53857,6 +53919,20 @@ function stripScenarioPrefixes(title) {
53857
53919
  function normalizeTitle(title) {
53858
53920
  return stripScenarioPrefixes(title).replace(/^(R\d+)\s*[:\-\u2014]?\s*/, "").trim().toLowerCase().replace(/[\u0027\u2018\u2019\u201c\u201d]/g, "").replace(/\s+/g, " ").trim();
53859
53921
  }
53922
+ function stripCoversClause(text3) {
53923
+ return text3.replace(new RegExp(COVERS_RE_SOURCE, "gi"), " ").replace(/\s+/g, " ").trim();
53924
+ }
53925
+ function extractCoversAliases(text3) {
53926
+ const out = [];
53927
+ for (const m of text3.matchAll(new RegExp(COVERS_RE_SOURCE, "gi"))) {
53928
+ for (const part of (m[1] ?? "").split(";")) {
53929
+ const alias3 = part.trim();
53930
+ if (alias3.length > 0)
53931
+ out.push(alias3);
53932
+ }
53933
+ }
53934
+ return out;
53935
+ }
53860
53936
  function checkAcCoverage(featureAc, taskAc, taskChecklist, acAltitude) {
53861
53937
  if (acAltitude === "task-local") {
53862
53938
  return { covered: true, orphans: [], uncovered: [], issues: [] };
@@ -53868,7 +53944,10 @@ function checkAcCoverage(featureAc, taskAc, taskChecklist, acAltitude) {
53868
53944
  const checklistTitles = new Set;
53869
53945
  if (taskChecklist) {
53870
53946
  for (const item of taskChecklist) {
53871
- checklistTitles.add(normalizeTitle(item.text));
53947
+ checklistTitles.add(normalizeTitle(stripCoversClause(item.text)));
53948
+ for (const alias3 of extractCoversAliases(item.text)) {
53949
+ checklistTitles.add(normalizeTitle(alias3));
53950
+ }
53872
53951
  }
53873
53952
  }
53874
53953
  const uncovered = [];
@@ -53886,6 +53965,15 @@ function checkAcCoverage(featureAc, taskAc, taskChecklist, acAltitude) {
53886
53965
  }
53887
53966
  if (taskChecklist) {
53888
53967
  for (const item of taskChecklist) {
53968
+ const aliases = extractCoversAliases(item.text);
53969
+ if (aliases.length > 0) {
53970
+ for (const alias3 of aliases) {
53971
+ if (!featureTitles.has(normalizeTitle(alias3))) {
53972
+ uncovered.push(item.text);
53973
+ }
53974
+ }
53975
+ continue;
53976
+ }
53889
53977
  const normalized = normalizeTitle(item.text);
53890
53978
  if (!featureTitles.has(normalized) && !taskTitles.has(normalized)) {
53891
53979
  uncovered.push(item.text);
@@ -53933,6 +54021,7 @@ function tryParseFeature(content) {
53933
54021
  ${content}`;
53934
54022
  return parseFeature(toParse);
53935
54023
  }
54024
+ var COVERS_RE_SOURCE = "\\(covers:\\s*([^)]*)\\)";
53936
54025
  var init_coverage = () => {};
53937
54026
 
53938
54027
  // ../../packages/domain/src/bdd/fence.ts
@@ -64318,6 +64407,11 @@ ${HISTORY_RUN_SESSION_SCHEMA_SQL}
64318
64407
  id: "0025_spur_cli_history_checkpoint_identity_mtime",
64319
64408
  sql: "ALTER TABLE history_import_checkpoint ADD COLUMN source_mtime_ms REAL",
64320
64409
  addColumnIfMissing: { table: "history_import_checkpoint", column: "source_mtime_ms" }
64410
+ },
64411
+ {
64412
+ id: "0026_spur_cli_history_message_duration_source",
64413
+ sql: "ALTER TABLE history_message ADD COLUMN duration_source TEXT",
64414
+ addColumnIfMissing: { table: "history_message", column: "duration_source" }
64321
64415
  }
64322
64416
  ];
64323
64417
  });
@@ -65925,7 +66019,7 @@ class MarkdownDocument {
65925
66019
  _frontmatter;
65926
66020
  _preamble;
65927
66021
  _sections;
65928
- _strippedHeadings = [];
66022
+ _demotedHeadings = [];
65929
66023
  _duplicateSectionNames = [];
65930
66024
  constructor(domain2, frontmatterBlock, frontmatter, preamble, sections) {
65931
66025
  this._domain = domain2;
@@ -66008,8 +66102,8 @@ class MarkdownDocument {
66008
66102
  get sectionNames() {
66009
66103
  return this._sections.map((s2) => s2.name);
66010
66104
  }
66011
- get strippedHeadings() {
66012
- return this._strippedHeadings;
66105
+ get demotedHeadings() {
66106
+ return this._demotedHeadings;
66013
66107
  }
66014
66108
  get duplicateSectionNames() {
66015
66109
  return this._duplicateSectionNames;
@@ -66067,7 +66161,8 @@ ${withTrailer}`;
66067
66161
  continue;
66068
66162
  }
66069
66163
  if (!inCodeBlock && line.startsWith(prefix)) {
66070
- this._strippedHeadings.push(line);
66164
+ this._demotedHeadings.push(line);
66165
+ kept.push(`#${line}`);
66071
66166
  continue;
66072
66167
  }
66073
66168
  kept.push(line);
@@ -66978,6 +67073,7 @@ __export(exports_src, {
66978
67073
  drift: () => drift,
66979
67074
  deterministicExecutionSchema: () => deterministicExecutionSchema,
66980
67075
  derivedWarnings: () => derivedWarnings,
67076
+ deriveAssistantDurations: () => deriveAssistantDurations,
66981
67077
  dbHealthCheck: () => dbHealthCheck,
66982
67078
  dataWindow: () => dataWindow,
66983
67079
  dailyTokenMatrix: () => dailyTokenMatrix,
@@ -67087,7 +67183,9 @@ __export(exports_src, {
67087
67183
  FEATURE_ID_PATTERN: () => FEATURE_ID_PATTERN,
67088
67184
  FEATURE_CANONICAL_SECTIONS: () => FEATURE_CANONICAL_SECTIONS,
67089
67185
  EXECUTION_KINDS: () => EXECUTION_KINDS,
67186
+ DURATION_SOURCE_DERIVED: () => DURATION_SOURCE_DERIVED,
67090
67187
  DOMAIN_SCHEMA_SQL: () => DOMAIN_SCHEMA_SQL,
67188
+ DERIVED_DURATION_CEILING_MS: () => DERIVED_DURATION_CEILING_MS,
67091
67189
  DEFAULT_TASK_VARIANT: () => DEFAULT_TASK_VARIANT,
67092
67190
  DEFAULT_LOCK_TTL_MS: () => DEFAULT_LOCK_TTL_MS,
67093
67191
  DEFAULT_DISCLOSURE_BUDGET_BYTES: () => DEFAULT_DISCLOSURE_BUDGET_BYTES,
@@ -67885,12 +67983,14 @@ function toEnvelopeJson(value, opts = {}) {
67885
67983
  function toEnvelopeError(code, message, details) {
67886
67984
  return toJson(details !== undefined ? { ok: false, error: { code, message, details } } : { ok: false, error: { code, message } });
67887
67985
  }
67888
- function writeJsonError(output2, options, message) {
67986
+ function writeJsonError(output2, options, message, code = "INTERNAL_ERROR", details) {
67987
+ const text4 = typeof message === "string" ? message : String(message);
67889
67988
  if (options.json && envelopeEnabled(options.jsonEnvelope)) {
67890
- output2.write(toEnvelopeError("INTERNAL_ERROR", message));
67989
+ const bare = text4.startsWith("Error: ") ? text4.slice("Error: ".length) : text4;
67990
+ output2.write(toEnvelopeError(code, bare, details));
67891
67991
  return;
67892
67992
  }
67893
- output2.error(message);
67993
+ output2.error(text4);
67894
67994
  }
67895
67995
 
67896
67996
  // ../../packages/app/src/services/agent-instance-store.ts
@@ -70771,8 +70871,18 @@ function splitTableRow(line) {
70771
70871
  function unescapeTablePipe(s2) {
70772
70872
  return s2.replace(/\\\|/g, "|");
70773
70873
  }
70874
+ function isRecordAuthoredReview(body) {
70875
+ if (body === null)
70876
+ return false;
70877
+ const trimmed = body.trim();
70878
+ if (trimmed.startsWith(RECORD_REVIEW_MARKER))
70879
+ return true;
70880
+ return LEGACY_RECORD_REVIEW_RE.test(trimmed);
70881
+ }
70774
70882
  function renderReview(v) {
70775
70883
  const lines = [];
70884
+ lines.push(RECORD_REVIEW_MARKER);
70885
+ lines.push("");
70776
70886
  lines.push(`**SECU findings** (pipeline verify step \u2014 verdict: ${v.verdict})`);
70777
70887
  lines.push("");
70778
70888
  lines.push("| Priority | Dimension | Location | Finding |");
@@ -70864,10 +70974,12 @@ function gitDiffU0(cwd) {
70864
70974
  return "";
70865
70975
  }
70866
70976
  }
70977
+ var RECORD_REVIEW_MARKER = "<!-- spur:record-review -->", LEGACY_RECORD_REVIEW_RE;
70867
70978
  var init_task_record = __esm(() => {
70868
70979
  init_src2();
70869
70980
  init_dist5();
70870
70981
  init_verify_verdict();
70982
+ LEGACY_RECORD_REVIEW_RE = /^\*\*SECU findings\*\* \(pipeline verify step \u2014 verdict: [A-Z]+\)/;
70871
70983
  });
70872
70984
 
70873
70985
  // ../../packages/app/src/services/feature-check.ts
@@ -71330,7 +71442,14 @@ var init_feature_check = __esm(() => {
71330
71442
  if (touchesSelfRef && dogfoodDir !== undefined) {
71331
71443
  let hasDogfood = false;
71332
71444
  try {
71333
- const entries = await this.fs.readDir(dogfoodDir);
71445
+ let entries;
71446
+ try {
71447
+ const ledger = await this.fs.readFile(`${dogfoodDir}/INDEX.md`);
71448
+ entries = ledger.split(`
71449
+ `);
71450
+ } catch {
71451
+ entries = await this.fs.readDir(dogfoodDir);
71452
+ }
71334
71453
  const segmentRe = new RegExp(`(^|[^A-Za-z0-9])${featureId2}([^A-Za-z0-9]|$)`, "i");
71335
71454
  hasDogfood = entries.some((f) => segmentRe.test(f));
71336
71455
  } catch {}
@@ -71438,6 +71557,23 @@ var init_feature_check = __esm(() => {
71438
71557
  });
71439
71558
  }
71440
71559
  }
71560
+ for (const [taskWbs, artifact] of artifacts2) {
71561
+ if (artifact.diagnostics.artifactError === "artifact is missing")
71562
+ continue;
71563
+ const rows = [...artifact.requirements, ...artifact.acceptanceCriteria];
71564
+ if (rows.length === 0)
71565
+ continue;
71566
+ const anyMatch = rows.some((r) => scenarioAliases.some((sc) => rowMatchesScenario(r.id, sc)));
71567
+ if (!anyMatch) {
71568
+ findings.push({
71569
+ layer: "L4",
71570
+ code: FINDING_CODES.L4_VERDICT_ROWS_MATCH_NO_SCENARIO,
71571
+ severity: "warning",
71572
+ section: "Acceptance Criteria",
71573
+ message: `Task ${taskWbs} verdict evidence (${artifact.path}) carries ${rows.length} row(s) matching no scenario of this feature \u2014 key rows by scenario title or AC-N alias (repair: /sp:dev-verify ${taskWbs})`
71574
+ });
71575
+ }
71576
+ }
71441
71577
  for (const sc of scenarioAliases) {
71442
71578
  const linked = covers[sc.title] ?? [];
71443
71579
  if (linked.length === 0)
@@ -71904,15 +72040,23 @@ var init_task_check = __esm(() => {
71904
72040
  const reqBody = doc2.getSection("Requirements");
71905
72041
  if (reqBody !== null && !isPlaceholderBody(reqBody)) {
71906
72042
  const blocks = reqBody.trim().split(/\n\s*\n/).filter((b) => b.trim().length > 0);
72043
+ const R_ITEM_RE = /^\s*[-*]?\s*(?:\[[ xX]\]\s*)?[*_]{0,2}R\d+\.?[*_]{0,2}\s/;
71907
72044
  let numbered = 0;
72045
+ let proseBlocks = 0;
72046
+ let seenItem = false;
71908
72047
  for (const block of blocks) {
71909
- const firstLine = block.trimStart().split(`
71910
- `)[0] ?? "";
71911
- if (/^\s*[-*]?\s*(?:\[[ xX]\]\s*)?[*_]{0,2}R\d+\.?[*_]{0,2}\s/.test(firstLine)) {
71912
- numbered++;
72048
+ const itemLines = block.split(`
72049
+ `).filter((l) => R_ITEM_RE.test(l)).length;
72050
+ if (itemLines > 0) {
72051
+ numbered += itemLines;
72052
+ seenItem = true;
72053
+ continue;
72054
+ }
72055
+ if (!seenItem || /^\s*[*_]{1,2}[^*_\s]/.test(block.trimStart())) {
72056
+ proseBlocks++;
71913
72057
  }
71914
72058
  }
71915
- if (numbered === 0 || numbered < blocks.length * 0.5) {
72059
+ if (numbered === 0 || proseBlocks > numbered) {
71916
72060
  findings.push({
71917
72061
  layer: "L3",
71918
72062
  code: FINDING_CODES.L3_REQUIREMENTS_FORMAT,
@@ -72041,6 +72185,26 @@ var init_task_check = __esm(() => {
72041
72185
  message: `Task is ${status} but carries ${openBoxes} unchecked checklist box(es) \u2014 flip to [x] or remove before closing`
72042
72186
  });
72043
72187
  }
72188
+ const reviewBody = doc2.getSection("Review");
72189
+ const testingBody = doc2.getSection("Testing");
72190
+ if (reviewBody !== null && testingBody !== null) {
72191
+ const lastVerdict = (body) => {
72192
+ const lines = [...body.matchAll(/\**\s*Verdict\b\**\s*[:\uFF1A]?\s*([^*()\n]+)/gi)];
72193
+ return (lines[lines.length - 1]?.[1] ?? "").replace(/\*/g, "").trim();
72194
+ };
72195
+ const reviewVerdict = lastVerdict(reviewBody);
72196
+ const nonPassing = /\bPARTIAL\b|\bFAIL\b|request-changes/i.test(reviewVerdict);
72197
+ const testingVerdict = lastVerdict(testingBody);
72198
+ if (nonPassing && /^PASS\b/i.test(testingVerdict)) {
72199
+ findings.push({
72200
+ layer: "L3",
72201
+ code: FINDING_CODES.L3_REVIEW_TESTING_CONTRADICTION,
72202
+ severity: "error",
72203
+ section: "Review",
72204
+ message: `Review verdict is "${reviewVerdict}" while Testing records PASS \u2014 reconcile the stale Review via /sp:dev-review (semantics: the last Verdict line in the section is authoritative)`
72205
+ });
72206
+ }
72207
+ }
72044
72208
  }
72045
72209
  }
72046
72210
  async runL4(doc2, fm, status, findings, featuresDir, tasksDir, wbs) {
@@ -72178,6 +72342,18 @@ var init_task_check = __esm(() => {
72178
72342
  section: "Plan",
72179
72343
  message: "Parent task has sub-tasks but its Plan has no sub-task roster (decomposition.md)"
72180
72344
  });
72345
+ return;
72346
+ }
72347
+ const declared = new Set(this.extractDependencyWbs(doc2.frontmatterData?.dependencies));
72348
+ const undeclared = openKids.filter((kid) => !declared.has(kid.wbs)).map((kid) => kid.wbs);
72349
+ if (undeclared.length > 0) {
72350
+ findings.push({
72351
+ layer: "L4",
72352
+ code: FINDING_CODES.L4_ROLLUP_ROSTER_NOT_DECLARED_DEPENDENCY,
72353
+ severity: "warning",
72354
+ section: "Plan",
72355
+ message: `Plan rosters open sub-task(s) ${undeclared.join(", ")} that frontmatter dependencies[] does not declare \u2014 batch ordering reads dependencies[], not the Plan (repair: spur task deps ${wbs} add ${undeclared.join(" ")})`
72356
+ });
72181
72357
  }
72182
72358
  }
72183
72359
  hasSubtaskRoster(planBody, kids) {
@@ -72297,6 +72473,9 @@ var init_task_check = __esm(() => {
72297
72473
  }
72298
72474
  }
72299
72475
  checkGateLanguage(doc2, findings) {
72476
+ const declaredDeps = doc2.frontmatterData?.dependencies;
72477
+ if (Array.isArray(declaredDeps) && declaredDeps.length > 0)
72478
+ return;
72300
72479
  for (const section of ["Background", "Requirements", "Design", "Acceptance Criteria", "Plan"]) {
72301
72480
  const body = doc2.getSection(section);
72302
72481
  if (body === null)
@@ -72656,7 +72835,7 @@ async function structuralSweep(projectRoot) {
72656
72835
  });
72657
72836
  const taskService = new TaskCheckService(fs3, await loadTaskMatrix(projectRoot), locator);
72658
72837
  const findings = [];
72659
- for (const tasksDir of taskDirs) {
72838
+ for (const tasksDir of [activeTasksDir]) {
72660
72839
  if (!await fs3.exists(tasksDir))
72661
72840
  continue;
72662
72841
  for (const fileName of await fs3.readDir(tasksDir)) {
@@ -78058,6 +78237,7 @@ class HistoryService {
78058
78237
  });
78059
78238
  }
78060
78239
  await dao2.alignMessageProvenance();
78240
+ await deriveAssistantDurations(db2);
78061
78241
  }
78062
78242
  return result;
78063
78243
  }
@@ -79111,7 +79291,7 @@ class PlanningWriteService {
79111
79291
  } : {}
79112
79292
  };
79113
79293
  await this.emitter.emit(event);
79114
- const warnings = doc2.strippedHeadings.map((line) => `Stripped same-level heading from section body (would become a phantom section): "${line}". ` + "Use bullet lists, tables, or **bold** labels for sub-structure instead.");
79294
+ const warnings = doc2.demotedHeadings.map((line) => `Demoted same-level heading one level deeper in section body (would become a phantom section): "${line}". ` + "Use bullet lists, tables, or **bold** labels for sub-structure instead.");
79115
79295
  for (const dup of doc2.duplicateSectionNames) {
79116
79296
  warnings.push(`Dropped duplicate "${dup}" section (appeared more than once in the file \u2014 ` + "likely a copy-paste or double-write error). The first occurrence was kept.");
79117
79297
  }
@@ -79142,6 +79322,10 @@ function applyMutation(doc2, mutation) {
79142
79322
  case "updateSection":
79143
79323
  if (mutation.sectionName !== undefined && mutation.sectionBody !== undefined) {
79144
79324
  const body = mutation.sectionName.toLowerCase() === "acceptance criteria" ? normalizeAcFence(mutation.sectionBody) : mutation.sectionBody;
79325
+ if (mutation.sectionName.toLowerCase() === "q&a") {
79326
+ appendQaEntry(doc2, body);
79327
+ break;
79328
+ }
79145
79329
  doc2.replaceSection(mutation.sectionName, body);
79146
79330
  }
79147
79331
  break;
@@ -79172,6 +79356,22 @@ ${line}
79172
79356
  `;
79173
79357
  doc2.replaceSection("History", updated);
79174
79358
  }
79359
+ function appendQaEntry(doc2, body) {
79360
+ const REPLACE_MARKER = "<!-- qa:replace -->";
79361
+ if (body.trimStart().startsWith(REPLACE_MARKER)) {
79362
+ doc2.replaceSection("Q&A", body.trimStart().slice(REPLACE_MARKER.length).trimStart());
79363
+ return;
79364
+ }
79365
+ const existing = doc2.getSection("Q&A") ?? "";
79366
+ const entry = `#### Q&A entry \u2014 ${new Date().toISOString()}
79367
+
79368
+ ${body.trim()}
79369
+ `;
79370
+ const updated = existing.trim().length > 0 ? `${existing.trimEnd()}
79371
+
79372
+ ${entry}` : entry;
79373
+ doc2.replaceSection("Q&A", updated);
79374
+ }
79175
79375
  function resolveEventName(kind, domain2, statusChanged) {
79176
79376
  if (kind === "create") {
79177
79377
  return domain2 === "task" ? "task.created" : "feature.created";
@@ -81838,7 +82038,7 @@ class TaskService {
81838
82038
  await this.writeService.updateSection(ref, "Testing", testingBody);
81839
82039
  result.testingWritten = true;
81840
82040
  }
81841
- if (sectionIsBare(doc2, "Review")) {
82041
+ if (sectionIsBare(doc2, "Review") || isRecordAuthoredReview(doc2.getSection("Review"))) {
81842
82042
  const reviewBody = renderReview(verdict);
81843
82043
  await this.writeService.updateSection(ref, "Review", reviewBody);
81844
82044
  result.reviewWritten = true;
@@ -82163,8 +82363,11 @@ ${block}` : block);
82163
82363
  }
82164
82364
  async resolveTaskFile(wbs) {
82165
82365
  const result = await this.findTaskFileName(wbs);
82166
- if (!result)
82167
- throw new Error(`Task ${wbs} not found in any registered task folder`);
82366
+ if (!result) {
82367
+ const err = new Error(`Task ${wbs} not found in any registered task folder`);
82368
+ err.cliCode = "NOT_FOUND";
82369
+ throw err;
82370
+ }
82168
82371
  return result.filePath;
82169
82372
  }
82170
82373
  async findTaskFileName(wbs) {
@@ -87564,6 +87767,7 @@ __export(exports_src2, {
87564
87767
  looksLikeOpaqueId: () => looksLikeOpaqueId,
87565
87768
  loadAcceptedFindings: () => loadAcceptedFindings,
87566
87769
  isSystemEventEnvelopeV2: () => isSystemEventEnvelopeV2,
87770
+ isRecordAuthoredReview: () => isRecordAuthoredReview,
87567
87771
  isPortLive: () => isPortLive,
87568
87772
  isPortAvailable: () => isPortAvailable,
87569
87773
  isFindingCode: () => isFindingCode,
@@ -95318,12 +95522,12 @@ async function runAgentList(svc, context4, opts) {
95318
95522
  }
95319
95523
  async function runAgentCreate(id, context4, flags) {
95320
95524
  if (id === undefined) {
95321
- context4.output.error("agent create requires <id>");
95525
+ writeJsonError(context4.output, jsonFlags(flags), "agent create requires <id>", "VALIDATION_FAILED");
95322
95526
  return 2;
95323
95527
  }
95324
95528
  const type = typeof flags.type === "string" ? flags.type : "";
95325
95529
  if (type === "") {
95326
- context4.output.error("agent create requires --type <agent-type>");
95530
+ writeJsonError(context4.output, jsonFlags(flags), "agent create requires --type <agent-type>", "VALIDATION_FAILED");
95327
95531
  return 2;
95328
95532
  }
95329
95533
  const tags = typeof flags.tags === "string" ? flags.tags : "";
@@ -95423,6 +95627,13 @@ async function runAgentDelete(id, context4, flags) {
95423
95627
  return 1;
95424
95628
  }
95425
95629
  }
95630
+ function jsonFlags(flags) {
95631
+ const envelope2 = flags.jsonEnvelope ?? flags["json-envelope"];
95632
+ return {
95633
+ json: flags.json === true,
95634
+ jsonEnvelope: typeof envelope2 === "boolean" ? envelope2 : undefined
95635
+ };
95636
+ }
95426
95637
  async function runAgentRun(prompt, context4, flags, deps) {
95427
95638
  const bus = new EventBus;
95428
95639
  const ledger = await attachSystemEventLedger(bus, context4);
@@ -95431,19 +95642,19 @@ async function runAgentRun(prompt, context4, flags, deps) {
95431
95642
  if (flags.drain === true || typeof flags.spec === "string") {
95432
95643
  const { prompt: drained, flags: rewritten } = await drainIntoPrompt(prompt, context4, flags);
95433
95644
  if (typeof flags.spec === "string" && flags.spec !== "" && rewritten["spec-id"] !== flags.spec) {
95434
- context4.output.error(`--spec "${flags.spec}" does not match a team agent spec`);
95645
+ writeJsonError(context4.output, jsonFlags(flags), `--spec "${flags.spec}" does not match a team agent spec`, "VALIDATION_FAILED");
95435
95646
  return 2;
95436
95647
  }
95437
95648
  const invalid2 = validateAgentSelector(rewritten, context4);
95438
95649
  if (invalid2 !== null) {
95439
- context4.output.error(invalid2);
95650
+ writeJsonError(context4.output, jsonFlags(flags), invalid2, "VALIDATION_FAILED");
95440
95651
  return 2;
95441
95652
  }
95442
95653
  return await svc.run(drained, rewritten, deps);
95443
95654
  }
95444
95655
  const invalid = validateAgentSelector(flags, context4);
95445
95656
  if (invalid !== null) {
95446
- context4.output.error(invalid);
95657
+ writeJsonError(context4.output, jsonFlags(flags), invalid, "VALIDATION_FAILED");
95447
95658
  return 2;
95448
95659
  }
95449
95660
  return await svc.run(prompt, flags, deps);
@@ -96299,7 +96510,7 @@ function registerFeatureCommand(program2, context4) {
96299
96510
  try {
96300
96511
  const result = await svc.show(id);
96301
96512
  if (result === null) {
96302
- writeJsonError(context4.output, options, `Feature ${id} not found`);
96513
+ writeJsonError(context4.output, options, `Feature ${id} not found`, "NOT_FOUND");
96303
96514
  context4.setExitCode(1);
96304
96515
  return;
96305
96516
  }
@@ -96325,7 +96536,7 @@ function registerFeatureCommand(program2, context4) {
96325
96536
  let result;
96326
96537
  if (options.section !== undefined) {
96327
96538
  if (options.fromFile === undefined) {
96328
- context4.output.error("--from-file is required with --section");
96539
+ writeJsonError(context4.output, options, "--from-file is required with --section", "VALIDATION_FAILED");
96329
96540
  context4.setExitCode(2);
96330
96541
  return;
96331
96542
  }
@@ -96337,13 +96548,13 @@ function registerFeatureCommand(program2, context4) {
96337
96548
  context4.output.write(`Updated section '${options.section}' in feature ${result.ref.id}`);
96338
96549
  }
96339
96550
  } else if (options.fromFile !== undefined) {
96340
- context4.output.error("--section is required with --from-file");
96551
+ writeJsonError(context4.output, options, "--section is required with --from-file", "VALIDATION_FAILED");
96341
96552
  context4.setExitCode(2);
96342
96553
  return;
96343
96554
  }
96344
96555
  if (options.field !== undefined) {
96345
96556
  if (options.value === undefined) {
96346
- context4.output.error("--value is required with --field");
96557
+ writeJsonError(context4.output, options, "--value is required with --field", "VALIDATION_FAILED");
96347
96558
  context4.setExitCode(2);
96348
96559
  return;
96349
96560
  }
@@ -96352,7 +96563,7 @@ function registerFeatureCommand(program2, context4) {
96352
96563
  context4.output.write(`Updated ${options.field} on feature ${result.ref.id}`);
96353
96564
  }
96354
96565
  } else if (options.value !== undefined) {
96355
- context4.output.error("--field is required with --value");
96566
+ writeJsonError(context4.output, options, "--field is required with --value", "VALIDATION_FAILED");
96356
96567
  context4.setExitCode(2);
96357
96568
  return;
96358
96569
  }
@@ -96363,7 +96574,7 @@ function registerFeatureCommand(program2, context4) {
96363
96574
  }
96364
96575
  }
96365
96576
  if (result === undefined) {
96366
- context4.output.error("Either <status>, --field/--value, or --section/--from-file is required");
96577
+ writeJsonError(context4.output, options, "Either <status>, --field/--value, or --section/--from-file is required", "VALIDATION_FAILED");
96367
96578
  context4.setExitCode(2);
96368
96579
  return;
96369
96580
  }
@@ -96387,7 +96598,7 @@ function registerFeatureCommand(program2, context4) {
96387
96598
  try {
96388
96599
  const initial = await svc.show(id);
96389
96600
  if (initial === null) {
96390
- writeJsonError(context4.output, options, `Feature ${id} not found`);
96601
+ writeJsonError(context4.output, options, `Feature ${id} not found`, "NOT_FOUND");
96391
96602
  context4.setExitCode(1);
96392
96603
  return;
96393
96604
  }
@@ -96419,7 +96630,7 @@ function registerFeatureCommand(program2, context4) {
96419
96630
  next = forwardPath[current];
96420
96631
  }
96421
96632
  if (current !== target) {
96422
- context4.output.error(`${id}: cannot reach '${target}' from '${current}' along the forward path`);
96633
+ writeJsonError(context4.output, options, `${id}: cannot reach '${target}' from '${current}' along the forward path`, "GUARD_DENIED");
96423
96634
  context4.setExitCode(1);
96424
96635
  return;
96425
96636
  }
@@ -96495,12 +96706,12 @@ function registerFeatureCommand(program2, context4) {
96495
96706
  const svc = await makeService(context4, options.folder);
96496
96707
  try {
96497
96708
  if (options.all && options.feature) {
96498
- context4.output.error("--feature <id> and --all are mutually exclusive");
96709
+ writeJsonError(context4.output, options, "--feature <id> and --all are mutually exclusive", "VALIDATION_FAILED");
96499
96710
  context4.setExitCode(2);
96500
96711
  return;
96501
96712
  }
96502
96713
  if (!options.all && !options.feature) {
96503
- context4.output.error("--feature <id> or --all is required (refusing silent all-features sweep)");
96714
+ writeJsonError(context4.output, options, "--feature <id> or --all is required (refusing silent all-features sweep)", "VALIDATION_FAILED");
96504
96715
  context4.setExitCode(2);
96505
96716
  return;
96506
96717
  }
@@ -96534,9 +96745,9 @@ function registerFeatureCommand(program2, context4) {
96534
96745
  for (const fid of ids) {
96535
96746
  const fileName = entries.find((n3) => n3.match(new RegExp(`^${fid}_.+\\.md$`)));
96536
96747
  if (!fileName) {
96537
- context4.output.error(`Feature ${fid} not found`);
96748
+ writeJsonError(context4.output, options, `Feature ${fid} not found`, "NOT_FOUND");
96538
96749
  context4.setExitCode(1);
96539
- continue;
96750
+ return;
96540
96751
  }
96541
96752
  const result = await svc.check(`${featuresDir}/${fileName}`, fid, {
96542
96753
  strict,
@@ -96586,7 +96797,7 @@ ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
96586
96797
  const svc = await makeService(context4, options.folder);
96587
96798
  try {
96588
96799
  if (!options.all && !id) {
96589
- context4.output.error("Feature ID is required unless --all is passed");
96800
+ writeJsonError(context4.output, options, "Feature ID is required unless --all is passed", "VALIDATION_FAILED");
96590
96801
  context4.setExitCode(2);
96591
96802
  return;
96592
96803
  }
@@ -96675,7 +96886,7 @@ import { createRequire } from "module";
96675
96886
  var CLI_CONFIG = {
96676
96887
  binaryName: "spur",
96677
96888
  binaryLabel: "spur",
96678
- binaryVersion: "0.3.65",
96889
+ binaryVersion: "0.3.66",
96679
96890
  configDir: ".spur",
96680
96891
  configFile: ".spur/config.yaml",
96681
96892
  databaseFile: ".spur/spur.db"
@@ -97362,7 +97573,7 @@ function registerMessageCommand(program2, context4) {
97362
97573
  const svc = new TeamService(context4);
97363
97574
  const intervalMs = parseInterval2(options.interval);
97364
97575
  if (intervalMs === null) {
97365
- context4.output.error(`invalid --interval "${options.interval}" (expected a positive integer ms)`);
97576
+ writeJsonError(context4.output, options, `invalid --interval "${options.interval}" (expected a positive integer ms)`, "VALIDATION_FAILED");
97366
97577
  context4.setExitCode(2);
97367
97578
  return;
97368
97579
  }
@@ -97527,7 +97738,7 @@ async function runMessageInbox(svc, context4, options) {
97527
97738
  async function runMessageReply(svc, context4, msgId, body, options) {
97528
97739
  const trimmed = body.trim();
97529
97740
  if (trimmed === "") {
97530
- context4.output.error("message reply requires a non-empty body");
97741
+ writeJsonError(context4.output, options, "message reply requires a non-empty body", "VALIDATION_FAILED");
97531
97742
  return 2;
97532
97743
  }
97533
97744
  const result = await svc.replyToMessage(msgId, trimmed);
@@ -97930,17 +98141,17 @@ function registerRuleCommand(program2, context4) {
97930
98141
  rule.command("trace").summary("Show persisted rule run history.").argument("[run-id]", "Run ID for per-run detail").option("--preset <name>", "Filter by preset name").option(...SHARED_OPTIONS.statusDoneFailed).option(...SHARED_OPTIONS.since).option(...SHARED_OPTIONS.last, "20").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (runId, options) => {
97931
98142
  const last = parseInt(options.last, 10);
97932
98143
  if (!Number.isInteger(last) || last < 1) {
97933
- context4.output.error("--last must be a positive integer");
98144
+ writeJsonError(context4.output, options, "--last must be a positive integer", "VALIDATION_FAILED");
97934
98145
  context4.setExitCode(1);
97935
98146
  return;
97936
98147
  }
97937
98148
  if (options.status !== undefined && !["done", "failed"].includes(options.status)) {
97938
- context4.output.error("--status must be one of: done, failed");
98149
+ writeJsonError(context4.output, options, "--status must be one of: done, failed", "VALIDATION_FAILED");
97939
98150
  context4.setExitCode(1);
97940
98151
  return;
97941
98152
  }
97942
98153
  if (options.since !== undefined && Number.isNaN(Date.parse(options.since))) {
97943
- context4.output.error("--since must be a valid ISO date");
98154
+ writeJsonError(context4.output, options, "--since must be a valid ISO date", "VALIDATION_FAILED");
97944
98155
  context4.setExitCode(1);
97945
98156
  return;
97946
98157
  }
@@ -108141,7 +108352,7 @@ function registerServeCommand(program2, context4, options = {}) {
108141
108352
  } catch (err) {
108142
108353
  writeJsonError(context4.output, options2, err instanceof Error ? err.message : String(err));
108143
108354
  if (context4.env?.SPUR_DEBUG === "1" && err instanceof Error && err.stack) {
108144
- context4.output.error(err.stack);
108355
+ writeJsonError(context4.output, options2, err.stack, "INTERNAL_ERROR");
108145
108356
  }
108146
108357
  context4.setExitCode(1);
108147
108358
  }
@@ -109168,12 +109379,12 @@ function registerTaskCommand(program2, context4) {
109168
109379
  const task = program2.command("task").summary("manage tasks");
109169
109380
  task.command("create").summary("Create a new task with race-safe WBS allocation.").argument("<title>", "Task title").option(...SHARED_OPTIONS.featureTrace).option("--parent <wbs>", "Parent WBS for sub-task grouping").option("--template <variant>", `Template variant (${TASK_VARIANTS.join("|")})`).option(...SHARED_OPTIONS.folderTasks).option("--dedupe-within <seconds>", "Override the default dedup window (seconds). Guard is on (300s) by default when --feature is set.", Number).option("--allow-duplicate-name", "Disable the dedup guard entirely (creates anyway)").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (title2, options) => {
109170
109381
  if (options.template !== undefined && !TASK_VARIANTS.includes(options.template)) {
109171
- context4.output.error(`Unknown template variant "${options.template}". Valid: ${TASK_VARIANTS.join(", ")}`);
109382
+ writeJsonError(context4.output, options, `Unknown template variant "${options.template}". Valid: ${TASK_VARIANTS.join(", ")}`, "VALIDATION_FAILED");
109172
109383
  context4.setExitCode(2);
109173
109384
  return;
109174
109385
  }
109175
109386
  if (options.dedupeWithin !== undefined && (!Number.isInteger(options.dedupeWithin) || options.dedupeWithin <= 0)) {
109176
- context4.output.error("--dedupe-within must be a positive integer");
109387
+ writeJsonError(context4.output, options, "--dedupe-within must be a positive integer", "VALIDATION_FAILED");
109177
109388
  context4.setExitCode(2);
109178
109389
  return;
109179
109390
  }
@@ -109267,7 +109478,8 @@ function registerTaskCommand(program2, context4) {
109267
109478
  ${result.content}`);
109268
109479
  }
109269
109480
  } catch (err) {
109270
- writeJsonError(context4.output, options, String(err));
109481
+ const cliCode = err?.cliCode;
109482
+ writeJsonError(context4.output, options, String(err), "INTERNAL_ERROR", cliCode !== undefined ? { cliCode } : undefined);
109271
109483
  context4.setExitCode(1);
109272
109484
  }
109273
109485
  });
@@ -109287,7 +109499,7 @@ ${result.content}`);
109287
109499
  try {
109288
109500
  if (options.section !== undefined) {
109289
109501
  if (options.fromFile === undefined) {
109290
- context4.output.error("--from-file is required with --section");
109502
+ writeJsonError(context4.output, options, "--from-file is required with --section", "VALIDATION_FAILED");
109291
109503
  context4.setExitCode(2);
109292
109504
  return;
109293
109505
  }
@@ -109319,7 +109531,7 @@ ${result.content}`);
109319
109531
  }
109320
109532
  const ok = await runDoneGateCheck(context4, wbs, options.folder, status);
109321
109533
  if (!ok) {
109322
- context4.output.error(`Lifecycle transition blocked: \`spur task check ${wbs}\` failed. Fix the findings before transitioning to ${status}.`);
109534
+ writeJsonError(context4.output, options, `Lifecycle transition blocked: \`spur task check ${wbs}\` failed. Fix the findings before transitioning to ${status}.`, "GUARD_DENIED");
109323
109535
  context4.setExitCode(1);
109324
109536
  return;
109325
109537
  }
@@ -109350,7 +109562,7 @@ ${result.content}`);
109350
109562
  return;
109351
109563
  }
109352
109564
  if (guardOutcome.kind === "deny") {
109353
- context4.output.error(guardOutcome.message);
109565
+ writeJsonError(context4.output, options, guardOutcome.message, "GUARD_DENIED");
109354
109566
  context4.setExitCode(1);
109355
109567
  return;
109356
109568
  }
@@ -109397,7 +109609,7 @@ ${result.content}`);
109397
109609
  }
109398
109610
  } catch (err) {
109399
109611
  if (err instanceof SectionMutationError) {
109400
- context4.output.error(`[${err.code}] ${err.message}`);
109612
+ writeJsonError(context4.output, options, `[${err.code}] ${err.message}`, "INTERNAL_ERROR");
109401
109613
  context4.setExitCode(err.code === "usage" ? 2 : 3);
109402
109614
  } else {
109403
109615
  writeJsonError(context4.output, options, String(err));
@@ -109415,7 +109627,7 @@ ${result.content}`);
109415
109627
  ` + "2 usage error, 3 validation error.").argument("<wbs>", "Task WBS number to mutate").argument("<op>", "Operation: set | add | remove | clear").argument("[values...]", "WBS values (required for set/add/remove; forbidden for clear)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (wbs, op, values, options) => {
109416
109628
  const allowedOps = ["set", "add", "remove", "clear"];
109417
109629
  if (!allowedOps.includes(op)) {
109418
- context4.output.error(`Unknown op "${op}". Allowed: ${allowedOps.join(", ")}.`);
109630
+ writeJsonError(context4.output, options, `Unknown op "${op}". Allowed: ${allowedOps.join(", ")}.`, "VALIDATION_FAILED");
109419
109631
  context4.setExitCode(2);
109420
109632
  return;
109421
109633
  }
@@ -109431,7 +109643,7 @@ ${result.content}`);
109431
109643
  }
109432
109644
  } catch (err) {
109433
109645
  if (err instanceof DependencyMutationError) {
109434
- context4.output.error(`[${err.code}] ${err.message}`);
109646
+ writeJsonError(context4.output, options, `[${err.code}] ${err.message}`, "INTERNAL_ERROR");
109435
109647
  context4.setExitCode(err.code === "usage" ? 2 : 3);
109436
109648
  } else {
109437
109649
  writeJsonError(context4.output, options, String(err));
@@ -109455,18 +109667,18 @@ ${result.content}`);
109455
109667
  ` + "1 generic error, 2 usage error, 3 validation error.").argument("<wbs>", "Task WBS number to mutate").argument("<op>", "Operation: init | add | list").argument("[name]", "Canonical section name (required for add; forbidden for init/list)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (wbs, op, name, options) => {
109456
109668
  const allowedOps = ["init", "add", "list"];
109457
109669
  if (!allowedOps.includes(op)) {
109458
- context4.output.error(`Unknown op "${op}". Allowed: ${allowedOps.join(", ")}.`);
109670
+ writeJsonError(context4.output, options, `Unknown op "${op}". Allowed: ${allowedOps.join(", ")}.`, "VALIDATION_FAILED");
109459
109671
  context4.setExitCode(2);
109460
109672
  return;
109461
109673
  }
109462
109674
  const typedOp = op;
109463
109675
  if (typedOp === "add" && typeof name !== "string") {
109464
- context4.output.error('op "add" requires a section name argument.');
109676
+ writeJsonError(context4.output, options, 'op "add" requires a section name argument.', "VALIDATION_FAILED");
109465
109677
  context4.setExitCode(2);
109466
109678
  return;
109467
109679
  }
109468
109680
  if ((typedOp === "init" || typedOp === "list") && name !== undefined) {
109469
- context4.output.error(`op "${typedOp}" takes no section name argument.`);
109681
+ writeJsonError(context4.output, options, `op "${typedOp}" takes no section name argument.`, "VALIDATION_FAILED");
109470
109682
  context4.setExitCode(2);
109471
109683
  return;
109472
109684
  }
@@ -109491,7 +109703,7 @@ ${result.content}`);
109491
109703
  }
109492
109704
  } catch (err) {
109493
109705
  if (err instanceof SectionMutationError) {
109494
- context4.output.error(`[${err.code}] ${err.message}`);
109706
+ writeJsonError(context4.output, options, `[${err.code}] ${err.message}`, "INTERNAL_ERROR");
109495
109707
  context4.setExitCode(err.code === "usage" ? 2 : 3);
109496
109708
  } else {
109497
109709
  writeJsonError(context4.output, options, String(err));
@@ -109704,7 +109916,7 @@ ${result.content}`);
109704
109916
  }
109705
109917
  });
109706
109918
  task.command("verdict").summary("Derive PASS/PARTIAL/FAIL/UNKNOWN verdict from verify answer text (replaces pipeline grep/shell).").argument("<wbs>", "Task WBS number").option("--from-answer <path>", "Path to verify answer text file").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (wbs, options) => {
109707
- const { deriveVerdict: deriveVerdict2, verdictRowsMatchScenarios: verdictRowsMatchScenarios2 } = await init_src3().then(() => exports_src2);
109919
+ const { deriveVerdict: deriveVerdict2 } = await init_src3().then(() => exports_src2);
109708
109920
  const answerPath = options.fromAnswer ?? `.spur/run/${wbs}-verify-answer.txt`;
109709
109921
  let answerText;
109710
109922
  try {
@@ -109716,22 +109928,6 @@ ${result.content}`);
109716
109928
  }
109717
109929
  const taskCheckPassed = true;
109718
109930
  const result = deriveVerdict2(answerText, taskCheckPassed);
109719
- try {
109720
- const svc = await makeService2(context4, options.folder);
109721
- const task2 = await svc.show(wbs);
109722
- const featureId2 = task2.frontmatter.feature_id;
109723
- if (typeof featureId2 === "string" && featureId2.length > 0) {
109724
- const resolved = await resolvePlanningFolders(context4.fs);
109725
- const names = await context4.fs.readDir(context4.fs.resolve(resolved.featuresDir));
109726
- const name = names.find((n3) => n3.startsWith(`${featureId2}_`) && n3.endsWith(".md"));
109727
- if (name !== undefined) {
109728
- const raw2 = await context4.fs.readFile(context4.fs.resolve(`${resolved.featuresDir}/${name}`));
109729
- if (!verdictRowsMatchScenarios2(result.requirements, raw2)) {
109730
- context4.output.error(`warning: no verdict row matches any scenario in feature ${featureId2} \u2014 key rows by scenario title or AC-N alias, or the feature done gate reports L4.scenario-unverified`);
109731
- }
109732
- }
109733
- }
109734
- } catch {}
109735
109931
  const jsonOut = JSON.stringify({ wbs, ...result, source: "spur-task-verdict" }, null, 2);
109736
109932
  await context4.fs.ensureDir(".spur/run");
109737
109933
  await context4.fs.writeFile(`.spur/run/${wbs}-verdict.json`, `${jsonOut}
@@ -109792,34 +109988,34 @@ ${result.content}`);
109792
109988
  context4.setExitCode(1);
109793
109989
  }
109794
109990
  });
109795
- task.command("check").summary("Validate a task file through the four-layer check (design \xA73).").argument("[wbs]", "Task WBS number (validates all tasks in the folder when omitted)").option(...SHARED_OPTIONS.strictTaskAll).option("--strict-core", "Compatibility alias (F92 R2): historically the done-gate label; kept so installed plugins/workflows that call it keep working. No longer meaningful on its own \u2014 target-state selection (`--as`) supplies the real done semantics.").option(...SHARED_OPTIONS.asTaskF92).option("--corpus", "Sweep every task and feature against config/corpus-baseline.json").option("--since <ref>", "Scope the corpus fog check to changes since a git ref (requires --corpus)").option("--fix", "repair structural findings in place (heading presence/level/order, R-item checkboxes)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (wbs, options) => {
109991
+ task.command("check").summary("Validate a task file through the four-layer check (design \xA73).").argument("[wbs]", "Task WBS number (validates all tasks in the folder when omitted)").option(...SHARED_OPTIONS.strictTaskAll).option("--strict-core", "Compatibility alias (F92 R2): historically the done-gate label; kept so installed plugins/workflows that call it keep working. No longer meaningful on its own \u2014 target-state selection (`--as`) supplies the real done semantics.").option(...SHARED_OPTIONS.asTaskF92).option("--corpus", "Sweep every task and feature against config/corpus-baseline.json").option("--since <ref>", "Scope the corpus fog check to changes since a git ref (requires --corpus)").option("--fix", "repair structural findings in place (heading presence/level/order; adds missing R-item checkbox markers \u2014 flipping verified boxes is task record's job, not --fix)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (wbs, options) => {
109796
109992
  const json3 = options.json === true;
109797
109993
  const strict = options.strict === true;
109798
109994
  const asStatus = options.as === undefined ? undefined : canonicalStatusOrRaw(options.as);
109799
109995
  if (options.as !== undefined && !TASK_STATUSES.includes(asStatus ?? "")) {
109800
- context4.output.error(`invalid --as status "${options.as}" (canonical: ${TASK_STATUSES.join(", ")})`);
109996
+ writeJsonError(context4.output, options, `invalid --as status "${options.as}" (canonical: ${TASK_STATUSES.join(", ")})`, "VALIDATION_FAILED");
109801
109997
  context4.setExitCode(2);
109802
109998
  return;
109803
109999
  }
109804
110000
  if (asStatus !== undefined && options.corpus === true) {
109805
- context4.output.error("--as <status> is a single-task target projection and cannot be combined with --corpus");
110001
+ writeJsonError(context4.output, options, "--as <status> is a single-task target projection and cannot be combined with --corpus", "VALIDATION_FAILED");
109806
110002
  context4.setExitCode(2);
109807
110003
  return;
109808
110004
  }
109809
110005
  if (options.fix === true && options.corpus === true) {
109810
- context4.output.error("--fix repairs files in place and cannot be combined with --corpus");
110006
+ writeJsonError(context4.output, options, "--fix repairs files in place and cannot be combined with --corpus", "VALIDATION_FAILED");
109811
110007
  context4.setExitCode(2);
109812
110008
  return;
109813
110009
  }
109814
110010
  try {
109815
110011
  if (options.corpus === true) {
109816
110012
  if (wbs !== undefined) {
109817
- context4.output.error("--corpus validates the whole corpus and cannot be combined with a WBS");
110013
+ writeJsonError(context4.output, options, "--corpus validates the whole corpus and cannot be combined with a WBS", "VALIDATION_FAILED");
109818
110014
  context4.setExitCode(2);
109819
110015
  return;
109820
110016
  }
109821
110017
  if (String(options.since ?? "").startsWith("-")) {
109822
- context4.output.error("--since requires a git ref value (e.g. --since HEAD~1)");
110018
+ writeJsonError(context4.output, options, "--since requires a git ref value (e.g. --since HEAD~1)", "VALIDATION_FAILED");
109823
110019
  context4.setExitCode(2);
109824
110020
  return;
109825
110021
  }
@@ -109852,7 +110048,7 @@ ${result.content}`);
109852
110048
  return;
109853
110049
  }
109854
110050
  if (options.since !== undefined) {
109855
- context4.output.error("--since requires --corpus");
110051
+ writeJsonError(context4.output, options, "--since requires --corpus", "VALIDATION_FAILED");
109856
110052
  context4.setExitCode(2);
109857
110053
  return;
109858
110054
  }
@@ -109884,8 +110080,9 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
109884
110080
  const locator = options.folder !== undefined ? TaskLocator.forSingleDir(context4.fs, tasksDir) : await makeTaskLocator(context4);
109885
110081
  const hit = await locator.findByWbs(wbs);
109886
110082
  if (hit === null) {
109887
- context4.output.error(`Task ${wbs} not found`);
110083
+ writeJsonError(context4.output, options, `Task ${wbs} not found`, "NOT_FOUND");
109888
110084
  context4.setExitCode(1);
110085
+ return;
109889
110086
  } else {
109890
110087
  const result = await svc.check(hit.filePath, wbs, {
109891
110088
  strict,
@@ -109903,7 +110100,7 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
109903
110100
  for (const w of wbsPattern) {
109904
110101
  const fileName = entries.find((n3) => n3.startsWith(`${w}_`) && n3.endsWith(".md"));
109905
110102
  if (!fileName) {
109906
- context4.output.error(`Task ${w} not found`);
110103
+ writeJsonError(context4.output, options, `Task ${w} not found`, "NOT_FOUND");
109907
110104
  context4.setExitCode(1);
109908
110105
  continue;
109909
110106
  }
@@ -109957,7 +110154,7 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
109957
110154
  context4.output.write(`${result.wbs} ${result.filePath}`);
109958
110155
  }
109959
110156
  } else {
109960
- context4.output.error(`No owning task found for ${filePath}`);
110157
+ writeJsonError(context4.output, options, `No owning task found for ${filePath}`, "NOT_FOUND");
109961
110158
  context4.setExitCode(1);
109962
110159
  }
109963
110160
  } catch (err) {
@@ -109976,7 +110173,7 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
109976
110173
  context4.output.write(filePath);
109977
110174
  }
109978
110175
  } else {
109979
- context4.output.error(`Task ${wbs} not found`);
110176
+ writeJsonError(context4.output, options, `Task ${wbs} not found`, "NOT_FOUND");
109980
110177
  context4.setExitCode(1);
109981
110178
  }
109982
110179
  } catch (err) {
@@ -110253,7 +110450,8 @@ async function performTeamStart(agentId, options) {
110253
110450
  const body = await res.json();
110254
110451
  if (res.ok)
110255
110452
  return { ok: true, body };
110256
- return { ok: false, error: body.error ?? `start failed: ${res.status}`, status: res.status };
110453
+ const detail = errorText(body.error);
110454
+ return { ok: false, error: detail ?? `start failed: ${res.status}`, status: res.status };
110257
110455
  } catch (err) {
110258
110456
  return { ok: false, transportError: err };
110259
110457
  }
@@ -110262,11 +110460,11 @@ async function runTeamStart(agentId, options, context4) {
110262
110460
  const result = await performTeamStart(agentId, options);
110263
110461
  if ("transportError" in result) {
110264
110462
  const err = result.transportError;
110265
- context4.output.error(`Cannot reach server at ${options.server} \u2014 is spur serve running? (${err instanceof Error ? err.message : String(err)})`);
110463
+ writeJsonError(context4.output, options, `Cannot reach server at ${options.server} \u2014 is spur serve running? (${err instanceof Error ? err.message : String(err)})`, "INTERNAL_ERROR");
110266
110464
  return 1;
110267
110465
  }
110268
110466
  if (!result.ok) {
110269
- context4.output.error(result.error);
110467
+ writeJsonError(context4.output, options, result.error, "INTERNAL_ERROR");
110270
110468
  return 1;
110271
110469
  }
110272
110470
  if (options.json) {
@@ -110276,6 +110474,19 @@ async function runTeamStart(agentId, options, context4) {
110276
110474
  }
110277
110475
  return 0;
110278
110476
  }
110477
+ function errorText(raw2) {
110478
+ if (typeof raw2 === "string")
110479
+ return raw2;
110480
+ if (raw2 !== null && typeof raw2 === "object" && "message" in raw2) {
110481
+ const message = raw2.message;
110482
+ if (typeof message === "string" && message !== "")
110483
+ return message;
110484
+ if ("code" in raw2 && typeof raw2.code === "string") {
110485
+ return raw2.code;
110486
+ }
110487
+ }
110488
+ return raw2 === undefined ? undefined : JSON.stringify(raw2);
110489
+ }
110279
110490
  async function performTeamStop(agentId, options) {
110280
110491
  try {
110281
110492
  const url2 = `${options.server}/team/agents/${encodeURIComponent(agentId)}/stop`;
@@ -110283,7 +110494,8 @@ async function performTeamStop(agentId, options) {
110283
110494
  const body = await res.json();
110284
110495
  if (res.ok)
110285
110496
  return { ok: true, body };
110286
- return { ok: false, error: body.error ?? `stop failed: ${res.status}`, status: res.status };
110497
+ const detail = errorText(body.error);
110498
+ return { ok: false, error: detail ?? `stop failed: ${res.status}`, status: res.status };
110287
110499
  } catch (err) {
110288
110500
  return { ok: false, transportError: err };
110289
110501
  }
@@ -110292,11 +110504,11 @@ async function runTeamStop(agentId, options, context4) {
110292
110504
  const result = await performTeamStop(agentId, options);
110293
110505
  if ("transportError" in result) {
110294
110506
  const err = result.transportError;
110295
- context4.output.error(`Cannot reach server at ${options.server} \u2014 is spur serve running? (${err instanceof Error ? err.message : String(err)})`);
110507
+ writeJsonError(context4.output, options, `Cannot reach server at ${options.server} \u2014 is spur serve running? (${err instanceof Error ? err.message : String(err)})`, "INTERNAL_ERROR");
110296
110508
  return 1;
110297
110509
  }
110298
110510
  if (!result.ok) {
110299
- context4.output.error(result.error);
110511
+ writeJsonError(context4.output, options, result.error, "INTERNAL_ERROR");
110300
110512
  return 1;
110301
110513
  }
110302
110514
  if (options.json) {
@@ -110627,32 +110839,32 @@ ${result.errors.map((m) => ` - ${m}`).join(`
110627
110839
  const silent = !json3 && options.silent === true;
110628
110840
  const quiet = !json3 && options.quiet === true;
110629
110841
  if (!json3 && options.quiet === true && options.verbose === true) {
110630
- context4.output.error("--quiet and --verbose are mutually exclusive");
110842
+ writeJsonError(context4.output, options, "--quiet and --verbose are mutually exclusive", "VALIDATION_FAILED");
110631
110843
  context4.setExitCode(2);
110632
110844
  return;
110633
110845
  }
110634
110846
  if (!json3 && options.silent === true && (options.quiet === true || options.verbose === true)) {
110635
- context4.output.error("--silent cannot be combined with --quiet or --verbose");
110847
+ writeJsonError(context4.output, options, "--silent cannot be combined with --quiet or --verbose", "VALIDATION_FAILED");
110636
110848
  context4.setExitCode(2);
110637
110849
  return;
110638
110850
  }
110639
110851
  if (options.steer === true && (json3 || options.async === true)) {
110640
- context4.output.error("--steer is synchronous and in-process; it cannot be combined with --json or --async");
110852
+ writeJsonError(context4.output, options, "--steer is synchronous and in-process; it cannot be combined with --json or --async", "VALIDATION_FAILED");
110641
110853
  context4.setExitCode(2);
110642
110854
  return;
110643
110855
  }
110644
110856
  const requestedDetail = options.detail;
110645
110857
  if (requestedDetail !== undefined && !["minimal", "invocation", "full"].includes(requestedDetail)) {
110646
- context4.output.error("--detail must be one of: minimal, invocation, full");
110858
+ writeJsonError(context4.output, options, "--detail must be one of: minimal, invocation, full", "VALIDATION_FAILED");
110647
110859
  context4.setExitCode(2);
110648
110860
  return;
110649
110861
  }
110650
110862
  const detail = options.verbose === true ? "full" : requestedDetail ?? "invocation";
110651
110863
  if (process.env[WORKFLOW_RUN_ACTIVE_ENV] === "1") {
110652
- context4.output.error(`workflow run: refusing to start \u2014 already inside an active workflow run (${WORKFLOW_RUN_ACTIVE_ENV}=1).
110864
+ writeJsonError(context4.output, options, `workflow run: refusing to start \u2014 already inside an active workflow run (${WORKFLOW_RUN_ACTIVE_ENV}=1).
110653
110865
  ` + `A pipeline that starts another pipeline forks a worktree and an agent run per level, without bound.
110654
110866
  ` + `If you are an agent running inside a pipeline step: do NOT start a pipeline here. Report what you
110655
- ` + "needed and let the operator run it from a clean shell.");
110867
+ ` + "needed and let the operator run it from a clean shell.", "VALIDATION_FAILED");
110656
110868
  context4.setExitCode(1);
110657
110869
  return;
110658
110870
  }
@@ -110847,7 +111059,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
110847
111059
  if (options.answer !== undefined) {
110848
111060
  const v = String(options.answer).toLowerCase();
110849
111061
  if (v !== "yes" && v !== "no" && v !== "cancel") {
110850
- context4.output.error(`Invalid --answer value "${options.answer}" - must be yes, no, or cancel.`);
111062
+ writeJsonError(context4.output, options, `Invalid --answer value "${options.answer}" - must be yes, no, or cancel.`, "VALIDATION_FAILED");
110851
111063
  context4.setExitCode(2);
110852
111064
  return;
110853
111065
  }
@@ -110861,7 +111073,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
110861
111073
  if (targetId === undefined) {
110862
111074
  const latest = await svc.latestPausedRun();
110863
111075
  if (latest === null) {
110864
- context4.output.error("No paused workflow run to continue.");
111076
+ writeJsonError(context4.output, options, "No paused workflow run to continue.", "NOT_FOUND");
110865
111077
  context4.setExitCode(1);
110866
111078
  return;
110867
111079
  }
@@ -110874,7 +111086,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
110874
111086
  node: "continue"
110875
111087
  });
110876
111088
  if (answer.value !== "yes") {
110877
- context4.output.error(`Aborted - run ${latest.runId} not resumed.`);
111089
+ writeJsonError(context4.output, options, `Aborted - run ${latest.runId} not resumed.`, "GUARD_DENIED");
110878
111090
  context4.setExitCode(1);
110879
111091
  return;
110880
111092
  }
@@ -110900,7 +111112,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
110900
111112
  const force = options.force === true;
110901
111113
  const minutes = force ? 0 : Number.parseInt(options.olderThan ?? "30", 10);
110902
111114
  if (!Number.isFinite(minutes) || minutes < 0) {
110903
- context4.output.error(`Invalid --older-than value: ${options.olderThan}`);
111115
+ writeJsonError(context4.output, options, `Invalid --older-than value: ${options.olderThan}`, "VALIDATION_FAILED");
110904
111116
  context4.setExitCode(2);
110905
111117
  return;
110906
111118
  }
@@ -110945,7 +111157,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
110945
111157
  return;
110946
111158
  }
110947
111159
  if (result.status === "not_found") {
110948
- context4.output.error(`Run ${runId} not found.`);
111160
+ writeJsonError(context4.output, options, `Run ${runId} not found.`, "NOT_FOUND");
110949
111161
  context4.setExitCode(1);
110950
111162
  return;
110951
111163
  }
@@ -110967,14 +111179,14 @@ Monitor with: spur workflow trace ${runId2} --follow`);
110967
111179
  });
110968
111180
  workflow.command("show").description("Render a workflow definition: mermaid FSM diagram (default) or declared-step todo checklist.").argument("<file>", "Workflow YAML file").option("--format <name>", "Projection to render: mermaid (default) or todo", "mermaid").option(...SHARED_OPTIONS.jsonSupported).action(async (file2, options) => {
110969
111181
  if (options.format !== "mermaid" && options.format !== "todo") {
110970
- context4.output.error(`workflow show: unknown --format '${options.format}' \u2014 expected mermaid or todo`);
111182
+ writeJsonError(context4.output, options, `workflow show: unknown --format '${options.format}' \u2014 expected mermaid or todo`, "VALIDATION_FAILED");
110971
111183
  context4.setExitCode(1);
110972
111184
  return;
110973
111185
  }
110974
111186
  const resolved = resolveWorkflowFile(context4.cwd, file2);
110975
111187
  if (resolved.path === null) {
110976
111188
  const [probedProject, probedBundled] = resolved.probed;
110977
- context4.output.error(`workflow show: file not found: ${probedProject}${probedBundled !== null ? ` (bundled: ${probedBundled})` : ""}`);
111189
+ writeJsonError(context4.output, options, `workflow show: file not found: ${probedProject}${probedBundled !== null ? ` (bundled: ${probedBundled})` : ""}`, "NOT_FOUND");
110978
111190
  context4.setExitCode(1);
110979
111191
  return;
110980
111192
  }
@@ -110983,7 +111195,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
110983
111195
  try {
110984
111196
  def = await loadWorkflowDef(filePath, { validateSchema: true });
110985
111197
  } catch (err) {
110986
- context4.output.error(`workflow show: cannot read or parse ${file2} \u2014 ${err instanceof Error ? err.message : String(err)}`);
111198
+ writeJsonError(context4.output, options, `workflow show: cannot read or parse ${file2} \u2014 ${err instanceof Error ? err.message : String(err)}`, "VALIDATION_FAILED");
110987
111199
  context4.setExitCode(1);
110988
111200
  return;
110989
111201
  }
@@ -111011,38 +111223,38 @@ Monitor with: spur workflow trace ${runId2} --follow`);
111011
111223
  const svc = makeSvc();
111012
111224
  const last = parseInt(options.last, 10);
111013
111225
  if (Number.isNaN(last) || last < 1) {
111014
- context4.output.error("--last must be a positive integer");
111226
+ writeJsonError(context4.output, options, "--last must be a positive integer", "VALIDATION_FAILED");
111015
111227
  context4.setExitCode(1);
111016
111228
  return;
111017
111229
  }
111018
111230
  const pollMs = parseInt(options.poll, 10);
111019
111231
  if (Number.isNaN(pollMs) || pollMs < 50) {
111020
- context4.output.error("--poll must be an integer of at least 50ms");
111232
+ writeJsonError(context4.output, options, "--poll must be an integer of at least 50ms", "VALIDATION_FAILED");
111021
111233
  context4.setExitCode(1);
111022
111234
  return;
111023
111235
  }
111024
111236
  if (options.follow === true && runId === undefined) {
111025
- context4.output.error("--follow requires a run-id");
111237
+ writeJsonError(context4.output, options, "--follow requires a run-id", "VALIDATION_FAILED");
111026
111238
  context4.setExitCode(1);
111027
111239
  return;
111028
111240
  }
111029
111241
  if (options.follow === true && options.json === true) {
111030
- context4.output.error("--follow is a human streaming mode and cannot be combined with --json");
111242
+ writeJsonError(context4.output, options, "--follow is a human streaming mode and cannot be combined with --json", "VALIDATION_FAILED");
111031
111243
  context4.setExitCode(1);
111032
111244
  return;
111033
111245
  }
111034
111246
  if (options.output === true && options.follow !== true) {
111035
- context4.output.error("--output requires --follow");
111247
+ writeJsonError(context4.output, options, "--output requires --follow", "VALIDATION_FAILED");
111036
111248
  context4.setExitCode(1);
111037
111249
  return;
111038
111250
  }
111039
111251
  if (options.output === true && options.json === true) {
111040
- context4.output.error("--output is a human streaming mode and cannot be combined with --json");
111252
+ writeJsonError(context4.output, options, "--output is a human streaming mode and cannot be combined with --json", "VALIDATION_FAILED");
111041
111253
  context4.setExitCode(1);
111042
111254
  return;
111043
111255
  }
111044
111256
  if (options.status !== undefined && !["done", "failed", "running"].includes(options.status)) {
111045
- context4.output.error("--status must be one of: done, failed, running");
111257
+ writeJsonError(context4.output, options, "--status must be one of: done, failed, running", "VALIDATION_FAILED");
111046
111258
  context4.setExitCode(1);
111047
111259
  return;
111048
111260
  }