@astrosheep/keiyaku 1.0.0 → 1.0.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 (123) hide show
  1. package/README.md +73 -56
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms_v2.js +103 -0
  4. package/build/agents/harness/activity-values.js +54 -0
  5. package/build/agents/harness/event-channel.js +55 -20
  6. package/build/agents/harness/events.js +47 -1
  7. package/build/agents/harness/execution-handle.js +292 -61
  8. package/build/agents/harness/index.js +103 -121
  9. package/build/agents/harness/pump.js +36 -47
  10. package/build/agents/harness/runtime.js +3 -0
  11. package/build/agents/launch-snapshot_v2.js +348 -0
  12. package/build/agents/opencode-sdk.js +42 -169
  13. package/build/agents/providers/claude-agent-sdk.js +240 -79
  14. package/build/agents/providers/codex-app-server.js +280 -72
  15. package/build/agents/providers/codex-sdk.js +159 -41
  16. package/build/agents/providers/event-type-tail.js +16 -0
  17. package/build/agents/providers/opencode-sdk.js +165 -59
  18. package/build/agents/providers/pi.js +254 -42
  19. package/build/agents/selector_v2.js +122 -0
  20. package/build/cli/commands/akuma.js +120 -42
  21. package/build/cli/completion.js +6 -7
  22. package/build/cli/flags.js +36 -10
  23. package/build/cli/help.js +42 -26
  24. package/build/cli/index.js +225 -93
  25. package/build/cli/parse.js +194 -75
  26. package/build/cli/projection-address.js +11 -0
  27. package/build/cli/render/address.js +88 -0
  28. package/build/cli/render/arc.js +10 -10
  29. package/build/cli/render/call.js +59 -0
  30. package/build/cli/render/compact-text.js +3 -0
  31. package/build/cli/render/errors.js +4 -2
  32. package/build/cli/render/format.js +6 -0
  33. package/build/cli/render/line-width.js +70 -0
  34. package/build/cli/render/misc.js +7 -4
  35. package/build/cli/render/petition.js +4 -3
  36. package/build/cli/render/projection-activity.js +201 -0
  37. package/build/cli/render/shared.js +46 -12
  38. package/build/cli/render/status.js +59 -10
  39. package/build/cli/render/tool-ledger-rollup.js +74 -0
  40. package/build/cli/render/tool-presentation.js +97 -0
  41. package/build/cli/render/wait.js +229 -0
  42. package/build/cli/skills-install.js +42 -24
  43. package/build/cli/subagent-guard.js +1 -1
  44. package/build/config/akuma-loader_v2.js +222 -0
  45. package/build/config/env-keys.js +0 -4
  46. package/build/config/env.js +0 -6
  47. package/build/config/settings.js +325 -12
  48. package/build/core/addressing.js +224 -0
  49. package/build/core/amend.js +30 -47
  50. package/build/core/arc.js +68 -60
  51. package/build/core/bind.js +105 -25
  52. package/build/core/call-persist.js +69 -0
  53. package/build/core/call.js +658 -0
  54. package/build/core/claim.js +72 -44
  55. package/build/core/command-io.js +19 -19
  56. package/build/core/context.js +11 -36
  57. package/build/core/draft.js +2 -3
  58. package/build/core/entry.js +101 -6
  59. package/build/core/execution-coordinate.js +62 -0
  60. package/build/core/execution-pact.js +141 -0
  61. package/build/core/forfeit.js +2 -3
  62. package/build/core/hints.js +10 -10
  63. package/build/core/ids.js +6 -0
  64. package/build/core/ledger.js +16 -1
  65. package/build/core/petition-claim-gates.js +14 -12
  66. package/build/core/petition-claim.js +1 -0
  67. package/build/core/petition-run.js +6 -2
  68. package/build/core/petition.js +63 -21
  69. package/build/core/places.js +106 -13
  70. package/build/core/projection/generation.js +412 -0
  71. package/build/core/projection/heart.js +316 -0
  72. package/build/core/projection/identity.js +81 -0
  73. package/build/core/projection/leash.js +87 -0
  74. package/build/core/projection/mint.js +154 -0
  75. package/build/core/projection-activity.js +219 -0
  76. package/build/core/projection-coordinate.js +35 -0
  77. package/build/core/projection-core.js +69 -463
  78. package/build/core/projection-generation-continuation.js +31 -0
  79. package/build/core/projection-generation-execution.js +148 -0
  80. package/build/core/projection-generation-identity.js +17 -0
  81. package/build/core/projection-generation-launcher.js +90 -0
  82. package/build/core/projection-generation-process.js +43 -0
  83. package/build/core/projection-generation-runner.js +144 -0
  84. package/build/core/projection-generation-runtime.js +15 -0
  85. package/build/core/projection-generation-store.js +707 -0
  86. package/build/core/projection-identity.js +11 -0
  87. package/build/core/projection-life-observer.js +87 -0
  88. package/build/core/projection-life-protocol.js +95 -0
  89. package/build/core/projection-mint.js +96 -24
  90. package/build/core/projection-runner-lock.js +230 -0
  91. package/build/core/projection-status.js +207 -107
  92. package/build/core/projection-tells.js +401 -0
  93. package/build/core/projection-wait.js +155 -0
  94. package/build/core/projection-wake.js +109 -231
  95. package/build/core/queue_v2.js +342 -0
  96. package/build/core/registry.js +6 -3
  97. package/build/core/render.js +63 -1
  98. package/build/core/renew.js +80 -53
  99. package/build/core/scope.js +186 -137
  100. package/build/core/seal.js +16 -19
  101. package/build/core/status.js +88 -25
  102. package/build/core/stored-agent-event.js +101 -0
  103. package/build/core/transcripts.js +214 -228
  104. package/build/core/verdict.js +27 -15
  105. package/build/core/worktree-bootstrap.js +187 -0
  106. package/build/core/worktree-path.js +72 -18
  107. package/build/flow-error.js +4 -6
  108. package/build/generated/version.js +1 -1
  109. package/build/git/branches.js +6 -1
  110. package/build/index.js +8 -5
  111. package/build/keiyaku.js +1 -3
  112. package/package.json +6 -5
  113. package/skills/keiyaku/SKILL.md +6 -47
  114. package/skills/keiyaku-akuma/SKILL.md +75 -0
  115. package/build/agents/effective-policy.js +0 -32
  116. package/build/agents/index.js +0 -114
  117. package/build/agents/selector.js +0 -28
  118. package/build/cli/render/summon.js +0 -17
  119. package/build/cli/render/tell.js +0 -11
  120. package/build/config/akuma-loader.js +0 -268
  121. package/build/core/queue.js +0 -73
  122. package/build/core/summon-persist.js +0 -73
  123. package/build/core/summon.js +0 -377
