@gobing-ai/spur 0.3.65 → 0.3.67

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)
@@ -71765,20 +71901,6 @@ function citedLinesNameSubject(subjectTokens, cited) {
71765
71901
  const isRowId = (t) => /^(r|ac)-?\d+$/.test(t);
71766
71902
  return subjectTokens.every(isRowId);
71767
71903
  }
71768
- function extractPathSubjectTokens(path9) {
71769
- const basename4 = path9.split("/").pop() ?? "";
71770
- const stem = basename4.replace(/\.[^.]+$/, "");
71771
- const tokens = new Set;
71772
- for (const part of stem.split(/[-_.]+/)) {
71773
- if (!/^[A-Za-z][A-Za-z0-9]*$/.test(part))
71774
- continue;
71775
- const lower = part.toLowerCase();
71776
- if (lower.length < 3)
71777
- continue;
71778
- tokens.add(lower);
71779
- }
71780
- return [...tokens];
71781
- }
71782
71904
  function hasAdjacentFileLineColumns(body) {
71783
71905
  const lines = body.split(`
71784
71906
  `);
@@ -71904,15 +72026,23 @@ var init_task_check = __esm(() => {
71904
72026
  const reqBody = doc2.getSection("Requirements");
71905
72027
  if (reqBody !== null && !isPlaceholderBody(reqBody)) {
71906
72028
  const blocks = reqBody.trim().split(/\n\s*\n/).filter((b) => b.trim().length > 0);
72029
+ const R_ITEM_RE = /^\s*[-*]?\s*(?:\[[ xX]\]\s*)?[*_]{0,2}R\d+\.?[*_]{0,2}\s/;
71907
72030
  let numbered = 0;
72031
+ let proseBlocks = 0;
72032
+ let seenItem = false;
71908
72033
  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++;
72034
+ const itemLines = block.split(`
72035
+ `).filter((l) => R_ITEM_RE.test(l)).length;
72036
+ if (itemLines > 0) {
72037
+ numbered += itemLines;
72038
+ seenItem = true;
72039
+ continue;
72040
+ }
72041
+ if (!seenItem || /^\s*[*_]{1,2}[^*_\s]/.test(block.trimStart())) {
72042
+ proseBlocks++;
71913
72043
  }
71914
72044
  }
71915
- if (numbered === 0 || numbered < blocks.length * 0.5) {
72045
+ if (numbered === 0 || proseBlocks > numbered) {
71916
72046
  findings.push({
71917
72047
  layer: "L3",
71918
72048
  code: FINDING_CODES.L3_REQUIREMENTS_FORMAT,
@@ -72041,6 +72171,26 @@ var init_task_check = __esm(() => {
72041
72171
  message: `Task is ${status} but carries ${openBoxes} unchecked checklist box(es) \u2014 flip to [x] or remove before closing`
72042
72172
  });
72043
72173
  }
72174
+ const reviewBody = doc2.getSection("Review");
72175
+ const testingBody = doc2.getSection("Testing");
72176
+ if (reviewBody !== null && testingBody !== null) {
72177
+ const lastVerdict = (body) => {
72178
+ const lines = [...body.matchAll(/\**\s*Verdict\b\**\s*[:\uFF1A]?\s*([^*()\n]+)/gi)];
72179
+ return (lines[lines.length - 1]?.[1] ?? "").replace(/\*/g, "").trim();
72180
+ };
72181
+ const reviewVerdict = lastVerdict(reviewBody);
72182
+ const nonPassing = /\bPARTIAL\b|\bFAIL\b|request-changes/i.test(reviewVerdict);
72183
+ const testingVerdict = lastVerdict(testingBody);
72184
+ if (nonPassing && /^PASS\b/i.test(testingVerdict)) {
72185
+ findings.push({
72186
+ layer: "L3",
72187
+ code: FINDING_CODES.L3_REVIEW_TESTING_CONTRADICTION,
72188
+ severity: "error",
72189
+ section: "Review",
72190
+ 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)`
72191
+ });
72192
+ }
72193
+ }
72044
72194
  }
72045
72195
  }
72046
72196
  async runL4(doc2, fm, status, findings, featuresDir, tasksDir, wbs) {
@@ -72070,7 +72220,7 @@ var init_task_check = __esm(() => {
72070
72220
  });
72071
72221
  }
72072
72222
  }
72073
- await this.checkLineAnchors(doc2, tasksDir, findings);
72223
+ await this.checkLineAnchors(doc2, tasksDir, findings, status);
72074
72224
  const featureId2 = fm.feature_id ?? fm["feature-id"];
72075
72225
  const parentWbs = fm.parent_wbs ?? fm["parent-wbs"];
72076
72226
  const deps = fm.dependencies;
@@ -72178,6 +72328,18 @@ var init_task_check = __esm(() => {
72178
72328
  section: "Plan",
72179
72329
  message: "Parent task has sub-tasks but its Plan has no sub-task roster (decomposition.md)"
72180
72330
  });
72331
+ return;
72332
+ }
72333
+ const declared = new Set(this.extractDependencyWbs(doc2.frontmatterData?.dependencies));
72334
+ const undeclared = openKids.filter((kid) => !declared.has(kid.wbs)).map((kid) => kid.wbs);
72335
+ if (undeclared.length > 0) {
72336
+ findings.push({
72337
+ layer: "L4",
72338
+ code: FINDING_CODES.L4_ROLLUP_ROSTER_NOT_DECLARED_DEPENDENCY,
72339
+ severity: "warning",
72340
+ section: "Plan",
72341
+ 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(" ")})`
72342
+ });
72181
72343
  }
72182
72344
  }
72183
72345
  hasSubtaskRoster(planBody, kids) {
@@ -72297,6 +72459,9 @@ var init_task_check = __esm(() => {
72297
72459
  }
72298
72460
  }
72299
72461
  checkGateLanguage(doc2, findings) {
72462
+ const declaredDeps = doc2.frontmatterData?.dependencies;
72463
+ if (Array.isArray(declaredDeps) && declaredDeps.length > 0)
72464
+ return;
72300
72465
  for (const section of ["Background", "Requirements", "Design", "Acceptance Criteria", "Plan"]) {
72301
72466
  const body = doc2.getSection(section);
72302
72467
  if (body === null)
@@ -72328,8 +72493,9 @@ var init_task_check = __esm(() => {
72328
72493
  return null;
72329
72494
  }
72330
72495
  }
72331
- async checkLineAnchors(doc2, tasksDir, findings) {
72496
+ async checkLineAnchors(doc2, tasksDir, findings, status) {
72332
72497
  const projectRoot = resolveProjectRootFromTasksDir(tasksDir);
72498
+ const terminal = status === "done" || status === "cancelled";
72333
72499
  for (const section of ["Testing", "Solution"]) {
72334
72500
  const body = doc2.getSection(section);
72335
72501
  if (body === null || isPlaceholderBody(body))
@@ -72389,45 +72555,30 @@ var init_task_check = __esm(() => {
72389
72555
  message: `Stale line anchor \`${cite.raw}\` \u2014 line ${cite.startLine}${cite.endLine ? `-${cite.endLine}` : ""} outside file (${lineCount} lines)`
72390
72556
  });
72391
72557
  reported++;
72392
- } else {
72393
- const citedWindow = raw.split(`
72394
- `).slice(cite.startLine - 1, cite.endLine ?? cite.startLine).join(`
72395
- `) || "";
72396
- const citingRow = body.split(`
72558
+ continue;
72559
+ }
72560
+ if (terminal)
72561
+ continue;
72562
+ const citingRow = body.split(`
72397
72563
  `).find((l) => l.includes(`\`${cite.raw}\``)) ?? body.split(`
72398
72564
  `).find((l) => l.includes(cite.raw)) ?? "";
72399
- const tokens = extractSubjectTokens(citingRow);
72400
- const effectiveTokens = tokens.length === 0 && section === "Solution" ? extractPathSubjectTokens(cite.path) : tokens;
72401
- if (!citedLinesNameSubject(effectiveTokens, citedWindow)) {
72402
- let driftLine = -1;
72403
- const fileLines = raw.split(`
72404
- `);
72405
- for (let i2 = 0;i2 < fileLines.length; i2++) {
72406
- if (citedLinesNameSubject(effectiveTokens, fileLines[i2] ?? "")) {
72407
- driftLine = i2 + 1;
72408
- break;
72409
- }
72410
- }
72411
- if (driftLine > 0) {
72412
- findings.push({
72413
- layer: "L4",
72414
- code: FINDING_CODES.L4_STALE_LINE_ANCHOR,
72415
- severity: "warning",
72416
- section,
72417
- message: `Anchor drift \`${cite.raw}\` \u2014 subject (${effectiveTokens.join(", ")}) cited at ${cite.startLine}${cite.endLine ? `-${cite.endLine}` : ""} now sits at line ${driftLine}; re-point the citation`
72418
- });
72419
- reported++;
72420
- continue;
72421
- }
72422
- findings.push({
72423
- layer: "L4",
72424
- code: FINDING_CODES.L4_ANCHOR_SUBJECT_MISMATCH,
72425
- severity: "warning",
72426
- section,
72427
- message: `Anchor \`${cite.raw}\` subject mismatch \u2014 cited lines do not name the requirement's subject (${effectiveTokens.join(", ") || "none identifiable"}). Rewrite the citation to point at the code that implements this row.`
72428
- });
72429
- reported++;
72430
- }
72565
+ if (extractBacktickLineAnchors(citingRow).length !== 1)
72566
+ continue;
72567
+ const tokens = extractSubjectTokens(citingRow);
72568
+ if (tokens.length === 0)
72569
+ continue;
72570
+ const citedWindow = raw.split(`
72571
+ `).slice(cite.startLine - 1, cite.endLine ?? cite.startLine).join(`
72572
+ `) || "";
72573
+ if (!citedLinesNameSubject(tokens, citedWindow)) {
72574
+ findings.push({
72575
+ layer: "L4",
72576
+ code: FINDING_CODES.L4_ANCHOR_SUBJECT_MISMATCH,
72577
+ severity: "warning",
72578
+ section,
72579
+ message: `Anchor \`${cite.raw}\` subject mismatch \u2014 cited lines do not name the requirement's subject (${tokens.join(", ") || "none identifiable"}). Rewrite the citation to point at the code that implements this row.`
72580
+ });
72581
+ reported++;
72431
72582
  }
72432
72583
  } catch {}
72433
72584
  }
@@ -72656,7 +72807,7 @@ async function structuralSweep(projectRoot) {
72656
72807
  });
72657
72808
  const taskService = new TaskCheckService(fs3, await loadTaskMatrix(projectRoot), locator);
72658
72809
  const findings = [];
72659
- for (const tasksDir of taskDirs) {
72810
+ for (const tasksDir of [activeTasksDir]) {
72660
72811
  if (!await fs3.exists(tasksDir))
72661
72812
  continue;
72662
72813
  for (const fileName of await fs3.readDir(tasksDir)) {
@@ -78058,6 +78209,7 @@ class HistoryService {
78058
78209
  });
78059
78210
  }
78060
78211
  await dao2.alignMessageProvenance();
78212
+ await deriveAssistantDurations(db2);
78061
78213
  }
78062
78214
  return result;
78063
78215
  }
@@ -79111,7 +79263,7 @@ class PlanningWriteService {
79111
79263
  } : {}
79112
79264
  };
79113
79265
  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.");
79266
+ 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
79267
  for (const dup of doc2.duplicateSectionNames) {
79116
79268
  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
79269
  }
@@ -79142,6 +79294,10 @@ function applyMutation(doc2, mutation) {
79142
79294
  case "updateSection":
79143
79295
  if (mutation.sectionName !== undefined && mutation.sectionBody !== undefined) {
79144
79296
  const body = mutation.sectionName.toLowerCase() === "acceptance criteria" ? normalizeAcFence(mutation.sectionBody) : mutation.sectionBody;
79297
+ if (mutation.sectionName.toLowerCase() === "q&a") {
79298
+ appendQaEntry(doc2, body);
79299
+ break;
79300
+ }
79145
79301
  doc2.replaceSection(mutation.sectionName, body);
79146
79302
  }
79147
79303
  break;
@@ -79172,6 +79328,22 @@ ${line}
79172
79328
  `;
79173
79329
  doc2.replaceSection("History", updated);
79174
79330
  }
79331
+ function appendQaEntry(doc2, body) {
79332
+ const REPLACE_MARKER = "<!-- qa:replace -->";
79333
+ if (body.trimStart().startsWith(REPLACE_MARKER)) {
79334
+ doc2.replaceSection("Q&A", body.trimStart().slice(REPLACE_MARKER.length).trimStart());
79335
+ return;
79336
+ }
79337
+ const existing = doc2.getSection("Q&A") ?? "";
79338
+ const entry = `#### Q&A entry \u2014 ${new Date().toISOString()}
79339
+
79340
+ ${body.trim()}
79341
+ `;
79342
+ const updated = existing.trim().length > 0 ? `${existing.trimEnd()}
79343
+
79344
+ ${entry}` : entry;
79345
+ doc2.replaceSection("Q&A", updated);
79346
+ }
79175
79347
  function resolveEventName(kind, domain2, statusChanged) {
79176
79348
  if (kind === "create") {
79177
79349
  return domain2 === "task" ? "task.created" : "feature.created";
@@ -81838,7 +82010,7 @@ class TaskService {
81838
82010
  await this.writeService.updateSection(ref, "Testing", testingBody);
81839
82011
  result.testingWritten = true;
81840
82012
  }
81841
- if (sectionIsBare(doc2, "Review")) {
82013
+ if (sectionIsBare(doc2, "Review") || isRecordAuthoredReview(doc2.getSection("Review"))) {
81842
82014
  const reviewBody = renderReview(verdict);
81843
82015
  await this.writeService.updateSection(ref, "Review", reviewBody);
81844
82016
  result.reviewWritten = true;
@@ -82163,8 +82335,11 @@ ${block}` : block);
82163
82335
  }
82164
82336
  async resolveTaskFile(wbs) {
82165
82337
  const result = await this.findTaskFileName(wbs);
82166
- if (!result)
82167
- throw new Error(`Task ${wbs} not found in any registered task folder`);
82338
+ if (!result) {
82339
+ const err = new Error(`Task ${wbs} not found in any registered task folder`);
82340
+ err.cliCode = "NOT_FOUND";
82341
+ throw err;
82342
+ }
82168
82343
  return result.filePath;
82169
82344
  }
82170
82345
  async findTaskFileName(wbs) {
@@ -82301,6 +82476,11 @@ function extractRequirements(text4) {
82301
82476
  let colMap = null;
82302
82477
  for (const line of lines) {
82303
82478
  const trimmed = line.trim();
82479
+ if (inTable && /^#{1,6}\s/.test(trimmed)) {
82480
+ inTable = false;
82481
+ colMap = null;
82482
+ continue;
82483
+ }
82304
82484
  if (!trimmed.startsWith("|"))
82305
82485
  continue;
82306
82486
  const cells = splitTableCells(trimmed);
@@ -87564,6 +87744,7 @@ __export(exports_src2, {
87564
87744
  looksLikeOpaqueId: () => looksLikeOpaqueId,
87565
87745
  loadAcceptedFindings: () => loadAcceptedFindings,
87566
87746
  isSystemEventEnvelopeV2: () => isSystemEventEnvelopeV2,
87747
+ isRecordAuthoredReview: () => isRecordAuthoredReview,
87567
87748
  isPortLive: () => isPortLive,
87568
87749
  isPortAvailable: () => isPortAvailable,
87569
87750
  isFindingCode: () => isFindingCode,
@@ -95318,12 +95499,12 @@ async function runAgentList(svc, context4, opts) {
95318
95499
  }
95319
95500
  async function runAgentCreate(id, context4, flags) {
95320
95501
  if (id === undefined) {
95321
- context4.output.error("agent create requires <id>");
95502
+ writeJsonError(context4.output, jsonFlags(flags), "agent create requires <id>", "VALIDATION_FAILED");
95322
95503
  return 2;
95323
95504
  }
95324
95505
  const type = typeof flags.type === "string" ? flags.type : "";
95325
95506
  if (type === "") {
95326
- context4.output.error("agent create requires --type <agent-type>");
95507
+ writeJsonError(context4.output, jsonFlags(flags), "agent create requires --type <agent-type>", "VALIDATION_FAILED");
95327
95508
  return 2;
95328
95509
  }
95329
95510
  const tags = typeof flags.tags === "string" ? flags.tags : "";
@@ -95423,6 +95604,13 @@ async function runAgentDelete(id, context4, flags) {
95423
95604
  return 1;
95424
95605
  }
95425
95606
  }
95607
+ function jsonFlags(flags) {
95608
+ const envelope2 = flags.jsonEnvelope ?? flags["json-envelope"];
95609
+ return {
95610
+ json: flags.json === true,
95611
+ jsonEnvelope: typeof envelope2 === "boolean" ? envelope2 : undefined
95612
+ };
95613
+ }
95426
95614
  async function runAgentRun(prompt, context4, flags, deps) {
95427
95615
  const bus = new EventBus;
95428
95616
  const ledger = await attachSystemEventLedger(bus, context4);
@@ -95431,19 +95619,19 @@ async function runAgentRun(prompt, context4, flags, deps) {
95431
95619
  if (flags.drain === true || typeof flags.spec === "string") {
95432
95620
  const { prompt: drained, flags: rewritten } = await drainIntoPrompt(prompt, context4, flags);
95433
95621
  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`);
95622
+ writeJsonError(context4.output, jsonFlags(flags), `--spec "${flags.spec}" does not match a team agent spec`, "VALIDATION_FAILED");
95435
95623
  return 2;
95436
95624
  }
95437
95625
  const invalid2 = validateAgentSelector(rewritten, context4);
95438
95626
  if (invalid2 !== null) {
95439
- context4.output.error(invalid2);
95627
+ writeJsonError(context4.output, jsonFlags(flags), invalid2, "VALIDATION_FAILED");
95440
95628
  return 2;
95441
95629
  }
95442
95630
  return await svc.run(drained, rewritten, deps);
95443
95631
  }
95444
95632
  const invalid = validateAgentSelector(flags, context4);
95445
95633
  if (invalid !== null) {
95446
- context4.output.error(invalid);
95634
+ writeJsonError(context4.output, jsonFlags(flags), invalid, "VALIDATION_FAILED");
95447
95635
  return 2;
95448
95636
  }
95449
95637
  return await svc.run(prompt, flags, deps);
@@ -96299,7 +96487,7 @@ function registerFeatureCommand(program2, context4) {
96299
96487
  try {
96300
96488
  const result = await svc.show(id);
96301
96489
  if (result === null) {
96302
- writeJsonError(context4.output, options, `Feature ${id} not found`);
96490
+ writeJsonError(context4.output, options, `Feature ${id} not found`, "NOT_FOUND");
96303
96491
  context4.setExitCode(1);
96304
96492
  return;
96305
96493
  }
@@ -96325,7 +96513,7 @@ function registerFeatureCommand(program2, context4) {
96325
96513
  let result;
96326
96514
  if (options.section !== undefined) {
96327
96515
  if (options.fromFile === undefined) {
96328
- context4.output.error("--from-file is required with --section");
96516
+ writeJsonError(context4.output, options, "--from-file is required with --section", "VALIDATION_FAILED");
96329
96517
  context4.setExitCode(2);
96330
96518
  return;
96331
96519
  }
@@ -96337,13 +96525,13 @@ function registerFeatureCommand(program2, context4) {
96337
96525
  context4.output.write(`Updated section '${options.section}' in feature ${result.ref.id}`);
96338
96526
  }
96339
96527
  } else if (options.fromFile !== undefined) {
96340
- context4.output.error("--section is required with --from-file");
96528
+ writeJsonError(context4.output, options, "--section is required with --from-file", "VALIDATION_FAILED");
96341
96529
  context4.setExitCode(2);
96342
96530
  return;
96343
96531
  }
96344
96532
  if (options.field !== undefined) {
96345
96533
  if (options.value === undefined) {
96346
- context4.output.error("--value is required with --field");
96534
+ writeJsonError(context4.output, options, "--value is required with --field", "VALIDATION_FAILED");
96347
96535
  context4.setExitCode(2);
96348
96536
  return;
96349
96537
  }
@@ -96352,7 +96540,7 @@ function registerFeatureCommand(program2, context4) {
96352
96540
  context4.output.write(`Updated ${options.field} on feature ${result.ref.id}`);
96353
96541
  }
96354
96542
  } else if (options.value !== undefined) {
96355
- context4.output.error("--field is required with --value");
96543
+ writeJsonError(context4.output, options, "--field is required with --value", "VALIDATION_FAILED");
96356
96544
  context4.setExitCode(2);
96357
96545
  return;
96358
96546
  }
@@ -96363,7 +96551,7 @@ function registerFeatureCommand(program2, context4) {
96363
96551
  }
96364
96552
  }
96365
96553
  if (result === undefined) {
96366
- context4.output.error("Either <status>, --field/--value, or --section/--from-file is required");
96554
+ writeJsonError(context4.output, options, "Either <status>, --field/--value, or --section/--from-file is required", "VALIDATION_FAILED");
96367
96555
  context4.setExitCode(2);
96368
96556
  return;
96369
96557
  }
@@ -96387,7 +96575,7 @@ function registerFeatureCommand(program2, context4) {
96387
96575
  try {
96388
96576
  const initial = await svc.show(id);
96389
96577
  if (initial === null) {
96390
- writeJsonError(context4.output, options, `Feature ${id} not found`);
96578
+ writeJsonError(context4.output, options, `Feature ${id} not found`, "NOT_FOUND");
96391
96579
  context4.setExitCode(1);
96392
96580
  return;
96393
96581
  }
@@ -96419,7 +96607,7 @@ function registerFeatureCommand(program2, context4) {
96419
96607
  next = forwardPath[current];
96420
96608
  }
96421
96609
  if (current !== target) {
96422
- context4.output.error(`${id}: cannot reach '${target}' from '${current}' along the forward path`);
96610
+ writeJsonError(context4.output, options, `${id}: cannot reach '${target}' from '${current}' along the forward path`, "GUARD_DENIED");
96423
96611
  context4.setExitCode(1);
96424
96612
  return;
96425
96613
  }
@@ -96495,12 +96683,12 @@ function registerFeatureCommand(program2, context4) {
96495
96683
  const svc = await makeService(context4, options.folder);
96496
96684
  try {
96497
96685
  if (options.all && options.feature) {
96498
- context4.output.error("--feature <id> and --all are mutually exclusive");
96686
+ writeJsonError(context4.output, options, "--feature <id> and --all are mutually exclusive", "VALIDATION_FAILED");
96499
96687
  context4.setExitCode(2);
96500
96688
  return;
96501
96689
  }
96502
96690
  if (!options.all && !options.feature) {
96503
- context4.output.error("--feature <id> or --all is required (refusing silent all-features sweep)");
96691
+ writeJsonError(context4.output, options, "--feature <id> or --all is required (refusing silent all-features sweep)", "VALIDATION_FAILED");
96504
96692
  context4.setExitCode(2);
96505
96693
  return;
96506
96694
  }
@@ -96534,9 +96722,9 @@ function registerFeatureCommand(program2, context4) {
96534
96722
  for (const fid of ids) {
96535
96723
  const fileName = entries.find((n3) => n3.match(new RegExp(`^${fid}_.+\\.md$`)));
96536
96724
  if (!fileName) {
96537
- context4.output.error(`Feature ${fid} not found`);
96725
+ writeJsonError(context4.output, options, `Feature ${fid} not found`, "NOT_FOUND");
96538
96726
  context4.setExitCode(1);
96539
- continue;
96727
+ return;
96540
96728
  }
96541
96729
  const result = await svc.check(`${featuresDir}/${fileName}`, fid, {
96542
96730
  strict,
@@ -96586,7 +96774,7 @@ ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
96586
96774
  const svc = await makeService(context4, options.folder);
96587
96775
  try {
96588
96776
  if (!options.all && !id) {
96589
- context4.output.error("Feature ID is required unless --all is passed");
96777
+ writeJsonError(context4.output, options, "Feature ID is required unless --all is passed", "VALIDATION_FAILED");
96590
96778
  context4.setExitCode(2);
96591
96779
  return;
96592
96780
  }
@@ -96675,7 +96863,7 @@ import { createRequire } from "module";
96675
96863
  var CLI_CONFIG = {
96676
96864
  binaryName: "spur",
96677
96865
  binaryLabel: "spur",
96678
- binaryVersion: "0.3.65",
96866
+ binaryVersion: "0.3.67",
96679
96867
  configDir: ".spur",
96680
96868
  configFile: ".spur/config.yaml",
96681
96869
  databaseFile: ".spur/spur.db"
@@ -97362,7 +97550,7 @@ function registerMessageCommand(program2, context4) {
97362
97550
  const svc = new TeamService(context4);
97363
97551
  const intervalMs = parseInterval2(options.interval);
97364
97552
  if (intervalMs === null) {
97365
- context4.output.error(`invalid --interval "${options.interval}" (expected a positive integer ms)`);
97553
+ writeJsonError(context4.output, options, `invalid --interval "${options.interval}" (expected a positive integer ms)`, "VALIDATION_FAILED");
97366
97554
  context4.setExitCode(2);
97367
97555
  return;
97368
97556
  }
@@ -97527,7 +97715,7 @@ async function runMessageInbox(svc, context4, options) {
97527
97715
  async function runMessageReply(svc, context4, msgId, body, options) {
97528
97716
  const trimmed = body.trim();
97529
97717
  if (trimmed === "") {
97530
- context4.output.error("message reply requires a non-empty body");
97718
+ writeJsonError(context4.output, options, "message reply requires a non-empty body", "VALIDATION_FAILED");
97531
97719
  return 2;
97532
97720
  }
97533
97721
  const result = await svc.replyToMessage(msgId, trimmed);
@@ -97930,17 +98118,17 @@ function registerRuleCommand(program2, context4) {
97930
98118
  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
98119
  const last = parseInt(options.last, 10);
97932
98120
  if (!Number.isInteger(last) || last < 1) {
97933
- context4.output.error("--last must be a positive integer");
98121
+ writeJsonError(context4.output, options, "--last must be a positive integer", "VALIDATION_FAILED");
97934
98122
  context4.setExitCode(1);
97935
98123
  return;
97936
98124
  }
97937
98125
  if (options.status !== undefined && !["done", "failed"].includes(options.status)) {
97938
- context4.output.error("--status must be one of: done, failed");
98126
+ writeJsonError(context4.output, options, "--status must be one of: done, failed", "VALIDATION_FAILED");
97939
98127
  context4.setExitCode(1);
97940
98128
  return;
97941
98129
  }
97942
98130
  if (options.since !== undefined && Number.isNaN(Date.parse(options.since))) {
97943
- context4.output.error("--since must be a valid ISO date");
98131
+ writeJsonError(context4.output, options, "--since must be a valid ISO date", "VALIDATION_FAILED");
97944
98132
  context4.setExitCode(1);
97945
98133
  return;
97946
98134
  }
@@ -108141,7 +108329,7 @@ function registerServeCommand(program2, context4, options = {}) {
108141
108329
  } catch (err) {
108142
108330
  writeJsonError(context4.output, options2, err instanceof Error ? err.message : String(err));
108143
108331
  if (context4.env?.SPUR_DEBUG === "1" && err instanceof Error && err.stack) {
108144
- context4.output.error(err.stack);
108332
+ writeJsonError(context4.output, options2, err.stack, "INTERNAL_ERROR");
108145
108333
  }
108146
108334
  context4.setExitCode(1);
108147
108335
  }
@@ -109168,12 +109356,12 @@ function registerTaskCommand(program2, context4) {
109168
109356
  const task = program2.command("task").summary("manage tasks");
109169
109357
  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
109358
  if (options.template !== undefined && !TASK_VARIANTS.includes(options.template)) {
109171
- context4.output.error(`Unknown template variant "${options.template}". Valid: ${TASK_VARIANTS.join(", ")}`);
109359
+ writeJsonError(context4.output, options, `Unknown template variant "${options.template}". Valid: ${TASK_VARIANTS.join(", ")}`, "VALIDATION_FAILED");
109172
109360
  context4.setExitCode(2);
109173
109361
  return;
109174
109362
  }
109175
109363
  if (options.dedupeWithin !== undefined && (!Number.isInteger(options.dedupeWithin) || options.dedupeWithin <= 0)) {
109176
- context4.output.error("--dedupe-within must be a positive integer");
109364
+ writeJsonError(context4.output, options, "--dedupe-within must be a positive integer", "VALIDATION_FAILED");
109177
109365
  context4.setExitCode(2);
109178
109366
  return;
109179
109367
  }
@@ -109267,7 +109455,8 @@ function registerTaskCommand(program2, context4) {
109267
109455
  ${result.content}`);
109268
109456
  }
109269
109457
  } catch (err) {
109270
- writeJsonError(context4.output, options, String(err));
109458
+ const cliCode = err?.cliCode;
109459
+ writeJsonError(context4.output, options, String(err), "INTERNAL_ERROR", cliCode !== undefined ? { cliCode } : undefined);
109271
109460
  context4.setExitCode(1);
109272
109461
  }
109273
109462
  });
@@ -109287,7 +109476,7 @@ ${result.content}`);
109287
109476
  try {
109288
109477
  if (options.section !== undefined) {
109289
109478
  if (options.fromFile === undefined) {
109290
- context4.output.error("--from-file is required with --section");
109479
+ writeJsonError(context4.output, options, "--from-file is required with --section", "VALIDATION_FAILED");
109291
109480
  context4.setExitCode(2);
109292
109481
  return;
109293
109482
  }
@@ -109319,7 +109508,7 @@ ${result.content}`);
109319
109508
  }
109320
109509
  const ok = await runDoneGateCheck(context4, wbs, options.folder, status);
109321
109510
  if (!ok) {
109322
- context4.output.error(`Lifecycle transition blocked: \`spur task check ${wbs}\` failed. Fix the findings before transitioning to ${status}.`);
109511
+ writeJsonError(context4.output, options, `Lifecycle transition blocked: \`spur task check ${wbs}\` failed. Fix the findings before transitioning to ${status}.`, "GUARD_DENIED");
109323
109512
  context4.setExitCode(1);
109324
109513
  return;
109325
109514
  }
@@ -109350,7 +109539,7 @@ ${result.content}`);
109350
109539
  return;
109351
109540
  }
109352
109541
  if (guardOutcome.kind === "deny") {
109353
- context4.output.error(guardOutcome.message);
109542
+ writeJsonError(context4.output, options, guardOutcome.message, "GUARD_DENIED");
109354
109543
  context4.setExitCode(1);
109355
109544
  return;
109356
109545
  }
@@ -109397,7 +109586,7 @@ ${result.content}`);
109397
109586
  }
109398
109587
  } catch (err) {
109399
109588
  if (err instanceof SectionMutationError) {
109400
- context4.output.error(`[${err.code}] ${err.message}`);
109589
+ writeJsonError(context4.output, options, `[${err.code}] ${err.message}`, "INTERNAL_ERROR");
109401
109590
  context4.setExitCode(err.code === "usage" ? 2 : 3);
109402
109591
  } else {
109403
109592
  writeJsonError(context4.output, options, String(err));
@@ -109415,7 +109604,7 @@ ${result.content}`);
109415
109604
  ` + "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
109605
  const allowedOps = ["set", "add", "remove", "clear"];
109417
109606
  if (!allowedOps.includes(op)) {
109418
- context4.output.error(`Unknown op "${op}". Allowed: ${allowedOps.join(", ")}.`);
109607
+ writeJsonError(context4.output, options, `Unknown op "${op}". Allowed: ${allowedOps.join(", ")}.`, "VALIDATION_FAILED");
109419
109608
  context4.setExitCode(2);
109420
109609
  return;
109421
109610
  }
@@ -109431,7 +109620,7 @@ ${result.content}`);
109431
109620
  }
109432
109621
  } catch (err) {
109433
109622
  if (err instanceof DependencyMutationError) {
109434
- context4.output.error(`[${err.code}] ${err.message}`);
109623
+ writeJsonError(context4.output, options, `[${err.code}] ${err.message}`, "INTERNAL_ERROR");
109435
109624
  context4.setExitCode(err.code === "usage" ? 2 : 3);
109436
109625
  } else {
109437
109626
  writeJsonError(context4.output, options, String(err));
@@ -109455,18 +109644,18 @@ ${result.content}`);
109455
109644
  ` + "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
109645
  const allowedOps = ["init", "add", "list"];
109457
109646
  if (!allowedOps.includes(op)) {
109458
- context4.output.error(`Unknown op "${op}". Allowed: ${allowedOps.join(", ")}.`);
109647
+ writeJsonError(context4.output, options, `Unknown op "${op}". Allowed: ${allowedOps.join(", ")}.`, "VALIDATION_FAILED");
109459
109648
  context4.setExitCode(2);
109460
109649
  return;
109461
109650
  }
109462
109651
  const typedOp = op;
109463
109652
  if (typedOp === "add" && typeof name !== "string") {
109464
- context4.output.error('op "add" requires a section name argument.');
109653
+ writeJsonError(context4.output, options, 'op "add" requires a section name argument.', "VALIDATION_FAILED");
109465
109654
  context4.setExitCode(2);
109466
109655
  return;
109467
109656
  }
109468
109657
  if ((typedOp === "init" || typedOp === "list") && name !== undefined) {
109469
- context4.output.error(`op "${typedOp}" takes no section name argument.`);
109658
+ writeJsonError(context4.output, options, `op "${typedOp}" takes no section name argument.`, "VALIDATION_FAILED");
109470
109659
  context4.setExitCode(2);
109471
109660
  return;
109472
109661
  }
@@ -109491,7 +109680,7 @@ ${result.content}`);
109491
109680
  }
109492
109681
  } catch (err) {
109493
109682
  if (err instanceof SectionMutationError) {
109494
- context4.output.error(`[${err.code}] ${err.message}`);
109683
+ writeJsonError(context4.output, options, `[${err.code}] ${err.message}`, "INTERNAL_ERROR");
109495
109684
  context4.setExitCode(err.code === "usage" ? 2 : 3);
109496
109685
  } else {
109497
109686
  writeJsonError(context4.output, options, String(err));
@@ -109704,7 +109893,7 @@ ${result.content}`);
109704
109893
  }
109705
109894
  });
109706
109895
  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);
109896
+ const { deriveVerdict: deriveVerdict2 } = await init_src3().then(() => exports_src2);
109708
109897
  const answerPath = options.fromAnswer ?? `.spur/run/${wbs}-verify-answer.txt`;
109709
109898
  let answerText;
109710
109899
  try {
@@ -109716,22 +109905,6 @@ ${result.content}`);
109716
109905
  }
