@bastani/atomic 0.8.25 → 0.8.26-alpha.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 (74) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +12 -0
  3. package/dist/builtin/intercom/index-heavy.ts +1754 -0
  4. package/dist/builtin/intercom/index.ts +374 -1746
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/intercom/result-renderers.ts +77 -0
  7. package/dist/builtin/mcp/CHANGELOG.md +16 -0
  8. package/dist/builtin/mcp/index.ts +151 -57
  9. package/dist/builtin/mcp/package.json +1 -1
  10. package/dist/builtin/subagents/CHANGELOG.md +13 -0
  11. package/dist/builtin/subagents/package.json +1 -1
  12. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +8 -3
  13. package/dist/builtin/subagents/src/runs/foreground/execution.ts +42 -4
  14. package/dist/builtin/subagents/src/runs/shared/acceptance.ts +2 -1
  15. package/dist/builtin/subagents/src/runs/shared/worktree.ts +2 -2
  16. package/dist/builtin/web-access/CHANGELOG.md +12 -0
  17. package/dist/builtin/web-access/index-heavy.ts +2060 -0
  18. package/dist/builtin/web-access/index.ts +182 -2274
  19. package/dist/builtin/web-access/package.json +1 -1
  20. package/dist/builtin/web-access/result-renderers.ts +364 -0
  21. package/dist/builtin/workflows/CHANGELOG.md +21 -0
  22. package/dist/builtin/workflows/package.json +1 -1
  23. package/dist/builtin/workflows/skills/research-codebase/SKILL.md +28 -9
  24. package/dist/builtin/workflows/src/extension/index.ts +13 -3
  25. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +59 -3
  26. package/dist/builtin/workflows/src/runs/shared/worktree.ts +2 -2
  27. package/dist/builtin/workflows/src/shared/store.ts +61 -7
  28. package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +12 -3
  29. package/dist/builtin/workflows/src/tui/inline-form-store.ts +17 -6
  30. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +37 -2
  31. package/dist/core/agent-session-services.d.ts.map +1 -1
  32. package/dist/core/agent-session-services.js +13 -0
  33. package/dist/core/agent-session-services.js.map +1 -1
  34. package/dist/core/extensions/loader.d.ts.map +1 -1
  35. package/dist/core/extensions/loader.js +7 -0
  36. package/dist/core/extensions/loader.js.map +1 -1
  37. package/dist/core/extensions/types.d.ts +13 -1
  38. package/dist/core/extensions/types.d.ts.map +1 -1
  39. package/dist/core/extensions/types.js.map +1 -1
  40. package/dist/core/footer-data-provider.d.ts.map +1 -1
  41. package/dist/core/footer-data-provider.js +3 -0
  42. package/dist/core/footer-data-provider.js.map +1 -1
  43. package/dist/core/package-manager.d.ts.map +1 -1
  44. package/dist/core/package-manager.js +14 -7
  45. package/dist/core/package-manager.js.map +1 -1
  46. package/dist/core/resource-loader.d.ts.map +1 -1
  47. package/dist/core/resource-loader.js +17 -0
  48. package/dist/core/resource-loader.js.map +1 -1
  49. package/dist/core/timings.d.ts +9 -0
  50. package/dist/core/timings.d.ts.map +1 -1
  51. package/dist/core/timings.js +28 -1
  52. package/dist/core/timings.js.map +1 -1
  53. package/dist/index.d.ts +1 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +1 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/main.d.ts.map +1 -1
  58. package/dist/main.js +4 -2
  59. package/dist/main.js.map +1 -1
  60. package/dist/modes/interactive/components/custom-message.d.ts +1 -0
  61. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  62. package/dist/modes/interactive/components/custom-message.js +36 -4
  63. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  64. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  65. package/dist/modes/interactive/components/footer.js +4 -1
  66. package/dist/modes/interactive/components/footer.js.map +1 -1
  67. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  68. package/dist/modes/interactive/interactive-mode.js +22 -9
  69. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  70. package/dist/utils/git-env.d.ts +10 -0
  71. package/dist/utils/git-env.d.ts.map +1 -0
  72. package/dist/utils/git-env.js +33 -0
  73. package/dist/utils/git-env.js.map +1 -0
  74. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.8.25",
