@markjaquith/agency 2.17.0 → 2.19.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/src/cli.test.ts CHANGED
@@ -217,6 +217,15 @@ describe("CLI", () => {
217
217
  await expect(access(root)).rejects.toThrow()
218
218
  })
219
219
 
220
+ test("resolves relative init paths from explicit cwd", async () => {
221
+ const parent = await createTempDir()
222
+ tempDirs.push(parent)
223
+ const result = parseJson(
224
+ await runCli(["init", "child", "--cwd", parent, "--json"], projectRoot),
225
+ )
226
+ expect(result.root).toBe(join(parent, "child"))
227
+ })
228
+
220
229
  test("refuses guided input without a TTY or with --no-input", async () => {
221
230
  for (const args of [
222
231
  ["task", "new"],
@@ -349,12 +358,93 @@ describe("CLI", () => {
349
358
  ).toEqual({
350
359
  root,
351
360
  })
352
- expect(
353
- parseJson(await runCli(["workbase", "add", root, "--json"], parent, env)),
354
- ).toEqual({ path: await realpath(root) })
361
+ const registration = parseJson(
362
+ await runCli(
363
+ [
364
+ "workbase",
365
+ "add",
366
+ "workbase",
367
+ "--cwd",
368
+ parent,
369
+ "--name",
370
+ "primary",
371
+ "--json",
372
+ ],
373
+ projectRoot,
374
+ env,
375
+ ),
376
+ )
377
+ expect(registration).toMatchObject({
378
+ name: "primary",
379
+ path: await realpath(root),
380
+ })
355
381
  expect(
356
382
  parseJson(await runCli(["workbase", "list", "--json"], parent, env)),
357
- ).toEqual([await realpath(root)])
383
+ ).toEqual({ workbases: [registration] })
384
+
385
+ await mkdir(join(root, "repos", "agency"), { recursive: true })
386
+ parseJson(
387
+ await runCli(
388
+ [
389
+ "task",
390
+ "create",
391
+ "explicit",
392
+ "--repo",
393
+ "agency",
394
+ "--workbase",
395
+ "primary",
396
+ "--no-input",
397
+ "--json",
398
+ ],
399
+ parent,
400
+ env,
401
+ ),
402
+ )
403
+ const shown = parseJson(
404
+ await runCli(
405
+ [
406
+ "task",
407
+ "show",
408
+ "--task",
409
+ "explicit",
410
+ "--workbase",
411
+ registration.id,
412
+ "--no-input",
413
+ "--json",
414
+ ],
415
+ parent,
416
+ env,
417
+ ),
418
+ )
419
+ expect(shown.id).toBe("explicit")
420
+ const inferred = parseJson(
421
+ await runCli(
422
+ ["context", "--cwd", join(root, "tasks", "explicit"), "--json"],
423
+ projectRoot,
424
+ env,
425
+ ),
426
+ )
427
+ expect(inferred.target).toMatchObject({
428
+ kind: "task",
429
+ taskId: "explicit",
430
+ })
431
+
432
+ parseJson(
433
+ await runCli(["workbase", "default", "primary", "--json"], parent, env),
434
+ )
435
+ expect(
436
+ parseJson(await runCli(["task", "list", "--json"], parent, env)),
437
+ ).toHaveLength(1)
438
+ const explicitOutside = await runCli(
439
+ ["task", "list", "--cwd", parent, "--json"],
440
+ projectRoot,
441
+ env,
442
+ )
443
+ expect(explicitOutside.exitCode).toBe(1)
444
+ expect(explicitOutside.stderr).toBe("")
445
+ expect(JSON.parse(explicitOutside.stdout).error.message).toContain(
446
+ "No Agency workbase found from",
447
+ )
358
448
  })
359
449
 
360
450
  test("exports equivalent JSON and JSONL graph contracts", async () => {
@@ -684,10 +774,20 @@ status: open
684
774
  await runCli(["phase", "list", "ship", "--json"], root),
685
775
  )
686
776
  expect(phaseList.map((phase: { id: string }) => phase.id)).toEqual([
687
- "implement",
688
777
  "prepare",
778
+ "implement",
689
779
  "release",
690
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
+ )
691
791
 
692
792
  const phaseShow = parseJson(
693
793
  await runCli(["phase", "show", "ship", "release", "--json"], root),
@@ -26,4 +26,7 @@ defaults to the current directory and may be an entity path or task ID.
26
26
  Options:
27
27
  --json Output a versioned machine result
28
28
  --compact Omit prose bodies and low-level Git details
29
+ --epic <id> Select an epic
30
+ --task <id> Select a task
31
+ --phase <id> Select a phase together with --task
29
32
  `
@@ -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
  `
@@ -1,4 +1,5 @@
1
1
  import { Effect } from "effect"
2
+ import { resolve } from "node:path"
2
3
  import type { BaseCommandOptions } from "../utils/command"
3
4
  import { WorkbaseService } from "../services/WorkbaseService"
4
5
  import { createLoggers } from "../utils/effect"
@@ -11,8 +12,9 @@ export const init = (options: InitOptions = {}) =>
11
12
  Effect.gen(function* () {
12
13
  const workbase = yield* WorkbaseService
13
14
  const { log } = createLoggers(options)
15
+ const cwd = options.cwd ?? process.cwd()
14
16
  const root = yield* workbase.initialize(
15
- options.path ?? options.cwd ?? process.cwd(),
17
+ options.path ? resolve(cwd, options.path) : cwd,
16
18
  )
17
19
  log(
18
20
  options.json
@@ -2,6 +2,8 @@ import { Effect } from "effect"
2
2
  import type { BaseCommandOptions } from "../utils/command"
3
3
  import { PhaseService } from "../services/PhaseService"
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 PhaseOptions extends BaseCommandOptions {
@@ -15,6 +17,11 @@ interface PhaseOptions extends BaseCommandOptions {
15
17
  readonly dependsOn?: readonly string[]
16
18
  readonly firstPhase?: string
17
19
  readonly json?: boolean
20
+ readonly statuses?: readonly string[]
21
+ readonly repositories?: readonly string[]
22
+ readonly ready?: boolean
23
+ readonly blocked?: boolean
24
+ readonly pr?: boolean
18
25
  }
19
26
 
20
27
  export const phase = (options: PhaseOptions) =>
@@ -64,15 +71,53 @@ export const phase = (options: PhaseOptions) =>
64
71
  case "list": {
65
72
  if (!taskId) return yield* Effect.fail(new Error("Task ID is required"))
66
73
  const records = yield* phases.list(taskId, cwd)
74
+ const { phaseRows } = yield* getWorkViews({
75
+ cwd,
76
+ statuses: options.statuses,
77
+ repositories: options.repositories,
78
+ ready: options.ready,
79
+ blocked: options.blocked,
80
+ pr: options.pr,
81
+ })
82
+ const rows = phaseRows.filter((row) => row.parent === taskId)
83
+ const ordered = rows.flatMap((row) => {
84
+ const record = records.find((item) => item.id === row.id)
85
+ return record ? [record] : []
86
+ })
67
87
  if (options.json) {
68
88
  log(
69
89
  JSON.stringify(
70
- records.map(({ content: _, ...record }) => record),
90
+ ordered.map(({ content: _, ...record }) => record),
71
91
  null,
72
92
  2,
73
93
  ),
74
94
  )
75
- } else for (const record of records) log(record.id)
95
+ } else {
96
+ log(
97
+ formatTable(
98
+ [
99
+ "PHASE",
100
+ "PARENT",
101
+ "STATUS",
102
+ "READINESS",
103
+ "REPOSITORIES",
104
+ "BRANCH",
105
+ "PR",
106
+ "WORKTREE",
107
+ ],
108
+ rows.map((row) => [
109
+ row.id,
110
+ row.parent,
111
+ row.status,
112
+ row.readiness,
113
+ row.repositories,
114
+ row.branch,
115
+ row.pr,
116
+ row.worktree,
117
+ ]),
118
+ ),
119
+ )
120
+ }
76
121
  return
77
122
  }
78
123
  case "show": {
@@ -139,4 +184,9 @@ Create options:
139
184
 
140
185
  Options:
141
186
  --json Output results as JSON
187
+ --status <status> Filter list by status; repeatable
188
+ --repository <alias> Filter list by repository; repeatable
189
+ --ready Include only ready phases
190
+ --blocked Include only blocked phases
191
+ --pr / --no-pr Filter by recorded PR presence
142
192
  `
@@ -8,6 +8,7 @@ import {
8
8
  runTestEffect,
9
9
  } from "../test-utils"
10
10
  import { status } from "./status"
11
+ import { task } from "./task"
11
12
 
12
13
  describe("status command", () => {
13
14
  let root: string
@@ -44,4 +45,47 @@ describe("status command", () => {
44
45
  },
45
46
  ])
46
47
  })
48
+
49
+ test("renders and filters the execution dashboard", async () => {
50
+ await runTestEffect(
51
+ task({
52
+ subcommand: "create",
53
+ args: ["example"],
54
+ repo: "agency",
55
+ branch: "feat/example",
56
+ base: "main",
57
+ cwd: root,
58
+ silent: true,
59
+ }),
60
+ )
61
+
62
+ const logs = await captureLogs(() =>
63
+ runTestEffect(
64
+ status({ cwd: root, repositories: ["agency"], ready: true, pr: false }),
65
+ ),
66
+ )
67
+ expect(logs).toContain("Repositories: 1")
68
+ expect(logs.at(-1)).toContain(
69
+ "KIND WORK PARENT STATUS READINESS REPOSITORIES BRANCH",
70
+ )
71
+ expect(logs.at(-1)).toContain(
72
+ "task example - open ready agency feat/example absent absent",
73
+ )
74
+ })
75
+
76
+ test("reports validation issues without requiring a decodable graph", async () => {
77
+ await mkdir(join(root, "tasks/broken"), { recursive: true })
78
+ await Bun.write(
79
+ join(root, "tasks/broken/TASK.md"),
80
+ "---\nrepo: agency\nstatus: invalid\n---\n",
81
+ )
82
+
83
+ const logs = await captureLogs(() =>
84
+ runTestEffect(status({ cwd: root, json: true })),
85
+ )
86
+ const output = JSON.parse(logs[0]!)
87
+ expect(output.valid).toBe(false)
88
+ expect(output.issues.length).toBeGreaterThan(0)
89
+ expect(output.work).toEqual([])
90
+ })
47
91
  })
@@ -3,9 +3,16 @@ import type { BaseCommandOptions } from "../utils/command"
3
3
  import { WorkbaseService } from "../services/WorkbaseService"
4
4
  import { RepositoryService } from "../services/RepositoryService"
5
5
  import { createLoggers } from "../utils/effect"
6
+ import { formatTable } from "../utils/table"
7
+ import { getWorkViews } from "../work-view"
6
8
 
7
9
  interface StatusOptions extends BaseCommandOptions {
8
10
  readonly json?: boolean
11
+ readonly statuses?: readonly string[]
12
+ readonly repositories?: readonly string[]
13
+ readonly ready?: boolean
14
+ readonly blocked?: boolean
15
+ readonly pr?: boolean
9
16
  }
10
17
 
11
18
  export const status = (options: StatusOptions = {}) =>
@@ -16,7 +23,17 @@ export const status = (options: StatusOptions = {}) =>
16
23
  const cwd = options.cwd ?? process.cwd()
17
24
  const report = yield* workbase.validate(cwd)
18
25
  const repos = yield* repositories.list(report.root)
19
- const data = { ...report, repositories: repos }
26
+ const executionRows = report.valid
27
+ ? (yield* getWorkViews({
28
+ cwd,
29
+ statuses: options.statuses,
30
+ repositories: options.repositories,
31
+ ready: options.ready,
32
+ blocked: options.blocked,
33
+ pr: options.pr,
34
+ })).executionRows
35
+ : []
36
+ const data = { ...report, repositories: repos, work: executionRows }
20
37
 
21
38
  if (options.json) {
22
39
  log(JSON.stringify(data, null, 2))
@@ -31,6 +48,33 @@ export const status = (options: StatusOptions = {}) =>
31
48
  log(
32
49
  `Validation: ${report.valid ? "valid" : `${report.issues.length} issues`}`,
33
50
  )
51
+ log("")
52
+ log(
53
+ formatTable(
54
+ [
55
+ "KIND",
56
+ "WORK",
57
+ "PARENT",
58
+ "STATUS",
59
+ "READINESS",
60
+ "REPOSITORIES",
61
+ "BRANCH",
62
+ "PR",
63
+ "WORKTREE",
64
+ ],
65
+ executionRows.map((row) => [
66
+ row.kind,
67
+ row.kind === "phase" ? row.key : row.id,
68
+ row.parent,
69
+ row.status,
70
+ row.readiness,
71
+ row.repositories,
72
+ row.branch,
73
+ row.pr,
74
+ row.worktree,
75
+ ]),
76
+ ),
77
+ )
34
78
  })
35
79
 
36
80
  export const help = `
@@ -39,5 +83,10 @@ Usage: agency status [options]
39
83
  Show workbase repository, entity, and validation status.
40
84
 
41
85
  Options:
42
- --json Output status as JSON
86
+ --json Output status as JSON
87
+ --status <status> Filter by status; repeatable
88
+ --repository <alias> Filter by repository; repeatable
89
+ --ready Include only ready work
90
+ --blocked Include only blocked work
91
+ --pr / --no-pr Filter by recorded PR presence
43
92
  `
@@ -124,6 +124,26 @@ describe("task and phase command JSON output", () => {
124
124
  })
125
125
  })
126
126
 
127
+ test("renders task and phase operational tables", async () => {
128
+ const taskLogs = await captureLogs(() =>
129
+ runTestEffect(task({ subcommand: "list", args: [], cwd: root })),
130
+ )
131
+ expect(taskLogs[0]).toContain(
132
+ "TASK PARENT STATUS READINESS REPOSITORIES BRANCH",
133
+ )
134
+ expect(taskLogs[0]).toContain("multi - open ready")
135
+
136
+ const phaseLogs = await captureLogs(() =>
137
+ runTestEffect(phase({ subcommand: "list", args: ["multi"], cwd: root })),
138
+ )
139
+ expect(phaseLogs[0]).toContain(
140
+ "PHASE PARENT STATUS READINESS REPOSITORIES BRANCH",
141
+ )
142
+ expect(phaseLogs[0]).toContain(
143
+ "first multi open ready agency task/first absent absent",
144
+ )
145
+ })
146
+
127
147
  test("outputs created task and phase records as JSON", async () => {
128
148
  const taskLogs = await captureLogs(() =>
129
149
  runTestEffect(
@@ -8,6 +8,8 @@ import { createLoggers } from "../utils/effect"
8
8
  import { parseRepositoryReferences } from "../workbase/repository-reference"
9
9
  import { WorkbaseService } from "../services/WorkbaseService"
10
10
  import { choose } from "../utils/chooser"
11
+ import { formatTable } from "../utils/table"
12
+ import { getWorkViews } from "../work-view"
11
13
 
12
14
  interface TaskOptions extends BaseCommandOptions {
13
15
  readonly subcommand?: string
@@ -21,6 +23,11 @@ interface TaskOptions extends BaseCommandOptions {
21
23
  readonly base?: string
22
24
  readonly multiPhase?: boolean
23
25
  readonly json?: boolean
26
+ readonly statuses?: readonly string[]
27
+ readonly repositories?: readonly string[]
28
+ readonly ready?: boolean
29
+ readonly blocked?: boolean
30
+ readonly pr?: boolean
24
31
  }
25
32
 
26
33
  export interface TaskInteraction {
@@ -208,16 +215,51 @@ export const task = (options: TaskOptions, interaction?: TaskInteraction) =>
208
215
  }
209
216
  case "list": {
210
217
  const records = yield* tasks.list(cwd)
218
+ const { taskRows } = yield* getWorkViews({
219
+ cwd,
220
+ statuses: options.statuses,
221
+ repositories: options.repositories,
222
+ ready: options.ready,
223
+ blocked: options.blocked,
224
+ pr: options.pr,
225
+ })
226
+ const ordered = taskRows.flatMap((row) => {
227
+ const record = records.find((item) => item.id === row.key)
228
+ return record ? [record] : []
229
+ })
211
230
  if (options.json) {
212
231
  log(
213
232
  JSON.stringify(
214
- records.map(({ content: _, ...record }) => record),
233
+ ordered.map(({ content: _, ...record }) => record),
215
234
  null,
216
235
  2,
217
236
  ),
218
237
  )
219
238
  } else {
220
- for (const record of records) log(record.id)
239
+ log(
240
+ formatTable(
241
+ [
242
+ "TASK",
243
+ "PARENT",
244
+ "STATUS",
245
+ "READINESS",
246
+ "REPOSITORIES",
247
+ "BRANCH",
248
+ "PR",
249
+ "WORKTREE",
250
+ ],
251
+ taskRows.map((row) => [
252
+ row.id,
253
+ row.parent,
254
+ row.status,
255
+ row.readiness,
256
+ row.repositories,
257
+ row.branch,
258
+ row.pr,
259
+ row.worktree,
260
+ ]),
261
+ ),
262
+ )
221
263
  }
222
264
  return
223
265
  }
@@ -286,4 +328,9 @@ through task new, which fails when --no-input is set or no TTY is available.
286
328
  Options:
287
329
  --json Output results as JSON
288
330
  --no-input Never open interactive task creation
331
+ --status <status> Filter list by status; repeatable
332
+ --repository <alias> Filter list by repository; repeatable
333
+ --ready Include only ready tasks
334
+ --blocked Include only blocked tasks
335
+ --pr / --no-pr Filter by recorded PR presence
289
336
  `
@@ -80,6 +80,7 @@ pr: null
80
80
  : Effect.succeed(path)
81
81
  },
82
82
  listRegistered: () => Effect.succeed(["/first", "/selected"]),
83
+ getDefault: () => Effect.succeed(undefined),
83
84
  validate: (path: string) =>
84
85
  Effect.succeed({
85
86
  root: path,
@@ -116,6 +117,7 @@ pr: null
116
117
  message: "No Agency workbase found from /outside",
117
118
  }),
118
119
  listRegistered: () => Effect.succeed(["/selected"]),
120
+ getDefault: () => Effect.succeed(undefined),
119
121
  }
120
122
  const fs = {
121
123
  runCommand: () => Effect.fail(new Error("unexpected fzf probe")),
@@ -98,6 +98,7 @@ const createHarness = (options: HarnessOptions = {}) => {
98
98
  })
99
99
  : Effect.succeed("/workbase"),
100
100
  listRegistered: () => Effect.succeed(options.registeredWorkbases ?? []),
101
+ getDefault: () => Effect.succeed(undefined),
101
102
  loadConfig: () =>
102
103
  Effect.succeed({
103
104
  root: "/workbase",
@@ -297,9 +297,13 @@ export const workPrepare = (options: WorkOptions = {}) =>
297
297
  const isDirectory = yield* fs.isDirectory(targetPath)
298
298
  const root = yield* workbase.discover(isDirectory ? targetPath : cwd)
299
299
 
300
- let taskId: string | undefined
301
- let phaseId: string | undefined
302
- if (options.directory && !isDirectory) {
300
+ let taskId = options.taskId
301
+ let phaseId = options.phaseId
302
+ if (taskId) {
303
+ const task = yield* tasks.show(taskId, root)
304
+ taskId = task.id
305
+ if (phaseId) phaseId = (yield* phases.show(task.id, phaseId, root)).id
306
+ } else if (options.directory && !isDirectory) {
303
307
  const task = yield* tasks.show(options.directory, root)
304
308
  taskId = task.id
305
309
  } else {
@@ -353,12 +357,16 @@ launching an agent or changing lifecycle status. --dry-run reports planned Git
353
357
  changes without fetching, creating branches, or creating worktrees.
354
358
 
355
359
  Options:
356
- --epic <id> Work on an epic
360
+ --epic <id> Work on an epic
361
+ --task <id> Work on a task
362
+ --phase <id> Work on a phase selected with --task
363
+ --workbase <target> Select a workbase by ID, name, or path
364
+ --cwd <path> Resolve context from a specific directory
357
365
  --opencode Require OpenCode
358
366
  --claude Require Claude Code
359
367
  --force Override readiness and terminal-state guards
360
- --no-input Never open an interactive selector
368
+ --no-input Never open an interactive selector
361
369
 
362
- Without interactive input, provide a directory, task ID, or --epic and run the
363
- command from a workbase. Workbase and target selection otherwise fail.
370
+ Without interactive input, provide an explicit workbase or cwd and an entity
371
+ selector. Workbase and target selection otherwise fail.
364
372
  `