@gobing-ai/spur 0.3.74 → 0.3.75

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 (68) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/workflows/docs-pipeline.yaml +26 -1
  3. package/config/workflows/feature-dev.yaml +143 -102
  4. package/config/workflows/idea-pipeline.yaml +58 -11
  5. package/config/workflows/task-pipeline.yaml +76 -40
  6. package/config/workflows/wrapup-pipeline.yaml +183 -111
  7. package/package.json +1 -1
  8. package/plugins/sp/agents/expert-spur.md +7 -4
  9. package/plugins/sp/commands/dev-idea.md +1 -1
  10. package/plugins/sp/plugin.json +1 -1
  11. package/plugins/sp/skills/spur-cli/references/tasks.md +3 -1
  12. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +23 -8
  13. package/plugins/sp/skills/spur-dev/references/dev-operations.md +1 -1
  14. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +4 -1
  15. package/spur.js +2010 -362
  16. package/web/_astro/{BoardApp.C60RJZRj.js → BoardApp.BOW1815F.js} +97 -97
  17. package/web/_astro/BoardApp.C4zimv1U.js +1 -0
  18. package/web/_astro/{TaskDetail.Guk8VuNu.js → TaskDetail.CX6C7dSJ.js} +1 -1
  19. package/web/_astro/{arc.CAZtlWJM.js → arc.CwSvH1ji.js} +1 -1
  20. package/web/_astro/{architectureDiagram-3BPJPVTR.9-XbF_Tz.js → architectureDiagram-3BPJPVTR.Dxne1RvP.js} +1 -1
  21. package/web/_astro/{blockDiagram-GPEHLZMM.DC4WLx3L.js → blockDiagram-GPEHLZMM.D9Fb14rC.js} +1 -1
  22. package/web/_astro/{c4Diagram-AAUBKEIU.JgjUQMgG.js → c4Diagram-AAUBKEIU.DmvWSFvv.js} +1 -1
  23. package/web/_astro/channel.CQHdDVp9.js +1 -0
  24. package/web/_astro/{chunk-2J33WTMH.D_JFiXa-.js → chunk-2J33WTMH.BCCktIrE.js} +1 -1
  25. package/web/_astro/{chunk-4BX2VUAB.Bk_RxoeT.js → chunk-4BX2VUAB.BzCk9q27.js} +1 -1
  26. package/web/_astro/{chunk-55IACEB6.a_Rj2VxL.js → chunk-55IACEB6.CZXVgTk4.js} +1 -1
  27. package/web/_astro/{chunk-727SXJPM.C2NoS1U6.js → chunk-727SXJPM.CJL8UIpX.js} +1 -1
  28. package/web/_astro/{chunk-AQP2D5EJ.D1Xn6CNa.js → chunk-AQP2D5EJ.Dz8MDEdw.js} +1 -1
  29. package/web/_astro/{chunk-FMBD7UC4.BhJrSBsX.js → chunk-FMBD7UC4.CkQeYYUw.js} +1 -1
  30. package/web/_astro/{chunk-ND2GUHAM.ByVGduYQ.js → chunk-ND2GUHAM.KiC1QgzH.js} +1 -1
  31. package/web/_astro/{chunk-QZHKN3VN.B47Paunq.js → chunk-QZHKN3VN.AJ08mw2e.js} +1 -1
  32. package/web/_astro/{classDiagram-4FO5ZUOK.BERMSD5C.js → classDiagram-4FO5ZUOK.DRWRDKzK.js} +1 -1
  33. package/web/_astro/{classDiagram-v2-Q7XG4LA2.BERMSD5C.js → classDiagram-v2-Q7XG4LA2.DRWRDKzK.js} +1 -1
  34. package/web/_astro/{cose-bilkent-S5V4N54A.O_rHGlhU.js → cose-bilkent-S5V4N54A.CavydfLP.js} +1 -1
  35. package/web/_astro/{cynefin-OW5HDTMX.qCW2GdNp.js → cynefin-OW5HDTMX.D_7o_a0B.js} +1 -1
  36. package/web/_astro/{dagre-BM42HDAG.C5Y4lH_u.js → dagre-BM42HDAG.LWk2dKgg.js} +1 -1
  37. package/web/_astro/{diagram-2AECGRRQ.DxZmRXxF.js → diagram-2AECGRRQ.82lbq6aB.js} +1 -1
  38. package/web/_astro/{diagram-5GNKFQAL.qSxZeYSK.js → diagram-5GNKFQAL.hyNL1pwY.js} +1 -1
  39. package/web/_astro/{diagram-KO2AKTUF.-7vl3bXs.js → diagram-KO2AKTUF.BaLqVf-b.js} +1 -1
  40. package/web/_astro/{diagram-LMA3HP47.BHVV-UH3.js → diagram-LMA3HP47.DQJxj0oz.js} +1 -1
  41. package/web/_astro/{diagram-OG6HWLK6.DHAuW9sK.js → diagram-OG6HWLK6.DhlL8cG2.js} +1 -1
  42. package/web/_astro/{erDiagram-TEJ5UH35.DMRLQwPN.js → erDiagram-TEJ5UH35.HoTnXwkF.js} +1 -1
  43. package/web/_astro/{flowDiagram-I6XJVG4X.B3EpPp_8.js → flowDiagram-I6XJVG4X.DkFKlZIZ.js} +1 -1
  44. package/web/_astro/{ganttDiagram-6RSMTGT7.BgUrExXM.js → ganttDiagram-6RSMTGT7.9qPfOTDb.js} +1 -1
  45. package/web/_astro/{gitGraphDiagram-PVQCEYII.CU-9yVN4.js → gitGraphDiagram-PVQCEYII.B4G18Dwc.js} +1 -1
  46. package/web/_astro/index.Dcr_8fiK.css +1 -0
  47. package/web/_astro/{infoDiagram-5YYISTIA.CvVTCRLe.js → infoDiagram-5YYISTIA.e3KJkXAM.js} +1 -1
  48. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BUzOfKPf.js → ishikawaDiagram-YF4QCWOH.CY3yddhD.js} +1 -1
  49. package/web/_astro/{journeyDiagram-JHISSGLW.BBTb7ziz.js → journeyDiagram-JHISSGLW.DaAKIO1t.js} +1 -1
  50. package/web/_astro/{kanban-definition-UN3LZRKU.CTQQr70i.js → kanban-definition-UN3LZRKU.DGkDddtc.js} +1 -1
  51. package/web/_astro/{linear.CLKlJPiS.js → linear.DnDPcpd1.js} +1 -1
  52. package/web/_astro/{mermaid.core.BA4wFhLP.js → mermaid.core.1uBmxa9t.js} +4 -4
  53. package/web/_astro/{mindmap-definition-RKZ34NQL.t1vG2l5e.js → mindmap-definition-RKZ34NQL.XFJVayxw.js} +1 -1
  54. package/web/_astro/{pieDiagram-4H26LBE5.C8zJhyTu.js → pieDiagram-4H26LBE5.BXm2OgHh.js} +1 -1
  55. package/web/_astro/{quadrantDiagram-W4KKPZXB.DqYYvb7T.js → quadrantDiagram-W4KKPZXB.DgB2p5fc.js} +1 -1
  56. package/web/_astro/{requirementDiagram-4Y6WPE33.lzfDZ9nI.js → requirementDiagram-4Y6WPE33.jBR39bnS.js} +1 -1
  57. package/web/_astro/{sankeyDiagram-5OEKKPKP.lm6N5ORM.js → sankeyDiagram-5OEKKPKP.N3u9OhE3.js} +1 -1
  58. package/web/_astro/{sequenceDiagram-3UESZ5HK.E6_IV4Dj.js → sequenceDiagram-3UESZ5HK.CZhqZXNL.js} +1 -1
  59. package/web/_astro/{stateDiagram-AJRCARHV.Bnv6Ok2p.js → stateDiagram-AJRCARHV.DlNu1VEa.js} +1 -1
  60. package/web/_astro/{stateDiagram-v2-BHNVJYJU.mJHH_Ng_.js → stateDiagram-v2-BHNVJYJU.BEqi8NQV.js} +1 -1
  61. package/web/_astro/{timeline-definition-PNZ67QCA.BhRgqSLa.js → timeline-definition-PNZ67QCA.BPcexclc.js} +1 -1
  62. package/web/_astro/{vennDiagram-CIIHVFJN.pjb5DMC9.js → vennDiagram-CIIHVFJN.BZalxKGQ.js} +1 -1
  63. package/web/_astro/{wardleyDiagram-YWT4CUSO.CW20KUng.js → wardleyDiagram-YWT4CUSO.BqOljod9.js} +1 -1
  64. package/web/_astro/{xychartDiagram-2RQKCTM6.vX9_yuMl.js → xychartDiagram-2RQKCTM6.tkO4ppN5.js} +1 -1
  65. package/web/index.html +2 -2
  66. package/web/_astro/BoardApp.ymfj1EHA.js +0 -1
  67. package/web/_astro/channel.Bxxe2Byc.js +0 -1
  68. package/web/_astro/index.BhvM3djt.css +0 -1
package/spur.js CHANGED
@@ -58609,6 +58609,132 @@ class SystemEventDao {
58609
58609
  throw error51;
58610
58610
  }
58611
58611
  }
58612
+ async eventSummary(spec) {
58613
+ const until = spec.until ?? new Date().toISOString();
58614
+ const since = spec.since;
58615
+ const sinceMs = Date.parse(since);
58616
+ const untilMs = Date.parse(until);
58617
+ if (Number.isNaN(sinceMs) || Number.isNaN(untilMs) || untilMs <= sinceMs) {
58618
+ return {
58619
+ window: { since, until },
58620
+ totalEvents: 0,
58621
+ errorEventCount: 0,
58622
+ warningEventCount: 0,
58623
+ eventVolumeBuckets: [],
58624
+ topEventTypes: [],
58625
+ recentErrors: []
58626
+ };
58627
+ }
58628
+ const width = untilMs - sinceMs;
58629
+ let bucketMs = spec.bucketMs && spec.bucketMs > 0 ? spec.bucketMs : width <= 3600000 ? 60000 : width <= 21600000 ? 300000 : width <= 86400000 ? 900000 : width <= 604800000 ? 3600000 : 86400000;
58630
+ while (Math.ceil((untilMs - sinceMs) / bucketMs) > 240) {
58631
+ bucketMs *= 2;
58632
+ }
58633
+ const bucketCount = Math.max(1, Math.ceil((untilMs - sinceMs) / bucketMs));
58634
+ const buckets = [];
58635
+ for (let i2 = 0;i2 < bucketCount; i2++) {
58636
+ buckets.push({
58637
+ timestamp: new Date(sinceMs + i2 * bucketMs).toISOString(),
58638
+ total: 0,
58639
+ byPrefix: {},
58640
+ bySeverity: { info: 0, warning: 0, error: 0, unknown: 0 }
58641
+ });
58642
+ }
58643
+ try {
58644
+ const totalsRow = await this.db.queryFirst(`SELECT
58645
+ COUNT(*) AS total,
58646
+ SUM(CASE WHEN sev = 'error' THEN 1 ELSE 0 END) AS error_count,
58647
+ SUM(CASE WHEN sev = 'warning' THEN 1 ELSE 0 END) AS warning_count
58648
+ FROM (
58649
+ SELECT COALESCE(json_extract(payload_json, '$.presentation.severity'), 'unknown') AS sev
58650
+ FROM system_events
58651
+ WHERE occurred_at >= ?1 AND occurred_at < ?2
58652
+ )`, since, until);
58653
+ const bucketRows = await this.db.queryAll(`SELECT
58654
+ CAST((strftime('%s', occurred_at) * 1000 - ?1) / ?2 AS INTEGER) AS bucket_idx,
58655
+ CASE WHEN instr(event_name, '.') > 0 THEN substr(event_name, 1, instr(event_name, '.') - 1) ELSE event_name END AS prefix,
58656
+ COALESCE(json_extract(payload_json, '$.presentation.severity'), 'unknown') AS sev,
58657
+ COUNT(*) AS cnt
58658
+ FROM system_events
58659
+ WHERE occurred_at >= ?3 AND occurred_at < ?4
58660
+ GROUP BY bucket_idx, prefix, sev`, sinceMs, bucketMs, since, until);
58661
+ for (const row of bucketRows) {
58662
+ const idx = Number(row.bucket_idx);
58663
+ if (idx >= 0 && idx < buckets.length) {
58664
+ const b = buckets[idx];
58665
+ if (b !== undefined) {
58666
+ const cnt = Number(row.cnt);
58667
+ b.total += cnt;
58668
+ b.byPrefix[row.prefix] = (b.byPrefix[row.prefix] ?? 0) + cnt;
58669
+ if (row.sev === "info" || row.sev === "warning" || row.sev === "error") {
58670
+ b.bySeverity[row.sev] += cnt;
58671
+ } else {
58672
+ b.bySeverity.unknown += cnt;
58673
+ }
58674
+ }
58675
+ }
58676
+ }
58677
+ const topRows = await this.db.queryAll(`SELECT
58678
+ event_name AS name,
58679
+ CASE WHEN instr(event_name, '.') > 0 THEN substr(event_name, 1, instr(event_name, '.') - 1) ELSE event_name END AS prefix,
58680
+ COUNT(*) AS count,
58681
+ MAX(occurred_at) AS latest_at
58682
+ FROM system_events
58683
+ WHERE occurred_at >= ?1 AND occurred_at < ?2
58684
+ GROUP BY event_name
58685
+ ORDER BY count DESC, event_name ASC
58686
+ LIMIT 10`, since, until);
58687
+ const recentRows = await this.db.queryAll(`SELECT
58688
+ id,
58689
+ event_name AS name,
58690
+ occurred_at,
58691
+ COALESCE(
58692
+ json_extract(payload_json, '$.presentation.summary'),
58693
+ json_extract(payload_json, '$.presentation.title'),
58694
+ json_extract(payload_json, '$.error'),
58695
+ event_name
58696
+ ) AS message,
58697
+ run_id AS ref_id
58698
+ FROM system_events
58699
+ WHERE occurred_at >= ?1 AND occurred_at < ?2
58700
+ AND COALESCE(json_extract(payload_json, '$.presentation.severity'), '') = 'error'
58701
+ ORDER BY occurred_at DESC, id DESC
58702
+ LIMIT 10`, since, until);
58703
+ return {
58704
+ window: { since, until },
58705
+ totalEvents: Number(totalsRow?.total ?? 0),
58706
+ errorEventCount: Number(totalsRow?.error_count ?? 0),
58707
+ warningEventCount: Number(totalsRow?.warning_count ?? 0),
58708
+ eventVolumeBuckets: buckets,
58709
+ topEventTypes: topRows.map((r) => ({
58710
+ name: r.name,
58711
+ prefix: r.prefix,
58712
+ count: Number(r.count),
58713
+ latestAt: r.latest_at
58714
+ })),
58715
+ recentErrors: recentRows.map((r) => ({
58716
+ id: r.id,
58717
+ name: r.name,
58718
+ occurredAt: r.occurred_at,
58719
+ message: String(r.message ?? r.name),
58720
+ ...r.ref_id ? { refId: r.ref_id } : {}
58721
+ }))
58722
+ };
58723
+ } catch (error51) {
58724
+ if (error51 instanceof Error && error51.message.includes("no such table: system_events")) {
58725
+ return {
58726
+ window: { since, until },
58727
+ totalEvents: 0,
58728
+ errorEventCount: 0,
58729
+ warningEventCount: 0,
58730
+ eventVolumeBuckets: buckets,
58731
+ topEventTypes: [],
58732
+ recentErrors: []
58733
+ };
58734
+ }
58735
+ throw error51;
58736
+ }
58737
+ }
58612
58738
  async deleteAll() {
58613
58739
  await this.db.run("DELETE FROM system_events");
58614
58740
  }
