@markjaquith/agency 2.7.2 → 2.8.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 +20 -7
- package/cli.ts +33 -98
- package/package.json +1 -1
- package/skills/agency/SKILL.md +8 -1
- package/src/cli-parser.test.ts +205 -0
- package/src/cli-parser.ts +573 -0
- package/src/cli.test.ts +49 -2
- package/src/commands/init.test.ts +3 -8
- package/src/commands/integration.test.ts +51 -0
- package/src/commands/integration.ts +62 -0
- package/src/commands/read-only.test.ts +144 -0
- package/src/commands/task.test.ts +43 -0
- package/src/commands/task.ts +75 -38
- package/src/commands/validate.test.ts +28 -0
- package/src/commands/validate.ts +10 -1
- package/src/commands/work.test.ts +48 -0
- package/src/commands/work.ts +13 -1
- package/src/services/IntegrationService.test.ts +137 -0
- package/src/services/IntegrationService.ts +136 -0
- package/src/services/WorkbaseService.test.ts +4 -118
- package/src/services/WorkbaseService.ts +0 -53
- package/src/test-utils.ts +2 -0
- package/src/utils/command.ts +2 -0
- package/src/workbase/AGENTS.md +2 -0
- package/src/workbase/workbase-choice.ts +8 -0
package/README.md
CHANGED
|
@@ -71,11 +71,12 @@ workbase/
|
|
|
71
71
|
backend/
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
Agency
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
Agency keeps discovery and other observational commands read-only. Run
|
|
75
|
+
`agency integration status` to inspect `AGENTS.md` and
|
|
76
|
+
`.opencode/opencode.jsonc`, then `agency integration sync` to create missing
|
|
77
|
+
files or refresh checksum-safe managed files. The OpenCode config grants
|
|
78
|
+
external-directory access to task and epic references. Customized files are
|
|
79
|
+
reported but never overwritten.
|
|
79
80
|
|
|
80
81
|
Repository metadata comes directly from Git under `repos/{alias}`. Workbase
|
|
81
82
|
configuration may provide a custom writable-worktree creation command.
|
|
@@ -242,6 +243,8 @@ agency pr create refresh-copy
|
|
|
242
243
|
agency init [path] [--json]
|
|
243
244
|
agency workbase add <path> [--json]
|
|
244
245
|
agency workbase list [--json]
|
|
246
|
+
agency integration status [--json]
|
|
247
|
+
agency integration sync [--json]
|
|
245
248
|
agency repo add <alias> <remote> [--json]
|
|
246
249
|
agency repo link <alias> <path> [--json]
|
|
247
250
|
agency repo list [--json]
|
|
@@ -253,7 +256,8 @@ Registered workbases are stored in
|
|
|
253
256
|
repository. Alias names are then used by all documents and commands.
|
|
254
257
|
|
|
255
258
|
Commands that print Agency-owned results accept `--json`, including initialization,
|
|
256
|
-
repository mutations, entity creation/list/show,
|
|
259
|
+
integration inspection/sync, repository mutations, entity creation/list/show,
|
|
260
|
+
status, validation, and PR creation.
|
|
257
261
|
|
|
258
262
|
### Epics
|
|
259
263
|
|
|
@@ -270,7 +274,7 @@ back-reference.
|
|
|
270
274
|
### Tasks
|
|
271
275
|
|
|
272
276
|
Create a task interactively. Text prompts identify optional values, and known
|
|
273
|
-
choices use fzf
|
|
277
|
+
choices use fzf. This command requires a TTY and fails with `--no-input`:
|
|
274
278
|
|
|
275
279
|
```text
|
|
276
280
|
agency task new [id]
|
|
@@ -285,6 +289,8 @@ agency task create <id> --repo <alias>
|
|
|
285
289
|
```
|
|
286
290
|
|
|
287
291
|
The branch defaults to `task/<id>` and the base defaults to `main`.
|
|
292
|
+
`task create` is always noninteractive and requires `--repo` for a single-phase
|
|
293
|
+
task. Use it instead of `task new` in scripts and agent workflows.
|
|
288
294
|
|
|
289
295
|
Create a multi-phase task container:
|
|
290
296
|
|
|
@@ -293,6 +299,13 @@ agency task create <id> --multi-phase
|
|
|
293
299
|
[--ticket-url <url>] [--description <text>] [--epic <id>] [--json]
|
|
294
300
|
```
|
|
295
301
|
|
|
302
|
+
### Noninteractive Use
|
|
303
|
+
|
|
304
|
+
Agency never prompts when `--no-input` is set or stdin/stderr are not TTYs.
|
|
305
|
+
Commands with explicit inputs continue normally. `task new` fails immediately;
|
|
306
|
+
`work` requires an explicit directory, task ID, or `--epic` and must run from a
|
|
307
|
+
workbase; `validate` requires an explicit path or must run from a workbase.
|
|
308
|
+
|
|
296
309
|
Inspect tasks:
|
|
297
310
|
|
|
298
311
|
```text
|
package/cli.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { parseArgs } from "util"
|
|
4
3
|
import { Effect, Either, Layer } from "effect"
|
|
4
|
+
import { parseCli } from "./src/cli-parser"
|
|
5
5
|
import { init, help as initHelp } from "./src/commands/init"
|
|
6
6
|
import { task, help as taskHelp } from "./src/commands/task"
|
|
7
7
|
import { pr, help as prHelp } from "./src/commands/pr"
|
|
@@ -13,6 +13,10 @@ import { epic, help as epicHelp } from "./src/commands/epic"
|
|
|
13
13
|
import { phase, help as phaseHelp } from "./src/commands/phase"
|
|
14
14
|
import { archive, help as archiveHelp } from "./src/commands/archive"
|
|
15
15
|
import { workbase, help as workbaseHelp } from "./src/commands/workbase"
|
|
16
|
+
import {
|
|
17
|
+
integration,
|
|
18
|
+
help as integrationHelp,
|
|
19
|
+
} from "./src/commands/integration"
|
|
16
20
|
import type { Command } from "./src/types"
|
|
17
21
|
import { FileSystemService } from "./src/services/FileSystemService"
|
|
18
22
|
import { WorkbaseService } from "./src/services/WorkbaseService"
|
|
@@ -23,6 +27,7 @@ import { PhaseService } from "./src/services/PhaseService"
|
|
|
23
27
|
import { WorktreeService } from "./src/services/WorktreeService"
|
|
24
28
|
import { PullRequestService } from "./src/services/PullRequestService"
|
|
25
29
|
import { ArchiveService } from "./src/services/ArchiveService"
|
|
30
|
+
import { IntegrationService } from "./src/services/IntegrationService"
|
|
26
31
|
|
|
27
32
|
// Create CLI layer with all services
|
|
28
33
|
const CliLayer = Layer.mergeAll(
|
|
@@ -35,6 +40,7 @@ const CliLayer = Layer.mergeAll(
|
|
|
35
40
|
WorktreeService.Default,
|
|
36
41
|
PullRequestService.Default,
|
|
37
42
|
ArchiveService.Default,
|
|
43
|
+
IntegrationService.Default,
|
|
38
44
|
)
|
|
39
45
|
|
|
40
46
|
/**
|
|
@@ -191,6 +197,22 @@ const commands: Record<string, Command> = {
|
|
|
191
197
|
)
|
|
192
198
|
},
|
|
193
199
|
},
|
|
200
|
+
integration: {
|
|
201
|
+
run: async (args: string[], options: Record<string, any>) => {
|
|
202
|
+
if (options.help) {
|
|
203
|
+
console.log(integrationHelp)
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
await runCommand(
|
|
207
|
+
integration({
|
|
208
|
+
subcommand: args[0],
|
|
209
|
+
json: options.json,
|
|
210
|
+
silent: options.silent,
|
|
211
|
+
verbose: options.verbose,
|
|
212
|
+
}),
|
|
213
|
+
)
|
|
214
|
+
},
|
|
215
|
+
},
|
|
194
216
|
repo: {
|
|
195
217
|
run: async (args: string[], options: Record<string, any>) => {
|
|
196
218
|
if (options.help) {
|
|
@@ -229,6 +251,7 @@ const commands: Record<string, Command> = {
|
|
|
229
251
|
json: options.json,
|
|
230
252
|
silent: options.silent,
|
|
231
253
|
verbose: options.verbose,
|
|
254
|
+
inputAllowed: options.inputAllowed,
|
|
232
255
|
}),
|
|
233
256
|
)
|
|
234
257
|
},
|
|
@@ -239,12 +262,6 @@ const commands: Record<string, Command> = {
|
|
|
239
262
|
console.log(workHelp)
|
|
240
263
|
return
|
|
241
264
|
}
|
|
242
|
-
if (args.length > 1) {
|
|
243
|
-
throw new Error(
|
|
244
|
-
"Usage: agency work [<directory-or-task-id> | --epic <epic-id>]",
|
|
245
|
-
)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
265
|
await runCommand(
|
|
249
266
|
work({
|
|
250
267
|
directory: args[0],
|
|
@@ -253,6 +270,7 @@ const commands: Record<string, Command> = {
|
|
|
253
270
|
verbose: options.verbose,
|
|
254
271
|
opencode: options.opencode,
|
|
255
272
|
claude: options.claude,
|
|
273
|
+
inputAllowed: options.inputAllowed,
|
|
256
274
|
}),
|
|
257
275
|
)
|
|
258
276
|
},
|
|
@@ -284,6 +302,7 @@ const commands: Record<string, Command> = {
|
|
|
284
302
|
silent: options.silent,
|
|
285
303
|
verbose: options.verbose,
|
|
286
304
|
json: options.json,
|
|
305
|
+
inputAllowed: options.inputAllowed,
|
|
287
306
|
}),
|
|
288
307
|
)
|
|
289
308
|
},
|
|
@@ -299,6 +318,7 @@ Usage: agency <command> [options]
|
|
|
299
318
|
Commands:
|
|
300
319
|
init [path] Initialize an Agency workbase
|
|
301
320
|
workbase <subcommand> Manage registered workbases
|
|
321
|
+
integration <command> Inspect or sync managed integration files
|
|
302
322
|
epic <subcommand> Manage epics
|
|
303
323
|
phase <subcommand> Manage task phases
|
|
304
324
|
archive <type> Archive a work item
|
|
@@ -314,6 +334,7 @@ Global Options:
|
|
|
314
334
|
-V, --version Show version number
|
|
315
335
|
-s, --silent Suppress output messages
|
|
316
336
|
-v, --verbose Show verbose output including detailed debugging info
|
|
337
|
+
--no-input Never open an interactive prompt or selector
|
|
317
338
|
|
|
318
339
|
Examples:
|
|
319
340
|
agency init # Initialize the current directory
|
|
@@ -327,29 +348,7 @@ For more information about a command, run:
|
|
|
327
348
|
|
|
328
349
|
try {
|
|
329
350
|
const args = process.argv.slice(2)
|
|
330
|
-
const {
|
|
331
|
-
args,
|
|
332
|
-
options: {
|
|
333
|
-
help: {
|
|
334
|
-
type: "boolean",
|
|
335
|
-
short: "h",
|
|
336
|
-
},
|
|
337
|
-
version: {
|
|
338
|
-
type: "boolean",
|
|
339
|
-
short: "V",
|
|
340
|
-
},
|
|
341
|
-
silent: {
|
|
342
|
-
type: "boolean",
|
|
343
|
-
short: "s",
|
|
344
|
-
},
|
|
345
|
-
verbose: {
|
|
346
|
-
type: "boolean",
|
|
347
|
-
short: "v",
|
|
348
|
-
},
|
|
349
|
-
},
|
|
350
|
-
strict: false,
|
|
351
|
-
allowPositionals: true,
|
|
352
|
-
})
|
|
351
|
+
const { commandName, args: commandArgs, values } = parseCli(args)
|
|
353
352
|
|
|
354
353
|
// Handle global flags
|
|
355
354
|
if (values.version) {
|
|
@@ -358,80 +357,16 @@ try {
|
|
|
358
357
|
}
|
|
359
358
|
|
|
360
359
|
// Get command
|
|
361
|
-
const commandName = positionals[0]
|
|
362
|
-
|
|
363
360
|
// Show help if no command
|
|
364
361
|
if (!commandName) {
|
|
365
362
|
showMainHelp()
|
|
366
363
|
process.exit(values.help ? 0 : 1)
|
|
367
364
|
}
|
|
368
365
|
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
console.error("\nRun 'agency --help' for usage information.")
|
|
374
|
-
process.exit(1)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const commandIndex = args.indexOf(commandName)
|
|
378
|
-
const commandArgs = [
|
|
379
|
-
...args.slice(0, commandIndex),
|
|
380
|
-
...args.slice(commandIndex + 1),
|
|
381
|
-
]
|
|
382
|
-
const { values: cmdValues, positionals: cmdPositionals } = parseArgs({
|
|
383
|
-
args: commandArgs,
|
|
384
|
-
options: {
|
|
385
|
-
help: {
|
|
386
|
-
type: "boolean",
|
|
387
|
-
short: "h",
|
|
388
|
-
},
|
|
389
|
-
silent: {
|
|
390
|
-
type: "boolean",
|
|
391
|
-
short: "s",
|
|
392
|
-
},
|
|
393
|
-
verbose: {
|
|
394
|
-
type: "boolean",
|
|
395
|
-
short: "v",
|
|
396
|
-
},
|
|
397
|
-
branch: {
|
|
398
|
-
type: "string",
|
|
399
|
-
},
|
|
400
|
-
json: {
|
|
401
|
-
type: "boolean",
|
|
402
|
-
},
|
|
403
|
-
"ticket-url": {
|
|
404
|
-
type: "string",
|
|
405
|
-
},
|
|
406
|
-
description: {
|
|
407
|
-
type: "string",
|
|
408
|
-
},
|
|
409
|
-
repo: {
|
|
410
|
-
type: "string",
|
|
411
|
-
multiple: true,
|
|
412
|
-
},
|
|
413
|
-
reference: {
|
|
414
|
-
type: "string",
|
|
415
|
-
multiple: true,
|
|
416
|
-
},
|
|
417
|
-
epic: { type: "string" },
|
|
418
|
-
base: { type: "string" },
|
|
419
|
-
"multi-phase": { type: "boolean" },
|
|
420
|
-
"depends-on": { type: "string", multiple: true },
|
|
421
|
-
"first-phase": { type: "string" },
|
|
422
|
-
draft: { type: "boolean" },
|
|
423
|
-
opencode: {
|
|
424
|
-
type: "boolean",
|
|
425
|
-
},
|
|
426
|
-
claude: {
|
|
427
|
-
type: "boolean",
|
|
428
|
-
},
|
|
429
|
-
},
|
|
430
|
-
strict: false,
|
|
431
|
-
allowPositionals: true,
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
await command.run(cmdPositionals, cmdValues)
|
|
366
|
+
const command = commands[commandName]!
|
|
367
|
+
const inputAllowed =
|
|
368
|
+
!values["no-input"] && Boolean(process.stdin.isTTY && process.stderr.isTTY)
|
|
369
|
+
await command.run(commandArgs, { ...values, inputAllowed })
|
|
435
370
|
} catch (error) {
|
|
436
371
|
if (error instanceof Error) {
|
|
437
372
|
let message = error.message
|
package/package.json
CHANGED
package/skills/agency/SKILL.md
CHANGED
|
@@ -39,6 +39,7 @@ From anywhere beneath a workbase, run:
|
|
|
39
39
|
```bash
|
|
40
40
|
agency status --json
|
|
41
41
|
agency validate --json
|
|
42
|
+
agency integration status --json
|
|
42
43
|
agency repo list --json
|
|
43
44
|
agency epic list --json
|
|
44
45
|
agency task list --json
|
|
@@ -56,6 +57,7 @@ If no workbase is found, do not initialize one without user intent. When asked:
|
|
|
56
57
|
|
|
57
58
|
```bash
|
|
58
59
|
agency init [path]
|
|
60
|
+
agency integration sync
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
Register known workbases so `agency work` can select one when run elsewhere:
|
|
@@ -108,7 +110,9 @@ Epic task ordering and dependencies live in `EPIC.md`. Creating a task with
|
|
|
108
110
|
## Create Single-Phase Tasks
|
|
109
111
|
|
|
110
112
|
For guided creation, run `agency task new`. It prompts for text input, uses fzf
|
|
111
|
-
for known choices, and allows optional inputs to be skipped.
|
|
113
|
+
for known choices, and allows optional inputs to be skipped. It requires a TTY
|
|
114
|
+
and fails when `--no-input` is set. `task create` never prompts and is the command
|
|
115
|
+
to use from agents and scripts.
|
|
112
116
|
|
|
113
117
|
```bash
|
|
114
118
|
agency task create <id> \
|
|
@@ -241,6 +245,7 @@ validation errors before materializing worktrees or creating PRs. Validation
|
|
|
241
245
|
checks schemas, aliases, backlinks, phase directories, duplicate references,
|
|
242
246
|
duplicate writable branch ownership, unknown dependencies, and dependency cycles.
|
|
243
247
|
Outside a workbase, omitting path opens the registered-workbase picker.
|
|
248
|
+
With `--no-input` or without a TTY, pass a path or run from a workbase instead.
|
|
244
249
|
|
|
245
250
|
## Worktrees And Agent Launch
|
|
246
251
|
|
|
@@ -256,6 +261,8 @@ replaces the current process with the selected agent. With no directory it opens
|
|
|
256
261
|
an `fzf` picker containing the workbase hierarchy. Pass `.`, or another
|
|
257
262
|
directory, to infer the nearest epic, task, or phase.
|
|
258
263
|
Outside a workbase, it first opens a picker containing registered workbases.
|
|
264
|
+
With `--no-input` or without a TTY, run from a workbase and provide an explicit
|
|
265
|
+
directory, task ID, or `--epic` so no picker is needed.
|
|
259
266
|
|
|
260
267
|
Epic and multi-phase task targets are orchestration sessions launched beside
|
|
261
268
|
their documents. Single-phase tasks and phases are execution sessions launched
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { parseCli } from "./cli-parser"
|
|
3
|
+
|
|
4
|
+
const expectUsageError = (args: string[], usage: string) => {
|
|
5
|
+
expect(() => parseCli(args)).toThrow(`Usage: ${usage}`)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe("strict CLI parsing", () => {
|
|
9
|
+
test("rejects misspelled and command-inapplicable options", () => {
|
|
10
|
+
expectUsageError(["task", "list", "--josn"], "agency task")
|
|
11
|
+
expectUsageError(
|
|
12
|
+
["task", "list", "--repo", "agency"],
|
|
13
|
+
"agency task list [--json]",
|
|
14
|
+
)
|
|
15
|
+
expectUsageError(["status", "--draft"], "agency status [--json]")
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test("rejects duplicate scalar, boolean, and single-value multiple options", () => {
|
|
19
|
+
for (const args of [
|
|
20
|
+
["status", "--json", "--json"],
|
|
21
|
+
["task", "create", "example", "--repo", "one", "--repo", "two"],
|
|
22
|
+
[
|
|
23
|
+
"task",
|
|
24
|
+
"create",
|
|
25
|
+
"example",
|
|
26
|
+
"--repo",
|
|
27
|
+
"one",
|
|
28
|
+
"--base",
|
|
29
|
+
"a",
|
|
30
|
+
"--base",
|
|
31
|
+
"b",
|
|
32
|
+
],
|
|
33
|
+
["status", "-s", "--silent"],
|
|
34
|
+
]) {
|
|
35
|
+
expect(() => parseCli(args)).toThrow("may only be specified once")
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test("preserves explicitly repeatable options", () => {
|
|
40
|
+
expect(
|
|
41
|
+
parseCli([
|
|
42
|
+
"epic",
|
|
43
|
+
"create",
|
|
44
|
+
"delivery",
|
|
45
|
+
"--ticket-url",
|
|
46
|
+
"https://example.com",
|
|
47
|
+
"--repo",
|
|
48
|
+
"one:main",
|
|
49
|
+
"--repo",
|
|
50
|
+
"two:main",
|
|
51
|
+
]).values.repo,
|
|
52
|
+
).toEqual(["one:main", "two:main"])
|
|
53
|
+
expect(
|
|
54
|
+
parseCli([
|
|
55
|
+
"phase",
|
|
56
|
+
"create",
|
|
57
|
+
"task",
|
|
58
|
+
"phase",
|
|
59
|
+
"--repo",
|
|
60
|
+
"one",
|
|
61
|
+
"--branch",
|
|
62
|
+
"feature",
|
|
63
|
+
"--base",
|
|
64
|
+
"main",
|
|
65
|
+
"--reference",
|
|
66
|
+
"two:main",
|
|
67
|
+
"--reference",
|
|
68
|
+
"three:main",
|
|
69
|
+
"--depends-on",
|
|
70
|
+
"first",
|
|
71
|
+
"--depends-on",
|
|
72
|
+
"second",
|
|
73
|
+
]).values,
|
|
74
|
+
).toMatchObject({
|
|
75
|
+
reference: ["two:main", "three:main"],
|
|
76
|
+
"depends-on": ["first", "second"],
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("enforces exact maximum positional arity for every leaf command", () => {
|
|
81
|
+
for (const [args, usage] of [
|
|
82
|
+
[["init", "one", "two"], "agency init"],
|
|
83
|
+
[["workbase", "add", "one", "two"], "agency workbase add"],
|
|
84
|
+
[["workbase", "list", "extra"], "agency workbase list"],
|
|
85
|
+
[["integration", "status", "extra"], "agency integration status"],
|
|
86
|
+
[["integration", "sync", "extra"], "agency integration sync"],
|
|
87
|
+
[["repo", "add", "a", "b", "extra"], "agency repo add"],
|
|
88
|
+
[["repo", "link", "a", "b", "extra"], "agency repo link"],
|
|
89
|
+
[["repo", "list", "extra"], "agency repo list"],
|
|
90
|
+
[
|
|
91
|
+
[
|
|
92
|
+
"epic",
|
|
93
|
+
"create",
|
|
94
|
+
"one",
|
|
95
|
+
"two",
|
|
96
|
+
"--ticket-url",
|
|
97
|
+
"url",
|
|
98
|
+
"--repo",
|
|
99
|
+
"repo:main",
|
|
100
|
+
],
|
|
101
|
+
"agency epic create",
|
|
102
|
+
],
|
|
103
|
+
[["epic", "list", "extra"], "agency epic list"],
|
|
104
|
+
[["epic", "show", "one", "two"], "agency epic show"],
|
|
105
|
+
[["task", "new", "one", "two"], "agency task new"],
|
|
106
|
+
[
|
|
107
|
+
["task", "create", "one", "two", "--repo", "repo"],
|
|
108
|
+
"agency task create",
|
|
109
|
+
],
|
|
110
|
+
[["task", "list", "extra"], "agency task list"],
|
|
111
|
+
[["task", "show", "one", "two"], "agency task show"],
|
|
112
|
+
[["task", "status", "one", "open", "extra"], "agency task status"],
|
|
113
|
+
[
|
|
114
|
+
[
|
|
115
|
+
"phase",
|
|
116
|
+
"create",
|
|
117
|
+
"task",
|
|
118
|
+
"phase",
|
|
119
|
+
"extra",
|
|
120
|
+
"--repo",
|
|
121
|
+
"repo",
|
|
122
|
+
"--branch",
|
|
123
|
+
"branch",
|
|
124
|
+
"--base",
|
|
125
|
+
"main",
|
|
126
|
+
],
|
|
127
|
+
"agency phase create",
|
|
128
|
+
],
|
|
129
|
+
[["phase", "list", "one", "two"], "agency phase list"],
|
|
130
|
+
[["phase", "show", "one", "two", "three"], "agency phase show"],
|
|
131
|
+
[
|
|
132
|
+
["phase", "status", "one", "two", "open", "extra"],
|
|
133
|
+
"agency phase status",
|
|
134
|
+
],
|
|
135
|
+
[["archive", "epic", "one", "two"], "agency archive epic"],
|
|
136
|
+
[["archive", "task", "one", "two"], "agency archive task"],
|
|
137
|
+
[["archive", "phase", "one", "two", "three"], "agency archive phase"],
|
|
138
|
+
[["work", "one", "two"], "agency work"],
|
|
139
|
+
[["pr", "create", "one", "two", "three"], "agency pr create"],
|
|
140
|
+
[["status", "extra"], "agency status"],
|
|
141
|
+
[["validate", "one", "two"], "agency validate"],
|
|
142
|
+
] as const) {
|
|
143
|
+
expectUsageError([...args], usage)
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test("reports unknown subcommands with parent usage", () => {
|
|
148
|
+
expect(() => parseCli(["task", "crate"])).toThrow(
|
|
149
|
+
"Unknown subcommand 'crate'",
|
|
150
|
+
)
|
|
151
|
+
expectUsageError(
|
|
152
|
+
["task", "crate"],
|
|
153
|
+
"agency task <new|create|list|show|status>",
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test("rejects required-option omissions and explicit conflicts", () => {
|
|
158
|
+
expect(() => parseCli(["task", "create", "example"])).toThrow(
|
|
159
|
+
"--repo' is required",
|
|
160
|
+
)
|
|
161
|
+
for (const option of ["repo", "reference", "branch", "base"] as const) {
|
|
162
|
+
const value = option === "reference" ? "other:main" : "value"
|
|
163
|
+
expect(() =>
|
|
164
|
+
parseCli([
|
|
165
|
+
"task",
|
|
166
|
+
"create",
|
|
167
|
+
"example",
|
|
168
|
+
"--multi-phase",
|
|
169
|
+
`--${option}`,
|
|
170
|
+
value,
|
|
171
|
+
]),
|
|
172
|
+
).toThrow("cannot be combined")
|
|
173
|
+
}
|
|
174
|
+
expect(() =>
|
|
175
|
+
parseCli(["task", "new", "example", "--multi-phase", "--repo", "repo"]),
|
|
176
|
+
).toThrow("cannot be combined")
|
|
177
|
+
expect(() => parseCli(["work", "--opencode", "--claude"])).toThrow(
|
|
178
|
+
"cannot be combined",
|
|
179
|
+
)
|
|
180
|
+
expect(() => parseCli(["work", "task", "--epic", "epic"])).toThrow(
|
|
181
|
+
"cannot be combined",
|
|
182
|
+
)
|
|
183
|
+
expect(() => parseCli(["status", "--silent", "--verbose"])).toThrow(
|
|
184
|
+
"cannot be combined",
|
|
185
|
+
)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test("accepts the global interaction control on every command", () => {
|
|
189
|
+
expect(parseCli(["--no-input", "task", "new"]).values["no-input"]).toBe(
|
|
190
|
+
true,
|
|
191
|
+
)
|
|
192
|
+
expect(parseCli(["work", "--no-input"]).values["no-input"]).toBe(true)
|
|
193
|
+
expect(parseCli(["validate", "--no-input"]).values["no-input"]).toBe(true)
|
|
194
|
+
expect(parseCli(["status", "--no-input"]).values["no-input"]).toBe(true)
|
|
195
|
+
expect(
|
|
196
|
+
parseCli(["task", "create", "example", "--repo", "repo", "--no-input"])
|
|
197
|
+
.values["no-input"],
|
|
198
|
+
).toBe(true)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test("accepts grouped global short options before a command", () => {
|
|
202
|
+
const parsed = parseCli(["-sh", "task"])
|
|
203
|
+
expect(parsed.values).toMatchObject({ silent: true, help: true })
|
|
204
|
+
})
|
|
205
|
+
})
|