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.
Files changed (2) hide show
  1. package/orchestrator.js +118 -27
  2. 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
- const session = await unwrap(
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
- const session = await unwrap(
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
- const session = await unwrap(
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 unwrap(
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 unwrap(
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
- const response = await unwrap(
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
- const statusMap = await unwrap(
238
- client.session.status({ query: { directory: root } }),
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
- const message = await unwrap(
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 unwrap(
484
- client.instance.dispose({ query: { directory: root } }),
485
- "instance.dispose"
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrar",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "OpenCode milestone orchestrator",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",