@bastani/atomic 0.6.4-0 → 0.6.5-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 (120) hide show
  1. package/.agents/skills/create-spec/SKILL.md +6 -3
  2. package/.agents/skills/tdd/SKILL.md +107 -0
  3. package/.agents/skills/tdd/deep-modules.md +33 -0
  4. package/.agents/skills/tdd/interface-design.md +31 -0
  5. package/.agents/skills/tdd/mocking.md +59 -0
  6. package/.agents/skills/tdd/refactoring.md +10 -0
  7. package/.agents/skills/tdd/tests.md +61 -0
  8. package/.agents/skills/workflow-creator/SKILL.md +550 -0
  9. package/.agents/skills/workflow-creator/references/agent-sessions.md +891 -0
  10. package/.agents/skills/workflow-creator/references/agent-setup-recipe.md +266 -0
  11. package/.agents/skills/workflow-creator/references/computation-and-validation.md +201 -0
  12. package/.agents/skills/workflow-creator/references/control-flow.md +470 -0
  13. package/.agents/skills/workflow-creator/references/failure-modes.md +1014 -0
  14. package/.agents/skills/workflow-creator/references/getting-started.md +392 -0
  15. package/.agents/skills/workflow-creator/references/registry-and-validation.md +141 -0
  16. package/.agents/skills/workflow-creator/references/running-workflows.md +418 -0
  17. package/.agents/skills/workflow-creator/references/session-config.md +384 -0
  18. package/.agents/skills/workflow-creator/references/state-and-data-flow.md +356 -0
  19. package/.agents/skills/workflow-creator/references/user-input.md +234 -0
  20. package/.agents/skills/workflow-creator/references/workflow-inputs.md +392 -0
  21. package/.claude/agents/debugger.md +2 -2
  22. package/.claude/agents/reviewer.md +1 -1
  23. package/.claude/agents/worker.md +2 -2
  24. package/.github/agents/debugger.md +1 -1
  25. package/.github/agents/worker.md +1 -1
  26. package/.mcp.json +5 -1
  27. package/.opencode/agents/debugger.md +1 -1
  28. package/.opencode/agents/worker.md +1 -1
  29. package/README.md +236 -201
  30. package/dist/sdk/define-workflow.d.ts +11 -6
  31. package/dist/sdk/define-workflow.d.ts.map +1 -1
  32. package/dist/sdk/errors.d.ts +10 -0
  33. package/dist/sdk/errors.d.ts.map +1 -1
  34. package/dist/sdk/index.d.ts +21 -9
  35. package/dist/sdk/index.d.ts.map +1 -1
  36. package/dist/sdk/primitives/inputs.d.ts +36 -0
  37. package/dist/sdk/primitives/inputs.d.ts.map +1 -0
  38. package/dist/sdk/primitives/metadata.d.ts +40 -0
  39. package/dist/sdk/primitives/metadata.d.ts.map +1 -0
  40. package/dist/sdk/primitives/run.d.ts +57 -0
  41. package/dist/sdk/primitives/run.d.ts.map +1 -0
  42. package/dist/sdk/primitives/sessions.d.ts +128 -0
  43. package/dist/sdk/primitives/sessions.d.ts.map +1 -0
  44. package/dist/sdk/runtime/executor.d.ts +24 -56
  45. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  46. package/dist/sdk/runtime/orchestrator-entry.d.ts +26 -0
  47. package/dist/sdk/runtime/orchestrator-entry.d.ts.map +1 -0
  48. package/dist/sdk/runtime/tmux.d.ts +20 -0
  49. package/dist/sdk/runtime/tmux.d.ts.map +1 -1
  50. package/dist/sdk/types.d.ts +26 -86
  51. package/dist/sdk/types.d.ts.map +1 -1
  52. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -1
  53. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -1
  54. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
  55. package/dist/sdk/workflows/builtin/open-claude-design/claude/index.d.ts.map +1 -1
  56. package/dist/sdk/workflows/builtin/open-claude-design/copilot/index.d.ts.map +1 -1
  57. package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -1
  58. package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -1
  59. package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -1
  60. package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -1
  61. package/dist/sdk/workflows/index.d.ts +20 -12
  62. package/dist/sdk/workflows/index.d.ts.map +1 -1
  63. package/dist/services/config/additional-instructions.d.ts +1 -1
  64. package/dist/services/config/additional-instructions.d.ts.map +1 -1
  65. package/package.json +4 -4
  66. package/src/cli.ts +39 -56
  67. package/src/commands/builtin-registry.ts +37 -0
  68. package/src/commands/cli/chat/index.ts +1 -3
  69. package/src/{sdk → commands/cli}/management-commands.ts +15 -55
  70. package/src/commands/cli/session.ts +1 -1
  71. package/src/commands/cli/workflow-command.test.ts +250 -16
  72. package/src/commands/cli/workflow-inputs.test.ts +1 -0
  73. package/src/commands/cli/workflow-inputs.ts +13 -3
  74. package/src/commands/cli/workflow-list.test.ts +1 -0
  75. package/src/commands/cli/workflow-list.ts +0 -0
  76. package/src/commands/cli/workflow-status.ts +1 -1
  77. package/src/commands/cli/workflow.ts +191 -11
  78. package/src/sdk/define-workflow.test.ts +47 -16
  79. package/src/sdk/define-workflow.ts +24 -6
  80. package/src/sdk/errors.test.ts +11 -0
  81. package/src/sdk/errors.ts +13 -0
  82. package/src/sdk/index.test.ts +92 -0
  83. package/src/sdk/index.ts +71 -15
  84. package/src/sdk/primitives/inputs.ts +48 -0
  85. package/src/sdk/primitives/metadata.ts +63 -0
  86. package/src/sdk/primitives/run.ts +81 -0
  87. package/src/sdk/primitives/sessions.test.ts +594 -0
  88. package/src/sdk/primitives/sessions.ts +328 -0
  89. package/src/sdk/runtime/executor.ts +36 -115
  90. package/src/sdk/runtime/orchestrator-entry.ts +110 -0
  91. package/src/sdk/runtime/tmux.ts +33 -0
  92. package/src/sdk/types.ts +26 -91
  93. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +1 -0
  94. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +1 -0
  95. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +1 -0
  96. package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +1 -0
  97. package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +1 -0
  98. package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +1 -0
  99. package/src/sdk/workflows/builtin/ralph/claude/index.ts +1 -0
  100. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +1 -0
  101. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +1 -0
  102. package/src/sdk/workflows/index.ts +68 -51
  103. package/src/services/config/additional-instructions.ts +1 -1
  104. package/.agents/skills/test-driven-development/SKILL.md +0 -371
  105. package/.agents/skills/test-driven-development/testing-anti-patterns.md +0 -299
  106. package/dist/commands/cli/session.d.ts +0 -67
  107. package/dist/commands/cli/session.d.ts.map +0 -1
  108. package/dist/commands/cli/workflow-status.d.ts +0 -63
  109. package/dist/commands/cli/workflow-status.d.ts.map +0 -1
  110. package/dist/sdk/commander.d.ts +0 -74
  111. package/dist/sdk/commander.d.ts.map +0 -1
  112. package/dist/sdk/management-commands.d.ts +0 -42
  113. package/dist/sdk/management-commands.d.ts.map +0 -1
  114. package/dist/sdk/workflow-cli.d.ts +0 -103
  115. package/dist/sdk/workflow-cli.d.ts.map +0 -1
  116. package/dist/sdk/workflows/builtin-registry.d.ts +0 -113
  117. package/dist/sdk/workflows/builtin-registry.d.ts.map +0 -1
  118. package/src/sdk/commander.ts +0 -161
  119. package/src/sdk/workflow-cli.ts +0 -409
  120. package/src/sdk/workflows/builtin-registry.ts +0 -23
