@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 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 or Claude Code for `agency work`
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, selected agent, and
497
- process. The launched agent receives `AGENCY_SESSION_ID` and
498
- `AGENCY_CLAIM_REVISION` for a later release or finish operation.
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>] [--opencode | --claude]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.18.0",
3
+ "version": "2.20.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -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 `--opencode` or `--claude` to require a specific agent. This command fetches
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
@@ -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
  })
@@ -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
- records.map(({ content: _, ...record }) => record),
72
+ ordered.map(({ content: _, ...record }) => record),
54
73
  null,
55
74
  2,
56
75
  ),
57
76
  )
58
77
  } else {
59
- for (const record of records) log(record.id)
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
  `