@gobing-ai/spur 0.3.95 → 0.3.96

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 (155) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/plugin-scripts.json +0 -54
  3. package/config/rules/boundary/sp-script-placement.yaml +19 -0
  4. package/config/rules/strict/runtime-boundaries.yaml +5 -0
  5. package/config/rules/structure/test-location.yaml +2 -0
  6. package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +1 -1
  7. package/config/rules/typescript/output-boundaries.yaml +15 -2
  8. package/config/script-placement-baseline.json +51 -0
  9. package/config/workflows/feature-verification.yaml +1 -1
  10. package/config/workflows/history-anatomy.yaml +23 -11
  11. package/config/workflows/idea-pipeline.yaml +80 -85
  12. package/config/workflows/pr-review.yaml +18 -9
  13. package/config/workflows/task-pipeline.yaml +42 -95
  14. package/config/workflows/wrapup-pipeline.yaml +89 -37
  15. package/package.json +1 -1
  16. package/plugins/sp/README.md +8 -10
  17. package/plugins/sp/agents/super-reviewer.md +43 -18
  18. package/plugins/sp/commands/dev-fixgha.md +83 -0
  19. package/plugins/sp/commands/dev-gitmsg.md +8 -6
  20. package/plugins/sp/commands/dev-gtd.md +2 -2
  21. package/plugins/sp/commands/dev-idea.md +22 -12
  22. package/plugins/sp/commands/dev-plan.md +8 -9
  23. package/plugins/sp/commands/dev-review.md +22 -13
  24. package/plugins/sp/commands/dev-verifyall.md +1 -1
  25. package/plugins/sp/commands/spur-init.md +2 -2
  26. package/plugins/sp/lib/history-anatomy.generated.d.mts +112 -0
  27. package/plugins/sp/lib/history-anatomy.generated.mjs +686 -0
  28. package/plugins/sp/lib/idea-handoff.generated.mjs +5 -4
  29. package/plugins/sp/lib/inline-run.generated.d.mts +11 -0
  30. package/plugins/sp/lib/inline-run.generated.mjs +25 -8
  31. package/plugins/sp/lib/quality-gate.generated.d.mts +104 -0
  32. package/plugins/sp/lib/quality-gate.generated.mjs +438 -0
  33. package/plugins/sp/lib/residual-scan.generated.d.mts +62 -0
  34. package/plugins/sp/lib/residual-scan.generated.mjs +210 -0
  35. package/plugins/sp/lib/spur-bin.ts +36 -0
  36. package/plugins/sp/lib/step-profile.generated.d.mts +71 -0
  37. package/plugins/sp/lib/step-profile.generated.mjs +174 -0
  38. package/plugins/sp/plugin.json +1 -1
  39. package/plugins/sp/references/roles.md +1 -1
  40. package/plugins/sp/scripts/history-anatomy-cache.mjs +20 -19
  41. package/plugins/sp/scripts/history-anatomy-cache.ts +23 -928
  42. package/plugins/sp/scripts/inline-run-setup.mjs +85 -320
  43. package/plugins/sp/scripts/inline-run-setup.ts +104 -667
  44. package/plugins/sp/scripts/quality-gate.mjs +34 -19
  45. package/plugins/sp/scripts/quality-gate.ts +12 -658
  46. package/plugins/sp/scripts/residual-scan.mjs +135 -156
  47. package/plugins/sp/scripts/residual-scan.ts +102 -499
  48. package/plugins/sp/scripts/script-root.mjs +5 -1
  49. package/plugins/sp/scripts/script-root.ts +5 -1
  50. package/plugins/sp/scripts/workflow-step-profile.mjs +25 -17
  51. package/plugins/sp/scripts/workflow-step-profile.ts +21 -315
  52. package/plugins/sp/scripts/wrapup-drift-probe.mjs +8 -2
  53. package/plugins/sp/scripts/wrapup-drift-probe.ts +3 -2
  54. package/plugins/sp/scripts/wrapup-steps.mjs +10 -32
  55. package/plugins/sp/scripts/wrapup-steps.ts +8 -36
  56. package/plugins/sp/skills/code-improvement/SKILL.md +5 -4
  57. package/plugins/sp/skills/code-verification/SKILL.md +34 -9
  58. package/plugins/sp/skills/code-verification/references/verdict-schema.md +3 -3
  59. package/plugins/sp/skills/functional-review/SKILL.md +7 -4
  60. package/plugins/sp/skills/functional-review/references/verdict-schema.md +1 -1
  61. package/plugins/sp/skills/history-anatomy/references/modes.md +2 -1
  62. package/plugins/sp/skills/next-feature/references/handoff-routing.md +1 -1
  63. package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
  64. package/plugins/sp/skills/spur-cli/SKILL.md +3 -3
  65. package/plugins/sp/skills/spur-cli/references/agent.md +10 -10
  66. package/plugins/sp/skills/spur-cli/references/features.md +17 -6
  67. package/plugins/sp/skills/spur-cli/references/init.md +17 -16
  68. package/plugins/sp/skills/spur-cli/references/self.md +3 -2
  69. package/plugins/sp/skills/spur-cli/references/serve.md +10 -10
  70. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +21 -6
  71. package/plugins/sp/skills/spur-cli/references/tasks.md +14 -8
  72. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +4 -3
  73. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +11 -9
  74. package/plugins/sp/skills/spur-dev/references/decision-brief.md +1 -1
  75. package/plugins/sp/skills/spur-dev/references/dev-operations.md +86 -70
  76. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +3 -3
  77. package/plugins/sp/skills/spur-dev/references/execution-batch.md +77 -34
  78. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +5 -9
  79. package/plugins/sp/skills/spur-dev/references/feature-link-helper.md +3 -3
  80. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +31 -21
  81. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +19 -19
  82. package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +4 -3
  83. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +29 -3
  84. package/plugins/sp/skills/spur-doctor/SKILL.md +1 -1
  85. package/plugins/sp/skills/sys-architecture/SKILL.md +3 -2
  86. package/spur.js +1495 -369
  87. package/web/_astro/{BoardApp.Cpxntzad.js → BoardApp.BIjMatT1.js} +1 -1
  88. package/web/_astro/{BoardApp.BxJuwD7I.js → BoardApp.GvjIe9Z6.js} +42 -42
  89. package/web/_astro/{TaskDetail.C5bW4WGV.js → TaskDetail.BZM3EAFx.js} +1 -1
  90. package/web/_astro/{arc.CyjRvNMY.js → arc.HkRiZnoI.js} +1 -1
  91. package/web/_astro/{architectureDiagram-3BPJPVTR.B4lWRzJA.js → architectureDiagram-3BPJPVTR.BDdK-tgZ.js} +1 -1
  92. package/web/_astro/{blockDiagram-GPEHLZMM.Be38USQ2.js → blockDiagram-GPEHLZMM.CZ9kOBxx.js} +1 -1
  93. package/web/_astro/{c4Diagram-AAUBKEIU.DT9Fj5Qx.js → c4Diagram-AAUBKEIU.DXK9qeRD.js} +1 -1
  94. package/web/_astro/channel.DlhX2MEt.js +1 -0
  95. package/web/_astro/{chunk-2J33WTMH.SeSBWLg5.js → chunk-2J33WTMH.BTD2WeX5.js} +1 -1
  96. package/web/_astro/{chunk-4BX2VUAB.DoO4VE14.js → chunk-4BX2VUAB.Ci6Qbcvk.js} +1 -1
  97. package/web/_astro/{chunk-55IACEB6.DrmTFA53.js → chunk-55IACEB6.yl3zsj7p.js} +1 -1
  98. package/web/_astro/{chunk-727SXJPM.Bi-TUb_V.js → chunk-727SXJPM.ClXZsfyR.js} +1 -1
  99. package/web/_astro/{chunk-AQP2D5EJ.B-xp8Uhi.js → chunk-AQP2D5EJ.BmKWZBcP.js} +1 -1
  100. package/web/_astro/{chunk-FMBD7UC4.DGWrDnUU.js → chunk-FMBD7UC4.By30fcb7.js} +1 -1
  101. package/web/_astro/{chunk-ND2GUHAM._BagvPDy.js → chunk-ND2GUHAM.24NmrD-k.js} +1 -1
  102. package/web/_astro/{chunk-QZHKN3VN.VOboYWQ0.js → chunk-QZHKN3VN.BN4sKdcS.js} +1 -1
  103. package/web/_astro/{classDiagram-4FO5ZUOK.D_apfHS7.js → classDiagram-4FO5ZUOK.CJpoMPb5.js} +1 -1
  104. package/web/_astro/{classDiagram-v2-Q7XG4LA2.D_apfHS7.js → classDiagram-v2-Q7XG4LA2.CJpoMPb5.js} +1 -1
  105. package/web/_astro/{cose-bilkent-S5V4N54A.DNLU_L-x.js → cose-bilkent-S5V4N54A.BraxQ2Nt.js} +1 -1
  106. package/web/_astro/{cynefin-OW5HDTMX.D8borhV-.js → cynefin-OW5HDTMX.gzoU73oL.js} +1 -1
  107. package/web/_astro/{dagre-BM42HDAG.Uc0lvjcV.js → dagre-BM42HDAG.wyDfWySU.js} +1 -1
  108. package/web/_astro/{diagram-2AECGRRQ.DDIxtDBo.js → diagram-2AECGRRQ.h2_EbMxV.js} +1 -1
  109. package/web/_astro/{diagram-5GNKFQAL.DVoDnD3H.js → diagram-5GNKFQAL.Db6dcs89.js} +1 -1
  110. package/web/_astro/{diagram-KO2AKTUF.CRubTJ-0.js → diagram-KO2AKTUF.NVwzxu_U.js} +1 -1
  111. package/web/_astro/{diagram-LMA3HP47.m9SeYcXQ.js → diagram-LMA3HP47.B-eNgDQk.js} +1 -1
  112. package/web/_astro/{diagram-OG6HWLK6.C6s2ZjV5.js → diagram-OG6HWLK6.Bsa8ZRbJ.js} +1 -1
  113. package/web/_astro/{erDiagram-TEJ5UH35.nSzr5KTj.js → erDiagram-TEJ5UH35.BospRE5Q.js} +1 -1
  114. package/web/_astro/{flowDiagram-I6XJVG4X.DrZcys16.js → flowDiagram-I6XJVG4X.DMzvHwAA.js} +1 -1
  115. package/web/_astro/{ganttDiagram-6RSMTGT7.Do4M1b-K.js → ganttDiagram-6RSMTGT7.Dmdxgd-M.js} +1 -1
  116. package/web/_astro/{gitGraphDiagram-PVQCEYII.BWc9oJ4l.js → gitGraphDiagram-PVQCEYII.CaqdO0rL.js} +1 -1
  117. package/web/_astro/index.EoZlzLK-.css +1 -0
  118. package/web/_astro/{infoDiagram-5YYISTIA.BIdOctFk.js → infoDiagram-5YYISTIA.DDVD9Wn7.js} +1 -1
  119. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CV5SuG2t.js → ishikawaDiagram-YF4QCWOH.EKyCUFsL.js} +1 -1
  120. package/web/_astro/{journeyDiagram-JHISSGLW.CxE-rEJ-.js → journeyDiagram-JHISSGLW.By2-RkYB.js} +1 -1
  121. package/web/_astro/{kanban-definition-UN3LZRKU.BPRdKWs_.js → kanban-definition-UN3LZRKU.ar-WmlyR.js} +1 -1
  122. package/web/_astro/{linear.zRsuuDTE.js → linear.BnzMBgo_.js} +1 -1
  123. package/web/_astro/{mermaid.core.jAJTcMKc.js → mermaid.core.DSqeFu2Y.js} +4 -4
  124. package/web/_astro/{mindmap-definition-RKZ34NQL.BffJxfCr.js → mindmap-definition-RKZ34NQL.1XcF8Wh2.js} +1 -1
  125. package/web/_astro/{pieDiagram-4H26LBE5.CacwWaE3.js → pieDiagram-4H26LBE5.DnItHOdK.js} +1 -1
  126. package/web/_astro/{quadrantDiagram-W4KKPZXB.hXELD06-.js → quadrantDiagram-W4KKPZXB.BQQyrnBt.js} +1 -1
  127. package/web/_astro/{requirementDiagram-4Y6WPE33.DdEbPcqT.js → requirementDiagram-4Y6WPE33.Dmzxun82.js} +1 -1
  128. package/web/_astro/{sankeyDiagram-5OEKKPKP.DgaDocKv.js → sankeyDiagram-5OEKKPKP.BBIeRKYB.js} +1 -1
  129. package/web/_astro/{sequenceDiagram-3UESZ5HK.B0KScnAu.js → sequenceDiagram-3UESZ5HK.CUNVdBBf.js} +1 -1
  130. package/web/_astro/{stateDiagram-AJRCARHV.CV9M_WNc.js → stateDiagram-AJRCARHV.DlFf2sJm.js} +1 -1
  131. package/web/_astro/{stateDiagram-v2-BHNVJYJU.BOAo74Et.js → stateDiagram-v2-BHNVJYJU.DXR7I1H5.js} +1 -1
  132. package/web/_astro/{timeline-definition-PNZ67QCA.CqqepUMe.js → timeline-definition-PNZ67QCA.BcO-GyX2.js} +1 -1
  133. package/web/_astro/{vennDiagram-CIIHVFJN.DDqe4dOG.js → vennDiagram-CIIHVFJN.ChCECmmW.js} +1 -1
  134. package/web/_astro/{wardleyDiagram-YWT4CUSO.B3R9OPdO.js → wardleyDiagram-YWT4CUSO.CRaT12mD.js} +1 -1
  135. package/web/_astro/{xychartDiagram-2RQKCTM6.B3SvgXpQ.js → xychartDiagram-2RQKCTM6.B3lsHVpj.js} +1 -1
  136. package/web/index.html +2 -2
  137. package/plugins/sp/lib/artifact-digest.generated.d.mts +0 -7
  138. package/plugins/sp/lib/artifact-digest.generated.mjs +0 -48
  139. package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -301
  140. package/plugins/sp/scripts/feature-sync-bounded.ts +0 -481
  141. package/plugins/sp/scripts/idea-coverage-check.ts +0 -168
  142. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -298
  143. package/plugins/sp/scripts/record-feature-sync.mjs +0 -63
  144. package/plugins/sp/scripts/record-feature-sync.ts +0 -84
  145. package/plugins/sp/scripts/script-contract-check.ts +0 -506
  146. package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
  147. package/plugins/sp/scripts/surface-drift-inventory.ts +0 -989
  148. package/plugins/sp/scripts/task-evidence-precheck.ts +0 -189
  149. package/plugins/sp/scripts/task-size-precheck.ts +0 -212
  150. package/plugins/sp/scripts/transition-shim-check.ts +0 -238
  151. package/plugins/sp/scripts/validate-commands.ts +0 -689
  152. package/plugins/sp/scripts/validate-flag-contracts.ts +0 -890
  153. package/plugins/sp/scripts/verify-answer-lint.ts +0 -549
  154. package/web/_astro/channel.CI6N_tCg.js +0 -1
  155. package/web/_astro/index.CENnIEqT.css +0 -1
