@welluable/orch 1.0.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 +264 -0
- package/agents/ask.js +22 -0
- package/agents/code-writer.js +55 -0
- package/agents/index.js +9 -0
- package/agents/planner.js +26 -0
- package/agents/quick-fix.js +30 -0
- package/agents/research.js +23 -0
- package/agents/test-critic.js +40 -0
- package/agents/test-runner.js +39 -0
- package/agents/test-writer.js +50 -0
- package/agents/triage.js +31 -0
- package/lib/agent-agn.js +57 -0
- package/lib/agent-claude.js +66 -0
- package/lib/agent-cursor.js +59 -0
- package/lib/agent.js +326 -0
- package/lib/commit.js +35 -0
- package/lib/parse-triage-json.js +33 -0
- package/lib/parse-verdict.js +25 -0
- package/lib/run-context.js +33 -0
- package/lib/slug.js +47 -0
- package/lib/stage-summary.js +34 -0
- package/lib/tool-status.js +214 -0
- package/lib/worktree.js +38 -0
- package/main.js +509 -0
- package/package.json +38 -0
package/main.js
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command, Option } from 'commander';
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { AgentCursor } from './lib/agent-cursor.js';
|
|
9
|
+
import { AgentClaude } from './lib/agent-claude.js';
|
|
10
|
+
import { AgentAgn } from './lib/agent-agn.js';
|
|
11
|
+
import { parseTriageJson } from './lib/parse-triage-json.js';
|
|
12
|
+
import { parseVerdict } from './lib/parse-verdict.js';
|
|
13
|
+
import { splitStageSummary, printStageSummary } from './lib/stage-summary.js';
|
|
14
|
+
import { createRunContext } from './lib/run-context.js';
|
|
15
|
+
import { createWorktree } from './lib/worktree.js';
|
|
16
|
+
import { commitWorktree } from './lib/commit.js';
|
|
17
|
+
import { askAgentArgs } from './agents/ask.js';
|
|
18
|
+
import { triageAgentArgs } from './agents/triage.js';
|
|
19
|
+
import { quickFixAgentArgs } from './agents/quick-fix.js';
|
|
20
|
+
import { researchAgentArgs } from './agents/research.js';
|
|
21
|
+
import { plannerAgentArgs } from './agents/planner.js';
|
|
22
|
+
import { testWriterAgentArgs } from './agents/test-writer.js';
|
|
23
|
+
import { testCriticAgentArgs } from './agents/test-critic.js';
|
|
24
|
+
import { codeWriterAgentArgs } from './agents/code-writer.js';
|
|
25
|
+
import { testRunnerAgentArgs } from './agents/test-runner.js';
|
|
26
|
+
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
28
|
+
const __dirname = path.dirname(__filename);
|
|
29
|
+
|
|
30
|
+
const { version } = JSON.parse(
|
|
31
|
+
fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const AGENT_BACKENDS = {
|
|
35
|
+
cursor: {
|
|
36
|
+
AgentClass: AgentCursor,
|
|
37
|
+
binary: 'agent',
|
|
38
|
+
missingHint: 'agent not found; install Cursor Agent CLI or use --agent claude',
|
|
39
|
+
},
|
|
40
|
+
claude: {
|
|
41
|
+
AgentClass: AgentClaude,
|
|
42
|
+
binary: 'claude',
|
|
43
|
+
missingHint: 'claude not found; install Claude Code or use --agent cursor',
|
|
44
|
+
},
|
|
45
|
+
agn: {
|
|
46
|
+
AgentClass: AgentAgn,
|
|
47
|
+
binary: 'agn',
|
|
48
|
+
missingHint: 'agn not found; run npm install -g @welluable/agn-cli or use --agent cursor',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function isBinaryOnPath(binary) {
|
|
53
|
+
try {
|
|
54
|
+
execFileSync('which', [binary], { stdio: 'ignore' });
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function binaryMissingHint(agentName) {
|
|
62
|
+
const backend = AGENT_BACKENDS[agentName];
|
|
63
|
+
if (!backend) throw new Error(`Unknown agent backend: ${agentName}`);
|
|
64
|
+
return backend.missingHint;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ensureBinaryOnPath(binary, agentName) {
|
|
68
|
+
if (!isBinaryOnPath(binary)) {
|
|
69
|
+
console.error(binaryMissingHint(agentName));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatVerdictFeedback(verdict, rawResult) {
|
|
75
|
+
const lines = [];
|
|
76
|
+
if (verdict.summary) lines.push(verdict.summary);
|
|
77
|
+
if (Array.isArray(verdict.failures)) {
|
|
78
|
+
for (const failure of verdict.failures) {
|
|
79
|
+
lines.push(String(failure));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (lines.length === 0 && typeof rawResult === 'string') {
|
|
83
|
+
return rawResult;
|
|
84
|
+
}
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function appendLoopStatus(statusPath, title, { round, maxRounds, passed, summary }) {
|
|
89
|
+
fs.appendFileSync(
|
|
90
|
+
statusPath,
|
|
91
|
+
`\n## ${title}\n\n- Rounds: ${round}/${maxRounds}\n- Result: ${passed ? 'passed' : 'failed'}\n- Summary: ${summary || ''}\n`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function roundLabel(role, round, maxRounds) {
|
|
96
|
+
return `${role} ${round}/${maxRounds}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function runPipeline(prompt, options) {
|
|
100
|
+
const verbose = Boolean(options.verbose);
|
|
101
|
+
const maxRounds = options.maxRounds ?? 5;
|
|
102
|
+
const backend = AGENT_BACKENDS[options.agent];
|
|
103
|
+
if (!backend) {
|
|
104
|
+
throw new Error(`Unknown agent backend: ${options.agent}`);
|
|
105
|
+
}
|
|
106
|
+
const AgentClass = options.AgentClass ?? backend.AgentClass;
|
|
107
|
+
const binary = backend.binary;
|
|
108
|
+
const createRunContextFn = options.createRunContext ?? createRunContext;
|
|
109
|
+
const createWorktreeFn = options.createWorktree ?? createWorktree;
|
|
110
|
+
const commitWorktreeFn = options.commitWorktree ?? commitWorktree;
|
|
111
|
+
const invocationCwd = process.cwd();
|
|
112
|
+
|
|
113
|
+
console.log(`cwd: ${invocationCwd}`);
|
|
114
|
+
console.log(`agent: ${options.agent}`);
|
|
115
|
+
|
|
116
|
+
if (options.dryRun) {
|
|
117
|
+
const ready = isBinaryOnPath(binary);
|
|
118
|
+
console.log(ready ? 'pass' : 'fail');
|
|
119
|
+
if (!ready) {
|
|
120
|
+
console.error(binaryMissingHint(options.agent));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!options.AgentClass) {
|
|
127
|
+
ensureBinaryOnPath(binary, options.agent);
|
|
128
|
+
}
|
|
129
|
+
console.log();
|
|
130
|
+
|
|
131
|
+
if (options.ask) {
|
|
132
|
+
const ask = askAgentArgs({ prompt, cwd: invocationCwd });
|
|
133
|
+
const askAgent = new AgentClass(ask.name, ask.instructions, ask.prompt, ask.options);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const askResult = await askAgent.run({ verbose });
|
|
137
|
+
if (!askResult.ok) {
|
|
138
|
+
console.error(`Error: ask agent failed`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const { content, summary } = splitStageSummary(askResult.result);
|
|
143
|
+
printStageSummary('ask', summary);
|
|
144
|
+
console.log(content);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error(`Error: ${err.message}`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (options.quick) {
|
|
153
|
+
const quickFix = quickFixAgentArgs({ prompt, cwd: invocationCwd });
|
|
154
|
+
const quickFixAgent = new AgentClass(
|
|
155
|
+
quickFix.name,
|
|
156
|
+
quickFix.instructions,
|
|
157
|
+
quickFix.prompt,
|
|
158
|
+
quickFix.options,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const quickFixResult = await quickFixAgent.run({ verbose });
|
|
163
|
+
if (!quickFixResult.ok) {
|
|
164
|
+
console.error(`Error: quick-fix agent failed`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error(`Error: ${err.message}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const triage = triageAgentArgs({ prompt, cwd: invocationCwd });
|
|
176
|
+
const triageAgent = new AgentClass(
|
|
177
|
+
triage.name,
|
|
178
|
+
triage.instructions,
|
|
179
|
+
triage.prompt,
|
|
180
|
+
triage.options,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const triageResult = await triageAgent.run({ verbose });
|
|
185
|
+
const { content: triageContent, summary: triageSummary } = splitStageSummary(triageResult.result);
|
|
186
|
+
printStageSummary('triage', triageSummary);
|
|
187
|
+
const parsed = parseTriageJson(triageContent);
|
|
188
|
+
|
|
189
|
+
if (parsed?.simple === true) {
|
|
190
|
+
const quickFix = quickFixAgentArgs({
|
|
191
|
+
prompt,
|
|
192
|
+
cwd: invocationCwd,
|
|
193
|
+
fix_plan: parsed.fix_plan,
|
|
194
|
+
});
|
|
195
|
+
const quickFixAgent = new AgentClass(
|
|
196
|
+
quickFix.name,
|
|
197
|
+
quickFix.instructions,
|
|
198
|
+
quickFix.prompt,
|
|
199
|
+
quickFix.options,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const quickFixResult = await quickFixAgent.run({ verbose });
|
|
203
|
+
const { summary: quickFixSummary } = splitStageSummary(quickFixResult.result);
|
|
204
|
+
printStageSummary('quick-fix', quickFixSummary);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const runContext = createRunContextFn({ cwd: invocationCwd });
|
|
209
|
+
console.log(`task ${runContext.slug} is started`);
|
|
210
|
+
|
|
211
|
+
const research = researchAgentArgs({
|
|
212
|
+
prompt,
|
|
213
|
+
cwd: invocationCwd,
|
|
214
|
+
researchPath: runContext.researchPath,
|
|
215
|
+
});
|
|
216
|
+
const researchAgent = new AgentClass(
|
|
217
|
+
research.name,
|
|
218
|
+
research.instructions,
|
|
219
|
+
research.prompt,
|
|
220
|
+
research.options,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const result = await researchAgent.run({ verbose });
|
|
224
|
+
const { content: researchContent, summary: researchSummary } = splitStageSummary(result.result);
|
|
225
|
+
printStageSummary('research', researchSummary);
|
|
226
|
+
|
|
227
|
+
const planner = plannerAgentArgs({
|
|
228
|
+
prompt,
|
|
229
|
+
cwd: invocationCwd,
|
|
230
|
+
researchPath: runContext.researchPath,
|
|
231
|
+
taskPath: runContext.taskPath,
|
|
232
|
+
researchOutput: researchContent,
|
|
233
|
+
});
|
|
234
|
+
const plannerAgent = new AgentClass(
|
|
235
|
+
planner.name,
|
|
236
|
+
planner.instructions,
|
|
237
|
+
planner.prompt,
|
|
238
|
+
planner.options,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const plannerResult = await plannerAgent.run({ verbose });
|
|
242
|
+
const { summary: plannerSummary } = splitStageSummary(plannerResult.result);
|
|
243
|
+
printStageSummary('planner', plannerSummary);
|
|
244
|
+
|
|
245
|
+
const worktree = createWorktreeFn({ cwd: invocationCwd, slug: runContext.slug });
|
|
246
|
+
|
|
247
|
+
fs.mkdirSync(path.dirname(runContext.statusPath), { recursive: true });
|
|
248
|
+
fs.writeFileSync(
|
|
249
|
+
runContext.statusPath,
|
|
250
|
+
`# Status\n\n- Slug: \`${runContext.slug}\`\n- Branch: \`${worktree.branch}\`\n- Worktree: \`${worktree.worktreePath}\`\n`,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// --- test loop: test-writer ⇄ test-critic ---
|
|
254
|
+
let testAccepted = null;
|
|
255
|
+
let criticFeedback = null;
|
|
256
|
+
let testRound = 0;
|
|
257
|
+
let testSummary = '';
|
|
258
|
+
|
|
259
|
+
for (let round = 1; round <= maxRounds; round++) {
|
|
260
|
+
testRound = round;
|
|
261
|
+
|
|
262
|
+
const testWriterArgs = testWriterAgentArgs({
|
|
263
|
+
prompt,
|
|
264
|
+
cwd: worktree.worktreePath,
|
|
265
|
+
worktreePath: worktree.worktreePath,
|
|
266
|
+
branch: worktree.branch,
|
|
267
|
+
taskPath: runContext.taskPath,
|
|
268
|
+
statusPath: runContext.statusPath,
|
|
269
|
+
criticFeedback,
|
|
270
|
+
});
|
|
271
|
+
const testWriter = new AgentClass(
|
|
272
|
+
roundLabel('test-writer', round, maxRounds),
|
|
273
|
+
testWriterArgs.instructions,
|
|
274
|
+
testWriterArgs.prompt,
|
|
275
|
+
testWriterArgs.options,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const testOut = await testWriter.run({ verbose });
|
|
279
|
+
const { content: testWriterContent, summary: testWriterSummary } = splitStageSummary(testOut.result);
|
|
280
|
+
printStageSummary(roundLabel('test-writer', round, maxRounds), testWriterSummary);
|
|
281
|
+
if (!testOut.ok) {
|
|
282
|
+
appendLoopStatus(runContext.statusPath, 'Test loop', {
|
|
283
|
+
round: testRound,
|
|
284
|
+
maxRounds,
|
|
285
|
+
passed: false,
|
|
286
|
+
summary: 'test-writer failed',
|
|
287
|
+
});
|
|
288
|
+
throw new Error('test-writer failed; stopping before code-writer');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const testCriticArgs = testCriticAgentArgs({
|
|
292
|
+
prompt,
|
|
293
|
+
cwd: worktree.worktreePath,
|
|
294
|
+
worktreePath: worktree.worktreePath,
|
|
295
|
+
branch: worktree.branch,
|
|
296
|
+
taskPath: runContext.taskPath,
|
|
297
|
+
statusPath: runContext.statusPath,
|
|
298
|
+
testWriterOutput: testWriterContent,
|
|
299
|
+
});
|
|
300
|
+
const testCritic = new AgentClass(
|
|
301
|
+
roundLabel('test-critic', round, maxRounds),
|
|
302
|
+
testCriticArgs.instructions,
|
|
303
|
+
testCriticArgs.prompt,
|
|
304
|
+
testCriticArgs.options,
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const criticOut = await testCritic.run({ verbose });
|
|
308
|
+
const { content: testCriticContent, summary: testCriticSummary } = splitStageSummary(criticOut.result);
|
|
309
|
+
printStageSummary(roundLabel('test-critic', round, maxRounds), testCriticSummary);
|
|
310
|
+
if (!criticOut.ok) {
|
|
311
|
+
appendLoopStatus(runContext.statusPath, 'Test loop', {
|
|
312
|
+
round: testRound,
|
|
313
|
+
maxRounds,
|
|
314
|
+
passed: false,
|
|
315
|
+
summary: 'test-critic failed',
|
|
316
|
+
});
|
|
317
|
+
throw new Error('test-critic failed; stopping before code-writer');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const verdict = parseVerdict(testCriticContent);
|
|
321
|
+
testSummary = verdict.summary;
|
|
322
|
+
if (verdict.passed) {
|
|
323
|
+
testAccepted = { writerContent: testWriterContent, criticOut, verdict, round };
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
criticFeedback = formatVerdictFeedback(verdict, testCriticContent);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
appendLoopStatus(runContext.statusPath, 'Test loop', {
|
|
330
|
+
round: testAccepted?.round ?? testRound,
|
|
331
|
+
maxRounds,
|
|
332
|
+
passed: Boolean(testAccepted),
|
|
333
|
+
summary: testAccepted?.verdict.summary ?? testSummary,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
if (!testAccepted) {
|
|
337
|
+
throw new Error(`test loop exhausted after ${maxRounds} rounds`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// --- code loop: code-writer ⇄ test-runner ---
|
|
341
|
+
let codeAccepted = null;
|
|
342
|
+
let runnerFeedback = null;
|
|
343
|
+
let codeRound = 0;
|
|
344
|
+
let codeSummary = '';
|
|
345
|
+
|
|
346
|
+
const acceptedVerification = [
|
|
347
|
+
testAccepted.verdict.summary,
|
|
348
|
+
testAccepted.writerContent,
|
|
349
|
+
]
|
|
350
|
+
.filter(Boolean)
|
|
351
|
+
.join('\n');
|
|
352
|
+
|
|
353
|
+
for (let round = 1; round <= maxRounds; round++) {
|
|
354
|
+
codeRound = round;
|
|
355
|
+
|
|
356
|
+
const codeWriterArgs = codeWriterAgentArgs({
|
|
357
|
+
prompt,
|
|
358
|
+
cwd: worktree.worktreePath,
|
|
359
|
+
worktreePath: worktree.worktreePath,
|
|
360
|
+
branch: worktree.branch,
|
|
361
|
+
taskPath: runContext.taskPath,
|
|
362
|
+
statusPath: runContext.statusPath,
|
|
363
|
+
round,
|
|
364
|
+
acceptedVerification,
|
|
365
|
+
runnerFeedback,
|
|
366
|
+
});
|
|
367
|
+
const codeWriter = new AgentClass(
|
|
368
|
+
roundLabel('code-writer', round, maxRounds),
|
|
369
|
+
codeWriterArgs.instructions,
|
|
370
|
+
codeWriterArgs.prompt,
|
|
371
|
+
codeWriterArgs.options,
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const codeOut = await codeWriter.run({ verbose });
|
|
375
|
+
const { content: codeWriterContent, summary: codeWriterSummary } = splitStageSummary(codeOut.result);
|
|
376
|
+
printStageSummary(roundLabel('code-writer', round, maxRounds), codeWriterSummary);
|
|
377
|
+
if (!codeOut.ok) {
|
|
378
|
+
appendLoopStatus(runContext.statusPath, 'Code loop', {
|
|
379
|
+
round: codeRound,
|
|
380
|
+
maxRounds,
|
|
381
|
+
passed: false,
|
|
382
|
+
summary: 'code-writer failed',
|
|
383
|
+
});
|
|
384
|
+
throw new Error('code-writer failed; stopping before commit');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const testRunnerArgs = testRunnerAgentArgs({
|
|
388
|
+
prompt,
|
|
389
|
+
cwd: worktree.worktreePath,
|
|
390
|
+
worktreePath: worktree.worktreePath,
|
|
391
|
+
branch: worktree.branch,
|
|
392
|
+
statusPath: runContext.statusPath,
|
|
393
|
+
codeWriterOutput: codeWriterContent,
|
|
394
|
+
});
|
|
395
|
+
const testRunner = new AgentClass(
|
|
396
|
+
roundLabel('test-runner', round, maxRounds),
|
|
397
|
+
testRunnerArgs.instructions,
|
|
398
|
+
testRunnerArgs.prompt,
|
|
399
|
+
testRunnerArgs.options,
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
const runnerOut = await testRunner.run({ verbose });
|
|
403
|
+
const { content: testRunnerContent, summary: testRunnerSummary } = splitStageSummary(runnerOut.result);
|
|
404
|
+
printStageSummary(roundLabel('test-runner', round, maxRounds), testRunnerSummary);
|
|
405
|
+
if (!runnerOut.ok) {
|
|
406
|
+
appendLoopStatus(runContext.statusPath, 'Code loop', {
|
|
407
|
+
round: codeRound,
|
|
408
|
+
maxRounds,
|
|
409
|
+
passed: false,
|
|
410
|
+
summary: 'test-runner failed',
|
|
411
|
+
});
|
|
412
|
+
throw new Error('test-runner failed; stopping before commit');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const verdict = parseVerdict(testRunnerContent);
|
|
416
|
+
codeSummary = verdict.summary;
|
|
417
|
+
if (verdict.passed) {
|
|
418
|
+
codeAccepted = { writerContent: codeWriterContent, verdict, round };
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
runnerFeedback = formatVerdictFeedback(verdict, testRunnerContent);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
appendLoopStatus(runContext.statusPath, 'Code loop', {
|
|
425
|
+
round: codeAccepted?.round ?? codeRound,
|
|
426
|
+
maxRounds,
|
|
427
|
+
passed: Boolean(codeAccepted),
|
|
428
|
+
summary: codeAccepted?.verdict.summary ?? codeSummary,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
if (!codeAccepted) {
|
|
432
|
+
throw new Error(`code loop exhausted after ${maxRounds} rounds`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const message = `orch: ${runContext.slug} ${prompt.split('\n')[0]}`;
|
|
436
|
+
const commitResult = commitWorktreeFn({
|
|
437
|
+
worktreePath: worktree.worktreePath,
|
|
438
|
+
branch: worktree.branch,
|
|
439
|
+
message,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (commitResult.committed) {
|
|
443
|
+
fs.appendFileSync(
|
|
444
|
+
runContext.statusPath,
|
|
445
|
+
`\n## Commit\n\n- SHA: \`${commitResult.sha}\`\n- Branch: \`${commitResult.branch}\`\n`,
|
|
446
|
+
);
|
|
447
|
+
console.log(`commit: ${commitResult.sha.slice(0, 7)} on ${commitResult.branch}`);
|
|
448
|
+
console.log(`merge: git merge ${commitResult.branch}`);
|
|
449
|
+
} else {
|
|
450
|
+
fs.appendFileSync(
|
|
451
|
+
runContext.statusPath,
|
|
452
|
+
`\n## Commit\n\n- No changes to commit on \`${commitResult.branch}\`.\n`,
|
|
453
|
+
);
|
|
454
|
+
console.log(`commit: no changes on ${commitResult.branch}`);
|
|
455
|
+
}
|
|
456
|
+
} catch (err) {
|
|
457
|
+
console.error(`Error: ${err.message}`);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const program = new Command();
|
|
463
|
+
|
|
464
|
+
program
|
|
465
|
+
.name('orch')
|
|
466
|
+
.version(version)
|
|
467
|
+
.description('The Orchestrator: triage → research → plan → implement pipeline against a task')
|
|
468
|
+
.argument('<task...>', 'Task description to use as the prompt (mention a file path and the agent will read it)')
|
|
469
|
+
.option('-v, --verbose', 'Stream agent thinking/output deltas to stderr as the pipeline runs')
|
|
470
|
+
.option('--dry-run', 'Check that the selected agent CLI is on PATH and exit; do not run the pipeline')
|
|
471
|
+
.option('--ask', 'Ask a read-only question about the codebase; print the reply and exit (skips triage and all write pipelines)')
|
|
472
|
+
.option('--quick', 'Skip triage, run quick-fix directly in the current working tree; create no artifacts, worktrees, or commits')
|
|
473
|
+
.option('--max-rounds <n>', 'Max writer⇄critic and writer⇄runner iterations per implementer loop (ignored with --ask and --quick)', (value) => {
|
|
474
|
+
const n = Number.parseInt(value, 10);
|
|
475
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
476
|
+
throw new Error('--max-rounds must be a positive integer');
|
|
477
|
+
}
|
|
478
|
+
return n;
|
|
479
|
+
}, 5)
|
|
480
|
+
.addOption(
|
|
481
|
+
new Option('--agent <agent>', 'Agent backend to run the pipeline with: "cursor" (Cursor Agent CLI), "claude" (Claude Code CLI), or "agn" (agn CLI)')
|
|
482
|
+
.choices(['cursor', 'claude', 'agn'])
|
|
483
|
+
.default('cursor'),
|
|
484
|
+
)
|
|
485
|
+
.addHelpText(
|
|
486
|
+
'after',
|
|
487
|
+
`
|
|
488
|
+
Examples:
|
|
489
|
+
$ orch "fix the typo in the README" --agent claude
|
|
490
|
+
$ orch "fix the bug described in task.md" --agent cursor -v
|
|
491
|
+
$ orch "implement the local spec" --agent agn -v
|
|
492
|
+
$ orch --ask "where is the CLI entrypoint?" --agent claude
|
|
493
|
+
$ orch --quick "fix the typo in the README" --agent claude
|
|
494
|
+
$ orch "noop" --dry-run --agent cursor
|
|
495
|
+
`,
|
|
496
|
+
)
|
|
497
|
+
.action(async (task, options) => {
|
|
498
|
+
const prompt = task.join(' ').trim();
|
|
499
|
+
if (!prompt) {
|
|
500
|
+
console.error('Error: task cannot be empty');
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
await runPipeline(prompt, options);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const invokedPath = process.argv[1] ? fs.realpathSync(process.argv[1]) : '';
|
|
507
|
+
if (invokedPath === __filename) {
|
|
508
|
+
program.parse();
|
|
509
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@welluable/orch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI orchestrator: triage → research → plan → implement",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "main.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"orch": "./main.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"main.js",
|
|
12
|
+
"lib/**",
|
|
13
|
+
"agents/**"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test \"test/**/*.test.js\"",
|
|
17
|
+
"commit": "agn 'commit with a detailed message and push'",
|
|
18
|
+
"docs": "orch --quick 'Update README.md and orch --help so they match the current CLI. Only update what changes and add what is missing.' --agent agn"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/Welluable/orch.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/Welluable/orch/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/Welluable/orch#readme",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"commander": "^15.0.0",
|
|
36
|
+
"ora": "^9.4.1"
|
|
37
|
+
}
|
|
38
|
+
}
|