@gobing-ai/spur 0.4.27 → 0.4.28

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
@@ -55606,6 +55606,38 @@ async function readWorkflowStateVisitRows(db, workflows, opts = {}) {
55606
55606
  }
55607
55607
  return rows;
55608
55608
  }
55609
+ async function readWorkflowHostPauseRows(db, workflows, opts = {}) {
55610
+ const sql = `SELECT json_extract(r.metadata_json, '$.hostPauses') AS hostPausesJson
55611
+ FROM runs r
55612
+ WHERE r.workflow_name = ?
55613
+ AND ${perStatePopulation(opts.sinceMs)}
55614
+ AND json_extract(r.metadata_json, '$.hostPauses') IS NOT NULL`;
55615
+ const rows = [];
55616
+ for (const workflow of workflows) {
55617
+ const found = await db.queryAll(sql, workflow, ...TERMINAL_RUN_STATUSES, ...opts.sinceMs !== undefined ? [opts.sinceMs] : []);
55618
+ for (const r of found)
55619
+ rows.push({ workflow, hostPausesJson: r.hostPausesJson });
55620
+ }
55621
+ return rows;
55622
+ }
55623
+ async function readWorkflowOpenPauseRows(db, workflows) {
55624
+ const sql = `SELECT r.id AS runId, r.updated_at AS updatedAt,
55625
+ (SELECT a.result_json FROM action_runs a
55626
+ WHERE a.run_id = r.id AND (a.status = 'paused' OR a.result_json LIKE '%"pause":true%')
55627
+ ORDER BY a.created_at DESC, a.rowid DESC LIMIT 1) AS pausedResultJson
55628
+ FROM runs r
55629
+ WHERE r.workflow_name = ?
55630
+ AND r.status IN ('paused', 'interrupted')
55631
+ AND json_extract(r.metadata_json, '$.dryRun') IS NOT 1
55632
+ ORDER BY r.id`;
55633
+ const rows = [];
55634
+ for (const workflow of workflows) {
55635
+ const found = await db.queryAll(sql, workflow);
55636
+ for (const r of found)
55637
+ rows.push({ workflow, ...r });
55638
+ }
55639
+ return rows;
55640
+ }
55609
55641
  var TERMINAL_RUN_STATUSES;