package/spur.js CHANGED
@@ -14897,6 +14897,7 @@ var init_finding_codes = __esm(() => {
14897
14897
  "L2.disallowed-section",
14898
14898
  "L2.heading-level",
14899
14899
  "L2.section-order",
14900
+ "L2.unclosed-code-fence",
14900
14901
  "L3.requirements-format",
14901
14902
  "L3.requirements-checkbox",
14902
14903
  "L3.requirements-empty",
@@ -14951,7 +14952,10 @@ var init_finding_codes = __esm(() => {
14951
14952
  "L4.feature-receipt-failed",
14952
14953
  "L4.feature-receipt-run",
14953
14954
  "L4.feature-receipt-stale",
14954
- "L4.feature-receipt-contract"
14955
+ "L4.feature-receipt-contract",
14956
+ "precheck-size",
14957
+ "precheck-evidence",
14958
+ "inventory-coverage"
14955
14959
  ];
14956
14960
  FINDING_CODES = {
14957
14961
  L1_MARKDOWN_PARSE: "L1.markdown-parse",
@@ -14961,6 +14965,7 @@ var init_finding_codes = __esm(() => {
14961
14965
  L2_DISALLOWED_SECTION: "L2.disallowed-section",
14962
14966
  L2_HEADING_LEVEL: "L2.heading-level",
14963
14967
  L2_SECTION_ORDER: "L2.section-order",
14968
+ L2_UNCLOSED_CODE_FENCE: "L2.unclosed-code-fence",
14964
14969
  L3_REQUIREMENTS_FORMAT: "L3.requirements-format",
14965
14970
  L3_REQUIREMENTS_CHECKBOX: "L3.requirements-checkbox",
14966
14971
  L3_REQUIREMENTS_EMPTY: "L3.requirements-empty",
@@ -15015,7 +15020,10 @@ var init_finding_codes = __esm(() => {
15015
15020
  L4_FEATURE_RECEIPT_FAILED: "L4.feature-receipt-failed",
15016
15021
  L4_FEATURE_RECEIPT_RUN: "L4.feature-receipt-run",
15017
15022
  L4_FEATURE_RECEIPT_STALE: "L4.feature-receipt-stale",
15018
- L4_FEATURE_RECEIPT_CONTRACT: "L4.feature-receipt-contract"
15023
+ L4_FEATURE_RECEIPT_CONTRACT: "L4.feature-receipt-contract",
15024
+ PRECHECK_SIZE: "precheck-size",
15025
+ PRECHECK_EVIDENCE: "precheck-evidence",
15026
+ INVENTORY_COVERAGE: "inventory-coverage"
15019
15027
  };
15020
15028
  });
15021
15029
 
@@ -52233,6 +52241,10 @@ async function countToolCallsSince(db2, source, runStartedAt) {
52233
52241
  const row = await db2.queryFirst("SELECT COUNT(*) AS cnt FROM history_tool_call WHERE source = ? AND imported_at >= ?", source, runStartedAt);
52234
52242
  return row?.cnt ?? 0;
52235
52243
  }
52244
+ async function countToolCallArgsRaw(db2, source) {
52245
+ const row = await db2.queryFirst("SELECT COUNT(*) AS cnt FROM history_tool_call WHERE args_raw IS NOT NULL AND source = ?", source);
52246
+ return row?.cnt ?? 0;
52247
+ }
52236
52248
  function withStepPredicates(wmWhere, predicates) {
52237
52249
  return wmWhere === "" ? `WHERE ${predicates}` : `${wmWhere} AND ${predicates}`;
52238
52250
  }
@@ -73045,6 +73057,30 @@ function findHeadings(body, hashes) {
73045
73057
  }
73046
73058
  return hits;
73047
73059
  }
73060
+ function findUnclosedFenceLine(text4) {
73061
+ let inCodeBlock = false;
73062
+ let openLine = 0;
73063
+ let lineNo = 0;
73064
+ let lineStart = 0;
73065
+ for (let i2 = 0;i2 <= text4.length; i2++) {
73066
+ const atEnd = i2 === text4.length;
73067
+ if (!atEnd && text4[i2] !== `
73068
+ `)
73069
+ continue;
73070
+ const line = text4.slice(lineStart, i2);
73071
+ lineNo++;
73072
+ if (line.startsWith("```")) {
73073
+ if (inCodeBlock) {
73074
+ inCodeBlock = false;
73075
+ } else {
73076
+ inCodeBlock = true;
73077
+ openLine = lineNo;
73078
+ }
73079
+ }
73080
+ lineStart = i2 + 1;
73081
+ }
73082
+ return inCodeBlock ? openLine : null;
73083
+ }
73048
73084
  function normalizeYamlScalars(data) {
73049
73085
  const out = {};
73050
73086
  for (const [key, value] of Object.entries(data)) {
@@ -73067,12 +73103,14 @@ class MarkdownDocument {
73067
73103
  _sections;
73068
73104
  _demotedHeadings = [];
73069
73105
  _duplicateSectionNames = [];
73070
- constructor(domain2, frontmatterBlock, frontmatter, preamble, sections) {
73106
+ _unclosedFenceLine;
73107
+ constructor(domain2, frontmatterBlock, frontmatter, preamble, sections, unclosedFenceLine) {
73071
73108
  this._domain = domain2;
73072
73109
  this._frontmatterBlock = frontmatterBlock;
73073
73110
  this._frontmatter = frontmatter;
73074
73111
  this._preamble = preamble;
73075
73112
  this._sections = sections;
73113
+ this._unclosedFenceLine = unclosedFenceLine;
73076
73114
  }
73077
73115
  static parse(content, domain2) {
73078
73116
  const level = HEADING_LEVELS[domain2];
@@ -73095,6 +73133,8 @@ class MarkdownDocument {
73095
73133
  frontmatterBlock = fmMatch[0];
73096
73134
  body = content.slice(fmMatch[0].length);
73097
73135
  }
73136
+ const bodyUnclosedFenceLine = findUnclosedFenceLine(body);
73137
+ const unclosedFenceLine = bodyUnclosedFenceLine === null ? null : bodyUnclosedFenceLine + (frontmatterBlock.match(/\n/g) ?? []).length;
73098
73138
  const hits = findHeadings(body, hashes);
73099
73139
  const first = hits[0];
73100
73140
  const preamble = first !== undefined ? body.slice(0, first.index) : body;
@@ -73119,7 +73159,7 @@ class MarkdownDocument {
73119
73159
  sections.push(s2);
73120
73160
  }
73121
73161
  }
73122
- const doc2 = new MarkdownDocument(domain2, frontmatterBlock, frontmatter, preamble, sections);
73162
+ const doc2 = new MarkdownDocument(domain2, frontmatterBlock, frontmatter, preamble, sections, unclosedFenceLine);
73123
73163
  doc2._duplicateSectionNames.push(...duplicateNames);
73124
73164
  return doc2;
73125
73165
  }
@@ -73154,6 +73194,23 @@ class MarkdownDocument {
73154
73194
  get duplicateSectionNames() {
73155
73195
  return this._duplicateSectionNames;
73156
73196
  }
73197
+ unclosedFenceLine() {
73198
+ return this._unclosedFenceLine;
73199
+ }
73200
+ assertFenceBalance(name, body) {
73201
+ const docFenceLine = this._unclosedFenceLine;
73202
+ if (docFenceLine !== null) {
73203
+ throw new Error(`Cannot write section "${name}": the document has an unclosed code fence at line ${docFenceLine}; ` + "later sections are hidden and would be dropped by this write. Close the fence and re-parse.");
73204
+ }
73205
+ if (this._duplicateSectionNames.length > 0) {
73206
+ const names = this._duplicateSectionNames.map((n2) => `"${n2}"`).join(", ");
73207
+ throw new Error(`Cannot write section "${name}": the document has duplicate top-level sections (${names}); ` + "the serialized result would have fewer sections than the input and the duplicates would be dropped. " + "Merge or remove the duplicates and re-parse.");
73208
+ }
73209
+ const bodyFenceLine = findUnclosedFenceLine(body);
73210
+ if (bodyFenceLine !== null) {
73211
+ throw new Error(`Cannot write section "${name}": the new body has an unclosed code fence (opening at body line ${bodyFenceLine}); ` + "writing it would leave the document inside an open fence and hide later sections.");
73212
+ }
73213
+ }
73157
73214
  get canonicalSections() {
73158
73215
  const base2 = this._domain === "task" ? TASK_CANONICAL_SECTIONS : FEATURE_CANONICAL_SECTIONS;
73159
73216
  const extra = UNIVERSAL_SECTIONS.filter((s2) => !base2.includes(s2));
@@ -73182,6 +73239,7 @@ class MarkdownDocument {
73182
73239
  }
73183
73240
  replaceSection(name, body) {
73184
73241
  this.validateSectionName(name);
73242
+ this.assertFenceBalance(name, body);
73185
73243
  const cleaned = this.stripSameLevelHeadings(body);
73186
73244
  const section = this.findSection(name);
73187
73245
  if (section !== undefined) {
@@ -73245,6 +73303,7 @@ ${body.trimEnd()}
73245
73303
  }
73246
73304
  replaceMarkerRegion(name, content) {
73247
73305
  this.validateSectionName(name);
73306
+ this.assertFenceBalance(name, content);
73248
73307
  const section = this.findSection(name);
73249
73308
  if (section === undefined) {
73250
73309
  throw new Error(`Section "${name}" does not exist in this document.`);
@@ -74133,6 +74192,7 @@ __export(exports_src, {
74133
74192
  createDefaultRegistry: () => createDefaultRegistry,
74134
74193
  createBoundaryContext: () => createBoundaryContext,
74135
74194
  countToolCallsSince: () => countToolCallsSince,
74195
+ countToolCallArgsRaw: () => countToolCallArgsRaw,
74136
74196
  countCheckpointsBySource: () => countCheckpointsBySource,
74137
74197
  consolidatedTimeline: () => consolidatedTimeline,
74138
74198
  computeSnapshotHash: () => computeSnapshotHash,
@@ -78315,7 +78375,8 @@ class AgentService {
78315
78375
  cacheInfo = { hit: true, ageMs: serveCached.ageMs, path: DOCTOR_CACHE_REL };
78316
78376
  return this.renderDoctor(serveCached.results, executors, args.json, undefined, cacheInfo, args.enveloped, usage);
78317
78377
  }
78318
- const probeResults = await doctorRunner.runAll();
78378
+ const allDisabled = (executors?.length ?? 0) > 0 && enabledExecutors?.length === 0;
78379
+ const probeResults = allDisabled ? [] : await doctorRunner.runAll();
78319
78380
  const results2 = withDisabledRows(probeResults, executors);
78320
78381
  if (cacheOn && !args.probeHealth) {
78321
78382
  const writeErr = await writeDoctorCacheFile(fileSystem, fingerprint2, results2, now2);
@@ -81511,6 +81572,82 @@ var init_finding_codes2 = __esm(() => {
81511
81572
  init_src();
81512
81573
  });
81513
81574
 
81575
+ // ../../packages/app/src/services/feature-inventory.ts
81576
+ function parseInventory(report) {
81577
+ const lines = report.split(`
81578
+ `);
81579
+ const start = lines.findIndex((line) => /^#{1,6}\s*Requirement inventory\s*$/i.test(line));
81580
+ if (start === -1)
81581
+ return { owing: new Set, deferred: new Set, hasSection: false };
81582
+ const owing = new Set;
81583
+ const deferred = new Set;
81584
+ for (let i2 = start + 1;i2 < lines.length; i2++) {
81585
+ if (/^#{1,6}\s/.test(lines[i2] ?? ""))
81586
+ break;
81587
+ const match = INVENTORY_ITEM_RE.exec(lines[i2] ?? "");
81588
+ if (match === null)
81589
+ continue;
81590
+ const id = `I${match[1]}`;
81591
+ if (/\[\s*deferred\s*:/i.test(match[2] ?? ""))
81592
+ deferred.add(id);
81593
+ else
81594
+ owing.add(id);
81595
+ }
81596
+ return { owing, deferred, hasSection: true };
81597
+ }
81598
+ function parseCoverage(ac) {
81599
+ const covered = new Map;
81600
+ let inScenario = false;
81601
+ for (const line of ac.split(`
81602
+ `)) {
81603
+ if (SCENARIO_RE.test(line)) {
81604
+ inScenario = true;
81605
+ continue;
81606
+ }
81607
+ if (!inScenario)
81608
+ continue;
81609
+ const match = COVERS_RE.exec(line);
81610
+ if (match === null)
81611
+ continue;
81612
+ for (const raw of (match[1] ?? "").split(",")) {
81613
+ const id = raw.trim().toUpperCase();
81614
+ if (/^I\d+$/.test(id))
81615
+ covered.set(id, (covered.get(id) ?? 0) + 1);
81616
+ }
81617
+ }
81618
+ return covered;
81619
+ }
81620
+ function checkInventoryCoverage(reportText, acText) {
81621
+ const inventory = parseInventory(reportText);
81622
+ if (!inventory.hasSection || inventory.owing.size + inventory.deferred.size === 0) {
81623
+ return [
81624
+ {
81625
+ layer: "L3",
81626
+ code: FINDING_CODES.INVENTORY_COVERAGE,
81627
+ severity: "error",
81628
+ section: "Requirements",
81629
+ message: "inventory report has no `## Requirement inventory` items"
81630
+ }
81631
+ ];
81632
+ }
81633
+ const covered = parseCoverage(acText);
81634
+ const uncovered = [...inventory.owing].filter((id) => (covered.get(id) ?? 0) === 0).sort();
81635
+ return uncovered.map((id) => ({
81636
+ layer: "L3",
81637
+ code: FINDING_CODES.INVENTORY_COVERAGE,
81638
+ severity: "error",
81639
+ section: "Requirements",
81640
+ message: `Requirement ${id} is not covered by any AC scenario and is not [deferred:]`
81641
+ }));
81642
+ }
81643
+ var INVENTORY_ITEM_RE, COVERS_RE, SCENARIO_RE;
81644
+ var init_feature_inventory = __esm(() => {
81645
+ init_finding_codes2();
81646
+ INVENTORY_ITEM_RE = /^\s*[-*]\s+\**I(\d+)\**\s*[.:\u2014-]?\s+(.*)$/;
81647
+ COVERS_RE = /^\s*#\s*covers:\s*(.+)$/i;
81648
+ SCENARIO_RE = /^\s*Scenario(?:\s+Outline)?:/;
81649
+ });
81650
+
81514
81651
  // ../../packages/app/src/services/structural-repair.ts
81515
81652
  function canonicalOrder(domain2) {
81516
81653
  return domain2 === "task" ? TASK_CANONICAL_SECTIONS : FEATURE_CANONICAL_SECTIONS;
@@ -81834,6 +81971,16 @@ class PlanningCheckService {
81834
81971
  return doc2;
81835
81972
  }
81836
81973
  runL2(doc2, entry, findings, raw) {
81974
+ const fenceLine = doc2.unclosedFenceLine();
81975
+ if (fenceLine !== null) {
81976
+ findings.push({
81977
+ layer: "L2",
81978
+ code: FINDING_CODES.L2_UNCLOSED_CODE_FENCE,
81979
+ severity: "error",
81980
+ section: "",
81981
+ message: `Unclosed code fence at line ${fenceLine}: all later sections are hidden from this check. Close the fence to restore them.`
81982
+ });
81983
+ }
81837
81984
  if (!entry)
81838
81985
  return;
81839
81986
  const sectionNames = doc2.sectionNames;
@@ -81984,6 +82131,7 @@ var init_planning_check_base = __esm(() => {
81984
82131
  FINDING_CODES.L1_MARKDOWN_PARSE,
81985
82132
  FINDING_CODES.L1_SCHEMA_VALIDATION,
81986
82133
  FINDING_CODES.L2_MISSING_REQUIRED_SECTION,
82134
+ FINDING_CODES.L2_UNCLOSED_CODE_FENCE,
81987
82135
  FINDING_CODES.L3_AC_BDD_ERROR,
81988
82136
  FINDING_CODES.L3_AC_BDD_INVALID,
81989
82137
  FINDING_CODES.L3_REQUIREMENTS_EMPTY,
@@ -81991,7 +82139,10 @@ var init_planning_check_base = __esm(() => {
81991
82139
  FINDING_CODES.L3_REQUIRED_SECTION_PLACEHOLDER,
81992
82140
  ...REQUIRED_REFERENCE_CODES,
81993
82141
  FINDING_CODES.L4_PREREQUISITE_CYCLE,
81994
- ...COMPLETION_FINDING_CODES
82142
+ ...COMPLETION_FINDING_CODES,
82143
+ FINDING_CODES.PRECHECK_SIZE,
82144
+ FINDING_CODES.PRECHECK_EVIDENCE,
82145
+ FINDING_CODES.INVENTORY_COVERAGE
81995
82146
  ]);
81996
82147
  });
81997
82148
 
@@ -82049,20 +82200,21 @@ function prefixId(id) {
82049
82200
  const m = /^(?:AC|R)\d+/.exec(id);
82050
82201
  return m ? m[0] : id;
82051
82202
  }
82052
- function acRowProves(rowId, items) {
82053
- const key2 = normalizeKey2(rowId);
82203
+ function acRowProves(rowId, items, scenarioTitles) {
82204
+ const ordinal = /^AC-(\d+)$/.exec(rowId.trim());
82205
+ const key2 = normalizeKey2(ordinal ? scenarioTitles[Number(ordinal[1]) - 1] ?? "" : rowId);
82054
82206
  for (const item of items) {
82055
82207
  if (!item.requirementId?.startsWith("AC"))
82056
82208
  continue;
82057
82209
  const alias3 = normalizeKey2(item.text.replace(/\s*\(req:[^)]*\)\s*$/, ""));
82058
- if (alias3 !== "" && (alias3 === key2 || alias3.endsWith(`\u2014 ${key2}`)))
82210
+ if (alias3 !== "" && key2 !== "" && (alias3 === key2 || alias3.endsWith(`\u2014 ${key2}`)))
82059
82211
  return item.requirementId;
82060
82212
  }
82061
- if (/^R\d+\s*[\u2014-]\s*\S/.test(rowId.trim()))
82213
+ if (ordinal || /^R\d+\s*[\u2014-]\s*\S/.test(rowId.trim()))
82062
82214
  return;
82063
82215
  return prefixId(rowId);
82064
82216
  }
82065
- function flipVerifiedCheckboxes(body, verdict) {
82217
+ function flipVerifiedCheckboxes(body, verdict, scenarioTitles = []) {
82066
82218
  if (verdict.verdict === "FAIL" || verdict.verdict === "UNKNOWN")
82067
82219
  return body;
82068
82220
  const items = parseChecklist(body);
@@ -82070,13 +82222,16 @@ function flipVerifiedCheckboxes(body, verdict) {
82070
82222
  return body;
82071
82223
  const proven = new Set;
82072
82224
  for (const req of verdict.requirements) {
82073
- if (req.status === "MET")
82074
- proven.add(prefixId(req.id));
82225
+ if (req.status !== "MET")
82226
+ continue;
82227
+ const id = /^AC-\d+$/.test(req.id.trim()) ? acRowProves(req.id, items, scenarioTitles) : prefixId(req.id);
82228
+ if (id !== undefined)
82229
+ proven.add(id);
82075
82230
  }
82076
82231
  for (const ac of verdict.acceptanceCriteria ?? []) {
82077
82232
  if (ac.status !== "MET")
82078
82233
  continue;
82079
- const id = acRowProves(ac.id, items);
82234
+ const id = acRowProves(ac.id, items, scenarioTitles);
82080
82235
  if (id !== undefined)
82081
82236
  proven.add(id);
82082
82237
  }
@@ -82359,6 +82514,288 @@ var init_task_record = __esm(() => {
82359
82514
  };
82360
82515
  });
82361
82516
 
82517
+ // ../../packages/app/src/services/verify-answer-lint.ts
82518
+ function lintVerifyAnswer(answerText, taskContent, featureContent) {
82519
+ const findings = [];
82520
+ const add = (line, rule, message) => {
82521
+ if (findings.length < ANSWER_LINT_MAX_FINDINGS)
82522
+ findings.push({ line, rule, message });
82523
+ };
82524
+ const tables = parseAnswer(answerText);
82525
+ if (tables.verdict === null) {
82526
+ add(0, "verdict-missing", "no `Verdict:` line (expected exactly one `Verdict: PASS|PARTIAL|FAIL` line)");
82527
+ } else if (!/^(PASS|PARTIAL|FAIL)$/i.test(tables.verdict.value)) {
82528
+ add(tables.verdict.line, "verdict-value", `invalid Verdict value "${tables.verdict.value}" (PASS | PARTIAL | FAIL)`);
82529
+ }
82530
+ const reqIds = extractRequirementIds(taskContent);
82531
+ const acIndex = buildAcIdentityIndex(taskContent, featureContent ?? null);
82532
+ const seenReq = new Set;
82533
+ for (const row of tables.reqs) {
82534
+ if (!reqIds.includes(row.id))
82535
+ add(row.line, "req-unknown", `unknown requirement ID "${row.id}" (task declares: ${reqIds.join(", ") || "none"})`);
82536
+ else if (seenReq.has(row.id))
82537
+ add(row.line, "req-duplicate", `duplicate requirement row "${row.id}"`);
82538
+ seenReq.add(row.id);
82539
+ if (normalizeReqStatus(row.status) === null)
82540
+ add(row.line, "req-status", `"${row.id}" invalid status "${row.status}" (MET | PARTIAL | UNMET)`);
82541
+ if (!row.evidence.trim())
82542
+ add(row.line, "req-evidence", `"${row.id}" has empty evidence`);
82543
+ }
82544
+ for (const id of reqIds) {
82545
+ if (!seenReq.has(id))
82546
+ add(0, "req-missing", `missing requirement row for "${id}"`);
82547
+ }
82548
+ const seenAc = new Map;
82549
+ for (const row of tables.acs) {
82550
+ const resolution = resolveAcIdentity(row.id, acIndex);
82551
+ const canonicalKey = resolution.ok ? normalizeAcTitle(resolution.canonical) : null;
82552
+ if (!resolution.ok) {
82553
+ if (resolution.error !== "")
82554
+ add(row.line, "ac-ordinal", resolution.error);
82555
+ else
82556
+ add(row.line, "ac-identity", `AC ID "${row.id.slice(0, 60)}" matches no task AC checklist label or scenario title ` + "(accepted forms: exact title, bare title, a criterion bullet's bold head or full bold span, " + "`Scenario:` prefix, bracket tags, declared AC-N alias)");
82557
+ } else if (canonicalKey !== null && seenAc.has(canonicalKey)) {
82558
+ const first = seenAc.get(canonicalKey) ?? "";
82559
+ add(row.line, "ac-duplicate", `duplicate AC row "${row.id.slice(0, 60)}" \u2014 alias-equivalent to "${first.slice(0, 60)}"`);
82560
+ }
82561
+ if (canonicalKey !== null && !seenAc.has(canonicalKey))
82562
+ seenAc.set(canonicalKey, row.id);
82563
+ if (normalizeAcStatus(row.status) === null)
82564
+ add(row.line, "ac-status", `invalid AC status "${row.status}" (MET | PARTIAL | UNMET | N/A)`);
82565
+ if (normalizeEvidenceType(row.evidenceType) === null)
82566
+ add(row.line, "ac-evidence-type", `invalid evidence type "${row.evidenceType}" (test | command | static-ref | manual-review | llm-judge | n/a, or a + compound)`);
82567
+ if (!row.evidence.trim())
82568
+ add(row.line, "ac-evidence", `AC "${row.id.slice(0, 40)}" has empty evidence`);
82569
+ }
82570
+ return findings;
82571
+ }
82572
+ function splitTableCells(line) {
82573
+ return line.split(/(?<!\\)\|/).map((c3) => c3.replace(/\\\|/g, "|").trim()).filter(Boolean);
82574
+ }
82575
+ function normalizeReqStatus(raw) {
82576
+ if (/\bMET\b/.test(raw))
82577
+ return "MET";
82578
+ if (/\bPARTIAL\b/.test(raw))
82579
+ return "PARTIAL";
82580
+ if (/\bUNMET\b/.test(raw))
82581
+ return "UNMET";
82582
+ return null;
82583
+ }
82584
+ function normalizeAcStatus(raw) {
82585
+ if (/\bMET\b/.test(raw))
82586
+ return "MET";
82587
+ if (/\bPARTIAL\b/.test(raw))
82588
+ return "PARTIAL";
82589
+ if (/\bUNMET\b/.test(raw))
82590
+ return "UNMET";
82591
+ if (/\bN\/A\b/.test(raw) || /\bNA\b/.test(raw))
82592
+ return "N/A";
82593
+ return null;
82594
+ }
82595
+ function normalizeEvidenceTypeToken(normalized) {
82596
+ if (normalized === "test")
82597
+ return "test";
82598
+ if (normalized === "command")
82599
+ return "command";
82600
+ if (normalized === "static-ref" || normalized === "static" || normalized === "doc" || normalized === "docs" || normalized === "documentation") {
82601
+ return "static-ref";
82602
+ }
82603
+ if (normalized === "manual-review" || normalized === "manual")
82604
+ return "manual-review";
82605
+ if (normalized === "llm-judge" || normalized === "judge")
82606
+ return "llm-judge";
82607
+ if (normalized === "n/a" || normalized === "na")
82608
+ return "n/a";
82609
+ return null;
82610
+ }
82611
+ function normalizeEvidenceType(raw) {
82612
+ const normalized = raw.toLowerCase().trim();
82613
+ const single = normalizeEvidenceTypeToken(normalized);
82614
+ if (single !== null)
82615
+ return single;
82616
+ const parts = normalized.split(/[+,/]/).filter((p) => p.trim());
82617
+ const tokens = parts.map((part) => normalizeEvidenceTypeToken(part.trim())).filter((t) => t !== null);
82618
+ if (tokens.length < 2 || tokens.length !== parts.length)
82619
+ return null;
82620
+ return EVIDENCE_TYPE_PRECEDENCE.find((candidate) => tokens.includes(candidate)) ?? null;
82621
+ }
82622
+ function parseAnswer(text4) {
82623
+ const out = { verdict: null, reqs: [], acs: [] };
82624
+ const lines = text4.split(`
82625
+ `);
82626
+ let reqTable = false;
82627
+ let acTable = false;
82628
+ for (let i2 = 0;i2 < lines.length; i2++) {
82629
+ const trimmed = lines[i2]?.trim() ?? "";
82630
+ const lineNo = i2 + 1;
82631
+ if ((reqTable || acTable) && /^#{1,6}\s/.test(trimmed)) {
82632
+ reqTable = false;
82633
+ acTable = false;
82634
+ continue;
82635
+ }
82636
+ if (!trimmed.startsWith("|"))
82637
+ continue;
82638
+ const cells = splitTableCells(trimmed);
82639
+ if (/^[-:]+$/.test(cells[0] ?? ""))
82640
+ continue;
82641
+ const h0 = (cells[0] ?? "").toLowerCase();
82642
+ const h1 = (cells[1] ?? "").toLowerCase();
82643
+ if (!reqTable && !acTable && cells.length >= 2) {
82644
+ const idLike = h0.includes("req") || h0 === "requirement" || h0 === "r#" || h0 === "r" || /^r\d+$/.test(h0);
82645
+ if (idLike && (h1.includes("status") || h1 === "verdict")) {
82646
+ reqTable = true;
82647
+ continue;
82648
+ }
82649
+ if ((h0 === "ac" || h0.includes("acceptance")) && h1.includes("status") && (cells[2] ?? "").toLowerCase().includes("evidence")) {
82650
+ acTable = true;
82651
+ continue;
82652
+ }
82653
+ }
82654
+ if (reqTable && cells.length >= 2) {
82655
+ if ((h0 === "ac" || h0.includes("acceptance")) && h1.includes("status")) {
82656
+ reqTable = false;
82657
+ acTable = true;
82658
+ continue;
82659
+ }
82660
+ out.reqs.push({
82661
+ id: cells[0] ?? "",
82662
+ status: (cells[1] ?? "").toUpperCase(),
82663
+ evidence: cells[2] ?? "",
82664
+ line: lineNo
82665
+ });
82666
+ continue;
82667
+ }
82668
+ if (acTable && cells.length >= 3) {
82669
+ out.acs.push({
82670
+ id: cells[0] ?? "",
82671
+ status: cells[1] ?? "",
82672
+ evidenceType: cells[2] ?? "",
82673
+ evidence: cells[3] ?? "",
82674
+ line: lineNo
82675
+ });
82676
+ }
82677
+ }
82678
+ const verdictMatches = [...text4.matchAll(/^\s*Verdict:\s*(\S+)\s*$/gim)];
82679
+ if (verdictMatches.length === 1) {
82680
+ out.verdict = {
82681
+ value: verdictMatches[0]?.[1] ?? "",
82682
+ line: text4.slice(0, verdictMatches[0]?.index ?? 0).split(`
82683
+ `).length
82684
+ };
82685
+ }
82686
+ return out;
82687
+ }
82688
+ function sectionBetween(text4, heading) {
82689
+ const marker = new RegExp(`^#{1,6}\\s+${heading}\\s*$`, "im");
82690
+ const match = marker.exec(text4);
82691
+ if (!match || match.index === undefined)
82692
+ return "";
82693
+ const rest = text4.slice(match.index);
82694
+ const lineEnd = rest.indexOf(`
82695
+ `);
82696
+ const afterHeading = lineEnd === -1 ? "" : rest.slice(lineEnd + 1);
82697
+ const next = afterHeading.search(/^#{1,6}\s/m);
82698
+ return next === -1 ? afterHeading : afterHeading.slice(0, next);
82699
+ }
82700
+ function extractRequirementIds(taskContent) {
82701
+ const section = sectionBetween(taskContent, "Requirements");
82702
+ const ids = new Set;
82703
+ for (const m of section.matchAll(/\*\*(R\d+(?:\.\d+)*)\b/g))
82704
+ ids.add(m[1] ?? "");
82705
+ for (const m of section.matchAll(/^(?:[-*]\s+(?:\[[ xX]\]\s+)?|\[[ xX]\]\s+)(R\d+(?:\.\d+)*)/gm))
82706
+ ids.add(m[1] ?? "");
82707
+ for (const m of section.matchAll(/^(R\d+(?:\.\d+)*)\s*[.:]/gm))
82708
+ ids.add(m[1] ?? "");
82709
+ return [...ids];
82710
+ }
82711
+ function stripAcWrappers(title) {
82712
+ let out = title.trim();
82713
+ let prev;
82714
+ do {
82715
+ prev = out;
82716
+ out = out.replace(/^\[[^\]]*\]\s*/, "").replace(/\s*\[[^\]]*\]\s*$/, "").replace(/^Scenario:\s*/i, "").trim();
82717
+ } while (out !== prev);
82718
+ return out;
82719
+ }
82720
+ function normalizeAcTitle(title) {
82721
+ return stripAcWrappers(title).replace(/^R\d+\s*[:\-\u2014]?\s*/, "").toLowerCase().replace(/[\u0027\u2018\u2019\u201c\u201d]/g, "").replace(/\s+/g, " ").replace(/\s*\.+$/, "").trim();
82722
+ }
82723
+ function buildAcIdentityIndex(taskContent, featureContent) {
82724
+ const byTitle = new Map;
82725
+ const declareIdentity = (spelling) => {
82726
+ const key2 = normalizeAcTitle(spelling);
82727
+ if (key2 !== "" && !byTitle.has(key2))
82728
+ byTitle.set(key2, spelling);
82729
+ };
82730
+ const section = sectionBetween(taskContent, "Acceptance Criteria");
82731
+ for (const m of section.matchAll(/^[-*]\s+(?:\[[ xX]\]\s+)?(.+?)\s*(?::|$)/gm)) {
82732
+ const label = (m[1] ?? "").trim();
82733
+ if (!label)
82734
+ continue;
82735
+ declareIdentity(label);
82736
+ const leading = label.split(/\s+/)[0] ?? "";
82737
+ if (leading && leading !== label)
82738
+ declareIdentity(leading);
82739
+ }
82740
+ for (const m of section.matchAll(/^[-*]\s+(?:\[[ xX]\]\s+)?\*\*(.+?)\*\*/gm)) {
82741
+ const inner = (m[1] ?? "").trim();
82742
+ if (!inner)
82743
+ continue;
82744
+ declareIdentity(inner);
82745
+ const head = inner.split(/\s+[\u2014\u2013]\s+|:/)[0]?.trim() ?? "";
82746
+ if (head && head !== inner)
82747
+ declareIdentity(head);
82748
+ }
82749
+ for (const m of section.matchAll(/^\*\*(.+?)\*\*\s*$/gm)) {
82750
+ const inner = (m[1] ?? "").trim();
82751
+ if (!inner)
82752
+ continue;
82753
+ declareIdentity(inner);
82754
+ const head = inner.split(":")[0]?.trim() ?? "";
82755
+ if (head && head !== inner)
82756
+ declareIdentity(head);
82757
+ }
82758
+ const scenarioTitles = (content) => [...content.matchAll(/^[ \t]*Scenario:\s*(.+)\s*$/gm)].map((m) => (m[1] ?? "").trim()).filter((t) => t !== "");
82759
+ const taskScenarios = scenarioTitles(sectionBetween(taskContent, "Acceptance Criteria"));
82760
+ const featureScenarios = featureContent !== null ? scenarioTitles(featureContent) : [];
82761
+ for (const title of [...taskScenarios, ...featureScenarios])
82762
+ declareIdentity(title);
82763
+ return { byTitle, taskScenarios, featureScenarios };
82764
+ }
82765
+ function resolveAcIdentity(rowId, index2) {
82766
+ const canonical = index2.byTitle.get(normalizeAcTitle(rowId));
82767
+ if (canonical !== undefined)
82768
+ return { ok: true, canonical };
82769
+ const stripped = stripAcWrappers(rowId);
82770
+ const ordinal = /^AC-(\d+)$/i.exec(stripped);
82771
+ if (ordinal !== null) {
82772
+ const n2 = Number(ordinal[1]);
82773
+ const taskTitle = index2.taskScenarios[n2 - 1];
82774
+ const featureTitle = index2.featureScenarios[n2 - 1];
82775
+ const candidates = [...new Set([taskTitle, featureTitle].filter((t) => t !== undefined))];
82776
+ if (candidates.length === 0) {
82777
+ return {
82778
+ ok: false,
82779
+ error: `AC id "${rowId}" uses the AC-${n2} positional alias but no scenario exists at that ordinal ` + "(task scenario list and linked-feature scenario list) \u2014 cite the exact scenario title, checklist label, or a bare `**AC id**` paragraph"
82780
+ };
82781
+ }
82782
+ if (candidates.length > 1) {
82783
+ return {
82784
+ ok: false,
82785
+ error: `AC id "${rowId}" is ambiguous: task AC #${n2} ("${taskTitle}") and feature scenario #${n2} ` + `("${featureTitle}") are different scenarios with different ordering \u2014 cite the exact title`
82786
+ };
82787
+ }
82788
+ const resolved = index2.byTitle.get(normalizeAcTitle(candidates[0] ?? ""));
82789
+ if (resolved !== undefined)
82790
+ return { ok: true, canonical: resolved };
82791
+ }
82792
+ return { ok: false, error: "" };
82793
+ }
82794
+ var ANSWER_LINT_MAX_FINDINGS = 10, EVIDENCE_TYPE_PRECEDENCE;
82795
+ var init_verify_answer_lint = __esm(() => {
82796
+ EVIDENCE_TYPE_PRECEDENCE = ["test", "command", "static-ref", "manual-review", "llm-judge", "n/a"];
82797
+ });
82798
+
82362
82799
  // ../../packages/app/src/services/feature-check.ts
82363
82800
  var exports_feature_check = {};
82364
82801
  __export(exports_feature_check, {
@@ -82366,9 +82803,13 @@ __export(exports_feature_check, {
82366
82803
  verdictRowsMatchScenarios: () => verdictRowsMatchScenarios,
82367
82804
  taskCoversAnyFeatureScenario: () => taskCoversAnyFeatureScenario,
82368
82805
  summarizeRowIds: () => summarizeRowIds,
82806
+ readFeatureBody: () => readFeatureBody,
82369
82807
  readFeatureAcBody: () => readFeatureAcBody,
82370
82808
  matchedScenarioKeys: () => matchedScenarioKeys,
82809
+ lintVerifyAnswerForTask: () => lintVerifyAnswerForTask,
82810
+ isFeatureFile: () => isFeatureFile,
82371
82811
  findOtherP0InStatus: () => findOtherP0InStatus,
82812
+ featureScenarioTitles: () => featureScenarioTitles,
82372
82813
  defaultVerdictRunDir: () => defaultVerdictRunDir,
82373
82814
  VERDICT_SCENARIO_KEY_FORMS: () => VERDICT_SCENARIO_KEY_FORMS,
82374
82815
  FeatureCheckService: () => FeatureCheckService,
@@ -82524,6 +82965,9 @@ function matchedScenarioKeys(rows, ac) {
82524
82965
  const met = rows.filter((r) => r.status === "MET");
82525
82966
  return scenarios.filter((sc) => met.some((r) => rowMatchesScenario(r.id, sc))).map((sc) => sc.title);
82526
82967
  }
82968
+ function featureScenarioTitles(ac) {
82969
+ return indexScenarioAliases(ac).map((sc) => sc.title);
82970
+ }
82527
82971
  function indexScenarioAliases(ac) {
82528
82972
  const keys = [];
82529
82973
  let inDocString = false;
@@ -82601,28 +83045,63 @@ function taskCoversAnyFeatureScenario(taskAc, featureAc) {
82601
83045
  return false;
82602
83046
  return scenarioKeys(parsed.scenarios).some((sc) => taskCoversScenario(taskAc, sc));
82603
83047
  }
82604
- async function readFeatureAcBody(fs3, featuresDir, featureId2) {
83048
+ function isFeatureFile(name, id) {
83049
+ return name.startsWith(`${id}_`) && name.endsWith(".md") && name.length > id.length + 4;
83050
+ }
83051
+ async function readFeatureBody(fs3, featuresDir, featureId2) {
82605
83052
  try {
82606
83053
  for (const name of await fs3.readDir(featuresDir)) {
82607
- if (!name.startsWith(`${featureId2}_`) || !name.endsWith(".md"))
83054
+ if (!isFeatureFile(name, featureId2))
82608
83055
  continue;
82609
- const doc2 = MarkdownDocument.parse(await fs3.readFile(`${featuresDir}/${name}`), "feature");
82610
- const ac = stripAcFence(doc2.getSection("Acceptance Criteria") ?? "");
82611
- return ac.trim().length > 0 ? ac : null;
83056
+ return await fs3.readFile(`${featuresDir}/${name}`);
82612
83057
  }
82613
83058
  } catch {}
82614
83059
  return null;
82615
83060
  }
83061
+ async function readFeatureAcBody(fs3, featuresDir, featureId2) {
83062
+ const raw = await readFeatureBody(fs3, featuresDir, featureId2);
83063
+ if (raw === null)
83064
+ return null;
83065
+ try {
83066
+ const doc2 = MarkdownDocument.parse(raw, "feature");
83067
+ const ac = stripAcFence(doc2.getSection("Acceptance Criteria") ?? "");
83068
+ return ac.trim().length > 0 ? ac : null;
83069
+ } catch {
83070
+ return null;
83071
+ }
83072
+ }
83073
+ async function lintVerifyAnswerForTask(wbs, answerText, deps) {
83074
+ let taskContent;
83075
+ let featureId2 = "";
83076
+ try {
83077
+ const hit = await new TaskLocator({
83078
+ fs: deps.fs,
83079
+ tasksDir: deps.tasksDir,
83080
+ ...deps.foldersConfig !== undefined ? { foldersConfig: deps.foldersConfig } : {}
83081
+ }).findByWbs(wbs);
83082
+ if (hit === null)
83083
+ return [];
83084
+ taskContent = await deps.fs.readFile(hit.filePath);
83085
+ const tfm = MarkdownDocument.parse(taskContent, "task").frontmatterData ?? {};
83086
+ featureId2 = tfm.feature_id ?? tfm["feature-id"] ?? "";
83087
+ } catch {
83088
+ return [];
83089
+ }
83090
+ const featureContent = featureId2 !== "" && deps.featuresDir !== undefined ? await readFeatureBody(deps.fs, deps.featuresDir, featureId2) : null;
83091
+ return lintVerifyAnswer(answerText, taskContent, featureContent);
83092
+ }
82616
83093
  var DEFAULT_FEATURE_MATRIX, FeatureCheckService, EMBEDDED_SCENARIO_REF_RES, VERDICT_SCENARIO_KEY_FORMS = "scenario title, AC-N alias, (feature R<n>), (covers: R<n>), or [R<n>]";
82617
83094
  var init_feature_check = __esm(() => {
82618
83095
  init_src2();
82619
83096
  init_feature_verification_receipt();
82620
83097
  init_workflow_resolver();
82621
83098
  init_done_transition_guard();
83099
+ init_feature_inventory();
82622
83100
  init_planning_check_base();
82623
83101
  init_structural_repair();
82624
83102
  init_task_locator();
82625
83103
  init_task_record();
83104
+ init_verify_answer_lint();
82626
83105
  init_verify_verdict();
82627
83106
  DEFAULT_FEATURE_MATRIX = {
82628
83107
  variants: {
@@ -82737,6 +83216,10 @@ var init_feature_check = __esm(() => {
82737
83216
  const runDir = options?.runDir ?? (primaryTasksDir ? defaultVerdictRunDir(primaryTasksDir) : undefined);
82738
83217
  const taskScanDirs = options?.tasksDirs && options.tasksDirs.length > 0 ? options.tasksDirs : options?.tasksDir ? [options.tasksDir] : [];
82739
83218
  await this.runL4(doc2, featureId2, effectiveStatus, taskScanDirs, dogfoodDir, runDir, findings);
83219
+ if (typeof options?.inventory === "string") {
83220
+ const acText = doc2.getSection("Acceptance Criteria") ?? "";
83221
+ findings.push(...checkInventoryCoverage(options.inventory, acText));
83222
+ }
82740
83223
  return {
82741
83224
  id: featureId2,
82742
83225
  ...this.summarizeWithStatus(effectiveStatus, findings, strict, options?.severityOverrides),
@@ -83226,6 +83709,111 @@ var init_feature_check = __esm(() => {
83226
83709
  EMBEDDED_SCENARIO_REF_RES = [/\((?:feature|covers:)[ \t]*(R\d+)\b[^)]*\)/gi, /\[(R\d+)\]/gi];
83227
83710
  });
83228
83711
 
83712
+ // ../../packages/app/src/services/task-evidence-precheck.ts
83713
+ function parseEvidenceChannels(content) {
83714
+ const channels = [];
83715
+ for (const match of content.matchAll(/evidence-channel:\s*(\S+)/g)) {
83716
+ channels.push(match[1] ?? "");
83717
+ }
83718
+ return channels;
83719
+ }
83720
+ async function evaluateTaskEvidence(content, deps) {
83721
+ const declarations = parseEvidenceChannels(content);
83722
+ const unknown2 = declarations.filter((d) => !ALLOWED_EVIDENCE_CHANNELS.includes(d));
83723
+ if (unknown2.length > 0) {
83724
+ return {
83725
+ ok: false,
83726
+ reasons: [
83727
+ `unknown evidence-channel declaration(s): ${unknown2.join(", ")}`,
83728
+ `allowlisted declaration: evidence-channel: ${ALLOWED_EVIDENCE_CHANNELS[0] ?? ""}`
83729
+ ]
83730
+ };
83731
+ }
83732
+ if (declarations.length === 0) {
83733
+ return { ok: true, reasons: [] };
83734
+ }
83735
+ const channel = declarations[0] ?? "";
83736
+ const source = /\[([a-z0-9-]+)\]$/.exec(channel)?.[1] ?? "";
83737
+ let count3;
83738
+ try {
83739
+ count3 = await deps.countArgsRaw(source);
83740
+ } catch {
83741
+ count3 = null;
83742
+ }
83743
+ if (count3 === null) {
83744
+ return {
83745
+ ok: false,
83746
+ reasons: [
83747
+ `local spur database or history_tool_call table missing/unreadable for source '${source}' \u2014 run a real history import first`
83748
+ ]
83749
+ };
83750
+ }
83751
+ if (!(count3 > 0)) {
83752
+ return {
83753
+ ok: false,
83754
+ reasons: [
83755
+ `0 live ${source} rows with args_raw \u2014 run a non-dry-run ${source} history import with a safe importer before implementing`
83756
+ ]
83757
+ };
83758
+ }
83759
+ return { ok: true, reasons: [] };
83760
+ }
83761
+ var ALLOWED_EVIDENCE_CHANNELS;
83762
+ var init_task_evidence_precheck = __esm(() => {
83763
+ init_src2();
83764
+ ALLOWED_EVIDENCE_CHANNELS = ["history_tool_call.args_raw[pi]"];
83765
+ });
83766
+
83767
+ // ../../packages/app/src/services/task-size-precheck.ts
83768
+ function sectionBody(content, headingPattern) {
83769
+ const heading = content.match(new RegExp(headingPattern, "m"));
83770
+ if (!heading)
83771
+ return null;
83772
+ const rest = content.slice((heading.index ?? 0) + heading[0].length);
83773
+ const nextHeading = rest.match(/^#{2,3}\s+/m);
83774
+ return nextHeading ? rest.slice(0, nextHeading.index ?? 0) : rest;
83775
+ }
83776
+ function countRItems(content) {
83777
+ const body = sectionBody(content, "^#{2,3}\\s+Requirements\\s*$") ?? content;
83778
+ const numbers = new Set;
83779
+ for (const match of body.matchAll(new RegExp(R_ITEM_RE_SOURCE, "gm"))) {
83780
+ numbers.add(match[1] ?? "");
83781
+ }
83782
+ return numbers.size;
83783
+ }
83784
+ function countPlanItems(content) {
83785
+ const body = sectionBody(content, "^#{2,3}\\s+Plan\\s*$");
83786
+ if (body === null)
83787
+ return 0;
83788
+ const numbered = body.match(new RegExp(NUMBERED_PLAN_ITEM_RE_SOURCE, "gm"))?.length ?? 0;
83789
+ const checklist = body.match(new RegExp(CHECKLIST_ITEM_RE_SOURCE, "gm"))?.length ?? 0;
83790
+ return numbered + checklist;
83791
+ }
83792
+ function evaluateTaskSize(content, limits = DEFAULT_TASK_SIZE_LIMITS) {
83793
+ const reqCount = countRItems(content);
83794
+ const planItemCount = countPlanItems(content);
83795
+ const reasons = [];
83796
+ if (reqCount > limits.maxReqs) {
83797
+ reasons.push(`Task has ${reqCount} R-items (max ${limits.maxReqs}). Consider decomposing into smaller tasks.`);
83798
+ }
83799
+ if (planItemCount > limits.maxPlanItems) {
83800
+ reasons.push(`Task has ${planItemCount} Plan items (max ${limits.maxPlanItems}). Consider simplifying the plan.`);
83801
+ }
83802
+ return {
83803
+ reqCount,
83804
+ planItemCount,
83805
+ ok: reasons.length === 0,
83806
+ reasons
83807
+ };
83808
+ }
83809
+ var DEFAULT_TASK_SIZE_LIMITS, R_ITEM_RE_SOURCE = "^\\s*[-*]\\s+(?:\\[[ xX]\\]\\s*)?[*_]{0,2}R(\\d+)\\.?[*_]{0,2}(?:\\s|$)", NUMBERED_PLAN_ITEM_RE_SOURCE = "^\\d+\\.\\s", CHECKLIST_ITEM_RE_SOURCE = "^\\s*[-*]\\s+\\[[ xX]\\]";
83810
+ var init_task_size_precheck = __esm(() => {
83811
+ DEFAULT_TASK_SIZE_LIMITS = {
83812
+ maxReqs: 10,
83813
+ maxPlanItems: 16
83814
+ };
83815
+ });
83816
+
83229
83817
  // ../../packages/app/src/services/task-check.ts
83230
83818
  import { basename as basename8, dirname as dirname14, join as join22 } from "path";
83231
83819
  function isPlaceholderBody(body) {
@@ -83430,9 +84018,12 @@ function hasSolutionFileLineCitation(body) {
83430
84018
  var TESTING_VERDICT_STUB_RE, EXTERNAL_EVIDENCE_RE, GATE_LANGUAGE_SECTIONS, TaskCheckService;
83431
84019
  var init_task_check = __esm(() => {
83432
84020
  init_src2();
84021
+ init_feature_check();
83433
84022
  init_planning_check_base();
83434
84023
  init_structural_repair();
84024
+ init_task_evidence_precheck();
83435
84025
  init_task_locator();
84026
+ init_task_size_precheck();
83436
84027
  init_verify_verdict();
83437
84028
  TESTING_VERDICT_STUB_RE = /^\|\s*[\u2014\u2013-]+\s*\|\s*[\u2014\u2013-]+\s*\|\s*No requirements recorded; verify verdict\s+\S+\s*\|/m;
83438
84029
  EXTERNAL_EVIDENCE_RE = /`([^`\n]+?)`\s+(?:line|lines?)\s+(\d+)(?:-(\d+))?/g;
@@ -83497,6 +84088,29 @@ var init_task_check = __esm(() => {
83497
84088
  if (effectiveStatus !== "done" && effectiveStatus !== "cancelled") {
83498
84089
  await this.runL4Readiness(doc2, fm, wbs, effectiveStatus, findings, tasksDir);
83499
84090
  }
84091
+ if (options?.precheck === true) {
84092
+ const size = evaluateTaskSize(doc2.bodyWithoutFrontmatter);
84093
+ for (const reason of size.reasons) {
84094
+ findings.push({
84095
+ layer: "L4",
84096
+ code: FINDING_CODES.PRECHECK_SIZE,
84097
+ severity: "error",
84098
+ section: "",
84099
+ message: reason
84100
+ });
84101
+ }
84102
+ const deps = { countArgsRaw: options.countArgsRaw ?? (async () => null) };
84103
+ const evidence = await evaluateTaskEvidence(doc2.bodyWithoutFrontmatter, deps);
84104
+ for (const reason of evidence.reasons) {
84105
+ findings.push({
84106
+ layer: "L4",
84107
+ code: FINDING_CODES.PRECHECK_EVIDENCE,
84108
+ severity: "error",
84109
+ section: "",
84110
+ message: reason
84111
+ });
84112
+ }
84113
+ }
83500
84114
  return {
83501
84115
  wbs,
83502
84116
  ...this.summarizeWithStatus(effectiveStatus, findings, strict, options?.severityOverrides, options?.accepted, wbs, entry?.required),
@@ -84192,7 +84806,7 @@ var init_task_check = __esm(() => {
84192
84806
  try {
84193
84807
  const entries = await this.fs.readDir(featuresDir);
84194
84808
  for (const name of entries) {
84195
- if (name.startsWith(`${id}_`) && name.endsWith(".md")) {
84809
+ if (isFeatureFile(name, id)) {
84196
84810
  return `${featuresDir}/${name}`;
84197
84811
  }
84198
84812
  }
@@ -87183,7 +87797,60 @@ var init_execution_policy = __esm(() => {
87183
87797
  init_bounded_child_run();
87184
87798
  });
87185
87799
 
87800
+ // ../../packages/app/src/services/feature-sync-suppression.ts
87801
+ import { createHash as createHash9 } from "crypto";
87802
+ function computeSyncFingerprint(input) {
87803
+ const material = [
87804
+ input.featureContentHash,
87805
+ ...[...input.taskStatusVector].sort(),
87806
+ ...[...input.verdictMtimeVector].sort()
87807
+ ].join(`
87808
+ `);
87809
+ return createHash9("sha256").update(material).digest("hex").slice(0, 32);
87810
+ }
87811
+ function serializeBlockedState(state) {
87812
+ return `${JSON.stringify(state)}
87813
+ `;
87814
+ }
87815
+ function parseBlockedState(raw) {
87816
+ const trimmed = raw.trim();
87817
+ if (trimmed.length === 0)
87818
+ return null;
87819
+ try {
87820
+ const parsed = JSON.parse(trimmed);
87821
+ if (typeof parsed.featureId !== "string" || typeof parsed.inputFingerprint !== "string" || typeof parsed.proposal !== "object" || parsed.proposal === null) {
87822
+ return null;
87823
+ }
87824
+ return parsed;
87825
+ } catch {
87826
+ return null;
87827
+ }
87828
+ }
87829
+ async function readVerdictMtimeVector(fs3, runDir) {
87830
+ const dir = runDir.replace(/\/$/, "");
87831
+ let entries;
87832
+ try {
87833
+ entries = await fs3.readDir(dir);
87834
+ } catch {
87835
+ return [];
87836
+ }
87837
+ const vector2 = [];
87838
+ for (const entry of entries) {
87839
+ if (!entry.endsWith("-verdict.json"))
87840
+ continue;
87841
+ try {
87842
+ const stat2 = await fs3.stat(`${dir}/${entry}`);
87843
+ if (stat2)
87844
+ vector2.push(`${entry.replace("-verdict.json", "")}:${stat2.mtimeMs}`);
87845
+ } catch {}
87846
+ }
87847
+ return vector2.sort();
87848
+ }
87849
+ var blockedStateFile = (featureId2, runDir) => `${runDir.replace(/\/$/, "")}/feature-sync-blocked-${featureId2}.json`;
87850
+ var init_feature_sync_suppression = () => {};
87851
+
87186
87852
  // ../../packages/app/src/services/feature-service.ts
87853
+ import { createHash as createHash10 } from "crypto";
87187
87854
  function isFeatureActionName(action) {
87188
87855
  return FEATURE_ACTION_NAMES.includes(action);
87189
87856
  }
@@ -87327,16 +87994,30 @@ class FeatureService {
87327
87994
  const tasksByFeature = await this.collectTasksByFeature();
87328
87995
  const targets = options?.featureId ? features.filter((f) => f.id === options.featureId) : features;
87329
87996
  let tasksUpdated = 0;
87997
+ const skipped = [];
87330
87998
  for (const feature of targets) {
87331
87999
  const rows = tasksByFeature.get(feature.id) ?? [];
87332
88000
  const table3 = renderTasksTable(rows);
87333
88001
  const raw = await this.ctx.fs.readFile(feature.filePath);
87334
88002
  const doc2 = MarkdownDocument.parse(raw, "feature");
87335
- if (!doc2.hasSection("Tasks"))
88003
+ if (doc2.unclosedFenceLine() !== null) {
88004
+ skipped.push({ id: feature.id, reason: "unclosed-code-fence" });
87336
88005
  continue;
88006
+ }
88007
+ if (doc2.duplicateSectionNames.length > 0) {
88008
+ skipped.push({ id: feature.id, reason: "duplicate-sections" });
88009
+ continue;
88010
+ }
88011
+ if (!doc2.hasSection("Tasks")) {
88012
+ skipped.push({ id: feature.id, reason: "missing-tasks-section" });
88013
+ continue;
88014
+ }
87337
88015
  try {
87338
88016
  doc2.replaceMarkerRegion("Tasks", table3);
87339
- } catch {
88017
+ } catch (err) {
88018
+ if (!(err instanceof Error && err.message.startsWith("No marker region found")))
88019
+ throw err;
88020
+ skipped.push({ id: feature.id, reason: "no-tasks-marker-region" });
87340
88021
  continue;
87341
88022
  }
87342
88023
  const next = doc2.serialize();
@@ -87345,7 +88026,7 @@ class FeatureService {
87345
88026
  await atomicWriteAsync(feature.filePath, next, feature.id, this.ctx.fs, this.ctx.projectName ?? "spur");
87346
88027
  tasksUpdated += 1;
87347
88028
  }
87348
- return { index: index2, tasksUpdated };
88029
+ return { index: index2, tasksUpdated, skipped };
87349
88030
  }
87350
88031
  async deriveFeatureStatus(featureId2) {
87351
88032
  const feature = await this.show(featureId2);
@@ -87465,6 +88146,18 @@ class FeatureService {
87465
88146
  };
87466
88147
  }
87467
88148
  async syncFeature(featureId2, options) {
88149
+ const suppressible = options?.force !== true && options?.dryRun !== true && options?.forceConfirm !== true;
88150
+ if (suppressible) {
88151
+ const replay = await this.replaySuppressedBlocked(featureId2);
88152
+ if (replay)
88153
+ return { ...replay, suppressed: true };
88154
+ }
88155
+ const result = await this.runSyncFeature(featureId2, options);
88156
+ if (options?.dryRun !== true)
88157
+ await this.recordBlockedSyncState(featureId2, result);
88158
+ return result;
88159
+ }
88160
+ async runSyncFeature(featureId2, options) {
87468
88161
  const proposal = await this.deriveFeatureStatus(featureId2);
87469
88162
  if (proposal.from === proposal.to) {
87470
88163
  return { proposal, applied: false, appliedHops: [] };
@@ -87506,6 +88199,60 @@ class FeatureService {
87506
88199
  }
87507
88200
  return { proposal, applied: true, appliedHops };
87508
88201
  }
88202
+ async replaySuppressedBlocked(featureId2) {
88203
+ const statePath = blockedStateFile(featureId2, defaultVerdictRunDir(this.ctx.tasksDir));
88204
+ let prior = null;
88205
+ try {
88206
+ if (await this.ctx.fs.exists(statePath))
88207
+ prior = parseBlockedState(await this.ctx.fs.readFile(statePath));
88208
+ } catch {
88209
+ return null;
88210
+ }
88211
+ if (!prior)
88212
+ return null;
88213
+ const inputs = await this.syncFingerprintInputs(featureId2);
88214
+ if (inputs === null)
88215
+ return null;
88216
+ return computeSyncFingerprint(inputs) === prior.inputFingerprint ? prior.result : null;
88217
+ }
88218
+ async recordBlockedSyncState(featureId2, result) {
88219
+ const runDir = defaultVerdictRunDir(this.ctx.tasksDir);
88220
+ const statePath = blockedStateFile(featureId2, runDir);
88221
+ const blocked = result.goalConflict === undefined && (result.proposal.gateBlocked === true || !result.applied && result.proposal.from !== result.proposal.to);
88222
+ if (!blocked) {
88223
+ try {
88224
+ if (await this.ctx.fs.exists(statePath))
88225
+ await this.ctx.fs.deleteFile(statePath);
88226
+ } catch {}
88227
+ return;
88228
+ }
88229
+ const inputs = await this.syncFingerprintInputs(featureId2);
88230
+ if (inputs === null)
88231
+ return;
88232
+ const state = {
88233
+ featureId: result.proposal.featureId,
88234
+ inputFingerprint: computeSyncFingerprint(inputs),
88235
+ proposal: result.proposal,
88236
+ classification: "blocked",
88237
+ result,
88238
+ persistedAt: new Date().toISOString()
88239
+ };
88240
+ try {
88241
+ await this.ctx.fs.ensureDir(runDir);
88242
+ await this.ctx.fs.writeFile(statePath, serializeBlockedState(state));
88243
+ } catch {}
88244
+ }
88245
+ async syncFingerprintInputs(featureId2) {
88246
+ const feature = await this.show(featureId2);
88247
+ if (!feature)
88248
+ return null;
88249
+ const tasks = (await this.collectTasksByFeature()).get(featureId2) ?? [];
88250
+ return {
88251
+ featureContentHash: createHash10("sha256").update(feature.content).digest("hex"),
88252
+ taskStatusVector: tasks.map((t) => `${t.wbs}:${t.status}`),
88253
+ verdictMtimeVector: await readVerdictMtimeVector(this.ctx.fs, defaultVerdictRunDir(this.ctx.tasksDir))
88254
+ };
88255
+ }
87509
88256
  async findOneActiveGoalConflict(featureId2) {
87510
88257
  const feature = await this.show(featureId2);
87511
88258
  if (feature === null || feature.frontmatter.priority !== "P0")
@@ -87900,6 +88647,7 @@ var FEATURE_ACTION_NAMES, FEATURE_ACTION_COMMANDS, FEATURE_FILE_RE;
87900
88647
  var init_feature_service = __esm(() => {
87901
88648
  init_src2();
87902
88649
  init_feature_check();
88650
+ init_feature_sync_suppression();
87903
88651
  init_task_locator();
87904
88652
  FEATURE_ACTION_NAMES = ["brainstorm", "plan"];
87905
88653
  FEATURE_ACTION_COMMANDS = {
@@ -90846,8 +91594,182 @@ var init_history_service = __esm(() => {
90846
91594
  HISTORY_BUSY_ERROR_PATTERN = /database is locked|SQLITE_BUSY/i;
90847
91595
  });
90848
91596
 
91597
+ // ../../packages/app/src/workflow/action-trace.ts
91598
+ import { join as join26 } from "path";
91599
+
91600
+ class WorkflowActionTraceWriter {
91601
+ inner;
91602
+ recordFailure;
91603
+ db;
91604
+ boundaries = new Map;
91605
+ constructor(inner, recordFailure, db2) {
91606
+ this.inner = inner;
91607
+ this.recordFailure = recordFailure;
91608
+ this.db = db2;
91609
+ }
91610
+ async saveActionStart(runId, node, kind, options) {
91611
+ const outcome = await this.guard("action.start", { runId, node, kind }, () => this.inner.saveActionStart(runId, node, kind, options));
91612
+ if (!outcome.ok) {
91613
+ const syntheticId = `${UNPERSISTED_PREFIX}${crypto.randomUUID()}`;
91614
+ this.boundaries.set(syntheticId, { runId, node, kind });
91615
+ this.lastStart = { runId, node, kind };
91616
+ return syntheticId;
91617
+ }
91618
+ this.boundaries.set(outcome.value, { runId, node, kind });
91619
+ this.lastStart = { runId, node, kind };
91620
+ return outcome.value;
91621
+ }
91622
+ lastStart;
91623
+ async saveActionFinalize(actionId, status, durationMs, ok, kind, result, redactor) {
91624
+ const boundary2 = this.boundaries.get(actionId);
91625
+ const origin2 = boundary2 ?? this.lastStart;
91626
+ await this.guard("action.finish", { runId: origin2?.runId ?? "", node: origin2?.node, kind }, () => this.inner.saveActionFinalize(actionId, status, durationMs, ok, kind, result, redactor));
91627
+ this.boundaries.delete(actionId);
91628
+ }
91629
+ async finalizeRun(runId, status, completedAt, fence, reason) {
91630
+ await this.inner.finalizeRun(runId, status, completedAt, fence, reason === undefined ? undefined : classifyTerminalReason({ status, engineReason: reason, errorText: reason }));
91631
+ }
91632
+ async claimRunOwnership(runId, owner, expectedStatuses) {
91633
+ return this.inner.claimRunOwnership(runId, owner, expectedStatuses);
91634
+ }
91635
+ async interruptRun(runId, reason) {
91636
+ return this.inner.interruptRun(runId, reason);
91637
+ }
91638
+ async recordAction(boundary2) {
91639
+ const start = await this.guard("action.start", { runId: boundary2.runId, node: boundary2.node, kind: boundary2.kind }, () => this.inner.saveActionStart(boundary2.runId, boundary2.node, boundary2.kind));
91640
+ if (!start.ok)
91641
+ return start;
91642
+ const finish = await this.guard("action.finish", { runId: boundary2.runId, node: boundary2.node, kind: boundary2.kind }, () => this.inner.saveActionFinalize(start.value, boundary2.status, boundary2.durationMs, boundary2.ok, boundary2.kind, boundary2.result, boundary2.redactor));
91643
+ if (!finish.ok)
91644
+ return finish;
91645
+ await this.backdateStart(start.value, boundary2);
91646
+ return { ok: true, actionId: start.value };
91647
+ }
91648
+ async backdateStart(actionId, boundary2) {
91649
+ const db2 = this.db;
91650
+ if (db2 === undefined || boundary2.durationMs <= 0)
91651
+ return;
91652
+ await this.guard("action.backdate", { runId: boundary2.runId, node: boundary2.node, kind: boundary2.kind }, async () => {
91653
+ const dao2 = new ActionRunDao(db2);
91654
+ const completedAt = await dao2.completedAtById(actionId);
91655
+ if (completedAt === null)
91656
+ return;
91657
+ const startedAt = new Date(new Date(completedAt).getTime() - boundary2.durationMs).toISOString();
91658
+ await dao2.setStartedAt(actionId, startedAt);
91659
+ });
91660
+ }
91661
+ async closeRun(runId, status, completedAt, reason) {
91662
+ const existing = await this.inner.loadRun(runId);
91663
+ if (existing === undefined) {
91664
+ throw new RunRowNotFoundError(runId);
91665
+ }
91666
+ await this.inner.finalizeRun(runId, status, completedAt ?? new Date().toISOString(), undefined, reason === undefined ? undefined : classifyTerminalReason({ status, engineReason: reason, errorText: reason }));
91667
+ if (this.db === undefined)
91668
+ return { ok: true };
91669
+ const rows = await new ActionRunDao(this.db).actionRowsByRunId(runId);
91670
+ return { ok: true, actionRows: rows.length };
91671
+ }
91672
+ async guard(operation, context4, emit) {
91673
+ try {
91674
+ return { ok: true, value: await emit() };
91675
+ } catch (error51) {
91676
+ const failure = {
91677
+ operation,
91678
+ runId: context4.runId,
91679
+ ...context4.node !== undefined ? { node: context4.node } : {},
91680
+ ...context4.kind !== undefined ? { kind: context4.kind } : {},
91681
+ error: error51 instanceof Error ? error51.message : String(error51),
91682
+ at: new Date().toISOString()
91683
+ };
91684
+ try {
91685
+ this.recordFailure?.(failure);
91686
+ } catch {}
91687
+ return { ok: false, failure };
91688
+ }
91689
+ }
91690
+ createRun(record3) {
91691
+ return this.inner.createRun(record3);
91692
+ }
91693
+ savePhase(runId, phase, status) {
91694
+ return this.inner.savePhase(runId, phase, status);
91695
+ }
91696
+ saveTransition(runId, from, to, trigger) {
91697
+ return this.inner.saveTransition(runId, from, to, trigger);
91698
+ }
91699
+ saveWorkflowState(runId, state, data) {
91700
+ return this.inner.saveWorkflowState(runId, state, data);
91701
+ }
91702
+ commitTransition(runId, from, to, trigger, state, data, phase) {
91703
+ return this.inner.commitTransition(runId, from, to, trigger, state, data, phase);
91704
+ }
91705
+ loadRun(runId) {
91706
+ return this.inner.loadRun(runId);
91707
+ }
91708
+ listRuns() {
91709
+ return this.inner.listRuns();
91710
+ }
91711
+ findRunByKey(workflowName, externalKey) {
91712
+ return this.inner.findRunByKey(workflowName, externalKey);
91713
+ }
91714
+ createOrAttachRun(record3) {
91715
+ return this.inner.createOrAttachRun(record3);
91716
+ }
91717
+ reseedRun(runId, newState) {
91718
+ return this.inner.reseedRun(runId, newState);
91719
+ }
91720
+ loadCurrentState(runId) {
91721
+ return this.inner.loadCurrentState(runId);
91722
+ }
91723
+ loadLatestStateSnapshot(runId) {
91724
+ return this.inner.loadLatestStateSnapshot(runId);
91725
+ }
91726
+ listPausedRuns(options) {
91727
+ return this.inner.listPausedRuns(options);
91728
+ }
91729
+ }
91730
+ function withActionTrace(persistence2, recordFailure) {
91731
+ return new WorkflowActionTraceWriter(persistence2, recordFailure);
91732
+ }
91733
+ function createWorkflowActionTraceWriter(db2, recordFailure) {
91734
+ return new WorkflowActionTraceWriter(new DbWorkflowPersistenceAdapter(db2), recordFailure, db2);
91735
+ }
91736
+ function createRunLogTraceFailureRecorder(cwd) {
91737
+ const fileSystem = createNodeFileSystem3();
91738
+ return (failure) => {
91739
+ const safeRunId = failure.runId.replace(/[^A-Za-z0-9._-]/g, "_");
91740
+ const dir = join26(cwd, ".spur", "run");
91741
+ const location = [
91742
+ failure.node !== undefined ? `node=${failure.node}` : "",
91743
+ failure.kind !== undefined ? `kind=${failure.kind}` : ""
91744
+ ].filter((part) => part !== "").join(" ");
91745
+ const line = `[${failure.at.replace(/\.\d{3}Z$/, "Z")}] trace-emission-failed operation=${failure.operation} run=${failure.runId}${location === "" ? "" : ` ${location}`}: ${failure.error}
91746
+ `;
91747
+ (async () => {
91748
+ await fileSystem.ensureDir(dir);
91749
+ await fileSystem.appendFile(join26(dir, `${safeRunId}.log`), line);
91750
+ })().catch(() => {
91751
+ return;
91752
+ });
91753
+ };
91754
+ }
91755
+ var UNPERSISTED_PREFIX = "trace-unpersisted:", RunRowNotFoundError;
91756
+ var init_action_trace = __esm(() => {
91757
+ init_src2();
91758
+ init_dist8();
91759
+ init_dist5();
91760
+ init_terminal_reason();
91761
+ RunRowNotFoundError = class RunRowNotFoundError extends Error {
91762
+ runId;
91763
+ constructor(runId) {
91764
+ super(`run row not found: ${runId}`);
91765
+ this.name = "RunRowNotFoundError";
91766
+ this.runId = runId;
91767
+ }
91768
+ };
91769
+ });
91770
+
90849
91771
  // ../../packages/app/src/workflow/decision-evidence.ts
90850
- import { createHash as createHash9 } from "crypto";
91772
+ import { createHash as createHash11 } from "crypto";
90851
91773
  function selectEvidence(allRows, producerNodes, cleaner) {
90852
91774
  if (producerNodes.length > MAX_EVIDENCE_ROWS) {
90853
91775
  return { ok: false, reason: "oversized-evidence" };
@@ -90926,7 +91848,7 @@ function parseSummaryEnvelope(raw, runId, evidenceNodes, selected, cleaner) {
90926
91848
  return { ok: true, summary };
90927
91849
  }
90928
91850
  function evidencePayloadDigest(payload) {
90929
- return `sha256:${createHash9("sha256").update(canonicalJsonStringify(payload), "utf8").digest("hex")}`;
91851
+ return `sha256:${createHash11("sha256").update(canonicalJsonStringify(payload), "utf8").digest("hex")}`;
90930
91852
  }
90931
91853
  function outcomeText(raw, clean) {
90932
91854
  if (!raw)
@@ -91370,7 +92292,7 @@ var init_decide = __esm(() => {
91370
92292
  });
91371
92293
 
91372
92294
  // ../../packages/app/src/workflow/actions/decide.ts
91373
- import { join as join26 } from "path";
92295
+ import { join as join27 } from "path";
91374
92296
 
91375
92297
  class DecideActionRunner {
91376
92298
  fileSystem;
@@ -91392,15 +92314,15 @@ class DecideActionRunner {
91392
92314
  const workdir = context4.workdir ?? ".";
91393
92315
  const result = await runDecide({
91394
92316
  ...options,
91395
- ...options.evidence !== undefined ? { evidence: options.evidence.map((path9) => join26(workdir, path9)) } : {},
91396
- resultFile: join26(workdir, options.resultFile)
92317
+ ...options.evidence !== undefined ? { evidence: options.evidence.map((path9) => join27(workdir, path9)) } : {},
92318
+ resultFile: join27(workdir, options.resultFile)
91397
92319
  }, {
91398
92320
  enabled: this.deps.enabled,
91399
92321
  ...this.deps.decisionMaker !== undefined ? { decisionMaker: this.deps.decisionMaker } : {},
91400
92322
  readFile: async (path9) => await this.fileSystem.readFile(path9),
91401
92323
  now: Date.now.bind(Date)
91402
92324
  });
91403
- await this.fileSystem.writeFile(join26(workdir, options.resultFile), `${JSON.stringify(result, null, 2)}
92325
+ await this.fileSystem.writeFile(join27(workdir, options.resultFile), `${JSON.stringify(result, null, 2)}
91404
92326
  `);
91405
92327
  return { ok: true, data: { value: result.value, decision: result } };
91406
92328
  }
@@ -91516,180 +92438,6 @@ function renderEventTrace(events2) {
91516
92438
  `);
91517
92439
  }
91518
92440
 
91519
- // ../../packages/app/src/workflow/action-trace.ts
91520
- import { join as join27 } from "path";
91521
-
91522
- class WorkflowActionTraceWriter {
91523
- inner;
91524
- recordFailure;
91525
- db;
91526
- boundaries = new Map;
91527
- constructor(inner, recordFailure, db2) {
91528
- this.inner = inner;
91529
- this.recordFailure = recordFailure;
91530
- this.db = db2;
91531
- }
91532
- async saveActionStart(runId, node, kind, options) {
91533
- const outcome = await this.guard("action.start", { runId, node, kind }, () => this.inner.saveActionStart(runId, node, kind, options));
91534
- if (!outcome.ok) {
91535
- const syntheticId = `${UNPERSISTED_PREFIX}${crypto.randomUUID()}`;
91536
- this.boundaries.set(syntheticId, { runId, node, kind });
91537
- this.lastStart = { runId, node, kind };
91538
- return syntheticId;
91539
- }
91540
- this.boundaries.set(outcome.value, { runId, node, kind });
91541
- this.lastStart = { runId, node, kind };
91542
- return outcome.value;
91543
- }
91544
- lastStart;
91545
- async saveActionFinalize(actionId, status, durationMs, ok, kind, result, redactor) {
91546
- const boundary2 = this.boundaries.get(actionId);
91547
- const origin2 = boundary2 ?? this.lastStart;
91548
- await this.guard("action.finish", { runId: origin2?.runId ?? "", node: origin2?.node, kind }, () => this.inner.saveActionFinalize(actionId, status, durationMs, ok, kind, result, redactor));
91549
- this.boundaries.delete(actionId);
91550
- }
91551
- async finalizeRun(runId, status, completedAt, fence, reason) {
91552
- await this.inner.finalizeRun(runId, status, completedAt, fence, reason === undefined ? undefined : classifyTerminalReason({ status, engineReason: reason, errorText: reason }));
91553
- }
91554
- async claimRunOwnership(runId, owner, expectedStatuses) {
91555
- return this.inner.claimRunOwnership(runId, owner, expectedStatuses);
91556
- }
91557
- async interruptRun(runId, reason) {
91558
- return this.inner.interruptRun(runId, reason);
91559
- }
91560
- async recordAction(boundary2) {
91561
- const start = await this.guard("action.start", { runId: boundary2.runId, node: boundary2.node, kind: boundary2.kind }, () => this.inner.saveActionStart(boundary2.runId, boundary2.node, boundary2.kind));
91562
- if (!start.ok)
91563
- return start;
91564
- const finish = await this.guard("action.finish", { runId: boundary2.runId, node: boundary2.node, kind: boundary2.kind }, () => this.inner.saveActionFinalize(start.value, boundary2.status, boundary2.durationMs, boundary2.ok, boundary2.kind, boundary2.result, boundary2.redactor));
91565
- if (!finish.ok)
91566
- return finish;
91567
- await this.backdateStart(start.value, boundary2);
91568
- return { ok: true, actionId: start.value };
91569
- }
91570
- async backdateStart(actionId, boundary2) {
91571
- const db2 = this.db;
91572
- if (db2 === undefined || boundary2.durationMs <= 0)
91573
- return;
91574
- await this.guard("action.backdate", { runId: boundary2.runId, node: boundary2.node, kind: boundary2.kind }, async () => {
91575
- const dao2 = new ActionRunDao(db2);
91576
- const completedAt = await dao2.completedAtById(actionId);
91577
- if (completedAt === null)
91578
- return;
91579
- const startedAt = new Date(new Date(completedAt).getTime() - boundary2.durationMs).toISOString();
91580
- await dao2.setStartedAt(actionId, startedAt);
91581
- });
91582
- }
91583
- async closeRun(runId, status, completedAt, reason) {
91584
- const existing = await this.inner.loadRun(runId);
91585
- if (existing === undefined) {
91586
- throw new RunRowNotFoundError(runId);
91587
- }
91588
- await this.inner.finalizeRun(runId, status, completedAt ?? new Date().toISOString(), undefined, reason === undefined ? undefined : classifyTerminalReason({ status, engineReason: reason, errorText: reason }));
91589
- if (this.db === undefined)
91590
- return { ok: true };
91591
- const rows = await new ActionRunDao(this.db).actionRowsByRunId(runId);
91592
- return { ok: true, actionRows: rows.length };
91593
- }
91594
- async guard(operation, context4, emit) {
91595
- try {
91596
- return { ok: true, value: await emit() };
91597
- } catch (error51) {
91598
- const failure = {
91599
- operation,
91600
- runId: context4.runId,
91601
- ...context4.node !== undefined ? { node: context4.node } : {},
91602
- ...context4.kind !== undefined ? { kind: context4.kind } : {},
91603
- error: error51 instanceof Error ? error51.message : String(error51),
91604
- at: new Date().toISOString()
91605
- };
91606
- try {
91607
- this.recordFailure?.(failure);
91608
- } catch {}
91609
- return { ok: false, failure };
91610
- }
91611
- }
91612
- createRun(record3) {
91613
- return this.inner.createRun(record3);
91614
- }
91615
- savePhase(runId, phase, status) {
91616
- return this.inner.savePhase(runId, phase, status);
91617
- }
91618
- saveTransition(runId, from, to, trigger) {
91619
- return this.inner.saveTransition(runId, from, to, trigger);
91620
- }
91621
- saveWorkflowState(runId, state, data) {
91622
- return this.inner.saveWorkflowState(runId, state, data);
91623
- }
91624
- commitTransition(runId, from, to, trigger, state, data, phase) {
91625
- return this.inner.commitTransition(runId, from, to, trigger, state, data, phase);
91626
- }
91627
- loadRun(runId) {
91628
- return this.inner.loadRun(runId);
91629
- }
91630
- listRuns() {
91631
- return this.inner.listRuns();
91632
- }
91633
- findRunByKey(workflowName, externalKey) {
91634
- return this.inner.findRunByKey(workflowName, externalKey);
91635
- }
91636
- createOrAttachRun(record3) {
91637
- return this.inner.createOrAttachRun(record3);
91638
- }
91639
- reseedRun(runId, newState) {
91640
- return this.inner.reseedRun(runId, newState);
91641
- }
91642
- loadCurrentState(runId) {
91643
- return this.inner.loadCurrentState(runId);
91644
- }
91645
- loadLatestStateSnapshot(runId) {
91646
- return this.inner.loadLatestStateSnapshot(runId);
91647
- }
91648
- listPausedRuns(options) {
91649
- return this.inner.listPausedRuns(options);
91650
- }
91651
- }
91652
- function withActionTrace(persistence2, recordFailure) {
91653
- return new WorkflowActionTraceWriter(persistence2, recordFailure);
91654
- }
91655
- function createWorkflowActionTraceWriter(db2, recordFailure) {
91656
- return new WorkflowActionTraceWriter(new DbWorkflowPersistenceAdapter(db2), recordFailure, db2);
91657
- }
91658
- function createRunLogTraceFailureRecorder(cwd) {
91659
- const fileSystem = createNodeFileSystem3();
91660
- return (failure) => {
91661
- const safeRunId = failure.runId.replace(/[^A-Za-z0-9._-]/g, "_");
91662
- const dir = join27(cwd, ".spur", "run");
91663
- const location = [
91664
- failure.node !== undefined ? `node=${failure.node}` : "",
91665
- failure.kind !== undefined ? `kind=${failure.kind}` : ""
91666
- ].filter((part) => part !== "").join(" ");
91667
- const line = `[${failure.at.replace(/\.\d{3}Z$/, "Z")}] trace-emission-failed operation=${failure.operation} run=${failure.runId}${location === "" ? "" : ` ${location}`}: ${failure.error}
91668
- `;
91669
- (async () => {
91670
- await fileSystem.ensureDir(dir);
91671
- await fileSystem.appendFile(join27(dir, `${safeRunId}.log`), line);
91672
- })().catch(() => {
91673
- return;
91674
- });
91675
- };
91676
- }
91677
- var UNPERSISTED_PREFIX = "trace-unpersisted:", RunRowNotFoundError;
91678
- var init_action_trace = __esm(() => {
91679
- init_src2();
91680
- init_dist8();
91681
- init_dist5();
91682
- init_terminal_reason();
91683
- RunRowNotFoundError = class RunRowNotFoundError extends Error {
91684
- runId;
91685
- constructor(runId) {
91686
- super(`run row not found: ${runId}`);
91687
- this.name = "RunRowNotFoundError";
91688
- this.runId = runId;
91689
- }
91690
- };
91691
- });
91692
-
91693
92441
  // ../../packages/app/src/workflow/actions/run-path.ts
91694
92442
  import { dirname as dirname17, join as join28, normalize, relative as relative4, resolve as resolve10, sep as sep5 } from "path";
91695
92443
  function within(child, ancestor) {
@@ -97171,7 +97919,7 @@ var init_workflow_service = __esm(() => {
97171
97919
  });
97172
97920
 
97173
97921
  // ../../packages/app/src/services/inline-run-setup.ts
97174
- import { join as join33, resolve as resolve14 } from "path";
97922
+ import { basename as basename12, join as join33, resolve as resolve14 } from "path";
97175
97923
  function parseIdentityMetadata(raw) {
97176
97924
  if (raw.trim() === "")
97177
97925
  return {};
@@ -97219,6 +97967,43 @@ async function persistWorktreeRuns(input) {
97219
97967
  citedNames.add(name);
97220
97968
  }
97221
97969
  }
97970
+ if ((input.taskFiles ?? []).length > 0) {
97971
+ const prefixes = (input.taskFiles ?? []).flatMap((taskFile) => {
97972
+ const wbs = /^(\d{4})_/.exec(basename12(taskFile))?.[1];
97973
+ return wbs === undefined ? [] : [`${wbs}-`];
97974
+ });
97975
+ const recordNames = new Set;
97976
+ const owner = await openInlineRunProjectDb(fromDir);
97977
+ try {
97978
+ for (const row of await listRunIdRows(owner.adapter)) {
97979
+ if (!SAFE_RUN_ID_RE.test(row.id))
97980
+ throw new InvalidWorkflowRunIdError(row.id);
97981
+ prefixes.push(`${row.id}-`);
97982
+ recordNames.add(`${row.id}.md`).add(`${row.id}.state.json`);
97983
+ }
97984
+ } finally {
97985
+ owner.close();
97986
+ }
97987
+ const entries = await readdir2(fromRunDir).catch((error51) => {
97988
+ if (error51.code === "ENOENT")
97989
+ return [];
97990
+ throw error51;
97991
+ });
97992
+ const ownedCounts = new Map;
97993
+ for (const name of entries.sort()) {
97994
+ if (citedNames.has(name) || recordNames.has(name))
97995
+ continue;
97996
+ const owner2 = prefixes.find((prefix) => name.startsWith(prefix));
97997
+ if (owner2 === undefined)
97998
+ continue;
97999
+ const count3 = (ownedCounts.get(owner2) ?? 0) + 1;
98000
+ if (count3 > MAX_CITED_RUN_FILES) {
98001
+ throw new Error(`persist-out: owner ${owner2} owns more than ${MAX_CITED_RUN_FILES} .spur/run/ files ` + "\u2014 over the per-owner evidence cap (1012 R1, 1034 R1); split the batch or prune the evidence");
98002
+ }
98003
+ ownedCounts.set(owner2, count3);
98004
+ citedNames.add(name);
98005
+ }
98006
+ }
97222
98007
  const citedCopies = [];
97223
98008
  const citedSkips = [];
97224
98009
  for (const name of citedNames) {
@@ -97488,20 +98273,334 @@ async function runDecideForInlineRun(input) {
97488
98273
  durationMs: decision.durationMs
97489
98274
  };
97490
98275
  }
97491
- var mkdirSync8, writeFileSync7, lstat2, readFile3, SAFE_RUN_ID_RE, MAX_CITED_RUN_FILES = 64, RUN_CITATION_RE;
98276
+ function writeInlineRunOutcome(runId, outcome) {
98277
+ const runDir = join33(process.cwd(), ".spur", "run");
98278
+ if (!existsSync8(runDir))
98279
+ mkdirSync8(runDir, { recursive: true });
98280
+ const statePath = join33(runDir, `${runId}.state.json`);
98281
+ const markdownPath = join33(runDir, `${runId}.md`);
98282
+ let prior = {};
98283
+ try {
98284
+ const parsed = JSON.parse(readFileSync12(statePath, "utf8"));
98285
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
98286
+ prior = parsed;
98287
+ }
98288
+ } catch {}
98289
+ const at2 = new Date().toISOString();
98290
+ const state = {
98291
+ schemaVersion: 1,
98292
+ runId,
98293
+ ...outcome.workflowName !== undefined ? { workflowName: outcome.workflowName } : {},
98294
+ ...outcome.status !== undefined ? { status: outcome.status } : {},
98295
+ startedAt: typeof prior.startedAt === "string" ? prior.startedAt : at2,
98296
+ updatedAt: at2,
98297
+ ...outcome.attached !== undefined ? { attached: outcome.attached } : {},
98298
+ ...outcome.definitionDigest !== undefined ? { definitionDigest: outcome.definitionDigest } : {},
98299
+ ...outcome.workflowVersion !== undefined ? { workflowVersion: outcome.workflowVersion } : {},
98300
+ ...outcome.resolvedPath !== undefined ? { resolvedPath: outcome.resolvedPath } : {},
98301
+ ...outcome.layer !== undefined ? { layer: outcome.layer } : {},
98302
+ ...outcome.workdir !== undefined ? { workdir: outcome.workdir } : {},
98303
+ ok: outcome.ok,
98304
+ ...outcome.ok === false && outcome.error !== undefined ? { error: outcome.error } : {}
98305
+ };
98306
+ const temp = `${statePath}.tmp`;
98307
+ try {
98308
+ writeFileSync7(temp, `${JSON.stringify(state, null, 4)}
98309
+ `);
98310
+ renameSync6(temp, statePath);
98311
+ } catch {
98312
+ try {
98313
+ unlinkSync4(temp);
98314
+ } catch {}
98315
+ }
98316
+ let headerFd;
98317
+ try {
98318
+ headerFd = openSync4(markdownPath, "wx");
98319
+ writeSync3(headerFd, `# spur inline run ${runId} \u2014 ${outcome.workflowName ?? "unknown workflow"} \u2014 setup ${at2}
98320
+ `);
98321
+ } catch {} finally {
98322
+ if (headerFd !== undefined)
98323
+ closeSync4(headerFd);
98324
+ }
98325
+ }
98326
+ function isInlineRunCloseStatus(status) {
98327
+ return CLOSE_STATUSES.has(status);
98328
+ }
98329
+ function isInlineRunActionStatus(status) {
98330
+ return ACTION_STATUSES.has(status);
98331
+ }
98332
+ function inlineRunRecordLogPath(runDir, runId) {
98333
+ const markdownPath = join33(runDir, `${runId}.md`);
98334
+ const legacyLogPath = join33(runDir, `${runId}.log`);
98335
+ if (existsSync8(legacyLogPath) && !existsSync8(markdownPath))
98336
+ return legacyLogPath;
98337
+ return markdownPath;
98338
+ }
98339
+ function appendInlineRunLogLine(runId, detail) {
98340
+ try {
98341
+ const runDir = join33(process.cwd(), ".spur", "run");
98342
+ if (!existsSync8(runDir))
98343
+ mkdirSync8(runDir, { recursive: true });
98344
+ const safeRunId = runId.replace(/[^A-Za-z0-9._-]/g, "_");
98345
+ const stamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
98346
+ appendFileSync5(inlineRunRecordLogPath(runDir, safeRunId), `[${stamp}] ${detail}
98347
+ `);
98348
+ } catch {}
98349
+ }
98350
+ async function runInlineRunTrace(input) {
98351
+ const operation = input.close ? "run.close" : "action.finish";
98352
+ const fail = (error51) => {
98353
+ appendInlineRunLogLine(input.runId, `trace-emission-failed operation=${operation} run=${input.runId}${input.node === "" ? "" : ` node=${input.node}`}${input.kind === "" ? "" : ` kind=${input.kind}`}: ${error51}`);
98354
+ process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error: error51 })}
98355
+ `);
98356
+ return input.close ? 1 : 0;
98357
+ };
98358
+ let projectDb;
98359
+ try {
98360
+ projectDb = await openInlineRunProjectDb(process.cwd());
98361
+ const writer = createWorkflowActionTraceWriter(projectDb.adapter, (failure) => {
98362
+ const detail = failure;
98363
+ appendInlineRunLogLine(input.runId, `trace-emission-failed operation=${detail.operation ?? operation} run=${input.runId}: ${detail.error ?? "unknown error"}`);
98364
+ });
98365
+ const result = input.close ? await writer.closeRun(input.runId, input.status, undefined, input.reason) : await writer.recordAction({
98366
+ runId: input.runId,
98367
+ node: input.node,
98368
+ kind: input.kind,
98369
+ status: input.status,
98370
+ ok: input.ok,
98371
+ durationMs: input.durationMs
98372
+ });
98373
+ if (result.ok !== true && result.failure !== undefined) {
98374
+ const failure = result.failure;
98375
+ return fail(failure.error ?? "unknown trace emission failure");
98376
+ }
98377
+ if (input.close && input.status === "done" && result.actionRows === 0) {
98378
+ const error51 = `run ${input.runId} closed done with zero action_runs rows`;
98379
+ appendInlineRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${error51}`);
98380
+ process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error: error51, code: "NO_ACTION_ROWS", actionRows: 0 })}
98381
+ `);
98382
+ return 1;
98383
+ }
98384
+ process.stdout.write(`${JSON.stringify({ ...result, runId: input.runId })}
98385
+ `);
98386
+ return 0;
98387
+ } catch (error51) {
98388
+ if (input.close && error51.name === "RunRowNotFoundError") {
98389
+ const message = error51 instanceof Error ? error51.message : String(error51);
98390
+ appendInlineRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${message}`);
98391
+ process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error: message, code: "RUN_NOT_FOUND" })}
98392
+ `);
98393
+ return 1;
98394
+ }
98395
+ return fail(error51 instanceof Error ? error51.message : String(error51));
98396
+ } finally {
98397
+ projectDb?.close();
98398
+ }
98399
+ }
98400
+ async function runInlineRunTraceBatch(input) {
98401
+ const batchFailed = (error51) => {
98402
+ process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error: error51 })}
98403
+ `);
98404
+ return 1;
98405
+ };
98406
+ let rows;
98407
+ try {
98408
+ rows = JSON.parse(readFileSync12(input.actionsFile, "utf8"));
98409
+ } catch (error51) {
98410
+ return batchFailed(`cannot read actions file ${input.actionsFile}: ${error51 instanceof Error ? error51.message : String(error51)}`);
98411
+ }
98412
+ if (!Array.isArray(rows)) {
98413
+ return batchFailed("actions file must be a JSON array of {node,kind,status,ok,durationMs}");
98414
+ }
98415
+ const entries = [];
98416
+ const rowError = (index2, error51) => `actions[${index2}]: ${error51}`;
98417
+ for (const [index2, row] of rows.entries()) {
98418
+ if (row === null || typeof row !== "object" || Array.isArray(row)) {
98419
+ return batchFailed(rowError(index2, "entry must be a JSON object"));
98420
+ }
98421
+ const record3 = row;
98422
+ const { node, kind, status, ok, durationMs } = record3;
98423
+ if (typeof node !== "string" || node.trim() === "") {
98424
+ return batchFailed(rowError(index2, "node must be a non-empty string"));
98425
+ }
98426
+ if (typeof kind !== "string" || kind.trim() === "") {
98427
+ return batchFailed(rowError(index2, "kind must be a non-empty string"));
98428
+ }
98429
+ if (typeof status !== "string" || !isInlineRunActionStatus(status)) {
98430
+ return batchFailed(rowError(index2, 'status must be "done" or "failed"'));
98431
+ }
98432
+ if (typeof ok !== "boolean")
98433
+ return batchFailed(rowError(index2, "ok must be a boolean"));
98434
+ if (typeof durationMs !== "number" || !Number.isFinite(durationMs) || durationMs < 0) {
98435
+ return batchFailed(rowError(index2, "durationMs must be a finite non-negative number"));
98436
+ }
98437
+ entries.push({ node, kind, status, ok, durationMs });
98438
+ }
98439
+ let projectDb;
98440
+ let recorded = 0;
98441
+ const reportEmissionFailure = (node, kind, error51) => {
98442
+ appendInlineRunLogLine(input.runId, `trace-emission-failed operation=action.finish run=${input.runId} node=${node} kind=${kind}: ${error51}`);
98443
+ process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, recorded, error: error51 })}
98444
+ `);
98445
+ };
98446
+ try {
98447
+ projectDb = await openInlineRunProjectDb(process.cwd());
98448
+ const writer = createWorkflowActionTraceWriter(projectDb.adapter, (failure) => {
98449
+ const detail = failure;
98450
+ appendInlineRunLogLine(input.runId, `trace-emission-failed operation=${detail.operation ?? "action.finish"} run=${input.runId}: ${detail.error ?? "unknown error"}`);
98451
+ });
98452
+ for (const entry of entries) {
98453
+ const result = await writer.recordAction({
98454
+ runId: input.runId,
98455
+ node: entry.node,
98456
+ kind: entry.kind,
98457
+ status: entry.status,
98458
+ ok: entry.ok,
98459
+ durationMs: entry.durationMs
98460
+ });
98461
+ if (result.ok === true) {
98462
+ recorded += 1;
98463
+ continue;
98464
+ }
98465
+ const failure = result.failure;
98466
+ reportEmissionFailure(entry.node, entry.kind, failure?.error ?? "unknown trace emission failure");
98467
+ return 0;
98468
+ }
98469
+ process.stdout.write(`${JSON.stringify({ ok: true, runId: input.runId, recorded })}
98470
+ `);
98471
+ return 0;
98472
+ } catch (error51) {
98473
+ const message = error51 instanceof Error ? error51.message : String(error51);
98474
+ appendInlineRunLogLine(input.runId, `trace-emission-failed run=${input.runId}: ${message}`);
98475
+ process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, recorded, error: message })}
98476
+ `);
98477
+ return 0;
98478
+ } finally {
98479
+ projectDb?.close();
98480
+ }
98481
+ }
98482
+ async function runInlineRunFingerprint(input) {
98483
+ const workdir = process.cwd();
98484
+ const contents = await readProofInputContents(undefined, workdir, {
98485
+ taskFile: input.taskFile,
98486
+ ...input.featureFile !== undefined && input.featureFile.trim() !== "" ? { featureFile: input.featureFile } : {}
98487
+ });
98488
+ if (!contents.ok) {
98489
+ console.error(`inline-run-setup: FAIL \u2014 ${contents.error}`);
98490
+ return 1;
98491
+ }
98492
+ const digest = await computeProofInputFingerprint({
98493
+ cwd: workdir,
98494
+ ...contents.taskContent !== undefined ? { taskContent: contents.taskContent } : {},
98495
+ ...contents.featureContent !== undefined ? { featureContent: contents.featureContent } : {}
98496
+ });
98497
+ process.stdout.write(`${digest}
98498
+ `);
98499
+ return 0;
98500
+ }
98501
+ async function runInlineRunDecide(input) {
98502
+ const decideFailed = (error51) => {
98503
+ process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error: error51 })}
98504
+ `);
98505
+ return 1;
98506
+ };
98507
+ let outcome;
98508
+ try {
98509
+ outcome = await runDecideForInlineRun({
98510
+ workdir: process.cwd(),
98511
+ optionsFile: input.optionsFile,
98512
+ enabled: input.enabled
98513
+ });
98514
+ } catch (error51) {
98515
+ return decideFailed(error51 instanceof Error ? error51.message : String(error51));
98516
+ }
98517
+ if (!outcome.ok)
98518
+ return decideFailed(outcome.error ?? "decide failed without an error message");
98519
+ process.stdout.write(`${JSON.stringify({ runId: input.runId, node: input.node, ...outcome, ok: true })}
98520
+ `);
98521
+ appendInlineRunLogLine(input.runId, `decide node=${input.node} value=${outcome.value ?? ""} source=${outcome.source ?? "default"} reason=${outcome.reason ?? ""}`);
98522
+ return runInlineRunTrace({
98523
+ runId: input.runId,
98524
+ close: false,
98525
+ node: input.node,
98526
+ kind: "decide",
98527
+ status: "done",
98528
+ ok: true,
98529
+ durationMs: outcome.durationMs ?? 0
98530
+ });
98531
+ }
98532
+ async function runInlineRunPersistOut(input) {
98533
+ try {
98534
+ const result = await persistWorktreeRuns({
98535
+ fromWorkdir: input.from,
98536
+ toWorkdir: process.cwd(),
98537
+ ...input.taskFiles.length > 0 ? { taskFiles: input.taskFiles } : {}
98538
+ });
98539
+ process.stdout.write(`${JSON.stringify({ ok: true, persisted: result.persisted, skipped: result.skipped })}
98540
+ `);
98541
+ return 0;
98542
+ } catch (error51) {
98543
+ const message = error51 instanceof Error ? error51.message : String(error51);
98544
+ process.stdout.write(`${JSON.stringify({ ok: false, error: message })}
98545
+ `);
98546
+ return 1;
98547
+ }
98548
+ }
98549
+ async function runInlineRunSetup(input) {
98550
+ const workdir = process.cwd();
98551
+ const projectDb = await openInlineRunProjectDb(workdir);
98552
+ let exitCode = 0;
98553
+ try {
98554
+ const result = await createOrAttachInlineRun({
98555
+ workdir,
98556
+ getDb: async () => projectDb.adapter,
98557
+ file: input.file,
98558
+ runId: input.runId,
98559
+ inventory: input.inventory,
98560
+ ...input.embeddedSchemas !== undefined ? { embeddedSchemas: input.embeddedSchemas } : {}
98561
+ });
98562
+ writeInlineRunOutcome(input.runId, result);
98563
+ if (!result.ok) {
98564
+ console.error(`inline-run-setup: FAIL for run ${input.runId}`);
98565
+ console.error(` ${result.error}`);
98566
+ exitCode = 1;
98567
+ } else {
98568
+ console.error(`inline-run-setup: ${result.attached ? "attached" : "created"} run ${input.runId} (${result.workflowName}, layer ${result.layer}, digest ${result.definitionDigest}, status ${result.status})`);
98569
+ }
98570
+ } finally {
98571
+ projectDb.close();
98572
+ }
98573
+ return exitCode;
98574
+ }
98575
+ var appendFileSync5, closeSync4, existsSync8, mkdirSync8, openSync4, readFileSync12, renameSync6, unlinkSync4, writeFileSync7, writeSync3, lstat2, readdir2, readFile3, SAFE_RUN_ID_RE, MAX_CITED_RUN_FILES = 64, RUN_CITATION_RE, CLOSE_STATUSES, ACTION_STATUSES;
97492
98576
  var init_inline_run_setup = __esm(async () => {
97493
98577
  init_src2();
97494
98578
  init_dist8();
97495
98579
  init_dist5();
98580
+ init_action_trace();
97496
98581
  init_decide2();
98582
+ init_proof_input_fingerprint();
97497
98583
  init_run_record();
97498
98584
  init_terminal_reason();
97499
98585
  init_workflow_resolver();
97500
98586
  init_workflow_service();
97501
- ({ mkdirSync: mkdirSync8, writeFileSync: writeFileSync7 } = await import("fs"));
97502
- ({ lstat: lstat2, readFile: readFile3 } = await import("fs/promises"));
98587
+ ({
98588
+ appendFileSync: appendFileSync5,
98589
+ closeSync: closeSync4,
98590
+ existsSync: existsSync8,
98591
+ mkdirSync: mkdirSync8,
98592
+ openSync: openSync4,
98593
+ readFileSync: readFileSync12,
98594
+ renameSync: renameSync6,
98595
+ unlinkSync: unlinkSync4,
98596
+ writeFileSync: writeFileSync7,
98597
+ writeSync: writeSync3
98598
+ } = await import("fs"));
98599
+ ({ lstat: lstat2, readdir: readdir2, readFile: readFile3 } = await import("fs/promises"));
97503
98600
  SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
97504
98601
  RUN_CITATION_RE = /(?<![\w~:-]|[\w~:-]\/)\.spur\/run\/([A-Za-z0-9][A-Za-z0-9._*?<>{}|,\u2026-]*)/g;
98602
+ CLOSE_STATUSES = new Set(["done", "failed", "paused"]);
98603
+ ACTION_STATUSES = new Set(["done", "failed"]);
97505
98604
  });
97506
98605
 
97507
98606
  // ../../packages/app/src/services/job-worker-service.ts
@@ -98198,8 +99297,8 @@ var init_process_inventory_service = __esm(() => {
98198
99297
  });
98199
99298
 
98200
99299
  // ../../packages/app/src/services/project-start.ts
98201
- import { existsSync as existsSync8 } from "fs";
98202
- import { basename as basename12, resolve as resolve15 } from "path";
99300
+ import { existsSync as existsSync9 } from "fs";
99301
+ import { basename as basename13, resolve as resolve15 } from "path";
98203
99302
  function shQuote(value) {
98204
99303
  return `'${value.replace(/'/g, `'\\''`)}'`;
98205
99304
  }
@@ -98244,7 +99343,7 @@ function setDetachedServeSpawnForTests(spawn2) {
98244
99343
  }
98245
99344
  function resolveSpurServeCommand() {
98246
99345
  const cliPath = getEnvVar("SPUR_CLI_PATH");
98247
- if (cliPath !== undefined && existsSync8(cliPath)) {
99346
+ if (cliPath !== undefined && existsSync9(cliPath)) {
98248
99347
  return [process.execPath, cliPath];
98249
99348
  }
98250
99349
  const argv1 = process.argv[1];
@@ -98255,7 +99354,7 @@ function resolveSpurServeCommand() {
98255
99354
  }
98256
99355
  }
98257
99356
  const monorepoCli = resolve15(process.cwd(), "apps/cli/src/index.ts");
98258
- if (existsSync8(monorepoCli)) {
99357
+ if (existsSync9(monorepoCli)) {
98259
99358
  return [process.execPath, monorepoCli];
98260
99359
  }
98261
99360
  const fromPath = typeof Bun !== "undefined" && typeof Bun.which === "function" ? Bun.which("spur") : null;
@@ -98268,14 +99367,14 @@ async function startRegisteredProject(registry2, target, options = {}) {
98268
99367
  let entry = await registry2.getByName(target) ?? await registry2.getByPath(target);
98269
99368
  if (!entry) {
98270
99369
  const absPath = normalizeProjectPath(target);
98271
- if (existsSync8(absPath)) {
98272
- entry = await registry2.upsert({ path: absPath, name: basename12(absPath), port: 0 });
99370
+ if (existsSync9(absPath)) {
99371
+ entry = await registry2.upsert({ path: absPath, name: basename13(absPath), port: 0 });
98273
99372
  } else {
98274
99373
  throw new Error(`Project not found in registry: "${target}"`);
98275
99374
  }
98276
99375
  }
98277
99376
  const projectPath = normalizeProjectPath(entry.path);
98278
- if (!existsSync8(projectPath)) {
99377
+ if (!existsSync9(projectPath)) {
98279
99378
  throw new Error(`Project path does not exist: "${entry.path}" (resolved to ${projectPath}). Update ~/.config/spur/projects.json.`);
98280
99379
  }
98281
99380
  if (projectPath !== entry.path) {
@@ -99313,9 +100412,9 @@ var init_section_matrix_loader = __esm(() => {
99313
100412
  });
99314
100413
 
99315
100414
  // ../../packages/app/src/services/slash-commands-service.ts
99316
- import { existsSync as existsSync9, mkdirSync as mkdirSync9, readdirSync as readdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
100415
+ import { existsSync as existsSync10, mkdirSync as mkdirSync9, readdirSync as readdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
99317
100416
  import { homedir as homedir9 } from "os";
99318
- import { basename as basename13, dirname as dirname20, join as join36, resolve as resolve17 } from "path";
100417
+ import { basename as basename14, dirname as dirname20, join as join36, resolve as resolve17 } from "path";
99319
100418
  function getBuiltinClaudeCommands() {
99320
100419
  return BUILTIN_CLAUDE_COMMANDS.map((cmd) => ({ ...cmd }));
99321
100420
  }
@@ -99337,7 +100436,7 @@ function findPluginsSpCommandsDir(startDir = process.cwd()) {
99337
100436
  while (true) {
99338
100437
  try {
99339
100438
  const candidate = join36(current, "plugins", "sp", "commands");
99340
- if (existsSync9(candidate)) {
100439
+ if (existsSync10(candidate)) {
99341
100440
  return candidate;
99342
100441
  }
99343
100442
  } catch {
@@ -99356,7 +100455,7 @@ function findProjectClaudeCommandsDir(startDir = process.cwd()) {
99356
100455
  while (true) {
99357
100456
  try {
99358
100457
  const candidate = join36(current, ".claude", "commands");
99359
- if (existsSync9(candidate)) {
100458
+ if (existsSync10(candidate)) {
99360
100459
  return candidate;
99361
100460
  }
99362
100461
  } catch {
@@ -99390,7 +100489,7 @@ function parseMarkdownFrontmatter(rawContent) {
99390
100489
  }
99391
100490
  }
99392
100491
  function scanMarkdownCommands(dirPath, options = {}) {
99393
- if (!existsSync9(dirPath)) {
100492
+ if (!existsSync10(dirPath)) {
99394
100493
  return [];
99395
100494
  }
99396
100495
  const { prefix = "/", source = "plugin:sp", defaultCategory, pluginId } = options;
@@ -99402,8 +100501,8 @@ function scanMarkdownCommands(dirPath, options = {}) {
99402
100501
  continue;
99403
100502
  }
99404
100503
  const fullPath = join36(dirPath, entry.name);
99405
- const stem = basename13(entry.name, ".md");
99406
- const fileContent = readFileSync12(fullPath, "utf-8");
100504
+ const stem = basename14(entry.name, ".md");
100505
+ const fileContent = readFileSync13(fullPath, "utf-8");
99407
100506
  const { frontmatter, body } = parseMarkdownFrontmatter(fileContent);
99408
100507
  const cleanPrefix = prefix.endsWith(":") ? prefix : prefix.endsWith("/") ? prefix : `${prefix}:`;
99409
100508
  const name = prefix === "/" ? `/${stem}` : `${cleanPrefix}${stem}`;
@@ -99493,15 +100592,15 @@ function discoverEnabledClaudePlugins(options = {}) {
99493
100592
  const projectSettingsPath = join36(projectRoot, ".claude", "settings.json");
99494
100593
  const installedPath = join36(claudeDir, "plugins", "installed_plugins.json");
99495
100594
  let userSettings = {};
99496
- if (existsSync9(userSettingsPath)) {
100595
+ if (existsSync10(userSettingsPath)) {
99497
100596
  try {
99498
- userSettings = JSON.parse(readFileSync12(userSettingsPath, "utf-8"));
100597
+ userSettings = JSON.parse(readFileSync13(userSettingsPath, "utf-8"));
99499
100598
  } catch {}
99500
100599
  }
99501
100600
  let projectSettings = {};
99502
- if (existsSync9(projectSettingsPath)) {
100601
+ if (existsSync10(projectSettingsPath)) {
99503
100602
  try {
99504
- projectSettings = JSON.parse(readFileSync12(projectSettingsPath, "utf-8"));
100603
+ projectSettings = JSON.parse(readFileSync13(projectSettingsPath, "utf-8"));
99505
100604
  } catch {}
99506
100605
  }
99507
100606
  const enabledPlugins = {
@@ -99513,9 +100612,9 @@ function discoverEnabledClaudePlugins(options = {}) {
99513
100612
  ...projectSettings.extraKnownMarketplaces ?? {}
99514
100613
  };
99515
100614
  let installedManifest = { plugins: {} };
99516
- if (existsSync9(installedPath)) {
100615
+ if (existsSync10(installedPath)) {
99517
100616
  try {
99518
- installedManifest = JSON.parse(readFileSync12(installedPath, "utf-8"));
100617
+ installedManifest = JSON.parse(readFileSync13(installedPath, "utf-8"));
99519
100618
  } catch {}
99520
100619
  }
99521
100620
  const installedPlugins = installedManifest.plugins ?? {};
@@ -99544,7 +100643,7 @@ function discoverEnabledClaudePlugins(options = {}) {
99544
100643
  const installs = installedPlugins[pluginKey];
99545
100644
  if (Array.isArray(installs)) {
99546
100645
  for (const inst of installs) {
99547
- if (typeof inst.installPath === "string" && existsSync9(inst.installPath)) {
100646
+ if (typeof inst.installPath === "string" && existsSync10(inst.installPath)) {
99548
100647
  resolvedDir = inst.installPath;
99549
100648
  break;
99550
100649
  }
@@ -99555,16 +100654,16 @@ function discoverEnabledClaudePlugins(options = {}) {
99555
100654
  if (mSrc?.source === "directory" && typeof mSrc.path === "string") {
99556
100655
  const cand1 = join36(mSrc.path, "plugins", pluginName);
99557
100656
  const cand2 = join36(mSrc.path, pluginName);
99558
- if (existsSync9(cand1)) {
100657
+ if (existsSync10(cand1)) {
99559
100658
  resolvedDir = cand1;
99560
- } else if (existsSync9(cand2)) {
100659
+ } else if (existsSync10(cand2)) {
99561
100660
  resolvedDir = cand2;
99562
100661
  }
99563
100662
  }
99564
100663
  }
99565
100664
  if (!resolvedDir && marketplaceName) {
99566
100665
  const cacheDir = join36(claudeDir, "plugins", "cache", marketplaceName, pluginName);
99567
- if (existsSync9(cacheDir)) {
100666
+ if (existsSync10(cacheDir)) {
99568
100667
  try {
99569
100668
  const entries = readdirSync7(cacheDir, { withFileTypes: true });
99570
100669
  const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
@@ -99577,20 +100676,20 @@ function discoverEnabledClaudePlugins(options = {}) {
99577
100676
  }
99578
100677
  if (!resolvedDir && marketplaceName) {
99579
100678
  const mpDir = join36(claudeDir, "plugins", "marketplaces", marketplaceName, "plugins", pluginName);
99580
- if (existsSync9(mpDir)) {
100679
+ if (existsSync10(mpDir)) {
99581
100680
  resolvedDir = mpDir;
99582
100681
  }
99583
100682
  }
99584
100683
  if (!resolvedDir) {
99585
100684
  const localPlugin = resolve17(projectRoot, "plugins", pluginName);
99586
- if (existsSync9(localPlugin)) {
100685
+ if (existsSync10(localPlugin)) {
99587
100686
  resolvedDir = localPlugin;
99588
100687
  }
99589
100688
  }
99590
100689
  let commandsDir;
99591
100690
  if (resolvedDir) {
99592
100691
  const candidateCommands = join36(resolvedDir, "commands");
99593
- if (existsSync9(candidateCommands)) {
100692
+ if (existsSync10(candidateCommands)) {
99594
100693
  commandsDir = candidateCommands;
99595
100694
  }
99596
100695
  }
@@ -99639,7 +100738,7 @@ function generateSlashCommands(options = {}) {
99639
100738
  }
99640
100739
  if (includePluginSp) {
99641
100740
  const spCommandsDir = findPluginsSpCommandsDir(projectRoot) ?? resolve17(projectRoot, "plugins", "sp", "commands");
99642
- if (existsSync9(spCommandsDir)) {
100741
+ if (existsSync10(spCommandsDir)) {
99643
100742
  const spCommands = scanMarkdownCommands(spCommandsDir, {
99644
100743
  prefix: "/sp:",
99645
100744
  source: "plugin:sp",
@@ -99694,7 +100793,7 @@ function generateSlashCommandsFile(targetPath = getSlashCommandsFilePath(), opti
99694
100793
  };
99695
100794
  try {
99696
100795
  const dir = dirname20(targetPath);
99697
- if (!existsSync9(dir)) {
100796
+ if (!existsSync10(dir)) {
99698
100797
  mkdirSync9(dir, { recursive: true });
99699
100798
  }
99700
100799
  writeFileSync8(targetPath, JSON.stringify(filePayload, null, 2), "utf-8");
@@ -99706,7 +100805,7 @@ function generateSlashCommandsFile(targetPath = getSlashCommandsFilePath(), opti
99706
100805
  };
99707
100806
  }
99708
100807
  function loadSlashCommands(filePath = getSlashCommandsFilePath(), autoGenerateIfMissing = true, options = {}) {
99709
- if (!existsSync9(filePath)) {
100808
+ if (!existsSync10(filePath)) {
99710
100809
  if (autoGenerateIfMissing) {
99711
100810
  const generated = generateSlashCommandsFile(filePath, options);
99712
100811
  return generated.commands;
@@ -99714,7 +100813,7 @@ function loadSlashCommands(filePath = getSlashCommandsFilePath(), autoGenerateIf
99714
100813
  return getBuiltinClaudeCommands();
99715
100814
  }
99716
100815
  try {
99717
- const content = readFileSync12(filePath, "utf-8");
100816
+ const content = readFileSync13(filePath, "utf-8");
99718
100817
  const raw = JSON.parse(content);
99719
100818
  const parsed = slashCommandsFileSchema.safeParse(raw);
99720
100819
  if (parsed.success && parsed.data.commands.length > 0) {
@@ -100763,7 +101862,7 @@ var init_system_event_emitter = __esm(() => {
100763
101862
  });
100764
101863
 
100765
101864
  // ../../packages/app/src/services/task-readiness.ts
100766
- import { createHash as createHash10 } from "crypto";
101865
+ import { createHash as createHash12 } from "crypto";
100767
101866
  function readyRefineCommand(wbs) {
100768
101867
  return `/sp:dev-refine ${wbs} --auto --depth ready`;
100769
101868
  }
@@ -100951,7 +102050,7 @@ function computePlanningDigest(raw) {
100951
102050
  featureId: typeof fm.feature_id === "string" ? fm.feature_id : null,
100952
102051
  template: typeof fm.template === "string" ? fm.template : null
100953
102052
  };
100954
- return createHash10("sha256").update(JSON.stringify(payload)).digest("hex");
102053
+ return createHash12("sha256").update(JSON.stringify(payload)).digest("hex");
100955
102054
  }
100956
102055
  function verifyReadyChecks(checks4) {
100957
102056
  if (checks4 === undefined || checks4.length === 0) {
@@ -101100,56 +102199,6 @@ var init_task_scaffold = __esm(() => {
101100
102199
  init_task_locator();
101101
102200
  });
101102
102201
 
101103
- // ../../packages/app/src/services/task-size-precheck.ts
101104
- function sectionBody(content, headingPattern) {
101105
- const heading = content.match(new RegExp(headingPattern, "m"));
101106
- if (!heading)
101107
- return null;
101108
- const rest = content.slice((heading.index ?? 0) + heading[0].length);
101109
- const nextHeading = rest.match(/^#{2,3}\s+/m);
101110
- return nextHeading ? rest.slice(0, nextHeading.index ?? 0) : rest;
101111
- }
101112
- function countRItems(content) {
101113
- const body = sectionBody(content, "^#{2,3}\\s+Requirements\\s*$") ?? content;
101114
- const numbers = new Set;
101115
- for (const match of body.matchAll(new RegExp(R_ITEM_RE_SOURCE, "gm"))) {
101116
- numbers.add(match[1] ?? "");
101117
- }
101118
- return numbers.size;
101119
- }
101120
- function countPlanItems(content) {
101121
- const body = sectionBody(content, "^#{2,3}\\s+Plan\\s*$");
101122
- if (body === null)
101123
- return 0;
101124
- const numbered = body.match(new RegExp(NUMBERED_PLAN_ITEM_RE_SOURCE, "gm"))?.length ?? 0;
101125
- const checklist = body.match(new RegExp(CHECKLIST_ITEM_RE_SOURCE, "gm"))?.length ?? 0;
101126
- return numbered + checklist;
101127
- }
101128
- function evaluateTaskSize(content, limits = DEFAULT_TASK_SIZE_LIMITS) {
101129
- const reqCount = countRItems(content);
101130
- const planItemCount = countPlanItems(content);
101131
- const reasons = [];
101132
- if (reqCount > limits.maxReqs) {
101133
- reasons.push(`Task has ${reqCount} R-items (max ${limits.maxReqs}). ` + `Consider decomposing into smaller tasks or raise maxImplementReqs via --vars.`);
101134
- }
101135
- if (planItemCount > limits.maxPlanItems) {
101136
- reasons.push(`Task has ${planItemCount} Plan items (max ${limits.maxPlanItems}). ` + `Consider simplifying the plan or raise maxImplementPlanItems via --vars.`);
101137
- }
101138
- return {
101139
- reqCount,
101140
- planItemCount,
101141
- ok: reasons.length === 0,
101142
- reasons
101143
- };
101144
- }
101145
- var DEFAULT_TASK_SIZE_LIMITS, R_ITEM_RE_SOURCE = "^\\s*[-*]\\s+(?:\\[[ xX]\\]\\s*)?[*_]{0,2}R(\\d+)\\.?[*_]{0,2}(?:\\s|$)", NUMBERED_PLAN_ITEM_RE_SOURCE = "^\\d+\\.\\s", CHECKLIST_ITEM_RE_SOURCE = "^\\s*[-*]\\s+\\[[ xX]\\]";
101146
- var init_task_size_precheck = __esm(() => {
101147
- DEFAULT_TASK_SIZE_LIMITS = {
101148
- maxReqs: 10,
101149
- maxPlanItems: 16
101150
- };
101151
- });
101152
-
101153
102202
  // ../../packages/app/src/services/task-transition.ts
101154
102203
  async function runTransitionCheckGate(gate, wbs, filePath, target) {
101155
102204
  const result = await gate.service.check(filePath, wbs, {
@@ -101831,11 +102880,13 @@ ${templateBackground}`;
101831
102880
  await this.writeService.updateSection(ref, "Solution", solutionBody);
101832
102881
  result.solutionBackfilled = true;
101833
102882
  }
102883
+ const featureAc = await this.resolveFeatureAcBody(filePath).catch(() => null);
102884
+ const scenarioTitles = featureScenarioTitles(featureAc?.ac ?? "");
101834
102885
  for (const section of ["Requirements", "Acceptance Criteria"]) {
101835
102886
  const body = doc2.getSection(section);
101836
102887
  if (body === null || body.trim().length === 0)
101837
102888
  continue;
101838
- const flipped = flipVerifiedCheckboxes(body, verdict);
102889
+ const flipped = flipVerifiedCheckboxes(body, verdict, scenarioTitles);
101839
102890
  if (flipped !== body) {
101840
102891
  await this.writeService.updateSection(ref, section, flipped);
101841
102892
  }
@@ -102087,8 +103138,8 @@ ${block}` : block);
102087
103138
  return { wbs: hit.wbs, filePath: hit.filePath };
102088
103139
  if (opts.strict === true)
102089
103140
  return null;
102090
- const basename14 = filePath.split("/").pop() ?? "";
102091
- const capturedWbs = /^(\d{4})_.+\.md$/.exec(basename14)?.[1];
103141
+ const basename15 = filePath.split("/").pop() ?? "";
103142
+ const capturedWbs = /^(\d{4})_.+\.md$/.exec(basename15)?.[1];
102092
103143
  if (capturedWbs) {
102093
103144
  const taskPath = await this.locator.findPathByWbs(capturedWbs);
102094
103145
  if (taskPath !== null)
@@ -102305,7 +103356,7 @@ function extractRequirements(text4) {
102305
103356
  }
102306
103357
  if (!trimmed.startsWith("|"))
102307
103358
  continue;
102308
- const cells = splitTableCells(trimmed);
103359
+ const cells = splitTableCells2(trimmed);
102309
103360
  if (!inTable && cells.length >= 2) {
102310
103361
  const h0 = (cells[0] ?? "").toLowerCase().trim();
102311
103362
  const h0IsId = h0.includes("req") || h0 === "requirement" || h0 === "r#" || h0 === "r" || /^r\d+$/.test(h0);
@@ -102356,7 +103407,7 @@ function normalizeStatus(raw) {
102356
103407
  return "UNMET";
102357
103408
  return null;
102358
103409
  }
102359
- function splitTableCells(row) {
103410
+ function splitTableCells2(row) {
102360
103411
  return row.split(/(?<!\\)\|/).map((c3) => c3.replace(/\\\|/g, "|").trim()).filter(Boolean);
102361
103412
  }
102362
103413
  function extractAcceptanceCriteria(text4) {
@@ -102373,7 +103424,7 @@ function extractAcceptanceCriteria(text4) {
102373
103424
  }
102374
103425
  if (!trimmed.startsWith("|"))
102375
103426
  continue;
102376
- const cells = splitTableCells(trimmed);
103427
+ const cells = splitTableCells2(trimmed);
102377
103428
  if (!inTable && cells.length >= 4) {
102378
103429
  const h0 = (cells[0] ?? "").toLowerCase();
102379
103430
  const h1 = (cells[1] ?? "").toLowerCase();
@@ -102390,7 +103441,7 @@ function extractAcceptanceCriteria(text4) {
102390
103441
  const statusRaw = cells[1] ?? "";
102391
103442
  const evidenceTypeRaw = cells[2] ?? "";
102392
103443
  const status = normalizeAcceptanceCriteriaStatus(statusRaw);
102393
- const evidenceType = normalizeEvidenceType(evidenceTypeRaw);
103444
+ const evidenceType = normalizeEvidenceType2(evidenceTypeRaw);
102394
103445
  const acShaped = cells.length >= 4 || id.length > 0 && status !== null;
102395
103446
  const evidence = cells[3] ?? "";
102396
103447
  if (acShaped && status !== null && evidenceType !== null && id.length > 0) {
@@ -102414,7 +103465,7 @@ function normalizeAcceptanceCriteriaStatus(raw) {
102414
103465
  return "N/A";
102415
103466
  return null;
102416
103467
  }
102417
- function normalizeEvidenceTypeToken(normalized) {
103468
+ function normalizeEvidenceTypeToken2(normalized) {
102418
103469
  if (normalized === "test")
102419
103470
  return "test";
102420
103471
  if (normalized === "command")
@@ -102430,16 +103481,16 @@ function normalizeEvidenceTypeToken(normalized) {
102430
103481
  return "n/a";
102431
103482
  return null;
102432
103483
  }
102433
- function normalizeEvidenceType(raw) {
103484
+ function normalizeEvidenceType2(raw) {
102434
103485
  const normalized = raw.toLowerCase().trim();
102435
- const single = normalizeEvidenceTypeToken(normalized);
103486
+ const single = normalizeEvidenceTypeToken2(normalized);
102436
103487
  if (single !== null)
102437
103488
  return single;
102438
- const tokens = normalized.split(/[+,/]/).map((part) => normalizeEvidenceTypeToken(part.trim())).filter((token) => token !== null);
103489
+ const tokens = normalized.split(/[+,/]/).map((part) => normalizeEvidenceTypeToken2(part.trim())).filter((token) => token !== null);
102439
103490
  if (tokens.length < 2 || tokens.length !== normalized.split(/[+,/]/).filter((p) => p.trim()).length) {
102440
103491
  return null;
102441
103492
  }
102442
- return EVIDENCE_TYPE_PRECEDENCE.find((candidate) => tokens.includes(candidate)) ?? null;
103493
+ return EVIDENCE_TYPE_PRECEDENCE2.find((candidate) => tokens.includes(candidate)) ?? null;
102443
103494
  }
102444
103495
  function applyAcceptanceCriteriaEvidenceRule(rows) {
102445
103496
  return rows.map((row) => {
@@ -102470,7 +103521,7 @@ function extractChecks(_text, taskCheckPassed, requirements, acceptanceCriteria,
102470
103521
  const trimmed = line.trim();
102471
103522
  if (!trimmed.startsWith("|"))
102472
103523
  continue;
102473
- const cells = splitTableCells(trimmed);
103524
+ const cells = splitTableCells2(trimmed);
102474
103525
  if (cells.length >= 2) {
102475
103526
  const h0 = (cells[0] ?? "").toLowerCase();
102476
103527
  const h1 = (cells[1] ?? "").toLowerCase();
@@ -102548,10 +103599,10 @@ function aggregateBatchVerdicts(results) {
102548
103599
  const summary = `${counts.PASS} PASS, ${counts.PARTIAL} PARTIAL, ${counts.FAIL} FAIL, ${notStarted.length} NOT-STARTED (excluded)`;
102549
103600
  return { verdict, rolledUp, notStarted, summary };
102550
103601
  }
102551
- var EVIDENCE_TYPE_PRECEDENCE, NOT_STARTED_STATUSES;
103602
+ var EVIDENCE_TYPE_PRECEDENCE2, NOT_STARTED_STATUSES;
102552
103603
  var init_task_verdict = __esm(() => {
102553
103604
  init_verify_verdict();
102554
- EVIDENCE_TYPE_PRECEDENCE = ["test", "command", "static-ref", "manual-review", "llm-judge", "n/a"];
103605
+ EVIDENCE_TYPE_PRECEDENCE2 = ["test", "command", "static-ref", "manual-review", "llm-judge", "n/a"];
102555
103606
  NOT_STARTED_STATUSES = {
102556
103607
  backlog: true,
102557
103608
  todo: true,
@@ -102564,7 +103615,7 @@ var init_task_verdict = __esm(() => {
102564
103615
  });
102565
103616
 
102566
103617
  // ../../packages/app/src/services/token-ledger-service.ts
102567
- import { closeSync as closeSync4, existsSync as existsSync10, fstatSync as fstatSync2, openSync as openSync4, readSync } from "fs";
103618
+ import { closeSync as closeSync5, existsSync as existsSync11, fstatSync as fstatSync2, openSync as openSync5, readSync } from "fs";
102568
103619
  import { join as join38 } from "path";
102569
103620
  function isSparseToolActivity(events2) {
102570
103621
  return events2.length === 0 || !events2.some((e) => TOOL_ACTIVITY_TYPES.has(e.type));
@@ -102616,11 +103667,11 @@ function parseLedgerLine(line) {
102616
103667
  }
102617
103668
  }
102618
103669
  function tailTokenLedgerFile(filePath, limit, chunkSize = DEFAULT_CHUNK, before) {
102619
- if (!existsSync10(filePath)) {
103670
+ if (!existsSync11(filePath)) {
102620
103671
  return { events: [], truncated: false };
102621
103672
  }
102622
103673
  const target = limit + 1;
102623
- const fd = openSync4(filePath, "r");
103674
+ const fd = openSync5(filePath, "r");
102624
103675
  try {
102625
103676
  const { size } = fstatSync2(fd);
102626
103677
  if (size === 0)
@@ -102658,7 +103709,7 @@ function tailTokenLedgerFile(filePath, limit, chunkSize = DEFAULT_CHUNK, before)
102658
103709
  truncated
102659
103710
  };
102660
103711
  } finally {
102661
- closeSync4(fd);
103712
+ closeSync5(fd);
102662
103713
  }
102663
103714
  }
102664
103715
 
@@ -102712,7 +103763,7 @@ var init_token_ledger_service = __esm(() => {
102712
103763
  });
102713
103764
 
102714
103765
  // ../../packages/app/src/services/token-ledger-watcher.ts
102715
- import { closeSync as closeSync5, existsSync as existsSync11, fstatSync as fstatSync3, openSync as openSync5, readSync as readSync2, watch } from "fs";
103766
+ import { closeSync as closeSync6, existsSync as existsSync12, fstatSync as fstatSync3, openSync as openSync6, readSync as readSync2, watch } from "fs";
102716
103767
 
102717
103768
  class TokenLedgerWatcher {
102718
103769
  ledgerPath;
@@ -102733,7 +103784,7 @@ class TokenLedgerWatcher {
102733
103784
  this.started = true;
102734
103785
  this.offset = this.currentSize();
102735
103786
  try {
102736
- const target = existsSync11(this.ledgerPath) ? this.ledgerPath : this.parentDir();
103787
+ const target = existsSync12(this.ledgerPath) ? this.ledgerPath : this.parentDir();
102737
103788
  this.watcher = watch(target, () => this.schedulePoll());
102738
103789
  } catch {
102739
103790
  this.watcher = undefined;
@@ -102768,14 +103819,14 @@ class TokenLedgerWatcher {
102768
103819
  return idx > 0 ? this.ledgerPath.slice(0, idx) : ".";
102769
103820
  }
102770
103821
  currentSize() {
102771
- if (!existsSync11(this.ledgerPath))
103822
+ if (!existsSync12(this.ledgerPath))
102772
103823
  return 0;
102773
103824
  try {
102774
- const fd = openSync5(this.ledgerPath, "r");
103825
+ const fd = openSync6(this.ledgerPath, "r");
102775
103826
  try {
102776
103827
  return fstatSync3(fd).size;
102777
103828
  } finally {
102778
- closeSync5(fd);
103829
+ closeSync6(fd);
102779
103830
  }
102780
103831
  } catch {
102781
103832
  return 0;
@@ -102790,14 +103841,14 @@ class TokenLedgerWatcher {
102790
103841
  }, this.debounceMs);
102791
103842
  }
102792
103843
  pollNewBytes() {
102793
- if (!existsSync11(this.ledgerPath)) {
103844
+ if (!existsSync12(this.ledgerPath)) {
102794
103845
  this.offset = 0;
102795
103846
  this.carry = "";
102796
103847
  return;
102797
103848
  }
102798
103849
  let size;
102799
103850
  try {
102800
- const fd = openSync5(this.ledgerPath, "r");
103851
+ const fd = openSync6(this.ledgerPath, "r");
102801
103852
  try {
102802
103853
  size = fstatSync3(fd).size;
102803
103854
  if (size < this.offset) {
@@ -102825,7 +103876,7 @@ class TokenLedgerWatcher {
102825
103876
  }
102826
103877
  }
102827
103878
  } finally {
102828
- closeSync5(fd);
103879
+ closeSync6(fd);
102829
103880
  }
102830
103881
  } catch {}
102831
103882
  }
@@ -103277,7 +104328,10 @@ class LifecycleAdapter {
103277
104328
  });
103278
104329
  }
103279
104330
  await svc.reseedRun(workflow, runId, currentStatus);
103280
- const result = await svc.requestTransition(workflow, runId, to, { workdir: this.opts.cwd });
104331
+ const result = await svc.requestTransition(workflow, runId, to, {
104332
+ workdir: this.opts.cwd,
104333
+ vars: { [profile.varKey]: ref.id, spurBin: this.opts.spurBin }
104334
+ });
103281
104335
  if (result.allowed) {
103282
104336
  const enterFailure = await this.runTargetEnterActions(workflow, to, runId);
103283
104337
  if (enterFailure !== null) {
@@ -103768,6 +104822,7 @@ var init_trace_writer = __esm(() => {
103768
104822
  var exports_src2 = {};
103769
104823
  __export(exports_src2, {
103770
104824
  writeJsonError: () => writeJsonError,
104825
+ writeInlineRunOutcome: () => writeInlineRunOutcome,
103771
104826
  workflowLayers: () => workflowLayers,
103772
104827
  withActionTrace: () => withActionTrace,
103773
104828
  waitForOccupant: () => waitForOccupant,
@@ -103799,6 +104854,12 @@ __export(exports_src2, {
103799
104854
  schedulerJobTimeoutEnvName: () => schedulerJobTimeoutEnvName,
103800
104855
  scanMarkdownCommands: () => scanMarkdownCommands,
103801
104856
  satisfies: () => satisfies,
104857
+ runInlineRunTraceBatch: () => runInlineRunTraceBatch,
104858
+ runInlineRunTrace: () => runInlineRunTrace,
104859
+ runInlineRunSetup: () => runInlineRunSetup,
104860
+ runInlineRunPersistOut: () => runInlineRunPersistOut,
104861
+ runInlineRunFingerprint: () => runInlineRunFingerprint,
104862
+ runInlineRunDecide: () => runInlineRunDecide,
103802
104863
  runIdeaHandoffCli: () => runIdeaHandoffCli,
103803
104864
  runHistoryReport: () => runHistoryReport,
103804
104865
  runDecideForInlineRun: () => runDecideForInlineRun,
@@ -103850,6 +104911,7 @@ __export(exports_src2, {
103850
104911
  readVerdictArtifact: () => readVerdictArtifact,
103851
104912
  readVerdict: () => readVerdict,
103852
104913
  readProofInputContents: () => readProofInputContents,
104914
+ readFeatureBody: () => readFeatureBody,
103853
104915
  qualifySectionBody: () => qualifySectionBody,
103854
104916
  qualifyAnchors: () => qualifyAnchors,
103855
104917
  pruneReports: () => pruneReports,
@@ -103884,6 +104946,8 @@ __export(exports_src2, {
103884
104946
  looksLikeOpaqueId: () => looksLikeOpaqueId,
103885
104947
  loadSlashCommands: () => loadSlashCommands,
103886
104948
  loadSectionMatrix: () => loadSectionMatrix,
104949
+ lintVerifyAnswerForTask: () => lintVerifyAnswerForTask,
104950
+ lintVerifyAnswer: () => lintVerifyAnswer,
103887
104951
  isTerminalReason: () => isTerminalReason,
103888
104952
  isSystemEventEnvelopeV2: () => isSystemEventEnvelopeV2,
103889
104953
  isSchedulerCustomActiveConflict: () => isSchedulerCustomActiveConflict,
@@ -103892,11 +104956,15 @@ __export(exports_src2, {
103892
104956
  isPortLive: () => isPortLive,
103893
104957
  isPortAvailable: () => isPortAvailable,
103894
104958
  isInsideRoot: () => isInsideRoot,
104959
+ isInlineRunCloseStatus: () => isInlineRunCloseStatus,
104960
+ isInlineRunActionStatus: () => isInlineRunActionStatus,
103895
104961
  isFindingCode: () => isFindingCode,
104962
+ isFeatureFile: () => isFeatureFile,
103896
104963
  isFeatureActionName: () => isFeatureActionName,
103897
104964
  isExclusiveJobActive: () => isExclusiveJobActive,
103898
104965
  isBookkeepingWorkflow: () => isBookkeepingWorkflow,
103899
104966
  installSystemEventCatchAll: () => installSystemEventCatchAll,
104967
+ inlineRunRecordLogPath: () => inlineRunRecordLogPath,
103900
104968
  humanWorkflowTitle: () => humanWorkflowTitle,
103901
104969
  humanStepLabel: () => humanStepLabel,
103902
104970
  hitlConfirmDefault: () => hitlConfirmDefault,
@@ -103983,6 +105051,7 @@ __export(exports_src2, {
103983
105051
  assertPiImporterSafe: () => assertPiImporterSafe,
103984
105052
  assertInventoryIdentity: () => assertInventoryIdentity,
103985
105053
  assertCanonicalTimeoutMs: () => assertCanonicalTimeoutMs,
105054
+ appendInlineRunLogLine: () => appendInlineRunLogLine,
103986
105055
  anchorQualify: () => anchorQualify,
103987
105056
  aggregateBatchVerdicts: () => aggregateBatchVerdicts,
103988
105057
  acquireExclusiveJob: () => acquireExclusiveJob,
@@ -104129,6 +105198,7 @@ __export(exports_src2, {
104129
105198
  AgentRunActionRunner: () => AgentRunActionRunner,
104130
105199
  AgentExecutionLifecycle: () => AgentExecutionLifecycle,
104131
105200
  AgentCoordinationService: () => AgentCoordinationService,
105201
+ ANSWER_LINT_MAX_FINDINGS: () => ANSWER_LINT_MAX_FINDINGS,
104132
105202
  ALL_FINDING_CODES: () => ALL_FINDING_CODES,
104133
105203
  AGENT_SESSION_SOURCES: () => AGENT_SESSION_SOURCES
104134
105204
  });
@@ -104201,6 +105271,7 @@ var init_src3 = __esm(async () => {
104201
105271
  init_task_verdict();
104202
105272
  init_token_ledger_service();
104203
105273
  init_token_ledger_watcher();
105274
+ init_verify_answer_lint();
104204
105275
  init_workflow_service();
104205
105276
  init_write_slot_service();
104206
105277
  init_action_trace();
@@ -112043,7 +113114,10 @@ var SHARED_OPTIONS = {
112043
113114
  fileRuleAdhocPath: ["--file <path>", "Ad-hoc rule file path"],
112044
113115
  fileTaskBatch: ["--file <path>", "Path to the batch JSON file validated against task-batch.schema.json"],
112045
113116
  fileTaskTest: ["--file <path>", "Custom target test file path"],
112046
- forceFeatureReopen: ["--force", "Force applying reopen proposals without confirmation"],
113117
+ forceFeatureReopen: [
113118
+ "--force",
113119
+ "Force applying reopen proposals without confirmation; also bypasses repeated-BLOCKED suppression"
113120
+ ],
112047
113121
  forceInitRecreate: ["--force", "Recreate files that already exist"],
112048
113122
  forceWorkflowClean: ["--force", "Clean ALL non-terminal runs regardless of age (overrides --older-than)"],
112049
113123
  forceWorkflowContinue: ["--force", "Proceed with resume even if workflow definition drift is detected"],
@@ -112059,7 +113133,7 @@ init_loader();
112059
113133
  init_src2();
112060
113134
  init_dist7();
112061
113135
  await init_src3();
112062
- import { existsSync as existsSync12, readFileSync as readFileSync13 } from "fs";
113136
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
112063
113137
  import { rm, writeFile as writeFile2 } from "fs/promises";
112064
113138
  import { tmpdir as tmpdir3 } from "os";
112065
113139
  import { dirname as dirname25, join as join41 } from "path";
@@ -113053,9 +114127,9 @@ init_src2();
113053
114127
  await init_src3();
113054
114128
 
113055
114129
  // src/workflow/resolve-spur-bin.ts
113056
- import { basename as basename14 } from "path";
114130
+ import { basename as basename15 } from "path";
113057
114131
  function resolveSpurBinFrom(launch) {
113058
- const runtime = basename14(launch.execPath).toLowerCase().replace(/\.exe$/, "");
114132
+ const runtime = basename15(launch.execPath).toLowerCase().replace(/\.exe$/, "");
113059
114133
  const isJsRuntime = runtime === "bun" || runtime === "node";
113060
114134
  if (isJsRuntime && launch.mainModule) {
113061
114135
  return `${launch.execPath} ${launch.mainModule}`;
@@ -113804,7 +114878,14 @@ Recover with: ${err.recoveryCommand}` : err.message;
113804
114878
  }
113805
114879
  });
113806
114880
  task.command("verdict").summary("Derive PASS/PARTIAL/FAIL/UNKNOWN verdict from verify answer text (replaces pipeline grep/shell).").argument("<wbs>", "Task WBS number").option("--from-answer <path>", "Path to verify answer text file").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (wbs, options) => {
113807
- const { deriveVerdict: deriveVerdict2, summarizeRowIds: summarizeRowIds2, VERDICT_SCENARIO_KEY_FORMS: VERDICT_SCENARIO_KEY_FORMS2, verdictScenarioKeyGap: verdictScenarioKeyGap2 } = await init_src3().then(() => exports_src2);
114881
+ const {
114882
+ ANSWER_LINT_MAX_FINDINGS: ANSWER_LINT_MAX_FINDINGS2,
114883
+ deriveVerdict: deriveVerdict2,
114884
+ lintVerifyAnswerForTask: lintVerifyAnswerForTask2,
114885
+ summarizeRowIds: summarizeRowIds2,
114886
+ VERDICT_SCENARIO_KEY_FORMS: VERDICT_SCENARIO_KEY_FORMS2,
114887
+ verdictScenarioKeyGap: verdictScenarioKeyGap2
114888
+ } = await init_src3().then(() => exports_src2);
113808
114889
  const answerPath = options.fromAnswer ?? `.spur/run/${wbs}-verify-answer.txt`;
113809
114890
  let answerText;
113810
114891
  try {
@@ -113814,15 +114895,28 @@ Recover with: ${err.recoveryCommand}` : err.message;
113814
114895
  context4.setExitCode(1);
113815
114896
  return;
113816
114897
  }
113817
- const taskCheckPassed = true;
113818
- const result = deriveVerdict2(answerText, taskCheckPassed);
113819
114898
  const { foldersConfig, featuresDir } = await resolvePlanningFolders(context4.fs);
113820
- const gap = await verdictScenarioKeyGap2(wbs, [...result.requirements, ...result.acceptanceCriteria ?? []], {
114899
+ const corpusDeps = {
113821
114900
  fs: context4.fs,
113822
114901
  tasksDir: context4.fs.resolve(options.folder ?? foldersConfig.active_folder),
113823
114902
  foldersConfig,
113824
114903
  featuresDir: context4.fs.resolve(featuresDir)
113825
- });
114904
+ };
114905
+ const lintFindings = await lintVerifyAnswerForTask2(wbs, answerText, corpusDeps);
114906
+ if (lintFindings.length > 0) {
114907
+ if (options.json) {
114908
+ context4.output.write(JSON.stringify({ wbs, lintFindings }, null, 2));
114909
+ } else {
114910
+ context4.output.error(`task verdict: answer lint failed \u2014 ${lintFindings.length}${lintFindings.length >= ANSWER_LINT_MAX_FINDINGS2 ? "+" : ""} finding(s) in ${answerPath}`);
114911
+ for (const f of lintFindings)
114912
+ context4.output.error(` line ${f.line}: ${f.message}`);
114913
+ }
114914
+ context4.setExitCode(1);
114915
+ return;
114916
+ }
114917
+ const taskCheckPassed = true;
114918
+ const result = deriveVerdict2(answerText, taskCheckPassed);
114919
+ const gap = await verdictScenarioKeyGap2(wbs, [...result.requirements, ...result.acceptanceCriteria ?? []], corpusDeps);
113826
114920
  if (gap !== null) {
113827
114921
  writeJsonError(context4.output, options, `Verdict rows key to no scenario of linked feature ${gap.featureId}: ${summarizeRowIds2(gap.offendingIds)}. Key each row by ${VERDICT_SCENARIO_KEY_FORMS2} (repair: /sp:dev-verify ${wbs})`);
113828
114922
  context4.setExitCode(1);
@@ -113888,9 +114982,14 @@ Recover with: ${err.recoveryCommand}` : err.message;
113888
114982
  context4.setExitCode(1);
113889
114983
  }
113890
114984
  });
113891
- 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) => {
114985
+ 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("--precheck", "Also run the implement-readiness prechecks (size, evidence channel); failures are errors.").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) => {
113892
114986
  const json4 = options.json === true;
113893
114987
  const strict = options.strict === true;
114988
+ if (options.precheck === true && (options.corpus || !wbs)) {
114989
+ writeJsonError(context4.output, options, "--precheck requires a <wbs> argument and cannot be combined with --corpus", "VALIDATION_FAILED");
114990
+ context4.setExitCode(2);
114991
+ return;
114992
+ }
113894
114993
  if (options.since !== undefined && !options.corpus || options.corpus && (wbs || options.as || options.fix || options.folder || strict || options.strictCore) || options.since?.startsWith("-")) {
113895
114994
  writeJsonError(context4.output, options, "--corpus accepts only --since and JSON output options; --since requires --corpus and a git ref", "VALIDATION_FAILED");
113896
114995
  context4.setExitCode(2);
@@ -113957,7 +115056,17 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
113957
115056
  strict,
113958
115057
  asStatus,
113959
115058
  severityOverrides: planningFolders.severityOverrides,
113960
- fix: options.fix === true
115059
+ fix: options.fix === true,
115060
+ ...options.precheck === true ? {
115061
+ precheck: true,
115062
+ countArgsRaw: async (source) => {
115063
+ try {
115064
+ return await countToolCallArgsRaw(await context4.getDb(), source);
115065
+ } catch {
115066
+ return null;
115067
+ }
115068
+ }
115069
+ } : {}
113961
115070
  });
113962
115071
  results.push(result);
113963
115072
  printResult(result);
@@ -114145,14 +115254,14 @@ function loadTemplateBodies(projectRoot, variant) {
114145
115254
  return cached2;
114146
115255
  let bodies = {};
114147
115256
  const localPath = join41(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
114148
- if (existsSync12(localPath)) {
114149
- bodies = extractTemplateBodies(readFileSync13(localPath, "utf8"));
115257
+ if (existsSync13(localPath)) {
115258
+ bodies = extractTemplateBodies(readFileSync14(localPath, "utf8"));
114150
115259
  } else {
114151
115260
  const root = bundledConfigRoot();
114152
115261
  if (root !== null) {
114153
115262
  const templatePath = join41(root, "templates", "task", `${variant}.md`);
114154
- if (existsSync12(templatePath)) {
114155
- bodies = extractTemplateBodies(readFileSync13(templatePath, "utf8"));
115263
+ if (existsSync13(templatePath)) {
115264
+ bodies = extractTemplateBodies(readFileSync14(templatePath, "utf8"));
114156
115265
  }
114157
115266
  }
114158
115267
  }
@@ -114860,12 +115969,12 @@ function waitFail(context4, options, code, message) {
114860
115969
  // src/release-ops.ts
114861
115970
  init_src();
114862
115971
  init_dist5();
114863
- import { existsSync as existsSync14, readdirSync as readdirSync8, readFileSync as readFileSync14 } from "fs";
115972
+ import { existsSync as existsSync15, readdirSync as readdirSync8, readFileSync as readFileSync15 } from "fs";
114864
115973
  import { join as join43 } from "path";
114865
115974
 
114866
115975
  // src/version-carriers.ts
114867
115976
  init_zod();
114868
- import { existsSync as existsSync13 } from "fs";
115977
+ import { existsSync as existsSync14 } from "fs";
114869
115978
  import { join as join42 } from "path";
114870
115979
  var defaultLiteralProbe = { file: "src/config.ts", identifier: "binaryVersion" };
114871
115980
  var registry2 = new Map;
@@ -114902,7 +116011,7 @@ function findLiteralProbe(carriers, packageDir) {
114902
116011
  return;
114903
116012
  }
114904
116013
  async function readJson(path9) {
114905
- if (!existsSync13(path9))
116014
+ if (!existsSync14(path9))
114906
116015
  return null;
114907
116016
  try {
114908
116017
  return JSON.parse(await Bun.file(path9).text());
@@ -114977,7 +116086,7 @@ async function run(repoRoot, cmd) {
114977
116086
  }
114978
116087
  async function verifyLockfile(ctx, output2) {
114979
116088
  const lockPath = join43(ctx.repoRoot, "bun.lock");
114980
- if (!existsSync14(lockPath) || Bun.file(lockPath).size === 0)
116089
+ if (!existsSync15(lockPath) || Bun.file(lockPath).size === 0)
114981
116090
  return;
114982
116091
  const result = await run(ctx.repoRoot, ["bun", "install", "--frozen-lockfile"]);
114983
116092
  if (!result.ok) {
@@ -114987,7 +116096,7 @@ async function verifyLockfile(ctx, output2) {
114987
116096
  }
114988
116097
  async function assertPluginInstallable(ctx, output2) {
114989
116098
  const smokePath = join43(ctx.repoRoot, "scripts", "commands", "plugin-install-smoke.ts");
114990
- if (!existsSync14(smokePath)) {
116099
+ if (!existsSync15(smokePath)) {
114991
116100
  output2.write(" \u21B3 plugin-install-smoke: gate script not present in this repo \u2014 skipped");
114992
116101
  return;
114993
116102
  }
@@ -115028,7 +116137,7 @@ async function readJson2(path9) {
115028
116137
  function rootWorkspaceGlobs(repoRoot) {
115029
116138
  let root;
115030
116139
  try {
115031
- root = JSON.parse(readFileSync14(join43(repoRoot, "package.json"), "utf8"));
116140
+ root = JSON.parse(readFileSync15(join43(repoRoot, "package.json"), "utf8"));
115032
116141
  } catch {
115033
116142
  return [];
115034
116143
  }
@@ -115046,10 +116155,10 @@ function workspaceDirs(repoRoot) {
115046
116155
  continue;
115047
116156
  const parent = glob.slice(0, -2);
115048
116157
  const parentAbs = join43(repoRoot, parent);
115049
- if (!existsSync14(parentAbs))
116158
+ if (!existsSync15(parentAbs))
115050
116159
  continue;
115051
116160
  for (const entry of readdirSync8(parentAbs, { withFileTypes: true })) {
115052
- if (entry.isDirectory() && existsSync14(join43(parentAbs, entry.name, "package.json"))) {
116161
+ if (entry.isDirectory() && existsSync15(join43(parentAbs, entry.name, "package.json"))) {
115053
116162
  dirs.push(`${parent}/${entry.name}`);
115054
116163
  }
115055
116164
  }
@@ -115080,7 +116189,7 @@ async function releaseContext(repoRoot, bumpVerConfig = resolveBuilderBumpVerCon
115080
116189
  for (const ws of workspaces2) {
115081
116190
  const probe = findLiteralProbe(versionCarriers, ws.dir) ?? defaultLiteralProbe;
115082
116191
  const literalProbe = join43(ws.dir, probe.file);
115083
- const versionSourceFile = existsSync14(join43(repoRoot, literalProbe)) ? literalProbe : undefined;
116192
+ const versionSourceFile = existsSync15(join43(repoRoot, literalProbe)) ? literalProbe : undefined;
115084
116193
  packages.set(shortName(ws.name), {
115085
116194
  packageDir: ws.dir,
115086
116195
  packageName: ws.name,
@@ -115144,7 +116253,7 @@ async function updateWorkspacePins(ctx, pkgName, oldVersion, newVersion, output2
115144
116253
  if (changed.length === 0)
115145
116254
  return changed;
115146
116255
  const lockPath = join43(ctx.repoRoot, "bun.lock");
115147
- if (existsSync14(lockPath)) {
116256
+ if (existsSync15(lockPath)) {
115148
116257
  const lock = await Bun.file(lockPath).text();
115149
116258
  const staleSpec = `"${pkgName}": "workspace:${oldVersion}"`;
115150
116259
  if (lock.includes(staleSpec)) {
@@ -115272,7 +116381,7 @@ async function bumpVersion(ctx, config4, version3, options, output2) {
115272
116381
  await syncExtraCarriers(ctx, version3, staged, output2);
115273
116382
  await verifyLockfile(ctx, output2);
115274
116383
  const lockPath = join43(ctx.repoRoot, "bun.lock");
115275
- if (existsSync14(lockPath) && Bun.file(lockPath).size > 0)
116384
+ if (existsSync15(lockPath) && Bun.file(lockPath).size > 0)
115276
116385
  staged.push("bun.lock");
115277
116386
  await git2(ctx.repoRoot, ["add", ...staged]);
115278
116387
  const message = `${config4.releaseCommitType}(${config4.releaseCommitScope}): ${config4.releaseCommitSubject(version3)}`;
@@ -115350,7 +116459,7 @@ async function bumpAll(ctx, version3, options, output2) {
115350
116459
  await syncExtraCarriers(ctx, version3, staged, output2);
115351
116460
  await verifyLockfile(ctx, output2);
115352
116461
  const lockPath = join43(ctx.repoRoot, "bun.lock");
115353
- if (existsSync14(lockPath) && Bun.file(lockPath).size > 0)
116462
+ if (existsSync15(lockPath) && Bun.file(lockPath).size > 0)
115354
116463
  staged.push("bun.lock");
115355
116464
  await git2(ctx.repoRoot, ["add", ...staged]);
115356
116465
  const shortNames = configs.map((c3) => c3.packageName.split("/").pop() ?? c3.packageName).join(" + ");
@@ -115787,8 +116896,15 @@ function registerFeatureCommand(program2, context4) {
115787
116896
  const result = await svc.refresh({ featureId: options.feature });
115788
116897
  if (options.json) {
115789
116898
  const featuresDir = options.folder ?? (await resolvePlanningFolders(context4.fs)).featuresDir;
115790
- context4.output.write(toEnvelopeJson({ index_path: `${featuresDir}/INDEX.md`, tasksUpdated: result.tasksUpdated }, { enveloped: options.jsonEnvelope }));
116899
+ context4.output.write(toEnvelopeJson({
116900
+ index_path: `${featuresDir}/INDEX.md`,
116901
+ tasksUpdated: result.tasksUpdated,
116902
+ skipped: result.skipped
116903
+ }, { enveloped: options.jsonEnvelope }));
115791
116904
  } else {
116905
+ for (const s3 of result.skipped) {
116906
+ context4.output.write(`Skipped ${s3.id} (${s3.reason})`);
116907
+ }
115792
116908
  context4.output.write(`INDEX.md regenerated (${result.index.split(`
115793
116909
  `).length} lines); ${result.tasksUpdated} feature Tasks region(s) updated`);
115794
116910
  }
@@ -115797,7 +116913,7 @@ function registerFeatureCommand(program2, context4) {
115797
116913
  context4.setExitCode(1);
115798
116914
  }
115799
116915
  });
115800
- feature.command("check").summary("Validate feature file(s) through the four-layer check (design \xA73).").argument("[id]", "Feature ID (validates all features in the folder when omitted)").option(...SHARED_OPTIONS.strictFeature).option(...SHARED_OPTIONS.asFeature0418).option(...SHARED_OPTIONS.folderFeatures).option("--fix", "repair structural findings in place (heading presence/level/order, R-item checkboxes)").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (id, options) => {
116916
+ feature.command("check").summary("Validate feature file(s) through the four-layer check (design \xA73).").argument("[id]", "Feature ID (validates all features in the folder when omitted)").option(...SHARED_OPTIONS.strictFeature).option(...SHARED_OPTIONS.asFeature0418).option(...SHARED_OPTIONS.folderFeatures).option("--fix", "repair structural findings in place (heading presence/level/order, R-item checkboxes)").option("--inventory <report>", "Cross-check the report requirement inventory against the AC covers lines (errors on uncovered items)").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (id, options) => {
115801
116917
  const resolved = await resolvePlanningFolders(context4.fs);
115802
116918
  const featuresDir = options.folder ?? context4.fs.resolve(resolved.featuresDir);
115803
116919
  const tasksDir = context4.fs.resolve(resolved.tasksDir);
@@ -115807,6 +116923,7 @@ function registerFeatureCommand(program2, context4) {
115807
116923
  const svc = new FeatureCheckService(context4.fs);
115808
116924
  const json4 = options.json === true;
115809
116925
  const strict = options.strict === true;
116926
+ const inventoryText = typeof options.inventory === "string" ? await Promise.resolve(context4.fs.readFile(context4.fs.resolve(options.inventory))).catch(() => "") : undefined;
115810
116927
  try {
115811
116928
  const entries = await context4.fs.readDir(featuresDir);
115812
116929
  const ids = id ? [id] : entries.map((n3) => n3.match(/^([A-Z][1-9]*)_.+\.md$/)?.[1]).filter((n3) => n3 !== undefined);
@@ -115827,7 +116944,8 @@ function registerFeatureCommand(program2, context4) {
115827
116944
  receiptRunPort: await makeReceiptRunPort(context4),
115828
116945
  severityOverrides: resolved.severityOverrides,
115829
116946
  asStatus: options.as,
115830
- fix: options.fix === true
116947
+ fix: options.fix === true,
116948
+ inventory: inventoryText
115831
116949
  });
115832
116950
  results.push(result);
115833
116951
  if (!json4) {
@@ -115874,7 +116992,8 @@ ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
115874
116992
  if (options.all) {
115875
116993
  const result = await svc.syncAllFeatures({
115876
116994
  dryRun: options.dryRun,
115877
- forceConfirm: options.force
116995
+ forceConfirm: options.force,
116996
+ force: options.force
115878
116997
  });
115879
116998
  if (options.json) {
115880
116999
  context4.output.write(toEnvelopeJson(result, { enveloped: options.jsonEnvelope }));
@@ -115888,7 +117007,8 @@ ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
115888
117007
  } else if (id) {
115889
117008
  const result = await svc.syncFeature(id, {
115890
117009
  dryRun: options.dryRun,
115891
- forceConfirm: options.force
117010
+ forceConfirm: options.force,
117011
+ force: options.force
115892
117012
  });
115893
117013
  if (options.json) {
115894
117014
  context4.output.write(toEnvelopeJson(result, { enveloped: options.jsonEnvelope }));
@@ -115903,9 +117023,6 @@ ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
115903
117023
  }
115904
117024
  });
115905
117025
  }
115906
- function isFeatureFile(name, id) {
115907
- return name.startsWith(`${id}_`) && name.endsWith(".md") && name.length > id.length + 4;
115908
- }
115909
117026
  async function makeReceiptRunPort(context4) {
115910
117027
  const db2 = await context4.getDb();
115911
117028
  const runs2 = new RunDao(db2);
@@ -115993,7 +117110,7 @@ import { createRequire } from "module";
115993
117110
  var CLI_CONFIG = {
115994
117111
  binaryName: "spur",
115995
117112
  binaryLabel: "spur",
115996
- binaryVersion: "0.3.95",
117113
+ binaryVersion: "0.3.96",
115997
117114
  configDir: ".spur",
115998
117115
  configFile: ".spur/config.yaml",
115999
117116
  databaseFile: ".spur/spur.db"
@@ -117184,7 +118301,7 @@ function registerMigrateCommand(program2, context4, options = {}) {
117184
118301
  init_src2();
117185
118302
  init_dist5();
117186
118303
  await init_src3();
117187
- import { basename as basename15, dirname as dirname26, join as join47, resolve as resolve20 } from "path";
118304
+ import { basename as basename16, dirname as dirname26, join as join47, resolve as resolve20 } from "path";
117188
118305
  function registerProjectsCommand(program2, context4) {
117189
118306
  const projectsCmd = program2.command("projects").summary("manage the Spur multi-project registry");
117190
118307
  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) => {
@@ -117193,7 +118310,7 @@ function registerProjectsCommand(program2, context4) {
117193
118310
  if (!await context4.fs.exists(absolutePath)) {
117194
118311
  throw new Error(`Directory does not exist: ${absolutePath}`);
117195
118312
  }
117196
- const name = options.name ?? basename15(absolutePath);
118313
+ const name = options.name ?? basename16(absolutePath);
117197
118314
  const registry3 = new ProjectRegistry;
117198
118315
  const entry = await registry3.upsert({ path: absolutePath, name, port: 0 });
117199
118316
  if (options.json) {
@@ -117732,7 +118849,7 @@ init_src();
117732
118849
  init_loader();
117733
118850
  init_src2();
117734
118851
  await init_src3();
117735
- import { basename as basename17, dirname as dirname28, isAbsolute as isAbsolute9, join as join51 } from "path";
118852
+ import { basename as basename18, dirname as dirname28, isAbsolute as isAbsolute9, join as join51 } from "path";
117736
118853
  init_dist5();
117737
118854
 
117738
118855
  // ../server/src/bootstrap.ts
@@ -123750,7 +124867,8 @@ var featureContract = {
123750
124867
  summary: "Refresh feature index and kanban",
123751
124868
  tags: ["feature"]
123752
124869
  }).output(apiSuccessSchema(exports_external.object({
123753
- rebuilt: exports_external.number().int().nonnegative()
124870
+ rebuilt: exports_external.number().int().nonnegative(),
124871
+ skipped: exports_external.array(exports_external.object({ id: exports_external.string(), reason: exports_external.string() }))
123754
124872
  }))),
123755
124873
  check: oc.route({
123756
124874
  method: "POST",
@@ -124451,6 +125569,9 @@ var taskSummarySchema = exports_external.object({
124451
125569
  featureId: exports_external.string().nullable().optional(),
124452
125570
  parentWbs: exports_external.string().regex(/^\d{4}$/).nullable().optional(),
124453
125571
  type: exports_external.enum(TASK_TYPES).optional(),
125572
+ template: exports_external.string().optional(),
125573
+ estimateHours: exports_external.number().optional(),
125574
+ dependencyCount: exports_external.number().optional(),
124454
125575
  filePath: exports_external.string(),
124455
125576
  updatedAt: exports_external.string().optional()
124456
125577
  });
@@ -124731,8 +125852,8 @@ function createFeatureHandlers(ctx) {
124731
125852
  return { ok: true, data: { id: input.id, status: input.toStatus } };
124732
125853
  }),
124733
125854
  refresh: os2.feature.refresh.handler(async () => {
124734
- const { tasksUpdated } = await ctx.featureService().refresh();
124735
- return { ok: true, data: { rebuilt: tasksUpdated } };
125855
+ const { tasksUpdated, skipped } = await ctx.featureService().refresh();
125856
+ return { ok: true, data: { rebuilt: tasksUpdated, skipped } };
124736
125857
  }),
124737
125858
  check: os2.feature.check.handler(async ({ input }) => {
124738
125859
  const folders = ctx.planningFolders();
@@ -124814,7 +125935,7 @@ init_src2();
124814
125935
  init_dist5();
124815
125936
  await init_src3();
124816
125937
  import { homedir as homedir12 } from "os";
124817
- import { basename as basename16, join as join48, relative as relative7, resolve as resolve22 } from "path";
125938
+ import { basename as basename17, join as join48, relative as relative7, resolve as resolve22 } from "path";
124818
125939
  var startedAt = Date.now();
124819
125940
  var OPERATOR_AGENT_ID = "board-operator";
124820
125941
  var healthModule = {
@@ -124841,7 +125962,7 @@ var healthModule = {
124841
125962
  return c3.json({ status: "error", db: "unreachable" }, 503);
124842
125963
  });
124843
125964
  app.get("/api/project", (c3) => {
124844
- return c3.json({ name: ctx ? basename16(ctx.cwd) : null, path: ctx ? normalizeProjectPath(ctx.cwd) : null });
125965
+ return c3.json({ name: ctx ? basename17(ctx.cwd) : null, path: ctx ? normalizeProjectPath(ctx.cwd) : null });
124845
125966
  });
124846
125967
  app.get("/api/project/fleet", async (c3) => {
124847
125968
  if (!ctx) {
@@ -125473,7 +126594,7 @@ var healthModule = {
125473
126594
  return c3.json({ ok: false, error: "File not found" }, 404);
125474
126595
  }
125475
126596
  const content = await fs3.readFile(fullPath);
125476
- let title2 = basename16(reqPath, ".md");
126597
+ let title2 = basename17(reqPath, ".md");
125477
126598
  const match2 = content.match(/^#\s+(.+)$/m);
125478
126599
  if (match2?.[1])
125479
126600
  title2 = match2[1].trim();
@@ -125563,7 +126684,7 @@ var healthModule = {
125563
126684
  return c3.json({ ok: false, error: "File not found" }, 404);
125564
126685
  }
125565
126686
  const content = await fs3.readFile(fullPath);
125566
- let title2 = basename16(reqPath, ".md");
126687
+ let title2 = basename17(reqPath, ".md");
125567
126688
  const match2 = content.match(/^#\s+(.+)$/m);
125568
126689
  if (match2?.[1])
125569
126690
  title2 = match2[1].trim();
@@ -126419,6 +127540,8 @@ function createTaskHandlers(ctx) {
126419
127540
  const tasks = await ctx.taskService().list(filters);
126420
127541
  const data = tasks.map((t) => {
126421
127542
  const fm = t.frontmatter ?? {};
127543
+ const estimateHours = typeof fm.estimate_hours === "number" ? fm.estimate_hours : typeof fm.estimated_hours === "number" ? fm.estimated_hours : undefined;
127544
+ const deps = Array.isArray(fm.dependencies) ? fm.dependencies : [];
126422
127545
  return {
126423
127546
  wbs: t.wbs,
126424
127547
  name: t.name,
@@ -126427,6 +127550,9 @@ function createTaskHandlers(ctx) {
126427
127550
  featureId: fm.feature_id ?? undefined,
126428
127551
  parentWbs: fm.parent_wbs ?? undefined,
126429
127552
  type: TASK_TYPES.includes(fm.type) ? fm.type : undefined,
127553
+ template: typeof fm.template === "string" ? fm.template : undefined,
127554
+ estimateHours,
127555
+ dependencyCount: deps.length > 0 ? deps.length : undefined,
126430
127556
  filePath: t.filePath,
126431
127557
  updatedAt: fm.updated_at
126432
127558
  };
@@ -129453,7 +130579,7 @@ async function startServer(options, deps = defaultDeps) {
129453
130579
  });
129454
130580
  const projectRegistry = new ProjectRegistry;
129455
130581
  const projectCwd = projectRoot;
129456
- const projectName = basename17(projectCwd);
130582
+ const projectName = basename18(projectCwd);
129457
130583
  try {
129458
130584
  const existingProject = await projectRegistry.getByPath(projectCwd);
129459
130585
  await projectRegistry.upsert({
@@ -129732,7 +130858,7 @@ init_src();
129732
130858
  init_dist7();
129733
130859
  init_dist5();
129734
130860
  await init_src3();
129735
- import { closeSync as closeSync6, fstatSync as fstatSync4, openSync as openSync6, readFileSync as readFileSync15, readSync as readSync3 } from "fs";
130861
+ import { closeSync as closeSync7, fstatSync as fstatSync4, openSync as openSync7, readFileSync as readFileSync16, readSync as readSync3 } from "fs";
129736
130862
  import { join as join54 } from "path";
129737
130863
  import { createInterface } from "readline";
129738
130864
  import { setTimeout as sleep6 } from "timers/promises";
@@ -130860,7 +131986,7 @@ function isTerminalTraceStatus(status) {
130860
131986
  function readRunLogChunk(logPath, offset) {
130861
131987
  let fd;
130862
131988
  try {
130863
- fd = openSync6(logPath, "r");
131989
+ fd = openSync7(logPath, "r");
130864
131990
  } catch {
130865
131991
  return { exists: false, lines: [], offset };
130866
131992
  }
@@ -130879,7 +132005,7 @@ function readRunLogChunk(logPath, offset) {
130879
132005
  const lines = parts.length > 0 && parts.at(-1) === "" ? parts.slice(0, -1) : parts;
130880
132006
  return { exists: true, lines, offset: offset + consumed };
130881
132007
  } finally {
130882
- closeSync6(fd);
132008
+ closeSync7(fd);
130883
132009
  }
130884
132010
  }
130885
132011
  async function followRunLog(service, runId, dir, pollMs, write, wait = (ms) => sleep6(ms), now2 = Date.now, deadlineMs) {
@@ -130901,7 +132027,7 @@ async function followRunLog(service, runId, dir, pollMs, write, wait = (ms) => s
130901
132027
  offset = 0;
130902
132028
  if (carried !== "") {
130903
132029
  try {
130904
- const nextText = readFileSync15(target, "utf8");
132030
+ const nextText = readFileSync16(target, "utf8");
130905
132031
  if (nextText.startsWith(carried))
130906
132032
  offset = Buffer.byteLength(carried);
130907
132033
  } catch {