@markjaquith/agency 2.16.0 → 2.18.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 +33 -4
- package/cli.ts +93 -8
- package/package.json +1 -1
- package/skills/agency/SKILL.md +13 -6
- package/src/cli-parser.test.ts +93 -0
- package/src/cli-parser.ts +229 -29
- package/src/cli.test.ts +144 -4
- package/src/commands/context.ts +3 -0
- package/src/commands/init.ts +3 -1
- package/src/commands/next.ts +64 -0
- package/src/commands/pr.ts +2 -0
- package/src/commands/read-only.test.ts +2 -0
- package/src/commands/validate.test.ts +2 -0
- package/src/commands/work.test.ts +85 -0
- package/src/commands/work.ts +40 -9
- package/src/commands/workbase.test.ts +63 -2
- package/src/commands/workbase.ts +77 -10
- package/src/protocol.ts +6 -0
- package/src/services/GraphService.test.ts +36 -0
- package/src/services/GraphService.ts +8 -1
- package/src/services/PullRequestService.test.ts +20 -1
- package/src/services/PullRequestService.ts +14 -1
- package/src/services/ReadinessService.test.ts +223 -0
- package/src/services/ReadinessService.ts +230 -0
- package/src/services/WorkbaseService.test.ts +147 -4
- package/src/services/WorkbaseService.ts +214 -12
- package/src/services/WorktreeService.test.ts +12 -0
- package/src/services/WorktreeService.ts +6 -2
- package/src/test-utils.ts +2 -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/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 = {
|
|
@@ -86,14 +94,18 @@ const commands = {
|
|
|
86
94
|
},
|
|
87
95
|
},
|
|
88
96
|
workbase: {
|
|
89
|
-
usage: "agency workbase <add|list>",
|
|
90
|
-
options:
|
|
97
|
+
usage: "agency workbase <add|list|remove|prune|default>",
|
|
98
|
+
options: {
|
|
99
|
+
...outputOptions,
|
|
100
|
+
name: { type: "string" },
|
|
101
|
+
clear: { type: "boolean" },
|
|
102
|
+
},
|
|
91
103
|
subcommands: {
|
|
92
104
|
add: {
|
|
93
105
|
usage: "agency workbase add <path> [--json]",
|
|
94
106
|
minArgs: 1,
|
|
95
107
|
maxArgs: 1,
|
|
96
|
-
options: ["json"],
|
|
108
|
+
options: ["json", "name"],
|
|
97
109
|
},
|
|
98
110
|
list: {
|
|
99
111
|
usage: "agency workbase list [--json]",
|
|
@@ -101,6 +113,25 @@ const commands = {
|
|
|
101
113
|
maxArgs: 0,
|
|
102
114
|
options: ["json"],
|
|
103
115
|
},
|
|
116
|
+
remove: {
|
|
117
|
+
usage: "agency workbase remove <selector> [--json]",
|
|
118
|
+
minArgs: 1,
|
|
119
|
+
maxArgs: 1,
|
|
120
|
+
options: ["json"],
|
|
121
|
+
},
|
|
122
|
+
prune: {
|
|
123
|
+
usage: "agency workbase prune [--json]",
|
|
124
|
+
minArgs: 0,
|
|
125
|
+
maxArgs: 0,
|
|
126
|
+
options: ["json"],
|
|
127
|
+
},
|
|
128
|
+
default: {
|
|
129
|
+
usage: "agency workbase default [selector | --clear] [--json]",
|
|
130
|
+
minArgs: 0,
|
|
131
|
+
maxArgs: 1,
|
|
132
|
+
options: ["json", "clear"],
|
|
133
|
+
conflicts: [["clear", "$positional"]],
|
|
134
|
+
},
|
|
104
135
|
},
|
|
105
136
|
},
|
|
106
137
|
integration: {
|
|
@@ -147,7 +178,7 @@ const commands = {
|
|
|
147
178
|
},
|
|
148
179
|
epic: {
|
|
149
180
|
usage: "agency epic <create|list|show>",
|
|
150
|
-
options: createOptions,
|
|
181
|
+
options: { ...createOptions, epic: { type: "string" } },
|
|
151
182
|
subcommands: {
|
|
152
183
|
create: {
|
|
153
184
|
usage:
|
|
@@ -168,13 +199,13 @@ const commands = {
|
|
|
168
199
|
usage: "agency epic show <id> [--json]",
|
|
169
200
|
minArgs: 1,
|
|
170
201
|
maxArgs: 1,
|
|
171
|
-
options: ["json"],
|
|
202
|
+
options: ["json", "epic"],
|
|
172
203
|
},
|
|
173
204
|
},
|
|
174
205
|
},
|
|
175
206
|
task: {
|
|
176
207
|
usage: "agency task <new|create|list|show|status>",
|
|
177
|
-
options: taskCreateOptions,
|
|
208
|
+
options: { ...taskCreateOptions, task: { type: "string" } },
|
|
178
209
|
subcommands: {
|
|
179
210
|
new: {
|
|
180
211
|
usage: "agency task new [id] [options]",
|
|
@@ -221,19 +252,23 @@ const commands = {
|
|
|
221
252
|
usage: "agency task show <id> [--json]",
|
|
222
253
|
minArgs: 1,
|
|
223
254
|
maxArgs: 1,
|
|
224
|
-
options: ["json"],
|
|
255
|
+
options: ["json", "task"],
|
|
225
256
|
},
|
|
226
257
|
status: {
|
|
227
258
|
usage: "agency task status <id> <status> [--json]",
|
|
228
259
|
minArgs: 2,
|
|
229
260
|
maxArgs: 2,
|
|
230
|
-
options: ["json"],
|
|
261
|
+
options: ["json", "task"],
|
|
231
262
|
},
|
|
232
263
|
},
|
|
233
264
|
},
|
|
234
265
|
phase: {
|
|
235
266
|
usage: "agency phase <create|list|show|status>",
|
|
236
|
-
options:
|
|
267
|
+
options: {
|
|
268
|
+
...phaseCreateOptions,
|
|
269
|
+
task: { type: "string" },
|
|
270
|
+
phase: { type: "string" },
|
|
271
|
+
},
|
|
237
272
|
subcommands: {
|
|
238
273
|
create: {
|
|
239
274
|
usage:
|
|
@@ -249,6 +284,8 @@ const commands = {
|
|
|
249
284
|
"depends-on",
|
|
250
285
|
"first-phase",
|
|
251
286
|
"json",
|
|
287
|
+
"task",
|
|
288
|
+
"phase",
|
|
252
289
|
],
|
|
253
290
|
required: ["repo", "branch", "base"],
|
|
254
291
|
repeatable: ["reference", "depends-on"],
|
|
@@ -257,25 +294,29 @@ const commands = {
|
|
|
257
294
|
usage: "agency phase list <task-id> [--json]",
|
|
258
295
|
minArgs: 1,
|
|
259
296
|
maxArgs: 1,
|
|
260
|
-
options: ["json"],
|
|
297
|
+
options: ["json", "task"],
|
|
261
298
|
},
|
|
262
299
|
show: {
|
|
263
300
|
usage: "agency phase show <task-id> <phase-id> [--json]",
|
|
264
301
|
minArgs: 2,
|
|
265
302
|
maxArgs: 2,
|
|
266
|
-
options: ["json"],
|
|
303
|
+
options: ["json", "task", "phase"],
|
|
267
304
|
},
|
|
268
305
|
status: {
|
|
269
306
|
usage: "agency phase status <task-id> <phase-id> <status> [--json]",
|
|
270
307
|
minArgs: 3,
|
|
271
308
|
maxArgs: 3,
|
|
272
|
-
options: ["json"],
|
|
309
|
+
options: ["json", "task", "phase"],
|
|
273
310
|
},
|
|
274
311
|
},
|
|
275
312
|
},
|
|
276
313
|
claim: {
|
|
277
314
|
usage: "agency claim <task-id> [phase-id] [options]",
|
|
278
|
-
options:
|
|
315
|
+
options: {
|
|
316
|
+
...claimOptions,
|
|
317
|
+
task: { type: "string" },
|
|
318
|
+
phase: { type: "string" },
|
|
319
|
+
},
|
|
279
320
|
command: {
|
|
280
321
|
usage:
|
|
281
322
|
"agency claim <task-id> [phase-id] --claimant <id> --runner <id> --session-id <id> --revision <sha256> [--expires-at <timestamp>] [--json]",
|
|
@@ -288,19 +329,25 @@ const commands = {
|
|
|
288
329
|
"revision",
|
|
289
330
|
"expires-at",
|
|
290
331
|
"json",
|
|
332
|
+
"task",
|
|
333
|
+
"phase",
|
|
291
334
|
],
|
|
292
335
|
required: ["claimant", "runner", "session-id", "revision"],
|
|
293
336
|
},
|
|
294
337
|
},
|
|
295
338
|
release: {
|
|
296
339
|
usage: "agency release <task-id> [phase-id] [options]",
|
|
297
|
-
options:
|
|
340
|
+
options: {
|
|
341
|
+
...ownedClaimOptions,
|
|
342
|
+
task: { type: "string" },
|
|
343
|
+
phase: { type: "string" },
|
|
344
|
+
},
|
|
298
345
|
command: {
|
|
299
346
|
usage:
|
|
300
347
|
"agency release <task-id> [phase-id] --session-id <id> --revision <sha256> [--json]",
|
|
301
348
|
minArgs: 1,
|
|
302
349
|
maxArgs: 2,
|
|
303
|
-
options: ["session-id", "revision", "json"],
|
|
350
|
+
options: ["session-id", "revision", "json", "task", "phase"],
|
|
304
351
|
required: ["session-id", "revision"],
|
|
305
352
|
},
|
|
306
353
|
},
|
|
@@ -309,13 +356,15 @@ const commands = {
|
|
|
309
356
|
options: {
|
|
310
357
|
...ownedClaimOptions,
|
|
311
358
|
outcome: { type: "string" },
|
|
359
|
+
task: { type: "string" },
|
|
360
|
+
phase: { type: "string" },
|
|
312
361
|
},
|
|
313
362
|
command: {
|
|
314
363
|
usage:
|
|
315
364
|
"agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped> [--json]",
|
|
316
365
|
minArgs: 1,
|
|
317
366
|
maxArgs: 2,
|
|
318
|
-
options: ["session-id", "revision", "outcome", "json"],
|
|
367
|
+
options: ["session-id", "revision", "outcome", "json", "task", "phase"],
|
|
319
368
|
required: ["session-id", "revision", "outcome"],
|
|
320
369
|
},
|
|
321
370
|
},
|
|
@@ -336,25 +385,25 @@ const commands = {
|
|
|
336
385
|
},
|
|
337
386
|
archive: {
|
|
338
387
|
usage: "agency archive <epic|task|phase>",
|
|
339
|
-
options: outputOptions,
|
|
388
|
+
options: { ...outputOptions, ...entitySelectorOptions },
|
|
340
389
|
subcommands: {
|
|
341
390
|
epic: {
|
|
342
391
|
usage: "agency archive epic <epic-id> [--json]",
|
|
343
392
|
minArgs: 1,
|
|
344
393
|
maxArgs: 1,
|
|
345
|
-
options: ["json"],
|
|
394
|
+
options: ["json", "epic"],
|
|
346
395
|
},
|
|
347
396
|
task: {
|
|
348
397
|
usage: "agency archive task <task-id> [--json]",
|
|
349
398
|
minArgs: 1,
|
|
350
399
|
maxArgs: 1,
|
|
351
|
-
options: ["json"],
|
|
400
|
+
options: ["json", "task"],
|
|
352
401
|
},
|
|
353
402
|
phase: {
|
|
354
403
|
usage: "agency archive phase <task-id> <phase-id> [--json]",
|
|
355
404
|
minArgs: 2,
|
|
356
405
|
maxArgs: 2,
|
|
357
|
-
options: ["json"],
|
|
406
|
+
options: ["json", "task", "phase"],
|
|
358
407
|
},
|
|
359
408
|
},
|
|
360
409
|
},
|
|
@@ -363,18 +412,28 @@ const commands = {
|
|
|
363
412
|
"agency work [<directory-or-task-id> | --epic <epic-id>] | agency work prepare [target] [--dry-run] [--json]",
|
|
364
413
|
options: {
|
|
365
414
|
...commonOptions,
|
|
415
|
+
...entitySelectorOptions,
|
|
366
416
|
json: { type: "boolean" },
|
|
367
417
|
"dry-run": { type: "boolean" },
|
|
368
|
-
epic: { type: "string" },
|
|
369
418
|
opencode: { type: "boolean" },
|
|
370
419
|
claude: { type: "boolean" },
|
|
420
|
+
force: { type: "boolean" },
|
|
371
421
|
},
|
|
372
422
|
command: {
|
|
373
423
|
usage:
|
|
374
424
|
"agency work [<directory-or-task-id> | --epic <epic-id>] | agency work prepare [target] [--dry-run] [--json]",
|
|
375
425
|
minArgs: 0,
|
|
376
426
|
maxArgs: 2,
|
|
377
|
-
options: [
|
|
427
|
+
options: [
|
|
428
|
+
"json",
|
|
429
|
+
"dry-run",
|
|
430
|
+
"epic",
|
|
431
|
+
"task",
|
|
432
|
+
"phase",
|
|
433
|
+
"opencode",
|
|
434
|
+
"claude",
|
|
435
|
+
"force",
|
|
436
|
+
],
|
|
378
437
|
conflicts: [
|
|
379
438
|
["opencode", "claude"],
|
|
380
439
|
["epic", "$positional"],
|
|
@@ -386,16 +445,33 @@ const commands = {
|
|
|
386
445
|
options: {
|
|
387
446
|
...outputOptions,
|
|
388
447
|
draft: { type: "boolean" },
|
|
448
|
+
force: { type: "boolean" },
|
|
449
|
+
task: { type: "string" },
|
|
450
|
+
phase: { type: "string" },
|
|
389
451
|
},
|
|
390
452
|
subcommands: {
|
|
391
453
|
create: {
|
|
392
|
-
usage:
|
|
454
|
+
usage:
|
|
455
|
+
"agency pr create <task-id> [phase-id] [--draft] [--force] [--json]",
|
|
393
456
|
minArgs: 1,
|
|
394
457
|
maxArgs: 2,
|
|
395
|
-
options: ["draft", "json"],
|
|
458
|
+
options: ["draft", "force", "json", "task", "phase"],
|
|
396
459
|
},
|
|
397
460
|
},
|
|
398
461
|
},
|
|
462
|
+
next: {
|
|
463
|
+
usage: "agency next [--select] [--json]",
|
|
464
|
+
options: {
|
|
465
|
+
...outputOptions,
|
|
466
|
+
select: { type: "boolean" },
|
|
467
|
+
},
|
|
468
|
+
command: {
|
|
469
|
+
usage: "agency next [--select] [--json]",
|
|
470
|
+
minArgs: 0,
|
|
471
|
+
maxArgs: 0,
|
|
472
|
+
options: ["select", "json"],
|
|
473
|
+
},
|
|
474
|
+
},
|
|
399
475
|
status: {
|
|
400
476
|
usage: "agency status [--json]",
|
|
401
477
|
options: outputOptions,
|
|
@@ -422,13 +498,14 @@ const commands = {
|
|
|
422
498
|
usage: "agency context [target] [--json] [--compact]",
|
|
423
499
|
options: {
|
|
424
500
|
...outputOptions,
|
|
501
|
+
...entitySelectorOptions,
|
|
425
502
|
compact: { type: "boolean" },
|
|
426
503
|
},
|
|
427
504
|
command: {
|
|
428
505
|
usage: "agency context [target] [--json] [--compact]",
|
|
429
506
|
minArgs: 0,
|
|
430
507
|
maxArgs: 1,
|
|
431
|
-
options: ["json", "compact"],
|
|
508
|
+
options: ["json", "compact", "epic", "task", "phase"],
|
|
432
509
|
},
|
|
433
510
|
},
|
|
434
511
|
graph: {
|
|
@@ -479,6 +556,7 @@ const preCommandOptions = new Set([
|
|
|
479
556
|
"-v",
|
|
480
557
|
"--no-input",
|
|
481
558
|
])
|
|
559
|
+
const preCommandValueOptions = new Set(["--workbase", "--cwd"])
|
|
482
560
|
|
|
483
561
|
export interface ParsedCli {
|
|
484
562
|
readonly commandName?: keyof typeof commands
|
|
@@ -507,8 +585,22 @@ const usageError = (message: string, usage: string) =>
|
|
|
507
585
|
const optionLabel = (name: string) => `--${name}`
|
|
508
586
|
|
|
509
587
|
function findCommandIndex(args: readonly string[]) {
|
|
510
|
-
for (
|
|
588
|
+
for (let index = 0; index < args.length; index++) {
|
|
589
|
+
const argument = args[index]!
|
|
511
590
|
if (!argument.startsWith("-")) return index
|
|
591
|
+
if (argument.startsWith("--workbase=") || argument.startsWith("--cwd=")) {
|
|
592
|
+
continue
|
|
593
|
+
}
|
|
594
|
+
if (preCommandValueOptions.has(argument)) {
|
|
595
|
+
if (args[index + 1] === undefined) {
|
|
596
|
+
throw usageError(
|
|
597
|
+
`Option '${argument}' expects a value.`,
|
|
598
|
+
"agency <command> [options]",
|
|
599
|
+
)
|
|
600
|
+
}
|
|
601
|
+
index++
|
|
602
|
+
continue
|
|
603
|
+
}
|
|
512
604
|
if (!preCommandOptions.has(argument) && !/^-[hVsv]+$/.test(argument)) {
|
|
513
605
|
throw usageError(
|
|
514
606
|
`Unknown option '${argument}'.`,
|
|
@@ -519,6 +611,87 @@ function findCommandIndex(args: readonly string[]) {
|
|
|
519
611
|
return -1
|
|
520
612
|
}
|
|
521
613
|
|
|
614
|
+
const targetSlots = (
|
|
615
|
+
commandName: string,
|
|
616
|
+
subcommand: string | undefined,
|
|
617
|
+
): readonly ("epic" | "task" | "phase")[] => {
|
|
618
|
+
if (commandName === "epic" && subcommand === "show") return ["epic"]
|
|
619
|
+
if (commandName === "task" && ["show", "status"].includes(subcommand ?? ""))
|
|
620
|
+
return ["task"]
|
|
621
|
+
if (commandName === "phase")
|
|
622
|
+
return subcommand === "list" ? ["task"] : ["task", "phase"]
|
|
623
|
+
if (["claim", "release", "finish"].includes(commandName))
|
|
624
|
+
return ["task", "phase"]
|
|
625
|
+
if (commandName === "archive")
|
|
626
|
+
return subcommand === "epic"
|
|
627
|
+
? ["epic"]
|
|
628
|
+
: subcommand === "task"
|
|
629
|
+
? ["task"]
|
|
630
|
+
: ["task", "phase"]
|
|
631
|
+
if (commandName === "pr" && subcommand === "create") return ["task", "phase"]
|
|
632
|
+
return []
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function applyEntitySelectors(
|
|
636
|
+
commandName: string,
|
|
637
|
+
subcommand: string | undefined,
|
|
638
|
+
positionals: readonly string[],
|
|
639
|
+
values: ParsedCli["values"],
|
|
640
|
+
spec: LeafCommand,
|
|
641
|
+
) {
|
|
642
|
+
const supplied = ["epic", "task", "phase"].filter(
|
|
643
|
+
(name) => values[name] !== undefined,
|
|
644
|
+
)
|
|
645
|
+
if (supplied.length === 0) return [...positionals]
|
|
646
|
+
if (values.phase !== undefined && values.task === undefined) {
|
|
647
|
+
throw usageError("Option '--phase' requires '--task'.", spec.usage)
|
|
648
|
+
}
|
|
649
|
+
if (values.epic !== undefined && (values.task || values.phase)) {
|
|
650
|
+
throw usageError(
|
|
651
|
+
"Option '--epic' cannot be combined with '--task' or '--phase'.",
|
|
652
|
+
spec.usage,
|
|
653
|
+
)
|
|
654
|
+
}
|
|
655
|
+
if (commandName === "work" || commandName === "context") {
|
|
656
|
+
if (
|
|
657
|
+
positionals.length >
|
|
658
|
+
(commandName === "work" && positionals[0] === "prepare" ? 1 : 0)
|
|
659
|
+
) {
|
|
660
|
+
throw usageError(
|
|
661
|
+
"Entity selector options cannot be combined with a positional target.",
|
|
662
|
+
spec.usage,
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
return [...positionals]
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const slots = targetSlots(commandName, subcommand)
|
|
669
|
+
const selectedSlots = slots.filter((slot) => values[slot] !== undefined)
|
|
670
|
+
if (selectedSlots.length === 0) return [...positionals]
|
|
671
|
+
const trailingCount = spec.maxArgs - slots.length
|
|
672
|
+
if (positionals.length > trailingCount) {
|
|
673
|
+
throw usageError(
|
|
674
|
+
"Entity selector options cannot be combined with positional target IDs.",
|
|
675
|
+
spec.usage,
|
|
676
|
+
)
|
|
677
|
+
}
|
|
678
|
+
const requiredSlots = slots.slice(0, spec.minArgs - trailingCount)
|
|
679
|
+
for (const slot of requiredSlots) {
|
|
680
|
+
if (values[slot] === undefined) {
|
|
681
|
+
throw usageError(
|
|
682
|
+
`Option '--${slot}' is required with explicit selectors.`,
|
|
683
|
+
spec.usage,
|
|
684
|
+
)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return [
|
|
688
|
+
...slots.flatMap((slot) =>
|
|
689
|
+
typeof values[slot] === "string" ? [values[slot] as string] : [],
|
|
690
|
+
),
|
|
691
|
+
...positionals,
|
|
692
|
+
]
|
|
693
|
+
}
|
|
694
|
+
|
|
522
695
|
function assertNoDuplicateOptions(
|
|
523
696
|
tokens: readonly { readonly kind: string; readonly name?: string }[],
|
|
524
697
|
repeatable: ReadonlySet<string>,
|
|
@@ -658,7 +831,7 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
658
831
|
throw usageError(message, definition.usage)
|
|
659
832
|
}
|
|
660
833
|
|
|
661
|
-
|
|
834
|
+
let commandPositionals = definition.subcommands
|
|
662
835
|
? parsed.positionals.slice(1)
|
|
663
836
|
: parsed.positionals
|
|
664
837
|
const allowed = new Set([...commonOptionNames, ...(spec.options ?? [])])
|
|
@@ -678,6 +851,20 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
678
851
|
spec.usage,
|
|
679
852
|
)
|
|
680
853
|
}
|
|
854
|
+
for (const selector of ["workbase", "cwd", "epic", "task", "phase"]) {
|
|
855
|
+
if (parsed.values[selector] === "") {
|
|
856
|
+
throw usageError(
|
|
857
|
+
`Option '--${selector}' requires a non-empty value.`,
|
|
858
|
+
spec.usage,
|
|
859
|
+
)
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (parsed.values.workbase && parsed.values.cwd) {
|
|
863
|
+
throw usageError(
|
|
864
|
+
"Options '--workbase' and '--cwd' cannot be combined.",
|
|
865
|
+
spec.usage,
|
|
866
|
+
)
|
|
867
|
+
}
|
|
681
868
|
if (parsed.values.version) {
|
|
682
869
|
return {
|
|
683
870
|
commandName: commandName as keyof typeof commands,
|
|
@@ -693,6 +880,14 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
693
880
|
}
|
|
694
881
|
}
|
|
695
882
|
|
|
883
|
+
commandPositionals = applyEntitySelectors(
|
|
884
|
+
commandName,
|
|
885
|
+
subcommand,
|
|
886
|
+
commandPositionals,
|
|
887
|
+
parsed.values,
|
|
888
|
+
spec,
|
|
889
|
+
)
|
|
890
|
+
|
|
696
891
|
if (
|
|
697
892
|
commandPositionals.length < spec.minArgs ||
|
|
698
893
|
commandPositionals.length > spec.maxArgs
|
|
@@ -744,7 +939,10 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
744
939
|
if (
|
|
745
940
|
(!preparing && commandPositionals.length > 1) ||
|
|
746
941
|
(preparing &&
|
|
747
|
-
(parsed.values.epic ||
|
|
942
|
+
(parsed.values.epic ||
|
|
943
|
+
parsed.values.opencode ||
|
|
944
|
+
parsed.values.claude ||
|
|
945
|
+
parsed.values.force))
|
|
748
946
|
) {
|
|
749
947
|
throw usageError(
|
|
750
948
|
preparing
|
|
@@ -763,7 +961,9 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
763
961
|
|
|
764
962
|
return {
|
|
765
963
|
commandName: commandName as keyof typeof commands,
|
|
766
|
-
args:
|
|
964
|
+
args: definition.subcommands
|
|
965
|
+
? [subcommand!, ...commandPositionals]
|
|
966
|
+
: commandPositionals,
|
|
767
967
|
values: parsed.values,
|
|
768
968
|
}
|
|
769
969
|
}
|
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"],
|
|
@@ -245,6 +254,7 @@ describe("CLI", () => {
|
|
|
245
254
|
["validate", "Usage: agency validate"],
|
|
246
255
|
["context", "Usage: agency context"],
|
|
247
256
|
["graph", "Usage: agency graph"],
|
|
257
|
+
["next", "Usage: agency next"],
|
|
248
258
|
] as const) {
|
|
249
259
|
const result = await runCli([command, "--help"])
|
|
250
260
|
expect(result.exitCode).toBe(0)
|
|
@@ -264,6 +274,55 @@ describe("CLI", () => {
|
|
|
264
274
|
|
|
265
275
|
const after = await runCli(["status", "--silent"], root)
|
|
266
276
|
expect(after).toEqual({ exitCode: 0, stdout: "", stderr: "" })
|
|
277
|
+
}, 10_000)
|
|
278
|
+
|
|
279
|
+
test("lists ready work and exposes excluded blockers through one result", async () => {
|
|
280
|
+
const root = await createTempDir()
|
|
281
|
+
tempDirs.push(root)
|
|
282
|
+
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
283
|
+
await mkdir(join(root, "repos/agency"), { recursive: true })
|
|
284
|
+
for (const id of ["ready", "finished"]) {
|
|
285
|
+
parseJson(
|
|
286
|
+
await runCli(
|
|
287
|
+
["task", "create", id, "--repo", "agency", "--json"],
|
|
288
|
+
root,
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
parseJson(
|
|
293
|
+
await runCli(["task", "status", "finished", "done", "--json"], root),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
const human = await runCli(["next"], root)
|
|
297
|
+
expect(human).toMatchObject({ exitCode: 0, stderr: "" })
|
|
298
|
+
expect(human.stdout).toContain("1. task/ready")
|
|
299
|
+
expect(human.stdout).not.toContain("task/finished")
|
|
300
|
+
|
|
301
|
+
const result = parseJson(await runCli(["next", "--select", "--json"], root))
|
|
302
|
+
expect(result.selected).toMatchObject({ key: "task/ready", rank: 1 })
|
|
303
|
+
expect(result.ready.map((item: any) => item.key)).toEqual(["task/ready"])
|
|
304
|
+
expect(result.excluded).toMatchObject([
|
|
305
|
+
{
|
|
306
|
+
key: "task/finished",
|
|
307
|
+
status: "done",
|
|
308
|
+
terminal: true,
|
|
309
|
+
blockers: [{ kind: "status", reason: "Task status is done" }],
|
|
310
|
+
},
|
|
311
|
+
])
|
|
312
|
+
|
|
313
|
+
const blockedPr = await runCli(["pr", "create", "finished", "--json"], root)
|
|
314
|
+
expect(blockedPr.exitCode).toBe(1)
|
|
315
|
+
expect(blockedPr.stderr).toBe("")
|
|
316
|
+
expect(JSON.parse(blockedPr.stdout)).toMatchObject({
|
|
317
|
+
ok: false,
|
|
318
|
+
error: {
|
|
319
|
+
code: "EXECUTION_BLOCKED",
|
|
320
|
+
fields: {
|
|
321
|
+
status: "done",
|
|
322
|
+
blockers: [{ kind: "status", reason: "Task status is done" }],
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
})
|
|
267
326
|
})
|
|
268
327
|
|
|
269
328
|
test("reports and synchronizes managed integration files", async () => {
|
|
@@ -299,12 +358,93 @@ describe("CLI", () => {
|
|
|
299
358
|
).toEqual({
|
|
300
359
|
root,
|
|
301
360
|
})
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
})
|
|
305
381
|
expect(
|
|
306
382
|
parseJson(await runCli(["workbase", "list", "--json"], parent, env)),
|
|
307
|
-
).toEqual([
|
|
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
|
+
)
|
|
308
448
|
})
|
|
309
449
|
|
|
310
450
|
test("exports equivalent JSON and JSONL graph contracts", async () => {
|
package/src/commands/context.ts
CHANGED
|
@@ -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
|
`
|
package/src/commands/init.ts
CHANGED
|
@@ -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
|
|
17
|
+
options.path ? resolve(cwd, options.path) : cwd,
|
|
16
18
|
)
|
|
17
19
|
log(
|
|
18
20
|
options.json
|