@@ -0,0 +1,348 @@
1
+ import * as path from "node:path";
2
+ import { FlowError } from "../flow-error.js";
3
+ import { buildCodexAppServerWorkspaceWriteSandboxPolicy, } from "./providers/codex-app-server.js";
4
+ const ACCESS_VALUES = new Set(["read", "write", "auto"]);
5
+ const NETWORK_VALUES = new Set(["disabled", "enabled"]);
6
+ const WEB_SEARCH_VALUES = new Set(["disabled", "cached", "live"]);
7
+ const AKUMA_SOURCES = new Set(["builtin", "user", "project"]);
8
+ const PROVIDER_SOURCES = new Set(["builtin", "global", "project"]);
9
+ const CLAUDE_SETTING_SOURCES = new Set(["project", "user", "local"]);
10
+ function snapshotError(fieldPath, message) {
11
+ return new Error(`Invalid resolved Akuma launch snapshot at ${fieldPath}: ${message}`);
12
+ }
13
+ function isPlainObject(value) {
14
+ if (value === null || typeof value !== "object" || Array.isArray(value))
15
+ return false;
16
+ const prototype = Object.getPrototypeOf(value);
17
+ return prototype === Object.prototype || prototype === null;
18
+ }
19
+ function objectAt(value, fieldPath) {
20
+ if (!isPlainObject(value))
21
+ throw snapshotError(fieldPath, "must be a plain JSON object");
22
+ return value;
23
+ }
24
+ function exactKeys(value, allowed, fieldPath) {
25
+ const allowedSet = new Set(allowed);
26
+ for (const key of Object.keys(value)) {
27
+ if (!allowedSet.has(key))
28
+ throw snapshotError(`${fieldPath}.${key}`, "is an unknown field");
29
+ }
30
+ }
31
+ function requiredString(value, key, fieldPath, allowBlank = false) {
32
+ const item = value[key];
33
+ if (typeof item !== "string" || (!allowBlank && item.trim().length === 0)) {
34
+ throw snapshotError(`${fieldPath}.${key}`, allowBlank ? "must be a string" : "must be a nonblank string");
35
+ }
36
+ return item;
37
+ }
38
+ function optionalString(value, key, fieldPath) {
39
+ if (!(key in value))
40
+ return undefined;
41
+ return requiredString(value, key, fieldPath);
42
+ }
43
+ function requiredEnum(value, key, fieldPath, values) {
44
+ const item = requiredString(value, key, fieldPath);
45
+ if (!values.has(item))
46
+ throw snapshotError(`${fieldPath}.${key}`, `has invalid value '${item}'`);
47
+ return item;
48
+ }
49
+ function cloneJson(value, fieldPath, ancestors = new Set()) {
50
+ if (value === null || typeof value === "string" || typeof value === "boolean")
51
+ return value;
52
+ if (typeof value === "number") {
53
+ if (!Number.isFinite(value))
54
+ throw snapshotError(fieldPath, "contains a non-finite number");
55
+ return value;
56
+ }
57
+ if (typeof value !== "object")
58
+ throw snapshotError(fieldPath, "contains a non-JSON value");
59
+ if (ancestors.has(value))
60
+ throw snapshotError(fieldPath, "contains a cycle");
61
+ ancestors.add(value);
62
+ try {
63
+ if (Array.isArray(value)) {
64
+ return Object.freeze(value.map((item, index) => cloneJson(item, `${fieldPath}[${index}]`, ancestors)));
65
+ }
66
+ if (!isPlainObject(value))
67
+ throw snapshotError(fieldPath, "contains a non-plain JSON object");
68
+ const result = {};
69
+ for (const [key, item] of Object.entries(value)) {
70
+ Object.defineProperty(result, key, {
71
+ value: cloneJson(item, `${fieldPath}.${key}`, ancestors),
72
+ enumerable: true,
73
+ configurable: true,
74
+ writable: true,
75
+ });
76
+ }
77
+ return Object.freeze(result);
78
+ }
79
+ finally {
80
+ ancestors.delete(value);
81
+ }
82
+ }
83
+ function cloneJsonObject(value, fieldPath) {
84
+ const cloned = cloneJson(value, fieldPath);
85
+ if (!isPlainObject(cloned)) {
86
+ throw snapshotError(fieldPath, "must be a plain JSON object");
87
+ }
88
+ return cloned;
89
+ }
90
+ function effortOptionsFrom(akuma) {
91
+ if (!("effortOptions" in akuma))
92
+ return undefined;
93
+ if (!Array.isArray(akuma.effortOptions)) {
94
+ throw snapshotError("akuma.effortOptions", "must be an array");
95
+ }
96
+ const options = akuma.effortOptions.map((value, index) => {
97
+ if (typeof value !== "string" || value.trim().length === 0) {
98
+ throw snapshotError(`akuma.effortOptions[${index}]`, "must be a nonblank string");
99
+ }
100
+ return value;
101
+ });
102
+ if (new Set(options).size !== options.length) {
103
+ throw snapshotError("akuma.effortOptions", "must contain unique strings");
104
+ }
105
+ return Object.freeze(options);
106
+ }
107
+ function assertEffortAllowed(effort, options) {
108
+ if (effort === undefined || options === undefined || options.includes(effort))
109
+ return;
110
+ throw new FlowError("INVALID_EFFORT_OVERRIDE", `Effort '${effort}' is not permitted; legal values: ${options.join(", ")}.`);
111
+ }
112
+ function parsePolicy(value) {
113
+ const policy = objectAt(value, "akuma.policy");
114
+ exactKeys(policy, ["access", "network", "webSearch"], "akuma.policy");
115
+ const result = {};
116
+ if ("access" in policy)
117
+ result.access = requiredEnum(policy, "access", "akuma.policy", ACCESS_VALUES);
118
+ if ("network" in policy)
119
+ result.network = requiredEnum(policy, "network", "akuma.policy", NETWORK_VALUES);
120
+ if ("webSearch" in policy)
121
+ result.webSearch = requiredEnum(policy, "webSearch", "akuma.policy", WEB_SEARCH_VALUES);
122
+ return Object.freeze(result);
123
+ }
124
+ function parseAkuma(value) {
125
+ const akuma = objectAt(value, "akuma");
126
+ exactKeys(akuma, ["name", "source", "body", "description", "model", "effort", "effortOptions", "policy"], "akuma");
127
+ if (!("policy" in akuma))
128
+ throw snapshotError("akuma.policy", "is required");
129
+ const effortOptions = effortOptionsFrom(akuma);
130
+ const effort = optionalString(akuma, "effort", "akuma");
131
+ if (effort !== undefined && effortOptions !== undefined && !effortOptions.includes(effort)) {
132
+ throw snapshotError("akuma.effort", "must be a member of akuma.effortOptions");
133
+ }
134
+ const result = {
135
+ name: requiredString(akuma, "name", "akuma"),
136
+ source: requiredEnum(akuma, "source", "akuma", AKUMA_SOURCES),
137
+ body: requiredString(akuma, "body", "akuma", true),
138
+ ...(optionalString(akuma, "description", "akuma") !== undefined ? { description: optionalString(akuma, "description", "akuma") } : {}),
139
+ ...(optionalString(akuma, "model", "akuma") !== undefined ? { model: optionalString(akuma, "model", "akuma") } : {}),
140
+ ...(effort !== undefined ? { effort } : {}),
141
+ ...(effortOptions !== undefined ? { effortOptions } : {}),
142
+ policy: parsePolicy(akuma.policy),
143
+ };
144
+ return Object.freeze(result);
145
+ }
146
+ function parseProviderCommon(provider) {
147
+ return {
148
+ name: requiredString(provider, "name", "provider"),
149
+ source: requiredEnum(provider, "source", "provider", PROVIDER_SOURCES),
150
+ };
151
+ }
152
+ function optionalJsonObject(provider, key) {
153
+ return key in provider ? cloneJsonObject(provider[key], `provider.${key}`) : undefined;
154
+ }
155
+ function parseSandboxPolicy(value) {
156
+ const sandbox = objectAt(value, "provider.sandboxPolicy");
157
+ exactKeys(sandbox, ["type", "writableRoots", "networkAccess", "excludeTmpdirEnvVar", "excludeSlashTmp"], "provider.sandboxPolicy");
158
+ if (sandbox.type !== "workspaceWrite")
159
+ throw snapshotError("provider.sandboxPolicy.type", "must equal 'workspaceWrite'");
160
+ if (!Array.isArray(sandbox.writableRoots) || sandbox.writableRoots.length === 0) {
161
+ throw snapshotError("provider.sandboxPolicy.writableRoots", "must be a nonempty array");
162
+ }
163
+ const roots = sandbox.writableRoots.map((root, index) => {
164
+ if (typeof root !== "string" || !path.isAbsolute(root) || path.resolve(root) !== root) {
165
+ throw snapshotError(`provider.sandboxPolicy.writableRoots[${index}]`, "must be a lexically normalized absolute path");
166
+ }
167
+ return root;
168
+ });
169
+ if (new Set(roots).size !== roots.length) {
170
+ throw snapshotError("provider.sandboxPolicy.writableRoots", "must not contain duplicates");
171
+ }
172
+ for (const key of ["networkAccess", "excludeTmpdirEnvVar", "excludeSlashTmp"]) {
173
+ if (typeof sandbox[key] !== "boolean")
174
+ throw snapshotError(`provider.sandboxPolicy.${key}`, "must be a boolean");
175
+ }
176
+ Object.freeze(roots);
177
+ return Object.freeze({
178
+ type: "workspaceWrite",
179
+ writableRoots: roots,
180
+ networkAccess: sandbox.networkAccess,
181
+ excludeTmpdirEnvVar: sandbox.excludeTmpdirEnvVar,
182
+ excludeSlashTmp: sandbox.excludeSlashTmp,
183
+ });
184
+ }
185
+ function parseProvider(value) {
186
+ const provider = objectAt(value, "provider");
187
+ const kind = requiredString(provider, "kind", "provider");
188
+ const common = parseProviderCommon(provider);
189
+ switch (kind) {
190
+ case "codex-sdk": {
191
+ exactKeys(provider, ["name", "source", "kind", "executable", "profile", "config", "threadOptions"], "provider");
192
+ if (!("threadOptions" in provider))
193
+ throw snapshotError("provider.threadOptions", "is required");
194
+ const executable = optionalString(provider, "executable", "provider");
195
+ const profile = optionalString(provider, "profile", "provider");
196
+ const config = optionalJsonObject(provider, "config");
197
+ return Object.freeze({ ...common, kind, ...(executable !== undefined ? { executable } : {}), ...(profile !== undefined ? { profile } : {}), ...(config !== undefined ? { config } : {}), threadOptions: cloneJsonObject(provider.threadOptions, "provider.threadOptions") });
198
+ }
199
+ case "codex-app-server": {
200
+ exactKeys(provider, ["name", "source", "kind", "executable", "profile", "config", "model", "effort", "sandboxPolicy", "approvalPolicy"], "provider");
201
+ if (!("sandboxPolicy" in provider))
202
+ throw snapshotError("provider.sandboxPolicy", "is required");
203
+ if (provider.approvalPolicy !== "never")
204
+ throw snapshotError("provider.approvalPolicy", "must equal 'never'");
205
+ const executable = optionalString(provider, "executable", "provider");
206
+ const profile = optionalString(provider, "profile", "provider");
207
+ const config = optionalJsonObject(provider, "config");
208
+ const model = optionalString(provider, "model", "provider");
209
+ const effort = optionalString(provider, "effort", "provider");
210
+ return Object.freeze({ ...common, kind, ...(executable !== undefined ? { executable } : {}), ...(profile !== undefined ? { profile } : {}), ...(config !== undefined ? { config } : {}), ...(model !== undefined ? { model } : {}), ...(effort !== undefined ? { effort } : {}), sandboxPolicy: parseSandboxPolicy(provider.sandboxPolicy), approvalPolicy: "never" });
211
+ }
212
+ case "claude-agent-sdk": {
213
+ exactKeys(provider, ["name", "source", "kind", "executable", "model", "effort", "settingSources", "permissionMode", "allowDangerouslySkipPermissions", "disallowedTools"], "provider");
214
+ const modes = new Set(["default", "plan", "acceptEdits", "bypassPermissions"]);
215
+ const permissionMode = requiredEnum(provider, "permissionMode", "provider", modes);
216
+ if (permissionMode === "bypassPermissions") {
217
+ if (provider.allowDangerouslySkipPermissions !== true)
218
+ throw snapshotError("provider.allowDangerouslySkipPermissions", "must be true for bypassPermissions");
219
+ }
220
+ else if ("allowDangerouslySkipPermissions" in provider) {
221
+ throw snapshotError("provider.allowDangerouslySkipPermissions", "is only valid for bypassPermissions");
222
+ }
223
+ let settingSources;
224
+ if ("settingSources" in provider) {
225
+ if (!Array.isArray(provider.settingSources))
226
+ throw snapshotError("provider.settingSources", "must be an array");
227
+ settingSources = Object.freeze(provider.settingSources.map((source, index) => {
228
+ if (typeof source !== "string" || !CLAUDE_SETTING_SOURCES.has(source))
229
+ throw snapshotError(`provider.settingSources[${index}]`, "has an invalid setting source");
230
+ return source;
231
+ }));
232
+ }
233
+ let disallowedTools;
234
+ if ("disallowedTools" in provider) {
235
+ if (!Array.isArray(provider.disallowedTools) || provider.disallowedTools.length !== 1 || provider.disallowedTools[0] !== "WebSearch") {
236
+ throw snapshotError("provider.disallowedTools", "must equal ['WebSearch']");
237
+ }
238
+ disallowedTools = Object.freeze(["WebSearch"]);
239
+ }
240
+ const executable = optionalString(provider, "executable", "provider");
241
+ const model = optionalString(provider, "model", "provider");
242
+ const effort = optionalString(provider, "effort", "provider");
243
+ return Object.freeze({ ...common, kind, ...(executable !== undefined ? { executable } : {}), ...(model !== undefined ? { model } : {}), ...(effort !== undefined ? { effort } : {}), ...(settingSources !== undefined ? { settingSources } : {}), permissionMode, ...(permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}), ...(disallowedTools !== undefined ? { disallowedTools } : {}) });
244
+ }
245
+ case "opencode-sdk": {
246
+ exactKeys(provider, ["name", "source", "kind", "executable", "model", "effort"], "provider");
247
+ const executable = optionalString(provider, "executable", "provider");
248
+ const model = optionalString(provider, "model", "provider");
249
+ const effort = optionalString(provider, "effort", "provider");
250
+ return Object.freeze({ ...common, kind, ...(executable !== undefined ? { executable } : {}), ...(model !== undefined ? { model } : {}), ...(effort !== undefined ? { effort } : {}) });
251
+ }
252
+ case "pi": {
253
+ exactKeys(provider, ["name", "source", "kind", "model", "thinkingLevel"], "provider");
254
+ const model = optionalString(provider, "model", "provider");
255
+ const thinkingLevel = optionalString(provider, "thinkingLevel", "provider");
256
+ return Object.freeze({ ...common, kind, ...(model !== undefined ? { model } : {}), ...(thinkingLevel !== undefined ? { thinkingLevel } : {}) });
257
+ }
258
+ default:
259
+ throw snapshotError("provider.kind", `has invalid value '${kind}'`);
260
+ }
261
+ }
262
+ export function parseResolvedAkumaLaunchSnapshot(value) {
263
+ const snapshot = objectAt(value, "resolved Akuma launch snapshot");
264
+ exactKeys(snapshot, ["version", "akuma", "provider"], "resolved Akuma launch snapshot");
265
+ if (snapshot.version !== 1)
266
+ throw snapshotError("version", "must equal 1");
267
+ if (!("akuma" in snapshot))
268
+ throw snapshotError("akuma", "is required");
269
+ if (!("provider" in snapshot))
270
+ throw snapshotError("provider", "is required");
271
+ return Object.freeze({ version: 1, akuma: parseAkuma(snapshot.akuma), provider: parseProvider(snapshot.provider) });
272
+ }
273
+ export function resolveAkumaLaunchSnapshot(selected, input) {
274
+ const model = input.model ?? selected.profile.model;
275
+ const effort = input.effort ?? selected.profile.effort;
276
+ assertEffortAllowed(effort, selected.profile.effortOptions);
277
+ const policy = {
278
+ ...(selected.profile.access !== undefined ? { access: selected.profile.access } : {}),
279
+ ...(selected.profile.network !== undefined ? { network: selected.profile.network } : {}),
280
+ ...(selected.profile.webSearch !== undefined ? { webSearch: selected.profile.webSearch } : {}),
281
+ };
282
+ const akuma = {
283
+ name: selected.name,
284
+ source: selected.definition.source,
285
+ body: selected.definition.body,
286
+ ...(selected.profile.description !== undefined ? { description: selected.profile.description } : {}),
287
+ ...(model !== undefined ? { model } : {}),
288
+ ...(effort !== undefined ? { effort } : {}),
289
+ ...(selected.profile.effortOptions !== undefined ? { effortOptions: selected.profile.effortOptions } : {}),
290
+ policy,
291
+ };
292
+ const instance = selected.profile.providerInstance;
293
+ const common = { name: selected.profile.provider, source: selected.profile.providerSource, kind: instance.kind };
294
+ let provider;
295
+ switch (instance.kind) {
296
+ case "codex-sdk": {
297
+ const threadOptions = { ...(instance.threadOptions ?? {}) };
298
+ threadOptions.sandboxMode = selected.profile.access === "read" ? "read-only" : selected.profile.access === "auto" ? "danger-full-access" : "workspace-write";
299
+ threadOptions.networkAccessEnabled = selected.profile.network === "enabled";
300
+ threadOptions.webSearchMode = "disabled";
301
+ if (model !== undefined)
302
+ threadOptions.model = model;
303
+ if (effort !== undefined)
304
+ threadOptions.modelReasoningEffort = effort;
305
+ threadOptions.workingDirectory = input.executionCwd;
306
+ threadOptions.skipGitRepoCheck = true;
307
+ provider = { ...common, ...(instance.executable !== undefined ? { executable: instance.executable } : {}), ...(instance.profile !== undefined ? { profile: instance.profile } : {}), ...(instance.config !== undefined ? { config: instance.config } : {}), threadOptions };
308
+ break;
309
+ }
310
+ case "codex-app-server": {
311
+ const roots = [path.resolve(input.executionCwd), ...(instance.writableRoots ?? []).map((root) => path.resolve(input.projectRoot, root))];
312
+ const writableRoots = [...new Set(roots)];
313
+ provider = { ...common, ...(instance.executable !== undefined ? { executable: instance.executable } : {}), ...(instance.profile !== undefined ? { profile: instance.profile } : {}), ...(instance.config !== undefined ? { config: instance.config } : {}), ...(model !== undefined ? { model } : {}), ...(effort !== undefined ? { effort } : {}), sandboxPolicy: buildCodexAppServerWorkspaceWriteSandboxPolicy({ writableRoots, networkAccess: selected.profile.network === "enabled" }), approvalPolicy: "never" };
314
+ break;
315
+ }
316
+ case "claude-agent-sdk": {
317
+ const permissionMode = selected.profile.access === "read" ? "plan" : selected.profile.access === "write" ? "acceptEdits" : selected.profile.access === "auto" ? "bypassPermissions" : "default";
318
+ provider = { ...common, ...(instance.executable !== undefined ? { executable: instance.executable } : {}), ...(model !== undefined ? { model } : {}), ...(effort !== undefined ? { effort } : {}), ...(instance.settingSources !== undefined ? { settingSources: instance.settingSources } : {}), permissionMode, ...(permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}), ...(selected.profile.webSearch === "disabled" ? { disallowedTools: ["WebSearch"] } : {}) };
319
+ break;
320
+ }
321
+ case "opencode-sdk":
322
+ provider = { ...common, ...(instance.executable !== undefined ? { executable: instance.executable } : {}), ...(model !== undefined ? { model } : {}), ...(effort !== undefined ? { effort } : {}) };
323
+ break;
324
+ case "pi":
325
+ provider = { ...common, ...(model !== undefined ? { model } : {}), ...(effort !== undefined ? { thinkingLevel: effort } : {}) };
326
+ break;
327
+ }
328
+ return parseResolvedAkumaLaunchSnapshot({ version: 1, akuma, provider });
329
+ }
330
+ export function applySnapshotEffort(snapshot, effort) {
331
+ assertEffortAllowed(effort, snapshot.akuma.effortOptions);
332
+ const akuma = { ...snapshot.akuma, effort };
333
+ let provider;
334
+ switch (snapshot.provider.kind) {
335
+ case "codex-sdk":
336
+ provider = { ...snapshot.provider, threadOptions: { ...snapshot.provider.threadOptions, modelReasoningEffort: effort } };
337
+ break;
338
+ case "codex-app-server":
339
+ case "claude-agent-sdk":
340
+ case "opencode-sdk":
341
+ provider = { ...snapshot.provider, effort };
342
+ break;
343
+ case "pi":
344
+ provider = { ...snapshot.provider, thinkingLevel: effort };
345
+ break;
346
+ }
347
+ return parseResolvedAkumaLaunchSnapshot({ version: 1, akuma, provider });
348
+ }
@@ -1,142 +1,18 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import net from "node:net";
3
- import { coerceString } from "./index.js";
4
- import { createAbortError } from "./harness/runtime.js";
3
+ import { createOpencodeClient, } from "@opencode-ai/sdk/v2";
4
+ import { coerceString, createAbortError } from "./harness/runtime.js";
5
+ export { createOpencodeClient, };
5
6
  const DEFAULT_HOSTNAME = "127.0.0.1";
