@runfusion/fusion 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/bin.js +2707 -1417
  2. package/dist/client/assets/ChatView-BomXmqar.js +1 -0
  3. package/dist/client/assets/{DevServerView-DIrmWI5T.js → DevServerView-yFvF4xL4.js} +1 -1
  4. package/dist/client/assets/DirectoryPicker-BDNodhtF.js +1 -0
  5. package/dist/client/assets/DocumentsView-CAWtDEaL.js +1 -0
  6. package/dist/client/assets/{InsightsView-DAJSq4gV.js → InsightsView-CDkiJeW1.js} +2 -2
  7. package/dist/client/assets/MemoryView-ZRQ9EL9H.js +2 -0
  8. package/dist/client/assets/NodesView-DosrOyeH.js +14 -0
  9. package/dist/client/assets/NodesView-sJgPLTzz.css +1 -0
  10. package/dist/client/assets/{PiExtensionsManager-DD0fTQNf.js → PiExtensionsManager-CzZ1LEpz.js} +3 -3
  11. package/dist/client/assets/PluginManager-Dp3vPsMO.js +1 -0
  12. package/dist/client/assets/ResearchView-PvNkdaQE.js +1 -0
  13. package/dist/client/assets/{RoadmapsView-DsH7Hicx.js → RoadmapsView-BUW-HJz5.js} +2 -2
  14. package/dist/client/assets/SettingsModal-BNSrO1M9.css +1 -0
  15. package/dist/client/assets/{SettingsModal-Cn_CIPXu.js → SettingsModal-ByVl_fUi.js} +1 -1
  16. package/dist/client/assets/SettingsModal-oOnIed5O.css +1 -0
  17. package/dist/client/assets/SettingsModal-uzo470XS.js +31 -0
  18. package/dist/client/assets/SetupWizardModal-DH1hpyiP.js +1 -0
  19. package/dist/client/assets/SkillsView-B-RqQSFE.js +1 -0
  20. package/dist/client/assets/index-CtiRbTNv.js +1229 -0
  21. package/dist/client/assets/index-Dy-xC2C2.css +1 -0
  22. package/dist/client/assets/{users-DEicv0kj.js → users-WyHhw14V.js} +2 -2
  23. package/dist/client/index.html +2 -2
  24. package/dist/client/version.json +1 -1
  25. package/dist/droid-cli/index.ts +12 -7
  26. package/dist/droid-cli/package.json +4 -1
  27. package/dist/droid-cli/src/__tests__/provider.test.ts +42 -6
  28. package/dist/extension.js +1701 -565
  29. package/dist/pi-claude-cli/package.json +1 -1
  30. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  31. package/package.json +2 -1
  32. package/skill/fusion/SKILL.md +2 -2
  33. package/skill/fusion/references/best-practices.md +33 -0
  34. package/skill/fusion/references/extension-tools.md +47 -2
  35. package/skill/fusion/references/fusion-capabilities.md +7 -2
  36. package/dist/client/assets/AgentDetailView-BmxnuM0D.js +0 -18
  37. package/dist/client/assets/AgentDetailView-yu8Xltqk.css +0 -1
  38. package/dist/client/assets/AgentsView-1xSqjJxs.js +0 -517
  39. package/dist/client/assets/AgentsView-Bs03ptrd.css +0 -1
  40. package/dist/client/assets/ChatView-CkWkEwXL.js +0 -1
  41. package/dist/client/assets/DirectoryPicker-Sqwdifcb.js +0 -1
  42. package/dist/client/assets/DocumentsView-Cx_02o_z.js +0 -1
  43. package/dist/client/assets/MemoryView-CCIBAre3.js +0 -2
  44. package/dist/client/assets/NodesView-D02HxGCl.js +0 -14
  45. package/dist/client/assets/NodesView-DuAXX_0j.css +0 -1
  46. package/dist/client/assets/PluginManager-Cfl0VBX9.js +0 -1
  47. package/dist/client/assets/ResearchView-B9RqOVbr.js +0 -1
  48. package/dist/client/assets/SettingsModal-D_AFkDJa.css +0 -1
  49. package/dist/client/assets/SettingsModal-Dq4a5KSX.css +0 -1
  50. package/dist/client/assets/SettingsModal-YH_rM1ZT.js +0 -31
  51. package/dist/client/assets/SetupWizardModal-k5vqrHZU.js +0 -1
  52. package/dist/client/assets/SkillsView-BIdt5cfB.js +0 -1
  53. package/dist/client/assets/folder-open-B3TO7t7Z.js +0 -6
  54. package/dist/client/assets/index-BIJgrHEn.css +0 -1
  55. package/dist/client/assets/index-BlkXZ4C5.js +0 -682
  56. package/dist/client/assets/star-DW-M-BD_.js +0 -6
  57. package/dist/client/assets/upload-BzG6fknr.js +0 -6
package/dist/extension.js CHANGED
@@ -1389,7 +1389,14 @@ Note: Refs (@e1, @e2) are invalidated after page navigation. Re-snapshot after c
1389
1389
  toolMode: "readonly",
1390
1390
  prompt: `You are a UX design reviewer. Verify frontend changes maintain visual polish and consistency with existing UI patterns and design tokens.
1391
1391
 
1392
- Design System Review:
1392
+ FAST-BAIL RULE (check this FIRST):
1393
+ - The task harness gives you a "Diff Scope" listing the files this task actually changed.
1394
+ - If that list contains NO frontend/UI files (no .tsx/.jsx/.ts/.js component files, no .css/.scss/.sass/.styl, no .html/.vue/.svelte/.astro, no design-token/theme files), respond IMMEDIATELY with a single short line such as "No UI changes in scope \u2014 approved." and STOP.
1395
+ - Do NOT explore the worktree looking for related-looking UI code to critique. If this task didn't change a UI file, your review is a no-op by definition.
1396
+
1397
+ Otherwise, restrict your review to the UI files actually present in the diff scope.
1398
+
1399
+ Design System Review (only for UI files in the diff scope):
1393
1400
  1. **Visual Hierarchy** \u2014 Check that the changes maintain consistent heading levels, content flow, and information architecture
1394
1401
  2. **Spacing and Typography** \u2014 Verify consistent spacing (margins, padding, gaps) and typography scale usage
1395
1402
  3. **Color and Token Consistency** \u2014 Check that CSS custom properties and design tokens are used correctly; no hardcoded color values that bypass the design system
@@ -1397,15 +1404,16 @@ Design System Review:
1397
1404
  5. **Responsive Behavior** \u2014 Check that layouts adapt properly across viewport sizes and maintain usability on mobile
1398
1405
  6. **Fit with Design Language** \u2014 Verify the visual style matches existing patterns (border radius, shadows, transitions, icon style, etc.)
1399
1406
 
1400
- Files to Review:
1407
+ Files to Review (only those that appear in the Diff Scope):
1401
1408
  - Modified UI components (React, Vue, Angular, HTML)
1402
1409
  - CSS/SCSS/styled-component files
1403
1410
  - Design token or theme configuration files
1404
1411
 
1405
1412
  Output Requirements:
1406
- - If design is consistent and polished: call task_done() with success status
1407
- - If issues found: describe each finding with specific file paths and suggested corrections via task_log()
1408
- - Prioritize issues by impact: layout breaks > visual inconsistency > style preferences`
1413
+ - If design is consistent and polished (or there are no UI files in scope): respond with a brief approval line and stop.
1414
+ - If issues found: start your response with "REQUEST REVISION" and describe each finding with specific file paths and suggested corrections.
1415
+ - Prioritize issues by impact: layout breaks > visual inconsistency > style preferences.
1416
+ - Do NOT spend time on stylistic nits when no real issues exist.`
1409
1417
  }
1410
1418
  ];
1411
1419
  DOCUMENT_KEY_RE = /^[a-zA-Z0-9_-]{1,64}$/;
@@ -2763,7 +2771,7 @@ var init_db = __esm({
2763
2771
  "use strict";
2764
2772
  init_sqlite_adapter();
2765
2773
  init_types();
2766
- SCHEMA_VERSION = 59;
2774
+ SCHEMA_VERSION = 60;
2767
2775
  SCHEMA_SQL = `
2768
2776
  -- Tasks table with JSON columns for nested data
2769
2777
  CREATE TABLE IF NOT EXISTS tasks (
@@ -2831,6 +2839,7 @@ CREATE TABLE IF NOT EXISTS tasks (
2831
2839
  missionId TEXT,
2832
2840
  sliceId TEXT,
2833
2841
  assignedAgentId TEXT,
2842
+ pausedByAgentId TEXT,
2834
2843
  assigneeUserId TEXT,
2835
2844
  sourceType TEXT,
2836
2845
  sourceAgentId TEXT,
@@ -4628,6 +4637,12 @@ This means a caller passed a .fusion directory where a project root was expected
4628
4637
  this.db.exec(`CREATE INDEX IF NOT EXISTS idxInsightRunEventsRunIdSeq ON project_insight_run_events(runId, seq)`);
4629
4638
  });
4630
4639
  }