109717
109906
  const taskCheckPassed = true;
109718
109907
  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
109908
  const jsonOut = JSON.stringify({ wbs, ...result, source: "spur-task-verdict" }, null, 2);
109736
109909
  await context4.fs.ensureDir(".spur/run");
109737
109910
  await context4.fs.writeFile(`.spur/run/${wbs}-verdict.json`, `${jsonOut}
@@ -109792,34 +109965,34 @@ ${result.content}`);
109792
109965
  context4.setExitCode(1);
109793
109966
  }
109794
109967
  });
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) => {
109968
+ 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
109969
  const json3 = options.json === true;
109797
109970
  const strict = options.strict === true;
109798
109971
  const asStatus = options.as === undefined ? undefined : canonicalStatusOrRaw(options.as);
109799
109972
  if (options.as !== undefined && !TASK_STATUSES.includes(asStatus ?? "")) {
109800
- context4.output.error(`invalid --as status "${options.as}" (canonical: ${TASK_STATUSES.join(", ")})`);
109973
+ writeJsonError(context4.output, options, `invalid --as status "${options.as}" (canonical: ${TASK_STATUSES.join(", ")})`, "VALIDATION_FAILED");
109801
109974
  context4.setExitCode(2);
109802
109975
  return;
109803
109976
  }
109804
109977
  if (asStatus !== undefined && options.corpus === true) {
109805
- context4.output.error("--as <status> is a single-task target projection and cannot be combined with --corpus");
109978
+ writeJsonError(context4.output, options, "--as <status> is a single-task target projection and cannot be combined with --corpus", "VALIDATION_FAILED");
109806
109979
  context4.setExitCode(2);
109807
109980
  return;
109808
109981
  }
109809
109982
  if (options.fix === true && options.corpus === true) {
109810
- context4.output.error("--fix repairs files in place and cannot be combined with --corpus");
109983
+ writeJsonError(context4.output, options, "--fix repairs files in place and cannot be combined with --corpus", "VALIDATION_FAILED");
109811
109984
  context4.setExitCode(2);
109812
109985
  return;
109813
109986
  }
109814
109987
  try {
109815
109988
  if (options.corpus === true) {
109816
109989
  if (wbs !== undefined) {
109817
- context4.output.error("--corpus validates the whole corpus and cannot be combined with a WBS");
109990
+ writeJsonError(context4.output, options, "--corpus validates the whole corpus and cannot be combined with a WBS", "VALIDATION_FAILED");
109818
109991
  context4.setExitCode(2);
109819
109992
  return;
109820
109993
  }
109821
109994
  if (String(options.since ?? "").startsWith("-")) {
109822
- context4.output.error("--since requires a git ref value (e.g. --since HEAD~1)");
109995
+ writeJsonError(context4.output, options, "--since requires a git ref value (e.g. --since HEAD~1)", "VALIDATION_FAILED");
109823
109996
  context4.setExitCode(2);
109824
109997
  return;
109825
109998
  }
@@ -109852,7 +110025,7 @@ ${result.content}`);
109852
110025
  return;
109853
110026
  }
109854
110027
  if (options.since !== undefined) {
109855
- context4.output.error("--since requires --corpus");
110028
+ writeJsonError(context4.output, options, "--since requires --corpus", "VALIDATION_FAILED");
109856
110029
  context4.setExitCode(2);
109857
110030
  return;
109858
110031
  }
@@ -109884,8 +110057,9 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
109884
110057
  const locator = options.folder !== undefined ? TaskLocator.forSingleDir(context4.fs, tasksDir) : await makeTaskLocator(context4);
109885
110058
  const hit = await locator.findByWbs(wbs);
109886
110059
  if (hit === null) {
109887
- context4.output.error(`Task ${wbs} not found`);
110060
+ writeJsonError(context4.output, options, `Task ${wbs} not found`, "NOT_FOUND");
109888
110061
  context4.setExitCode(1);
110062
+ return;
109889
110063
  } else {
109890
110064
  const result = await svc.check(hit.filePath, wbs, {
109891
110065
  strict,
@@ -109903,7 +110077,7 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
109903
110077
  for (const w of wbsPattern) {
109904
110078
  const fileName = entries.find((n3) => n3.startsWith(`${w}_`) && n3.endsWith(".md"));
109905
110079
  if (!fileName) {
109906
- context4.output.error(`Task ${w} not found`);
110080
+ writeJsonError(context4.output, options, `Task ${w} not found`, "NOT_FOUND");
109907
110081
  context4.setExitCode(1);
109908
110082
  continue;
109909
110083
  }
@@ -109957,7 +110131,7 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
109957
110131
  context4.output.write(`${result.wbs} ${result.filePath}`);
109958
110132
  }
109959
110133
  } else {
109960
- context4.output.error(`No owning task found for ${filePath}`);
110134
+ writeJsonError(context4.output, options, `No owning task found for ${filePath}`, "NOT_FOUND");
109961
110135
  context4.setExitCode(1);
109962
110136
  }
109963
110137
  } catch (err) {
@@ -109976,7 +110150,7 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
109976
110150
  context4.output.write(filePath);
109977
110151
  }
109978
110152
  } else {
109979
- context4.output.error(`Task ${wbs} not found`);
110153
+ writeJsonError(context4.output, options, `Task ${wbs} not found`, "NOT_FOUND");
109980
110154
  context4.setExitCode(1);
109981
110155
  }
109982
110156
  } catch (err) {
@@ -110253,7 +110427,8 @@ async function performTeamStart(agentId, options) {
110253
110427
  const body = await res.json();
110254
110428
  if (res.ok)
110255
110429
  return { ok: true, body };
110256
- return { ok: false, error: body.error ?? `start failed: ${res.status}`, status: res.status };
110430
+ const detail = errorText(body.error);
110431
+ return { ok: false, error: detail ?? `start failed: ${res.status}`, status: res.status };
110257
110432
  } catch (err) {
110258
110433
  return { ok: false, transportError: err };
110259
110434
  }
@@ -110262,11 +110437,11 @@ async function runTeamStart(agentId, options, context4) {
110262
110437
  const result = await performTeamStart(agentId, options);
110263
110438
  if ("transportError" in result) {
110264
110439
  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)})`);
110440
+ 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
110441
  return 1;
110267
110442
  }
110268
110443
  if (!result.ok) {
110269
- context4.output.error(result.error);
110444
+ writeJsonError(context4.output, options, result.error, "INTERNAL_ERROR");
110270
110445
  return 1;
110271
110446
  }
110272
110447
  if (options.json) {
@@ -110276,6 +110451,19 @@ async function runTeamStart(agentId, options, context4) {
110276
110451
  }
110277
110452
  return 0;
110278
110453
  }
110454
+ function errorText(raw2) {
110455
+ if (typeof raw2 === "string")
110456
+ return raw2;
110457
+ if (raw2 !== null && typeof raw2 === "object" && "message" in raw2) {
110458
+ const message = raw2.message;
110459
+ if (typeof message === "string" && message !== "")
110460
+ return message;
110461
+ if ("code" in raw2 && typeof raw2.code === "string") {
110462
+ return raw2.code;
110463
+ }
110464
+ }
110465
+ return raw2 === undefined ? undefined : JSON.stringify(raw2);
110466
+ }
110279
110467
  async function performTeamStop(agentId, options) {
110280
110468
  try {
110281
110469
  const url2 = `${options.server}/team/agents/${encodeURIComponent(agentId)}/stop`;
@@ -110283,7 +110471,8 @@ async function performTeamStop(agentId, options) {
110283
110471
  const body = await res.json();
110284
110472
  if (res.ok)
110285
110473
  return { ok: true, body };
110286
- return { ok: false, error: body.error ?? `stop failed: ${res.status}`, status: res.status };
110474
+ const detail = errorText(body.error);
110475
+ return { ok: false, error: detail ?? `stop failed: ${res.status}`, status: res.status };
110287
110476
  } catch (err) {
110288
110477
  return { ok: false, transportError: err };
110289
110478
  }
@@ -110292,11 +110481,11 @@ async function runTeamStop(agentId, options, context4) {
110292
110481
  const result = await performTeamStop(agentId, options);
110293
110482
  if ("transportError" in result) {
110294
110483
  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)})`);
110484
+ 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
110485
  return 1;
110297
110486
  }
110298
110487
  if (!result.ok) {
110299
- context4.output.error(result.error);
110488
+ writeJsonError(context4.output, options, result.error, "INTERNAL_ERROR");
110300
110489
  return 1;
110301
110490
  }
110302
110491
  if (options.json) {
@@ -110627,32 +110816,32 @@ ${result.errors.map((m) => ` - ${m}`).join(`
110627
110816
  const silent = !json3 && options.silent === true;
110628
110817
  const quiet = !json3 && options.quiet === true;
110629
110818
  if (!json3 && options.quiet === true && options.verbose === true) {
110630
- context4.output.error("--quiet and --verbose are mutually exclusive");
110819
+ writeJsonError(context4.output, options, "--quiet and --verbose are mutually exclusive", "VALIDATION_FAILED");
110631
110820
  context4.setExitCode(2);
110632
110821
  return;
110633
110822
  }
110634
110823
  if (!json3 && options.silent === true && (options.quiet === true || options.verbose === true)) {
110635
- context4.output.error("--silent cannot be combined with --quiet or --verbose");
110824
+ writeJsonError(context4.output, options, "--silent cannot be combined with --quiet or --verbose", "VALIDATION_FAILED");
110636
110825
  context4.setExitCode(2);
110637
110826
  return;
110638
110827
  }
110639
110828
  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");
110829
+ writeJsonError(context4.output, options, "--steer is synchronous and in-process; it cannot be combined with --json or --async", "VALIDATION_FAILED");
110641
110830
  context4.setExitCode(2);
110642
110831
  return;
110643
110832
  }
110644
110833
  const requestedDetail = options.detail;
110645
110834
  if (requestedDetail !== undefined && !["minimal", "invocation", "full"].includes(requestedDetail)) {
110646
- context4.output.error("--detail must be one of: minimal, invocation, full");
110835
+ writeJsonError(context4.output, options, "--detail must be one of: minimal, invocation, full", "VALIDATION_FAILED");
110647
110836
  context4.setExitCode(2);
110648
110837
  return;
110649
110838
  }
110650
110839
  const detail = options.verbose === true ? "full" : requestedDetail ?? "invocation";
110651
110840
  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).
