@gobing-ai/spur 0.3.13 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/config/workflows/task-lifecycle.yaml +10 -0
  2. package/config/workflows/wayfinder-resolution.yaml +199 -0
  3. package/package.json +1 -1
  4. package/spur.js +353 -98
  5. package/web/_astro/BoardApp.BdyUxAQU.js +1 -0
  6. package/web/_astro/{BoardApp.CqBOrPzf.js → BoardApp.oOZMY52P.js} +65 -65
  7. package/web/_astro/{TaskDetail.BmxycoeS.js → TaskDetail.DZVfe6fy.js} +1 -1
  8. package/web/_astro/{arc.-ZkWbWcN.js → arc.CmzWyu5T.js} +1 -1
  9. package/web/_astro/{architectureDiagram-3BPJPVTR.fJ8ETc6n.js → architectureDiagram-3BPJPVTR.DEvXeSWP.js} +1 -1
  10. package/web/_astro/{blockDiagram-GPEHLZMM.CdhoveXr.js → blockDiagram-GPEHLZMM.DULgqAsN.js} +1 -1
  11. package/web/_astro/{c4Diagram-AAUBKEIU.CWv7Jy0p.js → c4Diagram-AAUBKEIU.CtwqGUxK.js} +1 -1
  12. package/web/_astro/channel.C6f9ncOV.js +1 -0
  13. package/web/_astro/{chunk-2J33WTMH.CvYWDwal.js → chunk-2J33WTMH.lyKyWJjV.js} +1 -1
  14. package/web/_astro/{chunk-4BX2VUAB.BVp_y1oc.js → chunk-4BX2VUAB.COAavIDd.js} +1 -1
  15. package/web/_astro/{chunk-55IACEB6.CQCviGQk.js → chunk-55IACEB6.EoRbsXM1.js} +1 -1
  16. package/web/_astro/{chunk-727SXJPM.B3E6Bisv.js → chunk-727SXJPM.DhjfPs_B.js} +1 -1
  17. package/web/_astro/{chunk-AQP2D5EJ.BvWfYj9E.js → chunk-AQP2D5EJ.BgSRWTUm.js} +1 -1
  18. package/web/_astro/{chunk-FMBD7UC4.DcLP2Ypw.js → chunk-FMBD7UC4.Dj0MuLrK.js} +1 -1
  19. package/web/_astro/{chunk-ND2GUHAM.DvXZJEqI.js → chunk-ND2GUHAM.B2_roVTb.js} +1 -1
  20. package/web/_astro/{chunk-QZHKN3VN.D4rK2HTI.js → chunk-QZHKN3VN.Cr3PSROo.js} +1 -1
  21. package/web/_astro/{classDiagram-4FO5ZUOK.DSCYtdVf.js → classDiagram-4FO5ZUOK.CXEuLRp6.js} +1 -1
  22. package/web/_astro/{classDiagram-v2-Q7XG4LA2.DSCYtdVf.js → classDiagram-v2-Q7XG4LA2.CXEuLRp6.js} +1 -1
  23. package/web/_astro/{cose-bilkent-S5V4N54A.Bf7HMV4Q.js → cose-bilkent-S5V4N54A._ulIkUyI.js} +1 -1
  24. package/web/_astro/{dagre-BM42HDAG.DsBoJTBN.js → dagre-BM42HDAG.D5SRsv4S.js} +1 -1
  25. package/web/_astro/{diagram-2AECGRRQ.BMVxdKV3.js → diagram-2AECGRRQ.rMwI7OR9.js} +1 -1
  26. package/web/_astro/{diagram-5GNKFQAL.BllW5zst.js → diagram-5GNKFQAL.DWZ2ui2V.js} +1 -1
  27. package/web/_astro/{diagram-KO2AKTUF.CNHoSD6q.js → diagram-KO2AKTUF.DuqJ_D0B.js} +1 -1
  28. package/web/_astro/{diagram-LMA3HP47.DZT6otFZ.js → diagram-LMA3HP47.BDxKBl9B.js} +1 -1
  29. package/web/_astro/{diagram-OG6HWLK6.EwFWHBRO.js → diagram-OG6HWLK6.D6IMfAPf.js} +1 -1
  30. package/web/_astro/{erDiagram-TEJ5UH35.CaXPcR5W.js → erDiagram-TEJ5UH35.DSsh9EFG.js} +1 -1
  31. package/web/_astro/{flowDiagram-I6XJVG4X.FEcfML44.js → flowDiagram-I6XJVG4X.DwjNzc4e.js} +1 -1
  32. package/web/_astro/{ganttDiagram-6RSMTGT7.Bs7t3AQM.js → ganttDiagram-6RSMTGT7.DbYs_nXA.js} +1 -1
  33. package/web/_astro/{gitGraphDiagram-PVQCEYII.DmxLV9JC.js → gitGraphDiagram-PVQCEYII.DX5-xqVb.js} +1 -1
  34. package/web/_astro/{infoDiagram-5YYISTIA.E7_NrFUE.js → infoDiagram-5YYISTIA.jP6EGbwr.js} +1 -1
  35. package/web/_astro/{ishikawaDiagram-YF4QCWOH.Bs5Q4r-U.js → ishikawaDiagram-YF4QCWOH.Cn89YzBJ.js} +1 -1
  36. package/web/_astro/{journeyDiagram-JHISSGLW.D3U7T1y1.js → journeyDiagram-JHISSGLW.CNT2WvMT.js} +1 -1
  37. package/web/_astro/{kanban-definition-UN3LZRKU.DfdWsO2E.js → kanban-definition-UN3LZRKU.CLEZjEjD.js} +1 -1
  38. package/web/_astro/{linear.CAqUTUf3.js → linear.Cn4ewR2z.js} +1 -1
  39. package/web/_astro/{mermaid.core.ZlPUJ21K.js → mermaid.core.BEACwKCZ.js} +4 -4
  40. package/web/_astro/{mindmap-definition-RKZ34NQL.ClTJkcTG.js → mindmap-definition-RKZ34NQL.BE9cnWaH.js} +1 -1
  41. package/web/_astro/{pieDiagram-4H26LBE5.B9kJfBw_.js → pieDiagram-4H26LBE5.CLcCK_JM.js} +1 -1
  42. package/web/_astro/{quadrantDiagram-W4KKPZXB.CtTJH5Fo.js → quadrantDiagram-W4KKPZXB.qw8LmkgG.js} +1 -1
  43. package/web/_astro/{requirementDiagram-4Y6WPE33.WVLq9rr2.js → requirementDiagram-4Y6WPE33.D02534qk.js} +1 -1
  44. package/web/_astro/{sankeyDiagram-5OEKKPKP.L_XUdyqe.js → sankeyDiagram-5OEKKPKP.I6nOztAU.js} +1 -1
  45. package/web/_astro/{sequenceDiagram-3UESZ5HK.DCdtsEdj.js → sequenceDiagram-3UESZ5HK.CXTuiAV8.js} +1 -1
  46. package/web/_astro/{stateDiagram-AJRCARHV.-Koi9kLh.js → stateDiagram-AJRCARHV.BV-mNJRM.js} +1 -1
  47. package/web/_astro/{stateDiagram-v2-BHNVJYJU.DHXu_l8w.js → stateDiagram-v2-BHNVJYJU.kF2_t5fy.js} +1 -1
  48. package/web/_astro/{timeline-definition-PNZ67QCA.Czotaq0j.js → timeline-definition-PNZ67QCA.BiIzvhe5.js} +1 -1
  49. package/web/_astro/{vennDiagram-CIIHVFJN.BfEI3zhc.js → vennDiagram-CIIHVFJN.9cR-p_zq.js} +1 -1
  50. package/web/_astro/{wardley-L42UT6IY.C33mahwT.js → wardley-L42UT6IY.CwwpzGfz.js} +1 -1
  51. package/web/_astro/{wardleyDiagram-YWT4CUSO.D8IzxB1w.js → wardleyDiagram-YWT4CUSO.Q3hswh_f.js} +1 -1
  52. package/web/_astro/{xychartDiagram-2RQKCTM6.D-cfi9uT.js → xychartDiagram-2RQKCTM6.Co-1OW2E.js} +1 -1
  53. package/web/index.html +1 -1
  54. package/web/_astro/BoardApp.XG2FoRlA.js +0 -1
  55. package/web/_astro/channel.BrBVc6tg.js +0 -1
