@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-parser.ts CHANGED
@@ -25,6 +25,14 @@ const commonOptions = {
25
25
  silent: { type: "boolean", short: "s" },
26
26
  verbose: { type: "boolean", short: "v" },
27
27
  "no-input": { type: "boolean" },
28
+ workbase: { type: "string" },
29
+ cwd: { type: "string" },
30
+ } satisfies OptionConfig
31
+
32
+ const entitySelectorOptions = {
33
+ epic: { type: "string" },
34
+ task: { type: "string" },
35
+ phase: { type: "string" },
28
36
  } satisfies OptionConfig
29
37
 
30
38
  const outputOptions = {
@@ -32,6 +40,29 @@ const outputOptions = {
32
40
  json: { type: "boolean" },
33
41
  } satisfies OptionConfig
34
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
+
35
66
  const createOptions = {
36
67
  ...outputOptions,
37
68
  "ticket-url": { type: "string" },
@@ -86,14 +117,18 @@ const commands = {
86
117
  },
87
118
  },
88
119
  workbase: {
89
- usage: "agency workbase <add|list>",
90
- options: outputOptions,
120
+ usage: "agency workbase <add|list|remove|prune|default>",
121
+ options: {
122
+ ...outputOptions,
123
+ name: { type: "string" },
124
+ clear: { type: "boolean" },
125
+ },
91
126
  subcommands: {
92
127
  add: {
93
128
  usage: "agency workbase add <path> [--json]",
94
129
  minArgs: 1,
95
130
  maxArgs: 1,
96
- options: ["json"],
131
+ options: ["json", "name"],
97
132
  },
98
133
  list: {
99
134
  usage: "agency workbase list [--json]",
@@ -101,6 +136,25 @@ const commands = {
101
136
  maxArgs: 0,
102
137
  options: ["json"],
103
138
  },
139
+ remove: {
140
+ usage: "agency workbase remove <selector> [--json]",
141
+ minArgs: 1,
142
+ maxArgs: 1,
143
+ options: ["json"],
144
+ },
145
+ prune: {
146
+ usage: "agency workbase prune [--json]",
147
+ minArgs: 0,
148
+ maxArgs: 0,
149
+ options: ["json"],
150
+ },
151
+ default: {
152
+ usage: "agency workbase default [selector | --clear] [--json]",
153
+ minArgs: 0,
154
+ maxArgs: 1,
155
+ options: ["json", "clear"],
156
+ conflicts: [["clear", "$positional"]],
157
+ },
104
158
  },
105
159
  },
106
160
  integration: {
@@ -147,7 +201,7 @@ const commands = {
147
201
  },
148
202
  epic: {
149
203
  usage: "agency epic <create|list|show>",
150
- options: createOptions,
204
+ options: { ...createOptions, ...viewOptions, epic: { type: "string" } },
151
205
  subcommands: {
152
206
  create: {
153
207
  usage:
@@ -159,22 +213,24 @@ const commands = {
159
213
  repeatable: ["repo"],
160
214
  },
161
215
  list: {
162
- usage: "agency epic list [--json]",
216
+ usage: "agency epic list [filters] [--json]",
163
217
  minArgs: 0,
164
218
  maxArgs: 0,
165
- options: ["json"],
219
+ options: ["json", ...viewOptionNames],
220
+ repeatable: ["status", "repository"],
221
+ conflicts: viewConflicts,
166
222
  },
167
223
  show: {
168
224
  usage: "agency epic show <id> [--json]",
169
225
  minArgs: 1,
170
226
  maxArgs: 1,
171
- options: ["json"],
227
+ options: ["json", "epic"],
172
228
  },
173
229
  },
174
230
  },
175
231
  task: {
176
232
  usage: "agency task <new|create|list|show|status>",
177
- options: taskCreateOptions,
233
+ options: { ...taskCreateOptions, ...viewOptions, task: { type: "string" } },
178
234
  subcommands: {
179
235
  new: {
180
236
  usage: "agency task new [id] [options]",
@@ -212,28 +268,35 @@ const commands = {
212
268
  repeatable: ["reference"],
213
269
  },
214
270
  list: {
215
- usage: "agency task list [--json]",
271
+ usage: "agency task list [filters] [--json]",
216
272
  minArgs: 0,
217
273
  maxArgs: 0,
218
- options: ["json"],
274
+ options: ["json", ...viewOptionNames],
275
+ repeatable: ["status", "repository"],
276
+ conflicts: viewConflicts,
219
277
  },
220
278
  show: {
221
279
  usage: "agency task show <id> [--json]",
222
280
  minArgs: 1,
223
281
  maxArgs: 1,
224
- options: ["json"],
282
+ options: ["json", "task"],
225
283
  },
226
284
  status: {
227
285
  usage: "agency task status <id> <status> [--json]",
228
286
  minArgs: 2,
229
287
  maxArgs: 2,
230
- options: ["json"],
288
+ options: ["json", "task"],
231
289
  },
232
290
  },
233
291
  },
234
292
  phase: {
235
293
  usage: "agency phase <create|list|show|status>",
236
- options: phaseCreateOptions,
294
+ options: {
295
+ ...phaseCreateOptions,
296
+ ...viewOptions,
297
+ task: { type: "string" },
298
+ phase: { type: "string" },
299
+ },
237
300
  subcommands: {
238
301
  create: {
239
302
  usage:
@@ -249,33 +312,41 @@ const commands = {
249
312
  "depends-on",
250
313
  "first-phase",
251
314
  "json",
315
+ "task",
316
+ "phase",
252
317
  ],
253
318
  required: ["repo", "branch", "base"],
254
319
  repeatable: ["reference", "depends-on"],
255
320
  },
256
321
  list: {
257
- usage: "agency phase list <task-id> [--json]",
322
+ usage: "agency phase list <task-id> [filters] [--json]",
258
323
  minArgs: 1,
259
324
  maxArgs: 1,
260
- options: ["json"],
325
+ options: ["json", "task", ...viewOptionNames],
326
+ repeatable: ["status", "repository"],
327
+ conflicts: viewConflicts,
261
328
  },
262
329
  show: {
263
330
  usage: "agency phase show <task-id> <phase-id> [--json]",
264
331
  minArgs: 2,
265
332
  maxArgs: 2,
266
- options: ["json"],
333
+ options: ["json", "task", "phase"],
267
334
  },
268
335
  status: {
269
336
  usage: "agency phase status <task-id> <phase-id> <status> [--json]",
270
337
  minArgs: 3,
271
338
  maxArgs: 3,
272
- options: ["json"],
339
+ options: ["json", "task", "phase"],
273
340
  },
274
341
  },
275
342
  },
276
343
  claim: {
277
344
  usage: "agency claim <task-id> [phase-id] [options]",
278
- options: claimOptions,
345
+ options: {
346
+ ...claimOptions,
347
+ task: { type: "string" },
348
+ phase: { type: "string" },
349
+ },
279
350
  command: {
280
351
  usage:
281
352
  "agency claim <task-id> [phase-id] --claimant <id> --runner <id> --session-id <id> --revision <sha256> [--expires-at <timestamp>] [--json]",
@@ -288,19 +359,25 @@ const commands = {
288
359
  "revision",
289
360
  "expires-at",
290
361
  "json",
362
+ "task",
363
+ "phase",
291
364
  ],
292
365
  required: ["claimant", "runner", "session-id", "revision"],
293
366
  },
294
367
  },
295
368
  release: {
296
369
  usage: "agency release <task-id> [phase-id] [options]",
297
- options: ownedClaimOptions,
370
+ options: {
371
+ ...ownedClaimOptions,
372
+ task: { type: "string" },
373
+ phase: { type: "string" },
374
+ },
298
375
  command: {
299
376
  usage:
300
377
  "agency release <task-id> [phase-id] --session-id <id> --revision <sha256> [--json]",
301
378
  minArgs: 1,
302
379
  maxArgs: 2,
303
- options: ["session-id", "revision", "json"],
380
+ options: ["session-id", "revision", "json", "task", "phase"],
304
381
  required: ["session-id", "revision"],
305
382
  },
306
383
  },
@@ -309,13 +386,15 @@ const commands = {
309
386
  options: {
310
387
  ...ownedClaimOptions,
311
388
  outcome: { type: "string" },
389
+ task: { type: "string" },
390
+ phase: { type: "string" },
312
391
  },
313
392
  command: {
314
393
  usage:
315
394
  "agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped> [--json]",
316
395
  minArgs: 1,
317
396
  maxArgs: 2,
318
- options: ["session-id", "revision", "outcome", "json"],
397
+ options: ["session-id", "revision", "outcome", "json", "task", "phase"],
319
398
  required: ["session-id", "revision", "outcome"],
320
399
  },
321
400
  },
@@ -336,25 +415,25 @@ const commands = {
336
415
  },
337
416
  archive: {
338
417
  usage: "agency archive <epic|task|phase>",
339
- options: outputOptions,
418
+ options: { ...outputOptions, ...entitySelectorOptions },
340
419
  subcommands: {
341
420
  epic: {
342
421
  usage: "agency archive epic <epic-id> [--json]",
343
422
  minArgs: 1,
344
423
  maxArgs: 1,
345
- options: ["json"],
424
+ options: ["json", "epic"],
346
425
  },
347
426
  task: {
348
427
  usage: "agency archive task <task-id> [--json]",
349
428
  minArgs: 1,
350
429
  maxArgs: 1,
351
- options: ["json"],
430
+ options: ["json", "task"],
352
431
  },
353
432
  phase: {
354
433
  usage: "agency archive phase <task-id> <phase-id> [--json]",
355
434
  minArgs: 2,
356
435
  maxArgs: 2,
357
- options: ["json"],
436
+ options: ["json", "task", "phase"],
358
437
  },
359
438
  },
360
439
  },
@@ -363,9 +442,9 @@ const commands = {
363
442
  "agency work [<directory-or-task-id> | --epic <epic-id>] | agency work prepare [target] [--dry-run] [--json]",
364
443
  options: {
365
444
  ...commonOptions,
445
+ ...entitySelectorOptions,
366
446
  json: { type: "boolean" },
367
447
  "dry-run": { type: "boolean" },
368
- epic: { type: "string" },
369
448
  opencode: { type: "boolean" },
370
449
  claude: { type: "boolean" },
371
450
  force: { type: "boolean" },
@@ -375,7 +454,16 @@ const commands = {
375
454
  "agency work [<directory-or-task-id> | --epic <epic-id>] | agency work prepare [target] [--dry-run] [--json]",
376
455
  minArgs: 0,
377
456
  maxArgs: 2,
378
- options: ["json", "dry-run", "epic", "opencode", "claude", "force"],
457
+ options: [
458
+ "json",
459
+ "dry-run",
460
+ "epic",
461
+ "task",
462
+ "phase",
463
+ "opencode",
464
+ "claude",
465
+ "force",
466
+ ],
379
467
  conflicts: [
380
468
  ["opencode", "claude"],
381
469
  ["epic", "$positional"],
@@ -388,6 +476,8 @@ const commands = {
388
476
  ...outputOptions,
389
477
  draft: { type: "boolean" },
390
478
  force: { type: "boolean" },
479
+ task: { type: "string" },
480
+ phase: { type: "string" },
391
481
  },
392
482
  subcommands: {
393
483
  create: {
@@ -395,7 +485,7 @@ const commands = {
395
485
  "agency pr create <task-id> [phase-id] [--draft] [--force] [--json]",
396
486
  minArgs: 1,
397
487
  maxArgs: 2,
398
- options: ["draft", "force", "json"],
488
+ options: ["draft", "force", "json", "task", "phase"],
399
489
  },
400
490
  },
401
491
  },
@@ -413,13 +503,15 @@ const commands = {
413
503
  },
414
504
  },
415
505
  status: {
416
- usage: "agency status [--json]",
417
- options: outputOptions,
506
+ usage: "agency status [filters] [--json]",
507
+ options: { ...outputOptions, ...viewOptions },
418
508
  command: {
419
- usage: "agency status [--json]",
509
+ usage: "agency status [filters] [--json]",
420
510
  minArgs: 0,
421
511
  maxArgs: 0,
422
- options: ["json"],
512
+ options: ["json", ...viewOptionNames],
513
+ repeatable: ["status", "repository"],
514
+ conflicts: viewConflicts,
423
515
  },
424
516
  },
425
517
  validate: {
@@ -438,13 +530,14 @@ const commands = {
438
530
  usage: "agency context [target] [--json] [--compact]",
439
531
  options: {
440
532
  ...outputOptions,
533
+ ...entitySelectorOptions,
441
534
  compact: { type: "boolean" },
442
535
  },
443
536
  command: {
444
537
  usage: "agency context [target] [--json] [--compact]",
445
538
  minArgs: 0,
446
539
  maxArgs: 1,
447
- options: ["json", "compact"],
540
+ options: ["json", "compact", "epic", "task", "phase"],
448
541
  },
449
542
  },
450
543
  graph: {
@@ -495,6 +588,7 @@ const preCommandOptions = new Set([
495
588
  "-v",
496
589
  "--no-input",
497
590
  ])
591
+ const preCommandValueOptions = new Set(["--workbase", "--cwd"])
498
592
 
499
593
  export interface ParsedCli {
500
594
  readonly commandName?: keyof typeof commands
@@ -523,8 +617,22 @@ const usageError = (message: string, usage: string) =>
523
617
  const optionLabel = (name: string) => `--${name}`
524
618
 
525
619
  function findCommandIndex(args: readonly string[]) {
526
- for (const [index, argument] of args.entries()) {
620
+ for (let index = 0; index < args.length; index++) {
621
+ const argument = args[index]!
527
622
  if (!argument.startsWith("-")) return index
623
+ if (argument.startsWith("--workbase=") || argument.startsWith("--cwd=")) {
624
+ continue
625
+ }
626
+ if (preCommandValueOptions.has(argument)) {
627
+ if (args[index + 1] === undefined) {
628
+ throw usageError(
629
+ `Option '${argument}' expects a value.`,
630
+ "agency <command> [options]",
631
+ )
632
+ }
633
+ index++
634
+ continue
635
+ }
528
636
  if (!preCommandOptions.has(argument) && !/^-[hVsv]+$/.test(argument)) {
529
637
  throw usageError(
530
638
  `Unknown option '${argument}'.`,
@@ -535,6 +643,87 @@ function findCommandIndex(args: readonly string[]) {
535
643
  return -1
536
644
  }
537
645
 
646
+ const targetSlots = (
647
+ commandName: string,
648
+ subcommand: string | undefined,
649
+ ): readonly ("epic" | "task" | "phase")[] => {
650
+ if (commandName === "epic" && subcommand === "show") return ["epic"]
651
+ if (commandName === "task" && ["show", "status"].includes(subcommand ?? ""))
652
+ return ["task"]
653
+ if (commandName === "phase")
654
+ return subcommand === "list" ? ["task"] : ["task", "phase"]
655
+ if (["claim", "release", "finish"].includes(commandName))
656
+ return ["task", "phase"]
657
+ if (commandName === "archive")
658
+ return subcommand === "epic"
659
+ ? ["epic"]
660
+ : subcommand === "task"
661
+ ? ["task"]
662
+ : ["task", "phase"]
663
+ if (commandName === "pr" && subcommand === "create") return ["task", "phase"]
664
+ return []
665
+ }
666
+
667
+ function applyEntitySelectors(
668
+ commandName: string,
669
+ subcommand: string | undefined,
670
+ positionals: readonly string[],
671
+ values: ParsedCli["values"],
672
+ spec: LeafCommand,
673
+ ) {
674
+ const supplied = ["epic", "task", "phase"].filter(
675
+ (name) => values[name] !== undefined,
676
+ )
677
+ if (supplied.length === 0) return [...positionals]
678
+ if (values.phase !== undefined && values.task === undefined) {
679
+ throw usageError("Option '--phase' requires '--task'.", spec.usage)
680
+ }
681
+ if (values.epic !== undefined && (values.task || values.phase)) {
682
+ throw usageError(
683
+ "Option '--epic' cannot be combined with '--task' or '--phase'.",
684
+ spec.usage,
685
+ )
686
+ }
687
+ if (commandName === "work" || commandName === "context") {
688
+ if (
689
+ positionals.length >
690
+ (commandName === "work" && positionals[0] === "prepare" ? 1 : 0)
691
+ ) {
692
+ throw usageError(
693
+ "Entity selector options cannot be combined with a positional target.",
694
+ spec.usage,
695
+ )
696
+ }
697
+ return [...positionals]
698
+ }
699
+
700
+ const slots = targetSlots(commandName, subcommand)
701
+ const selectedSlots = slots.filter((slot) => values[slot] !== undefined)
702
+ if (selectedSlots.length === 0) return [...positionals]
703
+ const trailingCount = spec.maxArgs - slots.length
704
+ if (positionals.length > trailingCount) {
705
+ throw usageError(
706
+ "Entity selector options cannot be combined with positional target IDs.",
707
+ spec.usage,
708
+ )
709
+ }
710
+ const requiredSlots = slots.slice(0, spec.minArgs - trailingCount)
711
+ for (const slot of requiredSlots) {
712
+ if (values[slot] === undefined) {
713
+ throw usageError(
714
+ `Option '--${slot}' is required with explicit selectors.`,
715
+ spec.usage,
716
+ )
717
+ }
718
+ }
719
+ return [
720
+ ...slots.flatMap((slot) =>
721
+ typeof values[slot] === "string" ? [values[slot] as string] : [],
722
+ ),
723
+ ...positionals,
724
+ ]
725
+ }
726
+
538
727
  function assertNoDuplicateOptions(
539
728
  tokens: readonly { readonly kind: string; readonly name?: string }[],
540
729
  repeatable: ReadonlySet<string>,
@@ -615,6 +804,24 @@ function validateGraphOptions(values: ParsedCli["values"], spec: LeafCommand) {
615
804
  }
616
805
  }
617
806
 
807
+ function validateViewOptions(values: ParsedCli["values"], spec: LeafCommand) {
808
+ const supplied = values.status
809
+ const statuses = Array.isArray(supplied)
810
+ ? supplied
811
+ : supplied === undefined
812
+ ? []
813
+ : [supplied]
814
+ const accepted = new Set(["open", "working", "delegated", "done", "dropped"])
815
+ for (const status of statuses) {
816
+ if (typeof status !== "string" || !accepted.has(status)) {
817
+ throw usageError(
818
+ `Invalid '--status' value '${String(status)}'. Expected one of: ${[...accepted].join(", ")}.`,
819
+ spec.usage,
820
+ )
821
+ }
822
+ }
823
+ }
824
+
618
825
  export function parseCli(args: readonly string[]): ParsedCli {
619
826
  const commandIndex = findCommandIndex(args)
620
827
  if (commandIndex === -1) {
@@ -674,7 +881,7 @@ export function parseCli(args: readonly string[]): ParsedCli {
674
881
  throw usageError(message, definition.usage)
675
882
  }
676
883
 
677
- const commandPositionals = definition.subcommands
884
+ let commandPositionals = definition.subcommands
678
885
  ? parsed.positionals.slice(1)
679
886
  : parsed.positionals
680
887
  const allowed = new Set([...commonOptionNames, ...(spec.options ?? [])])
@@ -694,6 +901,20 @@ export function parseCli(args: readonly string[]): ParsedCli {
694
901
  spec.usage,
695
902
  )
696
903
  }
904
+ for (const selector of ["workbase", "cwd", "epic", "task", "phase"]) {
905
+ if (parsed.values[selector] === "") {
906
+ throw usageError(
907
+ `Option '--${selector}' requires a non-empty value.`,
908
+ spec.usage,
909
+ )
910
+ }
911
+ }
912
+ if (parsed.values.workbase && parsed.values.cwd) {
913
+ throw usageError(
914
+ "Options '--workbase' and '--cwd' cannot be combined.",
915
+ spec.usage,
916
+ )
917
+ }
697
918
  if (parsed.values.version) {
698
919
  return {
699
920
  commandName: commandName as keyof typeof commands,
@@ -709,6 +930,14 @@ export function parseCli(args: readonly string[]): ParsedCli {
709
930
  }
710
931
  }
711
932
 
933
+ commandPositionals = applyEntitySelectors(
934
+ commandName,
935
+ subcommand,
936
+ commandPositionals,
937
+ parsed.values,
938
+ spec,
939
+ )
940
+
712
941
  if (
713
942
  commandPositionals.length < spec.minArgs ||
714
943
  commandPositionals.length > spec.maxArgs
@@ -745,6 +974,12 @@ export function parseCli(args: readonly string[]): ParsedCli {
745
974
  if (commandName === "graph") {
746
975
  validateGraphOptions(parsed.values, spec)
747
976
  }
977
+ if (
978
+ commandName === "status" ||
979
+ (["epic", "task", "phase"].includes(commandName) && subcommand === "list")
980
+ ) {
981
+ validateViewOptions(parsed.values, spec)
982
+ }
748
983
  if (
749
984
  commandName === "finish" &&
750
985
  parsed.values.outcome !== "done" &&
@@ -782,7 +1017,9 @@ export function parseCli(args: readonly string[]): ParsedCli {
782
1017
 
783
1018
  return {
784
1019
  commandName: commandName as keyof typeof commands,
785
- args: parsed.positionals,
1020
+ args: definition.subcommands
1021
+ ? [subcommand!, ...commandPositionals]
1022
+ : commandPositionals,
786
1023
  values: parsed.values,
787
1024
  }
788
1025
  }