110841
+ writeJsonError(context4.output, options, `workflow run: refusing to start \u2014 already inside an active workflow run (${WORKFLOW_RUN_ACTIVE_ENV}=1).
110653
110842
  ` + `A pipeline that starts another pipeline forks a worktree and an agent run per level, without bound.
110654
110843
  ` + `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.");
110844
+ ` + "needed and let the operator run it from a clean shell.", "VALIDATION_FAILED");
110656
110845
  context4.setExitCode(1);
110657
110846
  return;
110658
110847
  }
@@ -110847,7 +111036,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
110847
111036
  if (options.answer !== undefined) {
110848
111037
  const v = String(options.answer).toLowerCase();
110849
111038
  if (v !== "yes" && v !== "no" && v !== "cancel") {
110850
- context4.output.error(`Invalid --answer value "${options.answer}" - must be yes, no, or cancel.`);
111039
+ writeJsonError(context4.output, options, `Invalid --answer value "${options.answer}" - must be yes, no, or cancel.`, "VALIDATION_FAILED");
110851
111040
  context4.setExitCode(2);
110852
111041
  return;
110853
111042
  }
@@ -110861,7 +111050,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
110861
111050
  if (targetId === undefined) {
110862
111051
  const latest = await svc.latestPausedRun();
110863
111052
  if (latest === null) {
110864
- context4.output.error("No paused workflow run to continue.");
111053
+ writeJsonError(context4.output, options, "No paused workflow run to continue.", "NOT_FOUND");
110865
111054
  context4.setExitCode(1);
110866
111055
  return;
110867
111056
  }
@@ -110874,7 +111063,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
110874
111063
  node: "continue"
110875
111064
  });
