@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/extension.js CHANGED
@@ -564,6 +564,20 @@ var init_mcp_config = __esm({
564
564
  }
565
565
  });
566
566
 
567
+ // ../core/src/task-helpers.ts
568
+ function getPrimaryPrInfo(task) {
569
+ return task.prInfos?.[0] ?? task.prInfo;
570
+ }
571
+ function taskHasManualOpenPullRequest(task) {
572
+ const prs = task.prInfos ?? (task.prInfo ? [task.prInfo] : []);
573
+ return prs.some((pr2) => pr2.manual === true && (pr2.status === "open" || pr2.status === "draft"));
574
+ }
575
+ var init_task_helpers = __esm({
576
+ "../core/src/task-helpers.ts"() {
577
+ "use strict";
578
+ }
579
+ });
580
+
567
581
  // ../core/src/task-merge.ts
568
582
  function isFusionSiblingBranch(branch) {
569
583
  return FUSION_SIBLING_BRANCH_RE.test(branch);
@@ -572,7 +586,7 @@ function resolveEffectiveAutoMerge(task, settings) {
572
586
  return task.autoMerge ?? settings.autoMerge;
573
587
  }
574
588
  function allowsAutoMergeProcessing(task, settings) {
575
- return settings.autoMerge !== false || task.autoMerge === true;
589
+ return (settings.autoMerge !== false || task.autoMerge === true) && !taskHasManualOpenPullRequest(task);
576
590
  }
577
591
  function resolveEffectiveGroupAutoMerge(group, settings) {
578
592
  return group.autoMerge ?? settings.autoMerge;
@@ -688,6 +702,7 @@ var FUSION_SIBLING_BRANCH_RE, HARD_BLOCKING_TASK_STATUSES, SCHEDULER_TRANSIENT_S
688
702
  var init_task_merge = __esm({
689
703
  "../core/src/task-merge.ts"() {
690
704
  "use strict";
705
+ init_task_helpers();
691
706
  FUSION_SIBLING_BRANCH_RE = /^fusion\/fn-/i;
692
707
  HARD_BLOCKING_TASK_STATUSES = /* @__PURE__ */ new Set([
693
708
  "failed",
@@ -1244,6 +1259,8 @@ var init_settings_schema = __esm({
1244
1259
  testCommand: void 0,
1245
1260
  buildCommand: void 0,
1246
1261
  recycleWorktrees: false,
1262
+ showWorktreeGrouping: false,
1263
+ openTasksInRightSidebar: false,
1247
1264
  executorAllowSiblingBranchRename: false,
1248
1265
  worktreeNaming: "random",
1249
1266
  worktrunk: {
@@ -1411,6 +1428,8 @@ var init_settings_schema = __esm({
1411
1428
  titleSummarizerModelId: void 0,
1412
1429
  titleSummarizerFallbackProvider: void 0,
1413
1430
  titleSummarizerFallbackModelId: void 0,
1431
+ prTitlePromptInstructions: void 0,
1432
+ prDescriptionPromptInstructions: void 0,
1414
1433
  scripts: void 0,
1415
1434
  setupScript: void 0,
1416
1435
  insightExtractionEnabled: false,
@@ -4611,19 +4630,21 @@ The tool prevents your session from being killed by the inactivity watchdog duri
4611
4630
  - 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.
4612
4631
  - If you need to run \`pnpm install\` (e.g. you added a new package), use \`fn_run_verification\` with \`scope: "workspace"\` and \`timeoutSec: 600\`.
4613
4632
  - 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.`;
4614
- 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.
4615
-
4616
- ## Your Role
4617
- You are a fast-path spec writer. Keep output lean but executable, with enough precision that an executor can run immediately.
4633
+ FAST_TRIAGE_PROMPT_TEXT = `You are a task specification agent for "fn". This task is running in **fast mode**.
4618
4634
 
4619
- Your job: turn a rough task description into a focused PROMPT.md another agent can execute autonomously.
4635
+ Write a lean, executable PROMPT.md quickly. Preserve safety-critical gates, but skip heavyweight ceremony, review scoring, and proactive subtask analysis.
4620
4636
 
4621
- ## What you produce
4622
- Write a complete PROMPT.md specification to the given path using the write tool.
4637
+ ## Fast-mode priorities
4638
+ - Read only the source/docs needed to make the spec precise.
4639
+ - Keep prose brief; use concrete file paths, commands, and expected outcomes.
4640
+ - Do not expand scope. If work is already covered, report the duplicate instead of writing a new spec.
4641
+ - Keep the selected workflow unchanged unless the user explicitly requested a workflow for this task.
4623
4642
 
4624
- ## PROMPT.md Format
4643
+ ## Duplicate check
4644
+ 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}\`.
4625
4645
 
4626
- Follow this structure exactly:
4646
+ ## Required PROMPT.md shape
4647
+ Write a real PROMPT.md to the requested path using the write tool:
4627
4648
 
4628
4649
  \`\`\`markdown
4629
4650
  # Task: {ID} - {Name}
@@ -4632,205 +4653,69 @@ Follow this structure exactly:
4632
4653
  **Size:** {S | M}
4633
4654
 
4634
4655
  ## Mission
4635
-
4636
- {One paragraph: what to build and why it matters}
4637
-
4638
- ## Surface Enumeration
4639
-
4640
- {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.}
4641
-
4642
- ## Symptom Verification
4643
-
4644
- {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.}
4656
+ {One short paragraph describing the outcome and why it matters.}
4645
4657
 
4646
4658
  ## Dependencies
4647
-
4648
4659
  - **None**
4649
- {OR}
4650
- - **Task:** {ID} ({what must be complete first})
4660
+ {or list concrete task dependencies after inspecting them with fn_task_show.}
4651
4661
 
4652
4662
  ## Context to Read First
4653
-
4654
- {List the minimal, specific files needed for implementation}
4663
+ - \`specific/file-or-doc.md\` \u2014 why it matters
4655
4664
 
4656
4665
  ## File Scope
4657
-
4658
- {List exact files/directories expected to change}
4659
-
4660
- - \`path/to/file.ext\`
4661
- - \`path/to/directory/*\`
4666
+ - \`expected/file/or/directory\`
4662
4667
 
4663
4668
  ## Steps
4664
-
4665
- > Optional: a step heading may carry a \`(depends: N,M)\` annotation listing the 1-indexed
4666
- > step numbers it depends on \u2014 e.g. \`### Step 3 (depends: 1): Title\`. Annotate ONLY steps
4667
- > that are genuinely independent of their immediate predecessor; an unannotated step is
4668
- > assumed to depend on the one before it (fully sequential). Be conservative \u2014 only mark a
4669
- > step independent when it truly does not read or modify the prior step's output.
4670
-
4671
4669
  ### Step 0: Preflight
4670
+ - [ ] Confirm required context, dependencies, and paths.
4672
4671
 
4673
- - [ ] Required files and paths exist
4674
- - [ ] Dependencies satisfied
4672
+ ### Step 1: {Outcome-oriented implementation step}
4673
+ - [ ] {Specific verifiable outcome.}
4674
+ - [ ] {Specific verifiable outcome.}
4675
4675
 
4676
- ### Step 1: {Implementation step name}
4676
+ ### Step N-1: Testing & Verification
4677
+ - [ ] Run targeted real automated tests in the project test runner; typecheck/build/manual checks do not replace tests.
4678
+ - [ ] Run lint/typecheck/build commands required by the task or repo.
4677
4679
 
4678
- - [ ] {Specific, verifiable outcome}
4679
- - [ ] {Specific, verifiable outcome}
4680
- - [ ] 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)
4681
-
4682
- For bug-fix and UI-affordance add/remove tasks, paste and fill in this checklist in the \`## Surface Enumeration\` section:
4683
- - [ ] Providers / bridges / execution paths touched by the invariant
4684
- - [ ] Desktop + mobile breakpoints / platforms that exercise the behavior
4685
- - [ ] Empty / undefined / duplicate / populated data states
4686
- - [ ] Shared hooks / components / modules / helpers reusing the logic
4687
- - [ ] Every component that renders the affordance (search the codebase for the icon/class/testid, not just the one the user pointed at)
4688
- - [ ] Leftover shells after removal \u2014 empty buttons, orphaned click targets, now-unused wrappers, dangling aria-labels \u2014 are explicitly checked and fixed/hidden
4689
-
4690
- For bug-class/bug-fix tasks, add and fill in the exact \`## Symptom Verification\` section:
4691
- - [ ] **Original symptom** \u2014 what the user/issue reported was broken
4692
- - [ ] **Exact reproduction** \u2014 the precise steps, inputs, fixture, or automated repro that triggered the failure
4693
- - [ ] **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
4694
-
4695
- **Artifacts:**
4696
- - \`path/to/file\` (new | modified)
4697
-
4698
- ### Step {N-1}: Testing & Verification
4699
-
4700
- > 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.
4701
- > If keeping lint/tests/build/typecheck green requires edits outside the initial File Scope, make those fixes as part of this task.
4702
-
4703
- - [ ] Run lint check (\`pnpm lint\`)
4704
- - [ ] Run impacted tests
4705
- - [ ] Run project typecheck if available
4706
- - [ ] Build passes
4707
-
4708
- ### Step {N}: Documentation & Delivery
4709
-
4710
- - [ ] Update relevant documentation
4711
- - [ ] Save documentation deliverables as task documents via \`fn_task_document_write\` (key="docs", content=...)
4712
- - [ ] Create out-of-scope follow-up tasks via \`fn_task_create\` when needed
4680
+ ### Step N: Documentation & Delivery
4681
+ - [ ] Update required docs/changesets/task documents.
4682
+ - [ ] Create follow-up tasks for out-of-scope findings.
4713
4683
 
4714
4684
  ## Documentation Requirements
4715
-
4716
4685
  **Must Update:**
4717
- - \`path/to/doc.md\` \u2014 {what to add/change}
4686
+ - \`path/to/doc-or-changeset.md\` \u2014 required update, or **None**
4718
4687
 
4719
4688
  **Check If Affected:**
4720
- - \`path/to/doc.md\` \u2014 {update if relevant}
4689
+ - \`path/to/doc.md\` \u2014 update if impacted
4721
4690
 
4722
4691
  ## Completion Criteria
4723
-
4724
- - [ ] All steps complete
4725
- - [ ] Lint passing
4726
- - [ ] All tests passing
4727
- - [ ] Typecheck passing (if available)
4728
- - [ ] Documentation updated
4692
+ - [ ] Implementation outcomes complete
4693
+ - [ ] Required automated tests, lint/typecheck/build pass
4694
+ - [ ] Documentation/delivery complete
4729
4695
 
4730
4696
  ## Git Commit Convention
4731
-
4732
- Commits at step boundaries. All commits include the task ID:
4733
-
4734
- - **Step completion:** \`feat({ID}): complete Step N \u2014 <short summary>\` (the \`<short summary>\` is required \u2014 use a concrete 5\u201310 word description)
4735
- - **Bug fixes:** \`fix({ID}): description\` (short, concrete summary required)
4736
- - **Tests:** \`test({ID}): description\` (short, concrete summary required)
4737
-
4738
- Good examples:
4739
- - \`feat(FN-1234): complete Step 2 \u2014 add retry guard for workflow step timeouts\`
4740
- - \`test(FN-1234): add regression tests for paused-session cleanup\`
4741
-
4742
- Bad example:
4743
- - \`feat(FN-1234): complete Step 2\`
4697
+ - Use task ID-prefixed conventional commits, e.g. \`feat({ID}): complete Step 1 \u2014 concrete summary\`.
4744
4698
 
4745
4699
  ## Do NOT
4746
-
4747
- - Expand task scope
4748
- - Skip tests
4749
- - Refuse necessary fixes just because they touch files outside the initial File Scope
4750
- - Commit without the task ID prefix
4751
- - Remove, delete, or gut modules, settings, interfaces, exports, or test files outside the File Scope
4752
- - Remove features as "cleanup" \u2014 if something seems unused, create a task via \`fn_task_create\`
4753
-
4754
- ## Changeset Requirements
4755
-
4756
- If this task REMOVES existing functionality (deleting modules, settings, API endpoints, or exports), a changeset file is REQUIRED:
4757
- - Create \`.changeset/{task-id}-removal.md\` explaining what was removed and why
4758
- - This is mandatory for any net-negative change (more deletions than additions to existing files)
4700
+ - Expand scope, skip tests, weaken acceptance, or delete/gut unrelated modules/features.
4759
4701
  \`\`\`
4760
4702
 
4761
- ## Testing requirements
4762
- - Require real automated tests with assertions that run in the project's test runner
4763
- - Typecheck/build/manual checks are not tests and cannot replace tests
4764
- - 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.
4765
- - 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.
4766
- - 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)
4767
- - 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\`.
4768
- - 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
4769
-
4770
- ## Duplicate check
4771
- 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).
4772
- For any likely match in \`done\` or \`archived\`, call \`fn_task_show\` to inspect details before deciding.
4773
- If an existing task already covers the same work, do NOT write a PROMPT.md. Instead write exactly:
4774
- \`DUPLICATE: {existing-task-id}\`
4775
-
4776
- ## Dependency awareness
4777
- When adding a dependency in \`## Dependencies\`, first call \`fn_task_show\` for that task and read its PROMPT.md.
4778
- Use that context to align file paths, APIs, assumptions, and completion expectations. If the dependency has no PROMPT.md yet, note that explicitly.
4779
-
4780
- ## Decision-only task flag (noCommitsExpected)
4781
- When ALL of the following are true, include this metadata line in the header block after Size:
4782
-
4783
- - Add this exact line: **No commits expected:** true
4784
-
4785
- Set it only when all of these conditions hold:
4786
- - 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
4787
- - 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
4788
- - 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"
4789
-
4790
- Anti-heuristics (bias to false-negative when ambiguous):
4791
- - SET: Decide whether FN-XYZ needs a fix
4792
- - SET: Assign ready implementation task to active owner, or record no-route state (no source files expected)
4793
- - LEAVE UNSET: Investigate FN-XYZ
4794
- - LEAVE UNSET: Investigate FN-XYZ and fix if needed
4795
- - LEAVE UNSET: Investigate and fix routing if needed
4796
-
4797
- 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.
4798
-
4799
- ## Guidelines
4800
- - Read relevant source files before writing the spec
4801
- - Be specific: reference concrete files, modules, and commands from this repo
4802
- - Keep steps outcome-focused with 2\u20134 checkboxes per step
4803
- - Keep file scope realistic: include tests and integration touchpoints likely required for green quality gates
4804
- - Always include Testing & Verification and Documentation & Delivery steps
4805
- - Keep fast-mode scope lean and executable; do not add heavyweight review scoring or subtask-analysis sections
4806
-
4807
- ## Project commands
4808
- When the user prompt includes explicit test/build commands, use those exact commands in the generated spec.
4809
-
4810
- <!--
4811
- FNXC:WorkflowRouting 2026-06-22-17:24:
4812
- 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.
4813
- -->
4814
- ## Workflow Routing
4815
- 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.
4816
-
4817
- ## Task Artifact Location for Forensic / Reconciliation Tasks
4818
-
4819
- 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.
4820
- - Target-task files live at \`<rootDir>/.fusion/tasks/{TARGET_ID}/\` (\`task.json\`, \`PROMPT.md\`, \`attachments/\`, logs).
4821
- - Task DB truth lives at \`<rootDir>/.fusion/fusion.db\` (SQLite/WAL) and should be accessed via \`TaskStore\`/task tools, not direct SQL edits.
4822
- - \`.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.
4823
-
4824
- ## Spec Review
4825
-
4826
- After writing the PROMPT.md, call \`fn_review_spec()\` to confirm the spec.
4703
+ ## Bug and UI-affordance gates
4704
+ - 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.
4705
+ - 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.
4827
4706
 
4828
- 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.
4707
+ ## Testing requirements
4708
+ - Every implementation spec needs a Testing & Verification step with real automated tests and bounded commands.
4709
+ - Prefer changed-file/package-scoped tests; do not require full workspace suites unless the user/task explicitly requires them.
4710
+ - If the user supplied exact test/build commands, include those exact commands.
4829
4711
 
4830
- 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.
4712
+ ## Safety and workflow rules
4713
+ - Never kill port 4040; use a random/free port for test servers.
4714
+ - 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.
4715
+ - 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\`.
4831
4716
 
4832
4717
  ## Output
4833
- Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\` to confirm.`;
4718
+ Write PROMPT.md directly, then call \`fn_review_spec()\`. Fast-mode specs auto-approve, but the call is still required.`;
4834
4719
  TRIAGE_PROMPT_TEXT = `You are a task specification agent for "fn", an AI-orchestrated task board.
4835
4720
 
4836
4721
  ## Your Role
@@ -10723,6 +10608,18 @@ function defaultColumnForLinearNode(node) {
10723
10608
  if (seam === "merge") return "in-review";
10724
10609
  return "todo";
10725
10610
  }
10611
+ function canonicalBuiltinWorkflowColumns() {
10612
+ if (BUILTIN_CODING_WORKFLOW_IR.version !== "v2") {
10613
+ throw new Error("builtin coding workflow must be v2 to provide canonical columns");
10614
+ }
10615
+ return BUILTIN_CODING_WORKFLOW_IR.columns.map((column) => ({
10616
+ ...column,
10617
+ traits: column.traits.map((trait) => ({
10618
+ ...trait,
10619
+ config: trait.config ? { ...trait.config } : void 0
10620
+ }))
10621
+ }));
10622
+ }
10726
10623
  function linear(spec) {
10727
10624
  const nodes = [
10728
10625
  { id: "start", kind: "start" },
@@ -10742,14 +10639,16 @@ function linear(spec) {
10742
10639
  nodes.forEach((node, i) => {
10743
10640
  layout[node.id] = { x: 60 + i * 170, y: 160 };
10744
10641
  });
10745
- const hasV2OnlyNode = nodes.some((node) => !V1_LINEAR_NODE_KINDS.has(node.kind));
10746
- const ir2 = hasV2OnlyNode ? parseWorkflowIr({
10642
+ const ir2 = parseWorkflowIr({
10747
10643
  version: "v2",
10748
10644
  name: spec.name,
10749
- columns: DEFAULT_WORKFLOW_COLUMN_IDS.map((id) => ({ id, name: id, traits: [] })),
10645
+ columns: canonicalBuiltinWorkflowColumns(),
10750
10646
  nodes: nodes.map((node) => node.column ? node : { ...node, column: defaultColumnForLinearNode(node) }),
10751
10647
  edges
10752
- }) : parseWorkflowIr({ version: "v1", name: spec.name, nodes, edges });
10648
+ });
10649
+ if (ir2.version !== "v2" || !ir2.columns.find((column) => column.id === "todo")?.traits.some((trait) => trait.trait === "hold")) {
10650
+ throw new Error(`linear built-in workflow '${spec.id}' must synthesize a hold-capacity todo column`);
10651
+ }
10753
10652
  if (ir2.version === "v2") {
10754
10653
  ir2.settings = BUILTIN_WORKFLOW_SETTINGS;
10755
10654
  }
@@ -10769,7 +10668,7 @@ function linear(spec) {
10769
10668
  function getBuiltinWorkflow(id) {
10770
10669
  return BUILTIN_BY_ID.get(id);
10771
10670
  }
10772
- var BUILTIN_WORKFLOW_ID_PREFIX, PLUGIN_GATED_BUILTIN_WORKFLOWS, BUILTIN_TS, V1_LINEAR_NODE_KINDS, BUILTIN_WORKFLOWS, BUILTIN_BY_ID;
10671
+ var BUILTIN_WORKFLOW_ID_PREFIX, PLUGIN_GATED_BUILTIN_WORKFLOWS, BUILTIN_TS, BUILTIN_WORKFLOWS, BUILTIN_BY_ID;
10773
10672
  var init_builtin_workflows = __esm({
10774
10673
  "../core/src/builtin-workflows.ts"() {
10775
10674
  "use strict";
@@ -10786,7 +10685,6 @@ var init_builtin_workflows = __esm({
10786
10685
  ["builtin:compound-engineering", "fusion-plugin-compound-engineering"]
10787
10686
  ]);
10788
10687
  BUILTIN_TS = "2026-01-01T00:00:00.000Z";
10789
- V1_LINEAR_NODE_KINDS = /* @__PURE__ */ new Set(["start", "prompt", "script", "gate", "end"]);
10790
10688
  BUILTIN_WORKFLOWS = [
10791
10689
  {
10792
10690
  id: "builtin:coding",
@@ -11300,6 +11198,21 @@ function setRunningAgentCountSource(fn2) {
11300
11198
  function getRunningAgentCountSource() {
11301
11199
  return runningAgentCountSource;
11302
11200
  }
11201
+ function isRunningAgentTask(task) {
11202
+ if (task.column === "in-progress") {
11203
+ return true;
11204
+ }
11205
+ if (task.column === "triage") {
11206
+ return task.status === "planning" && !task.paused;
11207
+ }
11208
+ if (task.column === "in-review") {
11209
+ return ACTIVE_IN_REVIEW_AGENT_STATUSES.has(String(task.status ?? "")) && !task.paused;
11210
+ }
11211
+ return false;
11212
+ }
11213
+ function countRunningAgentTasks(tasks) {
11214
+ return tasks.filter(isRunningAgentTask).length;
11215
+ }
11303
11216
  function deriveRunningAgentCounts(perProject) {
11304
11217
  const projectsActive = {};
11305
11218
  let currentlyActive = 0;
@@ -11312,10 +11225,11 @@ function deriveRunningAgentCounts(perProject) {
11312
11225
  }
11313
11226
  return { currentlyActive, projectsActive };
11314
11227
  }
11315
- var runningAgentCountSource;
11228
+ var ACTIVE_IN_REVIEW_AGENT_STATUSES, runningAgentCountSource;
11316
11229
  var init_live_agent_count = __esm({
11317
11230
  "../core/src/live-agent-count.ts"() {
11318
11231
  "use strict";
11232
+ ACTIVE_IN_REVIEW_AGENT_STATUSES = /* @__PURE__ */ new Set(["merging", "merging-pr", "merging-fix", "reviewing", "fixing"]);
11319
11233
  }
11320
11234
  });
11321
11235
 
@@ -47778,6 +47692,35 @@ var init_automation_store = __esm({
47778
47692
  return schedule;
47779
47693
  });
47780
47694
  }
47695
+ /**
47696
+ * Atomically claim one due schedule occurrence before execution.
47697
+ *
47698
+ * FNXC:Automations 2026-06-27-00:00:
47699
+ * 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.
47700
+ */
47701
+ async claimDueSchedule(id, expectedNextRunAt) {
47702
+ return this.withScheduleLock(id, async () => {
47703
+ const row = this.db.prepare(
47704
+ "SELECT id, cronExpression, enabled, nextRunAt FROM automations WHERE id = ?"
47705
+ ).get(id);
47706
+ if (!row || row.enabled !== 1 || !row.nextRunAt) {
47707
+ return false;
47708
+ }
47709
+ const nextRunAt = this.computeNextRun(row.cronExpression);
47710
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
47711
+ const result = this.db.prepare(`
47712
+ UPDATE automations
47713
+ SET nextRunAt = ?, updatedAt = ?
47714
+ WHERE id = ? AND enabled = 1 AND nextRunAt = ?
47715
+ `).run(nextRunAt, updatedAt, id, expectedNextRunAt);
47716
+ const changes = typeof result.changes === "bigint" ? Number(result.changes) : result.changes;
47717
+ if (changes === 1) {
47718
+ this.db.bumpLastModified();
47719
+ return true;
47720
+ }
47721
+ return false;
47722
+ });
47723
+ }
47781
47724
  /**
47782
47725
  * Record a run result for a schedule. Updates lastRunAt, lastRunResult,
47783
47726
  * nextRunAt, runCount, and appends to runHistory.
@@ -54600,20 +54543,10 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
54600
54543
  throw new Error("Feedback is required and cannot be empty");
54601
54544
  }
54602
54545
  const now = (/* @__PURE__ */ new Date()).toISOString();
54603
- let sourceLabel;
54604
- if (sourceTask.title?.trim()) {
54605
- sourceLabel = sourceTask.title.trim();
54606
- } else {
54607
- const firstLine = sourceTask.description.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
54608
- sourceLabel = firstLine ? firstLine.replace(/\s+/g, " ") : sourceTask.id;
54609
- }
54546
+ const normalizedFeedback = feedback.trim().replace(/\s+/g, " ");
54547
+ const refinementTitle = `${id}: ${normalizedFeedback}`.slice(0, MAX_TITLE_LENGTH).trim();
54610
54548
  return this.createTaskWithDistributedReservation({ description: feedback.trim() }, {
54611
54549
  createTaskWithId: async (newId, reservationCommit) => {
54612
- const normalizedTitle = normalizeTitleForTaskId(`Refinement: ${sourceLabel}`, newId);
54613
- if (normalizedTitle.changed) {
54614
- const removed = extractTaskIdTokens(`Refinement: ${sourceLabel}`).filter((token) => token !== newId.toUpperCase());
54615
- storeLog.log(`[title-id-drift] normalized title for ${newId}: removed=[${removed.join(",")}]`);
54616
- }
54617
54550
  const sourceGithubLinked = sourceTask.githubTracking?.enabled === true || Boolean(sourceTask.githubTracking?.issue);
54618
54551
  const refinementGithubTracking = sourceGithubLinked ? {
54619
54552
  enabled: true,
@@ -54622,7 +54555,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
54622
54555
  const newTask = {
54623
54556
  id: newId,
54624
54557
  lineageId: generateTaskLineageId(),
54625
- title: normalizedTitle.title ?? "Refinement",
54558
+ title: refinementTitle,
54626
54559
  description: `${feedback.trim()}
54627
54560
 
54628
54561
  Refines: ${id}`,
@@ -60870,6 +60803,9 @@ ${newTask.description}
60870
60803
  /**
60871
60804
  * FNXC:ArtifactRegistry 2026-06-19-22:04:
60872
60805
  * 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.
60806
+ *
60807
+ * FNXC:ArtifactRegistry 2026-06-27-00:00:
60808
+ * 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.
60873
60809
  */
60874
60810
  async registerArtifact(input) {
60875
60811
  const id = randomUUID15();
@@ -60897,7 +60833,9 @@ ${newTask.description}
60897
60833
  throw error2;
60898
60834
  }
60899
60835
  };
60900
- return input.taskId ? this.withTaskLock(input.taskId, register) : register();
60836
+ const artifact = input.taskId ? await this.withTaskLock(input.taskId, register) : await register();
60837
+ this.emit("artifact:registered", artifact);
60838
+ return artifact;
60901
60839
  }
60902
60840
  /**
60903
60841
  * FNXC:ArtifactRegistry 2026-06-19-22:04:
@@ -64154,6 +64092,51 @@ var init_retry_storm_error = __esm({
64154
64092
  });
64155
64093
 
64156
64094
  // ../core/src/agent-token-usage.ts
64095
+ function aggregateTaskTokenTotalsByAgentLink(db) {
64096
+ const rows = db.prepare(`
64097
+ SELECT
64098
+ id AS taskId,
64099
+ assignedAgentId,
64100
+ sourceAgentId,
64101
+ checkedOutBy,
64102
+ tokenUsageInputTokens AS inputTokens,
64103
+ tokenUsageCachedTokens AS cachedTokens,
64104
+ tokenUsageCacheWriteTokens AS cacheWriteTokens,
64105
+ tokenUsageOutputTokens AS outputTokens,
64106
+ tokenUsageTotalTokens AS totalTokens
64107
+ FROM tasks
64108
+ WHERE tokenUsageInputTokens IS NOT NULL
64109
+ OR tokenUsageCachedTokens IS NOT NULL
64110
+ OR tokenUsageCacheWriteTokens IS NOT NULL
64111
+ OR tokenUsageOutputTokens IS NOT NULL
64112
+ OR tokenUsageTotalTokens IS NOT NULL
64113
+ `).all();
64114
+ const totalsByAgentId = /* @__PURE__ */ new Map();
64115
+ for (const row of rows) {
64116
+ const agentIds = new Set([row.assignedAgentId, row.sourceAgentId, row.checkedOutBy].filter((value) => Boolean(value)));
64117
+ for (const agentId of agentIds) {
64118
+ const existing = totalsByAgentId.get(agentId) ?? createTaskTokenTotals();
64119
+ existing.inputTokens += row.inputTokens ?? 0;
64120
+ existing.cachedTokens += row.cachedTokens ?? 0;
64121
+ existing.cacheWriteTokens += row.cacheWriteTokens ?? 0;
64122
+ existing.outputTokens += row.outputTokens ?? 0;
64123
+ existing.totalTokens += row.totalTokens ?? (row.inputTokens ?? 0) + (row.cachedTokens ?? 0) + (row.cacheWriteTokens ?? 0) + (row.outputTokens ?? 0);
64124
+ existing.nTasks += 1;
64125
+ totalsByAgentId.set(agentId, existing);
64126
+ }
64127
+ }
64128
+ return totalsByAgentId;
64129
+ }
64130
+ function createTaskTokenTotals() {
64131
+ return {
64132
+ inputTokens: 0,
64133
+ cachedTokens: 0,
64134
+ cacheWriteTokens: 0,
64135
+ outputTokens: 0,
64136
+ totalTokens: 0,
64137
+ nTasks: 0
64138
+ };
64139
+ }
64157
64140
  async function aggregateAgentTokenUsage({
64158
64141
  taskStore,
64159
64142
  agentStore,
@@ -64161,7 +64144,7 @@ async function aggregateAgentTokenUsage({
64161
64144
  now = /* @__PURE__ */ new Date()
64162
64145
  }) {
64163
64146
  const agent = await agentStore.getAgent(agentId);
64164
- if (!agent || isEphemeralAgent(agent)) {
64147
+ if (!agent) {
64165
64148
  return null;
64166
64149
  }
64167
64150
  const tasks = await taskStore.listTasks({ slim: true, includeArchived: true });
@@ -64221,7 +64204,6 @@ function finalizeWindowSummary(summary) {
64221
64204
  var init_agent_token_usage = __esm({
64222
64205
  "../core/src/agent-token-usage.ts"() {
64223
64206
  "use strict";
64224
- init_types();
64225
64207
  }
64226
64208
  });
64227
64209
 
@@ -66135,16 +66117,6 @@ var init_otel_metrics = __esm({
66135
66117
  }
66136
66118
  });
66137
66119
 
66138
- // ../core/src/task-helpers.ts
66139
- function getPrimaryPrInfo(task) {
66140
- return task.prInfos?.[0] ?? task.prInfo;
66141
- }
66142
- var init_task_helpers = __esm({
66143
- "../core/src/task-helpers.ts"() {
66144
- "use strict";
66145
- }
66146
- });
66147
-
66148
66120
  // ../core/src/manual-retry-reset.ts
66149
66121
  function buildAutoPauseClearPatch(task) {
66150
66122
  if (task.paused === true && task.userPaused !== true && task.pausedReason === IN_REVIEW_STALL_DEADLOCK_PAUSE_REASON) {
@@ -88160,6 +88132,7 @@ __export(src_exports, {
88160
88132
  aggregatePluginActivations: () => aggregatePluginActivations,
88161
88133
  aggregateProductivityAnalytics: () => aggregateProductivityAnalytics,
88162
88134
  aggregateSignalsAnalytics: () => aggregateSignalsAnalytics,
88135
+ aggregateTaskTokenTotalsByAgentLink: () => aggregateTaskTokenTotalsByAgentLink,
88163
88136
  aggregateTeamAnalytics: () => aggregateTeamAnalytics,
88164
88137
  aggregateTokenAnalytics: () => aggregateTokenAnalytics,
88165
88138
  aggregateToolAnalytics: () => aggregateToolAnalytics,
@@ -88250,6 +88223,7 @@ __export(src_exports, {
88250
88223
  countDualAcceptDisagreements: () => countDualAcceptDisagreements,
88251
88224
  countInterventions: () => countInterventions,
88252
88225
  countRecentIdenticalStallEntries: () => countRecentIdenticalStallEntries,
88226
+ countRunningAgentTasks: () => countRunningAgentTasks,
88253
88227
  countUsageEventsBy: () => countUsageEventsBy,
88254
88228
  createActivityLogSnapshot: () => createActivityLogSnapshot,
88255
88229
  createAgentRunSnapshot: () => createAgentRunSnapshot,
@@ -88452,6 +88426,7 @@ __export(src_exports, {
88452
88426
  isResearchExperimentalEnabled: () => isResearchExperimentalEnabled,
88453
88427
  isRetryableMeshWriteFailure: () => isRetryableMeshWriteFailure,
88454
88428
  isRunRecord: () => isRunRecord,
88429
+ isRunningAgentTask: () => isRunningAgentTask,
88455
88430
  isSandboxExperimentalEnabled: () => isSandboxExperimentalEnabled,
88456
88431
  isSecretAccessPolicy: () => isSecretAccessPolicy,
88457
88432
  isSecretScope: () => isSecretScope,
@@ -88713,6 +88688,7 @@ __export(src_exports, {
88713
88688
  syncMemoryBackupRoutine: () => syncMemoryBackupRoutine,
88714
88689
  syncMemoryDreamsAutomation: () => syncMemoryDreamsAutomation,
88715
88690
  syncScheduledEvalBatchAutomation: () => syncScheduledEvalBatchAutomation,
88691
+ taskHasManualOpenPullRequest: () => taskHasManualOpenPullRequest,
88716
88692
  taskMatchesReplicatedCreate: () => taskMatchesReplicatedCreate,
88717
88693
  toJson: () => toJson,
88718
88694
  toJsonNullable: () => toJsonNullable,
@@ -88873,7 +88849,6 @@ var init_src = __esm({
88873
88849
  init_stalled_review_detector();
88874
88850
  init_task_id_integrity();
88875
88851
  init_task_title_id_drift();
88876
- init_task_helpers();
88877
88852
  init_manual_retry_reset();
88878
88853
  init_task_lineage();
88879
88854
  init_distributed_task_id();
@@ -88893,6 +88868,7 @@ var init_src = __esm({
88893
88868
  init_blocker_fanout();
88894
88869
  init_capacity();
88895
88870
  init_dependency_blocked_todo_report();
88871
+ init_task_helpers();
88896
88872
  init_task_merge();
88897
88873
  init_branch_group_completion();
88898
88874
  init_pr_entity();
@@ -89629,6 +89605,11 @@ var init_gating_classifications = __esm({
89629
89605
  "fn_task_done",
89630
89606
  "fn_heartbeat_done",
89631
89607
  "fn_memory_append",
89608
+ /**
89609
+ * FNXC:ToolGovernance 2026-06-28-00:00:
89610
+ * 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.
89611
+ */
89612
+ "fn_ask_question",
89632
89613
  "fn_send_message",
89633
89614
  "fn_read_messages",
89634
89615
  "fn_post_room_message",
@@ -89670,6 +89651,11 @@ var init_gating_classifications = __esm({
89670
89651
  "fn_agent_show",
89671
89652
  "fn_agent_org_chart",
89672
89653
  "fn_workflow_list",
89654
+ /**
89655
+ * FNXC:ToolGovernance 2026-06-28-00:00:
89656
+ * 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.
89657
+ */
89658
+ "fn_ask_question",
89673
89659
  "fn_send_message",
89674
89660
  "fn_post_room_message",
89675
89661
  "fn_memory_append",
@@ -90557,7 +90543,7 @@ var init_hold_release = __esm({
90557
90543
 
90558
90544
  // ../engine/src/concurrency.ts
90559
90545
  function persistedTopLevelAgentSlots(tasks) {
90560
- 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;
90546
+ return countRunningAgentTasks(tasks);
90561
90547
  }
90562
90548
  function recoverIdleSemaphoreLeakCandidate(params) {
90563
90549
  const {
@@ -90584,10 +90570,11 @@ function recoverIdleSemaphoreLeakCandidate(params) {
90584
90570
  reconciliation: semaphore.reconcileActiveCount(0)
90585
90571
  };
90586
90572
  }
90587
- var concurrencyLog, PRIORITY_MERGE, PRIORITY_EXECUTE, PRIORITY_SPECIFY, IDLE_SEMAPHORE_LEAK_REPAIR_MS, AgentSemaphore;
90573
+ var concurrencyLog, PRIORITY_MERGE, PRIORITY_EXECUTE, PRIORITY_SPECIFY, IDLE_SEMAPHORE_LEAK_REPAIR_MS, AgentSemaphore, ScopedAgentSemaphore;
90588
90574
  var init_concurrency = __esm({
90589
90575
  "../engine/src/concurrency.ts"() {
90590
90576
  "use strict";
90577
+ init_src();
90591
90578
  init_logger2();
90592
90579
  concurrencyLog = createLogger2("concurrency");
90593
90580
  PRIORITY_MERGE = 2;
@@ -90738,13 +90725,21 @@ var init_concurrency = __esm({
90738
90725
  * deadlock that would occur if both parent and child needed a queued slot.
90739
90726
  */
90740
90727
  async runNested(fn2) {
90741
- this._active++;
90728
+ this.acquireNestedSlot();
90742
90729
  try {
90743
90730
  return await fn2();
90744
90731
  } finally {
90745
- this.returnSlot("runNested");
90732
+ this.releaseNestedSlot();
90746
90733
  }
90747
90734
  }
90735
+ /** Reserve a nested helper-agent slot without queueing. */
90736
+ acquireNestedSlot() {
90737
+ this._active++;
90738
+ }
90739
+ /** Return a nested helper-agent slot. */
90740
+ releaseNestedSlot() {
90741
+ this.returnSlot("runNested");
90742
+ }
90748
90743
  /**
90749
90744
  * FNXC:Scheduler-Concurrency 2026-06-13-19:58:
90750
90745
  * 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.
@@ -90793,6 +90788,95 @@ var init_concurrency = __esm({
90793
90788
  return bestIdx;
90794
90789
  }
90795
90790
  };
90791
+ ScopedAgentSemaphore = class extends AgentSemaphore {
90792
+ delegate;
90793
+ _held = 0;
90794
+ constructor(delegate) {
90795
+ super(() => delegate.limit);
90796
+ this.delegate = delegate;
90797
+ }
90798
+ /** Number of slots this scope currently owns in the delegated semaphore. */
90799
+ get heldCount() {
90800
+ return this._held;
90801
+ }
90802
+ /** Shared-pool active count, preserving scheduler/metrics semantics. */
90803
+ get activeCount() {
90804
+ return this.delegate.activeCount;
90805
+ }
90806
+ get waitingCount() {
90807
+ return this.delegate.waitingCount;
90808
+ }
90809
+ get availableCount() {
90810
+ return this.delegate.availableCount;
90811
+ }
90812
+ get limit() {
90813
+ return this.delegate.limit;
90814
+ }
90815
+ snapshot() {
90816
+ return this.delegate.snapshot();
90817
+ }
90818
+ reconcileActiveCount(maxActive) {
90819
+ const bounded = Math.max(0, Math.floor(maxActive));
90820
+ const before = this._held;
90821
+ if (before > bounded) {
90822
+ const returned = before - bounded;
90823
+ this._held = bounded;
90824
+ for (let i = 0; i < returned; i++) {
90825
+ this.delegate.release();
90826
+ }
90827
+ }
90828
+ return { before, after: this._held, changed: before !== this._held };
90829
+ }
90830
+ async acquire(priority = 0) {
90831
+ await this.delegate.acquire(priority);
90832
+ this._held++;
90833
+ }
90834
+ tryAcquire() {
90835
+ const acquired = this.delegate.tryAcquire();
90836
+ if (acquired) this._held++;
90837
+ return acquired;
90838
+ }
90839
+ release() {
90840
+ if (this._held <= 0) return;
90841
+ this._held--;
90842
+ this.delegate.release();
90843
+ }
90844
+ async run(fn2, priority = 0) {
90845
+ await this.acquire(priority);
90846
+ try {
90847
+ return await fn2();
90848
+ } finally {
90849
+ this.release();
90850
+ }
90851
+ }
90852
+ async runNested(fn2) {
90853
+ this.delegate.acquireNestedSlot();
90854
+ this._held++;
90855
+ try {
90856
+ return await fn2();
90857
+ } finally {
90858
+ if (this._held > 0) {
90859
+ this._held--;
90860
+ this.delegate.releaseNestedSlot();
90861
+ }
90862
+ }
90863
+ }
90864
+ /**
90865
+ * Return every slot still attributed to this scope.
90866
+ *
90867
+ * Late normal releases from already-aborted agents become no-ops because the
90868
+ * scope's held count is zeroed before returning residual slots to the pool.
90869
+ */
90870
+ returnAllHeldSlots() {
90871
+ const residual = this._held;
90872
+ if (residual <= 0) return 0;
90873
+ this._held = 0;
90874
+ for (let i = 0; i < residual; i++) {
90875
+ this.delegate.release();
90876
+ }
90877
+ return residual;
90878
+ }
90879
+ };
90796
90880
  }
90797
90881
  });
90798
90882
 
@@ -161599,7 +161683,7 @@ ${isRevision ? "1. Review the existing specification and user feedback carefully
161599
161683
 
161600
161684
  Use the write tool to write the specification file.${commandsSection}${completionDocumentationSection}${memorySection}${attachmentsSection}${userCommentsSection}`;
161601
161685
  }
161602
- var TriageProcessor, IMAGE_MIME_TYPES, TEXT_INLINE_LIMIT;
161686
+ var TRIAGE_STUCK_RESUME_LOG_ACTION, TRIAGE_STUCK_RESUME_FEEDBACK, TriageProcessor, IMAGE_MIME_TYPES, TEXT_INLINE_LIMIT;
161603
161687
  var init_triage = __esm({
161604
161688
  "../engine/src/triage.ts"() {
161605
161689
  "use strict";
@@ -161631,6 +161715,8 @@ var init_triage = __esm({
161631
161715
  init_run_audit();
161632
161716
  init_goal_injection_diagnostics();
161633
161717
  init_session_token_usage();
161718
+ TRIAGE_STUCK_RESUME_LOG_ACTION = "Triage stuck re-queue will resume existing planning draft";
161719
+ 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.";
161634
161720
  TriageProcessor = class _TriageProcessor {
161635
161721
  /**
161636
161722
  * @param store — Task store instance (also used to listen for `settings:updated` events)
@@ -161941,6 +162027,106 @@ var init_triage = __esm({
161941
162027
  });
161942
162028
  return true;
161943
162029
  }
162030
+ async readNonEmptyPromptDraft(taskId, context) {
162031
+ const promptPath = join49(this.rootDir, ".fusion", "tasks", taskId, "PROMPT.md");
162032
+ const written = await readFile22(promptPath, "utf-8").catch((err) => {
162033
+ const msg = err instanceof Error ? err.message : String(err);
162034
+ planLog.warn(`${taskId}: failed to read PROMPT.md during ${context} (${promptPath}): ${msg}`);
162035
+ return "";
162036
+ });
162037
+ return written.trim().length > 0 ? written : void 0;
162038
+ }
162039
+ async readNonEmptyPlanDocument(taskId, context) {
162040
+ const readTaskDocument = this.store.getTaskDocument;
162041
+ if (typeof readTaskDocument !== "function") {
162042
+ return void 0;
162043
+ }
162044
+ const document2 = await readTaskDocument.call(this.store, taskId, "plan").catch((err) => {
162045
+ const msg = err instanceof Error ? err.message : String(err);
162046
+ planLog.warn(`${taskId}: failed to read plan task document during ${context}: ${msg}`);
162047
+ return null;
162048
+ });
162049
+ const content = typeof document2?.content === "string" ? document2.content : "";
162050
+ return content.trim().length > 0 ? content : void 0;
162051
+ }
162052
+ async readNonEmptyPlanningDraft(taskId, context) {
162053
+ const promptDraft = await this.readNonEmptyPromptDraft(taskId, context);
162054
+ if (promptDraft) {
162055
+ return { content: promptDraft, source: "prompt" };
162056
+ }
162057
+ const planDocument = await this.readNonEmptyPlanDocument(taskId, context);
162058
+ return planDocument ? { content: planDocument, source: "plan-document" } : void 0;
162059
+ }
162060
+ async handleStuckAbortRequeue(task, context) {
162061
+ const freshTask = await this.store.getTask(task.id).catch((err) => {
162062
+ const msg = err instanceof Error ? err.message : String(err);
162063
+ planLog.warn(`${task.id}: failed to refresh task during stuck-detector ${context} cleanup: ${msg}`);
162064
+ return task;
162065
+ });
162066
+ if (hasLatestSpecReviewApproval2(freshTask)) {
162067
+ const recovered = await this.recoverApprovedTask(freshTask).catch((err) => {
162068
+ const msg = err instanceof Error ? err.message : String(err);
162069
+ planLog.warn(`${task.id}: approved-spec recovery failed during stuck-detector ${context} cleanup: ${msg}`);
162070
+ return false;
162071
+ });
162072
+ if (recovered) {
162073
+ return;
162074
+ }
162075
+ }
162076
+ const maxStuckSettings = await this.store.getSettings().catch((err) => {
162077
+ const msg = err instanceof Error ? err.message : String(err);
162078
+ planLog.warn(`${task.id}: failed to read maxStuckKills during stuck-detector ${context} cleanup, using default 6: ${msg}`);
162079
+ return {};
162080
+ });
162081
+ const maxKills = Math.max(1, maxStuckSettings.maxStuckKills ?? 6);
162082
+ const nextStuckKillCount = (freshTask.stuckKillCount ?? task.stuckKillCount ?? 0) + 1;
162083
+ const draft = await this.readNonEmptyPlanningDraft(task.id, `stuck-detector ${context} cleanup`);
162084
+ if (nextStuckKillCount >= maxKills) {
162085
+ const exhaustedError = `STUCK_LOOP_EXHAUSTED: triage stuck detector killed ${task.id} ${nextStuckKillCount}/${maxKills} times without planning completion; task paused for manual intervention.`;
162086
+ planLog.error(exhaustedError);
162087
+ await this.store.logEntry(task.id, exhaustedError).catch((err) => {
162088
+ const msg = err instanceof Error ? err.message : String(err);
162089
+ planLog.warn(`${task.id}: failed to log stuck-loop exhaustion: ${msg}`);
162090
+ });
162091
+ await this.store.updateTask(task.id, {
162092
+ stuckKillCount: nextStuckKillCount,
162093
+ status: "failed",
162094
+ error: exhaustedError,
162095
+ paused: true,
162096
+ pausedReason: "stuck-loop-exhausted-manual-intervention-required",
162097
+ pausedByAgentId: "triage"
162098
+ }).catch((err) => {
162099
+ const msg = err instanceof Error ? err.message : String(err);
162100
+ planLog.warn(`${task.id}: failed to persist stuck-loop exhaustion during stuck-detector ${context} cleanup: ${msg}`);
162101
+ });
162102
+ return;
162103
+ }
162104
+ if (draft) {
162105
+ const sourceLabel = draft.source === "prompt" ? "PROMPT.md draft" : "plan task document";
162106
+ planLog.log(`${task.id} killed by stuck detector \u2014 requeueing to resume existing ${sourceLabel} (${nextStuckKillCount}/${maxKills})`);
162107
+ await this.store.logEntry(task.id, TRIAGE_STUCK_RESUME_LOG_ACTION, TRIAGE_STUCK_RESUME_FEEDBACK).catch((err) => {
162108
+ const msg = err instanceof Error ? err.message : String(err);
162109
+ planLog.warn(`${task.id}: failed to log stuck-resume feedback: ${msg}`);
162110
+ });
162111
+ await this.store.updateTask(task.id, {
162112
+ status: "needs-replan",
162113
+ stuckKillCount: nextStuckKillCount
162114
+ }).catch((err) => {
162115
+ const msg = err instanceof Error ? err.message : String(err);
162116
+ planLog.warn(`${task.id}: failed to restore status to 'needs-replan' during stuck-detector ${context} cleanup: ${msg}`);
162117
+ });
162118
+ return;
162119
+ }
162120
+ planLog.log(`${task.id} killed by stuck detector \u2014 clearing status for cold retry (${nextStuckKillCount}/${maxKills})`);
162121
+ const restoreStatus = (freshTask.status ?? task.status) === "needs-replan" ? "needs-replan" : null;
162122
+ await this.store.updateTask(task.id, {
162123
+ status: restoreStatus,
162124
+ stuckKillCount: nextStuckKillCount
162125
+ }).catch((err) => {
162126
+ const msg = err instanceof Error ? err.message : String(err);
162127
+ planLog.warn(`${task.id}: failed to restore status to '${restoreStatus}' during stuck-detector ${context} cleanup: ${msg}`);
162128
+ });
162129
+ }
161944
162130
  /**
161945
162131
  * If `newIntervalMs` differs from the currently active timer, restart
161946
162132
  * the `setInterval` so the new cadence takes effect immediately.
@@ -162299,9 +162485,16 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
162299
162485
  let feedback;
162300
162486
  if (isReplan) {
162301
162487
  const feedbackLogEntry = [...task.log].reverse().find(
162302
- (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"
162488
+ (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
162303
162489
  );
162304
162490
  feedback = feedbackLogEntry?.outcome;
162491
+ if (feedbackLogEntry?.action === TRIAGE_STUCK_RESUME_LOG_ACTION) {
162492
+ const planningDraft = await this.readNonEmptyPlanningDraft(task.id, "stuck-resume replan seed");
162493
+ existingPrompt = planningDraft?.content;
162494
+ if (!existingPrompt) {
162495
+ feedback = void 0;
162496
+ }
162497
+ }
162305
162498
  if (!feedback) {
162306
162499
  const latestUserComment = [...detail.comments || []].reverse().find((comment) => comment.author === "user");
162307
162500
  feedback = latestUserComment?.text;
@@ -162336,12 +162529,7 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
162336
162529
  }
162337
162530
  if (this.stuckAborted.has(task.id)) {
162338
162531
  this.stuckAborted.delete(task.id);
162339
- planLog.log(`${task.id} killed by stuck detector \u2014 clearing status for retry`);
162340
- const restoreStatus = task.status === "needs-replan" ? "needs-replan" : null;
162341
- await this.store.updateTask(task.id, { status: restoreStatus }).catch((err) => {
162342
- const msg = err instanceof Error ? err.message : String(err);
162343
- planLog.warn(`${task.id}: failed to restore status to '${restoreStatus}' during stuck-detector abort cleanup: ${msg}`);
162344
- });
162532
+ await this.handleStuckAbortRequeue(task, "in-loop");
162345
162533
  return;
162346
162534
  }
162347
162535
  if (createdSubtasksRef.current.length > 0) {
@@ -162590,12 +162778,7 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
162590
162778
  });
162591
162779
  } else if (this.stuckAborted.has(task.id)) {
162592
162780
  this.stuckAborted.delete(task.id);
162593
- planLog.log(`${task.id} killed by stuck detector \u2014 clearing status for retry`);
162594
- const restoreStatus = task.status === "needs-replan" ? "needs-replan" : null;
162595
- await this.store.updateTask(task.id, { status: restoreStatus }).catch((err2) => {
162596
- const msg = err2 instanceof Error ? err2.message : String(err2);
162597
- planLog.warn(`${task.id}: failed to restore status to '${restoreStatus}' during stuck-detector error cleanup: ${msg}`);
162598
- });
162781
+ await this.handleStuckAbortRequeue(task, "catch");
162599
162782
  } else {
162600
162783
  if (this.options.usageLimitPauser && isUsageLimitError(errorMessage2)) {
162601
162784
  await this.options.usageLimitPauser.onUsageLimitHit(
@@ -175847,9 +176030,7 @@ ${summary}`,
175847
176030
  } else {
175848
176031
  const settings2 = await this.store.getSettings();
175849
176032
  const preserveProgress = settings2.preserveProgressOnStuckRequeue !== false;
175850
- if (!preserveProgress) {
175851
- await this.resetStepsIfWorkLost(latestTask);
175852
- }
176033
+ await this.resetStepsIfWorkLost(latestTask);
175853
176034
  if (worktreePath && existsSync47(worktreePath)) {
175854
176035
  try {
175855
176036
  await removeWorktree({
@@ -177197,9 +177378,7 @@ ${summary}`,
177197
177378
  } else {
177198
177379
  const settings2 = await this.store.getSettings();
177199
177380
  const preserveProgress = settings2.preserveProgressOnStuckRequeue !== false;
177200
- if (!preserveProgress) {
177201
- await this.resetStepsIfWorkLost(latestTask);
177202
- }
177381
+ await this.resetStepsIfWorkLost(latestTask);
177203
177382
  if (worktreePath && existsSync47(worktreePath)) {
177204
177383
  try {
177205
177384
  await removeWorktree({
@@ -178544,28 +178723,48 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
178544
178723
  await this.store.logEntry(taskId, "Execution stopped \u2014 work discarded, moved to triage for re-planning");
178545
178724
  }
178546
178725
  /**
178547
- * Re-open the last non-pending step so a revision/failure handler gives the
178548
- * executor exactly one pending slot to re-enter through. Returns the index
178549
- * and name of the step that was flipped to `pending`, or null when there
178550
- * was nothing to re-open.
178726
+ * Re-open the implementation-bearing slice of work for a revision/failure
178727
+ * handler. Returns the earliest reopened step and all reopened indexes, or
178728
+ * null when there was nothing to re-open.
178551
178729
  */
178552
178730
  async reopenLastStepForRevision(taskId, task) {
178553
178731
  const steps = task.steps;
178554
178732
  if (steps.length === 0) return null;
178555
- let targetIndex = -1;
178733
+ let lastNonPendingIndex = -1;
178556
178734
  for (let i = steps.length - 1; i >= 0; i--) {
178557
178735
  if (steps[i].status !== "pending") {
178558
- targetIndex = i;
178736
+ lastNonPendingIndex = i;
178559
178737
  break;
178560
178738
  }
178561
178739
  }
178562
- if (targetIndex === -1) {
178740
+ if (lastNonPendingIndex === -1) {
178563
178741
  await this.store.updateTask(taskId, { currentStep: 0 });
178564
178742
  return null;
178565
178743
  }
178566
- await this.store.updateStep(taskId, targetIndex, "pending");
178567
- await this.store.updateTask(taskId, { currentStep: targetIndex });
178568
- return { index: targetIndex, name: steps[targetIndex].name };
178744
+ const isTerminalVerificationOrDeliveryStep = (name) => /(^|[^a-z])(testing|verification|documentation|delivery)([^a-z]|$)/i.test(name);
178745
+ const resetIndexes = /* @__PURE__ */ new Set([lastNonPendingIndex]);
178746
+ if (isTerminalVerificationOrDeliveryStep(steps[lastNonPendingIndex].name)) {
178747
+ let cursor = lastNonPendingIndex;
178748
+ while (cursor >= 0 && isTerminalVerificationOrDeliveryStep(steps[cursor].name)) {
178749
+ resetIndexes.add(cursor);
178750
+ cursor--;
178751
+ }
178752
+ while (cursor >= 0 && steps[cursor].status === "pending") {
178753
+ cursor--;
178754
+ }
178755
+ if (cursor >= 0) {
178756
+ resetIndexes.add(cursor);
178757
+ }
178758
+ }
178759
+ const indexes = [...resetIndexes].sort((a, b2) => a - b2);
178760
+ for (const index of indexes) {
178761
+ if (steps[index].status !== "pending") {
178762
+ await this.store.updateStep(taskId, index, "pending");
178763
+ }
178764
+ }
178765
+ const currentStep = indexes[0] ?? lastNonPendingIndex;
178766
+ await this.store.updateTask(taskId, { currentStep });
178767
+ return { index: currentStep, name: steps[currentStep].name, indexes };
178569
178768
  }
178570
178769
  /**
178571
178770
  * Run deterministic verification (test + build commands) in the task's worktree.
@@ -181129,39 +181328,45 @@ Review the work done in this worktree and evaluate it against the criteria in yo
181129
181328
  const mergeBase = mergeBaseStdout.trim();
181130
181329
  const branchHead = branchHeadStdout.trim();
181131
181330
  if (mergeBase === branchHead) {
181132
- executorLog.warn(
181133
- `${task.id} branch has no unique commits \u2014 resetting ${completedSteps.length} step(s) to pending`
181331
+ await this.resetLostWorkStepProgress(task, completedSteps.length, "branch had no commits");
181332
+ }
181333
+ } catch (err) {
181334
+ const msg = err instanceof Error ? err.message : String(err);
181335
+ executorLog.warn(
181336
+ `${task.id}: unable to prove surviving branch commits before worktree removal \u2014 resetting ${completedSteps.length} step(s) to pending: ${msg}`
181337
+ );
181338
+ await this.resetLostWorkStepProgress(task, completedSteps.length, `git proof failed: ${msg}`);
181339
+ }
181340
+ }
181341
+ async resetLostWorkStepProgress(task, completedStepCount, reason) {
181342
+ executorLog.warn(
181343
+ `${task.id} ${reason} \u2014 resetting ${completedStepCount} step(s) to pending`
181344
+ );
181345
+ for (let i = 0; i < task.steps.length; i++) {
181346
+ if (task.steps[i].status === "done" || task.steps[i].status === "in-progress") {
181347
+ await this.store.updateStep(task.id, i, "pending");
181348
+ }
181349
+ }
181350
+ const refreshedTask = await this.store.getTask(task.id);
181351
+ const prevCurrentStep = refreshedTask.currentStep;
181352
+ if (refreshedTask.steps.length > 0) {
181353
+ const firstPendingStep = refreshedTask.steps.findIndex((s3) => s3.status === "pending");
181354
+ const newCurrentStep = firstPendingStep >= 0 ? firstPendingStep : 0;
181355
+ if (newCurrentStep !== prevCurrentStep) {
181356
+ await this.store.updateTask(task.id, { currentStep: newCurrentStep });
181357
+ executorLog.log(
181358
+ `${task.id}: reset currentStep to ${newCurrentStep} after lost-work reset (was ${prevCurrentStep})`
181134
181359
  );
181135
- for (let i = 0; i < task.steps.length; i++) {
181136
- if (task.steps[i].status === "done" || task.steps[i].status === "in-progress") {
181137
- await this.store.updateStep(task.id, i, "pending");
181138
- }
181139
- }
181140
- const refreshedTask = await this.store.getTask(task.id);
181141
- const prevCurrentStep = refreshedTask.currentStep;
181142
- if (refreshedTask.steps.length > 0) {
181143
- const firstPendingStep = refreshedTask.steps.findIndex((s3) => s3.status === "pending");
181144
- const newCurrentStep = firstPendingStep >= 0 ? firstPendingStep : 0;
181145
- if (newCurrentStep !== prevCurrentStep) {
181146
- await this.store.updateTask(task.id, { currentStep: newCurrentStep });
181147
- executorLog.log(
181148
- `${task.id}: reset currentStep to ${newCurrentStep} after lost-work reset (was ${prevCurrentStep})`
181149
- );
181150
- await this.store.logEntry(
181151
- task.id,
181152
- `Reset currentStep to ${newCurrentStep} after lost-work step reset (was ${prevCurrentStep})`
181153
- );
181154
- }
181155
- }
181156
181360
  await this.store.logEntry(
181157
181361
  task.id,
181158
- `Reset ${completedSteps.length} step(s) to pending \u2014 branch had no commits (uncommitted work lost with worktree)`
181362
+ `Reset currentStep to ${newCurrentStep} after lost-work step reset (was ${prevCurrentStep})`
181159
181363
  );
181160
181364
  }
181161
- } catch (err) {
181162
- const msg = err instanceof Error ? err.message : String(err);
181163
- executorLog.warn(`${task.id}: step-reset-on-work-lost failed (non-fatal, steps keep current status): ${msg}`);
181164
181365
  }
181366
+ await this.store.logEntry(
181367
+ task.id,
181368
+ `Reset ${completedStepCount} step(s) to pending \u2014 ${reason} (uncommitted work lost with worktree)`
181369
+ );
181165
181370
  }
181166
181371
  /**
181167
181372
  * Mark a task as stuck-aborted so the executor's error handling
@@ -181224,9 +181429,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
181224
181429
  });
181225
181430
  await this.awaitAbortInFlightTaskWork(taskId, "force-requeue after stuck-kill unwind timeout");
181226
181431
  this.clearPausedAborted(taskId);
181227
- if (!preserveProgress) {
181228
- await this.resetStepsIfWorkLost(latestTask);
181229
- }
181432
+ await this.resetStepsIfWorkLost(latestTask);
181230
181433
  let cleanupFailed = false;
181231
181434
  if (worktreePath && existsSync47(worktreePath)) {
181232
181435
  try {
@@ -186515,6 +186718,7 @@ var init_agent_reflection = __esm({
186515
186718
  init_logger2();
186516
186719
  init_pi();
186517
186720
  init_mcp_resolution();
186721
+ init_run_audit();
186518
186722
  reflectionLog = createLogger2("reflection");
186519
186723
  REFLECTION_SYSTEM_PROMPT = `You are an autonomous performance analyst reviewing an AI agent's recent execution history.
186520
186724
 
@@ -186553,11 +186757,22 @@ Rules:
186553
186757
  this.modelId = options.modelId;
186554
186758
  }
186555
186759
  async generateReflection(agentId, trigger, options = {}) {
186760
+ const runContext = {
186761
+ runId: generateSyntheticRunId("reflection", agentId),
186762
+ agentId,
186763
+ ...options.taskId ? { taskId: options.taskId } : {},
186764
+ phase: "reflection",
186765
+ source: trigger
186766
+ };
186767
+ const auditor = createRunAuditor(this.taskStore, runContext);
186556
186768
  try {
186557
186769
  const context = await this.buildReflectionContext(agentId);
186558
186770
  const recentRuns = await this.agentStore.getRecentRuns(agentId, DEFAULT_OUTCOME_LIMIT);
186559
186771
  if (context.recentOutcomes.length === 0 && recentRuns.length === 0) {
186560
186772
  reflectionLog.log(`Skipping reflection for ${agentId}: no recent tasks or heartbeat runs`);
186773
+ await this.emitReflectionAudit(auditor, "reflection:skipped", agentId, trigger, options, {
186774
+ reason: "no-history"
186775
+ });
186561
186776
  return null;
186562
186777
  }
186563
186778
  let responseText = "";
@@ -186587,7 +186802,7 @@ Rules:
186587
186802
  }
186588
186803
  const parsed = this.parseReflectionResponse(responseText);
186589
186804
  const metrics = this.buildReflectionMetrics(context.recentOutcomes, context.performanceSummary, recentRuns);
186590
- return await this.reflectionStore.createReflection({
186805
+ const reflection = await this.reflectionStore.createReflection({
186591
186806
  agentId,
186592
186807
  trigger,
186593
186808
  triggerDetail: options.triggerDetail,
@@ -186597,11 +186812,42 @@ Rules:
186597
186812
  suggestedImprovements: parsed.suggestedImprovements,
186598
186813
  summary: parsed.summary
186599
186814
  });
186815
+ await this.emitReflectionAudit(auditor, "reflection:generated", agentId, trigger, options, {
186816
+ reflectionId: reflection.id,
186817
+ ...metrics.tasksCompleted !== void 0 ? { tasksCompleted: metrics.tasksCompleted } : {},
186818
+ ...metrics.tasksFailed !== void 0 ? { tasksFailed: metrics.tasksFailed } : {},
186819
+ ...metrics.avgDurationMs !== void 0 ? { avgDurationMs: metrics.avgDurationMs } : {},
186820
+ commonErrorCount: metrics.commonErrors?.length ?? 0,
186821
+ insightCount: parsed.insights.length,
186822
+ suggestedImprovementCount: parsed.suggestedImprovements.length
186823
+ });
186824
+ return reflection;
186600
186825
  } catch (error2) {
186826
+ await this.emitReflectionAudit(auditor, "reflection:failed", agentId, trigger, options, {
186827
+ errorClass: error2 instanceof Error ? error2.name : typeof error2
186828
+ });
186601
186829
  reflectionLog.error(`Failed to generate reflection for ${agentId}: ${error2.message}`);
186602
186830
  return null;
186603
186831
  }
186604
186832
  }
186833
+ async emitReflectionAudit(auditor, type, agentId, trigger, options, metadata) {
186834
+ try {
186835
+ await auditor.database({
186836
+ type,
186837
+ target: agentId,
186838
+ metadata: {
186839
+ agentId,
186840
+ trigger,
186841
+ ...options.taskId ? { taskId: options.taskId } : {},
186842
+ ...metadata
186843
+ }
186844
+ });
186845
+ } catch (auditError) {
186846
+ reflectionLog.warn(
186847
+ `Failed to record reflection telemetry for ${agentId}: ${auditError instanceof Error ? auditError.message : String(auditError)}`
186848
+ );
186849
+ }
186850
+ }
186605
186851
  async buildReflectionContext(agentId) {
186606
186852
  const [agentRecord, recentOutcomes, performanceSummaryRaw, latestReflection] = await Promise.all([
186607
186853
  this.agentStore.getAgent(agentId),
@@ -187703,6 +187949,8 @@ Your job:
187703
187949
  - Surface the blocker concretely with fn_task_log.
187704
187950
  - Chase the dependency: comment on the blocking task, send a message to the responsible agent, or ping an owner.
187705
187951
  - Look for unblocking work you can spawn or delegate right now.
187952
+ - Do NOT call fn_task_pause to handle a failed or blocked task. Pausing is reserved for explicit user requests for manual control.
187953
+ - 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.
187706
187954
  - Pivot to other relevant coordination work if the blocker cannot be immediately resolved.
187707
187955
 
187708
187956
  **If your bound task is not blocked:**
@@ -187739,8 +187987,8 @@ Prefer fn_delegate_task when immediate ownership by a specific agent materially
187739
187987
 
187740
187988
  ## Common Patterns
187741
187989
 
187742
- - **Blocked task:** log the concrete blocker, chase the dependency via fn_send_message, create a narrowly scoped unblocker task if needed.
187743
- - **Stuck task with no blockedBy:** log the observation and create a follow-up task to investigate the root cause.
187990
+ - **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.
187991
+ - **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.
187744
187992
  - **Completed task with follow-up risk:** create explicit follow-up task(s) for residual risk instead of burying notes in a long log.
187745
187993
  - **New user/agent comments:** summarize what changed, identify required action, and route via task creation/delegation.
187746
187994
  - **Dependency drift:** log the mismatch and create reconciliation tasks with clear dependencies.
@@ -187832,6 +188080,7 @@ If unsure who should do the work, prefer fn_task_create and let scheduler routin
187832
188080
 
187833
188081
  ## Common Patterns
187834
188082
 
188083
+ - **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.
187835
188084
  - **Unowned risk discovered:** create one focused task with concrete acceptance language.
187836
188085
  - **Known specialist needed:** list agents, then delegate to matching role/capability.
187837
188086
  - **Repeated confusion across runs:** append a concise memory entry so future agents avoid the same mistake.
@@ -193360,12 +193609,21 @@ var init_cron_runner = __esm({
193360
193609
  log24.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 belongs to ${scheduleScope} scope, not polling`);
193361
193610
  continue;
193362
193611
  }
193363
- log24.log(`Executing ${schedule.name} (${schedule.id}) [scope: ${scheduleScope}]`);
193364
193612
  const currentSettings = await this.store.getSettings();
193365
193613
  if (currentSettings.globalPause || currentSettings.enginePaused) {
193366
193614
  log24.log("Pause detected mid-tick \u2014 stopping schedule execution");
193367
193615
  break;
193368
193616
  }
193617
+ if (!schedule.nextRunAt) {
193618
+ log24.warn(`Skipping ${schedule.name} (${schedule.id}) \u2014 missing nextRunAt for due claim`);
193619
+ continue;
193620
+ }
193621
+ const claimed = await this.automationStore.claimDueSchedule(schedule.id, schedule.nextRunAt);
193622
+ if (!claimed) {
193623
+ log24.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 claim lost to another poller`);
193624
+ continue;
193625
+ }
193626
+ log24.log(`Executing ${schedule.name} (${schedule.id}) [scope: ${scheduleScope}]`);
193369
193627
  await this.executeSchedule(schedule);
193370
193628
  }
193371
193629
  } catch (err) {
@@ -198355,6 +198613,7 @@ var init_in_process_runtime = __esm({
198355
198613
  executor;
198356
198614
  worktreePool;
198357
198615
  globalSemaphore;
198616
+ projectSemaphore;
198358
198617
  stuckTaskDetector;
198359
198618
  /**
198360
198619
  * Per-project CLI Agent Executor runtime bundle (PTY manager + telemetry hub +
@@ -198502,6 +198761,7 @@ var init_in_process_runtime = __esm({
198502
198761
  this.centralCore.on("concurrency:changed", this.concurrencyChangedListener);
198503
198762
  }
198504
198763
  }
198764
+ this.projectSemaphore = new ScopedAgentSemaphore(this.globalSemaphore);
198505
198765
  await yieldEventLoop4();
198506
198766
  let agentStoreForReflection;
198507
198767
  try {
@@ -198561,7 +198821,7 @@ var init_in_process_runtime = __esm({
198561
198821
  this.scheduler = new Scheduler(this.taskStore, {
198562
198822
  maxConcurrent: this.config.maxConcurrent,
198563
198823
  maxWorktrees: this.config.maxWorktrees,
198564
- semaphore: this.globalSemaphore,
198824
+ semaphore: this.projectSemaphore,
198565
198825
  agentStore: this.agentStore,
198566
198826
  hasActiveAgentExecution: (agentId) => this.heartbeatMonitor?.getTrackedAgents().includes(agentId) ?? false,
198567
198827
  missionStore,
@@ -198658,7 +198918,7 @@ var init_in_process_runtime = __esm({
198658
198918
  const prNodeGithubOps = this.config.prNodeGithubOps;
198659
198919
  const workflowAuthoritativeDriverRef = {};
198660
198920
  const executorOptions = {
198661
- semaphore: this.globalSemaphore,
198921
+ semaphore: this.projectSemaphore,
198662
198922
  pool: this.worktreePool,
198663
198923
  usageLimitPauser: this.usageLimitPauser,
198664
198924
  stuckTaskDetector: this.stuckTaskDetector,
@@ -198856,7 +199116,7 @@ var init_in_process_runtime = __esm({
198856
199116
  this.taskStore,
198857
199117
  this.config.workingDirectory,
198858
199118
  {
198859
- semaphore: this.globalSemaphore,
199119
+ semaphore: this.projectSemaphore,
198860
199120
  stuckTaskDetector: this.stuckTaskDetector,
198861
199121
  agentStore: this.agentStore,
198862
199122
  pluginRunner: this.pluginRunner,
@@ -199155,6 +199415,12 @@ var init_in_process_runtime = __esm({
199155
199415
  `post-abort drain timeout: shutdown reached with ${finalMetrics.inFlightTasks} tasks still in-flight`
199156
199416
  );
199157
199417
  }
199418
+ const returnedResidualSlots = this.projectSemaphore?.returnAllHeldSlots() ?? 0;
199419
+ if (returnedResidualSlots > 0) {
199420
+ runtimeLog.warn(
199421
+ `Returned ${returnedResidualSlots} residual global concurrency slot(s) after project stop drain`
199422
+ );
199423
+ }
199158
199424
  if (this.pluginRunner) {
199159
199425
  await this.pluginRunner.shutdown();
199160
199426
  runtimeLog.log("PluginRunner shutdown complete");
@@ -203690,7 +203956,7 @@ ${detail}`
203690
203956
  }
203691
203957
  } else {
203692
203958
  runtimeLog.log(`${hasManualResolver ? "Manual" : "Auto"}-merge merging ${taskId}...`);
203693
- const semaphore = this.runtime.globalSemaphore;
203959
+ const semaphore = this.runtime.projectSemaphore ?? this.runtime.globalSemaphore;
203694
203960
  const pool = this.runtime.worktreePool;
203695
203961
  const agentStore = this.runtime.agentStore;
203696
203962
  const usageLimitPauser = this.runtime.usageLimitPauser;
@@ -210701,6 +210967,7 @@ var init_sse2 = __esm({
210701
210967
  var init_project_store_resolver = __esm({
210702
210968
  "../dashboard/src/project-store-resolver.ts"() {
210703
210969
  "use strict";
210970
+ init_src();
210704
210971
  }
210705
210972
  });
210706
210973
 
@@ -215816,6 +216083,15 @@ async function generatePrMetadata(input) {
215816
216083
  const templateExists = await raceWithAbort(access9(templatePath).then(() => true).catch(() => false), combinedSignal);
215817
216084
  const template = templateExists ? await raceWithAbort(readFile26(templatePath, "utf8"), combinedSignal) : "";
215818
216085
  const model = resolveTitleSummarizerSettingsModel(settings);
216086
+ const systemPrompt = [PR_METADATA_SYSTEM_PROMPT];
216087
+ const titleGuidance = settings.prTitlePromptInstructions?.trim();
216088
+ const descriptionGuidance = settings.prDescriptionPromptInstructions?.trim();
216089
+ if (titleGuidance) {
216090
+ systemPrompt.push(`Title guidance: ${titleGuidance}`);
216091
+ }
216092
+ if (descriptionGuidance) {
216093
+ systemPrompt.push(`Description guidance: ${descriptionGuidance}`);
216094
+ }
215819
216095
  const mcpServers = (await raceWithAbort(resolveMcpServersForStore(store ?? {}), combinedSignal)).servers;
215820
216096
  let aiText = "";
215821
216097
  const { session } = await raceWithAbort(createFnAgent2({
@@ -215828,26 +216104,27 @@ async function generatePrMetadata(input) {
215828
216104
  mcpServers,
215829
216105
  defaultProvider: model.provider,
215830
216106
  defaultModelId: model.modelId,
215831
- systemPrompt: [
215832
- "Generate GitHub PR metadata.",
215833
- "Respond with strict JSON only.",
215834
- "Schema: {title, summary, changes, testing, linkedTask}"
215835
- ].join("\n"),
216107
+ systemPrompt: systemPrompt.join("\n"),
215836
216108
  onText: (delta) => {
215837
216109
  aiText += delta;
215838
216110
  }
215839
216111
  }), combinedSignal);
215840
216112
  try {
215841
216113
  const contextPrompt = [
216114
+ "Source-of-truth guidance:",
216115
+ "- Ground the PR title, summary, changes, and testing in the commit log and diff stat below.",
216116
+ "- Use task title, task description, and task prompt only to clarify intent when the git evidence supports it.",
216117
+ "- Omit speculation; when evidence for testing or a change is absent, say so instead of inventing details.",
216118
+ "- Return only strict JSON matching the required schema.",
215842
216119
  `Task ID: ${task.id}`,
215843
216120
  `Task title: ${task.title}`,
215844
216121
  `Task description: ${task.description ?? ""}`,
215845
216122
  `Base branch: ${baseBranch}`,
215846
- "Commit log:",
216123
+ "Commit log (source of truth):",
215847
216124
  logOut || "(none)",
215848
- "Diff stat:",
216125
+ "Diff stat (source of truth):",
215849
216126
  diffStatOut || "(none)",
215850
- "Task prompt:",
216127
+ "Task prompt (supporting context):",
215851
216128
  promptContent || "(none)"
215852
216129
  ].join("\n\n");
215853
216130
  throwIfAborted3(combinedSignal);
@@ -215880,7 +216157,7 @@ async function generatePrMetadata(input) {
215880
216157
  }
215881
216158
  }
215882
216159
  }
215883
- var execAsync36, PR_METADATA_TIMEOUT_MS;
216160
+ var execAsync36, PR_METADATA_TIMEOUT_MS, PR_METADATA_SYSTEM_PROMPT;
215884
216161
  var init_pr_metadata_generator = __esm({
215885
216162
  "../dashboard/src/pr-metadata-generator.ts"() {
215886
216163
  "use strict";
@@ -215888,6 +216165,17 @@ var init_pr_metadata_generator = __esm({
215888
216165
  init_src4();
215889
216166
  execAsync36 = promisify55(execCb);
215890
216167
  PR_METADATA_TIMEOUT_MS = 6e4;
216168
+ PR_METADATA_SYSTEM_PROMPT = [
216169
+ "You generate GitHub pull request metadata for Fusion tasks.",
216170
+ "Respond with STRICT JSON ONLY: no markdown fences, no commentary, no prose outside the JSON object.",
216171
+ 'Use exactly this schema: {"title": string, "summary": string, "changes": string, "testing": string, "linkedTask": string}.',
216172
+ "Title: concise imperative conventional-commit style, at most 72 characters, no trailing period, with a scope when the change clearly suggests one.",
216173
+ "Summary: 1-3 short plain-sentence paragraphs explaining what changed and why; be specific and avoid marketing fluff.",
216174
+ "Changes: a tight markdown bullet list of concrete, verifiable changes supported by the commit log or diff stat; do not speculate.",
216175
+ "Testing: markdown bullets naming actual verification shown in the evidence; if no testing evidence exists, say '- Not provided.' or '- Not run (not provided).'.",
216176
+ "LinkedTask: set to the task ID exactly as provided.",
216177
+ "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."
216178
+ ].join("\n");
215891
216179
  }
215892
216180
  });
215893
216181
 
@@ -227842,8 +228130,8 @@ Path: .fusion/tasks/${params.id}/attachments/${attachment.filename}`
227842
228130
  pi2.registerTool({
227843
228131
  name: "fn_task_pause",
227844
228132
  label: "fn: Pause Task",
227845
- description: "Pause a task \u2014 stops all automated agent and scheduler interaction for this task.",
227846
- promptSnippet: "Pause a Fusion task (stops automation)",
228133
+ description: "Pause a task for explicit user-requested manual control \u2014 stops all automated agent and scheduler interaction. Agents should not pause tasks to handle failures or blockers; use retry, create/delegate follow-up work, or let the task surface as failed instead.",
228134
+ promptSnippet: "Pause a Fusion task only for explicit user-requested manual control; do not use for failure/blocker handling",
227847
228135
  parameters: Type9.Object({
227848
228136
  id: Type9.String({ description: "Task ID (e.g. FN-001)" })
227849
228137
  }),