orchestrar 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/orchestrator.js +144 -33
  2. package/package.json +1 -1
package/orchestrator.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require("node:fs/promises");
4
4
  const path = require("node:path");
5
+ const crypto = require("node:crypto");
5
6
  const DEFAULT_MODEL = "github-copilot/gpt-5.2-codex";
6
7
  const COMMIT_MODEL = "github-copilot/gpt-5-mini";
7
8
  const DEFAULT_AGENT = "build-gpt-5.2-codex";
@@ -12,10 +13,19 @@ const DEFAULT_REVIEW_TIMEOUT_MS = 60 * 60 * 1000;
12
13
  const DEFAULT_SESSION_TIMEOUT_MS = 2 * 60 * 60 * 1000;
13
14
  const DEFAULT_STATUS_POLL_INTERVAL_MS = 2000;
14
15
  const DEFAULT_MAX_REVIEW_ITERATIONS = 20;
16
+ const DEFAULT_DEBUG_ENABLED = true;
17
+ const DEBUG_ENABLED = parseBoolean(
18
+ process.env.ORCHESTRATOR_DEBUG,
19
+ DEFAULT_DEBUG_ENABLED
20
+ );
15
21
 
16
22
  async function main() {
17
23
  const root = process.cwd();
24
+ logDebug(`Starting orchestrator in ${root}`);
18
25
  const docs = await resolveDocs(root);
26
+ logDebug(
27
+ `Docs resolved: prd=${docs.prd}, spec=${docs.spec}, plan=${docs.plan}`
28
+ );
19
29
  const planPath = docs.plan;
20
30
  const { createOpencode } = await import("@opencode-ai/sdk");
21
31
 
@@ -38,13 +48,15 @@ async function runWorkInstance(createOpencode, docs, root) {
38
48
  const { client, server } = await createOpencode({
39
49
  config: buildConfig(DEFAULT_MODEL),
40
50
  });
51
+ logDebug(`OpenCode server started at ${server.url}`);
41
52
  try {
42
- const session = await unwrap(
53
+ logDebug("Creating milestone session");
54
+ const session = await callClient(
55
+ "session.create",
43
56
  client.session.create({
44
57
  query: { directory: root },
45
58
  body: { title: "Milestone Orchestrator" },
46
- }),
47
- "session.create"
59
+ })
48
60
  );
49
61
 
50
62
  const sessionID = extractSessionID(session, "session.create (work instance)");
@@ -76,13 +88,15 @@ async function runCommitInstance(createOpencode, root) {
76
88
  const { client, server } = await createOpencode({
77
89
  config: buildConfig(COMMIT_MODEL),
78
90
  });
91
+ logDebug(`OpenCode server started at ${server.url}`);
79
92
  try {
80
- const session = await unwrap(
93
+ logDebug("Creating commit session");
94
+ const session = await callClient(
95
+ "session.create",
81
96
  client.session.create({
82
97
  query: { directory: root },
83
98
  body: { title: "Commit & Push" },
84
- }),
85
- "session.create"
99
+ })
86
100
  );
87
101
 
88
102
  const sessionID = extractSessionID(session, "session.create (commit instance)");
@@ -105,6 +119,11 @@ async function runReviewLoop(client, sessionID, root) {
105
119
  process.env.ORCHESTRATOR_MAX_REVIEW_ITERATIONS,
106
120
  DEFAULT_MAX_REVIEW_ITERATIONS
107
121
  );
122
+ logDebug(
123
+ `Review loop configured: maxIterations=${maxIterations}, command=${
124
+ process.env.ORCHESTRATOR_REVIEW_COMMAND || DEFAULT_REVIEW_COMMAND_NAME
125
+ }`
126
+ );
108
127
 
109
128
  for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
110
129
  logStep(`Running review (${iteration}/${maxIterations})`);
@@ -142,16 +161,21 @@ async function runReviewCommand(client, root) {
142
161
  process.env.ORCHESTRATOR_REVIEW_TIMEOUT_MS,
143
162
  DEFAULT_REVIEW_TIMEOUT_MS
144
163
  );
145
- const session = await unwrap(
164
+ logDebug(
165
+ `Starting review command: name=${commandName}, args=${commandArguments ||
166
+ "<none>"}, timeoutMs=${timeoutMs}`
167
+ );
168
+ const session = await callClient(
169
+ "session.create",
146
170
  client.session.create({
147
171
  query: { directory: root },
148
172
  body: { title: "Review" },
149
- }),
150
- "session.create"
173
+ })
151
174
  );
152
175
 
153
176
  const sessionID = extractSessionID(session, "session.create (review instance)");
154
- const commandResult = await unwrap(
177
+ const commandResult = await callClient(
178
+ "session.command",
155
179
  client.session.command({
156
180
  path: { id: sessionID },
157
181
  query: { directory: root },
@@ -161,8 +185,7 @@ async function runReviewCommand(client, root) {
161
185
  agent: DEFAULT_AGENT,
162
186
  model: DEFAULT_MODEL,
163
187
  },
164
- }),
165
- "session.command"
188
+ })
166
189
  );
167
190
 
168
191
  const commandMessageID = extractMessageID(commandResult);
@@ -177,17 +200,18 @@ async function runReviewCommand(client, root) {
177
200
  let parts = commandResult?.parts ?? [];
178
201
  const messageID = extractMessageID(commandResult);
179
202
  if (messageID) {
180
- const message = await unwrap(
203
+ const message = await callClient(
204
+ "session.message",
181
205
  client.session.message({
182
206
  path: { id: sessionID, messageID },
183
207
  query: { directory: root },
184
- }),
185
- "session.message"
208
+ })
186
209
  );
187
210
  parts = message?.parts ?? parts;
188
211
  }
189
212
 
190
213
  const output = collectCommandOutput(parts);
214
+ logDebug(`Review command output length: ${output.length}`);
191
215
  if (!output.trim()) {
192
216
  throw new Error("Review command produced no output.");
193
217
  }
@@ -203,20 +227,25 @@ async function sendPrompt(
203
227
  agentSpec = DEFAULT_AGENT
204
228
  ) {
205
229
  const model = parseModelSpec(modelSpec);
206
- const response = await unwrap(
207
- client.session.prompt({
230
+ logDebug(
231
+ `Sending prompt to session ${sessionID}: ${summarizeText(text, 160)}`
232
+ );
233
+ const messageID = generateMessageID();
234
+ await callClient(
235
+ "session.promptAsync",
236
+ client.session.promptAsync({
208
237
  path: { id: sessionID },
209
238
  query: { directory: root },
210
239
  body: {
240
+ messageID,
211
241
  agent: agentSpec,
212
242
  model,
213
243
  parts: [{ type: "text", text }],
214
244
  },
215
- }),
216
- "session.prompt"
245
+ })
217
246
  );
218
247
 
219
- return extractMessageID(response);
248
+ return messageID;
220
249
  }
221
250
 
222
251
  async function waitForSessionIdle(client, sessionID, root, timeoutOverrideMs) {
@@ -230,18 +259,28 @@ async function waitForSessionIdle(client, sessionID, root, timeoutOverrideMs) {
230
259
  process.env.ORCHESTRATOR_STATUS_POLL_INTERVAL_MS,
231
260
  DEFAULT_STATUS_POLL_INTERVAL_MS
232
261
  );
262
+ logDebug(
263
+ `Waiting for session ${sessionID} idle (timeoutMs=${timeoutMs}, pollMs=${pollIntervalMs})`
264
+ );
233
265
 
234
266
  const start = Date.now();
235
267
  let lastKnownSessions = [];
268
+ let attempt = 0;
236
269
  while (Date.now() - start < timeoutMs) {
237
- const statusMap = await unwrap(
238
- client.session.status({ query: { directory: root } }),
239
- "session.status"
270
+ attempt += 1;
271
+ const statusMap = await callClient(
272
+ "session.status",
273
+ client.session.status({ query: { directory: root } })
240
274
  );
241
275
  if (statusMap && typeof statusMap === "object") {
242
276
  lastKnownSessions = Object.keys(statusMap);
243
277
  }
244
278
  const status = statusMap?.[sessionID];
279
+ logDebug(
280
+ `Session status poll ${attempt}: session=${sessionID}, status=${
281
+ status?.type || "<missing>"
282
+ }, known=${lastKnownSessions.length}`
283
+ );
245
284
  if (!status) {
246
285
  await delay(pollIntervalMs);
247
286
  continue;
@@ -282,17 +321,40 @@ async function waitForMessageComplete(
282
321
  process.env.ORCHESTRATOR_STATUS_POLL_INTERVAL_MS,
283
322
  DEFAULT_STATUS_POLL_INTERVAL_MS
284
323
  );
324
+ logDebug(
325
+ `Waiting for message ${messageID} (timeoutMs=${timeoutMs}, pollMs=${pollIntervalMs})`
326
+ );
285
327
 
286
328
  const start = Date.now();
329
+ let attempt = 0;
287
330
  while (Date.now() - start < timeoutMs) {
288
- const message = await unwrap(
289
- client.session.message({
290
- path: { id: sessionID, messageID },
291
- query: { directory: root },
292
- }),
293
- "session.message"
294
- );
331
+ attempt += 1;
332
+ let message;
333
+ try {
334
+ message = await callClient(
335
+ "session.message",
336
+ client.session.message({
337
+ path: { id: sessionID, messageID },
338
+ query: { directory: root },
339
+ })
340
+ );
341
+ } catch (error) {
342
+ const formatted = formatError(error).toLowerCase();
343
+ if (formatted.includes("not found") || formatted.includes("404")) {
344
+ logDebug(
345
+ `Message poll ${attempt}: message=${messageID}, not found yet`
346
+ );
347
+ await delay(pollIntervalMs);
348
+ continue;
349
+ }
350
+ throw error;
351
+ }
295
352
  const info = message?.info ?? message;
353
+ logDebug(
354
+ `Message poll ${attempt}: message=${messageID}, completed=${
355
+ info?.time?.completed ? "yes" : "no"
356
+ }, finish=${info?.finish || "<none>"}`
357
+ );
296
358
  if (info?.error) {
297
359
  throw new Error(
298
360
  `Session message ${messageID} failed: ${formatError(info.error)}`
@@ -454,6 +516,20 @@ function parseNumber(value, fallback) {
454
516
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
455
517
  }
456
518
 
519
+ function parseBoolean(value, fallback) {
520
+ if (value === undefined || value === null || value === "") {
521
+ return fallback;
522
+ }
523
+ const normalized = String(value).trim().toLowerCase();
524
+ if (["1", "true", "yes", "on"].includes(normalized)) {
525
+ return true;
526
+ }
527
+ if (["0", "false", "no", "off"].includes(normalized)) {
528
+ return false;
529
+ }
530
+ return fallback;
531
+ }
532
+
457
533
  function buildConfig(model) {
458
534
  return {
459
535
  model,
@@ -480,9 +556,9 @@ function parseModelSpec(spec) {
480
556
 
481
557
  async function disposeInstance(client, server, root) {
482
558
  try {
483
- await unwrap(
484
- client.instance.dispose({ query: { directory: root } }),
485
- "instance.dispose"
559
+ await callClient(
560
+ "instance.dispose",
561
+ client.instance.dispose({ query: { directory: root } })
486
562
  );
487
563
  } catch (error) {
488
564
  logStep(`Instance dispose failed: ${formatError(error)}`);
@@ -527,6 +603,19 @@ async function unwrap(result, label) {
527
603
  return result;
528
604
  }
529
605
 
606
+ async function callClient(label, promise) {
607
+ const start = Date.now();
608
+ logDebug(`${label} -> start`);
609
+ try {
610
+ const data = await unwrap(promise, label);
611
+ logDebug(`${label} -> ok (${Date.now() - start}ms)`);
612
+ return data;
613
+ } catch (error) {
614
+ logDebug(`${label} -> error (${Date.now() - start}ms): ${formatError(error)}`);
615
+ throw error;
616
+ }
617
+ }
618
+
530
619
  function extractSessionID(session, context) {
531
620
  if (!session || typeof session !== "object") {
532
621
  throw new Error(`${context} did not return a session object.`);
@@ -620,6 +709,28 @@ function logStep(message) {
620
709
  console.log(`[orchestrator] ${message}`);
621
710
  }
622
711
 
712
+ function logDebug(message) {
713
+ if (!DEBUG_ENABLED) {
714
+ return;
715
+ }
716
+ console.log(`[orchestrator][debug] ${message}`);
717
+ }
718
+
719
+ function summarizeText(text, maxLength) {
720
+ if (typeof text !== "string") {
721
+ return "<non-text>";
722
+ }
723
+ const trimmed = text.replace(/\s+/g, " ").trim();
724
+ if (trimmed.length <= maxLength) {
725
+ return trimmed;
726
+ }
727
+ return `${trimmed.slice(0, maxLength)}...`;
728
+ }
729
+
730
+ function generateMessageID() {
731
+ return `msg_${crypto.randomBytes(12).toString("hex")}`;
732
+ }
733
+
623
734
  main().catch((error) => {
624
735
  console.error(`[orchestrator] Failed: ${formatError(error)}`);
625
736
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrar",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "OpenCode milestone orchestrator",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",