110876
111065
  if (answer.value !== "yes") {
110877
- context4.output.error(`Aborted - run ${latest.runId} not resumed.`);
111066
+ writeJsonError(context4.output, options, `Aborted - run ${latest.runId} not resumed.`, "GUARD_DENIED");
110878
111067
  context4.setExitCode(1);
110879
111068
  return;
110880
111069
  }
@@ -110900,7 +111089,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
110900
111089
  const force = options.force === true;
110901
111090
  const minutes = force ? 0 : Number.parseInt(options.olderThan ?? "30", 10);
110902
111091
  if (!Number.isFinite(minutes) || minutes < 0) {
110903
- context4.output.error(`Invalid --older-than value: ${options.olderThan}`);
111092
+ writeJsonError(context4.output, options, `Invalid --older-than value: ${options.olderThan}`, "VALIDATION_FAILED");
110904
111093
  context4.setExitCode(2);
110905
111094
  return;
110906
111095
  }
@@ -110945,7 +111134,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
110945
111134
  return;
110946
111135
  }
110947
111136
  if (result.status === "not_found") {
110948
- context4.output.error(`Run ${runId} not found.`);
111137
+ writeJsonError(context4.output, options, `Run ${runId} not found.`, "NOT_FOUND");
110949
111138
  context4.setExitCode(1);
110950
111139
  return;
110951
111140
  }
