@runfusion/fusion 0.17.1 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/bin.js +1706 -787
  2. package/dist/client/assets/{AgentDetailView-BmxnuM0D.js → AgentDetailView-17J-F0Rl.js} +1 -1
  3. package/dist/client/assets/{AgentsView-1xSqjJxs.js → AgentsView-sbBkb7Wd.js} +3 -3
  4. package/dist/client/assets/ChatView-BR5cvK_B.js +1 -0
  5. package/dist/client/assets/{DevServerView-DIrmWI5T.js → DevServerView-GFFVXHVP.js} +1 -1
  6. package/dist/client/assets/{DirectoryPicker-Sqwdifcb.js → DirectoryPicker-WPDSBdT6.js} +1 -1
  7. package/dist/client/assets/{DocumentsView-Cx_02o_z.js → DocumentsView-BHpDsIIt.js} +1 -1
  8. package/dist/client/assets/{InsightsView-DAJSq4gV.js → InsightsView-Bxu0TJkt.js} +1 -1
  9. package/dist/client/assets/{MemoryView-CCIBAre3.js → MemoryView-CmnzZorw.js} +1 -1
  10. package/dist/client/assets/{NodesView-D02HxGCl.js → NodesView-CO9_4hCr.js} +1 -1
  11. package/dist/client/assets/{PiExtensionsManager-DD0fTQNf.js → PiExtensionsManager-4e3MlD62.js} +2 -2
  12. package/dist/client/assets/{PluginManager-Cfl0VBX9.js → PluginManager-DGN2rvOY.js} +1 -1
  13. package/dist/client/assets/{ResearchView-B9RqOVbr.js → ResearchView-Dsa6Gykl.js} +1 -1
  14. package/dist/client/assets/{RoadmapsView-DsH7Hicx.js → RoadmapsView-jHTOK0RQ.js} +1 -1
  15. package/dist/client/assets/{SettingsModal-Cn_CIPXu.js → SettingsModal-4Z8ZJMzD.js} +1 -1
  16. package/dist/client/assets/SettingsModal-D0kuJpBA.js +31 -0
  17. package/dist/client/assets/{SetupWizardModal-k5vqrHZU.js → SetupWizardModal-Bhumd4Rf.js} +1 -1
  18. package/dist/client/assets/{SkillsView-BIdt5cfB.js → SkillsView-MHweJTz4.js} +1 -1
  19. package/dist/client/assets/{folder-open-B3TO7t7Z.js → folder-open-BNQW9dE9.js} +1 -1
  20. package/dist/client/assets/{index-BIJgrHEn.css → index-DEVBHvyW.css} +1 -1
  21. package/dist/client/assets/index-k_85J1DS.js +682 -0
  22. package/dist/client/assets/{star-DW-M-BD_.js → star-7L86NZrT.js} +1 -1
  23. package/dist/client/assets/{upload-BzG6fknr.js → upload-DsAS6tno.js} +1 -1
  24. package/dist/client/assets/{users-DEicv0kj.js → users-D3u6f2Rz.js} +1 -1
  25. package/dist/client/index.html +2 -2
  26. package/dist/client/version.json +1 -1
  27. package/dist/extension.js +1215 -524
  28. package/dist/pi-claude-cli/package.json +1 -1
  29. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  30. package/package.json +1 -1
  31. package/dist/client/assets/ChatView-CkWkEwXL.js +0 -1
  32. package/dist/client/assets/SettingsModal-YH_rM1ZT.js +0 -31
  33. package/dist/client/assets/index-BlkXZ4C5.js +0 -682
package/dist/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();
55449
55650
  }