6
- const DEFAULT_PORT = 4096;
7
7
  const DEFAULT_STARTUP_TIMEOUT_MS = 10_000;
8
8
  const SERVER_READY_POLL_INTERVAL_MS = 100;
9
9
  const SERVER_SHUTDOWN_FORCE_KILL_MS = 1000;
10
10
  const OPENCODE_SERVE_SUBCOMMAND = "serve";
11
11
  const OPENCODE_HOSTNAME_FLAG = "--hostname";
12
12
  const OPENCODE_PORT_FLAG = "--port";
13
- const RESPONSE_HEADER_ACCEPT = "accept";
14
- const RESPONSE_HEADER_CONTENT_TYPE = "content-type";
15
- const JSON_CONTENT_TYPE = "application/json";
16
- const EVENT_STREAM_CONTENT_TYPE = "text/event-stream";
17
- const SESSION_PATH = "/session";
18
- const EVENT_PATH = "/event";
19
- const CONFIG_PATH = "/config";
20
- const SESSION_MESSAGE_PATH_SUFFIX = "/message";
21
- const EVENT_DELIMITER = "\n\n";
22
- const EVENT_DATA_PREFIX = "data:";
23
13
  function sleep(ms) {
24
14
  return new Promise((resolve) => setTimeout(resolve, ms));
25
15
  }
