@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/web-access",
3
- "version": "0.8.25",
3
+ "version": "0.8.26-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -0,0 +1,364 @@
1
+ import type { ToolDefinition } from "@bastani/atomic";
2
+ import { Box, Text } from "@mariozechner/pi-tui";
3
+ import { formatSeconds } from "./utils.js";
4
+
5
+ type ToolResultRenderer = NonNullable<ToolDefinition["renderResult"]>;
6
+ type ToolRenderResultArgs = Parameters<ToolResultRenderer>;
7
+ type ToolRenderResult = ReturnType<ToolResultRenderer>;
8
+ type RenderedResult = ToolRenderResultArgs[0];
9
+ type TextContentBlock = Extract<RenderedResult["content"][number], { type: "text" }>;
10
+
11
+ type QueryDetail = {
12
+ query: string;
13
+ provider: string | null;
14
+ answer: string | null;
15
+ sources: Array<{ title: string; url: string }>;
16
+ error: string | null;
17
+ };
18
+
19
+ type WebSearchResultDetails = {
20
+ queryCount?: number;
21
+ successfulQueries?: number;
22
+ totalResults?: number;
23
+ error?: string;
24
+ fetchId?: string;
25
+ fetchUrls?: string[];
26
+ phase?: string;
27
+ progress?: number;
28
+ currentQuery?: string;
29
+ curated?: boolean;
30
+ curatedFrom?: number;
31
+ curatedQueries?: QueryDetail[];
32
+ cancelled?: boolean;
33
+ cancelReason?: string;
34
+ summary?: {
35
+ text: string;
36
+ workflow: "summary-review";
37
+ model: string | null;
38
+ durationMs: number;
39
+ tokenEstimate: number;
40
+ fallbackUsed: boolean;
41
+ fallbackReason?: string;
42
+ edited?: boolean;
43
+ };
44
+ };
45
+
46
+ type CodeSearchResultDetails = {
47
+ query?: string;
48
+ maxTokens?: number;
49
+ error?: string;
50
+ };
51
+
52
+ type FetchContentResultDetails = {
53
+ urlCount?: number;
54
+ successful?: number;
55
+ totalChars?: number;
56
+ error?: string;
57
+ title?: string;
58
+ truncated?: boolean;
59
+ responseId?: string;
60
+ phase?: string;
61
+ progress?: number;
62
+ hasImage?: boolean;
63
+ imageCount?: number;
64
+ prompt?: string;
65
+ timestamp?: string;
66
+ frames?: number;
67
+ duration?: number;
68
+ };
69
+
70
+ type GetSearchContentResultDetails = {
71
+ error?: string;
72
+ query?: string;
73
+ url?: string;
74
+ title?: string;
75
+ resultCount?: number;
76
+ contentLength?: number;
77
+ };
78
+
79
+ function isTextContentBlock(block: RenderedResult["content"][number]): block is TextContentBlock {
80
+ return block.type === "text";
81
+ }
82
+
83
+ function firstTextContent(result: RenderedResult): string {
84
+ return result.content.find(isTextContentBlock)?.text ?? "";
85
+ }
86
+
87
+ function progressBar(progress: number): string {
88
+ const filled = Math.floor(progress * 10);
89
+ return "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
90
+ }
91
+
92
+ export const renderWebSearchResult: ToolResultRenderer = (result, { expanded, isPartial }, theme) => {
93
+ const details = result.details as WebSearchResultDetails | undefined;
94
+
95
+ if (isPartial) {
96
+ if (details?.phase === "curating") {
97
+ return new Text(theme.fg("accent", "waiting for summary approval..."), 0, 0);
98
+ }
99
+ if (details?.phase === "searching") {
100
+ const progress = details?.progress ?? 0;
101
+ const bar = progressBar(progress);
102
+ const query = details?.currentQuery || "";
103
+ const display = query.length > 40 ? query.slice(0, 37) + "..." : query;
104
+ return new Text(theme.fg("accent", `[${bar}] ${display}`), 0, 0);
105
+ }
106
+ const progress = details?.progress ?? 0;
107
+ const bar = progressBar(progress);
108
+ return new Text(theme.fg("accent", `[${bar}] ${details?.phase || "searching"}`), 0, 0);
109
+ }
110
+
111
+ if (details?.error) {
112
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
113
+ }
114
+
115
+ let statusLine: string;
116
+ const queryInfo = details?.queryCount === 1 ? "" : `${details?.successfulQueries}/${details?.queryCount} queries, `;
117
+ statusLine = theme.fg("success", `${queryInfo}${details?.totalResults ?? 0} sources`);
118
+ if (details?.curated && details?.curatedFrom) {
119
+ statusLine += theme.fg("muted", ` (${details.queryCount}/${details.curatedFrom} queries curated)`);
120
+ }
121
+ if (details?.fetchId && details?.fetchUrls) {
122
+ statusLine += theme.fg("muted", ` (fetching ${details.fetchUrls.length} URLs)`);
123
+ } else if (details?.fetchId) {
124
+ statusLine += theme.fg("muted", " (content ready)");
125
+ }
126
+
127
+ // Build expanded lines first so collapsed view can reference total count
128
+ const lines = [statusLine];
129
+ if (details?.summary?.text) {
130
+ lines.push("");
131
+ lines.push(theme.fg("accent", `── Summary (${details.summary.workflow}) ` + "─".repeat(32)));
132
+ lines.push("");
133
+ for (const line of details.summary.text.split("\n")) {
134
+ lines.push(` ${line}`);
135
+ }
136
+ lines.push("");
137
+ const metaParts = [
138
+ details.summary.model ? `model=${details.summary.model}` : "model=deterministic",
139
+ `duration=${details.summary.durationMs}ms`,
140
+ `tokens~${details.summary.tokenEstimate}`,
141
+ details.summary.fallbackUsed ? "fallback=true" : "fallback=false",
142
+ details.summary.edited ? "edited=true" : "edited=false",
143
+ ];
144
+ if (details.summary.fallbackReason) {
145
+ metaParts.push(`reason=${details.summary.fallbackReason}`);
146
+ }
147
+ lines.push(theme.fg("dim", " " + metaParts.join(" · ")));
148
+ }
149
+
150
+ const queryDetails = details?.curatedQueries;
151
+ if (queryDetails?.length) {
152
+ const kept = queryDetails.length;
153
+ const from = details?.curatedFrom ?? kept;
154
+ lines.push("");
155
+ lines.push(theme.fg("accent", `\u2500\u2500 Curated Results (${kept} of ${from} queries kept) ` + "\u2500".repeat(24)));
156
+
157
+ for (const cq of queryDetails) {
158
+ lines.push("");
159
+ const dq = cq.query.length > 65 ? cq.query.slice(0, 62) + "..." : cq.query;
160
+ const providerLabel = cq.provider ? ` (${cq.provider})` : "";
161
+ lines.push(theme.fg("accent", ` "${dq}"${providerLabel}`));
162
+
163
+ if (cq.error) {
164
+ lines.push(theme.fg("error", ` ${cq.error}`));
165
+ } else if (cq.answer) {
166
+ lines.push("");
167
+ for (const line of cq.answer.split("\n")) {
168
+ lines.push(` ${line}`);
169
+ }
170
+ }
171
+
172
+ if (cq.sources.length > 0) {
173
+ lines.push("");
174
+ for (const s of cq.sources) {
175
+ const domain = s.url.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
176
+ const title = s.title.length > 50 ? s.title.slice(0, 47) + "..." : s.title;
177
+ lines.push(theme.fg("muted", ` \u25b8 ${title}`) + theme.fg("dim", ` \u00b7 ${domain}`));
178
+ }
179
+ }
180
+ }
181
+ lines.push("");
182
+ } else {
183
+ const textContent = firstTextContent(result);
184
+ const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
185
+ for (const line of preview.split("\n")) {
186
+ lines.push(theme.fg("dim", line));
187
+ }
188
+ }
189
+
190
+ if (details?.fetchUrls && details.fetchUrls.length > 0) {
191
+ if (details.curated) {
192
+ lines.push(theme.fg("muted", `Fetching ${details.fetchUrls.length} URLs in background`));
193
+ } else {
194
+ lines.push(theme.fg("muted", "Fetching:"));
195
+ for (const u of details.fetchUrls.slice(0, 5)) {
196
+ const display = u.length > 60 ? u.slice(0, 57) + "..." : u;
197
+ lines.push(theme.fg("dim", " " + display));
198
+ }
199
+ if (details.fetchUrls.length > 5) {
200
+ lines.push(theme.fg("dim", ` ... and ${details.fetchUrls.length - 5} more`));
201
+ }
202
+ }
203
+ }
204
+
205
+ const totalLines = lines.length;
206
+
207
+ if (!expanded) {
208
+ const box = new Box(1, 0, (t) => theme.bg("toolSuccessBg", t));
209
+ box.addChild(new Text(statusLine, 0, 0));
210
+
211
+ let collapsedLines = 1; // statusLine
212
+ const summaryPreview = details?.summary?.text?.trim() || "";
213
+ if (summaryPreview) {
214
+ const preview = summaryPreview.length > 120 ? summaryPreview.slice(0, 117) + "..." : summaryPreview;
215
+ box.addChild(new Text(theme.fg("dim", preview), 0, 0));
216
+ collapsedLines++;
217
+ } else if (details?.curatedQueries?.length) {
218
+ for (const cq of details.curatedQueries.slice(0, 3)) {
219
+ const dq = cq.query.length > 55 ? cq.query.slice(0, 52) + "..." : cq.query;
220
+ const srcCount = cq.sources?.length ?? 0;
221
+ const suffix = cq.error ? theme.fg("error", " (error)") : theme.fg("dim", ` · ${srcCount} sources`);
222
+ box.addChild(new Text(theme.fg("accent", ` "${dq}"`) + suffix, 0, 0));
223
+ collapsedLines++;
224
+ }
225
+ if (details.curatedQueries.length > 3) {
226
+ box.addChild(new Text(theme.fg("dim", ` ... and ${details.curatedQueries.length - 3} more`), 0, 0));
227
+ collapsedLines++;
228
+ }
229
+ } else {
230
+ const textContent = firstTextContent(result);
231
+ const firstContentLine = textContent.split("\n").find(l => {
232
+ const t = l.trim();
233
+ return t && !t.startsWith("[") && !t.startsWith("#") && !t.startsWith("---");
234
+ });
235
+ const fallbackLine = (firstContentLine?.trim() || "").replace(/\*\*/g, "");
236
+ if (fallbackLine) {
237
+ const preview = fallbackLine.length > 120 ? fallbackLine.slice(0, 117) + "..." : fallbackLine;
238
+ box.addChild(new Text(theme.fg("dim", preview), 0, 0));
239
+ collapsedLines++;
240
+ }
241
+ }
242
+ const moreLines = Math.max(0, totalLines - collapsedLines);
243
+ if (moreLines > 0) {
244
+ box.addChild(new Text(theme.fg("muted", `\n... (${moreLines} more lines, ${totalLines} total, CTRL+O Expand)`), 0, 0));
245
+ }
246
+ return box;
247
+ }
248
+
249
+ return new Text(lines.join("\n"), 0, 0);
250
+ };
251
+
252
+ export const renderCodeSearchResult: ToolResultRenderer = (result, { expanded }, theme) => {
253
+ const details = result.details as CodeSearchResultDetails | undefined;
254
+ if (details?.error) {
255
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
256
+ }
257
+
258
+ const summary = theme.fg("success", "code context returned") +
259
+ theme.fg("muted", ` (${details?.maxTokens ?? 5000} tokens max)`);
260
+ if (!expanded) return new Text(summary, 0, 0);
261
+
262
+ const textContent = firstTextContent(result);
263
+ const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
264
+ return new Text(summary + "\n" + theme.fg("dim", preview), 0, 0);
265
+ };
266
+
267
+ export const renderFetchContentResult: ToolResultRenderer = (result, { expanded, isPartial }, theme) => {
268
+ const details = result.details as FetchContentResultDetails | undefined;
269
+
270
+ if (isPartial) {
271
+ const progress = details?.progress ?? 0;
272
+ const bar = progressBar(progress);
273
+ return new Text(theme.fg("accent", `[${bar}] ${details?.phase || "fetching"}`), 0, 0);
274
+ }
275
+
276
+ if (details?.error) {
277
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
278
+ }
279
+
280
+ if (details?.urlCount === 1) {
281
+ const title = details?.title || "Untitled";
282
+ const imgCount = details?.imageCount ?? (details?.hasImage ? 1 : 0);
283
+ const imageBadge = imgCount > 1
284
+ ? theme.fg("accent", ` [${imgCount} images]`)
285
+ : imgCount === 1
286
+ ? theme.fg("accent", " [image]")
287
+ : "";
288
+ let statusLine = theme.fg("success", title) + theme.fg("muted", ` (${details?.totalChars ?? 0} chars)`) + imageBadge;
289
+ if (details?.truncated) {
290
+ statusLine += theme.fg("warning", " [truncated]");
291
+ }
292
+ if (typeof details?.duration === "number") {
293
+ statusLine += theme.fg("muted", ` | ${formatSeconds(Math.floor(details.duration))} total`);
294
+ }
295
+ const textContent = firstTextContent(result);
296
+ if (!expanded) {
297
+ const brief = textContent.length > 200 ? textContent.slice(0, 200) + "..." : textContent;
298
+ return new Text(statusLine + "\n" + theme.fg("dim", brief), 0, 0);
299
+ }
300
+ const lines = [statusLine];
301
+ if (details?.prompt) {
302
+ const display = details.prompt.length > 250 ? details.prompt.slice(0, 247) + "..." : details.prompt;
303
+ lines.push(theme.fg("dim", ` prompt: "${display}"`));
304
+ }
305
+ if (details?.timestamp) {
306
+ lines.push(theme.fg("dim", ` timestamp: ${details.timestamp}`));
307
+ }
308
+ if (typeof details?.frames === "number") {
309
+ lines.push(theme.fg("dim", ` frames: ${details.frames}`));
310
+ }
311
+ const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
312
+ lines.push(theme.fg("dim", preview));
313
+ return new Text(lines.join("\n"), 0, 0);
314
+ }
315
+
316
+ const countColor = (details?.successful ?? 0) > 0 ? "success" : "error";
317
+ const statusLine = theme.fg(countColor, `${details?.successful}/${details?.urlCount} URLs`) + theme.fg("muted", " (content stored)");
318
+ if (!expanded) {
319
+ return new Text(statusLine, 0, 0);
320
+ }
321
+ const textContent = firstTextContent(result);
322
+ const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
323
+ return new Text(statusLine + "\n" + theme.fg("dim", preview), 0, 0);
324
+ };
325
+
326
+ export const renderGetSearchContentResult: ToolResultRenderer = (result, { expanded }, theme) => {
327
+ const details = result.details as GetSearchContentResultDetails | undefined;
328
+
329
+ if (details?.error) {
330
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
331
+ }
332
+
333
+ let statusLine: string;
334
+ if (details?.query) {
335
+ statusLine = theme.fg("success", `"${details.query}"`) + theme.fg("muted", ` (${details.resultCount} results)`);
336
+ } else {
337
+ statusLine = theme.fg("success", details?.title || "Content") + theme.fg("muted", ` (${details?.contentLength ?? 0} chars)`);
338
+ }
339
+
340
+ if (!expanded) {
341
+ return new Text(statusLine, 0, 0);
342
+ }
343
+
344
+ const textContent = firstTextContent(result);
345
+ const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
346
+ return new Text(statusLine + "\n" + theme.fg("dim", preview), 0, 0);
347
+ };
348
+
349
+ export function renderWebAccessToolResult(name: string, args: ToolRenderResultArgs): ToolRenderResult {
350
+ switch (name) {
351
+ case "web_search":
352
+ return renderWebSearchResult(...args);
353
+ case "code_search":
354
+ return renderCodeSearchResult(...args);
355
+ case "fetch_content":
356
+ return renderFetchContentResult(...args);
357
+ case "get_search_content":
358
+ return renderGetSearchContentResult(...args);
359
+ default: {
360
+ const theme = args[2];
361
+ return new Text(theme.fg("error", `Result renderer not found: ${name}`), 0, 0);
362
+ }
363
+ }
364
+ }
@@ -6,6 +6,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.26-alpha.2] - 2026-06-05
10
+
11
+ ### Changed
12
+
13
+ - Updated the `research-codebase` skill to capture a `breaking_changes_allowed` compatibility posture before research fanout, carry it through sub-agent prompts, and record it in research documents so downstream specs and workflows do not preserve legacy APIs by default when breaking changes are allowed ([#1225](https://github.com/bastani-inc/atomic/issues/1225)).
14
+
15
+ ### Fixed
16
+
17
+ - Fixed stage-local workflow HIL `input` and `editor` prompts losing draft text across Ctrl+D detach/reattach; drafts are kept live-only in memory and cleared when the prompt or run/stage exits ([#1179](https://github.com/bastani-inc/atomic/issues/1179)).
18
+ - Fixed workflow worktree Git commands to strip ambient repository-local Git environment variables before inspecting or creating targeted worktrees.
19
+ - Suppressed intermediate model fallback failure warnings from successful workflow stages while preserving final failures and raw per-attempt diagnostics ([#1226](https://github.com/bastani-inc/atomic/issues/1226)).
20
+
21
+ ## [0.8.26-alpha.1] - 2026-06-05
22
+
23
+ ### Fixed
24
+
25
+ - Fixed the inline-form "snapshot lost" renderer and the `workflow.run.start`/`workflow.run.end` banner renderers returning bare strings, which crashed the host TUI with `child.render is not a function` when resuming a session containing persisted workflow custom messages. These renderers now return proper render components ([#1236](https://github.com/bastani-inc/atomic/issues/1236)).
26
+ - Fixed the workflow input form (the `/workflow <name>` argument selector) leaking into model context: spawning the picker and exiting without running the workflow no longer sends the form to the LLM. The input-form card is now emitted with `excludeFromContext` since it is transient UI, not conversation.
27
+ - Fixed the workflow input widget re-rendering in chat after `/resume`. Inline-form state is now cleared on `session_start`, and a rehydrated `workflows:input-form` card whose backing state is gone now renders nothing (returns `null`) instead of a stale form or "snapshot lost" placeholder.
28
+ - Stage sessions now emit `session_shutdown` before `dispose()` (mirroring the host `AgentSessionRuntime` teardown) so bound extensions receive a graceful shutdown signal instead of being silently invalidated. This stops disposed stage sessions from leaking child MCP servers and from triggering spurious stale-context "MCP initialization failed" errors when an extension's deferred `session_start` work races with stage disposal.
29
+
9
30
  ## [0.8.25] - 2026-06-04
10
31
 
11
32
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.8.25",
3
+ "version": "0.8.26-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -24,14 +24,24 @@ The user's research question/request is: **$ARGUMENTS**
24
24
  - **CRITICAL**: Read these files yourself in the main context before spawning any sub-tasks
25
25
  - This ensures you have full context before decomposing the research
26
26
 
27
- 2. **Analyze and decompose the research question:**
27
+ 2. **Determine the compatibility posture:**
28
+ - Before decomposing the research request, identify whether this project must preserve backward compatibility for real downstream users.
29
+ - If the user explicitly allows breaking changes, public API changes, cleanup, or says there are no real users/downstream dependencies, set `breaking_changes_allowed: true`.
30
+ - If the user mentions production users, published APIs, downstream consumers, migration safety, or compatibility requirements, set `breaking_changes_allowed: false`.
31
+ - If the posture is not inferable from the request, ask the user once before continuing, using the available structured question tool when possible.
32
+ - Carry this posture into the research plan, every sub-agent prompt, the final research document frontmatter, and the `## Compatibility Context` section.
33
+ - When `breaking_changes_allowed: true`, document existing legacy behavior, compatibility shims, optional flags, and public APIs as current state, not as constraints future specs must preserve unless the user explicitly asks for preservation.
34
+ - When `breaking_changes_allowed: false`, document public APIs, compatibility-sensitive surfaces, downstream callers, migration constraints, and behavior that future work must preserve.
35
+
36
+ 3. **Analyze and decompose the research question:**
28
37
  - Break the research question down into composable research areas
29
38
  - Take time to ultrathink about the underlying patterns, connections, and architectural implications the user might be seeking
30
39
  - Identify specific components, patterns, or concepts to investigate
31
40
  - Create a research plan using TodoWrite to track all subtasks
41
+ - Include the compatibility posture in the plan so later synthesis and spec creation inherit the same constraint.
32
42
  - Consider which directories, files, or architectural patterns are relevant
33
43
 
34
- 3. **Spawn parallel sub-agent tasks:**
44
+ 4. **Spawn parallel sub-agent tasks:**
35
45
  - Create multiple Task agents to research different aspects concurrently
36
46
  - We now have specialized agents that know how to do specific research tasks:
37
47
 
@@ -67,8 +77,9 @@ The user's research question/request is: **$ARGUMENTS**
67
77
  - Each agent knows its job - just tell it what you're looking for
68
78
  - Don't write detailed prompts about HOW to search - the agents already know
69
79
  - Remind agents they are documenting, not evaluating or improving
80
+ - Include `breaking_changes_allowed: true` or `breaking_changes_allowed: false` in each sub-agent prompt so compatibility-sensitive findings are documented with the right posture.
70
81
 
71
- 4. **Wait for all sub-agents to complete and synthesize:**
82
+ 5. **Wait for all sub-agents to complete and synthesize:**
72
83
  - IMPORTANT: Wait for ALL sub-agent tasks to complete before proceeding
73
84
  - Compile all sub-agent results (both codebase and research findings)
74
85
  - Prioritize live codebase findings as primary source of truth
@@ -79,7 +90,7 @@ The user's research question/request is: **$ARGUMENTS**
79
90
  - Answer the user's research question with concrete evidence
80
91
  - **If findings reveal the original question was misframed** (e.g., the system works differently than assumed, or the components don't exist where expected), flag this to the user before finalizing the document. This is valuable signal — don't bury it.
81
92
 
82
- 5. **Generate research document:**
93
+ 6. **Generate research document:**
83
94
  - Follow the directory structure for research documents:
84
95
 
85
96
  ```
@@ -117,6 +128,8 @@ research/
117
128
  status: complete
118
129
  last_updated: !`date '+%Y-%m-%d'`
119
130
  last_updated_by: [Researcher name]
131
+ breaking_changes_allowed: [true or false]
132
+ compatibility_context: "[Short explanation of downstream-user/API compatibility posture]"
120
133
  ---
121
134
 
122
135
  # Research
@@ -125,6 +138,10 @@ research/
125
138
 
126
139
  [Original user query]
127
140
 
141
+ ## Compatibility Context
142
+
143
+ [State whether breaking changes are allowed. If true, note that existing compatibility shims, optional flags, legacy APIs, and public APIs are documented as current state rather than preservation constraints. If false, summarize compatibility-sensitive surfaces, downstream users/callers, migration constraints, and behavior future work must preserve.]
144
+
128
145
  ## Summary
129
146
 
130
147
  [High-level documentation of what was found, answering the user's question by describing what exists]
@@ -167,19 +184,19 @@ research/
167
184
  [Any areas that need further investigation]
168
185
  ```
169
186
 
170
- 1. **Add GitHub permalinks (if applicable):**
187
+ 7. **Add GitHub permalinks (if applicable):**
171
188
  - Check if on main branch or if commit is pushed: `git branch --show-current` and `git status`
172
189
  - If on main/master or pushed, generate GitHub permalinks:
173
190
  - Get repo info: `gh repo view --json owner,name`
174
191
  - Create permalinks: `https://github.com/{owner}/{repo}/blob/{commit}/{file}#L{line}`
175
192
  - Replace local file references with permalinks in the document
176
193
 
177
- 2. **Present findings:**
194
+ 8. **Present findings:**
178
195
  - Present a concise summary of findings to the user
179
196
  - Include key file references for easy navigation
180
197
  - Ask if they have follow-up questions or need clarification
181
198
 
182
- 3. **Handle follow-up questions:**
199
+ 9. **Handle follow-up questions:**
183
200
 
184
201
  - If the user has follow-up questions, append to the same research document
185
202
  - Update the frontmatter fields `last_updated` and `last_updated_by` to reflect the update
@@ -207,10 +224,12 @@ research/
207
224
  - **REMEMBER**: Document what IS, not what SHOULD BE
208
225
  - **NO RECOMMENDATIONS**: Only describe the current state of the codebase
209
226
  - **File reading**: Always read mentioned files FULLY (no limit/offset) before spawning sub-tasks
227
+ - **Compatibility posture**: Always determine `breaking_changes_allowed` before decomposing the question. This is a single project/research posture, not a request to add compatibility flags. Use it to document whether old APIs and shims are constraints for future work.
210
228
  - **Critical ordering**: Follow the numbered steps exactly
211
229
  - ALWAYS read mentioned files first before spawning sub-tasks (step 1)
212
- - ALWAYS wait for all sub-agents to complete before synthesizing (step 4)
213
- - ALWAYS gather metadata before writing the document (step 5 before step 6)
230
+ - ALWAYS determine compatibility posture before decomposing the question (step 2)
231
+ - ALWAYS wait for all sub-agents to complete before synthesizing (step 5)
232
+ - ALWAYS gather metadata before writing the document (as part of step 6)
214
233
  - NEVER write the research document with placeholder values
215
234
 
216
235
  - **Frontmatter consistency**:
@@ -54,6 +54,7 @@ import {
54
54
  openInlineInputsForm,
55
55
  registerInlineFormRenderer,
56
56
  } from "../tui/inline-form-overlay.js";
57
+ import { clearForms } from "../tui/inline-form-store.js";
57
58
  import {
58
59
  registerChatSurfaceRenderer,
59
60
  emitChatSurface,
@@ -166,7 +167,7 @@ export interface PiMessageRenderOptions {
166
167
  expanded: boolean;
167
168
  }
168
169
 
169
- export type PiMessageRendererResult = string | PiMessageRenderComponent | undefined;
170
+ export type PiMessageRendererResult = string | PiMessageRenderComponent | null | undefined;
170
171
  export type PiMessageRenderer = (
171
172
  payload: unknown,
172
173
  options?: PiMessageRenderOptions,
@@ -3756,11 +3757,14 @@ function factory(pi: ExtensionAPI): void {
3756
3757
  // duplicating it into chat scroll just creates visual noise and pushes
3757
3758
  // older chat content out of view every time a stage transitions.
3758
3759
  if (typeof pi.registerMessageRenderer === "function") {
3760
+ // Wrap the string-producing banners in a render component: the host adds a
3761
+ // renderer's result directly as a TUI child, so a bare string would crash
3762
+ // `Container.render()` with "child.render is not a function".
3759
3763
  pi.registerMessageRenderer("workflow.run.start", (payload) =>
3760
- renderRunBanner(payload as RunStartPayload),
3764
+ dynamicTextRenderComponent(() => renderRunBanner(payload as RunStartPayload)),
3761
3765
  );
3762
3766
  pi.registerMessageRenderer("workflow.run.end", (payload) =>
3763
- renderRunSummary(payload as RunEndPayload),
3767
+ dynamicTextRenderComponent(() => renderRunSummary(payload as RunEndPayload)),
3764
3768
  );
3765
3769
  // Inline workflow-input form (Option C in the design conversation):
3766
3770
  // a sticky chat-history card driven by a custom EditorComponent. The
@@ -3834,6 +3838,12 @@ function factory(pi: ExtensionAPI): void {
3834
3838
  persistence: persistenceRef.current,
3835
3839
  });
3836
3840
  store.clear();
3841
+ // Drop any inline input-form state from a previous session in this pi
3842
+ // process. A resumed/replaced session must not render a stale live form,
3843
+ // and rehydrated `workflows:input-form` cards then resolve to no backing
3844
+ // state so their renderer suppresses output (input widget hidden after
3845
+ // /resume).
3846
+ clearForms();
3837
3847
  resetWorkflowLifecycleNotificationState(lifecycleNotificationState);
3838
3848
  resetWorkflowHilAnswerNotificationState(hilAnswerNotificationState);
3839
3849
  stageControlRegistry.clear();
@@ -285,6 +285,57 @@ function terminatingToolResultText(
285
285
  return undefined;
286
286
  }
287
287
 
288
+ /**
289
+ * A stage session backed by a real Atomic `AgentSession` exposes its
290
+ * `extensionRunner`. When workflow wiring binds extensions to a stage session it
291
+ * replays the `session_start` lifecycle (see wiring.ts `bindExtensions`), so
292
+ * extensions such as MCP begin per-session initialization. Tearing that session
293
+ * down with `dispose()` alone invalidates the extension runtime WITHOUT emitting
294
+ * `session_shutdown`, so those extensions never receive a graceful teardown
295
+ * signal: MCP, for example, logs a spurious stale-context "initialization
296
+ * failed" error when its deferred init races with disposal, and leaves any child
297
+ * MCP servers running.
298
+ *
299
+ * The test stub session (createTestAgentSession) has no `extensionRunner`, so the
300
+ * capability is optional and feature-detected at runtime.
301
+ */
302
+ type StageSessionExtensionRunner = {
303
+ hasHandlers(eventType: string): boolean;
304
+ emit(event: { readonly type: "session_shutdown"; readonly reason: "quit" }): Promise<unknown>;
305
+ };
306
+
307
+ function stageSessionExtensionRunner(
308
+ current: StageSessionRuntime,
309
+ ): StageSessionExtensionRunner | undefined {
310
+ const runner = (current as StageSessionRuntime & { extensionRunner?: StageSessionExtensionRunner })
311
+ .extensionRunner;
312
+ if (runner && typeof runner.hasHandlers === "function" && typeof runner.emit === "function") {
313
+ return runner;
314
+ }
315
+ return undefined;
316
+ }
317
+
318
+ /**
319
+ * Dispose a stage session, mirroring the host `AgentSessionRuntime` teardown:
320
+ * emit `session_shutdown` before `dispose()` whenever the session exposes a
321
+ * compatible extension runner, so extensions tear down per-session resources
322
+ * (and bump their lifecycle generation) instead of being silently invalidated.
323
+ * A throwing shutdown handler must never strand the session, so disposal always
324
+ * runs.
325
+ */
326
+ async function disposeStageSession(current: StageSessionRuntime | undefined): Promise<void> {
327
+ if (!current) return;
328
+ const runner = stageSessionExtensionRunner(current);
329
+ if (runner?.hasHandlers("session_shutdown")) {
330
+ try {
331
+ await runner.emit({ type: "session_shutdown", reason: "quit" });
332
+ } catch (error) {
333
+ console.error("atomic-workflows: stage session_shutdown handler failed", error);
334
+ }
335
+ }
336
+ await current.dispose();
337
+ }
338
+
288
339
  function asAgentSession(activeSession: StageSessionRuntime | undefined): AgentSession | undefined {
289
340
  if (!activeSession) return undefined;
290
341
  const candidate = activeSession as StageSessionRuntime & Partial<Pick<AgentSession, "state" | "sessionManager" | "modelRegistry" | "getContextUsage">>;
@@ -535,6 +586,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
535
586
  let selectedModel: string | undefined;
536
587
  const modelAttempts: WorkflowModelAttempt[] = [];
537
588
  const modelWarnings: string[] = [];
589
+ const pendingFallbackWarnings: string[] = [];
538
590
  const modelCatalog = opts.models === undefined
539
591
  ? undefined
540
592
  : {
@@ -675,7 +727,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
675
727
  unsubscribeTerminateWatcher?.();
676
728
  unsubscribeTerminateWatcher = undefined;
677
729
  terminatingToolCallIds.clear();
678
- await current?.dispose();
730
+ await disposeStageSession(current);
679
731
  }
680
732
 
681
733
  async function promptWithPauseResume(
@@ -745,15 +797,19 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
745
797
  try {
746
798
  await promptWithPauseResume(activeSession, text, sdkOptions);
747
799
  modelAttempts.push({ model: candidate.id, success: true, ...modelAttemptReasoning(candidate) });
800
+ pendingFallbackWarnings.length = 0;
748
801
  return;
749
802
  } catch (err) {
750
803
  const message = errorMessage(err);
751
804
  modelAttempts.push({ model: candidate.id, success: false, ...modelAttemptReasoning(candidate), error: message });
752
805
  if (signal?.aborted || !isRetryableModelFailure(message) || index === candidates.length - 1) {
806
+ modelWarnings.push(...pendingFallbackWarnings);
807
+ pendingFallbackWarnings.length = 0;
808
+ notifyModelFallbackMetaChange();
753
809
  throw err;
754
810
  }
755
811
  const nextCandidate = candidates[index + 1]!;
756
- modelWarnings.push(`[fallback] ${candidateLabel(candidate)} failed: ${message}. Retrying with ${candidateLabel(nextCandidate)}.`);
812
+ pendingFallbackWarnings.push(`[fallback] ${candidateLabel(candidate)} failed: ${message}. Retrying with ${candidateLabel(nextCandidate)}.`);
757
813
  await disposeCurrentSession();
758
814
  index += 1;
759
815
  }
@@ -895,7 +951,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
895
951
  unsubscribeTerminateWatcher?.();
896
952
  unsubscribeTerminateWatcher = undefined;
897
953
  terminatingToolCallIds.clear();
898
- await session?.dispose();
954
+ await disposeStageSession(session);
899
955
  },
900
956
 
901
957
  __getLastAssistantText() {