@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/README.md +29 -8
- package/cli.ts +94 -8
- package/package.json +1 -1
- package/skills/agency/SKILL.md +13 -6
- package/src/cli-parser.test.ts +111 -2
- package/src/cli-parser.ts +273 -36
- package/src/cli.test.ts +105 -5
- package/src/commands/context.ts +3 -0
- package/src/commands/epic.test.ts +22 -0
- package/src/commands/epic.ts +38 -2
- package/src/commands/init.ts +3 -1
- 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/validate.test.ts +2 -0
- package/src/commands/work.test.ts +1 -0
- package/src/commands/work.ts +15 -7
- package/src/commands/workbase.test.ts +63 -2
- package/src/commands/workbase.ts +77 -10
- package/src/services/WorkbaseService.test.ts +147 -4
- package/src/services/WorkbaseService.ts +214 -12
- 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/schemas.test.ts +17 -6
- package/src/workbase/schemas.ts +16 -1
- package/src/workbase/workbase-choice.ts +2 -0
package/README.md
CHANGED
|
@@ -342,8 +342,11 @@ conditions remain visible in `warnings` or `unresolved` with a suggested action.
|
|
|
342
342
|
|
|
343
343
|
```text
|
|
344
344
|
agency init [path] [--json]
|
|
345
|
-
agency workbase add <path> [--json]
|
|
345
|
+
agency workbase add <path> [--name <name>] [--json]
|
|
346
346
|
agency workbase list [--json]
|
|
347
|
+
agency workbase remove <id|name|path> [--json]
|
|
348
|
+
agency workbase prune [--json]
|
|
349
|
+
agency workbase default [<id|name> | --clear] [--json]
|
|
347
350
|
agency integration status [--json]
|
|
348
351
|
agency integration sync [--json]
|
|
349
352
|
agency repo add <alias> <remote> [--json]
|
|
@@ -353,6 +356,9 @@ agency repo list [--json]
|
|
|
353
356
|
|
|
354
357
|
Registered workbases are stored in
|
|
355
358
|
`$XDG_CONFIG_HOME/agency/workbases.json` (or `~/.config/agency/workbases.json`).
|
|
359
|
+
Each registration has a stable ID and may have a unique name. A default workbase
|
|
360
|
+
is used when the current directory is outside every workbase. `prune` removes
|
|
361
|
+
registrations whose workbase configuration no longer exists.
|
|
356
362
|
`repo add` creates a bare clone. `repo link` creates a symlink to an existing Git
|
|
357
363
|
repository. Alias names are then used by all documents and commands.
|
|
358
364
|
|
|
@@ -365,7 +371,7 @@ status, validation, graph export, reconciliation, and PR creation.
|
|
|
365
371
|
```text
|
|
366
372
|
agency epic create <id> --ticket-url <url> [--description <text>] [--json]
|
|
367
373
|
--repo <alias>:<ref> [--repo <alias>:<ref>...]
|
|
368
|
-
agency epic list [--json]
|
|
374
|
+
agency epic list [filters] [--json]
|
|
369
375
|
agency epic show <id> [--json]
|
|
370
376
|
```
|
|
371
377
|
|
|
@@ -404,14 +410,22 @@ agency task create <id> --multi-phase
|
|
|
404
410
|
|
|
405
411
|
Agency never prompts when `--no-input` is set or stdin/stderr are not TTYs.
|
|
406
412
|
`--json` also disables prompts and selectors, even when a TTY is available.
|
|
407
|
-
Commands with explicit inputs continue normally.
|
|
408
|
-
|
|
409
|
-
|
|
413
|
+
Commands with explicit inputs continue normally. `--workbase <id|name|path>`
|
|
414
|
+
selects a workbase directly; `--cwd <path>` performs the same inference Agency
|
|
415
|
+
would perform from that directory. These options are mutually exclusive and take
|
|
416
|
+
precedence over ambient cwd and the configured default.
|
|
417
|
+
|
|
418
|
+
Targeted commands accept `--epic`, `--task`, and `--phase` where those entity
|
|
419
|
+
kinds apply. A phase selector requires a task selector. Entity selectors cannot
|
|
420
|
+
be mixed with positional target IDs, and an epic selector cannot be mixed with
|
|
421
|
+
task or phase selectors. This makes commands such as
|
|
422
|
+
`agency phase status done --task ship --phase release --workbase primary --no-input`
|
|
423
|
+
fully independent of process cwd and prompts.
|
|
410
424
|
|
|
411
425
|
Inspect tasks:
|
|
412
426
|
|
|
413
427
|
```text
|
|
414
|
-
agency task list [--json]
|
|
428
|
+
agency task list [filters] [--json]
|
|
415
429
|
agency task show <id> [--json]
|
|
416
430
|
agency task status <id> <open|done|dropped> [--json]
|
|
417
431
|
```
|
|
@@ -438,7 +452,7 @@ agency phase create <task-id> <phase-id>
|
|
|
438
452
|
[--description <text>] [--reference <alias>:<ref>...]
|
|
439
453
|
[--depends-on <phase-id>...] [--first-phase <phase-id>] [--json]
|
|
440
454
|
|
|
441
|
-
agency phase list <task-id> [--json]
|
|
455
|
+
agency phase list <task-id> [filters] [--json]
|
|
442
456
|
agency phase show <task-id> <phase-id> [--json]
|
|
443
457
|
agency phase status <task-id> <phase-id> <open|done|dropped> [--json]
|
|
444
458
|
```
|
|
@@ -456,6 +470,13 @@ work before changing its outcome.
|
|
|
456
470
|
Delegation is now explicit: the claimant identifies the orchestrator and the
|
|
457
471
|
runner identifies the assigned agent.
|
|
458
472
|
|
|
473
|
+
Human list output is a compact table with lifecycle, readiness, parent,
|
|
474
|
+
repository, branch, recorded PR, and worktree state where applicable. List and
|
|
475
|
+
status views accept composable `--status <status>` and `--repository <alias>`
|
|
476
|
+
filters, plus `--ready`, `--blocked`, `--pr`, and `--no-pr`. Status and repository
|
|
477
|
+
filters are repeatable. Rows follow task and phase declaration order; plain text
|
|
478
|
+
labels remain complete without color or icon fonts.
|
|
479
|
+
|
|
459
480
|
### Claims
|
|
460
481
|
|
|
461
482
|
Claim mutations require the SHA-256 revision exposed by `agency context` or
|
|
@@ -541,7 +562,7 @@ the owning `TASK.md` or `PHASE.md`.
|
|
|
541
562
|
### Status and Validation
|
|
542
563
|
|
|
543
564
|
```text
|
|
544
|
-
agency status [--json]
|
|
565
|
+
agency status [filters] [--json]
|
|
545
566
|
agency validate [path] [--json]
|
|
546
567
|
```
|
|
547
568
|
|
package/cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { Effect, Either, Layer } from "effect"
|
|
4
|
+
import { join, resolve } from "node:path"
|
|
4
5
|
import { parseCli } from "./src/cli-parser"
|
|
5
6
|
import { init, help as initHelp } from "./src/commands/init"
|
|
6
7
|
import { task, help as taskHelp } from "./src/commands/task"
|
|
@@ -72,11 +73,9 @@ const CliLayer = Layer.mergeAll(
|
|
|
72
73
|
/**
|
|
73
74
|
* Run a command Effect with all services provided
|
|
74
75
|
*/
|
|
75
|
-
async function
|
|
76
|
-
effect: Effect.Effect<void, E, any>,
|
|
77
|
-
): Promise<void> {
|
|
76
|
+
async function runEffect<A, E>(effect: Effect.Effect<A, E, any>): Promise<A> {
|
|
78
77
|
const providedEffect = Effect.provide(effect, CliLayer) as Effect.Effect<
|
|
79
|
-
|
|
78
|
+
A,
|
|
80
79
|
E,
|
|
81
80
|
never
|
|
82
81
|
>
|
|
@@ -103,8 +102,44 @@ async function runCommand<E>(
|
|
|
103
102
|
),
|
|
104
103
|
)
|
|
105
104
|
if (Either.isLeft(result)) throw result.left
|
|
105
|
+
return result.right
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
const runCommand = <E>(effect: Effect.Effect<void, E, any>) => runEffect(effect)
|
|
109
|
+
|
|
110
|
+
const resolveInvocationCwd = (
|
|
111
|
+
commandName: string,
|
|
112
|
+
options: Record<string, any>,
|
|
113
|
+
) =>
|
|
114
|
+
runEffect(
|
|
115
|
+
Effect.gen(function* () {
|
|
116
|
+
if (
|
|
117
|
+
options.help ||
|
|
118
|
+
commandName === "init" ||
|
|
119
|
+
commandName === "workbase"
|
|
120
|
+
) {
|
|
121
|
+
return resolve(options.cwd ?? process.cwd())
|
|
122
|
+
}
|
|
123
|
+
const workbases = yield* WorkbaseService
|
|
124
|
+
if (options.workbase) {
|
|
125
|
+
return yield* workbases.resolveRegistered(options.workbase)
|
|
126
|
+
}
|
|
127
|
+
if (options.cwd) {
|
|
128
|
+
const selectedCwd = resolve(options.cwd)
|
|
129
|
+
yield* workbases.discover(selectedCwd)
|
|
130
|
+
return selectedCwd
|
|
131
|
+
}
|
|
132
|
+
return yield* workbases.discover(process.cwd()).pipe(
|
|
133
|
+
Effect.as(process.cwd()),
|
|
134
|
+
Effect.catchTag("WorkbaseNotFoundError", () =>
|
|
135
|
+
workbases
|
|
136
|
+
.getDefault()
|
|
137
|
+
.pipe(Effect.map((entry) => entry?.path ?? process.cwd())),
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
}),
|
|
141
|
+
)
|
|
142
|
+
|
|
108
143
|
// Read version from package.json
|
|
109
144
|
const packageJson = await Bun.file(
|
|
110
145
|
new URL("./package.json", import.meta.url),
|
|
@@ -129,6 +164,7 @@ const commands: Record<string, Command> = {
|
|
|
129
164
|
json: options.json,
|
|
130
165
|
silent: options.silent,
|
|
131
166
|
verbose: options.verbose,
|
|
167
|
+
cwd: options.cwd,
|
|
132
168
|
}),
|
|
133
169
|
)
|
|
134
170
|
},
|
|
@@ -146,6 +182,7 @@ const commands: Record<string, Command> = {
|
|
|
146
182
|
json: options.json,
|
|
147
183
|
silent: options.silent,
|
|
148
184
|
verbose: options.verbose,
|
|
185
|
+
cwd: options.cwd,
|
|
149
186
|
}),
|
|
150
187
|
)
|
|
151
188
|
},
|
|
@@ -164,6 +201,7 @@ const commands: Record<string, Command> = {
|
|
|
164
201
|
json: options.json,
|
|
165
202
|
silent: options.silent,
|
|
166
203
|
verbose: options.verbose,
|
|
204
|
+
cwd: options.cwd,
|
|
167
205
|
}),
|
|
168
206
|
)
|
|
169
207
|
},
|
|
@@ -180,6 +218,7 @@ const commands: Record<string, Command> = {
|
|
|
180
218
|
json: options.json,
|
|
181
219
|
silent: options.silent,
|
|
182
220
|
verbose: options.verbose,
|
|
221
|
+
cwd: options.cwd,
|
|
183
222
|
}),
|
|
184
223
|
)
|
|
185
224
|
},
|
|
@@ -198,8 +237,14 @@ const commands: Record<string, Command> = {
|
|
|
198
237
|
description: options.description,
|
|
199
238
|
repos: options.repo,
|
|
200
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,
|
|
201
245
|
silent: options.silent,
|
|
202
246
|
verbose: options.verbose,
|
|
247
|
+
cwd: options.cwd,
|
|
203
248
|
}),
|
|
204
249
|
)
|
|
205
250
|
},
|
|
@@ -220,6 +265,7 @@ const commands: Record<string, Command> = {
|
|
|
220
265
|
json: options.json,
|
|
221
266
|
silent: options.silent,
|
|
222
267
|
verbose: options.verbose,
|
|
268
|
+
cwd: options.cwd,
|
|
223
269
|
}),
|
|
224
270
|
)
|
|
225
271
|
},
|
|
@@ -239,8 +285,14 @@ const commands: Record<string, Command> = {
|
|
|
239
285
|
dependsOn: options["depends-on"],
|
|
240
286
|
firstPhase: options["first-phase"],
|
|
241
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,
|
|
242
293
|
silent: options.silent,
|
|
243
294
|
verbose: options.verbose,
|
|
295
|
+
cwd: options.cwd,
|
|
244
296
|
}),
|
|
245
297
|
)
|
|
246
298
|
},
|
|
@@ -258,6 +310,7 @@ const commands: Record<string, Command> = {
|
|
|
258
310
|
json: options.json,
|
|
259
311
|
silent: options.silent,
|
|
260
312
|
verbose: options.verbose,
|
|
313
|
+
cwd: options.cwd,
|
|
261
314
|
}),
|
|
262
315
|
)
|
|
263
316
|
},
|
|
@@ -273,8 +326,11 @@ const commands: Record<string, Command> = {
|
|
|
273
326
|
subcommand: args[0],
|
|
274
327
|
args: args.slice(1),
|
|
275
328
|
json: options.json,
|
|
329
|
+
name: options.name,
|
|
330
|
+
clear: options.clear,
|
|
276
331
|
silent: options.silent,
|
|
277
332
|
verbose: options.verbose,
|
|
333
|
+
cwd: options.cwd,
|
|
278
334
|
}),
|
|
279
335
|
)
|
|
280
336
|
},
|
|
@@ -291,6 +347,7 @@ const commands: Record<string, Command> = {
|
|
|
291
347
|
json: options.json,
|
|
292
348
|
silent: options.silent,
|
|
293
349
|
verbose: options.verbose,
|
|
350
|
+
cwd: options.cwd,
|
|
294
351
|
}),
|
|
295
352
|
)
|
|
296
353
|
},
|
|
@@ -308,6 +365,7 @@ const commands: Record<string, Command> = {
|
|
|
308
365
|
silent: options.silent,
|
|
309
366
|
verbose: options.verbose,
|
|
310
367
|
json: options.json,
|
|
368
|
+
cwd: options.cwd,
|
|
311
369
|
}),
|
|
312
370
|
)
|
|
313
371
|
},
|
|
@@ -331,9 +389,15 @@ const commands: Record<string, Command> = {
|
|
|
331
389
|
base: options.base,
|
|
332
390
|
multiPhase: options["multi-phase"],
|
|
333
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,
|
|
334
397
|
silent: options.silent,
|
|
335
398
|
verbose: options.verbose,
|
|
336
399
|
inputAllowed: options.inputAllowed,
|
|
400
|
+
cwd: options.cwd,
|
|
337
401
|
}),
|
|
338
402
|
)
|
|
339
403
|
},
|
|
@@ -357,6 +421,9 @@ const commands: Record<string, Command> = {
|
|
|
357
421
|
claude: options.claude,
|
|
358
422
|
force: options.force,
|
|
359
423
|
inputAllowed: options.inputAllowed,
|
|
424
|
+
cwd: options.cwd,
|
|
425
|
+
taskId: options.task,
|
|
426
|
+
phaseId: options.phase,
|
|
360
427
|
}),
|
|
361
428
|
)
|
|
362
429
|
},
|
|
@@ -385,6 +452,12 @@ const commands: Record<string, Command> = {
|
|
|
385
452
|
silent: options.silent,
|
|
386
453
|
verbose: options.verbose,
|
|
387
454
|
json: options.json,
|
|
455
|
+
statuses: options.status,
|
|
456
|
+
repositories: options.repository,
|
|
457
|
+
ready: options.ready,
|
|
458
|
+
blocked: options.blocked,
|
|
459
|
+
pr: options.pr ? true : options["no-pr"] ? false : undefined,
|
|
460
|
+
cwd: options.cwd,
|
|
388
461
|
}),
|
|
389
462
|
)
|
|
390
463
|
},
|
|
@@ -402,6 +475,7 @@ const commands: Record<string, Command> = {
|
|
|
402
475
|
verbose: options.verbose,
|
|
403
476
|
json: options.json,
|
|
404
477
|
inputAllowed: options.inputAllowed,
|
|
478
|
+
cwd: options.cwd,
|
|
405
479
|
}),
|
|
406
480
|
)
|
|
407
481
|
},
|
|
@@ -414,11 +488,18 @@ const commands: Record<string, Command> = {
|
|
|
414
488
|
}
|
|
415
489
|
await runCommand(
|
|
416
490
|
context({
|
|
417
|
-
target:
|
|
491
|
+
target: options.epic
|
|
492
|
+
? join("epics", options.epic)
|
|
493
|
+
: options.phase
|
|
494
|
+
? join("tasks", options.task, "phases", options.phase)
|
|
495
|
+
: options.task
|
|
496
|
+
? join("tasks", options.task)
|
|
497
|
+
: args[0],
|
|
418
498
|
compact: options.compact,
|
|
419
499
|
json: options.json,
|
|
420
500
|
silent: options.silent,
|
|
421
501
|
verbose: options.verbose,
|
|
502
|
+
cwd: options.cwd,
|
|
422
503
|
}),
|
|
423
504
|
)
|
|
424
505
|
},
|
|
@@ -441,6 +522,7 @@ const commands: Record<string, Command> = {
|
|
|
441
522
|
include: options.include,
|
|
442
523
|
silent: options.silent,
|
|
443
524
|
verbose: options.verbose,
|
|
525
|
+
cwd: options.cwd,
|
|
444
526
|
}),
|
|
445
527
|
)
|
|
446
528
|
},
|
|
@@ -458,6 +540,7 @@ const commands: Record<string, Command> = {
|
|
|
458
540
|
json: options.json,
|
|
459
541
|
silent: options.silent,
|
|
460
542
|
verbose: options.verbose,
|
|
543
|
+
cwd: options.cwd,
|
|
461
544
|
}),
|
|
462
545
|
)
|
|
463
546
|
},
|
|
@@ -497,6 +580,8 @@ Global Options:
|
|
|
497
580
|
-s, --silent Suppress output messages
|
|
498
581
|
-v, --verbose Show verbose output including detailed debugging info
|
|
499
582
|
--no-input Never open an interactive prompt or selector
|
|
583
|
+
--workbase <selector> Use a registered workbase ID, name, or path
|
|
584
|
+
--cwd <path> Resolve context from this directory
|
|
500
585
|
|
|
501
586
|
Examples:
|
|
502
587
|
agency init # Initialize the current directory
|
|
@@ -538,15 +623,16 @@ try {
|
|
|
538
623
|
!values.json &&
|
|
539
624
|
!values["no-input"] &&
|
|
540
625
|
Boolean(process.stdin.isTTY && process.stderr.isTTY)
|
|
626
|
+
const cwd = await resolveInvocationCwd(commandName, values)
|
|
541
627
|
if (values.json || (values.jsonl && values.help)) {
|
|
542
628
|
const result = await collectCommandResult(() =>
|
|
543
|
-
command.run(commandArgs, { ...values, inputAllowed }),
|
|
629
|
+
command.run(commandArgs, { ...values, cwd, inputAllowed }),
|
|
544
630
|
)
|
|
545
631
|
writeEnvelope(successEnvelope(result))
|
|
546
632
|
} else if (values.jsonl) {
|
|
547
|
-
await command.run(commandArgs, { ...values, inputAllowed: false })
|
|
633
|
+
await command.run(commandArgs, { ...values, cwd, inputAllowed: false })
|
|
548
634
|
} else {
|
|
549
|
-
await command.run(commandArgs, { ...values, inputAllowed })
|
|
635
|
+
await command.run(commandArgs, { ...values, cwd, inputAllowed })
|
|
550
636
|
}
|
|
551
637
|
} catch (error) {
|
|
552
638
|
if (machineMode) {
|
package/package.json
CHANGED
package/skills/agency/SKILL.md
CHANGED
|
@@ -60,13 +60,19 @@ agency init [path]
|
|
|
60
60
|
agency integration sync
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
Register known workbases so
|
|
63
|
+
Register and name known workbases so commands can select one from anywhere:
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
agency workbase add <path>
|
|
66
|
+
agency workbase add <path> [--name <name>]
|
|
67
67
|
agency workbase list
|
|
68
|
+
agency workbase default [<id|name> | --clear]
|
|
69
|
+
agency workbase remove <id|name|path>
|
|
70
|
+
agency workbase prune
|
|
68
71
|
```
|
|
69
72
|
|
|
73
|
+
Use `--workbase <id|name|path>` to bypass cwd inference, or `--cwd <path>` to
|
|
74
|
+
infer context from a specific directory. The options are mutually exclusive.
|
|
75
|
+
|
|
70
76
|
## Repository Aliases
|
|
71
77
|
|
|
72
78
|
Add a remote as an Agency-managed bare repository:
|
|
@@ -261,8 +267,8 @@ Use `--json` when diagnostics will be consumed programmatically. Resolve all
|
|
|
261
267
|
validation errors before materializing worktrees or creating PRs. Validation
|
|
262
268
|
checks schemas, aliases, backlinks, phase directories, duplicate references,
|
|
263
269
|
duplicate writable branch ownership, unknown dependencies, and dependency cycles.
|
|
264
|
-
Outside a workbase,
|
|
265
|
-
With `--no-input` or without a TTY, pass
|
|
270
|
+
Outside a workbase, Agency uses the configured default or opens the registered
|
|
271
|
+
workbase picker. With `--no-input` or without a TTY, pass `--workbase` or `--cwd`.
|
|
266
272
|
|
|
267
273
|
## Worktrees And Agent Launch
|
|
268
274
|
|
|
@@ -270,6 +276,7 @@ With `--no-input` or without a TTY, pass a path or run from a workbase instead.
|
|
|
270
276
|
agency work
|
|
271
277
|
agency work <directory>
|
|
272
278
|
agency work --epic <epic-id>
|
|
279
|
+
agency work --task <task-id> [--phase <phase-id>] --workbase <selector>
|
|
273
280
|
```
|
|
274
281
|
|
|
275
282
|
Use `--opencode` or `--claude` to require a specific agent. This command fetches
|
|
@@ -278,8 +285,8 @@ replaces the current process with the selected agent. With no directory it opens
|
|
|
278
285
|
an `fzf` picker containing the workbase hierarchy. Pass `.`, or another
|
|
279
286
|
directory, to infer the nearest epic, task, or phase.
|
|
280
287
|
Outside a workbase, it first opens a picker containing registered workbases.
|
|
281
|
-
With `--no-input` or without a TTY,
|
|
282
|
-
|
|
288
|
+
With `--no-input` or without a TTY, provide an explicit workbase or cwd and an
|
|
289
|
+
`--epic`, `--task`, or `--task` plus `--phase` selector so no picker is needed.
|
|
283
290
|
|
|
284
291
|
Epic and multi-phase task targets are orchestration sessions launched beside
|
|
285
292
|
their documents. Single-phase tasks and phases are execution sessions launched
|
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"],
|
|
@@ -384,6 +419,80 @@ describe("strict CLI parsing", () => {
|
|
|
384
419
|
).toBe(true)
|
|
385
420
|
})
|
|
386
421
|
|
|
422
|
+
test("normalizes explicit entity selectors into command targets", () => {
|
|
423
|
+
expect(
|
|
424
|
+
parseCli([
|
|
425
|
+
"phase",
|
|
426
|
+
"status",
|
|
427
|
+
"done",
|
|
428
|
+
"--task",
|
|
429
|
+
"ship",
|
|
430
|
+
"--phase",
|
|
431
|
+
"release",
|
|
432
|
+
]),
|
|
433
|
+
).toMatchObject({
|
|
434
|
+
commandName: "phase",
|
|
435
|
+
args: ["status", "ship", "release", "done"],
|
|
436
|
+
})
|
|
437
|
+
expect(
|
|
438
|
+
parseCli([
|
|
439
|
+
"claim",
|
|
440
|
+
"--task",
|
|
441
|
+
"ship",
|
|
442
|
+
"--phase",
|
|
443
|
+
"release",
|
|
444
|
+
"--claimant",
|
|
445
|
+
"agent",
|
|
446
|
+
"--runner",
|
|
447
|
+
"opencode",
|
|
448
|
+
"--session-id",
|
|
449
|
+
"session",
|
|
450
|
+
"--revision",
|
|
451
|
+
"0".repeat(64),
|
|
452
|
+
]),
|
|
453
|
+
).toMatchObject({ args: ["ship", "release"] })
|
|
454
|
+
expect(parseCli(["context", "--epic", "delivery"]).values.epic).toBe(
|
|
455
|
+
"delivery",
|
|
456
|
+
)
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
test("enforces explicit selector precedence and exclusions", () => {
|
|
460
|
+
expect(() =>
|
|
461
|
+
parseCli(["task", "show", "positional", "--task", "explicit"]),
|
|
462
|
+
).toThrow("cannot be combined with positional target IDs")
|
|
463
|
+
expect(() => parseCli(["context", "--phase", "release"])).toThrow(
|
|
464
|
+
"--phase' requires '--task",
|
|
465
|
+
)
|
|
466
|
+
expect(() =>
|
|
467
|
+
parseCli(["work", "--epic", "delivery", "--task", "ship"]),
|
|
468
|
+
).toThrow("cannot be combined with '--task'")
|
|
469
|
+
expect(() =>
|
|
470
|
+
parseCli(["status", "--workbase", "primary", "--cwd", "/tmp"]),
|
|
471
|
+
).toThrow("--workbase' and '--cwd' cannot be combined")
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
test("accepts explicit workbase context before or after commands", () => {
|
|
475
|
+
expect(
|
|
476
|
+
parseCli(["--workbase", "primary", "task", "list", "--no-input"]).values
|
|
477
|
+
.workbase,
|
|
478
|
+
).toBe("primary")
|
|
479
|
+
expect(parseCli(["status", "--cwd", "/tmp"]).values.cwd).toBe("/tmp")
|
|
480
|
+
expect(
|
|
481
|
+
parseCli(["--workbase=primary", "task", "list"]).values.workbase,
|
|
482
|
+
).toBe("primary")
|
|
483
|
+
expect(parseCli(["--cwd=/tmp", "status"]).values.cwd).toBe("/tmp")
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
test("rejects empty selectors", () => {
|
|
487
|
+
for (const args of [
|
|
488
|
+
["status", "--workbase="],
|
|
489
|
+
["status", "--cwd="],
|
|
490
|
+
["context", "--task="],
|
|
491
|
+
]) {
|
|
492
|
+
expect(() => parseCli(args)).toThrow("requires a non-empty value")
|
|
493
|
+
}
|
|
494
|
+
})
|
|
495
|
+
|
|
387
496
|
test("accepts grouped global short options before a command", () => {
|
|
388
497
|
const parsed = parseCli(["-sh", "task"])
|
|
389
498
|
expect(parsed.values).toMatchObject({ silent: true, help: true })
|