@runfusion/fusion 0.50.0 → 0.51.0

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 (122) hide show
  1. package/dist/bin.js +815 -352
  2. package/dist/client/.vite/manifest.json +242 -241
  3. package/dist/client/assets/{AgentDetailView-D2oYUApl.js → AgentDetailView-CjwOlcMI.js} +1 -1
  4. package/dist/client/assets/{AgentPermissionPolicyEditor-DGgx46f3.js → AgentPermissionPolicyEditor-CD8znNHS.js} +1 -1
  5. package/dist/client/assets/{AgentsView-Dhf5Vq_T.js → AgentsView-CTjNaKu-.js} +3 -3
  6. package/dist/client/assets/ChatView-DCWypaw3.js +19 -0
  7. package/dist/client/assets/{CommandCenter-DzQhGq_J.js → CommandCenter-C4v0FMxx.js} +9 -9
  8. package/dist/client/assets/{DevServerView-DjJYp6Jj.js → DevServerView-BE_o7v9j.js} +1 -1
  9. package/dist/client/assets/{DirectoryPicker-CYPYp9T4.js → DirectoryPicker-BpcPo65A.js} +1 -1
  10. package/dist/client/assets/{DocumentsView-CeQVRVdp.js → DocumentsView-Cpma23hu.js} +1 -1
  11. package/dist/client/assets/{EvalsView-D250mVR5.js → EvalsView-DYSobhb7.js} +1 -1
  12. package/dist/client/assets/{ExperimentalAgentOnboardingModal-B1EEY_bl.js → ExperimentalAgentOnboardingModal-D_iQqD-n.js} +1 -1
  13. package/dist/client/assets/{GoalsView-CUh8JBnJ.css → GoalsView-4-eEOeqw.css} +1 -1
  14. package/dist/client/assets/{GoalsView-MftVwE9g.js → GoalsView-B6Wfxk59.js} +1 -1
  15. package/dist/client/assets/{InsightsView-CbWItYe8.js → InsightsView-DZyMvYQ8.js} +1 -1
  16. package/dist/client/assets/{MemoryView-Ct0nOTES.js → MemoryView-B9Z6cRma.js} +1 -1
  17. package/dist/client/assets/{PiExtensionsManager-DSQwngX-.js → PiExtensionsManager-BQt3JU60.js} +1 -1
  18. package/dist/client/assets/{PluginManager-DqLxCiNg.js → PluginManager-DEsUstVo.js} +1 -1
  19. package/dist/client/assets/PullRequestView-B7k8ogy4.js +6 -0
  20. package/dist/client/assets/PullRequestView-NkeImY1n.css +1 -0
  21. package/dist/client/assets/{ResearchView-CJQL9XKC.js → ResearchView-BseF58_R.js} +1 -1
  22. package/dist/client/assets/{SecretsView-ChN9aTbi.js → SecretsView-bkNS5VBC.js} +1 -1
  23. package/dist/client/assets/{SessionTerminal-BVon9kbr.js → SessionTerminal-BSllyqgF.js} +1 -1
  24. package/dist/client/assets/{SettingsModal-CIC5jhgo.js → SettingsModal-DAH7LuLw.js} +1 -1
  25. package/dist/client/assets/SettingsModal-Dso0K76J.js +21 -0
  26. package/dist/client/assets/{SetupWizardModal-C6EGjmbd.js → SetupWizardModal-Bm6bG31V.js} +2 -2
  27. package/dist/client/assets/{ShadcnColorPicker-4e_aXoGX.js → ShadcnColorPicker-CLiOvo7W.js} +1 -1
  28. package/dist/client/assets/{SkillsView-Dz8dDwfP.js → SkillsView-D_UEJY_f.js} +1 -1
  29. package/dist/client/assets/{TodoView-DvJFzoOv.js → TodoView-CMCoPep1.js} +1 -1
  30. package/dist/client/assets/{WorkflowNodeEditor-DrdTCstS.js → WorkflowNodeEditor-BeQcn0Wa.js} +1 -1
  31. package/dist/client/assets/{agentSkills-B2dD5Nm5.js → agentSkills-BiipFw1z.js} +1 -1
  32. package/dist/client/assets/{app-D27ewSC0.js → app-A1yCJR4Q.js} +3 -3
  33. package/dist/client/assets/{app-BYq4S7Cb.js → app-Bfiafek7.js} +3 -3
  34. package/dist/client/assets/{app-OPrfgjjn.js → app-Bh-Xj32t.js} +3 -3
  35. package/dist/client/assets/{app-k1K9zAjp.js → app-DxJHfzxz.js} +3 -3
  36. package/dist/client/assets/{app-dATBczl3.js → app-T5iMjZpX.js} +2 -2
  37. package/dist/client/assets/{app-DFLDkeOE.js → app-zR_n4GL2.js} +3 -3
  38. package/dist/client/assets/{architectureDiagram-3BPJPVTR-ClO0MnzR.js → architectureDiagram-3BPJPVTR-glwWkaDt.js} +1 -1
  39. package/dist/client/assets/{blockDiagram-GPEHLZMM-C4cyy4WE.js → blockDiagram-GPEHLZMM-C8DhxwG4.js} +1 -1
  40. package/dist/client/assets/{c4Diagram-AAUBKEIU-NHPC9TJo.js → c4Diagram-AAUBKEIU-CQXf6PzI.js} +1 -1
  41. package/dist/client/assets/channel-BY6zFSV1.js +1 -0
  42. package/dist/client/assets/{chunk-2J33WTMH-CGYIKTVR.js → chunk-2J33WTMH-kak65s83.js} +1 -1
  43. package/dist/client/assets/{chunk-4BX2VUAB-BkCCD0tE.js → chunk-4BX2VUAB-B_yEpoxU.js} +1 -1
  44. package/dist/client/assets/{chunk-55IACEB6-Be3yOIDe.js → chunk-55IACEB6-C9lQVeH5.js} +1 -1
  45. package/dist/client/assets/{chunk-727SXJPM-DxjyMaQQ.js → chunk-727SXJPM-D4SVzqNs.js} +1 -1
  46. package/dist/client/assets/{chunk-AQP2D5EJ-BC0PjN0W.js → chunk-AQP2D5EJ-DBHuj4BH.js} +1 -1
  47. package/dist/client/assets/{chunk-FMBD7UC4-DoWxmM8k.js → chunk-FMBD7UC4-BJ3ghjau.js} +1 -1
  48. package/dist/client/assets/{chunk-ND2GUHAM-EOpuc9pP.js → chunk-ND2GUHAM-CE4tkh6e.js} +1 -1
  49. package/dist/client/assets/{chunk-QZHKN3VN-DiRyx9QZ.js → chunk-QZHKN3VN-DmVw_cEu.js} +1 -1
  50. package/dist/client/assets/classDiagram-4FO5ZUOK-CeX_WCiC.js +1 -0
  51. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CeX_WCiC.js +1 -0
  52. package/dist/client/assets/{cose-bilkent-S5V4N54A-CfVoUUxT.js → cose-bilkent-S5V4N54A-DI_-BOYE.js} +1 -1
  53. package/dist/client/assets/{dagre-BM42HDAG-BfILvxLb.js → dagre-BM42HDAG-ywUbHZxt.js} +1 -1
  54. package/dist/client/assets/{dashboard-view-wFC1pe4d.js → dashboard-view-DJvdban3.js} +1 -1
  55. package/dist/client/assets/{dashboard-view-C9YIcx-O.js → dashboard-view-r4NTTh_X.js} +1633 -1633
  56. package/dist/client/assets/{diagram-2AECGRRQ-6mdmbH-5.js → diagram-2AECGRRQ-CAGucHCe.js} +1 -1
  57. package/dist/client/assets/{diagram-5GNKFQAL-CY7MMwO1.js → diagram-5GNKFQAL-D_1uzVtW.js} +1 -1
  58. package/dist/client/assets/{diagram-KO2AKTUF-D05XnQqd.js → diagram-KO2AKTUF-D7zY4Nlm.js} +1 -1
  59. package/dist/client/assets/{diagram-LMA3HP47-B18FTOJG.js → diagram-LMA3HP47-C1T9eOzA.js} +1 -1
  60. package/dist/client/assets/{diagram-OG6HWLK6-xmXlYf5m.js → diagram-OG6HWLK6-sWxhPvzU.js} +1 -1
  61. package/dist/client/assets/{erDiagram-TEJ5UH35-4XlgVLCs.js → erDiagram-TEJ5UH35-DfHtnaNg.js} +1 -1
  62. package/dist/client/assets/{flowDiagram-I6XJVG4X-BYl3ps7f.js → flowDiagram-I6XJVG4X-Brbb_Jlh.js} +1 -1
  63. package/dist/client/assets/{folder-open-BH6FwmO7.js → folder-open-BLwukrE0.js} +1 -1
  64. package/dist/client/assets/{ganttDiagram-6RSMTGT7-4vxuH9Dx.js → ganttDiagram-6RSMTGT7-CLvZ7891.js} +1 -1
  65. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-BESmFgXT.js → gitGraphDiagram-PVQCEYII-DD56Jsa3.js} +1 -1
  66. package/dist/client/assets/index-DAv17W73.js +1317 -0
  67. package/dist/client/assets/index-DhBnj8YN.css +1 -0
  68. package/dist/client/assets/{infoDiagram-5YYISTIA-DvjdjtwB.js → infoDiagram-5YYISTIA-uXai801q.js} +1 -1
  69. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-Cis5IoEm.js → ishikawaDiagram-YF4QCWOH-C_bWlUP_.js} +1 -1
  70. package/dist/client/assets/{journeyDiagram-JHISSGLW-C20pB1W8.js → journeyDiagram-JHISSGLW-B2kB9niE.js} +1 -1
  71. package/dist/client/assets/{kanban-definition-UN3LZRKU-CiVfCeU3.js → kanban-definition-UN3LZRKU-BJwlpR9z.js} +1 -1
  72. package/dist/client/assets/{mermaid.core-CW0vCXA6.js → mermaid.core-0hUjZF5S.js} +4 -4
  73. package/dist/client/assets/{mindmap-definition-RKZ34NQL-E5rPHUdw.js → mindmap-definition-RKZ34NQL-DsTgTpX-.js} +1 -1
  74. package/dist/client/assets/{pieDiagram-4H26LBE5-DVIFbPjH.js → pieDiagram-4H26LBE5-BUQsNWHN.js} +1 -1
  75. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-C8qPRe_x.js → quadrantDiagram-W4KKPZXB-DOYWgfAz.js} +1 -1
  76. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DQ-pwXh3.js → requirementDiagram-4Y6WPE33-DUnPKaMd.js} +1 -1
  77. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-BnghLuUc.js → sankeyDiagram-5OEKKPKP-Btl4NRo-.js} +1 -1
  78. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-ChlGxGVk.js → sequenceDiagram-3UESZ5HK-b0hyJ_Wr.js} +1 -1
  79. package/dist/client/assets/{shield-alert-BfOnDO6O.js → shield-alert-yWvC7t2v.js} +1 -1
  80. package/dist/client/assets/{stateDiagram-AJRCARHV-ByUvNEch.js → stateDiagram-AJRCARHV-orREjkd9.js} +1 -1
  81. package/dist/client/assets/{stateDiagram-v2-BHNVJYJU-Df38ITSP.js → stateDiagram-v2-BHNVJYJU-DYJRRNh-.js} +1 -1
  82. package/dist/client/assets/{timeline-definition-PNZ67QCA-bsAGc83D.js → timeline-definition-PNZ67QCA-DZga7lqO.js} +1 -1
  83. package/dist/client/assets/{upload-BK1crTqn.js → upload-BsYRkl_8.js} +1 -1
  84. package/dist/client/assets/{users-DW8jC2Z9.js → users-dyoJEvrM.js} +1 -1
  85. package/dist/client/assets/{vennDiagram-CIIHVFJN-BBIj6LFN.js → vennDiagram-CIIHVFJN-C2P26V71.js} +1 -1
  86. package/dist/client/assets/{wardley-L42UT6IY-De6JFroy.js → wardley-L42UT6IY-ckctH9bL.js} +1 -1
  87. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-P1LOLx0_.js → wardleyDiagram-YWT4CUSO-9It3QAYs.js} +1 -1
  88. package/dist/client/assets/{xychartDiagram-2RQKCTM6-v1G4ki6U.js → xychartDiagram-2RQKCTM6-Dj3sSBqg.js} +1 -1
  89. package/dist/client/index.html +2 -2
  90. package/dist/client/version.json +1 -1
  91. package/dist/droid-cli/package.json +1 -1
  92. package/dist/extension.js +584 -296
  93. package/dist/pi-claude-cli/package.json +1 -1
  94. package/dist/plugin-sdk/index.d.ts +6 -0
  95. package/dist/plugins/fusion-plugin-acp-runtime/package.json +1 -1
  96. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +1 -1
  97. package/dist/plugins/fusion-plugin-compound-engineering/bundled.js +1 -1
  98. package/dist/plugins/fusion-plugin-compound-engineering/manifest.json +1 -1
  99. package/dist/plugins/fusion-plugin-compound-engineering/package.json +1 -1
  100. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  101. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  102. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  103. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  104. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  105. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  106. package/dist/plugins/fusion-plugin-reports/package.json +1 -1
  107. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  108. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  109. package/package.json +1 -1
  110. package/skill/fusion/references/best-practices.md +3 -3
  111. package/skill/fusion/references/extension-tools.md +1 -1
  112. package/skill/fusion/references/fusion-capabilities.md +1 -1
  113. package/skill/fusion/workflows/task-management.md +1 -1
  114. package/dist/client/assets/ChatView-DpgpxHva.js +0 -19
  115. package/dist/client/assets/PullRequestView-CpdE3Yo_.css +0 -1
  116. package/dist/client/assets/PullRequestView-D0WgwdAi.js +0 -6
  117. package/dist/client/assets/SettingsModal-B4WANQnu.js +0 -21
  118. package/dist/client/assets/channel-Q2WKocEP.js +0 -1
  119. package/dist/client/assets/classDiagram-4FO5ZUOK-CPFl2Aqd.js +0 -1
  120. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CPFl2Aqd.js +0 -1
  121. package/dist/client/assets/index-B9jtBwFy.css +0 -1
  122. package/dist/client/assets/index-DFeBJ3GM.js +0 -1302
package/dist/bin.js CHANGED
@@ -566,6 +566,20 @@ var init_mcp_config = __esm({
566
566
  }
567
567
  });
568
568
 
569
+ // ../core/src/task-helpers.ts
570
+ function getPrimaryPrInfo(task) {
571
+ return task.prInfos?.[0] ?? task.prInfo;
572
+ }
573
+ function taskHasManualOpenPullRequest(task) {
574
+ const prs = task.prInfos ?? (task.prInfo ? [task.prInfo] : []);
575
+ return prs.some((pr2) => pr2.manual === true && (pr2.status === "open" || pr2.status === "draft"));
576
+ }
577
+ var init_task_helpers = __esm({
578
+ "../core/src/task-helpers.ts"() {
579
+ "use strict";
580
+ }
581
+ });
582
+
569
583
  // ../core/src/task-merge.ts
