@markjaquith/agency 1.3.0 → 1.4.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/cli.ts +8 -1
- package/package.json +1 -1
- package/src/commands/loop.test.ts +95 -0
- package/src/commands/loop.ts +22 -7
package/cli.ts
CHANGED
|
@@ -409,11 +409,17 @@ const commands: Record<string, Command> = {
|
|
|
409
409
|
loop: {
|
|
410
410
|
name: "loop",
|
|
411
411
|
description: "Run a Ralph Wiggum loop to complete all tasks",
|
|
412
|
-
run: async (
|
|
412
|
+
run: async (args: string[], options: Record<string, any>) => {
|
|
413
413
|
if (options.help) {
|
|
414
414
|
console.log(loopHelp)
|
|
415
415
|
return
|
|
416
416
|
}
|
|
417
|
+
|
|
418
|
+
// Extract extra args (anything after --)
|
|
419
|
+
// Note: parseArgs with strict:false and allowPositionals:true will put
|
|
420
|
+
// args after -- in the positionals array
|
|
421
|
+
const extraArgs = args.length > 0 ? args : undefined
|
|
422
|
+
|
|
417
423
|
await runCommand(
|
|
418
424
|
loop({
|
|
419
425
|
silent: options.silent,
|
|
@@ -422,6 +428,7 @@ const commands: Record<string, Command> = {
|
|
|
422
428
|
minLoops: options["min-loops"],
|
|
423
429
|
opencode: options.opencode,
|
|
424
430
|
claude: options.claude,
|
|
431
|
+
extraArgs,
|
|
425
432
|
}),
|
|
426
433
|
)
|
|
427
434
|
},
|
package/package.json
CHANGED
|
@@ -523,4 +523,99 @@ describe("loop command", () => {
|
|
|
523
523
|
expect(capturedOptions.stderr).toBe("pipe")
|
|
524
524
|
})
|
|
525
525
|
})
|
|
526
|
+
|
|
527
|
+
describe("extra args pass-through", () => {
|
|
528
|
+
test("passes extra args to opencode", async () => {
|
|
529
|
+
const taskPath = join(tempDir, "TASK.md")
|
|
530
|
+
writeFileSync(taskPath, "# Test Task\n\n- [x] Already done")
|
|
531
|
+
|
|
532
|
+
let capturedArgs: string[] = []
|
|
533
|
+
const restore = mockCliTools({
|
|
534
|
+
onSpawn: (args) => {
|
|
535
|
+
capturedArgs = args
|
|
536
|
+
return {
|
|
537
|
+
exited: Promise.resolve(0),
|
|
538
|
+
exitCode: 0,
|
|
539
|
+
stdout: createEmptyStream(),
|
|
540
|
+
stderr: createEmptyStream(),
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
await runTestEffect(
|
|
546
|
+
loop({
|
|
547
|
+
silent: true,
|
|
548
|
+
maxLoops: 1,
|
|
549
|
+
extraArgs: ["--agent", "deep"],
|
|
550
|
+
}),
|
|
551
|
+
)
|
|
552
|
+
restore()
|
|
553
|
+
|
|
554
|
+
expect(capturedArgs[0]).toBe("opencode")
|
|
555
|
+
expect(capturedArgs[1]).toBe("run")
|
|
556
|
+
expect(capturedArgs[2]).toContain("Find the next logical task")
|
|
557
|
+
expect(capturedArgs.slice(-2)).toEqual(["--agent", "deep"])
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
test("passes extra args to claude", async () => {
|
|
561
|
+
const taskPath = join(tempDir, "TASK.md")
|
|
562
|
+
writeFileSync(taskPath, "# Test Task\n\n- [x] Already done")
|
|
563
|
+
|
|
564
|
+
let capturedArgs: string[] = []
|
|
565
|
+
const restore = mockCliTools({
|
|
566
|
+
hasClaude: true,
|
|
567
|
+
onSpawn: (args) => {
|
|
568
|
+
capturedArgs = args
|
|
569
|
+
return {
|
|
570
|
+
exited: Promise.resolve(0),
|
|
571
|
+
exitCode: 0,
|
|
572
|
+
stdout: createEmptyStream(),
|
|
573
|
+
stderr: createEmptyStream(),
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
await runTestEffect(
|
|
579
|
+
loop({
|
|
580
|
+
silent: true,
|
|
581
|
+
maxLoops: 1,
|
|
582
|
+
claude: true,
|
|
583
|
+
extraArgs: ["--arbitrary", "switches"],
|
|
584
|
+
}),
|
|
585
|
+
)
|
|
586
|
+
restore()
|
|
587
|
+
|
|
588
|
+
expect(capturedArgs[0]).toBe("claude")
|
|
589
|
+
expect(capturedArgs[1]).toContain("Find the next logical task")
|
|
590
|
+
expect(capturedArgs.slice(-2)).toEqual(["--arbitrary", "switches"])
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
test("works without extra args", async () => {
|
|
594
|
+
const taskPath = join(tempDir, "TASK.md")
|
|
595
|
+
writeFileSync(taskPath, "# Test Task\n\n- [x] Already done")
|
|
596
|
+
|
|
597
|
+
let capturedArgs: string[] = []
|
|
598
|
+
const restore = mockCliTools({
|
|
599
|
+
onSpawn: (args) => {
|
|
600
|
+
capturedArgs = args
|
|
601
|
+
return {
|
|
602
|
+
exited: Promise.resolve(0),
|
|
603
|
+
exitCode: 0,
|
|
604
|
+
stdout: createEmptyStream(),
|
|
605
|
+
stderr: createEmptyStream(),
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
await runTestEffect(loop({ silent: true, maxLoops: 1 }))
|
|
611
|
+
restore()
|
|
612
|
+
|
|
613
|
+
// Should just have the base args without extra args
|
|
614
|
+
expect(capturedArgs).toEqual([
|
|
615
|
+
"opencode",
|
|
616
|
+
"run",
|
|
617
|
+
expect.stringContaining("Find the next logical task"),
|
|
618
|
+
])
|
|
619
|
+
})
|
|
620
|
+
})
|
|
526
621
|
})
|
package/src/commands/loop.ts
CHANGED
|
@@ -31,6 +31,10 @@ interface LoopOptions extends BaseCommandOptions {
|
|
|
31
31
|
* Force use of Claude Code CLI
|
|
32
32
|
*/
|
|
33
33
|
claude?: boolean
|
|
34
|
+
/**
|
|
35
|
+
* Additional arguments to pass to the CLI tool
|
|
36
|
+
*/
|
|
37
|
+
extraArgs?: string[]
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
interface LoopState {
|
|
@@ -193,6 +197,7 @@ export const loop = (options: LoopOptions = {}) =>
|
|
|
193
197
|
gitRoot,
|
|
194
198
|
options.verbose ?? false,
|
|
195
199
|
verboseLog,
|
|
200
|
+
options.extraArgs,
|
|
196
201
|
).pipe(
|
|
197
202
|
Effect.map(() => true),
|
|
198
203
|
Effect.catchAll(() => Effect.succeed(false)),
|
|
@@ -210,6 +215,7 @@ export const loop = (options: LoopOptions = {}) =>
|
|
|
210
215
|
gitRoot,
|
|
211
216
|
options.verbose ?? false,
|
|
212
217
|
verboseLog,
|
|
218
|
+
options.extraArgs,
|
|
213
219
|
).pipe(
|
|
214
220
|
Effect.map(() => true),
|
|
215
221
|
Effect.catchAll(() => Effect.succeed(false)),
|
|
@@ -317,6 +323,7 @@ function runHarness(
|
|
|
317
323
|
gitRoot: string,
|
|
318
324
|
verbose: boolean,
|
|
319
325
|
verboseLog: (msg: string) => void,
|
|
326
|
+
extraArgs?: string[],
|
|
320
327
|
): Effect.Effect<void, Error> {
|
|
321
328
|
return Effect.gen(function* () {
|
|
322
329
|
const prompt =
|
|
@@ -326,9 +333,13 @@ function runHarness(
|
|
|
326
333
|
// claude: use `claude "prompt"` (positional argument)
|
|
327
334
|
const baseArgs = useOpencode ? [cliName, "run", prompt] : [cliName, prompt]
|
|
328
335
|
|
|
336
|
+
// Append extra args if provided
|
|
337
|
+
const cliArgs =
|
|
338
|
+
extraArgs && extraArgs.length > 0 ? [...baseArgs, ...extraArgs] : baseArgs
|
|
339
|
+
|
|
329
340
|
// Log the command being run when verbose
|
|
330
341
|
// Quote arguments that contain spaces for proper display
|
|
331
|
-
const quotedArgs =
|
|
342
|
+
const quotedArgs = cliArgs.map((arg) =>
|
|
332
343
|
arg.includes(" ") ? `"${arg}"` : arg,
|
|
333
344
|
)
|
|
334
345
|
verboseLog(`Running: ${quotedArgs.join(" ")}`)
|
|
@@ -336,7 +347,7 @@ function runHarness(
|
|
|
336
347
|
// When verbose, stream output directly to the terminal
|
|
337
348
|
// When not verbose, capture output silently
|
|
338
349
|
// Always inherit stdin so the process doesn't hang waiting for input
|
|
339
|
-
const result = yield* spawnProcess(
|
|
350
|
+
const result = yield* spawnProcess(cliArgs, {
|
|
340
351
|
cwd: gitRoot,
|
|
341
352
|
stdin: "inherit",
|
|
342
353
|
stdout: verbose ? "inherit" : "pipe",
|
|
@@ -356,7 +367,7 @@ function runHarness(
|
|
|
356
367
|
}
|
|
357
368
|
|
|
358
369
|
export const help = `
|
|
359
|
-
Usage: agency loop [options]
|
|
370
|
+
Usage: agency loop [options] [-- extra-args...]
|
|
360
371
|
|
|
361
372
|
Run a Ralph Wiggum loop that repeatedly invokes the harness (opencode or claude)
|
|
362
373
|
to work on tasks defined in TASK.md until all tasks are complete.
|
|
@@ -377,15 +388,19 @@ Options:
|
|
|
377
388
|
-s, --silent Suppress output messages
|
|
378
389
|
-v, --verbose Stream harness output to terminal
|
|
379
390
|
|
|
391
|
+
Pass-through Arguments:
|
|
392
|
+
Use -- to pass additional arguments to the underlying CLI tool.
|
|
393
|
+
Everything after -- will be forwarded to opencode or claude.
|
|
394
|
+
|
|
380
395
|
Examples:
|
|
381
|
-
agency loop
|
|
382
|
-
agency loop --max-loops 5
|
|
383
|
-
agency loop --
|
|
384
|
-
agency loop --min-loops 3 --max-loops 10 # Between 3-10 iterations
|
|
396
|
+
agency loop # Run until all tasks are done
|
|
397
|
+
agency loop --max-loops 5 # Stop after 5 iterations
|
|
398
|
+
agency loop -- --agent deep # Pass --agent deep to harness
|
|
385
399
|
|
|
386
400
|
Notes:
|
|
387
401
|
- Requires TASK.md to exist (run 'agency task' first)
|
|
388
402
|
- Each iteration calls the harness which handles its own commit
|
|
389
403
|
- The loop validates that completion claims match actual task status
|
|
390
404
|
- Use --min-loops as a safeguard if you know the minimum work required
|
|
405
|
+
- Arguments after -- are passed directly to the underlying tool
|
|
391
406
|
`
|