@gobing-ai/spur 0.3.73 → 0.3.74

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 (88) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +15 -4
  3. package/config/pipeline-budgets.json +4 -4
  4. package/config/rules/strict/runtime-boundaries.yaml +1 -1
  5. package/config/templates/AGENTS.md +5 -0
  6. package/config/templates/docs/99_PROJECT_CONSTITUTION.md +2 -0
  7. package/config/workflows/basic.yaml +5 -2
  8. package/config/workflows/docs-pipeline.yaml +92 -55
  9. package/config/workflows/feature-dev.yaml +43 -20
  10. package/config/workflows/feature-lifecycle.yaml +1 -0
  11. package/config/workflows/history-anatomy.yaml +8 -4
  12. package/config/workflows/idea-pipeline.yaml +66 -33
  13. package/config/workflows/pr-review.yaml +31 -8
  14. package/config/workflows/task-lifecycle.yaml +1 -0
  15. package/config/workflows/task-pipeline.yaml +929 -905
  16. package/config/workflows/wayfinder-resolution.yaml +124 -90
  17. package/config/workflows/wrapup-pipeline.yaml +208 -83
  18. package/package.json +1 -1
  19. package/plugins/sp/plugin.json +1 -1
  20. package/plugins/sp/scripts/history-anatomy-cache.mjs +7 -1
  21. package/plugins/sp/scripts/history-anatomy-cache.ts +11 -2
  22. package/plugins/sp/scripts/pr-reviewing.mjs +5 -1
  23. package/plugins/sp/scripts/pr-reviewing.ts +7 -1
  24. package/plugins/sp/scripts/transition-shim-check.ts +6 -6
  25. package/plugins/sp/skills/pr-reviewing/SKILL.md +4 -1
  26. package/plugins/sp/skills/spur-cli/references/self.md +17 -3
  27. package/plugins/sp/skills/spur-cli/references/tasks.md +1 -1
  28. package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +5 -5
  29. package/plugins/sp/skills/spur-cli/references/workflows.md +4 -4
  30. package/plugins/sp/skills/spur-dev/SKILL.md +17 -0
  31. package/plugins/sp/skills/spur-dev/references/execution-batch.md +3 -3
  32. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -2
  33. package/plugins/sp/skills/wayfinder/SKILL.md +1 -1
  34. package/spur.js +1125 -784
  35. package/web/_astro/{BoardApp.BYCNkMOn.js → BoardApp.C60RJZRj.js} +78 -77
  36. package/web/_astro/BoardApp.ymfj1EHA.js +1 -0
  37. package/web/_astro/{TaskDetail.CgUreSP2.js → TaskDetail.Guk8VuNu.js} +1 -1
  38. package/web/_astro/{arc.BySSh34M.js → arc.CAZtlWJM.js} +1 -1
  39. package/web/_astro/{architectureDiagram-3BPJPVTR.DM46TS_h.js → architectureDiagram-3BPJPVTR.9-XbF_Tz.js} +1 -1
  40. package/web/_astro/{blockDiagram-GPEHLZMM.tZhvNUHA.js → blockDiagram-GPEHLZMM.DC4WLx3L.js} +1 -1
  41. package/web/_astro/{c4Diagram-AAUBKEIU.PT4Or4Nf.js → c4Diagram-AAUBKEIU.JgjUQMgG.js} +1 -1
  42. package/web/_astro/channel.Bxxe2Byc.js +1 -0
  43. package/web/_astro/{chunk-2J33WTMH.J9r0_Bbe.js → chunk-2J33WTMH.D_JFiXa-.js} +1 -1
  44. package/web/_astro/{chunk-4BX2VUAB.hzyeIvhR.js → chunk-4BX2VUAB.Bk_RxoeT.js} +1 -1
  45. package/web/_astro/{chunk-55IACEB6.B0rO7qVh.js → chunk-55IACEB6.a_Rj2VxL.js} +1 -1
  46. package/web/_astro/{chunk-727SXJPM.wE_Uk5D4.js → chunk-727SXJPM.C2NoS1U6.js} +1 -1
  47. package/web/_astro/{chunk-AQP2D5EJ.DqEEjQw7.js → chunk-AQP2D5EJ.D1Xn6CNa.js} +1 -1
  48. package/web/_astro/{chunk-FMBD7UC4.CDoD9sBX.js → chunk-FMBD7UC4.BhJrSBsX.js} +1 -1
  49. package/web/_astro/{chunk-ND2GUHAM.CtX5nF9P.js → chunk-ND2GUHAM.ByVGduYQ.js} +1 -1
  50. package/web/_astro/{chunk-QZHKN3VN.CK_EwfaT.js → chunk-QZHKN3VN.B47Paunq.js} +1 -1
  51. package/web/_astro/{classDiagram-4FO5ZUOK.DLt5a8Lh.js → classDiagram-4FO5ZUOK.BERMSD5C.js} +1 -1
  52. package/web/_astro/{classDiagram-v2-Q7XG4LA2.DLt5a8Lh.js → classDiagram-v2-Q7XG4LA2.BERMSD5C.js} +1 -1
  53. package/web/_astro/{cose-bilkent-S5V4N54A.CMCWP49h.js → cose-bilkent-S5V4N54A.O_rHGlhU.js} +1 -1
  54. package/web/_astro/{cynefin-OW5HDTMX.HyXw_vdS.js → cynefin-OW5HDTMX.qCW2GdNp.js} +1 -1
  55. package/web/_astro/{dagre-BM42HDAG.BTuAzh01.js → dagre-BM42HDAG.C5Y4lH_u.js} +1 -1
  56. package/web/_astro/{diagram-2AECGRRQ.D9dr9wfT.js → diagram-2AECGRRQ.DxZmRXxF.js} +1 -1
  57. package/web/_astro/{diagram-5GNKFQAL.C4Rot0hj.js → diagram-5GNKFQAL.qSxZeYSK.js} +1 -1
  58. package/web/_astro/{diagram-KO2AKTUF.B_TK5uWC.js → diagram-KO2AKTUF.-7vl3bXs.js} +1 -1
  59. package/web/_astro/{diagram-LMA3HP47.JkXKK7CO.js → diagram-LMA3HP47.BHVV-UH3.js} +1 -1
  60. package/web/_astro/{diagram-OG6HWLK6.BzMN8Bd6.js → diagram-OG6HWLK6.DHAuW9sK.js} +1 -1
  61. package/web/_astro/{erDiagram-TEJ5UH35.DVZaWGUd.js → erDiagram-TEJ5UH35.DMRLQwPN.js} +1 -1
  62. package/web/_astro/{flowDiagram-I6XJVG4X.rjEiWUfR.js → flowDiagram-I6XJVG4X.B3EpPp_8.js} +1 -1
  63. package/web/_astro/{ganttDiagram-6RSMTGT7.C_EgAarK.js → ganttDiagram-6RSMTGT7.BgUrExXM.js} +1 -1
  64. package/web/_astro/{gitGraphDiagram-PVQCEYII.B-QQSDsK.js → gitGraphDiagram-PVQCEYII.CU-9yVN4.js} +1 -1
  65. package/web/_astro/{index.B5MTfe7k.css → index.BhvM3djt.css} +1 -1
  66. package/web/_astro/{infoDiagram-5YYISTIA.DlWesz7T.js → infoDiagram-5YYISTIA.CvVTCRLe.js} +1 -1
  67. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BUMZOawi.js → ishikawaDiagram-YF4QCWOH.BUzOfKPf.js} +1 -1
  68. package/web/_astro/{journeyDiagram-JHISSGLW.CWfkxfjY.js → journeyDiagram-JHISSGLW.BBTb7ziz.js} +1 -1
  69. package/web/_astro/{kanban-definition-UN3LZRKU.B-YpMwXf.js → kanban-definition-UN3LZRKU.CTQQr70i.js} +1 -1
  70. package/web/_astro/{linear.D7uqzENp.js → linear.CLKlJPiS.js} +1 -1
  71. package/web/_astro/{mermaid.core.CxrNppBD.js → mermaid.core.BA4wFhLP.js} +4 -4
  72. package/web/_astro/{mindmap-definition-RKZ34NQL.B4Qe7cM2.js → mindmap-definition-RKZ34NQL.t1vG2l5e.js} +1 -1
  73. package/web/_astro/{pieDiagram-4H26LBE5.Ds-5j2ro.js → pieDiagram-4H26LBE5.C8zJhyTu.js} +1 -1
  74. package/web/_astro/{quadrantDiagram-W4KKPZXB.tBd38uNC.js → quadrantDiagram-W4KKPZXB.DqYYvb7T.js} +1 -1
  75. package/web/_astro/{requirementDiagram-4Y6WPE33.sFENkWl3.js → requirementDiagram-4Y6WPE33.lzfDZ9nI.js} +1 -1
  76. package/web/_astro/{sankeyDiagram-5OEKKPKP.BeB-Hk7C.js → sankeyDiagram-5OEKKPKP.lm6N5ORM.js} +1 -1
  77. package/web/_astro/{sequenceDiagram-3UESZ5HK.DnTeaSpx.js → sequenceDiagram-3UESZ5HK.E6_IV4Dj.js} +1 -1
  78. package/web/_astro/{stateDiagram-AJRCARHV.B-8Jt5EJ.js → stateDiagram-AJRCARHV.Bnv6Ok2p.js} +1 -1
  79. package/web/_astro/{stateDiagram-v2-BHNVJYJU.Br7xoqMW.js → stateDiagram-v2-BHNVJYJU.mJHH_Ng_.js} +1 -1
  80. package/web/_astro/{timeline-definition-PNZ67QCA.C-3WdOyi.js → timeline-definition-PNZ67QCA.BhRgqSLa.js} +1 -1
  81. package/web/_astro/{vennDiagram-CIIHVFJN.DCIs7Lc6.js → vennDiagram-CIIHVFJN.pjb5DMC9.js} +1 -1
  82. package/web/_astro/{wardleyDiagram-YWT4CUSO.rGAL-bbz.js → wardleyDiagram-YWT4CUSO.CW20KUng.js} +1 -1
  83. package/web/_astro/{xychartDiagram-2RQKCTM6.hkfQKiRl.js → xychartDiagram-2RQKCTM6.vX9_yuMl.js} +1 -1
  84. package/web/index.html +2 -2
  85. package/config/corpus-baseline.json +0 -1805
  86. package/config/workflow-composition-baseline.json +0 -615
  87. package/web/_astro/BoardApp.E12MFjOS.js +0 -1
  88. package/web/_astro/channel.5cYKr5cs.js +0 -1