@@ -110967,14 +111156,14 @@ Monitor with: spur workflow trace ${runId2} --follow`);
110967
111156
  });
110968
111157
  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
111158
  if (options.format !== "mermaid" && options.format !== "todo") {
110970
- context4.output.error(`workflow show: unknown --format '${options.format}' \u2014 expected mermaid or todo`);
111159
+ writeJsonError(context4.output, options, `workflow show: unknown --format '${options.format}' \u2014 expected mermaid or todo`, "VALIDATION_FAILED");
110971
111160
  context4.setExitCode(1);
110972
111161
  return;
110973
111162
  }
110974
111163
  const resolved = resolveWorkflowFile(context4.cwd, file2);
110975
111164
  if (resolved.path === null) {
110976
111165
  const [probedProject, probedBundled] = resolved.probed;
110977
- context4.output.error(`workflow show: file not found: ${probedProject}${probedBundled !== null ? ` (bundled: ${probedBundled})` : ""}`);
111166
+ writeJsonError(context4.output, options, `workflow show: file not found: ${probedProject}${probedBundled !== null ? ` (bundled: ${probedBundled})` : ""}`, "NOT_FOUND");
110978
111167
  context4.setExitCode(1);
110979
111168
  return;
110980
111169
  }
@@ -110983,7 +111172,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
110983
111172
  try {
110984
111173
  def = await loadWorkflowDef(filePath, { validateSchema: true });
110985
111174
  } catch (err) {
110986
- context4.output.error(`workflow show: cannot read or parse ${file2} \u2014 ${err instanceof Error ? err.message : String(err)}`);
111175
+ writeJsonError(context4.output, options, `workflow show: cannot read or parse ${file2} \u2014 ${err instanceof Error ? err.message : String(err)}`, "VALIDATION_FAILED");
110987
111176
  context4.setExitCode(1);
110988
111177
  return;
110989
111178
  }
@@ -111011,38 +111200,38 @@ Monitor with: spur workflow trace ${runId2} --follow`);
111011
111200
  const svc = makeSvc();