package/spur.js CHANGED
@@ -48,10 +48,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
48
  var __require = import.meta.require;
49
49
 
50
50
  // ../../node_modules/.bun/@gobing-ai+ts-utils@0.4.10/node_modules/@gobing-ai/ts-utils/dist/errors.js
51
- function isAppError(error) {
52
- return error instanceof AppError;
53
- }
54
- var ErrorCode, AppError, NotFoundError;
51
+ var ErrorCode;
55
52
  var init_errors = __esm(() => {
56
53
  ErrorCode = {
57
54
  NotFound: "NOT_FOUND",
@@ -59,20 +56,6 @@ var init_errors = __esm(() => {
59
56
  Conflict: "CONFLICT",
60
57
  Internal: "INTERNAL"
61
58
  };
62
- AppError = class AppError extends Error {
63
- code;
64
- constructor(code, message) {
65
- super(message);
66
- this.name = "AppError";
67
- this.code = code;
68
- }
69
- };
70
- NotFoundError = class NotFoundError extends AppError {
71
- constructor(message) {
72
- super(ErrorCode.NotFound, message);
73
- this.name = "NotFoundError";
74
- }
75
- };
76
59
  });
77
60
 
78
61
  // ../../node_modules/.bun/@gobing-ai+ts-utils@0.4.10/node_modules/@gobing-ai/ts-utils/dist/api-response.js
@@ -50904,7 +50887,7 @@ class AgentService {
50904
50887
  return results.some((result) => !result.usable && result.tier === 1) ? 1 : 0;
50905
50888
  }
50906
50889
  async run(prompt, flags, deps) {
50907
- const outcome = await this.executeRun(prompt, flags, deps, false);
50890
+ const outcome = await this.executeRun(prompt, flags, deps, { silent: false });
50908
50891
  if (!outcome.ok) {
50909
50892
  this.ctx.output.error(outcome.message);
50910
50893
  return outcome.exitCode;
@@ -50922,7 +50905,7 @@ class AgentService {
50922
50905
  return 3;
50923
50906
  }
50924
50907
  async runCapture(prompt, flags, deps) {
50925
- const outcome = await this.executeRun(prompt, flags, deps, true);
50908
+ const outcome = await this.executeRun(prompt, flags, deps, { silent: true });
50926
50909
  if (!outcome.ok) {
50927
50910
  return { exitCode: outcome.exitCode, answer: "" };
50928
50911
  }
@@ -50936,7 +50919,25 @@ class AgentService {
50936
50919
  stderr: result.stderr
50937
50920
  };
50938
50921
  }
50939
- async executeRun(prompt, flags, deps, silent) {
50922
+ async runTraced(prompt, flags, deps) {
50923
+ const outcome = await this.executeRun(prompt, flags, deps, { silent: true, nonInteractive: true });
50924
+ if (!outcome.ok) {
50925
+ return { exitCode: outcome.exitCode, stdout: "", message: outcome.message };
50926
+ }
50927
+ const result = outcome.result;
50928
+ const exitCode = result.exitCode === 0 ? 0 : 3;
50929
+ return {
50930
+ exitCode,
50931
+ invocation: outcome.invocation,
50932
+ stdout: result.stdout,
50933
+ stderr: result.stderr,
50934
+ durationMs: result.durationMs,
50935
+ ...result.signal !== undefined ? { signal: result.signal } : {}
50936
+ };
50937
+ }
50938
+ async executeRun(prompt, flags, deps, options) {
50939
+ const silent = options.silent;
50940
+ const nonInteractive = options.nonInteractive === true;
50940
50941
  const mode = stringFlag(flags, "mode", "text");
50941
50942
  if (mode !== "text" && mode !== "json") {
50942
50943
  return { ok: false, exitCode: 2, message: `Invalid mode: ${mode} (must be text or json)` };
@@ -50960,7 +50961,9 @@ class AgentService {
50960
50961
  return { ok: false, exitCode: 2, message: "Prompt is required" };
50961
50962
  }
50962
50963
  const jsonOutput = silent || booleanFlag(flags, "json");
50963
- const outputPolicy = jsonOutput ? { mode: "buffered" } : { mode: "stream", isTTY: isatty3(1) };
50964
+ const forceBuffered = silent || nonInteractive;
50965
+ const outputPolicy = forceBuffered ? { mode: "buffered" } : jsonOutput ? { mode: "buffered" } : { mode: "stream", isTTY: isatty3(1) };
50966
+ const outputMode = outputPolicy.mode;
50964
50967
  const runner = deps?.runner ?? new AiRunner({
50965
50968
  processExecutor: new NodeProcessExecutor3({
50966
50969
  output: outputPolicy,
@@ -50979,7 +50982,8 @@ class AgentService {
50979
50982
  if (!jsonOutput && TIER2_AGENTS.has(agent)) {
50980
50983
  this.ctx.output.error(`Warning: ${agent} is a Tier-2 agent (TUI/gateway only)`);
50981
50984
  }
50982
- const input = prompt !== undefined && isClaudeStyleSlashCommand(prompt) ? translateSlashCommand(agent, prompt) : prompt;
50985
+ const translated = prompt !== undefined && isClaudeStyleSlashCommand(prompt) ? translateSlashCommand(agent, prompt) : undefined;
50986
+ const input = translated ?? prompt;
50983
50987
  const purpose = stringFlag(flags, "purpose", "") || undefined;
50984
50988
  const tags = parseTagsFlag(flags);
50985
50989
  const systemPrompt = stringFlag(flags, "system-prompt", "") || undefined;
@@ -50996,9 +51000,9 @@ class AgentService {
50996
51000
  ...systemPrompt !== undefined ? { systemPrompt } : {},
50997
51001
  ...taskId !== undefined ? { taskId } : {}
50998
51002
  };
51003
+ let shimCommand;
50999
51004
  try {
51000
- const shim = getAgentShim(agent);
51001
- const shimCommand = shim.getPromptCommand(promptOptions);
51005
+ shimCommand = typeof runner.buildPromptCommand === "function" ? runner.buildPromptCommand(agent, promptOptions, { cwd: cwd || undefined }) : getAgentShim(agent).getPromptCommand(promptOptions);
51002
51006
  if (!jsonOutput) {
51003
51007
  const version3 = (await detector.detectOne(agent)).version;
51004
51008
  this.ctx.output.error(`\u2699\uFE0F ${agent}${version3 !== null ? ` v${version3}` : ""}
@@ -51007,6 +51011,21 @@ class AgentService {
51007
51011
  } catch (error51) {
51008
51012
  return { ok: false, exitCode: 2, message: error51 instanceof Error ? error51.message : String(error51) };
51009
51013
  }
51014
+ const traceInput = input === undefined ? undefined : traceSafePrompt(input);
51015
+ const invocation = {
51016
+ agent,
51017
+ source: resolved.source,
51018
+ command: shimCommand.command,
51019
+ argv: sanitizeInvocationArgv(shimCommand.args, input, traceInput),
51020
+ ...cwd !== "" ? { cwd } : {},
51021
+ mode,
51022
+ outputMode,
51023
+ ...timeoutMs !== undefined ? { timeoutMs } : {},
51024
+ continue: continueFlag,
51025
+ stdinInteractive: false,
51026
+ ...model !== undefined ? { model } : {},
51027
+ ...translated !== undefined && prompt !== undefined ? { translatedFrom: traceSafePrompt(prompt) } : {}
51028
+ };
51010
51029
  let result;
51011
51030
  const controller = new AbortController;
51012
51031
  const onTerminate = () => controller.abort();
@@ -51024,7 +51043,7 @@ class AgentService {
51024
51043
  process.off("SIGTERM", onTerminate);
51025
51044
  process.off("SIGINT", onTerminate);
51026
51045
  }
51027
- return { ok: true, result };
51046
+ return { ok: true, result, invocation };
51028
51047
  }
51029
51048
  async resolveAgent(prompt, flags, doctorRunner) {
51030
51049
  const raw = stringFlag(flags, "agent", "auto");
@@ -51148,6 +51167,32 @@ class AgentService {
51148
51167
  function toJson(value) {
51149
51168
  return JSON.stringify(value, null, 2);
51150
51169
  }
51170
+ function traceSafePrompt(prompt) {
51171
+ const trimmed = prompt.trim();
51172
+ if (!TRACE_SAFE_SLASH_COMMAND.test(trimmed)) {
51173
+ return `[redacted prompt: ${prompt.length} chars]`;
51174
+ }
51175
+ const [command = "", ...tokens] = trimmed.split(/\s+/);
51176
+ const safeTokens = tokens.map((token) => TRACE_SAFE_SLASH_TOKEN.test(token) ? token : "[redacted]");
51177
+ return [command, ...safeTokens].join(" ");
51178
+ }
51179
+ function sanitizeInvocationArgv(argv, rawInput, traceInput) {
51180
+ let redactNext = false;
51181
+ return argv.map((arg) => {
51182
+ if (redactNext) {
51183
+ redactNext = false;
51184
+ return "[redacted]";
51185
+ }
51186
+ if (SENSITIVE_FLAG.test(arg)) {
51187
+ redactNext = true;
51188
+ return arg;
51189
+ }
51190
+ if (rawInput !== undefined && traceInput !== undefined && arg.includes(rawInput)) {
51191
+ return traceInput;
51192
+ }
51193
+ return arg.replace(SENSITIVE_INLINE_FLAG, "$1[redacted]").replace(/\bBearer\s+\S+/gi, "Bearer [redacted]").replace(/\b(?:sk|ghp|github_pat)-?[A-Za-z0-9_]{12,}\b/g, "[redacted]");
51194
+ });
51195
+ }
51151
51196
  function extractPhase(prompt) {
51152
51197
  if (prompt === undefined)
51153
51198
  return;
@@ -51248,9 +51293,14 @@ function parseTagsFlag(flags) {
51248
51293
  const tags = raw.split(",").map((tag) => tag.trim()).filter(Boolean);
51249
51294
  return tags.length > 0 ? tags : undefined;
51250
51295
  }
51296
+ var TRACE_SAFE_SLASH_COMMAND, TRACE_SAFE_SLASH_TOKEN, SENSITIVE_FLAG, SENSITIVE_INLINE_FLAG;
51251
51297
  var init_agent_service = __esm(() => {
51252
51298
  init_dist7();
51253
51299
  init_dist5();
51300
+ TRACE_SAFE_SLASH_COMMAND = /^(?:\/skill:(?:sp|rd3)-|\/(?:sp|rd3)[:-]|\$(?:sp|rd3)-)[A-Za-z0-9._-]+(?:\s|$)/;
51301
+ TRACE_SAFE_SLASH_TOKEN = /^(?:\d{4}|--(?:auto|next|force|bdd)|--(?:mode|fix|focus)|implement|test|review|verify|all|none|blockers-first|quick|requirements|background|constraints|acceptance)$/;
51302
+ SENSITIVE_FLAG = /^--?(?:api[-_]?key|authorization|credential|password|secret|token)$/i;
51303
+ SENSITIVE_INLINE_FLAG = /^(--?(?:api[-_]?key|authorization|credential|password|secret|token)=).+$/i;
51254
51304
  });
51255
51305
 
51256
51306
  // ../../packages/domain/src/analytics/models.ts
@@ -58399,6 +58449,8 @@ var init_schema4 = __esm(() => {
58399
58449
  priority: exports_external.enum(PRIORITIES).optional(),
58400
58450
  tags: exports_external.array(exports_external.string()).optional(),
58401
58451
  dependencies: exports_external.array(exports_external.string()).optional(),
58452
+ done_forced: exports_external.preprocess((v) => typeof v === "string" ? v === "true" : v, exports_external.boolean().optional()),
58453
+ done_reason: exports_external.string().optional(),
58402
58454
  created_at: isoDateString,
58403
58455
  updated_at: isoDateString
58404
58456
  });
@@ -59046,6 +59098,110 @@ var init_corpus_migrator = __esm(() => {
59046
59098
  NULL_PRESERVING_KEYS = new Set(["parent_wbs"]);
59047
59099
  });
59048
59100
 
59101
+ // ../../packages/app/src/services/done-transition-guard.ts
59102
+ async function readVerdictArtifact(fs3, runDir, wbs) {
59103
+ const path9 = `${runDir}/${wbs}-verdict.json`;
59104
+ let exists2;
59105
+ try {
59106
+ exists2 = await fs3.exists(path9);
59107
+ } catch {
59108
+ exists2 = true;
59109
+ }
59110
+ if (!exists2) {
59111
+ return { artifact: undefined, path: path9 };
59112
+ }
59113
+ let raw;
59114
+ try {
59115
+ raw = await fs3.readFile(path9);
59116
+ } catch (err) {
59117
+ return { artifact: undefined, path: path9, readError: `unreadable artifact: ${err.message}` };
59118
+ }
59119
+ let parsed;
59120
+ try {
59121
+ parsed = JSON.parse(raw);
59122
+ } catch (err) {
59123
+ return { artifact: undefined, path: path9, readError: `malformed JSON: ${err.message}` };
59124
+ }
59125
+ if (typeof parsed !== "object" || parsed === null || !("verdict" in parsed)) {
59126
+ return { artifact: undefined, path: path9, readError: "missing required `verdict` field" };
59127
+ }
59128
+ return { artifact: parsed, path: path9 };
59129
+ }
59130
+ function computeAggregate(artifact) {
59131
+ const reqs = artifact.requirements ?? [];
59132
+ const acs = artifact.acceptanceCriteria ?? [];
59133
+ if (reqs.length === 0 && acs.length === 0) {
59134
+ return artifact.verdict;
59135
+ }
59136
+ if (reqs.some((r) => r.status === "UNMET") || acs.some((a2) => a2.status === "UNMET")) {
59137
+ return "FAIL";
59138
+ }
59139
+ if (reqs.some((r) => r.status === "PARTIAL") || acs.some((a2) => a2.status === "PARTIAL")) {
59140
+ return "PARTIAL";
59141
+ }
59142
+ return "PASS";
59143
+ }
59144
+ function formatDenialMessage(args) {
59145
+ const { wbs, taskFilePath, verdictPath, verdict, inconsistency, artifact } = args;
59146
+ const lines = [
59147
+ `Cannot transition task ${wbs} to done: verify verdict is ${verdict}.`,
59148
+ ` task: ${taskFilePath}`,
59149
+ ` verdict: ${verdictPath}`
59150
+ ];
59151
+ if (inconsistency) {
59152
+ lines.push(` warning: artifact is self-inconsistent \u2014 stored aggregate ${inconsistency.stored} contradicts rows (computed ${inconsistency.computed}). Treated as non-PASS.`);
59153
+ }
59154
+ const reqCount = artifact?.requirements?.length ?? 0;
59155
+ const acCount = artifact?.acceptanceCriteria?.length ?? 0;
59156
+ const rowCount = reqCount + acCount;
59157
+ if (verdict === "UNKNOWN" && artifact !== undefined && (artifact.source === "spur-task-verdict" || rowCount === 0)) {
59158
+ const source = artifact.source ?? "unknown";
59159
+ lines.push(` source: ${source} artifact contains ${rowCount} structured row${rowCount === 1 ? "" : "s"} ` + `(${reqCount} requirement${reqCount === 1 ? "" : "s"}, ${acCount} AC${acCount === 1 ? "" : "s"}). ` + `UNKNOWN means the verify answer file carried no parseable markdown tables ` + `(\`| Req | Status | Evidence |\` and \`| AC | Status | Evidence Type | Evidence |\`).`);
59160
+ lines.push(` see sp:spur-cli \`tasks/verbs.md\` \xA7Answer-file shape for the expected format. ` + `Re-run \`/sp:dev-verify ${wbs}\` so the skill authors the tables; do not loosen the parser.`);
59161
+ }
59162
+ lines.push(" remediation: re-run `/sp:dev-verify " + wbs + "` until PASS, or override with `spur task update " + wbs + ' done --force-done --reason "<why>"`.');
59163
+ return lines.join(`
59164
+ `);
59165
+ }
59166
+ function formatNoopMessage(wbs, status) {
59167
+ return `${wbs}: already ${status} \u2014 no transition`;
59168
+ }
59169
+ function evaluateDoneTransition(input) {
59170
+ const { wbs, taskFilePath, currentStatus, targetStatus, forced, reason, artifact } = input;
59171
+ if (targetStatus === currentStatus) {
59172
+ return { kind: "noop", fromStatus: currentStatus, message: formatNoopMessage(wbs, currentStatus) };
59173
+ }
59174
+ if (artifact === undefined) {
59175
+ return { kind: "allow", reason: "no-artifact" };
59176
+ }
59177
+ if (forced) {
59178
+ return { kind: "allow", reason: "forced" };
59179
+ }
59180
+ const computed = computeAggregate(artifact);
59181
+ const effective = harshnessMax(artifact.verdict, computed);
59182
+ if (effective === "PASS") {
59183
+ return { kind: "allow", reason: "pass" };
59184
+ }
59185
+ const verdictPath = `.spur/run/${wbs}-verdict.json`;
59186
+ const inconsistency = artifact.verdict !== computed ? { stored: artifact.verdict, computed } : undefined;
59187
+ return {
59188
+ kind: "deny",
59189
+ verdict: effective,
59190
+ message: formatDenialMessage({
59191
+ wbs,
59192
+ taskFilePath,
59193
+ verdictPath,
59194
+ verdict: effective,
59195
+ inconsistency,
59196
+ artifact
59197
+ })
59198
+ };
59199
+ }
59200
+ function harshnessMax(a2, b) {
59201
+ const rank = { PASS: 0, UNKNOWN: 1, PARTIAL: 2, FAIL: 3 };
59202
+ return rank[a2] >= rank[b] ? a2 : b;
59203
+ }
59204
+
59049
59205
  // ../../packages/app/src/services/event-names.ts
59050
59206
  function event(name, source, renderer, payloadPolicy = "metadata-only", tier = "default") {
59051
59207
  return {
@@ -61467,6 +61623,10 @@ function isPlaceholderBody(body) {
61467
61623
  const stripped = body.replace(/<!--[\s\S]*?-->/g, "").replace(/^\s*>\s*TBD\s*$/gim, "").trim();
61468
61624
  return stripped.length === 0;
61469
61625
  }
61626
+ function isPlaceholderCell(cell) {
61627
+ const c3 = cell.trim();
61628
+ return c3 === "" || /^[\u2014\u2013-]+$/.test(c3) || /^n\/?a$/i.test(c3);
61629
+ }
61470
61630
  function hasPopulatedPriorityTable(body) {
61471
61631
  for (const line of body.split(`
61472
61632
  `)) {
@@ -61476,7 +61636,7 @@ function hasPopulatedPriorityTable(body) {
61476
61636
  const severityIdx = cells.findIndex((c3) => /^\s*P[1-4]\s*$/.test(c3));
61477
61637
  if (severityIdx === -1)
61478
61638
  continue;
61479
- const hasContent = cells.some((c3, i2) => i2 !== severityIdx && c3.trim().length > 0);
61639
+ const hasContent = cells.some((c3, i2) => i2 !== severityIdx && !isPlaceholderCell(c3));
61480
61640
  if (hasContent)
61481
61641
  return true;
61482
61642
  }
@@ -61498,7 +61658,7 @@ function isReviewScaffold(body) {
61498
61658
  continue;
61499
61659
  const cells = line.split("|").map((c3) => c3.trim());
61500
61660
  const isSeparator = cells.every((c3) => c3 === "" || /^:?-+:?$/.test(c3));
61501
- const isEmptyPRow = cells.every((c3) => c3 === "" || /^P[1-4]$/.test(c3)) && cells.some((c3) => /^P[1-4]$/.test(c3));
61661
+ const isEmptyPRow = cells.every((c3) => isPlaceholderCell(c3) || /^P[1-4]$/.test(c3)) && cells.some((c3) => /^P[1-4]$/.test(c3));
61502
61662
  const isHeader = cells.some((c3) => /severity|file|finding|recommendation/i.test(c3));
61503
61663
  if (isEmptyPRow)
61504
61664
  sawEmptyPRow = true;
@@ -61663,6 +61823,18 @@ var init_task_check = __esm(() => {
61663
61823
  }
61664
61824
  }
61665
61825
  async runL4(doc2, fm, findings, featuresDir, tasksDir) {
61826
+ const template = typeof fm.template === "string" ? fm.template : DEFAULT_TASK_VARIANT;
61827
+ if (template === DEFAULT_TASK_VARIANT) {
61828
+ const designBody = doc2.getSection("Design");
61829
+ if (designBody !== null && isPlaceholderBody(designBody)) {
61830
+ findings.push({
61831
+ layer: "L4",
61832
+ severity: "warning",
61833
+ section: "Design",
61834
+ message: "Design section is present but empty (placeholder-only) \u2014 populate it so the task retains an explicit implementation approach"
61835
+ });
61836
+ }
61837
+ }
61666
61838
  const featureId2 = fm.feature_id ?? fm["feature-id"];
61667
61839
  const parentWbs = fm.parent_wbs ?? fm["parent-wbs"];
61668
61840
  const deps = fm.dependencies;
@@ -62360,9 +62532,15 @@ class TaskService {
62360
62532
  return this.writeService.transition(ref, toStatus, actor ?? this.ctx.actor ?? "system");
62361
62533
  }
62362
62534
  async updateField(wbs, key, value) {
62363
- const allowed = new Set(["feature_id", "parent_wbs", "priority"]);
62364
- if (!allowed.has(key)) {
62365
- throw new Error(`Field "${key}" is not settable via update; allowed: ${[...allowed].join(", ")}.`);
62535
+ const allowed = {
62536
+ feature_id: true,
62537
+ parent_wbs: true,
62538
+ priority: true,
62539
+ done_forced: true,
62540
+ done_reason: true
62541
+ };
62542
+ if (!(key in allowed)) {
62543
+ throw new Error(`Field "${key}" is not settable via update; allowed: ${Object.keys(allowed).join(", ")}.`);
62366
62544
  }
62367
62545
  const filePath = await this.resolveTaskFile(wbs);
62368
62546
  const ref = { kind: "task", id: wbs, filePath, folder: this.ctx.tasksDir };
@@ -63710,55 +63888,46 @@ class AgentRunActionRunner {
63710
63888
  const expectFile = asOptionalString(options.expectFile);
63711
63889
  const capture = asOptionalBoolean(options.capture) || answerFile !== undefined;
63712
63890
  const agentLabel = agent ?? "<default>";
63713
- if (capture) {
63714
- const captured = await this.agentService.runCapture(input, flags);
63715
- const { exitCode: exitCode2, answer } = captured;
63716
- const ok2 = exitCode2 === 0;
63717
- if (answerFile !== undefined) {
63718
- const target = isAbsolute2(answerFile) ? answerFile : join12(cwd, answerFile);
63719
- await mkdir(dirname10(target), { recursive: true });
63720
- await writeFile(target, answer, "utf8");
63721
- }
63722
- if (ok2 && expectFile !== undefined) {
63723
- const target = isAbsolute2(expectFile) ? expectFile : join12(cwd, expectFile);
63724
- if (!existsSync7(target)) {
63725
- return {
63726
- ok: false,
63727
- data: { exitCode: exitCode2, agent: agentLabel, answer },
63728
- error: `agent.run (${agentLabel}) exited 0 but expected file is absent: ${expectFile}`
63729
- };
63730
- }
63731
- }
63732
- if (!ok2) {
63733
- await writePartialWorkArtifact(context4, agentLabel, model, captured, cwd);
63734
- }
63735
- return {
63736
- ok: ok2,
63737
- data: { exitCode: exitCode2, agent: agentLabel, answer },
63738
- error: ok2 ? undefined : `agent.run (${agentLabel}) exited with code ${exitCode2}`,
63739
- setVars: ok2 ? { __agentSession: "open" } : undefined
63740
- };
63741
- }
63742
- const exitCode = await this.agentService.run(input, flags);
63891
+ const traced = await this.agentService.runTraced(input, flags);
63892
+ const { exitCode, stdout: answer } = traced;
63743
63893
  const ok = exitCode === 0;
63894
+ const invocation = traced.invocation;
63895
+ if (capture && answerFile !== undefined) {
63896
+ const target = isAbsolute2(answerFile) ? answerFile : join12(cwd, answerFile);
63897
+ await mkdir(dirname10(target), { recursive: true });
63898
+ await writeFile(target, answer, "utf8");
63899
+ }
63744
63900
  if (ok && expectFile !== undefined) {
63745
63901
  const target = isAbsolute2(expectFile) ? expectFile : join12(cwd, expectFile);
63746
63902
  if (!existsSync7(target)) {
63747
63903
  return {
63748
63904
  ok: false,
63749
- data: { exitCode, agent: agentLabel },
63905
+ data: buildResultData(exitCode, agentLabel, capture, answer, invocation),
63750
63906
  error: `agent.run (${agentLabel}) exited 0 but expected file is absent: ${expectFile}`
63751
63907
  };
63752
63908
  }
63753
63909
  }
63910
+ if (!ok) {
63911
+ await writePartialWorkArtifact(context4, agentLabel, model, traced, cwd);
63912
+ }
63913
+ const stepLabel = context4.stateOrNodeId;
63914
+ const error51 = ok ? undefined : traced.signal !== undefined ? timeoutMs !== undefined ? `agent.run '${stepLabel}' (${agentLabel}) terminated by signal ${traced.signal} (configured timeout: ${timeoutMs}ms; timeout or cancellation); see partial-work artifact` : `agent.run '${stepLabel}' (${agentLabel}) was cancelled by signal ${traced.signal}; see partial-work artifact` : traced.message !== undefined ? `agent.run '${stepLabel}' (${agentLabel}) dispatch failed: ${traced.message}` : `agent.run '${stepLabel}' (${agentLabel}) exited with code ${exitCode}`;
63754
63915
  return {
63755
63916
  ok,
63756
- data: { exitCode, agent: agentLabel },
63757
- error: ok ? undefined : `agent.run (${agentLabel}) exited with code ${exitCode}`,
63917
+ data: buildResultData(exitCode, agentLabel, capture, answer, invocation),
63918
+ error: error51,
63758
63919
  setVars: ok ? { __agentSession: "open" } : undefined
63759
63920
  };
63760
63921
  }
63761
63922
  }
63923
+ function buildResultData(exitCode, agentLabel, capture, answer, invocation) {
63924
+ const data = { exitCode, agent: agentLabel };
63925
+ if (capture)
63926
+ data.answer = answer;
63927
+ if (invocation !== undefined)
63928
+ data.invocation = invocation;
63929
+ return data;
63930
+ }
63762
63931
  function asOptionalString(value) {
63763
63932
  if (value === undefined || value === null)
63764
63933
  return;
@@ -63786,13 +63955,16 @@ function asOptionalNumber(value) {
63786
63955
  }
63787
63956
  return;
63788
63957
  }
63789
- async function writePartialWorkArtifact(context4, agentLabel, model, captured, cwd) {
63958
+ async function writePartialWorkArtifact(context4, agentLabel, model, traced, cwd) {
63790
63959
  try {
63791
- const exitReason = captured.signal !== undefined ? `killed by signal ${captured.signal} (likely timeout)` : `exited with code ${captured.exitCode}`;
63960
+ const signal = traced.signal;
63961
+ const exitReason = signal !== undefined ? `killed by signal ${signal} (likely timeout or cancellation)` : traced.message !== undefined ? `dispatch error: ${traced.message}` : `exited with code ${traced.exitCode}`;
63792
63962
  const diffStat = await gitDiffStat(cwd);
63793
- const stdoutTail = tail(captured.answer, PARTIAL_ARTIFACT_TAIL_CHARS);
63794
- const stderrTail = tail(captured.stderr ?? "", PARTIAL_ARTIFACT_TAIL_CHARS);
63963
+ const stdoutTail = tail(traced.stdout, PARTIAL_ARTIFACT_TAIL_CHARS);
63964
+ const stderrTail = tail(traced.stderr ?? "", PARTIAL_ARTIFACT_TAIL_CHARS);
63795
63965
  const headerLine = model !== undefined ? `${agentLabel} (model: ${model})` : agentLabel;
63966
+ const inv = traced.invocation;
63967
+ const argvLine = inv ? `${inv.command} ${inv.argv.join(" ")}` : "(invocation not captured)";
63796
63968
  const body = [
63797
63969
  `# Partial-work handoff \u2014 ${headerLine}`,
63798
63970
  "",
@@ -63801,7 +63973,18 @@ async function writePartialWorkArtifact(context4, agentLabel, model, captured, c
63801
63973
  `- agent: ${agentLabel}`,
63802
63974
  `- model: ${model ?? "(default)"}`,
63803
63975
  `- exit reason: ${exitReason}`,
63804
- `- elapsed: ${captured.durationMs ?? "unknown"}ms`,
63976
+ `- elapsed: ${traced.durationMs ?? "unknown"}ms`,
63977
+ "",
63978
+ "## resolved invocation",
63979
+ "",
63980
+ `- command: ${argvLine}`,
63981
+ `- cwd: ${inv?.cwd ?? "(inherit)"}`,
63982
+ `- mode: ${inv?.mode ?? "unknown"}`,
63983
+ `- timeoutMs: ${inv?.timeoutMs ?? "(none)"}`,
63984
+ `- continue: ${inv?.continue ?? false}`,
63985
+ `- output: ${inv?.outputMode ?? "unknown"}`,
63986
+ `- stdinInteractive: ${inv?.stdinInteractive ?? false}`,
63987
+ `- translatedFrom: ${inv?.translatedFrom ?? "(none)"}`,
63805
63988
  "",
63806
63989
  "## git diff --stat",
63807
63990
  "```",
@@ -65068,6 +65251,7 @@ __export(exports_src2, {
65068
65251
  renderReview: () => renderReview,
65069
65252
  registerSystemEventTap: () => registerSystemEventTap,
65070
65253
  registerSpurBuiltins: () => registerSpurBuiltins,
65254
+ readVerdictArtifact: () => readVerdictArtifact,
65071
65255
  readVerdict: () => readVerdict,
65072
65256
  parseVerdict: () => parseVerdict,
65073
65257
  parsePsOutput: () => parsePsOutput,
@@ -65077,10 +65261,14 @@ __export(exports_src2, {
65077
65261
  hitlConfirmDefault: () => hitlConfirmDefault,
65078
65262
  hitlAutoApproveEnabled: () => hitlAutoApproveEnabled,
65079
65263
  hasPopulatedPriorityTable: () => hasPopulatedPriorityTable,
65264
+ formatNoopMessage: () => formatNoopMessage,
65265
+ formatDenialMessage: () => formatDenialMessage,
65080
65266
  extractSystemEventActor: () => extractSystemEventActor,
65081
65267
  extractReviewSectionBody: () => extractReviewSectionBody,
65268
+ evaluateDoneTransition: () => evaluateDoneTransition,
65082
65269
  deriveVerdict: () => deriveVerdict,
65083
65270
  createPsProcessInspector: () => createPsProcessInspector,
65271
+ computeAggregate: () => computeAggregate,
65084
65272
  clampToolUseLimit: () => clampToolUseLimit,
65085
65273
  bridgeEventBus: () => bridgeEventBus,
65086
65274
  WorkflowAppService: () => WorkflowAppService,
@@ -73122,7 +73310,7 @@ import { join as join15, resolve as resolve5 } from "path";
73122
73310
  var CLI_CONFIG = {
73123
73311
  binaryName: "spur",
73124
73312
  binaryLabel: "spur",
73125
- binaryVersion: "0.3.13",
73313
+ binaryVersion: "0.3.15",
73126
73314
  configDir: ".spur",
73127
73315
  configFile: ".spur/config.yaml",
73128
73316
  databaseFile: ".spur/spur.db"
@@ -78973,18 +79161,15 @@ var contextInjector = (appRt) => async function contextInjectorMiddleware(c3, ne
78973
79161
  await next();
78974
79162
  };
78975
79163
 
78976
- // ../server/src/middleware/error-handler.ts
78977
- init_dist();
78978
-
78979
79164
  // ../server/src/errors.ts
78980
- init_src3();
79165
+ init_errors5();
78981
79166
 
78982
79167
  // ../server/src/middleware/error-handler.ts
78983
79168
  function hasHttpStatus(err) {
78984
79169
  return typeof err === "object" && err !== null && "status" in err && typeof err.status === "number";
78985
79170
  }
78986
79171
  function isAppErrorLike(err) {
78987
- return isAppError(err);
79172
+ return typeof err === "object" && err !== null && "code" in err && typeof err.code === "string" && "message" in err && typeof err.message === "string";
78988
79173
  }
78989
79174
  function errorEnvelope(code, message, details) {
78990
79175
  return { ok: false, error: { code, message, ...details !== undefined ? { details } : {} } };
@@ -79661,7 +79846,6 @@ var contract = {
79661
79846
 
79662
79847
  // ../server/src/modules/feature/handlers.ts
79663
79848
  init_schema4();
79664
- init_dist();
79665
79849
  var os2 = implement(contract);
79666
79850
  function createResponseShape(r2) {
79667
79851
  return { id: r2.ref.id, filePath: r2.ref.filePath };
@@ -79684,7 +79868,7 @@ function createFeatureHandlers(ctx) {
79684
79868
  show: os2.feature.show.handler(async ({ input }) => {
79685
79869
  const result = await ctx.featureService().show(input.id);
79686
79870
  if (!result)
79687
- throw new NotFoundError(`Feature ${input.id} not found`);
79871
+ throw new HTTPException(404, { message: `Feature ${input.id} not found` });
79688
79872
  return {
79689
79873
  ok: true,
79690
79874
  data: {
@@ -79713,7 +79897,7 @@ function createFeatureHandlers(ctx) {
79713
79897
  const folders = ctx.planningFolders();
79714
79898
  const feature = await ctx.featureService().show(input.id);
79715
79899
  if (!feature)
79716
- throw new NotFoundError(`Feature ${input.id} not found`);
79900
+ throw new HTTPException(404, { message: `Feature ${input.id} not found` });
79717
79901
  const { FeatureCheckService: FeatureCheckService2 } = await Promise.resolve().then(() => (init_feature_check(), exports_feature_check));
79718
79902
  const svc = new FeatureCheckService2(ctx.fs);
79719
79903
  return {
@@ -79727,7 +79911,7 @@ function createFeatureHandlers(ctx) {
79727
79911
  body: os2.feature.body.handler(async ({ input }) => {
79728
79912
  const feature = await ctx.featureService().show(input.id);
79729
79913
  if (!feature)
79730
- throw new NotFoundError(`Feature ${input.id} not found`);
79914
+ throw new HTTPException(404, { message: `Feature ${input.id} not found` });
79731
79915
  await ctx.featureService().updateBody(input.id, input.body);
79732
79916
  return { ok: true, data: {} };
79733
79917
  }),
@@ -79899,17 +80083,33 @@ function parseLimit(raw2, fallback) {
79899
80083
  }
79900
80084
 
79901
80085
  // ../server/src/modules/observability/index.ts
79902
- init_src3();
79903
80086
  var ledgerWatcher;
80087
+ var ledgerWatcherLoad;
79904
80088
  var watcherPath;
79905
- function getLedgerWatcher(path9) {
79906
- if (!ledgerWatcher || watcherPath !== path9) {
79907
- ledgerWatcher?.stop();
79908
- ledgerWatcher = new TokenLedgerWatcher({ ledgerPath: path9 });
79909
- watcherPath = path9;
79910
- ledgerWatcher.start();
80089
+ async function getLedgerWatcher(path9) {
80090
+ if (watcherPath === path9) {
80091
+ if (ledgerWatcher)
80092
+ return ledgerWatcher;
80093
+ if (ledgerWatcherLoad)
80094
+ return ledgerWatcherLoad;
80095
+ }
80096
+ ledgerWatcher?.stop();
80097
+ ledgerWatcher = undefined;
80098
+ watcherPath = path9;
80099
+ const load2 = Promise.resolve().then(() => (init_src3(), exports_src2)).then(({ TokenLedgerWatcher: TokenLedgerWatcher2 }) => {
80100
+ const watcher2 = new TokenLedgerWatcher2({ ledgerPath: path9 });
80101
+ watcher2.start();
80102
+ return watcher2;
80103
+ });
80104
+ ledgerWatcherLoad = load2;
80105
+ const watcher = await load2;
80106
+ if (watcherPath === path9 && ledgerWatcherLoad === load2) {
80107
+ ledgerWatcher = watcher;
80108
+ ledgerWatcherLoad = undefined;
80109
+ } else {
80110
+ watcher.stop();
79911
80111
  }
79912
- return ledgerWatcher;
80112
+ return watcher;
79913
80113
  }
79914
80114
  function toolUseSsePayload(event2) {
79915
80115
  return {
@@ -79924,7 +80124,7 @@ function handleProcesses(ctx) {
79924
80124
  const snapshot = await ctx.processInventory().snapshot();
79925
80125
  return c3.json(snapshot);
79926
80126
  } catch (err) {
79927
- if (err instanceof UnsupportedProcessPlatformError) {
80127
+ if (err instanceof Error && "code" in err && err.code === "UNSUPPORTED_PLATFORM") {
79928
80128
  return c3.json({ error: err.message, code: err.code }, 501);
79929
80129
  }
79930
80130
  const message = err instanceof Error ? err.message : String(err);
@@ -79943,7 +80143,7 @@ function handleToolUseGet(ctx) {
79943
80143
  limitRaw = parsed;
79944
80144
  }
79945
80145
  const before = c3.req.query("before") ?? undefined;
79946
- const limit = clampToolUseLimit(limitRaw);
80146
+ const limit = limitRaw === undefined || !Number.isFinite(limitRaw) || limitRaw <= 0 ? 200 : Math.min(limitRaw, 1000);
79947
80147
  const snapshot = ctx.tokenLedger().snapshot({ limit, before });
79948
80148
  return c3.json(snapshot);
79949
80149
  } catch (err) {
@@ -79972,7 +80172,7 @@ function handleToolUseStream(ctx) {
79972
80172
  closeController();
79973
80173
  };
79974
80174
  const stream = new ReadableStream({
79975
- start(controller) {
80175
+ async start(controller) {
79976
80176
  const encoder = new TextEncoder;
79977
80177
  closeController = () => {
79978
80178
  try {
@@ -79985,17 +80185,17 @@ function handleToolUseStream(ctx) {
79985
80185
  }
79986
80186
  signal.addEventListener("abort", teardown);
79987
80187
  heartbeatInterval = setInterval(sendSseKeepalive, 15000, closed, controller, encoder);
79988
- enqueueSseFrame(closed, controller, encoder, {
79989
- type: "connected",
79990
- occurredAt: new Date().toISOString()
79991
- });
79992
80188
  try {
79993
80189
  const path9 = ctx.tokenLedger().path;
79994
- const watcher = getLedgerWatcher(path9);
80190
+ const watcher = await getLedgerWatcher(path9);
79995
80191
  unsubscribe = watcher.subscribe((event2) => {
79996
80192
  if (!enqueueSseFrame(closed, controller, encoder, toolUseSsePayload(event2)))
79997
80193
  teardown();
79998
80194
  });
80195
+ enqueueSseFrame(closed, controller, encoder, {
80196
+ type: "connected",
80197
+ occurredAt: new Date().toISOString()
80198
+ });
79999
80199
  } catch {
80000
80200
  enqueueSseFrame(closed, controller, encoder, {
80001
80201
  type: "error",
@@ -80027,7 +80227,6 @@ var observabilityModule = {
80027
80227
 
80028
80228
  // ../server/src/modules/task/handlers.ts
80029
80229
  init_schema4();
80030
- init_dist();
80031
80230
  var os3 = implement(contract);
80032
80231
  function toFilters(query) {
80033
80232
  if (!query)
@@ -80068,7 +80267,7 @@ function createTaskHandlers(ctx) {
80068
80267
  show: os3.task.show.handler(async ({ input }) => {
80069
80268
  const result = await ctx.taskService().show(input.wbs);
80070
80269
  if (!result)
80071
- throw new NotFoundError(`Task ${input.wbs} not found`);
80270
+ throw new HTTPException(404, { message: `Task ${input.wbs} not found` });
80072
80271
  return {
80073
80272
  ok: true,
80074
80273
  data: {
@@ -83546,6 +83745,13 @@ var STATUS_TITLE = {
83546
83745
  done: "Done",
83547
83746
  cancelled: "Canceled"
83548
83747
  };
83748
+ function canonicalStatusOrRaw(raw2) {
83749
+ try {
83750
+ return normalizeTaskStatus(raw2);
83751
+ } catch {
83752
+ return raw2;
83753
+ }
83754
+ }
83549
83755
  function renderTaskBoard(tasks, boardTitle, color, columns2) {
83550
83756
  const lines = ["", ` ${color.blue(`Kanban Board \u2014 ${boardTitle}`)}`, ""];
83551
83757
  for (const status of columns2) {
@@ -83631,7 +83837,7 @@ ${result.content}`);
83631
83837
  context4.setExitCode(1);
83632
83838
  }
83633
83839
  });
83634
- task.command("update").summary("Update a task status or replace a section.").argument("<wbs>", "Task WBS number").argument("[status]", "New status (for lifecycle transition)").option("--section <name>", "Section name to replace").option("--from-file <path>", "File to read section body from (requires --section)").option("--feature <id>", "Set the feature_id frontmatter field (traceability edge)").option("--priority <p>", "Set the priority frontmatter field (P0\u2013P3)").option("--no-lifecycle", "Suppress lifecycle workflow run creation (use during pipeline runs to avoid orphaned lifecycle runs)").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, status, options) => {
83840
+ task.command("update").summary("Update a task status or replace a section.").argument("<wbs>", "Task WBS number").argument("[status]", "New status (for lifecycle transition)").option("--section <name>", "Section name to replace").option("--from-file <path>", "File to read section body from (requires --section)").option("--feature <id>", "Set the feature_id frontmatter field (traceability edge)").option("--priority <p>", "Set the priority frontmatter field (P0\u2013P3)").option("--no-lifecycle", "Suppress lifecycle workflow run creation (use during pipeline runs to avoid orphaned lifecycle runs)").option("--force-done", "Allow transitioning to `done` even when the verify verdict is not PASS; records an override (task 0292)").option("--reason <text>", "Rationale for a forced-done override (paired with --force-done; persisted as done_reason)").option("--verdict-dir <path>", "Directory holding <wbs>-verdict.json artifacts (default: .spur/run)").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, status, options) => {
83635
83841
  const svc = await makeService2(context4, options.folder, options.lifecycle === false);
83636
83842
  try {
83637
83843
  if (options.section !== undefined) {
@@ -83659,6 +83865,7 @@ ${result.content}`);
83659
83865
  context4.output.write(`Set ${key}=${value2} on task ${result.ref.id}`);
83660
83866
  }
83661
83867
  } else if (status !== undefined) {
83868
+ status = canonicalStatusOrRaw(status);
83662
83869
  if (options.lifecycle !== false && (status === "done" || status === "testing")) {
83663
83870
  const adapter = makeLifecycleAdapter(context4, TASK_LIFECYCLE_PROFILE);
83664
83871
  if (adapter === undefined) {
@@ -83671,11 +83878,59 @@ ${result.content}`);
83671
83878
  }
83672
83879
  }
83673
83880
  }
83881
+ let forcedDone = false;
83882
+ let forcedDoneReason;
83883
+ let forcedDoneVerdict;
83884
+ if (status === "done") {
83885
+ const current = await svc.show(wbs);
83886
+ const verdictDir = options.verdictDir ?? join22(context4.cwd, ".spur", "run");
83887
+ const loaded = await readVerdictArtifact(context4.fs, verdictDir, wbs);
83888
+ const guardOutcome = evaluateDoneTransition({
83889
+ wbs,
83890
+ taskFilePath: current.filePath,
83891
+ currentStatus: canonicalStatusOrRaw(String(current.frontmatter.status)),
83892
+ targetStatus: "done",
83893
+ forced: options.forceDone === true,
83894
+ reason: options.reason,
83895
+ artifact: loaded.artifact
83896
+ });
83897
+ if (guardOutcome.kind === "noop") {
83898
+ if (options.json) {
83899
+ context4.output.write(toJson2({ ok: true, noop: true, wbs, status: "done" }));
83900
+ } else {
83901
+ context4.output.write(guardOutcome.message);
83902
+ }
83903
+ return;
83904
+ }
83905
+ if (guardOutcome.kind === "deny") {
83906
+ context4.output.error(guardOutcome.message);
83907
+ context4.setExitCode(1);
83908
+ return;
83909
+ }
83910
+ if (guardOutcome.reason === "forced" && loaded.artifact !== undefined) {
83911
+ forcedDone = true;
83912
+ forcedDoneReason = options.reason;
83913
+ forcedDoneVerdict = loaded.artifact.verdict;
83914
+ }
83915
+ }
83674
83916
  const result = await svc.updateStatus(wbs, status);
83917
+ if (forcedDone) {
83918
+ try {
83919
+ await svc.updateField(wbs, "done_forced", "true");
83920
+ if (forcedDoneReason !== undefined && forcedDoneReason.length > 0) {
83921
+ await svc.updateField(wbs, "done_reason", forcedDoneReason);
83922
+ }
83923
+ } catch (auditErr) {
83924
+ context4.output.error(`warning: failed to record done-forced audit fields: ${String(auditErr)}`);
83925
+ }
83926
+ }
83675
83927
  if (options.json) {
83676
83928
  context4.output.write(toJson2(result));
83677
83929
  } else {
83678
83930
  context4.output.write(`${result.ref.id}: ${result.fromStatus} \u2192 ${result.toStatus}`);
83931
+ if (forcedDone && forcedDoneVerdict !== undefined) {
83932
+ context4.output.write(`\u24D8 Override recorded: task advanced to done despite ${forcedDoneVerdict} verdict (done_forced=true).`);
83933
+ }
83679
83934
  }
83680
83935
  if (status === "done" && !options.json) {
83681
83936
  const task2 = await svc.show(wbs);