@runfusion/fusion 0.17.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/bin.js +1729 -789
  2. package/dist/client/assets/{AgentDetailView-DGqT1oDt.js → AgentDetailView-17J-F0Rl.js} +3 -3
  3. package/dist/client/assets/{AgentsView-BmemrfrO.js → AgentsView-sbBkb7Wd.js} +45 -45
  4. package/dist/client/assets/ChatView-BR5cvK_B.js +1 -0
  5. package/dist/client/assets/{DevServerView-C3Q0XqDA.js → DevServerView-GFFVXHVP.js} +1 -1
  6. package/dist/client/assets/{DirectoryPicker-BZWVA9ND.js → DirectoryPicker-WPDSBdT6.js} +1 -1
  7. package/dist/client/assets/{DocumentsView-DO48ivSq.js → DocumentsView-BHpDsIIt.js} +1 -1
  8. package/dist/client/assets/{InsightsView-CAngTfMf.js → InsightsView-Bxu0TJkt.js} +1 -1
  9. package/dist/client/assets/{MemoryView-B3rNcAOW.js → MemoryView-CmnzZorw.js} +2 -2
  10. package/dist/client/assets/{NodesView-BnV1LWa8.js → NodesView-CO9_4hCr.js} +4 -4
  11. package/dist/client/assets/{PiExtensionsManager-C3_Lw4sa.js → PiExtensionsManager-4e3MlD62.js} +3 -3
  12. package/dist/client/assets/{PluginManager-Vv3nzrJ1.js → PluginManager-DGN2rvOY.js} +1 -1
  13. package/dist/client/assets/ResearchView-Dsa6Gykl.js +1 -0
  14. package/dist/client/assets/{RoadmapsView-BiIpE-b8.js → RoadmapsView-jHTOK0RQ.js} +2 -2
  15. package/dist/client/assets/{SettingsModal-CK4w8Ztb.js → SettingsModal-4Z8ZJMzD.js} +1 -1
  16. package/dist/client/assets/SettingsModal-D0kuJpBA.js +31 -0
  17. package/dist/client/assets/{SetupWizardModal-Dw6N4UvY.js → SetupWizardModal-Bhumd4Rf.js} +1 -1
  18. package/dist/client/assets/{SkillsView-C1196wgA.js → SkillsView-MHweJTz4.js} +1 -1
  19. package/dist/client/assets/{folder-open-WVtgE4k3.js → folder-open-BNQW9dE9.js} +1 -1
  20. package/dist/client/assets/{index-BIJgrHEn.css → index-DEVBHvyW.css} +1 -1
  21. package/dist/client/assets/index-k_85J1DS.js +682 -0
  22. package/dist/client/assets/{star-MSImEC8V.js → star-7L86NZrT.js} +1 -1
  23. package/dist/client/assets/{upload-Dmvy3xXd.js → upload-DsAS6tno.js} +1 -1
  24. package/dist/client/assets/{users-CncYvHNf.js → users-D3u6f2Rz.js} +1 -1
  25. package/dist/client/index.html +2 -2
  26. package/dist/client/version.json +1 -1
  27. package/dist/extension.js +1239 -527
  28. package/dist/pi-claude-cli/package.json +1 -1
  29. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  30. package/package.json +1 -1
  31. package/dist/client/assets/ChatView-CZQUBFlV.js +0 -1
  32. package/dist/client/assets/ResearchView-Dfdsuc21.js +0 -1
  33. package/dist/client/assets/SettingsModal-BN00HYJ2.js +0 -31
  34. package/dist/client/assets/index-Bv0TGzDH.js +0 -682
package/dist/extension.js CHANGED
@@ -1389,7 +1389,14 @@ Note: Refs (@e1, @e2) are invalidated after page navigation. Re-snapshot after c
1389
1389
  toolMode: "readonly",
