@markjaquith/agency 2.19.0 → 2.20.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 +44 -5
- package/cli.ts +2 -0
- package/package.json +1 -1
- package/skills/agency/SKILL.md +4 -1
- package/src/cli-parser.test.ts +16 -0
- package/src/cli-parser.ts +10 -2
- package/src/commands/work.test.ts +105 -16
- package/src/commands/work.ts +99 -28
- package/src/services/WorkbaseService.test.ts +19 -0
- package/src/services/WorkbaseService.ts +12 -0
- package/src/workbase/runner-command.test.ts +79 -0
- package/src/workbase/runner-command.ts +118 -0
- package/src/workbase/schemas.test.ts +26 -0
- package/src/workbase/schemas.ts +15 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ or write.
|
|
|
10
10
|
- [Bun](https://bun.sh) 1.0 or newer
|
|
11
11
|
- Git
|
|
12
12
|
- [GitHub CLI](https://cli.github.com/) for `agency pr create`
|
|
13
|
-
- OpenCode
|
|
13
|
+
- OpenCode, Claude Code, or a configured runner for `agency work`
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
@@ -145,6 +145,41 @@ The configured command applies only to the writable checkout. Supplemental
|
|
|
145
145
|
read-only repositories remain detached Git worktrees at their declared refs so
|
|
146
146
|
they do not acquire writable branches.
|
|
147
147
|
|
|
148
|
+
### Agent Runners
|
|
149
|
+
|
|
150
|
+
OpenCode and Claude Code are built-in runner presets. Select either preset or a
|
|
151
|
+
configured runner with `agency work --runner <name>`. A launch is fresh unless
|
|
152
|
+
`AGENCY_SESSION_ID` is already set; resumed launches use the runner's
|
|
153
|
+
`resumeCommand` when configured. The built-in presets use `--continue` only for
|
|
154
|
+
resumed launches.
|
|
155
|
+
|
|
156
|
+
Custom runners are direct argv commands, never shell snippets:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"version": 2,
|
|
161
|
+
"runners": {
|
|
162
|
+
"custom": {
|
|
163
|
+
"command": ["my-agent", "--prompt", "{prompt}"],
|
|
164
|
+
"resumeCommand": ["my-agent", "resume", "{sessionId}", "{prompt}"],
|
|
165
|
+
"environment": { "MY_AGENT_TARGET": "{target}" }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Available placeholders are `{prompt}`, `{workbase}`, `{target}`, `{task}`,
|
|
172
|
+
`{phase}`, `{claimant}`, `{sessionId}`, and `{claimRevision}`. Task and phase
|
|
173
|
+
placeholders are empty when they do not apply. If `resumeCommand` is omitted,
|
|
174
|
+
the fresh command is also used for resumed sessions.
|
|
175
|
+
|
|
176
|
+
Every runner receives the same `AGENCY_RUNNER`, `AGENCY_CLAIMANT`,
|
|
177
|
+
`AGENCY_SESSION_ID`, `AGENCY_CLAIM_REVISION`, `AGENCY_WORKBASE`, `AGENCY_TARGET`,
|
|
178
|
+
`AGENCY_TASK_ID`, `AGENCY_PHASE_ID`, and `AGENCY_PROMPT` environment. Configured
|
|
179
|
+
environment is added without overriding these normalized values.
|
|
180
|
+
`--print-command` prints the exact cwd and argv plus non-secret environment keys
|
|
181
|
+
without launching the runner.
|
|
182
|
+
|
|
148
183
|
### Custom Chooser Command
|
|
149
184
|
|
|
150
185
|
Interactive selection uses a native numbered chooser by default. To use an
|
|
@@ -500,9 +535,9 @@ replaced with a revision-guarded claim.
|
|
|
500
535
|
|
|
501
536
|
`agency work` claims an execution unit before launching its agent. Set
|
|
502
537
|
`AGENCY_CLAIMANT`, `AGENCY_RUNNER`, or `AGENCY_SESSION_ID` to supply orchestrator
|
|
503
|
-
identities; otherwise Agency derives them from the user
|
|
504
|
-
|
|
505
|
-
|
|
538
|
+
identities; otherwise Agency derives them from the user and process. The selected
|
|
539
|
+
runner name is recorded on the claim. The launched agent receives the normalized
|
|
540
|
+
runner environment documented above for a later release or finish operation.
|
|
506
541
|
|
|
507
542
|
### Archive
|
|
508
543
|
|
|
@@ -520,7 +555,7 @@ before moving files, refuses dirty worktrees, and preserves branches.
|
|
|
520
555
|
### Work and Pull Requests
|
|
521
556
|
|
|
522
557
|
```text
|
|
523
|
-
agency work [<directory> | --epic <epic-id>] [--
|
|
558
|
+
agency work [<directory> | --epic <epic-id>] [--runner <name>] [--print-command]
|
|
524
559
|
agency work prepare [target] [--dry-run] [--json]
|
|
525
560
|
agency pr create <task-id> [phase-id] [--draft] [--json]
|
|
526
561
|
```
|
|
@@ -531,6 +566,10 @@ workbase, Agency first presents the registered workbases, then the selected
|
|
|
531
566
|
workbase's hierarchy. If `fzf` is not installed, Agency prints the available
|
|
532
567
|
choices and asks for an explicit directory.
|
|
533
568
|
|
|
569
|
+
OpenCode is the default runner, with automatic Claude fallback when neither is
|
|
570
|
+
explicitly selected. `--opencode` and `--claude` remain aliases for requiring
|
|
571
|
+
their built-in presets.
|
|
572
|
+
|
|
534
573
|
`agency work prepare` resolves an execution unit and creates or reuses its
|
|
535
574
|
writable and reference worktrees without launching an agent or changing status.
|
|
536
575
|
Its JSON result includes document and checkout paths, resolved commits, actions,
|
package/cli.ts
CHANGED
|
@@ -419,6 +419,8 @@ const commands: Record<string, Command> = {
|
|
|
419
419
|
verbose: options.verbose,
|
|
420
420
|
opencode: options.opencode,
|
|
421
421
|
claude: options.claude,
|
|
422
|
+
runner: options.runner,
|
|
423
|
+
printCommand: options["print-command"],
|
|
422
424
|
force: options.force,
|
|
423
425
|
inputAllowed: options.inputAllowed,
|
|
424
426
|
cwd: options.cwd,
|
package/package.json
CHANGED
package/skills/agency/SKILL.md
CHANGED
|
@@ -279,7 +279,10 @@ agency work --epic <epic-id>
|
|
|
279
279
|
agency work --task <task-id> [--phase <phase-id>] --workbase <selector>
|
|
280
280
|
```
|
|
281
281
|
|
|
282
|
-
Use `--
|
|
282
|
+
Use `--runner <name>` to select a configured runner or the built-in `opencode`
|
|
283
|
+
and `claude` presets. `--opencode` and `--claude` remain shorthand. Use
|
|
284
|
+
`--print-command` to inspect the resolved cwd, argv, and non-secret environment
|
|
285
|
+
without launching. This command fetches
|
|
283
286
|
repositories for execution targets, creates or reuses their worktrees, and
|
|
284
287
|
replaces the current process with the selected agent. With no directory it opens
|
|
285
288
|
an `fzf` picker containing the workbase hierarchy. Pass `.`, or another
|
package/src/cli-parser.test.ts
CHANGED
|
@@ -321,6 +321,22 @@ describe("strict CLI parsing", () => {
|
|
|
321
321
|
)
|
|
322
322
|
})
|
|
323
323
|
|
|
324
|
+
test("accepts runner selection and command inspection for work", () => {
|
|
325
|
+
expect(
|
|
326
|
+
parseCli(["work", "example", "--runner", "custom", "--print-command"]),
|
|
327
|
+
).toMatchObject({
|
|
328
|
+
commandName: "work",
|
|
329
|
+
args: ["example"],
|
|
330
|
+
values: { runner: "custom", "print-command": true },
|
|
331
|
+
})
|
|
332
|
+
expect(() =>
|
|
333
|
+
parseCli(["work", "example", "--runner", "custom", "--claude"]),
|
|
334
|
+
).toThrow("cannot be combined")
|
|
335
|
+
expect(() => parseCli(["work", "prepare", "--print-command"])).toThrow(
|
|
336
|
+
"cannot be combined",
|
|
337
|
+
)
|
|
338
|
+
})
|
|
339
|
+
|
|
324
340
|
test("accepts repeatable graph filters and rejects output conflicts", () => {
|
|
325
341
|
expect(
|
|
326
342
|
parseCli([
|
package/src/cli-parser.ts
CHANGED
|
@@ -439,19 +439,21 @@ const commands = {
|
|
|
439
439
|
},
|
|
440
440
|
work: {
|
|
441
441
|
usage:
|
|
442
|
-
"agency work [<directory-or-task-id> | --epic <epic-id>] | agency work prepare [target] [--dry-run] [--json]",
|
|
442
|
+
"agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>] | agency work prepare [target] [--dry-run] [--json]",
|
|
443
443
|
options: {
|
|
444
444
|
...commonOptions,
|
|
445
445
|
...entitySelectorOptions,
|
|
446
446
|
json: { type: "boolean" },
|
|
447
447
|
"dry-run": { type: "boolean" },
|
|
448
|
+
runner: { type: "string" },
|
|
449
|
+
"print-command": { type: "boolean" },
|
|
448
450
|
opencode: { type: "boolean" },
|
|
449
451
|
claude: { type: "boolean" },
|
|
450
452
|
force: { type: "boolean" },
|
|
451
453
|
},
|
|
452
454
|
command: {
|
|
453
455
|
usage:
|
|
454
|
-
"agency work [<directory-or-task-id> | --epic <epic-id>] | agency work prepare [target] [--dry-run] [--json]",
|
|
456
|
+
"agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>] | agency work prepare [target] [--dry-run] [--json]",
|
|
455
457
|
minArgs: 0,
|
|
456
458
|
maxArgs: 2,
|
|
457
459
|
options: [
|
|
@@ -460,12 +462,16 @@ const commands = {
|
|
|
460
462
|
"epic",
|
|
461
463
|
"task",
|
|
462
464
|
"phase",
|
|
465
|
+
"runner",
|
|
466
|
+
"print-command",
|
|
463
467
|
"opencode",
|
|
464
468
|
"claude",
|
|
465
469
|
"force",
|
|
466
470
|
],
|
|
467
471
|
conflicts: [
|
|
468
472
|
["opencode", "claude"],
|
|
473
|
+
["runner", "opencode"],
|
|
474
|
+
["runner", "claude"],
|
|
469
475
|
["epic", "$positional"],
|
|
470
476
|
],
|
|
471
477
|
},
|
|
@@ -998,6 +1004,8 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
998
1004
|
(parsed.values.epic ||
|
|
999
1005
|
parsed.values.opencode ||
|
|
1000
1006
|
parsed.values.claude ||
|
|
1007
|
+
parsed.values.runner ||
|
|
1008
|
+
parsed.values["print-command"] ||
|
|
1001
1009
|
parsed.values.force))
|
|
1002
1010
|
) {
|
|
1003
1011
|
throw usageError(
|
|
@@ -47,8 +47,16 @@ const multiPhaseWorkspace: ExecutionWorkspace = {
|
|
|
47
47
|
interface HarnessOptions {
|
|
48
48
|
readonly workspace?: ExecutionWorkspace
|
|
49
49
|
readonly materializeError?: Error
|
|
50
|
-
readonly available?:
|
|
50
|
+
readonly available?: Readonly<Record<string, boolean>>
|
|
51
51
|
readonly chooserCommand?: readonly string[]
|
|
52
|
+
readonly runners?: Record<
|
|
53
|
+
string,
|
|
54
|
+
{
|
|
55
|
+
command: readonly [string, ...string[]]
|
|
56
|
+
resumeCommand?: readonly [string, ...string[]]
|
|
57
|
+
environment?: Record<string, string>
|
|
58
|
+
}
|
|
59
|
+
>
|
|
52
60
|
readonly multiPhaseTasks?: readonly string[]
|
|
53
61
|
readonly epicRecords?: readonly any[]
|
|
54
62
|
readonly taskRecords?: readonly any[]
|
|
@@ -72,6 +80,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
72
80
|
args: readonly string[]
|
|
73
81
|
cwd: string
|
|
74
82
|
}> = []
|
|
83
|
+
const launchEnvironments: Array<Readonly<Record<string, string>>> = []
|
|
75
84
|
const materializeOptions: Array<
|
|
76
85
|
Parameters<WorktreeService["materialize"]>[3]
|
|
77
86
|
> = []
|
|
@@ -105,6 +114,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
105
114
|
config: {
|
|
106
115
|
version: 2 as const,
|
|
107
116
|
chooserCommand: options.chooserCommand,
|
|
117
|
+
runners: options.runners,
|
|
108
118
|
},
|
|
109
119
|
}),
|
|
110
120
|
}
|
|
@@ -204,7 +214,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
204
214
|
isDirectory: (path: string) =>
|
|
205
215
|
Effect.succeed(options.existingDirectories?.includes(path) ?? true),
|
|
206
216
|
runCommand: (args: readonly string[]) => {
|
|
207
|
-
const cli = args[1]
|
|
217
|
+
const cli = args[1]!
|
|
208
218
|
events.push(`probe:${cli}`)
|
|
209
219
|
probes.push(cli)
|
|
210
220
|
return Effect.succeed({
|
|
@@ -214,9 +224,15 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
214
224
|
})
|
|
215
225
|
},
|
|
216
226
|
}
|
|
217
|
-
const launch = (
|
|
227
|
+
const launch = (
|
|
228
|
+
cli: string,
|
|
229
|
+
args: readonly string[],
|
|
230
|
+
cwd: string,
|
|
231
|
+
environment: Readonly<Record<string, string>>,
|
|
232
|
+
) => {
|
|
218
233
|
events.push(`launch:${cli}`)
|
|
219
234
|
launches.push({ cli, args, cwd })
|
|
235
|
+
launchEnvironments.push(environment)
|
|
220
236
|
}
|
|
221
237
|
const defaultPick: PickWorkTarget = () => Effect.succeed(null)
|
|
222
238
|
const defaultPickWorkbase: PickWorkbase = () => Effect.succeed(null)
|
|
@@ -257,6 +273,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
257
273
|
events,
|
|
258
274
|
probes,
|
|
259
275
|
launches,
|
|
276
|
+
launchEnvironments,
|
|
260
277
|
materializeOptions,
|
|
261
278
|
statusUpdates,
|
|
262
279
|
shownTasks,
|
|
@@ -353,7 +370,6 @@ describe("work command", () => {
|
|
|
353
370
|
cli: "opencode",
|
|
354
371
|
args: [
|
|
355
372
|
"opencode",
|
|
356
|
-
"--continue",
|
|
357
373
|
"--prompt",
|
|
358
374
|
"Work on the epic. Read /workbase/epics/delivery/EPIC.md.",
|
|
359
375
|
],
|
|
@@ -403,7 +419,6 @@ describe("work command", () => {
|
|
|
403
419
|
cli: "opencode",
|
|
404
420
|
args: [
|
|
405
421
|
"opencode",
|
|
406
|
-
"--continue",
|
|
407
422
|
"--prompt",
|
|
408
423
|
"Work on the task. Read /workbase/tasks/delivery/TASK.md.",
|
|
409
424
|
],
|
|
@@ -429,7 +444,6 @@ describe("work command", () => {
|
|
|
429
444
|
cli: "opencode",
|
|
430
445
|
args: [
|
|
431
446
|
"opencode",
|
|
432
|
-
"--continue",
|
|
433
447
|
"--prompt",
|
|
434
448
|
"Start the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
435
449
|
],
|
|
@@ -596,7 +610,7 @@ describe("work command", () => {
|
|
|
596
610
|
|
|
597
611
|
await expect(
|
|
598
612
|
harness.run({ taskId: "example", opencode: true, claude: true }),
|
|
599
|
-
).rejects.toThrow("Cannot
|
|
613
|
+
).rejects.toThrow("Cannot combine --runner, --opencode, and --claude")
|
|
600
614
|
expect(harness.events).toEqual([])
|
|
601
615
|
})
|
|
602
616
|
|
|
@@ -624,7 +638,6 @@ describe("work command", () => {
|
|
|
624
638
|
cli: "opencode",
|
|
625
639
|
args: [
|
|
626
640
|
"opencode",
|
|
627
|
-
"--continue",
|
|
628
641
|
"--prompt",
|
|
629
642
|
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
630
643
|
],
|
|
@@ -638,14 +651,18 @@ describe("work command", () => {
|
|
|
638
651
|
])
|
|
639
652
|
})
|
|
640
653
|
|
|
641
|
-
test("
|
|
654
|
+
test("resumes OpenCode deterministically when a session identity exists", async () => {
|
|
642
655
|
const harness = createHarness({ workspace: multiPhaseWorkspace })
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
656
|
+
process.env.AGENCY_SESSION_ID = "existing-session"
|
|
657
|
+
try {
|
|
658
|
+
await harness.run({
|
|
659
|
+
taskId: "example",
|
|
660
|
+
phaseId: "implementation",
|
|
661
|
+
opencode: true,
|
|
662
|
+
})
|
|
663
|
+
} finally {
|
|
664
|
+
delete process.env.AGENCY_SESSION_ID
|
|
665
|
+
}
|
|
649
666
|
|
|
650
667
|
expect(harness.launches[0]).toEqual({
|
|
651
668
|
cli: "opencode",
|
|
@@ -659,6 +676,78 @@ describe("work command", () => {
|
|
|
659
676
|
})
|
|
660
677
|
})
|
|
661
678
|
|
|
679
|
+
test("expands a named runner with shared context and claim identity", async () => {
|
|
680
|
+
const harness = createHarness({
|
|
681
|
+
available: { codex: true },
|
|
682
|
+
runners: {
|
|
683
|
+
custom: {
|
|
684
|
+
command: ["codex", "--task", "{task}", "{prompt}"],
|
|
685
|
+
environment: {
|
|
686
|
+
CUSTOM_TARGET: "{target}",
|
|
687
|
+
AGENCY_TARGET: "cannot-override",
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
await harness.run({ taskId: "example", runner: "custom" })
|
|
694
|
+
|
|
695
|
+
expect(harness.probes).toEqual(["codex"])
|
|
696
|
+
expect(harness.launches[0]).toEqual({
|
|
697
|
+
cli: "codex",
|
|
698
|
+
args: [
|
|
699
|
+
"codex",
|
|
700
|
+
"--task",
|
|
701
|
+
"example",
|
|
702
|
+
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
703
|
+
],
|
|
704
|
+
cwd: singlePhaseWorkspace.writablePath,
|
|
705
|
+
})
|
|
706
|
+
expect(harness.launchEnvironments[0]).toMatchObject({
|
|
707
|
+
AGENCY_RUNNER: "custom",
|
|
708
|
+
AGENCY_CLAIMANT: process.env.USER ?? "agency",
|
|
709
|
+
AGENCY_WORKBASE: "/workbase",
|
|
710
|
+
AGENCY_TARGET: "execution-unit:task/example",
|
|
711
|
+
AGENCY_TASK_ID: "example",
|
|
712
|
+
AGENCY_PHASE_ID: "",
|
|
713
|
+
AGENCY_CLAIM_REVISION: "1".repeat(64),
|
|
714
|
+
CUSTOM_TARGET: "execution-unit:task/example",
|
|
715
|
+
})
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
test("prints the exact command contract without launching and omits secrets", async () => {
|
|
719
|
+
const harness = createHarness({
|
|
720
|
+
available: { agent: true },
|
|
721
|
+
runners: {
|
|
722
|
+
custom: {
|
|
723
|
+
command: ["agent", "{prompt}"],
|
|
724
|
+
environment: {
|
|
725
|
+
VISIBLE: "{task}",
|
|
726
|
+
API_TOKEN: "do-not-print",
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
const output = await captureLogs(() =>
|
|
733
|
+
harness.run({
|
|
734
|
+
taskId: "example",
|
|
735
|
+
runner: "custom",
|
|
736
|
+
printCommand: true,
|
|
737
|
+
}),
|
|
738
|
+
)
|
|
739
|
+
const printed = JSON.parse(output.join("\n"))
|
|
740
|
+
|
|
741
|
+
expect(harness.launches).toEqual([])
|
|
742
|
+
expect(printed.cwd).toBe(singlePhaseWorkspace.writablePath)
|
|
743
|
+
expect(printed.argv).toEqual([
|
|
744
|
+
"agent",
|
|
745
|
+
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
746
|
+
])
|
|
747
|
+
expect(printed.environment.VISIBLE).toBe("example")
|
|
748
|
+
expect(printed.environment.API_TOKEN).toBeUndefined()
|
|
749
|
+
})
|
|
750
|
+
|
|
662
751
|
test("automatically falls back to Claude", async () => {
|
|
663
752
|
const harness = createHarness({ available: { opencode: false } })
|
|
664
753
|
|
|
@@ -729,7 +818,7 @@ describe("work command", () => {
|
|
|
729
818
|
verboseHarness.run({ taskId: "example", verbose: true }),
|
|
730
819
|
)
|
|
731
820
|
expect(verboseLogs).toEqual([
|
|
732
|
-
"Launching command: opencode --
|
|
821
|
+
"Launching command: opencode --prompt 'Start the task. Read /workbase/tasks/example/TASK.md.' (cwd: /workbase/tasks/example/code/agency)",
|
|
733
822
|
])
|
|
734
823
|
expect(verboseHarness.materializeOptions[0]?.verbose).toBe(true)
|
|
735
824
|
|
package/src/commands/work.ts
CHANGED
|
@@ -23,6 +23,11 @@ import {
|
|
|
23
23
|
resolveWorkbase,
|
|
24
24
|
type PickWorkbase,
|
|
25
25
|
} from "../workbase/workbase-choice"
|
|
26
|
+
import {
|
|
27
|
+
printableEnvironment,
|
|
28
|
+
resolveRunnerCommand,
|
|
29
|
+
runnerEnvironment,
|
|
30
|
+
} from "../workbase/runner-command"
|
|
26
31
|
|
|
27
32
|
interface WorkOptions extends BaseCommandOptions {
|
|
28
33
|
readonly directory?: string
|
|
@@ -31,10 +36,17 @@ interface WorkOptions extends BaseCommandOptions {
|
|
|
31
36
|
readonly epicId?: string
|
|
32
37
|
readonly opencode?: boolean
|
|
33
38
|
readonly claude?: boolean
|
|
39
|
+
readonly runner?: string
|
|
40
|
+
readonly printCommand?: boolean
|
|
34
41
|
readonly force?: boolean
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
type LaunchAgent = (
|
|
44
|
+
type LaunchAgent = (
|
|
45
|
+
cli: string,
|
|
46
|
+
args: readonly string[],
|
|
47
|
+
cwd: string,
|
|
48
|
+
environment: Readonly<Record<string, string>>,
|
|
49
|
+
) => void
|
|
38
50
|
|
|
39
51
|
const formatCommand = (args: readonly string[]) =>
|
|
40
52
|
args
|
|
@@ -68,13 +80,15 @@ export const work = (
|
|
|
68
80
|
pickBase: PickWorkbase = pickWorkbase,
|
|
69
81
|
) =>
|
|
70
82
|
Effect.gen(function* () {
|
|
71
|
-
if (
|
|
83
|
+
if (
|
|
84
|
+
(options.opencode && options.claude) ||
|
|
85
|
+
(options.runner && (options.opencode || options.claude))
|
|
86
|
+
) {
|
|
72
87
|
return yield* Effect.fail(
|
|
73
|
-
new Error("Cannot
|
|
88
|
+
new Error("Cannot combine --runner, --opencode, and --claude"),
|
|
74
89
|
)
|
|
75
90
|
}
|
|
76
|
-
const
|
|
77
|
-
const previousClaimRevision = process.env.AGENCY_CLAIM_REVISION
|
|
91
|
+
const previousEnvironment = { ...process.env }
|
|
78
92
|
if (
|
|
79
93
|
options.epicId &&
|
|
80
94
|
(options.directory || options.taskId || options.phaseId)
|
|
@@ -104,6 +118,7 @@ export const work = (
|
|
|
104
118
|
const inputAllowed = options.inputAllowed ?? true
|
|
105
119
|
const root = yield* resolveWorkbase(startPath, pickBase, inputAllowed)
|
|
106
120
|
if (!root) return
|
|
121
|
+
const { config } = yield* workbase.loadConfig(root)
|
|
107
122
|
|
|
108
123
|
let target: WorkTarget | null = null
|
|
109
124
|
if (options.epicId) {
|
|
@@ -199,7 +214,6 @@ export const work = (
|
|
|
199
214
|
new Error("No ready work targets found in this workbase"),
|
|
200
215
|
)
|
|
201
216
|
}
|
|
202
|
-
const { config } = yield* workbase.loadConfig(root)
|
|
203
217
|
target = yield* pick(choices, config.chooserCommand)
|
|
204
218
|
if (!target) return
|
|
205
219
|
}
|
|
@@ -233,13 +247,49 @@ export const work = (
|
|
|
233
247
|
launchPath = workspace.writablePath
|
|
234
248
|
}
|
|
235
249
|
|
|
236
|
-
const
|
|
237
|
-
|
|
250
|
+
const explicitlyRequested = Boolean(
|
|
251
|
+
options.runner ||
|
|
252
|
+
options.opencode ||
|
|
253
|
+
options.claude ||
|
|
254
|
+
process.env.AGENCY_RUNNER,
|
|
255
|
+
)
|
|
256
|
+
let runner =
|
|
257
|
+
options.runner ??
|
|
258
|
+
process.env.AGENCY_RUNNER ??
|
|
259
|
+
(options.claude ? "claude" : "opencode")
|
|
260
|
+
const claimant = process.env.AGENCY_CLAIMANT ?? process.env.USER ?? "agency"
|
|
261
|
+
const sessionId =
|
|
262
|
+
process.env.AGENCY_SESSION_ID ?? `${process.pid}-${Date.now()}`
|
|
263
|
+
const resume = process.env.AGENCY_SESSION_ID !== undefined
|
|
264
|
+
let claimRevision = ""
|
|
265
|
+
let variables = {
|
|
266
|
+
prompt,
|
|
267
|
+
workbase: root,
|
|
268
|
+
target: targetNodeId(target),
|
|
269
|
+
task: target.kind === "epic" ? "" : target.taskId,
|
|
270
|
+
phase: target.kind === "phase" ? target.phaseId : "",
|
|
271
|
+
claimant,
|
|
272
|
+
sessionId,
|
|
273
|
+
claimRevision,
|
|
274
|
+
}
|
|
275
|
+
let resolved = resolveRunnerCommand(
|
|
276
|
+
runner,
|
|
277
|
+
config.runners,
|
|
278
|
+
variables,
|
|
279
|
+
resume,
|
|
280
|
+
)
|
|
281
|
+
let cli = resolved.argv[0]!
|
|
238
282
|
let available = yield* fs.runCommand(["which", cli], {
|
|
239
283
|
captureOutput: true,
|
|
240
284
|
})
|
|
241
|
-
if (
|
|
242
|
-
|
|
285
|
+
if (
|
|
286
|
+
available.exitCode !== 0 &&
|
|
287
|
+
!explicitlyRequested &&
|
|
288
|
+
runner === "opencode"
|
|
289
|
+
) {
|
|
290
|
+
runner = "claude"
|
|
291
|
+
resolved = resolveRunnerCommand(runner, config.runners, variables, resume)
|
|
292
|
+
cli = resolved.argv[0]!
|
|
243
293
|
available = yield* fs.runCommand(["which", cli], { captureOutput: true })
|
|
244
294
|
}
|
|
245
295
|
if (available.exitCode !== 0) {
|
|
@@ -251,36 +301,55 @@ export const work = (
|
|
|
251
301
|
) {
|
|
252
302
|
const phaseId = target.kind === "phase" ? target.phaseId : undefined
|
|
253
303
|
const current = yield* claims.inspect(target.taskId, phaseId, root)
|
|
254
|
-
const sessionId =
|
|
255
|
-
process.env.AGENCY_SESSION_ID ?? `${process.pid}-${Date.now()}`
|
|
256
304
|
const acquired = yield* claims.claim(
|
|
257
305
|
{
|
|
258
306
|
taskId: target.taskId,
|
|
259
307
|
...(phaseId ? { phaseId } : {}),
|
|
260
|
-
claimant
|
|
261
|
-
runner
|
|
308
|
+
claimant,
|
|
309
|
+
runner,
|
|
262
310
|
sessionId,
|
|
263
311
|
revision: current.revision,
|
|
264
312
|
},
|
|
265
313
|
root,
|
|
266
314
|
)
|
|
267
|
-
|
|
268
|
-
process.env.AGENCY_CLAIM_REVISION = acquired.revision
|
|
315
|
+
claimRevision = acquired.revision
|
|
269
316
|
}
|
|
270
317
|
|
|
271
|
-
|
|
272
|
-
|
|
318
|
+
variables = { ...variables, claimRevision }
|
|
319
|
+
resolved = resolveRunnerCommand(runner, config.runners, variables, resume)
|
|
320
|
+
cli = resolved.argv[0]!
|
|
321
|
+
const environment = {
|
|
322
|
+
...resolved.environment,
|
|
323
|
+
...runnerEnvironment(runner, variables),
|
|
324
|
+
}
|
|
325
|
+
if (options.printCommand) {
|
|
326
|
+
log(
|
|
327
|
+
JSON.stringify(
|
|
328
|
+
{
|
|
329
|
+
cwd: launchPath,
|
|
330
|
+
argv: resolved.argv,
|
|
331
|
+
environment: printableEnvironment(environment),
|
|
332
|
+
},
|
|
333
|
+
null,
|
|
334
|
+
2,
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
for (const [key, value] of Object.entries(environment)) {
|
|
340
|
+
process.env[key] = value
|
|
341
|
+
}
|
|
273
342
|
verboseLog(
|
|
274
|
-
`Launching command: ${formatCommand(
|
|
343
|
+
`Launching command: ${formatCommand(resolved.argv)} (cwd: ${launchPath})`,
|
|
275
344
|
)
|
|
276
345
|
try {
|
|
277
|
-
launch(cli,
|
|
346
|
+
launch(cli, resolved.argv, launchPath, environment)
|
|
278
347
|
} finally {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
348
|
+
for (const key of Object.keys(environment)) {
|
|
349
|
+
const previous = previousEnvironment[key]
|
|
350
|
+
if (previous === undefined) delete process.env[key]
|
|
351
|
+
else process.env[key] = previous
|
|
352
|
+
}
|
|
284
353
|
}
|
|
285
354
|
})
|
|
286
355
|
|
|
@@ -344,7 +413,7 @@ export const workPrepare = (options: WorkOptions = {}) =>
|
|
|
344
413
|
})
|
|
345
414
|
|
|
346
415
|
export const help = `
|
|
347
|
-
Usage: agency work [<directory-or-task-id> | --epic <epic-id>]
|
|
416
|
+
Usage: agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>]
|
|
348
417
|
agency work prepare [target] [--dry-run] [--json]
|
|
349
418
|
|
|
350
419
|
Launch an agent for an epic, task, or phase. With no directory, select one
|
|
@@ -362,8 +431,10 @@ Options:
|
|
|
362
431
|
--phase <id> Work on a phase selected with --task
|
|
363
432
|
--workbase <target> Select a workbase by ID, name, or path
|
|
364
433
|
--cwd <path> Resolve context from a specific directory
|
|
365
|
-
--
|
|
366
|
-
--
|
|
434
|
+
--runner <name> Select a configured runner or built-in preset
|
|
435
|
+
--print-command Print cwd, argv, and non-secret environment without launch
|
|
436
|
+
--opencode Require the OpenCode preset
|
|
437
|
+
--claude Require the Claude Code preset
|
|
367
438
|
--force Override readiness and terminal-state guards
|
|
368
439
|
--no-input Never open an interactive selector
|
|
369
440
|
|
|
@@ -235,6 +235,25 @@ describe("WorkbaseService", () => {
|
|
|
235
235
|
).rejects.toThrow("{worktree}")
|
|
236
236
|
})
|
|
237
237
|
|
|
238
|
+
test("rejects an unknown runner command placeholder", async () => {
|
|
239
|
+
await write(
|
|
240
|
+
root,
|
|
241
|
+
"agency.json",
|
|
242
|
+
JSON.stringify({
|
|
243
|
+
version: 2,
|
|
244
|
+
runners: { custom: { command: ["agent", "{unknown}"] } },
|
|
245
|
+
}),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
await expect(
|
|
249
|
+
runTestEffect(
|
|
250
|
+
WorkbaseService.pipe(
|
|
251
|
+
Effect.flatMap((service) => service.discover(root)),
|
|
252
|
+
),
|
|
253
|
+
),
|
|
254
|
+
).rejects.toThrow("{unknown}")
|
|
255
|
+
})
|
|
256
|
+
|
|
238
257
|
test("validates a workbase with an epic and multi-phase task", async () => {
|
|
239
258
|
await write(root, "agency.json", '{"version":2}\n')
|
|
240
259
|
await mkdir(join(root, "repos/agency"), { recursive: true })
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type WorkbaseRegistration,
|
|
22
22
|
} from "../workbase/schemas"
|
|
23
23
|
import { validateWorktreeCreateCommand } from "../workbase/worktree-command"
|
|
24
|
+
import { validateRunners } from "../workbase/runner-command"
|
|
24
25
|
|
|
25
26
|
class WorkbaseNotFoundError extends Data.TaggedError("WorkbaseNotFoundError")<{
|
|
26
27
|
readonly message: string
|
|
@@ -277,6 +278,17 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
277
278
|
})
|
|
278
279
|
}
|
|
279
280
|
}
|
|
281
|
+
try {
|
|
282
|
+
validateRunners(decoded.value.runners)
|
|
283
|
+
} catch (cause) {
|
|
284
|
+
return yield* new WorkbaseConfigError({
|
|
285
|
+
path: configPath,
|
|
286
|
+
message:
|
|
287
|
+
cause instanceof Error
|
|
288
|
+
? cause.message
|
|
289
|
+
: "Invalid runner configuration",
|
|
290
|
+
})
|
|
291
|
+
}
|
|
280
292
|
return current
|
|
281
293
|
}
|
|
282
294
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
printableEnvironment,
|
|
4
|
+
resolveRunnerCommand,
|
|
5
|
+
runnerEnvironment,
|
|
6
|
+
validateRunners,
|
|
7
|
+
} from "./runner-command"
|
|
8
|
+
|
|
9
|
+
const variables = {
|
|
10
|
+
prompt: "Read the task.",
|
|
11
|
+
workbase: "/workbase",
|
|
12
|
+
target: "execution-unit:phase/task/build",
|
|
13
|
+
task: "task",
|
|
14
|
+
phase: "build",
|
|
15
|
+
claimant: "orchestrator",
|
|
16
|
+
sessionId: "session-1",
|
|
17
|
+
claimRevision: "revision-1",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("runner commands", () => {
|
|
21
|
+
test("uses deterministic fresh and resume commands for built-in presets", () => {
|
|
22
|
+
expect(
|
|
23
|
+
resolveRunnerCommand("opencode", undefined, variables, false).argv,
|
|
24
|
+
).toEqual(["opencode", "--prompt", "Read the task."])
|
|
25
|
+
expect(
|
|
26
|
+
resolveRunnerCommand("opencode", undefined, variables, true).argv,
|
|
27
|
+
).toEqual(["opencode", "--continue", "--prompt", "Read the task."])
|
|
28
|
+
expect(
|
|
29
|
+
resolveRunnerCommand("claude", undefined, variables, true).argv,
|
|
30
|
+
).toEqual(["claude", "--continue", "Read the task."])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test("expands configured argv and environment without a shell", () => {
|
|
34
|
+
const resolved = resolveRunnerCommand(
|
|
35
|
+
"custom",
|
|
36
|
+
{
|
|
37
|
+
custom: {
|
|
38
|
+
command: ["agent", "--target={target}", "{prompt}"],
|
|
39
|
+
environment: { CUSTOM_SESSION: "{sessionId}" },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
variables,
|
|
43
|
+
false,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
expect(resolved).toEqual({
|
|
47
|
+
argv: [
|
|
48
|
+
"agent",
|
|
49
|
+
"--target=execution-unit:phase/task/build",
|
|
50
|
+
"Read the task.",
|
|
51
|
+
],
|
|
52
|
+
environment: { CUSTOM_SESSION: "session-1" },
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("rejects unknown placeholders", () => {
|
|
57
|
+
expect(() =>
|
|
58
|
+
validateRunners({ custom: { command: ["agent", "{unknown}"] } }),
|
|
59
|
+
).toThrow("Unknown runner 'custom' placeholder: {unknown}")
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test("provides normalized Agency environment and filters secret values", () => {
|
|
63
|
+
const environment = {
|
|
64
|
+
...runnerEnvironment("custom", variables),
|
|
65
|
+
VISIBLE: "yes",
|
|
66
|
+
ACCESS_TOKEN: "secret",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
expect(environment).toMatchObject({
|
|
70
|
+
AGENCY_RUNNER: "custom",
|
|
71
|
+
AGENCY_CLAIMANT: "orchestrator",
|
|
72
|
+
AGENCY_TARGET: "execution-unit:phase/task/build",
|
|
73
|
+
AGENCY_TASK_ID: "task",
|
|
74
|
+
AGENCY_PHASE_ID: "build",
|
|
75
|
+
})
|
|
76
|
+
expect(printableEnvironment(environment).VISIBLE).toBe("yes")
|
|
77
|
+
expect(printableEnvironment(environment).ACCESS_TOKEN).toBeUndefined()
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { WorkbaseConfig } from "./schemas"
|
|
2
|
+
|
|
3
|
+
export interface RunnerCommandVariables {
|
|
4
|
+
readonly prompt: string
|
|
5
|
+
readonly workbase: string
|
|
6
|
+
readonly target: string
|
|
7
|
+
readonly task: string
|
|
8
|
+
readonly phase: string
|
|
9
|
+
readonly claimant: string
|
|
10
|
+
readonly sessionId: string
|
|
11
|
+
readonly claimRevision: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface RunnerDefinition {
|
|
15
|
+
readonly command: readonly string[]
|
|
16
|
+
readonly resumeCommand?: readonly string[]
|
|
17
|
+
readonly environment?: Readonly<Record<string, string>>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PLACEHOLDERS = new Set<keyof RunnerCommandVariables>([
|
|
21
|
+
"prompt",
|
|
22
|
+
"workbase",
|
|
23
|
+
"target",
|
|
24
|
+
"task",
|
|
25
|
+
"phase",
|
|
26
|
+
"claimant",
|
|
27
|
+
"sessionId",
|
|
28
|
+
"claimRevision",
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
const BUILTIN_RUNNERS: Readonly<Record<string, RunnerDefinition>> = {
|
|
32
|
+
opencode: {
|
|
33
|
+
command: ["opencode", "--prompt", "{prompt}"],
|
|
34
|
+
resumeCommand: ["opencode", "--continue", "--prompt", "{prompt}"],
|
|
35
|
+
},
|
|
36
|
+
claude: {
|
|
37
|
+
command: ["claude", "{prompt}"],
|
|
38
|
+
resumeCommand: ["claude", "--continue", "{prompt}"],
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const validateTemplate = (runner: string, value: string) => {
|
|
43
|
+
for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
|
|
44
|
+
const placeholder = match[1]!
|
|
45
|
+
if (!PLACEHOLDERS.has(placeholder as keyof RunnerCommandVariables)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Unknown runner '${runner}' placeholder: {${placeholder}}`,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const validateRunners = (runners: WorkbaseConfig["runners"]): void => {
|
|
54
|
+
for (const [name, runner] of Object.entries(runners ?? {})) {
|
|
55
|
+
for (const value of [
|
|
56
|
+
...runner.command,
|
|
57
|
+
...(runner.resumeCommand ?? []),
|
|
58
|
+
...Object.values(runner.environment ?? {}),
|
|
59
|
+
]) {
|
|
60
|
+
validateTemplate(name, value)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const expand = (value: string, variables: RunnerCommandVariables) =>
|
|
66
|
+
value.replaceAll(
|
|
67
|
+
/\{([^{}]+)\}/g,
|
|
68
|
+
(match, placeholder: string) =>
|
|
69
|
+
variables[placeholder as keyof RunnerCommandVariables] ?? match,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
export const resolveRunnerCommand = (
|
|
73
|
+
name: string,
|
|
74
|
+
configured: WorkbaseConfig["runners"],
|
|
75
|
+
variables: RunnerCommandVariables,
|
|
76
|
+
resume: boolean,
|
|
77
|
+
) => {
|
|
78
|
+
validateRunners(configured)
|
|
79
|
+
const definition = configured?.[name] ?? BUILTIN_RUNNERS[name]
|
|
80
|
+
if (!definition) throw new Error(`Unknown runner: ${name}`)
|
|
81
|
+
const template =
|
|
82
|
+
resume && definition.resumeCommand
|
|
83
|
+
? definition.resumeCommand
|
|
84
|
+
: definition.command
|
|
85
|
+
const argv = template.map((argument) => expand(argument, variables))
|
|
86
|
+
const environment = Object.fromEntries(
|
|
87
|
+
Object.entries(definition.environment ?? {}).map(([key, value]) => [
|
|
88
|
+
key,
|
|
89
|
+
expand(value, variables),
|
|
90
|
+
]),
|
|
91
|
+
)
|
|
92
|
+
return { argv, environment }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const runnerEnvironment = (
|
|
96
|
+
runner: string,
|
|
97
|
+
variables: RunnerCommandVariables,
|
|
98
|
+
): Record<string, string> => ({
|
|
99
|
+
AGENCY_RUNNER: runner,
|
|
100
|
+
AGENCY_CLAIMANT: variables.claimant,
|
|
101
|
+
AGENCY_SESSION_ID: variables.sessionId,
|
|
102
|
+
AGENCY_CLAIM_REVISION: variables.claimRevision,
|
|
103
|
+
AGENCY_WORKBASE: variables.workbase,
|
|
104
|
+
AGENCY_TARGET: variables.target,
|
|
105
|
+
AGENCY_TASK_ID: variables.task,
|
|
106
|
+
AGENCY_PHASE_ID: variables.phase,
|
|
107
|
+
AGENCY_PROMPT: variables.prompt,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const SECRET_NAME =
|
|
111
|
+
/(secret|token|password|credential|api[_-]?key|private[_-]?key)/i
|
|
112
|
+
|
|
113
|
+
export const printableEnvironment = (environment: Record<string, string>) =>
|
|
114
|
+
Object.fromEntries(
|
|
115
|
+
Object.entries(environment)
|
|
116
|
+
.filter(([key]) => !SECRET_NAME.test(key))
|
|
117
|
+
.sort(([left], [right]) => left.localeCompare(right)),
|
|
118
|
+
)
|
|
@@ -78,6 +78,32 @@ describe("body-of-work descriptions", () => {
|
|
|
78
78
|
})
|
|
79
79
|
})
|
|
80
80
|
|
|
81
|
+
describe("runner configuration", () => {
|
|
82
|
+
test("accepts named argv commands with resume commands and environment", () => {
|
|
83
|
+
const config = Schema.decodeUnknownSync(WorkbaseConfig)({
|
|
84
|
+
version: 2,
|
|
85
|
+
runners: {
|
|
86
|
+
custom: {
|
|
87
|
+
command: ["agent", "{prompt}"],
|
|
88
|
+
resumeCommand: ["agent", "resume", "{sessionId}"],
|
|
89
|
+
environment: { CUSTOM_TARGET: "{target}" },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
expect(config.runners?.custom?.command).toEqual(["agent", "{prompt}"])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test("rejects shell strings in place of argv arrays", () => {
|
|
98
|
+
expect(() =>
|
|
99
|
+
Schema.decodeUnknownSync(WorkbaseConfig)({
|
|
100
|
+
version: 2,
|
|
101
|
+
runners: { custom: { command: "agent {prompt}" } },
|
|
102
|
+
}),
|
|
103
|
+
).toThrow()
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
81
107
|
describe("work status", () => {
|
|
82
108
|
const supportedStatuses: Record<WorkStatus, true> = {
|
|
83
109
|
open: true,
|
package/src/workbase/schemas.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Schema } from "@effect/schema"
|
|
2
2
|
|
|
3
3
|
const NonEmptyString = Schema.String.pipe(Schema.minLength(1))
|
|
4
|
+
const EnvironmentName = NonEmptyString.pipe(
|
|
5
|
+
Schema.pattern(/^[A-Za-z_][A-Za-z0-9_]*$/),
|
|
6
|
+
)
|
|
4
7
|
|
|
5
8
|
const Description = Schema.optional(NonEmptyString)
|
|
6
9
|
|
|
@@ -52,6 +55,18 @@ export const WorkbaseConfig = Schema.Struct({
|
|
|
52
55
|
version: Schema.Literal(2),
|
|
53
56
|
chooserCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
|
|
54
57
|
worktreeCreateCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
|
|
58
|
+
runners: Schema.optional(
|
|
59
|
+
Schema.Record({
|
|
60
|
+
key: EntityId,
|
|
61
|
+
value: Schema.Struct({
|
|
62
|
+
command: Schema.NonEmptyArray(NonEmptyString),
|
|
63
|
+
resumeCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
|
|
64
|
+
environment: Schema.optional(
|
|
65
|
+
Schema.Record({ key: EnvironmentName, value: Schema.String }),
|
|
66
|
+
),
|
|
67
|
+
}),
|
|
68
|
+
}),
|
|
69
|
+
),
|
|
55
70
|
})
|
|
56
71
|
|
|
57
72
|
export const LegacyWorkbaseRegistry = Schema.Struct({
|