570
584
  function isFusionSiblingBranch(branch) {
571
585
  return FUSION_SIBLING_BRANCH_RE.test(branch);
@@ -574,7 +588,7 @@ function resolveEffectiveAutoMerge(task, settings) {
574
588
  return task.autoMerge ?? settings.autoMerge;
575
589
  }
576
590
  function allowsAutoMergeProcessing(task, settings) {
577
- return settings.autoMerge !== false || task.autoMerge === true;
591
+ return (settings.autoMerge !== false || task.autoMerge === true) && !taskHasManualOpenPullRequest(task);
578
592
  }
579
593
  function resolveEffectiveGroupAutoMerge(group, settings) {
580
594
  return group.autoMerge ?? settings.autoMerge;
@@ -690,6 +704,7 @@ var FUSION_SIBLING_BRANCH_RE, HARD_BLOCKING_TASK_STATUSES, SCHEDULER_TRANSIENT_S
690
704
  var init_task_merge = __esm({
691
705
  "../core/src/task-merge.ts"() {
692
706
  "use strict";
707
+ init_task_helpers();
693
708
  FUSION_SIBLING_BRANCH_RE = /^fusion\/fn-/i;
694
709
  HARD_BLOCKING_TASK_STATUSES = /* @__PURE__ */ new Set([
695
710
  "failed",
@@ -1246,6 +1261,8 @@ var init_settings_schema = __esm({
1246
1261
  testCommand: void 0,
1247
1262
  buildCommand: void 0,
1248
1263
  recycleWorktrees: false,
1264
+ showWorktreeGrouping: false,
1265
+ openTasksInRightSidebar: false,
1249
1266
  executorAllowSiblingBranchRename: false,
1250
1267
  worktreeNaming: "random",
1251
1268
  worktrunk: {
@@ -1413,6 +1430,8 @@ var init_settings_schema = __esm({
1413
1430
  titleSummarizerModelId: void 0,
1414
1431
  titleSummarizerFallbackProvider: void 0,
1415
1432
  titleSummarizerFallbackModelId: void 0,
1433
+ prTitlePromptInstructions: void 0,
1434
+ prDescriptionPromptInstructions: void 0,
1416
1435
  scripts: void 0,
1417
1436
  setupScript: void 0,
1418
1437
  insightExtractionEnabled: false,
@@ -4613,19 +4632,21 @@ The tool prevents your session from being killed by the inactivity watchdog duri
4613
4632
  - Run **workspace-scoped non-test gates** (\`pnpm lint\`, \`pnpm build\`, and typecheck commands from root) when required for completion, but keep test verification targeted unless explicit task/workflow instructions require a full run.
4614
4633
  - If you need to run \`pnpm install\` (e.g. you added a new package), use \`fn_run_verification\` with \`scope: "workspace"\` and \`timeoutSec: 600\`.
4615
4634
  - If a verification command times out, do NOT blindly retry \u2014 investigate. Check for hung subprocesses, infinite test loops, or tests waiting on missing dependencies. Use \`node_modules/.modules.yaml\` presence to confirm bootstrap.`;
4616
- FAST_TRIAGE_PROMPT_TEXT = `You are a task specification agent for "fn", an AI-orchestrated task board. This task is running in **fast mode** \u2014 produce a lean, executable PROMPT.md without heavyweight review scoring or subtask analysis.
4635
+ FAST_TRIAGE_PROMPT_TEXT = `You are a task specification agent for "fn". This task is running in **fast mode**.
4617
4636
 
4618
- ## Your Role
4619
- You are a fast-path spec writer. Keep output lean but executable, with enough precision that an executor can run immediately.
4620
-
4621
- Your job: turn a rough task description into a focused PROMPT.md another agent can execute autonomously.
4637
+ Write a lean, executable PROMPT.md quickly. Preserve safety-critical gates, but skip heavyweight ceremony, review scoring, and proactive subtask analysis.
4622
4638
 
4623
- ## What you produce
4624
- Write a complete PROMPT.md specification to the given path using the write tool.
4639
+ ## Fast-mode priorities
4640
+ - Read only the source/docs needed to make the spec precise.
4641
+ - Keep prose brief; use concrete file paths, commands, and expected outcomes.
4642
+ - Do not expand scope. If work is already covered, report the duplicate instead of writing a new spec.
4643
+ - Keep the selected workflow unchanged unless the user explicitly requested a workflow for this task.
4625
4644
 
4626
- ## PROMPT.md Format
4645
+ ## Duplicate check
4646
+ Before writing a spec, call \`fn_task_list\` for active work, then call \`fn_task_search\` with 2-4 targeted keyword phrases from the title/description, such as file paths, symptoms, and symbols. For any likely match in \`done\` or \`archived\`, call \`fn_task_show\` and inspect it before deciding. If an existing task covers the same work, do not write PROMPT.md; write exactly \`DUPLICATE: {existing-task-id}\`.
4627
4647
 
4628
- Follow this structure exactly:
4648
+ ## Required PROMPT.md shape
4649
+ Write a real PROMPT.md to the requested path using the write tool:
4629
4650
 
4630
4651
  \`\`\`markdown
4631
4652
  # Task: {ID} - {Name}
@@ -4634,205 +4655,69 @@ Follow this structure exactly:
4634
4655
  **Size:** {S | M}
4635
4656
 
4636
4657
  ## Mission
4637
-
4638
- {One paragraph: what to build and why it matters}
4639
-
4640
- ## Surface Enumeration
4641
-
4642
- {Required for bug-fix tasks and UI-affordance add/remove tasks (adding, removing, or restructuring icons, buttons, chevrons/arrows, toggles, badges, menu entries, click targets): a checklist enumerating every surface the fixed invariant must hold across. Include every provider/bridge for streaming and agent paths; desktop AND mobile breakpoints; empty/undefined/duplicate/populated data states; and every hook/component/module that shares the affected logic. For UI-affordance add/remove tasks, enumerate every component that renders the affordance by searching the codebase for the icon/class/testid \u2014 not just the component the user pointed at. Explicitly check for leftover shells after removal (empty buttons, orphaned click targets, now-unused wrappers, dangling aria-labels) across both desktop and mobile breakpoints. Use the canonical checklist in docs/testing.md as the starting point.}
4643
-
4644
- ## Symptom Verification
4645
-
4646
- {Required for bug-class/bug-fix tasks only; feature/docs/non-bug tasks do not need this section. Use the exact heading \`## Symptom Verification\` and include: (1) **Original symptom** \u2014 what the user/issue reported was broken; (2) **Exact reproduction** \u2014 the precise steps, inputs, fixture, or automated repro that triggered the failure; (3) **Assertion it is gone** \u2014 the executor's final verification must reproduce that original failure condition and assert it no longer occurs via a real automated test. Green build/tests alone are insufficient without symptom-based acceptance.}
4658
+ {One short paragraph describing the outcome and why it matters.}
4647
4659
 
4648
4660
  ## Dependencies
4649
-
4650
4661
  - **None**
4651
- {OR}
4652
- - **Task:** {ID} ({what must be complete first})
4662
+ {or list concrete task dependencies after inspecting them with fn_task_show.}
4653
4663
 
4654
4664
  ## Context to Read First
4655
-
4656
- {List the minimal, specific files needed for implementation}
4665
+ - \`specific/file-or-doc.md\` \u2014 why it matters
4657
4666
 
4658
4667
  ## File Scope
4659
-
4660
- {List exact files/directories expected to change}
4661
-
4662
- - \`path/to/file.ext\`
4663
- - \`path/to/directory/*\`
4668
+ - \`expected/file/or/directory\`
4664
4669
 
4665
4670
  ## Steps
4666
-
4667
- > Optional: a step heading may carry a \`(depends: N,M)\` annotation listing the 1-indexed
4668
- > step numbers it depends on \u2014 e.g. \`### Step 3 (depends: 1): Title\`. Annotate ONLY steps
4669
- > that are genuinely independent of their immediate predecessor; an unannotated step is
4670
- > assumed to depend on the one before it (fully sequential). Be conservative \u2014 only mark a
4671
- > step independent when it truly does not read or modify the prior step's output.
4672
-
4673
4671
  ### Step 0: Preflight
4672
+ - [ ] Confirm required context, dependencies, and paths.
4674
4673
 
4675
- - [ ] Required files and paths exist
4676
- - [ ] Dependencies satisfied
4677
-
4678
- ### Step 1: {Implementation step name}
4674
+ ### Step 1: {Outcome-oriented implementation step}
4675
+ - [ ] {Specific verifiable outcome.}
4676
+ - [ ] {Specific verifiable outcome.}
4679
4677
 
4680
- - [ ] {Specific, verifiable outcome}
4681
- - [ ] {Specific, verifiable outcome}
4682
- - [ ] Run targeted tests for changed files, asserting the invariant across all known surfaces (enumerate every provider/bridge, desktop + mobile breakpoints, and empty/undefined/populated data states)
4678
+ ### Step N-1: Testing & Verification
4679
+ - [ ] Run targeted real automated tests in the project test runner; typecheck/build/manual checks do not replace tests.
4680
+ - [ ] Run lint/typecheck/build commands required by the task or repo.
4683
4681
 
4684
- For bug-fix and UI-affordance add/remove tasks, paste and fill in this checklist in the \`## Surface Enumeration\` section:
4685
- - [ ] Providers / bridges / execution paths touched by the invariant
4686
- - [ ] Desktop + mobile breakpoints / platforms that exercise the behavior
4687
- - [ ] Empty / undefined / duplicate / populated data states
4688
- - [ ] Shared hooks / components / modules / helpers reusing the logic
4689
- - [ ] Every component that renders the affordance (search the codebase for the icon/class/testid, not just the one the user pointed at)
4690
- - [ ] Leftover shells after removal \u2014 empty buttons, orphaned click targets, now-unused wrappers, dangling aria-labels \u2014 are explicitly checked and fixed/hidden
4691
-
4692
- For bug-class/bug-fix tasks, add and fill in the exact \`## Symptom Verification\` section:
4693
- - [ ] **Original symptom** \u2014 what the user/issue reported was broken
4694
- - [ ] **Exact reproduction** \u2014 the precise steps, inputs, fixture, or automated repro that triggered the failure
4695
- - [ ] **Assertion it is gone** \u2014 final verification reproduces the original failure condition and asserts it no longer occurs via a real automated test; green build/tests alone are insufficient
4696
-
4697
- **Artifacts:**
4698
- - \`path/to/file\` (new | modified)
4699
-
4700
- ### Step {N-1}: Testing & Verification
4701
-
4702
- > ZERO failures allowed for checks required by this task's quality gates. Run impacted/package-scoped verification first. Do NOT run the full workspace test suite (\`pnpm test:full\`, \`pnpm verify:workspace\`, or whole-package \`pnpm --filter <pkg> test\`) as routine or final-integration verification \u2014 a full run is allowed ONLY when the task or workflow explicitly requires it.
4703
- > If keeping lint/tests/build/typecheck green requires edits outside the initial File Scope, make those fixes as part of this task.
4704
-
4705
- - [ ] Run lint check (\`pnpm lint\`)
4706
- - [ ] Run impacted tests
4707
- - [ ] Run project typecheck if available
4708
- - [ ] Build passes
4709
-
4710
- ### Step {N}: Documentation & Delivery
4711
-
4712
- - [ ] Update relevant documentation
4713
- - [ ] Save documentation deliverables as task documents via \`fn_task_document_write\` (key="docs", content=...)
4714
- - [ ] Create out-of-scope follow-up tasks via \`fn_task_create\` when needed
4682
+ ### Step N: Documentation & Delivery
4683
+ - [ ] Update required docs/changesets/task documents.
4684
+ - [ ] Create follow-up tasks for out-of-scope findings.
4715
4685
 
4716
4686
  ## Documentation Requirements
4717
-
4718
4687
  **Must Update:**
4719
- - \`path/to/doc.md\` \u2014 {what to add/change}
4688
+ - \`path/to/doc-or-changeset.md\` \u2014 required update, or **None**
4720
4689
 
4721
4690
  **Check If Affected:**
4722
- - \`path/to/doc.md\` \u2014 {update if relevant}
4691
+ - \`path/to/doc.md\` \u2014 update if impacted
4723
4692
 
4724
4693
  ## Completion Criteria
4725
-
4726
- - [ ] All steps complete
4727
- - [ ] Lint passing
4728
- - [ ] All tests passing
4729
- - [ ] Typecheck passing (if available)
4730
- - [ ] Documentation updated
4694
+ - [ ] Implementation outcomes complete
4695
+ - [ ] Required automated tests, lint/typecheck/build pass
4696
+ - [ ] Documentation/delivery complete
4731
4697
 
4732
4698
  ## Git Commit Convention
4733
-
4734
- Commits at step boundaries. All commits include the task ID:
4735
-
4736
- - **Step completion:** \`feat({ID}): complete Step N \u2014 <short summary>\` (the \`<short summary>\` is required \u2014 use a concrete 5\u201310 word description)
4737
- - **Bug fixes:** \`fix({ID}): description\` (short, concrete summary required)
4738
- - **Tests:** \`test({ID}): description\` (short, concrete summary required)
4739
-
4740
- Good examples:
4741
- - \`feat(FN-1234): complete Step 2 \u2014 add retry guard for workflow step timeouts\`
4742
- - \`test(FN-1234): add regression tests for paused-session cleanup\`
4743
-
4744
- Bad example:
4745
- - \`feat(FN-1234): complete Step 2\`
4699
+ - Use task ID-prefixed conventional commits, e.g. \`feat({ID}): complete Step 1 \u2014 concrete summary\`.
4746
4700
 
4747
4701
  ## Do NOT
4748
-
4749
- - Expand task scope
4750
- - Skip tests
4751
- - Refuse necessary fixes just because they touch files outside the initial File Scope
4752
- - Commit without the task ID prefix
4753
- - Remove, delete, or gut modules, settings, interfaces, exports, or test files outside the File Scope
4754
- - Remove features as "cleanup" \u2014 if something seems unused, create a task via \`fn_task_create\`
4755
-
4756
- ## Changeset Requirements
4757
-
4758
- If this task REMOVES existing functionality (deleting modules, settings, API endpoints, or exports), a changeset file is REQUIRED:
4759
- - Create \`.changeset/{task-id}-removal.md\` explaining what was removed and why
4760
- - This is mandatory for any net-negative change (more deletions than additions to existing files)
4702
+ - Expand scope, skip tests, weaken acceptance, or delete/gut unrelated modules/features.
4761
4703
  \`\`\`
4762
4704
 
4763
- ## Testing requirements
4764
- - Require real automated tests with assertions that run in the project's test runner
4765
- - Typecheck/build/manual checks are not tests and cannot replace tests
4766
- - For bug fixes and UI-affordance add/remove tasks, the spec MUST include a \`## Surface Enumeration\` section. During self-review via \`fn_review_spec()\`, treat a missing section on a bug-fix or UI-affordance add/remove spec as a blocking REVISE.
4767
- - For bug fixes and UI-affordance add/remove tasks, populate \`## Surface Enumeration\` with this checklist from \`docs/testing.md\`: providers/bridges/execution paths; desktop + mobile breakpoints/platforms; empty/undefined/duplicate/populated data states; shared hooks/components/modules/helpers; every component that renders the affordance; leftover shells after removal.
4768
- - For bug fixes and UI-affordance add/remove tasks, regression tests must assert the invariant across all known surfaces \u2014 enumerate every provider/bridge, desktop + mobile breakpoints, empty/undefined/populated data states, and for UI-affordance changes every component rendering the affordance plus leftover shells after removal \u2014 not just the reported repro (see FN-5787/FN-5789/FN-5803, FN-5751, and FN-6115/FN-6118/FN-6123)
4769
- - For bug-class/bug-fix tasks, the spec MUST include a \`## Symptom Verification\` section with **Original symptom**, **Exact reproduction**, and **Assertion it is gone**. The final verification step must perform symptom-based acceptance: reproduce the original failure and prove it is gone with a real automated test. Green build/tests alone are insufficient. Feature/docs/non-bug tasks are not required to carry \`## Symptom Verification\`.
4770
- - Include targeted tests in implementation steps and bounded/changed-scoped quality-gate runs in final verification; never run the full workspace test suite unless the task explicitly requires it
4705
+ ## Bug and UI-affordance gates
4706
+ - For bug-fix or UI-affordance add/remove tasks, include \`## Surface Enumeration\` and tersely enumerate affected surfaces: providers/bridges, desktop/mobile, empty/undefined/duplicate/populated states, shared hooks/components/modules, all affordance renderers, and leftover shells after removal.
4707
+ - For bug-class tasks, include \`## Symptom Verification\` with **Original symptom**, **Exact reproduction**, and **Assertion it is gone**; final verification must reproduce the original failure and assert it no longer occurs with a real automated test.
4771
4708
 
4772
- ## Duplicate check
4773
- Before writing a spec, call \`fn_task_list\` to find existing active tasks, then call \`fn_task_search\` with 2-4 distinct keyword phrases from the task title and description (for example file paths, error symptoms, and symbol names).
4774
- For any likely match in \`done\` or \`archived\`, call \`fn_task_show\` to inspect details before deciding.
4775
- If an existing task already covers the same work, do NOT write a PROMPT.md. Instead write exactly:
4776
- \`DUPLICATE: {existing-task-id}\`
4777
-
4778
- ## Dependency awareness
4779
- When adding a dependency in \`## Dependencies\`, first call \`fn_task_show\` for that task and read its PROMPT.md.
4780
- Use that context to align file paths, APIs, assumptions, and completion expectations. If the dependency has no PROMPT.md yet, note that explicitly.
4781
-
4782
- ## Decision-only task flag (noCommitsExpected)
4783
- When ALL of the following are true, include this metadata line in the header block after Size:
4784
-
4785
- - Add this exact line: **No commits expected:** true
4786
-
4787
- Set it only when all of these conditions hold:
4788
- - Title/mission starts with decision verbs like "Decide", "Evaluate", "Verify", "Confirm", "Audit", "Review whether", or "Investigate and report", OR is an operational routing/coordination task whose only outcome is assigning/routing existing work or recording an intentional no-route/no-owner decision
4789
- - Acceptance criteria are strictly observational (record findings, routing evidence, no-route/no-owner state, log a decision, update task log/docs) with no required code/config/file mutations
4790
- - Task description explicitly says things like "no code changes expected", "no source files expected", "no product-source changes", or "the deliverable is the recorded decision"
4791
-
4792
- Anti-heuristics (bias to false-negative when ambiguous):
4793
- - SET: Decide whether FN-XYZ needs a fix
4794
- - SET: Assign ready implementation task to active owner, or record no-route state (no source files expected)
4795
- - LEAVE UNSET: Investigate FN-XYZ
4796
- - LEAVE UNSET: Investigate FN-XYZ and fix if needed
4797
- - LEAVE UNSET: Investigate and fix routing if needed
4798
-
4799
- If an executor later proves an ordinary implementation task is already satisfied on HEAD, it may close without fabricating a commit by calling \`fn_task_done\` with a leading verified no-op/duplicate sentinel summary: \`PREMISE STALE:\`, \`NO-OP:\`, \`NOOP:\`, \`DUPLICATE: FN-NNNN ...\`, or \`REDUNDANT:\`. This does not weaken ordinary tasks: zero-commit completions without one of these leading sentinels still fail the no-commits invariant.
4800
-
4801
- ## Guidelines
4802
- - Read relevant source files before writing the spec
4803
- - Be specific: reference concrete files, modules, and commands from this repo
4804
- - Keep steps outcome-focused with 2\u20134 checkboxes per step
4805
- - Keep file scope realistic: include tests and integration touchpoints likely required for green quality gates
4806
- - Always include Testing & Verification and Documentation & Delivery steps
4807
- - Keep fast-mode scope lean and executable; do not add heavyweight review scoring or subtask-analysis sections
4808
-
4809
- ## Project commands
4810
- When the user prompt includes explicit test/build commands, use those exact commands in the generated spec.
4811
-
4812
- <!--
4813
- FNXC:WorkflowRouting 2026-06-22-17:24:
4814
- Fast triage must keep tasks on the project default workflow unless the user explicitly asked for a specific workflow or the agent created the task. The no-commits header remains a PROMPT.md marker only; it is not permission to select a lightweight workflow automatically.
4815
- -->
4816
- ## Workflow Routing
4817
- Keep the project default workflow (\`builtin:coding\`) unless the user explicitly requested a specific workflow for this task or subtask, or you created that task yourself. Do NOT call \`fn_workflow_select\` or pass \`workflow_id\` to \`fn_task_create\` just because a task looks like investigation, audit, research, coordination, decision-only work, or coding work. If the user explicitly asks for a workflow, call \`fn_workflow_list\` to discover valid IDs, then use \`fn_workflow_select\` for the current task or pass \`workflow_id\` to \`fn_task_create\` for the requested subtask. When you create a task via \`fn_task_create\` or \`fn_delegate_task\`, you may select that created task's workflow with \`workflow_id\` at create time or \`fn_workflow_select\` afterward; do not move a task you did not create unless the user asked. For investigation/audit/research, operational routing/coordination, or decision-only tasks that meet the no-commits criteria above, still include \`**No commits expected:** true\` in the PROMPT.md header when appropriate; that header marker does not change the workflow.
4818
-
4819
- ## Task Artifact Location for Forensic / Reconciliation Tasks
4820
-
4821
- For audit/forensic/historical reconciliation tasks that target a different task ID, explicitly state in generated PROMPT.md context/scope that authoritative artifacts and DB state are at project root, not the worktree.
4822
- - Target-task files live at \`<rootDir>/.fusion/tasks/{TARGET_ID}/\` (\`task.json\`, \`PROMPT.md\`, \`attachments/\`, logs).
4823
- - Task DB truth lives at \`<rootDir>/.fusion/fusion.db\` (SQLite/WAL) and should be accessed via \`TaskStore\`/task tools, not direct SQL edits.
4824
- - \`.fusion/\` is gitignored: fresh worktrees from \`main\` do not contain other tasks' \`.fusion/tasks/{TARGET_ID}/\` or \`.fusion/fusion.db\`; worktree-local \`.fusion/\` is running-task scratch/session state only.
4825
-
4826
- ## Spec Review
4827
-
4828
- After writing the PROMPT.md, call \`fn_review_spec()\` to confirm the spec.
4829
-
4830
- Fast-mode specs are auto-approved \u2014 the review tool will return APPROVE immediately without spawning an independent reviewer. You do NOT need to wait for or iterate on review feedback.
4709
+ ## Testing requirements
4710
+ - Every implementation spec needs a Testing & Verification step with real automated tests and bounded commands.
4711
+ - Prefer changed-file/package-scoped tests; do not require full workspace suites unless the user/task explicitly requires them.
4712
+ - If the user supplied exact test/build commands, include those exact commands.
4831
4713
 
4832
- Never reference a \`.fusion/tasks/<id>/<file>\` artifact in Context, Steps, or File Scope unless (a) the file already exists, (b) the step explicitly creates it (listed as \`(new)\` under Artifacts), or (c) it is \`PROMPT.md\` / \`task.json\` / \`attachments/*\` for a sibling task. Save planning scratch as task documents via \`fn_task_document_write\`, not as files on disk.
4714
+ ## Safety and workflow rules
4715
+ - Never kill port 4040; use a random/free port for test servers.
4716
+ - Do not call workflow-selection tools unless the user explicitly requested a workflow for this task, or you are selecting a workflow for a task you created.
4717
+ - Do not cite task-local \`.fusion/tasks/<id>/<file>\` paths unless the file exists, is PROMPT.md/task.json/attachments for a sibling task, or the spec creates it. Save scratch notes with \`fn_task_document_write\`.
4833
4718
 
4834
4719
  ## Output
4835
- Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\` to confirm.`;
4720
+ Write PROMPT.md directly, then call \`fn_review_spec()\`. Fast-mode specs auto-approve, but the call is still required.`;
4836
4721
  TRIAGE_PROMPT_TEXT = `You are a task specification agent for "fn", an AI-orchestrated task board.
4837
4722
 
4838
4723
  ## Your Role
@@ -10725,6 +10610,18 @@ function defaultColumnForLinearNode(node) {
10725
10610
  if (seam === "merge") return "in-review";
10726
10611
  return "todo";
10727
10612
  }
10613
+ function canonicalBuiltinWorkflowColumns() {
10614
+ if (BUILTIN_CODING_WORKFLOW_IR.version !== "v2") {
10615
+ throw new Error("builtin coding workflow must be v2 to provide canonical columns");
10616
+ }
10617
+ return BUILTIN_CODING_WORKFLOW_IR.columns.map((column) => ({
10618
+ ...column,
10619
+ traits: column.traits.map((trait) => ({
10620
+ ...trait,
10621
+ config: trait.config ? { ...trait.config } : void 0
10622
+ }))
10623
+ }));
10624
+ }
10728
10625
  function linear(spec) {
10729
10626
  const nodes = [
10730
10627
  { id: "start", kind: "start" },
@@ -10744,14 +10641,16 @@ function linear(spec) {
10744
10641
  nodes.forEach((node, i) => {
10745
10642
  layout[node.id] = { x: 60 + i * 170, y: 160 };
10746
10643
  });
10747
- const hasV2OnlyNode = nodes.some((node) => !V1_LINEAR_NODE_KINDS.has(node.kind));
10748
- const ir2 = hasV2OnlyNode ? parseWorkflowIr({
10644
+ const ir2 = parseWorkflowIr({
10749
10645
  version: "v2",
10750
10646
  name: spec.name,
10751
- columns: DEFAULT_WORKFLOW_COLUMN_IDS.map((id) => ({ id, name: id, traits: [] })),
10647
+ columns: canonicalBuiltinWorkflowColumns(),
10752
10648
  nodes: nodes.map((node) => node.column ? node : { ...node, column: defaultColumnForLinearNode(node) }),
10753
10649
  edges
10754
- }) : parseWorkflowIr({ version: "v1", name: spec.name, nodes, edges });
10650
+ });
10651
+ if (ir2.version !== "v2" || !ir2.columns.find((column) => column.id === "todo")?.traits.some((trait) => trait.trait === "hold")) {
10652
+ throw new Error(`linear built-in workflow '${spec.id}' must synthesize a hold-capacity todo column`);
10653
+ }
10755
10654
  if (ir2.version === "v2") {
10756
10655
  ir2.settings = BUILTIN_WORKFLOW_SETTINGS;
10757
10656
  }
@@ -10771,7 +10670,7 @@ function linear(spec) {
10771
10670
  function getBuiltinWorkflow(id) {
10772
10671
  return BUILTIN_BY_ID.get(id);
10773
10672
  }
10774
- var BUILTIN_WORKFLOW_ID_PREFIX, PLUGIN_GATED_BUILTIN_WORKFLOWS, BUILTIN_TS, V1_LINEAR_NODE_KINDS, BUILTIN_WORKFLOWS, BUILTIN_BY_ID;
10673
+ var BUILTIN_WORKFLOW_ID_PREFIX, PLUGIN_GATED_BUILTIN_WORKFLOWS, BUILTIN_TS, BUILTIN_WORKFLOWS, BUILTIN_BY_ID;
10775
10674
  var init_builtin_workflows = __esm({
10776
10675
  "../core/src/builtin-workflows.ts"() {
10777
10676
  "use strict";
@@ -10788,7 +10687,6 @@ var init_builtin_workflows = __esm({
10788
10687
  ["builtin:compound-engineering", "fusion-plugin-compound-engineering"]
10789
10688
  ]);
10790
10689
  BUILTIN_TS = "2026-01-01T00:00:00.000Z";
10791
- V1_LINEAR_NODE_KINDS = /* @__PURE__ */ new Set(["start", "prompt", "script", "gate", "end"]);
10792
10690
  BUILTIN_WORKFLOWS = [
10793
10691
  {
10794
10692
  id: "builtin:coding",
@@ -11302,6 +11200,21 @@ function setRunningAgentCountSource(fn2) {
11302
11200
  function getRunningAgentCountSource() {
11303
11201
  return runningAgentCountSource;
11304
11202
  }
11203
+ function isRunningAgentTask(task) {
11204
+ if (task.column === "in-progress") {
11205
+ return true;
11206
+ }
11207
+ if (task.column === "triage") {
11208
+ return task.status === "planning" && !task.paused;
11209
+ }
11210
+ if (task.column === "in-review") {
11211
+ return ACTIVE_IN_REVIEW_AGENT_STATUSES.has(String(task.status ?? "")) && !task.paused;
11212
+ }
11213
+ return false;
11214
+ }
11215
+ function countRunningAgentTasks(tasks) {
11216
+ return tasks.filter(isRunningAgentTask).length;
11217
+ }
11305
11218
  function deriveRunningAgentCounts(perProject) {
11306
11219
  const projectsActive = {};
11307
11220
  let currentlyActive = 0;
@@ -11314,10 +11227,11 @@ function deriveRunningAgentCounts(perProject) {
11314
11227
  }
11315
11228
  return { currentlyActive, projectsActive };
11316
11229
  }
11317
- var runningAgentCountSource;
11230
+ var ACTIVE_IN_REVIEW_AGENT_STATUSES, runningAgentCountSource;
11318
11231
  var init_live_agent_count = __esm({
11319
11232
  "../core/src/live-agent-count.ts"() {
11320
11233
  "use strict";
11234
+ ACTIVE_IN_REVIEW_AGENT_STATUSES = /* @__PURE__ */ new Set(["merging", "merging-pr", "merging-fix", "reviewing", "fixing"]);
11321
11235
  }
11322
11236
  });
11323
11237
 
@@ -47780,6 +47694,35 @@ var init_automation_store = __esm({
47780
47694
  return schedule;
47781
47695
  });
47782
47696
  }
47697
+ /**
47698
+ * Atomically claim one due schedule occurrence before execution.
47699
+ *
47700
+ * FNXC:Automations 2026-06-27-00:00:
47701
+ * Claiming advances nextRunAt before executing the schedule so concurrent CronRunner pollers, overlapping scopes, and separate engine processes sharing one SQLite DB cannot double-fire the same due window. The conditional UPDATE is the cross-process claim boundary; losers observe zero changed rows and skip execution.
47702
+ */
47703
+ async claimDueSchedule(id, expectedNextRunAt) {
47704
+ return this.withScheduleLock(id, async () => {
47705
+ const row = this.db.prepare(
47706
+ "SELECT id, cronExpression, enabled, nextRunAt FROM automations WHERE id = ?"
47707
+ ).get(id);
47708
+ if (!row || row.enabled !== 1 || !row.nextRunAt) {
47709
+ return false;
47710
+ }
47711
+ const nextRunAt = this.computeNextRun(row.cronExpression);
47712
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
47713
+ const result = this.db.prepare(`
47714
+ UPDATE automations
47715
+ SET nextRunAt = ?, updatedAt = ?
47716
+ WHERE id = ? AND enabled = 1 AND nextRunAt = ?
47717
+ `).run(nextRunAt, updatedAt, id, expectedNextRunAt);
47718
+ const changes = typeof result.changes === "bigint" ? Number(result.changes) : result.changes;
47719
+ if (changes === 1) {
47720
+ this.db.bumpLastModified();
47721
+ return true;
47722
+ }
47723
+ return false;
47724
+ });
47725
+ }
47783
47726
  /**
47784
47727
  * Record a run result for a schedule. Updates lastRunAt, lastRunResult,
47785
47728
  * nextRunAt, runCount, and appends to runHistory.
@@ -54602,20 +54545,10 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
54602
54545
  throw new Error("Feedback is required and cannot be empty");
54603
54546
  }
54604
54547
  const now = (/* @__PURE__ */ new Date()).toISOString();
54605
- let sourceLabel;
54606
- if (sourceTask.title?.trim()) {
54607
- sourceLabel = sourceTask.title.trim();
54608
- } else {
54609
- const firstLine = sourceTask.description.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
54610
- sourceLabel = firstLine ? firstLine.replace(/\s+/g, " ") : sourceTask.id;
54611
- }
54548
+ const normalizedFeedback = feedback.trim().replace(/\s+/g, " ");
54549
+ const refinementTitle = `${id}: ${normalizedFeedback}`.slice(0, MAX_TITLE_LENGTH).trim();
54612
54550
  return this.createTaskWithDistributedReservation({ description: feedback.trim() }, {
54613
54551
  createTaskWithId: async (newId, reservationCommit) => {
54614
- const normalizedTitle = normalizeTitleForTaskId(`Refinement: ${sourceLabel}`, newId);
54615
- if (normalizedTitle.changed) {
54616
- const removed = extractTaskIdTokens(`Refinement: ${sourceLabel}`).filter((token) => token !== newId.toUpperCase());
54617
- storeLog.log(`[title-id-drift] normalized title for ${newId}: removed=[${removed.join(",")}]`);
54618
- }
54619
54552
  const sourceGithubLinked = sourceTask.githubTracking?.enabled === true || Boolean(sourceTask.githubTracking?.issue);
54620
54553
  const refinementGithubTracking = sourceGithubLinked ? {
54621
54554
  enabled: true,
@@ -54624,7 +54557,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
54624
54557
  const newTask = {
54625
54558
  id: newId,
54626
54559
  lineageId: generateTaskLineageId(),
54627
- title: normalizedTitle.title ?? "Refinement",
54560
+ title: refinementTitle,
54628
54561
  description: `${feedback.trim()}
54629
54562
 
54630
54563
  Refines: ${id}`,
@@ -60872,6 +60805,9 @@ ${newTask.description}
60872
60805
  /**
60873
60806
  * FNXC:ArtifactRegistry 2026-06-19-22:04:
60874
60807
  * Register multi-type agent/user/system artifacts in SQLite while writing binary payloads to disk. Task-scoped binaries use `.fusion/tasks/{taskId}/artifacts/`; task-less binaries use `.fusion/artifacts/`, and both store only a relative `artifacts/<file>` uri in the row.
60808
+ *
60809
+ * FNXC:ArtifactRegistry 2026-06-27-00:00:
60810
+ * Successful registry writes emit `artifact:registered` as the authoritative live-update signal. Dashboard inbox notifications remain best-effort discovery messages, so already-open artifact lists must not depend on message delivery to invalidate their SWR cache.
60875
60811
  */
60876
60812
  async registerArtifact(input) {
60877
60813
  const id = randomUUID15();
@@ -60899,7 +60835,9 @@ ${newTask.description}
60899
60835
  throw error2;
60900
60836
  }
60901
60837
  };
60902
- return input.taskId ? this.withTaskLock(input.taskId, register) : register();
60838
+ const artifact = input.taskId ? await this.withTaskLock(input.taskId, register) : await register();
60839
+ this.emit("artifact:registered", artifact);
60840
+ return artifact;
60903
60841
  }
60904
60842
  /**
60905
60843
  * FNXC:ArtifactRegistry 2026-06-19-22:04:
@@ -64156,6 +64094,51 @@ var init_retry_storm_error = __esm({
64156
64094
  });
64157
64095
 
64158
64096
  // ../core/src/agent-token-usage.ts
64097
+ function aggregateTaskTokenTotalsByAgentLink(db) {
64098
+ const rows = db.prepare(`
64099
+ SELECT
64100
+ id AS taskId,
64101
+ assignedAgentId,
64102
+ sourceAgentId,
64103
+ checkedOutBy,
64104
+ tokenUsageInputTokens AS inputTokens,
64105
+ tokenUsageCachedTokens AS cachedTokens,
64106
+ tokenUsageCacheWriteTokens AS cacheWriteTokens,
64107
+ tokenUsageOutputTokens AS outputTokens,
64108
+ tokenUsageTotalTokens AS totalTokens
64109
+ FROM tasks
64110
+ WHERE tokenUsageInputTokens IS NOT NULL
64111
+ OR tokenUsageCachedTokens IS NOT NULL
64112
+ OR tokenUsageCacheWriteTokens IS NOT NULL
64113
+ OR tokenUsageOutputTokens IS NOT NULL
64114
+ OR tokenUsageTotalTokens IS NOT NULL
64115
+ `).all();
64116
+ const totalsByAgentId = /* @__PURE__ */ new Map();
64117
+ for (const row of rows) {
64118
+ const agentIds = new Set([row.assignedAgentId, row.sourceAgentId, row.checkedOutBy].filter((value) => Boolean(value)));
64119
+ for (const agentId of agentIds) {
64120
+ const existing = totalsByAgentId.get(agentId) ?? createTaskTokenTotals();
64121
+ existing.inputTokens += row.inputTokens ?? 0;
64122
+ existing.cachedTokens += row.cachedTokens ?? 0;
64123
+ existing.cacheWriteTokens += row.cacheWriteTokens ?? 0;
64124
+ existing.outputTokens += row.outputTokens ?? 0;
64125
+ existing.totalTokens += row.totalTokens ?? (row.inputTokens ?? 0) + (row.cachedTokens ?? 0) + (row.cacheWriteTokens ?? 0) + (row.outputTokens ?? 0);
64126
+ existing.nTasks += 1;
64127
+ totalsByAgentId.set(agentId, existing);
64128
+ }
64129
+ }
64130
+ return totalsByAgentId;
64131
+ }
64132
+ function createTaskTokenTotals() {
64133
+ return {
64134
+ inputTokens: 0,
64135
+ cachedTokens: 0,
64136
+ cacheWriteTokens: 0,
64137
+ outputTokens: 0,
64138
+ totalTokens: 0,
64139
+ nTasks: 0
64140
+ };
64141
+ }
64159
64142
  async function aggregateAgentTokenUsage({
64160
64143
  taskStore,
64161
64144
  agentStore,
@@ -64163,7 +64146,7 @@ async function aggregateAgentTokenUsage({
64163
64146
  now = /* @__PURE__ */ new Date()
64164
64147
  }) {
64165
64148
  const agent = await agentStore.getAgent(agentId);
64166
- if (!agent || isEphemeralAgent(agent)) {
64149
+ if (!agent) {
64167
64150
  return null;
64168
64151
  }
64169
64152
  const tasks = await taskStore.listTasks({ slim: true, includeArchived: true });
@@ -64223,7 +64206,6 @@ function finalizeWindowSummary(summary) {
64223
64206
  var init_agent_token_usage = __esm({
64224
64207
  "../core/src/agent-token-usage.ts"() {
64225
64208
  "use strict";
64226
- init_types();
64227
64209
  }
64228
64210
  });
64229
64211
 
@@ -66137,16 +66119,6 @@ var init_otel_metrics = __esm({
66137
66119
  }
66138
66120
  });
66139
66121
 
66140
- // ../core/src/task-helpers.ts
66141
- function getPrimaryPrInfo(task) {
66142
- return task.prInfos?.[0] ?? task.prInfo;
66143
- }
66144
- var init_task_helpers = __esm({
66145
- "../core/src/task-helpers.ts"() {
66146
- "use strict";
66147
- }
66148
- });
66149
-
66150
66122
  // ../core/src/manual-retry-reset.ts
66151
66123
  function buildAutoPauseClearPatch(task) {
66152
66124
  if (task.paused === true && task.userPaused !== true && task.pausedReason === IN_REVIEW_STALL_DEADLOCK_PAUSE_REASON) {
@@ -88162,6 +88134,7 @@ __export(src_exports, {
88162
88134
  aggregatePluginActivations: () => aggregatePluginActivations,
88163
88135
  aggregateProductivityAnalytics: () => aggregateProductivityAnalytics,
88164
88136
  aggregateSignalsAnalytics: () => aggregateSignalsAnalytics,
88137
+ aggregateTaskTokenTotalsByAgentLink: () => aggregateTaskTokenTotalsByAgentLink,
88165
88138
  aggregateTeamAnalytics: () => aggregateTeamAnalytics,
88166
88139
  aggregateTokenAnalytics: () => aggregateTokenAnalytics,
88167
88140
  aggregateToolAnalytics: () => aggregateToolAnalytics,
@@ -88252,6 +88225,7 @@ __export(src_exports, {
88252
88225
  countDualAcceptDisagreements: () => countDualAcceptDisagreements,
88253
88226
  countInterventions: () => countInterventions,
88254
88227
  countRecentIdenticalStallEntries: () => countRecentIdenticalStallEntries,
88228
+ countRunningAgentTasks: () => countRunningAgentTasks,
88255
88229
  countUsageEventsBy: () => countUsageEventsBy,
88256
88230
  createActivityLogSnapshot: () => createActivityLogSnapshot,
88257
88231
  createAgentRunSnapshot: () => createAgentRunSnapshot,
@@ -88454,6 +88428,7 @@ __export(src_exports, {
88454
88428
  isResearchExperimentalEnabled: () => isResearchExperimentalEnabled,
88455
88429
  isRetryableMeshWriteFailure: () => isRetryableMeshWriteFailure,
88456
88430
  isRunRecord: () => isRunRecord,
88431
+ isRunningAgentTask: () => isRunningAgentTask,
88457
88432
  isSandboxExperimentalEnabled: () => isSandboxExperimentalEnabled,
88458
88433
  isSecretAccessPolicy: () => isSecretAccessPolicy,
88459
88434
  isSecretScope: () => isSecretScope,
@@ -88715,6 +88690,7 @@ __export(src_exports, {
88715
88690
  syncMemoryBackupRoutine: () => syncMemoryBackupRoutine,
88716
88691
  syncMemoryDreamsAutomation: () => syncMemoryDreamsAutomation,
88717
88692
  syncScheduledEvalBatchAutomation: () => syncScheduledEvalBatchAutomation,
88693
+ taskHasManualOpenPullRequest: () => taskHasManualOpenPullRequest,
88718
88694
  taskMatchesReplicatedCreate: () => taskMatchesReplicatedCreate,
88719
88695
  toJson: () => toJson,
88720
88696
  toJsonNullable: () => toJsonNullable,
@@ -88875,7 +88851,6 @@ var init_src = __esm({
88875
88851
  init_stalled_review_detector();
88876
88852
  init_task_id_integrity();
88877
88853
  init_task_title_id_drift();
88878
- init_task_helpers();
88879
88854
  init_manual_retry_reset();
88880
88855
  init_task_lineage();
88881
88856
  init_distributed_task_id();
@@ -88895,6 +88870,7 @@ var init_src = __esm({
88895
88870
  init_blocker_fanout();
88896
88871
  init_capacity();
88897
88872
  init_dependency_blocked_todo_report();
88873
+ init_task_helpers();
88898
88874
  init_task_merge();
88899
88875
  init_branch_group_completion();
88900
88876
  init_pr_entity();
@@ -90580,6 +90556,11 @@ var init_gating_classifications = __esm({
90580
90556
  "fn_task_done",
90581
90557
  "fn_heartbeat_done",
90582
90558
  "fn_memory_append",
90559
+ /**
90560
+ * FNXC:ToolGovernance 2026-06-28-00:00:
90561
+ * FN-7191 requires permanent agents to call fn_ask_question directly without an approval gate. The tool only posts a structured question to the user's inbox, so it has the same trust level as fn_send_message and must be positively recognized in both gate paths.
90562
+ */
90563
+ "fn_ask_question",
90583
90564
  "fn_send_message",
90584
90565
  "fn_read_messages",
90585
90566
  "fn_post_room_message",
@@ -90621,6 +90602,11 @@ var init_gating_classifications = __esm({
90621
90602
  "fn_agent_show",
90622
90603
  "fn_agent_org_chart",
90623
90604
  "fn_workflow_list",
90605
+ /**
90606
+ * FNXC:ToolGovernance 2026-06-28-00:00:
90607
+ * FN-7191 requires fn_ask_question to bypass permanent-agent approval gates like other user-messaging coordination tools; membership here makes the action gate classify it as exempt/allow even under locked-down policies.
90608
+ */
90609
+ "fn_ask_question",
90624
90610
  "fn_send_message",
90625
90611
  "fn_post_room_message",
90626
90612
  "fn_memory_append",
@@ -91508,7 +91494,7 @@ var init_hold_release = __esm({
91508
91494
 
91509
91495
  // ../engine/src/concurrency.ts
91510
91496
  function persistedTopLevelAgentSlots(tasks) {
91511
- return tasks.filter((task) => task.column === "in-progress" || task.column === "triage" && task.status === "planning" && !task.paused || task.column === "in-review" && ["merging", "reviewing", "fixing"].includes(String(task.status ?? ""))).length;
91497
+ return countRunningAgentTasks(tasks);
91512
91498
  }
91513
91499
  function recoverIdleSemaphoreLeakCandidate(params) {
91514
91500
  const {
@@ -91535,10 +91521,11 @@ function recoverIdleSemaphoreLeakCandidate(params) {
91535
91521
  reconciliation: semaphore.reconcileActiveCount(0)
91536
91522
  };
91537
91523
  }
91538
- var concurrencyLog, PRIORITY_MERGE, PRIORITY_EXECUTE, PRIORITY_SPECIFY, IDLE_SEMAPHORE_LEAK_REPAIR_MS, AgentSemaphore;
91524
+ var concurrencyLog, PRIORITY_MERGE, PRIORITY_EXECUTE, PRIORITY_SPECIFY, IDLE_SEMAPHORE_LEAK_REPAIR_MS, AgentSemaphore, ScopedAgentSemaphore;
91539
91525
  var init_concurrency = __esm({
91540
91526
  "../engine/src/concurrency.ts"() {
91541
91527
  "use strict";
91528
+ init_src();
91542
91529
  init_logger2();
91543
91530
  concurrencyLog = createLogger2("concurrency");
91544
91531
  PRIORITY_MERGE = 2;
@@ -91689,13 +91676,21 @@ var init_concurrency = __esm({
91689
91676
  * deadlock that would occur if both parent and child needed a queued slot.
91690
91677
  */
91691
91678
  async runNested(fn2) {
91692
- this._active++;
91679
+ this.acquireNestedSlot();
91693
91680
  try {
91694
91681
  return await fn2();
91695
91682
  } finally {
91696
- this.returnSlot("runNested");
91683
+ this.releaseNestedSlot();
91697
91684
  }
91698
91685
  }
91686
+ /** Reserve a nested helper-agent slot without queueing. */
91687
+ acquireNestedSlot() {
91688
+ this._active++;
91689
+ }
91690
+ /** Return a nested helper-agent slot. */
91691
+ releaseNestedSlot() {
91692
+ this.returnSlot("runNested");
91693
+ }
91699
91694
  /**
91700
91695
  * FNXC:Scheduler-Concurrency 2026-06-13-19:58:
91701
91696
  * FN-6423 requires excess slot returns to remain observable without corrupting scheduler capacity accounting. Clamp the active slot count at zero and warn once so a release leak cannot surface as negative `activeCount` or a negative `semaphore used=` diagnostic.
@@ -91744,6 +91739,95 @@ var init_concurrency = __esm({
91744
91739
  return bestIdx;
91745
91740
  }
91746
91741
  };
91742
+ ScopedAgentSemaphore = class extends AgentSemaphore {
91743
+ delegate;
91744
+ _held = 0;
91745
+ constructor(delegate) {
91746
+ super(() => delegate.limit);
91747
+ this.delegate = delegate;
91748
+ }
91749
+ /** Number of slots this scope currently owns in the delegated semaphore. */
91750
+ get heldCount() {
91751
+ return this._held;
91752
+ }
91753
+ /** Shared-pool active count, preserving scheduler/metrics semantics. */
91754
+ get activeCount() {
91755
+ return this.delegate.activeCount;
91756
+ }
91757
+ get waitingCount() {
91758
+ return this.delegate.waitingCount;
91759
+ }
91760
+ get availableCount() {
91761
+ return this.delegate.availableCount;
91762
+ }
91763
+ get limit() {
91764
+ return this.delegate.limit;
91765
+ }
91766
+ snapshot() {
91767
+ return this.delegate.snapshot();
91768
+ }
91769
+ reconcileActiveCount(maxActive) {
91770
+ const bounded = Math.max(0, Math.floor(maxActive));
91771
+ const before = this._held;
91772
+ if (before > bounded) {
91773
+ const returned = before - bounded;
91774
+ this._held = bounded;
91775
+ for (let i = 0; i < returned; i++) {
91776
+ this.delegate.release();
91777
+ }
91778
+ }
91779
+ return { before, after: this._held, changed: before !== this._held };
91780
+ }
91781
+ async acquire(priority = 0) {
91782
+ await this.delegate.acquire(priority);
91783
+ this._held++;
91784
+ }
91785
+ tryAcquire() {
91786
+ const acquired = this.delegate.tryAcquire();
91787
+ if (acquired) this._held++;
91788
+ return acquired;
91789
+ }
91790
+ release() {
91791
+ if (this._held <= 0) return;
91792
+ this._held--;
91793
+ this.delegate.release();
91794
+ }
91795
+ async run(fn2, priority = 0) {
91796
+ await this.acquire(priority);
91797
+ try {
91798
+ return await fn2();
91799
+ } finally {
91800
+ this.release();
91801
+ }
91802
+ }
91803
+ async runNested(fn2) {
91804
+ this.delegate.acquireNestedSlot();
91805
+ this._held++;
91806
+ try {
91807
+ return await fn2();
91808
+ } finally {
91809
+ if (this._held > 0) {
91810
+ this._held--;
91811
+ this.delegate.releaseNestedSlot();
91812
+ }
91813
+ }
91814
+ }
91815
+ /**
91816
+ * Return every slot still attributed to this scope.
91817
+ *
91818
+ * Late normal releases from already-aborted agents become no-ops because the
91819
+ * scope's held count is zeroed before returning residual slots to the pool.
91820
+ */
91821
+ returnAllHeldSlots() {
91822
+ const residual = this._held;
91823
+ if (residual <= 0) return 0;
91824
+ this._held = 0;
91825
+ for (let i = 0; i < residual; i++) {
91826
+ this.delegate.release();
91827
+ }
91828
+ return residual;
91829
+ }
91830
+ };
91747
91831
  }
91748
91832
  });
91749
91833
 
@@ -162550,7 +162634,7 @@ ${isRevision ? "1. Review the existing specification and user feedback carefully
162550
162634
 
162551
162635
  Use the write tool to write the specification file.${commandsSection}${completionDocumentationSection}${memorySection}${attachmentsSection}${userCommentsSection}`;
162552
162636
  }
162553
- var TriageProcessor, IMAGE_MIME_TYPES, TEXT_INLINE_LIMIT;
162637
+ var TRIAGE_STUCK_RESUME_LOG_ACTION, TRIAGE_STUCK_RESUME_FEEDBACK, TriageProcessor, IMAGE_MIME_TYPES, TEXT_INLINE_LIMIT;
162554
162638
  var init_triage = __esm({
162555
162639
  "../engine/src/triage.ts"() {
162556
162640
  "use strict";
@@ -162582,6 +162666,8 @@ var init_triage = __esm({
162582
162666
  init_run_audit();
162583
162667
  init_goal_injection_diagnostics();
162584
162668
  init_session_token_usage();
162669
+ TRIAGE_STUCK_RESUME_LOG_ACTION = "Triage stuck re-queue will resume existing planning draft";
162670
+ TRIAGE_STUCK_RESUME_FEEDBACK = "The previous triage session was killed by the stuck-task detector after writing a non-empty planning draft. Resume from the existing draft below: preserve useful structure and decisions, fill gaps, and continue toward review instead of restarting planning from scratch.";
162585
162671
  TriageProcessor = class _TriageProcessor {
162586
162672
  /**
162587
162673
  * @param store — Task store instance (also used to listen for `settings:updated` events)
@@ -162892,6 +162978,106 @@ var init_triage = __esm({
162892
162978
  });
162893
162979
  return true;
162894
162980
  }
162981
+ async readNonEmptyPromptDraft(taskId, context) {
162982
+ const promptPath = join49(this.rootDir, ".fusion", "tasks", taskId, "PROMPT.md");
162983
+ const written = await readFile22(promptPath, "utf-8").catch((err) => {
162984
+ const msg = err instanceof Error ? err.message : String(err);
162985
+ planLog.warn(`${taskId}: failed to read PROMPT.md during ${context} (${promptPath}): ${msg}`);
162986
+ return "";
162987
+ });
162988
+ return written.trim().length > 0 ? written : void 0;
162989
+ }
162990
+ async readNonEmptyPlanDocument(taskId, context) {
162991
+ const readTaskDocument = this.store.getTaskDocument;
162992
+ if (typeof readTaskDocument !== "function") {
162993
+ return void 0;
162994
+ }
162995
+ const document2 = await readTaskDocument.call(this.store, taskId, "plan").catch((err) => {
162996
+ const msg = err instanceof Error ? err.message : String(err);
162997
+ planLog.warn(`${taskId}: failed to read plan task document during ${context}: ${msg}`);
162998
+ return null;
162999
+ });
163000
+ const content = typeof document2?.content === "string" ? document2.content : "";
163001
+ return content.trim().length > 0 ? content : void 0;
163002
+ }
163003
+ async readNonEmptyPlanningDraft(taskId, context) {
163004
+ const promptDraft = await this.readNonEmptyPromptDraft(taskId, context);
163005
+ if (promptDraft) {
163006
+ return { content: promptDraft, source: "prompt" };
163007
+ }
163008
+ const planDocument = await this.readNonEmptyPlanDocument(taskId, context);
163009
+ return planDocument ? { content: planDocument, source: "plan-document" } : void 0;
163010
+ }
163011
+ async handleStuckAbortRequeue(task, context) {
163012
+ const freshTask = await this.store.getTask(task.id).catch((err) => {
163013
+ const msg = err instanceof Error ? err.message : String(err);
163014
+ planLog.warn(`${task.id}: failed to refresh task during stuck-detector ${context} cleanup: ${msg}`);
163015
+ return task;
163016
+ });
163017
+ if (hasLatestSpecReviewApproval2(freshTask)) {
163018
+ const recovered = await this.recoverApprovedTask(freshTask).catch((err) => {
163019
+ const msg = err instanceof Error ? err.message : String(err);
163020
+ planLog.warn(`${task.id}: approved-spec recovery failed during stuck-detector ${context} cleanup: ${msg}`);
163021
+ return false;
163022
+ });
163023
+ if (recovered) {
163024
+ return;
163025
+ }
163026
+ }
163027
+ const maxStuckSettings = await this.store.getSettings().catch((err) => {
163028
+ const msg = err instanceof Error ? err.message : String(err);
163029
+ planLog.warn(`${task.id}: failed to read maxStuckKills during stuck-detector ${context} cleanup, using default 6: ${msg}`);
163030
+ return {};
163031
+ });
163032
+ const maxKills = Math.max(1, maxStuckSettings.maxStuckKills ?? 6);
163033
+ const nextStuckKillCount = (freshTask.stuckKillCount ?? task.stuckKillCount ?? 0) + 1;
163034
+ const draft = await this.readNonEmptyPlanningDraft(task.id, `stuck-detector ${context} cleanup`);
163035
+ if (nextStuckKillCount >= maxKills) {
163036
+ const exhaustedError = `STUCK_LOOP_EXHAUSTED: triage stuck detector killed ${task.id} ${nextStuckKillCount}/${maxKills} times without planning completion; task paused for manual intervention.`;
163037
+ planLog.error(exhaustedError);
163038
+ await this.store.logEntry(task.id, exhaustedError).catch((err) => {
163039
+ const msg = err instanceof Error ? err.message : String(err);
163040
+ planLog.warn(`${task.id}: failed to log stuck-loop exhaustion: ${msg}`);
163041
+ });
163042
+ await this.store.updateTask(task.id, {
163043
+ stuckKillCount: nextStuckKillCount,
163044
+ status: "failed",
163045
+ error: exhaustedError,
163046
+ paused: true,
163047
+ pausedReason: "stuck-loop-exhausted-manual-intervention-required",
163048
+ pausedByAgentId: "triage"
163049
+ }).catch((err) => {
163050
+ const msg = err instanceof Error ? err.message : String(err);
163051
+ planLog.warn(`${task.id}: failed to persist stuck-loop exhaustion during stuck-detector ${context} cleanup: ${msg}`);
163052
+ });
163053
+ return;
163054
+ }
163055
+ if (draft) {
163056
+ const sourceLabel = draft.source === "prompt" ? "PROMPT.md draft" : "plan task document";
163057
+ planLog.log(`${task.id} killed by stuck detector \u2014 requeueing to resume existing ${sourceLabel} (${nextStuckKillCount}/${maxKills})`);
163058
+ await this.store.logEntry(task.id, TRIAGE_STUCK_RESUME_LOG_ACTION, TRIAGE_STUCK_RESUME_FEEDBACK).catch((err) => {
163059
+ const msg = err instanceof Error ? err.message : String(err);
163060
+ planLog.warn(`${task.id}: failed to log stuck-resume feedback: ${msg}`);
163061
+ });
163062
+ await this.store.updateTask(task.id, {
163063
+ status: "needs-replan",
163064
+ stuckKillCount: nextStuckKillCount
163065
+ }).catch((err) => {
163066
+ const msg = err instanceof Error ? err.message : String(err);
163067
+ planLog.warn(`${task.id}: failed to restore status to 'needs-replan' during stuck-detector ${context} cleanup: ${msg}`);
163068
+ });
163069
+ return;
163070
+ }
163071
+ planLog.log(`${task.id} killed by stuck detector \u2014 clearing status for cold retry (${nextStuckKillCount}/${maxKills})`);
163072
+ const restoreStatus = (freshTask.status ?? task.status) === "needs-replan" ? "needs-replan" : null;
163073
+ await this.store.updateTask(task.id, {
163074
+ status: restoreStatus,
163075
+ stuckKillCount: nextStuckKillCount
163076
+ }).catch((err) => {
163077
+ const msg = err instanceof Error ? err.message : String(err);
163078
+ planLog.warn(`${task.id}: failed to restore status to '${restoreStatus}' during stuck-detector ${context} cleanup: ${msg}`);
163079
+ });
163080
+ }
162895
163081
  /**
162896
163082
  * If `newIntervalMs` differs from the currently active timer, restart
162897
163083
  * the `setInterval` so the new cadence takes effect immediately.
@@ -163250,9 +163436,16 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
163250
163436
  let feedback;
163251
163437
  if (isReplan) {
163252
163438
  const feedbackLogEntry = [...task.log].reverse().find(
163253
- (entry) => entry.action === "User comment requested re-specification of planned task" || entry.action === "User comment invalidated spec approval \u2014 task needs re-specification" || entry.action === "AI spec revision requested"
163439
+ (entry) => entry.action === "User comment requested re-specification of planned task" || entry.action === "User comment invalidated spec approval \u2014 task needs re-specification" || entry.action === "AI spec revision requested" || entry.action === TRIAGE_STUCK_RESUME_LOG_ACTION
163254
163440
  );
163255
163441
  feedback = feedbackLogEntry?.outcome;
163442
+ if (feedbackLogEntry?.action === TRIAGE_STUCK_RESUME_LOG_ACTION) {
163443
+ const planningDraft = await this.readNonEmptyPlanningDraft(task.id, "stuck-resume replan seed");
163444
+ existingPrompt = planningDraft?.content;
163445
+ if (!existingPrompt) {
163446
+ feedback = void 0;
163447
+ }
163448
+ }
163256
163449
  if (!feedback) {
163257
163450
  const latestUserComment = [...detail.comments || []].reverse().find((comment) => comment.author === "user");
163258
163451
  feedback = latestUserComment?.text;
@@ -163287,12 +163480,7 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
163287
163480
  }
163288
163481
  if (this.stuckAborted.has(task.id)) {
163289
163482
  this.stuckAborted.delete(task.id);
163290
- planLog.log(`${task.id} killed by stuck detector \u2014 clearing status for retry`);
163291
- const restoreStatus = task.status === "needs-replan" ? "needs-replan" : null;
163292
- await this.store.updateTask(task.id, { status: restoreStatus }).catch((err) => {
163293
- const msg = err instanceof Error ? err.message : String(err);
163294
- planLog.warn(`${task.id}: failed to restore status to '${restoreStatus}' during stuck-detector abort cleanup: ${msg}`);
163295
- });
163483
+ await this.handleStuckAbortRequeue(task, "in-loop");
163296
163484
  return;
163297
163485
  }
163298
163486
  if (createdSubtasksRef.current.length > 0) {
@@ -163541,12 +163729,7 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
163541
163729
  });
163542
163730
  } else if (this.stuckAborted.has(task.id)) {
163543
163731
  this.stuckAborted.delete(task.id);
163544
- planLog.log(`${task.id} killed by stuck detector \u2014 clearing status for retry`);
163545
- const restoreStatus = task.status === "needs-replan" ? "needs-replan" : null;
163546
- await this.store.updateTask(task.id, { status: restoreStatus }).catch((err2) => {
163547
- const msg = err2 instanceof Error ? err2.message : String(err2);
163548
- planLog.warn(`${task.id}: failed to restore status to '${restoreStatus}' during stuck-detector error cleanup: ${msg}`);
163549
- });
163732
+ await this.handleStuckAbortRequeue(task, "catch");
163550
163733
  } else {
163551
163734
  if (this.options.usageLimitPauser && isUsageLimitError(errorMessage2)) {
163552
163735
  await this.options.usageLimitPauser.onUsageLimitHit(
@@ -176798,9 +176981,7 @@ ${summary}`,
176798
176981
  } else {
176799
176982
  const settings2 = await this.store.getSettings();
176800
176983
  const preserveProgress = settings2.preserveProgressOnStuckRequeue !== false;
176801
- if (!preserveProgress) {
176802
- await this.resetStepsIfWorkLost(latestTask);
176803
- }
176984
+ await this.resetStepsIfWorkLost(latestTask);
176804
176985
  if (worktreePath && existsSync47(worktreePath)) {
176805
176986
  try {
176806
176987
  await removeWorktree({
@@ -178148,9 +178329,7 @@ ${summary}`,
178148
178329
  } else {
178149
178330
  const settings2 = await this.store.getSettings();
178150
178331
  const preserveProgress = settings2.preserveProgressOnStuckRequeue !== false;
178151
- if (!preserveProgress) {
178152
- await this.resetStepsIfWorkLost(latestTask);
178153
- }
178332
+ await this.resetStepsIfWorkLost(latestTask);
178154
178333
  if (worktreePath && existsSync47(worktreePath)) {
178155
178334
  try {
178156
178335
  await removeWorktree({
@@ -179495,28 +179674,48 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
179495
179674
  await this.store.logEntry(taskId, "Execution stopped \u2014 work discarded, moved to triage for re-planning");
179496
179675
  }
179497
179676
  /**
179498
- * Re-open the last non-pending step so a revision/failure handler gives the
179499
- * executor exactly one pending slot to re-enter through. Returns the index
179500
- * and name of the step that was flipped to `pending`, or null when there
179501
- * was nothing to re-open.
179677
+ * Re-open the implementation-bearing slice of work for a revision/failure
179678
+ * handler. Returns the earliest reopened step and all reopened indexes, or
179679
+ * null when there was nothing to re-open.
179502
179680
  */
179503
179681
  async reopenLastStepForRevision(taskId, task) {
179504
179682
  const steps = task.steps;
179505
179683
  if (steps.length === 0) return null;
179506
- let targetIndex = -1;
179684
+ let lastNonPendingIndex = -1;
179507
179685
  for (let i = steps.length - 1; i >= 0; i--) {
179508
179686
  if (steps[i].status !== "pending") {
179509
- targetIndex = i;
179687
+ lastNonPendingIndex = i;
179510
179688
  break;
179511
179689
  }
179512
179690
  }
179513
- if (targetIndex === -1) {
179691
+ if (lastNonPendingIndex === -1) {
179514
179692
  await this.store.updateTask(taskId, { currentStep: 0 });
179515
179693
  return null;
179516
179694
  }
179517
- await this.store.updateStep(taskId, targetIndex, "pending");
179518
- await this.store.updateTask(taskId, { currentStep: targetIndex });
179519
- return { index: targetIndex, name: steps[targetIndex].name };
179695
+ const isTerminalVerificationOrDeliveryStep = (name) => /(^|[^a-z])(testing|verification|documentation|delivery)([^a-z]|$)/i.test(name);
179696
+ const resetIndexes = /* @__PURE__ */ new Set([lastNonPendingIndex]);
179697
+ if (isTerminalVerificationOrDeliveryStep(steps[lastNonPendingIndex].name)) {
179698
+ let cursor = lastNonPendingIndex;
179699
+ while (cursor >= 0 && isTerminalVerificationOrDeliveryStep(steps[cursor].name)) {
179700
+ resetIndexes.add(cursor);
179701
+ cursor--;
179702
+ }
179703
+ while (cursor >= 0 && steps[cursor].status === "pending") {
179704
+ cursor--;
179705
+ }
179706
+ if (cursor >= 0) {
179707
+ resetIndexes.add(cursor);
179708
+ }
179709
+ }
179710
+ const indexes = [...resetIndexes].sort((a, b2) => a - b2);
179711
+ for (const index2 of indexes) {
179712
+ if (steps[index2].status !== "pending") {
179713
+ await this.store.updateStep(taskId, index2, "pending");
179714
+ }
179715
+ }
179716
+ const currentStep = indexes[0] ?? lastNonPendingIndex;
179717
+ await this.store.updateTask(taskId, { currentStep });
179718
+ return { index: currentStep, name: steps[currentStep].name, indexes };
179520
179719
  }
179521
179720
  /**
179522
179721
  * Run deterministic verification (test + build commands) in the task's worktree.
@@ -182080,39 +182279,45 @@ Review the work done in this worktree and evaluate it against the criteria in yo
182080
182279
  const mergeBase = mergeBaseStdout.trim();
182081
182280
  const branchHead = branchHeadStdout.trim();
182082
182281
  if (mergeBase === branchHead) {
182083
- executorLog.warn(
182084
- `${task.id} branch has no unique commits \u2014 resetting ${completedSteps.length} step(s) to pending`
182282
+ await this.resetLostWorkStepProgress(task, completedSteps.length, "branch had no commits");
182283
+ }
182284
+ } catch (err) {
182285
+ const msg = err instanceof Error ? err.message : String(err);
182286
+ executorLog.warn(
182287
+ `${task.id}: unable to prove surviving branch commits before worktree removal \u2014 resetting ${completedSteps.length} step(s) to pending: ${msg}`
182288
+ );
182289
+ await this.resetLostWorkStepProgress(task, completedSteps.length, `git proof failed: ${msg}`);
182290
+ }
182291
+ }
182292
+ async resetLostWorkStepProgress(task, completedStepCount, reason) {
182293
+ executorLog.warn(
182294
+ `${task.id} ${reason} \u2014 resetting ${completedStepCount} step(s) to pending`
182295
+ );
182296
+ for (let i = 0; i < task.steps.length; i++) {
182297
+ if (task.steps[i].status === "done" || task.steps[i].status === "in-progress") {
182298
+ await this.store.updateStep(task.id, i, "pending");
182299
+ }
182300
+ }
182301
+ const refreshedTask = await this.store.getTask(task.id);
182302
+ const prevCurrentStep = refreshedTask.currentStep;
182303
+ if (refreshedTask.steps.length > 0) {
182304
+ const firstPendingStep = refreshedTask.steps.findIndex((s3) => s3.status === "pending");
182305
+ const newCurrentStep = firstPendingStep >= 0 ? firstPendingStep : 0;
182306
+ if (newCurrentStep !== prevCurrentStep) {
182307
+ await this.store.updateTask(task.id, { currentStep: newCurrentStep });
182308
+ executorLog.log(
182309
+ `${task.id}: reset currentStep to ${newCurrentStep} after lost-work reset (was ${prevCurrentStep})`
182085
182310
  );
182086
- for (let i = 0; i < task.steps.length; i++) {
182087
- if (task.steps[i].status === "done" || task.steps[i].status === "in-progress") {
182088
- await this.store.updateStep(task.id, i, "pending");
182089
- }
182090
- }
182091
- const refreshedTask = await this.store.getTask(task.id);
182092
- const prevCurrentStep = refreshedTask.currentStep;
182093
- if (refreshedTask.steps.length > 0) {
182094
- const firstPendingStep = refreshedTask.steps.findIndex((s3) => s3.status === "pending");
182095
- const newCurrentStep = firstPendingStep >= 0 ? firstPendingStep : 0;
182096
- if (newCurrentStep !== prevCurrentStep) {
182097
- await this.store.updateTask(task.id, { currentStep: newCurrentStep });
182098
- executorLog.log(
182099
- `${task.id}: reset currentStep to ${newCurrentStep} after lost-work reset (was ${prevCurrentStep})`
182100
- );
182101
- await this.store.logEntry(
182102
- task.id,
182103
- `Reset currentStep to ${newCurrentStep} after lost-work step reset (was ${prevCurrentStep})`
182104
- );
182105
- }
182106
- }
182107
182311
  await this.store.logEntry(
182108
182312
  task.id,
182109
- `Reset ${completedSteps.length} step(s) to pending \u2014 branch had no commits (uncommitted work lost with worktree)`
182313
+ `Reset currentStep to ${newCurrentStep} after lost-work step reset (was ${prevCurrentStep})`
182110
182314
  );
182111
182315
  }
182112
- } catch (err) {
182113
- const msg = err instanceof Error ? err.message : String(err);
182114
- executorLog.warn(`${task.id}: step-reset-on-work-lost failed (non-fatal, steps keep current status): ${msg}`);
182115
182316
  }
182317
+ await this.store.logEntry(
182318
+ task.id,
182319
+ `Reset ${completedStepCount} step(s) to pending \u2014 ${reason} (uncommitted work lost with worktree)`
182320
+ );
182116
182321
  }
182117
182322
  /**
182118
182323
  * Mark a task as stuck-aborted so the executor's error handling
@@ -182175,9 +182380,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
182175
182380
  });
182176
182381
  await this.awaitAbortInFlightTaskWork(taskId, "force-requeue after stuck-kill unwind timeout");
182177
182382
  this.clearPausedAborted(taskId);
182178
- if (!preserveProgress) {
182179
- await this.resetStepsIfWorkLost(latestTask);
182180
- }
182383
+ await this.resetStepsIfWorkLost(latestTask);
182181
182384
  let cleanupFailed = false;
182182
182385
  if (worktreePath && existsSync47(worktreePath)) {
182183
182386
  try {
@@ -187466,6 +187669,7 @@ var init_agent_reflection = __esm({
187466
187669
  init_logger2();
187467
187670
  init_pi();
187468
187671
  init_mcp_resolution();
187672
+ init_run_audit();
187469
187673
  reflectionLog = createLogger2("reflection");
187470
187674
  REFLECTION_SYSTEM_PROMPT = `You are an autonomous performance analyst reviewing an AI agent's recent execution history.
187471
187675
 
@@ -187504,11 +187708,22 @@ Rules:
187504
187708
  this.modelId = options.modelId;
187505
187709
  }
187506
187710
  async generateReflection(agentId, trigger, options = {}) {
187711
+ const runContext = {
187712
+ runId: generateSyntheticRunId("reflection", agentId),
187713
+ agentId,
187714
+ ...options.taskId ? { taskId: options.taskId } : {},
187715
+ phase: "reflection",
187716
+ source: trigger
187717
+ };
187718
+ const auditor = createRunAuditor(this.taskStore, runContext);
187507
187719
  try {
187508
187720
  const context = await this.buildReflectionContext(agentId);
187509
187721
  const recentRuns = await this.agentStore.getRecentRuns(agentId, DEFAULT_OUTCOME_LIMIT);
187510
187722
  if (context.recentOutcomes.length === 0 && recentRuns.length === 0) {
187511
187723
  reflectionLog.log(`Skipping reflection for ${agentId}: no recent tasks or heartbeat runs`);
187724
+ await this.emitReflectionAudit(auditor, "reflection:skipped", agentId, trigger, options, {
187725
+ reason: "no-history"
187726
+ });
187512
187727
  return null;
187513
187728
  }
187514
187729
  let responseText = "";
@@ -187538,7 +187753,7 @@ Rules:
187538
187753
  }
187539
187754
  const parsed = this.parseReflectionResponse(responseText);
187540
187755
  const metrics = this.buildReflectionMetrics(context.recentOutcomes, context.performanceSummary, recentRuns);
187541
- return await this.reflectionStore.createReflection({
187756
+ const reflection = await this.reflectionStore.createReflection({
187542
187757
  agentId,
187543
187758
  trigger,
187544
187759
  triggerDetail: options.triggerDetail,
@@ -187548,11 +187763,42 @@ Rules:
187548
187763
  suggestedImprovements: parsed.suggestedImprovements,
187549
187764
  summary: parsed.summary
187550
187765
  });
187766
+ await this.emitReflectionAudit(auditor, "reflection:generated", agentId, trigger, options, {
187767
+ reflectionId: reflection.id,
187768
+ ...metrics.tasksCompleted !== void 0 ? { tasksCompleted: metrics.tasksCompleted } : {},
187769
+ ...metrics.tasksFailed !== void 0 ? { tasksFailed: metrics.tasksFailed } : {},
187770
+ ...metrics.avgDurationMs !== void 0 ? { avgDurationMs: metrics.avgDurationMs } : {},
187771
+ commonErrorCount: metrics.commonErrors?.length ?? 0,
187772
+ insightCount: parsed.insights.length,
187773
+ suggestedImprovementCount: parsed.suggestedImprovements.length
187774
+ });
187775
+ return reflection;
187551
187776
  } catch (error2) {
187777
+ await this.emitReflectionAudit(auditor, "reflection:failed", agentId, trigger, options, {
187778
+ errorClass: error2 instanceof Error ? error2.name : typeof error2
187779
+ });
187552
187780
  reflectionLog.error(`Failed to generate reflection for ${agentId}: ${error2.message}`);
187553
187781
  return null;
187554
187782
  }
187555
187783
  }
187784
+ async emitReflectionAudit(auditor, type, agentId, trigger, options, metadata) {
187785
+ try {
187786
+ await auditor.database({
187787
+ type,
187788
+ target: agentId,
187789
+ metadata: {
187790
+ agentId,
187791
+ trigger,
187792
+ ...options.taskId ? { taskId: options.taskId } : {},
187793
+ ...metadata
187794
+ }
187795
+ });
187796
+ } catch (auditError) {
187797
+ reflectionLog.warn(
187798
+ `Failed to record reflection telemetry for ${agentId}: ${auditError instanceof Error ? auditError.message : String(auditError)}`
187799
+ );
187800
+ }
187801
+ }
187556
187802
  async buildReflectionContext(agentId) {
187557
187803
  const [agentRecord, recentOutcomes, performanceSummaryRaw, latestReflection] = await Promise.all([
187558
187804
  this.agentStore.getAgent(agentId),
@@ -188654,6 +188900,8 @@ Your job:
188654
188900
  - Surface the blocker concretely with fn_task_log.
188655
188901
  - Chase the dependency: comment on the blocking task, send a message to the responsible agent, or ping an owner.
188656
188902
  - Look for unblocking work you can spawn or delegate right now.
188903
+ - Do NOT call fn_task_pause to handle a failed or blocked task. Pausing is reserved for explicit user requests for manual control.
188904
+ - If the task needs recovery, create/delegate focused follow-up work with fn_task_create or fn_delegate_task, log the needed operator action, or let the task surface as failed.
188657
188905
  - Pivot to other relevant coordination work if the blocker cannot be immediately resolved.
188658
188906
 
188659
188907
  **If your bound task is not blocked:**
@@ -188690,8 +188938,8 @@ Prefer fn_delegate_task when immediate ownership by a specific agent materially
188690
188938
 
188691
188939
  ## Common Patterns
188692
188940
 
188693
- - **Blocked task:** log the concrete blocker, chase the dependency via fn_send_message, create a narrowly scoped unblocker task if needed.
188694
- - **Stuck task with no blockedBy:** log the observation and create a follow-up task to investigate the root cause.
188941
+ - **Blocked task:** log the concrete blocker, chase the dependency via fn_send_message, create a narrowly scoped unblocker task if needed; do not pause it unless the user explicitly requested manual control.
188942
+ - **Stuck task with no blockedBy:** log the observation and create a follow-up task to investigate the root cause; do not use fn_task_pause as failure handling.
188695
188943
  - **Completed task with follow-up risk:** create explicit follow-up task(s) for residual risk instead of burying notes in a long log.
188696
188944
  - **New user/agent comments:** summarize what changed, identify required action, and route via task creation/delegation.
188697
188945
  - **Dependency drift:** log the mismatch and create reconciliation tasks with clear dependencies.
@@ -188783,6 +189031,7 @@ If unsure who should do the work, prefer fn_task_create and let scheduler routin
188783
189031
 
188784
189032
  ## Common Patterns
188785
189033
 
189034
+ - **Failed or blocked task:** do NOT call fn_task_pause to handle the failure or blocker. Pausing is reserved for explicit user requests for manual control; instead surface the blocker through available task or message context, create/delegate follow-up work, or let the task surface as failed.
188786
189035
  - **Unowned risk discovered:** create one focused task with concrete acceptance language.
188787
189036
  - **Known specialist needed:** list agents, then delegate to matching role/capability.
188788
189037
  - **Repeated confusion across runs:** append a concise memory entry so future agents avoid the same mistake.
@@ -194311,12 +194560,21 @@ var init_cron_runner = __esm({
194311
194560
  log24.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 belongs to ${scheduleScope} scope, not polling`);
194312
194561
  continue;
194313
194562
  }
194314
- log24.log(`Executing ${schedule.name} (${schedule.id}) [scope: ${scheduleScope}]`);
194315
194563
  const currentSettings = await this.store.getSettings();
194316
194564
  if (currentSettings.globalPause || currentSettings.enginePaused) {
194317
194565
  log24.log("Pause detected mid-tick \u2014 stopping schedule execution");
194318
194566
  break;
194319
194567
  }
194568
+ if (!schedule.nextRunAt) {
194569
+ log24.warn(`Skipping ${schedule.name} (${schedule.id}) \u2014 missing nextRunAt for due claim`);
194570
+ continue;
194571
+ }
194572
+ const claimed = await this.automationStore.claimDueSchedule(schedule.id, schedule.nextRunAt);
194573
+ if (!claimed) {
194574
+ log24.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 claim lost to another poller`);
194575
+ continue;
194576
+ }
194577
+ log24.log(`Executing ${schedule.name} (${schedule.id}) [scope: ${scheduleScope}]`);
194320
194578
  await this.executeSchedule(schedule);
194321
194579
  }
194322
194580
  } catch (err) {
@@ -199306,6 +199564,7 @@ var init_in_process_runtime = __esm({
199306
199564
  executor;
199307
199565
  worktreePool;
199308
199566
  globalSemaphore;
199567
+ projectSemaphore;
199309
199568
  stuckTaskDetector;
199310
199569
  /**
199311
199570
  * Per-project CLI Agent Executor runtime bundle (PTY manager + telemetry hub +
@@ -199453,6 +199712,7 @@ var init_in_process_runtime = __esm({
199453
199712
  this.centralCore.on("concurrency:changed", this.concurrencyChangedListener);
199454
199713
  }
199455
199714
  }
199715
+ this.projectSemaphore = new ScopedAgentSemaphore(this.globalSemaphore);
199456
199716
  await yieldEventLoop4();
199457
199717
  let agentStoreForReflection;
199458
199718
  try {
@@ -199512,7 +199772,7 @@ var init_in_process_runtime = __esm({
199512
199772
  this.scheduler = new Scheduler(this.taskStore, {
199513
199773
  maxConcurrent: this.config.maxConcurrent,
199514
199774
  maxWorktrees: this.config.maxWorktrees,
199515
- semaphore: this.globalSemaphore,
199775
+ semaphore: this.projectSemaphore,
199516
199776
  agentStore: this.agentStore,
199517
199777
  hasActiveAgentExecution: (agentId) => this.heartbeatMonitor?.getTrackedAgents().includes(agentId) ?? false,
199518
199778
  missionStore,
@@ -199609,7 +199869,7 @@ var init_in_process_runtime = __esm({
199609
199869
  const prNodeGithubOps = this.config.prNodeGithubOps;
199610
199870
  const workflowAuthoritativeDriverRef = {};
199611
199871
  const executorOptions = {
199612
- semaphore: this.globalSemaphore,
199872
+ semaphore: this.projectSemaphore,
199613
199873
  pool: this.worktreePool,
199614
199874
  usageLimitPauser: this.usageLimitPauser,
199615
199875
  stuckTaskDetector: this.stuckTaskDetector,
@@ -199807,7 +200067,7 @@ var init_in_process_runtime = __esm({
199807
200067
  this.taskStore,
199808
200068
  this.config.workingDirectory,
199809
200069
  {
199810
- semaphore: this.globalSemaphore,
200070
+ semaphore: this.projectSemaphore,
199811
200071
  stuckTaskDetector: this.stuckTaskDetector,
199812
200072
  agentStore: this.agentStore,
199813
200073
  pluginRunner: this.pluginRunner,
@@ -200106,6 +200366,12 @@ var init_in_process_runtime = __esm({
200106
200366
  `post-abort drain timeout: shutdown reached with ${finalMetrics.inFlightTasks} tasks still in-flight`
200107
200367
  );
200108
200368
  }
200369
+ const returnedResidualSlots = this.projectSemaphore?.returnAllHeldSlots() ?? 0;
200370
+ if (returnedResidualSlots > 0) {
200371
+ runtimeLog.warn(
200372
+ `Returned ${returnedResidualSlots} residual global concurrency slot(s) after project stop drain`
200373
+ );
200374
+ }
200109
200375
  if (this.pluginRunner) {
200110
200376
  await this.pluginRunner.shutdown();
200111
200377
  runtimeLog.log("PluginRunner shutdown complete");
@@ -204641,7 +204907,7 @@ ${detail}`
204641
204907
  }
204642
204908
  } else {
204643
204909
  runtimeLog.log(`${hasManualResolver ? "Manual" : "Auto"}-merge merging ${taskId}...`);
204644
- const semaphore = this.runtime.globalSemaphore;
204910
+ const semaphore = this.runtime.projectSemaphore ?? this.runtime.globalSemaphore;
204645
204911
  const pool = this.runtime.worktreePool;
204646
204912
  const agentStore = this.runtime.agentStore;
204647
204913
  const usageLimitPauser = this.runtime.usageLimitPauser;
@@ -214800,6 +215066,12 @@ data: ${JSON.stringify(stripTaskListHeavyFields(task))}
214800
215066
  send(`event: task:merged
214801
215067
  data: ${JSON.stringify(stripTaskEventHeavyFields(result))}
214802
215068
 
215069
+ `);
215070
+ };
215071
+ const onArtifactRegistered = (artifact) => {
215072
+ send(`event: artifact:registered
215073
+ data: ${JSON.stringify(artifact)}
215074
+
214803
215075
  `);
214804
215076
  };
214805
215077
  const onResearchRunCreated = (run2) => {
@@ -215238,6 +215510,7 @@ data: ${JSON.stringify(data)}
215238
215510
  store.off("task:updated", onUpdated);
215239
215511
  store.off("task:deleted", onDeleted);
215240
215512
  store.off("task:merged", onMerged);
215513
+ store.off("artifact:registered", onArtifactRegistered);
215241
215514
  if (missionStore) {
215242
215515
  missionStore.off("mission:created", onMissionCreated);
215243
215516
  missionStore.off("mission:updated", onMissionUpdated);
@@ -215341,6 +215614,7 @@ data: ${JSON.stringify(data)}
215341
215614
  store.on("task:updated", onUpdated);
215342
215615
  store.on("task:deleted", onDeleted);
215343
215616
  store.on("task:merged", onMerged);
215617
+ store.on("artifact:registered", onArtifactRegistered);
215344
215618
  if (missionStore) {
215345
215619
  missionStore.on("mission:created", onMissionCreated);
215346
215620
  missionStore.on("mission:updated", onMissionUpdated);
@@ -215707,7 +215981,7 @@ function listRegisteredProjectStores() {
215707
215981
  }
215708
215982
  async function countRunningAgentsInStore(store) {
215709
215983
  const tasks = await store.listTasks({ slim: true });
215710
- return tasks.filter((task) => task.column === "in-progress" || task.column === "triage" && task.status === "planning" && !task.paused).length;
215984
+ return countRunningAgentTasks(tasks);
215711
215985
  }
215712
215986
  async function countRunningAgentsInRegisteredProjectStores(projectIds) {
215713
215987
  const requestedProjectIds = new Set(projectIds);
@@ -215730,6 +216004,7 @@ var storeCache, pendingCreations, initializedProjects, projectRegisteredListener
215730
216004
  var init_project_store_resolver = __esm({
215731
216005
  "../dashboard/src/project-store-resolver.ts"() {
215732
216006
  "use strict";
216007
+ init_src();
215733
216008
  storeCache = /* @__PURE__ */ new Map();
215734
216009
  pendingCreations = /* @__PURE__ */ new Map();
215735
216010
  initializedProjects = /* @__PURE__ */ new Set();
@@ -223105,6 +223380,15 @@ async function generatePrMetadata(input) {
223105
223380
  const templateExists = await raceWithAbort(access10(templatePath).then(() => true).catch(() => false), combinedSignal);
223106
223381
  const template = templateExists ? await raceWithAbort(readFile27(templatePath, "utf8"), combinedSignal) : "";
223107
223382
  const model = resolveTitleSummarizerSettingsModel(settings);
223383
+ const systemPrompt = [PR_METADATA_SYSTEM_PROMPT];
223384
+ const titleGuidance = settings.prTitlePromptInstructions?.trim();
223385
+ const descriptionGuidance = settings.prDescriptionPromptInstructions?.trim();
223386
+ if (titleGuidance) {
223387
+ systemPrompt.push(`Title guidance: ${titleGuidance}`);
223388
+ }
223389
+ if (descriptionGuidance) {
223390
+ systemPrompt.push(`Description guidance: ${descriptionGuidance}`);
223391
+ }
223108
223392
  const mcpServers = (await raceWithAbort(resolveMcpServersForStore(store ?? {}), combinedSignal)).servers;
223109
223393
  let aiText = "";
223110
223394
  const { session } = await raceWithAbort(createFnAgent2({
@@ -223117,26 +223401,27 @@ async function generatePrMetadata(input) {
223117
223401
  mcpServers,
223118
223402
  defaultProvider: model.provider,
223119
223403
  defaultModelId: model.modelId,
223120
- systemPrompt: [
223121
- "Generate GitHub PR metadata.",
223122
- "Respond with strict JSON only.",
223123
- "Schema: {title, summary, changes, testing, linkedTask}"
223124
- ].join("\n"),
223404
+ systemPrompt: systemPrompt.join("\n"),
223125
223405
  onText: (delta) => {
223126
223406
  aiText += delta;
223127
223407
  }
223128
223408
  }), combinedSignal);
223129
223409
  try {
223130
223410
  const contextPrompt = [
223411
+ "Source-of-truth guidance:",
223412
+ "- Ground the PR title, summary, changes, and testing in the commit log and diff stat below.",
223413
+ "- Use task title, task description, and task prompt only to clarify intent when the git evidence supports it.",
223414
+ "- Omit speculation; when evidence for testing or a change is absent, say so instead of inventing details.",
223415
+ "- Return only strict JSON matching the required schema.",
223131
223416
  `Task ID: ${task.id}`,
223132
223417
  `Task title: ${task.title}`,
223133
223418
  `Task description: ${task.description ?? ""}`,
223134
223419
  `Base branch: ${baseBranch}`,
223135
- "Commit log:",
223420
+ "Commit log (source of truth):",
223136
223421
  logOut || "(none)",
223137
- "Diff stat:",
223422
+ "Diff stat (source of truth):",
223138
223423
  diffStatOut || "(none)",
223139
- "Task prompt:",
223424
+ "Task prompt (supporting context):",
223140
223425
  promptContent || "(none)"
223141
223426
  ].join("\n\n");
223142
223427
  throwIfAborted3(combinedSignal);
@@ -223169,7 +223454,7 @@ async function generatePrMetadata(input) {
223169
223454
  }
223170
223455
  }
223171
223456
  }
223172
- var execAsync36, PR_METADATA_TIMEOUT_MS;
223457
+ var execAsync36, PR_METADATA_TIMEOUT_MS, PR_METADATA_SYSTEM_PROMPT;
223173
223458
  var init_pr_metadata_generator = __esm({
223174
223459
  "../dashboard/src/pr-metadata-generator.ts"() {
223175
223460
  "use strict";
@@ -223177,6 +223462,17 @@ var init_pr_metadata_generator = __esm({
223177
223462
  init_src4();
223178
223463
  execAsync36 = promisify55(execCb);
223179
223464
  PR_METADATA_TIMEOUT_MS = 6e4;
223465
+ PR_METADATA_SYSTEM_PROMPT = [
223466
+ "You generate GitHub pull request metadata for Fusion tasks.",
223467
+ "Respond with STRICT JSON ONLY: no markdown fences, no commentary, no prose outside the JSON object.",
223468
+ 'Use exactly this schema: {"title": string, "summary": string, "changes": string, "testing": string, "linkedTask": string}.',
223469
+ "Title: concise imperative conventional-commit style, at most 72 characters, no trailing period, with a scope when the change clearly suggests one.",
223470
+ "Summary: 1-3 short plain-sentence paragraphs explaining what changed and why; be specific and avoid marketing fluff.",
223471
+ "Changes: a tight markdown bullet list of concrete, verifiable changes supported by the commit log or diff stat; do not speculate.",
223472
+ "Testing: markdown bullets naming actual verification shown in the evidence; if no testing evidence exists, say '- Not provided.' or '- Not run (not provided).'.",
223473
+ "LinkedTask: set to the task ID exactly as provided.",
223474
+ "Ground every field in the commit log and diff stat first, then use the task title, task description, and task prompt only as supporting context."
223475
+ ].join("\n");
223180
223476
  }
223181
223477
  });
223182
223478
 
@@ -225199,6 +225495,7 @@ async function mergeTaskPr(scopedStore, task, token, explicitMethod, runIdPrefix
225199
225495
  ...mergedPrInfo,
225200
225496
  autoMergeOnGreen: task.prInfo.autoMergeOnGreen,
225201
225497
  autoMergeStrategy: task.prInfo.autoMergeStrategy,
225498
+ manual: task.prInfo.manual,
225202
225499
  lastMergeError: void 0,
225203
225500
  lastMergeErrorAt: void 0,
225204
225501
  draft: mergedPrInfo.draft ?? mergedPrInfo.isDraft
@@ -225285,6 +225582,7 @@ async function refreshPrInBackground(store, taskId, currentPrInfos, token, optio
225285
225582
  autoMergeStrategy: prior?.autoMergeStrategy,
225286
225583
  lastMergeError: prior?.lastMergeError,
225287
225584
  lastMergeErrorAt: prior?.lastMergeErrorAt,
225585
+ manual: prior?.manual,
225288
225586
  draft: mergeStatus.prInfo.draft ?? mergeStatus.prInfo.isDraft,
225289
225587
  lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
225290
225588
  lastReviewDecision: reviewSnapshot.decision
@@ -226969,7 +227267,7 @@ ${body}`;
226969
227267
  for (const match of matchingTasks) {
226970
227268
  if (match.resourceType === "pr") {
226971
227269
  const current = match.current;
226972
- const next = { ...badgeData, lastCheckedAt: checkedAt };
227270
+ const next = { ...current, ...badgeData, manual: current.manual, lastCheckedAt: checkedAt };
226973
227271
  const changed = hasPrBadgeFieldsChanged(current, badgeData);
226974
227272
  if (changed || current.lastCheckedAt !== checkedAt) {
226975
227273
  await scopedStore.updatePrInfo(match.id, next);
@@ -227135,7 +227433,9 @@ ${body}`;
227135
227433
  continue;
227136
227434
  }
227137
227435
  const updatedPrInfo = {
227436
+ ...task.prInfo,
227138
227437
  ...prInfo,
227438
+ manual: task.prInfo.manual,
227139
227439
  lastCheckedAt: refreshedAt
227140
227440
  };
227141
227441
  await scopedStore.updatePrInfo(taskId, updatedPrInfo);
@@ -227218,10 +227518,10 @@ ${body}`;
227218
227518
  const existingPr = await client.findPrForBranch({ head: branchName, state: "all", owner, repo });
227219
227519
  let prInfo;
227220
227520
  if (existingPr) {
227221
- prInfo = existingPr;
227521
+ prInfo = { ...existingPr, manual: true };
227222
227522
  } else {
227223
227523
  await runGitCommand2(["push", "-u", "origin", branchName], scopedStore.getRootDir(), 6e4);
227224
- prInfo = await client.createPr({
227524
+ const createdPrInfo = await client.createPr({
227225
227525
  owner,
227226
227526
  repo,
227227
227527
  title: prTitle,
@@ -227229,6 +227529,7 @@ ${body}`;
227229
227529
  head: branchName,
227230
227530
  base
227231
227531
  });
227532
+ prInfo = { ...createdPrInfo, manual: true };
227232
227533
  }
227233
227534
  if (existingPrs.length > 0) {
227234
227535
  await scopedStore.addPrInfo(task.id, prInfo);
@@ -227620,6 +227921,7 @@ ${body}`;
227620
227921
  autoMergeStrategy: priorPr.autoMergeStrategy,
227621
227922
  lastMergeError: priorPr.lastMergeError,
227622
227923
  lastMergeErrorAt: priorPr.lastMergeErrorAt,
227924
+ manual: priorPr.manual,
227623
227925
  draft: mergeStatus.prInfo.draft ?? mergeStatus.prInfo.isDraft,
227624
227926
  lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
227625
227927
  lastReviewDecision: reviewSnapshot.decision
@@ -228719,6 +229021,39 @@ function registerTaskWorkflowRoutes(ctx, deps) {
228719
229021
  resolveSelfHealingManager: _resolveSelfHealingManager
228720
229022
  } = deps;
228721
229023
  const TASK_DETAIL_ACTIVITY_LOG_LIMIT2 = taskDetailActivityLogLimit;
229024
+ async function reengageInReviewTaskForUserComment(scopedStore, task, wake) {
229025
+ if (task.column !== "in-review") {
229026
+ return { task, reengaged: false, suppressedReason: "not-in-review" };
229027
+ }
229028
+ if (task.sessionFile) {
229029
+ return { task, reengaged: false, suppressedReason: "live-session" };
229030
+ }
229031
+ const activePrEntity = scopedStore.getActivePrEntityBySource?.("task", task.id) ?? (task.branchContext?.groupId ? scopedStore.getActivePrEntityBySource?.("branch-group", task.branchContext.groupId) : null);
229032
+ if (isBackwardMoveBlockedByOpenPr({
229033
+ fromIndex: COLUMNS.indexOf(task.column),
229034
+ toIndex: COLUMNS.indexOf("in-progress"),
229035
+ activePrEntity
229036
+ })) {
229037
+ await scopedStore.logEntry(
229038
+ task.id,
229039
+ "In-review user comment re-engagement suppressed",
229040
+ PR_OPEN_BLOCKS_MOVE_BACK_MESSAGE
229041
+ );
229042
+ return { task, reengaged: false, suppressedReason: "blocking-pr" };
229043
+ }
229044
+ await scopedStore.updateTask(task.id, {
229045
+ status: null,
229046
+ error: null,
229047
+ sessionFile: null
229048
+ });
229049
+ const lastDoneStep = [...task.steps ?? []].map((step, index2) => ({ step, index: index2 })).reverse().find(({ step }) => step.status === "done" || step.status === "in-progress");
229050
+ if (lastDoneStep) {
229051
+ await scopedStore.updateStep(task.id, lastDoneStep.index, "pending");
229052
+ }
229053
+ const reengagedTask = await scopedStore.moveTask(task.id, "in-progress", { preserveProgress: true });
229054
+ await triggerCommentWakeForAssignedAgent(scopedStore, reengagedTask, wake);
229055
+ return { task: reengagedTask, reengaged: true };
229056
+ }
228722
229057
  router.get("/tasks/merge-advance-events", async (req, res) => {
228723
229058
  try {
228724
229059
  const { store: scopedStore } = await getProjectContext3(req);
@@ -230266,17 +230601,26 @@ ${normalizedDescription}`;
230266
230601
  if (author !== void 0 && typeof author !== "string") {
230267
230602
  throw badRequest("author must be a string");
230268
230603
  }
230269
- const task = await scopedStore.addTaskComment(req.params.id, text, author?.trim() || "user");
230604
+ const normalizedAuthor = author?.trim() || "user";
230605
+ const task = await scopedStore.addTaskComment(req.params.id, text, normalizedAuthor);
230270
230606
  const newCommentId = task.comments?.at(-1)?.id;
230271
- void triggerCommentWakeForAssignedAgent(scopedStore, task, {
230607
+ const wake = {
230272
230608
  triggeringCommentType: "task",
230273
230609
  triggeringCommentIds: newCommentId ? [newCommentId] : void 0,
230274
230610
  triggerDetail: "task-comment"
230275
- }).catch((error2) => {
230276
- runtimeLogger.warn(
230277
- `failed to trigger task-comment heartbeat for ${task.id}: ${error2 instanceof Error ? error2.message : String(error2)}`
230278
- );
230279
- });
230611
+ };
230612
+ if (normalizedAuthor === "user") {
230613
+ if (task.column === "in-review" && !task.sessionFile) {
230614
+ const { task: reengagedTask } = await reengageInReviewTaskForUserComment(scopedStore, task, wake);
230615
+ res.json(reengagedTask);
230616
+ return;
230617
+ }
230618
+ void triggerCommentWakeForAssignedAgent(scopedStore, task, wake).catch((error2) => {
230619
+ runtimeLogger.warn(
230620
+ `failed to trigger task-comment heartbeat for ${task.id}: ${error2 instanceof Error ? error2.message : String(error2)}`
230621
+ );
230622
+ });
230623
+ }
230280
230624
  res.json(task);
230281
230625
  } catch (err) {
230282
230626
  if (err instanceof ApiError) {
@@ -230549,11 +230893,17 @@ ${normalizedDescription}`;
230549
230893
  }
230550
230894
  const task = await scopedStore.addSteeringComment(req.params.id, text, "user");
230551
230895
  const newSteeringCommentId = task.steeringComments?.at(-1)?.id;
230552
- void triggerCommentWakeForAssignedAgent(scopedStore, task, {
230896
+ const wake = {
230553
230897
  triggeringCommentType: "steering",
230554
230898
  triggeringCommentIds: newSteeringCommentId ? [newSteeringCommentId] : void 0,
230555
230899
  triggerDetail: "steering-comment"
230556
- }).catch((error2) => {
230900
+ };
230901
+ if (task.column === "in-review" && !task.sessionFile) {
230902
+ const { task: reengagedTask } = await reengageInReviewTaskForUserComment(scopedStore, task, wake);
230903
+ res.json(reengagedTask);
230904
+ return;
230905
+ }
230906
+ void triggerCommentWakeForAssignedAgent(scopedStore, task, wake).catch((error2) => {
230557
230907
  runtimeLogger.warn(
230558
230908
  `failed to trigger steering-comment heartbeat for ${task.id}: ${error2 instanceof Error ? error2.message : String(error2)}`
230559
230909
  );
@@ -231101,26 +231451,57 @@ ${normalizedDescription}`;
231101
231451
  if (unsupportedSource) {
231102
231452
  throw badRequest(`Unsupported review source: ${String(unsupportedSource.source)}`);
231103
231453
  }
231104
- if (!task.reviewState) {
231105
- throw badRequest("Task has no reviewState payload");
231106
- }
231454
+ let canonicalReviewData;
231455
+ if (task.prInfo) {
231456
+ const badgeParsed = parseGitHubBadgeUrl(task.prInfo.url);
231457
+ const repoInfo = getCurrentRepo(scopedStore.getRootDir());
231458
+ const owner = badgeParsed?.owner ?? repoInfo?.owner;
231459
+ const repo = badgeParsed?.repo ?? repoInfo?.repo;
231460
+ if (!owner || !repo) {
231461
+ throw badRequest("Could not determine GitHub repository for PR review fetch");
231462
+ }
231463
+ canonicalReviewData = await new GitHubClient(options?.githubToken ?? process.env.GITHUB_TOKEN).getPrReviewDetails(owner, repo, task.prInfo.number);
231464
+ } else {
231465
+ canonicalReviewData = await buildDirectTaskReviewData(task, scopedStore);
231466
+ }
231467
+ const canonicalReviewItems = canonicalReviewData.items.map((item) => ({
231468
+ id: item.itemId,
231469
+ body: item.body,
231470
+ summary: item.title,
231471
+ author: { login: item.author },
231472
+ createdAt: item.createdAt ?? canonicalReviewData.fetchedAt ?? (/* @__PURE__ */ new Date(0)).toISOString(),
231473
+ updatedAt: item.updatedAt ?? void 0,
231474
+ path: item.filePath,
231475
+ line: item.line,
231476
+ threadId: item.threadId,
231477
+ htmlUrl: item.url,
231478
+ state: item.reviewState ?? void 0,
231479
+ isResolved: item.isResolved,
231480
+ source: item.sourceMode === "reviewer-agent" ? "reviewer-agent" : "github-pr"
231481
+ }));
231482
+ const reviewState = {
231483
+ source: canonicalReviewData.mode,
231484
+ summary: canonicalReviewData.summary ?? task.reviewState?.summary,
231485
+ items: canonicalReviewItems,
231486
+ addressing: task.reviewState?.addressing ?? [],
231487
+ lastRefreshedAt: task.reviewState?.lastRefreshedAt ?? canonicalReviewData.fetchedAt ?? void 0,
231488
+ refreshSource: task.reviewState?.refreshSource,
231489
+ refreshStatus: task.reviewState?.refreshStatus,
231490
+ refreshError: task.reviewState?.refreshError
231491
+ };
231107
231492
  const now = (/* @__PURE__ */ new Date()).toISOString();
231108
231493
  const selectedSet = new Set(selectedItems.map((item) => item.id));
231109
- const sourceById = new Map(selectedItems.map((item) => [item.id, item.source]));
231110
- const reviewSourceMismatch = task.reviewState.items.find((item) => {
231111
- const selectedSource = sourceById.get(item.id);
231112
- if (!selectedSource || !task.reviewState) return false;
231113
- const expectedSource = task.reviewState.source === "pull-request" ? "pr-review" : "reviewer-agent";
231114
- return selectedSource !== expectedSource;
231115
- });
231494
+ const canonicalIds = new Set(reviewState.items.map((item) => item.id));
231495
+ const expectedSource = canonicalReviewData.mode === "pull-request" ? "pr-review" : "reviewer-agent";
231496
+ const reviewSourceMismatch = selectedItems.find((item) => canonicalIds.has(item.id) && item.source !== expectedSource);
231116
231497
  if (reviewSourceMismatch) {
231117
231498
  throw badRequest("Selected review source does not match task review mode");
231118
231499
  }
231119
- const matchedItems = task.reviewState.items.filter((item) => selectedSet.has(item.id));
231120
- if (matchedItems.length !== selectedSet.size) {
231500
+ const hasUnknownSelection = selectedItems.some((item) => !canonicalIds.has(item.id));
231501
+ if (hasUnknownSelection) {
231121
231502
  throw badRequest("selectedItems must reference existing review items");
231122
231503
  }
231123
- const modeSummary = `${task.reviewState.source === "pull-request" ? "pull-request" : "reviewer-agent"} \xB7 ${selectedItems.length} selected item(s)`;
231504
+ const modeSummary = `${reviewState.source === "pull-request" ? "pull-request" : "reviewer-agent"} \xB7 ${selectedItems.length} selected item(s)`;
231124
231505
  const steeringItems = selectedItems.map((item, index2) => {
231125
231506
  const location = item.filePath ? `${item.filePath}${typeof item.lineNumber === "number" ? `:${item.lineNumber}` : ""}` : void 0;
231126
231507
  const snippetSource = item.body.trim() || item.summary.trim();
@@ -231129,9 +231510,9 @@ ${normalizedDescription}`;
231129
231510
  return `${index2 + 1}. source: ${item.source}${location ? ` | location: ${location}` : ""} | "${snippet}"${urlSuffix}`;
231130
231511
  });
231131
231512
  const steeringText = ["Selected review feedback to address", modeSummary, ...steeringItems].join("\n");
231132
- const priorAddressingById = new Map(task.reviewState.addressing.map((record2) => [record2.itemId, record2]));
231513
+ const priorAddressingById = new Map(reviewState.addressing.map((record2) => [record2.itemId, record2]));
231133
231514
  const nextAddressing = [
231134
- ...task.reviewState.addressing.filter((record2) => !selectedSet.has(record2.itemId)),
231515
+ ...reviewState.addressing.filter((record2) => !selectedSet.has(record2.itemId)),
231135
231516
  ...selectedItems.map((item) => {
231136
231517
  const existing = priorAddressingById.get(item.id);
231137
231518
  return {
@@ -231144,7 +231525,7 @@ ${normalizedDescription}`;
231144
231525
  stale: false,
231145
231526
  snapshot: {
231146
231527
  itemId: item.id,
231147
- sourceMode: task.reviewState?.source ?? "pull-request",
231528
+ sourceMode: reviewState.source,
231148
231529
  source: item.source,
231149
231530
  summary: item.summary,
231150
231531
  body: item.body,
@@ -231158,15 +231539,60 @@ ${normalizedDescription}`;
231158
231539
  };
231159
231540
  })
231160
231541
  ];
231161
- const reviewState = {
231162
- ...task.reviewState,
231542
+ const nextReviewState = {
231543
+ ...reviewState,
231163
231544
  addressing: nextAddressing
231164
231545
  };
231165
- await scopedStore.updateTask(task.id, { reviewState });
231546
+ await scopedStore.updateTask(task.id, { reviewState: nextReviewState });
231166
231547
  let steeringCommentId = null;
231167
231548
  const steeringComment = await scopedStore.addSteeringComment(task.id, steeringText, "user");
231168
231549
  steeringCommentId = steeringComment.id;
231169
231550
  let updatedTask = await scopedStore.getTask(task.id);
231551
+ if (task.column === "in-review") {
231552
+ updatedTask = (await reengageInReviewTaskForUserComment(scopedStore, updatedTask, {
231553
+ triggeringCommentType: "steering",
231554
+ triggeringCommentIds: steeringCommentId ? [steeringCommentId] : void 0,
231555
+ triggerDetail: "review-address"
231556
+ })).task;
231557
+ } else {
231558
+ const hasActiveSession = Boolean(updatedTask.sessionFile);
231559
+ if (steeringCommentId && updatedTask.column === "in-progress" && updatedTask.assignedAgentId && !hasActiveSession) {
231560
+ await triggerCommentWakeForAssignedAgent(scopedStore, updatedTask, {
231561
+ triggeringCommentType: "steering",
231562
+ triggeringCommentIds: [steeringCommentId],
231563
+ triggerDetail: "review-address"
231564
+ });
231565
+ }
231566
+ }
231567
+ await scopedStore.logEntry(task.id, "Same-task review revision requested", `${selectedItems.length} item(s) submitted from review tab`);
231568
+ res.json({ task: updatedTask, reviewState: nextReviewState });
231569
+ } catch (err) {
231570
+ if (err instanceof ApiError) {
231571
+ throw err;
231572
+ }
231573
+ rethrowAsApiError9(err);
231574
+ }
231575
+ });
231576
+ router.post("/tasks/:id/pr/address-feedback", async (req, res) => {
231577
+ try {
231578
+ const { store: scopedStore } = await getProjectContext3(req);
231579
+ const task = await scopedStore.getTask(req.params.id);
231580
+ const prInfo = task.prInfo ?? task.prInfos?.[0];
231581
+ if (!prInfo) {
231582
+ throw badRequest("Task must have a linked pull request before PR feedback can be addressed");
231583
+ }
231584
+ if (task.column !== "in-review" && task.column !== "in-progress") {
231585
+ throw badRequest("PR feedback can only be addressed for in-review or in-progress tasks");
231586
+ }
231587
+ const prLabel = `PR #${prInfo.number}`;
231588
+ const steeringText = [
231589
+ ADDRESS_PR_FEEDBACK_PROMPT,
231590
+ "If the compound-engineering skill is unavailable, inspect the linked pull request, evaluate each unresolved review thread, fix valid issues, reply with what changed or why no change was made, and resolve threads that are fully addressed.",
231591
+ `Context: ${prLabel} ${prInfo.url}`
231592
+ ].join("\n\n");
231593
+ const steeringComment = await scopedStore.addSteeringComment(task.id, steeringText, "user");
231594
+ const steeringCommentId = steeringComment.id;
231595
+ let updatedTask = await scopedStore.getTask(task.id);
231170
231596
  if (task.column === "in-review") {
231171
231597
  await scopedStore.updateTask(task.id, {
231172
231598
  status: null,
@@ -231180,15 +231606,15 @@ ${normalizedDescription}`;
231180
231606
  updatedTask = await scopedStore.moveTask(task.id, "in-progress", { preserveProgress: true });
231181
231607
  }
231182
231608
  const hasActiveSession = Boolean(updatedTask.sessionFile);
231183
- if (steeringCommentId && updatedTask.column === "in-progress" && updatedTask.assignedAgentId && !hasActiveSession) {
231609
+ if (updatedTask.column === "in-progress" && updatedTask.assignedAgentId && !hasActiveSession) {
231184
231610
  await triggerCommentWakeForAssignedAgent(scopedStore, updatedTask, {
231185
231611
  triggeringCommentType: "steering",
231186
231612
  triggeringCommentIds: [steeringCommentId],
231187
- triggerDetail: "review-address"
231613
+ triggerDetail: "pr-address-feedback"
231188
231614
  });
231189
231615
  }
231190
- await scopedStore.logEntry(task.id, "Same-task review revision requested", `${selectedItems.length} item(s) submitted from review tab`);
231191
- res.json({ task: updatedTask, reviewState });
231616
+ await scopedStore.logEntry(task.id, "Address PR feedback requested", `${prLabel} queued via ce-resolve-pr-feedback skill prompt`);
231617
+ res.json({ task: updatedTask });
231192
231618
  } catch (err) {
231193
231619
  if (err instanceof ApiError) {
231194
231620
  throw err;
@@ -231370,7 +231796,7 @@ ${normalizedDescription}`;
231370
231796
  }
231371
231797
  });
231372
231798
  }
231373
- var REVIEW_BLOCK_RE, REVIEW_VERDICT_RE, REVIEW_STEP_RE, DUPLICATE_STOPWORDS, ARTIFACT_TYPES, __fingerprintCreateLocksForTests, RESET_TASK_FIELDS, RESET_DRIFT_CORRECTION_FIELDS;
231799
+ var REVIEW_BLOCK_RE, REVIEW_VERDICT_RE, REVIEW_STEP_RE, DUPLICATE_STOPWORDS, ARTIFACT_TYPES, ADDRESS_PR_FEEDBACK_PROMPT, __fingerprintCreateLocksForTests, RESET_TASK_FIELDS, RESET_DRIFT_CORRECTION_FIELDS;
231374
231800
  var init_register_task_workflow_routes = __esm({
231375
231801
  "../dashboard/src/routes/register-task-workflow-routes.ts"() {
231376
231802
  "use strict";
@@ -231388,6 +231814,7 @@ var init_register_task_workflow_routes = __esm({
231388
231814
  REVIEW_STEP_RE = /^(plan|code) review Step (\d+): (APPROVE|REVISE|RETHINK|UNAVAILABLE)\b/i;
231389
231815
  DUPLICATE_STOPWORDS = /* @__PURE__ */ new Set(["a", "an", "the", "and", "or", "of", "to", "for", "in", "is", "on", "with", "fn"]);
231390
231816
  ARTIFACT_TYPES = /* @__PURE__ */ new Set(["document", "image", "video", "audio", "other"]);
231817
+ ADDRESS_PR_FEEDBACK_PROMPT = "Run /ce-resolve-pr-feedback to resolve open PR review feedback: evaluate each thread, fix valid issues, and reply.";
231391
231818
  __fingerprintCreateLocksForTests = deterministicGuardLocks;
231392
231819
  RESET_TASK_FIELDS = {
231393
231820
  worktree: null,
@@ -271981,9 +272408,7 @@ var init_register_project_routes = __esm({
271981
272408
  const tasks = await projectStore.listTasks({ slim: true });
271982
272409
  const activeCols = /* @__PURE__ */ new Set(["triage", "todo", "in-progress", "in-review"]);
271983
272410
  const activeTaskCount = tasks.filter((t) => activeCols.has(t.column)).length;
271984
- const inFlightAgentCount = tasks.filter(
271985
- (t) => t.column === "in-progress" || t.column === "triage" && t.status === "planning" && !t.paused
271986
- ).length;
272411
+ const inFlightAgentCount = countRunningAgentTasks(tasks);
271987
272412
  const totalTasksCompleted = tasks.filter((t) => t.column === "done" || t.column === "archived").length;
271988
272413
  const centralHealth = await central.getProjectHealth(req.params.id);
271989
272414
  const healthBase = centralHealth ?? {
@@ -279234,6 +279659,29 @@ function isCompatibleDefaultHeartbeatPath(path10, agent) {
279234
279659
  const safeId2 = agent.id.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
279235
279660
  return new RegExp(`^\\.fusion/agents/[^/]+-${safeId2}/HEARTBEAT\\.md$`).test(trimmed);
279236
279661
  }
279662
+ function withTaskDerivedTokenTotals(agents, scopedStore) {
279663
+ try {
279664
+ const tokenTotalsByAgentId = aggregateTaskTokenTotalsByAgentLink(scopedStore.getDatabase());
279665
+ return agents.map((agent) => {
279666
+ const storedInputTokens = agent.totalInputTokens ?? 0;
279667
+ const storedOutputTokens = agent.totalOutputTokens ?? 0;
279668
+ if (storedInputTokens > 0 || storedOutputTokens > 0) {
279669
+ return agent;
279670
+ }
279671
+ const taskTotals = tokenTotalsByAgentId.get(agent.id);
279672
+ if (!taskTotals || taskTotals.inputTokens <= 0 && taskTotals.outputTokens <= 0) {
279673
+ return agent;
279674
+ }
279675
+ return {
279676
+ ...agent,
279677
+ totalInputTokens: taskTotals.inputTokens,
279678
+ totalOutputTokens: taskTotals.outputTokens
279679
+ };
279680
+ });
279681
+ } catch {
279682
+ return agents;
279683
+ }
279684
+ }
279237
279685
  function withPendingApprovalCounts(agents, scopedStore) {
279238
279686
  try {
279239
279687
  const approvalStore = new ApprovalRequestStore(scopedStore.getDatabase());
@@ -279270,7 +279718,8 @@ function registerAgentCoreListCreateRoutes(ctx, deps) {
279270
279718
  await agentStore.init();
279271
279719
  const agents = await agentStore.listAgents(filter2);
279272
279720
  const sanitizedAgents = await sanitizeAgentTaskLinks(agents, scopedStore);
279273
- res.json(withPendingApprovalCounts(sanitizedAgents, scopedStore));
279721
+ const agentsWithTokenTotals = withTaskDerivedTokenTotals(sanitizedAgents, scopedStore);
279722
+ res.json(withPendingApprovalCounts(agentsWithTokenTotals, scopedStore));
279274
279723
  } catch (err) {
279275
279724
  if (err instanceof ApiError) {
279276
279725
  throw err;
@@ -280384,16 +280833,13 @@ function registerAgentRuntimeRoutes(ctx, deps) {
280384
280833
  router.get("/agents/:id/token-usage", async (req, res) => {
280385
280834
  try {
280386
280835
  const { store: scopedStore } = await getProjectContext3(req);
280387
- const { AgentStore: AgentStore2, aggregateAgentTokenUsage: aggregateAgentTokenUsage2, isEphemeralAgent: isEphemeralAgent2 } = await Promise.resolve().then(() => (init_src(), src_exports));
280836
+ const { AgentStore: AgentStore2, aggregateAgentTokenUsage: aggregateAgentTokenUsage2 } = await Promise.resolve().then(() => (init_src(), src_exports));
280388
280837
  const agentStore = new AgentStore2({ rootDir: scopedStore.getFusionDir() });
280389
280838
  await agentStore.init();
280390
280839
  const agent = await agentStore.getAgent(req.params.id);
280391
280840
  if (!agent) {
280392
280841
  throw notFound("Agent not found");
280393
280842
  }
280394
- if (isEphemeralAgent2(agent)) {
280395
- throw badRequest("Token usage is not available for ephemeral agents");
280396
- }
280397
280843
  const summary = await aggregateAgentTokenUsage2({
280398
280844
  taskStore: scopedStore,
280399
280845
  agentStore,
@@ -317168,6 +317614,7 @@ var init_log_sink = __esm({
317168
317614
  DashboardLogSink = class {
317169
317615
  tui = null;
317170
317616
  isTTY;
317617
+ silenced = false;
317171
317618
  originalConsole = null;
317172
317619
  constructor(tui) {
317173
317620
  this.tui = tui ?? null;
@@ -317177,7 +317624,26 @@ var init_log_sink = __esm({
317177
317624
  this.tui = tui;
317178
317625
  this.isTTY = true;
317179
317626
  }
317627
+ /*
317628
+ FNXC:DashboardShutdown 2026-06-28-00:00:
317629
+ Quit teardown spilled engine/mesh/dev-server log lines onto the user's
317630
+ restored shell — after `q`, dispose() called releaseConsole() (re-pointing
317631
+ console.* at the real terminal) and then tui.stop() left the alt-screen, so
317632
+ every subsequent slow-teardown log painted over the recovered prompt. That
317633
+ read as "the TUI keeps rendering after I get my terminal back."
317634
+ silence() makes the sink — and console.* — drop everything from here to
317635
+ process exit. It is irreversible by design; only shutdown calls it.
317636
+ */
317637
+ silence() {
317638
+ this.silenced = true;
317639
+ const noop3 = () => {
317640
+ };
317641
+ console.log = noop3;
317642
+ console.warn = noop3;
317643
+ console.error = noop3;
317644
+ }
317180
317645
  log(message, prefix) {
317646
+ if (this.silenced) return;
317181
317647
  const line = prefix ? `[${prefix}] ${message}` : message;
317182
317648
  if (this.tui && this.isTTY) {
317183
317649
  this.tui.log(message, prefix);
@@ -317188,6 +317654,7 @@ var init_log_sink = __esm({
317188
317654
  }
317189
317655
  }
317190
317656
  warn(message, prefix) {
317657
+ if (this.silenced) return;
317191
317658
  const line = prefix ? `[${prefix}] ${message}` : message;
317192
317659
  if (this.tui && this.isTTY) {
317193
317660
  this.tui.warn(message, prefix);
@@ -317198,6 +317665,7 @@ var init_log_sink = __esm({
317198
317665
  }
317199
317666
  }
317200
317667
  error(message, prefix) {
317668
+ if (this.silenced) return;
317201
317669
  const line = prefix ? `[${prefix}] ${message}` : message;
317202
317670
  if (this.tui && this.isTTY) {
317203
317671
  this.tui.error(message, prefix);
@@ -317954,7 +318422,7 @@ async function runDashboard(port, opts = {}) {
317954
318422
  let currentShutdownStep = null;
317955
318423
  function armHardExitWatchdog() {
317956
318424
  setTimeout(() => {
317957
- if (currentShutdownStep) {
318425
+ if (currentShutdownStep && process.env.FUSION_DEBUG_SHUTDOWN) {
317958
318426
  process.stderr.write(
317959
318427
  `fusion: graceful shutdown stalled on "${currentShutdownStep}" after ${SHUTDOWN_HARD_EXIT_GRACE_MS}ms \u2014 forcing exit
317960
318428
  `
@@ -317972,10 +318440,9 @@ async function runDashboard(port, opts = {}) {
317972
318440
  try {
317973
318441
  await fn2();
317974
318442
  const ms2 = Date.now() - startedAt;
317975
- if (debug) process.stderr.write(`fusion: shutdown step: ${label} done in ${ms2}ms
317976
- `);
317977
- else if (ms2 >= SHUTDOWN_STEP_SLOW_MS) {
317978
- process.stderr.write(`fusion: slow shutdown step: ${label} took ${ms2}ms
318443
+ if (debug) {
318444
+ const slow = ms2 >= SHUTDOWN_STEP_SLOW_MS ? " (slow)" : "";
318445
+ process.stderr.write(`fusion: shutdown step: ${label} done in ${ms2}ms${slow}
317979
318446
  `);
317980
318447
  }
317981
318448
  } catch (err) {
@@ -318334,7 +318801,7 @@ async function runDashboard(port, opts = {}) {
318334
318801
  tuiRefreshDebounceTimer = null;
318335
318802
  }
318336
318803
  if (tui) {
318337
- logSink.releaseConsole();
318804
+ logSink.silence();
318338
318805
  void tui.stop();
318339
318806
  }
318340
318807
  for (const { target, event, handler } of handlers) {
@@ -325897,23 +326364,19 @@ async function getTaskCounts(projectPath) {
325897
326364
  for (const col of COLUMNS) {
325898
326365
  counts[col] = 0;
325899
326366
  }
325900
- let activeTriagePlannerCount = 0;
325901
326367
  for (const task of tasks) {
325902
326368
  counts[task.column] = (counts[task.column] || 0) + 1;
325903
- if (task.column === "triage" && task.status === "planning" && !task.paused) {
325904
- activeTriagePlannerCount += 1;
325905
- }
325906
326369
  }
325907
- return { byColumn: counts, activeTriagePlannerCount };
326370
+ return { byColumn: counts, runningAgentCount: countRunningAgentTasks(tasks) };
325908
326371
  } catch {
325909
- return { byColumn: {}, activeTriagePlannerCount: 0 };
326372
+ return { byColumn: {}, runningAgentCount: 0 };
325910
326373
  }
325911
326374
  }
325912
326375
  async function getProjectHealth(central, projectId) {
325913
326376
  return central.getProjectHealth(projectId);
325914
326377
  }
325915
326378
  function getLiveInFlightAgentCount(taskCounts) {
325916
- return (taskCounts.byColumn["in-progress"] ?? 0) + taskCounts.activeTriagePlannerCount;
326379
+ return taskCounts.runningAgentCount;
325917
326380
  }
325918
326381
  function buildDisplayHealth(taskCounts, health) {
325919
326382
  return {