@runfusion/fusion 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/bin.js +2113 -1017
  2. package/dist/client/assets/AgentDetailView-CDZED6Dy.css +1 -0
  3. package/dist/client/assets/AgentDetailView-zycSdnO8.js +28 -0
  4. package/dist/client/assets/AgentsView-DoQkkDLf.css +1 -0
  5. package/dist/client/assets/AgentsView-pO7WiBS5.js +522 -0
  6. package/dist/client/assets/ChatView-BOd-sxbT.js +1 -0
  7. package/dist/client/assets/DevServerView-09GQf34f.js +11 -0
  8. package/dist/client/assets/DevServerView-ZeBGQkLI.css +1 -0
  9. package/dist/client/assets/DirectoryPicker-CcdN1Zs7.js +1 -0
  10. package/dist/client/assets/DocumentsView-CS8aiwtz.js +1 -0
  11. package/dist/client/assets/DocumentsView-Co9to4Zp.css +1 -0
  12. package/dist/client/assets/InsightsView-Bu9Cv8Ol.js +11 -0
  13. package/dist/client/assets/InsightsView-Egu71gmh.css +1 -0
  14. package/dist/client/assets/MemoryView-CtqgDtV9.js +2 -0
  15. package/dist/client/assets/MemoryView-DhinauGs.css +1 -0
  16. package/dist/client/assets/NodesView-BInPcedy.js +14 -0
  17. package/dist/client/assets/NodesView-DlQZHGXA.css +1 -0
  18. package/dist/client/assets/PiExtensionsManager-COxkYM2m.js +11 -0
  19. package/dist/client/assets/PiExtensionsManager-CPgmJgDk.css +1 -0
  20. package/dist/client/assets/PluginManager-CXUWZBOc.js +1 -0
  21. package/dist/client/assets/PluginManager-D64RIzmL.css +1 -0
  22. package/dist/client/assets/RoadmapsView-BOYnyMCh.css +1 -0
  23. package/dist/client/assets/RoadmapsView-BbCexaoi.js +6 -0
  24. package/dist/client/assets/SetupWizardModal-Cakxqkad.js +1 -0
  25. package/dist/client/assets/SkillsView-Cytf009Z.css +1 -0
  26. package/dist/client/assets/SkillsView-D3iqYCVf.js +1 -0
  27. package/dist/client/assets/folder-open-kO5Hsk66.js +6 -0
  28. package/dist/client/assets/index-BiSuUXCa.css +1 -0
  29. package/dist/client/assets/index-y194HxzU.js +644 -0
  30. package/dist/client/assets/upload-DHBQat92.js +6 -0
  31. package/dist/client/index.html +2 -2
  32. package/dist/extension.js +175 -66
  33. package/dist/pi-claude-cli/src/__tests__/control-handler.test.ts +191 -0
  34. package/dist/pi-claude-cli/src/__tests__/event-bridge.test.ts +1244 -0
  35. package/dist/pi-claude-cli/src/__tests__/mcp-config.test.ts +272 -0
  36. package/dist/pi-claude-cli/src/__tests__/process-manager.test.ts +619 -0
  37. package/dist/pi-claude-cli/src/__tests__/prompt-builder.test.ts +1067 -0
  38. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +1902 -0
  39. package/dist/pi-claude-cli/src/__tests__/stream-parser.test.ts +188 -0
  40. package/dist/pi-claude-cli/src/__tests__/thinking-config.test.ts +141 -0
  41. package/dist/pi-claude-cli/src/__tests__/tool-mapping.test.ts +252 -0
  42. package/package.json +11 -5
  43. package/dist/client/assets/index-BuenKJX0.css +0 -1
  44. package/dist/client/assets/index-CjGu8HRV.js +0 -1250
@@ -0,0 +1,6 @@
1
+ import{c as a}from"./index-y194HxzU.js";/**
2
+ * @license lucide-react v1.7.0 - ISC
3
+ *
4
+ * This source code is licensed under the ISC license.
5
+ * See the LICENSE file in the root directory of this source tree.
6
+ */const o=[["path",{d:"M12 3v12",key:"1x0j5s"}],["path",{d:"m17 8-5-5-5 5",key:"7q97r8"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}]],t=a("upload",o);export{t as U};
@@ -78,11 +78,11 @@
78
78
  }
79
79
  })();
80
80
  </script>
81
- <script type="module" crossorigin src="/assets/index-CjGu8HRV.js"></script>
81
+ <script type="module" crossorigin src="/assets/index-y194HxzU.js"></script>
82
82
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-K0fH_qHe.js">
83
83
  <link rel="modulepreload" crossorigin href="/assets/vendor-xterm-DzcZoU0P.js">
84
84
  <link rel="stylesheet" crossorigin href="/assets/vendor-xterm-LZoznX6r.css">
85
- <link rel="stylesheet" crossorigin href="/assets/index-BuenKJX0.css">
85
+ <link rel="stylesheet" crossorigin href="/assets/index-BiSuUXCa.css">
86
86
  </head>
87
87
  <body>
88
88
  <div id="root"></div>
