@markjaquith/agency 2.18.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 +55 -9
- package/cli.ts +22 -0
- package/package.json +1 -1
- package/skills/agency/SKILL.md +4 -1
- package/src/cli-parser.test.ts +53 -2
- package/src/cli-parser.ts +78 -14
- package/src/cli.test.ts +11 -1
- package/src/commands/epic.test.ts +22 -0
- package/src/commands/epic.ts +38 -2
- package/src/commands/phase.ts +52 -2
- package/src/commands/status.test.ts +44 -0
- package/src/commands/status.ts +51 -2
- package/src/commands/task-phase.test.ts +20 -0
- package/src/commands/task.ts +49 -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/utils/table.ts +19 -0
- package/src/work-view.test.ts +151 -0
- package/src/work-view.ts +253 -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
|
|
@@ -371,7 +406,7 @@ status, validation, graph export, reconciliation, and PR creation.
|
|
|
371
406
|
```text
|
|
372
407
|
agency epic create <id> --ticket-url <url> [--description <text>] [--json]
|
|
373
408
|
--repo <alias>:<ref> [--repo <alias>:<ref>...]
|
|
374
|
-
agency epic list [--json]
|
|
409
|
+
agency epic list [filters] [--json]
|
|
375
410
|
agency epic show <id> [--json]
|
|
376
411
|
```
|
|
377
412
|
|
|
@@ -425,7 +460,7 @@ fully independent of process cwd and prompts.
|
|
|
425
460
|
Inspect tasks:
|
|
426
461
|
|
|
427
462
|
```text
|
|
428
|
-
agency task list [--json]
|
|
463
|
+
agency task list [filters] [--json]
|
|
429
464
|
agency task show <id> [--json]
|
|
430
465
|
agency task status <id> <open|done|dropped> [--json]
|
|
431
466
|
```
|
|
@@ -452,7 +487,7 @@ agency phase create <task-id> <phase-id>
|
|
|
452
487
|
[--description <text>] [--reference <alias>:<ref>...]
|
|
453
488
|
[--depends-on <phase-id>...] [--first-phase <phase-id>] [--json]
|
|
454
489
|
|
|
455
|
-
agency phase list <task-id> [--json]
|
|
490
|
+
agency phase list <task-id> [filters] [--json]
|
|
456
491
|
agency phase show <task-id> <phase-id> [--json]
|
|
457
492
|
agency phase status <task-id> <phase-id> <open|done|dropped> [--json]
|
|
458
493
|
```
|
|
@@ -470,6 +505,13 @@ work before changing its outcome.
|
|
|
470
505
|
Delegation is now explicit: the claimant identifies the orchestrator and the
|
|
471
506
|
runner identifies the assigned agent.
|
|
472
507
|
|
|
508
|
+
Human list output is a compact table with lifecycle, readiness, parent,
|
|
509
|
+
repository, branch, recorded PR, and worktree state where applicable. List and
|
|
510
|
+
status views accept composable `--status <status>` and `--repository <alias>`
|
|
511
|
+
filters, plus `--ready`, `--blocked`, `--pr`, and `--no-pr`. Status and repository
|
|
512
|
+
filters are repeatable. Rows follow task and phase declaration order; plain text
|
|
513
|
+
labels remain complete without color or icon fonts.
|
|
514
|
+
|
|
473
515
|
### Claims
|
|
474
516
|
|
|
475
517
|
Claim mutations require the SHA-256 revision exposed by `agency context` or
|
|
@@ -493,9 +535,9 @@ replaced with a revision-guarded claim.
|
|
|
493
535
|
|
|
494
536
|
`agency work` claims an execution unit before launching its agent. Set
|
|
495
537
|
`AGENCY_CLAIMANT`, `AGENCY_RUNNER`, or `AGENCY_SESSION_ID` to supply orchestrator
|
|
496
|
-
identities; otherwise Agency derives them from the user
|
|
497
|
-
|
|
498
|
-
|
|
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.
|
|
499
541
|
|
|
500
542
|
### Archive
|
|
501
543
|
|
|
@@ -513,7 +555,7 @@ before moving files, refuses dirty worktrees, and preserves branches.
|
|
|
513
555
|
### Work and Pull Requests
|
|
514
556
|
|
|
515
557
|
```text
|
|
516
|
-
agency work [<directory> | --epic <epic-id>] [--
|
|
558
|
+
agency work [<directory> | --epic <epic-id>] [--runner <name>] [--print-command]
|
|
517
559
|
agency work prepare [target] [--dry-run] [--json]
|
|
518
560
|
agency pr create <task-id> [phase-id] [--draft] [--json]
|
|
519
561
|
```
|
|
@@ -524,6 +566,10 @@ workbase, Agency first presents the registered workbases, then the selected
|
|
|
524
566
|
workbase's hierarchy. If `fzf` is not installed, Agency prints the available
|
|
525
567
|
choices and asks for an explicit directory.
|
|
526
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
|
+
|
|
527
573
|
`agency work prepare` resolves an execution unit and creates or reuses its
|
|
528
574
|
writable and reference worktrees without launching an agent or changing status.
|
|
529
575
|
Its JSON result includes document and checkout paths, resolved commits, actions,
|
|
@@ -555,7 +601,7 @@ the owning `TASK.md` or `PHASE.md`.
|
|
|
555
601
|
### Status and Validation
|
|
556
602
|
|
|
557
603
|
```text
|
|
558
|
-
agency status [--json]
|
|
604
|
+
agency status [filters] [--json]
|
|
559
605
|
agency validate [path] [--json]
|
|
560
606
|
```
|
|
561
607
|
|
package/cli.ts
CHANGED
|
@@ -237,6 +237,11 @@ const commands: Record<string, Command> = {
|
|
|
237
237
|
description: options.description,
|
|
238
238
|
repos: options.repo,
|
|
239
239
|
json: options.json,
|
|
240
|
+
statuses: options.status,
|
|
241
|
+
repositories: options.repository,
|
|
242
|
+
ready: options.ready,
|
|
243
|
+
blocked: options.blocked,
|
|
244
|
+
pr: options.pr ? true : options["no-pr"] ? false : undefined,
|
|
240
245
|
silent: options.silent,
|
|
241
246
|
verbose: options.verbose,
|
|
242
247
|
cwd: options.cwd,
|
|
@@ -280,6 +285,11 @@ const commands: Record<string, Command> = {
|
|
|
280
285
|
dependsOn: options["depends-on"],
|
|
281
286
|
firstPhase: options["first-phase"],
|
|
282
287
|
json: options.json,
|
|
288
|
+
statuses: options.status,
|
|
289
|
+
repositories: options.repository,
|
|
290
|
+
ready: options.ready,
|
|
291
|
+
blocked: options.blocked,
|
|
292
|
+
pr: options.pr ? true : options["no-pr"] ? false : undefined,
|
|
283
293
|
silent: options.silent,
|
|
284
294
|
verbose: options.verbose,
|
|
285
295
|
cwd: options.cwd,
|
|
@@ -379,6 +389,11 @@ const commands: Record<string, Command> = {
|
|
|
379
389
|
base: options.base,
|
|
380
390
|
multiPhase: options["multi-phase"],
|
|
381
391
|
json: options.json,
|
|
392
|
+
statuses: options.status,
|
|
393
|
+
repositories: options.repository,
|
|
394
|
+
ready: options.ready,
|
|
395
|
+
blocked: options.blocked,
|
|
396
|
+
pr: options.pr ? true : options["no-pr"] ? false : undefined,
|
|
382
397
|
silent: options.silent,
|
|
383
398
|
verbose: options.verbose,
|
|
384
399
|
inputAllowed: options.inputAllowed,
|
|
@@ -404,6 +419,8 @@ const commands: Record<string, Command> = {
|
|
|
404
419
|
verbose: options.verbose,
|
|
405
420
|
opencode: options.opencode,
|
|
406
421
|
claude: options.claude,
|
|
422
|
+
runner: options.runner,
|
|
423
|
+
printCommand: options["print-command"],
|
|
407
424
|
force: options.force,
|
|
408
425
|
inputAllowed: options.inputAllowed,
|
|
409
426
|
cwd: options.cwd,
|
|
@@ -437,6 +454,11 @@ const commands: Record<string, Command> = {
|
|
|
437
454
|
silent: options.silent,
|
|
438
455
|
verbose: options.verbose,
|
|
439
456
|
json: options.json,
|
|
457
|
+
statuses: options.status,
|
|
458
|
+
repositories: options.repository,
|
|
459
|
+
ready: options.ready,
|
|
460
|
+
blocked: options.blocked,
|
|
461
|
+
pr: options.pr ? true : options["no-pr"] ? false : undefined,
|
|
440
462
|
cwd: options.cwd,
|
|
441
463
|
}),
|
|
442
464
|
)
|
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
|
@@ -10,9 +10,9 @@ describe("strict CLI parsing", () => {
|
|
|
10
10
|
expectUsageError(["task", "list", "--josn"], "agency task")
|
|
11
11
|
expectUsageError(
|
|
12
12
|
["task", "list", "--repo", "agency"],
|
|
13
|
-
"agency task list [--json]",
|
|
13
|
+
"agency task list [filters] [--json]",
|
|
14
14
|
)
|
|
15
|
-
expectUsageError(["status", "--draft"], "agency status [--json]")
|
|
15
|
+
expectUsageError(["status", "--draft"], "agency status [filters] [--json]")
|
|
16
16
|
})
|
|
17
17
|
|
|
18
18
|
test("rejects duplicate scalar, boolean, and single-value multiple options", () => {
|
|
@@ -77,6 +77,41 @@ describe("strict CLI parsing", () => {
|
|
|
77
77
|
})
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
+
test("parses composable view filters", () => {
|
|
81
|
+
expect(
|
|
82
|
+
parseCli([
|
|
83
|
+
"task",
|
|
84
|
+
"list",
|
|
85
|
+
"--status",
|
|
86
|
+
"open",
|
|
87
|
+
"--status",
|
|
88
|
+
"working",
|
|
89
|
+
"--repository",
|
|
90
|
+
"agency",
|
|
91
|
+
"--ready",
|
|
92
|
+
"--pr",
|
|
93
|
+
]).values,
|
|
94
|
+
).toMatchObject({
|
|
95
|
+
status: ["open", "working"],
|
|
96
|
+
repository: ["agency"],
|
|
97
|
+
ready: true,
|
|
98
|
+
pr: true,
|
|
99
|
+
})
|
|
100
|
+
expect(parseCli(["status", "--no-pr"]).values["no-pr"]).toBe(true)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test("validates view filter values and conflicts", () => {
|
|
104
|
+
expect(() => parseCli(["epic", "list", "--status", "invalid"])).toThrow(
|
|
105
|
+
"Invalid '--status' value",
|
|
106
|
+
)
|
|
107
|
+
expect(() =>
|
|
108
|
+
parseCli(["phase", "list", "task", "--ready", "--blocked"]),
|
|
109
|
+
).toThrow("cannot be combined")
|
|
110
|
+
expect(() => parseCli(["status", "--pr", "--no-pr"])).toThrow(
|
|
111
|
+
"cannot be combined",
|
|
112
|
+
)
|
|
113
|
+
})
|
|
114
|
+
|
|
80
115
|
test("enforces exact maximum positional arity for every leaf command", () => {
|
|
81
116
|
for (const [args, usage] of [
|
|
82
117
|
[["init", "one", "two"], "agency init"],
|
|
@@ -286,6 +321,22 @@ describe("strict CLI parsing", () => {
|
|
|
286
321
|
)
|
|
287
322
|
})
|
|
288
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
|
+
|
|
289
340
|
test("accepts repeatable graph filters and rejects output conflicts", () => {
|
|
290
341
|
expect(
|
|
291
342
|
parseCli([
|
package/src/cli-parser.ts
CHANGED
|
@@ -40,6 +40,29 @@ const outputOptions = {
|
|
|
40
40
|
json: { type: "boolean" },
|
|
41
41
|
} satisfies OptionConfig
|
|
42
42
|
|
|
43
|
+
const viewOptions = {
|
|
44
|
+
status: { type: "string", multiple: true },
|
|
45
|
+
repository: { type: "string", multiple: true },
|
|
46
|
+
ready: { type: "boolean" },
|
|
47
|
+
blocked: { type: "boolean" },
|
|
48
|
+
pr: { type: "boolean" },
|
|
49
|
+
"no-pr": { type: "boolean" },
|
|
50
|
+
} satisfies OptionConfig
|
|
51
|
+
|
|
52
|
+
const viewOptionNames = [
|
|
53
|
+
"status",
|
|
54
|
+
"repository",
|
|
55
|
+
"ready",
|
|
56
|
+
"blocked",
|
|
57
|
+
"pr",
|
|
58
|
+
"no-pr",
|
|
59
|
+
] as const
|
|
60
|
+
|
|
61
|
+
const viewConflicts = [
|
|
62
|
+
["ready", "blocked"],
|
|
63
|
+
["pr", "no-pr"],
|
|
64
|
+
] as const
|
|
65
|
+
|
|
43
66
|
const createOptions = {
|
|
44
67
|
...outputOptions,
|
|
45
68
|
"ticket-url": { type: "string" },
|
|
@@ -178,7 +201,7 @@ const commands = {
|
|
|
178
201
|
},
|
|
179
202
|
epic: {
|
|
180
203
|
usage: "agency epic <create|list|show>",
|
|
181
|
-
options: { ...createOptions, epic: { type: "string" } },
|
|
204
|
+
options: { ...createOptions, ...viewOptions, epic: { type: "string" } },
|
|
182
205
|
subcommands: {
|
|
183
206
|
create: {
|
|
184
207
|
usage:
|
|
@@ -190,10 +213,12 @@ const commands = {
|
|
|
190
213
|
repeatable: ["repo"],
|
|
191
214
|
},
|
|
192
215
|
list: {
|
|
193
|
-
usage: "agency epic list [--json]",
|
|
216
|
+
usage: "agency epic list [filters] [--json]",
|
|
194
217
|
minArgs: 0,
|
|
195
218
|
maxArgs: 0,
|
|
196
|
-
options: ["json"],
|
|
219
|
+
options: ["json", ...viewOptionNames],
|
|
220
|
+
repeatable: ["status", "repository"],
|
|
221
|
+
conflicts: viewConflicts,
|
|
197
222
|
},
|
|
198
223
|
show: {
|
|
199
224
|
usage: "agency epic show <id> [--json]",
|
|
@@ -205,7 +230,7 @@ const commands = {
|
|
|
205
230
|
},
|
|
206
231
|
task: {
|
|
207
232
|
usage: "agency task <new|create|list|show|status>",
|
|
208
|
-
options: { ...taskCreateOptions, task: { type: "string" } },
|
|
233
|
+
options: { ...taskCreateOptions, ...viewOptions, task: { type: "string" } },
|
|
209
234
|
subcommands: {
|
|
210
235
|
new: {
|
|
211
236
|
usage: "agency task new [id] [options]",
|
|
@@ -243,10 +268,12 @@ const commands = {
|
|
|
243
268
|
repeatable: ["reference"],
|
|
244
269
|
},
|
|
245
270
|
list: {
|
|
246
|
-
usage: "agency task list [--json]",
|
|
271
|
+
usage: "agency task list [filters] [--json]",
|
|
247
272
|
minArgs: 0,
|
|
248
273
|
maxArgs: 0,
|
|
249
|
-
options: ["json"],
|
|
274
|
+
options: ["json", ...viewOptionNames],
|
|
275
|
+
repeatable: ["status", "repository"],
|
|
276
|
+
conflicts: viewConflicts,
|
|
250
277
|
},
|
|
251
278
|
show: {
|
|
252
279
|
usage: "agency task show <id> [--json]",
|
|
@@ -266,6 +293,7 @@ const commands = {
|
|
|
266
293
|
usage: "agency phase <create|list|show|status>",
|
|
267
294
|
options: {
|
|
268
295
|
...phaseCreateOptions,
|
|
296
|
+
...viewOptions,
|
|
269
297
|
task: { type: "string" },
|
|
270
298
|
phase: { type: "string" },
|
|
271
299
|
},
|
|
@@ -291,10 +319,12 @@ const commands = {
|
|
|
291
319
|
repeatable: ["reference", "depends-on"],
|
|
292
320
|
},
|
|
293
321
|
list: {
|
|
294
|
-
usage: "agency phase list <task-id> [--json]",
|
|
322
|
+
usage: "agency phase list <task-id> [filters] [--json]",
|
|
295
323
|
minArgs: 1,
|
|
296
324
|
maxArgs: 1,
|
|
297
|
-
options: ["json", "task"],
|
|
325
|
+
options: ["json", "task", ...viewOptionNames],
|
|
326
|
+
repeatable: ["status", "repository"],
|
|
327
|
+
conflicts: viewConflicts,
|
|
298
328
|
},
|
|
299
329
|
show: {
|
|
300
330
|
usage: "agency phase show <task-id> <phase-id> [--json]",
|
|
@@ -409,19 +439,21 @@ const commands = {
|
|
|
409
439
|
},
|
|
410
440
|
work: {
|
|
411
441
|
usage:
|
|
412
|
-
"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]",
|
|
413
443
|
options: {
|
|
414
444
|
...commonOptions,
|
|
415
445
|
...entitySelectorOptions,
|
|
416
446
|
json: { type: "boolean" },
|
|
417
447
|
"dry-run": { type: "boolean" },
|
|
448
|
+
runner: { type: "string" },
|
|
449
|
+
"print-command": { type: "boolean" },
|
|
418
450
|
opencode: { type: "boolean" },
|
|
419
451
|
claude: { type: "boolean" },
|
|
420
452
|
force: { type: "boolean" },
|
|
421
453
|
},
|
|
422
454
|
command: {
|
|
423
455
|
usage:
|
|
424
|
-
"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]",
|
|
425
457
|
minArgs: 0,
|
|
426
458
|
maxArgs: 2,
|
|
427
459
|
options: [
|
|
@@ -430,12 +462,16 @@ const commands = {
|
|
|
430
462
|
"epic",
|
|
431
463
|
"task",
|
|
432
464
|
"phase",
|
|
465
|
+
"runner",
|
|
466
|
+
"print-command",
|
|
433
467
|
"opencode",
|
|
434
468
|
"claude",
|
|
435
469
|
"force",
|
|
436
470
|
],
|
|
437
471
|
conflicts: [
|
|
438
472
|
["opencode", "claude"],
|
|
473
|
+
["runner", "opencode"],
|
|
474
|
+
["runner", "claude"],
|
|
439
475
|
["epic", "$positional"],
|
|
440
476
|
],
|
|
441
477
|
},
|
|
@@ -473,13 +509,15 @@ const commands = {
|
|
|
473
509
|
},
|
|
474
510
|
},
|
|
475
511
|
status: {
|
|
476
|
-
usage: "agency status [--json]",
|
|
477
|
-
options: outputOptions,
|
|
512
|
+
usage: "agency status [filters] [--json]",
|
|
513
|
+
options: { ...outputOptions, ...viewOptions },
|
|
478
514
|
command: {
|
|
479
|
-
usage: "agency status [--json]",
|
|
515
|
+
usage: "agency status [filters] [--json]",
|
|
480
516
|
minArgs: 0,
|
|
481
517
|
maxArgs: 0,
|
|
482
|
-
options: ["json"],
|
|
518
|
+
options: ["json", ...viewOptionNames],
|
|
519
|
+
repeatable: ["status", "repository"],
|
|
520
|
+
conflicts: viewConflicts,
|
|
483
521
|
},
|
|
484
522
|
},
|
|
485
523
|
validate: {
|
|
@@ -772,6 +810,24 @@ function validateGraphOptions(values: ParsedCli["values"], spec: LeafCommand) {
|
|
|
772
810
|
}
|
|
773
811
|
}
|
|
774
812
|
|
|
813
|
+
function validateViewOptions(values: ParsedCli["values"], spec: LeafCommand) {
|
|
814
|
+
const supplied = values.status
|
|
815
|
+
const statuses = Array.isArray(supplied)
|
|
816
|
+
? supplied
|
|
817
|
+
: supplied === undefined
|
|
818
|
+
? []
|
|
819
|
+
: [supplied]
|
|
820
|
+
const accepted = new Set(["open", "working", "delegated", "done", "dropped"])
|
|
821
|
+
for (const status of statuses) {
|
|
822
|
+
if (typeof status !== "string" || !accepted.has(status)) {
|
|
823
|
+
throw usageError(
|
|
824
|
+
`Invalid '--status' value '${String(status)}'. Expected one of: ${[...accepted].join(", ")}.`,
|
|
825
|
+
spec.usage,
|
|
826
|
+
)
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
775
831
|
export function parseCli(args: readonly string[]): ParsedCli {
|
|
776
832
|
const commandIndex = findCommandIndex(args)
|
|
777
833
|
if (commandIndex === -1) {
|
|
@@ -924,6 +980,12 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
924
980
|
if (commandName === "graph") {
|
|
925
981
|
validateGraphOptions(parsed.values, spec)
|
|
926
982
|
}
|
|
983
|
+
if (
|
|
984
|
+
commandName === "status" ||
|
|
985
|
+
(["epic", "task", "phase"].includes(commandName) && subcommand === "list")
|
|
986
|
+
) {
|
|
987
|
+
validateViewOptions(parsed.values, spec)
|
|
988
|
+
}
|
|
927
989
|
if (
|
|
928
990
|
commandName === "finish" &&
|
|
929
991
|
parsed.values.outcome !== "done" &&
|
|
@@ -942,6 +1004,8 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
942
1004
|
(parsed.values.epic ||
|
|
943
1005
|
parsed.values.opencode ||
|
|
944
1006
|
parsed.values.claude ||
|
|
1007
|
+
parsed.values.runner ||
|
|
1008
|
+
parsed.values["print-command"] ||
|
|
945
1009
|
parsed.values.force))
|
|
946
1010
|
) {
|
|
947
1011
|
throw usageError(
|
package/src/cli.test.ts
CHANGED
|
@@ -774,10 +774,20 @@ status: open
|
|
|
774
774
|
await runCli(["phase", "list", "ship", "--json"], root),
|
|
775
775
|
)
|
|
776
776
|
expect(phaseList.map((phase: { id: string }) => phase.id)).toEqual([
|
|
777
|
-
"implement",
|
|
778
777
|
"prepare",
|
|
778
|
+
"implement",
|
|
779
779
|
"release",
|
|
780
780
|
])
|
|
781
|
+
const phaseTable = await runCli(
|
|
782
|
+
["phase", "list", "ship", "--repository", "agency", "--no-pr"],
|
|
783
|
+
root,
|
|
784
|
+
)
|
|
785
|
+
expect(phaseTable.stdout).toMatch(
|
|
786
|
+
/PHASE\s+PARENT\s+STATUS\s+READINESS\s+REPOSITORIES\s+BRANCH/,
|
|
787
|
+
)
|
|
788
|
+
expect(phaseTable.stdout.indexOf("prepare")).toBeLessThan(
|
|
789
|
+
phaseTable.stdout.indexOf("implement"),
|
|
790
|
+
)
|
|
781
791
|
|
|
782
792
|
const phaseShow = parseJson(
|
|
783
793
|
await runCli(["phase", "show", "ship", "release", "--json"], root),
|
|
@@ -100,4 +100,26 @@ describe("epic command", () => {
|
|
|
100
100
|
},
|
|
101
101
|
})
|
|
102
102
|
})
|
|
103
|
+
|
|
104
|
+
test("renders a readable operational table", async () => {
|
|
105
|
+
await runTestEffect(
|
|
106
|
+
epic({
|
|
107
|
+
subcommand: "create",
|
|
108
|
+
args: ["example"],
|
|
109
|
+
ticketUrl: "https://example.com/epic",
|
|
110
|
+
repos: ["agency:main"],
|
|
111
|
+
cwd: root,
|
|
112
|
+
silent: true,
|
|
113
|
+
}),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const logs = await captureLogs(() =>
|
|
117
|
+
runTestEffect(epic({ subcommand: "list", args: [], cwd: root })),
|
|
118
|
+
)
|
|
119
|
+
expect(logs[0]).toContain(
|
|
120
|
+
"EPIC STATUS READINESS REPOSITORIES PR WORKTREE",
|
|
121
|
+
)
|
|
122
|
+
expect(logs[0]).toContain("example open waiting agency")
|
|
123
|
+
expect(logs[0]).not.toMatch(/[\u{e000}-\u{f8ff}]/u)
|
|
124
|
+
})
|
|
103
125
|
})
|
package/src/commands/epic.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { Effect } from "effect"
|
|
|
2
2
|
import type { BaseCommandOptions } from "../utils/command"
|
|
3
3
|
import { EpicService } from "../services/EpicService"
|
|
4
4
|
import { createLoggers } from "../utils/effect"
|
|
5
|
+
import { formatTable } from "../utils/table"
|
|
6
|
+
import { getWorkViews } from "../work-view"
|
|
5
7
|
import { parseRepositoryReferences } from "../workbase/repository-reference"
|
|
6
8
|
|
|
7
9
|
interface EpicOptions extends BaseCommandOptions {
|
|
@@ -11,6 +13,11 @@ interface EpicOptions extends BaseCommandOptions {
|
|
|
11
13
|
readonly description?: string
|
|
12
14
|
readonly repos?: readonly string[]
|
|
13
15
|
readonly json?: boolean
|
|
16
|
+
readonly statuses?: readonly string[]
|
|
17
|
+
readonly repositories?: readonly string[]
|
|
18
|
+
readonly ready?: boolean
|
|
19
|
+
readonly blocked?: boolean
|
|
20
|
+
readonly pr?: boolean
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
export const epic = (options: EpicOptions) =>
|
|
@@ -47,16 +54,40 @@ export const epic = (options: EpicOptions) =>
|
|
|
47
54
|
|
|
48
55
|
case "list": {
|
|
49
56
|
const records = yield* epics.list(cwd)
|
|
57
|
+
const { epicRows } = yield* getWorkViews({
|
|
58
|
+
cwd,
|
|
59
|
+
statuses: options.statuses,
|
|
60
|
+
repositories: options.repositories,
|
|
61
|
+
ready: options.ready,
|
|
62
|
+
blocked: options.blocked,
|
|
63
|
+
pr: options.pr,
|
|
64
|
+
})
|
|
65
|
+
const ordered = epicRows.flatMap((row) => {
|
|
66
|
+
const record = records.find((item) => item.id === row.key)
|
|
67
|
+
return record ? [record] : []
|
|
68
|
+
})
|
|
50
69
|
if (options.json) {
|
|
51
70
|
log(
|
|
52
71
|
JSON.stringify(
|
|
53
|
-
|
|
72
|
+
ordered.map(({ content: _, ...record }) => record),
|
|
54
73
|
null,
|
|
55
74
|
2,
|
|
56
75
|
),
|
|
57
76
|
)
|
|
58
77
|
} else {
|
|
59
|
-
|
|
78
|
+
log(
|
|
79
|
+
formatTable(
|
|
80
|
+
["EPIC", "STATUS", "READINESS", "REPOSITORIES", "PR", "WORKTREE"],
|
|
81
|
+
epicRows.map((row) => [
|
|
82
|
+
row.id,
|
|
83
|
+
row.status,
|
|
84
|
+
row.readiness,
|
|
85
|
+
row.repositories,
|
|
86
|
+
row.pr,
|
|
87
|
+
row.worktree,
|
|
88
|
+
]),
|
|
89
|
+
),
|
|
90
|
+
)
|
|
60
91
|
}
|
|
61
92
|
return
|
|
62
93
|
}
|
|
@@ -98,4 +129,9 @@ Create options:
|
|
|
98
129
|
|
|
99
130
|
Options:
|
|
100
131
|
--json Output results as JSON
|
|
132
|
+
--status <status> Filter list by status; repeatable
|
|
133
|
+
--repository <alias> Filter list by repository; repeatable
|
|
134
|
+
--ready Include only ready epics
|
|
135
|
+
--blocked Include only blocked epics
|
|
136
|
+
--pr / --no-pr Filter by recorded PR presence
|
|
101
137
|
`
|