@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/bin.js CHANGED
@@ -1956,10 +1956,11 @@ function toSnapshot(result2) {
1956
1956
  const { priorAttempts: _drop, ...rest } = result2;
1957
1957
  return rest;
1958
1958
  }
1959
- function archiveTerminalWorkflowStepFailures(results, archivedAt = (/* @__PURE__ */ new Date()).toISOString()) {
1960
- if (!results?.some(isTerminalFailure)) return results;
1959
+ function archiveTerminalWorkflowStepFailures(results, archivedAt = (/* @__PURE__ */ new Date()).toISOString(), options = {}) {
1960
+ const inScope = (result2) => isTerminalFailure(result2) && (!options.workflowStepIds || options.workflowStepIds.has(result2.workflowStepId));
1961
+ if (!results?.some(inScope)) return results;
1961
1962
  return results.map((result2) => {
1962
- if (!isTerminalFailure(result2)) return result2;
1963
+ if (!inScope(result2)) return result2;
1963
1964
  const snapshot = toSnapshot(result2);
1964
1965
  const priorAttempts = [snapshot, ...result2.priorAttempts ?? []].slice(0, MAX_WORKFLOW_STEP_PRIOR_ATTEMPTS);
1965
1966
  const {
@@ -2135,6 +2136,26 @@ function isAuditedOperatorBypass(result2) {
2135
2136
  actor && !AUTOMATED_BYPASS_ACTORS.has(actor) && result2.bypassedAt?.trim() && result2.bypassReason?.trim()
2136
2137
  );
2137
2138
  }
2139
+ function resolveCollateralArchivedReviewGate(result2, options) {
2140
+ const archivedFrom = result2.remediationArchivedFromStatus;
2141
+ 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)) {
2142
+ return void 0;
2143
+ }
2144
+ const {
2145
+ remediationArchivedAt: _archivedAt,
2146
+ remediationArchivedFromStatus: _archivedFrom,
2147
+ ...rest
2148
+ } = result2;
2149
+ return {
2150
+ restored: {
2151
+ ...rest,
2152
+ status: archivedFrom,
2153
+ output: COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC,
2154
+ notes: COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC
2155
+ },
2156
+ reason: COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC
2157
+ };
2158
+ }
2138
2159
  function resolveUnprovenReviewApproval(result2, options) {
2139
2160
  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) {
2140
2161
  return void 0;
@@ -2166,9 +2187,10 @@ function evaluateStep(workflowStepId, results, task, descriptor) {
2166
2187
  const isPlanDomain = workflowStepId === PLAN_REVIEW_GROUP_ID || result2.reviewKind === "plan";
2167
2188
  const notRunApproves = isWorkflowStepNotRun(result2) && !requiresAuthoredVerdict && !isPlanDomain;
2168
2189
  const approved = result2.status === "passed" && (requiresAuthoredVerdict ? approvedVerdict : result2.verdict === void 0 || approvedVerdict) || result2.status === "skipped" && !!result2.bypassedBy || notRunApproves;
2169
- if (!approved || !!result2.remediationArchivedAt) return { workflowStepId, state: "not-approved" };
2190
+ const auditedOperatorWaiver = isAuditedOperatorBypass(result2) && descriptor?.kind !== "workspace";
2191
+ if (!approved || result2.remediationArchivedAt != null && !auditedOperatorWaiver) return { workflowStepId, state: "not-approved" };
2170
2192
  if (result2.reviewKind === "plan") return { workflowStepId, state: "approved" };
2171
- if (isAuditedOperatorBypass(result2) && descriptor?.kind !== "workspace") {
2193
+ if (auditedOperatorWaiver) {
2172
2194
  return { workflowStepId, state: "approved" };
2173
2195
  }
2174
2196
  const bindsContent = requiresExplicitVerdict || result2.reviewInputFingerprint !== void 0;
@@ -2201,7 +2223,7 @@ function evaluateStep(workflowStepId, results, task, descriptor) {
2201
2223
  if (stale2.length) return { workflowStepId, state: "stale-content", repositories: stale2 };
2202
2224
  return { workflowStepId, state: "approved" };
2203
2225
  }
2204
- var AUTOMATED_BYPASS_ACTORS, UNPROVEN_REVIEW_APPROVAL_DIAGNOSTIC;
2226
+ var AUTOMATED_BYPASS_ACTORS, UNPROVEN_REVIEW_APPROVAL_DIAGNOSTIC, COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC;
2205
2227
  var init_pre_merge_approval = __esm({
2206
2228
  "../core/src/merge/pre-merge-approval.ts"() {
2207
2229
  "use strict";
@@ -2210,6 +2232,7 @@ var init_pre_merge_approval = __esm({
2210
2232
  init_workflow_step_results();
2211
2233
  AUTOMATED_BYPASS_ACTORS = /* @__PURE__ */ new Set([FAST_MODE_BYPASS_ACTOR]);
2212
2234
  UNPROVEN_REVIEW_APPROVAL_DIAGNOSTIC = "Content-binding review approval recorded without reviewInputFingerprint; approval invalidated so the gate can run again.";
2235
+ 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.";
2213
2236
  }
2214
2237
  });
2215
2238
 
@@ -2318,6 +2341,9 @@ function hasNonTerminalSteps(task) {
2318
2341
  function isPreMergeStepsNotRunBlocker(blocker) {
2319
2342
  return blocker === PRE_MERGE_STEPS_NOT_RUN_BLOCKER;
2320
2343
  }
2344
+ function isStaleContentApprovalBlocker(blocker) {
2345
+ return typeof blocker === "string" && blocker.trim().endsWith(STALE_CONTENT_APPROVAL_BLOCKER);
2346
+ }
2321
2347
  function getTaskMergeBlocker(task, options = {}) {
2322
2348
  if (!options.skipColumnIdentityCheck) {
2323
2349
  const reviewColumns = options.reviewColumns?.size ? options.reviewColumns : void 0;
@@ -2339,8 +2365,10 @@ function getTaskMergeBlocker(task, options = {}) {
2339
2365
  }
2340
2366
  const approval = evaluatePreMergeApprovals(task, options).find((candidate) => candidate.state !== "approved");
2341
2367
  if (approval?.state === "missing") return PRE_MERGE_STEPS_NOT_RUN_BLOCKER;
2342
- if (approval?.state === "not-approved") return "task has enabled pre-merge workflow steps without a current approval";
2343
- if (approval?.state === "stale-content") return "task has a pre-merge approval recorded against different content";
2368
+ if (approval?.state === "not-approved") {
2369
+ return `task has enabled pre-merge workflow steps without a current approval (gate '${approval.workflowStepId}')`;
2370
+ }
2371
+ if (approval?.state === "stale-content") return STALE_CONTENT_APPROVAL_BLOCKER;
2344
2372
  if (approval?.state === "unprovable-content") return "task has no provable approval for the content being merged";
2345
2373
  if (task.workflowStepResults?.some((result2) => {
2346
2374
  const phase = result2.phase || "pre-merge";
@@ -2357,11 +2385,14 @@ function getTaskMergeBlocker(task, options = {}) {
2357
2385
  return void 0;
2358
2386
  }
2359
2387
  function getLatestFailedPreMergeReviewStep(task) {
2360
- return (task.workflowStepResults ?? []).filter((result2) => (result2.phase || "pre-merge") === "pre-merge" && result2.status === "failed").sort((a, b3) => {
2388
+ const results = task.workflowStepResults ?? [];
2389
+ const recentFirst = (a, b3) => {
2361
2390
  const aTs = Date.parse(a.completedAt || a.startedAt || "");
2362
2391
  const bTs = Date.parse(b3.completedAt || b3.startedAt || "");
2363
2392
  return (Number.isFinite(bTs) ? bTs : 0) - (Number.isFinite(aTs) ? aTs : 0);
2364
- })[0];
2393
+ };
2394
+ const isPreMerge = (result2) => (result2.phase || "pre-merge") === "pre-merge";
2395
+ 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];
2365
2396
  }
2366
2397
  function clearMergeConfirmedTransientStatus(status) {
2367
2398
  return status !== void 0 && MERGE_CONFIRMED_TRANSIENT_STATUSES.has(status) ? void 0 : status;
@@ -2445,13 +2476,14 @@ function findPendingPreMergeStep(task) {
2445
2476
  (a, b3) => new Date(b3.startedAt ?? 0).getTime() - new Date(a.startedAt ?? 0).getTime()
2446
2477
  )[0];
2447
2478
  }
2448
- 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;
2479
+ 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;
2449
2480
  var init_task_merge = __esm({
2450
2481
  "../core/src/merge/task-merge.ts"() {
2451
2482
  "use strict";
2452
2483
  init_branch_group_completion();
2453
2484
  init_task_helpers();
2454
2485
  init_pre_merge_approval();
2486
+ init_workflow_step_results();
2455
2487
  FUSION_SIBLING_BRANCH_RE = /^fusion\/fn-/i;
2456
2488
  AWAITING_APPROVAL_PAUSE_REASON = "awaiting-approval";
2457
2489
  HARD_BLOCKING_TASK_STATUSES = /* @__PURE__ */ new Set([
@@ -2495,6 +2527,7 @@ var init_task_merge = __esm({
2495
2527
  "pending"
2496
2528
  ]);
2497
2529
  PRE_MERGE_STEPS_NOT_RUN_BLOCKER = "task has enabled pre-merge workflow steps that never ran";
2530
+ STALE_CONTENT_APPROVAL_BLOCKER = "task has a pre-merge approval recorded against different content";
2498
2531
  PreMergeStepsNotRunError = class extends Error {
2499
2532
  code = "pre-merge-steps-not-run";
2500
2533
  taskId;
@@ -6474,8 +6507,9 @@ function renderValidationCause(diagnostics8) {
6474
6507
  }
6475
6508
  function featureValidationRepairEligibility(feature) {
6476
6509
  const repairableLoop = feature.loopState === "blocked" || feature.loopState === "needs_fix";
6510
+ const unvalidatedPassedMarker = feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId;
6477
6511
  return {
6478
- clear: repairableLoop || feature.status === "blocked",
6512
+ clear: repairableLoop || feature.status === "blocked" || unvalidatedPassedMarker,
6479
6513
  reRun: repairableLoop || feature.status === "blocked" && (feature.loopState === void 0 || feature.loopState === "idle")
6480
6514
  };
6481
6515
  }
@@ -6539,7 +6573,8 @@ var init_mission_types = __esm({
6539
6573
  implementing: [],
6540
6574
  validating: [],
6541
6575
  needs_fix: ["idle", "implementing"],
6542
- passed: [],
6576
+ /* FNXC:Missions 2026-09-05-22:07: Explicit repair may return a fabricated unvalidated passed marker to idle (issue #3574). */
6577
+ passed: ["idle"],
6543
6578
  blocked: ["idle", "implementing"]
6544
6579
  };
6545
6580
  VALIDATOR_RUN_STATUSES = ["running", "passed", "failed", "blocked", "error"];
@@ -13646,7 +13681,7 @@ function isBlockingFinding(finding, threshold) {
13646
13681
  if (!finding.severity) return true;
13647
13682
  return SEVERITY_RANK[finding.severity] >= SEVERITY_RANK[threshold];
13648
13683
  }
13649
- function applyReviewSeverityGate({ verdict, findings, threshold }) {
13684
+ function applyReviewSeverityGate({ verdict, findings, threshold, findingsUnreadable }) {
13650
13685
  const all = findings ?? [];
13651
13686
  const open7 = all.filter(isActionableReviewFinding);
13652
13687
  const resolved = all.filter((finding) => !isActionableReviewFinding(finding));
@@ -13654,6 +13689,7 @@ function applyReviewSeverityGate({ verdict, findings, threshold }) {
13654
13689
  const advisory = open7.filter((finding) => !isBlockingFinding(finding, threshold));
13655
13690
  if (verdict !== "REVISE") return { verdict, downgraded: false, blocking, advisory, resolved };
13656
13691
  if (blocking.length > 0) return { verdict, downgraded: false, blocking, advisory, resolved };
13692
+ if (findingsUnreadable) return { verdict, downgraded: false, blocking, advisory, resolved };
13657
13693
  return { verdict: "APPROVE_WITH_NOTES", downgraded: true, blocking, advisory, resolved };
13658
13694
  }
13659
13695
  function formatFindingsByPriority(findings) {
@@ -18395,6 +18431,18 @@ function detectWorkflowDrift(ir2, pin) {
18395
18431
  function defaultCodingWorkflowIr() {
18396
18432
  return resolveDefaultWorkflowIr();
18397
18433
  }
18434
+ async function prefetchWorkflowSelections(store, taskIds, cache8) {
18435
+ const missingIds = [...new Set(taskIds)].filter((id) => !cache8.has(id));
18436
+ if (missingIds.length === 0) return { batched: 0, singles: 0 };
18437
+ if (!store.getTaskWorkflowSelectionsAsync) return { batched: 0, singles: missingIds.length };
18438
+ try {
18439
+ const selections = await store.getTaskWorkflowSelectionsAsync(missingIds);
18440
+ for (const id of missingIds) cache8.set(id, selections.get(id));
18441
+ return { batched: 1, singles: 0 };
18442
+ } catch {
18443
+ return { batched: 0, singles: missingIds.length };
18444
+ }
18445
+ }
18398
18446
  function resolveSeamPromptFromIr(ir2, seam) {
18399
18447
  for (const node of ir2.nodes) {
18400
18448
  if (node.kind !== "prompt") continue;
@@ -18613,9 +18661,9 @@ function resolveLifecycleColumns(ir2) {
18613
18661
  archived: first(LIFECYCLE_ROLE_FLAGS.archived)
18614
18662
  };
18615
18663
  }
18616
- async function resolveTaskLifecycleColumns(store, taskId, cache8) {
18664
+ async function resolveTaskLifecycleColumns(store, taskId, cache8, selectionCache) {
18617
18665
  try {
18618
- const ir2 = await resolveWorkflowIrForTask(store, taskId, cache8);
18666
+ const ir2 = await resolveWorkflowIrForTask(store, taskId, cache8, selectionCache);
18619
18667
  return resolveLifecycleColumns(ir2);
18620
18668
  } catch {
18621
18669
  return void 0;
@@ -30611,18 +30659,33 @@ function isValidPluginParserId(id) {
30611
30659
  const seg = /^[a-z0-9-]+$/;
30612
30660
  return seg.test(parts[1]) && seg.test(parts[2]);
30613
30661
  }
30614
- function parseStepHeadings(content) {
30615
- const steps = [];
30662
+ function matchStepHeadings(content) {
30616
30663
  const stepRegex = /^###\s+Step\s+\d+[^:]*:\s*(.+)$/gm;
30617
- const annotatedRegex = /^###\s+Step\s+\d+\s*\(depends:\s*([^)]*)\)\s*:\s*([^\n]+)$/;
30618
30664
  const matches = [];
30619
30665
  let match2;
30620
30666
  while ((match2 = stepRegex.exec(content)) !== null) {
30621
30667
  const heading = /^###\s+Step\s+(\d+)/.exec(match2[0]);
30622
- if (heading) {
30623
- matches.push({ full: match2[0], name: match2[1], headingNumber: Number(heading[1]) });
30624
- }
30668
+ if (!heading) continue;
30669
+ const index3 = match2.index;
30670
+ const newline = content.indexOf("\n", index3);
30671
+ matches.push({
30672
+ headingNumber: Number(heading[1]),
30673
+ index: index3,
30674
+ headingLineEnd: newline === -1 ? content.length : newline,
30675
+ match: match2[0]
30676
+ });
30625
30677
  }
30678
+ return matches;
30679
+ }
30680
+ function parseStepHeadings(content) {
30681
+ const steps = [];
30682
+ const annotatedRegex = /^###\s+Step\s+\d+\s*\(depends:\s*([^)]*)\)\s*:\s*([^\n]+)$/;
30683
+ const matches = matchStepHeadings(content).map((entry) => ({
30684
+ full: entry.match,
30685
+ name: /^###\s+Step\s+\d+[^:]*:\s*(.+)$/m.exec(entry.match)?.[1] ?? "",
30686
+ headingNumber: entry.headingNumber
30687
+ }));
30688
+ let match2;
30626
30689
  const offset = resolveAuthoredStepHeadingOffset(matches.map((entry) => entry.headingNumber));
30627
30690
  for (const { full, name: legacyName } of matches) {
30628
30691
  if (!full.includes("(depends:")) {
@@ -61919,12 +61982,12 @@ async function listGlobalConfigurationRevisions(layer, configKind, configTarget,
61919
61982
  });
61920
61983
  }
61921
61984
  async function listConfigurationRevisionsPage(handle2, params) {
61922
- const clampInteger2 = (value2, fallback2, minimum, maximum = Number.MAX_SAFE_INTEGER) => {
61985
+ const clampInteger3 = (value2, fallback2, minimum, maximum = Number.MAX_SAFE_INTEGER) => {
61923
61986
  const integer3 = typeof value2 === "number" && Number.isFinite(value2) ? Math.trunc(value2) : fallback2;
61924
61987
  return Math.min(maximum, Math.max(minimum, integer3));
61925
61988
  };
61926
- const limit2 = clampInteger2(params.limit, 100, 1, 500);
61927
- const offset = clampInteger2(params.offset, 0, 0);
61989
+ const limit2 = clampInteger3(params.limit, 100, 1, 500);
61990
+ const offset = clampInteger3(params.offset, 0, 0);
61928
61991
  const rows = await handle2.select().from(schema_exports.project.configurationRevisions).where(and(
61929
61992
  eq(schema_exports.project.configurationRevisions.projectId, params.projectId),
61930
61993
  eq(schema_exports.project.configurationRevisions.configKind, params.configKind),
@@ -75219,8 +75282,8 @@ function appendAgentLogEntriesSync(taskDir, entries2) {
75219
75282
  if (entries2.length === 0) return [];
75220
75283
  const filePath = getAgentLogFilePath(taskDir);
75221
75284
  mkdirSync5(dirname15(filePath), { recursive: true });
75222
- const startingLineNo = countLineNumbers(filePath);
75223
- const payload = entries2.map((entry) => serializeEntry(entry)).join("");
75285
+ const { lineCount: startingLineNo, endsWithNewline } = inspectAgentLogTail(filePath);
75286
+ const payload = `${startingLineNo > 0 && !endsWithNewline ? "\n" : ""}${entries2.map((entry) => serializeEntry(entry)).join("")}`;
75224
75287
  appendFileSync(filePath, payload, "utf8");
75225
75288
  return entries2.map((entry, index3) => materializeEntry(entry, startingLineNo + index3 + 1));
75226
75289
  }
@@ -75256,6 +75319,7 @@ function readAllAgentLogEntries(taskDir, options = {}) {
75256
75319
  }
75257
75320
  const lines = content.split("\n");
75258
75321
  const entries2 = [];
75322
+ const skippedLines = [];
75259
75323
  for (let index3 = 0; index3 < lines.length; index3 += 1) {
75260
75324
  const rawLine = lines[index3];
75261
75325
  if (!rawLine) continue;
@@ -75277,9 +75341,18 @@ function readAllAgentLogEntries(taskDir, options = {}) {
75277
75341
  }
75278
75342
  entries2.push(entry);
75279
75343
  } catch (error3) {
75280
- log7.warn(`Skipping malformed JSONL line ${lineNo} in ${filePath}`, error3);
75344
+ skippedLines.push({
75345
+ lineNo,
75346
+ message: error3 instanceof Error ? error3.message : String(error3)
75347
+ });
75281
75348
  }
75282
75349
  }
75350
+ const firstSkipped = skippedLines[0];
75351
+ if (firstSkipped) {
75352
+ log7.warn(
75353
+ `Skipped ${skippedLines.length} malformed JSONL line(s) in ${filePath}; first line ${firstSkipped.lineNo}: ${firstSkipped.message}`
75354
+ );
75355
+ }
75283
75356
  return entries2;
75284
75357
  }
75285
75358
  function normalizeTimingMs(value2) {
@@ -75322,16 +75395,19 @@ function materializeEntry(entry, lineNo) {
75322
75395
  sourceRef: buildAgentLogSourceRef(entry.taskId, lineNo)
75323
75396
  };
75324
75397
  }
75325
- function countLineNumbers(filePath) {
75398
+ function inspectAgentLogTail(filePath) {
75326
75399
  if (!existsSync17(filePath)) {
75327
- return 0;
75400
+ return { lineCount: 0, endsWithNewline: true };
75328
75401
  }
75329
75402
  const content = readFileSync8(filePath, "utf8");
75330
75403
  if (content.length === 0) {
75331
- return 0;
75404
+ return { lineCount: 0, endsWithNewline: true };
75332
75405
  }
75333
75406
  const lines = content.split("\n");
75334
- return lines.at(-1) === "" ? lines.length - 1 : lines.length;
75407
+ return {
75408
+ lineCount: lines.at(-1) === "" ? lines.length - 1 : lines.length,
75409
+ endsWithNewline: content.endsWith("\n")
75410
+ };
75335
75411
  }
75336
75412
  function pruneAgentLogFiles(tasksDir, retentionDays, scanTaskIds) {
75337
75413
  if (!Number.isFinite(retentionDays) || retentionDays <= 0 || !existsSync17(tasksDir)) {
@@ -79121,6 +79197,10 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0;
79121
79197
  * FNXC:Missions 2026-07-11-12:35:
79122
79198
  * A generated fix can also supersede itself once its own validator/loop evidence has passed.
79123
79199
  * Reconciliation treats that as terminal evidence so a completed fix does not stay active only because its ancestor previously failed.
79200
+ *
79201
+ * FNXC:Missions 2026-09-05-22:07:
79202
+ * Superseding a generated fix means it is no longer needed, not that it passed validation.
79203
+ * Never stamp unearned validator evidence because that re-arms the reconciler's own trigger (issue #3574).
79124
79204
  */
79125
79205
  reconcileSupersededGeneratedFixFeatures(sliceId) {
79126
79206
  const features = this.listFeatures(sliceId);
@@ -79145,7 +79225,42 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0;
79145
79225
  ancestorPassedMemo.set(feature.id, passed);
79146
79226
  return passed;
79147
79227
  };
79148
- 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);
79228
+ const isFabricatedMarker = (feature) => Boolean(
79229
+ (feature.generatedFromFeatureId || feature.generatedFromRunId) && feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId && !feature.taskId && feature.status === "done" && !hasPassedAncestor(feature)
79230
+ );
79231
+ const repairedFeatureIds = features.filter(isFabricatedMarker).map((feature) => feature.id);
79232
+ const repairedIds = new Set(repairedFeatureIds);
79233
+ 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);
79234
+ if (repairedFeatureIds.length > 0) {
79235
+ this.db.transaction(() => {
79236
+ const hasCurrentPassedAncestor = (feature, seen = /* @__PURE__ */ new Set()) => {
79237
+ const sourceFeatureId = feature.generatedFromFeatureId;
79238
+ if (!sourceFeatureId || seen.has(sourceFeatureId)) return false;
79239
+ seen.add(sourceFeatureId);
79240
+ const sourceFeature = this.getFeature(sourceFeatureId);
79241
+ return featureHasPassed(sourceFeature) || (sourceFeature ? hasCurrentPassedAncestor(sourceFeature, seen) : false);
79242
+ };
79243
+ const isCurrentFabricatedMarker = (feature) => Boolean(
79244
+ (feature.generatedFromFeatureId || feature.generatedFromRunId) && feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId && !feature.taskId && feature.status === "done" && !hasCurrentPassedAncestor(feature)
79245
+ );
79246
+ for (const featureId of repairedFeatureIds) {
79247
+ const feature = this.getFeature(featureId);
79248
+ if (!feature || !isCurrentFabricatedMarker(feature)) continue;
79249
+ this.updateFeature(featureId, { status: "defined", taskId: void 0, loopState: "idle", lastValidatorStatus: void 0 });
79250
+ const slice = this.getSlice(feature.sliceId);
79251
+ const milestone = slice ? this.getMilestone(slice.milestoneId) : void 0;
79252
+ if (milestone) {
79253
+ this.logMissionEvent(milestone.missionId, "feature_status_changed", `Feature status changed from ${feature.status} to defined`, {
79254
+ featureId,
79255
+ field: "status",
79256
+ from: feature.status,
79257
+ to: "defined",
79258
+ source: "superseded-fix-marker-repair"
79259
+ });
79260
+ }
79261
+ }
79262
+ });
79263
+ }
79149
79264
  if (supersededFeatureIds.length > 0) {
79150
79265
  this.db.transaction(() => {
79151
79266
  for (const featureId of supersededFeatureIds) {
@@ -79154,8 +79269,7 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0;
79154
79269
  this.updateFeature(featureId, {
79155
79270
  status: "done",
79156
79271
  taskId: void 0,
79157
- loopState: "passed",
79158
- lastValidatorStatus: "passed"
79272
+ loopState: "passed"
79159
79273
  });
79160
79274
  if (feature.taskId) {
79161
79275
  this.db.prepare('UPDATE tasks SET missionId = NULL, sliceId = NULL WHERE id = ? AND "deletedAt" IS NULL').run(feature.taskId);
@@ -79165,7 +79279,9 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0;
79165
79279
  }
79166
79280
  return {
79167
79281
  supersededCount: supersededFeatureIds.length,
79168
- featureIds: supersededFeatureIds
79282
+ featureIds: supersededFeatureIds,
79283
+ repairedCount: repairedFeatureIds.length,
79284
+ repairedFeatureIds
79169
79285
  };
79170
79286
  }
79171
79287
  /**
@@ -82509,10 +82625,12 @@ var init_async_mission_store = __esm({
82509
82625
  throw new RepairNotEligibleError(featureId, options.action);
82510
82626
  }
82511
82627
  const now = (/* @__PURE__ */ new Date()).toISOString();
82628
+ const hasUnvalidatedMarker = options.action === "clear" && feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId;
82629
+ const appliesUnvalidatedMarker = hasUnvalidatedMarker && Boolean(feature.generatedFromFeatureId || feature.generatedFromRunId) && !feature.taskId && feature.status === "done";
82512
82630
  const priorLoopState = feature.loopState;
82513
82631
  const priorStatus = feature.status;
82514
82632
  let groundTruthMetadata = {};
82515
- if (options.action === "clear" && feature.status === "blocked") {
82633
+ if (options.action === "clear" && (feature.status === "blocked" || appliesUnvalidatedMarker)) {
82516
82634
  const fence = options.groundTruth;
82517
82635
  if (!fence || fence.featureId !== featureId || fence.taskId !== (feature.taskId ?? null)) {
82518
82636
  throw new RepairGroundTruthStaleError(featureId);
@@ -82542,14 +82660,14 @@ var init_async_mission_store = __esm({
82542
82660
  let updated;
82543
82661
  let run2;
82544
82662
  if (options.action === "clear") {
82545
- const currentLoop = feature.loopState;
82546
- const appliesLoop = currentLoop === "blocked" || currentLoop === "needs_fix";
82663
+ const currentLoop = feature.loopState ?? "idle";
82664
+ const appliesLoop = currentLoop === "blocked" || currentLoop === "needs_fix" || appliesUnvalidatedMarker;
82547
82665
  const nextLoop = appliesLoop ? options.resolvedLoopState ?? "idle" : currentLoop;
82548
82666
  if (appliesLoop && !FEATURE_LOOP_REPAIR_TRANSITIONS[currentLoop].includes(nextLoop)) {
82549
82667
  throw new Error(`Invalid validation repair transition from '${currentLoop}' to '${nextLoop}'`);
82550
82668
  }
82551
- const appliesStatus = feature.status === "blocked";
82552
- const nextStatus = appliesStatus ? options.resolvedStatus : feature.status;
82669
+ const appliesStatus = feature.status === "blocked" || appliesUnvalidatedMarker;
82670
+ const nextStatus = appliesUnvalidatedMarker ? "defined" : appliesStatus ? options.resolvedStatus : feature.status;
82553
82671
  if (appliesStatus && (nextStatus !== "in-progress" && nextStatus !== "triaged" && nextStatus !== "defined")) {
82554
82672
  throw new Error("Validation repair requires resolvedStatus of in-progress, triaged, or defined");
82555
82673
  }
@@ -82561,13 +82679,13 @@ var init_async_mission_store = __esm({
82561
82679
  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";
82562
82680
  if (!matchesLane) throw new RepairGroundTruthStaleError(featureId);
82563
82681
  }
82564
- if (!appliesLoop && !appliesStatus) throw new RepairNotEligibleError(featureId, options.action);
82682
+ if (!appliesLoop && !appliesStatus && !hasUnvalidatedMarker) throw new RepairNotEligibleError(featureId, options.action);
82565
82683
  updated = {
82566
82684
  ...feature,
82567
82685
  loopState: nextLoop,
82568
82686
  status: nextStatus,
82569
82687
  implementationAttemptCount: 0,
82570
- ...feature.lastValidatorStatus === "blocked" || feature.lastValidatorStatus === "failed" ? { lastValidatorStatus: void 0 } : {},
82688
+ ...feature.lastValidatorStatus === "blocked" || feature.lastValidatorStatus === "failed" || hasUnvalidatedMarker ? { lastValidatorStatus: void 0 } : {},
82571
82689
  updatedAt: now
82572
82690
  };
82573
82691
  await updateFeature(tx, updated);
@@ -83035,10 +83153,60 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0,
83035
83153
  const source = byId.get(sourceId);
83036
83154
  return passed(source) || (source ? hasPassedAncestor(source, seen) : false);
83037
83155
  };
83156
+ const isFabricatedMarker = (feature) => Boolean(
83157
+ (feature.generatedFromFeatureId || feature.generatedFromRunId) && feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId && !feature.taskId && feature.status === "done" && !hasPassedAncestor(feature)
83158
+ );
83159
+ const repairIds = new Set(features.filter(isFabricatedMarker).map((feature) => feature.id));
83038
83160
  const ids = [];
83039
83161
  for (const feature of features) {
83040
- if (!feature.generatedFromFeatureId || !(passed(feature) || hasPassedAncestor(feature))) continue;
83041
- if (feature.status !== "done" || feature.loopState !== "passed" || feature.lastValidatorStatus !== "passed" || feature.taskId) ids.push(feature.id);
83162
+ if (repairIds.has(feature.id) || !feature.generatedFromFeatureId || !(passed(feature) || hasPassedAncestor(feature))) continue;
83163
+ if (feature.status !== "done" || feature.loopState !== "passed" || feature.taskId) ids.push(feature.id);
83164
+ }
83165
+ const repairedFeatureIds = [];
83166
+ if (repairIds.size > 0) {
83167
+ const now = (/* @__PURE__ */ new Date()).toISOString();
83168
+ const repaired = await this.layer.transactionImmediate(async (tx) => {
83169
+ const locked = await tx.select({ id: project_exports.missionFeatures.id }).from(project_exports.missionFeatures).where(inArray(project_exports.missionFeatures.id, [...repairIds])).for("update");
83170
+ const preImages = locked.length > 0 ? await listFeaturesByIds(tx, locked.map((row) => row.id)) : [];
83171
+ const currentById = new Map(preImages.map((feature) => [feature.id, feature]));
83172
+ let missingAncestorIds = [...new Set(preImages.map((feature) => feature.generatedFromFeatureId).filter((id) => Boolean(id) && !currentById.has(id)))];
83173
+ while (missingAncestorIds.length > 0) {
83174
+ const lockedAncestors = await tx.select({ id: project_exports.missionFeatures.id }).from(project_exports.missionFeatures).where(inArray(project_exports.missionFeatures.id, missingAncestorIds)).for("update");
83175
+ const ancestors = lockedAncestors.length > 0 ? await listFeaturesByIds(tx, lockedAncestors.map((row) => row.id)) : [];
83176
+ for (const ancestor of ancestors) currentById.set(ancestor.id, ancestor);
83177
+ missingAncestorIds = [...new Set(ancestors.map((ancestor) => ancestor.generatedFromFeatureId).filter((id) => Boolean(id) && !currentById.has(id)))];
83178
+ }
83179
+ const hasCurrentPassedAncestor = (feature, seen = /* @__PURE__ */ new Set()) => {
83180
+ const sourceId = feature.generatedFromFeatureId;
83181
+ if (!sourceId || seen.has(sourceId)) return false;
83182
+ seen.add(sourceId);
83183
+ const source = currentById.get(sourceId);
83184
+ return passed(source) || (source ? hasCurrentPassedAncestor(source, seen) : false);
83185
+ };
83186
+ const changed = preImages.filter((feature) => Boolean(
83187
+ (feature.generatedFromFeatureId || feature.generatedFromRunId) && feature.lastValidatorStatus === "passed" && !feature.lastValidatorRunId && !feature.taskId && feature.status === "done" && !hasCurrentPassedAncestor(feature)
83188
+ ));
83189
+ if (changed.length === 0) return { events: [], features: [] };
83190
+ await tx.update(project_exports.missionFeatures).set({
83191
+ status: "defined",
83192
+ taskId: null,
83193
+ loopState: "idle",
83194
+ lastValidatorStatus: null,
83195
+ updatedAt: now
83196
+ }).where(inArray(project_exports.missionFeatures.id, changed.map((feature) => feature.id)));
83197
+ let seq = await getMaxEventSeq(tx);
83198
+ const events = [];
83199
+ for (const feature of changed) {
83200
+ const event = await this.recordFeatureStatusChange(tx, feature, "defined", { type: "system", id: "mission-store", source: "superseded-fix-marker-repair" }, void 0, ++seq);
83201
+ if (event) events.push(event);
83202
+ }
83203
+ return { events, features: changed };
83204
+ });
83205
+ for (const event of repaired.events) this.emit("mission:event", event);
83206
+ for (const feature of repaired.features) {
83207
+ this.emit("feature:updated", { ...feature, status: "defined", taskId: void 0, loopState: "idle", lastValidatorStatus: void 0, updatedAt: now });
83208
+ repairedFeatureIds.push(feature.id);
83209
+ }
83042
83210
  }
83043
83211
  if (ids.length > 0) {
83044
83212
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -83046,13 +83214,12 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0,
83046
83214
  const locked = await tx.select({ id: project_exports.missionFeatures.id }).from(project_exports.missionFeatures).where(inArray(project_exports.missionFeatures.id, ids)).for("update");
83047
83215
  const lockedIds = locked.map((row) => row.id);
83048
83216
  const preImages = lockedIds.length > 0 ? await listFeaturesByIds(tx, lockedIds) : [];
83049
- const changed = preImages.filter((feature) => feature.status !== "done" || feature.loopState !== "passed" || feature.lastValidatorStatus !== "passed" || feature.taskId);
83217
+ const changed = preImages.filter((feature) => feature.status !== "done" || feature.loopState !== "passed" || feature.taskId);
83050
83218
  if (changed.length === 0) return { events: [], updatedFeatures: [] };
83051
83219
  await tx.update(project_exports.missionFeatures).set({
83052
83220
  status: "done",
83053
83221
  taskId: null,
83054
83222
  loopState: "passed",
83055
- lastValidatorStatus: "passed",
83056
83223
  updatedAt: now
83057
83224
  }).where(inArray(project_exports.missionFeatures.id, changed.map((feature) => feature.id)));
83058
83225
  let seq = await getMaxEventSeq(tx);
@@ -83067,13 +83234,14 @@ ${reasonText}` : void 0)].filter(Boolean).join("\n\n") || void 0,
83067
83234
  });
83068
83235
  for (const event of events) this.emit("mission:event", event);
83069
83236
  for (const feature of updatedFeatures) {
83070
- const updated = { ...feature, status: "done", taskId: void 0, loopState: "passed", lastValidatorStatus: "passed", updatedAt: now };
83237
+ const updated = { ...feature, status: "done", taskId: void 0, loopState: "passed", updatedAt: now };
83071
83238
  this.emit("feature:updated", updated);
83072
83239
  if (feature.taskId) await clearTaskMissionLinkage(this.db, feature.taskId);
83073
83240
  }
83074
83241
  if (updatedFeatures.length > 0) await this.recomputeSliceStatus(sliceId);
83075
83242
  }
83076
- return { supersededCount: ids.length, featureIds: ids };
83243
+ if (repairedFeatureIds.length > 0) await this.recomputeSliceStatus(sliceId);
83244
+ return { supersededCount: ids.length, featureIds: ids, repairedCount: repairedFeatureIds.length, repairedFeatureIds };
83077
83245
  }
83078
83246
  async transitionLoopState(featureId, newState) {
83079
83247
  const feature = await getFeature(this.db, featureId);
@@ -88705,19 +88873,81 @@ async function archiveAllDoneImpl(store, options) {
88705
88873
  for (const column of completeColumns) {
88706
88874
  for (const task of await store.listTasks({ slim: true, column })) doneById.set(task.id, task);
88707
88875
  }
88708
- const doneTasks = [...doneById.values()];
88709
- if (doneTasks.length === 0) {
88710
- return [];
88876
+ if (doneById.size === 0) return { archived: [], skipped: [] };
88877
+ const archivedColumns = await resolveArchivedLanes(store).catch(() => void 0) ?? new Set(LEGACY_ARCHIVE_LANES);
88878
+ const liveTasks = (await store.listTasks({ slim: true, includeArchived: false })).filter((task) => !task.deletedAt && !archivedColumns.has(task.column));
88879
+ const childrenByParentId = /* @__PURE__ */ new Map();
88880
+ for (const task of liveTasks) {
88881
+ if (!task.sourceParentTaskId) continue;
88882
+ const children = childrenByParentId.get(task.sourceParentTaskId) ?? [];
88883
+ children.push(task.id);
88884
+ childrenByParentId.set(task.sourceParentTaskId, children);
88885
+ }
88886
+ for (const children of childrenByParentId.values()) children.sort();
88887
+ const bypassLineageGate = options?.removeLineageReferences === true;
88888
+ const archivableIds = new Set(doneById.keys());
88889
+ const skipped = [];
88890
+ if (!bypassLineageGate) {
88891
+ let firstRemovalWave = true;
88892
+ while (true) {
88893
+ const removals = [...archivableIds].filter(
88894
+ (id) => (childrenByParentId.get(id) ?? []).some((childId) => !archivableIds.has(childId))
88895
+ ).sort();
88896
+ if (removals.length === 0) break;
88897
+ for (const id of removals) {
88898
+ const blockers = (childrenByParentId.get(id) ?? []).filter((childId) => !archivableIds.has(childId));
88899
+ skipped.push({
88900
+ id,
88901
+ reason: firstRemovalWave ? "open-lineage-children" : "blocked-by-unarchived-batch-member",
88902
+ blockers,
88903
+ message: firstRemovalWave ? `Live lineage children prevent archiving ${id}.` : `Unarchived batch members prevent archiving ${id}.`
88904
+ });
88905
+ archivableIds.delete(id);
88906
+ }
88907
+ firstRemovalWave = false;
88908
+ }
88909
+ }
88910
+ const archived = [];
88911
+ const remaining = new Set(archivableIds);
88912
+ const archivedIds = /* @__PURE__ */ new Set();
88913
+ const archiveWave = async (ids) => {
88914
+ const settled = await Promise.allSettled(ids.map((id) => store.archiveTask(id, {
88915
+ cleanup: true,
88916
+ removeLineageReferences: options?.removeLineageReferences
88917
+ })));
88918
+ for (const [index3, result2] of settled.entries()) {
88919
+ const id = ids[index3];
88920
+ if (result2.status === "fulfilled") {
88921
+ archived.push(result2.value);
88922
+ archivedIds.add(id);
88923
+ } else {
88924
+ const error3 = result2.reason;
88925
+ const blockers = error3 instanceof TaskHasLineageChildrenError ? error3.childIds : error3 instanceof TaskHasDependentsError ? error3.dependentIds : [];
88926
+ skipped.push({ id, reason: "archive-failed", blockers, message: error3 instanceof Error ? error3.message : String(error3) });
88927
+ }
88928
+ remaining.delete(id);
88929
+ }
88930
+ };
88931
+ while (remaining.size > 0) {
88932
+ const wave = [...remaining].filter(
88933
+ (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))
88934
+ ).sort();
88935
+ if (wave.length > 0) {
88936
+ await archiveWave(wave);
88937
+ continue;
88938
+ }
88939
+ if (bypassLineageGate) {
88940
+ await archiveWave([...remaining].sort());
88941
+ } else {
88942
+ for (const id of [...remaining].sort()) {
88943
+ const blockers = (childrenByParentId.get(id) ?? []).filter((childId) => remaining.has(childId) || !archivedIds.has(childId));
88944
+ skipped.push({ id, reason: "blocked-by-unarchived-batch-member", blockers, message: `Unarchived batch members prevent archiving ${id}.` });
88945
+ remaining.delete(id);
88946
+ }
88947
+ }
88711
88948
  }
88712
- const archivedTasks3 = await Promise.all(
88713
- doneTasks.map(
88714
- (task) => store.archiveTask(task.id, {
88715
- cleanup: true,
88716
- removeLineageReferences: options?.removeLineageReferences
88717
- })
88718
- )
88719
- );
88720
- return archivedTasks3;
88949
+ skipped.sort((left, right) => left.id.localeCompare(right.id));
88950
+ return { archived, skipped };
88721
88951
  }
88722
88952
  async function resolveUnarchiveTargetColumnImpl(store, preArchiveColumn, taskId) {
88723
88953
  const lifecycle2 = taskId ? await resolveTaskLifecycleColumns(store, taskId) : void 0;
@@ -89181,6 +89411,7 @@ var init_task_artifacts_ops = __esm({
89181
89411
  init_types();
89182
89412
  init_store();
89183
89413
  init_project_lane_vocabulary();
89414
+ init_errors3();
89184
89415
  LEGACY_ARCHIVE_LANES = ["archived"];
89185
89416
  }
89186
89417
  });
@@ -107052,18 +107283,18 @@ function hasFreshAgentLogActivitySinceTaskUpdate(store, task, now, reviewColumns
107052
107283
  }
107053
107284
  return Math.max(0, now - latestAgentLogMs) < DEFAULT_STALE_MERGING_MIN_AGE_MS;
107054
107285
  }
107055
- async function resolveHoldColumnForTask(store, taskId, cache8) {
107286
+ async function resolveHoldColumnForTask(store, taskId, cache8, selectionCache) {
107056
107287
  try {
107057
- const lifecycle2 = resolveLifecycleColumns(await resolveWorkflowIrForTask(store, taskId, cache8));
107288
+ const lifecycle2 = resolveLifecycleColumns(await resolveWorkflowIrForTask(store, taskId, cache8, selectionCache));
107058
107289
  return lifecycle2?.hold ?? "todo";
107059
107290
  } catch {
107060
107291
  return "todo";
107061
107292
  }
107062
107293
  }
107063
- async function resolveReviewColumnsForTask(store, taskId, cache8) {
107294
+ async function resolveReviewColumnsForTask(store, taskId, cache8, selectionCache) {
107064
107295
  const columns = /* @__PURE__ */ new Set(["in-review"]);
107065
107296
  try {
107066
- const ir2 = await resolveWorkflowIrForTask(store, taskId, cache8);
107297
+ const ir2 = await resolveWorkflowIrForTask(store, taskId, cache8, selectionCache);
107067
107298
  if (ir2) for (const id of resolveReviewColumns(ir2)) columns.add(id);
107068
107299
  } catch {
107069
107300
  }
@@ -107184,6 +107415,12 @@ async function listTasksImpl(store, options) {
107184
107415
  const settings = await store.getSettingsFast();
107185
107416
  const mergeQueuedTaskIds = await store.getMergeQueuedTaskIdsAsync();
107186
107417
  const listPassIrCache = /* @__PURE__ */ new Map();
107418
+ const listPassSelectionCache = options?.selectionCache ?? /* @__PURE__ */ new Map();
107419
+ const listSelectionReads = await prefetchWorkflowSelections(store, filteredRows.map((row) => String(row.id)), listPassSelectionCache);
107420
+ if (options?.selectionReadTally) {
107421
+ options.selectionReadTally.batched += listSelectionReads.batched;
107422
+ options.selectionReadTally.singles += listSelectionReads.singles;
107423
+ }
107187
107424
  const staleThresholds = {
107188
107425
  inProgressWarningMs: settings.staleInProgressWarningMs,
107189
107426
  inProgressCriticalMs: settings.staleInProgressCriticalMs,
@@ -107194,7 +107431,7 @@ async function listTasksImpl(store, options) {
107194
107431
  const row = store.pgRowToTaskRow(pgRow);
107195
107432
  const task = store.rowToTask(row);
107196
107433
  const isMergeQueued = mergeQueuedTaskIds.has(task.id);
107197
- const reviewColumnsForRow = await resolveReviewColumnsForTask(store, task.id, listPassIrCache);
107434
+ const reviewColumnsForRow = await resolveReviewColumnsForTask(store, task.id, listPassIrCache, listPassSelectionCache);
107198
107435
  const hasFreshAgentLogActivity = hasFreshAgentLogActivitySinceTaskUpdate(store, task, now, reviewColumnsForRow);
107199
107436
  const executingTaskIds = hasFreshAgentLogActivity ? /* @__PURE__ */ new Set([task.id]) : void 0;
107200
107437
  task.inReviewStall = isMergeQueued ? void 0 : getInReviewStallReason(task, {
@@ -107226,7 +107463,7 @@ async function listTasksImpl(store, options) {
107226
107463
  thresholdMs: settings.stalePausedTodoThresholdMs,
107227
107464
  // Paused-only (the signal is a no-op otherwise), sharing the list-pass
107228
107465
  // IR cache so one workflow is read once per pass, not once per card.
107229
- holdColumn: task.paused === true ? await resolveHoldColumnForTask(store, task.id, listPassIrCache) : void 0,
107466
+ holdColumn: task.paused === true ? await resolveHoldColumnForTask(store, task.id, listPassIrCache, listPassSelectionCache) : void 0,
107230
107467
  engineActiveSinceMs: settings.engineActiveSinceMs,
107231
107468
  engineActivationGraceMs: settings.engineActivationGraceMs
107232
107469
  });
@@ -107236,7 +107473,7 @@ async function listTasksImpl(store, options) {
107236
107473
  thresholds: staleThresholds,
107237
107474
  engineActiveSinceMs: settings.engineActiveSinceMs,
107238
107475
  engineActivationGraceMs: settings.engineActivationGraceMs,
107239
- lifecycle: await resolveTaskLifecycleColumns(store, task.id, listPassIrCache)
107476
+ lifecycle: await resolveTaskLifecycleColumns(store, task.id, listPassIrCache, listPassSelectionCache)
107240
107477
  });
107241
107478
  } catch (err) {
107242
107479
  if (!(err instanceof RangeError)) throw err;
@@ -107315,12 +107552,14 @@ async function listTasksModifiedSinceImpl(store, since, limit2, opts) {
107315
107552
  const lifecycleByTaskId = /* @__PURE__ */ new Map();
107316
107553
  {
107317
107554
  const irCache = /* @__PURE__ */ new Map();
107555
+ const selectionCache = /* @__PURE__ */ new Map();
107556
+ await prefetchWorkflowSelections(store, pageRows.map((row) => row.id), selectionCache);
107318
107557
  for (const pgRow of pageRows) {
107319
107558
  const row = store.pgRowToTaskRow(pgRow);
107320
- reviewColumnsByTaskId.set(row.id, await resolveReviewColumnsForTask(store, row.id, irCache));
107321
- lifecycleByTaskId.set(row.id, await resolveTaskLifecycleColumns(store, row.id, irCache));
107559
+ reviewColumnsByTaskId.set(row.id, await resolveReviewColumnsForTask(store, row.id, irCache, selectionCache));
107560
+ lifecycleByTaskId.set(row.id, await resolveTaskLifecycleColumns(store, row.id, irCache, selectionCache));
107322
107561
  if (store.rowToTask(row).paused !== true) continue;
107323
- holdColumnByTaskId.set(row.id, await resolveHoldColumnForTask(store, row.id, irCache));
107562
+ holdColumnByTaskId.set(row.id, await resolveHoldColumnForTask(store, row.id, irCache, selectionCache));
107324
107563
  }
107325
107564
  }
107326
107565
  const tasks2 = pageRows.map((pgRow) => {
@@ -107438,10 +107677,12 @@ async function searchTasksImpl(store, query, options) {
107438
107677
  const settings = await store.getSettingsFast();
107439
107678
  const mergeQueuedTaskIds = await store.getMergeQueuedTaskIdsAsync();
107440
107679
  const searchPassIrCache = /* @__PURE__ */ new Map();
107680
+ const searchPassSelectionCache = /* @__PURE__ */ new Map();
107681
+ await prefetchWorkflowSelections(store, pgRows.map((row) => String(row.id)), searchPassSelectionCache);
107441
107682
  const tasks2 = await Promise.all(pgRows.map(async (pgRow) => {
107442
107683
  const task = store.rowToTask(store.pgRowToTaskRow(pgRow));
107443
107684
  const isMergeQueued = mergeQueuedTaskIds.has(task.id);
107444
- const reviewColumnsForRow = await resolveReviewColumnsForTask(store, task.id, searchPassIrCache);
107685
+ const reviewColumnsForRow = await resolveReviewColumnsForTask(store, task.id, searchPassIrCache, searchPassSelectionCache);
107445
107686
  const hasFreshAgentLogActivity = hasFreshAgentLogActivitySinceTaskUpdate(store, task, now, reviewColumnsForRow);
107446
107687
  const executingTaskIds = hasFreshAgentLogActivity ? /* @__PURE__ */ new Set([task.id]) : void 0;
107447
107688
  task.inReviewStall = isMergeQueued ? void 0 : getInReviewStallReason(task, {
@@ -108681,6 +108922,7 @@ var init_store = __esm({
108681
108922
  init_async_self_healing();
108682
108923
  init_task_merge();
108683
108924
  init_required_pre_merge_steps();
108925
+ init_pre_merge_approval();
108684
108926
  init_logger();
108685
108927
  init_project_root_guard();
108686
108928
  init_agent_store();
@@ -110446,29 +110688,35 @@ var init_store = __esm({
110446
110688
  const results = task.workflowStepResults ?? [];
110447
110689
  const failedTarget = getLatestFailedPreMergeReviewStep(task);
110448
110690
  const reviewIrForBypass = failedTarget ? void 0 : await resolveWorkflowIrForTask(this, task.id);
110449
- const absentStepId = reviewIrForBypass ? [...resolveRequiredPreMergeStepIds(reviewIrForBypass, task.enabledWorkflowSteps, task)].find((workflowStepId) => !results.some((result2) => result2.workflowStepId === workflowStepId)) : void 0;
110450
- if (!failedTarget && !absentStepId) {
110691
+ const requiredForBypass = reviewIrForBypass ? [...resolveRequiredPreMergeStepIds(reviewIrForBypass, task.enabledWorkflowSteps, task)] : [];
110692
+ const absentStepId = requiredForBypass.find((workflowStepId) => !results.some((result2) => result2.workflowStepId === workflowStepId));
110693
+ const unapprovedTarget = !failedTarget && !absentStepId && requiredForBypass.length > 0 ? (() => {
110694
+ const blocked = evaluatePreMergeApprovals(task, { requiredPreMergeStepIds: new Set(requiredForBypass) }).find((approval) => approval.state !== "approved");
110695
+ return blocked ? results.find((result2) => result2.workflowStepId === blocked.workflowStepId) : void 0;
110696
+ })() : void 0;
110697
+ if (!failedTarget && !absentStepId && !unapprovedTarget) {
110451
110698
  throw new Error(`Cannot bypass review lane for ${id}: no failed pre-merge review step found`);
110452
110699
  }
110453
- const target = failedTarget ?? {
110700
+ const target = failedTarget ?? unapprovedTarget ?? {
110454
110701
  workflowStepId: absentStepId,
110455
110702
  workflowStepName: absentStepId,
110456
110703
  phase: "pre-merge",
110457
110704
  status: "absent"
110458
110705
  };
110459
- const targetIndex = failedTarget ? results.indexOf(failedTarget) : -1;
110460
- if (failedTarget && targetIndex === -1) {
110706
+ const rowTarget = failedTarget ?? unapprovedTarget;
110707
+ const targetIndex = rowTarget ? results.indexOf(rowTarget) : -1;
110708
+ if (rowTarget && targetIndex === -1) {
110461
110709
  throw new Error(`Cannot bypass review lane for ${id}: failed step result not found`);
110462
110710
  }
110463
110711
  const now = (/* @__PURE__ */ new Date()).toISOString();
110464
- const bypassed = failedTarget ? {
110465
- ...failedTarget,
110712
+ const bypassed = rowTarget ? {
110713
+ ...rowTarget,
110466
110714
  status: "skipped",
110467
110715
  bypassedBy: actor,
110468
110716
  bypassedAt: now,
110469
110717
  bypassReason: reason,
110470
- bypassedFromStatus: failedTarget.status,
110471
- bypassedFromVerdict: failedTarget.verdict
110718
+ bypassedFromStatus: rowTarget.remediationArchivedFromStatus ?? rowTarget.status,
110719
+ bypassedFromVerdict: rowTarget.verdict ?? rowTarget.priorAttempts?.[0]?.verdict
110472
110720
  } : {
110473
110721
  workflowStepId: absentStepId,
110474
110722
  workflowStepName: absentStepId,
@@ -110502,8 +110750,8 @@ var init_store = __esm({
110502
110750
  metadata: {
110503
110751
  workflowStepId: target.workflowStepId,
110504
110752
  workflowStepName: target.workflowStepName,
110505
- bypassedFromStatus: target.status,
110506
- bypassedFromVerdict: failedTarget?.verdict ?? null,
110753
+ bypassedFromStatus: bypassed.bypassedFromStatus,
110754
+ bypassedFromVerdict: bypassed.bypassedFromVerdict ?? null,
110507
110755
  reason
110508
110756
  }
110509
110757
  });
@@ -110565,9 +110813,8 @@ var init_store = __esm({
110565
110813
  );
110566
110814
  }
110567
110815
  if (target.status !== "pending") {
110568
- throw new Error(
110569
- `Cannot resume workflow step for ${id}: only pending steps can be resumed`
110570
- );
110816
+ const archivedFailure = target.remediationArchivedAt != null && (target.remediationArchivedFromStatus === "failed" || target.remediationArchivedFromStatus === "advisory_failure") && !target.bypassedBy && !target.supersededAt;
110817
+ 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)` : ""}`);
110571
110818
  }
110572
110819
  const now = (/* @__PURE__ */ new Date()).toISOString();
110573
110820
  const resumed = {
@@ -141075,6 +141322,7 @@ __export(src_exports, {
141075
141322
  CLOUD_TICKET_QUERY: () => CLOUD_TICKET_QUERY,
141076
141323
  CODE_REVIEW_BLOCKING_SEVERITY_SETTING_ID: () => CODE_REVIEW_BLOCKING_SEVERITY_SETTING_ID,
141077
141324
  CODE_REVIEW_MAX_REVISIONS_SETTING_ID: () => CODE_REVIEW_MAX_REVISIONS_SETTING_ID,
141325
+ COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC: () => COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC,
141078
141326
  COLOR_THEMES: () => COLOR_THEMES,
141079
141327
  COLUMNS: () => COLUMNS,
141080
141328
  COLUMN_DESCRIPTIONS: () => COLUMN_DESCRIPTIONS,
@@ -141477,6 +141725,7 @@ __export(src_exports, {
141477
141725
  SLICE_PLAN_STATES: () => SLICE_PLAN_STATES,
141478
141726
  SLICE_STATUSES: () => SLICE_STATUSES,
141479
141727
  SPEC_LOCK_PARSER_VERSION: () => SPEC_LOCK_PARSER_VERSION,
141728
+ STALE_CONTENT_APPROVAL_BLOCKER: () => STALE_CONTENT_APPROVAL_BLOCKER,
141480
141729
  STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS: () => STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS,
141481
141730
  STALLED_REVIEW_INVALID_TRANSITION_PATTERN: () => STALLED_REVIEW_INVALID_TRANSITION_PATTERN,
141482
141731
  STALLED_REVIEW_INVALID_TRANSITION_THRESHOLD: () => STALLED_REVIEW_INVALID_TRANSITION_THRESHOLD,
@@ -142206,6 +142455,7 @@ __export(src_exports, {
142206
142455
  isSqliteLockError: () => isSqliteLockError,
142207
142456
  isSqliteMigrationComplete: () => isSqliteMigrationComplete,
142208
142457
  isStaleBlockedByBlocker: () => isStaleBlockedByBlocker,
142458
+ isStaleContentApprovalBlocker: () => isStaleContentApprovalBlocker,
142209
142459
  isStoredAuthCredential: () => isStoredAuthCredential,
142210
142460
  isStrictDescendantPath: () => isStrictDescendantPath,
142211
142461
  isTailscaleCgnatIpv4: () => isTailscaleCgnatIpv4,
@@ -142285,6 +142535,7 @@ __export(src_exports, {
142285
142535
  mapRoleToCapability: () => mapRoleToCapability,
142286
142536
  markDraftSummarized: () => markDraftSummarized,
142287
142537
  markdownHtmlCommentUnits: () => markdownHtmlCommentUnits,
142538
+ matchStepHeadings: () => matchStepHeadings,
142288
142539
  matchesFrontendUxPath: () => matchesFrontendUxPath,
142289
142540
  matchesPatchnodeQuery: () => matchesPatchnodeQuery,
142290
142541
  materializeMcpServerSecrets: () => materializeMcpServerSecrets,
@@ -142398,6 +142649,7 @@ __export(src_exports, {
142398
142649
  postMergeOptionalGroupNode: () => postMergeOptionalGroupNode,
142399
142650
  postMergeVerificationOptionalGroupNode: () => postMergeVerificationOptionalGroupNode,
142400
142651
  postgresSchema: () => schema_exports,
142652
+ prefetchWorkflowSelections: () => prefetchWorkflowSelections,
142401
142653
  prepareAgentCompaniesImport: () => prepareAgentCompaniesImport,
142402
142654
  pricingAsOf: () => pricingAsOf,
142403
142655
  probeGitCliStatus: () => probeGitCliStatus,
@@ -142509,6 +142761,7 @@ __export(src_exports, {
142509
142761
  resolveBackupSettingsMigrationConflict: () => resolveBackupSettingsMigrationConflict,
142510
142762
  resolveCapacityPoolId: () => resolveCapacityPoolId,
142511
142763
  resolveClaudeCodeClientVersion: () => resolveClaudeCodeClientVersion,
142764
+ resolveCollateralArchivedReviewGate: () => resolveCollateralArchivedReviewGate,
142512
142765
  resolveColumnAdjacency: () => resolveColumnAdjacency,
142513
142766
  resolveColumnAgentBinding: () => resolveColumnAgentBinding,
142514
142767
  resolveColumnCapacity: () => resolveColumnCapacity,
@@ -154389,7 +154642,10 @@ async function runHoldReleaseSweep(store, deps) {
154389
154642
  const settings = await store.getSettings();
154390
154643
  if (expired()) return logPreambleTruncation(0);
154391
154644
  counters.tasks += 1;
154392
- const allTasks = await store.listTasks({ includeArchived: false });
154645
+ const selectionReadTally = { batched: 0, singles: 0 };
154646
+ const allTasks = await store.listTasks({ includeArchived: false, selectionCache, selectionReadTally });
154647
+ counters.batchSelections += selectionReadTally.batched;
154648
+ counters.selections += selectionReadTally.singles;
154393
154649
  if (expired()) return logPreambleTruncation(allTasks.length);
154394
154650
  const effectiveWorkflowIdByTask = /* @__PURE__ */ new Map();
154395
154651
  const missingIds = [...new Set(allTasks.map((task) => task.id))].filter((id) => !selectionCache.has(id));
@@ -180117,7 +180373,7 @@ function extractMarkdownSection(document2, headingName) {
180117
180373
  return document2.slice(start, end).trim();
180118
180374
  }
180119
180375
  function compactTaskPromptStepsSection(section) {
180120
- const stepTitles = Array.from(section.matchAll(/^### Step \d+:.*$/gm), (match2) => match2[0].trim());
180376
+ const stepTitles = matchStepHeadings(section).map(({ index: index3, headingLineEnd }) => section.slice(index3, headingLineEnd).trim());
180121
180377
  if (stepTitles.length === 0) {
180122
180378
  return section.trim();
180123
180379
  }
@@ -189473,7 +189729,7 @@ var init_scheduler = __esm({
189473
189729
  for (const slice of hierarchy.milestones.flatMap((milestone) => milestone.slices).filter((slice2) => slice2.status === "active")) {
189474
189730
  try {
189475
189731
  const superseded = await missionStore.reconcileSupersededGeneratedFixFeatures(slice.id);
189476
- fixed += superseded.supersededCount;
189732
+ fixed += superseded.supersededCount + (superseded.repairedCount ?? 0);
189477
189733
  if (superseded.supersededCount > 0) {
189478
189734
  const refreshed = await missionStore.getSlice(slice.id);
189479
189735
  if (refreshed?.status === "complete") {
@@ -189486,8 +189742,9 @@ var init_scheduler = __esm({
189486
189742
  const supersededFeatureIds = new Set(superseded.featureIds ?? []);
189487
189743
  const autoTriage = mission.autopilotEnabled === true || mission.autoAdvance === true;
189488
189744
  for (const feature of features) {
189489
- if (supersededFeatureIds.has(feature.id) || feature.taskId || !autoTriage || feature.status === "blocked") continue;
189745
+ if (supersededFeatureIds.has(feature.id) || feature.taskId || !autoTriage || feature.status === "blocked" || feature.status === "done") continue;
189490
189746
  if (feature.status !== "defined" && this.isGeneratedFixFeature(feature)) {
189747
+ schedulerLog.warn(`Parking stranded generated Fix feature ${feature.id} from ${feature.status}: not triageable without a defined state`);
189491
189748
  await missionStore.updateFeature(feature.id, { status: "blocked", loopState: "blocked", taskId: void 0 });
189492
189749
  fixed++;
189493
189750
  continue;
@@ -190539,12 +190796,12 @@ function createTaskSearchTool(store) {
190539
190796
  }
190540
190797
  };
190541
190798
  }
190542
- function createPatchnodeReadTool(store) {
190799
+ function createHistoryReadTool(store) {
190543
190800
  return {
190544
- name: "fn_patchnode_read",
190801
+ name: "fn_history_read",
190545
190802
  label: "Read History",
190546
190803
  description: "Read the permanent daily history of completed and reverted task deliveries.",
190547
- parameters: patchnodeReadParams,
190804
+ parameters: historyReadParams,
190548
190805
  execute: async (_id, params) => {
190549
190806
  const limit2 = Math.min(50, Math.max(1, Math.floor(params.limit ?? 20)));
190550
190807
  const result2 = await store.listPatchnodeEntries({
@@ -190804,7 +191061,7 @@ function createTaskDocumentReadTool(store, taskId) {
190804
191061
  };
190805
191062
  }
190806
191063
  function createInstallWorktreeDependenciesTool(store, taskId, options) {
190807
- const errorResult = (message) => ({
191064
+ const errorResult2 = (message) => ({
190808
191065
  content: [{ type: "text", text: `ERROR: ${message}` }],
190809
191066
  details: {},
190810
191067
  isError: true
@@ -190817,33 +191074,33 @@ function createInstallWorktreeDependenciesTool(store, taskId, options) {
190817
191074
  parameters: installWorktreeDependenciesParams,
190818
191075
  execute: async (_id, params) => {
190819
191076
  const task = await store.getTask(taskId);
190820
- if (!task) return errorResult(`Task ${taskId} no longer exists.`);
191077
+ if (!task) return errorResult2(`Task ${taskId} no longer exists.`);
190821
191078
  const workspaceRepos = (await loadWorkspaceConfig(options.rootDir))?.repos ?? [];
190822
191079
  const requestedRepository = params.repository?.trim();
190823
191080
  let worktreePath;
190824
191081
  let repositoryLabel;
190825
191082
  if (workspaceRepos.length > 0) {
190826
191083
  if (workspaceRepos.length > 1 && !requestedRepository) {
190827
- return errorResult("repository is required for a workspace with multiple configured repositories.");
191084
+ return errorResult2("repository is required for a workspace with multiple configured repositories.");
190828
191085
  }
190829
191086
  if (workspaceRepos.length === 1 && requestedRepository && requestedRepository !== workspaceRepos[0]) {
190830
- return errorResult(`Unknown workspace repository: ${requestedRepository}`);
191087
+ return errorResult2(`Unknown workspace repository: ${requestedRepository}`);
190831
191088
  }
190832
191089
  const repository = requestedRepository ?? workspaceRepos[0];
190833
- if (!workspaceRepos.includes(repository)) return errorResult(`Unknown workspace repository: ${repository}`);
191090
+ if (!workspaceRepos.includes(repository)) return errorResult2(`Unknown workspace repository: ${repository}`);
190834
191091
  worktreePath = task.workspaceWorktrees?.[repository]?.worktreePath;
190835
191092
  repositoryLabel = repository;
190836
191093
  } else {
190837
- if (requestedRepository) return errorResult("repository is only valid for a configured workspace task.");
191094
+ if (requestedRepository) return errorResult2("repository is only valid for a configured workspace task.");
190838
191095
  worktreePath = task.worktree;
190839
191096
  }
190840
191097
  if (!worktreePath || !existsSync56(worktreePath)) {
190841
- return errorResult(`No acquired task worktree is available${repositoryLabel ? ` for ${repositoryLabel}` : ""}.`);
191098
+ return errorResult2(`No acquired task worktree is available${repositoryLabel ? ` for ${repositoryLabel}` : ""}.`);
190842
191099
  }
190843
191100
  const settings = await options.getSettings();
190844
191101
  if (params.action === "none") {
190845
191102
  const reason = params.reason?.trim();
190846
- if (!reason) return errorResult("reason is required for action=none.");
191103
+ if (!reason) return errorResult2("reason is required for action=none.");
190847
191104
  const readiness2 = recordPlannerDependencyResolution({
190848
191105
  worktreePath,
190849
191106
  action: "none",
@@ -190862,7 +191119,7 @@ function createInstallWorktreeDependenciesTool(store, taskId, options) {
190862
191119
  };
190863
191120
  }
190864
191121
  const command2 = params.command?.trim();
190865
- if (!command2) return errorResult("command is required for action=install.");
191122
+ if (!command2) return errorResult2("command is required for action=install.");
190866
191123
  let result2;
190867
191124
  try {
190868
191125
  result2 = await options.runConfiguredCommand(
@@ -194132,7 +194389,7 @@ ${lines.join("\n")}`
194132
194389
  }
194133
194390
  };
194134
194391
  }
194135
- 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;
194392
+ 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;
194136
194393
  var init_agent_tools = __esm({
194137
194394
  "../engine/src/agent-tools.ts"() {
194138
194395
  "use strict";
@@ -194223,7 +194480,7 @@ var init_agent_tools = __esm({
194223
194480
  includeArchived: Type2.Optional(Type2.Boolean({ description: "Include archived tasks (default true)" })),
194224
194481
  limit: Type2.Optional(Type2.Number({ minimum: 1, maximum: 50, description: "Max results (default 20, max 50)" }))
194225
194482
  });
194226
- patchnodeReadParams = Type2.Object({
194483
+ historyReadParams = Type2.Object({
194227
194484
  query: Type2.Optional(Type2.String({ description: "Search task IDs, titles, and completion summaries" })),
194228
194485
  from: Type2.Optional(Type2.String({ description: "First UTC day, YYYY-MM-DD (inclusive)" })),
194229
194486
  to: Type2.Optional(Type2.String({ description: "Last UTC day, YYYY-MM-DD (inclusive)" })),
@@ -197433,6 +197690,55 @@ var init_fallback_model_observer = __esm({
197433
197690
  }
197434
197691
  });
197435
197692
 
197693
+ // ../engine/src/execution/resolve-activity-run-agent-id.ts
197694
+ async function resolveWorkflowStepRunAgentId(store, candidate, options = {}) {
197695
+ if (!candidate?.trim()) return null;
197696
+ const resolve120 = async () => {
197697
+ if (typeof store?.getAgent === "function") {
197698
+ const agent = await store.getAgent(candidate);
197699
+ if (agent) return agent.id;
197700
+ }
197701
+ if (typeof store?.listAgents !== "function") return null;
197702
+ const matches = (await store.listAgents({ includeEphemeral: true })).filter(
197703
+ (agent) => agent.role === candidate || agent.roles?.includes(candidate)
197704
+ );
197705
+ const builtInOwners = matches.filter(
197706
+ (agent) => isBuiltinWorkflowRoleAgent(agent) && agent.metadata?.workflowRole === candidate
197707
+ );
197708
+ if (builtInOwners.length === 1) return builtInOwners[0].id;
197709
+ return matches.length === 1 ? matches[0].id : null;
197710
+ };
197711
+ let lookup2;
197712
+ try {
197713
+ lookup2 = Promise.resolve(resolve120());
197714
+ } catch {
197715
+ return null;
197716
+ }
197717
+ void lookup2.catch(() => void 0);
197718
+ return await new Promise((complete) => {
197719
+ const timer2 = setTimeout(() => complete(null), options.timeoutMs ?? WORKFLOW_STEP_RUN_ATTRIBUTION_TIMEOUT_MS);
197720
+ timer2.unref?.();
197721
+ void lookup2.then(
197722
+ (agentId) => {
197723
+ clearTimeout(timer2);
197724
+ complete(agentId);
197725
+ },
197726
+ () => {
197727
+ clearTimeout(timer2);
197728
+ complete(null);
197729
+ }
197730
+ );
197731
+ });
197732
+ }
197733
+ var WORKFLOW_STEP_RUN_ATTRIBUTION_TIMEOUT_MS;
197734
+ var init_resolve_activity_run_agent_id = __esm({
197735
+ "../engine/src/execution/resolve-activity-run-agent-id.ts"() {
197736
+ "use strict";
197737
+ init_src2();
197738
+ WORKFLOW_STEP_RUN_ATTRIBUTION_TIMEOUT_MS = 2e3;
197739
+ }
197740
+ });
197741
+
197436
197742
  // ../engine/src/execution/step-session-executor.ts
197437
197743
  import { exec as exec20 } from "node:child_process";
197438
197744
  import { promisify as promisify27 } from "node:util";
@@ -197441,12 +197747,7 @@ import { rm as rm8 } from "node:fs/promises";
197441
197747
  function parseStepFileScopes(prompt) {
197442
197748
  const result2 = /* @__PURE__ */ new Map();
197443
197749
  if (!prompt) return result2;
197444
- const stepRegex = /^### Step (\d+):.*/gm;
197445
- const splits = [];
197446
- let match2;
197447
- while ((match2 = stepRegex.exec(prompt)) !== null) {
197448
- splits.push({ index: match2.index, stepNum: parseInt(match2[1], 10) });
197449
- }
197750
+ const splits = matchStepHeadings(prompt).map(({ index: index3, headingNumber }) => ({ index: index3, stepNum: headingNumber }));
197450
197751
  if (splits.length === 0) return result2;
197451
197752
  for (let i = 0; i < splits.length; i++) {
197452
197753
  const start = splits[i].index;
@@ -197722,12 +198023,7 @@ function scopePromptToWorktree(prompt, rootDir, worktreePath) {
197722
198023
  return prompt.replaceAll(`${rootDir}/`, `${worktreePath}/`).replaceAll(`${worktreePath}/.fusion/`, `${rootDir}/.fusion/`);
197723
198024
  }
197724
198025
  function extractStepSection(prompt, stepIndex) {
197725
- const stepRegex = /^### Step (\d+):.*/gm;
197726
- const splits = [];
197727
- let match2;
197728
- while ((match2 = stepRegex.exec(prompt)) !== null) {
197729
- splits.push({ index: match2.index, stepNum: parseInt(match2[1], 10) });
197730
- }
198026
+ const splits = matchStepHeadings(prompt).map(({ index: index3, headingNumber }) => ({ index: index3, stepNum: headingNumber }));
197731
198027
  const offset = resolveAuthoredStepHeadingOffset(splits.map((split) => split.stepNum));
197732
198028
  const targetSplit = splits.find((s3) => s3.stepNum === stepIndex + offset);
197733
198029
  if (!targetSplit) return "";
@@ -197737,9 +198033,7 @@ function extractStepSection(prompt, stepIndex) {
197737
198033
  return prompt.slice(start, end).trim();
197738
198034
  }
197739
198035
  function countSteps(prompt) {
197740
- const stepRegex = /^### Step \d+:/gm;
197741
- const matches = prompt.match(stepRegex);
197742
- return matches ? matches.length : 0;
198036
+ return matchStepHeadings(prompt).length;
197743
198037
  }
197744
198038
  function resolveStepSection(taskDetail, prompt, stepIndex, authoredStepCount) {
197745
198039
  const authoredSection = extractStepSection(prompt, stepIndex);
@@ -197834,6 +198128,7 @@ var init_step_session_executor = __esm({
197834
198128
  init_usage_limit_detector();
197835
198129
  init_agent_tools();
197836
198130
  init_worktree_backend();
198131
+ init_resolve_activity_run_agent_id();
197837
198132
  init_worktree_prune();
197838
198133
  init_active_session_registry();
197839
198134
  execAsync18 = promisify27(exec20);
@@ -197866,6 +198161,8 @@ var init_step_session_executor = __esm({
197866
198161
  credentialInstanceId;
197867
198162
  reusablePrimaryRetargetPending = false;
197868
198163
  reusablePrimaryAttemptActive = false;
198164
+ workflowStepRunAgentId;
198165
+ warnedUnattributableWorkflowStepRun = false;
197869
198166
  registerActiveStepSession(stepIndex, handle2, worktreePath) {
197870
198167
  this.activeSessions.set(stepIndex, handle2);
197871
198168
  activeSessionRegistry.registerPath(worktreePath, {
@@ -198210,10 +198507,20 @@ var init_step_session_executor = __esm({
198210
198507
  modelId: current.modelId
198211
198508
  };
198212
198509
  }
198213
- createWorkflowStepActivityRun(stepIndex, startedAt) {
198510
+ async createWorkflowStepActivityRun(stepIndex, startedAt) {
198214
198511
  const { taskDetail } = this.options;
198215
198512
  const step = taskDetail.steps?.[stepIndex];
198216
- const agentId = this.options.effectiveAgentId ?? taskDetail.assignedAgentId ?? "executor";
198513
+ if (typeof this.options.agentStore?.saveRun !== "function") return null;
198514
+ const agentIdCandidate = this.options.effectiveAgentId ?? taskDetail.assignedAgentId ?? "executor";
198515
+ this.workflowStepRunAgentId ??= resolveWorkflowStepRunAgentId(this.options.agentStore, agentIdCandidate);
198516
+ const agentId = await this.workflowStepRunAgentId;
198517
+ if (!agentId) {
198518
+ if (!this.warnedUnattributableWorkflowStepRun) {
198519
+ this.warnedUnattributableWorkflowStepRun = true;
198520
+ stepExecLog.warn(`Skipping unattributable workflow-step activity runs for task ${taskDetail.id} (candidate: ${agentIdCandidate})`);
198521
+ }
198522
+ return null;
198523
+ }
198217
198524
  return {
198218
198525
  id: `${generateSyntheticRunId("workflow-step", taskDetail.id)}-step-${stepIndex}`,
198219
198526
  agentId,
@@ -198233,6 +198540,7 @@ var init_step_session_executor = __esm({
198233
198540
  taskTitle: taskDetail.title,
198234
198541
  assignedAgentId: taskDetail.assignedAgentId,
198235
198542
  effectiveAgentId: this.options.effectiveAgentId,
198543
+ agentIdCandidate,
198236
198544
  agentId,
198237
198545
  stepIndex,
198238
198546
  stepName: step?.name ?? `Step ${stepIndex}`
@@ -198247,6 +198555,7 @@ var init_step_session_executor = __esm({
198247
198555
  };
198248
198556
  }
198249
198557
  async saveWorkflowStepActivityRun(run2) {
198558
+ if (!run2) return;
198250
198559
  const saveRun2 = this.options.agentStore?.saveRun?.bind(this.options.agentStore);
198251
198560
  if (!saveRun2) return;
198252
198561
  try {
@@ -198259,6 +198568,7 @@ var init_step_session_executor = __esm({
198259
198568
  }
198260
198569
  }
198261
198570
  async completeWorkflowStepActivityRun(run2, status, result2) {
198571
+ if (!run2) return;
198262
198572
  const terminalRun = {
198263
198573
  ...run2,
198264
198574
  endedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -198318,7 +198628,7 @@ var init_step_session_executor = __esm({
198318
198628
  if (semaphore) {
198319
198629
  await semaphore.acquire();
198320
198630
  }
198321
- const activityRun = this.createWorkflowStepActivityRun(stepIndex, (/* @__PURE__ */ new Date()).toISOString());
198631
+ const activityRun = await this.createWorkflowStepActivityRun(stepIndex, (/* @__PURE__ */ new Date()).toISOString());
198322
198632
  await this.saveWorkflowStepActivityRun(activityRun);
198323
198633
  const trackingKey = this.makeTrackingKey(stepIndex);
198324
198634
  let retries = 0;
@@ -198867,12 +199177,11 @@ function collectTaskPaths(text4) {
198867
199177
  }
198868
199178
  function collectStepSections(stepsSection) {
198869
199179
  const sections2 = [];
198870
- const headingMatches = Array.from(stepsSection.matchAll(/^### Step (\d+):[^\n]*$/gm));
199180
+ const headingMatches = matchStepHeadings(stepsSection);
198871
199181
  for (let i = 0; i < headingMatches.length; i += 1) {
198872
199182
  const heading = headingMatches[i];
198873
- const start = (heading.index ?? 0) + heading[0].length;
198874
- const end = i + 1 < headingMatches.length ? headingMatches[i + 1].index ?? stepsSection.length : stepsSection.length;
198875
- sections2.push({ name: `Step ${heading[1]}`, body: stepsSection.slice(start, end).trim() });
199183
+ const end = i + 1 < headingMatches.length ? headingMatches[i + 1].index : stepsSection.length;
199184
+ sections2.push({ name: `Step ${heading.headingNumber}`, body: stepsSection.slice(heading.headingLineEnd, end).trim() });
198876
199185
  }
198877
199186
  return sections2;
198878
199187
  }
@@ -198964,6 +199273,7 @@ var TASK_PATH_REGEX;
198964
199273
  var init_task_document_references = __esm({
198965
199274
  "../engine/src/spec-validation/task-document-references.ts"() {
198966
199275
  "use strict";
199276
+ init_src2();
198967
199277
  init_step_session_executor();
198968
199278
  TASK_PATH_REGEX = /\.fusion\/tasks\/[A-Z0-9]+(?:-[A-Z0-9]+)*\/[^\s`'"\]]+/g;
198969
199279
  }
@@ -199990,6 +200300,24 @@ var init_configured_command = __esm({
199990
200300
  }
199991
200301
  });
199992
200302
 
200303
+ // ../engine/src/merge/stale-content-park.ts
200304
+ function classifyStaleContentPark(task) {
200305
+ if (task.paused || task.userPaused || task.deletedAt) return void 0;
200306
+ if (typeof task.error !== "string" || !isStaleContentApprovalBlocker(task.error)) return void 0;
200307
+ if (task.status === "failed") {
200308
+ return task.error.startsWith(AUTO_MERGE_RETRY_REJECTED_PREFIX) ? "retry-rejected" : "raw-blocker";
200309
+ }
200310
+ return task.status == null ? "retry-exhausted" : void 0;
200311
+ }
200312
+ var AUTO_MERGE_RETRY_REJECTED_PREFIX;
200313
+ var init_stale_content_park = __esm({
200314
+ "../engine/src/merge/stale-content-park.ts"() {
200315
+ "use strict";
200316
+ init_src2();
200317
+ AUTO_MERGE_RETRY_REJECTED_PREFIX = "AUTO_MERGE_RETRY_REJECTED:";
200318
+ }
200319
+ });
200320
+
199993
200321
  // ../engine/src/execution/lifecycle-move-log.ts
199994
200322
  function laneRole(column, lanes) {
199995
200323
  if (!lanes) return void 0;
@@ -223770,6 +224098,17 @@ function isRecoveryRetryDue(task, now) {
223770
224098
  const retryAt = Date.parse(task.nextRecoveryAt);
223771
224099
  return !Number.isFinite(retryAt) || retryAt <= now;
223772
224100
  }
224101
+ function formatMergeableReviewRecoverySummary({
224102
+ enqueued,
224103
+ merged,
224104
+ parked
224105
+ }) {
224106
+ const outcomes = [];
224107
+ if (enqueued > 0) outcomes.push(`${enqueued} re-enqueued for merge`);
224108
+ if (merged > 0) outcomes.push(`${merged} merged \u2192 done`);
224109
+ if (parked > 0) outcomes.push(`${parked} parked after enqueue starvation`);
224110
+ return `Mergeable review recovery: ${outcomes.join(", ") || "no tasks recovered"}`;
224111
+ }
223773
224112
  async function resolveNoOpFinalizeGateIds2(store, task) {
223774
224113
  const selection = store.getTaskWorkflowSelectionAsync ? await store.getTaskWorkflowSelectionAsync(task.id) : store.getTaskWorkflowSelection?.(task.id);
223775
224114
  if (!selection) return void 0;
@@ -223816,6 +224155,7 @@ var init_self_healing = __esm({
223816
224155
  "use strict";
223817
224156
  init_src2();
223818
224157
  init_src2();
224158
+ init_stale_content_park();
223819
224159
  init_logger3();
223820
224160
  init_lifecycle_move_log();
223821
224161
  init_lifecycle_move();
@@ -223836,6 +224176,7 @@ var init_self_healing = __esm({
223836
224176
  init_post_landing_worktree_cleanup();
223837
224177
  init_auto_recovery();
223838
224178
  init_active_session_registry();
224179
+ init_planning_liveness();
223839
224180
  init_replan_target();
223840
224181
  init_planning_handoff_recovery();
223841
224182
  init_spec_staleness();
@@ -224002,6 +224343,9 @@ var init_self_healing = __esm({
224002
224343
  maintenanceTickCounter = 0;
224003
224344
  taskLifecycleRetentionLastPrunedAt = /* @__PURE__ */ new Map();
224004
224345
  staleContentRerouteAuditKeys = /* @__PURE__ */ new Set();
224346
+ mergeRecoveryBlockerWarnKeys = /* @__PURE__ */ new Set();
224347
+ staleContentParkRecoveryAttempts = /* @__PURE__ */ new Map();
224348
+ staleContentParkRecoveryBudgetLogged = /* @__PURE__ */ new Set();
224005
224349
  unrunPreMergeGateRerouteAuditKeys = /* @__PURE__ */ new Set();
224006
224350
  githubCheckStateRetentionLastPrunedAt = /* @__PURE__ */ new Map();
224007
224351
  processBootStartedAt = Date.now();
@@ -224720,6 +225064,13 @@ var init_self_healing = __esm({
224720
225064
  { name: "reconcile-orphaned-pending-step-results", fn: () => this.reconcileOrphanedPendingStepResults().then(() => void 0) },
224721
225065
  { name: "reconcile-unproven-review-approvals", fn: () => this.reconcileUnprovenReviewApprovals().then(() => void 0) },
224722
225066
  /*
225067
+ FNXC:PreMergeApproval 2026-09-05-22:11:
225068
+ Runs AFTER the orphaned-step sweep, because that sweep is what produces the `failed` row this
225069
+ defect archives, and BEFORE failed-step recovery, so the restored gate is visible to it in the
225070
+ same startup pass instead of one cycle later.
225071
+ */
225072
+ { name: "reconcile-collateral-archived-review-gates", fn: () => this.reconcileCollateralArchivedReviewGates().then(() => void 0) },
225073
+ /*
224723
225074
  FNXC:WorkspaceContention 2026-08-23-08:00:
224724
225075
  `contention-hold` is owned by an in-memory retry timer. After a process restart that
224725
225076
  timer is gone, so clear only an unowned wait before ordinary dispatch classification;
@@ -225600,6 +225951,7 @@ var init_self_healing = __esm({
225600
225951
  */
225601
225952
  { name: "reconcile-orphaned-pending-step-results", fn: () => this.reconcileOrphanedPendingStepResults() },
225602
225953
  { name: "reconcile-unproven-review-approvals", fn: () => this.reconcileUnprovenReviewApprovals() },
225954
+ { name: "reconcile-collateral-archived-review-gates", fn: () => this.reconcileCollateralArchivedReviewGates() },
225603
225955
  { name: "recover-failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps() },
225604
225956
  { name: "recover-missing-worktree-review-failures", fn: () => this.recoverMissingWorktreeReviewFailures() },
225605
225957
  { name: "recover-interrupted-merging", fn: () => this.recoverInterruptedMergingTasks() },
@@ -229086,29 +229438,28 @@ var init_self_healing = __esm({
229086
229438
  /** Repair legacy dependency residue before it can re-enter executor dispatch. */
229087
229439
  async reconcileMissingDependencies() {
229088
229440
  let repaired = 0;
229089
- const tasks2 = await this.store.listTasks({ slim: true });
229441
+ const archivedColumns = new Set(await Promise.resolve().then(() => resolveProjectColumnsForRoles(this.store, ["archived"])).catch(() => /* @__PURE__ */ new Set()));
229442
+ archivedColumns.add("archived");
229443
+ const tasks2 = await this.store.listTasks({ slim: true, includeArchived: false });
229090
229444
  for (const snapshot of tasks2) {
229445
+ if (snapshot.deletedAt || archivedColumns.has(snapshot.column)) continue;
229091
229446
  if (!snapshot.dependencies.length || snapshot.userPaused || snapshot.paused || snapshot.autoMerge === false) continue;
229092
229447
  const livePaths = activeSessionRegistry.pathsForTask(snapshot.id);
229093
229448
  const liveExecution = livePaths.some((path18) => activeSessionRegistry.isPathActive(path18)) || executingTaskLock.has(snapshot.id) || this.options.getExecutingTaskIds?.().has(snapshot.id) === true || this.options.isTaskActive?.(snapshot.id) === true || Boolean(snapshot.checkedOutBy);
229094
229449
  if (liveExecution) continue;
229095
229450
  let removed = 0;
229096
229451
  for (const dependencyId of snapshot.dependencies) {
229097
- try {
229098
- const dependency = await this.store.getTask(dependencyId, { includeDeleted: true });
229099
- if (!dependency.deletedAt) continue;
229100
- } catch (error3) {
229101
- if (!isTaskNotFoundError2(error3)) throw error3;
229102
- }
229103
- const current = await this.store.getTask(snapshot.id).catch((error3) => {
229104
- if (isTaskNotFoundError2(error3)) return void 0;
229105
- throw error3;
229106
- });
229452
+ if (await readLinkedTaskOrUndefined(this.store, dependencyId)) continue;
229453
+ const current = await readLinkedTaskOrUndefined(this.store, snapshot.id);
229107
229454
  if (!current || current.userPaused || current.paused || current.autoMerge === false || current.checkedOutBy || activeSessionRegistry.pathsForTask(current.id).some((path18) => activeSessionRegistry.isPathActive(path18)) || executingTaskLock.has(current.id) || this.options.getExecutingTaskIds?.().has(current.id) === true || this.options.isTaskActive?.(current.id) === true || !current.dependencies.includes(dependencyId)) continue;
229108
229455
  try {
229109
229456
  await this.store.updateTaskDependencies(current.id, { operation: "remove", dependency: dependencyId });
229110
229457
  removed += 1;
229111
229458
  } catch (error3) {
229459
+ if (isMissingTaskLookupError(error3)) {
229460
+ log34.debug(`reconcileMissingDependencies: skipped unreadable ${current.id} dependency ${dependencyId}`);
229461
+ continue;
229462
+ }
229112
229463
  if (!/does not depend on/i.test(error3 instanceof Error ? error3.message : String(error3))) throw error3;
229113
229464
  }
229114
229465
  }
@@ -229874,7 +230225,7 @@ var init_self_healing = __esm({
229874
230225
  const graceMs = 10 * 6e4;
229875
230226
  const now = Date.now();
229876
230227
  const nowIso3 = new Date(now).toISOString();
229877
- const live = (taskId) => activeSessionRegistry.pathsForTask(taskId).some((path18) => activeSessionRegistry.isPathActive(path18)) || executingTaskLock.has(taskId) || this.options.isTaskActive?.(taskId) === true;
230228
+ const live = (taskId) => activeSessionRegistry.pathsForTask(taskId).some((path18) => activeSessionRegistry.isPathActive(path18)) || executingTaskLock.has(taskId) || isPlanningLive(taskId) || this.options.isTaskActive?.(taskId) === true;
229878
230229
  const due = await this.store.listDueWorkflowWorkItems({
229879
230230
  now: nowIso3,
229880
230231
  states: [...ACTIVE_WORKFLOW_WORK_ITEM_STATES]
@@ -230251,6 +230602,109 @@ var init_self_healing = __esm({
230251
230602
  return 0;
230252
230603
  }
230253
230604
  }
230605
+ /**
230606
+ * FNXC:PreMergeApproval 2026-09-05-22:11:
230607
+ * FN-295: restore a required pre-merge gate that was archived as COLLATERAL of another gate's
230608
+ * remediation, so the card stops being permanently unmergeable with no owner.
230609
+ *
230610
+ * The wedge needs four ingredients, none of which is individually wrong: a review row left
230611
+ * `pending` by a dead session, the orphaned-step sweep rewriting it to `failed`, the remediation
230612
+ * archiver stamping EVERY terminal failure (not just its own gate), and the merge door treating
230613
+ * `remediationArchivedAt` as an unconditional veto. The result is a row the reseed cannot re-run
230614
+ * (it handles `missing`), the stale-content reroute cannot re-run (it handles `stale-content`),
230615
+ * and the operator bypass cannot select (it selects `status:"failed"`). Measured on FN-295: three
230616
+ * review restarts re-ran Code Review and Documentation to APPROVE and merged nothing.
230617
+ *
230618
+ * This sweep does NOT approve or merge anything: it undoes the collateral archive so the gate is
230619
+ * once again a visible failed step that the ordinary FN-7720 audited bypass — or a genuine re-run
230620
+ * — can clear. It never touches an operator waiver, the gate that owns the remediation wave, a
230621
+ * workspace card, a user-paused card, or a card with a live session, and it re-validates every
230622
+ * condition inside the atomic mutation.
230623
+ */
230624
+ async reconcileCollateralArchivedReviewGates() {
230625
+ try {
230626
+ const reviewColumns = await resolveProjectColumnsForRoles(this.store, REVIEW_ROLES);
230627
+ const candidates = /* @__PURE__ */ new Map();
230628
+ for (const column of reviewColumns) {
230629
+ for (const task of await this.store.listTasks({ column, slim: true })) candidates.set(task.id, task);
230630
+ }
230631
+ let repairedTasks = 0;
230632
+ const isSessionLive = (taskId) => {
230633
+ const livePaths = activeSessionRegistry.pathsForTask(taskId);
230634
+ return livePaths.some((path18) => activeSessionRegistry.isPathActive(path18)) || executingTaskLock.has(taskId) || this.options.isTaskActive?.(taskId) === true;
230635
+ };
230636
+ const remediationGateIdsFor = (task) => new Set(
230637
+ (task.steps ?? []).map((step) => step.remediation?.gateStepId).filter((gateStepId) => typeof gateStepId === "string" && gateStepId.length > 0)
230638
+ );
230639
+ for (const candidate of candidates.values()) {
230640
+ if (candidate.userPaused === true || candidate.workspaceWorktrees !== void 0 || isSessionLive(candidate.id)) continue;
230641
+ const fresh = await this.store.getTask(candidate.id);
230642
+ if (!fresh || !reviewColumns.has(fresh.column) || fresh.userPaused === true || fresh.workspaceWorktrees !== void 0 || isSessionLive(fresh.id)) continue;
230643
+ if (!(fresh.workflowStepResults ?? []).some((result2) => resolveCollateralArchivedReviewGate(result2, { remediationGateIds: remediationGateIdsFor(fresh) }))) continue;
230644
+ let requiredPreMergeStepIds;
230645
+ try {
230646
+ requiredPreMergeStepIds = (await resolvePreMergeGateForTask(
230647
+ this.store,
230648
+ fresh.id,
230649
+ fresh.enabledWorkflowSteps,
230650
+ fresh
230651
+ )).requiredPreMergeStepIds;
230652
+ } catch {
230653
+ continue;
230654
+ }
230655
+ if (requiredPreMergeStepIds.size === 0) continue;
230656
+ let repair;
230657
+ try {
230658
+ await this.store.updateTaskAtomic(fresh.id, (live) => {
230659
+ repair = void 0;
230660
+ if (!reviewColumns.has(live.column) || live.userPaused === true || live.workspaceWorktrees !== void 0 || isSessionLive(live.id)) return null;
230661
+ const remediationGateIds = remediationGateIdsFor(live);
230662
+ const restoredStepIds = [];
230663
+ const results = (live.workflowStepResults ?? []).map((result2) => {
230664
+ if (!requiredPreMergeStepIds.has(result2.workflowStepId)) return result2;
230665
+ const resolution = resolveCollateralArchivedReviewGate(result2, { remediationGateIds });
230666
+ if (!resolution) return result2;
230667
+ restoredStepIds.push(result2.workflowStepId);
230668
+ return resolution.restored;
230669
+ });
230670
+ if (restoredStepIds.length === 0) return null;
230671
+ repair = { stepIds: restoredStepIds, column: live.column, resultCount: results.length };
230672
+ return { workflowStepResults: results };
230673
+ });
230674
+ if (!repair) continue;
230675
+ repairedTasks += 1;
230676
+ } catch (error3) {
230677
+ log34.warn(`reconcileCollateralArchivedReviewGates: failed for ${fresh.id}: ${error3 instanceof Error ? error3.message : String(error3)}`);
230678
+ continue;
230679
+ }
230680
+ await this.store.logEntry(
230681
+ fresh.id,
230682
+ `[pre-merge] Restored collaterally archived review gate(s): ${repair.stepIds.join(", ")}`,
230683
+ COLLATERAL_ARCHIVED_REVIEW_GATE_DIAGNOSTIC
230684
+ ).catch(() => void 0);
230685
+ await emitBoundedRunAudit2(this.store, {
230686
+ taskId: fresh.id,
230687
+ agentId: "self-healing",
230688
+ runId: generateSyntheticRunId("reconcile-collateral-archived-review-gate", fresh.id),
230689
+ domain: "database",
230690
+ mutationType: "task:reconcile-collateral-archived-review-gate",
230691
+ target: fresh.id,
230692
+ metadata: {
230693
+ taskId: fresh.id,
230694
+ column: repair.column,
230695
+ workflowStepId: repair.stepIds[0],
230696
+ restoredCount: repair.stepIds.length,
230697
+ resultCount: repair.resultCount
230698
+ }
230699
+ }, { log: log34 });
230700
+ }
230701
+ if (repairedTasks > 0) log34.log(`Restored collaterally archived review gates on ${repairedTasks} task(s)`);
230702
+ return repairedTasks;
230703
+ } catch (error3) {
230704
+ log34.error(`reconcileCollateralArchivedReviewGates failed: ${error3 instanceof Error ? error3.message : String(error3)}`);
230705
+ return 0;
230706
+ }
230707
+ }
230254
230708
  /**
230255
230709
  * Backward lifecycle move gated on triple proof (FN-5335).
230256
230710
  * When the unproven fallback predicate fails, emits `task:finalize-no-op-review-no-action` and skips lifecycle mutation.
@@ -230610,25 +231064,14 @@ var init_self_healing = __esm({
230610
231064
  blocker check lacks a fresh Git descriptor. Capture the same merge content the merge queue uses and
230611
231065
  seed the graph back at Code Review, so cards like FN-9234 stop looping through doomed merge retries.
230612
231066
  */
230613
- async routeStaleSingularApprovalBackToReview(task, reviewColumns, settings) {
230614
- let mergeGate;
230615
- try {
230616
- mergeGate = await resolvePreMergeGateForTask(this.store, task.id, task.enabledWorkflowSteps, task);
230617
- } catch {
230618
- return false;
230619
- }
230620
- if (mergeGate.provenance === "default" && !mergeGate.selectionAbsent) return false;
230621
- const mergeContent = await captureMergeContentDescriptor(task, {
230622
- workspaceRootDir: this.options.rootDir,
230623
- settings
230624
- });
231067
+ async routeStaleSingularApprovalBackToReview(task, reviewColumns, mergeGate, mergeContent, parkShape) {
230625
231068
  const blocker = getTaskMergeBlocker(task, {
230626
231069
  reviewColumns,
230627
231070
  requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds,
230628
231071
  mergeContent
230629
231072
  });
230630
- if (blocker !== "task has a pre-merge approval recorded against different content" || mergeContent.kind !== "singular") {
230631
- return false;
231073
+ if (!isStaleContentApprovalBlocker(blocker) || mergeContent.kind !== "singular") {
231074
+ return { handled: false, seeded: false, parkCleared: false, mergeRetriesReset: false };
230632
231075
  }
230633
231076
  const reroute = await rerouteSingularStaleContentToReview(this.store, task, {
230634
231077
  requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds,
@@ -230639,11 +231082,25 @@ var init_self_healing = __esm({
230639
231082
  nodeId: void 0,
230640
231083
  workflowStepId: void 0
230641
231084
  }));
231085
+ let parkCleared = false;
231086
+ if (reroute.rerouted && parkShape) {
231087
+ await this.store.updateTaskAtomic(task.id, (live) => {
231088
+ if (classifyStaleContentPark(live) !== parkShape || live.paused || live.userPaused || live.deletedAt) {
231089
+ return null;
231090
+ }
231091
+ parkCleared = true;
231092
+ return { status: null, error: null, mergeRetries: 0 };
231093
+ });
231094
+ }
231095
+ const mergeRetriesReset = parkCleared;
230642
231096
  if (reroute.rerouted) {
230643
- await this.store.logEntry(task.id, "[pre-merge] Self-healing re-seeded Code Review after stale content evidence blocked merge.");
231097
+ 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.`);
231098
+ if (parkCleared) {
231099
+ 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.`);
231100
+ }
230644
231101
  log34.warn(`Stale content approval for ${task.id} re-seeded at ${reroute.nodeId ?? "code-review"}`);
230645
231102
  }
230646
- const auditKey = `${task.id}:${reroute.reason}:${reroute.nodeId ?? ""}`;
231103
+ const auditKey = `${task.id}:${reroute.reason}:${reroute.nodeId ?? ""}:${parkShape ?? ""}`;
230647
231104
  if (!this.staleContentRerouteAuditKeys.has(auditKey)) {
230648
231105
  this.staleContentRerouteAuditKeys.add(auditKey);
230649
231106
  await emitBoundedRunAudit2(this.store, {
@@ -230658,21 +231115,23 @@ var init_self_healing = __esm({
230658
231115
  nodeId: reroute.nodeId,
230659
231116
  workflowStepId: reroute.workflowStepId,
230660
231117
  reason: reroute.reason,
230661
- source: "self-healing"
231118
+ source: "self-healing",
231119
+ parkShape,
231120
+ parkCleared,
231121
+ mergeRetriesReset
230662
231122
  }
230663
231123
  });
230664
231124
  }
230665
- return true;
231125
+ return {
231126
+ handled: true,
231127
+ seeded: reroute.rerouted,
231128
+ nodeId: reroute.nodeId,
231129
+ parkShape,
231130
+ parkCleared,
231131
+ mergeRetriesReset
231132
+ };
230666
231133
  }
230667
- async routeUnrunPreMergeGateBackToReview(task, reviewColumns, settings) {
230668
- let mergeGate;
230669
- try {
230670
- mergeGate = await resolvePreMergeGateForTask(this.store, task.id, task.enabledWorkflowSteps, task);
230671
- } catch {
230672
- return false;
230673
- }
230674
- if (mergeGate.provenance === "default" && !mergeGate.selectionAbsent) return false;
230675
- const mergeContent = await captureMergeContentDescriptor(task, { workspaceRootDir: this.options.rootDir, settings });
231134
+ async routeUnrunPreMergeGateBackToReview(task, reviewColumns, mergeGate, mergeContent) {
230676
231135
  if (getTaskMergeBlocker(task, { reviewColumns, requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds, mergeContent }) !== "task has enabled pre-merge workflow steps that never ran") return false;
230677
231136
  const reroute = await rerouteUnrunPreMergeGateToReview(this.store, task, { requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds, mergeContent }).catch(() => ({ rerouted: false, reason: "no-unrun-gate", nodeId: void 0, workflowStepId: void 0 }));
230678
231137
  if (reroute.rerouted) {
@@ -230747,11 +231206,62 @@ var init_self_healing = __esm({
230747
231206
  for (const task of mergeable) {
230748
231207
  const reviewColumns = await ownReviewLanesFor(task);
230749
231208
  if (!reviewColumns.has(task.column)) continue;
230750
- if (await this.routeStaleSingularApprovalBackToReview(task, reviewColumns, settings)) continue;
230751
- if (await this.routeUnrunPreMergeGateBackToReview(task, reviewColumns, settings)) continue;
230752
- if (getTaskMergeBlocker(task, { reviewColumns }) !== void 0) continue;
231209
+ let mergeGate;
231210
+ try {
231211
+ mergeGate = await resolvePreMergeGateForTask(this.store, task.id, task.enabledWorkflowSteps, task);
231212
+ } catch {
231213
+ continue;
231214
+ }
231215
+ if (mergeGate.provenance === "default" && !mergeGate.selectionAbsent) continue;
231216
+ const mergeContent = await captureMergeContentDescriptor(task, { workspaceRootDir: this.options.rootDir, settings });
231217
+ if ((await this.routeStaleSingularApprovalBackToReview(task, reviewColumns, mergeGate, mergeContent)).handled) continue;
231218
+ if (await this.routeUnrunPreMergeGateBackToReview(task, reviewColumns, mergeGate, mergeContent)) continue;
231219
+ const blocker = getTaskMergeBlocker(task, {
231220
+ reviewColumns,
231221
+ requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds,
231222
+ mergeContent
231223
+ });
231224
+ if (blocker !== void 0) {
231225
+ const warningKey = `${task.id}:${blocker}`;
231226
+ if (!this.mergeRecoveryBlockerWarnKeys.has(warningKey)) {
231227
+ this.mergeRecoveryBlockerWarnKeys.add(warningKey);
231228
+ log34.warn(`mergeable-review recovery declined ${task.id}: ${blocker}`);
231229
+ }
231230
+ continue;
231231
+ }
230753
231232
  laneQualifiedMergeable.push(task);
230754
231233
  }
231234
+ const hiddenStaleContentCandidateIds = /* @__PURE__ */ new Set();
231235
+ for (const task of tasks2) {
231236
+ if (mergeable.includes(task)) continue;
231237
+ const parkShape = classifyStaleContentPark(task);
231238
+ if (!parkShape || mergeAdmissionByTaskId.get(task.id) !== true || executingIds.has(task.id) || task.status === "merging" || task.status === "merging-pr") continue;
231239
+ const reviewColumns = await ownReviewLanesFor(task);
231240
+ if (!reviewColumns.has(task.column)) continue;
231241
+ hiddenStaleContentCandidateIds.add(task.id);
231242
+ const attempts = (this.staleContentParkRecoveryAttempts.get(task.id) ?? 0) + 1;
231243
+ this.staleContentParkRecoveryAttempts.set(task.id, attempts);
231244
+ if (attempts === MAX_STARVATION_DROPS && !this.staleContentParkRecoveryBudgetLogged.has(task.id)) {
231245
+ this.staleContentParkRecoveryBudgetLogged.add(task.id);
231246
+ await this.store.logEntry(task.id, `[pre-merge] Stopped stale-content park recovery after ${MAX_STARVATION_DROPS} attempts; operator attention is required.`);
231247
+ }
231248
+ if (attempts > MAX_STARVATION_DROPS) continue;
231249
+ let mergeGate;
231250
+ try {
231251
+ mergeGate = await resolvePreMergeGateForTask(this.store, task.id, task.enabledWorkflowSteps, task);
231252
+ } catch {
231253
+ continue;
231254
+ }
231255
+ if (mergeGate.provenance === "default" && !mergeGate.selectionAbsent) continue;
231256
+ const mergeContent = await captureMergeContentDescriptor(task, { workspaceRootDir: this.options.rootDir, settings });
231257
+ await this.routeStaleSingularApprovalBackToReview(task, reviewColumns, mergeGate, mergeContent, parkShape);
231258
+ }
231259
+ for (const taskId of this.staleContentParkRecoveryAttempts.keys()) {
231260
+ if (!hiddenStaleContentCandidateIds.has(taskId)) {
231261
+ this.staleContentParkRecoveryAttempts.delete(taskId);
231262
+ this.staleContentParkRecoveryBudgetLogged.delete(taskId);
231263
+ }
231264
+ }
230755
231265
  if (unresolvedMergeableCards.length > 0) {
230756
231266
  log34.warn(
230757
231267
  `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.`
@@ -230774,6 +231284,9 @@ var init_self_healing = __esm({
230774
231284
  );
230775
231285
  const enqueueMerge = this.options.enqueueMerge;
230776
231286
  let recovered = 0;
231287
+ let enqueued = 0;
231288
+ let merged = 0;
231289
+ let parked = 0;
230777
231290
  for (const task of unownedMergeable) {
230778
231291
  try {
230779
231292
  if (enqueueMerge) {
@@ -230789,19 +231302,22 @@ var init_self_healing = __esm({
230789
231302
  await this.store.updateTask(task.id, { status: "failed", error: error3 });
230790
231303
  await this.store.logEntry(task.id, error3);
230791
231304
  this.mergeStarvationDrops.delete(task.id);
231305
+ parked++;
230792
231306
  recovered++;
230793
231307
  }
230794
231308
  continue;
230795
231309
  }
230796
231310
  this.mergeStarvationDrops.delete(task.id);
231311
+ enqueued++;
230797
231312
  } else {
230798
231313
  await this.store.mergeTask(task.id);
231314
+ merged++;
230799
231315
  }
230800
231316
  await this.store.logEntry(
230801
231317
  task.id,
230802
231318
  enqueueMerge ? "Auto-recovered: eligible in-review task re-enqueued for merge" : "Auto-recovered: eligible in-review task was merged and moved to done"
230803
231319
  );
230804
- log34.log(`Recovered mergeable review task ${task.id}`);
231320
+ log34.log(enqueueMerge ? `Re-enqueued mergeable review task ${task.id} for merge` : `Merged review task ${task.id} and moved it to done`);
230805
231321
  recovered++;
230806
231322
  } catch (err) {
230807
231323
  const errorMessage6 = err instanceof Error ? err.message : String(err);
@@ -230809,7 +231325,7 @@ var init_self_healing = __esm({
230809
231325
  }
230810
231326
  }
230811
231327
  if (recovered > 0) {
230812
- log34.log(`Recovered ${recovered} mergeable review task(s) \u2192 done`);
231328
+ log34.log(formatMergeableReviewRecoverySummary({ enqueued, merged, parked }));
230813
231329
  }
230814
231330
  return recovered;
230815
231331
  } catch (err) {
@@ -238801,7 +239317,7 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
238801
239317
  planLog.warn(`${task.id} ${retryMessage}`);
238802
239318
  await this.store.logEntry(task.id, retryMessage);
238803
239319
  await this.updatePlanningStateIfStillCurrent(task, {
238804
- status: this.restoreStatusAfterInterruptedTriageWork(task),
239320
+ status: await this.resolvePlanningRetryHoldStatus(task, written),
238805
239321
  error: null,
238806
239322
  recoveryRetryCount: decision.nextState.recoveryRetryCount,
238807
239323
  nextRecoveryAt: decision.nextState.nextRecoveryAt
@@ -238838,9 +239354,13 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
238838
239354
  const retryMessage = `Generated plan failed deterministic validation (${deterministicSpecFailure}) \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}.`;
238839
239355
  planLog.warn(`${task.id} ${retryMessage}`);
238840
239356
  await this.store.logEntry(task.id, retryMessage);
238841
- const restoreStatus = this.restoreStatusAfterInterruptedTriageWork(task);
239357
+ await this.store.logEntry(
239358
+ task.id,
239359
+ "AI spec revision requested",
239360
+ `Deterministic plan validation rejected the generated plan: ${deterministicSpecFailure}`
239361
+ ).catch(() => void 0);
238842
239362
  await this.updatePlanningStateIfStillCurrent(task, {
238843
- status: restoreStatus,
239363
+ status: await this.resolvePlanningRetryHoldStatus(task, written),
238844
239364
  error: null,
238845
239365
  recoveryRetryCount: decision.nextState.recoveryRetryCount,
238846
239366
  nextRecoveryAt: decision.nextState.nextRecoveryAt
@@ -239010,9 +239530,10 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
239010
239530
  const retryMessage = `${failureMessage} \u2014 retry ${decision.nextState.recoveryRetryCount}/${MAX_RECOVERY_RETRIES} in ${formatDelay(decision.delayMs)}.`;
239011
239531
  planLog.warn(`${task.id} ${retryMessage}`);
239012
239532
  await this.store.logEntry(task.id, retryMessage).catch(() => void 0);
239533
+ const retryHoldStatus = await this.resolvePlanningRetryHoldStatus(task);
239013
239534
  await this.updatePlanningStateIfStillCurrent(task, (live) => ({
239014
239535
  ...persistMarker(live),
239015
- status: this.restoreStatusAfterInterruptedTriageWork(task),
239536
+ status: retryHoldStatus,
239016
239537
  error: null,
239017
239538
  recoveryRetryCount: decision.nextState.recoveryRetryCount,
239018
239539
  nextRecoveryAt: decision.nextState.nextRecoveryAt
@@ -239042,7 +239563,7 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
239042
239563
  planLog.warn(`${task.id}: failed to log transient-error retry entry: ${msg}`);
239043
239564
  });
239044
239565
  }
239045
- const restoreStatus = this.restoreStatusAfterInterruptedTriageWork(task);
239566
+ const restoreStatus = await this.resolvePlanningRetryHoldStatus(task);
239046
239567
  await this.updatePlanningStateIfStillCurrent(task, {
239047
239568
  status: restoreStatus,
239048
239569
  recoveryRetryCount: decision.nextState.recoveryRetryCount,
@@ -239094,7 +239615,7 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
239094
239615
  const msg = logErr instanceof Error ? logErr.message : String(logErr);
239095
239616
  planLog.warn(`${task.id}: failed to log planning-failure retry entry: ${msg}`);
239096
239617
  });
239097
- const restoreStatus = this.restoreStatusAfterInterruptedTriageWork(task);
239618
+ const restoreStatus = await this.resolvePlanningRetryHoldStatus(task);
239098
239619
  await this.updatePlanningStateIfStillCurrent(task, {
239099
239620
  status: restoreStatus,
239100
239621
  recoveryRetryCount: genericDecision.nextState.recoveryRetryCount,
@@ -239378,11 +239899,22 @@ ${lines.join("\n")}` }],
239378
239899
  }
239379
239900
  return null;
239380
239901
  }
239902
+ /*
239903
+ FNXC:TriagePlanningRetry 2026-09-05-22:06:
239904
+ A retry over a finished-looking PROMPT.md must hold `needs-replan`, matching scheduler validation
239905
+ retries. Clearing status makes the card dispatchable and bypasses finalizeApprovedTask approval.
239906
+ */
239907
+ async resolvePlanningRetryHoldStatus(task, knownPromptContent) {
239908
+ const restored = this.restoreStatusAfterInterruptedTriageWork(task);
239909
+ if (restored) return restored;
239910
+ const content = knownPromptContent ?? await this.readNonEmptyPromptDraft(task.id, "planning retry hold");
239911
+ return content && !isTaskAwaitingPlanning(task, content) ? "needs-replan" : null;
239912
+ }
239381
239913
  async validateGeneratedPrompt(taskId, promptContent) {
239382
239914
  if (!promptContent.trim()) {
239383
239915
  return "PROMPT.md file not found or empty";
239384
239916
  }
239385
- const headingNumbers = Array.from(promptContent.matchAll(/^### Step (\d+):/gm), (match2) => Number(match2[1]));
239917
+ const headingNumbers = matchStepHeadings(promptContent).map((match2) => match2.headingNumber);
239386
239918
  if (headingNumbers.length > 0 && !headingNumbers.every((heading, index3) => heading === index3)) {
239387
239919
  const diagnostic = `Step headings must be contiguous 0-based execution indices (observed: ${headingNumbers.join(", ")}). Renumber from Step 0 and update prose cross-references.`;
239388
239920
  planLog.warn(`${taskId}: ${diagnostic}`);
@@ -239992,6 +240524,101 @@ ${lines.join("\n")}` }],
239992
240524
  }
239993
240525
  });
239994
240526
 
240527
+ // ../engine/src/executor/optional-step-revision.ts
240528
+ function normalizeOptionalStepRevisionKey2(value2) {
240529
+ return (value2 ?? "").trim().toLowerCase();
240530
+ }
240531
+ function optionalStepRevisionKey2(nodeId, stepName) {
240532
+ return normalizeOptionalStepRevisionKey2(nodeId) || normalizeOptionalStepRevisionKey2(stepName) || "pre-merge-optional-step";
240533
+ }
240534
+ function optionalStepRevisionResetOutcome(key) {
240535
+ return `${OPTIONAL_STEP_REVISION_RESET_MARKER} ${key}`;
240536
+ }
240537
+ function resetIndexFor(log44, normalizedKey) {
240538
+ for (let index3 = log44.length - 1; index3 >= 0; index3 -= 1) {
240539
+ const outcome = log44[index3]?.outcome ?? "";
240540
+ const markerIndex = outcome.indexOf(OPTIONAL_STEP_REVISION_RESET_MARKER);
240541
+ if (markerIndex < 0) continue;
240542
+ const markerValue = outcome.slice(markerIndex + OPTIONAL_STEP_REVISION_RESET_MARKER.length).split(/\r?\n/, 1)[0]?.trim();
240543
+ if (normalizeOptionalStepRevisionKey2(markerValue) === normalizedKey) return index3;
240544
+ }
240545
+ return -1;
240546
+ }
240547
+ function countOptionalStepRevisionAttempts2(task, key, stepName) {
240548
+ const normalizedKey = normalizeOptionalStepRevisionKey2(key);
240549
+ const normalizedStepName = normalizeOptionalStepRevisionKey2(stepName);
240550
+ const log44 = task.log ?? [];
240551
+ const since = resetIndexFor(log44, normalizedKey);
240552
+ return (since >= 0 ? log44.slice(since + 1) : log44).filter((entry) => {
240553
+ const action = entry.action ?? "";
240554
+ const outcome = entry.outcome ?? "";
240555
+ if (!/attempt \d+\//.test(action)) return false;
240556
+ const markerIndex = outcome.indexOf(OPTIONAL_STEP_REVISION_KEY_MARKER2);
240557
+ if (markerIndex >= 0) {
240558
+ const markerValue = outcome.slice(markerIndex + OPTIONAL_STEP_REVISION_KEY_MARKER2.length).split(/\r?\n/, 1)[0]?.trim();
240559
+ return normalizeOptionalStepRevisionKey2(markerValue) === normalizedKey;
240560
+ }
240561
+ if (!normalizedStepName) return false;
240562
+ return normalizeOptionalStepRevisionKey2(outcome).includes(`step: ${normalizedStepName}`);
240563
+ }).length;
240564
+ }
240565
+ function optionalStepRevisionLogOutcome2(details, key) {
240566
+ return `${details}
240567
+ ${OPTIONAL_STEP_REVISION_KEY_MARKER2} ${key}`;
240568
+ }
240569
+ function buildReviewConvergenceContext(task, options) {
240570
+ const revisionKey = options.revisionKey;
240571
+ const priorAttemptCount = countPlanReviewRevisionAttempts(task.workflowStepResults, { revisionKey });
240572
+ const attempt = priorAttemptCount + 1;
240573
+ if (attempt <= 1) return "";
240574
+ const history = collectPlanReviewFeedbackHistory(task.workflowStepResults, { revisionKey });
240575
+ const lines = [
240576
+ `## Convergence \u2014 ${options.reviewKind === "plan" ? "Plan Review" : "Code Review"} attempt ${attempt}`
240577
+ ];
240578
+ if (options.changeSummaryBlock?.trim()) lines.push("", options.changeSummaryBlock.trim());
240579
+ lines.push(
240580
+ "Treat the cumulative prior feedback below as a decision primer. Verify each prior blocker against the current PROMPT.md before looking for new findings.",
240581
+ "- Do not re-raise a resolved or semantically duplicate blocker.",
240582
+ "- 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."
240583
+ );
240584
+ if (attempt >= 3) {
240585
+ lines.push(
240586
+ "- Severity ratchet (attempt 3+): only delivery-blocking critical defects may return REVISE; important/minor wording or implementation-detail findings are advisory."
240587
+ );
240588
+ }
240589
+ if (history.length > 0) {
240590
+ lines.push("", "### Cumulative prior Plan Review ledger");
240591
+ history.forEach((feedback, index3) => lines.push(`#### PR${index3 + 1}`, feedback));
240592
+ }
240593
+ const own2 = task.workflowStepResults?.find((result2) => result2.workflowStepId === revisionKey);
240594
+ const findings = (own2?.priorAttempts ?? []).flatMap((attempt2) => attempt2.findings ?? []).filter(isOpenWorkflowReviewFinding);
240595
+ if (findings.length > 0) {
240596
+ lines.push("", "### Your prior findings on this gate");
240597
+ findings.forEach((finding) => lines.push(`- ${finding.id} \u2014 [${finding.severity ?? "unclassified"}] ${finding.title}: ${finding.body}${finding.filePath ? ` (${finding.filePath}${finding.line ? `:${finding.line}` : ""})` : ""}`));
240598
+ lines.push("Match prior defects by file path, line region, and described behavior; IDs and titles may change between rounds.");
240599
+ }
240600
+ const disputed = findings.filter((finding) => finding.disputedAt);
240601
+ if (disputed.length > 0) {
240602
+ lines.push("", "### Disputed by the implementer");
240603
+ disputed.forEach((finding) => lines.push(`- ${finding.id}: ${finding.disputeRationale ?? "No rationale recorded."}`));
240604
+ 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.");
240605
+ }
240606
+ return lines.join("\n");
240607
+ }
240608
+ function buildGraphPlanReviewConvergenceContext(task, revisionKey) {
240609
+ return buildReviewConvergenceContext(task, { revisionKey, reviewKind: "plan" });
240610
+ }
240611
+ var OPTIONAL_STEP_REVISION_KEY_MARKER2, OPTIONAL_STEP_REVISION_RESET_MARKER;
240612
+ var init_optional_step_revision = __esm({
240613
+ "../engine/src/executor/optional-step-revision.ts"() {
240614
+ "use strict";
240615
+ init_src2();
240616
+ init_plan_review_feedback_history();
240617
+ OPTIONAL_STEP_REVISION_KEY_MARKER2 = "Workflow revision key:";
240618
+ OPTIONAL_STEP_REVISION_RESET_MARKER = "Workflow revision ledger reset:";
240619
+ }
240620
+ });
240621
+
239995
240622
  // ../engine/src/executor/workflow-gate-activity.ts
239996
240623
  function resolveWorkflowGateActivityClaim(routedPrincipalAgentId, assignedAgentId) {
239997
240624
  const agentId = routedPrincipalAgentId ?? assignedAgentId ?? "executor";
@@ -240823,7 +241450,7 @@ function extractPromptSection(promptContent, sectionName) {
240823
241450
  return promptContent.slice(start, end).trim();
240824
241451
  }
240825
241452
  function summarizePromptSteps(promptContent) {
240826
- const stepTitles = Array.from(promptContent.matchAll(/^### Step \d+:.*$/gm), (match2) => match2[0].trim());
241453
+ const stepTitles = matchStepHeadings(promptContent).map(({ index: index3, headingLineEnd }) => promptContent.slice(index3, headingLineEnd).trim());
240827
241454
  if (stepTitles.length === 0) {
240828
241455
  return "";
240829
241456
  }
@@ -245594,85 +246221,6 @@ var init_review_inline_fix_recapture = __esm({
245594
246221
  }
245595
246222
  });
245596
246223
 
245597
- // ../engine/src/executor/optional-step-revision.ts
245598
- function normalizeOptionalStepRevisionKey2(value2) {
245599
- return (value2 ?? "").trim().toLowerCase();
245600
- }
245601
- function optionalStepRevisionKey2(nodeId, stepName) {
245602
- return normalizeOptionalStepRevisionKey2(nodeId) || normalizeOptionalStepRevisionKey2(stepName) || "pre-merge-optional-step";
245603
- }
245604
- function countOptionalStepRevisionAttempts2(task, key, stepName) {
245605
- const normalizedKey = normalizeOptionalStepRevisionKey2(key);
245606
- const normalizedStepName = normalizeOptionalStepRevisionKey2(stepName);
245607
- return (task.log ?? []).filter((entry) => {
245608
- const action = entry.action ?? "";
245609
- const outcome = entry.outcome ?? "";
245610
- if (!/attempt \d+\//.test(action)) return false;
245611
- const markerIndex = outcome.indexOf(OPTIONAL_STEP_REVISION_KEY_MARKER2);
245612
- if (markerIndex >= 0) {
245613
- const markerValue = outcome.slice(markerIndex + OPTIONAL_STEP_REVISION_KEY_MARKER2.length).split(/\r?\n/, 1)[0]?.trim();
245614
- return normalizeOptionalStepRevisionKey2(markerValue) === normalizedKey;
245615
- }
245616
- if (!normalizedStepName) return false;
245617
- return normalizeOptionalStepRevisionKey2(outcome).includes(`step: ${normalizedStepName}`);
245618
- }).length;
245619
- }
245620
- function optionalStepRevisionLogOutcome2(details, key) {
245621
- return `${details}
245622
- ${OPTIONAL_STEP_REVISION_KEY_MARKER2} ${key}`;
245623
- }
245624
- function buildReviewConvergenceContext(task, options) {
245625
- const revisionKey = options.revisionKey;
245626
- const priorAttemptCount = countPlanReviewRevisionAttempts(task.workflowStepResults, { revisionKey });
245627
- const attempt = priorAttemptCount + 1;
245628
- if (attempt <= 1) return "";
245629
- const history = collectPlanReviewFeedbackHistory(task.workflowStepResults, { revisionKey });
245630
- const lines = [
245631
- `## Convergence \u2014 ${options.reviewKind === "plan" ? "Plan Review" : "Code Review"} attempt ${attempt}`
245632
- ];
245633
- if (options.changeSummaryBlock?.trim()) lines.push("", options.changeSummaryBlock.trim());
245634
- lines.push(
245635
- "Treat the cumulative prior feedback below as a decision primer. Verify each prior blocker against the current PROMPT.md before looking for new findings.",
245636
- "- Do not re-raise a resolved or semantically duplicate blocker.",
245637
- "- 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."
245638
- );
245639
- if (attempt >= 3) {
245640
- lines.push(
245641
- "- Severity ratchet (attempt 3+): only delivery-blocking critical defects may return REVISE; important/minor wording or implementation-detail findings are advisory."
245642
- );
245643
- }
245644
- if (history.length > 0) {
245645
- lines.push("", "### Cumulative prior Plan Review ledger");
245646
- history.forEach((feedback, index3) => lines.push(`#### PR${index3 + 1}`, feedback));
245647
- }
245648
- const own2 = task.workflowStepResults?.find((result2) => result2.workflowStepId === revisionKey);
245649
- const findings = (own2?.priorAttempts ?? []).flatMap((attempt2) => attempt2.findings ?? []).filter(isOpenWorkflowReviewFinding);
245650
- if (findings.length > 0) {
245651
- lines.push("", "### Your prior findings on this gate");
245652
- findings.forEach((finding) => lines.push(`- ${finding.id} \u2014 [${finding.severity ?? "unclassified"}] ${finding.title}: ${finding.body}${finding.filePath ? ` (${finding.filePath}${finding.line ? `:${finding.line}` : ""})` : ""}`));
245653
- lines.push("Match prior defects by file path, line region, and described behavior; IDs and titles may change between rounds.");
245654
- }
245655
- const disputed = findings.filter((finding) => finding.disputedAt);
245656
- if (disputed.length > 0) {
245657
- lines.push("", "### Disputed by the implementer");
245658
- disputed.forEach((finding) => lines.push(`- ${finding.id}: ${finding.disputeRationale ?? "No rationale recorded."}`));
245659
- 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.");
245660
- }
245661
- return lines.join("\n");
245662
- }
245663
- function buildGraphPlanReviewConvergenceContext(task, revisionKey) {
245664
- return buildReviewConvergenceContext(task, { revisionKey, reviewKind: "plan" });
245665
- }
245666
- var OPTIONAL_STEP_REVISION_KEY_MARKER2;
245667
- var init_optional_step_revision = __esm({
245668
- "../engine/src/executor/optional-step-revision.ts"() {
245669
- "use strict";
245670
- init_src2();
245671
- init_plan_review_feedback_history();
245672
- OPTIONAL_STEP_REVISION_KEY_MARKER2 = "Workflow revision key:";
245673
- }
245674
- });
245675
-
245676
246224
  // ../engine/src/executor/execute-workflow-step.ts
245677
246225
  import { exec as exec44 } from "node:child_process";
245678
246226
  import { promisify as promisify63 } from "node:util";
@@ -246438,10 +246986,12 @@ ${JSON.stringify({ verdict: repairedVerdict })}`,
246438
246986
  session.dispose();
246439
246987
  await agentLogger.flush();
246440
246988
  if (parsed.verdict) {
246989
+ const findingsUnreadable = verdictRepairResult === "repaired" && (parsed.findings?.length ?? 0) === 0;
246441
246990
  const gated = reviewBlockingSeverity ? applyReviewSeverityGate({
246442
246991
  verdict: parsed.verdict,
246443
246992
  findings: parsed.findings,
246444
- threshold: reviewBlockingSeverity
246993
+ threshold: reviewBlockingSeverity,
246994
+ findingsUnreadable
246445
246995
  }) : void 0;
246446
246996
  const effectiveVerdict = gated?.verdict ?? parsed.verdict;
246447
246997
  if (gated?.downgraded) {
@@ -260956,7 +261506,7 @@ async function routeGraphMergeFailureToRetry(deps, live, result2, abortProvenanc
260956
261506
  const parked = await parkTaskFailed(
260957
261507
  deps.store,
260958
261508
  live.id,
260959
- `AUTO_MERGE_RETRY_REJECTED: ${reason}`,
261509
+ `${AUTO_MERGE_RETRY_REJECTED_PREFIX} ${reason}`,
260960
261510
  deps.getRunContextFor(live.id),
260961
261511
  live.columnMovedAt
260962
261512
  );
@@ -260972,6 +261522,7 @@ var init_route_graph_merge_failure_to_retry = __esm({
260972
261522
  init_logger3();
260973
261523
  init_workflow_merge_nodes();
260974
261524
  init_emit_merge_boundary_unproven_audit();
261525
+ init_stale_content_park();
260975
261526
  BIT_BACKOFF_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 32e3, 64e3];
260976
261527
  }
260977
261528
  });
@@ -265633,9 +266184,13 @@ async function runImplementation(deps, task, graphCompletion, reportImplementati
265633
266184
  */
265634
266185
  credentialInstanceId: detail.credentialInstanceId,
265635
266186
  resolveCredentialInstanceRetarget: nextStepInstance,
265636
- // Attribute the per-step run auditor to the column agent when it governs
265637
- // (U4); absent → StepSessionExecutor falls back to assignedAgentId.
265638
- effectiveAgentId: stepColumnAgent?.agent.id,
266187
+ /*
266188
+ FNXC:ColumnAgent 2026-09-04-14:11:
266189
+ The effective step identity is the governing column agent or the authoritative assignee.
266190
+ Both are existence-proved reads; with empty project.boards the column seam is absent, so
266191
+ retaining only it lost attribution for every assigned task.
266192
+ */
266193
+ effectiveAgentId: stepIdentityAgent?.id,
265639
266194
  actionGateContext: deps.buildActionGateContext(task.id, stepIdentityAgent, settings.defaultAgentPermissionPolicy),
265640
266195
  permanentAgentGating: deps.buildPermanentAgentGatingContext(task.id, stepIdentityAgent, settings.defaultAgentPermissionPolicy),
265641
266196
  // 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.
@@ -270498,10 +271053,22 @@ var init_terminate_all_children = __esm({
270498
271053
  });
270499
271054
 
270500
271055
  // ../engine/src/executor/clear-terminal-step-failures-for-retry.ts
271056
+ function latestTerminalPreMergeFailureId(results) {
271057
+ return (results ?? []).filter((result2) => (result2.phase || "pre-merge") === "pre-merge" && (result2.status === "failed" || result2.status === "advisory_failure")).sort((left, right) => {
271058
+ const leftTime = Date.parse(left.completedAt || left.startedAt || "");
271059
+ const rightTime = Date.parse(right.completedAt || right.startedAt || "");
271060
+ return (Number.isFinite(rightTime) ? rightTime : 0) - (Number.isFinite(leftTime) ? leftTime : 0);
271061
+ })[0]?.workflowStepId;
271062
+ }
270501
271063
  async function clearTerminalStepFailuresForRetry(deps, taskId, mode) {
270502
271064
  const live = await deps.store.getTask(taskId).catch(() => null);
270503
271065
  if (!live) return;
270504
- const cleared = mode === "archive" ? archiveTerminalWorkflowStepFailures(live.workflowStepResults) : clearTerminalWorkflowStepFailures(live.workflowStepResults);
271066
+ const remediatedStepId = mode === "archive" ? latestTerminalPreMergeFailureId(live.workflowStepResults) : void 0;
271067
+ const cleared = mode === "archive" ? archiveTerminalWorkflowStepFailures(
271068
+ live.workflowStepResults,
271069
+ void 0,
271070
+ remediatedStepId ? { workflowStepIds: /* @__PURE__ */ new Set([remediatedStepId]) } : {}
271071
+ ) : clearTerminalWorkflowStepFailures(live.workflowStepResults);
270505
271072
  if (cleared !== live.workflowStepResults) {
270506
271073
  await deps.store.updateTask(taskId, { workflowStepResults: cleared }, deps.getRunContextFor(taskId));
270507
271074
  }
@@ -275108,10 +275675,14 @@ var init_mission_execution_loop = __esm({
275108
275675
  for (const slice of milestone.slices) {
275109
275676
  if (slice.status !== "active") continue;
275110
275677
  const supersededFixes = await this.missionStore.reconcileSupersededGeneratedFixFeatures(slice.id);
275111
- const supersededFeatureIds = new Set(supersededFixes.featureIds);
275678
+ const supersededFeatureIds = new Set(supersededFixes.featureIds ?? []);
275679
+ if ((supersededFixes.repairedCount ?? 0) > 0) {
275680
+ loopLog.warn(`Recovery: restored ${supersededFixes.repairedCount ?? 0} generated Fix Features in slice ${slice.id}: ${(supersededFixes.repairedFeatureIds ?? []).join(", ")}`);
275681
+ recoveredCount += supersededFixes.repairedCount ?? 0;
275682
+ }
275112
275683
  if (supersededFixes.supersededCount > 0) {
275113
275684
  loopLog.warn(
275114
- `Recovery: superseded ${supersededFixes.supersededCount} generated Fix Features in slice ${slice.id} because an ancestor feature already passed validation`
275685
+ `Recovery: superseded ${supersededFixes.supersededCount} generated Fix Features in slice ${slice.id} because an ancestor or the Fix feature's own validation already passed`
275115
275686
  );
275116
275687
  recoveredCount += supersededFixes.supersededCount;
275117
275688
  }
@@ -298121,7 +298692,7 @@ ${detail}`);
298121
298692
  metadata: { taskId: task.id, nodeId: reroute.nodeId, workflowStepId: reroute.workflowStepId, reason: reroute.reason, source: "merge-gate", missingGateCount: mergeGate.requiredPreMergeStepIds.size }
298122
298693
  });
298123
298694
  }
298124
- if (blocker === "task has a pre-merge approval recorded against different content" && mergeContent.kind === "singular") {
298695
+ if (isStaleContentApprovalBlocker(blocker) && mergeContent.kind === "singular") {
298125
298696
  const reroute = await rerouteSingularStaleContentToReview(store, task, {
298126
298697
  requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds,
298127
298698
  mergeContent
@@ -298131,9 +298702,10 @@ ${detail}`);
298131
298702
  nodeId: void 0,
298132
298703
  workflowStepId: void 0
298133
298704
  }));
298134
- if (reroute.rerouted) {
298135
- await store.logEntry(task.id, `[pre-merge] Code Review re-entry is owned by the workflow graph after stale content evidence was refused.`);
298136
- }
298705
+ await store.logEntry(
298706
+ task.id,
298707
+ `[pre-merge] Review lane '${reroute.nodeId ?? "unknown"}' approved older content; re-review is owned by the workflow graph (${reroute.reason}).`
298708
+ );
298137
298709
  const auditKey = `${task.id}:${reroute.reason}`;
298138
298710
  if (!this.staleContentRerouteAuditKeys.has(auditKey)) {
298139
298711
  this.staleContentRerouteAuditKeys.add(auditKey);
@@ -302499,6 +303071,7 @@ __export(src_exports2, {
302499
303071
  NotificationService: () => NotificationService,
302500
303072
  NtfyNotificationProvider: () => NtfyNotificationProvider,
302501
303073
  NtfyNotifier: () => NtfyNotifier,
303074
+ OPTIONAL_STEP_REVISION_RESET_MARKER: () => OPTIONAL_STEP_REVISION_RESET_MARKER,
302502
303075
  OVERSEER_ADVISOR_REPLY_CONTRACT: () => OVERSEER_ADVISOR_REPLY_CONTRACT,
302503
303076
  OVERSEER_ADVISOR_SYSTEM_PROMPT: () => OVERSEER_ADVISOR_SYSTEM_PROMPT,
302504
303077
  OVERSEER_WATCHED_STAGES: () => OVERSEER_WATCHED_STAGES,
@@ -302678,6 +303251,7 @@ __export(src_exports2, {
302678
303251
  createFusionModelRegistry: () => createFusionModelRegistry,
302679
303252
  createGetAgentConfigTool: () => createGetAgentConfigTool,
302680
303253
  createGoalRetrievalTools: () => createGoalRetrievalTools,
303254
+ createHistoryReadTool: () => createHistoryReadTool,
302681
303255
  createIdeationTools: () => createIdeationTools,
302682
303256
  createInteractiveAiSessionWith: () => createInteractiveAiSessionWith,
302683
303257
  createListAgentsTool: () => createListAgentsTool,
@@ -302687,7 +303261,6 @@ __export(src_exports2, {
302687
303261
  createNoopLegacySeams: () => createNoopLegacySeams,
302688
303262
  createParseStepsHandler: () => createParseStepsHandler,
302689
303263
  createParsingOverseerAgent: () => createParsingOverseerAgent,
302690
- createPatchnodeReadTool: () => createPatchnodeReadTool,
302691
303264
  createPrNodeHandlers: () => createPrNodeHandlers,
302692
303265
  createPrimitivePromptLikeHandler: () => createPrimitivePromptLikeHandler,
302693
303266
  createPrimitiveStepReviewHandler: () => createPrimitiveStepReviewHandler,
@@ -302859,6 +303432,7 @@ __export(src_exports2, {
302859
303432
  normalizeAgentLogPaging: () => normalizeAgentLogPaging,
302860
303433
  normalizeJiraIssueKey: () => normalizeJiraIssueKey,
302861
303434
  notifyAutostashOrphans: () => notifyAutostashOrphans,
303435
+ optionalStepRevisionResetOutcome: () => optionalStepRevisionResetOutcome,
302862
303436
  parseAdvisorReplyForAdvice: () => parseAdvisorReplyForAdvice,
302863
303437
  parseInteractiveAgentResponse: () => parseAgentResponse,
302864
303438
  parsePrEntityMarker: () => parsePrEntityMarker,
@@ -303048,6 +303622,7 @@ var init_src5 = __esm({
303048
303622
  init_postgres_migration_notice();
303049
303623
  init_concurrency();
303050
303624
  init_triage();
303625
+ init_optional_step_revision();
303051
303626
  init_executor();
303052
303627
  init_workflow_graph_executor();
303053
303628
  init_workflow_graph_branches();
@@ -326943,6 +327518,13 @@ async function restartTaskStage(deps) {
326943
327518
  fenced = false;
326944
327519
  const preservedWorkspaceRepositoryRecords = Object.keys(task.workspaceWorktrees ?? {}).length;
326945
327520
  await store.logEntry(taskId, `Retry requested from dashboard (${plan.scope} restart in ${task.column}, discarded ${plan.discardedWorkflowStepIds.length} workflow step result(s), preserved ${preservedWorkspaceRepositoryRecords} workspace repository record(s), re-entering at ${plan.entryNodeId})`);
327521
+ for (const workflowStepId of plan.discardedWorkflowStepIds) {
327522
+ await store.logEntry(
327523
+ taskId,
327524
+ `Revision budget reset for '${workflowStepId}' by operator retry`,
327525
+ optionalStepRevisionResetOutcome(workflowStepId)
327526
+ ).catch(() => void 0);
327527
+ }
326946
327528
  const updated = await store.getTask(taskId);
326947
327529
  if (!updated) throw notFound(`Task ${taskId} not found after retry`);
326948
327530
  return updated;
@@ -330057,8 +330639,9 @@ See \`GET /api/tasks/${task.id}/diff\` for the landed diff being reverted.
330057
330639
  );
330058
330640
  }
330059
330641
  const { store: scopedStore } = await getProjectContext2(req);
330060
- const archived = await scopedStore.archiveAllDone();
330061
- res.json({ archived });
330642
+ const result2 = await scopedStore.archiveAllDone();
330643
+ const { archived, skipped } = Array.isArray(result2) ? { archived: result2, skipped: [] } : result2;
330644
+ res.json({ archived, skipped });
330062
330645
  } catch (err) {
330063
330646
  if (err instanceof ApiError) {
330064
330647
  throw err;
@@ -335169,6 +335752,261 @@ var init_task_planner_chat_metrics = __esm({
335169
335752
  }
335170
335753
  });
335171
335754
 
335755
+ // ../dashboard/src/chat-conversation-references.ts
335756
+ function textResult(text4, details = {}) {
335757
+ return { content: [{ type: "text", text: text4 }], details };
335758
+ }
335759
+ function errorResult(text4 = UNAVAILABLE_TOOL_MESSAGE) {
335760
+ return { content: [{ type: "text", text: text4 }], isError: true, details: {} };
335761
+ }
335762
+ function clampInteger(value2, fallback2, minimum, maximum) {
335763
+ if (typeof value2 !== "number" || !Number.isFinite(value2)) return fallback2;
335764
+ return Math.min(maximum, Math.max(minimum, Math.trunc(value2)));
335765
+ }
335766
+ function truncateText2(value2, maximum) {
335767
+ if (value2.length <= maximum) return { text: value2, truncated: false };
335768
+ if (maximum <= 1) return { text: "\u2026".slice(0, maximum), truncated: true };
335769
+ return { text: `${value2.slice(0, maximum - 1)}\u2026`, truncated: true };
335770
+ }
335771
+ function boundToolResponse(value2, hint) {
335772
+ if (value2.length <= CONVERSATION_TOOL_RESPONSE_MAX_CHARS) return value2;
335773
+ const prefixLength = CONVERSATION_TOOL_RESPONSE_MAX_CHARS - hint.length - 2;
335774
+ return `${value2.slice(0, prefixLength)}
335775
+
335776
+ ${hint}`;
335777
+ }
335778
+ function wrapReferenceBlock(id, body) {
335779
+ return `[Referenced Conversation: ${id}]
335780
+ ${body}
335781
+ [/Referenced Conversation: ${id}]`;
335782
+ }
335783
+ function formatModel(session) {
335784
+ if (session.modelProvider && session.modelId) return `${session.modelProvider}/${session.modelId}`;
335785
+ return session.modelId ?? session.modelProvider ?? "default";
335786
+ }
335787
+ function formatSessionHeader(session, currentSessionId) {
335788
+ return [
335789
+ `Conversation: ${session.id}${session.id === currentSessionId ? " (current conversation)" : ""}`,
335790
+ `Title: ${session.title ?? "Untitled"}`,
335791
+ `Status: ${session.status}`,
335792
+ `Agent: ${session.agentId}`,
335793
+ `Model: ${formatModel(session)}`,
335794
+ `Created: ${session.createdAt}`,
335795
+ `Updated: ${session.updatedAt}`
335796
+ ].join("\n");
335797
+ }
335798
+ function formatMessage(message, maximum) {
335799
+ const content = truncateText2(message.content, maximum);
335800
+ return {
335801
+ text: `[${message.role} @ ${message.createdAt}] ${content.text}`,
335802
+ truncated: content.truncated
335803
+ };
335804
+ }
335805
+ async function resolveProjectSession(chatStore, conversationId, projectId2) {
335806
+ try {
335807
+ const session = await chatStore.getSession(conversationId);
335808
+ return session?.projectId === projectId2 ? session : void 0;
335809
+ } catch {
335810
+ return void 0;
335811
+ }
335812
+ }
335813
+ function parseConversationReferences(content) {
335814
+ const references = [];
335815
+ const seen = /* @__PURE__ */ new Set();
335816
+ const pattern = /(?:^|[\s,.;:!?"'()[\]{}])#(chat-[a-zA-Z0-9]{8})(?![./a-zA-Z0-9-])/g;
335817
+ let match2;
335818
+ while ((match2 = pattern.exec(content)) !== null) {
335819
+ const id = match2[1];
335820
+ if (seen.has(id)) continue;
335821
+ seen.add(id);
335822
+ references.push(id);
335823
+ if (references.length >= MAX_REFERENCED_CONVERSATIONS) break;
335824
+ }
335825
+ return references;
335826
+ }
335827
+ async function buildConversationReferenceContext(input) {
335828
+ const references = parseConversationReferences(input.content);
335829
+ if (references.length === 0) return "";
335830
+ const blocks = await Promise.all(references.map(async (id) => {
335831
+ try {
335832
+ const session = await input.chatStore.getSession(id);
335833
+ if (!session || session.projectId !== input.currentProjectId) {
335834
+ return wrapReferenceBlock(id, UNAVAILABLE_CONTEXT_MESSAGE);
335835
+ }
335836
+ if (session.id === input.currentSessionId) {
335837
+ return wrapReferenceBlock(
335838
+ id,
335839
+ "Current conversation: this ID refers to the current conversation; no preview was added."
335840
+ );
335841
+ }
335842
+ const messages3 = await input.chatStore.getMessages(id, {
335843
+ limit: CONVERSATION_PREVIEW_MESSAGE_COUNT,
335844
+ order: "desc"
335845
+ });
335846
+ const preview2 = messages3.length > 0 ? messages3.map((message) => formatMessage(message, CONVERSATION_PREVIEW_MESSAGE_CHARS).text).join("\n") : "(no messages)";
335847
+ return wrapReferenceBlock(
335848
+ id,
335849
+ [
335850
+ formatSessionHeader(session, input.currentSessionId),
335851
+ "Recent messages (newest first):",
335852
+ preview2,
335853
+ "Use fn_chat_conversation_read or fn_chat_conversation_search for more detail."
335854
+ ].join("\n")
335855
+ );
335856
+ } catch {
335857
+ return wrapReferenceBlock(id, UNAVAILABLE_CONTEXT_MESSAGE);
335858
+ }
335859
+ }));
335860
+ return blocks.join("\n\n");
335861
+ }
335862
+ function createConversationReadTool(chatStore, options) {
335863
+ return {
335864
+ name: "fn_chat_conversation_read",
335865
+ label: "Read Chat Conversation",
335866
+ description: "Read a bounded page of messages from a conversation in the current Direct chat project. Requires an explicit conversation_id.",
335867
+ parameters: {
335868
+ type: "object",
335869
+ properties: {
335870
+ conversation_id: { type: "string" },
335871
+ limit: { type: "number", minimum: 1 },
335872
+ offset: { type: "number", minimum: 0 },
335873
+ order: { type: "string", enum: ["asc", "desc"] }
335874
+ },
335875
+ required: ["conversation_id"],
335876
+ additionalProperties: false
335877
+ },
335878
+ execute: async (_toolCallId, raw) => {
335879
+ const conversationId = typeof raw.conversation_id === "string" ? raw.conversation_id.trim() : "";
335880
+ if (!conversationId) return errorResult();
335881
+ const session = await resolveProjectSession(chatStore, conversationId, options.projectId);
335882
+ if (!session) return errorResult();
335883
+ const limit2 = clampInteger(raw.limit, CONVERSATION_READ_DEFAULT_LIMIT, 1, CONVERSATION_READ_MAX_LIMIT);
335884
+ const offset = clampInteger(raw.offset, 0, 0, Number.MAX_SAFE_INTEGER);
335885
+ const order = raw.order === "asc" ? "asc" : "desc";
335886
+ try {
335887
+ const messages3 = await chatStore.getMessages(conversationId, { limit: limit2, offset, order });
335888
+ let messageTruncated = false;
335889
+ const rows = messages3.map((message) => {
335890
+ const formatted = formatMessage(message, CONVERSATION_MESSAGE_MAX_CHARS);
335891
+ messageTruncated ||= formatted.truncated;
335892
+ return formatted.text;
335893
+ });
335894
+ const body = [
335895
+ formatSessionHeader(session, options.currentSessionId),
335896
+ `Messages (${order}, limit ${limit2}, offset ${offset}):`,
335897
+ rows.length > 0 ? rows.join("\n") : "(no messages)",
335898
+ ...messageTruncated ? [MESSAGE_TRUNCATION_HINT] : []
335899
+ ].join("\n");
335900
+ return textResult(boundToolResponse(body, READ_GLOBAL_TRUNCATION_HINT), {
335901
+ conversationId,
335902
+ count: messages3.length,
335903
+ limit: limit2,
335904
+ offset,
335905
+ order
335906
+ });
335907
+ } catch {
335908
+ return errorResult(TOOL_READ_ERROR_MESSAGE);
335909
+ }
335910
+ }
335911
+ };
335912
+ }
335913
+ function createSearchExcerpt(content, matchIndex, queryLength) {
335914
+ if (content.length <= CONVERSATION_SEARCH_EXCERPT_CHARS) return content;
335915
+ const bodyLength = CONVERSATION_SEARCH_EXCERPT_CHARS - 2;
335916
+ const contextBudget = Math.max(0, bodyLength - Math.min(queryLength, bodyLength));
335917
+ let start = Math.max(0, matchIndex - Math.floor(contextBudget / 2));
335918
+ start = Math.min(start, content.length - bodyLength);
335919
+ const end = Math.min(content.length, start + bodyLength);
335920
+ return `${start > 0 ? "\u2026" : ""}${content.slice(start, end)}${end < content.length ? "\u2026" : ""}`;
335921
+ }
335922
+ function createConversationSearchTool(chatStore, options) {
335923
+ return {
335924
+ name: "fn_chat_conversation_search",
335925
+ label: "Search Chat Conversation",
335926
+ description: "Search recent messages in one conversation in the current Direct chat project using a literal case-insensitive query.",
335927
+ parameters: {
335928
+ type: "object",
335929
+ properties: {
335930
+ conversation_id: { type: "string" },
335931
+ query: { type: "string", minLength: 1 },
335932
+ limit: { type: "number", minimum: 1 }
335933
+ },
335934
+ required: ["conversation_id", "query"],
335935
+ additionalProperties: false
335936
+ },
335937
+ execute: async (_toolCallId, raw) => {
335938
+ const conversationId = typeof raw.conversation_id === "string" ? raw.conversation_id.trim() : "";
335939
+ const query = typeof raw.query === "string" ? raw.query.trim() : "";
335940
+ if (!conversationId) return errorResult();
335941
+ if (!query) return errorResult("ERROR: query must be a non-empty string.");
335942
+ const session = await resolveProjectSession(chatStore, conversationId, options.projectId);
335943
+ if (!session) return errorResult();
335944
+ const limit2 = clampInteger(raw.limit, CONVERSATION_SEARCH_DEFAULT_LIMIT, 1, CONVERSATION_SEARCH_MAX_MATCHES);
335945
+ try {
335946
+ const messages3 = await chatStore.getMessages(conversationId, {
335947
+ limit: CONVERSATION_SEARCH_SCAN_LIMIT,
335948
+ order: "desc"
335949
+ });
335950
+ const normalizedQuery = query.toLocaleLowerCase();
335951
+ const matches = [];
335952
+ for (const message of messages3) {
335953
+ const matchIndex = message.content.toLocaleLowerCase().indexOf(normalizedQuery);
335954
+ if (matchIndex < 0) continue;
335955
+ matches.push(
335956
+ `[${message.role} @ ${message.createdAt}] ${createSearchExcerpt(message.content, matchIndex, query.length)}`
335957
+ );
335958
+ if (matches.length >= limit2) break;
335959
+ }
335960
+ const saturated = messages3.length >= CONVERSATION_SEARCH_SCAN_LIMIT;
335961
+ const body = [
335962
+ formatSessionHeader(session, options.currentSessionId),
335963
+ `Search query: ${query}`,
335964
+ matches.length > 0 ? matches.join("\n") : "(no matches)",
335965
+ ...saturated ? [`[Search window saturated at the ${CONVERSATION_SEARCH_SCAN_LIMIT} most recent messages; older matches may exist.]`] : []
335966
+ ].join("\n");
335967
+ return textResult(boundToolResponse(body, SEARCH_GLOBAL_TRUNCATION_HINT), {
335968
+ conversationId,
335969
+ matchCount: matches.length,
335970
+ limit: limit2,
335971
+ scannedCount: messages3.length,
335972
+ saturated
335973
+ });
335974
+ } catch {
335975
+ return errorResult(TOOL_READ_ERROR_MESSAGE);
335976
+ }
335977
+ }
335978
+ };
335979
+ }
335980
+ function createChatConversationTools(chatStore, options) {
335981
+ return [
335982
+ createConversationReadTool(chatStore, options),
335983
+ createConversationSearchTool(chatStore, options)
335984
+ ];
335985
+ }
335986
+ var MAX_REFERENCED_CONVERSATIONS, CONVERSATION_PREVIEW_MESSAGE_COUNT, CONVERSATION_PREVIEW_MESSAGE_CHARS, CONVERSATION_READ_DEFAULT_LIMIT, CONVERSATION_READ_MAX_LIMIT, CONVERSATION_SEARCH_DEFAULT_LIMIT, CONVERSATION_SEARCH_MAX_MATCHES, CONVERSATION_SEARCH_SCAN_LIMIT, CONVERSATION_SEARCH_EXCERPT_CHARS, CONVERSATION_MESSAGE_MAX_CHARS, CONVERSATION_TOOL_RESPONSE_MAX_CHARS, UNAVAILABLE_CONTEXT_MESSAGE, UNAVAILABLE_TOOL_MESSAGE, TOOL_READ_ERROR_MESSAGE, READ_GLOBAL_TRUNCATION_HINT, SEARCH_GLOBAL_TRUNCATION_HINT, MESSAGE_TRUNCATION_HINT;
335987
+ var init_chat_conversation_references = __esm({
335988
+ "../dashboard/src/chat-conversation-references.ts"() {
335989
+ "use strict";
335990
+ MAX_REFERENCED_CONVERSATIONS = 3;
335991
+ CONVERSATION_PREVIEW_MESSAGE_COUNT = 5;
335992
+ CONVERSATION_PREVIEW_MESSAGE_CHARS = 300;
335993
+ CONVERSATION_READ_DEFAULT_LIMIT = 30;
335994
+ CONVERSATION_READ_MAX_LIMIT = 100;
335995
+ CONVERSATION_SEARCH_DEFAULT_LIMIT = 10;
335996
+ CONVERSATION_SEARCH_MAX_MATCHES = 25;
335997
+ CONVERSATION_SEARCH_SCAN_LIMIT = 400;
335998
+ CONVERSATION_SEARCH_EXCERPT_CHARS = 200;
335999
+ CONVERSATION_MESSAGE_MAX_CHARS = 1500;
336000
+ CONVERSATION_TOOL_RESPONSE_MAX_CHARS = 12e3;
336001
+ UNAVAILABLE_CONTEXT_MESSAGE = "Unavailable: no conversation with this ID exists in this project.";
336002
+ UNAVAILABLE_TOOL_MESSAGE = "ERROR: no conversation with this ID exists in this project.";
336003
+ TOOL_READ_ERROR_MESSAGE = "ERROR: conversation messages could not be read.";
336004
+ READ_GLOBAL_TRUNCATION_HINT = "[Output truncated. Retry with a smaller limit or a larger offset.]";
336005
+ SEARCH_GLOBAL_TRUNCATION_HINT = "[Output truncated. Retry with a smaller limit or a narrower query.]";
336006
+ MESSAGE_TRUNCATION_HINT = "[Some messages were truncated. Retry with a smaller limit or a different offset.]";
336007
+ }
336008
+ });
336009
+
335172
336010
  // ../dashboard/src/chat.ts
335173
336011
  var chat_exports = {};
335174
336012
  __export(chat_exports, {
@@ -335487,15 +336325,32 @@ function createTaskVerificationTools(taskStore, actionGateContext) {
335487
336325
  ];
335488
336326
  }
335489
336327
  async function createChatFusionToolset(options) {
335490
- const { taskStore, agentStore, rootDir, agentId, missionMutationGated = false, actionGateContext, focus } = options;
336328
+ const {
336329
+ taskStore,
336330
+ agentStore,
336331
+ rootDir,
336332
+ agentId,
336333
+ missionMutationGated = false,
336334
+ actionGateContext,
336335
+ focus,
336336
+ chatStore,
336337
+ currentChatSessionId,
336338
+ currentProjectId
336339
+ } = options;
335491
336340
  const tools = [];
336341
+ if (chatStore && currentChatSessionId) {
336342
+ tools.push(...createChatConversationTools(chatStore, {
336343
+ currentSessionId: currentChatSessionId,
336344
+ projectId: currentProjectId ?? null
336345
+ }));
336346
+ }
335492
336347
  if (taskStore) {
335493
336348
  const settings = await taskStore.getSettings?.();
335494
336349
  tools.push(
335495
336350
  createTaskListTool(taskStore),
335496
336351
  createTaskShowTool(taskStore),
335497
336352
  createTaskSearchTool(taskStore),
335498
- createPatchnodeReadTool(taskStore),
336353
+ createHistoryReadTool(taskStore),
335499
336354
  ...createTaskVerificationTools(taskStore, options.actionGateContext),
335500
336355
  createTaskCreateTool(taskStore, { sourceType: "api" }, { rootDir })
335501
336356
  );
@@ -335921,6 +336776,7 @@ var init_chat = __esm({
335921
336776
  init_task_planner_chat_context();
335922
336777
  init_task_planner_chat_metrics();
335923
336778
  init_sse2();
336779
+ init_chat_conversation_references();
335924
336780
  init_src5();
335925
336781
  init_src5();
335926
336782
  createFnAgent9 = createFnAgent2;
@@ -337231,7 +338087,16 @@ ${taskContext}`;
337231
338087
  ${mentionContext}`;
337232
338088
  }
337233
338089
  }
337234
- const resolvedContent = await resolveFileReferences(parsedSkillCommands.strippedContent, this.rootDir);
338090
+ const fileResolvedContent = await resolveFileReferences(parsedSkillCommands.strippedContent, this.rootDir);
338091
+ const conversationReferenceContext = await buildConversationReferenceContext({
338092
+ chatStore: this.chatStore,
338093
+ content: parsedSkillCommands.strippedContent,
338094
+ currentSessionId: sessionId,
338095
+ currentProjectId: session.projectId ?? null
338096
+ });
338097
+ const resolvedContent = conversationReferenceContext ? `${fileResolvedContent}
338098
+
338099
+ ${conversationReferenceContext}` : fileResolvedContent;
337235
338100
  const attachmentSummary = attachments && attachments.length > 0 ? `[User attached: ${attachments.map((attachment) => `${attachment.originalName} (${attachment.mimeType}, ${formatAttachmentSize(attachment.size)})`).join(", ")}]` : "";
337236
338101
  const { attachmentContents, imageContents } = await readChatAttachmentContents(
337237
338102
  this.rootDir,
@@ -337303,7 +338168,10 @@ ${mentionContext}`;
337303
338168
  toolset applies it only while experimentalFeatures.chatFocus is enabled. Otherwise the
337304
338169
  value is inert and both direct and room chat recall remain whole-project.
337305
338170
  */
337306
- focus: session?.memoryFocus ?? void 0
338171
+ focus: session?.memoryFocus ?? void 0,
338172
+ chatStore: this.chatStore,
338173
+ currentChatSessionId: sessionId,
338174
+ currentProjectId: session?.projectId ?? null
337307
338175
  });
337308
338176
  const customTools = dedupeChatTools([
337309
338177
  createAskQuestionTool(),
@@ -375222,12 +376090,9 @@ var init_dashboard_views = __esm({
375222
376090
  { id: "board", label: "Board", labelKey: "nav.board" },
375223
376091
  { id: "list", label: "List", labelKey: "nav.list" },
375224
376092
  /*
375225
- FNXC:PatchnoteNaming 2026-08-30-01:40:
375226
- Operators call the delivery history Patchnote. Keep the stable `patchnode` identifier and translation key unchanged because routes, storage, and integrations depend on them.
375227
-
375228
- FNXC:PatchnoteNaming 2026-08-30-06:36:
375229
- 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.
375230
- */
376093
+ FNXC:HistoryNaming 2026-09-04-09:35:
376094
+ 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.
376095
+ */
375231
376096
  { id: "patchnode", label: "History", labelKey: "nav.patchnode" },
375232
376097
  { id: "graph", label: "Graph" },
375233
376098
  { id: "agents", label: "Agents", labelKey: "nav.agents" },
@@ -397985,12 +398850,12 @@ async function rebuildProjectKnowledgeGraph(store, options = {}) {
397985
398850
  rebuilds.delete(graphDir);
397986
398851
  }
397987
398852
  }
397988
- function clampInteger(value2, fallback2, minimum, maximum) {
398853
+ function clampInteger2(value2, fallback2, minimum, maximum) {
397989
398854
  return typeof value2 === "number" && Number.isFinite(value2) ? Math.min(maximum, Math.max(minimum, Math.trunc(value2))) : fallback2;
397990
398855
  }
397991
398856
  function findBoundedShortestPath(graph, fromId, toId, options = {}) {
397992
- const maxHops = clampInteger(options.maxHops, KNOWLEDGE_GRAPH_PATH_DEFAULT_HOPS, 1, KNOWLEDGE_GRAPH_PATH_MAX_HOPS);
397993
- const maxExpansions = clampInteger(options.maxExpansions, KNOWLEDGE_GRAPH_PATH_MAX_EXPANSIONS, 1, KNOWLEDGE_GRAPH_PATH_MAX_EXPANSIONS);
398857
+ const maxHops = clampInteger2(options.maxHops, KNOWLEDGE_GRAPH_PATH_DEFAULT_HOPS, 1, KNOWLEDGE_GRAPH_PATH_MAX_HOPS);
398858
+ const maxExpansions = clampInteger2(options.maxExpansions, KNOWLEDGE_GRAPH_PATH_MAX_EXPANSIONS, 1, KNOWLEDGE_GRAPH_PATH_MAX_EXPANSIONS);
397994
398859
  const nodes2 = new Map(graph.nodes.map((node) => [node.id, node]));
397995
398860
  if (!nodes2.has(fromId) || !nodes2.has(toId)) throw new KnowledgeGraphError("Unknown graph node");
397996
398861
  if (fromId === toId) return { outcome: "found", path: { nodes: [nodes2.get(fromId)], edges: [] }, hops: 0, maxHops, expansions: 0, truncated: false };