orchestrar 0.3.4 → 0.3.5
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/orchestrator.js +118 -27
- package/package.json +1 -1
package/orchestrator.js
CHANGED
|
@@ -12,10 +12,19 @@ const DEFAULT_REVIEW_TIMEOUT_MS = 60 * 60 * 1000;
|
|
|
12
12
|
const DEFAULT_SESSION_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
13
13
|
const DEFAULT_STATUS_POLL_INTERVAL_MS = 2000;
|
|
14
14
|
const DEFAULT_MAX_REVIEW_ITERATIONS = 20;
|
|
15
|
+
const DEFAULT_DEBUG_ENABLED = true;
|
|
16
|
+
const DEBUG_ENABLED = parseBoolean(
|
|
17
|
+
process.env.ORCHESTRATOR_DEBUG,
|
|
18
|
+
DEFAULT_DEBUG_ENABLED
|
|
19
|
+
);
|
|
15
20
|
|
|
16
21
|
async function main() {
|
|
17
22
|
const root = process.cwd();
|
|
23
|
+
logDebug(`Starting orchestrator in ${root}`);
|
|
18
24
|
const docs = await resolveDocs(root);
|
|
25
|
+
logDebug(
|
|
26
|
+
`Docs resolved: prd=${docs.prd}, spec=${docs.spec}, plan=${docs.plan}`
|
|
27
|
+
);
|
|
19
28
|
const planPath = docs.plan;
|
|
20
29
|
const { createOpencode } = await import("@opencode-ai/sdk");
|
|
21
30
|
|
|
@@ -38,13 +47,15 @@ async function runWorkInstance(createOpencode, docs, root) {
|
|
|
38
47
|
const { client, server } = await createOpencode({
|
|
39
48
|
config: buildConfig(DEFAULT_MODEL),
|
|
40
49
|
});
|
|
50
|
+
logDebug(`OpenCode server started at ${server.url}`);
|
|
41
51
|
try {
|
|
42
|
-
|
|
52
|
+
logDebug("Creating milestone session");
|
|
53
|
+
const session = await callClient(
|
|
54
|
+
"session.create",
|
|
43
55
|
client.session.create({
|
|
44
56
|
query: { directory: root },
|
|
45
57
|
body: { title: "Milestone Orchestrator" },
|
|
46
|
-
})
|
|
47
|
-
"session.create"
|
|
58
|
+
})
|
|
48
59
|
);
|
|
49
60
|
|
|
50
61
|
const sessionID = extractSessionID(session, "session.create (work instance)");
|
|
@@ -76,13 +87,15 @@ async function runCommitInstance(createOpencode, root) {
|
|
|
76
87
|
const { client, server } = await createOpencode({
|
|
77
88
|
config: buildConfig(COMMIT_MODEL),
|
|
78
89
|
});
|
|
90
|
+
logDebug(`OpenCode server started at ${server.url}`);
|
|
79
91
|
try {
|
|
80
|
-
|
|
92
|
+
logDebug("Creating commit session");
|
|
93
|
+
const session = await callClient(
|
|
94
|
+
"session.create",
|
|
81
95
|
client.session.create({
|
|
82
96
|
query: { directory: root },
|
|
83
97
|
body: { title: "Commit & Push" },
|
|
84
|
-
})
|
|
85
|
-
"session.create"
|
|
98
|
+
})
|
|
86
99
|
);
|
|
87
100
|
|
|
88
101
|
const sessionID = extractSessionID(session, "session.create (commit instance)");
|
|
@@ -105,6 +118,11 @@ async function runReviewLoop(client, sessionID, root) {
|
|
|
105
118
|
process.env.ORCHESTRATOR_MAX_REVIEW_ITERATIONS,
|
|
106
119
|
DEFAULT_MAX_REVIEW_ITERATIONS
|
|
107
120
|
);
|
|
121
|
+
logDebug(
|
|
122
|
+
`Review loop configured: maxIterations=${maxIterations}, command=${
|
|
123
|
+
process.env.ORCHESTRATOR_REVIEW_COMMAND || DEFAULT_REVIEW_COMMAND_NAME
|
|
124
|
+
}`
|
|
125
|
+
);
|
|
108
126
|
|
|
109
127
|
for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
|
|
110
128
|
logStep(`Running review (${iteration}/${maxIterations})`);
|
|
@@ -142,16 +160,21 @@ async function runReviewCommand(client, root) {
|
|
|
142
160
|
process.env.ORCHESTRATOR_REVIEW_TIMEOUT_MS,
|
|
143
161
|
DEFAULT_REVIEW_TIMEOUT_MS
|
|
144
162
|
);
|
|
145
|
-
|
|
163
|
+
logDebug(
|
|
164
|
+
`Starting review command: name=${commandName}, args=${commandArguments ||
|
|
165
|
+
"<none>"}, timeoutMs=${timeoutMs}`
|
|
166
|
+
);
|
|
167
|
+
const session = await callClient(
|
|
168
|
+
"session.create",
|
|
146
169
|
client.session.create({
|
|
147
170
|
query: { directory: root },
|
|
148
171
|
body: { title: "Review" },
|
|
149
|
-
})
|
|
150
|
-
"session.create"
|
|
172
|
+
})
|
|
151
173
|
);
|
|
152
174
|
|
|
153
175
|
const sessionID = extractSessionID(session, "session.create (review instance)");
|
|
154
|
-
const commandResult = await
|
|
176
|
+
const commandResult = await callClient(
|
|
177
|
+
"session.command",
|
|
155
178
|
client.session.command({
|
|
156
179
|
path: { id: sessionID },
|
|
157
180
|
query: { directory: root },
|
|
@@ -161,8 +184,7 @@ async function runReviewCommand(client, root) {
|
|
|
161
184
|
agent: DEFAULT_AGENT,
|
|
162
185
|
model: DEFAULT_MODEL,
|
|
163
186
|
},
|
|
164
|
-
})
|
|
165
|
-
"session.command"
|
|
187
|
+
})
|
|
166
188
|
);
|
|
167
189
|
|
|
168
190
|
const commandMessageID = extractMessageID(commandResult);
|
|
@@ -177,17 +199,18 @@ async function runReviewCommand(client, root) {
|
|
|
177
199
|
let parts = commandResult?.parts ?? [];
|
|
178
200
|
const messageID = extractMessageID(commandResult);
|
|
179
201
|
if (messageID) {
|
|
180
|
-
const message = await
|
|
202
|
+
const message = await callClient(
|
|
203
|
+
"session.message",
|
|
181
204
|
client.session.message({
|
|
182
205
|
path: { id: sessionID, messageID },
|
|
183
206
|
query: { directory: root },
|
|
184
|
-
})
|
|
185
|
-
"session.message"
|
|
207
|
+
})
|
|
186
208
|
);
|
|
187
209
|
parts = message?.parts ?? parts;
|
|
188
210
|
}
|
|
189
211
|
|
|
190
212
|
const output = collectCommandOutput(parts);
|
|
213
|
+
logDebug(`Review command output length: ${output.length}`);
|
|
191
214
|
if (!output.trim()) {
|
|
192
215
|
throw new Error("Review command produced no output.");
|
|
193
216
|
}
|
|
@@ -203,7 +226,11 @@ async function sendPrompt(
|
|
|
203
226
|
agentSpec = DEFAULT_AGENT
|
|
204
227
|
) {
|
|
205
228
|
const model = parseModelSpec(modelSpec);
|
|
206
|
-
|
|
229
|
+
logDebug(
|
|
230
|
+
`Sending prompt to session ${sessionID}: ${summarizeText(text, 160)}`
|
|
231
|
+
);
|
|
232
|
+
const response = await callClient(
|
|
233
|
+
"session.prompt",
|
|
207
234
|
client.session.prompt({
|
|
208
235
|
path: { id: sessionID },
|
|
209
236
|
query: { directory: root },
|
|
@@ -212,8 +239,7 @@ async function sendPrompt(
|
|
|
212
239
|
model,
|
|
213
240
|
parts: [{ type: "text", text }],
|
|
214
241
|
},
|
|
215
|
-
})
|
|
216
|
-
"session.prompt"
|
|
242
|
+
})
|
|
217
243
|
);
|
|
218
244
|
|
|
219
245
|
return extractMessageID(response);
|
|
@@ -230,18 +256,28 @@ async function waitForSessionIdle(client, sessionID, root, timeoutOverrideMs) {
|
|
|
230
256
|
process.env.ORCHESTRATOR_STATUS_POLL_INTERVAL_MS,
|
|
231
257
|
DEFAULT_STATUS_POLL_INTERVAL_MS
|
|
232
258
|
);
|
|
259
|
+
logDebug(
|
|
260
|
+
`Waiting for session ${sessionID} idle (timeoutMs=${timeoutMs}, pollMs=${pollIntervalMs})`
|
|
261
|
+
);
|
|
233
262
|
|
|
234
263
|
const start = Date.now();
|
|
235
264
|
let lastKnownSessions = [];
|
|
265
|
+
let attempt = 0;
|
|
236
266
|
while (Date.now() - start < timeoutMs) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
"session.status"
|
|
267
|
+
attempt += 1;
|
|
268
|
+
const statusMap = await callClient(
|
|
269
|
+
"session.status",
|
|
270
|
+
client.session.status({ query: { directory: root } })
|
|
240
271
|
);
|
|
241
272
|
if (statusMap && typeof statusMap === "object") {
|
|
242
273
|
lastKnownSessions = Object.keys(statusMap);
|
|
243
274
|
}
|
|
244
275
|
const status = statusMap?.[sessionID];
|
|
276
|
+
logDebug(
|
|
277
|
+
`Session status poll ${attempt}: session=${sessionID}, status=${
|
|
278
|
+
status?.type || "<missing>"
|
|
279
|
+
}, known=${lastKnownSessions.length}`
|
|
280
|
+
);
|
|
245
281
|
if (!status) {
|
|
246
282
|
await delay(pollIntervalMs);
|
|
247
283
|
continue;
|
|
@@ -282,17 +318,27 @@ async function waitForMessageComplete(
|
|
|
282
318
|
process.env.ORCHESTRATOR_STATUS_POLL_INTERVAL_MS,
|
|
283
319
|
DEFAULT_STATUS_POLL_INTERVAL_MS
|
|
284
320
|
);
|
|
321
|
+
logDebug(
|
|
322
|
+
`Waiting for message ${messageID} (timeoutMs=${timeoutMs}, pollMs=${pollIntervalMs})`
|
|
323
|
+
);
|
|
285
324
|
|
|
286
325
|
const start = Date.now();
|
|
326
|
+
let attempt = 0;
|
|
287
327
|
while (Date.now() - start < timeoutMs) {
|
|
288
|
-
|
|
328
|
+
attempt += 1;
|
|
329
|
+
const message = await callClient(
|
|
330
|
+
"session.message",
|
|
289
331
|
client.session.message({
|
|
290
332
|
path: { id: sessionID, messageID },
|
|
291
333
|
query: { directory: root },
|
|
292
|
-
})
|
|
293
|
-
"session.message"
|
|
334
|
+
})
|
|
294
335
|
);
|
|
295
336
|
const info = message?.info ?? message;
|
|
337
|
+
logDebug(
|
|
338
|
+
`Message poll ${attempt}: message=${messageID}, completed=${
|
|
339
|
+
info?.time?.completed ? "yes" : "no"
|
|
340
|
+
}, finish=${info?.finish || "<none>"}`
|
|
341
|
+
);
|
|
296
342
|
if (info?.error) {
|
|
297
343
|
throw new Error(
|
|
298
344
|
`Session message ${messageID} failed: ${formatError(info.error)}`
|
|
@@ -454,6 +500,20 @@ function parseNumber(value, fallback) {
|
|
|
454
500
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
455
501
|
}
|
|
456
502
|
|
|
503
|
+
function parseBoolean(value, fallback) {
|
|
504
|
+
if (value === undefined || value === null || value === "") {
|
|
505
|
+
return fallback;
|
|
506
|
+
}
|
|
507
|
+
const normalized = String(value).trim().toLowerCase();
|
|
508
|
+
if (["1", "true", "yes", "on"].includes(normalized)) {
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
if (["0", "false", "no", "off"].includes(normalized)) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
return fallback;
|
|
515
|
+
}
|
|
516
|
+
|
|
457
517
|
function buildConfig(model) {
|
|
458
518
|
return {
|
|
459
519
|
model,
|
|
@@ -480,9 +540,9 @@ function parseModelSpec(spec) {
|
|
|
480
540
|
|
|
481
541
|
async function disposeInstance(client, server, root) {
|
|
482
542
|
try {
|
|
483
|
-
await
|
|
484
|
-
|
|
485
|
-
|
|
543
|
+
await callClient(
|
|
544
|
+
"instance.dispose",
|
|
545
|
+
client.instance.dispose({ query: { directory: root } })
|
|
486
546
|
);
|
|
487
547
|
} catch (error) {
|
|
488
548
|
logStep(`Instance dispose failed: ${formatError(error)}`);
|
|
@@ -527,6 +587,19 @@ async function unwrap(result, label) {
|
|
|
527
587
|
return result;
|
|
528
588
|
}
|
|
529
589
|
|
|
590
|
+
async function callClient(label, promise) {
|
|
591
|
+
const start = Date.now();
|
|
592
|
+
logDebug(`${label} -> start`);
|
|
593
|
+
try {
|
|
594
|
+
const data = await unwrap(promise, label);
|
|
595
|
+
logDebug(`${label} -> ok (${Date.now() - start}ms)`);
|
|
596
|
+
return data;
|
|
597
|
+
} catch (error) {
|
|
598
|
+
logDebug(`${label} -> error (${Date.now() - start}ms): ${formatError(error)}`);
|
|
599
|
+
throw error;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
530
603
|
function extractSessionID(session, context) {
|
|
531
604
|
if (!session || typeof session !== "object") {
|
|
532
605
|
throw new Error(`${context} did not return a session object.`);
|
|
@@ -620,6 +693,24 @@ function logStep(message) {
|
|
|
620
693
|
console.log(`[orchestrator] ${message}`);
|
|
621
694
|
}
|
|
622
695
|
|
|
696
|
+
function logDebug(message) {
|
|
697
|
+
if (!DEBUG_ENABLED) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
console.log(`[orchestrator][debug] ${message}`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function summarizeText(text, maxLength) {
|
|
704
|
+
if (typeof text !== "string") {
|
|
705
|
+
return "<non-text>";
|
|
706
|
+
}
|
|
707
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
708
|
+
if (trimmed.length <= maxLength) {
|
|
709
|
+
return trimmed;
|
|
710
|
+
}
|
|
711
|
+
return `${trimmed.slice(0, maxLength)}...`;
|
|
712
|
+
}
|
|
713
|
+
|
|
623
714
|
main().catch((error) => {
|
|
624
715
|
console.error(`[orchestrator] Failed: ${formatError(error)}`);
|
|
625
716
|
process.exitCode = 1;
|