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