@runfusion/fusion 0.17.0 → 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 (34) hide show
  1. package/dist/bin.js +1729 -789
  2. package/dist/client/assets/{AgentDetailView-DGqT1oDt.js → AgentDetailView-17J-F0Rl.js} +3 -3
  3. package/dist/client/assets/{AgentsView-BmemrfrO.js → AgentsView-sbBkb7Wd.js} +45 -45
  4. package/dist/client/assets/ChatView-BR5cvK_B.js +1 -0
  5. package/dist/client/assets/{DevServerView-C3Q0XqDA.js → DevServerView-GFFVXHVP.js} +1 -1
  6. package/dist/client/assets/{DirectoryPicker-BZWVA9ND.js → DirectoryPicker-WPDSBdT6.js} +1 -1
  7. package/dist/client/assets/{DocumentsView-DO48ivSq.js → DocumentsView-BHpDsIIt.js} +1 -1
  8. package/dist/client/assets/{InsightsView-CAngTfMf.js → InsightsView-Bxu0TJkt.js} +1 -1
  9. package/dist/client/assets/{MemoryView-B3rNcAOW.js → MemoryView-CmnzZorw.js} +2 -2
  10. package/dist/client/assets/{NodesView-BnV1LWa8.js → NodesView-CO9_4hCr.js} +4 -4
  11. package/dist/client/assets/{PiExtensionsManager-C3_Lw4sa.js → PiExtensionsManager-4e3MlD62.js} +3 -3
  12. package/dist/client/assets/{PluginManager-Vv3nzrJ1.js → PluginManager-DGN2rvOY.js} +1 -1
  13. package/dist/client/assets/ResearchView-Dsa6Gykl.js +1 -0
  14. package/dist/client/assets/{RoadmapsView-BiIpE-b8.js → RoadmapsView-jHTOK0RQ.js} +2 -2
  15. package/dist/client/assets/{SettingsModal-CK4w8Ztb.js → SettingsModal-4Z8ZJMzD.js} +1 -1
  16. package/dist/client/assets/SettingsModal-D0kuJpBA.js +31 -0
  17. package/dist/client/assets/{SetupWizardModal-Dw6N4UvY.js → SetupWizardModal-Bhumd4Rf.js} +1 -1
  18. package/dist/client/assets/{SkillsView-C1196wgA.js → SkillsView-MHweJTz4.js} +1 -1
  19. package/dist/client/assets/{folder-open-WVtgE4k3.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-MSImEC8V.js → star-7L86NZrT.js} +1 -1
  23. package/dist/client/assets/{upload-Dmvy3xXd.js → upload-DsAS6tno.js} +1 -1
  24. package/dist/client/assets/{users-CncYvHNf.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 +1239 -527
  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-CZQUBFlV.js +0 -1
  32. package/dist/client/assets/ResearchView-Dfdsuc21.js +0 -1
  33. package/dist/client/assets/SettingsModal-BN00HYJ2.js +0 -31
  34. package/dist/client/assets/index-Bv0TGzDH.js +0 -682
package/dist/bin.js CHANGED
@@ -1391,7 +1391,14 @@ Note: Refs (@e1, @e2) are invalidated after page navigation. Re-snapshot after c
1391
1391
  toolMode: "readonly",
1392
1392
  prompt: `You are a UX design reviewer. Verify frontend changes maintain visual polish and consistency with existing UI patterns and design tokens.
1393
1393
 
1394
- Design System Review:
1394
+ FAST-BAIL RULE (check this FIRST):
1395
+ - The task harness gives you a "Diff Scope" listing the files this task actually changed.
1396
+ - 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.
1397
+ - 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.
1398
+
1399
+ Otherwise, restrict your review to the UI files actually present in the diff scope.
1400
+
1401
+ Design System Review (only for UI files in the diff scope):
1395
1402
  1. **Visual Hierarchy** \u2014 Check that the changes maintain consistent heading levels, content flow, and information architecture
1396
1403
  2. **Spacing and Typography** \u2014 Verify consistent spacing (margins, padding, gaps) and typography scale usage
1397
1404
  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
@@ -1399,15 +1406,16 @@ Design System Review:
1399
1406
  5. **Responsive Behavior** \u2014 Check that layouts adapt properly across viewport sizes and maintain usability on mobile
1400
1407
  6. **Fit with Design Language** \u2014 Verify the visual style matches existing patterns (border radius, shadows, transitions, icon style, etc.)
1401
1408
 
1402
- Files to Review:
1409
+ Files to Review (only those that appear in the Diff Scope):
1403
1410
  - Modified UI components (React, Vue, Angular, HTML)
1404
1411
  - CSS/SCSS/styled-component files
1405
1412
  - Design token or theme configuration files
1406
1413
 
1407
1414
  Output Requirements:
1408
- - If design is consistent and polished: call task_done() with success status
1409
- - If issues found: describe each finding with specific file paths and suggested corrections via task_log()
1410
- - Prioritize issues by impact: layout breaks > visual inconsistency > style preferences`
1415
+ - If design is consistent and polished (or there are no UI files in scope): respond with a brief approval line and stop.
1416
+ - If issues found: start your response with "REQUEST REVISION" and describe each finding with specific file paths and suggested corrections.
1417
+ - Prioritize issues by impact: layout breaks > visual inconsistency > style preferences.
1418
+ - Do NOT spend time on stylistic nits when no real issues exist.`
1411
1419
  }
1412
1420
  ];
1413
1421
  DOCUMENT_KEY_RE = /^[a-zA-Z0-9_-]{1,64}$/;
@@ -52659,6 +52667,193 @@ var init_chat_store = __esm({
52659
52667
  }
52660
52668
  });
52661
52669
 
52670
+ // ../core/src/oauth-credential-interop.ts
52671
+ import { existsSync as existsSync19, readFileSync as readFileSync6 } from "node:fs";
52672
+ import { homedir as homedir4 } from "node:os";
52673
+ import { join as join22 } from "node:path";
52674
+ function getHomeDir4() {
52675
+ return process.env.HOME || process.env.USERPROFILE || homedir4();
52676
+ }
52677
+ function getCodexCliAuthPath(home = getHomeDir4()) {
52678
+ return join22(home, ".codex", "auth.json");
52679
+ }
52680
+ function parseJwtPayload(token) {
52681
+ try {
52682
+ const [, payload = ""] = token.split(".", 3);
52683
+ if (!payload) {
52684
+ return null;
52685
+ }
52686
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
52687
+ } catch {
52688
+ return null;
52689
+ }
52690
+ }
52691
+ function getJwtExpiryMs(token) {
52692
+ if (!token) {
52693
+ return void 0;
52694
+ }
52695
+ const payload = parseJwtPayload(token);
52696
+ const exp = payload?.exp;
52697
+ if (typeof exp !== "number" || !Number.isFinite(exp)) {
52698
+ return void 0;
52699
+ }
52700
+ return exp * 1e3;
52701
+ }
52702
+ function getCodexAccountId(accessToken, fallbackAccountId) {
52703
+ const payload = parseJwtPayload(accessToken);
52704
+ const authClaim = payload?.[OPENAI_AUTH_CLAIM];
52705
+ const claimAccountId = authClaim && typeof authClaim === "object" ? authClaim.chatgpt_account_id : void 0;
52706
+ if (typeof claimAccountId === "string" && claimAccountId.trim().length > 0) {
52707
+ return claimAccountId;
52708
+ }
52709
+ if (typeof fallbackAccountId === "string" && fallbackAccountId.trim().length > 0) {
52710
+ return fallbackAccountId;
52711
+ }
52712
+ return void 0;
52713
+ }
52714
+ function getLastRefreshFallbackExpiryMs(lastRefresh) {
52715
+ if (typeof lastRefresh !== "string" || lastRefresh.trim().length === 0) {
52716
+ return void 0;
52717
+ }
52718
+ const parsed = Date.parse(lastRefresh);
52719
+ if (!Number.isFinite(parsed)) {
52720
+ return void 0;
52721
+ }
52722
+ return parsed + CODEX_REFRESH_FALLBACK_WINDOW_MS;
52723
+ }
52724
+ function isStoredAuthCredential(value) {
52725
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
52726
+ return false;
52727
+ }
52728
+ const record = value;
52729
+ return record.type === "oauth" || record.type === "api_key";
52730
+ }
52731
+ function isValidOauthCredential(credential) {
52732
+ 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;
52733
+ }
52734
+ function isRefreshableOauthCredential(credential) {
52735
+ return credential?.type === "oauth" && typeof credential.refresh === "string" && credential.refresh.length > 0 && typeof credential.expires === "number" && Number.isFinite(credential.expires);
52736
+ }
52737
+ function compareStoredCredentials(left, right) {
52738
+ if (!left && !right) {
52739
+ return 0;
52740
+ }
52741
+ if (left && !right) {
52742
+ return 1;
52743
+ }
52744
+ if (!left && right) {
52745
+ return -1;
52746
+ }
52747
+ if (left?.type === "api_key" && right?.type !== "api_key") {
52748
+ return 1;
52749
+ }
52750
+ if (right?.type === "api_key" && left?.type !== "api_key") {
52751
+ return -1;
52752
+ }
52753
+ if (left?.type === "oauth" && right?.type === "oauth") {
52754
+ const leftValid = isValidOauthCredential(left);
52755
+ const rightValid = isValidOauthCredential(right);
52756
+ if (leftValid !== rightValid) {
52757
+ return leftValid ? 1 : -1;
52758
+ }
52759
+ const leftRefreshable = isRefreshableOauthCredential(left);
52760
+ const rightRefreshable = isRefreshableOauthCredential(right);
52761
+ if (leftRefreshable !== rightRefreshable) {
52762
+ return leftRefreshable ? 1 : -1;
52763
+ }
52764
+ const leftExpiry = typeof left.expires === "number" && Number.isFinite(left.expires) ? left.expires : -Infinity;
52765
+ const rightExpiry = typeof right.expires === "number" && Number.isFinite(right.expires) ? right.expires : -Infinity;
52766
+ if (leftExpiry !== rightExpiry) {
52767
+ return leftExpiry > rightExpiry ? 1 : -1;
52768
+ }
52769
+ const leftAccessLength = typeof left.access === "string" ? left.access.length : 0;
52770
+ const rightAccessLength = typeof right.access === "string" ? right.access.length : 0;
52771
+ if (leftAccessLength !== rightAccessLength) {
52772
+ return leftAccessLength > rightAccessLength ? 1 : -1;
52773
+ }
52774
+ }
52775
+ return 0;
52776
+ }
52777
+ function choosePreferredStoredCredential(...credentials) {
52778
+ let best;
52779
+ for (const credential of credentials) {
52780
+ if (compareStoredCredentials(credential, best) > 0) {
52781
+ best = credential;
52782
+ }
52783
+ }
52784
+ return best;
52785
+ }
52786
+ function shouldHydrateStoredCredential(current, candidate) {
52787
+ if (!candidate || candidate.type !== "oauth") {
52788
+ return false;
52789
+ }
52790
+ if (current?.type === "api_key") {
52791
+ return false;
52792
+ }
52793
+ return compareStoredCredentials(candidate, current) > 0;
52794
+ }
52795
+ function extractCodexCliStoredCredential(raw) {
52796
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
52797
+ return void 0;
52798
+ }
52799
+ const record = raw;
52800
+ const tokens = record.tokens;
52801
+ if (!tokens || typeof tokens !== "object" || Array.isArray(tokens)) {
52802
+ return void 0;
52803
+ }
52804
+ const tokenRecord = tokens;
52805
+ const access12 = typeof tokenRecord.access_token === "string" ? tokenRecord.access_token : void 0;
52806
+ const refresh = typeof tokenRecord.refresh_token === "string" ? tokenRecord.refresh_token : void 0;
52807
+ if (!access12 || !refresh) {
52808
+ return void 0;
52809
+ }
52810
+ const expires = getJwtExpiryMs(access12) ?? getJwtExpiryMs(typeof tokenRecord.id_token === "string" ? tokenRecord.id_token : void 0) ?? getLastRefreshFallbackExpiryMs(record.last_refresh);
52811
+ if (typeof expires !== "number" || !Number.isFinite(expires)) {
52812
+ return void 0;
52813
+ }
52814
+ const accountId = getCodexAccountId(access12, tokenRecord.account_id);
52815
+ return {
52816
+ type: "oauth",
52817
+ access: access12,
52818
+ refresh,
52819
+ expires,
52820
+ ...accountId ? { accountId } : {}
52821
+ };
52822
+ }
52823
+ function readStoredCredentialsFromAuthFile(authPath) {
52824
+ if (!existsSync19(authPath)) {
52825
+ return {};
52826
+ }
52827
+ try {
52828
+ const parsed = JSON.parse(readFileSync6(authPath, "utf-8"));
52829
+ const codexCliCredential = extractCodexCliStoredCredential(parsed);
52830
+ if (codexCliCredential) {
52831
+ return { "openai-codex": codexCliCredential };
52832
+ }
52833
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
52834
+ return {};
52835
+ }
52836
+ const credentials = {};
52837
+ for (const [providerId, value] of Object.entries(parsed)) {
52838
+ if (!isStoredAuthCredential(value)) {
52839
+ continue;
52840
+ }
52841
+ credentials[providerId] = value;
52842
+ }
52843
+ return credentials;
52844
+ } catch {
52845
+ return {};
52846
+ }
52847
+ }
52848
+ var OPENAI_AUTH_CLAIM, CODEX_REFRESH_FALLBACK_WINDOW_MS;
52849
+ var init_oauth_credential_interop = __esm({
52850
+ "../core/src/oauth-credential-interop.ts"() {
52851
+ "use strict";
52852
+ OPENAI_AUTH_CLAIM = "https://api.openai.com/auth";
52853
+ CODEX_REFRESH_FALLBACK_WINDOW_MS = 55 * 60 * 1e3;
52854
+ }
52855
+ });
52856
+
52662
52857
  // ../core/src/index.ts
52663
52858
  var src_exports = {};
52664
52859
  __export(src_exports, {
@@ -52815,6 +53010,7 @@ __export(src_exports, {
52815
53010
  buildTriageMemoryInstructions: () => buildTriageMemoryInstructions,
52816
53011
  canTransition: () => canTransition,
52817
53012
  checkRateLimit: () => checkRateLimit,
53013
+ choosePreferredStoredCredential: () => choosePreferredStoredCredential,
52818
53014
  classifyInsightRunError: () => classifyInsightRunError,
52819
53015
  clearOverrides: () => clearOverrides,
52820
53016
  collectSystemMetrics: () => collectSystemMetrics,
@@ -52845,6 +53041,7 @@ __export(src_exports, {
52845
53041
  executeInsightRunLifecycle: () => executeInsightRunLifecycle,
52846
53042
  exportAgentsToDirectory: () => exportAgentsToDirectory,
52847
53043
  exportSettings: () => exportSettings,
53044
+ extractCodexCliStoredCredential: () => extractCodexCliStoredCredential,
52848
53045
  extractDreamProcessorResult: () => extractDreamProcessorResult,
52849
53046
  formatPiExtensionSource: () => formatPiExtensionSource,
52850
53047
  fromJson: () => fromJson,
@@ -52855,6 +53052,7 @@ __export(src_exports, {
52855
53052
  generateMemoryAudit: () => generateMemoryAudit,
52856
53053
  getAppVersion: () => getAppVersion,
52857
53054
  getAvailableTemplates: () => getAvailableTemplates,
53055
+ getCodexCliAuthPath: () => getCodexCliAuthPath,
52858
53056
  getCurrentRepo: () => getCurrentRepo,
52859
53057
  getDefaultDailyMemoryScaffold: () => getDefaultDailyMemoryScaffold,
52860
53058
  getDefaultDreamsScaffold: () => getDefaultDreamsScaffold,
@@ -52952,6 +53150,7 @@ __export(src_exports, {
52952
53150
  readProjectMemoryFile: () => readProjectMemoryFile,
52953
53151
  readProjectMemoryFileContent: () => readProjectMemoryFileContent,
52954
53152
  readProjectMemoryWithBackend: () => readProjectMemoryWithBackend,
53153
+ readStoredCredentialsFromAuthFile: () => readStoredCredentialsFromAuthFile,
52955
53154
  readWorkingMemory: () => readWorkingMemory,
52956
53155
  reconcileClaudeCliPaths: () => reconcileClaudeCliPaths,
52957
53156
  reconcileDroidCliPaths: () => reconcileDroidCliPaths,
@@ -52987,6 +53186,7 @@ __export(src_exports, {
52987
53186
  scheduleQmdProjectMemoryRefresh: () => scheduleQmdProjectMemoryRefresh,
52988
53187
  searchProjectMemory: () => searchProjectMemory,
52989
53188
  setCreateFnAgent: () => setCreateFnAgent,
53189
+ shouldHydrateStoredCredential: () => shouldHydrateStoredCredential,
52990
53190
  shouldSkipBackgroundQmdRefresh: () => shouldSkipBackgroundQmdRefresh,
52991
53191
  shouldTriggerExtraction: () => shouldTriggerExtraction,
52992
53192
  slugify: () => slugify,
@@ -53091,13 +53291,14 @@ var init_src = __esm({
53091
53291
  init_agent_companies_parser();
53092
53292
  init_agent_companies_exporter();
53093
53293
  init_chat_store();
53294
+ init_oauth_credential_interop();
53094
53295
  init_error_message();
53095
53296
  }
53096
53297
  });
53097
53298
 
53098
53299
  // ../dashboard/src/github-webhooks.ts
53099
53300
  import { createHmac, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
53100
- import { readFileSync as readFileSync6 } from "node:fs";
53301
+ import { readFileSync as readFileSync7 } from "node:fs";
53101
53302
  function getGitHubAppConfig() {
53102
53303
  const appId = process.env.FUSION_GITHUB_APP_ID;
53103
53304
  const webhookSecret = process.env.FUSION_GITHUB_WEBHOOK_SECRET;
@@ -53107,7 +53308,7 @@ function getGitHubAppConfig() {
53107
53308
  } else if (process.env.FUSION_GITHUB_APP_PRIVATE_KEY_PATH) {
53108
53309
  if (cachedPrivateKey === void 0) {
53109
53310
  try {
53110
- cachedPrivateKey = readFileSync6(process.env.FUSION_GITHUB_APP_PRIVATE_KEY_PATH, "utf-8");
53311
+ cachedPrivateKey = readFileSync7(process.env.FUSION_GITHUB_APP_PRIVATE_KEY_PATH, "utf-8");
53111
53312
  } catch {
53112
53313
  cachedPrivateKey = null;
53113
53314
  }
@@ -55186,12 +55387,12 @@ var init_github_provider = __esm({
55186
55387
  });
55187
55388
 
55188
55389
  // ../engine/src/skill-resolver.ts
55189
- import { existsSync as existsSync19, readFileSync as readFileSync7 } from "node:fs";
55190
- import { dirname as dirname8, join as join22, resolve as resolve11 } from "node:path";
55390
+ import { existsSync as existsSync20, readFileSync as readFileSync8 } from "node:fs";
55391
+ import { dirname as dirname8, join as join23, resolve as resolve11 } from "node:path";
55191
55392
  function resolveProjectRoot(cwd) {
55192
55393
  let current = resolve11(cwd);
55193
55394
  while (true) {
55194
- if (existsSync19(join22(current, ".fusion"))) {
55395
+ if (existsSync20(join23(current, ".fusion"))) {
55195
55396
  return current;
55196
55397
  }
55197
55398
  const parent2 = dirname8(current);
@@ -55202,19 +55403,19 @@ function resolveProjectRoot(cwd) {
55202
55403
  }
55203
55404
  }
55204
55405
  function readJsonObject(path5) {
55205
- if (!existsSync19(path5)) {
55406
+ if (!existsSync20(path5)) {
55206
55407
  return {};
55207
55408
  }
55208
55409
  try {
55209
- const parsed = JSON.parse(readFileSync7(path5, "utf-8"));
55410
+ const parsed = JSON.parse(readFileSync8(path5, "utf-8"));
55210
55411
  return parsed && typeof parsed === "object" ? parsed : {};
55211
55412
  } catch {
55212
55413
  return {};
55213
55414
  }
55214
55415
  }
55215
55416
  function readProjectSettings(projectRootDir) {
55216
- const fusionSettings = join22(projectRootDir, ".fusion", "settings.json");
55217
- if (existsSync19(fusionSettings)) {
55417
+ const fusionSettings = join23(projectRootDir, ".fusion", "settings.json");
55418
+ if (existsSync20(fusionSettings)) {
55218
55419
  const parsed = readJsonObject(fusionSettings);
55219
55420
  return {
55220
55421
  skills: Array.isArray(parsed.skills) ? parsed.skills : void 0,
@@ -55439,51 +55640,51 @@ var init_context_limit_detector = __esm({
55439
55640
  });
55440
55641
 
55441
55642
  // ../engine/src/auth-storage.ts
55442
- import { existsSync as existsSync20, readFileSync as readFileSync8 } from "node:fs";
55443
- import { homedir as homedir4 } from "node:os";
55444
- import { join as join23 } from "node:path";
55643
+ import { existsSync as existsSync21, readFileSync as readFileSync9 } from "node:fs";
55644
+ import { homedir as homedir5 } from "node:os";
55645
+ import { join as join24 } from "node:path";
55445
55646
  import { AuthStorage } from "@mariozechner/pi-coding-agent";
55446
55647
  import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
55447
- function getHomeDir4() {
55448
- return process.env.HOME || process.env.USERPROFILE || homedir4();
55648
+ function getHomeDir5() {
55649
+ return process.env.HOME || process.env.USERPROFILE || homedir5();
55650
+ }
55651
+ function getFusionAuthPath(home = getHomeDir5()) {
55652
+ return join24(home, ".fusion", "agent", "auth.json");
55449
55653
  }
55450
- function getFusionAuthPath(home = getHomeDir4()) {
55451
- return join23(home, ".fusion", "agent", "auth.json");
55654
+ function getFusionModelsPath(home = getHomeDir5()) {
55655
+ return join24(home, ".fusion", "agent", "models.json");
55452
55656
  }
55453
- function getFusionModelsPath(home = getHomeDir4()) {
55454
- return join23(home, ".fusion", "agent", "models.json");
55657
+ function getLegacyAuthPaths(home = getHomeDir5()) {
55658
+ return [
55659
+ join24(home, ".pi", "agent", "auth.json"),
55660
+ join24(home, ".pi", "auth.json")
55661
+ ];
55455
55662
  }
55456
- function getLegacyAuthPaths(home = getHomeDir4()) {
55663
+ function getSupplementalAuthPaths(home = getHomeDir5()) {
55457
55664
  return [
55458
- join23(home, ".pi", "agent", "auth.json"),
55459
- join23(home, ".pi", "auth.json")
55665
+ ...getLegacyAuthPaths(home),
55666
+ getCodexCliAuthPath(home)
55460
55667
  ];
55461
55668
  }
55462
- function getLegacyModelsPaths(home = getHomeDir4()) {
55669
+ function getLegacyModelsPaths(home = getHomeDir5()) {
55463
55670
  return [
55464
- join23(home, ".pi", "agent", "models.json"),
55465
- join23(home, ".pi", "models.json")
55671
+ join24(home, ".pi", "agent", "models.json"),
55672
+ join24(home, ".pi", "models.json")
55466
55673
  ];
55467
55674
  }
55468
- function getModelRegistryModelsPath(home = getHomeDir4()) {
55675
+ function getModelRegistryModelsPath(home = getHomeDir5()) {
55469
55676
  const fusionModelsPath = getFusionModelsPath(home);
55470
- if (existsSync20(fusionModelsPath)) {
55677
+ if (existsSync21(fusionModelsPath)) {
55471
55678
  return fusionModelsPath;
55472
55679
  }
55473
- return getLegacyModelsPaths(home).find((modelsPath) => existsSync20(modelsPath)) ?? fusionModelsPath;
55680
+ return getLegacyModelsPaths(home).find((modelsPath) => existsSync21(modelsPath)) ?? fusionModelsPath;
55474
55681
  }
55475
- function readLegacyCredentials(authPaths = getLegacyAuthPaths()) {
55682
+ function readSupplementalCredentials(authPaths = getSupplementalAuthPaths()) {
55476
55683
  const credentials = {};
55477
55684
  for (const authPath of authPaths) {
55478
- if (!existsSync20(authPath)) {
55479
- continue;
55480
- }
55481
- try {
55482
- const parsed = JSON.parse(readFileSync8(authPath, "utf-8"));
55483
- for (const [provider, credential] of Object.entries(parsed)) {
55484
- credentials[provider] ??= credential;
55485
- }
55486
- } catch {
55685
+ const parsed = readStoredCredentialsFromAuthFile(authPath);
55686
+ for (const [provider, credential] of Object.entries(parsed)) {
55687
+ credentials[provider] = choosePreferredStoredCredential(credentials[provider], credential) ?? credential;
55487
55688
  }
55488
55689
  }
55489
55690
  return credentials;
@@ -55507,14 +55708,14 @@ function resolveStoredCredentialApiKey(providerId, credential) {
55507
55708
  }
55508
55709
  return void 0;
55509
55710
  }
55510
- function readModelsJsonApiKeys(home = getHomeDir4()) {
55711
+ function readModelsJsonApiKeys(home = getHomeDir5()) {
55511
55712
  const apiKeys = /* @__PURE__ */ new Map();
55512
55713
  const modelsPath = getModelRegistryModelsPath(home);
55513
- if (!existsSync20(modelsPath)) {
55714
+ if (!existsSync21(modelsPath)) {
55514
55715
  return apiKeys;
55515
55716
  }
55516
55717
  try {
55517
- const parsed = JSON.parse(readFileSync8(modelsPath, "utf-8"));
55718
+ const parsed = JSON.parse(readFileSync9(modelsPath, "utf-8"));
55518
55719
  const providers = parsed?.providers;
55519
55720
  if (providers) {
55520
55721
  for (const [providerId, config] of Object.entries(providers)) {
@@ -55529,8 +55730,20 @@ function readModelsJsonApiKeys(home = getHomeDir4()) {
55529
55730
  }
55530
55731
  function createFusionAuthStorage() {
55531
55732
  const primary = AuthStorage.create(getFusionAuthPath());
55532
- let legacyCredentials = readLegacyCredentials();
55733
+ let supplementalCredentials = readSupplementalCredentials();
55533
55734
  let modelsJsonApiKeys = readModelsJsonApiKeys();
55735
+ const syncSupplementalOauthCredentials = () => {
55736
+ for (const [provider, credential] of Object.entries(supplementalCredentials)) {
55737
+ const current = primary.get(provider);
55738
+ if (!shouldHydrateStoredCredential(current, credential)) {
55739
+ continue;
55740
+ }
55741
+ if (credential.type === "oauth" || credential.type === "api_key") {
55742
+ primary.set(provider, credential);
55743
+ }
55744
+ }
55745
+ };
55746
+ syncSupplementalOauthCredentials();
55534
55747
  return new Proxy(primary, {
55535
55748
  // Forward property writes to the target so that methods like
55536
55749
  // `setFallbackResolver` (called by ModelRegistry) correctly update the
@@ -55544,31 +55757,51 @@ function createFusionAuthStorage() {
55544
55757
  if (prop === "reload") {
55545
55758
  return () => {
55546
55759
  target.reload();
55547
- legacyCredentials = readLegacyCredentials();
55760
+ supplementalCredentials = readSupplementalCredentials();
55761
+ syncSupplementalOauthCredentials();
55548
55762
  modelsJsonApiKeys = readModelsJsonApiKeys();
55549
55763
  };
55550
55764
  }
55551
55765
  if (prop === "get") {
55552
- return (provider) => target.get(provider) ?? legacyCredentials[provider];
55766
+ return (provider) => choosePreferredStoredCredential(
55767
+ target.get(provider),
55768
+ supplementalCredentials[provider]
55769
+ );
55553
55770
  }
55554
55771
  if (prop === "has") {
55555
- return (provider) => target.has(provider) || provider in legacyCredentials || modelsJsonApiKeys.has(provider);
55772
+ return (provider) => target.has(provider) || provider in supplementalCredentials || modelsJsonApiKeys.has(provider);
55556
55773
  }
55557
55774
  if (prop === "hasAuth") {
55558
- return (provider) => target.hasAuth(provider) || Boolean(legacyCredentials[provider]) || modelsJsonApiKeys.has(provider);
55775
+ return (provider) => target.hasAuth(provider) || Boolean(supplementalCredentials[provider]) || modelsJsonApiKeys.has(provider);
55559
55776
  }
55560
55777
  if (prop === "getAll") {
55561
- return () => ({ ...legacyCredentials, ...target.getAll() });
55778
+ return () => {
55779
+ const providerIds = /* @__PURE__ */ new Set([
55780
+ ...Object.keys(supplementalCredentials),
55781
+ ...Object.keys(target.getAll())
55782
+ ]);
55783
+ const merged = {};
55784
+ for (const providerId of providerIds) {
55785
+ const credential = choosePreferredStoredCredential(
55786
+ target.get(providerId),
55787
+ supplementalCredentials[providerId]
55788
+ );
55789
+ if (credential) {
55790
+ merged[providerId] = credential;
55791
+ }
55792
+ }
55793
+ return merged;
55794
+ };
55562
55795
  }
55563
55796
  if (prop === "list") {
55564
- return () => Array.from(/* @__PURE__ */ new Set([...Object.keys(legacyCredentials), ...target.list(), ...modelsJsonApiKeys.keys()]));
55797
+ return () => Array.from(/* @__PURE__ */ new Set([...Object.keys(supplementalCredentials), ...target.list(), ...modelsJsonApiKeys.keys()]));
55565
55798
  }
55566
55799
  if (prop === "getApiKey") {
55567
55800
  return async (provider) => {
55568
55801
  const primaryKey = await target.getApiKey(provider);
55569
55802
  if (primaryKey) return primaryKey;
55570
- const legacyKey = resolveStoredCredentialApiKey(provider, legacyCredentials[provider]);
55571
- if (legacyKey) return legacyKey;
55803
+ const supplementalKey = resolveStoredCredentialApiKey(provider, supplementalCredentials[provider]);
55804
+ if (supplementalKey) return supplementalKey;
55572
55805
  return modelsJsonApiKeys.get(provider);
55573
55806
  };
55574
55807
  }
@@ -55579,17 +55812,18 @@ function createFusionAuthStorage() {
55579
55812
  var init_auth_storage = __esm({
55580
55813
  "../engine/src/auth-storage.ts"() {
55581
55814
  "use strict";
55815
+ init_src();
55582
55816
  }
55583
55817
  });
55584
55818
 
55585
55819
  // ../engine/src/custom-providers.ts
55586
- import { readFileSync as readFileSync9 } from "node:fs";
55587
- import { homedir as homedir5 } from "node:os";
55588
- import { join as join24 } from "node:path";
55820
+ import { readFileSync as readFileSync10 } from "node:fs";
55821
+ import { homedir as homedir6 } from "node:os";
55822
+ import { join as join25 } from "node:path";
55589
55823
  function readCustomProviders() {
55590
55824
  try {
55591
- const settingsPath = join24(homedir5(), ".fusion", "settings.json");
55592
- const raw = readFileSync9(settingsPath, "utf-8");
55825
+ const settingsPath = join25(homedir6(), ".fusion", "settings.json");
55826
+ const raw = readFileSync10(settingsPath, "utf-8");
55593
55827
  const parsed = JSON.parse(raw);
55594
55828
  return Array.isArray(parsed.customProviders) ? parsed.customProviders : [];
55595
55829
  } catch {
@@ -55603,11 +55837,11 @@ var init_custom_providers = __esm({
55603
55837
  });
55604
55838
 
55605
55839
  // ../engine/src/pi.ts
55606
- import { existsSync as existsSync21, readFileSync as readFileSync10 } from "node:fs";
55840
+ import { existsSync as existsSync22, readFileSync as readFileSync11 } from "node:fs";
55607
55841
  import { exec as exec2 } from "node:child_process";
55608
55842
  import { promisify as promisify3 } from "node:util";
55609
55843
  import { createRequire as createRequire2 } from "node:module";
55610
- import { basename as basename7, dirname as dirname9, join as join25, relative as relative3, isAbsolute as isAbsolute7, resolve as resolve12 } from "node:path";
55844
+ import { basename as basename7, dirname as dirname9, join as join26, relative as relative3, isAbsolute as isAbsolute7, resolve as resolve12 } from "node:path";
55611
55845
  import {
55612
55846
  createAgentSession,
55613
55847
  createBashTool,
@@ -55861,11 +56095,11 @@ function isRetryableModelSelectionError(message) {
55861
56095
  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");
55862
56096
  }
55863
56097
  function readJsonObject2(path5) {
55864
- if (!existsSync21(path5)) {
56098
+ if (!existsSync22(path5)) {
55865
56099
  return {};
55866
56100
  }
55867
56101
  try {
55868
- const parsed = JSON.parse(readFileSync10(path5, "utf-8"));
56102
+ const parsed = JSON.parse(readFileSync11(path5, "utf-8"));
55869
56103
  return parsed && typeof parsed === "object" ? parsed : {};
55870
56104
  } catch {
55871
56105
  return {};
@@ -56002,17 +56236,17 @@ function siblingAgentDir(agentDir, siblingRoot) {
56002
56236
  if (basename7(agentDir) !== "agent") {
56003
56237
  return void 0;
56004
56238
  }
56005
- return join25(dirname9(dirname9(agentDir)), siblingRoot, "agent");
56239
+ return join26(dirname9(dirname9(agentDir)), siblingRoot, "agent");
56006
56240
  }
56007
56241
  function createReadOnlyPiSettingsView(cwd, agentDir) {
56008
56242
  const projectRoot = resolvePiExtensionProjectRoot(cwd);
56009
- const fusionAgentDir = agentDir.includes(`${join25(".fusion", "agent")}`) ? agentDir : siblingAgentDir(agentDir, ".fusion");
56010
- const legacyAgentDir = agentDir.includes(`${join25(".pi", "agent")}`) ? agentDir : siblingAgentDir(agentDir, ".pi");
56011
- const legacyGlobalSettings = legacyAgentDir ? readJsonObject2(join25(legacyAgentDir, "settings.json")) : {};
56012
- const fusionGlobalSettings = fusionAgentDir ? readJsonObject2(join25(fusionAgentDir, "settings.json")) : {};
56013
- const directGlobalSettings = readJsonObject2(join25(agentDir, "settings.json"));
56243
+ const fusionAgentDir = agentDir.includes(`${join26(".fusion", "agent")}`) ? agentDir : siblingAgentDir(agentDir, ".fusion");
56244
+ const legacyAgentDir = agentDir.includes(`${join26(".pi", "agent")}`) ? agentDir : siblingAgentDir(agentDir, ".pi");
56245
+ const legacyGlobalSettings = legacyAgentDir ? readJsonObject2(join26(legacyAgentDir, "settings.json")) : {};
56246
+ const fusionGlobalSettings = fusionAgentDir ? readJsonObject2(join26(fusionAgentDir, "settings.json")) : {};
56247
+ const directGlobalSettings = readJsonObject2(join26(agentDir, "settings.json"));
56014
56248
  const globalSettings = { ...legacyGlobalSettings, ...directGlobalSettings, ...fusionGlobalSettings };
56015
- const fusionProjectSettings = readJsonObject2(join25(projectRoot, ".fusion", "settings.json"));
56249
+ const fusionProjectSettings = readJsonObject2(join26(projectRoot, ".fusion", "settings.json"));
56016
56250
  const mergedSettings = { ...globalSettings, ...fusionProjectSettings };
56017
56251
  return {
56018
56252
  getGlobalSettings: () => structuredClone(globalSettings),
@@ -56023,27 +56257,27 @@ function createReadOnlyPiSettingsView(cwd, agentDir) {
56023
56257
  function getPackageManagerAgentDir() {
56024
56258
  const fusionAgentDir = getFusionAgentDir();
56025
56259
  const legacyAgentDir = getLegacyPiAgentDir();
56026
- const fusionSettings = readJsonObject2(join25(fusionAgentDir, "settings.json"));
56027
- const legacySettings = readJsonObject2(join25(legacyAgentDir, "settings.json"));
56028
- if (hasPackageManagerSettings(fusionSettings) || !existsSync21(legacyAgentDir)) {
56260
+ const fusionSettings = readJsonObject2(join26(fusionAgentDir, "settings.json"));
56261
+ const legacySettings = readJsonObject2(join26(legacyAgentDir, "settings.json"));
56262
+ if (hasPackageManagerSettings(fusionSettings) || !existsSync22(legacyAgentDir)) {
56029
56263
  return fusionAgentDir;
56030
56264
  }
56031
56265
  if (hasPackageManagerSettings(legacySettings)) {
56032
56266
  return legacyAgentDir;
56033
56267
  }
56034
- return existsSync21(fusionAgentDir) ? fusionAgentDir : legacyAgentDir;
56268
+ return existsSync22(fusionAgentDir) ? fusionAgentDir : legacyAgentDir;
56035
56269
  }
56036
56270
  function resolveVendoredClaudeCliEntry() {
56037
56271
  try {
56038
56272
  const require_3 = createRequire2(import.meta.url);
56039
56273
  const pkgJsonPath = require_3.resolve("@fusion/pi-claude-cli/package.json");
56040
- const pkgJson = JSON.parse(readFileSync10(pkgJsonPath, "utf-8"));
56274
+ const pkgJson = JSON.parse(readFileSync11(pkgJsonPath, "utf-8"));
56041
56275
  const extensions = pkgJson.pi?.extensions;
56042
56276
  if (!Array.isArray(extensions) || extensions.length === 0) return null;
56043
56277
  const entry = extensions[0];
56044
56278
  if (typeof entry !== "string" || entry.length === 0) return null;
56045
56279
  const path5 = resolve12(dirname9(pkgJsonPath), entry);
56046
- return existsSync21(path5) ? path5 : null;
56280
+ return existsSync22(path5) ? path5 : null;
56047
56281
  } catch {
56048
56282
  return null;
56049
56283
  }
@@ -56052,13 +56286,13 @@ function resolveVendoredDroidCliEntry() {
56052
56286
  try {
56053
56287
  const require_3 = createRequire2(import.meta.url);
56054
56288
  const pkgJsonPath = require_3.resolve("@fusion/droid-cli/package.json");
56055
- const pkgJson = JSON.parse(readFileSync10(pkgJsonPath, "utf-8"));
56289
+ const pkgJson = JSON.parse(readFileSync11(pkgJsonPath, "utf-8"));
56056
56290
  const extensions = pkgJson.pi?.extensions;
56057
56291
  if (!Array.isArray(extensions) || extensions.length === 0) return null;
56058
56292
  const entry = extensions[0];
56059
56293
  if (typeof entry !== "string" || entry.length === 0) return null;
56060
56294
  const path5 = resolve12(dirname9(pkgJsonPath), entry);
56061
- return existsSync21(path5) ? path5 : null;
56295
+ return existsSync22(path5) ? path5 : null;
56062
56296
  } catch {
56063
56297
  return null;
56064
56298
  }
@@ -56086,7 +56320,7 @@ async function registerExtensionProviders(cwd, modelRegistry) {
56086
56320
  const extensionsResult = await discoverAndLoadExtensions(
56087
56321
  doubleReconciledPaths,
56088
56322
  cwd,
56089
- join25(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery")
56323
+ join26(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery")
56090
56324
  );
56091
56325
  for (const { path: path5, error } of extensionsResult.errors) {
56092
56326
  extensionsLog.warn(`Failed to load ${path5}: ${error}`);
@@ -56141,10 +56375,10 @@ async function isCompleteGitWorktree(worktreePath) {
56141
56375
  }
56142
56376
  }
56143
56377
  async function assertValidWorktreeSession(cwd, projectRoot) {
56144
- if (!existsSync21(cwd)) {
56378
+ if (!existsSync22(cwd)) {
56145
56379
  throw new Error(`Refusing to start coding agent in missing worktree: ${cwd}`);
56146
56380
  }
56147
- if (!existsSync21(join25(cwd, ".git")) || !await isCompleteGitWorktree(cwd)) {
56381
+ if (!existsSync22(join26(cwd, ".git")) || !await isCompleteGitWorktree(cwd)) {
56148
56382
  throw new Error(`Refusing to start coding agent in incomplete worktree: ${cwd}`);
56149
56383
  }
56150
56384
  if (!await isRegisteredGitWorktree(projectRoot, cwd)) {
@@ -56725,7 +56959,7 @@ ${source.content ?? ""}`;
56725
56959
 
56726
56960
  // ../engine/src/research/providers/local-docs-provider.ts
56727
56961
  import { promises as fs } from "node:fs";
56728
- import { extname as extname2, join as join26, relative as relative4, resolve as resolve13 } from "node:path";
56962
+ import { extname as extname2, join as join27, relative as relative4, resolve as resolve13 } from "node:path";
56729
56963
  function buildExcerpt(content, terms) {
56730
56964
  const lower = content.toLowerCase();
56731
56965
  const first = terms.find((term) => lower.includes(term));
@@ -56854,7 +57088,7 @@ var init_local_docs_provider = __esm({
56854
57088
  const rootEntries = await fs.readdir(this.projectRoot, { withFileTypes: true });
56855
57089
  for (const entry of rootEntries) {
56856
57090
  if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
56857
- files.push(join26(this.projectRoot, entry.name));
57091
+ files.push(join27(this.projectRoot, entry.name));
56858
57092
  }
56859
57093
  }
56860
57094
  return [...new Set(files)];
@@ -56864,7 +57098,7 @@ var init_local_docs_provider = __esm({
56864
57098
  const entries = await fs.readdir(dir2, { withFileTypes: true });
56865
57099
  for (const entry of entries) {
56866
57100
  this.throwIfAborted(signal);
56867
- const fullPath = join26(dir2, entry.name);
57101
+ const fullPath = join27(dir2, entry.name);
56868
57102
  const relPath = relative4(this.projectRoot, fullPath).replace(/\\/g, "/");
56869
57103
  if (matchesGitignore(relPath, ignorePatterns)) continue;
56870
57104
  if (entry.isDirectory()) {
@@ -56889,7 +57123,7 @@ var init_local_docs_provider = __esm({
56889
57123
  }
56890
57124
  async readGitignore() {
56891
57125
  try {
56892
- const content = await fs.readFile(join26(this.projectRoot, ".gitignore"), "utf-8");
57126
+ const content = await fs.readFile(join27(this.projectRoot, ".gitignore"), "utf-8");
56893
57127
  return content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
56894
57128
  } catch {
56895
57129
  return [];
@@ -57499,9 +57733,9 @@ var init_research_step_runner = __esm({
57499
57733
 
57500
57734
  // ../engine/src/agent-tools.ts
57501
57735
  import { appendFile as appendFile3, mkdir as mkdir11, readFile as readFile12, readdir as readdir7, stat as stat4, writeFile as writeFile10 } from "node:fs/promises";
57502
- import { existsSync as existsSync22 } from "node:fs";
57736
+ import { existsSync as existsSync23 } from "node:fs";
57503
57737
  import { createHash as createHash4 } from "node:crypto";
57504
- import { join as join27 } from "node:path";
57738
+ import { join as join28 } from "node:path";
57505
57739
  import { Type } from "@mariozechner/pi-ai";
57506
57740
  function sanitizeAgentMemoryId(agentId) {
57507
57741
  return agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
@@ -57513,16 +57747,16 @@ function agentDreamsDisplayPath(agentId) {
57513
57747
  return `${AGENT_MEMORY_ROOT2}/${sanitizeAgentMemoryId(agentId)}/${AGENT_DREAMS_FILENAME2}`;
57514
57748
  }
57515
57749
  function agentMemoryDirectory(rootDir, agentId) {
57516
- return join27(rootDir, AGENT_MEMORY_ROOT2, sanitizeAgentMemoryId(agentId));
57750
+ return join28(rootDir, AGENT_MEMORY_ROOT2, sanitizeAgentMemoryId(agentId));
57517
57751
  }
57518
57752
  function agentMemoryFilePath(rootDir, agentId) {
57519
- return join27(agentMemoryDirectory(rootDir, agentId), AGENT_MEMORY_FILENAME2);
57753
+ return join28(agentMemoryDirectory(rootDir, agentId), AGENT_MEMORY_FILENAME2);
57520
57754
  }
57521
57755
  function agentDreamsFilePath(rootDir, agentId) {
57522
- return join27(agentMemoryDirectory(rootDir, agentId), AGENT_DREAMS_FILENAME2);
57756
+ return join28(agentMemoryDirectory(rootDir, agentId), AGENT_DREAMS_FILENAME2);
57523
57757
  }
57524
57758
  function agentDailyFilePath(rootDir, agentId, date = /* @__PURE__ */ new Date()) {
57525
- return join27(agentMemoryDirectory(rootDir, agentId), `${date.toISOString().slice(0, 10)}.md`);
57759
+ return join28(agentMemoryDirectory(rootDir, agentId), `${date.toISOString().slice(0, 10)}.md`);
57526
57760
  }
57527
57761
  function qmdAgentMemoryCollectionName(rootDir, agentId) {
57528
57762
  const hash = createHash4("sha1").update(`${rootDir}:${agentId}`).digest("hex").slice(0, 12);
@@ -57558,7 +57792,7 @@ async function syncAgentMemoryFile(rootDir, agentMemory) {
57558
57792
  const dir2 = agentMemoryDirectory(rootDir, agentMemory.agentId);
57559
57793
  await mkdir11(dir2, { recursive: true });
57560
57794
  const longTermPath = agentMemoryFilePath(rootDir, agentMemory.agentId);
57561
- if (!existsSync22(longTermPath)) {
57795
+ if (!existsSync23(longTermPath)) {
57562
57796
  const title = agentMemory.agentName?.trim() ? `# Agent Memory: ${agentMemory.agentName.trim()}` : "# Agent Memory";
57563
57797
  const fileContent = `${title}
57564
57798
 
@@ -57569,11 +57803,11 @@ ${content || ""}
57569
57803
  await writeFile10(longTermPath, fileContent, "utf-8");
57570
57804
  }
57571
57805
  const dreamsPath = agentDreamsFilePath(rootDir, agentMemory.agentId);
57572
- if (!existsSync22(dreamsPath)) {
57806
+ if (!existsSync23(dreamsPath)) {
57573
57807
  await writeFile10(dreamsPath, "# Agent Memory Dreams\n\n<!-- Synthesized patterns from this agent's daily notes. -->\n", "utf-8");
57574
57808
  }
57575
57809
  const dailyPath = agentDailyFilePath(rootDir, agentMemory.agentId);
57576
- if (!existsSync22(dailyPath)) {
57810
+ if (!existsSync23(dailyPath)) {
57577
57811
  await writeFile10(dailyPath, `# Agent Daily Memory ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
57578
57812
 
57579
57813
  <!-- Running observations for this agent. -->
@@ -57597,7 +57831,7 @@ async function listAgentMemoryFiles2(rootDir, agentMemory) {
57597
57831
  }
57598
57832
  for (const entry of entries) {
57599
57833
  if (!DAILY_AGENT_MEMORY_RE2.test(entry)) continue;
57600
- const absPath = join27(dir2, entry);
57834
+ const absPath = join28(dir2, entry);
57601
57835
  const fileStat = await stat4(absPath);
57602
57836
  if (fileStat.isFile()) {
57603
57837
  files.push({
@@ -57759,7 +57993,7 @@ function resolveAgentMemoryPath(rootDir, agentId, path5) {
57759
57993
  return null;
57760
57994
  }
57761
57995
  return {
57762
- absPath: join27(agentMemoryDirectory(rootDir, agentId), filename),
57996
+ absPath: join28(agentMemoryDirectory(rootDir, agentId), filename),
57763
57997
  displayPath: `${prefix}${filename}`
57764
57998
  };
57765
57999
  }
@@ -58774,6 +59008,7 @@ __export(agent_session_helpers_exports, {
58774
59008
  createResolvedAgentSession: () => createResolvedAgentSession,
58775
59009
  describeAgentModel: () => describeAgentModel,
58776
59010
  extractRuntimeHint: () => extractRuntimeHint,
59011
+ extractRuntimeModel: () => extractRuntimeModel,
58777
59012
  promptWithAutoRetry: () => promptWithAutoRetry
58778
59013
  });
58779
59014
  function extractRuntimeHint(runtimeConfig) {
@@ -58784,6 +59019,24 @@ function extractRuntimeHint(runtimeConfig) {
58784
59019
  const normalizedHint = hint.trim();
58785
59020
  return normalizedHint.length > 0 ? normalizedHint : void 0;
58786
59021
  }
59022
+ function extractRuntimeModel(runtimeConfig) {
59023
+ const combined = typeof runtimeConfig?.model === "string" ? runtimeConfig.model.trim() : "";
59024
+ if (combined) {
59025
+ const slashIdx = combined.indexOf("/");
59026
+ if (slashIdx > 0 && slashIdx < combined.length - 1) {
59027
+ return {
59028
+ provider: combined.slice(0, slashIdx).trim() || void 0,
59029
+ modelId: combined.slice(slashIdx + 1).trim() || void 0
59030
+ };
59031
+ }
59032
+ }
59033
+ const provider = typeof runtimeConfig?.modelProvider === "string" ? runtimeConfig.modelProvider.trim() : "";
59034
+ const modelId = typeof runtimeConfig?.modelId === "string" ? runtimeConfig.modelId.trim() : "";
59035
+ return {
59036
+ provider: provider || void 0,
59037
+ modelId: modelId || void 0
59038
+ };
59039
+ }
58787
59040
  async function createResolvedAgentSession(options) {
58788
59041
  const { sessionPurpose, pluginRunner, runtimeHint, ...runtimeOptions } = options;
58789
59042
  const context = buildRuntimeResolutionContext(sessionPurpose, pluginRunner, runtimeHint);
@@ -60037,6 +60290,38 @@ var init_notifier = __esm({
60037
60290
  }
60038
60291
  });
60039
60292
 
60293
+ // ../engine/src/fallback-model-observer.ts
60294
+ function buildFallbackLogMessage(label, payload) {
60295
+ return `[fallback] ${label} switched from ${payload.primaryModel} to ${payload.fallbackModel} (${payload.triggerPoint})`;
60296
+ }
60297
+ function createFallbackModelObserver(options) {
60298
+ return async (payload) => {
60299
+ const taskId = options.taskId ?? payload.taskId;
60300
+ const taskTitle = options.taskTitle ?? payload.taskTitle;
60301
+ const message = buildFallbackLogMessage(options.label, payload);
60302
+ if (taskId && options.store?.logEntry) {
60303
+ await options.store.logEntry(taskId, message).catch(() => void 0);
60304
+ }
60305
+ if (taskId && options.store?.appendAgentLog) {
60306
+ await options.store.appendAgentLog(taskId, message, "text", void 0, options.agent).catch(() => void 0);
60307
+ }
60308
+ await notifyFallbackUsed({
60309
+ primaryModel: payload.primaryModel,
60310
+ fallbackModel: payload.fallbackModel,
60311
+ triggerPoint: payload.triggerPoint,
60312
+ taskId,
60313
+ taskTitle,
60314
+ timestamp: payload.timestamp
60315
+ });
60316
+ };
60317
+ }
60318
+ var init_fallback_model_observer = __esm({
60319
+ "../engine/src/fallback-model-observer.ts"() {
60320
+ "use strict";
60321
+ init_notifier();
60322
+ }
60323
+ });
60324
+
60040
60325
  // ../engine/src/reviewer.ts
60041
60326
  async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptContent, baseline, options = {}) {
60042
60327
  let liveSettings = options.settings;
@@ -60167,7 +60452,13 @@ async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptC
60167
60452
  ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
60168
60453
  taskId: options.taskId,
60169
60454
  taskTitle: options.taskTitle,
60170
- onFallbackModelUsed: notifyFallbackUsed,
60455
+ onFallbackModelUsed: createFallbackModelObserver({
60456
+ agent: "reviewer",
60457
+ label: "reviewer",
60458
+ store: options.store,
60459
+ taskId: options.taskId,
60460
+ taskTitle: options.taskTitle
60461
+ }),
60171
60462
  beforeSpawnSession: async () => {
60172
60463
  if (!options.store) return;
60173
60464
  let finalSettings;
@@ -60354,7 +60645,7 @@ var init_reviewer = __esm({
60354
60645
  init_logger2();
60355
60646
  init_usage_limit_detector();
60356
60647
  init_agent_instructions();
60357
- init_notifier();
60648
+ init_fallback_model_observer();
60358
60649
  init_agent_tools();
60359
60650
  REVIEWER_SYSTEM_PROMPT = `You are an independent code and plan reviewer.
60360
60651
 
@@ -60715,7 +61006,7 @@ var init_recovery_policy = __esm({
60715
61006
  // ../engine/src/triage.ts
60716
61007
  import { Type as Type2 } from "@mariozechner/pi-ai";
60717
61008
  import { readFile as readFile14 } from "node:fs/promises";
60718
- import { join as join28 } from "node:path";
61009
+ import { join as join29 } from "node:path";
60719
61010
  function extractPromptDeclaredTitle(prompt, taskId) {
60720
61011
  const headingMatch = prompt.match(/^#\s+Task:\s+([A-Z]+-\d+)\s+-\s+(.+)$/m);
60721
61012
  if (!headingMatch) return null;
@@ -60744,9 +61035,9 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
60744
61035
  return { attachmentContents, imageContents };
60745
61036
  }
60746
61037
  const { readFile: readFile26 } = await import("node:fs/promises");
60747
- const { join: join71 } = await import("node:path");
61038
+ const { join: join72 } = await import("node:path");
60748
61039
  for (const att of attachments) {
60749
- const filePath = join71(
61040
+ const filePath = join72(
60750
61041
  rootDir,
60751
61042
  ".fusion",
60752
61043
  "tasks",
@@ -60971,7 +61262,7 @@ var init_triage = __esm({
60971
61262
  init_concurrency();
60972
61263
  init_agent_logger();
60973
61264
  init_agent_instructions();
60974
- init_notifier();
61265
+ init_fallback_model_observer();
60975
61266
  init_logger2();
60976
61267
  init_usage_limit_detector();
60977
61268
  init_transient_error_detector();
@@ -61581,7 +61872,7 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
61581
61872
  return false;
61582
61873
  }
61583
61874
  const settings = await this.store.getSettings();
61584
- const promptPath = join28(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
61875
+ const promptPath = join29(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
61585
61876
  const written = await readFile14(promptPath, "utf-8").catch((err) => {
61586
61877
  const msg = err instanceof Error ? err.message : String(err);
61587
61878
  planLog.warn(`${task.id}: failed to read PROMPT.md during approved-spec recovery (${promptPath}): ${msg}`);
@@ -61811,7 +62102,13 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
61811
62102
  ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
61812
62103
  taskId: task.id,
61813
62104
  taskTitle: task.title,
61814
- onFallbackModelUsed: notifyFallbackUsed
62105
+ onFallbackModelUsed: createFallbackModelObserver({
62106
+ agent: "triage",
62107
+ label: "triage",
62108
+ store: this.store,
62109
+ taskId: task.id,
62110
+ taskTitle: task.title
62111
+ })
61815
62112
  });
61816
62113
  const modelDesc = describeModel(session);
61817
62114
  planLog.log(`${task.id}: using model ${modelDesc}`);
@@ -61954,7 +62251,13 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
61954
62251
  ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
61955
62252
  taskId: task.id,
61956
62253
  taskTitle: task.title,
61957
- onFallbackModelUsed: notifyFallbackUsed
62254
+ onFallbackModelUsed: createFallbackModelObserver({
62255
+ agent: "triage",
62256
+ label: "triage",
62257
+ store: this.store,
62258
+ taskId: task.id,
62259
+ taskTitle: task.title
62260
+ })
61958
62261
  });
61959
62262
  session = fallbackResult.session;
61960
62263
  const fallbackModelDesc = describeModel(session);
@@ -62039,7 +62342,7 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
62039
62342
  return;
62040
62343
  }
62041
62344
  const written = await readFile14(
62042
- join28(this.rootDir, promptPath),
62345
+ join29(this.rootDir, promptPath),
62043
62346
  "utf-8"
62044
62347
  ).catch((err) => {
62045
62348
  const msg = err instanceof Error ? err.message : String(err);
@@ -62358,9 +62661,9 @@ Remove or replace these ids and call fn_task_create again.`
62358
62661
  }
62359
62662
  try {
62360
62663
  const { readFile: readFile26 } = await import("node:fs/promises");
62361
- const { join: join71 } = await import("node:path");
62664
+ const { join: join72 } = await import("node:path");
62362
62665
  const promptContent = await readFile26(
62363
- join71(rootDir, promptPath),
62666
+ join72(rootDir, promptPath),
62364
62667
  "utf-8"
62365
62668
  ).catch((err) => {
62366
62669
  const msg = err instanceof Error ? err.message : String(err);
@@ -62588,160 +62891,8 @@ Take a completely different approach to writing this specification. Do NOT repea
62588
62891
  }
62589
62892
  });
62590
62893
 
62591
- // ../engine/src/session-token-usage.ts
62592
- var session_token_usage_exports = {};
62593
- __export(session_token_usage_exports, {
62594
- accumulateSessionTokenUsage: () => accumulateSessionTokenUsage
62595
- });
62596
- function readSessionStats(session) {
62597
- const accessor = session.getSessionStats;
62598
- if (typeof accessor !== "function") return void 0;
62599
- try {
62600
- return accessor.call(session);
62601
- } catch {
62602
- return void 0;
62603
- }
62604
- }
62605
- async function accumulateSessionTokenUsage(store, taskId, session) {
62606
- try {
62607
- const stats = readSessionStats(session);
62608
- const tokens = stats?.tokens;
62609
- if (!tokens) return;
62610
- const currentInput = (tokens.input ?? 0) + (tokens.cacheWrite ?? 0);
62611
- const currentOutput = tokens.output ?? 0;
62612
- const currentCached = tokens.cacheRead ?? 0;
62613
- const baseline = sessionBaselines.get(session) ?? { input: 0, output: 0, cached: 0 };
62614
- const inputDelta = Math.max(0, currentInput - baseline.input);
62615
- const outputDelta = Math.max(0, currentOutput - baseline.output);
62616
- const cachedDelta = Math.max(0, currentCached - baseline.cached);
62617
- sessionBaselines.set(session, {
62618
- input: currentInput,
62619
- output: currentOutput,
62620
- cached: currentCached
62621
- });
62622
- if (inputDelta === 0 && outputDelta === 0 && cachedDelta === 0) return;
62623
- const task = await store.getTask(taskId);
62624
- const now = (/* @__PURE__ */ new Date()).toISOString();
62625
- const newInput = (task.tokenUsage?.inputTokens ?? 0) + inputDelta;
62626
- const newOutput = (task.tokenUsage?.outputTokens ?? 0) + outputDelta;
62627
- const newCached = (task.tokenUsage?.cachedTokens ?? 0) + cachedDelta;
62628
- await store.updateTask(taskId, {
62629
- tokenUsage: {
62630
- inputTokens: newInput,
62631
- outputTokens: newOutput,
62632
- cachedTokens: newCached,
62633
- totalTokens: newInput + newOutput + newCached,
62634
- firstUsedAt: task.tokenUsage?.firstUsedAt ?? now,
62635
- lastUsedAt: now
62636
- }
62637
- });
62638
- } catch (err) {
62639
- const message = err instanceof Error ? err.message : String(err);
62640
- log14.warn(`${taskId}: session token usage accumulate failed: ${message}`);
62641
- }
62642
- }
62643
- var log14, sessionBaselines;
62644
- var init_session_token_usage = __esm({
62645
- "../engine/src/session-token-usage.ts"() {
62646
- "use strict";
62647
- init_logger2();
62648
- log14 = createLogger2("session-token-usage");
62649
- sessionBaselines = /* @__PURE__ */ new WeakMap();
62650
- }
62651
- });
62652
-
62653
- // ../engine/src/run-audit.ts
62654
- function createRunAuditor(store, context) {
62655
- if (!context) {
62656
- return {
62657
- git: async () => {
62658
- },
62659
- database: async () => {
62660
- },
62661
- filesystem: async () => {
62662
- }
62663
- };
62664
- }
62665
- const hasRecordAuditEvent = typeof store.recordRunAuditEvent === "function";
62666
- if (!hasRecordAuditEvent) {
62667
- return {
62668
- git: async () => {
62669
- },
62670
- database: async () => {
62671
- },
62672
- filesystem: async () => {
62673
- }
62674
- };
62675
- }
62676
- return {
62677
- git: async (input) => {
62678
- const eventInput = {
62679
- taskId: context.taskId,
62680
- agentId: context.agentId,
62681
- runId: context.runId,
62682
- domain: "git",
62683
- mutationType: input.type,
62684
- target: input.target,
62685
- metadata: {
62686
- phase: context.phase,
62687
- ...context.source ? { source: context.source } : {},
62688
- ...input.metadata
62689
- }
62690
- };
62691
- await store.recordRunAuditEvent(eventInput);
62692
- },
62693
- database: async (input) => {
62694
- const inferredTaskId = input.target.startsWith("FN-") || input.target.startsWith("KB-") ? input.target : context.taskId;
62695
- const eventInput = {
62696
- taskId: inferredTaskId,
62697
- agentId: context.agentId,
62698
- runId: context.runId,
62699
- domain: "database",
62700
- mutationType: input.type,
62701
- target: input.target,
62702
- metadata: {
62703
- phase: context.phase,
62704
- ...context.source ? { source: context.source } : {},
62705
- ...input.metadata
62706
- }
62707
- };
62708
- await store.recordRunAuditEvent(eventInput);
62709
- },
62710
- filesystem: async (input) => {
62711
- const eventInput = {
62712
- taskId: context.taskId,
62713
- agentId: context.agentId,
62714
- runId: context.runId,
62715
- domain: "filesystem",
62716
- mutationType: input.type,
62717
- target: input.target,
62718
- metadata: {
62719
- phase: context.phase,
62720
- ...context.source ? { source: context.source } : {},
62721
- ...input.metadata
62722
- }
62723
- };
62724
- await store.recordRunAuditEvent(eventInput);
62725
- }
62726
- };
62727
- }
62728
- function generateSyntheticRunId(prefix, taskId) {
62729
- const timestamp = Date.now();
62730
- const random = Math.random().toString(36).slice(2, 6);
62731
- return `${prefix}-${taskId}-${timestamp}-${random}`;
62732
- }
62733
- var init_run_audit = __esm({
62734
- "../engine/src/run-audit.ts"() {
62735
- "use strict";
62736
- }
62737
- });
62738
-
62739
- // ../engine/src/merger.ts
62740
- import { execSync, exec as exec3, spawn as spawn3 } from "node:child_process";
62741
- import { promisify as promisify4 } from "node:util";
62742
- import { existsSync as existsSync23 } from "node:fs";
62743
- import { join as join29 } from "node:path";
62744
- import { Type as Type3 } from "typebox";
62894
+ // ../engine/src/verification-utils.ts
62895
+ import { spawn as spawn3 } from "node:child_process";
62745
62896
  async function execWithProcessGroup(command, options) {
62746
62897
  return new Promise((resolve44, reject2) => {
62747
62898
  if (options.signal?.aborted) {
@@ -62853,6 +63004,11 @@ function truncateWithEllipsis(text, maxChars) {
62853
63004
  return `${text.slice(0, maxChars)}
62854
63005
  ... (truncated)`;
62855
63006
  }
63007
+ function truncateOutput(output) {
63008
+ if (output.length <= VERIFICATION_LOG_MAX_CHARS) return output;
63009
+ return `... output truncated to last ${VERIFICATION_LOG_MAX_CHARS} characters ...
63010
+ ${output.slice(-VERIFICATION_LOG_MAX_CHARS)}`;
63011
+ }
62856
63012
  function summarizeVerificationOutput(output, type) {
62857
63013
  const lines = output.split("\n");
62858
63014
  let summaryLine = null;
@@ -62918,6 +63074,13 @@ function summarizeVerificationOutput(output, type) {
62918
63074
  failureNames.add(truncated);
62919
63075
  }
62920
63076
  const footer = "(full output available in engine logs)";
63077
+ if (type === "build") {
63078
+ const buildError = output.length > 500 ? `${output.slice(0, 500)}
63079
+ ... (truncated)` : output;
63080
+ return `Build output:
63081
+ ${buildError}
63082
+ ${footer}`;
63083
+ }
62921
63084
  const parts = [];
62922
63085
  if (summaryLine) {
62923
63086
  parts.push(summaryLine);
@@ -62935,29 +63098,292 @@ function summarizeVerificationOutput(output, type) {
62935
63098
  parts.push(` \u2022 ... and ${names.length - 5} more failures`);
62936
63099
  }
62937
63100
  }
62938
- if (parts.length > 0) {
62939
- parts.push(footer);
62940
- return parts.join("\n");
62941
- }
62942
- const trimmed = output.trim();
62943
- if (!trimmed) {
62944
- return `Verification command failed with no output
63101
+ if (parts.length === 0) {
63102
+ if (output.trim().length === 0) {
63103
+ return `no output
63104
+ ${footer}`;
63105
+ }
63106
+ return `${truncateOutput(output)}
62945
63107
  ${footer}`;
62946
63108
  }
62947
- if (trimmed.length <= 500) {
62948
- return `${trimmed}
63109
+ return parts.join("\n") + `
62949
63110
  ${footer}`;
63111
+ }
63112
+ async function runVerificationCommand(store, rootDir, taskId, command, type, signal, log19, agentLabel) {
63113
+ const logger2 = log19 ?? { log: console.log, error: console.error, warn: console.warn };
63114
+ const label = agentLabel ?? "merger";
63115
+ if (signal?.aborted) {
63116
+ throw Object.assign(
63117
+ new Error(`Command aborted before start: ${command}`),
63118
+ { code: "ABORT_ERR", aborted: true }
63119
+ );
62950
63120
  }
62951
- let cutoff = 500;
62952
- for (let i = 500; i < trimmed.length; i++) {
62953
- if (trimmed[i] === " " || trimmed[i] === "\n") {
62954
- cutoff = i;
62955
- break;
63121
+ logger2.log(`${taskId}: running ${type} command: ${command}`);
63122
+ await store.logEntry(taskId, `[verification] Running ${type} command: ${command}`);
63123
+ await store.appendAgentLog(taskId, `Running ${type} command`, "tool", command, label);
63124
+ const result = {
63125
+ command,
63126
+ exitCode: null,
63127
+ stdout: "",
63128
+ stderr: "",
63129
+ success: false
63130
+ };
63131
+ const verificationStartedAt = Date.now();
63132
+ try {
63133
+ const { stdout, stderr, bufferOverflow } = await execWithProcessGroup(command, {
63134
+ cwd: rootDir,
63135
+ timeout: VERIFICATION_COMMAND_TIMEOUT_MS,
63136
+ maxBuffer: VERIFICATION_COMMAND_MAX_BUFFER,
63137
+ signal
63138
+ });
63139
+ if (signal?.aborted) {
63140
+ throw Object.assign(
63141
+ new Error(`Command aborted: ${command}`),
63142
+ { code: "ABORT_ERR", aborted: true }
63143
+ );
63144
+ }
63145
+ result.stdout = stdout?.toString?.() || "";
63146
+ result.stderr = stderr?.toString?.() || "";
63147
+ result.exitCode = 0;
63148
+ result.success = true;
63149
+ const verificationDurationMs = Date.now() - verificationStartedAt;
63150
+ const timingDetail = `${verificationDurationMs}ms`;
63151
+ if (bufferOverflow) {
63152
+ logger2.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
63153
+ await store.logEntry(
63154
+ taskId,
63155
+ `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
63156
+ );
63157
+ await store.appendAgentLog(
63158
+ taskId,
63159
+ `${type} command succeeded (exit 0)`,
63160
+ "tool_result",
63161
+ timingDetail,
63162
+ label
63163
+ );
63164
+ } else {
63165
+ logger2.log(`${taskId}: ${type} command succeeded in ${verificationDurationMs}ms`);
63166
+ await store.logEntry(taskId, `[timing] [verification] ${type} command succeeded (exit 0) in ${verificationDurationMs}ms`);
63167
+ await store.appendAgentLog(
63168
+ taskId,
63169
+ `${type} command succeeded (exit 0)`,
63170
+ "tool_result",
63171
+ timingDetail,
63172
+ label
63173
+ );
63174
+ }
63175
+ return result;
63176
+ } catch (error) {
63177
+ if (signal?.aborted) {
63178
+ throw Object.assign(
63179
+ new Error(`Command aborted: ${command}`),
63180
+ { code: "ABORT_ERR", aborted: true }
63181
+ );
63182
+ }
63183
+ const verificationDurationMs = Date.now() - verificationStartedAt;
63184
+ const err = error;
63185
+ result.stdout = err?.stdout?.toString?.() || "";
63186
+ result.stderr = err?.stderr?.toString?.() || "";
63187
+ result.exitCode = typeof err?.status === "number" ? err.status : typeof err?.code === "number" ? err.code : null;
63188
+ const maxBufferExceeded = err?.code === "ENOBUFS" || err?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || String(err?.message ?? "").includes("maxBuffer");
63189
+ result.success = maxBufferExceeded && result.exitCode === 0;
63190
+ if (result.success) {
63191
+ logger2.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
63192
+ await store.logEntry(
63193
+ taskId,
63194
+ `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
63195
+ );
63196
+ await store.appendAgentLog(
63197
+ taskId,
63198
+ `${type} command succeeded (exit 0)`,
63199
+ "tool_result",
63200
+ `${verificationDurationMs}ms`,
63201
+ label
63202
+ );
63203
+ return result;
62956
63204
  }
63205
+ const output = result.stderr || result.stdout || err?.message || "Unknown error";
63206
+ const summary = summarizeVerificationOutput(output, type);
63207
+ logger2.error(`${taskId}: ${type} command failed (exit ${result.exitCode}) in ${verificationDurationMs}ms; output captured in task log`);
63208
+ await store.logEntry(
63209
+ taskId,
63210
+ `[timing] [verification] ${type} command failed (exit ${result.exitCode}) after ${verificationDurationMs}ms:
63211
+ ${summary}`
63212
+ );
63213
+ await store.appendAgentLog(
63214
+ taskId,
63215
+ `${type} command failed (exit ${result.exitCode})`,
63216
+ "tool_error",
63217
+ summary,
63218
+ label
63219
+ );
62957
63220
  }
62958
- return `${trimmed.slice(0, cutoff)}...
62959
- ${footer}`;
63221
+ return result;
62960
63222
  }
63223
+ var VERIFICATION_COMMAND_MAX_BUFFER, VERIFICATION_COMMAND_TIMEOUT_MS, VERIFICATION_LOG_MAX_CHARS;
63224
+ var init_verification_utils = __esm({
63225
+ "../engine/src/verification-utils.ts"() {
63226
+ "use strict";
63227
+ VERIFICATION_COMMAND_MAX_BUFFER = 50 * 1024 * 1024;
63228
+ VERIFICATION_COMMAND_TIMEOUT_MS = 6e5;
63229
+ VERIFICATION_LOG_MAX_CHARS = 2e4;
63230
+ }
63231
+ });
63232
+
63233
+ // ../engine/src/session-token-usage.ts
63234
+ var session_token_usage_exports = {};
63235
+ __export(session_token_usage_exports, {
63236
+ accumulateSessionTokenUsage: () => accumulateSessionTokenUsage
63237
+ });
63238
+ function readSessionStats(session) {
63239
+ const accessor = session.getSessionStats;
63240
+ if (typeof accessor !== "function") return void 0;
63241
+ try {
63242
+ return accessor.call(session);
63243
+ } catch {
63244
+ return void 0;
63245
+ }
63246
+ }
63247
+ async function accumulateSessionTokenUsage(store, taskId, session) {
63248
+ try {
63249
+ const stats = readSessionStats(session);
63250
+ const tokens = stats?.tokens;
63251
+ if (!tokens) return;
63252
+ const currentInput = (tokens.input ?? 0) + (tokens.cacheWrite ?? 0);
63253
+ const currentOutput = tokens.output ?? 0;
63254
+ const currentCached = tokens.cacheRead ?? 0;
63255
+ const baseline = sessionBaselines.get(session) ?? { input: 0, output: 0, cached: 0 };
63256
+ const inputDelta = Math.max(0, currentInput - baseline.input);
63257
+ const outputDelta = Math.max(0, currentOutput - baseline.output);
63258
+ const cachedDelta = Math.max(0, currentCached - baseline.cached);
63259
+ sessionBaselines.set(session, {
63260
+ input: currentInput,
63261
+ output: currentOutput,
63262
+ cached: currentCached
63263
+ });
63264
+ if (inputDelta === 0 && outputDelta === 0 && cachedDelta === 0) return;
63265
+ const task = await store.getTask(taskId);
63266
+ const now = (/* @__PURE__ */ new Date()).toISOString();
63267
+ const newInput = (task.tokenUsage?.inputTokens ?? 0) + inputDelta;
63268
+ const newOutput = (task.tokenUsage?.outputTokens ?? 0) + outputDelta;
63269
+ const newCached = (task.tokenUsage?.cachedTokens ?? 0) + cachedDelta;
63270
+ await store.updateTask(taskId, {
63271
+ tokenUsage: {
63272
+ inputTokens: newInput,
63273
+ outputTokens: newOutput,
63274
+ cachedTokens: newCached,
63275
+ totalTokens: newInput + newOutput + newCached,
63276
+ firstUsedAt: task.tokenUsage?.firstUsedAt ?? now,
63277
+ lastUsedAt: now
63278
+ }
63279
+ });
63280
+ } catch (err) {
63281
+ const message = err instanceof Error ? err.message : String(err);
63282
+ log14.warn(`${taskId}: session token usage accumulate failed: ${message}`);
63283
+ }
63284
+ }
63285
+ var log14, sessionBaselines;
63286
+ var init_session_token_usage = __esm({
63287
+ "../engine/src/session-token-usage.ts"() {
63288
+ "use strict";
63289
+ init_logger2();
63290
+ log14 = createLogger2("session-token-usage");
63291
+ sessionBaselines = /* @__PURE__ */ new WeakMap();
63292
+ }
63293
+ });
63294
+
63295
+ // ../engine/src/run-audit.ts
63296
+ function createRunAuditor(store, context) {
63297
+ if (!context) {
63298
+ return {
63299
+ git: async () => {
63300
+ },
63301
+ database: async () => {
63302
+ },
63303
+ filesystem: async () => {
63304
+ }
63305
+ };
63306
+ }
63307
+ const hasRecordAuditEvent = typeof store.recordRunAuditEvent === "function";
63308
+ if (!hasRecordAuditEvent) {
63309
+ return {
63310
+ git: async () => {
63311
+ },
63312
+ database: async () => {
63313
+ },
63314
+ filesystem: async () => {
63315
+ }
63316
+ };
63317
+ }
63318
+ return {
63319
+ git: async (input) => {
63320
+ const eventInput = {
63321
+ taskId: context.taskId,
63322
+ agentId: context.agentId,
63323
+ runId: context.runId,
63324
+ domain: "git",
63325
+ mutationType: input.type,
63326
+ target: input.target,
63327
+ metadata: {
63328
+ phase: context.phase,
63329
+ ...context.source ? { source: context.source } : {},
63330
+ ...input.metadata
63331
+ }
63332
+ };
63333
+ await store.recordRunAuditEvent(eventInput);
63334
+ },
63335
+ database: async (input) => {
63336
+ const inferredTaskId = input.target.startsWith("FN-") || input.target.startsWith("KB-") ? input.target : context.taskId;
63337
+ const eventInput = {
63338
+ taskId: inferredTaskId,
63339
+ agentId: context.agentId,
63340
+ runId: context.runId,
63341
+ domain: "database",
63342
+ mutationType: input.type,
63343
+ target: input.target,
63344
+ metadata: {
63345
+ phase: context.phase,
63346
+ ...context.source ? { source: context.source } : {},
63347
+ ...input.metadata
63348
+ }
63349
+ };
63350
+ await store.recordRunAuditEvent(eventInput);
63351
+ },
63352
+ filesystem: async (input) => {
63353
+ const eventInput = {
63354
+ taskId: context.taskId,
63355
+ agentId: context.agentId,
63356
+ runId: context.runId,
63357
+ domain: "filesystem",
63358
+ mutationType: input.type,
63359
+ target: input.target,
63360
+ metadata: {
63361
+ phase: context.phase,
63362
+ ...context.source ? { source: context.source } : {},
63363
+ ...input.metadata
63364
+ }
63365
+ };
63366
+ await store.recordRunAuditEvent(eventInput);
63367
+ }
63368
+ };
63369
+ }
63370
+ function generateSyntheticRunId(prefix, taskId) {
63371
+ const timestamp = Date.now();
63372
+ const random = Math.random().toString(36).slice(2, 6);
63373
+ return `${prefix}-${taskId}-${timestamp}-${random}`;
63374
+ }
63375
+ var init_run_audit = __esm({
63376
+ "../engine/src/run-audit.ts"() {
63377
+ "use strict";
63378
+ }
63379
+ });
63380
+
63381
+ // ../engine/src/merger.ts
63382
+ import { execSync, exec as exec3 } from "node:child_process";
63383
+ import { promisify as promisify4 } from "node:util";
63384
+ import { existsSync as existsSync24 } from "node:fs";
63385
+ import { join as join30 } from "node:path";
63386
+ import { Type as Type3 } from "typebox";
62961
63387
  function truncateWorkflowScriptOutput(output) {
62962
63388
  if (output.length <= WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS) return output;
62963
63389
  return `... output truncated to last ${WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS} characters ...
@@ -63001,7 +63427,7 @@ async function getStagedFiles(cwd) {
63001
63427
  }
63002
63428
  }
63003
63429
  function hasInstallState(rootDir) {
63004
- return existsSync23(join29(rootDir, "node_modules")) || existsSync23(join29(rootDir, ".pnp.cjs"));
63430
+ return existsSync24(join30(rootDir, "node_modules")) || existsSync24(join30(rootDir, ".pnp.cjs"));
63005
63431
  }
63006
63432
  function shouldSyncDependenciesForMerge(stagedFiles, installStatePresent) {
63007
63433
  if (!installStatePresent) return true;
@@ -63010,10 +63436,10 @@ function shouldSyncDependenciesForMerge(stagedFiles, installStatePresent) {
63010
63436
  );
63011
63437
  }
63012
63438
  function getDependencySyncCommand(rootDir) {
63013
- if (existsSync23(join29(rootDir, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
63014
- if (existsSync23(join29(rootDir, "package-lock.json"))) return "npm install";
63015
- if (existsSync23(join29(rootDir, "yarn.lock"))) return "yarn install --frozen-lockfile";
63016
- if (existsSync23(join29(rootDir, "bun.lock")) || existsSync23(join29(rootDir, "bun.lockb"))) {
63439
+ if (existsSync24(join30(rootDir, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
63440
+ if (existsSync24(join30(rootDir, "package-lock.json"))) return "npm install";
63441
+ if (existsSync24(join30(rootDir, "yarn.lock"))) return "yarn install --frozen-lockfile";
63442
+ if (existsSync24(join30(rootDir, "bun.lock")) || existsSync24(join30(rootDir, "bun.lockb"))) {
63017
63443
  return "bun install --frozen-lockfile";
63018
63444
  }
63019
63445
  return null;
@@ -63046,8 +63472,8 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
63046
63472
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
63047
63473
  };
63048
63474
  }
63049
- if (existsSync23(join29(rootDir, "pnpm-lock.yaml"))) {
63050
- if (existsSync23(join29(rootDir, "pnpm-workspace.yaml"))) {
63475
+ if (existsSync24(join30(rootDir, "pnpm-lock.yaml"))) {
63476
+ if (existsSync24(join30(rootDir, "pnpm-workspace.yaml"))) {
63051
63477
  mergerLog.warn(
63052
63478
  `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\`.`
63053
63479
  );
@@ -63058,21 +63484,21 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
63058
63484
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
63059
63485
  };
63060
63486
  }
63061
- if (existsSync23(join29(rootDir, "yarn.lock"))) {
63487
+ if (existsSync24(join30(rootDir, "yarn.lock"))) {
63062
63488
  return {
63063
63489
  command: "yarn test",
63064
63490
  testSource: "inferred",
63065
63491
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
63066
63492
  };
63067
63493
  }
63068
- if (existsSync23(join29(rootDir, "bun.lock")) || existsSync23(join29(rootDir, "bun.lockb"))) {
63494
+ if (existsSync24(join30(rootDir, "bun.lock")) || existsSync24(join30(rootDir, "bun.lockb"))) {
63069
63495
  return {
63070
63496
  command: "bun test",
63071
63497
  testSource: "inferred",
63072
63498
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
63073
63499
  };
63074
63500
  }
63075
- if (existsSync23(join29(rootDir, "package-lock.json"))) {
63501
+ if (existsSync24(join30(rootDir, "package-lock.json"))) {
63076
63502
  return {
63077
63503
  command: "npm test",
63078
63504
  testSource: "inferred",
@@ -63109,7 +63535,7 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
63109
63535
  await store.logEntry(taskId, deterministicVerificationMessage);
63110
63536
  await store.appendAgentLog(taskId, deterministicVerificationMessage, "text", void 0, "merger");
63111
63537
  if (hasTestCommand) {
63112
- const testResult = await runVerificationCommand(
63538
+ const testResult = await runVerificationCommand2(
63113
63539
  store,
63114
63540
  rootDir,
63115
63541
  taskId,
@@ -63140,7 +63566,7 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
63140
63566
  }
63141
63567
  }
63142
63568
  if (hasBuildCommand) {
63143
- const buildResult = await runVerificationCommand(
63569
+ const buildResult = await runVerificationCommand2(
63144
63570
  store,
63145
63571
  rootDir,
63146
63572
  taskId,
@@ -63175,98 +63601,9 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
63175
63601
  await store.appendAgentLog(taskId, "Deterministic merge verification passed", "text", void 0, "merger");
63176
63602
  return result;
63177
63603
  }
63178
- async function runVerificationCommand(store, rootDir, taskId, command, type, signal) {
63604
+ async function runVerificationCommand2(store, rootDir, taskId, command, type, signal) {
63179
63605
  throwIfAborted(signal, taskId);
63180
- mergerLog.log(`${taskId}: running ${type} command: ${command}`);
63181
- await store.logEntry(taskId, `[verification] Running ${type} command: ${command}`);
63182
- await store.appendAgentLog(taskId, `Running ${type} command`, "tool", command, "merger");
63183
- const result = {
63184
- command,
63185
- exitCode: null,
63186
- stdout: "",
63187
- stderr: "",
63188
- success: false
63189
- };
63190
- const verificationStartedAt = Date.now();
63191
- try {
63192
- const { stdout, stderr, bufferOverflow } = await execWithProcessGroup(command, {
63193
- cwd: rootDir,
63194
- timeout: VERIFICATION_COMMAND_TIMEOUT_MS,
63195
- maxBuffer: VERIFICATION_COMMAND_MAX_BUFFER,
63196
- signal
63197
- });
63198
- throwIfAborted(signal, taskId);
63199
- result.stdout = stdout?.toString?.() || "";
63200
- result.stderr = stderr?.toString?.() || "";
63201
- result.exitCode = 0;
63202
- result.success = true;
63203
- const verificationDurationMs = Date.now() - verificationStartedAt;
63204
- const timingDetail = `${verificationDurationMs}ms`;
63205
- if (bufferOverflow) {
63206
- mergerLog.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
63207
- await store.logEntry(
63208
- taskId,
63209
- `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
63210
- );
63211
- await store.appendAgentLog(
63212
- taskId,
63213
- `${type} command succeeded (exit 0)`,
63214
- "tool_result",
63215
- timingDetail,
63216
- "merger"
63217
- );
63218
- } else {
63219
- mergerLog.log(`${taskId}: ${type} command succeeded in ${verificationDurationMs}ms`);
63220
- await store.logEntry(taskId, `[timing] [verification] ${type} command succeeded (exit 0) in ${verificationDurationMs}ms`);
63221
- await store.appendAgentLog(
63222
- taskId,
63223
- `${type} command succeeded (exit 0)`,
63224
- "tool_result",
63225
- timingDetail,
63226
- "merger"
63227
- );
63228
- }
63229
- return result;
63230
- } catch (error) {
63231
- throwIfAborted(signal, taskId);
63232
- const verificationDurationMs = Date.now() - verificationStartedAt;
63233
- result.stdout = error?.stdout?.toString?.() || "";
63234
- result.stderr = error?.stderr?.toString?.() || "";
63235
- result.exitCode = typeof error?.status === "number" ? error.status : typeof error?.code === "number" ? error.code : null;
63236
- const maxBufferExceeded = error?.code === "ENOBUFS" || error?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || String(error?.message ?? "").includes("maxBuffer");
63237
- result.success = maxBufferExceeded && result.exitCode === 0;
63238
- if (result.success) {
63239
- mergerLog.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
63240
- await store.logEntry(
63241
- taskId,
63242
- `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
63243
- );
63244
- await store.appendAgentLog(
63245
- taskId,
63246
- `${type} command succeeded (exit 0)`,
63247
- "tool_result",
63248
- `${verificationDurationMs}ms`,
63249
- "merger"
63250
- );
63251
- return result;
63252
- }
63253
- const output = result.stderr || result.stdout || error?.message || "Unknown error";
63254
- const summary = summarizeVerificationOutput(output, type);
63255
- mergerLog.error(`${taskId}: ${type} command failed (exit ${result.exitCode}) in ${verificationDurationMs}ms; output captured in task log`);
63256
- await store.logEntry(
63257
- taskId,
63258
- `[timing] [verification] ${type} command failed (exit ${result.exitCode}) after ${verificationDurationMs}ms:
63259
- ${summary}`
63260
- );
63261
- await store.appendAgentLog(
63262
- taskId,
63263
- `${type} command failed (exit ${result.exitCode})`,
63264
- "tool_error",
63265
- summary,
63266
- "merger"
63267
- );
63268
- }
63269
- return result;
63606
+ return runVerificationCommand(store, rootDir, taskId, command, type, signal, mergerLog, "merger");
63270
63607
  }
63271
63608
  async function attemptInMergeVerificationFix(store, rootDir, taskId, failureContext, settings, options, mergeRunContext, fixAttemptNumber, _testCommand, _buildCommand) {
63272
63609
  try {
@@ -63329,9 +63666,20 @@ Do not refactor, rename broadly, or make opportunistic improvements.
63329
63666
  onToolEnd: logger2.onToolEnd,
63330
63667
  defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
63331
63668
  defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
63669
+ fallbackProvider: settings.fallbackProvider,
63670
+ fallbackModelId: settings.fallbackModelId,
63332
63671
  defaultThinkingLevel: settings.defaultThinkingLevel,
63333
63672
  // Skill selection: use assigned agent skills if available, otherwise role fallback
63334
- ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
63673
+ ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
63674
+ taskId,
63675
+ taskTitle: taskForSkillContext?.title,
63676
+ onFallbackModelUsed: createFallbackModelObserver({
63677
+ agent: "merger",
63678
+ label: "merge verification fix agent",
63679
+ store,
63680
+ taskId,
63681
+ taskTitle: taskForSkillContext?.title
63682
+ })
63335
63683
  });
63336
63684
  const runId = mergeRunContext?.runId;
63337
63685
  const agentId = mergeRunContext?.agentId ?? "merger";
@@ -63384,7 +63732,7 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
63384
63732
  void 0,
63385
63733
  "merger"
63386
63734
  );
63387
- const reRunResult = await runVerificationCommand(
63735
+ const reRunResult = await runVerificationCommand2(
63388
63736
  store,
63389
63737
  rootDir,
63390
63738
  taskId,
@@ -64126,9 +64474,16 @@ You are assisting with a paused \`git pull --rebase\`.
64126
64474
  onToolEnd: agentLogger.onToolEnd,
64127
64475
  defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
64128
64476
  defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
64477
+ fallbackProvider: settings.fallbackProvider,
64478
+ fallbackModelId: settings.fallbackModelId,
64129
64479
  defaultThinkingLevel: settings.defaultThinkingLevel,
64130
64480
  taskId,
64131
- onFallbackModelUsed: notifyFallbackUsed
64481
+ onFallbackModelUsed: createFallbackModelObserver({
64482
+ agent: "merger",
64483
+ label: "rebase conflict resolver",
64484
+ store,
64485
+ taskId
64486
+ })
64132
64487
  });
64133
64488
  const prompt = [
64134
64489
  `Resolve rebase conflicts for task ${taskId}.`,
@@ -64320,7 +64675,7 @@ async function pushToRemoteAfterMerge(store, rootDir, taskId, settings, options)
64320
64675
  }
64321
64676
  async function createPostMergeWorktree(rootDir, taskId) {
64322
64677
  const randomSuffix = Math.random().toString(36).slice(2, 10);
64323
- const postMergeWorktree = join29(rootDir, ".worktrees", `post-merge-${taskId}-${randomSuffix}`);
64678
+ const postMergeWorktree = join30(rootDir, ".worktrees", `post-merge-${taskId}-${randomSuffix}`);
64324
64679
  try {
64325
64680
  await execAsync2(`git worktree add ${quoteArg(postMergeWorktree)} HEAD`, { cwd: rootDir });
64326
64681
  return postMergeWorktree;
@@ -65261,7 +65616,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
65261
65616
  }
65262
65617
  }
65263
65618
  throwIfAborted(options.signal, taskId);
65264
- if (worktreePath && existsSync23(worktreePath)) {
65619
+ if (worktreePath && existsSync24(worktreePath)) {
65265
65620
  const otherUser = await findWorktreeUser(store, worktreePath, taskId);
65266
65621
  if (otherUser) {
65267
65622
  mergerLog.log(`Worktree retained \u2014 still needed by ${otherUser}`);
@@ -65915,9 +66270,20 @@ async function runAiAgentForCommit(params) {
65915
66270
  onToolEnd: agentLogger.onToolEnd,
65916
66271
  defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
65917
66272
  defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
66273
+ fallbackProvider: settings.fallbackProvider,
66274
+ fallbackModelId: settings.fallbackModelId,
65918
66275
  defaultThinkingLevel: settings.defaultThinkingLevel,
65919
66276
  // Skill selection: use assigned agent skills if available, otherwise role fallback
65920
- ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
66277
+ ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
66278
+ taskId,
66279
+ taskTitle: taskForSkillContext?.title,
66280
+ onFallbackModelUsed: createFallbackModelObserver({
66281
+ agent: "merger",
66282
+ label: "merge agent",
66283
+ store,
66284
+ taskId,
66285
+ taskTitle: taskForSkillContext?.title
66286
+ })
65921
66287
  });
65922
66288
  options.onSession?.(session);
65923
66289
  try {
@@ -66348,7 +66714,14 @@ If issues are found that need attention, describe them clearly and include concr
66348
66714
  fallbackModelId: settings.fallbackModelId,
66349
66715
  defaultThinkingLevel: settings.defaultThinkingLevel,
66350
66716
  // Skill selection: use assigned agent skills if available, otherwise role fallback
66351
- ...postMergeSkillContext?.skillSelectionContext ? { skillSelection: postMergeSkillContext.skillSelectionContext } : {}
66717
+ ...postMergeSkillContext?.skillSelectionContext ? { skillSelection: postMergeSkillContext.skillSelectionContext } : {},
66718
+ taskId,
66719
+ onFallbackModelUsed: createFallbackModelObserver({
66720
+ agent: "merger",
66721
+ label: `post-merge workflow step '${workflowStep.name}'`,
66722
+ store,
66723
+ taskId
66724
+ })
66352
66725
  });
66353
66726
  mergerLog.log(`${taskId}: [post-merge] workflow step '${workflowStep.name}' using model ${describeModel(session)}${useOverride ? " (workflow step override)" : ""}`);
66354
66727
  await store.logEntry(taskId, `[post-merge] Workflow step '${workflowStep.name}' using model: ${describeModel(session)}${useOverride ? " (workflow step override)" : ""}`);
@@ -66384,15 +66757,17 @@ async function completeTask(store, taskId, result) {
66384
66757
  result.task = task;
66385
66758
  store.emit("task:merged", result);
66386
66759
  }
66387
- 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;
66760
+ 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;
66388
66761
  var init_merger = __esm({
66389
66762
  "../engine/src/merger.ts"() {
66390
66763
  "use strict";
66764
+ init_verification_utils();
66765
+ init_verification_utils();
66391
66766
  init_src();
66392
66767
  init_pi();
66393
66768
  init_session_token_usage();
66394
66769
  init_agent_session_helpers();
66395
- init_notifier();
66770
+ init_fallback_model_observer();
66396
66771
  init_session_skill_context();
66397
66772
  init_agent_logger();
66398
66773
  init_logger2();
@@ -66438,9 +66813,6 @@ var init_merger = __esm({
66438
66813
  "bun.lock",
66439
66814
  "packages/*/package.json"
66440
66815
  ];
66441
- VERIFICATION_COMMAND_MAX_BUFFER = 50 * 1024 * 1024;
66442
- VERIFICATION_COMMAND_TIMEOUT_MS = 6e5;
66443
- VERIFICATION_LOG_MAX_CHARS = 2e4;
66444
66816
  WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS = 4e3;
66445
66817
  PULL_REBASE_TIMEOUT_MS = 12e4;
66446
66818
  PUSH_TIMEOUT_MS = 6e4;
@@ -66465,8 +66837,8 @@ var init_merger = __esm({
66465
66837
 
66466
66838
  // ../engine/src/worktree-names.ts
66467
66839
  import { readdirSync as readdirSync3 } from "node:fs";
66468
- import { join as join30 } from "node:path";
66469
- import { existsSync as existsSync24 } from "node:fs";
66840
+ import { join as join31 } from "node:path";
66841
+ import { existsSync as existsSync25 } from "node:fs";
66470
66842
  function slugify2(str) {
66471
66843
  return str.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
66472
66844
  }
@@ -66477,7 +66849,7 @@ function generateReservedWorktreeName(rootDir, reservedNames = /* @__PURE__ */ n
66477
66849
  const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
66478
66850
  const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
66479
66851
  const baseName = `${adjective}-${noun}`;
66480
- const worktreesDir = join30(rootDir, ".worktrees");
66852
+ const worktreesDir = join31(rootDir, ".worktrees");
66481
66853
  const existing = getExistingWorktreeNames(worktreesDir);
66482
66854
  for (const reserved of reservedNames) {
66483
66855
  existing.add(reserved);
@@ -66492,7 +66864,7 @@ function generateReservedWorktreeName(rootDir, reservedNames = /* @__PURE__ */ n
66492
66864
  return `${baseName}-${suffix}`;
66493
66865
  }
66494
66866
  function getExistingWorktreeNames(worktreesDir) {
66495
- if (!existsSync24(worktreesDir)) {
66867
+ if (!existsSync25(worktreesDir)) {
66496
66868
  return /* @__PURE__ */ new Set();
66497
66869
  }
66498
66870
  try {
@@ -66629,8 +67001,8 @@ __export(worktree_pool_exports, {
66629
67001
  });
66630
67002
  import { exec as exec4 } from "node:child_process";
66631
67003
  import { promisify as promisify5 } from "node:util";
66632
- import { existsSync as existsSync25, lstatSync, readdirSync as readdirSync4, rmSync as rmSync2 } from "node:fs";
66633
- import { join as join31, relative as relative6, resolve as resolve15, isAbsolute as isAbsolute9 } from "node:path";
67004
+ import { existsSync as existsSync26, lstatSync, readdirSync as readdirSync4, rmSync as rmSync2 } from "node:fs";
67005
+ import { join as join32, relative as relative6, resolve as resolve15, isAbsolute as isAbsolute9 } from "node:path";
66634
67006
  function getExecStdout(result) {
66635
67007
  if (typeof result === "string") return result;
66636
67008
  if (result && typeof result === "object" && "stdout" in result) {
@@ -66676,10 +67048,10 @@ async function isRegisteredGitWorktree2(rootDir, worktreePath) {
66676
67048
  return (await getRegisteredWorktreePaths(rootDir)).has(resolve15(worktreePath));
66677
67049
  }
66678
67050
  function hasRequiredWorktreeFiles(worktreePath) {
66679
- return existsSync25(join31(worktreePath, ".git")) && existsSync25(join31(worktreePath, "package.json"));
67051
+ return existsSync26(join32(worktreePath, ".git")) && existsSync26(join32(worktreePath, "package.json"));
66680
67052
  }
66681
67053
  async function isUsableTaskWorktree(rootDir, worktreePath) {
66682
- return existsSync25(worktreePath) && await isRegisteredGitWorktree2(rootDir, worktreePath) && hasRequiredWorktreeFiles(worktreePath);
67054
+ return existsSync26(worktreePath) && await isRegisteredGitWorktree2(rootDir, worktreePath) && hasRequiredWorktreeFiles(worktreePath);
66683
67055
  }
66684
67056
  function isInsideWorktreesDir(rootDir, worktreePath) {
66685
67057
  const worktreesDir = resolve15(rootDir, ".worktrees");
@@ -66688,14 +67060,14 @@ function isInsideWorktreesDir(rootDir, worktreePath) {
66688
67060
  return rel !== "" && !rel.startsWith("..") && !isAbsolute9(rel);
66689
67061
  }
66690
67062
  async function scanIdleWorktrees(rootDir, store) {
66691
- const worktreesDir = join31(rootDir, ".worktrees");
66692
- if (!existsSync25(worktreesDir)) {
67063
+ const worktreesDir = join32(rootDir, ".worktrees");
67064
+ if (!existsSync26(worktreesDir)) {
66693
67065
  return [];
66694
67066
  }
66695
67067
  let dirs;
66696
67068
  try {
66697
67069
  const entries = readdirSync4(worktreesDir, { withFileTypes: true });
66698
- dirs = entries.filter((e) => e.isDirectory()).map((e) => join31(worktreesDir, e.name));
67070
+ dirs = entries.filter((e) => e.isDirectory()).map((e) => join32(worktreesDir, e.name));
66699
67071
  } catch (err) {
66700
67072
  const errorMessage = err instanceof Error ? err.message : String(err);
66701
67073
  worktreePoolLog.warn(`Failed to read .worktrees/ directory: ${errorMessage}`);
@@ -66718,16 +67090,16 @@ async function scanIdleWorktrees(rootDir, store) {
66718
67090
  return registeredDirs.filter((dir2) => !activeWorktrees.has(resolve15(dir2)));
66719
67091
  }
66720
67092
  async function cleanupOrphanedWorktrees(rootDir, store) {
66721
- const worktreesDir = join31(rootDir, ".worktrees");
66722
- if (!existsSync25(worktreesDir)) {
67093
+ const worktreesDir = join32(rootDir, ".worktrees");
67094
+ if (!existsSync26(worktreesDir)) {
66723
67095
  return 0;
66724
67096
  }
66725
67097
  const orphaned = await scanIdleWorktrees(rootDir, store);
66726
67098
  const registeredWorktrees = await getRegisteredWorktreePaths(rootDir);
66727
67099
  let dirs = [];
66728
- if (existsSync25(worktreesDir)) {
67100
+ if (existsSync26(worktreesDir)) {
66729
67101
  try {
66730
- dirs = readdirSync4(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join31(worktreesDir, e.name));
67102
+ dirs = readdirSync4(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join32(worktreesDir, e.name));
66731
67103
  } catch (err) {
66732
67104
  const errorMessage = err instanceof Error ? err.message : String(err);
66733
67105
  worktreePoolLog.warn(`Failed to read .worktrees/ directory for cleanup: ${errorMessage}`);
@@ -66759,8 +67131,8 @@ async function cleanupOrphanedWorktrees(rootDir, store) {
66759
67131
  return cleaned;
66760
67132
  }
66761
67133
  async function reapOrphanWorktrees(projectRoot) {
66762
- const worktreesDir = join31(projectRoot, ".worktrees");
66763
- if (!existsSync25(worktreesDir)) {
67134
+ const worktreesDir = join32(projectRoot, ".worktrees");
67135
+ if (!existsSync26(worktreesDir)) {
66764
67136
  return 0;
66765
67137
  }
66766
67138
  let entries;
@@ -66768,11 +67140,11 @@ async function reapOrphanWorktrees(projectRoot) {
66768
67140
  entries = readdirSync4(worktreesDir, { withFileTypes: true }).filter((e) => {
66769
67141
  if (!e.isDirectory()) return false;
66770
67142
  try {
66771
- return lstatSync(join31(worktreesDir, e.name)).isDirectory() && !lstatSync(join31(worktreesDir, e.name)).isSymbolicLink();
67143
+ return lstatSync(join32(worktreesDir, e.name)).isDirectory() && !lstatSync(join32(worktreesDir, e.name)).isSymbolicLink();
66772
67144
  } catch {
66773
67145
  return false;
66774
67146
  }
66775
- }).map((e) => ({ name: e.name, fullPath: join31(worktreesDir, e.name) }));
67147
+ }).map((e) => ({ name: e.name, fullPath: join32(worktreesDir, e.name) }));
66776
67148
  } catch (err) {
66777
67149
  const msg = err instanceof Error ? err.message : String(err);
66778
67150
  worktreePoolLog.warn(`reapOrphanWorktrees: failed to read .worktrees/ \u2014 ${msg}`);
@@ -66791,8 +67163,8 @@ async function reapOrphanWorktrees(projectRoot) {
66791
67163
  if (registered.has(resolvedFull)) {
66792
67164
  continue;
66793
67165
  }
66794
- const dotGit = join31(resolvedFull, ".git");
66795
- if (existsSync25(dotGit)) {
67166
+ const dotGit = join32(resolvedFull, ".git");
67167
+ if (existsSync26(dotGit)) {
66796
67168
  worktreePoolLog.log(`reapOrphanWorktrees: skipping ${name} (has .git entry but not in registered list \u2014 may be partially registered)`);
66797
67169
  continue;
66798
67170
  }
@@ -66852,7 +67224,7 @@ var init_worktree_pool = __esm({
66852
67224
  acquire() {
66853
67225
  for (const path5 of this.idle) {
66854
67226
  this.idle.delete(path5);
66855
- if (existsSync25(path5)) {
67227
+ if (existsSync26(path5)) {
66856
67228
  return path5;
66857
67229
  }
66858
67230
  worktreePoolLog.log(`Pruned stale entry: ${path5}`);
@@ -66899,7 +67271,7 @@ var init_worktree_pool = __esm({
66899
67271
  */
66900
67272
  rehydrate(idlePaths) {
66901
67273
  for (const path5 of idlePaths) {
66902
- if (existsSync25(path5)) {
67274
+ if (existsSync26(path5)) {
66903
67275
  this.idle.add(path5);
66904
67276
  } else {
66905
67277
  worktreePoolLog.log(`Rehydrate skipped (not on disk): ${path5}`);
@@ -66955,7 +67327,7 @@ var init_worktree_pool = __esm({
66955
67327
  throw err;
66956
67328
  }
66957
67329
  const conflictingPath = match[1];
66958
- if (!existsSync25(conflictingPath)) {
67330
+ if (!existsSync26(conflictingPath)) {
66959
67331
  await execAsync3("git worktree prune", { cwd: worktreePath });
66960
67332
  await execAsync3(checkoutCmd, { cwd: worktreePath });
66961
67333
  return branchName;
@@ -67032,8 +67404,8 @@ var init_token_cap_detector = __esm({
67032
67404
  // ../engine/src/step-session-executor.ts
67033
67405
  import { exec as exec5 } from "node:child_process";
67034
67406
  import { promisify as promisify6 } from "node:util";
67035
- import { existsSync as existsSync26 } from "node:fs";
67036
- import { join as join32 } from "node:path";
67407
+ import { existsSync as existsSync27 } from "node:fs";
67408
+ import { join as join33 } from "node:path";
67037
67409
  function parseStepFileScopes(prompt) {
67038
67410
  const result = /* @__PURE__ */ new Map();
67039
67411
  if (!prompt) return result;
@@ -67320,7 +67692,7 @@ var init_step_session_executor = __esm({
67320
67692
  init_worktree_names();
67321
67693
  init_agent_logger();
67322
67694
  init_logger2();
67323
- init_notifier();
67695
+ init_fallback_model_observer();
67324
67696
  init_context_limit_detector();
67325
67697
  init_usage_limit_detector();
67326
67698
  init_agent_tools();
@@ -67437,7 +67809,7 @@ var init_step_session_executor = __esm({
67437
67809
  }
67438
67810
  for (const [stepIdx, worktreePath] of this.parallelWorktrees) {
67439
67811
  try {
67440
- if (existsSync26(worktreePath)) {
67812
+ if (existsSync27(worktreePath)) {
67441
67813
  await execAsync4(`git worktree remove "${worktreePath}" --force`, {
67442
67814
  cwd: this.options.rootDir
67443
67815
  });
@@ -67602,7 +67974,13 @@ Follow instructions precisely and avoid unrelated changes.`,
67602
67974
  ...this.options.skillSelection ? { skillSelection: this.options.skillSelection } : {},
67603
67975
  taskId: taskDetail.id,
67604
67976
  taskTitle: taskDetail.title,
67605
- onFallbackModelUsed: notifyFallbackUsed
67977
+ onFallbackModelUsed: createFallbackModelObserver({
67978
+ agent: "executor",
67979
+ label: "workflow step agent",
67980
+ store: this.store,
67981
+ taskId: taskDetail.id,
67982
+ taskTitle: taskDetail.title
67983
+ })
67606
67984
  });
67607
67985
  session = createResult.session;
67608
67986
  const handle = {
@@ -67782,7 +68160,7 @@ Follow instructions precisely and avoid unrelated changes.`,
67782
68160
  for (const [stepIdx, worktreePath] of worktreePaths) {
67783
68161
  if (worktreePath !== this.options.worktreePath) {
67784
68162
  try {
67785
- if (existsSync26(worktreePath)) {
68163
+ if (existsSync27(worktreePath)) {
67786
68164
  await execAsync4(`git worktree remove "${worktreePath}" --force`, {
67787
68165
  cwd: this.options.rootDir
67788
68166
  });
@@ -67812,7 +68190,7 @@ Follow instructions precisely and avoid unrelated changes.`,
67812
68190
  async createStepWorktree(stepIndex) {
67813
68191
  const { rootDir } = this.options;
67814
68192
  const name = generateWorktreeName(rootDir);
67815
- const worktreePath = join32(rootDir, ".worktrees", name);
68193
+ const worktreePath = join33(rootDir, ".worktrees", name);
67816
68194
  const branchName = `fusion/step-${stepIndex}-${name}`;
67817
68195
  stepExecLog.log(`Creating worktree for step ${stepIndex}: ${worktreePath} (branch: ${branchName})`);
67818
68196
  try {
@@ -67887,7 +68265,7 @@ Follow instructions precisely and avoid unrelated changes.`,
67887
68265
 
67888
68266
  // ../engine/src/spec-staleness.ts
67889
68267
  import { stat as stat5 } from "node:fs/promises";
67890
- import { join as join33 } from "node:path";
68268
+ import { join as join34 } from "node:path";
67891
68269
  async function evaluateSpecStaleness(options) {
67892
68270
  const { settings, promptPath, nowMs } = options;
67893
68271
  if (settings.specStalenessEnabled !== true) {
@@ -67927,7 +68305,7 @@ async function evaluateSpecStaleness(options) {
67927
68305
  };
67928
68306
  }
67929
68307
  function getPromptPath(tasksDir, taskId) {
67930
- return join33(tasksDir, taskId, "PROMPT.md");
68308
+ return join34(tasksDir, taskId, "PROMPT.md");
67931
68309
  }
67932
68310
  var DEFAULT_SPEC_STALENESS_MAX_AGE_MS;
67933
68311
  var init_spec_staleness = __esm({
@@ -67958,8 +68336,8 @@ var init_task_completion = __esm({
67958
68336
 
67959
68337
  // ../engine/src/run-verification-tool.ts
67960
68338
  import { spawn as spawn4 } from "node:child_process";
67961
- import { existsSync as existsSync27 } from "node:fs";
67962
- import { isAbsolute as isAbsolute10, join as join34 } from "node:path";
68339
+ import { existsSync as existsSync28 } from "node:fs";
68340
+ import { isAbsolute as isAbsolute10, join as join35 } from "node:path";
67963
68341
  import { Type as Type4 } from "@mariozechner/pi-ai";
67964
68342
  function createBuffer() {
67965
68343
  return { headChunks: [], headBytes: 0, tailChunks: [], tailBytes: 0, totalBytes: 0 };
@@ -67992,7 +68370,7 @@ function flattenBuffer(buf) {
67992
68370
 
67993
68371
  ` + tail;
67994
68372
  }
67995
- async function runVerificationCommand2(opts) {
68373
+ async function runVerificationCommand3(opts) {
67996
68374
  const { command, cwd, timeoutMs, expectFailure = false, onHeartbeat, onLine } = opts;
67997
68375
  const startMs = Date.now();
67998
68376
  const warnings = [];
@@ -68132,7 +68510,7 @@ function createRunVerificationTool(opts) {
68132
68510
  if (params.cwd && isAbsolute10(params.cwd)) {
68133
68511
  resolvedCwd = params.cwd;
68134
68512
  } else if (params.cwd) {
68135
- resolvedCwd = join34(worktreePath, params.cwd);
68513
+ resolvedCwd = join35(worktreePath, params.cwd);
68136
68514
  } else {
68137
68515
  resolvedCwd = worktreePath;
68138
68516
  }
@@ -68147,8 +68525,8 @@ function createRunVerificationTool(opts) {
68147
68525
  }
68148
68526
  let effectiveCommand = command;
68149
68527
  if (command.trimStart().startsWith("pnpm --filter")) {
68150
- const modulesYaml = join34(rootDir, "node_modules", ".modules.yaml");
68151
- if (!existsSync27(modulesYaml)) {
68528
+ const modulesYaml = join35(rootDir, "node_modules", ".modules.yaml");
68529
+ if (!existsSync28(modulesYaml)) {
68152
68530
  const installCmd = "pnpm install --prefer-offline";
68153
68531
  const msg = `node_modules/.modules.yaml not found in workspace root \u2014 auto-prepending \`${installCmd}\` before running the command.`;
68154
68532
  warnings.push(msg);
@@ -68159,7 +68537,7 @@ function createRunVerificationTool(opts) {
68159
68537
  log19.info(
68160
68538
  `[fn_run_verification] ${taskId}: scope=${scope} timeout=${timeoutSec}s cwd=${resolvedCwd} cmd=${effectiveCommand}`
68161
68539
  );
68162
- const result = await runVerificationCommand2({
68540
+ const result = await runVerificationCommand3({
68163
68541
  command: effectiveCommand,
68164
68542
  cwd: resolvedCwd,
68165
68543
  timeoutMs,
@@ -68259,8 +68637,8 @@ var init_run_verification_tool = __esm({
68259
68637
  // ../engine/src/executor.ts
68260
68638
  import { exec as exec6 } from "node:child_process";
68261
68639
  import { promisify as promisify7 } from "node:util";
68262
- import { isAbsolute as isAbsolute11, join as join35, relative as relative7, resolve as resolvePath } from "node:path";
68263
- import { existsSync as existsSync28 } from "node:fs";
68640
+ import { isAbsolute as isAbsolute11, join as join36, relative as relative7, resolve as resolvePath } from "node:path";
68641
+ import { existsSync as existsSync29 } from "node:fs";
68264
68642
  import { readFile as readFile15, writeFile as writeFile12 } from "node:fs/promises";
68265
68643
  import { Type as Type5 } from "@mariozechner/pi-ai";
68266
68644
  import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
@@ -68554,6 +68932,7 @@ var init_executor = __esm({
68554
68932
  "use strict";
68555
68933
  init_src();
68556
68934
  init_merger();
68935
+ init_verification_utils();
68557
68936
  init_worktree_names();
68558
68937
  init_pi();
68559
68938
  init_session_token_usage();
@@ -68578,7 +68957,7 @@ var init_executor = __esm({
68578
68957
  init_task_completion();
68579
68958
  init_auth_storage();
68580
68959
  init_run_verification_tool();
68581
- init_notifier();
68960
+ init_fallback_model_observer();
68582
68961
  init_agent_logger();
68583
68962
  init_agent_tools();
68584
68963
  execAsync5 = promisify7(exec6);
@@ -69725,7 +70104,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69725
70104
  );
69726
70105
  return false;
69727
70106
  }
69728
- if (task.worktree && existsSync28(task.worktree)) {
70107
+ if (task.worktree && existsSync29(task.worktree)) {
69729
70108
  const modifiedFiles = await this.captureModifiedFiles(task.worktree, task.baseCommitSha);
69730
70109
  if (modifiedFiles.length > 0) {
69731
70110
  await this.store.updateTask(task.id, { modifiedFiles });
@@ -69905,7 +70284,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69905
70284
  if (task.dependencies.length === 0) return null;
69906
70285
  for (const depId of task.dependencies) {
69907
70286
  const dep = allTasks.find((t) => t.id === depId);
69908
- if (dep && dep.worktree && (dep.column === "done" || dep.column === "in-review") && existsSync28(dep.worktree)) {
70287
+ if (dep && dep.worktree && (dep.column === "done" || dep.column === "in-review") && existsSync29(dep.worktree)) {
69909
70288
  return dep.worktree;
69910
70289
  }
69911
70290
  }
@@ -69956,7 +70335,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69956
70335
  const activeMergeStatuses = /* @__PURE__ */ new Set(["merging", "merging-pr"]);
69957
70336
  const isActiveTask = activeColumns.has(task.column) || activeMergeStatuses.has(task.status ?? "");
69958
70337
  if (!isActiveTask) {
69959
- const tasksDir = join35(this.store.getFusionDir(), "tasks");
70338
+ const tasksDir = join36(this.store.getFusionDir(), "tasks");
69960
70339
  const promptPath = getPromptPath(tasksDir, task.id);
69961
70340
  const staleness = await evaluateSpecStaleness({ settings, promptPath });
69962
70341
  if (staleness.isStale) {
@@ -70003,7 +70382,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70003
70382
  worktreeName = generateWorktreeName(this.rootDir);
70004
70383
  break;
70005
70384
  }
70006
- worktreePath = join35(this.rootDir, ".worktrees", worktreeName);
70385
+ worktreePath = join36(this.rootDir, ".worktrees", worktreeName);
70007
70386
  }
70008
70387
  let stuckRequeue = null;
70009
70388
  let taskDone = false;
@@ -70027,7 +70406,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70027
70406
  );
70028
70407
  }
70029
70408
  const branchName = task.branch || `fusion/${task.id.toLowerCase()}`;
70030
- let isResume = existsSync28(worktreePath);
70409
+ let isResume = existsSync29(worktreePath);
70031
70410
  let acquiredFromPool = false;
70032
70411
  const baseBranch = task.baseBranch || null;
70033
70412
  if (task.worktree && isResume && !await isUsableTaskWorktree(this.rootDir, worktreePath)) {
@@ -70040,8 +70419,8 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70040
70419
  this.currentRunContext
70041
70420
  );
70042
70421
  await this.store.updateTask(task.id, { worktree: null, branch: null });
70043
- worktreePath = join35(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
70044
- isResume = existsSync28(worktreePath);
70422
+ worktreePath = join36(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
70423
+ isResume = existsSync29(worktreePath);
70045
70424
  }
70046
70425
  if (!isResume) {
70047
70426
  if (this.options.pool && settings.recycleWorktrees) {
@@ -70267,6 +70646,84 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70267
70646
  if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps after step-session completion")) {
70268
70647
  return;
70269
70648
  }
70649
+ if (executionMode !== "fast") {
70650
+ if (settings.testCommand?.trim() || settings.buildCommand?.trim()) {
70651
+ const verificationResult = await this.runExecutorDeterministicVerification(task, worktreePath, settings);
70652
+ if (!verificationResult.allPassed) {
70653
+ const failedType = verificationResult.failedCommand === "testCommand" ? "test" : "build";
70654
+ const failedResult = failedType === "test" ? verificationResult.testResult : verificationResult.buildResult;
70655
+ const failedCommand = failedResult.command;
70656
+ const failureOutput = failedResult.stderr || failedResult.stdout || "Unknown error";
70657
+ const summary = summarizeVerificationOutput(failureOutput, failedType);
70658
+ executorLog.log(`${task.id}: [verification] ${failedType} failed \u2014 attempting fix agent`);
70659
+ await this.store.logEntry(
70660
+ task.id,
70661
+ `[verification] ${failedType} command failed (exit ${failedResult.exitCode}). Attempting fix agent...`,
70662
+ summary,
70663
+ this.currentRunContext
70664
+ );
70665
+ const maxFixRetries = Math.min(settings.verificationFixRetries ?? 3, 3);
70666
+ if (maxFixRetries === 0) {
70667
+ executorLog.log(`${task.id}: [verification] fix retries set to 0 \u2014 sending task back immediately`);
70668
+ await this.sendTaskBackForFix(
70669
+ task,
70670
+ worktreePath,
70671
+ `${failedType} command \`${failedCommand}\` failed (exit ${failedResult.exitCode}):
70672
+ ${summary}`,
70673
+ `Verification (${failedType})`,
70674
+ `Deterministic verification failed (${failedType})`
70675
+ );
70676
+ return;
70677
+ }
70678
+ let fixSucceeded = false;
70679
+ for (let attempt = 1; attempt <= maxFixRetries; attempt++) {
70680
+ const fixed = await this.attemptExecutorVerificationFix(
70681
+ task,
70682
+ worktreePath,
70683
+ {
70684
+ command: failedCommand,
70685
+ exitCode: failedResult.exitCode,
70686
+ output: failureOutput,
70687
+ type: failedType
70688
+ },
70689
+ settings,
70690
+ attempt,
70691
+ maxFixRetries
70692
+ );
70693
+ if (fixed) {
70694
+ fixSucceeded = true;
70695
+ executorLog.log(`${task.id}: [verification] fix agent succeeded on attempt ${attempt}/${maxFixRetries}`);
70696
+ await this.store.logEntry(
70697
+ task.id,
70698
+ `[verification] Fix agent succeeded on attempt ${attempt}/${maxFixRetries}. Verification now passing.`,
70699
+ void 0,
70700
+ this.currentRunContext
70701
+ );
70702
+ break;
70703
+ }
70704
+ executorLog.log(`${task.id}: [verification] fix agent attempt ${attempt}/${maxFixRetries} failed`);
70705
+ await this.store.logEntry(
70706
+ task.id,
70707
+ `[verification] Fix agent attempt ${attempt}/${maxFixRetries} failed`,
70708
+ void 0,
70709
+ this.currentRunContext
70710
+ );
70711
+ }
70712
+ if (!fixSucceeded) {
70713
+ executorLog.log(`${task.id}: [verification] all fix attempts exhausted (${maxFixRetries}/${maxFixRetries}) \u2014 sending task back`);
70714
+ await this.sendTaskBackForFix(
70715
+ task,
70716
+ worktreePath,
70717
+ `${failedType} command \`${failedCommand}\` failed (exit ${failedResult.exitCode}) after ${maxFixRetries} fix attempts:
70718
+ ${summary}`,
70719
+ `Verification (${failedType})`,
70720
+ `Deterministic verification failed after ${maxFixRetries} fix attempts`
70721
+ );
70722
+ return;
70723
+ }
70724
+ }
70725
+ }
70726
+ }
70270
70727
  if (executionMode !== "fast") {
70271
70728
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
70272
70729
  if (workflowResult === "deferred-paused") {
@@ -70355,7 +70812,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70355
70812
  executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}: ${errorMessage}`);
70356
70813
  await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}): ${errorMessage}`, void 0, this.currentRunContext);
70357
70814
  }
70358
- if (worktreePath && existsSync28(worktreePath)) {
70815
+ if (worktreePath && existsSync29(worktreePath)) {
70359
70816
  try {
70360
70817
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
70361
70818
  await audit.git({ type: "worktree:remove", target: worktreePath });
@@ -70414,7 +70871,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70414
70871
  try {
70415
70872
  const latestTask = await this.store.getTask(task.id);
70416
70873
  await this.resetStepsIfWorkLost(latestTask);
70417
- if (worktreePath && existsSync28(worktreePath)) {
70874
+ if (worktreePath && existsSync29(worktreePath)) {
70418
70875
  try {
70419
70876
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
70420
70877
  } catch (wtErr) {
@@ -70526,7 +70983,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70526
70983
  const executorFallbackProvider = settings.fallbackProvider;
70527
70984
  const executorFallbackModelId = settings.fallbackModelId;
70528
70985
  const executorThinkingLevel = detail.thinkingLevel ?? settings.defaultThinkingLevel;
70529
- const isResuming = !!task.sessionFile && existsSync28(task.sessionFile);
70986
+ const isResuming = !!task.sessionFile && existsSync29(task.sessionFile);
70530
70987
  const sessionManager = isResuming ? SessionManager2.open(task.sessionFile) : SessionManager2.create(worktreePath);
70531
70988
  executorLog.log(`${task.id}: creating agent session (provider=${executorProvider ?? "default"}, model=${executorModelId ?? "default"}, resuming=${isResuming})`);
70532
70989
  const executorInstructions = await this.resolveInstructionsForRole("executor");
@@ -70556,7 +71013,13 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70556
71013
  ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
70557
71014
  taskId: task.id,
70558
71015
  taskTitle: detail.title,
70559
- onFallbackModelUsed: notifyFallbackUsed
71016
+ onFallbackModelUsed: createFallbackModelObserver({
71017
+ agent: "executor",
71018
+ label: "executor",
71019
+ store: this.store,
71020
+ taskId: task.id,
71021
+ taskTitle: detail.title
71022
+ })
70560
71023
  });
70561
71024
  if (isResuming) {
70562
71025
  executorLog.log(`${task.id}: resumed session from ${task.sessionFile}`);
@@ -70990,7 +71453,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70990
71453
  this.options.onComplete?.(task);
70991
71454
  } else {
70992
71455
  executorLog.log(`${task.id} paused \u2014 moving to todo`);
70993
- if (worktreePath && existsSync28(worktreePath)) {
71456
+ if (worktreePath && existsSync29(worktreePath)) {
70994
71457
  try {
70995
71458
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
70996
71459
  executorLog.log(`Removed old worktree for paused task: ${worktreePath}`);
@@ -71086,7 +71549,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
71086
71549
  executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}: ${errorMessage}`);
71087
71550
  await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}): ${errorMessage}`, void 0, this.currentRunContext);
71088
71551
  }
71089
- if (worktreePath && existsSync28(worktreePath)) {
71552
+ if (worktreePath && existsSync29(worktreePath)) {
71090
71553
  try {
71091
71554
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
71092
71555
  executorLog.log(`Removed old worktree for transient retry: ${worktreePath}`);
@@ -71141,7 +71604,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
71141
71604
  try {
71142
71605
  const latestTask = await this.store.getTask(task.id);
71143
71606
  await this.resetStepsIfWorkLost(latestTask);
71144
- if (worktreePath && existsSync28(worktreePath)) {
71607
+ if (worktreePath && existsSync29(worktreePath)) {
71145
71608
  try {
71146
71609
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
71147
71610
  executorLog.log(`Removed old worktree for stuck-killed retry: ${worktreePath}`);
@@ -71646,7 +72109,7 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
71646
72109
  * The section is replaced entirely to avoid accumulation of old feedback.
71647
72110
  */
71648
72111
  async injectWorkflowRevisionInstructions(task, feedback) {
71649
- const promptPath = join35(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
72112
+ const promptPath = join36(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
71650
72113
  let content;
71651
72114
  try {
71652
72115
  content = await readFile15(promptPath, "utf-8");
@@ -71700,6 +72163,217 @@ ${feedback}
71700
72163
  *
71701
72164
  * @returns true if a retry was scheduled, false if retries are exhausted
71702
72165
  */
72166
+ /**
72167
+ * Run deterministic verification (test + build commands) in the task's worktree.
72168
+ * Returns a structured result indicating whether all commands passed.
72169
+ */
72170
+ async runExecutorDeterministicVerification(task, worktreePath, settings) {
72171
+ const testCommand = settings.testCommand?.trim();
72172
+ const buildCommand2 = settings.buildCommand?.trim();
72173
+ if (!testCommand && !buildCommand2) {
72174
+ executorLog.log(`${task.id}: no test/build commands configured \u2014 skipping verification`);
72175
+ return { allPassed: true };
72176
+ }
72177
+ const parts = [];
72178
+ if (testCommand) parts.push(`test: ${testCommand}`);
72179
+ if (buildCommand2) parts.push(`build: ${buildCommand2}`);
72180
+ executorLog.log(`${task.id}: [verification] running deterministic verification (${parts.join(", ")})`);
72181
+ await this.store.logEntry(
72182
+ task.id,
72183
+ `[verification] Running deterministic verification (${parts.join(", ")})`,
72184
+ void 0,
72185
+ this.currentRunContext
72186
+ );
72187
+ const result = { allPassed: true };
72188
+ if (testCommand) {
72189
+ const testResult = await runVerificationCommand(
72190
+ this.store,
72191
+ worktreePath,
72192
+ task.id,
72193
+ testCommand,
72194
+ "test",
72195
+ void 0,
72196
+ executorLog,
72197
+ "executor"
72198
+ );
72199
+ result.testResult = testResult;
72200
+ if (!testResult.success) {
72201
+ result.allPassed = false;
72202
+ result.failedCommand = "testCommand";
72203
+ executorLog.log(`${task.id}: [verification] test failed (exit ${testResult.exitCode})`);
72204
+ return result;
72205
+ }
72206
+ }
72207
+ if (buildCommand2) {
72208
+ const buildResult = await runVerificationCommand(
72209
+ this.store,
72210
+ worktreePath,
72211
+ task.id,
72212
+ buildCommand2,
72213
+ "build",
72214
+ void 0,
72215
+ executorLog,
72216
+ "executor"
72217
+ );
72218
+ result.buildResult = buildResult;
72219
+ if (!buildResult.success) {
72220
+ result.allPassed = false;
72221
+ result.failedCommand = "buildCommand";
72222
+ executorLog.log(`${task.id}: [verification] build failed (exit ${buildResult.exitCode})`);
72223
+ return result;
72224
+ }
72225
+ }
72226
+ executorLog.log(`${task.id}: [verification] passed`);
72227
+ await this.store.logEntry(
72228
+ task.id,
72229
+ `[verification] Deterministic verification passed`,
72230
+ void 0,
72231
+ this.currentRunContext
72232
+ );
72233
+ return result;
72234
+ }
72235
+ /**
72236
+ * Attempt to fix verification failures by spawning a dedicated AI fix agent.
72237
+ * Follows the pattern established by the merger's attemptInMergeVerificationFix.
72238
+ * Returns true if verification passes after the fix attempt, false otherwise.
72239
+ */
72240
+ async attemptExecutorVerificationFix(task, worktreePath, failureContext, settings, retryNumber, maxRetries) {
72241
+ try {
72242
+ executorLog.log(`${task.id}: spawning executor verification fix agent (attempt ${retryNumber}/${maxRetries})`);
72243
+ const logger2 = new AgentLogger({
72244
+ store: this.store,
72245
+ taskId: task.id,
72246
+ agent: "executor",
72247
+ persistAgentToolOutput: settings.persistAgentToolOutput,
72248
+ onAgentText: this.options.onAgentText,
72249
+ onAgentTool: this.options.onAgentTool
72250
+ });
72251
+ let skillContext;
72252
+ if (this.options.agentStore) {
72253
+ try {
72254
+ skillContext = await buildSessionSkillContext({
72255
+ agentStore: this.options.agentStore,
72256
+ task,
72257
+ sessionPurpose: "executor",
72258
+ projectRootDir: worktreePath,
72259
+ pluginRunner: this.options.pluginRunner
72260
+ });
72261
+ } catch {
72262
+ }
72263
+ }
72264
+ const { provider: executorProvider, modelId: executorModelId } = resolveExecutorModelPair2(
72265
+ task.modelProvider,
72266
+ task.modelId,
72267
+ settings
72268
+ );
72269
+ const { session } = await createResolvedAgentSession({
72270
+ sessionPurpose: "executor",
72271
+ pluginRunner: this.options.pluginRunner,
72272
+ cwd: worktreePath,
72273
+ // Run in the task's worktree
72274
+ systemPrompt: `You are a verification fix agent running during task execution in a worktree.
72275
+
72276
+ 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.
72277
+
72278
+ ## Scope
72279
+ Only fix what is required to make the failing verification pass.
72280
+ Do not refactor, rename broadly, or make opportunistic improvements.
72281
+
72282
+ ## Rules
72283
+ 1. Read the error output carefully to understand what is failing before editing anything
72284
+ 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.
72285
+ 3. Make targeted fixes to the failing code path
72286
+ 4. After fixing, run the verification command to confirm the fix works
72287
+ 5. Do NOT make any git commits \u2014 just fix the code
72288
+ 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.
72289
+ 7. If you cannot fix the issue within scope, explain why and what evidence indicates a deeper/root problem`,
72290
+ tools: "coding",
72291
+ onText: logger2.onText,
72292
+ onThinking: logger2.onThinking,
72293
+ onToolStart: logger2.onToolStart,
72294
+ onToolEnd: logger2.onToolEnd,
72295
+ defaultProvider: executorProvider,
72296
+ defaultModelId: executorModelId,
72297
+ defaultThinkingLevel: settings.defaultThinkingLevel,
72298
+ ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
72299
+ });
72300
+ await this.store.logEntry(
72301
+ task.id,
72302
+ `Executor verification fix agent started (model: ${describeModel(session)}, attempt ${retryNumber}/${maxRetries})`,
72303
+ void 0,
72304
+ this.currentRunContext
72305
+ );
72306
+ await this.store.appendAgentLog(
72307
+ task.id,
72308
+ `Fix agent started (model: ${describeModel(session)}, attempt ${retryNumber}/${maxRetries})`,
72309
+ "text",
72310
+ void 0,
72311
+ "executor"
72312
+ );
72313
+ try {
72314
+ const fixPrompt = `Fix the failing ${failureContext.type} verification for task ${task.id}.
72315
+
72316
+ ## Failed command
72317
+ Command: \`${failureContext.command}\`
72318
+ Exit code: ${failureContext.exitCode}
72319
+
72320
+ ## Error output
72321
+ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
72322
+
72323
+ ## Instructions
72324
+ 1. Read the error output and identify the root cause
72325
+ 2. Make targeted fixes to resolve the failure
72326
+ 3. Run the verification command \`${failureContext.command}\` to confirm your fix works
72327
+ 4. If the fix doesn't work, try a different approach
72328
+ 5. Do NOT make any git commits`;
72329
+ await withRateLimitRetry(async () => {
72330
+ await promptWithFallback(session, fixPrompt);
72331
+ }, {
72332
+ onRetry: (attempt, delayMs, error) => {
72333
+ const delaySec = Math.round(delayMs / 1e3);
72334
+ executorLog.warn(`\u23F3 ${task.id} executor fix agent rate limited \u2014 retry ${attempt} in ${delaySec}s: ${error.message}`);
72335
+ }
72336
+ });
72337
+ await accumulateSessionTokenUsage(this.store, task.id, session);
72338
+ executorLog.log(`${task.id}: re-running deterministic verification after fix attempt ${retryNumber}/${maxRetries}`);
72339
+ await this.store.logEntry(
72340
+ task.id,
72341
+ `Re-running deterministic verification (attempt ${retryNumber}/${maxRetries})`,
72342
+ void 0,
72343
+ this.currentRunContext
72344
+ );
72345
+ await this.store.appendAgentLog(
72346
+ task.id,
72347
+ `Re-running verification (attempt ${retryNumber}/${maxRetries})`,
72348
+ "text",
72349
+ void 0,
72350
+ "executor"
72351
+ );
72352
+ const reRunResult = await this.runExecutorDeterministicVerification(task, worktreePath, settings);
72353
+ return reRunResult.allPassed;
72354
+ } finally {
72355
+ await logger2.flush();
72356
+ await session.dispose();
72357
+ }
72358
+ } catch (err) {
72359
+ const errorMessage = err instanceof Error ? err.message : String(err);
72360
+ executorLog.warn(`${task.id}: executor verification fix agent error: ${errorMessage}`);
72361
+ await this.store.logEntry(
72362
+ task.id,
72363
+ `Executor verification fix agent encountered an error`,
72364
+ errorMessage,
72365
+ this.currentRunContext
72366
+ );
72367
+ await this.store.appendAgentLog(
72368
+ task.id,
72369
+ "Fix agent encountered an error",
72370
+ "tool_error",
72371
+ errorMessage,
72372
+ "executor"
72373
+ );
72374
+ return false;
72375
+ }
72376
+ }
71703
72377
  async handleWorkflowStepFailure(task, worktreePath, failureFeedback, stepName) {
71704
72378
  this.clearCompletedTaskWatchdog(task.id);
71705
72379
  const currentRetries = task.workflowStepRetries ?? 0;
@@ -71769,7 +72443,7 @@ Please fix the issues so the verification can pass on the next attempt.`,
71769
72443
  * The section is replaced entirely to avoid accumulation of old feedback.
71770
72444
  */
71771
72445
  async injectWorkflowStepFailureInstructions(task, failureFeedback, stepName, retryCount) {
71772
- const promptPath = join35(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
72446
+ const promptPath = join36(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
71773
72447
  let content;
71774
72448
  try {
71775
72449
  content = await readFile15(promptPath, "utf-8");
@@ -71834,31 +72508,33 @@ ${failureFeedback}
71834
72508
  * Uses git diff against the stored baseCommitSha to determine what changed.
71835
72509
  * Returns an empty array if no changes or if git commands fail.
71836
72510
  */
72511
+ async resolveDiffBaseRef(worktreePath, baseCommitSha) {
72512
+ if (baseCommitSha) return baseCommitSha;
72513
+ try {
72514
+ const { stdout } = await execAsync5(
72515
+ "git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main",
72516
+ { cwd: worktreePath, encoding: "utf-8" }
72517
+ );
72518
+ const ref = stdout.trim();
72519
+ if (ref) return ref;
72520
+ } catch (mergeBaseErr) {
72521
+ const mergeBaseMsg = mergeBaseErr instanceof Error ? mergeBaseErr.message : String(mergeBaseErr);
72522
+ executorLog.warn(`Failed merge-base lookup for diff base in ${worktreePath}, trying HEAD~1 fallback: ${mergeBaseMsg}`);
72523
+ }
72524
+ try {
72525
+ const { stdout } = await execAsync5("git rev-parse HEAD~1", {
72526
+ cwd: worktreePath,
72527
+ encoding: "utf-8"
72528
+ });
72529
+ return stdout.trim() || void 0;
72530
+ } catch {
72531
+ executorLog.log(`Could not determine base commit for diff in ${worktreePath}`);
72532
+ return void 0;
72533
+ }
72534
+ }
71837
72535
  async captureModifiedFiles(worktreePath, baseCommitSha) {
71838
72536
  try {
71839
- let baseRef = baseCommitSha;
71840
- if (!baseRef) {
71841
- try {
71842
- const { stdout: stdout2 } = await execAsync5("git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main", {
71843
- cwd: worktreePath,
71844
- encoding: "utf-8"
71845
- });
71846
- baseRef = stdout2.trim();
71847
- } catch (mergeBaseErr) {
71848
- const mergeBaseMsg = mergeBaseErr instanceof Error ? mergeBaseErr.message : String(mergeBaseErr);
71849
- executorLog.warn(`Failed merge-base lookup for diff base in ${worktreePath}, trying HEAD~1 fallback: ${mergeBaseMsg}`);
71850
- try {
71851
- const { stdout: stdout2 } = await execAsync5("git rev-parse HEAD~1", {
71852
- cwd: worktreePath,
71853
- encoding: "utf-8"
71854
- });
71855
- baseRef = stdout2.trim();
71856
- } catch {
71857
- executorLog.log(`Could not determine base commit for diff in ${worktreePath}`);
71858
- return [];
71859
- }
71860
- }
71861
- }
72537
+ const baseRef = await this.resolveDiffBaseRef(worktreePath, baseCommitSha);
71862
72538
  if (!baseRef) {
71863
72539
  return [];
71864
72540
  }
@@ -72094,6 +72770,30 @@ ${failureFeedback}
72094
72770
  */
72095
72771
  async executeWorkflowStep(task, workflowStep, worktreePath, settings) {
72096
72772
  const toolMode = workflowStep.toolMode || "readonly";
72773
+ const scopedFiles = await this.captureModifiedFiles(worktreePath, task.baseCommitSha);
72774
+ let diffShortstat;
72775
+ try {
72776
+ const baseRef = await this.resolveDiffBaseRef(worktreePath, task.baseCommitSha);
72777
+ if (baseRef) {
72778
+ const { stdout } = await execAsync5(`git diff --shortstat ${baseRef}..HEAD`, {
72779
+ cwd: worktreePath,
72780
+ encoding: "utf-8"
72781
+ });
72782
+ diffShortstat = stdout.trim() || void 0;
72783
+ }
72784
+ } catch {
72785
+ }
72786
+ const MAX_SCOPE_FILES = 100;
72787
+ 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")}
72788
+ - ... (${scopedFiles.length - MAX_SCOPE_FILES} more files truncated)` : scopedFiles.map((f) => `- ${f}`).join("\n");
72789
+ const scopeBlock = `Diff Scope (files changed by THIS task vs base):
72790
+ ${scopeFileBlock}${diffShortstat ? `
72791
+ Diff stat: ${diffShortstat}` : ""}
72792
+
72793
+ CRITICAL SCOPING RULES \u2014 read before doing anything else:
72794
+ - Review ONLY the files listed above. Do NOT analyze unmodified files or unrelated parts of the codebase.
72795
+ - 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.
72796
+ - Your wall-clock budget is short. Spending it browsing unmodified files will cause this step to time out and block merge.`;
72097
72797
  const systemPrompt = `You are a workflow step agent executing: ${workflowStep.name}
72098
72798
 
72099
72799
  Task Context:
@@ -72101,6 +72801,8 @@ Task Context:
72101
72801
  - Task Description: ${task.description}
72102
72802
  - Worktree: ${worktreePath}
72103
72803
 
72804
+ ${scopeBlock}
72805
+
72104
72806
  Your role:
72105
72807
  - Execute this workflow step exactly as scoped.
72106
72808
  - Prioritize high-impact correctness/risk findings over stylistic nits.
@@ -72569,7 +73271,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
72569
73271
  * rather than fail the task permanently.
72570
73272
  */
72571
73273
  async resolveWorktreeStartPoint(startPoint, taskId) {
72572
- const command = isAbsolute11(startPoint) && existsSync28(startPoint) ? `git -C "${startPoint}" rev-parse --verify HEAD^{commit}` : `git rev-parse --verify "${startPoint}^{commit}"`;
73274
+ const command = isAbsolute11(startPoint) && existsSync29(startPoint) ? `git -C "${startPoint}" rev-parse --verify HEAD^{commit}` : `git rev-parse --verify "${startPoint}^{commit}"`;
72573
73275
  try {
72574
73276
  const { stdout } = await execAsync5(command, { cwd: this.rootDir });
72575
73277
  return stdout.trim() || startPoint;
@@ -72589,7 +73291,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
72589
73291
  */
72590
73292
  async tryCreateWorktree(branch, path5, taskId, startPoint, attemptNumber = 0, recoveryDepth = 0) {
72591
73293
  await this.assertWorktreePathNotNested(path5, taskId);
72592
- if (existsSync28(path5)) {
73294
+ if (existsSync29(path5)) {
72593
73295
  const isRegistered = await this.isRegisteredWorktree(path5);
72594
73296
  if (!isRegistered) {
72595
73297
  await this.store.logEntry(
@@ -72740,7 +73442,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
72740
73442
  );
72741
73443
  if (shouldGenerateNewName) {
72742
73444
  const conflictStartPoint = branch;
72743
- const newPath = join35(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
73445
+ const newPath = join36(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
72744
73446
  for (let suffix = 2; suffix <= 6; suffix++) {
72745
73447
  const suffixedBranch = `${branch}-${suffix}`;
72746
73448
  try {
@@ -73340,7 +74042,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
73340
74042
  metadata: { type: "spawned", parentTaskId: taskId }
73341
74043
  });
73342
74044
  const childWorktreeName = generateWorktreeName(this.rootDir);
73343
- const childWorktreePath = join35(this.rootDir, ".worktrees", childWorktreeName);
74045
+ const childWorktreePath = join36(this.rootDir, ".worktrees", childWorktreeName);
73344
74046
  const childBranch = `fusion/spawn-${agent.id}`;
73345
74047
  await this.createWorktree(childBranch, childWorktreePath, taskId, worktreePath);
73346
74048
  await this.options.agentStore.updateAgentState(agent.id, "active");
@@ -73529,9 +74231,9 @@ var init_node_routing_policy = __esm({
73529
74231
  });
73530
74232
 
73531
74233
  // ../engine/src/scheduler.ts
73532
- import { existsSync as existsSync29 } from "node:fs";
74234
+ import { existsSync as existsSync30 } from "node:fs";
73533
74235
  import { readFile as readFile16 } from "node:fs/promises";
73534
- import { basename as basename8, join as join36 } from "node:path";
74236
+ import { basename as basename8, join as join37 } from "node:path";
73535
74237
  function pathsOverlap2(a, b) {
73536
74238
  for (const pa of a) {
73537
74239
  const prefixA = pa.endsWith("/*") ? pa.slice(0, -1) : null;
@@ -73701,12 +74403,12 @@ var init_scheduler = __esm({
73701
74403
  * @returns Object with `valid: true` if checks pass, or `valid: false` with a `reason` string if they fail
73702
74404
  */
73703
74405
  async validateTaskFilesystem(id) {
73704
- const taskDir = join36(this.store.getTasksDir(), id);
73705
- if (!existsSync29(taskDir)) {
74406
+ const taskDir = join37(this.store.getTasksDir(), id);
74407
+ if (!existsSync30(taskDir)) {
73706
74408
  return { valid: false, reason: "missing directory" };
73707
74409
  }
73708
- const promptPath = join36(taskDir, "PROMPT.md");
73709
- if (!existsSync29(promptPath)) {
74410
+ const promptPath = join37(taskDir, "PROMPT.md");
74411
+ if (!existsSync30(promptPath)) {
73710
74412
  return { valid: false, reason: "missing or empty PROMPT.md" };
73711
74413
  }
73712
74414
  try {
@@ -73845,7 +74547,7 @@ var init_scheduler = __esm({
73845
74547
  break;
73846
74548
  }
73847
74549
  reservedNames.add(worktreeName);
73848
- return join36(this.store.getRootDir(), ".worktrees", worktreeName);
74550
+ return join37(this.store.getRootDir(), ".worktrees", worktreeName);
73849
74551
  }
73850
74552
  /**
73851
74553
  * Run one scheduling pass.
@@ -75122,7 +75824,7 @@ var init_mission_execution_loop = __esm({
75122
75824
  init_pi();
75123
75825
  init_agent_session_helpers();
75124
75826
  init_logger2();
75125
- init_notifier();
75827
+ init_fallback_model_observer();
75126
75828
  loopLog = createLogger2("mission-loop");
75127
75829
  VALIDATION_TIMEOUT_MS = 10 * 60 * 1e3;
75128
75830
  MissionExecutionLoop = class extends EventEmitter18 {
@@ -75356,7 +76058,13 @@ Assertions: ${assertions.map((a) => a.title).join(", ")}`,
75356
76058
  },
75357
76059
  taskId: task?.id,
75358
76060
  taskTitle: task?.title,
75359
- onFallbackModelUsed: notifyFallbackUsed
76061
+ onFallbackModelUsed: createFallbackModelObserver({
76062
+ agent: "reviewer",
76063
+ label: "mission validator",
76064
+ store: this.taskStore,
76065
+ taskId: task?.id,
76066
+ taskTitle: task?.title
76067
+ })
75360
76068
  });
75361
76069
  session = { session: sessionResult.session, sessionFile: sessionResult.sessionFile };
75362
76070
  loopLog.log(`Validation session created for feature ${feature.id}`);
@@ -77164,7 +77872,7 @@ not loop on the same plan across heartbeats without recording why.`;
77164
77872
  };
77165
77873
  }
77166
77874
  };
77167
- const { createResolvedAgentSession: createResolvedAgentSession2, extractRuntimeHint: extractRuntimeHint2 } = await Promise.resolve().then(() => (init_agent_session_helpers(), agent_session_helpers_exports));
77875
+ const { createResolvedAgentSession: createResolvedAgentSession3, extractRuntimeHint: extractRuntimeHint2, extractRuntimeModel: extractRuntimeModel2 } = await Promise.resolve().then(() => (init_agent_session_helpers(), agent_session_helpers_exports));
77168
77876
  const { buildSessionSkillContextSync: buildSessionSkillContextSync2 } = await Promise.resolve().then(() => (init_session_skill_context(), session_skill_context_exports));
77169
77877
  let heartbeatTools;
77170
77878
  if (isNoTaskRun) {
@@ -77235,7 +77943,7 @@ not loop on the same plan across heartbeats without recording why.`;
77235
77943
  persistAgentToolOutput: memorySettings?.persistAgentToolOutput
77236
77944
  });
77237
77945
  }
77238
- const { session } = await createResolvedAgentSession2({
77946
+ const { session } = await createResolvedAgentSession3({
77239
77947
  sessionPurpose: "heartbeat",
77240
77948
  runtimeHint: extractRuntimeHint2(agent.runtimeConfig),
77241
77949
  pluginRunner: this.pluginRunner,
@@ -77243,8 +77951,10 @@ not loop on the same plan across heartbeats without recording why.`;
77243
77951
  systemPrompt,
77244
77952
  tools: "readonly",
77245
77953
  customTools: heartbeatTools,
77246
- defaultProvider: agent.runtimeConfig?.modelProvider,
77247
- defaultModelId: agent.runtimeConfig?.modelId,
77954
+ ...(() => {
77955
+ const { provider, modelId } = extractRuntimeModel2(agent.runtimeConfig);
77956
+ return { defaultProvider: provider, defaultModelId: modelId };
77957
+ })(),
77248
77958
  onText: (delta) => {
77249
77959
  outputLength += delta.length;
77250
77960
  appendStdoutExcerpt(delta);
@@ -78619,7 +79329,7 @@ async function createAiPromptExecutor(cwd) {
78619
79329
  }
78620
79330
  };
78621
79331
  }
78622
- function truncateOutput(stdout, stderr) {
79332
+ function truncateOutput2(stdout, stderr) {
78623
79333
  const out = stdout ?? "";
78624
79334
  const err = stderr ?? "";
78625
79335
  let combined = out;
@@ -78799,7 +79509,7 @@ var init_cron_runner = __esm({
78799
79509
  maxBuffer: MAX_BUFFER,
78800
79510
  shell: defaultShell
78801
79511
  });
78802
- const output = truncateOutput(stdout, stderr);
79512
+ const output = truncateOutput2(stdout, stderr);
78803
79513
  log15.log(`\u2713 ${schedule.name} completed (${output.length} bytes output)`);
78804
79514
  return {
78805
79515
  success: true,
@@ -78810,7 +79520,7 @@ var init_cron_runner = __esm({
78810
79520
  } catch (err) {
78811
79521
  const stdout = err.stdout ?? "";
78812
79522
  const stderr = err.stderr ?? "";
78813
- const output = truncateOutput(stdout, stderr);
79523
+ const output = truncateOutput2(stdout, stderr);
78814
79524
  const errorMessage = err.killed ? `Command timed out after ${(schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS6) / 1e3}s` : err.message ?? String(err);
78815
79525
  log15.warn(`\u2717 ${schedule.name} failed: ${errorMessage}`);
78816
79526
  return {
@@ -78855,7 +79565,7 @@ var init_cron_runner = __esm({
78855
79565
  const result = await runBackupCommand2(fusionDir, settings);
78856
79566
  return {
78857
79567
  success: result.success,
78858
- output: truncateOutput(result.output ?? "", ""),
79568
+ output: truncateOutput2(result.output ?? "", ""),
78859
79569
  error: result.success ? void 0 : result.output
78860
79570
  };
78861
79571
  } catch (err) {
@@ -78896,7 +79606,7 @@ var init_cron_runner = __esm({
78896
79606
  if (sr.output) outputParts.push(sr.output);
78897
79607
  if (sr.error) outputParts.push(`Error: ${sr.error}`);
78898
79608
  }
78899
- const output = truncateOutput(outputParts.join("\n"), "");
79609
+ const output = truncateOutput2(outputParts.join("\n"), "");
78900
79610
  const failedSteps = stepResults.filter((sr) => !sr.success);
78901
79611
  const error = failedSteps.length > 0 ? `${failedSteps.length} step(s) failed: ${failedSteps.map((s) => s.stepName).join(", ")}${stoppedEarly ? " (execution stopped)" : ""}` : void 0;
78902
79612
  const status = overallSuccess ? "\u2713" : "\u2717";
@@ -78975,7 +79685,7 @@ var init_cron_runner = __esm({
78975
79685
  stepName: step.name,
78976
79686
  stepIndex,
78977
79687
  success: true,
78978
- output: truncateOutput(stdout, stderr),
79688
+ output: truncateOutput2(stdout, stderr),
78979
79689
  startedAt,
78980
79690
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
78981
79691
  };
@@ -78988,7 +79698,7 @@ var init_cron_runner = __esm({
78988
79698
  stepName: step.name,
78989
79699
  stepIndex,
78990
79700
  success: false,
78991
- output: truncateOutput(stdout, stderr),
79701
+ output: truncateOutput2(stdout, stderr),
78992
79702
  error: errorMessage,
78993
79703
  startedAt,
78994
79704
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -79138,7 +79848,7 @@ var init_cron_runner = __esm({
79138
79848
  // ../engine/src/routine-runner.ts
79139
79849
  import { exec as exec8 } from "node:child_process";
79140
79850
  import { promisify as promisify8 } from "node:util";
79141
- function truncateOutput2(stdout, stderr) {
79851
+ function truncateOutput3(stdout, stderr) {
79142
79852
  let output = stdout;
79143
79853
  if (stderr) {
79144
79854
  output += stdout ? "\n--- stderr ---\n" : "";
@@ -79321,7 +80031,7 @@ var init_routine_runner = __esm({
79321
80031
  const result = await runBackupCommand2(fusionDir, settings);
79322
80032
  return {
79323
80033
  success: result.success,
79324
- output: truncateOutput2(result.output ?? "", ""),
80034
+ output: truncateOutput3(result.output ?? "", ""),
79325
80035
  error: result.success ? void 0 : result.output,
79326
80036
  startedAt,
79327
80037
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -79345,7 +80055,7 @@ var init_routine_runner = __esm({
79345
80055
  });
79346
80056
  return {
79347
80057
  success: true,
79348
- output: truncateOutput2(stdout, stderr),
80058
+ output: truncateOutput3(stdout, stderr),
79349
80059
  startedAt,
79350
80060
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
79351
80061
  };
@@ -79356,7 +80066,7 @@ var init_routine_runner = __esm({
79356
80066
  const error = errObj.killed === true ? `Command timed out after ${(timeoutMs ?? DEFAULT_TIMEOUT_MS7) / 1e3}s` : (err instanceof Error ? err.message : null) ?? String(err);
79357
80067
  return {
79358
80068
  success: false,
79359
- output: truncateOutput2(stdout, stderr),
80069
+ output: truncateOutput3(stdout, stderr),
79360
80070
  error,
79361
80071
  startedAt,
79362
80072
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -79389,7 +80099,7 @@ var init_routine_runner = __esm({
79389
80099
  const failedSteps = stepResults.filter((sr) => !sr.success);
79390
80100
  return {
79391
80101
  success: overallSuccess,
79392
- output: truncateOutput2(outputParts.join("\n"), ""),
80102
+ output: truncateOutput3(outputParts.join("\n"), ""),
79393
80103
  error: failedSteps.length > 0 ? `${failedSteps.length} step(s) failed: ${failedSteps.map((s) => s.stepName).join(", ")}${stoppedEarly ? " (execution stopped)" : ""}` : void 0,
79394
80104
  startedAt,
79395
80105
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -79424,7 +80134,7 @@ var init_routine_runner = __esm({
79424
80134
  this.options.aiPromptExecutor(step.prompt, step.modelProvider, step.modelId),
79425
80135
  new Promise((_resolve, reject2) => setTimeout(() => reject2(new Error(`AI prompt step timed out after ${timeoutMs / 1e3}s`)), timeoutMs))
79426
80136
  ]);
79427
- return { stepId: step.id, stepName: step.name, stepIndex, success: true, output: truncateOutput2(output, ""), startedAt, completedAt: (/* @__PURE__ */ new Date()).toISOString() };
80137
+ return { stepId: step.id, stepName: step.name, stepIndex, success: true, output: truncateOutput3(output, ""), startedAt, completedAt: (/* @__PURE__ */ new Date()).toISOString() };
79428
80138
  } catch (err) {
79429
80139
  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() };
79430
80140
  }
@@ -80038,8 +80748,8 @@ var init_stuck_task_detector = __esm({
80038
80748
  // ../engine/src/self-healing.ts
80039
80749
  import { exec as exec9 } from "node:child_process";
80040
80750
  import { promisify as promisify9 } from "node:util";
80041
- import { existsSync as existsSync30, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
80042
- import { isAbsolute as isAbsolute13, join as join37, relative as relative8, resolve as resolve17 } from "node:path";
80751
+ import { existsSync as existsSync31, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
80752
+ import { isAbsolute as isAbsolute13, join as join38, relative as relative8, resolve as resolve17 } from "node:path";
80043
80753
  function shellQuote(value) {
80044
80754
  return `'${value.replace(/'/g, "'\\''")}'`;
80045
80755
  }
@@ -80435,7 +81145,7 @@ var init_self_healing = __esm({
80435
81145
  return commit;
80436
81146
  }
80437
81147
  async cleanupInterruptedMergeArtifacts(task) {
80438
- if (task.worktree && existsSync30(task.worktree)) {
81148
+ if (task.worktree && existsSync31(task.worktree)) {
80439
81149
  try {
80440
81150
  await execAsync7(`git worktree remove ${shellQuote(task.worktree)} --force`, {
80441
81151
  cwd: this.options.rootDir,
@@ -81056,7 +81766,7 @@ var init_self_healing = __esm({
81056
81766
  return false;
81057
81767
  }
81058
81768
  const staleness = now - new Date(t.updatedAt).getTime();
81059
- const hasWorktree = t.worktree && existsSync30(t.worktree);
81769
+ const hasWorktree = t.worktree && existsSync31(t.worktree);
81060
81770
  const graceMs = hasWorktree ? ORPHANED_WITH_WORKTREE_GRACE_MS : ORPHANED_EXECUTION_RECOVERY_GRACE_MS;
81061
81771
  return staleness >= graceMs;
81062
81772
  });
@@ -81065,7 +81775,7 @@ var init_self_healing = __esm({
81065
81775
  let recovered = 0;
81066
81776
  for (const task of orphaned) {
81067
81777
  try {
81068
- const hadWorktree = task.worktree && existsSync30(task.worktree);
81778
+ const hadWorktree = task.worktree && existsSync31(task.worktree);
81069
81779
  const reason = hadWorktree ? "worktree exists but no active session" : "missing worktree/session";
81070
81780
  await this.resetStepsIfWorkLost(task);
81071
81781
  await this.store.updateTask(task.id, {
@@ -81211,7 +81921,7 @@ var init_self_healing = __esm({
81211
81921
  }
81212
81922
  }
81213
81923
  async hasRecoverableGitWork(task) {
81214
- if (task.worktree && existsSync30(task.worktree)) {
81924
+ if (task.worktree && existsSync31(task.worktree)) {
81215
81925
  try {
81216
81926
  const { stdout: status } = await execAsync7("git status --porcelain", {
81217
81927
  cwd: task.worktree,
@@ -81396,11 +82106,11 @@ var init_self_healing = __esm({
81396
82106
  * tracks registered idle worktrees, never these orphans.
81397
82107
  */
81398
82108
  async reapUnregisteredOrphans() {
81399
- const worktreesDir = join37(this.options.rootDir, ".worktrees");
81400
- if (!existsSync30(worktreesDir)) return 0;
82109
+ const worktreesDir = join38(this.options.rootDir, ".worktrees");
82110
+ if (!existsSync31(worktreesDir)) return 0;
81401
82111
  let dirs;
81402
82112
  try {
81403
- dirs = readdirSync5(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join37(worktreesDir, e.name));
82113
+ dirs = readdirSync5(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join38(worktreesDir, e.name));
81404
82114
  } catch (err) {
81405
82115
  log17.warn(`Failed to read .worktrees/ for unregistered orphan reap: ${err instanceof Error ? err.message : String(err)}`);
81406
82116
  return 0;
@@ -81505,8 +82215,8 @@ var init_self_healing = __esm({
81505
82215
  }
81506
82216
  /** Remove oldest idle worktrees if total count exceeds 2× maxWorktrees. */
81507
82217
  async enforceWorktreeCap() {
81508
- const worktreesDir = join37(this.options.rootDir, ".worktrees");
81509
- if (!existsSync30(worktreesDir)) return;
82218
+ const worktreesDir = join38(this.options.rootDir, ".worktrees");
82219
+ if (!existsSync31(worktreesDir)) return;
81510
82220
  try {
81511
82221
  const settings = await this.store.getSettings();
81512
82222
  const cap = (settings.maxWorktrees ?? 4) * 2;
@@ -83344,7 +84054,7 @@ var init_ipc_host = __esm({
83344
84054
  import { EventEmitter as EventEmitter21 } from "node:events";
83345
84055
  import { fork } from "node:child_process";
83346
84056
  import { fileURLToPath as fileURLToPath3 } from "node:url";
83347
- import { dirname as dirname11, join as join38 } from "node:path";
84057
+ import { dirname as dirname11, join as join39 } from "node:path";
83348
84058
  var HealthMonitor, ChildProcessRuntime;
83349
84059
  var init_child_process_runtime = __esm({
83350
84060
  "../engine/src/runtimes/child-process-runtime.ts"() {
@@ -83506,7 +84216,7 @@ var init_child_process_runtime = __esm({
83506
84216
  const isCompiled = !import.meta.url.endsWith(".ts");
83507
84217
  const currentDir = dirname11(fileURLToPath3(import.meta.url));
83508
84218
  const workerFile = isCompiled ? "child-process-worker.js" : "child-process-worker.ts";
83509
- return join38(currentDir, workerFile);
84219
+ return join39(currentDir, workerFile);
83510
84220
  }
83511
84221
  /**
83512
84222
  * Set up event forwarding from IPC host to runtime listeners.
@@ -87759,6 +88469,8 @@ __export(src_exports2, {
87759
88469
  describeAgentModel: () => describeAgentModel,
87760
88470
  describeModel: () => describeModel,
87761
88471
  ensureDefaultHeartbeatProcedureFile: () => ensureDefaultHeartbeatProcedureFile,
88472
+ extractRuntimeHint: () => extractRuntimeHint,
88473
+ extractRuntimeModel: () => extractRuntimeModel,
87762
88474
  formatTaskIdentifier: () => formatTaskIdentifier,
87763
88475
  getDefaultPiRuntime: () => getDefaultPiRuntime,
87764
88476
  getHostExtensionPaths: () => getHostExtensionPaths,
@@ -92103,7 +92815,7 @@ var init_api_error = __esm({
92103
92815
  // ../dashboard/src/plugin-routes.ts
92104
92816
  import { Router } from "express";
92105
92817
  import { access as access5, stat as stat6, readFile as readFile18 } from "node:fs/promises";
92106
- import { join as join39, isAbsolute as isAbsolute14, dirname as dirname12, basename as basename9 } from "node:path";
92818
+ import { join as join40, isAbsolute as isAbsolute14, dirname as dirname12, basename as basename9 } from "node:path";
92107
92819
  async function resolvePluginManifest(sourcePath) {
92108
92820
  try {
92109
92821
  await access5(sourcePath);
@@ -92119,7 +92831,7 @@ async function resolvePluginManifest(sourcePath) {
92119
92831
  if (!sourceStat.isDirectory()) {
92120
92832
  throw badRequest(`Path is not a directory: ${sourcePath}`);
92121
92833
  }
92122
- const directManifestPath = join39(sourcePath, "manifest.json");
92834
+ const directManifestPath = join40(sourcePath, "manifest.json");
92123
92835
  try {
92124
92836
  await access5(directManifestPath);
92125
92837
  const manifest = await readAndValidateManifest(directManifestPath);
@@ -92130,7 +92842,7 @@ async function resolvePluginManifest(sourcePath) {
92130
92842
  const dirName = basename9(sourcePath).toLowerCase();
92131
92843
  if (DIST_DIR_NAMES.has(dirName)) {
92132
92844
  const parentDir = dirname12(sourcePath);
92133
- const parentManifestPath = join39(parentDir, "manifest.json");
92845
+ const parentManifestPath = join40(parentDir, "manifest.json");
92134
92846
  try {
92135
92847
  await access5(parentManifestPath);
92136
92848
  const manifest = await readAndValidateManifest(parentManifestPath);
@@ -93601,8 +94313,8 @@ function registerTaskWorkflowRoutes(ctx, deps) {
93601
94313
  });
93602
94314
  if (retrySpecification) {
93603
94315
  const { rm: rm6 } = await import("node:fs/promises");
93604
- const { join: join71 } = await import("node:path");
93605
- const promptPath = join71(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
94316
+ const { join: join72 } = await import("node:path");
94317
+ const promptPath = join72(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
93606
94318
  await rm6(promptPath, { force: true });
93607
94319
  await scopedStore.logEntry(req.params.id, "Retry requested from dashboard (planning retry budget reset)");
93608
94320
  const updated2 = await scopedStore.getTask(req.params.id);
@@ -94067,8 +94779,8 @@ function registerTaskWorkflowRoutes(ctx, deps) {
94067
94779
  await scopedStore.logEntry(task.id, "Plan rejected by user", "Specification will be regenerated");
94068
94780
  await scopedStore.updateTask(task.id, { status: void 0 });
94069
94781
  const { rm: rm6 } = await import("node:fs/promises");
94070
- const { join: join71 } = await import("node:path");
94071
- const promptPath = join71(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
94782
+ const { join: join72 } = await import("node:path");
94783
+ const promptPath = join72(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
94072
94784
  await rm6(promptPath, { force: true });
94073
94785
  const updated = await scopedStore.getTask(task.id);
94074
94786
  res.json(updated);
@@ -94338,8 +95050,8 @@ function registerTaskWorkflowRoutes(ctx, deps) {
94338
95050
  if (task.column === "triage") {
94339
95051
  await scopedStore.logEntry(task.id, "AI spec revision requested", feedback);
94340
95052
  const { rm: rm7 } = await import("node:fs/promises");
94341
- const { join: join72 } = await import("node:path");
94342
- const promptPath2 = join72(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
95053
+ const { join: join73 } = await import("node:path");
95054
+ const promptPath2 = join73(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
94343
95055
  await rm7(promptPath2, { force: true });
94344
95056
  await scopedStore.updateTask(task.id, { status: "needs-replan" });
94345
95057
  const updated2 = await scopedStore.getTask(task.id);
@@ -94355,8 +95067,8 @@ function registerTaskWorkflowRoutes(ctx, deps) {
94355
95067
  await scopedStore.logEntry(task.id, "AI spec revision requested", feedback);
94356
95068
  const updated = await scopedStore.moveTask(task.id, "triage");
94357
95069
  const { rm: rm6 } = await import("node:fs/promises");
94358
- const { join: join71 } = await import("node:path");
94359
- const promptPath = join71(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
95070
+ const { join: join72 } = await import("node:path");
95071
+ const promptPath = join72(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
94360
95072
  await rm6(promptPath, { force: true });
94361
95073
  await scopedStore.updateTask(task.id, { status: "needs-replan" });
94362
95074
  res.json(updated);
@@ -94376,8 +95088,8 @@ function registerTaskWorkflowRoutes(ctx, deps) {
94376
95088
  if (task.column === "triage") {
94377
95089
  await scopedStore.logEntry(task.id, "Specification rebuild requested by user");
94378
95090
  const { rm: rm7 } = await import("node:fs/promises");
94379
- const { join: join72 } = await import("node:path");
94380
- const promptPath2 = join72(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
95091
+ const { join: join73 } = await import("node:path");
95092
+ const promptPath2 = join73(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
94381
95093
  await rm7(promptPath2, { force: true });
94382
95094
  await scopedStore.updateTask(task.id, { status: "needs-replan" });
94383
95095
  const updated2 = await scopedStore.getTask(task.id);
@@ -94391,8 +95103,8 @@ function registerTaskWorkflowRoutes(ctx, deps) {
94391
95103
  await scopedStore.logEntry(task.id, "Specification rebuild requested by user");
94392
95104
  const updated = await scopedStore.moveTask(task.id, "triage");
94393
95105
  const { rm: rm6 } = await import("node:fs/promises");
94394
- const { join: join71 } = await import("node:path");
94395
- const promptPath = join71(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
95106
+ const { join: join72 } = await import("node:path");
95107
+ const promptPath = join72(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
94396
95108
  await rm6(promptPath, { force: true });
94397
95109
  await scopedStore.updateTask(task.id, { status: "needs-replan" });
94398
95110
  res.json(updated);
@@ -95773,14 +96485,15 @@ __export(chat_exports, {
95773
96485
  __setBuildAgentChatPrompt: () => __setBuildAgentChatPrompt,
95774
96486
  __setChatDiagnostics: () => __setChatDiagnostics,
95775
96487
  __setCreateFnAgent: () => __setCreateFnAgent2,
96488
+ __setCreateResolvedAgentSession: () => __setCreateResolvedAgentSession,
95776
96489
  chatStreamManager: () => chatStreamManager,
95777
96490
  checkRateLimit: () => checkRateLimit5,
95778
96491
  getRateLimitResetTime: () => getRateLimitResetTime5,
95779
96492
  resolveFileReferences: () => resolveFileReferences
95780
96493
  });
95781
96494
  import { EventEmitter as EventEmitter29 } from "node:events";
95782
- import { existsSync as existsSync31 } from "node:fs";
95783
- import { join as join40, resolve as resolve19, relative as relative9 } from "node:path";
96495
+ import { existsSync as existsSync32 } from "node:fs";
96496
+ import { join as join41, resolve as resolve19, relative as relative9 } from "node:path";
95784
96497
  import { SessionManager as SessionManager3 } from "@mariozechner/pi-coding-agent";
95785
96498
  function __getChatDiagnostics() {
95786
96499
  return _diagnostics;
@@ -95810,7 +96523,7 @@ function validateFilePath(basePath, filePath) {
95810
96523
  throw new Error(`Access denied: Absolute paths not allowed`);
95811
96524
  }
95812
96525
  const resolvedBase = resolve19(basePath);
95813
- const resolvedPath = resolve19(join40(resolvedBase, decodedPath));
96526
+ const resolvedPath = resolve19(join41(resolvedBase, decodedPath));
95814
96527
  const relativePath = relative9(resolvedBase, resolvedPath);
95815
96528
  if (relativePath.startsWith("..") || relativePath.startsWith("../") || relativePath === "..") {
95816
96529
  throw new Error(`Access denied: Path traversal detected`);
@@ -95887,6 +96600,9 @@ function getRateLimitResetTime5(ip) {
95887
96600
  function __setCreateFnAgent2(mock) {
95888
96601
  createFnAgent8 = mock;
95889
96602
  }
96603
+ function __setCreateResolvedAgentSession(mock) {
96604
+ createResolvedAgentSession2 = mock;
96605
+ }
95890
96606
  function __setBuildAgentChatPrompt(mock) {
95891
96607
  buildAgentChatPromptFn = mock;
95892
96608
  }
@@ -95894,9 +96610,11 @@ function __resetChatState() {
95894
96610
  chatStreamManager.reset();
95895
96611
  rateLimits5.clear();
95896
96612
  buildAgentChatPromptFn = void 0;
96613
+ createFnAgent8 = createFnAgent2;
96614
+ createResolvedAgentSession2 = createResolvedAgentSession;
95897
96615
  __setChatDiagnostics(null);
95898
96616
  }
95899
- var createFnAgent8, buildAgentChatPromptFn, defaultDiagnostics, _diagnostics, diagnostics6, CHAT_SYSTEM_PROMPT, RATE_LIMIT_WINDOW_MS5, MAX_MESSAGES_PER_IP_PER_MINUTE, MAX_REFERENCED_FILE_SIZE, rateLimits5, ChatStreamManager, chatStreamManager, ChatManager;
96617
+ var createFnAgent8, createResolvedAgentSession2, buildAgentChatPromptFn, defaultDiagnostics, _diagnostics, diagnostics6, CHAT_SYSTEM_PROMPT, RATE_LIMIT_WINDOW_MS5, MAX_MESSAGES_PER_IP_PER_MINUTE, MAX_REFERENCED_FILE_SIZE, rateLimits5, ChatStreamManager, chatStreamManager, ChatManager;
95900
96618
  var init_chat = __esm({
95901
96619
  "../dashboard/src/chat.ts"() {
95902
96620
  "use strict";
@@ -95905,6 +96623,7 @@ var init_chat = __esm({
95905
96623
  init_src2();
95906
96624
  init_src2();
95907
96625
  createFnAgent8 = createFnAgent2;
96626
+ createResolvedAgentSession2 = createResolvedAgentSession;
95908
96627
  defaultDiagnostics = {
95909
96628
  log(message, ...args) {
95910
96629
  console.log(`[chat] ${message}`, ...args);
@@ -96023,13 +96742,49 @@ var init_chat = __esm({
96023
96742
  };
96024
96743
  chatStreamManager = new ChatStreamManager();
96025
96744
  ChatManager = class {
96026
- constructor(chatStore, rootDir, agentStore) {
96745
+ constructor(chatStore, rootDir, agentStore, pluginRunner, getSettings) {
96027
96746
  this.chatStore = chatStore;
96028
96747
  this.rootDir = rootDir;
96029
96748
  this.agentStore = agentStore;
96749
+ this.pluginRunner = pluginRunner;
96750
+ this.getSettings = getSettings;
96030
96751
  }
96031
96752
  agentStoreReady;
96032
96753
  activeGenerations = /* @__PURE__ */ new Map();
96754
+ async getChatModelSettings() {
96755
+ if (!this.getSettings) {
96756
+ return {};
96757
+ }
96758
+ try {
96759
+ const settings = await this.getSettings();
96760
+ return {
96761
+ fallbackProvider: settings?.fallbackProvider ?? void 0,
96762
+ fallbackModelId: settings?.fallbackModelId ?? void 0,
96763
+ defaultProvider: settings?.defaultProvider ?? void 0,
96764
+ defaultModelId: settings?.defaultModelId ?? void 0
96765
+ };
96766
+ } catch (err) {
96767
+ const message = err instanceof Error ? err.message : String(err);
96768
+ diagnostics6.warn(`Failed to load chat fallback settings: ${message}`);
96769
+ return {};
96770
+ }
96771
+ }
96772
+ handleFallbackModelUsed(sessionId, payload) {
96773
+ const slashIndex = payload.fallbackModel.indexOf("/");
96774
+ if (slashIndex > 0 && slashIndex < payload.fallbackModel.length - 1) {
96775
+ this.chatStore.updateSession(sessionId, {
96776
+ modelProvider: payload.fallbackModel.slice(0, slashIndex),
96777
+ modelId: payload.fallbackModel.slice(slashIndex + 1)
96778
+ });
96779
+ }
96780
+ diagnostics6.warn(
96781
+ `[fallback] chat ${sessionId} switched from ${payload.primaryModel} to ${payload.fallbackModel} (${payload.triggerPoint})`
96782
+ );
96783
+ chatStreamManager.broadcast(sessionId, {
96784
+ type: "fallback",
96785
+ data: payload
96786
+ });
96787
+ }
96033
96788
  /**
96034
96789
  * Resolve the per-chat pi/Claude CLI SessionManager.
96035
96790
  *
@@ -96048,7 +96803,7 @@ var init_chat = __esm({
96048
96803
  * keep the CLI session stable across user messages.
96049
96804
  */
96050
96805
  resolveCliSessionManager(session) {
96051
- if (session.cliSessionFile && existsSync31(session.cliSessionFile)) {
96806
+ if (session.cliSessionFile && existsSync32(session.cliSessionFile)) {
96052
96807
  try {
96053
96808
  return SessionManager3.open(session.cliSessionFile);
96054
96809
  } catch (err) {
@@ -96182,6 +96937,7 @@ var init_chat = __esm({
96182
96937
  let accumulatedText = "";
96183
96938
  const toolCallsAccum = [];
96184
96939
  const pendingToolStarts = /* @__PURE__ */ new Map();
96940
+ let fallbackInfo;
96185
96941
  try {
96186
96942
  if (!session) {
96187
96943
  chatStreamManager.broadcast(sessionId, {
@@ -96207,30 +96963,12 @@ var init_chat = __esm({
96207
96963
  });
96208
96964
  return;
96209
96965
  }
96210
- const effectiveModelProvider = modelProvider ?? session.modelProvider ?? void 0;
96211
- const effectiveModelId = modelId ?? session.modelId ?? void 0;
96966
+ const requestedModelProvider = modelProvider ?? session.modelProvider ?? void 0;
96967
+ const requestedModelId = modelId ?? session.modelId ?? void 0;
96968
+ let effectiveModelProvider = requestedModelProvider;
96969
+ let effectiveModelId = requestedModelId;
96970
+ let hasExplicitAgentRuntimeModel = false;
96212
96971
  const needsTitle = session.title === null || session.title === void 0 || session.title.trim() === "";
96213
- if (needsTitle) {
96214
- (async () => {
96215
- try {
96216
- const generated = await summarizeTitle(
96217
- content.trim(),
96218
- this.rootDir,
96219
- effectiveModelProvider,
96220
- effectiveModelId
96221
- );
96222
- const title = generated ?? content.trim().slice(0, 60).trim();
96223
- if (title) {
96224
- this.chatStore.updateSession(sessionId, { title });
96225
- }
96226
- } catch {
96227
- const fallback2 = content.trim().slice(0, 60).trim();
96228
- if (fallback2) {
96229
- this.chatStore.updateSession(sessionId, { title: fallback2 });
96230
- }
96231
- }
96232
- })();
96233
- }
96234
96972
  await ensureEngineReady5();
96235
96973
  if (!createFnAgent8) {
96236
96974
  throw new Error("AI agent not available");
@@ -96261,6 +96999,35 @@ var init_chat = __esm({
96261
96999
  diagnostics6.warn(`Failed to build enriched system prompt for ${agent.id}: ${message}`);
96262
97000
  }
96263
97001
  }
97002
+ if (agent) {
97003
+ const runtimeModel = extractRuntimeModel(agent.runtimeConfig);
97004
+ if (runtimeModel.provider && runtimeModel.modelId) {
97005
+ hasExplicitAgentRuntimeModel = true;
97006
+ }
97007
+ effectiveModelProvider ??= runtimeModel.provider;
97008
+ effectiveModelId ??= runtimeModel.modelId;
97009
+ }
97010
+ if (needsTitle) {
97011
+ (async () => {
97012
+ try {
97013
+ const generated = await summarizeTitle(
97014
+ content.trim(),
97015
+ this.rootDir,
97016
+ effectiveModelProvider,
97017
+ effectiveModelId
97018
+ );
97019
+ const title = generated ?? content.trim().slice(0, 60).trim();
97020
+ if (title) {
97021
+ this.chatStore.updateSession(sessionId, { title });
97022
+ }
97023
+ } catch {
97024
+ const fallback2 = content.trim().slice(0, 60).trim();
97025
+ if (fallback2) {
97026
+ this.chatStore.updateSession(sessionId, { title: fallback2 });
97027
+ }
97028
+ }
97029
+ })();
97030
+ }
96264
97031
  if (mentions.length > 0) {
96265
97032
  const mentionContext = await this.buildMentionContext(mentions, mentionAgents);
96266
97033
  if (mentionContext) {
@@ -96273,7 +97040,10 @@ ${mentionContext}`;
96273
97040
  const attachmentSummary = attachments && attachments.length > 0 ? `[User attached: ${attachments.map((attachment) => `${attachment.originalName} (${attachment.mimeType}, ${formatAttachmentSize(attachment.size)})`).join(", ")}]` : "";
96274
97041
  const promptContent = [attachmentSummary, resolvedContent].filter(Boolean).join("\n\n");
96275
97042
  const sessionManager = this.resolveCliSessionManager(session);
96276
- agentResult = await createFnAgent8({
97043
+ const chatModelSettings = await this.getChatModelSettings();
97044
+ const usesConfiguredDefaultModel = requestedModelProvider === chatModelSettings.defaultProvider && requestedModelId === chatModelSettings.defaultModelId && !!requestedModelProvider && !!requestedModelId;
97045
+ const allowFallback = !hasExplicitAgentRuntimeModel && (!(requestedModelProvider && requestedModelId) || usesConfiguredDefaultModel);
97046
+ const sessionOptions = {
96277
97047
  cwd: this.rootDir,
96278
97048
  systemPrompt,
96279
97049
  tools: "coding",
@@ -96282,6 +97052,14 @@ ${mentionContext}`;
96282
97052
  defaultProvider: effectiveModelProvider,
96283
97053
  defaultModelId: effectiveModelId
96284
97054
  } : {},
97055
+ ...allowFallback && chatModelSettings.fallbackProvider && chatModelSettings.fallbackModelId ? {
97056
+ fallbackProvider: chatModelSettings.fallbackProvider,
97057
+ fallbackModelId: chatModelSettings.fallbackModelId
97058
+ } : {},
97059
+ onFallbackModelUsed: (payload) => {
97060
+ fallbackInfo = payload;
97061
+ this.handleFallbackModelUsed(sessionId, payload);
97062
+ },
96285
97063
  onThinking: (delta) => {
96286
97064
  accumulatedThinking += delta;
96287
97065
  chatStreamManager.broadcast(sessionId, {
@@ -96322,16 +97100,35 @@ ${mentionContext}`;
96322
97100
  data: { toolName: name, isError, result }
96323
97101
  });
96324
97102
  }
96325
- });
97103
+ };
97104
+ const agentRuntimeHint = agent ? extractRuntimeHint(agent.runtimeConfig) : void 0;
97105
+ if (agentRuntimeHint) {
97106
+ agentResult = await createResolvedAgentSession2({
97107
+ sessionPurpose: "executor",
97108
+ runtimeHint: agentRuntimeHint,
97109
+ pluginRunner: this.pluginRunner,
97110
+ ...sessionOptions
97111
+ });
97112
+ } else {
97113
+ agentResult = await createFnAgent8(sessionOptions);
97114
+ }
96326
97115
  this.activeGenerations.set(sessionId, { abortController, agentResult });
96327
97116
  if (abortController.signal.aborted) {
96328
97117
  agentResult.session.dispose?.();
96329
97118
  return;
96330
97119
  }
96331
- await agentResult.session.prompt(promptContent);
97120
+ await promptWithFallback(agentResult.session, promptContent);
96332
97121
  if (abortController.signal.aborted) {
96333
97122
  return;
96334
97123
  }
97124
+ const sessionErrorMessage = agentResult.session.state.errorMessage;
97125
+ if (typeof sessionErrorMessage === "string" && sessionErrorMessage.trim().length > 0 && !accumulatedText && !accumulatedThinking && toolCallsAccum.length === 0) {
97126
+ chatStreamManager.broadcast(sessionId, {
97127
+ type: "error",
97128
+ data: sessionErrorMessage
97129
+ });
97130
+ return;
97131
+ }
96335
97132
  let responseText = "";
96336
97133
  const lastMessage = agentResult.session.state.messages.filter((m) => m.role === "assistant").pop();
96337
97134
  if (lastMessage?.content) {
@@ -96342,11 +97139,18 @@ ${mentionContext}`;
96342
97139
  }
96343
97140
  }
96344
97141
  const finalResponseText = accumulatedText || responseText;
97142
+ const assistantMetadata = {};
97143
+ if (toolCallsAccum.length > 0) {
97144
+ assistantMetadata.toolCalls = toolCallsAccum;
97145
+ }
97146
+ if (fallbackInfo) {
97147
+ assistantMetadata.fallback = fallbackInfo;
97148
+ }
96345
97149
  const assistantMessage = this.chatStore.addMessage(sessionId, {
96346
97150
  role: "assistant",
96347
97151
  content: finalResponseText,
96348
97152
  thinkingOutput: accumulatedThinking || void 0,
96349
- metadata: toolCallsAccum.length > 0 ? { toolCalls: toolCallsAccum } : void 0
97153
+ metadata: Object.keys(assistantMetadata).length > 0 ? assistantMetadata : void 0
96350
97154
  });
96351
97155
  chatStreamManager.broadcast(sessionId, {
96352
97156
  type: "done",
@@ -96370,6 +97174,7 @@ ${mentionContext}`;
96370
97174
  thinkingOutput: accumulatedThinking || void 0,
96371
97175
  metadata: {
96372
97176
  interrupted: true,
97177
+ ...fallbackInfo ? { fallback: fallbackInfo } : {},
96373
97178
  ...toolCallsAccum.length > 0 ? { toolCalls: toolCallsAccum } : {}
96374
97179
  }
96375
97180
  });
@@ -96432,7 +97237,7 @@ ${mentionContext}`;
96432
97237
  import { randomUUID as randomUUID19 } from "node:crypto";
96433
97238
  import { createReadStream as createReadStream2 } from "node:fs";
96434
97239
  import { mkdir as mkdir13, rm as rm2, writeFile as writeFile13 } from "node:fs/promises";
96435
- import { basename as basename10, join as join41, resolve as resolve20 } from "node:path";
97240
+ import { basename as basename10, join as join42, resolve as resolve20 } from "node:path";
96436
97241
  function resolveAttachmentPath(rootDir, sessionId, filename) {
96437
97242
  const sessionDir = resolve20(rootDir, ".fusion", "chat-attachments", sessionId);
96438
97243
  const safeName = basename10(filename);
@@ -96690,7 +97495,7 @@ function registerChatRoutes(ctx, deps) {
96690
97495
  await mkdir13(sessionDir, { recursive: true });
96691
97496
  const sanitizedFilename = (file.originalname || "attachment").replace(/[^a-zA-Z0-9._-]/g, "_");
96692
97497
  const filename = `${Date.now()}-${sanitizedFilename}`;
96693
- const filePath = join41(sessionDir, filename);
97498
+ const filePath = join42(sessionDir, filename);
96694
97499
  await writeFile13(filePath, file.buffer);
96695
97500
  const attachment = {
96696
97501
  id: `att-${randomUUID19().slice(0, 8)}`,
@@ -101612,7 +102417,7 @@ var init_remote_auth = __esm({
101612
102417
 
101613
102418
  // ../dashboard/src/routes/register-settings-memory-routes.ts
101614
102419
  import { execFile as execFile5 } from "node:child_process";
101615
- import { homedir as homedir6 } from "node:os";
102420
+ import { homedir as homedir7 } from "node:os";
101616
102421
  import { promisify as promisify12 } from "node:util";
101617
102422
  function registerSettingsMemoryRoutes(ctx, deps) {
101618
102423
  const { router, options, store, runtimeLogger, getProjectContext: getProjectContext3, rethrowAsApiError: rethrowAsApiError8 } = ctx;
@@ -101708,7 +102513,7 @@ function registerSettingsMemoryRoutes(ctx, deps) {
101708
102513
  try {
101709
102514
  await execFileAsync7("mv", [tempPath, globalInstallPath], { timeout: 3e4 });
101710
102515
  } catch (error) {
101711
- const localBinDir = `${homedir6()}/.local/bin`;
102516
+ const localBinDir = `${homedir7()}/.local/bin`;
101712
102517
  const localInstallPath = `${localBinDir}/cloudflared`;
101713
102518
  attemptedCommands.push(`mkdir -p ${localBinDir}`);
101714
102519
  attemptedCommands.push(`mv ${tempPath} ${localInstallPath}`);
@@ -103074,7 +103879,7 @@ import * as os3 from "os";
103074
103879
  import * as path2 from "path";
103075
103880
  import * as fs2 from "node:fs";
103076
103881
  import { createRequire as createRequire3 } from "node:module";
103077
- import { join as join42, dirname as dirname13 } from "node:path";
103882
+ import { join as join43, dirname as dirname13 } from "node:path";
103078
103883
  function getNativePrebuildName() {
103079
103884
  const platform4 = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : process.platform === "win32" ? "win32" : "unknown";
103080
103885
  const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : "unknown";
@@ -103084,12 +103889,12 @@ function findInstalledNodePtyNativeDir() {
103084
103889
  try {
103085
103890
  const packageJsonPath = require2.resolve("node-pty/package.json");
103086
103891
  const pkgRoot = dirname13(packageJsonPath);
103087
- const releaseDir = join42(pkgRoot, "build", "Release");
103088
- if (fs2.existsSync(join42(releaseDir, "pty.node"))) {
103892
+ const releaseDir = join43(pkgRoot, "build", "Release");
103893
+ if (fs2.existsSync(join43(releaseDir, "pty.node"))) {
103089
103894
  return releaseDir;
103090
103895
  }
103091
- const prebuildDir = join42(pkgRoot, "prebuilds", getNativePrebuildName());
103092
- if (fs2.existsSync(join42(prebuildDir, "pty.node"))) {
103896
+ const prebuildDir = join43(pkgRoot, "prebuilds", getNativePrebuildName());
103897
+ if (fs2.existsSync(join43(prebuildDir, "pty.node"))) {
103093
103898
  return prebuildDir;
103094
103899
  }
103095
103900
  return null;
@@ -103115,8 +103920,8 @@ function ensureNodePtyNativePermissions() {
103115
103920
  candidateDirs.add(installedNativeDir);
103116
103921
  }
103117
103922
  for (const nativeDir of candidateDirs) {
103118
- const helperPath = join42(nativeDir, "spawn-helper");
103119
- const nativeModulePath = join42(nativeDir, "pty.node");
103923
+ const helperPath = join43(nativeDir, "spawn-helper");
103924
+ const nativeModulePath = join43(nativeDir, "pty.node");
103120
103925
  try {
103121
103926
  fs2.chmodSync(helperPath, 493);
103122
103927
  } catch {
@@ -103134,14 +103939,14 @@ function ensureNodePtyNativePermissions() {
103134
103939
  function findStagedNativeDir() {
103135
103940
  const prebuildName = getNativePrebuildName();
103136
103941
  if (process.env.FUSION_RUNTIME_DIR) {
103137
- const envPath = join42(process.env.FUSION_RUNTIME_DIR, prebuildName);
103138
- if (fs2.existsSync(join42(envPath, "pty.node"))) {
103942
+ const envPath = join43(process.env.FUSION_RUNTIME_DIR, prebuildName);
103943
+ if (fs2.existsSync(join43(envPath, "pty.node"))) {
103139
103944
  return envPath;
103140
103945
  }
103141
103946
  }
103142
103947
  const execDir = dirname13(process.execPath);
103143
- const nextToBinary = join42(execDir, "runtime", prebuildName);
103144
- if (fs2.existsSync(join42(nextToBinary, "pty.node"))) {
103948
+ const nextToBinary = join43(execDir, "runtime", prebuildName);
103949
+ if (fs2.existsSync(join43(nextToBinary, "pty.node"))) {
103145
103950
  return nextToBinary;
103146
103951
  }
103147
103952
  return null;
@@ -103161,7 +103966,7 @@ async function loadPtyModule() {
103161
103966
  process.env.NODE_PTY_SPAWN_HELPER_DIR = nativeDir;
103162
103967
  }
103163
103968
  process.env.FUSION_NATIVE_ASSETS_PATH = nativeDir;
103164
- const nativePath = join42(nativeDir, "pty.node");
103969
+ const nativePath = join43(nativeDir, "pty.node");
103165
103970
  if (fs2.existsSync(nativePath)) {
103166
103971
  try {
103167
103972
  const nativeModule = { exports: {} };
@@ -108851,7 +109656,7 @@ var init_terminal = __esm({
108851
109656
  });
108852
109657
 
108853
109658
  // ../dashboard/src/file-service.ts
108854
- import { join as join43, resolve as resolve22, relative as relative11, dirname as dirname14, basename as basename12 } from "node:path";
109659
+ import { join as join44, resolve as resolve22, relative as relative11, dirname as dirname14, basename as basename12 } from "node:path";
108855
109660
  import { readdir as readdir8, readFile as fsReadFile, writeFile as fsWriteFile, stat as stat7, copyFile as fsCopyFile, rename as fsRename, rm as fsRm, mkdir as mkdir14, access as access6 } from "node:fs/promises";
108856
109661
  async function getTaskBasePath(store, taskId) {
108857
109662
  try {
@@ -108864,7 +109669,7 @@ async function getTaskBasePath(store, taskId) {
108864
109669
  }
108865
109670
  }
108866
109671
  const rootDir = store.getRootDir();
108867
- return resolve22(join43(rootDir, ".fusion", "tasks", taskId));
109672
+ return resolve22(join44(rootDir, ".fusion", "tasks", taskId));
108868
109673
  } catch (err) {
108869
109674
  const error = err;
108870
109675
  if (error.code === "ENOENT" || error.message && error.message.includes("not found")) {
@@ -108891,7 +109696,7 @@ function validatePath(basePath, filePath) {
108891
109696
  throw new FileServiceError(`Access denied: Absolute paths not allowed`, "EINVAL");
108892
109697
  }
108893
109698
  const resolvedBase = resolve22(basePath);
108894
- const resolvedPath = resolve22(join43(resolvedBase, decodedPath));
109699
+ const resolvedPath = resolve22(join44(resolvedBase, decodedPath));
108895
109700
  const relativePath = relative11(resolvedBase, resolvedPath);
108896
109701
  if (relativePath.startsWith("..") || relativePath.startsWith("../") || relativePath === "..") {
108897
109702
  throw new FileServiceError(`Access denied: Path traversal detected`, "EINVAL");
@@ -108920,7 +109725,7 @@ async function listFilesForBasePath(basePath, subPath) {
108920
109725
  const entries = await readdir8(targetPath, { withFileTypes: true });
108921
109726
  const fileNodes = [];
108922
109727
  for (const entry of entries) {
108923
- const entryPath = join43(targetPath, entry.name);
109728
+ const entryPath = join44(targetPath, entry.name);
108924
109729
  const entryStats = await stat7(entryPath);
108925
109730
  fileNodes.push({
108926
109731
  name: entry.name,
@@ -109247,7 +110052,7 @@ async function renameWorkspaceFile(store, workspace, filePath, newName) {
109247
110052
  }
109248
110053
  throw err;
109249
110054
  }
109250
- const destPath = join43(dirname14(resolvedPath), newName);
110055
+ const destPath = join44(dirname14(resolvedPath), newName);
109251
110056
  const destRelative = relative11(resolve22(workspaceBase), destPath);
109252
110057
  if (destRelative.startsWith("..") || destRelative.startsWith("../") || destRelative === "..") {
109253
110058
  throw new FileServiceError("Destination would be outside workspace", "EINVAL");
@@ -109340,7 +110145,7 @@ function isHiddenPathSegment(name) {
109340
110145
  return name.startsWith(".");
109341
110146
  }
109342
110147
  async function walkDirForMarkdown(basePath, currentRelative, results, options) {
109343
- const currentPath = currentRelative ? join43(basePath, currentRelative) : basePath;
110148
+ const currentPath = currentRelative ? join44(basePath, currentRelative) : basePath;
109344
110149
  let entries;
109345
110150
  try {
109346
110151
  entries = await readdir8(currentPath, { withFileTypes: true });
@@ -109348,7 +110153,7 @@ async function walkDirForMarkdown(basePath, currentRelative, results, options) {
109348
110153
  return;
109349
110154
  }
109350
110155
  for (const entry of entries) {
109351
- const entryRelativePath = currentRelative ? join43(currentRelative, entry.name) : entry.name;
110156
+ const entryRelativePath = currentRelative ? join44(currentRelative, entry.name) : entry.name;
109352
110157
  if (entry.isDirectory()) {
109353
110158
  if (MARKDOWN_SCAN_EXCLUDED_DIRS.has(entry.name)) {
109354
110159
  continue;
@@ -109365,7 +110170,7 @@ async function walkDirForMarkdown(basePath, currentRelative, results, options) {
109365
110170
  if (!options.showHidden && isHiddenPathSegment(entry.name)) {
109366
110171
  continue;
109367
110172
  }
109368
- const fullPath = join43(basePath, entryRelativePath);
110173
+ const fullPath = join44(basePath, entryRelativePath);
109369
110174
  let fileStats;
109370
110175
  try {
109371
110176
  fileStats = await stat7(fullPath);
@@ -109415,7 +110220,7 @@ async function scanMarkdownFiles(store, options) {
109415
110220
  return;
109416
110221
  }
109417
110222
  for (const entry of entries) {
109418
- const entryRelativePath = relativeDir ? join43(relativeDir, entry.name) : entry.name;
110223
+ const entryRelativePath = relativeDir ? join44(relativeDir, entry.name) : entry.name;
109419
110224
  let shouldRecurse = entry.isDirectory();
109420
110225
  if (!shouldRecurse && typeof entry.isSymbolicLink === "function" && entry.isSymbolicLink()) {
109421
110226
  let symlinkPath;
@@ -109505,8 +110310,8 @@ async function searchWorkspaceFiles(store, workspace, query) {
109505
110310
  if (entry.isDirectory() && EXCLUDED_DIRS.has(entry.name)) {
109506
110311
  continue;
109507
110312
  }
109508
- const fullPath = join43(dir2, entry.name);
109509
- const relPath = join43(relativeDir, entry.name);
110313
+ const fullPath = join44(dir2, entry.name);
110314
+ const relPath = join44(relativeDir, entry.name);
109510
110315
  if (entry.isFile()) {
109511
110316
  if (entry.name.toLowerCase().includes(lowerQuery)) {
109512
110317
  results.push({
@@ -109527,8 +110332,8 @@ async function copyDirectoryRecursive(source, destination) {
109527
110332
  await mkdir14(destination, { recursive: true });
109528
110333
  const entries = await readdir8(source, { withFileTypes: true });
109529
110334
  for (const entry of entries) {
109530
- const sourcePath = join43(source, entry.name);
109531
- const destPath = join43(destination, entry.name);
110335
+ const sourcePath = join44(source, entry.name);
110336
+ const destPath = join44(destination, entry.name);
109532
110337
  if (entry.isDirectory()) {
109533
110338
  await copyDirectoryRecursive(sourcePath, destPath);
109534
110339
  } else {
@@ -113597,7 +114402,7 @@ var require_BufferList = __commonJS({
113597
114402
  this.head = this.tail = null;
113598
114403
  this.length = 0;
113599
114404
  };
113600
- BufferList.prototype.join = function join71(s) {
114405
+ BufferList.prototype.join = function join72(s) {
113601
114406
  if (this.length === 0) return "";
113602
114407
  var p = this.head;
113603
114408
  var ret = "" + p.data;
@@ -136106,7 +136911,7 @@ var init_exec_file = __esm({
136106
136911
 
136107
136912
  // ../dashboard/src/routes/register-project-routes.ts
136108
136913
  import * as fsPromises from "node:fs/promises";
136109
- import { dirname as dirname15, isAbsolute as isAbsolute17, join as join44 } from "node:path";
136914
+ import { dirname as dirname15, isAbsolute as isAbsolute17, join as join45 } from "node:path";
136110
136915
  var access9, stat8, mkdir15, readdir9, rm3, registerProjectRoutes;
136111
136916
  var init_register_project_routes = __esm({
136112
136917
  "../dashboard/src/routes/register-project-routes.ts"() {
@@ -136329,7 +137134,7 @@ var init_register_project_routes = __esm({
136329
137134
  }
136330
137135
  }
136331
137136
  let hasFusionDir = false;
136332
- const fusionDirPath = join44(normalizedPath, ".fusion");
137137
+ const fusionDirPath = join45(normalizedPath, ".fusion");
136333
137138
  try {
136334
137139
  await access9(fusionDirPath);
136335
137140
  hasFusionDir = true;
@@ -136393,8 +137198,8 @@ var init_register_project_routes = __esm({
136393
137198
  const entries = await readdir9(searchPath, { withFileTypes: true });
136394
137199
  for (const entry of entries) {
136395
137200
  if (!entry.isDirectory()) continue;
136396
- const dirPath = join44(searchPath, entry.name);
136397
- if (isValidSqliteDatabaseFile(join44(dirPath, ".fusion", "fusion.db"))) {
137201
+ const dirPath = join45(searchPath, entry.name);
137202
+ if (isValidSqliteDatabaseFile(join45(dirPath, ".fusion", "fusion.db"))) {
136398
137203
  detected.push({
136399
137204
  path: dirPath,
136400
137205
  suggestedName: entry.name,
@@ -137408,14 +138213,14 @@ var init_register_docker_provisioning_routes = __esm({
137408
138213
 
137409
138214
  // ../dashboard/src/auth-paths.ts
137410
138215
  import path3 from "node:path";
137411
- import { homedir as homedir7 } from "node:os";
137412
- function getFusionAgentDir2(home = process.env.HOME || process.env.USERPROFILE || homedir7()) {
138216
+ import { homedir as homedir8 } from "node:os";
138217
+ function getFusionAgentDir2(home = process.env.HOME || process.env.USERPROFILE || homedir8()) {
137413
138218
  return path3.join(home, ".fusion", "agent");
137414
138219
  }
137415
- function getFusionAuthPath2(home = process.env.HOME || process.env.USERPROFILE || homedir7()) {
138220
+ function getFusionAuthPath2(home = process.env.HOME || process.env.USERPROFILE || homedir8()) {
137416
138221
  return path3.join(getFusionAgentDir2(home), "auth.json");
137417
138222
  }
137418
- function getAuthFileCandidates(cwd = process.cwd(), home = process.env.HOME || process.env.USERPROFILE || homedir7()) {
138223
+ function getAuthFileCandidates(cwd = process.cwd(), home = process.env.HOME || process.env.USERPROFILE || homedir8()) {
137419
138224
  return [
137420
138225
  path3.join(home, ".fusion", "agent", "auth.json"),
137421
138226
  path3.join(home, ".fusion", "auth.json"),
@@ -140718,7 +141523,7 @@ Rules:
140718
141523
  import { createWriteStream } from "node:fs";
140719
141524
  import * as fsPromises2 from "node:fs/promises";
140720
141525
  import { tmpdir as tmpdir4 } from "node:os";
140721
- import { join as join45, resolve as resolve23 } from "node:path";
141526
+ import { join as join46, resolve as resolve23 } from "node:path";
140722
141527
  import { Readable } from "node:stream";
140723
141528
  import { pipeline as streamPipeline } from "node:stream/promises";
140724
141529
  function registerAgentImportExportRoutes(ctx) {
@@ -140759,7 +141564,7 @@ function registerAgentImportExportRoutes(ctx) {
140759
141564
  } else if (typeof outputDir === "string") {
140760
141565
  throw badRequest("outputDir cannot be empty");
140761
141566
  } else {
140762
- resolvedOutputDir = await mkdtemp(join45(tmpdir4(), "fusion-agent-export-"));
141567
+ resolvedOutputDir = await mkdtemp(join46(tmpdir4(), "fusion-agent-export-"));
140763
141568
  }
140764
141569
  const result = await exportAgentsToDirectory2(agentsToExport, resolvedOutputDir, {
140765
141570
  companyName: typeof companyName === "string" ? companyName : void 0,
@@ -140886,7 +141691,7 @@ ${body}`;
140886
141691
  return result;
140887
141692
  }
140888
141693
  const safeCompanySlug = companySlug ? slugifyPathSegment2(companySlug, "unknown-company") : "unknown-company";
140889
- const skillsBaseDir = join45(projectRoot, "skills", "imported", safeCompanySlug);
141694
+ const skillsBaseDir = join46(projectRoot, "skills", "imported", safeCompanySlug);
140890
141695
  const usedSlugs = /* @__PURE__ */ new Set();
140891
141696
  for (const skill of skills) {
140892
141697
  const name = typeof skill.name === "string" && skill.name.trim().length > 0 ? skill.name.trim() : null;
@@ -140903,8 +141708,8 @@ ${body}`;
140903
141708
  skillSlug = `${skillSlug}-${counter}`;
140904
141709
  }
140905
141710
  usedSlugs.add(skillSlug);
140906
- const skillDir = join45(skillsBaseDir, skillSlug);
140907
- const skillPath = join45(skillDir, "SKILL.md");
141711
+ const skillDir = join46(skillsBaseDir, skillSlug);
141712
+ const skillPath = join46(skillDir, "SKILL.md");
140908
141713
  try {
140909
141714
  await access10(skillPath);
140910
141715
  result.skipped.push(name);
@@ -141074,8 +141879,8 @@ ${body}`;
141074
141879
  const archiveUrl = `https://github.com/${repoOwner}/${repoName}/archive/refs/heads/main.tar.gz`;
141075
141880
  let tempDir = null;
141076
141881
  try {
141077
- tempDir = await mkdtemp(join45(tmpdir4(), `fn-agent-import-${importCompanySlug}-`));
141078
- const archivePath = join45(tempDir, "archive.tar.gz");
141882
+ tempDir = await mkdtemp(join46(tmpdir4(), `fn-agent-import-${importCompanySlug}-`));
141883
+ const archivePath = join46(tempDir, "archive.tar.gz");
141079
141884
  const downloadController = new AbortController();
141080
141885
  const downloadTimeout = setTimeout(() => downloadController.abort(), 3e4);
141081
141886
  let archiveResponse;
@@ -142356,7 +143161,7 @@ import * as path4 from "node:path";
142356
143161
  import { readFile as readFile20 } from "node:fs/promises";
142357
143162
  import * as https from "node:https";
142358
143163
  import * as child_process from "node:child_process";
142359
- function getHomeDir5() {
143164
+ function getHomeDir6() {
142360
143165
  return process.env.HOME || process.env.USERPROFILE || os4.homedir();
142361
143166
  }
142362
143167
  function execFileAsync5(file, args, options) {
@@ -142866,8 +143671,8 @@ async function fetchClaudeUsage(authStorage) {
142866
143671
  }
142867
143672
  if (!creds) {
142868
143673
  const credPaths = [
142869
- path4.join(getHomeDir5(), ".claude", ".credentials.json"),
142870
- path4.join(getHomeDir5(), ".config", "claude", ".credentials.json")
143674
+ path4.join(getHomeDir6(), ".claude", ".credentials.json"),
143675
+ path4.join(getHomeDir6(), ".config", "claude", ".credentials.json")
142871
143676
  ];
142872
143677
  for (const p of credPaths) {
142873
143678
  try {
@@ -143027,7 +143832,7 @@ async function fetchCodexUsage() {
143027
143832
  status: "no-auth",
143028
143833
  windows: []
143029
143834
  };
143030
- const codexHome = process.env.CODEX_HOME || path4.join(getHomeDir5(), ".codex");
143835
+ const codexHome = process.env.CODEX_HOME || path4.join(getHomeDir6(), ".codex");
143031
143836
  const authPath = path4.join(codexHome, "auth.json");
143032
143837
  let auth = null;
143033
143838
  try {
@@ -143118,7 +143923,7 @@ async function fetchGeminiUsage() {
143118
143923
  status: "no-auth",
143119
143924
  windows: []
143120
143925
  };
143121
- const oauthPath = path4.join(getHomeDir5(), ".gemini", "oauth_creds.json");
143926
+ const oauthPath = path4.join(getHomeDir6(), ".gemini", "oauth_creds.json");
143122
143927
  let oauthCreds = null;
143123
143928
  try {
143124
143929
  oauthCreds = JSON.parse(await readFile20(oauthPath, "utf-8"));
@@ -143134,7 +143939,7 @@ async function fetchGeminiUsage() {
143134
143939
  const claims = decodeJwtPayload(oauthCreds.id_token);
143135
143940
  if (claims?.email) usage.email = claims.email;
143136
143941
  }
143137
- const settingsPath = path4.join(getHomeDir5(), ".gemini", "settings.json");
143942
+ const settingsPath = path4.join(getHomeDir6(), ".gemini", "settings.json");
143138
143943
  try {
143139
143944
  const settings = JSON.parse(await readFile20(settingsPath, "utf-8"));
143140
143945
  const authType = settings?.security?.auth?.selectedType;
@@ -143771,6 +144576,13 @@ var init_register_auth_routes = __esm({
143771
144576
  }
143772
144577
  return key.slice(0, 3) + "\u2022\u2022\u2022\u2022\u2022" + key.slice(-4);
143773
144578
  }
144579
+ function isExpiredOauthCredential(providerId, storage) {
144580
+ const credential = storage.get?.(providerId);
144581
+ if (!credential || credential.type !== "oauth" || typeof credential.expires !== "number") {
144582
+ return false;
144583
+ }
144584
+ return Date.now() >= credential.expires;
144585
+ }
143774
144586
  const loginInProgress = /* @__PURE__ */ new Map();
143775
144587
  const OAUTH_SESSION_TTL_MS = 5 * 60 * 1e3;
143776
144588
  const oauthSessions = /* @__PURE__ */ new Map();
@@ -143831,6 +144643,40 @@ var init_register_auth_routes = __esm({
143831
144643
  path: `${redirectUriUrl.pathname}${redirectUriUrl.search}`
143832
144644
  };
143833
144645
  }
144646
+ function shouldRewriteOauthRedirect(providerId, origin) {
144647
+ if (!origin || isLocalhostOrigin(origin)) {
144648
+ return false;
144649
+ }
144650
+ if (providerId === "openai-codex") {
144651
+ return false;
144652
+ }
144653
+ return true;
144654
+ }
144655
+ function getManualCodeConfig(providerId, origin) {
144656
+ if (providerId !== "openai-codex") {
144657
+ return void 0;
144658
+ }
144659
+ const remoteDashboard = origin !== void 0 && !isLocalhostOrigin(origin);
144660
+ return {
144661
+ prompt: "Paste the final redirect URL or authorization code",
144662
+ placeholder: "http://localhost:1455/auth/callback?code=...&state=... or just the code",
144663
+ helpText: remoteDashboard ? "After sign-in, OpenAI may redirect to a localhost callback that cannot open from this dashboard host. Copy the full browser URL from the address bar and paste it here." : "If the browser cannot finish the localhost callback automatically, copy the full browser URL from the address bar and paste it here."
144664
+ };
144665
+ }
144666
+ function appendManualCodeHint(instructions, providerId, origin) {
144667
+ const manualCode = getManualCodeConfig(providerId, origin);
144668
+ if (!manualCode) {
144669
+ return instructions;
144670
+ }
144671
+ const hint = manualCode.helpText;
144672
+ if (!hint) {
144673
+ return instructions;
144674
+ }
144675
+ if (!instructions?.trim()) {
144676
+ return hint;
144677
+ }
144678
+ return `${instructions.trim()} ${hint}`;
144679
+ }
143834
144680
  router.get("/auth/status", async (_req, res) => {
143835
144681
  try {
143836
144682
  const storage = getAuthStorage();
@@ -143839,7 +144685,7 @@ var init_register_auth_routes = __esm({
143839
144685
  const providers = oauthProviders.map((p) => ({
143840
144686
  id: p.id,
143841
144687
  name: p.name,
143842
- authenticated: storage.hasAuth(p.id),
144688
+ authenticated: storage.hasAuth(p.id) && !isExpiredOauthCredential(p.id, storage),
143843
144689
  type: "oauth",
143844
144690
  loginInProgress: loginInProgress.has(p.id)
143845
144691
  }));
@@ -144093,7 +144939,29 @@ var init_register_auth_routes = __esm({
144093
144939
  throw badRequest(`Unknown provider: ${provider}`);
144094
144940
  }
144095
144941
  const abortController = new AbortController();
144096
- loginInProgress.set(provider, abortController);
144942
+ let resolveInput = () => {
144943
+ };
144944
+ let rejectInput = () => {
144945
+ };
144946
+ const inputPromise = new Promise((resolve44, reject2) => {
144947
+ resolveInput = resolve44;
144948
+ rejectInput = reject2;
144949
+ });
144950
+ void inputPromise.catch((error) => {
144951
+ const message = error instanceof Error ? error.message : String(error);
144952
+ if (message !== "cancelled") {
144953
+ console.warn(`[auth/login] manual OAuth input promise rejected for ${provider}: ${message}`);
144954
+ }
144955
+ });
144956
+ const pendingLogin = {
144957
+ abortController,
144958
+ inputPromise,
144959
+ resolveInput,
144960
+ rejectInput,
144961
+ inputSubmitted: false,
144962
+ manualCode: getManualCodeConfig(provider, origin)
144963
+ };
144964
+ loginInProgress.set(provider, pendingLogin);
144097
144965
  let authResolve;
144098
144966
  let authReject;
144099
144967
  const authUrlPromise = new Promise((resolve44, reject2) => {
@@ -144102,12 +144970,16 @@ var init_register_auth_routes = __esm({
144102
144970
  });
144103
144971
  const loginPromise = storage.login(provider, {
144104
144972
  onAuth: (info) => {
144105
- authResolve({ url: info.url, instructions: info.instructions });
144106
- },
144107
- onPrompt: async (prompt) => {
144108
- if (prompt.allowEmpty) return "";
144109
- return prompt.placeholder || "";
144973
+ authResolve({
144974
+ url: info.url,
144975
+ instructions: appendManualCodeHint(info.instructions, provider, origin)
144976
+ });
144110
144977
  },
144978
+ onPrompt: async () => await pendingLogin.inputPromise,
144979
+ // AuthStorage.login() forwards callbacks to provider-specific OAuth
144980
+ // implementations verbatim. openai-codex supports this optional hook
144981
+ // to race pasted codes against the localhost callback server.
144982
+ onManualCodeInput: async () => await pendingLogin.inputPromise,
144111
144983
  onProgress: () => {
144112
144984
  },
144113
144985
  // no-op for web UI
@@ -144126,7 +144998,7 @@ var init_register_auth_routes = __esm({
144126
144998
  const authInfo = await authUrlPromise;
144127
144999
  clearTimeout(timeout2);
144128
145000
  let responseUrl = authInfo.url;
144129
- if (origin && !isLocalhostOrigin(origin)) {
145001
+ if (shouldRewriteOauthRedirect(provider, origin)) {
144130
145002
  const rewritten = rewriteAuthUrl(authInfo.url, origin);
144131
145003
  setOauthSession(rewritten.state, {
144132
145004
  port: rewritten.port,
@@ -144135,7 +145007,11 @@ var init_register_auth_routes = __esm({
144135
145007
  });
144136
145008
  responseUrl = rewritten.url;
144137
145009
  }
144138
- res.json({ url: responseUrl, instructions: authInfo.instructions });
145010
+ res.json({
145011
+ url: responseUrl,
145012
+ instructions: authInfo.instructions,
145013
+ manualCode: pendingLogin.manualCode
145014
+ });
144139
145015
  } catch (err) {
144140
145016
  if (err instanceof ApiError) {
144141
145017
  throw err;
@@ -144157,7 +145033,9 @@ var init_register_auth_routes = __esm({
144157
145033
  return;
144158
145034
  }
144159
145035
  loginInProgress.delete(provider);
144160
- activeLogin.abort();
145036
+ activeLogin.inputSubmitted = true;
145037
+ activeLogin.rejectInput(new Error("cancelled"));
145038
+ activeLogin.abortController.abort();
144161
145039
  res.json({ success: true, cancelled: true });
144162
145040
  } catch (err) {
144163
145041
  if (err instanceof ApiError) {
@@ -144166,6 +145044,33 @@ var init_register_auth_routes = __esm({
144166
145044
  rethrowAsApiError8(err);
144167
145045
  }
144168
145046
  });
145047
+ router.post("/auth/manual-code", (req, res) => {
145048
+ try {
145049
+ const { provider, code } = req.body;
145050
+ if (!provider || typeof provider !== "string") {
145051
+ throw badRequest("provider is required");
145052
+ }
145053
+ if (!code || typeof code !== "string" || !code.trim()) {
145054
+ throw badRequest("code is required");
145055
+ }
145056
+ const activeLogin = loginInProgress.get(provider);
145057
+ if (!activeLogin) {
145058
+ throw conflict(`No login in progress for ${provider}`);
145059
+ }
145060
+ if (activeLogin.inputSubmitted) {
145061
+ res.json({ success: true, submitted: false });
145062
+ return;
145063
+ }
145064
+ activeLogin.inputSubmitted = true;
145065
+ activeLogin.resolveInput(code.trim());
145066
+ res.json({ success: true, submitted: true });
145067
+ } catch (err) {
145068
+ if (err instanceof ApiError) {
145069
+ throw err;
145070
+ }
145071
+ rethrowAsApiError8(err);
145072
+ }
145073
+ });
144169
145074
  router.get("/auth/oauth-callback", async (req, res) => {
144170
145075
  try {
144171
145076
  const error = typeof req.query.error === "string" ? req.query.error : void 0;
@@ -145497,15 +146402,15 @@ var init_register_runtime_provider_routes = __esm({
145497
146402
  });
145498
146403
 
145499
146404
  // ../dashboard/src/cli-package-version.ts
145500
- import { existsSync as existsSync33, readFileSync as readFileSync11 } from "node:fs";
146405
+ import { existsSync as existsSync34, readFileSync as readFileSync12 } from "node:fs";
145501
146406
  import { dirname as dirname16, resolve as resolve24 } from "node:path";
145502
146407
  import { fileURLToPath as fileURLToPath4 } from "node:url";
145503
146408
  function readCliPackageVersion(pkgPath) {
145504
- if (!existsSync33(pkgPath)) {
146409
+ if (!existsSync34(pkgPath)) {
145505
146410
  return null;
145506
146411
  }
145507
146412
  try {
145508
- const parsed = JSON.parse(readFileSync11(pkgPath, "utf-8"));
146413
+ const parsed = JSON.parse(readFileSync12(pkgPath, "utf-8"));
145509
146414
  if (parsed.name === CLI_PACKAGE_NAME && typeof parsed.version === "string" && parsed.version.length > 0) {
145510
146415
  return {
145511
146416
  packageJsonPath: pkgPath,
@@ -145707,9 +146612,9 @@ var init_register_fn_binary_routes = __esm({
145707
146612
  });
145708
146613
 
145709
146614
  // ../dashboard/src/update-check.ts
145710
- import { readFileSync as readFileSync12 } from "node:fs";
146615
+ import { readFileSync as readFileSync13 } from "node:fs";
145711
146616
  import { mkdir as mkdir17, rm as rm5, writeFile as writeFile15 } from "node:fs/promises";
145712
- import { join as join47 } from "node:path";
146617
+ import { join as join48 } from "node:path";
145713
146618
  function ttlForFrequency(frequency) {
145714
146619
  switch (frequency) {
145715
146620
  case "manual":
@@ -145723,7 +146628,7 @@ function ttlForFrequency(frequency) {
145723
146628
  }
145724
146629
  }
145725
146630
  function getCachePath(fusionDir) {
145726
- return join47(fusionDir, CACHE_FILENAME);
146631
+ return join48(fusionDir, CACHE_FILENAME);
145727
146632
  }
145728
146633
  function parseVersion(version) {
145729
146634
  return version.split(".").slice(0, 3).map((part) => Number.parseInt(part, 10)).map((value) => Number.isFinite(value) ? value : 0);
@@ -145747,7 +146652,7 @@ function isValidResult(value) {
145747
146652
  }
145748
146653
  function readCachedUpdateCheck(fusionDir) {
145749
146654
  try {
145750
- const raw = readFileSync12(getCachePath(fusionDir), "utf-8");
146655
+ const raw = readFileSync13(getCachePath(fusionDir), "utf-8");
145751
146656
  const parsed = JSON.parse(raw);
145752
146657
  return isValidResult(parsed) ? parsed : null;
145753
146658
  } catch {
@@ -150394,7 +151299,7 @@ var init_todo_routes = __esm({
150394
151299
 
150395
151300
  // ../dashboard/src/dev-server-detect.ts
150396
151301
  import { glob, readFile as readFile21 } from "node:fs/promises";
150397
- import { dirname as dirname17, join as join48, relative as relative12, resolve as resolve25 } from "node:path";
151302
+ import { dirname as dirname17, join as join49, relative as relative12, resolve as resolve25 } from "node:path";
150398
151303
  async function readPackageJson(filePath) {
150399
151304
  try {
150400
151305
  const raw = await readFile21(filePath, "utf-8");
@@ -150437,7 +151342,7 @@ function scoreCandidate(scriptName, pkg) {
150437
151342
  async function collectWorkspacePackageJsons(projectRoot) {
150438
151343
  const discovered = /* @__PURE__ */ new Set();
150439
151344
  try {
150440
- await readFile21(join48(projectRoot, "pnpm-workspace.yaml"), "utf-8");
151345
+ await readFile21(join49(projectRoot, "pnpm-workspace.yaml"), "utf-8");
150441
151346
  } catch {
150442
151347
  }
150443
151348
  for (const pattern of ["packages/*/package.json", "apps/*/package.json"]) {
@@ -150471,7 +151376,7 @@ function toSource(projectRoot, packageJsonPath) {
150471
151376
  async function detectDevServerScripts(projectRoot) {
150472
151377
  const root = resolve25(projectRoot);
150473
151378
  const candidates = [];
150474
- const rootPackagePath = join48(root, "package.json");
151379
+ const rootPackagePath = join49(root, "package.json");
150475
151380
  const rootPackage = await readPackageJson(rootPackagePath);
150476
151381
  if (rootPackage) {
150477
151382
  for (const script of extractScripts(rootPackage)) {
@@ -150538,9 +151443,9 @@ var init_dev_server_detect = __esm({
150538
151443
 
150539
151444
  // ../dashboard/src/dev-server-store.ts
150540
151445
  import { mkdir as mkdir18, readFile as readFile22, writeFile as writeFile16 } from "node:fs/promises";
150541
- import { dirname as dirname18, join as join49, resolve as resolve26 } from "node:path";
151446
+ import { dirname as dirname18, join as join50, resolve as resolve26 } from "node:path";
150542
151447
  function devServerFilePath(projectDir) {
150543
- return join49(resolve26(projectDir), ".fusion", "dev-server.json");
151448
+ return join50(resolve26(projectDir), ".fusion", "dev-server.json");
150544
151449
  }
150545
151450
  function normalizeState(candidate) {
150546
151451
  const defaults = DEV_SERVER_DEFAULT_STATE();
@@ -151794,7 +152699,7 @@ Your job is to refine task descriptions based on the user's selected refinement
151794
152699
 
151795
152700
  // ../dashboard/src/routes.ts
151796
152701
  import multer from "multer";
151797
- import { resolve as resolve27, sep as sep7, join as join50, isAbsolute as isAbsolute18 } from "node:path";
152702
+ import { resolve as resolve27, sep as sep7, join as join51, isAbsolute as isAbsolute18 } from "node:path";
151798
152703
  import * as nodeFs from "node:fs";
151799
152704
  import os5 from "node:os";
151800
152705
  import v8 from "node:v8";
@@ -151822,8 +152727,8 @@ async function getPiPackageManagerAgentDir() {
151822
152727
  const fusionAgentDir = getFusionAgentDir();
151823
152728
  const legacyAgentDir = getLegacyPiAgentDir();
151824
152729
  const [fusionSettings, legacySettings, legacyExists, fusionExists] = await Promise.all([
151825
- readJsonObject3(join50(fusionAgentDir, "settings.json")),
151826
- readJsonObject3(join50(legacyAgentDir, "settings.json")),
152730
+ readJsonObject3(join51(fusionAgentDir, "settings.json")),
152731
+ readJsonObject3(join51(legacyAgentDir, "settings.json")),
151827
152732
  pathExists(legacyAgentDir),
151828
152733
  pathExists(fusionAgentDir)
151829
152734
  ]);
@@ -151850,9 +152755,9 @@ async function discoverDashboardPiExtensions(cwd) {
151850
152755
  const { DefaultPackageManager: DefaultPackageManager5 } = await import("@mariozechner/pi-coding-agent");
151851
152756
  const [agentDir, legacyGlobalSettings, fusionGlobalSettings, projectSettings] = await Promise.all([
151852
152757
  getPiPackageManagerAgentDir(),
151853
- readJsonObject3(join50(getLegacyPiAgentDir(), "settings.json")),
151854
- readJsonObject3(join50(getFusionAgentDir(), "settings.json")),
151855
- readJsonObject3(join50(cwd, ".fusion", "settings.json"))
152758
+ readJsonObject3(join51(getLegacyPiAgentDir(), "settings.json")),
152759
+ readJsonObject3(join51(getFusionAgentDir(), "settings.json")),
152760
+ readJsonObject3(join51(cwd, ".fusion", "settings.json"))
151856
152761
  ]);
151857
152762
  const globalSettings = { ...legacyGlobalSettings, ...fusionGlobalSettings };
151858
152763
  const mergedSettings = { ...globalSettings, ...projectSettings };
@@ -154266,7 +155171,7 @@ Description: ${step.description}`
154266
155171
  return;
154267
155172
  }
154268
155173
  }
154269
- const { resolve: resolve44, dirname: dirname34, join: join71 } = await import("node:path");
155174
+ const { resolve: resolve44, dirname: dirname34, join: join72 } = await import("node:path");
154270
155175
  const { readdir: readdir12, stat: stat12 } = await import("node:fs/promises");
154271
155176
  const rawPath = req.query.path || process.env.HOME || process.env.USERPROFILE || "/";
154272
155177
  const showHidden = req.query.showHidden === "true";
@@ -154291,7 +155196,7 @@ Description: ${step.description}`
154291
155196
  for (const entry of dirEntries) {
154292
155197
  if (!entry.isDirectory()) continue;
154293
155198
  if (!showHidden && entry.name.startsWith(".")) continue;
154294
- const entryPath = join71(resolvedPath, entry.name);
155199
+ const entryPath = join72(resolvedPath, entry.name);
154295
155200
  let hasChildren = false;
154296
155201
  try {
154297
155202
  const subEntries = await readdir12(entryPath, { withFileTypes: true });
@@ -159899,8 +160804,8 @@ var init_auth_middleware = __esm({
159899
160804
  // ../dashboard/src/server.ts
159900
160805
  import express from "express";
159901
160806
  import { randomUUID as randomUUID25 } from "node:crypto";
159902
- import { join as join51, dirname as dirname19 } from "node:path";
159903
- import { existsSync as existsSync34, readFileSync as readFileSync13 } from "node:fs";
160807
+ import { join as join52, dirname as dirname19 } from "node:path";
160808
+ import { existsSync as existsSync35, readFileSync as readFileSync14 } from "node:fs";
159904
160809
  import { fileURLToPath as fileURLToPath5 } from "node:url";
159905
160810
  import { createSecureServer as createHttp2SecureServer } from "node:http2";
159906
160811
  function parseVersion2(version) {
@@ -160002,11 +160907,11 @@ function loadTlsCredentialsFromEnv(env = process.env) {
160002
160907
  "FUSION_TLS_* environment is incomplete: set both a cert and a key (inline via FUSION_TLS_CERT/FUSION_TLS_KEY or paths via *_FILE)."
160003
160908
  );
160004
160909
  }
160005
- const cert = certInline ? Buffer.from(certInline) : readFileSync13(certFile);
160006
- const key = keyInline ? Buffer.from(keyInline) : readFileSync13(keyFile);
160910
+ const cert = certInline ? Buffer.from(certInline) : readFileSync14(certFile);
160911
+ const key = keyInline ? Buffer.from(keyInline) : readFileSync14(keyFile);
160007
160912
  const caInline = env.FUSION_TLS_CA;
160008
160913
  const caFile = env.FUSION_TLS_CA_FILE;
160009
- const ca = caInline ? Buffer.from(caInline) : caFile ? readFileSync13(caFile) : void 0;
160914
+ const ca = caInline ? Buffer.from(caInline) : caFile ? readFileSync14(caFile) : void 0;
160010
160915
  return { cert, key, ca };
160011
160916
  }
160012
160917
  function createServer(store, options) {
@@ -160082,11 +160987,11 @@ function createServer(store, options) {
160082
160987
  getTerminalService(store.getRootDir());
160083
160988
  const isHeadless = options?.headless === true;
160084
160989
  const execDir = dirname19(process.execPath);
160085
- const clientDir = process.env.FUSION_CLIENT_DIR ? process.env.FUSION_CLIENT_DIR : existsSync34(join51(execDir, "client", "index.html")) ? join51(execDir, "client") : existsSync34(join51(__dirname, "..", "dist", "client")) ? join51(__dirname, "..", "dist", "client") : join51(__dirname, "..", "client");
160990
+ const clientDir = process.env.FUSION_CLIENT_DIR ? process.env.FUSION_CLIENT_DIR : existsSync35(join52(execDir, "client", "index.html")) ? join52(execDir, "client") : existsSync35(join52(__dirname, "..", "dist", "client")) ? join52(__dirname, "..", "dist", "client") : join52(__dirname, "..", "client");
160086
160991
  if (!isHeadless) {
160087
160992
  app.get("/version.json", (_req, res) => {
160088
160993
  res.setHeader("Cache-Control", "no-store, max-age=0");
160089
- res.sendFile(join51(clientDir, "version.json"), (err) => {
160994
+ res.sendFile(join52(clientDir, "version.json"), (err) => {
160090
160995
  if (err) {
160091
160996
  res.status(404).json({ version: null });
160092
160997
  }
@@ -160350,7 +161255,13 @@ data: ${JSON.stringify({ type: event.type, data: event.data })}
160350
161255
  });
160351
161256
  }
160352
161257
  const chatAgentStore = new AgentStore({ rootDir: store.getFusionDir() });
160353
- const chatManager = options?.chatManager ?? new ChatManager(chatStore, store.getRootDir(), chatAgentStore);
161258
+ const chatManager = options?.chatManager ?? new ChatManager(
161259
+ chatStore,
161260
+ store.getRootDir(),
161261
+ chatAgentStore,
161262
+ options?.pluginRunner,
161263
+ () => store.getSettings()
161264
+ );
160354
161265
  const runAiSessionCleanup = (maxAgeMs, source) => {
160355
161266
  const result = aiSessionStore.cleanupStaleSessions(maxAgeMs);
160356
161267
  runtimeLogger.info("AI session cleanup summary", {
@@ -160543,7 +161454,7 @@ data: ${JSON.stringify({ type: event.type, data: event.data })}
160543
161454
  });
160544
161455
  if (!isHeadless) {
160545
161456
  app.get("/{*splat}", (_req, res) => {
160546
- res.sendFile(join51(clientDir, "index.html"));
161457
+ res.sendFile(join52(clientDir, "index.html"));
160547
161458
  });
160548
161459
  }
160549
161460
  const dashboardApp = app;
@@ -160998,7 +161909,7 @@ var init_server = __esm({
160998
161909
 
160999
161910
  // ../dashboard/src/skills-adapter.ts
161000
161911
  import { access as access11, readFile as readFile23, writeFile as writeFile17, mkdir as mkdir19, readdir as readdir10, stat as stat10 } from "node:fs/promises";
161001
- import { join as join52, relative as relative13, dirname as dirname20 } from "node:path";
161912
+ import { join as join53, relative as relative13, dirname as dirname20 } from "node:path";
161002
161913
  async function pathExists2(path5) {
161003
161914
  try {
161004
161915
  await access11(path5);
@@ -161254,7 +162165,7 @@ function createSkillsAdapter(options) {
161254
162165
  } catch {
161255
162166
  skillDir = dirname20(skill.path);
161256
162167
  }
161257
- const skillMdPath = join52(skillDir, "SKILL.md");
162168
+ const skillMdPath = join53(skillDir, "SKILL.md");
161258
162169
  let skillMd = "";
161259
162170
  try {
161260
162171
  skillMd = await readFile23(skillMdPath, "utf-8");
@@ -161462,7 +162373,7 @@ function normalizeEntry(entry) {
161462
162373
  };
161463
162374
  }
161464
162375
  function getProjectSettingsPath(rootDir) {
161465
- return join52(rootDir, ".fusion", "settings.json");
162376
+ return join53(rootDir, ".fusion", "settings.json");
161466
162377
  }
161467
162378
  var MIN_PUBLIC_SEARCH_QUERY_LENGTH;
161468
162379
  var init_skills_adapter = __esm({
@@ -161705,33 +162616,33 @@ var init_port_prompt = __esm({
161705
162616
  });
161706
162617
 
161707
162618
  // src/commands/provider-settings.ts
161708
- import { existsSync as existsSync35, readFileSync as readFileSync14, writeFileSync as writeFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
161709
- import { join as join53, dirname as dirname21, basename as basename15 } from "node:path";
162619
+ import { existsSync as existsSync36, readFileSync as readFileSync15, writeFileSync as writeFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
162620
+ import { join as join54, dirname as dirname21, basename as basename15 } from "node:path";
161710
162621
  function siblingAgentDir2(agentDir, siblingRoot) {
161711
162622
  if (basename15(agentDir) !== "agent") {
161712
162623
  return void 0;
161713
162624
  }
161714
- return join53(dirname21(dirname21(agentDir)), siblingRoot, "agent");
162625
+ return join54(dirname21(dirname21(agentDir)), siblingRoot, "agent");
161715
162626
  }
161716
162627
  function readJsonObject4(path5) {
161717
- if (!existsSync35(path5)) {
162628
+ if (!existsSync36(path5)) {
161718
162629
  return {};
161719
162630
  }
161720
162631
  try {
161721
- const parsed = JSON.parse(readFileSync14(path5, "utf-8"));
162632
+ const parsed = JSON.parse(readFileSync15(path5, "utf-8"));
161722
162633
  return parsed !== null && typeof parsed === "object" ? parsed : {};
161723
162634
  } catch {
161724
162635
  return {};
161725
162636
  }
161726
162637
  }
161727
162638
  function createReadOnlyProviderSettingsView(cwd, agentDir) {
161728
- const fusionAgentDir = agentDir.includes(`${join53(".fusion", "agent")}`) ? agentDir : siblingAgentDir2(agentDir, ".fusion");
161729
- const legacyAgentDir = agentDir.includes(`${join53(".pi", "agent")}`) ? agentDir : siblingAgentDir2(agentDir, ".pi");
161730
- const legacyGlobalSettings = legacyAgentDir ? readJsonObject4(join53(legacyAgentDir, "settings.json")) : {};
161731
- const fusionGlobalSettings = fusionAgentDir ? readJsonObject4(join53(fusionAgentDir, "settings.json")) : {};
161732
- const directGlobalSettings = readJsonObject4(join53(agentDir, "settings.json"));
162639
+ const fusionAgentDir = agentDir.includes(`${join54(".fusion", "agent")}`) ? agentDir : siblingAgentDir2(agentDir, ".fusion");
162640
+ const legacyAgentDir = agentDir.includes(`${join54(".pi", "agent")}`) ? agentDir : siblingAgentDir2(agentDir, ".pi");
162641
+ const legacyGlobalSettings = legacyAgentDir ? readJsonObject4(join54(legacyAgentDir, "settings.json")) : {};
162642
+ const fusionGlobalSettings = fusionAgentDir ? readJsonObject4(join54(fusionAgentDir, "settings.json")) : {};
162643
+ const directGlobalSettings = readJsonObject4(join54(agentDir, "settings.json"));
161733
162644
  const globalSettings = { ...legacyGlobalSettings, ...directGlobalSettings, ...fusionGlobalSettings };
161734
- const fusionProjectSettings = readJsonObject4(join53(cwd, ".fusion", "settings.json"));
162645
+ const fusionProjectSettings = readJsonObject4(join54(cwd, ".fusion", "settings.json"));
161735
162646
  const mergedSettings = { ...globalSettings, ...fusionProjectSettings };
161736
162647
  return {
161737
162648
  getGlobalSettings: () => structuredClone(globalSettings),
@@ -161746,7 +162657,6 @@ var init_provider_settings = __esm({
161746
162657
  });
161747
162658
 
161748
162659
  // src/commands/provider-auth.ts
161749
- import { existsSync as existsSync36, readFileSync as readFileSync15 } from "node:fs";
161750
162660
  import { getOAuthProvider as getOAuthProvider2 } from "@mariozechner/pi-ai/oauth";
161751
162661
  function getProviderDisplayName(providerId) {
161752
162662
  const knownProviderNames = new Map(
@@ -161762,7 +162672,10 @@ function wrapAuthStorageWithApiKeyProviders(authStorage, modelRegistry, readFall
161762
162672
  reload: () => mergedAuthStorage.reload(),
161763
162673
  getOAuthProviders: () => mergedAuthStorage.getOAuthProviders().filter((provider) => !OAUTH_TO_API_KEY_RECLASSIFICATIONS.has(provider.id)).map((provider) => ({ id: provider.id, name: provider.name })),
161764
162674
  hasAuth: (provider) => mergedAuthStorage.hasAuth(provider),
161765
- login: (providerId, callbacks) => mergedAuthStorage.login(providerId, callbacks),
162675
+ login: (providerId, callbacks) => mergedAuthStorage.login(
162676
+ providerId,
162677
+ callbacks
162678
+ ),
161766
162679
  logout: (provider) => mergedAuthStorage.logout(provider),
161767
162680
  getApiKeyProviders: () => {
161768
162681
  const oauthProviderIds = new Set(
@@ -161801,13 +162714,28 @@ function wrapAuthStorageWithApiKeyProviders(authStorage, modelRegistry, readFall
161801
162714
  }
161802
162715
  function mergeAuthStorageReads(authStorage, readFallbackAuthStorages = []) {
161803
162716
  const readAuthStorages = [authStorage, ...readFallbackAuthStorages];
161804
- const getCredential = (providerId) => {
161805
- for (const storage of readAuthStorages) {
161806
- const credential = storage.get(providerId);
161807
- if (credential) return credential;
162717
+ const selectCredential = (providerId, storages) => {
162718
+ let best;
162719
+ for (const storage of storages) {
162720
+ best = choosePreferredStoredCredential(best, storage.get(providerId));
161808
162721
  }
161809
- return void 0;
162722
+ return best;
161810
162723
  };
162724
+ const getCredential = (providerId) => selectCredential(providerId, readAuthStorages);
162725
+ const syncFallbackOauthCredentials = () => {
162726
+ const providerIds = new Set(readFallbackAuthStorages.flatMap((storage) => storage.list()));
162727
+ for (const providerId of providerIds) {
162728
+ const current = authStorage.get(providerId);
162729
+ const candidate = selectCredential(providerId, readFallbackAuthStorages);
162730
+ if (!shouldHydrateStoredCredential(current, candidate)) {
162731
+ continue;
162732
+ }
162733
+ if (candidate && (candidate.type === "oauth" || candidate.type === "api_key")) {
162734
+ authStorage.set(providerId, candidate);
162735
+ }
162736
+ }
162737
+ };
162738
+ syncFallbackOauthCredentials();
161811
162739
  return new Proxy(authStorage, {
161812
162740
  get(target, prop, receiver) {
161813
162741
  if (prop === "reload") {
@@ -161815,6 +162743,7 @@ function mergeAuthStorageReads(authStorage, readFallbackAuthStorages = []) {
161815
162743
  for (const storage of readAuthStorages) {
161816
162744
  storage.reload();
161817
162745
  }
162746
+ syncFallbackOauthCredentials();
161818
162747
  };
161819
162748
  }
161820
162749
  if (prop === "get") {
@@ -161827,13 +162756,17 @@ function mergeAuthStorageReads(authStorage, readFallbackAuthStorages = []) {
161827
162756
  return (provider) => readAuthStorages.some((storage) => storage.hasAuth(provider));
161828
162757
  }
161829
162758
  if (prop === "getAll") {
161830
- return () => ({
161831
- ...readFallbackAuthStorages.reduce(
161832
- (merged, storage) => ({ ...merged, ...storage.getAll() }),
161833
- {}
161834
- ),
161835
- ...target.getAll()
161836
- });
162759
+ return () => {
162760
+ const providerIds = new Set(readAuthStorages.flatMap((storage) => storage.list()));
162761
+ const merged = {};
162762
+ for (const providerId of providerIds) {
162763
+ const credential = getCredential(providerId);
162764
+ if (credential) {
162765
+ merged[providerId] = credential;
162766
+ }
162767
+ }
162768
+ return merged;
162769
+ };
161837
162770
  }
161838
162771
  if (prop === "list") {
161839
162772
  return () => Array.from(new Set(readAuthStorages.flatMap((storage) => storage.list())));
@@ -161875,15 +162808,9 @@ function createReadOnlyAuthFileStorage(authPaths) {
161875
162808
  const reload = () => {
161876
162809
  const nextCredentials = {};
161877
162810
  for (const authPath of authPaths) {
161878
- if (!existsSync36(authPath)) {
161879
- continue;
161880
- }
161881
- try {
161882
- const parsed = JSON.parse(readFileSync15(authPath, "utf-8"));
161883
- for (const [provider, credential] of Object.entries(parsed)) {
161884
- nextCredentials[provider] ??= credential;
161885
- }
161886
- } catch {
162811
+ const parsed = readStoredCredentialsFromAuthFile(authPath);
162812
+ for (const [provider, credential] of Object.entries(parsed)) {
162813
+ nextCredentials[provider] = choosePreferredStoredCredential(nextCredentials[provider], credential) ?? credential;
161887
162814
  }
161888
162815
  }
161889
162816
  credentials = nextCredentials;
@@ -161904,6 +162831,7 @@ var OAUTH_TO_API_KEY_RECLASSIFICATIONS, BUILT_IN_API_KEY_PROVIDERS, CLI_PROVIDER
161904
162831
  var init_provider_auth = __esm({
161905
162832
  "src/commands/provider-auth.ts"() {
161906
162833
  "use strict";
162834
+ init_src();
161907
162835
  OAUTH_TO_API_KEY_RECLASSIFICATIONS = /* @__PURE__ */ new Set([
161908
162836
  "anthropic"
161909
162837
  ]);
@@ -161921,34 +162849,37 @@ var init_provider_auth = __esm({
161921
162849
  });
161922
162850
 
161923
162851
  // src/commands/auth-paths.ts
161924
- import { homedir as homedir9 } from "node:os";
162852
+ import { homedir as homedir10 } from "node:os";
161925
162853
  import { existsSync as existsSync37, readFileSync as readFileSync16 } from "node:fs";
161926
- import { join as join54 } from "node:path";
161927
- function getFusionAgentDir3(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
161928
- return join54(home, ".fusion", "agent");
162854
+ import { join as join55 } from "node:path";
162855
+ function getFusionAgentDir3(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
162856
+ return join55(home, ".fusion", "agent");
162857
+ }
162858
+ function getLegacyAgentDir(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
162859
+ return join55(home, ".pi", "agent");
161929
162860
  }
161930
- function getLegacyAgentDir(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
161931
- return join54(home, ".pi", "agent");
162861
+ function getFusionAuthPath3(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
162862
+ return join55(getFusionAgentDir3(home), "auth.json");
161932
162863
  }
161933
- function getFusionAuthPath3(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
161934
- return join54(getFusionAgentDir3(home), "auth.json");
162864
+ function getCodexCliAuthPath2(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
162865
+ return join55(home, ".codex", "auth.json");
161935
162866
  }
161936
- function getLegacyAuthPaths2(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
162867
+ function getLegacyAuthPaths2(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
161937
162868
  return [
161938
- join54(home, ".pi", "agent", "auth.json"),
161939
- join54(home, ".pi", "auth.json")
162869
+ join55(home, ".pi", "agent", "auth.json"),
162870
+ join55(home, ".pi", "auth.json")
161940
162871
  ];
161941
162872
  }
161942
- function getFusionModelsPath2(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
161943
- return join54(getFusionAgentDir3(home), "models.json");
162873
+ function getFusionModelsPath2(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
162874
+ return join55(getFusionAgentDir3(home), "models.json");
161944
162875
  }
161945
- function getLegacyModelsPaths2(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
162876
+ function getLegacyModelsPaths2(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
161946
162877
  return [
161947
- join54(home, ".pi", "agent", "models.json"),
161948
- join54(home, ".pi", "models.json")
162878
+ join55(home, ".pi", "agent", "models.json"),
162879
+ join55(home, ".pi", "models.json")
161949
162880
  ];
161950
162881
  }
161951
- function getModelRegistryModelsPath2(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
162882
+ function getModelRegistryModelsPath2(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
161952
162883
  const fusionModelsPath = getFusionModelsPath2(home);
161953
162884
  if (existsSync37(fusionModelsPath)) {
161954
162885
  return fusionModelsPath;
@@ -161969,11 +162900,11 @@ function readJsonObject5(path5) {
161969
162900
  function hasPackageManagerSettings3(settings) {
161970
162901
  return Array.isArray(settings.packages) || Array.isArray(settings.npmCommand);
161971
162902
  }
161972
- function getPackageManagerAgentDir2(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
162903
+ function getPackageManagerAgentDir2(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
161973
162904
  const fusionAgentDir = getFusionAgentDir3(home);
161974
162905
  const legacyAgentDir = getLegacyAgentDir(home);
161975
- const fusionSettings = readJsonObject5(join54(fusionAgentDir, "settings.json"));
161976
- const legacySettings = readJsonObject5(join54(legacyAgentDir, "settings.json"));
162906
+ const fusionSettings = readJsonObject5(join55(fusionAgentDir, "settings.json"));
162907
+ const legacySettings = readJsonObject5(join55(legacyAgentDir, "settings.json"));
161977
162908
  if (hasPackageManagerSettings3(fusionSettings) || !existsSync37(legacyAgentDir)) {
161978
162909
  return fusionAgentDir;
161979
162910
  }
@@ -162149,7 +163080,7 @@ import {
162149
163080
  symlinkSync,
162150
163081
  unlinkSync
162151
163082
  } from "node:fs";
162152
- import { dirname as dirname23, join as join55, resolve as resolve29 } from "node:path";
163083
+ import { dirname as dirname23, join as join56, resolve as resolve29 } from "node:path";
162153
163084
  import { fileURLToPath as fileURLToPath6 } from "node:url";
162154
163085
  function isPiClaudeCliConfigured(globalSettings) {
162155
163086
  if (!globalSettings || typeof globalSettings !== "object") {
@@ -162173,7 +163104,7 @@ function resolveFusionSkillSource() {
162173
163104
  return existsSync38(candidate) ? candidate : null;
162174
163105
  }
162175
163106
  function installFusionSkillIntoProject(projectPath, options = {}) {
162176
- const target = join55(projectPath, ".claude", "skills", FUSION_SKILL_NAME);
163107
+ const target = join56(projectPath, ".claude", "skills", FUSION_SKILL_NAME);
162177
163108
  if (options.enabled === false) {
162178
163109
  return { outcome: "skipped", target, reason: "pi-claude-cli not configured" };
162179
163110
  }
@@ -162198,7 +163129,7 @@ function installFusionSkillIntoProject(projectPath, options = {}) {
162198
163129
  unlinkSync(target);
162199
163130
  replaced = true;
162200
163131
  } else {
162201
- const skillMd = join55(target, "SKILL.md");
163132
+ const skillMd = join56(target, "SKILL.md");
162202
163133
  if (!existsSync38(skillMd)) {
162203
163134
  return {
162204
163135
  outcome: "failed",
@@ -162245,7 +163176,7 @@ function ensureFusionSkillForProjects(projects, options = { enabled: false }) {
162245
163176
  if (!options.enabled) {
162246
163177
  return projects.map((p) => ({
162247
163178
  outcome: "skipped",
162248
- target: join55(p.path, ".claude", "skills", FUSION_SKILL_NAME),
163179
+ target: join56(p.path, ".claude", "skills", FUSION_SKILL_NAME),
162249
163180
  reason: "pi-claude-cli not configured"
162250
163181
  }));
162251
163182
  }
@@ -162548,10 +163479,10 @@ var init_droid_cli_extension = __esm({
162548
163479
 
162549
163480
  // src/update-cache.ts
162550
163481
  import { readFileSync as readFileSync19 } from "node:fs";
162551
- import { join as join56 } from "node:path";
163482
+ import { join as join57 } from "node:path";
162552
163483
  function getCachedUpdateStatus(currentVersion) {
162553
163484
  try {
162554
- const cachePath = join56(resolveGlobalDir(), "update-check.json");
163485
+ const cachePath = join57(resolveGlobalDir(), "update-check.json");
162555
163486
  const raw = readFileSync19(cachePath, "utf-8");
162556
163487
  const parsed = JSON.parse(raw);
162557
163488
  if (parsed.updateAvailable === true && typeof parsed.latestVersion === "string" && parsed.latestVersion.length > 0 && typeof parsed.currentVersion === "string" && parsed.currentVersion.length > 0) {
@@ -162641,19 +163572,19 @@ var init_self_extension = __esm({
162641
163572
  // src/plugins/bundled-plugin-install.ts
162642
163573
  import { existsSync as existsSync42 } from "node:fs";
162643
163574
  import { readFile as readFile24 } from "node:fs/promises";
162644
- import { dirname as dirname27, join as join57, resolve as resolve33 } from "node:path";
163575
+ import { dirname as dirname27, join as join58, resolve as resolve33 } from "node:path";
162645
163576
  import { fileURLToPath as fileURLToPath10 } from "node:url";
162646
163577
  function getCandidatePluginPaths() {
162647
163578
  const moduleDir = dirname27(fileURLToPath10(import.meta.url));
162648
163579
  const cliPackageRoot = resolve33(moduleDir, "..", "..");
162649
163580
  return [
162650
- join57(cliPackageRoot, "dist", "plugins", DEPENDENCY_GRAPH_PLUGIN_ID),
162651
- join57(cliPackageRoot, "plugins", DEPENDENCY_GRAPH_PLUGIN_ID),
162652
- join57(cliPackageRoot, "..", "..", "plugins", DEPENDENCY_GRAPH_PLUGIN_ID)
163581
+ join58(cliPackageRoot, "dist", "plugins", DEPENDENCY_GRAPH_PLUGIN_ID),
163582
+ join58(cliPackageRoot, "plugins", DEPENDENCY_GRAPH_PLUGIN_ID),
163583
+ join58(cliPackageRoot, "..", "..", "plugins", DEPENDENCY_GRAPH_PLUGIN_ID)
162653
163584
  ];
162654
163585
  }
162655
163586
  async function loadManifest(pluginDir) {
162656
- const manifestPath = join57(pluginDir, "manifest.json");
163587
+ const manifestPath = join58(pluginDir, "manifest.json");
162657
163588
  const content = await readFile24(manifestPath, "utf-8");
162658
163589
  const manifest = JSON.parse(content);
162659
163590
  const validation = validatePluginManifest(manifest);
@@ -162664,7 +163595,7 @@ async function loadManifest(pluginDir) {
162664
163595
  }
162665
163596
  function resolveBundledDependencyGraphPath() {
162666
163597
  for (const path5 of getCandidatePluginPaths()) {
162667
- if (existsSync42(join57(path5, "manifest.json"))) {
163598
+ if (existsSync42(join58(path5, "manifest.json"))) {
162668
163599
  return path5;
162669
163600
  }
162670
163601
  }
@@ -167093,7 +168024,7 @@ __export(dashboard_exports, {
167093
168024
  promptForPort: () => promptForPort,
167094
168025
  runDashboard: () => runDashboard
167095
168026
  });
167096
- import { dirname as dirname28, join as join58, resolve as pathResolve } from "node:path";
168027
+ import { dirname as dirname28, join as join59, resolve as pathResolve } from "node:path";
167097
168028
  import { execFile as execFileCb } from "node:child_process";
167098
168029
  import { promisify as promisify16 } from "node:util";
167099
168030
  import { stat as stat11, readdir as readdir11, readFile as fsReadFile3 } from "node:fs/promises";
@@ -167431,7 +168362,7 @@ async function buildFileListDirectory(projectPath, relativePath) {
167431
168362
  let size = 0;
167432
168363
  let modifiedAt = (/* @__PURE__ */ new Date(0)).toISOString();
167433
168364
  try {
167434
- const s = await stat11(join58(absDir, d.name));
168365
+ const s = await stat11(join59(absDir, d.name));
167435
168366
  size = d.isDirectory() ? 0 : s.size;
167436
168367
  modifiedAt = s.mtime.toISOString();
167437
168368
  } catch {
@@ -167843,8 +168774,11 @@ async function runDashboard(port, opts = {}) {
167843
168774
  rootDir: cwd
167844
168775
  });
167845
168776
  const authStorage = AuthStorage2.create(getFusionAuthPath3());
167846
- const legacyAuthStorage = createReadOnlyAuthFileStorage(getLegacyAuthPaths2());
167847
- const mergedAuthStorage = mergeAuthStorageReads(authStorage, [legacyAuthStorage]);
168777
+ const supplementalAuthStorage = createReadOnlyAuthFileStorage([
168778
+ ...getLegacyAuthPaths2(),
168779
+ getCodexCliAuthPath2()
168780
+ ]);
168781
+ const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
167848
168782
  const modelRegistry = ModelRegistry3.create(mergedAuthStorage, getModelRegistryModelsPath2());
167849
168783
  const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);
167850
168784
  let packageManager;
@@ -167906,7 +168840,7 @@ async function runDashboard(port, opts = {}) {
167906
168840
  ...droidCliPaths
167907
168841
  ],
167908
168842
  cwd,
167909
- join58(cwd, ".fusion", "disabled-auto-extension-discovery")
168843
+ join59(cwd, ".fusion", "disabled-auto-extension-discovery")
167910
168844
  );
167911
168845
  for (const { path: path5, error } of extensionsResult.errors) {
167912
168846
  logSink.log(`Failed to load ${path5}: ${error}`, "extensions");
@@ -169429,7 +170363,7 @@ var serve_exports = {};
169429
170363
  __export(serve_exports, {
169430
170364
  runServe: () => runServe
169431
170365
  });
169432
- import { dirname as dirname29, join as join59 } from "node:path";
170366
+ import { dirname as dirname29, join as join60 } from "node:path";
169433
170367
  import {
169434
170368
  AuthStorage as AuthStorage3,
169435
170369
  DefaultPackageManager as DefaultPackageManager3,
@@ -169682,8 +170616,11 @@ async function runServe(port, opts = {}) {
169682
170616
  const missionExecutionLoop = cwdEngine.getRuntime().getMissionExecutionLoop();
169683
170617
  const automationStore = cwdEngine.getAutomationStore();
169684
170618
  const authStorage = AuthStorage3.create(getFusionAuthPath3());
169685
- const legacyAuthStorage = createReadOnlyAuthFileStorage(getLegacyAuthPaths2());
169686
- const mergedAuthStorage = mergeAuthStorageReads(authStorage, [legacyAuthStorage]);
170619
+ const supplementalAuthStorage = createReadOnlyAuthFileStorage([
170620
+ ...getLegacyAuthPaths2(),
170621
+ getCodexCliAuthPath2()
170622
+ ]);
170623
+ const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
169687
170624
  const modelRegistry = ModelRegistry4.create(mergedAuthStorage, getModelRegistryModelsPath2());
169688
170625
  const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);
169689
170626
  let packageManager;
@@ -169745,7 +170682,7 @@ async function runServe(port, opts = {}) {
169745
170682
  ...droidCliPaths
169746
170683
  ],
169747
170684
  cwd,
169748
- join59(cwd, ".fusion", "disabled-auto-extension-discovery")
170685
+ join60(cwd, ".fusion", "disabled-auto-extension-discovery")
169749
170686
  );
169750
170687
  for (const { path: path5, error } of extensionsResult.errors) {
169751
170688
  console.log(`[extensions] Failed to load ${path5}: ${error}`);
@@ -170100,7 +171037,7 @@ var daemon_exports = {};
170100
171037
  __export(daemon_exports, {
170101
171038
  runDaemon: () => runDaemon
170102
171039
  });
170103
- import { join as join60 } from "node:path";
171040
+ import { join as join61 } from "node:path";
170104
171041
  import {
170105
171042
  AuthStorage as AuthStorage4,
170106
171043
  DefaultPackageManager as DefaultPackageManager4,
@@ -170346,8 +171283,11 @@ async function runDaemon(opts = {}) {
170346
171283
  const missionExecutionLoop = cwdEngine.getRuntime().getMissionExecutionLoop();
170347
171284
  const automationStore = cwdEngine.getAutomationStore();
170348
171285
  const authStorage = AuthStorage4.create(getFusionAuthPath3());
170349
- const legacyAuthStorage = createReadOnlyAuthFileStorage(getLegacyAuthPaths2());
170350
- const mergedAuthStorage = mergeAuthStorageReads(authStorage, [legacyAuthStorage]);
171286
+ const supplementalAuthStorage = createReadOnlyAuthFileStorage([
171287
+ ...getLegacyAuthPaths2(),
171288
+ getCodexCliAuthPath2()
171289
+ ]);
171290
+ const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
170351
171291
  const modelRegistry = ModelRegistry5.create(mergedAuthStorage, getModelRegistryModelsPath2());
170352
171292
  const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);
170353
171293
  let packageManager;
@@ -170407,7 +171347,7 @@ async function runDaemon(opts = {}) {
170407
171347
  const extensionsResult = await discoverAndLoadExtensions4(
170408
171348
  [...reconciledExtensionPaths, ...droidCliPaths],
170409
171349
  cwd,
170410
- join60(cwd, ".fusion", "disabled-auto-extension-discovery")
171350
+ join61(cwd, ".fusion", "disabled-auto-extension-discovery")
170411
171351
  );
170412
171352
  for (const { path: path5, error } of extensionsResult.errors) {
170413
171353
  console.log(`[extensions] Failed to load ${path5}: ${error}`);
@@ -170628,7 +171568,7 @@ __export(desktop_exports, {
170628
171568
  });
170629
171569
  import { spawn as spawn18 } from "node:child_process";
170630
171570
  import { once as once2 } from "node:events";
170631
- import { join as join61 } from "node:path";
171571
+ import { join as join62 } from "node:path";
170632
171572
  import { createRequire as createRequire6 } from "node:module";
170633
171573
  function runCommand(command, args, cwd) {
170634
171574
  return new Promise((resolve44, reject2) => {
@@ -170707,7 +171647,7 @@ async function runDesktop(options = {}) {
170707
171647
  }
170708
171648
  const runtime = await startDashboardRuntime(rootDir, Boolean(options.paused));
170709
171649
  const electronBinary = resolveElectronBinary();
170710
- const desktopEntry = join61(rootDir, "packages", "desktop", "dist", "main.js");
171650
+ const desktopEntry = join62(rootDir, "packages", "desktop", "dist", "main.js");
170711
171651
  const electronArgs = ["--enable-source-maps", desktopEntry, ...options.dev ? ["--dev"] : []];
170712
171652
  const electronEnv = {
170713
171653
  ...process.env,
@@ -170793,7 +171733,7 @@ __export(task_exports, {
170793
171733
  });
170794
171734
  import { createInterface as createInterface3 } from "node:readline/promises";
170795
171735
  import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync43, readFileSync as readFileSync21 } from "node:fs";
170796
- import { basename as basename17, join as join62 } from "node:path";
171736
+ import { basename as basename17, join as join63 } from "node:path";
170797
171737
  function getGitHubIssueUrl(sourceMetadata) {
170798
171738
  if (!sourceMetadata || typeof sourceMetadata !== "object") return void 0;
170799
171739
  const issueUrl = sourceMetadata.issueUrl;
@@ -171106,7 +172046,7 @@ async function runTaskLogs(id, options = {}, projectName) {
171106
172046
  printEntries(filteredEntries);
171107
172047
  if (options.follow) {
171108
172048
  const projectPath = projectContext?.projectPath ?? process.cwd();
171109
- const logPath = join62(projectPath, ".fusion", "tasks", id, "agent.log");
172049
+ const logPath = join63(projectPath, ".fusion", "tasks", id, "agent.log");
171110
172050
  if (!existsSync43(logPath)) {
171111
172051
  console.log(`
171112
172052
  Waiting for log file to be created...`);
@@ -172384,7 +173324,7 @@ __export(settings_export_exports, {
172384
173324
  runSettingsExport: () => runSettingsExport
172385
173325
  });
172386
173326
  import { writeFile as writeFile18 } from "node:fs/promises";
172387
- import { resolve as resolve34, join as join63 } from "node:path";
173327
+ import { resolve as resolve34, join as join64 } from "node:path";
172388
173328
  async function runSettingsExport(options = {}) {
172389
173329
  const scope = options.scope ?? "both";
172390
173330
  const project = options.projectName ? await resolveProject(options.projectName) : void 0;
@@ -172398,7 +173338,7 @@ async function runSettingsExport(options = {}) {
172398
173338
  targetPath = resolve34(outputPath);
172399
173339
  } else {
172400
173340
  const filename = generateExportFilename();
172401
- targetPath = join63(process.cwd(), filename);
173341
+ targetPath = join64(process.cwd(), filename);
172402
173342
  }
172403
173343
  const jsonContent = JSON.stringify(exportData, null, 2);
172404
173344
  await writeFile18(targetPath, jsonContent);
@@ -173972,14 +174912,14 @@ var init_project = __esm({
173972
174912
 
173973
174913
  // src/commands/skill-installation.ts
173974
174914
  import { cpSync as cpSync2, existsSync as existsSync47, mkdirSync as mkdirSync8 } from "node:fs";
173975
- import { homedir as homedir10 } from "node:os";
173976
- import { dirname as dirname31, join as join64, resolve as resolve38 } from "node:path";
174915
+ import { homedir as homedir11 } from "node:os";
174916
+ import { dirname as dirname31, join as join65, resolve as resolve38 } from "node:path";
173977
174917
  import { fileURLToPath as fileURLToPath11 } from "node:url";
173978
- function getSupportedSkillInstallTargets(homeDir = process.env.HOME || process.env.USERPROFILE || homedir10()) {
174918
+ function getSupportedSkillInstallTargets(homeDir = process.env.HOME || process.env.USERPROFILE || homedir11()) {
173979
174919
  return [
173980
- { client: "claude", targetDir: join64(homeDir, ".claude", "skills", FUSION_SKILL_NAME2) },
173981
- { client: "codex", targetDir: join64(homeDir, ".codex", "skills", FUSION_SKILL_NAME2) },
173982
- { client: "gemini", targetDir: join64(homeDir, ".gemini", "skills", FUSION_SKILL_NAME2) }
174920
+ { client: "claude", targetDir: join65(homeDir, ".claude", "skills", FUSION_SKILL_NAME2) },
174921
+ { client: "codex", targetDir: join65(homeDir, ".codex", "skills", FUSION_SKILL_NAME2) },
174922
+ { client: "gemini", targetDir: join65(homeDir, ".gemini", "skills", FUSION_SKILL_NAME2) }
173983
174923
  ];
173984
174924
  }
173985
174925
  function resolveBundledFusionSkillSource() {
@@ -174043,13 +174983,13 @@ __export(init_exports, {
174043
174983
  runInit: () => runInit
174044
174984
  });
174045
174985
  import { existsSync as existsSync48, mkdirSync as mkdirSync9, writeFileSync as writeFileSync3, readFileSync as readFileSync22 } from "node:fs";
174046
- import { join as join65, resolve as resolve39, basename as basename20 } from "node:path";
174986
+ import { join as join66, resolve as resolve39, basename as basename20 } from "node:path";
174047
174987
  import { exec as exec13 } from "node:child_process";
174048
174988
  import { promisify as promisify18 } from "node:util";
174049
174989
  async function runInit(options = {}) {
174050
174990
  const cwd = options.path ? resolve39(options.path) : process.cwd();
174051
- const fusionDir = join65(cwd, ".fusion");
174052
- const dbPath = join65(fusionDir, "fusion.db");
174991
+ const fusionDir = join66(cwd, ".fusion");
174992
+ const dbPath = join66(fusionDir, "fusion.db");
174053
174993
  const hasDbPath = existsSync48(dbPath);
174054
174994
  const hasValidDb = hasDbPath && isValidSqliteDatabaseFile(dbPath);
174055
174995
  if (existsSync48(fusionDir) && hasDbPath && hasValidDb) {
@@ -174145,7 +175085,7 @@ async function runInit(options = {}) {
174145
175085
  }
174146
175086
  }
174147
175087
  async function detectProjectName(dir2) {
174148
- if (!existsSync48(join65(dir2, ".git"))) {
175088
+ if (!existsSync48(join66(dir2, ".git"))) {
174149
175089
  return basename20(dir2) || "my-project";
174150
175090
  }
174151
175091
  try {
@@ -174165,7 +175105,7 @@ async function detectProjectName(dir2) {
174165
175105
  return basename20(dir2) || "my-project";
174166
175106
  }
174167
175107
  async function addLocalStorageToGitignore(cwd) {
174168
- const gitignorePath = join65(cwd, ".gitignore");
175108
+ const gitignorePath = join66(cwd, ".gitignore");
174169
175109
  let content = "";
174170
175110
  if (existsSync48(gitignorePath)) {
174171
175111
  try {
@@ -174208,7 +175148,7 @@ async function initializeGitRepo(cwd) {
174208
175148
  }
174209
175149
  await ensureGitConfig(cwd, "user.name", "Fusion");
174210
175150
  await ensureGitConfig(cwd, "user.email", "noreply@runfusion.ai");
174211
- const gitkeepPath = join65(cwd, ".gitkeep");
175151
+ const gitkeepPath = join66(cwd, ".gitkeep");
174212
175152
  if (!existsSync48(gitkeepPath)) {
174213
175153
  writeFileSync3(gitkeepPath, "\n");
174214
175154
  }
@@ -174875,7 +175815,7 @@ __export(plugin_exports, {
174875
175815
  runPluginUninstall: () => runPluginUninstall
174876
175816
  });
174877
175817
  import { existsSync as existsSync50 } from "node:fs";
174878
- import { join as join66 } from "node:path";
175818
+ import { join as join67 } from "node:path";
174879
175819
  import { readFile as readFile25 } from "node:fs/promises";
174880
175820
  import * as readline from "node:readline";
174881
175821
  async function getProjectPath6(projectName) {
@@ -174913,7 +175853,7 @@ async function createPluginLoader(pluginStore, projectName) {
174913
175853
  return { store: pluginStore, loader };
174914
175854
  }
174915
175855
  async function loadManifestFromPath(pluginPath) {
174916
- const manifestPath = join66(pluginPath, "manifest.json");
175856
+ const manifestPath = join67(pluginPath, "manifest.json");
174917
175857
  if (!existsSync50(manifestPath)) {
174918
175858
  throw new Error(`Plugin manifest not found at: ${manifestPath}`);
174919
175859
  }
@@ -175105,7 +176045,7 @@ __export(plugin_scaffold_exports, {
175105
176045
  runPluginCreate: () => runPluginCreate
175106
176046
  });
175107
176047
  import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync5, existsSync as existsSync51 } from "node:fs";
175108
- import { join as join67 } from "node:path";
176048
+ import { join as join68 } from "node:path";
175109
176049
  function toTitleCase(str) {
175110
176050
  return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
175111
176051
  }
@@ -175238,7 +176178,7 @@ async function runPluginCreate(name, options) {
175238
176178
  process.exit(1);
175239
176179
  }
175240
176180
  const targetDir = options?.output ?? name;
175241
- const targetPath = join67(process.cwd(), targetDir);
176181
+ const targetPath = join68(process.cwd(), targetDir);
175242
176182
  if (existsSync51(targetPath)) {
175243
176183
  console.error(`Error: Directory '${targetDir}' already exists.`);
175244
176184
  console.error("Please choose a different name or remove the existing directory.");
@@ -175246,16 +176186,16 @@ async function runPluginCreate(name, options) {
175246
176186
  }
175247
176187
  try {
175248
176188
  mkdirSync11(targetPath, { recursive: true });
175249
- mkdirSync11(join67(targetPath, "src", "__tests__"), { recursive: true });
175250
- writeFileSync5(join67(targetPath, "package.json"), generatePackageJson(name));
175251
- writeFileSync5(join67(targetPath, "tsconfig.json"), generateTsconfig());
175252
- writeFileSync5(join67(targetPath, "vitest.config.ts"), generateVitestConfig());
175253
- writeFileSync5(join67(targetPath, "src", "index.ts"), generateIndexTs(name));
176189
+ mkdirSync11(join68(targetPath, "src", "__tests__"), { recursive: true });
176190
+ writeFileSync5(join68(targetPath, "package.json"), generatePackageJson(name));
176191
+ writeFileSync5(join68(targetPath, "tsconfig.json"), generateTsconfig());
176192
+ writeFileSync5(join68(targetPath, "vitest.config.ts"), generateVitestConfig());
176193
+ writeFileSync5(join68(targetPath, "src", "index.ts"), generateIndexTs(name));
175254
176194
  writeFileSync5(
175255
- join67(targetPath, "src", "__tests__", "index.test.ts"),
176195
+ join68(targetPath, "src", "__tests__", "index.test.ts"),
175256
176196
  generateTestTs(name)
175257
176197
  );
175258
- writeFileSync5(join67(targetPath, "README.md"), generateReadme(name));
176198
+ writeFileSync5(join68(targetPath, "README.md"), generateReadme(name));
175259
176199
  } catch (err) {
175260
176200
  console.error(
175261
176201
  `Error creating plugin files: ${err instanceof Error ? err.message : String(err)}`
@@ -175407,7 +176347,7 @@ __export(research_exports, {
175407
176347
  runResearchShow: () => runResearchShow
175408
176348
  });
175409
176349
  import { writeFile as writeFile19 } from "node:fs/promises";
175410
- import { join as join68, resolve as resolve42 } from "node:path";
176350
+ import { join as join69, resolve as resolve42 } from "node:path";
175411
176351
  async function getStore3(projectName) {
175412
176352
  const project = projectName ? await resolveProject(projectName) : void 0;
175413
176353
  const store = new TaskStore(project?.projectPath ?? process.cwd());
@@ -175563,7 +176503,7 @@ async function runResearchExport(options) {
175563
176503
  }
175564
176504
  const content = format === "json" ? JSON.stringify(run, null, 2) : renderMarkdown(run);
175565
176505
  const ext = format === "json" ? "json" : "md";
175566
- const outputPath = options.output ? resolve42(options.output) : join68(process.cwd(), `research-${run.id.toLowerCase()}.${ext}`);
176506
+ const outputPath = options.output ? resolve42(options.output) : join69(process.cwd(), `research-${run.id.toLowerCase()}.${ext}`);
175567
176507
  await writeFile19(outputPath, content, "utf8");
175568
176508
  store.getResearchStore().createExport(run.id, format, content);
175569
176509
  if (options.json) {
@@ -175628,7 +176568,7 @@ __export(native_patch_exports, {
175628
176568
  isTerminalAvailable: () => isTerminalAvailable,
175629
176569
  setupNativeResolution: () => setupNativeResolution
175630
176570
  });
175631
- import { join as join69, basename as basename21, dirname as dirname32 } from "node:path";
176571
+ import { join as join70, basename as basename21, dirname as dirname32 } from "node:path";
175632
176572
  import { existsSync as existsSync52, copyFileSync, mkdirSync as mkdirSync12, symlinkSync as symlinkSync2, rmSync as rmSync5, lstatSync as lstatSync3, readlinkSync as readlinkSync2 } from "node:fs";
175633
176573
  import { tmpdir as tmpdir5 } from "node:os";
175634
176574
  function findStagedNativeDir2() {
@@ -175636,13 +176576,13 @@ function findStagedNativeDir2() {
175636
176576
  const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : "unknown";
175637
176577
  const prebuildName = `${platform4}-${arch}`;
175638
176578
  const execDir = dirname32(process.execPath);
175639
- const nextToBinary = join69(execDir, "runtime", prebuildName);
175640
- if (existsSync52(join69(nextToBinary, "pty.node"))) {
176579
+ const nextToBinary = join70(execDir, "runtime", prebuildName);
176580
+ if (existsSync52(join70(nextToBinary, "pty.node"))) {
175641
176581
  return nextToBinary;
175642
176582
  }
175643
176583
  if (process.env.FUSION_RUNTIME_DIR) {
175644
- const envPath = join69(process.env.FUSION_RUNTIME_DIR, prebuildName);
175645
- if (existsSync52(join69(envPath, "pty.node"))) {
176584
+ const envPath = join70(process.env.FUSION_RUNTIME_DIR, prebuildName);
176585
+ if (existsSync52(join70(envPath, "pty.node"))) {
175646
176586
  return envPath;
175647
176587
  }
175648
176588
  }
@@ -175675,17 +176615,17 @@ function setupNativeResolution() {
175675
176615
  process.env.NODE_PTY_SPAWN_HELPER_DIR = nativeDir;
175676
176616
  }
175677
176617
  process.env.FUSION_NATIVE_ASSETS_PATH = nativeDir;
175678
- const tmpRoot = join69(tmpdir5(), `fn-bunfs-${process.pid}`);
175679
- const fnDir = join69(tmpRoot, "fn");
175680
- const prebuildsDir = join69(fnDir, "prebuilds");
175681
- const platformDir = join69(prebuildsDir, basename21(nativeDir));
176618
+ const tmpRoot = join70(tmpdir5(), `fn-bunfs-${process.pid}`);
176619
+ const fnDir = join70(tmpRoot, "fn");
176620
+ const prebuildsDir = join70(fnDir, "prebuilds");
176621
+ const platformDir = join70(prebuildsDir, basename21(nativeDir));
175682
176622
  try {
175683
176623
  cleanupStaleBunfsLinks();
175684
176624
  mkdirSync12(platformDir, { recursive: true });
175685
- const ptyNodeDest = join69(platformDir, "pty.node");
175686
- copyFileSync(join69(nativeDir, "pty.node"), ptyNodeDest);
175687
- if (existsSync52(join69(nativeDir, "spawn-helper"))) {
175688
- copyFileSync(join69(nativeDir, "spawn-helper"), join69(platformDir, "spawn-helper"));
176625
+ const ptyNodeDest = join70(platformDir, "pty.node");
176626
+ copyFileSync(join70(nativeDir, "pty.node"), ptyNodeDest);
176627
+ if (existsSync52(join70(nativeDir, "spawn-helper"))) {
176628
+ copyFileSync(join70(nativeDir, "spawn-helper"), join70(platformDir, "spawn-helper"));
175689
176629
  }
175690
176630
  process.env.FUSION_FAKE_BUNFS_ROOT = tmpRoot;
175691
176631
  if (process.platform !== "win32") {
@@ -175762,7 +176702,7 @@ var init_native_patch = __esm({
175762
176702
  // src/bin.ts
175763
176703
  import { existsSync as existsSync53, mkdtempSync as mkdtempSync2, readFileSync as readFileSync24, symlinkSync as symlinkSync3, writeFileSync as writeFileSync6 } from "node:fs";
175764
176704
  import { createRequire as createRequire7 } from "node:module";
175765
- import { join as join70, dirname as dirname33, resolve as resolve43 } from "node:path";
176705
+ import { join as join71, dirname as dirname33, resolve as resolve43 } from "node:path";
175766
176706
  import { tmpdir as tmpdir6 } from "node:os";
175767
176707
  import { performance as performance3 } from "node:perf_hooks";
175768
176708
  import { fileURLToPath as fileURLToPath12 } from "node:url";
@@ -175771,7 +176711,7 @@ function configurePiPackage() {
175771
176711
  if (process.env.PI_PACKAGE_DIR) {
175772
176712
  return;
175773
176713
  }
175774
- const tmp = mkdtempSync2(join70(tmpdir6(), "fn-pkg-"));
176714
+ const tmp = mkdtempSync2(join71(tmpdir6(), "fn-pkg-"));
175775
176715
  let packageJson = {
175776
176716
  name: "pi",
175777
176717
  version: "0.1.0",
@@ -175783,9 +176723,9 @@ function configurePiPackage() {
175783
176723
  const piPackageDir = dirname33(piPackagePath);
175784
176724
  packageJson = JSON.parse(readFileSync24(piPackagePath, "utf-8"));
175785
176725
  for (const entry of ["dist", "docs", "examples", "README.md", "CHANGELOG.md"]) {
175786
- const source = join70(piPackageDir, entry);
176726
+ const source = join71(piPackageDir, entry);
175787
176727
  if (existsSync53(source)) {
175788
- symlinkSync3(source, join70(tmp, entry));
176728
+ symlinkSync3(source, join71(tmp, entry));
175789
176729
  }
175790
176730
  }
175791
176731
  } catch {
@@ -175794,7 +176734,7 @@ function configurePiPackage() {
175794
176734
  ...packageJson.piConfig ?? {},
175795
176735
  configDir: ".fusion"
175796
176736
  };
175797
- writeFileSync6(join70(tmp, "package.json"), JSON.stringify(packageJson, null, 2) + "\n");
176737
+ writeFileSync6(join71(tmp, "package.json"), JSON.stringify(packageJson, null, 2) + "\n");
175798
176738
  process.env.PI_PACKAGE_DIR = tmp;
175799
176739
  }
175800
176740
  configurePiPackage();
@@ -175822,8 +176762,8 @@ function loadEnvFile(path5) {
175822
176762
  }
175823
176763
  function loadLocalEnv() {
175824
176764
  const cwd = process.cwd();
175825
- loadEnvFile(join70(cwd, ".env"));
175826
- loadEnvFile(join70(cwd, ".env.local"));
176765
+ loadEnvFile(join71(cwd, ".env"));
176766
+ loadEnvFile(join71(cwd, ".env.local"));
175827
176767
  }
175828
176768
  loadLocalEnv();
175829
176769
  async function loadCommandHandlers() {