@@ -0,0 +1,356 @@
1
+ # State and Data Flow
2
+
3
+ Data flows between sessions via `s.save()` (in the producing session) and `transcript()` / `getMessages()` (in consuming sessions or at the `.run()` level). Within a session, use plain TypeScript variables. This is the programmatic equivalent of `globalState` and reducers.
4
+
5
+ ## Between sessions: `s.save()` → `transcript()` / `getMessages()`
6
+
7
+ **Completion rule:** `transcript()` and `getMessages()` can only access data from sessions whose callbacks have already returned (i.e., sessions in the `completedRegistry`). In a `Promise.all()` group, sibling sessions cannot read each other's output — only sessions that completed before the group started are available.
8
+
9
+ ### Saving output
10
+
11
+ Each SDK has its own save pattern:
12
+
13
+ ```ts
14
+ // Claude — pass session ID (auto-reads transcript)
15
+ s.save(s.sessionId);
16
+
17
+ // Copilot — pass SessionEvent[] from getMessages()
18
+ s.save(await s.session.getMessages());
19
+
20
+ // OpenCode — pass response { info, parts } from session.prompt()
21
+ s.save(result.data!);
22
+ ```
23
+
24
+ ### Retrieving as rendered text
25
+
26
+ `s.transcript(handle)` returns `{ path: string, content: string }`:
27
+ - `path` — absolute file path to the transcript on disk
28
+ - `content` — extracted assistant text, ready to embed in prompts
29
+
30
+ Pass the session handle returned by a prior `ctx.stage()` call (handle-based, recommended). The string name `s.transcript("name")` also works when no handle is in scope.
31
+
32
+ ```ts
33
+ .run(async (ctx) => {
34
+ const researchHandle = await ctx.stage({ name: "research" }, {}, {}, async (s) => {
35
+ await s.session.query("Research the topic.");
36
+ s.save(s.sessionId);
37
+ });
38
+
39
+ await ctx.stage({ name: "synthesize" }, {}, {}, async (s) => {
40
+ const research = await s.transcript(researchHandle);
41
+
42
+ // Use rendered text in a prompt
43
+ await s.session.query(`Synthesize this research:\n${research.content}`);
44
+
45
+ // Or reference the file path (useful for Claude file triggers)
46
+ await s.session.query(`Read ${research.path} and summarize the key findings.`);
47
+
48
+ s.save(s.sessionId);
49
+ });
50
+ })
51
+ ```
52
+
53
+ ### Retrieving as raw messages
54
+
55
+ `s.getMessages(handle)` returns `SavedMessage[]` — the native SDK messages exactly as stored:
56
+
57
+ ```ts
58
+ .run(async (ctx) => {
59
+ const researchHandle = await ctx.stage({ name: "research" }, {}, {}, async (s) => {
60
+ // ... research work ...
61
+ s.save(s.sessionId);
62
+ });
63
+
64
+ await ctx.stage({ name: "analyze-results" }, {}, {}, async (s) => {
65
+ const messages = await s.getMessages(researchHandle);
66
+
67
+ // messages is SavedMessage[], where each entry is:
68
+ // { provider: "copilot", data: SessionEvent }
69
+ // { provider: "opencode", data: SessionPromptResponse }
70
+ // { provider: "claude", data: SessionMessage }
71
+
72
+ // Process raw messages for detailed analysis
73
+ for (const msg of messages) {
74
+ if (msg.provider === "copilot") {
75
+ // Access Copilot-specific fields
76
+ const event = msg.data;
77
+ }
78
+ }
79
+ });
80
+ })
81
+ ```
82
+
83
+ ### Returning values from session callbacks
84
+
85
+ Session callbacks can return a value directly. The handle exposes it via `.result`:
86
+
87
+ ```ts
88
+ .run(async (ctx) => {
89
+ const planHandle = await ctx.stage({ name: "plan" }, {}, {}, async (s) => {
90
+ // ... planning work ...
91
+ return { taskCount: 5, priority: "high" };
92
+ });
93
+
94
+ // Access the returned value on the handle
95
+ console.log(planHandle.result.taskCount); // 5
96
+ })
97
+ ```
98
+
99
+ ## Within a session: TypeScript variables
100
+
101
+ Use closures and variables for state within a single session:
102
+
103
+ ```ts
104
+ .run(async (ctx) => {
105
+ await ctx.stage({ name: "review-fix" }, {}, {}, async (s) => {
106
+ // Local state — plain variables
107
+ let consecutiveClean = 0;
108
+ let priorOutput = "";
109
+ const findings: string[] = [];
110
+
111
+ for (let cycle = 0; cycle < 10; cycle++) {
112
+ const result = await s.session.query(
113
+ buildReviewPrompt((s.inputs.prompt ?? ""), priorOutput),
114
+ );
115
+
116
+ // Accumulate findings
117
+ const review = parseReviewResult(extractAssistantText(result, 0));
118
+ if (review) {
119
+ findings.push(...review.findings.map(f => f.title));
120
+ }
121
+
122
+ // Track clean streak
123
+ if (!hasActionableFindings(review, extractAssistantText(result, 0))) {
124
+ consecutiveClean++;
125
+ if (consecutiveClean >= 2) break;
126
+ continue;
127
+ }
128
+ consecutiveClean = 0;
129
+
130
+ // Apply fix
131
+ const fixResult = await s.session.query(buildFixSpec(review, (s.inputs.prompt ?? "")));
132
+ priorOutput = extractAssistantText(fixResult, 0);
133
+ }
134
+
135
+ // All local state is available here
136
+ console.log(`Total findings across cycles: ${findings.length}`);
137
+ s.save(s.sessionId);
138
+ });
139
+ })
140
+ ```
141
+
142
+ ## File-based persistence
143
+
144
+ For data that needs to survive session restarts or be accessible outside the workflow, use file I/O:
145
+
146
+ ```ts
147
+ import { readFile, writeFile, mkdir } from "fs/promises";
148
+ import { join } from "path";
149
+
150
+ .run(async (ctx) => {
151
+ const planHandle = await ctx.stage({ name: "plan" }, {}, {}, async (s) => {
152
+ // Write artifacts to session directory
153
+ const artifactDir = join(s.sessionDir, "artifacts");
154
+ await mkdir(artifactDir, { recursive: true });
155
+
156
+ const report = { timestamp: Date.now(), status: "complete" };
157
+ await writeFile(
158
+ join(artifactDir, "report.json"),
159
+ JSON.stringify(report, null, 2),
160
+ );
161
+
162
+ s.save(s.sessionId);
163
+ });
164
+
165
+ await ctx.stage({ name: "generate-report" }, {}, {}, async (s) => {
166
+ // Read prior session's output via transcript (preferred over path traversal)
167
+ const planTranscript = await s.transcript(planHandle);
168
+
169
+ // Or read artifacts using the transcript's path to locate the session directory
170
+ const planSessionDir = join(planTranscript.path, "..");
171
+ const priorReport = JSON.parse(
172
+ await readFile(join(planSessionDir, "artifacts", "report.json"), "utf-8"),
173
+ );
174
+ });
175
+ })
176
+ ```
177
+
178
+ ## Shared helper functions
179
+
180
+ Extract SDK-agnostic logic into shared helpers. This is the key pattern for building workflows that work across all three SDKs:
181
+
182
+ ```
183
+ src/workflows/my-workflow/
184
+ ├── claude.ts # Claude SDK code — exports WorkflowDefinition
185
+ ├── copilot.ts # Copilot SDK code — exports WorkflowDefinition
186
+ ├── opencode.ts # OpenCode SDK code — exports WorkflowDefinition
187
+ └── helpers/
188
+ ├── prompts.ts # Prompt builders
189
+ ├── parsers.ts # Response parsers
190
+ └── validation.ts # Validation logic
191
+ ```
192
+
193
+ ### Prompt builders
194
+
195
+ ```ts
196
+ // src/workflows/my-workflow/helpers/prompts.ts
197
+ export function buildPlanPrompt(spec: string): string {
198
+ return `Decompose into tasks:\n${spec}`;
199
+ }
200
+
201
+ export function buildReviewPrompt(spec: string, priorOutput?: string): string {
202
+ let prompt = `Review the implementation against:\n${spec}`;
203
+ if (priorOutput) {
204
+ prompt += `\n\nPrior fixes:\n${priorOutput}`;
205
+ }
206
+ return prompt;
207
+ }
208
+ ```
209
+
210
+ ### Response parsers
211
+
212
+ For tolerant JSON parsing, see `failure-modes.md` §F8 — the canonical
213
+ `parseReviewResult` helper uses a layered fallback (direct parse → last
214
+ fenced block → last balanced object) that survives prose interleaving.
215
+ Copy that implementation into `helpers/parsers.ts` and import.
216
+
217
+ ```ts
218
+ // src/workflows/my-workflow/helpers/parsers.ts
219
+ export interface ReviewResult {
220
+ findings: Array<{ title: string; body: string; priority: number }>;
221
+ overall_correctness: string;
222
+ }
223
+
224
+ // See failure-modes.md §F8 for the full implementation.
225
+ export function parseReviewResult(text: string): ReviewResult | null {
226
+ // ... three-layer fallback per §F8
227
+ }
228
+ ```
229
+
230
+ ### Usage in workflows
231
+
232
+ ```ts
233
+ // src/workflows/my-workflow/claude.ts
234
+ import { buildPlanPrompt, buildReviewPrompt } from "./helpers/prompts.ts";
235
+ import { parseReviewResult } from "./helpers/parsers.ts";
236
+
237
+ // ... use in run() callbacks
238
+ ```
239
+
240
+ ## Data flow patterns
241
+
242
+ ### Linear pipeline
243
+
244
+ ```
245
+ Session A → s.save() → Session B reads via s.transcript(handleA)
246
+ → s.save() → Session C reads via s.transcript(handleB)
247
+ ```
248
+
249
+ ### Fan-in (multiple prior sessions)
250
+
251
+ ```ts
252
+ .run(async (ctx) => {
253
+ const researchHandle = await ctx.stage({ name: "research" }, {}, {}, async (s) => {
254
+ // ... research work ...
255
+ s.save(s.sessionId);
256
+ });
257
+ const analysisHandle = await ctx.stage({ name: "analysis" }, {}, {}, async (s) => {
258
+ // ... analysis work ...
259
+ s.save(s.sessionId);
260
+ });
261
+ const feedbackHandle = await ctx.stage({ name: "feedback" }, {}, {}, async (s) => {
262
+ // ... feedback work ...
263
+ s.save(s.sessionId);
264
+ });
265
+
266
+ await ctx.stage({ name: "merge" }, {}, {}, async (s) => {
267
+ const research = await s.transcript(researchHandle);
268
+ const analysis = await s.transcript(analysisHandle);
269
+ const userFeedback = await s.transcript(feedbackHandle);
270
+
271
+ await s.session.query(`Combine these inputs:
272
+ Research: ${research.content}
273
+ Analysis: ${analysis.content}
274
+ Feedback: ${userFeedback.content}`);
275
+ s.save(s.sessionId);
276
+ });
277
+ })
278
+ ```
279
+
280
+ ### Accumulating state across sessions
281
+
282
+ Each session can read all prior completed steps (but not parallel siblings):
283
+
284
+ ```ts
285
+ .run(async (ctx) => {
286
+ const h1 = await ctx.stage({ name: "session-1" }, {}, {}, async (s) => {
287
+ // ...
288
+ s.save(s.sessionId);
289
+ });
290
+ const h2 = await ctx.stage({ name: "session-2" }, {}, {}, async (s) => {
291
+ // ...
292
+ s.save(s.sessionId);
293
+ });
294
+
295
+ await ctx.stage({ name: "session-3" }, {}, {}, async (s) => {
296
+ // Read from any prior completed session via its handle
297
+ const s1 = await s.transcript(h1);
298
+ const s2 = await s.transcript(h2);
299
+
300
+ // Combine and process
301
+ const combined = `${s1.content}\n${s2.content}`;
302
+ // ...
303
+ });
304
+ })
305
+ ```
306
+
307
+ ## Context-Aware Transcript Handoff
308
+
309
+ When passing transcripts between sessions, compress at the boundary to prevent downstream context degradation. Use structured summaries that preserve actionable information while dropping verbose tool output (applies `context-compression` + `context-degradation`):
310
+
311
+ ```ts
312
+ // helpers/compression.ts
313
+ export function compressTranscript(content: string, maxTokenEstimate: number = 4000): string {
314
+ // Rough estimate: ~4 chars/token for English prose, ~2-3 for code.
315
+ // For precise budgeting, use the provider's tokenizer instead.
316
+ const maxChars = maxTokenEstimate * 4;
317
+ if (content.length <= maxChars) return content;
318
+
319
+ // Preserve first and last sections (recency + primacy bias)
320
+ const headSize = Math.floor(maxChars * 0.4);
321
+ const tailSize = Math.floor(maxChars * 0.4);
322
+ const head = content.slice(0, headSize);
323
+ const tail = content.slice(-tailSize);
324
+
325
+ return `${head}\n\n[... ${content.length - headSize - tailSize} chars compressed ...]\n\n${tail}`;
326
+ }
327
+ ```
328
+
329
+ ```ts
330
+ await ctx.stage({ name: "synthesize" }, {}, {}, async (s) => {
331
+ const research = await s.transcript("research");
332
+ // Compress before injecting into prompt to stay within token budget
333
+ const compressed = compressTranscript(research.content, 4000);
334
+ await s.session.query(`Synthesize this research:\n${compressed}`);
335
+ s.save(s.sessionId);
336
+ });
337
+ ```
338
+
339
+ ## File-Based Coordination
340
+
341
+ Use the filesystem as a coordination layer instead of inlining large data into prompts. This applies `filesystem-context`:
342
+
343
+ ```ts
344
+ .run(async (ctx) => {
345
+ await ctx.stage({ name: "plan" }, {}, {}, async (s) => {
346
+ await s.session.query(`Create a plan for: ${(s.inputs.prompt ?? "")}\n\nWrite it to plan.md.`);
347
+ s.save(s.sessionId);
348
+ });
349
+
350
+ await ctx.stage({ name: "execute" }, {}, {}, async (s) => {
351
+ // Reference the file by path — lets the agent read selectively
352
+ await s.session.query(`Read plan.md and implement each task. Mark tasks done as you go.`);
353
+ s.save(s.sessionId);
354
+ });
355
+ })
356
+ ```
@@ -0,0 +1,234 @@
1
+ # User Input (mid-workflow)
2
+
3
+ This reference covers **mid-workflow** user interaction — pausing a running stage to ask the user a question, approve a permission, or confirm a decision. It's the programmatic equivalent of `.askUserQuestion()`.
4
+
5
+ For **invocation-time** inputs (the values the user supplies when they launch the workflow from the CLI or the picker), see `workflow-inputs.md` instead. Invocation-time inputs are declared on `defineWorkflow({ inputs: [...] })` and arrive in `ctx.inputs` before any stage starts — the workflow author reads them via `ctx.inputs.<name>`.
6
+
7
+ ## Claude
8
+
9
+ Never import `query` from `@anthropic-ai/claude-agent-sdk` inside a stage
10
+ callback — that's the F16 anti-pattern (see `failure-modes.md` §F16). All
11
+ options route through `s.session.query(prompt, sdkOptions)` in headless
12
+ stages, or through `chatFlags` in interactive stages.
13
+
14
+ ### Via `canUseTool` callback (headless stages only)
15
+
16
+ `canUseTool` is an SDK option — it only applies in a headless stage, where
17
+ the second argument to `s.session.query()` is forwarded to the Agent SDK as
18
+ `Partial<SDKOptions>`. In interactive stages the option is silently ignored
19
+ because `s.session.query()` is driving the `claude` CLI binary, not the SDK.
20
+
21
+ ```ts
22
+ await ctx.stage(
23
+ { name: "implement", headless: true },
24
+ {}, {},
25
+ async (s) => {
26
+ const messages = await s.session.query(
27
+ "Implement the feature, but ask me before making any database changes.",
28
+ {
29
+ canUseTool: async (toolName, toolInput) => {
30
+ if (toolName === "Write" && typeof toolInput.file_path === "string" && toolInput.file_path.includes("migration")) {
31
+ const approved = await promptUser("Allow database migration?");
32
+ return approved
33
+ ? { behavior: "allow", updatedInput: toolInput }
34
+ : { behavior: "deny", message: "User declined migration" };
35
+ }
36
+ return { behavior: "allow", updatedInput: toolInput };
37
+ },
38
+ },
39
+ );
40
+ s.save(s.sessionId);
41
+ return extractAssistantText(messages, 0);
42
+ },
43
+ );
44
+ ```
45
+
46
+ ### Via `AskUserQuestion` tool
47
+
48
+ Allow the agent to ask the user questions by including `AskUserQuestion` in
49
+ `allowedTools`. This works for both interactive stages (via `chatFlags`) and
50
+ headless stages (via sdkOptions on `s.session.query()`).
51
+
52
+ **Interactive stage** — pass the tool allowlist via `chatFlags`:
53
+
54
+ ```ts
55
+ await ctx.stage(
56
+ { name: "implement" },
57
+ { chatFlags: ["--allowed-tools", "Read,Write,Edit,Bash,AskUserQuestion"] },
58
+ {},
59
+ async (s) => {
60
+ await s.session.query(s.inputs.prompt ?? "");
61
+ s.save(s.sessionId);
62
+ },
63
+ );
64
+ ```
65
+
66
+ **Headless stage** — pass `allowedTools` in the sdkOptions:
67
+
68
+ ```ts
69
+ await ctx.stage(
70
+ { name: "implement", headless: true },
71
+ {}, {},
72
+ async (s) => {
73
+ const messages = await s.session.query(s.inputs.prompt ?? "", {
74
+ allowedTools: ["Read", "Write", "Edit", "Bash", "AskUserQuestion"],
75
+ });
76
+ s.save(s.sessionId);
77
+ return extractAssistantText(messages, 0);
78
+ },
79
+ );
80
+ ```
81
+
82
+ ### Via streaming input (headless stages only)
83
+
84
+ The Agent SDK's `streamInput()` feeds additional input while a query is
85
+ running. It's only reachable from headless stages via an async iterable
86
+ prompt — pass an `AsyncIterable<SDKUserMessage>` as the first argument to
87
+ `s.session.query()` instead of a plain string. In interactive stages, send
88
+ follow-up turns with another `s.session.query()` call to the same session.
89
+
90
+ ## Copilot
91
+
92
+ Session callbacks (`onUserInputRequest`, `onElicitationRequest`,
93
+ `onPermissionRequest`) are passed as `sessionOpts` — the third argument to
94
+ `ctx.stage()`. The runtime forwards them to `client.createSession()`.
95
+ `onPermissionRequest` defaults to `approveAll` when not specified.
96
+
97
+ ### Via `onUserInputRequest`
98
+
99
+ Handle `ask_user` tool requests from the agent:
100
+
101
+ ```ts
102
+ await ctx.stage({ name: "plan" }, {}, {
103
+ onUserInputRequest: async (request) => {
104
+ // request.question contains the agent's question
105
+ const answer = await promptUser(request.question);
106
+ return answer;
107
+ },
108
+ }, async (s) => {
109
+ await s.session.send({ prompt: (s.inputs.prompt ?? "") });
110
+ s.save(await s.session.getMessages());
111
+ });
112
+ ```
113
+
114
+ ### Via `onElicitationRequest`
115
+
116
+ For form-style UI with structured options:
117
+
118
+ ```ts
119
+ await ctx.stage({ name: "plan" }, {}, {
120
+ onPermissionRequest: approveAll,
121
+ onElicitationRequest: async (request) => {
122
+ // request contains form fields and options
123
+ return {
124
+ action: "submit",
125
+ values: { strategy: "conservative", confirm: true },
126
+ };
127
+ },
128
+ }, async (s) => {
129
+ await s.session.send({ prompt: (s.inputs.prompt ?? "") });
130
+ s.save(await s.session.getMessages());
131
+ });
132
+ ```
133
+
134
+ ### Programmatic approval
135
+
136
+ For fully autonomous workflows, use `approveAll` to skip all permission prompts.
137
+ This is the **default** when `onPermissionRequest` is not specified in `sessionOpts`:
138
+
139
+ ```ts
140
+ import { approveAll } from "@github/copilot-sdk";
141
+
142
+ // Explicit (same as the default):
143
+ await ctx.stage({ name: "plan" }, {}, { onPermissionRequest: approveAll }, async (s) => {
144
+ await s.session.send({ prompt: (s.inputs.prompt ?? "") });
145
+ s.save(await s.session.getMessages());
146
+ });
147
+ ```
148
+
149
+ ### Custom permission handling
150
+
151
+ For fine-grained control over permissions:
152
+
153
+ ```ts
154
+ await ctx.stage({ name: "plan" }, {}, {
155
+ onPermissionRequest: async (request) => {
156
+ // request.kind: "shell" | "write" | "read" | "mcp" | "custom-tool" | "url" | "memory" | "hook"
157
+ if (request.kind === "shell" && request.command?.includes("rm -rf")) {
158
+ return { kind: "denied-permanently", reason: "Dangerous command" };
159
+ }
160
+ return { kind: "approved" };
161
+ },
162
+ }, async (s) => {
163
+ await s.session.send({ prompt: (s.inputs.prompt ?? "") });
164
+ s.save(await s.session.getMessages());
165
+ });
166
+ ```
167
+
168
+ ## OpenCode
169
+
170
+ The `s.client` and `s.session` are auto-created by the runtime. Use them
171
+ directly — no manual client creation needed.
172
+
173
+ ### Via TUI control endpoints
174
+
175
+ OpenCode uses TUI control endpoints for user interaction:
176
+
177
+ ```ts
178
+ // Inside a ctx.stage() callback:
179
+ async (s) => {
180
+ // Wait for the next TUI control request
181
+ const controlRequest = await s.client.tui.next();
182
+
183
+ // Respond to the control request
184
+ await s.client.tui.response({
185
+ requestID: controlRequest.data!.id,
186
+ response: "User's answer here",
187
+ });
188
+ },
189
+ ```
190
+
191
+ ### Via permission handling
192
+
193
+ Handle permission requests programmatically:
194
+
195
+ ```ts
196
+ // Inside a ctx.stage() callback:
197
+ async (s) => {
198
+ // Subscribe to events and handle permission requests
199
+ const unsubscribe = await s.client.event.subscribe((event) => {
200
+ if (event.type === "permission.requested") {
201
+ s.client.session.permission({
202
+ sessionID: event.sessionID,
203
+ permissionID: event.permissionID,
204
+ approved: true,
205
+ });
206
+ }
207
+ });
208
+ },
209
+ ```
210
+
211
+ ## Combining user input with control flow
212
+
213
+ Use user input results in conditional logic. This Claude example uses
214
+ `AskUserQuestion` by including it in `allowedTools` — the agent asks the
215
+ user directly, and you parse the response to branch:
216
+
217
+ ```ts
218
+ // Inside a ctx.stage() callback (Claude example).
219
+ // AskUserQuestion must be in allowedTools — see "Via AskUserQuestion tool" above.
220
+ async (s) => {
221
+ const plan = await s.transcript("plan"); // or s.transcript(handle) if a handle is in scope (preferred)
222
+
223
+ // Let the agent ask the user for approval via AskUserQuestion
224
+ const result = await s.session.query(
225
+ `Here is the plan:\n${plan.content}\n\nAsk the user if they approve this plan. ` +
226
+ `If they approve, execute it. If not, revise it based on their feedback.`,
227
+ );
228
+
229
+ s.save(s.sessionId);
230
+ },
231
+ ```
232
+
233
+ For Copilot, use `onUserInputRequest` in `sessionOpts` (see above). For
234
+ OpenCode, use the TUI control endpoints.