26
- function normalizeBaseUrl(baseUrl) {
27
- return coerceString(baseUrl)?.replace(/\/+$/, "") ?? `http://${DEFAULT_HOSTNAME}:${DEFAULT_PORT}`;
28
- }
29
- function buildSessionPath(sessionId) {
30
- return `${SESSION_PATH}/${encodeURIComponent(sessionId)}`;
31
- }
32
- async function createJsonRequest(fetcher, baseUrl, requestPath, init = {}) {
33
- const response = await fetcher(`${baseUrl}${requestPath}`, {
34
- ...init,
35
- headers: {
36
- [RESPONSE_HEADER_ACCEPT]: JSON_CONTENT_TYPE,
37
- [RESPONSE_HEADER_CONTENT_TYPE]: JSON_CONTENT_TYPE,
38
- ...(init.headers ?? {}),
39
- },
40
- });
41
- if (!response.ok) {
42
- const body = await response.text();
43
- throw new Error(body.trim() || `OpenCode request failed: ${response.status}`);
44
- }
45
- if (response.status === 204) {
46
- return undefined;
47
- }
48
- return response.json();
49
- }
50
- function createSseStream(response) {
51
- if (!response.body) {
52
- throw new Error("OpenCode event stream missing response body");
53
- }
54
- const reader = response.body.getReader();
55
- const decoder = new TextDecoder();
56
- return {
57
- async *[Symbol.asyncIterator]() {
58
- let buffer = "";
59
- while (true) {
60
- const { done, value } = await reader.read();
61
- buffer += decoder.decode(value ?? new Uint8Array(), { stream: !done });
62
- let delimiterIndex = buffer.indexOf(EVENT_DELIMITER);
63
- while (delimiterIndex !== -1) {
64
- const rawEvent = buffer.slice(0, delimiterIndex);
65
- buffer = buffer.slice(delimiterIndex + EVENT_DELIMITER.length);
66
- const data = rawEvent
67
- .split("\n")
68
- .filter((line) => line.startsWith(EVENT_DATA_PREFIX))
69
- .map((line) => line.slice(EVENT_DATA_PREFIX.length).trim())
70
- .join("\n");
71
- if (data) {
72
- yield JSON.parse(data);
73
- }
74
- delimiterIndex = buffer.indexOf(EVENT_DELIMITER);
75
- }
76
- if (done) {
77
- const data = buffer
78
- .split("\n")
79
- .filter((line) => line.startsWith(EVENT_DATA_PREFIX))
80
- .map((line) => line.slice(EVENT_DATA_PREFIX.length).trim())
81
- .join("\n");
82
- if (data) {
83
- yield JSON.parse(data);
84
- }
85
- return;
86
- }
87
- }
88
- },
89
- };
90
- }
91
- export function createOpencodeClient(options = {}) {
92
- const fetcher = options.fetch ?? globalThis.fetch;
93
- if (typeof fetcher !== "function") {
94
- throw new Error("OpenCode SDK requires global fetch support");
95
- }
96
- const baseUrl = normalizeBaseUrl(options.baseUrl);
97
- return {
98
- config: {
99
- async get() {
100
- return createJsonRequest(fetcher, baseUrl, CONFIG_PATH);
101
- },
102
- },
103
- session: {
104
- async create(input = {}) {
105
- return createJsonRequest(fetcher, baseUrl, SESSION_PATH, {
106
- method: "POST",
107
- body: JSON.stringify(input.body ?? {}),
108
- });
109
- },
110
- async get(input) {
111
- return createJsonRequest(fetcher, baseUrl, buildSessionPath(input.path.id));
112
- },
113
- async prompt(input) {
114
- return createJsonRequest(fetcher, baseUrl, `${buildSessionPath(input.path.id)}${SESSION_MESSAGE_PATH_SUFFIX}`, {
115
- method: "POST",
116
- body: JSON.stringify(input.body),
117
- });
118
- },
119
- async messages(input) {
120
- return createJsonRequest(fetcher, baseUrl, `${buildSessionPath(input.path.id)}${SESSION_MESSAGE_PATH_SUFFIX}`);
121
- },
122
- },
123
- event: {
124
- async subscribe() {
125
- const response = await fetcher(`${baseUrl}${EVENT_PATH}`, {
126
- method: "GET",
127
- headers: {
128
- [RESPONSE_HEADER_ACCEPT]: EVENT_STREAM_CONTENT_TYPE,
129
- },
130
- });
131
- if (!response.ok) {
132
- const body = await response.text();
133
- throw new Error(body.trim() || `OpenCode event subscribe failed: ${response.status}`);
134
- }
135
- return { stream: createSseStream(response) };
136
- },
137
- },
138
- };
139
- }
140
16
  function getAvailablePort(hostname) {
141
17
  return new Promise((resolve, reject) => {
142
18
  const server = net.createServer();
@@ -147,24 +23,17 @@ function getAvailablePort(hostname) {
147
23
  server.close(() => reject(new Error("Unable to determine OpenCode server port")));
148
24
  return;
149
25
  }
150
- server.close((error) => {
151
- if (error) {
152
- reject(error);
153
- return;
154
- }
155
- resolve(address.port);
156
- });
26
+ server.close((error) => error ? reject(error) : resolve(address.port));
157
27
  });
158
28
  });