55450
- function getFusionAuthPath(home = getHomeDir4()) {
55451
- return join23(home, ".fusion", "agent", "auth.json");
55651
+ function getFusionAuthPath(home = getHomeDir5()) {
55652
+ return join24(home, ".fusion", "agent", "auth.json");
55452
55653
  }
55453
- function getFusionModelsPath(home = getHomeDir4()) {
55454
- return join23(home, ".fusion", "agent", "models.json");
55654
+ function getFusionModelsPath(home = getHomeDir5()) {
55655
+ return join24(home, ".fusion", "agent", "models.json");
55455
55656
  }
55456
- function getLegacyAuthPaths(home = getHomeDir4()) {
55657
+ function getLegacyAuthPaths(home = getHomeDir5()) {
55457
55658
  return [
55458
- join23(home, ".pi", "agent", "auth.json"),
55459
- join23(home, ".pi", "auth.json")
55659
+ join24(home, ".pi", "agent", "auth.json"),
55660
+ join24(home, ".pi", "auth.json")
55460
55661
  ];
55461
55662
  }
55462
- function getLegacyModelsPaths(home = getHomeDir4()) {
55663
+ function getSupplementalAuthPaths(home = getHomeDir5()) {
55463
55664
  return [
55464
- join23(home, ".pi", "agent", "models.json"),
55465
- join23(home, ".pi", "models.json")
55665
+ ...getLegacyAuthPaths(home),
55666
+ getCodexCliAuthPath(home)
55466
55667
  ];
55467
55668
  }
55468
- function getModelRegistryModelsPath(home = getHomeDir4()) {
55669
+ function getLegacyModelsPaths(home = getHomeDir5()) {
55670
+ return [
55671
+ join24(home, ".pi", "agent", "models.json"),
55672
+ join24(home, ".pi", "models.json")
55673
+ ];
55674
+ }
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
  }
@@ -60056,6 +60290,38 @@ var init_notifier = __esm({
60056
60290
  }
60057
60291
  });
60058
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
+
60059
60325
  // ../engine/src/reviewer.ts
60060
60326
  async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptContent, baseline, options = {}) {
60061
60327
  let liveSettings = options.settings;
@@ -60186,7 +60452,13 @@ async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptC
60186
60452
  ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
60187
60453
  taskId: options.taskId,
60188
60454
  taskTitle: options.taskTitle,
60189
- onFallbackModelUsed: notifyFallbackUsed,
60455
+ onFallbackModelUsed: createFallbackModelObserver({
60456
+ agent: "reviewer",
60457
+ label: "reviewer",
60458
+ store: options.store,
60459
+ taskId: options.taskId,
60460
+ taskTitle: options.taskTitle
60461
+ }),
60190
60462
  beforeSpawnSession: async () => {
60191
60463
  if (!options.store) return;
60192
60464
  let finalSettings;
@@ -60373,7 +60645,7 @@ var init_reviewer = __esm({
60373
60645
  init_logger2();
60374
60646
  init_usage_limit_detector();
60375
60647
  init_agent_instructions();
60376
- init_notifier();
60648
+ init_fallback_model_observer();
60377
60649
  init_agent_tools();
60378
60650
  REVIEWER_SYSTEM_PROMPT = `You are an independent code and plan reviewer.
60379
60651
 
@@ -60734,7 +61006,7 @@ var init_recovery_policy = __esm({
60734
61006
  // ../engine/src/triage.ts
60735
61007
  import { Type as Type2 } from "@mariozechner/pi-ai";
60736
61008
  import { readFile as readFile14 } from "node:fs/promises";
60737
- import { join as join28 } from "node:path";
61009
+ import { join as join29 } from "node:path";
60738
61010
  function extractPromptDeclaredTitle(prompt, taskId) {
60739
61011
  const headingMatch = prompt.match(/^#\s+Task:\s+([A-Z]+-\d+)\s+-\s+(.+)$/m);
60740
61012
  if (!headingMatch) return null;
@@ -60763,9 +61035,9 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
60763
61035
  return { attachmentContents, imageContents };
60764
61036
  }
60765
61037
  const { readFile: readFile26 } = await import("node:fs/promises");
60766
- const { join: join71 } = await import("node:path");
61038
+ const { join: join72 } = await import("node:path");
60767
61039
  for (const att of attachments) {
60768
- const filePath = join71(
61040
+ const filePath = join72(
60769
61041
  rootDir,
60770
61042
  ".fusion",
60771
61043
  "tasks",
@@ -60990,7 +61262,7 @@ var init_triage = __esm({
60990
61262
  init_concurrency();
60991
61263
  init_agent_logger();
60992
61264
  init_agent_instructions();
60993
- init_notifier();
61265
+ init_fallback_model_observer();
60994
61266
  init_logger2();
60995
61267
  init_usage_limit_detector();
60996
61268
  init_transient_error_detector();
@@ -61600,7 +61872,7 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
61600
61872
  return false;
61601
61873
  }
61602
61874
  const settings = await this.store.getSettings();
61603
- const promptPath = join28(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
61875
+ const promptPath = join29(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
61604
61876
  const written = await readFile14(promptPath, "utf-8").catch((err) => {
61605
61877
  const msg = err instanceof Error ? err.message : String(err);
61606
61878
  planLog.warn(`${task.id}: failed to read PROMPT.md during approved-spec recovery (${promptPath}): ${msg}`);
@@ -61830,7 +62102,13 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
61830
62102
  ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
61831
62103
  taskId: task.id,
61832
62104
  taskTitle: task.title,
61833
- onFallbackModelUsed: notifyFallbackUsed
62105
+ onFallbackModelUsed: createFallbackModelObserver({
62106
+ agent: "triage",
62107
+ label: "triage",
62108
+ store: this.store,
62109
+ taskId: task.id,
62110
+ taskTitle: task.title
62111
+ })
61834
62112
  });
61835
62113
  const modelDesc = describeModel(session);
61836
62114
  planLog.log(`${task.id}: using model ${modelDesc}`);
@@ -61973,7 +62251,13 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
61973
62251
  ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
61974
62252
  taskId: task.id,
61975
62253
  taskTitle: task.title,
61976
- onFallbackModelUsed: notifyFallbackUsed
62254
+ onFallbackModelUsed: createFallbackModelObserver({
62255
+ agent: "triage",
62256
+ label: "triage",
62257
+ store: this.store,
62258
+ taskId: task.id,
62259
+ taskTitle: task.title
62260
+ })
61977
62261
  });
61978
62262
  session = fallbackResult.session;
61979
62263
  const fallbackModelDesc = describeModel(session);
@@ -62058,7 +62342,7 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
62058
62342
  return;
62059
62343
  }
62060
62344
  const written = await readFile14(
62061
- join28(this.rootDir, promptPath),
62345
+ join29(this.rootDir, promptPath),
62062
62346
  "utf-8"
62063
62347
  ).catch((err) => {
62064
62348
  const msg = err instanceof Error ? err.message : String(err);
@@ -62377,9 +62661,9 @@ Remove or replace these ids and call fn_task_create again.`
62377
62661
  }
62378
62662
  try {
62379
62663
  const { readFile: readFile26 } = await import("node:fs/promises");
62380
- const { join: join71 } = await import("node:path");
62664
+ const { join: join72 } = await import("node:path");
62381
62665
  const promptContent = await readFile26(
62382
- join71(rootDir, promptPath),
62666
+ join72(rootDir, promptPath),
62383
62667
  "utf-8"
62384
62668
  ).catch((err) => {
62385
62669
  const msg = err instanceof Error ? err.message : String(err);
@@ -62607,160 +62891,8 @@ Take a completely different approach to writing this specification. Do NOT repea
62607
62891
  }
62608
62892
  });
62609
62893
 
62610
- // ../engine/src/session-token-usage.ts
62611
- var session_token_usage_exports = {};
62612
- __export(session_token_usage_exports, {
62613
- accumulateSessionTokenUsage: () => accumulateSessionTokenUsage
62614
- });
62615
- function readSessionStats(session) {
62616
- const accessor = session.getSessionStats;
62617
- if (typeof accessor !== "function") return void 0;
62618
- try {
62619
- return accessor.call(session);
62620
- } catch {
62621
- return void 0;
62622
- }
62623
- }
62624
- async function accumulateSessionTokenUsage(store, taskId, session) {
62625
- try {
62626
- const stats = readSessionStats(session);
62627
- const tokens = stats?.tokens;
62628
- if (!tokens) return;
62629
- const currentInput = (tokens.input ?? 0) + (tokens.cacheWrite ?? 0);
62630
- const currentOutput = tokens.output ?? 0;
62631
- const currentCached = tokens.cacheRead ?? 0;
62632
- const baseline = sessionBaselines.get(session) ?? { input: 0, output: 0, cached: 0 };
62633
- const inputDelta = Math.max(0, currentInput - baseline.input);
62634
- const outputDelta = Math.max(0, currentOutput - baseline.output);
62635
- const cachedDelta = Math.max(0, currentCached - baseline.cached);
62636
- sessionBaselines.set(session, {
62637
- input: currentInput,
62638
- output: currentOutput,
62639
- cached: currentCached
62640
- });
62641
- if (inputDelta === 0 && outputDelta === 0 && cachedDelta === 0) return;
62642
- const task = await store.getTask(taskId);
62643
- const now = (/* @__PURE__ */ new Date()).toISOString();
62644
- const newInput = (task.tokenUsage?.inputTokens ?? 0) + inputDelta;
62645
- const newOutput = (task.tokenUsage?.outputTokens ?? 0) + outputDelta;
62646
- const newCached = (task.tokenUsage?.cachedTokens ?? 0) + cachedDelta;
62647
- await store.updateTask(taskId, {
62648
- tokenUsage: {
62649
- inputTokens: newInput,
62650
- outputTokens: newOutput,
62651
- cachedTokens: newCached,
62652
- totalTokens: newInput + newOutput + newCached,
62653
- firstUsedAt: task.tokenUsage?.firstUsedAt ?? now,
62654
- lastUsedAt: now
62655
- }
62656
- });
62657
- } catch (err) {
62658
- const message = err instanceof Error ? err.message : String(err);
62659
- log14.warn(`${taskId}: session token usage accumulate failed: ${message}`);
62660
- }
62661
- }
62662
- var log14, sessionBaselines;
62663
- var init_session_token_usage = __esm({
62664
- "../engine/src/session-token-usage.ts"() {
62665
- "use strict";
62666
- init_logger2();
62667
- log14 = createLogger2("session-token-usage");
62668
- sessionBaselines = /* @__PURE__ */ new WeakMap();
62669
- }
62670
- });
62671
-
62672
- // ../engine/src/run-audit.ts
62673
- function createRunAuditor(store, context) {
62674
- if (!context) {
62675
- return {
62676
- git: async () => {
62677
- },
62678
- database: async () => {
62679
- },
62680
- filesystem: async () => {
62681
- }
62682
- };
62683
- }
62684
- const hasRecordAuditEvent = typeof store.recordRunAuditEvent === "function";
62685
- if (!hasRecordAuditEvent) {
62686
- return {
62687
- git: async () => {
62688
- },
62689
- database: async () => {
62690
- },
62691
- filesystem: async () => {
62692
- }
62693
- };
62694
- }
62695
- return {
62696
- git: async (input) => {
62697
- const eventInput = {
62698
- taskId: context.taskId,
62699
- agentId: context.agentId,
62700
- runId: context.runId,
62701
- domain: "git",
62702
- mutationType: input.type,
62703
- target: input.target,
62704
- metadata: {
62705
- phase: context.phase,
62706
- ...context.source ? { source: context.source } : {},
62707
- ...input.metadata
62708
- }
62709
- };
62710
- await store.recordRunAuditEvent(eventInput);
62711
- },
62712
- database: async (input) => {
62713
- const inferredTaskId = input.target.startsWith("FN-") || input.target.startsWith("KB-") ? input.target : context.taskId;
62714
- const eventInput = {
62715
- taskId: inferredTaskId,
62716
- agentId: context.agentId,
62717
- runId: context.runId,
62718
- domain: "database",
62719
- mutationType: input.type,
62720
- target: input.target,
62721
- metadata: {
62722
- phase: context.phase,
62723
- ...context.source ? { source: context.source } : {},
62724
- ...input.metadata
62725
- }
62726
- };
62727
- await store.recordRunAuditEvent(eventInput);
62728
- },
62729
- filesystem: async (input) => {
62730
- const eventInput = {
62731
- taskId: context.taskId,
62732
- agentId: context.agentId,
62733
- runId: context.runId,
62734
- domain: "filesystem",
62735
- mutationType: input.type,
62736
- target: input.target,
62737
- metadata: {
62738
- phase: context.phase,
62739
- ...context.source ? { source: context.source } : {},
62740
- ...input.metadata
62741
- }
62742
- };
62743
- await store.recordRunAuditEvent(eventInput);
62744
- }
62745
- };
62746
- }
62747
- function generateSyntheticRunId(prefix, taskId) {
62748
- const timestamp = Date.now();
62749
- const random = Math.random().toString(36).slice(2, 6);
62750
- return `${prefix}-${taskId}-${timestamp}-${random}`;
62751
- }
62752
- var init_run_audit = __esm({
62753
- "../engine/src/run-audit.ts"() {
62754
- "use strict";
62755
- }
62756
- });
62757
-
62758
- // ../engine/src/merger.ts
62759
- import { execSync, exec as exec3, spawn as spawn3 } from "node:child_process";
62760
- import { promisify as promisify4 } from "node:util";
62761
- import { existsSync as existsSync23 } from "node:fs";
62762
- import { join as join29 } from "node:path";
62763
- import { Type as Type3 } from "typebox";
62894
+ // ../engine/src/verification-utils.ts
62895
+ import { spawn as spawn3 } from "node:child_process";
62764
62896
  async function execWithProcessGroup(command, options) {
62765
62897
  return new Promise((resolve44, reject2) => {
62766
62898
  if (options.signal?.aborted) {
@@ -62872,6 +63004,11 @@ function truncateWithEllipsis(text, maxChars) {
62872
63004
  return `${text.slice(0, maxChars)}
62873
63005
  ... (truncated)`;
62874
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
+ }
62875
63012
  function summarizeVerificationOutput(output, type) {
62876
63013
  const lines = output.split("\n");
62877
63014
  let summaryLine = null;
@@ -62937,6 +63074,13 @@ function summarizeVerificationOutput(output, type) {
62937
63074
  failureNames.add(truncated);
62938
63075
  }
62939
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
+ }
62940
63084
  const parts = [];
62941
63085
  if (summaryLine) {
62942
63086
  parts.push(summaryLine);
@@ -62954,29 +63098,292 @@ function summarizeVerificationOutput(output, type) {
62954
63098
  parts.push(` \u2022 ... and ${names.length - 5} more failures`);
62955
63099
  }
62956
63100
  }
62957
- if (parts.length > 0) {
62958
- parts.push(footer);
62959
- return parts.join("\n");
62960
- }
62961
- const trimmed = output.trim();
62962
- if (!trimmed) {
62963
- 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)}
62964
63107
  ${footer}`;
62965
63108
  }
62966
- if (trimmed.length <= 500) {
62967
- return `${trimmed}
63109
+ return parts.join("\n") + `
62968
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
+ );
62969
63120
  }
62970
- let cutoff = 500;
62971
- for (let i = 500; i < trimmed.length; i++) {
62972
- if (trimmed[i] === " " || trimmed[i] === "\n") {
62973
- cutoff = i;
62974
- 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;
62975
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
+ );
62976
63220
  }
62977
- return `${trimmed.slice(0, cutoff)}...
62978
- ${footer}`;
63221
+ return result;
62979
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";
62980
63387
  function truncateWorkflowScriptOutput(output) {
62981
63388
  if (output.length <= WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS) return output;
62982
63389
  return `... output truncated to last ${WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS} characters ...
@@ -63020,7 +63427,7 @@ async function getStagedFiles(cwd) {
63020
63427
  }
63021
63428
  }
63022
63429
  function hasInstallState(rootDir) {
63023
- return existsSync23(join29(rootDir, "node_modules")) || existsSync23(join29(rootDir, ".pnp.cjs"));
63430
+ return existsSync24(join30(rootDir, "node_modules")) || existsSync24(join30(rootDir, ".pnp.cjs"));
63024
63431
  }
63025
63432
  function shouldSyncDependenciesForMerge(stagedFiles, installStatePresent) {
63026
63433
  if (!installStatePresent) return true;
@@ -63029,10 +63436,10 @@ function shouldSyncDependenciesForMerge(stagedFiles, installStatePresent) {
63029
63436
  );
63030
63437
  }
63031
63438
  function getDependencySyncCommand(rootDir) {
63032
- if (existsSync23(join29(rootDir, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
63033
- if (existsSync23(join29(rootDir, "package-lock.json"))) return "npm install";
63034
- if (existsSync23(join29(rootDir, "yarn.lock"))) return "yarn install --frozen-lockfile";
63035
- 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"))) {
63036
63443
  return "bun install --frozen-lockfile";
63037
63444
  }
63038
63445
  return null;
@@ -63065,8 +63472,8 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
63065
63472
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
63066
63473
  };
63067
63474
  }
63068
- if (existsSync23(join29(rootDir, "pnpm-lock.yaml"))) {
63069
- if (existsSync23(join29(rootDir, "pnpm-workspace.yaml"))) {
63475
+ if (existsSync24(join30(rootDir, "pnpm-lock.yaml"))) {
63476
+ if (existsSync24(join30(rootDir, "pnpm-workspace.yaml"))) {
63070
63477
  mergerLog.warn(
63071
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\`.`
63072
63479
  );
@@ -63077,21 +63484,21 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
63077
63484
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
63078
63485
  };
63079
63486
  }
63080
- if (existsSync23(join29(rootDir, "yarn.lock"))) {
63487
+ if (existsSync24(join30(rootDir, "yarn.lock"))) {
63081
63488
  return {
63082
63489
  command: "yarn test",
63083
63490
  testSource: "inferred",
63084
63491
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
63085
63492
  };
63086
63493
  }
63087
- if (existsSync23(join29(rootDir, "bun.lock")) || existsSync23(join29(rootDir, "bun.lockb"))) {
63494
+ if (existsSync24(join30(rootDir, "bun.lock")) || existsSync24(join30(rootDir, "bun.lockb"))) {
63088
63495
  return {
63089
63496
  command: "bun test",
63090
63497
  testSource: "inferred",
63091
63498
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
63092
63499
  };
63093
63500
  }
63094
- if (existsSync23(join29(rootDir, "package-lock.json"))) {
63501
+ if (existsSync24(join30(rootDir, "package-lock.json"))) {
63095
63502
  return {
63096
63503
  command: "npm test",
63097
63504
  testSource: "inferred",
@@ -63128,7 +63535,7 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
63128
63535
  await store.logEntry(taskId, deterministicVerificationMessage);
63129
63536
  await store.appendAgentLog(taskId, deterministicVerificationMessage, "text", void 0, "merger");
63130
63537
  if (hasTestCommand) {
63131
- const testResult = await runVerificationCommand(
63538
+ const testResult = await runVerificationCommand2(
63132
63539
  store,
63133
63540
  rootDir,
63134
63541
  taskId,
@@ -63159,7 +63566,7 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
63159
63566
  }
63160
63567
  }
63161
63568
  if (hasBuildCommand) {
63162
- const buildResult = await runVerificationCommand(
63569
+ const buildResult = await runVerificationCommand2(
63163
63570
  store,
63164
63571
  rootDir,
63165
63572
  taskId,
@@ -63194,98 +63601,9 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
63194
63601
  await store.appendAgentLog(taskId, "Deterministic merge verification passed", "text", void 0, "merger");
63195
63602
  return result;
63196
63603
  }
63197
- async function runVerificationCommand(store, rootDir, taskId, command, type, signal) {
63604
+ async function runVerificationCommand2(store, rootDir, taskId, command, type, signal) {
63198
63605
  throwIfAborted(signal, taskId);
63199
- mergerLog.log(`${taskId}: running ${type} command: ${command}`);
63200
- await store.logEntry(taskId, `[verification] Running ${type} command: ${command}`);
63201
- await store.appendAgentLog(taskId, `Running ${type} command`, "tool", command, "merger");
63202
- const result = {
63203
- command,
63204
- exitCode: null,
63205
- stdout: "",
63206
- stderr: "",
63207
- success: false
63208
- };
63209
- const verificationStartedAt = Date.now();
63210
- try {
63211
- const { stdout, stderr, bufferOverflow } = await execWithProcessGroup(command, {
63212
- cwd: rootDir,
63213
- timeout: VERIFICATION_COMMAND_TIMEOUT_MS,
63214
- maxBuffer: VERIFICATION_COMMAND_MAX_BUFFER,
63215
- signal
63216
- });
63217
- throwIfAborted(signal, taskId);
63218
- result.stdout = stdout?.toString?.() || "";
63219
- result.stderr = stderr?.toString?.() || "";
63220
- result.exitCode = 0;
63221
- result.success = true;
63222
- const verificationDurationMs = Date.now() - verificationStartedAt;
63223
- const timingDetail = `${verificationDurationMs}ms`;
63224
- if (bufferOverflow) {
63225
- mergerLog.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
63226
- await store.logEntry(
63227
- taskId,
63228
- `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
63229
- );
63230
- await store.appendAgentLog(
63231
- taskId,
63232
- `${type} command succeeded (exit 0)`,
63233
- "tool_result",
63234
- timingDetail,
63235
- "merger"
63236
- );
63237
- } else {
63238
- mergerLog.log(`${taskId}: ${type} command succeeded in ${verificationDurationMs}ms`);
63239
- await store.logEntry(taskId, `[timing] [verification] ${type} command succeeded (exit 0) in ${verificationDurationMs}ms`);
63240
- await store.appendAgentLog(
63241
- taskId,
63242
- `${type} command succeeded (exit 0)`,
63243
- "tool_result",
63244
- timingDetail,
63245
- "merger"
63246
- );
63247
- }
63248
- return result;
63249
- } catch (error) {
63250
- throwIfAborted(signal, taskId);
63251
- const verificationDurationMs = Date.now() - verificationStartedAt;
63252
- result.stdout = error?.stdout?.toString?.() || "";
63253
- result.stderr = error?.stderr?.toString?.() || "";
63254
- result.exitCode = typeof error?.status === "number" ? error.status : typeof error?.code === "number" ? error.code : null;
63255
- const maxBufferExceeded = error?.code === "ENOBUFS" || error?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || String(error?.message ?? "").includes("maxBuffer");
63256
- result.success = maxBufferExceeded && result.exitCode === 0;
63257
- if (result.success) {
63258
- mergerLog.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
63259
- await store.logEntry(
63260
- taskId,
63261
- `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
63262
- );
63263
- await store.appendAgentLog(
63264
- taskId,
63265
- `${type} command succeeded (exit 0)`,
63266
- "tool_result",
63267
- `${verificationDurationMs}ms`,
63268
- "merger"
63269
- );
63270
- return result;
63271
- }
63272
- const output = result.stderr || result.stdout || error?.message || "Unknown error";
63273
- const summary = summarizeVerificationOutput(output, type);
63274
- mergerLog.error(`${taskId}: ${type} command failed (exit ${result.exitCode}) in ${verificationDurationMs}ms; output captured in task log`);
63275
- await store.logEntry(
63276
- taskId,
63277
- `[timing] [verification] ${type} command failed (exit ${result.exitCode}) after ${verificationDurationMs}ms:
63278
- ${summary}`
63279
- );
63280
- await store.appendAgentLog(
63281
- taskId,
63282
- `${type} command failed (exit ${result.exitCode})`,
63283
- "tool_error",
63284
- summary,
63285
- "merger"
63286
- );
63287
- }
63288
- return result;
63606
+ return runVerificationCommand(store, rootDir, taskId, command, type, signal, mergerLog, "merger");
63289
63607
  }
63290
63608
  async function attemptInMergeVerificationFix(store, rootDir, taskId, failureContext, settings, options, mergeRunContext, fixAttemptNumber, _testCommand, _buildCommand) {
63291
63609
  try {
@@ -63348,9 +63666,20 @@ Do not refactor, rename broadly, or make opportunistic improvements.
63348
63666
  onToolEnd: logger2.onToolEnd,
63349
63667
  defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
63350
63668
  defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
63669
+ fallbackProvider: settings.fallbackProvider,
63670
+ fallbackModelId: settings.fallbackModelId,
63351
63671
  defaultThinkingLevel: settings.defaultThinkingLevel,
63352
63672
  // Skill selection: use assigned agent skills if available, otherwise role fallback
63353
- ...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
+ })
63354
63683
  });
63355
63684
  const runId = mergeRunContext?.runId;
63356
63685
  const agentId = mergeRunContext?.agentId ?? "merger";
@@ -63403,7 +63732,7 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
63403
63732
  void 0,
63404
63733
  "merger"
63405
63734
  );
63406
- const reRunResult = await runVerificationCommand(
63735
+ const reRunResult = await runVerificationCommand2(
63407
63736
  store,
63408
63737
  rootDir,
63409
63738
  taskId,
@@ -64145,9 +64474,16 @@ You are assisting with a paused \`git pull --rebase\`.
64145
64474
  onToolEnd: agentLogger.onToolEnd,
64146
64475
  defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
64147
64476
  defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
64477
+ fallbackProvider: settings.fallbackProvider,
64478
+ fallbackModelId: settings.fallbackModelId,
64148
64479
  defaultThinkingLevel: settings.defaultThinkingLevel,
64149
64480
  taskId,
64150
- onFallbackModelUsed: notifyFallbackUsed
64481
+ onFallbackModelUsed: createFallbackModelObserver({
64482
+ agent: "merger",
64483
+ label: "rebase conflict resolver",
64484
+ store,
64485
+ taskId
64486
+ })
64151
64487
  });
64152
64488
  const prompt = [
64153
64489
  `Resolve rebase conflicts for task ${taskId}.`,
@@ -64339,7 +64675,7 @@ async function pushToRemoteAfterMerge(store, rootDir, taskId, settings, options)
64339
64675
  }
64340
64676
  async function createPostMergeWorktree(rootDir, taskId) {
64341
64677
  const randomSuffix = Math.random().toString(36).slice(2, 10);
64342
- const postMergeWorktree = join29(rootDir, ".worktrees", `post-merge-${taskId}-${randomSuffix}`);
64678
+ const postMergeWorktree = join30(rootDir, ".worktrees", `post-merge-${taskId}-${randomSuffix}`);
64343
64679
  try {
64344
64680
  await execAsync2(`git worktree add ${quoteArg(postMergeWorktree)} HEAD`, { cwd: rootDir });
64345
64681
  return postMergeWorktree;
@@ -65280,7 +65616,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
65280
65616
  }
65281
65617
  }
65282
65618
  throwIfAborted(options.signal, taskId);
65283
- if (worktreePath && existsSync23(worktreePath)) {
65619
+ if (worktreePath && existsSync24(worktreePath)) {
65284
65620
  const otherUser = await findWorktreeUser(store, worktreePath, taskId);
65285
65621
  if (otherUser) {
65286
65622
  mergerLog.log(`Worktree retained \u2014 still needed by ${otherUser}`);
@@ -65934,9 +66270,20 @@ async function runAiAgentForCommit(params) {
65934
66270
  onToolEnd: agentLogger.onToolEnd,
65935
66271
  defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
65936
66272
  defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
66273
+ fallbackProvider: settings.fallbackProvider,
66274
+ fallbackModelId: settings.fallbackModelId,
65937
66275
  defaultThinkingLevel: settings.defaultThinkingLevel,
65938
66276
  // Skill selection: use assigned agent skills if available, otherwise role fallback
65939
- ...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
+ })
65940
66287
  });
65941
66288
  options.onSession?.(session);
65942
66289
  try {
@@ -66367,7 +66714,14 @@ If issues are found that need attention, describe them clearly and include concr
66367
66714
  fallbackModelId: settings.fallbackModelId,
66368
66715
  defaultThinkingLevel: settings.defaultThinkingLevel,
66369
66716
  // Skill selection: use assigned agent skills if available, otherwise role fallback
66370
- ...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
+ })
66371
66725
  });
66372
66726
  mergerLog.log(`${taskId}: [post-merge] workflow step '${workflowStep.name}' using model ${describeModel(session)}${useOverride ? " (workflow step override)" : ""}`);
66373
66727
  await store.logEntry(taskId, `[post-merge] Workflow step '${workflowStep.name}' using model: ${describeModel(session)}${useOverride ? " (workflow step override)" : ""}`);
@@ -66403,15 +66757,17 @@ async function completeTask(store, taskId, result) {
66403
66757
  result.task = task;
66404
66758
  store.emit("task:merged", result);
66405
66759
  }
66406
- 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;
66407
66761
  var init_merger = __esm({
66408
66762
  "../engine/src/merger.ts"() {
66409
66763
  "use strict";
66764
+ init_verification_utils();
66765
+ init_verification_utils();
66410
66766
  init_src();
66411
66767
  init_pi();
66412
66768
  init_session_token_usage();
66413
66769
  init_agent_session_helpers();
66414
- init_notifier();
66770
+ init_fallback_model_observer();
66415
66771
  init_session_skill_context();
66416
66772
  init_agent_logger();
66417
66773
  init_logger2();
@@ -66457,9 +66813,6 @@ var init_merger = __esm({
66457
66813
  "bun.lock",
66458
66814
  "packages/*/package.json"
66459
66815
  ];
66460
- VERIFICATION_COMMAND_MAX_BUFFER = 50 * 1024 * 1024;
66461
- VERIFICATION_COMMAND_TIMEOUT_MS = 6e5;
66462
- VERIFICATION_LOG_MAX_CHARS = 2e4;
66463
66816
  WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS = 4e3;
66464
66817
  PULL_REBASE_TIMEOUT_MS = 12e4;
66465
66818
  PUSH_TIMEOUT_MS = 6e4;
@@ -66484,8 +66837,8 @@ var init_merger = __esm({
66484
66837
 
66485
66838
  // ../engine/src/worktree-names.ts
66486
66839
  import { readdirSync as readdirSync3 } from "node:fs";
66487
- import { join as join30 } from "node:path";
66488
- import { existsSync as existsSync24 } from "node:fs";
66840
+ import { join as join31 } from "node:path";
66841
+ import { existsSync as existsSync25 } from "node:fs";
66489
66842
  function slugify2(str) {
66490
66843
  return str.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
66491
66844
  }
@@ -66496,7 +66849,7 @@ function generateReservedWorktreeName(rootDir, reservedNames = /* @__PURE__ */ n
66496
66849
  const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
66497
66850
  const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
66498
66851
  const baseName = `${adjective}-${noun}`;
66499
- const worktreesDir = join30(rootDir, ".worktrees");
66852
+ const worktreesDir = join31(rootDir, ".worktrees");
66500
66853
  const existing = getExistingWorktreeNames(worktreesDir);
66501
66854
  for (const reserved of reservedNames) {
66502
66855
  existing.add(reserved);
@@ -66511,7 +66864,7 @@ function generateReservedWorktreeName(rootDir, reservedNames = /* @__PURE__ */ n
66511
66864
  return `${baseName}-${suffix}`;
66512
66865
  }
66513
66866
  function getExistingWorktreeNames(worktreesDir) {
66514
- if (!existsSync24(worktreesDir)) {
66867
+ if (!existsSync25(worktreesDir)) {
66515
66868
  return /* @__PURE__ */ new Set();
66516
66869
  }
66517
66870
  try {
@@ -66648,8 +67001,8 @@ __export(worktree_pool_exports, {
66648
67001
  });
66649
67002
  import { exec as exec4 } from "node:child_process";
66650
67003
  import { promisify as promisify5 } from "node:util";
66651
- import { existsSync as existsSync25, lstatSync, readdirSync as readdirSync4, rmSync as rmSync2 } from "node:fs";
66652
- 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";
66653
67006
  function getExecStdout(result) {
66654
67007
  if (typeof result === "string") return result;
66655
67008
  if (result && typeof result === "object" && "stdout" in result) {
@@ -66695,10 +67048,10 @@ async function isRegisteredGitWorktree2(rootDir, worktreePath) {
66695
67048
  return (await getRegisteredWorktreePaths(rootDir)).has(resolve15(worktreePath));
66696
67049
  }
66697
67050
  function hasRequiredWorktreeFiles(worktreePath) {
66698
- return existsSync25(join31(worktreePath, ".git")) && existsSync25(join31(worktreePath, "package.json"));
67051
+ return existsSync26(join32(worktreePath, ".git")) && existsSync26(join32(worktreePath, "package.json"));
66699
67052
  }
66700
67053
  async function isUsableTaskWorktree(rootDir, worktreePath) {
66701
- return existsSync25(worktreePath) && await isRegisteredGitWorktree2(rootDir, worktreePath) && hasRequiredWorktreeFiles(worktreePath);
67054
+ return existsSync26(worktreePath) && await isRegisteredGitWorktree2(rootDir, worktreePath) && hasRequiredWorktreeFiles(worktreePath);
66702
67055
  }
66703
67056
  function isInsideWorktreesDir(rootDir, worktreePath) {
66704
67057
  const worktreesDir = resolve15(rootDir, ".worktrees");
@@ -66707,14 +67060,14 @@ function isInsideWorktreesDir(rootDir, worktreePath) {
66707
67060
  return rel !== "" && !rel.startsWith("..") && !isAbsolute9(rel);
66708
67061
  }
66709
67062
  async function scanIdleWorktrees(rootDir, store) {
66710
- const worktreesDir = join31(rootDir, ".worktrees");
66711
- if (!existsSync25(worktreesDir)) {
67063
+ const worktreesDir = join32(rootDir, ".worktrees");
67064
+ if (!existsSync26(worktreesDir)) {
66712
67065
  return [];
66713
67066
  }
66714
67067
  let dirs;
66715
67068
  try {
66716
67069
  const entries = readdirSync4(worktreesDir, { withFileTypes: true });
66717
- 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));
66718
67071
  } catch (err) {
66719
67072
  const errorMessage = err instanceof Error ? err.message : String(err);
66720
67073
  worktreePoolLog.warn(`Failed to read .worktrees/ directory: ${errorMessage}`);
@@ -66737,16 +67090,16 @@ async function scanIdleWorktrees(rootDir, store) {
66737
67090
  return registeredDirs.filter((dir2) => !activeWorktrees.has(resolve15(dir2)));
66738
67091
  }
66739
67092
  async function cleanupOrphanedWorktrees(rootDir, store) {
66740
- const worktreesDir = join31(rootDir, ".worktrees");
66741
- if (!existsSync25(worktreesDir)) {
67093
+ const worktreesDir = join32(rootDir, ".worktrees");
67094
+ if (!existsSync26(worktreesDir)) {
66742
67095
  return 0;
66743
67096
  }
66744
67097
  const orphaned = await scanIdleWorktrees(rootDir, store);
66745
67098
  const registeredWorktrees = await getRegisteredWorktreePaths(rootDir);
66746
67099
  let dirs = [];
66747
- if (existsSync25(worktreesDir)) {
67100
+ if (existsSync26(worktreesDir)) {
66748
67101
  try {
66749
- 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));
66750
67103
  } catch (err) {
66751
67104
  const errorMessage = err instanceof Error ? err.message : String(err);
66752
67105
  worktreePoolLog.warn(`Failed to read .worktrees/ directory for cleanup: ${errorMessage}`);
@@ -66778,8 +67131,8 @@ async function cleanupOrphanedWorktrees(rootDir, store) {
66778
67131
  return cleaned;
66779
67132
  }
66780
67133
  async function reapOrphanWorktrees(projectRoot) {
66781
- const worktreesDir = join31(projectRoot, ".worktrees");
66782
- if (!existsSync25(worktreesDir)) {
67134
+ const worktreesDir = join32(projectRoot, ".worktrees");
67135
+ if (!existsSync26(worktreesDir)) {
66783
67136
  return 0;
66784
67137
  }
66785
67138
  let entries;
@@ -66787,11 +67140,11 @@ async function reapOrphanWorktrees(projectRoot) {
66787
67140
  entries = readdirSync4(worktreesDir, { withFileTypes: true }).filter((e) => {
66788
67141
  if (!e.isDirectory()) return false;
66789
67142
  try {
66790
- 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();
66791
67144
  } catch {
66792
67145
  return false;
66793
67146
  }
66794
- }).map((e) => ({ name: e.name, fullPath: join31(worktreesDir, e.name) }));
67147
+ }).map((e) => ({ name: e.name, fullPath: join32(worktreesDir, e.name) }));
66795
67148
  } catch (err) {
66796
67149
  const msg = err instanceof Error ? err.message : String(err);
66797
67150
  worktreePoolLog.warn(`reapOrphanWorktrees: failed to read .worktrees/ \u2014 ${msg}`);
@@ -66810,8 +67163,8 @@ async function reapOrphanWorktrees(projectRoot) {
66810
67163
  if (registered.has(resolvedFull)) {
66811
67164
  continue;
66812
67165
  }
66813
- const dotGit = join31(resolvedFull, ".git");
66814
- if (existsSync25(dotGit)) {
67166
+ const dotGit = join32(resolvedFull, ".git");
67167
+ if (existsSync26(dotGit)) {
66815
67168
  worktreePoolLog.log(`reapOrphanWorktrees: skipping ${name} (has .git entry but not in registered list \u2014 may be partially registered)`);
66816
67169
  continue;
66817
67170
  }
@@ -66871,7 +67224,7 @@ var init_worktree_pool = __esm({
66871
67224
  acquire() {
66872
67225
  for (const path5 of this.idle) {
66873
67226
  this.idle.delete(path5);
66874
- if (existsSync25(path5)) {
67227
+ if (existsSync26(path5)) {
66875
67228
  return path5;
66876
67229
  }
66877
67230
  worktreePoolLog.log(`Pruned stale entry: ${path5}`);
@@ -66918,7 +67271,7 @@ var init_worktree_pool = __esm({
66918
67271
  */
66919
67272
  rehydrate(idlePaths) {
66920
67273
  for (const path5 of idlePaths) {
66921
- if (existsSync25(path5)) {
67274
+ if (existsSync26(path5)) {
66922
67275
  this.idle.add(path5);
66923
67276
  } else {
66924
67277
  worktreePoolLog.log(`Rehydrate skipped (not on disk): ${path5}`);
@@ -66974,7 +67327,7 @@ var init_worktree_pool = __esm({
66974
67327
  throw err;
66975
67328
  }
66976
67329
  const conflictingPath = match[1];
66977
- if (!existsSync25(conflictingPath)) {
67330
+ if (!existsSync26(conflictingPath)) {
66978
67331
  await execAsync3("git worktree prune", { cwd: worktreePath });
66979
67332
  await execAsync3(checkoutCmd, { cwd: worktreePath });
66980
67333
  return branchName;
@@ -67051,8 +67404,8 @@ var init_token_cap_detector = __esm({
67051
67404
  // ../engine/src/step-session-executor.ts
67052
67405
  import { exec as exec5 } from "node:child_process";
67053
67406
  import { promisify as promisify6 } from "node:util";
67054
- import { existsSync as existsSync26 } from "node:fs";
67055
- import { join as join32 } from "node:path";
67407
+ import { existsSync as existsSync27 } from "node:fs";
67408
+ import { join as join33 } from "node:path";
67056
67409
  function parseStepFileScopes(prompt) {
67057
67410
  const result = /* @__PURE__ */ new Map();
67058
67411
  if (!prompt) return result;
@@ -67339,7 +67692,7 @@ var init_step_session_executor = __esm({
67339
67692
  init_worktree_names();
67340
67693
  init_agent_logger();
67341
67694
  init_logger2();
67342
- init_notifier();
67695
+ init_fallback_model_observer();
67343
67696
  init_context_limit_detector();
67344
67697
  init_usage_limit_detector();
67345
67698
  init_agent_tools();
@@ -67456,7 +67809,7 @@ var init_step_session_executor = __esm({
67456
67809
  }
67457
67810
  for (const [stepIdx, worktreePath] of this.parallelWorktrees) {
67458
67811
  try {
67459
- if (existsSync26(worktreePath)) {
67812
+ if (existsSync27(worktreePath)) {
67460
67813
  await execAsync4(`git worktree remove "${worktreePath}" --force`, {
67461
67814
  cwd: this.options.rootDir
67462
67815
  });
@@ -67621,7 +67974,13 @@ Follow instructions precisely and avoid unrelated changes.`,
67621
67974
  ...this.options.skillSelection ? { skillSelection: this.options.skillSelection } : {},
67622
67975
  taskId: taskDetail.id,
67623
67976
  taskTitle: taskDetail.title,
67624
- 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
+ })
67625
67984
  });
67626
67985
  session = createResult.session;
67627
67986
  const handle = {
@@ -67801,7 +68160,7 @@ Follow instructions precisely and avoid unrelated changes.`,
67801
68160
  for (const [stepIdx, worktreePath] of worktreePaths) {
67802
68161
  if (worktreePath !== this.options.worktreePath) {
67803
68162
  try {
67804
- if (existsSync26(worktreePath)) {
68163
+ if (existsSync27(worktreePath)) {
67805
68164
  await execAsync4(`git worktree remove "${worktreePath}" --force`, {
67806
68165
  cwd: this.options.rootDir
67807
68166
  });
@@ -67831,7 +68190,7 @@ Follow instructions precisely and avoid unrelated changes.`,
67831
68190
  async createStepWorktree(stepIndex) {
67832
68191
  const { rootDir } = this.options;
67833
68192
  const name = generateWorktreeName(rootDir);
67834
- const worktreePath = join32(rootDir, ".worktrees", name);
68193
+ const worktreePath = join33(rootDir, ".worktrees", name);
67835
68194
  const branchName = `fusion/step-${stepIndex}-${name}`;
67836
68195
  stepExecLog.log(`Creating worktree for step ${stepIndex}: ${worktreePath} (branch: ${branchName})`);
67837
68196
  try {
@@ -67906,7 +68265,7 @@ Follow instructions precisely and avoid unrelated changes.`,
67906
68265
 
67907
68266
  // ../engine/src/spec-staleness.ts
67908
68267
  import { stat as stat5 } from "node:fs/promises";
67909
- import { join as join33 } from "node:path";
68268
+ import { join as join34 } from "node:path";
67910
68269
  async function evaluateSpecStaleness(options) {
67911
68270
  const { settings, promptPath, nowMs } = options;
67912
68271
  if (settings.specStalenessEnabled !== true) {
@@ -67946,7 +68305,7 @@ async function evaluateSpecStaleness(options) {
67946
68305
  };
67947
68306
  }
67948
68307
  function getPromptPath(tasksDir, taskId) {
67949
- return join33(tasksDir, taskId, "PROMPT.md");
68308
+ return join34(tasksDir, taskId, "PROMPT.md");
67950
68309
  }
67951
68310
  var DEFAULT_SPEC_STALENESS_MAX_AGE_MS;
67952
68311
  var init_spec_staleness = __esm({
@@ -67977,8 +68336,8 @@ var init_task_completion = __esm({
67977
68336
 
67978
68337
  // ../engine/src/run-verification-tool.ts
67979
68338
  import { spawn as spawn4 } from "node:child_process";
67980
- import { existsSync as existsSync27 } from "node:fs";
67981
- 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";
67982
68341
  import { Type as Type4 } from "@mariozechner/pi-ai";
67983
68342
  function createBuffer() {
67984
68343
  return { headChunks: [], headBytes: 0, tailChunks: [], tailBytes: 0, totalBytes: 0 };
@@ -68011,7 +68370,7 @@ function flattenBuffer(buf) {
68011
68370
 
68012
68371
  ` + tail;
68013
68372
  }
68014
- async function runVerificationCommand2(opts) {
68373
+ async function runVerificationCommand3(opts) {
68015
68374
  const { command, cwd, timeoutMs, expectFailure = false, onHeartbeat, onLine } = opts;
68016
68375
  const startMs = Date.now();
68017
68376
  const warnings = [];
@@ -68151,7 +68510,7 @@ function createRunVerificationTool(opts) {
68151
68510
  if (params.cwd && isAbsolute10(params.cwd)) {
68152
68511
  resolvedCwd = params.cwd;
68153
68512
  } else if (params.cwd) {
68154
- resolvedCwd = join34(worktreePath, params.cwd);
68513
+ resolvedCwd = join35(worktreePath, params.cwd);
68155
68514
  } else {
68156
68515
  resolvedCwd = worktreePath;
68157
68516
  }
@@ -68166,8 +68525,8 @@ function createRunVerificationTool(opts) {
68166
68525
  }
68167
68526
  let effectiveCommand = command;
68168
68527
  if (command.trimStart().startsWith("pnpm --filter")) {
68169
- const modulesYaml = join34(rootDir, "node_modules", ".modules.yaml");
68170
- if (!existsSync27(modulesYaml)) {
68528
+ const modulesYaml = join35(rootDir, "node_modules", ".modules.yaml");
68529
+ if (!existsSync28(modulesYaml)) {
68171
68530
  const installCmd = "pnpm install --prefer-offline";
68172
68531
  const msg = `node_modules/.modules.yaml not found in workspace root \u2014 auto-prepending \`${installCmd}\` before running the command.`;
68173
68532
  warnings.push(msg);
@@ -68178,7 +68537,7 @@ function createRunVerificationTool(opts) {
68178
68537
  log19.info(
68179
68538
  `[fn_run_verification] ${taskId}: scope=${scope} timeout=${timeoutSec}s cwd=${resolvedCwd} cmd=${effectiveCommand}`
68180
68539
  );
68181
- const result = await runVerificationCommand2({
68540
+ const result = await runVerificationCommand3({
68182
68541
  command: effectiveCommand,
68183
68542
  cwd: resolvedCwd,
68184
68543
  timeoutMs,
@@ -68278,8 +68637,8 @@ var init_run_verification_tool = __esm({
68278
68637
  // ../engine/src/executor.ts
68279
68638
  import { exec as exec6 } from "node:child_process";
68280
68639
  import { promisify as promisify7 } from "node:util";
68281
- import { isAbsolute as isAbsolute11, join as join35, relative as relative7, resolve as resolvePath } from "node:path";
68282
- 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";
68283
68642
  import { readFile as readFile15, writeFile as writeFile12 } from "node:fs/promises";
68284
68643
  import { Type as Type5 } from "@mariozechner/pi-ai";
68285
68644
  import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
@@ -68573,6 +68932,7 @@ var init_executor = __esm({
68573
68932
  "use strict";
68574
68933
  init_src();
68575
68934
  init_merger();
68935
+ init_verification_utils();
68576
68936
  init_worktree_names();
68577
68937
  init_pi();
68578
68938
  init_session_token_usage();
@@ -68597,7 +68957,7 @@ var init_executor = __esm({
68597
68957
  init_task_completion();
68598
68958
  init_auth_storage();
68599
68959
  init_run_verification_tool();
68600
- init_notifier();
68960
+ init_fallback_model_observer();
68601
68961
  init_agent_logger();
68602
68962
  init_agent_tools();
68603
68963
  execAsync5 = promisify7(exec6);
@@ -69744,7 +70104,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69744
70104
  );
69745
70105
  return false;
69746
70106
  }
69747
- if (task.worktree && existsSync28(task.worktree)) {
70107
+ if (task.worktree && existsSync29(task.worktree)) {
69748
70108
  const modifiedFiles = await this.captureModifiedFiles(task.worktree, task.baseCommitSha);
69749
70109
  if (modifiedFiles.length > 0) {
69750
70110
  await this.store.updateTask(task.id, { modifiedFiles });
@@ -69924,7 +70284,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69924
70284
  if (task.dependencies.length === 0) return null;
69925
70285
  for (const depId of task.dependencies) {
69926
70286
  const dep = allTasks.find((t) => t.id === depId);
69927
- 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)) {
69928
70288
  return dep.worktree;
69929
70289
  }
69930
70290
  }
@@ -69975,7 +70335,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69975
70335
  const activeMergeStatuses = /* @__PURE__ */ new Set(["merging", "merging-pr"]);
69976
70336
  const isActiveTask = activeColumns.has(task.column) || activeMergeStatuses.has(task.status ?? "");
69977
70337
  if (!isActiveTask) {
69978
- const tasksDir = join35(this.store.getFusionDir(), "tasks");
70338
+ const tasksDir = join36(this.store.getFusionDir(), "tasks");
69979
70339
  const promptPath = getPromptPath(tasksDir, task.id);
69980
70340
  const staleness = await evaluateSpecStaleness({ settings, promptPath });
69981
70341
  if (staleness.isStale) {
@@ -70022,7 +70382,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70022
70382
  worktreeName = generateWorktreeName(this.rootDir);
70023
70383
  break;
70024
70384
  }
70025
- worktreePath = join35(this.rootDir, ".worktrees", worktreeName);
70385
+ worktreePath = join36(this.rootDir, ".worktrees", worktreeName);
70026
70386
  }
70027
70387
  let stuckRequeue = null;
70028
70388
  let taskDone = false;
@@ -70046,7 +70406,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70046
70406
  );
70047
70407
  }
70048
70408
  const branchName = task.branch || `fusion/${task.id.toLowerCase()}`;
70049
- let isResume = existsSync28(worktreePath);
70409
+ let isResume = existsSync29(worktreePath);
70050
70410
  let acquiredFromPool = false;
70051
70411
  const baseBranch = task.baseBranch || null;
70052
70412
  if (task.worktree && isResume && !await isUsableTaskWorktree(this.rootDir, worktreePath)) {
@@ -70059,8 +70419,8 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70059
70419
  this.currentRunContext
70060
70420
  );
70061
70421
  await this.store.updateTask(task.id, { worktree: null, branch: null });
70062
- worktreePath = join35(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
70063
- isResume = existsSync28(worktreePath);
70422
+ worktreePath = join36(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
70423
+ isResume = existsSync29(worktreePath);
70064
70424
  }
70065
70425
  if (!isResume) {
70066
70426
  if (this.options.pool && settings.recycleWorktrees) {
@@ -70286,6 +70646,84 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70286
70646
  if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps after step-session completion")) {
70287
70647
  return;
70288
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
+ }
70289
70727
  if (executionMode !== "fast") {
70290
70728
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
70291
70729
  if (workflowResult === "deferred-paused") {
@@ -70374,7 +70812,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70374
70812
  executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}: ${errorMessage}`);
70375
70813
  await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}): ${errorMessage}`, void 0, this.currentRunContext);
70376
70814
  }
70377
- if (worktreePath && existsSync28(worktreePath)) {
70815
+ if (worktreePath && existsSync29(worktreePath)) {
70378
70816
  try {
70379
70817
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
70380
70818
  await audit.git({ type: "worktree:remove", target: worktreePath });
@@ -70433,7 +70871,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70433
70871
  try {
70434
70872
  const latestTask = await this.store.getTask(task.id);
70435
70873
  await this.resetStepsIfWorkLost(latestTask);
70436
- if (worktreePath && existsSync28(worktreePath)) {
70874
+ if (worktreePath && existsSync29(worktreePath)) {
70437
70875
  try {
70438
70876
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
70439
70877
  } catch (wtErr) {
@@ -70545,7 +70983,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70545
70983
  const executorFallbackProvider = settings.fallbackProvider;
70546
70984
  const executorFallbackModelId = settings.fallbackModelId;
70547
70985
  const executorThinkingLevel = detail.thinkingLevel ?? settings.defaultThinkingLevel;
70548
- const isResuming = !!task.sessionFile && existsSync28(task.sessionFile);
70986
+ const isResuming = !!task.sessionFile && existsSync29(task.sessionFile);
70549
70987
  const sessionManager = isResuming ? SessionManager2.open(task.sessionFile) : SessionManager2.create(worktreePath);
70550
70988
  executorLog.log(`${task.id}: creating agent session (provider=${executorProvider ?? "default"}, model=${executorModelId ?? "default"}, resuming=${isResuming})`);
70551
70989
  const executorInstructions = await this.resolveInstructionsForRole("executor");
@@ -70575,7 +71013,13 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70575
71013
  ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
70576
71014
  taskId: task.id,
70577
71015
  taskTitle: detail.title,
70578
- onFallbackModelUsed: notifyFallbackUsed
71016
+ onFallbackModelUsed: createFallbackModelObserver({
71017
+ agent: "executor",
71018
+ label: "executor",
71019
+ store: this.store,
71020
+ taskId: task.id,
71021
+ taskTitle: detail.title
71022
+ })
70579
71023
  });
70580
71024
  if (isResuming) {
70581
71025
  executorLog.log(`${task.id}: resumed session from ${task.sessionFile}`);
@@ -71009,7 +71453,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
71009
71453
  this.options.onComplete?.(task);
71010
71454
  } else {
71011
71455
  executorLog.log(`${task.id} paused \u2014 moving to todo`);
71012
- if (worktreePath && existsSync28(worktreePath)) {
71456
+ if (worktreePath && existsSync29(worktreePath)) {
71013
71457
  try {
71014
71458
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
71015
71459
  executorLog.log(`Removed old worktree for paused task: ${worktreePath}`);
@@ -71105,7 +71549,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
71105
71549
  executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}: ${errorMessage}`);
71106
71550
  await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}): ${errorMessage}`, void 0, this.currentRunContext);
71107
71551
  }
71108
- if (worktreePath && existsSync28(worktreePath)) {
71552
+ if (worktreePath && existsSync29(worktreePath)) {
71109
71553
  try {
71110
71554
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
71111
71555
  executorLog.log(`Removed old worktree for transient retry: ${worktreePath}`);
@@ -71160,7 +71604,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
71160
71604
  try {
71161
71605
  const latestTask = await this.store.getTask(task.id);
71162
71606
  await this.resetStepsIfWorkLost(latestTask);
71163
- if (worktreePath && existsSync28(worktreePath)) {
71607
+ if (worktreePath && existsSync29(worktreePath)) {
71164
71608
  try {
71165
71609
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
71166
71610
  executorLog.log(`Removed old worktree for stuck-killed retry: ${worktreePath}`);
@@ -71665,7 +72109,7 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
71665
72109
  * The section is replaced entirely to avoid accumulation of old feedback.
71666
72110
  */
71667
72111
  async injectWorkflowRevisionInstructions(task, feedback) {
71668
- const promptPath = join35(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
72112
+ const promptPath = join36(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
71669
72113
  let content;
71670
72114
  try {
71671
72115
  content = await readFile15(promptPath, "utf-8");
@@ -71719,6 +72163,217 @@ ${feedback}
71719
72163
  *
71720
72164
  * @returns true if a retry was scheduled, false if retries are exhausted
71721
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
+ }
71722
72377
  async handleWorkflowStepFailure(task, worktreePath, failureFeedback, stepName) {
71723
72378
  this.clearCompletedTaskWatchdog(task.id);
71724
72379
  const currentRetries = task.workflowStepRetries ?? 0;
@@ -71788,7 +72443,7 @@ Please fix the issues so the verification can pass on the next attempt.`,
71788
72443
  * The section is replaced entirely to avoid accumulation of old feedback.
71789
72444
  */
71790
72445
  async injectWorkflowStepFailureInstructions(task, failureFeedback, stepName, retryCount) {
71791
- const promptPath = join35(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
72446
+ const promptPath = join36(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
71792
72447
  let content;
71793
72448
  try {
71794
72449
  content = await readFile15(promptPath, "utf-8");
@@ -71853,31 +72508,33 @@ ${failureFeedback}
71853
72508
  * Uses git diff against the stored baseCommitSha to determine what changed.
71854
72509
  * Returns an empty array if no changes or if git commands fail.
71855
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
+ }
71856
72535
  async captureModifiedFiles(worktreePath, baseCommitSha) {
71857
72536
  try {
71858
- let baseRef = baseCommitSha;
71859
- if (!baseRef) {
71860
- try {
71861
- const { stdout: stdout2 } = await execAsync5("git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main", {
71862
- cwd: worktreePath,
71863
- encoding: "utf-8"
71864
- });
71865
- baseRef = stdout2.trim();
71866
- } catch (mergeBaseErr) {
71867
- const mergeBaseMsg = mergeBaseErr instanceof Error ? mergeBaseErr.message : String(mergeBaseErr);
71868
- executorLog.warn(`Failed merge-base lookup for diff base in ${worktreePath}, trying HEAD~1 fallback: ${mergeBaseMsg}`);
71869
- try {
71870
- const { stdout: stdout2 } = await execAsync5("git rev-parse HEAD~1", {
71871
- cwd: worktreePath,
71872
- encoding: "utf-8"
71873
- });
71874
- baseRef = stdout2.trim();
71875
- } catch {
71876
- executorLog.log(`Could not determine base commit for diff in ${worktreePath}`);
71877
- return [];
71878
- }
71879
- }
71880
- }
72537
+ const baseRef = await this.resolveDiffBaseRef(worktreePath, baseCommitSha);
71881
72538
  if (!baseRef) {
71882
72539
  return [];
71883
72540
  }
@@ -72113,6 +72770,30 @@ ${failureFeedback}
72113
72770
  */
72114
72771
  async executeWorkflowStep(task, workflowStep, worktreePath, settings) {
72115
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.`;
72116
72797
  const systemPrompt = `You are a workflow step agent executing: ${workflowStep.name}
72117
72798
 
72118
72799
  Task Context:
@@ -72120,6 +72801,8 @@ Task Context:
72120
72801
  - Task Description: ${task.description}
72121
72802
  - Worktree: ${worktreePath}
72122
72803
 
72804
+ ${scopeBlock}
72805
+
72123
72806
  Your role:
72124
72807
  - Execute this workflow step exactly as scoped.
72125
72808
  - Prioritize high-impact correctness/risk findings over stylistic nits.
@@ -72588,7 +73271,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
72588
73271
  * rather than fail the task permanently.
72589
73272
  */
72590
73273
  async resolveWorktreeStartPoint(startPoint, taskId) {
72591
- 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}"`;
72592
73275
  try {
72593
73276
  const { stdout } = await execAsync5(command, { cwd: this.rootDir });
72594
73277
  return stdout.trim() || startPoint;
@@ -72608,7 +73291,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
72608
73291
  */
72609
73292
  async tryCreateWorktree(branch, path5, taskId, startPoint, attemptNumber = 0, recoveryDepth = 0) {
72610
73293
  await this.assertWorktreePathNotNested(path5, taskId);
72611
- if (existsSync28(path5)) {
73294
+ if (existsSync29(path5)) {
72612
73295
  const isRegistered = await this.isRegisteredWorktree(path5);
72613
73296
  if (!isRegistered) {
72614
73297
  await this.store.logEntry(
@@ -72759,7 +73442,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
72759
73442
  );
72760
73443
  if (shouldGenerateNewName) {
72761
73444
  const conflictStartPoint = branch;
72762
- const newPath = join35(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
73445
+ const newPath = join36(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
72763
73446
  for (let suffix = 2; suffix <= 6; suffix++) {
72764
73447
  const suffixedBranch = `${branch}-${suffix}`;
72765
73448
  try {
@@ -73359,7 +74042,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
73359
74042
  metadata: { type: "spawned", parentTaskId: taskId }
73360
74043
  });
73361
74044
  const childWorktreeName = generateWorktreeName(this.rootDir);
73362
- const childWorktreePath = join35(this.rootDir, ".worktrees", childWorktreeName);
74045
+ const childWorktreePath = join36(this.rootDir, ".worktrees", childWorktreeName);
73363
74046
  const childBranch = `fusion/spawn-${agent.id}`;
73364
74047
  await this.createWorktree(childBranch, childWorktreePath, taskId, worktreePath);
73365
74048
  await this.options.agentStore.updateAgentState(agent.id, "active");
@@ -73548,9 +74231,9 @@ var init_node_routing_policy = __esm({
73548
74231
  });
73549
74232
 
73550
74233
  // ../engine/src/scheduler.ts
73551
- import { existsSync as existsSync29 } from "node:fs";
74234
+ import { existsSync as existsSync30 } from "node:fs";
73552
74235
  import { readFile as readFile16 } from "node:fs/promises";
73553
- import { basename as basename8, join as join36 } from "node:path";
74236
+ import { basename as basename8, join as join37 } from "node:path";
73554
74237
  function pathsOverlap2(a, b) {
73555
74238
  for (const pa of a) {
73556
74239
  const prefixA = pa.endsWith("/*") ? pa.slice(0, -1) : null;
@@ -73720,12 +74403,12 @@ var init_scheduler = __esm({
73720
74403
  * @returns Object with `valid: true` if checks pass, or `valid: false` with a `reason` string if they fail
73721
74404
  */
73722
74405
  async validateTaskFilesystem(id) {
73723
- const taskDir = join36(this.store.getTasksDir(), id);
73724
- if (!existsSync29(taskDir)) {
74406
+ const taskDir = join37(this.store.getTasksDir(), id);
74407
+ if (!existsSync30(taskDir)) {
73725
74408
  return { valid: false, reason: "missing directory" };
73726
74409
  }
73727
- const promptPath = join36(taskDir, "PROMPT.md");
73728
- if (!existsSync29(promptPath)) {
74410
+ const promptPath = join37(taskDir, "PROMPT.md");
74411
+ if (!existsSync30(promptPath)) {
73729
74412
  return { valid: false, reason: "missing or empty PROMPT.md" };
73730
74413
  }
73731
74414
  try {
@@ -73864,7 +74547,7 @@ var init_scheduler = __esm({
73864
74547
  break;
73865
74548
  }
73866
74549
  reservedNames.add(worktreeName);
73867
- return join36(this.store.getRootDir(), ".worktrees", worktreeName);
74550
+ return join37(this.store.getRootDir(), ".worktrees", worktreeName);
73868
74551
  }
73869
74552
  /**
73870
74553
  * Run one scheduling pass.
@@ -75141,7 +75824,7 @@ var init_mission_execution_loop = __esm({
75141
75824
  init_pi();
75142
75825
  init_agent_session_helpers();
75143
75826
  init_logger2();
75144
- init_notifier();
75827
+ init_fallback_model_observer();
75145
75828
  loopLog = createLogger2("mission-loop");
75146
75829
  VALIDATION_TIMEOUT_MS = 10 * 60 * 1e3;
75147
75830
  MissionExecutionLoop = class extends EventEmitter18 {
@@ -75375,7 +76058,13 @@ Assertions: ${assertions.map((a) => a.title).join(", ")}`,
75375
76058
  },
75376
76059
  taskId: task?.id,
75377
76060
  taskTitle: task?.title,
75378
- 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
+ })
75379
76068
  });
75380
76069
  session = { session: sessionResult.session, sessionFile: sessionResult.sessionFile };
75381
76070
  loopLog.log(`Validation session created for feature ${feature.id}`);
@@ -77183,7 +77872,7 @@ not loop on the same plan across heartbeats without recording why.`;
77183
77872
  };
77184
77873
  }
77185
77874
  };
77186
- const { createResolvedAgentSession: createResolvedAgentSession2, extractRuntimeHint: extractRuntimeHint2, extractRuntimeModel: extractRuntimeModel2 } = 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));
77187
77876
  const { buildSessionSkillContextSync: buildSessionSkillContextSync2 } = await Promise.resolve().then(() => (init_session_skill_context(), session_skill_context_exports));
77188
77877
  let heartbeatTools;
77189
77878
  if (isNoTaskRun) {
@@ -77254,7 +77943,7 @@ not loop on the same plan across heartbeats without recording why.`;
77254
77943
  persistAgentToolOutput: memorySettings?.persistAgentToolOutput
77255
77944
  });
77256
77945
  }
77257
- const { session } = await createResolvedAgentSession2({
77946
+ const { session } = await createResolvedAgentSession3({
77258
77947
  sessionPurpose: "heartbeat",
77259
77948
  runtimeHint: extractRuntimeHint2(agent.runtimeConfig),
77260
77949
  pluginRunner: this.pluginRunner,
@@ -78640,7 +79329,7 @@ async function createAiPromptExecutor(cwd) {
78640
79329
  }
78641
79330
  };
78642
79331
  }
78643
- function truncateOutput(stdout, stderr) {
79332
+ function truncateOutput2(stdout, stderr) {
78644
79333
  const out = stdout ?? "";
78645
79334
  const err = stderr ?? "";
78646
79335
  let combined = out;
@@ -78820,7 +79509,7 @@ var init_cron_runner = __esm({
78820
79509
  maxBuffer: MAX_BUFFER,
78821
79510
  shell: defaultShell
78822
79511
  });
78823
- const output = truncateOutput(stdout, stderr);
79512
+ const output = truncateOutput2(stdout, stderr);
78824
79513
  log15.log(`\u2713 ${schedule.name} completed (${output.length} bytes output)`);
78825
79514
  return {
78826
79515
  success: true,
@@ -78831,7 +79520,7 @@ var init_cron_runner = __esm({
78831
79520
  } catch (err) {
78832
79521
  const stdout = err.stdout ?? "";
78833
79522
  const stderr = err.stderr ?? "";
78834
- const output = truncateOutput(stdout, stderr);
79523
+ const output = truncateOutput2(stdout, stderr);
78835
79524
  const errorMessage = err.killed ? `Command timed out after ${(schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS6) / 1e3}s` : err.message ?? String(err);
78836
79525
  log15.warn(`\u2717 ${schedule.name} failed: ${errorMessage}`);
78837
79526
  return {
@@ -78876,7 +79565,7 @@ var init_cron_runner = __esm({
78876
79565
  const result = await runBackupCommand2(fusionDir, settings);
78877
79566
  return {
78878
79567
  success: result.success,
78879
- output: truncateOutput(result.output ?? "", ""),
79568
+ output: truncateOutput2(result.output ?? "", ""),
78880
79569
  error: result.success ? void 0 : result.output
78881
79570
  };
78882
79571
  } catch (err) {
@@ -78917,7 +79606,7 @@ var init_cron_runner = __esm({
78917
79606
  if (sr.output) outputParts.push(sr.output);
78918
79607
  if (sr.error) outputParts.push(`Error: ${sr.error}`);
78919
79608
  }
78920
- const output = truncateOutput(outputParts.join("\n"), "");
79609
+ const output = truncateOutput2(outputParts.join("\n"), "");
78921
79610
  const failedSteps = stepResults.filter((sr) => !sr.success);
78922
79611
  const error = failedSteps.length > 0 ? `${failedSteps.length} step(s) failed: ${failedSteps.map((s) => s.stepName).join(", ")}${stoppedEarly ? " (execution stopped)" : ""}` : void 0;
78923
79612
  const status = overallSuccess ? "\u2713" : "\u2717";
@@ -78996,7 +79685,7 @@ var init_cron_runner = __esm({
78996
79685
  stepName: step.name,
78997
79686
  stepIndex,
78998
79687
  success: true,
78999
- output: truncateOutput(stdout, stderr),
79688
+ output: truncateOutput2(stdout, stderr),
79000
79689
  startedAt,
79001
79690
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
79002
79691
  };
@@ -79009,7 +79698,7 @@ var init_cron_runner = __esm({
79009
79698
  stepName: step.name,
79010
79699
  stepIndex,
79011
79700
  success: false,
79012
- output: truncateOutput(stdout, stderr),
79701
+ output: truncateOutput2(stdout, stderr),
79013
79702
  error: errorMessage,
79014
79703
  startedAt,
79015
79704
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -79159,7 +79848,7 @@ var init_cron_runner = __esm({
79159
79848
  // ../engine/src/routine-runner.ts
79160
79849
  import { exec as exec8 } from "node:child_process";
79161
79850
  import { promisify as promisify8 } from "node:util";
79162
- function truncateOutput2(stdout, stderr) {
79851
+ function truncateOutput3(stdout, stderr) {
79163
79852
  let output = stdout;
79164
79853
  if (stderr) {
79165
79854
  output += stdout ? "\n--- stderr ---\n" : "";
@@ -79342,7 +80031,7 @@ var init_routine_runner = __esm({
79342
80031
  const result = await runBackupCommand2(fusionDir, settings);
79343
80032
  return {
79344
80033
  success: result.success,
79345
- output: truncateOutput2(result.output ?? "", ""),
80034
+ output: truncateOutput3(result.output ?? "", ""),
79346
80035
  error: result.success ? void 0 : result.output,
79347
80036
  startedAt,
79348
80037
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -79366,7 +80055,7 @@ var init_routine_runner = __esm({
79366
80055
  });
79367
80056
  return {
79368
80057
  success: true,
79369
- output: truncateOutput2(stdout, stderr),
80058
+ output: truncateOutput3(stdout, stderr),
79370
80059
  startedAt,
79371
80060
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
79372
80061
  };
@@ -79377,7 +80066,7 @@ var init_routine_runner = __esm({
79377
80066
  const error = errObj.killed === true ? `Command timed out after ${(timeoutMs ?? DEFAULT_TIMEOUT_MS7) / 1e3}s` : (err instanceof Error ? err.message : null) ?? String(err);
79378
80067
  return {
79379
80068
  success: false,
79380
- output: truncateOutput2(stdout, stderr),
80069
+ output: truncateOutput3(stdout, stderr),
79381
80070
  error,
79382
80071
  startedAt,
79383
80072
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -79410,7 +80099,7 @@ var init_routine_runner = __esm({
79410
80099
  const failedSteps = stepResults.filter((sr) => !sr.success);
79411
80100
  return {
79412
80101
  success: overallSuccess,
79413
- output: truncateOutput2(outputParts.join("\n"), ""),
80102
+ output: truncateOutput3(outputParts.join("\n"), ""),
79414
80103
  error: failedSteps.length > 0 ? `${failedSteps.length} step(s) failed: ${failedSteps.map((s) => s.stepName).join(", ")}${stoppedEarly ? " (execution stopped)" : ""}` : void 0,
79415
80104
  startedAt,
79416
80105
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -79445,7 +80134,7 @@ var init_routine_runner = __esm({
79445
80134
  this.options.aiPromptExecutor(step.prompt, step.modelProvider, step.modelId),
79446
80135
  new Promise((_resolve, reject2) => setTimeout(() => reject2(new Error(`AI prompt step timed out after ${timeoutMs / 1e3}s`)), timeoutMs))
79447
80136
  ]);
79448
- 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() };
79449
80138
  } catch (err) {
79450
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() };
79451
80140
  }
@@ -80059,8 +80748,8 @@ var init_stuck_task_detector = __esm({
80059
80748
  // ../engine/src/self-healing.ts
80060
80749
  import { exec as exec9 } from "node:child_process";
80061
80750
  import { promisify as promisify9 } from "node:util";
80062
- import { existsSync as existsSync30, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
80063
- 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";
80064
80753
  function shellQuote(value) {
80065
80754
  return `'${value.replace(/'/g, "'\\''")}'`;
80066
80755
  }
@@ -80456,7 +81145,7 @@ var init_self_healing = __esm({
80456
81145
  return commit;
80457
81146
  }
80458
81147
  async cleanupInterruptedMergeArtifacts(task) {
80459
- if (task.worktree && existsSync30(task.worktree)) {
81148
+ if (task.worktree && existsSync31(task.worktree)) {
80460
81149
  try {
80461
81150
  await execAsync7(`git worktree remove ${shellQuote(task.worktree)} --force`, {
80462
81151
  cwd: this.options.rootDir,
@@ -81077,7 +81766,7 @@ var init_self_healing = __esm({
81077
81766
  return false;
81078
81767
  }
81079
81768
  const staleness = now - new Date(t.updatedAt).getTime();
81080
- const hasWorktree = t.worktree && existsSync30(t.worktree);
81769
+ const hasWorktree = t.worktree && existsSync31(t.worktree);
81081
81770
  const graceMs = hasWorktree ? ORPHANED_WITH_WORKTREE_GRACE_MS : ORPHANED_EXECUTION_RECOVERY_GRACE_MS;
81082
81771
  return staleness >= graceMs;
81083
81772
  });
@@ -81086,7 +81775,7 @@ var init_self_healing = __esm({
81086
81775
  let recovered = 0;
81087
81776
  for (const task of orphaned) {
81088
81777
  try {
81089
- const hadWorktree = task.worktree && existsSync30(task.worktree);
81778
+ const hadWorktree = task.worktree && existsSync31(task.worktree);
81090
81779
  const reason = hadWorktree ? "worktree exists but no active session" : "missing worktree/session";
81091
81780
  await this.resetStepsIfWorkLost(task);
81092
81781
  await this.store.updateTask(task.id, {
@@ -81232,7 +81921,7 @@ var init_self_healing = __esm({
81232
81921
  }
81233
81922
  }
81234
81923
  async hasRecoverableGitWork(task) {
81235
- if (task.worktree && existsSync30(task.worktree)) {
81924
+ if (task.worktree && existsSync31(task.worktree)) {
81236
81925
  try {
81237
81926
  const { stdout: status } = await execAsync7("git status --porcelain", {
81238
81927
  cwd: task.worktree,
@@ -81417,11 +82106,11 @@ var init_self_healing = __esm({
81417
82106
  * tracks registered idle worktrees, never these orphans.
81418
82107
  */
81419
82108
  async reapUnregisteredOrphans() {
81420
- const worktreesDir = join37(this.options.rootDir, ".worktrees");
81421
- if (!existsSync30(worktreesDir)) return 0;
82109
+ const worktreesDir = join38(this.options.rootDir, ".worktrees");
82110
+ if (!existsSync31(worktreesDir)) return 0;
81422
82111
  let dirs;
81423
82112
  try {
81424
- 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));
81425
82114
  } catch (err) {
81426
82115
  log17.warn(`Failed to read .worktrees/ for unregistered orphan reap: ${err instanceof Error ? err.message : String(err)}`);
81427
82116
  return 0;
@@ -81526,8 +82215,8 @@ var init_self_healing = __esm({
81526
82215
  }
81527
82216
  /** Remove oldest idle worktrees if total count exceeds 2× maxWorktrees. */
81528
82217
  async enforceWorktreeCap() {
81529
- const worktreesDir = join37(this.options.rootDir, ".worktrees");
81530
- if (!existsSync30(worktreesDir)) return;
82218
+ const worktreesDir = join38(this.options.rootDir, ".worktrees");
82219
+ if (!existsSync31(worktreesDir)) return;
81531
82220
  try {
81532
82221
  const settings = await this.store.getSettings();
81533
82222
  const cap = (settings.maxWorktrees ?? 4) * 2;
@@ -83365,7 +84054,7 @@ var init_ipc_host = __esm({
83365
84054
  import { EventEmitter as EventEmitter21 } from "node:events";
83366
84055
  import { fork } from "node:child_process";
83367
84056
  import { fileURLToPath as fileURLToPath3 } from "node:url";
83368
- import { dirname as dirname11, join as join38 } from "node:path";
84057
+ import { dirname as dirname11, join as join39 } from "node:path";
83369
84058
  var HealthMonitor, ChildProcessRuntime;
83370
84059
  var init_child_process_runtime = __esm({
83371
84060
  "../engine/src/runtimes/child-process-runtime.ts"() {
@@ -83527,7 +84216,7 @@ var init_child_process_runtime = __esm({
83527
84216
  const isCompiled = !import.meta.url.endsWith(".ts");
83528
84217
  const currentDir = dirname11(fileURLToPath3(import.meta.url));
83529
84218
  const workerFile = isCompiled ? "child-process-worker.js" : "child-process-worker.ts";
83530
- return join38(currentDir, workerFile);
84219
+ return join39(currentDir, workerFile);
83531
84220
  }
83532
84221
  /**
83533
84222
  * Set up event forwarding from IPC host to runtime listeners.
@@ -87780,6 +88469,8 @@ __export(src_exports2, {
87780
88469
  describeAgentModel: () => describeAgentModel,
87781
88470
  describeModel: () => describeModel,
87782
88471
  ensureDefaultHeartbeatProcedureFile: () => ensureDefaultHeartbeatProcedureFile,
88472
+ extractRuntimeHint: () => extractRuntimeHint,
88473
+ extractRuntimeModel: () => extractRuntimeModel,
87783
88474
  formatTaskIdentifier: () => formatTaskIdentifier,
87784
88475
  getDefaultPiRuntime: () => getDefaultPiRuntime,
87785
88476
  getHostExtensionPaths: () => getHostExtensionPaths,
@@ -92124,7 +92815,7 @@ var init_api_error = __esm({
92124
92815
  // ../dashboard/src/plugin-routes.ts
92125
92816
  import { Router } from "express";
92126
92817
  import { access as access5, stat as stat6, readFile as readFile18 } from "node:fs/promises";
92127
- 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";
92128
92819
  async function resolvePluginManifest(sourcePath) {
92129
92820
  try {
92130
92821
  await access5(sourcePath);
@@ -92140,7 +92831,7 @@ async function resolvePluginManifest(sourcePath) {
92140
92831
  if (!sourceStat.isDirectory()) {
92141
92832
  throw badRequest(`Path is not a directory: ${sourcePath}`);
92142
92833
  }
92143
- const directManifestPath = join39(sourcePath, "manifest.json");
92834
+ const directManifestPath = join40(sourcePath, "manifest.json");
92144
92835
  try {
92145
92836
  await access5(directManifestPath);
92146
92837
  const manifest = await readAndValidateManifest(directManifestPath);
@@ -92151,7 +92842,7 @@ async function resolvePluginManifest(sourcePath) {
92151
92842
  const dirName = basename9(sourcePath).toLowerCase();
92152
92843
  if (DIST_DIR_NAMES.has(dirName)) {
92153
92844
  const parentDir = dirname12(sourcePath);
92154
- const parentManifestPath = join39(parentDir, "manifest.json");
92845
+ const parentManifestPath = join40(parentDir, "manifest.json");
92155
92846
  try {
92156
92847
  await access5(parentManifestPath);
92157
92848
  const manifest = await readAndValidateManifest(parentManifestPath);
@@ -93622,8 +94313,8 @@ function registerTaskWorkflowRoutes(ctx, deps) {
93622
94313
  });
93623
94314
  if (retrySpecification) {
93624
94315
  const { rm: rm6 } = await import("node:fs/promises");
93625
- const { join: join71 } = await import("node:path");
93626
- 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");
93627
94318
  await rm6(promptPath, { force: true });
93628
94319
  await scopedStore.logEntry(req.params.id, "Retry requested from dashboard (planning retry budget reset)");
93629
94320
  const updated2 = await scopedStore.getTask(req.params.id);
@@ -94088,8 +94779,8 @@ function registerTaskWorkflowRoutes(ctx, deps) {
94088
94779
  await scopedStore.logEntry(task.id, "Plan rejected by user", "Specification will be regenerated");
94089
94780
  await scopedStore.updateTask(task.id, { status: void 0 });
94090
94781
  const { rm: rm6 } = await import("node:fs/promises");
94091
- const { join: join71 } = await import("node:path");
94092
- 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");
94093
94784
  await rm6(promptPath, { force: true });
94094
94785
  const updated = await scopedStore.getTask(task.id);
94095
94786
  res.json(updated);
@@ -94359,8 +95050,8 @@ function registerTaskWorkflowRoutes(ctx, deps) {
94359
95050
  if (task.column === "triage") {
94360
95051
  await scopedStore.logEntry(task.id, "AI spec revision requested", feedback);
94361
95052
  const { rm: rm7 } = await import("node:fs/promises");
94362
- const { join: join72 } = await import("node:path");
94363
- 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");
94364
95055
  await rm7(promptPath2, { force: true });
94365
95056
  await scopedStore.updateTask(task.id, { status: "needs-replan" });
94366
95057
  const updated2 = await scopedStore.getTask(task.id);
@@ -94376,8 +95067,8 @@ function registerTaskWorkflowRoutes(ctx, deps) {
94376
95067
  await scopedStore.logEntry(task.id, "AI spec revision requested", feedback);
94377
95068
  const updated = await scopedStore.moveTask(task.id, "triage");
94378
95069
  const { rm: rm6 } = await import("node:fs/promises");
94379
- const { join: join71 } = await import("node:path");
94380
- 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");
94381
95072
  await rm6(promptPath, { force: true });
94382
95073
  await scopedStore.updateTask(task.id, { status: "needs-replan" });
94383
95074
  res.json(updated);
@@ -94397,8 +95088,8 @@ function registerTaskWorkflowRoutes(ctx, deps) {
94397
95088
  if (task.column === "triage") {
94398
95089
  await scopedStore.logEntry(task.id, "Specification rebuild requested by user");
94399
95090
  const { rm: rm7 } = await import("node:fs/promises");
94400
- const { join: join72 } = await import("node:path");
94401
- 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");
94402
95093
  await rm7(promptPath2, { force: true });
94403
95094
  await scopedStore.updateTask(task.id, { status: "needs-replan" });
94404
95095
  const updated2 = await scopedStore.getTask(task.id);
@@ -94412,8 +95103,8 @@ function registerTaskWorkflowRoutes(ctx, deps) {
94412
95103
  await scopedStore.logEntry(task.id, "Specification rebuild requested by user");
94413
95104
  const updated = await scopedStore.moveTask(task.id, "triage");
94414
95105
  const { rm: rm6 } = await import("node:fs/promises");
94415
- const { join: join71 } = await import("node:path");
94416
- 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");
94417
95108
  await rm6(promptPath, { force: true });
94418
95109
  await scopedStore.updateTask(task.id, { status: "needs-replan" });
94419
95110
  res.json(updated);
@@ -95794,14 +96485,15 @@ __export(chat_exports, {
95794
96485
  __setBuildAgentChatPrompt: () => __setBuildAgentChatPrompt,
95795
96486
  __setChatDiagnostics: () => __setChatDiagnostics,
95796
96487
  __setCreateFnAgent: () => __setCreateFnAgent2,
96488
+ __setCreateResolvedAgentSession: () => __setCreateResolvedAgentSession,
95797
96489
  chatStreamManager: () => chatStreamManager,
95798
96490
  checkRateLimit: () => checkRateLimit5,
95799
96491
  getRateLimitResetTime: () => getRateLimitResetTime5,
95800
96492
  resolveFileReferences: () => resolveFileReferences
95801
96493
  });
95802
96494
  import { EventEmitter as EventEmitter29 } from "node:events";
95803
- import { existsSync as existsSync31 } from "node:fs";
95804
- 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";
95805
96497
  import { SessionManager as SessionManager3 } from "@mariozechner/pi-coding-agent";
95806
96498
  function __getChatDiagnostics() {
95807
96499
  return _diagnostics;
@@ -95831,7 +96523,7 @@ function validateFilePath(basePath, filePath) {
95831
96523
  throw new Error(`Access denied: Absolute paths not allowed`);
95832
96524
  }
95833
96525
  const resolvedBase = resolve19(basePath);
95834
- const resolvedPath = resolve19(join40(resolvedBase, decodedPath));
96526
+ const resolvedPath = resolve19(join41(resolvedBase, decodedPath));
95835
96527
  const relativePath = relative9(resolvedBase, resolvedPath);
95836
96528
  if (relativePath.startsWith("..") || relativePath.startsWith("../") || relativePath === "..") {
95837
96529
  throw new Error(`Access denied: Path traversal detected`);
@@ -95908,6 +96600,9 @@ function getRateLimitResetTime5(ip) {
95908
96600
  function __setCreateFnAgent2(mock) {
95909
96601
  createFnAgent8 = mock;
95910
96602
  }
96603
+ function __setCreateResolvedAgentSession(mock) {
96604
+ createResolvedAgentSession2 = mock;
96605
+ }
95911
96606
  function __setBuildAgentChatPrompt(mock) {
95912
96607
  buildAgentChatPromptFn = mock;
95913
96608
  }
@@ -95915,9 +96610,11 @@ function __resetChatState() {
95915
96610
  chatStreamManager.reset();
95916
96611
  rateLimits5.clear();
95917
96612
  buildAgentChatPromptFn = void 0;
96613
+ createFnAgent8 = createFnAgent2;
96614
+ createResolvedAgentSession2 = createResolvedAgentSession;
95918
96615
  __setChatDiagnostics(null);
95919
96616
  }
95920
- 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;
95921
96618
  var init_chat = __esm({
95922
96619
  "../dashboard/src/chat.ts"() {
95923
96620
  "use strict";
@@ -95926,6 +96623,7 @@ var init_chat = __esm({
95926
96623
  init_src2();
95927
96624
  init_src2();
95928
96625
  createFnAgent8 = createFnAgent2;
96626
+ createResolvedAgentSession2 = createResolvedAgentSession;
95929
96627
  defaultDiagnostics = {
95930
96628
  log(message, ...args) {
95931
96629
  console.log(`[chat] ${message}`, ...args);
@@ -96044,13 +96742,49 @@ var init_chat = __esm({
96044
96742
  };
96045
96743
  chatStreamManager = new ChatStreamManager();
96046
96744
  ChatManager = class {
96047
- constructor(chatStore, rootDir, agentStore) {
96745
+ constructor(chatStore, rootDir, agentStore, pluginRunner, getSettings) {
96048
96746
  this.chatStore = chatStore;
96049
96747
  this.rootDir = rootDir;
96050
96748
  this.agentStore = agentStore;
96749
+ this.pluginRunner = pluginRunner;
96750
+ this.getSettings = getSettings;
96051
96751
  }
96052
96752
  agentStoreReady;
96053
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
+ }
96054
96788
  /**
96055
96789
  * Resolve the per-chat pi/Claude CLI SessionManager.
96056
96790
  *
@@ -96069,7 +96803,7 @@ var init_chat = __esm({
96069
96803
  * keep the CLI session stable across user messages.
96070
96804
  */
96071
96805
  resolveCliSessionManager(session) {
96072
- if (session.cliSessionFile && existsSync31(session.cliSessionFile)) {
96806
+ if (session.cliSessionFile && existsSync32(session.cliSessionFile)) {
96073
96807
  try {
96074
96808
  return SessionManager3.open(session.cliSessionFile);
96075
96809
  } catch (err) {
@@ -96203,6 +96937,7 @@ var init_chat = __esm({
96203
96937
  let accumulatedText = "";
96204
96938
  const toolCallsAccum = [];
96205
96939
  const pendingToolStarts = /* @__PURE__ */ new Map();
96940
+ let fallbackInfo;
96206
96941
  try {
96207
96942
  if (!session) {
96208
96943
  chatStreamManager.broadcast(sessionId, {
@@ -96228,30 +96963,12 @@ var init_chat = __esm({
96228
96963
  });
96229
96964
  return;
96230
96965
  }
96231
- const effectiveModelProvider = modelProvider ?? session.modelProvider ?? void 0;
96232
- 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;
96233
96971
  const needsTitle = session.title === null || session.title === void 0 || session.title.trim() === "";
96234
- if (needsTitle) {
96235
- (async () => {
96236
- try {
96237
- const generated = await summarizeTitle(
96238
- content.trim(),
96239
- this.rootDir,
96240
- effectiveModelProvider,
96241
- effectiveModelId
96242
- );
96243
- const title = generated ?? content.trim().slice(0, 60).trim();
96244
- if (title) {
96245
- this.chatStore.updateSession(sessionId, { title });
96246
- }
96247
- } catch {
96248
- const fallback2 = content.trim().slice(0, 60).trim();
96249
- if (fallback2) {
96250
- this.chatStore.updateSession(sessionId, { title: fallback2 });
96251
- }
96252
- }
96253
- })();
96254
- }
96255
96972
  await ensureEngineReady5();
96256
96973
  if (!createFnAgent8) {
96257
96974
  throw new Error("AI agent not available");
@@ -96282,6 +96999,35 @@ var init_chat = __esm({
96282
96999
  diagnostics6.warn(`Failed to build enriched system prompt for ${agent.id}: ${message}`);
96283
97000
  }
96284
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
+ }
96285
97031
  if (mentions.length > 0) {
96286
97032
  const mentionContext = await this.buildMentionContext(mentions, mentionAgents);
96287
97033
  if (mentionContext) {
@@ -96294,7 +97040,10 @@ ${mentionContext}`;
96294
97040
  const attachmentSummary = attachments && attachments.length > 0 ? `[User attached: ${attachments.map((attachment) => `${attachment.originalName} (${attachment.mimeType}, ${formatAttachmentSize(attachment.size)})`).join(", ")}]` : "";
96295
97041
  const promptContent = [attachmentSummary, resolvedContent].filter(Boolean).join("\n\n");
96296
97042
  const sessionManager = this.resolveCliSessionManager(session);
96297
- 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 = {
96298
97047
  cwd: this.rootDir,
96299
97048
  systemPrompt,
96300
97049
  tools: "coding",
@@ -96303,6 +97052,14 @@ ${mentionContext}`;
96303
97052
  defaultProvider: effectiveModelProvider,
96304
97053
  defaultModelId: effectiveModelId
96305
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
+ },
96306
97063
  onThinking: (delta) => {
96307
97064
  accumulatedThinking += delta;
96308
97065
  chatStreamManager.broadcast(sessionId, {
@@ -96343,16 +97100,35 @@ ${mentionContext}`;
96343
97100
  data: { toolName: name, isError, result }
96344
97101
  });
96345
97102
  }
96346
- });
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
+ }
96347
97115
  this.activeGenerations.set(sessionId, { abortController, agentResult });
96348
97116
  if (abortController.signal.aborted) {
96349
97117
  agentResult.session.dispose?.();
96350
97118
  return;
96351
97119
  }
96352
- await agentResult.session.prompt(promptContent);
97120
+ await promptWithFallback(agentResult.session, promptContent);
96353
97121
  if (abortController.signal.aborted) {
96354
97122
  return;
96355
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
+ }
96356
97132
  let responseText = "";
96357
97133
  const lastMessage = agentResult.session.state.messages.filter((m) => m.role === "assistant").pop();
96358
97134
  if (lastMessage?.content) {
@@ -96363,11 +97139,18 @@ ${mentionContext}`;
96363
97139
  }
96364
97140
  }
96365
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
+ }
96366
97149
  const assistantMessage = this.chatStore.addMessage(sessionId, {
96367
97150
  role: "assistant",
96368
97151
  content: finalResponseText,
96369
97152
  thinkingOutput: accumulatedThinking || void 0,
96370
- metadata: toolCallsAccum.length > 0 ? { toolCalls: toolCallsAccum } : void 0
97153
+ metadata: Object.keys(assistantMetadata).length > 0 ? assistantMetadata : void 0
96371
97154
  });
96372
97155
  chatStreamManager.broadcast(sessionId, {
96373
97156
  type: "done",
@@ -96391,6 +97174,7 @@ ${mentionContext}`;
96391
97174
  thinkingOutput: accumulatedThinking || void 0,
96392
97175
  metadata: {
96393
97176
  interrupted: true,
97177
+ ...fallbackInfo ? { fallback: fallbackInfo } : {},
96394
97178
  ...toolCallsAccum.length > 0 ? { toolCalls: toolCallsAccum } : {}
96395
97179
  }
96396
97180
  });
@@ -96453,7 +97237,7 @@ ${mentionContext}`;
96453
97237
  import { randomUUID as randomUUID19 } from "node:crypto";
96454
97238
  import { createReadStream as createReadStream2 } from "node:fs";
96455
97239
  import { mkdir as mkdir13, rm as rm2, writeFile as writeFile13 } from "node:fs/promises";
96456
- 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";
96457
97241
  function resolveAttachmentPath(rootDir, sessionId, filename) {
96458
97242
  const sessionDir = resolve20(rootDir, ".fusion", "chat-attachments", sessionId);
96459
97243
  const safeName = basename10(filename);
@@ -96711,7 +97495,7 @@ function registerChatRoutes(ctx, deps) {
96711
97495
  await mkdir13(sessionDir, { recursive: true });
96712
97496
  const sanitizedFilename = (file.originalname || "attachment").replace(/[^a-zA-Z0-9._-]/g, "_");
96713
97497
  const filename = `${Date.now()}-${sanitizedFilename}`;
96714
- const filePath = join41(sessionDir, filename);
97498
+ const filePath = join42(sessionDir, filename);
96715
97499
  await writeFile13(filePath, file.buffer);
96716
97500
  const attachment = {
96717
97501
  id: `att-${randomUUID19().slice(0, 8)}`,
@@ -101633,7 +102417,7 @@ var init_remote_auth = __esm({
101633
102417
 
101634
102418
  // ../dashboard/src/routes/register-settings-memory-routes.ts
101635
102419
  import { execFile as execFile5 } from "node:child_process";
101636
- import { homedir as homedir6 } from "node:os";
102420
+ import { homedir as homedir7 } from "node:os";
101637
102421
  import { promisify as promisify12 } from "node:util";
101638
102422
  function registerSettingsMemoryRoutes(ctx, deps) {
101639
102423
  const { router, options, store, runtimeLogger, getProjectContext: getProjectContext3, rethrowAsApiError: rethrowAsApiError8 } = ctx;
@@ -101729,7 +102513,7 @@ function registerSettingsMemoryRoutes(ctx, deps) {
101729
102513
  try {
101730
102514
  await execFileAsync7("mv", [tempPath, globalInstallPath], { timeout: 3e4 });
101731
102515
  } catch (error) {
101732
- const localBinDir = `${homedir6()}/.local/bin`;
102516
+ const localBinDir = `${homedir7()}/.local/bin`;
101733
102517
  const localInstallPath = `${localBinDir}/cloudflared`;
101734
102518
  attemptedCommands.push(`mkdir -p ${localBinDir}`);
101735
102519
  attemptedCommands.push(`mv ${tempPath} ${localInstallPath}`);
@@ -103095,7 +103879,7 @@ import * as os3 from "os";
103095
103879
  import * as path2 from "path";
103096
103880
  import * as fs2 from "node:fs";
103097
103881
  import { createRequire as createRequire3 } from "node:module";
103098
- import { join as join42, dirname as dirname13 } from "node:path";
103882
+ import { join as join43, dirname as dirname13 } from "node:path";
103099
103883
  function getNativePrebuildName() {
103100
103884
  const platform4 = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : process.platform === "win32" ? "win32" : "unknown";
103101
103885
  const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : "unknown";
@@ -103105,12 +103889,12 @@ function findInstalledNodePtyNativeDir() {
103105
103889
  try {
103106
103890
  const packageJsonPath = require2.resolve("node-pty/package.json");
103107
103891
  const pkgRoot = dirname13(packageJsonPath);
103108
- const releaseDir = join42(pkgRoot, "build", "Release");
103109
- if (fs2.existsSync(join42(releaseDir, "pty.node"))) {
103892
+ const releaseDir = join43(pkgRoot, "build", "Release");
103893
+ if (fs2.existsSync(join43(releaseDir, "pty.node"))) {
103110
103894
  return releaseDir;
103111
103895
  }
103112
- const prebuildDir = join42(pkgRoot, "prebuilds", getNativePrebuildName());
103113
- if (fs2.existsSync(join42(prebuildDir, "pty.node"))) {
103896
+ const prebuildDir = join43(pkgRoot, "prebuilds", getNativePrebuildName());
103897
+ if (fs2.existsSync(join43(prebuildDir, "pty.node"))) {
103114
103898
  return prebuildDir;
103115
103899
  }
103116
103900
  return null;
@@ -103136,8 +103920,8 @@ function ensureNodePtyNativePermissions() {
103136
103920
  candidateDirs.add(installedNativeDir);
103137
103921
  }
103138
103922
  for (const nativeDir of candidateDirs) {
103139
- const helperPath = join42(nativeDir, "spawn-helper");
103140
- const nativeModulePath = join42(nativeDir, "pty.node");
103923
+ const helperPath = join43(nativeDir, "spawn-helper");
103924
+ const nativeModulePath = join43(nativeDir, "pty.node");
103141
103925
  try {
103142
103926
  fs2.chmodSync(helperPath, 493);
103143
103927
  } catch {
@@ -103155,14 +103939,14 @@ function ensureNodePtyNativePermissions() {
103155
103939
  function findStagedNativeDir() {
103156
103940
  const prebuildName = getNativePrebuildName();
103157
103941
  if (process.env.FUSION_RUNTIME_DIR) {
103158
- const envPath = join42(process.env.FUSION_RUNTIME_DIR, prebuildName);
103159
- 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"))) {
103160
103944
  return envPath;
103161
103945
  }
103162
103946
  }
103163
103947
  const execDir = dirname13(process.execPath);
103164
- const nextToBinary = join42(execDir, "runtime", prebuildName);
103165
- if (fs2.existsSync(join42(nextToBinary, "pty.node"))) {
103948
+ const nextToBinary = join43(execDir, "runtime", prebuildName);
103949
+ if (fs2.existsSync(join43(nextToBinary, "pty.node"))) {
103166
103950
  return nextToBinary;
103167
103951
  }
103168
103952
  return null;
@@ -103182,7 +103966,7 @@ async function loadPtyModule() {
103182
103966
  process.env.NODE_PTY_SPAWN_HELPER_DIR = nativeDir;
103183
103967
  }
103184
103968
  process.env.FUSION_NATIVE_ASSETS_PATH = nativeDir;
103185
- const nativePath = join42(nativeDir, "pty.node");
103969
+ const nativePath = join43(nativeDir, "pty.node");
103186
103970
  if (fs2.existsSync(nativePath)) {
103187
103971
  try {
103188
103972
  const nativeModule = { exports: {} };
@@ -108872,7 +109656,7 @@ var init_terminal = __esm({
108872
109656
  });
108873
109657
 
108874
109658
  // ../dashboard/src/file-service.ts
108875
- 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";
108876
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";
108877
109661
  async function getTaskBasePath(store, taskId) {
108878
109662
  try {
@@ -108885,7 +109669,7 @@ async function getTaskBasePath(store, taskId) {
108885
109669
  }
108886
109670
  }
108887
109671
  const rootDir = store.getRootDir();
108888
- return resolve22(join43(rootDir, ".fusion", "tasks", taskId));
109672
+ return resolve22(join44(rootDir, ".fusion", "tasks", taskId));
108889
109673
  } catch (err) {
108890
109674
  const error = err;
108891
109675
  if (error.code === "ENOENT" || error.message && error.message.includes("not found")) {
@@ -108912,7 +109696,7 @@ function validatePath(basePath, filePath) {
108912
109696
  throw new FileServiceError(`Access denied: Absolute paths not allowed`, "EINVAL");
108913
109697
  }
108914
109698
  const resolvedBase = resolve22(basePath);
108915
- const resolvedPath = resolve22(join43(resolvedBase, decodedPath));
109699
+ const resolvedPath = resolve22(join44(resolvedBase, decodedPath));
108916
109700
  const relativePath = relative11(resolvedBase, resolvedPath);
108917
109701
  if (relativePath.startsWith("..") || relativePath.startsWith("../") || relativePath === "..") {
108918
109702
  throw new FileServiceError(`Access denied: Path traversal detected`, "EINVAL");
@@ -108941,7 +109725,7 @@ async function listFilesForBasePath(basePath, subPath) {
108941
109725
  const entries = await readdir8(targetPath, { withFileTypes: true });
108942
109726
  const fileNodes = [];
108943
109727
  for (const entry of entries) {
108944
- const entryPath = join43(targetPath, entry.name);
109728
+ const entryPath = join44(targetPath, entry.name);
108945
109729
  const entryStats = await stat7(entryPath);
108946
109730
  fileNodes.push({
108947
109731
  name: entry.name,
@@ -109268,7 +110052,7 @@ async function renameWorkspaceFile(store, workspace, filePath, newName) {
109268
110052
  }
109269
110053
  throw err;
109270
110054
  }
109271
- const destPath = join43(dirname14(resolvedPath), newName);
110055
+ const destPath = join44(dirname14(resolvedPath), newName);
109272
110056
  const destRelative = relative11(resolve22(workspaceBase), destPath);
109273
110057
  if (destRelative.startsWith("..") || destRelative.startsWith("../") || destRelative === "..") {
109274
110058
  throw new FileServiceError("Destination would be outside workspace", "EINVAL");
@@ -109361,7 +110145,7 @@ function isHiddenPathSegment(name) {
109361
110145
  return name.startsWith(".");
109362
110146
  }
109363
110147
  async function walkDirForMarkdown(basePath, currentRelative, results, options) {
109364
- const currentPath = currentRelative ? join43(basePath, currentRelative) : basePath;
110148
+ const currentPath = currentRelative ? join44(basePath, currentRelative) : basePath;
109365
110149
  let entries;
109366
110150
  try {
109367
110151
  entries = await readdir8(currentPath, { withFileTypes: true });
@@ -109369,7 +110153,7 @@ async function walkDirForMarkdown(basePath, currentRelative, results, options) {
109369
110153
  return;
109370
110154
  }
109371
110155
  for (const entry of entries) {
109372
- const entryRelativePath = currentRelative ? join43(currentRelative, entry.name) : entry.name;
110156
+ const entryRelativePath = currentRelative ? join44(currentRelative, entry.name) : entry.name;
109373
110157
  if (entry.isDirectory()) {
109374
110158
  if (MARKDOWN_SCAN_EXCLUDED_DIRS.has(entry.name)) {
109375
110159
  continue;
@@ -109386,7 +110170,7 @@ async function walkDirForMarkdown(basePath, currentRelative, results, options) {
109386
110170
  if (!options.showHidden && isHiddenPathSegment(entry.name)) {
109387
110171
  continue;
109388
110172
  }
109389
- const fullPath = join43(basePath, entryRelativePath);
110173
+ const fullPath = join44(basePath, entryRelativePath);
109390
110174
  let fileStats;
109391
110175
  try {
109392
110176
  fileStats = await stat7(fullPath);
@@ -109436,7 +110220,7 @@ async function scanMarkdownFiles(store, options) {
109436
110220
  return;
109437
110221
  }
109438
110222
  for (const entry of entries) {
109439
- const entryRelativePath = relativeDir ? join43(relativeDir, entry.name) : entry.name;
110223
+ const entryRelativePath = relativeDir ? join44(relativeDir, entry.name) : entry.name;
109440
110224
  let shouldRecurse = entry.isDirectory();
109441
110225
  if (!shouldRecurse && typeof entry.isSymbolicLink === "function" && entry.isSymbolicLink()) {
109442
110226
  let symlinkPath;
@@ -109526,8 +110310,8 @@ async function searchWorkspaceFiles(store, workspace, query) {
109526
110310
  if (entry.isDirectory() && EXCLUDED_DIRS.has(entry.name)) {
109527
110311
  continue;
109528
110312
  }
109529
- const fullPath = join43(dir2, entry.name);
109530
- const relPath = join43(relativeDir, entry.name);
110313
+ const fullPath = join44(dir2, entry.name);
110314
+ const relPath = join44(relativeDir, entry.name);
109531
110315
  if (entry.isFile()) {
109532
110316
  if (entry.name.toLowerCase().includes(lowerQuery)) {
109533
110317
  results.push({
@@ -109548,8 +110332,8 @@ async function copyDirectoryRecursive(source, destination) {
109548
110332
  await mkdir14(destination, { recursive: true });
109549
110333
  const entries = await readdir8(source, { withFileTypes: true });
109550
110334
  for (const entry of entries) {
109551
- const sourcePath = join43(source, entry.name);
109552
- const destPath = join43(destination, entry.name);
110335
+ const sourcePath = join44(source, entry.name);
110336
+ const destPath = join44(destination, entry.name);
109553
110337
  if (entry.isDirectory()) {
109554
110338
  await copyDirectoryRecursive(sourcePath, destPath);
109555
110339
  } else {
@@ -113618,7 +114402,7 @@ var require_BufferList = __commonJS({
113618
114402
  this.head = this.tail = null;
113619
114403
  this.length = 0;
113620
114404
  };
113621
- BufferList.prototype.join = function join71(s) {
114405
+ BufferList.prototype.join = function join72(s) {
113622
114406
  if (this.length === 0) return "";
113623
114407
  var p = this.head;
113624
114408
  var ret = "" + p.data;
@@ -136127,7 +136911,7 @@ var init_exec_file = __esm({
136127
136911
 
136128
136912
  // ../dashboard/src/routes/register-project-routes.ts
136129
136913
  import * as fsPromises from "node:fs/promises";
136130
- 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";
136131
136915
  var access9, stat8, mkdir15, readdir9, rm3, registerProjectRoutes;
136132
136916
  var init_register_project_routes = __esm({
136133
136917
  "../dashboard/src/routes/register-project-routes.ts"() {
@@ -136350,7 +137134,7 @@ var init_register_project_routes = __esm({
136350
137134
  }
136351
137135
  }
136352
137136
  let hasFusionDir = false;
136353
- const fusionDirPath = join44(normalizedPath, ".fusion");
137137
+ const fusionDirPath = join45(normalizedPath, ".fusion");
136354
137138
  try {
136355
137139
  await access9(fusionDirPath);
136356
137140
  hasFusionDir = true;
@@ -136414,8 +137198,8 @@ var init_register_project_routes = __esm({
136414
137198
  const entries = await readdir9(searchPath, { withFileTypes: true });
136415
137199
  for (const entry of entries) {
136416
137200
  if (!entry.isDirectory()) continue;
136417
- const dirPath = join44(searchPath, entry.name);
136418
- if (isValidSqliteDatabaseFile(join44(dirPath, ".fusion", "fusion.db"))) {
137201
+ const dirPath = join45(searchPath, entry.name);
137202
+ if (isValidSqliteDatabaseFile(join45(dirPath, ".fusion", "fusion.db"))) {
136419
137203
  detected.push({
136420
137204
  path: dirPath,
136421
137205
  suggestedName: entry.name,
@@ -137429,14 +138213,14 @@ var init_register_docker_provisioning_routes = __esm({
137429
138213
 
137430
138214
  // ../dashboard/src/auth-paths.ts
137431
138215
  import path3 from "node:path";
137432
- import { homedir as homedir7 } from "node:os";
137433
- 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()) {
137434
138218
  return path3.join(home, ".fusion", "agent");
137435
138219
  }
137436
- function getFusionAuthPath2(home = process.env.HOME || process.env.USERPROFILE || homedir7()) {
138220
+ function getFusionAuthPath2(home = process.env.HOME || process.env.USERPROFILE || homedir8()) {
137437
138221
  return path3.join(getFusionAgentDir2(home), "auth.json");
137438
138222
  }
137439
- 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()) {
137440
138224
  return [
137441
138225
  path3.join(home, ".fusion", "agent", "auth.json"),
137442
138226
  path3.join(home, ".fusion", "auth.json"),
@@ -140739,7 +141523,7 @@ Rules:
140739
141523
  import { createWriteStream } from "node:fs";
140740
141524
  import * as fsPromises2 from "node:fs/promises";
140741
141525
  import { tmpdir as tmpdir4 } from "node:os";
140742
- import { join as join45, resolve as resolve23 } from "node:path";
141526
+ import { join as join46, resolve as resolve23 } from "node:path";
140743
141527
  import { Readable } from "node:stream";
140744
141528
  import { pipeline as streamPipeline } from "node:stream/promises";
140745
141529
  function registerAgentImportExportRoutes(ctx) {
@@ -140780,7 +141564,7 @@ function registerAgentImportExportRoutes(ctx) {
140780
141564
  } else if (typeof outputDir === "string") {
140781
141565
  throw badRequest("outputDir cannot be empty");
140782
141566
  } else {
140783
- resolvedOutputDir = await mkdtemp(join45(tmpdir4(), "fusion-agent-export-"));
141567
+ resolvedOutputDir = await mkdtemp(join46(tmpdir4(), "fusion-agent-export-"));
140784
141568
  }
140785
141569
  const result = await exportAgentsToDirectory2(agentsToExport, resolvedOutputDir, {
140786
141570
  companyName: typeof companyName === "string" ? companyName : void 0,
@@ -140907,7 +141691,7 @@ ${body}`;
140907
141691
  return result;
140908
141692
  }
140909
141693
  const safeCompanySlug = companySlug ? slugifyPathSegment2(companySlug, "unknown-company") : "unknown-company";
140910
- const skillsBaseDir = join45(projectRoot, "skills", "imported", safeCompanySlug);
141694
+ const skillsBaseDir = join46(projectRoot, "skills", "imported", safeCompanySlug);
140911
141695
  const usedSlugs = /* @__PURE__ */ new Set();
140912
141696
  for (const skill of skills) {
140913
141697
  const name = typeof skill.name === "string" && skill.name.trim().length > 0 ? skill.name.trim() : null;
@@ -140924,8 +141708,8 @@ ${body}`;
140924
141708
  skillSlug = `${skillSlug}-${counter}`;
140925
141709
  }
140926
141710
  usedSlugs.add(skillSlug);
140927
- const skillDir = join45(skillsBaseDir, skillSlug);
140928
- const skillPath = join45(skillDir, "SKILL.md");
141711
+ const skillDir = join46(skillsBaseDir, skillSlug);
141712
+ const skillPath = join46(skillDir, "SKILL.md");
140929
141713
  try {
140930
141714
  await access10(skillPath);
140931
141715
  result.skipped.push(name);
@@ -141095,8 +141879,8 @@ ${body}`;
141095
141879
  const archiveUrl = `https://github.com/${repoOwner}/${repoName}/archive/refs/heads/main.tar.gz`;
141096
141880
  let tempDir = null;
141097
141881
  try {
141098
- tempDir = await mkdtemp(join45(tmpdir4(), `fn-agent-import-${importCompanySlug}-`));
141099
- 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");
141100
141884
  const downloadController = new AbortController();
141101
141885
  const downloadTimeout = setTimeout(() => downloadController.abort(), 3e4);
141102
141886
  let archiveResponse;
@@ -142377,7 +143161,7 @@ import * as path4 from "node:path";
142377
143161
  import { readFile as readFile20 } from "node:fs/promises";
142378
143162
  import * as https from "node:https";
142379
143163
  import * as child_process from "node:child_process";
142380
- function getHomeDir5() {
143164
+ function getHomeDir6() {
142381
143165
  return process.env.HOME || process.env.USERPROFILE || os4.homedir();
142382
143166
  }
142383
143167
  function execFileAsync5(file, args, options) {
@@ -142887,8 +143671,8 @@ async function fetchClaudeUsage(authStorage) {
142887
143671
  }
142888
143672
  if (!creds) {
142889
143673
  const credPaths = [
142890
- path4.join(getHomeDir5(), ".claude", ".credentials.json"),
142891
- path4.join(getHomeDir5(), ".config", "claude", ".credentials.json")
143674
+ path4.join(getHomeDir6(), ".claude", ".credentials.json"),
143675
+ path4.join(getHomeDir6(), ".config", "claude", ".credentials.json")
142892
143676
  ];
142893
143677
  for (const p of credPaths) {
142894
143678
  try {
@@ -143048,7 +143832,7 @@ async function fetchCodexUsage() {
143048
143832
  status: "no-auth",
143049
143833
  windows: []
143050
143834
  };
143051
- const codexHome = process.env.CODEX_HOME || path4.join(getHomeDir5(), ".codex");
143835
+ const codexHome = process.env.CODEX_HOME || path4.join(getHomeDir6(), ".codex");
143052
143836
  const authPath = path4.join(codexHome, "auth.json");
143053
143837
  let auth = null;
143054
143838
  try {
@@ -143139,7 +143923,7 @@ async function fetchGeminiUsage() {
143139
143923
  status: "no-auth",
143140
143924
  windows: []
143141
143925
  };
143142
- const oauthPath = path4.join(getHomeDir5(), ".gemini", "oauth_creds.json");
143926
+ const oauthPath = path4.join(getHomeDir6(), ".gemini", "oauth_creds.json");
143143
143927
  let oauthCreds = null;
143144
143928
  try {
143145
143929
  oauthCreds = JSON.parse(await readFile20(oauthPath, "utf-8"));
@@ -143155,7 +143939,7 @@ async function fetchGeminiUsage() {
143155
143939
  const claims = decodeJwtPayload(oauthCreds.id_token);
143156
143940
  if (claims?.email) usage.email = claims.email;
143157
143941
  }
143158
- const settingsPath = path4.join(getHomeDir5(), ".gemini", "settings.json");
143942
+ const settingsPath = path4.join(getHomeDir6(), ".gemini", "settings.json");
143159
143943
  try {
143160
143944
  const settings = JSON.parse(await readFile20(settingsPath, "utf-8"));
143161
143945
  const authType = settings?.security?.auth?.selectedType;
@@ -143792,6 +144576,13 @@ var init_register_auth_routes = __esm({
143792
144576
  }
143793
144577
  return key.slice(0, 3) + "\u2022\u2022\u2022\u2022\u2022" + key.slice(-4);
143794
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
+ }
143795
144586
  const loginInProgress = /* @__PURE__ */ new Map();
143796
144587
  const OAUTH_SESSION_TTL_MS = 5 * 60 * 1e3;
143797
144588
  const oauthSessions = /* @__PURE__ */ new Map();
@@ -143852,6 +144643,40 @@ var init_register_auth_routes = __esm({
143852
144643
  path: `${redirectUriUrl.pathname}${redirectUriUrl.search}`
143853
144644
  };
143854
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
+ }
143855
144680
  router.get("/auth/status", async (_req, res) => {
143856
144681
  try {
143857
144682
  const storage = getAuthStorage();
@@ -143860,7 +144685,7 @@ var init_register_auth_routes = __esm({
143860
144685
  const providers = oauthProviders.map((p) => ({
143861
144686
  id: p.id,
143862
144687
  name: p.name,
143863
- authenticated: storage.hasAuth(p.id),
144688
+ authenticated: storage.hasAuth(p.id) && !isExpiredOauthCredential(p.id, storage),
143864
144689
  type: "oauth",
143865
144690
  loginInProgress: loginInProgress.has(p.id)
143866
144691
  }));
@@ -144114,7 +144939,29 @@ var init_register_auth_routes = __esm({
144114
144939
  throw badRequest(`Unknown provider: ${provider}`);
144115
144940
  }
144116
144941
  const abortController = new AbortController();
144117
- 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);
144118
144965
  let authResolve;
144119
144966
  let authReject;
144120
144967
  const authUrlPromise = new Promise((resolve44, reject2) => {
@@ -144123,12 +144970,16 @@ var init_register_auth_routes = __esm({
144123
144970
  });
144124
144971
  const loginPromise = storage.login(provider, {
144125
144972
  onAuth: (info) => {
144126
- authResolve({ url: info.url, instructions: info.instructions });
144127
- },
144128
- onPrompt: async (prompt) => {
144129
- if (prompt.allowEmpty) return "";
144130
- return prompt.placeholder || "";
144973
+ authResolve({
144974
+ url: info.url,
144975
+ instructions: appendManualCodeHint(info.instructions, provider, origin)
144976
+ });
144131
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,
144132
144983
  onProgress: () => {
144133
144984
  },
144134
144985
  // no-op for web UI
@@ -144147,7 +144998,7 @@ var init_register_auth_routes = __esm({
144147
144998
  const authInfo = await authUrlPromise;
144148
144999
  clearTimeout(timeout2);
144149
145000
  let responseUrl = authInfo.url;
144150
- if (origin && !isLocalhostOrigin(origin)) {
145001
+ if (shouldRewriteOauthRedirect(provider, origin)) {
144151
145002
  const rewritten = rewriteAuthUrl(authInfo.url, origin);
144152
145003
  setOauthSession(rewritten.state, {
144153
145004
  port: rewritten.port,
@@ -144156,7 +145007,11 @@ var init_register_auth_routes = __esm({
144156
145007
  });
144157
145008
  responseUrl = rewritten.url;
144158
145009
  }
144159
- res.json({ url: responseUrl, instructions: authInfo.instructions });
145010
+ res.json({
145011
+ url: responseUrl,
145012
+ instructions: authInfo.instructions,
145013
+ manualCode: pendingLogin.manualCode
145014
+ });
144160
145015
  } catch (err) {
144161
145016
  if (err instanceof ApiError) {
144162
145017
  throw err;
@@ -144178,7 +145033,9 @@ var init_register_auth_routes = __esm({
144178
145033
  return;
144179
145034
  }
144180
145035
  loginInProgress.delete(provider);
144181
- activeLogin.abort();
145036
+ activeLogin.inputSubmitted = true;
145037
+ activeLogin.rejectInput(new Error("cancelled"));
145038
+ activeLogin.abortController.abort();
144182
145039
  res.json({ success: true, cancelled: true });
144183
145040
  } catch (err) {
144184
145041
  if (err instanceof ApiError) {
@@ -144187,6 +145044,33 @@ var init_register_auth_routes = __esm({
144187
145044
  rethrowAsApiError8(err);
144188
145045
  }
144189
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
+ });
144190
145074
  router.get("/auth/oauth-callback", async (req, res) => {
144191
145075
  try {
144192
145076
  const error = typeof req.query.error === "string" ? req.query.error : void 0;
@@ -145518,15 +146402,15 @@ var init_register_runtime_provider_routes = __esm({
145518
146402
  });
145519
146403
 
145520
146404
  // ../dashboard/src/cli-package-version.ts
145521
- import { existsSync as existsSync33, readFileSync as readFileSync11 } from "node:fs";
146405
+ import { existsSync as existsSync34, readFileSync as readFileSync12 } from "node:fs";
145522
146406
  import { dirname as dirname16, resolve as resolve24 } from "node:path";
145523
146407
  import { fileURLToPath as fileURLToPath4 } from "node:url";
145524
146408
  function readCliPackageVersion(pkgPath) {
145525
- if (!existsSync33(pkgPath)) {
146409
+ if (!existsSync34(pkgPath)) {
145526
146410
  return null;
145527
146411
  }
145528
146412
  try {
145529
- const parsed = JSON.parse(readFileSync11(pkgPath, "utf-8"));
146413
+ const parsed = JSON.parse(readFileSync12(pkgPath, "utf-8"));
145530
146414
  if (parsed.name === CLI_PACKAGE_NAME && typeof parsed.version === "string" && parsed.version.length > 0) {
145531
146415
  return {
145532
146416
  packageJsonPath: pkgPath,
@@ -145728,9 +146612,9 @@ var init_register_fn_binary_routes = __esm({
145728
146612
  });
145729
146613
 
145730
146614
  // ../dashboard/src/update-check.ts
145731
- import { readFileSync as readFileSync12 } from "node:fs";
146615
+ import { readFileSync as readFileSync13 } from "node:fs";
145732
146616
  import { mkdir as mkdir17, rm as rm5, writeFile as writeFile15 } from "node:fs/promises";
145733
- import { join as join47 } from "node:path";
146617
+ import { join as join48 } from "node:path";
145734
146618
  function ttlForFrequency(frequency) {
145735
146619
  switch (frequency) {
145736
146620
  case "manual":
@@ -145744,7 +146628,7 @@ function ttlForFrequency(frequency) {
145744
146628
  }
145745
146629
  }
145746
146630
  function getCachePath(fusionDir) {
145747
- return join47(fusionDir, CACHE_FILENAME);
146631
+ return join48(fusionDir, CACHE_FILENAME);
145748
146632
  }
145749
146633
  function parseVersion(version) {
145750
146634
  return version.split(".").slice(0, 3).map((part) => Number.parseInt(part, 10)).map((value) => Number.isFinite(value) ? value : 0);
@@ -145768,7 +146652,7 @@ function isValidResult(value) {
145768
146652
  }
145769
146653
  function readCachedUpdateCheck(fusionDir) {
145770
146654
  try {
145771
- const raw = readFileSync12(getCachePath(fusionDir), "utf-8");
146655
+ const raw = readFileSync13(getCachePath(fusionDir), "utf-8");
145772
146656
  const parsed = JSON.parse(raw);
145773
146657
  return isValidResult(parsed) ? parsed : null;
145774
146658
  } catch {
@@ -150415,7 +151299,7 @@ var init_todo_routes = __esm({
150415
151299
 
150416
151300
  // ../dashboard/src/dev-server-detect.ts
150417
151301
  import { glob, readFile as readFile21 } from "node:fs/promises";
150418
- 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";
150419
151303
  async function readPackageJson(filePath) {
150420
151304
  try {
150421
151305
  const raw = await readFile21(filePath, "utf-8");
@@ -150458,7 +151342,7 @@ function scoreCandidate(scriptName, pkg) {
150458
151342
  async function collectWorkspacePackageJsons(projectRoot) {
150459
151343
  const discovered = /* @__PURE__ */ new Set();
150460
151344
  try {
150461
- await readFile21(join48(projectRoot, "pnpm-workspace.yaml"), "utf-8");
151345
+ await readFile21(join49(projectRoot, "pnpm-workspace.yaml"), "utf-8");
150462
151346
  } catch {
150463
151347
  }
150464
151348
  for (const pattern of ["packages/*/package.json", "apps/*/package.json"]) {
@@ -150492,7 +151376,7 @@ function toSource(projectRoot, packageJsonPath) {
150492
151376
  async function detectDevServerScripts(projectRoot) {
150493
151377
  const root = resolve25(projectRoot);
150494
151378
  const candidates = [];
150495
- const rootPackagePath = join48(root, "package.json");
151379
+ const rootPackagePath = join49(root, "package.json");
150496
151380
  const rootPackage = await readPackageJson(rootPackagePath);
150497
151381
  if (rootPackage) {
150498
151382
  for (const script of extractScripts(rootPackage)) {
@@ -150559,9 +151443,9 @@ var init_dev_server_detect = __esm({
150559
151443
 
150560
151444
  // ../dashboard/src/dev-server-store.ts
150561
151445
  import { mkdir as mkdir18, readFile as readFile22, writeFile as writeFile16 } from "node:fs/promises";
150562
- 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";
150563
151447
  function devServerFilePath(projectDir) {
150564
- return join49(resolve26(projectDir), ".fusion", "dev-server.json");
151448
+ return join50(resolve26(projectDir), ".fusion", "dev-server.json");
150565
151449
  }
150566
151450
  function normalizeState(candidate) {
150567
151451
  const defaults = DEV_SERVER_DEFAULT_STATE();
@@ -151815,7 +152699,7 @@ Your job is to refine task descriptions based on the user's selected refinement
151815
152699
 
151816
152700
  // ../dashboard/src/routes.ts
151817
152701
  import multer from "multer";
151818
- 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";
151819
152703
  import * as nodeFs from "node:fs";
151820
152704
  import os5 from "node:os";
151821
152705
  import v8 from "node:v8";
@@ -151843,8 +152727,8 @@ async function getPiPackageManagerAgentDir() {
151843
152727
  const fusionAgentDir = getFusionAgentDir();
151844
152728
  const legacyAgentDir = getLegacyPiAgentDir();
151845
152729
  const [fusionSettings, legacySettings, legacyExists, fusionExists] = await Promise.all([
151846
- readJsonObject3(join50(fusionAgentDir, "settings.json")),
151847
- readJsonObject3(join50(legacyAgentDir, "settings.json")),
152730
+ readJsonObject3(join51(fusionAgentDir, "settings.json")),
152731
+ readJsonObject3(join51(legacyAgentDir, "settings.json")),
151848
152732
  pathExists(legacyAgentDir),
151849
152733
  pathExists(fusionAgentDir)
151850
152734
  ]);
@@ -151871,9 +152755,9 @@ async function discoverDashboardPiExtensions(cwd) {
151871
152755
  const { DefaultPackageManager: DefaultPackageManager5 } = await import("@mariozechner/pi-coding-agent");
151872
152756
  const [agentDir, legacyGlobalSettings, fusionGlobalSettings, projectSettings] = await Promise.all([
151873
152757
  getPiPackageManagerAgentDir(),
151874
- readJsonObject3(join50(getLegacyPiAgentDir(), "settings.json")),
151875
- readJsonObject3(join50(getFusionAgentDir(), "settings.json")),
151876
- 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"))
151877
152761
  ]);
151878
152762
  const globalSettings = { ...legacyGlobalSettings, ...fusionGlobalSettings };
151879
152763
  const mergedSettings = { ...globalSettings, ...projectSettings };
@@ -154287,7 +155171,7 @@ Description: ${step.description}`
154287
155171
  return;
154288
155172
  }
154289
155173
  }
154290
- const { resolve: resolve44, dirname: dirname34, join: join71 } = await import("node:path");
155174
+ const { resolve: resolve44, dirname: dirname34, join: join72 } = await import("node:path");
154291
155175
  const { readdir: readdir12, stat: stat12 } = await import("node:fs/promises");
154292
155176
  const rawPath = req.query.path || process.env.HOME || process.env.USERPROFILE || "/";
154293
155177
  const showHidden = req.query.showHidden === "true";
@@ -154312,7 +155196,7 @@ Description: ${step.description}`
154312
155196
  for (const entry of dirEntries) {
154313
155197
  if (!entry.isDirectory()) continue;
154314
155198
  if (!showHidden && entry.name.startsWith(".")) continue;
154315
- const entryPath = join71(resolvedPath, entry.name);
155199
+ const entryPath = join72(resolvedPath, entry.name);
154316
155200
  let hasChildren = false;
154317
155201
  try {
154318
155202
  const subEntries = await readdir12(entryPath, { withFileTypes: true });
@@ -159920,8 +160804,8 @@ var init_auth_middleware = __esm({
159920
160804
  // ../dashboard/src/server.ts
159921
160805
  import express from "express";
159922
160806
  import { randomUUID as randomUUID25 } from "node:crypto";
159923
- import { join as join51, dirname as dirname19 } from "node:path";
159924
- 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";
159925
160809
  import { fileURLToPath as fileURLToPath5 } from "node:url";
159926
160810
  import { createSecureServer as createHttp2SecureServer } from "node:http2";
159927
160811
  function parseVersion2(version) {
@@ -160023,11 +160907,11 @@ function loadTlsCredentialsFromEnv(env = process.env) {
160023
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)."
160024
160908
  );
160025
160909
  }
160026
- const cert = certInline ? Buffer.from(certInline) : readFileSync13(certFile);
160027
- 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);
160028
160912
  const caInline = env.FUSION_TLS_CA;
160029
160913
  const caFile = env.FUSION_TLS_CA_FILE;
160030
- const ca = caInline ? Buffer.from(caInline) : caFile ? readFileSync13(caFile) : void 0;
160914
+ const ca = caInline ? Buffer.from(caInline) : caFile ? readFileSync14(caFile) : void 0;
160031
160915
  return { cert, key, ca };
160032
160916
  }
160033
160917
  function createServer(store, options) {
@@ -160103,11 +160987,11 @@ function createServer(store, options) {
160103
160987
  getTerminalService(store.getRootDir());
160104
160988
  const isHeadless = options?.headless === true;
160105
160989
  const execDir = dirname19(process.execPath);
160106
- 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");
160107
160991
  if (!isHeadless) {
160108
160992
  app.get("/version.json", (_req, res) => {
160109
160993
  res.setHeader("Cache-Control", "no-store, max-age=0");
160110
- res.sendFile(join51(clientDir, "version.json"), (err) => {
160994
+ res.sendFile(join52(clientDir, "version.json"), (err) => {
160111
160995
  if (err) {
160112
160996
  res.status(404).json({ version: null });
160113
160997
  }
@@ -160371,7 +161255,13 @@ data: ${JSON.stringify({ type: event.type, data: event.data })}
160371
161255
  });
160372
161256
  }
160373
161257
  const chatAgentStore = new AgentStore({ rootDir: store.getFusionDir() });
160374
- 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
+ );
160375
161265
  const runAiSessionCleanup = (maxAgeMs, source) => {
160376
161266
  const result = aiSessionStore.cleanupStaleSessions(maxAgeMs);
160377
161267
  runtimeLogger.info("AI session cleanup summary", {
@@ -160564,7 +161454,7 @@ data: ${JSON.stringify({ type: event.type, data: event.data })}
160564
161454
  });
160565
161455
  if (!isHeadless) {
160566
161456
  app.get("/{*splat}", (_req, res) => {
160567
- res.sendFile(join51(clientDir, "index.html"));
161457
+ res.sendFile(join52(clientDir, "index.html"));
160568
161458
  });
160569
161459
  }
160570
161460
  const dashboardApp = app;
@@ -161019,7 +161909,7 @@ var init_server = __esm({
161019
161909
 
161020
161910
  // ../dashboard/src/skills-adapter.ts
161021
161911
  import { access as access11, readFile as readFile23, writeFile as writeFile17, mkdir as mkdir19, readdir as readdir10, stat as stat10 } from "node:fs/promises";
161022
- 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";
161023
161913
  async function pathExists2(path5) {
161024
161914
  try {
161025
161915
  await access11(path5);
@@ -161275,7 +162165,7 @@ function createSkillsAdapter(options) {
161275
162165
  } catch {
161276
162166
  skillDir = dirname20(skill.path);
161277
162167
  }
161278
- const skillMdPath = join52(skillDir, "SKILL.md");
162168
+ const skillMdPath = join53(skillDir, "SKILL.md");
161279
162169
  let skillMd = "";
161280
162170
  try {
161281
162171
  skillMd = await readFile23(skillMdPath, "utf-8");
@@ -161483,7 +162373,7 @@ function normalizeEntry(entry) {
161483
162373
  };
161484
162374
  }
161485
162375
  function getProjectSettingsPath(rootDir) {
161486
- return join52(rootDir, ".fusion", "settings.json");
162376
+ return join53(rootDir, ".fusion", "settings.json");
161487
162377
  }
161488
162378
  var MIN_PUBLIC_SEARCH_QUERY_LENGTH;
161489
162379
  var init_skills_adapter = __esm({
@@ -161726,33 +162616,33 @@ var init_port_prompt = __esm({
161726
162616
  });
161727
162617
 
161728
162618
  // src/commands/provider-settings.ts
161729
- import { existsSync as existsSync35, readFileSync as readFileSync14, writeFileSync as writeFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
161730
- 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";
161731
162621
  function siblingAgentDir2(agentDir, siblingRoot) {
161732
162622
  if (basename15(agentDir) !== "agent") {
161733
162623
  return void 0;
161734
162624
  }
161735
- return join53(dirname21(dirname21(agentDir)), siblingRoot, "agent");
162625
+ return join54(dirname21(dirname21(agentDir)), siblingRoot, "agent");
161736
162626
  }
161737
162627
  function readJsonObject4(path5) {
161738
- if (!existsSync35(path5)) {
162628
+ if (!existsSync36(path5)) {
161739
162629
  return {};
161740
162630
  }
161741
162631
  try {
161742
- const parsed = JSON.parse(readFileSync14(path5, "utf-8"));
162632
+ const parsed = JSON.parse(readFileSync15(path5, "utf-8"));
161743
162633
  return parsed !== null && typeof parsed === "object" ? parsed : {};
161744
162634
  } catch {
161745
162635
  return {};
161746
162636
  }
161747
162637
  }
161748
162638
  function createReadOnlyProviderSettingsView(cwd, agentDir) {
161749
- const fusionAgentDir = agentDir.includes(`${join53(".fusion", "agent")}`) ? agentDir : siblingAgentDir2(agentDir, ".fusion");
161750
- const legacyAgentDir = agentDir.includes(`${join53(".pi", "agent")}`) ? agentDir : siblingAgentDir2(agentDir, ".pi");
161751
- const legacyGlobalSettings = legacyAgentDir ? readJsonObject4(join53(legacyAgentDir, "settings.json")) : {};
161752
- const fusionGlobalSettings = fusionAgentDir ? readJsonObject4(join53(fusionAgentDir, "settings.json")) : {};
161753
- 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"));
161754
162644
  const globalSettings = { ...legacyGlobalSettings, ...directGlobalSettings, ...fusionGlobalSettings };
161755
- const fusionProjectSettings = readJsonObject4(join53(cwd, ".fusion", "settings.json"));
162645
+ const fusionProjectSettings = readJsonObject4(join54(cwd, ".fusion", "settings.json"));
161756
162646
  const mergedSettings = { ...globalSettings, ...fusionProjectSettings };
161757
162647
  return {
161758
162648
  getGlobalSettings: () => structuredClone(globalSettings),
@@ -161767,7 +162657,6 @@ var init_provider_settings = __esm({
161767
162657
  });
161768
162658
 
161769
162659
  // src/commands/provider-auth.ts
161770
- import { existsSync as existsSync36, readFileSync as readFileSync15 } from "node:fs";
161771
162660
  import { getOAuthProvider as getOAuthProvider2 } from "@mariozechner/pi-ai/oauth";
161772
162661
  function getProviderDisplayName(providerId) {
161773
162662
  const knownProviderNames = new Map(
@@ -161783,7 +162672,10 @@ function wrapAuthStorageWithApiKeyProviders(authStorage, modelRegistry, readFall
161783
162672
  reload: () => mergedAuthStorage.reload(),
161784
162673
  getOAuthProviders: () => mergedAuthStorage.getOAuthProviders().filter((provider) => !OAUTH_TO_API_KEY_RECLASSIFICATIONS.has(provider.id)).map((provider) => ({ id: provider.id, name: provider.name })),
161785
162674
  hasAuth: (provider) => mergedAuthStorage.hasAuth(provider),
161786
- login: (providerId, callbacks) => mergedAuthStorage.login(providerId, callbacks),
162675
+ login: (providerId, callbacks) => mergedAuthStorage.login(
162676
+ providerId,
162677
+ callbacks
162678
+ ),
161787
162679
  logout: (provider) => mergedAuthStorage.logout(provider),
161788
162680
  getApiKeyProviders: () => {
161789
162681
  const oauthProviderIds = new Set(
@@ -161822,13 +162714,28 @@ function wrapAuthStorageWithApiKeyProviders(authStorage, modelRegistry, readFall
161822
162714
  }
161823
162715
  function mergeAuthStorageReads(authStorage, readFallbackAuthStorages = []) {
161824
162716
  const readAuthStorages = [authStorage, ...readFallbackAuthStorages];
161825
- const getCredential = (providerId) => {
161826
- for (const storage of readAuthStorages) {
161827
- const credential = storage.get(providerId);
161828
- 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));
161829
162721
  }
161830
- return void 0;
162722
+ return best;
161831
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();
161832
162739
  return new Proxy(authStorage, {
161833
162740
  get(target, prop, receiver) {
161834
162741
  if (prop === "reload") {
@@ -161836,6 +162743,7 @@ function mergeAuthStorageReads(authStorage, readFallbackAuthStorages = []) {
161836
162743
  for (const storage of readAuthStorages) {
161837
162744
  storage.reload();
161838
162745
  }
162746
+ syncFallbackOauthCredentials();
161839
162747
  };
161840
162748
  }
161841
162749
  if (prop === "get") {
@@ -161848,13 +162756,17 @@ function mergeAuthStorageReads(authStorage, readFallbackAuthStorages = []) {
161848
162756
  return (provider) => readAuthStorages.some((storage) => storage.hasAuth(provider));
161849
162757
  }
161850
162758
  if (prop === "getAll") {
161851
- return () => ({
161852
- ...readFallbackAuthStorages.reduce(
161853
- (merged, storage) => ({ ...merged, ...storage.getAll() }),
161854
- {}
161855
- ),
161856
- ...target.getAll()
161857
- });
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
+ };
161858
162770
  }
161859
162771
  if (prop === "list") {
161860
162772
  return () => Array.from(new Set(readAuthStorages.flatMap((storage) => storage.list())));
@@ -161896,15 +162808,9 @@ function createReadOnlyAuthFileStorage(authPaths) {
161896
162808
  const reload = () => {
161897
162809
  const nextCredentials = {};
161898
162810
  for (const authPath of authPaths) {
161899
- if (!existsSync36(authPath)) {
161900
- continue;
161901
- }
161902
- try {
161903
- const parsed = JSON.parse(readFileSync15(authPath, "utf-8"));
161904
- for (const [provider, credential] of Object.entries(parsed)) {
161905
- nextCredentials[provider] ??= credential;
161906
- }
161907
- } catch {
162811
+ const parsed = readStoredCredentialsFromAuthFile(authPath);
162812
+ for (const [provider, credential] of Object.entries(parsed)) {
162813
+ nextCredentials[provider] = choosePreferredStoredCredential(nextCredentials[provider], credential) ?? credential;
161908
162814
  }
161909
162815
  }
161910
162816
  credentials = nextCredentials;
@@ -161925,6 +162831,7 @@ var OAUTH_TO_API_KEY_RECLASSIFICATIONS, BUILT_IN_API_KEY_PROVIDERS, CLI_PROVIDER
161925
162831
  var init_provider_auth = __esm({
161926
162832
  "src/commands/provider-auth.ts"() {
161927
162833
  "use strict";
162834
+ init_src();
161928
162835
  OAUTH_TO_API_KEY_RECLASSIFICATIONS = /* @__PURE__ */ new Set([
161929
162836
  "anthropic"
161930
162837
  ]);
@@ -161942,34 +162849,37 @@ var init_provider_auth = __esm({
161942
162849
  });
161943
162850
 
161944
162851
  // src/commands/auth-paths.ts
161945
- import { homedir as homedir9 } from "node:os";
162852
+ import { homedir as homedir10 } from "node:os";
161946
162853
  import { existsSync as existsSync37, readFileSync as readFileSync16 } from "node:fs";
161947
- import { join as join54 } from "node:path";
161948
- function getFusionAgentDir3(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
161949
- 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");
161950
162860
  }
161951
- function getLegacyAgentDir(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
161952
- return join54(home, ".pi", "agent");
162861
+ function getFusionAuthPath3(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
162862
+ return join55(getFusionAgentDir3(home), "auth.json");
161953
162863
  }
161954
- function getFusionAuthPath3(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
161955
- return join54(getFusionAgentDir3(home), "auth.json");
162864
+ function getCodexCliAuthPath2(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
162865
+ return join55(home, ".codex", "auth.json");
161956
162866
  }
161957
- function getLegacyAuthPaths2(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
162867
+ function getLegacyAuthPaths2(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
161958
162868
  return [
161959
- join54(home, ".pi", "agent", "auth.json"),
161960
- join54(home, ".pi", "auth.json")
162869
+ join55(home, ".pi", "agent", "auth.json"),
162870
+ join55(home, ".pi", "auth.json")
161961
162871
  ];
161962
162872
  }
161963
- function getFusionModelsPath2(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
161964
- return join54(getFusionAgentDir3(home), "models.json");
162873
+ function getFusionModelsPath2(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
162874
+ return join55(getFusionAgentDir3(home), "models.json");
161965
162875
  }
161966
- function getLegacyModelsPaths2(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
162876
+ function getLegacyModelsPaths2(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
161967
162877
  return [
161968
- join54(home, ".pi", "agent", "models.json"),
161969
- join54(home, ".pi", "models.json")
162878
+ join55(home, ".pi", "agent", "models.json"),
162879
+ join55(home, ".pi", "models.json")
161970
162880
  ];
161971
162881
  }
161972
- function getModelRegistryModelsPath2(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
162882
+ function getModelRegistryModelsPath2(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
161973
162883
  const fusionModelsPath = getFusionModelsPath2(home);
161974
162884
  if (existsSync37(fusionModelsPath)) {
161975
162885
  return fusionModelsPath;
@@ -161990,11 +162900,11 @@ function readJsonObject5(path5) {
161990
162900
  function hasPackageManagerSettings3(settings) {
161991
162901
  return Array.isArray(settings.packages) || Array.isArray(settings.npmCommand);
161992
162902
  }
161993
- function getPackageManagerAgentDir2(home = process.env.HOME || process.env.USERPROFILE || homedir9()) {
162903
+ function getPackageManagerAgentDir2(home = process.env.HOME || process.env.USERPROFILE || homedir10()) {
161994
162904
  const fusionAgentDir = getFusionAgentDir3(home);
161995
162905
  const legacyAgentDir = getLegacyAgentDir(home);
161996
- const fusionSettings = readJsonObject5(join54(fusionAgentDir, "settings.json"));
161997
- const legacySettings = readJsonObject5(join54(legacyAgentDir, "settings.json"));
162906
+ const fusionSettings = readJsonObject5(join55(fusionAgentDir, "settings.json"));
162907
+ const legacySettings = readJsonObject5(join55(legacyAgentDir, "settings.json"));
161998
162908
  if (hasPackageManagerSettings3(fusionSettings) || !existsSync37(legacyAgentDir)) {
161999
162909
  return fusionAgentDir;
162000
162910
  }
@@ -162170,7 +163080,7 @@ import {
162170
163080
  symlinkSync,
162171
163081
  unlinkSync
162172
163082
  } from "node:fs";
162173
- 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";
162174
163084
  import { fileURLToPath as fileURLToPath6 } from "node:url";
162175
163085
  function isPiClaudeCliConfigured(globalSettings) {
162176
163086
  if (!globalSettings || typeof globalSettings !== "object") {
@@ -162194,7 +163104,7 @@ function resolveFusionSkillSource() {
162194
163104
  return existsSync38(candidate) ? candidate : null;
162195
163105
  }
162196
163106
  function installFusionSkillIntoProject(projectPath, options = {}) {
162197
- const target = join55(projectPath, ".claude", "skills", FUSION_SKILL_NAME);
163107
+ const target = join56(projectPath, ".claude", "skills", FUSION_SKILL_NAME);
162198
163108
  if (options.enabled === false) {
162199
163109
  return { outcome: "skipped", target, reason: "pi-claude-cli not configured" };
162200
163110
  }
@@ -162219,7 +163129,7 @@ function installFusionSkillIntoProject(projectPath, options = {}) {
162219
163129
  unlinkSync(target);
162220
163130
  replaced = true;
162221
163131
  } else {
162222
- const skillMd = join55(target, "SKILL.md");
163132
+ const skillMd = join56(target, "SKILL.md");
162223
163133
  if (!existsSync38(skillMd)) {
162224
163134
  return {
162225
163135
  outcome: "failed",
@@ -162266,7 +163176,7 @@ function ensureFusionSkillForProjects(projects, options = { enabled: false }) {
162266
163176
  if (!options.enabled) {
162267
163177
  return projects.map((p) => ({
162268
163178
  outcome: "skipped",
162269
- target: join55(p.path, ".claude", "skills", FUSION_SKILL_NAME),
163179
+ target: join56(p.path, ".claude", "skills", FUSION_SKILL_NAME),
162270
163180
  reason: "pi-claude-cli not configured"
162271
163181
  }));
162272
163182
  }
@@ -162569,10 +163479,10 @@ var init_droid_cli_extension = __esm({
162569
163479
 
162570
163480
  // src/update-cache.ts
162571
163481
  import { readFileSync as readFileSync19 } from "node:fs";
162572
- import { join as join56 } from "node:path";
163482
+ import { join as join57 } from "node:path";
162573
163483
  function getCachedUpdateStatus(currentVersion) {
162574
163484
  try {
162575
- const cachePath = join56(resolveGlobalDir(), "update-check.json");
163485
+ const cachePath = join57(resolveGlobalDir(), "update-check.json");
162576
163486
  const raw = readFileSync19(cachePath, "utf-8");
162577
163487
  const parsed = JSON.parse(raw);
162578
163488
  if (parsed.updateAvailable === true && typeof parsed.latestVersion === "string" && parsed.latestVersion.length > 0 && typeof parsed.currentVersion === "string" && parsed.currentVersion.length > 0) {
@@ -162662,19 +163572,19 @@ var init_self_extension = __esm({
162662
163572
  // src/plugins/bundled-plugin-install.ts
162663
163573
  import { existsSync as existsSync42 } from "node:fs";
162664
163574
  import { readFile as readFile24 } from "node:fs/promises";
162665
- 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";
162666
163576
  import { fileURLToPath as fileURLToPath10 } from "node:url";
162667
163577
  function getCandidatePluginPaths() {
162668
163578
  const moduleDir = dirname27(fileURLToPath10(import.meta.url));
162669
163579
  const cliPackageRoot = resolve33(moduleDir, "..", "..");
162670
163580
  return [
162671
- join57(cliPackageRoot, "dist", "plugins", DEPENDENCY_GRAPH_PLUGIN_ID),
162672
- join57(cliPackageRoot, "plugins", DEPENDENCY_GRAPH_PLUGIN_ID),
162673
- 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)
162674
163584
  ];
162675
163585
  }
162676
163586
  async function loadManifest(pluginDir) {
162677
- const manifestPath = join57(pluginDir, "manifest.json");
163587
+ const manifestPath = join58(pluginDir, "manifest.json");
162678
163588
  const content = await readFile24(manifestPath, "utf-8");
162679
163589
  const manifest = JSON.parse(content);
162680
163590
  const validation = validatePluginManifest(manifest);
@@ -162685,7 +163595,7 @@ async function loadManifest(pluginDir) {
162685
163595
  }
162686
163596
  function resolveBundledDependencyGraphPath() {
162687
163597
  for (const path5 of getCandidatePluginPaths()) {
162688
- if (existsSync42(join57(path5, "manifest.json"))) {
163598
+ if (existsSync42(join58(path5, "manifest.json"))) {
162689
163599
  return path5;
162690
163600
  }
162691
163601
  }
@@ -167114,7 +168024,7 @@ __export(dashboard_exports, {
167114
168024
  promptForPort: () => promptForPort,
167115
168025
  runDashboard: () => runDashboard
167116
168026
  });
167117
- 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";
167118
168028
  import { execFile as execFileCb } from "node:child_process";
167119
168029
  import { promisify as promisify16 } from "node:util";
167120
168030
  import { stat as stat11, readdir as readdir11, readFile as fsReadFile3 } from "node:fs/promises";
@@ -167452,7 +168362,7 @@ async function buildFileListDirectory(projectPath, relativePath) {
167452
168362
  let size = 0;
167453
168363
  let modifiedAt = (/* @__PURE__ */ new Date(0)).toISOString();
167454
168364
  try {
167455
- const s = await stat11(join58(absDir, d.name));
168365
+ const s = await stat11(join59(absDir, d.name));
167456
168366
  size = d.isDirectory() ? 0 : s.size;
167457
168367
  modifiedAt = s.mtime.toISOString();
167458
168368
  } catch {
@@ -167864,8 +168774,11 @@ async function runDashboard(port, opts = {}) {
167864
168774
  rootDir: cwd
167865
168775
  });
167866
168776
  const authStorage = AuthStorage2.create(getFusionAuthPath3());
167867
- const legacyAuthStorage = createReadOnlyAuthFileStorage(getLegacyAuthPaths2());
167868
- const mergedAuthStorage = mergeAuthStorageReads(authStorage, [legacyAuthStorage]);
168777
+ const supplementalAuthStorage = createReadOnlyAuthFileStorage([
168778
+ ...getLegacyAuthPaths2(),
168779
+ getCodexCliAuthPath2()
168780
+ ]);
168781
+ const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
167869
168782
  const modelRegistry = ModelRegistry3.create(mergedAuthStorage, getModelRegistryModelsPath2());
167870
168783
  const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);
167871
168784
  let packageManager;
@@ -167927,7 +168840,7 @@ async function runDashboard(port, opts = {}) {
167927
168840
  ...droidCliPaths
167928
168841
  ],
167929
168842
  cwd,
167930
- join58(cwd, ".fusion", "disabled-auto-extension-discovery")
168843
+ join59(cwd, ".fusion", "disabled-auto-extension-discovery")
167931
168844
  );
167932
168845
  for (const { path: path5, error } of extensionsResult.errors) {
167933
168846
  logSink.log(`Failed to load ${path5}: ${error}`, "extensions");
@@ -169450,7 +170363,7 @@ var serve_exports = {};
169450
170363
  __export(serve_exports, {
169451
170364
  runServe: () => runServe
169452
170365
  });
169453
- import { dirname as dirname29, join as join59 } from "node:path";
170366
+ import { dirname as dirname29, join as join60 } from "node:path";
169454
170367
  import {
169455
170368
  AuthStorage as AuthStorage3,
169456
170369
  DefaultPackageManager as DefaultPackageManager3,
@@ -169703,8 +170616,11 @@ async function runServe(port, opts = {}) {
169703
170616
  const missionExecutionLoop = cwdEngine.getRuntime().getMissionExecutionLoop();
169704
170617
  const automationStore = cwdEngine.getAutomationStore();
169705
170618
  const authStorage = AuthStorage3.create(getFusionAuthPath3());
169706
- const legacyAuthStorage = createReadOnlyAuthFileStorage(getLegacyAuthPaths2());
169707
- const mergedAuthStorage = mergeAuthStorageReads(authStorage, [legacyAuthStorage]);
170619
+ const supplementalAuthStorage = createReadOnlyAuthFileStorage([
170620
+ ...getLegacyAuthPaths2(),
170621
+ getCodexCliAuthPath2()
170622
+ ]);
170623
+ const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
169708
170624
  const modelRegistry = ModelRegistry4.create(mergedAuthStorage, getModelRegistryModelsPath2());
169709
170625
  const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);
169710
170626
  let packageManager;
@@ -169766,7 +170682,7 @@ async function runServe(port, opts = {}) {
169766
170682
  ...droidCliPaths
169767
170683
  ],
169768
170684
  cwd,
169769
- join59(cwd, ".fusion", "disabled-auto-extension-discovery")
170685
+ join60(cwd, ".fusion", "disabled-auto-extension-discovery")
169770
170686
  );
169771
170687
  for (const { path: path5, error } of extensionsResult.errors) {
169772
170688
  console.log(`[extensions] Failed to load ${path5}: ${error}`);
@@ -170121,7 +171037,7 @@ var daemon_exports = {};
170121
171037
  __export(daemon_exports, {
170122
171038
  runDaemon: () => runDaemon
170123
171039
  });
170124
- import { join as join60 } from "node:path";
171040
+ import { join as join61 } from "node:path";
170125
171041
  import {
170126
171042
  AuthStorage as AuthStorage4,
170127
171043
  DefaultPackageManager as DefaultPackageManager4,
@@ -170367,8 +171283,11 @@ async function runDaemon(opts = {}) {
170367
171283
  const missionExecutionLoop = cwdEngine.getRuntime().getMissionExecutionLoop();
170368
171284
  const automationStore = cwdEngine.getAutomationStore();
170369
171285
  const authStorage = AuthStorage4.create(getFusionAuthPath3());
170370
- const legacyAuthStorage = createReadOnlyAuthFileStorage(getLegacyAuthPaths2());
170371
- const mergedAuthStorage = mergeAuthStorageReads(authStorage, [legacyAuthStorage]);
171286
+ const supplementalAuthStorage = createReadOnlyAuthFileStorage([
171287
+ ...getLegacyAuthPaths2(),
171288
+ getCodexCliAuthPath2()
171289
+ ]);
171290
+ const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
170372
171291
  const modelRegistry = ModelRegistry5.create(mergedAuthStorage, getModelRegistryModelsPath2());
170373
171292
  const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);
170374
171293
  let packageManager;
@@ -170428,7 +171347,7 @@ async function runDaemon(opts = {}) {
170428
171347
  const extensionsResult = await discoverAndLoadExtensions4(
170429
171348
  [...reconciledExtensionPaths, ...droidCliPaths],
170430
171349
  cwd,
170431
- join60(cwd, ".fusion", "disabled-auto-extension-discovery")
171350
+ join61(cwd, ".fusion", "disabled-auto-extension-discovery")
170432
171351
  );
170433
171352
  for (const { path: path5, error } of extensionsResult.errors) {
170434
171353
  console.log(`[extensions] Failed to load ${path5}: ${error}`);
@@ -170649,7 +171568,7 @@ __export(desktop_exports, {
170649
171568
  });
170650
171569
  import { spawn as spawn18 } from "node:child_process";
170651
171570
  import { once as once2 } from "node:events";
170652
- import { join as join61 } from "node:path";
171571
+ import { join as join62 } from "node:path";
170653
171572
  import { createRequire as createRequire6 } from "node:module";
170654
171573
  function runCommand(command, args, cwd) {
170655
171574
  return new Promise((resolve44, reject2) => {
@@ -170728,7 +171647,7 @@ async function runDesktop(options = {}) {
170728
171647
  }
170729
171648
  const runtime = await startDashboardRuntime(rootDir, Boolean(options.paused));
170730
171649
  const electronBinary = resolveElectronBinary();
170731
- const desktopEntry = join61(rootDir, "packages", "desktop", "dist", "main.js");
171650
+ const desktopEntry = join62(rootDir, "packages", "desktop", "dist", "main.js");
170732
171651
  const electronArgs = ["--enable-source-maps", desktopEntry, ...options.dev ? ["--dev"] : []];
170733
171652
  const electronEnv = {
170734
171653
  ...process.env,
@@ -170814,7 +171733,7 @@ __export(task_exports, {
170814
171733
  });
170815
171734
  import { createInterface as createInterface3 } from "node:readline/promises";
170816
171735
  import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync43, readFileSync as readFileSync21 } from "node:fs";
170817
- import { basename as basename17, join as join62 } from "node:path";
171736
+ import { basename as basename17, join as join63 } from "node:path";
170818
171737
  function getGitHubIssueUrl(sourceMetadata) {
170819
171738
  if (!sourceMetadata || typeof sourceMetadata !== "object") return void 0;
170820
171739
  const issueUrl = sourceMetadata.issueUrl;
@@ -171127,7 +172046,7 @@ async function runTaskLogs(id, options = {}, projectName) {
171127
172046
  printEntries(filteredEntries);
171128
172047
  if (options.follow) {
171129
172048
  const projectPath = projectContext?.projectPath ?? process.cwd();
171130
- const logPath = join62(projectPath, ".fusion", "tasks", id, "agent.log");
172049
+ const logPath = join63(projectPath, ".fusion", "tasks", id, "agent.log");
171131
172050
  if (!existsSync43(logPath)) {
171132
172051
  console.log(`
171133
172052
  Waiting for log file to be created...`);
@@ -172405,7 +173324,7 @@ __export(settings_export_exports, {
172405
173324
  runSettingsExport: () => runSettingsExport
172406
173325
  });
172407
173326
  import { writeFile as writeFile18 } from "node:fs/promises";
172408
- import { resolve as resolve34, join as join63 } from "node:path";
173327
+ import { resolve as resolve34, join as join64 } from "node:path";
172409
173328
  async function runSettingsExport(options = {}) {
172410
173329
  const scope = options.scope ?? "both";
172411
173330
  const project = options.projectName ? await resolveProject(options.projectName) : void 0;
@@ -172419,7 +173338,7 @@ async function runSettingsExport(options = {}) {
172419
173338
  targetPath = resolve34(outputPath);
172420
173339
  } else {
172421
173340
  const filename = generateExportFilename();
172422
- targetPath = join63(process.cwd(), filename);
173341
+ targetPath = join64(process.cwd(), filename);
172423
173342
  }
172424
173343
  const jsonContent = JSON.stringify(exportData, null, 2);
172425
173344
  await writeFile18(targetPath, jsonContent);
@@ -173993,14 +174912,14 @@ var init_project = __esm({
173993
174912
 
173994
174913
  // src/commands/skill-installation.ts
173995
174914
  import { cpSync as cpSync2, existsSync as existsSync47, mkdirSync as mkdirSync8 } from "node:fs";
173996
- import { homedir as homedir10 } from "node:os";
173997
- 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";
173998
174917
  import { fileURLToPath as fileURLToPath11 } from "node:url";
173999
- function getSupportedSkillInstallTargets(homeDir = process.env.HOME || process.env.USERPROFILE || homedir10()) {
174918
+ function getSupportedSkillInstallTargets(homeDir = process.env.HOME || process.env.USERPROFILE || homedir11()) {
174000
174919
  return [
174001
- { client: "claude", targetDir: join64(homeDir, ".claude", "skills", FUSION_SKILL_NAME2) },
174002
- { client: "codex", targetDir: join64(homeDir, ".codex", "skills", FUSION_SKILL_NAME2) },
174003
- { 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) }
174004
174923
  ];
174005
174924
  }
174006
174925
  function resolveBundledFusionSkillSource() {
@@ -174064,13 +174983,13 @@ __export(init_exports, {
174064
174983
  runInit: () => runInit
174065
174984
  });
174066
174985
  import { existsSync as existsSync48, mkdirSync as mkdirSync9, writeFileSync as writeFileSync3, readFileSync as readFileSync22 } from "node:fs";
174067
- 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";
174068
174987
  import { exec as exec13 } from "node:child_process";
174069
174988
  import { promisify as promisify18 } from "node:util";
174070
174989
  async function runInit(options = {}) {
174071
174990
  const cwd = options.path ? resolve39(options.path) : process.cwd();
174072
- const fusionDir = join65(cwd, ".fusion");
174073
- const dbPath = join65(fusionDir, "fusion.db");
174991
+ const fusionDir = join66(cwd, ".fusion");
174992
+ const dbPath = join66(fusionDir, "fusion.db");
174074
174993
  const hasDbPath = existsSync48(dbPath);
174075
174994
  const hasValidDb = hasDbPath && isValidSqliteDatabaseFile(dbPath);
174076
174995
  if (existsSync48(fusionDir) && hasDbPath && hasValidDb) {
@@ -174166,7 +175085,7 @@ async function runInit(options = {}) {
174166
175085
  }
174167
175086
  }
174168
175087
  async function detectProjectName(dir2) {
174169
- if (!existsSync48(join65(dir2, ".git"))) {
175088
+ if (!existsSync48(join66(dir2, ".git"))) {
174170
175089
  return basename20(dir2) || "my-project";
174171
175090
  }
174172
175091
  try {
@@ -174186,7 +175105,7 @@ async function detectProjectName(dir2) {
174186
175105
  return basename20(dir2) || "my-project";
174187
175106
  }
174188
175107
  async function addLocalStorageToGitignore(cwd) {
174189
- const gitignorePath = join65(cwd, ".gitignore");
175108
+ const gitignorePath = join66(cwd, ".gitignore");
174190
175109
  let content = "";
174191
175110
  if (existsSync48(gitignorePath)) {
174192
175111
  try {
@@ -174229,7 +175148,7 @@ async function initializeGitRepo(cwd) {
174229
175148
  }
174230
175149
  await ensureGitConfig(cwd, "user.name", "Fusion");
174231
175150
  await ensureGitConfig(cwd, "user.email", "noreply@runfusion.ai");
174232
- const gitkeepPath = join65(cwd, ".gitkeep");
175151
+ const gitkeepPath = join66(cwd, ".gitkeep");
174233
175152
  if (!existsSync48(gitkeepPath)) {
174234
175153
  writeFileSync3(gitkeepPath, "\n");
174235
175154
  }
@@ -174896,7 +175815,7 @@ __export(plugin_exports, {
174896
175815
  runPluginUninstall: () => runPluginUninstall
174897
175816
  });
174898
175817
  import { existsSync as existsSync50 } from "node:fs";
174899
- import { join as join66 } from "node:path";
175818
+ import { join as join67 } from "node:path";
174900
175819
  import { readFile as readFile25 } from "node:fs/promises";
174901
175820
  import * as readline from "node:readline";
174902
175821
  async function getProjectPath6(projectName) {
@@ -174934,7 +175853,7 @@ async function createPluginLoader(pluginStore, projectName) {
174934
175853
  return { store: pluginStore, loader };
174935
175854
  }
174936
175855
  async function loadManifestFromPath(pluginPath) {
174937
- const manifestPath = join66(pluginPath, "manifest.json");
175856
+ const manifestPath = join67(pluginPath, "manifest.json");
174938
175857
  if (!existsSync50(manifestPath)) {
174939
175858
  throw new Error(`Plugin manifest not found at: ${manifestPath}`);
174940
175859
  }
@@ -175126,7 +176045,7 @@ __export(plugin_scaffold_exports, {
175126
176045
  runPluginCreate: () => runPluginCreate
175127
176046
  });
175128
176047
  import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync5, existsSync as existsSync51 } from "node:fs";
175129
- import { join as join67 } from "node:path";
176048
+ import { join as join68 } from "node:path";
175130
176049
  function toTitleCase(str) {
175131
176050
  return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
175132
176051
  }
@@ -175259,7 +176178,7 @@ async function runPluginCreate(name, options) {
175259
176178
  process.exit(1);
175260
176179
  }
175261
176180
  const targetDir = options?.output ?? name;
175262
- const targetPath = join67(process.cwd(), targetDir);
176181
+ const targetPath = join68(process.cwd(), targetDir);
175263
176182
  if (existsSync51(targetPath)) {
175264
176183
  console.error(`Error: Directory '${targetDir}' already exists.`);
175265
176184
  console.error("Please choose a different name or remove the existing directory.");
@@ -175267,16 +176186,16 @@ async function runPluginCreate(name, options) {
175267
176186
  }
175268
176187
  try {
175269
176188
  mkdirSync11(targetPath, { recursive: true });
175270
- mkdirSync11(join67(targetPath, "src", "__tests__"), { recursive: true });
175271
- writeFileSync5(join67(targetPath, "package.json"), generatePackageJson(name));
175272
- writeFileSync5(join67(targetPath, "tsconfig.json"), generateTsconfig());
175273
- writeFileSync5(join67(targetPath, "vitest.config.ts"), generateVitestConfig());
175274
- 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));
175275
176194
  writeFileSync5(
175276
- join67(targetPath, "src", "__tests__", "index.test.ts"),
176195
+ join68(targetPath, "src", "__tests__", "index.test.ts"),
175277
176196
  generateTestTs(name)
175278
176197
  );
175279
- writeFileSync5(join67(targetPath, "README.md"), generateReadme(name));
176198
+ writeFileSync5(join68(targetPath, "README.md"), generateReadme(name));
175280
176199
  } catch (err) {
175281
176200
  console.error(
175282
176201
  `Error creating plugin files: ${err instanceof Error ? err.message : String(err)}`
@@ -175428,7 +176347,7 @@ __export(research_exports, {
175428
176347
  runResearchShow: () => runResearchShow
175429
176348
  });
175430
176349
  import { writeFile as writeFile19 } from "node:fs/promises";
175431
- import { join as join68, resolve as resolve42 } from "node:path";
176350
+ import { join as join69, resolve as resolve42 } from "node:path";
175432
176351
  async function getStore3(projectName) {
175433
176352
  const project = projectName ? await resolveProject(projectName) : void 0;
175434
176353
  const store = new TaskStore(project?.projectPath ?? process.cwd());
@@ -175584,7 +176503,7 @@ async function runResearchExport(options) {
175584
176503
  }
175585
176504
  const content = format === "json" ? JSON.stringify(run, null, 2) : renderMarkdown(run);
175586
176505
  const ext = format === "json" ? "json" : "md";
175587
- 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}`);
175588
176507
  await writeFile19(outputPath, content, "utf8");
175589
176508
  store.getResearchStore().createExport(run.id, format, content);
175590
176509
  if (options.json) {
@@ -175649,7 +176568,7 @@ __export(native_patch_exports, {
175649
176568
  isTerminalAvailable: () => isTerminalAvailable,
175650
176569
  setupNativeResolution: () => setupNativeResolution
175651
176570
  });
175652
- 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";
175653
176572
  import { existsSync as existsSync52, copyFileSync, mkdirSync as mkdirSync12, symlinkSync as symlinkSync2, rmSync as rmSync5, lstatSync as lstatSync3, readlinkSync as readlinkSync2 } from "node:fs";
175654
176573
  import { tmpdir as tmpdir5 } from "node:os";
175655
176574
  function findStagedNativeDir2() {
@@ -175657,13 +176576,13 @@ function findStagedNativeDir2() {
175657
176576
  const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : "unknown";
175658
176577
  const prebuildName = `${platform4}-${arch}`;
175659
176578
  const execDir = dirname32(process.execPath);
175660
- const nextToBinary = join69(execDir, "runtime", prebuildName);
175661
- if (existsSync52(join69(nextToBinary, "pty.node"))) {
176579
+ const nextToBinary = join70(execDir, "runtime", prebuildName);
176580
+ if (existsSync52(join70(nextToBinary, "pty.node"))) {
175662
176581
  return nextToBinary;
175663
176582
  }
175664
176583
  if (process.env.FUSION_RUNTIME_DIR) {
175665
- const envPath = join69(process.env.FUSION_RUNTIME_DIR, prebuildName);
175666
- if (existsSync52(join69(envPath, "pty.node"))) {
176584
+ const envPath = join70(process.env.FUSION_RUNTIME_DIR, prebuildName);
176585
+ if (existsSync52(join70(envPath, "pty.node"))) {
175667
176586
  return envPath;
175668
176587
  }
175669
176588
  }
@@ -175696,17 +176615,17 @@ function setupNativeResolution() {
175696
176615
  process.env.NODE_PTY_SPAWN_HELPER_DIR = nativeDir;
175697
176616
  }
175698
176617
  process.env.FUSION_NATIVE_ASSETS_PATH = nativeDir;
175699
- const tmpRoot = join69(tmpdir5(), `fn-bunfs-${process.pid}`);
175700
- const fnDir = join69(tmpRoot, "fn");
175701
- const prebuildsDir = join69(fnDir, "prebuilds");
175702
- 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));
175703
176622
  try {
175704
176623
  cleanupStaleBunfsLinks();
175705
176624
  mkdirSync12(platformDir, { recursive: true });
175706
- const ptyNodeDest = join69(platformDir, "pty.node");
175707
- copyFileSync(join69(nativeDir, "pty.node"), ptyNodeDest);
175708
- if (existsSync52(join69(nativeDir, "spawn-helper"))) {
175709
- 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"));
175710
176629
  }
175711
176630
  process.env.FUSION_FAKE_BUNFS_ROOT = tmpRoot;
175712
176631
  if (process.platform !== "win32") {
@@ -175783,7 +176702,7 @@ var init_native_patch = __esm({
175783
176702
  // src/bin.ts
175784
176703
  import { existsSync as existsSync53, mkdtempSync as mkdtempSync2, readFileSync as readFileSync24, symlinkSync as symlinkSync3, writeFileSync as writeFileSync6 } from "node:fs";
175785
176704
  import { createRequire as createRequire7 } from "node:module";
175786
- 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";
175787
176706
  import { tmpdir as tmpdir6 } from "node:os";
175788
176707
  import { performance as performance3 } from "node:perf_hooks";
175789
176708
  import { fileURLToPath as fileURLToPath12 } from "node:url";
@@ -175792,7 +176711,7 @@ function configurePiPackage() {
175792
176711
  if (process.env.PI_PACKAGE_DIR) {
175793
176712
  return;
175794
176713
  }
175795
- const tmp = mkdtempSync2(join70(tmpdir6(), "fn-pkg-"));
176714
+ const tmp = mkdtempSync2(join71(tmpdir6(), "fn-pkg-"));
175796
176715
  let packageJson = {
175797
176716
  name: "pi",
175798
176717
  version: "0.1.0",
@@ -175804,9 +176723,9 @@ function configurePiPackage() {
175804
176723
  const piPackageDir = dirname33(piPackagePath);
175805
176724
  packageJson = JSON.parse(readFileSync24(piPackagePath, "utf-8"));
175806
176725
  for (const entry of ["dist", "docs", "examples", "README.md", "CHANGELOG.md"]) {
175807
- const source = join70(piPackageDir, entry);
176726
+ const source = join71(piPackageDir, entry);
175808
176727
  if (existsSync53(source)) {
175809
- symlinkSync3(source, join70(tmp, entry));
176728
+ symlinkSync3(source, join71(tmp, entry));
175810
176729
  }
175811
176730
  }
175812
176731
  } catch {
@@ -175815,7 +176734,7 @@ function configurePiPackage() {
175815
176734
  ...packageJson.piConfig ?? {},
175816
176735
  configDir: ".fusion"
175817
176736
  };
175818
- writeFileSync6(join70(tmp, "package.json"), JSON.stringify(packageJson, null, 2) + "\n");
176737
+ writeFileSync6(join71(tmp, "package.json"), JSON.stringify(packageJson, null, 2) + "\n");
175819
176738
  process.env.PI_PACKAGE_DIR = tmp;
175820
176739
  }
175821
176740
  configurePiPackage();
@@ -175843,8 +176762,8 @@ function loadEnvFile(path5) {
175843
176762
  }
175844
176763
  function loadLocalEnv() {
175845
176764
  const cwd = process.cwd();
175846
- loadEnvFile(join70(cwd, ".env"));
175847
- loadEnvFile(join70(cwd, ".env.local"));
176765
+ loadEnvFile(join71(cwd, ".env"));
176766
+ loadEnvFile(join71(cwd, ".env.local"));
175848
176767
  }
175849
176768
  loadLocalEnv();
175850
176769
  async function loadCommandHandlers() {