@bastani/atomic 0.5.23 → 0.5.24

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.
@@ -196,6 +196,11 @@ export declare function extractAssistantText(msgs: ReadonlyArray<{
196
196
  * ```
197
197
  */
198
198
  export declare function claudeQuery(options: ClaudeQueryOptions): Promise<SessionMessage[]>;
199
+ /**
200
+ * Merge two `disallowedTools` lists, preserving caller entries and appending
201
+ * any extras that aren't already present. Exported for unit testing.
202
+ */
203
+ export declare function mergeDisallowedTools(existing: string[] | undefined, extras: string[]): string[];
199
204
  /**
200
205
  * Synthetic client wrapper for Claude stages.
201
206
  * Auto-starts the Claude CLI in the tmux pane during `start()`.
@@ -232,7 +237,7 @@ export declare class ClaudeSessionWrapper {
232
237
  * Send a prompt to Claude and wait for the response.
233
238
  *
234
239
  * The `_options` parameter exists for signature compatibility with
235
- * {@link HeadlessClaudeSessionWrapper.query} (which forwards SDK options
240
+ * {@link HeadlessClaudeSessionWrapper#query} (which forwards SDK options
236
241
  * like `agent`, `permissionMode`, etc. to the Agent SDK). In the
237
242
  * interactive pane path these options don't apply — we're driving the
238
243
  * `claude` CLI binary, not the SDK — so they are silently ignored.
@@ -250,7 +255,7 @@ export declare class HeadlessClaudeClientWrapper {
250
255
  * Headless Claude stages don't pre-allocate a session — each `query()` call
251
256
  * to {@link HeadlessClaudeSessionWrapper} spawns a fresh Agent SDK run that
252
257
  * emits its own `session_id`. We still return an empty string here so the
253
- * method signature matches {@link ClaudeClientWrapper.start}.
258
+ * method signature matches {@link ClaudeClientWrapper#start}.
254
259
  */
255
260
  start(): Promise<string>;
256
261
  stop(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../../src/sdk/providers/claude.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,OAAO,IAAI,UAAU,EAC3B,MAAM,gCAAgC,CAAC;AAgCxC;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWtE;AA+GD,MAAM,WAAW,oBAAoB;IACnC,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,sIAAsI;IACtI,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,CAexF;AAiID;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,OAAO,CAUnE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,cAAc,CAClC,eAAe,EAAE,MAAM,EACvB,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,EACjC,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,IAAI,CAAC,CAyCf;AAMD;;;;;;GAMG;AACH,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAE3D;AAiED;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGjF;AAuCD;;GAEG;AACH,wBAAsB,WAAW,CAC/B,eAAe,EAAE,MAAM,EACvB,qBAAqB,EAAE,MAAM,GAC5B,OAAO,CAAC,cAAc,EAAE,CAAC,CAqG3B;AAMD,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,EACvD,UAAU,EAAE,MAAM,GACjB,MAAM,CAoBR;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CA8FxF;AAMD;;;GAGG;AACH,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAoD;gBAGvE,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAO;IAM9D;;;;;;;OAOG;IACG,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAQ9B,yEAAyE;IACnE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAC5B;AAED;;;GAGG;AACH,qBAAa,oBAAoB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA2C;gBAG/D,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI;IAOpC;;;;;;;;OAQG;IACG,KAAK,CACT,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAC7B,OAAO,CAAC,cAAc,EAAE,CAAC;IAQ5B,gEAAgE;IAC1D,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;CAClC;AAMD;;;GAGG;AACH,qBAAa,2BAA2B;IACtC;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAGxB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAC5B;AAED;;;;;;;;;;GAUG;AACH,qBAAa,4BAA4B;IACvC,QAAQ,CAAC,MAAM,MAAM;IACrB;;;;;OAKG;IACH,OAAO,CAAC,cAAc,CAAc;IAEpC,IAAI,SAAS,IAAI,MAAM,CAEtB;IAEK,KAAK,CACT,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,cAAc,CAAC,EAC9C,OAAO,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAC5B,OAAO,CAAC,cAAc,EAAE,CAAC;IAetB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;CAClC;AAQD;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,+DAejC,CAAC"}
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../../src/sdk/providers/claude.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,25 @@
1
1
  /**
2
- * OpenCode workflow source validation.
2
+ * OpenCode workflow source validation + headless env helper.
3
3
  *
4
4
  * Checks that OpenCode workflow source files use the runtime-managed
5
- * `s.client` and `s.session` instead of manual SDK client creation.
5
+ * `s.client` and `s.session` instead of manual SDK client creation, and
6
+ * exports the `OPENCODE_CLIENT` override used to keep the interactive
7
+ * `question` tool out of headless stages.
6
8
  */
9
+ /**
10
+ * Client identifier passed to SDK-spawned OpenCode subprocesses in headless
11
+ * stages.
12
+ *
13
+ * OpenCode only registers its interactive `question` tool when
14
+ * `OPENCODE_CLIENT` is one of `"app" | "cli" | "desktop"` (see
15
+ * `packages/opencode/src/tool/registry.ts` upstream — the `questionEnabled`
16
+ * gate). In unattended runs nobody is attached to answer, so we identify
17
+ * ourselves as `"sdk"` to keep the tool off the registry entirely. This
18
+ * mirrors how the upstream ACP integration excludes the tool by default
19
+ * (`packages/opencode/src/cli/cmd/acp.ts` sets `OPENCODE_CLIENT=acp`).
20
+ */
21
+ export declare const HEADLESS_OPENCODE_CLIENT_ID = "sdk";
22
+ export declare function withHeadlessOpencodeEnv<T>(fn: () => Promise<T>): Promise<T>;
7
23
  /**
8
24
  * Validate an OpenCode workflow source file for common mistakes.
9
25
  */
@@ -1 +1 @@
1
- {"version":3,"file":"opencode.d.ts","sourceRoot":"","sources":["../../../src/sdk/providers/opencode.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;GAEG;AACH,eAAO,MAAM,wBAAwB,+DAenC,CAAC"}
1
+ {"version":3,"file":"opencode.d.ts","sourceRoot":"","sources":["../../../src/sdk/providers/opencode.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,2BAA2B,QAAQ,CAAC;AAuBjD,wBAAsB,uBAAuB,CAAC,CAAC,EAC7C,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAmBZ;AAED;;GAEG;AACH,eAAO,MAAM,wBAAwB,+DAenC,CAAC"}
@@ -219,5 +219,10 @@ export declare function watchCopilotSessionForHIL(session: CopilotHILSessionSurf
219
219
  * Exported for unit testing.
220
220
  */
221
221
  export declare function watchCopilotSessionForElicitation(session: CopilotHILSessionSurface, onHIL: (waiting: boolean) => void): () => void;
222
+ /**
223
+ * Append tool names to a Copilot `excludedTools` list without duplicating
224
+ * entries the caller already supplied. Exported for unit testing.
225
+ */
226
+ export declare function mergeExcludedTools(existing: string[] | undefined, extras: string[]): string[];
222
227
  export declare function runOrchestrator(): Promise<void>;
223
228
  //# sourceMappingURL=executor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/sdk/runtime/executor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,OAAO,KAAK,EACV,kBAAkB,EAMlB,SAAS,EAET,YAAY,EAMb,MAAM,aAAa,CAAC;AAuErB,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAa5C,MAAM,WAAW,kBAAkB;IACjC,uCAAuC;IACvC,UAAU,EAAE,kBAAkB,CAAC;IAC/B,iBAAiB;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,qEAAqE;IACrE,YAAY,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAoDD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,GAAG,SAAS,CAgB1D;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,IAAI,OAAO,CAKtD;AAyBD;;;;;GAKG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAMhD;AAiHD;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAKzC;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAMzC;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgBxB;AAMD;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAkFf;AAoDD,gGAAgG;AAChG,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAOvE;AA2OD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,MAAM,CA0CrE;AAOD;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACjF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,CAAC,EAClC,OAAO,EAAE,yBAAyB,EAClC,UAAU,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GACrC,CAAC,OAAO,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAuB5B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;CAC5D;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,aAAa,CAAC,gBAAgB,CAAC,EACvC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAChC,OAAO,CAAC,IAAI,CAAC,CAef;AAED;;;;;GAKG;AACH,MAAM,WAAW,wBAAwB;IACvC,EAAE,CACA,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAC3C,MAAM,IAAI,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,wBAAwB,EACjC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAChC,MAAM,IAAI,CA0BZ;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,wBAAwB,EACjC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAChC,MAAM,IAAI,CAwBZ;AA+nBD,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAkMrD"}
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/sdk/runtime/executor.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.5.23",
3
+ "version": "0.5.24",
4
4
  "description": "Configuration management CLI and SDK for coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,7 +24,7 @@ import {
24
24
  type SDKUserMessage,
25
25
  type Options as SDKOptions,
26
26
  } from "@anthropic-ai/claude-agent-sdk";
27
- import { sendKeysAndSubmit } from "../runtime/tmux.ts";
27
+ import { sendKeysAndSubmit, waitForPaneReady } from "../runtime/tmux.ts";
28
28
  import { escBash } from "../runtime/executor.ts";
29
29
  import { watch, unlink, mkdir, writeFile } from "node:fs/promises";
30
30
  import { existsSync, writeFileSync } from "node:fs";
@@ -283,10 +283,18 @@ async function spawnClaudeWithPrompt(
283
283
  argvPrompt,
284
284
  ].join(" ");
285
285
 
286
+ // Wait for the pane's shell to finish init and activate its line editor
287
+ // (starship `❯` / bare zsh `>`). Sending keys before this point lets zsh's
288
+ // TCSAFLUSH on ZLE startup discard the buffered `\r`, so the command ends
289
+ // up displayed at the prompt but never submitted. This wait was dropped in
290
+ // eca267b0 alongside the post-submit pane-scrape — we only needed to drop
291
+ // the latter.
292
+ await waitForPaneReady(paneId, readyTimeoutMs);
293
+
286
294
  await sendKeysAndSubmit(paneId, cmd);
287
295
 
288
296
  // SDK-native readiness signal: wait for Claude to create its JSONL file
289
- // at the known UUID path. No pane scraping, no paneLooksReady check.
297
+ // at the known UUID path.
290
298
  await waitForSessionFileAt(sessionId, readyTimeoutMs);
291
299
  }
292
300
 
@@ -912,6 +920,21 @@ export async function claudeQuery(options: ClaudeQueryOptions): Promise<SessionM
912
920
  // Synthetic wrappers — uniform s.client / s.session API for Claude stages
913
921
  // ---------------------------------------------------------------------------
914
922
 
923
+ /**
924
+ * Merge two `disallowedTools` lists, preserving caller entries and appending
925
+ * any extras that aren't already present. Exported for unit testing.
926
+ */
927
+ export function mergeDisallowedTools(
928
+ existing: string[] | undefined,
929
+ extras: string[],
930
+ ): string[] {
931
+ const merged = [...(existing ?? [])];
932
+ for (const tool of extras) {
933
+ if (!merged.includes(tool)) merged.push(tool);
934
+ }
935
+ return merged;
936
+ }
937
+
915
938
  /**
916
939
  * Synthetic client wrapper for Claude stages.
917
940
  * Auto-starts the Claude CLI in the tmux pane during `start()`.
@@ -971,7 +994,7 @@ export class ClaudeSessionWrapper {
971
994
  * Send a prompt to Claude and wait for the response.
972
995
  *
973
996
  * The `_options` parameter exists for signature compatibility with
974
- * {@link HeadlessClaudeSessionWrapper.query} (which forwards SDK options
997
+ * {@link HeadlessClaudeSessionWrapper#query} (which forwards SDK options
975
998
  * like `agent`, `permissionMode`, etc. to the Agent SDK). In the
976
999
  * interactive pane path these options don't apply — we're driving the
977
1000
  * `claude` CLI binary, not the SDK — so they are silently ignored.
@@ -1004,7 +1027,7 @@ export class HeadlessClaudeClientWrapper {
1004
1027
  * Headless Claude stages don't pre-allocate a session — each `query()` call
1005
1028
  * to {@link HeadlessClaudeSessionWrapper} spawns a fresh Agent SDK run that
1006
1029
  * emits its own `session_id`. We still return an empty string here so the
1007
- * method signature matches {@link ClaudeClientWrapper.start}.
1030
+ * method signature matches {@link ClaudeClientWrapper#start}.
1008
1031
  */
1009
1032
  async start(): Promise<string> {
1010
1033
  return "";
@@ -1041,18 +1064,40 @@ export class HeadlessClaudeSessionWrapper {
1041
1064
  prompt: string | AsyncIterable<SDKUserMessage>,
1042
1065
  options?: Partial<SDKOptions>,
1043
1066
  ): Promise<SessionMessage[]> {
1067
+ // Auto-deny the `AskUserQuestion` tool in headless runs. Without this, the
1068
+ // agent can call it and the SDK query will sit blocked forever since no
1069
+ // human is attached to answer.
1070
+ const sdkOpts = options ?? {};
1071
+ const headlessSdkOpts: Partial<SDKOptions> = {
1072
+ ...sdkOpts,
1073
+ disallowedTools: mergeDisallowedTools(sdkOpts.disallowedTools, [
1074
+ "AskUserQuestion",
1075
+ ]),
1076
+ };
1077
+
1044
1078
  let sdkSessionId = "";
1045
- for await (const msg of sdkQuery({ prompt, options: options ?? {} })) {
1046
- if (msg.type === "result") {
1047
- sdkSessionId = String((msg as Record<string, unknown>).session_id ?? "");
1079
+ try {
1080
+ for await (const msg of sdkQuery({ prompt, options: options ?? {} })) {
1081
+ if (msg.type === "result") {
1082
+ sdkSessionId = String(
1083
+ (msg as Record<string, unknown>).session_id ?? "",
1084
+ );
1085
+ }
1048
1086
  }
1087
+ } catch (err) {
1088
+ const detail = err instanceof Error ? err.message : String(err);
1089
+ throw new Error(`Claude SDK query failed: ${detail}`);
1049
1090
  }
1050
- // Read the transcript to return native SessionMessage[]
1051
- if (sdkSessionId) {
1052
- this._lastSessionId = sdkSessionId;
1053
- return getSessionMessages(sdkSessionId, { dir: process.cwd() });
1091
+ if (!sdkSessionId) {
1092
+ throw new Error(
1093
+ "Claude SDK query completed without a `result` message — " +
1094
+ "likely a stream idle timeout, aborted request, or upstream API error. " +
1095
+ "Set CLAUDE_ENABLE_STREAM_WATCHDOG=1 (and tune CLAUDE_STREAM_IDLE_TIMEOUT_MS / " +
1096
+ "API_TIMEOUT_MS) so the CLI surfaces a concrete failure instead of exiting silently.",
1097
+ );
1054
1098
  }
1055
- return [];
1099
+ this._lastSessionId = sdkSessionId;
1100
+ return getSessionMessages(sdkSessionId, { dir: process.cwd() });
1056
1101
  }
1057
1102
 
1058
1103
  async disconnect(): Promise<void> {}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Tests for the headless human-in-the-loop (HIL) auto-deny policy.
3
+ *
4
+ * In unattended runs (headless stages), no human is attached to answer
5
+ * interactive questions from the agent. If the SDK's ask-user tool is
6
+ * not disabled, a query will sit blocked forever. These tests verify
7
+ * each provider's headless integration blocks the relevant tool.
8
+ */
9
+
10
+ import { test, expect, describe } from "bun:test";
11
+ import { mergeDisallowedTools } from "./claude.ts";
12
+ import {
13
+ HEADLESS_OPENCODE_CLIENT_ID,
14
+ withHeadlessOpencodeEnv,
15
+ } from "./opencode.ts";
16
+ import { mergeExcludedTools } from "../runtime/executor.ts";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Claude — disallowedTools: ["AskUserQuestion"]
20
+ // ---------------------------------------------------------------------------
21
+
22
+ describe("mergeDisallowedTools (Claude)", () => {
23
+ test("adds AskUserQuestion when no existing disallow list", () => {
24
+ expect(mergeDisallowedTools(undefined, ["AskUserQuestion"])).toEqual([
25
+ "AskUserQuestion",
26
+ ]);
27
+ });
28
+
29
+ test("preserves caller-supplied entries", () => {
30
+ expect(
31
+ mergeDisallowedTools(["Bash", "WebFetch"], ["AskUserQuestion"]),
32
+ ).toEqual(["Bash", "WebFetch", "AskUserQuestion"]);
33
+ });
34
+
35
+ test("does not duplicate AskUserQuestion if caller already disallowed it", () => {
36
+ expect(
37
+ mergeDisallowedTools(["AskUserQuestion", "Bash"], ["AskUserQuestion"]),
38
+ ).toEqual(["AskUserQuestion", "Bash"]);
39
+ });
40
+ });
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Copilot — excludedTools: ["ask_user"]
44
+ // ---------------------------------------------------------------------------
45
+
46
+ describe("mergeExcludedTools (Copilot)", () => {
47
+ test("adds ask_user when no existing excluded list", () => {
48
+ expect(mergeExcludedTools(undefined, ["ask_user"])).toEqual(["ask_user"]);
49
+ });
50
+
51
+ test("preserves caller-supplied entries", () => {
52
+ expect(mergeExcludedTools(["bash"], ["ask_user"])).toEqual([
53
+ "bash",
54
+ "ask_user",
55
+ ]);
56
+ });
57
+
58
+ test("does not duplicate ask_user if caller already excluded it", () => {
59
+ expect(mergeExcludedTools(["ask_user", "bash"], ["ask_user"])).toEqual([
60
+ "ask_user",
61
+ "bash",
62
+ ]);
63
+ });
64
+ });
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // OpenCode — OPENCODE_CLIENT override excludes the question tool
68
+ // ---------------------------------------------------------------------------
69
+ //
70
+ // Upstream (`packages/opencode/src/tool/registry.ts`) gates the question
71
+ // tool on `["app","cli","desktop"].includes(OPENCODE_CLIENT)`. The SDK
72
+ // spawns `opencode serve` via cross-spawn and inherits `process.env` at
73
+ // fork time, so scoping the override around `createOpencode()` is
74
+ // sufficient to keep the tool off the registry.
75
+
76
+ describe("withHeadlessOpencodeEnv", () => {
77
+ test("sets OPENCODE_CLIENT to the headless id while fn runs", async () => {
78
+ const seen: string | undefined = await withHeadlessOpencodeEnv(async () =>
79
+ process.env.OPENCODE_CLIENT,
80
+ );
81
+ expect(seen).toBe(HEADLESS_OPENCODE_CLIENT_ID);
82
+ });
83
+
84
+ test("restores prior value when it was set before", async () => {
85
+ const before = process.env.OPENCODE_CLIENT;
86
+ process.env.OPENCODE_CLIENT = "preexisting";
87
+ try {
88
+ await withHeadlessOpencodeEnv(async () => {});
89
+ expect(process.env.OPENCODE_CLIENT).toBe("preexisting");
90
+ } finally {
91
+ if (before === undefined) delete process.env.OPENCODE_CLIENT;
92
+ else process.env.OPENCODE_CLIENT = before;
93
+ }
94
+ });
95
+
96
+ test("unsets the variable when it was unset before", async () => {
97
+ const before = process.env.OPENCODE_CLIENT;
98
+ delete process.env.OPENCODE_CLIENT;
99
+ try {
100
+ await withHeadlessOpencodeEnv(async () => {});
101
+ expect(
102
+ Object.prototype.hasOwnProperty.call(process.env, "OPENCODE_CLIENT"),
103
+ ).toBe(false);
104
+ } finally {
105
+ if (before === undefined) delete process.env.OPENCODE_CLIENT;
106
+ else process.env.OPENCODE_CLIENT = before;
107
+ }
108
+ });
109
+
110
+ test("restores prior value even when fn throws", async () => {
111
+ const before = process.env.OPENCODE_CLIENT;
112
+ process.env.OPENCODE_CLIENT = "preexisting";
113
+ try {
114
+ await expect(
115
+ withHeadlessOpencodeEnv(async () => {
116
+ throw new Error("boom");
117
+ }),
118
+ ).rejects.toThrow("boom");
119
+ expect(process.env.OPENCODE_CLIENT).toBe("preexisting");
120
+ } finally {
121
+ if (before === undefined) delete process.env.OPENCODE_CLIENT;
122
+ else process.env.OPENCODE_CLIENT = before;
123
+ }
124
+ });
125
+
126
+ test("is not one of the values that would enable the question tool", () => {
127
+ // Guard against accidental future edits: picking "cli", "app", or
128
+ // "desktop" here would silently re-enable the interactive question tool
129
+ // and make headless stages hang again.
130
+ expect(["app", "cli", "desktop"]).not.toContain(
131
+ HEADLESS_OPENCODE_CLIENT_ID,
132
+ );
133
+ });
134
+
135
+ test("concurrent uses do not leak the override after both unwind", async () => {
136
+ // Race regression: without a reference counter, the second concurrent
137
+ // stage reads the first's already-overridden value as its "prior" and
138
+ // restores "sdk" instead of the true original on unwind.
139
+ const before = process.env.OPENCODE_CLIENT;
140
+ delete process.env.OPENCODE_CLIENT;
141
+ try {
142
+ let releaseA!: () => void;
143
+ let releaseB!: () => void;
144
+ const waitA = new Promise<void>((r) => {
145
+ releaseA = r;
146
+ });
147
+ const waitB = new Promise<void>((r) => {
148
+ releaseB = r;
149
+ });
150
+
151
+ const a = withHeadlessOpencodeEnv(async () => {
152
+ await waitA;
153
+ });
154
+ const b = withHeadlessOpencodeEnv(async () => {
155
+ await waitB;
156
+ });
157
+
158
+ // Release A first, then B — the order that exposes the naive bug.
159
+ releaseA();
160
+ releaseB();
161
+ await Promise.all([a, b]);
162
+
163
+ expect(
164
+ Object.prototype.hasOwnProperty.call(process.env, "OPENCODE_CLIENT"),
165
+ ).toBe(false);
166
+ } finally {
167
+ if (before === undefined) delete process.env.OPENCODE_CLIENT;
168
+ else process.env.OPENCODE_CLIENT = before;
169
+ }
170
+ });
171
+ });
@@ -1,12 +1,72 @@
1
1
  /**
2
- * OpenCode workflow source validation.
2
+ * OpenCode workflow source validation + headless env helper.
3
3
  *
4
4
  * Checks that OpenCode workflow source files use the runtime-managed
5
- * `s.client` and `s.session` instead of manual SDK client creation.
5
+ * `s.client` and `s.session` instead of manual SDK client creation, and
6
+ * exports the `OPENCODE_CLIENT` override used to keep the interactive
7
+ * `question` tool out of headless stages.
6
8
  */
7
9
 
8
10
  import { createProviderValidator } from "../types.ts";
9
11
 
12
+ /**
13
+ * Client identifier passed to SDK-spawned OpenCode subprocesses in headless
14
+ * stages.
15
+ *
16
+ * OpenCode only registers its interactive `question` tool when
17
+ * `OPENCODE_CLIENT` is one of `"app" | "cli" | "desktop"` (see
18
+ * `packages/opencode/src/tool/registry.ts` upstream — the `questionEnabled`
19
+ * gate). In unattended runs nobody is attached to answer, so we identify
20
+ * ourselves as `"sdk"` to keep the tool off the registry entirely. This
21
+ * mirrors how the upstream ACP integration excludes the tool by default
22
+ * (`packages/opencode/src/cli/cmd/acp.ts` sets `OPENCODE_CLIENT=acp`).
23
+ */
24
+ export const HEADLESS_OPENCODE_CLIENT_ID = "sdk";
25
+
26
+ /**
27
+ * Run `fn` with `process.env.OPENCODE_CLIENT` set to
28
+ * `HEADLESS_OPENCODE_CLIENT_ID`, restoring the prior value afterward. The
29
+ * SDK spawns `opencode serve` via `cross-spawn` and inherits the parent's
30
+ * env at spawn time, so scoping the override around `createOpencode(...)`
31
+ * is enough to influence the subprocess without leaking into later work.
32
+ *
33
+ * A reference counter keeps the override in place while any concurrent
34
+ * headless spawn is still running — otherwise two parallel stages can
35
+ * race, and the second one restores the first one's already-overridden
36
+ * value as if it were the original. The captured "pre-override" state is
37
+ * only read on the outermost entry and only replayed on the outermost
38
+ * exit.
39
+ *
40
+ * Prior value handling is explicit so we distinguish "was unset" from
41
+ * "was set to empty string".
42
+ */
43
+ let headlessEnvDepth = 0;
44
+ let headlessEnvHadPrior = false;
45
+ let headlessEnvPrior: string | undefined;
46
+
47
+ export async function withHeadlessOpencodeEnv<T>(
48
+ fn: () => Promise<T>,
49
+ ): Promise<T> {
50
+ if (headlessEnvDepth === 0) {
51
+ headlessEnvHadPrior = Object.prototype.hasOwnProperty.call(
52
+ process.env,
53
+ "OPENCODE_CLIENT",
54
+ );
55
+ headlessEnvPrior = process.env.OPENCODE_CLIENT;
56
+ }
57
+ headlessEnvDepth++;
58
+ try {
59
+ process.env.OPENCODE_CLIENT = HEADLESS_OPENCODE_CLIENT_ID;
60
+ return await fn();
61
+ } finally {
62
+ headlessEnvDepth--;
63
+ if (headlessEnvDepth === 0) {
64
+ if (headlessEnvHadPrior) process.env.OPENCODE_CLIENT = headlessEnvPrior;
65
+ else delete process.env.OPENCODE_CLIENT;
66
+ }
67
+ }
68
+ }
69
+
10
70
  /**
11
71
  * Validate an OpenCode workflow source file for common mistakes.
12
72
  */
@@ -53,6 +53,7 @@ import {
53
53
  HeadlessClaudeClientWrapper,
54
54
  HeadlessClaudeSessionWrapper,
55
55
  } from "../providers/claude.ts";
56
+ import { withHeadlessOpencodeEnv } from "../providers/opencode.ts";
56
57
  import { OrchestratorPanel } from "./panel.tsx";
57
58
  import { GraphFrontierTracker } from "./graph-inference.ts";
58
59
  import { buildSnapshot, writeSnapshot } from "./status-writer.ts";
@@ -1176,6 +1177,21 @@ interface SharedRunnerState {
1176
1177
  failedRegistry: Set<string>;
1177
1178
  }
1178
1179
 
1180
+ /**
1181
+ * Append tool names to a Copilot `excludedTools` list without duplicating
1182
+ * entries the caller already supplied. Exported for unit testing.
1183
+ */
1184
+ export function mergeExcludedTools(
1185
+ existing: string[] | undefined,
1186
+ extras: string[],
1187
+ ): string[] {
1188
+ const merged = [...(existing ?? [])];
1189
+ for (const tool of extras) {
1190
+ if (!merged.includes(tool)) merged.push(tool);
1191
+ }
1192
+ return merged;
1193
+ }
1194
+
1179
1195
  /**
1180
1196
  * Create the provider-specific client and session for a stage.
1181
1197
  * Called by the session runner after server readiness is confirmed.
@@ -1217,10 +1233,22 @@ async function initProviderClientAndSession<A extends AgentType>(
1217
1233
  ? new CopilotClient({ ...copilotClientOpts })
1218
1234
  : new CopilotClient({ ...copilotClientOpts, cliUrl: serverUrl });
1219
1235
  await client.start();
1220
- const session = await client.createSession({
1236
+ // In headless stages, add `ask_user` to the session's excludedTools so
1237
+ // the agent cannot call the interactive question tool — there is no
1238
+ // human attached to answer and the SDK would otherwise sit blocked.
1239
+ const sessionConfig = {
1221
1240
  onPermissionRequest: approveAll,
1222
1241
  ...copilotSessionOpts,
1223
- });
1242
+ ...(headless
1243
+ ? {
1244
+ excludedTools: mergeExcludedTools(
1245
+ copilotSessionOpts.excludedTools,
1246
+ ["ask_user"],
1247
+ ),
1248
+ }
1249
+ : {}),
1250
+ };
1251
+ const session = await client.createSession(sessionConfig);
1224
1252
  if (!headless) {
1225
1253
  await client.setForegroundSessionId(session.sessionId);
1226
1254
  }
@@ -1230,13 +1258,22 @@ async function initProviderClientAndSession<A extends AgentType>(
1230
1258
  const ocSessionOpts = sessionOpts as StageSessionOptions<"opencode">;
1231
1259
  if (headless) {
1232
1260
  const { createOpencode } = await import("@opencode-ai/sdk/v2");
1233
- const oc = await createOpencode({ port: 0 });
1234
- const sessionResult = await oc.client.session.create(ocSessionOpts);
1235
- return {
1236
- client: oc.client,
1237
- session: sessionResult.data!,
1238
- cleanup: () => oc.server.close(),
1239
- } as Result;
1261
+ // Scope OPENCODE_CLIENT=sdk around the SDK spawn so the subprocess
1262
+ // inherits it at fork time. OpenCode only registers its interactive
1263
+ // `question` tool when OPENCODE_CLIENT is "app"/"cli"/"desktop", so
1264
+ // identifying as "sdk" keeps the tool out of the registry entirely
1265
+ // — otherwise an unattended stage can hang forever on question.asked
1266
+ // (the tool's execute calls Question.ask directly and never consults
1267
+ // the session permission ruleset).
1268
+ return await withHeadlessOpencodeEnv(async () => {
1269
+ const oc = await createOpencode({ port: 0 });
1270
+ const sessionResult = await oc.client.session.create(ocSessionOpts);
1271
+ return {
1272
+ client: oc.client,
1273
+ session: sessionResult.data!,
1274
+ cleanup: () => oc.server.close(),
1275
+ } as Result;
1276
+ });
1240
1277
  }
1241
1278
  const { createOpencodeClient } = await import("@opencode-ai/sdk/v2");
1242
1279
  const ocClientOpts = clientOpts as StageClientOptions<"opencode">;
@@ -1622,10 +1659,14 @@ function createSessionRunner(
1622
1659
  // session id. This is what workflows pass to `s.save(s.sessionId)`
1623
1660
  // to disambiguate their own transcript when several sessions run
1624
1661
  // in parallel under the same workflow.
1625
- const providerSessionId = resolveProviderSessionId(
1626
- shared.agent,
1627
- providerSession,
1628
- );
1662
+ //
1663
+ // Exposed as a getter (not a snapshot) because headless Claude stages
1664
+ // don't know their SDK-assigned `session_id` until the first `query()`
1665
+ // completes — `HeadlessClaudeSessionWrapper._lastSessionId` starts empty
1666
+ // and is populated when the SDK emits a `result` event. A snapshot
1667
+ // captured at stage creation would leave `s.sessionId === ""` forever,
1668
+ // so `s.save(s.sessionId)` would always throw "empty Claude session id"
1669
+ // even though the query completed successfully.
1629
1670
  const ctx: SessionContext = {
1630
1671
  client: providerClient,
1631
1672
  session: providerSession,
@@ -1633,7 +1674,9 @@ function createSessionRunner(
1633
1674
  agent: shared.agent,
1634
1675
  sessionDir,
1635
1676
  paneId,
1636
- sessionId: providerSessionId,
1677
+ get sessionId() {
1678
+ return resolveProviderSessionId(shared.agent, providerSession);
1679
+ },
1637
1680
  save,
1638
1681
  transcript: transcriptFn,
1639
1682
  getMessages: getMessagesFn,