@bastani/atomic 0.5.23 → 0.5.24-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.
- package/.agents/skills/workflow-creator/SKILL.md +137 -326
- package/.agents/skills/workflow-creator/references/agent-sessions.md +211 -152
- package/.agents/skills/workflow-creator/references/computation-and-validation.md +12 -37
- package/.agents/skills/workflow-creator/references/control-flow.md +20 -14
- package/.agents/skills/workflow-creator/references/discovery-and-verification.md +1 -1
- package/.agents/skills/workflow-creator/references/failure-modes.md +87 -62
- package/.agents/skills/workflow-creator/references/getting-started.md +14 -40
- package/.agents/skills/workflow-creator/references/running-workflows.md +235 -0
- package/.agents/skills/workflow-creator/references/session-config.md +24 -9
- package/.agents/skills/workflow-creator/references/state-and-data-flow.md +9 -26
- package/.agents/skills/workflow-creator/references/user-input.md +71 -43
- package/.agents/skills/workflow-creator/references/workflow-inputs.md +25 -42
- package/dist/sdk/providers/claude.d.ts +7 -2
- package/dist/sdk/providers/claude.d.ts.map +1 -1
- package/dist/sdk/providers/opencode.d.ts +18 -2
- package/dist/sdk/providers/opencode.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts +5 -0
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/sdk/providers/claude.ts +57 -12
- package/src/sdk/providers/headless-hil-policy.test.ts +171 -0
- package/src/sdk/providers/opencode.ts +62 -2
- package/src/sdk/runtime/executor.ts +57 -14
|
@@ -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
|
|
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
|
|
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;
|
|
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
|
|
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;
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/sdk/runtime/executor.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
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
|
|
1677
|
+
get sessionId() {
|
|
1678
|
+
return resolveProviderSessionId(shared.agent, providerSession);
|
|
1679
|
+
},
|
|
1637
1680
|
save,
|
|
1638
1681
|
transcript: transcriptFn,
|
|
1639
1682
|
getMessages: getMessagesFn,
|