orchestrar 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 +40 -0
- package/bin/orchestrar.js +7 -0
- package/orchestrator.js +474 -0
- package/package.json +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# orchestrar
|
|
2
|
+
|
|
3
|
+
Orchestrates OpenCode milestones based on PRD.md, SPEC.md, and PLAN.md.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 18+
|
|
8
|
+
- An OpenCode-compatible config with a `review-uncommited` command
|
|
9
|
+
- A repo containing PRD.md, SPEC.md, and PLAN.md in the root or `docs/`
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
npm install -g orchestrar
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Run from the repo root:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
orchestrar
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Behavior
|
|
26
|
+
|
|
27
|
+
- Uses `github-copilot/gpt-5.2-codex` for work and review instances.
|
|
28
|
+
- Uses `github-copilot/gpt-5-mini` for the commit/push instance.
|
|
29
|
+
- Loops until all unchecked tasks in PLAN.md are marked.
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
Optional environment variables:
|
|
34
|
+
|
|
35
|
+
- `ORCHESTRATOR_REVIEW_COMMAND` (default: `review-uncommited`)
|
|
36
|
+
- `ORCHESTRATOR_REVIEW_ARGUMENTS` (default: empty)
|
|
37
|
+
- `ORCHESTRATOR_REVIEW_TIMEOUT_MS` (default: 3600000)
|
|
38
|
+
- `ORCHESTRATOR_SESSION_TIMEOUT_MS` (default: 7200000)
|
|
39
|
+
- `ORCHESTRATOR_STATUS_POLL_INTERVAL_MS` (default: 2000)
|
|
40
|
+
- `ORCHESTRATOR_MAX_REVIEW_ITERATIONS` (default: 20)
|
package/orchestrator.js
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs/promises");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const DEFAULT_MODEL = "github-copilot/gpt-5.2-codex";
|
|
6
|
+
const COMMIT_MODEL = "github-copilot/gpt-5-mini";
|
|
7
|
+
const DEFAULT_REVIEW_COMMAND_NAME = "review-uncommited";
|
|
8
|
+
const DEFAULT_REVIEW_COMMAND_ARGUMENTS = "";
|
|
9
|
+
const DEFAULT_REVIEW_TIMEOUT_MS = 60 * 60 * 1000;
|
|
10
|
+
const DEFAULT_SESSION_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
11
|
+
const DEFAULT_STATUS_POLL_INTERVAL_MS = 2000;
|
|
12
|
+
const DEFAULT_MAX_REVIEW_ITERATIONS = 20;
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
const root = process.cwd();
|
|
16
|
+
const docs = await resolveDocs(root);
|
|
17
|
+
const planPath = docs.plan;
|
|
18
|
+
const { createOpencode } = await import("@opencode-ai/sdk");
|
|
19
|
+
|
|
20
|
+
let milestoneIndex = 0;
|
|
21
|
+
while (await hasUnfinishedTasks(planPath)) {
|
|
22
|
+
milestoneIndex += 1;
|
|
23
|
+
logStep(`Starting milestone ${milestoneIndex}`);
|
|
24
|
+
await runMilestoneCycle(createOpencode, docs, root);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
logStep("All milestones completed.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function runMilestoneCycle(createOpencode, docs, root) {
|
|
31
|
+
await runWorkInstance(createOpencode, docs, root);
|
|
32
|
+
await runCommitInstance(createOpencode, root);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function runWorkInstance(createOpencode, docs, root) {
|
|
36
|
+
const { client, server } = await createOpencode({
|
|
37
|
+
config: buildConfig(DEFAULT_MODEL),
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
const session = await unwrap(
|
|
41
|
+
client.session.create({
|
|
42
|
+
query: { directory: root },
|
|
43
|
+
body: { title: "Milestone Orchestrator" },
|
|
44
|
+
}),
|
|
45
|
+
"session.create"
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const sessionID = session.id;
|
|
49
|
+
const promptPaths = buildPromptPaths(docs, root);
|
|
50
|
+
|
|
51
|
+
await sendPrompt(
|
|
52
|
+
client,
|
|
53
|
+
sessionID,
|
|
54
|
+
buildMilestonePrompt(promptPaths),
|
|
55
|
+
root
|
|
56
|
+
);
|
|
57
|
+
await waitForSessionIdle(client, sessionID, root);
|
|
58
|
+
|
|
59
|
+
await runReviewLoop(createOpencode, client, sessionID, root);
|
|
60
|
+
|
|
61
|
+
await sendPrompt(
|
|
62
|
+
client,
|
|
63
|
+
sessionID,
|
|
64
|
+
buildMarkTasksPrompt(promptPaths.plan),
|
|
65
|
+
root
|
|
66
|
+
);
|
|
67
|
+
await waitForSessionIdle(client, sessionID, root);
|
|
68
|
+
} finally {
|
|
69
|
+
await disposeInstance(client, server, root);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function runCommitInstance(createOpencode, root) {
|
|
74
|
+
const { client, server } = await createOpencode({
|
|
75
|
+
config: buildConfig(COMMIT_MODEL),
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
const session = await unwrap(
|
|
79
|
+
client.session.create({
|
|
80
|
+
query: { directory: root },
|
|
81
|
+
body: { title: "Commit & Push" },
|
|
82
|
+
}),
|
|
83
|
+
"session.create"
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const sessionID = session.id;
|
|
87
|
+
await sendPrompt(
|
|
88
|
+
client,
|
|
89
|
+
sessionID,
|
|
90
|
+
buildCommitPrompt(),
|
|
91
|
+
root,
|
|
92
|
+
COMMIT_MODEL
|
|
93
|
+
);
|
|
94
|
+
await waitForSessionIdle(client, sessionID, root);
|
|
95
|
+
} finally {
|
|
96
|
+
await disposeInstance(client, server, root);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function runReviewLoop(createOpencode, client, sessionID, root) {
|
|
101
|
+
const maxIterations = parseNumber(
|
|
102
|
+
process.env.ORCHESTRATOR_MAX_REVIEW_ITERATIONS,
|
|
103
|
+
DEFAULT_MAX_REVIEW_ITERATIONS
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
|
|
107
|
+
logStep(`Running review (${iteration}/${maxIterations})`);
|
|
108
|
+
const reviewResult = await runReviewCommand(createOpencode, root);
|
|
109
|
+
if (isFindingsEmpty(reviewResult)) {
|
|
110
|
+
logStep("Review clean; no findings.");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const findingsCount = Array.isArray(reviewResult.findings)
|
|
115
|
+
? reviewResult.findings.length
|
|
116
|
+
: "unknown";
|
|
117
|
+
logStep(`Review found ${findingsCount} issues; requesting fixes.`);
|
|
118
|
+
await sendPrompt(
|
|
119
|
+
client,
|
|
120
|
+
sessionID,
|
|
121
|
+
buildFindingsPrompt(reviewResult),
|
|
122
|
+
root
|
|
123
|
+
);
|
|
124
|
+
await waitForSessionIdle(client, sessionID, root);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Review loop exceeded ${maxIterations} iterations without clean results.`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function runReviewCommand(createOpencode, root) {
|
|
133
|
+
const commandName =
|
|
134
|
+
process.env.ORCHESTRATOR_REVIEW_COMMAND || DEFAULT_REVIEW_COMMAND_NAME;
|
|
135
|
+
const commandArguments =
|
|
136
|
+
process.env.ORCHESTRATOR_REVIEW_ARGUMENTS ||
|
|
137
|
+
DEFAULT_REVIEW_COMMAND_ARGUMENTS;
|
|
138
|
+
const timeoutMs = parseNumber(
|
|
139
|
+
process.env.ORCHESTRATOR_REVIEW_TIMEOUT_MS,
|
|
140
|
+
DEFAULT_REVIEW_TIMEOUT_MS
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const { client, server } = await createOpencode({
|
|
144
|
+
config: buildConfig(DEFAULT_MODEL),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const session = await unwrap(
|
|
149
|
+
client.session.create({
|
|
150
|
+
query: { directory: root },
|
|
151
|
+
body: { title: "Review" },
|
|
152
|
+
}),
|
|
153
|
+
"session.create"
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const sessionID = session.id;
|
|
157
|
+
const commandResult = await unwrap(
|
|
158
|
+
client.session.command({
|
|
159
|
+
path: { id: sessionID },
|
|
160
|
+
query: { directory: root },
|
|
161
|
+
body: {
|
|
162
|
+
command: commandName,
|
|
163
|
+
arguments: commandArguments,
|
|
164
|
+
model: DEFAULT_MODEL,
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
"session.command"
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
await waitForSessionIdle(client, sessionID, root, timeoutMs);
|
|
171
|
+
|
|
172
|
+
let parts = commandResult?.parts ?? [];
|
|
173
|
+
const messageID = commandResult?.info?.id;
|
|
174
|
+
if (messageID) {
|
|
175
|
+
const message = await unwrap(
|
|
176
|
+
client.session.message({
|
|
177
|
+
path: { id: sessionID, messageID },
|
|
178
|
+
query: { directory: root },
|
|
179
|
+
}),
|
|
180
|
+
"session.message"
|
|
181
|
+
);
|
|
182
|
+
parts = message?.parts ?? parts;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const output = collectCommandOutput(parts);
|
|
186
|
+
if (!output.trim()) {
|
|
187
|
+
throw new Error("Review command produced no output.");
|
|
188
|
+
}
|
|
189
|
+
return extractReviewJson(output);
|
|
190
|
+
} finally {
|
|
191
|
+
await disposeInstance(client, server, root);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function sendPrompt(
|
|
196
|
+
client,
|
|
197
|
+
sessionID,
|
|
198
|
+
text,
|
|
199
|
+
root,
|
|
200
|
+
modelSpec = DEFAULT_MODEL
|
|
201
|
+
) {
|
|
202
|
+
const model = parseModelSpec(modelSpec);
|
|
203
|
+
await unwrap(
|
|
204
|
+
client.session.prompt({
|
|
205
|
+
path: { id: sessionID },
|
|
206
|
+
query: { directory: root },
|
|
207
|
+
body: {
|
|
208
|
+
model,
|
|
209
|
+
parts: [{ type: "text", text }],
|
|
210
|
+
},
|
|
211
|
+
}),
|
|
212
|
+
"session.prompt"
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function waitForSessionIdle(client, sessionID, root, timeoutOverrideMs) {
|
|
217
|
+
const timeoutMs =
|
|
218
|
+
timeoutOverrideMs ??
|
|
219
|
+
parseNumber(
|
|
220
|
+
process.env.ORCHESTRATOR_SESSION_TIMEOUT_MS,
|
|
221
|
+
DEFAULT_SESSION_TIMEOUT_MS
|
|
222
|
+
);
|
|
223
|
+
const pollIntervalMs = parseNumber(
|
|
224
|
+
process.env.ORCHESTRATOR_STATUS_POLL_INTERVAL_MS,
|
|
225
|
+
DEFAULT_STATUS_POLL_INTERVAL_MS
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const start = Date.now();
|
|
229
|
+
while (Date.now() - start < timeoutMs) {
|
|
230
|
+
const statusMap = await unwrap(
|
|
231
|
+
client.session.status({ query: { directory: root } }),
|
|
232
|
+
"session.status"
|
|
233
|
+
);
|
|
234
|
+
const status = statusMap?.[sessionID];
|
|
235
|
+
if (!status) {
|
|
236
|
+
throw new Error(`Session status missing for ${sessionID}.`);
|
|
237
|
+
}
|
|
238
|
+
if (status.type === "idle") {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
await delay(pollIntervalMs);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
throw new Error(`Timed out waiting for session ${sessionID} to go idle.`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function resolveDocs(root) {
|
|
248
|
+
const prd = await resolveDocPath(root, "PRD.md");
|
|
249
|
+
const spec = await resolveDocPath(root, "SPEC.md");
|
|
250
|
+
const plan = await resolveDocPath(root, "PLAN.md");
|
|
251
|
+
return { prd, spec, plan };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function resolveDocPath(root, fileName) {
|
|
255
|
+
const rootPath = path.join(root, fileName);
|
|
256
|
+
if (await fileExists(rootPath)) {
|
|
257
|
+
return rootPath;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const docsPath = path.join(root, "docs", fileName);
|
|
261
|
+
if (await fileExists(docsPath)) {
|
|
262
|
+
return docsPath;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Required file not found: ${fileName} (checked ${rootPath} and ${docsPath}).`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function buildPromptPaths(docs, root) {
|
|
271
|
+
return {
|
|
272
|
+
prd: path.relative(root, docs.prd) || "PRD.md",
|
|
273
|
+
spec: path.relative(root, docs.spec) || "SPEC.md",
|
|
274
|
+
plan: path.relative(root, docs.plan) || "PLAN.md",
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function buildMilestonePrompt(paths) {
|
|
279
|
+
return [
|
|
280
|
+
"Read the product documents and implement the next unchecked milestone.",
|
|
281
|
+
"",
|
|
282
|
+
"Docs:",
|
|
283
|
+
`- PRD: ${paths.prd}`,
|
|
284
|
+
`- SPEC: ${paths.spec}`,
|
|
285
|
+
`- PLAN: ${paths.plan}`,
|
|
286
|
+
"",
|
|
287
|
+
"Requirements:",
|
|
288
|
+
"- Implement one unchecked milestone and its tasks from PLAN.md.",
|
|
289
|
+
"- Do not update PLAN.md checkboxes yet.",
|
|
290
|
+
"- Do not commit or push.",
|
|
291
|
+
"- When finished, briefly confirm completion.",
|
|
292
|
+
].join("\n");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function buildFindingsPrompt(reviewJson) {
|
|
296
|
+
const jsonBlock = JSON.stringify(reviewJson, null, 2);
|
|
297
|
+
return [
|
|
298
|
+
"The review command reported issues. Address the findings below.",
|
|
299
|
+
"",
|
|
300
|
+
jsonBlock,
|
|
301
|
+
"",
|
|
302
|
+
"After fixing the issues, wait for the next instruction.",
|
|
303
|
+
].join("\n");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function buildMarkTasksPrompt(planPath) {
|
|
307
|
+
return [
|
|
308
|
+
`Update ${planPath} by marking completed tasks with [x].`,
|
|
309
|
+
"Leave incomplete tasks unchecked.",
|
|
310
|
+
"Do not commit or push yet.",
|
|
311
|
+
"When done, confirm the PLAN.md updates.",
|
|
312
|
+
].join("\n");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function buildCommitPrompt() {
|
|
316
|
+
return [
|
|
317
|
+
"Commit the changes with an appropriate commit message and git push.",
|
|
318
|
+
"After pushing, confirm completion.",
|
|
319
|
+
].join("\n");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function hasUnfinishedTasks(planPath) {
|
|
323
|
+
const content = await fs.readFile(planPath, "utf8");
|
|
324
|
+
return /^\s*[-*+]\s+\[\s\]/m.test(content);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function isFindingsEmpty(reviewJson) {
|
|
328
|
+
if (!reviewJson || typeof reviewJson !== "object") {
|
|
329
|
+
throw new Error("Review JSON was not an object.");
|
|
330
|
+
}
|
|
331
|
+
if (!Array.isArray(reviewJson.findings)) {
|
|
332
|
+
throw new Error("Review JSON is missing a findings array.");
|
|
333
|
+
}
|
|
334
|
+
return reviewJson.findings.length === 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function extractReviewJson(output) {
|
|
338
|
+
const trimmed = output.trim();
|
|
339
|
+
if (!trimmed) {
|
|
340
|
+
throw new Error("Review command produced no output.");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let index = trimmed.lastIndexOf("{");
|
|
344
|
+
while (index !== -1) {
|
|
345
|
+
const slice = trimmed.slice(index).trim();
|
|
346
|
+
try {
|
|
347
|
+
const parsed = JSON.parse(slice);
|
|
348
|
+
if (parsed && typeof parsed === "object" && "findings" in parsed) {
|
|
349
|
+
return parsed;
|
|
350
|
+
}
|
|
351
|
+
} catch (error) {
|
|
352
|
+
// Ignore parse failures and continue scanning earlier braces.
|
|
353
|
+
}
|
|
354
|
+
index = trimmed.lastIndexOf("{", index - 1);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
throw new Error("Failed to parse review JSON from command output.");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function collectCommandOutput(parts) {
|
|
361
|
+
if (!Array.isArray(parts)) {
|
|
362
|
+
return "";
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const chunks = [];
|
|
366
|
+
for (const part of parts) {
|
|
367
|
+
if (!part || typeof part !== "object") {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
371
|
+
chunks.push(part.text);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (part.type === "tool" && part.state && typeof part.state === "object") {
|
|
375
|
+
if (part.state.status === "completed" && typeof part.state.output === "string") {
|
|
376
|
+
chunks.push(part.state.output);
|
|
377
|
+
} else if (part.state.status === "error" && typeof part.state.error === "string") {
|
|
378
|
+
chunks.push(part.state.error);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return chunks.join("\n");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function parseNumber(value, fallback) {
|
|
387
|
+
if (!value) {
|
|
388
|
+
return fallback;
|
|
389
|
+
}
|
|
390
|
+
const parsed = Number.parseInt(value, 10);
|
|
391
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function buildConfig(model) {
|
|
395
|
+
return {
|
|
396
|
+
model,
|
|
397
|
+
permission: {
|
|
398
|
+
edit: "allow",
|
|
399
|
+
bash: "allow",
|
|
400
|
+
webfetch: "allow",
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function parseModelSpec(spec) {
|
|
406
|
+
const separatorIndex = spec.indexOf("/");
|
|
407
|
+
if (separatorIndex === -1) {
|
|
408
|
+
throw new Error(`Invalid model spec: ${spec}`);
|
|
409
|
+
}
|
|
410
|
+
const providerID = spec.slice(0, separatorIndex).trim();
|
|
411
|
+
const modelID = spec.slice(separatorIndex + 1).trim();
|
|
412
|
+
if (!providerID || !modelID) {
|
|
413
|
+
throw new Error(`Invalid model spec: ${spec}`);
|
|
414
|
+
}
|
|
415
|
+
return { providerID, modelID };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function disposeInstance(client, server, root) {
|
|
419
|
+
try {
|
|
420
|
+
await unwrap(
|
|
421
|
+
client.instance.dispose({ query: { directory: root } }),
|
|
422
|
+
"instance.dispose"
|
|
423
|
+
);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
logStep(`Instance dispose failed: ${formatError(error)}`);
|
|
426
|
+
}
|
|
427
|
+
server.close();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function fileExists(filePath) {
|
|
431
|
+
try {
|
|
432
|
+
const stat = await fs.stat(filePath);
|
|
433
|
+
return stat.isFile();
|
|
434
|
+
} catch (error) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function unwrap(result, label) {
|
|
440
|
+
if (result && typeof result === "object" && "data" in result) {
|
|
441
|
+
if (result.error) {
|
|
442
|
+
throw new Error(`${label} failed: ${formatError(result.error)}`);
|
|
443
|
+
}
|
|
444
|
+
return result.data;
|
|
445
|
+
}
|
|
446
|
+
return result;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function formatError(error) {
|
|
450
|
+
if (!error) {
|
|
451
|
+
return "Unknown error";
|
|
452
|
+
}
|
|
453
|
+
if (error instanceof Error) {
|
|
454
|
+
return error.message;
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
return JSON.stringify(error);
|
|
458
|
+
} catch (stringifyError) {
|
|
459
|
+
return String(error);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function delay(ms) {
|
|
464
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function logStep(message) {
|
|
468
|
+
console.log(`[orchestrator] ${message}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
main().catch((error) => {
|
|
472
|
+
console.error(`[orchestrator] Failed: ${formatError(error)}`);
|
|
473
|
+
process.exitCode = 1;
|
|
474
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "orchestrar",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode milestone orchestrator",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"orchestrar": "bin/orchestrar.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"orchestrator.js",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@opencode-ai/sdk": "^1.1.44"
|
|
17
|
+
}
|
|
18
|
+
}
|