@runfusion/fusion 0.78.0-beta.2 → 0.78.0-beta.3

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 (139) hide show
  1. package/dist/bin.js +1150 -285
  2. package/dist/child-process-worker.js +760 -203
  3. package/dist/client/.vite/manifest.json +253 -253
  4. package/dist/client/assets/{AgentDetailView-4tEdPTG2.js → AgentDetailView-DAtUBPar.js} +1 -1
  5. package/dist/client/assets/{AgentPermissionPolicyEditor-DIGxjQFE.js → AgentPermissionPolicyEditor-Cth5Qyvz.js} +1 -1
  6. package/dist/client/assets/{AgentsView-BXFYQAPe.js → AgentsView-D6odi29-.js} +3 -3
  7. package/dist/client/assets/{CommandCenter-vknNrxBN.js → CommandCenter-DUlZFMvJ.js} +1 -1
  8. package/dist/client/assets/{DevServerView-COFb49kW.js → DevServerView-GD1-vwId.js} +1 -1
  9. package/dist/client/assets/{DirectoryPicker-C-wYRUc5.js → DirectoryPicker-De3Q7_zu.js} +1 -1
  10. package/dist/client/assets/{DocumentsView-B-uzhK3C.js → DocumentsView-BMYBV8DV.js} +1 -1
  11. package/dist/client/assets/{EvalsView-BcOgXvPe.js → EvalsView-CYfclysr.js} +1 -1
  12. package/dist/client/assets/{ExperimentalAgentOnboardingModal-B0zF29ER.js → ExperimentalAgentOnboardingModal-B1dQuReY.js} +1 -1
  13. package/dist/client/assets/{GoalsView--Cj2se_t.js → GoalsView-BHxjegdt.js} +1 -1
  14. package/dist/client/assets/{InsightsView-qMoM8oFe.js → InsightsView-DK0EP68R.js} +1 -1
  15. package/dist/client/assets/{MemoryView-Ca0Y2fYg.js → MemoryView-O8vfqipX.js} +1 -1
  16. package/dist/client/assets/{PatchnodeView--E259o36.js → PatchnodeView-WM5ZlnTS.js} +1 -1
  17. package/dist/client/assets/{PiExtensionsManager-DKKLn5YE.js → PiExtensionsManager-BrTit_vk.js} +1 -1
  18. package/dist/client/assets/{PluginManager-e0Rnv7RI.js → PluginManager-DBUm9Q30.js} +1 -1
  19. package/dist/client/assets/{PullRequestView-BFe77u-d.js → PullRequestView-DW9EqNT3.js} +1 -1
  20. package/dist/client/assets/{ReportModal-DvyDEj8D.js → ReportModal-Cv7Lxy1u.js} +1 -1
  21. package/dist/client/assets/{ResearchView-DROedGZR.js → ResearchView-DDGbBXlh.js} +1 -1
  22. package/dist/client/assets/{SecretsView-DTrrJEt9.js → SecretsView-C2K-n5_X.js} +1 -1
  23. package/dist/client/assets/{SettingsModal-CSbSmwxZ.js → SettingsModal-C7npF9nB.js} +3 -3
  24. package/dist/client/assets/{SettingsModal-DnTX4rMK.js → SettingsModal-Crpg0GmF.js} +1 -1
  25. package/dist/client/assets/{SettingsTextareaRow-y3gQ4u_G.js → SettingsTextareaRow-B7FIDTtF.js} +1 -1
  26. package/dist/client/assets/{SetupWizardModal-ClAbtrU0.js → SetupWizardModal-CBWFrde9.js} +2 -2
  27. package/dist/client/assets/{SkillsView-BpEJE16a.js → SkillsView-B1l4qkYj.js} +1 -1
  28. package/dist/client/assets/{WorkflowNodeEditor-CSbhpY5K.js → WorkflowNodeEditor-e122mQ0Z.js} +1 -1
  29. package/dist/client/assets/{agent-import-generation-BT0ZBGQA.js → agent-import-generation-CX_NVRPC.js} +1 -1
  30. package/dist/client/assets/{agentsOrgChartLayout-C_tkb_Pq.js → agentsOrgChartLayout-BAhAecHd.js} +1 -1
  31. package/dist/client/assets/{app-C8kudFlj.js → app-BO1iuWOt.js} +1 -1
  32. package/dist/client/assets/{app-B0hINWeH.js → app-BRFfR55q.js} +1 -1
  33. package/dist/client/assets/{app-BSxI9vvS.js → app-BW03hpJV.js} +3 -3
  34. package/dist/client/assets/{app-C8jogXBu.js → app-B_skBURN.js} +1 -1
  35. package/dist/client/assets/{app-hEhR2PdL.js → app-C0JL4IcI.js} +3 -3
  36. package/dist/client/assets/{app-BeDYumHl.js → app-C5uZ_9kI.js} +3 -3
  37. package/dist/client/assets/app-DxFiSzSJ.js +5 -0
  38. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BfPYqDRS.js → architectureDiagram-3BPJPVTR-BOmFDW7M.js} +1 -1
  39. package/dist/client/assets/{blockDiagram-GPEHLZMM-BEcaGTmc.js → blockDiagram-GPEHLZMM-D7CnZ9-e.js} +1 -1
  40. package/dist/client/assets/{c4Diagram-AAUBKEIU-BX1xrinF.js → c4Diagram-AAUBKEIU-CQZw8PeP.js} +1 -1
  41. package/dist/client/assets/channel-Drj7VQBu.js +1 -0
  42. package/dist/client/assets/{chunk-2J33WTMH-OksyoE_c.js → chunk-2J33WTMH-DBS2o2AZ.js} +1 -1
  43. package/dist/client/assets/{chunk-4BX2VUAB-C5ivKrB5.js → chunk-4BX2VUAB-CFKEnTmJ.js} +1 -1
  44. package/dist/client/assets/{chunk-55IACEB6-C7vkVq-W.js → chunk-55IACEB6-DpjYRScs.js} +1 -1
  45. package/dist/client/assets/{chunk-727SXJPM-c-khhz21.js → chunk-727SXJPM-Db7lQvPG.js} +1 -1
  46. package/dist/client/assets/{chunk-AQP2D5EJ-DwxYJXXQ.js → chunk-AQP2D5EJ-rSGEvDdj.js} +1 -1
  47. package/dist/client/assets/{chunk-FMBD7UC4-BKhdu4up.js → chunk-FMBD7UC4-Cdi9zNME.js} +1 -1
  48. package/dist/client/assets/{chunk-ND2GUHAM-okVZM2p2.js → chunk-ND2GUHAM-D_3BQZ27.js} +1 -1
  49. package/dist/client/assets/{chunk-QZHKN3VN-DDso7rM3.js → chunk-QZHKN3VN-DnVoTnz9.js} +1 -1
  50. package/dist/client/assets/classDiagram-4FO5ZUOK-DGxwjUYU.js +1 -0
  51. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DGxwjUYU.js +1 -0
  52. package/dist/client/assets/{cose-bilkent-S5V4N54A-CNT9ltBk.js → cose-bilkent-S5V4N54A-HQFw9frh.js} +1 -1
  53. package/dist/client/assets/{dagre-BM42HDAG-C5a30-g5.js → dagre-BM42HDAG-BdHp0sc2.js} +1 -1
  54. package/dist/client/assets/{dashboard-view-Brqe-SVE.js → dashboard-view-C3vvo4zP.js} +1 -1
  55. package/dist/client/assets/{dashboard-view-DxpkE7dL.js → dashboard-view-CoxDPI4m.js} +1 -1
  56. package/dist/client/assets/{dashboard-view-WFOf0Kpl.js → dashboard-view-DaqL1Ufr.js} +1 -1
  57. package/dist/client/assets/{dashboard-view-DyAQ_agZ.js → dashboard-view-mWhkEiwE.js} +1 -1
  58. package/dist/client/assets/{diagram-2AECGRRQ-DG7EyUNe.js → diagram-2AECGRRQ-BDu9AETT.js} +1 -1
  59. package/dist/client/assets/{diagram-5GNKFQAL-DE7lhEna.js → diagram-5GNKFQAL-2qzqfWTz.js} +1 -1
  60. package/dist/client/assets/{diagram-KO2AKTUF-DFtJNqNF.js → diagram-KO2AKTUF-DYa-tplS.js} +1 -1
  61. package/dist/client/assets/{diagram-LMA3HP47-DUAuxZQD.js → diagram-LMA3HP47-ChwcQccK.js} +1 -1
  62. package/dist/client/assets/{diagram-OG6HWLK6-DCKETcWy.js → diagram-OG6HWLK6-DoGKft5E.js} +1 -1
  63. package/dist/client/assets/{erDiagram-TEJ5UH35-pa0blyAD.js → erDiagram-TEJ5UH35-5dsSgUXk.js} +1 -1
  64. package/dist/client/assets/{flowDiagram-I6XJVG4X-D7Veqs1W.js → flowDiagram-I6XJVG4X-BOHB4RzL.js} +1 -1
  65. package/dist/client/assets/{folder-open-CojKP0zy.js → folder-open-BQcY8jvq.js} +1 -1
  66. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CsqBMy2D.js → ganttDiagram-6RSMTGT7-BD3aivUG.js} +1 -1
  67. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-BRrVKZJo.js → gitGraphDiagram-PVQCEYII-HBZEAECF.js} +1 -1
  68. package/dist/client/assets/{index-Dliz9s5K.css → index-26HUNwwJ.css} +1 -1
  69. package/dist/client/assets/{index-Cv_ouAgx.js → index-QU8sENvc.js} +298 -298
  70. package/dist/client/assets/{infoDiagram-5YYISTIA-o2GEmmb2.js → infoDiagram-5YYISTIA-d7hsqM0V.js} +1 -1
  71. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-k1p6nWtj.js → ishikawaDiagram-YF4QCWOH-k4Dnhc_y.js} +1 -1
  72. package/dist/client/assets/{journeyDiagram-JHISSGLW-B43V37S0.js → journeyDiagram-JHISSGLW-Dh3yqZPQ.js} +1 -1
  73. package/dist/client/assets/{kanban-definition-UN3LZRKU-HRMYyAQB.js → kanban-definition-UN3LZRKU-BYqpe7Ju.js} +1 -1
  74. package/dist/client/assets/{mermaid.core-6vt3zKxL.js → mermaid.core-D5Ccw5tQ.js} +4 -4
  75. package/dist/client/assets/{mindmap-definition-RKZ34NQL-D3VJuvfj.js → mindmap-definition-RKZ34NQL-D64P3Mc4.js} +1 -1
  76. package/dist/client/assets/{pieDiagram-4H26LBE5-D0RjFhk9.js → pieDiagram-4H26LBE5-CtMfKnlZ.js} +1 -1
  77. package/dist/client/assets/{puzzle-D3dJEsTf.js → puzzle-D4y4g-nb.js} +1 -1
  78. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DHaEqU3Q.js → quadrantDiagram-W4KKPZXB-B5gSMr9M.js} +1 -1
  79. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DnfFCRP0.js → requirementDiagram-4Y6WPE33-DKtBfWDi.js} +1 -1
  80. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-D6GCuiqV.js → sankeyDiagram-5OEKKPKP-BcWeBcNk.js} +1 -1
  81. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DaGYYzxS.js → sequenceDiagram-3UESZ5HK-Crp7Mc0M.js} +1 -1
  82. package/dist/client/assets/{standing-instructions-template-CN8Kp4Hq.js → standing-instructions-template-lD5Vcanq.js} +1 -1
  83. package/dist/client/assets/{stateDiagram-AJRCARHV-ek4Wb8ai.js → stateDiagram-AJRCARHV-CA5FEPYe.js} +1 -1
  84. package/dist/client/assets/{stateDiagram-v2-BHNVJYJU-EPn8Vq7v.js → stateDiagram-v2-BHNVJYJU-Bo75sO6t.js} +1 -1
  85. package/dist/client/assets/{timeline-definition-PNZ67QCA-CyAwCbnE.js → timeline-definition-PNZ67QCA-Dmr4NeTD.js} +1 -1
  86. package/dist/client/assets/{upload-BhpNL3xI.js → upload-CzJ1o8Px.js} +1 -1
  87. package/dist/client/assets/{users-Bka1cIkR.js → users-BMnytPKO.js} +1 -1
  88. package/dist/client/assets/{vennDiagram-CIIHVFJN-Bd8-8r3a.js → vennDiagram-CIIHVFJN-DfbBSNgT.js} +1 -1
  89. package/dist/client/assets/{wardley-L42UT6IY-nNvWflNF.js → wardley-L42UT6IY-Wxq1L5VU.js} +1 -1
  90. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Dy7XrvPx.js → wardleyDiagram-YWT4CUSO-D6HnIS8j.js} +1 -1
  91. package/dist/client/assets/{xychartDiagram-2RQKCTM6-38VTptR4.js → xychartDiagram-2RQKCTM6-5MfFlu30.js} +1 -1
  92. package/dist/client/index.html +2 -2
  93. package/dist/client/version.json +1 -1
  94. package/dist/droid-cli/package.json +1 -1
  95. package/dist/extension.js +844 -264
  96. package/dist/pi-claude-cli/package.json +1 -1
  97. package/dist/plugin-sdk/index.d.ts +39 -11
  98. package/dist/plugin-sdk/index.js +2 -2
  99. package/dist/plugins/fusion-plugin-acp-runtime/bundled.js +2 -2
  100. package/dist/plugins/fusion-plugin-acp-runtime/package.json +1 -1
  101. package/dist/plugins/fusion-plugin-claude-runtime/bundled.js +2 -2
  102. package/dist/plugins/fusion-plugin-claude-runtime/package.json +1 -1
  103. package/dist/plugins/fusion-plugin-cli-printing-press/bundled.js +2 -2
  104. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +1 -1
  105. package/dist/plugins/fusion-plugin-compound-engineering/bundled.js +2 -2
  106. package/dist/plugins/fusion-plugin-compound-engineering/package.json +1 -1
  107. package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +2 -2
  108. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  109. package/dist/plugins/fusion-plugin-dependency-graph/bundled.js +2 -2
  110. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  111. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +2 -2
  112. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  113. package/dist/plugins/fusion-plugin-grok-runtime/bundled.js +2 -2
  114. package/dist/plugins/fusion-plugin-grok-runtime/package.json +1 -1
  115. package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +2 -2
  116. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  117. package/dist/plugins/fusion-plugin-linear-import/bundled.js +2 -2
  118. package/dist/plugins/fusion-plugin-linear-import/package.json +1 -1
  119. package/dist/plugins/fusion-plugin-omp-runtime/bundled.js +2 -2
  120. package/dist/plugins/fusion-plugin-omp-runtime/package.json +1 -1
  121. package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +2 -2
  122. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  123. package/dist/plugins/fusion-plugin-paperclip-runtime/bundled.js +2 -2
  124. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  125. package/dist/plugins/fusion-plugin-quality/bundled.js +2 -2
  126. package/dist/plugins/fusion-plugin-quality/package.json +1 -1
  127. package/dist/plugins/fusion-plugin-reports/bundled.js +2 -2
  128. package/dist/plugins/fusion-plugin-reports/package.json +1 -1
  129. package/dist/plugins/fusion-plugin-roadmap/bundled.js +2 -2
  130. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  131. package/dist/plugins/fusion-plugin-todos/bundled.js +2 -2
  132. package/dist/plugins/fusion-plugin-todos/package.json +1 -1
  133. package/dist/plugins/fusion-plugin-whatsapp-chat/bundled.js +2 -2
  134. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  135. package/package.json +1 -1
  136. package/dist/client/assets/app-DZ5j1n3j.js +0 -5
  137. package/dist/client/assets/channel-BjDq20V4.js +0 -1
  138. package/dist/client/assets/classDiagram-4FO5ZUOK-B114c_Fi.js +0 -1
  139. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B114c_Fi.js +0 -1
@@ -1919,10 +1919,11 @@ function toSnapshot(result) {
1919
1919
  const { priorAttempts: _drop, ...rest } = result;
1920
1920
  return rest;
1921
1921
  }
