@runfusion/fusion 0.17.1 → 0.17.2

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 (33) hide show
  1. package/dist/bin.js +1706 -787
  2. package/dist/client/assets/{AgentDetailView-BmxnuM0D.js → AgentDetailView-17J-F0Rl.js} +1 -1
  3. package/dist/client/assets/{AgentsView-1xSqjJxs.js → AgentsView-sbBkb7Wd.js} +3 -3
  4. package/dist/client/assets/ChatView-BR5cvK_B.js +1 -0
  5. package/dist/client/assets/{DevServerView-DIrmWI5T.js → DevServerView-GFFVXHVP.js} +1 -1
  6. package/dist/client/assets/{DirectoryPicker-Sqwdifcb.js → DirectoryPicker-WPDSBdT6.js} +1 -1
  7. package/dist/client/assets/{DocumentsView-Cx_02o_z.js → DocumentsView-BHpDsIIt.js} +1 -1
  8. package/dist/client/assets/{InsightsView-DAJSq4gV.js → InsightsView-Bxu0TJkt.js} +1 -1
  9. package/dist/client/assets/{MemoryView-CCIBAre3.js → MemoryView-CmnzZorw.js} +1 -1
  10. package/dist/client/assets/{NodesView-D02HxGCl.js → NodesView-CO9_4hCr.js} +1 -1
  11. package/dist/client/assets/{PiExtensionsManager-DD0fTQNf.js → PiExtensionsManager-4e3MlD62.js} +2 -2
  12. package/dist/client/assets/{PluginManager-Cfl0VBX9.js → PluginManager-DGN2rvOY.js} +1 -1
  13. package/dist/client/assets/{ResearchView-B9RqOVbr.js → ResearchView-Dsa6Gykl.js} +1 -1
  14. package/dist/client/assets/{RoadmapsView-DsH7Hicx.js → RoadmapsView-jHTOK0RQ.js} +1 -1
  15. package/dist/client/assets/{SettingsModal-Cn_CIPXu.js → SettingsModal-4Z8ZJMzD.js} +1 -1
  16. package/dist/client/assets/SettingsModal-D0kuJpBA.js +31 -0
  17. package/dist/client/assets/{SetupWizardModal-k5vqrHZU.js → SetupWizardModal-Bhumd4Rf.js} +1 -1
  18. package/dist/client/assets/{SkillsView-BIdt5cfB.js → SkillsView-MHweJTz4.js} +1 -1
  19. package/dist/client/assets/{folder-open-B3TO7t7Z.js → folder-open-BNQW9dE9.js} +1 -1
  20. package/dist/client/assets/{index-BIJgrHEn.css → index-DEVBHvyW.css} +1 -1
  21. package/dist/client/assets/index-k_85J1DS.js +682 -0
  22. package/dist/client/assets/{star-DW-M-BD_.js → star-7L86NZrT.js} +1 -1
  23. package/dist/client/assets/{upload-BzG6fknr.js → upload-DsAS6tno.js} +1 -1
  24. package/dist/client/assets/{users-DEicv0kj.js → users-D3u6f2Rz.js} +1 -1
  25. package/dist/client/index.html +2 -2
  26. package/dist/client/version.json +1 -1
  27. package/dist/extension.js +1215 -524
  28. package/dist/pi-claude-cli/package.json +1 -1
  29. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  30. package/package.json +1 -1
  31. package/dist/client/assets/ChatView-CkWkEwXL.js +0 -1
  32. package/dist/client/assets/SettingsModal-YH_rM1ZT.js +0 -31
  33. package/dist/client/assets/index-BlkXZ4C5.js +0 -682
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}$/;
@@ -29610,8 +29618,8 @@ var require_CronFileParser = __commonJS({
29610
29618
  * @throws If file cannot be read
29611
29619
  */
29612
29620
  static parseFileSync(filePath) {
29613
- const { readFileSync: readFileSync11 } = __require("fs");
29614
- const data = readFileSync11(filePath, "utf8");
29621
+ const { readFileSync: readFileSync12 } = __require("fs");
29622
+ const data = readFileSync12(filePath, "utf8");
29615
29623
  return _CronFileParser.#parseContent(data);
29616
29624
  }
29617
29625
  /**
@@ -52657,6 +52665,193 @@ var init_chat_store = __esm({
52657
52665
  }
52658
52666
  });
52659
52667
 
52668
+ // ../core/src/oauth-credential-interop.ts
52669
+ import { existsSync as existsSync19, readFileSync as readFileSync6 } from "node:fs";
52670
+ import { homedir as homedir4 } from "node:os";
52671
+ import { join as join22 } from "node:path";
52672
+ function getHomeDir4() {
52673
+ return process.env.HOME || process.env.USERPROFILE || homedir4();
52674
+ }
52675
+ function getCodexCliAuthPath(home = getHomeDir4()) {
52676
+ return join22(home, ".codex", "auth.json");
52677
+ }
52678
+ function parseJwtPayload(token) {
52679
+ try {
52680
+ const [, payload = ""] = token.split(".", 3);
52681
+ if (!payload) {
52682
+ return null;
52683
+ }
52684
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
52685
+ } catch {
52686
+ return null;
52687
+ }
52688
+ }
52689
+ function getJwtExpiryMs(token) {
52690
+ if (!token) {
52691
+ return void 0;
52692
+ }
52693
+ const payload = parseJwtPayload(token);
52694
+ const exp = payload?.exp;
52695
+ if (typeof exp !== "number" || !Number.isFinite(exp)) {
52696
+ return void 0;
52697
+ }
52698
+ return exp * 1e3;
52699
+ }
52700
+ function getCodexAccountId(accessToken, fallbackAccountId) {
52701
+ const payload = parseJwtPayload(accessToken);
52702
+ const authClaim = payload?.[OPENAI_AUTH_CLAIM];
52703
+ const claimAccountId = authClaim && typeof authClaim === "object" ? authClaim.chatgpt_account_id : void 0;
52704
+ if (typeof claimAccountId === "string" && claimAccountId.trim().length > 0) {
52705
+ return claimAccountId;
52706
+ }
52707
+ if (typeof fallbackAccountId === "string" && fallbackAccountId.trim().length > 0) {
52708
+ return fallbackAccountId;
52709
+ }
52710
+ return void 0;
52711
+ }
52712
+ function getLastRefreshFallbackExpiryMs(lastRefresh) {
52713
+ if (typeof lastRefresh !== "string" || lastRefresh.trim().length === 0) {
52714
+ return void 0;
52715
+ }
52716
+ const parsed = Date.parse(lastRefresh);
52717
+ if (!Number.isFinite(parsed)) {
52718
+ return void 0;
52719
+ }
52720
+ return parsed + CODEX_REFRESH_FALLBACK_WINDOW_MS;
52721
+ }
52722
+ function isStoredAuthCredential(value) {
52723
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
52724
+ return false;
52725
+ }
52726
+ const record = value;
52727
+ return record.type === "oauth" || record.type === "api_key";
52728
+ }
52729
+ function isValidOauthCredential(credential) {
52730
+ 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;
52731
+ }
52732
+ function isRefreshableOauthCredential(credential) {
52733
+ return credential?.type === "oauth" && typeof credential.refresh === "string" && credential.refresh.length > 0 && typeof credential.expires === "number" && Number.isFinite(credential.expires);
52734
+ }
52735
+ function compareStoredCredentials(left, right) {
52736
+ if (!left && !right) {
52737
+ return 0;
52738
+ }
52739
+ if (left && !right) {
52740
+ return 1;
52741
+ }
52742
+ if (!left && right) {
52743
+ return -1;
52744
+ }
52745
+ if (left?.type === "api_key" && right?.type !== "api_key") {
52746
+ return 1;
52747
+ }
52748
+ if (right?.type === "api_key" && left?.type !== "api_key") {
52749
+ return -1;
52750
+ }
52751
+ if (left?.type === "oauth" && right?.type === "oauth") {
52752
+ const leftValid = isValidOauthCredential(left);
52753
+ const rightValid = isValidOauthCredential(right);
52754
+ if (leftValid !== rightValid) {
52755
+ return leftValid ? 1 : -1;
52756
+ }
52757
+ const leftRefreshable = isRefreshableOauthCredential(left);
52758
+ const rightRefreshable = isRefreshableOauthCredential(right);
52759
+ if (leftRefreshable !== rightRefreshable) {
52760
+ return leftRefreshable ? 1 : -1;
52761
+ }
52762
+ const leftExpiry = typeof left.expires === "number" && Number.isFinite(left.expires) ? left.expires : -Infinity;
52763
+ const rightExpiry = typeof right.expires === "number" && Number.isFinite(right.expires) ? right.expires : -Infinity;
52764
+ if (leftExpiry !== rightExpiry) {
52765
+ return leftExpiry > rightExpiry ? 1 : -1;
52766
+ }
52767
+ const leftAccessLength = typeof left.access === "string" ? left.access.length : 0;
52768
+ const rightAccessLength = typeof right.access === "string" ? right.access.length : 0;
52769
+ if (leftAccessLength !== rightAccessLength) {
52770
+ return leftAccessLength > rightAccessLength ? 1 : -1;
52771
+ }
52772
+ }
52773
+ return 0;
52774
+ }
52775
+ function choosePreferredStoredCredential(...credentials) {
52776
+ let best;
52777
+ for (const credential of credentials) {
52778
+ if (compareStoredCredentials(credential, best) > 0) {
52779
+ best = credential;
52780
+ }
52781
+ }
52782
+ return best;
52783
+ }
52784
+ function shouldHydrateStoredCredential(current, candidate) {
52785
+ if (!candidate || candidate.type !== "oauth") {
52786
+ return false;
52787
+ }
52788
+ if (current?.type === "api_key") {
52789
+ return false;
52790
+ }
52791
+ return compareStoredCredentials(candidate, current) > 0;
52792
+ }
52793
+ function extractCodexCliStoredCredential(raw) {
52794
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
52795
+ return void 0;
52796
+ }
52797
+ const record = raw;
52798
+ const tokens = record.tokens;
52799
+ if (!tokens || typeof tokens !== "object" || Array.isArray(tokens)) {
52800
+ return void 0;
52801
+ }
52802
+ const tokenRecord = tokens;
52803
+ const access7 = typeof tokenRecord.access_token === "string" ? tokenRecord.access_token : void 0;
52804
+ const refresh = typeof tokenRecord.refresh_token === "string" ? tokenRecord.refresh_token : void 0;
52805
+ if (!access7 || !refresh) {
52806
+ return void 0;
52807
+ }
52808
+ const expires = getJwtExpiryMs(access7) ?? getJwtExpiryMs(typeof tokenRecord.id_token === "string" ? tokenRecord.id_token : void 0) ?? getLastRefreshFallbackExpiryMs(record.last_refresh);
52809
+ if (typeof expires !== "number" || !Number.isFinite(expires)) {
52810
+ return void 0;
52811
+ }
52812
+ const accountId = getCodexAccountId(access7, tokenRecord.account_id);
52813
+ return {
52814
+ type: "oauth",
52815
+ access: access7,
52816
+ refresh,
52817
+ expires,
52818
+ ...accountId ? { accountId } : {}
52819
+ };
52820
+ }
52821
+ function readStoredCredentialsFromAuthFile(authPath) {
52822
+ if (!existsSync19(authPath)) {
52823
+ return {};
52824
+ }
52825
+ try {
52826
+ const parsed = JSON.parse(readFileSync6(authPath, "utf-8"));
52827
+ const codexCliCredential = extractCodexCliStoredCredential(parsed);
52828
+ if (codexCliCredential) {
52829
+ return { "openai-codex": codexCliCredential };
52830
+ }
52831
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
52832
+ return {};
52833
+ }
52834
+ const credentials = {};
52835
+ for (const [providerId, value] of Object.entries(parsed)) {
52836
+ if (!isStoredAuthCredential(value)) {
52837
+ continue;
52838
+ }
52839
+ credentials[providerId] = value;
52840
+ }
52841
+ return credentials;
52842
+ } catch {
52843
+ return {};
52844
+ }
52845
+ }
52846
+ var OPENAI_AUTH_CLAIM, CODEX_REFRESH_FALLBACK_WINDOW_MS;
52847
+ var init_oauth_credential_interop = __esm({
52848
+ "../core/src/oauth-credential-interop.ts"() {
52849
+ "use strict";
52850
+ OPENAI_AUTH_CLAIM = "https://api.openai.com/auth";
52851
+ CODEX_REFRESH_FALLBACK_WINDOW_MS = 55 * 60 * 1e3;
52852
+ }
52853
+ });
52854
+
52660
52855
  // ../core/src/index.ts
52661
52856
  var src_exports = {};
52662
52857
  __export(src_exports, {
@@ -52813,6 +53008,7 @@ __export(src_exports, {
52813
53008
  buildTriageMemoryInstructions: () => buildTriageMemoryInstructions,
52814
53009
  canTransition: () => canTransition,
52815
53010
  checkRateLimit: () => checkRateLimit,
53011
+ choosePreferredStoredCredential: () => choosePreferredStoredCredential,
52816
53012
  classifyInsightRunError: () => classifyInsightRunError,
52817
53013
  clearOverrides: () => clearOverrides,
52818
53014
  collectSystemMetrics: () => collectSystemMetrics,
@@ -52843,6 +53039,7 @@ __export(src_exports, {
52843
53039
  executeInsightRunLifecycle: () => executeInsightRunLifecycle,
52844
53040
  exportAgentsToDirectory: () => exportAgentsToDirectory,
52845
53041
  exportSettings: () => exportSettings,
53042
+ extractCodexCliStoredCredential: () => extractCodexCliStoredCredential,
52846
53043
  extractDreamProcessorResult: () => extractDreamProcessorResult,
52847
53044
  formatPiExtensionSource: () => formatPiExtensionSource,
52848
53045
  fromJson: () => fromJson,
@@ -52853,6 +53050,7 @@ __export(src_exports, {
52853
53050
  generateMemoryAudit: () => generateMemoryAudit,
52854
53051
  getAppVersion: () => getAppVersion,
52855
53052
  getAvailableTemplates: () => getAvailableTemplates,
53053
+ getCodexCliAuthPath: () => getCodexCliAuthPath,
52856
53054
  getCurrentRepo: () => getCurrentRepo,
52857
53055
  getDefaultDailyMemoryScaffold: () => getDefaultDailyMemoryScaffold,
52858
53056
  getDefaultDreamsScaffold: () => getDefaultDreamsScaffold,
@@ -52950,6 +53148,7 @@ __export(src_exports, {
52950
53148
  readProjectMemoryFile: () => readProjectMemoryFile,
52951
53149
  readProjectMemoryFileContent: () => readProjectMemoryFileContent,
52952
53150
  readProjectMemoryWithBackend: () => readProjectMemoryWithBackend,
53151
+ readStoredCredentialsFromAuthFile: () => readStoredCredentialsFromAuthFile,
52953
53152
  readWorkingMemory: () => readWorkingMemory,
52954
53153
  reconcileClaudeCliPaths: () => reconcileClaudeCliPaths,
52955
53154
  reconcileDroidCliPaths: () => reconcileDroidCliPaths,
@@ -52985,6 +53184,7 @@ __export(src_exports, {
52985
53184
  scheduleQmdProjectMemoryRefresh: () => scheduleQmdProjectMemoryRefresh,
52986
53185
  searchProjectMemory: () => searchProjectMemory,
52987
53186
  setCreateFnAgent: () => setCreateFnAgent,
53187
+ shouldHydrateStoredCredential: () => shouldHydrateStoredCredential,
52988
53188
  shouldSkipBackgroundQmdRefresh: () => shouldSkipBackgroundQmdRefresh,
52989
53189
  shouldTriggerExtraction: () => shouldTriggerExtraction,
52990
53190
  slugify: () => slugify,
@@ -53089,6 +53289,7 @@ var init_src = __esm({
53089
53289
  init_agent_companies_parser();
53090
53290
  init_agent_companies_exporter();
53091
53291
  init_chat_store();
53292
+ init_oauth_credential_interop();
53092
53293
  init_error_message();
53093
53294
  }
53094
53295
  });
@@ -54278,12 +54479,12 @@ var init_github_provider = __esm({
54278
54479
  });
54279
54480
 
54280
54481
  // ../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";
54482
+ import { existsSync as existsSync20, readFileSync as readFileSync7 } from "node:fs";
54483
+ import { dirname as dirname8, join as join23, resolve as resolve11 } from "node:path";
54283
54484
  function resolveProjectRoot(cwd) {
54284
54485
  let current = resolve11(cwd);
54285
54486
  while (true) {
54286
- if (existsSync19(join22(current, ".fusion"))) {
54487
+ if (existsSync20(join23(current, ".fusion"))) {
54287
54488
  return current;
54288
54489
  }
54289
54490
  const parent = dirname8(current);
@@ -54294,19 +54495,19 @@ function resolveProjectRoot(cwd) {
54294
54495
  }
54295
54496
  }
54296
54497
  function readJsonObject(path2) {
54297
- if (!existsSync19(path2)) {
54498
+ if (!existsSync20(path2)) {
54298
54499
  return {};
54299
54500
  }
54300
54501
  try {
54301
- const parsed = JSON.parse(readFileSync6(path2, "utf-8"));
54502
+ const parsed = JSON.parse(readFileSync7(path2, "utf-8"));
54302
54503
  return parsed && typeof parsed === "object" ? parsed : {};
54303
54504
  } catch {
54304
54505
  return {};
54305
54506
  }
54306
54507
  }
54307
54508
  function readProjectSettings(projectRootDir) {
54308
- const fusionSettings = join22(projectRootDir, ".fusion", "settings.json");
54309
- if (existsSync19(fusionSettings)) {
54509
+ const fusionSettings = join23(projectRootDir, ".fusion", "settings.json");
54510
+ if (existsSync20(fusionSettings)) {
54310
54511
  const parsed = readJsonObject(fusionSettings);
54311
54512
  return {
54312
54513
  skills: Array.isArray(parsed.skills) ? parsed.skills : void 0,
@@ -54531,51 +54732,51 @@ var init_context_limit_detector = __esm({
54531
54732
  });
54532
54733
 
54533
54734
  // ../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";
54735
+ import { existsSync as existsSync21, readFileSync as readFileSync8 } from "node:fs";
54736
+ import { homedir as homedir5 } from "node:os";
54737
+ import { join as join24 } from "node:path";
54537
54738
  import { AuthStorage } from "@mariozechner/pi-coding-agent";
54538
54739
  import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
54539
- function getHomeDir4() {
54540
- return process.env.HOME || process.env.USERPROFILE || homedir4();
54740
+ function getHomeDir5() {
54741
+ return process.env.HOME || process.env.USERPROFILE || homedir5();
54742
+ }
54743
+ function getFusionAuthPath(home = getHomeDir5()) {
54744
+ return join24(home, ".fusion", "agent", "auth.json");
54541
54745
  }
54542
- function getFusionAuthPath(home = getHomeDir4()) {
54543
- return join23(home, ".fusion", "agent", "auth.json");
54746
+ function getFusionModelsPath(home = getHomeDir5()) {
54747
+ return join24(home, ".fusion", "agent", "models.json");
54544
54748
  }
54545
- function getFusionModelsPath(home = getHomeDir4()) {
54546
- return join23(home, ".fusion", "agent", "models.json");
54749
+ function getLegacyAuthPaths(home = getHomeDir5()) {
54750
+ return [
54751
+ join24(home, ".pi", "agent", "auth.json"),
54752
+ join24(home, ".pi", "auth.json")
54753
+ ];
54547
54754
  }
54548
- function getLegacyAuthPaths(home = getHomeDir4()) {
54755
+ function getSupplementalAuthPaths(home = getHomeDir5()) {
54549
54756
  return [
54550
- join23(home, ".pi", "agent", "auth.json"),
54551
- join23(home, ".pi", "auth.json")
54757
+ ...getLegacyAuthPaths(home),
54758
+ getCodexCliAuthPath(home)
54552
54759
  ];
54553
54760
  }
54554
- function getLegacyModelsPaths(home = getHomeDir4()) {
54761
+ function getLegacyModelsPaths(home = getHomeDir5()) {
54555
54762
  return [
54556
- join23(home, ".pi", "agent", "models.json"),
54557
- join23(home, ".pi", "models.json")
54763
+ join24(home, ".pi", "agent", "models.json"),
54764
+ join24(home, ".pi", "models.json")
54558
54765
  ];
54559
54766
  }
54560
- function getModelRegistryModelsPath(home = getHomeDir4()) {
54767
+ function getModelRegistryModelsPath(home = getHomeDir5()) {
54561
54768
  const fusionModelsPath = getFusionModelsPath(home);
54562
- if (existsSync20(fusionModelsPath)) {
54769
+ if (existsSync21(fusionModelsPath)) {
54563
54770
  return fusionModelsPath;
54564
54771
  }
54565
- return getLegacyModelsPaths(home).find((modelsPath) => existsSync20(modelsPath)) ?? fusionModelsPath;
54772
+ return getLegacyModelsPaths(home).find((modelsPath) => existsSync21(modelsPath)) ?? fusionModelsPath;
54566
54773
  }
54567
- function readLegacyCredentials(authPaths = getLegacyAuthPaths()) {
54774
+ function readSupplementalCredentials(authPaths = getSupplementalAuthPaths()) {
54568
54775
  const credentials = {};
54569
54776
  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 {
54777
+ const parsed = readStoredCredentialsFromAuthFile(authPath);
54778
+ for (const [provider, credential] of Object.entries(parsed)) {
54779
+ credentials[provider] = choosePreferredStoredCredential(credentials[provider], credential) ?? credential;
54579
54780
  }
54580
54781
  }
54581
54782
  return credentials;
@@ -54599,14 +54800,14 @@ function resolveStoredCredentialApiKey(providerId, credential) {
54599
54800
  }
54600
54801
  return void 0;
54601
54802
  }
54602
- function readModelsJsonApiKeys(home = getHomeDir4()) {
54803
+ function readModelsJsonApiKeys(home = getHomeDir5()) {
54603
54804
  const apiKeys = /* @__PURE__ */ new Map();
54604
54805
  const modelsPath = getModelRegistryModelsPath(home);
54605
- if (!existsSync20(modelsPath)) {
54806
+ if (!existsSync21(modelsPath)) {
54606
54807
  return apiKeys;
54607
54808
  }
54608
54809
  try {
54609
- const parsed = JSON.parse(readFileSync7(modelsPath, "utf-8"));
54810
+ const parsed = JSON.parse(readFileSync8(modelsPath, "utf-8"));
54610
54811
  const providers = parsed?.providers;
54611
54812
  if (providers) {
54612
54813
  for (const [providerId, config] of Object.entries(providers)) {
@@ -54621,8 +54822,20 @@ function readModelsJsonApiKeys(home = getHomeDir4()) {
54621
54822
  }
54622
54823
  function createFusionAuthStorage() {
54623
54824
  const primary = AuthStorage.create(getFusionAuthPath());
54624
- let legacyCredentials = readLegacyCredentials();
54825
+ let supplementalCredentials = readSupplementalCredentials();
54625
54826
  let modelsJsonApiKeys = readModelsJsonApiKeys();
54827
+ const syncSupplementalOauthCredentials = () => {
54828
+ for (const [provider, credential] of Object.entries(supplementalCredentials)) {
54829
+ const current = primary.get(provider);
54830
+ if (!shouldHydrateStoredCredential(current, credential)) {
54831
+ continue;
54832
+ }
54833
+ if (credential.type === "oauth" || credential.type === "api_key") {
54834
+ primary.set(provider, credential);
54835
+ }
54836
+ }
54837
+ };
54838
+ syncSupplementalOauthCredentials();
54626
54839
  return new Proxy(primary, {
54627
54840
  // Forward property writes to the target so that methods like
54628
54841
  // `setFallbackResolver` (called by ModelRegistry) correctly update the
@@ -54636,31 +54849,51 @@ function createFusionAuthStorage() {
54636
54849
  if (prop === "reload") {
54637
54850
  return () => {
54638
54851
  target.reload();
54639
- legacyCredentials = readLegacyCredentials();
54852
+ supplementalCredentials = readSupplementalCredentials();
54853
+ syncSupplementalOauthCredentials();
54640
54854
  modelsJsonApiKeys = readModelsJsonApiKeys();
54641
54855
  };
54642
54856
  }
54643
54857
  if (prop === "get") {
54644
- return (provider) => target.get(provider) ?? legacyCredentials[provider];
54858
+ return (provider) => choosePreferredStoredCredential(
54859
+ target.get(provider),
54860
+ supplementalCredentials[provider]
54861
+ );
54645
54862
  }
54646
54863
  if (prop === "has") {
54647
- return (provider) => target.has(provider) || provider in legacyCredentials || modelsJsonApiKeys.has(provider);
54864
+ return (provider) => target.has(provider) || provider in supplementalCredentials || modelsJsonApiKeys.has(provider);
54648
54865
  }
54649
54866
  if (prop === "hasAuth") {
54650
- return (provider) => target.hasAuth(provider) || Boolean(legacyCredentials[provider]) || modelsJsonApiKeys.has(provider);
54867
+ return (provider) => target.hasAuth(provider) || Boolean(supplementalCredentials[provider]) || modelsJsonApiKeys.has(provider);
54651
54868
  }
54652
54869
  if (prop === "getAll") {
54653
- return () => ({ ...legacyCredentials, ...target.getAll() });
54870
+ return () => {
54871
+ const providerIds = /* @__PURE__ */ new Set([
54872
+ ...Object.keys(supplementalCredentials),
54873
+ ...Object.keys(target.getAll())
54874
+ ]);
54875
+ const merged = {};
54876
+ for (const providerId of providerIds) {
54877
+ const credential = choosePreferredStoredCredential(
54878
+ target.get(providerId),
54879
+ supplementalCredentials[providerId]
54880
+ );
54881
+ if (credential) {
54882
+ merged[providerId] = credential;
54883
+ }
54884
+ }
54885
+ return merged;
54886
+ };
54654
54887
  }
54655
54888
  if (prop === "list") {
54656
- return () => Array.from(/* @__PURE__ */ new Set([...Object.keys(legacyCredentials), ...target.list(), ...modelsJsonApiKeys.keys()]));
54889
+ return () => Array.from(/* @__PURE__ */ new Set([...Object.keys(supplementalCredentials), ...target.list(), ...modelsJsonApiKeys.keys()]));
54657
54890
  }
54658
54891
  if (prop === "getApiKey") {
54659
54892
  return async (provider) => {
54660
54893
  const primaryKey = await target.getApiKey(provider);
54661
54894
  if (primaryKey) return primaryKey;
54662
- const legacyKey = resolveStoredCredentialApiKey(provider, legacyCredentials[provider]);
54663
- if (legacyKey) return legacyKey;
54895
+ const supplementalKey = resolveStoredCredentialApiKey(provider, supplementalCredentials[provider]);
54896
+ if (supplementalKey) return supplementalKey;
54664
54897
  return modelsJsonApiKeys.get(provider);
54665
54898
  };
54666
54899
  }
@@ -54671,17 +54904,18 @@ function createFusionAuthStorage() {
54671
54904
  var init_auth_storage = __esm({
54672
54905
  "../engine/src/auth-storage.ts"() {
54673
54906
  "use strict";
54907
+ init_src();
54674
54908
  }
54675
54909
  });
54676
54910
 
54677
54911
  // ../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";
54912
+ import { readFileSync as readFileSync9 } from "node:fs";
54913
+ import { homedir as homedir6 } from "node:os";
54914
+ import { join as join25 } from "node:path";
54681
54915
  function readCustomProviders() {
54682
54916
  try {
54683
- const settingsPath = join24(homedir5(), ".fusion", "settings.json");
54684
- const raw = readFileSync8(settingsPath, "utf-8");
54917
+ const settingsPath = join25(homedir6(), ".fusion", "settings.json");
54918
+ const raw = readFileSync9(settingsPath, "utf-8");
54685
54919
  const parsed = JSON.parse(raw);
54686
54920
  return Array.isArray(parsed.customProviders) ? parsed.customProviders : [];
54687
54921
  } catch {
@@ -54695,11 +54929,11 @@ var init_custom_providers = __esm({
54695
54929
  });
54696
54930
 
54697
54931
  // ../engine/src/pi.ts
54698
- import { existsSync as existsSync21, readFileSync as readFileSync9 } from "node:fs";
54932
+ import { existsSync as existsSync22, readFileSync as readFileSync10 } from "node:fs";
54699
54933
  import { exec as exec2 } from "node:child_process";
54700
54934
  import { promisify as promisify3 } from "node:util";
54701
54935
  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";
54936
+ import { basename as basename7, dirname as dirname9, join as join26, relative as relative3, isAbsolute as isAbsolute7, resolve as resolve12 } from "node:path";
54703
54937
  import {
54704
54938
  createAgentSession,
54705
54939
  createBashTool,
@@ -54953,11 +55187,11 @@ function isRetryableModelSelectionError(message) {
54953
55187
  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
55188
  }
54955
55189
  function readJsonObject2(path2) {
54956
- if (!existsSync21(path2)) {
55190
+ if (!existsSync22(path2)) {
54957
55191
  return {};
54958
55192
  }
54959
55193
  try {
54960
- const parsed = JSON.parse(readFileSync9(path2, "utf-8"));
55194
+ const parsed = JSON.parse(readFileSync10(path2, "utf-8"));
54961
55195
  return parsed && typeof parsed === "object" ? parsed : {};
54962
55196
  } catch {
54963
55197
  return {};
@@ -55094,17 +55328,17 @@ function siblingAgentDir(agentDir, siblingRoot) {
55094
55328
  if (basename7(agentDir) !== "agent") {
55095
55329
  return void 0;
55096
55330
  }
55097
- return join25(dirname9(dirname9(agentDir)), siblingRoot, "agent");
55331
+ return join26(dirname9(dirname9(agentDir)), siblingRoot, "agent");
55098
55332
  }
55099
55333
  function createReadOnlyPiSettingsView(cwd, agentDir) {
55100
55334
  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"));
55335
+ const fusionAgentDir = agentDir.includes(`${join26(".fusion", "agent")}`) ? agentDir : siblingAgentDir(agentDir, ".fusion");
55336
+ const legacyAgentDir = agentDir.includes(`${join26(".pi", "agent")}`) ? agentDir : siblingAgentDir(agentDir, ".pi");
55337
+ const legacyGlobalSettings = legacyAgentDir ? readJsonObject2(join26(legacyAgentDir, "settings.json")) : {};
55338
+ const fusionGlobalSettings = fusionAgentDir ? readJsonObject2(join26(fusionAgentDir, "settings.json")) : {};
55339
+ const directGlobalSettings = readJsonObject2(join26(agentDir, "settings.json"));
55106
55340
  const globalSettings = { ...legacyGlobalSettings, ...directGlobalSettings, ...fusionGlobalSettings };
55107
- const fusionProjectSettings = readJsonObject2(join25(projectRoot, ".fusion", "settings.json"));
55341
+ const fusionProjectSettings = readJsonObject2(join26(projectRoot, ".fusion", "settings.json"));
55108
55342
  const mergedSettings = { ...globalSettings, ...fusionProjectSettings };
55109
55343
  return {
55110
55344
  getGlobalSettings: () => structuredClone(globalSettings),
@@ -55115,27 +55349,27 @@ function createReadOnlyPiSettingsView(cwd, agentDir) {
55115
55349
  function getPackageManagerAgentDir() {
55116
55350
  const fusionAgentDir = getFusionAgentDir();
55117
55351
  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)) {
55352
+ const fusionSettings = readJsonObject2(join26(fusionAgentDir, "settings.json"));
55353
+ const legacySettings = readJsonObject2(join26(legacyAgentDir, "settings.json"));
55354
+ if (hasPackageManagerSettings(fusionSettings) || !existsSync22(legacyAgentDir)) {
55121
55355
  return fusionAgentDir;
55122
55356
  }
55123
55357
  if (hasPackageManagerSettings(legacySettings)) {
55124
55358
  return legacyAgentDir;
55125
55359
  }
55126
- return existsSync21(fusionAgentDir) ? fusionAgentDir : legacyAgentDir;
55360
+ return existsSync22(fusionAgentDir) ? fusionAgentDir : legacyAgentDir;
55127
55361
  }
55128
55362
  function resolveVendoredClaudeCliEntry() {
55129
55363
  try {
55130
55364
  const require_ = createRequire2(import.meta.url);
55131
55365
  const pkgJsonPath = require_.resolve("@fusion/pi-claude-cli/package.json");
55132
- const pkgJson = JSON.parse(readFileSync9(pkgJsonPath, "utf-8"));
55366
+ const pkgJson = JSON.parse(readFileSync10(pkgJsonPath, "utf-8"));
55133
55367
  const extensions = pkgJson.pi?.extensions;
55134
55368
  if (!Array.isArray(extensions) || extensions.length === 0) return null;
55135
55369
  const entry = extensions[0];
55136
55370
  if (typeof entry !== "string" || entry.length === 0) return null;
55137
55371
  const path2 = resolve12(dirname9(pkgJsonPath), entry);
55138
- return existsSync21(path2) ? path2 : null;
55372
+ return existsSync22(path2) ? path2 : null;
55139
55373
  } catch {
55140
55374
  return null;
55141
55375
  }
@@ -55144,13 +55378,13 @@ function resolveVendoredDroidCliEntry() {
55144
55378
  try {
55145
55379
  const require_ = createRequire2(import.meta.url);
55146
55380
  const pkgJsonPath = require_.resolve("@fusion/droid-cli/package.json");
55147
- const pkgJson = JSON.parse(readFileSync9(pkgJsonPath, "utf-8"));
55381
+ const pkgJson = JSON.parse(readFileSync10(pkgJsonPath, "utf-8"));
55148
55382
  const extensions = pkgJson.pi?.extensions;
55149
55383
  if (!Array.isArray(extensions) || extensions.length === 0) return null;
55150
55384
  const entry = extensions[0];
55151
55385
  if (typeof entry !== "string" || entry.length === 0) return null;
55152
55386
  const path2 = resolve12(dirname9(pkgJsonPath), entry);
55153
- return existsSync21(path2) ? path2 : null;
55387
+ return existsSync22(path2) ? path2 : null;
55154
55388
  } catch {
55155
55389
  return null;
55156
55390
  }
@@ -55178,7 +55412,7 @@ async function registerExtensionProviders(cwd, modelRegistry) {
55178
55412
  const extensionsResult = await discoverAndLoadExtensions(
55179
55413
  doubleReconciledPaths,
55180
55414
  cwd,
55181
- join25(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery")
55415
+ join26(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery")
55182
55416
  );
55183
55417
  for (const { path: path2, error } of extensionsResult.errors) {
55184
55418
  extensionsLog.warn(`Failed to load ${path2}: ${error}`);
@@ -55233,10 +55467,10 @@ async function isCompleteGitWorktree(worktreePath) {
55233
55467
  }
55234
55468
  }
55235
55469
  async function assertValidWorktreeSession(cwd, projectRoot) {
55236
- if (!existsSync21(cwd)) {
55470
+ if (!existsSync22(cwd)) {
55237
55471
  throw new Error(`Refusing to start coding agent in missing worktree: ${cwd}`);
55238
55472
  }
55239
- if (!existsSync21(join25(cwd, ".git")) || !await isCompleteGitWorktree(cwd)) {
55473
+ if (!existsSync22(join26(cwd, ".git")) || !await isCompleteGitWorktree(cwd)) {
55240
55474
  throw new Error(`Refusing to start coding agent in incomplete worktree: ${cwd}`);
55241
55475
  }
55242
55476
  if (!await isRegisteredGitWorktree(projectRoot, cwd)) {
@@ -55817,7 +56051,7 @@ ${source.content ?? ""}`;
55817
56051
 
55818
56052
  // ../engine/src/research/providers/local-docs-provider.ts
55819
56053
  import { promises as fs } from "node:fs";
55820
- import { extname as extname2, join as join26, relative as relative4, resolve as resolve13 } from "node:path";
56054
+ import { extname as extname2, join as join27, relative as relative4, resolve as resolve13 } from "node:path";
55821
56055
  function buildExcerpt(content, terms) {
55822
56056
  const lower = content.toLowerCase();
55823
56057
  const first = terms.find((term) => lower.includes(term));
@@ -55946,7 +56180,7 @@ var init_local_docs_provider = __esm({
55946
56180
  const rootEntries = await fs.readdir(this.projectRoot, { withFileTypes: true });
55947
56181
  for (const entry of rootEntries) {
55948
56182
  if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
55949
- files.push(join26(this.projectRoot, entry.name));
56183
+ files.push(join27(this.projectRoot, entry.name));
55950
56184
  }
55951
56185
  }
55952
56186
  return [...new Set(files)];
@@ -55956,7 +56190,7 @@ var init_local_docs_provider = __esm({
55956
56190
  const entries = await fs.readdir(dir, { withFileTypes: true });
55957
56191
  for (const entry of entries) {
55958
56192
  this.throwIfAborted(signal);
55959
- const fullPath = join26(dir, entry.name);
56193
+ const fullPath = join27(dir, entry.name);
55960
56194
  const relPath = relative4(this.projectRoot, fullPath).replace(/\\/g, "/");
55961
56195
  if (matchesGitignore(relPath, ignorePatterns)) continue;
55962
56196
  if (entry.isDirectory()) {
@@ -55981,7 +56215,7 @@ var init_local_docs_provider = __esm({
55981
56215
  }
55982
56216
  async readGitignore() {
55983
56217
  try {
55984
- const content = await fs.readFile(join26(this.projectRoot, ".gitignore"), "utf-8");
56218
+ const content = await fs.readFile(join27(this.projectRoot, ".gitignore"), "utf-8");
55985
56219
  return content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
55986
56220
  } catch {
55987
56221
  return [];
@@ -56591,9 +56825,9 @@ var init_research_step_runner = __esm({
56591
56825
 
56592
56826
  // ../engine/src/agent-tools.ts
56593
56827
  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";
56828
+ import { existsSync as existsSync23 } from "node:fs";
56595
56829
  import { createHash as createHash4 } from "node:crypto";
56596
- import { join as join27 } from "node:path";
56830
+ import { join as join28 } from "node:path";
56597
56831
  import { Type } from "@mariozechner/pi-ai";
56598
56832
  function sanitizeAgentMemoryId(agentId) {
56599
56833
  return agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
@@ -56605,16 +56839,16 @@ function agentDreamsDisplayPath(agentId) {
56605
56839
  return `${AGENT_MEMORY_ROOT2}/${sanitizeAgentMemoryId(agentId)}/${AGENT_DREAMS_FILENAME2}`;
56606
56840
  }
56607
56841
  function agentMemoryDirectory(rootDir, agentId) {
56608
- return join27(rootDir, AGENT_MEMORY_ROOT2, sanitizeAgentMemoryId(agentId));
56842
+ return join28(rootDir, AGENT_MEMORY_ROOT2, sanitizeAgentMemoryId(agentId));
56609
56843
  }
56610
56844
  function agentMemoryFilePath(rootDir, agentId) {
56611
- return join27(agentMemoryDirectory(rootDir, agentId), AGENT_MEMORY_FILENAME2);
56845
+ return join28(agentMemoryDirectory(rootDir, agentId), AGENT_MEMORY_FILENAME2);
56612
56846
  }
56613
56847
  function agentDreamsFilePath(rootDir, agentId) {
56614
- return join27(agentMemoryDirectory(rootDir, agentId), AGENT_DREAMS_FILENAME2);
56848
+ return join28(agentMemoryDirectory(rootDir, agentId), AGENT_DREAMS_FILENAME2);
56615
56849
  }
56616
56850
  function agentDailyFilePath(rootDir, agentId, date = /* @__PURE__ */ new Date()) {
56617
- return join27(agentMemoryDirectory(rootDir, agentId), `${date.toISOString().slice(0, 10)}.md`);
56851
+ return join28(agentMemoryDirectory(rootDir, agentId), `${date.toISOString().slice(0, 10)}.md`);
56618
56852
  }
56619
56853
  function qmdAgentMemoryCollectionName(rootDir, agentId) {
56620
56854
  const hash = createHash4("sha1").update(`${rootDir}:${agentId}`).digest("hex").slice(0, 12);
@@ -56650,7 +56884,7 @@ async function syncAgentMemoryFile(rootDir, agentMemory) {
56650
56884
  const dir = agentMemoryDirectory(rootDir, agentMemory.agentId);
56651
56885
  await mkdir11(dir, { recursive: true });
56652
56886
  const longTermPath = agentMemoryFilePath(rootDir, agentMemory.agentId);
56653
- if (!existsSync22(longTermPath)) {
56887
+ if (!existsSync23(longTermPath)) {
56654
56888
  const title = agentMemory.agentName?.trim() ? `# Agent Memory: ${agentMemory.agentName.trim()}` : "# Agent Memory";
56655
56889
  const fileContent = `${title}
56656
56890
 
@@ -56661,11 +56895,11 @@ ${content || ""}
56661
56895
  await writeFile10(longTermPath, fileContent, "utf-8");
56662
56896
  }
56663
56897
  const dreamsPath = agentDreamsFilePath(rootDir, agentMemory.agentId);
56664
- if (!existsSync22(dreamsPath)) {
56898
+ if (!existsSync23(dreamsPath)) {
56665
56899
  await writeFile10(dreamsPath, "# Agent Memory Dreams\n\n<!-- Synthesized patterns from this agent's daily notes. -->\n", "utf-8");
56666
56900
  }
56667
56901
  const dailyPath = agentDailyFilePath(rootDir, agentMemory.agentId);
56668
- if (!existsSync22(dailyPath)) {
56902
+ if (!existsSync23(dailyPath)) {
56669
56903
  await writeFile10(dailyPath, `# Agent Daily Memory ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
56670
56904
 
56671
56905
  <!-- Running observations for this agent. -->
@@ -56689,7 +56923,7 @@ async function listAgentMemoryFiles2(rootDir, agentMemory) {
56689
56923
  }
56690
56924
  for (const entry of entries) {
56691
56925
  if (!DAILY_AGENT_MEMORY_RE2.test(entry)) continue;
56692
- const absPath = join27(dir, entry);
56926
+ const absPath = join28(dir, entry);
56693
56927
  const fileStat = await stat4(absPath);
56694
56928
  if (fileStat.isFile()) {
56695
56929
  files.push({
@@ -56851,7 +57085,7 @@ function resolveAgentMemoryPath(rootDir, agentId, path2) {
56851
57085
  return null;
56852
57086
  }
56853
57087
  return {
56854
- absPath: join27(agentMemoryDirectory(rootDir, agentId), filename),
57088
+ absPath: join28(agentMemoryDirectory(rootDir, agentId), filename),
56855
57089
  displayPath: `${prefix}${filename}`
56856
57090
  };
56857
57091
  }
@@ -59148,6 +59382,38 @@ var init_notifier = __esm({
59148
59382
  }
59149
59383
  });
59150
59384
 
59385
+ // ../engine/src/fallback-model-observer.ts
59386
+ function buildFallbackLogMessage(label, payload) {
59387
+ return `[fallback] ${label} switched from ${payload.primaryModel} to ${payload.fallbackModel} (${payload.triggerPoint})`;
59388
+ }
59389
+ function createFallbackModelObserver(options) {
59390
+ return async (payload) => {
59391
+ const taskId = options.taskId ?? payload.taskId;
59392
+ const taskTitle = options.taskTitle ?? payload.taskTitle;
59393
+ const message = buildFallbackLogMessage(options.label, payload);
59394
+ if (taskId && options.store?.logEntry) {
59395
+ await options.store.logEntry(taskId, message).catch(() => void 0);
59396
+ }
59397
+ if (taskId && options.store?.appendAgentLog) {
59398
+ await options.store.appendAgentLog(taskId, message, "text", void 0, options.agent).catch(() => void 0);
59399
+ }
59400
+ await notifyFallbackUsed({
59401
+ primaryModel: payload.primaryModel,
59402
+ fallbackModel: payload.fallbackModel,
59403
+ triggerPoint: payload.triggerPoint,
59404
+ taskId,
59405
+ taskTitle,
59406
+ timestamp: payload.timestamp
59407
+ });
59408
+ };
59409
+ }
59410
+ var init_fallback_model_observer = __esm({
59411
+ "../engine/src/fallback-model-observer.ts"() {
59412
+ "use strict";
59413
+ init_notifier();
59414
+ }
59415
+ });
59416
+
59151
59417
  // ../engine/src/reviewer.ts
59152
59418
  async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptContent, baseline, options = {}) {
59153
59419
  let liveSettings = options.settings;
@@ -59278,7 +59544,13 @@ async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptC
59278
59544
  ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
59279
59545
  taskId: options.taskId,
59280
59546
  taskTitle: options.taskTitle,
59281
- onFallbackModelUsed: notifyFallbackUsed,
59547
+ onFallbackModelUsed: createFallbackModelObserver({
59548
+ agent: "reviewer",
59549
+ label: "reviewer",
59550
+ store: options.store,
59551
+ taskId: options.taskId,
59552
+ taskTitle: options.taskTitle
59553
+ }),
59282
59554
  beforeSpawnSession: async () => {
59283
59555
  if (!options.store) return;
59284
59556
  let finalSettings;
@@ -59465,7 +59737,7 @@ var init_reviewer = __esm({
59465
59737
  init_logger2();
59466
59738
  init_usage_limit_detector();
59467
59739
  init_agent_instructions();
59468
- init_notifier();
59740
+ init_fallback_model_observer();
59469
59741
  init_agent_tools();
59470
59742
  REVIEWER_SYSTEM_PROMPT = `You are an independent code and plan reviewer.
59471
59743
 
@@ -59826,7 +60098,7 @@ var init_recovery_policy = __esm({
59826
60098
  // ../engine/src/triage.ts
59827
60099
  import { Type as Type2 } from "@mariozechner/pi-ai";
59828
60100
  import { readFile as readFile14 } from "node:fs/promises";
59829
- import { join as join28 } from "node:path";
60101
+ import { join as join29 } from "node:path";
59830
60102
  function extractPromptDeclaredTitle(prompt, taskId) {
59831
60103
  const headingMatch = prompt.match(/^#\s+Task:\s+([A-Z]+-\d+)\s+-\s+(.+)$/m);
59832
60104
  if (!headingMatch) return null;
@@ -59855,9 +60127,9 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
59855
60127
  return { attachmentContents, imageContents };
59856
60128
  }
59857
60129
  const { readFile: readFile20 } = await import("node:fs/promises");
59858
- const { join: join42 } = await import("node:path");
60130
+ const { join: join43 } = await import("node:path");
59859
60131
  for (const att of attachments) {
59860
- const filePath = join42(
60132
+ const filePath = join43(
59861
60133
  rootDir,
59862
60134
  ".fusion",
59863
60135
  "tasks",
@@ -60082,7 +60354,7 @@ var init_triage = __esm({
60082
60354
  init_concurrency();
60083
60355
  init_agent_logger();
60084
60356
  init_agent_instructions();
60085
- init_notifier();
60357
+ init_fallback_model_observer();
60086
60358
  init_logger2();
60087
60359
  init_usage_limit_detector();
60088
60360
  init_transient_error_detector();
@@ -60692,7 +60964,7 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
60692
60964
  return false;
60693
60965
  }
60694
60966
  const settings = await this.store.getSettings();
60695
- const promptPath = join28(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
60967
+ const promptPath = join29(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
60696
60968
  const written = await readFile14(promptPath, "utf-8").catch((err) => {
60697
60969
  const msg = err instanceof Error ? err.message : String(err);
60698
60970
  planLog.warn(`${task.id}: failed to read PROMPT.md during approved-spec recovery (${promptPath}): ${msg}`);
@@ -60922,7 +61194,13 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
60922
61194
  ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
60923
61195
  taskId: task.id,
60924
61196
  taskTitle: task.title,
60925
- onFallbackModelUsed: notifyFallbackUsed
61197
+ onFallbackModelUsed: createFallbackModelObserver({
61198
+ agent: "triage",
61199
+ label: "triage",
61200
+ store: this.store,
61201
+ taskId: task.id,
61202
+ taskTitle: task.title
61203
+ })
60926
61204
  });
60927
61205
  const modelDesc = describeModel(session);
60928
61206
  planLog.log(`${task.id}: using model ${modelDesc}`);
@@ -61065,7 +61343,13 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
61065
61343
  ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
61066
61344
  taskId: task.id,
61067
61345
  taskTitle: task.title,
61068
- onFallbackModelUsed: notifyFallbackUsed
61346
+ onFallbackModelUsed: createFallbackModelObserver({
61347
+ agent: "triage",
61348
+ label: "triage",
61349
+ store: this.store,
61350
+ taskId: task.id,
61351
+ taskTitle: task.title
61352
+ })
61069
61353
  });
61070
61354
  session = fallbackResult.session;
61071
61355
  const fallbackModelDesc = describeModel(session);
@@ -61150,7 +61434,7 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
61150
61434
  return;
61151
61435
  }
61152
61436
  const written = await readFile14(
61153
- join28(this.rootDir, promptPath),
61437
+ join29(this.rootDir, promptPath),
61154
61438
  "utf-8"
61155
61439
  ).catch((err) => {
61156
61440
  const msg = err instanceof Error ? err.message : String(err);
@@ -61469,9 +61753,9 @@ Remove or replace these ids and call fn_task_create again.`
61469
61753
  }
61470
61754
  try {
61471
61755
  const { readFile: readFile20 } = await import("node:fs/promises");
61472
- const { join: join42 } = await import("node:path");
61756
+ const { join: join43 } = await import("node:path");
61473
61757
  const promptContent = await readFile20(
61474
- join42(rootDir, promptPath),
61758
+ join43(rootDir, promptPath),
61475
61759
  "utf-8"
61476
61760
  ).catch((err) => {
61477
61761
  const msg = err instanceof Error ? err.message : String(err);
@@ -61699,160 +61983,8 @@ Take a completely different approach to writing this specification. Do NOT repea
61699
61983
  }
61700
61984
  });
61701
61985
 
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";
61986
+ // ../engine/src/verification-utils.ts
61987
+ import { spawn as spawn3 } from "node:child_process";
61856
61988
  async function execWithProcessGroup(command, options) {
61857
61989
  return new Promise((resolve20, reject) => {
61858
61990
  if (options.signal?.aborted) {
@@ -61964,6 +62096,11 @@ function truncateWithEllipsis(text, maxChars) {
61964
62096
  return `${text.slice(0, maxChars)}
61965
62097
  ... (truncated)`;
61966
62098
  }
62099
+ function truncateOutput(output) {
62100
+ if (output.length <= VERIFICATION_LOG_MAX_CHARS) return output;
62101
+ return `... output truncated to last ${VERIFICATION_LOG_MAX_CHARS} characters ...
62102
+ ${output.slice(-VERIFICATION_LOG_MAX_CHARS)}`;
62103
+ }
61967
62104
  function summarizeVerificationOutput(output, type) {
61968
62105
  const lines = output.split("\n");
61969
62106
  let summaryLine = null;
@@ -62029,6 +62166,13 @@ function summarizeVerificationOutput(output, type) {
62029
62166
  failureNames.add(truncated);
62030
62167
  }
62031
62168
  const footer = "(full output available in engine logs)";
62169
+ if (type === "build") {
62170
+ const buildError = output.length > 500 ? `${output.slice(0, 500)}
62171
+ ... (truncated)` : output;
62172
+ return `Build output:
62173
+ ${buildError}
62174
+ ${footer}`;
62175
+ }
62032
62176
  const parts = [];
62033
62177
  if (summaryLine) {
62034
62178
  parts.push(summaryLine);
@@ -62046,29 +62190,292 @@ function summarizeVerificationOutput(output, type) {
62046
62190
  parts.push(` \u2022 ... and ${names.length - 5} more failures`);
62047
62191
  }
62048
62192
  }
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
62193
+ if (parts.length === 0) {
62194
+ if (output.trim().length === 0) {
62195
+ return `no output
62196
+ ${footer}`;
62197
+ }
62198
+ return `${truncateOutput(output)}
62056
62199
  ${footer}`;
62057
62200
  }
62058
- if (trimmed.length <= 500) {
62059
- return `${trimmed}
62201
+ return parts.join("\n") + `
62060
62202
  ${footer}`;
62203
+ }
62204
+ async function runVerificationCommand(store, rootDir, taskId, command, type, signal, log18, agentLabel) {
62205
+ const logger2 = log18 ?? { log: console.log, error: console.error, warn: console.warn };
62206
+ const label = agentLabel ?? "merger";
62207
+ if (signal?.aborted) {
62208
+ throw Object.assign(
62209
+ new Error(`Command aborted before start: ${command}`),
62210
+ { code: "ABORT_ERR", aborted: true }
62211
+ );
62061
62212
  }
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;
62213
+ logger2.log(`${taskId}: running ${type} command: ${command}`);
62214
+ await store.logEntry(taskId, `[verification] Running ${type} command: ${command}`);
62215
+ await store.appendAgentLog(taskId, `Running ${type} command`, "tool", command, label);
62216
+ const result = {
62217
+ command,
62218
+ exitCode: null,
62219
+ stdout: "",
62220
+ stderr: "",
62221
+ success: false
62222
+ };
62223
+ const verificationStartedAt = Date.now();
62224
+ try {
62225
+ const { stdout, stderr, bufferOverflow } = await execWithProcessGroup(command, {
62226
+ cwd: rootDir,
62227
+ timeout: VERIFICATION_COMMAND_TIMEOUT_MS,
62228
+ maxBuffer: VERIFICATION_COMMAND_MAX_BUFFER,
62229
+ signal
62230
+ });
62231
+ if (signal?.aborted) {
62232
+ throw Object.assign(
62233
+ new Error(`Command aborted: ${command}`),
62234
+ { code: "ABORT_ERR", aborted: true }
62235
+ );
62236
+ }
62237
+ result.stdout = stdout?.toString?.() || "";
62238
+ result.stderr = stderr?.toString?.() || "";
62239
+ result.exitCode = 0;
62240
+ result.success = true;
62241
+ const verificationDurationMs = Date.now() - verificationStartedAt;
62242
+ const timingDetail = `${verificationDurationMs}ms`;
62243
+ if (bufferOverflow) {
62244
+ logger2.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
62245
+ await store.logEntry(
62246
+ taskId,
62247
+ `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
62248
+ );
62249
+ await store.appendAgentLog(
62250
+ taskId,
62251
+ `${type} command succeeded (exit 0)`,
62252
+ "tool_result",
62253
+ timingDetail,
62254
+ label
62255
+ );
62256
+ } else {
62257
+ logger2.log(`${taskId}: ${type} command succeeded in ${verificationDurationMs}ms`);
62258
+ await store.logEntry(taskId, `[timing] [verification] ${type} command succeeded (exit 0) in ${verificationDurationMs}ms`);
62259
+ await store.appendAgentLog(
62260
+ taskId,
62261
+ `${type} command succeeded (exit 0)`,
62262
+ "tool_result",
62263
+ timingDetail,
62264
+ label
62265
+ );
62266
+ }
62267
+ return result;
62268
+ } catch (error) {
62269
+ if (signal?.aborted) {
62270
+ throw Object.assign(
62271
+ new Error(`Command aborted: ${command}`),
62272
+ { code: "ABORT_ERR", aborted: true }
62273
+ );
62274
+ }
62275
+ const verificationDurationMs = Date.now() - verificationStartedAt;
62276
+ const err = error;
62277
+ result.stdout = err?.stdout?.toString?.() || "";
62278
+ result.stderr = err?.stderr?.toString?.() || "";
62279
+ result.exitCode = typeof err?.status === "number" ? err.status : typeof err?.code === "number" ? err.code : null;
62280
+ const maxBufferExceeded = err?.code === "ENOBUFS" || err?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || String(err?.message ?? "").includes("maxBuffer");
62281
+ result.success = maxBufferExceeded && result.exitCode === 0;
62282
+ if (result.success) {
62283
+ logger2.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
62284
+ await store.logEntry(
62285
+ taskId,
62286
+ `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
62287
+ );
62288
+ await store.appendAgentLog(
62289
+ taskId,
62290
+ `${type} command succeeded (exit 0)`,
62291
+ "tool_result",
62292
+ `${verificationDurationMs}ms`,
62293
+ label
62294
+ );
62295
+ return result;
62067
62296
  }
62297
+ const output = result.stderr || result.stdout || err?.message || "Unknown error";
62298
+ const summary = summarizeVerificationOutput(output, type);
62299
+ logger2.error(`${taskId}: ${type} command failed (exit ${result.exitCode}) in ${verificationDurationMs}ms; output captured in task log`);
62300
+ await store.logEntry(
62301
+ taskId,
62302
+ `[timing] [verification] ${type} command failed (exit ${result.exitCode}) after ${verificationDurationMs}ms:
62303
+ ${summary}`
62304
+ );
62305
+ await store.appendAgentLog(
62306
+ taskId,
62307
+ `${type} command failed (exit ${result.exitCode})`,
62308
+ "tool_error",
62309
+ summary,
62310
+ label
62311
+ );
62068
62312
  }
62069
- return `${trimmed.slice(0, cutoff)}...
62070
- ${footer}`;
62313
+ return result;
62071
62314
  }
62315
+ var VERIFICATION_COMMAND_MAX_BUFFER, VERIFICATION_COMMAND_TIMEOUT_MS, VERIFICATION_LOG_MAX_CHARS;
62316
+ var init_verification_utils = __esm({
62317
+ "../engine/src/verification-utils.ts"() {
62318
+ "use strict";
62319
+ VERIFICATION_COMMAND_MAX_BUFFER = 50 * 1024 * 1024;
62320
+ VERIFICATION_COMMAND_TIMEOUT_MS = 6e5;
62321
+ VERIFICATION_LOG_MAX_CHARS = 2e4;
62322
+ }
62323
+ });
62324
+
62325
+ // ../engine/src/session-token-usage.ts
62326
+ var session_token_usage_exports = {};
62327
+ __export(session_token_usage_exports, {
62328
+ accumulateSessionTokenUsage: () => accumulateSessionTokenUsage
62329
+ });
62330
+ function readSessionStats(session) {
62331
+ const accessor = session.getSessionStats;
62332
+ if (typeof accessor !== "function") return void 0;
62333
+ try {
62334
+ return accessor.call(session);
62335
+ } catch {
62336
+ return void 0;
62337
+ }
62338
+ }
62339
+ async function accumulateSessionTokenUsage(store, taskId, session) {
62340
+ try {
62341
+ const stats = readSessionStats(session);
62342
+ const tokens = stats?.tokens;
62343
+ if (!tokens) return;
62344
+ const currentInput = (tokens.input ?? 0) + (tokens.cacheWrite ?? 0);
62345
+ const currentOutput = tokens.output ?? 0;
62346
+ const currentCached = tokens.cacheRead ?? 0;
62347
+ const baseline = sessionBaselines.get(session) ?? { input: 0, output: 0, cached: 0 };
62348
+ const inputDelta = Math.max(0, currentInput - baseline.input);
62349
+ const outputDelta = Math.max(0, currentOutput - baseline.output);
62350
+ const cachedDelta = Math.max(0, currentCached - baseline.cached);
62351
+ sessionBaselines.set(session, {
62352
+ input: currentInput,
62353
+ output: currentOutput,
62354
+ cached: currentCached
62355
+ });
62356
+ if (inputDelta === 0 && outputDelta === 0 && cachedDelta === 0) return;
62357
+ const task = await store.getTask(taskId);
62358
+ const now = (/* @__PURE__ */ new Date()).toISOString();
62359
+ const newInput = (task.tokenUsage?.inputTokens ?? 0) + inputDelta;
62360
+ const newOutput = (task.tokenUsage?.outputTokens ?? 0) + outputDelta;
62361
+ const newCached = (task.tokenUsage?.cachedTokens ?? 0) + cachedDelta;
62362
+ await store.updateTask(taskId, {
62363
+ tokenUsage: {
62364
+ inputTokens: newInput,
62365
+ outputTokens: newOutput,
62366
+ cachedTokens: newCached,
62367
+ totalTokens: newInput + newOutput + newCached,
62368
+ firstUsedAt: task.tokenUsage?.firstUsedAt ?? now,
62369
+ lastUsedAt: now
62370
+ }
62371
+ });
62372
+ } catch (err) {
62373
+ const message = err instanceof Error ? err.message : String(err);
62374
+ log14.warn(`${taskId}: session token usage accumulate failed: ${message}`);
62375
+ }
62376
+ }
62377
+ var log14, sessionBaselines;
62378
+ var init_session_token_usage = __esm({
62379
+ "../engine/src/session-token-usage.ts"() {
62380
+ "use strict";
62381
+ init_logger2();
62382
+ log14 = createLogger2("session-token-usage");
62383
+ sessionBaselines = /* @__PURE__ */ new WeakMap();
62384
+ }
62385
+ });
62386
+
62387
+ // ../engine/src/run-audit.ts
62388
+ function createRunAuditor(store, context) {
62389
+ if (!context) {
62390
+ return {
62391
+ git: async () => {
62392
+ },
62393
+ database: async () => {
62394
+ },
62395
+ filesystem: async () => {
62396
+ }
62397
+ };
62398
+ }
62399
+ const hasRecordAuditEvent = typeof store.recordRunAuditEvent === "function";
62400
+ if (!hasRecordAuditEvent) {
62401
+ return {
62402
+ git: async () => {
62403
+ },
62404
+ database: async () => {
62405
+ },
62406
+ filesystem: async () => {
62407
+ }
62408
+ };
62409
+ }
62410
+ return {
62411
+ git: async (input) => {
62412
+ const eventInput = {
62413
+ taskId: context.taskId,
62414
+ agentId: context.agentId,
62415
+ runId: context.runId,
62416
+ domain: "git",
62417
+ mutationType: input.type,
62418
+ target: input.target,
62419
+ metadata: {
62420
+ phase: context.phase,
62421
+ ...context.source ? { source: context.source } : {},
62422
+ ...input.metadata
62423
+ }
62424
+ };
62425
+ await store.recordRunAuditEvent(eventInput);
62426
+ },
62427
+ database: async (input) => {
62428
+ const inferredTaskId = input.target.startsWith("FN-") || input.target.startsWith("KB-") ? input.target : context.taskId;
62429
+ const eventInput = {
62430
+ taskId: inferredTaskId,
62431
+ agentId: context.agentId,
62432
+ runId: context.runId,
62433
+ domain: "database",
62434
+ mutationType: input.type,
62435
+ target: input.target,
62436
+ metadata: {
62437
+ phase: context.phase,
62438
+ ...context.source ? { source: context.source } : {},
62439
+ ...input.metadata
62440
+ }
62441
+ };
62442
+ await store.recordRunAuditEvent(eventInput);
62443
+ },
62444
+ filesystem: async (input) => {
62445
+ const eventInput = {
62446
+ taskId: context.taskId,
62447
+ agentId: context.agentId,
62448
+ runId: context.runId,
62449
+ domain: "filesystem",
62450
+ mutationType: input.type,
62451
+ target: input.target,
62452
+ metadata: {
62453
+ phase: context.phase,
62454
+ ...context.source ? { source: context.source } : {},
62455
+ ...input.metadata
62456
+ }
62457
+ };
62458
+ await store.recordRunAuditEvent(eventInput);
62459
+ }
62460
+ };
62461
+ }
62462
+ function generateSyntheticRunId(prefix, taskId) {
62463
+ const timestamp = Date.now();
62464
+ const random = Math.random().toString(36).slice(2, 6);
62465
+ return `${prefix}-${taskId}-${timestamp}-${random}`;
62466
+ }
62467
+ var init_run_audit = __esm({
62468
+ "../engine/src/run-audit.ts"() {
62469
+ "use strict";
62470
+ }
62471
+ });
62472
+
62473
+ // ../engine/src/merger.ts
62474
+ import { execSync, exec as exec3 } from "node:child_process";
62475
+ import { promisify as promisify4 } from "node:util";
62476
+ import { existsSync as existsSync24 } from "node:fs";
62477
+ import { join as join30 } from "node:path";
62478
+ import { Type as Type3 } from "typebox";
62072
62479
  function truncateWorkflowScriptOutput(output) {
62073
62480
  if (output.length <= WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS) return output;
62074
62481
  return `... output truncated to last ${WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS} characters ...
@@ -62112,7 +62519,7 @@ async function getStagedFiles(cwd) {
62112
62519
  }
62113
62520
  }
62114
62521
  function hasInstallState(rootDir) {
62115
- return existsSync23(join29(rootDir, "node_modules")) || existsSync23(join29(rootDir, ".pnp.cjs"));
62522
+ return existsSync24(join30(rootDir, "node_modules")) || existsSync24(join30(rootDir, ".pnp.cjs"));
62116
62523
  }
62117
62524
  function shouldSyncDependenciesForMerge(stagedFiles, installStatePresent) {
62118
62525
  if (!installStatePresent) return true;
@@ -62121,10 +62528,10 @@ function shouldSyncDependenciesForMerge(stagedFiles, installStatePresent) {
62121
62528
  );
62122
62529
  }
62123
62530
  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"))) {
62531
+ if (existsSync24(join30(rootDir, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
62532
+ if (existsSync24(join30(rootDir, "package-lock.json"))) return "npm install";
62533
+ if (existsSync24(join30(rootDir, "yarn.lock"))) return "yarn install --frozen-lockfile";
62534
+ if (existsSync24(join30(rootDir, "bun.lock")) || existsSync24(join30(rootDir, "bun.lockb"))) {
62128
62535
  return "bun install --frozen-lockfile";
62129
62536
  }
62130
62537
  return null;
@@ -62157,8 +62564,8 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
62157
62564
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
62158
62565
  };
62159
62566
  }
62160
- if (existsSync23(join29(rootDir, "pnpm-lock.yaml"))) {
62161
- if (existsSync23(join29(rootDir, "pnpm-workspace.yaml"))) {
62567
+ if (existsSync24(join30(rootDir, "pnpm-lock.yaml"))) {
62568
+ if (existsSync24(join30(rootDir, "pnpm-workspace.yaml"))) {
62162
62569
  mergerLog.warn(
62163
62570
  `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
62571
  );
@@ -62169,21 +62576,21 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
62169
62576
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
62170
62577
  };
62171
62578
  }
62172
- if (existsSync23(join29(rootDir, "yarn.lock"))) {
62579
+ if (existsSync24(join30(rootDir, "yarn.lock"))) {
62173
62580
  return {
62174
62581
  command: "yarn test",
62175
62582
  testSource: "inferred",
62176
62583
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
62177
62584
  };
62178
62585
  }
62179
- if (existsSync23(join29(rootDir, "bun.lock")) || existsSync23(join29(rootDir, "bun.lockb"))) {
62586
+ if (existsSync24(join30(rootDir, "bun.lock")) || existsSync24(join30(rootDir, "bun.lockb"))) {
62180
62587
  return {
62181
62588
  command: "bun test",
62182
62589
  testSource: "inferred",
62183
62590
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
62184
62591
  };
62185
62592
  }
62186
- if (existsSync23(join29(rootDir, "package-lock.json"))) {
62593
+ if (existsSync24(join30(rootDir, "package-lock.json"))) {
62187
62594
  return {
62188
62595
  command: "npm test",
62189
62596
  testSource: "inferred",
@@ -62220,7 +62627,7 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
62220
62627
  await store.logEntry(taskId, deterministicVerificationMessage);
62221
62628
  await store.appendAgentLog(taskId, deterministicVerificationMessage, "text", void 0, "merger");
62222
62629
  if (hasTestCommand) {
62223
- const testResult = await runVerificationCommand(
62630
+ const testResult = await runVerificationCommand2(
62224
62631
  store,
62225
62632
  rootDir,
62226
62633
  taskId,
@@ -62251,7 +62658,7 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
62251
62658
  }
62252
62659
  }
62253
62660
  if (hasBuildCommand) {
62254
- const buildResult = await runVerificationCommand(
62661
+ const buildResult = await runVerificationCommand2(
62255
62662
  store,
62256
62663
  rootDir,
62257
62664
  taskId,
@@ -62286,98 +62693,9 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
62286
62693
  await store.appendAgentLog(taskId, "Deterministic merge verification passed", "text", void 0, "merger");
62287
62694
  return result;
62288
62695
  }
62289
- async function runVerificationCommand(store, rootDir, taskId, command, type, signal) {
62696
+ async function runVerificationCommand2(store, rootDir, taskId, command, type, signal) {
62290
62697
  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;
62698
+ return runVerificationCommand(store, rootDir, taskId, command, type, signal, mergerLog, "merger");
62381
62699
  }
62382
62700
  async function attemptInMergeVerificationFix(store, rootDir, taskId, failureContext, settings, options, mergeRunContext, fixAttemptNumber, _testCommand, _buildCommand) {
62383
62701
  try {
@@ -62440,9 +62758,20 @@ Do not refactor, rename broadly, or make opportunistic improvements.
62440
62758
  onToolEnd: logger2.onToolEnd,
62441
62759
  defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
62442
62760
  defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
62761
+ fallbackProvider: settings.fallbackProvider,
62762
+ fallbackModelId: settings.fallbackModelId,
62443
62763
  defaultThinkingLevel: settings.defaultThinkingLevel,
62444
62764
  // Skill selection: use assigned agent skills if available, otherwise role fallback
62445
- ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
62765
+ ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
62766
+ taskId,
62767
+ taskTitle: taskForSkillContext?.title,
62768
+ onFallbackModelUsed: createFallbackModelObserver({
62769
+ agent: "merger",
62770
+ label: "merge verification fix agent",
62771
+ store,
62772
+ taskId,
62773
+ taskTitle: taskForSkillContext?.title
62774
+ })
62446
62775
  });
62447
62776
  const runId = mergeRunContext?.runId;
62448
62777
  const agentId = mergeRunContext?.agentId ?? "merger";
@@ -62495,7 +62824,7 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
62495
62824
  void 0,
62496
62825
  "merger"
62497
62826
  );
62498
- const reRunResult = await runVerificationCommand(
62827
+ const reRunResult = await runVerificationCommand2(
62499
62828
  store,
62500
62829
  rootDir,
62501
62830
  taskId,
@@ -63237,9 +63566,16 @@ You are assisting with a paused \`git pull --rebase\`.
63237
63566
  onToolEnd: agentLogger.onToolEnd,
63238
63567
  defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
63239
63568
  defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
63569
+ fallbackProvider: settings.fallbackProvider,
63570
+ fallbackModelId: settings.fallbackModelId,
63240
63571
  defaultThinkingLevel: settings.defaultThinkingLevel,
63241
63572
  taskId,
63242
- onFallbackModelUsed: notifyFallbackUsed
63573
+ onFallbackModelUsed: createFallbackModelObserver({
63574
+ agent: "merger",
63575
+ label: "rebase conflict resolver",
63576
+ store,
63577
+ taskId
63578
+ })
63243
63579
  });
63244
63580
  const prompt = [
63245
63581
  `Resolve rebase conflicts for task ${taskId}.`,
@@ -63431,7 +63767,7 @@ async function pushToRemoteAfterMerge(store, rootDir, taskId, settings, options)
63431
63767
  }
63432
63768
  async function createPostMergeWorktree(rootDir, taskId) {
63433
63769
  const randomSuffix = Math.random().toString(36).slice(2, 10);
63434
- const postMergeWorktree = join29(rootDir, ".worktrees", `post-merge-${taskId}-${randomSuffix}`);
63770
+ const postMergeWorktree = join30(rootDir, ".worktrees", `post-merge-${taskId}-${randomSuffix}`);
63435
63771
  try {
63436
63772
  await execAsync2(`git worktree add ${quoteArg(postMergeWorktree)} HEAD`, { cwd: rootDir });
63437
63773
  return postMergeWorktree;
@@ -64372,7 +64708,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
64372
64708
  }
64373
64709
  }
64374
64710
  throwIfAborted(options.signal, taskId);
64375
- if (worktreePath && existsSync23(worktreePath)) {
64711
+ if (worktreePath && existsSync24(worktreePath)) {
64376
64712
  const otherUser = await findWorktreeUser(store, worktreePath, taskId);
64377
64713
  if (otherUser) {
64378
64714
  mergerLog.log(`Worktree retained \u2014 still needed by ${otherUser}`);
@@ -65026,9 +65362,20 @@ async function runAiAgentForCommit(params) {
65026
65362
  onToolEnd: agentLogger.onToolEnd,
65027
65363
  defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
65028
65364
  defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
65365
+ fallbackProvider: settings.fallbackProvider,
65366
+ fallbackModelId: settings.fallbackModelId,
65029
65367
  defaultThinkingLevel: settings.defaultThinkingLevel,
65030
65368
  // Skill selection: use assigned agent skills if available, otherwise role fallback
65031
- ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
65369
+ ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
65370
+ taskId,
65371
+ taskTitle: taskForSkillContext?.title,
65372
+ onFallbackModelUsed: createFallbackModelObserver({
65373
+ agent: "merger",
65374
+ label: "merge agent",
65375
+ store,
65376
+ taskId,
65377
+ taskTitle: taskForSkillContext?.title
65378
+ })
65032
65379
  });
65033
65380
  options.onSession?.(session);
65034
65381
  try {
@@ -65459,7 +65806,14 @@ If issues are found that need attention, describe them clearly and include concr
65459
65806
  fallbackModelId: settings.fallbackModelId,
65460
65807
  defaultThinkingLevel: settings.defaultThinkingLevel,
65461
65808
  // Skill selection: use assigned agent skills if available, otherwise role fallback
65462
- ...postMergeSkillContext?.skillSelectionContext ? { skillSelection: postMergeSkillContext.skillSelectionContext } : {}
65809
+ ...postMergeSkillContext?.skillSelectionContext ? { skillSelection: postMergeSkillContext.skillSelectionContext } : {},
65810
+ taskId,
65811
+ onFallbackModelUsed: createFallbackModelObserver({
65812
+ agent: "merger",
65813
+ label: `post-merge workflow step '${workflowStep.name}'`,
65814
+ store,
65815
+ taskId
65816
+ })
65463
65817
  });
65464
65818
  mergerLog.log(`${taskId}: [post-merge] workflow step '${workflowStep.name}' using model ${describeModel(session)}${useOverride ? " (workflow step override)" : ""}`);
65465
65819
  await store.logEntry(taskId, `[post-merge] Workflow step '${workflowStep.name}' using model: ${describeModel(session)}${useOverride ? " (workflow step override)" : ""}`);
@@ -65495,15 +65849,17 @@ async function completeTask(store, taskId, result) {
65495
65849
  result.task = task;
65496
65850
  store.emit("task:merged", result);
65497
65851
  }
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;
65852
+ 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
65853
  var init_merger = __esm({
65500
65854
  "../engine/src/merger.ts"() {
65501
65855
  "use strict";
65856
+ init_verification_utils();
65857
+ init_verification_utils();
65502
65858
  init_src();
65503
65859
  init_pi();
65504
65860
  init_session_token_usage();
65505
65861
  init_agent_session_helpers();
65506
- init_notifier();
65862
+ init_fallback_model_observer();
65507
65863
  init_session_skill_context();
65508
65864
  init_agent_logger();
65509
65865
  init_logger2();
@@ -65549,9 +65905,6 @@ var init_merger = __esm({
65549
65905
  "bun.lock",
65550
65906
  "packages/*/package.json"
65551
65907
  ];
65552
- VERIFICATION_COMMAND_MAX_BUFFER = 50 * 1024 * 1024;
65553
- VERIFICATION_COMMAND_TIMEOUT_MS = 6e5;
65554
- VERIFICATION_LOG_MAX_CHARS = 2e4;
65555
65908
  WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS = 4e3;
65556
65909
  PULL_REBASE_TIMEOUT_MS = 12e4;
65557
65910
  PUSH_TIMEOUT_MS = 6e4;
@@ -65576,8 +65929,8 @@ var init_merger = __esm({
65576
65929
 
65577
65930
  // ../engine/src/worktree-names.ts
65578
65931
  import { readdirSync as readdirSync3 } from "node:fs";
65579
- import { join as join30 } from "node:path";
65580
- import { existsSync as existsSync24 } from "node:fs";
65932
+ import { join as join31 } from "node:path";
65933
+ import { existsSync as existsSync25 } from "node:fs";
65581
65934
  function slugify2(str) {
65582
65935
  return str.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
65583
65936
  }
@@ -65588,7 +65941,7 @@ function generateReservedWorktreeName(rootDir, reservedNames = /* @__PURE__ */ n
65588
65941
  const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
65589
65942
  const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
65590
65943
  const baseName = `${adjective}-${noun}`;
65591
- const worktreesDir = join30(rootDir, ".worktrees");
65944
+ const worktreesDir = join31(rootDir, ".worktrees");
65592
65945
  const existing = getExistingWorktreeNames(worktreesDir);
65593
65946
  for (const reserved of reservedNames) {
65594
65947
  existing.add(reserved);
@@ -65603,7 +65956,7 @@ function generateReservedWorktreeName(rootDir, reservedNames = /* @__PURE__ */ n
65603
65956
  return `${baseName}-${suffix}`;
65604
65957
  }
65605
65958
  function getExistingWorktreeNames(worktreesDir) {
65606
- if (!existsSync24(worktreesDir)) {
65959
+ if (!existsSync25(worktreesDir)) {
65607
65960
  return /* @__PURE__ */ new Set();
65608
65961
  }
65609
65962
  try {
@@ -65740,8 +66093,8 @@ __export(worktree_pool_exports, {
65740
66093
  });
65741
66094
  import { exec as exec4 } from "node:child_process";
65742
66095
  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";
66096
+ import { existsSync as existsSync26, lstatSync, readdirSync as readdirSync4, rmSync as rmSync2 } from "node:fs";
66097
+ import { join as join32, relative as relative6, resolve as resolve15, isAbsolute as isAbsolute9 } from "node:path";
65745
66098
  function getExecStdout(result) {
65746
66099
  if (typeof result === "string") return result;
65747
66100
  if (result && typeof result === "object" && "stdout" in result) {
@@ -65787,10 +66140,10 @@ async function isRegisteredGitWorktree2(rootDir, worktreePath) {
65787
66140
  return (await getRegisteredWorktreePaths(rootDir)).has(resolve15(worktreePath));
65788
66141
  }
65789
66142
  function hasRequiredWorktreeFiles(worktreePath) {
65790
- return existsSync25(join31(worktreePath, ".git")) && existsSync25(join31(worktreePath, "package.json"));
66143
+ return existsSync26(join32(worktreePath, ".git")) && existsSync26(join32(worktreePath, "package.json"));
65791
66144
  }
65792
66145
  async function isUsableTaskWorktree(rootDir, worktreePath) {
65793
- return existsSync25(worktreePath) && await isRegisteredGitWorktree2(rootDir, worktreePath) && hasRequiredWorktreeFiles(worktreePath);
66146
+ return existsSync26(worktreePath) && await isRegisteredGitWorktree2(rootDir, worktreePath) && hasRequiredWorktreeFiles(worktreePath);
65794
66147
  }
65795
66148
  function isInsideWorktreesDir(rootDir, worktreePath) {
65796
66149
  const worktreesDir = resolve15(rootDir, ".worktrees");
@@ -65799,14 +66152,14 @@ function isInsideWorktreesDir(rootDir, worktreePath) {
65799
66152
  return rel !== "" && !rel.startsWith("..") && !isAbsolute9(rel);
65800
66153
  }
65801
66154
  async function scanIdleWorktrees(rootDir, store) {
65802
- const worktreesDir = join31(rootDir, ".worktrees");
65803
- if (!existsSync25(worktreesDir)) {
66155
+ const worktreesDir = join32(rootDir, ".worktrees");
66156
+ if (!existsSync26(worktreesDir)) {
65804
66157
  return [];
65805
66158
  }
65806
66159
  let dirs;
65807
66160
  try {
65808
66161
  const entries = readdirSync4(worktreesDir, { withFileTypes: true });
65809
- dirs = entries.filter((e) => e.isDirectory()).map((e) => join31(worktreesDir, e.name));
66162
+ dirs = entries.filter((e) => e.isDirectory()).map((e) => join32(worktreesDir, e.name));
65810
66163
  } catch (err) {
65811
66164
  const errorMessage = err instanceof Error ? err.message : String(err);
65812
66165
  worktreePoolLog.warn(`Failed to read .worktrees/ directory: ${errorMessage}`);
@@ -65829,16 +66182,16 @@ async function scanIdleWorktrees(rootDir, store) {
65829
66182
  return registeredDirs.filter((dir) => !activeWorktrees.has(resolve15(dir)));
65830
66183
  }
65831
66184
  async function cleanupOrphanedWorktrees(rootDir, store) {
65832
- const worktreesDir = join31(rootDir, ".worktrees");
65833
- if (!existsSync25(worktreesDir)) {
66185
+ const worktreesDir = join32(rootDir, ".worktrees");
66186
+ if (!existsSync26(worktreesDir)) {
65834
66187
  return 0;
65835
66188
  }
65836
66189
  const orphaned = await scanIdleWorktrees(rootDir, store);
65837
66190
  const registeredWorktrees = await getRegisteredWorktreePaths(rootDir);
65838
66191
  let dirs = [];
65839
- if (existsSync25(worktreesDir)) {
66192
+ if (existsSync26(worktreesDir)) {
65840
66193
  try {
65841
- dirs = readdirSync4(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join31(worktreesDir, e.name));
66194
+ dirs = readdirSync4(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join32(worktreesDir, e.name));
65842
66195
  } catch (err) {
65843
66196
  const errorMessage = err instanceof Error ? err.message : String(err);
65844
66197
  worktreePoolLog.warn(`Failed to read .worktrees/ directory for cleanup: ${errorMessage}`);
@@ -65870,8 +66223,8 @@ async function cleanupOrphanedWorktrees(rootDir, store) {
65870
66223
  return cleaned;
65871
66224
  }
65872
66225
  async function reapOrphanWorktrees(projectRoot) {
65873
- const worktreesDir = join31(projectRoot, ".worktrees");
65874
- if (!existsSync25(worktreesDir)) {
66226
+ const worktreesDir = join32(projectRoot, ".worktrees");
66227
+ if (!existsSync26(worktreesDir)) {
65875
66228
  return 0;
65876
66229
  }
65877
66230
  let entries;
@@ -65879,11 +66232,11 @@ async function reapOrphanWorktrees(projectRoot) {
65879
66232
  entries = readdirSync4(worktreesDir, { withFileTypes: true }).filter((e) => {
65880
66233
  if (!e.isDirectory()) return false;
65881
66234
  try {
65882
- return lstatSync(join31(worktreesDir, e.name)).isDirectory() && !lstatSync(join31(worktreesDir, e.name)).isSymbolicLink();
66235
+ return lstatSync(join32(worktreesDir, e.name)).isDirectory() && !lstatSync(join32(worktreesDir, e.name)).isSymbolicLink();
65883
66236
  } catch {
65884
66237
  return false;
65885
66238
  }
65886
- }).map((e) => ({ name: e.name, fullPath: join31(worktreesDir, e.name) }));
66239
+ }).map((e) => ({ name: e.name, fullPath: join32(worktreesDir, e.name) }));
65887
66240
  } catch (err) {
65888
66241
  const msg = err instanceof Error ? err.message : String(err);
65889
66242
  worktreePoolLog.warn(`reapOrphanWorktrees: failed to read .worktrees/ \u2014 ${msg}`);
@@ -65902,8 +66255,8 @@ async function reapOrphanWorktrees(projectRoot) {
65902
66255
  if (registered.has(resolvedFull)) {
65903
66256
  continue;
65904
66257
  }
65905
- const dotGit = join31(resolvedFull, ".git");
65906
- if (existsSync25(dotGit)) {
66258
+ const dotGit = join32(resolvedFull, ".git");
66259
+ if (existsSync26(dotGit)) {
65907
66260
  worktreePoolLog.log(`reapOrphanWorktrees: skipping ${name} (has .git entry but not in registered list \u2014 may be partially registered)`);
65908
66261
  continue;
65909
66262
  }
@@ -65963,7 +66316,7 @@ var init_worktree_pool = __esm({
65963
66316
  acquire() {
65964
66317
  for (const path2 of this.idle) {
65965
66318
  this.idle.delete(path2);
65966
- if (existsSync25(path2)) {
66319
+ if (existsSync26(path2)) {
65967
66320
  return path2;
65968
66321
  }
65969
66322
  worktreePoolLog.log(`Pruned stale entry: ${path2}`);
@@ -66010,7 +66363,7 @@ var init_worktree_pool = __esm({
66010
66363
  */
66011
66364
  rehydrate(idlePaths) {
66012
66365
  for (const path2 of idlePaths) {
66013
- if (existsSync25(path2)) {
66366
+ if (existsSync26(path2)) {
66014
66367
  this.idle.add(path2);
66015
66368
  } else {
66016
66369
  worktreePoolLog.log(`Rehydrate skipped (not on disk): ${path2}`);
@@ -66066,7 +66419,7 @@ var init_worktree_pool = __esm({
66066
66419
  throw err;
66067
66420
  }
66068
66421
  const conflictingPath = match[1];
66069
- if (!existsSync25(conflictingPath)) {
66422
+ if (!existsSync26(conflictingPath)) {
66070
66423
  await execAsync3("git worktree prune", { cwd: worktreePath });
66071
66424
  await execAsync3(checkoutCmd, { cwd: worktreePath });
66072
66425
  return branchName;
@@ -66143,8 +66496,8 @@ var init_token_cap_detector = __esm({
66143
66496
  // ../engine/src/step-session-executor.ts
66144
66497
  import { exec as exec5 } from "node:child_process";
66145
66498
  import { promisify as promisify6 } from "node:util";
66146
- import { existsSync as existsSync26 } from "node:fs";
66147
- import { join as join32 } from "node:path";
66499
+ import { existsSync as existsSync27 } from "node:fs";
66500
+ import { join as join33 } from "node:path";
66148
66501
  function parseStepFileScopes(prompt) {
66149
66502
  const result = /* @__PURE__ */ new Map();
66150
66503
  if (!prompt) return result;
@@ -66431,7 +66784,7 @@ var init_step_session_executor = __esm({
66431
66784
  init_worktree_names();
66432
66785
  init_agent_logger();
66433
66786
  init_logger2();
66434
- init_notifier();
66787
+ init_fallback_model_observer();
66435
66788
  init_context_limit_detector();
66436
66789
  init_usage_limit_detector();
66437
66790
  init_agent_tools();
@@ -66548,7 +66901,7 @@ var init_step_session_executor = __esm({
66548
66901
  }
66549
66902
  for (const [stepIdx, worktreePath] of this.parallelWorktrees) {
66550
66903
  try {
66551
- if (existsSync26(worktreePath)) {
66904
+ if (existsSync27(worktreePath)) {
66552
66905
  await execAsync4(`git worktree remove "${worktreePath}" --force`, {
66553
66906
  cwd: this.options.rootDir
66554
66907
  });
@@ -66713,7 +67066,13 @@ Follow instructions precisely and avoid unrelated changes.`,
66713
67066
  ...this.options.skillSelection ? { skillSelection: this.options.skillSelection } : {},
66714
67067
  taskId: taskDetail.id,
66715
67068
  taskTitle: taskDetail.title,
66716
- onFallbackModelUsed: notifyFallbackUsed
67069
+ onFallbackModelUsed: createFallbackModelObserver({
67070
+ agent: "executor",
67071
+ label: "workflow step agent",
67072
+ store: this.store,
67073
+ taskId: taskDetail.id,
67074
+ taskTitle: taskDetail.title
67075
+ })
66717
67076
  });
66718
67077
  session = createResult.session;
66719
67078
  const handle = {
@@ -66893,7 +67252,7 @@ Follow instructions precisely and avoid unrelated changes.`,
66893
67252
  for (const [stepIdx, worktreePath] of worktreePaths) {
66894
67253
  if (worktreePath !== this.options.worktreePath) {
66895
67254
  try {
66896
- if (existsSync26(worktreePath)) {
67255
+ if (existsSync27(worktreePath)) {
66897
67256
  await execAsync4(`git worktree remove "${worktreePath}" --force`, {
66898
67257
  cwd: this.options.rootDir
66899
67258
  });
@@ -66923,7 +67282,7 @@ Follow instructions precisely and avoid unrelated changes.`,
66923
67282
  async createStepWorktree(stepIndex) {
66924
67283
  const { rootDir } = this.options;
66925
67284
  const name = generateWorktreeName(rootDir);
66926
- const worktreePath = join32(rootDir, ".worktrees", name);
67285
+ const worktreePath = join33(rootDir, ".worktrees", name);
66927
67286
  const branchName = `fusion/step-${stepIndex}-${name}`;
66928
67287
  stepExecLog.log(`Creating worktree for step ${stepIndex}: ${worktreePath} (branch: ${branchName})`);
66929
67288
  try {
@@ -66998,7 +67357,7 @@ Follow instructions precisely and avoid unrelated changes.`,
66998
67357
 
66999
67358
  // ../engine/src/spec-staleness.ts
67000
67359
  import { stat as stat5 } from "node:fs/promises";
67001
- import { join as join33 } from "node:path";
67360
+ import { join as join34 } from "node:path";
67002
67361
  async function evaluateSpecStaleness(options) {
67003
67362
  const { settings, promptPath, nowMs } = options;
67004
67363
  if (settings.specStalenessEnabled !== true) {
@@ -67038,7 +67397,7 @@ async function evaluateSpecStaleness(options) {
67038
67397
  };
67039
67398
  }
67040
67399
  function getPromptPath(tasksDir, taskId) {
67041
- return join33(tasksDir, taskId, "PROMPT.md");
67400
+ return join34(tasksDir, taskId, "PROMPT.md");
67042
67401
  }
67043
67402
  var DEFAULT_SPEC_STALENESS_MAX_AGE_MS;
67044
67403
  var init_spec_staleness = __esm({
@@ -67069,8 +67428,8 @@ var init_task_completion = __esm({
67069
67428
 
67070
67429
  // ../engine/src/run-verification-tool.ts
67071
67430
  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";
67431
+ import { existsSync as existsSync28 } from "node:fs";
67432
+ import { isAbsolute as isAbsolute10, join as join35 } from "node:path";
67074
67433
  import { Type as Type4 } from "@mariozechner/pi-ai";
67075
67434
  function createBuffer() {
67076
67435
  return { headChunks: [], headBytes: 0, tailChunks: [], tailBytes: 0, totalBytes: 0 };
@@ -67103,7 +67462,7 @@ function flattenBuffer(buf) {
67103
67462
 
67104
67463
  ` + tail;
67105
67464
  }
67106
- async function runVerificationCommand2(opts) {
67465
+ async function runVerificationCommand3(opts) {
67107
67466
  const { command, cwd, timeoutMs, expectFailure = false, onHeartbeat, onLine } = opts;
67108
67467
  const startMs = Date.now();
67109
67468
  const warnings = [];
@@ -67243,7 +67602,7 @@ function createRunVerificationTool(opts) {
67243
67602
  if (params.cwd && isAbsolute10(params.cwd)) {
67244
67603
  resolvedCwd = params.cwd;
67245
67604
  } else if (params.cwd) {
67246
- resolvedCwd = join34(worktreePath, params.cwd);
67605
+ resolvedCwd = join35(worktreePath, params.cwd);
67247
67606
  } else {
67248
67607
  resolvedCwd = worktreePath;
67249
67608
  }
@@ -67258,8 +67617,8 @@ function createRunVerificationTool(opts) {
67258
67617
  }
67259
67618
  let effectiveCommand = command;
67260
67619
  if (command.trimStart().startsWith("pnpm --filter")) {
67261
- const modulesYaml = join34(rootDir, "node_modules", ".modules.yaml");
67262
- if (!existsSync27(modulesYaml)) {
67620
+ const modulesYaml = join35(rootDir, "node_modules", ".modules.yaml");
67621
+ if (!existsSync28(modulesYaml)) {
67263
67622
  const installCmd = "pnpm install --prefer-offline";
67264
67623
  const msg = `node_modules/.modules.yaml not found in workspace root \u2014 auto-prepending \`${installCmd}\` before running the command.`;
67265
67624
  warnings.push(msg);
@@ -67270,7 +67629,7 @@ function createRunVerificationTool(opts) {
67270
67629
  log18.info(
67271
67630
  `[fn_run_verification] ${taskId}: scope=${scope} timeout=${timeoutSec}s cwd=${resolvedCwd} cmd=${effectiveCommand}`
67272
67631
  );
67273
- const result = await runVerificationCommand2({
67632
+ const result = await runVerificationCommand3({
67274
67633
  command: effectiveCommand,
67275
67634
  cwd: resolvedCwd,
67276
67635
  timeoutMs,
@@ -67370,8 +67729,8 @@ var init_run_verification_tool = __esm({
67370
67729
  // ../engine/src/executor.ts
67371
67730
  import { exec as exec6 } from "node:child_process";
67372
67731
  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";
67732
+ import { isAbsolute as isAbsolute11, join as join36, relative as relative7, resolve as resolvePath } from "node:path";
67733
+ import { existsSync as existsSync29 } from "node:fs";
67375
67734
  import { readFile as readFile15, writeFile as writeFile12 } from "node:fs/promises";
67376
67735
  import { Type as Type5 } from "@mariozechner/pi-ai";
67377
67736
  import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
@@ -67665,6 +68024,7 @@ var init_executor = __esm({
67665
68024
  "use strict";
67666
68025
  init_src();
67667
68026
  init_merger();
68027
+ init_verification_utils();
67668
68028
  init_worktree_names();
67669
68029
  init_pi();
67670
68030
  init_session_token_usage();
@@ -67689,7 +68049,7 @@ var init_executor = __esm({
67689
68049
  init_task_completion();
67690
68050
  init_auth_storage();
67691
68051
  init_run_verification_tool();
67692
- init_notifier();
68052
+ init_fallback_model_observer();
67693
68053
  init_agent_logger();
67694
68054
  init_agent_tools();
67695
68055
  execAsync5 = promisify7(exec6);
@@ -68836,7 +69196,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
68836
69196
  );
68837
69197
  return false;
68838
69198
  }
68839
- if (task.worktree && existsSync28(task.worktree)) {
69199
+ if (task.worktree && existsSync29(task.worktree)) {
68840
69200
  const modifiedFiles = await this.captureModifiedFiles(task.worktree, task.baseCommitSha);
68841
69201
  if (modifiedFiles.length > 0) {
68842
69202
  await this.store.updateTask(task.id, { modifiedFiles });
@@ -69016,7 +69376,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69016
69376
  if (task.dependencies.length === 0) return null;
69017
69377
  for (const depId of task.dependencies) {
69018
69378
  const dep = allTasks.find((t) => t.id === depId);
69019
- if (dep && dep.worktree && (dep.column === "done" || dep.column === "in-review") && existsSync28(dep.worktree)) {
69379
+ if (dep && dep.worktree && (dep.column === "done" || dep.column === "in-review") && existsSync29(dep.worktree)) {
69020
69380
  return dep.worktree;
69021
69381
  }
69022
69382
  }
@@ -69067,7 +69427,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69067
69427
  const activeMergeStatuses = /* @__PURE__ */ new Set(["merging", "merging-pr"]);
69068
69428
  const isActiveTask = activeColumns.has(task.column) || activeMergeStatuses.has(task.status ?? "");
69069
69429
  if (!isActiveTask) {
69070
- const tasksDir = join35(this.store.getFusionDir(), "tasks");
69430
+ const tasksDir = join36(this.store.getFusionDir(), "tasks");
69071
69431
  const promptPath = getPromptPath(tasksDir, task.id);
69072
69432
  const staleness = await evaluateSpecStaleness({ settings, promptPath });
69073
69433
  if (staleness.isStale) {
@@ -69114,7 +69474,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69114
69474
  worktreeName = generateWorktreeName(this.rootDir);
69115
69475
  break;
69116
69476
  }
69117
- worktreePath = join35(this.rootDir, ".worktrees", worktreeName);
69477
+ worktreePath = join36(this.rootDir, ".worktrees", worktreeName);
69118
69478
  }
69119
69479
  let stuckRequeue = null;
69120
69480
  let taskDone = false;
@@ -69138,7 +69498,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69138
69498
  );
69139
69499
  }
69140
69500
  const branchName = task.branch || `fusion/${task.id.toLowerCase()}`;
69141
- let isResume = existsSync28(worktreePath);
69501
+ let isResume = existsSync29(worktreePath);
69142
69502
  let acquiredFromPool = false;
69143
69503
  const baseBranch = task.baseBranch || null;
69144
69504
  if (task.worktree && isResume && !await isUsableTaskWorktree(this.rootDir, worktreePath)) {
@@ -69151,8 +69511,8 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69151
69511
  this.currentRunContext
69152
69512
  );
69153
69513
  await this.store.updateTask(task.id, { worktree: null, branch: null });
69154
- worktreePath = join35(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
69155
- isResume = existsSync28(worktreePath);
69514
+ worktreePath = join36(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
69515
+ isResume = existsSync29(worktreePath);
69156
69516
  }
69157
69517
  if (!isResume) {
69158
69518
  if (this.options.pool && settings.recycleWorktrees) {
@@ -69378,6 +69738,84 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69378
69738
  if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps after step-session completion")) {
69379
69739
  return;
69380
69740
  }
69741
+ if (executionMode !== "fast") {
69742
+ if (settings.testCommand?.trim() || settings.buildCommand?.trim()) {
69743
+ const verificationResult = await this.runExecutorDeterministicVerification(task, worktreePath, settings);
69744
+ if (!verificationResult.allPassed) {
69745
+ const failedType = verificationResult.failedCommand === "testCommand" ? "test" : "build";
69746
+ const failedResult = failedType === "test" ? verificationResult.testResult : verificationResult.buildResult;
69747
+ const failedCommand = failedResult.command;
69748
+ const failureOutput = failedResult.stderr || failedResult.stdout || "Unknown error";
69749
+ const summary = summarizeVerificationOutput(failureOutput, failedType);
69750
+ executorLog.log(`${task.id}: [verification] ${failedType} failed \u2014 attempting fix agent`);
69751
+ await this.store.logEntry(
69752
+ task.id,
69753
+ `[verification] ${failedType} command failed (exit ${failedResult.exitCode}). Attempting fix agent...`,
69754
+ summary,
69755
+ this.currentRunContext
69756
+ );
69757
+ const maxFixRetries = Math.min(settings.verificationFixRetries ?? 3, 3);
69758
+ if (maxFixRetries === 0) {
69759
+ executorLog.log(`${task.id}: [verification] fix retries set to 0 \u2014 sending task back immediately`);
69760
+ await this.sendTaskBackForFix(
69761
+ task,
69762
+ worktreePath,
69763
+ `${failedType} command \`${failedCommand}\` failed (exit ${failedResult.exitCode}):
69764
+ ${summary}`,
69765
+ `Verification (${failedType})`,
69766
+ `Deterministic verification failed (${failedType})`
69767
+ );
69768
+ return;
69769
+ }
69770
+ let fixSucceeded = false;
69771
+ for (let attempt = 1; attempt <= maxFixRetries; attempt++) {
69772
+ const fixed = await this.attemptExecutorVerificationFix(
69773
+ task,
69774
+ worktreePath,
69775
+ {
69776
+ command: failedCommand,
69777
+ exitCode: failedResult.exitCode,
69778
+ output: failureOutput,
69779
+ type: failedType
69780
+ },
69781
+ settings,
69782
+ attempt,
69783
+ maxFixRetries
69784
+ );
69785
+ if (fixed) {
69786
+ fixSucceeded = true;
69787
+ executorLog.log(`${task.id}: [verification] fix agent succeeded on attempt ${attempt}/${maxFixRetries}`);
69788
+ await this.store.logEntry(
69789
+ task.id,
69790
+ `[verification] Fix agent succeeded on attempt ${attempt}/${maxFixRetries}. Verification now passing.`,
69791
+ void 0,
69792
+ this.currentRunContext
69793
+ );
69794
+ break;
69795
+ }
69796
+ executorLog.log(`${task.id}: [verification] fix agent attempt ${attempt}/${maxFixRetries} failed`);
69797
+ await this.store.logEntry(
69798
+ task.id,
69799
+ `[verification] Fix agent attempt ${attempt}/${maxFixRetries} failed`,
69800
+ void 0,
69801
+ this.currentRunContext
69802
+ );
69803
+ }
69804
+ if (!fixSucceeded) {
69805
+ executorLog.log(`${task.id}: [verification] all fix attempts exhausted (${maxFixRetries}/${maxFixRetries}) \u2014 sending task back`);
69806
+ await this.sendTaskBackForFix(
69807
+ task,
69808
+ worktreePath,
69809
+ `${failedType} command \`${failedCommand}\` failed (exit ${failedResult.exitCode}) after ${maxFixRetries} fix attempts:
69810
+ ${summary}`,
69811
+ `Verification (${failedType})`,
69812
+ `Deterministic verification failed after ${maxFixRetries} fix attempts`
69813
+ );
69814
+ return;
69815
+ }
69816
+ }
69817
+ }
69818
+ }
69381
69819
  if (executionMode !== "fast") {
69382
69820
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
69383
69821
  if (workflowResult === "deferred-paused") {
@@ -69466,7 +69904,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69466
69904
  executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}: ${errorMessage}`);
69467
69905
  await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}): ${errorMessage}`, void 0, this.currentRunContext);
69468
69906
  }
69469
- if (worktreePath && existsSync28(worktreePath)) {
69907
+ if (worktreePath && existsSync29(worktreePath)) {
69470
69908
  try {
69471
69909
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
69472
69910
  await audit.git({ type: "worktree:remove", target: worktreePath });
@@ -69525,7 +69963,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69525
69963
  try {
69526
69964
  const latestTask = await this.store.getTask(task.id);
69527
69965
  await this.resetStepsIfWorkLost(latestTask);
69528
- if (worktreePath && existsSync28(worktreePath)) {
69966
+ if (worktreePath && existsSync29(worktreePath)) {
69529
69967
  try {
69530
69968
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
69531
69969
  } catch (wtErr) {
@@ -69637,7 +70075,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69637
70075
  const executorFallbackProvider = settings.fallbackProvider;
69638
70076
  const executorFallbackModelId = settings.fallbackModelId;
69639
70077
  const executorThinkingLevel = detail.thinkingLevel ?? settings.defaultThinkingLevel;
69640
- const isResuming = !!task.sessionFile && existsSync28(task.sessionFile);
70078
+ const isResuming = !!task.sessionFile && existsSync29(task.sessionFile);
69641
70079
  const sessionManager = isResuming ? SessionManager2.open(task.sessionFile) : SessionManager2.create(worktreePath);
69642
70080
  executorLog.log(`${task.id}: creating agent session (provider=${executorProvider ?? "default"}, model=${executorModelId ?? "default"}, resuming=${isResuming})`);
69643
70081
  const executorInstructions = await this.resolveInstructionsForRole("executor");
@@ -69667,7 +70105,13 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69667
70105
  ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
69668
70106
  taskId: task.id,
69669
70107
  taskTitle: detail.title,
69670
- onFallbackModelUsed: notifyFallbackUsed
70108
+ onFallbackModelUsed: createFallbackModelObserver({
70109
+ agent: "executor",
70110
+ label: "executor",
70111
+ store: this.store,
70112
+ taskId: task.id,
70113
+ taskTitle: detail.title
70114
+ })
69671
70115
  });
69672
70116
  if (isResuming) {
69673
70117
  executorLog.log(`${task.id}: resumed session from ${task.sessionFile}`);
@@ -70101,7 +70545,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70101
70545
  this.options.onComplete?.(task);
70102
70546
  } else {
70103
70547
  executorLog.log(`${task.id} paused \u2014 moving to todo`);
70104
- if (worktreePath && existsSync28(worktreePath)) {
70548
+ if (worktreePath && existsSync29(worktreePath)) {
70105
70549
  try {
70106
70550
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
70107
70551
  executorLog.log(`Removed old worktree for paused task: ${worktreePath}`);
@@ -70197,7 +70641,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70197
70641
  executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}: ${errorMessage}`);
70198
70642
  await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}): ${errorMessage}`, void 0, this.currentRunContext);
70199
70643
  }
70200
- if (worktreePath && existsSync28(worktreePath)) {
70644
+ if (worktreePath && existsSync29(worktreePath)) {
70201
70645
  try {
70202
70646
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
70203
70647
  executorLog.log(`Removed old worktree for transient retry: ${worktreePath}`);
@@ -70252,7 +70696,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70252
70696
  try {
70253
70697
  const latestTask = await this.store.getTask(task.id);
70254
70698
  await this.resetStepsIfWorkLost(latestTask);
70255
- if (worktreePath && existsSync28(worktreePath)) {
70699
+ if (worktreePath && existsSync29(worktreePath)) {
70256
70700
  try {
70257
70701
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
70258
70702
  executorLog.log(`Removed old worktree for stuck-killed retry: ${worktreePath}`);
@@ -70757,7 +71201,7 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
70757
71201
  * The section is replaced entirely to avoid accumulation of old feedback.
70758
71202
  */
70759
71203
  async injectWorkflowRevisionInstructions(task, feedback) {
70760
- const promptPath = join35(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
71204
+ const promptPath = join36(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
70761
71205
  let content;
70762
71206
  try {
70763
71207
  content = await readFile15(promptPath, "utf-8");
@@ -70811,6 +71255,217 @@ ${feedback}
70811
71255
  *
70812
71256
  * @returns true if a retry was scheduled, false if retries are exhausted
70813
71257
  */
71258
+ /**
71259
+ * Run deterministic verification (test + build commands) in the task's worktree.
71260
+ * Returns a structured result indicating whether all commands passed.
71261
+ */
71262
+ async runExecutorDeterministicVerification(task, worktreePath, settings) {
71263
+ const testCommand = settings.testCommand?.trim();
71264
+ const buildCommand2 = settings.buildCommand?.trim();
71265
+ if (!testCommand && !buildCommand2) {
71266
+ executorLog.log(`${task.id}: no test/build commands configured \u2014 skipping verification`);
71267
+ return { allPassed: true };
71268
+ }
71269
+ const parts = [];
71270
+ if (testCommand) parts.push(`test: ${testCommand}`);
71271
+ if (buildCommand2) parts.push(`build: ${buildCommand2}`);
71272
+ executorLog.log(`${task.id}: [verification] running deterministic verification (${parts.join(", ")})`);
71273
+ await this.store.logEntry(
71274
+ task.id,
71275
+ `[verification] Running deterministic verification (${parts.join(", ")})`,
71276
+ void 0,
71277
+ this.currentRunContext
71278
+ );
71279
+ const result = { allPassed: true };
71280
+ if (testCommand) {
71281
+ const testResult = await runVerificationCommand(
71282
+ this.store,
71283
+ worktreePath,
71284
+ task.id,
71285
+ testCommand,
71286
+ "test",
71287
+ void 0,
71288
+ executorLog,
71289
+ "executor"
71290
+ );
71291
+ result.testResult = testResult;
71292
+ if (!testResult.success) {
71293
+ result.allPassed = false;
71294
+ result.failedCommand = "testCommand";
71295
+ executorLog.log(`${task.id}: [verification] test failed (exit ${testResult.exitCode})`);
71296
+ return result;
71297
+ }
71298
+ }
71299
+ if (buildCommand2) {
71300
+ const buildResult = await runVerificationCommand(
71301
+ this.store,
71302
+ worktreePath,
71303
+ task.id,
71304
+ buildCommand2,
71305
+ "build",
71306
+ void 0,
71307
+ executorLog,
71308
+ "executor"
71309
+ );
71310
+ result.buildResult = buildResult;
71311
+ if (!buildResult.success) {
71312
+ result.allPassed = false;
71313
+ result.failedCommand = "buildCommand";
71314
+ executorLog.log(`${task.id}: [verification] build failed (exit ${buildResult.exitCode})`);
71315
+ return result;
71316
+ }
71317
+ }
71318
+ executorLog.log(`${task.id}: [verification] passed`);
71319
+ await this.store.logEntry(
71320
+ task.id,
71321
+ `[verification] Deterministic verification passed`,
71322
+ void 0,
71323
+ this.currentRunContext
71324
+ );
71325
+ return result;
71326
+ }
71327
+ /**
71328
+ * Attempt to fix verification failures by spawning a dedicated AI fix agent.
71329
+ * Follows the pattern established by the merger's attemptInMergeVerificationFix.
71330
+ * Returns true if verification passes after the fix attempt, false otherwise.
71331
+ */
71332
+ async attemptExecutorVerificationFix(task, worktreePath, failureContext, settings, retryNumber, maxRetries) {
71333
+ try {
71334
+ executorLog.log(`${task.id}: spawning executor verification fix agent (attempt ${retryNumber}/${maxRetries})`);
71335
+ const logger2 = new AgentLogger({
71336
+ store: this.store,
71337
+ taskId: task.id,
71338
+ agent: "executor",
71339
+ persistAgentToolOutput: settings.persistAgentToolOutput,
71340
+ onAgentText: this.options.onAgentText,
71341
+ onAgentTool: this.options.onAgentTool
71342
+ });
71343
+ let skillContext;
71344
+ if (this.options.agentStore) {
71345
+ try {
71346
+ skillContext = await buildSessionSkillContext({
71347
+ agentStore: this.options.agentStore,
71348
+ task,
71349
+ sessionPurpose: "executor",
71350
+ projectRootDir: worktreePath,
71351
+ pluginRunner: this.options.pluginRunner
71352
+ });
71353
+ } catch {
71354
+ }
71355
+ }
71356
+ const { provider: executorProvider, modelId: executorModelId } = resolveExecutorModelPair2(
71357
+ task.modelProvider,
71358
+ task.modelId,
71359
+ settings
71360
+ );
71361
+ const { session } = await createResolvedAgentSession({
71362
+ sessionPurpose: "executor",
71363
+ pluginRunner: this.options.pluginRunner,
71364
+ cwd: worktreePath,
71365
+ // Run in the task's worktree
71366
+ systemPrompt: `You are a verification fix agent running during task execution in a worktree.
71367
+
71368
+ 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.
71369
+
71370
+ ## Scope
71371
+ Only fix what is required to make the failing verification pass.
71372
+ Do not refactor, rename broadly, or make opportunistic improvements.
71373
+
71374
+ ## Rules
71375
+ 1. Read the error output carefully to understand what is failing before editing anything
71376
+ 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.
71377
+ 3. Make targeted fixes to the failing code path
71378
+ 4. After fixing, run the verification command to confirm the fix works
71379
+ 5. Do NOT make any git commits \u2014 just fix the code
71380
+ 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.
71381
+ 7. If you cannot fix the issue within scope, explain why and what evidence indicates a deeper/root problem`,
71382
+ tools: "coding",
71383
+ onText: logger2.onText,
71384
+ onThinking: logger2.onThinking,
71385
+ onToolStart: logger2.onToolStart,
71386
+ onToolEnd: logger2.onToolEnd,
71387
+ defaultProvider: executorProvider,
71388
+ defaultModelId: executorModelId,
71389
+ defaultThinkingLevel: settings.defaultThinkingLevel,
71390
+ ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
71391
+ });
71392
+ await this.store.logEntry(
71393
+ task.id,
71394
+ `Executor verification fix agent started (model: ${describeModel(session)}, attempt ${retryNumber}/${maxRetries})`,
71395
+ void 0,
71396
+ this.currentRunContext
71397
+ );
71398
+ await this.store.appendAgentLog(
71399
+ task.id,
71400
+ `Fix agent started (model: ${describeModel(session)}, attempt ${retryNumber}/${maxRetries})`,
71401
+ "text",
71402
+ void 0,
71403
+ "executor"
71404
+ );
71405
+ try {
71406
+ const fixPrompt = `Fix the failing ${failureContext.type} verification for task ${task.id}.
71407
+
71408
+ ## Failed command
71409
+ Command: \`${failureContext.command}\`
71410
+ Exit code: ${failureContext.exitCode}
71411
+
71412
+ ## Error output
71413
+ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
71414
+
71415
+ ## Instructions
71416
+ 1. Read the error output and identify the root cause
71417
+ 2. Make targeted fixes to resolve the failure
71418
+ 3. Run the verification command \`${failureContext.command}\` to confirm your fix works
71419
+ 4. If the fix doesn't work, try a different approach
71420
+ 5. Do NOT make any git commits`;
71421
+ await withRateLimitRetry(async () => {
71422
+ await promptWithFallback(session, fixPrompt);
71423
+ }, {
71424
+ onRetry: (attempt, delayMs, error) => {
71425
+ const delaySec = Math.round(delayMs / 1e3);
71426
+ executorLog.warn(`\u23F3 ${task.id} executor fix agent rate limited \u2014 retry ${attempt} in ${delaySec}s: ${error.message}`);
71427
+ }
71428
+ });
71429
+ await accumulateSessionTokenUsage(this.store, task.id, session);
71430
+ executorLog.log(`${task.id}: re-running deterministic verification after fix attempt ${retryNumber}/${maxRetries}`);
71431
+ await this.store.logEntry(
71432
+ task.id,
71433
+ `Re-running deterministic verification (attempt ${retryNumber}/${maxRetries})`,
71434
+ void 0,
71435
+ this.currentRunContext
71436
+ );
71437
+ await this.store.appendAgentLog(
71438
+ task.id,
71439
+ `Re-running verification (attempt ${retryNumber}/${maxRetries})`,
71440
+ "text",
71441
+ void 0,
71442
+ "executor"
71443
+ );
71444
+ const reRunResult = await this.runExecutorDeterministicVerification(task, worktreePath, settings);
71445
+ return reRunResult.allPassed;
71446
+ } finally {
71447
+ await logger2.flush();
71448
+ await session.dispose();
71449
+ }
71450
+ } catch (err) {
71451
+ const errorMessage = err instanceof Error ? err.message : String(err);
71452
+ executorLog.warn(`${task.id}: executor verification fix agent error: ${errorMessage}`);
71453
+ await this.store.logEntry(
71454
+ task.id,
71455
+ `Executor verification fix agent encountered an error`,
71456
+ errorMessage,
71457
+ this.currentRunContext
71458
+ );
71459
+ await this.store.appendAgentLog(
71460
+ task.id,
71461
+ "Fix agent encountered an error",
71462
+ "tool_error",
71463
+ errorMessage,
71464
+ "executor"
71465
+ );
71466
+ return false;
71467
+ }
71468
+ }
70814
71469
  async handleWorkflowStepFailure(task, worktreePath, failureFeedback, stepName) {
70815
71470
  this.clearCompletedTaskWatchdog(task.id);
70816
71471
  const currentRetries = task.workflowStepRetries ?? 0;
@@ -70880,7 +71535,7 @@ Please fix the issues so the verification can pass on the next attempt.`,
70880
71535
  * The section is replaced entirely to avoid accumulation of old feedback.
70881
71536
  */
70882
71537
  async injectWorkflowStepFailureInstructions(task, failureFeedback, stepName, retryCount) {
70883
- const promptPath = join35(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
71538
+ const promptPath = join36(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
70884
71539
  let content;
70885
71540
  try {
70886
71541
  content = await readFile15(promptPath, "utf-8");
@@ -70945,31 +71600,33 @@ ${failureFeedback}
70945
71600
  * Uses git diff against the stored baseCommitSha to determine what changed.
70946
71601
  * Returns an empty array if no changes or if git commands fail.
70947
71602
  */
71603
+ async resolveDiffBaseRef(worktreePath, baseCommitSha) {
71604
+ if (baseCommitSha) return baseCommitSha;
71605
+ try {
71606
+ const { stdout } = await execAsync5(
71607
+ "git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main",
71608
+ { cwd: worktreePath, encoding: "utf-8" }
71609
+ );
71610
+ const ref = stdout.trim();
71611
+ if (ref) return ref;
71612
+ } catch (mergeBaseErr) {
71613
+ const mergeBaseMsg = mergeBaseErr instanceof Error ? mergeBaseErr.message : String(mergeBaseErr);
71614
+ executorLog.warn(`Failed merge-base lookup for diff base in ${worktreePath}, trying HEAD~1 fallback: ${mergeBaseMsg}`);
71615
+ }
71616
+ try {
71617
+ const { stdout } = await execAsync5("git rev-parse HEAD~1", {
71618
+ cwd: worktreePath,
71619
+ encoding: "utf-8"
71620
+ });
71621
+ return stdout.trim() || void 0;
71622
+ } catch {
71623
+ executorLog.log(`Could not determine base commit for diff in ${worktreePath}`);
71624
+ return void 0;
71625
+ }
71626
+ }
70948
71627
  async captureModifiedFiles(worktreePath, baseCommitSha) {
70949
71628
  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
- }
71629
+ const baseRef = await this.resolveDiffBaseRef(worktreePath, baseCommitSha);
70973
71630
  if (!baseRef) {
70974
71631
  return [];
70975
71632
  }
@@ -71205,6 +71862,30 @@ ${failureFeedback}
71205
71862
  */
71206
71863
  async executeWorkflowStep(task, workflowStep, worktreePath, settings) {
71207
71864
  const toolMode = workflowStep.toolMode || "readonly";
71865
+ const scopedFiles = await this.captureModifiedFiles(worktreePath, task.baseCommitSha);
71866
+ let diffShortstat;
71867
+ try {
71868
+ const baseRef = await this.resolveDiffBaseRef(worktreePath, task.baseCommitSha);
71869
+ if (baseRef) {
71870
+ const { stdout } = await execAsync5(`git diff --shortstat ${baseRef}..HEAD`, {
71871
+ cwd: worktreePath,
71872
+ encoding: "utf-8"
71873
+ });
71874
+ diffShortstat = stdout.trim() || void 0;
71875
+ }
71876
+ } catch {
71877
+ }
71878
+ const MAX_SCOPE_FILES = 100;
71879
+ 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")}
71880
+ - ... (${scopedFiles.length - MAX_SCOPE_FILES} more files truncated)` : scopedFiles.map((f) => `- ${f}`).join("\n");
71881
+ const scopeBlock = `Diff Scope (files changed by THIS task vs base):
71882
+ ${scopeFileBlock}${diffShortstat ? `
71883
+ Diff stat: ${diffShortstat}` : ""}
71884
+
71885
+ CRITICAL SCOPING RULES \u2014 read before doing anything else:
71886
+ - Review ONLY the files listed above. Do NOT analyze unmodified files or unrelated parts of the codebase.
71887
+ - 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.
71888
+ - Your wall-clock budget is short. Spending it browsing unmodified files will cause this step to time out and block merge.`;
71208
71889
  const systemPrompt = `You are a workflow step agent executing: ${workflowStep.name}
71209
71890
 
71210
71891
  Task Context:
@@ -71212,6 +71893,8 @@ Task Context:
71212
71893
  - Task Description: ${task.description}
71213
71894
  - Worktree: ${worktreePath}
71214
71895
 
71896
+ ${scopeBlock}
71897
+
71215
71898
  Your role:
71216
71899
  - Execute this workflow step exactly as scoped.
71217
71900
  - Prioritize high-impact correctness/risk findings over stylistic nits.
@@ -71680,7 +72363,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
71680
72363
  * rather than fail the task permanently.
71681
72364
  */
71682
72365
  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}"`;
72366
+ const command = isAbsolute11(startPoint) && existsSync29(startPoint) ? `git -C "${startPoint}" rev-parse --verify HEAD^{commit}` : `git rev-parse --verify "${startPoint}^{commit}"`;
71684
72367
  try {
71685
72368
  const { stdout } = await execAsync5(command, { cwd: this.rootDir });
71686
72369
  return stdout.trim() || startPoint;
@@ -71700,7 +72383,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
71700
72383
  */
71701
72384
  async tryCreateWorktree(branch, path2, taskId, startPoint, attemptNumber = 0, recoveryDepth = 0) {
71702
72385
  await this.assertWorktreePathNotNested(path2, taskId);
71703
- if (existsSync28(path2)) {
72386
+ if (existsSync29(path2)) {
71704
72387
  const isRegistered = await this.isRegisteredWorktree(path2);
71705
72388
  if (!isRegistered) {
71706
72389
  await this.store.logEntry(
@@ -71851,7 +72534,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
71851
72534
  );
71852
72535
  if (shouldGenerateNewName) {
71853
72536
  const conflictStartPoint = branch;
71854
- const newPath = join35(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
72537
+ const newPath = join36(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
71855
72538
  for (let suffix = 2; suffix <= 6; suffix++) {
71856
72539
  const suffixedBranch = `${branch}-${suffix}`;
71857
72540
  try {
@@ -72451,7 +73134,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
72451
73134
  metadata: { type: "spawned", parentTaskId: taskId }
72452
73135
  });
72453
73136
  const childWorktreeName = generateWorktreeName(this.rootDir);
72454
- const childWorktreePath = join35(this.rootDir, ".worktrees", childWorktreeName);
73137
+ const childWorktreePath = join36(this.rootDir, ".worktrees", childWorktreeName);
72455
73138
  const childBranch = `fusion/spawn-${agent.id}`;
72456
73139
  await this.createWorktree(childBranch, childWorktreePath, taskId, worktreePath);
72457
73140
  await this.options.agentStore.updateAgentState(agent.id, "active");
@@ -72640,9 +73323,9 @@ var init_node_routing_policy = __esm({
72640
73323
  });
72641
73324
 
72642
73325
  // ../engine/src/scheduler.ts
72643
- import { existsSync as existsSync29 } from "node:fs";
73326
+ import { existsSync as existsSync30 } from "node:fs";
72644
73327
  import { readFile as readFile16 } from "node:fs/promises";
72645
- import { basename as basename8, join as join36 } from "node:path";
73328
+ import { basename as basename8, join as join37 } from "node:path";
72646
73329
  function pathsOverlap2(a, b) {
72647
73330
  for (const pa of a) {
72648
73331
  const prefixA = pa.endsWith("/*") ? pa.slice(0, -1) : null;
@@ -72812,12 +73495,12 @@ var init_scheduler = __esm({
72812
73495
  * @returns Object with `valid: true` if checks pass, or `valid: false` with a `reason` string if they fail
72813
73496
  */
72814
73497
  async validateTaskFilesystem(id) {
72815
- const taskDir = join36(this.store.getTasksDir(), id);
72816
- if (!existsSync29(taskDir)) {
73498
+ const taskDir = join37(this.store.getTasksDir(), id);
73499
+ if (!existsSync30(taskDir)) {
72817
73500
  return { valid: false, reason: "missing directory" };
72818
73501
  }
72819
- const promptPath = join36(taskDir, "PROMPT.md");
72820
- if (!existsSync29(promptPath)) {
73502
+ const promptPath = join37(taskDir, "PROMPT.md");
73503
+ if (!existsSync30(promptPath)) {
72821
73504
  return { valid: false, reason: "missing or empty PROMPT.md" };
72822
73505
  }
72823
73506
  try {
@@ -72956,7 +73639,7 @@ var init_scheduler = __esm({
72956
73639
  break;
72957
73640
  }
72958
73641
  reservedNames.add(worktreeName);
72959
- return join36(this.store.getRootDir(), ".worktrees", worktreeName);
73642
+ return join37(this.store.getRootDir(), ".worktrees", worktreeName);
72960
73643
  }
72961
73644
  /**
72962
73645
  * Run one scheduling pass.
@@ -74233,7 +74916,7 @@ var init_mission_execution_loop = __esm({
74233
74916
  init_pi();
74234
74917
  init_agent_session_helpers();
74235
74918
  init_logger2();
74236
- init_notifier();
74919
+ init_fallback_model_observer();
74237
74920
  loopLog = createLogger2("mission-loop");
74238
74921
  VALIDATION_TIMEOUT_MS = 10 * 60 * 1e3;
74239
74922
  MissionExecutionLoop = class extends EventEmitter17 {
@@ -74467,7 +75150,13 @@ Assertions: ${assertions.map((a) => a.title).join(", ")}`,
74467
75150
  },
74468
75151
  taskId: task?.id,
74469
75152
  taskTitle: task?.title,
74470
- onFallbackModelUsed: notifyFallbackUsed
75153
+ onFallbackModelUsed: createFallbackModelObserver({
75154
+ agent: "reviewer",
75155
+ label: "mission validator",
75156
+ store: this.taskStore,
75157
+ taskId: task?.id,
75158
+ taskTitle: task?.title
75159
+ })
74471
75160
  });
74472
75161
  session = { session: sessionResult.session, sessionFile: sessionResult.sessionFile };
74473
75162
  loopLog.log(`Validation session created for feature ${feature.id}`);
@@ -77732,7 +78421,7 @@ async function createAiPromptExecutor(cwd) {
77732
78421
  }
77733
78422
  };
77734
78423
  }
77735
- function truncateOutput(stdout, stderr) {
78424
+ function truncateOutput2(stdout, stderr) {
77736
78425
  const out = stdout ?? "";
77737
78426
  const err = stderr ?? "";
77738
78427
  let combined = out;
@@ -77912,7 +78601,7 @@ var init_cron_runner = __esm({
77912
78601
  maxBuffer: MAX_BUFFER,
77913
78602
  shell: defaultShell
77914
78603
  });
77915
- const output = truncateOutput(stdout, stderr);
78604
+ const output = truncateOutput2(stdout, stderr);
77916
78605
  log15.log(`\u2713 ${schedule.name} completed (${output.length} bytes output)`);
77917
78606
  return {
77918
78607
  success: true,
@@ -77923,7 +78612,7 @@ var init_cron_runner = __esm({
77923
78612
  } catch (err) {
77924
78613
  const stdout = err.stdout ?? "";
77925
78614
  const stderr = err.stderr ?? "";
77926
- const output = truncateOutput(stdout, stderr);
78615
+ const output = truncateOutput2(stdout, stderr);
77927
78616
  const errorMessage = err.killed ? `Command timed out after ${(schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS6) / 1e3}s` : err.message ?? String(err);
77928
78617
  log15.warn(`\u2717 ${schedule.name} failed: ${errorMessage}`);
77929
78618
  return {
@@ -77968,7 +78657,7 @@ var init_cron_runner = __esm({
77968
78657
  const result = await runBackupCommand2(fusionDir, settings);
77969
78658
  return {
77970
78659
  success: result.success,
77971
- output: truncateOutput(result.output ?? "", ""),
78660
+ output: truncateOutput2(result.output ?? "", ""),
77972
78661
  error: result.success ? void 0 : result.output
77973
78662
  };
77974
78663
  } catch (err) {
@@ -78009,7 +78698,7 @@ var init_cron_runner = __esm({
78009
78698
  if (sr.output) outputParts.push(sr.output);
78010
78699
  if (sr.error) outputParts.push(`Error: ${sr.error}`);
78011
78700
  }
78012
- const output = truncateOutput(outputParts.join("\n"), "");
78701
+ const output = truncateOutput2(outputParts.join("\n"), "");
78013
78702
  const failedSteps = stepResults.filter((sr) => !sr.success);
78014
78703
  const error = failedSteps.length > 0 ? `${failedSteps.length} step(s) failed: ${failedSteps.map((s) => s.stepName).join(", ")}${stoppedEarly ? " (execution stopped)" : ""}` : void 0;
78015
78704
  const status = overallSuccess ? "\u2713" : "\u2717";
@@ -78088,7 +78777,7 @@ var init_cron_runner = __esm({
78088
78777
  stepName: step.name,
78089
78778
  stepIndex,
78090
78779
  success: true,
78091
- output: truncateOutput(stdout, stderr),
78780
+ output: truncateOutput2(stdout, stderr),
78092
78781
  startedAt,
78093
78782
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
78094
78783
  };
@@ -78101,7 +78790,7 @@ var init_cron_runner = __esm({
78101
78790
  stepName: step.name,
78102
78791
  stepIndex,
78103
78792
  success: false,
78104
- output: truncateOutput(stdout, stderr),
78793
+ output: truncateOutput2(stdout, stderr),
78105
78794
  error: errorMessage,
78106
78795
  startedAt,
78107
78796
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -78251,7 +78940,7 @@ var init_cron_runner = __esm({
78251
78940
  // ../engine/src/routine-runner.ts
78252
78941
  import { exec as exec8 } from "node:child_process";
78253
78942
  import { promisify as promisify8 } from "node:util";
78254
- function truncateOutput2(stdout, stderr) {
78943
+ function truncateOutput3(stdout, stderr) {
78255
78944
  let output = stdout;
78256
78945
  if (stderr) {
78257
78946
  output += stdout ? "\n--- stderr ---\n" : "";
@@ -78434,7 +79123,7 @@ var init_routine_runner = __esm({
78434
79123
  const result = await runBackupCommand2(fusionDir, settings);
78435
79124
  return {
78436
79125
  success: result.success,
78437
- output: truncateOutput2(result.output ?? "", ""),
79126
+ output: truncateOutput3(result.output ?? "", ""),
78438
79127
  error: result.success ? void 0 : result.output,
78439
79128
  startedAt,
78440
79129
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -78458,7 +79147,7 @@ var init_routine_runner = __esm({
78458
79147
  });
78459
79148
  return {
78460
79149
  success: true,
78461
- output: truncateOutput2(stdout, stderr),
79150
+ output: truncateOutput3(stdout, stderr),
78462
79151
  startedAt,
78463
79152
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
78464
79153
  };
@@ -78469,7 +79158,7 @@ var init_routine_runner = __esm({
78469
79158
  const error = errObj.killed === true ? `Command timed out after ${(timeoutMs ?? DEFAULT_TIMEOUT_MS7) / 1e3}s` : (err instanceof Error ? err.message : null) ?? String(err);
78470
79159
  return {
78471
79160
  success: false,
78472
- output: truncateOutput2(stdout, stderr),
79161
+ output: truncateOutput3(stdout, stderr),
78473
79162
  error,
78474
79163
  startedAt,
78475
79164
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -78502,7 +79191,7 @@ var init_routine_runner = __esm({
78502
79191
  const failedSteps = stepResults.filter((sr) => !sr.success);
78503
79192
  return {
78504
79193
  success: overallSuccess,
78505
- output: truncateOutput2(outputParts.join("\n"), ""),
79194
+ output: truncateOutput3(outputParts.join("\n"), ""),
78506
79195
  error: failedSteps.length > 0 ? `${failedSteps.length} step(s) failed: ${failedSteps.map((s) => s.stepName).join(", ")}${stoppedEarly ? " (execution stopped)" : ""}` : void 0,
78507
79196
  startedAt,
78508
79197
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -78537,7 +79226,7 @@ var init_routine_runner = __esm({
78537
79226
  this.options.aiPromptExecutor(step.prompt, step.modelProvider, step.modelId),
78538
79227
  new Promise((_resolve, reject) => setTimeout(() => reject(new Error(`AI prompt step timed out after ${timeoutMs / 1e3}s`)), timeoutMs))
78539
79228
  ]);
78540
- return { stepId: step.id, stepName: step.name, stepIndex, success: true, output: truncateOutput2(output, ""), startedAt, completedAt: (/* @__PURE__ */ new Date()).toISOString() };
79229
+ return { stepId: step.id, stepName: step.name, stepIndex, success: true, output: truncateOutput3(output, ""), startedAt, completedAt: (/* @__PURE__ */ new Date()).toISOString() };
78541
79230
  } catch (err) {
78542
79231
  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
79232
  }
@@ -79151,8 +79840,8 @@ var init_stuck_task_detector = __esm({
79151
79840
  // ../engine/src/self-healing.ts
79152
79841
  import { exec as exec9 } from "node:child_process";
79153
79842
  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";
79843
+ import { existsSync as existsSync31, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
79844
+ import { isAbsolute as isAbsolute13, join as join38, relative as relative8, resolve as resolve17 } from "node:path";
79156
79845
  function shellQuote(value) {
79157
79846
  return `'${value.replace(/'/g, "'\\''")}'`;
79158
79847
  }
@@ -79548,7 +80237,7 @@ var init_self_healing = __esm({
79548
80237
  return commit;
79549
80238
  }
79550
80239
  async cleanupInterruptedMergeArtifacts(task) {
79551
- if (task.worktree && existsSync30(task.worktree)) {
80240
+ if (task.worktree && existsSync31(task.worktree)) {
79552
80241
  try {
79553
80242
  await execAsync7(`git worktree remove ${shellQuote(task.worktree)} --force`, {
79554
80243
  cwd: this.options.rootDir,
@@ -80169,7 +80858,7 @@ var init_self_healing = __esm({
80169
80858
  return false;
80170
80859
  }
80171
80860
  const staleness = now - new Date(t.updatedAt).getTime();
80172
- const hasWorktree = t.worktree && existsSync30(t.worktree);
80861
+ const hasWorktree = t.worktree && existsSync31(t.worktree);
80173
80862
  const graceMs = hasWorktree ? ORPHANED_WITH_WORKTREE_GRACE_MS : ORPHANED_EXECUTION_RECOVERY_GRACE_MS;
80174
80863
  return staleness >= graceMs;
80175
80864
  });
@@ -80178,7 +80867,7 @@ var init_self_healing = __esm({
80178
80867
  let recovered = 0;
80179
80868
  for (const task of orphaned) {
80180
80869
  try {
80181
- const hadWorktree = task.worktree && existsSync30(task.worktree);
80870
+ const hadWorktree = task.worktree && existsSync31(task.worktree);
80182
80871
  const reason = hadWorktree ? "worktree exists but no active session" : "missing worktree/session";
80183
80872
  await this.resetStepsIfWorkLost(task);
80184
80873
  await this.store.updateTask(task.id, {
@@ -80324,7 +81013,7 @@ var init_self_healing = __esm({
80324
81013
  }
80325
81014
  }
80326
81015
  async hasRecoverableGitWork(task) {
80327
- if (task.worktree && existsSync30(task.worktree)) {
81016
+ if (task.worktree && existsSync31(task.worktree)) {
80328
81017
  try {
80329
81018
  const { stdout: status } = await execAsync7("git status --porcelain", {
80330
81019
  cwd: task.worktree,
@@ -80509,11 +81198,11 @@ var init_self_healing = __esm({
80509
81198
  * tracks registered idle worktrees, never these orphans.
80510
81199
  */
80511
81200
  async reapUnregisteredOrphans() {
80512
- const worktreesDir = join37(this.options.rootDir, ".worktrees");
80513
- if (!existsSync30(worktreesDir)) return 0;
81201
+ const worktreesDir = join38(this.options.rootDir, ".worktrees");
81202
+ if (!existsSync31(worktreesDir)) return 0;
80514
81203
  let dirs;
80515
81204
  try {
80516
- dirs = readdirSync5(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join37(worktreesDir, e.name));
81205
+ dirs = readdirSync5(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join38(worktreesDir, e.name));
80517
81206
  } catch (err) {
80518
81207
  log17.warn(`Failed to read .worktrees/ for unregistered orphan reap: ${err instanceof Error ? err.message : String(err)}`);
80519
81208
  return 0;
@@ -80618,8 +81307,8 @@ var init_self_healing = __esm({
80618
81307
  }
80619
81308
  /** Remove oldest idle worktrees if total count exceeds 2× maxWorktrees. */
80620
81309
  async enforceWorktreeCap() {
80621
- const worktreesDir = join37(this.options.rootDir, ".worktrees");
80622
- if (!existsSync30(worktreesDir)) return;
81310
+ const worktreesDir = join38(this.options.rootDir, ".worktrees");
81311
+ if (!existsSync31(worktreesDir)) return;
80623
81312
  try {
80624
81313
  const settings = await this.store.getSettings();
80625
81314
  const cap = (settings.maxWorktrees ?? 4) * 2;
@@ -82457,7 +83146,7 @@ var init_ipc_host = __esm({
82457
83146
  import { EventEmitter as EventEmitter20 } from "node:events";
82458
83147
  import { fork } from "node:child_process";
82459
83148
  import { fileURLToPath as fileURLToPath3 } from "node:url";
82460
- import { dirname as dirname11, join as join38 } from "node:path";
83149
+ import { dirname as dirname11, join as join39 } from "node:path";
82461
83150
  var HealthMonitor, ChildProcessRuntime;
82462
83151
  var init_child_process_runtime = __esm({
82463
83152
  "../engine/src/runtimes/child-process-runtime.ts"() {
@@ -82619,7 +83308,7 @@ var init_child_process_runtime = __esm({
82619
83308
  const isCompiled = !import.meta.url.endsWith(".ts");
82620
83309
  const currentDir = dirname11(fileURLToPath3(import.meta.url));
82621
83310
  const workerFile = isCompiled ? "child-process-worker.js" : "child-process-worker.ts";
82622
- return join38(currentDir, workerFile);
83311
+ return join39(currentDir, workerFile);
82623
83312
  }
82624
83313
  /**
82625
83314
  * Set up event forwarding from IPC host to runtime listeners.
@@ -86872,6 +87561,8 @@ __export(src_exports2, {
86872
87561
  describeAgentModel: () => describeAgentModel,
86873
87562
  describeModel: () => describeModel,
86874
87563
  ensureDefaultHeartbeatProcedureFile: () => ensureDefaultHeartbeatProcedureFile,
87564
+ extractRuntimeHint: () => extractRuntimeHint,
87565
+ extractRuntimeModel: () => extractRuntimeModel,
86875
87566
  formatTaskIdentifier: () => formatTaskIdentifier,
86876
87567
  getDefaultPiRuntime: () => getDefaultPiRuntime,
86877
87568
  getHostExtensionPaths: () => getHostExtensionPaths,
@@ -101430,7 +102121,7 @@ var init_auth_middleware = __esm({
101430
102121
 
101431
102122
  // ../dashboard/src/server.ts
101432
102123
  import express from "express";
101433
- import { join as join39, dirname as dirname12 } from "node:path";
102124
+ import { join as join40, dirname as dirname12 } from "node:path";
101434
102125
  import { fileURLToPath as fileURLToPath4 } from "node:url";
101435
102126
  function clearAiSessionCleanupInterval() {
101436
102127
  if (!aiSessionCleanupIntervalHandle) {
@@ -101722,8 +102413,8 @@ __export(task_exports, {
101722
102413
  runTaskUpdate: () => runTaskUpdate
101723
102414
  });
101724
102415
  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";
102416
+ import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync32, readFileSync as readFileSync11 } from "node:fs";
102417
+ import { basename as basename10, join as join41 } from "node:path";
101727
102418
  function getGitHubIssueUrl(sourceMetadata) {
101728
102419
  if (!sourceMetadata || typeof sourceMetadata !== "object") return void 0;
101729
102420
  const issueUrl = sourceMetadata.issueUrl;
@@ -102036,8 +102727,8 @@ async function runTaskLogs(id, options = {}, projectName) {
102036
102727
  printEntries(filteredEntries);
102037
102728
  if (options.follow) {
102038
102729
  const projectPath = projectContext?.projectPath ?? process.cwd();
102039
- const logPath = join40(projectPath, ".fusion", "tasks", id, "agent.log");
102040
- if (!existsSync31(logPath)) {
102730
+ const logPath = join41(projectPath, ".fusion", "tasks", id, "agent.log");
102731
+ if (!existsSync32(logPath)) {
102041
102732
  console.log(`
102042
102733
  Waiting for log file to be created...`);
102043
102734
  }
@@ -102066,7 +102757,7 @@ async function runTaskLogs(id, options = {}, projectName) {
102066
102757
  lastPosition = 0;
102067
102758
  }
102068
102759
  if (stats.size > lastPosition) {
102069
- const content = readFileSync10(logPath, "utf-8");
102760
+ const content = readFileSync11(logPath, "utf-8");
102070
102761
  const lines = content.slice(lastPosition).split("\n");
102071
102762
  for (const line of lines) {
102072
102763
  if (!line.trim()) continue;
@@ -103179,9 +103870,9 @@ init_src();
103179
103870
  init_gh_cli();
103180
103871
  import { Type as Type8 } from "typebox";
103181
103872
  import { StringEnum } from "@mariozechner/pi-ai";
103182
- import { resolve as resolve19, basename as basename11, extname as extname3, join as join41 } from "node:path";
103873
+ import { resolve as resolve19, basename as basename11, extname as extname3, join as join42 } from "node:path";
103183
103874
  import { readFile as readFile19 } from "node:fs/promises";
103184
- import { existsSync as existsSync32 } from "node:fs";
103875
+ import { existsSync as existsSync33 } from "node:fs";
103185
103876
  import { spawn as spawn11 } from "node:child_process";
103186
103877
  var MIME_TYPES2 = {
103187
103878
  ".png": "image/png",
@@ -103201,7 +103892,7 @@ var MIME_TYPES2 = {
103201
103892
  function resolveProjectRoot2(cwd) {
103202
103893
  let current = resolve19(cwd);
103203
103894
  while (true) {
103204
- if (existsSync32(join41(current, ".fusion"))) {
103895
+ if (existsSync33(join42(current, ".fusion"))) {
103205
103896
  return current;
103206
103897
  }
103207
103898
  const parent = resolve19(current, "..");
@@ -103222,7 +103913,7 @@ async function getStore2(cwd) {
103222
103913
  return store;
103223
103914
  }
103224
103915
  function getFusionDir(cwd) {
103225
- return join41(resolveProjectRoot2(cwd), ".fusion");
103916
+ return join42(resolveProjectRoot2(cwd), ".fusion");
103226
103917
  }
103227
103918
  async function validateAssignableAgentId(cwd, agentId) {
103228
103919
  const { AgentStore: AgentStore2, isEphemeralAgent: isEphemeralAgent2 } = await Promise.resolve().then(() => (init_src(), src_exports));