orchestrar 0.3.3 → 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 +156 -74
  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)");
@@ -58,7 +69,7 @@ async function runWorkInstance(createOpencode, docs, root) {
58
69
  );
59
70
  await waitForMessageComplete(client, sessionID, workMessageID, root);
60
71
 
61
- await runReviewLoop(createOpencode, client, sessionID, root);
72
+ await runReviewLoop(client, sessionID, root);
62
73
 
63
74
  const markTasksMessageID = await sendPrompt(
64
75
  client,
@@ -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)");
@@ -100,15 +113,20 @@ async function runCommitInstance(createOpencode, root) {
100
113
  }
101
114
  }
102
115
 
103
- async function runReviewLoop(createOpencode, client, sessionID, root) {
116
+ async function runReviewLoop(client, sessionID, root) {
104
117
  const maxIterations = parseNumber(
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})`);
111
- const reviewResult = await runReviewCommand(createOpencode, root);
129
+ const reviewResult = await runReviewCommand(client, root);
112
130
  if (isFindingsEmpty(reviewResult)) {
113
131
  logStep("Review clean; no findings.");
114
132
  return;
@@ -132,7 +150,7 @@ async function runReviewLoop(createOpencode, client, sessionID, root) {
132
150
  );
133
151
  }
134
152
 
135
- async function runReviewCommand(createOpencode, root) {
153
+ async function runReviewCommand(client, root) {
136
154
  const commandName =
137
155
  process.env.ORCHESTRATOR_REVIEW_COMMAND || DEFAULT_REVIEW_COMMAND_NAME;
138
156
  const commandArguments =
@@ -142,65 +160,61 @@ async function runReviewCommand(createOpencode, root) {
142
160
  process.env.ORCHESTRATOR_REVIEW_TIMEOUT_MS,
143
161
  DEFAULT_REVIEW_TIMEOUT_MS
144
162
  );
163
+ logDebug(
164
+ `Starting review command: name=${commandName}, args=${commandArguments ||
165
+ "<none>"}, timeoutMs=${timeoutMs}`
166
+ );
167
+ const session = await callClient(
168
+ "session.create",
169
+ client.session.create({
170
+ query: { directory: root },
171
+ body: { title: "Review" },
172
+ })
173
+ );
145
174
 
146
- const { client, server } = await createOpencode({
147
- config: buildConfig(DEFAULT_MODEL),
148
- });
175
+ const sessionID = extractSessionID(session, "session.create (review instance)");
176
+ const commandResult = await callClient(
177
+ "session.command",
178
+ client.session.command({
179
+ path: { id: sessionID },
180
+ query: { directory: root },
181
+ body: {
182
+ command: commandName,
183
+ arguments: commandArguments,
184
+ agent: DEFAULT_AGENT,
185
+ model: DEFAULT_MODEL,
186
+ },
187
+ })
188
+ );
149
189
 
150
- try {
151
- const session = await unwrap(
152
- client.session.create({
153
- query: { directory: root },
154
- body: { title: "Review" },
155
- }),
156
- "session.create"
157
- );
190
+ const commandMessageID = extractMessageID(commandResult);
191
+ await waitForMessageComplete(
192
+ client,
193
+ sessionID,
194
+ commandMessageID,
195
+ root,
196
+ timeoutMs
197
+ );
158
198
 
159
- const sessionID = extractSessionID(session, "session.create (review instance)");
160
- const commandResult = await unwrap(
161
- client.session.command({
162
- path: { id: sessionID },
199
+ let parts = commandResult?.parts ?? [];
200
+ const messageID = extractMessageID(commandResult);
201
+ if (messageID) {
202
+ const message = await callClient(
203
+ "session.message",
204
+ client.session.message({
205
+ path: { id: sessionID, messageID },
163
206
  query: { directory: root },
164
- body: {
165
- command: commandName,
166
- arguments: commandArguments,
167
- agent: DEFAULT_AGENT,
168
- model: DEFAULT_MODEL,
169
- },
170
- }),
171
- "session.command"
207
+ })
172
208
  );
209
+ parts = message?.parts ?? parts;
210
+ }
173
211
 
174
- const commandMessageID = extractMessageID(commandResult);
175
- await waitForMessageComplete(
176
- client,
177
- sessionID,
178
- commandMessageID,
179
- root,
180
- timeoutMs
181
- );
182
-
183
- let parts = commandResult?.parts ?? [];
184
- const messageID = extractMessageID(commandResult);
185
- if (messageID) {
186
- const message = await unwrap(
187
- client.session.message({
188
- path: { id: sessionID, messageID },
189
- query: { directory: root },
190
- }),
191
- "session.message"
192
- );
193
- parts = message?.parts ?? parts;
194
- }
195
-
196
- const output = collectCommandOutput(parts);
197
- if (!output.trim()) {
198
- throw new Error("Review command produced no output.");
199
- }
200
- return extractReviewJson(output);
201
- } finally {
202
- await disposeInstance(client, server, root);
212
+ const output = collectCommandOutput(parts);
213
+ logDebug(`Review command output length: ${output.length}`);
214
+ if (!output.trim()) {
215
+ throw new Error("Review command produced no output.");
203
216
  }
217
+ return extractReviewJson(output);
204
218
  }
205
219
 
206
220
  async function sendPrompt(
@@ -212,7 +226,11 @@ async function sendPrompt(
212
226
  agentSpec = DEFAULT_AGENT
213
227
  ) {
214
228
  const model = parseModelSpec(modelSpec);
215
- const response = await unwrap(
229
+ logDebug(
230
+ `Sending prompt to session ${sessionID}: ${summarizeText(text, 160)}`
231
+ );
232
+ const response = await callClient(
233
+ "session.prompt",
216
234
  client.session.prompt({
217
235
  path: { id: sessionID },
218
236
  query: { directory: root },
@@ -221,8 +239,7 @@ async function sendPrompt(
221
239
  model,
222
240
  parts: [{ type: "text", text }],
223
241
  },
224
- }),
225
- "session.prompt"
242
+ })
226
243
  );
227
244
 
228
245
  return extractMessageID(response);
@@ -239,18 +256,28 @@ async function waitForSessionIdle(client, sessionID, root, timeoutOverrideMs) {
239
256
  process.env.ORCHESTRATOR_STATUS_POLL_INTERVAL_MS,
240
257
  DEFAULT_STATUS_POLL_INTERVAL_MS
241
258
  );
259
+ logDebug(
260
+ `Waiting for session ${sessionID} idle (timeoutMs=${timeoutMs}, pollMs=${pollIntervalMs})`
261
+ );
242
262
 
243
263
  const start = Date.now();
244
264
  let lastKnownSessions = [];
265
+ let attempt = 0;
245
266
  while (Date.now() - start < timeoutMs) {
246
- const statusMap = await unwrap(
247
- client.session.status({ query: { directory: root } }),
248
- "session.status"
267
+ attempt += 1;
268
+ const statusMap = await callClient(
269
+ "session.status",
270
+ client.session.status({ query: { directory: root } })
249
271
  );
250
272
  if (statusMap && typeof statusMap === "object") {
251
273
  lastKnownSessions = Object.keys(statusMap);
252
274
  }
253
275
  const status = statusMap?.[sessionID];
276
+ logDebug(
277
+ `Session status poll ${attempt}: session=${sessionID}, status=${
278
+ status?.type || "<missing>"
279
+ }, known=${lastKnownSessions.length}`
280
+ );
254
281
  if (!status) {
255
282
  await delay(pollIntervalMs);
256
283
  continue;
@@ -291,17 +318,27 @@ async function waitForMessageComplete(
291
318
  process.env.ORCHESTRATOR_STATUS_POLL_INTERVAL_MS,
292
319
  DEFAULT_STATUS_POLL_INTERVAL_MS
293
320
  );
321
+ logDebug(
322
+ `Waiting for message ${messageID} (timeoutMs=${timeoutMs}, pollMs=${pollIntervalMs})`
323
+ );
294
324
 
295
325
  const start = Date.now();
326
+ let attempt = 0;
296
327
  while (Date.now() - start < timeoutMs) {
297
- const message = await unwrap(
328
+ attempt += 1;
329
+ const message = await callClient(
330
+ "session.message",
298
331
  client.session.message({
299
332
  path: { id: sessionID, messageID },
300
333
  query: { directory: root },
301
- }),
302
- "session.message"
334
+ })
303
335
  );
304
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
+ );
305
342
  if (info?.error) {
306
343
  throw new Error(
307
344
  `Session message ${messageID} failed: ${formatError(info.error)}`
@@ -463,6 +500,20 @@ function parseNumber(value, fallback) {
463
500
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
464
501
  }
465
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
+
466
517
  function buildConfig(model) {
467
518
  return {
468
519
  model,
@@ -489,9 +540,9 @@ function parseModelSpec(spec) {
489
540
 
490
541
  async function disposeInstance(client, server, root) {
491
542
  try {
492
- await unwrap(
493
- client.instance.dispose({ query: { directory: root } }),
494
- "instance.dispose"
543
+ await callClient(
544
+ "instance.dispose",
545
+ client.instance.dispose({ query: { directory: root } })
495
546
  );
496
547
  } catch (error) {
497
548
  logStep(`Instance dispose failed: ${formatError(error)}`);
@@ -536,6 +587,19 @@ async function unwrap(result, label) {
536
587
  return result;
537
588
  }
538
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
+
539
603
  function extractSessionID(session, context) {
540
604
  if (!session || typeof session !== "object") {
541
605
  throw new Error(`${context} did not return a session object.`);
@@ -629,6 +693,24 @@ function logStep(message) {
629
693
  console.log(`[orchestrator] ${message}`);
630
694
  }
631
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
+
632
714
  main().catch((error) => {
633
715
  console.error(`[orchestrator] Failed: ${formatError(error)}`);
634
716
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrar",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "OpenCode milestone orchestrator",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",