@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
package/dist/extension.js CHANGED
@@ -1840,10 +1840,11 @@ function toSnapshot(result2) {
1840
1840
  const { priorAttempts: _drop, ...rest } = result2;
1841
1841
  return rest;
1842
1842
  }
1843
- function archiveTerminalWorkflowStepFailures(results, archivedAt = (/* @__PURE__ */ new Date()).toISOString()) {
1844
- if (!results?.some(isTerminalFailure)) return results;
1843
+ function archiveTerminalWorkflowStepFailures(results, archivedAt = (/* @__PURE__ */ new Date()).toISOString(), options = {}) {
1844
+ const inScope = (result2) => isTerminalFailure(result2) && (!options.workflowStepIds || options.workflowStepIds.has(result2.workflowStepId));
1845
+ if (!results?.some(inScope)) return results;
1845
1846
  return results.map((result2) => {
1846
- if (!isTerminalFailure(result2)) return result2;
1847
+ if (!inScope(result2)) return result2;
1847
1848
  const snapshot = toSnapshot(result2);
1848
1849
  const priorAttempts = [snapshot, ...result2.priorAttempts ?? []].slice(0, MAX_WORKFLOW_STEP_PRIOR_ATTEMPTS);
1849
1850
  const {
@@ -2019,6 +2020,26 @@ function isAuditedOperatorBypass(result2) {
2019
2020
  actor && !AUTOMATED_BYPASS_ACTORS.has(actor) && result2.bypassedAt?.trim() && result2.bypassReason?.trim()
2020
2021
  );
2021
2022
  }
2023
+ function resolveCollateralArchivedReviewGate(result2, options) {
2024
+ const archivedFrom = result2.remediationArchivedFromStatus;
2025
+ if ((result2.phase ?? "pre-merge") !== "pre-merge" || result2.status !== "skipped" || result2.remediationArchivedAt == null || result2.bypassedBy !== void 0 || archivedFrom !== "failed" && archivedFrom !== "advisory_failure" || options.remediationGateIds.has(result2.workflowStepId)) {
2026
+ return void 0;
2027
+ }
2028
+ const {
2029
+ remediationArchivedAt: _archivedAt,
2030
+ remediationArchivedFromStatus: _archivedFrom,
2031
+ ...rest
2032
+ } = result2;
2033
+ return {
2034
+ restored: {
2035
+ ...rest,
2036
+ status: archivedFrom,
2037
+ output: COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC,
2038
+ notes: COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC
2039
+ },
2040
+ reason: COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC
2041
+ };
2042
+ }
2022
2043
  function resolveUnprovenReviewApproval(result2, options) {
2023
2044
  if ((result2.phase ?? "pre-merge") !== "pre-merge" || !requiresContentReviewProof(result2.workflowStepId, result2) || options.workspace || result2.status !== "passed" || result2.verdict !== "APPROVE" && result2.verdict !== "APPROVE_WITH_NOTES" || result2.reviewInputFingerprint !== void 0 || result2.bypassedBy !== void 0 || result2.remediationArchivedAt != null) {
2024
2045
  return void 0;
@@ -2050,9 +2071,10 @@ function evaluateStep(workflowStepId, results, task, descriptor) {
2050
2071
  const isPlanDomain = workflowStepId === PLAN_REVIEW_GROUP_ID || result2.reviewKind === "plan";
2051
2072
  const notRunApproves = isWorkflowStepNotRun(result2) && !requiresAuthoredVerdict && !isPlanDomain;
2052
2073
  const approved = result2.status === "passed" && (requiresAuthoredVerdict ? approvedVerdict : result2.verdict === void 0 || approvedVerdict) || result2.status === "skipped" && !!result2.bypassedBy || notRunApproves;
2053
- if (!approved || !!result2.remediationArchivedAt) return { workflowStepId, state: "not-approved" };
2074
+ const auditedOperatorWaiver = isAuditedOperatorBypass(result2) && descriptor?.kind !== "workspace";
2075
+ if (!approved || result2.remediationArchivedAt != null && !auditedOperatorWaiver) return { workflowStepId, state: "not-approved" };
2054
2076
  if (result2.reviewKind === "plan") return { workflowStepId, state: "approved" };
2055
- if (isAuditedOperatorBypass(result2) && descriptor?.kind !== "workspace") {
2077
+ if (auditedOperatorWaiver) {
2056
2078
  return { workflowStepId, state: "approved" };
2057
2079
  }
2058
2080
  const bindsContent = requiresExplicitVerdict || result2.reviewInputFingerprint !== void 0;
@@ -2085,7 +2107,7 @@ function evaluateStep(workflowStepId, results, task, descriptor) {
2085
2107
  if (stale2.length) return { workflowStepId, state: "stale-content", repositories: stale2 };
2086
2108
  return { workflowStepId, state: "approved" };
2087
2109
  }
2088
- var AUTOMATED_BYPASS_ACTORS, UNPROVEN_REVIEW_APPROVAL_DIAGNOSTIC;
2110
+ var AUTOMATED_BYPASS_ACTORS, UNPROVEN_REVIEW_APPROVAL_DIAGNOSTIC, COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC;
2089
2111
  var init_pre_merge_approval = __esm({
2090
2112
  "../core/src/merge/pre-merge-approval.ts"() {
2091
2113
  "use strict";
@@ -2094,6 +2116,7 @@ var init_pre_merge_approval = __esm({
2094
2116
  init_workflow_step_results();
2095
2117
  AUTOMATED_BYPASS_ACTORS = /* @__PURE__ */ new Set([FAST_MODE_BYPASS_ACTOR]);
2096
2118
  UNPROVEN_REVIEW_APPROVAL_DIAGNOSTIC = "Content-binding review approval recorded without reviewInputFingerprint; approval invalidated so the gate can run again.";
2119
+ 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.";
2097
2120
  }
2098
2121
  });
2099
2122
 
@@ -2202,6 +2225,9 @@ function hasNonTerminalSteps(task) {
2202
2225
  function isPreMergeStepsNotRunBlocker(blocker) {
2203
2226
  return blocker === PRE_MERGE_STEPS_NOT_RUN_BLOCKER;
2204
2227
  }
2228
+ function isStaleContentApprovalBlocker(blocker) {
2229
+ return typeof blocker === "string" && blocker.trim().endsWith(STALE_CONTENT_APPROVAL_BLOCKER);
2230
+ }
2205
2231
  function getTaskMergeBlocker(task, options = {}) {
2206
2232
  if (!options.skipColumnIdentityCheck) {
2207
2233
  const reviewColumns = options.reviewColumns?.size ? options.reviewColumns : void 0;
@@ -2223,8 +2249,10 @@ function getTaskMergeBlocker(task, options = {}) {
2223
2249
  }
2224
2250
  const approval = evaluatePreMergeApprovals(task, options).find((candidate) => candidate.state !== "approved");
2225
2251
  if (approval?.state === "missing") return PRE_MERGE_STEPS_NOT_RUN_BLOCKER;
2226
- if (approval?.state === "not-approved") return "task has enabled pre-merge workflow steps without a current approval";
2227
- if (approval?.state === "stale-content") return "task has a pre-merge approval recorded against different content";
2252
+ if (approval?.state === "not-approved") {
2253
+ return `task has enabled pre-merge workflow steps without a current approval (gate '${approval.workflowStepId}')`;
2254
+ }
2255
+ if (approval?.state === "stale-content") return STALE_CONTENT_APPROVAL_BLOCKER;
2228
2256
  if (approval?.state === "unprovable-content") return "task has no provable approval for the content being merged";
2229
2257
  if (task.workflowStepResults?.some((result2) => {
2230
2258
  const phase = result2.phase || "pre-merge";
@@ -2241,11 +2269,14 @@ function getTaskMergeBlocker(task, options = {}) {
2241
2269
  return void 0;
2242
2270
  }
2243
2271
  function getLatestFailedPreMergeReviewStep(task) {
2244
- return (task.workflowStepResults ?? []).filter((result2) => (result2.phase || "pre-merge") === "pre-merge" && result2.status === "failed").sort((a, b3) => {
2272
+ const results = task.workflowStepResults ?? [];
2273
+ const recentFirst = (a, b3) => {
2245
2274
  const aTs = Date.parse(a.completedAt || a.startedAt || "");
2246
2275
  const bTs = Date.parse(b3.completedAt || b3.startedAt || "");
2247
2276
  return (Number.isFinite(bTs) ? bTs : 0) - (Number.isFinite(aTs) ? aTs : 0);
2248
- })[0];
2277
+ };
2278
+ const isPreMerge = (result2) => (result2.phase || "pre-merge") === "pre-merge";
2279
+ return results.filter((result2) => isPreMerge(result2) && result2.status === "failed").sort(recentFirst)[0] ?? results.filter((result2) => isPreMerge(result2) && isArchivedRemediationCarrier(result2) && (result2.remediationArchivedFromStatus === "failed" || result2.remediationArchivedFromStatus === "advisory_failure") && !result2.bypassedBy && !result2.supersededAt).sort(recentFirst)[0];
2249
2280
  }
2250
2281
  function clearMergeConfirmedTransientStatus(status) {
2251
2282
  return status !== void 0 && MERGE_CONFIRMED_TRANSIENT_STATUSES.has(status) ? void 0 : status;
@@ -2329,13 +2360,14 @@ function findPendingPreMergeStep(task) {
2329
2360
  (a, b3) => new Date(b3.startedAt ?? 0).getTime() - new Date(a.startedAt ?? 0).getTime()
2330
2361
  )[0];
2331
2362
  }
2332
- 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;
2363
+ 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;
2333
2364
  var init_task_merge = __esm({
2334
2365
  "../core/src/merge/task-merge.ts"() {
2335
2366
  "use strict";
2336
2367
  init_branch_group_completion();
2337
2368
  init_task_helpers();
2338
2369
  init_pre_merge_approval();
2370
+ init_workflow_step_results();
2339
2371
  FUSION_SIBLING_BRANCH_RE = /^fusion\/fn-/i;
2340
2372
  AWAITING_APPROVAL_PAUSE_REASON = "awaiting-approval";
2341
2373
  HARD_BLOCKING_TASK_STATUSES = /* @__PURE__ */ new Set([
@@ -2379,6 +2411,7 @@ var init_task_merge = __esm({
2379
2411
  "pending"
2380
2412
  ]);
2381
2413
  PRE_MERGE_STEPS_NOT_RUN_BLOCKER = "task has enabled pre-merge workflow steps that never ran";
2414
+ STALE_CONTENT_APPROVAL_BLOCKER = "task has a pre-merge approval recorded against different content";
2382
2415
  PreMergeStepsNotRunError = class extends Error {
2383
2416
  code = "pre-merge-steps-not-run";
2384
2417
  taskId;
@@ -6358,8 +6391,9 @@ function renderValidationCause(diagnostics8) {
6358
6391
  }
6359
6392
  function featureValidationRepairEligibility(feature) {
6360
6393
  const repairableLoop = feature.loopState === "blocked" || feature.loopState === "needs_fix";
6394
+ const unvalidatedPassedMarker = feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId;
6361
6395
  return {
6362
- clear: repairableLoop || feature.status === "blocked",
6396
+ clear: repairableLoop || feature.status === "blocked" || unvalidatedPassedMarker,
6363
6397
  reRun: repairableLoop || feature.status === "blocked" && (feature.loopState === void 0 || feature.loopState === "idle")
6364
6398
  };
6365
6399
  }
@@ -6423,7 +6457,8 @@ var init_mission_types = __esm({
6423
6457
  implementing: [],
6424
6458
  validating: [],
6425
6459
  needs_fix: ["idle", "implementing"],
6426
- passed: [],
6460
+ /* FNXC:Missions 2026-09-05-22:07: Explicit repair may return a fabricated unvalidated passed marker to idle (issue #3574). */
6461
+ passed: ["idle"],
6427
6462
  blocked: ["idle", "implementing"]
6428
6463
  };
6429
6464
  VALIDATOR_RUN_STATUSES = ["running", "passed", "failed", "blocked", "error"];
@@ -13530,7 +13565,7 @@ function isBlockingFinding(finding, threshold) {
13530
13565
  if (!finding.severity) return true;
13531
13566
  return SEVERITY_RANK[finding.severity] >= SEVERITY_RANK[threshold];
13532
13567
  }
13533
- function applyReviewSeverityGate({ verdict, findings, threshold }) {
13568
+ function applyReviewSeverityGate({ verdict, findings, threshold, findingsUnreadable }) {
13534
13569
  const all = findings ?? [];
13535
13570
  const open7 = all.filter(isActionableReviewFinding);
13536
13571
  const resolved = all.filter((finding) => !isActionableReviewFinding(finding));
@@ -13538,6 +13573,7 @@ function applyReviewSeverityGate({ verdict, findings, threshold }) {
13538
13573
  const advisory = open7.filter((finding) => !isBlockingFinding(finding, threshold));
13539
13574
  if (verdict !== "REVISE") return { verdict, downgraded: false, blocking, advisory, resolved };
13540
13575
  if (blocking.length > 0) return { verdict, downgraded: false, blocking, advisory, resolved };
13576
+ if (findingsUnreadable) return { verdict, downgraded: false, blocking, advisory, resolved };
13541
13577
  return { verdict: "APPROVE_WITH_NOTES", downgraded: true, blocking, advisory, resolved };
13542
13578
  }
13543
13579
  function formatFindingsByPriority(findings) {
@@ -18279,6 +18315,18 @@ function detectWorkflowDrift(ir2, pin) {
18279
18315
  function defaultCodingWorkflowIr() {
18280
18316
  return resolveDefaultWorkflowIr();
18281
18317
  }
18318
+ async function prefetchWorkflowSelections(store, taskIds, cache) {
18319
+ const missingIds = [...new Set(taskIds)].filter((id) => !cache.has(id));
18320
+ if (missingIds.length === 0) return { batched: 0, singles: 0 };
18321
+ if (!store.getTaskWorkflowSelectionsAsync) return { batched: 0, singles: missingIds.length };
18322
+ try {
18323
+ const selections = await store.getTaskWorkflowSelectionsAsync(missingIds);
18324
+ for (const id of missingIds) cache.set(id, selections.get(id));
18325
+ return { batched: 1, singles: 0 };
18326
+ } catch {
18327
+ return { batched: 0, singles: missingIds.length };
18328
+ }
18329
+ }
18282
18330
  function resolveSeamPromptFromIr(ir2, seam) {
18283
18331
  for (const node of ir2.nodes) {
18284
18332
  if (node.kind !== "prompt") continue;
@@ -18497,9 +18545,9 @@ function resolveLifecycleColumns(ir2) {
18497
18545
  archived: first(LIFECYCLE_ROLE_FLAGS.archived)
18498
18546
  };
18499
18547
  }
18500
- async function resolveTaskLifecycleColumns(store, taskId, cache) {
18548
+ async function resolveTaskLifecycleColumns(store, taskId, cache, selectionCache) {
18501
18549
  try {
18502
- const ir2 = await resolveWorkflowIrForTask(store, taskId, cache);
18550
+ const ir2 = await resolveWorkflowIrForTask(store, taskId, cache, selectionCache);
18503
18551
  return resolveLifecycleColumns(ir2);
18504
18552
  } catch {
18505
18553
  return void 0;
@@ -30495,18 +30543,33 @@ function isValidPluginParserId(id) {
30495
30543
  const seg = /^[a-z0-9-]+$/;
30496
30544
  return seg.test(parts[1]) && seg.test(parts[2]);
30497
30545
  }
30498
- function parseStepHeadings(content) {
30499
- const steps = [];
30546
+ function matchStepHeadings(content) {
30500
30547
  const stepRegex = /^###\s+Step\s+\d+[^:]*:\s*(.+)$/gm;
30501
- const annotatedRegex = /^###\s+Step\s+\d+\s*\(depends:\s*([^)]*)\)\s*:\s*([^\n]+)$/;
30502
30548
  const matches = [];
30503
30549
  let match;
30504
30550
  while ((match = stepRegex.exec(content)) !== null) {
30505
30551
  const heading = /^###\s+Step\s+(\d+)/.exec(match[0]);
30506
- if (heading) {
30507
- matches.push({ full: match[0], name: match[1], headingNumber: Number(heading[1]) });
30508
- }
30552
+ if (!heading) continue;
30553
+ const index2 = match.index;
30554
+ const newline = content.indexOf("\n", index2);
30555
+ matches.push({
30556
+ headingNumber: Number(heading[1]),
30557
+ index: index2,
30558
+ headingLineEnd: newline === -1 ? content.length : newline,
30559
+ match: match[0]
30560
+ });
30509
30561
  }
30562
+ return matches;
30563
+ }
30564
+ function parseStepHeadings(content) {
30565
+ const steps = [];
30566
+ const annotatedRegex = /^###\s+Step\s+\d+\s*\(depends:\s*([^)]*)\)\s*:\s*([^\n]+)$/;
30567
+ const matches = matchStepHeadings(content).map((entry) => ({
30568
+ full: entry.match,
30569
+ name: /^###\s+Step\s+\d+[^:]*:\s*(.+)$/m.exec(entry.match)?.[1] ?? "",
30570
+ headingNumber: entry.headingNumber
30571
+ }));
30572
+ let match;
30510
30573
  const offset = resolveAuthoredStepHeadingOffset(matches.map((entry) => entry.headingNumber));
30511
30574
  for (const { full, name: legacyName } of matches) {
30512
30575
  if (!full.includes("(depends:")) {
@@ -75103,8 +75166,8 @@ function appendAgentLogEntriesSync(taskDir, entries) {
75103
75166
  if (entries.length === 0) return [];
75104
75167
  const filePath = getAgentLogFilePath(taskDir);
75105
75168
  mkdirSync5(dirname14(filePath), { recursive: true });
75106
- const startingLineNo = countLineNumbers(filePath);
75107
- const payload = entries.map((entry) => serializeEntry(entry)).join("");
75169
+ const { lineCount: startingLineNo, endsWithNewline } = inspectAgentLogTail(filePath);
75170
+ const payload = `${startingLineNo > 0 && !endsWithNewline ? "\n" : ""}${entries.map((entry) => serializeEntry(entry)).join("")}`;
75108
75171
  appendFileSync(filePath, payload, "utf8");
75109
75172
  return entries.map((entry, index2) => materializeEntry(entry, startingLineNo + index2 + 1));
75110
75173
  }
@@ -75140,6 +75203,7 @@ function readAllAgentLogEntries(taskDir, options = {}) {
75140
75203
  }
75141
75204
  const lines = content.split("\n");
75142
75205
  const entries = [];
75206
+ const skippedLines = [];
75143
75207
  for (let index2 = 0; index2 < lines.length; index2 += 1) {
75144
75208
  const rawLine = lines[index2];
75145
75209
  if (!rawLine) continue;
@@ -75161,9 +75225,18 @@ function readAllAgentLogEntries(taskDir, options = {}) {
75161
75225
  }
75162
75226
  entries.push(entry);
75163
75227
  } catch (error3) {
75164
- log7.warn(`Skipping malformed JSONL line ${lineNo} in ${filePath}`, error3);
75228
+ skippedLines.push({
75229
+ lineNo,
75230
+ message: error3 instanceof Error ? error3.message : String(error3)
75231
+ });
75165
75232
  }
75166
75233
  }
75234
+ const firstSkipped = skippedLines[0];
75235
+ if (firstSkipped) {
75236
+ log7.warn(
75237
+ `Skipped ${skippedLines.length} malformed JSONL line(s) in ${filePath}; first line ${firstSkipped.lineNo}: ${firstSkipped.message}`
75238
+ );
75239
+ }
75167
75240
  return entries;
75168
75241
  }
75169
75242
  function normalizeTimingMs(value) {
@@ -75206,16 +75279,19 @@ function materializeEntry(entry, lineNo) {
75206
75279
  sourceRef: buildAgentLogSourceRef(entry.taskId, lineNo)
75207
75280
  };
75208
75281
  }
75209
- function countLineNumbers(filePath) {
75282
+ function inspectAgentLogTail(filePath) {
75210
75283
  if (!existsSync16(filePath)) {
75211
- return 0;
75284
+ return { lineCount: 0, endsWithNewline: true };
75212
75285
  }
75213
75286
  const content = readFileSync7(filePath, "utf8");
75214
75287
  if (content.length === 0) {
75215
- return 0;
75288
+ return { lineCount: 0, endsWithNewline: true };
75216
75289
  }
75217
75290
  const lines = content.split("\n");
75218
- return lines.at(-1) === "" ? lines.length - 1 : lines.length;
75291
+ return {
75292
+ lineCount: lines.at(-1) === "" ? lines.length - 1 : lines.length,
75293
+ endsWithNewline: content.endsWith("\n")
75294
+ };
75219
75295
  }
75220
75296
  function pruneAgentLogFiles(tasksDir, retentionDays, scanTaskIds) {
75221
75297
  if (!Number.isFinite(retentionDays) || retentionDays <= 0 || !existsSync16(tasksDir)) {
@@ -79005,6 +79081,10 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0;
79005
79081
  * FNXC:Missions 2026-07-11-12:35:
79006
79082
  * A generated fix can also supersede itself once its own validator/loop evidence has passed.
79007
79083
  * Reconciliation treats that as terminal evidence so a completed fix does not stay active only because its ancestor previously failed.
79084
+ *
79085
+ * FNXC:Missions 2026-09-05-22:07:
79086
+ * Superseding a generated fix means it is no longer needed, not that it passed validation.
79087
+ * Never stamp unearned validator evidence because that re-arms the reconciler's own trigger (issue #3574).
79008
79088
  */
79009
79089
  reconcileSupersededGeneratedFixFeatures(sliceId) {
79010
79090
  const features = this.listFeatures(sliceId);
@@ -79029,7 +79109,42 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0;
79029
79109
  ancestorPassedMemo.set(feature.id, passed);
79030
79110
  return passed;
79031
79111
  };
79032
- 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);
79112
+ const isFabricatedMarker = (feature) => Boolean(
79113
+ (feature.generatedFromFeatureId || feature.generatedFromRunId) && feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId && !feature.taskId && feature.status === "done" && !hasPassedAncestor(feature)
79114
+ );
79115
+ const repairedFeatureIds = features.filter(isFabricatedMarker).map((feature) => feature.id);
79116
+ const repairedIds = new Set(repairedFeatureIds);
79117
+ 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);
79118
+ if (repairedFeatureIds.length > 0) {
79119
+ this.db.transaction(() => {
79120
+ const hasCurrentPassedAncestor = (feature, seen = /* @__PURE__ */ new Set()) => {
79121
+ const sourceFeatureId = feature.generatedFromFeatureId;
79122
+ if (!sourceFeatureId || seen.has(sourceFeatureId)) return false;
79123
+ seen.add(sourceFeatureId);
79124
+ const sourceFeature = this.getFeature(sourceFeatureId);
79125
+ return featureHasPassed(sourceFeature) || (sourceFeature ? hasCurrentPassedAncestor(sourceFeature, seen) : false);
79126
+ };
79127
+ const isCurrentFabricatedMarker = (feature) => Boolean(
79128
+ (feature.generatedFromFeatureId || feature.generatedFromRunId) && feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId && !feature.taskId && feature.status === "done" && !hasCurrentPassedAncestor(feature)
79129
+ );
79130
+ for (const featureId of repairedFeatureIds) {
79131
+ const feature = this.getFeature(featureId);
79132
+ if (!feature || !isCurrentFabricatedMarker(feature)) continue;
79133
+ this.updateFeature(featureId, { status: "defined", taskId: void 0, loopState: "idle", lastValidatorStatus: void 0 });
79134
+ const slice = this.getSlice(feature.sliceId);
79135
+ const milestone = slice ? this.getMilestone(slice.milestoneId) : void 0;
79136
+ if (milestone) {
79137
+ this.logMissionEvent(milestone.missionId, "feature_status_changed", `Feature status changed from ${feature.status} to defined`, {
79138
+ featureId,
79139
+ field: "status",
79140
+ from: feature.status,
79141
+ to: "defined",
79142
+ source: "superseded-fix-marker-repair"
79143
+ });
79144
+ }
79145
+ }
79146
+ });
79147
+ }
79033
79148
  if (supersededFeatureIds.length > 0) {
79034
79149
  this.db.transaction(() => {
79035
79150
  for (const featureId of supersededFeatureIds) {
@@ -79038,8 +79153,7 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0;
79038
79153
  this.updateFeature(featureId, {
79039
79154
  status: "done",
79040
79155
  taskId: void 0,
79041
- loopState: "passed",
79042
- lastValidatorStatus: "passed"
79156
+ loopState: "passed"
79043
79157
  });
79044
79158
  if (feature.taskId) {
79045
79159
  this.db.prepare('UPDATE tasks SET missionId = NULL, sliceId = NULL WHERE id = ? AND "deletedAt" IS NULL').run(feature.taskId);
@@ -79049,7 +79163,9 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0;
79049
79163
  }
79050
79164
  return {
79051
79165
  supersededCount: supersededFeatureIds.length,
79052
- featureIds: supersededFeatureIds
79166
+ featureIds: supersededFeatureIds,
79167
+ repairedCount: repairedFeatureIds.length,
79168
+ repairedFeatureIds
79053
79169
  };
79054
79170
  }
79055
79171
  /**
@@ -82393,10 +82509,12 @@ var init_async_mission_store = __esm({
82393
82509
  throw new RepairNotEligibleError(featureId, options.action);
82394
82510
  }
82395
82511
  const now = (/* @__PURE__ */ new Date()).toISOString();
82512
+ const hasUnvalidatedMarker = options.action === "clear" && feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId;
82513
+ const appliesUnvalidatedMarker = hasUnvalidatedMarker && Boolean(feature.generatedFromFeatureId || feature.generatedFromRunId) && !feature.taskId && feature.status === "done";
82396
82514
  const priorLoopState = feature.loopState;
82397
82515
  const priorStatus = feature.status;
82398
82516
  let groundTruthMetadata = {};
82399
- if (options.action === "clear" && feature.status === "blocked") {
82517
+ if (options.action === "clear" && (feature.status === "blocked" || appliesUnvalidatedMarker)) {
82400
82518
  const fence = options.groundTruth;
82401
82519
  if (!fence || fence.featureId !== featureId || fence.taskId !== (feature.taskId ?? null)) {
82402
82520
  throw new RepairGroundTruthStaleError(featureId);
@@ -82426,14 +82544,14 @@ var init_async_mission_store = __esm({
82426
82544
  let updated;
82427
82545
  let run;
82428
82546
  if (options.action === "clear") {
82429
- const currentLoop = feature.loopState;
82430
- const appliesLoop = currentLoop === "blocked" || currentLoop === "needs_fix";
82547
+ const currentLoop = feature.loopState ?? "idle";
82548
+ const appliesLoop = currentLoop === "blocked" || currentLoop === "needs_fix" || appliesUnvalidatedMarker;
82431
82549
  const nextLoop = appliesLoop ? options.resolvedLoopState ?? "idle" : currentLoop;
82432
82550
  if (appliesLoop && !FEATURE_LOOP_REPAIR_TRANSITIONS[currentLoop].includes(nextLoop)) {
82433
82551
  throw new Error(`Invalid validation repair transition from '${currentLoop}' to '${nextLoop}'`);
82434
82552
  }
82435
- const appliesStatus = feature.status === "blocked";
82436
- const nextStatus = appliesStatus ? options.resolvedStatus : feature.status;
82553
+ const appliesStatus = feature.status === "blocked" || appliesUnvalidatedMarker;
82554
+ const nextStatus = appliesUnvalidatedMarker ? "defined" : appliesStatus ? options.resolvedStatus : feature.status;
82437
82555
  if (appliesStatus && (nextStatus !== "in-progress" && nextStatus !== "triaged" && nextStatus !== "defined")) {
82438
82556
  throw new Error("Validation repair requires resolvedStatus of in-progress, triaged, or defined");
82439
82557
  }
@@ -82445,13 +82563,13 @@ var init_async_mission_store = __esm({
82445
82563
  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";
82446
82564
  if (!matchesLane) throw new RepairGroundTruthStaleError(featureId);
82447
82565
  }
82448
- if (!appliesLoop && !appliesStatus) throw new RepairNotEligibleError(featureId, options.action);
82566
+ if (!appliesLoop && !appliesStatus && !hasUnvalidatedMarker) throw new RepairNotEligibleError(featureId, options.action);
82449
82567
  updated = {
82450
82568
  ...feature,
82451
82569
  loopState: nextLoop,
82452
82570
  status: nextStatus,
82453
82571
  implementationAttemptCount: 0,
82454
- ...feature.lastValidatorStatus === "blocked" || feature.lastValidatorStatus === "failed" ? { lastValidatorStatus: void 0 } : {},
82572
+ ...feature.lastValidatorStatus === "blocked" || feature.lastValidatorStatus === "failed" || hasUnvalidatedMarker ? { lastValidatorStatus: void 0 } : {},
82455
82573
  updatedAt: now
82456
82574
  };
82457
82575
  await updateFeature(tx, updated);
@@ -82919,10 +83037,60 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0,
82919
83037
  const source = byId.get(sourceId);
82920
83038
  return passed(source) || (source ? hasPassedAncestor(source, seen) : false);
82921
83039
  };
83040
+ const isFabricatedMarker = (feature) => Boolean(
83041
+ (feature.generatedFromFeatureId || feature.generatedFromRunId) && feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId && !feature.taskId && feature.status === "done" && !hasPassedAncestor(feature)
83042
+ );
83043
+ const repairIds = new Set(features.filter(isFabricatedMarker).map((feature) => feature.id));
82922
83044
  const ids = [];
82923
83045
  for (const feature of features) {
82924
- if (!feature.generatedFromFeatureId || !(passed(feature) || hasPassedAncestor(feature))) continue;
82925
- if (feature.status !== "done" || feature.loopState !== "passed" || feature.lastValidatorStatus !== "passed" || feature.taskId) ids.push(feature.id);
83046
+ if (repairIds.has(feature.id) || !feature.generatedFromFeatureId || !(passed(feature) || hasPassedAncestor(feature))) continue;
83047
+ if (feature.status !== "done" || feature.loopState !== "passed" || feature.taskId) ids.push(feature.id);
83048
+ }
83049
+ const repairedFeatureIds = [];
83050
+ if (repairIds.size > 0) {
83051
+ const now = (/* @__PURE__ */ new Date()).toISOString();
83052
+ const repaired = await this.layer.transactionImmediate(async (tx) => {
83053
+ const locked = await tx.select({ id: project_exports.missionFeatures.id }).from(project_exports.missionFeatures).where(inArray(project_exports.missionFeatures.id, [...repairIds])).for("update");
83054
+ const preImages = locked.length > 0 ? await listFeaturesByIds(tx, locked.map((row) => row.id)) : [];
83055
+ const currentById = new Map(preImages.map((feature) => [feature.id, feature]));
83056
+ let missingAncestorIds = [...new Set(preImages.map((feature) => feature.generatedFromFeatureId).filter((id) => Boolean(id) && !currentById.has(id)))];
83057
+ while (missingAncestorIds.length > 0) {
83058
+ const lockedAncestors = await tx.select({ id: project_exports.missionFeatures.id }).from(project_exports.missionFeatures).where(inArray(project_exports.missionFeatures.id, missingAncestorIds)).for("update");
83059
+ const ancestors = lockedAncestors.length > 0 ? await listFeaturesByIds(tx, lockedAncestors.map((row) => row.id)) : [];
83060
+ for (const ancestor of ancestors) currentById.set(ancestor.id, ancestor);
83061
+ missingAncestorIds = [...new Set(ancestors.map((ancestor) => ancestor.generatedFromFeatureId).filter((id) => Boolean(id) && !currentById.has(id)))];
83062
+ }
83063
+ const hasCurrentPassedAncestor = (feature, seen = /* @__PURE__ */ new Set()) => {
83064
+ const sourceId = feature.generatedFromFeatureId;
83065
+ if (!sourceId || seen.has(sourceId)) return false;
83066
+ seen.add(sourceId);
83067
+ const source = currentById.get(sourceId);
83068
+ return passed(source) || (source ? hasCurrentPassedAncestor(source, seen) : false);
83069
+ };
83070
+ const changed = preImages.filter((feature) => Boolean(
83071
+ (feature.generatedFromFeatureId || feature.generatedFromRunId) && feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId && !feature.taskId && feature.status === "done" && !hasCurrentPassedAncestor(feature)
83072
+ ));
83073
+ if (changed.length === 0) return { events: [], features: [] };
83074
+ await tx.update(project_exports.missionFeatures).set({
83075
+ status: "defined",
83076
+ taskId: null,
83077
+ loopState: "idle",
83078
+ lastValidatorStatus: null,
83079
+ updatedAt: now
83080
+ }).where(inArray(project_exports.missionFeatures.id, changed.map((feature) => feature.id)));
83081
+ let seq = await getMaxEventSeq(tx);
83082
+ const events = [];
83083
+ for (const feature of changed) {
83084
+ const event = await this.recordFeatureStatusChange(tx, feature, "defined", { type: "system", id: "mission-store", source: "superseded-fix-marker-repair" }, void 0, ++seq);
83085
+ if (event) events.push(event);
83086
+ }
83087
+ return { events, features: changed };
83088
+ });
83089
+ for (const event of repaired.events) this.emit("mission:event", event);
83090
+ for (const feature of repaired.features) {
83091
+ this.emit("feature:updated", { ...feature, status: "defined", taskId: void 0, loopState: "idle", lastValidatorStatus: void 0, updatedAt: now });
83092
+ repairedFeatureIds.push(feature.id);
83093
+ }
82926
83094
  }
82927
83095
  if (ids.length > 0) {
82928
83096
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -82930,13 +83098,12 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0,
82930
83098
  const locked = await tx.select({ id: project_exports.missionFeatures.id }).from(project_exports.missionFeatures).where(inArray(project_exports.missionFeatures.id, ids)).for("update");
82931
83099
  const lockedIds = locked.map((row) => row.id);
82932
83100
  const preImages = lockedIds.length > 0 ? await listFeaturesByIds(tx, lockedIds) : [];
82933
- const changed = preImages.filter((feature) => feature.status !== "done" || feature.loopState !== "passed" || feature.lastValidatorStatus !== "passed" || feature.taskId);
83101
+ const changed = preImages.filter((feature) => feature.status !== "done" || feature.loopState !== "passed" || feature.taskId);
82934
83102
  if (changed.length === 0) return { events: [], updatedFeatures: [] };
82935
83103
  await tx.update(project_exports.missionFeatures).set({
82936
83104
  status: "done",
82937
83105
  taskId: null,
82938
83106
  loopState: "passed",
82939
- lastValidatorStatus: "passed",
82940
83107
  updatedAt: now
82941
83108
  }).where(inArray(project_exports.missionFeatures.id, changed.map((feature) => feature.id)));
82942
83109
  let seq = await getMaxEventSeq(tx);
@@ -82951,13 +83118,14 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0,
82951
83118
  });
82952
83119
  for (const event of events) this.emit("mission:event", event);
82953
83120
  for (const feature of updatedFeatures) {
82954
- const updated = { ...feature, status: "done", taskId: void 0, loopState: "passed", lastValidatorStatus: "passed", updatedAt: now };
83121
+ const updated = { ...feature, status: "done", taskId: void 0, loopState: "passed", updatedAt: now };
82955
83122
  this.emit("feature:updated", updated);
82956
83123
  if (feature.taskId) await clearTaskMissionLinkage(this.db, feature.taskId);
82957
83124
  }
82958
83125
  if (updatedFeatures.length > 0) await this.recomputeSliceStatus(sliceId);
82959
83126
  }
82960
- return { supersededCount: ids.length, featureIds: ids };
83127
+ if (repairedFeatureIds.length > 0) await this.recomputeSliceStatus(sliceId);
83128
+ return { supersededCount: ids.length, featureIds: ids, repairedCount: repairedFeatureIds.length, repairedFeatureIds };
82961
83129
  }
82962
83130
  async transitionLoopState(featureId, newState) {
82963
83131
  const feature = await getFeature(this.db, featureId);
@@ -88589,19 +88757,81 @@ async function archiveAllDoneImpl(store, options) {
88589
88757
  for (const column of completeColumns) {
88590
88758
  for (const task of await store.listTasks({ slim: true, column })) doneById.set(task.id, task);
88591
88759
  }
88592
- const doneTasks = [...doneById.values()];
88593
- if (doneTasks.length === 0) {
88594
- return [];
88760
+ if (doneById.size === 0) return { archived: [], skipped: [] };
88761
+ const archivedColumns = await resolveArchivedLanes(store).catch(() => void 0) ?? new Set(LEGACY_ARCHIVE_LANES);
88762
+ const liveTasks = (await store.listTasks({ slim: true, includeArchived: false })).filter((task) => !task.deletedAt && !archivedColumns.has(task.column));
88763
+ const childrenByParentId = /* @__PURE__ */ new Map();
88764
+ for (const task of liveTasks) {
88765
+ if (!task.sourceParentTaskId) continue;
88766
+ const children = childrenByParentId.get(task.sourceParentTaskId) ?? [];
88767
+ children.push(task.id);
88768
+ childrenByParentId.set(task.sourceParentTaskId, children);
88769
+ }
88770
+ for (const children of childrenByParentId.values()) children.sort();
88771
+ const bypassLineageGate = options?.removeLineageReferences === true;
88772
+ const archivableIds = new Set(doneById.keys());
88773
+ const skipped = [];
88774
+ if (!bypassLineageGate) {
88775
+ let firstRemovalWave = true;
88776
+ while (true) {
88777
+ const removals = [...archivableIds].filter(
88778
+ (id) => (childrenByParentId.get(id) ?? []).some((childId) => !archivableIds.has(childId))
88779
+ ).sort();
88780
+ if (removals.length === 0) break;
88781
+ for (const id of removals) {
88782
+ const blockers = (childrenByParentId.get(id) ?? []).filter((childId) => !archivableIds.has(childId));
88783
+ skipped.push({
88784
+ id,
88785
+ reason: firstRemovalWave ? "open-lineage-children" : "blocked-by-unarchived-batch-member",
88786
+ blockers,
88787
+ message: firstRemovalWave ? `Live lineage children prevent archiving ${id}.` : `Unarchived batch members prevent archiving ${id}.`
88788
+ });
88789
+ archivableIds.delete(id);
88790
+ }
88791
+ firstRemovalWave = false;
88792
+ }
88793
+ }
88794
+ const archived = [];
88795
+ const remaining = new Set(archivableIds);
88796
+ const archivedIds = /* @__PURE__ */ new Set();
88797
+ const archiveWave = async (ids) => {
88798
+ const settled = await Promise.allSettled(ids.map((id) => store.archiveTask(id, {
88799
+ cleanup: true,
88800
+ removeLineageReferences: options?.removeLineageReferences
88801
+ })));
88802
+ for (const [index2, result2] of settled.entries()) {
88803
+ const id = ids[index2];
88804
+ if (result2.status === "fulfilled") {
88805
+ archived.push(result2.value);
88806
+ archivedIds.add(id);
88807
+ } else {
88808
+ const error3 = result2.reason;
88809
+ const blockers = error3 instanceof TaskHasLineageChildrenError ? error3.childIds : error3 instanceof TaskHasDependentsError ? error3.dependentIds : [];
88810
+ skipped.push({ id, reason: "archive-failed", blockers, message: error3 instanceof Error ? error3.message : String(error3) });
88811
+ }
88812
+ remaining.delete(id);
88813
+ }
88814
+ };
88815
+ while (remaining.size > 0) {
88816
+ const wave = [...remaining].filter(
88817
+ (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))
88818
+ ).sort();
88819
+ if (wave.length > 0) {
88820
+ await archiveWave(wave);
88821
+ continue;
88822
+ }
88823
+ if (bypassLineageGate) {
88824
+ await archiveWave([...remaining].sort());
88825
+ } else {
88826
+ for (const id of [...remaining].sort()) {
88827
+ const blockers = (childrenByParentId.get(id) ?? []).filter((childId) => remaining.has(childId) || !archivedIds.has(childId));
88828
+ skipped.push({ id, reason: "blocked-by-unarchived-batch-member", blockers, message: `Unarchived batch members prevent archiving ${id}.` });
88829
+ remaining.delete(id);
88830
+ }
88831
+ }
88595
88832
  }
88596
- const archivedTasks3 = await Promise.all(
88597
- doneTasks.map(
88598
- (task) => store.archiveTask(task.id, {
88599
- cleanup: true,
88600
- removeLineageReferences: options?.removeLineageReferences
88601
- })
88602
- )
88603
- );
88604
- return archivedTasks3;
88833
+ skipped.sort((left, right) => left.id.localeCompare(right.id));
88834
+ return { archived, skipped };
88605
88835
  }
88606
88836
  async function resolveUnarchiveTargetColumnImpl(store, preArchiveColumn, taskId) {
88607
88837
  const lifecycle2 = taskId ? await resolveTaskLifecycleColumns(store, taskId) : void 0;
@@ -89065,6 +89295,7 @@ var init_task_artifacts_ops = __esm({
89065
89295
  init_types();
89066
89296
  init_store();
89067
89297
  init_project_lane_vocabulary();
89298
+ init_errors3();
89068
89299
  LEGACY_ARCHIVE_LANES = ["archived"];
89069
89300
  }
89070
89301
  });
@@ -106936,18 +107167,18 @@ function hasFreshAgentLogActivitySinceTaskUpdate(store, task, now, reviewColumns
106936
107167
  }
106937
107168
  return Math.max(0, now - latestAgentLogMs) < DEFAULT_STALE_MERGING_MIN_AGE_MS;
106938
107169
  }
106939
- async function resolveHoldColumnForTask(store, taskId, cache) {
107170
+ async function resolveHoldColumnForTask(store, taskId, cache, selectionCache) {
106940
107171
  try {
106941
- const lifecycle2 = resolveLifecycleColumns(await resolveWorkflowIrForTask(store, taskId, cache));
107172
+ const lifecycle2 = resolveLifecycleColumns(await resolveWorkflowIrForTask(store, taskId, cache, selectionCache));
106942
107173
  return lifecycle2?.hold ?? "todo";
106943
107174
  } catch {
106944
107175
  return "todo";
106945
107176
  }
106946
107177
  }
106947
- async function resolveReviewColumnsForTask(store, taskId, cache) {
107178
+ async function resolveReviewColumnsForTask(store, taskId, cache, selectionCache) {
106948
107179
  const columns = /* @__PURE__ */ new Set(["in-review"]);
106949
107180
  try {
106950
- const ir2 = await resolveWorkflowIrForTask(store, taskId, cache);
107181
+ const ir2 = await resolveWorkflowIrForTask(store, taskId, cache, selectionCache);
106951
107182
  if (ir2) for (const id of resolveReviewColumns(ir2)) columns.add(id);
106952
107183
  } catch {
106953
107184
  }
@@ -107068,6 +107299,12 @@ async function listTasksImpl(store, options) {
107068
107299
  const settings = await store.getSettingsFast();
107069
107300
  const mergeQueuedTaskIds = await store.getMergeQueuedTaskIdsAsync();
107070
107301
  const listPassIrCache = /* @__PURE__ */ new Map();
107302
+ const listPassSelectionCache = options?.selectionCache ?? /* @__PURE__ */ new Map();
107303
+ const listSelectionReads = await prefetchWorkflowSelections(store, filteredRows.map((row) => String(row.id)), listPassSelectionCache);
107304
+ if (options?.selectionReadTally) {
107305
+ options.selectionReadTally.batched += listSelectionReads.batched;
107306
+ options.selectionReadTally.singles += listSelectionReads.singles;
107307
+ }
107071
107308
  const staleThresholds = {
107072
107309
  inProgressWarningMs: settings.staleInProgressWarningMs,
107073
107310
  inProgressCriticalMs: settings.staleInProgressCriticalMs,
@@ -107078,7 +107315,7 @@ async function listTasksImpl(store, options) {
107078
107315
  const row = store.pgRowToTaskRow(pgRow);
107079
107316
  const task = store.rowToTask(row);
107080
107317
  const isMergeQueued = mergeQueuedTaskIds.has(task.id);
107081
- const reviewColumnsForRow = await resolveReviewColumnsForTask(store, task.id, listPassIrCache);
107318
+ const reviewColumnsForRow = await resolveReviewColumnsForTask(store, task.id, listPassIrCache, listPassSelectionCache);
107082
107319
  const hasFreshAgentLogActivity = hasFreshAgentLogActivitySinceTaskUpdate(store, task, now, reviewColumnsForRow);
107083
107320
  const executingTaskIds = hasFreshAgentLogActivity ? /* @__PURE__ */ new Set([task.id]) : void 0;
107084
107321
  task.inReviewStall = isMergeQueued ? void 0 : getInReviewStallReason(task, {
@@ -107110,7 +107347,7 @@ async function listTasksImpl(store, options) {
107110
107347
  thresholdMs: settings.stalePausedTodoThresholdMs,
107111
107348
  // Paused-only (the signal is a no-op otherwise), sharing the list-pass
107112
107349
  // IR cache so one workflow is read once per pass, not once per card.
107113
- holdColumn: task.paused === true ? await resolveHoldColumnForTask(store, task.id, listPassIrCache) : void 0,
107350
+ holdColumn: task.paused === true ? await resolveHoldColumnForTask(store, task.id, listPassIrCache, listPassSelectionCache) : void 0,
107114
107351
  engineActiveSinceMs: settings.engineActiveSinceMs,
107115
107352
  engineActivationGraceMs: settings.engineActivationGraceMs
107116
107353
  });
@@ -107120,7 +107357,7 @@ async function listTasksImpl(store, options) {
107120
107357
  thresholds: staleThresholds,
107121
107358
  engineActiveSinceMs: settings.engineActiveSinceMs,
107122
107359
  engineActivationGraceMs: settings.engineActivationGraceMs,
107123
- lifecycle: await resolveTaskLifecycleColumns(store, task.id, listPassIrCache)
107360
+ lifecycle: await resolveTaskLifecycleColumns(store, task.id, listPassIrCache, listPassSelectionCache)
107124
107361
  });
107125
107362
  } catch (err) {
107126
107363
  if (!(err instanceof RangeError)) throw err;
@@ -107199,12 +107436,14 @@ async function listTasksModifiedSinceImpl(store, since, limit, opts) {
107199
107436
  const lifecycleByTaskId = /* @__PURE__ */ new Map();
107200
107437
  {
107201
107438
  const irCache = /* @__PURE__ */ new Map();
107439
+ const selectionCache = /* @__PURE__ */ new Map();
107440
+ await prefetchWorkflowSelections(store, pageRows.map((row) => row.id), selectionCache);
107202
107441
  for (const pgRow of pageRows) {
107203
107442
  const row = store.pgRowToTaskRow(pgRow);
107204
- reviewColumnsByTaskId.set(row.id, await resolveReviewColumnsForTask(store, row.id, irCache));
107205
- lifecycleByTaskId.set(row.id, await resolveTaskLifecycleColumns(store, row.id, irCache));
107443
+ reviewColumnsByTaskId.set(row.id, await resolveReviewColumnsForTask(store, row.id, irCache, selectionCache));
107444
+ lifecycleByTaskId.set(row.id, await resolveTaskLifecycleColumns(store, row.id, irCache, selectionCache));
107206
107445
  if (store.rowToTask(row).paused !== true) continue;
107207
- holdColumnByTaskId.set(row.id, await resolveHoldColumnForTask(store, row.id, irCache));
107446
+ holdColumnByTaskId.set(row.id, await resolveHoldColumnForTask(store, row.id, irCache, selectionCache));
107208
107447
  }
107209
107448
  }
107210
107449
  const tasks2 = pageRows.map((pgRow) => {
@@ -107322,10 +107561,12 @@ async function searchTasksImpl(store, query, options) {
107322
107561
  const settings = await store.getSettingsFast();
107323
107562
  const mergeQueuedTaskIds = await store.getMergeQueuedTaskIdsAsync();
107324
107563
  const searchPassIrCache = /* @__PURE__ */ new Map();
107564
+ const searchPassSelectionCache = /* @__PURE__ */ new Map();
107565
+ await prefetchWorkflowSelections(store, pgRows.map((row) => String(row.id)), searchPassSelectionCache);
107325
107566
  const tasks2 = await Promise.all(pgRows.map(async (pgRow) => {
107326
107567
  const task = store.rowToTask(store.pgRowToTaskRow(pgRow));
107327
107568
  const isMergeQueued = mergeQueuedTaskIds.has(task.id);
107328
- const reviewColumnsForRow = await resolveReviewColumnsForTask(store, task.id, searchPassIrCache);
107569
+ const reviewColumnsForRow = await resolveReviewColumnsForTask(store, task.id, searchPassIrCache, searchPassSelectionCache);
107329
107570
  const hasFreshAgentLogActivity = hasFreshAgentLogActivitySinceTaskUpdate(store, task, now, reviewColumnsForRow);
107330
107571
  const executingTaskIds = hasFreshAgentLogActivity ? /* @__PURE__ */ new Set([task.id]) : void 0;
107331
107572
  task.inReviewStall = isMergeQueued ? void 0 : getInReviewStallReason(task, {
@@ -108565,6 +108806,7 @@ var init_store = __esm({
108565
108806
  init_async_self_healing();
108566
108807
  init_task_merge();
108567
108808
  init_required_pre_merge_steps();
108809
+ init_pre_merge_approval();
108568
108810
  init_logger();
108569
108811
  init_project_root_guard();
108570
108812
  init_agent_store();
@@ -110330,29 +110572,35 @@ var init_store = __esm({
110330
110572
  const results = task.workflowStepResults ?? [];
110331
110573
  const failedTarget = getLatestFailedPreMergeReviewStep(task);
110332
110574
  const reviewIrForBypass = failedTarget ? void 0 : await resolveWorkflowIrForTask(this, task.id);
110333
- const absentStepId = reviewIrForBypass ? [...resolveRequiredPreMergeStepIds(reviewIrForBypass, task.enabledWorkflowSteps, task)].find((workflowStepId) => !results.some((result2) => result2.workflowStepId === workflowStepId)) : void 0;
110334
- if (!failedTarget && !absentStepId) {
110575
+ const requiredForBypass = reviewIrForBypass ? [...resolveRequiredPreMergeStepIds(reviewIrForBypass, task.enabledWorkflowSteps, task)] : [];
110576
+ const absentStepId = requiredForBypass.find((workflowStepId) => !results.some((result2) => result2.workflowStepId === workflowStepId));
110577
+ const unapprovedTarget = !failedTarget && !absentStepId && requiredForBypass.length > 0 ? (() => {
110578
+ const blocked = evaluatePreMergeApprovals(task, { requiredPreMergeStepIds: new Set(requiredForBypass) }).find((approval) => approval.state !== "approved");
110579
+ return blocked ? results.find((result2) => result2.workflowStepId === blocked.workflowStepId) : void 0;
110580
+ })() : void 0;
110581
+ if (!failedTarget && !absentStepId && !unapprovedTarget) {
110335
110582
  throw new Error(`Cannot bypass review lane for ${id}: no failed pre-merge review step found`);
110336
110583
  }
110337
- const target = failedTarget ?? {
110584
+ const target = failedTarget ?? unapprovedTarget ?? {
110338
110585
  workflowStepId: absentStepId,
110339
110586
  workflowStepName: absentStepId,
110340
110587
  phase: "pre-merge",
110341
110588
  status: "absent"
110342
110589
  };
110343
- const targetIndex = failedTarget ? results.indexOf(failedTarget) : -1;
110344
- if (failedTarget && targetIndex === -1) {
110590
+ const rowTarget = failedTarget ?? unapprovedTarget;
110591
+ const targetIndex = rowTarget ? results.indexOf(rowTarget) : -1;
110592
+ if (rowTarget && targetIndex === -1) {
110345
110593
  throw new Error(`Cannot bypass review lane for ${id}: failed step result not found`);
110346
110594
  }
110347
110595
  const now = (/* @__PURE__ */ new Date()).toISOString();
110348
- const bypassed = failedTarget ? {
110349
- ...failedTarget,
110596
+ const bypassed = rowTarget ? {
110597
+ ...rowTarget,
110350
110598
  status: "skipped",
110351
110599
  bypassedBy: actor,
110352
110600
  bypassedAt: now,
110353
110601
  bypassReason: reason,
110354
- bypassedFromStatus: failedTarget.status,
110355
- bypassedFromVerdict: failedTarget.verdict
110602
+ bypassedFromStatus: rowTarget.remediationArchivedFromStatus ?? rowTarget.status,
110603
+ bypassedFromVerdict: rowTarget.verdict ?? rowTarget.priorAttempts?.[0]?.verdict
110356
110604
  } : {
110357
110605
  workflowStepId: absentStepId,
110358
110606
  workflowStepName: absentStepId,
@@ -110386,8 +110634,8 @@ var init_store = __esm({
110386
110634
  metadata: {
110387
110635
  workflowStepId: target.workflowStepId,
110388
110636
  workflowStepName: target.workflowStepName,
110389
- bypassedFromStatus: target.status,
110390
- bypassedFromVerdict: failedTarget?.verdict ?? null,
110637
+ bypassedFromStatus: bypassed.bypassedFromStatus,
110638
+ bypassedFromVerdict: bypassed.bypassedFromVerdict ?? null,
110391
110639
  reason
110392
110640
  }
110393
110641
  });
@@ -110449,9 +110697,8 @@ var init_store = __esm({
110449
110697
  );
110450
110698
  }
110451
110699
  if (target.status !== "pending") {
110452
- throw new Error(
110453
- `Cannot resume workflow step for ${id}: only pending steps can be resumed`
110454
- );
110700
+ const archivedFailure = target.remediationArchivedAt != null && (target.remediationArchivedFromStatus === "failed" || target.remediationArchivedFromStatus === "advisory_failure") && !target.bypassedBy && !target.supersededAt;
110701
+ 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)` : ""}`);
110455
110702
  }
110456
110703
  const now = (/* @__PURE__ */ new Date()).toISOString();
110457
110704
  const resumed = {
@@ -140959,6 +141206,7 @@ __export(src_exports, {
140959
141206
  CLOUD_TICKET_QUERY: () => CLOUD_TICKET_QUERY,
140960
141207
  CODE_REVIEW_BLOCKING_SEVERITY_SETTING_ID: () => CODE_REVIEW_BLOCKING_SEVERITY_SETTING_ID,
140961
141208
  CODE_REVIEW_MAX_REVISIONS_SETTING_ID: () => CODE_REVIEW_MAX_REVISIONS_SETTING_ID,
141209
+ COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC: () => COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC,
140962
141210
  COLOR_THEMES: () => COLOR_THEMES,
140963
141211
  COLUMNS: () => COLUMNS,
140964
141212
  COLUMN_DESCRIPTIONS: () => COLUMN_DESCRIPTIONS,
@@ -141361,6 +141609,7 @@ __export(src_exports, {
141361
141609
  SLICE_PLAN_STATES: () => SLICE_PLAN_STATES,
141362
141610
  SLICE_STATUSES: () => SLICE_STATUSES,
141363
141611
  SPEC_LOCK_PARSER_VERSION: () => SPEC_LOCK_PARSER_VERSION,
141612
+ STALE_CONTENT_APPROVAL_BLOCKER: () => STALE_CONTENT_APPROVAL_BLOCKER,
141364
141613
  STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS: () => STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS,
141365
141614
  STALLED_REVIEW_INVALID_TRANSITION_PATTERN: () => STALLED_REVIEW_INVALID_TRANSITION_PATTERN,
141366
141615
  STALLED_REVIEW_INVALID_TRANSITION_THRESHOLD: () => STALLED_REVIEW_INVALID_TRANSITION_THRESHOLD,
@@ -142090,6 +142339,7 @@ __export(src_exports, {
142090
142339
  isSqliteLockError: () => isSqliteLockError,
142091
142340
  isSqliteMigrationComplete: () => isSqliteMigrationComplete,
142092
142341
  isStaleBlockedByBlocker: () => isStaleBlockedByBlocker,
142342
+ isStaleContentApprovalBlocker: () => isStaleContentApprovalBlocker,
142093
142343
  isStoredAuthCredential: () => isStoredAuthCredential,
142094
142344
  isStrictDescendantPath: () => isStrictDescendantPath,
142095
142345
  isTailscaleCgnatIpv4: () => isTailscaleCgnatIpv4,
@@ -142169,6 +142419,7 @@ __export(src_exports, {
142169
142419
  mapRoleToCapability: () => mapRoleToCapability,
142170
142420
  markDraftSummarized: () => markDraftSummarized,
142171
142421
  markdownHtmlCommentUnits: () => markdownHtmlCommentUnits,
142422
+ matchStepHeadings: () => matchStepHeadings,
142172
142423
  matchesFrontendUxPath: () => matchesFrontendUxPath,
142173
142424
  matchesPatchnodeQuery: () => matchesPatchnodeQuery,
142174
142425
  materializeMcpServerSecrets: () => materializeMcpServerSecrets,
@@ -142282,6 +142533,7 @@ __export(src_exports, {
142282
142533
  postMergeOptionalGroupNode: () => postMergeOptionalGroupNode,
142283
142534
  postMergeVerificationOptionalGroupNode: () => postMergeVerificationOptionalGroupNode,
142284
142535
  postgresSchema: () => schema_exports,
142536
+ prefetchWorkflowSelections: () => prefetchWorkflowSelections,
142285
142537
  prepareAgentCompaniesImport: () => prepareAgentCompaniesImport,
142286
142538
  pricingAsOf: () => pricingAsOf,
142287
142539
  probeGitCliStatus: () => probeGitCliStatus,
@@ -142393,6 +142645,7 @@ __export(src_exports, {
142393
142645
  resolveBackupSettingsMigrationConflict: () => resolveBackupSettingsMigrationConflict,
142394
142646
  resolveCapacityPoolId: () => resolveCapacityPoolId,
142395
142647
  resolveClaudeCodeClientVersion: () => resolveClaudeCodeClientVersion,
142648
+ resolveCollateralArchivedReviewGate: () => resolveCollateralArchivedReviewGate,
142396
142649
  resolveColumnAdjacency: () => resolveColumnAdjacency,
142397
142650
  resolveColumnAgentBinding: () => resolveColumnAgentBinding,
142398
142651
  resolveColumnCapacity: () => resolveColumnCapacity,
@@ -153521,7 +153774,10 @@ async function runHoldReleaseSweep(store, deps) {
153521
153774
  const settings = await store.getSettings();
153522
153775
  if (expired()) return logPreambleTruncation(0);
153523
153776
  counters.tasks += 1;
153524
- const allTasks = await store.listTasks({ includeArchived: false });
153777
+ const selectionReadTally = { batched: 0, singles: 0 };
153778
+ const allTasks = await store.listTasks({ includeArchived: false, selectionCache, selectionReadTally });
153779
+ counters.batchSelections += selectionReadTally.batched;
153780
+ counters.selections += selectionReadTally.singles;
153525
153781
  if (expired()) return logPreambleTruncation(allTasks.length);
153526
153782
  const effectiveWorkflowIdByTask = /* @__PURE__ */ new Map();
153527
153783
  const missingIds = [...new Set(allTasks.map((task) => task.id))].filter((id) => !selectionCache.has(id));
@@ -179249,7 +179505,7 @@ function extractMarkdownSection(document2, headingName) {
179249
179505
  return document2.slice(start, end).trim();
179250
179506
  }
179251
179507
  function compactTaskPromptStepsSection(section) {
179252
- const stepTitles = Array.from(section.matchAll(/^### Step \d+:.*$/gm), (match) => match[0].trim());
179508
+ const stepTitles = matchStepHeadings(section).map(({ index: index2, headingLineEnd }) => section.slice(index2, headingLineEnd).trim());
179253
179509
  if (stepTitles.length === 0) {
179254
179510
  return section.trim();
179255
179511
  }
@@ -188605,7 +188861,7 @@ var init_scheduler = __esm({
188605
188861
  for (const slice of hierarchy.milestones.flatMap((milestone) => milestone.slices).filter((slice2) => slice2.status === "active")) {
188606
188862
  try {
188607
188863
  const superseded = await missionStore.reconcileSupersededGeneratedFixFeatures(slice.id);
188608
- fixed += superseded.supersededCount;
188864
+ fixed += superseded.supersededCount + (superseded.repairedCount ?? 0);
188609
188865
  if (superseded.supersededCount > 0) {
188610
188866
  const refreshed = await missionStore.getSlice(slice.id);
188611
188867
  if (refreshed?.status === "complete") {
@@ -188618,8 +188874,9 @@ var init_scheduler = __esm({
188618
188874
  const supersededFeatureIds = new Set(superseded.featureIds ?? []);
188619
188875
  const autoTriage = mission.autopilotEnabled === true || mission.autoAdvance === true;
188620
188876
  for (const feature of features) {
188621
- if (supersededFeatureIds.has(feature.id) || feature.taskId || !autoTriage || feature.status === "blocked") continue;
188877
+ if (supersededFeatureIds.has(feature.id) || feature.taskId || !autoTriage || feature.status === "blocked" || feature.status === "done") continue;
188622
188878
  if (feature.status !== "defined" && this.isGeneratedFixFeature(feature)) {
188879
+ schedulerLog.warn(`Parking stranded generated Fix feature ${feature.id} from ${feature.status}: not triageable without a defined state`);
188623
188880
  await missionStore.updateFeature(feature.id, { status: "blocked", loopState: "blocked", taskId: void 0 });
188624
188881
  fixed++;
188625
188882
  continue;
@@ -189671,12 +189928,12 @@ function createTaskSearchTool(store) {
189671
189928
  }
189672
189929
  };
189673
189930
  }
189674
- function createPatchnodeReadTool(store) {
189931
+ function createHistoryReadTool(store) {
189675
189932
  return {
189676
- name: "fn_patchnode_read",
189933
+ name: "fn_history_read",
189677
189934
  label: "Read History",
189678
189935
  description: "Read the permanent daily history of completed and reverted task deliveries.",
189679
- parameters: patchnodeReadParams,
189936
+ parameters: historyReadParams,
189680
189937
  execute: async (_id, params) => {
189681
189938
  const limit = Math.min(50, Math.max(1, Math.floor(params.limit ?? 20)));
189682
189939
  const result2 = await store.listPatchnodeEntries({
@@ -193264,7 +193521,7 @@ ${lines.join("\n")}`
193264
193521
  }
193265
193522
  };
193266
193523
  }
193267
- 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;
193524
+ 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;
193268
193525
  var init_agent_tools = __esm({
193269
193526
  "../engine/src/agent-tools.ts"() {
193270
193527
  "use strict";
@@ -193355,7 +193612,7 @@ var init_agent_tools = __esm({
193355
193612
  includeArchived: Type2.Optional(Type2.Boolean({ description: "Include archived tasks (default true)" })),
193356
193613
  limit: Type2.Optional(Type2.Number({ minimum: 1, maximum: 50, description: "Max results (default 20, max 50)" }))
193357
193614
  });
193358
- patchnodeReadParams = Type2.Object({
193615
+ historyReadParams = Type2.Object({
193359
193616
  query: Type2.Optional(Type2.String({ description: "Search task IDs, titles, and completion summaries" })),
193360
193617
  from: Type2.Optional(Type2.String({ description: "First UTC day, YYYY-MM-DD (inclusive)" })),
193361
193618
  to: Type2.Optional(Type2.String({ description: "Last UTC day, YYYY-MM-DD (inclusive)" })),
@@ -196565,6 +196822,55 @@ var init_fallback_model_observer = __esm({
196565
196822
  }
196566
196823
  });
196567
196824
 
196825
+ // ../engine/src/execution/resolve-activity-run-agent-id.ts
196826
+ async function resolveWorkflowStepRunAgentId(store, candidate, options = {}) {
196827
+ if (!candidate?.trim()) return null;
196828
+ const resolve66 = async () => {
196829
+ if (typeof store?.getAgent === "function") {
196830
+ const agent = await store.getAgent(candidate);
196831
+ if (agent) return agent.id;
196832
+ }
196833
+ if (typeof store?.listAgents !== "function") return null;
196834
+ const matches = (await store.listAgents({ includeEphemeral: true })).filter(
196835
+ (agent) => agent.role === candidate || agent.roles?.includes(candidate)
196836
+ );
196837
+ const builtInOwners = matches.filter(
196838
+ (agent) => isBuiltinWorkflowRoleAgent(agent) && agent.metadata?.workflowRole === candidate
196839
+ );
196840
+ if (builtInOwners.length === 1) return builtInOwners[0].id;
196841
+ return matches.length === 1 ? matches[0].id : null;
196842
+ };
196843
+ let lookup2;
196844
+ try {
196845
+ lookup2 = Promise.resolve(resolve66());
196846
+ } catch {
196847
+ return null;
196848
+ }
196849
+ void lookup2.catch(() => void 0);
196850
+ return await new Promise((complete) => {
196851
+ const timer2 = setTimeout(() => complete(null), options.timeoutMs ?? WORKFLOW_STEP_RUN_ATTRIBUTION_TIMEOUT_MS);
196852
+ timer2.unref?.();
196853
+ void lookup2.then(
196854
+ (agentId) => {
196855
+ clearTimeout(timer2);
196856
+ complete(agentId);
196857
+ },
196858
+ () => {
196859
+ clearTimeout(timer2);
196860
+ complete(null);
196861
+ }
196862
+ );
196863
+ });
196864
+ }
196865
+ var WORKFLOW_STEP_RUN_ATTRIBUTION_TIMEOUT_MS;
196866
+ var init_resolve_activity_run_agent_id = __esm({
196867
+ "../engine/src/execution/resolve-activity-run-agent-id.ts"() {
196868
+ "use strict";
196869
+ init_src2();
196870
+ WORKFLOW_STEP_RUN_ATTRIBUTION_TIMEOUT_MS = 2e3;
196871
+ }
196872
+ });
196873
+
196568
196874
  // ../engine/src/execution/step-session-executor.ts
196569
196875
  import { exec as exec20 } from "node:child_process";
196570
196876
  import { promisify as promisify27 } from "node:util";
@@ -196573,12 +196879,7 @@ import { rm as rm8 } from "node:fs/promises";
196573
196879
  function parseStepFileScopes(prompt) {
196574
196880
  const result2 = /* @__PURE__ */ new Map();
196575
196881
  if (!prompt) return result2;
196576
- const stepRegex = /^### Step (\d+):.*/gm;
196577
- const splits = [];
196578
- let match;
196579
- while ((match = stepRegex.exec(prompt)) !== null) {
196580
- splits.push({ index: match.index, stepNum: parseInt(match[1], 10) });
196581
- }
196882
+ const splits = matchStepHeadings(prompt).map(({ index: index2, headingNumber }) => ({ index: index2, stepNum: headingNumber }));
196582
196883
  if (splits.length === 0) return result2;
196583
196884
  for (let i = 0; i < splits.length; i++) {
196584
196885
  const start = splits[i].index;
@@ -196854,12 +197155,7 @@ function scopePromptToWorktree(prompt, rootDir, worktreePath) {
196854
197155
  return prompt.replaceAll(`${rootDir}/`, `${worktreePath}/`).replaceAll(`${worktreePath}/.fusion/`, `${rootDir}/.fusion/`);
196855
197156
  }
196856
197157
  function extractStepSection(prompt, stepIndex) {
196857
- const stepRegex = /^### Step (\d+):.*/gm;
196858
- const splits = [];
196859
- let match;
196860
- while ((match = stepRegex.exec(prompt)) !== null) {
196861
- splits.push({ index: match.index, stepNum: parseInt(match[1], 10) });
196862
- }
197158
+ const splits = matchStepHeadings(prompt).map(({ index: index2, headingNumber }) => ({ index: index2, stepNum: headingNumber }));
196863
197159
  const offset = resolveAuthoredStepHeadingOffset(splits.map((split) => split.stepNum));
196864
197160
  const targetSplit = splits.find((s3) => s3.stepNum === stepIndex + offset);
196865
197161
  if (!targetSplit) return "";
@@ -196869,9 +197165,7 @@ function extractStepSection(prompt, stepIndex) {
196869
197165
  return prompt.slice(start, end).trim();
196870
197166
  }
196871
197167
  function countSteps(prompt) {
196872
- const stepRegex = /^### Step \d+:/gm;
196873
- const matches = prompt.match(stepRegex);
196874
- return matches ? matches.length : 0;
197168
+ return matchStepHeadings(prompt).length;
196875
197169
  }
196876
197170
  function resolveStepSection(taskDetail, prompt, stepIndex, authoredStepCount) {
196877
197171
  const authoredSection = extractStepSection(prompt, stepIndex);
@@ -196966,6 +197260,7 @@ var init_step_session_executor = __esm({
196966
197260
  init_usage_limit_detector();
196967
197261
  init_agent_tools();
196968
197262
  init_worktree_backend();
197263
+ init_resolve_activity_run_agent_id();
196969
197264
  init_worktree_prune();
196970
197265
  init_active_session_registry();
196971
197266
  execAsync18 = promisify27(exec20);
@@ -196998,6 +197293,8 @@ var init_step_session_executor = __esm({
196998
197293
  credentialInstanceId;
196999
197294
  reusablePrimaryRetargetPending = false;
197000
197295
  reusablePrimaryAttemptActive = false;
197296
+ workflowStepRunAgentId;
197297
+ warnedUnattributableWorkflowStepRun = false;
197001
197298
  registerActiveStepSession(stepIndex, handle2, worktreePath) {
197002
197299
  this.activeSessions.set(stepIndex, handle2);
197003
197300
  activeSessionRegistry.registerPath(worktreePath, {
@@ -197342,10 +197639,20 @@ var init_step_session_executor = __esm({
197342
197639
  modelId: current.modelId
197343
197640
  };
197344
197641
  }
197345
- createWorkflowStepActivityRun(stepIndex, startedAt) {
197642
+ async createWorkflowStepActivityRun(stepIndex, startedAt) {
197346
197643
  const { taskDetail } = this.options;
197347
197644
  const step = taskDetail.steps?.[stepIndex];
197348
- const agentId = this.options.effectiveAgentId ?? taskDetail.assignedAgentId ?? "executor";
197645
+ if (typeof this.options.agentStore?.saveRun !== "function") return null;
197646
+ const agentIdCandidate = this.options.effectiveAgentId ?? taskDetail.assignedAgentId ?? "executor";
197647
+ this.workflowStepRunAgentId ??= resolveWorkflowStepRunAgentId(this.options.agentStore, agentIdCandidate);
197648
+ const agentId = await this.workflowStepRunAgentId;
197649
+ if (!agentId) {
197650
+ if (!this.warnedUnattributableWorkflowStepRun) {
197651
+ this.warnedUnattributableWorkflowStepRun = true;
197652
+ stepExecLog.warn(`Skipping unattributable workflow-step activity runs for task ${taskDetail.id} (candidate: ${agentIdCandidate})`);
197653
+ }
197654
+ return null;
197655
+ }
197349
197656
  return {
197350
197657
  id: `${generateSyntheticRunId("workflow-step", taskDetail.id)}-step-${stepIndex}`,
197351
197658
  agentId,
@@ -197365,6 +197672,7 @@ var init_step_session_executor = __esm({
197365
197672
  taskTitle: taskDetail.title,
197366
197673
  assignedAgentId: taskDetail.assignedAgentId,
197367
197674
  effectiveAgentId: this.options.effectiveAgentId,
197675
+ agentIdCandidate,
197368
197676
  agentId,
197369
197677
  stepIndex,
197370
197678
  stepName: step?.name ?? `Step ${stepIndex}`
@@ -197379,6 +197687,7 @@ var init_step_session_executor = __esm({
197379
197687
  };
197380
197688
  }
197381
197689
  async saveWorkflowStepActivityRun(run) {
197690
+ if (!run) return;
197382
197691
  const saveRun2 = this.options.agentStore?.saveRun?.bind(this.options.agentStore);
197383
197692
  if (!saveRun2) return;
197384
197693
  try {
@@ -197391,6 +197700,7 @@ var init_step_session_executor = __esm({
197391
197700
  }
197392
197701
  }
197393
197702
  async completeWorkflowStepActivityRun(run, status, result2) {
197703
+ if (!run) return;
197394
197704
  const terminalRun = {
197395
197705
  ...run,
197396
197706
  endedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -197450,7 +197760,7 @@ var init_step_session_executor = __esm({
197450
197760
  if (semaphore) {
197451
197761
  await semaphore.acquire();
197452
197762
  }
197453
- const activityRun = this.createWorkflowStepActivityRun(stepIndex, (/* @__PURE__ */ new Date()).toISOString());
197763
+ const activityRun = await this.createWorkflowStepActivityRun(stepIndex, (/* @__PURE__ */ new Date()).toISOString());
197454
197764
  await this.saveWorkflowStepActivityRun(activityRun);
197455
197765
  const trackingKey = this.makeTrackingKey(stepIndex);
197456
197766
  let retries = 0;
@@ -197999,12 +198309,11 @@ function collectTaskPaths(text3) {
197999
198309
  }
198000
198310
  function collectStepSections(stepsSection) {
198001
198311
  const sections2 = [];
198002
- const headingMatches = Array.from(stepsSection.matchAll(/^### Step (\d+):[^\n]*$/gm));
198312
+ const headingMatches = matchStepHeadings(stepsSection);
198003
198313
  for (let i = 0; i < headingMatches.length; i += 1) {
198004
198314
  const heading = headingMatches[i];
198005
- const start = (heading.index ?? 0) + heading[0].length;
198006
- const end = i + 1 < headingMatches.length ? headingMatches[i + 1].index ?? stepsSection.length : stepsSection.length;
198007
- sections2.push({ name: `Step ${heading[1]}`, body: stepsSection.slice(start, end).trim() });
198315
+ const end = i + 1 < headingMatches.length ? headingMatches[i + 1].index : stepsSection.length;
198316
+ sections2.push({ name: `Step ${heading.headingNumber}`, body: stepsSection.slice(heading.headingLineEnd, end).trim() });
198008
198317
  }
198009
198318
  return sections2;
198010
198319
  }
@@ -198096,6 +198405,7 @@ var TASK_PATH_REGEX;
198096
198405
  var init_task_document_references = __esm({
198097
198406
  "../engine/src/spec-validation/task-document-references.ts"() {
198098
198407
  "use strict";
198408
+ init_src2();
198099
198409
  init_step_session_executor();
198100
198410
  TASK_PATH_REGEX = /\.fusion\/tasks\/[A-Z0-9]+(?:-[A-Z0-9]+)*\/[^\s`'"\]]+/g;
198101
198411
  }
@@ -199122,6 +199432,24 @@ var init_configured_command = __esm({
199122
199432
  }
199123
199433
  });
199124
199434
 
199435
+ // ../engine/src/merge/stale-content-park.ts
199436
+ function classifyStaleContentPark(task) {
199437
+ if (task.paused || task.userPaused || task.deletedAt) return void 0;
199438
+ if (typeof task.error !== "string" || !isStaleContentApprovalBlocker(task.error)) return void 0;
199439
+ if (task.status === "failed") {
199440
+ return task.error.startsWith(AUTO_MERGE_RETRY_REJECTED_PREFIX) ? "retry-rejected" : "raw-blocker";
199441
+ }
199442
+ return task.status == null ? "retry-exhausted" : void 0;
199443
+ }
199444
+ var AUTO_MERGE_RETRY_REJECTED_PREFIX;
199445
+ var init_stale_content_park = __esm({
199446
+ "../engine/src/merge/stale-content-park.ts"() {
199447
+ "use strict";
199448
+ init_src2();
199449
+ AUTO_MERGE_RETRY_REJECTED_PREFIX = "AUTO_MERGE_RETRY_REJECTED:";
199450
+ }
199451
+ });
199452
+
199125
199453
  // ../engine/src/execution/lifecycle-move-log.ts
199126
199454
  function laneRole(column, lanes) {
199127
199455
  if (!lanes) return void 0;
@@ -222902,6 +223230,17 @@ function isRecoveryRetryDue(task, now) {
222902
223230
  const retryAt = Date.parse(task.nextRecoveryAt);
222903
223231
  return !Number.isFinite(retryAt) || retryAt <= now;
222904
223232
  }
223233
+ function formatMergeableReviewRecoverySummary({
223234
+ enqueued,
223235
+ merged,
223236
+ parked
223237
+ }) {
223238
+ const outcomes = [];
223239
+ if (enqueued > 0) outcomes.push(`${enqueued} re-enqueued for merge`);
223240
+ if (merged > 0) outcomes.push(`${merged} merged \u2192 done`);
223241
+ if (parked > 0) outcomes.push(`${parked} parked after enqueue starvation`);
223242
+ return `Mergeable review recovery: ${outcomes.join(", ") || "no tasks recovered"}`;
223243
+ }
222905
223244
  async function resolveNoOpFinalizeGateIds2(store, task) {
222906
223245
  const selection = store.getTaskWorkflowSelectionAsync ? await store.getTaskWorkflowSelectionAsync(task.id) : store.getTaskWorkflowSelection?.(task.id);
222907
223246
  if (!selection) return void 0;
@@ -222948,6 +223287,7 @@ var init_self_healing = __esm({
222948
223287
  "use strict";
222949
223288
  init_src2();
222950
223289
  init_src2();
223290
+ init_stale_content_park();
222951
223291
  init_logger3();
222952
223292
  init_lifecycle_move_log();
222953
223293
  init_lifecycle_move();
@@ -222968,6 +223308,7 @@ var init_self_healing = __esm({
222968
223308
  init_post_landing_worktree_cleanup();
222969
223309
  init_auto_recovery();
222970
223310
  init_active_session_registry();
223311
+ init_planning_liveness();
222971
223312
  init_replan_target();
222972
223313
  init_planning_handoff_recovery();
222973
223314
  init_spec_staleness();
@@ -223134,6 +223475,9 @@ var init_self_healing = __esm({
223134
223475
  maintenanceTickCounter = 0;
223135
223476
  taskLifecycleRetentionLastPrunedAt = /* @__PURE__ */ new Map();
223136
223477
  staleContentRerouteAuditKeys = /* @__PURE__ */ new Set();
223478
+ mergeRecoveryBlockerWarnKeys = /* @__PURE__ */ new Set();
223479
+ staleContentParkRecoveryAttempts = /* @__PURE__ */ new Map();
223480
+ staleContentParkRecoveryBudgetLogged = /* @__PURE__ */ new Set();
223137
223481
  unrunPreMergeGateRerouteAuditKeys = /* @__PURE__ */ new Set();
223138
223482
  githubCheckStateRetentionLastPrunedAt = /* @__PURE__ */ new Map();
223139
223483
  processBootStartedAt = Date.now();
@@ -223852,6 +224196,13 @@ var init_self_healing = __esm({
223852
224196
  { name: "reconcile-orphaned-pending-step-results", fn: () => this.reconcileOrphanedPendingStepResults().then(() => void 0) },
223853
224197
  { name: "reconcile-unproven-review-approvals", fn: () => this.reconcileUnprovenReviewApprovals().then(() => void 0) },
223854
224198
  /*
224199
+ FNXC:PreMergeApproval 2026-09-05-22:11:
224200
+ Runs AFTER the orphaned-step sweep, because that sweep is what produces the `failed` row this
224201
+ defect archives, and BEFORE failed-step recovery, so the restored gate is visible to it in the
224202
+ same startup pass instead of one cycle later.
224203
+ */
224204
+ { name: "reconcile-collateral-archived-review-gates", fn: () => this.reconcileCollateralArchivedReviewGates().then(() => void 0) },
224205
+ /*
223855
224206
  FNXC:WorkspaceContention 2026-08-23-08:00:
223856
224207
  `contention-hold` is owned by an in-memory retry timer. After a process restart that
223857
224208
  timer is gone, so clear only an unowned wait before ordinary dispatch classification;
@@ -224732,6 +225083,7 @@ var init_self_healing = __esm({
224732
225083
  */
224733
225084
  { name: "reconcile-orphaned-pending-step-results", fn: () => this.reconcileOrphanedPendingStepResults() },
224734
225085
  { name: "reconcile-unproven-review-approvals", fn: () => this.reconcileUnprovenReviewApprovals() },
225086
+ { name: "reconcile-collateral-archived-review-gates", fn: () => this.reconcileCollateralArchivedReviewGates() },
224735
225087
  { name: "recover-failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps() },
224736
225088
  { name: "recover-missing-worktree-review-failures", fn: () => this.recoverMissingWorktreeReviewFailures() },
224737
225089
  { name: "recover-interrupted-merging", fn: () => this.recoverInterruptedMergingTasks() },
@@ -228218,29 +228570,28 @@ var init_self_healing = __esm({
228218
228570
  /** Repair legacy dependency residue before it can re-enter executor dispatch. */
228219
228571
  async reconcileMissingDependencies() {
228220
228572
  let repaired = 0;
228221
- const tasks2 = await this.store.listTasks({ slim: true });
228573
+ const archivedColumns = new Set(await Promise.resolve().then(() => resolveProjectColumnsForRoles(this.store, ["archived"])).catch(() => /* @__PURE__ */ new Set()));
228574
+ archivedColumns.add("archived");
228575
+ const tasks2 = await this.store.listTasks({ slim: true, includeArchived: false });
228222
228576
  for (const snapshot of tasks2) {
228577
+ if (snapshot.deletedAt || archivedColumns.has(snapshot.column)) continue;
228223
228578
  if (!snapshot.dependencies.length || snapshot.userPaused || snapshot.paused || snapshot.autoMerge === false) continue;
228224
228579
  const livePaths = activeSessionRegistry.pathsForTask(snapshot.id);
228225
228580
  const liveExecution = livePaths.some((path13) => activeSessionRegistry.isPathActive(path13)) || executingTaskLock.has(snapshot.id) || this.options.getExecutingTaskIds?.().has(snapshot.id) === true || this.options.isTaskActive?.(snapshot.id) === true || Boolean(snapshot.checkedOutBy);
228226
228581
  if (liveExecution) continue;
228227
228582
  let removed = 0;
228228
228583
  for (const dependencyId of snapshot.dependencies) {
228229
- try {
228230
- const dependency = await this.store.getTask(dependencyId, { includeDeleted: true });
228231
- if (!dependency.deletedAt) continue;
228232
- } catch (error3) {
228233
- if (!isTaskNotFoundError2(error3)) throw error3;
228234
- }
228235
- const current = await this.store.getTask(snapshot.id).catch((error3) => {
228236
- if (isTaskNotFoundError2(error3)) return void 0;
228237
- throw error3;
228238
- });
228584
+ if (await readLinkedTaskOrUndefined(this.store, dependencyId)) continue;
228585
+ const current = await readLinkedTaskOrUndefined(this.store, snapshot.id);
228239
228586
  if (!current || current.userPaused || current.paused || current.autoMerge === false || current.checkedOutBy || activeSessionRegistry.pathsForTask(current.id).some((path13) => activeSessionRegistry.isPathActive(path13)) || executingTaskLock.has(current.id) || this.options.getExecutingTaskIds?.().has(current.id) === true || this.options.isTaskActive?.(current.id) === true || !current.dependencies.includes(dependencyId)) continue;
228240
228587
  try {
228241
228588
  await this.store.updateTaskDependencies(current.id, { operation: "remove", dependency: dependencyId });
228242
228589
  removed += 1;
228243
228590
  } catch (error3) {
228591
+ if (isMissingTaskLookupError(error3)) {
228592
+ log34.debug(`reconcileMissingDependencies: skipped unreadable ${current.id} dependency ${dependencyId}`);
228593
+ continue;
228594
+ }
228244
228595
  if (!/does not depend on/i.test(error3 instanceof Error ? error3.message : String(error3))) throw error3;
228245
228596
  }
228246
228597
  }
@@ -229006,7 +229357,7 @@ var init_self_healing = __esm({
229006
229357
  const graceMs = 10 * 6e4;
229007
229358
  const now = Date.now();
229008
229359
  const nowIso3 = new Date(now).toISOString();
229009
- const live = (taskId) => activeSessionRegistry.pathsForTask(taskId).some((path13) => activeSessionRegistry.isPathActive(path13)) || executingTaskLock.has(taskId) || this.options.isTaskActive?.(taskId) === true;
229360
+ const live = (taskId) => activeSessionRegistry.pathsForTask(taskId).some((path13) => activeSessionRegistry.isPathActive(path13)) || executingTaskLock.has(taskId) || isPlanningLive(taskId) || this.options.isTaskActive?.(taskId) === true;
229010
229361
  const due = await this.store.listDueWorkflowWorkItems({
229011
229362
  now: nowIso3,
229012
229363
  states: [...ACTIVE_WORKFLOW_WORK_ITEM_STATES]
@@ -229383,6 +229734,109 @@ var init_self_healing = __esm({
229383
229734
  return 0;
229384
229735
  }
229385
229736
  }
229737
+ /**
229738
+ * FNXC:PreMergeApproval 2026-09-05-22:11:
229739
+ * FN-295: restore a required pre-merge gate that was archived as COLLATERAL of another gate's
229740
+ * remediation, so the card stops being permanently unmergeable with no owner.
229741
+ *
229742
+ * The wedge needs four ingredients, none of which is individually wrong: a review row left
229743
+ * `pending` by a dead session, the orphaned-step sweep rewriting it to `failed`, the remediation
229744
+ * archiver stamping EVERY terminal failure (not just its own gate), and the merge door treating
229745
+ * `remediationArchivedAt` as an unconditional veto. The result is a row the reseed cannot re-run
229746
+ * (it handles `missing`), the stale-content reroute cannot re-run (it handles `stale-content`),
229747
+ * and the operator bypass cannot select (it selects `status:"failed"`). Measured on FN-295: three
229748
+ * review restarts re-ran Code Review and Documentation to APPROVE and merged nothing.
229749
+ *
229750
+ * This sweep does NOT approve or merge anything: it undoes the collateral archive so the gate is
229751
+ * once again a visible failed step that the ordinary FN-7720 audited bypass — or a genuine re-run
229752
+ * — can clear. It never touches an operator waiver, the gate that owns the remediation wave, a
229753
+ * workspace card, a user-paused card, or a card with a live session, and it re-validates every
229754
+ * condition inside the atomic mutation.
229755
+ */
229756
+ async reconcileCollateralArchivedReviewGates() {
229757
+ try {
229758
+ const reviewColumns = await resolveProjectColumnsForRoles(this.store, REVIEW_ROLES);
229759
+ const candidates = /* @__PURE__ */ new Map();
229760
+ for (const column of reviewColumns) {
229761
+ for (const task of await this.store.listTasks({ column, slim: true })) candidates.set(task.id, task);
229762
+ }
229763
+ let repairedTasks = 0;
229764
+ const isSessionLive = (taskId) => {
229765
+ const livePaths = activeSessionRegistry.pathsForTask(taskId);
229766
+ return livePaths.some((path13) => activeSessionRegistry.isPathActive(path13)) || executingTaskLock.has(taskId) || this.options.isTaskActive?.(taskId) === true;
229767
+ };
229768
+ const remediationGateIdsFor = (task) => new Set(
229769
+ (task.steps ?? []).map((step) => step.remediation?.gateStepId).filter((gateStepId) => typeof gateStepId === "string" && gateStepId.length > 0)
229770
+ );
229771
+ for (const candidate of candidates.values()) {
229772
+ if (candidate.userPaused === true || candidate.workspaceWorktrees !== void 0 || isSessionLive(candidate.id)) continue;
229773
+ const fresh = await this.store.getTask(candidate.id);
229774
+ if (!fresh || !reviewColumns.has(fresh.column) || fresh.userPaused === true || fresh.workspaceWorktrees !== void 0 || isSessionLive(fresh.id)) continue;
229775
+ if (!(fresh.workflowStepResults ?? []).some((result2) => resolveCollateralArchivedReviewGate(result2, { remediationGateIds: remediationGateIdsFor(fresh) }))) continue;
229776
+ let requiredPreMergeStepIds;
229777
+ try {
229778
+ requiredPreMergeStepIds = (await resolvePreMergeGateForTask(
229779
+ this.store,
229780
+ fresh.id,
229781
+ fresh.enabledWorkflowSteps,
229782
+ fresh
229783
+ )).requiredPreMergeStepIds;
229784
+ } catch {
229785
+ continue;
229786
+ }
229787
+ if (requiredPreMergeStepIds.size === 0) continue;
229788
+ let repair;
229789
+ try {
229790
+ await this.store.updateTaskAtomic(fresh.id, (live) => {
229791
+ repair = void 0;
229792
+ if (!reviewColumns.has(live.column) || live.userPaused === true || live.workspaceWorktrees !== void 0 || isSessionLive(live.id)) return null;
229793
+ const remediationGateIds = remediationGateIdsFor(live);
229794
+ const restoredStepIds = [];
229795
+ const results = (live.workflowStepResults ?? []).map((result2) => {
229796
+ if (!requiredPreMergeStepIds.has(result2.workflowStepId)) return result2;
229797
+ const resolution = resolveCollateralArchivedReviewGate(result2, { remediationGateIds });
229798
+ if (!resolution) return result2;
229799
+ restoredStepIds.push(result2.workflowStepId);
229800
+ return resolution.restored;
229801
+ });
229802
+ if (restoredStepIds.length === 0) return null;
229803
+ repair = { stepIds: restoredStepIds, column: live.column, resultCount: results.length };
229804
+ return { workflowStepResults: results };
229805
+ });
229806
+ if (!repair) continue;
229807
+ repairedTasks += 1;
229808
+ } catch (error3) {
229809
+ log34.warn(`reconcileCollateralArchivedReviewGates: failed for ${fresh.id}: ${error3 instanceof Error ? error3.message : String(error3)}`);
229810
+ continue;
229811
+ }
229812
+ await this.store.logEntry(
229813
+ fresh.id,
229814
+ `[pre-merge] Restored collaterally archived review gate(s): ${repair.stepIds.join(", ")}`,
229815
+ COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC
229816
+ ).catch(() => void 0);
229817
+ await emitBoundedRunAudit2(this.store, {
229818
+ taskId: fresh.id,
229819
+ agentId: "self-healing",
229820
+ runId: generateSyntheticRunId("reconcile-collateral-archived-review-gate", fresh.id),
229821
+ domain: "database",
229822
+ mutationType: "task:reconcile-collateral-archived-review-gate",
229823
+ target: fresh.id,
229824
+ metadata: {
229825
+ taskId: fresh.id,
229826
+ column: repair.column,
229827
+ workflowStepId: repair.stepIds[0],
229828
+ restoredCount: repair.stepIds.length,
229829
+ resultCount: repair.resultCount
229830
+ }
229831
+ }, { log: log34 });
229832
+ }
229833
+ if (repairedTasks > 0) log34.log(`Restored collaterally archived review gates on ${repairedTasks} task(s)`);
229834
+ return repairedTasks;
229835
+ } catch (error3) {
229836
+ log34.error(`reconcileCollateralArchivedReviewGates failed: ${error3 instanceof Error ? error3.message : String(error3)}`);
229837
+ return 0;
229838
+ }
229839
+ }
229386
229840
  /**
229387
229841
  * Backward lifecycle move gated on triple proof (FN-5335).
229388
229842
  * When the unproven fallback predicate fails, emits `task:finalize-no-op-review-no-action` and skips lifecycle mutation.
@@ -229742,25 +230196,14 @@ var init_self_healing = __esm({
229742
230196
  blocker check lacks a fresh Git descriptor. Capture the same merge content the merge queue uses and
229743
230197
  seed the graph back at Code Review, so cards like FN-9234 stop looping through doomed merge retries.
229744
230198
  */
229745
- async routeStaleSingularApprovalBackToReview(task, reviewColumns, settings) {
229746
- let mergeGate;
229747
- try {
229748
- mergeGate = await resolvePreMergeGateForTask(this.store, task.id, task.enabledWorkflowSteps, task);
229749
- } catch {
229750
- return false;
229751
- }
229752
- if (mergeGate.provenance === "default" && !mergeGate.selectionAbsent) return false;
229753
- const mergeContent = await captureMergeContentDescriptor(task, {
229754
- workspaceRootDir: this.options.rootDir,
229755
- settings
229756
- });
230199
+ async routeStaleSingularApprovalBackToReview(task, reviewColumns, mergeGate, mergeContent, parkShape) {
229757
230200
  const blocker = getTaskMergeBlocker(task, {
229758
230201
  reviewColumns,
229759
230202
  requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds,
229760
230203
  mergeContent
229761
230204
  });
229762
- if (blocker !== "task has a pre-merge approval recorded against different content" || mergeContent.kind !== "singular") {
229763
- return false;
230205
+ if (!isStaleContentApprovalBlocker(blocker) || mergeContent.kind !== "singular") {
230206
+ return { handled: false, seeded: false, parkCleared: false, mergeRetriesReset: false };
229764
230207
  }
229765
230208
  const reroute = await rerouteSingularStaleContentToReview(this.store, task, {
229766
230209
  requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds,
@@ -229771,11 +230214,25 @@ var init_self_healing = __esm({
229771
230214
  nodeId: void 0,
229772
230215
  workflowStepId: void 0
229773
230216
  }));
230217
+ let parkCleared = false;
230218
+ if (reroute.rerouted && parkShape) {
230219
+ await this.store.updateTaskAtomic(task.id, (live) => {
230220
+ if (classifyStaleContentPark(live) !== parkShape || live.paused || live.userPaused || live.deletedAt) {
230221
+ return null;
230222
+ }
230223
+ parkCleared = true;
230224
+ return { status: null, error: null, mergeRetries: 0 };
230225
+ });
230226
+ }
230227
+ const mergeRetriesReset = parkCleared;
229774
230228
  if (reroute.rerouted) {
229775
- await this.store.logEntry(task.id, "[pre-merge] Self-healing re-seeded Code Review after stale content evidence blocked merge.");
230229
+ 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.`);
230230
+ if (parkCleared) {
230231
+ 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.`);
230232
+ }
229776
230233
  log34.warn(`Stale content approval for ${task.id} re-seeded at ${reroute.nodeId ?? "code-review"}`);
229777
230234
  }
229778
- const auditKey = `${task.id}:${reroute.reason}:${reroute.nodeId ?? ""}`;
230235
+ const auditKey = `${task.id}:${reroute.reason}:${reroute.nodeId ?? ""}:${parkShape ?? ""}`;
229779
230236
  if (!this.staleContentRerouteAuditKeys.has(auditKey)) {
229780
230237
  this.staleContentRerouteAuditKeys.add(auditKey);
229781
230238
  await emitBoundedRunAudit2(this.store, {
@@ -229790,21 +230247,23 @@ var init_self_healing = __esm({
229790
230247
  nodeId: reroute.nodeId,
229791
230248
  workflowStepId: reroute.workflowStepId,
229792
230249
  reason: reroute.reason,
229793
- source: "self-healing"
230250
+ source: "self-healing",
230251
+ parkShape,
230252
+ parkCleared,
230253
+ mergeRetriesReset
229794
230254
  }
229795
230255
  });
229796
230256
  }
229797
- return true;
230257
+ return {
230258
+ handled: true,
230259
+ seeded: reroute.rerouted,
230260
+ nodeId: reroute.nodeId,
230261
+ parkShape,
230262
+ parkCleared,
230263
+ mergeRetriesReset
230264
+ };
229798
230265
  }
229799
- async routeUnrunPreMergeGateBackToReview(task, reviewColumns, settings) {
229800
- let mergeGate;
229801
- try {
229802
- mergeGate = await resolvePreMergeGateForTask(this.store, task.id, task.enabledWorkflowSteps, task);
229803
- } catch {
229804
- return false;
229805
- }
229806
- if (mergeGate.provenance === "default" && !mergeGate.selectionAbsent) return false;
229807
- const mergeContent = await captureMergeContentDescriptor(task, { workspaceRootDir: this.options.rootDir, settings });
230266
+ async routeUnrunPreMergeGateBackToReview(task, reviewColumns, mergeGate, mergeContent) {
229808
230267
  if (getTaskMergeBlocker(task, { reviewColumns, requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds, mergeContent }) !== "task has enabled pre-merge workflow steps that never ran") return false;
229809
230268
  const reroute = await rerouteUnrunPreMergeGateToReview(this.store, task, { requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds, mergeContent }).catch(() => ({ rerouted: false, reason: "no-unrun-gate", nodeId: void 0, workflowStepId: void 0 }));
229810
230269
  if (reroute.rerouted) {
@@ -229879,11 +230338,62 @@ var init_self_healing = __esm({
229879
230338
  for (const task of mergeable) {
229880
230339
  const reviewColumns = await ownReviewLanesFor(task);
229881
230340
  if (!reviewColumns.has(task.column)) continue;
229882
- if (await this.routeStaleSingularApprovalBackToReview(task, reviewColumns, settings)) continue;
229883
- if (await this.routeUnrunPreMergeGateBackToReview(task, reviewColumns, settings)) continue;
229884
- if (getTaskMergeBlocker(task, { reviewColumns }) !== void 0) continue;
230341
+ let mergeGate;
230342
+ try {
230343
+ mergeGate = await resolvePreMergeGateForTask(this.store, task.id, task.enabledWorkflowSteps, task);
230344
+ } catch {
230345
+ continue;
230346
+ }
230347
+ if (mergeGate.provenance === "default" && !mergeGate.selectionAbsent) continue;
230348
+ const mergeContent = await captureMergeContentDescriptor(task, { workspaceRootDir: this.options.rootDir, settings });
230349
+ if ((await this.routeStaleSingularApprovalBackToReview(task, reviewColumns, mergeGate, mergeContent)).handled) continue;
230350
+ if (await this.routeUnrunPreMergeGateBackToReview(task, reviewColumns, mergeGate, mergeContent)) continue;
230351
+ const blocker = getTaskMergeBlocker(task, {
230352
+ reviewColumns,
230353
+ requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds,
230354
+ mergeContent
230355
+ });
230356
+ if (blocker !== void 0) {
230357
+ const warningKey = `${task.id}:${blocker}`;
230358
+ if (!this.mergeRecoveryBlockerWarnKeys.has(warningKey)) {
230359
+ this.mergeRecoveryBlockerWarnKeys.add(warningKey);
230360
+ log34.warn(`mergeable-review recovery declined ${task.id}: ${blocker}`);
230361
+ }
230362
+ continue;
230363
+ }
229885
230364
  laneQualifiedMergeable.push(task);
229886
230365
  }
230366
+ const hiddenStaleContentCandidateIds = /* @__PURE__ */ new Set();
230367
+ for (const task of tasks2) {
230368
+ if (mergeable.includes(task)) continue;
230369
+ const parkShape = classifyStaleContentPark(task);
230370
+ if (!parkShape || mergeAdmissionByTaskId.get(task.id) !== true || executingIds.has(task.id) || task.status === "merging" || task.status === "merging-pr") continue;
230371
+ const reviewColumns = await ownReviewLanesFor(task);
230372
+ if (!reviewColumns.has(task.column)) continue;
230373
+ hiddenStaleContentCandidateIds.add(task.id);
230374
+ const attempts = (this.staleContentParkRecoveryAttempts.get(task.id) ?? 0) + 1;
230375
+ this.staleContentParkRecoveryAttempts.set(task.id, attempts);
230376
+ if (attempts === MAX_STARVATION_DROPS && !this.staleContentParkRecoveryBudgetLogged.has(task.id)) {
230377
+ this.staleContentParkRecoveryBudgetLogged.add(task.id);
230378
+ await this.store.logEntry(task.id, `[pre-merge] Stopped stale-content park recovery after ${MAX_STARVATION_DROPS} attempts; operator attention is required.`);
230379
+ }
230380
+ if (attempts > MAX_STARVATION_DROPS) continue;
230381
+ let mergeGate;
230382
+ try {
230383
+ mergeGate = await resolvePreMergeGateForTask(this.store, task.id, task.enabledWorkflowSteps, task);
230384
+ } catch {
230385
+ continue;
230386
+ }
230387
+ if (mergeGate.provenance === "default" && !mergeGate.selectionAbsent) continue;
230388
+ const mergeContent = await captureMergeContentDescriptor(task, { workspaceRootDir: this.options.rootDir, settings });
230389
+ await this.routeStaleSingularApprovalBackToReview(task, reviewColumns, mergeGate, mergeContent, parkShape);
230390
+ }
230391
+ for (const taskId of this.staleContentParkRecoveryAttempts.keys()) {
230392
+ if (!hiddenStaleContentCandidateIds.has(taskId)) {
230393
+ this.staleContentParkRecoveryAttempts.delete(taskId);
230394
+ this.staleContentParkRecoveryBudgetLogged.delete(taskId);
230395
+ }
230396
+ }
229887
230397
  if (unresolvedMergeableCards.length > 0) {
229888
230398
  log34.warn(
229889
230399
  `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.`
@@ -229906,6 +230416,9 @@ var init_self_healing = __esm({
229906
230416
  );
229907
230417
  const enqueueMerge = this.options.enqueueMerge;
229908
230418
  let recovered = 0;
230419
+ let enqueued = 0;
230420
+ let merged = 0;
230421
+ let parked = 0;
229909
230422
  for (const task of unownedMergeable) {
229910
230423
  try {
229911
230424
  if (enqueueMerge) {
@@ -229921,19 +230434,22 @@ var init_self_healing = __esm({
229921
230434
  await this.store.updateTask(task.id, { status: "failed", error: error3 });
229922
230435
  await this.store.logEntry(task.id, error3);
229923
230436
  this.mergeStarvationDrops.delete(task.id);
230437
+ parked++;
229924
230438
  recovered++;
229925
230439
  }
229926
230440
  continue;
229927
230441
  }
229928
230442
  this.mergeStarvationDrops.delete(task.id);
230443
+ enqueued++;
229929
230444
  } else {
229930
230445
  await this.store.mergeTask(task.id);
230446
+ merged++;
229931
230447
  }
229932
230448
  await this.store.logEntry(
229933
230449
  task.id,
229934
230450
  enqueueMerge ? "Auto-recovered: eligible in-review task re-enqueued for merge" : "Auto-recovered: eligible in-review task was merged and moved to done"
229935
230451
  );
229936
- log34.log(`Recovered mergeable review task ${task.id}`);
230452
+ log34.log(enqueueMerge ? `Re-enqueued mergeable review task ${task.id} for merge` : `Merged review task ${task.id} and moved it to done`);
229937
230453
  recovered++;
229938
230454
  } catch (err) {
229939
230455
  const errorMessage3 = err instanceof Error ? err.message : String(err);
@@ -229941,7 +230457,7 @@ var init_self_healing = __esm({
229941
230457
  }
229942
230458
  }
229943
230459
  if (recovered > 0) {
229944
- log34.log(`Recovered ${recovered} mergeable review task(s) \u2192 done`);
230460
+ log34.log(formatMergeableReviewRecoverySummary({ enqueued, merged, parked }));
229945
230461
  }
229946
230462
  return recovered;
229947
230463
  } catch (err) {
@@ -237933,7 +238449,7 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
237933
238449
  planLog.warn(`${task.id} ${retryMessage}`);
237934
238450
  await this.store.logEntry(task.id, retryMessage);
237935
238451
  await this.updatePlanningStateIfStillCurrent(task, {
237936
- status: this.restoreStatusAfterInterruptedTriageWork(task),
238452
+ status: await this.resolvePlanningRetryHoldStatus(task, written),
237937
238453
  error: null,
237938
238454
  recoveryRetryCount: decision.nextState.recoveryRetryCount,
237939
238455
  nextRecoveryAt: decision.nextState.nextRecoveryAt
@@ -237970,9 +238486,13 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
237970
238486
  const retryMessage = `Generated plan failed deterministic validation (${deterministicSpecFailure}) \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}.`;
237971
238487
  planLog.warn(`${task.id} ${retryMessage}`);
237972
238488
  await this.store.logEntry(task.id, retryMessage);
237973
- const restoreStatus = this.restoreStatusAfterInterruptedTriageWork(task);
238489
+ await this.store.logEntry(
238490
+ task.id,
238491
+ "AI spec revision requested",
238492
+ `Deterministic plan validation rejected the generated plan: ${deterministicSpecFailure}`
238493
+ ).catch(() => void 0);
237974
238494
  await this.updatePlanningStateIfStillCurrent(task, {
237975
- status: restoreStatus,
238495
+ status: await this.resolvePlanningRetryHoldStatus(task, written),
237976
238496
  error: null,
237977
238497
  recoveryRetryCount: decision.nextState.recoveryRetryCount,
237978
238498
  nextRecoveryAt: decision.nextState.nextRecoveryAt
@@ -238142,9 +238662,10 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
238142
238662
  const retryMessage = `${failureMessage} \u2014 retry ${decision.nextState.recoveryRetryCount}/${MAX_RECOVERY_RETRIES} in ${formatDelay(decision.delayMs)}.`;
238143
238663
  planLog.warn(`${task.id} ${retryMessage}`);
238144
238664
  await this.store.logEntry(task.id, retryMessage).catch(() => void 0);
238665
+ const retryHoldStatus = await this.resolvePlanningRetryHoldStatus(task);
238145
238666
  await this.updatePlanningStateIfStillCurrent(task, (live) => ({
238146
238667
  ...persistMarker(live),
238147
- status: this.restoreStatusAfterInterruptedTriageWork(task),
238668
+ status: retryHoldStatus,
238148
238669
  error: null,
238149
238670
  recoveryRetryCount: decision.nextState.recoveryRetryCount,
238150
238671
  nextRecoveryAt: decision.nextState.nextRecoveryAt
@@ -238174,7 +238695,7 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
238174
238695
  planLog.warn(`${task.id}: failed to log transient-error retry entry: ${msg}`);
238175
238696
  });
238176
238697
  }
238177
- const restoreStatus = this.restoreStatusAfterInterruptedTriageWork(task);
238698
+ const restoreStatus = await this.resolvePlanningRetryHoldStatus(task);
238178
238699
  await this.updatePlanningStateIfStillCurrent(task, {
238179
238700
  status: restoreStatus,
238180
238701
  recoveryRetryCount: decision.nextState.recoveryRetryCount,
@@ -238226,7 +238747,7 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
238226
238747
  const msg = logErr instanceof Error ? logErr.message : String(logErr);
238227
238748
  planLog.warn(`${task.id}: failed to log planning-failure retry entry: ${msg}`);
238228
238749
  });
238229
- const restoreStatus = this.restoreStatusAfterInterruptedTriageWork(task);
238750
+ const restoreStatus = await this.resolvePlanningRetryHoldStatus(task);
238230
238751
  await this.updatePlanningStateIfStillCurrent(task, {
238231
238752
  status: restoreStatus,
238232
238753
  recoveryRetryCount: genericDecision.nextState.recoveryRetryCount,
@@ -238510,11 +239031,22 @@ ${lines.join("\n")}` }],
238510
239031
  }
238511
239032
  return null;
238512
239033
  }
239034
+ /*
239035
+ FNXC:TriagePlanningRetry 2026-09-05-22:06:
239036
+ A retry over a finished-looking PROMPT.md must hold `needs-replan`, matching scheduler validation
239037
+ retries. Clearing status makes the card dispatchable and bypasses finalizeApprovedTask approval.
239038
+ */
239039
+ async resolvePlanningRetryHoldStatus(task, knownPromptContent) {
239040
+ const restored = this.restoreStatusAfterInterruptedTriageWork(task);
239041
+ if (restored) return restored;
239042
+ const content = knownPromptContent ?? await this.readNonEmptyPromptDraft(task.id, "planning retry hold");
239043
+ return content && !isTaskAwaitingPlanning(task, content) ? "needs-replan" : null;
239044
+ }
238513
239045
  async validateGeneratedPrompt(taskId, promptContent) {
238514
239046
  if (!promptContent.trim()) {
238515
239047
  return "PROMPT.md file not found or empty";
238516
239048
  }
238517
- const headingNumbers = Array.from(promptContent.matchAll(/^### Step (\d+):/gm), (match) => Number(match[1]));
239049
+ const headingNumbers = matchStepHeadings(promptContent).map((match) => match.headingNumber);
238518
239050
  if (headingNumbers.length > 0 && !headingNumbers.every((heading, index2) => heading === index2)) {
238519
239051
  const diagnostic = `Step headings must be contiguous 0-based execution indices (observed: ${headingNumbers.join(", ")}). Renumber from Step 0 and update prose cross-references.`;
238520
239052
  planLog.warn(`${taskId}: ${diagnostic}`);
@@ -239124,6 +239656,101 @@ ${lines.join("\n")}` }],
239124
239656
  }
239125
239657
  });
239126
239658
 
239659
+ // ../engine/src/executor/optional-step-revision.ts
239660
+ function normalizeOptionalStepRevisionKey2(value) {
239661
+ return (value ?? "").trim().toLowerCase();
239662
+ }
239663
+ function optionalStepRevisionKey2(nodeId, stepName) {
239664
+ return normalizeOptionalStepRevisionKey2(nodeId) || normalizeOptionalStepRevisionKey2(stepName) || "pre-merge-optional-step";
239665
+ }
239666
+ function optionalStepRevisionResetOutcome(key) {
239667
+ return `${OPTIONAL_STEP_REVISION_RESET_MARKER} ${key}`;
239668
+ }
239669
+ function resetIndexFor(log42, normalizedKey) {
239670
+ for (let index2 = log42.length - 1; index2 >= 0; index2 -= 1) {
239671
+ const outcome = log42[index2]?.outcome ?? "";
239672
+ const markerIndex = outcome.indexOf(OPTIONAL_STEP_REVISION_RESET_MARKER);
239673
+ if (markerIndex < 0) continue;
239674
+ const markerValue = outcome.slice(markerIndex + OPTIONAL_STEP_REVISION_RESET_MARKER.length).split(/\r?\n/, 1)[0]?.trim();
239675
+ if (normalizeOptionalStepRevisionKey2(markerValue) === normalizedKey) return index2;
239676
+ }
239677
+ return -1;
239678
+ }
239679
+ function countOptionalStepRevisionAttempts2(task, key, stepName) {
239680
+ const normalizedKey = normalizeOptionalStepRevisionKey2(key);
239681
+ const normalizedStepName = normalizeOptionalStepRevisionKey2(stepName);
239682
+ const log42 = task.log ?? [];
239683
+ const since = resetIndexFor(log42, normalizedKey);
239684
+ return (since >= 0 ? log42.slice(since + 1) : log42).filter((entry) => {
239685
+ const action = entry.action ?? "";
239686
+ const outcome = entry.outcome ?? "";
239687
+ if (!/attempt \d+\//.test(action)) return false;
239688
+ const markerIndex = outcome.indexOf(OPTIONAL_STEP_REVISION_KEY_MARKER2);
239689
+ if (markerIndex >= 0) {
239690
+ const markerValue = outcome.slice(markerIndex + OPTIONAL_STEP_REVISION_KEY_MARKER2.length).split(/\r?\n/, 1)[0]?.trim();
239691
+ return normalizeOptionalStepRevisionKey2(markerValue) === normalizedKey;
239692
+ }
239693
+ if (!normalizedStepName) return false;
239694
+ return normalizeOptionalStepRevisionKey2(outcome).includes(`step: ${normalizedStepName}`);
239695
+ }).length;
239696
+ }
239697
+ function optionalStepRevisionLogOutcome2(details, key) {
239698
+ return `${details}
239699
+ ${OPTIONAL_STEP_REVISION_KEY_MARKER2} ${key}`;
239700
+ }
239701
+ function buildReviewConvergenceContext(task, options) {
239702
+ const revisionKey = options.revisionKey;
239703
+ const priorAttemptCount = countPlanReviewRevisionAttempts(task.workflowStepResults, { revisionKey });
239704
+ const attempt = priorAttemptCount + 1;
239705
+ if (attempt <= 1) return "";
239706
+ const history = collectPlanReviewFeedbackHistory(task.workflowStepResults, { revisionKey });
239707
+ const lines = [
239708
+ `## Convergence \u2014 ${options.reviewKind === "plan" ? "Plan Review" : "Code Review"} attempt ${attempt}`
239709
+ ];
239710
+ if (options.changeSummaryBlock?.trim()) lines.push("", options.changeSummaryBlock.trim());
239711
+ lines.push(
239712
+ "Treat the cumulative prior feedback below as a decision primer. Verify each prior blocker against the current PROMPT.md before looking for new findings.",
239713
+ "- Do not re-raise a resolved or semantically duplicate blocker.",
239714
+ "- A newly blocking finding must identify the revision that introduced it, the prior blocker that genuinely masked it, or why it is independently delivery-blocking for correctness, security, data safety, or executability. Record an earlier reviewer miss explicitly; never demote a critical defect merely because it was missed before."
239715
+ );
239716
+ if (attempt >= 3) {
239717
+ lines.push(
239718
+ "- Severity ratchet (attempt 3+): only delivery-blocking critical defects may return REVISE; important/minor wording or implementation-detail findings are advisory."
239719
+ );
239720
+ }
239721
+ if (history.length > 0) {
239722
+ lines.push("", "### Cumulative prior Plan Review ledger");
239723
+ history.forEach((feedback, index2) => lines.push(`#### PR${index2 + 1}`, feedback));
239724
+ }
239725
+ const own2 = task.workflowStepResults?.find((result2) => result2.workflowStepId === revisionKey);
239726
+ const findings = (own2?.priorAttempts ?? []).flatMap((attempt2) => attempt2.findings ?? []).filter(isOpenWorkflowReviewFinding);
239727
+ if (findings.length > 0) {
239728
+ lines.push("", "### Your prior findings on this gate");
239729
+ findings.forEach((finding) => lines.push(`- ${finding.id} \u2014 [${finding.severity ?? "unclassified"}] ${finding.title}: ${finding.body}${finding.filePath ? ` (${finding.filePath}${finding.line ? `:${finding.line}` : ""})` : ""}`));
239730
+ lines.push("Match prior defects by file path, line region, and described behavior; IDs and titles may change between rounds.");
239731
+ }
239732
+ const disputed = findings.filter((finding) => finding.disputedAt);
239733
+ if (disputed.length > 0) {
239734
+ lines.push("", "### Disputed by the implementer");
239735
+ disputed.forEach((finding) => lines.push(`- ${finding.id}: ${finding.disputeRationale ?? "No rationale recorded."}`));
239736
+ lines.push("You MUST rule on each dispute: accept it by listing its ID in supersededFindingIds, or maintain it with a finding whose rebutsDisputedFindingId names it and whose body answers the rationale. A maintained objection needs that reference even if it has a new finding ID.");
239737
+ }
239738
+ return lines.join("\n");
239739
+ }
239740
+ function buildGraphPlanReviewConvergenceContext(task, revisionKey) {
239741
+ return buildReviewConvergenceContext(task, { revisionKey, reviewKind: "plan" });
239742
+ }
239743
+ var OPTIONAL_STEP_REVISION_KEY_MARKER2, OPTIONAL_STEP_REVISION_RESET_MARKER;
239744
+ var init_optional_step_revision = __esm({
239745
+ "../engine/src/executor/optional-step-revision.ts"() {
239746
+ "use strict";
239747
+ init_src2();
239748
+ init_plan_review_feedback_history();
239749
+ OPTIONAL_STEP_REVISION_KEY_MARKER2 = "Workflow revision key:";
239750
+ OPTIONAL_STEP_REVISION_RESET_MARKER = "Workflow revision ledger reset:";
239751
+ }
239752
+ });
239753
+
239127
239754
  // ../engine/src/executor/workflow-gate-activity.ts
239128
239755
  function resolveWorkflowGateActivityClaim(routedPrincipalAgentId, assignedAgentId) {
239129
239756
  const agentId = routedPrincipalAgentId ?? assignedAgentId ?? "executor";
@@ -239955,7 +240582,7 @@ function extractPromptSection(promptContent, sectionName) {
239955
240582
  return promptContent.slice(start, end).trim();
239956
240583
  }
239957
240584
  function summarizePromptSteps(promptContent) {
239958
- const stepTitles = Array.from(promptContent.matchAll(/^### Step \d+:.*$/gm), (match) => match[0].trim());
240585
+ const stepTitles = matchStepHeadings(promptContent).map(({ index: index2, headingLineEnd }) => promptContent.slice(index2, headingLineEnd).trim());
239959
240586
  if (stepTitles.length === 0) {
239960
240587
  return "";
239961
240588
  }
@@ -244726,85 +245353,6 @@ var init_review_inline_fix_recapture = __esm({
244726
245353
  }
244727
245354
  });
244728
245355
 
244729
- // ../engine/src/executor/optional-step-revision.ts
244730
- function normalizeOptionalStepRevisionKey2(value) {
244731
- return (value ?? "").trim().toLowerCase();
244732
- }
244733
- function optionalStepRevisionKey2(nodeId, stepName) {
244734
- return normalizeOptionalStepRevisionKey2(nodeId) || normalizeOptionalStepRevisionKey2(stepName) || "pre-merge-optional-step";
244735
- }
244736
- function countOptionalStepRevisionAttempts2(task, key, stepName) {
244737
- const normalizedKey = normalizeOptionalStepRevisionKey2(key);
244738
- const normalizedStepName = normalizeOptionalStepRevisionKey2(stepName);
244739
- return (task.log ?? []).filter((entry) => {
244740
- const action = entry.action ?? "";
244741
- const outcome = entry.outcome ?? "";
244742
- if (!/attempt \d+\//.test(action)) return false;
244743
- const markerIndex = outcome.indexOf(OPTIONAL_STEP_REVISION_KEY_MARKER2);
244744
- if (markerIndex >= 0) {
244745
- const markerValue = outcome.slice(markerIndex + OPTIONAL_STEP_REVISION_KEY_MARKER2.length).split(/\r?\n/, 1)[0]?.trim();
244746
- return normalizeOptionalStepRevisionKey2(markerValue) === normalizedKey;
244747
- }
244748
- if (!normalizedStepName) return false;
244749
- return normalizeOptionalStepRevisionKey2(outcome).includes(`step: ${normalizedStepName}`);
244750
- }).length;
244751
- }
244752
- function optionalStepRevisionLogOutcome2(details, key) {
244753
- return `${details}
244754
- ${OPTIONAL_STEP_REVISION_KEY_MARKER2} ${key}`;
244755
- }
244756
- function buildReviewConvergenceContext(task, options) {
244757
- const revisionKey = options.revisionKey;
244758
- const priorAttemptCount = countPlanReviewRevisionAttempts(task.workflowStepResults, { revisionKey });
244759
- const attempt = priorAttemptCount + 1;
244760
- if (attempt <= 1) return "";
244761
- const history = collectPlanReviewFeedbackHistory(task.workflowStepResults, { revisionKey });
244762
- const lines = [
244763
- `## Convergence \u2014 ${options.reviewKind === "plan" ? "Plan Review" : "Code Review"} attempt ${attempt}`
244764
- ];
244765
- if (options.changeSummaryBlock?.trim()) lines.push("", options.changeSummaryBlock.trim());
244766
- lines.push(
244767
- "Treat the cumulative prior feedback below as a decision primer. Verify each prior blocker against the current PROMPT.md before looking for new findings.",
244768
- "- Do not re-raise a resolved or semantically duplicate blocker.",
244769
- "- A newly blocking finding must identify the revision that introduced it, the prior blocker that genuinely masked it, or why it is independently delivery-blocking for correctness, security, data safety, or executability. Record an earlier reviewer miss explicitly; never demote a critical defect merely because it was missed before."
244770
- );
244771
- if (attempt >= 3) {
244772
- lines.push(
244773
- "- Severity ratchet (attempt 3+): only delivery-blocking critical defects may return REVISE; important/minor wording or implementation-detail findings are advisory."
244774
- );
244775
- }
244776
- if (history.length > 0) {
244777
- lines.push("", "### Cumulative prior Plan Review ledger");
244778
- history.forEach((feedback, index2) => lines.push(`#### PR${index2 + 1}`, feedback));
244779
- }
244780
- const own2 = task.workflowStepResults?.find((result2) => result2.workflowStepId === revisionKey);
244781
- const findings = (own2?.priorAttempts ?? []).flatMap((attempt2) => attempt2.findings ?? []).filter(isOpenWorkflowReviewFinding);
244782
- if (findings.length > 0) {
244783
- lines.push("", "### Your prior findings on this gate");
244784
- findings.forEach((finding) => lines.push(`- ${finding.id} \u2014 [${finding.severity ?? "unclassified"}] ${finding.title}: ${finding.body}${finding.filePath ? ` (${finding.filePath}${finding.line ? `:${finding.line}` : ""})` : ""}`));
244785
- lines.push("Match prior defects by file path, line region, and described behavior; IDs and titles may change between rounds.");
244786
- }
244787
- const disputed = findings.filter((finding) => finding.disputedAt);
244788
- if (disputed.length > 0) {
244789
- lines.push("", "### Disputed by the implementer");
244790
- disputed.forEach((finding) => lines.push(`- ${finding.id}: ${finding.disputeRationale ?? "No rationale recorded."}`));
244791
- lines.push("You MUST rule on each dispute: accept it by listing its ID in supersededFindingIds, or maintain it with a finding whose rebutsDisputedFindingId names it and whose body answers the rationale. A maintained objection needs that reference even if it has a new finding ID.");
244792
- }
244793
- return lines.join("\n");
244794
- }
244795
- function buildGraphPlanReviewConvergenceContext(task, revisionKey) {
244796
- return buildReviewConvergenceContext(task, { revisionKey, reviewKind: "plan" });
244797
- }
244798
- var OPTIONAL_STEP_REVISION_KEY_MARKER2;
244799
- var init_optional_step_revision = __esm({
244800
- "../engine/src/executor/optional-step-revision.ts"() {
244801
- "use strict";
244802
- init_src2();
244803
- init_plan_review_feedback_history();
244804
- OPTIONAL_STEP_REVISION_KEY_MARKER2 = "Workflow revision key:";
244805
- }
244806
- });
244807
-
244808
245356
  // ../engine/src/executor/execute-workflow-step.ts
244809
245357
  import { exec as exec44 } from "node:child_process";
244810
245358
  import { promisify as promisify63 } from "node:util";
@@ -245570,10 +246118,12 @@ ${JSON.stringify({ verdict: repairedVerdict })}`,
245570
246118
  session.dispose();
245571
246119
  await agentLogger.flush();
245572
246120
  if (parsed.verdict) {
246121
+ const findingsUnreadable = verdictRepairResult === "repaired" && (parsed.findings?.length ?? 0) === 0;
245573
246122
  const gated = reviewBlockingSeverity ? applyReviewSeverityGate({
245574
246123
  verdict: parsed.verdict,
245575
246124
  findings: parsed.findings,
245576
- threshold: reviewBlockingSeverity
246125
+ threshold: reviewBlockingSeverity,
246126
+ findingsUnreadable
245577
246127
  }) : void 0;
245578
246128
  const effectiveVerdict = gated?.verdict ?? parsed.verdict;
245579
246129
  if (gated?.downgraded) {
@@ -260088,7 +260638,7 @@ async function routeGraphMergeFailureToRetry(deps, live, result2, abortProvenanc
260088
260638
  const parked = await parkTaskFailed(
260089
260639
  deps.store,
260090
260640
  live.id,
260091
- `AUTO_MERGE_RETRY_REJECTED: ${reason}`,
260641
+ `${AUTO_MERGE_RETRY_REJECTED_PREFIX} ${reason}`,
260092
260642
  deps.getRunContextFor(live.id),
260093
260643
  live.columnMovedAt
260094
260644
  );
@@ -260104,6 +260654,7 @@ var init_route_graph_merge_failure_to_retry = __esm({
260104
260654
  init_logger3();
260105
260655
  init_workflow_merge_nodes();
260106
260656
  init_emit_merge_boundary_unproven_audit();
260657
+ init_stale_content_park();
260107
260658
  BIT_BACKOFF_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 32e3, 64e3];
260108
260659
  }
260109
260660
  });
@@ -264765,9 +265316,13 @@ async function runImplementation(deps, task, graphCompletion, reportImplementati
264765
265316
  */
264766
265317
  credentialInstanceId: detail.credentialInstanceId,
264767
265318
  resolveCredentialInstanceRetarget: nextStepInstance,
264768
- // Attribute the per-step run auditor to the column agent when it governs
264769
- // (U4); absent → StepSessionExecutor falls back to assignedAgentId.
264770
- effectiveAgentId: stepColumnAgent?.agent.id,
265319
+ /*
265320
+ FNXC:ColumnAgent 2026-09-04-14:11:
265321
+ The effective step identity is the governing column agent or the authoritative assignee.
265322
+ Both are existence-proved reads; with empty project.boards the column seam is absent, so
265323
+ retaining only it lost attribution for every assigned task.
265324
+ */
265325
+ effectiveAgentId: stepIdentityAgent?.id,
264771
265326
  actionGateContext: deps.buildActionGateContext(task.id, stepIdentityAgent, settings.defaultAgentPermissionPolicy),
264772
265327
  permanentAgentGating: deps.buildPermanentAgentGatingContext(task.id, stepIdentityAgent, settings.defaultAgentPermissionPolicy),
264773
265328
  // 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.
@@ -269630,10 +270185,22 @@ var init_terminate_all_children = __esm({
269630
270185
  });
269631
270186
 
269632
270187
  // ../engine/src/executor/clear-terminal-step-failures-for-retry.ts
270188
+ function latestTerminalPreMergeFailureId(results) {
270189
+ return (results ?? []).filter((result2) => (result2.phase || "pre-merge") === "pre-merge" && (result2.status === "failed" || result2.status === "advisory_failure")).sort((left, right) => {
270190
+ const leftTime = Date.parse(left.completedAt || left.startedAt || "");
270191
+ const rightTime = Date.parse(right.completedAt || right.startedAt || "");
270192
+ return (Number.isFinite(rightTime) ? rightTime : 0) - (Number.isFinite(leftTime) ? leftTime : 0);
270193
+ })[0]?.workflowStepId;
270194
+ }
269633
270195
  async function clearTerminalStepFailuresForRetry(deps, taskId, mode) {
269634
270196
  const live = await deps.store.getTask(taskId).catch(() => null);
269635
270197
  if (!live) return;
269636
- const cleared = mode === "archive" ? archiveTerminalWorkflowStepFailures(live.workflowStepResults) : clearTerminalWorkflowStepFailures(live.workflowStepResults);
270198
+ const remediatedStepId = mode === "archive" ? latestTerminalPreMergeFailureId(live.workflowStepResults) : void 0;
270199
+ const cleared = mode === "archive" ? archiveTerminalWorkflowStepFailures(
270200
+ live.workflowStepResults,
270201
+ void 0,
270202
+ remediatedStepId ? { workflowStepIds: /* @__PURE__ */ new Set([remediatedStepId]) } : {}
270203
+ ) : clearTerminalWorkflowStepFailures(live.workflowStepResults);
269637
270204
  if (cleared !== live.workflowStepResults) {
269638
270205
  await deps.store.updateTask(taskId, { workflowStepResults: cleared }, deps.getRunContextFor(taskId));
269639
270206
  }
@@ -274240,10 +274807,14 @@ var init_mission_execution_loop = __esm({
274240
274807
  for (const slice of milestone.slices) {
274241
274808
  if (slice.status !== "active") continue;
274242
274809
  const supersededFixes = await this.missionStore.reconcileSupersededGeneratedFixFeatures(slice.id);
274243
- const supersededFeatureIds = new Set(supersededFixes.featureIds);
274810
+ const supersededFeatureIds = new Set(supersededFixes.featureIds ?? []);
274811
+ if ((supersededFixes.repairedCount ?? 0) > 0) {
274812
+ loopLog.warn(`Recovery: restored ${supersededFixes.repairedCount ?? 0} generated Fix Features in slice ${slice.id}: ${(supersededFixes.repairedFeatureIds ?? []).join(", ")}`);
274813
+ recoveredCount += supersededFixes.repairedCount ?? 0;
274814
+ }
274244
274815
  if (supersededFixes.supersededCount > 0) {
274245
274816
  loopLog.warn(
274246
- `Recovery: superseded ${supersededFixes.supersededCount} generated Fix Features in slice ${slice.id} because an ancestor feature already passed validation`
274817
+ `Recovery: superseded ${supersededFixes.supersededCount} generated Fix Features in slice ${slice.id} because an ancestor or the Fix feature's own validation already passed`
274247
274818
  );
274248
274819
  recoveredCount += supersededFixes.supersededCount;
274249
274820
  }
@@ -297253,7 +297824,7 @@ ${detail}`);
297253
297824
  metadata: { taskId: task.id, nodeId: reroute.nodeId, workflowStepId: reroute.workflowStepId, reason: reroute.reason, source: "merge-gate", missingGateCount: mergeGate.requiredPreMergeStepIds.size }
297254
297825
  });
297255
297826
  }
297256
- if (blocker === "task has a pre-merge approval recorded against different content" && mergeContent.kind === "singular") {
297827
+ if (isStaleContentApprovalBlocker(blocker) && mergeContent.kind === "singular") {
297257
297828
  const reroute = await rerouteSingularStaleContentToReview(store, task, {
297258
297829
  requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds,
297259
297830
  mergeContent
@@ -297263,9 +297834,10 @@ ${detail}`);
297263
297834
  nodeId: void 0,
297264
297835
  workflowStepId: void 0
297265
297836
  }));
297266
- if (reroute.rerouted) {
297267
- await store.logEntry(task.id, `[pre-merge] Code Review re-entry is owned by the workflow graph after stale content evidence was refused.`);
297268
- }
297837
+ await store.logEntry(
297838
+ task.id,
297839
+ `[pre-merge] Review lane '${reroute.nodeId ?? "unknown"}' approved older content; re-review is owned by the workflow graph (${reroute.reason}).`
297840
+ );
297269
297841
  const auditKey = `${task.id}:${reroute.reason}`;
297270
297842
  if (!this.staleContentRerouteAuditKeys.has(auditKey)) {
297271
297843
  this.staleContentRerouteAuditKeys.add(auditKey);
@@ -301631,6 +302203,7 @@ __export(src_exports2, {
301631
302203
  NotificationService: () => NotificationService,
301632
302204
  NtfyNotificationProvider: () => NtfyNotificationProvider,
301633
302205
  NtfyNotifier: () => NtfyNotifier,
302206
+ OPTIONAL_STEP_REVISION_RESET_MARKER: () => OPTIONAL_STEP_REVISION_RESET_MARKER,
301634
302207
  OVERSEER_ADVISOR_REPLY_CONTRACT: () => OVERSEER_ADVISOR_REPLY_CONTRACT,
301635
302208
  OVERSEER_ADVISOR_SYSTEM_PROMPT: () => OVERSEER_ADVISOR_SYSTEM_PROMPT,
301636
302209
  OVERSEER_WATCHED_STAGES: () => OVERSEER_WATCHED_STAGES,
@@ -301810,6 +302383,7 @@ __export(src_exports2, {
301810
302383
  createFusionModelRegistry: () => createFusionModelRegistry,
301811
302384
  createGetAgentConfigTool: () => createGetAgentConfigTool,
301812
302385
  createGoalRetrievalTools: () => createGoalRetrievalTools,
302386
+ createHistoryReadTool: () => createHistoryReadTool,
301813
302387
  createIdeationTools: () => createIdeationTools,
301814
302388
  createInteractiveAiSessionWith: () => createInteractiveAiSessionWith,
301815
302389
  createListAgentsTool: () => createListAgentsTool,
@@ -301819,7 +302393,6 @@ __export(src_exports2, {
301819
302393
  createNoopLegacySeams: () => createNoopLegacySeams,
301820
302394
  createParseStepsHandler: () => createParseStepsHandler,
301821
302395
  createParsingOverseerAgent: () => createParsingOverseerAgent,
301822
- createPatchnodeReadTool: () => createPatchnodeReadTool,
301823
302396
  createPrNodeHandlers: () => createPrNodeHandlers,
301824
302397
  createPrimitivePromptLikeHandler: () => createPrimitivePromptLikeHandler,
301825
302398
  createPrimitiveStepReviewHandler: () => createPrimitiveStepReviewHandler,
@@ -301991,6 +302564,7 @@ __export(src_exports2, {
301991
302564
  normalizeAgentLogPaging: () => normalizeAgentLogPaging,
301992
302565
  normalizeJiraIssueKey: () => normalizeJiraIssueKey,
301993
302566
  notifyAutostashOrphans: () => notifyAutostashOrphans,
302567
+ optionalStepRevisionResetOutcome: () => optionalStepRevisionResetOutcome,
301994
302568
  parseAdvisorReplyForAdvice: () => parseAdvisorReplyForAdvice,
301995
302569
  parseInteractiveAgentResponse: () => parseAgentResponse,
301996
302570
  parsePrEntityMarker: () => parsePrEntityMarker,
@@ -302180,6 +302754,7 @@ var init_src5 = __esm({
302180
302754
  init_postgres_migration_notice();
302181
302755
  init_concurrency();
302182
302756
  init_triage();
302757
+ init_optional_step_revision();
302183
302758
  init_executor();
302184
302759
  init_workflow_graph_executor();
302185
302760
  init_workflow_graph_branches();
@@ -313293,6 +313868,13 @@ var init_task_planner_chat_metrics = __esm({
313293
313868
  }
313294
313869
  });
313295
313870
 
313871
+ // ../dashboard/src/chat-conversation-references.ts
313872
+ var init_chat_conversation_references = __esm({
313873
+ "../dashboard/src/chat-conversation-references.ts"() {
313874
+ "use strict";
313875
+ }
313876
+ });
313877
+
313296
313878
  // ../dashboard/src/chat.ts
313297
313879
  import { EventEmitter as EventEmitter39 } from "node:events";
313298
313880
  import { SessionManager as SessionManager3 } from "@earendil-works/pi-coding-agent";
@@ -313307,6 +313889,7 @@ var init_chat = __esm({
313307
313889
  init_task_planner_chat_context();
313308
313890
  init_task_planner_chat_metrics();
313309
313891
  init_sse2();
313892
+ init_chat_conversation_references();
313310
313893
  init_src5();
313311
313894
  init_src5();
313312
313895
  chatLog = createLogger("dashboard-chat");
@@ -318682,12 +319265,9 @@ var init_dashboard_views = __esm({
318682
319265
  { id: "board", label: "Board", labelKey: "nav.board" },
318683
319266
  { id: "list", label: "List", labelKey: "nav.list" },
318684
319267
  /*
318685
- FNXC:PatchnoteNaming 2026-08-30-01:40:
318686
- Operators call the delivery history Patchnote. Keep the stable `patchnode` identifier and translation key unchanged because routes, storage, and integrations depend on them.
318687
-
318688
- FNXC:PatchnoteNaming 2026-08-30-06:36:
318689
- Operators now call the delivery history History. The stable `patchnode` identifier and translation key remain unchanged for routes, storage, persisted mobile-nav preferences, and integrations.
318690
- */
319268
+ FNXC:HistoryNaming 2026-09-04-09:35:
319269
+ Operators call this destination History, and chat exposes it as `fn_history_read`. The `patchnode` view id, `nav.patchnode` key, `GET /api/patchnode` route, `project.patchnode_entries` table, and persisted mobile-navigation preferences intentionally remain unchanged.
319270
+ */
318691
319271
  { id: "patchnode", label: "History", labelKey: "nav.patchnode" },
318692
319272
  { id: "graph", label: "Graph" },
318693
319273
  { id: "agents", label: "Agents", labelKey: "nav.agents" },