159
29
  }
160
- async function waitForServerReady(client, timeoutMs, signal) {
30
+ async function waitForServerReady(client, directory, timeoutMs, signal) {
161
31
  const deadline = Date.now() + timeoutMs;
162
32
  while (Date.now() < deadline) {
163
- if (signal?.aborted) {
33
+ if (signal?.aborted)
164
34
  throw createAbortError("OpenCode server startup aborted");
165
- }
166
35
  try {
167
- await client.config.get();
36
+ await client.config.get({ directory }, { throwOnError: true });
168
37
  return;
169
38
  }
170
39
  catch {
@@ -178,55 +47,59 @@ export async function createOpencodeServer(options = {}) {
178
47
  const port = options.port ?? (await getAvailablePort(hostname));
179
48
  const url = `http://${hostname}:${port}`;
180
49
  const executable = coerceString(options.executable) ?? "opencode";
50
+ const directory = coerceString(options.directory);
181
51
  const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
182
52
  const child = spawn(executable, [OPENCODE_SERVE_SUBCOMMAND, OPENCODE_HOSTNAME_FLAG, hostname, OPENCODE_PORT_FLAG, String(port)], {
183
- cwd: coerceString(options.directory),
184
- env: {
185
- ...process.env,
186
- ...(options.env ?? {}),
187
- },
53
+ cwd: directory,
54
+ env: { ...process.env, ...(options.env ?? {}) },
188
55
  stdio: ["ignore", "pipe", "pipe"],
189
56
  });
190
57
  let exited = false;
191
58
  let stderr = "";
192
- child.stderr.on("data", (chunk) => {
193
- stderr += String(chunk);
194
- });
195
- child.on("close", () => {
196
- exited = true;
197
- });
198
- const client = createOpencodeClient({ baseUrl: url });
59
+ child.stderr.on("data", (chunk) => { stderr += String(chunk); });
60
+ child.on("error", (error) => { stderr += `${stderr ? " " : ""}${error.message}`; });
61
+ child.on("close", () => { exited = true; });
62
+ const client = createOpencodeClient({ baseUrl: url, directory });
63
+ const stop = async () => {
64
+ if (exited)
65
+ return;
66
+ await new Promise((resolve) => {
67
+ let settled = false;
68
+ let forceKill;
69
+ const finish = () => {
70
+ if (settled)
71
+ return;
72
+ settled = true;
73
+ if (forceKill)
74
+ clearTimeout(forceKill);
75
+ resolve();
76
+ };
77
+ child.once("close", finish);
78
+ child.kill("SIGTERM");
79
+ forceKill = setTimeout(() => {
80
+ if (!exited)
81
+ child.kill("SIGKILL");
82
+ }, SERVER_SHUTDOWN_FORCE_KILL_MS);
83
+ forceKill.unref();
84
+ });
85
+ };
199
86
  try {
200
- await waitForServerReady(client, startupTimeoutMs, options.signal);
87
+ await waitForServerReady(client, directory, startupTimeoutMs, options.signal);
201
88
  }
202
89
  catch (error) {
203
- if (!exited) {
204
- child.kill("SIGTERM");
205
- }
206
- const suffix = stderr.trim().length > 0 ? ` ${stderr.trim()}` : "";
90
+ await stop();
91
+ const suffix = stderr.trim() ? ` ${stderr.trim()}` : "";
207
92
  throw new Error(`${error instanceof Error ? error.message : String(error)}${suffix}`);
208
93
  }
209
94
  return {
210
95
  url,
211
- async close() {
212
- if (exited)
213
- return;
214
- await new Promise((resolve) => {
215
- child.once("close", () => resolve());
216
- child.kill("SIGTERM");
217
- setTimeout(() => {
218
- if (!exited) {
219
- child.kill("SIGKILL");
220
- }
221
- }, SERVER_SHUTDOWN_FORCE_KILL_MS);
222
- });
223
- },
96
+ close: stop,
224
97
  };
225
98
  }
226
99
  export async function createOpencode(options = {}) {
227
100
  const server = await createOpencodeServer(options);
228
101
  return {
229
- client: createOpencodeClient({ baseUrl: server.url }),
102
+ client: createOpencodeClient({ baseUrl: server.url, directory: options.directory }),
230
103
  server,
231
104
  };
232
105
  }