1390
1390
  prompt: `You are a UX design reviewer. Verify frontend changes maintain visual polish and consistency with existing UI patterns and design tokens.
1391
1391
 
1392
- Design System Review:
1392
+ FAST-BAIL RULE (check this FIRST):
1393
+ - The task harness gives you a "Diff Scope" listing the files this task actually changed.
1394
+ - If that list contains NO frontend/UI files (no .tsx/.jsx/.ts/.js component files, no .css/.scss/.sass/.styl, no .html/.vue/.svelte/.astro, no design-token/theme files), respond IMMEDIATELY with a single short line such as "No UI changes in scope \u2014 approved." and STOP.
1395
+ - Do NOT explore the worktree looking for related-looking UI code to critique. If this task didn't change a UI file, your review is a no-op by definition.
1396
+
1397
+ Otherwise, restrict your review to the UI files actually present in the diff scope.
1398
+
1399
+ Design System Review (only for UI files in the diff scope):
1393
1400
  1. **Visual Hierarchy** \u2014 Check that the changes maintain consistent heading levels, content flow, and information architecture
1394
1401
  2. **Spacing and Typography** \u2014 Verify consistent spacing (margins, padding, gaps) and typography scale usage
1395
1402
  3. **Color and Token Consistency** \u2014 Check that CSS custom properties and design tokens are used correctly; no hardcoded color values that bypass the design system
@@ -1397,15 +1404,16 @@ Design System Review:
1397
1404
  5. **Responsive Behavior** \u2014 Check that layouts adapt properly across viewport sizes and maintain usability on mobile
1398
1405
  6. **Fit with Design Language** \u2014 Verify the visual style matches existing patterns (border radius, shadows, transitions, icon style, etc.)
1399
1406
 
1400
- Files to Review:
1407
+ Files to Review (only those that appear in the Diff Scope):
1401
1408
  - Modified UI components (React, Vue, Angular, HTML)
1402
1409
  - CSS/SCSS/styled-component files
1403
1410
  - Design token or theme configuration files
1404
1411
 
1405
1412
  Output Requirements:
1406
- - If design is consistent and polished: call task_done() with success status
1407
- - If issues found: describe each finding with specific file paths and suggested corrections via task_log()
1408
- - Prioritize issues by impact: layout breaks > visual inconsistency > style preferences`
1413
+ - If design is consistent and polished (or there are no UI files in scope): respond with a brief approval line and stop.
1414
+ - If issues found: start your response with "REQUEST REVISION" and describe each finding with specific file paths and suggested corrections.
1415
+ - Prioritize issues by impact: layout breaks > visual inconsistency > style preferences.
1416
+ - Do NOT spend time on stylistic nits when no real issues exist.`
1409
1417
  }
1410
1418
  ];
1411
1419
  DOCUMENT_KEY_RE = /^[a-zA-Z0-9_-]{1,64}$/;
@@ -29610,8 +29618,8 @@ var require_CronFileParser = __commonJS({
29610
29618
  * @throws If file cannot be read
29611
29619
  */
29612
29620
  static parseFileSync(filePath) {
29613
- const { readFileSync: readFileSync11 } = __require("fs");
29614
- const data = readFileSync11(filePath, "utf8");
29621
+ const { readFileSync: readFileSync12 } = __require("fs");
29622
+ const data = readFileSync12(filePath, "utf8");
29615
29623
  return _CronFileParser.#parseContent(data);
29616
29624
  }
29617
29625
  /**
@@ -52657,6 +52665,193 @@ var init_chat_store = __esm({
52657
52665
  }
52658
52666
  });
52659
52667
 
52668
+ // ../core/src/oauth-credential-interop.ts
52669
+ import { existsSync as existsSync19, readFileSync as readFileSync6 } from "node:fs";
52670
+ import { homedir as homedir4 } from "node:os";
52671
+ import { join as join22 } from "node:path";
52672
+ function getHomeDir4() {
52673
+ return process.env.HOME || process.env.USERPROFILE || homedir4();
52674
+ }
52675
+ function getCodexCliAuthPath(home = getHomeDir4()) {
52676
+ return join22(home, ".codex", "auth.json");
52677
+ }
52678
+ function parseJwtPayload(token) {
52679
+ try {
52680
+ const [, payload = ""] = token.split(".", 3);
52681
+ if (!payload) {
52682
+ return null;
52683
+ }
52684
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
52685
+ } catch {
52686
+ return null;
52687
+ }
52688
+ }
52689
+ function getJwtExpiryMs(token) {
52690
+ if (!token) {
52691
+ return void 0;
52692
+ }
52693
+ const payload = parseJwtPayload(token);
52694
+ const exp = payload?.exp;
52695
+ if (typeof exp !== "number" || !Number.isFinite(exp)) {
52696
+ return void 0;
52697
+ }
52698
+ return exp * 1e3;
52699
+ }
52700
+ function getCodexAccountId(accessToken, fallbackAccountId) {
52701
+ const payload = parseJwtPayload(accessToken);
52702
+ const authClaim = payload?.[OPENAI_AUTH_CLAIM];
52703
+ const claimAccountId = authClaim && typeof authClaim === "object" ? authClaim.chatgpt_account_id : void 0;
52704
+ if (typeof claimAccountId === "string" && claimAccountId.trim().length > 0) {
52705
+ return claimAccountId;
52706
+ }
52707
+ if (typeof fallbackAccountId === "string" && fallbackAccountId.trim().length > 0) {
52708
+ return fallbackAccountId;
52709
+ }
52710
+ return void 0;
52711
+ }
52712
+ function getLastRefreshFallbackExpiryMs(lastRefresh) {
52713
+ if (typeof lastRefresh !== "string" || lastRefresh.trim().length === 0) {
52714
+ return void 0;
52715
+ }
52716
+ const parsed = Date.parse(lastRefresh);
52717
+ if (!Number.isFinite(parsed)) {
52718
+ return void 0;
52719
+ }
52720
+ return parsed + CODEX_REFRESH_FALLBACK_WINDOW_MS;
52721
+ }
52722
+ function isStoredAuthCredential(value) {
52723
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
52724
+ return false;
52725
+ }
52726
+ const record = value;
52727
+ return record.type === "oauth" || record.type === "api_key";
52728
+ }
52729
+ function isValidOauthCredential(credential) {
52730
+ return credential?.type === "oauth" && typeof credential.access === "string" && credential.access.length > 0 && typeof credential.refresh === "string" && credential.refresh.length > 0 && typeof credential.expires === "number" && Number.isFinite(credential.expires) && Date.now() < credential.expires;
52731
+ }
52732
+ function isRefreshableOauthCredential(credential) {
52733
+ return credential?.type === "oauth" && typeof credential.refresh === "string" && credential.refresh.length > 0 && typeof credential.expires === "number" && Number.isFinite(credential.expires);
52734
+ }
52735
+ function compareStoredCredentials(left, right) {
52736
+ if (!left && !right) {
52737
+ return 0;
52738
+ }
52739
+ if (left && !right) {
52740
+ return 1;
52741
+ }
52742
+ if (!left && right) {
52743
+ return -1;
52744
+ }
52745
+ if (left?.type === "api_key" && right?.type !== "api_key") {
52746
+ return 1;
52747
+ }
52748
+ if (right?.type === "api_key" && left?.type !== "api_key") {
52749
+ return -1;
52750
+ }
52751
+ if (left?.type === "oauth" && right?.type === "oauth") {
52752
+ const leftValid = isValidOauthCredential(left);
52753
+ const rightValid = isValidOauthCredential(right);
52754
+ if (leftValid !== rightValid) {
52755
+ return leftValid ? 1 : -1;
52756
+ }
52757
+ const leftRefreshable = isRefreshableOauthCredential(left);
52758
+ const rightRefreshable = isRefreshableOauthCredential(right);
52759
+ if (leftRefreshable !== rightRefreshable) {
52760
+ return leftRefreshable ? 1 : -1;
52761
+ }
52762
+ const leftExpiry = typeof left.expires === "number" && Number.isFinite(left.expires) ? left.expires : -Infinity;
52763
+ const rightExpiry = typeof right.expires === "number" && Number.isFinite(right.expires) ? right.expires : -Infinity;
52764
+ if (leftExpiry !== rightExpiry) {
52765
+ return leftExpiry > rightExpiry ? 1 : -1;
52766
+ }
52767
+ const leftAccessLength = typeof left.access === "string" ? left.access.length : 0;
52768
+ const rightAccessLength = typeof right.access === "string" ? right.access.length : 0;
52769
+ if (leftAccessLength !== rightAccessLength) {
52770
+ return leftAccessLength > rightAccessLength ? 1 : -1;
52771
+ }
52772
+ }
52773
+ return 0;
52774
+ }
52775
+ function choosePreferredStoredCredential(...credentials) {
52776
+ let best;
52777
+ for (const credential of credentials) {
52778
+ if (compareStoredCredentials(credential, best) > 0) {
52779
+ best = credential;
52780
+ }
52781
+ }
52782
+ return best;
52783
+ }
52784
+ function shouldHydrateStoredCredential(current, candidate) {
52785
+ if (!candidate || candidate.type !== "oauth") {
52786
+ return false;
52787
+ }
52788
+ if (current?.type === "api_key") {
52789
+ return false;
52790
+ }
52791
+ return compareStoredCredentials(candidate, current) > 0;
52792
+ }
52793
+ function extractCodexCliStoredCredential(raw) {
52794
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
52795
+ return void 0;
52796
+ }
52797
+ const record = raw;
52798
+ const tokens = record.tokens;
52799
+ if (!tokens || typeof tokens !== "object" || Array.isArray(tokens)) {
52800
+ return void 0;
52801
+ }
52802
+ const tokenRecord = tokens;
52803
+ const access7 = typeof tokenRecord.access_token === "string" ? tokenRecord.access_token : void 0;
52804
+ const refresh = typeof tokenRecord.refresh_token === "string" ? tokenRecord.refresh_token : void 0;
52805
+ if (!access7 || !refresh) {
52806
+ return void 0;
52807
+ }
52808
+ const expires = getJwtExpiryMs(access7) ?? getJwtExpiryMs(typeof tokenRecord.id_token === "string" ? tokenRecord.id_token : void 0) ?? getLastRefreshFallbackExpiryMs(record.last_refresh);
52809
+ if (typeof expires !== "number" || !Number.isFinite(expires)) {
52810
+ return void 0;
52811
+ }
52812
+ const accountId = getCodexAccountId(access7, tokenRecord.account_id);
52813
+ return {
52814
+ type: "oauth",
52815
+ access: access7,
52816
+ refresh,
52817
+ expires,
52818
+ ...accountId ? { accountId } : {}
52819
+ };
52820
+ }
52821
+ function readStoredCredentialsFromAuthFile(authPath) {
52822
+ if (!existsSync19(authPath)) {
52823
+ return {};
52824
+ }
52825
+ try {
52826
+ const parsed = JSON.parse(readFileSync6(authPath, "utf-8"));
52827
+ const codexCliCredential = extractCodexCliStoredCredential(parsed);
52828
+ if (codexCliCredential) {
52829
+ return { "openai-codex": codexCliCredential };
52830
+ }
52831
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
52832
+ return {};
52833
+ }
52834
+ const credentials = {};
52835
+ for (const [providerId, value] of Object.entries(parsed)) {
52836
+ if (!isStoredAuthCredential(value)) {
52837
+ continue;
52838
+ }
52839
+ credentials[providerId] = value;
52840
+ }
52841
+ return credentials;
52842
+ } catch {
52843
+ return {};
52844
+ }
52845
+ }
52846
+ var OPENAI_AUTH_CLAIM, CODEX_REFRESH_FALLBACK_WINDOW_MS;
52847
+ var init_oauth_credential_interop = __esm({
52848
+ "../core/src/oauth-credential-interop.ts"() {
52849
+ "use strict";
52850
+ OPENAI_AUTH_CLAIM = "https://api.openai.com/auth";
52851
+ CODEX_REFRESH_FALLBACK_WINDOW_MS = 55 * 60 * 1e3;
52852
+ }
52853
+ });
52854
+
52660
52855
  // ../core/src/index.ts
52661
52856
  var src_exports = {};
52662
52857
  __export(src_exports, {
@@ -52813,6 +53008,7 @@ __export(src_exports, {
52813
53008
  buildTriageMemoryInstructions: () => buildTriageMemoryInstructions,
52814
53009
  canTransition: () => canTransition,
52815
53010
  checkRateLimit: () => checkRateLimit,
53011
+ choosePreferredStoredCredential: () => choosePreferredStoredCredential,
52816
53012
  classifyInsightRunError: () => classifyInsightRunError,
52817
53013
  clearOverrides: () => clearOverrides,
52818
53014
  collectSystemMetrics: () => collectSystemMetrics,
@@ -52843,6 +53039,7 @@ __export(src_exports, {
52843
53039
  executeInsightRunLifecycle: () => executeInsightRunLifecycle,
52844
53040
  exportAgentsToDirectory: () => exportAgentsToDirectory,
52845
53041
  exportSettings: () => exportSettings,
53042
+ extractCodexCliStoredCredential: () => extractCodexCliStoredCredential,
52846
53043
  extractDreamProcessorResult: () => extractDreamProcessorResult,
52847
53044
  formatPiExtensionSource: () => formatPiExtensionSource,
52848
53045
  fromJson: () => fromJson,
@@ -52853,6 +53050,7 @@ __export(src_exports, {
52853
53050
  generateMemoryAudit: () => generateMemoryAudit,
52854
53051
  getAppVersion: () => getAppVersion,
52855
53052
  getAvailableTemplates: () => getAvailableTemplates,
53053
+ getCodexCliAuthPath: () => getCodexCliAuthPath,
52856
53054
  getCurrentRepo: () => getCurrentRepo,
52857
53055
  getDefaultDailyMemoryScaffold: () => getDefaultDailyMemoryScaffold,
52858
53056
  getDefaultDreamsScaffold: () => getDefaultDreamsScaffold,
@@ -52950,6 +53148,7 @@ __export(src_exports, {
52950
53148
  readProjectMemoryFile: () => readProjectMemoryFile,
52951
53149
  readProjectMemoryFileContent: () => readProjectMemoryFileContent,
52952
53150
  readProjectMemoryWithBackend: () => readProjectMemoryWithBackend,
53151
+ readStoredCredentialsFromAuthFile: () => readStoredCredentialsFromAuthFile,
52953
53152
  readWorkingMemory: () => readWorkingMemory,
52954
53153
  reconcileClaudeCliPaths: () => reconcileClaudeCliPaths,
52955
53154
  reconcileDroidCliPaths: () => reconcileDroidCliPaths,
@@ -52985,6 +53184,7 @@ __export(src_exports, {
52985
53184
  scheduleQmdProjectMemoryRefresh: () => scheduleQmdProjectMemoryRefresh,
52986
53185
  searchProjectMemory: () => searchProjectMemory,
52987
53186
  setCreateFnAgent: () => setCreateFnAgent,
53187
+ shouldHydrateStoredCredential: () => shouldHydrateStoredCredential,
52988
53188
  shouldSkipBackgroundQmdRefresh: () => shouldSkipBackgroundQmdRefresh,
52989
53189
  shouldTriggerExtraction: () => shouldTriggerExtraction,
52990
53190
  slugify: () => slugify,
@@ -53089,6 +53289,7 @@ var init_src = __esm({
53089
53289
  init_agent_companies_parser();
53090
53290
  init_agent_companies_exporter();
53091
53291
  init_chat_store();
53292
+ init_oauth_credential_interop();
53092
53293
  init_error_message();
53093
53294
  }
53094
53295
  });
@@ -54278,12 +54479,12 @@ var init_github_provider = __esm({
54278
54479
  });
54279
54480
 
54280
54481
  // ../engine/src/skill-resolver.ts
54281
- import { existsSync as existsSync19, readFileSync as readFileSync6 } from "node:fs";
54282
- import { dirname as dirname8, join as join22, resolve as resolve11 } from "node:path";
54482
+ import { existsSync as existsSync20, readFileSync as readFileSync7 } from "node:fs";
54483
+ import { dirname as dirname8, join as join23, resolve as resolve11 } from "node:path";
54283
54484
  function resolveProjectRoot(cwd) {
54284
54485
  let current = resolve11(cwd);
54285
54486
  while (true) {
54286
- if (existsSync19(join22(current, ".fusion"))) {
54487
+ if (existsSync20(join23(current, ".fusion"))) {
54287
54488
  return current;
54288
54489
  }
54289
54490
  const parent = dirname8(current);
@@ -54294,19 +54495,19 @@ function resolveProjectRoot(cwd) {
54294
54495
  }
54295
54496
  }
54296
54497
  function readJsonObject(path2) {
54297
- if (!existsSync19(path2)) {
54498
+ if (!existsSync20(path2)) {
54298
54499
  return {};
54299
54500
  }
54300
54501
  try {
54301
- const parsed = JSON.parse(readFileSync6(path2, "utf-8"));
54502
+ const parsed = JSON.parse(readFileSync7(path2, "utf-8"));
54302
54503
  return parsed && typeof parsed === "object" ? parsed : {};
54303
54504
  } catch {
54304
54505
  return {};
54305
54506
  }
54306
54507
  }
54307
54508
  function readProjectSettings(projectRootDir) {
54308
- const fusionSettings = join22(projectRootDir, ".fusion", "settings.json");
54309
- if (existsSync19(fusionSettings)) {
54509
+ const fusionSettings = join23(projectRootDir, ".fusion", "settings.json");
54510
+ if (existsSync20(fusionSettings)) {
54310
54511
  const parsed = readJsonObject(fusionSettings);
54311
54512
  return {
54312
54513
  skills: Array.isArray(parsed.skills) ? parsed.skills : void 0,
@@ -54531,51 +54732,51 @@ var init_context_limit_detector = __esm({
54531
54732
  });
54532
54733
 
54533
54734
  // ../engine/src/auth-storage.ts
54534
- import { existsSync as existsSync20, readFileSync as readFileSync7 } from "node:fs";
54535
- import { homedir as homedir4 } from "node:os";
54536
- import { join as join23 } from "node:path";
54735
+ import { existsSync as existsSync21, readFileSync as readFileSync8 } from "node:fs";
54736
+ import { homedir as homedir5 } from "node:os";
54737
+ import { join as join24 } from "node:path";
54537
54738
  import { AuthStorage } from "@mariozechner/pi-coding-agent";
54538
54739
  import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
54539
- function getHomeDir4() {
54540
- return process.env.HOME || process.env.USERPROFILE || homedir4();
54740
+ function getHomeDir5() {
54741
+ return process.env.HOME || process.env.USERPROFILE || homedir5();
54541
54742
  }
54542
- function getFusionAuthPath(home = getHomeDir4()) {
54543
- return join23(home, ".fusion", "agent", "auth.json");
54743
+ function getFusionAuthPath(home = getHomeDir5()) {
54744
+ return join24(home, ".fusion", "agent", "auth.json");
54544
54745
  }
54545
- function getFusionModelsPath(home = getHomeDir4()) {
54546
- return join23(home, ".fusion", "agent", "models.json");
54746
+ function getFusionModelsPath(home = getHomeDir5()) {
54747
+ return join24(home, ".fusion", "agent", "models.json");
54547
54748
  }
54548
- function getLegacyAuthPaths(home = getHomeDir4()) {
54749
+ function getLegacyAuthPaths(home = getHomeDir5()) {
54549
54750
  return [
54550
- join23(home, ".pi", "agent", "auth.json"),
54551
- join23(home, ".pi", "auth.json")
54751
+ join24(home, ".pi", "agent", "auth.json"),
54752
+ join24(home, ".pi", "auth.json")
54552
54753
  ];
54553
54754
  }
54554
- function getLegacyModelsPaths(home = getHomeDir4()) {
54755
+ function getSupplementalAuthPaths(home = getHomeDir5()) {
54555
54756
  return [
54556
- join23(home, ".pi", "agent", "models.json"),
54557
- join23(home, ".pi", "models.json")
54757
+ ...getLegacyAuthPaths(home),
54758
+ getCodexCliAuthPath(home)
54558
54759
  ];
54559
54760
  }
54560
- function getModelRegistryModelsPath(home = getHomeDir4()) {
54761
+ function getLegacyModelsPaths(home = getHomeDir5()) {
54762
+ return [
54763
+ join24(home, ".pi", "agent", "models.json"),
54764
+ join24(home, ".pi", "models.json")
54765
+ ];
54766
+ }
54767
+ function getModelRegistryModelsPath(home = getHomeDir5()) {
54561
54768
  const fusionModelsPath = getFusionModelsPath(home);
54562
- if (existsSync20(fusionModelsPath)) {
54769
+ if (existsSync21(fusionModelsPath)) {
54563
54770
  return fusionModelsPath;
54564
54771
  }
54565
- return getLegacyModelsPaths(home).find((modelsPath) => existsSync20(modelsPath)) ?? fusionModelsPath;
54772
+ return getLegacyModelsPaths(home).find((modelsPath) => existsSync21(modelsPath)) ?? fusionModelsPath;
54566
54773
  }
54567
- function readLegacyCredentials(authPaths = getLegacyAuthPaths()) {
54774
+ function readSupplementalCredentials(authPaths = getSupplementalAuthPaths()) {
54568
54775
  const credentials = {};
54569
54776
  for (const authPath of authPaths) {
54570
- if (!existsSync20(authPath)) {
54571
- continue;
54572
- }
54573
- try {
54574
- const parsed = JSON.parse(readFileSync7(authPath, "utf-8"));
54575
- for (const [provider, credential] of Object.entries(parsed)) {
54576
- credentials[provider] ??= credential;
54577
- }
54578
- } catch {
54777
+ const parsed = readStoredCredentialsFromAuthFile(authPath);
54778
+ for (const [provider, credential] of Object.entries(parsed)) {
54779
+ credentials[provider] = choosePreferredStoredCredential(credentials[provider], credential) ?? credential;
54579
54780
  }
54580
54781
  }
54581
54782
  return credentials;
@@ -54599,14 +54800,14 @@ function resolveStoredCredentialApiKey(providerId, credential) {
54599
54800
  }
54600
54801
  return void 0;
54601
54802
  }
54602
- function readModelsJsonApiKeys(home = getHomeDir4()) {
54803
+ function readModelsJsonApiKeys(home = getHomeDir5()) {
54603
54804
  const apiKeys = /* @__PURE__ */ new Map();
54604
54805
  const modelsPath = getModelRegistryModelsPath(home);
54605
- if (!existsSync20(modelsPath)) {
54806
+ if (!existsSync21(modelsPath)) {
54606
54807
  return apiKeys;
54607
54808
  }
54608
54809
  try {
54609
- const parsed = JSON.parse(readFileSync7(modelsPath, "utf-8"));
54810
+ const parsed = JSON.parse(readFileSync8(modelsPath, "utf-8"));
54610
54811
  const providers = parsed?.providers;
54611
54812
  if (providers) {
54612
54813
  for (const [providerId, config] of Object.entries(providers)) {
@@ -54621,8 +54822,20 @@ function readModelsJsonApiKeys(home = getHomeDir4()) {
54621
54822
  }
54622
54823
  function createFusionAuthStorage() {
54623
54824
  const primary = AuthStorage.create(getFusionAuthPath());
54624
- let legacyCredentials = readLegacyCredentials();
54825
+ let supplementalCredentials = readSupplementalCredentials();
54625
54826
  let modelsJsonApiKeys = readModelsJsonApiKeys();
54827
+ const syncSupplementalOauthCredentials = () => {
54828
+ for (const [provider, credential] of Object.entries(supplementalCredentials)) {
54829
+ const current = primary.get(provider);
54830
+ if (!shouldHydrateStoredCredential(current, credential)) {
54831
+ continue;
54832
+ }
54833
+ if (credential.type === "oauth" || credential.type === "api_key") {
54834
+ primary.set(provider, credential);
54835
+ }
54836
+ }
54837
+ };
54838
+ syncSupplementalOauthCredentials();
54626
54839
  return new Proxy(primary, {
54627
54840
  // Forward property writes to the target so that methods like
54628
54841
  // `setFallbackResolver` (called by ModelRegistry) correctly update the
@@ -54636,31 +54849,51 @@ function createFusionAuthStorage() {
54636
54849
  if (prop === "reload") {
54637
54850
  return () => {
54638
54851
  target.reload();
54639
- legacyCredentials = readLegacyCredentials();
54852
+ supplementalCredentials = readSupplementalCredentials();
54853
+ syncSupplementalOauthCredentials();
54640
54854
  modelsJsonApiKeys = readModelsJsonApiKeys();
54641
54855
  };
54642
54856
  }
54643
54857
  if (prop === "get") {
54644
- return (provider) => target.get(provider) ?? legacyCredentials[provider];
54858
+ return (provider) => choosePreferredStoredCredential(
54859
+ target.get(provider),
54860
+ supplementalCredentials[provider]
54861
+ );
54645
54862
  }
54646
54863
  if (prop === "has") {
54647
- return (provider) => target.has(provider) || provider in legacyCredentials || modelsJsonApiKeys.has(provider);
54864
+ return (provider) => target.has(provider) || provider in supplementalCredentials || modelsJsonApiKeys.has(provider);
54648
54865
  }
54649
54866
  if (prop === "hasAuth") {
54650
- return (provider) => target.hasAuth(provider) || Boolean(legacyCredentials[provider]) || modelsJsonApiKeys.has(provider);
54867
+ return (provider) => target.hasAuth(provider) || Boolean(supplementalCredentials[provider]) || modelsJsonApiKeys.has(provider);
54651
54868
  }
54652
54869
  if (prop === "getAll") {
54653
- return () => ({ ...legacyCredentials, ...target.getAll() });
54870
+ return () => {
54871
+ const providerIds = /* @__PURE__ */ new Set([
54872
+ ...Object.keys(supplementalCredentials),
54873
+ ...Object.keys(target.getAll())
54874
+ ]);
54875
+ const merged = {};
54876
+ for (const providerId of providerIds) {
54877
+ const credential = choosePreferredStoredCredential(
54878
+ target.get(providerId),
54879
+ supplementalCredentials[providerId]
54880
+ );
54881
+ if (credential) {
54882
+ merged[providerId] = credential;
54883
+ }
54884
+ }
54885
+ return merged;
54886
+ };
54654
54887
  }
54655
54888
  if (prop === "list") {
54656
- return () => Array.from(/* @__PURE__ */ new Set([...Object.keys(legacyCredentials), ...target.list(), ...modelsJsonApiKeys.keys()]));
54889
+ return () => Array.from(/* @__PURE__ */ new Set([...Object.keys(supplementalCredentials), ...target.list(), ...modelsJsonApiKeys.keys()]));
54657
54890
  }
54658
54891
  if (prop === "getApiKey") {
54659
54892
  return async (provider) => {
54660
54893
  const primaryKey = await target.getApiKey(provider);
54661
54894
  if (primaryKey) return primaryKey;
54662
- const legacyKey = resolveStoredCredentialApiKey(provider, legacyCredentials[provider]);
54663
- if (legacyKey) return legacyKey;
54895
+ const supplementalKey = resolveStoredCredentialApiKey(provider, supplementalCredentials[provider]);
54896
+ if (supplementalKey) return supplementalKey;
54664
54897
  return modelsJsonApiKeys.get(provider);
54665
54898
  };
54666
54899
  }
@@ -54671,17 +54904,18 @@ function createFusionAuthStorage() {
54671
54904
  var init_auth_storage = __esm({
54672
54905
  "../engine/src/auth-storage.ts"() {
54673
54906
  "use strict";
54907
+ init_src();
54674
54908
  }
54675
54909
  });
54676
54910
 
54677
54911
  // ../engine/src/custom-providers.ts
54678
- import { readFileSync as readFileSync8 } from "node:fs";
54679
- import { homedir as homedir5 } from "node:os";
54680
- import { join as join24 } from "node:path";
54912
+ import { readFileSync as readFileSync9 } from "node:fs";
54913
+ import { homedir as homedir6 } from "node:os";
54914
+ import { join as join25 } from "node:path";
54681
54915
  function readCustomProviders() {
54682
54916
  try {
54683
- const settingsPath = join24(homedir5(), ".fusion", "settings.json");
54684
- const raw = readFileSync8(settingsPath, "utf-8");
54917
+ const settingsPath = join25(homedir6(), ".fusion", "settings.json");
54918
+ const raw = readFileSync9(settingsPath, "utf-8");
54685
54919
  const parsed = JSON.parse(raw);
54686
54920
  return Array.isArray(parsed.customProviders) ? parsed.customProviders : [];
54687
54921
  } catch {
@@ -54695,11 +54929,11 @@ var init_custom_providers = __esm({
54695
54929
  });
54696
54930
 
54697
54931
  // ../engine/src/pi.ts
54698
- import { existsSync as existsSync21, readFileSync as readFileSync9 } from "node:fs";
54932
+ import { existsSync as existsSync22, readFileSync as readFileSync10 } from "node:fs";
54699
54933
  import { exec as exec2 } from "node:child_process";
54700
54934
  import { promisify as promisify3 } from "node:util";
54701
54935
  import { createRequire as createRequire2 } from "node:module";
54702
- import { basename as basename7, dirname as dirname9, join as join25, relative as relative3, isAbsolute as isAbsolute7, resolve as resolve12 } from "node:path";
54936
+ import { basename as basename7, dirname as dirname9, join as join26, relative as relative3, isAbsolute as isAbsolute7, resolve as resolve12 } from "node:path";
54703
54937
  import {
54704
54938
  createAgentSession,
54705
54939
  createBashTool,
@@ -54953,11 +55187,11 @@ function isRetryableModelSelectionError(message) {
54953
55187
  return normalized.includes("rate limit") || normalized.includes("too many requests") || normalized.includes("429") || normalized.includes("401") || normalized.includes("403") || normalized.includes("unauthorized") || normalized.includes("forbidden") || normalized.includes("authentication") || normalized.includes("invalid api key") || normalized.includes("invalid key") || normalized.includes("api key") || normalized.includes("overloaded") || normalized.includes("quota") || normalized.includes("capacity") || normalized.includes("temporarily unavailable") || normalized.includes("invalid temperature");
54954
55188
  }
54955
55189
  function readJsonObject2(path2) {
54956
- if (!existsSync21(path2)) {
55190
+ if (!existsSync22(path2)) {
54957
55191
  return {};
54958
55192
  }
54959
55193
  try {
54960
- const parsed = JSON.parse(readFileSync9(path2, "utf-8"));
55194
+ const parsed = JSON.parse(readFileSync10(path2, "utf-8"));
54961
55195
  return parsed && typeof parsed === "object" ? parsed : {};
54962
55196
  } catch {
54963
55197
  return {};
@@ -55094,17 +55328,17 @@ function siblingAgentDir(agentDir, siblingRoot) {
55094
55328
  if (basename7(agentDir) !== "agent") {
55095
55329
  return void 0;
55096
55330
  }
55097
- return join25(dirname9(dirname9(agentDir)), siblingRoot, "agent");
55331
+ return join26(dirname9(dirname9(agentDir)), siblingRoot, "agent");
55098
55332
  }
55099
55333
  function createReadOnlyPiSettingsView(cwd, agentDir) {
55100
55334
  const projectRoot = resolvePiExtensionProjectRoot(cwd);
55101
- const fusionAgentDir = agentDir.includes(`${join25(".fusion", "agent")}`) ? agentDir : siblingAgentDir(agentDir, ".fusion");
55102
- const legacyAgentDir = agentDir.includes(`${join25(".pi", "agent")}`) ? agentDir : siblingAgentDir(agentDir, ".pi");
55103
- const legacyGlobalSettings = legacyAgentDir ? readJsonObject2(join25(legacyAgentDir, "settings.json")) : {};
55104
- const fusionGlobalSettings = fusionAgentDir ? readJsonObject2(join25(fusionAgentDir, "settings.json")) : {};
55105
- const directGlobalSettings = readJsonObject2(join25(agentDir, "settings.json"));
55335
+ const fusionAgentDir = agentDir.includes(`${join26(".fusion", "agent")}`) ? agentDir : siblingAgentDir(agentDir, ".fusion");
55336
+ const legacyAgentDir = agentDir.includes(`${join26(".pi", "agent")}`) ? agentDir : siblingAgentDir(agentDir, ".pi");
55337
+ const legacyGlobalSettings = legacyAgentDir ? readJsonObject2(join26(legacyAgentDir, "settings.json")) : {};
55338
+ const fusionGlobalSettings = fusionAgentDir ? readJsonObject2(join26(fusionAgentDir, "settings.json")) : {};
55339
+ const directGlobalSettings = readJsonObject2(join26(agentDir, "settings.json"));
55106
55340
  const globalSettings = { ...legacyGlobalSettings, ...directGlobalSettings, ...fusionGlobalSettings };
55107
- const fusionProjectSettings = readJsonObject2(join25(projectRoot, ".fusion", "settings.json"));
55341
+ const fusionProjectSettings = readJsonObject2(join26(projectRoot, ".fusion", "settings.json"));
55108
55342
  const mergedSettings = { ...globalSettings, ...fusionProjectSettings };
55109
55343
  return {
55110
55344
  getGlobalSettings: () => structuredClone(globalSettings),
@@ -55115,27 +55349,27 @@ function createReadOnlyPiSettingsView(cwd, agentDir) {
55115
55349
  function getPackageManagerAgentDir() {
55116
55350
  const fusionAgentDir = getFusionAgentDir();
55117
55351
  const legacyAgentDir = getLegacyPiAgentDir();
55118
- const fusionSettings = readJsonObject2(join25(fusionAgentDir, "settings.json"));
55119
- const legacySettings = readJsonObject2(join25(legacyAgentDir, "settings.json"));
55120
- if (hasPackageManagerSettings(fusionSettings) || !existsSync21(legacyAgentDir)) {
55352
+ const fusionSettings = readJsonObject2(join26(fusionAgentDir, "settings.json"));
55353
+ const legacySettings = readJsonObject2(join26(legacyAgentDir, "settings.json"));
55354
+ if (hasPackageManagerSettings(fusionSettings) || !existsSync22(legacyAgentDir)) {
55121
55355
  return fusionAgentDir;
55122
55356
  }
55123
55357
  if (hasPackageManagerSettings(legacySettings)) {
55124
55358
  return legacyAgentDir;
55125
55359
  }
55126
- return existsSync21(fusionAgentDir) ? fusionAgentDir : legacyAgentDir;
55360
+ return existsSync22(fusionAgentDir) ? fusionAgentDir : legacyAgentDir;
55127
55361
  }
55128
55362
  function resolveVendoredClaudeCliEntry() {
55129
55363
  try {
55130
55364
  const require_ = createRequire2(import.meta.url);
55131
55365
  const pkgJsonPath = require_.resolve("@fusion/pi-claude-cli/package.json");
55132
- const pkgJson = JSON.parse(readFileSync9(pkgJsonPath, "utf-8"));
55366
+ const pkgJson = JSON.parse(readFileSync10(pkgJsonPath, "utf-8"));
55133
55367
  const extensions = pkgJson.pi?.extensions;
55134
55368
  if (!Array.isArray(extensions) || extensions.length === 0) return null;
55135
55369
  const entry = extensions[0];
55136
55370
  if (typeof entry !== "string" || entry.length === 0) return null;
55137
55371
  const path2 = resolve12(dirname9(pkgJsonPath), entry);
55138
- return existsSync21(path2) ? path2 : null;
55372
+ return existsSync22(path2) ? path2 : null;
55139
55373
  } catch {
55140
55374
  return null;
55141
55375
  }
@@ -55144,13 +55378,13 @@ function resolveVendoredDroidCliEntry() {
55144
55378
  try {
55145
55379
  const require_ = createRequire2(import.meta.url);
55146
55380
  const pkgJsonPath = require_.resolve("@fusion/droid-cli/package.json");
55147
- const pkgJson = JSON.parse(readFileSync9(pkgJsonPath, "utf-8"));
55381
+ const pkgJson = JSON.parse(readFileSync10(pkgJsonPath, "utf-8"));
55148
55382
  const extensions = pkgJson.pi?.extensions;
55149
55383
  if (!Array.isArray(extensions) || extensions.length === 0) return null;
55150
55384
  const entry = extensions[0];
55151
55385
  if (typeof entry !== "string" || entry.length === 0) return null;
55152
55386
  const path2 = resolve12(dirname9(pkgJsonPath), entry);
55153
- return existsSync21(path2) ? path2 : null;
55387
+ return existsSync22(path2) ? path2 : null;
55154
55388
  } catch {
55155
55389
  return null;
55156
55390
  }
@@ -55178,7 +55412,7 @@ async function registerExtensionProviders(cwd, modelRegistry) {
55178
55412
  const extensionsResult = await discoverAndLoadExtensions(
55179
55413
  doubleReconciledPaths,
55180
55414
  cwd,
55181
- join25(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery")
55415
+ join26(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery")
55182
55416
  );
55183
55417
  for (const { path: path2, error } of extensionsResult.errors) {
55184
55418
  extensionsLog.warn(`Failed to load ${path2}: ${error}`);
@@ -55233,10 +55467,10 @@ async function isCompleteGitWorktree(worktreePath) {
55233
55467
  }
55234
55468
  }
55235
55469
  async function assertValidWorktreeSession(cwd, projectRoot) {
55236
- if (!existsSync21(cwd)) {
55470
+ if (!existsSync22(cwd)) {
55237
55471
  throw new Error(`Refusing to start coding agent in missing worktree: ${cwd}`);
55238
55472
  }
55239
- if (!existsSync21(join25(cwd, ".git")) || !await isCompleteGitWorktree(cwd)) {
55473
+ if (!existsSync22(join26(cwd, ".git")) || !await isCompleteGitWorktree(cwd)) {
55240
55474
  throw new Error(`Refusing to start coding agent in incomplete worktree: ${cwd}`);
55241
55475
  }
55242
55476
  if (!await isRegisteredGitWorktree(projectRoot, cwd)) {
@@ -55817,7 +56051,7 @@ ${source.content ?? ""}`;
55817
56051
 
55818
56052
  // ../engine/src/research/providers/local-docs-provider.ts
55819
56053
  import { promises as fs } from "node:fs";