@@ -60660,14 +60786,19 @@ var init_run_dao = __esm(() => {
60660
60786
  stampMetadata(runId, metadata) {
60661
60787
  return this.mergeMetadata(runId, metadata);
60662
60788
  }
60663
- stampRunIdentity(runId, definitionDigest, workflowVersion) {
60789
+ stampRunIdentity(runId, definitionDigest, workflowVersion, source) {
60790
+ const sourceSet = source === undefined ? "" : `,
60791
+ '$.definitionSource.path', ?4,
60792
+ '$.definitionSource.layer', ?5,
60793
+ '$.definitionSource.workdir', ?6`;
60664
60794
  return this.adapter.run(`UPDATE runs
60665
60795
  SET metadata_json = json_set(
60666
60796
  COALESCE(NULLIF(metadata_json, ''), '{}'),
60667
60797
  '$.definitionDigest', ?1,
60668
- '$.workflowVersion', ?2
60798
+ '$.workflowVersion', ?2${sourceSet}
60669
60799
  )
60670
- WHERE id = ?3`, definitionDigest, workflowVersion, runId);
60800
+ WHERE id = ?3
60801
+ AND json_type(COALESCE(NULLIF(metadata_json, ''), '{}'), '$.definitionDigest') IS NULL`, definitionDigest, workflowVersion, runId, ...source !== undefined ? [source.path, source.layer, source.workdir] : []);
60671
60802
  }
60672
60803
  stampFailureReason(runId, reason) {
60673
60804
  return this.adapter.run(`UPDATE runs
@@ -68066,6 +68197,208 @@ async function enqueueCoalesced(db2, spec) {
68066
68197
  }
68067
68198
  return { status: "enqueued", jobId: fallback.id, payload: spec.payload };
68068
68199
  }
68200
+ async function queryQueueJobs(db2, spec = {}) {
68201
+ const limit = Math.min(500, Math.max(1, spec.limit ?? 100));
68202
+ const offset = Math.max(0, spec.offset ?? 0);
68203
+ let sinceMs;
68204
+ if (spec.since !== undefined) {
68205
+ sinceMs = typeof spec.since === "number" ? spec.since : Date.parse(spec.since);
68206
+ if (Number.isNaN(sinceMs))
68207
+ sinceMs = undefined;
68208
+ }
68209
+ try {
68210
+ const countConditions = ["1=1"];
68211
+ const countParams = [];
68212
+ if (sinceMs !== undefined) {
68213
+ countConditions.push("created_at >= ?");
68214
+ countParams.push(sinceMs);
68215
+ }
68216
+ const countRows = await db2.queryAll(`SELECT status, COUNT(*) AS cnt
68217
+ FROM queue_jobs
68218
+ WHERE ${countConditions.join(" AND ")}
68219
+ GROUP BY status`, ...countParams);
68220
+ const countsByStatus = {
68221
+ all: 0,
68222
+ pending: 0,
68223
+ processing: 0,
68224
+ completed: 0,
68225
+ failed: 0
68226
+ };
68227
+ for (const row of countRows) {
68228
+ const count3 = Number(row.cnt);
68229
+ countsByStatus.all += count3;
68230
+ if (row.status === "pending")
68231
+ countsByStatus.pending = count3;
68232
+ else if (row.status === "processing")
68233
+ countsByStatus.processing = count3;
68234
+ else if (row.status === "completed")
68235
+ countsByStatus.completed = count3;
68236
+ else if (row.status === "failed")
68237
+ countsByStatus.failed = count3;
68238
+ }
68239
+ const queryConditions = ["1=1"];
68240
+ const queryParams = [];
68241
+ if (spec.status !== undefined) {
68242
+ queryConditions.push("status = ?");
68243
+ queryParams.push(spec.status);
68244
+ }
68245
+ if (sinceMs !== undefined) {
68246
+ queryConditions.push("created_at >= ?");
68247
+ queryParams.push(sinceMs);
68248
+ }
68249
+ queryParams.push(limit + 1);
68250
+ queryParams.push(offset);
68251
+ const rows = await db2.queryAll(`SELECT id, type, payload, status, attempts, max_retries,
68252
+ created_at, updated_at, next_retry_at, last_error,
68253
+ processing_at, expires_at
68254
+ FROM queue_jobs
68255
+ WHERE ${queryConditions.join(" AND ")}
68256
+ ORDER BY created_at DESC, id DESC
68257
+ LIMIT ? OFFSET ?`, ...queryParams);
68258
+ const hasMore = rows.length > limit;
68259
+ const pageRows = hasMore ? rows.slice(0, limit) : rows;
68260
+ const jobs = pageRows.map((r) => {
68261
+ const queuedAt = new Date(r.created_at).toISOString();
68262
+ const startedAt = r.processing_at ? new Date(r.processing_at).toISOString() : null;
68263
+ const endedAt = r.status === "completed" || r.status === "failed" ? new Date(r.updated_at).toISOString() : null;
68264
+ const durationMs = startedAt && endedAt && r.processing_at != null ? r.updated_at - r.processing_at : null;
68265
+ let parsedPayload = null;
68266
+ if (r.payload) {
68267
+ try {
68268
+ const parsed = JSON.parse(r.payload);
68269
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
68270
+ parsedPayload = parsed;
68271
+ }
68272
+ } catch {
68273
+ parsedPayload = null;
68274
+ }
68275
+ }
68276
+ return {
68277
+ id: r.id,
68278
+ type: r.type,
68279
+ status: r.status,
68280
+ attempts: r.attempts,
68281
+ maxRetries: r.max_retries,
68282
+ queuedAt,
68283
+ startedAt,
68284
+ endedAt,
68285
+ durationMs,
68286
+ lastError: r.last_error ?? null,
68287
+ payload: parsedPayload
68288
+ };
68289
+ });
68290
+ const total = spec.status !== undefined ? countsByStatus[spec.status] ?? 0 : countsByStatus.all;
68291
+ return {
68292
+ jobs,
68293
+ total,
68294
+ hasMore,
68295
+ countsByStatus
68296
+ };
68297
+ } catch (error51) {
68298
+ if (error51 instanceof Error && error51.message.includes("no such table: queue_jobs")) {
68299
+ return {
68300
+ jobs: [],
68301
+ total: 0,
68302
+ hasMore: false,
68303
+ countsByStatus: { all: 0, pending: 0, processing: 0, completed: 0, failed: 0 }
68304
+ };
68305
+ }
68306
+ throw error51;
68307
+ }
68308
+ }
68309
+ async function queueJobKpis(db2, sinceMs, untilMs) {
68310
+ try {
68311
+ const countsRows = await db2.queryAll(`SELECT status, COUNT(*) AS cnt
68312
+ FROM queue_jobs
68313
+ WHERE created_at >= ?1 AND created_at < ?2
68314
+ GROUP BY status`, sinceMs, untilMs);
68315
+ let pending = 0;
68316
+ let processing = 0;
68317
+ let completed = 0;
68318
+ let failed = 0;
68319
+ for (const row of countsRows) {
68320
+ const count3 = Number(row.cnt);
68321
+ if (row.status === "pending")
68322
+ pending = count3;
68323
+ else if (row.status === "processing")
68324
+ processing = count3;
68325
+ else if (row.status === "completed")
68326
+ completed = count3;
68327
+ else if (row.status === "failed")
68328
+ failed = count3;
68329
+ }
68330
+ const activeJobs = pending + processing;
68331
+ const totalTerminal = completed + failed;
68332
+ const successRatePct = totalTerminal > 0 ? Math.round(completed / totalTerminal * 100) : 0;
68333
+ const errorRows = await db2.queryAll(`SELECT id, type, updated_at, last_error
68334
+ FROM queue_jobs
68335
+ WHERE status = 'failed' AND created_at >= ?1 AND created_at < ?2
68336
+ ORDER BY updated_at DESC, id DESC
68337
+ LIMIT 10`, sinceMs, untilMs);
68338
+ const recentJobErrors = errorRows.map((r) => ({
68339
+ id: r.id,
68340
+ name: r.type,
68341
+ occurredAt: new Date(r.updated_at).toISOString(),
68342
+ message: r.last_error ?? "Job failed with no error message"
68343
+ }));
68344
+ return {
68345
+ activeJobs,
68346
+ completedJobs: completed,
68347
+ failedJobs: failed,
68348
+ successRatePct,
68349
+ recentJobErrors
68350
+ };
68351
+ } catch (error51) {
68352
+ if (error51 instanceof Error && error51.message.includes("no such table: queue_jobs")) {
68353
+ return {
68354
+ activeJobs: 0,
68355
+ completedJobs: 0,
68356
+ failedJobs: 0,
68357
+ successRatePct: 0,
68358
+ recentJobErrors: []
68359
+ };
68360
+ }
68361
+ throw error51;
68362
+ }
68363
+ }
68364
+ async function queryScheduleLastExecution(db2, source, name) {
68365
+ try {
68366
+ if (source === "builtin") {
68367
+ const row2 = await db2.queryFirst(`SELECT status, updated_at FROM queue_jobs WHERE type = ? ORDER BY created_at DESC, id DESC LIMIT 1`, name);
68368
+ return row2 ?? null;
68369
+ }
68370
+ const row = await db2.queryFirst(`SELECT status, updated_at FROM queue_jobs
68371
+ WHERE type = 'scheduler.custom'
68372
+ AND json_extract(payload, '$.name') = ?
68373
+ ORDER BY created_at DESC, id DESC LIMIT 1`, name);
68374
+ return row ?? null;
68375
+ } catch (error51) {
68376
+ if (error51 instanceof Error && error51.message.includes("no such table: queue_jobs")) {
68377
+ return null;
68378
+ }
68379
+ throw error51;
68380
+ }
68381
+ }
68382
+ async function findActiveSchedulerCustomJob(db2, name) {
68383
+ const row = await db2.queryFirst(`SELECT id FROM queue_jobs
68384
+ WHERE type = 'scheduler.custom' AND status IN ('pending', 'processing')
68385
+ AND json_extract(payload, '$.name') = ?
68386
+ LIMIT 1`, name);
68387
+ return row ?? undefined;
68388
+ }
68389
+ async function failOrphanedProcessingJobs(db2, now) {
68390
+ const orphan = await db2.queryFirst(`SELECT count(*) AS cnt FROM queue_jobs WHERE status = 'processing'`);
68391
+ const count3 = orphan?.cnt ?? 0;
68392
+ if (count3 > 0) {
68393
+ await db2.run(`UPDATE queue_jobs
68394
+ SET status = 'failed',
68395
+ last_error = 'Process terminated: server restarted while job was in flight',
68396
+ processing_at = NULL,
68397
+ updated_at = ?
68398
+ WHERE status = 'processing'`, now);
68399
+ }
68400
+ return count3;
68401
+ }
68069
68402
  var SQLITE_BUSY_TIMEOUT_MS = 30000;
68070
68403
  var init_db2 = __esm(() => {
68071
68404
  init_migrations();
@@ -70009,6 +70342,9 @@ function buildTaskSkeleton(options) {
70009
70342
  `).trimEnd()}
70010
70343
  `;
70011
70344
  }
70345
+ function serializeTaskFrontmatter(data) {
70346
+ return $stringify(data, { lineWidth: 0 });
70347
+ }
70012
70348
  function extractTemplateBodies(templateMarkdown) {
70013
70349
  const doc2 = MarkdownDocument.parse(templateMarkdown, "task");
70014
70350
  const bodies = {};
@@ -70037,6 +70373,7 @@ function renderTaskTemplate(templateContent, vars) {
70037
70373
  }
70038
70374
  var sectionNameSchema, matrixEntrySchema, matrixStatusSchema, sectionMatrixSchema, SECTION_GUIDANCE, CANONICAL_INDEX;
70039
70375
  var init_task_skeleton = __esm(() => {
70376
+ init_dist2();
70040
70377
  init_zod();
70041
70378
  init_markdown_document();
70042
70379
  sectionNameSchema = exports_external.enum(TASK_CANONICAL_SECTIONS);
@@ -70351,6 +70688,7 @@ __export(exports_src, {
70351
70688
  sessionToolDurations: () => sessionToolDurations,
70352
70689
  sessionTimeline: () => sessionTimeline,
70353
70690
  sessionSpans: () => sessionSpans,
70691
+ serializeTaskFrontmatter: () => serializeTaskFrontmatter,
70354
70692
  serializeStablePrefix: () => serializeStablePrefix,
70355
70693
  serializeLayer: () => serializeLayer,
70356
70694
  serializeEnvelope: () => serializeEnvelope,
@@ -70378,6 +70716,9 @@ __export(exports_src, {
70378
70716
  readRollupWatermarks: () => readRollupWatermarks,
70379
70717
  readRecordedImporterSchemaVersion: () => readRecordedImporterSchemaVersion,
70380
70718
  readLockMetadata: () => readLockMetadata,
70719
+ queueJobKpis: () => queueJobKpis,
70720
+ queryScheduleLastExecution: () => queryScheduleLastExecution,
70721
+ queryQueueJobs: () => queryQueueJobs,
70381
70722
  projectSnapshotSchema: () => projectSnapshotSchema,
70382
70723
  passAllResolver: () => passAllResolver,
70383
70724
  parseTodoItems: () => parseTodoItems,
@@ -70448,9 +70789,11 @@ __export(exports_src, {
70448
70789
  formatRatio: () => formatRatio,
70449
70790
  foldTotals: () => foldTotals,
70450
70791
  findPendingQueueJob: () => findPendingQueueJob,
70792
+ findActiveSchedulerCustomJob: () => findActiveSchedulerCustomJob,
70451
70793
  featureStatusIcon: () => featureStatusIcon,
70452
70794
  featureSnapshotSchema: () => featureSnapshotSchema,
70453
70795
  featureFrontmatterSchema: () => featureFrontmatterSchema,
70796
+ failOrphanedProcessingJobs: () => failOrphanedProcessingJobs,
70454
70797
  extractTemplateBodies: () => extractTemplateBodies,
70455
70798
  extractStablePrefix: () => extractStablePrefix,
70456
70799
  extractPhases: () => extractPhases,
@@ -74601,7 +74944,7 @@ class PlanningCheckService {
74601
74944
  }
74602
74945
  findings.push(...structuralFindings(raw, this.docKind));
74603
74946
  }
74604
- summarizeWithStatus(status, findings, strict, overrides, accepted, id) {
74947
+ summarizeWithStatus(status, findings, strict, overrides, accepted, id, requiredList) {
74605
74948
  const effectiveFindings = [];
74606
74949
  const notes = [];
74607
74950
  for (const f of findings) {
@@ -74643,12 +74986,24 @@ class PlanningCheckService {
74643
74986
  break;
74644
74987
  }
74645
74988
  }
74646
- const requiredSections = [];
74647
74989
  const missingSections = [];
74648
- for (const f of effectiveFindings) {
74649
- if (f.layer === "L2" && f.section && f.message.startsWith("Missing required")) {
74650
- requiredSections.push(f.section);
74651
- missingSections.push(f.section);
74990
+ let requiredSections;
74991
+ if (requiredList !== undefined) {
74992
+ requiredSections = [...requiredList];
74993
+ const seen = new Set;
74994
+ for (const f of effectiveFindings) {
74995
+ if (f.layer === "L2" && f.section && f.message.startsWith("Missing required") && !seen.has(f.section)) {
74996
+ seen.add(f.section);
74997
+ missingSections.push(f.section);
74998
+ }
74999
+ }
75000
+ } else {
75001
+ requiredSections = [];
75002
+ for (const f of effectiveFindings) {
75003
+ if (f.layer === "L2" && f.section && f.message.startsWith("Missing required")) {
75004
+ requiredSections.push(f.section);
75005
+ missingSections.push(f.section);
75006
+ }
74652
75007
  }
74653
75008
  }
74654
75009
  return { status, findings: effectiveFindings, requiredSections, missingSections, pass: !hasError, notes };
@@ -76069,13 +76424,29 @@ var init_task_check = __esm(() => {
76069
76424
  }
76070
76425
  return {
76071
76426
  wbs,
76072
- ...this.summarizeWithStatus(effectiveStatus, findings, strict, options?.severityOverrides, options?.accepted, wbs),
76427
+ ...this.summarizeWithStatus(effectiveStatus, findings, strict, options?.severityOverrides, options?.accepted, wbs, entry?.required),
76073
76428
  repairs
76074
76429
  };
76075
76430
  }
76431
+ checkContentPolicy(raw, wbs, options) {
76432
+ const findings = [];
76433
+ const doc2 = this.runL1(raw, wbs, findings);
76434
+ if (doc2 === null) {
76435
+ return { doc: null, findings };
76436
+ }
76437
+ const fm = doc2.frontmatterData ?? {};
76438
+ const effectiveStatus = options?.asStatus ?? fm.status ?? "backlog";
76439
+ const variant = fm.template ?? DEFAULT_TASK_VARIANT;
76440
+ const entry = this.resolveMatrixEntry(variant, effectiveStatus);
76441
+ this.runL2(doc2, entry, findings, raw);
76442
+ this.runL3(doc2, entry, effectiveStatus, findings);
76443
+ const { findings: effectiveFindings } = this.summarizeWithStatus(effectiveStatus, findings, options?.strict, options?.severityOverrides, undefined, wbs, entry?.required);
76444
+ return { doc: doc2, findings: effectiveFindings, entry };
76445
+ }
76076
76446
  runL3(doc2, entry, status, findings) {
76447
+ const requiredAtStatus = new Set(entry?.required ?? []);
76077
76448
  const reqBodyRaw = doc2.getSection("Requirements");
76078
- if (reqBodyRaw !== null && isPlaceholderBody(reqBodyRaw)) {
76449
+ if (requiredAtStatus.has("Requirements") && reqBodyRaw !== null && isPlaceholderBody(reqBodyRaw)) {
76079
76450
  findings.push({
76080
76451
  layer: "L3",
76081
76452
  code: FINDING_CODES.L3_REQUIREMENTS_EMPTY,
@@ -76085,7 +76456,7 @@ var init_task_check = __esm(() => {
76085
76456
  });
76086
76457
  }
76087
76458
  const acBodyRaw = doc2.getSection("Acceptance Criteria");
76088
- if (acBodyRaw !== null && isPlaceholderBody(stripAcFence(acBodyRaw))) {
76459
+ if (requiredAtStatus.has("Acceptance Criteria") && acBodyRaw !== null && isPlaceholderBody(stripAcFence(acBodyRaw))) {
76089
76460
  findings.push({
76090
76461
  layer: "L3",
76091
76462
  code: FINDING_CODES.L3_AC_EMPTY,
@@ -76201,9 +76572,8 @@ var init_task_check = __esm(() => {
76201
76572
  });
76202
76573
  }
76203
76574
  }
76204
- const requiredSections = new Set(entry?.required ?? []);
76205
76575
  for (const sectionName of ["Testing", "Solution"]) {
76206
- if (!requiredSections.has(sectionName))
76576
+ if (!requiredAtStatus.has(sectionName))
76207
76577
  continue;
76208
76578
  const body = doc2.getSection(sectionName);
76209
76579
  if (body === null || !isPlaceholderBody(body))
@@ -83503,7 +83873,7 @@ class HistoryService {
83503
83873
  ...opts.file !== undefined && opts.file.length > 0 ? { files: [opts.file] } : opts.root !== undefined && opts.root.length > 0 ? { roots: [opts.root] } : { roots: discovery?.roots ?? [] },
83504
83874
  dryRun
83505
83875
  });
83506
- if (!dryRun) {
83876
+ if (!dryRun && result.importedRecords > 0) {
83507
83877
  for (const runRoot of discovery?.runRoots ?? []) {
83508
83878
  const sourceFilePrefix = runRoot.root.endsWith(sep) ? runRoot.root : `${runRoot.root}${sep}`;
83509
83879
  await dao2.observeImportedSessions({
@@ -86232,25 +86602,36 @@ function validateSchedulerCustomJobPayload(raw) {
86232
86602
  }
86233
86603
  async function handleSchedulerCustomJob(deps, job) {
86234
86604
  const payload = validateSchedulerCustomJobPayload(job.payload);
86235
- const result = await deps.executor.run({
86236
- command: "/bin/sh",
86237
- args: ["-c", payload.command],
86238
- cwd: deps.cwd,
86239
- timeout: deps.timeoutMs ?? SCHEDULER_CUSTOM_TIMEOUT_MS,
86240
- maxOutput: SCHEDULER_CUSTOM_MAX_OUTPUT,
86241
- forceBuffered: true,
86242
- rejectOnError: false
86243
- });
86244
- const detail = outputTail2(result.stderr) || outputTail2(result.stdout);
86245
- if (result.exitCode === null) {
86246
- const signalDetail = result.signal === undefined ? "" : ` (${result.signal})`;
86247
- throw new Error(`scheduler job "${payload.name}" terminated before a normal exit${signalDetail}${detail}`);
86605
+ if (activeJobs.has(payload.name)) {
86606
+ throw new Error(`scheduler job "${payload.name}" is already running in this process; skipping duplicate execution`);
86248
86607
  }
86249
- if (result.exitCode !== 0) {
86250
- throw new Error(`scheduler job "${payload.name}" exited ${result.exitCode}${detail}`);
86608
+ activeJobs.add(payload.name);
86609
+ try {
86610
+ const result = await deps.executor.run({
86611
+ command: "/bin/sh",
86612
+ args: ["-c", payload.command],
86613
+ cwd: deps.cwd,
86614
+ timeout: deps.timeoutMs ?? SCHEDULER_CUSTOM_TIMEOUT_MS,
86615
+ maxOutput: SCHEDULER_CUSTOM_MAX_OUTPUT,
86616
+ forceBuffered: true,
86617
+ rejectOnError: false
86618
+ });
86619
+ const detail = outputTail2(result.stderr) || outputTail2(result.stdout);
86620
+ if (result.exitCode === null) {
86621
+ const signalDetail = result.signal === undefined ? "" : ` (${result.signal})`;
86622
+ throw new Error(`scheduler job "${payload.name}" terminated before a normal exit${signalDetail}${detail}`);
86623
+ }
86624
+ if (result.exitCode !== 0) {
86625
+ throw new Error(`scheduler job "${payload.name}" exited ${result.exitCode}${detail}`);
86626
+ }
86627
+ } finally {
86628
+ activeJobs.delete(payload.name);
86251
86629
  }
86252
86630
  }
86253
- var SCHEDULER_CUSTOM_JOB = "scheduler.custom", SCHEDULER_CUSTOM_TIMEOUT_MS = 3600000, SCHEDULER_CUSTOM_MAX_OUTPUT = 1e6;
86631
+ var SCHEDULER_CUSTOM_JOB = "scheduler.custom", SCHEDULER_CUSTOM_TIMEOUT_MS = 3600000, SCHEDULER_CUSTOM_MAX_OUTPUT = 1e6, activeJobs;
86632
+ var init_scheduler_custom_job_service = __esm(() => {
86633
+ activeJobs = new Set;
86634
+ });
86254
86635
 
86255
86636
  // ../../packages/app/src/services/supervisor-service.ts
86256
86637
  function defaultWrapperArgv(agentId) {
@@ -86716,6 +87097,248 @@ var init_system_event_follow = __esm(() => {
86716
87097
  init_src2();
86717
87098
  });
86718
87099
 
87100
+ // ../../packages/app/src/services/task-readiness.ts
87101
+ import { createHash as createHash8 } from "crypto";
87102
+ function readyRefineCommand(wbs) {
87103
+ return `/sp:dev-refine ${wbs} --auto --depth ready`;
87104
+ }
87105
+ function readyRefineAllCommand(featureId2) {
87106
+ return `/sp:dev-refineall --feature ${featureId2} --auto --depth ready`;
87107
+ }
87108
+ async function prepareCreatedTaskReady(opts) {
87109
+ const { wbs, tasks, agents, checkTask } = opts;
87110
+ const recoveryCommand = readyRefineCommand(wbs);
87111
+ const before = await tasks.show(wbs).catch((err) => {
87112
+ throw new TaskPreparationError({
87113
+ stage: "post-check",
87114
+ message: `saved task ${wbs} is unreadable before preparation: ${err instanceof Error ? err.message : String(err)}`,
87115
+ wbs,
87116
+ recoveryCommand
87117
+ });
87118
+ });
87119
+ const flags = {
87120
+ mode: "text",
87121
+ timeout: String(opts.timeoutMs ?? DEFAULT_READY_PREPARE_TIMEOUT_MS)
87122
+ };
87123
+ if (opts.agentSelector !== undefined && opts.agentSelector !== "")
87124
+ flags.agent = opts.agentSelector;
87125
+ if (opts.cwd !== undefined && opts.cwd !== "")
87126
+ flags.cwd = opts.cwd;
87127
+ const traced = await agents.runTraced(readyRefineCommand(wbs), flags);
87128
+ if (traced.exitCode === 2) {
87129
+ throw new TaskPreparationError({
87130
+ stage: "agent-dispatch",
87131
+ message: `ready preparation could not dispatch an executor for ${wbs}: ${traced.message ?? "agent resolution failed"}`,
87132
+ wbs,
87133
+ filePath: before.filePath,
87134
+ recoveryCommand
87135
+ });
87136
+ }
87137
+ if (traced.exitCode !== 0) {
87138
+ const why = traced.signal !== undefined ? `terminated by signal ${traced.signal}` : `executor exited ${traced.exitCode}`;
87139
+ throw new TaskPreparationError({
87140
+ stage: "agent-run",
87141
+ message: `ready preparation failed for ${wbs}: ${why}. Partial section edits are preserved on the task.`,
87142
+ wbs,
87143
+ filePath: before.filePath,
87144
+ recoveryCommand
87145
+ });
87146
+ }
87147
+ const after = await tasks.show(wbs).catch((err) => {
87148
+ throw new TaskPreparationError({
87149
+ stage: "post-check",
87150
+ message: `saved task ${wbs} is unreadable after preparation: ${err instanceof Error ? err.message : String(err)}`,
87151
+ wbs,
87152
+ filePath: before.filePath,
87153
+ recoveryCommand
87154
+ });
87155
+ });
87156
+ const check2 = await checkTask(after.filePath, wbs).catch((err) => {
87157
+ throw new TaskPreparationError({
87158
+ stage: "post-check",
87159
+ message: `post-check for ${wbs} could not run: ${err instanceof Error ? err.message : String(err)}`,
87160
+ wbs,
87161
+ filePath: after.filePath,
87162
+ recoveryCommand
87163
+ });
87164
+ });
87165
+ if (!check2.pass) {
87166
+ const summary = check2.findings.filter((f) => f.severity === "error").map((f) => `${f.layer}:${f.code} ${f.section !== undefined ? `(${f.section}) ` : ""}${f.message}`).slice(0, 5).join("; ");
87167
+ throw new TaskPreparationError({
87168
+ stage: "post-check",
87169
+ message: `ready post-check failed for ${wbs}: ${summary || "check did not pass"}`,
87170
+ wbs,
87171
+ filePath: after.filePath,
87172
+ findings: check2.findings,
87173
+ recoveryCommand
87174
+ });
87175
+ }
87176
+ if (after.status !== "todo") {
87177
+ try {
87178
+ await tasks.updateStatus(wbs, "todo", opts.actor ?? "ready-preparation");
87179
+ } catch (err) {
87180
+ throw new TaskPreparationError({
87181
+ stage: "promotion",
87182
+ message: `ready preparation could not promote ${wbs} to todo: ${err instanceof Error ? err.message : String(err)}`,
87183
+ wbs,
87184
+ filePath: after.filePath,
87185
+ recoveryCommand
87186
+ });
87187
+ }
87188
+ }
87189
+ return READY_DONE;
87190
+ }
87191
+ function extractBatchArray(answer) {
87192
+ const attempt = (text4) => {
87193
+ try {
87194
+ return JSON.parse(text4);
87195
+ } catch {
87196
+ return;
87197
+ }
87198
+ };
87199
+ const direct = attempt(answer);
87200
+ if (direct !== undefined)
87201
+ return direct;
87202
+ const start = answer.indexOf("[");
87203
+ const end = answer.lastIndexOf("]");
87204
+ if (start === -1 || end <= start) {
87205
+ throw new TaskPreparationError({
87206
+ stage: "invalid-output",
87207
+ message: "prepared batch output carried no JSON array"
87208
+ });
87209
+ }
87210
+ const sliced = attempt(answer.slice(start, end + 1));
87211
+ if (sliced === undefined) {
87212
+ throw new TaskPreparationError({
87213
+ stage: "invalid-output",
87214
+ message: "prepared batch output is not valid JSON"
87215
+ });
87216
+ }
87217
+ return sliced;
87218
+ }
87219
+ async function prepareBatchTaskReady(opts) {
87220
+ const { agents } = opts;
87221
+ const flags = {
87222
+ mode: "text",
87223
+ timeout: String(opts.timeoutMs ?? DEFAULT_READY_PREPARE_TIMEOUT_MS)
87224
+ };
87225
+ if (opts.agentSelector !== undefined && opts.agentSelector !== "")
87226
+ flags.agent = opts.agentSelector;
87227
+ if (opts.cwd !== undefined && opts.cwd !== "")
87228
+ flags.cwd = opts.cwd;
87229
+ let recoveryCommand;
87230
+ try {
87231
+ const supplied = JSON.parse(opts.batchSource);
87232
+ if (Array.isArray(supplied)) {
87233
+ for (const item of supplied) {
87234
+ if (typeof item === "object" && item !== null && "feature_id" in item) {
87235
+ const featureId2 = item.feature_id;
87236
+ if (typeof featureId2 === "string" && featureId2 !== "") {
87237
+ recoveryCommand = readyRefineAllCommand(featureId2);
87238
+ break;
87239
+ }
87240
+ }
87241
+ }
87242
+ }
87243
+ } catch {}
87244
+ const prompt = "Prepare this task batch to the ready checklist (requirements, design, plan, ac, decisions, dependencies, " + "premises). Read the batch file, assess and synthesize every candidate item in input order, preserving " + "names, feature_id, parent_wbs and authored constraints, and fill Requirements, Design, Plan and Acceptance " + "Criteria substantively (never placeholders). Never author Solution, Testing or Review sections. " + `Print ONLY the prepared JSON array \u2014 no prose, no code fence.
87245
+ ` + `Batch file: ${opts.batchPath}
87246
+ ` + `Batch contents:
87247
+ ${opts.batchSource}`;
87248
+ const captured = await agents.runCapture(prompt, flags);
87249
+ if (captured.exitCode === 2) {
87250
+ throw new TaskPreparationError({
87251
+ stage: "agent-dispatch",
87252
+ message: `batch preparation could not dispatch an executor: ${captured.stderr || "agent resolution failed"}`,
87253
+ recoveryCommand
87254
+ });
87255
+ }
87256
+ if (captured.exitCode !== 0) {
87257
+ const why = captured.signal !== undefined ? `terminated by signal ${captured.signal}` : `executor exited ${captured.exitCode}`;
87258
+ throw new TaskPreparationError({
87259
+ stage: "agent-run",
87260
+ message: `batch preparation failed: ${why}`,
87261
+ recoveryCommand
87262
+ });
87263
+ }
87264
+ const parsed = extractBatchArray(captured.answer);
87265
+ const validated = taskBatchSchema.safeParse(parsed);
87266
+ if (!validated.success) {
87267
+ const issues = validated.error.issues.map((i2) => ` [${i2.path.join(".")}] ${i2.message}`).join(`
87268
+ `);
87269
+ throw new TaskPreparationError({
87270
+ stage: "validation",
87271
+ message: `prepared batch failed schema validation:
87272
+ ${issues}`,
87273
+ recoveryCommand
87274
+ });
87275
+ }
87276
+ return { items: validated.data };
87277
+ }
87278
+ function computePlanningDigest(raw) {
87279
+ const doc2 = MarkdownDocument.parse(raw, "task");
87280
+ const fm = doc2.frontmatterData ?? {};
87281
+ const payload = {
87282
+ sections: PLANNING_DIGEST_SECTIONS.map((name) => {
87283
+ const body = doc2.getSection(name);
87284
+ return { name, body: body === null ? null : body.trim() };
87285
+ }),
87286
+ featureId: typeof fm.feature_id === "string" ? fm.feature_id : null,
87287
+ template: typeof fm.template === "string" ? fm.template : null,
87288
+ dependencies: Array.isArray(fm.dependencies) ? [...fm.dependencies].map(String).sort() : null
87289
+ };
87290
+ return createHash8("sha256").update(JSON.stringify(payload)).digest("hex");
87291
+ }
87292
+ function verifyReadyChecks(checks4) {
87293
+ if (checks4 === undefined || checks4.length === 0) {
87294
+ return { ok: false, reason: "no ready-checklist evidence" };
87295
+ }
87296
+ for (const id of READY_CHECKLIST_IDS) {
87297
+ const row = checks4.find((c3) => c3.id === id);
87298
+ if (row === undefined)
87299
+ return { ok: false, reason: `checklist row "${id}" missing` };
87300
+ if (row.pass !== true)
87301
+ return { ok: false, reason: `checklist row "${id}" not passing` };
87302
+ if (typeof row.evidence !== "string" || row.evidence.trim() === "") {
87303
+ return { ok: false, reason: `checklist row "${id}" has empty evidence` };
87304
+ }
87305
+ }
87306
+ return { ok: true };
87307
+ }
87308
+ var READY_CHECKLIST_IDS, DEFAULT_READY_PREPARE_TIMEOUT_MS = 900000, TaskPreparationError, READY_SKIPPED, READY_DONE, EXECUTION_OWNED_SECTIONS, PLANNING_DIGEST_SECTIONS;
87309
+ var init_task_readiness = __esm(() => {
87310
+ init_src2();
87311
+ READY_CHECKLIST_IDS = [
87312
+ "requirements",
87313
+ "design",
87314
+ "plan",
87315
+ "ac",
87316
+ "decisions",
87317
+ "dependencies",
87318
+ "premises"
87319
+ ];
87320
+ TaskPreparationError = class TaskPreparationError extends Error {
87321
+ stage;
87322
+ wbs;
87323
+ filePath;
87324
+ findings;
87325
+ recoveryCommand;
87326
+ constructor(init) {
87327
+ super(init.message);
87328
+ this.name = "TaskPreparationError";
87329
+ this.stage = init.stage;
87330
+ this.wbs = init.wbs;
87331
+ this.filePath = init.filePath;
87332
+ this.findings = init.findings;
87333
+ this.recoveryCommand = init.recoveryCommand;
87334
+ }
87335
+ };
87336
+ READY_SKIPPED = { readiness: { status: "skipped", depth: "ready" } };
87337
+ READY_DONE = { readiness: { status: "ready", depth: "ready" } };
87338
+ EXECUTION_OWNED_SECTIONS = { Solution: true, Testing: true, Review: true, History: true };
87339
+ PLANNING_DIGEST_SECTIONS = TASK_CANONICAL_SECTIONS.filter((s3) => !(s3 in EXECUTION_OWNED_SECTIONS));
87340
+ });
87341
+
86719
87342
  // ../../packages/app/src/services/task-scaffold.ts
86720
87343
  import { dirname as dirname13 } from "path";
86721
87344
 
@@ -86934,14 +87557,99 @@ class TaskService {
86934
87557
  }
86935
87558
  return null;
86936
87559
  }
87560
+ async resolveCandidateShape(input) {
87561
+ const variant = input.template ?? (input.featureId !== undefined ? "feature-impl" : DEFAULT_TASK_VARIANT);
87562
+ let background = input.background ?? "";
87563
+ if (background === "" && input.featureId !== undefined) {
87564
+ background = await this.deriveBackground(input.featureId);
87565
+ }
87566
+ const taskBodies = {};
87567
+ if (background !== "")
87568
+ taskBodies.Background = background;
87569
+ if ((input.requirements ?? "").trim() !== "") {
87570
+ taskBodies.Requirements = bulletizeRequirements(input.requirements ?? "");
87571
+ }
87572
+ if ((input.design ?? "").trim() !== "") {
87573
+ taskBodies.Design = (input.design ?? "").trim();
87574
+ }
87575
+ if ((input.plan ?? "").trim() !== "") {
87576
+ taskBodies.Plan = (input.plan ?? "").trim();
87577
+ }
87578
+ if ((input.acceptanceCriteria ?? "").trim() !== "") {
87579
+ taskBodies["Acceptance Criteria"] = normalizeAcFence((input.acceptanceCriteria ?? "").trim());
87580
+ }
87581
+ if (taskBodies.Background === undefined) {
87582
+ taskBodies.Background = `Captured from the creation title: "${input.title}".`;
87583
+ }
87584
+ const templateBackground = this.ctx.resolveTemplateBodies?.(variant).Background;
87585
+ if (input.background === undefined && input.featureId === undefined && templateBackground !== undefined && templateBackground.trim() !== "") {
87586
+ taskBodies.Background = `${taskBodies.Background}
87587
+
87588
+ ${templateBackground}`;
87589
+ }
87590
+ const now2 = new Date().toISOString();
87591
+ const fmFor = (status2) => ({
87592
+ schema_version: 1,
87593
+ name: input.title,
87594
+ status: status2,
87595
+ template: variant,
87596
+ created_at: now2,
87597
+ updated_at: now2,
87598
+ ...input.featureId !== undefined ? { feature_id: input.featureId } : {},
87599
+ ...input.parentWbs !== undefined ? { parent_wbs: input.parentWbs } : {},
87600
+ ...input.priority !== undefined ? { priority: input.priority } : {},
87601
+ ...input.tags !== undefined && input.tags.length > 0 ? { tags: input.tags } : {}
87602
+ });
87603
+ let status;
87604
+ if (input.explicitStatus !== undefined) {
87605
+ status = input.explicitStatus;
87606
+ } else {
87607
+ const probe = buildTaskSkeleton({
87608
+ wbs: CANDIDATE_PROBE_WBS,
87609
+ title: input.title,
87610
+ frontmatter: serializeTaskFrontmatter(fmFor("todo")),
87611
+ sections: this.sectionsForStatus(variant, "todo"),
87612
+ bodies: this.bodiesFor(variant, taskBodies)
87613
+ });
87614
+ const probeFindings = this.checkCandidateContent(probe, CANDIDATE_PROBE_WBS, "todo");
87615
+ status = probeFindings.some((f) => f.severity === "error") ? "backlog" : "todo";
87616
+ }
87617
+ return { variant, status, fm: fmFor(status), taskBodies };
87618
+ }
87619
+ checkCandidateContent(raw, wbs, asStatus) {
87620
+ const matrix = this.ctx.sectionMatrix;
87621
+ if (matrix === undefined) {
87622
+ throw new Error(`no section-matrix available for candidate validation (status=${asStatus}); ` + "a canonical section-matrix.yaml is required for task creation (F92 R1)");
87623
+ }
87624
+ const checker = new TaskCheckService(this.ctx.fs, matrix);
87625
+ return checker.checkContentPolicy(raw, wbs, { asStatus }).findings;
87626
+ }
87627
+ renderCandidate(title, wbs, shape) {
87628
+ return buildTaskSkeleton({
87629
+ wbs,
87630
+ title,
87631
+ frontmatter: serializeTaskFrontmatter(shape.fm),
87632
+ sections: this.sectionsForStatus(shape.variant, shape.status),
87633
+ bodies: this.bodiesFor(shape.variant, shape.taskBodies)
87634
+ });
87635
+ }
87636
+ validateCandidateOrThrow(title, shape) {
87637
+ const probe = this.renderCandidate(title, CANDIDATE_PROBE_WBS, shape);
87638
+ const findings = this.checkCandidateContent(probe, CANDIDATE_PROBE_WBS, shape.status);
87639
+ if (findings.some((f) => f.severity === "error")) {
87640
+ throw new TaskCandidateInvalidError(title, findings);
87641
+ }
87642
+ }
86937
87643
  async create(params) {
86938
87644
  const folder = this.ctx.tasksDir;
86939
- let background = "";
86940
- if (params.featureId !== undefined) {
86941
- background = await this.deriveBackground(params.featureId);
86942
- }
86943
- const variant = params.template ?? (params.featureId !== undefined ? "feature-impl" : DEFAULT_TASK_VARIANT);
86944
- const status = params.status ?? (params.featureId !== undefined ? "todo" : "backlog");
87645
+ const shape = await this.resolveCandidateShape({
87646
+ title: params.title,
87647
+ featureId: params.featureId,
87648
+ parentWbs: params.parentWbs,
87649
+ template: params.template,
87650
+ explicitStatus: params.status
87651
+ });
87652
+ this.validateCandidateOrThrow(params.title, shape);
86945
87653
  return this.writeService.createAllocated(folder, async () => {
86946
87654
  const dedupeWithinSec = params.dedupeWithinSec === null ? undefined : params.dedupeWithinSec ?? 300;
86947
87655
  if (dedupeWithinSec !== undefined) {
@@ -86952,25 +87660,7 @@ class TaskService {
86952
87660
  }
86953
87661
  const slug = this.slugify(params.title);
86954
87662
  const { wbs, filePath } = await this.allocateWbsChecked(slug);
86955
- const now2 = new Date().toISOString();
86956
- const frontmatter = [
86957
- "schema_version: 1",
86958
- `name: "${params.title}"`,
86959
- `status: ${status}`,
86960
- `template: ${variant}`,
86961
- `created_at: ${now2}`,
86962
- `updated_at: ${now2}`,
86963
- params.featureId !== undefined ? `feature_id: ${params.featureId}` : null,
86964
- params.parentWbs !== undefined ? `parent_wbs: "${params.parentWbs}"` : null
86965
- ].filter(Boolean).join(`
86966
- `);
86967
- const content = buildTaskSkeleton({
86968
- wbs,
86969
- title: params.title,
86970
- frontmatter,
86971
- sections: this.sectionsForStatus(variant, status),
86972
- bodies: this.bodiesFor(variant, background !== "" ? { Background: background } : {})
86973
- });
87663
+ const content = this.renderCandidate(params.title, wbs, shape);
86974
87664
  const ref = { kind: "task", id: wbs, filePath, folder };
86975
87665
  return { ref, content };
86976
87666
  });
@@ -87389,11 +88079,37 @@ class TaskService {
87389
88079
  ${issues}`);
87390
88080
  }
87391
88081
  const items = result.data;
88082
+ const prepared = [];
88083
+ for (const [index2, item] of items.entries()) {
88084
+ const shape = await this.resolveCandidateShape({
88085
+ title: item.name,
88086
+ featureId: item.feature_id ?? undefined,
88087
+ parentWbs: item.parent_wbs ?? undefined,
88088
+ template: item.template,
88089
+ explicitStatus: undefined,
88090
+ background: item.background,
88091
+ requirements: item.requirements,
88092
+ design: item.design,
88093
+ plan: item.plan,
88094
+ acceptanceCriteria: item.acceptance_criteria,
88095
+ priority: item.priority,
88096
+ tags: item.tags
88097
+ });
88098
+ try {
88099
+ this.validateCandidateOrThrow(item.name, shape);
88100
+ } catch (err) {
88101
+ if (err instanceof TaskCandidateInvalidError) {
88102
+ throw new TaskCandidateInvalidError(item.name, err.findings, `batch item ${index2 + 1}/${items.length}`);
88103
+ }
88104
+ throw err;
88105
+ }
88106
+ prepared.push({ item, shape });
88107
+ }
87392
88108
  const writeResults = [];
87393
88109
  const createdRefs = [];
87394
88110
  try {
87395
- for (const item of items) {
87396
- const wr = await this.createBatchItem(item);
88111
+ for (const { item, shape } of prepared) {
88112
+ const wr = await this.createBatchItem(item, shape);
87397
88113
  writeResults.push(wr);
87398
88114
  createdRefs.push(wr.ref);
87399
88115
  }
@@ -87449,54 +88165,12 @@ ${issues}`);
87449
88165
  }
87450
88166
  return out;
87451
88167
  }
87452
- async createBatchItem(item) {
88168
+ async createBatchItem(item, shape) {
87453
88169
  const folder = this.ctx.tasksDir;
87454
- let background = item.background ?? "";
87455
- if (!background && item.feature_id !== undefined && item.feature_id !== null) {
87456
- background = await this.deriveBackground(item.feature_id);
87457
- }
87458
- const hasSpec = background !== "" || (item.requirements ?? "").trim() !== "" || (item.design ?? "").trim() !== "" || (item.plan ?? "").trim() !== "" || (item.acceptance_criteria ?? "").trim() !== "";
87459
- const status = hasSpec ? "todo" : "backlog";
87460
- const variant = item.template ?? (item.feature_id !== undefined && item.feature_id !== null ? "feature-impl" : DEFAULT_TASK_VARIANT);
87461
88170
  return this.writeService.createAllocated(folder, async () => {
87462
88171
  const slug = this.slugify(item.name);
87463
88172
  const { wbs, filePath } = await this.allocateWbsChecked(slug);
87464
- const now2 = new Date().toISOString();
87465
- const fmLines = [
87466
- "schema_version: 1",
87467
- `name: "${item.name}"`,
87468
- `status: ${status}`,
87469
- `template: ${variant}`,
87470
- `created_at: ${now2}`,
87471
- `updated_at: ${now2}`,
87472
- item.feature_id !== undefined ? `feature_id: ${item.feature_id}` : null,
87473
- item.parent_wbs !== undefined ? `parent_wbs: "${item.parent_wbs}"` : null,
87474
- item.priority !== undefined ? `priority: ${item.priority}` : null,
87475
- item.tags !== undefined && item.tags.length > 0 ? `tags: [${item.tags.map((t) => `"${t}"`).join(", ")}]` : null
87476
- ].filter(Boolean).join(`
87477
- `);
87478
- const taskBodies = {};
87479
- if (background !== "")
87480
- taskBodies.Background = background;
87481
- if ((item.requirements ?? "").trim() !== "") {
87482
- taskBodies.Requirements = bulletizeRequirements(item.requirements ?? "");
87483
- }
87484
- if ((item.design ?? "").trim() !== "") {
87485
- taskBodies.Design = (item.design ?? "").trim();
87486
- }
87487
- if ((item.plan ?? "").trim() !== "") {
87488
- taskBodies.Plan = (item.plan ?? "").trim();
87489
- }
87490
- if ((item.acceptance_criteria ?? "").trim() !== "") {
87491
- taskBodies["Acceptance Criteria"] = normalizeAcFence((item.acceptance_criteria ?? "").trim());
87492
- }
87493
- const content = buildTaskSkeleton({
87494
- wbs,
87495
- title: item.name,
87496
- frontmatter: fmLines,
87497
- sections: this.sectionsForStatus(variant, status),
87498
- bodies: this.bodiesFor(variant, taskBodies)
87499
- });
88173
+ const content = this.renderCandidate(item.name, wbs, shape);
87500
88174
  const ref = { kind: "task", id: wbs, filePath, folder };
87501
88175
  return { ref, content };
87502
88176
  });
@@ -87688,7 +88362,7 @@ ${block}` : block);
87688
88362
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
87689
88363
  }
87690
88364
  }
87691
- var DependencyMutationError, SectionMutationError, DuplicateFollowUpError, WbsCollisionError, ROSTER_START = "<!-- AUTO-GENERATED by spur task refresh-roster -->", ROSTER_END = "<!-- END AUTO-GENERATED -->", ROSTER_REGION_RE, TASK_ACTION_COMMANDS;
88365
+ var DependencyMutationError, SectionMutationError, DuplicateFollowUpError, WbsCollisionError, TaskCandidateInvalidError, CANDIDATE_PROBE_WBS = "0000", ROSTER_START = "<!-- AUTO-GENERATED by spur task refresh-roster -->", ROSTER_END = "<!-- END AUTO-GENERATED -->", ROSTER_REGION_RE, TASK_ACTION_COMMANDS;
87692
88366
  var init_task_service = __esm(() => {
87693
88367
  init_src2();
87694
88368
  init_errors7();
@@ -87737,6 +88411,16 @@ var init_task_service = __esm(() => {
87737
88411
  this.attemptedPath = attemptedPath;
87738
88412
  }
87739
88413
  };
88414
+ TaskCandidateInvalidError = class TaskCandidateInvalidError extends Error {
88415
+ findings;
88416
+ constructor(title, findings, context4) {
88417
+ const where = context4 !== undefined ? ` (${context4})` : "";
88418
+ const detail = findings.filter((f) => f.severity === "error").map((f) => `${f.layer}${f.section !== "" ? `/${f.section}` : ""}: ${f.message}`).join("; ");
88419
+ super(`candidate task "${title}"${where} rejected before persistence \u2014 it would fail \`spur task check\`: ${detail}`);
88420
+ this.name = "TaskCandidateInvalidError";
88421
+ this.findings = findings;
88422
+ }
88423
+ };
87740
88424
  ROSTER_REGION_RE = /<!--[ \t]*AUTO-GENERATED[^>]*-->[\s\S]*?<!--[ \t]*END[ \t]*AUTO-GENERATED[^>]*-->/g;
87741
88425
  TASK_ACTION_COMMANDS = {
87742
88426
  refine: (wbs) => `/sp:dev-refine ${wbs} --auto`,
@@ -89959,8 +90643,93 @@ var init_child_env = __esm(() => {
89959
90643
  SHIM_INTERNAL_ENV = /^PROTO_(SHIM|INTERNAL)_/;
89960
90644
  });
89961
90645
 
90646
+ // ../../packages/app/src/workflow/actions/run-path.ts
90647
+ import { dirname as dirname16, join as join22, normalize, relative as relative5, resolve as resolve9, sep as sep2 } from "path";
90648
+ function within(child, ancestor) {
90649
+ return child === ancestor || child.startsWith(`${ancestor}${sep2}`);
90650
+ }
90651
+ async function resolveRunArtifactPath(fileSystem, workdir, pathRaw) {
90652
+ if (typeof fileSystem.realPath !== "function") {
90653
+ throw new RunArtifactPathError("physical path confinement requires a FileSystem with realPath support \u2014 refusing to skip confinement (0785 R2)");
90654
+ }
90655
+ const lexicalTarget = normalize(resolve9(workdir, pathRaw));
90656
+ const lexicalRunRoot = normalize(join22(resolve9(workdir), ".spur", "run"));
90657
+ if (!lexicalTarget.startsWith(`${lexicalRunRoot}${sep2}`)) {
90658
+ throw new RunArtifactPathError(`must resolve beneath .spur/run/ (got ${pathRaw})`);
90659
+ }
90660
+ const workdirAbs = resolve9(workdir);
90661
+ let canonicalWorkdir;
90662
+ try {
90663
+ canonicalWorkdir = fileSystem.realPath(workdirAbs);
90664
+ } catch (error51) {
90665
+ throw new RunArtifactPathError(`project workdir could not be canonicalized: ${error51.message}`);
90666
+ }
90667
+ const canonicalRunRootCandidate = join22(canonicalWorkdir, ".spur", "run");
90668
+ let canonicalRunRoot = canonicalRunRootCandidate;
90669
+ const rootStat = await fileSystem.stat(canonicalRunRootCandidate);
90670
+ if (rootStat !== null) {
90671
+ try {
90672
+ canonicalRunRoot = fileSystem.realPath(canonicalRunRootCandidate);
90673
+ } catch (error51) {
90674
+ throw new RunArtifactPathError(`.spur/run could not be canonicalized: ${error51.message}`);
90675
+ }
90676
+ if (!within(canonicalRunRoot, canonicalWorkdir)) {
90677
+ throw new RunArtifactPathError(".spur/run resolves outside the project workdir through a symlink");
90678
+ }
90679
+ }
90680
+ let probe = lexicalTarget;
90681
+ for (;; ) {
90682
+ const stat = await fileSystem.stat(probe);
90683
+ if (stat !== null)
90684
+ break;
90685
+ const parent = dirname16(probe);
90686
+ let listed;
90687
+ try {
90688
+ listed = await fileSystem.readDir(parent);
90689
+ } catch (error51) {
90690
+ if (String(error51.message).includes("ENOENT")) {
90691
+ listed = null;
90692
+ } else {
90693
+ throw new RunArtifactPathError(`ancestor ${parent} is unreadable: ${error51.message}`);
90694
+ }
90695
+ }
90696
+ const base2 = probe.slice(parent.length + 1);
90697
+ if (listed?.includes(base2)) {
90698
+ throw new RunArtifactPathError(`dangling symlink at ${probe} \u2014 refusing artifact path`);
90699
+ }
90700
+ const parentNorm = normalize(parent);
90701
+ if (parentNorm === probe) {
90702
+ throw new RunArtifactPathError(`no existing ancestor found for ${pathRaw}`);
90703
+ }
90704
+ probe = parentNorm;
90705
+ }
90706
+ let anchor;
90707
+ try {
90708
+ anchor = fileSystem.realPath(probe);
90709
+ } catch (error51) {
90710
+ throw new RunArtifactPathError(`ancestor ${probe} could not be canonicalized: ${error51.message}`);
90711
+ }
90712
+ if (!within(anchor, canonicalWorkdir)) {
90713
+ throw new RunArtifactPathError(`ancestor ${probe} escapes the project workdir through a symlink`);
90714
+ }
90715
+ const reconstructed = normalize(join22(anchor, relative5(probe, lexicalTarget)));
90716
+ if (!reconstructed.startsWith(`${canonicalRunRoot}${sep2}`)) {
90717
+ throw new RunArtifactPathError(`path escapes .spur/run/ through a symlink (got ${pathRaw})`);
90718
+ }
90719
+ return reconstructed;
90720
+ }
90721
+ var RunArtifactPathError;
90722
+ var init_run_path = __esm(() => {
90723
+ RunArtifactPathError = class RunArtifactPathError extends Error {
90724
+ constructor(message) {
90725
+ super(message);
90726
+ this.name = "RunArtifactPathError";
90727
+ }
90728
+ };
90729
+ });
90730
+
89962
90731
  // ../../packages/app/src/workflow/actions/command-gate.ts
89963
- import { normalize, resolve as resolve9 } from "path";
90732
+ import { join as join23, normalize as normalize2 } from "path";
89964
90733
  function sleep3(ms) {
89965
90734
  return new Promise((resolve10) => setTimeout(resolve10, ms));
89966
90735
  }
@@ -90012,13 +90781,13 @@ class CommandGateActionRunner {
90012
90781
  };
90013
90782
  }
90014
90783
  const workdir = context4.workdir ?? process.cwd();
90015
- const allowedDir = resolve9(workdir, ".spur", "run");
90016
- const resolvedResultFile = resolve9(workdir, resultFileRaw);
90017
- const normalized = normalize(resolvedResultFile);
90018
- if (!normalized.startsWith(allowedDir)) {
90784
+ let normalized;
90785
+ try {
90786
+ normalized = await resolveRunArtifactPath(this.fileSystem, workdir, resultFileRaw);
90787
+ } catch (error51) {
90019
90788
  return {
90020
90789
  ok: false,
90021
- error: `resultFile must resolve beneath .spur/run/ (got ${resultFileRaw})`
90790
+ error: `resultFile ${error51.message}`
90022
90791
  };
90023
90792
  }
90024
90793
  const split = splitLaunchCommand(executable, 'command.gate "executable"');
@@ -90032,7 +90801,7 @@ class CommandGateActionRunner {
90032
90801
  const retryOn = Array.isArray(retry.on) ? retry.on : [];
90033
90802
  const timeoutMs = typeof options.timeoutMs === "number" ? options.timeoutMs : undefined;
90034
90803
  const env = childProcessEnv(context4.vars);
90035
- await this.fileSystem.ensureDir(allowedDir);
90804
+ await this.fileSystem.ensureDir(normalize2(join23(normalized, "..")));
90036
90805
  let lastStdout = "";
90037
90806
  let lastStderr = "";
90038
90807
  let lastExitCode = -1;
@@ -90102,10 +90871,11 @@ var init_command_gate = __esm(() => {
90102
90871
  init_dist5();
90103
90872
  init_split_launch_command();
90104
90873
  init_child_env();
90874
+ init_run_path();
90105
90875
  });
90106
90876
 
90107
90877
  // ../../packages/app/src/workflow/actions/doctor-probe.ts
90108
- import { normalize as normalize2, resolve as resolve10, sep as sep2 } from "path";
90878
+ import { normalize as normalize3, resolve as resolve10, sep as sep3 } from "path";
90109
90879
  function stringOption2(options, key2, fallback) {
90110
90880
  const value = options[key2];
90111
90881
  if (typeof value === "string")
@@ -90155,8 +90925,8 @@ class DoctorProbeActionRunner {
90155
90925
  const resolvedAgentVar = stringOption2(options, "resolvedAgentVar", "");
90156
90926
  const workdir = context4.workdir ?? process.cwd();
90157
90927
  const allowedDir = resolve10(workdir, ".spur", "run");
90158
- const normalized = normalize2(resolve10(workdir, resultFileRaw));
90159
- const withinAllowed = normalized === allowedDir || normalized.startsWith(allowedDir + sep2);
90928
+ const normalized = normalize3(resolve10(workdir, resultFileRaw));
90929
+ const withinAllowed = normalized === allowedDir || normalized.startsWith(allowedDir + sep3);
90160
90930
  if (!withinAllowed) {
90161
90931
  return {
90162
90932
  ok: false,
@@ -90753,11 +91523,59 @@ var init_composition_baseline = () => {};
90753
91523
  // ../../packages/app/src/workflow/proof-input-fingerprint.ts
90754
91524
  import crypto4 from "crypto";
90755
91525
  import { tmpdir as tmpdir2 } from "os";
90756
- import { dirname as dirname16, join as join22 } from "path";
91526
+ import { dirname as dirname17, join as join24, resolve as resolve11, sep as sep4 } from "path";
91527
+ async function readProofInputContents(fileSystem, workdir, options) {
91528
+ for (const name of ["taskFile", "featureFile"]) {
91529
+ const value = options[name];
91530
+ if (value !== undefined && typeof value !== "string") {
91531
+ return { ok: false, error: `${name} must be a string when supplied (got ${typeof value})` };
91532
+ }
91533
+ }
91534
+ const readOne = async (name) => {
91535
+ const raw = name === "taskFile" ? options.taskFile : options.featureFile;
91536
+ if (raw === undefined || raw === "")
91537
+ return {};
91538
+ const workdirAbs = resolve11(workdir);
91539
+ const resolved = resolve11(workdirAbs, raw);
91540
+ if (!(resolved === workdirAbs || resolved.startsWith(`${workdirAbs}${sep4}`))) {
91541
+ return { error: `${name} must resolve under the workflow workdir (${workdirAbs}): ${raw}` };
91542
+ }
91543
+ let stat;
91544
+ try {
91545
+ stat = await fileSystem.stat(resolved);
91546
+ } catch (error51) {
91547
+ return { error: `${name} could not be inspected: ${error51.message}` };
91548
+ }
91549
+ if (stat === null) {
91550
+ return {
91551
+ error: `${name} does not exist: ${raw} (resolved ${resolved}) \u2014 an explicitly supplied spec must be readable, not silently omitted`
91552
+ };
91553
+ }
91554
+ if (!stat.isFile()) {
91555
+ return { error: `${name} is not a regular file: ${resolved}` };
91556
+ }
91557
+ try {
91558
+ return { content: await fileSystem.readFile(resolved) };
91559
+ } catch (error51) {
91560
+ return { error: `${name} is not readable: ${error51.message}` };
91561
+ }
91562
+ };
91563
+ const task = await readOne("taskFile");
91564
+ if (task.error !== undefined)
91565
+ return { ok: false, error: task.error };
91566
+ const feature = await readOne("featureFile");
91567
+ if (feature.error !== undefined)
91568
+ return { ok: false, error: feature.error };
91569
+ return {
91570
+ ok: true,
91571
+ ...task.content !== undefined ? { taskContent: task.content } : {},
91572
+ ...feature.content !== undefined ? { featureContent: feature.content } : {}
91573
+ };
91574
+ }
90757
91575
  async function createGitAlternateTree(cwd, excludeGlobs = DEFAULT_EXCLUDE_GLOBS, executor = new NodeProcessExecutor3, fs3 = createNodeFileSystem3()) {
90758
- const indexFile = join22(tmpdir2(), `spur-proof-fingerprint-${crypto4.randomUUID()}.index`);
91576
+ const indexFile = join24(tmpdir2(), `spur-proof-fingerprint-${crypto4.randomUUID()}.index`);
90759
91577
  try {
90760
- await fs3.ensureDir(dirname16(indexFile));
91578
+ await fs3.ensureDir(dirname17(indexFile));
90761
91579
  const env = Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
90762
91580
  env.GIT_INDEX_FILE = indexFile;
90763
91581
  const common4 = { cwd, env, forceBuffered: true, rejectOnError: false };
@@ -90874,15 +91692,6 @@ var init_proof_input_fingerprint = __esm(() => {
90874
91692
  });
90875
91693
 
90876
91694
  // ../../packages/app/src/workflow/actions/proof-fingerprint.ts
90877
- async function readOptional(fileSystem, path9) {
90878
- if (path9 === undefined || path9 === "")
90879
- return;
90880
- const stat = await fileSystem.stat(path9);
90881
- if (stat === null)
90882
- return;
90883
- return await fileSystem.readFile(path9);
90884
- }
90885
-
90886
91695
  class ProofFingerprintActionRunner {
90887
91696
  fileSystem;
90888
91697
  processExecutor;
@@ -90901,14 +91710,16 @@ class ProofFingerprintActionRunner {
90901
91710
  if (!VAR_NAME_RE.test(varName)) {
90902
91711
  return { ok: false, error: `${KIND11}: var name must match ${VAR_NAME_RE}, got "${varName}"` };
90903
91712
  }
90904
- const taskContent = await readOptional(this.fileSystem, options.taskFile);
90905
- const featureContent = await readOptional(this.fileSystem, options.featureFile);
91713
+ const inputs = await readProofInputContents(this.fileSystem, context4.workdir ?? ".", options);
91714
+ if (!inputs.ok) {
91715
+ return { ok: false, error: `${KIND11}: ${inputs.error}` };
91716
+ }
90906
91717
  let digest;
90907
91718
  try {
90908
91719
  digest = await computeProofInputFingerprint({
90909
91720
  cwd: context4.workdir ?? ".",
90910
- ...taskContent !== undefined ? { taskContent } : {},
90911
- ...featureContent !== undefined ? { featureContent } : {},
91721
+ ...inputs.taskContent !== undefined ? { taskContent: inputs.taskContent } : {},
91722
+ ...inputs.featureContent !== undefined ? { featureContent: inputs.featureContent } : {},
90912
91723
  ...this.processExecutor !== undefined ? { processExecutor: this.processExecutor } : {},
90913
91724
  fileSystem: this.fileSystem
90914
91725
  });
@@ -91055,17 +91866,19 @@ var init_rule_check = __esm(() => {
91055
91866
  });
91056
91867
 
91057
91868
  // ../../packages/app/src/workflow/actions/run-artifact.ts
91058
- import { normalize as normalize3, resolve as resolve11 } from "path";
91869
+ import { join as join25, resolve as resolve12 } from "path";
91059
91870
 
91060
91871
  class RunArtifactActionRunner {
91061
91872
  getDb;
91062
91873
  fileSystem;
91063
91874
  artifactDao;
91875
+ processExecutor;
91064
91876
  kind = KIND14;
91065
- constructor(getDb, fileSystem = createNodeFileSystem3(), artifactDao) {
91877
+ constructor(getDb, fileSystem = createNodeFileSystem3(), artifactDao, processExecutor) {
91066
91878
  this.getDb = getDb;
91067
91879
  this.fileSystem = fileSystem;
91068
91880
  this.artifactDao = artifactDao;
91881
+ this.processExecutor = processExecutor;
91069
91882
  }
91070
91883
  async execute(options, context4) {
91071
91884
  const pathRaw = options.path;
@@ -91082,39 +91895,36 @@ class RunArtifactActionRunner {
91082
91895
  error: 'Action option "artifactKind" must be a non-empty string'
91083
91896
  };
91084
91897
  }
91085
- const workdir = context4.workdir ?? process.cwd();
91086
- const allowedDir = resolve11(workdir, ".spur", "run");
91087
- const resolvedPath = resolve11(workdir, pathRaw);
91088
- const normalized = normalize3(resolvedPath);
91089
- if (!normalized.startsWith(allowedDir)) {
91898
+ const proofBinding = options.proofBinding;
91899
+ if (proofBinding !== undefined && proofBinding !== "current") {
91090
91900
  return {
91091
91901
  ok: false,
91092
- error: `path must resolve beneath .spur/run/ (got ${pathRaw})`
91902
+ error: `${KIND14}: unsupported proofBinding "${String(proofBinding)}" - only "current" is defined (ADR-071)`
91093
91903
  };
91094
91904
  }
91905
+ const workdir = context4.workdir ?? process.cwd();
91906
+ if (proofBinding === "current") {
91907
+ return await this.executeBound(pathRaw, artifactKind, options, context4, workdir);
91908
+ }
91909
+ let normalized;
91910
+ try {
91911
+ normalized = await resolveRunArtifactPath(this.fileSystem, workdir, pathRaw);
91912
+ } catch (error51) {
91913
+ return { ok: false, error: `path ${error51.message}` };
91914
+ }
91095
91915
  const requireExisting = options.requireExisting !== false;
91096
91916
  if (requireExisting) {
91097
- const exists2 = await this.fileSystem.exists(normalized);
91098
- if (!exists2) {
91917
+ const stat = await this.fileSystem.stat(normalized);
91918
+ if (stat === null) {
91099
91919
  return {
91100
91920
  ok: false,
91101
91921
  error: `run.artifact required file does not exist: ${pathRaw} (${normalized})`
91102
91922
  };
91103
91923
  }
91104
- }
91105
- const proofBinding = options.proofBinding;
91106
- if (proofBinding !== undefined) {
91107
- if (proofBinding !== "current") {
91108
- return {
91109
- ok: false,
91110
- error: `${KIND14}: unsupported proofBinding "${String(proofBinding)}" - only "current" is defined (ADR-071)`
91111
- };
91112
- }
91113
- const digest = context4.vars.proofDigestNow ?? context4.vars.proofDigest;
91114
- if (typeof digest !== "string" || !PROOF_DIGEST_RE.test(digest)) {
91924
+ if (!stat.isFile()) {
91115
91925
  return {
91116
91926
  ok: false,
91117
- error: `${KIND14}: proofBinding "current" does not hold - the run has no current proof input ` + `digest (vars.proofDigestNow/proofDigest missing or malformed), so the artifact cannot be ` + `bound to a proof that was never captured; capture one with proof.fingerprint first`
91927
+ error: `run.artifact required file is not a regular file: ${pathRaw} (${normalized})`
91118
91928
  };
91119
91929
  }
91120
91930
  }
@@ -91138,9 +91948,232 @@ class RunArtifactActionRunner {
91138
91948
  id: recordId,
91139
91949
  path: normalized,
91140
91950
  kind: artifactKind,
91141
- runId: context4.runId,
91142
- ...options.proofBinding !== undefined ? { proofBinding: options.proofBinding } : {}
91951
+ runId: context4.runId
91952
+ }
91953
+ };
91954
+ }
91955
+ async executeBound(pathRaw, artifactKind, options, context4, workdir) {
91956
+ if (artifactKind !== "verify-verdict") {
91957
+ return {
91958
+ ok: false,
91959
+ error: `${KIND14}: proofBinding "current" is only supported for the verify-verdict artifact kind (got "${artifactKind}")`
91960
+ };
91961
+ }
91962
+ if (!this.getDb) {
91963
+ return {
91964
+ ok: false,
91965
+ error: `${KIND14}: proofBinding "current" requires the composed database context (RunDao run identity) \u2014 refusing an unverifiable binding`
91966
+ };
91967
+ }
91968
+ if (typeof options.taskFile !== "string" || options.taskFile.trim() === "") {
91969
+ return {
91970
+ ok: false,
91971
+ error: `${KIND14}: proofBinding "current" requires a non-empty "taskFile" option \u2014 the fresh digest must cover the canonical task spec`
91972
+ };
91973
+ }
91974
+ const inputs = await readProofInputContents(this.fileSystem, workdir, options);
91975
+ if (!inputs.ok) {
91976
+ return { ok: false, error: `${KIND14}: ${inputs.error}` };
91977
+ }
91978
+ let digest;
91979
+ try {
91980
+ digest = await computeProofInputFingerprint({
91981
+ cwd: workdir,
91982
+ ...inputs.taskContent !== undefined ? { taskContent: inputs.taskContent } : {},
91983
+ ...inputs.featureContent !== undefined ? { featureContent: inputs.featureContent } : {},
91984
+ ...this.processExecutor !== undefined ? { processExecutor: this.processExecutor } : {},
91985
+ fileSystem: this.fileSystem
91986
+ });
91987
+ } catch (error51) {
91988
+ return { ok: false, error: `${KIND14}: fresh proof capture failed: ${error51.message}` };
91989
+ }
91990
+ const varDigest = context4.vars.proofDigestNow ?? context4.vars.proofDigest;
91991
+ if (typeof varDigest !== "string" || !PROOF_DIGEST_RE.test(varDigest)) {
91992
+ return {
91993
+ ok: false,
91994
+ error: `${KIND14}: proofBinding "current" does not hold - the run has no current proof input ` + `digest (vars.proofDigestNow/proofDigest missing or malformed), so the artifact cannot be ` + `bound to a proof that was never captured; capture one with proof.fingerprint first`
91995
+ };
91996
+ }
91997
+ if (varDigest !== digest) {
91998
+ return {
91999
+ ok: false,
92000
+ error: `${KIND14}: the run's declared proof digest (${varDigest}) does not match the freshly ` + `captured digest (${digest}) \u2014 a stale or forged var cannot bless artifact content (0785 R3)`
92001
+ };
92002
+ }
92003
+ let normalized;
92004
+ try {
92005
+ normalized = await resolveRunArtifactPath(this.fileSystem, workdir, pathRaw);
92006
+ } catch (error51) {
92007
+ return { ok: false, error: `path ${error51.message}` };
92008
+ }
92009
+ const requireExisting = options.requireExisting !== false;
92010
+ if (requireExisting) {
92011
+ const stat = await this.fileSystem.stat(normalized);
92012
+ if (stat === null) {
92013
+ return {
92014
+ ok: false,
92015
+ error: `run.artifact required file does not exist: ${pathRaw} (${normalized})`
92016
+ };
92017
+ }
92018
+ if (!stat.isFile()) {
92019
+ return {
92020
+ ok: false,
92021
+ error: `run.artifact required file is not a regular file: ${pathRaw} (${normalized})`
92022
+ };
92023
+ }
92024
+ }
92025
+ let raw;
92026
+ try {
92027
+ raw = await this.fileSystem.readFile(normalized);
92028
+ } catch (error51) {
92029
+ return { ok: false, error: `${KIND14}: verdict artifact unreadable: ${error51.message}` };
92030
+ }
92031
+ const taskWbs = String(context4.vars.wbs ?? "");
92032
+ const outcome = parseVerifyVerdict(raw, taskWbs);
92033
+ if (outcome.kind !== "valid") {
92034
+ const detail = outcome.kind === "invalid" && outcome.reason !== undefined ? `: ${outcome.reason}` : "";
92035
+ return {
92036
+ ok: false,
92037
+ error: `${KIND14}: verdict artifact is ${outcome.kind}${detail} \u2014 refusing to bind (0785 R3)`
92038
+ };
92039
+ }
92040
+ const verdict = outcome.verdict;
92041
+ if (verdict.wbs !== taskWbs) {
92042
+ return {
92043
+ ok: false,
92044
+ error: `${KIND14}: verdict artifact names wbs "${verdict.wbs}" but the run carries task "${taskWbs}"`
92045
+ };
92046
+ }
92047
+ if (verdict.verdict !== "PASS") {
92048
+ return {
92049
+ ok: false,
92050
+ error: `${KIND14}: verdict aggregate is ${verdict.verdict}, not PASS \u2014 refusing to bind`
92051
+ };
92052
+ }
92053
+ let root;
92054
+ try {
92055
+ root = JSON.parse(raw);
92056
+ } catch {
92057
+ return { ok: false, error: `${KIND14}: verdict artifact is malformed JSON` };
92058
+ }
92059
+ const proof = root.proof;
92060
+ if (proof === null || typeof proof !== "object" || Array.isArray(proof)) {
92061
+ return {
92062
+ ok: false,
92063
+ error: `${KIND14}: verdict artifact carries no raw proof block \u2014 binding refused (0785 R3)`
92064
+ };
92065
+ }
92066
+ const p = proof;
92067
+ const stageOf = (name) => {
92068
+ const stages = p.stages;
92069
+ if (stages === null || typeof stages !== "object" || Array.isArray(stages))
92070
+ return null;
92071
+ const s3 = stages[name];
92072
+ return s3 !== null && typeof s3 === "object" && !Array.isArray(s3) ? s3 : null;
92073
+ };
92074
+ if (p.digest !== digest) {
92075
+ return {
92076
+ ok: false,
92077
+ error: `${KIND14}: proof.digest ${String(p.digest)} does not equal the freshly captured digest (${digest})`
92078
+ };
92079
+ }
92080
+ if (p.runId !== context4.runId) {
92081
+ return {
92082
+ ok: false,
92083
+ error: `${KIND14}: proof.runId ${String(p.runId)} does not match the certifying run ${context4.runId}`
92084
+ };
92085
+ }
92086
+ const db2 = await this.getDb();
92087
+ const runRow = await new RunDao(db2).traceRowById(context4.runId);
92088
+ if (runRow === undefined) {
92089
+ return {
92090
+ ok: false,
92091
+ error: `${KIND14}: run ${context4.runId} has no authoritative row \u2014 refusing binding (0785 R3)`
92092
+ };
92093
+ }
92094
+ let metadata = {};
92095
+ try {
92096
+ metadata = JSON.parse(runRow.metadata_json || "{}");
92097
+ } catch {
92098
+ return { ok: false, error: `${KIND14}: run ${context4.runId} metadata_json is malformed \u2014 refusing binding` };
92099
+ }
92100
+ const resumeDigest = metadata.resumeDefinitionDigest;
92101
+ const expectedDefinition = typeof resumeDigest === "string" && resumeDigest !== "" ? resumeDigest : metadata.definitionDigest;
92102
+ if (typeof expectedDefinition !== "string" || p.definitionDigest !== expectedDefinition) {
92103
+ return {
92104
+ ok: false,
92105
+ error: `${KIND14}: proof.definitionDigest ${String(p.definitionDigest)} does not match the run's ` + `${typeof resumeDigest === "string" && resumeDigest !== "" ? "resume " : ""}definition digest \u2014 ` + `a stale-definition artifact cannot certify this run (0785 R3)`
92106
+ };
92107
+ }
92108
+ const qualityGate = stageOf("qualityGate");
92109
+ const review = stageOf("review");
92110
+ const verification = stageOf("verification");
92111
+ if (qualityGate === null || review === null || verification === null) {
92112
+ return {
92113
+ ok: false,
92114
+ error: `${KIND14}: proof.stages is missing qualityGate/review/verification evidence \u2014 binding refused`
92115
+ };
92116
+ }
92117
+ for (const [name, stage] of [
92118
+ ["qualityGate", qualityGate],
92119
+ ["review", review],
92120
+ ["verification", verification]
92121
+ ]) {
92122
+ if (stage.digest !== digest) {
92123
+ return {
92124
+ ok: false,
92125
+ error: `${KIND14}: proof.stages.${name}.digest does not equal the freshly captured digest`
92126
+ };
91143
92127
  }
92128
+ }
92129
+ if (qualityGate.status !== "PASS") {
92130
+ return {
92131
+ ok: false,
92132
+ error: `${KIND14}: proof.stages.qualityGate.status is ${String(qualityGate.status)}, not PASS`
92133
+ };
92134
+ }
92135
+ if (verification.status !== "PASS") {
92136
+ return {
92137
+ ok: false,
92138
+ error: `${KIND14}: proof.stages.verification.status is ${String(verification.status)}, not PASS`
92139
+ };
92140
+ }
92141
+ if (review.status !== "completed") {
92142
+ return {
92143
+ ok: false,
92144
+ error: `${KIND14}: proof.stages.review.status is "${String(review.status)}" \u2014 a skipped or stale review is not completed evidence (0785 R4)`
92145
+ };
92146
+ }
92147
+ const markerPath = join25(resolve12(workdir), ".spur", "run", `${context4.runId}-review-proof.digest`);
92148
+ let marker = null;
92149
+ try {
92150
+ marker = await this.fileSystem.readFile(markerPath);
92151
+ } catch {
92152
+ marker = null;
92153
+ }
92154
+ if (marker === null || marker.trim() !== digest) {
92155
+ return {
92156
+ ok: false,
92157
+ error: `${KIND14}: review-proof marker ${context4.runId}-review-proof.digest is missing or does not name ` + `the current digest \u2014 review completion is not evidenced for this run (0785 R4)`
92158
+ };
92159
+ }
92160
+ const dao2 = this.artifactDao ?? new ArtifactDao(db2);
92161
+ const record2 = await dao2.record({
92162
+ path: normalized,
92163
+ kind: artifactKind,
92164
+ runId: context4.runId
92165
+ });
92166
+ return {
92167
+ ok: true,
92168
+ data: {
92169
+ id: record2.id,
92170
+ path: normalized,
92171
+ kind: artifactKind,
92172
+ runId: context4.runId,
92173
+ proofBinding: "current",
92174
+ proofDigest: digest
92175
+ },
92176
+ setVars: { proofDigestNow: digest }
91144
92177
  };
91145
92178
  }
91146
92179
  }
@@ -91148,6 +92181,9 @@ var KIND14 = "run.artifact", PROOF_DIGEST_RE;
91148
92181
  var init_run_artifact = __esm(() => {
91149
92182
  init_src2();
91150
92183
  init_dist5();
92184
+ init_verify_verdict();
92185
+ init_proof_input_fingerprint();
92186
+ init_run_path();
91151
92187
  PROOF_DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
91152
92188
  });
91153
92189
 
@@ -91332,7 +92368,7 @@ function registerSpurBuiltins(host, options) {
91332
92368
  host.registerAction(new HitlInputActionRunner(options.hitlResponder), "builtin");
91333
92369
  host.registerAction(new CommandGateActionRunner(options.processExecutor ?? new NodeProcessExecutor3, fileSystem), "builtin");
91334
92370
  host.registerAction(new DoctorProbeActionRunner(options.processExecutor ?? new NodeProcessExecutor3, fileSystem, options.observabilityBus), "builtin");
91335
- host.registerAction(new RunArtifactActionRunner(options.getDb, fileSystem, options.artifactDao), "builtin");
92371
+ host.registerAction(new RunArtifactActionRunner(options.getDb, fileSystem, options.artifactDao, options.processExecutor), "builtin");
91336
92372
  host.registerAction(new ProofFingerprintActionRunner(fileSystem, options.processExecutor, options.observabilityBus), "builtin");
91337
92373
  if (options.httpRequester) {
91338
92374
  host.registerAction(new HttpRequestActionRunner(options.httpRequester, options.hostAllowlist ?? new Set), "builtin");
@@ -91377,11 +92413,11 @@ function parseCheckpointMetadata(raw) {
91377
92413
  continue;
91378
92414
  }
91379
92415
  inArtifacts = false;
91380
- const sep3 = line.indexOf(":");
91381
- if (sep3 <= 0)
92416
+ const sep5 = line.indexOf(":");
92417
+ if (sep5 <= 0)
91382
92418
  continue;
91383
- const key2 = line.slice(0, sep3).trim();
91384
- const value = line.slice(sep3 + 1).trim().replace(/^"|"$/g, "");
92419
+ const key2 = line.slice(0, sep5).trim();
92420
+ const value = line.slice(sep5 + 1).trim().replace(/^"|"$/g, "");
91385
92421
  if (key2 === "artifacts") {
91386
92422
  inArtifacts = true;
91387
92423
  if (value !== "" && value !== "[]") {
@@ -91451,7 +92487,7 @@ var init_checkpoint_contract = __esm(() => {
91451
92487
  });
91452
92488
 
91453
92489
  // ../../packages/app/src/workflow/workflow-resolver.ts
91454
- import { basename as basename7, isAbsolute as isAbsolute6, join as join23, resolve as resolve12 } from "path";
92490
+ import { basename as basename7, isAbsolute as isAbsolute6, join as join26, resolve as resolve13 } from "path";
91455
92491
  function createEmbeddedSchemaOptions(embedded) {
91456
92492
  if (embedded === undefined || embedded.size === 0)
91457
92493
  return;
@@ -91482,12 +92518,12 @@ async function readWorkflowNameFast(fs3, filePath) {
91482
92518
  }
91483
92519
  async function scanWorkflowByName(cwd, name) {
91484
92520
  const fs3 = createNodeFileSystem3();
91485
- const projectSpurDir = resolve12(cwd, ".spur", "workflows");
92521
+ const projectSpurDir = resolve13(cwd, ".spur", "workflows");
91486
92522
  if (fs3.exists(projectSpurDir)) {
91487
92523
  try {
91488
92524
  const files = (await fs3.readDir(projectSpurDir)).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
91489
92525
  for (const f of files) {
91490
- const abs = join23(projectSpurDir, f);
92526
+ const abs = join26(projectSpurDir, f);
91491
92527
  const wfName = await readWorkflowNameFast(fs3, abs);
91492
92528
  if (wfName === name) {
91493
92529
  return { path: abs, source: "project" };
@@ -91497,12 +92533,12 @@ async function scanWorkflowByName(cwd, name) {
91497
92533
  }
91498
92534
  const bundledRoot = bundledConfigRoot();
91499
92535
  if (bundledRoot !== null) {
91500
- const bundledWorkflowsDir = join23(bundledRoot, "workflows");
92536
+ const bundledWorkflowsDir = join26(bundledRoot, "workflows");
91501
92537
  if (fs3.exists(bundledWorkflowsDir)) {
91502
92538
  try {
91503
92539
  const files = (await fs3.readDir(bundledWorkflowsDir)).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
91504
92540
  for (const f of files) {
91505
- const abs = join23(bundledWorkflowsDir, f);
92541
+ const abs = join26(bundledWorkflowsDir, f);
91506
92542
  const wfName = await readWorkflowNameFast(fs3, abs);
91507
92543
  if (wfName === name) {
91508
92544
  return { path: abs, source: "bundled" };
@@ -91515,7 +92551,7 @@ async function scanWorkflowByName(cwd, name) {
91515
92551
  }
91516
92552
  function resolveWorkflowFile(cwd, file2) {
91517
92553
  const fs3 = createNodeFileSystem3();
91518
- const projectPath = resolve12(cwd, file2);
92554
+ const projectPath = resolve13(cwd, file2);
91519
92555
  if (fs3.exists(projectPath)) {
91520
92556
  return { path: projectPath, source: "project" };
91521
92557
  }
@@ -91523,20 +92559,20 @@ function resolveWorkflowFile(cwd, file2) {
91523
92559
  const base2 = basename7(file2);
91524
92560
  if (!isAbsolute6(file2)) {
91525
92561
  const withYaml = isYaml ? file2 : `${file2}.yaml`;
91526
- const projectSpur = resolve12(cwd, ".spur", "workflows", withYaml);
92562
+ const projectSpur = resolve13(cwd, ".spur", "workflows", withYaml);
91527
92563
  if (fs3.exists(projectSpur)) {
91528
92564
  return { path: projectSpur, source: "project" };
91529
92565
  }
91530
92566
  if (!isYaml) {
91531
- const projectDirect = resolve12(cwd, `${file2}.yaml`);
92567
+ const projectDirect = resolve13(cwd, `${file2}.yaml`);
91532
92568
  if (fs3.exists(projectDirect)) {
91533
92569
  return { path: projectDirect, source: "project" };
91534
92570
  }
91535
- const projectSpurPipeline = resolve12(cwd, ".spur", "workflows", `${file2}-pipeline.yaml`);
92571
+ const projectSpurPipeline = resolve13(cwd, ".spur", "workflows", `${file2}-pipeline.yaml`);
91536
92572
  if (fs3.exists(projectSpurPipeline)) {
91537
92573
  return { path: projectSpurPipeline, source: "project" };
91538
92574
  }
91539
- const projectDirectPipeline = resolve12(cwd, `${file2}-pipeline.yaml`);
92575
+ const projectDirectPipeline = resolve13(cwd, `${file2}-pipeline.yaml`);
91540
92576
  if (fs3.exists(projectDirectPipeline)) {
91541
92577
  return { path: projectDirectPipeline, source: "project" };
91542
92578
  }
@@ -91545,16 +92581,16 @@ function resolveWorkflowFile(cwd, file2) {
91545
92581
  const bundledRoot = bundledConfigRoot();
91546
92582
  if (bundledRoot !== null) {
91547
92583
  const bundledName = isYaml ? base2 : `${base2}.yaml`;
91548
- const bundledPath = join23(bundledRoot, "workflows", bundledName);
92584
+ const bundledPath = join26(bundledRoot, "workflows", bundledName);
91549
92585
  if (fs3.exists(bundledPath)) {
91550
92586
  return { path: bundledPath, source: "bundled" };
91551
92587
  }
91552
92588
  if (!isYaml) {
91553
- const bundledPipeline = join23(bundledRoot, "workflows", `${base2}-pipeline.yaml`);
92589
+ const bundledPipeline = join26(bundledRoot, "workflows", `${base2}-pipeline.yaml`);
91554
92590
  if (fs3.exists(bundledPipeline)) {
91555
92591
  return { path: bundledPipeline, source: "bundled" };
91556
92592
  }
91557
- const bundledLiteral = join23(bundledRoot, "workflows", base2);
92593
+ const bundledLiteral = join26(bundledRoot, "workflows", base2);
91558
92594
  if (fs3.exists(bundledLiteral)) {
91559
92595
  return { path: bundledLiteral, source: "bundled" };
91560
92596
  }
@@ -91602,7 +92638,7 @@ var init_workflow_resolver = __esm(() => {
91602
92638
  // ../../packages/app/src/services/workflow-service.ts
91603
92639
  import { realpathSync as realpathSync5 } from "fs";
91604
92640
  import { homedir as homedir8 } from "os";
91605
- import { basename as basename8, dirname as dirname17, join as join24, resolve as resolve13, sep as sep3 } from "path";
92641
+ import { basename as basename8, dirname as dirname18, isAbsolute as isAbsolute7, join as join27, resolve as resolve14, sep as sep5 } from "path";
91606
92642
  function signalSubprocess(pid) {
91607
92643
  if (!Number.isInteger(pid) || pid <= 1)
91608
92644
  return false;
@@ -91647,7 +92683,7 @@ function withSelfPidRecording(inner, db2) {
91647
92683
  }
91648
92684
  function withRunIdentityRecording(inner, db2, identity7) {
91649
92685
  const stamp = async (runId) => {
91650
- await new RunDao(db2).stampRunIdentity(runId, identity7.definitionDigest, identity7.workflowVersion);
92686
+ await new RunDao(db2).stampRunIdentity(runId, identity7.definitionDigest, identity7.workflowVersion, identity7.source);
91651
92687
  };
91652
92688
  return new Proxy(inner, {
91653
92689
  get(target, prop, receiver) {
@@ -91659,8 +92695,10 @@ function withRunIdentityRecording(inner, db2, identity7) {
91659
92695
  }
91660
92696
  if (prop === "createOrAttachRun") {
91661
92697
  return async (record2) => {
92698
+ const existing = await new RunDao(db2).traceRowById(record2.id);
91662
92699
  const result = await target.createOrAttachRun(record2);
91663
- await stamp(result.id);
92700
+ if (existing === undefined)
92701
+ await stamp(result.id);
91664
92702
  return result;
91665
92703
  };
91666
92704
  }
@@ -91761,7 +92799,8 @@ class WorkflowAppService {
91761
92799
  recordSelfPid: opts.recordSelfPid === true,
91762
92800
  events: eventsBus,
91763
92801
  steeringController: opts.steeringController,
91764
- extensions: { workflow, file: absolute }
92802
+ extensions: { workflow, file: absolute },
92803
+ definitionSource: { path: absolute, layer: resolved.layer, workdir: this.ctx.cwd }
91765
92804
  });
91766
92805
  const runId = opts.runId ?? crypto.randomUUID();
91767
92806
  const isDry = opts.dryRun === true;
@@ -91862,7 +92901,7 @@ class WorkflowAppService {
91862
92901
  };
91863
92902
  }
91864
92903
  async cleanRunLogs(retentionDays = 30, dryRun = false) {
91865
- const runDir = join24(this.ctx.cwd, ".spur", "run");
92904
+ const runDir = join27(this.ctx.cwd, ".spur", "run");
91866
92905
  const fs3 = createNodeFileSystem3();
91867
92906
  const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
91868
92907
  const reclaimed = [];
@@ -91874,7 +92913,7 @@ class WorkflowAppService {
91874
92913
  return { retentionDays, dryRun, reclaimed, failures };
91875
92914
  }
91876
92915
  for (const name of entries) {
91877
- const path9 = join24(runDir, name);
92916
+ const path9 = join27(runDir, name);
91878
92917
  const stat = await fs3.stat(path9);
91879
92918
  if (stat === null || !stat.isFile() || stat.mtimeMs >= cutoffMs)
91880
92919
  continue;
@@ -91893,13 +92932,13 @@ class WorkflowAppService {
91893
92932
  return { retentionDays, dryRun, reclaimed, failures };
91894
92933
  }
91895
92934
  async cleanCheckpoints(retentionDays = 30, dryRun = false) {
91896
- const sessionsDir = join24(this.ctx.cwd, ".spur", "memory", "sessions");
92935
+ const sessionsDir = join27(this.ctx.cwd, ".spur", "memory", "sessions");
91897
92936
  const fs3 = createNodeFileSystem3();
91898
92937
  let sessionsReal;
91899
92938
  try {
91900
92939
  sessionsReal = realpathSync5(sessionsDir);
91901
92940
  } catch {
91902
- sessionsReal = resolve13(sessionsDir);
92941
+ sessionsReal = resolve14(sessionsDir);
91903
92942
  }
91904
92943
  const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
91905
92944
  const reclaimed = [];
@@ -91913,14 +92952,14 @@ class WorkflowAppService {
91913
92952
  }
91914
92953
  const activeRunIds = new Set((await new RunDao(await this.ctx.getDb()).listActiveRuns()).map((r) => r.id));
91915
92954
  for (const name of entries) {
91916
- const path9 = join24(sessionsDir, name);
92955
+ const path9 = join27(sessionsDir, name);
91917
92956
  let resolved;
91918
92957
  try {
91919
92958
  resolved = realpathSync5(path9);
91920
92959
  } catch {
91921
- resolved = resolve13(path9);
92960
+ resolved = resolve14(path9);
91922
92961
  }
91923
- if (!resolved.startsWith(sessionsReal + sep3)) {
92962
+ if (!resolved.startsWith(sessionsReal + sep5)) {
91924
92963
  skipped.push({ name, reason: "path-confinement: resolved outside .spur/memory/sessions" });
91925
92964
  continue;
91926
92965
  }
@@ -91990,22 +93029,60 @@ class WorkflowAppService {
91990
93029
  if (row?.status !== "paused") {
91991
93030
  throw new Error(`Run "${runId}" is not paused (or does not exist) - nothing to continue.`);
91992
93031
  }
91993
- await this.validateResumeCheckpointFreshness(runId, row);
91994
- let resolved;
91995
- try {
91996
- resolved = await resolveWorkflowDefinition(this.ctx.cwd, row.workflow_name, {
91997
- validateSchema: true,
91998
- embeddedSchemas: this.ctx.embeddedSchemas?.()
91999
- });
92000
- } catch {
92001
- throw new Error(`Cannot resume run "${runId}": workflow definition "${row.workflow_name}" not found in the workflow search paths.`);
92002
- }
92003
93032
  let rawMeta = {};
92004
93033
  try {
92005
93034
  rawMeta = JSON.parse(row.metadata_json || "{}");
92006
93035
  } catch {
92007
93036
  rawMeta = {};
92008
93037
  }
93038
+ const warnings = [];
93039
+ const emitWarning = (message) => {
93040
+ warnings.push(message);
93041
+ try {
93042
+ this.ctx.warn?.(message);
93043
+ } catch {}
93044
+ };
93045
+ let pinned = null;
93046
+ const rawSource = rawMeta.definitionSource;
93047
+ if (rawSource !== undefined) {
93048
+ if (rawSource === null || typeof rawSource !== "object" || Array.isArray(rawSource)) {
93049
+ throw new Error(`Cannot resume run "${runId}": recorded definitionSource metadata is malformed (expected {path, layer, workdir}). Resume refused.`);
93050
+ }
93051
+ const candidate = rawSource;
93052
+ const sourcePath = candidate.path;
93053
+ const sourceLayer = candidate.layer;
93054
+ const sourceWorkdir = candidate.workdir;
93055
+ if (typeof sourcePath !== "string" || sourcePath === "" || sourceLayer !== "project" && sourceLayer !== "bundled" || typeof sourceWorkdir !== "string" || sourceWorkdir === "") {
93056
+ throw new Error(`Cannot resume run "${runId}": recorded definitionSource metadata is malformed (expected an absolute path, a project|bundled layer, and a workdir). Resume refused.`);
93057
+ }
93058
+ pinned = { path: sourcePath, layer: sourceLayer, workdir: sourceWorkdir };
93059
+ }
93060
+ const launchWorkdir = pinned?.workdir ?? this.ctx.cwd;
93061
+ await this.validateResumeCheckpointFreshness(runId, row, launchWorkdir);
93062
+ let resolved;
93063
+ if (pinned !== null) {
93064
+ const fs3 = createNodeFileSystem3();
93065
+ if (!fs3.exists(pinned.path)) {
93066
+ throw new Error(`Cannot resume run "${runId}": recorded launch source "${pinned.path}" no longer exists. Repair the source or start a new run; name-based fallback is refused.`);
93067
+ }
93068
+ resolved = await resolveWorkflowDefinition(pinned.workdir, pinned.path, {
93069
+ validateSchema: true,
93070
+ embeddedSchemas: this.ctx.embeddedSchemas?.()
93071
+ });
93072
+ if (resolved.path !== pinned.path) {
93073
+ throw new Error(`Cannot resume run "${runId}": definition resolved to "${resolved.path}" instead of the recorded launch source "${pinned.path}". Resume refused.`);
93074
+ }
93075
+ } else {
93076
+ try {
93077
+ resolved = await resolveWorkflowDefinition(this.ctx.cwd, row.workflow_name, {
93078
+ validateSchema: true,
93079
+ embeddedSchemas: this.ctx.embeddedSchemas?.()
93080
+ });
93081
+ } catch {
93082
+ throw new Error(`Cannot resume run "${runId}": workflow definition "${row.workflow_name}" not found in the workflow search paths.`);
93083
+ }
93084
+ emitWarning(`Run "${runId}" resumed by workflow name "${row.workflow_name}" (no recorded launch source \u2014 pre-source-pin run); identity checks run against the name-resolved definition.`);
93085
+ }
92009
93086
  const persistedDigest = typeof rawMeta.definitionDigest === "string" ? rawMeta.definitionDigest : null;
92010
93087
  const identityStamped = "workflowVersion" in rawMeta;
92011
93088
  const persistedVersion = identityStamped && (typeof rawMeta.workflowVersion === "string" || rawMeta.workflowVersion === null) ? rawMeta.workflowVersion : undefined;
@@ -92025,6 +93102,18 @@ class WorkflowAppService {
92025
93102
  throw new Error(`Cannot resume run "${runId}": workflow definition drift detected (launched with ${persistedDigest}, currently ${resolved.digest}). Resume refused.`);
92026
93103
  }
92027
93104
  }
93105
+ const db2 = await this.ctx.getDb();
93106
+ await new RunDao(db2).mergeMetadata(runId, {
93107
+ resumeDefinitionDigest: resolved.digest,
93108
+ resumeWorkflowVersion: workflowVersionLiteral(resolved.workflow)
93109
+ });
93110
+ emitWarning(`Consented definition drift on resume of run "${runId}": launched ${persistedDigest}, executing ${resolved.digest}; launch identity preserved, proof vars carry the executed digest.`);
93111
+ } else if ((("resumeDefinitionDigest" in rawMeta) || ("resumeWorkflowVersion" in rawMeta)) && (persistedDigest === null || persistedDigest === resolved.digest)) {
93112
+ const db2 = await this.ctx.getDb();
93113
+ await new RunDao(db2).mergeMetadata(runId, {
93114
+ resumeDefinitionDigest: null,
93115
+ resumeWorkflowVersion: null
93116
+ });
92028
93117
  }
92029
93118
  const svc = await this.createEngineService({
92030
93119
  events: eventsBus,
@@ -92032,10 +93121,15 @@ class WorkflowAppService {
92032
93121
  });
92033
93122
  const workflow = resolved.workflow;
92034
93123
  const hitlVar = opts?.hitlVar ?? "__hitlAnswer";
92035
- const resumeVars = opts?.hitlAnswer !== undefined ? { [hitlVar]: opts.hitlAnswer } : undefined;
93124
+ const resumeVars = {};
93125
+ if (opts?.hitlAnswer !== undefined)
93126
+ resumeVars[hitlVar] = opts.hitlAnswer;
93127
+ if (persistedDigest !== null && persistedDigest !== resolved.digest && "resumeDefinitionDigest" in rawMeta) {
93128
+ resumeVars.__definitionDigest = resolved.digest;
93129
+ }
92036
93130
  const result = await svc.resumeRun(workflow, runId, {
92037
- workdir: this.ctx.cwd,
92038
- ...resumeVars !== undefined ? { vars: resumeVars } : {},
93131
+ workdir: launchWorkdir,
93132
+ ...Object.keys(resumeVars).length > 0 ? { vars: resumeVars } : {},
92039
93133
  ...eventsBus !== undefined ? {
92040
93134
  events: withWorkflowIdentity(bridgeEventBus(eventsBus), workflow)
92041
93135
  } : {}
@@ -92045,6 +93139,8 @@ class WorkflowAppService {
92045
93139
  runResult.definitionDigest = persistedDigest;
92046
93140
  if (persistedVersion !== undefined)
92047
93141
  runResult.version = persistedVersion;
93142
+ if (warnings.length > 0)
93143
+ runResult.warnings = warnings;
92048
93144
  return runResult;
92049
93145
  }
92050
93146
  async stampFailureReason(runId, result) {
@@ -92054,8 +93150,8 @@ class WorkflowAppService {
92054
93150
  const db2 = await this.ctx.getDb();
92055
93151
  await new RunDao(db2).stampFailureReason(runId, result.reason);
92056
93152
  }
92057
- async validateResumeCheckpointFreshness(runId, row) {
92058
- const sessionsDir = join24(this.ctx.cwd, ".spur", "memory", "sessions");
93153
+ async validateResumeCheckpointFreshness(runId, row, workdir) {
93154
+ const sessionsDir = join27(workdir, ".spur", "memory", "sessions");
92059
93155
  const fs3 = createNodeFileSystem3();
92060
93156
  let entries;
92061
93157
  try {
@@ -92070,8 +93166,26 @@ class WorkflowAppService {
92070
93166
  rawMeta = {};
92071
93167
  }
92072
93168
  const wbs = typeof rawMeta.wbs === "string" ? rawMeta.wbs : undefined;
93169
+ const executor = this.ctx.processExecutor?.() ?? new NodeProcessExecutor3;
93170
+ let head = null;
93171
+ const resolveHead = async () => {
93172
+ if (head !== null)
93173
+ return head;
93174
+ const probe = await executor.run({
93175
+ command: "git",
93176
+ args: ["rev-parse", "HEAD"],
93177
+ cwd: workdir,
93178
+ forceBuffered: true,
93179
+ rejectOnError: false
93180
+ });
93181
+ if (probe.exitCode !== 0 || probe.stdout.trim() === "") {
93182
+ throw new Error(`Cannot resume run "${runId}": checkpoint freshness requires the launch workdir's git HEAD, but "git rev-parse HEAD" failed in "${workdir}". Refusing rather than skipping the commit-drift check.`);
93183
+ }
93184
+ head = probe.stdout.trim();
93185
+ return head;
93186
+ };
92073
93187
  for (const name of entries) {
92074
- const filePath = join24(sessionsDir, name);
93188
+ const filePath = join27(sessionsDir, name);
92075
93189
  let raw;
92076
93190
  try {
92077
93191
  raw = await fs3.readFile(filePath);
@@ -92090,10 +93204,20 @@ class WorkflowAppService {
92090
93204
  if (meta3.workflow !== "" && meta3.workflow !== row.workflow_name) {
92091
93205
  throw new Error(`Cannot resume run "${runId}": checkpoint is stale (workflow-mismatch: checkpoint workflow "${meta3.workflow}" != "${row.workflow_name}").`);
92092
93206
  }
92093
- if (meta3.status !== "" && meta3.status !== row.status) {
92094
- throw new Error(`Cannot resume run "${runId}": checkpoint is stale (status-mismatch: checkpoint status "${meta3.status}" does not match run status "${row.status}").`);
93207
+ if (meta3.status === "") {
93208
+ throw new Error(`Cannot resume run "${runId}": checkpoint is stale (status-missing: checkpoint carries no status).`);
92095
93209
  }
92096
- const staleness = checkpointStaleness(meta3);
93210
+ if (isTerminalCheckpointStatus(meta3.status)) {
93211
+ throw new Error(`Cannot resume run "${runId}": checkpoint is stale (terminal: status=${meta3.status}).`);
93212
+ }
93213
+ if (meta3.status !== "pending" && meta3.status !== "running" && meta3.status !== "approved") {
93214
+ throw new Error(`Cannot resume run "${runId}": checkpoint is stale (unknown checkpoint status "${meta3.status}" \u2014 expected pending/running/approved).`);
93215
+ }
93216
+ const staleness = checkpointStaleness(meta3, {
93217
+ ...wbs !== undefined ? { taskWbs: wbs } : {},
93218
+ ...meta3.sourceCommit === "" ? {} : { sourceCommit: await resolveHead() },
93219
+ artifactExists: (p) => fs3.exists(isAbsolute7(p) ? p : join27(workdir, p))
93220
+ });
92097
93221
  if (staleness.stale) {
92098
93222
  throw new Error(`Cannot resume run "${runId}": checkpoint is stale (${staleness.reason}).`);
92099
93223
  }
@@ -92102,12 +93226,12 @@ class WorkflowAppService {
92102
93226
  }
92103
93227
  async list(workflowPaths = [".spur/workflows/"]) {
92104
93228
  const projectRoot = this.ctx.cwd;
92105
- const globalRoot = join24(homedir8(), ".config", "spur");
93229
+ const globalRoot = join27(homedir8(), ".config", "spur");
92106
93230
  const layers = [];
92107
93231
  const entries = [];
92108
93232
  const scannedPaths = new Set;
92109
93233
  for (const relPath of workflowPaths) {
92110
- const absPath = resolve13(projectRoot, relPath);
93234
+ const absPath = resolve14(projectRoot, relPath);
92111
93235
  layers.push({ id: "project", path: absPath });
92112
93236
  scannedPaths.add(absPath);
92113
93237
  try {
@@ -92116,7 +93240,7 @@ class WorkflowAppService {
92116
93240
  } catch {}
92117
93241
  }
92118
93242
  for (const relPath of workflowPaths) {
92119
- const absPath = resolve13(globalRoot, relPath);
93243
+ const absPath = resolve14(globalRoot, relPath);
92120
93244
  if (scannedPaths.has(absPath))
92121
93245
  continue;
92122
93246
  layers.push({ id: "global", path: absPath });
@@ -92302,14 +93426,15 @@ class WorkflowAppService {
92302
93426
  if (opts.extensions !== undefined) {
92303
93427
  persistence2 = withRunIdentityRecording(persistence2, db2, {
92304
93428
  definitionDigest: computeDefinitionDigest(opts.extensions.workflow),
92305
- workflowVersion: workflowVersionLiteral(opts.extensions.workflow)
93429
+ workflowVersion: workflowVersionLiteral(opts.extensions.workflow),
93430
+ ...opts.definitionSource !== undefined ? { source: opts.definitionSource } : {}
92306
93431
  });
92307
93432
  }
92308
93433
  const adapter = bus ? new ObservableWorkflowAdapter(persistence2, opts.extensions !== undefined ? withWorkflowIdentity(bus, opts.extensions.workflow) : bus) : persistence2;
92309
93434
  return new WorkflowService(host, adapter);
92310
93435
  }
92311
93436
  async loadWorkflowExtensions(host, workflow, file2) {
92312
- const refs = collectWorkflowExtensions(workflow.name, dirname17(file2), workflow.extensions);
93437
+ const refs = collectWorkflowExtensions(workflow.name, dirname18(file2), workflow.extensions);
92313
93438
  if (refs.length === 0)
92314
93439
  return;
92315
93440
  const nodeFs = createNodeFileSystem3();
@@ -92576,8 +93701,8 @@ function resolveOutputLogConfig(config4) {
92576
93701
  };
92577
93702
  }
92578
93703
  async function outputArtifactForRun(cwd, runId) {
92579
- const relative5 = join24(".spur", "run", `${runId}.log`);
92580
- return await fileExists(join24(cwd, relative5)) ? relative5 : undefined;
93704
+ const relative6 = join27(".spur", "run", `${runId}.log`);
93705
+ return await fileExists(join27(cwd, relative6)) ? relative6 : undefined;
92581
93706
  }
92582
93707
  async function scanWorkflowFiles(rootPath, source, loadEntry) {
92583
93708
  const entries = [];
@@ -92590,7 +93715,7 @@ async function scanWorkflowFiles(rootPath, source, loadEntry) {
92590
93715
  async function walk3(dirPath) {
92591
93716
  const names = await fs3.readDir(dirPath);
92592
93717
  for (const name of names) {
92593
- const fullPath = resolve13(dirPath, name);
93718
+ const fullPath = resolve14(dirPath, name);
92594
93719
  const st = await fs3.stat(fullPath);
92595
93720
  if (!st)
92596
93721
  continue;
@@ -92762,8 +93887,8 @@ function projectActionTraceResult(resultJson, secretValues = []) {
92762
93887
  async function partialArtifactForAction(cwd, runId, node, ok) {
92763
93888
  if (ok !== false || !TRACE_IDENTIFIER.test(runId) || !TRACE_IDENTIFIER.test(node))
92764
93889
  return;
92765
- const relativePath2 = join24(".spur", "run", `${runId}-${node}-partial.md`);
92766
- return await fileExists(resolve13(cwd, relativePath2)) ? relativePath2 : undefined;
93890
+ const relativePath2 = join27(".spur", "run", `${runId}-${node}-partial.md`);
93891
+ return await fileExists(resolve14(cwd, relativePath2)) ? relativePath2 : undefined;
92767
93892
  }
92768
93893
  var TASK_PIPELINE_WORKFLOW = "task-pipeline", PIPELINE_LINK_KIND = "pipeline", TRACE_IDENTIFIER, TRACE_RESULT_FIELDS, TRACE_INVOCATION_FIELDS;
92769
93894
  var init_workflow_service = __esm(() => {
@@ -92801,7 +93926,7 @@ var init_workflow_service = __esm(() => {
92801
93926
  });
92802
93927
 
92803
93928
  // ../../packages/app/src/workflow/idea-handoff.ts
92804
- import { resolve as resolve14 } from "path";
93929
+ import { resolve as resolve15 } from "path";
92805
93930
  function processEvidence(r) {
92806
93931
  return `exit=${r.exitCode ?? "null"}${r.signal ? ` signal=${r.signal}` : ""}: ${r.stderr.trim() || "no stderr"}`;
92807
93932
  }
@@ -92811,11 +93936,12 @@ async function finalizeIdeaHandoff(options) {
92811
93936
  const executor = options.processExecutor ?? new NodeProcessExecutor3;
92812
93937
  const spurBin = options.spurBin ?? "spur";
92813
93938
  const { runId, featureId: featureId2 } = options;
92814
- const runDir = resolve14(root, ".spur", "run");
92815
- const batchPath = resolve14(runDir, `${runId}-idea-task-batch.json`);
92816
- const resultPath = resolve14(runDir, `${runId}-idea-batch-create-result.json`);
92817
- const orderPath = resolve14(runDir, `${runId}-idea-task-order.json`);
92818
- const reportPath = resolve14(runDir, `${runId}-idea-handoff.md`);
93939
+ const runDir = resolve15(root, ".spur", "run");
93940
+ const batchPath = resolve15(runDir, `${runId}-idea-task-batch.json`);
93941
+ const resultPath = resolve15(runDir, `${runId}-idea-batch-create-result.json`);
93942
+ const orderPath = resolve15(runDir, `${runId}-idea-task-order.json`);
93943
+ const readyPath = resolve15(runDir, `${runId}-idea-ready.json`);
93944
+ const reportPath = resolve15(runDir, `${runId}-idea-handoff.md`);
92819
93945
  const spurSplit = splitLaunchCommand(spurBin, 'idea-handoff "spurBin"');
92820
93946
  if ("error" in spurSplit) {
92821
93947
  return {
@@ -92841,6 +93967,23 @@ async function finalizeIdeaHandoff(options) {
92841
93967
  const batch = JSON.parse(await fs3.readFile(batchPath));
92842
93968
  const result = JSON.parse(await fs3.readFile(resultPath));
92843
93969
  const order = JSON.parse(await fs3.readFile(orderPath));
93970
+ const sidecarExists = await fs3.exists(readyPath);
93971
+ const evidenceRows = await (async () => {
93972
+ if (!sidecarExists)
93973
+ return [];
93974
+ try {
93975
+ const parsed = JSON.parse(await fs3.readFile(readyPath));
93976
+ const tasks = typeof parsed === "object" && parsed !== null && "tasks" in parsed ? parsed.tasks : undefined;
93977
+ return Array.isArray(tasks) ? tasks : [];
93978
+ } catch {
93979
+ return [];
93980
+ }
93981
+ })();
93982
+ const evidenceByWbs = new Map;
93983
+ for (const row of evidenceRows) {
93984
+ if (typeof row?.wbs === "string")
93985
+ evidenceByWbs.set(row.wbs, row);
93986
+ }
92844
93987
  if (!Array.isArray(batch) || !Array.isArray(result?.wbs) || !Array.isArray(order)) {
92845
93988
  return {
92846
93989
  ok: false,
@@ -92932,23 +94075,92 @@ async function finalizeIdeaHandoff(options) {
92932
94075
  }
92933
94076
  const checkResults = [];
92934
94077
  for (const wbs of result.wbs) {
92935
- const checkRes = await executor.run({
94078
+ const pathRes = await executor.run({
92936
94079
  command: spurCommand,
92937
- args: [...spurArgsPrefix, "task", "check", wbs, "--json"],
94080
+ args: [...spurArgsPrefix, "task", "path", wbs, "--json"],
92938
94081
  cwd: root,
92939
94082
  forceBuffered: true,
92940
94083
  rejectOnError: false
92941
94084
  });
92942
- if (checkRes.exitCode === null) {
94085
+ if (pathRes.exitCode === null) {
92943
94086
  return {
92944
94087
  ok: false,
92945
94088
  wbsList: result.wbs,
92946
94089
  nextCommand: "",
92947
94090
  reportPath,
92948
- error: `Task check for ${wbs} could not be spawned (${spurCommand}): ${processEvidence(checkRes)}`
94091
+ error: `Task path for ${wbs} could not be spawned (${spurCommand}): ${processEvidence(pathRes)}`
92949
94092
  };
92950
94093
  }
92951
- checkResults.push({ wbs, pass: checkRes.exitCode === 0 });
94094
+ let reason;
94095
+ if (pathRes.exitCode !== 0) {
94096
+ reason = `task path resolution failed: ${processEvidence(pathRes)}`;
94097
+ } else {
94098
+ let filePath;
94099
+ try {
94100
+ const parsed = JSON.parse(pathRes.stdout);
94101
+ if (typeof parsed === "object" && parsed !== null && "filePath" in parsed) {
94102
+ const fp = parsed.filePath;
94103
+ if (typeof fp === "string")
94104
+ filePath = fp;
94105
+ }
94106
+ } catch {
94107
+ filePath = undefined;
94108
+ }
94109
+ if (filePath === undefined) {
94110
+ reason = "task path output carried no filePath";
94111
+ } else {
94112
+ let digest;
94113
+ try {
94114
+ digest = computePlanningDigest(await fs3.readFile(filePath));
94115
+ } catch {
94116
+ digest = undefined;
94117
+ }
94118
+ if (digest === undefined) {
94119
+ reason = `task file unreadable at ${filePath}`;
94120
+ } else {
94121
+ const row = evidenceByWbs.get(wbs);
94122
+ if (row === undefined) {
94123
+ reason = "no ready evidence recorded for this run";
94124
+ } else if (row.status !== "ready") {
94125
+ reason = `ready evidence status is ${String(row.status)}`;
94126
+ } else if (typeof row.planningDigest !== "string" || row.planningDigest !== digest) {
94127
+ reason = "planning digest stale \u2014 task content changed after preparation";
94128
+ } else {
94129
+ const verdict = verifyReadyChecks(Array.isArray(row.checks) ? row.checks : undefined);
94130
+ if (!verdict.ok)
94131
+ reason = verdict.reason;
94132
+ }
94133
+ }
94134
+ }
94135
+ }
94136
+ if (reason === undefined) {
94137
+ const checkRes = await executor.run({
94138
+ command: spurCommand,
94139
+ args: [...spurArgsPrefix, "task", "check", wbs, "--json"],
94140
+ cwd: root,
94141
+ forceBuffered: true,
94142
+ rejectOnError: false
94143
+ });
94144
+ if (checkRes.exitCode === null) {
94145
+ return {
94146
+ ok: false,
94147
+ wbsList: result.wbs,
94148
+ nextCommand: "",
94149
+ reportPath,
94150
+ error: `Task check for ${wbs} could not be spawned (${spurCommand}): ${processEvidence(checkRes)}`
94151
+ };
94152
+ }
94153
+ if (checkRes.exitCode !== 0) {
94154
+ reason = `deterministic task check failed (${processEvidence(checkRes)})`;
94155
+ }
94156
+ }
94157
+ const pass = reason === undefined;
94158
+ checkResults.push({
94159
+ wbs,
94160
+ pass,
94161
+ status: pass ? "ready" : "unready",
94162
+ ...pass ? {} : { reason, action: readyRefineCommand(wbs) }
94163
+ });
92952
94164
  }
92953
94165
  const anyFailed = checkResults.some((c3) => !c3.pass);
92954
94166
  const nextCommand = anyFailed ? `/sp:dev-refineall --feature ${featureId2} --auto --depth ready` : `/sp:dev-runall --feature ${featureId2} --auto`;
@@ -92961,12 +94173,18 @@ async function finalizeIdeaHandoff(options) {
92961
94173
  "## Created tasks",
92962
94174
  ...result.wbs.map((w) => ` - ${w}`),
92963
94175
  "",
92964
- "## Per-task readiness (spur task check)",
94176
+ "## Per-task readiness (evidence + digest + task check)",
92965
94177
  "",
92966
- "| WBS | Outcome |",
92967
- "|-----|---------|",
92968
- ...checkResults.map((c3) => `| ${c3.wbs} | ${c3.pass ? "PASS" : "FAIL"} |`),
94178
+ "| WBS | Outcome | Reason |",
94179
+ "|-----|---------|--------|",
94180
+ ...checkResults.map((c3) => `| ${c3.wbs} | ${c3.pass ? "READY" : "UNREADY"} | ${c3.reason ?? "-"} |`),
92969
94181
  "",
94182
+ ...anyFailed ? [
94183
+ "## Preparation actions",
94184
+ "",
94185
+ ...checkResults.filter((c3) => !c3.pass).map((c3) => ` - ${c3.wbs}: ${c3.action}`),
94186
+ ""
94187
+ ] : [],
92970
94188
  "## Next command",
92971
94189
  "",
92972
94190
  nextCommand,
@@ -92993,6 +94211,7 @@ async function finalizeIdeaHandoff(options) {
92993
94211
  }
92994
94212
  var init_idea_handoff = __esm(() => {
92995
94213
  init_dist5();
94214
+ init_task_readiness();
92996
94215
  init_split_launch_command();
92997
94216
  });
92998
94217
 
@@ -93515,7 +94734,7 @@ var init_progress_follow = __esm(() => {
93515
94734
  });
93516
94735
 
93517
94736
  // ../../packages/app/src/workflow/trace-writer.ts
93518
- import { dirname as dirname18, join as join25 } from "path";
94737
+ import { dirname as dirname19, join as join28 } from "path";
93519
94738
 
93520
94739
  class WorkflowTraceWriter {
93521
94740
  path;
@@ -93523,7 +94742,7 @@ class WorkflowTraceWriter {
93523
94742
  pending = Promise.resolve();
93524
94743
  constructor(cwd, runId) {
93525
94744
  const safeRunId = runId.replace(/[^A-Za-z0-9._-]/g, "_");
93526
- this.path = join25(cwd, ".spur", "runs", "workflow", `${safeRunId}.jsonl`);
94745
+ this.path = join28(cwd, ".spur", "runs", "workflow", `${safeRunId}.jsonl`);
93527
94746
  }
93528
94747
  attach(bus) {
93529
94748
  bus.on("workflow.run.started", (event) => this.enqueue("workflow.run.started", event));
@@ -93549,7 +94768,7 @@ class WorkflowTraceWriter {
93549
94768
  `;
93550
94769
  this.pending = this.pending.then(async () => {
93551
94770
  const fs3 = createNodeFileSystem3();
93552
- await fs3.ensureDir(dirname18(this.path));
94771
+ await fs3.ensureDir(dirname19(this.path));
93553
94772
  await fs3.appendFile(this.path, line);
93554
94773
  });
93555
94774
  }
@@ -93563,6 +94782,7 @@ var exports_src2 = {};
93563
94782
  __export(exports_src2, {
93564
94783
  writeJsonError: () => writeJsonError,
93565
94784
  waitForOccupant: () => waitForOccupant,
94785
+ verifyReadyChecks: () => verifyReadyChecks,
93566
94786
  verdictRowsMatchScenarios: () => verdictRowsMatchScenarios,
93567
94787
  validateSchedulerCustomJobPayload: () => validateSchedulerCustomJobPayload,
93568
94788
  validateHistoryRefreshPayload: () => validateHistoryRefreshPayload,
@@ -93605,6 +94825,8 @@ __export(exports_src2, {
93605
94825
  registerSpurBuiltins: () => registerSpurBuiltins,
93606
94826
  refreshHistoryRollups: () => refreshHistoryRollups,
93607
94827
  redactAndBound: () => redactAndBound,
94828
+ readyRefineCommand: () => readyRefineCommand,
94829
+ readyRefineAllCommand: () => readyRefineAllCommand,
93608
94830
  readVerdictArtifact: () => readVerdictArtifact,
93609
94831
  readVerdict: () => readVerdict,
93610
94832
  qualifySectionBody: () => qualifySectionBody,
@@ -93615,6 +94837,8 @@ __export(exports_src2, {
93615
94837
  projectStoredSystemEventEnvelope: () => projectStoredSystemEventEnvelope,
93616
94838
  projectLifecycle: () => projectLifecycle,
93617
94839
  probePort: () => probePort,
94840
+ prepareCreatedTaskReady: () => prepareCreatedTaskReady,
94841
+ prepareBatchTaskReady: () => prepareBatchTaskReady,
93618
94842
  portBindingAvailable: () => portBindingAvailable,
93619
94843
  permissionFailureEvidence: () => permissionFailureEvidence,
93620
94844
  parseVerdict: () => parseVerdict,
@@ -93654,6 +94878,7 @@ __export(exports_src2, {
93654
94878
  extractResolvedWorkflowFacts: () => extractResolvedWorkflowFacts,
93655
94879
  extractProofDigest: () => extractProofDigest,
93656
94880
  extractFeatureProofData: () => extractFeatureProofData,
94881
+ extractBatchArray: () => extractBatchArray,
93657
94882
  evaluateDoneTransition: () => evaluateDoneTransition,
93658
94883
  escalationFingerprint: () => escalationFingerprint,
93659
94884
  envelopeEnabled: () => envelopeEnabled,
@@ -93670,6 +94895,7 @@ __export(exports_src2, {
93670
94895
  createFileAgentInstanceStore: () => createFileAgentInstanceStore,
93671
94896
  configuredSecretValues: () => configuredSecretValues,
93672
94897
  computeProofInputFingerprint: () => computeProofInputFingerprint,
94898
+ computePlanningDigest: () => computePlanningDigest,
93673
94899
  computeExitCode: () => computeExitCode,
93674
94900
  computeDefinitionDigest: () => computeDefinitionDigest,
93675
94901
  computeAggregate: () => computeAggregate,
@@ -93702,8 +94928,10 @@ __export(exports_src2, {
93702
94928
  TeamService: () => TeamService,
93703
94929
  TaskService: () => TaskService,
93704
94930
  TaskScaffoldService: () => TaskScaffoldService,
94931
+ TaskPreparationError: () => TaskPreparationError,
93705
94932
  TaskLocator: () => TaskLocator,
93706
94933
  TaskCheckService: () => TaskCheckService,
94934
+ TaskCandidateInvalidError: () => TaskCandidateInvalidError,
93707
94935
  TOKEN_LEDGER_RELATIVE_PATH: () => TOKEN_LEDGER_RELATIVE_PATH,
93708
94936
  TOKEN_LEDGER_MAX_LIMIT: () => TOKEN_LEDGER_MAX_LIMIT,
93709
94937
  TOKEN_LEDGER_DEFAULT_LIMIT: () => TOKEN_LEDGER_DEFAULT_LIMIT,
@@ -93735,6 +94963,9 @@ __export(exports_src2, {
93735
94963
  RUN_STORE_WBS_DEFAULT_LIMIT: () => RUN_STORE_WBS_DEFAULT_LIMIT,
93736
94964
  RUN_STORE_LIST_MAX_LIMIT: () => RUN_STORE_LIST_MAX_LIMIT,
93737
94965
  RUN_STORE_LIST_DEFAULT_LIMIT: () => RUN_STORE_LIST_DEFAULT_LIMIT,
94966
+ READY_SKIPPED: () => READY_SKIPPED,
94967
+ READY_DONE: () => READY_DONE,
94968
+ READY_CHECKLIST_IDS: () => READY_CHECKLIST_IDS,
93738
94969
  ProofInputFingerprint: () => ProofInputFingerprint,
93739
94970
  ProofCaptureError: () => ProofCaptureError,
93740
94971
  ProjectRegistry: () => ProjectRegistry,
@@ -93744,6 +94975,7 @@ __export(exports_src2, {
93744
94975
  PS_LIST_ARGV: () => PS_LIST_ARGV,
93745
94976
  POLL_INTERVAL_MS: () => POLL_INTERVAL_MS,
93746
94977
  PLANNING_EVENT_NAMES: () => PLANNING_EVENT_NAMES,
94978
+ PLANNING_DIGEST_SECTIONS: () => PLANNING_DIGEST_SECTIONS,
93747
94979
  ObservableWorkflowAdapter: () => ObservableWorkflowAdapter,
93748
94980
  MockHistoryBoardService: () => MockHistoryBoardService,
93749
94981
  MIN_SAFE_PI_BASH_IMPORTER_VERSION: () => MIN_SAFE_PI_BASH_IMPORTER_VERSION,
@@ -93776,6 +95008,7 @@ __export(exports_src2, {
93776
95008
  DEFAULT_SYSTEM_EVENT_RETENTION_QUOTA: () => DEFAULT_SYSTEM_EVENT_RETENTION_QUOTA,
93777
95009
  DEFAULT_STALL_MS: () => DEFAULT_STALL_MS,
93778
95010
  DEFAULT_RUN_LOG_MAX_BYTES: () => DEFAULT_RUN_LOG_MAX_BYTES,
95011
+ DEFAULT_READY_PREPARE_TIMEOUT_MS: () => DEFAULT_READY_PREPARE_TIMEOUT_MS,
93779
95012
  DEFAULT_FEATURE_MATRIX: () => DEFAULT_FEATURE_MATRIX,
93780
95013
  CorpusMigrator: () => CorpusMigrator,
93781
95014
  CommandGateActionRunner: () => CommandGateActionRunner,
@@ -93821,6 +95054,7 @@ var init_src3 = __esm(async () => {
93821
95054
  init_rule_service();
93822
95055
  init_run_session_observer();
93823
95056
  init_run_store_service();
95057
+ init_scheduler_custom_job_service();
93824
95058
  init_supervisor_service();
93825
95059
  init_system_event_emitter();
93826
95060
  init_system_event_follow();
@@ -93829,6 +95063,7 @@ var init_src3 = __esm(async () => {
93829
95063
  init_task_attribution2();
93830
95064
  init_task_check();
93831
95065
  init_task_locator();
95066
+ init_task_readiness();
93832
95067
  init_task_record();
93833
95068
  init_task_scaffold();
93834
95069
  init_task_service();
@@ -101924,15 +103159,15 @@ function parseTimeout(raw) {
101924
103159
  return n3;
101925
103160
  }
101926
103161
  function loopSleep(ms, signal) {
101927
- return new Promise((resolve15) => {
103162
+ return new Promise((resolve16) => {
101928
103163
  if (signal?.aborted) {
101929
- resolve15();
103164
+ resolve16();
101930
103165
  return;
101931
103166
  }
101932
- const timer = setTimeout(resolve15, ms);
103167
+ const timer = setTimeout(resolve16, ms);
101933
103168
  signal?.addEventListener("abort", () => {
101934
103169
  clearTimeout(timer);
101935
- resolve15();
103170
+ resolve16();
101936
103171
  }, { once: true });
101937
103172
  });
101938
103173
  }
@@ -102064,7 +103299,7 @@ function waitFail(context4, options, code, message) {
102064
103299
  // src/release-ops.ts
102065
103300
  init_dist5();
102066
103301
  import { existsSync as existsSync10, readdirSync as readdirSync7, readFileSync as readFileSync10 } from "fs";
102067
- import { join as join26 } from "path";
103302
+ import { join as join29 } from "path";
102068
103303
  var SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
102069
103304
  function releaseTag(config4, version3) {
102070
103305
  return `${config4.packageName}${config4.tagVersionSeparator}${version3}`;
@@ -102110,7 +103345,7 @@ async function readJson(path9) {
102110
103345
  function rootWorkspaceGlobs(repoRoot) {
102111
103346
  let root;
102112
103347
  try {
102113
- root = JSON.parse(readFileSync10(join26(repoRoot, "package.json"), "utf8"));
103348
+ root = JSON.parse(readFileSync10(join29(repoRoot, "package.json"), "utf8"));
102114
103349
  } catch {
102115
103350
  return [];
102116
103351
  }
@@ -102127,11 +103362,11 @@ function workspaceDirs(repoRoot) {
102127
103362
  if (!glob.endsWith("/*"))
102128
103363
  continue;
102129
103364
  const parent = glob.slice(0, -2);
102130
- const parentAbs = join26(repoRoot, parent);
103365
+ const parentAbs = join29(repoRoot, parent);
102131
103366
  if (!existsSync10(parentAbs))
102132
103367
  continue;
102133
103368
  for (const entry of readdirSync7(parentAbs, { withFileTypes: true })) {
102134
- if (entry.isDirectory() && existsSync10(join26(parentAbs, entry.name, "package.json"))) {
103369
+ if (entry.isDirectory() && existsSync10(join29(parentAbs, entry.name, "package.json"))) {
102135
103370
  dirs.push(`${parent}/${entry.name}`);
102136
103371
  }
102137
103372
  }
@@ -102139,14 +103374,14 @@ function workspaceDirs(repoRoot) {
102139
103374
  return dirs;
102140
103375
  }
102141
103376
  async function releaseContext(repoRoot) {
102142
- const rootManifest = await readJson(join26(repoRoot, "package.json"));
103377
+ const rootManifest = await readJson(join29(repoRoot, "package.json"));
102143
103378
  if (rootManifest === null) {
102144
103379
  throw new Error(`no package.json found at ${repoRoot} \u2014 builder operates on a package workspace root.`);
102145
103380
  }
102146
103381
  const rootName = typeof rootManifest.name === "string" ? rootManifest.name : "root";
102147
103382
  const workspaces2 = [];
102148
103383
  for (const dir of workspaceDirs(repoRoot)) {
102149
- const manifest = await readJson(join26(repoRoot, dir, "package.json"));
103384
+ const manifest = await readJson(join29(repoRoot, dir, "package.json"));
102150
103385
  if (manifest === null)
102151
103386
  continue;
102152
103387
  workspaces2.push({
@@ -102159,7 +103394,7 @@ async function releaseContext(repoRoot) {
102159
103394
  const shortName = (name) => name.split("/").pop() ?? name;
102160
103395
  const packages = new Map;
102161
103396
  for (const ws of workspaces2) {
102162
- const versionSourceFile = existsSync10(join26(repoRoot, ws.dir, "src", "config.ts")) ? `${ws.dir}/src/config.ts` : undefined;
103397
+ const versionSourceFile = existsSync10(join29(repoRoot, ws.dir, "src", "config.ts")) ? `${ws.dir}/src/config.ts` : undefined;
102163
103398
  packages.set(shortName(ws.name), {
102164
103399
  packageDir: ws.dir,
102165
103400
  packageName: ws.name,
@@ -102194,7 +103429,7 @@ async function updateWorkspacePins(ctx, pkgName, oldVersion, newVersion, output2
102194
103429
  const changed = [];
102195
103430
  for (const dir of workspaceDirs(ctx.repoRoot)) {
102196
103431
  const relPath = `${dir}/package.json`;
102197
- const manifest = await readJson(join26(ctx.repoRoot, relPath));
103432
+ const manifest = await readJson(join29(ctx.repoRoot, relPath));
102198
103433
  if (manifest === null)
102199
103434
  continue;
102200
103435
  let dirty = false;
@@ -102211,7 +103446,7 @@ async function updateWorkspacePins(ctx, pkgName, oldVersion, newVersion, output2
102211
103446
  }
102212
103447
  }
102213
103448
  if (dirty) {
102214
- await Bun.write(join26(ctx.repoRoot, relPath), `${JSON.stringify(manifest, null, 4)}
103449
+ await Bun.write(join29(ctx.repoRoot, relPath), `${JSON.stringify(manifest, null, 4)}
102215
103450
  `);
102216
103451
  output2.write(` \u21B3 ${relPath}: ${pkgName} workspace pin ${oldVersion} \u2192 ${newVersion}`);
102217
103452
  changed.push(relPath);
@@ -102220,7 +103455,7 @@ async function updateWorkspacePins(ctx, pkgName, oldVersion, newVersion, output2
102220
103455
  return changed;
102221
103456
  }
102222
103457
  async function updateVersionSourceFile(ctx, filePath, previous, next, output2) {
102223
- const absPath = join26(ctx.repoRoot, filePath);
103458
+ const absPath = join29(ctx.repoRoot, filePath);
102224
103459
  const content = await Bun.file(absPath).text();
102225
103460
  const updated = content.replace(/(binaryVersion:\s*['"])\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?(['"])/, `$1${next}$2`);
102226
103461
  if (updated === content) {
@@ -102232,7 +103467,7 @@ async function updateVersionSourceFile(ctx, filePath, previous, next, output2) {
102232
103467
  return true;
102233
103468
  }
102234
103469
  async function syncMarketplaceAndPlugins(ctx, version3, staged, output2) {
102235
- const marketplacePath = join26(ctx.repoRoot, ".claude-plugin", "marketplace.json");
103470
+ const marketplacePath = join29(ctx.repoRoot, ".claude-plugin", "marketplace.json");
102236
103471
  const marketplaceFile = Bun.file(marketplacePath);
102237
103472
  if (!await marketplaceFile.exists())
102238
103473
  return;
@@ -102262,7 +103497,7 @@ async function syncMarketplaceAndPlugins(ctx, version3, staged, output2) {
102262
103497
  output2.write(`Bumped marketplace plugins to ${version3}`);
102263
103498
  }
102264
103499
  for (const entry of plugins) {
102265
- const pluginJsonPath = join26(ctx.repoRoot, entry.source, "plugin.json");
103500
+ const pluginJsonPath = join29(ctx.repoRoot, entry.source, "plugin.json");
102266
103501
  const pluginJson = await readJson(pluginJsonPath);
102267
103502
  if (pluginJson === null) {
102268
103503
  output2.write(` \u26A0 plugin.json not found at ${entry.source}/plugin.json \u2014 skipping`);
@@ -102311,7 +103546,7 @@ async function bumpVersion(ctx, config4, version3, options, output2) {
102311
103546
  if (await npmViewVersion(ctx.repoRoot, config4.packageName, version3)) {
102312
103547
  throw new Error(`${config4.packageName}@${version3} is already published on npm. Use a new version.`);
102313
103548
  }
102314
- const manifestPath = join26(ctx.repoRoot, config4.packageDir, "package.json");
103549
+ const manifestPath = join29(ctx.repoRoot, config4.packageDir, "package.json");
102315
103550
  const manifest = await readJson(manifestPath);
102316
103551
  if (manifest === null)
102317
103552
  throw new Error(`missing or malformed manifest at ${config4.packageDir}/package.json`);
@@ -102328,7 +103563,7 @@ async function bumpVersion(ctx, config4, version3, options, output2) {
102328
103563
  }
102329
103564
  staged.push(...await updateWorkspacePins(ctx, config4.packageName, previous, version3, output2));
102330
103565
  await syncMarketplaceAndPlugins(ctx, version3, staged, output2);
102331
- const lockPath = join26(ctx.repoRoot, "bun.lock");
103566
+ const lockPath = join29(ctx.repoRoot, "bun.lock");
102332
103567
  if (existsSync10(lockPath) && Bun.file(lockPath).size > 0)
102333
103568
  staged.push("bun.lock");
102334
103569
  await git2(ctx.repoRoot, ["add", ...staged]);
@@ -102384,7 +103619,7 @@ async function bumpAll(ctx, version3, options, output2) {
102384
103619
  }
102385
103620
  const staged = [];
102386
103621
  for (const config4 of configs) {
102387
- const manifestPath = join26(ctx.repoRoot, config4.packageDir, "package.json");
103622
+ const manifestPath = join29(ctx.repoRoot, config4.packageDir, "package.json");
102388
103623
  const manifest = await readJson(manifestPath);
102389
103624
  if (manifest === null) {
102390
103625
  throw new Error(`missing or malformed manifest at ${config4.packageDir}/package.json`);
@@ -102403,7 +103638,7 @@ async function bumpAll(ctx, version3, options, output2) {
102403
103638
  staged.push(...await updateWorkspacePins(ctx, config4.packageName, previous, version3, output2));
102404
103639
  }
102405
103640
  await syncMarketplaceAndPlugins(ctx, version3, staged, output2);
102406
- const lockPath = join26(ctx.repoRoot, "bun.lock");
103641
+ const lockPath = join29(ctx.repoRoot, "bun.lock");
102407
103642
  if (existsSync10(lockPath) && Bun.file(lockPath).size > 0)
102408
103643
  staged.push("bun.lock");
102409
103644
  await git2(ctx.repoRoot, ["add", ...staged]);
@@ -103064,7 +104299,7 @@ import { createRequire } from "module";
103064
104299
  var CLI_CONFIG = {
103065
104300
  binaryName: "spur",
103066
104301
  binaryLabel: "spur",
103067
- binaryVersion: "0.3.74",
104302
+ binaryVersion: "0.3.75",
103068
104303
  configDir: ".spur",
103069
104304
  configFile: ".spur/config.yaml",
103070
104305
  databaseFile: ".spur/spur.db"
@@ -103449,7 +104684,7 @@ init_loader();
103449
104684
  init_src2();
103450
104685
  init_dist10();
103451
104686
  import { homedir as homedir9 } from "os";
103452
- import { join as join27, resolve as resolve15 } from "path";
104687
+ import { join as join30, resolve as resolve16 } from "path";
103453
104688
 
103454
104689
  // src/config/scaffold-manifest.ts
103455
104690
  var SCAFFOLD_MANIFEST = [
@@ -103478,8 +104713,8 @@ var SCAFFOLD_MANIFEST = [
103478
104713
  ];
103479
104714
 
103480
104715
  // src/commands/init.ts
103481
- var GLOBAL_CONFIG_DIR = join27(".config", "spur");
103482
- var GLOBAL_RULES_DIR2 = join27(GLOBAL_CONFIG_DIR, "rules");
104716
+ var GLOBAL_CONFIG_DIR = join30(".config", "spur");
104717
+ var GLOBAL_RULES_DIR2 = join30(GLOBAL_CONFIG_DIR, "rules");
103483
104718
  var GLOBAL_CONFIG_EXAMPLE = BUNDLED_GLOBAL_CONFIG;
103484
104719
  var GLOBAL_CONFIG_FILE2 = "config.yaml";
103485
104720
  var INDEXED_CONTEXT_MARKER = "## Indexed context";
@@ -103510,7 +104745,7 @@ If \`.spur/context/\` is absent, proceed normally. Never block work on its absen
103510
104745
  `;
103511
104746
  function globalRulesRoot(context4) {
103512
104747
  const override = context4.env.SPUR_GLOBAL_RULES_DIR;
103513
- return override !== undefined && override.length > 0 ? resolve15(context4.cwd, override) : join27(homedir9(), GLOBAL_RULES_DIR2);
104748
+ return override !== undefined && override.length > 0 ? resolve16(context4.cwd, override) : join30(homedir9(), GLOBAL_RULES_DIR2);
103514
104749
  }
103515
104750
  async function writeIfNew(context4, path9, content, force, result) {
103516
104751
  if (!force && await context4.fs.exists(path9)) {
@@ -103527,18 +104762,18 @@ async function seedGlobalRules(context4) {
103527
104762
  const target = globalRulesRoot(context4);
103528
104763
  let written = 0;
103529
104764
  for (const relPath of await listBundledRuleFiles()) {
103530
- const destination = join27(target, relPath);
104765
+ const destination = join30(target, relPath);
103531
104766
  if (await context4.fs.exists(destination))
103532
104767
  continue;
103533
- await context4.fs.ensureDir(join27(target, ...relPath.split("/").slice(0, -1)));
103534
- await context4.fs.writeFile(destination, await context4.fs.readFile(join27(source, relPath)));
104768
+ await context4.fs.ensureDir(join30(target, ...relPath.split("/").slice(0, -1)));
104769
+ await context4.fs.writeFile(destination, await context4.fs.readFile(join30(source, relPath)));
103535
104770
  written += 1;
103536
104771
  }
103537
104772
  return written;
103538
104773
  }
103539
104774
  function resolveGlobalConfigDir(context4) {
103540
104775
  const globalOverride = context4.env.SPUR_GLOBAL_RULES_DIR;
103541
- return globalOverride !== undefined && globalOverride.length > 0 ? resolve15(context4.cwd, globalOverride) : join27(homedir9(), GLOBAL_CONFIG_DIR);
104776
+ return globalOverride !== undefined && globalOverride.length > 0 ? resolve16(context4.cwd, globalOverride) : join30(homedir9(), GLOBAL_CONFIG_DIR);
103542
104777
  }
103543
104778
  async function seedGlobalConfig(context4) {
103544
104779
  const source = bundledConfigRoot();
@@ -103549,15 +104784,15 @@ async function seedGlobalConfig(context4) {
103549
104784
  for (const relPath of listBundledConfigFiles()) {
103550
104785
  if (relPath === GLOBAL_CONFIG_EXAMPLE)
103551
104786
  continue;
103552
- const destination = join27(target, relPath);
104787
+ const destination = join30(target, relPath);
103553
104788
  if (await context4.fs.exists(destination))
103554
104789
  continue;
103555
- await context4.fs.ensureDir(join27(target, ...relPath.split("/").slice(0, -1)));
103556
- await context4.fs.writeFile(destination, await context4.fs.readFile(join27(source, relPath)));
104790
+ await context4.fs.ensureDir(join30(target, ...relPath.split("/").slice(0, -1)));
104791
+ await context4.fs.writeFile(destination, await context4.fs.readFile(join30(source, relPath)));
103557
104792
  written += 1;
103558
104793
  }
103559
- const examplePath = join27(source, GLOBAL_CONFIG_EXAMPLE);
103560
- const globalConfigPath = join27(target, GLOBAL_CONFIG_FILE2);
104794
+ const examplePath = join30(source, GLOBAL_CONFIG_EXAMPLE);
104795
+ const globalConfigPath = join30(target, GLOBAL_CONFIG_FILE2);
103561
104796
  if (await context4.fs.exists(examplePath) && !await context4.fs.exists(globalConfigPath)) {
103562
104797
  await context4.fs.ensureDir(target);
103563
104798
  await context4.fs.writeFile(globalConfigPath, await context4.fs.readFile(examplePath));
@@ -103572,10 +104807,10 @@ function registerInitCommand(program2, context4, options = {}) {
103572
104807
  const minimal = options2.minimal === true;
103573
104808
  const adoptGlobal = options2.adoptGlobalConfig === true;
103574
104809
  const projectName = options2.name ?? "default";
103575
- const configPath = join27(context4.cwd, CLI_CONFIG.configFile);
104810
+ const configPath = join30(context4.cwd, CLI_CONFIG.configFile);
103576
104811
  const result = { created: [], skipped: [] };
103577
104812
  const globalConfigDir = resolveGlobalConfigDir(context4);
103578
- const globalConfigPath = join27(globalConfigDir, GLOBAL_CONFIG_FILE2);
104813
+ const globalConfigPath = join30(globalConfigDir, GLOBAL_CONFIG_FILE2);
103579
104814
  const globalConfigExists = await context4.fs.exists(globalConfigPath);
103580
104815
  let misplacedKeys = [];
103581
104816
  if (globalConfigExists) {
@@ -103588,20 +104823,20 @@ function registerInitCommand(program2, context4, options = {}) {
103588
104823
  const configRoot = bundledConfigRoot();
103589
104824
  if (configRoot !== null && !minimal) {
103590
104825
  for (const relPath of listBundledProjectSeedFiles()) {
103591
- const sourcePath = join27(configRoot, relPath);
104826
+ const sourcePath = join30(configRoot, relPath);
103592
104827
  if (!await context4.fs.exists(sourcePath))
103593
104828
  continue;
103594
- const targetPath = join27(context4.cwd, CLI_CONFIG.configDir, relPath);
103595
- await context4.fs.ensureDir(join27(targetPath, ".."));
104829
+ const targetPath = join30(context4.cwd, CLI_CONFIG.configDir, relPath);
104830
+ await context4.fs.ensureDir(join30(targetPath, ".."));
103596
104831
  await writeIfNew(context4, targetPath, await context4.fs.readFile(sourcePath), false, result);
103597
104832
  }
103598
104833
  for (const entry of SCAFFOLD_MANIFEST) {
103599
- const sourcePath = join27(configRoot, entry.source);
104834
+ const sourcePath = join30(configRoot, entry.source);
103600
104835
  if (!await context4.fs.exists(sourcePath))
103601
104836
  continue;
103602
- const baseDir = entry.root === true ? context4.cwd : join27(context4.cwd, CLI_CONFIG.configDir);
103603
- const targetPath = join27(baseDir, entry.target);
103604
- await context4.fs.ensureDir(join27(targetPath, ".."));
104837
+ const baseDir = entry.root === true ? context4.cwd : join30(context4.cwd, CLI_CONFIG.configDir);
104838
+ const targetPath = join30(baseDir, entry.target);
104839
+ await context4.fs.ensureDir(join30(targetPath, ".."));
103605
104840
  const entryForce = false;
103606
104841
  let body = await context4.fs.readFile(sourcePath);
103607
104842
  if (entry.target === "AGENTS.md") {
@@ -103618,7 +104853,7 @@ function registerInitCommand(program2, context4, options = {}) {
103618
104853
  const backupPath = `${globalConfigPath}.bak-${new Date().toISOString().replace(/[:.]/g, "-")}`;
103619
104854
  await context4.fs.writeFile(backupPath, await context4.fs.readFile(globalConfigPath));
103620
104855
  const bundledRoot = bundledConfigRoot();
103621
- const examplePath = bundledRoot !== null ? join27(bundledRoot, GLOBAL_CONFIG_EXAMPLE) : null;
104856
+ const examplePath = bundledRoot !== null ? join30(bundledRoot, GLOBAL_CONFIG_EXAMPLE) : null;
103622
104857
  if (examplePath !== null && await context4.fs.exists(examplePath)) {
103623
104858
  await context4.fs.writeFile(globalConfigPath, await context4.fs.readFile(examplePath));
103624
104859
  adoptedGlobal = true;
@@ -103688,13 +104923,13 @@ function registerInitCommand(program2, context4, options = {}) {
103688
104923
  ].join(`
103689
104924
  `)}
103690
104925
  `;
103691
- await context4.fs.ensureDir(join27(context4.cwd, CLI_CONFIG.configDir));
104926
+ await context4.fs.ensureDir(join30(context4.cwd, CLI_CONFIG.configDir));
103692
104927
  await context4.fs.writeFile(configPath, configYaml);
103693
104928
  result.created.push(configPath);
103694
- const agentsDir = join27(context4.cwd, CLI_CONFIG.configDir, "agents");
104929
+ const agentsDir = join30(context4.cwd, CLI_CONFIG.configDir, "agents");
103695
104930
  await context4.fs.ensureDir(agentsDir);
103696
- await writeIfNew(context4, join27(agentsDir, ".gitkeep"), "", force, result);
103697
- const gitignorePath = join27(context4.cwd, ".gitignore");
104931
+ await writeIfNew(context4, join30(agentsDir, ".gitkeep"), "", force, result);
104932
+ const gitignorePath = join30(context4.cwd, ".gitignore");
103698
104933
  const contextEntry = ".spur/context/";
103699
104934
  if (await context4.fs.exists(gitignorePath)) {
103700
104935
  const existing = await context4.fs.readFile(gitignorePath);
@@ -103714,20 +104949,20 @@ ${contextEntry}
103714
104949
  const configRoot = bundledConfigRoot();
103715
104950
  if (configRoot !== null) {
103716
104951
  for (const relPath of listBundledProjectSeedFiles()) {
103717
- const sourcePath = join27(configRoot, relPath);
104952
+ const sourcePath = join30(configRoot, relPath);
103718
104953
  if (!await context4.fs.exists(sourcePath))
103719
104954
  continue;
103720
- const targetPath = join27(context4.cwd, CLI_CONFIG.configDir, relPath);
103721
- await context4.fs.ensureDir(join27(targetPath, ".."));
104955
+ const targetPath = join30(context4.cwd, CLI_CONFIG.configDir, relPath);
104956
+ await context4.fs.ensureDir(join30(targetPath, ".."));
103722
104957
  await writeIfNew(context4, targetPath, await context4.fs.readFile(sourcePath), force, result);
103723
104958
  }
103724
104959
  for (const entry of SCAFFOLD_MANIFEST) {
103725
- const sourcePath = join27(configRoot, entry.source);
104960
+ const sourcePath = join30(configRoot, entry.source);
103726
104961
  if (!await context4.fs.exists(sourcePath))
103727
104962
  continue;
103728
- const baseDir = entry.root === true ? context4.cwd : join27(context4.cwd, CLI_CONFIG.configDir);
103729
- const targetPath = join27(baseDir, entry.target);
103730
- await context4.fs.ensureDir(join27(targetPath, ".."));
104963
+ const baseDir = entry.root === true ? context4.cwd : join30(context4.cwd, CLI_CONFIG.configDir);
104964
+ const targetPath = join30(baseDir, entry.target);
104965
+ await context4.fs.ensureDir(join30(targetPath, ".."));
103731
104966
  const entryForce = entry.preserve === true ? false : force;
103732
104967
  let body = await context4.fs.readFile(sourcePath);
103733
104968
  if (entry.target === "AGENTS.md") {
@@ -103740,7 +104975,7 @@ ${contextEntry}
103740
104975
  }
103741
104976
  }
103742
104977
  }
103743
- const agentsMdPath = join27(context4.cwd, "AGENTS.md");
104978
+ const agentsMdPath = join30(context4.cwd, "AGENTS.md");
103744
104979
  if (await context4.fs.exists(agentsMdPath)) {
103745
104980
  const existing = await context4.fs.readFile(agentsMdPath);
103746
104981
  if (!existing.includes(INDEXED_CONTEXT_MARKER)) {
@@ -103781,11 +105016,11 @@ ${contextEntry}
103781
105016
  // src/commands/maintain.ts
103782
105017
  init_src();
103783
105018
  init_src2();
103784
- import { join as join28 } from "path";
105019
+ import { join as join31 } from "path";
103785
105020
  function registerMaintainCommand(program2, context4, options = {}) {
103786
105021
  program2.command("maintain", { hidden: options.hidden === true }).summary("run database maintenance (optimize, WAL checkpoint truncation, optional VACUUM)").description("Run database maintenance: PRAGMA optimize, WAL checkpoint truncation; optionally VACUUM compaction.").option("--vacuum", "Run VACUUM defragmentation and page compaction", false).option(...SHARED_OPTIONS.jsonSupported).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options2) => {
103787
105022
  const db2 = await context4.getDb();
103788
- const dbPath = context4.env.DATABASE_URL ?? join28(context4.cwd, DEFAULT_DATABASE_URL);
105023
+ const dbPath = context4.env.DATABASE_URL ?? join31(context4.cwd, DEFAULT_DATABASE_URL);
103789
105024
  const result = await maintainDatabase(db2, {
103790
105025
  vacuum: options2.vacuum === true,
103791
105026
  dbPath: dbPath.startsWith(":memory:") ? undefined : dbPath
@@ -104084,15 +105319,15 @@ async function runMessageWatch(svc, output2, options, runtime = {}) {
104084
105319
  }
104085
105320
  }
104086
105321
  function defaultSleep(ms, signal) {
104087
- return new Promise((resolve16) => {
105322
+ return new Promise((resolve17) => {
104088
105323
  if (signal?.aborted) {
104089
- resolve16();
105324
+ resolve17();
104090
105325
  return;
104091
105326
  }
104092
- const timer = setTimeout(resolve16, ms);
105327
+ const timer = setTimeout(resolve17, ms);
104093
105328
  signal?.addEventListener("abort", () => {
104094
105329
  clearTimeout(timer);
104095
- resolve16();
105330
+ resolve17();
104096
105331
  }, { once: true });
104097
105332
  });
104098
105333
  }
@@ -104119,15 +105354,15 @@ function parseTimeout2(raw) {
104119
105354
  return n3;
104120
105355
  }
104121
105356
  function sendSleep(ms, signal) {
104122
- return new Promise((resolve16) => {
105357
+ return new Promise((resolve17) => {
104123
105358
  if (signal?.aborted) {
104124
- resolve16();
105359
+ resolve17();
104125
105360
  return;
104126
105361
  }
104127
- const timer = setTimeout(resolve16, ms);
105362
+ const timer = setTimeout(resolve17, ms);
104128
105363
  signal?.addEventListener("abort", () => {
104129
105364
  clearTimeout(timer);
104130
- resolve16();
105365
+ resolve17();
104131
105366
  }, { once: true });
104132
105367
  });
104133
105368
  }
@@ -104185,10 +105420,10 @@ async function waitForPendingDrain(agentService, teamService, pin, timeoutMs, si
104185
105420
 
104186
105421
  // src/commands/migrate.ts
104187
105422
  init_src2();
104188
- import { join as join29 } from "path";
105423
+ import { join as join32 } from "path";
104189
105424
  function registerMigrateCommand(program2, context4, options = {}) {
104190
105425
  program2.command("migrate", { hidden: options.hidden === true }).summary("apply CLI-owned schema migrations").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options2) => {
104191
- const migrations = await loadSqlMigrations(join29(context4.cwd, "drizzle")).catch(() => {
105426
+ const migrations = await loadSqlMigrations(join32(context4.cwd, "drizzle")).catch(() => {
104192
105427
  return;
104193
105428
  });
104194
105429
  const applied = await applyCliMigrations(await context4.getDb(), migrations);
@@ -104200,12 +105435,12 @@ function registerMigrateCommand(program2, context4, options = {}) {
104200
105435
  // src/commands/projects.ts
104201
105436
  init_dist5();
104202
105437
  await init_src3();
104203
- import { basename as basename10, resolve as resolve16 } from "path";
105438
+ import { basename as basename10, resolve as resolve17 } from "path";
104204
105439
  function registerProjectsCommand(program2, context4) {
104205
105440
  const projectsCmd = program2.command("projects").summary("manage the Spur multi-project registry");
104206
105441
  projectsCmd.command("add").argument("<path>", "Project root directory path").option(...SHARED_OPTIONS.nameProjectDisplay).option(...SHARED_OPTIONS.jsonProjectsResponse).option(...SHARED_OPTIONS.jsonEnvelope).action(async (pathArg, options) => {
104207
105442
  try {
104208
- const absolutePath = resolve16(context4.cwd, pathArg);
105443
+ const absolutePath = resolve17(context4.cwd, pathArg);
104209
105444
  if (!await context4.fs.exists(absolutePath)) {
104210
105445
  throw new Error(`Directory does not exist: ${absolutePath}`);
104211
105446
  }
@@ -104627,7 +105862,7 @@ function formatTraceDetail(detail) {
104627
105862
 
104628
105863
  // src/commands/serve.ts
104629
105864
  init_src();
104630
- import { join as join33 } from "path";
105865
+ import { join as join36 } from "path";
104631
105866
 
104632
105867
  // ../server/src/index.ts
104633
105868
  init_src();
@@ -104637,11 +105872,11 @@ init_src();
104637
105872
  init_loader();
104638
105873
  init_src2();
104639
105874
  await init_src3();
104640
- import { basename as basename12, dirname as dirname20, isAbsolute as isAbsolute7, join as join32 } from "path";
105875
+ import { basename as basename12, dirname as dirname21, isAbsolute as isAbsolute8, join as join35 } from "path";
104641
105876
  init_dist5();
104642
105877
 
104643
105878
  // ../server/src/bootstrap.ts
104644
- import { join as join30 } from "path";
105879
+ import { join as join33 } from "path";
104645
105880
 
104646
105881
  // ../../node_modules/.bun/radash@12.1.1/node_modules/radash/dist/esm/async.mjs
104647
105882
  var guard = (func, shouldGuard) => {
@@ -104914,8 +106149,8 @@ class EventPublisher {
104914
106149
  if (bufferedEvents.length > 0) {
104915
106150
  return { done: false, value: bufferedEvents.shift() };
104916
106151
  }
104917
- return new Promise((resolve17, reject) => {
104918
- pullResolvers.push([resolve17, reject]);
106152
+ return new Promise((resolve18, reject) => {
106153
+ pullResolvers.push([resolve18, reject]);
104919
106154
  });
104920
106155
  }, async () => {
104921
106156
  unsubscribe();
@@ -111108,6 +112343,93 @@ var taskContract = {
111108
112343
  tags: ["task"]
111109
112344
  }).input(taskActionInputSchema).output(taskActionResponseSchema)
111110
112345
  };
112346
+ // ../../packages/contracts/src/observability.ts
112347
+ init_zod();
112348
+ var observabilitySummaryKpisSchema = exports_external.object({
112349
+ totalEvents: exports_external.number(),
112350
+ activeJobs: exports_external.number(),
112351
+ completedJobs: exports_external.number(),
112352
+ failedJobs: exports_external.number(),
112353
+ successRatePct: exports_external.number(),
112354
+ errorEventCount: exports_external.number(),
112355
+ warningEventCount: exports_external.number()
112356
+ });
112357
+ var observabilityVolumeBucketSchema = exports_external.object({
112358
+ timestamp: exports_external.string(),
112359
+ total: exports_external.number(),
112360
+ byPrefix: exports_external.record(exports_external.string(), exports_external.number()),
112361
+ bySeverity: exports_external.object({
112362
+ info: exports_external.number(),
112363
+ warning: exports_external.number(),
112364
+ error: exports_external.number(),
112365
+ unknown: exports_external.number()
112366
+ })
112367
+ });
112368
+ var observabilityTopEventTypeSchema = exports_external.object({
112369
+ name: exports_external.string(),
112370
+ prefix: exports_external.string(),
112371
+ count: exports_external.number(),
112372
+ latestAt: exports_external.string()
112373
+ });
112374
+ var observabilityRecentErrorSchema = exports_external.object({
112375
+ id: exports_external.string(),
112376
+ source: exports_external.enum(["event", "job"]),
112377
+ name: exports_external.string(),
112378
+ occurredAt: exports_external.string(),
112379
+ message: exports_external.string(),
112380
+ refId: exports_external.string().optional()
112381
+ });
112382
+ var observabilitySummaryResponseSchema = exports_external.object({
112383
+ window: exports_external.object({
112384
+ since: exports_external.string(),
112385
+ until: exports_external.string(),
112386
+ range: exports_external.string()
112387
+ }),
112388
+ kpis: observabilitySummaryKpisSchema,
112389
+ eventVolumeBuckets: exports_external.array(observabilityVolumeBucketSchema),
112390
+ topEventTypes: exports_external.array(observabilityTopEventTypeSchema),
112391
+ recentErrors: exports_external.array(observabilityRecentErrorSchema)
112392
+ });
112393
+ var queueJobStatusSchema = exports_external.enum(["pending", "processing", "completed", "failed"]);
112394
+ var queueJobRowSchema = exports_external.object({
112395
+ id: exports_external.string(),
112396
+ type: exports_external.string(),
112397
+ status: queueJobStatusSchema,
112398
+ attempts: exports_external.number(),
112399
+ maxRetries: exports_external.number(),
112400
+ queuedAt: exports_external.string(),
112401
+ startedAt: exports_external.string().nullable(),
112402
+ endedAt: exports_external.string().nullable(),
112403
+ durationMs: exports_external.number().nullable(),
112404
+ lastError: exports_external.string().nullable(),
112405
+ payload: exports_external.record(exports_external.string(), exports_external.unknown()).nullable()
112406
+ });
112407
+ var queueJobStatusCountsSchema = exports_external.object({
112408
+ all: exports_external.number(),
112409
+ pending: exports_external.number(),
112410
+ processing: exports_external.number(),
112411
+ completed: exports_external.number(),
112412
+ failed: exports_external.number()
112413
+ });
112414
+ var queueJobListResponseSchema = exports_external.object({
112415
+ jobs: exports_external.array(queueJobRowSchema),
112416
+ total: exports_external.number(),
112417
+ hasMore: exports_external.boolean(),
112418
+ countsByStatus: queueJobStatusCountsSchema
112419
+ });
112420
+ var schedulerLastStatusSchema = exports_external.enum(["completed", "failed", "processing", "none"]);
112421
+ var schedulerScheduleRowSchema = exports_external.object({
112422
+ name: exports_external.string(),
112423
+ cron: exports_external.string(),
112424
+ cadence: exports_external.string(),
112425
+ nextFireAt: exports_external.string().nullable(),
112426
+ lastFiredAt: exports_external.string().nullable(),
112427
+ lastStatus: schedulerLastStatusSchema,
112428
+ source: exports_external.enum(["builtin", "config"])
112429
+ });
112430
+ var schedulerSchedulesResponseSchema = exports_external.object({
112431
+ schedules: exports_external.array(schedulerScheduleRowSchema)
112432
+ });
111111
112433
 
111112
112434
  // ../../packages/contracts/src/index.ts
111113
112435
  var healthResponseSchema = exports_external.object({
@@ -111374,6 +112696,18 @@ var historyModule = {
111374
112696
  mount(_app, _ctx) {}
111375
112697
  };
111376
112698
 
112699
+ // ../server/src/modules/jobs/index.ts
112700
+ init_src2();
112701
+
112702
+ // ../server/src/modules/jobs/schedule-registry.ts
112703
+ var registeredSchedules = [];
112704
+ function setRegisteredSchedules(entries) {
112705
+ registeredSchedules = [...entries];
112706
+ }
112707
+ function getRegisteredSchedules() {
112708
+ return [...registeredSchedules];
112709
+ }
112710
+
111377
112711
  // ../server/src/modules/jobs/index.ts
111378
112712
  var jobsModule = {
111379
112713
  name: "jobs",
@@ -111385,6 +112719,100 @@ var jobsModule = {
111385
112719
  const stats = await queue.stats();
111386
112720
  return c3.json({ stats });
111387
112721
  });
112722
+ app.get("/api/jobs", async (c3) => {
112723
+ try {
112724
+ const statusParam = c3.req.query("status");
112725
+ const sinceParam = c3.req.query("since");
112726
+ const limitParam = c3.req.query("limit");
112727
+ const offsetParam = c3.req.query("offset");
112728
+ const validStatuses = ["pending", "processing", "completed", "failed"];
112729
+ let status;
112730
+ if (statusParam !== undefined && statusParam !== "") {
112731
+ if (!validStatuses.includes(statusParam)) {
112732
+ return c3.json({
112733
+ error: `unknown status: "${statusParam}", expected one of: ${validStatuses.join(", ")}`,
112734
+ code: "VALIDATION_FAILED"
112735
+ }, 400);
112736
+ }
112737
+ status = statusParam;
112738
+ }
112739
+ let limit = 100;
112740
+ if (limitParam !== undefined && limitParam !== "") {
112741
+ const parsedLimit = Number(limitParam);
112742
+ if (!Number.isNaN(parsedLimit)) {
112743
+ limit = Math.min(500, Math.max(1, parsedLimit));
112744
+ }
112745
+ }
112746
+ let offset = 0;
112747
+ if (offsetParam !== undefined && offsetParam !== "") {
112748
+ const parsedOffset = Number(offsetParam);
112749
+ if (!Number.isNaN(parsedOffset)) {
112750
+ offset = Math.max(0, parsedOffset);
112751
+ }
112752
+ }
112753
+ const db2 = await ctx.getDb();
112754
+ const result = await queryQueueJobs(db2, {
112755
+ status,
112756
+ since: sinceParam,
112757
+ limit,
112758
+ offset
112759
+ });
112760
+ return c3.json(result);
112761
+ } catch (err) {
112762
+ const message = err instanceof Error ? err.message : String(err);
112763
+ return c3.json({ error: message }, 500);
112764
+ }
112765
+ });
112766
+ app.get("/api/jobs/schedules", async (c3) => {
112767
+ try {
112768
+ const registrations = getRegisteredSchedules();
112769
+ const db2 = await ctx.getDb();
112770
+ const now2 = Date.now();
112771
+ const schedules = await Promise.all(registrations.map(async (reg) => {
112772
+ const matchingRow = await queryScheduleLastExecution(db2, reg.source, reg.name);
112773
+ const lastFiredAt = matchingRow ? new Date(matchingRow.updated_at).toISOString() : null;
112774
+ let lastStatus = "none";
112775
+ if (matchingRow) {
112776
+ lastStatus = matchingRow.status === "pending" ? "processing" : matchingRow.status;
112777
+ }
112778
+ const intervalMs = Number(reg.schedule);
112779
+ const isInterval = !Number.isNaN(intervalMs) && intervalMs > 0 && /^\d+$/.test(reg.schedule);
112780
+ let nextFireAt = null;
112781
+ let cadence = reg.schedule;
112782
+ if (isInterval) {
112783
+ const elapsed = Math.max(0, now2 - reg.registeredAt);
112784
+ const steps = Math.ceil(elapsed / intervalMs);
112785
+ const nextTime = reg.registeredAt + (steps === 0 ? 1 : steps) * intervalMs;
112786
+ const resolvedNextTime = nextTime <= now2 ? nextTime + intervalMs : nextTime;
112787
+ nextFireAt = new Date(resolvedNextTime).toISOString();
112788
+ if (intervalMs >= 60000 && intervalMs % 60000 === 0) {
112789
+ cadence = `every ${intervalMs / 60000} minutes`;
112790
+ } else if (intervalMs >= 1000 && intervalMs % 1000 === 0) {
112791
+ cadence = `every ${intervalMs / 1000} seconds`;
112792
+ } else {
112793
+ cadence = `every ${intervalMs} ms`;
112794
+ }
112795
+ } else {
112796
+ nextFireAt = null;
112797
+ cadence = reg.schedule;
112798
+ }
112799
+ const row = {
112800
+ name: reg.name,
112801
+ cron: reg.schedule,
112802
+ cadence,
112803
+ nextFireAt,
112804
+ lastFiredAt,
112805
+ lastStatus,
112806
+ source: reg.source
112807
+ };
112808
+ return row;
112809
+ }));
112810
+ return c3.json({ schedules });
112811
+ } catch (err) {
112812
+ const message = err instanceof Error ? err.message : String(err);
112813
+ return c3.json({ error: message }, 500);
112814
+ }
112815
+ });
111388
112816
  }
111389
112817
  };
111390
112818
 
@@ -111626,6 +113054,96 @@ function handleRoutingSummary(ctx) {
111626
113054
  }
111627
113055
  };
111628
113056
  }
113057
+ function handleObservabilitySummary(ctx) {
113058
+ return async (c3) => {
113059
+ try {
113060
+ const sinceParam = c3.req.query("since");
113061
+ const untilParam = c3.req.query("until");
113062
+ const bucketParam = c3.req.query("bucket");
113063
+ let untilMs = Date.now();
113064
+ if (untilParam !== undefined && untilParam !== "") {
113065
+ untilMs = Date.parse(untilParam);
113066
+ if (Number.isNaN(untilMs)) {
113067
+ return c3.json({
113068
+ error: `malformed until: "${untilParam}" is not a valid ISO timestamp`,
113069
+ code: "MALFORMED_TIMESTAMP"
113070
+ }, 400);
113071
+ }
113072
+ }
113073
+ let sinceMs = untilMs - 14400000;
113074
+ if (sinceParam !== undefined && sinceParam !== "") {
113075
+ sinceMs = Date.parse(sinceParam);
113076
+ if (Number.isNaN(sinceMs)) {
113077
+ return c3.json({
113078
+ error: `malformed since: "${sinceParam}" is not a valid ISO timestamp`,
113079
+ code: "MALFORMED_TIMESTAMP"
113080
+ }, 400);
113081
+ }
113082
+ }
113083
+ if (untilMs < sinceMs) {
113084
+ return c3.json({
113085
+ error: "until timestamp must not precede since timestamp",
113086
+ code: "MALFORMED_RANGE"
113087
+ }, 400);
113088
+ }
113089
+ let bucketMs;
113090
+ if (bucketParam !== undefined && bucketParam !== "") {
113091
+ const parsed = Number(bucketParam);
113092
+ if (!Number.isNaN(parsed) && parsed > 0) {
113093
+ bucketMs = parsed;
113094
+ }
113095
+ }
113096
+ const since = new Date(sinceMs).toISOString();
113097
+ const until = new Date(untilMs).toISOString();
113098
+ const dao2 = await ctx.systemEventDao();
113099
+ const db2 = await ctx.getDb();
113100
+ const [eventSummary, jobKpis] = await Promise.all([
113101
+ dao2.eventSummary({ since, until, bucketMs }),
113102
+ queueJobKpis(db2, sinceMs, untilMs)
113103
+ ]);
113104
+ const combinedErrors = [
113105
+ ...eventSummary.recentErrors.map((e) => ({
113106
+ id: e.id,
113107
+ source: "event",
113108
+ name: e.name,
113109
+ occurredAt: e.occurredAt,
113110
+ message: e.message,
113111
+ ...e.refId ? { refId: e.refId } : {}
113112
+ })),
113113
+ ...jobKpis.recentJobErrors.map((j) => ({
113114
+ id: j.id,
113115
+ source: "job",
113116
+ name: j.name,
113117
+ occurredAt: j.occurredAt,
113118
+ message: j.message
113119
+ }))
113120
+ ].sort((a2, b) => Date.parse(b.occurredAt) - Date.parse(a2.occurredAt)).slice(0, 10);
113121
+ const payload = {
113122
+ window: {
113123
+ since,
113124
+ until,
113125
+ range: c3.req.query("range") ?? "custom"
113126
+ },
113127
+ kpis: {
113128
+ totalEvents: eventSummary.totalEvents,
113129
+ activeJobs: jobKpis.activeJobs,
113130
+ completedJobs: jobKpis.completedJobs,
113131
+ failedJobs: jobKpis.failedJobs,
113132
+ successRatePct: jobKpis.successRatePct,
113133
+ errorEventCount: eventSummary.errorEventCount,
113134
+ warningEventCount: eventSummary.warningEventCount
113135
+ },
113136
+ eventVolumeBuckets: eventSummary.eventVolumeBuckets,
113137
+ topEventTypes: eventSummary.topEventTypes,
113138
+ recentErrors: combinedErrors
113139
+ };
113140
+ return c3.json(payload);
113141
+ } catch (err) {
113142
+ const message = err instanceof Error ? err.message : String(err);
113143
+ return c3.json({ error: message }, 500);
113144
+ }
113145
+ };
113146
+ }
111629
113147
  var observabilityModule = {
111630
113148
  name: "observability",
111631
113149
  mount(app, ctx) {
@@ -111635,6 +113153,7 @@ var observabilityModule = {
111635
113153
  app.get("/api/observability/tool-use", handleToolUseGet(ctx));
111636
113154
  app.get("/api/observability/tool-use/stream", handleToolUseStream(ctx));
111637
113155
  app.get("/api/observability/routing-summary", handleRoutingSummary(ctx));
113156
+ app.get("/api/observability/summary", handleObservabilitySummary(ctx));
111638
113157
  }
111639
113158
  };
111640
113159
 
@@ -114062,7 +115581,7 @@ function createApp(appRt, opts) {
114062
115581
  app.use("*", async (c3, next) => {
114063
115582
  const pathname = c3.req.path === "/" ? "/index.html" : c3.req.path;
114064
115583
  try {
114065
- const file2 = Bun.file(join30(webDistPath, pathname));
115584
+ const file2 = Bun.file(join33(webDistPath, pathname));
114066
115585
  if (await file2.exists()) {
114067
115586
  const headers = new Headers({ "content-type": file2.type });
114068
115587
  return new Response(file2.stream(), { headers });
@@ -114075,7 +115594,7 @@ function createApp(appRt, opts) {
114075
115594
  return c3.json({ error: "Not Found" }, 404);
114076
115595
  }
114077
115596
  try {
114078
- const indexFile = Bun.file(join30(webDistPath, "index.html"));
115597
+ const indexFile = Bun.file(join33(webDistPath, "index.html"));
114079
115598
  if (await indexFile.exists()) {
114080
115599
  return new Response(indexFile.stream(), {
114081
115600
  headers: { "content-type": "text/html; charset=utf-8" }
@@ -114096,7 +115615,7 @@ init_src();
114096
115615
  init_src2();
114097
115616
  init_dist5();
114098
115617
  await init_src3();
114099
- import { dirname as dirname19, join as join31 } from "path";
115618
+ import { dirname as dirname20, join as join34 } from "path";
114100
115619
  var NOOP_OUTPUT = { write: (_s) => {}, error: (_s) => {} };
114101
115620
 
114102
115621
  class LazyPlanningEventEmitter {
@@ -114132,7 +115651,7 @@ class NotConfiguredError extends Error {
114132
115651
  function createServerContext(appRt, options) {
114133
115652
  const cwd = options.cwd;
114134
115653
  const fs3 = options.fs;
114135
- const dbUrl = options.dbUrl ?? join31(cwd, DEFAULT_DATABASE_URL);
115654
+ const dbUrl = options.dbUrl ?? join34(cwd, DEFAULT_DATABASE_URL);
114136
115655
  const eventsBus = options.eventsBus ?? appRt.events;
114137
115656
  const jobQueueEnabled = options.jobQueueEnabled ?? false;
114138
115657
  const eventProjectContext = systemEventProjectContext(cwd);
@@ -114170,7 +115689,7 @@ function createServerContext(appRt, options) {
114170
115689
  if (!dbPromise) {
114171
115690
  const created = (async () => {
114172
115691
  if (dbUrl !== IN_MEMORY_DATABASE_URL) {
114173
- await fs3.ensureDir(dirname19(dbUrl));
115692
+ await fs3.ensureDir(dirname20(dbUrl));
114174
115693
  }
114175
115694
  return createMigratedDbViaRuntime({ url: dbUrl });
114176
115695
  })();
@@ -114418,7 +115937,7 @@ async function loadServerSectionMatrix() {
114418
115937
  }
114419
115938
  const root = bundledConfigRoot();
114420
115939
  if (root !== null) {
114421
- const matrixPath = join32(root, "tasks", "section-matrix.yaml");
115940
+ const matrixPath = join35(root, "tasks", "section-matrix.yaml");
114422
115941
  if (await nodeFs.exists(matrixPath)) {
114423
115942
  return await loadStructuredSpurConfig(matrixPath, {
114424
115943
  validateJsonSchema: false
@@ -114427,7 +115946,7 @@ async function loadServerSectionMatrix() {
114427
115946
  }
114428
115947
  throw new Error(`no canonical section-matrix found for task creation (F92 R1); tried:
114429
115948
  ` + ` - ${localPath}
114430
- ` + (root !== null ? ` - ${join32(root, "tasks", "section-matrix.yaml")}
115949
+ ` + (root !== null ? ` - ${join35(root, "tasks", "section-matrix.yaml")}
114431
115950
  ` : "") + "copy/generate section-matrix.yaml from the canonical build-time matrix asset (repo `config` `tasks` tree) into one of those paths");
114432
115951
  }
114433
115952
  var SYSTEM_EVENTS_PRUNE_JOB = "system-events-prune";
@@ -114446,6 +115965,8 @@ var defaultDeps = {
114446
115965
  resolveConfigFile
114447
115966
  };
114448
115967
  function registerSchedulerEntries(scheduler3, ctx, jobs = []) {
115968
+ const registrations = [];
115969
+ const now2 = Date.now();
114449
115970
  const register = (cron, name, action) => {
114450
115971
  scheduler3.register(cron, async () => {
114451
115972
  const startedAt2 = Date.now();
@@ -114469,17 +115990,48 @@ function registerSchedulerEntries(scheduler3, ctx, jobs = []) {
114469
115990
  const queue = await ctx.jobQueue();
114470
115991
  await queue.enqueue(SYSTEM_EVENTS_PRUNE_JOB, { source: "scheduler" }, { maxRetries: 1 });
114471
115992
  });
115993
+ registrations.push({
115994
+ name: SYSTEM_EVENTS_PRUNE_JOB,
115995
+ schedule: SYSTEM_EVENTS_PRUNE_CRON,
115996
+ source: "builtin",
115997
+ registeredAt: now2
115998
+ });
114472
115999
  register(SMOKE_CRON, SMOKE_JOB, async () => {
114473
116000
  const queue = await ctx.jobQueue();
114474
116001
  await queue.enqueue(SMOKE_JOB, { source: "scheduler" }, { maxRetries: 1 });
114475
116002
  });
116003
+ registrations.push({
116004
+ name: SMOKE_JOB,
116005
+ schedule: SMOKE_CRON,
116006
+ source: "builtin",
116007
+ registeredAt: now2
116008
+ });
114476
116009
  for (const job of jobs) {
114477
116010
  const schedule = job.cron ?? String(job.intervalMinutes * 60000);
114478
116011
  register(schedule, `${SCHEDULER_CUSTOM_JOB}:${job.name}`, async () => {
114479
116012
  const queue = await ctx.jobQueue();
116013
+ const db2 = await ctx.getDb();
116014
+ const active = await findActiveSchedulerCustomJob(db2, job.name);
116015
+ if (active !== undefined) {
116016
+ ctx.eventBus().emit("scheduler.job.executed", {
116017
+ name: `${SCHEDULER_CUSTOM_JOB}:${job.name}`,
116018
+ durationMs: 0,
116019
+ severity: "info",
116020
+ skipped: true,
116021
+ reason: `active job ${active.id} already exists`
116022
+ });
116023
+ return;
116024
+ }
114480
116025
  await queue.enqueue(SCHEDULER_CUSTOM_JOB, { name: job.name, command: job.command });
114481
116026
  });
116027
+ registrations.push({
116028
+ name: job.name,
116029
+ schedule,
116030
+ source: "config",
116031
+ registeredAt: now2
116032
+ });
114482
116033
  }
116034
+ setRegisteredSchedules(registrations);
114483
116035
  }
114484
116036
  function parseTaskActionJob(payload) {
114485
116037
  if (typeof payload !== "object" || payload === null) {
@@ -114565,15 +116117,15 @@ async function handleFeatureActionJob(ctx, env, payload) {
114565
116117
  await runFeatureActionJob(ctx, env, payload);
114566
116118
  }
114567
116119
  async function resolveWebDistPath(configuredPath) {
114568
- const candidates = configuredPath && configuredPath.trim() !== "" ? [isAbsolute7(configuredPath) ? configuredPath : join32(process.cwd(), configuredPath)] : [
114569
- join32(process.cwd(), "dist/web"),
114570
- join32(import.meta.dir, "web"),
114571
- join32(dirname20(process.execPath), "web"),
114572
- join32(dirname20(process.execPath), "../web"),
114573
- join32(import.meta.dir, "../../../dist/web")
116120
+ const candidates = configuredPath && configuredPath.trim() !== "" ? [isAbsolute8(configuredPath) ? configuredPath : join35(process.cwd(), configuredPath)] : [
116121
+ join35(process.cwd(), "dist/web"),
116122
+ join35(import.meta.dir, "web"),
116123
+ join35(dirname21(process.execPath), "web"),
116124
+ join35(dirname21(process.execPath), "../web"),
116125
+ join35(import.meta.dir, "../../../dist/web")
114574
116126
  ];
114575
116127
  for (const candidate of candidates) {
114576
- if (await Bun.file(join32(candidate, "index.html")).exists()) {
116128
+ if (await Bun.file(join35(candidate, "index.html")).exists()) {
114577
116129
  return candidate;
114578
116130
  }
114579
116131
  }
@@ -114589,7 +116141,7 @@ async function startServer(options, deps = defaultDeps) {
114589
116141
  async start(appRt) {
114590
116142
  const fs3 = deps.createNodeFileSystem(process.cwd());
114591
116143
  if (options.dbUrl && options.dbUrl !== IN_MEMORY_DATABASE_URL) {
114592
- await fs3.ensureDir(dirname20(options.dbUrl));
116144
+ await fs3.ensureDir(dirname21(options.dbUrl));
114593
116145
  }
114594
116146
  const scheduler3 = appRt.config.scheduler.enabled ? appRt.scheduler : undefined;
114595
116147
  const webDistPath = await resolveWebDistPath(options.webDistPath);
@@ -114659,6 +116211,12 @@ async function startServer(options, deps = defaultDeps) {
114659
116211
  executor: childExecutor
114660
116212
  }, job));
114661
116213
  registry2.register(SCHEDULER_CUSTOM_JOB, (job) => handleSchedulerCustomJob({ cwd: ctx.cwd, executor: childExecutor }, job));
116214
+ const orphanCount = await failOrphanedProcessingJobs(await ctx.getDb(), Date.now());
116215
+ if (orphanCount > 0) {
116216
+ appRt.logger.warn("Swept orphaned processing jobs at startup", {
116217
+ count: orphanCount
116218
+ });
116219
+ }
114662
116220
  jobWorker = new JobWorkerService({
114663
116221
  consumer: await ctx.queueConsumer(),
114664
116222
  registry: registry2
@@ -114724,11 +116282,11 @@ async function startServer(options, deps = defaultDeps) {
114724
116282
  if (options.openBrowser) {
114725
116283
  await deps.openUrl(webDistPath ? `${url2}/board` : `${url2}/api/health`);
114726
116284
  }
114727
- if (options.keepAlive !== false) {
114728
- await new Promise(() => {});
114729
- }
114730
116285
  }
114731
116286
  });
116287
+ if (options.keepAlive !== false) {
116288
+ await new Promise(() => {});
116289
+ }
114732
116290
  }
114733
116291
  // ../server/src/worker-app.ts
114734
116292
  var startedAt2 = Date.now();
@@ -114737,7 +116295,7 @@ if (false) {}
114737
116295
 
114738
116296
  // src/commands/serve.ts
114739
116297
  function resolveServeDbUrl(cwd, env, configuredUrl) {
114740
- return env.DATABASE_URL === undefined ? join33(cwd, DEFAULT_DATABASE_URL) : configuredUrl;
116298
+ return env.DATABASE_URL === undefined ? join36(cwd, DEFAULT_DATABASE_URL) : configuredUrl;
114741
116299
  }
114742
116300
  function registerServeCommand(program2, context4, options = {}) {
114743
116301
  program2.command("serve", { hidden: options.hidden === true }).summary("start the Spur web server (local fallback)").option(...SHARED_OPTIONS.portServe, parseInt).option("--host <addr>", "Bind address (env: HOST, default: localhost)").option("--no-open", "Skip opening the browser").option(...SHARED_OPTIONS.cwdServe, context4.cwd).option(...SHARED_OPTIONS.jsonServePortUrl).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options2) => {
@@ -114777,7 +116335,7 @@ function registerServeCommand(program2, context4, options = {}) {
114777
116335
  }
114778
116336
 
114779
116337
  // src/commands/status.ts
114780
- import { join as join34 } from "path";
116338
+ import { join as join37 } from "path";
114781
116339
 
114782
116340
  // src/errors.ts
114783
116341
  class CommandError extends Error {
@@ -114848,8 +116406,8 @@ function registerStatusCommand(program2, context4, options = {}) {
114848
116406
  }
114849
116407
  async function runStatusCore(path9, options, context4) {
114850
116408
  const [packageJsonExists, spurConfigExists, git3, agentSpecs] = await Promise.all([
114851
- context4.fs.exists(join34(context4.cwd, "package.json")),
114852
- context4.fs.exists(join34(context4.cwd, ".spur", "config.yaml")),
116409
+ context4.fs.exists(join37(context4.cwd, "package.json")),
116410
+ context4.fs.exists(join37(context4.cwd, ".spur", "config.yaml")),
114853
116411
  gitContext(context4.cwd),
114854
116412
  listAgentSpecIds(context4)
114855
116413
  ]);
@@ -114877,14 +116435,14 @@ async function runStatusCore(path9, options, context4) {
114877
116435
  return status.ok ? 0 : 1;
114878
116436
  }
114879
116437
  async function listAgentSpecIds(context4) {
114880
- const dir = join34(context4.cwd, ".spur", "agents");
116438
+ const dir = join37(context4.cwd, ".spur", "agents");
114881
116439
  if (!await context4.fs.exists(dir))
114882
116440
  return [];
114883
116441
  const entries = await context4.fs.readDir(dir);
114884
116442
  return entries.filter((entry) => entry.endsWith(".yaml") || entry.endsWith(".yml")).map((entry) => entry.replace(/\.ya?ml$/, "")).sort();
114885
116443
  }
114886
116444
  async function readTargetStatus(context4, targetPath) {
114887
- const resolved = join34(context4.cwd, targetPath);
116445
+ const resolved = join37(context4.cwd, targetPath);
114888
116446
  const stat = await context4.fs.stat(resolved);
114889
116447
  if (stat === null)
114890
116448
  throw new CommandError(`status failed: path does not exist at ${resolved}`);
@@ -114897,7 +116455,9 @@ init_src2();
114897
116455
  init_dist5();
114898
116456
  await init_src3();
114899
116457
  import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
114900
- import { dirname as dirname21, join as join35 } from "path";
116458
+ import { rm, writeFile } from "fs/promises";
116459
+ import { tmpdir as tmpdir3 } from "os";
116460
+ import { dirname as dirname22, join as join38 } from "path";
114901
116461
  // schemas/section-matrix.schema.json
114902
116462
  var section_matrix_schema_default = {
114903
116463
  $schema: "http://json-schema.org/draft-07/schema#",
@@ -115832,16 +117392,33 @@ function renderMigrationReport(report, dryRun, corpusDir) {
115832
117392
  return lines.join(`
115833
117393
  `);
115834
117394
  }
117395
+ function writeCreateJsonError(context4, options, code, message, extra) {
117396
+ const text4 = typeof message === "string" ? message : String(message);
117397
+ if (options.json === true) {
117398
+ const bare = text4.startsWith("Error: ") ? text4.slice("Error: ".length) : text4;
117399
+ context4.output.write(toEnvelopeJson({ ok: false, error: { code, message: bare, ...extra ?? {} } }, {
117400
+ enveloped: options.jsonEnvelope,
117401
+ error: { code: "INTERNAL_ERROR", message: bare, details: { cliCode: code, ...extra ?? {} } }
117402
+ }));
117403
+ } else {
117404
+ context4.output.error(text4.startsWith("Error: ") ? text4.slice("Error: ".length) : text4);
117405
+ }
117406
+ }
115835
117407
  function registerTaskCommand(program2, context4) {
115836
117408
  const task = program2.command("task").summary("manage tasks");
115837
- task.command("create").summary("Create a new task with race-safe WBS allocation.").argument("<title>", "Task title").option(...SHARED_OPTIONS.featureTrace).option("--parent <wbs>", "Parent WBS for sub-task grouping").option("--template <variant>", `Template variant (${TASK_VARIANTS.join("|")})`).option(...SHARED_OPTIONS.folderTasks).option("--dedupe-within <seconds>", "Override the default dedup window (seconds). Guard is on (300s) by default when --feature is set.", Number).option("--allow-duplicate-name", "Disable the dedup guard entirely (creates anyway)").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (title2, options) => {
117409
+ task.command("create").summary("Create a new task with race-safe WBS allocation.").argument("<title>", "Task title").option(...SHARED_OPTIONS.featureTrace).option("--parent <wbs>", "Parent WBS for sub-task grouping").option("--template <variant>", `Template variant (${TASK_VARIANTS.join("|")})`).option(...SHARED_OPTIONS.folderTasks).option("--dedupe-within <seconds>", "Override the default dedup window (seconds). Guard is on (300s) by default when --feature is set.", Number).option("--allow-duplicate-name", "Disable the dedup guard entirely (creates anyway)").option("--skip-ready", "Skip ready preparation (backlog capture only, no model dispatch)").option("--agent <selector>", "Agent used for ready preparation (defaults to the configured agent)").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (title2, options) => {
115838
117410
  if (options.template !== undefined && !TASK_VARIANTS.includes(options.template)) {
115839
- writeJsonError(context4.output, options, `Unknown template variant "${options.template}". Valid: ${TASK_VARIANTS.join(", ")}`, "VALIDATION_FAILED");
117411
+ writeCreateJsonError(context4, options, "invalid-usage", `Unknown template variant "${options.template}". Valid: ${TASK_VARIANTS.join(", ")}`);
117412
+ context4.setExitCode(2);
117413
+ return;
117414
+ }
117415
+ if (options.skipReady === true && options.agent !== undefined) {
117416
+ writeCreateJsonError(context4, options, "invalid-usage", "--agent has no effect with --skip-ready (no preparation runs)");
115840
117417
  context4.setExitCode(2);
115841
117418
  return;
115842
117419
  }
115843
117420
  if (options.dedupeWithin !== undefined && (!Number.isInteger(options.dedupeWithin) || options.dedupeWithin <= 0)) {
115844
- writeJsonError(context4.output, options, "--dedupe-within must be a positive integer", "VALIDATION_FAILED");
117421
+ writeCreateJsonError(context4, options, "invalid-usage", "--dedupe-within must be a positive integer");
115845
117422
  context4.setExitCode(2);
115846
117423
  return;
115847
117424
  }
@@ -115854,10 +117431,25 @@ function registerTaskCommand(program2, context4) {
115854
117431
  template: options.template,
115855
117432
  dedupeWithinSec: options.allowDuplicateName ? null : options.dedupeWithin
115856
117433
  });
117434
+ let readiness = READY_SKIPPED;
117435
+ if (options.skipReady !== true) {
117436
+ const checkSvc = await makeCheckService(context4);
117437
+ readiness = await prepareCreatedTaskReady({
117438
+ wbs: result.ref.id,
117439
+ tasks: svc,
117440
+ agents: context4.agentService(),
117441
+ checkTask: (filePath, wbs) => checkSvc.check(filePath, wbs, { asStatus: "todo" }),
117442
+ agentSelector: options.agent,
117443
+ cwd: context4.cwd
117444
+ });
117445
+ }
115857
117446
  if (options.json) {
115858
- context4.output.write(toEnvelopeJson({ ...result, wbs: result.ref.id, filePath: result.ref.filePath }, { enveloped: options.jsonEnvelope }));
117447
+ context4.output.write(toEnvelopeJson({ ...result, wbs: result.ref.id, filePath: result.ref.filePath, ...readiness }, { enveloped: options.jsonEnvelope }));
115859
117448
  } else {
115860
117449
  context4.output.write(`Created task ${result.ref.id}: ${result.ref.filePath}`);
117450
+ if (readiness.readiness.status === "ready") {
117451
+ context4.output.write(`Ready: checklist + post-check passed (${result.ref.id} promoted to todo)`);
117452
+ }
115861
117453
  }
115862
117454
  } catch (err) {
115863
117455
  if (err instanceof WbsCollisionError) {
@@ -115916,8 +117508,25 @@ function registerTaskCommand(program2, context4) {
115916
117508
  context4.output.error(err.message);
115917
117509
  }
115918
117510
  context4.setExitCode(3);
117511
+ } else if (err instanceof TaskCandidateInvalidError) {
117512
+ writeCreateJsonError(context4, options, "candidate-invalid", err.message, {
117513
+ findings: err.findings
117514
+ });
117515
+ context4.setExitCode(1);
117516
+ } else if (err instanceof TaskPreparationError) {
117517
+ const message = !options.json && err.recoveryCommand !== undefined ? `${err.message}
117518
+ Recover with: ${err.recoveryCommand}` : err.message;
117519
+ writeCreateJsonError(context4, options, "preparation-failed", message, {
117520
+ wbs: err.wbs,
117521
+ filePath: err.filePath,
117522
+ failedStage: err.stage,
117523
+ ...err.recoveryCommand !== undefined ? { recoveryCommand: err.recoveryCommand } : {},
117524
+ ...err.findings !== undefined ? { findings: err.findings } : {},
117525
+ readiness: { status: "failed", depth: "ready" }
117526
+ });
117527
+ context4.setExitCode(1);
115919
117528
  } else {
115920
- writeJsonError(context4.output, options, String(err));
117529
+ writeCreateJsonError(context4, options, "create-failed", String(err));
115921
117530
  context4.setExitCode(1);
115922
117531
  }
115923
117532
  }
@@ -115999,7 +117608,7 @@ ${result.content}`);
115999
117608
  let forcedDoneVerdict;
116000
117609
  if (status === "done") {
116001
117610
  const current = await svc.show(wbs);
116002
- const verdictDir = options.verdictDir ?? join35(context4.cwd, ".spur", "run");
117611
+ const verdictDir = options.verdictDir ?? join38(context4.cwd, ".spur", "run");
116003
117612
  const loaded = await readVerdictArtifact(context4.fs, verdictDir, wbs);
116004
117613
  const guardOutcome = evaluateDoneTransition({
116005
117614
  wbs,
@@ -116235,7 +117844,7 @@ ${result.content}`);
116235
117844
  dryRun,
116236
117845
  projectRoot: context4.cwd,
116237
117846
  write: async (filePath, wbs, section, newBody) => {
116238
- const ref = { kind: "task", id: wbs, filePath, folder: dirname21(filePath) };
117847
+ const ref = { kind: "task", id: wbs, filePath, folder: dirname22(filePath) };
116239
117848
  const ws = new PlanningWriteService({ fs: context4.fs, emitter: makePlanningEmitter(context4) });
116240
117849
  await ws.updateSection(ref, section, newBody);
116241
117850
  }
@@ -116289,13 +117898,36 @@ ${result.content}`);
116289
117898
  context4.setExitCode(1);
116290
117899
  }
116291
117900
  });
116292
- task.command("batch-create").summary("Create many tasks from a validated JSON file \u2014 all-or-nothing (LLM\u2192CLI gate).").requiredOption(...SHARED_OPTIONS.fileTaskBatch).option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options) => {
117901
+ task.command("batch-create").summary("Create many tasks from a validated JSON file \u2014 all-or-nothing (LLM\u2192CLI gate).").requiredOption(...SHARED_OPTIONS.fileTaskBatch).option("--skip-ready", "Skip ready preparation (write supplied content as-is, no model dispatch)").option("--agent <selector>", "Agent used for ready preparation (defaults to the configured agent)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options) => {
117902
+ if (options.skipReady === true && options.agent !== undefined) {
117903
+ writeCreateJsonError(context4, options, "invalid-usage", "--agent has no effect with --skip-ready (no preparation runs)");
117904
+ context4.setExitCode(2);
117905
+ return;
117906
+ }
116293
117907
  const svc = await makeService2(context4, options.folder);
117908
+ let tmpPath;
116294
117909
  try {
116295
- const { children, parentsWired } = await svc.batchCreate(options.file);
117910
+ let batchFile = options.file;
117911
+ let readiness = READY_SKIPPED;
117912
+ if (options.skipReady !== true) {
117913
+ const batchSource = await context4.fs.readFile(options.file);
117914
+ const prepared = await prepareBatchTaskReady({
117915
+ batchPath: options.file,
117916
+ batchSource,
117917
+ agents: context4.agentService(),
117918
+ agentSelector: options.agent,
117919
+ cwd: context4.cwd
117920
+ });
117921
+ tmpPath = join38(tmpdir3(), `spur-ready-batch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
117922
+ await writeFile(tmpPath, `${JSON.stringify(prepared.items, null, 2)}
117923
+ `);
117924
+ batchFile = tmpPath;
117925
+ readiness = READY_DONE;
117926
+ }
117927
+ const { children, parentsWired } = await svc.batchCreate(batchFile);
116296
117928
  if (options.json) {
116297
117929
  const ids = children.map((r2) => r2.ref.id);
116298
- context4.output.write(toEnvelopeJson({ created: children.length, wbs: ids, parentsWired }, { enveloped: options.jsonEnvelope }));
117930
+ context4.output.write(toEnvelopeJson({ created: children.length, wbs: ids, parentsWired, ...readiness }, { enveloped: options.jsonEnvelope }));
116299
117931
  } else {
116300
117932
  context4.output.write(`Created ${children.length} task(s)`);
116301
117933
  for (const r2 of children) {
@@ -116311,7 +117943,16 @@ ${result.content}`);
116311
117943
  }
116312
117944
  }
116313
117945
  } catch (err) {
116314
- if (err instanceof WbsCollisionError) {
117946
+ if (err instanceof TaskPreparationError) {
117947
+ const message = !options.json && err.recoveryCommand !== undefined ? `${err.message}
117948
+ Recover with: ${err.recoveryCommand}` : err.message;
117949
+ writeCreateJsonError(context4, options, "preparation-failed", message, {
117950
+ failedStage: err.stage,
117951
+ ...err.recoveryCommand !== undefined ? { recoveryCommand: err.recoveryCommand } : {},
117952
+ readiness: { status: "failed", depth: "ready" }
117953
+ });
117954
+ context4.setExitCode(1);
117955
+ } else if (err instanceof WbsCollisionError) {
116315
117956
  if (options.json) {
116316
117957
  context4.output.write(toEnvelopeJson({
116317
117958
  ok: false,
@@ -116339,10 +117980,19 @@ ${result.content}`);
116339
117980
  context4.output.error(err.message);
116340
117981
  }
116341
117982
  context4.setExitCode(3);
117983
+ } else if (err instanceof TaskCandidateInvalidError) {
117984
+ writeCreateJsonError(context4, options, "candidate-invalid", err.message, {
117985
+ findings: err.findings
117986
+ });
117987
+ context4.setExitCode(1);
116342
117988
  } else {
116343
- writeJsonError(context4.output, options, String(err));
117989
+ writeCreateJsonError(context4, options, "batch-create-failed", String(err));
116344
117990
  context4.setExitCode(1);
116345
117991
  }
117992
+ } finally {
117993
+ if (tmpPath !== undefined) {
117994
+ await rm(tmpPath, { force: true }).catch(() => {});
117995
+ }
116346
117996
  }
116347
117997
  });
116348
117998
  task.command("record").summary("Record pipeline results into the task file \u2014 Testing, Review, and optional Solution backfill.").argument("<wbs>", "Task WBS number").option("--verdict-file <path>", "Path to verdict JSON (default: .spur/run/<wbs>-verdict.json)").option("--solution-from-diff", "Backfill Solution from git diff when bare").option("--transition <status>", "Optional lifecycle transition (e.g. testing)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (wbs, options) => {
@@ -116549,9 +118199,8 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
116549
118199
  const msg = `Duplicate WBS ${first.wbs} found in ${dup.length} files:
116550
118200
  ` + dup.map((h2) => ` ${h2.filePath}`).join(`
116551
118201
  `);
116552
- if (json3) {
116553
- results.push({ wbs: first.wbs, pass: false, status: "duplicate", findings: [msg] });
116554
- } else {
118202
+ results.push({ wbs: first.wbs, pass: false, status: "duplicate", findings: [msg] });
118203
+ if (!json3) {
116555
118204
  context4.output.error(msg);
116556
118205
  }
116557
118206
  }
@@ -116682,13 +118331,13 @@ function loadTemplateBodies(projectRoot, variant) {
116682
118331
  if (cached2 !== undefined)
116683
118332
  return cached2;
116684
118333
  let bodies = {};
116685
- const localPath = join35(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
118334
+ const localPath = join38(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
116686
118335
  if (existsSync11(localPath)) {
116687
118336
  bodies = extractTemplateBodies(readFileSync11(localPath, "utf8"));
116688
118337
  } else {
116689
118338
  const root = bundledConfigRoot();
116690
118339
  if (root !== null) {
116691
- const templatePath = join35(root, "templates", "task", `${variant}.md`);
118340
+ const templatePath = join38(root, "templates", "task", `${variant}.md`);
116692
118341
  if (existsSync11(templatePath)) {
116693
118342
  bodies = extractTemplateBodies(readFileSync11(templatePath, "utf8"));
116694
118343
  }
@@ -116746,7 +118395,7 @@ async function loadSectionMatrixUncached(projectRoot) {
116746
118395
  }
116747
118396
  const root = bundledConfigRoot();
116748
118397
  if (root !== null) {
116749
- const matrixPath = join35(root, "tasks", "section-matrix.yaml");
118398
+ const matrixPath = join38(root, "tasks", "section-matrix.yaml");
116750
118399
  if (await fs3.exists(matrixPath)) {
116751
118400
  const data = await loadStructuredSpurConfig(matrixPath, {
116752
118401
  validateJsonSchema: true,
@@ -116757,7 +118406,7 @@ async function loadSectionMatrixUncached(projectRoot) {
116757
118406
  }
116758
118407
  throw new Error(`no canonical section-matrix found for task section authority (F92 R1); tried:
116759
118408
  ` + ` - ${localPath}
116760
- ` + (root !== null ? ` - ${join35(root, "tasks", "section-matrix.yaml")}
118409
+ ` + (root !== null ? ` - ${join38(root, "tasks", "section-matrix.yaml")}
116761
118410
  ` : "") + "copy/generate section-matrix.yaml from the canonical build-time matrix asset (repo `config` `tasks` tree) into one of those paths");
116762
118411
  }
116763
118412
 
@@ -117046,7 +118695,7 @@ init_dist7();
117046
118695
  init_dist5();
117047
118696
  await init_src3();
117048
118697
  import { closeSync as closeSync5, fstatSync as fstatSync3, openSync as openSync5, readSync as readSync3 } from "fs";
117049
- import { join as join36 } from "path";
118698
+ import { join as join39 } from "path";
117050
118699
  import { createInterface } from "readline";
117051
118700
  import { setTimeout as sleep5 } from "timers/promises";
117052
118701
 
@@ -117224,11 +118873,11 @@ async function writeWorkflowPlanArtifact(cwd, runId, resolved, secretValues) {
117224
118873
  ...step.description !== undefined ? { description: redactAndBound(step.description, secretValues, 512) } : {}
117225
118874
  }))
117226
118875
  });
117227
- const { mkdir, writeFile } = await import("fs/promises");
117228
- const artifactsDir = join36(cwd, ".spur", "run");
118876
+ const { mkdir, writeFile: writeFile2 } = await import("fs/promises");
118877
+ const artifactsDir = join39(cwd, ".spur", "run");
117229
118878
  await mkdir(artifactsDir, { recursive: true });
117230
- const artifactPath = join36(artifactsDir, `${runId}-workflow-plan.json`);
117231
- await writeFile(artifactPath, payload);
118879
+ const artifactPath = join39(artifactsDir, `${runId}-workflow-plan.json`);
118880
+ await writeFile2(artifactPath, payload);
117232
118881
  return artifactPath;
117233
118882
  }
117234
118883
  async function makeEscalationPacketSink(bus, context4) {
@@ -117285,7 +118934,7 @@ function resolveWorkflowPaths(config4) {
117285
118934
  for (const path9 of paths) {
117286
118935
  if (path9.startsWith(BUNDLED_PATH_PREFIX)) {
117287
118936
  if (bundledRoot !== null)
117288
- expanded.push(join36(bundledRoot, path9.slice(BUNDLED_PATH_PREFIX.length)));
118937
+ expanded.push(join39(bundledRoot, path9.slice(BUNDLED_PATH_PREFIX.length)));
117289
118938
  } else {
117290
118939
  expanded.push(path9);
117291
118940
  }
@@ -117522,7 +119171,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
117522
119171
  }
117523
119172
  const runLog = options.log === false ? undefined : new WorkflowRunLogSink({
117524
119173
  bus,
117525
- dir: join36(context4.cwd, ".spur", "run"),
119174
+ dir: join39(context4.cwd, ".spur", "run"),
117526
119175
  runId,
117527
119176
  ...planPreview !== undefined ? { planPreview } : {},
117528
119177
  ...resolveOutputLogConfig(context4.spurConfig ?? null)
@@ -118049,7 +119698,7 @@ function readRunLogChunk(logPath, offset) {
118049
119698
  }
118050
119699
  }
118051
119700
  async function followRunLog(service, runId, dir, pollMs, write, wait = (ms) => sleep5(ms)) {
118052
- const logPath = join36(dir, ".spur", "run", `${runId}.log`);
119701
+ const logPath = join39(dir, ".spur", "run", `${runId}.log`);
118053
119702
  let offset = 0;
118054
119703
  let everRead = false;
118055
119704
  while (true) {
@@ -118112,7 +119761,7 @@ init_src();
118112
119761
  init_src2();
118113
119762
  init_dist5();
118114
119763
  await init_src3();
118115
- import { dirname as dirname22, join as join37, resolve as resolve17 } from "path";
119764
+ import { dirname as dirname23, join as join40, resolve as resolve18 } from "path";
118116
119765
  import { isatty as isatty4 } from "tty";
118117
119766
 
118118
119767
  // ../../node_modules/.bun/@clack+core@1.4.1/node_modules/@clack/core/dist/index.mjs
@@ -119527,7 +121176,7 @@ function resolveAgentRoles(agentConfig) {
119527
121176
  return resolved;
119528
121177
  }
119529
121178
  function createCliContext(options) {
119530
- const cwd = resolve17(options.cwd ?? process.cwd());
121179
+ const cwd = resolve18(options.cwd ?? process.cwd());
119531
121180
  const env = options.env ?? process.env;
119532
121181
  const fs3 = createNodeFileSystem3(cwd);
119533
121182
  const agentConfig = options.agentConfig ?? options.spurConfig?.agent;
@@ -119575,10 +121224,10 @@ function createCliContext(options) {
119575
121224
  function noopSetExitCode(_code) {}
119576
121225
  async function createMigratedDbAdapter(cwd = process.cwd(), env = process.env, dbUrl) {
119577
121226
  const config4 = buildConfigFromEnv(env);
119578
- const configuredUrl = env.DATABASE_URL === undefined ? join37(cwd, DEFAULT_DATABASE_URL) : config4.database.url;
121227
+ const configuredUrl = env.DATABASE_URL === undefined ? join40(cwd, DEFAULT_DATABASE_URL) : config4.database.url;
119579
121228
  const url2 = dbUrl ?? configuredUrl;
119580
121229
  if (url2 !== IN_MEMORY_DATABASE_URL) {
119581
- await createNodeFileSystem3().ensureDir(dirname22(url2));
121230
+ await createNodeFileSystem3().ensureDir(dirname23(url2));
119582
121231
  }
119583
121232
  return createMigratedDb({ url: url2 });
119584
121233
  }
@@ -119694,8 +121343,7 @@ async function runCli() {
119694
121343
  return main();
119695
121344
  }
119696
121345
  if (import.meta.main) {
119697
- const exitCode = await runCli();
119698
- process.exit(exitCode);
121346
+ process.exitCode = await runCli();
119699
121347
  }
119700
121348
  export {
119701
121349
  shouldRenderBanner,