@markjaquith/agency 2.19.0 → 2.21.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 +67 -5
- package/cli.ts +15 -0
- package/package.json +1 -1
- package/skills/agency/SKILL.md +4 -1
- package/src/cli-parser.test.ts +61 -1
- package/src/cli-parser.ts +168 -7
- package/src/cli.test.ts +62 -0
- package/src/commands/epic.ts +51 -1
- package/src/commands/phase.ts +94 -1
- package/src/commands/task.ts +108 -1
- package/src/commands/work.test.ts +105 -16
- package/src/commands/work.ts +99 -28
- package/src/services/GraphMutationService.test.ts +336 -0
- package/src/services/GraphMutationService.ts +952 -0
- package/src/services/WorkbaseService.test.ts +19 -0
- package/src/services/WorkbaseService.ts +15 -37
- package/src/test-utils.ts +2 -0
- package/src/workbase/dependency-graph.ts +51 -0
- package/src/workbase/runner-command.test.ts +79 -0
- package/src/workbase/runner-command.ts +118 -0
- package/src/workbase/schemas.test.ts +26 -0
- package/src/workbase/schemas.ts +15 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ or write.
|
|
|
10
10
|
- [Bun](https://bun.sh) 1.0 or newer
|
|
11
11
|
- Git
|
|
12
12
|
- [GitHub CLI](https://cli.github.com/) for `agency pr create`
|
|
13
|
-
- OpenCode
|
|
13
|
+
- OpenCode, Claude Code, or a configured runner for `agency work`
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
@@ -145,6 +145,41 @@ The configured command applies only to the writable checkout. Supplemental
|
|
|
145
145
|
read-only repositories remain detached Git worktrees at their declared refs so
|
|
146
146
|
they do not acquire writable branches.
|
|
147
147
|
|
|
148
|
+
### Agent Runners
|
|
149
|
+
|
|
150
|
+
OpenCode and Claude Code are built-in runner presets. Select either preset or a
|
|
151
|
+
configured runner with `agency work --runner <name>`. A launch is fresh unless
|
|
152
|
+
`AGENCY_SESSION_ID` is already set; resumed launches use the runner's
|
|
153
|
+
`resumeCommand` when configured. The built-in presets use `--continue` only for
|
|
154
|
+
resumed launches.
|
|
155
|
+
|
|
156
|
+
Custom runners are direct argv commands, never shell snippets:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"version": 2,
|
|
161
|
+
"runners": {
|
|
162
|
+
"custom": {
|
|
163
|
+
"command": ["my-agent", "--prompt", "{prompt}"],
|
|
164
|
+
"resumeCommand": ["my-agent", "resume", "{sessionId}", "{prompt}"],
|
|
165
|
+
"environment": { "MY_AGENT_TARGET": "{target}" }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Available placeholders are `{prompt}`, `{workbase}`, `{target}`, `{task}`,
|
|
172
|
+
`{phase}`, `{claimant}`, `{sessionId}`, and `{claimRevision}`. Task and phase
|
|
173
|
+
placeholders are empty when they do not apply. If `resumeCommand` is omitted,
|
|
174
|
+
the fresh command is also used for resumed sessions.
|
|
175
|
+
|
|
176
|
+
Every runner receives the same `AGENCY_RUNNER`, `AGENCY_CLAIMANT`,
|
|
177
|
+
`AGENCY_SESSION_ID`, `AGENCY_CLAIM_REVISION`, `AGENCY_WORKBASE`, `AGENCY_TARGET`,
|
|
178
|
+
`AGENCY_TASK_ID`, `AGENCY_PHASE_ID`, and `AGENCY_PROMPT` environment. Configured
|
|
179
|
+
environment is added without overriding these normalized values.
|
|
180
|
+
`--print-command` prints the exact cwd and argv plus non-secret environment keys
|
|
181
|
+
without launching the runner.
|
|
182
|
+
|
|
148
183
|
### Custom Chooser Command
|
|
149
184
|
|
|
150
185
|
Interactive selection uses a native numbered chooser by default. To use an
|
|
@@ -373,6 +408,9 @@ agency epic create <id> --ticket-url <url> [--description <text>] [--json]
|
|
|
373
408
|
--repo <alias>:<ref> [--repo <alias>:<ref>...]
|
|
374
409
|
agency epic list [filters] [--json]
|
|
375
410
|
agency epic show <id> [--json]
|
|
411
|
+
agency epic update <id> [--ticket-url <url>] [--description <text>]
|
|
412
|
+
[--clear-description] [--repo <alias>:<ref>...] [--json]
|
|
413
|
+
agency epic rename <id> <new-id> [--json]
|
|
376
414
|
```
|
|
377
415
|
|
|
378
416
|
Creating a task with `--epic <id>` adds the task to the epic and writes the task
|
|
@@ -428,8 +466,18 @@ Inspect tasks:
|
|
|
428
466
|
agency task list [filters] [--json]
|
|
429
467
|
agency task show <id> [--json]
|
|
430
468
|
agency task status <id> <open|done|dropped> [--json]
|
|
469
|
+
agency task update <id> [metadata options] [--json]
|
|
470
|
+
agency task rename <id> <new-id> [--json]
|
|
471
|
+
agency task move <id> (--epic <epic-id> | --no-epic) [--json]
|
|
472
|
+
agency task dependency <add|remove> <task-id> <dependency-id> [--json]
|
|
431
473
|
```
|
|
432
474
|
|
|
475
|
+
Task updates can replace or clear descriptions, tickets, repository references,
|
|
476
|
+
and pull request URLs, or replace writable repository, branch, and base metadata.
|
|
477
|
+
Execution metadata changes refuse to run while code is materialized. Moving a
|
|
478
|
+
task with scoped incoming or outgoing dependencies also refuses until those
|
|
479
|
+
dependencies are removed.
|
|
480
|
+
|
|
433
481
|
To add a phase to an existing single-phase task, name the phase that will own
|
|
434
482
|
the task's current execution fields with `--first-phase`:
|
|
435
483
|
|
|
@@ -455,8 +503,18 @@ agency phase create <task-id> <phase-id>
|
|
|
455
503
|
agency phase list <task-id> [filters] [--json]
|
|
456
504
|
agency phase show <task-id> <phase-id> [--json]
|
|
457
505
|
agency phase status <task-id> <phase-id> <open|done|dropped> [--json]
|
|
506
|
+
agency phase update <task-id> <phase-id> [metadata options] [--json]
|
|
507
|
+
agency phase rename <task-id> <phase-id> <new-id> [--json]
|
|
508
|
+
agency phase dependency <add|remove> <task-id> <phase-id> <dependency-id>
|
|
509
|
+
[--json]
|
|
458
510
|
```
|
|
459
511
|
|
|
512
|
+
Dependency additions append without reordering existing declarations and reject
|
|
513
|
+
unknown IDs, self-dependencies, and cycles. Rename operations update structured
|
|
514
|
+
references as one rollback-capable mutation and refuse when a materialized
|
|
515
|
+
worktree would make the directory move unsafe. Mutation JSON includes changed
|
|
516
|
+
paths and the focused validation scope.
|
|
517
|
+
|
|
460
518
|
Single-phase tasks and phases store status in YAML. New execution units start
|
|
461
519
|
`open`, and `agency work` marks the selected execution unit `working` immediately
|
|
462
520
|
before launch. Use claims for coordinated ownership and the status subcommands
|
|
@@ -500,9 +558,9 @@ replaced with a revision-guarded claim.
|
|
|
500
558
|
|
|
501
559
|
`agency work` claims an execution unit before launching its agent. Set
|
|
502
560
|
`AGENCY_CLAIMANT`, `AGENCY_RUNNER`, or `AGENCY_SESSION_ID` to supply orchestrator
|
|
503
|
-
identities; otherwise Agency derives them from the user
|
|
504
|
-
|
|
505
|
-
|
|
561
|
+
identities; otherwise Agency derives them from the user and process. The selected
|
|
562
|
+
runner name is recorded on the claim. The launched agent receives the normalized
|
|
563
|
+
runner environment documented above for a later release or finish operation.
|
|
506
564
|
|
|
507
565
|
### Archive
|
|
508
566
|
|
|
@@ -520,7 +578,7 @@ before moving files, refuses dirty worktrees, and preserves branches.
|
|
|
520
578
|
### Work and Pull Requests
|
|
521
579
|
|
|
522
580
|
```text
|
|
523
|
-
agency work [<directory> | --epic <epic-id>] [--
|
|
581
|
+
agency work [<directory> | --epic <epic-id>] [--runner <name>] [--print-command]
|
|
524
582
|
agency work prepare [target] [--dry-run] [--json]
|
|
525
583
|
agency pr create <task-id> [phase-id] [--draft] [--json]
|
|
526
584
|
```
|
|
@@ -531,6 +589,10 @@ workbase, Agency first presents the registered workbases, then the selected
|
|
|
531
589
|
workbase's hierarchy. If `fzf` is not installed, Agency prints the available
|
|
532
590
|
choices and asks for an explicit directory.
|
|
533
591
|
|
|
592
|
+
OpenCode is the default runner, with automatic Claude fallback when neither is
|
|
593
|
+
explicitly selected. `--opencode` and `--claude` remain aliases for requiring
|
|
594
|
+
their built-in presets.
|
|
595
|
+
|
|
534
596
|
`agency work prepare` resolves an execution unit and creates or reuses its
|
|
535
597
|
writable and reference worktrees without launching an agent or changing status.
|
|
536
598
|
Its JSON result includes document and checkout paths, resolved commits, actions,
|
package/cli.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { GraphService } from "./src/services/GraphService"
|
|
|
38
38
|
import { ClaimService } from "./src/services/ClaimService"
|
|
39
39
|
import { SyncService } from "./src/services/SyncService"
|
|
40
40
|
import { ReadinessService } from "./src/services/ReadinessService"
|
|
41
|
+
import { GraphMutationService } from "./src/services/GraphMutationService"
|
|
41
42
|
import {
|
|
42
43
|
claimCommand,
|
|
43
44
|
claimHelp,
|
|
@@ -68,6 +69,7 @@ const CliLayer = Layer.mergeAll(
|
|
|
68
69
|
ClaimService.Default,
|
|
69
70
|
SyncService.Default,
|
|
70
71
|
ReadinessService.Default,
|
|
72
|
+
GraphMutationService.Default,
|
|
71
73
|
)
|
|
72
74
|
|
|
73
75
|
/**
|
|
@@ -235,6 +237,7 @@ const commands: Record<string, Command> = {
|
|
|
235
237
|
args: args.slice(1),
|
|
236
238
|
ticketUrl: options["ticket-url"],
|
|
237
239
|
description: options.description,
|
|
240
|
+
clearDescription: options["clear-description"],
|
|
238
241
|
repos: options.repo,
|
|
239
242
|
json: options.json,
|
|
240
243
|
statuses: options.status,
|
|
@@ -278,10 +281,14 @@ const commands: Record<string, Command> = {
|
|
|
278
281
|
subcommand: args[0],
|
|
279
282
|
args: args.slice(1),
|
|
280
283
|
description: options.description,
|
|
284
|
+
clearDescription: options["clear-description"],
|
|
281
285
|
repo: options.repo?.[0],
|
|
282
286
|
references: options.reference,
|
|
283
287
|
branch: options.branch,
|
|
284
288
|
base: options.base,
|
|
289
|
+
clearReferences: options["clear-references"],
|
|
290
|
+
prUrl: options["pr-url"],
|
|
291
|
+
clearPr: options["clear-pr"],
|
|
285
292
|
dependsOn: options["depends-on"],
|
|
286
293
|
firstPhase: options["first-phase"],
|
|
287
294
|
json: options.json,
|
|
@@ -381,12 +388,18 @@ const commands: Record<string, Command> = {
|
|
|
381
388
|
subcommand: args[0],
|
|
382
389
|
args: args.slice(1),
|
|
383
390
|
ticketUrl: options["ticket-url"],
|
|
391
|
+
clearTicket: options["clear-ticket"],
|
|
384
392
|
description: options.description,
|
|
393
|
+
clearDescription: options["clear-description"],
|
|
385
394
|
epic: options.epic,
|
|
386
395
|
repo: options.repo?.[0],
|
|
387
396
|
references: options.reference,
|
|
388
397
|
branch: options.branch,
|
|
389
398
|
base: options.base,
|
|
399
|
+
clearReferences: options["clear-references"],
|
|
400
|
+
prUrl: options["pr-url"],
|
|
401
|
+
clearPr: options["clear-pr"],
|
|
402
|
+
noEpic: options["no-epic"],
|
|
390
403
|
multiPhase: options["multi-phase"],
|
|
391
404
|
json: options.json,
|
|
392
405
|
statuses: options.status,
|
|
@@ -419,6 +432,8 @@ const commands: Record<string, Command> = {
|
|
|
419
432
|
verbose: options.verbose,
|
|
420
433
|
opencode: options.opencode,
|
|
421
434
|
claude: options.claude,
|
|
435
|
+
runner: options.runner,
|
|
436
|
+
printCommand: options["print-command"],
|
|
422
437
|
force: options.force,
|
|
423
438
|
inputAllowed: options.inputAllowed,
|
|
424
439
|
cwd: options.cwd,
|
package/package.json
CHANGED
package/skills/agency/SKILL.md
CHANGED
|
@@ -279,7 +279,10 @@ agency work --epic <epic-id>
|
|
|
279
279
|
agency work --task <task-id> [--phase <phase-id>] --workbase <selector>
|
|
280
280
|
```
|
|
281
281
|
|
|
282
|
-
Use `--
|
|
282
|
+
Use `--runner <name>` to select a configured runner or the built-in `opencode`
|
|
283
|
+
and `claude` presets. `--opencode` and `--claude` remain shorthand. Use
|
|
284
|
+
`--print-command` to inspect the resolved cwd, argv, and non-secret environment
|
|
285
|
+
without launching. This command fetches
|
|
283
286
|
repositories for execution targets, creates or reuses their worktrees, and
|
|
284
287
|
replaces the current process with the selected agent. With no directory it opens
|
|
285
288
|
an `fzf` picker containing the workbase hierarchy. Pass `.`, or another
|
package/src/cli-parser.test.ts
CHANGED
|
@@ -321,6 +321,22 @@ describe("strict CLI parsing", () => {
|
|
|
321
321
|
)
|
|
322
322
|
})
|
|
323
323
|
|
|
324
|
+
test("accepts runner selection and command inspection for work", () => {
|
|
325
|
+
expect(
|
|
326
|
+
parseCli(["work", "example", "--runner", "custom", "--print-command"]),
|
|
327
|
+
).toMatchObject({
|
|
328
|
+
commandName: "work",
|
|
329
|
+
args: ["example"],
|
|
330
|
+
values: { runner: "custom", "print-command": true },
|
|
331
|
+
})
|
|
332
|
+
expect(() =>
|
|
333
|
+
parseCli(["work", "example", "--runner", "custom", "--claude"]),
|
|
334
|
+
).toThrow("cannot be combined")
|
|
335
|
+
expect(() => parseCli(["work", "prepare", "--print-command"])).toThrow(
|
|
336
|
+
"cannot be combined",
|
|
337
|
+
)
|
|
338
|
+
})
|
|
339
|
+
|
|
324
340
|
test("accepts repeatable graph filters and rejects output conflicts", () => {
|
|
325
341
|
expect(
|
|
326
342
|
parseCli([
|
|
@@ -371,7 +387,51 @@ describe("strict CLI parsing", () => {
|
|
|
371
387
|
)
|
|
372
388
|
expectUsageError(
|
|
373
389
|
["task", "crate"],
|
|
374
|
-
"agency task <new|create|list|show|status>",
|
|
390
|
+
"agency task <new|create|list|show|status|update|rename|move|dependency>",
|
|
391
|
+
)
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
test("parses graph mutation commands and rejects unsafe ambiguity", () => {
|
|
395
|
+
expect(
|
|
396
|
+
parseCli([
|
|
397
|
+
"task",
|
|
398
|
+
"update",
|
|
399
|
+
"example",
|
|
400
|
+
"--description",
|
|
401
|
+
"Revised",
|
|
402
|
+
"--reference",
|
|
403
|
+
"docs:main",
|
|
404
|
+
"--reference",
|
|
405
|
+
"api:main",
|
|
406
|
+
"--clear-pr",
|
|
407
|
+
]),
|
|
408
|
+
).toMatchObject({
|
|
409
|
+
args: ["update", "example"],
|
|
410
|
+
values: {
|
|
411
|
+
description: "Revised",
|
|
412
|
+
reference: ["docs:main", "api:main"],
|
|
413
|
+
"clear-pr": true,
|
|
414
|
+
},
|
|
415
|
+
})
|
|
416
|
+
expect(
|
|
417
|
+
parseCli(["task", "move", "example", "--no-epic"]).values,
|
|
418
|
+
).toMatchObject({
|
|
419
|
+
"no-epic": true,
|
|
420
|
+
})
|
|
421
|
+
expect(parseCli(["phase", "rename", "task", "old", "new"]).args).toEqual([
|
|
422
|
+
"rename",
|
|
423
|
+
"task",
|
|
424
|
+
"old",
|
|
425
|
+
"new",
|
|
426
|
+
])
|
|
427
|
+
expect(() => parseCli(["task", "update", "example"])).toThrow(
|
|
428
|
+
"At least one update option",
|
|
429
|
+
)
|
|
430
|
+
expect(() =>
|
|
431
|
+
parseCli(["task", "move", "example", "--epic", "one", "--no-epic"]),
|
|
432
|
+
).toThrow("cannot be combined")
|
|
433
|
+
expect(() => parseCli(["task", "dependency", "replace", "a", "b"])).toThrow(
|
|
434
|
+
"must be 'add' or 'remove'",
|
|
375
435
|
)
|
|
376
436
|
})
|
|
377
437
|
|
package/src/cli-parser.ts
CHANGED
|
@@ -90,6 +90,15 @@ const phaseCreateOptions = {
|
|
|
90
90
|
"first-phase": { type: "string" },
|
|
91
91
|
} satisfies OptionConfig
|
|
92
92
|
|
|
93
|
+
const mutationOptions = {
|
|
94
|
+
"clear-description": { type: "boolean" },
|
|
95
|
+
"clear-ticket": { type: "boolean" },
|
|
96
|
+
"clear-references": { type: "boolean" },
|
|
97
|
+
"pr-url": { type: "string" },
|
|
98
|
+
"clear-pr": { type: "boolean" },
|
|
99
|
+
"no-epic": { type: "boolean" },
|
|
100
|
+
} satisfies OptionConfig
|
|
101
|
+
|
|
93
102
|
const claimOptions = {
|
|
94
103
|
...outputOptions,
|
|
95
104
|
claimant: { type: "string" },
|
|
@@ -200,8 +209,13 @@ const commands = {
|
|
|
200
209
|
},
|
|
201
210
|
},
|
|
202
211
|
epic: {
|
|
203
|
-
usage: "agency epic <create|list|show>",
|
|
204
|
-
options: {
|
|
212
|
+
usage: "agency epic <create|list|show|update|rename>",
|
|
213
|
+
options: {
|
|
214
|
+
...createOptions,
|
|
215
|
+
...viewOptions,
|
|
216
|
+
...mutationOptions,
|
|
217
|
+
epic: { type: "string" },
|
|
218
|
+
},
|
|
205
219
|
subcommands: {
|
|
206
220
|
create: {
|
|
207
221
|
usage:
|
|
@@ -226,11 +240,38 @@ const commands = {
|
|
|
226
240
|
maxArgs: 1,
|
|
227
241
|
options: ["json", "epic"],
|
|
228
242
|
},
|
|
243
|
+
update: {
|
|
244
|
+
usage: "agency epic update <id> [options] [--json]",
|
|
245
|
+
minArgs: 1,
|
|
246
|
+
maxArgs: 1,
|
|
247
|
+
options: [
|
|
248
|
+
"description",
|
|
249
|
+
"clear-description",
|
|
250
|
+
"ticket-url",
|
|
251
|
+
"repo",
|
|
252
|
+
"json",
|
|
253
|
+
],
|
|
254
|
+
repeatable: ["repo"],
|
|
255
|
+
conflicts: [["description", "clear-description"]],
|
|
256
|
+
},
|
|
257
|
+
rename: {
|
|
258
|
+
usage: "agency epic rename <id> <new-id> [--json]",
|
|
259
|
+
minArgs: 2,
|
|
260
|
+
maxArgs: 2,
|
|
261
|
+
options: ["json"],
|
|
262
|
+
},
|
|
229
263
|
},
|
|
230
264
|
},
|
|
231
265
|
task: {
|
|
232
|
-
usage:
|
|
233
|
-
|
|
266
|
+
usage:
|
|
267
|
+
"agency task <new|create|list|show|status|update|rename|move|dependency>",
|
|
268
|
+
options: {
|
|
269
|
+
...taskCreateOptions,
|
|
270
|
+
...viewOptions,
|
|
271
|
+
...mutationOptions,
|
|
272
|
+
"pr-url": { type: "string" },
|
|
273
|
+
task: { type: "string" },
|
|
274
|
+
},
|
|
234
275
|
subcommands: {
|
|
235
276
|
new: {
|
|
236
277
|
usage: "agency task new [id] [options]",
|
|
@@ -287,13 +328,61 @@ const commands = {
|
|
|
287
328
|
maxArgs: 2,
|
|
288
329
|
options: ["json", "task"],
|
|
289
330
|
},
|
|
331
|
+
update: {
|
|
332
|
+
usage: "agency task update <id> [options] [--json]",
|
|
333
|
+
minArgs: 1,
|
|
334
|
+
maxArgs: 1,
|
|
335
|
+
options: [
|
|
336
|
+
"ticket-url",
|
|
337
|
+
"clear-ticket",
|
|
338
|
+
"description",
|
|
339
|
+
"clear-description",
|
|
340
|
+
"repo",
|
|
341
|
+
"reference",
|
|
342
|
+
"clear-references",
|
|
343
|
+
"branch",
|
|
344
|
+
"base",
|
|
345
|
+
"pr-url",
|
|
346
|
+
"clear-pr",
|
|
347
|
+
"json",
|
|
348
|
+
],
|
|
349
|
+
repeatable: ["reference"],
|
|
350
|
+
conflicts: [
|
|
351
|
+
["ticket-url", "clear-ticket"],
|
|
352
|
+
["description", "clear-description"],
|
|
353
|
+
["reference", "clear-references"],
|
|
354
|
+
["pr-url", "clear-pr"],
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
rename: {
|
|
358
|
+
usage: "agency task rename <id> <new-id> [--json]",
|
|
359
|
+
minArgs: 2,
|
|
360
|
+
maxArgs: 2,
|
|
361
|
+
options: ["json"],
|
|
362
|
+
},
|
|
363
|
+
move: {
|
|
364
|
+
usage: "agency task move <id> (--epic <id> | --no-epic) [--json]",
|
|
365
|
+
minArgs: 1,
|
|
366
|
+
maxArgs: 1,
|
|
367
|
+
options: ["epic", "no-epic", "json"],
|
|
368
|
+
conflicts: [["epic", "no-epic"]],
|
|
369
|
+
},
|
|
370
|
+
dependency: {
|
|
371
|
+
usage:
|
|
372
|
+
"agency task dependency <add|remove> <task-id> <dependency-id> [--json]",
|
|
373
|
+
minArgs: 3,
|
|
374
|
+
maxArgs: 3,
|
|
375
|
+
options: ["json"],
|
|
376
|
+
},
|
|
290
377
|
},
|
|
291
378
|
},
|
|
292
379
|
phase: {
|
|
293
|
-
usage: "agency phase <create|list|show|status>",
|
|
380
|
+
usage: "agency phase <create|list|show|status|update|rename|dependency>",
|
|
294
381
|
options: {
|
|
295
382
|
...phaseCreateOptions,
|
|
296
383
|
...viewOptions,
|
|
384
|
+
...mutationOptions,
|
|
385
|
+
"pr-url": { type: "string" },
|
|
297
386
|
task: { type: "string" },
|
|
298
387
|
phase: { type: "string" },
|
|
299
388
|
},
|
|
@@ -338,6 +427,42 @@ const commands = {
|
|
|
338
427
|
maxArgs: 3,
|
|
339
428
|
options: ["json", "task", "phase"],
|
|
340
429
|
},
|
|
430
|
+
update: {
|
|
431
|
+
usage: "agency phase update <task-id> <phase-id> [options] [--json]",
|
|
432
|
+
minArgs: 2,
|
|
433
|
+
maxArgs: 2,
|
|
434
|
+
options: [
|
|
435
|
+
"description",
|
|
436
|
+
"clear-description",
|
|
437
|
+
"repo",
|
|
438
|
+
"reference",
|
|
439
|
+
"clear-references",
|
|
440
|
+
"branch",
|
|
441
|
+
"base",
|
|
442
|
+
"pr-url",
|
|
443
|
+
"clear-pr",
|
|
444
|
+
"json",
|
|
445
|
+
],
|
|
446
|
+
repeatable: ["reference"],
|
|
447
|
+
conflicts: [
|
|
448
|
+
["description", "clear-description"],
|
|
449
|
+
["reference", "clear-references"],
|
|
450
|
+
["pr-url", "clear-pr"],
|
|
451
|
+
],
|
|
452
|
+
},
|
|
453
|
+
rename: {
|
|
454
|
+
usage: "agency phase rename <task-id> <phase-id> <new-id> [--json]",
|
|
455
|
+
minArgs: 3,
|
|
456
|
+
maxArgs: 3,
|
|
457
|
+
options: ["json"],
|
|
458
|
+
},
|
|
459
|
+
dependency: {
|
|
460
|
+
usage:
|
|
461
|
+
"agency phase dependency <add|remove> <task-id> <phase-id> <dependency-id> [--json]",
|
|
462
|
+
minArgs: 4,
|
|
463
|
+
maxArgs: 4,
|
|
464
|
+
options: ["json"],
|
|
465
|
+
},
|
|
341
466
|
},
|
|
342
467
|
},
|
|
343
468
|
claim: {
|
|
@@ -439,19 +564,21 @@ const commands = {
|
|
|
439
564
|
},
|
|
440
565
|
work: {
|
|
441
566
|
usage:
|
|
442
|
-
"agency work [<directory-or-task-id> | --epic <epic-id>] | agency work prepare [target] [--dry-run] [--json]",
|
|
567
|
+
"agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>] | agency work prepare [target] [--dry-run] [--json]",
|
|
443
568
|
options: {
|
|
444
569
|
...commonOptions,
|
|
445
570
|
...entitySelectorOptions,
|
|
446
571
|
json: { type: "boolean" },
|
|
447
572
|
"dry-run": { type: "boolean" },
|
|
573
|
+
runner: { type: "string" },
|
|
574
|
+
"print-command": { type: "boolean" },
|
|
448
575
|
opencode: { type: "boolean" },
|
|
449
576
|
claude: { type: "boolean" },
|
|
450
577
|
force: { type: "boolean" },
|
|
451
578
|
},
|
|
452
579
|
command: {
|
|
453
580
|
usage:
|
|
454
|
-
"agency work [<directory-or-task-id> | --epic <epic-id>] | agency work prepare [target] [--dry-run] [--json]",
|
|
581
|
+
"agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>] | agency work prepare [target] [--dry-run] [--json]",
|
|
455
582
|
minArgs: 0,
|
|
456
583
|
maxArgs: 2,
|
|
457
584
|
options: [
|
|
@@ -460,12 +587,16 @@ const commands = {
|
|
|
460
587
|
"epic",
|
|
461
588
|
"task",
|
|
462
589
|
"phase",
|
|
590
|
+
"runner",
|
|
591
|
+
"print-command",
|
|
463
592
|
"opencode",
|
|
464
593
|
"claude",
|
|
465
594
|
"force",
|
|
466
595
|
],
|
|
467
596
|
conflicts: [
|
|
468
597
|
["opencode", "claude"],
|
|
598
|
+
["runner", "opencode"],
|
|
599
|
+
["runner", "claude"],
|
|
469
600
|
["epic", "$positional"],
|
|
470
601
|
],
|
|
471
602
|
},
|
|
@@ -971,6 +1102,34 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
971
1102
|
) {
|
|
972
1103
|
validateTaskCreate(parsed.values, spec, subcommand === "create")
|
|
973
1104
|
}
|
|
1105
|
+
if (["task", "phase"].includes(commandName) && subcommand === "dependency") {
|
|
1106
|
+
if (!["add", "remove"].includes(commandPositionals[0] ?? "")) {
|
|
1107
|
+
throw usageError(
|
|
1108
|
+
"Dependency operation must be 'add' or 'remove'.",
|
|
1109
|
+
spec.usage,
|
|
1110
|
+
)
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (commandName === "task" && subcommand === "move") {
|
|
1114
|
+
if (
|
|
1115
|
+
parsed.values.epic === undefined &&
|
|
1116
|
+
parsed.values["no-epic"] === undefined
|
|
1117
|
+
) {
|
|
1118
|
+
throw usageError(
|
|
1119
|
+
"One of '--epic' or '--no-epic' is required.",
|
|
1120
|
+
spec.usage,
|
|
1121
|
+
)
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (
|
|
1125
|
+
["epic", "task", "phase"].includes(commandName) &&
|
|
1126
|
+
subcommand === "update"
|
|
1127
|
+
) {
|
|
1128
|
+
const mutationNames = (spec.options ?? []).filter((name) => name !== "json")
|
|
1129
|
+
if (!mutationNames.some((name) => parsed.values[name] !== undefined)) {
|
|
1130
|
+
throw usageError("At least one update option is required.", spec.usage)
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
974
1133
|
if (commandName === "graph") {
|
|
975
1134
|
validateGraphOptions(parsed.values, spec)
|
|
976
1135
|
}
|
|
@@ -998,6 +1157,8 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
998
1157
|
(parsed.values.epic ||
|
|
999
1158
|
parsed.values.opencode ||
|
|
1000
1159
|
parsed.values.claude ||
|
|
1160
|
+
parsed.values.runner ||
|
|
1161
|
+
parsed.values["print-command"] ||
|
|
1001
1162
|
parsed.values.force))
|
|
1002
1163
|
) {
|
|
1003
1164
|
throw usageError(
|
package/src/cli.test.ts
CHANGED
|
@@ -166,6 +166,68 @@ describe("CLI", () => {
|
|
|
166
166
|
expect(finished.claim).toMatchObject({ state: "finished", outcome: "done" })
|
|
167
167
|
})
|
|
168
168
|
|
|
169
|
+
test("routes graph mutations through structured output", async () => {
|
|
170
|
+
const root = await createTempDir()
|
|
171
|
+
tempDirs.push(root)
|
|
172
|
+
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
173
|
+
await mkdir(join(root, "repos", "agency"), { recursive: true })
|
|
174
|
+
parseJson(
|
|
175
|
+
await runCli(
|
|
176
|
+
[
|
|
177
|
+
"epic",
|
|
178
|
+
"create",
|
|
179
|
+
"delivery",
|
|
180
|
+
"--ticket-url",
|
|
181
|
+
"https://example.com/delivery",
|
|
182
|
+
"--repo",
|
|
183
|
+
"agency:main",
|
|
184
|
+
"--json",
|
|
185
|
+
],
|
|
186
|
+
root,
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
for (const id of ["first", "second"]) {
|
|
190
|
+
parseJson(
|
|
191
|
+
await runCli(
|
|
192
|
+
[
|
|
193
|
+
"task",
|
|
194
|
+
"create",
|
|
195
|
+
id,
|
|
196
|
+
"--repo",
|
|
197
|
+
"agency",
|
|
198
|
+
"--epic",
|
|
199
|
+
"delivery",
|
|
200
|
+
"--json",
|
|
201
|
+
],
|
|
202
|
+
root,
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const updated = parseJson(
|
|
208
|
+
await runCli(
|
|
209
|
+
["task", "update", "second", "--description", "Revised", "--json"],
|
|
210
|
+
root,
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
expect(updated).toMatchObject({
|
|
214
|
+
operation: "task.update",
|
|
215
|
+
entity: { kind: "task", id: "second" },
|
|
216
|
+
validation: { valid: true, scope: ["tasks/second/TASK.md"] },
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const dependency = parseJson(
|
|
220
|
+
await runCli(
|
|
221
|
+
["task", "dependency", "add", "second", "first", "--json"],
|
|
222
|
+
root,
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
expect(dependency).toMatchObject({
|
|
226
|
+
operation: "task.dependency.add",
|
|
227
|
+
changedPaths: ["epics/delivery/EPIC.md"],
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
169
231
|
test("emits one versioned error envelope for usage and command failures", async () => {
|
|
170
232
|
const usage = await runCli(["unknown", "--json"])
|
|
171
233
|
expect(usage.exitCode).toBe(1)
|