package/spur.js CHANGED
@@ -50296,6 +50296,7 @@ var init_models = __esm(() => {
50296
50296
  "gemini-3-flash-preview": { inputPricePer1M: 0.15, outputPricePer1M: 0.6 },
50297
50297
  "gemini-3-flash": { inputPricePer1M: 0.15, outputPricePer1M: 0.6 },
50298
50298
  "gemini-2.0-flash": { inputPricePer1M: 0.1, outputPricePer1M: 0.4 },
50299
+ "gpt-6-astra": { inputPricePer1M: 2.5, outputPricePer1M: 10 },
50299
50300
  "gpt-4o": { inputPricePer1M: 2.5, outputPricePer1M: 10 },
50300
50301
  "gpt-4o-mini": { inputPricePer1M: 0.15, outputPricePer1M: 0.6 },
50301
50302
  "gpt-4-turbo": { inputPricePer1M: 10, outputPricePer1M: 30 },
@@ -50754,9 +50755,6 @@ var init_watermark = __esm(() => {
50754
50755
  });
50755
50756
 
50756
50757
  // ../../packages/domain/src/analytics/forensic-query.ts
50757
- function withMessageDedup(where) {
50758
- return where === "" ? `WHERE ${MESSAGE_DEDUP}` : `${where} AND ${MESSAGE_DEDUP}`;
50759
- }
50760
50758
  function escapeLike(value) {
50761
50759
  return value.replaceAll("!", "!!").replaceAll("%", "!%").replaceAll("_", "!_");
50762
50760
  }
@@ -50848,7 +50846,7 @@ async function messageRollup(db2, sel, opts) {
50848
50846
  const { where, params } = buildMessageWhere(sel);
50849
50847
  const wm = applyWatermarkToWhere(where, opts?.watermark);
50850
50848
  return db2.queryAll(`WITH filtered AS (
50851
- SELECT m.source, m.model, m.ts, m.input_tokens, m.output_tokens,
50849
+ SELECT m.source, m.session_id, m.model, m.ts, m.input_tokens, m.output_tokens,
50852
50850
  m.cache_read_tokens, m.cache_write_tokens, m.cost_usd, m.role, m.duration_ms,
50853
50851
  ROW_NUMBER() OVER (PARTITION BY m.request_id ORDER BY m.rowid DESC) AS rn,
50854
50852
  m.request_id
@@ -50856,7 +50854,17 @@ async function messageRollup(db2, sel, opts) {
50856
50854
  ${wm.where}
50857
50855
  ),
50858
50856
  selected AS (
50859
- SELECT * FROM filtered WHERE request_id IS NULL OR rn = 1
50857
+ SELECT source,
50858
+ COALESCE(
50859
+ NULLIF(NULLIF(model, ''), 'unknown'),
50860
+ MAX(CASE WHEN model IS NOT NULL AND model != '' AND model != 'unknown' THEN model END)
50861
+ OVER (PARTITION BY source, session_id),
50862
+ NULLIF(model, ''),
50863
+ 'unknown'
50864
+ ) AS model,
50865
+ ts, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
50866
+ cost_usd, role, duration_ms
50867
+ FROM filtered WHERE request_id IS NULL OR rn = 1
50860
50868
  )
50861
50869
  SELECT source, model, DATE(ts) AS day,
50862
50870
  COUNT(*) AS messages,
@@ -50876,7 +50884,15 @@ async function toolRollup(db2, sel, opts) {
50876
50884
  const { where, params } = buildMessageWhere(sel);
50877
50885
  const wm = applyWatermarkToWhere(where, opts?.watermark);
50878
50886
  return db2.queryAll(`WITH filtered_messages AS (
50879
- SELECT m.record_hash, m.source, m.model, m.ts
50887
+ SELECT m.record_hash, m.source, m.session_id,
50888
+ COALESCE(
50889
+ NULLIF(NULLIF(m.model, ''), 'unknown'),
50890
+ MAX(CASE WHEN m.model IS NOT NULL AND m.model != '' AND m.model != 'unknown' THEN m.model END)
50891
+ OVER (PARTITION BY m.source, m.session_id),
50892
+ NULLIF(m.model, ''),
50893
+ 'unknown'
50894
+ ) AS model,
50895
+ m.ts
50880
50896
  FROM history_message m
50881
50897
  ${wm.where}
50882
50898
  )
@@ -51243,6 +51259,8 @@ async function loops(db2, sel, opts) {
51243
51259
  ORDER BY repeats DESC`, ...params, ...wm.params);
51244
51260
  }
51245
51261
  async function loopRepeatedCallsQuery(db2, params) {
51262
+ const argsDigestSql = params.argsDigest !== null && params.argsDigest !== "" ? "tc.args_digest = ?" : "(tc.args_digest IS NULL OR tc.args_digest = '')";
51263
+ const argsDigestParam = params.argsDigest !== null && params.argsDigest !== "" ? [params.argsDigest] : [];
51246
51264
  return db2.queryAll(`SELECT tc.seq AS toolSeq,
51247
51265
  COALESCE(tc.started_at, m.ts) AS ts,
51248
51266
  tc.tool_name AS toolName,
@@ -51256,17 +51274,17 @@ async function loopRepeatedCallsQuery(db2, params) {
51256
51274
  tc.message_hash AS messageHash,
51257
51275
  tc.session_id AS sessionId,
51258
51276
  tc.source AS source,
51259
- m.model AS model,
51260
- (SELECT COUNT(*) FROM history_tool_call l WHERE l.message_hash = m.record_hash) AS links,
51277
+ COALESCE(NULLIF(NULLIF(m.model, ''), 'unknown'), ?) AS model,
51278
+ 1 AS links,
51261
51279
  m.input_tokens AS inputTokens,
51262
51280
  m.cache_read_tokens AS cacheReadTokens,
51263
51281
  m.output_tokens AS outputTokens
51264
51282
  FROM history_tool_call tc
51265
51283
  JOIN history_message m ON m.record_hash = tc.message_hash
51266
51284
  WHERE tc.source = ? AND tc.session_id = ? AND tc.tool_name = ?
51267
- AND (tc.args_digest = ? OR (tc.args_digest IS NULL AND ? = ''))
51285
+ AND ${argsDigestSql}
51268
51286
  ORDER BY tc.seq ASC
51269
- LIMIT ?`, params.source, params.sessionId, params.toolName, params.argsDigest ?? "", params.argsDigest ?? "", params.limit ?? 50);
51287
+ LIMIT ?`, params.model ?? null, params.source, params.sessionId, params.toolName, ...argsDigestParam, params.limit ?? 50);
51270
51288
  }
51271
51289
  async function drift(db2, sel, opts) {
51272
51290
  const { where, params } = buildMessageWhere(sel);
@@ -51338,7 +51356,17 @@ async function topStepsByTokens(db2, sel, top, opts) {
51338
51356
  const tokensOrder = rankOrderExpr(sel, "COALESCE(m.input_tokens, 0) + COALESCE(m.cache_read_tokens, 0)");
51339
51357
  const tokensGuard = rankOrderExpr(sel, "(m.input_tokens IS NOT NULL OR m.output_tokens IS NOT NULL)");
51340
51358
  const tokensPredicate = withStepPredicates(wm.where, `m.role = 'assistant' AND ${tokensGuard}`);
51341
- return db2.queryAll(`SELECT m.session_id AS sessionId, m.source AS source, m.ts AS ts, m.model AS model,
51359
+ return db2.queryAll(`SELECT m.session_id AS sessionId, m.source AS source, m.ts AS ts,
51360
+ CASE
51361
+ WHEN m.model IS NOT NULL AND m.model != '' AND m.model != 'unknown' THEN m.model
51362
+ WHEN m.session_id IS NOT NULL AND m.session_id NOT IN ('', 'unknown', 'session') THEN (
51363
+ SELECT m2.model FROM history_message m2
51364
+ WHERE m2.source = m.source AND m2.session_id = m.session_id
51365
+ AND m2.model IS NOT NULL AND m2.model != '' AND m2.model != 'unknown'
51366
+ LIMIT 1
51367
+ )
51368
+ ELSE 'unknown'
51369
+ END AS model,
51342
51370
  m.input_tokens AS inputTokens, m.cache_read_tokens AS cacheReadTokens,
51343
51371
  m.output_tokens AS outputTokens, m.cost_usd AS costUsd, m.duration_ms AS durationMs
51344
51372
  FROM history_message m
@@ -51351,7 +51379,17 @@ async function topStepsByDuration(db2, sel, top, opts) {
51351
51379
  const wm = applyWatermarkToWhere(where, opts?.watermark);
51352
51380
  const durationOrder = rankOrderExpr(sel, "m.duration_ms");
51353
51381
  const durationPredicate = withStepPredicates(wm.where, `m.role = 'assistant' AND ${durationOrder} IS NOT NULL`);
51354
- return db2.queryAll(`SELECT m.session_id AS sessionId, m.source AS source, m.ts AS ts, m.model AS model,
51382
+ return db2.queryAll(`SELECT m.session_id AS sessionId, m.source AS source, m.ts AS ts,
51383
+ CASE
51384
+ WHEN m.model IS NOT NULL AND m.model != '' AND m.model != 'unknown' THEN m.model
51385
+ WHEN m.session_id IS NOT NULL AND m.session_id NOT IN ('', 'unknown', 'session') THEN (
51386
+ SELECT m2.model FROM history_message m2
51387
+ WHERE m2.source = m.source AND m2.session_id = m.session_id
51388
+ AND m2.model IS NOT NULL AND m2.model != '' AND m2.model != 'unknown'
51389
+ LIMIT 1
51390
+ )
51391
+ ELSE 'unknown'
51392
+ END AS model,
51355
51393
  m.input_tokens AS inputTokens, m.cache_read_tokens AS cacheReadTokens,
51356
51394
  m.output_tokens AS outputTokens, m.cost_usd AS costUsd, m.duration_ms AS durationMs
51357
51395
  FROM history_message m
@@ -51374,7 +51412,17 @@ async function topCacheWasteSteps(db2, sel, top, opts) {
51374
51412
  const wm = applyWatermarkToWhere(where, opts?.watermark);
51375
51413
  const inputOrder = rankOrderExpr(sel, "m.input_tokens");
51376
51414
  const wastePredicate2 = withStepPredicates(wm.where, `m.role = 'assistant' AND ${inputOrder} > ? AND m.cache_read_tokens < m.input_tokens * ?`);
51377
- return db2.queryAll(`SELECT m.session_id AS sessionId, m.source AS source, m.ts AS ts, m.model AS model,
51415
+ return db2.queryAll(`SELECT m.session_id AS sessionId, m.source AS source, m.ts AS ts,
51416
+ CASE
51417
+ WHEN m.model IS NOT NULL AND m.model != '' AND m.model != 'unknown' THEN m.model
51418
+ WHEN m.session_id IS NOT NULL AND m.session_id NOT IN ('', 'unknown', 'session') THEN (
51419
+ SELECT m2.model FROM history_message m2
51420
+ WHERE m2.source = m.source AND m2.session_id = m.session_id
51421
+ AND m2.model IS NOT NULL AND m2.model != '' AND m2.model != 'unknown'
51422
+ LIMIT 1
51423
+ )
51424
+ ELSE 'unknown'
51425
+ END AS model,
51378
51426
  m.input_tokens AS inputTokens, m.cache_read_tokens AS cacheReadTokens,
51379
51427
  m.output_tokens AS outputTokens, m.cost_usd AS costUsd, m.duration_ms AS durationMs
51380
51428
  FROM history_message m
@@ -51563,29 +51611,123 @@ function finalizeSessionDurations(events) {
51563
51611
  }
51564
51612
  }
51565
51613
  }
51566
- async function queryTimelineEvents(db2, whereClause, params, subWhereClause, subParams, limit, correlationMap) {
51614
+ async function queryTimelineEvents(db2, whereClause, params, _subWhereClause, _subParams, limit, correlationMap) {
51567
51615
  const fetchLimit = limit + 1;
51568
51616
  const mainCondition = whereClause.startsWith("WHERE ") ? whereClause.slice(6) : whereClause;
51569
- const subCondition = subWhereClause.startsWith("WHERE ") ? subWhereClause.slice(6) : subWhereClause;
51570
- const dedupSubWhere = subCondition ? `WHERE ${subCondition} AND ` : "WHERE ";
51571
- const fullWhere = mainCondition ? `WHERE ${mainCondition} AND (m.request_id IS NULL OR m.rowid IN (SELECT MIN(dm.rowid) FROM history_message dm ${dedupSubWhere}dm.request_id IS NOT NULL GROUP BY dm.request_id))` : `WHERE (m.request_id IS NULL OR m.rowid IN (SELECT MIN(dm.rowid) FROM history_message dm ${dedupSubWhere}dm.request_id IS NOT NULL GROUP BY dm.request_id))`;
51572
- const rows = await db2.queryAll(`SELECT m.record_hash AS messageHash, m.seq AS messageSeq,
51617
+ const dedupCondition = `(m.request_id IS NULL OR NOT EXISTS (
51618
+ SELECT 1 FROM history_message dm
51619
+ WHERE dm.request_id = m.request_id AND dm.rowid < m.rowid
51620
+ ))`;
51621
+ const fullWhere = mainCondition ? `WHERE ${mainCondition} AND ${dedupCondition}` : `WHERE ${dedupCondition}`;
51622
+ const msgRows = await db2.queryAll(`SELECT m.record_hash AS messageHash, m.seq AS messageSeq,
51573
51623
  COALESCE(m.turn_index, m.seq) AS turnIndex,
51574
51624
  m.ts AS messageTs, m.role AS messageRole, COALESCE(m.record_type, m.role) AS recordType,
51575
51625
  m.source, m.session_id AS sessionId, m.model,
51576
51626
  m.duration_ms AS messageDurationMs, m.input_tokens AS inputTokens,
51577
51627
  m.cache_read_tokens AS cacheReadTokens, m.output_tokens AS outputTokens,
51578
- m.content_text AS messagePayload,
51579
- (SELECT COUNT(*) FROM history_tool_call links WHERE links.message_hash = m.record_hash) AS links,
51580
- tc.tool_name AS toolName, COALESCE(tc.started_at, m.ts) AS toolTs,
51581
- tc.duration_ms AS toolDurationMs, tc.status AS toolStatus,
51582
- tc.args_raw AS toolArgsRaw, tc.args_digest AS toolArgsDigest, tc.error_text AS toolErrorText,
51583
- COALESCE(tc.seq, 0) AS toolSeq, m.rowid AS messageRowId
51628
+ m.content_text AS messagePayload, m.rowid AS messageRowId
51584
51629
  FROM history_message m
51585
- LEFT JOIN history_tool_call tc ON tc.message_hash = m.record_hash
51586
51630
  ${fullWhere}
51587
- ORDER BY COALESCE(m.ts, '0000-00-00') DESC, m.rowid DESC, COALESCE(tc.seq, 0) DESC
51588
- LIMIT ?`, ...params, ...subParams, fetchLimit);
51631
+ ORDER BY m.ts DESC NULLS LAST, m.rowid DESC
51632
+ LIMIT ?`, ...params, fetchLimit);
51633
+ const msgHashes = msgRows.map((r) => r.messageHash);
51634
+ const tcMap = new Map;
51635
+ if (msgHashes.length > 0) {
51636
+ const inPlaceholders = msgHashes.map(() => "?").join(", ");
51637
+ const tcRows = await db2.queryAll(`SELECT tc.message_hash AS messageHash,
51638
+ tc.tool_name AS toolName, COALESCE(tc.started_at, m.ts) AS toolTs,
51639
+ tc.duration_ms AS toolDurationMs, tc.status AS toolStatus,
51640
+ tc.args_raw AS toolArgsRaw, tc.args_digest AS toolArgsDigest, tc.error_text AS toolErrorText,
51641
+ COALESCE(tc.seq, 0) AS toolSeq
51642
+ FROM history_tool_call tc
51643
+ JOIN history_message m ON m.record_hash = tc.message_hash
51644
+ WHERE tc.message_hash IN (${inPlaceholders})
51645
+ ORDER BY tc.message_hash, COALESCE(tc.seq, 0)
51646
+ LIMIT ?`, ...msgHashes, fetchLimit * 50);
51647
+ for (const tc of tcRows) {
51648
+ let list = tcMap.get(tc.messageHash);
51649
+ if (list === undefined) {
51650
+ list = [];
51651
+ tcMap.set(tc.messageHash, list);
51652
+ }
51653
+ list.push(tc);
51654
+ }
51655
+ }
51656
+ const sessionModelMap = new Map;
51657
+ for (const row of msgRows) {
51658
+ if (row.model && row.model !== "" && row.model !== "unknown") {
51659
+ const key = `${row.source}:::${row.sessionId}`;
51660
+ if (!sessionModelMap.has(key))
51661
+ sessionModelMap.set(key, row.model);
51662
+ }
51663
+ }
51664
+ for (const row of msgRows) {
51665
+ if (!row.model || row.model === "" || row.model === "unknown") {
51666
+ const key = `${row.source}:::${row.sessionId}`;
51667
+ const resolved = sessionModelMap.get(key);
51668
+ if (resolved)
51669
+ row.model = resolved;
51670
+ }
51671
+ }
51672
+ const rows = [];
51673
+ for (const m of msgRows) {
51674
+ const tcs = tcMap.get(m.messageHash) ?? [];
51675
+ const links = tcs.length;
51676
+ rows.push({
51677
+ messageHash: m.messageHash,
51678
+ messageSeq: m.messageSeq,
51679
+ turnIndex: m.turnIndex,
51680
+ messageTs: m.messageTs,
51681
+ messageRole: m.messageRole,
51682
+ recordType: m.recordType,
51683
+ source: m.source,
51684
+ sessionId: m.sessionId,
51685
+ model: m.model,
51686
+ messageDurationMs: m.messageDurationMs,
51687
+ inputTokens: m.inputTokens,
51688
+ cacheReadTokens: m.cacheReadTokens,
51689
+ outputTokens: m.outputTokens,
51690
+ messagePayload: m.messagePayload,
51691
+ links,
51692
+ toolName: null,
51693
+ toolTs: null,
51694
+ toolDurationMs: null,
51695
+ toolStatus: null,
51696
+ toolArgsRaw: null,
51697
+ toolArgsDigest: null,
51698
+ toolErrorText: null,
51699
+ toolSeq: 0,
51700
+ messageRowId: m.messageRowId
51701
+ });
51702
+ for (const tc of tcs) {
51703
+ rows.push({
51704
+ messageHash: m.messageHash,
51705
+ messageSeq: m.messageSeq,
51706
+ turnIndex: m.turnIndex,
51707
+ messageTs: m.messageTs,
51708
+ messageRole: m.messageRole,
51709
+ recordType: m.recordType,
51710
+ source: m.source,
51711
+ sessionId: m.sessionId,
51712
+ model: m.model,
51713
+ messageDurationMs: m.messageDurationMs,
51714
+ inputTokens: m.inputTokens,
51715
+ cacheReadTokens: m.cacheReadTokens,
51716
+ outputTokens: m.outputTokens,
51717
+ messagePayload: m.messagePayload,
51718
+ links,
51719
+ toolName: tc.toolName,
51720
+ toolTs: tc.toolTs,
51721
+ toolDurationMs: tc.toolDurationMs,
51722
+ toolStatus: tc.toolStatus,
51723
+ toolArgsRaw: tc.toolArgsRaw,
51724
+ toolArgsDigest: tc.toolArgsDigest,
51725
+ toolErrorText: tc.toolErrorText,
51726
+ toolSeq: tc.toolSeq,
51727
+ messageRowId: m.messageRowId
51728
+ });
51729
+ }
51730
+ }
51589
51731
  const rawEvents = [];
51590
51732
  let previousMessage = "";
51591
51733
  for (const row of rows) {
@@ -51904,37 +52046,45 @@ async function historyKpiTrend(db2, sel, opts) {
51904
52046
  }
51905
52047
  async function toolSequenceQuery(db2, scope, filters = {}, limit = 5000) {
51906
52048
  const fetchLimit = limit + 1;
51907
- const params = [];
51908
- const clauses = [];
52049
+ const msgCteParams = [];
52050
+ const tcFilterParams = [];
52051
+ let msgCteWhere;
51909
52052
  if (scope.mode === "session") {
51910
- clauses.push("tc.source = ? AND tc.session_id = ?");
51911
- params.push(scope.source, scope.sessionId);
52053
+ msgCteWhere = `WHERE m.source = ? AND m.session_id = ? AND ${MESSAGE_DEDUP}`;
52054
+ msgCteParams.push(scope.source, scope.sessionId);
51912
52055
  } else {
51913
- const { where, params: selParams } = buildMessageWhere(scope.sel, "m");
51914
- if (where !== "") {
51915
- clauses.push(where.startsWith("WHERE ") ? where.slice(6) : where);
51916
- params.push(...selParams);
51917
- }
52056
+ const { where: mw, params: mp } = buildMessageWhere(scope.sel, "m");
52057
+ const msgCondition = mw.startsWith("WHERE ") ? mw.slice(6) : mw;
52058
+ msgCteWhere = msgCondition ? `WHERE ${msgCondition} AND ${MESSAGE_DEDUP}` : `WHERE ${MESSAGE_DEDUP}`;
52059
+ msgCteParams.push(...mp);
52060
+ }
52061
+ const tcFilterClauses = [];
52062
+ if (scope.mode === "session") {
52063
+ tcFilterClauses.push("tc.source = ? AND tc.session_id = ?");
52064
+ tcFilterParams.push(scope.source, scope.sessionId);
51918
52065
  }
51919
52066
  if (filters.toolNames && filters.toolNames.length > 0) {
51920
52067
  const placeholders = filters.toolNames.map(() => "?").join(", ");
51921
- clauses.push(toolSelectionSql("tc", placeholders));
51922
- params.push(...filters.toolNames, ...filters.toolNames);
52068
+ tcFilterClauses.push(toolSelectionSql("tc", placeholders));
52069
+ tcFilterParams.push(...filters.toolNames, ...filters.toolNames);
51923
52070
  }
51924
52071
  if (filters.status && filters.status !== "all") {
51925
- clauses.push("tc.status = ?");
51926
- params.push(filters.status);
52072
+ tcFilterClauses.push("tc.status = ?");
52073
+ tcFilterParams.push(filters.status);
51927
52074
  }
51928
52075
  if (filters.search && filters.search.trim().length > 0) {
51929
52076
  const searchPattern = `%${escapeLike(filters.search.trim())}%`;
51930
- clauses.push("(tc.args_raw LIKE ? ESCAPE '!' OR tc.error_text LIKE ? ESCAPE '!' OR tc.effective_tool_name LIKE ? ESCAPE '!' OR tc.tool_name LIKE ? ESCAPE '!')");
51931
- params.push(searchPattern, searchPattern, searchPattern, searchPattern);
52077
+ tcFilterClauses.push("(tc.args_raw LIKE ? ESCAPE '!' OR tc.error_text LIKE ? ESCAPE '!' OR tc.effective_tool_name LIKE ? ESCAPE '!' OR tc.tool_name LIKE ? ESCAPE '!')");
52078
+ tcFilterParams.push(searchPattern, searchPattern, searchPattern, searchPattern);
51932
52079
  }
51933
- const whereCombined = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
51934
- const folded = withMessageDedup(whereCombined);
51935
- params.push(fetchLimit);
51936
- const rows = await db2.queryAll(`SELECT tc.seq AS toolSeq,
51937
- COALESCE(tc.started_at, m.ts) AS ts,
52080
+ const tcWhere = tcFilterClauses.length > 0 ? `AND ${tcFilterClauses.join(" AND ")}` : "";
52081
+ const rows = await db2.queryAll(`WITH deduped_messages AS (
52082
+ SELECT record_hash, session_id, source, model, input_tokens, cache_read_tokens, output_tokens, ts
52083
+ FROM history_message m
52084
+ ${msgCteWhere}
52085
+ )
52086
+ SELECT tc.seq AS toolSeq,
52087
+ COALESCE(tc.started_at, dm.ts) AS ts,
51938
52088
  tc.tool_name AS toolName,
51939
52089
  tc.effective_tool_name AS effectiveToolName,
51940
52090
  tc.status AS status,
@@ -51947,16 +52097,67 @@ async function toolSequenceQuery(db2, scope, filters = {}, limit = 5000) {
51947
52097
  tc.message_hash AS messageHash,
51948
52098
  tc.session_id AS sessionId,
51949
52099
  tc.source AS source,
51950
- m.model AS model,
51951
- (SELECT COUNT(*) FROM history_tool_call l WHERE l.message_hash = m.record_hash) AS links,
51952
- m.input_tokens AS inputTokens,
51953
- m.cache_read_tokens AS cacheReadTokens,
51954
- m.output_tokens AS outputTokens
51955
- FROM history_tool_call tc
51956
- JOIN history_message m ON m.record_hash = tc.message_hash
51957
- ${folded}
51958
- ORDER BY COALESCE(tc.started_at, m.ts), tc.source, tc.session_id, tc.seq
51959
- LIMIT ?`, ...params);
52100
+ dm.model,
52101
+ 0 AS links,
52102
+ dm.input_tokens AS inputTokens,
52103
+ dm.cache_read_tokens AS cacheReadTokens,
52104
+ dm.output_tokens AS outputTokens
52105
+ FROM deduped_messages dm
52106
+ CROSS JOIN history_tool_call tc ON tc.message_hash = dm.record_hash
52107
+ ${tcWhere}
52108
+ ORDER BY COALESCE(tc.started_at, dm.ts), tc.source, tc.session_id, tc.seq
52109
+ LIMIT ?`, ...msgCteParams, ...tcFilterParams, fetchLimit);
52110
+ const msgHashCount = new Map;
52111
+ for (const row of rows) {
52112
+ msgHashCount.set(row.messageHash, (msgHashCount.get(row.messageHash) ?? 0) + 1);
52113
+ }
52114
+ for (const row of rows) {
52115
+ row.links = msgHashCount.get(row.messageHash) ?? 1;
52116
+ }
52117
+ const tsqModelMap = new Map;
52118
+ for (const row of rows) {
52119
+ if (row.model && row.model !== "" && row.model !== "unknown") {
52120
+ const key = `${row.source}:::${row.sessionId}`;
52121
+ if (!tsqModelMap.has(key))
52122
+ tsqModelMap.set(key, row.model);
52123
+ }
52124
+ }
52125
+ const missingKeys = new Set;
52126
+ for (const row of rows) {
52127
+ if (!row.model || row.model === "" || row.model === "unknown") {
52128
+ const key = `${row.source}:::${row.sessionId}`;
52129
+ if (!tsqModelMap.has(key))
52130
+ missingKeys.add(key);
52131
+ }
52132
+ }
52133
+ if (missingKeys.size > 0) {
52134
+ const pairs = [...missingKeys].map((k) => {
52135
+ const [src, sid] = k.split(":::");
52136
+ return { src, sid };
52137
+ });
52138
+ const placeholders = pairs.map(() => "(?, ?)").join(", ");
52139
+ const batchParams = [];
52140
+ for (const { src, sid } of pairs) {
52141
+ batchParams.push(src, sid);
52142
+ }
52143
+ const modelRows = await db2.queryAll(`SELECT source, session_id, model
52144
+ FROM history_message
52145
+ WHERE (source, session_id) IN (${placeholders})
52146
+ AND model IS NOT NULL AND model != '' AND model != 'unknown'
52147
+ GROUP BY source, session_id
52148
+ LIMIT ?`, ...batchParams, pairs.length);
52149
+ for (const mr of modelRows) {
52150
+ tsqModelMap.set(`${mr.source}:::${mr.session_id}`, mr.model);
52151
+ }
52152
+ }
52153
+ for (const row of rows) {
52154
+ if (!row.model || row.model === "" || row.model === "unknown") {
52155
+ const key = `${row.source}:::${row.sessionId}`;
52156
+ const resolved = tsqModelMap.get(key);
52157
+ if (resolved)
52158
+ row.model = resolved;
52159
+ }
52160
+ }
51960
52161
  const truncated = rows.length > limit;
51961
52162
  const finalRows = truncated ? rows.slice(0, limit) : rows;
51962
52163
  return {
@@ -52470,7 +52671,7 @@ async function rollupTableFreshness(db2) {
52470
52671
  }
52471
52672
  return result;
52472
52673
  }
52473
- var ROLLUP_DEFINITION_VERSION = "v4", EMPTY_ROLLUP_WATERMARK, BUCKETED_ROLLUP_TABLES, KEYED_ROLLUP_TABLES, GLOBAL_RANKED_ROLLUP_TABLES, ALL_ROLLUP_TABLES, MSG_BUCKET_5M = `strftime('%Y-%m-%dT%H:%M:00Z', CAST(strftime('%s', m.ts) / 60 * 60 AS INTEGER), 'unixepoch')`, SKILL_BUCKET_5M = `strftime('%Y-%m-%dT%H:%M:00Z', CAST(strftime('%s', sc.started_at) / 60 * 60 AS INTEGER), 'unixepoch')`, MSG_DAY;
52674
+ var ROLLUP_DEFINITION_VERSION = "v5", EMPTY_ROLLUP_WATERMARK, BUCKETED_ROLLUP_TABLES, KEYED_ROLLUP_TABLES, GLOBAL_RANKED_ROLLUP_TABLES, ALL_ROLLUP_TABLES, MSG_BUCKET_5M = `strftime('%Y-%m-%dT%H:%M:00Z', CAST(strftime('%s', m.ts) / 60 * 60 AS INTEGER), 'unixepoch')`, SKILL_BUCKET_5M = `strftime('%Y-%m-%dT%H:%M:00Z', CAST(strftime('%s', sc.started_at) / 60 * 60 AS INTEGER), 'unixepoch')`, MSG_DAY;
52474
52675
  var init_rollup_watermark = __esm(() => {
52475
52676
  EMPTY_ROLLUP_WATERMARK = {
52476
52677
  importedAtWatermark: "",
@@ -52510,6 +52711,11 @@ async function historyBoardHistoryVersion(db2) {
52510
52711
  FROM history_message ORDER BY rowid DESC LIMIT 1`);
52511
52712
  return `v${HISTORY_BOARD_ROLLUP_VERSION}:message:${row?.importedAt ?? ""}:${row?.maxRowId ?? 0}`;
52512
52713
  }
52714
+ async function markHistoryBoardRollupsRefreshed(db2, historyVersion) {
52715
+ await db2.run(`INSERT INTO history_board_rollup_meta (id, history_version, refreshed_at)
52716
+ VALUES (1, ?, ?)
52717
+ ON CONFLICT(id) DO UPDATE SET history_version = excluded.history_version, refreshed_at = excluded.refreshed_at`, historyVersion, new Date().toISOString());
52718
+ }
52513
52719
  async function skillCallRollup(db2) {
52514
52720
  return db2.queryAll(`SELECT strftime('%Y-%m-%dT%H:%M:00Z', CAST(strftime('%s', started_at) / 60 * 60 AS INTEGER), 'unixepoch') AS bucketStart,
52515
52721
  source,
@@ -52555,9 +52761,10 @@ async function replaceHistoryBoardRollups(db2, seed) {
52555
52761
  WITH enriched AS (
52556
52762
  SELECT m.*,
52557
52763
  COALESCE(
52558
- m.model,
52764
+ NULLIF(NULLIF(m.model, ''), 'unknown'),
52559
52765
  MAX(CASE WHEN m.model IS NOT NULL AND m.model != '' AND m.model != 'unknown' THEN m.model END)
52560
52766
  OVER (PARTITION BY m.source, m.session_id),
52767
+ NULLIF(m.model, ''),
52561
52768
  'unknown'
52562
52769
  ) AS effective_model
52563
52770
  FROM history_message m
@@ -52588,9 +52795,10 @@ async function replaceHistoryBoardRollups(db2, seed) {
52588
52795
  WITH enriched AS (
52589
52796
  SELECT m.*,
52590
52797
  COALESCE(
52591
- m.model,
52798
+ NULLIF(NULLIF(m.model, ''), 'unknown'),
52592
52799
  MAX(CASE WHEN m.model IS NOT NULL AND m.model != '' AND m.model != 'unknown' THEN m.model END)
52593
52800
  OVER (PARTITION BY m.source, m.session_id),
52801
+ NULLIF(m.model, ''),
52594
52802
  'unknown'
52595
52803
  ) AS effective_model,
52596
52804
  COALESCE(
@@ -53022,7 +53230,9 @@ function buildSessionWhere(sel, alias3 = "s") {
53022
53230
  }
53023
53231
  async function historyBoardSessionsFromRollup(db2, sel, input) {
53024
53232
  const spec = buildSessionWhere(sel);
53025
- const order = SESSION_ORDER_COLUMNS[input.sortBy] ?? SESSION_ORDER_COLUMNS.start;
53233
+ const sortKey = input.sortBy ?? input.sortField ?? "start";
53234
+ const order = SESSION_ORDER_COLUMNS[sortKey] ?? SESSION_ORDER_COLUMNS.start;
53235
+ const sortDir = (input.sortDir ?? "desc").toUpperCase();
53026
53236
  const offset = (input.page - 1) * input.pageSize;
53027
53237
  const [items, total] = await Promise.all([
53028
53238
  db2.queryAll(`SELECT s.source, s.session_id AS sessionId, s.model,
@@ -53034,7 +53244,7 @@ async function historyBoardSessionsFromRollup(db2, sel, input) {
53034
53244
  s.assistant_duration_ms AS assistantDurationMs,
53035
53245
  s.top_tool AS topTool, s.state
53036
53246
  FROM history_board_session_stats s ${spec.where}
53037
- ORDER BY ${order} ${input.sortDir.toUpperCase()}, s.session_id ASC
53247
+ ORDER BY ${order} ${sortDir}, s.session_id ASC
53038
53248
  LIMIT ? OFFSET ?`, ...spec.params, input.pageSize, offset),
53039
53249
  db2.queryFirst(`SELECT COUNT(*) AS total FROM history_board_session_stats s ${spec.where}`, ...spec.params)
53040
53250
  ]);
@@ -53278,9 +53488,11 @@ function message5mBucketOps(bucket) {
53278
53488
  WITH enriched AS (
53279
53489
  SELECT m.*,
53280
53490
  COALESCE(
53281
- m.model,
53282
- MAX(CASE WHEN m.model IS NOT NULL AND m.model != '' AND m.model != 'unknown' THEN m.model END)
53283
- OVER (PARTITION BY m.source, m.session_id),
53491
+ NULLIF(NULLIF(m.model, ''), 'unknown'),
53492
+ (SELECT m2.model FROM history_message m2
53493
+ WHERE m2.source = m.source AND m2.session_id = m.session_id
53494
+ AND m2.model IS NOT NULL AND m2.model != '' AND m2.model != 'unknown'
53495
+ LIMIT 1),
53284
53496
  'unknown'
53285
53497
  ) AS effective_model
53286
53498
  FROM history_message m
@@ -53317,9 +53529,11 @@ function tool5mBucketOps(bucket) {
53317
53529
  WITH enriched AS (
53318
53530
  SELECT m.*,
53319
53531
  COALESCE(
53320
- m.model,
53321
- MAX(CASE WHEN m.model IS NOT NULL AND m.model != '' AND m.model != 'unknown' THEN m.model END)
53322
- OVER (PARTITION BY m.source, m.session_id),
53532
+ NULLIF(NULLIF(m.model, ''), 'unknown'),
53533
+ (SELECT m2.model FROM history_message m2
53534
+ WHERE m2.source = m.source AND m2.session_id = m.session_id
53535
+ AND m2.model IS NOT NULL AND m2.model != '' AND m2.model != 'unknown'
53536
+ LIMIT 1),
53323
53537
  'unknown'
53324
53538
  ) AS effective_model,
53325
53539
  COALESCE(
@@ -53799,6 +54013,23 @@ async function refreshHistoryBoardRollupsIncremental(db2) {
53799
54013
  });
53800
54014
  }
53801
54015
  }
54016
+ async function recordHistoryBoardRollupMeta(db2, historyVersion, refreshedAt = new Date().toISOString()) {
54017
+ await db2.run(`INSERT INTO history_board_rollup_meta (id, history_version, refreshed_at)
54018
+ VALUES (1, ?, ?)
54019
+ ON CONFLICT(id) DO UPDATE SET history_version = excluded.history_version, refreshed_at = excluded.refreshed_at`, historyVersion, refreshedAt);
54020
+ }
54021
+ async function hasHistoryBoardRollupRows(db2, table3) {
54022
+ const validTables = {
54023
+ history_board_message_5m: true,
54024
+ history_board_session_stats: true,
54025
+ history_board_loop_findings: true
54026
+ };
54027
+ if (!validTables[table3]) {
54028
+ throw new Error(`Invalid rollup probe table: ${table3}`);
54029
+ }
54030
+ const row = await db2.queryFirst(`SELECT 1 AS ok FROM ${table3} LIMIT 1`);
54031
+ return row?.ok === 1;
54032
+ }
53802
54033
  var MESSAGE_DEDUP2 = `(m.request_id IS NULL OR NOT EXISTS (
53803
54034
  SELECT 1 FROM history_message o
53804
54035
  WHERE o.request_id = m.request_id AND o.rowid > m.rowid
@@ -60429,6 +60660,15 @@ var init_run_dao = __esm(() => {
60429
60660
  stampMetadata(runId, metadata) {
60430
60661
  return this.mergeMetadata(runId, metadata);
60431
60662
  }
60663
+ stampRunIdentity(runId, definitionDigest, workflowVersion) {
60664
+ return this.adapter.run(`UPDATE runs
60665
+ SET metadata_json = json_set(
60666
+ COALESCE(NULLIF(metadata_json, ''), '{}'),
60667
+ '$.definitionDigest', ?1,
60668
+ '$.workflowVersion', ?2
60669
+ )
60670
+ WHERE id = ?3`, definitionDigest, workflowVersion, runId);
60671
+ }
60432
60672
  stampFailureReason(runId, reason) {
60433
60673
  return this.adapter.run(`UPDATE runs
60434
60674
  SET metadata_json = json_set(
@@ -68824,14 +69064,190 @@ var init_envelope = __esm(() => {
68824
69064
  init_serialization();
68825
69065
  });
68826
69066
 
69067
+ // ../../packages/domain/src/retention.ts
69068
+ import { readdirSync as readdirSync5, rmSync as rmSync3, statfsSync, statSync as statSync6 } from "fs";
69069
+ import { join as join6 } from "path";
69070
+ async function estimateReclaimableBytes(db2) {
69071
+ try {
69072
+ const rows = await db2.queryAll("SELECT pageno, pagetype, ncell, payload, unused FROM dbstat ORDER BY pageno");
69073
+ let slack = 0;
69074
+ for (const row of rows)
69075
+ slack += row.unused || 0;
69076
+ return Math.max(0, Math.min(slack, await databaseBytes(db2)));
69077
+ } catch {
69078
+ return 0;
69079
+ }
69080
+ }
69081
+ async function databaseBytes(db2) {
69082
+ const [pageCount, pageSize] = await Promise.all([
69083
+ db2.queryFirst("PRAGMA page_count"),
69084
+ db2.queryFirst("PRAGMA page_size")
69085
+ ]);
69086
+ return (pageCount?.page_count ?? 0) * (pageSize?.page_size ?? 0);
69087
+ }
69088
+ function hasSufficientDiskSpace(dbPath, requiredBytes) {
69089
+ try {
69090
+ const fs3 = statfsSync(dbPath);
69091
+ const freeBytes = fs3.bavail * fs3.bsize;
69092
+ return freeBytes >= requiredBytes;
69093
+ } catch {
69094
+ return true;
69095
+ }
69096
+ }
69097
+ async function compactDatabase(db2, opts) {
69098
+ const bytesBefore = await databaseBytes(db2);
69099
+ try {
69100
+ const lastRun = await db2.queryFirst("SELECT COALESCE(MAX(ran_at), 0) AS ran_at FROM spur_retention_meta WHERE kind = 'compaction'");
69101
+ const now = opts.now ?? new Date;
69102
+ const lastRunAt = lastRun?.ran_at ?? 0;
69103
+ if (lastRunAt !== undefined && lastRunAt > 0 && now.getTime() - lastRunAt < COMPACTION_MIN_INTERVAL_DAYS * DAY_MS) {
69104
+ return { ran: false, skippedReason: "recent-run", bytesBefore, bytesAfter: bytesBefore };
69105
+ }
69106
+ const reclaimable = await (opts.estimateReclaim ?? estimateReclaimableBytes)(db2);
69107
+ if (bytesBefore === 0) {
69108
+ return { ran: false, skippedReason: "empty-db", bytesBefore, bytesAfter: bytesBefore };
69109
+ }
69110
+ if (reclaimable / bytesBefore < COMPACTION_MIN_RECLAIM_RATIO) {
69111
+ return { ran: false, skippedReason: "reclaim-below-threshold", bytesBefore, bytesAfter: bytesBefore };
69112
+ }
69113
+ if (!hasSufficientDiskSpace(opts.dbPath, bytesBefore * 2)) {
69114
+ return { ran: false, skippedReason: "insufficient-disk", bytesBefore, bytesAfter: bytesBefore };
69115
+ }
69116
+ await db2.exec("VACUUM");
69117
+ await db2.exec("PRAGMA wal_checkpoint(TRUNCATE)");
69118
+ const bytesAfter = await databaseBytes(db2);
69119
+ await db2.run("INSERT INTO spur_retention_meta (kind, ran_at) VALUES ('compaction', ?)", now.getTime());
69120
+ return { ran: true, bytesBefore, bytesAfter };
69121
+ } catch {
69122
+ return { ran: false, skippedReason: "error", bytesBefore, bytesAfter: bytesBefore };
69123
+ }
69124
+ }
69125
+ async function runRetention(db2, cwd, now = new Date) {
69126
+ const result = {
69127
+ ruleEvalRuns: 0,
69128
+ queueJobs: 0,
69129
+ ledgerRows: 0,
69130
+ backupFiles: 0,
69131
+ compaction: { ran: false, skippedReason: "not-run", bytesBefore: 0, bytesAfter: 0 }
69132
+ };
69133
+ result.ruleEvalRuns = await purgeRuleEvalRuns(db2, now);
69134
+ result.queueJobs = await purgeQueueJobs(db2, now);
69135
+ result.ledgerRows = await purgeLedger(db2, now);
69136
+ result.backupFiles = pruneBackups(cwd, now);
69137
+ result.compaction = await compactDatabase(db2, { dbPath: join6(cwd, ".spur", "spur.db"), now });
69138
+ return result;
69139
+ }
69140
+ async function purgeRuleEvalRuns(db2, now) {
69141
+ return purgeRows(db2, `DELETE FROM rule_eval_runs WHERE created_at < ?`, new Date(now.getTime() - RULE_EVAL_RUN_RETENTION_DAYS * DAY_MS).toISOString());
69142
+ }
69143
+ async function purgeQueueJobs(db2, now) {
69144
+ return purgeRows(db2, `DELETE FROM queue_jobs WHERE status IN ('completed', 'failed') AND updated_at < ?`, now.getTime() - QUEUE_JOB_RETENTION_DAYS * DAY_MS);
69145
+ }
69146
+ async function purgeLedger(db2, now) {
69147
+ return purgeRows(db2, `DELETE FROM history_import_ledger WHERE imported_at < ?`, new Date(now.getTime() - LEDGER_RETENTION_DAYS * DAY_MS).toISOString());
69148
+ }
69149
+ function pruneBackups(cwd, now) {
69150
+ const dir = join6(cwd, ".spur", "backups");
69151
+ let entries;
69152
+ try {
69153
+ entries = readdirSync5(dir);
69154
+ } catch {
69155
+ return 0;
69156
+ }
69157
+ const cutoffMs = now.getTime() - BACKUP_RETENTION_DAYS * DAY_MS;
69158
+ let reclaimed = 0;
69159
+ for (const name of entries) {
69160
+ const path9 = join6(dir, name);
69161
+ try {
69162
+ const stat = statSync6(path9);
69163
+ if (!stat.isFile() || stat.mtimeMs >= cutoffMs)
69164
+ continue;
69165
+ rmSync3(path9, { force: true });
69166
+ reclaimed += 1;
69167
+ } catch {}
69168
+ }
69169
+ return reclaimed;
69170
+ }
69171
+ async function purgeRows(db2, sql4, cutoff) {
69172
+ try {
69173
+ await db2.run(sql4, cutoff);
69174
+ const row = await db2.queryFirst("SELECT changes() AS n");
69175
+ return row?.n ?? 0;
69176
+ } catch {
69177
+ return 0;
69178
+ }
69179
+ }
69180
+ var RULE_EVAL_RUN_RETENTION_DAYS = 90, QUEUE_JOB_RETENTION_DAYS = 30, LEDGER_RETENTION_DAYS = 180, BACKUP_RETENTION_DAYS = 30, DAY_MS, COMPACTION_MIN_INTERVAL_DAYS = 7, COMPACTION_MIN_RECLAIM_RATIO = 0.03;
69181
+ var init_retention = __esm(() => {
69182
+ DAY_MS = 24 * 60 * 60 * 1000;
69183
+ });
69184
+
69185
+ // ../../packages/domain/src/maintenance.ts
69186
+ async function maintainDatabase(db2, options = {}) {
69187
+ const start = performance.now();
69188
+ const bytesBefore = await databaseBytes(db2);
69189
+ let optimized = false;
69190
+ let checkpointed = false;
69191
+ let vacuumed = false;
69192
+ let vacuumSkippedReason;
69193
+ if (options.vacuum) {
69194
+ let canVacuum = true;
69195
+ if (options.dbPath && !hasSufficientDiskSpace(options.dbPath, bytesBefore * 2)) {
69196
+ canVacuum = false;
69197
+ vacuumSkippedReason = "insufficient-disk";
69198
+ }
69199
+ if (canVacuum) {
69200
+ try {
69201
+ await db2.exec("VACUUM");
69202
+ vacuumed = true;
69203
+ const now = options.now ?? new Date;
69204
+ try {
69205
+ await db2.run("INSERT INTO spur_retention_meta (kind, ran_at) VALUES ('compaction', ?)", now.getTime());
69206
+ } catch {}
69207
+ } catch {
69208
+ vacuumed = false;
69209
+ vacuumSkippedReason = "error";
69210
+ }
69211
+ }
69212
+ }
69213
+ try {
69214
+ await db2.exec("PRAGMA optimize;");
69215
+ optimized = true;
69216
+ } catch {
69217
+ optimized = false;
69218
+ }
69219
+ try {
69220
+ await db2.exec("PRAGMA wal_checkpoint(TRUNCATE);");
69221
+ checkpointed = true;
69222
+ } catch {
69223
+ checkpointed = false;
69224
+ }
69225
+ const bytesAfter = await databaseBytes(db2);
69226
+ const bytesReclaimed = Math.max(0, bytesBefore - bytesAfter);
69227
+ const durationMs = Math.round(performance.now() - start);
69228
+ return {
69229
+ optimized,
69230
+ checkpointed,
69231
+ vacuumed,
69232
+ ...vacuumSkippedReason ? { vacuumSkippedReason } : {},
69233
+ bytesBefore,
69234
+ bytesAfter,
69235
+ bytesReclaimed,
69236
+ durationMs
69237
+ };
69238
+ }
69239
+ var init_maintenance = __esm(() => {
69240
+ init_retention();
69241
+ });
69242
+
68827
69243
  // ../../packages/domain/src/planning/locks.ts
68828
69244
  import { closeSync, fsyncSync, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync7, renameSync as renameSync3, unlinkSync, writeSync } from "fs";
68829
- import { dirname as dirname7, join as join6 } from "path";
69245
+ import { dirname as dirname7, join as join7 } from "path";
68830
69246
  function lockPathForEntity(folder, entityId) {
68831
- return join6(folder, `.${entityId}.lock`);
69247
+ return join7(folder, `.${entityId}.lock`);
68832
69248
  }
68833
69249
  function createLockPath(folder) {
68834
- return join6(folder, ".create.lock");
69250
+ return join7(folder, ".create.lock");
68835
69251
  }
68836
69252
  function parseLockContent(raw) {
68837
69253
  const str = typeof raw === "string" ? raw : raw.toString();
@@ -68936,7 +69352,7 @@ function fsyncPath(path9, swallowErrors = false) {
68936
69352
  function atomicWrite(dest, content, entityId, fs3, projectName = "spur") {
68937
69353
  const dir = dirname7(dest);
68938
69354
  const rand = Math.random().toString(36).slice(2, 10);
68939
- const tempPath = join6(dir, `.${projectName}.${entityId}.${rand}.tmp`);
69355
+ const tempPath = join7(dir, `.${projectName}.${entityId}.${rand}.tmp`);
68940
69356
  fs3.writeFile(tempPath, content);
68941
69357
  fsyncPath(tempPath);
68942
69358
  renameSync3(tempPath, dest);
@@ -68945,7 +69361,7 @@ function atomicWrite(dest, content, entityId, fs3, projectName = "spur") {
68945
69361
  async function atomicWriteAsync(dest, content, entityId, fs3, projectName = "spur") {
68946
69362
  const dir = dirname7(dest);
68947
69363
  const rand = Math.random().toString(36).slice(2, 10);
68948
- const tempPath = join6(dir, `.${projectName}.${entityId}.${rand}.tmp`);
69364
+ const tempPath = join7(dir, `.${projectName}.${entityId}.${rand}.tmp`);
68949
69365
  await fs3.writeFile(tempPath, content);
68950
69366
  fsyncPath(tempPath);
68951
69367
  renameSync3(tempPath, dest);
@@ -68960,7 +69376,7 @@ function cleanupTempFiles(folder, entityId, fs3, projectName = "spur") {
68960
69376
  let count3 = 0;
68961
69377
  for (const entry of entries) {
68962
69378
  if (entry.startsWith(prefix) && entry.endsWith(suffix)) {
68963
- fs3.deleteFile(join6(folder, entry));
69379
+ fs3.deleteFile(join7(folder, entry));
68964
69380
  count3++;
68965
69381
  }
68966
69382
  }
@@ -69651,118 +70067,6 @@ var init_task_skeleton = __esm(() => {
69651
70067
  CANONICAL_INDEX = new Map(TASK_CANONICAL_SECTIONS.map((s2, i2) => [s2, i2]));
69652
70068
  });
69653
70069
 
69654
- // ../../packages/domain/src/retention.ts
69655
- import { readdirSync as readdirSync5, rmSync as rmSync3, statfsSync, statSync as statSync6 } from "fs";
69656
- import { join as join7 } from "path";
69657
- async function estimateReclaimableBytes(db2) {
69658
- try {
69659
- const rows = await db2.queryAll("SELECT pageno, pagetype, ncell, payload, unused FROM dbstat ORDER BY pageno");
69660
- let slack = 0;
69661
- for (const row of rows)
69662
- slack += row.unused || 0;
69663
- return Math.max(0, Math.min(slack, await databaseBytes(db2)));
69664
- } catch {
69665
- return 0;
69666
- }
69667
- }
69668
- async function databaseBytes(db2) {
69669
- const [pageCount, pageSize] = await Promise.all([
69670
- db2.queryFirst("PRAGMA page_count"),
69671
- db2.queryFirst("PRAGMA page_size")
69672
- ]);
69673
- return (pageCount?.page_count ?? 0) * (pageSize?.page_size ?? 0);
69674
- }
69675
- async function compactDatabase(db2, opts) {
69676
- const bytesBefore = await databaseBytes(db2);
69677
- try {
69678
- const lastRun = await db2.queryFirst("SELECT COALESCE(MAX(ran_at), 0) AS ran_at FROM spur_retention_meta WHERE kind = 'compaction'");
69679
- const now = opts.now ?? new Date;
69680
- const lastRunAt = lastRun?.ran_at ?? 0;
69681
- if (lastRunAt !== undefined && lastRunAt > 0 && now.getTime() - lastRunAt < COMPACTION_MIN_INTERVAL_DAYS * DAY_MS) {
69682
- return { ran: false, skippedReason: "recent-run", bytesBefore, bytesAfter: bytesBefore };
69683
- }
69684
- const reclaimable = await (opts.estimateReclaim ?? estimateReclaimableBytes)(db2);
69685
- if (bytesBefore === 0) {
69686
- return { ran: false, skippedReason: "empty-db", bytesBefore, bytesAfter: bytesBefore };
69687
- }
69688
- if (reclaimable / bytesBefore < COMPACTION_MIN_RECLAIM_RATIO) {
69689
- return { ran: false, skippedReason: "reclaim-below-threshold", bytesBefore, bytesAfter: bytesBefore };
69690
- }
69691
- try {
69692
- const fs3 = statfsSync(opts.dbPath);
69693
- const freeBytes = fs3.bavail * fs3.bsize;
69694
- if (freeBytes < bytesBefore * 2) {
69695
- return { ran: false, skippedReason: "insufficient-disk", bytesBefore, bytesAfter: bytesBefore };
69696
- }
69697
- } catch {}
69698
- await db2.exec("VACUUM");
69699
- const bytesAfter = await databaseBytes(db2);
69700
- await db2.run("INSERT INTO spur_retention_meta (kind, ran_at) VALUES ('compaction', ?)", now.getTime());
69701
- return { ran: true, bytesBefore, bytesAfter };
69702
- } catch {
69703
- return { ran: false, skippedReason: "error", bytesBefore, bytesAfter: bytesBefore };
69704
- }
69705
- }
69706
- async function runRetention(db2, cwd, now = new Date) {
69707
- const result = {
69708
- ruleEvalRuns: 0,
69709
- queueJobs: 0,
69710
- ledgerRows: 0,
69711
- backupFiles: 0,
69712
- compaction: { ran: false, skippedReason: "not-run", bytesBefore: 0, bytesAfter: 0 }
69713
- };
69714
- result.ruleEvalRuns = await purgeRuleEvalRuns(db2, now);
69715
- result.queueJobs = await purgeQueueJobs(db2, now);
69716
- result.ledgerRows = await purgeLedger(db2, now);
69717
- result.backupFiles = pruneBackups(cwd, now);
69718
- result.compaction = await compactDatabase(db2, { dbPath: join7(cwd, ".spur", "spur.db"), now });
69719
- return result;
69720
- }
69721
- async function purgeRuleEvalRuns(db2, now) {
69722
- return purgeRows(db2, `DELETE FROM rule_eval_runs WHERE created_at < ?`, new Date(now.getTime() - RULE_EVAL_RUN_RETENTION_DAYS * DAY_MS).toISOString());
69723
- }
69724
- async function purgeQueueJobs(db2, now) {
69725
- return purgeRows(db2, `DELETE FROM queue_jobs WHERE status IN ('completed', 'failed') AND updated_at < ?`, now.getTime() - QUEUE_JOB_RETENTION_DAYS * DAY_MS);
69726
- }
69727
- async function purgeLedger(db2, now) {
69728
- return purgeRows(db2, `DELETE FROM history_import_ledger WHERE imported_at < ?`, new Date(now.getTime() - LEDGER_RETENTION_DAYS * DAY_MS).toISOString());
69729
- }
69730
- function pruneBackups(cwd, now) {
69731
- const dir = join7(cwd, ".spur", "backups");
69732
- let entries;
69733
- try {
69734
- entries = readdirSync5(dir);
69735
- } catch {
69736
- return 0;
69737
- }
69738
- const cutoffMs = now.getTime() - BACKUP_RETENTION_DAYS * DAY_MS;
69739
- let reclaimed = 0;
69740
- for (const name of entries) {
69741
- const path9 = join7(dir, name);
69742
- try {
69743
- const stat = statSync6(path9);
69744
- if (!stat.isFile() || stat.mtimeMs >= cutoffMs)
69745
- continue;
69746
- rmSync3(path9, { force: true });
69747
- reclaimed += 1;
69748
- } catch {}
69749
- }
69750
- return reclaimed;
69751
- }
69752
- async function purgeRows(db2, sql4, cutoff) {
69753
- try {
69754
- await db2.run(sql4, cutoff);
69755
- const row = await db2.queryFirst("SELECT changes() AS n");
69756
- return row?.n ?? 0;
69757
- } catch {
69758
- return 0;
69759
- }
69760
- }
69761
- var RULE_EVAL_RUN_RETENTION_DAYS = 90, QUEUE_JOB_RETENTION_DAYS = 30, LEDGER_RETENTION_DAYS = 180, BACKUP_RETENTION_DAYS = 30, DAY_MS, COMPACTION_MIN_INTERVAL_DAYS = 7, COMPACTION_MIN_RECLAIM_RATIO = 0.03;
69762
- var init_retention = __esm(() => {
69763
- DAY_MS = 24 * 60 * 60 * 1000;
69764
- });
69765
-
69766
70070
  // ../../packages/domain/src/stage-registry/validator.ts
69767
70071
  function generateRunId() {
69768
70072
  const crypto3 = globalThis.crypto;
@@ -70070,6 +70374,7 @@ __export(exports_src, {
70070
70374
  renderMarkdown: () => renderMarkdown,
70071
70375
  renderForensics: () => renderForensics,
70072
70376
  refreshHistoryBoardRollupsIncremental: () => refreshHistoryBoardRollupsIncremental,
70377
+ recordHistoryBoardRollupMeta: () => recordHistoryBoardRollupMeta,
70073
70378
  readRollupWatermarks: () => readRollupWatermarks,
70074
70379
  readRecordedImporterSchemaVersion: () => readRecordedImporterSchemaVersion,
70075
70380
  readLockMetadata: () => readLockMetadata,
@@ -70095,6 +70400,8 @@ __export(exports_src, {
70095
70400
  mergeStubs: () => mergeStubs,
70096
70401
  matrixEntrySchema: () => matrixEntrySchema,
70097
70402
  materializeWatermarkExclude: () => materializeWatermarkExclude,
70403
+ markHistoryBoardRollupsRefreshed: () => markHistoryBoardRollupsRefreshed,
70404
+ maintainDatabase: () => maintainDatabase,
70098
70405
  loops: () => loops,
70099
70406
  loopRepeatedCallsQuery: () => loopRepeatedCallsQuery,
70100
70407
  looksLikeGherkinAc: () => looksLikeGherkinAc,
@@ -70129,6 +70436,8 @@ __export(exports_src, {
70129
70436
  historyBoardDimensionDailyFromMart: () => historyBoardDimensionDailyFromMart,
70130
70437
  historyBoardDatabaseBytes: () => historyBoardDatabaseBytes,
70131
70438
  historyBoardBucketsFromRollup: () => historyBoardBucketsFromRollup,
70439
+ hasSufficientDiskSpace: () => hasSufficientDiskSpace,
70440
+ hasHistoryBoardRollupRows: () => hasHistoryBoardRollupRows,
70132
70441
  getVolatileLayerNames: () => getVolatileLayerNames,
70133
70442
  getStageLayerSelection: () => getStageLayerSelection,
70134
70443
  getStageLayerNames: () => getStageLayerNames,
@@ -70165,6 +70474,7 @@ __export(exports_src, {
70165
70474
  deriveDimensionMartsOps: () => deriveDimensionMartsOps,
70166
70475
  deriveDimensionMarts: () => deriveDimensionMarts,
70167
70476
  dbHealthCheck: () => dbHealthCheck,
70477
+ databaseBytes: () => databaseBytes,
70168
70478
  dataWindow: () => dataWindow,
70169
70479
  dailyTokenMatrix: () => dailyTokenMatrix,
70170
70480
  createQueueConsumer: () => createQueueConsumer,
@@ -70330,6 +70640,7 @@ var init_src2 = __esm(() => {
70330
70640
  init_bdd();
70331
70641
  init_dao();
70332
70642
  init_envelope();
70643
+ init_maintenance();
70333
70644
  init_locks();
70334
70645
  init_markdown_document();
70335
70646
  init_rebuild_events();
@@ -71270,7 +71581,8 @@ function buildWorkflowSteps(def) {
71270
71581
  pause: node.pause === true,
71271
71582
  loopBack: incoming.some((e) => (order2.get(e.from) ?? -1) >= (order2.get(node.id) ?? 0)),
71272
71583
  conditional: node.id !== def.initialNode && incoming.length > 0 && incoming.every((e) => e.condition !== undefined),
71273
- nodeType: node.type ?? "action"
71584
+ nodeType: node.type ?? "action",
71585
+ ...typeof node.description === "string" && node.description !== "" ? { description: node.description } : {}
71274
71586
  };
71275
71587
  });
71276
71588
  }
@@ -71286,7 +71598,8 @@ function buildWorkflowSteps(def) {
71286
71598
  failure: failure.has(state.id),
71287
71599
  pause: state.pause === true,
71288
71600
  loopBack: incoming.some((t) => (order.get(t.from) ?? -1) >= (order.get(state.id) ?? 0)),
71289
- conditional: state.id !== def.initialState && incoming.length > 0 && incoming.every((t) => t.guard !== undefined)
71601
+ conditional: state.id !== def.initialState && incoming.length > 0 && incoming.every((t) => t.guard !== undefined),
71602
+ ...typeof state.description === "string" && state.description !== "" ? { description: state.description } : {}
71290
71603
  };
71291
71604
  });
71292
71605
  }
@@ -71297,22 +71610,30 @@ function renderWorkflowTodo(def) {
71297
71610
  lines.push("Declared step inventory in declaration order, not a predicted execution path.", "");
71298
71611
  }
71299
71612
  for (const step of buildWorkflowSteps(def)) {
71300
- const markers = [
71301
- step.initial ? "initial" : undefined,
71302
- step.terminal ? "terminal" : undefined,
71303
- step.failure ? "failure" : undefined,
71304
- step.pause ? "pause" : undefined,
71305
- step.loopBack ? "loop-back" : undefined,
71306
- step.conditional ? "conditional" : undefined,
71307
- step.nodeType !== undefined && step.nodeType !== "action" ? step.nodeType : undefined
71308
- ].filter((m) => m !== undefined);
71309
- lines.push(markers.length > 0 ? `- [ ] ${step.id} \u2014 ${markers.join(" \xB7 ")}` : `- [ ] ${step.id}`);
71613
+ lines.push(formatWorkflowStepLine(step));
71310
71614
  }
71311
71615
  return lines.join(`
71312
71616
  `);
71313
71617
  }
71618
+ function formatWorkflowStepLine(step) {
71619
+ const markers = [
71620
+ step.initial ? "initial" : undefined,
71621
+ step.terminal ? "terminal" : undefined,
71622
+ step.failure ? "failure" : undefined,
71623
+ step.pause ? "pause" : undefined,
71624
+ step.loopBack ? "loop-back" : undefined,
71625
+ step.conditional ? "conditional" : undefined,
71626
+ step.nodeType !== undefined && step.nodeType !== "action" ? step.nodeType : undefined
71627
+ ].filter((m) => m !== undefined);
71628
+ return markers.length > 0 ? `- [ ] ${step.id} \u2014 ${markers.join(" \xB7 ")}` : `- [ ] ${step.id}`;
71629
+ }
71314
71630
  function renderRunPlan(def) {
71315
- return `plan: ${buildWorkflowSteps(def).map((s2) => s2.id).join(" \u2192 ")}`;
71631
+ const kind = def.kind ?? "state-machine";
71632
+ return [
71633
+ `plan (${kind}) \u2014 declared inventory, not a predicted route:`,
71634
+ ...buildWorkflowSteps(def).map(formatWorkflowStepLine)
71635
+ ].join(`
71636
+ `);
71316
71637
  }
71317
71638
  function formatDuration(ms) {
71318
71639
  const totalSeconds = Math.round(ms / 1000);
@@ -74183,6 +74504,9 @@ var init_structural_repair = __esm(() => {
74183
74504
  function key(e) {
74184
74505
  return `${e.kind}:${e.id}:${e.code}`;
74185
74506
  }
74507
+ function isUnsuppressibleFinding(code) {
74508
+ return REQUIRED_FINDING_CODES.has(code);
74509
+ }
74186
74510
 
74187
74511
  class PlanningCheckService {
74188
74512
  fs;
@@ -74279,18 +74603,31 @@ class PlanningCheckService {
74279
74603
  }
74280
74604
  summarizeWithStatus(status, findings, strict, overrides, accepted, id) {
74281
74605
  const effectiveFindings = [];
74606
+ const notes = [];
74282
74607
  for (const f of findings) {
74608
+ if (REQUIRED_REFERENCE_CODES.has(f.code) || status === "done" && COMPLETION_FINDING_CODES.has(f.code)) {
74609
+ f.severity = "error";
74610
+ }
74611
+ const unsuppressible = isUnsuppressibleFinding(f.code);
74283
74612
  const override = overrides?.[f.code];
74284
74613
  if (override === "off") {
74285
- continue;
74614
+ if (unsuppressible) {
74615
+ notes.push(`summarize: override 'off' refused for unsuppressible ${f.code}`);
74616
+ } else {
74617
+ continue;
74618
+ }
74286
74619
  }
74287
74620
  if (override === "error" || override === "warning") {
74288
- f.severity = override;
74621
+ if (unsuppressible) {
74622
+ notes.push(`summarize: severity override for unsuppressible ${f.code} ignored (required severity preserved)`);
74623
+ } else {
74624
+ f.severity = override;
74625
+ }
74289
74626
  }
74290
74627
  if (strict && f.severity === "warning") {
74291
74628
  f.severity = "error";
74292
74629
  }
74293
- if (accepted && id) {
74630
+ if (accepted && id && !unsuppressible) {
74294
74631
  const k = key({ kind: this.docKind, id, code: f.code });
74295
74632
  const acceptedSev = accepted.get(k);
74296
74633
  if (acceptedSev !== undefined && acceptedSev === f.severity) {
@@ -74314,13 +74651,49 @@ class PlanningCheckService {
74314
74651
  missingSections.push(f.section);
74315
74652
  }
74316
74653
  }
74317
- return { status, findings: effectiveFindings, requiredSections, missingSections, pass: !hasError };
74654
+ return { status, findings: effectiveFindings, requiredSections, missingSections, pass: !hasError, notes };
74318
74655
  }
74319
74656
  }
74657
+ var REQUIRED_REFERENCE_CODES, COMPLETION_FINDING_CODES, REQUIRED_FINDING_CODES;
74320
74658
  var init_planning_check_base = __esm(() => {
74321
74659
  init_src2();
74322
74660
  init_finding_codes2();
74323
74661
  init_structural_repair();
74662
+ REQUIRED_REFERENCE_CODES = new Set([
74663
+ FINDING_CODES.L4_FEATURE_NOT_FOUND,
74664
+ FINDING_CODES.L4_PARENT_NOT_FOUND,
74665
+ FINDING_CODES.L4_DEPENDENCY_NOT_FOUND,
74666
+ FINDING_CODES.L4_LINKED_TASK_PARSE_FAILED
74667
+ ]);
74668
+ COMPLETION_FINDING_CODES = new Set([
74669
+ FINDING_CODES.L4_ORPHAN_SCENARIOS,
74670
+ FINDING_CODES.L4_UNCOVERED_FEATURE_SCENARIO,
74671
+ FINDING_CODES.L4_SCENARIO_UNVERIFIED,
74672
+ FINDING_CODES.L4_VERIFYING_INCOMPLETE_TASKS,
74673
+ FINDING_CODES.L4_VERDICT_ROWS_MATCH_NO_SCENARIO,
74674
+ FINDING_CODES.L4_MALFORMED_VERDICT_ARTIFACT,
74675
+ FINDING_CODES.L4_EVIDENCE_NOT_RECOVERABLE,
74676
+ FINDING_CODES.L4_DOGFOOD_MISSING,
74677
+ FINDING_CODES.L4_TESTING_VERDICT_STUB,
74678
+ FINDING_CODES.L4_UNCOVERED_TASK_SCENARIO,
74679
+ FINDING_CODES.L3_REVIEW_TESTING_CONTRADICTION,
74680
+ FINDING_CODES.L4_ROLLUP_SUBTASKS_OPEN,
74681
+ FINDING_CODES.L4_ROLLUP_MISSING_ROSTER,
74682
+ FINDING_CODES.L4_ROLLUP_ROSTER_NOT_DECLARED_DEPENDENCY
74683
+ ]);
74684
+ REQUIRED_FINDING_CODES = new Set([
74685
+ FINDING_CODES.L1_MARKDOWN_PARSE,
74686
+ FINDING_CODES.L1_SCHEMA_VALIDATION,
74687
+ FINDING_CODES.L2_MISSING_REQUIRED_SECTION,
74688
+ FINDING_CODES.L3_AC_BDD_ERROR,
74689
+ FINDING_CODES.L3_AC_BDD_INVALID,
74690
+ FINDING_CODES.L3_REQUIREMENTS_EMPTY,
74691
+ FINDING_CODES.L3_AC_EMPTY,
74692
+ FINDING_CODES.L3_REQUIRED_SECTION_PLACEHOLDER,
74693
+ ...REQUIRED_REFERENCE_CODES,
74694
+ FINDING_CODES.L4_PREREQUISITE_CYCLE,
74695
+ ...COMPLETION_FINDING_CODES
74696
+ ]);
74324
74697
  });
74325
74698
 
74326
74699
  // ../../packages/app/src/services/task-record.ts
@@ -74901,7 +75274,8 @@ var init_feature_check = __esm(() => {
74901
75274
  }
74902
75275
  const fm = doc2.frontmatterData ?? {};
74903
75276
  const status = fm.status ?? "backlog";
74904
- const entry = this.resolveMatrixEntry(isGroupFeature(fm), status);
75277
+ const effectiveStatus = options?.asStatus ?? status;
75278
+ const entry = this.resolveMatrixEntry(isGroupFeature(fm), effectiveStatus);
74905
75279
  this.runL2(doc2, entry, findings, raw);
74906
75280
  this.runL3(doc2, findings, fm);
74907
75281
  if (options?.featuresDir) {
@@ -74912,10 +75286,10 @@ var init_feature_check = __esm(() => {
74912
75286
  const primaryTasksDir = options?.tasksDir ?? options?.tasksDirs?.[0];
74913
75287
  const runDir = options?.runDir ?? (primaryTasksDir ? defaultVerdictRunDir(primaryTasksDir) : undefined);
74914
75288
  const taskScanDirs = options?.tasksDirs && options.tasksDirs.length > 0 ? options.tasksDirs : options?.tasksDir ? [options.tasksDir] : [];
74915
- await this.runL4(doc2, featureId2, status, taskScanDirs, dogfoodDir, runDir, findings);
75289
+ await this.runL4(doc2, featureId2, effectiveStatus, taskScanDirs, dogfoodDir, runDir, findings);
74916
75290
  return {
74917
75291
  id: featureId2,
74918
- ...this.summarizeWithStatus(status, findings, strict, options?.severityOverrides),
75292
+ ...this.summarizeWithStatus(effectiveStatus, findings, strict, options?.severityOverrides),
74919
75293
  repairs
74920
75294
  };
74921
75295
  }
@@ -75119,6 +75493,15 @@ var init_feature_check = __esm(() => {
75119
75493
  message: `Feature "${featureId2}" is verifying but ${incompleteTasks.length} linked task(s) are not done/cancelled: ${incompleteTasks.join(", ")}`
75120
75494
  });
75121
75495
  }
75496
+ if (status === "done" && incompleteTasks.length > 0) {
75497
+ findings.push({
75498
+ layer: "L4",
75499
+ code: FINDING_CODES.L4_VERIFYING_INCOMPLETE_TASKS,
75500
+ severity: "error",
75501
+ section: "",
75502
+ message: `Feature "${featureId2}" is done but ${incompleteTasks.length} linked task(s) are not done/cancelled: ${incompleteTasks.join(", ")}`
75503
+ });
75504
+ }
75122
75505
  if (status === "verifying" || status === "done") {
75123
75506
  const SELF_REFERENTIAL_PATTERNS = [
75124
75507
  /\.spur\/workflows\//,
@@ -76438,22 +76821,6 @@ var init_task_check = __esm(() => {
76438
76821
 
76439
76822
  // ../../packages/app/src/services/corpus-check.ts
76440
76823
  import { basename as basename4, dirname as dirname10, join as join13, relative, resolve as resolve3 } from "path";
76441
- function baselineSeverity(e) {
76442
- return e.severity ?? "error";
76443
- }
76444
- function resolveProjectRoot(cwd) {
76445
- const fs3 = createNodeFileSystem3(cwd);
76446
- let current = resolve3(cwd);
76447
- while (true) {
76448
- if (fs3.exists(join13(current, ".spur", "config.yaml")) || fs3.exists(join13(current, "config", "corpus-baseline.json"))) {
76449
- return current;
76450
- }
76451
- const parent = dirname10(current);
76452
- if (parent === current)
76453
- return resolve3(cwd);
76454
- current = parent;
76455
- }
76456
- }
76457
76824
  async function loadTaskMatrix(projectRoot) {
76458
76825
  const fs3 = createNodeFileSystem3(projectRoot);
76459
76826
  const candidates = [join13(projectRoot, ".spur", "tasks", "section-matrix.yaml")];
@@ -76502,9 +76869,7 @@ async function structuralSweep(projectRoot) {
76502
76869
  const wbs = fileName.match(/^(\d{4})_.+\.md$/)?.[1];
76503
76870
  if (wbs === undefined)
76504
76871
  continue;
76505
- const result = await taskService.check(join13(tasksDir, fileName), wbs, {
76506
- severityOverrides: planning.severityOverrides
76507
- });
76872
+ const result = await taskService.check(join13(tasksDir, fileName), wbs);
76508
76873
  for (const finding of result.findings) {
76509
76874
  findings.push({
76510
76875
  kind: "task",
@@ -76519,15 +76884,14 @@ async function structuralSweep(projectRoot) {
76519
76884
  if (await fs3.exists(featuresDir)) {
76520
76885
  const featureService = new FeatureCheckService(fs3);
76521
76886
  for (const fileName of await fs3.readDir(featuresDir)) {
76522
- const id = fileName.match(/^([A-Z][1-9]*)_.+\.md$/)?.[1];
76887
+ const id = fileName.match(/^([A-Z][0-9]*)_.+\.md$/)?.[1];
76523
76888
  if (id === undefined)
76524
76889
  continue;
76525
76890
  const result = await featureService.check(join13(featuresDir, fileName), id, {
76526
76891
  featuresDir,
76527
76892
  tasksDir: activeTasksDir,
76528
76893
  tasksDirs: taskDirs,
76529
- runDir: fs3.resolve(".spur/run"),
76530
- severityOverrides: planning.severityOverrides
76894
+ runDir: fs3.resolve(".spur/run")
76531
76895
  });
76532
76896
  for (const finding of result.findings) {
76533
76897
  findings.push({
@@ -76578,6 +76942,43 @@ async function duplicateIds(cwd, taskDirs, featuresDir) {
76578
76942
  }
76579
76943
  return errors6;
76580
76944
  }
76945
+ async function runCorpusCheck(cwd, since, report) {
76946
+ const projectRoot = resolveProjectRoot(cwd);
76947
+ const sweep = await structuralSweep(projectRoot);
76948
+ const fog = await ungraduatedFog(projectRoot, { since, report });
76949
+ const findings = [
76950
+ ...sweep.findings,
76951
+ ...await duplicateIds(projectRoot, sweep.taskDirs, sweep.featuresDir),
76952
+ ...fog.map((finding) => ({ ...finding, severity: "warning" }))
76953
+ ];
76954
+ const newErrors = findings.filter((finding) => finding.severity === "error");
76955
+ const newWarnings = findings.filter((finding) => finding.severity === "warning");
76956
+ return {
76957
+ observed: findings.length,
76958
+ baselined: 0,
76959
+ newErrors,
76960
+ newWarnings,
76961
+ bySeverity: {
76962
+ error: { observed: newErrors.length, baselined: 0, newCount: newErrors.length },
76963
+ warning: { observed: newWarnings.length, baselined: 0, newCount: newWarnings.length }
76964
+ },
76965
+ duplicateKeys: [],
76966
+ ok: newErrors.length === 0
76967
+ };
76968
+ }
76969
+ function resolveProjectRoot(cwd) {
76970
+ const fs3 = createNodeFileSystem3(cwd);
76971
+ let current = resolve3(cwd);
76972
+ while (true) {
76973
+ if (fs3.exists(join13(current, ".spur", "config.yaml"))) {
76974
+ return current;
76975
+ }
76976
+ const parent = dirname10(current);
76977
+ if (parent === current)
76978
+ return resolve3(cwd);
76979
+ current = parent;
76980
+ }
76981
+ }
76581
76982
  async function git(cwd, args) {
76582
76983
  try {
76583
76984
  const p = await new NodeProcessExecutor3().run({ command: "git", args, cwd, rejectOnError: false });
@@ -76742,71 +77143,6 @@ async function ungraduatedFog(cwd = process.cwd(), opts = {}) {
76742
77143
  report(`corpus-check: fog check evaluated ${spec} \u2014 ${maps} map(s) inspected.`);
76743
77144
  return errors6;
76744
77145
  }
76745
- async function collectObservedFindings(projectRoot, since) {
76746
- const sweep = await structuralSweep(projectRoot);
76747
- const fogErrors = await ungraduatedFog(projectRoot, { since, report: () => {} });
76748
- return [...sweep.findings, ...await duplicateIds(projectRoot, sweep.taskDirs, sweep.featuresDir), ...fogErrors];
76749
- }
76750
- function duplicateBaselineKeys(baseline) {
76751
- const counts = new Map;
76752
- for (const e of baseline.entries)
76753
- counts.set(key(e), (counts.get(key(e)) ?? 0) + 1);
76754
- return [...counts].filter(([, n2]) => n2 > 1).map(([k, count3]) => ({ key: k, count: count3 })).sort((a2, b) => b.count - a2.count || a2.key.localeCompare(b.key));
76755
- }
76756
- function reconcileBaseline(observed, baseline) {
76757
- const accepted = new Map(baseline.entries.map((e) => [key(e), baselineSeverity(e)]));
76758
- const unexpected = observed.filter((e) => accepted.get(key(e)) !== e.severity);
76759
- const newErrors = unexpected.filter((e) => e.severity === "error");
76760
- const newWarnings = unexpected.filter((e) => e.severity === "warning");
76761
- const bySeverity = {
76762
- error: {
76763
- observed: observed.filter((o3) => o3.severity === "error").length,
76764
- baselined: baseline.entries.filter((e) => baselineSeverity(e) === "error").length,
76765
- newCount: newErrors.length
76766
- },
76767
- warning: {
76768
- observed: observed.filter((o3) => o3.severity === "warning").length,
76769
- baselined: baseline.entries.filter((e) => baselineSeverity(e) === "warning").length,
76770
- newCount: newWarnings.length
76771
- }
76772
- };
76773
- const duplicateKeys = duplicateBaselineKeys(baseline);
76774
- return {
76775
- observed: observed.length,
76776
- baselined: baseline.entries.length,
76777
- newErrors,
76778
- newWarnings,
76779
- bySeverity,
76780
- duplicateKeys,
76781
- ok: unexpected.length === 0 && duplicateKeys.length === 0
76782
- };
76783
- }
76784
- async function runCorpusCheck(cwd, since) {
76785
- const projectRoot = resolveProjectRoot(cwd);
76786
- const baselineFile = join13(projectRoot, "config", "corpus-baseline.json");
76787
- let baseline = { entries: [] };
76788
- if (await Bun.file(baselineFile).exists()) {
76789
- baseline = await Bun.file(baselineFile).json();
76790
- }
76791
- const observed = await collectObservedFindings(projectRoot, since);
76792
- return reconcileBaseline(observed, baseline);
76793
- }
76794
- async function loadAcceptedFindings(cwd) {
76795
- const projectRoot = resolveProjectRoot(cwd);
76796
- const baselineFile = join13(projectRoot, "config", "corpus-baseline.json");
76797
- const accepted = new Map;
76798
- try {
76799
- if (await Bun.file(baselineFile).exists()) {
76800
- const baseline = await Bun.file(baselineFile).json();
76801
- if (Array.isArray(baseline?.entries)) {
76802
- for (const e of baseline.entries) {
76803
- accepted.set(key(e), baselineSeverity(e));
76804
- }
76805
- }
76806
- }
76807
- } catch {}
76808
- return accepted;
76809
- }
76810
77146
  var FOG_HEADING, OUT_OF_SCOPE_HEADING, FEATURE_ID, DEFAULT_BRANCHES;
76811
77147
  var init_corpus_check = __esm(() => {
76812
77148
  init_loader();
@@ -80027,32 +80363,13 @@ async function refreshHistoryRollups(db2) {
80027
80363
  if (await historyBoardRollupsFresh(db2)) {
80028
80364
  return { status: "unchanged", historyVersion: await historyBoardHistoryVersion(db2), cacheWasteSteps: 0 };
80029
80365
  }
80366
+ await refreshHistoryBoardRollupsIncremental(db2);
80030
80367
  const historyVersion = await historyBoardHistoryVersion(db2);
80031
- const [messageRows, toolRows, loopRows, sourceRows, tokenSteps, durationSteps, waste, cacheWasteSteps, skillRows] = await Promise.all([
80032
- messageRollup(db2, ALL_HISTORY2),
80033
- toolRollup(db2, ALL_HISTORY2),
80034
- loops(db2, ALL_HISTORY2),
80035
- sourceSummary(db2, ALL_HISTORY2),
80036
- topStepsByTokens(db2, ALL_HISTORY2, RANK_DEPTH2),
80037
- topStepsByDuration(db2, ALL_HISTORY2, RANK_DEPTH2),
80038
- cacheWasteAggregate(db2, ALL_HISTORY2),
80039
- topCacheWasteSteps(db2, ALL_HISTORY2, RANK_DEPTH2),
80040
- skillCallRollup(db2)
80041
- ]);
80042
- await replaceHistoryBoardRollups(db2, {
80043
- historyVersion,
80044
- messageRows,
80045
- toolRows,
80046
- loopRows,
80047
- sourceRows,
80048
- tokenSteps,
80049
- durationSteps,
80050
- cacheWasteSteps,
80051
- skill5m: skillRows
80052
- });
80368
+ await recordHistoryBoardRollupMeta(db2, historyVersion);
80369
+ const waste = await cacheWasteAggregate(db2, ALL_HISTORY2);
80053
80370
  return { status: "refreshed", historyVersion, cacheWasteSteps: waste?.steps ?? 0 };
80054
80371
  }
80055
- var ALL_HISTORY2, RANK_DEPTH2 = 1000, HistorySchemaVersionMismatchError;
80372
+ var ALL_HISTORY2, HistorySchemaVersionMismatchError;
80056
80373
  var init_history_analysis_service = __esm(() => {
80057
80374
  init_src2();
80058
80375
  ALL_HISTORY2 = {
@@ -80258,7 +80575,7 @@ class MockHistoryBoardService {
80258
80575
  const totalTokensWithCache = totalBilled + totalCacheSaved;
80259
80576
  const cacheSavedPercent = totalTokensWithCache > 0 ? Math.round(totalCacheSaved / totalTokensWithCache * 100) : 0;
80260
80577
  const errorRate = totalTools > 0 ? Math.round(totalErrors / totalTools * 1000) / 10 : 0;
80261
- const bucket = filter?.bucket === "auto" || filter?.bucket === undefined ? filter?.range === "1h" ? "1m" : filter?.range === "4h" ? "3m" : filter?.range === "24h" ? "10m" : filter?.range === "7d" ? "30m" : "1d" : filter.bucket;
80578
+ const bucket = filter?.bucket === "auto" || filter?.bucket === undefined ? filter?.range === "1h" ? "1m" : filter?.range === "4h" ? "1m" : filter?.range === "24h" ? "10m" : filter?.range === "7d" ? "30m" : "1d" : filter.bucket;
80262
80579
  const bucketInterval = {
80263
80580
  "1m": 1 * 60000,
80264
80581
  "3m": 3 * 60000,
@@ -80270,6 +80587,14 @@ class MockHistoryBoardService {
80270
80587
  "1d": 86400000
80271
80588
  }[bucket];
80272
80589
  const dimension = filter?.dimension ?? "model";
80590
+ const ensureBucket = (map2, key2) => {
80591
+ let entry = map2[key2];
80592
+ if (!entry) {
80593
+ entry = { total: 0, cacheRead: 0, output: 0, series: {} };
80594
+ map2[key2] = entry;
80595
+ }
80596
+ return entry;
80597
+ };
80273
80598
  const buckets = {};
80274
80599
  const modelBuckets = {};
80275
80600
  const sourceBuckets = {};
@@ -80277,35 +80602,35 @@ class MockHistoryBoardService {
80277
80602
  const skillBuckets = {};
80278
80603
  for (const s3 of matching) {
80279
80604
  const bKey = new Date(Math.floor(s3.start / bucketInterval) * bucketInterval).toISOString();
80280
- if (!buckets[bKey])
80281
- buckets[bKey] = { total: 0, cacheRead: 0, series: {} };
80282
- if (!modelBuckets[bKey])
80283
- modelBuckets[bKey] = { total: 0, cacheRead: 0, series: {} };
80284
- if (!sourceBuckets[bKey])
80285
- sourceBuckets[bKey] = { total: 0, cacheRead: 0, series: {} };
80286
- if (!toolBuckets[bKey])
80287
- toolBuckets[bKey] = { total: 0, cacheRead: 0, series: {} };
80288
- if (!skillBuckets[bKey])
80289
- skillBuckets[bKey] = { total: 0, cacheRead: 0, series: {} };
80290
- buckets[bKey].total += s3.tokens.billedTokens;
80291
- buckets[bKey].cacheRead += s3.tokens.cacheReadTokens;
80292
- modelBuckets[bKey].total += s3.tokens.billedTokens;
80293
- modelBuckets[bKey].cacheRead += s3.tokens.cacheReadTokens;
80294
- sourceBuckets[bKey].total += s3.tokens.billedTokens;
80295
- sourceBuckets[bKey].cacheRead += s3.tokens.cacheReadTokens;
80296
- toolBuckets[bKey].total += s3.tokens.billedTokens;
80297
- toolBuckets[bKey].cacheRead += s3.tokens.cacheReadTokens;
80298
- skillBuckets[bKey].total += s3.tokens.billedTokens;
80299
- skillBuckets[bKey].cacheRead += s3.tokens.cacheReadTokens;
80300
- modelBuckets[bKey].series[s3.model] = (modelBuckets[bKey].series[s3.model] ?? 0) + s3.tokens.billedTokens;
80301
- sourceBuckets[bKey].series[s3.source] = (sourceBuckets[bKey].series[s3.source] ?? 0) + s3.tokens.billedTokens;
80605
+ const b = ensureBucket(buckets, bKey);
80606
+ const mb = ensureBucket(modelBuckets, bKey);
80607
+ const sb = ensureBucket(sourceBuckets, bKey);
80608
+ const tb = ensureBucket(toolBuckets, bKey);
80609
+ const kb = ensureBucket(skillBuckets, bKey);
80610
+ b.total += s3.tokens.billedTokens;
80611
+ b.cacheRead += s3.tokens.cacheReadTokens;
80612
+ b.output += s3.tokens.outputTokens;
80613
+ mb.total += s3.tokens.billedTokens;
80614
+ mb.cacheRead += s3.tokens.cacheReadTokens;
80615
+ mb.output += s3.tokens.outputTokens;
80616
+ sb.total += s3.tokens.billedTokens;
80617
+ sb.cacheRead += s3.tokens.cacheReadTokens;
80618
+ sb.output += s3.tokens.outputTokens;
80619
+ tb.total += s3.tokens.billedTokens;
80620
+ tb.cacheRead += s3.tokens.cacheReadTokens;
80621
+ tb.output += s3.tokens.outputTokens;
80622
+ kb.total += s3.tokens.billedTokens;
80623
+ kb.cacheRead += s3.tokens.cacheReadTokens;
80624
+ kb.output += s3.tokens.outputTokens;
80625
+ mb.series[s3.model] = (mb.series[s3.model] ?? 0) + s3.tokens.billedTokens;
80626
+ sb.series[s3.source] = (sb.series[s3.source] ?? 0) + s3.tokens.billedTokens;
80302
80627
  const toolWeight = Object.values(s3.toolMix).reduce((sum, count3) => sum + count3, 0);
80303
80628
  for (const [tool, count3] of Object.entries(s3.toolMix)) {
80304
- toolBuckets[bKey].series[tool] = (toolBuckets[bKey].series[tool] ?? 0) + (toolWeight > 0 ? s3.tokens.billedTokens * count3 / toolWeight : 0);
80629
+ tb.series[tool] = (tb.series[tool] ?? 0) + (toolWeight > 0 ? s3.tokens.billedTokens * count3 / toolWeight : 0);
80305
80630
  }
80306
80631
  const skillWeight = Object.values(s3.skillMix).reduce((sum, count3) => sum + count3, 0);
80307
80632
  for (const [skill, count3] of Object.entries(s3.skillMix)) {
80308
- skillBuckets[bKey].series[skill] = (skillBuckets[bKey].series[skill] ?? 0) + (skillWeight > 0 ? s3.tokens.billedTokens * count3 / skillWeight : 0);
80633
+ kb.series[skill] = (kb.series[skill] ?? 0) + (skillWeight > 0 ? s3.tokens.billedTokens * count3 / skillWeight : 0);
80309
80634
  }
80310
80635
  const dimensions = dimension === "model" ? [[s3.model, s3.tokens.billedTokens]] : dimension === "source" ? [[s3.source, s3.tokens.billedTokens]] : Object.entries(dimension === "tool" ? s3.toolMix : s3.skillMix).map(([key2, count3]) => [
80311
80636
  key2,
@@ -80314,15 +80639,17 @@ class MockHistoryBoardService {
80314
80639
  const weight = dimensions.reduce((sum, [, count3]) => sum + count3, 0);
80315
80640
  for (const [key2, count3] of dimensions) {
80316
80641
  const tokens = dimension === "model" || dimension === "source" ? count3 : weight > 0 ? s3.tokens.billedTokens * count3 / weight : 0;
80317
- buckets[bKey].series[key2] = (buckets[bKey].series[key2] ?? 0) + tokens;
80642
+ b.series[key2] = (b.series[key2] ?? 0) + tokens;
80318
80643
  }
80319
80644
  }
80320
80645
  const toTimeSeries = (map2) => Object.entries(map2).sort(([a2], [b]) => a2.localeCompare(b)).map(([bKey, bVal]) => {
80321
80646
  const denom = bVal.total + bVal.cacheRead;
80322
80647
  const ratio = denom > 0 ? Math.round(bVal.cacheRead / denom * 100) : 0;
80648
+ const gainRatio = denom > 0 ? Math.round(bVal.output / denom * 1000) / 10 : 0;
80323
80649
  return {
80324
80650
  bucketStart: bKey,
80325
80651
  cacheHitRatio: ratio,
80652
+ gainRatio,
80326
80653
  series: bVal.series
80327
80654
  };
80328
80655
  });
@@ -81239,7 +81566,7 @@ function resolveBucket(bucket, range = "4h") {
81239
81566
  if (range === "1h")
81240
81567
  return "1m";
81241
81568
  if (range === "4h")
81242
- return "3m";
81569
+ return "1m";
81243
81570
  if (range === "24h")
81244
81571
  return "10m";
81245
81572
  if (range === "7d")
@@ -81364,9 +81691,12 @@ function projectSummary(rows, extras) {
81364
81691
  const useCalls = totalBucketTokens === 0;
81365
81692
  const timeSeries = new Map;
81366
81693
  for (const row of rows.buckets) {
81367
- const point = timeSeries.get(row.bucketStart) ?? { cacheRead: 0, billed: 0, series: {} };
81368
- const billed = useCalls ? row.calls ?? 0 : (row.freshInputTokens ?? 0) + (row.outputTokens ?? 0);
81694
+ const point = timeSeries.get(row.bucketStart) ?? { cacheRead: 0, billed: 0, output: 0, series: {} };
81695
+ const fresh = row.freshInputTokens ?? 0;
81696
+ const output2 = row.outputTokens ?? 0;
81697
+ const billed = useCalls ? row.calls ?? 0 : fresh + output2;
81369
81698
  point.billed += billed;
81699
+ point.output += output2;
81370
81700
  point.cacheRead += row.cacheReadTokens ?? 0;
81371
81701
  point.series[row.key] = (point.series[row.key] ?? 0) + billed;
81372
81702
  timeSeries.set(row.bucketStart, point);
@@ -81400,11 +81730,15 @@ function projectSummary(rows, extras) {
81400
81730
  toolCallsCount: rows.toolCalls,
81401
81731
  errorRate: rows.toolCalls > 0 ? Math.round(rows.toolErrors / rows.toolCalls * 1000) / 10 : 0
81402
81732
  },
81403
- timeSeries: Array.from(timeSeries.entries()).map(([bucketStart, point]) => ({
81404
- bucketStart,
81405
- cacheHitRatio: point.billed + point.cacheRead > 0 ? Math.round(point.cacheRead / (point.billed + point.cacheRead) * 100) : 0,
81406
- series: point.series
81407
- })),
81733
+ timeSeries: Array.from(timeSeries.entries()).map(([bucketStart, point]) => {
81734
+ const totalTokens = point.billed + point.cacheRead;
81735
+ return {
81736
+ bucketStart,
81737
+ cacheHitRatio: totalTokens > 0 ? Math.round(point.cacheRead / totalTokens * 100) : 0,
81738
+ gainRatio: totalTokens > 0 ? Math.round(point.output / totalTokens * 1000) / 10 : 0,
81739
+ series: point.series
81740
+ };
81741
+ }),
81408
81742
  topModels: toTopItems(rows.models, false),
81409
81743
  topSources: toTopItems(rows.sources, true),
81410
81744
  topTools: topToolRows.map((row) => ({
@@ -81546,18 +81880,25 @@ function projectSkillTimeSeries(buckets) {
81546
81880
  const useCalls = totalTokens === 0;
81547
81881
  const byBucket = new Map;
81548
81882
  for (const row of buckets) {
81549
- const point = byBucket.get(row.bucketStart) ?? { cacheRead: 0, billed: 0, series: {} };
81550
- const val = useCalls ? row.calls ?? 0 : (row.freshInputTokens ?? 0) + (row.outputTokens ?? 0);
81883
+ const point = byBucket.get(row.bucketStart) ?? { cacheRead: 0, billed: 0, output: 0, series: {} };
81884
+ const fresh = row.freshInputTokens ?? 0;
81885
+ const output2 = row.outputTokens ?? 0;
81886
+ const val = useCalls ? row.calls ?? 0 : fresh + output2;
81551
81887
  point.billed += val;
81888
+ point.output += output2;
81552
81889
  point.cacheRead += row.cacheReadTokens ?? 0;
81553
81890
  point.series[row.key] = (point.series[row.key] ?? 0) + val;
81554
81891
  byBucket.set(row.bucketStart, point);
81555
81892
  }
81556
- return Array.from(byBucket.entries()).sort(([a2], [b]) => a2.localeCompare(b)).map(([bucketStart, point]) => ({
81557
- bucketStart,
81558
- cacheHitRatio: point.billed + point.cacheRead > 0 ? Math.round(point.cacheRead / (point.billed + point.cacheRead) * 100) : 0,
81559
- series: point.series
81560
- }));
81893
+ return Array.from(byBucket.entries()).sort(([a2], [b]) => a2.localeCompare(b)).map(([bucketStart, point]) => {
81894
+ const totalTokens2 = point.billed + point.cacheRead;
81895
+ return {
81896
+ bucketStart,
81897
+ cacheHitRatio: totalTokens2 > 0 ? Math.round(point.cacheRead / totalTokens2 * 100) : 0,
81898
+ gainRatio: totalTokens2 > 0 ? Math.round(point.output / totalTokens2 * 1000) / 10 : 0,
81899
+ series: point.series
81900
+ };
81901
+ });
81561
81902
  }
81562
81903
  function projectPreviousKpis(rows) {
81563
81904
  const totalBilledTokens = rows.models.reduce((sum, row) => sum + row.freshInputTokens + row.outputTokens, 0);
@@ -82430,7 +82771,10 @@ class LiveHistoryBoardService {
82430
82771
  }
82431
82772
  const sel = toArtifactSelector(filter);
82432
82773
  const rollupsFresh = await historyBoardRollupsFresh(db2);
82433
- const [loopRows, cacheWasteRows, sessionRows, largeSteps, slowStepsRows, modelCompRows] = rollupsFresh ? await Promise.all([
82774
+ const hasRollupRows = await hasHistoryBoardRollupRows(db2, "history_board_loop_findings");
82775
+ const useRollup = rollupsFresh || hasRollupRows;
82776
+ const effectiveSel = rollupsFresh ? sel : boundStaleSelector(sel);
82777
+ const [loopRows, cacheWasteRows, sessionRows, largeSteps, slowStepsRows, modelCompRows] = useRollup ? await Promise.all([
82434
82778
  historyBoardLoopsFromRollup(db2, sel, 100),
82435
82779
  historyBoardRankedStepsFromRollup(db2, sel, "cache-waste", 10),
82436
82780
  historyBoardHeavySessionsFromRollup(db2, sel, 5),
@@ -82438,57 +82782,69 @@ class LiveHistoryBoardService {
82438
82782
  historyBoardRankedStepsFromRollup(db2, sel, "duration", 10),
82439
82783
  historyBoardModelComparisonFromRollup(db2, sel)
82440
82784
  ]) : await Promise.all([
82441
- loops(db2, sel),
82442
- topCacheWasteSteps(db2, sel, 10),
82443
- bySession(db2, sel, 5),
82444
- topStepsByTokens(db2, sel, 10),
82445
- topStepsByDuration(db2, sel, 10),
82446
- modelComparison(db2, sel)
82785
+ loops(db2, effectiveSel),
82786
+ topCacheWasteSteps(db2, effectiveSel, 10),
82787
+ bySession(db2, effectiveSel, 5),
82788
+ topStepsByTokens(db2, effectiveSel, 10),
82789
+ topStepsByDuration(db2, effectiveSel, 10),
82790
+ modelComparison(db2, effectiveSel)
82447
82791
  ]);
82448
- const loopFindings = await Promise.all(loopRows.map(async (l) => {
82449
- let repeatedCalls = [];
82450
- try {
82451
- const callRows = await loopRepeatedCallsQuery(db2, {
82452
- source: l.source,
82453
- sessionId: l.sessionId,
82454
- toolName: l.toolName,
82455
- argsDigest: l.argsDigest,
82456
- limit: 50
82457
- });
82458
- repeatedCalls = callRows.map((row) => {
82459
- const category = toolCategory(row.toolName);
82460
- const links = row.links && row.links > 0 ? row.links : 1;
82461
- const billedTokens = row.inputTokens + row.outputTokens;
82462
- return {
82463
- seq: row.toolSeq,
82464
- toolSeq: row.toolSeq,
82465
- ts: row.ts,
82466
- toolName: row.toolName,
82467
- category,
82468
- status: row.status === "ok" ? "ok" : row.status === "error" ? "error" : "unknown",
82469
- durationMs: row.durationMs,
82470
- durationSource: row.durationMs !== null ? "measured" : "unmeasured",
82471
- resultBytes: row.resultBytes,
82472
- argsRaw: row.argsRaw,
82473
- argsDigest: row.argsDigest,
82474
- errorText: row.errorText,
82475
- callId: row.callId,
82476
- messageHash: row.messageHash,
82477
- sessionId: row.sessionId,
82478
- source: row.source,
82479
- model: row.model,
82480
- tokens: {
82481
- billedTokens: Math.round(billedTokens / links),
82482
- freshInputTokens: Math.round(row.inputTokens / links),
82483
- cacheReadTokens: Math.round(row.cacheReadTokens / links),
82484
- outputTokens: Math.round(row.outputTokens / links),
82485
- cacheSavedTokens: 0
82486
- }
82487
- };
82488
- });
82489
- } catch {
82490
- repeatedCalls = [];
82792
+ const EAGER_LOOP_DETAIL_LIMIT = 10;
82793
+ const loopFindings = await Promise.all(loopRows.map(async (l, idx) => {
82794
+ let repeatedCalls;
82795
+ let avgTokens = 250;
82796
+ if (idx < EAGER_LOOP_DETAIL_LIMIT) {
82797
+ try {
82798
+ const callRows = await loopRepeatedCallsQuery(db2, {
82799
+ source: l.source,
82800
+ sessionId: l.sessionId,
82801
+ toolName: l.toolName,
82802
+ argsDigest: l.argsDigest,
82803
+ model: l.model,
82804
+ limit: 50
82805
+ });
82806
+ if (callRows.length > 0) {
82807
+ const totalSampleTokens = callRows.reduce((acc, r) => acc + (r.inputTokens ?? 0) + (r.outputTokens ?? 0), 0);
82808
+ avgTokens = Math.max(1, Math.round(totalSampleTokens / callRows.length));
82809
+ }
82810
+ repeatedCalls = callRows.map((row) => {
82811
+ const category = toolCategory(row.toolName);
82812
+ const freshInputTokens = row.inputTokens ?? 0;
82813
+ const cacheReadTokens = row.cacheReadTokens ?? 0;
82814
+ const outputTokens = row.outputTokens ?? 0;
82815
+ const billedTokens = freshInputTokens + outputTokens;
82816
+ return {
82817
+ seq: row.toolSeq,
82818
+ toolSeq: row.toolSeq,
82819
+ ts: row.ts,
82820
+ toolName: row.toolName,
82821
+ category,
82822
+ status: row.status === "ok" ? "ok" : row.status === "error" ? "error" : "unknown",
82823
+ durationMs: row.durationMs ?? null,
82824
+ durationSource: row.durationMs !== null ? "measured" : "unmeasured",
82825
+ resultBytes: row.resultBytes ?? null,
82826
+ argsRaw: row.argsRaw ?? null,
82827
+ argsDigest: row.argsDigest ?? null,
82828
+ errorText: row.errorText ?? null,
82829
+ callId: row.callId ?? null,
82830
+ messageHash: row.messageHash,
82831
+ sessionId: row.sessionId,
82832
+ source: row.source,
82833
+ model: row.model ?? null,
82834
+ tokens: {
82835
+ billedTokens,
82836
+ freshInputTokens,
82837
+ cacheReadTokens,
82838
+ outputTokens,
82839
+ cacheSavedTokens: 0
82840
+ }
82841
+ };
82842
+ });
82843
+ } catch {
82844
+ repeatedCalls = [];
82845
+ }
82491
82846
  }
82847
+ const wastedTokens = Math.max(0, l.repeats - 1) * avgTokens;
82492
82848
  return {
82493
82849
  tool: l.toolName,
82494
82850
  argsHint: l.argsDigest ?? "repeated execution",
@@ -82496,7 +82852,7 @@ class LiveHistoryBoardService {
82496
82852
  repeats: l.repeats,
82497
82853
  fromSeq: l.firstSeq,
82498
82854
  toSeq: l.lastSeq,
82499
- wastedTokens: l.repeats * 250,
82855
+ wastedTokens,
82500
82856
  repeatedCalls
82501
82857
  };
82502
82858
  }));
@@ -83200,6 +83556,14 @@ class HistoryService {
83200
83556
  const db2 = await this.ctx.getDb();
83201
83557
  return resetHistoryTables(db2);
83202
83558
  }
83559
+ async maintain(opts = {}) {
83560
+ const db2 = await this.ctx.getDb();
83561
+ const dbPath = this.ctx.cwd ? join15(this.ctx.cwd, ".spur", "spur.db") : undefined;
83562
+ return maintainDatabase(db2, {
83563
+ ...opts,
83564
+ ...dbPath && opts.dbPath === undefined ? { dbPath } : {}
83565
+ });
83566
+ }
83203
83567
  async analyze(selector, opts = {}) {
83204
83568
  const top = opts.top ?? 20;
83205
83569
  const db2 = await this.ctx.getDb();
@@ -83353,6 +83717,12 @@ class HistoryService {
83353
83717
  attribution2.ambiguousEvidence += sourceAttribution.ambiguousEvidence;
83354
83718
  }
83355
83719
  }
83720
+ if (opts.dryRun !== true && opts.skipRollupRefresh !== true) {
83721
+ const db2 = await this.ctx.getDb();
83722
+ try {
83723
+ await refreshHistoryRollups(db2);
83724
+ } catch {}
83725
+ }
83356
83726
  return { entries, exitCode: computeExitCode(entries), warnings, attribution: attribution2 };
83357
83727
  }
83358
83728
  async daily(opts = {}) {
@@ -90317,7 +90687,6 @@ var init_http_request = __esm(() => {
90317
90687
 
90318
90688
  // ../../packages/app/src/workflow/composition-baseline.ts
90319
90689
  import crypto3 from "crypto";
90320
- import { resolve as resolve11 } from "path";
90321
90690
  function canonicalJsonStringify(obj) {
90322
90691
  if (obj === null || typeof obj !== "object") {
90323
90692
  return JSON.stringify(obj);
@@ -90379,131 +90748,7 @@ function extractResolvedWorkflowFacts(workflow) {
90379
90748
  actions
90380
90749
  };
90381
90750
  }
90382
- async function checkWorkflowComposition(options) {
90383
- const fs3 = options.fileSystem ?? createNodeFileSystem3();
90384
- const baselineFile = options.baselinePath ?? resolve11(options.projectRoot, "config/workflow-composition-baseline.json");
90385
- let baseline = options.baseline;
90386
- if (!baseline) {
90387
- if (!await fs3.exists(baselineFile)) {
90388
- return {
90389
- pass: false,
90390
- errors: [`Baseline file not found at ${baselineFile}`],
90391
- diffs: []
90392
- };
90393
- }
90394
- const content = await fs3.readFile(baselineFile);
90395
- try {
90396
- baseline = JSON.parse(content);
90397
- } catch (e) {
90398
- return {
90399
- pass: false,
90400
- errors: [`Failed to parse baseline JSON: ${e instanceof Error ? e.message : String(e)}`],
90401
- diffs: []
90402
- };
90403
- }
90404
- }
90405
- const errors6 = [];
90406
- const diffs = [];
90407
- for (const [name, expected] of Object.entries(baseline.workflows)) {
90408
- const defPath = resolve11(options.projectRoot, expected.definition);
90409
- if (!await fs3.exists(defPath)) {
90410
- errors6.push(`Workflow definition for "${name}" missing at ${expected.definition}`);
90411
- diffs.push({
90412
- workflow: name,
90413
- field: "definition",
90414
- expected: expected.definition,
90415
- actual: null
90416
- });
90417
- continue;
90418
- }
90419
- let def;
90420
- try {
90421
- def = await loadWorkflowDef(defPath, { validateSchema: false });
90422
- } catch (e) {
90423
- errors6.push(`Failed to load workflow definition "${name}": ${e instanceof Error ? e.message : String(e)}`);
90424
- continue;
90425
- }
90426
- const facts = extractResolvedWorkflowFacts(def);
90427
- const sortedExpectedTerminals = [...expected.terminalStates].sort();
90428
- const sortedActualTerminals = [...facts.terminalStates].sort();
90429
- if (JSON.stringify(sortedExpectedTerminals) !== JSON.stringify(sortedActualTerminals)) {
90430
- errors6.push(`Workflow "${name}" terminalStates mismatch`);
90431
- diffs.push({
90432
- workflow: name,
90433
- field: "terminalStates",
90434
- expected: expected.terminalStates,
90435
- actual: facts.terminalStates
90436
- });
90437
- }
90438
- const sortedExpectedModelQueries = [...expected.modelQueries].sort();
90439
- const sortedActualModelQueries = [...facts.modelQueries].sort();
90440
- if (JSON.stringify(sortedExpectedModelQueries) !== JSON.stringify(sortedActualModelQueries)) {
90441
- errors6.push(`Workflow "${name}" modelQueries mismatch`);
90442
- diffs.push({
90443
- workflow: name,
90444
- field: "modelQueries",
90445
- expected: expected.modelQueries,
90446
- actual: facts.modelQueries
90447
- });
90448
- }
90449
- const expectedActionKeys = Object.keys(expected.actions).sort();
90450
- const actualActionKeys = Object.keys(facts.actions).sort();
90451
- for (const actionKey of expectedActionKeys) {
90452
- if (!facts.actions[actionKey]) {
90453
- errors6.push(`Workflow "${name}" action "${actionKey}" is listed in baseline but missing from live definition`);
90454
- diffs.push({
90455
- workflow: name,
90456
- field: `actions.${actionKey}`,
90457
- expected: expected.actions[actionKey],
90458
- actual: null
90459
- });
90460
- } else {
90461
- const expAction = expected.actions[actionKey];
90462
- const actAction = facts.actions[actionKey];
90463
- if (expAction && actAction) {
90464
- if (expAction.kind !== actAction.kind) {
90465
- errors6.push(`Workflow "${name}" action "${actionKey}" kind mismatch: expected ${expAction.kind}, got ${actAction.kind}`);
90466
- diffs.push({
90467
- workflow: name,
90468
- field: `actions.${actionKey}.kind`,
90469
- expected: expAction.kind,
90470
- actual: actAction.kind
90471
- });
90472
- }
90473
- if (expAction.invocation !== actAction.invocation) {
90474
- errors6.push(`Workflow "${name}" action "${actionKey}" invocation mismatch`);
90475
- diffs.push({
90476
- workflow: name,
90477
- field: `actions.${actionKey}.invocation`,
90478
- expected: expAction.invocation,
90479
- actual: actAction.invocation
90480
- });
90481
- }
90482
- }
90483
- }
90484
- }
90485
- for (const actionKey of actualActionKeys) {
90486
- if (!expected.actions[actionKey]) {
90487
- errors6.push(`Workflow "${name}" action "${actionKey}" present in live definition but unlisted in baseline`);
90488
- diffs.push({
90489
- workflow: name,
90490
- field: `actions.${actionKey}`,
90491
- expected: null,
90492
- actual: facts.actions[actionKey]
90493
- });
90494
- }
90495
- }
90496
- }
90497
- return {
90498
- pass: errors6.length === 0,
90499
- errors: errors6,
90500
- diffs
90501
- };
90502
- }
90503
- var init_composition_baseline = __esm(() => {
90504
- init_dist8();
90505
- init_dist5();
90506
- });
90751
+ var init_composition_baseline = () => {};
90507
90752
 
90508
90753
  // ../../packages/app/src/workflow/proof-input-fingerprint.ts
90509
90754
  import crypto4 from "crypto";
@@ -90810,7 +91055,7 @@ var init_rule_check = __esm(() => {
90810
91055
  });
90811
91056
 
90812
91057
  // ../../packages/app/src/workflow/actions/run-artifact.ts
90813
- import { normalize as normalize3, resolve as resolve12 } from "path";
91058
+ import { normalize as normalize3, resolve as resolve11 } from "path";
90814
91059
 
90815
91060
  class RunArtifactActionRunner {
90816
91061
  getDb;
@@ -90838,8 +91083,8 @@ class RunArtifactActionRunner {
90838
91083
  };
90839
91084
  }
90840
91085
  const workdir = context4.workdir ?? process.cwd();
90841
- const allowedDir = resolve12(workdir, ".spur", "run");
90842
- const resolvedPath = resolve12(workdir, pathRaw);
91086
+ const allowedDir = resolve11(workdir, ".spur", "run");
91087
+ const resolvedPath = resolve11(workdir, pathRaw);
90843
91088
  const normalized = normalize3(resolvedPath);
90844
91089
  if (!normalized.startsWith(allowedDir)) {
90845
91090
  return {
@@ -91206,7 +91451,7 @@ var init_checkpoint_contract = __esm(() => {
91206
91451
  });
91207
91452
 
91208
91453
  // ../../packages/app/src/workflow/workflow-resolver.ts
91209
- import { basename as basename7, isAbsolute as isAbsolute6, join as join23, resolve as resolve13 } from "path";
91454
+ import { basename as basename7, isAbsolute as isAbsolute6, join as join23, resolve as resolve12 } from "path";
91210
91455
  function createEmbeddedSchemaOptions(embedded) {
91211
91456
  if (embedded === undefined || embedded.size === 0)
91212
91457
  return;
@@ -91237,7 +91482,7 @@ async function readWorkflowNameFast(fs3, filePath) {
91237
91482
  }
91238
91483
  async function scanWorkflowByName(cwd, name) {
91239
91484
  const fs3 = createNodeFileSystem3();
91240
- const projectSpurDir = resolve13(cwd, ".spur", "workflows");
91485
+ const projectSpurDir = resolve12(cwd, ".spur", "workflows");
91241
91486
  if (fs3.exists(projectSpurDir)) {
91242
91487
  try {
91243
91488
  const files = (await fs3.readDir(projectSpurDir)).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
@@ -91270,7 +91515,7 @@ async function scanWorkflowByName(cwd, name) {
91270
91515
  }
91271
91516
  function resolveWorkflowFile(cwd, file2) {
91272
91517
  const fs3 = createNodeFileSystem3();
91273
- const projectPath = resolve13(cwd, file2);
91518
+ const projectPath = resolve12(cwd, file2);
91274
91519
  if (fs3.exists(projectPath)) {
91275
91520
  return { path: projectPath, source: "project" };
91276
91521
  }
@@ -91278,20 +91523,20 @@ function resolveWorkflowFile(cwd, file2) {
91278
91523
  const base2 = basename7(file2);
91279
91524
  if (!isAbsolute6(file2)) {
91280
91525
  const withYaml = isYaml ? file2 : `${file2}.yaml`;
91281
- const projectSpur = resolve13(cwd, ".spur", "workflows", withYaml);
91526
+ const projectSpur = resolve12(cwd, ".spur", "workflows", withYaml);
91282
91527
  if (fs3.exists(projectSpur)) {
91283
91528
  return { path: projectSpur, source: "project" };
91284
91529
  }
91285
91530
  if (!isYaml) {
91286
- const projectDirect = resolve13(cwd, `${file2}.yaml`);
91531
+ const projectDirect = resolve12(cwd, `${file2}.yaml`);
91287
91532
  if (fs3.exists(projectDirect)) {
91288
91533
  return { path: projectDirect, source: "project" };
91289
91534
  }
91290
- const projectSpurPipeline = resolve13(cwd, ".spur", "workflows", `${file2}-pipeline.yaml`);
91535
+ const projectSpurPipeline = resolve12(cwd, ".spur", "workflows", `${file2}-pipeline.yaml`);
91291
91536
  if (fs3.exists(projectSpurPipeline)) {
91292
91537
  return { path: projectSpurPipeline, source: "project" };
91293
91538
  }
91294
- const projectDirectPipeline = resolve13(cwd, `${file2}-pipeline.yaml`);
91539
+ const projectDirectPipeline = resolve12(cwd, `${file2}-pipeline.yaml`);
91295
91540
  if (fs3.exists(projectDirectPipeline)) {
91296
91541
  return { path: projectDirectPipeline, source: "project" };
91297
91542
  }
@@ -91355,9 +91600,9 @@ var init_workflow_resolver = __esm(() => {
91355
91600
  });
91356
91601
 
91357
91602
  // ../../packages/app/src/services/workflow-service.ts
91358
- import { existsSync as existsSync10, readFileSync as readFileSync10, realpathSync as realpathSync5 } from "fs";
91603
+ import { realpathSync as realpathSync5 } from "fs";
91359
91604
  import { homedir as homedir8 } from "os";
91360
- import { basename as basename8, dirname as dirname17, join as join24, resolve as resolve14, sep as sep3 } from "path";
91605
+ import { basename as basename8, dirname as dirname17, join as join24, resolve as resolve13, sep as sep3 } from "path";
91361
91606
  function signalSubprocess(pid) {
91362
91607
  if (!Number.isInteger(pid) || pid <= 1)
91363
91608
  return false;
@@ -91400,13 +91645,9 @@ function withSelfPidRecording(inner, db2) {
91400
91645
  }
91401
91646
  });
91402
91647
  }
91403
- function withDefinitionDigestRecording(inner, db2, definitionDigest) {
91404
- if (!definitionDigest)
91405
- return inner;
91648
+ function withRunIdentityRecording(inner, db2, identity7) {
91406
91649
  const stamp = async (runId) => {
91407
- try {
91408
- await new RunDao(db2).mergeMetadata(runId, { definitionDigest });
91409
- } catch {}
91650
+ await new RunDao(db2).stampRunIdentity(runId, identity7.definitionDigest, identity7.workflowVersion);
91410
91651
  };
91411
91652
  return new Proxy(inner, {
91412
91653
  get(target, prop, receiver) {
@@ -91510,7 +91751,7 @@ class WorkflowAppService {
91510
91751
  }
91511
91752
  async run(file2, opts = {}) {
91512
91753
  const eventsBus = this.ctx.events?.();
91513
- const resolved = await resolveWorkflowDefinition(this.ctx.cwd, file2, {
91754
+ const resolved = opts.resolvedDefinition ?? await resolveWorkflowDefinition(this.ctx.cwd, file2, {
91514
91755
  validateSchema: true,
91515
91756
  embeddedSchemas: this.ctx.embeddedSchemas?.()
91516
91757
  });
@@ -91547,6 +91788,8 @@ class WorkflowAppService {
91547
91788
  await this.stampFailureReason(runId, result);
91548
91789
  await this.maybeLinkPipelineRun(file2, runId, opts);
91549
91790
  const runResult = result;
91791
+ runResult.definitionDigest = resolved.digest;
91792
+ runResult.version = workflowVersionLiteral(workflow);
91550
91793
  if (warnings.length > 0) {
91551
91794
  for (const warning of warnings) {
91552
91795
  try {
@@ -91656,7 +91899,7 @@ class WorkflowAppService {
91656
91899
  try {
91657
91900
  sessionsReal = realpathSync5(sessionsDir);
91658
91901
  } catch {
91659
- sessionsReal = resolve14(sessionsDir);
91902
+ sessionsReal = resolve13(sessionsDir);
91660
91903
  }
91661
91904
  const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
91662
91905
  const reclaimed = [];
@@ -91675,7 +91918,7 @@ class WorkflowAppService {
91675
91918
  try {
91676
91919
  resolved = realpathSync5(path9);
91677
91920
  } catch {
91678
- resolved = resolve14(path9);
91921
+ resolved = resolve13(path9);
91679
91922
  }
91680
91923
  if (!resolved.startsWith(sessionsReal + sep3)) {
91681
91924
  skipped.push({ name, reason: "path-confinement: resolved outside .spur/memory/sessions" });
@@ -91764,6 +92007,11 @@ class WorkflowAppService {
91764
92007
  rawMeta = {};
91765
92008
  }
91766
92009
  const persistedDigest = typeof rawMeta.definitionDigest === "string" ? rawMeta.definitionDigest : null;
92010
+ const identityStamped = "workflowVersion" in rawMeta;
92011
+ const persistedVersion = identityStamped && (typeof rawMeta.workflowVersion === "string" || rawMeta.workflowVersion === null) ? rawMeta.workflowVersion : undefined;
92012
+ if (identityStamped && persistedDigest === null) {
92013
+ throw new Error(`Cannot resume run "${runId}": workflow definition drift detected (identity-stamped run has no recorded definition digest, currently ${resolved.digest}). Resume refused.`);
92014
+ }
91767
92015
  if (persistedDigest !== null && persistedDigest !== resolved.digest) {
91768
92016
  if (opts?.allowDigestMismatch !== true && opts?.force !== true) {
91769
92017
  const prompt = `Workflow definition drift detected for run "${runId}": launched with ${persistedDigest}, currently ${resolved.digest}. Continue with modified definition?`;
@@ -91793,7 +92041,11 @@ class WorkflowAppService {
91793
92041
  } : {}
91794
92042
  });
91795
92043
  await this.stampFailureReason(runId, result);
91796
- return result;
92044
+ const runResult = result;
92045
+ runResult.definitionDigest = persistedDigest;
92046
+ if (persistedVersion !== undefined)
92047
+ runResult.version = persistedVersion;
92048
+ return runResult;
91797
92049
  }
91798
92050
  async stampFailureReason(runId, result) {
91799
92051
  if (result.status !== "failed" || typeof result.reason !== "string" || result.reason === "") {
@@ -91855,21 +92107,21 @@ class WorkflowAppService {
91855
92107
  const entries = [];
91856
92108
  const scannedPaths = new Set;
91857
92109
  for (const relPath of workflowPaths) {
91858
- const absPath = resolve14(projectRoot, relPath);
92110
+ const absPath = resolve13(projectRoot, relPath);
91859
92111
  layers.push({ id: "project", path: absPath });
91860
92112
  scannedPaths.add(absPath);
91861
92113
  try {
91862
- const found = await scanWorkflowFiles(absPath, "project");
92114
+ const found = await scanWorkflowFiles(absPath, "project", (filePath) => this.loadListEntry(filePath));
91863
92115
  entries.push(...found);
91864
92116
  } catch {}
91865
92117
  }
91866
92118
  for (const relPath of workflowPaths) {
91867
- const absPath = resolve14(globalRoot, relPath);
92119
+ const absPath = resolve13(globalRoot, relPath);
91868
92120
  if (scannedPaths.has(absPath))
91869
92121
  continue;
91870
92122
  layers.push({ id: "global", path: absPath });
91871
92123
  try {
91872
- const found = await scanWorkflowFiles(absPath, "global");
92124
+ const found = await scanWorkflowFiles(absPath, "global", (filePath) => this.loadListEntry(filePath));
91873
92125
  entries.push(...found);
91874
92126
  } catch {}
91875
92127
  }
@@ -91978,6 +92230,33 @@ class WorkflowAppService {
91978
92230
  ...outputArtifact !== undefined ? { outputArtifact } : {}
91979
92231
  };
91980
92232
  }
92233
+ async loadListEntry(filePath) {
92234
+ try {
92235
+ const resolved = await resolveWorkflowDefinition(this.ctx.cwd, filePath, {
92236
+ validateSchema: true,
92237
+ embeddedSchemas: this.ctx.embeddedSchemas?.()
92238
+ });
92239
+ return {
92240
+ name: resolved.workflow.name,
92241
+ kind: resolved.workflow.kind ?? "state-machine",
92242
+ valid: true,
92243
+ version: workflowVersionLiteral(resolved.workflow),
92244
+ definitionDigest: resolved.digest
92245
+ };
92246
+ } catch (error51) {
92247
+ const meta3 = await extractWorkflowMeta(filePath);
92248
+ return {
92249
+ name: meta3.name,
92250
+ kind: meta3.kind,
92251
+ valid: false,
92252
+ error: error51 instanceof Error ? error51.message : String(error51)
92253
+ };
92254
+ }
92255
+ }
92256
+ async stampPlanArtifactPath(runId, planArtifactPath) {
92257
+ const db2 = await this.ctx.getDb();
92258
+ await new RunDao(db2).mergeMetadata(runId, { planArtifactPath });
92259
+ }
91981
92260
  async createEngineService(opts = {}) {
91982
92261
  const processExec = this.ctx.processExecutor?.();
91983
92262
  const host = createDefaultWorkflowEngineHost(processExec !== undefined ? { processExecutor: processExec } : {});
@@ -92021,8 +92300,10 @@ class WorkflowAppService {
92021
92300
  persistence2 = withSelfPidRecording(persistence2, db2);
92022
92301
  }
92023
92302
  if (opts.extensions !== undefined) {
92024
- const digest = computeDefinitionDigest(opts.extensions.workflow);
92025
- persistence2 = withDefinitionDigestRecording(persistence2, db2, digest);
92303
+ persistence2 = withRunIdentityRecording(persistence2, db2, {
92304
+ definitionDigest: computeDefinitionDigest(opts.extensions.workflow),
92305
+ workflowVersion: workflowVersionLiteral(opts.extensions.workflow)
92306
+ });
92026
92307
  }
92027
92308
  const adapter = bus ? new ObservableWorkflowAdapter(persistence2, opts.extensions !== undefined ? withWorkflowIdentity(bus, opts.extensions.workflow) : bus) : persistence2;
92028
92309
  return new WorkflowService(host, adapter);
@@ -92039,6 +92320,9 @@ class WorkflowAppService {
92039
92320
  });
92040
92321
  }
92041
92322
  }
92323
+ function workflowVersionLiteral(workflow) {
92324
+ return typeof workflow.version === "string" && workflow.version !== "" ? workflow.version : null;
92325
+ }
92042
92326
  function collectShellCommands(def) {
92043
92327
  const entries = [];
92044
92328
  const visitAction = (stateId, action, idx) => {
@@ -92196,7 +92480,7 @@ function collectUndeclaredShellVarViolations(def) {
92196
92480
  }
92197
92481
  function collectCompositionAdvisory(def, workflowFile) {
92198
92482
  const findings = [];
92199
- let suppressed = 0;
92483
+ const workflowName = basename8(workflowFile, ".yaml");
92200
92484
  const shellLines = (command) => command.split(/\n|;/).map((u2) => u2.trim()).filter((u2) => u2.length > 0 && !u2.startsWith("#")).length;
92201
92485
  const agentRunSeverity = (prompt) => {
92202
92486
  if (prompt.length < 200)
@@ -92205,17 +92489,6 @@ function collectCompositionAdvisory(def, workflowFile) {
92205
92489
  return "medium";
92206
92490
  return "high";
92207
92491
  };
92208
- const baseline = loadCompositionBaselineFor(workflowFile);
92209
- const workflowName = basename8(workflowFile, ".yaml");
92210
- const adjudicated = (actionKey) => {
92211
- if (!baseline)
92212
- return false;
92213
- const entry = baseline.workflows[workflowName];
92214
- if (!entry)
92215
- return false;
92216
- const action = entry.actions[actionKey];
92217
- return action?.disposition !== undefined;
92218
- };
92219
92492
  const visitAction = (stateId, action, idx, phase) => {
92220
92493
  const actionKey = `${stateId}:${phase}:${idx}`;
92221
92494
  if (action.kind === "shell") {
@@ -92225,16 +92498,12 @@ function collectCompositionAdvisory(def, workflowFile) {
92225
92498
  const measured = shellLines(cmd);
92226
92499
  if (measured < 6)
92227
92500
  return;
92228
- if (adjudicated(actionKey)) {
92229
- suppressed += 1;
92230
- return;
92231
- }
92232
92501
  findings.push({
92233
92502
  workflow: workflowName,
92234
92503
  state: stateId,
92235
92504
  actionKey,
92236
92505
  measure: { kind: "shell-lines", measured, threshold: 6 },
92237
- recommendation: `shell action at ${actionKey} measures ${measured} lines (>5 frozen threshold, ADR-069) \u2014 extract to a script or record a disposition in config/workflow-composition-baseline.json`
92506
+ recommendation: `shell action at ${actionKey} measures ${measured} lines (>5 frozen threshold, ADR-069) \u2014 extract to a script under scripts/ or plugins/sp/scripts`
92238
92507
  });
92239
92508
  } else if (action.kind === "agent.run") {
92240
92509
  const input = action.options?.input;
@@ -92242,18 +92511,14 @@ function collectCompositionAdvisory(def, workflowFile) {
92242
92511
  return;
92243
92512
  if (input.trimStart().startsWith("/"))
92244
92513
  return;
92245
- if (adjudicated(actionKey)) {
92246
- suppressed += 1;
92247
- } else {
92248
- const severity = agentRunSeverity(input);
92249
- findings.push({
92250
- workflow: workflowName,
92251
- state: stateId,
92252
- actionKey,
92253
- measure: { kind: "agent-run-chars", measured: input.length, severity },
92254
- recommendation: `agent.run prompt at ${actionKey} is ${input.length} chars, not slash-pinned (severity ${severity}) \u2014 pin to a slash command or a script with a bounded prompt`
92255
- });
92256
- }
92514
+ const severity = agentRunSeverity(input);
92515
+ findings.push({
92516
+ workflow: workflowName,
92517
+ state: stateId,
92518
+ actionKey,
92519
+ measure: { kind: "agent-run-chars", measured: input.length, severity },
92520
+ recommendation: `agent.run prompt at ${actionKey} is ${input.length} chars, not slash-pinned (severity ${severity}) \u2014 pin to a slash command or a script with a bounded prompt`
92521
+ });
92257
92522
  }
92258
92523
  };
92259
92524
  if (def.kind === "transition-flow" || def.kind === undefined) {
@@ -92271,22 +92536,7 @@ function collectCompositionAdvisory(def, workflowFile) {
92271
92536
  visitAction(state.id, action, i2, "onExit");
92272
92537
  }
92273
92538
  }
92274
- return { findings, suppressed };
92275
- }
92276
- function loadCompositionBaselineFor(workflowFile) {
92277
- let dir = dirname17(resolve14(workflowFile));
92278
- for (let i2 = 0;i2 < 10; i2++) {
92279
- const candidate = join24(dir, "config/workflow-composition-baseline.json");
92280
- if (existsSync10(candidate)) {
92281
- try {
92282
- return JSON.parse(readFileSync10(candidate, "utf8"));
92283
- } catch {
92284
- return;
92285
- }
92286
- }
92287
- dir = dirname17(dir);
92288
- }
92289
- return;
92539
+ return { findings };
92290
92540
  }
92291
92541
  async function fileExists(path9) {
92292
92542
  const fs3 = createNodeFileSystem3();
@@ -92329,7 +92579,7 @@ async function outputArtifactForRun(cwd, runId) {
92329
92579
  const relative5 = join24(".spur", "run", `${runId}.log`);
92330
92580
  return await fileExists(join24(cwd, relative5)) ? relative5 : undefined;
92331
92581
  }
92332
- async function scanWorkflowFiles(rootPath, source) {
92582
+ async function scanWorkflowFiles(rootPath, source, loadEntry) {
92333
92583
  const entries = [];
92334
92584
  const fs3 = createNodeFileSystem3();
92335
92585
  const rootStat = await fs3.stat(rootPath);
@@ -92340,7 +92590,7 @@ async function scanWorkflowFiles(rootPath, source) {
92340
92590
  async function walk3(dirPath) {
92341
92591
  const names = await fs3.readDir(dirPath);
92342
92592
  for (const name of names) {
92343
- const fullPath = resolve14(dirPath, name);
92593
+ const fullPath = resolve13(dirPath, name);
92344
92594
  const st = await fs3.stat(fullPath);
92345
92595
  if (!st)
92346
92596
  continue;
@@ -92348,8 +92598,8 @@ async function scanWorkflowFiles(rootPath, source) {
92348
92598
  await walk3(fullPath);
92349
92599
  } else if (st.isFile() && (name.endsWith(".yaml") || name.endsWith(".yml"))) {
92350
92600
  const displayPath = fullPath.startsWith(rootPath) ? fullPath.slice(rootPath.length + 1) : fullPath;
92351
- const meta3 = await extractWorkflowMeta(fullPath);
92352
- entries.push({ ...meta3, path: displayPath, source });
92601
+ const entry = await loadEntry(fullPath);
92602
+ entries.push({ ...entry, path: displayPath, source });
92353
92603
  }
92354
92604
  }
92355
92605
  }
@@ -92388,12 +92638,20 @@ async function extractWorkflowMeta(filePath) {
92388
92638
  function rowToTraceEntry(row, cwd) {
92389
92639
  let isDryRun = false;
92390
92640
  let failureReason;
92641
+ let version3;
92642
+ let definitionDigest;
92391
92643
  try {
92392
92644
  const meta3 = JSON.parse(row.metadata_json);
92393
92645
  isDryRun = meta3.dryRun === true;
92394
92646
  if (typeof meta3.failureReason === "string" && meta3.failureReason !== "") {
92395
92647
  failureReason = meta3.failureReason;
92396
92648
  }
92649
+ if ("workflowVersion" in meta3 && (typeof meta3.workflowVersion === "string" || meta3.workflowVersion === null)) {
92650
+ version3 = meta3.workflowVersion;
92651
+ }
92652
+ if (typeof meta3.definitionDigest === "string" && meta3.definitionDigest !== "") {
92653
+ definitionDigest = meta3.definitionDigest;
92654
+ }
92397
92655
  } catch {}
92398
92656
  const entry = {
92399
92657
  runId: row.id,
@@ -92406,7 +92664,9 @@ function rowToTraceEntry(row, cwd) {
92406
92664
  project: systemEventProjectContext(cwd),
92407
92665
  durationMs: durationBetween(row.started_at, row.completed_at),
92408
92666
  outcome: traceOutcome(row.status),
92409
- ...failureReason !== undefined ? { failureReason } : {}
92667
+ ...failureReason !== undefined ? { failureReason } : {},
92668
+ ...version3 !== undefined ? { version: version3 } : {},
92669
+ ...definitionDigest !== undefined ? { definitionDigest } : {}
92410
92670
  };
92411
92671
  const nextAction = traceNextAction(entry);
92412
92672
  if (nextAction !== undefined)
@@ -92503,7 +92763,7 @@ async function partialArtifactForAction(cwd, runId, node, ok) {
92503
92763
  if (ok !== false || !TRACE_IDENTIFIER.test(runId) || !TRACE_IDENTIFIER.test(node))
92504
92764
  return;
92505
92765
  const relativePath2 = join24(".spur", "run", `${runId}-${node}-partial.md`);
92506
- return await fileExists(resolve14(cwd, relativePath2)) ? relativePath2 : undefined;
92766
+ return await fileExists(resolve13(cwd, relativePath2)) ? relativePath2 : undefined;
92507
92767
  }
92508
92768
  var TASK_PIPELINE_WORKFLOW = "task-pipeline", PIPELINE_LINK_KIND = "pipeline", TRACE_IDENTIFIER, TRACE_RESULT_FIELDS, TRACE_INVOCATION_FIELDS;
92509
92769
  var init_workflow_service = __esm(() => {
@@ -92541,7 +92801,7 @@ var init_workflow_service = __esm(() => {
92541
92801
  });
92542
92802
 
92543
92803
  // ../../packages/app/src/workflow/idea-handoff.ts
92544
- import { resolve as resolve15 } from "path";
92804
+ import { resolve as resolve14 } from "path";
92545
92805
  function processEvidence(r) {
92546
92806
  return `exit=${r.exitCode ?? "null"}${r.signal ? ` signal=${r.signal}` : ""}: ${r.stderr.trim() || "no stderr"}`;
92547
92807
  }
@@ -92551,11 +92811,11 @@ async function finalizeIdeaHandoff(options) {
92551
92811
  const executor = options.processExecutor ?? new NodeProcessExecutor3;
92552
92812
  const spurBin = options.spurBin ?? "spur";
92553
92813
  const { runId, featureId: featureId2 } = options;
92554
- const runDir = resolve15(root, ".spur", "run");
92555
- const batchPath = resolve15(runDir, `${runId}-idea-task-batch.json`);
92556
- const resultPath = resolve15(runDir, `${runId}-idea-batch-create-result.json`);
92557
- const orderPath = resolve15(runDir, `${runId}-idea-task-order.json`);
92558
- const reportPath = resolve15(runDir, `${runId}-idea-handoff.md`);
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`);
92559
92819
  const spurSplit = splitLaunchCommand(spurBin, 'idea-handoff "spurBin"');
92560
92820
  if ("error" in spurSplit) {
92561
92821
  return {
@@ -92955,6 +93215,7 @@ async function projectWorkflowProgress(runId, options) {
92955
93215
  }
92956
93216
  } catch {}
92957
93217
  const recordedDigest = typeof rawMeta.definitionDigest === "string" ? rawMeta.definitionDigest : null;
93218
+ const recordedVersion = "workflowVersion" in rawMeta && (typeof rawMeta.workflowVersion === "string" || rawMeta.workflowVersion === null) ? rawMeta.workflowVersion : undefined;
92958
93219
  let normalizedStatus = "unknown";
92959
93220
  const statusLower = (runRow.status ?? "").toLowerCase();
92960
93221
  if (statusLower === "done" || statusLower === "completed") {
@@ -93006,6 +93267,7 @@ async function projectWorkflowProgress(runId, options) {
93006
93267
  workflow: workflowName,
93007
93268
  status: normalizedStatus,
93008
93269
  definitionDigest: recordedDigest,
93270
+ ...recordedVersion !== undefined ? { version: recordedVersion } : {},
93009
93271
  currentState,
93010
93272
  states: [],
93011
93273
  transitions,
@@ -93184,6 +93446,7 @@ async function projectWorkflowProgress(runId, options) {
93184
93446
  workflow: smDef.name || workflowName,
93185
93447
  status: normalizedStatus,
93186
93448
  definitionDigest: recordedDigest,
93449
+ ...recordedVersion !== undefined ? { version: recordedVersion } : {},
93187
93450
  currentState,
93188
93451
  states: statesProgress,
93189
93452
  transitions,
@@ -93342,7 +93605,6 @@ __export(exports_src2, {
93342
93605
  registerSpurBuiltins: () => registerSpurBuiltins,
93343
93606
  refreshHistoryRollups: () => refreshHistoryRollups,
93344
93607
  redactAndBound: () => redactAndBound,
93345
- reconcileBaseline: () => reconcileBaseline,
93346
93608
  readVerdictArtifact: () => readVerdictArtifact,
93347
93609
  readVerdict: () => readVerdict,
93348
93610
  qualifySectionBody: () => qualifySectionBody,
@@ -93365,7 +93627,6 @@ __export(exports_src2, {
93365
93627
  normalizeSystemEventPayload: () => normalizeSystemEventPayload,
93366
93628
  normalizeProjectPath: () => normalizeProjectPath,
93367
93629
  looksLikeOpaqueId: () => looksLikeOpaqueId,
93368
- loadAcceptedFindings: () => loadAcceptedFindings,
93369
93630
  isSystemEventEnvelopeV2: () => isSystemEventEnvelopeV2,
93370
93631
  isRecordAuthoredReview: () => isRecordAuthoredReview,
93371
93632
  isPortLive: () => isPortLive,
@@ -93412,20 +93673,17 @@ __export(exports_src2, {
93412
93673
  computeExitCode: () => computeExitCode,
93413
93674
  computeDefinitionDigest: () => computeDefinitionDigest,
93414
93675
  computeAggregate: () => computeAggregate,
93415
- collectObservedFindings: () => collectObservedFindings,
93416
93676
  classifyTaskOutcome: () => classifyTaskOutcome,
93417
93677
  classifyPortBindError: () => classifyPortBindError,
93418
93678
  classifyDispatch: () => classifyDispatch,
93419
93679
  clampToolUseLimit: () => clampToolUseLimit,
93420
93680
  clampRunStoreLimit: () => clampRunStoreLimit,
93421
- checkWorkflowComposition: () => checkWorkflowComposition,
93422
93681
  canonicalJsonStringify: () => canonicalJsonStringify,
93423
93682
  buildWorkflowSteps: () => buildWorkflowSteps,
93424
93683
  buildTrackedBasenameIndex: () => buildTrackedBasenameIndex,
93425
93684
  buildSystemEventEnvelope: () => buildSystemEventEnvelope,
93426
93685
  buildEscalationPacket: () => buildEscalationPacket,
93427
93686
  bridgeEventBus: () => bridgeEventBus,
93428
- baselineSeverity: () => baselineSeverity,
93429
93687
  attributeSessions: () => attributeSessions,
93430
93688
  assertPiImporterSafe: () => assertPiImporterSafe,
93431
93689
  anchorQualify: () => anchorQualify,
@@ -101666,15 +101924,15 @@ function parseTimeout(raw) {
101666
101924
  return n3;
101667
101925
  }
101668
101926
  function loopSleep(ms, signal) {
101669
- return new Promise((resolve16) => {
101927
+ return new Promise((resolve15) => {
101670
101928
  if (signal?.aborted) {
101671
- resolve16();
101929
+ resolve15();
101672
101930
  return;
101673
101931
  }
101674
- const timer = setTimeout(resolve16, ms);
101932
+ const timer = setTimeout(resolve15, ms);
101675
101933
  signal?.addEventListener("abort", () => {
101676
101934
  clearTimeout(timer);
101677
- resolve16();
101935
+ resolve15();
101678
101936
  }, { once: true });
101679
101937
  });
101680
101938
  }
@@ -101805,7 +102063,7 @@ function waitFail(context4, options, code, message) {
101805
102063
 
101806
102064
  // src/release-ops.ts
101807
102065
  init_dist5();
101808
- import { existsSync as existsSync11, readdirSync as readdirSync7, readFileSync as readFileSync11 } from "fs";
102066
+ import { existsSync as existsSync10, readdirSync as readdirSync7, readFileSync as readFileSync10 } from "fs";
101809
102067
  import { join as join26 } from "path";
101810
102068
  var SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
101811
102069
  function releaseTag(config4, version3) {
@@ -101852,7 +102110,7 @@ async function readJson(path9) {
101852
102110
  function rootWorkspaceGlobs(repoRoot) {
101853
102111
  let root;
101854
102112
  try {
101855
- root = JSON.parse(readFileSync11(join26(repoRoot, "package.json"), "utf8"));
102113
+ root = JSON.parse(readFileSync10(join26(repoRoot, "package.json"), "utf8"));
101856
102114
  } catch {
101857
102115
  return [];
101858
102116
  }
@@ -101870,10 +102128,10 @@ function workspaceDirs(repoRoot) {
101870
102128
  continue;
101871
102129
  const parent = glob.slice(0, -2);
101872
102130
  const parentAbs = join26(repoRoot, parent);
101873
- if (!existsSync11(parentAbs))
102131
+ if (!existsSync10(parentAbs))
101874
102132
  continue;
101875
102133
  for (const entry of readdirSync7(parentAbs, { withFileTypes: true })) {
101876
- if (entry.isDirectory() && existsSync11(join26(parentAbs, entry.name, "package.json"))) {
102134
+ if (entry.isDirectory() && existsSync10(join26(parentAbs, entry.name, "package.json"))) {
101877
102135
  dirs.push(`${parent}/${entry.name}`);
101878
102136
  }
101879
102137
  }
@@ -101901,7 +102159,7 @@ async function releaseContext(repoRoot) {
101901
102159
  const shortName = (name) => name.split("/").pop() ?? name;
101902
102160
  const packages = new Map;
101903
102161
  for (const ws of workspaces2) {
101904
- const versionSourceFile = existsSync11(join26(repoRoot, ws.dir, "src", "config.ts")) ? `${ws.dir}/src/config.ts` : undefined;
102162
+ const versionSourceFile = existsSync10(join26(repoRoot, ws.dir, "src", "config.ts")) ? `${ws.dir}/src/config.ts` : undefined;
101905
102163
  packages.set(shortName(ws.name), {
101906
102164
  packageDir: ws.dir,
101907
102165
  packageName: ws.name,
@@ -102071,7 +102329,7 @@ async function bumpVersion(ctx, config4, version3, options, output2) {
102071
102329
  staged.push(...await updateWorkspacePins(ctx, config4.packageName, previous, version3, output2));
102072
102330
  await syncMarketplaceAndPlugins(ctx, version3, staged, output2);
102073
102331
  const lockPath = join26(ctx.repoRoot, "bun.lock");
102074
- if (existsSync11(lockPath) && Bun.file(lockPath).size > 0)
102332
+ if (existsSync10(lockPath) && Bun.file(lockPath).size > 0)
102075
102333
  staged.push("bun.lock");
102076
102334
  await git2(ctx.repoRoot, ["add", ...staged]);
102077
102335
  const message = `${config4.releaseCommitType}(${config4.releaseCommitScope}): ${config4.releaseCommitSubject(version3)}`;
@@ -102146,7 +102404,7 @@ async function bumpAll(ctx, version3, options, output2) {
102146
102404
  }
102147
102405
  await syncMarketplaceAndPlugins(ctx, version3, staged, output2);
102148
102406
  const lockPath = join26(ctx.repoRoot, "bun.lock");
102149
- if (existsSync11(lockPath) && Bun.file(lockPath).size > 0)
102407
+ if (existsSync10(lockPath) && Bun.file(lockPath).size > 0)
102150
102408
  staged.push("bun.lock");
102151
102409
  await git2(ctx.repoRoot, ["add", ...staged]);
102152
102410
  const shortNames = configs.map((c3) => c3.packageName.split("/").pop() ?? c3.packageName).join(" + ");
@@ -102536,7 +102794,7 @@ function registerFeatureCommand(program2, context4) {
102536
102794
  if (current === "active") {
102537
102795
  await assertFeatureCheckPass(context4, id, options.folder, false, "verifying");
102538
102796
  } else if (current === "verifying") {
102539
- await assertFeatureCheckPass(context4, id, options.folder, true, "done");
102797
+ await assertFeatureCheckPass(context4, id, options.folder, false, "done");
102540
102798
  }
102541
102799
  const result = await svc.transition(id, next);
102542
102800
  history.push({ from: result.fromStatus ?? current, to: result.toStatus ?? next });
@@ -102806,7 +103064,7 @@ import { createRequire } from "module";
102806
103064
  var CLI_CONFIG = {
102807
103065
  binaryName: "spur",
102808
103066
  binaryLabel: "spur",
102809
- binaryVersion: "0.3.73",
103067
+ binaryVersion: "0.3.74",
102810
103068
  configDir: ".spur",
102811
103069
  configFile: ".spur/config.yaml",
102812
103070
  databaseFile: ".spur/spur.db"
@@ -103191,7 +103449,7 @@ init_loader();
103191
103449
  init_src2();
103192
103450
  init_dist10();
103193
103451
  import { homedir as homedir9 } from "os";
103194
- import { join as join27, resolve as resolve16 } from "path";
103452
+ import { join as join27, resolve as resolve15 } from "path";
103195
103453
 
103196
103454
  // src/config/scaffold-manifest.ts
103197
103455
  var SCAFFOLD_MANIFEST = [
@@ -103252,7 +103510,7 @@ If \`.spur/context/\` is absent, proceed normally. Never block work on its absen
103252
103510
  `;
103253
103511
  function globalRulesRoot(context4) {
103254
103512
  const override = context4.env.SPUR_GLOBAL_RULES_DIR;
103255
- return override !== undefined && override.length > 0 ? resolve16(context4.cwd, override) : join27(homedir9(), GLOBAL_RULES_DIR2);
103513
+ return override !== undefined && override.length > 0 ? resolve15(context4.cwd, override) : join27(homedir9(), GLOBAL_RULES_DIR2);
103256
103514
  }
103257
103515
  async function writeIfNew(context4, path9, content, force, result) {
103258
103516
  if (!force && await context4.fs.exists(path9)) {
@@ -103280,7 +103538,7 @@ async function seedGlobalRules(context4) {
103280
103538
  }
103281
103539
  function resolveGlobalConfigDir(context4) {
103282
103540
  const globalOverride = context4.env.SPUR_GLOBAL_RULES_DIR;
103283
- return globalOverride !== undefined && globalOverride.length > 0 ? resolve16(context4.cwd, globalOverride) : join27(homedir9(), GLOBAL_CONFIG_DIR);
103541
+ return globalOverride !== undefined && globalOverride.length > 0 ? resolve15(context4.cwd, globalOverride) : join27(homedir9(), GLOBAL_CONFIG_DIR);
103284
103542
  }
103285
103543
  async function seedGlobalConfig(context4) {
103286
103544
  const source = bundledConfigRoot();
@@ -103520,6 +103778,42 @@ ${contextEntry}
103520
103778
  });
103521
103779
  }
103522
103780
 
103781
+ // src/commands/maintain.ts
103782
+ init_src();
103783
+ init_src2();
103784
+ import { join as join28 } from "path";
103785
+ function registerMaintainCommand(program2, context4, options = {}) {
103786
+ 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
+ const db2 = await context4.getDb();
103788
+ const dbPath = context4.env.DATABASE_URL ?? join28(context4.cwd, DEFAULT_DATABASE_URL);
103789
+ const result = await maintainDatabase(db2, {
103790
+ vacuum: options2.vacuum === true,
103791
+ dbPath: dbPath.startsWith(":memory:") ? undefined : dbPath
103792
+ });
103793
+ context4.output.write(options2.json ? toEnvelopeJson(result, { enveloped: options2.jsonEnvelope }) : formatMaintenanceResult(result));
103794
+ if (!result.optimized && !result.checkpointed) {
103795
+ context4.setExitCode(1);
103796
+ }
103797
+ });
103798
+ }
103799
+ function formatMaintenanceResult(r) {
103800
+ const lines = [
103801
+ `database maintenance completed in ${r.durationMs}ms:`,
103802
+ ` optimize: ${r.optimized ? "ok" : "failed"}`,
103803
+ ` wal_checkpoint: ${r.checkpointed ? "ok (truncated)" : "failed"}`,
103804
+ ` vacuum: ${r.vacuumed ? "ok" : r.vacuumSkippedReason ? `skipped (${r.vacuumSkippedReason})` : "skipped"}`
103805
+ ];
103806
+ if (r.vacuumed && r.bytesReclaimed > 0) {
103807
+ const mbReclaimed = (r.bytesReclaimed / (1024 * 1024)).toFixed(2);
103808
+ lines.push(` reclaimed: ${mbReclaimed} MB`);
103809
+ }
103810
+ const mbBefore = (r.bytesBefore / (1024 * 1024)).toFixed(2);
103811
+ const mbAfter = (r.bytesAfter / (1024 * 1024)).toFixed(2);
103812
+ lines.push(` size: ${mbAfter} MB (was ${mbBefore} MB)`);
103813
+ return lines.join(`
103814
+ `);
103815
+ }
103816
+
103523
103817
  // src/commands/message.ts
103524
103818
  init_src2();
103525
103819
  await init_src3();
@@ -103790,15 +104084,15 @@ async function runMessageWatch(svc, output2, options, runtime = {}) {
103790
104084
  }
103791
104085
  }
103792
104086
  function defaultSleep(ms, signal) {
103793
- return new Promise((resolve17) => {
104087
+ return new Promise((resolve16) => {
103794
104088
  if (signal?.aborted) {
103795
- resolve17();
104089
+ resolve16();
103796
104090
  return;
103797
104091
  }
103798
- const timer = setTimeout(resolve17, ms);
104092
+ const timer = setTimeout(resolve16, ms);
103799
104093
  signal?.addEventListener("abort", () => {
103800
104094
  clearTimeout(timer);
103801
- resolve17();
104095
+ resolve16();
103802
104096
  }, { once: true });
103803
104097
  });
103804
104098
  }
@@ -103825,15 +104119,15 @@ function parseTimeout2(raw) {
103825
104119
  return n3;
103826
104120
  }
103827
104121
  function sendSleep(ms, signal) {
103828
- return new Promise((resolve17) => {
104122
+ return new Promise((resolve16) => {
103829
104123
  if (signal?.aborted) {
103830
- resolve17();
104124
+ resolve16();
103831
104125
  return;
103832
104126
  }
103833
- const timer = setTimeout(resolve17, ms);
104127
+ const timer = setTimeout(resolve16, ms);
103834
104128
  signal?.addEventListener("abort", () => {
103835
104129
  clearTimeout(timer);
103836
- resolve17();
104130
+ resolve16();
103837
104131
  }, { once: true });
103838
104132
  });
103839
104133
  }
@@ -103891,10 +104185,10 @@ async function waitForPendingDrain(agentService, teamService, pin, timeoutMs, si
103891
104185
 
103892
104186
  // src/commands/migrate.ts
103893
104187
  init_src2();
103894
- import { join as join28 } from "path";
104188
+ import { join as join29 } from "path";
103895
104189
  function registerMigrateCommand(program2, context4, options = {}) {
103896
104190
  program2.command("migrate", { hidden: options.hidden === true }).summary("apply CLI-owned schema migrations").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options2) => {
103897
- const migrations = await loadSqlMigrations(join28(context4.cwd, "drizzle")).catch(() => {
104191
+ const migrations = await loadSqlMigrations(join29(context4.cwd, "drizzle")).catch(() => {
103898
104192
  return;
103899
104193
  });
103900
104194
  const applied = await applyCliMigrations(await context4.getDb(), migrations);
@@ -103906,12 +104200,12 @@ function registerMigrateCommand(program2, context4, options = {}) {
103906
104200
  // src/commands/projects.ts
103907
104201
  init_dist5();
103908
104202
  await init_src3();
103909
- import { basename as basename10, resolve as resolve17 } from "path";
104203
+ import { basename as basename10, resolve as resolve16 } from "path";
103910
104204
  function registerProjectsCommand(program2, context4) {
103911
104205
  const projectsCmd = program2.command("projects").summary("manage the Spur multi-project registry");
103912
104206
  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) => {
103913
104207
  try {
103914
- const absolutePath = resolve17(context4.cwd, pathArg);
104208
+ const absolutePath = resolve16(context4.cwd, pathArg);
103915
104209
  if (!await context4.fs.exists(absolutePath)) {
103916
104210
  throw new Error(`Directory does not exist: ${absolutePath}`);
103917
104211
  }
@@ -104333,7 +104627,7 @@ function formatTraceDetail(detail) {
104333
104627
 
104334
104628
  // src/commands/serve.ts
104335
104629
  init_src();
104336
- import { join as join32 } from "path";
104630
+ import { join as join33 } from "path";
104337
104631
 
104338
104632
  // ../server/src/index.ts
104339
104633
  init_src();
@@ -104343,11 +104637,11 @@ init_src();
104343
104637
  init_loader();
104344
104638
  init_src2();
104345
104639
  await init_src3();
104346
- import { basename as basename12, dirname as dirname20, isAbsolute as isAbsolute7, join as join31 } from "path";
104640
+ import { basename as basename12, dirname as dirname20, isAbsolute as isAbsolute7, join as join32 } from "path";
104347
104641
  init_dist5();
104348
104642
 
104349
104643
  // ../server/src/bootstrap.ts
104350
- import { join as join29 } from "path";
104644
+ import { join as join30 } from "path";
104351
104645
 
104352
104646
  // ../../node_modules/.bun/radash@12.1.1/node_modules/radash/dist/esm/async.mjs
104353
104647
  var guard = (func, shouldGuard) => {
@@ -104620,8 +104914,8 @@ class EventPublisher {
104620
104914
  if (bufferedEvents.length > 0) {
104621
104915
  return { done: false, value: bufferedEvents.shift() };
104622
104916
  }
104623
- return new Promise((resolve18, reject) => {
104624
- pullResolvers.push([resolve18, reject]);
104917
+ return new Promise((resolve17, reject) => {
104918
+ pullResolvers.push([resolve17, reject]);
104625
104919
  });
104626
104920
  }, async () => {
104627
104921
  unsubscribe();
@@ -110265,6 +110559,7 @@ var historyKpiTrendPointSchema = exports_external.object({
110265
110559
  var historyTimeSeriesPointSchema = exports_external.object({
110266
110560
  bucketStart: exports_external.string(),
110267
110561
  cacheHitRatio: exports_external.number(),
110562
+ gainRatio: exports_external.number().optional(),
110268
110563
  series: exports_external.record(exports_external.string(), exports_external.number())
110269
110564
  });
110270
110565
  var historyTopItemSchema = exports_external.object({
@@ -113767,7 +114062,7 @@ function createApp(appRt, opts) {
113767
114062
  app.use("*", async (c3, next) => {
113768
114063
  const pathname = c3.req.path === "/" ? "/index.html" : c3.req.path;
113769
114064
  try {
113770
- const file2 = Bun.file(join29(webDistPath, pathname));
114065
+ const file2 = Bun.file(join30(webDistPath, pathname));
113771
114066
  if (await file2.exists()) {
113772
114067
  const headers = new Headers({ "content-type": file2.type });
113773
114068
  return new Response(file2.stream(), { headers });
@@ -113780,7 +114075,7 @@ function createApp(appRt, opts) {
113780
114075
  return c3.json({ error: "Not Found" }, 404);
113781
114076
  }
113782
114077
  try {
113783
- const indexFile = Bun.file(join29(webDistPath, "index.html"));
114078
+ const indexFile = Bun.file(join30(webDistPath, "index.html"));
113784
114079
  if (await indexFile.exists()) {
113785
114080
  return new Response(indexFile.stream(), {
113786
114081
  headers: { "content-type": "text/html; charset=utf-8" }
@@ -113801,7 +114096,7 @@ init_src();
113801
114096
  init_src2();
113802
114097
  init_dist5();
113803
114098
  await init_src3();
113804
- import { dirname as dirname19, join as join30 } from "path";
114099
+ import { dirname as dirname19, join as join31 } from "path";
113805
114100
  var NOOP_OUTPUT = { write: (_s) => {}, error: (_s) => {} };
113806
114101
 
113807
114102
  class LazyPlanningEventEmitter {
@@ -113837,7 +114132,7 @@ class NotConfiguredError extends Error {
113837
114132
  function createServerContext(appRt, options) {
113838
114133
  const cwd = options.cwd;
113839
114134
  const fs3 = options.fs;
113840
- const dbUrl = options.dbUrl ?? join30(cwd, DEFAULT_DATABASE_URL);
114135
+ const dbUrl = options.dbUrl ?? join31(cwd, DEFAULT_DATABASE_URL);
113841
114136
  const eventsBus = options.eventsBus ?? appRt.events;
113842
114137
  const jobQueueEnabled = options.jobQueueEnabled ?? false;
113843
114138
  const eventProjectContext = systemEventProjectContext(cwd);
@@ -114123,7 +114418,7 @@ async function loadServerSectionMatrix() {
114123
114418
  }
114124
114419
  const root = bundledConfigRoot();
114125
114420
  if (root !== null) {
114126
- const matrixPath = join31(root, "tasks", "section-matrix.yaml");
114421
+ const matrixPath = join32(root, "tasks", "section-matrix.yaml");
114127
114422
  if (await nodeFs.exists(matrixPath)) {
114128
114423
  return await loadStructuredSpurConfig(matrixPath, {
114129
114424
  validateJsonSchema: false
@@ -114132,7 +114427,7 @@ async function loadServerSectionMatrix() {
114132
114427
  }
114133
114428
  throw new Error(`no canonical section-matrix found for task creation (F92 R1); tried:
114134
114429
  ` + ` - ${localPath}
114135
- ` + (root !== null ? ` - ${join31(root, "tasks", "section-matrix.yaml")}
114430
+ ` + (root !== null ? ` - ${join32(root, "tasks", "section-matrix.yaml")}
114136
114431
  ` : "") + "copy/generate section-matrix.yaml from the canonical build-time matrix asset (repo `config` `tasks` tree) into one of those paths");
114137
114432
  }
114138
114433
  var SYSTEM_EVENTS_PRUNE_JOB = "system-events-prune";
@@ -114270,15 +114565,15 @@ async function handleFeatureActionJob(ctx, env, payload) {
114270
114565
  await runFeatureActionJob(ctx, env, payload);
114271
114566
  }
114272
114567
  async function resolveWebDistPath(configuredPath) {
114273
- const candidates = configuredPath && configuredPath.trim() !== "" ? [isAbsolute7(configuredPath) ? configuredPath : join31(process.cwd(), configuredPath)] : [
114274
- join31(process.cwd(), "dist/web"),
114275
- join31(import.meta.dir, "web"),
114276
- join31(dirname20(process.execPath), "web"),
114277
- join31(dirname20(process.execPath), "../web"),
114278
- join31(import.meta.dir, "../../../dist/web")
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")
114279
114574
  ];
114280
114575
  for (const candidate of candidates) {
114281
- if (await Bun.file(join31(candidate, "index.html")).exists()) {
114576
+ if (await Bun.file(join32(candidate, "index.html")).exists()) {
114282
114577
  return candidate;
114283
114578
  }
114284
114579
  }
@@ -114442,7 +114737,7 @@ if (false) {}
114442
114737
 
114443
114738
  // src/commands/serve.ts
114444
114739
  function resolveServeDbUrl(cwd, env, configuredUrl) {
114445
- return env.DATABASE_URL === undefined ? join32(cwd, DEFAULT_DATABASE_URL) : configuredUrl;
114740
+ return env.DATABASE_URL === undefined ? join33(cwd, DEFAULT_DATABASE_URL) : configuredUrl;
114446
114741
  }
114447
114742
  function registerServeCommand(program2, context4, options = {}) {
114448
114743
  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) => {
@@ -114482,7 +114777,7 @@ function registerServeCommand(program2, context4, options = {}) {
114482
114777
  }
114483
114778
 
114484
114779
  // src/commands/status.ts
114485
- import { join as join33 } from "path";
114780
+ import { join as join34 } from "path";
114486
114781
 
114487
114782
  // src/errors.ts
114488
114783
  class CommandError extends Error {
@@ -114553,8 +114848,8 @@ function registerStatusCommand(program2, context4, options = {}) {
114553
114848
  }
114554
114849
  async function runStatusCore(path9, options, context4) {
114555
114850
  const [packageJsonExists, spurConfigExists, git3, agentSpecs] = await Promise.all([
114556
- context4.fs.exists(join33(context4.cwd, "package.json")),
114557
- context4.fs.exists(join33(context4.cwd, ".spur", "config.yaml")),
114851
+ context4.fs.exists(join34(context4.cwd, "package.json")),
114852
+ context4.fs.exists(join34(context4.cwd, ".spur", "config.yaml")),
114558
114853
  gitContext(context4.cwd),
114559
114854
  listAgentSpecIds(context4)
114560
114855
  ]);
@@ -114582,14 +114877,14 @@ async function runStatusCore(path9, options, context4) {
114582
114877
  return status.ok ? 0 : 1;
114583
114878
  }
114584
114879
  async function listAgentSpecIds(context4) {
114585
- const dir = join33(context4.cwd, ".spur", "agents");
114880
+ const dir = join34(context4.cwd, ".spur", "agents");
114586
114881
  if (!await context4.fs.exists(dir))
114587
114882
  return [];
114588
114883
  const entries = await context4.fs.readDir(dir);
114589
114884
  return entries.filter((entry) => entry.endsWith(".yaml") || entry.endsWith(".yml")).map((entry) => entry.replace(/\.ya?ml$/, "")).sort();
114590
114885
  }
114591
114886
  async function readTargetStatus(context4, targetPath) {
114592
- const resolved = join33(context4.cwd, targetPath);
114887
+ const resolved = join34(context4.cwd, targetPath);
114593
114888
  const stat = await context4.fs.stat(resolved);
114594
114889
  if (stat === null)
114595
114890
  throw new CommandError(`status failed: path does not exist at ${resolved}`);
@@ -114601,8 +114896,8 @@ init_loader();
114601
114896
  init_src2();
114602
114897
  init_dist5();
114603
114898
  await init_src3();
114604
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
114605
- import { dirname as dirname21, join as join34 } from "path";
114899
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
114900
+ import { dirname as dirname21, join as join35 } from "path";
114606
114901
  // schemas/section-matrix.schema.json
114607
114902
  var section_matrix_schema_default = {
114608
114903
  $schema: "http://json-schema.org/draft-07/schema#",
@@ -115704,7 +115999,7 @@ ${result.content}`);
115704
115999
  let forcedDoneVerdict;
115705
116000
  if (status === "done") {
115706
116001
  const current = await svc.show(wbs);
115707
- const verdictDir = options.verdictDir ?? join34(context4.cwd, ".spur", "run");
116002
+ const verdictDir = options.verdictDir ?? join35(context4.cwd, ".spur", "run");
115708
116003
  const loaded = await readVerdictArtifact(context4.fs, verdictDir, wbs);
115709
116004
  const guardOutcome = evaluateDoneTransition({
115710
116005
  wbs,
@@ -116150,73 +116445,42 @@ ${result.content}`);
116150
116445
  context4.setExitCode(1);
116151
116446
  }
116152
116447
  });
116153
- task.command("check").summary("Validate a task file through the four-layer check (design \xA73).").argument("[wbs]", "Task WBS number (validates all tasks in the folder when omitted)").option(...SHARED_OPTIONS.strictTaskAll).option("--strict-core", "Compatibility alias (F92 R2): historically the done-gate label; kept so installed plugins/workflows that call it keep working. No longer meaningful on its own \u2014 target-state selection (`--as`) supplies the real done semantics.").option(...SHARED_OPTIONS.asTaskF92).option("--corpus", "Sweep every task and feature against config/corpus-baseline.json").option("--since <ref>", "Scope the corpus fog check to changes since a git ref (requires --corpus)").option("--fix", "repair structural findings in place (heading presence/level/order; adds missing R-item checkbox markers \u2014 flipping verified boxes is task record's job, not --fix)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (wbs, options) => {
116448
+ task.command("check").summary("Validate a task file through the four-layer check (design \xA73).").argument("[wbs]", "Task WBS number (validates all tasks in the folder when omitted)").option("--corpus", "Explicit unsuppressed audit of active tasks and features; warnings are advisory").option("--since <ref>", "Scope the corpus fog comparison to changes since a git ref (requires --corpus)").option(...SHARED_OPTIONS.strictTaskAll).option("--strict-core", "Compatibility alias (F92 R2): historically the done-gate label; kept so installed plugins/workflows that call it keep working. No longer meaningful on its own \u2014 target-state selection (`--as`) supplies the real done semantics.").option(...SHARED_OPTIONS.asTaskF92).option("--fix", "repair structural findings in place (heading presence/level/order; adds missing R-item checkbox markers \u2014 flipping verified boxes is task record's job, not --fix)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (wbs, options) => {
116154
116449
  const json3 = options.json === true;
116155
116450
  const strict = options.strict === true;
116156
- const asStatus = options.as === undefined ? undefined : canonicalStatusOrRaw(options.as);
116157
- if (options.as !== undefined && !TASK_STATUSES.includes(asStatus ?? "")) {
116158
- writeJsonError(context4.output, options, `invalid --as status "${options.as}" (canonical: ${TASK_STATUSES.join(", ")})`, "VALIDATION_FAILED");
116159
- context4.setExitCode(2);
116160
- return;
116161
- }
116162
- if (asStatus !== undefined && options.corpus === true) {
116163
- writeJsonError(context4.output, options, "--as <status> is a single-task target projection and cannot be combined with --corpus", "VALIDATION_FAILED");
116164
- context4.setExitCode(2);
116165
- return;
116166
- }
116167
- if (options.fix === true && options.corpus === true) {
116168
- writeJsonError(context4.output, options, "--fix repairs files in place and cannot be combined with --corpus", "VALIDATION_FAILED");
116451
+ if (options.since !== undefined && !options.corpus || options.corpus && (wbs || options.as || options.fix || options.folder || strict || options.strictCore) || options.since?.startsWith("-")) {
116452
+ writeJsonError(context4.output, options, "--corpus accepts only --since and JSON output options; --since requires --corpus and a git ref", "VALIDATION_FAILED");
116169
116453
  context4.setExitCode(2);
116170
116454
  return;
116171
116455
  }
116172
- try {
116173
- if (options.corpus === true) {
116174
- if (wbs !== undefined) {
116175
- writeJsonError(context4.output, options, "--corpus validates the whole corpus and cannot be combined with a WBS", "VALIDATION_FAILED");
116176
- context4.setExitCode(2);
116177
- return;
116178
- }
116179
- if (String(options.since ?? "").startsWith("-")) {
116180
- writeJsonError(context4.output, options, "--since requires a git ref value (e.g. --since HEAD~1)", "VALIDATION_FAILED");
116181
- context4.setExitCode(2);
116182
- return;
116183
- }
116184
- if (options.since !== undefined) {
116185
- const fogRange = await resolveFogRange(context4.cwd, options.since);
116186
- if ("skip" in fogRange) {
116187
- context4.output.error(`corpus-check: fog check SKIPPED (${fogRange.skip}) \u2014 range ${fogRange.spec} was not evaluated.`);
116188
- }
116189
- }
116190
- const result = await runCorpusCheck(context4.cwd, options.since);
116456
+ if (options.corpus) {
116457
+ try {
116458
+ const result = await runCorpusCheck(context4.cwd, options.since, context4.output.error.bind(context4.output));
116191
116459
  if (json3) {
116192
116460
  context4.output.write(toEnvelopeJson(result, { enveloped: options.jsonEnvelope }));
116193
116461
  } else {
116194
- const e = result.bySeverity.error;
116195
- const w = result.bySeverity.warning;
116196
- context4.output.write(`corpus-check: swept tasks + features \u2014 errors ${e.observed} observed, ` + `${e.baselined} baselined, ${e.newCount} new; ` + `warnings ${w.observed} observed, ${w.baselined} baselined, ` + `${w.newCount} new.`);
116197
- for (const error51 of result.newErrors) {
116198
- context4.output.error(` NEW [error] ${error51.kind} ${error51.id}: ${error51.code} \u2014 ${error51.message}`);
116199
- }
116200
- for (const warning of result.newWarnings) {
116201
- context4.output.error(` NEW [warning] ${warning.kind} ${warning.id}: ${warning.code} \u2014 ${warning.message}`);
116462
+ context4.output.write(`corpus-check: ${result.newErrors.length} errors, ${result.newWarnings.length} warnings; no suppressions. Use --json for full findings.`);
116463
+ for (const finding of result.newErrors) {
116464
+ writeJsonError(context4.output, options, `${finding.kind}:${finding.id}:${finding.code}: ${finding.message}`);
116202
116465
  }
116203
- for (const dup of result.duplicateKeys) {
116204
- context4.output.error(` DUP ${dup.key} \u2014 ${dup.count} entries for one key; reconciliation is key-addressed, ` + "so the extras over-cover and hide a partial reduction. Keep one entry per key.");
116205
- }
116206
- context4.output.write(result.ok ? "corpus-check OK \u2014 no corpus errors or warnings outside the accepted baseline." : "corpus-check FAILED \u2014 fix the new findings above, or accept them by regenerating the snapshot: bun run scripts/commands/regen-corpus-baseline.ts.");
116207
116466
  }
116208
116467
  if (!result.ok)
116209
116468
  context4.setExitCode(1);
116210
- return;
116211
- }
116212
- if (options.since !== undefined) {
116213
- writeJsonError(context4.output, options, "--since requires --corpus", "VALIDATION_FAILED");
116214
- context4.setExitCode(2);
116215
- return;
116469
+ } catch (err) {
116470
+ writeJsonError(context4.output, options, String(err));
116471
+ context4.setExitCode(1);
116216
116472
  }
116473
+ return;
116474
+ }
116475
+ const asStatus = options.as === undefined ? undefined : canonicalStatusOrRaw(options.as);
116476
+ if (options.as !== undefined && !TASK_STATUSES.includes(asStatus ?? "")) {
116477
+ writeJsonError(context4.output, options, `invalid --as status "${options.as}" (canonical: ${TASK_STATUSES.join(", ")})`, "VALIDATION_FAILED");
116478
+ context4.setExitCode(2);
116479
+ return;
116480
+ }
116481
+ try {
116217
116482
  const svc = await makeCheckService(context4);
116218
116483
  const planningFolders = await resolvePlanningFolders(context4.fs);
116219
- const accepted = await loadAcceptedFindings(context4.cwd);
116220
116484
  const activeFolder = planningFolders.foldersConfig.active_folder;
116221
116485
  const tasksDir = context4.fs.resolve(options.folder ?? activeFolder);
116222
116486
  const printResult = (result) => {
@@ -116250,7 +116514,6 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
116250
116514
  strict,
116251
116515
  asStatus,
116252
116516
  severityOverrides: planningFolders.severityOverrides,
116253
- accepted,
116254
116517
  fix: options.fix === true
116255
116518
  });
116256
116519
  results.push(result);
@@ -116270,7 +116533,6 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
116270
116533
  strict,
116271
116534
  asStatus,
116272
116535
  severityOverrides: planningFolders.severityOverrides,
116273
- accepted,
116274
116536
  fix: options.fix === true
116275
116537
  });
116276
116538
  results.push(result);
@@ -116420,15 +116682,15 @@ function loadTemplateBodies(projectRoot, variant) {
116420
116682
  if (cached2 !== undefined)
116421
116683
  return cached2;
116422
116684
  let bodies = {};
116423
- const localPath = join34(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
116424
- if (existsSync12(localPath)) {
116425
- bodies = extractTemplateBodies(readFileSync12(localPath, "utf8"));
116685
+ const localPath = join35(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
116686
+ if (existsSync11(localPath)) {
116687
+ bodies = extractTemplateBodies(readFileSync11(localPath, "utf8"));
116426
116688
  } else {
116427
116689
  const root = bundledConfigRoot();
116428
116690
  if (root !== null) {
116429
- const templatePath = join34(root, "templates", "task", `${variant}.md`);
116430
- if (existsSync12(templatePath)) {
116431
- bodies = extractTemplateBodies(readFileSync12(templatePath, "utf8"));
116691
+ const templatePath = join35(root, "templates", "task", `${variant}.md`);
116692
+ if (existsSync11(templatePath)) {
116693
+ bodies = extractTemplateBodies(readFileSync11(templatePath, "utf8"));
116432
116694
  }
116433
116695
  }
116434
116696
  }
@@ -116455,12 +116717,10 @@ async function runDoneGateCheck(context4, wbs, folderOverride, targetStatus) {
116455
116717
  return false;
116456
116718
  }
116457
116719
  const svc = new TaskCheckService(context4.fs, await loadSectionMatrix(context4.cwd), await makeTaskLocator(context4));
116458
- const accepted = await loadAcceptedFindings(context4.cwd);
116459
116720
  const result = await svc.check(hit.filePath, wbs, {
116460
116721
  strict: false,
116461
116722
  asStatus: targetStatus,
116462
- severityOverrides: planningFolders.severityOverrides,
116463
- accepted
116723
+ severityOverrides: planningFolders.severityOverrides
116464
116724
  });
116465
116725
  return result.pass;
116466
116726
  }
@@ -116486,7 +116746,7 @@ async function loadSectionMatrixUncached(projectRoot) {
116486
116746
  }
116487
116747
  const root = bundledConfigRoot();
116488
116748
  if (root !== null) {
116489
- const matrixPath = join34(root, "tasks", "section-matrix.yaml");
116749
+ const matrixPath = join35(root, "tasks", "section-matrix.yaml");
116490
116750
  if (await fs3.exists(matrixPath)) {
116491
116751
  const data = await loadStructuredSpurConfig(matrixPath, {
116492
116752
  validateJsonSchema: true,
@@ -116497,7 +116757,7 @@ async function loadSectionMatrixUncached(projectRoot) {
116497
116757
  }
116498
116758
  throw new Error(`no canonical section-matrix found for task section authority (F92 R1); tried:
116499
116759
  ` + ` - ${localPath}
116500
- ` + (root !== null ? ` - ${join34(root, "tasks", "section-matrix.yaml")}
116760
+ ` + (root !== null ? ` - ${join35(root, "tasks", "section-matrix.yaml")}
116501
116761
  ` : "") + "copy/generate section-matrix.yaml from the canonical build-time matrix asset (repo `config` `tasks` tree) into one of those paths");
116502
116762
  }
116503
116763
 
@@ -116780,13 +117040,13 @@ async function runTeamDown(team, options, context4) {
116780
117040
  }
116781
117041
 
116782
117042
  // src/commands/workflow.ts
117043
+ init_capability_attestation();
116783
117044
  init_loader();
116784
- init_dist8();
116785
117045
  init_dist7();
116786
117046
  init_dist5();
116787
117047
  await init_src3();
116788
117048
  import { closeSync as closeSync5, fstatSync as fstatSync3, openSync as openSync5, readSync as readSync3 } from "fs";
116789
- import { join as join35 } from "path";
117049
+ import { join as join36 } from "path";
116790
117050
  import { createInterface } from "readline";
116791
117051
  import { setTimeout as sleep5 } from "timers/promises";
116792
117052
 
@@ -116878,7 +117138,7 @@ function markWorkflowRunActive() {
116878
117138
  function clearWorkflowRunActive() {
116879
117139
  delete process.env[WORKFLOW_RUN_ACTIVE_ENV];
116880
117140
  }
116881
- async function spawnAsyncWorkflowWorker(spurBin, cmd) {
117141
+ async function spawnAsyncWorkflowWorker(spurBin, cmd, extraEnv) {
116882
117142
  const line = [spurBin, ...cmd].map(shQuote2).join(" ");
116883
117143
  const env = {};
116884
117144
  for (const [key2, value2] of Object.entries(process.env)) {
@@ -116886,6 +117146,9 @@ async function spawnAsyncWorkflowWorker(spurBin, cmd) {
116886
117146
  env[key2] = value2;
116887
117147
  }
116888
117148
  env.SPUR_ASYNC_WORKER = "1";
117149
+ for (const [key2, value2] of Object.entries(extraEnv ?? {})) {
117150
+ env[key2] = value2;
117151
+ }
116889
117152
  await new NodeProcessExecutor3().run({
116890
117153
  command: process.platform === "win32" ? "cmd" : "/bin/sh",
116891
117154
  args: process.platform === "win32" ? ["/c", `start /b "" ${[spurBin, ...cmd].map((c3) => `"${c3.replace(/"/g, '""')}"`).join(" ")}`] : ["-c", `nohup ${line} </dev/null >/dev/null 2>&1 &`],
@@ -116939,6 +117202,35 @@ async function waitForRunRegistration(service, runId, timeoutMs = asyncRegisterT
116939
117202
  }
116940
117203
  }
116941
117204
  }
117205
+ function expectedDigestMismatch(resolvedDigest, expectedDigest) {
117206
+ if (expectedDigest === undefined)
117207
+ return null;
117208
+ if (resolvedDigest !== expectedDigest) {
117209
+ return "workflow run: refusing to start \u2014 resolved definition digest " + `${resolvedDigest ?? "<resolution failed>"} differs from the expected digest ` + `${expectedDigest} sent by the async launcher`;
117210
+ }
117211
+ return null;
117212
+ }
117213
+ async function writeWorkflowPlanArtifact(cwd, runId, resolved, secretValues) {
117214
+ const def = resolved.workflow;
117215
+ const payload = toJson2({
117216
+ runId,
117217
+ name: def.name,
117218
+ kind: def.kind ?? "state-machine",
117219
+ format: "todo",
117220
+ version: def.version ?? null,
117221
+ definitionDigest: resolved.digest,
117222
+ steps: buildWorkflowSteps(def).map((step) => ({
117223
+ ...step,
117224
+ ...step.description !== undefined ? { description: redactAndBound(step.description, secretValues, 512) } : {}
117225
+ }))
117226
+ });
117227
+ const { mkdir, writeFile } = await import("fs/promises");
117228
+ const artifactsDir = join36(cwd, ".spur", "run");
117229
+ await mkdir(artifactsDir, { recursive: true });
117230
+ const artifactPath = join36(artifactsDir, `${runId}-workflow-plan.json`);
117231
+ await writeFile(artifactPath, payload);
117232
+ return artifactPath;
117233
+ }
116942
117234
  async function makeEscalationPacketSink(bus, context4) {
116943
117235
  try {
116944
117236
  let locator;
@@ -116993,7 +117285,7 @@ function resolveWorkflowPaths(config4) {
116993
117285
  for (const path9 of paths) {
116994
117286
  if (path9.startsWith(BUNDLED_PATH_PREFIX)) {
116995
117287
  if (bundledRoot !== null)
116996
- expanded.push(join35(bundledRoot, path9.slice(BUNDLED_PATH_PREFIX.length)));
117288
+ expanded.push(join36(bundledRoot, path9.slice(BUNDLED_PATH_PREFIX.length)));
116997
117289
  } else {
116998
117290
  expanded.push(path9);
116999
117291
  }
@@ -117027,14 +117319,11 @@ function registerWorkflowCommand(program2, context4) {
117027
117319
  } else if (result.valid) {
117028
117320
  context4.output.write(`workflow valid: ${result.workflow.name} (${formatWorkflowVersion(result.workflow.version)})`);
117029
117321
  const c3 = result.composition;
117030
- if (c3 && (c3.findings.length > 0 || c3.suppressed > 0)) {
117322
+ if (c3 && c3.findings.length > 0) {
117031
117323
  for (const f of c3.findings) {
117032
117324
  const m = f.measure.kind === "shell-lines" ? `${f.measure.measured} shell lines (threshold ${f.measure.threshold})` : `${f.measure.measured} prompt chars (severity ${f.measure.severity})`;
117033
117325
  context4.output.error(`composition advisory: ${f.actionKey} \u2014 ${m} \u2014 ${f.recommendation}`);
117034
117326
  }
117035
- if (c3.suppressed > 0) {
117036
- context4.output.error(`composition advisory: ${c3.suppressed} shell finding(s) suppressed by baseline dispositions (guards excluded wholesale; docs/design/workflow-shell-ownership.md)`);
117037
- }
117038
117327
  }
117039
117328
  } else {
117040
117329
  context4.output.error(`workflow invalid: ${result.file}
@@ -117071,9 +117360,9 @@ ${result.errors.map((m) => ` - ${m}`).join(`
117071
117360
  const detail = options.verbose === true ? "full" : requestedDetail ?? "invocation";
117072
117361
  if (process.env[WORKFLOW_RUN_ACTIVE_ENV] === "1") {
117073
117362
  writeJsonError(context4.output, options, `workflow run: refusing to start \u2014 already inside an active workflow run (${WORKFLOW_RUN_ACTIVE_ENV}=1).
117074
- ` + `A pipeline that starts another pipeline forks a worktree and an agent run per level, without bound.
117075
- ` + `If you are an agent running inside a pipeline step: do NOT start a pipeline here. Report what you
117076
- ` + "needed and let the operator run it from a clean shell.", "VALIDATION_FAILED");
117363
+ A pipeline that starts another pipeline forks a worktree and an agent run per level, without bound.
117364
+ If you are an agent running inside a pipeline step: do NOT start a pipeline here. Report what you
117365
+ needed and let the operator run it from a clean shell.`, "VALIDATION_FAILED");
117077
117366
  context4.setExitCode(1);
117078
117367
  return;
117079
117368
  }
@@ -117106,8 +117395,22 @@ ${result.errors.map((m) => ` - ${m}`).join(`
117106
117395
  if (options.log === false) {
117107
117396
  cmd.push("--no-log");
117108
117397
  }
117398
+ let resolvedDefinition2;
117399
+ let planArtifactPath;
117400
+ try {
117401
+ resolvedDefinition2 = await resolveWorkflowDefinition(context4.cwd, file2, {
117402
+ embeddedSchemas: EMBEDDED_SPUR_SCHEMAS
117403
+ });
117404
+ planArtifactPath = await writeWorkflowPlanArtifact(context4.cwd, runId2, resolvedDefinition2, configuredSecretValues(context4.env));
117405
+ } catch (error51) {
117406
+ writeJsonError(context4.output, options, `workflow run --async: ${error51 instanceof Error ? error51.message : String(error51)}`, "VALIDATION_FAILED");
117407
+ context4.setExitCode(1);
117408
+ return;
117409
+ }
117109
117410
  try {
117110
- await spawnAsyncWorkflowWorker(spurBin, cmd);
117411
+ await spawnAsyncWorkflowWorker(spurBin, cmd, {
117412
+ SPUR_EXPECTED_DEFINITION_DIGEST: resolvedDefinition2.digest
117413
+ });
117111
117414
  } catch {
117112
117415
  markWorkflowRunActive();
117113
117416
  let result2;
@@ -117115,7 +117418,8 @@ ${result.errors.map((m) => ` - ${m}`).join(`
117115
117418
  result2 = await makeSvc(options.json).run(file2, {
117116
117419
  runId: runId2,
117117
117420
  vars: { spurBin: resolveSpurBin(), ...parseVars(options.vars) },
117118
- dryRun: options.dryRun || undefined
117421
+ dryRun: options.dryRun || undefined,
117422
+ resolvedDefinition: resolvedDefinition2
117119
117423
  });
117120
117424
  } finally {
117121
117425
  clearWorkflowRunActive();
@@ -117140,11 +117444,15 @@ ${result.errors.map((m) => ` - ${m}`).join(`
117140
117444
  context4.setExitCode(1);
117141
117445
  return;
117142
117446
  }
117143
- const asyncResult = { runId: runId2, status: "started", workflowName: file2 };
117447
+ try {
117448
+ await makeSvc(options.json).stampPlanArtifactPath(runId2, planArtifactPath);
117449
+ } catch {}
117450
+ const asyncResult = { runId: runId2, status: "started", workflowName: file2, planArtifactPath };
117144
117451
  if (json3)
117145
117452
  context4.output.write(toEnvelopeJson(asyncResult, { enveloped: options.jsonEnvelope }));
117146
117453
  else if (!silent) {
117147
117454
  context4.output.write(`Started async run: ${runId2}
117455
+ Plan: ${planArtifactPath}
117148
117456
  Monitor with: spur workflow trace ${runId2} --follow`);
117149
117457
  }
117150
117458
  context4.setExitCode(0);
@@ -117172,19 +117480,49 @@ Monitor with: spur workflow trace ${runId2} --follow`);
117172
117480
  traceWriter = new WorkflowTraceWriter(context4.cwd, runId);
117173
117481
  traceWriter.attach(bus);
117174
117482
  }
117175
- let planPreview;
117176
- if (options.plan !== false) {
117483
+ const expectedDigest = process.env.SPUR_EXPECTED_DEFINITION_DIGEST;
117484
+ let resolvedDefinition;
117485
+ if (options.plan !== false || expectedDigest !== undefined) {
117177
117486
  try {
117178
- const resolved = resolveWorkflowFile(context4.cwd, file2);
117179
- if (resolved.path !== null) {
117180
- const def = await loadWorkflowDef(resolved.path, { validateSchema: false });
117181
- planPreview = renderRunPlan(def);
117487
+ resolvedDefinition = await resolveWorkflowDefinition(context4.cwd, file2, {
117488
+ embeddedSchemas: EMBEDDED_SPUR_SCHEMAS
117489
+ });
117490
+ } catch (error51) {
117491
+ if (expectedDigest !== undefined) {
117492
+ writeJsonError(context4.output, options, `workflow run: refusing to start \u2014 ${error51 instanceof Error ? error51.message : String(error51)}`, "VALIDATION_FAILED");
117493
+ context4.setExitCode(1);
117494
+ return;
117182
117495
  }
117183
- } catch {}
117496
+ }
117497
+ }
117498
+ const mismatch = expectedDigestMismatch(resolvedDefinition?.digest, expectedDigest);
117499
+ if (mismatch !== null) {
117500
+ writeJsonError(context4.output, options, mismatch, "VALIDATION_FAILED");
117501
+ context4.setExitCode(1);
117502
+ return;
117503
+ }
117504
+ let planPreview;
117505
+ if (options.plan !== false && resolvedDefinition !== undefined) {
117506
+ planPreview = renderRunPlan(resolvedDefinition.workflow);
117507
+ }
117508
+ if (resolvedDefinition !== undefined) {
117509
+ const executors = context4.spurConfig?.agent?.executors;
117510
+ const flow = resolvedDefinition.workflow;
117511
+ const agentRunActions = flow.kind === "transition-flow" ? flow.nodes.flatMap((node) => node.action?.kind === "agent.run" ? [node.action] : []) : flow.states.flatMap((state) => (state.onEnter ?? []).filter((a2) => a2.kind === "agent.run"));
117512
+ for (const action of agentRunActions) {
117513
+ const pin = action.options?.agent;
117514
+ const requires = parseRequiresCapabilities(action.options?.requiresCapabilities);
117515
+ if (typeof pin !== "string" || !requires.ok)
117516
+ continue;
117517
+ const evaluation = evaluateCapabilities(requires.requires, executors?.find((e) => e.name === pin));
117518
+ if (!evaluation.ok) {
117519
+ context4.output.error(`Warning: capability preflight (0777 R4): ${capabilityDiagnostic(pin, evaluation)}`);
117520
+ }
117521
+ }
117184
117522
  }
117185
117523
  const runLog = options.log === false ? undefined : new WorkflowRunLogSink({
117186
117524
  bus,
117187
- dir: join35(context4.cwd, ".spur", "run"),
117525
+ dir: join36(context4.cwd, ".spur", "run"),
117188
117526
  runId,
117189
117527
  ...planPreview !== undefined ? { planPreview } : {},
117190
117528
  ...resolveOutputLogConfig(context4.spurConfig ?? null)
@@ -117234,15 +117572,9 @@ Monitor with: spur workflow trace ${runId2} --follow`);
117234
117572
  }
117235
117573
  const steeringController = options.steer === true ? await (async () => {
117236
117574
  let identity7;
117237
- try {
117238
- const resolved = resolveWorkflowFile(context4.cwd, file2);
117239
- if (resolved.path !== null) {
117240
- const def = await loadWorkflowDef(resolved.path, {
117241
- validateSchema: false
117242
- });
117243
- identity7 = createWorkflowEventIdentity(def);
117244
- }
117245
- } catch {}
117575
+ if (resolvedDefinition !== undefined) {
117576
+ identity7 = createWorkflowEventIdentity(resolvedDefinition.workflow);
117577
+ }
117246
117578
  return new WorkflowSteeringController((ack) => {
117247
117579
  bus.emit("workflow.steering", identity7 === undefined ? ack : decorateWorkflowEvent(identity7, "workflow.steering", ack));
117248
117580
  context4.output.write(`[steer] ${ack.accepted ? "ack" : "nack"} ${ack.operation} \xB7 ${ack.reason ?? `version=${ack.version}`}`);
@@ -117264,7 +117596,8 @@ Monitor with: spur workflow trace ${runId2} --follow`);
117264
117596
  vars,
117265
117597
  dryRun: options.dryRun || undefined,
117266
117598
  recordSelfPid: process.env.SPUR_ASYNC_WORKER === "1",
117267
- ...steeringController !== undefined ? { steeringController } : {}
117599
+ ...steeringController !== undefined ? { steeringController } : {},
117600
+ ...resolvedDefinition !== undefined ? { resolvedDefinition } : {}
117268
117601
  });
117269
117602
  } finally {
117270
117603
  clearWorkflowRunActive();
@@ -117342,7 +117675,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
117342
117675
  ledger.unsubscribe();
117343
117676
  }
117344
117677
  });
117345
- workflow.command("clean").description("Housekeeping: finalize orphaned runs stuck in running/pending past a staleness threshold " + "(mark as failed) and reclaim retained run logs older than workflow.logRetentionDays. " + "`--logs` scopes to log reclamation only. To cancel a single live run by id, use " + "`spur workflow cancel <run-id>` instead.").option("--older-than <minutes>", "Staleness threshold in minutes (stale-run scope only)", "30").option(...SHARED_OPTIONS.forceWorkflowClean).option("--logs", "Scope to retained run-log reclamation only (skip stale-run finalization)").option(...SHARED_OPTIONS.dryRunWorkflowClean).option(...SHARED_OPTIONS.jsonSupported).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options) => {
117678
+ workflow.command("clean").description("Housekeeping: finalize orphaned runs stuck in running/pending past a staleness threshold (mark as failed) and reclaim retained run logs older than workflow.logRetentionDays. `--logs` scopes to log reclamation only. To cancel a single live run by id, use `spur workflow cancel <run-id>` instead.").option("--older-than <minutes>", "Staleness threshold in minutes (stale-run scope only)", "30").option(...SHARED_OPTIONS.forceWorkflowClean).option("--logs", "Scope to retained run-log reclamation only (skip stale-run finalization)").option(...SHARED_OPTIONS.dryRunWorkflowClean).option(...SHARED_OPTIONS.jsonSupported).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options) => {
117346
117679
  const dryRun = options.dryRun === true;
117347
117680
  const logsOnly = options.logs === true;
117348
117681
  const force = options.force === true;
@@ -117441,21 +117774,25 @@ Monitor with: spur workflow trace ${runId2} --follow`);
117441
117774
  context4.setExitCode(1);
117442
117775
  return;
117443
117776
  }
117444
- const filePath = resolved.path;
117445
- let def;
117777
+ let resolvedDefinition;
117446
117778
  try {
117447
- def = await loadWorkflowDef(filePath, { validateSchema: true });
117779
+ resolvedDefinition = await resolveWorkflowDefinition(context4.cwd, file2, {
117780
+ embeddedSchemas: EMBEDDED_SPUR_SCHEMAS
117781
+ });
117448
117782
  } catch (err) {
117449
117783
  writeJsonError(context4.output, options, `workflow show: cannot read or parse ${file2} \u2014 ${err instanceof Error ? err.message : String(err)}`, "VALIDATION_FAILED");
117450
117784
  context4.setExitCode(1);
117451
117785
  return;
117452
117786
  }
117787
+ const def = resolvedDefinition.workflow;
117453
117788
  if (options.json) {
117454
117789
  if (options.format === "todo") {
117455
117790
  context4.output.write(toJson2({
117456
117791
  name: def.name,
117457
117792
  kind: def.kind ?? "state-machine",
117458
117793
  format: "todo",
117794
+ version: resolvedDefinition.workflow.version ?? null,
117795
+ definitionDigest: resolvedDefinition.digest,
117459
117796
  steps: buildWorkflowSteps(def)
117460
117797
  }));
117461
117798
  } else {
@@ -117463,6 +117800,8 @@ Monitor with: spur workflow trace ${runId2} --follow`);
117463
117800
  name: def.name,
117464
117801
  kind: def.kind ?? "state-machine",
117465
117802
  format: "mermaid",
117803
+ version: resolvedDefinition.workflow.version ?? null,
117804
+ definitionDigest: resolvedDefinition.digest,
117466
117805
  diagram: renderWorkflowMermaid(def)
117467
117806
  }));
117468
117807
  }
@@ -117710,7 +118049,7 @@ function readRunLogChunk(logPath, offset) {
117710
118049
  }
117711
118050
  }
117712
118051
  async function followRunLog(service, runId, dir, pollMs, write, wait = (ms) => sleep5(ms)) {
117713
- const logPath = join35(dir, ".spur", "run", `${runId}.log`);
118052
+ const logPath = join36(dir, ".spur", "run", `${runId}.log`);
117714
118053
  let offset = 0;
117715
118054
  let everRead = false;
117716
118055
  while (true) {
@@ -117773,7 +118112,7 @@ init_src();
117773
118112
  init_src2();
117774
118113
  init_dist5();
117775
118114
  await init_src3();
117776
- import { dirname as dirname22, join as join36, resolve as resolve18 } from "path";
118115
+ import { dirname as dirname22, join as join37, resolve as resolve17 } from "path";
117777
118116
  import { isatty as isatty4 } from "tty";
117778
118117
 
117779
118118
  // ../../node_modules/.bun/@clack+core@1.4.1/node_modules/@clack/core/dist/index.mjs
@@ -119188,7 +119527,7 @@ function resolveAgentRoles(agentConfig) {
119188
119527
  return resolved;
119189
119528
  }
119190
119529
  function createCliContext(options) {
119191
- const cwd = resolve18(options.cwd ?? process.cwd());
119530
+ const cwd = resolve17(options.cwd ?? process.cwd());
119192
119531
  const env = options.env ?? process.env;
119193
119532
  const fs3 = createNodeFileSystem3(cwd);
119194
119533
  const agentConfig = options.agentConfig ?? options.spurConfig?.agent;
@@ -119236,7 +119575,7 @@ function createCliContext(options) {
119236
119575
  function noopSetExitCode(_code) {}
119237
119576
  async function createMigratedDbAdapter(cwd = process.cwd(), env = process.env, dbUrl) {
119238
119577
  const config4 = buildConfigFromEnv(env);
119239
- const configuredUrl = env.DATABASE_URL === undefined ? join36(cwd, DEFAULT_DATABASE_URL) : config4.database.url;
119578
+ const configuredUrl = env.DATABASE_URL === undefined ? join37(cwd, DEFAULT_DATABASE_URL) : config4.database.url;
119240
119579
  const url2 = dbUrl ?? configuredUrl;
119241
119580
  if (url2 !== IN_MEMORY_DATABASE_URL) {
119242
119581
  await createNodeFileSystem3().ensureDir(dirname22(url2));
@@ -119317,10 +119656,12 @@ async function runCommandDispatch(argv, context4, output2) {
119317
119656
  registerRuleCommand(program2, context4);
119318
119657
  const selfCommand = program2.command("self").summary("inspect and manage the Spur installation itself");
119319
119658
  registerInitCommand(selfCommand, context4);
119659
+ registerMaintainCommand(selfCommand, context4);
119320
119660
  registerMigrateCommand(selfCommand, context4);
119321
119661
  registerServeCommand(selfCommand, context4);
119322
119662
  registerStatusCommand(selfCommand, context4);
119323
119663
  registerInitCommand(program2, context4, { hidden: true });
119664
+ registerMaintainCommand(program2, context4, { hidden: true });
119324
119665
  registerMigrateCommand(program2, context4, { hidden: true });
119325
119666
  registerServeCommand(program2, context4, { hidden: true });
119326
119667
  registerStatusCommand(program2, context4, { hidden: true });