3
+ "version": "0.8.26-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
6
6
  "contributors": [
@@ -0,0 +1,77 @@
1
+ import type { ToolDefinition } from "@bastani/atomic";
2
+ import { Text } from "@mariozechner/pi-tui";
3
+
4
+ type ToolResultRenderer = NonNullable<ToolDefinition["renderResult"]>;
5
+ type ToolRenderResultArgs = Parameters<ToolResultRenderer>;
6
+ type ToolRenderResult = ReturnType<ToolResultRenderer>;
7
+ type RenderedResult = ToolRenderResultArgs[0];
8
+ type TextContentBlock = Extract<RenderedResult["content"][number], { type: "text" }>;
9
+
10
+ type IntercomResultDetails = {
11
+ delivered?: boolean;
12
+ error?: boolean;
13
+ messageId?: string;
14
+ reason?: string;
15
+ };
16
+
17
+ type ContactSupervisorResultDetails = IntercomResultDetails & {
18
+ structuredReplyParseError?: string;
19
+ };
20
+
21
+ function isTextContentBlock(block: RenderedResult["content"][number]): block is TextContentBlock {
22
+ return block.type === "text";
23
+ }
24
+
25
+ function firstTextContent(result: RenderedResult): string {
26
+ return result.content.find(isTextContentBlock)?.text.replace(/\*\*/g, "") ?? "";
27
+ }
28
+
29
+ export const renderContactSupervisorResult: ToolResultRenderer = (result, { isPartial }, theme, context) => {
30
+ if (isPartial) {
31
+ return new Text(theme.fg("warning", "Waiting for supervisor..."), 0, 0);
32
+ }
33
+ const details = result.details as ContactSupervisorResultDetails | undefined;
34
+ const textContent = firstTextContent(result);
35
+ const failed = Boolean(context.isError || details?.error === true || details?.delivered === false);
36
+ const parseWarning = typeof details?.structuredReplyParseError === "string";
37
+ let text = failed
38
+ ? theme.fg("error", "✗ ")
39
+ : parseWarning
40
+ ? theme.fg("warning", "⚠ ")
41
+ : theme.fg("success", "✓ ");
42
+ text += theme.fg(failed ? "error" : "text", textContent);
43
+ if (parseWarning) {
44
+ text += "\n" + theme.fg("warning", `Structured reply parse issue: ${details.structuredReplyParseError}`);
45
+ }
46
+ return new Text(text, 0, 0);
47
+ };
48
+
49
+ export const renderIntercomResult: ToolResultRenderer = (result, { isPartial }, theme, context) => {
50
+ if (isPartial) {
51
+ return new Text(theme.fg("warning", "Intercom working..."), 0, 0);
52
+ }
53
+ const details = result.details as IntercomResultDetails | undefined;
54
+ const failed = Boolean(context.isError || details?.error === true || details?.delivered === false);
55
+ let text = failed ? theme.fg("error", "✗ ") : theme.fg("success", "✓ ");
56
+ text += theme.fg(failed ? "error" : "text", firstTextContent(result));
57
+ if (details?.messageId && !context.expanded) {
58
+ text += theme.fg("dim", ` (${details.messageId.slice(0, 8)})`);
59
+ }
60
+ if (details?.reason && context.expanded) {
61
+ text += "\n" + theme.fg("dim", `Reason: ${details.reason}`);
62
+ }
63
+ return new Text(text, 0, 0);
64
+ };
65
+
66
+ export function renderIntercomToolResult(name: string, args: ToolRenderResultArgs): ToolRenderResult {
67
+ switch (name) {
68
+ case "intercom":
69
+ return renderIntercomResult(...args);
70
+ case "contact_supervisor":
71
+ return renderContactSupervisorResult(...args);
72
+ default: {
73
+ const theme = args[2];
74
+ return new Text(theme.fg("error", `Result renderer not found: ${name}`), 0, 0);
75
+ }
76
+ }
77
+ }
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.26-alpha.2] - 2026-06-05
11
+
12
+ ### Changed
13
+
14
+ - Bumped package version for the Atomic 0.8.26-alpha.2 prerelease.
15
+
16
+ ## [0.8.26-alpha.1] - 2026-06-05
17
+
18
+ ### Changed
19
+
20
+ - Kept MCP startup registration cheap by lazily importing the heavy MCP command, initialization, proxy-mode, and OAuth/auth-flow modules (and deferring config/cache-backed direct-tool discovery) instead of loading them on the cold extension factory path, while keeping the result renderer synchronous so restored MCP tool results still render ([#1223](https://github.com/bastani-inc/atomic/issues/1223)).
21
+
22
+ ### Fixed
23
+
24
+ - Stopped logging a spurious "MCP initialization failed: This extension ctx is stale..." error when a session backing the captured `pi`/`ctx` is disposed (e.g. a workflow child stage session) during MCP's deferred `session_start` initialization. The deferred init now liveness-checks the captured context before and after `initializeMcp()` and treats a stale-context error as a cancellation rather than a failure. The async gap that exposed this race was introduced by the lazy-load startup change ([#1223](https://github.com/bastani-inc/atomic/issues/1223)).
25
+
10
26
  ## [0.8.25] - 2026-06-04
11
27
 
12
28
  ### Changed
@@ -1,20 +1,93 @@
1
1
  import type { AgentToolUpdateCallback, ExtensionAPI, ExtensionContext, ToolInfo } from "@bastani/atomic";
2
2
  import type { McpExtensionState } from "./state.ts";
3
+ import type { McpConfig } from "./types.ts";
4
+ import type { MetadataCache } from "./metadata-cache.ts";
3
5
  import { Type } from "typebox";
4
- import { showStatus, showTools, reconnectServers, authenticateServer, logoutServer, openMcpAuthPanel, openMcpPanel, openMcpSetup } from "./commands.ts";
5
6
  import { loadMcpConfig } from "./config.ts";
6
- import { buildProxyDescription, createDirectToolExecutor, getMissingConfiguredDirectToolServers, resolveDirectTools } from "./direct-tools.ts";
7
- import { flushMetadataCache, initializeMcp, updateStatusBar } from "./init.ts";
8
- import { loadMetadataCache } from "./metadata-cache.ts";
9
- import { executeCall, executeConnect, executeDescribe, executeList, executeSearch, executeStatus, executeUiMessages } from "./proxy-modes.ts";
10
- import { getConfigPathFromArgv, truncateAtWord } from "./utils.ts";
11
- import { shutdownOAuth } from "./mcp-auth-flow.ts";
7
+ import { getConfigPathFromArgv } from "./utils.ts";
12
8
  import { renderMcpToolResult } from "./tool-result-renderer.ts";
13
9
 
10
+ /**
11
+ * Marker substring from the host's stale-context error (see ExtensionRunner.invalidate).
12
+ * A captured `pi`/`ctx` becomes stale when its backing session is disposed (e.g. a
13
+ * workflow child stage session, or a reload/replace) without emitting `session_shutdown`.
14
+ */
15
+ const STALE_EXTENSION_CONTEXT_MARKER = "extension ctx is stale";
16
+
17
+ function isStaleExtensionContextError(error: unknown): boolean {
18
+ return error instanceof Error && error.message.includes(STALE_EXTENSION_CONTEXT_MARKER);
19
+ }
20
+
21
+ /**
22
+ * Probe whether a captured extension context is still active. Every `ctx` getter runs
23
+ * the host's `assertActive()` guard, so a cheap property read surfaces staleness without
24
+ * mutating anything. Returns false when the context has been invalidated by a dispose.
25
+ */
26
+ function isContextActive(ctx: ExtensionContext): boolean {
27
+ try {
28
+ void ctx.cwd;
29
+ return true;
30
+ } catch (error) {
31
+ if (isStaleExtensionContextError(error)) return false;
32
+ throw error;
33
+ }
34
+ }
35
+
14
36
  export default function mcpAdapter(pi: ExtensionAPI) {
15
37
  let state: McpExtensionState | null = null;
16
38
  let initPromise: Promise<McpExtensionState> | null = null;
17
39
  let lifecycleGeneration = 0;
40
+ let registeredDirectToolNames = new Set<string>();
41
+ let registeredProxyTool = false;
42
+
43
+ async function registerDirectToolsFromConfig(
44
+ config: McpConfig,
45
+ cache: MetadataCache | null,
46
+ ): Promise<{ directToolCount: number; missingConfiguredDirectToolServers: string[] }> {
47
+ const [{ resolveDirectTools, createDirectToolExecutor, getMissingConfiguredDirectToolServers }, { truncateAtWord }] = await Promise.all([
48
+ import("./direct-tools.ts"),
49
+ import("./utils.ts"),
50
+ ]);
51
+ const prefix = config.settings?.toolPrefix ?? "server";
52
+ const envRaw = process.env.MCP_DIRECT_TOOLS;
53
+ const directSpecs = envRaw === "__none__"
54
+ ? []
55
+ : resolveDirectTools(
56
+ config,
57
+ cache,
58
+ prefix,
59
+ envRaw?.split(",").map(s => s.trim()).filter(Boolean),
60
+ );
61
+ for (const spec of directSpecs) {
62
+ if (registeredDirectToolNames.has(spec.prefixedName)) continue;
63
+ registeredDirectToolNames.add(spec.prefixedName);
64
+ (pi.registerTool as (tool: unknown) => unknown)({
65
+ name: spec.prefixedName,
66
+ label: `MCP: ${spec.originalName}`,
67
+ description: spec.description || "(no description)",
68
+ promptSnippet: truncateAtWord(spec.description, 100) || `MCP tool from ${spec.serverName}`,
69
+ parameters: Type.Unsafe((spec.inputSchema || { type: "object", properties: {} }) as never),
70
+ execute: createDirectToolExecutor(() => state, () => initPromise, spec),
71
+ renderResult: renderMcpToolResult,
72
+ });
73
+ }
74
+ const refreshTools = (pi as { refreshTools?: () => void }).refreshTools;
75
+ refreshTools?.();
76
+ return {
77
+ directToolCount: directSpecs.length,
78
+ missingConfiguredDirectToolServers: getMissingConfiguredDirectToolServers(config, cache),
79
+ };
80
+ }
81
+
82
+ async function registerDirectTools(nextState: McpExtensionState): Promise<{ directToolCount: number; missingConfiguredDirectToolServers: string[] }> {
83
+ const { loadMetadataCache } = await import("./metadata-cache.ts");
84
+ return registerDirectToolsFromConfig(nextState.config, loadMetadataCache());
85
+ }
86
+
87
+ async function shutdownOAuthFlow(): Promise<void> {
88
+ const { shutdownOAuth } = await import("./mcp-auth-flow.ts");
89
+ await shutdownOAuth();
90
+ }
18
91
 
19
92
  async function shutdownState(currentState: McpExtensionState | null, reason: string): Promise<void> {
20
93
  if (!currentState) return;
@@ -26,6 +99,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
26
99
 
27
100
  let flushError: unknown;
28
101
  try {
102
+ const { flushMetadataCache } = await import("./init.ts");
29
103
  flushMetadataCache(currentState);
30
104
  } catch (error) {
31
105
  flushError = error;
@@ -47,36 +121,6 @@ export default function mcpAdapter(pi: ExtensionAPI) {
47
121
  }
48
122
 
49
123
  const earlyConfigPath = getConfigPathFromArgv();
50
- const earlyConfig = loadMcpConfig(earlyConfigPath);
51
- const earlyCache = loadMetadataCache();
52
- const prefix = earlyConfig.settings?.toolPrefix ?? "server";
53
-
54
- const envRaw = process.env.MCP_DIRECT_TOOLS;
55
- const directSpecs = envRaw === "__none__"
56
- ? []
57
- : resolveDirectTools(
58
- earlyConfig,
59
- earlyCache,
60
- prefix,
61
- envRaw?.split(",").map(s => s.trim()).filter(Boolean),
62
- );
63
- const missingConfiguredDirectToolServers = getMissingConfiguredDirectToolServers(earlyConfig, earlyCache);
64
- const shouldRegisterProxyTool =
65
- earlyConfig.settings?.disableProxyTool !== true
66
- || directSpecs.length === 0
67
- || missingConfiguredDirectToolServers.length > 0;
68
-
69
- for (const spec of directSpecs) {
70
- (pi.registerTool as (tool: unknown) => unknown)({
71
- name: spec.prefixedName,
72
- label: `MCP: ${spec.originalName}`,
73
- description: spec.description || "(no description)",
74
- promptSnippet: truncateAtWord(spec.description, 100) || `MCP tool from ${spec.serverName}`,
75
- parameters: Type.Unsafe((spec.inputSchema || { type: "object", properties: {} }) as never),
76
- execute: createDirectToolExecutor(() => state, () => initPromise, spec),
77
- renderResult: renderMcpToolResult,
78
- });
79
- }
80
124
 
81
125
  const getPiTools = (): ToolInfo[] => pi.getAllTools();
82
126
 
@@ -90,45 +134,89 @@ export default function mcpAdapter(pi: ExtensionAPI) {
90
134
  const previousState = state;
91
135
  state = null;
92
136
  initPromise = null;
137
+ registeredDirectToolNames = new Set<string>();
93
138
 
94
139
  try {
95
- await Promise.all([
96
- shutdownState(previousState, "session_restart"),
97
- shutdownOAuth(),
98
- ]);
140
+ const config = loadMcpConfig(earlyConfigPath, ctx.cwd);
141
+ const { loadMetadataCache } = await import("./metadata-cache.ts");
142
+ const directToolState = await registerDirectToolsFromConfig(config, loadMetadataCache());
143
+ if (
144
+ config.settings?.disableProxyTool !== true
145
+ || directToolState.directToolCount === 0
146
+ || directToolState.missingConfiguredDirectToolServers.length > 0
147
+ ) {
148
+ registerProxyTool();
149
+ }
99
150
  } catch (error) {
100
- console.error("MCP: failed to shut down previous session state", error);
151
+ if (isStaleExtensionContextError(error)) return;
152
+ console.error("MCP: failed to register cached startup tools; enabling MCP proxy fallback", error);
153
+ registerProxyTool();
101
154
  }
102
155
 
103
- if (generation !== lifecycleGeneration) {
104
- return;
105
- }
156
+ const promiseRef: { current: Promise<McpExtensionState> | null } = { current: null };
157
+ const promise = (async () => {
158
+ try {
159
+ await Promise.all([
160
+ shutdownState(previousState, "session_restart"),
161
+ shutdownOAuthFlow(),
162
+ ]);
163
+ } catch (error) {
164
+ console.error("MCP: failed to shut down previous session state", error);
165
+ }
106
166
 
107
- const promise = initializeMcp(pi, ctx);
108
- initPromise = promise;
167
+ if (generation !== lifecycleGeneration || !isContextActive(ctx)) {
168
+ throw new Error("Stale MCP session initialization cancelled before startup");
169
+ }
109
170
 
110
- promise.then(async (nextState) => {
111
- if (generation !== lifecycleGeneration || initPromise !== promise) {
171
+ const { initializeMcp, updateStatusBar } = await import("./init.ts");
172
+ if (generation !== lifecycleGeneration || !isContextActive(ctx)) {
173
+ throw new Error("Stale MCP session initialization cancelled before startup");
174
+ }
175
+
176
+ const nextState = await initializeMcp(pi, ctx);
177
+ if (generation !== lifecycleGeneration || initPromise !== promiseRef.current || !isContextActive(ctx)) {
112
178
  try {
113
179
  await shutdownState(nextState, "stale_session_start");
114
180
  } catch (error) {
115
181
  console.error("MCP: failed to clean stale session state", error);
116
182
  }
117
- return;
183
+ throw new Error("Stale MCP session initialization cancelled after startup");
118
184
  }
119
185
 
120
186
  state = nextState;
121
187
  updateStatusBar(nextState);
122
- initPromise = null;
123
- }).catch(err => {
188
+ const directToolState = await registerDirectTools(nextState);
189
+ if (
190
+ nextState.config.settings?.disableProxyTool !== true
191
+ || directToolState.directToolCount === 0
192
+ || directToolState.missingConfiguredDirectToolServers.length > 0
193
+ ) {
194
+ registerProxyTool();
195
+ }
196
+ if (initPromise === promiseRef.current) {
197
+ initPromise = null;
198
+ }
199
+ return nextState;
200
+ })();
201
+ promiseRef.current = promise;
202
+ initPromise = promise;
203
+ promise.catch((err) => {
124
204
  if (generation !== lifecycleGeneration) {
125
205
  return;
126
206
  }
127
207
  if (initPromise !== promise && initPromise !== null) {
128
208
  return;
129
209
  }
130
- console.error("MCP initialization failed:", err);
131
- initPromise = null;
210
+ const message = err instanceof Error ? err.message : String(err);
211
+ if (
212
+ !message.startsWith("Stale MCP session initialization cancelled") &&
213
+ !isStaleExtensionContextError(err)
214
+ ) {
215
+ console.error("MCP initialization failed:", err);
216
+ }
217
+ if (initPromise === promise) {
218
+ initPromise = null;
219
+ }
132
220
  });
133
221
  });
134
222
 
@@ -137,11 +225,12 @@ export default function mcpAdapter(pi: ExtensionAPI) {
137
225
  const currentState = state;
138
226
  state = null;
139
227
  initPromise = null;
228
+ registeredDirectToolNames = new Set<string>();
140
229
 
141
230
  try {
142
231
  await Promise.all([
143
232
  shutdownState(currentState, "session_shutdown"),
144
- shutdownOAuth(),
233
+ shutdownOAuthFlow(),
145
234
  ]);
146
235
  } catch (error) {
147
236
  console.error("MCP: session shutdown cleanup failed", error);
@@ -165,6 +254,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
165
254
  return;
166
255
  }
167
256
 
257
+ const { showStatus, showTools, reconnectServers, logoutServer, openMcpPanel, openMcpSetup } = await import("./commands.ts");
168
258
  const parts = args?.trim()?.split(/\s+/) ?? [];
169
259
  const subcommand = parts[0] ?? "";
170
260
  const targetServer = parts[1];
@@ -233,6 +323,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
233
323
  return;
234
324
  }
235
325
 
326
+ const { authenticateServer, openMcpAuthPanel } = await import("./commands.ts");
236
327
  if (!serverName) {
237
328
  await openMcpAuthPanel(state, pi, ctx, earlyConfigPath);
238
329
  return;
@@ -242,11 +333,13 @@ export default function mcpAdapter(pi: ExtensionAPI) {
242
333
  },
243
334
  });
244
335
 
245
- if (shouldRegisterProxyTool) {
336
+ function registerProxyTool(): void {
337
+ if (registeredProxyTool) return;
338
+ registeredProxyTool = true;
246
339
  (pi.registerTool as (tool: unknown) => unknown)({
247
340
  name: "mcp",
248
341
  label: "MCP",
249
- description: buildProxyDescription(earlyConfig, earlyCache, directSpecs),
342
+ description: "MCP gateway for connecting to configured MCP servers, searching tools, describing schemas, and calling tools lazily after MCP initialization.",
250
343
  promptSnippet: "MCP gateway - connect to MCP servers and call their tools",
251
344
  parameters: Type.Object({
252
345
  tool: Type.Optional(Type.String({ description: "Tool name to call (e.g., 'xcodebuild_list_sims')" })),
@@ -305,6 +398,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
305
398
  };
306
399
  }
307
400
 
401
+ const { executeCall, executeConnect, executeDescribe, executeList, executeSearch, executeStatus, executeUiMessages } = await import("./proxy-modes.ts");
308
402
  if (params.action === "ui-messages") {
309
403
  return executeUiMessages(state);
310
404
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.8.25",
3
+ "version": "0.8.26-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
6
6
  "contributors": [
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.8.26-alpha.2] - 2026-06-05
6
+
7
+ ### Fixed
8
+
9
+ - Fixed the `no-staged-files` acceptance runtime check and subagent worktree Git commands to ignore ambient Git repository environment variables, so subagents inspect the intended working tree instead of a parent hook or unrelated worktree.
10
+ - Suppressed intermediate model fallback failure notes and live foreground failure updates from successful subagent runs while preserving final failures and raw per-attempt diagnostics ([#1226](https://github.com/bastani-inc/atomic/issues/1226)).
11
+
12
+ ## [0.8.26-alpha.1] - 2026-06-05
13
+
14
+ ### Changed
15
+
16
+ - Bumped package version for the Atomic 0.8.26-alpha.1 prerelease.
17
+
5
18
  ## [0.8.25] - 2026-06-04
6
19
 
7
20
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.8.25",
3
+ "version": "0.8.26-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -650,6 +650,7 @@ async function runSingleStep(
650
650
  const attemptedModels: string[] = [];
651
651
  const modelAttempts: ModelAttempt[] = [];
652
652
  const attemptNotes: string[] = [];
653
+ const pendingAttemptNotes: string[] = [];
653
654
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
654
655
  let finalResult: RunPiStreamingResult | undefined;
655
656
  let finalFastMode: boolean | undefined;
@@ -768,9 +769,13 @@ async function runSingleStep(
768
769
  finalFastMode = attemptFastMode;
769
770
  finalOutputSnapshot = outputSnapshot;
770
771
  finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
771
- if (attempt.success || completionGuardTriggered) break;
772
- if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
773
- attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
772
+ if (attempt.success) break;
773
+ if (!completionGuardTriggered && isRetryableModelFailure(error) && index < candidates.length - 1) {
774
+ pendingAttemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
775
+ continue;
776
+ }
777
+ attemptNotes.push(...pendingAttemptNotes);
778
+ break;
774
779
  }
775
780
 
776
781
  const rawOutput = finalResult?.finalOutput ?? "";
@@ -17,6 +17,7 @@ import {
17
17
  type AgentProgress,
18
18
  type ArtifactPaths,
19
19
  type ControlEvent,
20
+ type Details,
20
21
  type ModelAttempt,
21
22
  type RunSyncOptions,
22
23
  type SingleResult,
@@ -139,6 +140,29 @@ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleRe
139
140
  };
140
141
  }
141
142
 
143
+ type RunSyncUpdate = import("@earendil-works/pi-agent-core").AgentToolResult<Details>;
144
+
145
+ function extractUpdateText(update: RunSyncUpdate): string | undefined {
146
+ const text = update.content
147
+ .map((item) => item.type === "text" ? item.text : undefined)
148
+ .filter((item): item is string => Boolean(item?.trim()))
149
+ .join("\n");
150
+ return text || undefined;
151
+ }
152
+
153
+ export function shouldSuppressIntermediateRetryableFailureUpdate(update: RunSyncUpdate): boolean {
154
+ const result = update.details?.results?.[0];
155
+ if (!result) return false;
156
+ const progress = update.details?.progress?.[0];
157
+ const status = result.progress?.status ?? progress?.status;
158
+ if (status !== "failed") return false;
159
+ const failureText = result.error
160
+ ?? result.progress?.error
161
+ ?? progress?.error
162
+ ?? extractUpdateText(update);
163
+ return isRetryableModelFailure(failureText);
164
+ }
165
+
142
166
  async function runSingleAttempt(
143
167
  runtimeCwd: string,
144
168
  agent: AgentConfig,
@@ -875,6 +899,7 @@ export async function runSync(
875
899
  const modelAttempts: ModelAttempt[] = [];
876
900
  const aggregateUsage = emptyUsage();
877
901
  const attemptNotes: string[] = [];
902
+ const pendingAttemptNotes: string[] = [];
878
903
  let totalToolCount = 0;
879
904
  let totalDurationMs = 0;
880
905
 
@@ -897,7 +922,18 @@ export async function runSync(
897
922
  const candidate = modelsToTry[i];
898
923
  if (candidate) attemptedModels.push(candidate);
899
924
  const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
900
- const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, options, {
925
+ let attemptOptions = options;
926
+ if (i < modelsToTry.length - 1 && options.onUpdate) {
927
+ const forwardUpdate = options.onUpdate;
928
+ attemptOptions = {
929
+ ...options,
930
+ onUpdate: (update) => {
931
+ if (shouldSuppressIntermediateRetryableFailureUpdate(update)) return;
932
+ forwardUpdate(update);
933
+ },
934
+ };
935
+ }
936
+ const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, attemptOptions, {
901
937
  sessionEnabled,
902
938
  systemPrompt,
903
939
  resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
@@ -928,10 +964,12 @@ export async function runSync(
928
964
  if (attemptSucceeded) {
929
965
  break;
930
966
  }
931
- if (!isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
932
- break;
967
+ if (isRetryableModelFailure(result.error) && i < modelsToTry.length - 1) {
968
+ pendingAttemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
969
+ continue;
933
970
  }
934
- attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
971
+ attemptNotes.push(...pendingAttemptNotes);
972
+ break;
935
973
  }
936
974
 
937
975
  const result = lastResult ?? {
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import * as path from "node:path";
4
+ import { createGitEnvironment } from "@bastani/atomic";
4
5
  import type {
5
6
  AcceptanceConfig,
6
7
  AcceptanceEvidenceKind,
@@ -399,7 +400,7 @@ function reportEvidencePresent(report: AcceptanceReport, kind: AcceptanceEvidenc
399
400
  }
400
401
 
401
402
  function checkNoStagedFiles(cwd: string): AcceptanceRuntimeCheck {
402
- const result = spawnSync("git", ["status", "--short"], { cwd, encoding: "utf-8" });
403
+ const result = spawnSync("git", ["status", "--short"], { cwd, env: createGitEnvironment(), encoding: "utf-8" });
403
404
  if (result.status !== 0) {
404
405
  return { id: "no-staged-files", status: "not-applicable", message: "git status unavailable; no staged-files check skipped" };
405
406
  }
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
- import { APP_NAME } from "@bastani/atomic";
5
+ import { APP_NAME, createGitEnvironment } from "@bastani/atomic";
6
6
 
7
7
  export interface WorktreeSetup {
8
8
  cwd: string;
@@ -82,7 +82,7 @@ interface RepoState {
82
82
  const DEFAULT_WORKTREE_SETUP_HOOK_TIMEOUT_MS = 30000;
83
83
 
84
84
  function runGit(cwd: string, args: string[]): GitResult {
85
- const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8" });
85
+ const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8", env: createGitEnvironment() });
86
86
  return {
87
87
  stdout: result.stdout ?? "",
88
88
  stderr: result.stderr ?? "",
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.8.26-alpha.2] - 2026-06-05
8
+
9
+ ### Changed
10
+
11
+ - Bumped package version for the Atomic 0.8.26-alpha.2 prerelease.
12
+
13
+ ## [0.8.26-alpha.1] - 2026-06-05
14
+
15
+ ### Changed
16
+
17
+ - Deferred heavy search, fetch, curator, summary, provider-probing, and code-search modules until the relevant web-access tool or command is invoked, reducing default CLI startup cost ([#1223](https://github.com/bastani-inc/atomic/issues/1223)).
18
+
7
19
  ## [0.8.25] - 2026-06-04
8
20
 
9
21
  ### Changed