@meego-harness/opencode-worker 0.1.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/README.md +51 -0
- package/bin/meego-opencode-worker.mjs +9 -0
- package/dist/chunk-4MUQ7X6C.js +1833 -0
- package/dist/chunk-4MUQ7X6C.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +342 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +205 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/package.json +127 -0
|
@@ -0,0 +1,1833 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var OPENCODE_WORKER_ROLE = "coder";
|
|
3
|
+
var OPENCODE_WORKER_WORKING_TEXT = "OpenCode CLI is working";
|
|
4
|
+
var OPENCODE_WORKER_CONFIG_DIR = ".meego-harness/opencode-worker";
|
|
5
|
+
|
|
6
|
+
// src/config.ts
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { dirname, isAbsolute, join } from "path";
|
|
10
|
+
import { confirm, isCancel, select, text } from "@clack/prompts";
|
|
11
|
+
import { renderCliPrompt } from "@meego-harness/prompt-registry";
|
|
12
|
+
import { resolveDefaultWorkerDeviceIdentityFile } from "@meego-harness/worker-sdk";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
var openCodeModelSelectionSchema = z.string().trim().min(1, "modelSelection is required");
|
|
15
|
+
var openCodeVariantSelectionSchema = z.string().trim().min(1, "variantDefault is required");
|
|
16
|
+
var openCodeAgentSelectionSchema = z.string().trim().min(1, "agentDefault is required");
|
|
17
|
+
var openCodeWorkerConfigSchema = z.object({
|
|
18
|
+
serverUrl: z.string().trim().min(1, "serverUrl is required"),
|
|
19
|
+
email: z.string().trim().min(1, "email is required"),
|
|
20
|
+
workerId: z.string().trim().min(1, "workerId is required"),
|
|
21
|
+
capabilitySummary: z.string().trim().min(1, "capabilitySummary is required"),
|
|
22
|
+
defaultWorkspace: z.string().trim().min(1, "defaultWorkspace is required"),
|
|
23
|
+
artifactStagingRoot: z.string().trim().min(1, "artifactStagingRoot is required").optional(),
|
|
24
|
+
repoMappings: z.record(z.string(), z.string()),
|
|
25
|
+
enabled: z.boolean().default(true),
|
|
26
|
+
permissionPreset: z.enum(["safe", "default", "full-access"]),
|
|
27
|
+
modelSelection: openCodeModelSelectionSchema,
|
|
28
|
+
variantDefault: openCodeVariantSelectionSchema,
|
|
29
|
+
agentDefault: openCodeAgentSelectionSchema,
|
|
30
|
+
openCodeShell: z.boolean().default(false)
|
|
31
|
+
});
|
|
32
|
+
var openCodeWorkerStateSchema = z.object({
|
|
33
|
+
contexts: z.array(
|
|
34
|
+
z.object({
|
|
35
|
+
contextId: z.string(),
|
|
36
|
+
repo: z.string().optional(),
|
|
37
|
+
cwd: z.string(),
|
|
38
|
+
sessionId: z.string().optional(),
|
|
39
|
+
variant: z.string().optional(),
|
|
40
|
+
agent: z.string().optional(),
|
|
41
|
+
planMode: z.boolean()
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
});
|
|
45
|
+
function resolveOpenCodeWorkerStorageDir(options = {}) {
|
|
46
|
+
return join(options.homeDir ?? homedir(), OPENCODE_WORKER_CONFIG_DIR);
|
|
47
|
+
}
|
|
48
|
+
function resolveOpenCodeWorkerConfigFile(workerId, options = {}) {
|
|
49
|
+
return join(resolveOpenCodeWorkerStorageDir(options), `${workerId}.json`);
|
|
50
|
+
}
|
|
51
|
+
function resolveOpenCodeWorkerStateFile(workerId, options = {}) {
|
|
52
|
+
return join(resolveOpenCodeWorkerStorageDir(options), `${workerId}.state.json`);
|
|
53
|
+
}
|
|
54
|
+
function loadOpenCodeWorkerConfig(workerId, options = {}) {
|
|
55
|
+
const file = resolveOpenCodeWorkerConfigFile(workerId, options);
|
|
56
|
+
if (!existsSync(file)) {
|
|
57
|
+
throw new Error(`OpenCode worker config not found for ${workerId}: ${file}`);
|
|
58
|
+
}
|
|
59
|
+
return openCodeWorkerConfigSchema.parse(JSON.parse(readFileSync(file, "utf8")));
|
|
60
|
+
}
|
|
61
|
+
function writeOpenCodeWorkerConfig(config, options = {}) {
|
|
62
|
+
mkdirSync(resolveOpenCodeWorkerStorageDir(options), { recursive: true });
|
|
63
|
+
writeFileSync(
|
|
64
|
+
resolveOpenCodeWorkerConfigFile(config.workerId, options),
|
|
65
|
+
`${JSON.stringify(config, null, 2)}
|
|
66
|
+
`,
|
|
67
|
+
"utf8"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
function loadOpenCodeWorkerState(stateFile) {
|
|
71
|
+
if (!existsSync(stateFile)) {
|
|
72
|
+
return { contexts: [] };
|
|
73
|
+
}
|
|
74
|
+
return openCodeWorkerStateSchema.parse(JSON.parse(readFileSync(stateFile, "utf8")));
|
|
75
|
+
}
|
|
76
|
+
function writeOpenCodeWorkerState(stateFile, state) {
|
|
77
|
+
mkdirSync(dirname(stateFile), { recursive: true });
|
|
78
|
+
writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}
|
|
79
|
+
`, "utf8");
|
|
80
|
+
}
|
|
81
|
+
async function runOpenCodeWorkerSetup(dependencies, prompter, input) {
|
|
82
|
+
const homeDir = dependencies.homeDir;
|
|
83
|
+
const serverUrlPrompt = renderCliPrompt("cli.opencode-worker.server-url", void 0);
|
|
84
|
+
const serverUrl = await resolveTextInput(
|
|
85
|
+
input?.serverUrl,
|
|
86
|
+
prompter,
|
|
87
|
+
{
|
|
88
|
+
message: serverUrlPrompt.message,
|
|
89
|
+
placeholder: serverUrlPrompt.placeholder,
|
|
90
|
+
validate: requiredString("serverUrl")
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
const emailPrompt = renderCliPrompt("cli.opencode-worker.email", void 0);
|
|
94
|
+
const email = await resolveTextInput(
|
|
95
|
+
input?.email,
|
|
96
|
+
prompter,
|
|
97
|
+
{
|
|
98
|
+
message: emailPrompt.message,
|
|
99
|
+
placeholder: emailPrompt.placeholder,
|
|
100
|
+
validate: requiredString("email")
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
const workerIdPrompt = renderCliPrompt("cli.opencode-worker.worker-id", void 0);
|
|
104
|
+
const workerId = await resolveTextInput(
|
|
105
|
+
input?.workerId,
|
|
106
|
+
prompter,
|
|
107
|
+
{
|
|
108
|
+
message: workerIdPrompt.message,
|
|
109
|
+
placeholder: workerIdPrompt.placeholder,
|
|
110
|
+
validate: requiredString("workerId")
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
const capabilitySummaryPrompt = renderCliPrompt("cli.opencode-worker.capability-summary", void 0);
|
|
114
|
+
const capabilitySummary = await resolveTextInput(
|
|
115
|
+
input?.capabilitySummary,
|
|
116
|
+
prompter,
|
|
117
|
+
{
|
|
118
|
+
message: capabilitySummaryPrompt.message,
|
|
119
|
+
placeholder: capabilitySummaryPrompt.placeholder,
|
|
120
|
+
validate: requiredString("capabilitySummary")
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
const defaultWorkspacePrompt = renderCliPrompt("cli.opencode-worker.default-workspace", void 0);
|
|
124
|
+
const defaultWorkspace = await resolveTextInput(
|
|
125
|
+
input?.defaultWorkspace,
|
|
126
|
+
prompter,
|
|
127
|
+
{
|
|
128
|
+
message: defaultWorkspacePrompt.message,
|
|
129
|
+
placeholder: defaultWorkspacePrompt.placeholder,
|
|
130
|
+
validate: validateDirectory("defaultWorkspace")
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
const permissionPresetPrompt = renderCliPrompt("cli.opencode-worker.permission-preset", void 0);
|
|
134
|
+
const permissionPreset = await resolveSelectInput(
|
|
135
|
+
input?.permissionPreset,
|
|
136
|
+
prompter,
|
|
137
|
+
{
|
|
138
|
+
message: permissionPresetPrompt.message,
|
|
139
|
+
options: [
|
|
140
|
+
{ value: "safe", label: "safe" },
|
|
141
|
+
{ value: "default", label: "default" },
|
|
142
|
+
{ value: "full-access", label: "full-access" }
|
|
143
|
+
],
|
|
144
|
+
initialValue: "default"
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
const modelSelectionPrompt = renderCliPrompt("cli.opencode-worker.model-selection", void 0);
|
|
148
|
+
const modelSelection = await resolveTextInput(
|
|
149
|
+
input?.modelSelection,
|
|
150
|
+
prompter,
|
|
151
|
+
{
|
|
152
|
+
message: modelSelectionPrompt.message,
|
|
153
|
+
placeholder: modelSelectionPrompt.placeholder,
|
|
154
|
+
initialValue: "use-opencode-default",
|
|
155
|
+
validate: requiredString("modelSelection")
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
const variantPrompt = renderCliPrompt("cli.opencode-worker.variant", void 0);
|
|
159
|
+
const variantDefault = await resolveTextInput(
|
|
160
|
+
input?.variantDefault,
|
|
161
|
+
prompter,
|
|
162
|
+
{
|
|
163
|
+
message: variantPrompt.message,
|
|
164
|
+
placeholder: variantPrompt.placeholder,
|
|
165
|
+
initialValue: "use-opencode-default",
|
|
166
|
+
validate: requiredString("variantDefault")
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
const agentPrompt = renderCliPrompt("cli.opencode-worker.agent", void 0);
|
|
170
|
+
const agentDefault = await resolveTextInput(
|
|
171
|
+
input?.agentDefault,
|
|
172
|
+
prompter,
|
|
173
|
+
{
|
|
174
|
+
message: agentPrompt.message,
|
|
175
|
+
placeholder: agentPrompt.placeholder,
|
|
176
|
+
initialValue: "use-opencode-default",
|
|
177
|
+
validate: requiredString("agentDefault")
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
const repoMappings = input?.repoMappings ? { ...input.repoMappings } : await promptRepoMappings(prompter, defaultWorkspace);
|
|
181
|
+
const hasArtifactStagingRoot = input?.artifactStagingRoot !== void 0;
|
|
182
|
+
const artifactStagingRoot = input?.artifactStagingRoot?.trim();
|
|
183
|
+
const result = openCodeWorkerConfigSchema.parse({
|
|
184
|
+
serverUrl: serverUrl.trim(),
|
|
185
|
+
email: email.trim(),
|
|
186
|
+
workerId: workerId.trim(),
|
|
187
|
+
capabilitySummary: capabilitySummary.trim(),
|
|
188
|
+
defaultWorkspace: defaultWorkspace.trim(),
|
|
189
|
+
...hasArtifactStagingRoot ? { artifactStagingRoot } : {},
|
|
190
|
+
repoMappings,
|
|
191
|
+
enabled: input?.enabled ?? true,
|
|
192
|
+
permissionPreset,
|
|
193
|
+
modelSelection,
|
|
194
|
+
variantDefault,
|
|
195
|
+
agentDefault,
|
|
196
|
+
openCodeShell: input?.openCodeShell ?? false
|
|
197
|
+
});
|
|
198
|
+
writeOpenCodeWorkerConfig(result, { homeDir });
|
|
199
|
+
dependencies.logger.info(
|
|
200
|
+
`Configured OpenCode worker ${result.workerId} at ${resolveOpenCodeWorkerConfigFile(result.workerId, { homeDir })}`
|
|
201
|
+
);
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
function listOpenCodeWorkerConfigs(options = {}) {
|
|
205
|
+
const storageDir = resolveOpenCodeWorkerStorageDir(options);
|
|
206
|
+
if (!existsSync(storageDir)) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
return readdirSync(storageDir).filter((file) => file.endsWith(".json") && !file.endsWith(".state.json")).map((file) => {
|
|
210
|
+
const workerId = file.replace(/\.json$/, "");
|
|
211
|
+
const config = loadOpenCodeWorkerConfig(workerId, options);
|
|
212
|
+
return {
|
|
213
|
+
...config,
|
|
214
|
+
configFile: resolveOpenCodeWorkerConfigFile(workerId, options),
|
|
215
|
+
stateFile: resolveOpenCodeWorkerStateFile(workerId, options)
|
|
216
|
+
};
|
|
217
|
+
}).sort((left, right) => left.workerId.localeCompare(right.workerId));
|
|
218
|
+
}
|
|
219
|
+
function getOpenCodeWorkerDoctorReport(options = {}, dependencies = {}) {
|
|
220
|
+
return {
|
|
221
|
+
workers: listOpenCodeWorkerConfigs(options).map((config) => {
|
|
222
|
+
const runtimeErrors = [];
|
|
223
|
+
try {
|
|
224
|
+
dependencies.assertOpenCodeCliRuntimeContract?.("opencode", {
|
|
225
|
+
useShell: config.openCodeShell === true
|
|
226
|
+
});
|
|
227
|
+
} catch (error) {
|
|
228
|
+
runtimeErrors.push(error instanceof Error ? error.message : "OpenCode CLI is unavailable");
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
...config,
|
|
232
|
+
configStatus: "ok",
|
|
233
|
+
runtimeStatus: runtimeErrors.length > 0 ? "error" : "ok",
|
|
234
|
+
errors: runtimeErrors
|
|
235
|
+
};
|
|
236
|
+
})
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function setOpenCodeWorkerEnabled(workerId, enabled, options = {}) {
|
|
240
|
+
const config = loadOpenCodeWorkerConfig(workerId, options);
|
|
241
|
+
writeOpenCodeWorkerConfig(
|
|
242
|
+
{
|
|
243
|
+
...config,
|
|
244
|
+
enabled
|
|
245
|
+
},
|
|
246
|
+
options
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
function uninstallOpenCodeWorker(workerId, options = {}) {
|
|
250
|
+
const configFile = resolveOpenCodeWorkerConfigFile(workerId, options);
|
|
251
|
+
const stateFile = resolveOpenCodeWorkerStateFile(workerId, options);
|
|
252
|
+
const config = existsSync(configFile) ? loadOpenCodeWorkerConfig(workerId, options) : void 0;
|
|
253
|
+
const credentialFile = config ? resolveDefaultWorkerDeviceIdentityFile(
|
|
254
|
+
config.serverUrl,
|
|
255
|
+
config.workerId,
|
|
256
|
+
config.email,
|
|
257
|
+
options.homeDir
|
|
258
|
+
) : void 0;
|
|
259
|
+
const removedConfig = removeFileIfPresent(configFile);
|
|
260
|
+
const removedState = removeFileIfPresent(stateFile);
|
|
261
|
+
const removedCredential = credentialFile ? removeFileIfPresent(credentialFile) : false;
|
|
262
|
+
return {
|
|
263
|
+
configFile,
|
|
264
|
+
stateFile,
|
|
265
|
+
credentialFile,
|
|
266
|
+
removedConfig,
|
|
267
|
+
removedState,
|
|
268
|
+
removedCredential
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function createClackPrompter() {
|
|
272
|
+
return {
|
|
273
|
+
async text(params) {
|
|
274
|
+
const value = await text({
|
|
275
|
+
message: params.message,
|
|
276
|
+
placeholder: params.placeholder,
|
|
277
|
+
defaultValue: params.initialValue,
|
|
278
|
+
validate: params.validate ? (input) => params.validate?.(input ?? "") : void 0
|
|
279
|
+
});
|
|
280
|
+
const resolved = unwrapCanceledValue(value, "OpenCode worker setup canceled");
|
|
281
|
+
return resolved;
|
|
282
|
+
},
|
|
283
|
+
async select(params) {
|
|
284
|
+
const value = await select({
|
|
285
|
+
message: params.message,
|
|
286
|
+
initialValue: params.initialValue,
|
|
287
|
+
options: params.options.map((option) => ({
|
|
288
|
+
value: option.value,
|
|
289
|
+
label: option.label
|
|
290
|
+
}))
|
|
291
|
+
});
|
|
292
|
+
const resolved = unwrapCanceledValue(value, "OpenCode worker setup canceled");
|
|
293
|
+
return resolved;
|
|
294
|
+
},
|
|
295
|
+
async confirm(params) {
|
|
296
|
+
const value = await confirm({
|
|
297
|
+
message: params.message,
|
|
298
|
+
initialValue: params.initialValue
|
|
299
|
+
});
|
|
300
|
+
const resolved = unwrapCanceledValue(value, "OpenCode worker setup canceled");
|
|
301
|
+
return resolved;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function requiredString(field) {
|
|
306
|
+
return (value) => value.trim() ? void 0 : `${field} is required`;
|
|
307
|
+
}
|
|
308
|
+
function validateDirectory(field) {
|
|
309
|
+
return (value) => {
|
|
310
|
+
const normalized = value.trim();
|
|
311
|
+
if (!normalized) {
|
|
312
|
+
return `${field} is required`;
|
|
313
|
+
}
|
|
314
|
+
if (!isAbsolute(normalized)) {
|
|
315
|
+
return `${field} must be an absolute path`;
|
|
316
|
+
}
|
|
317
|
+
if (!existsSync(normalized)) {
|
|
318
|
+
return `${field} does not exist`;
|
|
319
|
+
}
|
|
320
|
+
return void 0;
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function unwrapCanceledValue(value, message) {
|
|
324
|
+
if (isCancel(value)) {
|
|
325
|
+
throw new Error(message);
|
|
326
|
+
}
|
|
327
|
+
return value;
|
|
328
|
+
}
|
|
329
|
+
async function resolveTextInput(providedValue, prompter, params) {
|
|
330
|
+
if (providedValue !== void 0) {
|
|
331
|
+
const error = params.validate?.(providedValue);
|
|
332
|
+
if (error) {
|
|
333
|
+
throw new Error(error);
|
|
334
|
+
}
|
|
335
|
+
return providedValue;
|
|
336
|
+
}
|
|
337
|
+
return await prompter.text(params);
|
|
338
|
+
}
|
|
339
|
+
async function resolveSelectInput(providedValue, prompter, params) {
|
|
340
|
+
if (providedValue !== void 0) {
|
|
341
|
+
return providedValue;
|
|
342
|
+
}
|
|
343
|
+
return await prompter.select(params);
|
|
344
|
+
}
|
|
345
|
+
async function promptRepoMappings(prompter, defaultWorkspace) {
|
|
346
|
+
const repoMappings = {};
|
|
347
|
+
const addFirstPrompt = renderCliPrompt("cli.opencode-worker.repo-mapping.add-first", void 0);
|
|
348
|
+
let addAnotherMapping = await prompter.confirm({
|
|
349
|
+
message: addFirstPrompt.message,
|
|
350
|
+
initialValue: false
|
|
351
|
+
});
|
|
352
|
+
while (addAnotherMapping) {
|
|
353
|
+
const repoNamePrompt = renderCliPrompt("cli.opencode-worker.repo-mapping.repo-name", void 0);
|
|
354
|
+
const repo = await prompter.text({
|
|
355
|
+
message: repoNamePrompt.message,
|
|
356
|
+
placeholder: repoNamePrompt.placeholder,
|
|
357
|
+
validate: requiredString("repo")
|
|
358
|
+
});
|
|
359
|
+
const directoryPrompt = renderCliPrompt("cli.opencode-worker.repo-mapping.directory", {
|
|
360
|
+
repo,
|
|
361
|
+
defaultWorkspace
|
|
362
|
+
});
|
|
363
|
+
const cwd = await prompter.text({
|
|
364
|
+
message: directoryPrompt.message,
|
|
365
|
+
placeholder: directoryPrompt.placeholder,
|
|
366
|
+
validate: validateDirectory("cwd")
|
|
367
|
+
});
|
|
368
|
+
repoMappings[repo.trim()] = cwd.trim();
|
|
369
|
+
const addAnotherPrompt = renderCliPrompt("cli.opencode-worker.repo-mapping.add-another", void 0);
|
|
370
|
+
addAnotherMapping = await prompter.confirm({
|
|
371
|
+
message: addAnotherPrompt.message,
|
|
372
|
+
initialValue: false
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return repoMappings;
|
|
376
|
+
}
|
|
377
|
+
function removeFileIfPresent(file) {
|
|
378
|
+
if (!existsSync(file)) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
rmSync(file, { force: true });
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/parts.ts
|
|
386
|
+
function stringifyTaskMessageParts(parts) {
|
|
387
|
+
return parts.map((part) => {
|
|
388
|
+
if (part.type === "text") {
|
|
389
|
+
return part.text;
|
|
390
|
+
}
|
|
391
|
+
if (part.type === "data") {
|
|
392
|
+
return [
|
|
393
|
+
`[data${part.mimeType ? ` mimeType=${part.mimeType}` : ""}]`,
|
|
394
|
+
JSON.stringify(part.data, null, 2),
|
|
395
|
+
"[/data]"
|
|
396
|
+
].join("\n");
|
|
397
|
+
}
|
|
398
|
+
return [
|
|
399
|
+
"[file-ref]",
|
|
400
|
+
`uri: ${part.uri}`,
|
|
401
|
+
part.name ? `name: ${part.name}` : void 0,
|
|
402
|
+
part.mimeType ? `mimeType: ${part.mimeType}` : void 0,
|
|
403
|
+
"[/file-ref]"
|
|
404
|
+
].filter(Boolean).join("\n");
|
|
405
|
+
}).join("\n\n").trim();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/runtime.ts
|
|
409
|
+
import { Buffer } from "buffer";
|
|
410
|
+
import { spawn, spawnSync } from "child_process";
|
|
411
|
+
import { mkdtempSync } from "fs";
|
|
412
|
+
import { tmpdir } from "os";
|
|
413
|
+
import { join as join2 } from "path";
|
|
414
|
+
import process from "process";
|
|
415
|
+
import { renderTextLlmPrompt } from "@meego-harness/prompt-registry";
|
|
416
|
+
var OPENCODE_PREFLIGHT_TIMEOUT_MS = 1e4;
|
|
417
|
+
var OPENCODE_CONTRACT_TIMEOUT_MS = 3e4;
|
|
418
|
+
var OPENCODE_MAX_BUFFER = 1024 * 1024;
|
|
419
|
+
var OpenCodeCliTaskExecutor = class {
|
|
420
|
+
openCodeBin;
|
|
421
|
+
useShell;
|
|
422
|
+
shell;
|
|
423
|
+
spawnChild;
|
|
424
|
+
constructor(config = {}) {
|
|
425
|
+
this.openCodeBin = config.openCodeBin ?? "opencode";
|
|
426
|
+
this.useShell = config.useShell ?? false;
|
|
427
|
+
this.shell = config.shell ?? process.env.SHELL ?? "sh";
|
|
428
|
+
this.spawnChild = config.spawn ?? spawn;
|
|
429
|
+
}
|
|
430
|
+
async runTurn(request) {
|
|
431
|
+
if (request.abortSignal.aborted) {
|
|
432
|
+
throw createAbortError();
|
|
433
|
+
}
|
|
434
|
+
const args = buildOpenCodeArgs(request);
|
|
435
|
+
const command = buildOpenCodeSpawnCommand(this.openCodeBin, args, {
|
|
436
|
+
useShell: this.useShell,
|
|
437
|
+
shell: this.shell
|
|
438
|
+
});
|
|
439
|
+
const child = this.spawnChild(command.command, command.args, {
|
|
440
|
+
cwd: request.cwd,
|
|
441
|
+
detached: command.detached,
|
|
442
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
443
|
+
});
|
|
444
|
+
return await new Promise((resolve3, reject) => {
|
|
445
|
+
let settled = false;
|
|
446
|
+
const stdoutChunks = [];
|
|
447
|
+
const stderrChunks = [];
|
|
448
|
+
function settle(callback) {
|
|
449
|
+
if (settled) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
settled = true;
|
|
453
|
+
request.abortSignal.removeEventListener("abort", onAbort);
|
|
454
|
+
callback();
|
|
455
|
+
}
|
|
456
|
+
function onAbort() {
|
|
457
|
+
killChildProcess(child, {
|
|
458
|
+
killProcessGroup: command.killProcessGroup
|
|
459
|
+
});
|
|
460
|
+
settle(() => reject(createAbortError()));
|
|
461
|
+
}
|
|
462
|
+
request.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
463
|
+
child.stdout?.on("data", (chunk) => {
|
|
464
|
+
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
465
|
+
});
|
|
466
|
+
child.stderr?.on("data", (chunk) => {
|
|
467
|
+
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
468
|
+
});
|
|
469
|
+
child.once("error", (error) => {
|
|
470
|
+
settle(() => reject(error));
|
|
471
|
+
});
|
|
472
|
+
child.once("close", (code, signal) => {
|
|
473
|
+
if (request.abortSignal.aborted) {
|
|
474
|
+
settle(() => reject(createAbortError()));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
478
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
479
|
+
if (code !== 0) {
|
|
480
|
+
settle(
|
|
481
|
+
() => reject(new Error(buildOpenCodeExitError({
|
|
482
|
+
code,
|
|
483
|
+
signal,
|
|
484
|
+
stderr,
|
|
485
|
+
stdout
|
|
486
|
+
})))
|
|
487
|
+
);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const result = parseOpenCodeJsonResult(stdout, request.contextId);
|
|
492
|
+
settle(() => resolve3(result));
|
|
493
|
+
} catch (error) {
|
|
494
|
+
settle(() => reject(error instanceof Error ? error : new Error("Failed to parse OpenCode output")));
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
function assertOpenCodeCliAvailable(openCodeBin = "opencode", run = spawnSync, options = {}) {
|
|
501
|
+
for (const args of [
|
|
502
|
+
["--help"],
|
|
503
|
+
["run", "--help"]
|
|
504
|
+
]) {
|
|
505
|
+
const command = buildOpenCodeSpawnCommand(openCodeBin, args, options);
|
|
506
|
+
const result = run(command.command, command.args, {
|
|
507
|
+
...command.detached ? { detached: true } : {},
|
|
508
|
+
killSignal: "SIGTERM",
|
|
509
|
+
stdio: "ignore",
|
|
510
|
+
timeout: OPENCODE_PREFLIGHT_TIMEOUT_MS
|
|
511
|
+
});
|
|
512
|
+
if (result.error) {
|
|
513
|
+
throw new Error(`Unable to execute ${openCodeBin}: ${result.error.message}`);
|
|
514
|
+
}
|
|
515
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
516
|
+
throw new Error(
|
|
517
|
+
`OpenCode CLI preflight failed for "${[openCodeBin, ...args].join(" ")}" with exit code ${String(result.status)}`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function assertOpenCodeCliRuntimeContract(openCodeBin = "opencode", dependencies = {}) {
|
|
523
|
+
const run = dependencies.run ?? spawnSync;
|
|
524
|
+
const createTempDir = dependencies.createTempDir ?? (() => mkdtempSync(join2(tmpdir(), "meego-opencode-worker-doctor-")));
|
|
525
|
+
const tempDir = createTempDir();
|
|
526
|
+
const prompt = renderTextLlmPrompt("worker.opencode.runtime-contract-check", void 0).text;
|
|
527
|
+
const executionRequest = {
|
|
528
|
+
taskId: "doctor-contract",
|
|
529
|
+
contextId: "doctor-contract",
|
|
530
|
+
cwd: tempDir,
|
|
531
|
+
prompt,
|
|
532
|
+
variant: "low",
|
|
533
|
+
permissionPreset: "safe",
|
|
534
|
+
abortSignal: new AbortController().signal
|
|
535
|
+
};
|
|
536
|
+
const firstResult = runOpenCodeContractCommand(
|
|
537
|
+
run,
|
|
538
|
+
openCodeBin,
|
|
539
|
+
buildOpenCodeArgs(executionRequest),
|
|
540
|
+
tempDir,
|
|
541
|
+
dependencies
|
|
542
|
+
);
|
|
543
|
+
const firstParsed = parseOpenCodeJsonResult(firstResult.stdout, "doctor-contract");
|
|
544
|
+
const resumeResult = runOpenCodeContractCommand(
|
|
545
|
+
run,
|
|
546
|
+
openCodeBin,
|
|
547
|
+
buildOpenCodeArgs({
|
|
548
|
+
...executionRequest,
|
|
549
|
+
sessionId: firstParsed.sessionId
|
|
550
|
+
}),
|
|
551
|
+
tempDir,
|
|
552
|
+
dependencies
|
|
553
|
+
);
|
|
554
|
+
parseOpenCodeJsonResult(resumeResult.stdout, "doctor-contract");
|
|
555
|
+
}
|
|
556
|
+
function buildOpenCodeArgs(request) {
|
|
557
|
+
const args = [
|
|
558
|
+
"run",
|
|
559
|
+
"--format",
|
|
560
|
+
"json"
|
|
561
|
+
];
|
|
562
|
+
if (request.sessionId) {
|
|
563
|
+
args.push("--session", request.sessionId);
|
|
564
|
+
}
|
|
565
|
+
if (request.model) {
|
|
566
|
+
args.push("--model", request.model);
|
|
567
|
+
}
|
|
568
|
+
if (request.variant) {
|
|
569
|
+
args.push("--variant", request.variant);
|
|
570
|
+
}
|
|
571
|
+
if (request.agent) {
|
|
572
|
+
args.push("--agent", request.agent);
|
|
573
|
+
}
|
|
574
|
+
if (shouldSkipPermissionsForPreset(request.permissionPreset)) {
|
|
575
|
+
args.push("--dangerously-skip-permissions");
|
|
576
|
+
}
|
|
577
|
+
args.push(request.prompt);
|
|
578
|
+
return args;
|
|
579
|
+
}
|
|
580
|
+
function shouldSkipPermissionsForPreset(preset) {
|
|
581
|
+
return preset === "full-access";
|
|
582
|
+
}
|
|
583
|
+
function createAbortError() {
|
|
584
|
+
const error = new Error("OpenCode run aborted");
|
|
585
|
+
error.name = "AbortError";
|
|
586
|
+
return error;
|
|
587
|
+
}
|
|
588
|
+
function isAbortError(error) {
|
|
589
|
+
return error instanceof Error && error.name === "AbortError" || error instanceof Error && /abort/i.test(error.message);
|
|
590
|
+
}
|
|
591
|
+
function parseOpenCodeJsonResult(stdout, contextId) {
|
|
592
|
+
const text2 = Buffer.isBuffer(stdout) ? stdout.toString("utf8") : stdout ?? "";
|
|
593
|
+
const trimmed = text2.trim();
|
|
594
|
+
if (!trimmed) {
|
|
595
|
+
throw new Error(`OpenCode did not emit JSON output for context ${contextId}`);
|
|
596
|
+
}
|
|
597
|
+
const events = trimmed.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
598
|
+
try {
|
|
599
|
+
return JSON.parse(line);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
throw new Error(
|
|
602
|
+
`OpenCode emitted invalid JSONL for context ${contextId}: ${error instanceof Error ? error.message : String(error)}`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
const errorEvent = events.find((event) => event.type === "error");
|
|
607
|
+
if (errorEvent) {
|
|
608
|
+
throw new Error(`OpenCode returned an error for context ${contextId}: ${summarizeOpenCodeError(errorEvent.error)}`);
|
|
609
|
+
}
|
|
610
|
+
const sessionId = events.map((event) => typeof event.sessionID === "string" ? event.sessionID.trim() : "").find((value) => value.length > 0);
|
|
611
|
+
const textParts = events.filter((event) => event.type === "text" && event.part?.type === "text").map((event) => typeof event.part?.text === "string" ? event.part.text.trim() : "").filter(Boolean);
|
|
612
|
+
if (!sessionId || textParts.length === 0) {
|
|
613
|
+
throw new Error(`OpenCode returned an invalid result for context ${contextId}`);
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
text: textParts.join("\n\n").trim(),
|
|
617
|
+
sessionId
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function summarizeOpenCodeError(error) {
|
|
621
|
+
if (!error || typeof error !== "object") {
|
|
622
|
+
return String(error ?? "unknown");
|
|
623
|
+
}
|
|
624
|
+
const data = error.data;
|
|
625
|
+
if (data && typeof data === "object" && typeof data.message === "string") {
|
|
626
|
+
return data.message;
|
|
627
|
+
}
|
|
628
|
+
if (typeof error.message === "string") {
|
|
629
|
+
return error.message;
|
|
630
|
+
}
|
|
631
|
+
if (typeof error.name === "string") {
|
|
632
|
+
return error.name;
|
|
633
|
+
}
|
|
634
|
+
return "unknown";
|
|
635
|
+
}
|
|
636
|
+
function buildOpenCodeSpawnCommand(openCodeBin, args, options) {
|
|
637
|
+
if (!options.useShell) {
|
|
638
|
+
return {
|
|
639
|
+
command: openCodeBin,
|
|
640
|
+
args,
|
|
641
|
+
detached: false,
|
|
642
|
+
killProcessGroup: false
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
command: options.shell ?? process.env.SHELL ?? "sh",
|
|
647
|
+
args: ["-ic", buildShellCommand(openCodeBin, args)],
|
|
648
|
+
detached: true,
|
|
649
|
+
killProcessGroup: true
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function buildShellCommand(command, args) {
|
|
653
|
+
return [command, ...args.map(quoteShellArg)].join(" ");
|
|
654
|
+
}
|
|
655
|
+
function quoteShellArg(value) {
|
|
656
|
+
if (value.length === 0) {
|
|
657
|
+
return "''";
|
|
658
|
+
}
|
|
659
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
660
|
+
}
|
|
661
|
+
function killChildProcess(child, options) {
|
|
662
|
+
if (options.killProcessGroup && typeof child.pid === "number") {
|
|
663
|
+
try {
|
|
664
|
+
process.kill(-child.pid, "SIGTERM");
|
|
665
|
+
return;
|
|
666
|
+
} catch {
|
|
667
|
+
child.kill("SIGTERM");
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
child.kill("SIGTERM");
|
|
672
|
+
}
|
|
673
|
+
function runOpenCodeContractCommand(run, openCodeBin, args, cwd, options = {}) {
|
|
674
|
+
const command = buildOpenCodeSpawnCommand(openCodeBin, args, options);
|
|
675
|
+
const result = run(command.command, command.args, {
|
|
676
|
+
...command.detached ? { detached: true } : {},
|
|
677
|
+
cwd,
|
|
678
|
+
encoding: "utf8",
|
|
679
|
+
maxBuffer: OPENCODE_MAX_BUFFER,
|
|
680
|
+
timeout: OPENCODE_CONTRACT_TIMEOUT_MS
|
|
681
|
+
});
|
|
682
|
+
if (result.error) {
|
|
683
|
+
throw new Error(`Unable to execute ${openCodeBin}: ${result.error.message}`);
|
|
684
|
+
}
|
|
685
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
686
|
+
throw new Error(buildOpenCodeExitError({
|
|
687
|
+
code: result.status,
|
|
688
|
+
signal: result.signal,
|
|
689
|
+
stderr: stringifyOutput(result.stderr),
|
|
690
|
+
stdout: stringifyOutput(result.stdout)
|
|
691
|
+
}));
|
|
692
|
+
}
|
|
693
|
+
return result;
|
|
694
|
+
}
|
|
695
|
+
function buildOpenCodeExitError(params) {
|
|
696
|
+
const base = `OpenCode exited with code ${params.code}${params.signal ? ` (${params.signal})` : ""}`;
|
|
697
|
+
const stderr = summarizeOutput(params.stderr);
|
|
698
|
+
if (stderr) {
|
|
699
|
+
return `${base}; stderr=${stderr}`;
|
|
700
|
+
}
|
|
701
|
+
const stdout = summarizeOutput(params.stdout);
|
|
702
|
+
if (stdout) {
|
|
703
|
+
return `${base}; stdout=${stdout}`;
|
|
704
|
+
}
|
|
705
|
+
return base;
|
|
706
|
+
}
|
|
707
|
+
function summarizeOutput(value) {
|
|
708
|
+
return value.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).slice(-5).join(" | ");
|
|
709
|
+
}
|
|
710
|
+
function stringifyOutput(value) {
|
|
711
|
+
if (typeof value === "string") {
|
|
712
|
+
return value;
|
|
713
|
+
}
|
|
714
|
+
if (Buffer.isBuffer(value)) {
|
|
715
|
+
return value.toString("utf8");
|
|
716
|
+
}
|
|
717
|
+
return "";
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/bridge.ts
|
|
721
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
722
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join4, relative as relative2, resolve as resolve2 } from "path";
|
|
723
|
+
import process2 from "process";
|
|
724
|
+
import {
|
|
725
|
+
parseManagerRunResult
|
|
726
|
+
} from "@meego-harness/manager-contract";
|
|
727
|
+
import { renderTextLlmPrompt as renderTextLlmPrompt2, renderTextPrompt } from "@meego-harness/prompt-registry";
|
|
728
|
+
import { WorkerClientSDK } from "@meego-harness/worker-sdk";
|
|
729
|
+
|
|
730
|
+
// src/artifacts.ts
|
|
731
|
+
import { lstat, mkdir, readFile, rm } from "fs/promises";
|
|
732
|
+
import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join3, relative, resolve, sep } from "path";
|
|
733
|
+
var DEFAULT_ARTIFACT_STAGING_ROOT = "artifacts";
|
|
734
|
+
var MAX_STAGED_ARTIFACT_BYTES = 20 * 1024 * 1024;
|
|
735
|
+
var SAFE_IDENTIFIER_PATTERN = /^[A-Za-z0-9._-]+$/u;
|
|
736
|
+
var StagedArtifactValidationError = class extends Error {
|
|
737
|
+
artifactId;
|
|
738
|
+
constructor(artifactId, message) {
|
|
739
|
+
super(message);
|
|
740
|
+
this.name = "StagedArtifactValidationError";
|
|
741
|
+
this.artifactId = artifactId;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
function resolveTaskArtifactStagingDir(input) {
|
|
745
|
+
assertSafeIdentifier(input.taskId, "taskId");
|
|
746
|
+
const cwd = resolve(input.cwd);
|
|
747
|
+
const rootInput = input.artifactStagingRoot ?? DEFAULT_ARTIFACT_STAGING_ROOT;
|
|
748
|
+
const root = isAbsolute2(rootInput) ? resolve(rootInput) : resolve(cwd, rootInput);
|
|
749
|
+
const rootRelativePath = relative(cwd, root);
|
|
750
|
+
if (rootRelativePath.startsWith("..") || isAbsolute2(rootRelativePath)) {
|
|
751
|
+
throw new Error(`artifactStagingRoot must resolve inside cwd: ${root}`);
|
|
752
|
+
}
|
|
753
|
+
return join3(root, input.taskId);
|
|
754
|
+
}
|
|
755
|
+
async function prepareArtifactStagingDir(artifactStagingDir, boundaryDir = dirname2(dirname2(artifactStagingDir))) {
|
|
756
|
+
await assertExistingPathComponentsAreNotSymlinks(artifactStagingDir, boundaryDir);
|
|
757
|
+
await rm(artifactStagingDir, { recursive: true, force: true });
|
|
758
|
+
await mkdir(artifactStagingDir, { recursive: true });
|
|
759
|
+
}
|
|
760
|
+
function validateArtifactManifest(manifests) {
|
|
761
|
+
const artifactIds = /* @__PURE__ */ new Set();
|
|
762
|
+
for (const manifest of manifests) {
|
|
763
|
+
validateSingleArtifactManifest(manifest);
|
|
764
|
+
if (artifactIds.has(manifest.artifactId)) {
|
|
765
|
+
throw new StagedArtifactValidationError(
|
|
766
|
+
manifest.artifactId,
|
|
767
|
+
`duplicate staged artifact id: ${manifest.artifactId}`
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
artifactIds.add(manifest.artifactId);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async function readStagedArtifact(artifactStagingDir, manifest, boundaryDir) {
|
|
774
|
+
validateSingleArtifactManifest(manifest);
|
|
775
|
+
await assertExistingPathComponentsAreNotSymlinks(artifactStagingDir, boundaryDir);
|
|
776
|
+
const artifactDir = join3(artifactStagingDir, manifest.artifactId);
|
|
777
|
+
const file = join3(artifactStagingDir, manifest.artifactId, manifest.fileName);
|
|
778
|
+
const artifactDirStat = await lstat(artifactDir).catch(() => {
|
|
779
|
+
throw new StagedArtifactValidationError(
|
|
780
|
+
manifest.artifactId,
|
|
781
|
+
`missing staged artifact directory: ${manifest.artifactId}`
|
|
782
|
+
);
|
|
783
|
+
});
|
|
784
|
+
if (artifactDirStat.isSymbolicLink() || !artifactDirStat.isDirectory()) {
|
|
785
|
+
throw new StagedArtifactValidationError(
|
|
786
|
+
manifest.artifactId,
|
|
787
|
+
`invalid staged artifact directory: ${manifest.artifactId}`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
let fileStat;
|
|
791
|
+
try {
|
|
792
|
+
fileStat = await lstat(file);
|
|
793
|
+
} catch {
|
|
794
|
+
throw new StagedArtifactValidationError(
|
|
795
|
+
manifest.artifactId,
|
|
796
|
+
`missing staged artifact file: ${manifest.fileName}`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
if (fileStat.isSymbolicLink() || !fileStat.isFile() || fileStat.size === 0 || fileStat.size > MAX_STAGED_ARTIFACT_BYTES) {
|
|
800
|
+
throw new StagedArtifactValidationError(
|
|
801
|
+
manifest.artifactId,
|
|
802
|
+
`invalid staged artifact file: ${manifest.fileName}`
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
const data = await readFile(file);
|
|
806
|
+
return {
|
|
807
|
+
artifactId: manifest.artifactId,
|
|
808
|
+
name: manifest.fileName,
|
|
809
|
+
parts: [
|
|
810
|
+
{
|
|
811
|
+
type: "data",
|
|
812
|
+
data: {
|
|
813
|
+
base64: data.toString("base64"),
|
|
814
|
+
fileName: manifest.fileName
|
|
815
|
+
},
|
|
816
|
+
...manifest.mimeType ? { mimeType: manifest.mimeType } : {}
|
|
817
|
+
}
|
|
818
|
+
]
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
async function cleanupStagedArtifact(artifactStagingDir, manifest, boundaryDir) {
|
|
822
|
+
await assertExistingPathComponentsAreNotSymlinks(artifactStagingDir, boundaryDir);
|
|
823
|
+
const artifactDir = join3(artifactStagingDir, manifest.artifactId);
|
|
824
|
+
await assertStagedArtifactDirectory(artifactDir, manifest);
|
|
825
|
+
await assertStagedArtifactFile(join3(artifactDir, manifest.fileName), manifest);
|
|
826
|
+
await rm(join3(artifactDir, manifest.fileName), { force: true });
|
|
827
|
+
await assertExistingPathComponentsAreNotSymlinks(artifactStagingDir, boundaryDir);
|
|
828
|
+
await assertStagedArtifactDirectory(artifactDir, manifest);
|
|
829
|
+
await rm(artifactDir, { recursive: false }).catch(ignoreNonEmptyDirectoryError);
|
|
830
|
+
await assertExistingPathIsNotSymlink(artifactStagingDir, manifest.artifactId, "artifactStagingDir");
|
|
831
|
+
await rm(artifactStagingDir, { recursive: false }).catch(ignoreNonEmptyDirectoryError);
|
|
832
|
+
}
|
|
833
|
+
function validateSingleArtifactManifest(manifest) {
|
|
834
|
+
assertSafeIdentifier(manifest.artifactId, "artifactId");
|
|
835
|
+
assertSafeFileName(manifest.artifactId, manifest.fileName);
|
|
836
|
+
if (manifest.mimeType !== void 0 && manifest.mimeType.trim().length === 0) {
|
|
837
|
+
throw new StagedArtifactValidationError(manifest.artifactId, "staged artifact mimeType is empty");
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function assertSafeIdentifier(value, label) {
|
|
841
|
+
if (value === "." || value === ".." || !SAFE_IDENTIFIER_PATTERN.test(value)) {
|
|
842
|
+
throw new StagedArtifactValidationError(value, `unsafe staged artifact ${label}: ${value}`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function assertSafeFileName(artifactId, fileName) {
|
|
846
|
+
if (fileName.length === 0 || fileName !== basename(fileName) || fileName.includes("..")) {
|
|
847
|
+
throw new StagedArtifactValidationError(artifactId, `unsafe staged artifact fileName: ${fileName}`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async function assertExistingPathComponentsAreNotSymlinks(path, boundaryDir) {
|
|
851
|
+
const resolvedPath = resolve(path);
|
|
852
|
+
const resolvedBoundary = resolve(boundaryDir);
|
|
853
|
+
const relativePath = relative(resolvedBoundary, resolvedPath);
|
|
854
|
+
if (relativePath.startsWith("..") || isAbsolute2(relativePath)) {
|
|
855
|
+
throw new Error(`staged artifact path must stay inside boundary: ${resolvedPath}`);
|
|
856
|
+
}
|
|
857
|
+
await assertExistingPathIsNotSymlink(resolvedBoundary, resolvedBoundary, "path component");
|
|
858
|
+
const relativeParts = relativePath.split(sep).filter(Boolean);
|
|
859
|
+
let currentPath = resolvedBoundary;
|
|
860
|
+
for (const part of relativeParts) {
|
|
861
|
+
currentPath = join3(currentPath, part);
|
|
862
|
+
await assertExistingPathIsNotSymlink(currentPath, currentPath, "path component");
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
async function assertExistingPathIsNotSymlink(path, artifactId, label) {
|
|
866
|
+
const pathStat = await lstat(path).catch((error) => {
|
|
867
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
868
|
+
return void 0;
|
|
869
|
+
}
|
|
870
|
+
throw error;
|
|
871
|
+
});
|
|
872
|
+
if (pathStat?.isSymbolicLink()) {
|
|
873
|
+
throw new StagedArtifactValidationError(artifactId, `staged artifact ${label} is a symlink: ${path}`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async function assertStagedArtifactDirectory(artifactDir, manifest) {
|
|
877
|
+
const artifactDirStat = await lstat(artifactDir);
|
|
878
|
+
if (artifactDirStat.isSymbolicLink() || !artifactDirStat.isDirectory()) {
|
|
879
|
+
throw new StagedArtifactValidationError(
|
|
880
|
+
manifest.artifactId,
|
|
881
|
+
`invalid staged artifact directory: ${manifest.artifactId}`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
async function assertStagedArtifactFile(file, manifest) {
|
|
886
|
+
const fileStat = await lstat(file);
|
|
887
|
+
if (fileStat.isSymbolicLink() || !fileStat.isFile()) {
|
|
888
|
+
throw new StagedArtifactValidationError(
|
|
889
|
+
manifest.artifactId,
|
|
890
|
+
`invalid staged artifact file: ${manifest.fileName}`
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
function ignoreNonEmptyDirectoryError(error) {
|
|
895
|
+
if (!isNodeError(error) || error.code !== "ENOTEMPTY" && error.code !== "EEXIST") {
|
|
896
|
+
throw error;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function isNodeError(error) {
|
|
900
|
+
return error instanceof Error && "code" in error;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/bridge.ts
|
|
904
|
+
var OpenCodeWorkerBridge = class {
|
|
905
|
+
config;
|
|
906
|
+
executor;
|
|
907
|
+
logger;
|
|
908
|
+
client;
|
|
909
|
+
activeTasks = /* @__PURE__ */ new Map();
|
|
910
|
+
pendingCanceledTaskIds = /* @__PURE__ */ new Set();
|
|
911
|
+
bindings = /* @__PURE__ */ new Map();
|
|
912
|
+
contextQueues = /* @__PURE__ */ new Map();
|
|
913
|
+
stateFile;
|
|
914
|
+
constructor(config, dependencies) {
|
|
915
|
+
this.config = config;
|
|
916
|
+
this.executor = dependencies.executor;
|
|
917
|
+
this.logger = dependencies.logger;
|
|
918
|
+
this.client = dependencies.client ?? new WorkerClientSDK({
|
|
919
|
+
serverUrl: config.serverUrl,
|
|
920
|
+
reconnectBaseDelayMs: config.reconnectBaseDelayMs,
|
|
921
|
+
reconnectMaxDelayMs: config.reconnectMaxDelayMs
|
|
922
|
+
});
|
|
923
|
+
this.stateFile = config.stateFile ?? join4(process2.cwd(), `${config.workerId}.state.json`);
|
|
924
|
+
for (const binding of loadOpenCodeWorkerState(this.stateFile).contexts) {
|
|
925
|
+
this.bindings.set(binding.contextId, binding);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
async start() {
|
|
929
|
+
this.client.onMessage(async (context) => {
|
|
930
|
+
await this.handleTaskMessage(context);
|
|
931
|
+
});
|
|
932
|
+
this.client.onCancel(async ({ taskId }) => {
|
|
933
|
+
await this.handleTaskCancel(taskId);
|
|
934
|
+
});
|
|
935
|
+
this.client.onRoleChange(async (request) => {
|
|
936
|
+
if (request.type === "promote-to-manager") {
|
|
937
|
+
await this.client.switchRole({
|
|
938
|
+
workerId: this.config.workerId,
|
|
939
|
+
email: this.config.email,
|
|
940
|
+
role: "manager",
|
|
941
|
+
managerGrant: request.managerGrant,
|
|
942
|
+
capabilitySummary: this.config.capabilitySummary,
|
|
943
|
+
supportedRoles: buildSupportedRoles(OPENCODE_WORKER_ROLE),
|
|
944
|
+
initialAvailability: "available"
|
|
945
|
+
});
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
await this.client.switchRole({
|
|
949
|
+
workerId: this.config.workerId,
|
|
950
|
+
email: this.config.email,
|
|
951
|
+
role: request.role,
|
|
952
|
+
capabilitySummary: this.config.capabilitySummary,
|
|
953
|
+
supportedRoles: buildSupportedRoles(OPENCODE_WORKER_ROLE),
|
|
954
|
+
initialAvailability: "available"
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
await this.client.connect();
|
|
958
|
+
await this.client.login(buildWorkerLoginPayload(this.config, OPENCODE_WORKER_ROLE));
|
|
959
|
+
return this;
|
|
960
|
+
}
|
|
961
|
+
async stop() {
|
|
962
|
+
for (const execution of this.activeTasks.values()) {
|
|
963
|
+
execution.canceled = true;
|
|
964
|
+
execution.abortController.abort();
|
|
965
|
+
}
|
|
966
|
+
await Promise.allSettled(this.contextQueues.values());
|
|
967
|
+
this.activeTasks.clear();
|
|
968
|
+
this.pendingCanceledTaskIds.clear();
|
|
969
|
+
await this.client.disconnect();
|
|
970
|
+
}
|
|
971
|
+
async handleTaskMessage({
|
|
972
|
+
task,
|
|
973
|
+
message,
|
|
974
|
+
controller
|
|
975
|
+
}) {
|
|
976
|
+
this.supersedeActiveTaskExecution(task.id);
|
|
977
|
+
const previous = this.contextQueues.get(task.contextId) ?? Promise.resolve();
|
|
978
|
+
const current = previous.catch(() => void 0).then(async () => {
|
|
979
|
+
await this.processTaskMessage({ task, message, controller });
|
|
980
|
+
});
|
|
981
|
+
this.contextQueues.set(task.contextId, current);
|
|
982
|
+
try {
|
|
983
|
+
await current;
|
|
984
|
+
} finally {
|
|
985
|
+
if (this.contextQueues.get(task.contextId) === current) {
|
|
986
|
+
this.contextQueues.delete(task.contextId);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
async processTaskMessage({
|
|
991
|
+
task,
|
|
992
|
+
message,
|
|
993
|
+
controller
|
|
994
|
+
}) {
|
|
995
|
+
if (this.activeTasks.has(task.id)) {
|
|
996
|
+
this.logger.warn(`Ignoring duplicate active task delivery for ${task.id}`);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (this.pendingCanceledTaskIds.has(task.id)) {
|
|
1000
|
+
this.pendingCanceledTaskIds.delete(task.id);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const execution = {
|
|
1004
|
+
abortController: new AbortController(),
|
|
1005
|
+
canceled: false,
|
|
1006
|
+
supersededByMessage: false
|
|
1007
|
+
};
|
|
1008
|
+
this.activeTasks.set(task.id, execution);
|
|
1009
|
+
const requestedRepo = readOptionalStringMetadata(task.metadata, "repo");
|
|
1010
|
+
const existingBinding = this.bindings.get(task.contextId);
|
|
1011
|
+
let binding = existingBinding;
|
|
1012
|
+
let createdBinding = false;
|
|
1013
|
+
let resolvedBinding;
|
|
1014
|
+
let activeBinding;
|
|
1015
|
+
try {
|
|
1016
|
+
if (!binding) {
|
|
1017
|
+
binding = this.createBinding(task.contextId, task.metadata);
|
|
1018
|
+
this.bindings.set(task.contextId, binding);
|
|
1019
|
+
this.persistState();
|
|
1020
|
+
createdBinding = true;
|
|
1021
|
+
} else {
|
|
1022
|
+
const hydratedBinding = this.hydrateBindingFromMetadata(binding, task.metadata);
|
|
1023
|
+
if (hydratedBinding !== binding) {
|
|
1024
|
+
binding = hydratedBinding;
|
|
1025
|
+
this.bindings.set(task.contextId, binding);
|
|
1026
|
+
this.persistState();
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
resolvedBinding = binding;
|
|
1030
|
+
if (!resolvedBinding) {
|
|
1031
|
+
throw new Error(`Missing context binding for ${task.contextId}`);
|
|
1032
|
+
}
|
|
1033
|
+
const currentBinding = resolvedBinding;
|
|
1034
|
+
activeBinding = currentBinding;
|
|
1035
|
+
if (!await this.trySendTaskStatusUpdate(
|
|
1036
|
+
task.id,
|
|
1037
|
+
"working",
|
|
1038
|
+
async () => await controller.working({
|
|
1039
|
+
parts: [
|
|
1040
|
+
{
|
|
1041
|
+
type: "text",
|
|
1042
|
+
text: OPENCODE_WORKER_WORKING_TEXT
|
|
1043
|
+
}
|
|
1044
|
+
]
|
|
1045
|
+
})
|
|
1046
|
+
)) {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const managerRunInput = readManagerRunInput(task.metadata);
|
|
1050
|
+
const structuredProtocol = readOptionalStringMetadata(
|
|
1051
|
+
task.metadata,
|
|
1052
|
+
"projectNodeResultProtocol"
|
|
1053
|
+
);
|
|
1054
|
+
const artifactStagingDir = structuredProtocol && !managerRunInput ? resolveTaskArtifactStagingDir({
|
|
1055
|
+
cwd: currentBinding.cwd,
|
|
1056
|
+
taskId: task.id,
|
|
1057
|
+
artifactStagingRoot: this.config.artifactStagingRoot
|
|
1058
|
+
}) : void 0;
|
|
1059
|
+
if (artifactStagingDir) {
|
|
1060
|
+
await prepareArtifactStagingDir(artifactStagingDir, currentBinding.cwd);
|
|
1061
|
+
}
|
|
1062
|
+
const prompt = managerRunInput ? buildManagerRunPrompt(managerRunInput) : stringifyTaskMessageParts(message.parts);
|
|
1063
|
+
const promptWithArtifactInstructions = managerRunInput ? prompt : appendArtifactStagingInstructions(prompt, artifactStagingDir);
|
|
1064
|
+
const finalPrompt = createdBinding && currentBinding.planMode ? `${renderTextLlmPrompt2("worker.opencode.plan-mode-preamble", void 0).text}
|
|
1065
|
+
|
|
1066
|
+
${promptWithArtifactInstructions}`.trim() : promptWithArtifactInstructions;
|
|
1067
|
+
const result = await this.runExecutorTurn(buildOpenCodeTaskExecutionRequest(
|
|
1068
|
+
task,
|
|
1069
|
+
finalPrompt,
|
|
1070
|
+
currentBinding,
|
|
1071
|
+
this.config,
|
|
1072
|
+
execution
|
|
1073
|
+
));
|
|
1074
|
+
if (this.isTaskCanceled(task.id)) {
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (currentBinding.sessionId !== result.sessionId) {
|
|
1078
|
+
currentBinding.sessionId = result.sessionId;
|
|
1079
|
+
this.bindings.set(task.contextId, currentBinding);
|
|
1080
|
+
this.persistState();
|
|
1081
|
+
}
|
|
1082
|
+
if (managerRunInput) {
|
|
1083
|
+
const managerRunResult = parseManagerRunResult(
|
|
1084
|
+
parseJsonObjectOrThrow(result.text)
|
|
1085
|
+
);
|
|
1086
|
+
await this.trySendTaskStatusUpdate(
|
|
1087
|
+
task.id,
|
|
1088
|
+
"completed",
|
|
1089
|
+
async () => await controller.complete({
|
|
1090
|
+
parts: [
|
|
1091
|
+
{
|
|
1092
|
+
type: "text",
|
|
1093
|
+
text: managerRunResult.summary
|
|
1094
|
+
}
|
|
1095
|
+
],
|
|
1096
|
+
metadata: buildOpenCodeMetadata(currentBinding, requestedRepo, {
|
|
1097
|
+
managerRunResult
|
|
1098
|
+
})
|
|
1099
|
+
})
|
|
1100
|
+
);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
const structuredResult = structuredProtocol ? await this.resolveStructuredProjectNodeResult(
|
|
1104
|
+
task,
|
|
1105
|
+
currentBinding,
|
|
1106
|
+
structuredProtocol,
|
|
1107
|
+
artifactStagingDir,
|
|
1108
|
+
result.text,
|
|
1109
|
+
execution
|
|
1110
|
+
) : {
|
|
1111
|
+
status: "completed",
|
|
1112
|
+
summary: result.text
|
|
1113
|
+
};
|
|
1114
|
+
if (structuredResult.status === "input_required") {
|
|
1115
|
+
await this.trySendTaskStatusUpdate(
|
|
1116
|
+
task.id,
|
|
1117
|
+
"input-required",
|
|
1118
|
+
async () => await controller.inputRequired({
|
|
1119
|
+
parts: [
|
|
1120
|
+
{
|
|
1121
|
+
type: "text",
|
|
1122
|
+
text: structuredResult.question
|
|
1123
|
+
}
|
|
1124
|
+
],
|
|
1125
|
+
metadata: buildOpenCodeMetadata(currentBinding, requestedRepo, {
|
|
1126
|
+
"openCode.summary": structuredResult.summary,
|
|
1127
|
+
...structuredResult.options ? {
|
|
1128
|
+
responseSpec: {
|
|
1129
|
+
required: true,
|
|
1130
|
+
allowedPartTypes: ["text"],
|
|
1131
|
+
options: structuredResult.options
|
|
1132
|
+
}
|
|
1133
|
+
} : {},
|
|
1134
|
+
...structuredResult.reasonType ? { projectNodeReasonType: structuredResult.reasonType } : {}
|
|
1135
|
+
})
|
|
1136
|
+
})
|
|
1137
|
+
);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
await this.registerStructuredArtifacts(
|
|
1141
|
+
artifactStagingDir,
|
|
1142
|
+
currentBinding.cwd,
|
|
1143
|
+
structuredResult.artifacts,
|
|
1144
|
+
structuredResult.writes,
|
|
1145
|
+
controller
|
|
1146
|
+
);
|
|
1147
|
+
await this.trySendTaskStatusUpdate(
|
|
1148
|
+
task.id,
|
|
1149
|
+
"completed",
|
|
1150
|
+
async () => await controller.complete({
|
|
1151
|
+
parts: [
|
|
1152
|
+
{
|
|
1153
|
+
type: "text",
|
|
1154
|
+
text: structuredResult.summary
|
|
1155
|
+
}
|
|
1156
|
+
],
|
|
1157
|
+
metadata: buildOpenCodeMetadata(
|
|
1158
|
+
currentBinding,
|
|
1159
|
+
requestedRepo,
|
|
1160
|
+
structuredResult.writes || structuredResult.childWorkItems ? {
|
|
1161
|
+
...structuredResult.writes ? { projectNodeWrites: structuredResult.writes } : {},
|
|
1162
|
+
...structuredResult.childWorkItems ? { projectNodeChildWorkItems: structuredResult.childWorkItems } : {}
|
|
1163
|
+
} : void 0
|
|
1164
|
+
)
|
|
1165
|
+
})
|
|
1166
|
+
);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
if (this.isTaskCanceled(task.id) || isAbortError(error)) {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
await this.trySendTaskStatusUpdate(
|
|
1172
|
+
task.id,
|
|
1173
|
+
"failed",
|
|
1174
|
+
async () => await controller.fail({
|
|
1175
|
+
parts: [
|
|
1176
|
+
{
|
|
1177
|
+
type: "text",
|
|
1178
|
+
text: error instanceof Error ? error.message : "OpenCode run failed"
|
|
1179
|
+
}
|
|
1180
|
+
],
|
|
1181
|
+
metadata: buildOpenCodeFailureMetadata(
|
|
1182
|
+
activeBinding,
|
|
1183
|
+
requestedRepo,
|
|
1184
|
+
this.config,
|
|
1185
|
+
error
|
|
1186
|
+
)
|
|
1187
|
+
})
|
|
1188
|
+
);
|
|
1189
|
+
} finally {
|
|
1190
|
+
this.activeTasks.delete(task.id);
|
|
1191
|
+
this.pendingCanceledTaskIds.delete(task.id);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
async trySendTaskStatusUpdate(taskId, targetState, action) {
|
|
1195
|
+
try {
|
|
1196
|
+
await action();
|
|
1197
|
+
return true;
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
if (isTerminalTaskConflictError(error)) {
|
|
1200
|
+
this.logger.warn(
|
|
1201
|
+
`Ignoring stale terminal task update for ${taskId} while reporting ${targetState}: ${error.message}`
|
|
1202
|
+
);
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
if (isWorkerConnectionClosedError(error)) {
|
|
1206
|
+
this.logger.warn(
|
|
1207
|
+
`Skipping ${targetState} update for ${taskId} because the worker connection is already closed`
|
|
1208
|
+
);
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1211
|
+
throw error;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
async handleTaskCancel(taskId) {
|
|
1215
|
+
const execution = this.activeTasks.get(taskId);
|
|
1216
|
+
if (!execution) {
|
|
1217
|
+
this.pendingCanceledTaskIds.add(taskId);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
execution.canceled = true;
|
|
1221
|
+
execution.abortController.abort();
|
|
1222
|
+
}
|
|
1223
|
+
supersedeActiveTaskExecution(taskId) {
|
|
1224
|
+
const execution = this.activeTasks.get(taskId);
|
|
1225
|
+
if (!execution || execution.canceled || execution.supersededByMessage) {
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
execution.supersededByMessage = true;
|
|
1229
|
+
execution.abortController.abort();
|
|
1230
|
+
}
|
|
1231
|
+
async runExecutorTurn(request) {
|
|
1232
|
+
if (request.abortSignal.aborted) {
|
|
1233
|
+
throw createAbortError();
|
|
1234
|
+
}
|
|
1235
|
+
const abortedResult = { aborted: true };
|
|
1236
|
+
let abortObserved = false;
|
|
1237
|
+
const executorPromise = this.executor.runTurn(request).catch((error) => {
|
|
1238
|
+
if (abortObserved && (request.abortSignal.aborted || isAbortError(error))) {
|
|
1239
|
+
return abortedResult;
|
|
1240
|
+
}
|
|
1241
|
+
throw error;
|
|
1242
|
+
});
|
|
1243
|
+
const abortPromise = new Promise((resolve3) => {
|
|
1244
|
+
function onAbort() {
|
|
1245
|
+
request.abortSignal.removeEventListener("abort", onAbort);
|
|
1246
|
+
abortObserved = true;
|
|
1247
|
+
resolve3(abortedResult);
|
|
1248
|
+
}
|
|
1249
|
+
request.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
1250
|
+
void executorPromise.then(() => {
|
|
1251
|
+
request.abortSignal.removeEventListener("abort", onAbort);
|
|
1252
|
+
}, () => {
|
|
1253
|
+
request.abortSignal.removeEventListener("abort", onAbort);
|
|
1254
|
+
});
|
|
1255
|
+
});
|
|
1256
|
+
const result = await Promise.race([
|
|
1257
|
+
executorPromise,
|
|
1258
|
+
abortPromise
|
|
1259
|
+
]);
|
|
1260
|
+
if ("aborted" in result) {
|
|
1261
|
+
throw createAbortError();
|
|
1262
|
+
}
|
|
1263
|
+
return result;
|
|
1264
|
+
}
|
|
1265
|
+
createBinding(contextId, metadata) {
|
|
1266
|
+
const repo = readOptionalStringMetadata(metadata, "repo");
|
|
1267
|
+
const explicitVariant = readOptionalStringMetadata(metadata, "openCodeVariant");
|
|
1268
|
+
const explicitAgent = readOptionalStringMetadata(metadata, "openCodeAgent");
|
|
1269
|
+
const planMode = readOptionalBooleanMetadata(metadata, "openCodePlanMode") ?? false;
|
|
1270
|
+
const cwd = this.resolveCwd(repo);
|
|
1271
|
+
return {
|
|
1272
|
+
contextId,
|
|
1273
|
+
...repo ? { repo } : {},
|
|
1274
|
+
cwd,
|
|
1275
|
+
variant: explicitVariant ?? (this.config.variantDefault === "use-opencode-default" ? void 0 : this.config.variantDefault),
|
|
1276
|
+
agent: explicitAgent ?? (this.config.agentDefault === "use-opencode-default" ? void 0 : this.config.agentDefault),
|
|
1277
|
+
planMode
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
hydrateBindingFromMetadata(binding, metadata) {
|
|
1281
|
+
const repo = readOptionalStringMetadata(metadata, "repo");
|
|
1282
|
+
if (binding.repo) {
|
|
1283
|
+
return binding;
|
|
1284
|
+
}
|
|
1285
|
+
if (!repo) {
|
|
1286
|
+
return binding;
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
...binding,
|
|
1290
|
+
repo,
|
|
1291
|
+
cwd: this.resolveCwd(repo)
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
resolveCwd(repo) {
|
|
1295
|
+
if (!repo) {
|
|
1296
|
+
this.ensureDirectory(this.config.defaultWorkspace, "default workspace directory");
|
|
1297
|
+
return this.config.defaultWorkspace;
|
|
1298
|
+
}
|
|
1299
|
+
const mapped = this.config.repoMappings[repo];
|
|
1300
|
+
if (Object.keys(this.config.repoMappings).length > 0) {
|
|
1301
|
+
if (!mapped) {
|
|
1302
|
+
throw new Error(`No repo mapping configured for ${repo}`);
|
|
1303
|
+
}
|
|
1304
|
+
this.ensureDirectory(mapped, `mapped repo directory for ${repo}`);
|
|
1305
|
+
this.ensureWorkspaceSubdirectory(mapped, `mapped repo directory for ${repo}`);
|
|
1306
|
+
return mapped;
|
|
1307
|
+
}
|
|
1308
|
+
const inferred = join4(this.config.defaultWorkspace, repo);
|
|
1309
|
+
this.ensureDirectory(inferred, `workspace repo directory for ${repo}`);
|
|
1310
|
+
this.ensureWorkspaceSubdirectory(inferred, `workspace repo directory for ${repo}`);
|
|
1311
|
+
return inferred;
|
|
1312
|
+
}
|
|
1313
|
+
ensureDirectory(path, label) {
|
|
1314
|
+
if (!existsSync2(path)) {
|
|
1315
|
+
throw new Error(`Missing ${label}: ${path}`);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
ensureWorkspaceSubdirectory(path, label) {
|
|
1319
|
+
const workspaceRoot = resolve2(this.config.defaultWorkspace);
|
|
1320
|
+
const candidate = resolve2(path);
|
|
1321
|
+
const relativePath = relative2(workspaceRoot, candidate);
|
|
1322
|
+
if (!relativePath || relativePath === "." || relativePath.startsWith("..") || isAbsolute3(relativePath)) {
|
|
1323
|
+
throw new Error(`Expected ${label} to be a subdirectory of ${workspaceRoot}: ${candidate}`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
persistState() {
|
|
1327
|
+
mkdirSync2(dirname3(this.stateFile), { recursive: true });
|
|
1328
|
+
writeOpenCodeWorkerState(this.stateFile, {
|
|
1329
|
+
contexts: Array.from(this.bindings.values())
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
isTaskCanceled(taskId) {
|
|
1333
|
+
return this.activeTasks.get(taskId)?.canceled ?? this.pendingCanceledTaskIds.has(taskId);
|
|
1334
|
+
}
|
|
1335
|
+
async resolveStructuredProjectNodeResult(task, binding, protocol, artifactStagingDir, firstText, execution) {
|
|
1336
|
+
const firstParsed = parseProjectNodeResult(protocol, firstText);
|
|
1337
|
+
if (firstParsed) {
|
|
1338
|
+
return firstParsed;
|
|
1339
|
+
}
|
|
1340
|
+
const retryResult = await this.runExecutorTurn(buildOpenCodeTaskExecutionRequest(
|
|
1341
|
+
task,
|
|
1342
|
+
buildStructuredRetryPrompt(protocol, artifactStagingDir),
|
|
1343
|
+
binding,
|
|
1344
|
+
this.config,
|
|
1345
|
+
execution
|
|
1346
|
+
));
|
|
1347
|
+
if (this.isTaskCanceled(task.id)) {
|
|
1348
|
+
throw new Error("task canceled before structured retry completed");
|
|
1349
|
+
}
|
|
1350
|
+
if (binding.sessionId !== retryResult.sessionId) {
|
|
1351
|
+
binding.sessionId = retryResult.sessionId;
|
|
1352
|
+
this.bindings.set(task.contextId, binding);
|
|
1353
|
+
this.persistState();
|
|
1354
|
+
}
|
|
1355
|
+
const retryParsed = parseProjectNodeResult(protocol, retryResult.text);
|
|
1356
|
+
if (retryParsed) {
|
|
1357
|
+
return retryParsed;
|
|
1358
|
+
}
|
|
1359
|
+
throw new Error("OpenCode returned an invalid structured result after one retry");
|
|
1360
|
+
}
|
|
1361
|
+
async registerStructuredArtifacts(artifactStagingDir, artifactBoundaryDir, manifests, writes, controller) {
|
|
1362
|
+
const attachmentWrites = writes?.filter(isProjectNodeAttachmentWrite) ?? [];
|
|
1363
|
+
if (!manifests?.length && attachmentWrites.length === 0) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
const manifestById = /* @__PURE__ */ new Map();
|
|
1367
|
+
for (const manifest of manifests ?? []) {
|
|
1368
|
+
manifestById.set(manifest.artifactId, manifest);
|
|
1369
|
+
}
|
|
1370
|
+
for (const write of attachmentWrites) {
|
|
1371
|
+
if (!manifestById.has(write.artifactId)) {
|
|
1372
|
+
throw createAttachmentArtifactNotFoundError(write.artifactId);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (!artifactStagingDir) {
|
|
1376
|
+
throw createAttachmentArtifactNotFoundError(attachmentWrites[0].artifactId);
|
|
1377
|
+
}
|
|
1378
|
+
try {
|
|
1379
|
+
validateArtifactManifest(manifests ?? []);
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
if (error instanceof StagedArtifactValidationError) {
|
|
1382
|
+
throw createAttachmentArtifactSourceInvalidError(error.artifactId);
|
|
1383
|
+
}
|
|
1384
|
+
throw error;
|
|
1385
|
+
}
|
|
1386
|
+
for (const manifest of manifests ?? []) {
|
|
1387
|
+
let artifact;
|
|
1388
|
+
try {
|
|
1389
|
+
artifact = await readStagedArtifact(artifactStagingDir, manifest, artifactBoundaryDir);
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
if (error instanceof StagedArtifactValidationError) {
|
|
1392
|
+
throw createAttachmentArtifactSourceInvalidError(manifest.artifactId);
|
|
1393
|
+
}
|
|
1394
|
+
throw error;
|
|
1395
|
+
}
|
|
1396
|
+
await controller.addArtifact(artifact);
|
|
1397
|
+
await cleanupStagedArtifact(artifactStagingDir, manifest, artifactBoundaryDir).catch((error) => {
|
|
1398
|
+
this.logger.warn(
|
|
1399
|
+
`Failed to clean staged artifact ${manifest.artifactId}: ${error instanceof Error ? error.message : String(error)}`
|
|
1400
|
+
);
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
function isProjectNodeAttachmentWrite(write) {
|
|
1406
|
+
return write.kind === "upload-attachment";
|
|
1407
|
+
}
|
|
1408
|
+
function createAttachmentArtifactNotFoundError(artifactId) {
|
|
1409
|
+
return new Error(renderTextPrompt("sdk.runtime-error.message", {
|
|
1410
|
+
errorCode: "work-item-attachment-artifact-not-found",
|
|
1411
|
+
artifactId
|
|
1412
|
+
}).text);
|
|
1413
|
+
}
|
|
1414
|
+
function createAttachmentArtifactSourceInvalidError(artifactId) {
|
|
1415
|
+
return new Error(renderTextPrompt("sdk.runtime-error.message", {
|
|
1416
|
+
errorCode: "work-item-attachment-source-invalid",
|
|
1417
|
+
artifactId
|
|
1418
|
+
}).text);
|
|
1419
|
+
}
|
|
1420
|
+
function buildWorkerLoginPayload(config, role) {
|
|
1421
|
+
return {
|
|
1422
|
+
workerId: config.workerId,
|
|
1423
|
+
email: config.email,
|
|
1424
|
+
role,
|
|
1425
|
+
capabilitySummary: config.capabilitySummary,
|
|
1426
|
+
supportedRoles: buildSupportedRoles(OPENCODE_WORKER_ROLE),
|
|
1427
|
+
initialAvailability: "available"
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
function buildSupportedRoles(baseRole) {
|
|
1431
|
+
return [baseRole, "manager"];
|
|
1432
|
+
}
|
|
1433
|
+
function buildOpenCodeMetadata(binding, requestedRepo, extraMetadata) {
|
|
1434
|
+
return {
|
|
1435
|
+
"openCode.sessionId": binding.sessionId,
|
|
1436
|
+
"openCode.cwd": binding.cwd,
|
|
1437
|
+
...binding.repo ? { "openCode.repo": binding.repo } : {},
|
|
1438
|
+
...requestedRepo ? { "openCode.requestedRepo": requestedRepo } : {},
|
|
1439
|
+
...binding.variant ? { "openCode.variant": binding.variant } : {},
|
|
1440
|
+
...binding.agent ? { "openCode.agent": binding.agent } : {},
|
|
1441
|
+
"openCode.planMode": binding.planMode,
|
|
1442
|
+
...extraMetadata ? structuredClone(extraMetadata) : {}
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
function buildOpenCodeFailureMetadata(binding, requestedRepo, config, error) {
|
|
1446
|
+
const baseMetadata = binding ? buildOpenCodeMetadata(binding, requestedRepo) : {};
|
|
1447
|
+
const failureReason = readOpenCodeFailureReason(error);
|
|
1448
|
+
return {
|
|
1449
|
+
...baseMetadata,
|
|
1450
|
+
"openCode.modelSelection": config.modelSelection,
|
|
1451
|
+
"openCode.effectiveModel": config.modelSelection === "use-opencode-default" ? "opencode-default" : config.modelSelection,
|
|
1452
|
+
"openCode.permissionPreset": config.permissionPreset,
|
|
1453
|
+
...config.variantDefault !== "use-opencode-default" ? { "openCode.defaultVariant": config.variantDefault } : {},
|
|
1454
|
+
...config.agentDefault !== "use-opencode-default" ? { "openCode.defaultAgent": config.agentDefault } : {},
|
|
1455
|
+
...failureReason ? { "openCode.failureReason": failureReason } : {}
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
function buildOpenCodeTaskExecutionRequest(task, prompt, binding, config, execution) {
|
|
1459
|
+
return {
|
|
1460
|
+
taskId: task.id,
|
|
1461
|
+
contextId: task.contextId,
|
|
1462
|
+
cwd: binding.cwd,
|
|
1463
|
+
prompt,
|
|
1464
|
+
sessionId: binding.sessionId,
|
|
1465
|
+
model: config.modelSelection === "use-opencode-default" ? void 0 : config.modelSelection,
|
|
1466
|
+
variant: binding.variant,
|
|
1467
|
+
agent: binding.agent,
|
|
1468
|
+
permissionPreset: config.permissionPreset,
|
|
1469
|
+
abortSignal: execution.abortController.signal
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
function appendArtifactStagingInstructions(prompt, artifactStagingDir) {
|
|
1473
|
+
if (!artifactStagingDir) {
|
|
1474
|
+
return prompt;
|
|
1475
|
+
}
|
|
1476
|
+
const instructions = renderTextLlmPrompt2("worker.opencode.artifact-staging-instructions", {
|
|
1477
|
+
artifactStagingDir
|
|
1478
|
+
}).text;
|
|
1479
|
+
return `${prompt}
|
|
1480
|
+
|
|
1481
|
+
${instructions}`.trim();
|
|
1482
|
+
}
|
|
1483
|
+
function buildStructuredRetryPrompt(_protocol, artifactStagingDir) {
|
|
1484
|
+
return renderTextLlmPrompt2(
|
|
1485
|
+
"worker.opencode.structured-retry",
|
|
1486
|
+
artifactStagingDir ? { artifactStagingDir } : void 0
|
|
1487
|
+
).text;
|
|
1488
|
+
}
|
|
1489
|
+
function buildManagerRunPrompt(input) {
|
|
1490
|
+
return renderTextLlmPrompt2("worker.opencode.manager-run", {
|
|
1491
|
+
managerRunInput: input
|
|
1492
|
+
}).text;
|
|
1493
|
+
}
|
|
1494
|
+
function isTerminalTaskConflictError(error) {
|
|
1495
|
+
return error instanceof Error && (error.message.includes("task is already terminal") || error.message.includes("cannot continue a terminal task"));
|
|
1496
|
+
}
|
|
1497
|
+
function isWorkerConnectionClosedError(error) {
|
|
1498
|
+
return error instanceof Error && error.message.includes("WebSocket is not connected");
|
|
1499
|
+
}
|
|
1500
|
+
function parseProjectNodeResult(protocol, text2) {
|
|
1501
|
+
const parsed = parseProjectNodeResultObject(text2);
|
|
1502
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1503
|
+
return null;
|
|
1504
|
+
}
|
|
1505
|
+
if ("status" in parsed) {
|
|
1506
|
+
const status = normalizeStatus(parsed.status);
|
|
1507
|
+
const summary = normalizeSummary(parsed.summary);
|
|
1508
|
+
if (!status || !summary) {
|
|
1509
|
+
return null;
|
|
1510
|
+
}
|
|
1511
|
+
if (status === "completed") {
|
|
1512
|
+
const writes = normalizeProjectNodeWrites(
|
|
1513
|
+
parsed.projectNodeWrites ?? parsed.writes
|
|
1514
|
+
);
|
|
1515
|
+
if (writes === null) {
|
|
1516
|
+
return null;
|
|
1517
|
+
}
|
|
1518
|
+
const artifacts = normalizeProjectNodeArtifacts(parsed.artifacts);
|
|
1519
|
+
if (artifacts === null) {
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
const childWorkItems = normalizeProjectNodeChildWorkItems(
|
|
1523
|
+
parsed.childWorkItems
|
|
1524
|
+
);
|
|
1525
|
+
return {
|
|
1526
|
+
status,
|
|
1527
|
+
summary,
|
|
1528
|
+
...artifacts && artifacts.length > 0 ? { artifacts } : {},
|
|
1529
|
+
...writes && writes.length > 0 ? { writes } : {},
|
|
1530
|
+
...childWorkItems && childWorkItems.length > 0 ? { childWorkItems } : {}
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
const question = normalizeSummary(parsed.question);
|
|
1534
|
+
if (!question) {
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
const reasonType = normalizeProjectNodeReasonType(
|
|
1538
|
+
parsed.reasonType
|
|
1539
|
+
) ?? void 0;
|
|
1540
|
+
const options = normalizeResponseOptions(
|
|
1541
|
+
parsed.options
|
|
1542
|
+
);
|
|
1543
|
+
return {
|
|
1544
|
+
status,
|
|
1545
|
+
summary,
|
|
1546
|
+
question,
|
|
1547
|
+
...options ? { options } : {},
|
|
1548
|
+
...reasonType ? { reasonType } : {}
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
const choice = normalizeChoicePrefixedResult(parsed);
|
|
1552
|
+
if (!choice) {
|
|
1553
|
+
return null;
|
|
1554
|
+
}
|
|
1555
|
+
return choice;
|
|
1556
|
+
}
|
|
1557
|
+
function readManagerRunInput(metadata) {
|
|
1558
|
+
const input = metadata?.managerRunInput;
|
|
1559
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
1560
|
+
return null;
|
|
1561
|
+
}
|
|
1562
|
+
return structuredClone(input);
|
|
1563
|
+
}
|
|
1564
|
+
function readOpenCodeFailureReason(error) {
|
|
1565
|
+
if (!(error instanceof Error)) {
|
|
1566
|
+
return void 0;
|
|
1567
|
+
}
|
|
1568
|
+
const explicitReason = error.message.match(/;\s*reason=([^;]+)/u)?.[1]?.trim();
|
|
1569
|
+
if (explicitReason) {
|
|
1570
|
+
return explicitReason;
|
|
1571
|
+
}
|
|
1572
|
+
if (/context window/i.test(error.message)) {
|
|
1573
|
+
return "context_window_exceeded";
|
|
1574
|
+
}
|
|
1575
|
+
return void 0;
|
|
1576
|
+
}
|
|
1577
|
+
function parseJsonObjectOrThrow(text2) {
|
|
1578
|
+
const parsed = parseProjectNodeResultObject(text2);
|
|
1579
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1580
|
+
throw new Error("OpenCode returned an invalid manager run result");
|
|
1581
|
+
}
|
|
1582
|
+
return parsed;
|
|
1583
|
+
}
|
|
1584
|
+
function parseProjectNodeResultObject(text2) {
|
|
1585
|
+
const trimmed = text2.trim();
|
|
1586
|
+
if (!trimmed) {
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
const jsonText = unwrapJsonCodeFence(trimmed) ?? trimmed;
|
|
1590
|
+
try {
|
|
1591
|
+
return JSON.parse(jsonText);
|
|
1592
|
+
} catch {
|
|
1593
|
+
const matched = jsonText.match(/^([12])\s+(\{[\s\S]+\})$/);
|
|
1594
|
+
if (!matched) {
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
try {
|
|
1598
|
+
return {
|
|
1599
|
+
choice: matched[1],
|
|
1600
|
+
...JSON.parse(matched[2])
|
|
1601
|
+
};
|
|
1602
|
+
} catch {
|
|
1603
|
+
return null;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function unwrapJsonCodeFence(text2) {
|
|
1608
|
+
if (!text2.startsWith("```") || !text2.endsWith("```")) {
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1611
|
+
const inner = text2.slice(3, -3);
|
|
1612
|
+
const normalizedInner = inner.startsWith("\r\n") ? inner.slice(2) : inner.startsWith("\n") ? inner.slice(1) : inner;
|
|
1613
|
+
const newlineIndex = normalizedInner.search(/\r?\n/u);
|
|
1614
|
+
if (newlineIndex < 0) {
|
|
1615
|
+
return null;
|
|
1616
|
+
}
|
|
1617
|
+
const header = normalizedInner.slice(0, newlineIndex).trim().toLowerCase();
|
|
1618
|
+
if (header && header !== "json") {
|
|
1619
|
+
return null;
|
|
1620
|
+
}
|
|
1621
|
+
return normalizedInner.slice(newlineIndex + 1).trim();
|
|
1622
|
+
}
|
|
1623
|
+
function normalizeStatus(value) {
|
|
1624
|
+
if (value === "completed" || value === "input_required") {
|
|
1625
|
+
return value;
|
|
1626
|
+
}
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
function normalizeSummary(value) {
|
|
1630
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1631
|
+
}
|
|
1632
|
+
function normalizeChoicePrefixedResult(parsed) {
|
|
1633
|
+
const choice = parsed.choice;
|
|
1634
|
+
const summary = normalizeSummary(parsed.summary);
|
|
1635
|
+
if (!summary || choice !== "1" && choice !== "2") {
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
if (choice === "1") {
|
|
1639
|
+
return {
|
|
1640
|
+
status: "completed",
|
|
1641
|
+
summary
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
const question = normalizeSummary(parsed.question);
|
|
1645
|
+
if (!question) {
|
|
1646
|
+
return null;
|
|
1647
|
+
}
|
|
1648
|
+
const reasonType = normalizeProjectNodeReasonType(parsed.reasonType) ?? void 0;
|
|
1649
|
+
return {
|
|
1650
|
+
status: "input_required",
|
|
1651
|
+
summary,
|
|
1652
|
+
question,
|
|
1653
|
+
...reasonType ? { reasonType } : {}
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
function normalizeResponseOptions(value) {
|
|
1657
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1658
|
+
return void 0;
|
|
1659
|
+
}
|
|
1660
|
+
const normalized = [];
|
|
1661
|
+
const seenValues = /* @__PURE__ */ new Set();
|
|
1662
|
+
for (const item of value) {
|
|
1663
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
1664
|
+
return void 0;
|
|
1665
|
+
}
|
|
1666
|
+
const optionValue = normalizeSummary(item.value);
|
|
1667
|
+
const label = normalizeSummary(item.label);
|
|
1668
|
+
const responseText = normalizeSummary(item.responseText);
|
|
1669
|
+
const description = normalizeSummary(item.description);
|
|
1670
|
+
if (!optionValue || !label || !responseText || seenValues.has(optionValue)) {
|
|
1671
|
+
return void 0;
|
|
1672
|
+
}
|
|
1673
|
+
seenValues.add(optionValue);
|
|
1674
|
+
normalized.push({
|
|
1675
|
+
value: optionValue,
|
|
1676
|
+
label,
|
|
1677
|
+
responseText,
|
|
1678
|
+
...description ? { description } : {}
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
1682
|
+
}
|
|
1683
|
+
function normalizeProjectNodeWrites(value) {
|
|
1684
|
+
if (value === void 0 || value === null) {
|
|
1685
|
+
return void 0;
|
|
1686
|
+
}
|
|
1687
|
+
if (!Array.isArray(value)) {
|
|
1688
|
+
return null;
|
|
1689
|
+
}
|
|
1690
|
+
const writes = [];
|
|
1691
|
+
for (const item of value) {
|
|
1692
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
const record = item;
|
|
1696
|
+
const fieldKey = normalizeSummary(record.fieldKey);
|
|
1697
|
+
const reason = normalizeSummary(record.reason) ?? void 0;
|
|
1698
|
+
if (!fieldKey) {
|
|
1699
|
+
return null;
|
|
1700
|
+
}
|
|
1701
|
+
if (record.kind === "set-field") {
|
|
1702
|
+
if (record.value === void 0 || record.value === null) {
|
|
1703
|
+
return null;
|
|
1704
|
+
}
|
|
1705
|
+
writes.push({
|
|
1706
|
+
kind: "set-field",
|
|
1707
|
+
fieldKey,
|
|
1708
|
+
value: structuredClone(record.value),
|
|
1709
|
+
...reason ? { reason } : {}
|
|
1710
|
+
});
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
if (record.kind === "upload-attachment") {
|
|
1714
|
+
const artifactId = normalizeSummary(record.artifactId);
|
|
1715
|
+
if (!artifactId) {
|
|
1716
|
+
return null;
|
|
1717
|
+
}
|
|
1718
|
+
writes.push({
|
|
1719
|
+
kind: "upload-attachment",
|
|
1720
|
+
fieldKey,
|
|
1721
|
+
artifactId,
|
|
1722
|
+
...reason ? { reason } : {}
|
|
1723
|
+
});
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
return null;
|
|
1727
|
+
}
|
|
1728
|
+
return writes;
|
|
1729
|
+
}
|
|
1730
|
+
function normalizeProjectNodeChildWorkItems(value) {
|
|
1731
|
+
if (!Array.isArray(value)) {
|
|
1732
|
+
return void 0;
|
|
1733
|
+
}
|
|
1734
|
+
const childWorkItems = value.flatMap((item) => {
|
|
1735
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
1736
|
+
return [];
|
|
1737
|
+
}
|
|
1738
|
+
const record = item;
|
|
1739
|
+
const stableKey = normalizeSummary(record.stableKey);
|
|
1740
|
+
const name = normalizeSummary(record.name);
|
|
1741
|
+
const description = normalizeSummary(record.description) ?? void 0;
|
|
1742
|
+
if (!stableKey || !name) {
|
|
1743
|
+
return [];
|
|
1744
|
+
}
|
|
1745
|
+
return [{
|
|
1746
|
+
stableKey,
|
|
1747
|
+
name,
|
|
1748
|
+
...description ? { description } : {}
|
|
1749
|
+
}];
|
|
1750
|
+
});
|
|
1751
|
+
return childWorkItems;
|
|
1752
|
+
}
|
|
1753
|
+
function normalizeProjectNodeArtifacts(value) {
|
|
1754
|
+
if (value === void 0 || value === null) {
|
|
1755
|
+
return void 0;
|
|
1756
|
+
}
|
|
1757
|
+
if (!Array.isArray(value)) {
|
|
1758
|
+
return null;
|
|
1759
|
+
}
|
|
1760
|
+
const artifacts = [];
|
|
1761
|
+
for (const item of value) {
|
|
1762
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
1763
|
+
return null;
|
|
1764
|
+
}
|
|
1765
|
+
const record = item;
|
|
1766
|
+
const artifactId = normalizeSummary(record.artifactId);
|
|
1767
|
+
const fileName = normalizeSummary(record.fileName);
|
|
1768
|
+
if (!artifactId || !fileName) {
|
|
1769
|
+
return null;
|
|
1770
|
+
}
|
|
1771
|
+
if ("mimeType" in record && typeof record.mimeType !== "string") {
|
|
1772
|
+
return null;
|
|
1773
|
+
}
|
|
1774
|
+
artifacts.push({
|
|
1775
|
+
artifactId,
|
|
1776
|
+
fileName,
|
|
1777
|
+
...typeof record.mimeType === "string" ? { mimeType: record.mimeType } : {}
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
return artifacts;
|
|
1781
|
+
}
|
|
1782
|
+
function normalizeProjectNodeReasonType(value) {
|
|
1783
|
+
return value === "guidance_missing" || value === "guidance_conflict" || value === "project_context_missing" || value === "project_context_conflict" || value === "business_fact_missing" || value === "permission_missing" || value === "external_dependency" || value === "unknown" ? value : null;
|
|
1784
|
+
}
|
|
1785
|
+
function readOptionalStringMetadata(metadata, key) {
|
|
1786
|
+
if (!metadata || !(key in metadata) || metadata[key] == null) {
|
|
1787
|
+
return void 0;
|
|
1788
|
+
}
|
|
1789
|
+
const value = metadata[key];
|
|
1790
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1791
|
+
throw new Error(`${key} must be a non-empty string`);
|
|
1792
|
+
}
|
|
1793
|
+
return value.trim();
|
|
1794
|
+
}
|
|
1795
|
+
function readOptionalBooleanMetadata(metadata, key) {
|
|
1796
|
+
if (!metadata || !(key in metadata) || metadata[key] == null) {
|
|
1797
|
+
return void 0;
|
|
1798
|
+
}
|
|
1799
|
+
const value = metadata[key];
|
|
1800
|
+
if (typeof value !== "boolean") {
|
|
1801
|
+
throw new TypeError(`${key} must be a boolean`);
|
|
1802
|
+
}
|
|
1803
|
+
return value;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
export {
|
|
1807
|
+
OPENCODE_WORKER_ROLE,
|
|
1808
|
+
OPENCODE_WORKER_WORKING_TEXT,
|
|
1809
|
+
OPENCODE_WORKER_CONFIG_DIR,
|
|
1810
|
+
resolveOpenCodeWorkerStorageDir,
|
|
1811
|
+
resolveOpenCodeWorkerConfigFile,
|
|
1812
|
+
resolveOpenCodeWorkerStateFile,
|
|
1813
|
+
loadOpenCodeWorkerConfig,
|
|
1814
|
+
writeOpenCodeWorkerConfig,
|
|
1815
|
+
loadOpenCodeWorkerState,
|
|
1816
|
+
writeOpenCodeWorkerState,
|
|
1817
|
+
runOpenCodeWorkerSetup,
|
|
1818
|
+
listOpenCodeWorkerConfigs,
|
|
1819
|
+
getOpenCodeWorkerDoctorReport,
|
|
1820
|
+
setOpenCodeWorkerEnabled,
|
|
1821
|
+
uninstallOpenCodeWorker,
|
|
1822
|
+
createClackPrompter,
|
|
1823
|
+
stringifyTaskMessageParts,
|
|
1824
|
+
OpenCodeCliTaskExecutor,
|
|
1825
|
+
assertOpenCodeCliAvailable,
|
|
1826
|
+
assertOpenCodeCliRuntimeContract,
|
|
1827
|
+
buildOpenCodeArgs,
|
|
1828
|
+
shouldSkipPermissionsForPreset,
|
|
1829
|
+
createAbortError,
|
|
1830
|
+
isAbortError,
|
|
1831
|
+
OpenCodeWorkerBridge
|
|
1832
|
+
};
|
|
1833
|
+
//# sourceMappingURL=chunk-4MUQ7X6C.js.map
|