1922
- function archiveTerminalWorkflowStepFailures(results, archivedAt = (/* @__PURE__ */ new Date()).toISOString()) {
1923
- if (!results?.some(isTerminalFailure)) return results;
1922
+ function archiveTerminalWorkflowStepFailures(results, archivedAt = (/* @__PURE__ */ new Date()).toISOString(), options = {}) {
1923
+ const inScope = (result) => isTerminalFailure(result) && (!options.workflowStepIds || options.workflowStepIds.has(result.workflowStepId));
1924
+ if (!results?.some(inScope)) return results;
1924
1925
  return results.map((result) => {
1925
- if (!isTerminalFailure(result)) return result;
1926
+ if (!inScope(result)) return result;
1926
1927
  const snapshot = toSnapshot(result);
1927
1928
  const priorAttempts = [snapshot, ...result.priorAttempts ?? []].slice(0, MAX_WORKFLOW_STEP_PRIOR_ATTEMPTS);
1928
1929
  const {
@@ -2098,6 +2099,26 @@ function isAuditedOperatorBypass(result) {
2098
2099
  actor && !AUTOMATED_BYPASS_ACTORS.has(actor) && result.bypassedAt?.trim() && result.bypassReason?.trim()
2099
2100
  );
2100
2101
  }
2102
+ function resolveCollateralArchivedReviewGate(result, options) {
2103
+ const archivedFrom = result.remediationArchivedFromStatus;
2104
+ if ((result.phase ?? "pre-merge") !== "pre-merge" || result.status !== "skipped" || result.remediationArchivedAt == null || result.bypassedBy !== void 0 || archivedFrom !== "failed" && archivedFrom !== "advisory_failure" || options.remediationGateIds.has(result.workflowStepId)) {
2105
+ return void 0;
2106
+ }
2107
+ const {
2108
+ remediationArchivedAt: _archivedAt,
2109
+ remediationArchivedFromStatus: _archivedFrom,
2110
+ ...rest
2111
+ } = result;
2112
+ return {
2113
+ restored: {
2114
+ ...rest,
2115
+ status: archivedFrom,
2116
+ output: COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC,
2117
+ notes: COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC
2118
+ },
2119
+ reason: COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC
2120
+ };
2121
+ }
2101
2122
  function resolveUnprovenReviewApproval(result, options) {
2102
2123
  if ((result.phase ?? "pre-merge") !== "pre-merge" || !requiresContentReviewProof(result.workflowStepId, result) || options.workspace || result.status !== "passed" || result.verdict !== "APPROVE" && result.verdict !== "APPROVE_WITH_NOTES" || result.reviewInputFingerprint !== void 0 || result.bypassedBy !== void 0 || result.remediationArchivedAt != null) {
2103
2124
  return void 0;
@@ -2129,9 +2150,10 @@ function evaluateStep(workflowStepId, results, task, descriptor) {
2129
2150
  const isPlanDomain = workflowStepId === PLAN_REVIEW_GROUP_ID || result.reviewKind === "plan";
2130
2151
  const notRunApproves = isWorkflowStepNotRun(result) && !requiresAuthoredVerdict && !isPlanDomain;
2131
2152
  const approved = result.status === "passed" && (requiresAuthoredVerdict ? approvedVerdict : result.verdict === void 0 || approvedVerdict) || result.status === "skipped" && !!result.bypassedBy || notRunApproves;
2132
- if (!approved || !!result.remediationArchivedAt) return { workflowStepId, state: "not-approved" };
2153
+ const auditedOperatorWaiver = isAuditedOperatorBypass(result) && descriptor?.kind !== "workspace";
2154
+ if (!approved || result.remediationArchivedAt != null && !auditedOperatorWaiver) return { workflowStepId, state: "not-approved" };
2133
2155
  if (result.reviewKind === "plan") return { workflowStepId, state: "approved" };
2134
- if (isAuditedOperatorBypass(result) && descriptor?.kind !== "workspace") {
2156
+ if (auditedOperatorWaiver) {
2135
2157
  return { workflowStepId, state: "approved" };
2136
2158
  }
2137
2159
  const bindsContent = requiresExplicitVerdict || result.reviewInputFingerprint !== void 0;
@@ -2164,7 +2186,7 @@ function evaluateStep(workflowStepId, results, task, descriptor) {
2164
2186
  if (stale.length) return { workflowStepId, state: "stale-content", repositories: stale };
2165
2187
  return { workflowStepId, state: "approved" };
2166
2188
  }
2167
- var AUTOMATED_BYPASS_ACTORS, UNPROVEN_REVIEW_APPROVAL_DIAGNOSTIC;
2189
+ var AUTOMATED_BYPASS_ACTORS, UNPROVEN_REVIEW_APPROVAL_DIAGNOSTIC, COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC;
2168
2190
  var init_pre_merge_approval = __esm({
2169
2191
  "../core/src/merge/pre-merge-approval.ts"() {
2170
2192
  "use strict";
@@ -2173,6 +2195,7 @@ var init_pre_merge_approval = __esm({
2173
2195
  init_workflow_step_results();
2174
2196
  AUTOMATED_BYPASS_ACTORS = /* @__PURE__ */ new Set([FAST_MODE_BYPASS_ACTOR]);
2175
2197
  UNPROVEN_REVIEW_APPROVAL_DIAGNOSTIC = "Content-binding review approval recorded without reviewInputFingerprint; approval invalidated so the gate can run again.";
2198
+ COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC = "Gate archived as collateral of another gate's remediation and restored to its recoverable failed state by self-healing. No reviewer verdict was fabricated; re-run or bypass this gate to clear the merge door.";
2176
2199
  }
2177
2200
  });
2178
2201
 
@@ -2281,6 +2304,9 @@ function hasNonTerminalSteps(task) {
2281
2304
  function isPreMergeStepsNotRunBlocker(blocker) {
2282
2305
  return blocker === PRE_MERGE_STEPS_NOT_RUN_BLOCKER;
2283
2306
  }
2307
+ function isStaleContentApprovalBlocker(blocker) {
2308
+ return typeof blocker === "string" && blocker.trim().endsWith(STALE_CONTENT_APPROVAL_BLOCKER);
2309
+ }
2284
2310
  function getTaskMergeBlocker(task, options = {}) {
2285
2311
  if (!options.skipColumnIdentityCheck) {
2286
2312
  const reviewColumns = options.reviewColumns?.size ? options.reviewColumns : void 0;
@@ -2302,8 +2328,10 @@ function getTaskMergeBlocker(task, options = {}) {
2302
2328
  }
2303
2329
  const approval = evaluatePreMergeApprovals(task, options).find((candidate) => candidate.state !== "approved");
2304
2330
  if (approval?.state === "missing") return PRE_MERGE_STEPS_NOT_RUN_BLOCKER;
2305
- if (approval?.state === "not-approved") return "task has enabled pre-merge workflow steps without a current approval";
2306
- if (approval?.state === "stale-content") return "task has a pre-merge approval recorded against different content";
2331
+ if (approval?.state === "not-approved") {
2332
+ return `task has enabled pre-merge workflow steps without a current approval (gate '${approval.workflowStepId}')`;
2333
+ }
2334
+ if (approval?.state === "stale-content") return STALE_CONTENT_APPROVAL_BLOCKER;
2307
2335
  if (approval?.state === "unprovable-content") return "task has no provable approval for the content being merged";
2308
2336
  if (task.workflowStepResults?.some((result) => {
2309
2337
  const phase = result.phase || "pre-merge";
@@ -2320,11 +2348,14 @@ function getTaskMergeBlocker(task, options = {}) {
2320
2348
  return void 0;
2321
2349
  }
2322
2350
  function getLatestFailedPreMergeReviewStep(task) {
2323
- return (task.workflowStepResults ?? []).filter((result) => (result.phase || "pre-merge") === "pre-merge" && result.status === "failed").sort((a, b3) => {
2351
+ const results = task.workflowStepResults ?? [];
2352
+ const recentFirst = (a, b3) => {
2324
2353
  const aTs = Date.parse(a.completedAt || a.startedAt || "");
2325
2354
  const bTs = Date.parse(b3.completedAt || b3.startedAt || "");
2326
2355
  return (Number.isFinite(bTs) ? bTs : 0) - (Number.isFinite(aTs) ? aTs : 0);
2327
- })[0];
2356
+ };
2357
+ const isPreMerge = (result) => (result.phase || "pre-merge") === "pre-merge";
2358
+ return results.filter((result) => isPreMerge(result) && result.status === "failed").sort(recentFirst)[0] ?? results.filter((result) => isPreMerge(result) && isArchivedRemediationCarrier(result) && (result.remediationArchivedFromStatus === "failed" || result.remediationArchivedFromStatus === "advisory_failure") && !result.bypassedBy && !result.supersededAt).sort(recentFirst)[0];
2328
2359
  }
2329
2360
  function clearMergeConfirmedTransientStatus(status) {
2330
2361
  return status !== void 0 && MERGE_CONFIRMED_TRANSIENT_STATUSES.has(status) ? void 0 : status;
@@ -2408,13 +2439,14 @@ function findPendingPreMergeStep(task) {
2408
2439
  (a, b3) => new Date(b3.startedAt ?? 0).getTime() - new Date(a.startedAt ?? 0).getTime()
2409
2440
  )[0];
2410
2441
  }
2411
- var FUSION_SIBLING_BRANCH_RE, AWAITING_APPROVAL_PAUSE_REASON, HARD_BLOCKING_TASK_STATUSES, SCHEDULER_TRANSIENT_STATUSES, BLOCKING_TASK_STATUSES, NON_TERMINAL_STEP_STATUSES, NON_TERMINAL_WORKFLOW_STATUSES, PRE_MERGE_STEPS_NOT_RUN_BLOCKER, PreMergeStepsNotRunError, REVIEW_ELIGIBLE_SENTINEL_COLUMN, MERGE_CONFIRMED_TRANSIENT_STATUSES;
2442
+ var FUSION_SIBLING_BRANCH_RE, AWAITING_APPROVAL_PAUSE_REASON, HARD_BLOCKING_TASK_STATUSES, SCHEDULER_TRANSIENT_STATUSES, BLOCKING_TASK_STATUSES, NON_TERMINAL_STEP_STATUSES, NON_TERMINAL_WORKFLOW_STATUSES, PRE_MERGE_STEPS_NOT_RUN_BLOCKER, STALE_CONTENT_APPROVAL_BLOCKER, PreMergeStepsNotRunError, REVIEW_ELIGIBLE_SENTINEL_COLUMN, MERGE_CONFIRMED_TRANSIENT_STATUSES;
2412
2443
  var init_task_merge = __esm({
2413
2444
  "../core/src/merge/task-merge.ts"() {
2414
2445
  "use strict";
2415
2446
  init_branch_group_completion();
2416
2447
  init_task_helpers();
2417
2448
  init_pre_merge_approval();
2449
+ init_workflow_step_results();
2418
2450
  FUSION_SIBLING_BRANCH_RE = /^fusion\/fn-/i;
2419
2451
  AWAITING_APPROVAL_PAUSE_REASON = "awaiting-approval";
2420
2452
  HARD_BLOCKING_TASK_STATUSES = /* @__PURE__ */ new Set([
@@ -2458,6 +2490,7 @@ var init_task_merge = __esm({
2458
2490
  "pending"
2459
2491
  ]);
2460
2492
  PRE_MERGE_STEPS_NOT_RUN_BLOCKER = "task has enabled pre-merge workflow steps that never ran";
2493
+ STALE_CONTENT_APPROVAL_BLOCKER = "task has a pre-merge approval recorded against different content";
2461
2494
  PreMergeStepsNotRunError = class extends Error {
2462
2495
  code = "pre-merge-steps-not-run";
2463
2496
  taskId;
@@ -6437,8 +6470,9 @@ function renderValidationCause(diagnostics) {
6437
6470
  }
6438
6471
  function featureValidationRepairEligibility(feature) {
6439
6472
  const repairableLoop = feature.loopState === "blocked" || feature.loopState === "needs_fix";
6473
+ const unvalidatedPassedMarker = feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId;
6440
6474
  return {
6441
- clear: repairableLoop || feature.status === "blocked",
6475
+ clear: repairableLoop || feature.status === "blocked" || unvalidatedPassedMarker,
6442
6476
  reRun: repairableLoop || feature.status === "blocked" && (feature.loopState === void 0 || feature.loopState === "idle")
6443
6477
  };
6444
6478
  }
@@ -6502,7 +6536,8 @@ var init_mission_types = __esm({
6502
6536
  implementing: [],
6503
6537
  validating: [],
6504
6538
  needs_fix: ["idle", "implementing"],
6505
- passed: [],
6539
+ /* FNXC:Missions 2026-09-05-22:07: Explicit repair may return a fabricated unvalidated passed marker to idle (issue #3574). */
6540
+ passed: ["idle"],
6506
6541
  blocked: ["idle", "implementing"]
6507
6542
  };
6508
6543
  VALIDATOR_RUN_STATUSES = ["running", "passed", "failed", "blocked", "error"];
@@ -13609,7 +13644,7 @@ function isBlockingFinding(finding, threshold) {
13609
13644
  if (!finding.severity) return true;
13610
13645
  return SEVERITY_RANK[finding.severity] >= SEVERITY_RANK[threshold];
13611
13646
  }
13612
- function applyReviewSeverityGate({ verdict, findings, threshold }) {
13647
+ function applyReviewSeverityGate({ verdict, findings, threshold, findingsUnreadable }) {
13613
13648
  const all = findings ?? [];
13614
13649
  const open3 = all.filter(isActionableReviewFinding);
13615
13650
  const resolved = all.filter((finding) => !isActionableReviewFinding(finding));
@@ -13617,6 +13652,7 @@ function applyReviewSeverityGate({ verdict, findings, threshold }) {
13617
13652
  const advisory = open3.filter((finding) => !isBlockingFinding(finding, threshold));
13618
13653
  if (verdict !== "REVISE") return { verdict, downgraded: false, blocking, advisory, resolved };
13619
13654
  if (blocking.length > 0) return { verdict, downgraded: false, blocking, advisory, resolved };
13655
+ if (findingsUnreadable) return { verdict, downgraded: false, blocking, advisory, resolved };
13620
13656
  return { verdict: "APPROVE_WITH_NOTES", downgraded: true, blocking, advisory, resolved };
13621
13657
  }
13622
13658
  function formatFindingsByPriority(findings) {
@@ -18358,6 +18394,18 @@ function detectWorkflowDrift(ir2, pin) {
18358
18394
  function defaultCodingWorkflowIr() {
18359
18395
  return resolveDefaultWorkflowIr();
18360
18396
  }
18397
+ async function prefetchWorkflowSelections(store, taskIds, cache) {
18398
+ const missingIds = [...new Set(taskIds)].filter((id) => !cache.has(id));
18399
+ if (missingIds.length === 0) return { batched: 0, singles: 0 };
18400
+ if (!store.getTaskWorkflowSelectionsAsync) return { batched: 0, singles: missingIds.length };
18401
+ try {
18402
+ const selections = await store.getTaskWorkflowSelectionsAsync(missingIds);
18403
+ for (const id of missingIds) cache.set(id, selections.get(id));
18404
+ return { batched: 1, singles: 0 };
18405
+ } catch {
18406
+ return { batched: 0, singles: missingIds.length };
18407
+ }
18408
+ }
18361
18409
  function resolveSeamPromptFromIr(ir2, seam) {
18362
18410
  for (const node of ir2.nodes) {
18363
18411
  if (node.kind !== "prompt") continue;
@@ -18576,9 +18624,9 @@ function resolveLifecycleColumns(ir2) {
18576
18624
  archived: first(LIFECYCLE_ROLE_FLAGS.archived)
18577
18625
  };
18578
18626
  }
18579
- async function resolveTaskLifecycleColumns(store, taskId, cache) {
18627
+ async function resolveTaskLifecycleColumns(store, taskId, cache, selectionCache) {
18580
18628
  try {
18581
- const ir2 = await resolveWorkflowIrForTask(store, taskId, cache);
18629
+ const ir2 = await resolveWorkflowIrForTask(store, taskId, cache, selectionCache);
18582
18630
  return resolveLifecycleColumns(ir2);
18583
18631
  } catch {
18584
18632
  return void 0;
@@ -30574,18 +30622,33 @@ function isValidPluginParserId(id) {
30574
30622
  const seg = /^[a-z0-9-]+$/;
30575
30623
  return seg.test(parts[1]) && seg.test(parts[2]);
30576
30624
  }
30577
- function parseStepHeadings(content) {
30578
- const steps = [];
30625
+ function matchStepHeadings(content) {
30579
30626
  const stepRegex = /^###\s+Step\s+\d+[^:]*:\s*(.+)$/gm;
30580
- const annotatedRegex = /^###\s+Step\s+\d+\s*\(depends:\s*([^)]*)\)\s*:\s*([^\n]+)$/;
30581
30627
  const matches = [];
30582
30628
  let match;
30583
30629
  while ((match = stepRegex.exec(content)) !== null) {
30584
30630
  const heading = /^###\s+Step\s+(\d+)/.exec(match[0]);
30585
- if (heading) {
30586
- matches.push({ full: match[0], name: match[1], headingNumber: Number(heading[1]) });
30587
- }
30631
+ if (!heading) continue;
30632
+ const index2 = match.index;
30633
+ const newline = content.indexOf("\n", index2);
30634
+ matches.push({
30635
+ headingNumber: Number(heading[1]),
30636
+ index: index2,
30637
+ headingLineEnd: newline === -1 ? content.length : newline,
30638
+ match: match[0]
30639
+ });
30588
30640
  }
30641
+ return matches;
30642
+ }
30643
+ function parseStepHeadings(content) {
30644
+ const steps = [];
30645
+ const annotatedRegex = /^###\s+Step\s+\d+\s*\(depends:\s*([^)]*)\)\s*:\s*([^\n]+)$/;
30646
+ const matches = matchStepHeadings(content).map((entry) => ({
30647
+ full: entry.match,
30648
+ name: /^###\s+Step\s+\d+[^:]*:\s*(.+)$/m.exec(entry.match)?.[1] ?? "",
30649
+ headingNumber: entry.headingNumber
30650
+ }));
30651
+ let match;
30589
30652
  const offset = resolveAuthoredStepHeadingOffset(matches.map((entry) => entry.headingNumber));
30590
30653
  for (const { full, name: legacyName } of matches) {
30591
30654
  if (!full.includes("(depends:")) {
@@ -75182,8 +75245,8 @@ function appendAgentLogEntriesSync(taskDir, entries) {
75182
75245
  if (entries.length === 0) return [];
75183
75246
  const filePath = getAgentLogFilePath(taskDir);
75184
75247
  mkdirSync5(dirname14(filePath), { recursive: true });
75185
- const startingLineNo = countLineNumbers(filePath);
75186
- const payload = entries.map((entry) => serializeEntry(entry)).join("");
75248
+ const { lineCount: startingLineNo, endsWithNewline } = inspectAgentLogTail(filePath);
75249
+ const payload = `${startingLineNo > 0 && !endsWithNewline ? "\n" : ""}${entries.map((entry) => serializeEntry(entry)).join("")}`;
75187
75250
  appendFileSync(filePath, payload, "utf8");
75188
75251
  return entries.map((entry, index2) => materializeEntry(entry, startingLineNo + index2 + 1));
75189
75252
  }
@@ -75219,6 +75282,7 @@ function readAllAgentLogEntries(taskDir, options = {}) {
75219
75282
  }
75220
75283
  const lines = content.split("\n");
75221
75284
  const entries = [];
75285
+ const skippedLines = [];
75222
75286
  for (let index2 = 0; index2 < lines.length; index2 += 1) {
75223
75287
  const rawLine = lines[index2];
75224
75288
  if (!rawLine) continue;
@@ -75240,9 +75304,18 @@ function readAllAgentLogEntries(taskDir, options = {}) {
75240
75304
  }
75241
75305
  entries.push(entry);
75242
75306
  } catch (error2) {
75243
- log7.warn(`Skipping malformed JSONL line ${lineNo} in ${filePath}`, error2);
75307
+ skippedLines.push({
75308
+ lineNo,
75309
+ message: error2 instanceof Error ? error2.message : String(error2)
75310
+ });
75244
75311
  }
75245
75312
  }
75313
+ const firstSkipped = skippedLines[0];
75314
+ if (firstSkipped) {
75315
+ log7.warn(
75316
+ `Skipped ${skippedLines.length} malformed JSONL line(s) in ${filePath}; first line ${firstSkipped.lineNo}: ${firstSkipped.message}`
75317
+ );
75318
+ }
75246
75319
  return entries;
75247
75320
  }
75248
75321
  function normalizeTimingMs(value) {
@@ -75285,16 +75358,19 @@ function materializeEntry(entry, lineNo) {
75285
75358
  sourceRef: buildAgentLogSourceRef(entry.taskId, lineNo)
75286
75359
  };
75287
75360
  }
75288
- function countLineNumbers(filePath) {
75361
+ function inspectAgentLogTail(filePath) {
75289
75362
  if (!existsSync16(filePath)) {
75290
- return 0;
75363
+ return { lineCount: 0, endsWithNewline: true };
75291
75364
  }
75292
75365
  const content = readFileSync7(filePath, "utf8");
75293
75366
  if (content.length === 0) {
75294
- return 0;
75367
+ return { lineCount: 0, endsWithNewline: true };
75295
75368
  }
75296
75369
  const lines = content.split("\n");
75297
- return lines.at(-1) === "" ? lines.length - 1 : lines.length;
75370
+ return {
75371
+ lineCount: lines.at(-1) === "" ? lines.length - 1 : lines.length,
75372
+ endsWithNewline: content.endsWith("\n")
75373
+ };
75298
75374
  }
75299
75375
  function pruneAgentLogFiles(tasksDir, retentionDays, scanTaskIds) {
75300
75376
  if (!Number.isFinite(retentionDays) || retentionDays <= 0 || !existsSync16(tasksDir)) {
@@ -79084,6 +79160,10 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0;
79084
79160
  * FNXC:Missions 2026-07-11-12:35:
79085
79161
  * A generated fix can also supersede itself once its own validator/loop evidence has passed.
79086
79162
  * Reconciliation treats that as terminal evidence so a completed fix does not stay active only because its ancestor previously failed.
79163
+ *
79164
+ * FNXC:Missions 2026-09-05-22:07:
79165
+ * Superseding a generated fix means it is no longer needed, not that it passed validation.
79166
+ * Never stamp unearned validator evidence because that re-arms the reconciler's own trigger (issue #3574).
79087
79167
  */
79088
79168
  reconcileSupersededGeneratedFixFeatures(sliceId) {
79089
79169
  const features = this.listFeatures(sliceId);
@@ -79108,7 +79188,42 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0;
79108
79188
  ancestorPassedMemo.set(feature.id, passed);
79109
79189
  return passed;
79110
79190
  };
79111
- const supersededFeatureIds = features.filter((feature) => feature.generatedFromFeatureId && (featureHasPassed(feature) || hasPassedAncestor(feature))).filter((feature) => feature.status !== "done" || feature.loopState !== "passed" || feature.lastValidatorStatus !== "passed" || feature.taskId).map((feature) => feature.id);
79191
+ const isFabricatedMarker = (feature) => Boolean(
79192
+ (feature.generatedFromFeatureId || feature.generatedFromRunId) && feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId && !feature.taskId && feature.status === "done" && !hasPassedAncestor(feature)
79193
+ );
79194
+ const repairedFeatureIds = features.filter(isFabricatedMarker).map((feature) => feature.id);
79195
+ const repairedIds = new Set(repairedFeatureIds);
79196
+ const supersededFeatureIds = features.filter((feature) => !repairedIds.has(feature.id)).filter((feature) => feature.generatedFromFeatureId && (featureHasPassed(feature) || hasPassedAncestor(feature))).filter((feature) => feature.status !== "done" || feature.loopState !== "passed" || feature.taskId).map((feature) => feature.id);
79197
+ if (repairedFeatureIds.length > 0) {
79198
+ this.db.transaction(() => {
79199
+ const hasCurrentPassedAncestor = (feature, seen = /* @__PURE__ */ new Set()) => {
79200
+ const sourceFeatureId = feature.generatedFromFeatureId;
79201
+ if (!sourceFeatureId || seen.has(sourceFeatureId)) return false;
79202
+ seen.add(sourceFeatureId);
79203
+ const sourceFeature = this.getFeature(sourceFeatureId);
79204
+ return featureHasPassed(sourceFeature) || (sourceFeature ? hasCurrentPassedAncestor(sourceFeature, seen) : false);
79205
+ };
79206
+ const isCurrentFabricatedMarker = (feature) => Boolean(
79207
+ (feature.generatedFromFeatureId || feature.generatedFromRunId) && feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId && !feature.taskId && feature.status === "done" && !hasCurrentPassedAncestor(feature)
79208
+ );
79209
+ for (const featureId of repairedFeatureIds) {
79210
+ const feature = this.getFeature(featureId);
79211
+ if (!feature || !isCurrentFabricatedMarker(feature)) continue;
79212
+ this.updateFeature(featureId, { status: "defined", taskId: void 0, loopState: "idle", lastValidatorStatus: void 0 });
79213
+ const slice = this.getSlice(feature.sliceId);
79214
+ const milestone = slice ? this.getMilestone(slice.milestoneId) : void 0;
79215
+ if (milestone) {
79216
+ this.logMissionEvent(milestone.missionId, "feature_status_changed", `Feature status changed from ${feature.status} to defined`, {
79217
+ featureId,
79218
+ field: "status",
79219
+ from: feature.status,
79220
+ to: "defined",
79221
+ source: "superseded-fix-marker-repair"
79222
+ });
79223
+ }
79224
+ }
79225
+ });
79226
+ }
79112
79227
  if (supersededFeatureIds.length > 0) {
79113
79228
  this.db.transaction(() => {
79114
79229
  for (const featureId of supersededFeatureIds) {
@@ -79117,8 +79232,7 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0;
79117
79232
  this.updateFeature(featureId, {
79118
79233
  status: "done",
79119
79234
  taskId: void 0,
79120
- loopState: "passed",
79121
- lastValidatorStatus: "passed"
79235
+ loopState: "passed"
79122
79236
  });
79123
79237
  if (feature.taskId) {
79124
79238
  this.db.prepare('UPDATE tasks SET missionId = NULL, sliceId = NULL WHERE id = ? AND "deletedAt" IS NULL').run(feature.taskId);
@@ -79128,7 +79242,9 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0;
79128
79242
  }
79129
79243
  return {
79130
79244
  supersededCount: supersededFeatureIds.length,
79131
- featureIds: supersededFeatureIds
79245
+ featureIds: supersededFeatureIds,
79246
+ repairedCount: repairedFeatureIds.length,
79247
+ repairedFeatureIds
79132
79248
  };
79133
79249
  }
79134
79250
  /**
@@ -82472,10 +82588,12 @@ var init_async_mission_store = __esm({
82472
82588
  throw new RepairNotEligibleError(featureId, options.action);
82473
82589
  }
82474
82590
  const now = (/* @__PURE__ */ new Date()).toISOString();
82591
+ const hasUnvalidatedMarker = options.action === "clear" && feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId;
82592
+ const appliesUnvalidatedMarker = hasUnvalidatedMarker && Boolean(feature.generatedFromFeatureId || feature.generatedFromRunId) && !feature.taskId && feature.status === "done";
82475
82593
  const priorLoopState = feature.loopState;
82476
82594
  const priorStatus = feature.status;
82477
82595
  let groundTruthMetadata = {};
82478
- if (options.action === "clear" && feature.status === "blocked") {
82596
+ if (options.action === "clear" && (feature.status === "blocked" || appliesUnvalidatedMarker)) {
82479
82597
  const fence = options.groundTruth;
82480
82598
  if (!fence || fence.featureId !== featureId || fence.taskId !== (feature.taskId ?? null)) {
82481
82599
  throw new RepairGroundTruthStaleError(featureId);
@@ -82505,14 +82623,14 @@ var init_async_mission_store = __esm({
82505
82623
  let updated;
82506
82624
  let run;
82507
82625
  if (options.action === "clear") {
82508
- const currentLoop = feature.loopState;
82509
- const appliesLoop = currentLoop === "blocked" || currentLoop === "needs_fix";
82626
+ const currentLoop = feature.loopState ?? "idle";
82627
+ const appliesLoop = currentLoop === "blocked" || currentLoop === "needs_fix" || appliesUnvalidatedMarker;
82510
82628
  const nextLoop = appliesLoop ? options.resolvedLoopState ?? "idle" : currentLoop;
82511
82629
  if (appliesLoop && !FEATURE_LOOP_REPAIR_TRANSITIONS[currentLoop].includes(nextLoop)) {
82512
82630
  throw new Error(`Invalid validation repair transition from '${currentLoop}' to '${nextLoop}'`);
82513
82631
  }
82514
- const appliesStatus = feature.status === "blocked";
82515
- const nextStatus = appliesStatus ? options.resolvedStatus : feature.status;
82632
+ const appliesStatus = feature.status === "blocked" || appliesUnvalidatedMarker;
82633
+ const nextStatus = appliesUnvalidatedMarker ? "defined" : appliesStatus ? options.resolvedStatus : feature.status;
82516
82634
  if (appliesStatus && (nextStatus !== "in-progress" && nextStatus !== "triaged" && nextStatus !== "defined")) {
82517
82635
  throw new Error("Validation repair requires resolvedStatus of in-progress, triaged, or defined");
82518
82636
  }
@@ -82524,13 +82642,13 @@ var init_async_mission_store = __esm({
82524
82642
  const matchesLane = nextStatus === "in-progress" && fence.taskLiveness === "live" && fence.laneRole === "wip" || nextStatus === "triaged" && fence.taskLiveness === "live" && fence.laneRole === "planner" || nextStatus === "defined" && fence.taskLiveness === "absent" && fence.laneRole === "none";
82525
82643
  if (!matchesLane) throw new RepairGroundTruthStaleError(featureId);
82526
82644
  }
82527
- if (!appliesLoop && !appliesStatus) throw new RepairNotEligibleError(featureId, options.action);
82645
+ if (!appliesLoop && !appliesStatus && !hasUnvalidatedMarker) throw new RepairNotEligibleError(featureId, options.action);
82528
82646
  updated = {
82529
82647
  ...feature,
82530
82648
  loopState: nextLoop,
82531
82649
  status: nextStatus,
82532
82650
  implementationAttemptCount: 0,
82533
- ...feature.lastValidatorStatus === "blocked" || feature.lastValidatorStatus === "failed" ? { lastValidatorStatus: void 0 } : {},
82651
+ ...feature.lastValidatorStatus === "blocked" || feature.lastValidatorStatus === "failed" || hasUnvalidatedMarker ? { lastValidatorStatus: void 0 } : {},
82534
82652
  updatedAt: now
82535
82653
  };
82536
82654
  await updateFeature(tx, updated);
@@ -82998,10 +83116,60 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0,
82998
83116
  const source = byId.get(sourceId);
82999
83117
  return passed(source) || (source ? hasPassedAncestor(source, seen) : false);
83000
83118
  };
83119
+ const isFabricatedMarker = (feature) => Boolean(
83120
+ (feature.generatedFromFeatureId || feature.generatedFromRunId) && feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId && !feature.taskId && feature.status === "done" && !hasPassedAncestor(feature)
83121
+ );
83122
+ const repairIds = new Set(features.filter(isFabricatedMarker).map((feature) => feature.id));
83001
83123
  const ids = [];
83002
83124
  for (const feature of features) {
83003
- if (!feature.generatedFromFeatureId || !(passed(feature) || hasPassedAncestor(feature))) continue;
83004
- if (feature.status !== "done" || feature.loopState !== "passed" || feature.lastValidatorStatus !== "passed" || feature.taskId) ids.push(feature.id);
83125
+ if (repairIds.has(feature.id) || !feature.generatedFromFeatureId || !(passed(feature) || hasPassedAncestor(feature))) continue;
83126
+ if (feature.status !== "done" || feature.loopState !== "passed" || feature.taskId) ids.push(feature.id);
83127
+ }
83128
+ const repairedFeatureIds = [];
83129
+ if (repairIds.size > 0) {
83130
+ const now = (/* @__PURE__ */ new Date()).toISOString();
83131
+ const repaired = await this.layer.transactionImmediate(async (tx) => {
83132
+ const locked = await tx.select({ id: project_exports.missionFeatures.id }).from(project_exports.missionFeatures).where(inArray(project_exports.missionFeatures.id, [...repairIds])).for("update");
83133
+ const preImages = locked.length > 0 ? await listFeaturesByIds(tx, locked.map((row) => row.id)) : [];
83134
+ const currentById = new Map(preImages.map((feature) => [feature.id, feature]));
83135
+ let missingAncestorIds = [...new Set(preImages.map((feature) => feature.generatedFromFeatureId).filter((id) => Boolean(id) && !currentById.has(id)))];
83136
+ while (missingAncestorIds.length > 0) {
83137
+ const lockedAncestors = await tx.select({ id: project_exports.missionFeatures.id }).from(project_exports.missionFeatures).where(inArray(project_exports.missionFeatures.id, missingAncestorIds)).for("update");
83138
+ const ancestors = lockedAncestors.length > 0 ? await listFeaturesByIds(tx, lockedAncestors.map((row) => row.id)) : [];
83139
+ for (const ancestor of ancestors) currentById.set(ancestor.id, ancestor);
83140
+ missingAncestorIds = [...new Set(ancestors.map((ancestor) => ancestor.generatedFromFeatureId).filter((id) => Boolean(id) && !currentById.has(id)))];
83141
+ }
83142
+ const hasCurrentPassedAncestor = (feature, seen = /* @__PURE__ */ new Set()) => {
83143
+ const sourceId = feature.generatedFromFeatureId;
83144
+ if (!sourceId || seen.has(sourceId)) return false;
83145
+ seen.add(sourceId);
83146
+ const source = currentById.get(sourceId);
83147
+ return passed(source) || (source ? hasCurrentPassedAncestor(source, seen) : false);
83148
+ };
83149
+ const changed = preImages.filter((feature) => Boolean(
83150
+ (feature.generatedFromFeatureId || feature.generatedFromRunId) && feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId && !feature.taskId && feature.status === "done" && !hasCurrentPassedAncestor(feature)
83151
+ ));
83152
+ if (changed.length === 0) return { events: [], features: [] };
83153
+ await tx.update(project_exports.missionFeatures).set({
83154
+ status: "defined",
83155
+ taskId: null,
83156
+ loopState: "idle",
83157
+ lastValidatorStatus: null,
83158
+ updatedAt: now
83159
+ }).where(inArray(project_exports.missionFeatures.id, changed.map((feature) => feature.id)));
83160
+ let seq = await getMaxEventSeq(tx);
83161
+ const events = [];
83162
+ for (const feature of changed) {
83163
+ const event = await this.recordFeatureStatusChange(tx, feature, "defined", { type: "system", id: "mission-store", source: "superseded-fix-marker-repair" }, void 0, ++seq);
83164
+ if (event) events.push(event);
83165
+ }
83166
+ return { events, features: changed };
83167
+ });
83168
+ for (const event of repaired.events) this.emit("mission:event", event);
83169
+ for (const feature of repaired.features) {
83170
+ this.emit("feature:updated", { ...feature, status: "defined", taskId: void 0, loopState: "idle", lastValidatorStatus: void 0, updatedAt: now });
83171
+ repairedFeatureIds.push(feature.id);
83172
+ }
83005
83173
  }
83006
83174
  if (ids.length > 0) {
83007
83175
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -83009,13 +83177,12 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0,
83009
83177
  const locked = await tx.select({ id: project_exports.missionFeatures.id }).from(project_exports.missionFeatures).where(inArray(project_exports.missionFeatures.id, ids)).for("update");
83010
83178
  const lockedIds = locked.map((row) => row.id);
83011
83179
  const preImages = lockedIds.length > 0 ? await listFeaturesByIds(tx, lockedIds) : [];
83012
- const changed = preImages.filter((feature) => feature.status !== "done" || feature.loopState !== "passed" || feature.lastValidatorStatus !== "passed" || feature.taskId);
83180
+ const changed = preImages.filter((feature) => feature.status !== "done" || feature.loopState !== "passed" || feature.taskId);
83013
83181
  if (changed.length === 0) return { events: [], updatedFeatures: [] };
83014
83182
  await tx.update(project_exports.missionFeatures).set({
83015
83183
  status: "done",
83016
83184
  taskId: null,
83017
83185
  loopState: "passed",
83018
- lastValidatorStatus: "passed",
83019
83186
  updatedAt: now
83020
83187
  }).where(inArray(project_exports.missionFeatures.id, changed.map((feature) => feature.id)));
83021
83188
  let seq = await getMaxEventSeq(tx);
@@ -83030,13 +83197,14 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0,
83030
83197
  });
83031
83198
  for (const event of events) this.emit("mission:event", event);
83032
83199
  for (const feature of updatedFeatures) {
83033
- const updated = { ...feature, status: "done", taskId: void 0, loopState: "passed", lastValidatorStatus: "passed", updatedAt: now };
83200
+ const updated = { ...feature, status: "done", taskId: void 0, loopState: "passed", updatedAt: now };
83034
83201
  this.emit("feature:updated", updated);
83035
83202
  if (feature.taskId) await clearTaskMissionLinkage(this.db, feature.taskId);
83036
83203
  }
83037
83204
  if (updatedFeatures.length > 0) await this.recomputeSliceStatus(sliceId);
83038
83205
  }
83039
- return { supersededCount: ids.length, featureIds: ids };
83206
+ if (repairedFeatureIds.length > 0) await this.recomputeSliceStatus(sliceId);
83207
+ return { supersededCount: ids.length, featureIds: ids, repairedCount: repairedFeatureIds.length, repairedFeatureIds };
83040
83208
  }
83041
83209
  async transitionLoopState(featureId, newState) {
83042
83210
  const feature = await getFeature(this.db, featureId);
@@ -88668,19 +88836,81 @@ async function archiveAllDoneImpl(store, options) {
88668
88836
  for (const column of completeColumns) {
88669
88837
  for (const task of await store.listTasks({ slim: true, column })) doneById.set(task.id, task);
88670
88838
  }
88671
- const doneTasks = [...doneById.values()];
88672
- if (doneTasks.length === 0) {
88673
- return [];
88839
+ if (doneById.size === 0) return { archived: [], skipped: [] };
88840
+ const archivedColumns = await resolveArchivedLanes(store).catch(() => void 0) ?? new Set(LEGACY_ARCHIVE_LANES);
88841
+ const liveTasks = (await store.listTasks({ slim: true, includeArchived: false })).filter((task) => !task.deletedAt && !archivedColumns.has(task.column));
88842
+ const childrenByParentId = /* @__PURE__ */ new Map();
88843
+ for (const task of liveTasks) {
88844
+ if (!task.sourceParentTaskId) continue;
88845
+ const children = childrenByParentId.get(task.sourceParentTaskId) ?? [];
88846
+ children.push(task.id);
88847
+ childrenByParentId.set(task.sourceParentTaskId, children);
88848
+ }
88849
+ for (const children of childrenByParentId.values()) children.sort();
88850
+ const bypassLineageGate = options?.removeLineageReferences === true;
88851
+ const archivableIds = new Set(doneById.keys());
88852
+ const skipped = [];
88853
+ if (!bypassLineageGate) {
88854
+ let firstRemovalWave = true;
88855
+ while (true) {
88856
+ const removals = [...archivableIds].filter(
88857
+ (id) => (childrenByParentId.get(id) ?? []).some((childId) => !archivableIds.has(childId))
88858
+ ).sort();
88859
+ if (removals.length === 0) break;
88860
+ for (const id of removals) {
88861
+ const blockers = (childrenByParentId.get(id) ?? []).filter((childId) => !archivableIds.has(childId));
88862
+ skipped.push({
88863
+ id,
88864
+ reason: firstRemovalWave ? "open-lineage-children" : "blocked-by-unarchived-batch-member",
88865
+ blockers,
88866
+ message: firstRemovalWave ? `Live lineage children prevent archiving ${id}.` : `Unarchived batch members prevent archiving ${id}.`
88867
+ });
88868
+ archivableIds.delete(id);
88869
+ }
88870
+ firstRemovalWave = false;
88871
+ }
88872
+ }
88873
+ const archived = [];
88874
+ const remaining = new Set(archivableIds);
88875
+ const archivedIds = /* @__PURE__ */ new Set();
88876
+ const archiveWave = async (ids) => {
88877
+ const settled = await Promise.allSettled(ids.map((id) => store.archiveTask(id, {
88878
+ cleanup: true,
88879
+ removeLineageReferences: options?.removeLineageReferences
88880
+ })));
88881
+ for (const [index2, result] of settled.entries()) {
88882
+ const id = ids[index2];
88883
+ if (result.status === "fulfilled") {
88884
+ archived.push(result.value);
88885
+ archivedIds.add(id);
88886
+ } else {
88887
+ const error2 = result.reason;
88888
+ const blockers = error2 instanceof TaskHasLineageChildrenError ? error2.childIds : error2 instanceof TaskHasDependentsError ? error2.dependentIds : [];
88889
+ skipped.push({ id, reason: "archive-failed", blockers, message: error2 instanceof Error ? error2.message : String(error2) });
88890
+ }
88891
+ remaining.delete(id);
88892
+ }
88893
+ };
88894
+ while (remaining.size > 0) {
88895
+ const wave = [...remaining].filter(
88896
+ (id) => bypassLineageGate ? (childrenByParentId.get(id) ?? []).filter((childId) => remaining.has(childId)).length === 0 : (childrenByParentId.get(id) ?? []).filter((childId) => archivableIds.has(childId)).every((childId) => archivedIds.has(childId))
88897
+ ).sort();
88898
+ if (wave.length > 0) {
88899
+ await archiveWave(wave);
88900
+ continue;
88901
+ }
88902
+ if (bypassLineageGate) {
88903
+ await archiveWave([...remaining].sort());
88904
+ } else {
88905
+ for (const id of [...remaining].sort()) {
88906
+ const blockers = (childrenByParentId.get(id) ?? []).filter((childId) => remaining.has(childId) || !archivedIds.has(childId));
88907
+ skipped.push({ id, reason: "blocked-by-unarchived-batch-member", blockers, message: `Unarchived batch members prevent archiving ${id}.` });
88908
+ remaining.delete(id);
88909
+ }
88910
+ }
88674
88911
  }
88675
- const archivedTasks3 = await Promise.all(
88676
- doneTasks.map(
88677
- (task) => store.archiveTask(task.id, {
88678
- cleanup: true,
88679
- removeLineageReferences: options?.removeLineageReferences
88680
- })
88681
- )
88682
- );
88683
- return archivedTasks3;
88912
+ skipped.sort((left, right) => left.id.localeCompare(right.id));
88913
+ return { archived, skipped };
88684
88914
  }
88685
88915
  async function resolveUnarchiveTargetColumnImpl(store, preArchiveColumn, taskId) {
88686
88916
  const lifecycle = taskId ? await resolveTaskLifecycleColumns(store, taskId) : void 0;
@@ -89144,6 +89374,7 @@ var init_task_artifacts_ops = __esm({
89144
89374
  init_types();
89145
89375
  init_store();
89146
89376
  init_project_lane_vocabulary();
89377
+ init_errors3();
89147
89378
  LEGACY_ARCHIVE_LANES = ["archived"];
89148
89379
  }
89149
89380
  });
@@ -107015,18 +107246,18 @@ function hasFreshAgentLogActivitySinceTaskUpdate(store, task, now, reviewColumns
107015
107246
  }
107016
107247
  return Math.max(0, now - latestAgentLogMs) < DEFAULT_STALE_MERGING_MIN_AGE_MS;
107017
107248
  }
107018
- async function resolveHoldColumnForTask(store, taskId, cache) {
107249
+ async function resolveHoldColumnForTask(store, taskId, cache, selectionCache) {
107019
107250
  try {
107020
- const lifecycle = resolveLifecycleColumns(await resolveWorkflowIrForTask(store, taskId, cache));
107251
+ const lifecycle = resolveLifecycleColumns(await resolveWorkflowIrForTask(store, taskId, cache, selectionCache));
107021
107252
  return lifecycle?.hold ?? "todo";
107022
107253
  } catch {
107023
107254
  return "todo";
107024
107255
  }
107025
107256
  }
107026
- async function resolveReviewColumnsForTask(store, taskId, cache) {
107257
+ async function resolveReviewColumnsForTask(store, taskId, cache, selectionCache) {
107027
107258
  const columns = /* @__PURE__ */ new Set(["in-review"]);
107028
107259
  try {
107029
- const ir2 = await resolveWorkflowIrForTask(store, taskId, cache);
107260
+ const ir2 = await resolveWorkflowIrForTask(store, taskId, cache, selectionCache);
107030
107261
  if (ir2) for (const id of resolveReviewColumns(ir2)) columns.add(id);
107031
107262
  } catch {
107032
107263
  }
@@ -107147,6 +107378,12 @@ async function listTasksImpl(store, options) {
107147
107378
  const settings = await store.getSettingsFast();
107148
107379
  const mergeQueuedTaskIds = await store.getMergeQueuedTaskIdsAsync();
107149
107380
  const listPassIrCache = /* @__PURE__ */ new Map();
107381
+ const listPassSelectionCache = options?.selectionCache ?? /* @__PURE__ */ new Map();
107382
+ const listSelectionReads = await prefetchWorkflowSelections(store, filteredRows.map((row) => String(row.id)), listPassSelectionCache);
107383
+ if (options?.selectionReadTally) {
107384
+ options.selectionReadTally.batched += listSelectionReads.batched;
107385
+ options.selectionReadTally.singles += listSelectionReads.singles;
107386
+ }
107150
107387
  const staleThresholds = {
107151
107388
  inProgressWarningMs: settings.staleInProgressWarningMs,
107152
107389
  inProgressCriticalMs: settings.staleInProgressCriticalMs,
@@ -107157,7 +107394,7 @@ async function listTasksImpl(store, options) {
107157
107394
  const row = store.pgRowToTaskRow(pgRow);
107158
107395
  const task = store.rowToTask(row);
107159
107396
  const isMergeQueued = mergeQueuedTaskIds.has(task.id);
107160
- const reviewColumnsForRow = await resolveReviewColumnsForTask(store, task.id, listPassIrCache);
107397
+ const reviewColumnsForRow = await resolveReviewColumnsForTask(store, task.id, listPassIrCache, listPassSelectionCache);
107161
107398
  const hasFreshAgentLogActivity = hasFreshAgentLogActivitySinceTaskUpdate(store, task, now, reviewColumnsForRow);
107162
107399
  const executingTaskIds = hasFreshAgentLogActivity ? /* @__PURE__ */ new Set([task.id]) : void 0;
107163
107400
  task.inReviewStall = isMergeQueued ? void 0 : getInReviewStallReason(task, {
@@ -107189,7 +107426,7 @@ async function listTasksImpl(store, options) {
107189
107426
  thresholdMs: settings.stalePausedTodoThresholdMs,
107190
107427
  // Paused-only (the signal is a no-op otherwise), sharing the list-pass
107191
107428
  // IR cache so one workflow is read once per pass, not once per card.
107192
- holdColumn: task.paused === true ? await resolveHoldColumnForTask(store, task.id, listPassIrCache) : void 0,
107429
+ holdColumn: task.paused === true ? await resolveHoldColumnForTask(store, task.id, listPassIrCache, listPassSelectionCache) : void 0,
107193
107430
  engineActiveSinceMs: settings.engineActiveSinceMs,
107194
107431
  engineActivationGraceMs: settings.engineActivationGraceMs
107195
107432
  });
@@ -107199,7 +107436,7 @@ async function listTasksImpl(store, options) {
107199
107436
  thresholds: staleThresholds,
107200
107437
  engineActiveSinceMs: settings.engineActiveSinceMs,
107201
107438
  engineActivationGraceMs: settings.engineActivationGraceMs,
107202
- lifecycle: await resolveTaskLifecycleColumns(store, task.id, listPassIrCache)
107439
+ lifecycle: await resolveTaskLifecycleColumns(store, task.id, listPassIrCache, listPassSelectionCache)
107203
107440
  });
107204
107441
  } catch (err) {
107205
107442
  if (!(err instanceof RangeError)) throw err;
@@ -107278,12 +107515,14 @@ async function listTasksModifiedSinceImpl(store, since, limit, opts) {
107278
107515
  const lifecycleByTaskId = /* @__PURE__ */ new Map();
107279
107516
  {
107280
107517
  const irCache = /* @__PURE__ */ new Map();
107518
+ const selectionCache = /* @__PURE__ */ new Map();
107519
+ await prefetchWorkflowSelections(store, pageRows.map((row) => row.id), selectionCache);
107281
107520
  for (const pgRow of pageRows) {
107282
107521
  const row = store.pgRowToTaskRow(pgRow);
107283
- reviewColumnsByTaskId.set(row.id, await resolveReviewColumnsForTask(store, row.id, irCache));
107284
- lifecycleByTaskId.set(row.id, await resolveTaskLifecycleColumns(store, row.id, irCache));
107522
+ reviewColumnsByTaskId.set(row.id, await resolveReviewColumnsForTask(store, row.id, irCache, selectionCache));
107523
+ lifecycleByTaskId.set(row.id, await resolveTaskLifecycleColumns(store, row.id, irCache, selectionCache));
107285
107524
  if (store.rowToTask(row).paused !== true) continue;
107286
- holdColumnByTaskId.set(row.id, await resolveHoldColumnForTask(store, row.id, irCache));
107525
+ holdColumnByTaskId.set(row.id, await resolveHoldColumnForTask(store, row.id, irCache, selectionCache));
107287
107526
  }
107288
107527
  }
107289
107528
  const tasks2 = pageRows.map((pgRow) => {
@@ -107401,10 +107640,12 @@ async function searchTasksImpl(store, query, options) {
107401
107640
  const settings = await store.getSettingsFast();
107402
107641
  const mergeQueuedTaskIds = await store.getMergeQueuedTaskIdsAsync();
107403
107642
  const searchPassIrCache = /* @__PURE__ */ new Map();
107643
+ const searchPassSelectionCache = /* @__PURE__ */ new Map();
107644
+ await prefetchWorkflowSelections(store, pgRows.map((row) => String(row.id)), searchPassSelectionCache);
107404
107645
  const tasks2 = await Promise.all(pgRows.map(async (pgRow) => {
107405
107646
  const task = store.rowToTask(store.pgRowToTaskRow(pgRow));
107406
107647
  const isMergeQueued = mergeQueuedTaskIds.has(task.id);
107407
- const reviewColumnsForRow = await resolveReviewColumnsForTask(store, task.id, searchPassIrCache);
107648
+ const reviewColumnsForRow = await resolveReviewColumnsForTask(store, task.id, searchPassIrCache, searchPassSelectionCache);
107408
107649
  const hasFreshAgentLogActivity = hasFreshAgentLogActivitySinceTaskUpdate(store, task, now, reviewColumnsForRow);
107409
107650
  const executingTaskIds = hasFreshAgentLogActivity ? /* @__PURE__ */ new Set([task.id]) : void 0;
107410
107651
  task.inReviewStall = isMergeQueued ? void 0 : getInReviewStallReason(task, {
@@ -108644,6 +108885,7 @@ var init_store = __esm({
108644
108885
  init_async_self_healing();
108645
108886
  init_task_merge();
108646
108887
  init_required_pre_merge_steps();
108888
+ init_pre_merge_approval();
108647
108889
  init_logger2();
108648
108890
  init_project_root_guard();
108649
108891
  init_agent_store();
@@ -110409,29 +110651,35 @@ var init_store = __esm({
110409
110651
  const results = task.workflowStepResults ?? [];
110410
110652
  const failedTarget = getLatestFailedPreMergeReviewStep(task);
110411
110653
  const reviewIrForBypass = failedTarget ? void 0 : await resolveWorkflowIrForTask(this, task.id);
110412
- const absentStepId = reviewIrForBypass ? [...resolveRequiredPreMergeStepIds(reviewIrForBypass, task.enabledWorkflowSteps, task)].find((workflowStepId) => !results.some((result) => result.workflowStepId === workflowStepId)) : void 0;
110413
- if (!failedTarget && !absentStepId) {
110654
+ const requiredForBypass = reviewIrForBypass ? [...resolveRequiredPreMergeStepIds(reviewIrForBypass, task.enabledWorkflowSteps, task)] : [];
110655
+ const absentStepId = requiredForBypass.find((workflowStepId) => !results.some((result) => result.workflowStepId === workflowStepId));
110656
+ const unapprovedTarget = !failedTarget && !absentStepId && requiredForBypass.length > 0 ? (() => {
110657
+ const blocked = evaluatePreMergeApprovals(task, { requiredPreMergeStepIds: new Set(requiredForBypass) }).find((approval) => approval.state !== "approved");
110658
+ return blocked ? results.find((result) => result.workflowStepId === blocked.workflowStepId) : void 0;
110659
+ })() : void 0;
110660
+ if (!failedTarget && !absentStepId && !unapprovedTarget) {
110414
110661
  throw new Error(`Cannot bypass review lane for ${id}: no failed pre-merge review step found`);
110415
110662
  }
110416
- const target = failedTarget ?? {
110663
+ const target = failedTarget ?? unapprovedTarget ?? {
110417
110664
  workflowStepId: absentStepId,
110418
110665
  workflowStepName: absentStepId,
110419
110666
  phase: "pre-merge",
110420
110667
  status: "absent"
110421
110668
  };
110422
- const targetIndex = failedTarget ? results.indexOf(failedTarget) : -1;
110423
- if (failedTarget && targetIndex === -1) {
110669
+ const rowTarget = failedTarget ?? unapprovedTarget;
110670
+ const targetIndex = rowTarget ? results.indexOf(rowTarget) : -1;
110671
+ if (rowTarget && targetIndex === -1) {
110424
110672
  throw new Error(`Cannot bypass review lane for ${id}: failed step result not found`);
110425
110673
  }
110426
110674
  const now = (/* @__PURE__ */ new Date()).toISOString();
110427
- const bypassed = failedTarget ? {
110428
- ...failedTarget,
110675
+ const bypassed = rowTarget ? {
110676
+ ...rowTarget,
110429
110677
  status: "skipped",
110430
110678
  bypassedBy: actor,
110431
110679
  bypassedAt: now,
110432
110680
  bypassReason: reason,
110433
- bypassedFromStatus: failedTarget.status,
110434
- bypassedFromVerdict: failedTarget.verdict
110681
+ bypassedFromStatus: rowTarget.remediationArchivedFromStatus ?? rowTarget.status,
110682
+ bypassedFromVerdict: rowTarget.verdict ?? rowTarget.priorAttempts?.[0]?.verdict
110435
110683
  } : {
110436
110684
  workflowStepId: absentStepId,
110437
110685
  workflowStepName: absentStepId,
@@ -110465,8 +110713,8 @@ var init_store = __esm({
110465
110713
  metadata: {
110466
110714
  workflowStepId: target.workflowStepId,
110467
110715
  workflowStepName: target.workflowStepName,
110468
- bypassedFromStatus: target.status,
110469
- bypassedFromVerdict: failedTarget?.verdict ?? null,
110716
+ bypassedFromStatus: bypassed.bypassedFromStatus,
110717
+ bypassedFromVerdict: bypassed.bypassedFromVerdict ?? null,
110470
110718
  reason
110471
110719
  }
110472
110720
  });
@@ -110528,9 +110776,8 @@ var init_store = __esm({
110528
110776
  );
110529
110777
  }
110530
110778
  if (target.status !== "pending") {
110531
- throw new Error(
110532
- `Cannot resume workflow step for ${id}: only pending steps can be resumed`
110533
- );
110779
+ const archivedFailure = target.remediationArchivedAt != null && (target.remediationArchivedFromStatus === "failed" || target.remediationArchivedFromStatus === "advisory_failure") && !target.bypassedBy && !target.supersededAt;
110780
+ throw new Error(`Cannot resume workflow step for ${id}: only pending steps can be resumed${archivedFailure ? ` (step '${stepId}' is an archived remediation carrier \u2014 use fn_task_bypass_review / POST /tasks/:id/bypass-review to clear it)` : ""}`);
110534
110781
  }
110535
110782
  const now = (/* @__PURE__ */ new Date()).toISOString();
110536
110783
  const resumed = {
@@ -141038,6 +141285,7 @@ __export(src_exports, {
141038
141285
  CLOUD_TICKET_QUERY: () => CLOUD_TICKET_QUERY,
141039
141286
  CODE_REVIEW_BLOCKING_SEVERITY_SETTING_ID: () => CODE_REVIEW_BLOCKING_SEVERITY_SETTING_ID,
141040
141287
  CODE_REVIEW_MAX_REVISIONS_SETTING_ID: () => CODE_REVIEW_MAX_REVISIONS_SETTING_ID,
141288
+ COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC: () => COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC,
141041
141289
  COLOR_THEMES: () => COLOR_THEMES,
141042
141290
  COLUMNS: () => COLUMNS,
141043
141291
  COLUMN_DESCRIPTIONS: () => COLUMN_DESCRIPTIONS,
@@ -141440,6 +141688,7 @@ __export(src_exports, {
141440
141688
  SLICE_PLAN_STATES: () => SLICE_PLAN_STATES,
141441
141689
  SLICE_STATUSES: () => SLICE_STATUSES,
141442
141690
  SPEC_LOCK_PARSER_VERSION: () => SPEC_LOCK_PARSER_VERSION,
141691
+ STALE_CONTENT_APPROVAL_BLOCKER: () => STALE_CONTENT_APPROVAL_BLOCKER,
141443
141692
  STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS: () => STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS,
141444
141693
  STALLED_REVIEW_INVALID_TRANSITION_PATTERN: () => STALLED_REVIEW_INVALID_TRANSITION_PATTERN,
141445
141694
  STALLED_REVIEW_INVALID_TRANSITION_THRESHOLD: () => STALLED_REVIEW_INVALID_TRANSITION_THRESHOLD,
@@ -142169,6 +142418,7 @@ __export(src_exports, {
142169
142418
  isSqliteLockError: () => isSqliteLockError,
142170
142419
  isSqliteMigrationComplete: () => isSqliteMigrationComplete,
142171
142420
  isStaleBlockedByBlocker: () => isStaleBlockedByBlocker,
142421
+ isStaleContentApprovalBlocker: () => isStaleContentApprovalBlocker,
142172
142422
  isStoredAuthCredential: () => isStoredAuthCredential,
142173
142423
  isStrictDescendantPath: () => isStrictDescendantPath,
142174
142424
  isTailscaleCgnatIpv4: () => isTailscaleCgnatIpv4,
@@ -142248,6 +142498,7 @@ __export(src_exports, {
142248
142498
  mapRoleToCapability: () => mapRoleToCapability,
142249
142499
  markDraftSummarized: () => markDraftSummarized,
142250
142500
  markdownHtmlCommentUnits: () => markdownHtmlCommentUnits,
142501
+ matchStepHeadings: () => matchStepHeadings,
142251
142502
  matchesFrontendUxPath: () => matchesFrontendUxPath,
142252
142503
  matchesPatchnodeQuery: () => matchesPatchnodeQuery,
142253
142504
  materializeMcpServerSecrets: () => materializeMcpServerSecrets,
@@ -142361,6 +142612,7 @@ __export(src_exports, {
142361
142612
  postMergeOptionalGroupNode: () => postMergeOptionalGroupNode,
142362
142613
  postMergeVerificationOptionalGroupNode: () => postMergeVerificationOptionalGroupNode,
142363
142614
  postgresSchema: () => schema_exports,
142615
+ prefetchWorkflowSelections: () => prefetchWorkflowSelections,
142364
142616
  prepareAgentCompaniesImport: () => prepareAgentCompaniesImport,
142365
142617
  pricingAsOf: () => pricingAsOf,
142366
142618
  probeGitCliStatus: () => probeGitCliStatus,
@@ -142472,6 +142724,7 @@ __export(src_exports, {
142472
142724
  resolveBackupSettingsMigrationConflict: () => resolveBackupSettingsMigrationConflict,
142473
142725
  resolveCapacityPoolId: () => resolveCapacityPoolId,
142474
142726
  resolveClaudeCodeClientVersion: () => resolveClaudeCodeClientVersion,
142727
+ resolveCollateralArchivedReviewGate: () => resolveCollateralArchivedReviewGate,
142475
142728
  resolveColumnAdjacency: () => resolveColumnAdjacency,
142476
142729
  resolveColumnAgentBinding: () => resolveColumnAgentBinding,
142477
142730
  resolveColumnCapacity: () => resolveColumnCapacity,
@@ -149531,7 +149784,10 @@ async function runHoldReleaseSweep(store, deps) {
149531
149784
  const settings = await store.getSettings();
149532
149785
  if (expired()) return logPreambleTruncation(0);
149533
149786
  counters.tasks += 1;
149534
- const allTasks = await store.listTasks({ includeArchived: false });
149787
+ const selectionReadTally = { batched: 0, singles: 0 };
149788
+ const allTasks = await store.listTasks({ includeArchived: false, selectionCache, selectionReadTally });
149789
+ counters.batchSelections += selectionReadTally.batched;
149790
+ counters.selections += selectionReadTally.singles;
149535
149791
  if (expired()) return logPreambleTruncation(allTasks.length);
149536
149792
  const effectiveWorkflowIdByTask = /* @__PURE__ */ new Map();
149537
149793
  const missingIds = [...new Set(allTasks.map((task) => task.id))].filter((id) => !selectionCache.has(id));
@@ -152369,7 +152625,7 @@ var init_scheduler = __esm({
152369
152625
  for (const slice of hierarchy.milestones.flatMap((milestone) => milestone.slices).filter((slice2) => slice2.status === "active")) {
152370
152626
  try {
152371
152627
  const superseded = await missionStore.reconcileSupersededGeneratedFixFeatures(slice.id);
152372
- fixed += superseded.supersededCount;
152628
+ fixed += superseded.supersededCount + (superseded.repairedCount ?? 0);
152373
152629
  if (superseded.supersededCount > 0) {
152374
152630
  const refreshed = await missionStore.getSlice(slice.id);
152375
152631
  if (refreshed?.status === "complete") {
@@ -152382,8 +152638,9 @@ var init_scheduler = __esm({
152382
152638
  const supersededFeatureIds = new Set(superseded.featureIds ?? []);
152383
152639
  const autoTriage = mission.autopilotEnabled === true || mission.autoAdvance === true;
152384
152640
  for (const feature of features) {
152385
- if (supersededFeatureIds.has(feature.id) || feature.taskId || !autoTriage || feature.status === "blocked") continue;
152641
+ if (supersededFeatureIds.has(feature.id) || feature.taskId || !autoTriage || feature.status === "blocked" || feature.status === "done") continue;
152386
152642
  if (feature.status !== "defined" && this.isGeneratedFixFeature(feature)) {
152643
+ schedulerLog.warn(`Parking stranded generated Fix feature ${feature.id} from ${feature.status}: not triageable without a defined state`);
152387
152644
  await missionStore.updateFeature(feature.id, { status: "blocked", loopState: "blocked", taskId: void 0 });
152388
152645
  fixed++;
152389
152646
  continue;
@@ -179652,7 +179909,7 @@ function extractMarkdownSection(document2, headingName) {
179652
179909
  return document2.slice(start, end).trim();
179653
179910
  }
179654
179911
  function compactTaskPromptStepsSection(section) {
179655
- const stepTitles = Array.from(section.matchAll(/^### Step \d+:.*$/gm), (match) => match[0].trim());
179912
+ const stepTitles = matchStepHeadings(section).map(({ index: index2, headingLineEnd }) => section.slice(index2, headingLineEnd).trim());
179656
179913
  if (stepTitles.length === 0) {
179657
179914
  return section.trim();
179658
179915
  }
@@ -190744,7 +191001,7 @@ ${lines.join("\n")}`
190744
191001
  }
190745
191002
  };
190746
191003
  }
190747
- var TASK_CREATE_PRIORITY_VALUES, missionLineageParams, taskCreateParams, taskLogParams, agentLogTypeParams, agentLogDetailModeParams, taskLogsReadParams, chatTaskLogsReadParams, taskListParams, taskShowParams, taskSearchParams, patchnodeReadParams, installWorktreeDependenciesParams, taskDocumentWriteParams, taskDocumentReadParams, taskPromptWriteParams, taskFileScopeAddParams, chatTaskDocumentWriteParams, chatTaskDocumentReadParams, ARTIFACT_TYPE_VALUES, artifactTypeSchema, artifactRegisterParams, artifactListParams, artifactViewParams, chatArtifactRegisterParams, chatArtifactListParams, workflowListParams, workflowGetParams, workflowSelectParams, taskPromoteParams, taskArchiveParams, taskDeleteParams, taskUnarchiveParams, taskRetryParams, taskPauseParams, taskUnpauseParams, taskDuplicateParams, taskMergeParams, taskAddDepParams, STEP_STATUSES, taskUpdateParams, workflowCreateParams, workflowValidateParams, workflowUpdateParams, workflowDeleteParams, workflowSettingsParams, traitListParams, reflectOnPerformanceParams, readEvaluationsParams, updateIdentityParams, goalListParams, goalShowParams, listAgentsParams, delegateTaskParams, taskAssignParams, getAgentConfigParams, updateAgentConfigParams, createAgentParams, deleteAgentParams, sendMessageParams, readMessagesParams, postRoomMessageParams, askQuestionParams, memorySearchParams, memoryGetParams, webFetchParams, researchRunParams, researchListParams, researchGetParams, researchCancelParams, researchRetryParams, memoryAppendParams, log25, MAX_INSTRUCTIONS_TEXT_LENGTH, MAX_MEMORY_LENGTH, MAX_SOUL_LENGTH, AGENT_MEMORY_ROOT3, AGENT_MEMORY_FILENAME2, AGENT_DREAMS_FILENAME2, agentQmdRefreshState, AGENT_QMD_REFRESH_INTERVAL_MS, DAILY_AGENT_MEMORY_RE2, SEMANTIC_TOOL_READ_MAX_CHARS, AGENT_LOG_READ_DETAIL_PREVIEW_MAX, ARTIFACT_FILE_MAX_BYTES, ARTIFACT_EXTENSION_MIME_TYPES, GOAL_LIST_HARD_LIMIT, GOAL_LIST_SOFT_WARNING_THRESHOLD, GOAL_SNIPPET_MAX_CHARS, missionListParams, missionShowParams, missionCreateParams, missionUpdateParams, missionDeleteParams, missionSetStatusParams, missionReconcileParams, milestoneAddParams, milestoneUpdateParams, milestoneDeleteParams, sliceAddParams, sliceActivateParams, sliceDeleteParams, featureAddParams, featureUpdateParams, featureDeleteParams, featureSetStatusParams, featureLinkTaskParams, featureRepointTaskParams, featureUnlinkTaskParams, featureRepairValidationParams, researchFindingPromoteParams, missionToolResult, optionalText, updateFields, ideationStartParams, ideationDivergeParams, ideationShowParams, ideationConvergeParams, ideationToolResult, formatIdeationCandidate;
191004
+ var TASK_CREATE_PRIORITY_VALUES, missionLineageParams, taskCreateParams, taskLogParams, agentLogTypeParams, agentLogDetailModeParams, taskLogsReadParams, chatTaskLogsReadParams, taskListParams, taskShowParams, taskSearchParams, historyReadParams, installWorktreeDependenciesParams, taskDocumentWriteParams, taskDocumentReadParams, taskPromptWriteParams, taskFileScopeAddParams, chatTaskDocumentWriteParams, chatTaskDocumentReadParams, ARTIFACT_TYPE_VALUES, artifactTypeSchema, artifactRegisterParams, artifactListParams, artifactViewParams, chatArtifactRegisterParams, chatArtifactListParams, workflowListParams, workflowGetParams, workflowSelectParams, taskPromoteParams, taskArchiveParams, taskDeleteParams, taskUnarchiveParams, taskRetryParams, taskPauseParams, taskUnpauseParams, taskDuplicateParams, taskMergeParams, taskAddDepParams, STEP_STATUSES, taskUpdateParams, workflowCreateParams, workflowValidateParams, workflowUpdateParams, workflowDeleteParams, workflowSettingsParams, traitListParams, reflectOnPerformanceParams, readEvaluationsParams, updateIdentityParams, goalListParams, goalShowParams, listAgentsParams, delegateTaskParams, taskAssignParams, getAgentConfigParams, updateAgentConfigParams, createAgentParams, deleteAgentParams, sendMessageParams, readMessagesParams, postRoomMessageParams, askQuestionParams, memorySearchParams, memoryGetParams, webFetchParams, researchRunParams, researchListParams, researchGetParams, researchCancelParams, researchRetryParams, memoryAppendParams, log25, MAX_INSTRUCTIONS_TEXT_LENGTH, MAX_MEMORY_LENGTH, MAX_SOUL_LENGTH, AGENT_MEMORY_ROOT3, AGENT_MEMORY_FILENAME2, AGENT_DREAMS_FILENAME2, agentQmdRefreshState, AGENT_QMD_REFRESH_INTERVAL_MS, DAILY_AGENT_MEMORY_RE2, SEMANTIC_TOOL_READ_MAX_CHARS, AGENT_LOG_READ_DETAIL_PREVIEW_MAX, ARTIFACT_FILE_MAX_BYTES, ARTIFACT_EXTENSION_MIME_TYPES, GOAL_LIST_HARD_LIMIT, GOAL_LIST_SOFT_WARNING_THRESHOLD, GOAL_SNIPPET_MAX_CHARS, missionListParams, missionShowParams, missionCreateParams, missionUpdateParams, missionDeleteParams, missionSetStatusParams, missionReconcileParams, milestoneAddParams, milestoneUpdateParams, milestoneDeleteParams, sliceAddParams, sliceActivateParams, sliceDeleteParams, featureAddParams, featureUpdateParams, featureDeleteParams, featureSetStatusParams, featureLinkTaskParams, featureRepointTaskParams, featureUnlinkTaskParams, featureRepairValidationParams, researchFindingPromoteParams, missionToolResult, optionalText, updateFields, ideationStartParams, ideationDivergeParams, ideationShowParams, ideationConvergeParams, ideationToolResult, formatIdeationCandidate;
190748
191005
  var init_agent_tools = __esm({
190749
191006
  "../engine/src/agent-tools.ts"() {
190750
191007
  "use strict";
@@ -190835,7 +191092,7 @@ var init_agent_tools = __esm({
190835
191092
  includeArchived: Type2.Optional(Type2.Boolean({ description: "Include archived tasks (default true)" })),
190836
191093
  limit: Type2.Optional(Type2.Number({ minimum: 1, maximum: 50, description: "Max results (default 20, max 50)" }))
190837
191094
  });
190838
- patchnodeReadParams = Type2.Object({
191095
+ historyReadParams = Type2.Object({
190839
191096
  query: Type2.Optional(Type2.String({ description: "Search task IDs, titles, and completion summaries" })),
190840
191097
  from: Type2.Optional(Type2.String({ description: "First UTC day, YYYY-MM-DD (inclusive)" })),
190841
191098
  to: Type2.Optional(Type2.String({ description: "Last UTC day, YYYY-MM-DD (inclusive)" })),
@@ -211838,7 +212095,7 @@ function extractPromptSection(promptContent, sectionName) {
211838
212095
  return promptContent.slice(start, end).trim();
211839
212096
  }
211840
212097
  function summarizePromptSteps(promptContent) {
211841
- const stepTitles = Array.from(promptContent.matchAll(/^### Step \d+:.*$/gm), (match) => match[0].trim());
212098
+ const stepTitles = matchStepHeadings(promptContent).map(({ index: index2, headingLineEnd }) => promptContent.slice(index2, headingLineEnd).trim());
211842
212099
  if (stepTitles.length === 0) {
211843
212100
  return "";
211844
212101
  }
@@ -214862,7 +215119,6 @@ init_transient_error_detector();
214862
215119
  // ../engine/src/self-healing.ts
214863
215120
  init_src2();
214864
215121
  init_src2();
214865
- init_logger();
214866
215122
  import { execSync as execSync7 } from "node:child_process";
214867
215123
  import { setImmediate as setImmediateCb2 } from "node:timers";
214868
215124
  import { existsSync as existsSync64, mkdirSync as mkdirSync10, readdirSync as readdirSync10, readFileSync as readFileSync25, realpathSync as realpathSync9, rmdirSync as rmdirSync3, rmSync as rmSync6, statSync as statSync12, writeFileSync as writeFileSync11 } from "node:fs";
@@ -214870,6 +215126,21 @@ import { readFile as readFile46 } from "node:fs/promises";
214870
215126
  import { tmpdir as tmpdir8 } from "node:os";
214871
215127
  import { basename as basename22, dirname as dirname42, isAbsolute as isAbsolute19, join as join82, relative as relative19, resolve as resolve49 } from "node:path";
214872
215128
 
215129
+ // ../engine/src/merge/stale-content-park.ts
215130
+ init_src2();
215131
+ var AUTO_MERGE_RETRY_REJECTED_PREFIX = "AUTO_MERGE_RETRY_REJECTED:";
215132
+ function classifyStaleContentPark(task) {
215133
+ if (task.paused || task.userPaused || task.deletedAt) return void 0;
215134
+ if (typeof task.error !== "string" || !isStaleContentApprovalBlocker(task.error)) return void 0;
215135
+ if (task.status === "failed") {
215136
+ return task.error.startsWith(AUTO_MERGE_RETRY_REJECTED_PREFIX) ? "retry-rejected" : "raw-blocker";
215137
+ }
215138
+ return task.status == null ? "retry-exhausted" : void 0;
215139
+ }
215140
+
215141
+ // ../engine/src/self-healing.ts
215142
+ init_logger();
215143
+
214873
215144
  // ../engine/src/execution/lifecycle-move-log.ts
214874
215145
  init_src2();
214875
215146
  function laneRole(column, lanes) {
@@ -221295,6 +221566,30 @@ var AutoRecoveryDispatcher = class {
221295
221566
  // ../engine/src/self-healing.ts
221296
221567
  init_active_session_registry();
221297
221568
 
221569
+ // ../engine/src/agents/planning-liveness.ts
221570
+ var probes = /* @__PURE__ */ new Set();
221571
+ var planningLivenessRegistry = {
221572
+ registerPlanningLivenessProbe(probe) {
221573
+ probes.add(probe);
221574
+ return () => probes.delete(probe);
221575
+ },
221576
+ isPlanningLive(taskId) {
221577
+ for (const probe of probes) {
221578
+ try {
221579
+ if (probe(taskId)) return true;
221580
+ } catch {
221581
+ return true;
221582
+ }
221583
+ }
221584
+ return false;
221585
+ },
221586
+ hasRegisteredProbe() {
221587
+ return probes.size > 0;
221588
+ }
221589
+ };
221590
+ var registerPlanningLivenessProbe = planningLivenessRegistry.registerPlanningLivenessProbe.bind(planningLivenessRegistry);
221591
+ var isPlanningLive = planningLivenessRegistry.isPlanningLive.bind(planningLivenessRegistry);
221592
+
221298
221593
  // ../engine/src/execution/replan-target.ts
221299
221594
  init_src2();
221300
221595
  init_logger();
@@ -222634,6 +222929,17 @@ var STRANDED_COMPLETED_TODO_ACTIVE_STATUSES = /* @__PURE__ */ new Set([
222634
222929
  ]);
222635
222930
  var ORPHANED_WITH_WORKTREE_GRACE_MS = 3e5;
222636
222931
  var MAX_TASK_DONE_RETRIES = 3;
222932
+ function formatMergeableReviewRecoverySummary({
222933
+ enqueued,
222934
+ merged,
222935
+ parked
222936
+ }) {
222937
+ const outcomes = [];
222938
+ if (enqueued > 0) outcomes.push(`${enqueued} re-enqueued for merge`);
222939
+ if (merged > 0) outcomes.push(`${merged} merged \u2192 done`);
222940
+ if (parked > 0) outcomes.push(`${parked} parked after enqueue starvation`);
222941
+ return `Mergeable review recovery: ${outcomes.join(", ") || "no tasks recovered"}`;
222942
+ }
222637
222943
  var RECONCILE_SCOPE_OVERRIDE_MERGE_ACTIVE_STATUS_SET = new Set(MERGE_ACTIVE_MISSING_WORKTREE_STATUSES);
222638
222944
  async function resolveNoOpFinalizeGateIds2(store, task) {
222639
222945
  const selection = store.getTaskWorkflowSelectionAsync ? await store.getTaskWorkflowSelectionAsync(task.id) : store.getTaskWorkflowSelection?.(task.id);
@@ -222766,6 +223072,9 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
222766
223072
  maintenanceTickCounter = 0;
222767
223073
  taskLifecycleRetentionLastPrunedAt = /* @__PURE__ */ new Map();
222768
223074
  staleContentRerouteAuditKeys = /* @__PURE__ */ new Set();
223075
+ mergeRecoveryBlockerWarnKeys = /* @__PURE__ */ new Set();
223076
+ staleContentParkRecoveryAttempts = /* @__PURE__ */ new Map();
223077
+ staleContentParkRecoveryBudgetLogged = /* @__PURE__ */ new Set();
222769
223078
  unrunPreMergeGateRerouteAuditKeys = /* @__PURE__ */ new Set();
222770
223079
  githubCheckStateRetentionLastPrunedAt = /* @__PURE__ */ new Map();
222771
223080
  processBootStartedAt = Date.now();
@@ -223484,6 +223793,13 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
223484
223793
  { name: "reconcile-orphaned-pending-step-results", fn: () => this.reconcileOrphanedPendingStepResults().then(() => void 0) },
223485
223794
  { name: "reconcile-unproven-review-approvals", fn: () => this.reconcileUnprovenReviewApprovals().then(() => void 0) },
223486
223795
  /*
223796
+ FNXC:PreMergeApproval 2026-09-05-22:11:
223797
+ Runs AFTER the orphaned-step sweep, because that sweep is what produces the `failed` row this
223798
+ defect archives, and BEFORE failed-step recovery, so the restored gate is visible to it in the
223799
+ same startup pass instead of one cycle later.
223800
+ */
223801
+ { name: "reconcile-collateral-archived-review-gates", fn: () => this.reconcileCollateralArchivedReviewGates().then(() => void 0) },
223802
+ /*
223487
223803
  FNXC:WorkspaceContention 2026-08-23-08:00:
223488
223804
  `contention-hold` is owned by an in-memory retry timer. After a process restart that
223489
223805
  timer is gone, so clear only an unowned wait before ordinary dispatch classification;
@@ -224364,6 +224680,7 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
224364
224680
  */
224365
224681
  { name: "reconcile-orphaned-pending-step-results", fn: () => this.reconcileOrphanedPendingStepResults() },
224366
224682
  { name: "reconcile-unproven-review-approvals", fn: () => this.reconcileUnprovenReviewApprovals() },
224683
+ { name: "reconcile-collateral-archived-review-gates", fn: () => this.reconcileCollateralArchivedReviewGates() },
224367
224684
  { name: "recover-failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps() },
224368
224685
  { name: "recover-missing-worktree-review-failures", fn: () => this.recoverMissingWorktreeReviewFailures() },
224369
224686
  { name: "recover-interrupted-merging", fn: () => this.recoverInterruptedMergingTasks() },
@@ -227850,29 +228167,28 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
227850
228167
  /** Repair legacy dependency residue before it can re-enter executor dispatch. */
227851
228168
  async reconcileMissingDependencies() {
227852
228169
  let repaired = 0;
227853
- const tasks2 = await this.store.listTasks({ slim: true });
228170
+ const archivedColumns = new Set(await Promise.resolve().then(() => resolveProjectColumnsForRoles(this.store, ["archived"])).catch(() => /* @__PURE__ */ new Set()));
228171
+ archivedColumns.add("archived");
228172
+ const tasks2 = await this.store.listTasks({ slim: true, includeArchived: false });
227854
228173
  for (const snapshot of tasks2) {
228174
+ if (snapshot.deletedAt || archivedColumns.has(snapshot.column)) continue;
227855
228175
  if (!snapshot.dependencies.length || snapshot.userPaused || snapshot.paused || snapshot.autoMerge === false) continue;
227856
228176
  const livePaths = activeSessionRegistry.pathsForTask(snapshot.id);
227857
228177
  const liveExecution = livePaths.some((path6) => activeSessionRegistry.isPathActive(path6)) || executingTaskLock.has(snapshot.id) || this.options.getExecutingTaskIds?.().has(snapshot.id) === true || this.options.isTaskActive?.(snapshot.id) === true || Boolean(snapshot.checkedOutBy);
227858
228178
  if (liveExecution) continue;
227859
228179
  let removed = 0;
227860
228180
  for (const dependencyId of snapshot.dependencies) {
227861
- try {
227862
- const dependency = await this.store.getTask(dependencyId, { includeDeleted: true });
227863
- if (!dependency.deletedAt) continue;
227864
- } catch (error2) {
227865
- if (!isTaskNotFoundError2(error2)) throw error2;
227866
- }
227867
- const current = await this.store.getTask(snapshot.id).catch((error2) => {
227868
- if (isTaskNotFoundError2(error2)) return void 0;
227869
- throw error2;
227870
- });
228181
+ if (await readLinkedTaskOrUndefined(this.store, dependencyId)) continue;
228182
+ const current = await readLinkedTaskOrUndefined(this.store, snapshot.id);
227871
228183
  if (!current || current.userPaused || current.paused || current.autoMerge === false || current.checkedOutBy || activeSessionRegistry.pathsForTask(current.id).some((path6) => activeSessionRegistry.isPathActive(path6)) || executingTaskLock.has(current.id) || this.options.getExecutingTaskIds?.().has(current.id) === true || this.options.isTaskActive?.(current.id) === true || !current.dependencies.includes(dependencyId)) continue;
227872
228184
  try {
227873
228185
  await this.store.updateTaskDependencies(current.id, { operation: "remove", dependency: dependencyId });
227874
228186
  removed += 1;
227875
228187
  } catch (error2) {
228188
+ if (isMissingTaskLookupError(error2)) {
228189
+ log34.debug(`reconcileMissingDependencies: skipped unreadable ${current.id} dependency ${dependencyId}`);
228190
+ continue;
228191
+ }
227876
228192
  if (!/does not depend on/i.test(error2 instanceof Error ? error2.message : String(error2))) throw error2;
227877
228193
  }
227878
228194
  }
@@ -228638,7 +228954,7 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
228638
228954
  const graceMs = 10 * 6e4;
228639
228955
  const now = Date.now();
228640
228956
  const nowIso3 = new Date(now).toISOString();
228641
- const live = (taskId) => activeSessionRegistry.pathsForTask(taskId).some((path6) => activeSessionRegistry.isPathActive(path6)) || executingTaskLock.has(taskId) || this.options.isTaskActive?.(taskId) === true;
228957
+ const live = (taskId) => activeSessionRegistry.pathsForTask(taskId).some((path6) => activeSessionRegistry.isPathActive(path6)) || executingTaskLock.has(taskId) || isPlanningLive(taskId) || this.options.isTaskActive?.(taskId) === true;
228642
228958
  const due = await this.store.listDueWorkflowWorkItems({
228643
228959
  now: nowIso3,
228644
228960
  states: [...ACTIVE_WORKFLOW_WORK_ITEM_STATES]
@@ -229015,6 +229331,109 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
229015
229331
  return 0;
229016
229332
  }
229017
229333
  }
229334
+ /**
229335
+ * FNXC:PreMergeApproval 2026-09-05-22:11:
229336
+ * FN-295: restore a required pre-merge gate that was archived as COLLATERAL of another gate's
229337
+ * remediation, so the card stops being permanently unmergeable with no owner.
229338
+ *
229339
+ * The wedge needs four ingredients, none of which is individually wrong: a review row left
229340
+ * `pending` by a dead session, the orphaned-step sweep rewriting it to `failed`, the remediation
229341
+ * archiver stamping EVERY terminal failure (not just its own gate), and the merge door treating
229342
+ * `remediationArchivedAt` as an unconditional veto. The result is a row the reseed cannot re-run
229343
+ * (it handles `missing`), the stale-content reroute cannot re-run (it handles `stale-content`),
229344
+ * and the operator bypass cannot select (it selects `status:"failed"`). Measured on FN-295: three
229345
+ * review restarts re-ran Code Review and Documentation to APPROVE and merged nothing.
229346
+ *
229347
+ * This sweep does NOT approve or merge anything: it undoes the collateral archive so the gate is
229348
+ * once again a visible failed step that the ordinary FN-7720 audited bypass — or a genuine re-run
229349
+ * — can clear. It never touches an operator waiver, the gate that owns the remediation wave, a
229350
+ * workspace card, a user-paused card, or a card with a live session, and it re-validates every
229351
+ * condition inside the atomic mutation.
229352
+ */
229353
+ async reconcileCollateralArchivedReviewGates() {
229354
+ try {
229355
+ const reviewColumns = await resolveProjectColumnsForRoles(this.store, REVIEW_ROLES);
229356
+ const candidates = /* @__PURE__ */ new Map();
229357
+ for (const column of reviewColumns) {
229358
+ for (const task of await this.store.listTasks({ column, slim: true })) candidates.set(task.id, task);
229359
+ }
229360
+ let repairedTasks = 0;
229361
+ const isSessionLive = (taskId) => {
229362
+ const livePaths = activeSessionRegistry.pathsForTask(taskId);
229363
+ return livePaths.some((path6) => activeSessionRegistry.isPathActive(path6)) || executingTaskLock.has(taskId) || this.options.isTaskActive?.(taskId) === true;
229364
+ };
229365
+ const remediationGateIdsFor = (task) => new Set(
229366
+ (task.steps ?? []).map((step) => step.remediation?.gateStepId).filter((gateStepId) => typeof gateStepId === "string" && gateStepId.length > 0)
229367
+ );
229368
+ for (const candidate of candidates.values()) {
229369
+ if (candidate.userPaused === true || candidate.workspaceWorktrees !== void 0 || isSessionLive(candidate.id)) continue;
229370
+ const fresh = await this.store.getTask(candidate.id);
229371
+ if (!fresh || !reviewColumns.has(fresh.column) || fresh.userPaused === true || fresh.workspaceWorktrees !== void 0 || isSessionLive(fresh.id)) continue;
229372
+ if (!(fresh.workflowStepResults ?? []).some((result) => resolveCollateralArchivedReviewGate(result, { remediationGateIds: remediationGateIdsFor(fresh) }))) continue;
229373
+ let requiredPreMergeStepIds;
229374
+ try {
229375
+ requiredPreMergeStepIds = (await resolvePreMergeGateForTask(
229376
+ this.store,
229377
+ fresh.id,
229378
+ fresh.enabledWorkflowSteps,
229379
+ fresh
229380
+ )).requiredPreMergeStepIds;
229381
+ } catch {
229382
+ continue;
229383
+ }
229384
+ if (requiredPreMergeStepIds.size === 0) continue;
229385
+ let repair;
229386
+ try {
229387
+ await this.store.updateTaskAtomic(fresh.id, (live) => {
229388
+ repair = void 0;
229389
+ if (!reviewColumns.has(live.column) || live.userPaused === true || live.workspaceWorktrees !== void 0 || isSessionLive(live.id)) return null;
229390
+ const remediationGateIds = remediationGateIdsFor(live);
229391
+ const restoredStepIds = [];
229392
+ const results = (live.workflowStepResults ?? []).map((result) => {
229393
+ if (!requiredPreMergeStepIds.has(result.workflowStepId)) return result;
229394
+ const resolution = resolveCollateralArchivedReviewGate(result, { remediationGateIds });
229395
+ if (!resolution) return result;
229396
+ restoredStepIds.push(result.workflowStepId);
229397
+ return resolution.restored;
229398
+ });
229399
+ if (restoredStepIds.length === 0) return null;
229400
+ repair = { stepIds: restoredStepIds, column: live.column, resultCount: results.length };
229401
+ return { workflowStepResults: results };
229402
+ });
229403
+ if (!repair) continue;
229404
+ repairedTasks += 1;
229405
+ } catch (error2) {
229406
+ log34.warn(`reconcileCollateralArchivedReviewGates: failed for ${fresh.id}: ${error2 instanceof Error ? error2.message : String(error2)}`);
229407
+ continue;
229408
+ }
229409
+ await this.store.logEntry(
229410
+ fresh.id,
229411
+ `[pre-merge] Restored collaterally archived review gate(s): ${repair.stepIds.join(", ")}`,
229412
+ COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC
229413
+ ).catch(() => void 0);
229414
+ await emitBoundedRunAudit2(this.store, {
229415
+ taskId: fresh.id,
229416
+ agentId: "self-healing",
229417
+ runId: generateSyntheticRunId("reconcile-collateral-archived-review-gate", fresh.id),
229418
+ domain: "database",
229419
+ mutationType: "task:reconcile-collateral-archived-review-gate",
229420
+ target: fresh.id,
229421
+ metadata: {
229422
+ taskId: fresh.id,
229423
+ column: repair.column,
229424
+ workflowStepId: repair.stepIds[0],
229425
+ restoredCount: repair.stepIds.length,
229426
+ resultCount: repair.resultCount
229427
+ }
229428
+ }, { log: log34 });
229429
+ }
229430
+ if (repairedTasks > 0) log34.log(`Restored collaterally archived review gates on ${repairedTasks} task(s)`);
229431
+ return repairedTasks;
229432
+ } catch (error2) {
229433
+ log34.error(`reconcileCollateralArchivedReviewGates failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
229434
+ return 0;
229435
+ }
229436
+ }
229018
229437
  /**
229019
229438
  * Backward lifecycle move gated on triple proof (FN-5335).
229020
229439
  * When the unproven fallback predicate fails, emits `task:finalize-no-op-review-no-action` and skips lifecycle mutation.
@@ -229374,25 +229793,14 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
229374
229793
  blocker check lacks a fresh Git descriptor. Capture the same merge content the merge queue uses and
229375
229794
  seed the graph back at Code Review, so cards like FN-9234 stop looping through doomed merge retries.
229376
229795
  */
229377
- async routeStaleSingularApprovalBackToReview(task, reviewColumns, settings) {
229378
- let mergeGate;
229379
- try {
229380
- mergeGate = await resolvePreMergeGateForTask(this.store, task.id, task.enabledWorkflowSteps, task);
229381
- } catch {
229382
- return false;
229383
- }
229384
- if (mergeGate.provenance === "default" && !mergeGate.selectionAbsent) return false;
229385
- const mergeContent = await captureMergeContentDescriptor(task, {
229386
- workspaceRootDir: this.options.rootDir,
229387
- settings
229388
- });
229796
+ async routeStaleSingularApprovalBackToReview(task, reviewColumns, mergeGate, mergeContent, parkShape) {
229389
229797
  const blocker = getTaskMergeBlocker(task, {
229390
229798
  reviewColumns,
229391
229799
  requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds,
229392
229800
  mergeContent
229393
229801
  });
229394
- if (blocker !== "task has a pre-merge approval recorded against different content" || mergeContent.kind !== "singular") {
229395
- return false;
229802
+ if (!isStaleContentApprovalBlocker(blocker) || mergeContent.kind !== "singular") {
229803
+ return { handled: false, seeded: false, parkCleared: false, mergeRetriesReset: false };
229396
229804
  }
229397
229805
  const reroute = await rerouteSingularStaleContentToReview(this.store, task, {
229398
229806
  requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds,
@@ -229403,11 +229811,25 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
229403
229811
  nodeId: void 0,
229404
229812
  workflowStepId: void 0
229405
229813
  }));
229814
+ let parkCleared = false;
229815
+ if (reroute.rerouted && parkShape) {
229816
+ await this.store.updateTaskAtomic(task.id, (live) => {
229817
+ if (classifyStaleContentPark(live) !== parkShape || live.paused || live.userPaused || live.deletedAt) {
229818
+ return null;
229819
+ }
229820
+ parkCleared = true;
229821
+ return { status: null, error: null, mergeRetries: 0 };
229822
+ });
229823
+ }
229824
+ const mergeRetriesReset = parkCleared;
229406
229825
  if (reroute.rerouted) {
229407
- await this.store.logEntry(task.id, "[pre-merge] Self-healing re-seeded Code Review after stale content evidence blocked merge.");
229826
+ await this.store.logEntry(task.id, `[pre-merge] Review lane '${reroute.nodeId ?? "code-review"}' approved older content; self-healing re-seeded it against current content.`);
229827
+ if (parkCleared) {
229828
+ await this.store.logEntry(task.id, `[pre-merge] Cleared the stale-content merge park after re-seeding review lane '${reroute.nodeId ?? "code-review"}' against current content.`);
229829
+ }
229408
229830
  log34.warn(`Stale content approval for ${task.id} re-seeded at ${reroute.nodeId ?? "code-review"}`);
229409
229831
  }
229410
- const auditKey = `${task.id}:${reroute.reason}:${reroute.nodeId ?? ""}`;
229832
+ const auditKey = `${task.id}:${reroute.reason}:${reroute.nodeId ?? ""}:${parkShape ?? ""}`;
229411
229833
  if (!this.staleContentRerouteAuditKeys.has(auditKey)) {
229412
229834
  this.staleContentRerouteAuditKeys.add(auditKey);
229413
229835
  await emitBoundedRunAudit2(this.store, {
@@ -229422,21 +229844,23 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
229422
229844
  nodeId: reroute.nodeId,
229423
229845
  workflowStepId: reroute.workflowStepId,
229424
229846
  reason: reroute.reason,
229425
- source: "self-healing"
229847
+ source: "self-healing",
229848
+ parkShape,
229849
+ parkCleared,
229850
+ mergeRetriesReset
229426
229851
  }
229427
229852
  });
229428
229853
  }
229429
- return true;
229854
+ return {
229855
+ handled: true,
229856
+ seeded: reroute.rerouted,
229857
+ nodeId: reroute.nodeId,
229858
+ parkShape,
229859
+ parkCleared,
229860
+ mergeRetriesReset
229861
+ };
229430
229862
  }
229431
- async routeUnrunPreMergeGateBackToReview(task, reviewColumns, settings) {
229432
- let mergeGate;
229433
- try {
229434
- mergeGate = await resolvePreMergeGateForTask(this.store, task.id, task.enabledWorkflowSteps, task);
229435
- } catch {
229436
- return false;
229437
- }
229438
- if (mergeGate.provenance === "default" && !mergeGate.selectionAbsent) return false;
229439
- const mergeContent = await captureMergeContentDescriptor(task, { workspaceRootDir: this.options.rootDir, settings });
229863
+ async routeUnrunPreMergeGateBackToReview(task, reviewColumns, mergeGate, mergeContent) {
229440
229864
  if (getTaskMergeBlocker(task, { reviewColumns, requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds, mergeContent }) !== "task has enabled pre-merge workflow steps that never ran") return false;
229441
229865
  const reroute = await rerouteUnrunPreMergeGateToReview(this.store, task, { requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds, mergeContent }).catch(() => ({ rerouted: false, reason: "no-unrun-gate", nodeId: void 0, workflowStepId: void 0 }));
229442
229866
  if (reroute.rerouted) {
@@ -229511,11 +229935,62 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
229511
229935
  for (const task of mergeable) {
229512
229936
  const reviewColumns = await ownReviewLanesFor(task);
229513
229937
  if (!reviewColumns.has(task.column)) continue;
229514
- if (await this.routeStaleSingularApprovalBackToReview(task, reviewColumns, settings)) continue;
229515
- if (await this.routeUnrunPreMergeGateBackToReview(task, reviewColumns, settings)) continue;
229516
- if (getTaskMergeBlocker(task, { reviewColumns }) !== void 0) continue;
229938
+ let mergeGate;
229939
+ try {
229940
+ mergeGate = await resolvePreMergeGateForTask(this.store, task.id, task.enabledWorkflowSteps, task);
229941
+ } catch {
229942
+ continue;
229943
+ }
229944
+ if (mergeGate.provenance === "default" && !mergeGate.selectionAbsent) continue;
229945
+ const mergeContent = await captureMergeContentDescriptor(task, { workspaceRootDir: this.options.rootDir, settings });
229946
+ if ((await this.routeStaleSingularApprovalBackToReview(task, reviewColumns, mergeGate, mergeContent)).handled) continue;
229947
+ if (await this.routeUnrunPreMergeGateBackToReview(task, reviewColumns, mergeGate, mergeContent)) continue;
229948
+ const blocker = getTaskMergeBlocker(task, {
229949
+ reviewColumns,
229950
+ requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds,
229951
+ mergeContent
229952
+ });
229953
+ if (blocker !== void 0) {
229954
+ const warningKey = `${task.id}:${blocker}`;
229955
+ if (!this.mergeRecoveryBlockerWarnKeys.has(warningKey)) {
229956
+ this.mergeRecoveryBlockerWarnKeys.add(warningKey);
229957
+ log34.warn(`mergeable-review recovery declined ${task.id}: ${blocker}`);
229958
+ }
229959
+ continue;
229960
+ }
229517
229961
  laneQualifiedMergeable.push(task);
229518
229962
  }
229963
+ const hiddenStaleContentCandidateIds = /* @__PURE__ */ new Set();
229964
+ for (const task of tasks2) {
229965
+ if (mergeable.includes(task)) continue;
229966
+ const parkShape = classifyStaleContentPark(task);
229967
+ if (!parkShape || mergeAdmissionByTaskId.get(task.id) !== true || executingIds.has(task.id) || task.status === "merging" || task.status === "merging-pr") continue;
229968
+ const reviewColumns = await ownReviewLanesFor(task);
229969
+ if (!reviewColumns.has(task.column)) continue;
229970
+ hiddenStaleContentCandidateIds.add(task.id);
229971
+ const attempts = (this.staleContentParkRecoveryAttempts.get(task.id) ?? 0) + 1;
229972
+ this.staleContentParkRecoveryAttempts.set(task.id, attempts);
229973
+ if (attempts === MAX_STARVATION_DROPS && !this.staleContentParkRecoveryBudgetLogged.has(task.id)) {
229974
+ this.staleContentParkRecoveryBudgetLogged.add(task.id);
229975
+ await this.store.logEntry(task.id, `[pre-merge] Stopped stale-content park recovery after ${MAX_STARVATION_DROPS} attempts; operator attention is required.`);
229976
+ }
229977
+ if (attempts > MAX_STARVATION_DROPS) continue;
229978
+ let mergeGate;
229979
+ try {
229980
+ mergeGate = await resolvePreMergeGateForTask(this.store, task.id, task.enabledWorkflowSteps, task);
229981
+ } catch {
229982
+ continue;
229983
+ }
229984
+ if (mergeGate.provenance === "default" && !mergeGate.selectionAbsent) continue;
229985
+ const mergeContent = await captureMergeContentDescriptor(task, { workspaceRootDir: this.options.rootDir, settings });
229986
+ await this.routeStaleSingularApprovalBackToReview(task, reviewColumns, mergeGate, mergeContent, parkShape);
229987
+ }
229988
+ for (const taskId of this.staleContentParkRecoveryAttempts.keys()) {
229989
+ if (!hiddenStaleContentCandidateIds.has(taskId)) {
229990
+ this.staleContentParkRecoveryAttempts.delete(taskId);
229991
+ this.staleContentParkRecoveryBudgetLogged.delete(taskId);
229992
+ }
229993
+ }
229519
229994
  if (unresolvedMergeableCards.length > 0) {
229520
229995
  log34.warn(
229521
229996
  `mergeable-review recovery: ${unresolvedMergeableCards.length} card(s) measured against the built-in review lane because their own workflow could not be resolved (${unresolvedMergeableCards.slice(0, 5).join(", ")}); a renamed review lane there stays stuck.`
@@ -229538,6 +230013,9 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
229538
230013
  );
229539
230014
  const enqueueMerge = this.options.enqueueMerge;
229540
230015
  let recovered = 0;
230016
+ let enqueued = 0;
230017
+ let merged = 0;
230018
+ let parked = 0;
229541
230019
  for (const task of unownedMergeable) {
229542
230020
  try {
229543
230021
  if (enqueueMerge) {
@@ -229553,19 +230031,22 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
229553
230031
  await this.store.updateTask(task.id, { status: "failed", error: error2 });
229554
230032
  await this.store.logEntry(task.id, error2);
229555
230033
  this.mergeStarvationDrops.delete(task.id);
230034
+ parked++;
229556
230035
  recovered++;
229557
230036
  }
229558
230037
  continue;
229559
230038
  }
229560
230039
  this.mergeStarvationDrops.delete(task.id);
230040
+ enqueued++;
229561
230041
  } else {
229562
230042
  await this.store.mergeTask(task.id);
230043
+ merged++;
229563
230044
  }
229564
230045
  await this.store.logEntry(
229565
230046
  task.id,
229566
230047
  enqueueMerge ? "Auto-recovered: eligible in-review task re-enqueued for merge" : "Auto-recovered: eligible in-review task was merged and moved to done"
229567
230048
  );
229568
- log34.log(`Recovered mergeable review task ${task.id}`);
230049
+ log34.log(enqueueMerge ? `Re-enqueued mergeable review task ${task.id} for merge` : `Merged review task ${task.id} and moved it to done`);
229569
230050
  recovered++;
229570
230051
  } catch (err) {
229571
230052
  const errorMessage2 = err instanceof Error ? err.message : String(err);
@@ -229573,7 +230054,7 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
229573
230054
  }
229574
230055
  }
229575
230056
  if (recovered > 0) {
229576
- log34.log(`Recovered ${recovered} mergeable review task(s) \u2192 done`);
230057
+ log34.log(formatMergeableReviewRecoverySummary({ enqueued, merged, parked }));
229577
230058
  }
229578
230059
  return recovered;
229579
230060
  } catch (err) {
@@ -236389,23 +236870,63 @@ init_context_limit_detector();
236389
236870
  init_usage_limit_detector();
236390
236871
  init_agent_tools();
236391
236872
  init_worktree_backend();
236392
- init_worktree_prune();
236393
- init_active_session_registry();
236394
236873
  import { exec as exec40 } from "node:child_process";
236395
236874
  import { promisify as promisify57 } from "node:util";
236396
236875
  import { existsSync as existsSync66 } from "node:fs";
236397
236876
  import { rm as rm10 } from "node:fs/promises";
236877
+
236878
+ // ../engine/src/execution/resolve-activity-run-agent-id.ts
236879
+ init_src2();
236880
+ var WORKFLOW_STEP_RUN_ATTRIBUTION_TIMEOUT_MS = 2e3;
236881
+ async function resolveWorkflowStepRunAgentId(store, candidate, options = {}) {
236882
+ if (!candidate?.trim()) return null;
236883
+ const resolve53 = async () => {
236884
+ if (typeof store?.getAgent === "function") {
236885
+ const agent = await store.getAgent(candidate);
236886
+ if (agent) return agent.id;
236887
+ }
236888
+ if (typeof store?.listAgents !== "function") return null;
236889
+ const matches = (await store.listAgents({ includeEphemeral: true })).filter(
236890
+ (agent) => agent.role === candidate || agent.roles?.includes(candidate)
236891
+ );
236892
+ const builtInOwners = matches.filter(
236893
+ (agent) => isBuiltinWorkflowRoleAgent(agent) && agent.metadata?.workflowRole === candidate
236894
+ );
236895
+ if (builtInOwners.length === 1) return builtInOwners[0].id;
236896
+ return matches.length === 1 ? matches[0].id : null;
236897
+ };
236898
+ let lookup2;
236899
+ try {
236900
+ lookup2 = Promise.resolve(resolve53());
236901
+ } catch {
236902
+ return null;
236903
+ }
236904
+ void lookup2.catch(() => void 0);
236905
+ return await new Promise((complete) => {
236906
+ const timer2 = setTimeout(() => complete(null), options.timeoutMs ?? WORKFLOW_STEP_RUN_ATTRIBUTION_TIMEOUT_MS);
236907
+ timer2.unref?.();
236908
+ void lookup2.then(
236909
+ (agentId) => {
236910
+ clearTimeout(timer2);
236911
+ complete(agentId);
236912
+ },
236913
+ () => {
236914
+ clearTimeout(timer2);
236915
+ complete(null);
236916
+ }
236917
+ );
236918
+ });
236919
+ }
236920
+
236921
+ // ../engine/src/execution/step-session-executor.ts
236922
+ init_worktree_prune();
236923
+ init_active_session_registry();
236398
236924
  var execAsync37 = promisify57(exec40);
236399
236925
  var stepExecLog = createLogger("step-session-executor");
236400
236926
  function parseStepFileScopes(prompt) {
236401
236927
  const result = /* @__PURE__ */ new Map();
236402
236928
  if (!prompt) return result;
236403
- const stepRegex = /^### Step (\d+):.*/gm;
236404
- const splits = [];
236405
- let match;
236406
- while ((match = stepRegex.exec(prompt)) !== null) {
236407
- splits.push({ index: match.index, stepNum: parseInt(match[1], 10) });
236408
- }
236929
+ const splits = matchStepHeadings(prompt).map(({ index: index2, headingNumber }) => ({ index: index2, stepNum: headingNumber }));
236409
236930
  if (splits.length === 0) return result;
236410
236931
  for (let i = 0; i < splits.length; i++) {
236411
236932
  const start = splits[i].index;
@@ -236681,12 +237202,7 @@ function scopePromptToWorktree(prompt, rootDir, worktreePath) {
236681
237202
  return prompt.replaceAll(`${rootDir}/`, `${worktreePath}/`).replaceAll(`${worktreePath}/.fusion/`, `${rootDir}/.fusion/`);
236682
237203
  }
236683
237204
  function extractStepSection(prompt, stepIndex) {
236684
- const stepRegex = /^### Step (\d+):.*/gm;
236685
- const splits = [];
236686
- let match;
236687
- while ((match = stepRegex.exec(prompt)) !== null) {
236688
- splits.push({ index: match.index, stepNum: parseInt(match[1], 10) });
236689
- }
237205
+ const splits = matchStepHeadings(prompt).map(({ index: index2, headingNumber }) => ({ index: index2, stepNum: headingNumber }));
236690
237206
  const offset = resolveAuthoredStepHeadingOffset(splits.map((split) => split.stepNum));
236691
237207
  const targetSplit = splits.find((s3) => s3.stepNum === stepIndex + offset);
236692
237208
  if (!targetSplit) return "";
@@ -236696,9 +237212,7 @@ function extractStepSection(prompt, stepIndex) {
236696
237212
  return prompt.slice(start, end).trim();
236697
237213
  }
236698
237214
  function countSteps(prompt) {
236699
- const stepRegex = /^### Step \d+:/gm;
236700
- const matches = prompt.match(stepRegex);
236701
- return matches ? matches.length : 0;
237215
+ return matchStepHeadings(prompt).length;
236702
237216
  }
236703
237217
  function resolveStepSection(taskDetail, prompt, stepIndex, authoredStepCount) {
236704
237218
  const authoredSection = extractStepSection(prompt, stepIndex);
@@ -236800,6 +237314,8 @@ var StepSessionExecutor = class {
236800
237314
  credentialInstanceId;
236801
237315
  reusablePrimaryRetargetPending = false;
236802
237316
  reusablePrimaryAttemptActive = false;
237317
+ workflowStepRunAgentId;
237318
+ warnedUnattributableWorkflowStepRun = false;
236803
237319
  registerActiveStepSession(stepIndex, handle2, worktreePath) {
236804
237320
  this.activeSessions.set(stepIndex, handle2);
236805
237321
  activeSessionRegistry.registerPath(worktreePath, {
@@ -237144,10 +237660,20 @@ var StepSessionExecutor = class {
237144
237660
  modelId: current.modelId
237145
237661
  };
237146
237662
  }
237147
- createWorkflowStepActivityRun(stepIndex, startedAt) {
237663
+ async createWorkflowStepActivityRun(stepIndex, startedAt) {
237148
237664
  const { taskDetail } = this.options;
237149
237665
  const step = taskDetail.steps?.[stepIndex];
237150
- const agentId = this.options.effectiveAgentId ?? taskDetail.assignedAgentId ?? "executor";
237666
+ if (typeof this.options.agentStore?.saveRun !== "function") return null;
237667
+ const agentIdCandidate = this.options.effectiveAgentId ?? taskDetail.assignedAgentId ?? "executor";
237668
+ this.workflowStepRunAgentId ??= resolveWorkflowStepRunAgentId(this.options.agentStore, agentIdCandidate);
237669
+ const agentId = await this.workflowStepRunAgentId;
237670
+ if (!agentId) {
237671
+ if (!this.warnedUnattributableWorkflowStepRun) {
237672
+ this.warnedUnattributableWorkflowStepRun = true;
237673
+ stepExecLog.warn(`Skipping unattributable workflow-step activity runs for task ${taskDetail.id} (candidate: ${agentIdCandidate})`);
237674
+ }
237675
+ return null;
237676
+ }
237151
237677
  return {
237152
237678
  id: `${generateSyntheticRunId("workflow-step", taskDetail.id)}-step-${stepIndex}`,
237153
237679
  agentId,
@@ -237167,6 +237693,7 @@ var StepSessionExecutor = class {
237167
237693
  taskTitle: taskDetail.title,
237168
237694
  assignedAgentId: taskDetail.assignedAgentId,
237169
237695
  effectiveAgentId: this.options.effectiveAgentId,
237696
+ agentIdCandidate,
237170
237697
  agentId,
237171
237698
  stepIndex,
237172
237699
  stepName: step?.name ?? `Step ${stepIndex}`
@@ -237181,6 +237708,7 @@ var StepSessionExecutor = class {
237181
237708
  };
237182
237709
  }
237183
237710
  async saveWorkflowStepActivityRun(run) {
237711
+ if (!run) return;
237184
237712
  const saveRun2 = this.options.agentStore?.saveRun?.bind(this.options.agentStore);
237185
237713
  if (!saveRun2) return;
237186
237714
  try {
@@ -237193,6 +237721,7 @@ var StepSessionExecutor = class {
237193
237721
  }
237194
237722
  }
237195
237723
  async completeWorkflowStepActivityRun(run, status, result) {
237724
+ if (!run) return;
237196
237725
  const terminalRun = {
237197
237726
  ...run,
237198
237727
  endedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -237252,7 +237781,7 @@ var StepSessionExecutor = class {
237252
237781
  if (semaphore) {
237253
237782
  await semaphore.acquire();
237254
237783
  }
237255
- const activityRun = this.createWorkflowStepActivityRun(stepIndex, (/* @__PURE__ */ new Date()).toISOString());
237784
+ const activityRun = await this.createWorkflowStepActivityRun(stepIndex, (/* @__PURE__ */ new Date()).toISOString());
237256
237785
  await this.saveWorkflowStepActivityRun(activityRun);
237257
237786
  const trackingKey = this.makeTrackingKey(stepIndex);
237258
237787
  let retries = 0;
@@ -238181,10 +238710,23 @@ function normalizeOptionalStepRevisionKey2(value) {
238181
238710
  function optionalStepRevisionKey2(nodeId, stepName) {
238182
238711
  return normalizeOptionalStepRevisionKey2(nodeId) || normalizeOptionalStepRevisionKey2(stepName) || "pre-merge-optional-step";
238183
238712
  }
238713
+ var OPTIONAL_STEP_REVISION_RESET_MARKER = "Workflow revision ledger reset:";
238714
+ function resetIndexFor(log42, normalizedKey) {
238715
+ for (let index2 = log42.length - 1; index2 >= 0; index2 -= 1) {
238716
+ const outcome = log42[index2]?.outcome ?? "";
238717
+ const markerIndex = outcome.indexOf(OPTIONAL_STEP_REVISION_RESET_MARKER);
238718
+ if (markerIndex < 0) continue;
238719
+ const markerValue = outcome.slice(markerIndex + OPTIONAL_STEP_REVISION_RESET_MARKER.length).split(/\r?\n/, 1)[0]?.trim();
238720
+ if (normalizeOptionalStepRevisionKey2(markerValue) === normalizedKey) return index2;
238721
+ }
238722
+ return -1;
238723
+ }
238184
238724
  function countOptionalStepRevisionAttempts2(task, key, stepName) {
238185
238725
  const normalizedKey = normalizeOptionalStepRevisionKey2(key);
238186
238726
  const normalizedStepName = normalizeOptionalStepRevisionKey2(stepName);
238187
- return (task.log ?? []).filter((entry) => {
238727
+ const log42 = task.log ?? [];
238728
+ const since = resetIndexFor(log42, normalizedKey);
238729
+ return (since >= 0 ? log42.slice(since + 1) : log42).filter((entry) => {
238188
238730
  const action = entry.action ?? "";
238189
238731
  const outcome = entry.outcome ?? "";
238190
238732
  if (!/attempt \d+\//.test(action)) return false;
@@ -239011,10 +239553,12 @@ ${JSON.stringify({ verdict: repairedVerdict })}`,
239011
239553
  session.dispose();
239012
239554
  await agentLogger.flush();
239013
239555
  if (parsed.verdict) {
239556
+ const findingsUnreadable = verdictRepairResult === "repaired" && (parsed.findings?.length ?? 0) === 0;
239014
239557
  const gated = reviewBlockingSeverity ? applyReviewSeverityGate({
239015
239558
  verdict: parsed.verdict,
239016
239559
  findings: parsed.findings,
239017
- threshold: reviewBlockingSeverity
239560
+ threshold: reviewBlockingSeverity,
239561
+ findingsUnreadable
239018
239562
  }) : void 0;
239019
239563
  const effectiveVerdict = gated?.verdict ?? parsed.verdict;
239020
239564
  if (gated?.downgraded) {
@@ -252725,7 +253269,7 @@ async function routeGraphMergeFailureToRetry(deps, live, result, abortProvenance
252725
253269
  const parked = await parkTaskFailed(
252726
253270
  deps.store,
252727
253271
  live.id,
252728
- `AUTO_MERGE_RETRY_REJECTED: ${reason}`,
253272
+ `${AUTO_MERGE_RETRY_REJECTED_PREFIX} ${reason}`,
252729
253273
  deps.getRunContextFor(live.id),
252730
253274
  live.columnMovedAt
252731
253275
  );
@@ -257222,9 +257766,13 @@ async function runImplementation(deps, task, graphCompletion, reportImplementati
257222
257766
  */
257223
257767
  credentialInstanceId: detail.credentialInstanceId,
257224
257768
  resolveCredentialInstanceRetarget: nextStepInstance,
257225
- // Attribute the per-step run auditor to the column agent when it governs
257226
- // (U4); absent → StepSessionExecutor falls back to assignedAgentId.
257227
- effectiveAgentId: stepColumnAgent?.agent.id,
257769
+ /*
257770
+ FNXC:ColumnAgent 2026-09-04-14:11:
257771
+ The effective step identity is the governing column agent or the authoritative assignee.
257772
+ Both are existence-proved reads; with empty project.boards the column seam is absent, so
257773
+ retaining only it lost attribution for every assigned task.
257774
+ */
257775
+ effectiveAgentId: stepIdentityAgent?.id,
257228
257776
  actionGateContext: deps.buildActionGateContext(task.id, stepIdentityAgent, settings.defaultAgentPermissionPolicy),
257229
257777
  permanentAgentGating: deps.buildPermanentAgentGatingContext(task.id, stepIdentityAgent, settings.defaultAgentPermissionPolicy),
257230
257778
  // FNXC:McpConfig 2026-06-25-23:03: Per-step workflow sessions are an executor lane, so they inherit the task's resolved MCP set from the effective step identity agent and never re-read or log plaintext secret values.
@@ -261806,10 +262354,22 @@ async function terminateAllChildren(deps, parentTaskId) {
261806
262354
 
261807
262355
  // ../engine/src/executor/clear-terminal-step-failures-for-retry.ts
261808
262356
  init_src2();
262357
+ function latestTerminalPreMergeFailureId(results) {
262358
+ return (results ?? []).filter((result) => (result.phase || "pre-merge") === "pre-merge" && (result.status === "failed" || result.status === "advisory_failure")).sort((left, right) => {
262359
+ const leftTime = Date.parse(left.completedAt || left.startedAt || "");
262360
+ const rightTime = Date.parse(right.completedAt || right.startedAt || "");
262361
+ return (Number.isFinite(rightTime) ? rightTime : 0) - (Number.isFinite(leftTime) ? leftTime : 0);
262362
+ })[0]?.workflowStepId;
262363
+ }
261809
262364
  async function clearTerminalStepFailuresForRetry(deps, taskId, mode) {
261810
262365
  const live = await deps.store.getTask(taskId).catch(() => null);
261811
262366
  if (!live) return;
261812
- const cleared = mode === "archive" ? archiveTerminalWorkflowStepFailures(live.workflowStepResults) : clearTerminalWorkflowStepFailures(live.workflowStepResults);
262367
+ const remediatedStepId = mode === "archive" ? latestTerminalPreMergeFailureId(live.workflowStepResults) : void 0;
262368
+ const cleared = mode === "archive" ? archiveTerminalWorkflowStepFailures(
262369
+ live.workflowStepResults,
262370
+ void 0,
262371
+ remediatedStepId ? { workflowStepIds: /* @__PURE__ */ new Set([remediatedStepId]) } : {}
262372
+ ) : clearTerminalWorkflowStepFailures(live.workflowStepResults);
261813
262373
  if (cleared !== live.workflowStepResults) {
261814
262374
  await deps.store.updateTask(taskId, { workflowStepResults: cleared }, deps.getRunContextFor(taskId));
261815
262375
  }
@@ -270446,10 +271006,14 @@ var MissionExecutionLoop = class extends EventEmitter26 {
270446
271006
  for (const slice of milestone.slices) {
270447
271007
  if (slice.status !== "active") continue;
270448
271008
  const supersededFixes = await this.missionStore.reconcileSupersededGeneratedFixFeatures(slice.id);
270449
- const supersededFeatureIds = new Set(supersededFixes.featureIds);
271009
+ const supersededFeatureIds = new Set(supersededFixes.featureIds ?? []);
271010
+ if ((supersededFixes.repairedCount ?? 0) > 0) {
271011
+ loopLog.warn(`Recovery: restored ${supersededFixes.repairedCount ?? 0} generated Fix Features in slice ${slice.id}: ${(supersededFixes.repairedFeatureIds ?? []).join(", ")}`);
271012
+ recoveredCount += supersededFixes.repairedCount ?? 0;
271013
+ }
270450
271014
  if (supersededFixes.supersededCount > 0) {
270451
271015
  loopLog.warn(
270452
- `Recovery: superseded ${supersededFixes.supersededCount} generated Fix Features in slice ${slice.id} because an ancestor feature already passed validation`
271016
+ `Recovery: superseded ${supersededFixes.supersededCount} generated Fix Features in slice ${slice.id} because an ancestor or the Fix feature's own validation already passed`
270453
271017
  );
270454
271018
  recoveredCount += supersededFixes.supersededCount;
270455
271019
  }
@@ -271828,6 +272392,7 @@ init_agent_session_helpers();
271828
272392
  init_effective_settings();
271829
272393
 
271830
272394
  // ../engine/src/spec-validation/task-document-references.ts
272395
+ init_src2();
271831
272396
  import { access as access11 } from "node:fs/promises";
271832
272397
  import { join as join95 } from "node:path";
271833
272398
  var TASK_PATH_REGEX = /\.fusion\/tasks\/[A-Z0-9]+(?:-[A-Z0-9]+)*\/[^\s`'"\]]+/g;
@@ -271840,12 +272405,11 @@ function collectTaskPaths(text2) {
271840
272405
  }
271841
272406
  function collectStepSections(stepsSection) {
271842
272407
  const sections2 = [];
271843
- const headingMatches = Array.from(stepsSection.matchAll(/^### Step (\d+):[^\n]*$/gm));
272408
+ const headingMatches = matchStepHeadings(stepsSection);
271844
272409
  for (let i = 0; i < headingMatches.length; i += 1) {
271845
272410
  const heading = headingMatches[i];
271846
- const start = (heading.index ?? 0) + heading[0].length;
271847
- const end = i + 1 < headingMatches.length ? headingMatches[i + 1].index ?? stepsSection.length : stepsSection.length;
271848
- sections2.push({ name: `Step ${heading[1]}`, body: stepsSection.slice(start, end).trim() });
272411
+ const end = i + 1 < headingMatches.length ? headingMatches[i + 1].index : stepsSection.length;
272412
+ sections2.push({ name: `Step ${heading.headingNumber}`, body: stepsSection.slice(heading.headingLineEnd, end).trim() });
271849
272413
  }
271850
272414
  return sections2;
271851
272415
  }
@@ -271979,30 +272543,6 @@ var PlanningResetFence = class {
271979
272543
  }
271980
272544
  };
271981
272545
 
271982
- // ../engine/src/agents/planning-liveness.ts
271983
- var probes = /* @__PURE__ */ new Set();
271984
- var planningLivenessRegistry = {
271985
- registerPlanningLivenessProbe(probe) {
271986
- probes.add(probe);
271987
- return () => probes.delete(probe);
271988
- },
271989
- isPlanningLive(taskId) {
271990
- for (const probe of probes) {
271991
- try {
271992
- if (probe(taskId)) return true;
271993
- } catch {
271994
- return true;
271995
- }
271996
- }
271997
- return false;
271998
- },
271999
- hasRegisteredProbe() {
272000
- return probes.size > 0;
272001
- }
272002
- };
272003
- var registerPlanningLivenessProbe = planningLivenessRegistry.registerPlanningLivenessProbe.bind(planningLivenessRegistry);
272004
- var isPlanningLive = planningLivenessRegistry.isPlanningLive.bind(planningLivenessRegistry);
272005
-
272006
272546
  // ../engine/src/triage.ts
272007
272547
  init_agent_instructions();
272008
272548
  init_fallback_model_observer();
@@ -274239,7 +274779,7 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
274239
274779
  planLog.warn(`${task.id} ${retryMessage}`);
274240
274780
  await this.store.logEntry(task.id, retryMessage);
274241
274781
  await this.updatePlanningStateIfStillCurrent(task, {
274242
- status: this.restoreStatusAfterInterruptedTriageWork(task),
274782
+ status: await this.resolvePlanningRetryHoldStatus(task, written),
274243
274783
  error: null,
274244
274784
  recoveryRetryCount: decision.nextState.recoveryRetryCount,
274245
274785
  nextRecoveryAt: decision.nextState.nextRecoveryAt
@@ -274276,9 +274816,13 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
274276
274816
  const retryMessage = `Generated plan failed deterministic validation (${deterministicSpecFailure}) \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}.`;
274277
274817
  planLog.warn(`${task.id} ${retryMessage}`);
274278
274818
  await this.store.logEntry(task.id, retryMessage);
274279
- const restoreStatus = this.restoreStatusAfterInterruptedTriageWork(task);
274819
+ await this.store.logEntry(
274820
+ task.id,
274821
+ "AI spec revision requested",
274822
+ `Deterministic plan validation rejected the generated plan: ${deterministicSpecFailure}`
274823
+ ).catch(() => void 0);
274280
274824
  await this.updatePlanningStateIfStillCurrent(task, {
274281
- status: restoreStatus,
274825
+ status: await this.resolvePlanningRetryHoldStatus(task, written),
274282
274826
  error: null,
274283
274827
  recoveryRetryCount: decision.nextState.recoveryRetryCount,
274284
274828
  nextRecoveryAt: decision.nextState.nextRecoveryAt
@@ -274448,9 +274992,10 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
274448
274992
  const retryMessage = `${failureMessage} \u2014 retry ${decision.nextState.recoveryRetryCount}/${MAX_RECOVERY_RETRIES} in ${formatDelay(decision.delayMs)}.`;
274449
274993
  planLog.warn(`${task.id} ${retryMessage}`);
274450
274994
  await this.store.logEntry(task.id, retryMessage).catch(() => void 0);
274995
+ const retryHoldStatus = await this.resolvePlanningRetryHoldStatus(task);
274451
274996
  await this.updatePlanningStateIfStillCurrent(task, (live) => ({
274452
274997
  ...persistMarker(live),
274453
- status: this.restoreStatusAfterInterruptedTriageWork(task),
274998
+ status: retryHoldStatus,
274454
274999
  error: null,
274455
275000
  recoveryRetryCount: decision.nextState.recoveryRetryCount,
274456
275001
  nextRecoveryAt: decision.nextState.nextRecoveryAt
@@ -274480,7 +275025,7 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
274480
275025
  planLog.warn(`${task.id}: failed to log transient-error retry entry: ${msg}`);
274481
275026
  });
274482
275027
  }
274483
- const restoreStatus = this.restoreStatusAfterInterruptedTriageWork(task);
275028
+ const restoreStatus = await this.resolvePlanningRetryHoldStatus(task);
274484
275029
  await this.updatePlanningStateIfStillCurrent(task, {
274485
275030
  status: restoreStatus,
274486
275031
  recoveryRetryCount: decision.nextState.recoveryRetryCount,
@@ -274532,7 +275077,7 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
274532
275077
  const msg = logErr instanceof Error ? logErr.message : String(logErr);
274533
275078
  planLog.warn(`${task.id}: failed to log planning-failure retry entry: ${msg}`);
274534
275079
  });
274535
- const restoreStatus = this.restoreStatusAfterInterruptedTriageWork(task);
275080
+ const restoreStatus = await this.resolvePlanningRetryHoldStatus(task);
274536
275081
  await this.updatePlanningStateIfStillCurrent(task, {
274537
275082
  status: restoreStatus,
274538
275083
  recoveryRetryCount: genericDecision.nextState.recoveryRetryCount,
@@ -274816,11 +275361,22 @@ ${lines.join("\n")}` }],
274816
275361
  }
274817
275362
  return null;
274818
275363
  }
275364
+ /*
275365
+ FNXC:TriagePlanningRetry 2026-09-05-22:06:
275366
+ A retry over a finished-looking PROMPT.md must hold `needs-replan`, matching scheduler validation
275367
+ retries. Clearing status makes the card dispatchable and bypasses finalizeApprovedTask approval.
275368
+ */
275369
+ async resolvePlanningRetryHoldStatus(task, knownPromptContent) {
275370
+ const restored = this.restoreStatusAfterInterruptedTriageWork(task);
275371
+ if (restored) return restored;
275372
+ const content = knownPromptContent ?? await this.readNonEmptyPromptDraft(task.id, "planning retry hold");
275373
+ return content && !isTaskAwaitingPlanning(task, content) ? "needs-replan" : null;
275374
+ }
274819
275375
  async validateGeneratedPrompt(taskId, promptContent) {
274820
275376
  if (!promptContent.trim()) {
274821
275377
  return "PROMPT.md file not found or empty";
274822
275378
  }
274823
- const headingNumbers = Array.from(promptContent.matchAll(/^### Step (\d+):/gm), (match) => Number(match[1]));
275379
+ const headingNumbers = matchStepHeadings(promptContent).map((match) => match.headingNumber);
274824
275380
  if (headingNumbers.length > 0 && !headingNumbers.every((heading, index2) => heading === index2)) {
274825
275381
  const diagnostic = `Step headings must be contiguous 0-based execution indices (observed: ${headingNumbers.join(", ")}). Renumber from Step 0 and update prose cross-references.`;
274826
275382
  planLog.warn(`${taskId}: ${diagnostic}`);
@@ -287735,7 +288291,7 @@ ${detail}`);
287735
288291
  metadata: { taskId: task.id, nodeId: reroute.nodeId, workflowStepId: reroute.workflowStepId, reason: reroute.reason, source: "merge-gate", missingGateCount: mergeGate.requiredPreMergeStepIds.size }
287736
288292
  });
287737
288293
  }
287738
- if (blocker === "task has a pre-merge approval recorded against different content" && mergeContent.kind === "singular") {
288294
+ if (isStaleContentApprovalBlocker(blocker) && mergeContent.kind === "singular") {
287739
288295
  const reroute = await rerouteSingularStaleContentToReview(store, task, {
287740
288296
  requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds,
287741
288297
  mergeContent
@@ -287745,9 +288301,10 @@ ${detail}`);
287745
288301
  nodeId: void 0,
287746
288302
  workflowStepId: void 0
287747
288303
  }));
287748
- if (reroute.rerouted) {
287749
- await store.logEntry(task.id, `[pre-merge] Code Review re-entry is owned by the workflow graph after stale content evidence was refused.`);
287750
- }
288304
+ await store.logEntry(
288305
+ task.id,
288306
+ `[pre-merge] Review lane '${reroute.nodeId ?? "unknown"}' approved older content; re-review is owned by the workflow graph (${reroute.reason}).`
288307
+ );
287751
288308
  const auditKey = `${task.id}:${reroute.reason}`;
287752
288309
  if (!this.staleContentRerouteAuditKeys.has(auditKey)) {
287753
288310
  this.staleContentRerouteAuditKeys.add(auditKey);