55820
- import { extname as extname2, join as join26, relative as relative4, resolve as resolve13 } from "node:path";
56054
+ import { extname as extname2, join as join27, relative as relative4, resolve as resolve13 } from "node:path";
55821
56055
  function buildExcerpt(content, terms) {
55822
56056
  const lower = content.toLowerCase();
55823
56057
  const first = terms.find((term) => lower.includes(term));
@@ -55946,7 +56180,7 @@ var init_local_docs_provider = __esm({
55946
56180
  const rootEntries = await fs.readdir(this.projectRoot, { withFileTypes: true });
55947
56181
  for (const entry of rootEntries) {
55948
56182
  if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
55949
- files.push(join26(this.projectRoot, entry.name));
56183
+ files.push(join27(this.projectRoot, entry.name));
55950
56184
  }
55951
56185
  }
55952
56186
  return [...new Set(files)];
@@ -55956,7 +56190,7 @@ var init_local_docs_provider = __esm({
55956
56190
  const entries = await fs.readdir(dir, { withFileTypes: true });
55957
56191
  for (const entry of entries) {
55958
56192
  this.throwIfAborted(signal);
55959
- const fullPath = join26(dir, entry.name);
56193
+ const fullPath = join27(dir, entry.name);
55960
56194
  const relPath = relative4(this.projectRoot, fullPath).replace(/\\/g, "/");
55961
56195
  if (matchesGitignore(relPath, ignorePatterns)) continue;
55962
56196
  if (entry.isDirectory()) {
@@ -55981,7 +56215,7 @@ var init_local_docs_provider = __esm({
55981
56215
  }
55982
56216
  async readGitignore() {
55983
56217
  try {
55984
- const content = await fs.readFile(join26(this.projectRoot, ".gitignore"), "utf-8");
56218
+ const content = await fs.readFile(join27(this.projectRoot, ".gitignore"), "utf-8");
55985
56219
  return content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
55986
56220
  } catch {
55987
56221
  return [];
@@ -56591,9 +56825,9 @@ var init_research_step_runner = __esm({
56591
56825
 
56592
56826
  // ../engine/src/agent-tools.ts
56593
56827
  import { appendFile as appendFile3, mkdir as mkdir11, readFile as readFile12, readdir as readdir7, stat as stat4, writeFile as writeFile10 } from "node:fs/promises";
56594
- import { existsSync as existsSync22 } from "node:fs";
56828
+ import { existsSync as existsSync23 } from "node:fs";
56595
56829
  import { createHash as createHash4 } from "node:crypto";
56596
- import { join as join27 } from "node:path";
56830
+ import { join as join28 } from "node:path";
56597
56831
  import { Type } from "@mariozechner/pi-ai";
56598
56832
  function sanitizeAgentMemoryId(agentId) {
56599
56833
  return agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
@@ -56605,16 +56839,16 @@ function agentDreamsDisplayPath(agentId) {
56605
56839
  return `${AGENT_MEMORY_ROOT2}/${sanitizeAgentMemoryId(agentId)}/${AGENT_DREAMS_FILENAME2}`;
56606
56840
  }
56607
56841
  function agentMemoryDirectory(rootDir, agentId) {
56608
- return join27(rootDir, AGENT_MEMORY_ROOT2, sanitizeAgentMemoryId(agentId));
56842
+ return join28(rootDir, AGENT_MEMORY_ROOT2, sanitizeAgentMemoryId(agentId));
56609
56843
  }
56610
56844
  function agentMemoryFilePath(rootDir, agentId) {
56611
- return join27(agentMemoryDirectory(rootDir, agentId), AGENT_MEMORY_FILENAME2);
56845
+ return join28(agentMemoryDirectory(rootDir, agentId), AGENT_MEMORY_FILENAME2);
56612
56846
  }
56613
56847
  function agentDreamsFilePath(rootDir, agentId) {
56614
- return join27(agentMemoryDirectory(rootDir, agentId), AGENT_DREAMS_FILENAME2);
56848
+ return join28(agentMemoryDirectory(rootDir, agentId), AGENT_DREAMS_FILENAME2);
56615
56849
  }
56616
56850
  function agentDailyFilePath(rootDir, agentId, date = /* @__PURE__ */ new Date()) {
56617
- return join27(agentMemoryDirectory(rootDir, agentId), `${date.toISOString().slice(0, 10)}.md`);
56851
+ return join28(agentMemoryDirectory(rootDir, agentId), `${date.toISOString().slice(0, 10)}.md`);
56618
56852
  }
56619
56853
  function qmdAgentMemoryCollectionName(rootDir, agentId) {
56620
56854
  const hash = createHash4("sha1").update(`${rootDir}:${agentId}`).digest("hex").slice(0, 12);
@@ -56650,7 +56884,7 @@ async function syncAgentMemoryFile(rootDir, agentMemory) {
56650
56884
  const dir = agentMemoryDirectory(rootDir, agentMemory.agentId);
56651
56885
  await mkdir11(dir, { recursive: true });
56652
56886
  const longTermPath = agentMemoryFilePath(rootDir, agentMemory.agentId);
56653
- if (!existsSync22(longTermPath)) {
56887
+ if (!existsSync23(longTermPath)) {
56654
56888
  const title = agentMemory.agentName?.trim() ? `# Agent Memory: ${agentMemory.agentName.trim()}` : "# Agent Memory";
56655
56889
  const fileContent = `${title}
56656
56890
 
@@ -56661,11 +56895,11 @@ ${content || ""}
56661
56895
  await writeFile10(longTermPath, fileContent, "utf-8");
56662
56896
  }
56663
56897
  const dreamsPath = agentDreamsFilePath(rootDir, agentMemory.agentId);
56664
- if (!existsSync22(dreamsPath)) {
56898
+ if (!existsSync23(dreamsPath)) {
56665
56899
  await writeFile10(dreamsPath, "# Agent Memory Dreams\n\n<!-- Synthesized patterns from this agent's daily notes. -->\n", "utf-8");
56666
56900
  }
56667
56901
  const dailyPath = agentDailyFilePath(rootDir, agentMemory.agentId);
56668
- if (!existsSync22(dailyPath)) {
56902
+ if (!existsSync23(dailyPath)) {
56669
56903
  await writeFile10(dailyPath, `# Agent Daily Memory ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
56670
56904
 
56671
56905
  <!-- Running observations for this agent. -->
@@ -56689,7 +56923,7 @@ async function listAgentMemoryFiles2(rootDir, agentMemory) {
56689
56923
  }
56690
56924
  for (const entry of entries) {
56691
56925
  if (!DAILY_AGENT_MEMORY_RE2.test(entry)) continue;
56692
- const absPath = join27(dir, entry);
56926
+ const absPath = join28(dir, entry);
56693
56927
  const fileStat = await stat4(absPath);
56694
56928
  if (fileStat.isFile()) {
56695
56929
  files.push({
@@ -56851,7 +57085,7 @@ function resolveAgentMemoryPath(rootDir, agentId, path2) {
56851
57085
  return null;
56852
57086
  }
56853
57087
  return {
56854
- absPath: join27(agentMemoryDirectory(rootDir, agentId), filename),
57088
+ absPath: join28(agentMemoryDirectory(rootDir, agentId), filename),
56855
57089
  displayPath: `${prefix}${filename}`
56856
57090
  };
56857
57091
  }
@@ -57866,6 +58100,7 @@ __export(agent_session_helpers_exports, {
57866
58100
  createResolvedAgentSession: () => createResolvedAgentSession,
57867
58101
  describeAgentModel: () => describeAgentModel,
57868
58102
  extractRuntimeHint: () => extractRuntimeHint,
58103
+ extractRuntimeModel: () => extractRuntimeModel,
57869
58104
  promptWithAutoRetry: () => promptWithAutoRetry
57870
58105
  });
57871
58106
  function extractRuntimeHint(runtimeConfig) {
@@ -57876,6 +58111,24 @@ function extractRuntimeHint(runtimeConfig) {
57876
58111
  const normalizedHint = hint.trim();
57877
58112
  return normalizedHint.length > 0 ? normalizedHint : void 0;
57878
58113
  }
58114
+ function extractRuntimeModel(runtimeConfig) {
58115
+ const combined = typeof runtimeConfig?.model === "string" ? runtimeConfig.model.trim() : "";
58116
+ if (combined) {
58117
+ const slashIdx = combined.indexOf("/");
58118
+ if (slashIdx > 0 && slashIdx < combined.length - 1) {
58119
+ return {
58120
+ provider: combined.slice(0, slashIdx).trim() || void 0,
58121
+ modelId: combined.slice(slashIdx + 1).trim() || void 0
58122
+ };
58123
+ }
58124
+ }
58125
+ const provider = typeof runtimeConfig?.modelProvider === "string" ? runtimeConfig.modelProvider.trim() : "";
58126
+ const modelId = typeof runtimeConfig?.modelId === "string" ? runtimeConfig.modelId.trim() : "";
58127
+ return {
58128
+ provider: provider || void 0,
58129
+ modelId: modelId || void 0
58130
+ };
58131
+ }
57879
58132
  async function createResolvedAgentSession(options) {
57880
58133
  const { sessionPurpose, pluginRunner, runtimeHint, ...runtimeOptions } = options;
57881
58134
  const context = buildRuntimeResolutionContext(sessionPurpose, pluginRunner, runtimeHint);
@@ -59129,6 +59382,38 @@ var init_notifier = __esm({
59129
59382
  }
59130
59383
  });
59131
59384
 
59385
+ // ../engine/src/fallback-model-observer.ts
59386
+ function buildFallbackLogMessage(label, payload) {
59387
+ return `[fallback] ${label} switched from ${payload.primaryModel} to ${payload.fallbackModel} (${payload.triggerPoint})`;
59388
+ }
59389
+ function createFallbackModelObserver(options) {
59390
+ return async (payload) => {
59391
+ const taskId = options.taskId ?? payload.taskId;
59392
+ const taskTitle = options.taskTitle ?? payload.taskTitle;
59393
+ const message = buildFallbackLogMessage(options.label, payload);
59394
+ if (taskId && options.store?.logEntry) {
59395
+ await options.store.logEntry(taskId, message).catch(() => void 0);
59396
+ }
59397
+ if (taskId && options.store?.appendAgentLog) {
59398
+ await options.store.appendAgentLog(taskId, message, "text", void 0, options.agent).catch(() => void 0);
59399
+ }
59400
+ await notifyFallbackUsed({
59401
+ primaryModel: payload.primaryModel,
59402
+ fallbackModel: payload.fallbackModel,
59403
+ triggerPoint: payload.triggerPoint,
59404
+ taskId,
59405
+ taskTitle,
59406
+ timestamp: payload.timestamp
59407
+ });
59408
+ };
59409
+ }
59410
+ var init_fallback_model_observer = __esm({
59411
+ "../engine/src/fallback-model-observer.ts"() {
59412
+ "use strict";
59413
+ init_notifier();
59414
+ }
59415
+ });
59416
+
59132
59417
  // ../engine/src/reviewer.ts
59133
59418
  async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptContent, baseline, options = {}) {
59134
59419
  let liveSettings = options.settings;
@@ -59259,7 +59544,13 @@ async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptC
59259
59544
  ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
59260
59545
  taskId: options.taskId,
59261
59546
  taskTitle: options.taskTitle,
59262
- onFallbackModelUsed: notifyFallbackUsed,
59547
+ onFallbackModelUsed: createFallbackModelObserver({
59548
+ agent: "reviewer",
59549
+ label: "reviewer",
59550
+ store: options.store,
59551
+ taskId: options.taskId,
59552
+ taskTitle: options.taskTitle
59553
+ }),
59263
59554
  beforeSpawnSession: async () => {
59264
59555
  if (!options.store) return;
59265
59556
  let finalSettings;
@@ -59446,7 +59737,7 @@ var init_reviewer = __esm({
59446
59737
  init_logger2();
59447
59738
  init_usage_limit_detector();
59448
59739
  init_agent_instructions();
59449
- init_notifier();
59740
+ init_fallback_model_observer();
59450
59741
  init_agent_tools();
59451
59742
  REVIEWER_SYSTEM_PROMPT = `You are an independent code and plan reviewer.
59452
59743
 
@@ -59807,7 +60098,7 @@ var init_recovery_policy = __esm({
59807
60098
  // ../engine/src/triage.ts
59808
60099
  import { Type as Type2 } from "@mariozechner/pi-ai";
59809
60100
  import { readFile as readFile14 } from "node:fs/promises";
59810
- import { join as join28 } from "node:path";
60101
+ import { join as join29 } from "node:path";
59811
60102
  function extractPromptDeclaredTitle(prompt, taskId) {
59812
60103
  const headingMatch = prompt.match(/^#\s+Task:\s+([A-Z]+-\d+)\s+-\s+(.+)$/m);
59813
60104
  if (!headingMatch) return null;
@@ -59836,9 +60127,9 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
59836
60127
  return { attachmentContents, imageContents };
59837
60128
  }
59838
60129
  const { readFile: readFile20 } = await import("node:fs/promises");
59839
- const { join: join42 } = await import("node:path");
60130
+ const { join: join43 } = await import("node:path");
59840
60131
  for (const att of attachments) {
59841
- const filePath = join42(
60132
+ const filePath = join43(
59842
60133
  rootDir,
59843
60134
  ".fusion",
59844
60135
  "tasks",
@@ -60063,7 +60354,7 @@ var init_triage = __esm({
60063
60354
  init_concurrency();
60064
60355
  init_agent_logger();
60065
60356
  init_agent_instructions();
60066
- init_notifier();
60357
+ init_fallback_model_observer();
60067
60358
  init_logger2();
60068
60359
  init_usage_limit_detector();
60069
60360
  init_transient_error_detector();
@@ -60673,7 +60964,7 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
60673
60964
  return false;
60674
60965
  }
60675
60966
  const settings = await this.store.getSettings();
60676
- const promptPath = join28(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
60967
+ const promptPath = join29(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
60677
60968
  const written = await readFile14(promptPath, "utf-8").catch((err) => {
60678
60969
  const msg = err instanceof Error ? err.message : String(err);
60679
60970
  planLog.warn(`${task.id}: failed to read PROMPT.md during approved-spec recovery (${promptPath}): ${msg}`);
@@ -60903,7 +61194,13 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
60903
61194
  ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
60904
61195
  taskId: task.id,
60905
61196
  taskTitle: task.title,
60906
- onFallbackModelUsed: notifyFallbackUsed
61197
+ onFallbackModelUsed: createFallbackModelObserver({
61198
+ agent: "triage",
61199
+ label: "triage",
61200
+ store: this.store,
61201
+ taskId: task.id,
61202
+ taskTitle: task.title
61203
+ })
60907
61204
  });
60908
61205
  const modelDesc = describeModel(session);
60909
61206
  planLog.log(`${task.id}: using model ${modelDesc}`);
@@ -61046,7 +61343,13 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
61046
61343
  ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
61047
61344
  taskId: task.id,
61048
61345
  taskTitle: task.title,
61049
- onFallbackModelUsed: notifyFallbackUsed
61346
+ onFallbackModelUsed: createFallbackModelObserver({
61347
+ agent: "triage",
61348
+ label: "triage",
61349
+ store: this.store,
61350
+ taskId: task.id,
61351
+ taskTitle: task.title
61352
+ })
61050
61353
  });
61051
61354
  session = fallbackResult.session;
61052
61355
  const fallbackModelDesc = describeModel(session);
@@ -61131,7 +61434,7 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
61131
61434
  return;
61132
61435
  }
61133
61436
  const written = await readFile14(
61134
- join28(this.rootDir, promptPath),
61437
+ join29(this.rootDir, promptPath),
61135
61438
  "utf-8"
61136
61439
  ).catch((err) => {
61137
61440
  const msg = err instanceof Error ? err.message : String(err);
@@ -61450,9 +61753,9 @@ Remove or replace these ids and call fn_task_create again.`
61450
61753
  }
61451
61754
  try {
61452
61755
  const { readFile: readFile20 } = await import("node:fs/promises");
61453
- const { join: join42 } = await import("node:path");
61756
+ const { join: join43 } = await import("node:path");
61454
61757
  const promptContent = await readFile20(
61455
- join42(rootDir, promptPath),
61758
+ join43(rootDir, promptPath),
61456
61759
  "utf-8"
61457
61760
  ).catch((err) => {
61458
61761
  const msg = err instanceof Error ? err.message : String(err);
@@ -61680,160 +61983,8 @@ Take a completely different approach to writing this specification. Do NOT repea
61680
61983
  }
61681
61984
  });
61682
61985
 
61683
- // ../engine/src/session-token-usage.ts
61684
- var session_token_usage_exports = {};
61685
- __export(session_token_usage_exports, {
61686
- accumulateSessionTokenUsage: () => accumulateSessionTokenUsage
61687
- });
61688
- function readSessionStats(session) {
61689
- const accessor = session.getSessionStats;
61690
- if (typeof accessor !== "function") return void 0;
61691
- try {
61692
- return accessor.call(session);
61693
- } catch {
61694
- return void 0;
61695
- }
61696
- }
61697
- async function accumulateSessionTokenUsage(store, taskId, session) {
61698
- try {
61699
- const stats = readSessionStats(session);
61700
- const tokens = stats?.tokens;
61701
- if (!tokens) return;
61702
- const currentInput = (tokens.input ?? 0) + (tokens.cacheWrite ?? 0);
61703
- const currentOutput = tokens.output ?? 0;
61704
- const currentCached = tokens.cacheRead ?? 0;
61705
- const baseline = sessionBaselines.get(session) ?? { input: 0, output: 0, cached: 0 };
61706
- const inputDelta = Math.max(0, currentInput - baseline.input);
61707
- const outputDelta = Math.max(0, currentOutput - baseline.output);
61708
- const cachedDelta = Math.max(0, currentCached - baseline.cached);
61709
- sessionBaselines.set(session, {
61710
- input: currentInput,
61711
- output: currentOutput,
61712
- cached: currentCached
61713
- });
61714
- if (inputDelta === 0 && outputDelta === 0 && cachedDelta === 0) return;
61715
- const task = await store.getTask(taskId);
61716
- const now = (/* @__PURE__ */ new Date()).toISOString();
61717
- const newInput = (task.tokenUsage?.inputTokens ?? 0) + inputDelta;
61718
- const newOutput = (task.tokenUsage?.outputTokens ?? 0) + outputDelta;
61719
- const newCached = (task.tokenUsage?.cachedTokens ?? 0) + cachedDelta;
61720
- await store.updateTask(taskId, {
61721
- tokenUsage: {
61722
- inputTokens: newInput,
61723
- outputTokens: newOutput,
61724
- cachedTokens: newCached,
61725
- totalTokens: newInput + newOutput + newCached,
61726
- firstUsedAt: task.tokenUsage?.firstUsedAt ?? now,
61727
- lastUsedAt: now
61728
- }
61729
- });
61730
- } catch (err) {
61731
- const message = err instanceof Error ? err.message : String(err);
61732
- log14.warn(`${taskId}: session token usage accumulate failed: ${message}`);
61733
- }
61734
- }
61735
- var log14, sessionBaselines;
61736
- var init_session_token_usage = __esm({
61737
- "../engine/src/session-token-usage.ts"() {
61738
- "use strict";
61739
- init_logger2();
61740
- log14 = createLogger2("session-token-usage");
61741
- sessionBaselines = /* @__PURE__ */ new WeakMap();
61742
- }
61743
- });
61744
-
61745
- // ../engine/src/run-audit.ts
61746
- function createRunAuditor(store, context) {
61747
- if (!context) {
61748
- return {
61749
- git: async () => {
61750
- },
61751
- database: async () => {
61752
- },
61753
- filesystem: async () => {
61754
- }
61755
- };
61756
- }
61757
- const hasRecordAuditEvent = typeof store.recordRunAuditEvent === "function";
61758
- if (!hasRecordAuditEvent) {
61759
- return {
61760
- git: async () => {
61761
- },
61762
- database: async () => {
61763
- },
61764
- filesystem: async () => {
61765
- }
61766
- };
61767
- }
61768
- return {
61769
- git: async (input) => {
61770
- const eventInput = {
61771
- taskId: context.taskId,
61772
- agentId: context.agentId,
61773
- runId: context.runId,
61774
- domain: "git",
61775
- mutationType: input.type,
61776
- target: input.target,
61777
- metadata: {
61778
- phase: context.phase,
61779
- ...context.source ? { source: context.source } : {},
61780
- ...input.metadata
61781
- }
61782
- };
61783
- await store.recordRunAuditEvent(eventInput);
61784
- },
61785
- database: async (input) => {
61786
- const inferredTaskId = input.target.startsWith("FN-") || input.target.startsWith("KB-") ? input.target : context.taskId;
61787
- const eventInput = {
61788
- taskId: inferredTaskId,
61789
- agentId: context.agentId,
61790
- runId: context.runId,
61791
- domain: "database",
61792
- mutationType: input.type,
61793
- target: input.target,
61794
- metadata: {
61795
- phase: context.phase,
61796
- ...context.source ? { source: context.source } : {},
61797
- ...input.metadata
61798
- }
61799
- };
61800
- await store.recordRunAuditEvent(eventInput);
61801
- },
61802
- filesystem: async (input) => {
61803
- const eventInput = {
61804
- taskId: context.taskId,
61805
- agentId: context.agentId,
61806
- runId: context.runId,
61807
- domain: "filesystem",
61808
- mutationType: input.type,
61809
- target: input.target,
61810
- metadata: {
61811
- phase: context.phase,
61812
- ...context.source ? { source: context.source } : {},
61813
- ...input.metadata
61814
- }
61815
- };
61816
- await store.recordRunAuditEvent(eventInput);
61817
- }
61818
- };
61819
- }
61820
- function generateSyntheticRunId(prefix, taskId) {
61821
- const timestamp = Date.now();
61822
- const random = Math.random().toString(36).slice(2, 6);
61823
- return `${prefix}-${taskId}-${timestamp}-${random}`;
61824
- }
61825
- var init_run_audit = __esm({
61826
- "../engine/src/run-audit.ts"() {
61827
- "use strict";
61828
- }
61829
- });
61830
-
61831
- // ../engine/src/merger.ts
61832
- import { execSync, exec as exec3, spawn as spawn3 } from "node:child_process";
61833
- import { promisify as promisify4 } from "node:util";
61834
- import { existsSync as existsSync23 } from "node:fs";
61835
- import { join as join29 } from "node:path";
61836
- import { Type as Type3 } from "typebox";
61986
+ // ../engine/src/verification-utils.ts
61987
+ import { spawn as spawn3 } from "node:child_process";
61837
61988
  async function execWithProcessGroup(command, options) {
61838
61989
  return new Promise((resolve20, reject) => {
61839
61990
  if (options.signal?.aborted) {
@@ -61945,6 +62096,11 @@ function truncateWithEllipsis(text, maxChars) {
61945
62096
  return `${text.slice(0, maxChars)}
61946
62097
  ... (truncated)`;
61947
62098
  }
62099
+ function truncateOutput(output) {
62100
+ if (output.length <= VERIFICATION_LOG_MAX_CHARS) return output;
62101
+ return `... output truncated to last ${VERIFICATION_LOG_MAX_CHARS} characters ...
62102
+ ${output.slice(-VERIFICATION_LOG_MAX_CHARS)}`;
62103
+ }
61948
62104
  function summarizeVerificationOutput(output, type) {
61949
62105
  const lines = output.split("\n");
61950
62106
  let summaryLine = null;
@@ -62010,6 +62166,13 @@ function summarizeVerificationOutput(output, type) {
62010
62166
  failureNames.add(truncated);
62011
62167
  }
62012
62168
  const footer = "(full output available in engine logs)";
62169
+ if (type === "build") {
62170
+ const buildError = output.length > 500 ? `${output.slice(0, 500)}
62171
+ ... (truncated)` : output;
62172
+ return `Build output:
62173
+ ${buildError}
62174
+ ${footer}`;
62175
+ }
62013
62176
  const parts = [];
62014
62177
  if (summaryLine) {
62015
62178
  parts.push(summaryLine);
@@ -62027,29 +62190,292 @@ function summarizeVerificationOutput(output, type) {
62027
62190
  parts.push(` \u2022 ... and ${names.length - 5} more failures`);
62028
62191
  }
62029
62192
  }
62030
- if (parts.length > 0) {
62031
- parts.push(footer);
62032
- return parts.join("\n");
62033
- }
62034
- const trimmed = output.trim();
62035
- if (!trimmed) {
62036
- return `Verification command failed with no output
62193
+ if (parts.length === 0) {
62194
+ if (output.trim().length === 0) {
62195
+ return `no output
62196
+ ${footer}`;
62197
+ }
62198
+ return `${truncateOutput(output)}
62037
62199
  ${footer}`;
62038
62200
  }
62039
- if (trimmed.length <= 500) {
62040
- return `${trimmed}
62201
+ return parts.join("\n") + `
62041
62202
  ${footer}`;
62203
+ }
62204
+ async function runVerificationCommand(store, rootDir, taskId, command, type, signal, log18, agentLabel) {
62205
+ const logger2 = log18 ?? { log: console.log, error: console.error, warn: console.warn };
62206
+ const label = agentLabel ?? "merger";
62207
+ if (signal?.aborted) {
62208
+ throw Object.assign(
62209
+ new Error(`Command aborted before start: ${command}`),
62210
+ { code: "ABORT_ERR", aborted: true }
62211
+ );
62042
62212
  }
62043
- let cutoff = 500;
62044
- for (let i = 500; i < trimmed.length; i++) {
62045
- if (trimmed[i] === " " || trimmed[i] === "\n") {
62046
- cutoff = i;
62047
- break;
62213
+ logger2.log(`${taskId}: running ${type} command: ${command}`);
62214
+ await store.logEntry(taskId, `[verification] Running ${type} command: ${command}`);
62215
+ await store.appendAgentLog(taskId, `Running ${type} command`, "tool", command, label);
62216
+ const result = {
62217
+ command,
62218
+ exitCode: null,
62219
+ stdout: "",
62220
+ stderr: "",
62221
+ success: false
62222
+ };
62223
+ const verificationStartedAt = Date.now();
62224
+ try {
62225
+ const { stdout, stderr, bufferOverflow } = await execWithProcessGroup(command, {
62226
+ cwd: rootDir,
62227
+ timeout: VERIFICATION_COMMAND_TIMEOUT_MS,
62228
+ maxBuffer: VERIFICATION_COMMAND_MAX_BUFFER,
62229
+ signal
62230
+ });
62231
+ if (signal?.aborted) {
62232
+ throw Object.assign(
62233
+ new Error(`Command aborted: ${command}`),
62234
+ { code: "ABORT_ERR", aborted: true }
62235
+ );
62048
62236
  }
62237
+ result.stdout = stdout?.toString?.() || "";
62238
+ result.stderr = stderr?.toString?.() || "";
62239
+ result.exitCode = 0;
62240
+ result.success = true;
62241
+ const verificationDurationMs = Date.now() - verificationStartedAt;
62242
+ const timingDetail = `${verificationDurationMs}ms`;
62243
+ if (bufferOverflow) {
62244
+ logger2.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
62245
+ await store.logEntry(
62246
+ taskId,
62247
+ `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
62248
+ );
62249
+ await store.appendAgentLog(
62250
+ taskId,
62251
+ `${type} command succeeded (exit 0)`,
62252
+ "tool_result",
62253
+ timingDetail,
62254
+ label
62255
+ );
62256
+ } else {
62257
+ logger2.log(`${taskId}: ${type} command succeeded in ${verificationDurationMs}ms`);
62258
+ await store.logEntry(taskId, `[timing] [verification] ${type} command succeeded (exit 0) in ${verificationDurationMs}ms`);
62259
+ await store.appendAgentLog(
62260
+ taskId,
62261
+ `${type} command succeeded (exit 0)`,
62262
+ "tool_result",
62263
+ timingDetail,
62264
+ label
62265
+ );
62266
+ }
62267
+ return result;
62268
+ } catch (error) {
62269
+ if (signal?.aborted) {
62270
+ throw Object.assign(
62271
+ new Error(`Command aborted: ${command}`),
62272
+ { code: "ABORT_ERR", aborted: true }
62273
+ );
62274
+ }
62275
+ const verificationDurationMs = Date.now() - verificationStartedAt;
62276
+ const err = error;
62277
+ result.stdout = err?.stdout?.toString?.() || "";
62278
+ result.stderr = err?.stderr?.toString?.() || "";
62279
+ result.exitCode = typeof err?.status === "number" ? err.status : typeof err?.code === "number" ? err.code : null;
62280
+ const maxBufferExceeded = err?.code === "ENOBUFS" || err?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || String(err?.message ?? "").includes("maxBuffer");
62281
+ result.success = maxBufferExceeded && result.exitCode === 0;
62282
+ if (result.success) {
62283
+ logger2.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
62284
+ await store.logEntry(
62285
+ taskId,
62286
+ `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
62287
+ );
62288
+ await store.appendAgentLog(
62289
+ taskId,
62290
+ `${type} command succeeded (exit 0)`,
62291
+ "tool_result",
62292
+ `${verificationDurationMs}ms`,
62293
+ label
62294
+ );
62295
+ return result;
62296
+ }
62297
+ const output = result.stderr || result.stdout || err?.message || "Unknown error";
62298
+ const summary = summarizeVerificationOutput(output, type);
62299
+ logger2.error(`${taskId}: ${type} command failed (exit ${result.exitCode}) in ${verificationDurationMs}ms; output captured in task log`);
62300
+ await store.logEntry(
62301
+ taskId,
62302
+ `[timing] [verification] ${type} command failed (exit ${result.exitCode}) after ${verificationDurationMs}ms:
62303
+ ${summary}`
62304
+ );
62305
+ await store.appendAgentLog(
62306
+ taskId,
62307
+ `${type} command failed (exit ${result.exitCode})`,
62308
+ "tool_error",
62309
+ summary,
62310
+ label
62311
+ );
62312
+ }
62313
+ return result;
62314
+ }
62315
+ var VERIFICATION_COMMAND_MAX_BUFFER, VERIFICATION_COMMAND_TIMEOUT_MS, VERIFICATION_LOG_MAX_CHARS;
62316
+ var init_verification_utils = __esm({
62317
+ "../engine/src/verification-utils.ts"() {
62318
+ "use strict";
62319
+ VERIFICATION_COMMAND_MAX_BUFFER = 50 * 1024 * 1024;
62320
+ VERIFICATION_COMMAND_TIMEOUT_MS = 6e5;
62321
+ VERIFICATION_LOG_MAX_CHARS = 2e4;
62322
+ }
62323
+ });
62324
+
62325
+ // ../engine/src/session-token-usage.ts
62326
+ var session_token_usage_exports = {};
62327
+ __export(session_token_usage_exports, {
62328
+ accumulateSessionTokenUsage: () => accumulateSessionTokenUsage
62329
+ });
62330
+ function readSessionStats(session) {
62331
+ const accessor = session.getSessionStats;
62332
+ if (typeof accessor !== "function") return void 0;
62333
+ try {
62334
+ return accessor.call(session);
62335
+ } catch {
62336
+ return void 0;
62337
+ }
62338
+ }
62339
+ async function accumulateSessionTokenUsage(store, taskId, session) {
62340
+ try {
62341
+ const stats = readSessionStats(session);
62342
+ const tokens = stats?.tokens;
62343
+ if (!tokens) return;
62344
+ const currentInput = (tokens.input ?? 0) + (tokens.cacheWrite ?? 0);
62345
+ const currentOutput = tokens.output ?? 0;
62346
+ const currentCached = tokens.cacheRead ?? 0;
62347
+ const baseline = sessionBaselines.get(session) ?? { input: 0, output: 0, cached: 0 };
62348
+ const inputDelta = Math.max(0, currentInput - baseline.input);
62349
+ const outputDelta = Math.max(0, currentOutput - baseline.output);
62350
+ const cachedDelta = Math.max(0, currentCached - baseline.cached);
62351
+ sessionBaselines.set(session, {
62352
+ input: currentInput,
62353
+ output: currentOutput,
62354
+ cached: currentCached
62355
+ });
62356
+ if (inputDelta === 0 && outputDelta === 0 && cachedDelta === 0) return;
62357
+ const task = await store.getTask(taskId);
62358
+ const now = (/* @__PURE__ */ new Date()).toISOString();
62359
+ const newInput = (task.tokenUsage?.inputTokens ?? 0) + inputDelta;
62360
+ const newOutput = (task.tokenUsage?.outputTokens ?? 0) + outputDelta;
62361
+ const newCached = (task.tokenUsage?.cachedTokens ?? 0) + cachedDelta;
62362
+ await store.updateTask(taskId, {
62363
+ tokenUsage: {
62364
+ inputTokens: newInput,
62365
+ outputTokens: newOutput,
62366
+ cachedTokens: newCached,
62367
+ totalTokens: newInput + newOutput + newCached,
62368
+ firstUsedAt: task.tokenUsage?.firstUsedAt ?? now,
62369
+ lastUsedAt: now
62370
+ }
62371
+ });
62372
+ } catch (err) {
62373
+ const message = err instanceof Error ? err.message : String(err);
62374
+ log14.warn(`${taskId}: session token usage accumulate failed: ${message}`);
62049
62375
  }
62050
- return `${trimmed.slice(0, cutoff)}...
62051
- ${footer}`;
62052
62376
  }
62377
+ var log14, sessionBaselines;
62378
+ var init_session_token_usage = __esm({
62379
+ "../engine/src/session-token-usage.ts"() {
62380
+ "use strict";
62381
+ init_logger2();
62382
+ log14 = createLogger2("session-token-usage");
62383
+ sessionBaselines = /* @__PURE__ */ new WeakMap();
62384
+ }
62385
+ });
62386
+
62387
+ // ../engine/src/run-audit.ts
62388
+ function createRunAuditor(store, context) {
62389
+ if (!context) {
62390
+ return {
62391
+ git: async () => {
62392
+ },
62393
+ database: async () => {
62394
+ },
62395
+ filesystem: async () => {
62396
+ }
62397
+ };
62398
+ }
62399
+ const hasRecordAuditEvent = typeof store.recordRunAuditEvent === "function";
62400
+ if (!hasRecordAuditEvent) {
62401
+ return {
62402
+ git: async () => {
62403
+ },
62404
+ database: async () => {
62405
+ },
62406
+ filesystem: async () => {
62407
+ }
62408
+ };
62409
+ }
62410
+ return {
62411
+ git: async (input) => {
62412
+ const eventInput = {
62413
+ taskId: context.taskId,
62414
+ agentId: context.agentId,
62415
+ runId: context.runId,
62416
+ domain: "git",
62417
+ mutationType: input.type,
62418
+ target: input.target,
62419
+ metadata: {
62420
+ phase: context.phase,
62421
+ ...context.source ? { source: context.source } : {},
62422
+ ...input.metadata
62423
+ }
62424
+ };
62425
+ await store.recordRunAuditEvent(eventInput);
62426
+ },
62427
+ database: async (input) => {
62428
+ const inferredTaskId = input.target.startsWith("FN-") || input.target.startsWith("KB-") ? input.target : context.taskId;
62429
+ const eventInput = {
62430
+ taskId: inferredTaskId,
62431
+ agentId: context.agentId,
62432
+ runId: context.runId,
62433
+ domain: "database",
62434
+ mutationType: input.type,
62435
+ target: input.target,
62436
+ metadata: {
62437
+ phase: context.phase,
62438
+ ...context.source ? { source: context.source } : {},
62439
+ ...input.metadata
62440
+ }
62441
+ };
62442
+ await store.recordRunAuditEvent(eventInput);
62443
+ },
62444
+ filesystem: async (input) => {
62445
+ const eventInput = {
62446
+ taskId: context.taskId,
62447
+ agentId: context.agentId,
62448
+ runId: context.runId,
62449
+ domain: "filesystem",
62450
+ mutationType: input.type,
62451
+ target: input.target,
62452
+ metadata: {
62453
+ phase: context.phase,
62454
+ ...context.source ? { source: context.source } : {},
62455
+ ...input.metadata
62456
+ }
62457
+ };
62458
+ await store.recordRunAuditEvent(eventInput);
62459
+ }
62460
+ };
62461
+ }
62462
+ function generateSyntheticRunId(prefix, taskId) {
62463
+ const timestamp = Date.now();
62464
+ const random = Math.random().toString(36).slice(2, 6);
62465
+ return `${prefix}-${taskId}-${timestamp}-${random}`;
62466
+ }
62467
+ var init_run_audit = __esm({
62468
+ "../engine/src/run-audit.ts"() {
62469
+ "use strict";
62470
+ }
62471
+ });
62472
+
62473
+ // ../engine/src/merger.ts
62474
+ import { execSync, exec as exec3 } from "node:child_process";
62475
+ import { promisify as promisify4 } from "node:util";
62476
+ import { existsSync as existsSync24 } from "node:fs";
62477
+ import { join as join30 } from "node:path";
62478
+ import { Type as Type3 } from "typebox";
62053
62479
  function truncateWorkflowScriptOutput(output) {
62054
62480
  if (output.length <= WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS) return output;
62055
62481
  return `... output truncated to last ${WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS} characters ...
@@ -62093,7 +62519,7 @@ async function getStagedFiles(cwd) {
62093
62519
  }
62094
62520
  }
62095
62521
  function hasInstallState(rootDir) {
62096
- return existsSync23(join29(rootDir, "node_modules")) || existsSync23(join29(rootDir, ".pnp.cjs"));
62522
+ return existsSync24(join30(rootDir, "node_modules")) || existsSync24(join30(rootDir, ".pnp.cjs"));
62097
62523
  }
62098
62524
  function shouldSyncDependenciesForMerge(stagedFiles, installStatePresent) {
62099
62525
  if (!installStatePresent) return true;
@@ -62102,10 +62528,10 @@ function shouldSyncDependenciesForMerge(stagedFiles, installStatePresent) {
62102
62528
  );
62103
62529
  }
62104
62530
  function getDependencySyncCommand(rootDir) {
62105
- if (existsSync23(join29(rootDir, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
62106
- if (existsSync23(join29(rootDir, "package-lock.json"))) return "npm install";
62107
- if (existsSync23(join29(rootDir, "yarn.lock"))) return "yarn install --frozen-lockfile";
62108
- if (existsSync23(join29(rootDir, "bun.lock")) || existsSync23(join29(rootDir, "bun.lockb"))) {
62531
+ if (existsSync24(join30(rootDir, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
62532
+ if (existsSync24(join30(rootDir, "package-lock.json"))) return "npm install";
62533
+ if (existsSync24(join30(rootDir, "yarn.lock"))) return "yarn install --frozen-lockfile";
62534
+ if (existsSync24(join30(rootDir, "bun.lock")) || existsSync24(join30(rootDir, "bun.lockb"))) {
62109
62535
  return "bun install --frozen-lockfile";
62110
62536
  }
62111
62537
  return null;
@@ -62138,8 +62564,8 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
62138
62564
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
62139
62565
  };
62140
62566
  }
62141
- if (existsSync23(join29(rootDir, "pnpm-lock.yaml"))) {
62142
- if (existsSync23(join29(rootDir, "pnpm-workspace.yaml"))) {
62567
+ if (existsSync24(join30(rootDir, "pnpm-lock.yaml"))) {
62568
+ if (existsSync24(join30(rootDir, "pnpm-workspace.yaml"))) {
62143
62569
  mergerLog.warn(
62144
62570
  `Inferred test command "pnpm test" in a pnpm workspace (${rootDir}). This runs the full monorepo suite on every merge. Consider setting an explicit scoped testCommand in project settings, e.g. \`pnpm -r --filter "...[main]" test\`.`
62145
62571
  );
@@ -62150,21 +62576,21 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
62150
62576
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
62151
62577
  };
62152
62578
  }
62153
- if (existsSync23(join29(rootDir, "yarn.lock"))) {
62579
+ if (existsSync24(join30(rootDir, "yarn.lock"))) {
62154
62580
  return {
62155
62581
  command: "yarn test",
62156
62582
  testSource: "inferred",
62157
62583
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
62158
62584
  };
62159
62585
  }
62160
- if (existsSync23(join29(rootDir, "bun.lock")) || existsSync23(join29(rootDir, "bun.lockb"))) {
62586
+ if (existsSync24(join30(rootDir, "bun.lock")) || existsSync24(join30(rootDir, "bun.lockb"))) {
62161
62587
  return {
62162
62588
  command: "bun test",
62163
62589
  testSource: "inferred",
62164
62590
  buildSource: explicitBuildCommand?.trim() ? "explicit" : void 0
62165
62591
  };
62166
62592
  }
62167
- if (existsSync23(join29(rootDir, "package-lock.json"))) {
62593
+ if (existsSync24(join30(rootDir, "package-lock.json"))) {
62168
62594
  return {
62169
62595
  command: "npm test",
62170
62596
  testSource: "inferred",
@@ -62201,7 +62627,7 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
62201
62627
  await store.logEntry(taskId, deterministicVerificationMessage);
62202
62628
  await store.appendAgentLog(taskId, deterministicVerificationMessage, "text", void 0, "merger");
62203
62629
  if (hasTestCommand) {
62204
- const testResult = await runVerificationCommand(
62630
+ const testResult = await runVerificationCommand2(
62205
62631
  store,
62206
62632
  rootDir,
62207
62633
  taskId,
@@ -62232,7 +62658,7 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
62232
62658
  }
62233
62659
  }
62234
62660
  if (hasBuildCommand) {
62235
- const buildResult = await runVerificationCommand(
62661
+ const buildResult = await runVerificationCommand2(
62236
62662
  store,
62237
62663
  rootDir,
62238
62664
  taskId,
@@ -62267,98 +62693,9 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
62267
62693
  await store.appendAgentLog(taskId, "Deterministic merge verification passed", "text", void 0, "merger");
62268
62694
  return result;
62269
62695
  }
62270
- async function runVerificationCommand(store, rootDir, taskId, command, type, signal) {
62696
+ async function runVerificationCommand2(store, rootDir, taskId, command, type, signal) {
62271
62697
  throwIfAborted(signal, taskId);
62272
- mergerLog.log(`${taskId}: running ${type} command: ${command}`);
62273
- await store.logEntry(taskId, `[verification] Running ${type} command: ${command}`);
62274
- await store.appendAgentLog(taskId, `Running ${type} command`, "tool", command, "merger");
62275
- const result = {
62276
- command,
62277
- exitCode: null,
62278
- stdout: "",
62279
- stderr: "",
62280
- success: false
62281
- };
62282
- const verificationStartedAt = Date.now();
62283
- try {
62284
- const { stdout, stderr, bufferOverflow } = await execWithProcessGroup(command, {
62285
- cwd: rootDir,
62286
- timeout: VERIFICATION_COMMAND_TIMEOUT_MS,
62287
- maxBuffer: VERIFICATION_COMMAND_MAX_BUFFER,
62288
- signal
62289
- });
62290
- throwIfAborted(signal, taskId);
62291
- result.stdout = stdout?.toString?.() || "";
62292
- result.stderr = stderr?.toString?.() || "";
62293
- result.exitCode = 0;
62294
- result.success = true;
62295
- const verificationDurationMs = Date.now() - verificationStartedAt;
62296
- const timingDetail = `${verificationDurationMs}ms`;
62297
- if (bufferOverflow) {
62298
- mergerLog.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
62299
- await store.logEntry(
62300
- taskId,
62301
- `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
62302
- );
62303
- await store.appendAgentLog(
62304
- taskId,
62305
- `${type} command succeeded (exit 0)`,
62306
- "tool_result",
62307
- timingDetail,
62308
- "merger"
62309
- );
62310
- } else {
62311
- mergerLog.log(`${taskId}: ${type} command succeeded in ${verificationDurationMs}ms`);
62312
- await store.logEntry(taskId, `[timing] [verification] ${type} command succeeded (exit 0) in ${verificationDurationMs}ms`);
62313
- await store.appendAgentLog(
62314
- taskId,
62315
- `${type} command succeeded (exit 0)`,
62316
- "tool_result",
62317
- timingDetail,
62318
- "merger"
62319
- );
62320
- }
62321
- return result;
62322
- } catch (error) {
62323
- throwIfAborted(signal, taskId);
62324
- const verificationDurationMs = Date.now() - verificationStartedAt;
62325
- result.stdout = error?.stdout?.toString?.() || "";
62326
- result.stderr = error?.stderr?.toString?.() || "";
62327
- result.exitCode = typeof error?.status === "number" ? error.status : typeof error?.code === "number" ? error.code : null;
62328
- const maxBufferExceeded = error?.code === "ENOBUFS" || error?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || String(error?.message ?? "").includes("maxBuffer");
62329
- result.success = maxBufferExceeded && result.exitCode === 0;
62330
- if (result.success) {
62331
- mergerLog.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
62332
- await store.logEntry(
62333
- taskId,
62334
- `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
62335
- );
62336
- await store.appendAgentLog(
62337
- taskId,
62338
- `${type} command succeeded (exit 0)`,
62339
- "tool_result",
62340
- `${verificationDurationMs}ms`,
62341
- "merger"
62342
- );
62343
- return result;
62344
- }
62345
- const output = result.stderr || result.stdout || error?.message || "Unknown error";
62346
- const summary = summarizeVerificationOutput(output, type);
62347
- mergerLog.error(`${taskId}: ${type} command failed (exit ${result.exitCode}) in ${verificationDurationMs}ms; output captured in task log`);
62348
- await store.logEntry(
62349
- taskId,
62350
- `[timing] [verification] ${type} command failed (exit ${result.exitCode}) after ${verificationDurationMs}ms:
62351
- ${summary}`
62352
- );
62353
- await store.appendAgentLog(
62354
- taskId,
62355
- `${type} command failed (exit ${result.exitCode})`,
62356
- "tool_error",
62357
- summary,
62358
- "merger"
62359
- );
62360
- }
62361
- return result;
62698
+ return runVerificationCommand(store, rootDir, taskId, command, type, signal, mergerLog, "merger");
62362
62699
  }
62363
62700
  async function attemptInMergeVerificationFix(store, rootDir, taskId, failureContext, settings, options, mergeRunContext, fixAttemptNumber, _testCommand, _buildCommand) {
62364
62701
  try {
@@ -62421,9 +62758,20 @@ Do not refactor, rename broadly, or make opportunistic improvements.
62421
62758
  onToolEnd: logger2.onToolEnd,
62422
62759
  defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
62423
62760
  defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
62761
+ fallbackProvider: settings.fallbackProvider,
62762
+ fallbackModelId: settings.fallbackModelId,
62424
62763
  defaultThinkingLevel: settings.defaultThinkingLevel,
62425
62764
  // Skill selection: use assigned agent skills if available, otherwise role fallback
62426
- ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
62765
+ ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
62766
+ taskId,
62767
+ taskTitle: taskForSkillContext?.title,
62768
+ onFallbackModelUsed: createFallbackModelObserver({
62769
+ agent: "merger",
62770
+ label: "merge verification fix agent",
62771
+ store,
62772
+ taskId,
62773
+ taskTitle: taskForSkillContext?.title
62774
+ })
62427
62775
  });
62428
62776
  const runId = mergeRunContext?.runId;
62429
62777
  const agentId = mergeRunContext?.agentId ?? "merger";
@@ -62476,7 +62824,7 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
62476
62824
  void 0,
62477
62825
  "merger"
62478
62826
  );
62479
- const reRunResult = await runVerificationCommand(
62827
+ const reRunResult = await runVerificationCommand2(
62480
62828
  store,
62481
62829
  rootDir,
62482
62830
  taskId,
@@ -63218,9 +63566,16 @@ You are assisting with a paused \`git pull --rebase\`.
63218
63566
  onToolEnd: agentLogger.onToolEnd,
63219
63567
  defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
63220
63568
  defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
63569
+ fallbackProvider: settings.fallbackProvider,
63570
+ fallbackModelId: settings.fallbackModelId,
63221
63571
  defaultThinkingLevel: settings.defaultThinkingLevel,
63222
63572
  taskId,
63223
- onFallbackModelUsed: notifyFallbackUsed
63573
+ onFallbackModelUsed: createFallbackModelObserver({
63574
+ agent: "merger",
63575
+ label: "rebase conflict resolver",
63576
+ store,
63577
+ taskId
63578
+ })
63224
63579
  });
63225
63580
  const prompt = [
63226
63581
  `Resolve rebase conflicts for task ${taskId}.`,
@@ -63412,7 +63767,7 @@ async function pushToRemoteAfterMerge(store, rootDir, taskId, settings, options)
63412
63767
  }
63413
63768
  async function createPostMergeWorktree(rootDir, taskId) {
63414
63769
  const randomSuffix = Math.random().toString(36).slice(2, 10);
63415
- const postMergeWorktree = join29(rootDir, ".worktrees", `post-merge-${taskId}-${randomSuffix}`);
63770
+ const postMergeWorktree = join30(rootDir, ".worktrees", `post-merge-${taskId}-${randomSuffix}`);
63416
63771
  try {
63417
63772
  await execAsync2(`git worktree add ${quoteArg(postMergeWorktree)} HEAD`, { cwd: rootDir });
63418
63773
  return postMergeWorktree;
@@ -64353,7 +64708,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
64353
64708
  }
64354
64709
  }
64355
64710
  throwIfAborted(options.signal, taskId);
64356
- if (worktreePath && existsSync23(worktreePath)) {
64711
+ if (worktreePath && existsSync24(worktreePath)) {
64357
64712
  const otherUser = await findWorktreeUser(store, worktreePath, taskId);
64358
64713
  if (otherUser) {
64359
64714
  mergerLog.log(`Worktree retained \u2014 still needed by ${otherUser}`);
@@ -65007,9 +65362,20 @@ async function runAiAgentForCommit(params) {
65007
65362
  onToolEnd: agentLogger.onToolEnd,
65008
65363
  defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
65009
65364
  defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
65365
+ fallbackProvider: settings.fallbackProvider,
65366
+ fallbackModelId: settings.fallbackModelId,
65010
65367
  defaultThinkingLevel: settings.defaultThinkingLevel,
65011
65368
  // Skill selection: use assigned agent skills if available, otherwise role fallback
65012
- ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
65369
+ ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
65370
+ taskId,
65371
+ taskTitle: taskForSkillContext?.title,
65372
+ onFallbackModelUsed: createFallbackModelObserver({
65373
+ agent: "merger",
65374
+ label: "merge agent",
65375
+ store,
65376
+ taskId,
65377
+ taskTitle: taskForSkillContext?.title
65378
+ })
65013
65379
  });
65014
65380
  options.onSession?.(session);
65015
65381
  try {
@@ -65440,7 +65806,14 @@ If issues are found that need attention, describe them clearly and include concr
65440
65806
  fallbackModelId: settings.fallbackModelId,
65441
65807
  defaultThinkingLevel: settings.defaultThinkingLevel,
65442
65808
  // Skill selection: use assigned agent skills if available, otherwise role fallback
65443
- ...postMergeSkillContext?.skillSelectionContext ? { skillSelection: postMergeSkillContext.skillSelectionContext } : {}
65809
+ ...postMergeSkillContext?.skillSelectionContext ? { skillSelection: postMergeSkillContext.skillSelectionContext } : {},
65810
+ taskId,
65811
+ onFallbackModelUsed: createFallbackModelObserver({
65812
+ agent: "merger",
65813
+ label: `post-merge workflow step '${workflowStep.name}'`,
65814
+ store,
65815
+ taskId
65816
+ })
65444
65817
  });
65445
65818
  mergerLog.log(`${taskId}: [post-merge] workflow step '${workflowStep.name}' using model ${describeModel(session)}${useOverride ? " (workflow step override)" : ""}`);
65446
65819
  await store.logEntry(taskId, `[post-merge] Workflow step '${workflowStep.name}' using model: ${describeModel(session)}${useOverride ? " (workflow step override)" : ""}`);
@@ -65476,15 +65849,17 @@ async function completeTask(store, taskId, result) {
65476
65849
  result.task = task;
65477
65850
  store.emit("task:merged", result);
65478
65851
  }
65479
- var execAsync2, LOCKFILE_PATTERNS, GENERATED_PATTERNS, DEPENDENCY_SYNC_TRIGGER_PATTERNS, VERIFICATION_COMMAND_MAX_BUFFER, VERIFICATION_COMMAND_TIMEOUT_MS, VERIFICATION_LOG_MAX_CHARS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS, PULL_REBASE_TIMEOUT_MS, PUSH_TIMEOUT_MS, MERGE_COMMIT_LOG_MAX_CHARS, MERGE_DIFF_STAT_MAX_CHARS, VerificationError, MergeAbortedError, FUSION_TASK_ID_TRAILER_KEY;
65852
+ var execAsync2, LOCKFILE_PATTERNS, GENERATED_PATTERNS, DEPENDENCY_SYNC_TRIGGER_PATTERNS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS, PULL_REBASE_TIMEOUT_MS, PUSH_TIMEOUT_MS, MERGE_COMMIT_LOG_MAX_CHARS, MERGE_DIFF_STAT_MAX_CHARS, VerificationError, MergeAbortedError, FUSION_TASK_ID_TRAILER_KEY;
65480
65853
  var init_merger = __esm({
65481
65854
  "../engine/src/merger.ts"() {
65482
65855
  "use strict";
65856
+ init_verification_utils();
65857
+ init_verification_utils();
65483
65858
  init_src();
65484
65859
  init_pi();
65485
65860
  init_session_token_usage();
65486
65861
  init_agent_session_helpers();
65487
- init_notifier();
65862
+ init_fallback_model_observer();
65488
65863
  init_session_skill_context();
65489
65864
  init_agent_logger();
65490
65865
  init_logger2();
@@ -65530,9 +65905,6 @@ var init_merger = __esm({
65530
65905
  "bun.lock",
65531
65906
  "packages/*/package.json"
65532
65907
  ];
65533
- VERIFICATION_COMMAND_MAX_BUFFER = 50 * 1024 * 1024;
65534
- VERIFICATION_COMMAND_TIMEOUT_MS = 6e5;
65535
- VERIFICATION_LOG_MAX_CHARS = 2e4;
65536
65908
  WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS = 4e3;
65537
65909
  PULL_REBASE_TIMEOUT_MS = 12e4;
65538
65910
  PUSH_TIMEOUT_MS = 6e4;
@@ -65557,8 +65929,8 @@ var init_merger = __esm({
65557
65929
 
65558
65930
  // ../engine/src/worktree-names.ts
65559
65931
  import { readdirSync as readdirSync3 } from "node:fs";
65560
- import { join as join30 } from "node:path";
65561
- import { existsSync as existsSync24 } from "node:fs";
65932
+ import { join as join31 } from "node:path";
65933
+ import { existsSync as existsSync25 } from "node:fs";
65562
65934
  function slugify2(str) {
65563
65935
  return str.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
65564
65936
  }
@@ -65569,7 +65941,7 @@ function generateReservedWorktreeName(rootDir, reservedNames = /* @__PURE__ */ n
65569
65941
  const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
65570
65942
  const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
65571
65943
  const baseName = `${adjective}-${noun}`;
65572
- const worktreesDir = join30(rootDir, ".worktrees");
65944
+ const worktreesDir = join31(rootDir, ".worktrees");
65573
65945
  const existing = getExistingWorktreeNames(worktreesDir);
65574
65946
  for (const reserved of reservedNames) {
65575
65947
  existing.add(reserved);
@@ -65584,7 +65956,7 @@ function generateReservedWorktreeName(rootDir, reservedNames = /* @__PURE__ */ n
65584
65956
  return `${baseName}-${suffix}`;
65585
65957
  }
65586
65958
  function getExistingWorktreeNames(worktreesDir) {
65587
- if (!existsSync24(worktreesDir)) {
65959
+ if (!existsSync25(worktreesDir)) {
65588
65960
  return /* @__PURE__ */ new Set();
65589
65961
  }
65590
65962
  try {
@@ -65721,8 +66093,8 @@ __export(worktree_pool_exports, {
65721
66093
  });
65722
66094
  import { exec as exec4 } from "node:child_process";
65723
66095
  import { promisify as promisify5 } from "node:util";
65724
- import { existsSync as existsSync25, lstatSync, readdirSync as readdirSync4, rmSync as rmSync2 } from "node:fs";
65725
- import { join as join31, relative as relative6, resolve as resolve15, isAbsolute as isAbsolute9 } from "node:path";
66096
+ import { existsSync as existsSync26, lstatSync, readdirSync as readdirSync4, rmSync as rmSync2 } from "node:fs";
66097
+ import { join as join32, relative as relative6, resolve as resolve15, isAbsolute as isAbsolute9 } from "node:path";
65726
66098
  function getExecStdout(result) {
65727
66099
  if (typeof result === "string") return result;
65728
66100
  if (result && typeof result === "object" && "stdout" in result) {
@@ -65768,10 +66140,10 @@ async function isRegisteredGitWorktree2(rootDir, worktreePath) {
65768
66140
  return (await getRegisteredWorktreePaths(rootDir)).has(resolve15(worktreePath));
65769
66141
  }
65770
66142
  function hasRequiredWorktreeFiles(worktreePath) {
65771
- return existsSync25(join31(worktreePath, ".git")) && existsSync25(join31(worktreePath, "package.json"));
66143
+ return existsSync26(join32(worktreePath, ".git")) && existsSync26(join32(worktreePath, "package.json"));
65772
66144
  }
65773
66145
  async function isUsableTaskWorktree(rootDir, worktreePath) {
65774
- return existsSync25(worktreePath) && await isRegisteredGitWorktree2(rootDir, worktreePath) && hasRequiredWorktreeFiles(worktreePath);
66146
+ return existsSync26(worktreePath) && await isRegisteredGitWorktree2(rootDir, worktreePath) && hasRequiredWorktreeFiles(worktreePath);
65775
66147
  }
65776
66148
  function isInsideWorktreesDir(rootDir, worktreePath) {
65777
66149
  const worktreesDir = resolve15(rootDir, ".worktrees");
@@ -65780,14 +66152,14 @@ function isInsideWorktreesDir(rootDir, worktreePath) {
65780
66152
  return rel !== "" && !rel.startsWith("..") && !isAbsolute9(rel);
65781
66153
  }
65782
66154
  async function scanIdleWorktrees(rootDir, store) {
65783
- const worktreesDir = join31(rootDir, ".worktrees");
65784
- if (!existsSync25(worktreesDir)) {
66155
+ const worktreesDir = join32(rootDir, ".worktrees");
66156
+ if (!existsSync26(worktreesDir)) {
65785
66157
  return [];
65786
66158
  }
65787
66159
  let dirs;
65788
66160
  try {
65789
66161
  const entries = readdirSync4(worktreesDir, { withFileTypes: true });
65790
- dirs = entries.filter((e) => e.isDirectory()).map((e) => join31(worktreesDir, e.name));
66162
+ dirs = entries.filter((e) => e.isDirectory()).map((e) => join32(worktreesDir, e.name));
65791
66163
  } catch (err) {
65792
66164
  const errorMessage = err instanceof Error ? err.message : String(err);
65793
66165
  worktreePoolLog.warn(`Failed to read .worktrees/ directory: ${errorMessage}`);
@@ -65810,16 +66182,16 @@ async function scanIdleWorktrees(rootDir, store) {
65810
66182
  return registeredDirs.filter((dir) => !activeWorktrees.has(resolve15(dir)));
65811
66183
  }
65812
66184
  async function cleanupOrphanedWorktrees(rootDir, store) {
65813
- const worktreesDir = join31(rootDir, ".worktrees");
65814
- if (!existsSync25(worktreesDir)) {
66185
+ const worktreesDir = join32(rootDir, ".worktrees");
66186
+ if (!existsSync26(worktreesDir)) {
65815
66187
  return 0;
65816
66188
  }
65817
66189
  const orphaned = await scanIdleWorktrees(rootDir, store);
65818
66190
  const registeredWorktrees = await getRegisteredWorktreePaths(rootDir);
65819
66191
  let dirs = [];
65820
- if (existsSync25(worktreesDir)) {
66192
+ if (existsSync26(worktreesDir)) {
65821
66193
  try {
65822
- dirs = readdirSync4(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join31(worktreesDir, e.name));
66194
+ dirs = readdirSync4(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join32(worktreesDir, e.name));
65823
66195
  } catch (err) {
65824
66196
  const errorMessage = err instanceof Error ? err.message : String(err);
65825
66197
  worktreePoolLog.warn(`Failed to read .worktrees/ directory for cleanup: ${errorMessage}`);
@@ -65851,8 +66223,8 @@ async function cleanupOrphanedWorktrees(rootDir, store) {
65851
66223
  return cleaned;
65852
66224
  }
65853
66225
  async function reapOrphanWorktrees(projectRoot) {
65854
- const worktreesDir = join31(projectRoot, ".worktrees");
65855
- if (!existsSync25(worktreesDir)) {
66226
+ const worktreesDir = join32(projectRoot, ".worktrees");
66227
+ if (!existsSync26(worktreesDir)) {
65856
66228
  return 0;
65857
66229
  }
65858
66230
  let entries;
@@ -65860,11 +66232,11 @@ async function reapOrphanWorktrees(projectRoot) {
65860
66232
  entries = readdirSync4(worktreesDir, { withFileTypes: true }).filter((e) => {
65861
66233
  if (!e.isDirectory()) return false;
65862
66234
  try {
65863
- return lstatSync(join31(worktreesDir, e.name)).isDirectory() && !lstatSync(join31(worktreesDir, e.name)).isSymbolicLink();
66235
+ return lstatSync(join32(worktreesDir, e.name)).isDirectory() && !lstatSync(join32(worktreesDir, e.name)).isSymbolicLink();
65864
66236
  } catch {
65865
66237
  return false;
65866
66238
  }
65867
- }).map((e) => ({ name: e.name, fullPath: join31(worktreesDir, e.name) }));
66239
+ }).map((e) => ({ name: e.name, fullPath: join32(worktreesDir, e.name) }));
65868
66240
  } catch (err) {
65869
66241
  const msg = err instanceof Error ? err.message : String(err);
65870
66242
  worktreePoolLog.warn(`reapOrphanWorktrees: failed to read .worktrees/ \u2014 ${msg}`);
@@ -65883,8 +66255,8 @@ async function reapOrphanWorktrees(projectRoot) {
65883
66255
  if (registered.has(resolvedFull)) {
65884
66256
  continue;
65885
66257
  }
65886
- const dotGit = join31(resolvedFull, ".git");
65887
- if (existsSync25(dotGit)) {
66258
+ const dotGit = join32(resolvedFull, ".git");
66259
+ if (existsSync26(dotGit)) {
65888
66260
  worktreePoolLog.log(`reapOrphanWorktrees: skipping ${name} (has .git entry but not in registered list \u2014 may be partially registered)`);
65889
66261
  continue;
65890
66262
  }
@@ -65944,7 +66316,7 @@ var init_worktree_pool = __esm({
65944
66316
  acquire() {
65945
66317
  for (const path2 of this.idle) {
65946
66318
  this.idle.delete(path2);
65947
- if (existsSync25(path2)) {
66319
+ if (existsSync26(path2)) {
65948
66320
  return path2;
65949
66321
  }
65950
66322
  worktreePoolLog.log(`Pruned stale entry: ${path2}`);
@@ -65991,7 +66363,7 @@ var init_worktree_pool = __esm({
65991
66363
  */
65992
66364
  rehydrate(idlePaths) {
65993
66365
  for (const path2 of idlePaths) {
65994
- if (existsSync25(path2)) {
66366
+ if (existsSync26(path2)) {
65995
66367
  this.idle.add(path2);
65996
66368
  } else {
65997
66369
  worktreePoolLog.log(`Rehydrate skipped (not on disk): ${path2}`);
@@ -66047,7 +66419,7 @@ var init_worktree_pool = __esm({
66047
66419
  throw err;
66048
66420
  }
66049
66421
  const conflictingPath = match[1];
66050
- if (!existsSync25(conflictingPath)) {
66422
+ if (!existsSync26(conflictingPath)) {
66051
66423
  await execAsync3("git worktree prune", { cwd: worktreePath });
66052
66424
  await execAsync3(checkoutCmd, { cwd: worktreePath });
66053
66425
  return branchName;
@@ -66124,8 +66496,8 @@ var init_token_cap_detector = __esm({
66124
66496
  // ../engine/src/step-session-executor.ts
66125
66497
  import { exec as exec5 } from "node:child_process";
66126
66498
  import { promisify as promisify6 } from "node:util";
66127
- import { existsSync as existsSync26 } from "node:fs";
66128
- import { join as join32 } from "node:path";
66499
+ import { existsSync as existsSync27 } from "node:fs";
66500
+ import { join as join33 } from "node:path";
66129
66501
  function parseStepFileScopes(prompt) {
66130
66502
  const result = /* @__PURE__ */ new Map();
66131
66503
  if (!prompt) return result;
@@ -66412,7 +66784,7 @@ var init_step_session_executor = __esm({
66412
66784
  init_worktree_names();
66413
66785
  init_agent_logger();
66414
66786
  init_logger2();
66415
- init_notifier();
66787
+ init_fallback_model_observer();
66416
66788
  init_context_limit_detector();
66417
66789
  init_usage_limit_detector();
66418
66790
  init_agent_tools();
@@ -66529,7 +66901,7 @@ var init_step_session_executor = __esm({
66529
66901
  }
66530
66902
  for (const [stepIdx, worktreePath] of this.parallelWorktrees) {
66531
66903
  try {
66532
- if (existsSync26(worktreePath)) {
66904
+ if (existsSync27(worktreePath)) {
66533
66905
  await execAsync4(`git worktree remove "${worktreePath}" --force`, {
66534
66906
  cwd: this.options.rootDir
66535
66907
  });
@@ -66694,7 +67066,13 @@ Follow instructions precisely and avoid unrelated changes.`,
66694
67066
  ...this.options.skillSelection ? { skillSelection: this.options.skillSelection } : {},
66695
67067
  taskId: taskDetail.id,
66696
67068
  taskTitle: taskDetail.title,
66697
- onFallbackModelUsed: notifyFallbackUsed
67069
+ onFallbackModelUsed: createFallbackModelObserver({
67070
+ agent: "executor",
67071
+ label: "workflow step agent",
67072
+ store: this.store,
67073
+ taskId: taskDetail.id,
67074
+ taskTitle: taskDetail.title
67075
+ })
66698
67076
  });
66699
67077
  session = createResult.session;
66700
67078
  const handle = {
@@ -66874,7 +67252,7 @@ Follow instructions precisely and avoid unrelated changes.`,
66874
67252
  for (const [stepIdx, worktreePath] of worktreePaths) {
66875
67253
  if (worktreePath !== this.options.worktreePath) {
66876
67254
  try {
66877
- if (existsSync26(worktreePath)) {
67255
+ if (existsSync27(worktreePath)) {
66878
67256
  await execAsync4(`git worktree remove "${worktreePath}" --force`, {
66879
67257
  cwd: this.options.rootDir
66880
67258
  });
@@ -66904,7 +67282,7 @@ Follow instructions precisely and avoid unrelated changes.`,
66904
67282
  async createStepWorktree(stepIndex) {
66905
67283
  const { rootDir } = this.options;
66906
67284
  const name = generateWorktreeName(rootDir);
66907
- const worktreePath = join32(rootDir, ".worktrees", name);
67285
+ const worktreePath = join33(rootDir, ".worktrees", name);
66908
67286
  const branchName = `fusion/step-${stepIndex}-${name}`;
66909
67287
  stepExecLog.log(`Creating worktree for step ${stepIndex}: ${worktreePath} (branch: ${branchName})`);
66910
67288
  try {
@@ -66979,7 +67357,7 @@ Follow instructions precisely and avoid unrelated changes.`,
66979
67357
 
66980
67358
  // ../engine/src/spec-staleness.ts
66981
67359
  import { stat as stat5 } from "node:fs/promises";
66982
- import { join as join33 } from "node:path";
67360
+ import { join as join34 } from "node:path";
66983
67361
  async function evaluateSpecStaleness(options) {
66984
67362
  const { settings, promptPath, nowMs } = options;
66985
67363
  if (settings.specStalenessEnabled !== true) {
@@ -67019,7 +67397,7 @@ async function evaluateSpecStaleness(options) {
67019
67397
  };
67020
67398
  }
67021
67399
  function getPromptPath(tasksDir, taskId) {
67022
- return join33(tasksDir, taskId, "PROMPT.md");
67400
+ return join34(tasksDir, taskId, "PROMPT.md");
67023
67401
  }
67024
67402
  var DEFAULT_SPEC_STALENESS_MAX_AGE_MS;
67025
67403
  var init_spec_staleness = __esm({
@@ -67050,8 +67428,8 @@ var init_task_completion = __esm({
67050
67428
 
67051
67429
  // ../engine/src/run-verification-tool.ts
67052
67430
  import { spawn as spawn4 } from "node:child_process";
67053
- import { existsSync as existsSync27 } from "node:fs";
67054
- import { isAbsolute as isAbsolute10, join as join34 } from "node:path";
67431
+ import { existsSync as existsSync28 } from "node:fs";
67432
+ import { isAbsolute as isAbsolute10, join as join35 } from "node:path";
67055
67433
  import { Type as Type4 } from "@mariozechner/pi-ai";
67056
67434
  function createBuffer() {
67057
67435
  return { headChunks: [], headBytes: 0, tailChunks: [], tailBytes: 0, totalBytes: 0 };
@@ -67084,7 +67462,7 @@ function flattenBuffer(buf) {
67084
67462
 
67085
67463
  ` + tail;
67086
67464
  }
67087
- async function runVerificationCommand2(opts) {
67465
+ async function runVerificationCommand3(opts) {
67088
67466
  const { command, cwd, timeoutMs, expectFailure = false, onHeartbeat, onLine } = opts;
67089
67467
  const startMs = Date.now();
67090
67468
  const warnings = [];
@@ -67224,7 +67602,7 @@ function createRunVerificationTool(opts) {
67224
67602
  if (params.cwd && isAbsolute10(params.cwd)) {
67225
67603
  resolvedCwd = params.cwd;
67226
67604
  } else if (params.cwd) {
67227
- resolvedCwd = join34(worktreePath, params.cwd);
67605
+ resolvedCwd = join35(worktreePath, params.cwd);
67228
67606
  } else {
67229
67607
  resolvedCwd = worktreePath;
67230
67608
  }
@@ -67239,8 +67617,8 @@ function createRunVerificationTool(opts) {
67239
67617
  }
67240
67618
  let effectiveCommand = command;
67241
67619
  if (command.trimStart().startsWith("pnpm --filter")) {
67242
- const modulesYaml = join34(rootDir, "node_modules", ".modules.yaml");
67243
- if (!existsSync27(modulesYaml)) {
67620
+ const modulesYaml = join35(rootDir, "node_modules", ".modules.yaml");
67621
+ if (!existsSync28(modulesYaml)) {
67244
67622
  const installCmd = "pnpm install --prefer-offline";
67245
67623
  const msg = `node_modules/.modules.yaml not found in workspace root \u2014 auto-prepending \`${installCmd}\` before running the command.`;
67246
67624
  warnings.push(msg);
@@ -67251,7 +67629,7 @@ function createRunVerificationTool(opts) {
67251
67629
  log18.info(
67252
67630
  `[fn_run_verification] ${taskId}: scope=${scope} timeout=${timeoutSec}s cwd=${resolvedCwd} cmd=${effectiveCommand}`
67253
67631
  );
67254
- const result = await runVerificationCommand2({
67632
+ const result = await runVerificationCommand3({
67255
67633
  command: effectiveCommand,
67256
67634
  cwd: resolvedCwd,
67257
67635
  timeoutMs,
@@ -67351,8 +67729,8 @@ var init_run_verification_tool = __esm({
67351
67729
  // ../engine/src/executor.ts
67352
67730
  import { exec as exec6 } from "node:child_process";
67353
67731
  import { promisify as promisify7 } from "node:util";
67354
- import { isAbsolute as isAbsolute11, join as join35, relative as relative7, resolve as resolvePath } from "node:path";
67355
- import { existsSync as existsSync28 } from "node:fs";
67732
+ import { isAbsolute as isAbsolute11, join as join36, relative as relative7, resolve as resolvePath } from "node:path";
67733
+ import { existsSync as existsSync29 } from "node:fs";
67356
67734
  import { readFile as readFile15, writeFile as writeFile12 } from "node:fs/promises";
67357
67735
  import { Type as Type5 } from "@mariozechner/pi-ai";
67358
67736
  import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
@@ -67646,6 +68024,7 @@ var init_executor = __esm({
67646
68024
  "use strict";
67647
68025
  init_src();
67648
68026
  init_merger();
68027
+ init_verification_utils();
67649
68028
  init_worktree_names();
67650
68029
  init_pi();
67651
68030
  init_session_token_usage();
@@ -67670,7 +68049,7 @@ var init_executor = __esm({
67670
68049
  init_task_completion();
67671
68050
  init_auth_storage();
67672
68051
  init_run_verification_tool();
67673
- init_notifier();
68052
+ init_fallback_model_observer();
67674
68053
  init_agent_logger();
67675
68054
  init_agent_tools();
67676
68055
  execAsync5 = promisify7(exec6);
@@ -68817,7 +69196,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
68817
69196
  );
68818
69197
  return false;
68819
69198
  }
68820
- if (task.worktree && existsSync28(task.worktree)) {
69199
+ if (task.worktree && existsSync29(task.worktree)) {
68821
69200
  const modifiedFiles = await this.captureModifiedFiles(task.worktree, task.baseCommitSha);
68822
69201
  if (modifiedFiles.length > 0) {
68823
69202
  await this.store.updateTask(task.id, { modifiedFiles });
@@ -68997,7 +69376,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
68997
69376
  if (task.dependencies.length === 0) return null;
68998
69377
  for (const depId of task.dependencies) {
68999
69378
  const dep = allTasks.find((t) => t.id === depId);
69000
- if (dep && dep.worktree && (dep.column === "done" || dep.column === "in-review") && existsSync28(dep.worktree)) {
69379
+ if (dep && dep.worktree && (dep.column === "done" || dep.column === "in-review") && existsSync29(dep.worktree)) {
69001
69380
  return dep.worktree;
69002
69381
  }
69003
69382
  }
@@ -69048,7 +69427,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69048
69427
  const activeMergeStatuses = /* @__PURE__ */ new Set(["merging", "merging-pr"]);
69049
69428
  const isActiveTask = activeColumns.has(task.column) || activeMergeStatuses.has(task.status ?? "");
69050
69429
  if (!isActiveTask) {
69051
- const tasksDir = join35(this.store.getFusionDir(), "tasks");
69430
+ const tasksDir = join36(this.store.getFusionDir(), "tasks");
69052
69431
  const promptPath = getPromptPath(tasksDir, task.id);
69053
69432
  const staleness = await evaluateSpecStaleness({ settings, promptPath });
69054
69433
  if (staleness.isStale) {
@@ -69095,7 +69474,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69095
69474
  worktreeName = generateWorktreeName(this.rootDir);
69096
69475
  break;
69097
69476
  }
69098
- worktreePath = join35(this.rootDir, ".worktrees", worktreeName);
69477
+ worktreePath = join36(this.rootDir, ".worktrees", worktreeName);
69099
69478
  }
69100
69479
  let stuckRequeue = null;
69101
69480
  let taskDone = false;
@@ -69119,7 +69498,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69119
69498
  );
69120
69499
  }
69121
69500
  const branchName = task.branch || `fusion/${task.id.toLowerCase()}`;
69122
- let isResume = existsSync28(worktreePath);
69501
+ let isResume = existsSync29(worktreePath);
69123
69502
  let acquiredFromPool = false;
69124
69503
  const baseBranch = task.baseBranch || null;
69125
69504
  if (task.worktree && isResume && !await isUsableTaskWorktree(this.rootDir, worktreePath)) {
@@ -69132,8 +69511,8 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69132
69511
  this.currentRunContext
69133
69512
  );
69134
69513
  await this.store.updateTask(task.id, { worktree: null, branch: null });
69135
- worktreePath = join35(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
69136
- isResume = existsSync28(worktreePath);
69514
+ worktreePath = join36(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
69515
+ isResume = existsSync29(worktreePath);
69137
69516
  }
69138
69517
  if (!isResume) {
69139
69518
  if (this.options.pool && settings.recycleWorktrees) {
@@ -69359,6 +69738,84 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69359
69738
  if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps after step-session completion")) {
69360
69739
  return;
69361
69740
  }
69741
+ if (executionMode !== "fast") {
69742
+ if (settings.testCommand?.trim() || settings.buildCommand?.trim()) {
69743
+ const verificationResult = await this.runExecutorDeterministicVerification(task, worktreePath, settings);
69744
+ if (!verificationResult.allPassed) {
69745
+ const failedType = verificationResult.failedCommand === "testCommand" ? "test" : "build";
69746
+ const failedResult = failedType === "test" ? verificationResult.testResult : verificationResult.buildResult;
69747
+ const failedCommand = failedResult.command;
69748
+ const failureOutput = failedResult.stderr || failedResult.stdout || "Unknown error";
69749
+ const summary = summarizeVerificationOutput(failureOutput, failedType);
69750
+ executorLog.log(`${task.id}: [verification] ${failedType} failed \u2014 attempting fix agent`);
69751
+ await this.store.logEntry(
69752
+ task.id,
69753
+ `[verification] ${failedType} command failed (exit ${failedResult.exitCode}). Attempting fix agent...`,
69754
+ summary,
69755
+ this.currentRunContext
69756
+ );
69757
+ const maxFixRetries = Math.min(settings.verificationFixRetries ?? 3, 3);
69758
+ if (maxFixRetries === 0) {
69759
+ executorLog.log(`${task.id}: [verification] fix retries set to 0 \u2014 sending task back immediately`);
69760
+ await this.sendTaskBackForFix(
69761
+ task,
69762
+ worktreePath,
69763
+ `${failedType} command \`${failedCommand}\` failed (exit ${failedResult.exitCode}):
69764
+ ${summary}`,
69765
+ `Verification (${failedType})`,
69766
+ `Deterministic verification failed (${failedType})`
69767
+ );
69768
+ return;
69769
+ }
69770
+ let fixSucceeded = false;
69771
+ for (let attempt = 1; attempt <= maxFixRetries; attempt++) {
69772
+ const fixed = await this.attemptExecutorVerificationFix(
69773
+ task,
69774
+ worktreePath,
69775
+ {
69776
+ command: failedCommand,
69777
+ exitCode: failedResult.exitCode,
69778
+ output: failureOutput,
69779
+ type: failedType
69780
+ },
69781
+ settings,
69782
+ attempt,
69783
+ maxFixRetries
69784
+ );
69785
+ if (fixed) {
69786
+ fixSucceeded = true;
69787
+ executorLog.log(`${task.id}: [verification] fix agent succeeded on attempt ${attempt}/${maxFixRetries}`);
69788
+ await this.store.logEntry(
69789
+ task.id,
69790
+ `[verification] Fix agent succeeded on attempt ${attempt}/${maxFixRetries}. Verification now passing.`,
69791
+ void 0,
69792
+ this.currentRunContext
69793
+ );
69794
+ break;
69795
+ }
69796
+ executorLog.log(`${task.id}: [verification] fix agent attempt ${attempt}/${maxFixRetries} failed`);
69797
+ await this.store.logEntry(
69798
+ task.id,
69799
+ `[verification] Fix agent attempt ${attempt}/${maxFixRetries} failed`,
69800
+ void 0,
69801
+ this.currentRunContext
69802
+ );
69803
+ }
69804
+ if (!fixSucceeded) {
69805
+ executorLog.log(`${task.id}: [verification] all fix attempts exhausted (${maxFixRetries}/${maxFixRetries}) \u2014 sending task back`);
69806
+ await this.sendTaskBackForFix(
69807
+ task,
69808
+ worktreePath,
69809
+ `${failedType} command \`${failedCommand}\` failed (exit ${failedResult.exitCode}) after ${maxFixRetries} fix attempts:
69810
+ ${summary}`,
69811
+ `Verification (${failedType})`,
69812
+ `Deterministic verification failed after ${maxFixRetries} fix attempts`
69813
+ );
69814
+ return;
69815
+ }
69816
+ }
69817
+ }
69818
+ }
69362
69819
  if (executionMode !== "fast") {
69363
69820
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
69364
69821
  if (workflowResult === "deferred-paused") {
@@ -69447,7 +69904,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69447
69904
  executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}: ${errorMessage}`);
69448
69905
  await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}): ${errorMessage}`, void 0, this.currentRunContext);
69449
69906
  }
69450
- if (worktreePath && existsSync28(worktreePath)) {
69907
+ if (worktreePath && existsSync29(worktreePath)) {
69451
69908
  try {
69452
69909
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
69453
69910
  await audit.git({ type: "worktree:remove", target: worktreePath });
@@ -69506,7 +69963,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69506
69963
  try {
69507
69964
  const latestTask = await this.store.getTask(task.id);
69508
69965
  await this.resetStepsIfWorkLost(latestTask);
69509
- if (worktreePath && existsSync28(worktreePath)) {
69966
+ if (worktreePath && existsSync29(worktreePath)) {
69510
69967
  try {
69511
69968
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
69512
69969
  } catch (wtErr) {
@@ -69618,7 +70075,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69618
70075
  const executorFallbackProvider = settings.fallbackProvider;
69619
70076
  const executorFallbackModelId = settings.fallbackModelId;
69620
70077
  const executorThinkingLevel = detail.thinkingLevel ?? settings.defaultThinkingLevel;
69621
- const isResuming = !!task.sessionFile && existsSync28(task.sessionFile);
70078
+ const isResuming = !!task.sessionFile && existsSync29(task.sessionFile);
69622
70079
  const sessionManager = isResuming ? SessionManager2.open(task.sessionFile) : SessionManager2.create(worktreePath);
69623
70080
  executorLog.log(`${task.id}: creating agent session (provider=${executorProvider ?? "default"}, model=${executorModelId ?? "default"}, resuming=${isResuming})`);
69624
70081
  const executorInstructions = await this.resolveInstructionsForRole("executor");
@@ -69648,7 +70105,13 @@ The tool prevents your session from being killed by the inactivity watchdog duri
69648
70105
  ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
69649
70106
  taskId: task.id,
69650
70107
  taskTitle: detail.title,
69651
- onFallbackModelUsed: notifyFallbackUsed
70108
+ onFallbackModelUsed: createFallbackModelObserver({
70109
+ agent: "executor",
70110
+ label: "executor",
70111
+ store: this.store,
70112
+ taskId: task.id,
70113
+ taskTitle: detail.title
70114
+ })
69652
70115
  });
69653
70116
  if (isResuming) {
69654
70117
  executorLog.log(`${task.id}: resumed session from ${task.sessionFile}`);
@@ -70082,7 +70545,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70082
70545
  this.options.onComplete?.(task);
70083
70546
  } else {
70084
70547
  executorLog.log(`${task.id} paused \u2014 moving to todo`);
70085
- if (worktreePath && existsSync28(worktreePath)) {
70548
+ if (worktreePath && existsSync29(worktreePath)) {
70086
70549
  try {
70087
70550
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
70088
70551
  executorLog.log(`Removed old worktree for paused task: ${worktreePath}`);
@@ -70178,7 +70641,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70178
70641
  executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}: ${errorMessage}`);
70179
70642
  await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay3}): ${errorMessage}`, void 0, this.currentRunContext);
70180
70643
  }
70181
- if (worktreePath && existsSync28(worktreePath)) {
70644
+ if (worktreePath && existsSync29(worktreePath)) {
70182
70645
  try {
70183
70646
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
70184
70647
  executorLog.log(`Removed old worktree for transient retry: ${worktreePath}`);
@@ -70233,7 +70696,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
70233
70696
  try {
70234
70697
  const latestTask = await this.store.getTask(task.id);
70235
70698
  await this.resetStepsIfWorkLost(latestTask);
70236
- if (worktreePath && existsSync28(worktreePath)) {
70699
+ if (worktreePath && existsSync29(worktreePath)) {
70237
70700
  try {
70238
70701
  await execAsync5(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir });
70239
70702
  executorLog.log(`Removed old worktree for stuck-killed retry: ${worktreePath}`);
@@ -70738,7 +71201,7 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
70738
71201
  * The section is replaced entirely to avoid accumulation of old feedback.
70739
71202
  */
70740
71203
  async injectWorkflowRevisionInstructions(task, feedback) {
70741
- const promptPath = join35(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
71204
+ const promptPath = join36(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
70742
71205
  let content;
70743
71206
  try {
70744
71207
  content = await readFile15(promptPath, "utf-8");
@@ -70792,6 +71255,217 @@ ${feedback}
70792
71255
  *
70793
71256
  * @returns true if a retry was scheduled, false if retries are exhausted
70794
71257
  */
71258
+ /**
71259
+ * Run deterministic verification (test + build commands) in the task's worktree.
71260
+ * Returns a structured result indicating whether all commands passed.
71261
+ */
71262
+ async runExecutorDeterministicVerification(task, worktreePath, settings) {
71263
+ const testCommand = settings.testCommand?.trim();
71264
+ const buildCommand2 = settings.buildCommand?.trim();
71265
+ if (!testCommand && !buildCommand2) {
71266
+ executorLog.log(`${task.id}: no test/build commands configured \u2014 skipping verification`);
71267
+ return { allPassed: true };
71268
+ }
71269
+ const parts = [];
71270
+ if (testCommand) parts.push(`test: ${testCommand}`);
71271
+ if (buildCommand2) parts.push(`build: ${buildCommand2}`);
71272
+ executorLog.log(`${task.id}: [verification] running deterministic verification (${parts.join(", ")})`);
71273
+ await this.store.logEntry(
71274
+ task.id,
71275
+ `[verification] Running deterministic verification (${parts.join(", ")})`,
71276
+ void 0,
71277
+ this.currentRunContext
71278
+ );
71279
+ const result = { allPassed: true };
71280
+ if (testCommand) {
71281
+ const testResult = await runVerificationCommand(
71282
+ this.store,
71283
+ worktreePath,
71284
+ task.id,
71285
+ testCommand,
71286
+ "test",
71287
+ void 0,
71288
+ executorLog,
71289
+ "executor"
71290
+ );
71291
+ result.testResult = testResult;
71292
+ if (!testResult.success) {
71293
+ result.allPassed = false;
71294
+ result.failedCommand = "testCommand";
71295
+ executorLog.log(`${task.id}: [verification] test failed (exit ${testResult.exitCode})`);
71296
+ return result;
71297
+ }
71298
+ }
71299
+ if (buildCommand2) {
71300
+ const buildResult = await runVerificationCommand(
71301
+ this.store,
71302
+ worktreePath,
71303
+ task.id,
71304
+ buildCommand2,
71305
+ "build",
71306
+ void 0,
71307
+ executorLog,
71308
+ "executor"
71309
+ );
71310
+ result.buildResult = buildResult;
71311
+ if (!buildResult.success) {
71312
+ result.allPassed = false;
71313
+ result.failedCommand = "buildCommand";
71314
+ executorLog.log(`${task.id}: [verification] build failed (exit ${buildResult.exitCode})`);
71315
+ return result;
71316
+ }
71317
+ }
71318
+ executorLog.log(`${task.id}: [verification] passed`);
71319
+ await this.store.logEntry(
71320
+ task.id,
71321
+ `[verification] Deterministic verification passed`,
71322
+ void 0,
71323
+ this.currentRunContext
71324
+ );
71325
+ return result;
71326
+ }
71327
+ /**
71328
+ * Attempt to fix verification failures by spawning a dedicated AI fix agent.
71329
+ * Follows the pattern established by the merger's attemptInMergeVerificationFix.
71330
+ * Returns true if verification passes after the fix attempt, false otherwise.
71331
+ */
71332
+ async attemptExecutorVerificationFix(task, worktreePath, failureContext, settings, retryNumber, maxRetries) {
71333
+ try {
71334
+ executorLog.log(`${task.id}: spawning executor verification fix agent (attempt ${retryNumber}/${maxRetries})`);
71335
+ const logger2 = new AgentLogger({
71336
+ store: this.store,
71337
+ taskId: task.id,
71338
+ agent: "executor",
71339
+ persistAgentToolOutput: settings.persistAgentToolOutput,
71340
+ onAgentText: this.options.onAgentText,
71341
+ onAgentTool: this.options.onAgentTool
71342
+ });
71343
+ let skillContext;
71344
+ if (this.options.agentStore) {
71345
+ try {
71346
+ skillContext = await buildSessionSkillContext({
71347
+ agentStore: this.options.agentStore,
71348
+ task,
71349
+ sessionPurpose: "executor",
71350
+ projectRootDir: worktreePath,
71351
+ pluginRunner: this.options.pluginRunner
71352
+ });
71353
+ } catch {
71354
+ }
71355
+ }
71356
+ const { provider: executorProvider, modelId: executorModelId } = resolveExecutorModelPair2(
71357
+ task.modelProvider,
71358
+ task.modelId,
71359
+ settings
71360
+ );
71361
+ const { session } = await createResolvedAgentSession({
71362
+ sessionPurpose: "executor",
71363
+ pluginRunner: this.options.pluginRunner,
71364
+ cwd: worktreePath,
71365
+ // Run in the task's worktree
71366
+ systemPrompt: `You are a verification fix agent running during task execution in a worktree.
71367
+
71368
+ All step-session steps completed successfully but the deterministic verification command failed. Your job is to fix the failing code directly in the working directory.
71369
+
71370
+ ## Scope
71371
+ Only fix what is required to make the failing verification pass.
71372
+ Do not refactor, rename broadly, or make opportunistic improvements.
71373
+
71374
+ ## Rules
71375
+ 1. Read the error output carefully to understand what is failing before editing anything
71376
+ 2. Before assuming a code fix is needed, check whether the failure is caused by stale/missing build artifacts in a sibling workspace package \u2014 typical signatures: \`Failed to resolve import "./X.js"\` pointing into another package's \`dist/\`, \`Cannot find module\`, or \`ERR_MODULE_NOT_FOUND\` referencing a workspace-internal path. In that case, rebuild the affected package(s) (e.g. \`pnpm --filter <pkg> build\`, or \`pnpm --filter "<scope>/*" build\` for a group) and re-run verification before editing source files.
71377
+ 3. Make targeted fixes to the failing code path
71378
+ 4. After fixing, run the verification command to confirm the fix works
71379
+ 5. Do NOT make any git commits \u2014 just fix the code
71380
+ 6. You MAY modify any files needed to make the verification pass, including files unrelated to this task's original change. Pre-existing build/test breakage is in scope: fix it. Prefer the smallest change that makes verification green.
71381
+ 7. If you cannot fix the issue within scope, explain why and what evidence indicates a deeper/root problem`,
71382
+ tools: "coding",
71383
+ onText: logger2.onText,
71384
+ onThinking: logger2.onThinking,
71385
+ onToolStart: logger2.onToolStart,
71386
+ onToolEnd: logger2.onToolEnd,
71387
+ defaultProvider: executorProvider,
71388
+ defaultModelId: executorModelId,
71389
+ defaultThinkingLevel: settings.defaultThinkingLevel,
71390
+ ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
71391
+ });
71392
+ await this.store.logEntry(
71393
+ task.id,
71394
+ `Executor verification fix agent started (model: ${describeModel(session)}, attempt ${retryNumber}/${maxRetries})`,
71395
+ void 0,
71396
+ this.currentRunContext
71397
+ );
71398
+ await this.store.appendAgentLog(
71399
+ task.id,
71400
+ `Fix agent started (model: ${describeModel(session)}, attempt ${retryNumber}/${maxRetries})`,
71401
+ "text",
71402
+ void 0,
71403
+ "executor"
71404
+ );
71405
+ try {
71406
+ const fixPrompt = `Fix the failing ${failureContext.type} verification for task ${task.id}.
71407
+
71408
+ ## Failed command
71409
+ Command: \`${failureContext.command}\`
71410
+ Exit code: ${failureContext.exitCode}
71411
+
71412
+ ## Error output
71413
+ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
71414
+
71415
+ ## Instructions
71416
+ 1. Read the error output and identify the root cause
71417
+ 2. Make targeted fixes to resolve the failure
71418
+ 3. Run the verification command \`${failureContext.command}\` to confirm your fix works
71419
+ 4. If the fix doesn't work, try a different approach
71420
+ 5. Do NOT make any git commits`;
71421
+ await withRateLimitRetry(async () => {
71422
+ await promptWithFallback(session, fixPrompt);
71423
+ }, {
71424
+ onRetry: (attempt, delayMs, error) => {
71425
+ const delaySec = Math.round(delayMs / 1e3);
71426
+ executorLog.warn(`\u23F3 ${task.id} executor fix agent rate limited \u2014 retry ${attempt} in ${delaySec}s: ${error.message}`);
71427
+ }
71428
+ });
71429
+ await accumulateSessionTokenUsage(this.store, task.id, session);
71430
+ executorLog.log(`${task.id}: re-running deterministic verification after fix attempt ${retryNumber}/${maxRetries}`);
71431
+ await this.store.logEntry(
71432
+ task.id,
71433
+ `Re-running deterministic verification (attempt ${retryNumber}/${maxRetries})`,
71434
+ void 0,
71435
+ this.currentRunContext
71436
+ );
71437
+ await this.store.appendAgentLog(
71438
+ task.id,
71439
+ `Re-running verification (attempt ${retryNumber}/${maxRetries})`,
71440
+ "text",
71441
+ void 0,
71442
+ "executor"
71443
+ );
71444
+ const reRunResult = await this.runExecutorDeterministicVerification(task, worktreePath, settings);
71445
+ return reRunResult.allPassed;
71446
+ } finally {
71447
+ await logger2.flush();
71448
+ await session.dispose();
71449
+ }
71450
+ } catch (err) {
71451
+ const errorMessage = err instanceof Error ? err.message : String(err);
71452
+ executorLog.warn(`${task.id}: executor verification fix agent error: ${errorMessage}`);
71453
+ await this.store.logEntry(
71454
+ task.id,
71455
+ `Executor verification fix agent encountered an error`,
71456
+ errorMessage,
71457
+ this.currentRunContext
71458
+ );
71459
+ await this.store.appendAgentLog(
71460
+ task.id,
71461
+ "Fix agent encountered an error",
71462
+ "tool_error",
71463
+ errorMessage,
71464
+ "executor"
71465
+ );
71466
+ return false;
71467
+ }
71468
+ }
70795
71469
  async handleWorkflowStepFailure(task, worktreePath, failureFeedback, stepName) {
70796
71470
  this.clearCompletedTaskWatchdog(task.id);
70797
71471
  const currentRetries = task.workflowStepRetries ?? 0;
@@ -70861,7 +71535,7 @@ Please fix the issues so the verification can pass on the next attempt.`,
70861
71535
  * The section is replaced entirely to avoid accumulation of old feedback.
70862
71536
  */
70863
71537
  async injectWorkflowStepFailureInstructions(task, failureFeedback, stepName, retryCount) {
70864
- const promptPath = join35(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
71538
+ const promptPath = join36(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
70865
71539
  let content;
70866
71540
  try {
70867
71541
  content = await readFile15(promptPath, "utf-8");
@@ -70926,31 +71600,33 @@ ${failureFeedback}
70926
71600
  * Uses git diff against the stored baseCommitSha to determine what changed.
70927
71601
  * Returns an empty array if no changes or if git commands fail.
70928
71602
  */
71603
+ async resolveDiffBaseRef(worktreePath, baseCommitSha) {
71604
+ if (baseCommitSha) return baseCommitSha;
71605
+ try {
71606
+ const { stdout } = await execAsync5(
71607
+ "git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main",
71608
+ { cwd: worktreePath, encoding: "utf-8" }
71609
+ );
71610
+ const ref = stdout.trim();
71611
+ if (ref) return ref;
71612
+ } catch (mergeBaseErr) {
71613
+ const mergeBaseMsg = mergeBaseErr instanceof Error ? mergeBaseErr.message : String(mergeBaseErr);
71614
+ executorLog.warn(`Failed merge-base lookup for diff base in ${worktreePath}, trying HEAD~1 fallback: ${mergeBaseMsg}`);
71615
+ }
71616
+ try {
71617
+ const { stdout } = await execAsync5("git rev-parse HEAD~1", {
71618
+ cwd: worktreePath,
71619
+ encoding: "utf-8"
71620
+ });
71621
+ return stdout.trim() || void 0;
71622
+ } catch {
71623
+ executorLog.log(`Could not determine base commit for diff in ${worktreePath}`);
71624
+ return void 0;
71625
+ }
71626
+ }
70929
71627
  async captureModifiedFiles(worktreePath, baseCommitSha) {
70930
71628
  try {
70931
- let baseRef = baseCommitSha;
70932
- if (!baseRef) {
70933
- try {
70934
- const { stdout: stdout2 } = await execAsync5("git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main", {
70935
- cwd: worktreePath,
70936
- encoding: "utf-8"
70937
- });
70938
- baseRef = stdout2.trim();
70939
- } catch (mergeBaseErr) {
70940
- const mergeBaseMsg = mergeBaseErr instanceof Error ? mergeBaseErr.message : String(mergeBaseErr);
70941
- executorLog.warn(`Failed merge-base lookup for diff base in ${worktreePath}, trying HEAD~1 fallback: ${mergeBaseMsg}`);
70942
- try {
70943
- const { stdout: stdout2 } = await execAsync5("git rev-parse HEAD~1", {
70944
- cwd: worktreePath,
70945
- encoding: "utf-8"
70946
- });
70947
- baseRef = stdout2.trim();
70948
- } catch {
70949
- executorLog.log(`Could not determine base commit for diff in ${worktreePath}`);
70950
- return [];
70951
- }
70952
- }
70953
- }
71629
+ const baseRef = await this.resolveDiffBaseRef(worktreePath, baseCommitSha);
70954
71630
  if (!baseRef) {
70955
71631
  return [];
70956
71632
  }
@@ -71186,6 +71862,30 @@ ${failureFeedback}
71186
71862
  */
71187
71863
  async executeWorkflowStep(task, workflowStep, worktreePath, settings) {
71188
71864
  const toolMode = workflowStep.toolMode || "readonly";
71865
+ const scopedFiles = await this.captureModifiedFiles(worktreePath, task.baseCommitSha);
71866
+ let diffShortstat;
71867
+ try {
71868
+ const baseRef = await this.resolveDiffBaseRef(worktreePath, task.baseCommitSha);
71869
+ if (baseRef) {
71870
+ const { stdout } = await execAsync5(`git diff --shortstat ${baseRef}..HEAD`, {
71871
+ cwd: worktreePath,
71872
+ encoding: "utf-8"
71873
+ });
71874
+ diffShortstat = stdout.trim() || void 0;
71875
+ }
71876
+ } catch {
71877
+ }
71878
+ const MAX_SCOPE_FILES = 100;
71879
+ const scopeFileBlock = scopedFiles.length === 0 ? "(no modified files detected for this task \u2014 review the worktree directly, but do NOT browse unrelated files)" : scopedFiles.length > MAX_SCOPE_FILES ? `${scopedFiles.slice(0, MAX_SCOPE_FILES).map((f) => `- ${f}`).join("\n")}
71880
+ - ... (${scopedFiles.length - MAX_SCOPE_FILES} more files truncated)` : scopedFiles.map((f) => `- ${f}`).join("\n");
71881
+ const scopeBlock = `Diff Scope (files changed by THIS task vs base):
71882
+ ${scopeFileBlock}${diffShortstat ? `
71883
+ Diff stat: ${diffShortstat}` : ""}
71884
+
71885
+ CRITICAL SCOPING RULES \u2014 read before doing anything else:
71886
+ - Review ONLY the files listed above. Do NOT analyze unmodified files or unrelated parts of the codebase.
71887
+ - If NONE of the files in the diff scope are relevant to your review category (e.g. a UX/design reviewer with no UI/CSS/component files in scope, a security reviewer with no auth/network code in scope, an a11y reviewer with no markup changes), respond IMMEDIATELY with a single short approval line such as "No relevant changes in scope \u2014 approved." and STOP. Do not start exploring the codebase.
71888
+ - Your wall-clock budget is short. Spending it browsing unmodified files will cause this step to time out and block merge.`;
71189
71889
  const systemPrompt = `You are a workflow step agent executing: ${workflowStep.name}
71190
71890
 
71191
71891
  Task Context:
@@ -71193,6 +71893,8 @@ Task Context:
71193
71893
  - Task Description: ${task.description}
71194
71894
  - Worktree: ${worktreePath}
71195
71895
 
71896
+ ${scopeBlock}
71897
+
71196
71898
  Your role:
71197
71899
  - Execute this workflow step exactly as scoped.
71198
71900
  - Prioritize high-impact correctness/risk findings over stylistic nits.
@@ -71661,7 +72363,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
71661
72363
  * rather than fail the task permanently.
71662
72364
  */
71663
72365
  async resolveWorktreeStartPoint(startPoint, taskId) {
71664
- const command = isAbsolute11(startPoint) && existsSync28(startPoint) ? `git -C "${startPoint}" rev-parse --verify HEAD^{commit}` : `git rev-parse --verify "${startPoint}^{commit}"`;
72366
+ const command = isAbsolute11(startPoint) && existsSync29(startPoint) ? `git -C "${startPoint}" rev-parse --verify HEAD^{commit}` : `git rev-parse --verify "${startPoint}^{commit}"`;
71665
72367
  try {
71666
72368
  const { stdout } = await execAsync5(command, { cwd: this.rootDir });
71667
72369
  return stdout.trim() || startPoint;
@@ -71681,7 +72383,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
71681
72383
  */
71682
72384
  async tryCreateWorktree(branch, path2, taskId, startPoint, attemptNumber = 0, recoveryDepth = 0) {
71683
72385
  await this.assertWorktreePathNotNested(path2, taskId);
71684
- if (existsSync28(path2)) {
72386
+ if (existsSync29(path2)) {
71685
72387
  const isRegistered = await this.isRegisteredWorktree(path2);
71686
72388
  if (!isRegistered) {
71687
72389
  await this.store.logEntry(
@@ -71832,7 +72534,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
71832
72534
  );
71833
72535
  if (shouldGenerateNewName) {
71834
72536
  const conflictStartPoint = branch;
71835
- const newPath = join35(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
72537
+ const newPath = join36(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
71836
72538
  for (let suffix = 2; suffix <= 6; suffix++) {
71837
72539
  const suffixedBranch = `${branch}-${suffix}`;
71838
72540
  try {
@@ -72432,7 +73134,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
72432
73134
  metadata: { type: "spawned", parentTaskId: taskId }
72433
73135
  });
72434
73136
  const childWorktreeName = generateWorktreeName(this.rootDir);
72435
- const childWorktreePath = join35(this.rootDir, ".worktrees", childWorktreeName);
73137
+ const childWorktreePath = join36(this.rootDir, ".worktrees", childWorktreeName);
72436
73138
  const childBranch = `fusion/spawn-${agent.id}`;
72437
73139
  await this.createWorktree(childBranch, childWorktreePath, taskId, worktreePath);
72438
73140
  await this.options.agentStore.updateAgentState(agent.id, "active");
@@ -72621,9 +73323,9 @@ var init_node_routing_policy = __esm({
72621
73323
  });
72622
73324
 
72623
73325
  // ../engine/src/scheduler.ts
72624
- import { existsSync as existsSync29 } from "node:fs";
73326
+ import { existsSync as existsSync30 } from "node:fs";
72625
73327
  import { readFile as readFile16 } from "node:fs/promises";
72626
- import { basename as basename8, join as join36 } from "node:path";
73328
+ import { basename as basename8, join as join37 } from "node:path";
72627
73329
  function pathsOverlap2(a, b) {
72628
73330
  for (const pa of a) {
72629
73331
  const prefixA = pa.endsWith("/*") ? pa.slice(0, -1) : null;
@@ -72793,12 +73495,12 @@ var init_scheduler = __esm({
72793
73495
  * @returns Object with `valid: true` if checks pass, or `valid: false` with a `reason` string if they fail
72794
73496
  */
72795
73497
  async validateTaskFilesystem(id) {
72796
- const taskDir = join36(this.store.getTasksDir(), id);
72797
- if (!existsSync29(taskDir)) {
73498
+ const taskDir = join37(this.store.getTasksDir(), id);
73499
+ if (!existsSync30(taskDir)) {
72798
73500
  return { valid: false, reason: "missing directory" };
72799
73501
  }
72800
- const promptPath = join36(taskDir, "PROMPT.md");
72801
- if (!existsSync29(promptPath)) {
73502
+ const promptPath = join37(taskDir, "PROMPT.md");
73503
+ if (!existsSync30(promptPath)) {
72802
73504
  return { valid: false, reason: "missing or empty PROMPT.md" };
72803
73505
  }
72804
73506
  try {
@@ -72937,7 +73639,7 @@ var init_scheduler = __esm({
72937
73639
  break;
72938
73640
  }
72939
73641
  reservedNames.add(worktreeName);
72940
- return join36(this.store.getRootDir(), ".worktrees", worktreeName);
73642
+ return join37(this.store.getRootDir(), ".worktrees", worktreeName);
72941
73643
  }
72942
73644
  /**
72943
73645
  * Run one scheduling pass.
@@ -74214,7 +74916,7 @@ var init_mission_execution_loop = __esm({
74214
74916
  init_pi();
74215
74917
  init_agent_session_helpers();
74216
74918
  init_logger2();
74217
- init_notifier();
74919
+ init_fallback_model_observer();
74218
74920
  loopLog = createLogger2("mission-loop");
74219
74921
  VALIDATION_TIMEOUT_MS = 10 * 60 * 1e3;
74220
74922
  MissionExecutionLoop = class extends EventEmitter17 {
@@ -74448,7 +75150,13 @@ Assertions: ${assertions.map((a) => a.title).join(", ")}`,
74448
75150
  },
74449
75151
  taskId: task?.id,
74450
75152
  taskTitle: task?.title,
74451
- onFallbackModelUsed: notifyFallbackUsed
75153
+ onFallbackModelUsed: createFallbackModelObserver({
75154
+ agent: "reviewer",
75155
+ label: "mission validator",
75156
+ store: this.taskStore,
75157
+ taskId: task?.id,
75158
+ taskTitle: task?.title
75159
+ })
74452
75160
  });
74453
75161
  session = { session: sessionResult.session, sessionFile: sessionResult.sessionFile };
74454
75162
  loopLog.log(`Validation session created for feature ${feature.id}`);
@@ -76256,7 +76964,7 @@ not loop on the same plan across heartbeats without recording why.`;
76256
76964
  };
76257
76965
  }
76258
76966
  };
76259
- const { createResolvedAgentSession: createResolvedAgentSession2, extractRuntimeHint: extractRuntimeHint2 } = await Promise.resolve().then(() => (init_agent_session_helpers(), agent_session_helpers_exports));
76967
+ const { createResolvedAgentSession: createResolvedAgentSession2, extractRuntimeHint: extractRuntimeHint2, extractRuntimeModel: extractRuntimeModel2 } = await Promise.resolve().then(() => (init_agent_session_helpers(), agent_session_helpers_exports));
76260
76968
  const { buildSessionSkillContextSync: buildSessionSkillContextSync2 } = await Promise.resolve().then(() => (init_session_skill_context(), session_skill_context_exports));
76261
76969
  let heartbeatTools;
76262
76970
  if (isNoTaskRun) {
@@ -76335,8 +77043,10 @@ not loop on the same plan across heartbeats without recording why.`;
76335
77043
  systemPrompt,
76336
77044
  tools: "readonly",
76337
77045
  customTools: heartbeatTools,
76338
- defaultProvider: agent.runtimeConfig?.modelProvider,
76339
- defaultModelId: agent.runtimeConfig?.modelId,
77046
+ ...(() => {
77047
+ const { provider, modelId } = extractRuntimeModel2(agent.runtimeConfig);
77048
+ return { defaultProvider: provider, defaultModelId: modelId };
77049
+ })(),
76340
77050
  onText: (delta) => {
76341
77051
  outputLength += delta.length;
76342
77052
  appendStdoutExcerpt(delta);
@@ -77711,7 +78421,7 @@ async function createAiPromptExecutor(cwd) {
77711
78421
  }
77712
78422
  };
77713
78423
  }
77714
- function truncateOutput(stdout, stderr) {
78424
+ function truncateOutput2(stdout, stderr) {
77715
78425
  const out = stdout ?? "";
77716
78426
  const err = stderr ?? "";
77717
78427
  let combined = out;
@@ -77891,7 +78601,7 @@ var init_cron_runner = __esm({
77891
78601
  maxBuffer: MAX_BUFFER,
77892
78602
  shell: defaultShell
77893
78603
  });
77894
- const output = truncateOutput(stdout, stderr);
78604
+ const output = truncateOutput2(stdout, stderr);
77895
78605
  log15.log(`\u2713 ${schedule.name} completed (${output.length} bytes output)`);
77896
78606
  return {
77897
78607
  success: true,
@@ -77902,7 +78612,7 @@ var init_cron_runner = __esm({
77902
78612
  } catch (err) {
77903
78613
  const stdout = err.stdout ?? "";
77904
78614
  const stderr = err.stderr ?? "";
77905
- const output = truncateOutput(stdout, stderr);
78615
+ const output = truncateOutput2(stdout, stderr);
77906
78616
  const errorMessage = err.killed ? `Command timed out after ${(schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS6) / 1e3}s` : err.message ?? String(err);
77907
78617
  log15.warn(`\u2717 ${schedule.name} failed: ${errorMessage}`);
77908
78618
  return {
@@ -77947,7 +78657,7 @@ var init_cron_runner = __esm({
77947
78657
  const result = await runBackupCommand2(fusionDir, settings);
77948
78658
  return {
77949
78659
  success: result.success,
77950
- output: truncateOutput(result.output ?? "", ""),
78660
+ output: truncateOutput2(result.output ?? "", ""),
77951
78661
  error: result.success ? void 0 : result.output
77952
78662
  };
77953
78663
  } catch (err) {
@@ -77988,7 +78698,7 @@ var init_cron_runner = __esm({
77988
78698
  if (sr.output) outputParts.push(sr.output);
77989
78699
  if (sr.error) outputParts.push(`Error: ${sr.error}`);
77990
78700
  }
77991
- const output = truncateOutput(outputParts.join("\n"), "");
78701
+ const output = truncateOutput2(outputParts.join("\n"), "");
77992
78702
  const failedSteps = stepResults.filter((sr) => !sr.success);
77993
78703
  const error = failedSteps.length > 0 ? `${failedSteps.length} step(s) failed: ${failedSteps.map((s) => s.stepName).join(", ")}${stoppedEarly ? " (execution stopped)" : ""}` : void 0;
77994
78704
  const status = overallSuccess ? "\u2713" : "\u2717";
@@ -78067,7 +78777,7 @@ var init_cron_runner = __esm({
78067
78777
  stepName: step.name,
78068
78778
  stepIndex,
78069
78779
  success: true,
78070
- output: truncateOutput(stdout, stderr),
78780
+ output: truncateOutput2(stdout, stderr),
78071
78781
  startedAt,
78072
78782
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
78073
78783
  };
@@ -78080,7 +78790,7 @@ var init_cron_runner = __esm({
78080
78790
  stepName: step.name,
78081
78791
  stepIndex,
78082
78792
  success: false,
78083
- output: truncateOutput(stdout, stderr),
78793
+ output: truncateOutput2(stdout, stderr),
78084
78794
  error: errorMessage,
78085
78795
  startedAt,
78086
78796
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -78230,7 +78940,7 @@ var init_cron_runner = __esm({
78230
78940
  // ../engine/src/routine-runner.ts
78231
78941
  import { exec as exec8 } from "node:child_process";
78232
78942
  import { promisify as promisify8 } from "node:util";
78233
- function truncateOutput2(stdout, stderr) {
78943
+ function truncateOutput3(stdout, stderr) {
78234
78944
  let output = stdout;
78235
78945
  if (stderr) {
78236
78946
  output += stdout ? "\n--- stderr ---\n" : "";
@@ -78413,7 +79123,7 @@ var init_routine_runner = __esm({
78413
79123
  const result = await runBackupCommand2(fusionDir, settings);
78414
79124
  return {
78415
79125
  success: result.success,
78416
- output: truncateOutput2(result.output ?? "", ""),
79126
+ output: truncateOutput3(result.output ?? "", ""),
78417
79127
  error: result.success ? void 0 : result.output,
78418
79128
  startedAt,
78419
79129
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -78437,7 +79147,7 @@ var init_routine_runner = __esm({
78437
79147
  });
78438
79148
  return {
78439
79149
  success: true,
78440
- output: truncateOutput2(stdout, stderr),
79150
+ output: truncateOutput3(stdout, stderr),
78441
79151
  startedAt,
78442
79152
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
78443
79153
  };
@@ -78448,7 +79158,7 @@ var init_routine_runner = __esm({
78448
79158
  const error = errObj.killed === true ? `Command timed out after ${(timeoutMs ?? DEFAULT_TIMEOUT_MS7) / 1e3}s` : (err instanceof Error ? err.message : null) ?? String(err);
78449
79159
  return {
78450
79160
  success: false,
78451
- output: truncateOutput2(stdout, stderr),
79161
+ output: truncateOutput3(stdout, stderr),
78452
79162
  error,
78453
79163
  startedAt,
78454
79164
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -78481,7 +79191,7 @@ var init_routine_runner = __esm({
78481
79191
  const failedSteps = stepResults.filter((sr) => !sr.success);
78482
79192
  return {
78483
79193
  success: overallSuccess,
78484
- output: truncateOutput2(outputParts.join("\n"), ""),
79194
+ output: truncateOutput3(outputParts.join("\n"), ""),
78485
79195
  error: failedSteps.length > 0 ? `${failedSteps.length} step(s) failed: ${failedSteps.map((s) => s.stepName).join(", ")}${stoppedEarly ? " (execution stopped)" : ""}` : void 0,
78486
79196
  startedAt,
78487
79197
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -78516,7 +79226,7 @@ var init_routine_runner = __esm({
78516
79226
  this.options.aiPromptExecutor(step.prompt, step.modelProvider, step.modelId),
78517
79227
  new Promise((_resolve, reject) => setTimeout(() => reject(new Error(`AI prompt step timed out after ${timeoutMs / 1e3}s`)), timeoutMs))
78518
79228
  ]);
78519
- return { stepId: step.id, stepName: step.name, stepIndex, success: true, output: truncateOutput2(output, ""), startedAt, completedAt: (/* @__PURE__ */ new Date()).toISOString() };
79229
+ return { stepId: step.id, stepName: step.name, stepIndex, success: true, output: truncateOutput3(output, ""), startedAt, completedAt: (/* @__PURE__ */ new Date()).toISOString() };
78520
79230
  } catch (err) {
78521
79231
  return { stepId: step.id, stepName: step.name, stepIndex, success: false, output: "", error: err instanceof Error ? err.message : String(err), startedAt, completedAt: (/* @__PURE__ */ new Date()).toISOString() };
78522
79232
  }
@@ -79130,8 +79840,8 @@ var init_stuck_task_detector = __esm({
79130
79840
  // ../engine/src/self-healing.ts
79131
79841
  import { exec as exec9 } from "node:child_process";
79132
79842
  import { promisify as promisify9 } from "node:util";
79133
- import { existsSync as existsSync30, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
79134
- import { isAbsolute as isAbsolute13, join as join37, relative as relative8, resolve as resolve17 } from "node:path";
79843
+ import { existsSync as existsSync31, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
79844
+ import { isAbsolute as isAbsolute13, join as join38, relative as relative8, resolve as resolve17 } from "node:path";
79135
79845
  function shellQuote(value) {
79136
79846
  return `'${value.replace(/'/g, "'\\''")}'`;
79137
79847
  }
@@ -79527,7 +80237,7 @@ var init_self_healing = __esm({
79527
80237
  return commit;
79528
80238
  }
79529
80239
  async cleanupInterruptedMergeArtifacts(task) {
79530
- if (task.worktree && existsSync30(task.worktree)) {
80240
+ if (task.worktree && existsSync31(task.worktree)) {
79531
80241
  try {
79532
80242
  await execAsync7(`git worktree remove ${shellQuote(task.worktree)} --force`, {
79533
80243
  cwd: this.options.rootDir,
@@ -80148,7 +80858,7 @@ var init_self_healing = __esm({
80148
80858
  return false;
80149
80859
  }
80150
80860
  const staleness = now - new Date(t.updatedAt).getTime();
80151
- const hasWorktree = t.worktree && existsSync30(t.worktree);
80861
+ const hasWorktree = t.worktree && existsSync31(t.worktree);
80152
80862
  const graceMs = hasWorktree ? ORPHANED_WITH_WORKTREE_GRACE_MS : ORPHANED_EXECUTION_RECOVERY_GRACE_MS;
80153
80863
  return staleness >= graceMs;
80154
80864
  });
@@ -80157,7 +80867,7 @@ var init_self_healing = __esm({
80157
80867
  let recovered = 0;
80158
80868
  for (const task of orphaned) {
80159
80869
  try {
80160
- const hadWorktree = task.worktree && existsSync30(task.worktree);
80870
+ const hadWorktree = task.worktree && existsSync31(task.worktree);
80161
80871
  const reason = hadWorktree ? "worktree exists but no active session" : "missing worktree/session";
80162
80872
  await this.resetStepsIfWorkLost(task);
80163
80873
  await this.store.updateTask(task.id, {
@@ -80303,7 +81013,7 @@ var init_self_healing = __esm({
80303
81013
  }
80304
81014
  }
80305
81015
  async hasRecoverableGitWork(task) {
80306
- if (task.worktree && existsSync30(task.worktree)) {
81016
+ if (task.worktree && existsSync31(task.worktree)) {
80307
81017
  try {
80308
81018
  const { stdout: status } = await execAsync7("git status --porcelain", {
80309
81019
  cwd: task.worktree,
@@ -80488,11 +81198,11 @@ var init_self_healing = __esm({
80488
81198
  * tracks registered idle worktrees, never these orphans.
80489
81199
  */
80490
81200
  async reapUnregisteredOrphans() {
80491
- const worktreesDir = join37(this.options.rootDir, ".worktrees");
80492
- if (!existsSync30(worktreesDir)) return 0;
81201
+ const worktreesDir = join38(this.options.rootDir, ".worktrees");
81202
+ if (!existsSync31(worktreesDir)) return 0;
80493
81203
  let dirs;
80494
81204
  try {
80495
- dirs = readdirSync5(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join37(worktreesDir, e.name));
81205
+ dirs = readdirSync5(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join38(worktreesDir, e.name));
80496
81206
  } catch (err) {
80497
81207
  log17.warn(`Failed to read .worktrees/ for unregistered orphan reap: ${err instanceof Error ? err.message : String(err)}`);
80498
81208
  return 0;
@@ -80597,8 +81307,8 @@ var init_self_healing = __esm({
80597
81307
  }
80598
81308
  /** Remove oldest idle worktrees if total count exceeds 2× maxWorktrees. */
80599
81309
  async enforceWorktreeCap() {
80600
- const worktreesDir = join37(this.options.rootDir, ".worktrees");
80601
- if (!existsSync30(worktreesDir)) return;
81310
+ const worktreesDir = join38(this.options.rootDir, ".worktrees");
81311
+ if (!existsSync31(worktreesDir)) return;
80602
81312
  try {
80603
81313
  const settings = await this.store.getSettings();
80604
81314
  const cap = (settings.maxWorktrees ?? 4) * 2;
@@ -82436,7 +83146,7 @@ var init_ipc_host = __esm({
82436
83146
  import { EventEmitter as EventEmitter20 } from "node:events";
82437
83147
  import { fork } from "node:child_process";
82438
83148
  import { fileURLToPath as fileURLToPath3 } from "node:url";
82439
- import { dirname as dirname11, join as join38 } from "node:path";
83149
+ import { dirname as dirname11, join as join39 } from "node:path";
82440
83150
  var HealthMonitor, ChildProcessRuntime;
82441
83151
  var init_child_process_runtime = __esm({
82442
83152
  "../engine/src/runtimes/child-process-runtime.ts"() {
@@ -82598,7 +83308,7 @@ var init_child_process_runtime = __esm({
82598
83308
  const isCompiled = !import.meta.url.endsWith(".ts");
82599
83309
  const currentDir = dirname11(fileURLToPath3(import.meta.url));
82600
83310
  const workerFile = isCompiled ? "child-process-worker.js" : "child-process-worker.ts";
82601
- return join38(currentDir, workerFile);
83311
+ return join39(currentDir, workerFile);
82602
83312
  }
82603
83313
  /**
82604
83314
  * Set up event forwarding from IPC host to runtime listeners.
@@ -86851,6 +87561,8 @@ __export(src_exports2, {
86851
87561
  describeAgentModel: () => describeAgentModel,
86852
87562
  describeModel: () => describeModel,
86853
87563
  ensureDefaultHeartbeatProcedureFile: () => ensureDefaultHeartbeatProcedureFile,
87564
+ extractRuntimeHint: () => extractRuntimeHint,
87565
+ extractRuntimeModel: () => extractRuntimeModel,
86854
87566
  formatTaskIdentifier: () => formatTaskIdentifier,
86855
87567
  getDefaultPiRuntime: () => getDefaultPiRuntime,
86856
87568
  getHostExtensionPaths: () => getHostExtensionPaths,
@@ -101409,7 +102121,7 @@ var init_auth_middleware = __esm({
101409
102121
 
101410
102122
  // ../dashboard/src/server.ts
101411
102123
  import express from "express";
101412
- import { join as join39, dirname as dirname12 } from "node:path";
102124
+ import { join as join40, dirname as dirname12 } from "node:path";
101413
102125
  import { fileURLToPath as fileURLToPath4 } from "node:url";
101414
102126
  function clearAiSessionCleanupInterval() {
101415
102127
  if (!aiSessionCleanupIntervalHandle) {
@@ -101701,8 +102413,8 @@ __export(task_exports, {
101701
102413
  runTaskUpdate: () => runTaskUpdate
101702
102414
  });
101703
102415
  import { createInterface as createInterface2 } from "node:readline/promises";
101704
- import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync31, readFileSync as readFileSync10 } from "node:fs";
101705
- import { basename as basename10, join as join40 } from "node:path";
102416
+ import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync32, readFileSync as readFileSync11 } from "node:fs";
102417
+ import { basename as basename10, join as join41 } from "node:path";
101706
102418
  function getGitHubIssueUrl(sourceMetadata) {
101707
102419
  if (!sourceMetadata || typeof sourceMetadata !== "object") return void 0;
101708
102420
  const issueUrl = sourceMetadata.issueUrl;
@@ -102015,8 +102727,8 @@ async function runTaskLogs(id, options = {}, projectName) {
102015
102727
  printEntries(filteredEntries);
102016
102728
  if (options.follow) {
102017
102729
  const projectPath = projectContext?.projectPath ?? process.cwd();
102018
- const logPath = join40(projectPath, ".fusion", "tasks", id, "agent.log");
102019
- if (!existsSync31(logPath)) {
102730
+ const logPath = join41(projectPath, ".fusion", "tasks", id, "agent.log");
102731
+ if (!existsSync32(logPath)) {
102020
102732
  console.log(`
102021
102733
  Waiting for log file to be created...`);
102022
102734
  }
@@ -102045,7 +102757,7 @@ async function runTaskLogs(id, options = {}, projectName) {
102045
102757
  lastPosition = 0;
102046
102758
  }
102047
102759
  if (stats.size > lastPosition) {
102048
- const content = readFileSync10(logPath, "utf-8");
102760
+ const content = readFileSync11(logPath, "utf-8");
102049
102761
  const lines = content.slice(lastPosition).split("\n");
102050
102762
  for (const line of lines) {
102051
102763
  if (!line.trim()) continue;
@@ -103158,9 +103870,9 @@ init_src();
103158
103870
  init_gh_cli();
103159
103871
  import { Type as Type8 } from "typebox";
103160
103872
  import { StringEnum } from "@mariozechner/pi-ai";
103161
- import { resolve as resolve19, basename as basename11, extname as extname3, join as join41 } from "node:path";
103873
+ import { resolve as resolve19, basename as basename11, extname as extname3, join as join42 } from "node:path";
103162
103874
  import { readFile as readFile19 } from "node:fs/promises";
103163
- import { existsSync as existsSync32 } from "node:fs";
103875
+ import { existsSync as existsSync33 } from "node:fs";
103164
103876
  import { spawn as spawn11 } from "node:child_process";
103165
103877
  var MIME_TYPES2 = {
103166
103878
  ".png": "image/png",
@@ -103180,7 +103892,7 @@ var MIME_TYPES2 = {
103180
103892
  function resolveProjectRoot2(cwd) {
103181
103893
  let current = resolve19(cwd);
103182
103894
  while (true) {
103183
- if (existsSync32(join41(current, ".fusion"))) {
103895
+ if (existsSync33(join42(current, ".fusion"))) {
103184
103896
  return current;
103185
103897
  }
103186
103898
  const parent = resolve19(current, "..");
@@ -103201,7 +103913,7 @@ async function getStore2(cwd) {
103201
103913
  return store;
103202
103914
  }
103203
103915
  function getFusionDir(cwd) {
103204
- return join41(resolveProjectRoot2(cwd), ".fusion");
103916
+ return join42(resolveProjectRoot2(cwd), ".fusion");
103205
103917
  }
103206
103918
  async function validateAssignableAgentId(cwd, agentId) {
103207
103919
  const { AgentStore: AgentStore2, isEphemeralAgent: isEphemeralAgent2 } = await Promise.resolve().then(() => (init_src(), src_exports));