package/dist/extension.js CHANGED
@@ -60,6 +60,7 @@ var init_settings_schema = __esm({
60
60
  defaultThinkingLevel: void 0,
61
61
  ntfyEnabled: false,
62
62
  ntfyTopic: void 0,
63
+ ntfyBaseUrl: void 0,
63
64
  ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval", "awaiting-user-review", "planning-awaiting-input"],
64
65
  ntfyDashboardHost: void 0,
65
66
  defaultProjectId: void 0,
@@ -47985,7 +47986,7 @@ var init_src = __esm({
47985
47986
  }
47986
47987
  });
47987
47988
 
47988
- // ../engine/src/logger.js
47989
+ // ../engine/src/logger.ts
47989
47990
  function withSeverityMarker2(level, payload) {
47990
47991
  return `${LOG_LEVEL_MARKER_PREFIX2}${level}${LOG_LEVEL_MARKER_SUFFIX2}${payload}`;
47991
47992
  }
@@ -47993,13 +47994,13 @@ function createLogger2(prefix) {
47993
47994
  const tag = `[${prefix}]`;
47994
47995
  return {
47995
47996
  log(message, ...args) {
47996
- globalThis.console.error(withSeverityMarker2("info", `${tag} ${message}`), ...args);
47997
+ console.error(withSeverityMarker2("info", `${tag} ${message}`), ...args);
47997
47998
  },
47998
47999
  warn(message, ...args) {
47999
- globalThis.console.warn(withSeverityMarker2("warn", `${tag} ${message}`), ...args);
48000
+ console.warn(withSeverityMarker2("warn", `${tag} ${message}`), ...args);
48000
48001
  },
48001
48002
  error(message, ...args) {
48002
- globalThis.console.error(withSeverityMarker2("error", `${tag} ${message}`), ...args);
48003
+ console.error(withSeverityMarker2("error", `${tag} ${message}`), ...args);
48003
48004
  }
48004
48005
  };
48005
48006
  }
@@ -48025,7 +48026,7 @@ ${stack}` : message2;
48025
48026
  }
48026
48027
  var LOG_LEVEL_MARKER_PREFIX2, LOG_LEVEL_MARKER_SUFFIX2, schedulerLog, executorLog, triageLog, piLog, extensionsLog, mergerLog, worktreePoolLog, reviewerLog, prMonitorLog, runtimeLog, ipcLog, projectManagerLog, hybridExecutorLog, autopilotLog, heartbeatLog, remoteNodeLog, nodeHealthMonitorLog, peerExchangeLog;
48027
48028
  var init_logger2 = __esm({
48028
- "../engine/src/logger.js"() {
48029
+ "../engine/src/logger.ts"() {
48029
48030
  "use strict";
48030
48031
  LOG_LEVEL_MARKER_PREFIX2 = "\0fnlvl=";
48031
48032
  LOG_LEVEL_MARKER_SUFFIX2 = "\0";
@@ -49184,7 +49185,7 @@ var init_concurrency = __esm({
49184
49185
  }
49185
49186
  });
49186
49187
 
49187
- // ../engine/src/skill-resolver.js
49188
+ // ../engine/src/skill-resolver.ts
49188
49189
  import { existsSync as existsSync18, readFileSync as readFileSync4 } from "node:fs";
49189
49190
  import { join as join22 } from "node:path";
49190
49191
  function readJsonObject(path) {
@@ -49308,9 +49309,13 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
49308
49309
  let filteredSkills;
49309
49310
  if (hasRequestedNames) {
49310
49311
  const requestedNamesLower = new Set(requestedSkillNames.map((n) => n.toLowerCase()));
49311
- filteredSkills = base.skills.filter((skill) => requestedNamesLower.has(skill.name.toLowerCase()) && !excludedSkillPaths.has(skill.filePath));
49312
+ filteredSkills = base.skills.filter(
49313
+ (skill) => requestedNamesLower.has(skill.name.toLowerCase()) && !excludedSkillPaths.has(skill.filePath)
49314
+ );
49312
49315
  } else if (hasPatterns) {
49313
- filteredSkills = base.skills.filter((skill) => allowedSkillPaths.has(skill.filePath) && !excludedSkillPaths.has(skill.filePath));
49316
+ filteredSkills = base.skills.filter(
49317
+ (skill) => allowedSkillPaths.has(skill.filePath) && !excludedSkillPaths.has(skill.filePath)
49318
+ );
49314
49319
  } else if (hasExcluded) {
49315
49320
  filteredSkills = base.skills.filter((skill) => !excludedSkillPaths.has(skill.filePath));
49316
49321
  } else {
@@ -49350,6 +49355,7 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
49350
49355
  }
49351
49356
  }
49352
49357
  if (newDiagnostics.length > 0) {
49358
+ const _purpose = sessionPurpose ? `[${sessionPurpose}]` : "skills";
49353
49359
  for (const diag of newDiagnostics) {
49354
49360
  piLog.warn(`[skills] ${diag.type}: ${diag.message}`);
49355
49361
  }
@@ -49361,21 +49367,20 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
49361
49367
  };
49362
49368
  }
49363
49369
  var init_skill_resolver = __esm({
49364
- "../engine/src/skill-resolver.js"() {
49370
+ "../engine/src/skill-resolver.ts"() {
49365
49371
  "use strict";
49366
49372
  init_logger2();
49367
49373
  }
49368
49374
  });
49369
49375
 
49370
- // ../engine/src/context-limit-detector.js
49376
+ // ../engine/src/context-limit-detector.ts
49371
49377
  function isContextLimitError(message) {
49372
- if (!message)
49373
- return false;
49378
+ if (!message) return false;
49374
49379
  return CONTEXT_OVERFLOW_PATTERNS.some((pattern) => pattern.test(message));
49375
49380
  }
49376
49381
  var CONTEXT_OVERFLOW_PATTERNS;
49377
49382
  var init_context_limit_detector = __esm({
49378
- "../engine/src/context-limit-detector.js"() {
49383
+ "../engine/src/context-limit-detector.ts"() {
49379
49384
  "use strict";
49380
49385
  CONTEXT_OVERFLOW_PATTERNS = [
49381
49386
  // Anthropic: "prompt is too long: X tokens > Y maximum"
@@ -49412,14 +49417,14 @@ var init_context_limit_detector = __esm({
49412
49417
  }
49413
49418
  });
49414
49419
 
49415
- // ../engine/src/auth-storage.js
49420
+ // ../engine/src/auth-storage.ts
49416
49421
  import { existsSync as existsSync19, readFileSync as readFileSync5 } from "node:fs";
49417
49422
  import { homedir as homedir4 } from "node:os";
49418
49423
  import { join as join23 } from "node:path";
49419
49424
  import { AuthStorage } from "@mariozechner/pi-coding-agent";
49420
49425
  import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
49421
49426
  function getHomeDir2() {
49422
- return globalThis.process.env.HOME || globalThis.process.env.USERPROFILE || homedir4();
49427
+ return process.env.HOME || process.env.USERPROFILE || homedir4();
49423
49428
  }
49424
49429
  function getFusionAuthPath(home = getHomeDir2()) {
49425
49430
  return join23(home, ".fusion", "agent", "auth.json");
@@ -49463,9 +49468,8 @@ function readLegacyCredentials(authPaths = getLegacyAuthPaths()) {
49463
49468
  return credentials;
49464
49469
  }
49465
49470
  function resolveStoredApiKey(key) {
49466
- if (!key)
49467
- return void 0;
49468
- return globalThis.process.env[key] ?? key;
49471
+ if (!key) return void 0;
49472
+ return process.env[key] ?? key;
49469
49473
  }
49470
49474
  function resolveOAuthApiKey(providerId, credential) {
49471
49475
  if (credential.type !== "oauth" || typeof credential.access !== "string" || typeof credential.refresh !== "string" || typeof credential.expires !== "number" || Date.now() >= credential.expires) {
@@ -49511,8 +49515,7 @@ function createFusionAuthStorage() {
49511
49515
  if (prop === "getApiKey") {
49512
49516
  return async (provider) => {
49513
49517
  const primaryKey = await target.getApiKey(provider);
49514
- if (primaryKey)
49515
- return primaryKey;
49518
+ if (primaryKey) return primaryKey;
49516
49519
  return resolveStoredCredentialApiKey(provider, legacyCredentials[provider]);
49517
49520
  };
49518
49521
  }
@@ -49521,12 +49524,12 @@ function createFusionAuthStorage() {
49521
49524
  });
49522
49525
  }
49523
49526
  var init_auth_storage = __esm({
49524
- "../engine/src/auth-storage.js"() {
49527
+ "../engine/src/auth-storage.ts"() {
49525
49528
  "use strict";
49526
49529
  }
49527
49530
  });
49528
49531
 
49529
- // ../engine/src/pi.js
49532
+ // ../engine/src/pi.ts
49530
49533
  var pi_exports = {};
49531
49534
  __export(pi_exports, {
49532
49535
  COMPACTION_FALLBACK_INSTRUCTIONS: () => COMPACTION_FALLBACK_INSTRUCTIONS,
@@ -49540,19 +49543,35 @@ import { existsSync as existsSync20, readFileSync as readFileSync6 } from "node:
49540
49543
  import { exec } from "node:child_process";
49541
49544
  import { promisify as promisify2 } from "node:util";
49542
49545
  import { basename as basename7, dirname as dirname7, join as join24, relative as relative3, isAbsolute as isAbsolute6, resolve as resolve10 } from "node:path";
49543
- import { createAgentSession, createCodingTools, createExtensionRuntime, createReadOnlyTools, DefaultResourceLoader, DefaultPackageManager, discoverAndLoadExtensions, ModelRegistry, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent";
49546
+ import {
49547
+ createAgentSession,
49548
+ createCodingTools,
49549
+ createExtensionRuntime,
49550
+ createReadOnlyTools,
49551
+ DefaultResourceLoader,
49552
+ DefaultPackageManager,
49553
+ discoverAndLoadExtensions,
49554
+ ModelRegistry,
49555
+ SessionManager,
49556
+ SettingsManager
49557
+ } from "@mariozechner/pi-coding-agent";
49544
49558
  function getSessionStateError(session) {
49545
- const error = session.state?.error;
49559
+ const state = session.state;
49560
+ const error = state?.errorMessage ?? state?.error;
49546
49561
  return typeof error === "string" ? error : "";
49547
49562
  }
49548
49563
  function clearSessionStateError(session) {
49549
49564
  const state = session.state;
49550
- if (!state || typeof state !== "object" || !("error" in state)) {
49565
+ if (!state || typeof state !== "object") {
49551
49566
  return;
49552
49567
  }
49553
- try {
49554
- state.error = void 0;
49555
- } catch {
49568
+ for (const key of ["errorMessage", "error"]) {
49569
+ if (key in state) {
49570
+ try {
49571
+ state[key] = void 0;
49572
+ } catch {
49573
+ }
49574
+ }
49556
49575
  }
49557
49576
  }
49558
49577
  async function promptSessionAndCheck(session, prompt, options) {
@@ -49564,6 +49583,29 @@ async function promptSessionAndCheck(session, prompt, options) {
49564
49583
  }
49565
49584
  const stateError = getSessionStateError(session);
49566
49585
  if (stateError) {
49586
+ if (/Cannot read propert(y|ies) of (undefined|null)/i.test(stateError)) {
49587
+ try {
49588
+ const messages = session.agent?.state?.messages ?? session.state?.messages;
49589
+ if (Array.isArray(messages)) {
49590
+ const recent = messages.slice(-6).map((m, idx) => {
49591
+ const i = messages.length - 6 + idx;
49592
+ const content = m?.content;
49593
+ return {
49594
+ index: i < 0 ? idx : i,
49595
+ role: m?.role,
49596
+ contentType: Array.isArray(content) ? `array(len=${content.length})` : typeof content,
49597
+ toolName: m.toolName,
49598
+ stopReason: m.stopReason
49599
+ };
49600
+ });
49601
+ piLog.error(`pi state error \u2014 transcript tail (${messages.length} msgs total): ${JSON.stringify(recent)}`);
49602
+ } else {
49603
+ piLog.error(`pi state error \u2014 state.messages is not an array: ${typeof messages}`);
49604
+ }
49605
+ } catch (inspectErr) {
49606
+ piLog.warn(`pi state error \u2014 failed to inspect transcript: ${inspectErr instanceof Error ? inspectErr.message : String(inspectErr)}`);
49607
+ }
49608
+ }
49567
49609
  throw new Error(stateError);
49568
49610
  }
49569
49611
  }
@@ -49615,8 +49657,7 @@ async function promptWithFallback(session, prompt, options) {
49615
49657
  }
49616
49658
  function describeModel(session) {
49617
49659
  const model = session.model;
49618
- if (!model)
49619
- return "unknown model";
49660
+ if (!model) return "unknown model";
49620
49661
  return `${model.provider}/${model.id}`;
49621
49662
  }
49622
49663
  function compactMarkdownMemorySection(sectionBody) {
@@ -49669,7 +49710,9 @@ async function retryWithCompactedPromptMemory(session, prompt, options) {
49669
49710
  if (!compactedPrompt) {
49670
49711
  return { recovered: false };
49671
49712
  }
49672
- piLog.log(`promptWithFallback: retrying with compacted prompt memory (${prompt.length} \u2192 ${compactedPrompt.length} chars)`);
49713
+ piLog.log(
49714
+ `promptWithFallback: retrying with compacted prompt memory (${prompt.length} \u2192 ${compactedPrompt.length} chars)`
49715
+ );
49673
49716
  try {
49674
49717
  await promptSessionAndCheck(session, compactedPrompt, options);
49675
49718
  piLog.log("promptWithFallback: prompt completed after prompt-memory compaction");
@@ -49686,7 +49729,7 @@ async function flushMemoryBeforeSessionCompaction(session) {
49686
49729
  }
49687
49730
  const flushPrompt = [
49688
49731
  "Before context compaction, preserve only unresolved durable memory if needed.",
49689
- "If memory_append is available and you learned reusable project decisions, conventions, pitfalls, or open loops that are not already saved, append them now.",
49732
+ "If fn_memory_append is available and you learned reusable project decisions, conventions, pitfalls, or open loops that are not already saved, append them now.",
49690
49733
  'Use layer="long-term" for durable facts and layer="daily" for running notes/open loops.',
49691
49734
  "If there is nothing durable to save, reply exactly: NONE."
49692
49735
  ].join("\n");
@@ -49731,7 +49774,9 @@ function resolveConfiguredModel(modelRegistry, kind, provider, modelId) {
49731
49774
  piLog.warn(`${kind} model ${provider}/${modelId} not in registry; using provider base model as template`);
49732
49775
  return { ...baseModel, id: modelId, name: modelId };
49733
49776
  }
49734
- throw new Error(`Configured ${kind} model ${provider}/${modelId} was not found in the pi model registry. Open Settings and choose a model from /api/models, or update your pi model configuration.`);
49777
+ throw new Error(
49778
+ `Configured ${kind} model ${provider}/${modelId} was not found in the pi model registry. Open Settings and choose a model from /api/models, or update your pi model configuration.`
49779
+ );
49735
49780
  }
49736
49781
  function isRetryableModelSelectionError(message) {
49737
49782
  const normalized = message.toLowerCase();
@@ -49776,11 +49821,18 @@ function normalizeAssistantOrToolResultMessage(message) {
49776
49821
  return false;
49777
49822
  }
49778
49823
  const role = message.role;
49779
- if (role !== "assistant" && role !== "toolResult") {
49824
+ if (role !== "assistant" && role !== "toolResult" && role !== "user") {
49780
49825
  return false;
49781
49826
  }
49782
- if (!Array.isArray(message.content)) {
49783
- message.content = [];
49827
+ const obj = message;
49828
+ if (role === "user") {
49829
+ if (typeof obj.content !== "string" && !Array.isArray(obj.content)) {
49830
+ obj.content = [];
49831
+ }
49832
+ return true;
49833
+ }
49834
+ if (!Array.isArray(obj.content)) {
49835
+ obj.content = [];
49784
49836
  }
49785
49837
  return true;
49786
49838
  }
@@ -49837,6 +49889,12 @@ function installMessageContentGuard(session, sessionManager) {
49837
49889
  if (session.__fusionMessageContentGuardInstalled) {
49838
49890
  return;
49839
49891
  }
49892
+ const existingMessages = session.agent?.state?.messages;
49893
+ if (Array.isArray(existingMessages)) {
49894
+ for (const candidate of existingMessages) {
49895
+ normalizeAssistantOrToolResultMessage(candidate);
49896
+ }
49897
+ }
49840
49898
  if (typeof session.subscribe === "function") {
49841
49899
  session.subscribe((event) => {
49842
49900
  if (!event || typeof event !== "object" || event.type !== "message_end") {
@@ -49879,8 +49937,8 @@ function createReadOnlyPiSettingsView(cwd, agentDir) {
49879
49937
  const fusionProjectSettings = readJsonObject2(join24(projectRoot, ".fusion", "settings.json"));
49880
49938
  const mergedSettings = { ...globalSettings, ...fusionProjectSettings };
49881
49939
  return {
49882
- getGlobalSettings: () => globalThis.structuredClone(globalSettings),
49883
- getProjectSettings: () => globalThis.structuredClone(fusionProjectSettings),
49940
+ getGlobalSettings: () => structuredClone(globalSettings),
49941
+ getProjectSettings: () => structuredClone(fusionProjectSettings),
49884
49942
  getNpmCommand: () => Array.isArray(mergedSettings.npmCommand) ? [...mergedSettings.npmCommand] : void 0
49885
49943
  };
49886
49944
  }
@@ -49907,7 +49965,11 @@ async function registerExtensionProviders(cwd, modelRegistry) {
49907
49965
  });
49908
49966
  const resolvedPaths = await packageManager.resolve();
49909
49967
  const packageExtensionPaths = resolvedPaths.extensions.filter((resource) => resource.enabled).map((resource) => resource.path);
49910
- const extensionsResult = await discoverAndLoadExtensions([...getEnabledPiExtensionPaths(cwd), ...packageExtensionPaths], cwd, join24(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery"));
49968
+ const extensionsResult = await discoverAndLoadExtensions(
49969
+ [...getEnabledPiExtensionPaths(cwd), ...packageExtensionPaths],
49970
+ cwd,
49971
+ join24(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery")
49972
+ );
49911
49973
  for (const { path, error } of extensionsResult.errors) {
49912
49974
  extensionsLog.warn(`Failed to load ${path}: ${error}`);
49913
49975
  }
@@ -49942,7 +50004,9 @@ async function isRegisteredGitWorktree(projectRoot, worktreePath) {
49942
50004
  encoding: "utf-8"
49943
50005
  });
49944
50006
  const resolvedWorktree = resolve10(worktreePath);
49945
- return stdout.split("\n").some((line) => line.startsWith("worktree ") && resolve10(line.slice("worktree ".length)) === resolvedWorktree);
50007
+ return stdout.split("\n").some(
50008
+ (line) => line.startsWith("worktree ") && resolve10(line.slice("worktree ".length)) === resolvedWorktree
50009
+ );
49946
50010
  } catch {
49947
50011
  return false;
49948
50012
  }
@@ -49969,7 +50033,7 @@ async function assertValidWorktreeSession(cwd, projectRoot) {
49969
50033
  throw new Error(`Refusing to start coding agent in unregistered git worktree: ${cwd}`);
49970
50034
  }
49971
50035
  }
49972
- function isWorktreeAllowedPath(worktreePath, projectRoot, requestedPath) {
50036
+ function isWorktreeAllowedPath(worktreePath, projectRoot, requestedPath, toolName) {
49973
50037
  const worktreeResolved = resolve10(worktreePath);
49974
50038
  const projectRootResolved = resolve10(projectRoot);
49975
50039
  const requestedResolved = isAbsolute6(requestedPath) ? resolve10(requestedPath) : resolve10(worktreeResolved, requestedPath);
@@ -49984,8 +50048,20 @@ function isWorktreeAllowedPath(worktreePath, projectRoot, requestedPath) {
49984
50048
  if (relToProjectRoot.match(/^\.fusion\/tasks\/[^/]+\/attachments\//)) {
49985
50049
  return true;
49986
50050
  }
50051
+ const readOnlyTools = /* @__PURE__ */ new Set(["read", "glob", "grep"]);
50052
+ if (toolName && readOnlyTools.has(toolName) && /^\.fusion\/tasks\/[^/]+\/(PROMPT\.md|task\.json)$/.test(relToProjectRoot)) {
50053
+ return true;
50054
+ }
49987
50055
  return false;
49988
50056
  }
50057
+ function boundaryRejection(message) {
50058
+ return {
50059
+ content: [{ type: "text", text: message }],
50060
+ isError: true,
50061
+ ok: false,
50062
+ error: message
50063
+ };
50064
+ }
49989
50065
  function wrapToolsWithBoundary(tools, worktreePath, projectRoot) {
49990
50066
  if (!worktreePath || !projectRoot) {
49991
50067
  return tools;
@@ -49999,21 +50075,21 @@ function wrapToolsWithBoundary(tools, worktreePath, projectRoot) {
49999
50075
  return {
50000
50076
  ...tool,
50001
50077
  execute: async (...args) => {
50078
+ const _toolCallId = args[0];
50002
50079
  const params = args[1];
50080
+ const _signal = args[2];
50003
50081
  const pathArg = params.path;
50004
- if (pathArg && !isWorktreeAllowedPath(worktreePath, projectRoot, pathArg)) {
50082
+ if (pathArg && !isWorktreeAllowedPath(worktreePath, projectRoot, pathArg, tool.name)) {
50005
50083
  const relToProject = relative3(projectRoot, pathArg);
50006
- return {
50007
- ok: false,
50008
- error: `Path "${relToProject}" is outside the worktree boundary. Coding agents can only modify files inside the current worktree. Exception: .fusion/memory/ (project root) and .fusion/tasks/*/attachments/* are permitted for reading.`
50009
- };
50084
+ return boundaryRejection(
50085
+ `Path "${relToProject}" is outside the worktree boundary. Coding agents can only modify files inside the current worktree. Exceptions (read-only): .fusion/memory/, .fusion/tasks/*/attachments/, and .fusion/tasks/*/{PROMPT.md,task.json} for dependency context.`
50086
+ );
50010
50087
  }
50011
50088
  const cwdArg = params.cwd;
50012
- if (tool.name === "bash" && cwdArg && !isWorktreeAllowedPath(worktreePath, projectRoot, cwdArg)) {
50013
- return {
50014
- ok: false,
50015
- error: `Working directory is outside the worktree boundary. Commands must run inside the worktree.`
50016
- };
50089
+ if (tool.name === "bash" && cwdArg && !isWorktreeAllowedPath(worktreePath, projectRoot, cwdArg, tool.name)) {
50090
+ return boundaryRejection(
50091
+ `Working directory is outside the worktree boundary. Commands must run inside the worktree.`
50092
+ );
50017
50093
  }
50018
50094
  return originalExecute(...args);
50019
50095
  }
@@ -50023,7 +50099,7 @@ function wrapToolsWithBoundary(tools, worktreePath, projectRoot) {
50023
50099
  async function createFnAgent2(options) {
50024
50100
  piLog.log(`createFnAgent called (cwd=${options.cwd}, tools=${options.tools}, provider=${options.defaultProvider}, model=${options.defaultModelId})`);
50025
50101
  const authStorage = createFusionAuthStorage();
50026
- const modelRegistry = new ModelRegistry(authStorage, getModelRegistryModelsPath());
50102
+ const modelRegistry = ModelRegistry.create(authStorage, getModelRegistryModelsPath());
50027
50103
  await registerExtensionProviders(options.cwd, modelRegistry);
50028
50104
  const tools = options.tools === "readonly" ? createReadOnlyTools(options.cwd) : createCodingTools(options.cwd);
50029
50105
  const worktreePath = options.cwd;
@@ -50036,8 +50112,18 @@ async function createFnAgent2(options) {
50036
50112
  compaction: { enabled: true },
50037
50113
  retry: { enabled: true, maxRetries: 3 }
50038
50114
  });
50039
- const selectedModel = resolveConfiguredModel(modelRegistry, "primary", options.defaultProvider, options.defaultModelId);
50040
- const fallbackModel = resolveConfiguredModel(modelRegistry, "fallback", options.fallbackProvider, options.fallbackModelId);
50115
+ const selectedModel = resolveConfiguredModel(
50116
+ modelRegistry,
50117
+ "primary",
50118
+ options.defaultProvider,
50119
+ options.defaultModelId
50120
+ );
50121
+ const fallbackModel = resolveConfiguredModel(
50122
+ modelRegistry,
50123
+ "fallback",
50124
+ options.fallbackProvider,
50125
+ options.fallbackModelId
50126
+ );
50041
50127
  let effectiveSkillSelection = options.skillSelection;
50042
50128
  if (!effectiveSkillSelection && options.skills && options.skills.length > 0) {
50043
50129
  piLog.log(`Using skills from convenience parameter: [${options.skills.join(", ")}]`);
@@ -50063,6 +50149,7 @@ async function createFnAgent2(options) {
50063
50149
  }
50064
50150
  const resourceLoader = new DefaultResourceLoader({
50065
50151
  cwd: options.cwd,
50152
+ agentDir: getFusionAgentDir(),
50066
50153
  settingsManager,
50067
50154
  systemPromptOverride: () => options.systemPrompt,
50068
50155
  appendSystemPromptOverride: () => [],
@@ -50072,13 +50159,17 @@ async function createFnAgent2(options) {
50072
50159
  const sessionManager = options.sessionManager ?? SessionManager.inMemory();
50073
50160
  normalizeSessionHistoryEntries(sessionManager);
50074
50161
  const createSessionWithModel = async (modelOverride) => {
50162
+ const customToolList = [
50163
+ ...wrappedTools,
50164
+ ...options.customTools ?? []
50165
+ ];
50075
50166
  return createAgentSession({
50076
50167
  cwd: options.cwd,
50077
50168
  authStorage,
50078
50169
  modelRegistry,
50079
50170
  resourceLoader,
50080
- tools: wrappedTools,
50081
- customTools: options.customTools,
50171
+ noTools: "builtin",
50172
+ customTools: customToolList,
50082
50173
  sessionManager,
50083
50174
  settingsManager,
50084
50175
  ...modelOverride ? { model: modelOverride } : {}
@@ -50102,7 +50193,7 @@ async function createFnAgent2(options) {
50102
50193
  const { session } = sessionResult;
50103
50194
  installToolResultContentGuard(session);
50104
50195
  installMessageContentGuard(session, sessionManager);
50105
- session.__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === "memory_append") === true;
50196
+ session.__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === FN_MEMORY_APPEND_TOOL_NAME) === true;
50106
50197
  const promptableSession = session;
50107
50198
  promptableSession.promptWithFallback = async (prompt, promptOptions) => {
50108
50199
  try {
@@ -50152,8 +50243,11 @@ async function createFnAgent2(options) {
50152
50243
  const fallbackSessionResult = await createSessionWithModel(fallbackModel);
50153
50244
  const fallbackSession = fallbackSessionResult.session;
50154
50245
  installToolResultContentGuard(fallbackSession);
50155
- installMessageContentGuard(fallbackSession, sessionManager);
50156
- fallbackSession.__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === "memory_append") === true;
50246
+ installMessageContentGuard(
50247
+ fallbackSession,
50248
+ sessionManager
50249
+ );
50250
+ fallbackSession.__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === FN_MEMORY_APPEND_TOOL_NAME) === true;
50157
50251
  if (options.defaultThinkingLevel) {
50158
50252
  fallbackSession.setThinkingLevel(options.defaultThinkingLevel);
50159
50253
  }
@@ -50235,9 +50329,9 @@ async function createFnAgent2(options) {
50235
50329
  });
50236
50330
  return { session: promptableSession, sessionFile: promptableSession.sessionFile };
50237
50331
  }
50238
- var execAsync, COMPACTION_FALLBACK_INSTRUCTIONS, MAX_COMPACTED_PROMPT_MEMORY_CHARS;
50332
+ var execAsync, FN_MEMORY_APPEND_TOOL_NAME, COMPACTION_FALLBACK_INSTRUCTIONS, MAX_COMPACTED_PROMPT_MEMORY_CHARS;
50239
50333
  var init_pi = __esm({
50240
- "../engine/src/pi.js"() {
50334
+ "../engine/src/pi.ts"() {
50241
50335
  "use strict";
50242
50336
  init_src();
50243
50337
  init_skill_resolver();
@@ -50245,6 +50339,7 @@ var init_pi = __esm({
50245
50339
  init_auth_storage();
50246
50340
  init_logger2();
50247
50341
  execAsync = promisify2(exec);
50342
+ FN_MEMORY_APPEND_TOOL_NAME = "fn_memory_append";
50248
50343
  COMPACTION_FALLBACK_INSTRUCTIONS = [
50249
50344
  "Summarize all completed steps concisely.",
50250
50345
  "Preserve the current step number and any in-progress work details.",
@@ -63639,6 +63734,13 @@ function formatTaskIdentifier(task) {
63639
63734
  const snippet = task.description.length > maxLen ? task.description.slice(0, maxLen) + "..." : task.description;
63640
63735
  return `${task.id}: ${snippet}`;
63641
63736
  }
63737
+ function resolveNtfyBaseUrl(baseUrl, fallback = DEFAULT_NTFY_BASE_URL) {
63738
+ const trimmed = baseUrl?.trim();
63739
+ if (!trimmed) {
63740
+ return fallback;
63741
+ }
63742
+ return trimmed.replace(/\/+$/, "");
63743
+ }
63642
63744
  function resolveNtfyEvents(events) {
63643
63745
  return events ? [...events] : [...DEFAULT_NTFY_EVENTS];
63644
63746
  }
@@ -63662,7 +63764,7 @@ function buildNtfyClickUrl(options) {
63662
63764
  return query ? `${normalizedHost}/?${query}` : `${normalizedHost}/`;
63663
63765
  }
63664
63766
  async function sendNtfyNotification({
63665
- ntfyBaseUrl = "https://ntfy.sh",
63767
+ ntfyBaseUrl,
63666
63768
  topic,
63667
63769
  title,
63668
63770
  message,
@@ -63679,7 +63781,8 @@ async function sendNtfyNotification({
63679
63781
  if (clickUrl) {
63680
63782
  headers.Click = clickUrl;
63681
63783
  }
63682
- const response = await fetch(`${ntfyBaseUrl}/${topic}`, {
63784
+ const resolvedBaseUrl = resolveNtfyBaseUrl(ntfyBaseUrl);
63785
+ const response = await fetch(`${resolvedBaseUrl}/${topic}`, {
63683
63786
  method: "POST",
63684
63787
  headers,
63685
63788
  body: message,
@@ -63695,11 +63798,12 @@ async function sendNtfyNotification({
63695
63798
  schedulerLog.log(`Failed to send ntfy notification: ${err}`);
63696
63799
  }
63697
63800
  }
63698
- var DEFAULT_NTFY_EVENTS, NtfyNotifier;
63801
+ var DEFAULT_NTFY_BASE_URL, DEFAULT_NTFY_EVENTS, NtfyNotifier;
63699
63802
  var init_notifier = __esm({
63700
63803
  "../engine/src/notifier.ts"() {
63701
63804
  "use strict";
63702
63805
  init_logger2();
63806
+ DEFAULT_NTFY_BASE_URL = "https://ntfy.sh";
63703
63807
  DEFAULT_NTFY_EVENTS = [
63704
63808
  "in-review",
63705
63809
  "merged",
@@ -63711,7 +63815,8 @@ var init_notifier = __esm({
63711
63815
  NtfyNotifier = class {
63712
63816
  constructor(store, options = {}) {
63713
63817
  this.store = store;
63714
- this.ntfyBaseUrl = options.ntfyBaseUrl ?? "https://ntfy.sh";
63818
+ this.defaultNtfyBaseUrl = resolveNtfyBaseUrl(options.ntfyBaseUrl);
63819
+ this.ntfyBaseUrl = this.defaultNtfyBaseUrl;
63715
63820
  this.projectId = options.projectId;
63716
63821
  }
63717
63822
  config = {
@@ -63721,6 +63826,7 @@ var init_notifier = __esm({
63721
63826
  events: [...DEFAULT_NTFY_EVENTS]
63722
63827
  };
63723
63828
  ntfyBaseUrl;
63829
+ defaultNtfyBaseUrl;
63724
63830
  projectId;
63725
63831
  notifiedEvents = /* @__PURE__ */ new Set();
63726
63832
  abortController = null;
@@ -63859,7 +63965,7 @@ var init_notifier = __esm({
63859
63965
  };
63860
63966
  handleSettingsUpdated = (data) => {
63861
63967
  const { settings, previous } = data;
63862
- if (settings.ntfyEnabled !== previous.ntfyEnabled || settings.ntfyTopic !== previous.ntfyTopic || settings.ntfyDashboardHost !== previous.ntfyDashboardHost || JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
63968
+ if (settings.ntfyEnabled !== previous.ntfyEnabled || settings.ntfyTopic !== previous.ntfyTopic || settings.ntfyBaseUrl !== previous.ntfyBaseUrl || settings.ntfyDashboardHost !== previous.ntfyDashboardHost || JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
63863
63969
  const wasEnabled = this.config.enabled;
63864
63970
  this.loadConfig(settings);
63865
63971
  if (this.config.enabled && !wasEnabled) {
@@ -63868,6 +63974,8 @@ var init_notifier = __esm({
63868
63974
  schedulerLog.log("NtfyNotifier disabled");
63869
63975
  } else if (this.config.topic !== previous.ntfyTopic) {
63870
63976
  schedulerLog.log("NtfyNotifier topic updated");
63977
+ } else if (this.ntfyBaseUrl !== resolveNtfyBaseUrl(previous.ntfyBaseUrl)) {
63978
+ schedulerLog.log("NtfyNotifier base URL updated");
63871
63979
  } else if (this.config.dashboardHost !== previous.ntfyDashboardHost) {
63872
63980
  schedulerLog.log("NtfyNotifier dashboard host updated");
63873
63981
  } else if (JSON.stringify(this.config.events) !== JSON.stringify(previous.ntfyEvents)) {
@@ -63882,6 +63990,7 @@ var init_notifier = __esm({
63882
63990
  dashboardHost: settings.ntfyDashboardHost,
63883
63991
  events: resolveNtfyEvents(settings.ntfyEvents)
63884
63992
  };
63993
+ this.ntfyBaseUrl = resolveNtfyBaseUrl(settings.ntfyBaseUrl, this.defaultNtfyBaseUrl);
63885
63994
  }
63886
63995
  isEventEnabled(event) {
63887
63996
  return isNtfyEventEnabled(this.config.events, event);