@markjaquith/agency 2.17.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 +18 -4
- package/cli.ts +74 -8
- package/package.json +1 -1
- package/skills/agency/SKILL.md +13 -6
- package/src/cli-parser.test.ts +74 -0
- package/src/cli-parser.ts +208 -27
- package/src/cli.test.ts +94 -4
- package/src/commands/context.ts +3 -0
- package/src/commands/init.ts +3 -1
- 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/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,9 +412,9 @@ 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" },
|
|
371
420
|
force: { type: "boolean" },
|
|
@@ -375,7 +424,16 @@ const commands = {
|
|
|
375
424
|
"agency work [<directory-or-task-id> | --epic <epic-id>] | agency work prepare [target] [--dry-run] [--json]",
|
|
376
425
|
minArgs: 0,
|
|
377
426
|
maxArgs: 2,
|
|
378
|
-
options: [
|
|
427
|
+
options: [
|
|
428
|
+
"json",
|
|
429
|
+
"dry-run",
|
|
430
|
+
"epic",
|
|
431
|
+
"task",
|
|
432
|
+
"phase",
|
|
433
|
+
"opencode",
|
|
434
|
+
"claude",
|
|
435
|
+
"force",
|
|
436
|
+
],
|
|
379
437
|
conflicts: [
|
|
380
438
|
["opencode", "claude"],
|
|
381
439
|
["epic", "$positional"],
|
|
@@ -388,6 +446,8 @@ const commands = {
|
|
|
388
446
|
...outputOptions,
|
|
389
447
|
draft: { type: "boolean" },
|
|
390
448
|
force: { type: "boolean" },
|
|
449
|
+
task: { type: "string" },
|
|
450
|
+
phase: { type: "string" },
|
|
391
451
|
},
|
|
392
452
|
subcommands: {
|
|
393
453
|
create: {
|
|
@@ -395,7 +455,7 @@ const commands = {
|
|
|
395
455
|
"agency pr create <task-id> [phase-id] [--draft] [--force] [--json]",
|
|
396
456
|
minArgs: 1,
|
|
397
457
|
maxArgs: 2,
|
|
398
|
-
options: ["draft", "force", "json"],
|
|
458
|
+
options: ["draft", "force", "json", "task", "phase"],
|
|
399
459
|
},
|
|
400
460
|
},
|
|
401
461
|
},
|
|
@@ -438,13 +498,14 @@ const commands = {
|
|
|
438
498
|
usage: "agency context [target] [--json] [--compact]",
|
|
439
499
|
options: {
|
|
440
500
|
...outputOptions,
|
|
501
|
+
...entitySelectorOptions,
|
|
441
502
|
compact: { type: "boolean" },
|
|
442
503
|
},
|
|
443
504
|
command: {
|
|
444
505
|
usage: "agency context [target] [--json] [--compact]",
|
|
445
506
|
minArgs: 0,
|
|
446
507
|
maxArgs: 1,
|
|
447
|
-
options: ["json", "compact"],
|
|
508
|
+
options: ["json", "compact", "epic", "task", "phase"],
|
|
448
509
|
},
|
|
449
510
|
},
|
|
450
511
|
graph: {
|
|
@@ -495,6 +556,7 @@ const preCommandOptions = new Set([
|
|
|
495
556
|
"-v",
|
|
496
557
|
"--no-input",
|
|
497
558
|
])
|
|
559
|
+
const preCommandValueOptions = new Set(["--workbase", "--cwd"])
|
|
498
560
|
|
|
499
561
|
export interface ParsedCli {
|
|
500
562
|
readonly commandName?: keyof typeof commands
|
|
@@ -523,8 +585,22 @@ const usageError = (message: string, usage: string) =>
|
|
|
523
585
|
const optionLabel = (name: string) => `--${name}`
|
|
524
586
|
|
|
525
587
|
function findCommandIndex(args: readonly string[]) {
|
|
526
|
-
for (
|
|
588
|
+
for (let index = 0; index < args.length; index++) {
|
|
589
|
+
const argument = args[index]!
|
|
527
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
|
+
}
|
|
528
604
|
if (!preCommandOptions.has(argument) && !/^-[hVsv]+$/.test(argument)) {
|
|
529
605
|
throw usageError(
|
|
530
606
|
`Unknown option '${argument}'.`,
|
|
@@ -535,6 +611,87 @@ function findCommandIndex(args: readonly string[]) {
|
|
|
535
611
|
return -1
|
|
536
612
|
}
|
|
537
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
|
+
|
|
538
695
|
function assertNoDuplicateOptions(
|
|
539
696
|
tokens: readonly { readonly kind: string; readonly name?: string }[],
|
|
540
697
|
repeatable: ReadonlySet<string>,
|
|
@@ -674,7 +831,7 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
674
831
|
throw usageError(message, definition.usage)
|
|
675
832
|
}
|
|
676
833
|
|
|
677
|
-
|
|
834
|
+
let commandPositionals = definition.subcommands
|
|
678
835
|
? parsed.positionals.slice(1)
|
|
679
836
|
: parsed.positionals
|
|
680
837
|
const allowed = new Set([...commonOptionNames, ...(spec.options ?? [])])
|
|
@@ -694,6 +851,20 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
694
851
|
spec.usage,
|
|
695
852
|
)
|
|
696
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
|
+
}
|
|
697
868
|
if (parsed.values.version) {
|
|
698
869
|
return {
|
|
699
870
|
commandName: commandName as keyof typeof commands,
|
|
@@ -709,6 +880,14 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
709
880
|
}
|
|
710
881
|
}
|
|
711
882
|
|
|
883
|
+
commandPositionals = applyEntitySelectors(
|
|
884
|
+
commandName,
|
|
885
|
+
subcommand,
|
|
886
|
+
commandPositionals,
|
|
887
|
+
parsed.values,
|
|
888
|
+
spec,
|
|
889
|
+
)
|
|
890
|
+
|
|
712
891
|
if (
|
|
713
892
|
commandPositionals.length < spec.minArgs ||
|
|
714
893
|
commandPositionals.length > spec.maxArgs
|
|
@@ -782,7 +961,9 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
782
961
|
|
|
783
962
|
return {
|
|
784
963
|
commandName: commandName as keyof typeof commands,
|
|
785
|
-
args:
|
|
964
|
+
args: definition.subcommands
|
|
965
|
+
? [subcommand!, ...commandPositionals]
|
|
966
|
+
: commandPositionals,
|
|
786
967
|
values: parsed.values,
|
|
787
968
|
}
|
|
788
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"],
|
|
@@ -349,12 +358,93 @@ describe("CLI", () => {
|
|
|
349
358
|
).toEqual({
|
|
350
359
|
root,
|
|
351
360
|
})
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
361
|
+
const registration = parseJson(
|
|
362
|
+
await runCli(
|
|
363
|
+
[
|
|
364
|
+
"workbase",
|
|
365
|
+
"add",
|
|
366
|
+
"workbase",
|
|
367
|
+
"--cwd",
|
|
368
|
+
parent,
|
|
369
|
+
"--name",
|
|
370
|
+
"primary",
|
|
371
|
+
"--json",
|
|
372
|
+
],
|
|
373
|
+
projectRoot,
|
|
374
|
+
env,
|
|
375
|
+
),
|
|
376
|
+
)
|
|
377
|
+
expect(registration).toMatchObject({
|
|
378
|
+
name: "primary",
|
|
379
|
+
path: await realpath(root),
|
|
380
|
+
})
|
|
355
381
|
expect(
|
|
356
382
|
parseJson(await runCli(["workbase", "list", "--json"], parent, env)),
|
|
357
|
-
).toEqual([
|
|
383
|
+
).toEqual({ workbases: [registration] })
|
|
384
|
+
|
|
385
|
+
await mkdir(join(root, "repos", "agency"), { recursive: true })
|
|
386
|
+
parseJson(
|
|
387
|
+
await runCli(
|
|
388
|
+
[
|
|
389
|
+
"task",
|
|
390
|
+
"create",
|
|
391
|
+
"explicit",
|
|
392
|
+
"--repo",
|
|
393
|
+
"agency",
|
|
394
|
+
"--workbase",
|
|
395
|
+
"primary",
|
|
396
|
+
"--no-input",
|
|
397
|
+
"--json",
|
|
398
|
+
],
|
|
399
|
+
parent,
|
|
400
|
+
env,
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
const shown = parseJson(
|
|
404
|
+
await runCli(
|
|
405
|
+
[
|
|
406
|
+
"task",
|
|
407
|
+
"show",
|
|
408
|
+
"--task",
|
|
409
|
+
"explicit",
|
|
410
|
+
"--workbase",
|
|
411
|
+
registration.id,
|
|
412
|
+
"--no-input",
|
|
413
|
+
"--json",
|
|
414
|
+
],
|
|
415
|
+
parent,
|
|
416
|
+
env,
|
|
417
|
+
),
|
|
418
|
+
)
|
|
419
|
+
expect(shown.id).toBe("explicit")
|
|
420
|
+
const inferred = parseJson(
|
|
421
|
+
await runCli(
|
|
422
|
+
["context", "--cwd", join(root, "tasks", "explicit"), "--json"],
|
|
423
|
+
projectRoot,
|
|
424
|
+
env,
|
|
425
|
+
),
|
|
426
|
+
)
|
|
427
|
+
expect(inferred.target).toMatchObject({
|
|
428
|
+
kind: "task",
|
|
429
|
+
taskId: "explicit",
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
parseJson(
|
|
433
|
+
await runCli(["workbase", "default", "primary", "--json"], parent, env),
|
|
434
|
+
)
|
|
435
|
+
expect(
|
|
436
|
+
parseJson(await runCli(["task", "list", "--json"], parent, env)),
|
|
437
|
+
).toHaveLength(1)
|
|
438
|
+
const explicitOutside = await runCli(
|
|
439
|
+
["task", "list", "--cwd", parent, "--json"],
|
|
440
|
+
projectRoot,
|
|
441
|
+
env,
|
|
442
|
+
)
|
|
443
|
+
expect(explicitOutside.exitCode).toBe(1)
|
|
444
|
+
expect(explicitOutside.stderr).toBe("")
|
|
445
|
+
expect(JSON.parse(explicitOutside.stdout).error.message).toContain(
|
|
446
|
+
"No Agency workbase found from",
|
|
447
|
+
)
|
|
358
448
|
})
|
|
359
449
|
|
|
360
450
|
test("exports equivalent JSON and JSONL graph contracts", async () => {
|
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
|
|
@@ -80,6 +80,7 @@ pr: null
|
|
|
80
80
|
: Effect.succeed(path)
|
|
81
81
|
},
|
|
82
82
|
listRegistered: () => Effect.succeed(["/first", "/selected"]),
|
|
83
|
+
getDefault: () => Effect.succeed(undefined),
|
|
83
84
|
validate: (path: string) =>
|
|
84
85
|
Effect.succeed({
|
|
85
86
|
root: path,
|
|
@@ -116,6 +117,7 @@ pr: null
|
|
|
116
117
|
message: "No Agency workbase found from /outside",
|
|
117
118
|
}),
|
|
118
119
|
listRegistered: () => Effect.succeed(["/selected"]),
|
|
120
|
+
getDefault: () => Effect.succeed(undefined),
|
|
119
121
|
}
|
|
120
122
|
const fs = {
|
|
121
123
|
runCommand: () => Effect.fail(new Error("unexpected fzf probe")),
|
|
@@ -98,6 +98,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
98
98
|
})
|
|
99
99
|
: Effect.succeed("/workbase"),
|
|
100
100
|
listRegistered: () => Effect.succeed(options.registeredWorkbases ?? []),
|
|
101
|
+
getDefault: () => Effect.succeed(undefined),
|
|
101
102
|
loadConfig: () =>
|
|
102
103
|
Effect.succeed({
|
|
103
104
|
root: "/workbase",
|
package/src/commands/work.ts
CHANGED
|
@@ -297,9 +297,13 @@ export const workPrepare = (options: WorkOptions = {}) =>
|
|
|
297
297
|
const isDirectory = yield* fs.isDirectory(targetPath)
|
|
298
298
|
const root = yield* workbase.discover(isDirectory ? targetPath : cwd)
|
|
299
299
|
|
|
300
|
-
let taskId
|
|
301
|
-
let phaseId
|
|
302
|
-
if (
|
|
300
|
+
let taskId = options.taskId
|
|
301
|
+
let phaseId = options.phaseId
|
|
302
|
+
if (taskId) {
|
|
303
|
+
const task = yield* tasks.show(taskId, root)
|
|
304
|
+
taskId = task.id
|
|
305
|
+
if (phaseId) phaseId = (yield* phases.show(task.id, phaseId, root)).id
|
|
306
|
+
} else if (options.directory && !isDirectory) {
|
|
303
307
|
const task = yield* tasks.show(options.directory, root)
|
|
304
308
|
taskId = task.id
|
|
305
309
|
} else {
|
|
@@ -353,12 +357,16 @@ launching an agent or changing lifecycle status. --dry-run reports planned Git
|
|
|
353
357
|
changes without fetching, creating branches, or creating worktrees.
|
|
354
358
|
|
|
355
359
|
Options:
|
|
356
|
-
--epic <id>
|
|
360
|
+
--epic <id> Work on an epic
|
|
361
|
+
--task <id> Work on a task
|
|
362
|
+
--phase <id> Work on a phase selected with --task
|
|
363
|
+
--workbase <target> Select a workbase by ID, name, or path
|
|
364
|
+
--cwd <path> Resolve context from a specific directory
|
|
357
365
|
--opencode Require OpenCode
|
|
358
366
|
--claude Require Claude Code
|
|
359
367
|
--force Override readiness and terminal-state guards
|
|
360
|
-
--no-input
|
|
368
|
+
--no-input Never open an interactive selector
|
|
361
369
|
|
|
362
|
-
Without interactive input, provide
|
|
363
|
-
|
|
370
|
+
Without interactive input, provide an explicit workbase or cwd and an entity
|
|
371
|
+
selector. Workbase and target selection otherwise fail.
|
|
364
372
|
`
|