@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/README.md
CHANGED
|
@@ -304,6 +304,21 @@ inspection are opt-in include layers.
|
|
|
304
304
|
`end` record with counts. Combining the metadata with the streamed node and edge
|
|
305
305
|
records reconstructs the same result as `--json`.
|
|
306
306
|
|
|
307
|
+
### Next Ready Work
|
|
308
|
+
|
|
309
|
+
`agency next` lists ready execution units in descending unlock priority, with
|
|
310
|
+
their epic and task context. `agency next --select` returns only the highest-
|
|
311
|
+
priority ready unit in human output.
|
|
312
|
+
|
|
313
|
+
`agency next --json` returns the same ranked `ready` set plus every `excluded`
|
|
314
|
+
execution unit. Excluded entries retain status, terminal state, `blockedBy`, and
|
|
315
|
+
detailed dependency, validation, or status blockers for orchestrators.
|
|
316
|
+
|
|
317
|
+
`agency work` and `agency pr create` consult this shared readiness model before
|
|
318
|
+
materializing or pushing. Blocked, done, and dropped targets are rejected unless
|
|
319
|
+
`--force` is supplied explicitly. PR creation permits active `working` and
|
|
320
|
+
`delegated` targets when they have no dependency or validation blocker.
|
|
321
|
+
|
|
307
322
|
### Reconciliation
|
|
308
323
|
|
|
309
324
|
`agency sync` compares every execution declaration with local branch and worktree
|
|
@@ -327,8 +342,11 @@ conditions remain visible in `warnings` or `unresolved` with a suggested action.
|
|
|
327
342
|
|
|
328
343
|
```text
|
|
329
344
|
agency init [path] [--json]
|
|
330
|
-
agency workbase add <path> [--json]
|
|
345
|
+
agency workbase add <path> [--name <name>] [--json]
|
|
331
346
|
agency workbase list [--json]
|
|
347
|
+
agency workbase remove <id|name|path> [--json]
|
|
348
|
+
agency workbase prune [--json]
|
|
349
|
+
agency workbase default [<id|name> | --clear] [--json]
|
|
332
350
|
agency integration status [--json]
|
|
333
351
|
agency integration sync [--json]
|
|
334
352
|
agency repo add <alias> <remote> [--json]
|
|
@@ -338,6 +356,9 @@ agency repo list [--json]
|
|
|
338
356
|
|
|
339
357
|
Registered workbases are stored in
|
|
340
358
|
`$XDG_CONFIG_HOME/agency/workbases.json` (or `~/.config/agency/workbases.json`).
|
|
359
|
+
Each registration has a stable ID and may have a unique name. A default workbase
|
|
360
|
+
is used when the current directory is outside every workbase. `prune` removes
|
|
361
|
+
registrations whose workbase configuration no longer exists.
|
|
341
362
|
`repo add` creates a bare clone. `repo link` creates a symlink to an existing Git
|
|
342
363
|
repository. Alias names are then used by all documents and commands.
|
|
343
364
|
|
|
@@ -389,9 +410,17 @@ agency task create <id> --multi-phase
|
|
|
389
410
|
|
|
390
411
|
Agency never prompts when `--no-input` is set or stdin/stderr are not TTYs.
|
|
391
412
|
`--json` also disables prompts and selectors, even when a TTY is available.
|
|
392
|
-
Commands with explicit inputs continue normally.
|
|
393
|
-
|
|
394
|
-
|
|
413
|
+
Commands with explicit inputs continue normally. `--workbase <id|name|path>`
|
|
414
|
+
selects a workbase directly; `--cwd <path>` performs the same inference Agency
|
|
415
|
+
would perform from that directory. These options are mutually exclusive and take
|
|
416
|
+
precedence over ambient cwd and the configured default.
|
|
417
|
+
|
|
418
|
+
Targeted commands accept `--epic`, `--task`, and `--phase` where those entity
|
|
419
|
+
kinds apply. A phase selector requires a task selector. Entity selectors cannot
|
|
420
|
+
be mixed with positional target IDs, and an epic selector cannot be mixed with
|
|
421
|
+
task or phase selectors. This makes commands such as
|
|
422
|
+
`agency phase status done --task ship --phase release --workbase primary --no-input`
|
|
423
|
+
fully independent of process cwd and prompts.
|
|
395
424
|
|
|
396
425
|
Inspect tasks:
|
|
397
426
|
|
package/cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { Effect, Either, Layer } from "effect"
|
|
4
|
+
import { join, resolve } from "node:path"
|
|
4
5
|
import { parseCli } from "./src/cli-parser"
|
|
5
6
|
import { init, help as initHelp } from "./src/commands/init"
|
|
6
7
|
import { task, help as taskHelp } from "./src/commands/task"
|
|
@@ -10,6 +11,7 @@ import { status, help as statusHelp } from "./src/commands/status"
|
|
|
10
11
|
import { validate, help as validateHelp } from "./src/commands/validate"
|
|
11
12
|
import { context, help as contextHelp } from "./src/commands/context"
|
|
12
13
|
import { graph, help as graphHelp } from "./src/commands/graph"
|
|
14
|
+
import { next, help as nextHelp } from "./src/commands/next"
|
|
13
15
|
import { sync, help as syncHelp } from "./src/commands/sync"
|
|
14
16
|
import { repo, help as repoHelp } from "./src/commands/repo"
|
|
15
17
|
import { epic, help as epicHelp } from "./src/commands/epic"
|
|
@@ -35,6 +37,7 @@ import { ContextService } from "./src/services/ContextService"
|
|
|
35
37
|
import { GraphService } from "./src/services/GraphService"
|
|
36
38
|
import { ClaimService } from "./src/services/ClaimService"
|
|
37
39
|
import { SyncService } from "./src/services/SyncService"
|
|
40
|
+
import { ReadinessService } from "./src/services/ReadinessService"
|
|
38
41
|
import {
|
|
39
42
|
claimCommand,
|
|
40
43
|
claimHelp,
|
|
@@ -64,16 +67,15 @@ const CliLayer = Layer.mergeAll(
|
|
|
64
67
|
GraphService.Default,
|
|
65
68
|
ClaimService.Default,
|
|
66
69
|
SyncService.Default,
|
|
70
|
+
ReadinessService.Default,
|
|
67
71
|
)
|
|
68
72
|
|
|
69
73
|
/**
|
|
70
74
|
* Run a command Effect with all services provided
|
|
71
75
|
*/
|
|
72
|
-
async function
|
|
73
|
-
effect: Effect.Effect<void, E, any>,
|
|
74
|
-
): Promise<void> {
|
|
76
|
+
async function runEffect<A, E>(effect: Effect.Effect<A, E, any>): Promise<A> {
|
|
75
77
|
const providedEffect = Effect.provide(effect, CliLayer) as Effect.Effect<
|
|
76
|
-
|
|
78
|
+
A,
|
|
77
79
|
E,
|
|
78
80
|
never
|
|
79
81
|
>
|
|
@@ -100,8 +102,44 @@ async function runCommand<E>(
|
|
|
100
102
|
),
|
|
101
103
|
)
|
|
102
104
|
if (Either.isLeft(result)) throw result.left
|
|
105
|
+
return result.right
|
|
103
106
|
}
|
|
104
107
|
|
|
108
|
+
const runCommand = <E>(effect: Effect.Effect<void, E, any>) => runEffect(effect)
|
|
109
|
+
|
|
110
|
+
const resolveInvocationCwd = (
|
|
111
|
+
commandName: string,
|
|
112
|
+
options: Record<string, any>,
|
|
113
|
+
) =>
|
|
114
|
+
runEffect(
|
|
115
|
+
Effect.gen(function* () {
|
|
116
|
+
if (
|
|
117
|
+
options.help ||
|
|
118
|
+
commandName === "init" ||
|
|
119
|
+
commandName === "workbase"
|
|
120
|
+
) {
|
|
121
|
+
return resolve(options.cwd ?? process.cwd())
|
|
122
|
+
}
|
|
123
|
+
const workbases = yield* WorkbaseService
|
|
124
|
+
if (options.workbase) {
|
|
125
|
+
return yield* workbases.resolveRegistered(options.workbase)
|
|
126
|
+
}
|
|
127
|
+
if (options.cwd) {
|
|
128
|
+
const selectedCwd = resolve(options.cwd)
|
|
129
|
+
yield* workbases.discover(selectedCwd)
|
|
130
|
+
return selectedCwd
|
|
131
|
+
}
|
|
132
|
+
return yield* workbases.discover(process.cwd()).pipe(
|
|
133
|
+
Effect.as(process.cwd()),
|
|
134
|
+
Effect.catchTag("WorkbaseNotFoundError", () =>
|
|
135
|
+
workbases
|
|
136
|
+
.getDefault()
|
|
137
|
+
.pipe(Effect.map((entry) => entry?.path ?? process.cwd())),
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
}),
|
|
141
|
+
)
|
|
142
|
+
|
|
105
143
|
// Read version from package.json
|
|
106
144
|
const packageJson = await Bun.file(
|
|
107
145
|
new URL("./package.json", import.meta.url),
|
|
@@ -126,6 +164,7 @@ const commands: Record<string, Command> = {
|
|
|
126
164
|
json: options.json,
|
|
127
165
|
silent: options.silent,
|
|
128
166
|
verbose: options.verbose,
|
|
167
|
+
cwd: options.cwd,
|
|
129
168
|
}),
|
|
130
169
|
)
|
|
131
170
|
},
|
|
@@ -143,6 +182,7 @@ const commands: Record<string, Command> = {
|
|
|
143
182
|
json: options.json,
|
|
144
183
|
silent: options.silent,
|
|
145
184
|
verbose: options.verbose,
|
|
185
|
+
cwd: options.cwd,
|
|
146
186
|
}),
|
|
147
187
|
)
|
|
148
188
|
},
|
|
@@ -161,6 +201,7 @@ const commands: Record<string, Command> = {
|
|
|
161
201
|
json: options.json,
|
|
162
202
|
silent: options.silent,
|
|
163
203
|
verbose: options.verbose,
|
|
204
|
+
cwd: options.cwd,
|
|
164
205
|
}),
|
|
165
206
|
)
|
|
166
207
|
},
|
|
@@ -177,6 +218,7 @@ const commands: Record<string, Command> = {
|
|
|
177
218
|
json: options.json,
|
|
178
219
|
silent: options.silent,
|
|
179
220
|
verbose: options.verbose,
|
|
221
|
+
cwd: options.cwd,
|
|
180
222
|
}),
|
|
181
223
|
)
|
|
182
224
|
},
|
|
@@ -197,6 +239,7 @@ const commands: Record<string, Command> = {
|
|
|
197
239
|
json: options.json,
|
|
198
240
|
silent: options.silent,
|
|
199
241
|
verbose: options.verbose,
|
|
242
|
+
cwd: options.cwd,
|
|
200
243
|
}),
|
|
201
244
|
)
|
|
202
245
|
},
|
|
@@ -213,9 +256,11 @@ const commands: Record<string, Command> = {
|
|
|
213
256
|
taskId: args[1],
|
|
214
257
|
phaseId: args[2],
|
|
215
258
|
draft: options.draft,
|
|
259
|
+
force: options.force,
|
|
216
260
|
json: options.json,
|
|
217
261
|
silent: options.silent,
|
|
218
262
|
verbose: options.verbose,
|
|
263
|
+
cwd: options.cwd,
|
|
219
264
|
}),
|
|
220
265
|
)
|
|
221
266
|
},
|
|
@@ -237,6 +282,7 @@ const commands: Record<string, Command> = {
|
|
|
237
282
|
json: options.json,
|
|
238
283
|
silent: options.silent,
|
|
239
284
|
verbose: options.verbose,
|
|
285
|
+
cwd: options.cwd,
|
|
240
286
|
}),
|
|
241
287
|
)
|
|
242
288
|
},
|
|
@@ -254,6 +300,7 @@ const commands: Record<string, Command> = {
|
|
|
254
300
|
json: options.json,
|
|
255
301
|
silent: options.silent,
|
|
256
302
|
verbose: options.verbose,
|
|
303
|
+
cwd: options.cwd,
|
|
257
304
|
}),
|
|
258
305
|
)
|
|
259
306
|
},
|
|
@@ -269,8 +316,11 @@ const commands: Record<string, Command> = {
|
|
|
269
316
|
subcommand: args[0],
|
|
270
317
|
args: args.slice(1),
|
|
271
318
|
json: options.json,
|
|
319
|
+
name: options.name,
|
|
320
|
+
clear: options.clear,
|
|
272
321
|
silent: options.silent,
|
|
273
322
|
verbose: options.verbose,
|
|
323
|
+
cwd: options.cwd,
|
|
274
324
|
}),
|
|
275
325
|
)
|
|
276
326
|
},
|
|
@@ -287,6 +337,7 @@ const commands: Record<string, Command> = {
|
|
|
287
337
|
json: options.json,
|
|
288
338
|
silent: options.silent,
|
|
289
339
|
verbose: options.verbose,
|
|
340
|
+
cwd: options.cwd,
|
|
290
341
|
}),
|
|
291
342
|
)
|
|
292
343
|
},
|
|
@@ -304,6 +355,7 @@ const commands: Record<string, Command> = {
|
|
|
304
355
|
silent: options.silent,
|
|
305
356
|
verbose: options.verbose,
|
|
306
357
|
json: options.json,
|
|
358
|
+
cwd: options.cwd,
|
|
307
359
|
}),
|
|
308
360
|
)
|
|
309
361
|
},
|
|
@@ -330,6 +382,7 @@ const commands: Record<string, Command> = {
|
|
|
330
382
|
silent: options.silent,
|
|
331
383
|
verbose: options.verbose,
|
|
332
384
|
inputAllowed: options.inputAllowed,
|
|
385
|
+
cwd: options.cwd,
|
|
333
386
|
}),
|
|
334
387
|
)
|
|
335
388
|
},
|
|
@@ -351,7 +404,24 @@ const commands: Record<string, Command> = {
|
|
|
351
404
|
verbose: options.verbose,
|
|
352
405
|
opencode: options.opencode,
|
|
353
406
|
claude: options.claude,
|
|
407
|
+
force: options.force,
|
|
354
408
|
inputAllowed: options.inputAllowed,
|
|
409
|
+
cwd: options.cwd,
|
|
410
|
+
taskId: options.task,
|
|
411
|
+
phaseId: options.phase,
|
|
412
|
+
}),
|
|
413
|
+
)
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
next: {
|
|
417
|
+
run: async (_args: string[], options: Record<string, any>) => {
|
|
418
|
+
if (options.help) return console.log(nextHelp)
|
|
419
|
+
await runCommand(
|
|
420
|
+
next({
|
|
421
|
+
select: options.select,
|
|
422
|
+
json: options.json,
|
|
423
|
+
silent: options.silent,
|
|
424
|
+
verbose: options.verbose,
|
|
355
425
|
}),
|
|
356
426
|
)
|
|
357
427
|
},
|
|
@@ -367,6 +437,7 @@ const commands: Record<string, Command> = {
|
|
|
367
437
|
silent: options.silent,
|
|
368
438
|
verbose: options.verbose,
|
|
369
439
|
json: options.json,
|
|
440
|
+
cwd: options.cwd,
|
|
370
441
|
}),
|
|
371
442
|
)
|
|
372
443
|
},
|
|
@@ -384,6 +455,7 @@ const commands: Record<string, Command> = {
|
|
|
384
455
|
verbose: options.verbose,
|
|
385
456
|
json: options.json,
|
|
386
457
|
inputAllowed: options.inputAllowed,
|
|
458
|
+
cwd: options.cwd,
|
|
387
459
|
}),
|
|
388
460
|
)
|
|
389
461
|
},
|
|
@@ -396,11 +468,18 @@ const commands: Record<string, Command> = {
|
|
|
396
468
|
}
|
|
397
469
|
await runCommand(
|
|
398
470
|
context({
|
|
399
|
-
target:
|
|
471
|
+
target: options.epic
|
|
472
|
+
? join("epics", options.epic)
|
|
473
|
+
: options.phase
|
|
474
|
+
? join("tasks", options.task, "phases", options.phase)
|
|
475
|
+
: options.task
|
|
476
|
+
? join("tasks", options.task)
|
|
477
|
+
: args[0],
|
|
400
478
|
compact: options.compact,
|
|
401
479
|
json: options.json,
|
|
402
480
|
silent: options.silent,
|
|
403
481
|
verbose: options.verbose,
|
|
482
|
+
cwd: options.cwd,
|
|
404
483
|
}),
|
|
405
484
|
)
|
|
406
485
|
},
|
|
@@ -423,6 +502,7 @@ const commands: Record<string, Command> = {
|
|
|
423
502
|
include: options.include,
|
|
424
503
|
silent: options.silent,
|
|
425
504
|
verbose: options.verbose,
|
|
505
|
+
cwd: options.cwd,
|
|
426
506
|
}),
|
|
427
507
|
)
|
|
428
508
|
},
|
|
@@ -440,6 +520,7 @@ const commands: Record<string, Command> = {
|
|
|
440
520
|
json: options.json,
|
|
441
521
|
silent: options.silent,
|
|
442
522
|
verbose: options.verbose,
|
|
523
|
+
cwd: options.cwd,
|
|
443
524
|
}),
|
|
444
525
|
)
|
|
445
526
|
},
|
|
@@ -464,6 +545,7 @@ Commands:
|
|
|
464
545
|
archive <type> Archive a work item
|
|
465
546
|
task <subcommand> Manage tasks
|
|
466
547
|
work [directory|task] Work on an epic, task, or phase
|
|
548
|
+
next List or select ready execution units
|
|
467
549
|
pr create Create a pull request for an execution unit
|
|
468
550
|
repo <subcommand> Manage workbase repositories
|
|
469
551
|
status Show status for the current workbase
|
|
@@ -478,6 +560,8 @@ Global Options:
|
|
|
478
560
|
-s, --silent Suppress output messages
|
|
479
561
|
-v, --verbose Show verbose output including detailed debugging info
|
|
480
562
|
--no-input Never open an interactive prompt or selector
|
|
563
|
+
--workbase <selector> Use a registered workbase ID, name, or path
|
|
564
|
+
--cwd <path> Resolve context from this directory
|
|
481
565
|
|
|
482
566
|
Examples:
|
|
483
567
|
agency init # Initialize the current directory
|
|
@@ -519,15 +603,16 @@ try {
|
|
|
519
603
|
!values.json &&
|
|
520
604
|
!values["no-input"] &&
|
|
521
605
|
Boolean(process.stdin.isTTY && process.stderr.isTTY)
|
|
606
|
+
const cwd = await resolveInvocationCwd(commandName, values)
|
|
522
607
|
if (values.json || (values.jsonl && values.help)) {
|
|
523
608
|
const result = await collectCommandResult(() =>
|
|
524
|
-
command.run(commandArgs, { ...values, inputAllowed }),
|
|
609
|
+
command.run(commandArgs, { ...values, cwd, inputAllowed }),
|
|
525
610
|
)
|
|
526
611
|
writeEnvelope(successEnvelope(result))
|
|
527
612
|
} else if (values.jsonl) {
|
|
528
|
-
await command.run(commandArgs, { ...values, inputAllowed: false })
|
|
613
|
+
await command.run(commandArgs, { ...values, cwd, inputAllowed: false })
|
|
529
614
|
} else {
|
|
530
|
-
await command.run(commandArgs, { ...values, inputAllowed })
|
|
615
|
+
await command.run(commandArgs, { ...values, cwd, inputAllowed })
|
|
531
616
|
}
|
|
532
617
|
} catch (error) {
|
|
533
618
|
if (machineMode) {
|
package/package.json
CHANGED
package/skills/agency/SKILL.md
CHANGED
|
@@ -60,13 +60,19 @@ agency init [path]
|
|
|
60
60
|
agency integration sync
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
Register known workbases so
|
|
63
|
+
Register and name known workbases so commands can select one from anywhere:
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
agency workbase add <path>
|
|
66
|
+
agency workbase add <path> [--name <name>]
|
|
67
67
|
agency workbase list
|
|
68
|
+
agency workbase default [<id|name> | --clear]
|
|
69
|
+
agency workbase remove <id|name|path>
|
|
70
|
+
agency workbase prune
|
|
68
71
|
```
|
|
69
72
|
|
|
73
|
+
Use `--workbase <id|name|path>` to bypass cwd inference, or `--cwd <path>` to
|
|
74
|
+
infer context from a specific directory. The options are mutually exclusive.
|
|
75
|
+
|
|
70
76
|
## Repository Aliases
|
|
71
77
|
|
|
72
78
|
Add a remote as an Agency-managed bare repository:
|
|
@@ -261,8 +267,8 @@ Use `--json` when diagnostics will be consumed programmatically. Resolve all
|
|
|
261
267
|
validation errors before materializing worktrees or creating PRs. Validation
|
|
262
268
|
checks schemas, aliases, backlinks, phase directories, duplicate references,
|
|
263
269
|
duplicate writable branch ownership, unknown dependencies, and dependency cycles.
|
|
264
|
-
Outside a workbase,
|
|
265
|
-
With `--no-input` or without a TTY, pass
|
|
270
|
+
Outside a workbase, Agency uses the configured default or opens the registered
|
|
271
|
+
workbase picker. With `--no-input` or without a TTY, pass `--workbase` or `--cwd`.
|
|
266
272
|
|
|
267
273
|
## Worktrees And Agent Launch
|
|
268
274
|
|
|
@@ -270,6 +276,7 @@ With `--no-input` or without a TTY, pass a path or run from a workbase instead.
|
|
|
270
276
|
agency work
|
|
271
277
|
agency work <directory>
|
|
272
278
|
agency work --epic <epic-id>
|
|
279
|
+
agency work --task <task-id> [--phase <phase-id>] --workbase <selector>
|
|
273
280
|
```
|
|
274
281
|
|
|
275
282
|
Use `--opencode` or `--claude` to require a specific agent. This command fetches
|
|
@@ -278,8 +285,8 @@ replaces the current process with the selected agent. With no directory it opens
|
|
|
278
285
|
an `fzf` picker containing the workbase hierarchy. Pass `.`, or another
|
|
279
286
|
directory, to infer the nearest epic, task, or phase.
|
|
280
287
|
Outside a workbase, it first opens a picker containing registered workbases.
|
|
281
|
-
With `--no-input` or without a TTY,
|
|
282
|
-
|
|
288
|
+
With `--no-input` or without a TTY, provide an explicit workbase or cwd and an
|
|
289
|
+
`--epic`, `--task`, or `--task` plus `--phase` selector so no picker is needed.
|
|
283
290
|
|
|
284
291
|
Epic and multi-phase task targets are orchestration sessions launched beside
|
|
285
292
|
their documents. Single-phase tasks and phases are execution sessions launched
|
package/src/cli-parser.test.ts
CHANGED
|
@@ -141,6 +141,7 @@ describe("strict CLI parsing", () => {
|
|
|
141
141
|
[["validate", "one", "two"], "agency validate"],
|
|
142
142
|
[["context", "one", "two"], "agency context"],
|
|
143
143
|
[["graph", "extra"], "agency graph"],
|
|
144
|
+
[["next", "extra"], "agency next"],
|
|
144
145
|
[["sync", "extra"], "agency sync"],
|
|
145
146
|
[
|
|
146
147
|
[
|
|
@@ -192,6 +193,24 @@ describe("strict CLI parsing", () => {
|
|
|
192
193
|
}
|
|
193
194
|
})
|
|
194
195
|
|
|
196
|
+
test("parses readiness selection and explicit guard overrides", () => {
|
|
197
|
+
expect(parseCli(["next", "--select", "--json"])).toMatchObject({
|
|
198
|
+
commandName: "next",
|
|
199
|
+
values: { select: true, json: true },
|
|
200
|
+
})
|
|
201
|
+
expect(parseCli(["work", "example", "--force"])).toMatchObject({
|
|
202
|
+
commandName: "work",
|
|
203
|
+
values: { force: true },
|
|
204
|
+
})
|
|
205
|
+
expect(parseCli(["pr", "create", "example", "--force"])).toMatchObject({
|
|
206
|
+
commandName: "pr",
|
|
207
|
+
values: { force: true },
|
|
208
|
+
})
|
|
209
|
+
expect(() => parseCli(["work", "prepare", "example", "--force"])).toThrow(
|
|
210
|
+
"cannot be combined",
|
|
211
|
+
)
|
|
212
|
+
})
|
|
213
|
+
|
|
195
214
|
test("parses reconciliation modes and rejects conflicting modes", () => {
|
|
196
215
|
expect(parseCli(["sync", "--dry-run", "--json"])).toMatchObject({
|
|
197
216
|
commandName: "sync",
|
|
@@ -365,6 +384,80 @@ describe("strict CLI parsing", () => {
|
|
|
365
384
|
).toBe(true)
|
|
366
385
|
})
|
|
367
386
|
|
|
387
|
+
test("normalizes explicit entity selectors into command targets", () => {
|
|
388
|
+
expect(
|
|
389
|
+
parseCli([
|
|
390
|
+
"phase",
|
|
391
|
+
"status",
|
|
392
|
+
"done",
|
|
393
|
+
"--task",
|
|
394
|
+
"ship",
|
|
395
|
+
"--phase",
|
|
396
|
+
"release",
|
|
397
|
+
]),
|
|
398
|
+
).toMatchObject({
|
|
399
|
+
commandName: "phase",
|
|
400
|
+
args: ["status", "ship", "release", "done"],
|
|
401
|
+
})
|
|
402
|
+
expect(
|
|
403
|
+
parseCli([
|
|
404
|
+
"claim",
|
|
405
|
+
"--task",
|
|
406
|
+
"ship",
|
|
407
|
+
"--phase",
|
|
408
|
+
"release",
|
|
409
|
+
"--claimant",
|
|
410
|
+
"agent",
|
|
411
|
+
"--runner",
|
|
412
|
+
"opencode",
|
|
413
|
+
"--session-id",
|
|
414
|
+
"session",
|
|
415
|
+
"--revision",
|
|
416
|
+
"0".repeat(64),
|
|
417
|
+
]),
|
|
418
|
+
).toMatchObject({ args: ["ship", "release"] })
|
|
419
|
+
expect(parseCli(["context", "--epic", "delivery"]).values.epic).toBe(
|
|
420
|
+
"delivery",
|
|
421
|
+
)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
test("enforces explicit selector precedence and exclusions", () => {
|
|
425
|
+
expect(() =>
|
|
426
|
+
parseCli(["task", "show", "positional", "--task", "explicit"]),
|
|
427
|
+
).toThrow("cannot be combined with positional target IDs")
|
|
428
|
+
expect(() => parseCli(["context", "--phase", "release"])).toThrow(
|
|
429
|
+
"--phase' requires '--task",
|
|
430
|
+
)
|
|
431
|
+
expect(() =>
|
|
432
|
+
parseCli(["work", "--epic", "delivery", "--task", "ship"]),
|
|
433
|
+
).toThrow("cannot be combined with '--task'")
|
|
434
|
+
expect(() =>
|
|
435
|
+
parseCli(["status", "--workbase", "primary", "--cwd", "/tmp"]),
|
|
436
|
+
).toThrow("--workbase' and '--cwd' cannot be combined")
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
test("accepts explicit workbase context before or after commands", () => {
|
|
440
|
+
expect(
|
|
441
|
+
parseCli(["--workbase", "primary", "task", "list", "--no-input"]).values
|
|
442
|
+
.workbase,
|
|
443
|
+
).toBe("primary")
|
|
444
|
+
expect(parseCli(["status", "--cwd", "/tmp"]).values.cwd).toBe("/tmp")
|
|
445
|
+
expect(
|
|
446
|
+
parseCli(["--workbase=primary", "task", "list"]).values.workbase,
|
|
447
|
+
).toBe("primary")
|
|
448
|
+
expect(parseCli(["--cwd=/tmp", "status"]).values.cwd).toBe("/tmp")
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
test("rejects empty selectors", () => {
|
|
452
|
+
for (const args of [
|
|
453
|
+
["status", "--workbase="],
|
|
454
|
+
["status", "--cwd="],
|
|
455
|
+
["context", "--task="],
|
|
456
|
+
]) {
|
|
457
|
+
expect(() => parseCli(args)).toThrow("requires a non-empty value")
|
|
458
|
+
}
|
|
459
|
+
})
|
|
460
|
+
|
|
368
461
|
test("accepts grouped global short options before a command", () => {
|
|
369
462
|
const parsed = parseCli(["-sh", "task"])
|
|
370
463
|
expect(parsed.values).toMatchObject({ silent: true, help: true })
|