111012
111201
  const last = parseInt(options.last, 10);
111013
111202
  if (Number.isNaN(last) || last < 1) {
111014
- context4.output.error("--last must be a positive integer");
111203
+ writeJsonError(context4.output, options, "--last must be a positive integer", "VALIDATION_FAILED");
111015
111204
  context4.setExitCode(1);
111016
111205
  return;
111017
111206
  }
111018
111207
  const pollMs = parseInt(options.poll, 10);
111019
111208
  if (Number.isNaN(pollMs) || pollMs < 50) {
111020
- context4.output.error("--poll must be an integer of at least 50ms");
111209
+ writeJsonError(context4.output, options, "--poll must be an integer of at least 50ms", "VALIDATION_FAILED");
111021
111210
  context4.setExitCode(1);
111022
111211
  return;
111023
111212
  }
111024
111213
  if (options.follow === true && runId === undefined) {
111025
- context4.output.error("--follow requires a run-id");
111214
+ writeJsonError(context4.output, options, "--follow requires a run-id", "VALIDATION_FAILED");
111026
111215
  context4.setExitCode(1);
111027
111216
  return;
111028
111217
  }
111029
111218
  if (options.follow === true && options.json === true) {
111030
- context4.output.error("--follow is a human streaming mode and cannot be combined with --json");
111219
+ writeJsonError(context4.output, options, "--follow is a human streaming mode and cannot be combined with --json", "VALIDATION_FAILED");
111031
111220
  context4.setExitCode(1);
111032
111221
  return;
111033
111222
  }
111034
111223
  if (options.output === true && options.follow !== true) {
111035
- context4.output.error("--output requires --follow");
111224
+ writeJsonError(context4.output, options, "--output requires --follow", "VALIDATION_FAILED");
111036
111225
  context4.setExitCode(1);
111037
111226
  return;
111038
111227
  }
111039
111228
  if (options.output === true && options.json === true) {
111040
- context4.output.error("--output is a human streaming mode and cannot be combined with --json");
111229
+ writeJsonError(context4.output, options, "--output is a human streaming mode and cannot be combined with --json", "VALIDATION_FAILED");
111041
111230
  context4.setExitCode(1);
111042
111231
  return;
111043
111232
  }
111044
111233
  if (options.status !== undefined && !["done", "failed", "running"].includes(options.status)) {
111045
- context4.output.error("--status must be one of: done, failed, running");
111234
+ writeJsonError(context4.output, options, "--status must be one of: done, failed, running", "VALIDATION_FAILED");
111046
111235
  context4.setExitCode(1);
111047
111236
  return;
111048
111237
  }