4640
+ if (version < 60) {
4641
+ this.applyMigration(60, () => {
4642
+ this.addColumnIfMissing("tasks", "pausedByAgentId", "TEXT");
4643
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idxTasksPausedByAgentId ON tasks(pausedByAgentId)`);
4644
+ });
4645
+ }
4631
4646
  }
4632
4647
  /**
4633
4648
  * Run a single migration step inside a transaction and bump the version.
@@ -29610,8 +29625,8 @@ var require_CronFileParser = __commonJS({
29610
29625
  * @throws If file cannot be read
29611
29626
  */
29612
29627
  static parseFileSync(filePath) {
29613
- const { readFileSync: readFileSync11 } = __require("fs");
29614
- const data = readFileSync11(filePath, "utf8");
29628
+ const { readFileSync: readFileSync12 } = __require("fs");
29629
+ const data = readFileSync12(filePath, "utf8");
29615
29630
  return _CronFileParser.#parseContent(data);
29616
29631
  }
29617
29632
  /**
@@ -32637,6 +32652,7 @@ var init_store = __esm({
32637
32652
  missionId: row.missionId || void 0,
32638
32653
  sliceId: row.sliceId || void 0,
32639
32654
  assignedAgentId: row.assignedAgentId || void 0,
32655
+ pausedByAgentId: row.pausedByAgentId || void 0,
32640
32656
  assigneeUserId: row.assigneeUserId || void 0,
32641
32657
  nodeId: row.nodeId || void 0,
32642
32658
  effectiveNodeId: row.effectiveNodeId || void 0,
@@ -32901,6 +32917,7 @@ ${recentText}` : void 0
32901
32917
  "missionId",
32902
32918
  "sliceId",
32903
32919
  "assignedAgentId",
32920
+ "pausedByAgentId",
32904
32921
  "assigneeUserId",
32905
32922
  "nodeId",
32906
32923
  "effectiveNodeId",
@@ -33013,6 +33030,7 @@ ${outcome}`;
33013
33030
  "missionId",
33014
33031
  "sliceId",
33015
33032
  "assignedAgentId",
33033
+ "pausedByAgentId",
33016
33034
  "assigneeUserId",
33017
33035
  "nodeId",
33018
33036
  "effectiveNodeId",
@@ -33063,9 +33081,9 @@ ${outcome}`;
33063
33081
  dependencies, steps, log, attachments, steeringComments,
33064
33082
  comments, workflowStepResults, prInfo, issueInfo,
33065
33083
  sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
33066
- mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt
33084
+ mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt
33067
33085
  ) VALUES (
33068
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
33086
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
33069
33087
  )
33070
33088
  ON CONFLICT(id) DO UPDATE SET
33071
33089
  title = excluded.title,
@@ -33134,6 +33152,7 @@ ${outcome}`;
33134
33152
  missionId = excluded.missionId,
33135
33153
  sliceId = excluded.sliceId,
33136
33154
  assignedAgentId = excluded.assignedAgentId,
33155
+ pausedByAgentId = excluded.pausedByAgentId,
33137
33156
  assigneeUserId = excluded.assigneeUserId,
33138
33157
  nodeId = excluded.nodeId,
33139
33158
  effectiveNodeId = excluded.effectiveNodeId,
@@ -33215,6 +33234,7 @@ ${outcome}`;
33215
33234
  task.missionId ?? null,
33216
33235
  task.sliceId ?? null,
33217
33236
  task.assignedAgentId ?? null,
33237
+ task.pausedByAgentId ?? null,
33218
33238
  task.assigneeUserId ?? null,
33219
33239
  task.nodeId ?? null,
33220
33240
  task.effectiveNodeId ?? null,
@@ -34332,6 +34352,23 @@ ${newTask.description}
34332
34352
  const matches = [...activeMatches, ...archiveMatches];
34333
34353
  return limit >= 0 ? matches.slice(0, limit) : matches;
34334
34354
  }
34355
+ async getTasksByAssignedAgent(agentId, options) {
34356
+ const whereClauses = ["assignedAgentId = ?"];
34357
+ const params = [agentId];
34358
+ if (options?.pausedOnly) {
34359
+ whereClauses.push("paused = 1");
34360
+ }
34361
+ if (options?.excludeArchived) {
34362
+ whereClauses.push(`"column" != 'archived'`);
34363
+ }
34364
+ const selectClause = this.getTaskSelectClause(false);
34365
+ const rows = this.db.prepare(`
34366
+ SELECT ${selectClause} FROM tasks
34367
+ WHERE ${whereClauses.join(" AND ")}
34368
+ ORDER BY createdAt ASC
34369
+ `).all(...params);
34370
+ return rows.map((row) => this.rowToTask(row));
34371
+ }
34335
34372
  async selectNextTaskForAgent(agentId) {
34336
34373
  const tasks = await this.listTasks({ slim: true });
34337
34374
  if (tasks.length === 0) {
@@ -34569,6 +34606,11 @@ ${newTask.description}
34569
34606
  } else if (updates.assignedAgentId !== void 0) {
34570
34607
  task.assignedAgentId = updates.assignedAgentId;
34571
34608
  }
34609
+ if (updates.pausedByAgentId === null) {
34610
+ task.pausedByAgentId = void 0;
34611
+ } else if (updates.pausedByAgentId !== void 0) {
34612
+ task.pausedByAgentId = updates.pausedByAgentId;
34613
+ }
34572
34614
  if (updates.assigneeUserId === null) {
34573
34615
  task.assigneeUserId = void 0;
34574
34616
  } else if (updates.assigneeUserId !== void 0) {
@@ -34807,14 +34849,21 @@ ${newTask.description}
34807
34849
  * Pause or unpause a task. Paused tasks are excluded from all automated
34808
34850
  * agent and scheduler interaction. Logs the action and emits `task:updated`.
34809
34851
  */
34810
- async pauseTask(id, paused, runContext) {
34852
+ async pauseTask(id, paused, runContext, agentOptions) {
34811
34853
  return this.withTaskLock(id, async () => {
34812
34854
  const dir = this.taskDir(id);
34813
34855
  const task = await this.readTaskJson(dir);
34814
34856
  if (!task.log) {
34815
34857
  task.log = [];
34816
34858
  }
34859
+ const previousPausedByAgentId = task.pausedByAgentId;
34817
34860
  task.paused = paused || void 0;
34861
+ if (paused && agentOptions?.pausedByAgentId) {
34862
+ task.pausedByAgentId = agentOptions.pausedByAgentId;
34863
+ }
34864
+ if (!paused) {
34865
+ task.pausedByAgentId = void 0;
34866
+ }
34818
34867
  if (task.column === "in-progress" || task.column === "in-review") {
34819
34868
  task.status = paused ? "paused" : void 0;
34820
34869
  }
@@ -34822,7 +34871,7 @@ ${newTask.description}
34822
34871
  task.updatedAt = now;
34823
34872
  const logEntry = {
34824
34873
  timestamp: now,
34825
- action: paused ? "Task paused" : "Task unpaused"
34874
+ action: paused ? agentOptions?.pausedByAgentId ? `Task paused (agent ${agentOptions.pausedByAgentId} paused)` : "Task paused" : previousPausedByAgentId ? `Task unpaused (agent ${previousPausedByAgentId} resumed)` : "Task unpaused"
34826
34875
  };
34827
34876
  if (runContext) {
34828
34877
  logEntry.runContext = runContext;
@@ -39973,10 +40022,17 @@ var init_docker_client = __esm({
39973
40022
  }
39974
40023
  return this.createDockerInstance(hostConfig);
39975
40024
  }
39976
- async getContainerInfo(containerId) {
40025
+ async getContainerInfo(containerId, hostConfig) {
39977
40026
  try {
39978
- const docker = await this.getInstance();
40027
+ const docker = await this.getDockerInstance(hostConfig);
39979
40028
  const inspect = await docker.getContainer(containerId).inspect();
40029
+ const ports = Object.entries(inspect.NetworkSettings?.Ports ?? {}).reduce((acc, [key, value]) => {
40030
+ const binding = Array.isArray(value) && value.length > 0 ? value[0] : void 0;
40031
+ if (binding?.HostPort) {
40032
+ acc[key] = binding.HostPort;
40033
+ }
40034
+ return acc;
40035
+ }, {});
39980
40036
  return {
39981
40037
  id: inspect.Id,
39982
40038
  name: (inspect.Name ?? "").replace(/^\//, ""),
@@ -39988,8 +40044,12 @@ var init_docker_client = __esm({
39988
40044
  paused: Boolean(inspect.State?.Paused),
39989
40045
  restarting: Boolean(inspect.State?.Restarting),
39990
40046
  dead: Boolean(inspect.State?.Dead),
39991
- error: inspect.State?.Error || void 0
39992
- }
40047
+ error: inspect.State?.Error || void 0,
40048
+ exitCode: typeof inspect.State?.ExitCode === "number" ? inspect.State.ExitCode : void 0,
40049
+ startedAt: inspect.State?.StartedAt || void 0,
40050
+ finishedAt: inspect.State?.FinishedAt || void 0
40051
+ },
40052
+ ports
39993
40053
  };
39994
40054
  } catch (error) {
39995
40055
  const message = toErrorMessage(error);
@@ -39999,6 +40059,18 @@ var init_docker_client = __esm({
39999
40059
  throw error;
40000
40060
  }
40001
40061
  }
40062
+ async getContainerLogs(containerId, hostConfig, options) {
40063
+ const docker = await this.getDockerInstance(hostConfig);
40064
+ const stream = await docker.getContainer(containerId).logs({
40065
+ stdout: true,
40066
+ stderr: true,
40067
+ tail: options?.tail ?? 100
40068
+ });
40069
+ if (Buffer.isBuffer(stream)) {
40070
+ return stream.toString("utf8");
40071
+ }
40072
+ return String(stream ?? "");
40073
+ }
40002
40074
  async getInstance() {
40003
40075
  if (!this.dockerInstance) {
40004
40076
  this.dockerInstance = await this.createDockerInstance(this.defaultHostConfig);
@@ -52657,6 +52729,193 @@ var init_chat_store = __esm({
52657
52729
  }
52658
52730
  });
52659
52731
 
52732
+ // ../core/src/oauth-credential-interop.ts
52733
+ import { existsSync as existsSync19, readFileSync as readFileSync6 } from "node:fs";
52734
+ import { homedir as homedir4 } from "node:os";
52735
+ import { join as join22 } from "node:path";
52736
+ function getHomeDir4() {
52737
+ return process.env.HOME || process.env.USERPROFILE || homedir4();
52738
+ }
52739
+ function getCodexCliAuthPath(home = getHomeDir4()) {
52740
+ return join22(home, ".codex", "auth.json");
52741
+ }
52742
+ function parseJwtPayload(token) {
52743
+ try {
52744
+ const [, payload = ""] = token.split(".", 3);
52745
+ if (!payload) {
52746
+ return null;
52747
+ }
52748
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
52749
+ } catch {
52750
+ return null;
52751
+ }
52752
+ }
52753
+ function getJwtExpiryMs(token) {
52754
+ if (!token) {
52755
+ return void 0;
52756
+ }
52757
+ const payload = parseJwtPayload(token);
52758
+ const exp = payload?.exp;
52759
+ if (typeof exp !== "number" || !Number.isFinite(exp)) {
52760
+ return void 0;
52761
+ }
52762
+ return exp * 1e3;
52763
+ }
52764
+ function getCodexAccountId(accessToken, fallbackAccountId) {
52765
+ const payload = parseJwtPayload(accessToken);
52766
+ const authClaim = payload?.[OPENAI_AUTH_CLAIM];
52767
+ const claimAccountId = authClaim && typeof authClaim === "object" ? authClaim.chatgpt_account_id : void 0;
52768
+ if (typeof claimAccountId === "string" && claimAccountId.trim().length > 0) {
52769
+ return claimAccountId;
52770
+ }
52771
+ if (typeof fallbackAccountId === "string" && fallbackAccountId.trim().length > 0) {
52772
+ return fallbackAccountId;
52773
+ }
52774
+ return void 0;
52775
+ }
52776
+ function getLastRefreshFallbackExpiryMs(lastRefresh) {
52777
+ if (typeof lastRefresh !== "string" || lastRefresh.trim().length === 0) {
52778
+ return void 0;
52779
+ }
52780
+ const parsed = Date.parse(lastRefresh);
52781
+ if (!Number.isFinite(parsed)) {
52782
+ return void 0;
52783
+ }
52784
+ return parsed + CODEX_REFRESH_FALLBACK_WINDOW_MS;
52785
+ }
52786
+ function isStoredAuthCredential(value) {
52787
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
52788
+ return false;
52789
+ }
52790
+ const record = value;
52791
+ return record.type === "oauth" || record.type === "api_key";
52792
+ }
52793
+ function isValidOauthCredential(credential) {
52794
+ return credential?.type === "oauth" && typeof credential.access === "string" && credential.access.length > 0 && typeof credential.refresh === "string" && credential.refresh.length > 0 && typeof credential.expires === "number" && Number.isFinite(credential.expires) && Date.now() < credential.expires;
52795
+ }
52796
+ function isRefreshableOauthCredential(credential) {
52797
+ return credential?.type === "oauth" && typeof credential.refresh === "string" && credential.refresh.length > 0 && typeof credential.expires === "number" && Number.isFinite(credential.expires);
52798
+ }
52799
+ function compareStoredCredentials(left, right) {
52800
+ if (!left && !right) {
52801
+ return 0;
52802
+ }
52803
+ if (left && !right) {
52804
+ return 1;
52805
+ }
52806
+ if (!left && right) {
52807
+ return -1;
52808
+ }
52809
+ if (left?.type === "api_key" && right?.type !== "api_key") {
52810
+ return 1;
52811
+ }
52812
+ if (right?.type === "api_key" && left?.type !== "api_key") {
52813
+ return -1;
52814
+ }
52815
+ if (left?.type === "oauth" && right?.type === "oauth") {
52816
+ const leftValid = isValidOauthCredential(left);
52817
+ const rightValid = isValidOauthCredential(right);
52818
+ if (leftValid !== rightValid) {
52819
+ return leftValid ? 1 : -1;
52820
+ }
52821
+ const leftRefreshable = isRefreshableOauthCredential(left);
52822
+ const rightRefreshable = isRefreshableOauthCredential(right);
52823
+ if (leftRefreshable !== rightRefreshable) {
52824
+ return leftRefreshable ? 1 : -1;
52825
+ }
52826
+ const leftExpiry = typeof left.expires === "number" && Number.isFinite(left.expires) ? left.expires : -Infinity;
52827
+ const rightExpiry = typeof right.expires === "number" && Number.isFinite(right.expires) ? right.expires : -Infinity;
52828
+ if (leftExpiry !== rightExpiry) {
52829
+ return leftExpiry > rightExpiry ? 1 : -1;
52830
+ }
52831
+ const leftAccessLength = typeof left.access === "string" ? left.access.length : 0;
52832
+ const rightAccessLength = typeof right.access === "string" ? right.access.length : 0;
52833
+ if (leftAccessLength !== rightAccessLength) {
52834
+ return leftAccessLength > rightAccessLength ? 1 : -1;
52835
+ }
52836
+ }
52837
+ return 0;
52838
+ }
52839
+ function choosePreferredStoredCredential(...credentials) {
52840
+ let best;
52841
+ for (const credential of credentials) {
52842
+ if (compareStoredCredentials(credential, best) > 0) {
52843
+ best = credential;
52844
+ }
52845
+ }
52846
+ return best;
52847
+ }
52848
+ function shouldHydrateStoredCredential(current, candidate) {
52849
+ if (!candidate || candidate.type !== "oauth") {
52850
+ return false;
52851
+ }
52852
+ if (current?.type === "api_key") {
52853
+ return false;
52854
+ }
52855
+ return compareStoredCredentials(candidate, current) > 0;
52856
+ }
52857
+ function extractCodexCliStoredCredential(raw) {
52858
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
52859
+ return void 0;
52860
+ }
52861
+ const record = raw;
52862
+ const tokens = record.tokens;
52863
+ if (!tokens || typeof tokens !== "object" || Array.isArray(tokens)) {
52864
+ return void 0;
52865
+ }
52866
+ const tokenRecord = tokens;
52867
+ const access7 = typeof tokenRecord.access_token === "string" ? tokenRecord.access_token : void 0;
52868
+ const refresh = typeof tokenRecord.refresh_token === "string" ? tokenRecord.refresh_token : void 0;
52869
+ if (!access7 || !refresh) {
52870
+ return void 0;
52871
+ }
52872
+ const expires = getJwtExpiryMs(access7) ?? getJwtExpiryMs(typeof tokenRecord.id_token === "string" ? tokenRecord.id_token : void 0) ?? getLastRefreshFallbackExpiryMs(record.last_refresh);
52873
+ if (typeof expires !== "number" || !Number.isFinite(expires)) {
52874
+ return void 0;
52875
+ }
52876
+ const accountId = getCodexAccountId(access7, tokenRecord.account_id);
52877
+ return {
52878
+ type: "oauth",
52879
+ access: access7,
52880
+ refresh,
52881
+ expires,
52882
+ ...accountId ? { accountId } : {}
52883
+ };
52884
+ }
52885
+ function readStoredCredentialsFromAuthFile(authPath) {
52886
+ if (!existsSync19(authPath)) {
52887
+ return {};
52888
+ }
52889
+ try {
52890
+ const parsed = JSON.parse(readFileSync6(authPath, "utf-8"));
52891
+ const codexCliCredential = extractCodexCliStoredCredential(parsed);
52892
+ if (codexCliCredential) {
52893
+ return { "openai-codex": codexCliCredential };
52894
+ }
52895
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
52896
+ return {};
52897
+ }
52898
+ const credentials = {};
52899
+ for (const [providerId, value] of Object.entries(parsed)) {
52900
+ if (!isStoredAuthCredential(value)) {
52901
+ continue;
52902
+ }
52903
+ credentials[providerId] = value;
52904
+ }
52905
+ return credentials;
52906
+ } catch {
52907
+ return {};
52908
+ }
52909
+ }
52910
+ var OPENAI_AUTH_CLAIM, CODEX_REFRESH_FALLBACK_WINDOW_MS;
52911
+ var init_oauth_credential_interop = __esm({
52912
+ "../core/src/oauth-credential-interop.ts"() {
52913
+ "use strict";
52914
+ OPENAI_AUTH_CLAIM = "https://api.openai.com/auth";
52915
+ CODEX_REFRESH_FALLBACK_WINDOW_MS = 55 * 60 * 1e3;
52916
+ }
52917
+ });
52918
+
52660
52919
  // ../core/src/index.ts
52661
52920
  var src_exports = {};
52662
52921
  __export(src_exports, {
@@ -52813,6 +53072,7 @@ __export(src_exports, {
52813
53072
  buildTriageMemoryInstructions: () => buildTriageMemoryInstructions,
52814
53073
  canTransition: () => canTransition,
52815
53074
  checkRateLimit: () => checkRateLimit,
53075
+ choosePreferredStoredCredential: () => choosePreferredStoredCredential,
52816
53076
  classifyInsightRunError: () => classifyInsightRunError,
52817
53077
  clearOverrides: () => clearOverrides,
52818
53078
  collectSystemMetrics: () => collectSystemMetrics,
@@ -52843,6 +53103,7 @@ __export(src_exports, {
52843
53103
  executeInsightRunLifecycle: () => executeInsightRunLifecycle,
52844
53104
  exportAgentsToDirectory: () => exportAgentsToDirectory,
52845
53105
  exportSettings: () => exportSettings,
53106
+ extractCodexCliStoredCredential: () => extractCodexCliStoredCredential,
52846
53107
  extractDreamProcessorResult: () => extractDreamProcessorResult,
52847
53108
  formatPiExtensionSource: () => formatPiExtensionSource,
52848
53109
  fromJson: () => fromJson,
@@ -52853,6 +53114,7 @@ __export(src_exports, {
52853
53114
  generateMemoryAudit: () => generateMemoryAudit,
52854
53115
  getAppVersion: () => getAppVersion,
52855
53116
  getAvailableTemplates: () => getAvailableTemplates,
53117
+ getCodexCliAuthPath: () => getCodexCliAuthPath,
52856
53118
  getCurrentRepo: () => getCurrentRepo,
52857
53119
  getDefaultDailyMemoryScaffold: () => getDefaultDailyMemoryScaffold,
52858
53120
  getDefaultDreamsScaffold: () => getDefaultDreamsScaffold,
@@ -52950,6 +53212,7 @@ __export(src_exports, {
52950
53212
  readProjectMemoryFile: () => readProjectMemoryFile,
52951
53213
  readProjectMemoryFileContent: () => readProjectMemoryFileContent,
52952
53214
  readProjectMemoryWithBackend: () => readProjectMemoryWithBackend,
53215
+ readStoredCredentialsFromAuthFile: () => readStoredCredentialsFromAuthFile,
52953
53216
  readWorkingMemory: () => readWorkingMemory,
52954
53217
  reconcileClaudeCliPaths: () => reconcileClaudeCliPaths,
52955
53218
  reconcileDroidCliPaths: () => reconcileDroidCliPaths,
@@ -52985,6 +53248,7 @@ __export(src_exports, {
52985
53248
  scheduleQmdProjectMemoryRefresh: () => scheduleQmdProjectMemoryRefresh,
52986
53249
  searchProjectMemory: () => searchProjectMemory,
52987
53250
  setCreateFnAgent: () => setCreateFnAgent,
53251
+ shouldHydrateStoredCredential: () => shouldHydrateStoredCredential,
52988
53252
  shouldSkipBackgroundQmdRefresh: () => shouldSkipBackgroundQmdRefresh,
52989
53253
  shouldTriggerExtraction: () => shouldTriggerExtraction,
52990
53254
  slugify: () => slugify,
@@ -53089,6 +53353,7 @@ var init_src = __esm({
53089
53353
  init_agent_companies_parser();
53090
53354
  init_agent_companies_exporter();
53091
53355
  init_chat_store();
53356
+ init_oauth_credential_interop();
53092
53357
  init_error_message();
53093
53358
  }
53094
53359
  });
@@ -54278,12 +54543,12 @@ var init_github_provider = __esm({
54278
54543
  });
54279
54544
 
54280
54545
  // ../engine/src/skill-resolver.ts
54281
- import { existsSync as existsSync19, readFileSync as readFileSync6 } from "node:fs";
54282
- import { dirname as dirname8, join as join22, resolve as resolve11 } from "node:path";
54546
+ import { existsSync as existsSync20, readFileSync as readFileSync7 } from "node:fs";
54547
+ import { dirname as dirname8, join as join23, resolve as resolve11 } from "node:path";
54283
54548
  function resolveProjectRoot(cwd) {
54284
54549
  let current = resolve11(cwd);
54285
54550
  while (true) {
54286
- if (existsSync19(join22(current, ".fusion"))) {
54551
+ if (existsSync20(join23(current, ".fusion"))) {
54287
54552
  return current;
54288
54553
  }
54289
54554
  const parent = dirname8(current);
@@ -54294,19 +54559,19 @@ function resolveProjectRoot(cwd) {
54294
54559
  }
54295
54560
  }
54296
54561
  function readJsonObject(path2) {
54297
- if (!existsSync19(path2)) {
54562
+ if (!existsSync20(path2)) {
54298
54563
  return {};
54299
54564
  }
54300
54565
  try {
54301
- const parsed = JSON.parse(readFileSync6(path2, "utf-8"));
54566
+ const parsed = JSON.parse(readFileSync7(path2, "utf-8"));
54302
54567
  return parsed && typeof parsed === "object" ? parsed : {};
54303
54568
  } catch {
54304
54569
  return {};
54305
54570
  }
54306
54571
  }
54307
54572
  function readProjectSettings(projectRootDir) {
54308
- const fusionSettings = join22(projectRootDir, ".fusion", "settings.json");
54309
- if (existsSync19(fusionSettings)) {
54573
+ const fusionSettings = join23(projectRootDir, ".fusion", "settings.json");
54574
+ if (existsSync20(fusionSettings)) {
54310
54575
  const parsed = readJsonObject(fusionSettings);
54311
54576
  return {
54312
54577
  skills: Array.isArray(parsed.skills) ? parsed.skills : void 0,
@@ -54324,6 +54589,9 @@ function normalizePattern(pattern) {
54324
54589
  function isExclusionPattern(pattern) {
54325
54590
  return pattern.startsWith("-");
54326
54591
  }
54592
+ function bareSkillName(name) {
54593
+ return name.replace(/\/SKILL\.md$/i, "");
54594
+ }
54327
54595
  function resolveSessionSkills(context) {
54328
54596
  const { requestedSkillNames } = context;
54329
54597
  const projectRootDir = resolveProjectRoot(context.projectRootDir);
@@ -54417,25 +54685,40 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
54417
54685
  const hasRequestedNames = Boolean(requestedSkillNames && requestedSkillNames.length > 0);
54418
54686
  const hasExcluded = excludedSkillPaths.size > 0;
54419
54687
  let filteredSkills;
54688
+ const skillNameMatches = (skill, pattern) => bareSkillName(skill.name).toLowerCase() === bareSkillName(pattern).toLowerCase() || skill.filePath === pattern;
54689
+ const isExcluded = (skill) => {
54690
+ for (const ep of excludedSkillPaths) {
54691
+ if (skillNameMatches(skill, ep)) return true;
54692
+ }
54693
+ return false;
54694
+ };
54695
+ const isAllowed = (skill) => {
54696
+ for (const ap of allowedSkillPaths) {
54697
+ if (skillNameMatches(skill, ap)) return true;
54698
+ }
54699
+ return false;
54700
+ };
54420
54701
  if (hasRequestedNames) {
54421
- const requestedNamesLower = new Set(requestedSkillNames.map((n) => n.toLowerCase()));
54702
+ const requestedBareNamesLower = new Set(requestedSkillNames.map((n) => bareSkillName(n).toLowerCase()));
54422
54703
  filteredSkills = base.skills.filter(
54423
- (skill) => requestedNamesLower.has(skill.name.toLowerCase()) && !excludedSkillPaths.has(skill.filePath)
54704
+ (skill) => requestedBareNamesLower.has(bareSkillName(skill.name).toLowerCase()) && !isExcluded(skill)
54424
54705
  );
54425
54706
  } else if (hasPatterns) {
54426
54707
  filteredSkills = base.skills.filter(
54427
- (skill) => allowedSkillPaths.has(skill.filePath) && !excludedSkillPaths.has(skill.filePath)
54708
+ (skill) => isAllowed(skill) && !isExcluded(skill)
54428
54709
  );
54429
54710
  } else if (hasExcluded) {
54430
- filteredSkills = base.skills.filter((skill) => !excludedSkillPaths.has(skill.filePath));
54711
+ filteredSkills = base.skills.filter((skill) => !isExcluded(skill));
54431
54712
  } else {
54432
54713
  filteredSkills = base.skills;
54433
54714
  }
54434
54715
  const newDiagnostics = [];
54435
54716
  const purpose = sessionPurpose ? ` [${sessionPurpose}]` : "";
54436
- const discoveredPaths = new Set(base.skills.map((s) => s.filePath));
54717
+ const discoveredBareNames = new Set(base.skills.map((s) => bareSkillName(s.name).toLowerCase()));
54718
+ const discoveredFilePaths = new Set(base.skills.map((s) => s.filePath));
54719
+ const hasDiscoveredMatch = (pattern) => discoveredBareNames.has(bareSkillName(pattern).toLowerCase()) || discoveredFilePaths.has(pattern);
54437
54720
  for (const excludedPath of excludedSkillPaths) {
54438
- if (discoveredPaths.has(excludedPath)) {
54721
+ if (hasDiscoveredMatch(excludedPath)) {
54439
54722
  newDiagnostics.push({
54440
54723
  type: "warning",
54441
54724
  message: `Skill at '${excludedPath}' exists but is disabled by project execution settings${purpose}`,
@@ -54444,7 +54727,7 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
54444
54727
  }
54445
54728
  }
54446
54729
  for (const allowedPath of allowedSkillPaths) {
54447
- if (!discoveredPaths.has(allowedPath)) {
54730
+ if (!hasDiscoveredMatch(allowedPath)) {
54448
54731
  newDiagnostics.push({
54449
54732
  type: "warning",
54450
54733
  message: `Configured skill pattern '${allowedPath}' not found in discovered skills${purpose}`,
@@ -54453,9 +54736,9 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
54453
54736
  }
54454
54737
  }
54455
54738
  if (requestedSkillNames) {
54456
- const discoveredNamesLower = new Set(base.skills.map((s) => s.name.toLowerCase()));
54739
+ const discoveredBareNamesLower = new Set(base.skills.map((s) => bareSkillName(s.name).toLowerCase()));
54457
54740
  for (const requestedName of requestedSkillNames) {
54458
- if (!discoveredNamesLower.has(requestedName.toLowerCase()) && !isBuiltInFallbackRequest(requestedName)) {
54741
+ if (!discoveredBareNamesLower.has(bareSkillName(requestedName).toLowerCase()) && !isBuiltInFallbackRequest(requestedName)) {
54459
54742
  const purpose2 = sessionPurpose ? ` [${sessionPurpose}]` : "";
54460
54743
  newDiagnostics.push({
54461
54744
  type: "warning",
@@ -54531,51 +54814,51 @@ var init_context_limit_detector = __esm({
54531
54814
  });
54532
54815
 
54533
54816
  // ../engine/src/auth-storage.ts
54534
- import { existsSync as existsSync20, readFileSync as readFileSync7 } from "node:fs";
54535
- import { homedir as homedir4 } from "node:os";
54536
- import { join as join23 } from "node:path";
54817
+ import { existsSync as existsSync21, readFileSync as readFileSync8 } from "node:fs";
54818
+ import { homedir as homedir5 } from "node:os";
54819
+ import { join as join24 } from "node:path";
54537
54820
  import { AuthStorage } from "@mariozechner/pi-coding-agent";
54538
54821
  import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
54539
- function getHomeDir4() {
54540
- return process.env.HOME || process.env.USERPROFILE || homedir4();
54822
+ function getHomeDir5() {
54823
+ return process.env.HOME || process.env.USERPROFILE || homedir5();
54541
54824
  }
54542
- function getFusionAuthPath(home = getHomeDir4()) {
54543
- return join23(home, ".fusion", "agent", "auth.json");
54825
+ function getFusionAuthPath(home = getHomeDir5()) {
54826
+ return join24(home, ".fusion", "agent", "auth.json");
54544
54827
  }
54545
- function getFusionModelsPath(home = getHomeDir4()) {
54546
- return join23(home, ".fusion", "agent", "models.json");
54828
+ function getFusionModelsPath(home = getHomeDir5()) {
54829
+ return join24(home, ".fusion", "agent", "models.json");
54830
+ }
54831
+ function getLegacyAuthPaths(home = getHomeDir5()) {
54832
+ return [
54833
+ join24(home, ".pi", "agent", "auth.json"),
54834
+ join24(home, ".pi", "auth.json")
54835
+ ];
54547
54836
  }
54548
- function getLegacyAuthPaths(home = getHomeDir4()) {
54837
+ function getSupplementalAuthPaths(home = getHomeDir5()) {
54549
54838
  return [
54550
- join23(home, ".pi", "agent", "auth.json"),
54551
- join23(home, ".pi", "auth.json")
54839
+ ...getLegacyAuthPaths(home),
54840
+ getCodexCliAuthPath(home)
54552
54841
  ];
54553
54842
  }
54554
- function getLegacyModelsPaths(home = getHomeDir4()) {
54843
+ function getLegacyModelsPaths(home = getHomeDir5()) {
54555
54844
  return [
54556
- join23(home, ".pi", "agent", "models.json"),
54557
- join23(home, ".pi", "models.json")
54845
+ join24(home, ".pi", "agent", "models.json"),
54846
+ join24(home, ".pi", "models.json")
54558
54847
  ];
54559
54848
  }
54560
- function getModelRegistryModelsPath(home = getHomeDir4()) {
54849
+ function getModelRegistryModelsPath(home = getHomeDir5()) {
54561
54850
  const fusionModelsPath = getFusionModelsPath(home);
54562
- if (existsSync20(fusionModelsPath)) {
54851
+ if (existsSync21(fusionModelsPath)) {
54563
54852
  return fusionModelsPath;
54564
54853
  }
54565
- return getLegacyModelsPaths(home).find((modelsPath) => existsSync20(modelsPath)) ?? fusionModelsPath;
54854
+ return getLegacyModelsPaths(home).find((modelsPath) => existsSync21(modelsPath)) ?? fusionModelsPath;
54566
54855
  }
54567
- function readLegacyCredentials(authPaths = getLegacyAuthPaths()) {
54856
+ function readSupplementalCredentials(authPaths = getSupplementalAuthPaths()) {
54568
54857
  const credentials = {};
54569
54858
  for (const authPath of authPaths) {
54570
- if (!existsSync20(authPath)) {
54571
- continue;
54572
- }
54573
- try {
54574
- const parsed = JSON.parse(readFileSync7(authPath, "utf-8"));
54575
- for (const [provider, credential] of Object.entries(parsed)) {
54576
- credentials[provider] ??= credential;
54577
- }
54578
- } catch {
54859
+ const parsed = readStoredCredentialsFromAuthFile(authPath);
54860
+ for (const [provider, credential] of Object.entries(parsed)) {
54861
+ credentials[provider] = choosePreferredStoredCredential(credentials[provider], credential) ?? credential;
54579
54862
  }
54580
54863
  }
54581
54864
  return credentials;
@@ -54599,14 +54882,14 @@ function resolveStoredCredentialApiKey(providerId, credential) {
54599
54882
  }
54600
54883
  return void 0;
54601
54884
  }
54602
- function readModelsJsonApiKeys(home = getHomeDir4()) {
54885
+ function readModelsJsonApiKeys(home = getHomeDir5()) {
54603
54886
  const apiKeys = /* @__PURE__ */ new Map();
54604
54887
  const modelsPath = getModelRegistryModelsPath(home);
54605
- if (!existsSync20(modelsPath)) {
54888
+ if (!existsSync21(modelsPath)) {
54606
54889
  return apiKeys;
54607
54890
  }
54608
54891
  try {
54609
- const parsed = JSON.parse(readFileSync7(modelsPath, "utf-8"));
54892
+ const parsed = JSON.parse(readFileSync8(modelsPath, "utf-8"));
54610
54893
  const providers = parsed?.providers;
54611
54894
  if (providers) {
54612
54895
  for (const [providerId, config] of Object.entries(providers)) {
@@ -54621,8 +54904,20 @@ function readModelsJsonApiKeys(home = getHomeDir4()) {
54621
54904
  }
54622
54905
  function createFusionAuthStorage() {
54623
54906
  const primary = AuthStorage.create(getFusionAuthPath());
54624
- let legacyCredentials = readLegacyCredentials();
54907
+ let supplementalCredentials = readSupplementalCredentials();
54625
54908
  let modelsJsonApiKeys = readModelsJsonApiKeys();
54909
+ const syncSupplementalOauthCredentials = () => {
54910
+ for (const [provider, credential] of Object.entries(supplementalCredentials)) {
54911
+ const current = primary.get(provider);
54912
+ if (!shouldHydrateStoredCredential(current, credential)) {
54913
+ continue;
54914
+ }
54915
+ if (credential.type === "oauth" || credential.type === "api_key") {
54916
+ primary.set(provider, credential);
54917
+ }
54918
+ }
54919
+ };
54920
+ syncSupplementalOauthCredentials();
54626
54921
  return new Proxy(primary, {
54627
54922
  // Forward property writes to the target so that methods like
54628
54923
  // `setFallbackResolver` (called by ModelRegistry) correctly update the
@@ -54636,31 +54931,51 @@ function createFusionAuthStorage() {
54636
54931
  if (prop === "reload") {
54637
54932
  return () => {
54638
54933
  target.reload();
54639
- legacyCredentials = readLegacyCredentials();
54934
+ supplementalCredentials = readSupplementalCredentials();
54935
+ syncSupplementalOauthCredentials();
54640
54936
  modelsJsonApiKeys = readModelsJsonApiKeys();
54641
54937
  };
54642
54938
  }
54643
54939
  if (prop === "get") {
54644
- return (provider) => target.get(provider) ?? legacyCredentials[provider];
54940
+ return (provider) => choosePreferredStoredCredential(
54941
+ target.get(provider),
54942
+ supplementalCredentials[provider]
54943
+ );
54645
54944
  }
54646
54945
  if (prop === "has") {
54647
- return (provider) => target.has(provider) || provider in legacyCredentials || modelsJsonApiKeys.has(provider);
54946
+ return (provider) => target.has(provider) || provider in supplementalCredentials || modelsJsonApiKeys.has(provider);
54648
54947
  }
54649
54948
  if (prop === "hasAuth") {
54650
- return (provider) => target.hasAuth(provider) || Boolean(legacyCredentials[provider]) || modelsJsonApiKeys.has(provider);
54949
+ return (provider) => target.hasAuth(provider) || Boolean(supplementalCredentials[provider]) || modelsJsonApiKeys.has(provider);
54651
54950
  }
54652
54951
  if (prop === "getAll") {
54653
- return () => ({ ...legacyCredentials, ...target.getAll() });
54952
+ return () => {
54953
+ const providerIds = /* @__PURE__ */ new Set([
54954
+ ...Object.keys(supplementalCredentials),
54955
+ ...Object.keys(target.getAll())
54956
+ ]);
54957
+ const merged = {};
54958
+ for (const providerId of providerIds) {
54959
+ const credential = choosePreferredStoredCredential(
54960
+ target.get(providerId),
54961
+ supplementalCredentials[providerId]
54962
+ );
54963
+ if (credential) {
54964
+ merged[providerId] = credential;
54965
+ }
54966
+ }
54967
+ return merged;
54968
+ };
54654
54969
  }
54655
54970
  if (prop === "list") {
54656
- return () => Array.from(/* @__PURE__ */ new Set([...Object.keys(legacyCredentials), ...target.list(), ...modelsJsonApiKeys.keys()]));
54971
+ return () => Array.from(/* @__PURE__ */ new Set([...Object.keys(supplementalCredentials), ...target.list(), ...modelsJsonApiKeys.keys()]));
54657
54972
  }
54658
54973
  if (prop === "getApiKey") {
54659
54974
  return async (provider) => {
54660
54975
  const primaryKey = await target.getApiKey(provider);
54661
54976
  if (primaryKey) return primaryKey;
54662
- const legacyKey = resolveStoredCredentialApiKey(provider, legacyCredentials[provider]);
54663
- if (legacyKey) return legacyKey;
54977
+ const supplementalKey = resolveStoredCredentialApiKey(provider, supplementalCredentials[provider]);
54978
+ if (supplementalKey) return supplementalKey;
54664
54979
  return modelsJsonApiKeys.get(provider);
54665
54980
  };
54666
54981
  }
@@ -54671,17 +54986,18 @@ function createFusionAuthStorage() {
54671
54986
  var init_auth_storage = __esm({
54672
54987
  "../engine/src/auth-storage.ts"() {
54673
54988
  "use strict";
54989
+ init_src();
54674
54990
  }
54675
54991
  });
54676
54992
 
54677
54993
  // ../engine/src/custom-providers.ts
54678
- import { readFileSync as readFileSync8 } from "node:fs";
54679
- import { homedir as homedir5 } from "node:os";
54680
- import { join as join24 } from "node:path";
54994
+ import { readFileSync as readFileSync9 } from "node:fs";
54995
+ import { homedir as homedir6 } from "node:os";
54996
+ import { join as join25 } from "node:path";
54681
54997
  function readCustomProviders() {
54682
54998
  try {
54683
- const settingsPath = join24(homedir5(), ".fusion", "settings.json");
54684
- const raw = readFileSync8(settingsPath, "utf-8");
54999
+ const settingsPath = join25(homedir6(), ".fusion", "settings.json");
55000
+ const raw = readFileSync9(settingsPath, "utf-8");
54685
55001
  const parsed = JSON.parse(raw);
54686
55002
  return Array.isArray(parsed.customProviders) ? parsed.customProviders : [];
54687
55003
  } catch {
@@ -54695,11 +55011,11 @@ var init_custom_providers = __esm({
54695
55011
  });
54696
55012
 
54697
55013
  // ../engine/src/pi.ts
54698
- import { existsSync as existsSync21, readFileSync as readFileSync9 } from "node:fs";
55014
+ import { existsSync as existsSync22, readFileSync as readFileSync10 } from "node:fs";
54699
55015
  import { exec as exec2 } from "node:child_process";
54700
55016
  import { promisify as promisify3 } from "node:util";
54701
55017
  import { createRequire as createRequire2 } from "node:module";
54702
- import { basename as basename7, dirname as dirname9, join as join25, relative as relative3, isAbsolute as isAbsolute7, resolve as resolve12 } from "node:path";
55018
+ import { basename as basename7, dirname as dirname9, join as join26, relative as relative3, isAbsolute as isAbsolute7, resolve as resolve12 } from "node:path";
54703
55019
  import {
54704
55020
  createAgentSession,
54705
55021
  createBashTool,
@@ -54776,6 +55092,11 @@ async function promptSessionAndCheck(session, prompt, options) {
54776
55092
  piLog.warn(`pi state error \u2014 failed to inspect transcript: ${inspectErr instanceof Error ? inspectErr.message : String(inspectErr)}`);
54777
55093
  }
54778
55094
  }
55095
+ if (/Provider finish_reason:\s*repeat\b/i.test(stateError)) {
55096
+ piLog.warn(`pi state error \u2014 treating provider finish_reason=repeat as soft stop: ${stateError}`);
55097
+ clearSessionStateError(session);
55098
+ return;
55099
+ }
54779
55100
  throw new Error(stateError);
54780
55101
  }
54781
55102
  }
@@ -54953,11 +55274,11 @@ function isRetryableModelSelectionError(message) {
54953
55274
  return normalized.includes("rate limit") || normalized.includes("too many requests") || normalized.includes("429") || normalized.includes("401") || normalized.includes("403") || normalized.includes("unauthorized") || normalized.includes("forbidden") || normalized.includes("authentication") || normalized.includes("invalid api key") || normalized.includes("invalid key") || normalized.includes("api key") || normalized.includes("overloaded") || normalized.includes("quota") || normalized.includes("capacity") || normalized.includes("temporarily unavailable") || normalized.includes("invalid temperature");
54954
55275
  }
54955
55276
  function readJsonObject2(path2) {
54956
- if (!existsSync21(path2)) {
55277
+ if (!existsSync22(path2)) {
54957
55278
  return {};
54958
55279
  }
54959
55280
  try {
54960
- const parsed = JSON.parse(readFileSync9(path2, "utf-8"));
55281
+ const parsed = JSON.parse(readFileSync10(path2, "utf-8"));
54961
55282
  return parsed && typeof parsed === "object" ? parsed : {};
54962
55283
  } catch {
54963
55284
  return {};
@@ -55094,17 +55415,17 @@ function siblingAgentDir(agentDir, siblingRoot) {
55094
55415
  if (basename7(agentDir) !== "agent") {
55095
55416
  return void 0;
55096
55417
  }
55097
- return join25(dirname9(dirname9(agentDir)), siblingRoot, "agent");
55418
+ return join26(dirname9(dirname9(agentDir)), siblingRoot, "agent");
55098
55419
  }
55099
55420
  function createReadOnlyPiSettingsView(cwd, agentDir) {
55100
55421
  const projectRoot = resolvePiExtensionProjectRoot(cwd);
55101
- const fusionAgentDir = agentDir.includes(`${join25(".fusion", "agent")}`) ? agentDir : siblingAgentDir(agentDir, ".fusion");
55102
- const legacyAgentDir = agentDir.includes(`${join25(".pi", "agent")}`) ? agentDir : siblingAgentDir(agentDir, ".pi");
55103
- const legacyGlobalSettings = legacyAgentDir ? readJsonObject2(join25(legacyAgentDir, "settings.json")) : {};
55104
- const fusionGlobalSettings = fusionAgentDir ? readJsonObject2(join25(fusionAgentDir, "settings.json")) : {};
55105
- const directGlobalSettings = readJsonObject2(join25(agentDir, "settings.json"));
55422
+ const fusionAgentDir = agentDir.includes(`${join26(".fusion", "agent")}`) ? agentDir : siblingAgentDir(agentDir, ".fusion");
55423
+ const legacyAgentDir = agentDir.includes(`${join26(".pi", "agent")}`) ? agentDir : siblingAgentDir(agentDir, ".pi");
55424
+ const legacyGlobalSettings = legacyAgentDir ? readJsonObject2(join26(legacyAgentDir, "settings.json")) : {};
55425
+ const fusionGlobalSettings = fusionAgentDir ? readJsonObject2(join26(fusionAgentDir, "settings.json")) : {};
55426
+ const directGlobalSettings = readJsonObject2(join26(agentDir, "settings.json"));
55106
55427
  const globalSettings = { ...legacyGlobalSettings, ...directGlobalSettings, ...fusionGlobalSettings };
55107
- const fusionProjectSettings = readJsonObject2(join25(projectRoot, ".fusion", "settings.json"));
55428
+ const fusionProjectSettings = readJsonObject2(join26(projectRoot, ".fusion", "settings.json"));
55108
55429
  const mergedSettings = { ...globalSettings, ...fusionProjectSettings };
55109
55430
  return {
55110
55431
  getGlobalSettings: () => structuredClone(globalSettings),
@@ -55115,27 +55436,27 @@ function createReadOnlyPiSettingsView(cwd, agentDir) {
55115
55436
  function getPackageManagerAgentDir() {
55116
55437
  const fusionAgentDir = getFusionAgentDir();
55117
55438
  const legacyAgentDir = getLegacyPiAgentDir();
55118
- const fusionSettings = readJsonObject2(join25(fusionAgentDir, "settings.json"));
55119
- const legacySettings = readJsonObject2(join25(legacyAgentDir, "settings.json"));
55120
- if (hasPackageManagerSettings(fusionSettings) || !existsSync21(legacyAgentDir)) {
55439
+ const fusionSettings = readJsonObject2(join26(fusionAgentDir, "settings.json"));
55440
+ const legacySettings = readJsonObject2(join26(legacyAgentDir, "settings.json"));
55441
+ if (hasPackageManagerSettings(fusionSettings) || !existsSync22(legacyAgentDir)) {
55121
55442
  return fusionAgentDir;
55122
55443
  }
55123
55444
  if (hasPackageManagerSettings(legacySettings)) {
55124
55445
  return legacyAgentDir;
55125
55446
  }
55126
- return existsSync21(fusionAgentDir) ? fusionAgentDir : legacyAgentDir;
55447
+ return existsSync22(fusionAgentDir) ? fusionAgentDir : legacyAgentDir;
55127
55448
  }
55128
55449
  function resolveVendoredClaudeCliEntry() {
55129
55450
  try {
55130
55451
  const require_ = createRequire2(import.meta.url);
55131
55452
  const pkgJsonPath = require_.resolve("@fusion/pi-claude-cli/package.json");
55132
- const pkgJson = JSON.parse(readFileSync9(pkgJsonPath, "utf-8"));
55453
+ const pkgJson = JSON.parse(readFileSync10(pkgJsonPath, "utf-8"));
55133
55454
  const extensions = pkgJson.pi?.extensions;
55134
55455
  if (!Array.isArray(extensions) || extensions.length === 0) return null;
55135
55456
  const entry = extensions[0];
55136
55457
  if (typeof entry !== "string" || entry.length === 0) return null;
55137
55458
  const path2 = resolve12(dirname9(pkgJsonPath), entry);
55138
- return existsSync21(path2) ? path2 : null;
55459
+ return existsSync22(path2) ? path2 : null;
55139
55460
  } catch {
55140
55461
  return null;
55141
55462
  }
@@ -55144,13 +55465,13 @@ function resolveVendoredDroidCliEntry() {
55144
55465
  try {
55145
55466
  const require_ = createRequire2(import.meta.url);
55146
55467
  const pkgJsonPath = require_.resolve("@fusion/droid-cli/package.json");
55147
- const pkgJson = JSON.parse(readFileSync9(pkgJsonPath, "utf-8"));
55468
+ const pkgJson = JSON.parse(readFileSync10(pkgJsonPath, "utf-8"));
55148
55469
  const extensions = pkgJson.pi?.extensions;
55149
55470
  if (!Array.isArray(extensions) || extensions.length === 0) return null;
55150
55471
  const entry = extensions[0];
55151
55472
  if (typeof entry !== "string" || entry.length === 0) return null;
55152
55473
  const path2 = resolve12(dirname9(pkgJsonPath), entry);
55153
- return existsSync21(path2) ? path2 : null;
55474
+ return existsSync22(path2) ? path2 : null;
55154
55475
  } catch {
55155
55476
  return null;
55156
55477
  }
@@ -55178,7 +55499,7 @@ async function registerExtensionProviders(cwd, modelRegistry) {
55178
55499
  const extensionsResult = await discoverAndLoadExtensions(
55179
55500
  doubleReconciledPaths,
55180
55501
  cwd,
55181
- join25(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery")
55502
+ join26(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery")
55182
55503
  );
55183
55504
  for (const { path: path2, error } of extensionsResult.errors) {
55184
55505
  extensionsLog.warn(`Failed to load ${path2}: ${error}`);
@@ -55233,10 +55554,10 @@ async function isCompleteGitWorktree(worktreePath) {
55233
55554
  }
55234
55555
  }
55235
55556
  async function assertValidWorktreeSession(cwd, projectRoot) {
55236
- if (!existsSync21(cwd)) {
55557
+ if (!existsSync22(cwd)) {
55237
55558
  throw new Error(`Refusing to start coding agent in missing worktree: ${cwd}`);
55238
55559
  }
55239
- if (!existsSync21(join25(cwd, ".git")) || !await isCompleteGitWorktree(cwd)) {
55560
+ if (!existsSync22(join26(cwd, ".git")) || !await isCompleteGitWorktree(cwd)) {
55240
55561
  throw new Error(`Refusing to start coding agent in incomplete worktree: ${cwd}`);
55241
55562
  }
55242
55563
  if (!await isRegisteredGitWorktree(projectRoot, cwd)) {
@@ -55817,7 +56138,7 @@ ${source.content ?? ""}`;
55817
56138
 
55818
56139
  // ../engine/src/research/providers/local-docs-provider.ts
55819
56140
  import { promises as fs } from "node:fs";
55820
- import { extname as extname2, join as join26, relative as relative4, resolve as resolve13 } from "node:path";
56141
+ import { extname as extname2, join as join27, relative as relative4, resolve as resolve13 } from "node:path";
55821
56142
  function buildExcerpt(content, terms) {
55822
56143
  const lower = content.toLowerCase();
55823
56144
  const first = terms.find((term) => lower.includes(term));
@@ -55946,7 +56267,7 @@ var init_local_docs_provider = __esm({
55946
56267
  const rootEntries = await fs.readdir(this.projectRoot, { withFileTypes: true });
55947
56268
  for (const entry of rootEntries) {
55948
56269
  if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
55949
- files.push(join26(this.projectRoot, entry.name));
56270
+ files.push(join27(this.projectRoot, entry.name));
55950
56271
  }
55951
56272
  }
55952
56273
  return [...new Set(files)];
@@ -55956,7 +56277,7 @@ var init_local_docs_provider = __esm({
55956
56277
  const entries = await fs.readdir(dir, { withFileTypes: true });
55957
56278
  for (const entry of entries) {
55958
56279
  this.throwIfAborted(signal);
55959
- const fullPath = join26(dir, entry.name);
56280
+ const fullPath = join27(dir, entry.name);
55960
56281
  const relPath = relative4(this.projectRoot, fullPath).replace(/\\/g, "/");
55961
56282
  if (matchesGitignore(relPath, ignorePatterns)) continue;
55962
56283
  if (entry.isDirectory()) {
@@ -55981,7 +56302,7 @@ var init_local_docs_provider = __esm({
55981
56302
  }
55982
56303
  async readGitignore() {
55983
56304
  try {
55984
- const content = await fs.readFile(join26(this.projectRoot, ".gitignore"), "utf-8");
56305
+ const content = await fs.readFile(join27(this.projectRoot, ".gitignore"), "utf-8");
55985
56306
  return content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
55986
56307
  } catch {
55987
56308
  return [];
@@ -56591,9 +56912,9 @@ var init_research_step_runner = __esm({
56591
56912
 
56592
56913
  // ../engine/src/agent-tools.ts
56593
56914
  import { appendFile as appendFile3, mkdir as mkdir11, readFile as readFile12, readdir as readdir7, stat as stat4, writeFile as writeFile10 } from "node:fs/promises";
56594
- import { existsSync as existsSync22 } from "node:fs";
56915
+ import { existsSync as existsSync23 } from "node:fs";
56595
56916
  import { createHash as createHash4 } from "node:crypto";
56596
- import { join as join27 } from "node:path";
56917
+ import { join as join28 } from "node:path";
56597
56918
  import { Type } from "@mariozechner/pi-ai";
56598
56919
  function sanitizeAgentMemoryId(agentId) {
56599
56920
  return agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
@@ -56605,16 +56926,16 @@ function agentDreamsDisplayPath(agentId) {
56605
56926
  return `${AGENT_MEMORY_ROOT2}/${sanitizeAgentMemoryId(agentId)}/${AGENT_DREAMS_FILENAME2}`;
56606
56927
  }
56607
56928
  function agentMemoryDirectory(rootDir, agentId) {
56608
- return join27(rootDir, AGENT_MEMORY_ROOT2, sanitizeAgentMemoryId(agentId));
56929
+ return join28(rootDir, AGENT_MEMORY_ROOT2, sanitizeAgentMemoryId(agentId));
56609
56930
  }
56610
56931
  function agentMemoryFilePath(rootDir, agentId) {
56611
- return join27(agentMemoryDirectory(rootDir, agentId), AGENT_MEMORY_FILENAME2);
56932
+ return join28(agentMemoryDirectory(rootDir, agentId), AGENT_MEMORY_FILENAME2);
56612
56933
  }
56613
56934
  function agentDreamsFilePath(rootDir, agentId) {
56614
- return join27(agentMemoryDirectory(rootDir, agentId), AGENT_DREAMS_FILENAME2);
56935
+ return join28(agentMemoryDirectory(rootDir, agentId), AGENT_DREAMS_FILENAME2);
56615
56936
  }
56616
56937
  function agentDailyFilePath(rootDir, agentId, date = /* @__PURE__ */ new Date()) {
56617
- return join27(agentMemoryDirectory(rootDir, agentId), `${date.toISOString().slice(0, 10)}.md`);
56938
+ return join28(agentMemoryDirectory(rootDir, agentId), `${date.toISOString().slice(0, 10)}.md`);
56618
56939
  }
56619
56940
  function qmdAgentMemoryCollectionName(rootDir, agentId) {
56620
56941
  const hash = createHash4("sha1").update(`${rootDir}:${agentId}`).digest("hex").slice(0, 12);
@@ -56650,7 +56971,7 @@ async function syncAgentMemoryFile(rootDir, agentMemory) {
56650
56971
  const dir = agentMemoryDirectory(rootDir, agentMemory.agentId);
56651
56972
  await mkdir11(dir, { recursive: true });
56652
56973
  const longTermPath = agentMemoryFilePath(rootDir, agentMemory.agentId);
56653
- if (!existsSync22(longTermPath)) {
56974
+ if (!existsSync23(longTermPath)) {
56654
56975
  const title = agentMemory.agentName?.trim() ? `# Agent Memory: ${agentMemory.agentName.trim()}` : "# Agent Memory";
56655
56976
  const fileContent = `${title}
56656
56977
 
@@ -56661,11 +56982,11 @@ ${content || ""}
56661
56982
  await writeFile10(longTermPath, fileContent, "utf-8");
56662
56983
  }
56663
56984
  const dreamsPath = agentDreamsFilePath(rootDir, agentMemory.agentId);
56664
- if (!existsSync22(dreamsPath)) {
56985
+ if (!existsSync23(dreamsPath)) {
56665
56986
  await writeFile10(dreamsPath, "# Agent Memory Dreams\n\n<!-- Synthesized patterns from this agent's daily notes. -->\n", "utf-8");
56666
56987
  }
56667
56988
  const dailyPath = agentDailyFilePath(rootDir, agentMemory.agentId);
56668
- if (!existsSync22(dailyPath)) {
56989
+ if (!existsSync23(dailyPath)) {
56669
56990
  await writeFile10(dailyPath, `# Agent Daily Memory ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
56670
56991
 
56671
56992
  <!-- Running observations for this agent. -->
@@ -56689,7 +57010,7 @@ async function listAgentMemoryFiles2(rootDir, agentMemory) {
56689
57010
  }
56690
57011
  for (const entry of entries) {
56691
57012
  if (!DAILY_AGENT_MEMORY_RE2.test(entry)) continue;
56692
- const absPath = join27(dir, entry);
57013
+ const absPath = join28(dir, entry);
56693
57014
  const fileStat = await stat4(absPath);
56694
57015
  if (fileStat.isFile()) {
56695
57016
  files.push({
@@ -56851,7 +57172,7 @@ function resolveAgentMemoryPath(rootDir, agentId, path2) {
56851
57172
  return null;
56852
57173
  }
56853
57174
  return {
56854
- absPath: join27(agentMemoryDirectory(rootDir, agentId), filename),
57175
+ absPath: join28(agentMemoryDirectory(rootDir, agentId), filename),
56855
57176
  displayPath: `${prefix}${filename}`
56856
57177
  };
56857
57178
  }
@@ -57957,9 +58278,18 @@ function normalizeAgentSkills(metadataSkills) {
57957
58278
  name = namedEntry.trim();
57958
58279
  }
57959
58280
  }
57960
- if (name && name.length > 0 && !seen.has(name)) {
57961
- seen.add(name);
57962
- result.push(name);
58281
+ if (name && name.length > 0) {
58282
+ if (name.includes("::")) {
58283
+ const idPath = name.split("::").pop();
58284
+ const parts = idPath.replace(/\\/g, "/").split("/").filter(Boolean);
58285
+ if (parts.length >= 2) {
58286
+ name = parts.slice(-2).join("/");
58287
+ }
58288
+ }
58289
+ if (!seen.has(name)) {
58290
+ seen.add(name);
58291
+ result.push(name);
58292
+ }
57963
58293
  }
57964
58294
  }
57965
58295
  return result;
@@ -59148,6 +59478,38 @@ var init_notifier = __esm({
59148
59478
  }
59149
59479
  });
59150
59480
 
59481
+ // ../engine/src/fallback-model-observer.ts
59482
+ function buildFallbackLogMessage(label, payload) {
59483
+ return `[fallback] ${label} switched from ${payload.primaryModel} to ${payload.fallbackModel} (${payload.triggerPoint})`;
59484
+ }
59485
+ function createFallbackModelObserver(options) {
59486
+ return async (payload) => {
59487
+ const taskId = options.taskId ?? payload.taskId;
59488
+ const taskTitle = options.taskTitle ?? payload.taskTitle;
59489
+ const message = buildFallbackLogMessage(options.label, payload);
59490
+ if (taskId && options.store?.logEntry) {
59491
+ await options.store.logEntry(taskId, message).catch(() => void 0);
59492
+ }
59493
+ if (taskId && options.store?.appendAgentLog) {
59494
+ await options.store.appendAgentLog(taskId, message, "text", void 0, options.agent).catch(() => void 0);
59495
+ }
59496
+ await notifyFallbackUsed({
59497
+ primaryModel: payload.primaryModel,
59498
+ fallbackModel: payload.fallbackModel,
59499
+ triggerPoint: payload.triggerPoint,
59500
+ taskId,
59501
+ taskTitle,
59502
+ timestamp: payload.timestamp
59503
+ });
59504
+ };
59505
+ }
59506
+ var init_fallback_model_observer = __esm({
59507
+ "../engine/src/fallback-model-observer.ts"() {
59508
+ "use strict";
59509
+ init_notifier();
59510
+ }
59511
+ });
59512
+
59151
59513
  // ../engine/src/reviewer.ts
59152
59514
  async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptContent, baseline, options = {}) {
59153
59515
  let liveSettings = options.settings;
@@ -59278,7 +59640,13 @@ async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptC
59278
59640
  ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
59279
59641
  taskId: options.taskId,
59280
59642
  taskTitle: options.taskTitle,
59281
- onFallbackModelUsed: notifyFallbackUsed,
59643
+ onFallbackModelUsed: createFallbackModelObserver({
59644
+ agent: "reviewer",
59645
+ label: "reviewer",
59646
+ store: options.store,
59647
+ taskId: options.taskId,
59648
+ taskTitle: options.taskTitle
59649
+ }),
59282
59650
  beforeSpawnSession: async () => {
59283
59651
  if (!options.store) return;
59284
59652
  let finalSettings;
@@ -59465,7 +59833,7 @@ var init_reviewer = __esm({
59465
59833
  init_logger2();
59466
59834
  init_usage_limit_detector();
59467
59835
  init_agent_instructions();
59468
- init_notifier();
59836
+ init_fallback_model_observer();
59469
59837
  init_agent_tools();
59470
59838
  REVIEWER_SYSTEM_PROMPT = `You are an independent code and plan reviewer.
59471
59839
 
@@ -59826,7 +60194,7 @@ var init_recovery_policy = __esm({
59826
60194
  // ../engine/src/triage.ts
59827
60195
  import { Type as Type2 } from "@mariozechner/pi-ai";
59828
60196
  import { readFile as readFile14 } from "node:fs/promises";
59829
- import { join as join28 } from "node:path";
60197
+ import { join as join29 } from "node:path";
59830
60198
  function extractPromptDeclaredTitle(prompt, taskId) {
59831
60199
  const headingMatch = prompt.match(/^#\s+Task:\s+([A-Z]+-\d+)\s+-\s+(.+)$/m);
59832
60200
  if (!headingMatch) return null;
@@ -59855,9 +60223,9 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
59855
60223
  return { attachmentContents, imageContents };
59856
60224
  }
59857
60225
  const { readFile: readFile20 } = await import("node:fs/promises");
59858
- const { join: join42 } = await import("node:path");
60226
+ const { join: join43 } = await import("node:path");
59859
60227
  for (const att of attachments) {
59860
- const filePath = join42(
60228
+ const filePath = join43(
59861
60229
  rootDir,
59862
60230
  ".fusion",
59863
60231
  "tasks",
@@ -60082,7 +60450,7 @@ var init_triage = __esm({
60082
60450
  init_concurrency();
60083
60451
  init_agent_logger();
60084
60452
  init_agent_instructions();
60085
- init_notifier();
60453
+ init_fallback_model_observer();
60086
60454
  init_logger2();
60087
60455
  init_usage_limit_detector();
60088
60456
  init_transient_error_detector();
@@ -60692,7 +61060,7 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
60692
61060
  return false;
60693
61061
  }
60694
61062
  const settings = await this.store.getSettings();
60695
- const promptPath = join28(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
61063
+ const promptPath = join29(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
60696
61064
  const written = await readFile14(promptPath, "utf-8").catch((err) => {
60697
61065
  const msg = err instanceof Error ? err.message : String(err);
60698
61066
  planLog.warn(`${task.id}: failed to read PROMPT.md during approved-spec recovery (${promptPath}): ${msg}`);
@@ -60922,7 +61290,13 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
60922
61290
  ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
60923
61291
  taskId: task.id,
60924
61292
  taskTitle: task.title,
60925
- onFallbackModelUsed: notifyFallbackUsed
61293
+ onFallbackModelUsed: createFallbackModelObserver({
61294
+ agent: "triage",
61295
+ label: "triage",
61296
+ store: this.store,
61297
+ taskId: task.id,
61298
+ taskTitle: task.title
61299
+ })
60926
61300
  });
60927
61301
  const modelDesc = describeModel(session);
60928
61302
  planLog.log(`${task.id}: using model ${modelDesc}`);
@@ -61065,7 +61439,13 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
61065
61439
  ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
61066
61440
  taskId: task.id,
61067
61441
  taskTitle: task.title,
61068
- onFallbackModelUsed: notifyFallbackUsed
61442
+ onFallbackModelUsed: createFallbackModelObserver({
61443
+ agent: "triage",
61444
+ label: "triage",
61445
+ store: this.store,
61446
+ taskId: task.id,
61447
+ taskTitle: task.title
61448
+ })
61069
61449
  });
61070
61450
  session = fallbackResult.session;
61071
61451
  const fallbackModelDesc = describeModel(session);
@@ -61150,7 +61530,7 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
61150
61530
  return;
61151
61531
  }
61152
61532
  const written = await readFile14(
61153
- join28(this.rootDir, promptPath),
61533
+ join29(this.rootDir, promptPath),
61154
61534
  "utf-8"
61155
61535
  ).catch((err) => {
61156
61536
  const msg = err instanceof Error ? err.message : String(err);
@@ -61469,9 +61849,9 @@ Remove or replace these ids and call fn_task_create again.`
61469
61849
  }
61470
61850
  try {
61471
61851
  const { readFile: readFile20 } = await import("node:fs/promises");
61472
- const { join: join42 } = await import("node:path");
61852
+ const { join: join43 } = await import("node:path");
61473
61853
  const promptContent = await readFile20(
61474
- join42(rootDir, promptPath),
61854
+ join43(rootDir, promptPath),
61475
61855
  "utf-8"
61476
61856
  ).catch((err) => {
61477
61857
  const msg = err instanceof Error ? err.message : String(err);
@@ -61699,160 +62079,8 @@ Take a completely different approach to writing this specification. Do NOT repea
61699
62079
  }
61700
62080
  });
61701
62081
 
61702
- // ../engine/src/session-token-usage.ts
61703
- var session_token_usage_exports = {};
61704
- __export(session_token_usage_exports, {
61705
- accumulateSessionTokenUsage: () => accumulateSessionTokenUsage
61706
- });
61707
- function readSessionStats(session) {
61708
- const accessor = session.getSessionStats;
61709
- if (typeof accessor !== "function") return void 0;
61710
- try {
61711
- return accessor.call(session);
61712
- } catch {
61713
- return void 0;
61714
- }
61715
- }
61716
- async function accumulateSessionTokenUsage(store, taskId, session) {
61717
- try {
61718
- const stats = readSessionStats(session);
61719
- const tokens = stats?.tokens;
61720
- if (!tokens) return;
61721
- const currentInput = (tokens.input ?? 0) + (tokens.cacheWrite ?? 0);
61722
- const currentOutput = tokens.output ?? 0;
61723
- const currentCached = tokens.cacheRead ?? 0;
61724
- const baseline = sessionBaselines.get(session) ?? { input: 0, output: 0, cached: 0 };
61725
- const inputDelta = Math.max(0, currentInput - baseline.input);
61726
- const outputDelta = Math.max(0, currentOutput - baseline.output);
61727
- const cachedDelta = Math.max(0, currentCached - baseline.cached);
61728
- sessionBaselines.set(session, {
61729
- input: currentInput,
61730
- output: currentOutput,
61731
- cached: currentCached
61732
- });
61733
- if (inputDelta === 0 && outputDelta === 0 && cachedDelta === 0) return;
61734
- const task = await store.getTask(taskId);
61735
- const now = (/* @__PURE__ */ new Date()).toISOString();
61736
- const newInput = (task.tokenUsage?.inputTokens ?? 0) + inputDelta;
61737
- const newOutput = (task.tokenUsage?.outputTokens ?? 0) + outputDelta;
61738
- const newCached = (task.tokenUsage?.cachedTokens ?? 0) + cachedDelta;
61739
- await store.updateTask(taskId, {
61740
- tokenUsage: {
61741
- inputTokens: newInput,
61742
- outputTokens: newOutput,
61743
- cachedTokens: newCached,
61744
- totalTokens: newInput + newOutput + newCached,
61745
- firstUsedAt: task.tokenUsage?.firstUsedAt ?? now,
61746
- lastUsedAt: now
61747
- }
61748
- });
61749
- } catch (err) {
61750
- const message = err instanceof Error ? err.message : String(err);
61751
- log14.warn(`${taskId}: session token usage accumulate failed: ${message}`);
61752
- }
61753
- }
61754
- var log14, sessionBaselines;
61755
- var init_session_token_usage = __esm({
61756
- "../engine/src/session-token-usage.ts"() {
61757
- "use strict";
61758
- init_logger2();
61759
- log14 = createLogger2("session-token-usage");
61760
- sessionBaselines = /* @__PURE__ */ new WeakMap();
61761
- }
61762
- });
61763
-
61764
- // ../engine/src/run-audit.ts
61765
- function createRunAuditor(store, context) {
61766
- if (!context) {
61767
- return {
61768
- git: async () => {
61769
- },
61770
- database: async () => {
61771
- },
61772
- filesystem: async () => {
61773
- }
61774
- };
61775
- }
61776
- const hasRecordAuditEvent = typeof store.recordRunAuditEvent === "function";
61777
- if (!hasRecordAuditEvent) {
61778
- return {
61779
- git: async () => {
61780
- },
61781
- database: async () => {
61782
- },
61783
- filesystem: async () => {
61784
- }
61785
- };
61786
- }
61787
- return {
61788
- git: async (input) => {
61789
- const eventInput = {
61790
- taskId: context.taskId,
61791
- agentId: context.agentId,
61792
- runId: context.runId,
61793
- domain: "git",
61794
- mutationType: input.type,
61795
- target: input.target,
61796
- metadata: {
61797
- phase: context.phase,
61798
- ...context.source ? { source: context.source } : {},
61799
- ...input.metadata
61800
- }
61801
- };
61802
- await store.recordRunAuditEvent(eventInput);
61803
- },
61804
- database: async (input) => {
61805
- const inferredTaskId = input.target.startsWith("FN-") || input.target.startsWith("KB-") ? input.target : context.taskId;
61806
- const eventInput = {
61807
- taskId: inferredTaskId,
61808
- agentId: context.agentId,
61809
- runId: context.runId,
61810
- domain: "database",
61811
- mutationType: input.type,
61812
- target: input.target,
61813
- metadata: {
61814
- phase: context.phase,
61815
- ...context.source ? { source: context.source } : {},
61816
- ...input.metadata
61817
- }
61818
- };
61819
- await store.recordRunAuditEvent(eventInput);
61820
- },
61821
- filesystem: async (input) => {
61822
- const eventInput = {
61823
- taskId: context.taskId,
61824
- agentId: context.agentId,
61825
- runId: context.runId,
61826
- domain: "filesystem",
61827
- mutationType: input.type,
61828
- target: input.target,
61829
- metadata: {
61830
- phase: context.phase,
61831
- ...context.source ? { source: context.source } : {},
61832
- ...input.metadata
61833
- }
61834
- };
61835
- await store.recordRunAuditEvent(eventInput);
61836
- }
61837
- };
61838
- }
61839
- function generateSyntheticRunId(prefix, taskId) {
61840
- const timestamp = Date.now();
61841
- const random = Math.random().toString(36).slice(2, 6);
61842
- return `${prefix}-${taskId}-${timestamp}-${random}`;
61843
- }
61844
- var init_run_audit = __esm({
61845
- "../engine/src/run-audit.ts"() {
61846
- "use strict";
61847
- }
61848
- });
61849
-
61850
- // ../engine/src/merger.ts
61851
- import { execSync, exec as exec3, spawn as spawn3 } from "node:child_process";
61852
- import { promisify as promisify4 } from "node:util";
61853
- import { existsSync as existsSync23 } from "node:fs";
61854
- import { join as join29 } from "node:path";
61855
- import { Type as Type3 } from "typebox";
62082
+ // ../engine/src/verification-utils.ts
62083
+ import { spawn as spawn3 } from "node:child_process";
61856
62084
  async function execWithProcessGroup(command, options) {
61857
62085
  return new Promise((resolve20, reject) => {
61858
62086
  if (options.signal?.aborted) {
@@ -61964,6 +62192,11 @@ function truncateWithEllipsis(text, maxChars) {
61964
62192
  return `${text.slice(0, maxChars)}
61965
62193
  ... (truncated)`;
61966
62194
  }
62195
+ function truncateOutput(output) {
62196
+ if (output.length <= VERIFICATION_LOG_MAX_CHARS) return output;
62197
+ return `... output truncated to last ${VERIFICATION_LOG_MAX_CHARS} characters ...
62198
+ ${output.slice(-VERIFICATION_LOG_MAX_CHARS)}`;
62199
+ }
61967
62200
  function summarizeVerificationOutput(output, type) {
61968
62201
  const lines = output.split("\n");
61969
62202
  let summaryLine = null;
@@ -62029,6 +62262,13 @@ function summarizeVerificationOutput(output, type) {
62029
62262
  failureNames.add(truncated);
62030
62263
  }
62031
62264
  const footer = "(full output available in engine logs)";
62265
+ if (type === "build") {
62266
+ const buildError = output.length > 500 ? `${output.slice(0, 500)}
62267
+ ... (truncated)` : output;
62268
+ return `Build output:
62269
+ ${buildError}
62270
+ ${footer}`;
62271
+ }
62032
62272
  const parts = [];
62033
62273
  if (summaryLine) {
62034
62274
  parts.push(summaryLine);
@@ -62046,29 +62286,292 @@ function summarizeVerificationOutput(output, type) {
62046
62286
  parts.push(` \u2022 ... and ${names.length - 5} more failures`);
62047
62287
  }
62048
62288
  }
62049
- if (parts.length > 0) {
62050
- parts.push(footer);
62051
- return parts.join("\n");
62052
- }
62053
- const trimmed = output.trim();
62054
- if (!trimmed) {
62055
- return `Verification command failed with no output
62289
+ if (parts.length === 0) {
62290
+ if (output.trim().length === 0) {
62291
+ return `no output
62292
+ ${footer}`;
62293
+ }
62294
+ return `${truncateOutput(output)}
62056
62295
  ${footer}`;
62057
62296
  }
62058
- if (trimmed.length <= 500) {
62059
- return `${trimmed}
62297
+ return parts.join("\n") + `
62060
62298
  ${footer}`;
62299
+ }
62300
+ async function runVerificationCommand(store, rootDir, taskId, command, type, signal, log18, agentLabel) {
62301
+ const logger2 = log18 ?? { log: console.log, error: console.error, warn: console.warn };
62302
+ const label = agentLabel ?? "merger";
62303
+ if (signal?.aborted) {
62304
+ throw Object.assign(
62305
+ new Error(`Command aborted before start: ${command}`),
62306
+ { code: "ABORT_ERR", aborted: true }
62307
+ );
62061
62308
  }
62062
- let cutoff = 500;
62063
- for (let i = 500; i < trimmed.length; i++) {
62064
- if (trimmed[i] === " " || trimmed[i] === "\n") {
62065
- cutoff = i;
62066
- break;
62309
+ logger2.log(`${taskId}: running ${type} command: ${command}`);
62310
+ await store.logEntry(taskId, `[verification] Running ${type} command: ${command}`);
62311
+ await store.appendAgentLog(taskId, `Running ${type} command`, "tool", command, label);
62312
+ const result = {
62313
+ command,
62314
+ exitCode: null,
62315
+ stdout: "",
62316
+ stderr: "",
62317
+ success: false
62318
+ };
62319
+ const verificationStartedAt = Date.now();
62320
+ try {
62321
+ const { stdout, stderr, bufferOverflow } = await execWithProcessGroup(command, {
62322
+ cwd: rootDir,
62323
+ timeout: VERIFICATION_COMMAND_TIMEOUT_MS,
62324
+ maxBuffer: VERIFICATION_COMMAND_MAX_BUFFER,
62325
+ signal
62326
+ });
62327
+ if (signal?.aborted) {
62328
+ throw Object.assign(
62329
+ new Error(`Command aborted: ${command}`),
62330
+ { code: "ABORT_ERR", aborted: true }
62331
+ );
62332
+ }
62333
+ result.stdout = stdout?.toString?.() || "";
62334
+ result.stderr = stderr?.toString?.() || "";
62335
+ result.exitCode = 0;
62336
+ result.success = true;
62337
+ const verificationDurationMs = Date.now() - verificationStartedAt;
62338
+ const timingDetail = `${verificationDurationMs}ms`;
62339
+ if (bufferOverflow) {
62340
+ logger2.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
62341
+ await store.logEntry(
62342
+ taskId,
62343
+ `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
62344
+ );
62345
+ await store.appendAgentLog(
62346
+ taskId,
62347
+ `${type} command succeeded (exit 0)`,
62348
+ "tool_result",
62349
+ timingDetail,
62350
+ label
62351
+ );
62352
+ } else {
62353
+ logger2.log(`${taskId}: ${type} command succeeded in ${verificationDurationMs}ms`);
62354
+ await store.logEntry(taskId, `[timing] [verification] ${type} command succeeded (exit 0) in ${verificationDurationMs}ms`);
62355
+ await store.appendAgentLog(
62356
+ taskId,
62357
+ `${type} command succeeded (exit 0)`,
62358
+ "tool_result",
62359
+ timingDetail,
62360
+ label
62361
+ );
62362
+ }
62363
+ return result;
62364
+ } catch (error) {
62365
+ if (signal?.aborted) {
62366
+ throw Object.assign(
62367
+ new Error(`Command aborted: ${command}`),
62368
+ { code: "ABORT_ERR", aborted: true }
62369
+ );
62370
+ }
62371
+ const verificationDurationMs = Date.now() - verificationStartedAt;
62372
+ const err = error;
62373
+ result.stdout = err?.stdout?.toString?.() || "";
62374
+ result.stderr = err?.stderr?.toString?.() || "";
62375
+ result.exitCode = typeof err?.status === "number" ? err.status : typeof err?.code === "number" ? err.code : null;
62376
+ const maxBufferExceeded = err?.code === "ENOBUFS" || err?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || String(err?.message ?? "").includes("maxBuffer");
62377
+ result.success = maxBufferExceeded && result.exitCode === 0;
62378
+ if (result.success) {
62379
+ logger2.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
62380
+ await store.logEntry(
62381
+ taskId,
62382
+ `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
62383
+ );
62384
+ await store.appendAgentLog(
62385
+ taskId,
62386
+ `${type} command succeeded (exit 0)`,
62387
+ "tool_result",
62388
+ `${verificationDurationMs}ms`,
62389
+ label
62390
+ );
62391
+ return result;
62067
62392
  }
62393
+ const output = result.stderr || result.stdout || err?.message || "Unknown error";
62394
+ const summary = summarizeVerificationOutput(output, type);
62395
+ logger2.error(`${taskId}: ${type} command failed (exit ${result.exitCode}) in ${verificationDurationMs}ms; output captured in task log`);
62396
+ await store.logEntry(
62397
+ taskId,
62398
+ `[timing] [verification] ${type} command failed (exit ${result.exitCode}) after ${verificationDurationMs}ms:
62399
+ ${summary}`
62400
+ );
62401
+ await store.appendAgentLog(
62402
+ taskId,
62403
+ `${type} command failed (exit ${result.exitCode})`,
62404
+ "tool_error",
62405
+ summary,
62406
+ label
62407
+ );
62068
62408
  }
62069
- return `${trimmed.slice(0, cutoff)}...
62070
- ${footer}`;
62409
+ return result;
62410
+ }
62411
+ var VERIFICATION_COMMAND_MAX_BUFFER, VERIFICATION_COMMAND_TIMEOUT_MS, VERIFICATION_LOG_MAX_CHARS;
62412
+ var init_verification_utils = __esm({
62413
+ "../engine/src/verification-utils.ts"() {
62414
+ "use strict";
62415
+ VERIFICATION_COMMAND_MAX_BUFFER = 50 * 1024 * 1024;
62416
+ VERIFICATION_COMMAND_TIMEOUT_MS = 6e5;
62417
+ VERIFICATION_LOG_MAX_CHARS = 2e4;
62418
+ }
62419
+ });
62420
+
62421
+ // ../engine/src/session-token-usage.ts
62422
+ var session_token_usage_exports = {};
62423
+ __export(session_token_usage_exports, {
62424
+ accumulateSessionTokenUsage: () => accumulateSessionTokenUsage
62425
+ });
62426
+ function readSessionStats(session) {
62427
+ const accessor = session.getSessionStats;
62428
+ if (typeof accessor !== "function") return void 0;
62429
+ try {
62430
+ return accessor.call(session);
62431
+ } catch {
62432
+ return void 0;
62433
+ }
62434
+ }
62435
+ async function accumulateSessionTokenUsage(store, taskId, session) {
62436
+ try {
62437
+ const stats = readSessionStats(session);
62438
+ const tokens = stats?.tokens;
62439
+ if (!tokens) return;
62440
+ const currentInput = (tokens.input ?? 0) + (tokens.cacheWrite ?? 0);
62441
+ const currentOutput = tokens.output ?? 0;
62442
+ const currentCached = tokens.cacheRead ?? 0;
62443
+ const baseline = sessionBaselines.get(session) ?? { input: 0, output: 0, cached: 0 };
62444
+ const inputDelta = Math.max(0, currentInput - baseline.input);
62445
+ const outputDelta = Math.max(0, currentOutput - baseline.output);
62446
+ const cachedDelta = Math.max(0, currentCached - baseline.cached);
62447
+ sessionBaselines.set(session, {
62448
+ input: currentInput,
62449
+ output: currentOutput,
62450
+ cached: currentCached
62451
+ });
62452
+ if (inputDelta === 0 && outputDelta === 0 && cachedDelta === 0) return;
62453
+ const task = await store.getTask(taskId);
62454
+ const now = (/* @__PURE__ */ new Date()).toISOString();
62455
+ const newInput = (task.tokenUsage?.inputTokens ?? 0) + inputDelta;
62456
+ const newOutput = (task.tokenUsage?.outputTokens ?? 0) + outputDelta;
62457
+ const newCached = (task.tokenUsage?.cachedTokens ?? 0) + cachedDelta;
62458
+ await store.updateTask(taskId, {
62459
+ tokenUsage: {
62460
+ inputTokens: newInput,
62461
+ outputTokens: newOutput,
62462
+ cachedTokens: newCached,
62463
+ totalTokens: newInput + newOutput + newCached,
62464
+ firstUsedAt: task.tokenUsage?.firstUsedAt ?? now,
62465
+ lastUsedAt: now
62466
+ }
62467
+ });
62468
+ } catch (err) {
62469
+ const message = err instanceof Error ? err.message : String(err);
62470
+ log14.warn(`${taskId}: session token usage accumulate failed: ${message}`);
62471
+ }
62472
+ }
62473
+ var log14, sessionBaselines;
62474
+ var init_session_token_usage = __esm({
62475
+ "../engine/src/session-token-usage.ts"() {
62476
+ "use strict";
62477
+ init_logger2();
62478
+ log14 = createLogger2("session-token-usage");
62479
+ sessionBaselines = /* @__PURE__ */ new WeakMap();
62480
+ }
62481
+ });
62482
+
62483
+ // ../engine/src/run-audit.ts
62484
+ function createRunAuditor(store, context) {
62485
+ if (!context) {
62486
+ return {
62487
+ git: async () => {
62488
+ },
62489
+ database: async () => {
62490
+ },
62491
+ filesystem: async () => {
62492
+ }
62493
+ };
62494
+ }
62495
+ const hasRecordAuditEvent = typeof store.recordRunAuditEvent === "function";
62496
+ if (!hasRecordAuditEvent) {
62497
+ return {
62498
+ git: async () => {
62499
+ },
62500
+ database: async () => {
62501
+ },
62502
+ filesystem: async () => {
62503
+ }
62504
+ };
62505
+ }
62506
+ return {
62507
+ git: async (input) => {
62508
+ const eventInput = {
62509
+ taskId: context.taskId,
62510
+ agentId: context.agentId,
62511
+ runId: context.runId,
62512
+ domain: "git",
62513
+ mutationType: input.type,
62514
+ target: input.target,
62515
+ metadata: {
62516
+ phase: context.phase,
62517
+ ...context.source ? { source: context.source } : {},
62518
+ ...input.metadata
62519
+ }
62520
+ };
62521
+ await store.recordRunAuditEvent(eventInput);
62522
+ },
62523
+ database: async (input) => {
62524
+ const inferredTaskId = input.target.startsWith("FN-") || input.target.startsWith("KB-") ? input.target : context.taskId;
62525
+ const eventInput = {
62526
+ taskId: inferredTaskId,
62527
+ agentId: context.agentId,
62528
+ runId: context.runId,
62529
+ domain: "database",
62530
+ mutationType: input.type,
62531
+ target: input.target,
62532
+ metadata: {
62533
+ phase: context.phase,
62534
+ ...context.source ? { source: context.source } : {},
62535
+ ...input.metadata
62536
+ }
62537
+ };
62538
+ await store.recordRunAuditEvent(eventInput);
62539
+ },
62540
+ filesystem: async (input) => {
62541
+ const eventInput = {
62542
+ taskId: context.taskId,
62543
+ agentId: context.agentId,
62544
+ runId: context.runId,
62545
+ domain: "filesystem",
62546
+ mutationType: input.type,
62547
+ target: input.target,
62548
+ metadata: {
62549
+ phase: context.phase,
62550
+ ...context.source ? { source: context.source } : {},
62551
+ ...input.metadata
62552
+ }
62553
+ };
62554
+ await store.recordRunAuditEvent(eventInput);
62555
+ }
62556
+ };
62071
62557
  }
62558
+ function generateSyntheticRunId(prefix, taskId) {
62559
+ const timestamp = Date.now();
62560
+ const random = Math.random().toString(36).slice(2, 6);
62561
+ return `${prefix}-${taskId}-${timestamp}-${random}`;
62562
+ }
62563
+ var init_run_audit = __esm({
62564
+ "../engine/src/run-audit.ts"() {
62565
+ "use strict";
62566
+ }
62567
+ });
62568
+
62569
+ // ../engine/src/merger.ts
62570
+ import { execSync, exec as exec3 } from "node:child_process";
62571
+ import { promisify as promisify4 } from "node:util";
62572
+ import { existsSync as existsSync24 } from "node:fs";
62573
+ import { join as join30 } from "node:path";
62574
+ import { Type as Type3 } from "typebox";
62072
62575
  function truncateWorkflowScriptOutput(output) {
62073
62576
  if (output.length <= WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS) return output;
62074
62577
  return `... output truncated to last ${WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS} characters ...
@@ -62112,7 +62615,7 @@ async function getStagedFiles(cwd) {
62112
62615
  }
62113
62616
  }
62114
62617
  function hasInstallState(rootDir) {
62115
- return existsSync23(join29(rootDir, "node_modules")) || existsSync23(join29(rootDir, ".pnp.cjs"));
62618
+ return existsSync24(join30(rootDir, "node_modules")) || existsSync24(join30(rootDir, ".pnp.cjs"));
62116
62619
  }
62117
62620
  function shouldSyncDependenciesForMerge(stagedFiles, installStatePresent) {
62118
62621
  if (!installStatePresent) return true;
@@ -62121,10 +62624,10 @@ function shouldSyncDependenciesForMerge(stagedFiles, installStatePresent) {
62121
62624
  );
62122
62625
  }
62123
62626
  function getDependencySyncCommand(rootDir) {
62124
- if (existsSync23(join29(rootDir, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
62125
- if (existsSync23(join29(rootDir, "package-lock.json"))) return "npm install";
62126
- if (existsSync23(join29(rootDir, "yarn.lock"))) return "yarn install --frozen-lockfile";
62127
- if (existsSync23(join29(rootDir, "bun.lock")) || existsSync23(join29(rootDir, "bun.lockb"))) {
62627
+ if (existsSync24(join30(rootDir, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
62628
+ if (existsSync24(join30(rootDir, "package-lock.json"))) return "npm install";
62629
+ if (existsSync24(join30(rootDir, "yarn.lock"))) return "yarn install --frozen-lockfile";
62630
+ if (existsSync24(join30(rootDir, "bun.lock")) || existsSync24(join30(rootDir, "bun.lockb"))) {
62128
62631
  return "bun install --frozen-lockfile";
62129
62632
  }
62130
62633
  return null;
@@ -62157,8 +62660,8 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
62157
62660
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
62158
62661
  };
62159
62662
  }
62160
- if (existsSync23(join29(rootDir, "pnpm-lock.yaml"))) {
62161
- if (existsSync23(join29(rootDir, "pnpm-workspace.yaml"))) {
62663
+ if (existsSync24(join30(rootDir, "pnpm-lock.yaml"))) {
62664
+ if (existsSync24(join30(rootDir, "pnpm-workspace.yaml"))) {
62162
62665
  mergerLog.warn(
62163
62666
  `Inferred test command "pnpm test" in a pnpm workspace (${rootDir}). This runs the full monorepo suite on every merge. Consider setting an explicit scoped testCommand in project settings, e.g. \`pnpm -r --filter "...[main]" test\`.`
62164
62667
  );
@@ -62169,21 +62672,21 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
62169
62672
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
62170
62673
  };
62171
62674
  }
62172
- if (existsSync23(join29(rootDir, "yarn.lock"))) {
62675
+ if (existsSync24(join30(rootDir, "yarn.lock"))) {
62173
62676
  return {
62174
62677
  command: "yarn test",
62175
62678
  testSource: "inferred",
62176
62679
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
62177
62680
  };
62178
62681
  }
62179
- if (existsSync23(join29(rootDir, "bun.lock")) || existsSync23(join29(rootDir, "bun.lockb"))) {
62682
+ if (existsSync24(join30(rootDir, "bun.lock")) || existsSync24(join30(rootDir, "bun.lockb"))) {
62180
62683
  return {
62181
62684
  command: "bun test",
62182
62685
  testSource: "inferred",
62183
62686
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
62184
62687
  };
62185
62688
  }
62186
- if (existsSync23(join29(rootDir, "package-lock.json"))) {
62689
+ if (existsSync24(join30(rootDir, "package-lock.json"))) {
62187
62690
  return {
62188
62691
  command: "npm test",
62189
62692
  testSource: "inferred",
@@ -62220,7 +62723,7 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
62220
62723
  await store.logEntry(taskId, deterministicVerificationMessage);
62221
62724
  await store.appendAgentLog(taskId, deterministicVerificationMessage, "text", void 0, "merger");
62222
62725
  if (hasTestCommand) {
62223
- const testResult = await runVerificationCommand(
62726
+ const testResult = await runVerificationCommand2(
62224
62727
  store,
62225
62728
  rootDir,
62226
62729
  taskId,
@@ -62251,7 +62754,7 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
62251
62754
  }
62252
62755
  }
62253
62756
  if (hasBuildCommand) {
62254
- const buildResult = await runVerificationCommand(
62757
+ const buildResult = await runVerificationCommand2(
62255
62758
  store,
62256
62759
  rootDir,
62257
62760
  taskId,
@@ -62286,98 +62789,9 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
62286
62789
  await store.appendAgentLog(taskId, "Deterministic merge verification passed", "text", void 0, "merger");
62287
62790
  return result;
62288
62791
  }
62289
- async function runVerificationCommand(store, rootDir, taskId, command, type, signal) {
62792
+ async function runVerificationCommand2(store, rootDir, taskId, command, type, signal) {
62290
62793
  throwIfAborted(signal, taskId);
62291
- mergerLog.log(`${taskId}: running ${type} command: ${command}`);
62292
- await store.logEntry(taskId, `[verification] Running ${type} command: ${command}`);
62293
- await store.appendAgentLog(taskId, `Running ${type} command`, "tool", command, "merger");
62294
- const result = {
62295
- command,
62296
- exitCode: null,
62297
- stdout: "",
62298
- stderr: "",
62299
- success: false
62300
- };
62301
- const verificationStartedAt = Date.now();
62302
- try {
62303
- const { stdout, stderr, bufferOverflow } = await execWithProcessGroup(command, {
62304
- cwd: rootDir,
62305
- timeout: VERIFICATION_COMMAND_TIMEOUT_MS,
62306
- maxBuffer: VERIFICATION_COMMAND_MAX_BUFFER,
62307
- signal
62308
- });
62309
- throwIfAborted(signal, taskId);
62310
- result.stdout = stdout?.toString?.() || "";
62311
- result.stderr = stderr?.toString?.() || "";
62312
- result.exitCode = 0;
62313
- result.success = true;
62314
- const verificationDurationMs = Date.now() - verificationStartedAt;
62315
- const timingDetail = `${verificationDurationMs}ms`;
62316
- if (bufferOverflow) {
62317
- mergerLog.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
62318
- await store.logEntry(
62319
- taskId,
62320
- `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
62321
- );
62322
- await store.appendAgentLog(
62323
- taskId,
62324
- `${type} command succeeded (exit 0)`,
62325
- "tool_result",
62326
- timingDetail,
62327
- "merger"
62328
- );
62329
- } else {
62330
- mergerLog.log(`${taskId}: ${type} command succeeded in ${verificationDurationMs}ms`);
62331
- await store.logEntry(taskId, `[timing] [verification] ${type} command succeeded (exit 0) in ${verificationDurationMs}ms`);
62332
- await store.appendAgentLog(
62333
- taskId,
62334
- `${type} command succeeded (exit 0)`,
62335
- "tool_result",
62336
- timingDetail,
62337
- "merger"
62338
- );
62339
- }
62340
- return result;
62341
- } catch (error) {
62342
- throwIfAborted(signal, taskId);
62343
- const verificationDurationMs = Date.now() - verificationStartedAt;
62344
- result.stdout = error?.stdout?.toString?.() || "";
62345
- result.stderr = error?.stderr?.toString?.() || "";
62346
- result.exitCode = typeof error?.status === "number" ? error.status : typeof error?.code === "number" ? error.code : null;
62347
- const maxBufferExceeded = error?.code === "ENOBUFS" || error?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || String(error?.message ?? "").includes("maxBuffer");
62348
- result.success = maxBufferExceeded && result.exitCode === 0;
62349
- if (result.success) {
62350
- mergerLog.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
62351
- await store.logEntry(
62352
- taskId,
62353
- `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
62354
- );
62355
- await store.appendAgentLog(
62356
- taskId,
62357
- `${type} command succeeded (exit 0)`,
62358
- "tool_result",
62359
- `${verificationDurationMs}ms`,
62360
- "merger"
62361
- );
62362
- return result;
62363
- }
62364
- const output = result.stderr || result.stdout || error?.message || "Unknown error";
62365
- const summary = summarizeVerificationOutput(output, type);
62366
- mergerLog.error(`${taskId}: ${type} command failed (exit ${result.exitCode}) in ${verificationDurationMs}ms; output captured in task log`);
62367
- await store.logEntry(
62368
- taskId,
62369
- `[timing] [verification] ${type} command failed (exit ${result.exitCode}) after ${verificationDurationMs}ms:
62370
- ${summary}`
62371
- );
62372
- await store.appendAgentLog(
62373
- taskId,
62374
- `${type} command failed (exit ${result.exitCode})`,
62375
- "tool_error",
62376
- summary,
62377
- "merger"
62378
- );
62379
- }
62380
- return result;
62794
+ return runVerificationCommand(store, rootDir, taskId, command, type, signal, mergerLog, "merger");
62381
62795
  }
62382
62796
  async function attemptInMergeVerificationFix(store, rootDir, taskId, failureContext, settings, options, mergeRunContext, fixAttemptNumber, _testCommand, _buildCommand) {
62383
62797
  try {
@@ -62440,9 +62854,20 @@ Do not refactor, rename broadly, or make opportunistic improvements.
62440
62854
  onToolEnd: logger2.onToolEnd,
62441
62855
  defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
62442
62856
  defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
62857
+ fallbackProvider: settings.fallbackProvider,
62858
+ fallbackModelId: settings.fallbackModelId,
62443
62859
  defaultThinkingLevel: settings.defaultThinkingLevel,
62444
62860
  // Skill selection: use assigned agent skills if available, otherwise role fallback
62445
- ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
62861
+ ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
62862
+ taskId,
62863
+ taskTitle: taskForSkillContext?.title,
62864
+ onFallbackModelUsed: createFallbackModelObserver({
62865
+ agent: "merger",
62866
+ label: "merge verification fix agent",
62867
+ store,
62868
+ taskId,
62869
+ taskTitle: taskForSkillContext?.title
62870
+ })
62446
62871
  });
62447
62872
  const runId = mergeRunContext?.runId;
62448
62873
  const agentId = mergeRunContext?.agentId ?? "merger";
@@ -62495,7 +62920,7 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
62495
62920
  void 0,
62496
62921
  "merger"
62497
62922
  );
62498
- const reRunResult = await runVerificationCommand(
62923
+ const reRunResult = await runVerificationCommand2(
62499
62924
  store,
62500
62925
  rootDir,
62501
62926
  taskId,
@@ -63237,9 +63662,16 @@ You are assisting with a paused \`git pull --rebase\`.
63237
63662
  onToolEnd: agentLogger.onToolEnd,
63238
63663
  defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
63239
63664
  defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
63665
+ fallbackProvider: settings.fallbackProvider,
63666
+ fallbackModelId: settings.fallbackModelId,
63240
63667
  defaultThinkingLevel: settings.defaultThinkingLevel,
63241
63668
  taskId,
63242
- onFallbackModelUsed: notifyFallbackUsed
63669
+ onFallbackModelUsed: createFallbackModelObserver({
63670
+ agent: "merger",
63671
+ label: "rebase conflict resolver",
63672
+ store,
63673
+ taskId
63674
+ })
63243
63675
  });
63244
63676
  const prompt = [
63245
63677
  `Resolve rebase conflicts for task ${taskId}.`,
@@ -63431,7 +63863,7 @@ async function pushToRemoteAfterMerge(store, rootDir, taskId, settings, options)
63431
63863
  }
63432
63864
  async function createPostMergeWorktree(rootDir, taskId) {
63433
63865
  const randomSuffix = Math.random().toString(36).slice(2, 10);
63434
- const postMergeWorktree = join29(rootDir, ".worktrees", `post-merge-${taskId}-${randomSuffix}`);
63866
+ const postMergeWorktree = join30(rootDir, ".worktrees", `post-merge-${taskId}-${randomSuffix}`);
63435
63867
  try {
63436
63868
  await execAsync2(`git worktree add ${quoteArg(postMergeWorktree)} HEAD`, { cwd: rootDir });
63437
63869
  return postMergeWorktree;
@@ -64372,7 +64804,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
64372
64804
  }
64373
64805
  }
64374
64806
  throwIfAborted(options.signal, taskId);
64375
- if (worktreePath && existsSync23(worktreePath)) {
64807
+ if (worktreePath && existsSync24(worktreePath)) {
64376
64808
  const otherUser = await findWorktreeUser(store, worktreePath, taskId);
64377
64809
  if (otherUser) {
64378
64810
  mergerLog.log(`Worktree retained \u2014 still needed by ${otherUser}`);
@@ -65026,9 +65458,20 @@ async function runAiAgentForCommit(params) {
65026
65458
  onToolEnd: agentLogger.onToolEnd,
65027
65459
  defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
65028
65460
  defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
65461
+ fallbackProvider: settings.fallbackProvider,
65462
+ fallbackModelId: settings.fallbackModelId,
65029
65463
  defaultThinkingLevel: settings.defaultThinkingLevel,
65030
65464
  // Skill selection: use assigned agent skills if available, otherwise role fallback
65031
- ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
65465
+ ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
65466
+ taskId,
65467
+ taskTitle: taskForSkillContext?.title,
65468
+ onFallbackModelUsed: createFallbackModelObserver({
65469
+ agent: "merger",
65470
+ label: "merge agent",
65471
+ store,
65472
+ taskId,
65473
+ taskTitle: taskForSkillContext?.title
65474
+ })
65032
65475
  });
65033
65476
  options.onSession?.(session);
65034
65477
  try {
@@ -65459,7 +65902,14 @@ If issues are found that need attention, describe them clearly and include concr
65459
65902
  fallbackModelId: settings.fallbackModelId,
65460
65903
  defaultThinkingLevel: settings.defaultThinkingLevel,
65461
65904
  // Skill selection: use assigned agent skills if available, otherwise role fallback
65462
- ...postMergeSkillContext?.skillSelectionContext ? { skillSelection: postMergeSkillContext.skillSelectionContext } : {}
65905
+ ...postMergeSkillContext?.skillSelectionContext ? { skillSelection: postMergeSkillContext.skillSelectionContext } : {},
65906
+ taskId,
65907
+ onFallbackModelUsed: createFallbackModelObserver({
65908
+ agent: "merger",
65909
+ label: `post-merge workflow step '${workflowStep.name}'`,
65910
+ store,
65911
+ taskId
65912
+ })
65463
65913
  });
65464
65914
  mergerLog.log(`${taskId}: [post-merge] workflow step '${workflowStep.name}' using model ${describeModel(session)}${useOverride ? " (workflow step override)" : ""}`);
65465
65915
  await store.logEntry(taskId, `[post-merge] Workflow step '${workflowStep.name}' using model: ${describeModel(session)}${useOverride ? " (workflow step override)" : ""}`);
@@ -65495,15 +65945,17 @@ async function completeTask(store, taskId, result) {
65495
65945
  result.task = task;
65496
65946
  store.emit("task:merged", result);
65497
65947
  }
65498
- var execAsync2, LOCKFILE_PATTERNS, GENERATED_PATTERNS, DEPENDENCY_SYNC_TRIGGER_PATTERNS, VERIFICATION_COMMAND_MAX_BUFFER, VERIFICATION_COMMAND_TIMEOUT_MS, VERIFICATION_LOG_MAX_CHARS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS, PULL_REBASE_TIMEOUT_MS, PUSH_TIMEOUT_MS, MERGE_COMMIT_LOG_MAX_CHARS, MERGE_DIFF_STAT_MAX_CHARS, VerificationError, MergeAbortedError, FUSION_TASK_ID_TRAILER_KEY;
65948
+ var execAsync2, LOCKFILE_PATTERNS, GENERATED_PATTERNS, DEPENDENCY_SYNC_TRIGGER_PATTERNS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS, PULL_REBASE_TIMEOUT_MS, PUSH_TIMEOUT_MS, MERGE_COMMIT_LOG_MAX_CHARS, MERGE_DIFF_STAT_MAX_CHARS, VerificationError, MergeAbortedError, FUSION_TASK_ID_TRAILER_KEY;
65499
65949
  var init_merger = __esm({
65500
65950
  "../engine/src/merger.ts"() {
65501
65951
  "use strict";
65952
+ init_verification_utils();
65953
+ init_verification_utils();
65502
65954
  init_src();
65503
65955
  init_pi();
65504
65956
  init_session_token_usage();
65505
65957
  init_agent_session_helpers();
65506
- init_notifier();
65958
+ init_fallback_model_observer();
65507
65959
  init_session_skill_context();
65508
65960
  init_agent_logger();
65509
65961
  init_logger2();
@@ -65549,9 +66001,6 @@ var init_merger = __esm({
65549
66001
  "bun.lock",
65550
66002
  "packages/*/package.json"
65551
66003
  ];
65552
- VERIFICATION_COMMAND_MAX_BUFFER = 50 * 1024 * 1024;
65553
- VERIFICATION_COMMAND_TIMEOUT_MS = 6e5;
65554
- VERIFICATION_LOG_MAX_CHARS = 2e4;
65555
66004
  WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS = 4e3;
65556
66005
  PULL_REBASE_TIMEOUT_MS = 12e4;
65557
66006
  PUSH_TIMEOUT_MS = 6e4;
@@ -65576,8 +66025,8 @@ var init_merger = __esm({
65576
66025
 
65577
66026
  // ../engine/src/worktree-names.ts
65578
66027
  import { readdirSync as readdirSync3 } from "node:fs";
65579
- import { join as join30 } from "node:path";
65580
- import { existsSync as existsSync24 } from "node:fs";
66028
+ import { join as join31 } from "node:path";
66029
+ import { existsSync as existsSync25 } from "node:fs";
65581
66030
  function slugify2(str) {
65582
66031
  return str.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
65583
66032
  }
@@ -65588,7 +66037,7 @@ function generateReservedWorktreeName(rootDir, reservedNames = /* @__PURE__ */ n
65588
66037
  const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
65589
66038
  const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
65590
66039
  const baseName = `${adjective}-${noun}`;
65591
- const worktreesDir = join30(rootDir, ".worktrees");
66040
+ const worktreesDir = join31(rootDir, ".worktrees");
65592
66041
  const existing = getExistingWorktreeNames(worktreesDir);
65593
66042
  for (const reserved of reservedNames) {
65594
66043
  existing.add(reserved);
@@ -65603,7 +66052,7 @@ function generateReservedWorktreeName(rootDir, reservedNames = /* @__PURE__ */ n
65603
66052
  return `${baseName}-${suffix}`;
65604
66053
  }
65605
66054
  function getExistingWorktreeNames(worktreesDir) {
65606
- if (!existsSync24(worktreesDir)) {
66055
+ if (!existsSync25(worktreesDir)) {
65607
66056
  return /* @__PURE__ */ new Set();
65608
66057
  }
65609
66058
  try {
@@ -65740,8 +66189,8 @@ __export(worktree_pool_exports, {
65740
66189
  });
65741
66190
  import { exec as exec4 } from "node:child_process";
65742
66191
  import { promisify as promisify5 } from "node:util";
65743
- import { existsSync as existsSync25, lstatSync, readdirSync as readdirSync4, rmSync as rmSync2 } from "node:fs";
65744
- import { join as join31, relative as relative6, resolve as resolve15, isAbsolute as isAbsolute9 } from "node:path";
66192
+ import { existsSync as existsSync26, lstatSync, readdirSync as readdirSync4, rmSync as rmSync2 } from "node:fs";
66193
+ import { join as join32, relative as relative6, resolve as resolve15, isAbsolute as isAbsolute9 } from "node:path";
65745
66194
  function getExecStdout(result) {
65746
66195
  if (typeof result === "string") return result;
65747
66196
  if (result && typeof result === "object" && "stdout" in result) {
@@ -65787,10 +66236,10 @@ async function isRegisteredGitWorktree2(rootDir, worktreePath) {
65787
66236
  return (await getRegisteredWorktreePaths(rootDir)).has(resolve15(worktreePath));
65788
66237
  }
65789
66238
  function hasRequiredWorktreeFiles(worktreePath) {
65790
- return existsSync25(join31(worktreePath, ".git")) && existsSync25(join31(worktreePath, "package.json"));
66239
+ return existsSync26(join32(worktreePath, ".git")) && existsSync26(join32(worktreePath, "package.json"));
65791
66240
  }
65792
66241
  async function isUsableTaskWorktree(rootDir, worktreePath) {
65793
- return existsSync25(worktreePath) && await isRegisteredGitWorktree2(rootDir, worktreePath) && hasRequiredWorktreeFiles(worktreePath);
66242
+ return existsSync26(worktreePath) && await isRegisteredGitWorktree2(rootDir, worktreePath) && hasRequiredWorktreeFiles(worktreePath);
65794
66243
  }
65795
66244
  function isInsideWorktreesDir(rootDir, worktreePath) {
65796
66245
  const worktreesDir = resolve15(rootDir, ".worktrees");
@@ -65799,14 +66248,14 @@ function isInsideWorktreesDir(rootDir, worktreePath) {
65799
66248
  return rel !== "" && !rel.startsWith("..") && !isAbsolute9(rel);
65800
66249
  }
65801
66250
  async function scanIdleWorktrees(rootDir, store) {
65802
- const worktreesDir = join31(rootDir, ".worktrees");
65803
- if (!existsSync25(worktreesDir)) {
66251
+ const worktreesDir = join32(rootDir, ".worktrees");
66252
+ if (!existsSync26(worktreesDir)) {
65804
66253
  return [];
65805
66254
  }
65806
66255
  let dirs;
65807
66256
  try {
65808
66257
  const entries = readdirSync4(worktreesDir, { withFileTypes: true });
65809
- dirs = entries.filter((e) => e.isDirectory()).map((e) => join31(worktreesDir, e.name));
66258
+ dirs = entries.filter((e) => e.isDirectory()).map((e) => join32(worktreesDir, e.name));
65810
66259
  } catch (err) {
65811
66260
  const errorMessage = err instanceof Error ? err.message : String(err);
65812
66261
  worktreePoolLog.warn(`Failed to read .worktrees/ directory: ${errorMessage}`);
@@ -65829,16 +66278,16 @@ async function scanIdleWorktrees(rootDir, store) {
65829
66278
  return registeredDirs.filter((dir) => !activeWorktrees.has(resolve15(dir)));
65830
66279
  }
65831
66280
  async function cleanupOrphanedWorktrees(rootDir, store) {
65832
- const worktreesDir = join31(rootDir, ".worktrees");
65833
- if (!existsSync25(worktreesDir)) {
66281
+ const worktreesDir = join32(rootDir, ".worktrees");
66282
+ if (!existsSync26(worktreesDir)) {
65834
66283
  return 0;
65835
66284
  }
65836
66285
  const orphaned = await scanIdleWorktrees(rootDir, store);
65837
66286
  const registeredWorktrees = await getRegisteredWorktreePaths(rootDir);
65838
66287
  let dirs = [];
65839
- if (existsSync25(worktreesDir)) {
66288
+ if (existsSync26(worktreesDir)) {
65840
66289
  try {
65841
- dirs = readdirSync4(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join31(worktreesDir, e.name));
66290
+ dirs = readdirSync4(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join32(worktreesDir, e.name));
65842
66291
  } catch (err) {
65843
66292
  const errorMessage = err instanceof Error ? err.message : String(err);
65844
66293
  worktreePoolLog.warn(`Failed to read .worktrees/ directory for cleanup: ${errorMessage}`);
@@ -65870,8 +66319,8 @@ async function cleanupOrphanedWorktrees(rootDir, store) {
65870
66319
  return cleaned;
65871
66320
  }
65872
66321
  async function reapOrphanWorktrees(projectRoot) {
65873
- const worktreesDir = join31(projectRoot, ".worktrees");
65874
- if (!existsSync25(worktreesDir)) {
66322
+ const worktreesDir = join32(projectRoot, ".worktrees");
66323
+ if (!existsSync26(worktreesDir)) {
65875
66324
  return 0;
65876
66325
  }
65877
66326
  let entries;
@@ -65879,11 +66328,11 @@ async function reapOrphanWorktrees(projectRoot) {
65879
66328
  entries = readdirSync4(worktreesDir, { withFileTypes: true }).filter((e) => {
65880
66329
  if (!e.isDirectory()) return false;
65881
66330
  try {
65882
- return lstatSync(join31(worktreesDir, e.name)).isDirectory() && !lstatSync(join31(worktreesDir, e.name)).isSymbolicLink();
66331
+ return lstatSync(join32(worktreesDir, e.name)).isDirectory() && !lstatSync(join32(worktreesDir, e.name)).isSymbolicLink();
65883
66332
  } catch {
65884
66333
  return false;
65885
66334
  }
65886
- }).map((e) => ({ name: e.name, fullPath: join31(worktreesDir, e.name) }));
66335
+ }).map((e) => ({ name: e.name, fullPath: join32(worktreesDir, e.name) }));
65887
66336
  } catch (err) {
65888
66337
  const msg = err instanceof Error ? err.message : String(err);
65889
66338
  worktreePoolLog.warn(`reapOrphanWorktrees: failed to read .worktrees/ \u2014 ${msg}`);
@@ -65902,8 +66351,8 @@ async function reapOrphanWorktrees(projectRoot) {
65902
66351
  if (registered.has(resolvedFull)) {
65903
66352
  continue;
65904
66353
  }
65905
- const dotGit = join31(resolvedFull, ".git");
65906
- if (existsSync25(dotGit)) {
66354
+ const dotGit = join32(resolvedFull, ".git");
66355
+ if (existsSync26(dotGit)) {
65907
66356
  worktreePoolLog.log(`reapOrphanWorktrees: skipping ${name} (has .git entry but not in registered list \u2014 may be partially registered)`);
65908
66357
  continue;
65909
66358
  }
@@ -65963,7 +66412,7 @@ var init_worktree_pool = __esm({
65963
66412
  acquire() {
65964
66413
  for (const path2 of this.idle) {
65965
66414
  this.idle.delete(path2);
65966
- if (existsSync25(path2)) {
66415
+ if (existsSync26(path2)) {
65967
66416
  return path2;
65968
66417
  }
65969
66418
  worktreePoolLog.log(`Pruned stale entry: ${path2}`);
@@ -66010,7 +66459,7 @@ var init_worktree_pool = __esm({
66010
66459
  */
66011
66460
  rehydrate(idlePaths) {
66012
66461
  for (const path2 of idlePaths) {
66013
- if (existsSync25(path2)) {
66462
+ if (existsSync26(path2)) {
66014
66463
  this.idle.add(path2);
66015
66464
  } else {
66016
66465
  worktreePoolLog.log(`Rehydrate skipped (not on disk): ${path2}`);
@@ -66066,7 +66515,7 @@ var init_worktree_pool = __esm({
66066
66515
  throw err;
66067
66516
  }
66068
66517
  const conflictingPath = match[1];
66069
- if (!existsSync25(conflictingPath)) {
66518
+ if (!existsSync26(conflictingPath)) {
66070
66519
  await execAsync3("git worktree prune", { cwd: worktreePath });
66071
66520
  await execAsync3(checkoutCmd, { cwd: worktreePath });
66072
66521
  return branchName;
@@ -66143,8 +66592,8 @@ var init_token_cap_detector = __esm({
66143
66592
  // ../engine/src/step-session-executor.ts
66144
66593
  import { exec as exec5 } from "node:child_process";
66145
66594
  import { promisify as promisify6 } from "node:util";
66146
- import { existsSync as existsSync26 } from "node:fs";
66147
- import { join as join32 } from "node:path";
66595
+ import { existsSync as existsSync27 } from "node:fs";
66596
+ import { join as join33 } from "node:path";
66148
66597
  function parseStepFileScopes(prompt) {
66149
66598
  const result = /* @__PURE__ */ new Map();
66150
66599
  if (!prompt) return result;
@@ -66431,7 +66880,7 @@ var init_step_session_executor = __esm({
66431
66880
  init_worktree_names();
66432
66881
  init_agent_logger();
66433
66882
  init_logger2();
66434
- init_notifier();
66883
+ init_fallback_model_observer();
66435
66884
  init_context_limit_detector();
66436
66885
  init_usage_limit_detector();
66437
66886
  init_agent_tools();
@@ -66548,7 +66997,7 @@ var init_step_session_executor = __esm({
66548
66997
  }
66549
66998
  for (const [stepIdx, worktreePath] of this.parallelWorktrees) {
66550
66999
  try {
66551
- if (existsSync26(worktreePath)) {
67000
+ if (existsSync27(worktreePath)) {
66552
67001
  await execAsync4(`git worktree remove "${worktreePath}" --force`, {
66553
67002
  cwd: this.options.rootDir
66554
67003
  });
@@ -66713,7 +67162,13 @@ Follow instructions precisely and avoid unrelated changes.`,
66713
67162
  ...this.options.skillSelection ? { skillSelection: this.options.skillSelection } : {},
66714
67163
  taskId: taskDetail.id,
66715
67164
  taskTitle: taskDetail.title,
66716
- onFallbackModelUsed: notifyFallbackUsed
67165
+ onFallbackModelUsed: createFallbackModelObserver({
67166
+ agent: "executor",
67167
+ label: "workflow step agent",
67168
+ store: this.store,
67169
+ taskId: taskDetail.id,
67170
+ taskTitle: taskDetail.title
67171
+ })
66717
67172
  });
66718
67173
  session = createResult.session;
66719
67174
  const handle = {
@@ -66893,7 +67348,7 @@ Follow instructions precisely and avoid unrelated changes.`,
66893
67348
  for (const [stepIdx, worktreePath] of worktreePaths) {
66894
67349
  if (worktreePath !== this.options.worktreePath) {
66895
67350
  try {
66896
- if (existsSync26(worktreePath)) {
67351
+ if (existsSync27(worktreePath)) {
66897
67352
  await execAsync4(`git worktree remove "${worktreePath}" --force`, {
66898
67353
  cwd: this.options.rootDir
66899
67354
  });
@@ -66923,7 +67378,7 @@ Follow instructions precisely and avoid unrelated changes.`,
66923
67378
  async createStepWorktree(stepIndex) {
66924
67379
  const { rootDir } = this.options;
66925
67380
  const name = generateWorktreeName(rootDir);
66926
- const worktreePath = join32(rootDir, ".worktrees", name);
67381
+ const worktreePath = join33(rootDir, ".worktrees", name);
66927
67382
  const branchName = `fusion/step-${stepIndex}-${name}`;
66928
67383
  stepExecLog.log(`Creating worktree for step ${stepIndex}: ${worktreePath} (branch: ${branchName})`);
66929
67384
  try {
@@ -66998,7 +67453,7 @@ Follow instructions precisely and avoid unrelated changes.`,
66998
67453
 
66999
67454
  // ../engine/src/spec-staleness.ts
67000
67455
  import { stat as stat5 } from "node:fs/promises";
67001
- import { join as join33 } from "node:path";
67456
+ import { join as join34 } from "node:path";
67002
67457
  async function evaluateSpecStaleness(options) {
67003
67458
  const { settings, promptPath, nowMs } = options;
67004
67459
  if (settings.specStalenessEnabled !== true) {
@@ -67038,7 +67493,7 @@ async function evaluateSpecStaleness(options) {
67038
67493
  };
67039
67494
  }
67040
67495
  function getPromptPath(tasksDir, taskId) {
67041
- return join33(tasksDir, taskId, "PROMPT.md");
67496
+ return join34(tasksDir, taskId, "PROMPT.md");
67042
67497
  }
67043
67498
  var DEFAULT_SPEC_STALENESS_MAX_AGE_MS;
67044
67499
  var init_spec_staleness = __esm({
@@ -67069,8 +67524,8 @@ var init_task_completion = __esm({
67069
67524
 
67070
67525
  // ../engine/src/run-verification-tool.ts
67071
67526
  import { spawn as spawn4 } from "node:child_process";
67072
- import { existsSync as existsSync27 } from "node:fs";
67073
- import { isAbsolute as isAbsolute10, join as join34 } from "node:path";
67527
+ import { existsSync as existsSync28 } from "node:fs";
67528
+ import { isAbsolute as isAbsolute10, join as join35 } from "node:path";
67074
67529
  import { Type as Type4 } from "@mariozechner/pi-ai";
67075
67530
  function createBuffer() {
67076
67531
  return { headChunks: [], headBytes: 0, tailChunks: [], tailBytes: 0, totalBytes: 0 };
@@ -67103,7 +67558,7 @@ function flattenBuffer(buf) {
67103
67558
 
67104
67559
  ` + tail;
67105
67560
  }
67106
- async function runVerificationCommand2(opts) {
67561
+ async function runVerificationCommand3(opts) {
67107
67562
  const { command, cwd, timeoutMs, expectFailure = false, onHeartbeat, onLine } = opts;
67108
67563
  const startMs = Date.now();
67109
67564
  const warnings = [];
@@ -67243,7 +67698,7 @@ function createRunVerificationTool(opts) {
67243
67698
  if (params.cwd && isAbsolute10(params.cwd)) {
67244
67699
  resolvedCwd = params.cwd;
67245
67700
  } else if (params.cwd) {
67246
- resolvedCwd = join34(worktreePath, params.cwd);
67701
+ resolvedCwd = join35(worktreePath, params.cwd);
67247
67702
  } else {
67248
67703
  resolvedCwd = worktreePath;
67249
67704
  }
@@ -67258,8 +67713,8 @@ function createRunVerificationTool(opts) {
67258
67713
  }
67259
67714
  let effectiveCommand = command;
67260
67715
  if (command.trimStart().startsWith("pnpm --filter")) {
67261
- const modulesYaml = join34(rootDir, "node_modules", ".modules.yaml");
67262
- if (!existsSync27(modulesYaml)) {
67716
+ const modulesYaml = join35(rootDir, "node_modules", ".modules.yaml");
67717
+ if (!existsSync28(modulesYaml)) {
67263
67718
  const installCmd = "pnpm install --prefer-offline";
67264
67719
  const msg = `node_modules/.modules.yaml not found in workspace root \u2014 auto-prepending \`${installCmd}\` before running the command.`;
67265
67720
  warnings.push(msg);
@@ -67270,7 +67725,7 @@ function createRunVerificationTool(opts) {
67270
67725
  log18.info(
67271
67726
  `[fn_run_verification] ${taskId}: scope=${scope} timeout=${timeoutSec}s cwd=${resolvedCwd} cmd=${effectiveCommand}`
67272
67727
  );
67273
- const result = await runVerificationCommand2({
67728
+ const result = await runVerificationCommand3({
67274
67729
  command: effectiveCommand,
67275
67730
  cwd: resolvedCwd,
67276
67731
  timeoutMs,
@@ -67370,8 +67825,8 @@ var init_run_verification_tool = __esm({
67370
67825
  // ../engine/src/executor.ts
67371
67826
  import { exec as exec6 } from "node:child_process";
67372
67827
  import { promisify as promisify7 } from "node:util";
67373
- import { isAbsolute as isAbsolute11, join as join35, relative as relative7, resolve as resolvePath } from "node:path";
67374
- import { existsSync as existsSync28 } from "node:fs";
67828
+ import { isAbsolute as isAbsolute11, join as join36, relative as relative7, resolve as resolvePath } from "node:path";
67829
+ import { existsSync as existsSync29 } from "node:fs";
67375
67830
  import { readFile as readFile15, writeFile as writeFile12 } from "node:fs/promises";
67376
67831
  import { Type as Type5 } from "@mariozechner/pi-ai";
67377
67832
  import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
@@ -67665,6 +68120,7 @@ var init_executor = __esm({
67665
68120
  "use strict";
67666
68121
  init_src();
67667
68122
  init_merger();
68123
+ init_verification_utils();
67668
68124
  init_worktree_names();
67669
68125
  init_pi();
67670
68126
  init_session_token_usage();
@@ -67689,7 +68145,7 @@ var init_executor = __esm({
67689
68145
  init_task_completion();
67690
68146
  init_auth_storage();
67691
68147
  init_run_verification_tool();
67692
- init_notifier();
68148
+ init_fallback_model_observer();
67693
68149
  init_agent_logger();
67694
68150
  init_agent_tools();
67695
68151
  execAsync5 = promisify7(exec6);
@@ -68456,20 +68912,26 @@ The tool prevents your session from being killed by the inactivity watchdog duri
68456
68912
  if (from !== "in-review" && from !== "done") {
68457
68913
  return task;
68458
68914
  }
68459
- const hasMergeEvidence = Boolean(task.mergeDetails) || (task.mergeRetries ?? 0) > 0 || (task.verificationFailureCount ?? 0) > 0 || task.status === "merging" || task.status === "merging-pr";
68915
+ const hasMergeEvidence = Boolean(task.mergeDetails) || (task.mergeRetries ?? 0) > 0 || (task.verificationFailureCount ?? 0) > 0 || task.status === "merging" || task.status === "merging-pr" || task.status === "merging-fix";
68460
68916
  if (!hasMergeEvidence) {
68461
68917
  return task;
68462
68918
  }
68463
68919
  return this.cleanupMergeStateForReverification(
68464
68920
  task,
68465
- `Task returned to in-progress from ${from} column \u2014 resetting verification steps and merge state for re-verification`
68921
+ `Task returned to in-progress from ${from} column \u2014 resetting verification steps and merge state for re-verification`,
68922
+ {
68923
+ // Keep deterministic merge-verification bounce budget across remediation
68924
+ // cycles. Status may be cleared by intermediate paths, so the counter is
68925
+ // the canonical signal once a bounce has started.
68926
+ preserveVerificationFailureCount: (task.verificationFailureCount ?? 0) > 0
68927
+ }
68466
68928
  );
68467
68929
  }
68468
- async cleanupMergeStateForReverification(task, logMessage) {
68930
+ async cleanupMergeStateForReverification(task, logMessage, options) {
68469
68931
  await this.store.updateTask(task.id, {
68470
68932
  mergeDetails: null,
68471
68933
  mergeRetries: 0,
68472
- verificationFailureCount: 0,
68934
+ verificationFailureCount: options?.preserveVerificationFailureCount ? task.verificationFailureCount ?? 0 : 0,
68473
68935
  workflowStepResults: []
68474
68936
  });
68475
68937
  const refreshedTask = await this.store.getTask(task.id);
@@ -68836,7 +69298,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
68836
69298
  );
68837
69299
  return false;
68838
69300
  }
68839
- if (task.worktree && existsSync28(task.worktree)) {
69301
+ if (task.worktree && existsSync29(task.worktree)) {
68840
69302
  const modifiedFiles = await this.captureModifiedFiles(task.worktree, task.baseCommitSha);
68841
69303
  if (modifiedFiles.length > 0) {
68842
69304
  await this.store.updateTask(task.id, { modifiedFiles });
@@ -69016,7 +69478,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69016
69478
  if (task.dependencies.length === 0) return null;
69017
69479
  for (const depId of task.dependencies) {
69018
69480
  const dep = allTasks.find((t) => t.id === depId);
69019
- if (dep && dep.worktree && (dep.column === "done" || dep.column === "in-review") && existsSync28(dep.worktree)) {
69481
+ if (dep && dep.worktree && (dep.column === "done" || dep.column === "in-review") && existsSync29(dep.worktree)) {
69020
69482
  return dep.worktree;
69021
69483
  }
69022
69484
  }
@@ -69064,10 +69526,10 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69064
69526
  };
69065
69527
  const audit = createRunAuditor(this.store, engineRunContext);
69066
69528
  const activeColumns = /* @__PURE__ */ new Set(["in-progress", "in-review", "done"]);
69067
- const activeMergeStatuses = /* @__PURE__ */ new Set(["merging", "merging-pr"]);
69529
+ const activeMergeStatuses = /* @__PURE__ */ new Set(["merging", "merging-pr", "merging-fix"]);
69068
69530
  const isActiveTask = activeColumns.has(task.column) || activeMergeStatuses.has(task.status ?? "");
69069
69531
  if (!isActiveTask) {
69070
- const tasksDir = join35(this.store.getFusionDir(), "tasks");
69532
+ const tasksDir = join36(this.store.getFusionDir(), "tasks");
69071
69533
  const promptPath = getPromptPath(tasksDir, task.id);
69072
69534
  const staleness = await evaluateSpecStaleness({ settings, promptPath });
69073
69535
  if (staleness.isStale) {
@@ -69114,7 +69576,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69114
69576
  worktreeName = generateWorktreeName(this.rootDir);
69115
69577
  break;
69116
69578
  }
69117
- worktreePath = join35(this.rootDir, ".worktrees", worktreeName);
69579
+ worktreePath = join36(this.rootDir, ".worktrees", worktreeName);
69118
69580
  }
69119
69581
  let stuckRequeue = null;
69120
69582
  let taskDone = false;
@@ -69138,7 +69600,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69138
69600
  );
69139
69601
  }
69140
69602
  const branchName = task.branch || `fusion/${task.id.toLowerCase()}`;
69141
- let isResume = existsSync28(worktreePath);
69603
+ let isResume = existsSync29(worktreePath);
69142
69604
  let acquiredFromPool = false;
69143
69605
  const baseBranch = task.baseBranch || null;
69144
69606
  if (task.worktree && isResume && !await isUsableTaskWorktree(this.rootDir, worktreePath)) {
@@ -69151,8 +69613,8 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69151
69613
  this.currentRunContext
69152
69614
  );
69153
69615
  await this.store.updateTask(task.id, { worktree: null, branch: null });
69154
- worktreePath = join35(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
69155
- isResume = existsSync28(worktreePath);
69616
+ worktreePath = join36(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
69617
+ isResume = existsSync29(worktreePath);
69156
69618
  }
69157
69619
  if (!isResume) {
69158
69620
  if (this.options.pool && settings.recycleWorktrees) {
@@ -69378,6 +69840,88 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69378
69840
  if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps after step-session completion")) {
69379
69841
  return;
69380
69842
  }
69843
+ if (executionMode !== "fast") {
69844
+ if (settings.testCommand?.trim() || settings.buildCommand?.trim()) {
69845
+ const verificationResult = await this.runExecutorDeterministicVerification(task, worktreePath, settings);
69846
+ if (!verificationResult.allPassed) {
69847
+ const failedType = verificationResult.failedCommand === "testCommand" ? "test" : "build";
69848
+ const failedResult = failedType === "test" ? verificationResult.testResult : verificationResult.buildResult;
69849
+ const failedCommand = failedResult.command;
69850
+ const failureOutput = failedResult.stderr || failedResult.stdout || "Unknown error";
69851
+ const summary = summarizeVerificationOutput(failureOutput, failedType);
69852
+ executorLog.log(`${task.id}: [verification] ${failedType} failed \u2014 attempting fix agent`);
69853
+ await this.store.logEntry(
69854
+ task.id,
69855
+ `[verification] ${failedType} command failed (exit ${failedResult.exitCode}). Attempting fix agent...`,
69856
+ summary,
69857
+ this.currentRunContext
69858
+ );
69859
+ const maxFixRetries = Math.min(settings.verificationFixRetries ?? 3, 3);
69860
+ if (maxFixRetries === 0) {
69861
+ executorLog.log(`${task.id}: [verification] fix retries set to 0 \u2014 sending task back immediately`);
69862
+ await this.sendTaskBackForFix(
69863
+ task,
69864
+ worktreePath,
69865
+ `${failedType} command \`${failedCommand}\` failed (exit ${failedResult.exitCode}):
69866
+ ${summary}`,
69867
+ `Verification (${failedType})`,
69868
+ `Deterministic verification failed (${failedType})`,
69869
+ true,
69870
+ true
69871
+ );
69872
+ return;
69873
+ }
69874
+ let fixSucceeded = false;
69875
+ for (let attempt = 1; attempt <= maxFixRetries; attempt++) {
69876
+ const fixed = await this.attemptExecutorVerificationFix(
69877
+ task,
69878
+ worktreePath,
69879
+ {
69880
+ command: failedCommand,
69881
+ exitCode: failedResult.exitCode,
69882
+ output: failureOutput,
69883
+ type: failedType
69884
+ },
69885
+ settings,
69886
+ attempt,
69887
+ maxFixRetries
69888
+ );
69889
+ if (fixed) {
69890
+ fixSucceeded = true;
69891
+ executorLog.log(`${task.id}: [verification] fix agent succeeded on attempt ${attempt}/${maxFixRetries}`);
69892
+ await this.store.logEntry(
69893
+ task.id,
69894
+ `[verification] Fix agent succeeded on attempt ${attempt}/${maxFixRetries}. Verification now passing.`,
69895
+ void 0,
69896
+ this.currentRunContext
69897
+ );
69898
+ break;
69899
+ }
69900
+ executorLog.log(`${task.id}: [verification] fix agent attempt ${attempt}/${maxFixRetries} failed`);
69901
+ await this.store.logEntry(
69902
+ task.id,
69903
+ `[verification] Fix agent attempt ${attempt}/${maxFixRetries} failed`,
69904
+ void 0,
69905
+ this.currentRunContext
69906
+ );
69907
+ }
69908
+ if (!fixSucceeded) {
69909
+ executorLog.log(`${task.id}: [verification] all fix attempts exhausted (${maxFixRetries}/${maxFixRetries}) \u2014 sending task back`);
69910
+ await this.sendTaskBackForFix(
69911
+ task,
69912
+ worktreePath,
69913
+ `${failedType} command \`${failedCommand}\` failed (exit ${failedResult.exitCode}) after ${maxFixRetries} fix attempts:
69914
+ ${summary}`,
69915
+ `Verification (${failedType})`,
69916
+ `Deterministic verification failed after ${maxFixRetries} fix attempts`,
69917
+ true,
69918
+ true
69919
+ );
69920
+ return;
69921
+ }
69922
+ }
69923
+ }
69924
+ }
69381
69925
  if (executionMode !== "fast") {
69382
69926
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
69383
69927
  if (workflowResult === "deferred-paused") {
@@ -69466,7 +70010,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69466
70010
  executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}: ${errorMessage}`);
69467
70011
  await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}): ${errorMessage}`, void 0, this.currentRunContext);
69468
70012
  }
69469
- if (worktreePath && existsSync28(worktreePath)) {
70013
+ if (worktreePath && existsSync29(worktreePath)) {
69470
70014
  try {
69471
70015
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
69472
70016
  await audit.git({ type: "worktree:remove", target: worktreePath });
@@ -69525,7 +70069,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69525
70069
  try {
69526
70070
  const latestTask = await this.store.getTask(task.id);
69527
70071
  await this.resetStepsIfWorkLost(latestTask);
69528
- if (worktreePath && existsSync28(worktreePath)) {
70072
+ if (worktreePath && existsSync29(worktreePath)) {
69529
70073
  try {
69530
70074
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
69531
70075
  } catch (wtErr) {
@@ -69637,7 +70181,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69637
70181
  const executorFallbackProvider = settings.fallbackProvider;
69638
70182
  const executorFallbackModelId = settings.fallbackModelId;
69639
70183
  const executorThinkingLevel = detail.thinkingLevel ?? settings.defaultThinkingLevel;
69640
- const isResuming = !!task.sessionFile && existsSync28(task.sessionFile);
70184
+ const isResuming = !!task.sessionFile && existsSync29(task.sessionFile);
69641
70185
  const sessionManager = isResuming ? SessionManager2.open(task.sessionFile) : SessionManager2.create(worktreePath);
69642
70186
  executorLog.log(`${task.id}: creating agent session (provider=${executorProvider ?? "default"}, model=${executorModelId ?? "default"}, resuming=${isResuming})`);
69643
70187
  const executorInstructions = await this.resolveInstructionsForRole("executor");
@@ -69667,7 +70211,13 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69667
70211
  ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
69668
70212
  taskId: task.id,
69669
70213
  taskTitle: detail.title,
69670
- onFallbackModelUsed: notifyFallbackUsed
70214
+ onFallbackModelUsed: createFallbackModelObserver({
70215
+ agent: "executor",
70216
+ label: "executor",
70217
+ store: this.store,
70218
+ taskId: task.id,
70219
+ taskTitle: detail.title
70220
+ })
69671
70221
  });
69672
70222
  if (isResuming) {
69673
70223
  executorLog.log(`${task.id}: resumed session from ${task.sessionFile}`);
@@ -70101,7 +70651,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70101
70651
  this.options.onComplete?.(task);
70102
70652
  } else {
70103
70653
  executorLog.log(`${task.id} paused \u2014 moving to todo`);
70104
- if (worktreePath && existsSync28(worktreePath)) {
70654
+ if (worktreePath && existsSync29(worktreePath)) {
70105
70655
  try {
70106
70656
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
70107
70657
  executorLog.log(`Removed old worktree for paused task: ${worktreePath}`);
@@ -70197,7 +70747,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70197
70747
  executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}: ${errorMessage}`);
70198
70748
  await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}): ${errorMessage}`, void 0, this.currentRunContext);
70199
70749
  }
70200
- if (worktreePath && existsSync28(worktreePath)) {
70750
+ if (worktreePath && existsSync29(worktreePath)) {
70201
70751
  try {
70202
70752
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
70203
70753
  executorLog.log(`Removed old worktree for transient retry: ${worktreePath}`);
@@ -70252,7 +70802,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70252
70802
  try {
70253
70803
  const latestTask = await this.store.getTask(task.id);
70254
70804
  await this.resetStepsIfWorkLost(latestTask);
70255
- if (worktreePath && existsSync28(worktreePath)) {
70805
+ if (worktreePath && existsSync29(worktreePath)) {
70256
70806
  try {
70257
70807
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
70258
70808
  executorLog.log(`Removed old worktree for stuck-killed retry: ${worktreePath}`);
@@ -70757,7 +71307,7 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
70757
71307
  * The section is replaced entirely to avoid accumulation of old feedback.
70758
71308
  */
70759
71309
  async injectWorkflowRevisionInstructions(task, feedback) {
70760
- const promptPath = join35(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
71310
+ const promptPath = join36(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
70761
71311
  let content;
70762
71312
  try {
70763
71313
  content = await readFile15(promptPath, "utf-8");
@@ -70811,6 +71361,217 @@ ${feedback}
70811
71361
  *
70812
71362
  * @returns true if a retry was scheduled, false if retries are exhausted
70813
71363
  */
71364
+ /**
71365
+ * Run deterministic verification (test + build commands) in the task's worktree.
71366
+ * Returns a structured result indicating whether all commands passed.
71367
+ */
71368
+ async runExecutorDeterministicVerification(task, worktreePath, settings) {
71369
+ const testCommand = settings.testCommand?.trim();
71370
+ const buildCommand2 = settings.buildCommand?.trim();
71371
+ if (!testCommand && !buildCommand2) {
71372
+ executorLog.log(`${task.id}: no test/build commands configured \u2014 skipping verification`);
71373
+ return { allPassed: true };
71374
+ }
71375
+ const parts = [];
71376
+ if (testCommand) parts.push(`test: ${testCommand}`);
71377
+ if (buildCommand2) parts.push(`build: ${buildCommand2}`);
71378
+ executorLog.log(`${task.id}: [verification] running deterministic verification (${parts.join(", ")})`);
71379
+ await this.store.logEntry(
71380
+ task.id,
71381
+ `[verification] Running deterministic verification (${parts.join(", ")})`,
71382
+ void 0,
71383
+ this.currentRunContext
71384
+ );
71385
+ const result = { allPassed: true };
71386
+ if (testCommand) {
71387
+ const testResult = await runVerificationCommand(
71388
+ this.store,
71389
+ worktreePath,
71390
+ task.id,
71391
+ testCommand,
71392
+ "test",
71393
+ void 0,
71394
+ executorLog,
71395
+ "executor"
71396
+ );
71397
+ result.testResult = testResult;
71398
+ if (!testResult.success) {
71399
+ result.allPassed = false;
71400
+ result.failedCommand = "testCommand";
71401
+ executorLog.log(`${task.id}: [verification] test failed (exit ${testResult.exitCode})`);
71402
+ return result;
71403
+ }
71404
+ }
71405
+ if (buildCommand2) {
71406
+ const buildResult = await runVerificationCommand(
71407
+ this.store,
71408
+ worktreePath,
71409
+ task.id,
71410
+ buildCommand2,
71411
+ "build",
71412
+ void 0,
71413
+ executorLog,
71414
+ "executor"
71415
+ );
71416
+ result.buildResult = buildResult;
71417
+ if (!buildResult.success) {
71418
+ result.allPassed = false;
71419
+ result.failedCommand = "buildCommand";
71420
+ executorLog.log(`${task.id}: [verification] build failed (exit ${buildResult.exitCode})`);
71421
+ return result;
71422
+ }
71423
+ }
71424
+ executorLog.log(`${task.id}: [verification] passed`);
71425
+ await this.store.logEntry(
71426
+ task.id,
71427
+ `[verification] Deterministic verification passed`,
71428
+ void 0,
71429
+ this.currentRunContext
71430
+ );
71431
+ return result;
71432
+ }
71433
+ /**
71434
+ * Attempt to fix verification failures by spawning a dedicated AI fix agent.
71435
+ * Follows the pattern established by the merger's attemptInMergeVerificationFix.
71436
+ * Returns true if verification passes after the fix attempt, false otherwise.
71437
+ */
71438
+ async attemptExecutorVerificationFix(task, worktreePath, failureContext, settings, retryNumber, maxRetries) {
71439
+ try {
71440
+ executorLog.log(`${task.id}: spawning executor verification fix agent (attempt ${retryNumber}/${maxRetries})`);
71441
+ const logger2 = new AgentLogger({
71442
+ store: this.store,
71443
+ taskId: task.id,
71444
+ agent: "executor",
71445
+ persistAgentToolOutput: settings.persistAgentToolOutput,
71446
+ onAgentText: this.options.onAgentText,
71447
+ onAgentTool: this.options.onAgentTool
71448
+ });
71449
+ let skillContext;
71450
+ if (this.options.agentStore) {
71451
+ try {
71452
+ skillContext = await buildSessionSkillContext({
71453
+ agentStore: this.options.agentStore,
71454
+ task,
71455
+ sessionPurpose: "executor",
71456
+ projectRootDir: worktreePath,
71457
+ pluginRunner: this.options.pluginRunner
71458
+ });
71459
+ } catch {
71460
+ }
71461
+ }
71462
+ const { provider: executorProvider, modelId: executorModelId } = resolveExecutorModelPair2(
71463
+ task.modelProvider,
71464
+ task.modelId,
71465
+ settings
71466
+ );
71467
+ const { session } = await createResolvedAgentSession({
71468
+ sessionPurpose: "executor",
71469
+ pluginRunner: this.options.pluginRunner,
71470
+ cwd: worktreePath,
71471
+ // Run in the task's worktree
71472
+ systemPrompt: `You are a verification fix agent running during task execution in a worktree.
71473
+
71474
+ All step-session steps completed successfully but the deterministic verification command failed. Your job is to fix the failing code directly in the working directory.
71475
+
71476
+ ## Scope
71477
+ Only fix what is required to make the failing verification pass.
71478
+ Do not refactor, rename broadly, or make opportunistic improvements.
71479
+
71480
+ ## Rules
71481
+ 1. Read the error output carefully to understand what is failing before editing anything
71482
+ 2. Before assuming a code fix is needed, check whether the failure is caused by stale/missing build artifacts in a sibling workspace package \u2014 typical signatures: \`Failed to resolve import "./X.js"\` pointing into another package's \`dist/\`, \`Cannot find module\`, or \`ERR_MODULE_NOT_FOUND\` referencing a workspace-internal path. In that case, rebuild the affected package(s) (e.g. \`pnpm --filter <pkg> build\`, or \`pnpm --filter "<scope>/*" build\` for a group) and re-run verification before editing source files.
71483
+ 3. Make targeted fixes to the failing code path
71484
+ 4. After fixing, run the verification command to confirm the fix works
71485
+ 5. Do NOT make any git commits \u2014 just fix the code
71486
+ 6. You MAY modify any files needed to make the verification pass, including files unrelated to this task's original change. Pre-existing build/test breakage is in scope: fix it. Prefer the smallest change that makes verification green.
71487
+ 7. If you cannot fix the issue within scope, explain why and what evidence indicates a deeper/root problem`,
71488
+ tools: "coding",
71489
+ onText: logger2.onText,
71490
+ onThinking: logger2.onThinking,
71491
+ onToolStart: logger2.onToolStart,
71492
+ onToolEnd: logger2.onToolEnd,
71493
+ defaultProvider: executorProvider,
71494
+ defaultModelId: executorModelId,
71495
+ defaultThinkingLevel: settings.defaultThinkingLevel,
71496
+ ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
71497
+ });
71498
+ await this.store.logEntry(
71499
+ task.id,
71500
+ `Executor verification fix agent started (model: ${describeModel(session)}, attempt ${retryNumber}/${maxRetries})`,
71501
+ void 0,
71502
+ this.currentRunContext
71503
+ );
71504
+ await this.store.appendAgentLog(
71505
+ task.id,
71506
+ `Fix agent started (model: ${describeModel(session)}, attempt ${retryNumber}/${maxRetries})`,
71507
+ "text",
71508
+ void 0,
71509
+ "executor"
71510
+ );
71511
+ try {
71512
+ const fixPrompt = `Fix the failing ${failureContext.type} verification for task ${task.id}.
71513
+
71514
+ ## Failed command
71515
+ Command: \`${failureContext.command}\`
71516
+ Exit code: ${failureContext.exitCode}
71517
+
71518
+ ## Error output
71519
+ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
71520
+
71521
+ ## Instructions
71522
+ 1. Read the error output and identify the root cause
71523
+ 2. Make targeted fixes to resolve the failure
71524
+ 3. Run the verification command \`${failureContext.command}\` to confirm your fix works
71525
+ 4. If the fix doesn't work, try a different approach
71526
+ 5. Do NOT make any git commits`;
71527
+ await withRateLimitRetry(async () => {
71528
+ await promptWithFallback(session, fixPrompt);
71529
+ }, {
71530
+ onRetry: (attempt, delayMs, error) => {
71531
+ const delaySec = Math.round(delayMs / 1e3);
71532
+ executorLog.warn(`\u23F3 ${task.id} executor fix agent rate limited \u2014 retry ${attempt} in ${delaySec}s: ${error.message}`);
71533
+ }
71534
+ });
71535
+ await accumulateSessionTokenUsage(this.store, task.id, session);
71536
+ executorLog.log(`${task.id}: re-running deterministic verification after fix attempt ${retryNumber}/${maxRetries}`);
71537
+ await this.store.logEntry(
71538
+ task.id,
71539
+ `Re-running deterministic verification (attempt ${retryNumber}/${maxRetries})`,
71540
+ void 0,
71541
+ this.currentRunContext
71542
+ );
71543
+ await this.store.appendAgentLog(
71544
+ task.id,
71545
+ `Re-running verification (attempt ${retryNumber}/${maxRetries})`,
71546
+ "text",
71547
+ void 0,
71548
+ "executor"
71549
+ );
71550
+ const reRunResult = await this.runExecutorDeterministicVerification(task, worktreePath, settings);
71551
+ return reRunResult.allPassed;
71552
+ } finally {
71553
+ await logger2.flush();
71554
+ await session.dispose();
71555
+ }
71556
+ } catch (err) {
71557
+ const errorMessage = err instanceof Error ? err.message : String(err);
71558
+ executorLog.warn(`${task.id}: executor verification fix agent error: ${errorMessage}`);
71559
+ await this.store.logEntry(
71560
+ task.id,
71561
+ `Executor verification fix agent encountered an error`,
71562
+ errorMessage,
71563
+ this.currentRunContext
71564
+ );
71565
+ await this.store.appendAgentLog(
71566
+ task.id,
71567
+ "Fix agent encountered an error",
71568
+ "tool_error",
71569
+ errorMessage,
71570
+ "executor"
71571
+ );
71572
+ return false;
71573
+ }
71574
+ }
70814
71575
  async handleWorkflowStepFailure(task, worktreePath, failureFeedback, stepName) {
70815
71576
  this.clearCompletedTaskWatchdog(task.id);
70816
71577
  const currentRetries = task.workflowStepRetries ?? 0;
@@ -70843,7 +71604,7 @@ ${feedback}
70843
71604
  * Injects failure feedback into PROMPT.md, resets steps, clears session,
70844
71605
  * and schedules a move to todo → in-progress after the executing guard clears.
70845
71606
  */
70846
- async sendTaskBackForFix(task, worktreePath, failureFeedback, stepName, reason, preserveResumeState = true) {
71607
+ async sendTaskBackForFix(task, worktreePath, failureFeedback, stepName, reason, preserveResumeState = true, mergeVerificationFailure = false) {
70847
71608
  const taskId = task.id;
70848
71609
  this.clearCompletedTaskWatchdog(taskId);
70849
71610
  await this.store.addTaskComment(
@@ -70862,7 +71623,7 @@ Please fix the issues so the verification can pass on the next attempt.`,
70862
71623
  const updatedTask = await this.store.getTask(taskId);
70863
71624
  await this.reopenLastStepForRevision(taskId, updatedTask);
70864
71625
  await this.store.updateTask(taskId, {
70865
- status: null,
71626
+ status: mergeVerificationFailure ? "merging-fix" : null,
70866
71627
  error: null,
70867
71628
  sessionFile: null,
70868
71629
  workflowStepRetries: 0
@@ -70880,7 +71641,7 @@ Please fix the issues so the verification can pass on the next attempt.`,
70880
71641
  * The section is replaced entirely to avoid accumulation of old feedback.
70881
71642
  */
70882
71643
  async injectWorkflowStepFailureInstructions(task, failureFeedback, stepName, retryCount) {
70883
- const promptPath = join35(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
71644
+ const promptPath = join36(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
70884
71645
  let content;
70885
71646
  try {
70886
71647
  content = await readFile15(promptPath, "utf-8");
@@ -70945,31 +71706,33 @@ ${failureFeedback}
70945
71706
  * Uses git diff against the stored baseCommitSha to determine what changed.
70946
71707
  * Returns an empty array if no changes or if git commands fail.
70947
71708
  */
71709
+ async resolveDiffBaseRef(worktreePath, baseCommitSha) {
71710
+ if (baseCommitSha) return baseCommitSha;
71711
+ try {
71712
+ const { stdout } = await execAsync5(
71713
+ "git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main",
71714
+ { cwd: worktreePath, encoding: "utf-8" }
71715
+ );
71716
+ const ref = stdout.trim();
71717
+ if (ref) return ref;
71718
+ } catch (mergeBaseErr) {
71719
+ const mergeBaseMsg = mergeBaseErr instanceof Error ? mergeBaseErr.message : String(mergeBaseErr);
71720
+ executorLog.warn(`Failed merge-base lookup for diff base in ${worktreePath}, trying HEAD~1 fallback: ${mergeBaseMsg}`);
71721
+ }
71722
+ try {
71723
+ const { stdout } = await execAsync5("git rev-parse HEAD~1", {
71724
+ cwd: worktreePath,
71725
+ encoding: "utf-8"
71726
+ });
71727
+ return stdout.trim() || void 0;
71728
+ } catch {
71729
+ executorLog.log(`Could not determine base commit for diff in ${worktreePath}`);
71730
+ return void 0;
71731
+ }
71732
+ }
70948
71733
  async captureModifiedFiles(worktreePath, baseCommitSha) {
70949
71734
  try {
70950
- let baseRef = baseCommitSha;
70951
- if (!baseRef) {
70952
- try {
70953
- const { stdout: stdout2 } = await execAsync5("git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main", {
70954
- cwd: worktreePath,
70955
- encoding: "utf-8"
70956
- });
70957
- baseRef = stdout2.trim();
70958
- } catch (mergeBaseErr) {
70959
- const mergeBaseMsg = mergeBaseErr instanceof Error ? mergeBaseErr.message : String(mergeBaseErr);
70960
- executorLog.warn(`Failed merge-base lookup for diff base in ${worktreePath}, trying HEAD~1 fallback: ${mergeBaseMsg}`);
70961
- try {
70962
- const { stdout: stdout2 } = await execAsync5("git rev-parse HEAD~1", {
70963
- cwd: worktreePath,
70964
- encoding: "utf-8"
70965
- });
70966
- baseRef = stdout2.trim();
70967
- } catch {
70968
- executorLog.log(`Could not determine base commit for diff in ${worktreePath}`);
70969
- return [];
70970
- }
70971
- }
70972
- }
71735
+ const baseRef = await this.resolveDiffBaseRef(worktreePath, baseCommitSha);
70973
71736
  if (!baseRef) {
70974
71737
  return [];
70975
71738
  }
@@ -71205,6 +71968,30 @@ ${failureFeedback}
71205
71968
  */
71206
71969
  async executeWorkflowStep(task, workflowStep, worktreePath, settings) {
71207
71970
  const toolMode = workflowStep.toolMode || "readonly";
71971
+ const scopedFiles = await this.captureModifiedFiles(worktreePath, task.baseCommitSha);
71972
+ let diffShortstat;
71973
+ try {
71974
+ const baseRef = await this.resolveDiffBaseRef(worktreePath, task.baseCommitSha);
71975
+ if (baseRef) {
71976
+ const { stdout } = await execAsync5(`git diff --shortstat ${baseRef}..HEAD`, {
71977
+ cwd: worktreePath,
71978
+ encoding: "utf-8"
71979
+ });
71980
+ diffShortstat = stdout.trim() || void 0;
71981
+ }
71982
+ } catch {
71983
+ }
71984
+ const MAX_SCOPE_FILES = 100;
71985
+ const scopeFileBlock = scopedFiles.length === 0 ? "(no modified files detected for this task \u2014 review the worktree directly, but do NOT browse unrelated files)" : scopedFiles.length > MAX_SCOPE_FILES ? `${scopedFiles.slice(0, MAX_SCOPE_FILES).map((f) => `- ${f}`).join("\n")}
71986
+ - ... (${scopedFiles.length - MAX_SCOPE_FILES} more files truncated)` : scopedFiles.map((f) => `- ${f}`).join("\n");
71987
+ const scopeBlock = `Diff Scope (files changed by THIS task vs base):
71988
+ ${scopeFileBlock}${diffShortstat ? `
71989
+ Diff stat: ${diffShortstat}` : ""}
71990
+
71991
+ CRITICAL SCOPING RULES \u2014 read before doing anything else:
71992
+ - Review ONLY the files listed above. Do NOT analyze unmodified files or unrelated parts of the codebase.
71993
+ - If NONE of the files in the diff scope are relevant to your review category (e.g. a UX/design reviewer with no UI/CSS/component files in scope, a security reviewer with no auth/network code in scope, an a11y reviewer with no markup changes), respond IMMEDIATELY with a single short approval line such as "No relevant changes in scope \u2014 approved." and STOP. Do not start exploring the codebase.
71994
+ - Your wall-clock budget is short. Spending it browsing unmodified files will cause this step to time out and block merge.`;
71208
71995
  const systemPrompt = `You are a workflow step agent executing: ${workflowStep.name}
71209
71996
 
71210
71997
  Task Context:
@@ -71212,6 +71999,8 @@ Task Context:
71212
71999
  - Task Description: ${task.description}
71213
72000
  - Worktree: ${worktreePath}
71214
72001
 
72002
+ ${scopeBlock}
72003
+
71215
72004
  Your role:
71216
72005
  - Execute this workflow step exactly as scoped.
71217
72006
  - Prioritize high-impact correctness/risk findings over stylistic nits.
@@ -71680,7 +72469,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
71680
72469
  * rather than fail the task permanently.
71681
72470
  */
71682
72471
  async resolveWorktreeStartPoint(startPoint, taskId) {
71683
- const command = isAbsolute11(startPoint) && existsSync28(startPoint) ? `git -C "${startPoint}" rev-parse --verify HEAD^{commit}` : `git rev-parse --verify "${startPoint}^{commit}"`;
72472
+ const command = isAbsolute11(startPoint) && existsSync29(startPoint) ? `git -C "${startPoint}" rev-parse --verify HEAD^{commit}` : `git rev-parse --verify "${startPoint}^{commit}"`;
71684
72473
  try {
71685
72474
  const { stdout } = await execAsync5(command, { cwd: this.rootDir });
71686
72475
  return stdout.trim() || startPoint;
@@ -71700,7 +72489,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
71700
72489
  */
71701
72490
  async tryCreateWorktree(branch, path2, taskId, startPoint, attemptNumber = 0, recoveryDepth = 0) {
71702
72491
  await this.assertWorktreePathNotNested(path2, taskId);
71703
- if (existsSync28(path2)) {
72492
+ if (existsSync29(path2)) {
71704
72493
  const isRegistered = await this.isRegisteredWorktree(path2);
71705
72494
  if (!isRegistered) {
71706
72495
  await this.store.logEntry(
@@ -71851,7 +72640,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
71851
72640
  );
71852
72641
  if (shouldGenerateNewName) {
71853
72642
  const conflictStartPoint = branch;
71854
- const newPath = join35(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
72643
+ const newPath = join36(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
71855
72644
  for (let suffix = 2; suffix <= 6; suffix++) {
71856
72645
  const suffixedBranch = `${branch}-${suffix}`;
71857
72646
  try {
@@ -72451,7 +73240,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
72451
73240
  metadata: { type: "spawned", parentTaskId: taskId }
72452
73241
  });
72453
73242
  const childWorktreeName = generateWorktreeName(this.rootDir);
72454
- const childWorktreePath = join35(this.rootDir, ".worktrees", childWorktreeName);
73243
+ const childWorktreePath = join36(this.rootDir, ".worktrees", childWorktreeName);
72455
73244
  const childBranch = `fusion/spawn-${agent.id}`;
72456
73245
  await this.createWorktree(childBranch, childWorktreePath, taskId, worktreePath);
72457
73246
  await this.options.agentStore.updateAgentState(agent.id, "active");
@@ -72640,9 +73429,9 @@ var init_node_routing_policy = __esm({
72640
73429
  });
72641
73430
 
72642
73431
  // ../engine/src/scheduler.ts
72643
- import { existsSync as existsSync29 } from "node:fs";
73432
+ import { existsSync as existsSync30 } from "node:fs";
72644
73433
  import { readFile as readFile16 } from "node:fs/promises";
72645
- import { basename as basename8, join as join36 } from "node:path";
73434
+ import { basename as basename8, join as join37 } from "node:path";
72646
73435
  function pathsOverlap2(a, b) {
72647
73436
  for (const pa of a) {
72648
73437
  const prefixA = pa.endsWith("/*") ? pa.slice(0, -1) : null;
@@ -72812,12 +73601,12 @@ var init_scheduler = __esm({
72812
73601
  * @returns Object with `valid: true` if checks pass, or `valid: false` with a `reason` string if they fail
72813
73602
  */
72814
73603
  async validateTaskFilesystem(id) {
72815
- const taskDir = join36(this.store.getTasksDir(), id);
72816
- if (!existsSync29(taskDir)) {
73604
+ const taskDir = join37(this.store.getTasksDir(), id);
73605
+ if (!existsSync30(taskDir)) {
72817
73606
  return { valid: false, reason: "missing directory" };
72818
73607
  }
72819
- const promptPath = join36(taskDir, "PROMPT.md");
72820
- if (!existsSync29(promptPath)) {
73608
+ const promptPath = join37(taskDir, "PROMPT.md");
73609
+ if (!existsSync30(promptPath)) {
72821
73610
  return { valid: false, reason: "missing or empty PROMPT.md" };
72822
73611
  }
72823
73612
  try {
@@ -72956,7 +73745,7 @@ var init_scheduler = __esm({
72956
73745
  break;
72957
73746
  }
72958
73747
  reservedNames.add(worktreeName);
72959
- return join36(this.store.getRootDir(), ".worktrees", worktreeName);
73748
+ return join37(this.store.getRootDir(), ".worktrees", worktreeName);
72960
73749
  }
72961
73750
  /**
72962
73751
  * Run one scheduling pass.
@@ -74233,7 +75022,7 @@ var init_mission_execution_loop = __esm({
74233
75022
  init_pi();
74234
75023
  init_agent_session_helpers();
74235
75024
  init_logger2();
74236
- init_notifier();
75025
+ init_fallback_model_observer();
74237
75026
  loopLog = createLogger2("mission-loop");
74238
75027
  VALIDATION_TIMEOUT_MS = 10 * 60 * 1e3;
74239
75028
  MissionExecutionLoop = class extends EventEmitter17 {
@@ -74467,7 +75256,13 @@ Assertions: ${assertions.map((a) => a.title).join(", ")}`,
74467
75256
  },
74468
75257
  taskId: task?.id,
74469
75258
  taskTitle: task?.title,
74470
- onFallbackModelUsed: notifyFallbackUsed
75259
+ onFallbackModelUsed: createFallbackModelObserver({
75260
+ agent: "reviewer",
75261
+ label: "mission validator",
75262
+ store: this.taskStore,
75263
+ taskId: task?.id,
75264
+ taskTitle: task?.title
75265
+ })
74471
75266
  });
74472
75267
  session = { session: sessionResult.session, sessionFile: sessionResult.sessionFile };
74473
75268
  loopLog.log(`Validation session created for feature ${feature.id}`);
@@ -77732,7 +78527,7 @@ async function createAiPromptExecutor(cwd) {
77732
78527
  }
77733
78528
  };
77734
78529
  }
77735
- function truncateOutput(stdout, stderr) {
78530
+ function truncateOutput2(stdout, stderr) {
77736
78531
  const out = stdout ?? "";
77737
78532
  const err = stderr ?? "";
77738
78533
  let combined = out;
@@ -77912,7 +78707,7 @@ var init_cron_runner = __esm({
77912
78707
  maxBuffer: MAX_BUFFER,
77913
78708
  shell: defaultShell
77914
78709
  });
77915
- const output = truncateOutput(stdout, stderr);
78710
+ const output = truncateOutput2(stdout, stderr);
77916
78711
  log15.log(`\u2713 ${schedule.name} completed (${output.length} bytes output)`);
77917
78712
  return {
77918
78713
  success: true,
@@ -77923,7 +78718,7 @@ var init_cron_runner = __esm({
77923
78718
  } catch (err) {
77924
78719
  const stdout = err.stdout ?? "";
77925
78720
  const stderr = err.stderr ?? "";
77926
- const output = truncateOutput(stdout, stderr);
78721
+ const output = truncateOutput2(stdout, stderr);
77927
78722
  const errorMessage = err.killed ? `Command timed out after ${(schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS6) / 1e3}s` : err.message ?? String(err);
77928
78723
  log15.warn(`\u2717 ${schedule.name} failed: ${errorMessage}`);
77929
78724
  return {
@@ -77968,7 +78763,7 @@ var init_cron_runner = __esm({
77968
78763
  const result = await runBackupCommand2(fusionDir, settings);
77969
78764
  return {
77970
78765
  success: result.success,
77971
- output: truncateOutput(result.output ?? "", ""),
78766
+ output: truncateOutput2(result.output ?? "", ""),
77972
78767
  error: result.success ? void 0 : result.output
77973
78768
  };
77974
78769
  } catch (err) {
@@ -78009,7 +78804,7 @@ var init_cron_runner = __esm({
78009
78804
  if (sr.output) outputParts.push(sr.output);
78010
78805
  if (sr.error) outputParts.push(`Error: ${sr.error}`);
78011
78806
  }
78012
- const output = truncateOutput(outputParts.join("\n"), "");
78807
+ const output = truncateOutput2(outputParts.join("\n"), "");
78013
78808
  const failedSteps = stepResults.filter((sr) => !sr.success);
78014
78809
  const error = failedSteps.length > 0 ? `${failedSteps.length} step(s) failed: ${failedSteps.map((s) => s.stepName).join(", ")}${stoppedEarly ? " (execution stopped)" : ""}` : void 0;
78015
78810
  const status = overallSuccess ? "\u2713" : "\u2717";
@@ -78088,7 +78883,7 @@ var init_cron_runner = __esm({
78088
78883
  stepName: step.name,
78089
78884
  stepIndex,
78090
78885
  success: true,
78091
- output: truncateOutput(stdout, stderr),
78886
+ output: truncateOutput2(stdout, stderr),
78092
78887
  startedAt,
78093
78888
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
78094
78889
  };
@@ -78101,7 +78896,7 @@ var init_cron_runner = __esm({
78101
78896
  stepName: step.name,
78102
78897
  stepIndex,
78103
78898
  success: false,
78104
- output: truncateOutput(stdout, stderr),
78899
+ output: truncateOutput2(stdout, stderr),
78105
78900
  error: errorMessage,
78106
78901
  startedAt,
78107
78902
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -78251,7 +79046,7 @@ var init_cron_runner = __esm({
78251
79046
  // ../engine/src/routine-runner.ts
78252
79047
  import { exec as exec8 } from "node:child_process";
78253
79048
  import { promisify as promisify8 } from "node:util";
78254
- function truncateOutput2(stdout, stderr) {
79049
+ function truncateOutput3(stdout, stderr) {
78255
79050
  let output = stdout;
78256
79051
  if (stderr) {
78257
79052
  output += stdout ? "\n--- stderr ---\n" : "";
@@ -78434,7 +79229,7 @@ var init_routine_runner = __esm({
78434
79229
  const result = await runBackupCommand2(fusionDir, settings);
78435
79230
  return {
78436
79231
  success: result.success,
78437
- output: truncateOutput2(result.output ?? "", ""),
79232
+ output: truncateOutput3(result.output ?? "", ""),
78438
79233
  error: result.success ? void 0 : result.output,
78439
79234
  startedAt,
78440
79235
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -78458,7 +79253,7 @@ var init_routine_runner = __esm({
78458
79253
  });
78459
79254
  return {
78460
79255
  success: true,
78461
- output: truncateOutput2(stdout, stderr),
79256
+ output: truncateOutput3(stdout, stderr),
78462
79257
  startedAt,
78463
79258
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
78464
79259
  };
@@ -78469,7 +79264,7 @@ var init_routine_runner = __esm({
78469
79264
  const error = errObj.killed === true ? `Command timed out after ${(timeoutMs ?? DEFAULT_TIMEOUT_MS7) / 1e3}s` : (err instanceof Error ? err.message : null) ?? String(err);
78470
79265
  return {
78471
79266
  success: false,
78472
- output: truncateOutput2(stdout, stderr),
79267
+ output: truncateOutput3(stdout, stderr),
78473
79268
  error,
78474
79269
  startedAt,
78475
79270
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -78502,7 +79297,7 @@ var init_routine_runner = __esm({
78502
79297
  const failedSteps = stepResults.filter((sr) => !sr.success);
78503
79298
  return {
78504
79299
  success: overallSuccess,
78505
- output: truncateOutput2(outputParts.join("\n"), ""),
79300
+ output: truncateOutput3(outputParts.join("\n"), ""),
78506
79301
  error: failedSteps.length > 0 ? `${failedSteps.length} step(s) failed: ${failedSteps.map((s) => s.stepName).join(", ")}${stoppedEarly ? " (execution stopped)" : ""}` : void 0,
78507
79302
  startedAt,
78508
79303
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -78537,7 +79332,7 @@ var init_routine_runner = __esm({
78537
79332
  this.options.aiPromptExecutor(step.prompt, step.modelProvider, step.modelId),
78538
79333
  new Promise((_resolve, reject) => setTimeout(() => reject(new Error(`AI prompt step timed out after ${timeoutMs / 1e3}s`)), timeoutMs))
78539
79334
  ]);
78540
- return { stepId: step.id, stepName: step.name, stepIndex, success: true, output: truncateOutput2(output, ""), startedAt, completedAt: (/* @__PURE__ */ new Date()).toISOString() };
79335
+ return { stepId: step.id, stepName: step.name, stepIndex, success: true, output: truncateOutput3(output, ""), startedAt, completedAt: (/* @__PURE__ */ new Date()).toISOString() };
78541
79336
  } catch (err) {
78542
79337
  return { stepId: step.id, stepName: step.name, stepIndex, success: false, output: "", error: err instanceof Error ? err.message : String(err), startedAt, completedAt: (/* @__PURE__ */ new Date()).toISOString() };
78543
79338
  }
@@ -79151,8 +79946,8 @@ var init_stuck_task_detector = __esm({
79151
79946
  // ../engine/src/self-healing.ts
79152
79947
  import { exec as exec9 } from "node:child_process";
79153
79948
  import { promisify as promisify9 } from "node:util";
79154
- import { existsSync as existsSync30, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
79155
- import { isAbsolute as isAbsolute13, join as join37, relative as relative8, resolve as resolve17 } from "node:path";
79949
+ import { existsSync as existsSync31, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
79950
+ import { isAbsolute as isAbsolute13, join as join38, relative as relative8, resolve as resolve17 } from "node:path";
79156
79951
  function shellQuote(value) {
79157
79952
  return `'${value.replace(/'/g, "'\\''")}'`;
79158
79953
  }
@@ -79197,14 +79992,15 @@ var init_self_healing = __esm({
79197
79992
  execAsync7 = promisify9(exec9);
79198
79993
  APPROVED_TRIAGE_RECOVERY_GRACE_MS = 6e4;
79199
79994
  ORPHANED_EXECUTION_RECOVERY_GRACE_MS = 6e4;
79200
- ACTIVE_MERGE_STATUSES = /* @__PURE__ */ new Set(["merging", "merging-pr"]);
79995
+ ACTIVE_MERGE_STATUSES = /* @__PURE__ */ new Set(["merging", "merging-pr", "merging-fix"]);
79201
79996
  NON_TERMINAL_STEP_STATUSES2 = /* @__PURE__ */ new Set(["pending", "in-progress"]);
79202
79997
  GHOST_REVIEW_PRESERVED_STATUSES = /* @__PURE__ */ new Set([
79203
79998
  "failed",
79204
79999
  "awaiting-user-review",
79205
80000
  "awaiting-approval",
79206
80001
  "merging",
79207
- "merging-pr"
80002
+ "merging-pr",
80003
+ "merging-fix"
79208
80004
  ]);
79209
80005
  ORPHANED_WITH_WORKTREE_GRACE_MS = 3e5;
79210
80006
  MAX_TASK_DONE_RETRIES = 3;
@@ -79548,7 +80344,7 @@ var init_self_healing = __esm({
79548
80344
  return commit;
79549
80345
  }
79550
80346
  async cleanupInterruptedMergeArtifacts(task) {
79551
- if (task.worktree && existsSync30(task.worktree)) {
80347
+ if (task.worktree && existsSync31(task.worktree)) {
79552
80348
  try {
79553
80349
  await execAsync7(`git worktree remove ${shellQuote(task.worktree)} --force`, {
79554
80350
  cwd: this.options.rootDir,
@@ -79927,7 +80723,7 @@ var init_self_healing = __esm({
79927
80723
  *
79928
80724
  * Preserved statuses (skipped):
79929
80725
  * - `awaiting-user-review`, `awaiting-approval`: explicit human handoff
79930
- * - `merging`, `merging-pr`: handled by `recoverInterruptedMergingTasks`
80726
+ * - `merging`, `merging-pr`, `merging-fix`: handled by `recoverInterruptedMergingTasks`
79931
80727
  *
79932
80728
  * Rate-limiting comes from the `updatedAt >= taskStuckTimeoutMs` gate —
79933
80729
  * each kick refreshes `updatedAt`, so a task that re-enters review and gets
@@ -80169,7 +80965,7 @@ var init_self_healing = __esm({
80169
80965
  return false;
80170
80966
  }
80171
80967
  const staleness = now - new Date(t.updatedAt).getTime();
80172
- const hasWorktree = t.worktree && existsSync30(t.worktree);
80968
+ const hasWorktree = t.worktree && existsSync31(t.worktree);
80173
80969
  const graceMs = hasWorktree ? ORPHANED_WITH_WORKTREE_GRACE_MS : ORPHANED_EXECUTION_RECOVERY_GRACE_MS;
80174
80970
  return staleness >= graceMs;
80175
80971
  });
@@ -80178,7 +80974,7 @@ var init_self_healing = __esm({
80178
80974
  let recovered = 0;
80179
80975
  for (const task of orphaned) {
80180
80976
  try {
80181
- const hadWorktree = task.worktree && existsSync30(task.worktree);
80977
+ const hadWorktree = task.worktree && existsSync31(task.worktree);
80182
80978
  const reason = hadWorktree ? "worktree exists but no active session" : "missing worktree/session";
80183
80979
  await this.resetStepsIfWorkLost(task);
80184
80980
  await this.store.updateTask(task.id, {
@@ -80324,7 +81120,7 @@ var init_self_healing = __esm({
80324
81120
  }
80325
81121
  }
80326
81122
  async hasRecoverableGitWork(task) {
80327
- if (task.worktree && existsSync30(task.worktree)) {
81123
+ if (task.worktree && existsSync31(task.worktree)) {
80328
81124
  try {
80329
81125
  const { stdout: status } = await execAsync7("git status --porcelain", {
80330
81126
  cwd: task.worktree,
@@ -80509,11 +81305,11 @@ var init_self_healing = __esm({
80509
81305
  * tracks registered idle worktrees, never these orphans.
80510
81306
  */
80511
81307
  async reapUnregisteredOrphans() {
80512
- const worktreesDir = join37(this.options.rootDir, ".worktrees");
80513
- if (!existsSync30(worktreesDir)) return 0;
81308
+ const worktreesDir = join38(this.options.rootDir, ".worktrees");
81309
+ if (!existsSync31(worktreesDir)) return 0;
80514
81310
  let dirs;
80515
81311
  try {
80516
- dirs = readdirSync5(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join37(worktreesDir, e.name));
81312
+ dirs = readdirSync5(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join38(worktreesDir, e.name));
80517
81313
  } catch (err) {
80518
81314
  log17.warn(`Failed to read .worktrees/ for unregistered orphan reap: ${err instanceof Error ? err.message : String(err)}`);
80519
81315
  return 0;
@@ -80618,8 +81414,8 @@ var init_self_healing = __esm({
80618
81414
  }
80619
81415
  /** Remove oldest idle worktrees if total count exceeds 2× maxWorktrees. */
80620
81416
  async enforceWorktreeCap() {
80621
- const worktreesDir = join37(this.options.rootDir, ".worktrees");
80622
- if (!existsSync30(worktreesDir)) return;
81417
+ const worktreesDir = join38(this.options.rootDir, ".worktrees");
81418
+ if (!existsSync31(worktreesDir)) return;
80623
81419
  try {
80624
81420
  const settings = await this.store.getSettings();
80625
81421
  const cap = (settings.maxWorktrees ?? 4) * 2;
@@ -82457,7 +83253,7 @@ var init_ipc_host = __esm({
82457
83253
  import { EventEmitter as EventEmitter20 } from "node:events";
82458
83254
  import { fork } from "node:child_process";
82459
83255
  import { fileURLToPath as fileURLToPath3 } from "node:url";
82460
- import { dirname as dirname11, join as join38 } from "node:path";
83256
+ import { dirname as dirname11, join as join39 } from "node:path";
82461
83257
  var HealthMonitor, ChildProcessRuntime;
82462
83258
  var init_child_process_runtime = __esm({
82463
83259
  "../engine/src/runtimes/child-process-runtime.ts"() {
@@ -82619,7 +83415,7 @@ var init_child_process_runtime = __esm({
82619
83415
  const isCompiled = !import.meta.url.endsWith(".ts");
82620
83416
  const currentDir = dirname11(fileURLToPath3(import.meta.url));
82621
83417
  const workerFile = isCompiled ? "child-process-worker.js" : "child-process-worker.ts";
82622
- return join38(currentDir, workerFile);
83418
+ return join39(currentDir, workerFile);
82623
83419
  }
82624
83420
  /**
82625
83421
  * Set up event forwarding from IPC host to runtime listeners.
@@ -85543,7 +86339,7 @@ ${detail}`
85543
86339
  "agent"
85544
86340
  );
85545
86341
  await store.updateTask(taskId, {
85546
- status: null,
86342
+ status: "merging-fix",
85547
86343
  mergeRetries: 0,
85548
86344
  error: null,
85549
86345
  verificationFailureCount: nextBounces
@@ -85551,10 +86347,10 @@ ${detail}`
85551
86347
  await store.moveTask(taskId, "in-progress");
85552
86348
  await store.logEntry(
85553
86349
  taskId,
85554
- `Deterministic ${failedKind} verification failed (${nextBounces}/${cap}) \u2014 moved back to in-progress for remediation`
86350
+ `Deterministic ${failedKind} verification failed (${nextBounces}/${cap}) \u2014 moved back to in-progress with status=merging-fix for remediation`
85555
86351
  );
85556
86352
  runtimeLog.log(
85557
- `Auto-merge: ${taskId} deterministic ${failedKind} verification failed (${nextBounces}/${cap}) \u2014 moved to in-progress`
86353
+ `Auto-merge: ${taskId} deterministic ${failedKind} verification failed (${nextBounces}/${cap}) \u2014 moved to in-progress with status=merging-fix`
85558
86354
  );
85559
86355
  } catch {
85560
86356
  runtimeLog.error(
@@ -86872,6 +87668,8 @@ __export(src_exports2, {
86872
87668
  describeAgentModel: () => describeAgentModel,
86873
87669
  describeModel: () => describeModel,
86874
87670
  ensureDefaultHeartbeatProcedureFile: () => ensureDefaultHeartbeatProcedureFile,
87671
+ extractRuntimeHint: () => extractRuntimeHint,
87672
+ extractRuntimeModel: () => extractRuntimeModel,
86875
87673
  formatTaskIdentifier: () => formatTaskIdentifier,
86876
87674
  getDefaultPiRuntime: () => getDefaultPiRuntime,
86877
87675
  getHostExtensionPaths: () => getHostExtensionPaths,
@@ -96539,10 +97337,18 @@ var init_claude_cli_probe = __esm({
96539
97337
  }
96540
97338
  });
96541
97339
 
97340
+ // ../../plugins/fusion-plugin-droid-runtime/dist/probe.js
97341
+ var init_probe3 = __esm({
97342
+ "../../plugins/fusion-plugin-droid-runtime/dist/probe.js"() {
97343
+ "use strict";
97344
+ }
97345
+ });
97346
+
96542
97347
  // ../dashboard/src/droid-cli-probe.ts
96543
97348
  var init_droid_cli_probe = __esm({
96544
97349
  "../dashboard/src/droid-cli-probe.ts"() {
96545
97350
  "use strict";
97351
+ init_probe3();
96546
97352
  }
96547
97353
  });
96548
97354
 
@@ -101430,7 +102236,7 @@ var init_auth_middleware = __esm({
101430
102236
 
101431
102237
  // ../dashboard/src/server.ts
101432
102238
  import express from "express";
101433
- import { join as join39, dirname as dirname12 } from "node:path";
102239
+ import { join as join40, dirname as dirname12 } from "node:path";
101434
102240
  import { fileURLToPath as fileURLToPath4 } from "node:url";
101435
102241
  function clearAiSessionCleanupInterval() {
101436
102242
  if (!aiSessionCleanupIntervalHandle) {
@@ -101722,8 +102528,8 @@ __export(task_exports, {
101722
102528
  runTaskUpdate: () => runTaskUpdate
101723
102529
  });
101724
102530
  import { createInterface as createInterface2 } from "node:readline/promises";
101725
- import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync31, readFileSync as readFileSync10 } from "node:fs";
101726
- import { basename as basename10, join as join40 } from "node:path";
102531
+ import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync32, readFileSync as readFileSync11 } from "node:fs";
102532
+ import { basename as basename10, join as join41 } from "node:path";
101727
102533
  function getGitHubIssueUrl(sourceMetadata) {
101728
102534
  if (!sourceMetadata || typeof sourceMetadata !== "object") return void 0;
101729
102535
  const issueUrl = sourceMetadata.issueUrl;
@@ -102036,8 +102842,8 @@ async function runTaskLogs(id, options = {}, projectName) {
102036
102842
  printEntries(filteredEntries);
102037
102843
  if (options.follow) {
102038
102844
  const projectPath = projectContext?.projectPath ?? process.cwd();
102039
- const logPath = join40(projectPath, ".fusion", "tasks", id, "agent.log");
102040
- if (!existsSync31(logPath)) {
102845
+ const logPath = join41(projectPath, ".fusion", "tasks", id, "agent.log");
102846
+ if (!existsSync32(logPath)) {
102041
102847
  console.log(`
102042
102848
  Waiting for log file to be created...`);
102043
102849
  }
@@ -102066,7 +102872,7 @@ async function runTaskLogs(id, options = {}, projectName) {
102066
102872
  lastPosition = 0;
102067
102873
  }
102068
102874
  if (stats.size > lastPosition) {
102069
- const content = readFileSync10(logPath, "utf-8");
102875
+ const content = readFileSync11(logPath, "utf-8");
102070
102876
  const lines = content.slice(lastPosition).split("\n");
102071
102877
  for (const line of lines) {
102072
102878
  if (!line.trim()) continue;
@@ -103179,9 +103985,9 @@ init_src();
103179
103985
  init_gh_cli();
103180
103986
  import { Type as Type8 } from "typebox";
103181
103987
  import { StringEnum } from "@mariozechner/pi-ai";
103182
- import { resolve as resolve19, basename as basename11, extname as extname3, join as join41 } from "node:path";
103988
+ import { resolve as resolve19, basename as basename11, extname as extname3, join as join42 } from "node:path";
103183
103989
  import { readFile as readFile19 } from "node:fs/promises";
103184
- import { existsSync as existsSync32 } from "node:fs";
103990
+ import { existsSync as existsSync33 } from "node:fs";
103185
103991
  import { spawn as spawn11 } from "node:child_process";
103186
103992
  var MIME_TYPES2 = {
103187
103993
  ".png": "image/png",
@@ -103201,7 +104007,7 @@ var MIME_TYPES2 = {
103201
104007
  function resolveProjectRoot2(cwd) {
103202
104008
  let current = resolve19(cwd);
103203
104009
  while (true) {
103204
- if (existsSync32(join41(current, ".fusion"))) {
104010
+ if (existsSync33(join42(current, ".fusion"))) {
103205
104011
  return current;
103206
104012
  }
103207
104013
  const parent = resolve19(current, "..");
@@ -103222,7 +104028,7 @@ async function getStore2(cwd) {
103222
104028
  return store;
103223
104029
  }
103224
104030
  function getFusionDir(cwd) {
103225
- return join41(resolveProjectRoot2(cwd), ".fusion");
104031
+ return join42(resolveProjectRoot2(cwd), ".fusion");
103226
104032
  }
103227
104033
  async function validateAssignableAgentId(cwd, agentId) {
103228
104034
  const { AgentStore: AgentStore2, isEphemeralAgent: isEphemeralAgent2 } = await Promise.resolve().then(() => (init_src(), src_exports));
@@ -103271,9 +104077,12 @@ async function getResearchAvailability(store) {
103271
104077
  }
103272
104078
  const backend = resolved.searchProvider ?? settings.researchWebSearchProvider;
103273
104079
  const configured = backend === "searxng" ? Boolean(settings.researchSearxngUrl) : backend === "brave" ? Boolean(settings.researchBraveApiKey) : backend === "google" ? Boolean(settings.researchGoogleSearchApiKey && settings.researchGoogleSearchCx) : backend === "tavily" ? Boolean(settings.researchTavilyApiKey) : false;
103274
- if (!configured && !resolved.searchProvider) {
104080
+ if (!backend) {
103275
104081
  return { ok: false, code: "provider-unavailable", message: "Research provider is not configured. Set research provider credentials in Settings." };
103276
104082
  }
104083
+ if (!configured) {
104084
+ return { ok: false, code: "missing-credentials", message: `Missing credentials for ${backend}. Add provider keys in Authentication and verify Research defaults.` };
104085
+ }
103277
104086
  return { ok: true };
103278
104087
  }
103279
104088
  function toResearchRunDetails(run) {
@@ -103664,12 +104473,13 @@ Path: .fusion/tasks/${params.id}/attachments/${attachment.filename}`
103664
104473
  pi.registerTool({
103665
104474
  name: "fn_task_retry",
103666
104475
  label: "fn: Retry Task",
103667
- description: "Retry a failed task \u2014 clears the error state and moves it back to the todo column for re-execution.",
103668
- promptSnippet: "Retry a failed Fusion task (clears error, moves to todo)",
104476
+ description: "Retry a failed task \u2014 clears the error state. Tasks in other columns move to todo; tasks in in-review stay in-place for auto-merge retry.",
104477
+ promptSnippet: "Retry a failed Fusion task (clears error, moves to todo or stays in in-review)",
103669
104478
  promptGuidelines: [
103670
- "Use when a task has failed and needs to be retried from the beginning",
103671
- "Only tasks in 'failed' state can be retried",
103672
- "The task will be moved to the todo column with error state cleared"
104479
+ "Use when a task has failed and needs to be retried",
104480
+ "Only tasks in 'failed' or 'stuck-killed' state can be retried",
104481
+ "Tasks in 'in-review' stay in in-review \u2014 only the error/retry state is cleared, and the auto-merge system re-attempts",
104482
+ "Tasks in other columns are moved to the todo column with error state cleared"
103673
104483
  ],
103674
104484
  parameters: Type8.Object({
103675
104485
  id: Type8.String({ description: "Task ID to retry (e.g. FN-001). Must be in 'failed' state." })
@@ -103693,6 +104503,14 @@ Path: .fusion/tasks/${params.id}/attachments/${attachment.filename}`
103693
104503
  details: { taskId: params.id, currentStatus: task.status }
103694
104504
  };
103695
104505
  }
104506
+ if (task.column === "in-review") {
104507
+ await store.updateTask(params.id, { status: null, error: null, stuckKillCount: 0, mergeRetries: 0 });
104508
+ await store.logEntry(params.id, "Retry requested via Fusion extension (in-review retry, mergeRetries reset)");
104509
+ return {
104510
+ content: [{ type: "text", text: `Retried ${params.id} \u2192 in-review (merge retry state cleared, task stays in in-review)` }],
104511
+ details: { taskId: params.id, newColumn: "in-review" }
104512
+ };
104513
+ }
103696
104514
  await store.updateTask(params.id, { status: null, error: null });
103697
104515
  await store.moveTask(params.id, "todo");
103698
104516
  await store.logEntry(params.id, "Retry requested via Fusion extension", "Task reset to todo for retry");
@@ -104184,7 +105002,7 @@ Planning session completed. Task ${taskId} is now in planning and will be auto-p
104184
105002
  pi.registerTool({
104185
105003
  name: "fn_research_cancel",
104186
105004
  label: "fn: Cancel Research Run",
104187
- description: "Cancel a research run.",
105005
+ description: "Cancel an in-flight research run. Terminal runs return INVALID_TRANSITION.",
104188
105006
  parameters: Type8.Object({ id: Type8.String({ description: "Research run ID" }) }),
104189
105007
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
104190
105008
  const store = await getStore2(ctx.cwd);
@@ -104196,6 +105014,17 @@ Planning session completed. Task ${taskId} is now in planning and will be auto-p
104196
105014
  details: { runId: params.id, status: "missing", summary: null, findings: [], citations: [], error: "not found", setup: null }
104197
105015
  };
104198
105016
  }
105017
+ if (!["queued", "running", "cancelling", "retry_waiting"].includes(run.status)) {
105018
+ return {
105019
+ content: [{ type: "text", text: `Research run ${params.id} cannot be cancelled from status ${run.status}.` }],
105020
+ isError: true,
105021
+ details: {
105022
+ ...toResearchRunDetails(run),
105023
+ error: "invalid transition",
105024
+ setup: { code: "INVALID_TRANSITION", message: "Cancel is only available for queued/running/cancelling/retry_waiting runs." }
105025
+ }
105026
+ };
105027
+ }
104199
105028
  const updated = researchStore.requestCancellation(params.id);
104200
105029
  return {
104201
105030
  content: [{ type: "text", text: `Requested cancellation for research run ${params.id} (status: ${updated.status}).` }],
@@ -104203,6 +105032,41 @@ Planning session completed. Task ${taskId} is now in planning and will be auto-p
104203
105032
  };
104204
105033
  }
104205
105034
  });
105035
+ pi.registerTool({
105036
+ name: "fn_research_retry",
105037
+ label: "fn: Retry Research Run",
105038
+ description: "Retry a failed research run when lifecycle marks it retryable.",
105039
+ parameters: Type8.Object({ id: Type8.String({ description: "Research run ID" }) }),
105040
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
105041
+ const store = await getStore2(ctx.cwd);
105042
+ const researchStore = store.getResearchStore();
105043
+ const run = researchStore.getRun(params.id);
105044
+ if (!run) {
105045
+ return {
105046
+ content: [{ type: "text", text: `Research run ${params.id} not found.` }],
105047
+ isError: true,
105048
+ details: { runId: params.id, status: "missing", summary: null, findings: [], citations: [], error: "not found", setup: null }
105049
+ };
105050
+ }
105051
+ const isRetryExhausted = run.status === "retry_exhausted" || run.lifecycle?.errorCode === "RETRY_EXHAUSTED";
105052
+ if (run.status !== "failed" && run.status !== "timed_out" || run.lifecycle?.retryable === false || isRetryExhausted) {
105053
+ return {
105054
+ content: [{ type: "text", text: `Research run ${params.id} is not retryable from status ${run.status}.` }],
105055
+ isError: true,
105056
+ details: {
105057
+ ...toResearchRunDetails(run),
105058
+ error: "not retryable",
105059
+ setup: { code: isRetryExhausted ? "RETRY_EXHAUSTED" : "INVALID_TRANSITION", message: "Retry is only available for failed/timed_out retryable runs." }
105060
+ }
105061
+ };
105062
+ }
105063
+ const retryRun = researchStore.createRetryRun(params.id);
105064
+ return {
105065
+ content: [{ type: "text", text: `Created retry run ${retryRun.id} from ${params.id}.` }],
105066
+ details: toResearchRunDetails(retryRun)
105067
+ };
105068
+ }
105069
+ });
104206
105070
  pi.registerTool({
104207
105071
  name: "fn_insight_list",
104208
105072
  label: "fn: List Insights",
@@ -104899,6 +105763,278 @@ Status: ${updated.status}`
104899
105763
  };
104900
105764
  }
104901
105765
  });
105766
+ pi.registerTool({
105767
+ name: "fn_list_agents",
105768
+ label: "fn: List Agents",
105769
+ description: "List all available agents in the system. Shows each agent's name, role, state, personality (soul), and current assignment. Use this to discover which agents exist and what they specialize in before delegating work.",
105770
+ promptSnippet: "List all available Fusion agents",
105771
+ promptGuidelines: [
105772
+ "Use fn_list_agents to discover which agents exist before delegating work",
105773
+ "Filter by role or state to narrow results",
105774
+ "Ephemeral/runtime agents are excluded by default"
105775
+ ],
105776
+ parameters: Type8.Object({
105777
+ role: Type8.Optional(
105778
+ Type8.String({ description: "Filter by agent role/capability (e.g., 'executor', 'reviewer', 'qa')" })
105779
+ ),
105780
+ state: Type8.Optional(
105781
+ Type8.String({ description: "Filter by agent state (e.g., 'idle', 'active', 'running')" })
105782
+ ),
105783
+ includeEphemeral: Type8.Optional(
105784
+ Type8.Boolean({ description: "Include ephemeral/runtime agents (default: false)" })
105785
+ )
105786
+ }),
105787
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
105788
+ const { AgentStore: AgentStore2 } = await Promise.resolve().then(() => (init_src(), src_exports));
105789
+ const agentStore = new AgentStore2({ rootDir: getFusionDir(ctx.cwd) });
105790
+ await agentStore.init();
105791
+ const filter = {};
105792
+ if (params.role) filter.role = params.role;
105793
+ if (params.state) filter.state = params.state;
105794
+ if (params.includeEphemeral !== void 0) filter.includeEphemeral = params.includeEphemeral;
105795
+ const agents = await agentStore.listAgents(filter);
105796
+ if (agents.length === 0) {
105797
+ return {
105798
+ content: [{ type: "text", text: "No agents found matching the specified filters." }],
105799
+ details: { agents: [], count: 0 }
105800
+ };
105801
+ }
105802
+ const lines = agents.map((agent) => {
105803
+ const parts = [
105804
+ `ID: ${agent.id}`,
105805
+ `Name: ${agent.name}`,
105806
+ `Role: ${agent.role}`,
105807
+ `State: ${agent.state}`
105808
+ ];
105809
+ if (agent.title) parts.push(`Title: ${agent.title}`);
105810
+ if (agent.soul) parts.push(`Soul: ${agent.soul.slice(0, 200)}`);
105811
+ if (agent.instructionsText) {
105812
+ const snippet = agent.instructionsText.slice(0, 100);
105813
+ parts.push(`Custom Instructions: ${snippet}${agent.instructionsText.length > 100 ? "\u2026" : ""}`);
105814
+ }
105815
+ if (agent.taskId) parts.push(`Current Task: ${agent.taskId}`);
105816
+ return parts.join("\n");
105817
+ });
105818
+ return {
105819
+ content: [{ type: "text", text: `Available agents (${agents.length}):
105820
+
105821
+ ${lines.join("\n\n")}` }],
105822
+ details: { agents, count: agents.length }
105823
+ };
105824
+ }
105825
+ });
105826
+ pi.registerTool({
105827
+ name: "fn_delegate_task",
105828
+ label: "fn: Delegate Task",
105829
+ description: "Create a new task and assign it to a specific agent for execution. The task goes to 'todo' and will be picked up by the target agent on their next heartbeat cycle. Use fn_list_agents first to find available agents and their capabilities.",
105830
+ promptSnippet: "Delegate a task to a specific Fusion agent",
105831
+ promptGuidelines: [
105832
+ "Use fn_list_agents first to find available agents and their capabilities",
105833
+ "The task is created in 'todo' and assigned to the target agent",
105834
+ "Cannot delegate to ephemeral/runtime agents",
105835
+ "Optionally specify dependencies on other tasks"
105836
+ ],
105837
+ parameters: Type8.Object({
105838
+ agent_id: Type8.String({ description: "The agent ID to delegate work to" }),
105839
+ description: Type8.String({ description: "What needs to be done" }),
105840
+ dependencies: Type8.Optional(
105841
+ Type8.Array(Type8.String(), { description: 'Task IDs this new task depends on (e.g. ["KB-001"]' })
105842
+ )
105843
+ }),
105844
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
105845
+ const agentError = await validateAssignableAgentId(ctx.cwd, params.agent_id);
105846
+ if (agentError) {
105847
+ return {
105848
+ content: [{ type: "text", text: `ERROR: ${agentError}` }],
105849
+ isError: true,
105850
+ details: { error: agentError }
105851
+ };
105852
+ }
105853
+ const { AgentStore: AgentStore2 } = await Promise.resolve().then(() => (init_src(), src_exports));
105854
+ const agentStore = new AgentStore2({ rootDir: getFusionDir(ctx.cwd) });
105855
+ await agentStore.init();
105856
+ const agent = await agentStore.getAgent(params.agent_id);
105857
+ const store = await getStore2(ctx.cwd);
105858
+ const task = await store.createTask({
105859
+ description: params.description,
105860
+ dependencies: params.dependencies,
105861
+ column: "todo",
105862
+ assignedAgentId: params.agent_id,
105863
+ source: { sourceType: "api" }
105864
+ });
105865
+ const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
105866
+ return {
105867
+ content: [{
105868
+ type: "text",
105869
+ text: `Delegated to ${agent.name} (${agent.id}): Created ${task.id}${deps}. The task will be picked up by ${agent.name} on their next heartbeat cycle.`
105870
+ }],
105871
+ details: { taskId: task.id, agentId: agent.id, agentName: agent.name }
105872
+ };
105873
+ }
105874
+ });
105875
+ pi.registerTool({
105876
+ name: "fn_agent_show",
105877
+ label: "fn: Show Agent",
105878
+ description: "Show detailed information about a single agent, including their role, state, position in the org hierarchy (reports-to, direct reports), skills, and current assignment.",
105879
+ promptSnippet: "Show details of a specific Fusion agent",
105880
+ promptGuidelines: [
105881
+ "Use to get full details about a specific agent",
105882
+ "Provide agent ID or a resolvable name",
105883
+ "Shows the agent's position in the org hierarchy"
105884
+ ],
105885
+ parameters: Type8.Object({
105886
+ id: Type8.String({ description: "Agent ID or resolvable name" })
105887
+ }),
105888
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
105889
+ const { AgentStore: AgentStore2 } = await Promise.resolve().then(() => (init_src(), src_exports));
105890
+ const agentStore = new AgentStore2({ rootDir: getFusionDir(ctx.cwd) });
105891
+ await agentStore.init();
105892
+ const agent = await agentStore.resolveAgent(params.id);
105893
+ if (!agent) {
105894
+ return {
105895
+ content: [{ type: "text", text: `Agent '${params.id}' not found` }],
105896
+ isError: true,
105897
+ details: { error: "Agent not found" }
105898
+ };
105899
+ }
105900
+ const directReports = await agentStore.getAgentsByReportsTo(agent.id);
105901
+ const parts = [
105902
+ `ID: ${agent.id}`,
105903
+ `Name: ${agent.name}`,
105904
+ `Role: ${agent.role}`,
105905
+ `State: ${agent.state}`
105906
+ ];
105907
+ if (agent.title) parts.push(`Title: ${agent.title}`);
105908
+ if (agent.icon) parts.push(`Icon: ${agent.icon}`);
105909
+ if (agent.reportsTo) {
105910
+ const manager = await agentStore.getAgent(agent.reportsTo);
105911
+ if (manager) {
105912
+ parts.push(`Reports To: ${manager.name} (${manager.id})`);
105913
+ } else {
105914
+ parts.push(`Reports To: ${agent.reportsTo}`);
105915
+ }
105916
+ }
105917
+ if (directReports.length > 0) {
105918
+ parts.push(`Direct Reports: ${directReports.map((r) => `${r.name} (${r.id})`).join(", ")}`);
105919
+ }
105920
+ if (agent.taskId) parts.push(`Current Task: ${agent.taskId}`);
105921
+ if (agent.instructionsText) {
105922
+ const snippet = agent.instructionsText.slice(0, 100);
105923
+ parts.push(`Custom Instructions: ${snippet}${agent.instructionsText.length > 100 ? "\u2026" : ""}`);
105924
+ }
105925
+ if (agent.soul) {
105926
+ const snippet = agent.soul.slice(0, 200);
105927
+ parts.push(`Soul: ${snippet}${agent.soul.length > 200 ? "\u2026" : ""}`);
105928
+ }
105929
+ if (agent.metadata?.skills) {
105930
+ parts.push(`Skills: ${JSON.stringify(agent.metadata.skills)}`);
105931
+ }
105932
+ return {
105933
+ content: [{ type: "text", text: parts.join("\n") }],
105934
+ details: {
105935
+ agent,
105936
+ directReports: directReports.map((r) => ({ id: r.id, name: r.name, role: r.role }))
105937
+ }
105938
+ };
105939
+ }
105940
+ });
105941
+ pi.registerTool({
105942
+ name: "fn_agent_org_chart",
105943
+ label: "fn: Agent Org Chart",
105944
+ description: "Show the organizational tree of agents, displaying the role hierarchy. Optionally filter to a subtree rooted at a specific agent.",
105945
+ promptSnippet: "Show the Fusion agent org chart",
105946
+ promptGuidelines: [
105947
+ "Use to understand the team structure and reporting hierarchy",
105948
+ "Optionally specify a root agent to see only their subtree",
105949
+ "Ephemeral/runtime agents are excluded by default"
105950
+ ],
105951
+ parameters: Type8.Object({
105952
+ root_agent_id: Type8.Optional(
105953
+ Type8.String({ description: "If provided, show only the subtree rooted at this agent" })
105954
+ ),
105955
+ include_ephemeral: Type8.Optional(
105956
+ Type8.Boolean({ description: "Include ephemeral/runtime agents (default: false)" })
105957
+ )
105958
+ }),
105959
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
105960
+ const { AgentStore: AgentStore2 } = await Promise.resolve().then(() => (init_src(), src_exports));
105961
+ const agentStore = new AgentStore2({ rootDir: getFusionDir(ctx.cwd) });
105962
+ await agentStore.init();
105963
+ const includeEphemeral = params.include_ephemeral ?? false;
105964
+ if (params.root_agent_id) {
105965
+ const rootAgent = await agentStore.resolveAgent(params.root_agent_id);
105966
+ if (!rootAgent) {
105967
+ return {
105968
+ content: [{ type: "text", text: `Agent '${params.root_agent_id}' not found` }],
105969
+ isError: true,
105970
+ details: { error: "Root agent not found" }
105971
+ };
105972
+ }
105973
+ const fullTree = await agentStore.getOrgTree({ includeEphemeral });
105974
+ const findSubtree = (nodes) => {
105975
+ for (const node of nodes) {
105976
+ if (node.agent.id === rootAgent.id) return node;
105977
+ const found = findSubtree(node.children);
105978
+ if (found) return found;
105979
+ }
105980
+ return null;
105981
+ };
105982
+ const subtree = findSubtree(fullTree);
105983
+ if (!subtree) {
105984
+ return {
105985
+ content: [{
105986
+ type: "text",
105987
+ text: `${rootAgent.icon ?? "\u{1F916}"} ${rootAgent.name} (${rootAgent.role}) \u2014 ${rootAgent.state}${rootAgent.taskId ? ` [${rootAgent.taskId}]` : ""}`
105988
+ }],
105989
+ details: { tree: [{ agent: rootAgent, children: [] }] }
105990
+ };
105991
+ }
105992
+ const lines2 = [];
105993
+ const renderNode2 = (node, indent) => {
105994
+ const a = node.agent;
105995
+ lines2.push(
105996
+ `${indent}${a.icon ?? "\u{1F916}"} ${a.name} (${a.role}) \u2014 ${a.state}${a.taskId ? ` [${a.taskId}]` : ""}`
105997
+ );
105998
+ for (const child of node.children) {
105999
+ renderNode2(child, indent + " ");
106000
+ }
106001
+ };
106002
+ renderNode2(subtree, "");
106003
+ return {
106004
+ content: [{ type: "text", text: `Agent Org Tree (subtree: ${rootAgent.name}):
106005
+ ${lines2.join("\n")}` }],
106006
+ details: { tree: [subtree] }
106007
+ };
106008
+ }
106009
+ const tree = await agentStore.getOrgTree({ includeEphemeral });
106010
+ if (tree.length === 0) {
106011
+ return {
106012
+ content: [{ type: "text", text: "No agents found." }],
106013
+ details: { tree: [], count: 0 }
106014
+ };
106015
+ }
106016
+ const lines = [];
106017
+ let count = 0;
106018
+ const renderNode = (node, indent) => {
106019
+ const a = node.agent;
106020
+ lines.push(
106021
+ `${indent}${a.icon ?? "\u{1F916}"} ${a.name} (${a.role}) \u2014 ${a.state}${a.taskId ? ` [${a.taskId}]` : ""}`
106022
+ );
106023
+ count++;
106024
+ for (const child of node.children) {
106025
+ renderNode(child, indent + " ");
106026
+ }
106027
+ };
106028
+ for (const root of tree) {
106029
+ renderNode(root, "");
106030
+ }
106031
+ return {
106032
+ content: [{ type: "text", text: `Agent Org Tree (${count} agents):
106033
+ ${lines.join("\n")}` }],
106034
+ details: { tree, count }
106035
+ };
106036
+ }
106037
+ });
104902
106038
  pi.registerTool({
104903
106039
  name: "fn_skills_search",
104904
106040
  label: "FN: Search Skills",