55610
55642
  var init_workflow_measurements = __esm(() => {
55611
55643
  TERMINAL_RUN_STATUSES = ["done", "failed", "cancelled"];
@@ -58117,7 +58149,7 @@ var init_run_dao = __esm(() => {
58117
58149
  });
58118
58150
  }
58119
58151
  traceRows(filter) {
58120
- return this.adapter.queryAll(`SELECT id, workflow_name, mode, status, agent, started_at, completed_at, metadata_json
58152
+ return this.adapter.queryAll(`SELECT id, workflow_name, mode, status, agent, started_at, completed_at, metadata_json, updated_at
58121
58153
  FROM runs
58122
58154
  WHERE (?1 IS NULL OR workflow_name = ?1)
58123
58155
  AND (?2 IS NULL OR status = ?2)
@@ -58131,7 +58163,7 @@ var init_run_dao = __esm(() => {
58131
58163
  LIMIT ?6`, filter.workflow ?? null, filter.status ?? null, filter.since ?? null, filter.before?.started_at ?? null, filter.before?.id ?? null, filter.limit);
58132
58164
  }
58133
58165
  traceRowById(runId) {
58134
- return this.adapter.queryFirst("SELECT id, workflow_name, mode, status, agent, started_at, completed_at, terminal_reason, metadata_json, pid FROM runs WHERE id = ?", runId);
58166
+ return this.adapter.queryFirst("SELECT id, workflow_name, mode, status, agent, started_at, completed_at, updated_at, terminal_reason, metadata_json, pid FROM runs WHERE id = ?", runId);
58135
58167
  }
58136
58168
  mergeMetadata(runId, patch) {
58137
58169
  return this.adapter.run(`UPDATE runs
@@ -104895,7 +104927,16 @@ class AgentRunActionRunner {
104895
104927
  error: `agent.run: work item for '${context.stateOrNodeId}' could not be written: ${error.message}`
104896
104928
  };
104897
104929
  }
104898
- return { ok: true, pause: true, data: { surface: "host", workItem, reason: "host-work-item" } };
104930
+ return {
104931
+ ok: true,
104932
+ pause: true,
104933
+ data: {
104934
+ surface: "host",
104935
+ workItem,
104936
+ reason: "host-work-item",
104937
+ ...invocation.timeoutMs !== undefined ? { timeoutMs: invocation.timeoutMs } : {}
104938
+ }
104939
+ };
104899
104940
  }
104900
104941
  async execute(options, context, execution) {
104901
104942
  const startedAt = new Date().toISOString();
@@ -121379,6 +121420,102 @@ var init_process_inventory_service = __esm(() => {
121379
121420
  init_process_inspector();
121380
121421
  });
121381
121422
 
121423
+ // ../../packages/app/src/workflow/host-pause.ts
121424
+ function readHostPauses(metadata) {
121425
+ if (metadata === null || typeof metadata !== "object")
121426
+ return [];
121427
+ const raw = metadata[HOST_PAUSES_KEY];
121428
+ if (!Array.isArray(raw))
121429
+ return [];
121430
+ const out = [];
121431
+ for (const entry of raw) {
121432
+ if (entry === null || typeof entry !== "object")
121433
+ continue;
121434
+ const row = entry;
121435
+ const { node, action, pausedAt, resumedAt, hostDurationMs } = row;
121436
+ if (typeof node !== "string" || typeof pausedAt !== "string" || typeof resumedAt !== "string")
121437
+ continue;
121438
+ if (typeof hostDurationMs !== "number" || !Number.isFinite(hostDurationMs))
121439
+ continue;
121440
+ out.push({
121441
+ node,
121442
+ action: typeof action === "string" ? action : "agent.run",
121443
+ pausedAt,
121444
+ resumedAt,
121445
+ hostDurationMs,
121446
+ ...typeof row.executor === "string" ? { executor: row.executor } : {}
121447
+ });
121448
+ }
121449
+ return out;
121450
+ }
121451
+ function appendHostPause(metadata, pause) {
121452
+ return [...readHostPauses(metadata), pause];
121453
+ }
121454
+ function hostPauseMsByNode(pauses) {
121455
+ const totals = new Map;
121456
+ for (const pause of pauses)
121457
+ totals.set(pause.node, (totals.get(pause.node) ?? 0) + pause.hostDurationMs);
121458
+ return totals;
121459
+ }
121460
+ function hostDurationMs(pausedAt, resumedAt) {
121461
+ const start = Date.parse(pausedAt);
121462
+ const end = Date.parse(resumedAt);
121463
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start)
121464
+ return null;
121465
+ return end - start;
121466
+ }
121467
+ function isDegenerateReceiptInterval(startedAt, finishedAt) {
121468
+ if (startedAt === undefined || finishedAt === undefined)
121469
+ return true;
121470
+ const start = Date.parse(startedAt);
121471
+ const end = Date.parse(finishedAt);
121472
+ if (!Number.isFinite(start) || !Number.isFinite(end))
121473
+ return true;
121474
+ return end <= start;
121475
+ }
121476
+ function resolveStallThresholdMs(nodeTimeoutMs, readEnvVar = getEnvVar) {
121477
+ if (nodeTimeoutMs !== undefined && Number.isFinite(nodeTimeoutMs) && nodeTimeoutMs > 0)
121478
+ return nodeTimeoutMs;
121479
+ const raw = readEnvVar(STALL_THRESHOLD_MS_ENV);
121480
+ if (raw !== undefined) {
121481
+ const parsed = Number(raw);
121482
+ if (Number.isFinite(parsed) && parsed > 0)
121483
+ return Math.trunc(parsed);
121484
+ }
121485
+ return STALL_THRESHOLD_MS_DEFAULT;
121486
+ }
121487
+ function openPauseMs(pausedAt, nowMs) {
121488
+ if (pausedAt === null || pausedAt === undefined)
121489
+ return null;
121490
+ const start = typeof pausedAt === "number" ? pausedAt : /^\d+$/.test(pausedAt) ? Number(pausedAt) : Date.parse(pausedAt);
121491
+ if (!Number.isFinite(start))
121492
+ return null;
121493
+ return Math.max(0, nowMs - start);
121494
+ }
121495
+ function pausedNodeBudgetMs(rows) {
121496
+ for (let i = rows.length - 1;i >= 0; i--) {
121497
+ const row = rows[i];
121498
+ if (row === undefined || row.result_json === null)
121499
+ continue;
121500
+ if (row.status !== "paused" && !/"pause"\s*:\s*true/.test(row.result_json))
121501
+ continue;
121502
+ try {
121503
+ const budget = JSON.parse(row.result_json)?.data?.timeoutMs;
121504
+ return typeof budget === "number" && Number.isFinite(budget) && budget > 0 ? budget : undefined;
121505
+ } catch {
121506
+ return;
121507
+ }
121508
+ }
121509
+ return;
121510
+ }
121511
+ function stalledLabel(runId, pauseMs) {
121512
+ return `stalled \u2014 paused ${Math.round(pauseMs / 1000)}s; resume: spur workflow continue ${runId}`;
121513
+ }
121514
+ var HOST_PAUSES_KEY = "hostPauses", STALL_THRESHOLD_MS_DEFAULT = 1800000, STALL_THRESHOLD_MS_ENV = "SPUR_WORKFLOW_STALL_MS";
121515
+ var init_host_pause = __esm(() => {
121516
+ init_src();
121517
+ });
121518
+
121382
121519
  // ../../packages/app/src/workflow/run-measurements.ts
121383
121520
  function median(values) {
121384
121521
  if (values.length === 0)
@@ -121479,6 +121616,18 @@ async function readStateMetrics(db, workflows, opts = {}) {
121479
121616
  row.visits += t.visitsInRun;
121480
121617
  row.retries += Math.max(0, t.visitsInRun - 1);
121481
121618
  }
121619
+ for (const h of await readWorkflowHostPauseRows(db, workflows, opts)) {
121620
+ let parsed;
121621
+ try {
121622
+ parsed = JSON.parse(h.hostPausesJson);
121623
+ } catch {
121624
+ continue;
121625
+ }
121626
+ for (const [node, ms] of hostPauseMsByNode(readHostPauses({ hostPauses: parsed }))) {
121627
+ const row = rowFor(h.workflow, node);
121628
+ row.hostMs = (row.hostMs ?? 0) + ms;
121629
+ }
121630
+ }
121482
121631
  const rows = [];
121483
121632
  for (const [workflow, states] of byWorkflow) {
121484
121633
  for (const [state, row] of states) {
@@ -121489,14 +121638,27 @@ async function readStateMetrics(db, workflows, opts = {}) {
121489
121638
  agentRunCount: row.agentRunCount,
121490
121639
  wallMsP50: nearestRankPercentile(row.wallSamples, 50),
121491
121640
  wallMsP90: nearestRankPercentile(row.wallSamples, 90),
121492
- retries: row.retries
121641
+ retries: row.retries,
121642
+ ...row.hostMs !== undefined ? { hostMs: row.hostMs } : {}
121493
121643
  });
121494
121644
  }
121495
121645
  }
121496
121646
  return rows.sort((a, b) => a.workflow === b.workflow ? a.state < b.state ? -1 : a.state > b.state ? 1 : 0 : a.workflow < b.workflow ? -1 : 1);
121497
121647
  }
121648
+ async function readStalledRuns(db, workflows, nowMs = Date.now()) {
121649
+ const stalled = [];
121650
+ for (const row of await readWorkflowOpenPauseRows(db, workflows)) {
121651
+ const ageMs = openPauseMs(row.updatedAt, nowMs);
121652
+ const budget = pausedNodeBudgetMs([{ status: "paused", result_json: row.pausedResultJson }]);
121653
+ if (ageMs !== null && ageMs > resolveStallThresholdMs(budget)) {
121654
+ stalled.push({ workflow: row.workflow, runId: row.runId, stalledMs: ageMs });
121655
+ }
121656
+ }
121657
+ return stalled;
121658
+ }
121498
121659
  var init_run_measurements = __esm(() => {
121499
121660
  init_src2();
121661
+ init_host_pause();
121500
121662
  init_terminal_reason();
121501
121663
  });
121502
121664
 
@@ -130963,6 +131125,174 @@ var init_token_ledger_watcher = __esm(() => {
130963
131125
  init_token_ledger_service();
130964
131126
  });
130965
131127
 
131128
+ // ../../packages/app/src/services/sdlc-actions.ts
131129
+ function isReservedAction(name) {
131130
+ return RESERVED_ACTIONS.includes(name);
131131
+ }
131132
+ function resolveAction(builder, name) {
131133
+ const reserved = isReservedAction(name);
131134
+ const declared = builder?.actions?.[name];
131135
+ if (declared !== undefined) {
131136
+ return Array.isArray(declared) ? { name, reserved, source: "config", kind: "composite", steps: [...declared] } : { name, reserved, source: "config", kind: "command", command: declared };
131137
+ }
131138
+ if ((builder?.preset ?? "bun") === "bun") {
131139
+ if (reserved)
131140
+ return { name, reserved, source: "preset", kind: "command", command: BUN_PRESET[name] };
131141
+ } else {
131142
+ const command = reserved ? STACK_NEUTRAL_DEFAULTS[name] : undefined;
131143
+ if (command !== undefined)
131144
+ return { name, reserved, source: "default", kind: "command", command };
131145
+ }
131146
+ return { name, reserved, source: "unbound", kind: "unbound" };
131147
+ }
131148
+ function listActions(builder) {
131149
+ const custom = Object.keys(builder?.actions ?? {}).filter((name) => !isReservedAction(name)).sort();
131150
+ return [...RESERVED_ACTIONS, ...custom].map((name) => resolveAction(builder, name));
131151
+ }
131152
+ function renderScoped(command, values) {
131153
+ let rendered = command;
131154
+ for (const [placeholder, items] of [
131155
+ ["{files}", values.files],
131156
+ ["{paths}", values.paths]
131157
+ ]) {
131158
+ if (!rendered.includes(placeholder))
131159
+ continue;
131160
+ if (items === undefined)
131161
+ throw new Error(`renderScoped: ${placeholder} has no value`);
131162
+ if (items.length === 0)
131163
+ throw new Error(`renderScoped: ${placeholder} has an empty list`);
131164
+ rendered = rendered.replaceAll(placeholder, () => items.map(shQuote2).join(" "));
131165
+ }
131166
+ if (rendered.includes("{workspace}")) {
131167
+ const workspace = values.workspace;
131168
+ if (workspace === undefined)
131169
+ throw new Error("renderScoped: {workspace} has no value");
131170
+ if (workspace.length === 0)
131171
+ throw new Error("renderScoped: {workspace} is empty");
131172
+ rendered = rendered.replaceAll("{workspace}", () => shQuote2(workspace));
131173
+ }
131174
+ return rendered;
131175
+ }
131176
+ var RESERVED_ACTIONS, BUN_PRESET, STACK_NEUTRAL_DEFAULTS;
131177
+ var init_sdlc_actions = __esm(() => {
131178
+ init_src();
131179
+ RESERVED_ACTIONS = SDLC_ACTIONS;
131180
+ BUN_PRESET = {
131181
+ build: "bun run build",
131182
+ dev: "bun run dev",
131183
+ clean: "bun run clean",
131184
+ format: "bun run format",
131185
+ autofix: "bun run autofix",
131186
+ lint: "bun run lint",
131187
+ typecheck: "bun run typecheck",
131188
+ test: "bun run test",
131189
+ "test-full": "bun run test:full",
131190
+ "test-heavy": "bun run test:heavy",
131191
+ "rules-pre": "bun run test-pre-check",
131192
+ "rules-post": "bun run test-post-check",
131193
+ check: "bun run spur-check",
131194
+ "check-full": "bun run spur-check:full",
131195
+ "check-feature": "bun run spur-check-feature",
131196
+ "lint-changed": "bunx biome check {files}",
131197
+ "typecheck-changed": "cd {workspace} && bun run typecheck",
131198
+ "test-changed": "cd {workspace} && bun test {paths}"
131199
+ };
131200
+ STACK_NEUTRAL_DEFAULTS = SDLC_STACK_NEUTRAL_ACTIONS;
131201
+ });
131202
+
131203
+ // ../../packages/app/src/services/verdict-replay.ts
131204
+ function citedTestAnchors(rows, max) {
131205
+ const seen = new Map;
131206
+ for (const row of rows) {
131207
+ for (const anchor of extractBacktickLineAnchors(row.evidence ?? "")) {
131208
+ if (!TEST_FILE_PATTERN.test(anchor.path) || seen.has(anchor.path))
131209
+ continue;
131210
+ seen.set(anchor.path, anchor.raw);
131211
+ }
131212
+ }
131213
+ return new Map([...seen].slice(0, max));
131214
+ }
131215
+ function replayWorkspace(file) {
131216
+ const parts = file.split("/");
131217
+ if (parts.length >= 3 && (parts[0] === "apps" || parts[0] === "packages"))
131218
+ return `${parts[0]}/${parts[1]}`;
131219
+ return ".";
131220
+ }
131221
+ async function replayCitedTests(rows, options = {}) {
131222
+ const cwd = options.cwd ?? process.cwd();
131223
+ const timeoutMs = options.timeoutMs ?? REPLAY_TIMEOUT_MS;
131224
+ const anchors = citedTestAnchors(rows, options.maxFiles ?? REPLAY_MAX_FILES);
131225
+ const files = [...anchors.keys()];
131226
+ const executor = options.executor ?? new NodeProcessExecutor2;
131227
+ const fileSystem = options.fs ?? createNodeFileSystem2();
131228
+ const resolved = resolveAction(options.builder, "test-changed");
131229
+ const template = resolved.kind === "command" ? resolved.command : resolved.kind === "composite" ? resolved.steps.join(" && ") : "";
131230
+ const ran = [];
131231
+ const skipped = [];
131232
+ const findings = [];
131233
+ const thresholdByWorkspace = new Map;
131234
+ for (const file of files) {
131235
+ const anchor = anchors.get(file) ?? file;
131236
+ const exists = await fileSystem.exists(`${cwd}/${file}`);
131237
+ if (!exists) {
131238
+ findings.push({ anchor, file, missing: true });
131239
+ continue;
131240
+ }
131241
+ if (template.length === 0)
131242
+ continue;
131243
+ const workspace = replayWorkspace(file);
131244
+ let threshold = thresholdByWorkspace.get(workspace);
131245
+ if (threshold === undefined) {
131246
+ threshold = await declaresCoverageThreshold(fileSystem, `${cwd}/${workspace}/bunfig.toml`);
131247
+ thresholdByWorkspace.set(workspace, threshold);
131248
+ }
131249
+ if (threshold) {
131250
+ skipped.push({ file, reason: `coverageThreshold in ${workspace}/bunfig.toml` });
131251
+ continue;
131252
+ }
131253
+ const path = workspace === "." ? file : file.slice(workspace.length + 1);
131254
+ const command = renderScoped(template, { workspace, paths: [path] });
131255
+ const result = await executor.run({
131256
+ command: "sh",
131257
+ args: ["-c", command],
131258
+ cwd,
131259
+ forceBuffered: true,
131260
+ rejectOnError: false,
131261
+ timeout: timeoutMs
131262
+ });
131263
+ const exitCode = result.exitCode ?? null;
131264
+ ran.push({ file, exitCode });
131265
+ if (result.outcome === "timeout")
131266
+ findings.push({ anchor, file, timedOut: true, exitCode });
131267
+ else if (exitCode !== 0)
131268
+ findings.push({ anchor, file, exitCode });
131269
+ }
131270
+ return { files, ran, skipped, findings };
131271
+ }
131272
+ async function declaresCoverageThreshold(fileSystem, bunfigPath) {
131273
+ try {
131274
+ if (!await fileSystem.exists(bunfigPath))
131275
+ return false;
131276
+ return COVERAGE_THRESHOLD_PATTERN.test(await fileSystem.readFile(bunfigPath));
131277
+ } catch {
131278
+ return false;
131279
+ }
131280
+ }
131281
+ function formatReplayReport(replay) {
131282
+ return {
131283
+ summary: `Replay: ${replay.files.length} cited test file(s), ${replay.findings.length} finding(s)${replay.skipped.length > 0 ? `, ${replay.skipped.length} skipped` : ""}`,
131284
+ findings: replay.findings.map((finding) => ` ${finding.anchor}: ${finding.missing ? "missing" : finding.timedOut ? "timed out" : `exit ${finding.exitCode ?? "unknown"}`}`)
131285
+ };
131286
+ }
131287
+ var REPLAY_MAX_FILES = 5, REPLAY_TIMEOUT_MS = 120000, TEST_FILE_PATTERN, COVERAGE_THRESHOLD_PATTERN;
131288
+ var init_verdict_replay = __esm(() => {
131289
+ init_dist5();
131290
+ init_sdlc_actions();
131291
+ init_task_check();
131292
+ TEST_FILE_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
131293
+ COVERAGE_THRESHOLD_PATTERN = /^\s*coverageThreshold\s*=/m;
131294
+ });
131295
+
130966
131296
  // ../../packages/app/src/workflow/checkpoint-contract.ts
130967
131297
  function parseCheckpointMetadata(raw) {
130968
131298
  if (!raw.startsWith("---"))
@@ -131429,6 +131759,16 @@ function commandGateOutcome(exitCode) {
131429
131759
  return "FAIL";
131430
131760
  return "BLOCKED";
131431
131761
  }
131762
+ function asOptionalTimeoutMs(value) {
131763
+ if (value === undefined || value === null)
131764
+ return { ok: true, ms: undefined };
131765
+ if (typeof value === "string" && value.trim() === "")
131766
+ return { ok: true, ms: undefined };
131767
+ const parsed = typeof value === "number" ? value : Number(value);
131768
+ if (!Number.isFinite(parsed) || parsed <= 0)
131769
+ return { ok: false };
131770
+ return { ok: true, ms: Math.trunc(parsed) };
131771
+ }
131432
131772
  function sleep4(ms) {
131433
131773
  return new Promise((resolve) => setTimeout(resolve, ms));
131434
131774
  }
@@ -131498,13 +131838,21 @@ class CommandGateActionRunner {
131498
131838
  const maxAttempts = Math.max(1, typeof retry.maxAttempts === "number" ? retry.maxAttempts : 1);
131499
131839
  const delayMs = Math.max(0, typeof retry.delayMs === "number" ? retry.delayMs : 1000);
131500
131840
  const retryOn = Array.isArray(retry.on) ? retry.on : [];
131501
- const timeoutMs = typeof options.timeoutMs === "number" ? options.timeoutMs : undefined;
131841
+ const timeout = asOptionalTimeoutMs(options.timeoutMs);
131842
+ if (!timeout.ok) {
131843
+ return {
131844
+ ok: false,
131845
+ error: `command.gate: timeoutMs must be a positive number, got "${String(options.timeoutMs)}"`
131846
+ };
131847
+ }
131848
+ const timeoutMs = timeout.ms;
131502
131849
  const env = childProcessEnv(context.vars);
131503
131850
  await this.fileSystem.ensureDir(normalize3(join59(normalized, "..")));
131504
131851
  let lastStdout = "";
131505
131852
  let lastStderr = "";
131506
131853
  let lastExitCode = -1;
131507
131854
  let attemptsRun = 0;
131855
+ let timedOut = false;
131508
131856
  for (let attempt = 1;attempt <= maxAttempts; attempt++) {
131509
131857
  attemptsRun = attempt;
131510
131858
  const res = await this.processExecutor.run({
@@ -131519,6 +131867,7 @@ class CommandGateActionRunner {
131519
131867
  lastStdout = res.stdout;
131520
131868
  lastStderr = res.stderr;
131521
131869
  lastExitCode = res.exitCode ?? -1;
131870
+ timedOut = res.outcome === "timeout";
131522
131871
  if (res.exitCode === 0) {
131523
131872
  await this.fileSystem.writeFile(normalized, `PASS
131524
131873
  `);
@@ -131553,7 +131902,8 @@ ${lastStderr}`;
131553
131902
  attempts: attemptsRun,
131554
131903
  stdout: lastStdout,
131555
131904
  stderr: lastStderr,
131556
- resultFile: normalized
131905
+ resultFile: normalized,
131906
+ ...timedOut ? { timedOut: true, timeoutMs } : {}
131557
131907
  };
131558
131908
  if (softFail) {
131559
131909
  return { ok: true, data: failureData };
@@ -135179,13 +135529,14 @@ async function outputArtifactForRun(cwd, runId) {
135179
135529
  }
135180
135530
  return;
135181
135531
  }
135182
- function rowToTraceEntry(row, cwd) {
135532
+ function rowToTraceEntry(row, cwd, nowMs = Date.now(), nodeBudgetMs) {
135183
135533
  let isDryRun = false;
135184
135534
  let failureReason;
135185
135535
  let version;
135186
135536
  let definitionDigest;
135187
135537
  let startState;
135188
135538
  let continuedFrom;
135539
+ let hostPauses;
135189
135540
  try {
135190
135541
  const meta = JSON.parse(row.metadata_json);
135191
135542
  isDryRun = meta.dryRun === true;
@@ -135206,6 +135557,9 @@ function rowToTraceEntry(row, cwd) {
135206
135557
  if (typeof meta.continuedFrom === "string" && meta.continuedFrom !== "") {
135207
135558
  continuedFrom = meta.continuedFrom;
135208
135559
  }
135560
+ const pauses = readHostPauses(meta);
135561
+ if (pauses.length > 0)
135562
+ hostPauses = pauses;
135209
135563
  } catch {}
135210
135564
  const entry = {
135211
135565
  runId: row.id,
@@ -135223,8 +135577,14 @@ function rowToTraceEntry(row, cwd) {
135223
135577
  ...version !== undefined ? { version } : {},
135224
135578
  ...definitionDigest !== undefined ? { definitionDigest } : {},
135225
135579
  ...startState !== undefined ? { startState } : {},
135226
- ...continuedFrom !== undefined ? { continuedFrom } : {}
135580
+ ...continuedFrom !== undefined ? { continuedFrom } : {},
135581
+ ...hostPauses !== undefined ? { hostPauses } : {}
135227
135582
  };
135583
+ const pauseAgeMs = row.status === "paused" || row.status === "interrupted" ? openPauseMs(row.updated_at ?? null, nowMs) : null;
135584
+ if (pauseAgeMs !== null && pauseAgeMs > resolveStallThresholdMs(nodeBudgetMs)) {
135585
+ entry.stalled = true;
135586
+ entry.stalledMs = pauseAgeMs;
135587
+ }
135228
135588
  const nextAction = traceNextAction(entry);
135229
135589
  if (nextAction !== undefined)
135230
135590
  entry.nextAction = nextAction;
@@ -135269,7 +135629,11 @@ function traceNextAction(run, outputArtifact) {
135269
135629
  return { label: "Follow run", kind: "command", value: `spur workflow trace ${run.runId} --follow` };
135270
135630
  }
135271
135631
  if (run.status === "paused" || run.status === "interrupted") {
135272
- return { label: "Continue run", kind: "command", value: `spur workflow continue ${run.runId}` };
135632
+ return {
135633
+ label: run.stalled === true ? "Resume stalled run" : "Continue run",
135634
+ kind: "command",
135635
+ value: `spur workflow continue ${run.runId}`
135636
+ };
135273
135637
  }
135274
135638
  if (run.status === "failed" && outputArtifact !== undefined) {
135275
135639
  return { label: "Inspect run log", kind: "path", value: outputArtifact };
@@ -135380,6 +135744,7 @@ var TRACE_IDENTIFIER, TRACE_RESULT_FIELDS, TRACE_INVOCATION_FIELDS;
135380
135744
  var init_workflow_trace_projection = __esm(() => {
135381
135745
  init_dist5();
135382
135746
  init_agent_execution();
135747
+ init_host_pause();
135383
135748
  init_run_storage();
135384
135749
  init_system_event_envelope();
135385
135750
  TRACE_IDENTIFIER = /^[A-Za-z0-9._:-]+$/;
@@ -135558,20 +135923,19 @@ class WorkflowInspectOps {
135558
135923
  since: filter.since,
135559
135924
  limit: filter.last ?? 20
135560
135925
  });
135561
- return {
135562
- entries: rows.map((row) => rowToTraceEntry(row, this.ctx.cwd)),
135563
- total: rows.length
135564
- };
135926
+ const actionDao = new ActionRunDao(db);
135927
+ const entries = await Promise.all(rows.map(async (row) => rowToTraceEntry(row, this.ctx.cwd, Date.now(), row.status === "paused" || row.status === "interrupted" ? pausedNodeBudgetMs(await actionDao.actionRowsByRunId(row.id)) : undefined)));
135928
+ return { entries, total: rows.length };
135565
135929
  }
135566
135930
  async traceRun(db, runId, preloadedRow) {
135567
135931
  const runDao = new RunDao(db);
135568
135932
  const row = preloadedRow ?? await runDao.traceRowById(runId);
135569
135933
  if (!row)
135570
135934
  throw new Error(`Run not found: ${runId}`);
135571
- const run = rowToTraceEntry(row, this.ctx.cwd);
135935
+ const actionRows = await new ActionRunDao(db).actionRowsByRunId(runId);
135936
+ const run = rowToTraceEntry(row, this.ctx.cwd, Date.now(), pausedNodeBudgetMs(actionRows));
135572
135937
  const phaseRows = await new PhaseRunDao(db).phaseRowsByRunId(runId);
135573
135938
  const transitionRows = await new TransitionRunDao(db).transitionRowsByRunId(runId);
135574
- const actionRows = await new ActionRunDao(db).actionRowsByRunId(runId);
135575
135939
  const costByActionId = new Map;
135576
135940
  if (actionRows.some((a) => a.kind === "agent.run")) {
135577
135941
  for (const a of actionRows) {
@@ -135679,6 +136043,7 @@ var init_workflow_inspect_ops = __esm(() => {
135679
136043
  init_dist5();
135680
136044
  init_decision_service();
135681
136045
  init_composition_lint();
136046
+ init_host_pause();
135682
136047
  init_plan_projection();
135683
136048
  init_run_record();
135684
136049
  init_workflow_resolver();
@@ -136198,8 +136563,9 @@ class WorkflowResumeOps {
136198
136563
  if (persistedDigest !== null && persistedDigest !== resolved.digest && "resumeDefinitionDigest" in rawMeta) {
136199
136564
  resumeVars.__definitionDigest = resolved.digest;
136200
136565
  }
136201
- if (hostPause.receipt !== undefined) {
136202
- await writeReceipt(createNodeFileSystem2(launchWorkdir), launchWorkdir, hostPause.receipt);
136566
+ const stamped = await this.recordHostPause(runId, hostPause.receipt, opts?.now);
136567
+ if (stamped !== undefined) {
136568
+ await writeReceipt(createNodeFileSystem2(launchWorkdir), launchWorkdir, stamped);
136203
136569
  }
136204
136570
  const result = await withWorktreeRunLease(await this.ctx.getDb(), launchWorkdir, workflow.name, runId, async (signal) => svc.resumeRun(workflow, runId, {
136205
136571
  ...signal !== undefined ? { signal } : {},
@@ -136221,6 +136587,39 @@ class WorkflowResumeOps {
136221
136587
  runResult.warnings = warnings;
136222
136588
  return runResult;
136223
136589
  }
136590
+ async recordHostPause(runId, receipt, now) {
136591
+ const db = await this.ctx.getDb();
136592
+ const rows = await new ActionRunDao(db).actionRowsByRunId(runId);
136593
+ const paused = [...rows].reverse().find((row) => row.status === "paused" || row.result_json !== null && /"pause"\s*:\s*true/.test(row.result_json));
136594
+ const pausedAt = paused?.started_at ?? null;
136595
+ if (pausedAt === null)
136596
+ return receipt;
136597
+ const resumedAt = (now ?? new Date).toISOString();
136598
+ const pauseMs = hostDurationMs(pausedAt, resumedAt);
136599
+ if (pauseMs === null)
136600
+ return receipt;
136601
+ const run = await new RunDao(db).traceRowById(runId);
136602
+ let metadata = {};
136603
+ try {
136604
+ metadata = JSON.parse(run?.metadata_json ?? "{}");
136605
+ } catch {
136606
+ metadata = {};
136607
+ }
136608
+ const pauses = appendHostPause(metadata, {
136609
+ node: paused?.node ?? "unknown",
136610
+ action: paused?.kind ?? "agent.run",
136611
+ pausedAt,
136612
+ resumedAt,
136613
+ hostDurationMs: pauseMs,
136614
+ executor: receipt?.executor ?? "host"
136615
+ });
136616
+ await new RunDao(db).mergeMetadata(runId, { [HOST_PAUSES_KEY]: pauses });
136617
+ if (receipt === undefined)
136618
+ return;
136619
+ if (!isDegenerateReceiptInterval(receipt.startedAt, receipt.finishedAt ?? undefined))
136620
+ return receipt;
136621
+ return { ...receipt, startedAt: pausedAt, finishedAt: resumedAt };
136622
+ }
136224
136623
  async resolveHostPauseReceipt(runId, workdir, opts, workflow) {
136225
136624
  const db = await this.ctx.getDb();
136226
136625
  if (!await hostExecutorPersisted(db, runId))
@@ -136452,6 +136851,7 @@ var init_workflow_resume_ops = __esm(() => {
136452
136851
  init_checkpoint_contract();
136453
136852
  init_composition_lint();
136454
136853
  init_host_executor();
136854
+ init_host_pause();
136455
136855
  init_progress_projection();
136456
136856
  init_receipt();
136457
136857
  init_workflow_resolver();
@@ -138346,6 +138746,7 @@ var init_src4 = __esm(() => {
138346
138746
  init_task_verdict();
138347
138747
  init_token_ledger_service();
138348
138748
  init_token_ledger_watcher();
138749
+ init_verdict_replay();
138349
138750
  init_verify_answer_lint();
138350
138751
  init_warm_turn_dispatch();
138351
138752
  init_workflow_clean_ops();
@@ -145574,81 +145975,6 @@ var init_task_diffstat = __esm(() => {
145574
145975
  init_task_triage();
145575
145976
  });
145576
145977
 
145577
- // ../../packages/app/src/services/sdlc-actions.ts
145578
- function isReservedAction(name) {
145579
- return RESERVED_ACTIONS.includes(name);
145580
- }
145581
- function resolveAction(builder, name) {
145582
- const reserved = isReservedAction(name);
145583
- const declared = builder?.actions?.[name];
145584
- if (declared !== undefined) {
145585
- return Array.isArray(declared) ? { name, reserved, source: "config", kind: "composite", steps: [...declared] } : { name, reserved, source: "config", kind: "command", command: declared };
145586
- }
145587
- if ((builder?.preset ?? "bun") === "bun") {
145588
- if (reserved)
145589
- return { name, reserved, source: "preset", kind: "command", command: BUN_PRESET[name] };
145590
- } else {
145591
- const command = reserved ? STACK_NEUTRAL_DEFAULTS[name] : undefined;
145592
- if (command !== undefined)
145593
- return { name, reserved, source: "default", kind: "command", command };
145594
- }
145595
- return { name, reserved, source: "unbound", kind: "unbound" };
145596
- }
145597
- function listActions(builder) {
145598
- const custom = Object.keys(builder?.actions ?? {}).filter((name) => !isReservedAction(name)).sort();
145599
- return [...RESERVED_ACTIONS, ...custom].map((name) => resolveAction(builder, name));
145600
- }
145601
- function renderScoped(command, values) {
145602
- let rendered = command;
145603
- for (const [placeholder, items] of [
145604
- ["{files}", values.files],
145605
- ["{paths}", values.paths]
145606
- ]) {
145607
- if (!rendered.includes(placeholder))
145608
- continue;
145609
- if (items === undefined)
145610
- throw new Error(`renderScoped: ${placeholder} has no value`);
145611
- if (items.length === 0)
145612
- throw new Error(`renderScoped: ${placeholder} has an empty list`);
145613
- rendered = rendered.replaceAll(placeholder, () => items.map(shQuote2).join(" "));
145614
- }
145615
- if (rendered.includes("{workspace}")) {
145616
- const workspace = values.workspace;
145617
- if (workspace === undefined)
145618
- throw new Error("renderScoped: {workspace} has no value");
145619
- if (workspace.length === 0)
145620
- throw new Error("renderScoped: {workspace} is empty");
145621
- rendered = rendered.replaceAll("{workspace}", () => shQuote2(workspace));
145622
- }
145623
- return rendered;
145624
- }
145625
- var RESERVED_ACTIONS, BUN_PRESET, STACK_NEUTRAL_DEFAULTS;
145626
- var init_sdlc_actions = __esm(() => {
145627
- init_src();
145628
- RESERVED_ACTIONS = SDLC_ACTIONS;
145629
- BUN_PRESET = {
145630
- build: "bun run build",
145631
- dev: "bun run dev",
145632
- clean: "bun run clean",
145633
- format: "bun run format",
145634
- autofix: "bun run autofix",
145635
- lint: "bun run lint",
145636
- typecheck: "bun run typecheck",
145637
- test: "bun run test",
145638
- "test-full": "bun run test:full",
145639
- "test-heavy": "bun run test:heavy",
145640
- "rules-pre": "bun run test-pre-check",
145641
- "rules-post": "bun run test-post-check",
145642
- check: "bun run spur-check",
145643
- "check-full": "bun run spur-check:full",
145644
- "check-feature": "bun run spur-check-feature",
145645
- "lint-changed": "bunx biome check {files}",
145646
- "typecheck-changed": "cd {workspace} && bun run typecheck",
145647
- "test-changed": "cd {workspace} && bun test {paths}"
145648
- };
145649
- STACK_NEUTRAL_DEFAULTS = SDLC_STACK_NEUTRAL_ACTIONS;
145650
- });
145651
-
145652
145978
  // ../../packages/app/src/services/quality-gate.ts
145653
145979
  import { spawnSync as spawnSync6 } from "child_process";
145654
145980
  import { randomUUID as randomUUID12 } from "crypto";
@@ -145669,6 +145995,21 @@ import {
145669
145995
  } from "fs";
145670
145996
  import { homedir as homedir13, tmpdir as tmpdir5 } from "os";
145671
145997
  import { dirname as dirname38, join as join78 } from "path";
145998
+ function resolveGateTimeoutMs(env) {
145999
+ const raw = env[GATE_TIMEOUT_MS_ENV];
146000
+ if (raw === undefined || raw.trim() === "")
146001
+ return { ok: true, ms: GATE_TIMEOUT_MS_DEFAULT };
146002
+ const parsed = Number(raw);
146003
+ if (!Number.isFinite(parsed) || parsed <= 0)
146004
+ return { ok: false, error: `quality-gate: ${GATE_TIMEOUT_MS_ENV} must be a positive number, got "${raw}"` };
146005
+ return { ok: true, ms: Math.trunc(parsed) };
146006
+ }
146007
+ function gateTimeoutMsFor(env, options) {
146008
+ if (options.gateTimeoutMs !== undefined && options.gateTimeoutMs > 0)
146009
+ return options.gateTimeoutMs;
146010
+ const resolved = resolveGateTimeoutMs(env);
146011
+ return resolved.ok ? resolved.ms : GATE_TIMEOUT_MS_DEFAULT;
146012
+ }
145672
146013
  function isTransientLock(attemptOutput) {
145673
146014
  return LOCKED_PATTERN.test(attemptOutput);
145674
146015
  }
@@ -145867,7 +146208,7 @@ function lightScope(changedFiles, exists = existsSync23) {
145867
146208
  tests.add(candidate);
145868
146209
  matched = true;
145869
146210
  }
145870
- } else if (rest.startsWith("tests/") && TEST_FILE_PATTERN.test(rest)) {
146211
+ } else if (rest.startsWith("tests/") && TEST_FILE_PATTERN2.test(rest)) {
145871
146212
  tests.add(file);
145872
146213
  matched = true;
145873
146214
  }
@@ -145884,7 +146225,7 @@ function lightScope(changedFiles, exists = existsSync23) {
145884
146225
  const candidate = `${workspace}/tests/${rest.slice("src/".length).replace(/\.tsx?$/, (ext) => `.test${ext}`)}`;
145885
146226
  if (exists(candidate))
145886
146227
  tests.add(candidate);
145887
- } else if (rest.startsWith("tests/") && TEST_FILE_PATTERN.test(rest)) {
146228
+ } else if (rest.startsWith("tests/") && TEST_FILE_PATTERN2.test(rest)) {
145888
146229
  tests.add(file);
145889
146230
  }
145890
146231
  }
@@ -145931,11 +146272,13 @@ function planLightChecks(scope, hasTypecheck = workspaceHasTypecheck, builder) {
145931
146272
  }
145932
146273
  return plans;
145933
146274
  }
145934
- function gitChangedFiles(cwd) {
146275
+ function gitChangedFiles(cwd, timeoutMs) {
145935
146276
  const abs = (p) => cwd ? join78(cwd, p) : p;
145936
146277
  const changed = new Set;
145937
146278
  for (const cmd of ["git diff --name-only HEAD", "git ls-files --others --exclude-standard"]) {
145938
- const result = runShellCommand(cmd, cwd);
146279
+ const result = runShellCommand(cmd, cwd, undefined, timeoutMs);
146280
+ if (result.timedOut)
146281
+ return null;
145939
146282
  for (const line of result.output.split(`
145940
146283
  `)) {
145941
146284
  const file = line.trim();
@@ -145974,13 +146317,29 @@ function runLightGate(env, options = {}) {
145974
146317
  const logFile = join78(runDir, `${env.wbs}-light-gate.log`);
145975
146318
  const receiptFile = join78(runDir, `${env.wbs}-check-receipt.json`);
145976
146319
  writeFileSync14(abs(logFile), "");
146320
+ const timeoutMs = gateTimeoutMsFor(env, options);
146321
+ const changed = gitChangedFiles(cwd, timeoutMs);
146322
+ if (changed === null) {
146323
+ const line = `--- light gate BLOCKED: changed-path git read timed out after ${timeoutMs}ms
146324
+ `;
146325
+ appendFileSync7(abs(logFile), line);
146326
+ process.stderr.write(line);
146327
+ return {
146328
+ status: "FAIL",
146329
+ scope: { files: [], workspaces: [], tests: [] },
146330
+ checks: [],
146331
+ logFile,
146332
+ receiptFile,
146333
+ blocked: line.trim()
146334
+ };
146335
+ }
145977
146336
  const capture = (options.captureGitTree ?? captureGateGitTree)(cwd ?? process.cwd());
145978
146337
  const digest = capture.ok ? capture.tree : "";
145979
146338
  if (!capture.ok) {
145980
146339
  appendFileSync7(abs(logFile), `--- light receipt: no git tree identity \u2014 ${capture.error}
145981
146340
  `);
145982
146341
  }
145983
- const scope = lightScope(gitChangedFiles(cwd), (p) => existsSync23(abs(p)));
146342
+ const scope = lightScope(changed, (p) => existsSync23(abs(p)));
145984
146343
  const plans = planLightChecks(scope, (workspace) => workspaceHasTypecheck(abs(workspace)), options.builder);
145985
146344
  const reusable = new Map;
145986
146345
  let preserveFullReceipt = false;
@@ -146021,11 +146380,13 @@ function runLightGate(env, options = {}) {
146021
146380
  continue;
146022
146381
  }
146023
146382
  const startedAtMs = Date.now();
146024
- const result = runShellCommand(plan.cmd, cwd);
146383
+ const result = runShellCommand(plan.cmd, cwd, undefined, timeoutMs);
146025
146384
  const durationMs = Date.now() - startedAtMs;
146026
- const status = result.code === 0 ? "PASS" : "FAIL";
146027
- appendFileSync7(abs(logFile), `--- light ${plan.id}: ${status} (${durationMs}ms)
146028
- ${result.output}`);
146385
+ const status = result.code === 0 && !result.timedOut ? "PASS" : "FAIL";
146386
+ const header = result.timedOut ? `--- light ${plan.id}: ${status} (timeout after ${timeoutMs}ms, ${durationMs}ms elapsed)
146387
+ ` : `--- light ${plan.id}: ${status} (${durationMs}ms)
146388
+ `;
146389
+ appendFileSync7(abs(logFile), `${header}${result.output}`);
146029
146390
  checks.push({ id: plan.id, cmd: plan.cmd, status, durationMs, logPath: logFile });
146030
146391
  }
146031
146392
  const receipt = buildReceipt({
@@ -146063,7 +146424,7 @@ function gateSleep(ms) {
146063
146424
  }
146064
146425
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
146065
146426
  }
146066
- function runShellCommand(cmd, cwd, extraEnv) {
146427
+ function runShellCommand(cmd, cwd, extraEnv, timeoutMs) {
146067
146428
  const dir = mkdtempSync(join78(tmpdir5(), "spur-quality-gate-"));
146068
146429
  const path = join78(dir, "output");
146069
146430
  const fd = openSync8(path, "w");
@@ -146071,13 +146432,15 @@ function runShellCommand(cmd, cwd, extraEnv) {
146071
146432
  const result = spawnSync6("sh", ["-c", cmd], {
146072
146433
  cwd,
146073
146434
  stdio: ["ignore", fd, fd],
146074
- env: extraEnv === undefined ? undefined : { ...getEnvVars(), ...extraEnv }
146435
+ env: extraEnv === undefined ? undefined : { ...getEnvVars(), ...extraEnv },
146436
+ ...timeoutMs !== undefined ? { timeout: timeoutMs, killSignal: "SIGKILL" } : {}
146075
146437
  });
146076
- if (result.error !== undefined) {
146438
+ const timedOut = result.error?.code === "ETIMEDOUT" || result.signal === "SIGKILL";
146439
+ if (result.error !== undefined && !timedOut) {
146077
146440
  return { output: `sh -c failed: ${result.error.message}
146078
- `, code: 1 };
146441
+ `, code: 1, timedOut: false };
146079
146442
  }
146080
- return { output: readFileSync21(path, "utf8"), code: result.status ?? 1 };
146443
+ return { output: readFileSync21(path, "utf8"), code: result.status ?? 1, timedOut };
146081
146444
  } finally {
146082
146445
  closeSync8(fd);
146083
146446
  rmSync5(dir, { recursive: true, force: true });
@@ -146291,6 +146654,8 @@ function runQualityGate(mode, env, options = {}) {
146291
146654
  const attemptFile = rel("-test-fix-attempt");
146292
146655
  const abs = (p) => cwd ? join78(cwd, p) : p;
146293
146656
  const priorLog = mode === "recheck" && existsSync23(abs(logFile)) ? readFileSync21(abs(logFile), "utf8") : "";
146657
+ const timeoutMs = gateTimeoutMsFor(env, options);
146658
+ let timedOut;
146294
146659
  writeFileSync14(abs(logFile), "");
146295
146660
  if (mode === "run")
146296
146661
  writeFileSync14(abs(attemptFile), `0
@@ -146345,15 +146710,17 @@ function runQualityGate(mode, env, options = {}) {
146345
146710
  gateRc = 1;
146346
146711
  }
146347
146712
  if (mode === "recheck" && gateRc === 0 && (env.gateProbeCmd ?? "").length > 0) {
146348
- const probe = runShellCommand(env.gateProbeCmd ?? "", cwd);
146713
+ const probe = runShellCommand(env.gateProbeCmd ?? "", cwd, undefined, timeoutMs);
146349
146714
  writeFileSync14(abs(`${logFile}.probe`), probe.output);
146350
146715
  gateRc = probe.code;
146716
+ if (probe.timedOut)
146717
+ timedOut = { cmd: env.gateProbeCmd ?? "" };
146351
146718
  appendFileSync7(abs(logFile), probe.output);
146352
146719
  rmSync5(abs(`${logFile}.probe`), { force: true });
146353
146720
  }
146354
146721
  let gateRuntimeMs = 0;
146355
146722
  let queueWaitMs = 0;
146356
- if (gateRc === 0) {
146723
+ if (gateRc === 0 && timedOut === undefined) {
146357
146724
  const delayMs = retryDelayMs2(env);
146358
146725
  const lockLine = (line) => {
146359
146726
  process.stdout.write(line);
@@ -146367,12 +146734,16 @@ function runQualityGate(mode, env, options = {}) {
146367
146734
  try {
146368
146735
  for (gateAttempt = 1;gateAttempt <= MAX_GATE_ATTEMPTS; gateAttempt++) {
146369
146736
  const attemptLogPath = `${logFile}.attempt-${gateAttempt}`;
146370
- const attempt = runShellCommand(env.qualityGateCmd ?? "", cwd, lock.token.length > 0 ? { [GATE_LOCK_TOKEN_ENV]: lock.token } : undefined);
146737
+ const attempt = runShellCommand(env.qualityGateCmd ?? "", cwd, lock.token.length > 0 ? { [GATE_LOCK_TOKEN_ENV]: lock.token } : undefined, timeoutMs);
146371
146738
  writeFileSync14(abs(attemptLogPath), attempt.output);
146372
146739
  const locked = isTransientLock(attempt.output);
146373
146740
  appendFileSync7(abs(logFile), attempt.output);
146374
146741
  rmSync5(abs(attemptLogPath), { force: true });
146375
146742
  gateRc = attempt.code;
146743
+ if (attempt.timedOut) {
146744
+ timedOut = { cmd: env.qualityGateCmd ?? "" };
146745
+ break;
146746
+ }
146376
146747
  if (gateRc === 0 || !locked || gateAttempt >= MAX_GATE_ATTEMPTS)
146377
146748
  break;
146378
146749
  const line = retryMessage(gateAttempt);
@@ -146408,6 +146779,26 @@ function runQualityGate(mode, env, options = {}) {
146408
146779
  }
146409
146780
  }
146410
146781
  }
146782
+ if (timedOut !== undefined) {
146783
+ const header = `gate timeout after ${timeoutMs}ms: ${timedOut.cmd}
146784
+ `;
146785
+ const tail = tailLines(readFileSync21(abs(logFile), "utf8"), 40);
146786
+ writeFileSync14(abs(findingsFile), `${header}${tail}`);
146787
+ writeFileSync14(abs(statusFile), `BLOCKED
146788
+ `);
146789
+ appendFileSync7(abs(logFile), `git-tree: ${tree.ok ? tree.tree : "unbound"}
146790
+ `);
146791
+ process.stderr.write(`quality gate BLOCKED \u2014 ${header}`);
146792
+ return {
146793
+ status: "BLOCKED",
146794
+ attempts: gateAttempt,
146795
+ logFile,
146796
+ findingsFile,
146797
+ statusFile,
146798
+ attemptFile,
146799
+ receiptFile: undefined
146800
+ };
146801
+ }
146411
146802
  if (gateRc === 0) {
146412
146803
  const bytes = existsSync23(abs(logFile)) ? statSync15(abs(logFile)).size : 0;
146413
146804
  const attemptsLabel = gateAttempt > 0 ? String(gateAttempt) : "";
@@ -146468,12 +146859,19 @@ function qualityGateMain(argv, rawEnv = getEnvVars(), options = {}) {
146468
146859
  return 2;
146469
146860
  }
146470
146861
  const env = { ...rawEnv, wbs };
146862
+ const timeout = resolveGateTimeoutMs(env);
146863
+ if (!timeout.ok) {
146864
+ process.stderr.write(`${timeout.error}
146865
+ `);
146866
+ return 2;
146867
+ }
146868
+ const gateOptions = { ...options, gateTimeoutMs: timeout.ms };
146471
146869
  if (mode === "light") {
146472
- runLightGate(env, options);
146473
- return 0;
146870
+ const light = runLightGate(env, gateOptions);
146871
+ return light.blocked === undefined ? 0 : 2;
146474
146872
  }
146475
146873
  if (mode === "deferred") {
146476
- return runDeferredGate(env, options).status === "DEFERRED" ? 0 : 1;
146874
+ return runDeferredGate(env, gateOptions).status === "DEFERRED" ? 0 : 1;
146477
146875
  }
146478
146876
  if (mode === "status") {
146479
146877
  const runDir = join78(options.cwd ?? ".", ".spur", "run");
@@ -146496,7 +146894,10 @@ function qualityGateMain(argv, rawEnv = getEnvVars(), options = {}) {
146496
146894
  return 0;
146497
146895
  }
146498
146896
  try {
146499
- return runQualityGate(mode, env, options).status === "PASS" ? 0 : 1;
146897
+ const result = runQualityGate(mode, env, gateOptions);
146898
+ if (result.status === "PASS")
146899
+ return 0;
146900
+ return result.status === "BLOCKED" ? 2 : 1;
146500
146901
  } catch (error) {
146501
146902
  if (!(error instanceof GateLockTimeoutError))
146502
146903
  throw error;
@@ -146504,7 +146905,7 @@ function qualityGateMain(argv, rawEnv = getEnvVars(), options = {}) {
146504
146905
  return 2;
146505
146906
  }
146506
146907
  }
146507
- var MAX_GATE_ATTEMPTS = 5, MAX_FINDINGS = 20, RETRY_DELAY_MS_DEFAULT = 1e4, RETRY_DELAY_MS_ENV = "SPUR_QUALITY_GATE_RETRY_DELAY_MS", LOCKED_PATTERN, FINDINGS_PATTERN, COVERAGE_ROW_PATTERN, RECEIPT_SCHEMA_VERSION = "check-receipt/v1", TEST_FILE_PATTERN, GATE_LOCK_DIR_ENV = "SPUR_GATE_LOCK_DIR", GATE_LOCK_POLL_MS_ENV = "SPUR_GATE_LOCK_POLL_MS", GATE_LOCK_OFF_ENV = "SPUR_GATE_LOCK", GATE_LOCK_TOKEN_ENV = "SPUR_GATE_LOCK_TOKEN", GATE_LOCK_POLL_MS_DEFAULT = 5000, GATE_LOCK_TIMEOUT_MS_ENV = "SPUR_GATE_LOCK_TIMEOUT_MS", GATE_LOCK_TIMEOUT_MS_DEFAULT, GATE_LOCK_MKDIR_HOLD_MS_ENV = "SPUR_GATE_LOCK_TEST_MKDIR_HOLD_MS", GATE_LOCK_LOST_RACE_SLEEP_MS = 20, GATE_LOCK_LOST_RACE_LOG_AT = 250, GateLockTimeoutError, QUALITY_GATE_MODES, QUALITY_GATE_USAGE = "usage: spur task gate <wbs> <run|recheck|light|deferred|status> (env: qualityGateCmd, gateProbeCmd, runId)";
146908
+ var MAX_GATE_ATTEMPTS = 5, MAX_FINDINGS = 20, RETRY_DELAY_MS_DEFAULT = 1e4, RETRY_DELAY_MS_ENV = "SPUR_QUALITY_GATE_RETRY_DELAY_MS", GATE_TIMEOUT_MS_ENV = "gateTimeoutMs", GATE_TIMEOUT_MS_DEFAULT = 900000, LOCKED_PATTERN, FINDINGS_PATTERN, COVERAGE_ROW_PATTERN, RECEIPT_SCHEMA_VERSION = "check-receipt/v1", TEST_FILE_PATTERN2, GATE_LOCK_DIR_ENV = "SPUR_GATE_LOCK_DIR", GATE_LOCK_POLL_MS_ENV = "SPUR_GATE_LOCK_POLL_MS", GATE_LOCK_OFF_ENV = "SPUR_GATE_LOCK", GATE_LOCK_TOKEN_ENV = "SPUR_GATE_LOCK_TOKEN", GATE_LOCK_POLL_MS_DEFAULT = 5000, GATE_LOCK_TIMEOUT_MS_ENV = "SPUR_GATE_LOCK_TIMEOUT_MS", GATE_LOCK_TIMEOUT_MS_DEFAULT, GATE_LOCK_MKDIR_HOLD_MS_ENV = "SPUR_GATE_LOCK_TEST_MKDIR_HOLD_MS", GATE_LOCK_LOST_RACE_SLEEP_MS = 20, GATE_LOCK_LOST_RACE_LOG_AT = 250, GateLockTimeoutError, QUALITY_GATE_MODES, QUALITY_GATE_USAGE = "usage: spur task gate <wbs> <run|recheck|light|deferred|status> (env: qualityGateCmd, gateProbeCmd, runId)";
146508
146909
  var init_quality_gate = __esm(() => {
146509
146910
  init_dist();
146510
146911
  init_proof_input_fingerprint();
@@ -146512,7 +146913,7 @@ var init_quality_gate = __esm(() => {
146512
146913
  LOCKED_PATTERN = /SQLiteError: database is locked|SQLite database .*is busy|SQLITE_BUSY/;
146513
146914
  FINDINGS_PATTERN = /[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+/g;
146514
146915
  COVERAGE_ROW_PATTERN = /^\s*(\S+\.[A-Za-z]+)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*(\S*)/;
146515
- TEST_FILE_PATTERN = /\.test\.tsx?$/;
146916
+ TEST_FILE_PATTERN2 = /\.test\.tsx?$/;
146516
146917
  GATE_LOCK_TIMEOUT_MS_DEFAULT = 2 * 60 * 60 * 1000;
146517
146918
  GateLockTimeoutError = class GateLockTimeoutError extends Error {
146518
146919
  constructor(message) {
@@ -148404,6 +148805,17 @@ var require_src10 = __commonJS(function(exports, module) {
148404
148805
  });
148405
148806
 
148406
148807
  // ../../packages/app/src/workflow/step-profile.ts
148808
+ function rowKey(node, actionKind) {
148809
+ return `${node}\x00${actionKind}`;
148810
+ }
148811
+ function hostMsByRow(pauses) {
148812
+ const totals = new Map;
148813
+ for (const pause of pauses) {
148814
+ const key = rowKey(pause.node, pause.action ?? "agent.run");
148815
+ totals.set(key, (totals.get(key) ?? 0) + pause.hostDurationMs);
148816
+ }
148817
+ return totals;
148818
+ }
148407
148819
  function nearestRankP50(values) {
148408
148820
  if (values.length === 0)
148409
148821
  return null;
@@ -148527,11 +148939,27 @@ function buildRows(executions, windowSec) {
148527
148939
  }
148528
148940
  function buildStepProfile(input) {
148529
148941
  const executions = input.runs.flatMap((run) => extractExecutions(run.runId, run.events));
148942
+ const hostTotals = new Map;
148943
+ for (const run of input.runs) {
148944
+ for (const [key, ms] of hostMsByRow(run.hostPauses ?? [])) {
148945
+ hostTotals.set(key, (hostTotals.get(key) ?? 0) + ms);
148946
+ }
148947
+ }
148948
+ const rows = buildRows(executions, input.windowSec).map((row) => {
148949
+ const hostMs = hostTotals.get(rowKey(row.node, row.actionKind));
148950
+ return hostMs === undefined ? row : { ...row, hostMs };
148951
+ });
148952
+ const stalledRuns = nonDryRuns(input.paused ?? []).filter((entry) => entry.stalled === true).map((entry) => ({
148953
+ runId: entry.runId,
148954
+ stalledMs: entry.stalledMs ?? 0,
148955
+ resume: `spur workflow continue ${entry.runId}`
148956
+ }));
148530
148957
  return {
148531
148958
  workflow: input.workflow,
148532
148959
  windowSec: input.windowSec,
148533
148960
  sampledRuns: input.runs.length,
148534
- rows: buildRows(executions, input.windowSec)
148961
+ rows,
148962
+ ...stalledRuns.length > 0 ? { stalledRuns } : {}
148535
148963
  };
148536
148964
  }
148537
148965
  function nonDryRuns(entries) {
@@ -148555,16 +148983,22 @@ function formatStepProfileHuman(profile) {
148555
148983
  `p50=${seconds(row.durationMs.p50)}`.padEnd(12),
148556
148984
  `max=${seconds(row.durationMs.max)}`.padEnd(12),
148557
148985
  `idle=${seconds(row.idleGapMs.p50)}`.padEnd(12),
148986
+ ...row.hostMs === undefined ? [] : [`host=${seconds(row.hostMs)}`.padEnd(12)],
148558
148987
  `session=${row.session ?? "?"}`.padEnd(15),
148559
148988
  `cacheHit=${ratio(row.cacheHit.p50)} (${row.cacheHit.known}/${row.cacheHit.of})`.padEnd(20),
148560
148989
  `flags=${row.flags.length === 0 ? "-" : row.flags.join(",")}`
148561
148990
  ].join(" "));
148562
148991
  }
148992
+ for (const run of profile.stalledRuns ?? [])
148993
+ lines.push(`${run.runId}: ${stalledLabel(run.runId, run.stalledMs)}`);
148563
148994
  return `${lines.join(`
148564
148995
  `)}
148565
148996
  `;
148566
148997
  }
148567
148998
  var DEFAULT_LAST = 20, DEFAULT_WINDOW_SEC = 300;
148999
+ var init_step_profile = __esm(() => {
149000
+ init_host_pause();
149001
+ });
148568
149002
 
148569
149003
  // ../../packages/app/src/workflow/step-profile-cli.ts
148570
149004
  function positiveInt(raw, fallback) {
@@ -148611,6 +149045,12 @@ function runFailure(spurBin, result, what) {
148611
149045
  `).slice(-1)[0] ?? "";
148612
149046
  return failure2(`${what} failed (exit ${result.exitCode}${detail === "" ? "" : `: ${detail}`}) [${spurBin}]`);
148613
149047
  }
149048
+ function parseTraceEntries(stdout) {
149049
+ const parsed = JSON.parse(stdout);
149050
+ if (!Array.isArray(parsed.entries))
149051
+ throw new Error('no "entries" array');
149052
+ return parsed.entries;
149053
+ }
148614
149054
  function runStepProfileCli(argv, defaultSpurBin = "spur", defaultSpurArgv) {
148615
149055
  const spurArgv = argv.includes("--spur-bin") ? undefined : defaultSpurArgv;
148616
149056
  const args = parseStepProfileCliArgs(argv, defaultSpurBin);
@@ -148636,10 +149076,7 @@ ${STEP_PROFILE_USAGE}`);
148636
149076
  return runFailure(args.spurBin, list, `spur ${listArgs.join(" ")}`);
148637
149077
  let entries;
148638
149078
  try {
148639
- const parsed = JSON.parse(list.stdout);
148640
- if (!Array.isArray(parsed.entries))
148641
- throw new Error('no "entries" array');
148642
- entries = parsed.entries;
149079
+ entries = parseTraceEntries(list.stdout);
148643
149080
  } catch (err) {
148644
149081
  return failure2(`spur workflow trace output did not parse as JSON: ${String(err)}`);
148645
149082
  }
@@ -148653,12 +149090,27 @@ ${STEP_PROFILE_USAGE}`);
148653
149090
  const parsed = JSON.parse(timeline.stdout);
148654
149091
  if (!Array.isArray(parsed.events))
148655
149092
  throw new Error('no "events" array');
148656
- runs.push({ runId: entry.runId, events: parsed.events });
149093
+ runs.push({
149094
+ runId: entry.runId,
149095
+ events: parsed.events,
149096
+ ...entry.hostPauses !== undefined ? { hostPauses: entry.hostPauses } : {}
149097
+ });
148657
149098
  } catch (err) {
148658
149099
  return failure2(`spur workflow trace ${entry.runId} output did not parse as JSON: ${String(err)}`);
148659
149100
  }
148660
149101
  }
148661
- const profile = buildStepProfile({ workflow: args.workflow, windowSec: args.windowSec, runs });
149102
+ const pausedArgs = [...listArgs];
149103
+ pausedArgs[pausedArgs.indexOf("done")] = "paused";
149104
+ const pausedList = runSpurJson(args.spurBin, pausedArgs, spurArgv);
149105
+ if (!pausedList.ok)
149106
+ return runFailure(args.spurBin, pausedList, `spur ${pausedArgs.join(" ")}`);
149107
+ let paused;
149108
+ try {
149109
+ paused = parseTraceEntries(pausedList.stdout);
149110
+ } catch (err) {
149111
+ return failure2(`spur workflow trace output did not parse as JSON: ${String(err)}`);
149112
+ }
149113
+ const profile = buildStepProfile({ workflow: args.workflow, windowSec: args.windowSec, runs, paused });
148662
149114
  return {
148663
149115
  exitCode: 0,
148664
149116
  stdout: args.json ? `${JSON.stringify(profile, null, 2)}
@@ -148669,6 +149121,7 @@ ${STEP_PROFILE_USAGE}`);
148669
149121
  var STEP_PROFILE_USAGE;
148670
149122
  var init_step_profile_cli = __esm(() => {
148671
149123
  init_dist5();
149124
+ init_step_profile();
148672
149125
  STEP_PROFILE_USAGE = `usage: spur workflow profile <workflow> [--last <N>] [--window <sec>] [--json] [--spur-bin <cmd>]
148673
149126
 
148674
149127
  Read-only step profile from \`spur workflow trace\`: per node and action kind it reports runs,
@@ -156573,7 +157026,7 @@ var SHARED_OPTIONS = {
156573
157026
  "pi|claude|codex|gemini|opencode|antigravity|openclaw|omp|grok|agy|all"
156574
157027
  ],
156575
157028
  statusDoneFailed: ["--status <status>", "Filter by status: done, failed"],
156576
- statusDoneFailedRunning: ["--status <status>", "Filter by status: done, failed, running"],
157029
+ statusWorkflowRun: ["--status <status>", "Filter by status: done, failed, running, paused, interrupted"],
156577
157030
  untilAgent: ["--until <state>", "Lifecycle state to wait for (repeatable OR)"],
156578
157031
  untilMessage: ["--until <state>", "Wait target for --wait: injected|invoke-exit"],
156579
157032
  verboseWorkflow: ["--verbose", "Include transitions and correlation diagnostics in human progress"],
@@ -159764,7 +160217,7 @@ function registerTaskUpdateCommand(task, context) {
159764
160217
  // src/commands/task/verdict.ts
159765
160218
  init_src4();
159766
160219
  function registerTaskVerdictCommand(task, context) {
159767
- 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) => {
160220
+ 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("--replay", "Re-execute the test files the verdict rows cite; a failure downgrades PASS to PARTIAL").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (wbs, options) => {
159768
160221
  await Promise.resolve().then(() => init_src4());
159769
160222
  const answerPath = options.fromAnswer ?? `.spur/run/${wbs}-verify-answer.txt`;
159770
160223
  let answerText;
@@ -159783,6 +160236,12 @@ function registerTaskVerdictCommand(task, context) {
159783
160236
  featuresDir: context.fs.resolve(featuresDir)
159784
160237
  };
159785
160238
  const lintFindings = await lintVerifyAnswerForTask(wbs, answerText, corpusDeps);
160239
+ const lintPath = `.spur/run/${wbs}-verdict-lint.txt`;
160240
+ const clearLintFile = async () => {
160241
+ try {
160242
+ await context.fs.deleteFile(lintPath);
160243
+ } catch {}
160244
+ };
159786
160245
  if (lintFindings.length > 0) {
159787
160246
  if (options.json) {
159788
160247
  context.output.write(JSON.stringify({ wbs, lintFindings }, null, 2));
@@ -159791,9 +160250,14 @@ function registerTaskVerdictCommand(task, context) {
159791
160250
  for (const f of lintFindings)
159792
160251
  context.output.error(` line ${f.line}: ${f.message}`);
159793
160252
  }
160253
+ await context.fs.ensureDir(".spur/run");
160254
+ await context.fs.writeFile(lintPath, `${lintFindings.map((f) => `line ${f.line}: ${f.message}`).join(`
160255
+ `)}
160256
+ `);
159794
160257
  context.setExitCode(1);
159795
160258
  return;
159796
160259
  }
160260
+ await clearLintFile();
159797
160261
  const taskCheckPassed = true;
159798
160262
  const taskAc = await readTaskAcceptanceCriteria(wbs, corpusDeps);
159799
160263
  const result = deriveVerdict(answerText, taskCheckPassed, taskAc);
@@ -159803,7 +160267,19 @@ function registerTaskVerdictCommand(task, context) {
159803
160267
  context.setExitCode(1);
159804
160268
  return;
159805
160269
  }
159806
- const jsonOut = JSON.stringify({ wbs, ...result, source: "spur-task-verdict" }, null, 2);
160270
+ const replay = options.replay === true ? await replayCitedTests([...result.requirements, ...result.acceptanceCriteria ?? []], {
160271
+ cwd: context.cwd,
160272
+ builder: context.spurConfig?.builder
160273
+ }) : undefined;
160274
+ const downgraded = replay !== undefined && replay.findings.length > 0 && result.verdict === "PASS";
160275
+ if (downgraded)
160276
+ result.verdict = "PARTIAL";
160277
+ const jsonOut = JSON.stringify({
160278
+ wbs,
160279
+ ...result,
160280
+ ...replay !== undefined ? { replay } : {},
160281
+ source: "spur-task-verdict"
160282
+ }, null, 2);
159807
160283
  await context.fs.ensureDir(".spur/run");
159808
160284
  await context.fs.writeFile(`.spur/run/${wbs}-verdict.json`, `${jsonOut}
159809
160285
  `);
@@ -159811,6 +160287,12 @@ function registerTaskVerdictCommand(task, context) {
159811
160287
  context.output.write(jsonOut);
159812
160288
  } else {
159813
160289
  context.output.write(`Verdict: ${result.verdict} (${result.requirements.length} requirements, ${result.checks.length} checks)`);
160290
+ if (replay !== undefined) {
160291
+ const report = formatReplayReport(replay);
160292
+ context.output.write(report.summary);
160293
+ for (const line of report.findings)
160294
+ context.output.error(line);
160295
+ }
159814
160296
  }
159815
160297
  if (result.verdict !== "PASS") {
159816
160298
  context.setExitCode(1);
@@ -164711,7 +165193,7 @@ init_src2();
164711
165193
  var CLI_CONFIG = {
164712
165194
  binaryName: "spur",
164713
165195
  binaryLabel: "spur",
164714
- binaryVersion: "0.4.27",
165196
+ binaryVersion: "0.4.28",
164715
165197
  configDir: ".spur",
164716
165198
  configFile: ".spur/config.yaml",
164717
165199
  databaseFile: ".spur/spur.db"
@@ -172450,7 +172932,7 @@ init_dist5();
172450
172932
  import { homedir as homedir17 } from "os";
172451
172933
  import { basename as basename23, join as join95, relative as relative19, resolve as resolve37 } from "path";
172452
172934
  // ../server/package.json
172453
- var version4 = "0.4.27";
172935
+ var version4 = "0.4.28";
172454
172936
 
172455
172937
  // ../server/src/modules/health/index.ts
172456
172938
  var startedAt = Date.now();
@@ -178008,6 +178490,7 @@ init_src2();
178008
178490
  init_dist5();
178009
178491
  init_dist2();
178010
178492
  init_composition_baseline();
178493
+ init_host_pause();
178011
178494
  init_run_measurements();
178012
178495
  init_terminal_reason();
178013
178496
  init_run_measurements();
@@ -178045,9 +178528,13 @@ async function buildReportJson(db, workflows, opts = {}) {
178045
178528
  function renderStateLines(rows) {
178046
178529
  return rows.map((s) => {
178047
178530
  const wall = s.wallMsP50 === null || s.wallMsP90 === null ? "n/a" : `p50=${s.wallMsP50}ms p90=${s.wallMsP90}ms`;
178048
- return `${s.workflow}/${s.state}: visits=${s.visits} agent.run=${s.agentRunCount} wall=${wall} retries=${s.retries}`;
178531
+ const host = s.hostMs === undefined ? "" : ` host=${s.hostMs}ms`;
178532
+ return `${s.workflow}/${s.state}: visits=${s.visits} agent.run=${s.agentRunCount} wall=${wall}${host} retries=${s.retries}`;
178049
178533
  });
178050
178534
  }
178535
+ function renderStalledLines(rows) {
178536
+ return rows.map((r) => `${r.workflow}: ${stalledLabel(r.runId, r.stalledMs)}`);
178537
+ }
178051
178538
  function renderWorkflowLines(rows) {
178052
178539
  return rows.map((m) => {
178053
178540
  const excluded = m.dryRuns > 0 || m.nonTerminalRuns > 0 ? ` [excluded ${m.dryRuns} dry, ${m.nonTerminalRuns} non-terminal]` : "";
@@ -178263,6 +178750,11 @@ function registerWorkflowCostCommand(workflow, context, deps = {}) {
178263
178750
  context.output.write(line);
178264
178751
  }
178265
178752
  }
178753
+ if (options.json !== true) {
178754
+ for (const line of renderStalledLines(await readStalledRuns(db, targets))) {
178755
+ context.output.write(line);
178756
+ }
178757
+ }
178266
178758
  } finally {
178267
178759
  db.close();
178268
178760
  }
@@ -179240,6 +179732,7 @@ init_src4();
179240
179732
  import { closeSync as closeSync10, fstatSync as fstatSync5, openSync as openSync10, readFileSync as readFileSync32, readSync as readSync3 } from "fs";
179241
179733
  import { join as join107 } from "path";
179242
179734
  import { setTimeout as sleep7 } from "timers/promises";
179735
+ var TRACE_STATUSES = ["done", "failed", "running", "paused", "interrupted"];
179243
179736
  function formatTraceList2(result) {
179244
179737
  if (result.entries.length === 0) {
179245
179738
  return "No workflow runs.";
@@ -179268,6 +179761,9 @@ function formatTraceTimeline(result) {
179268
179761
  `Outcome: ${run.outcome} Events: ${events.length}`,
179269
179762
  ""
179270
179763
  ];
179764
+ if (run.stalled === true && run.stalledMs !== undefined) {
179765
+ lines.splice(4, 0, `Stalled: paused ${Math.round(run.stalledMs / 1000)}s \u2014 resume: spur workflow continue ${run.runId}`);
179766
+ }
179271
179767
  if (run.nextAction !== undefined)
179272
179768
  lines.splice(4, 0, `Next: ${run.nextAction.label} \u2014 ${run.nextAction.value}`);
179273
179769
  if (result.outputArtifact !== undefined) {
@@ -179503,7 +179999,7 @@ function formatTokens2(n) {
179503
179999
  return String(n);
179504
180000
  }
179505
180001
  function registerWorkflowTraceCommand(workflow, context) {
179506
- workflow.command("trace").description("Show persisted workflow run history.").argument("[run-id]", "Run ID for per-run timeline detail").option("--workflow <name>", "Filter by workflow name").option(...SHARED_OPTIONS.statusDoneFailedRunning).option(...SHARED_OPTIONS.since).option(...SHARED_OPTIONS.last, "20").option("--follow", "Replay a run timeline and poll persisted state until it becomes terminal").option(...SHARED_OPTIONS.pollWorkflow, "1000").option("--output", "With --follow: stream .spur/memory/runs/<RUNID>.md instead of the DB timeline").option(...SHARED_OPTIONS.timeout).option(...SHARED_OPTIONS.jsonSupported).option(...SHARED_OPTIONS.jsonEnvelope).action(async (runId, options) => {
180002
+ workflow.command("trace").description("Show persisted workflow run history.").argument("[run-id]", "Run ID for per-run timeline detail").option("--workflow <name>", "Filter by workflow name").option(...SHARED_OPTIONS.statusWorkflowRun).option(...SHARED_OPTIONS.since).option(...SHARED_OPTIONS.last, "20").option("--follow", "Replay a run timeline and poll persisted state until it becomes terminal").option(...SHARED_OPTIONS.pollWorkflow, "1000").option("--output", "With --follow: stream .spur/memory/runs/<RUNID>.md instead of the DB timeline").option(...SHARED_OPTIONS.timeout).option(...SHARED_OPTIONS.jsonSupported).option(...SHARED_OPTIONS.jsonEnvelope).action(async (runId, options) => {
179507
180003
  const svc = makeWorkflowService(context);
179508
180004
  const last = parseInt(options.last, 10);
179509
180005
  if (Number.isNaN(last) || last < 1) {
@@ -179548,8 +180044,8 @@ function registerWorkflowTraceCommand(workflow, context) {
179548
180044
  context.setExitCode(1);
179549
180045
  return;
179550
180046
  }
179551
- if (options.status !== undefined && !["done", "failed", "running"].includes(options.status)) {
179552
- writeJsonError(context.output, options, "--status must be one of: done, failed, running", "VALIDATION_FAILED");
180047
+ if (options.status !== undefined && !TRACE_STATUSES.includes(options.status)) {
180048
+ writeJsonError(context.output, options, `--status must be one of: ${TRACE_STATUSES.join(", ")}`, "VALIDATION_FAILED");
179553
180049
  context.setExitCode(1);
179554
180050
  return;
179555
180051
  }