@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/README.md
CHANGED
|
@@ -342,8 +342,11 @@ conditions remain visible in `warnings` or `unresolved` with a suggested action.
|
|
|
342
342
|
|
|
343
343
|
```text
|
|
344
344
|
agency init [path] [--json]
|
|
345
|
-
agency workbase add <path> [--json]
|
|
345
|
+
agency workbase add <path> [--name <name>] [--json]
|
|
346
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]
|
|
347
350
|
agency integration status [--json]
|
|
348
351
|
agency integration sync [--json]
|
|
349
352
|
agency repo add <alias> <remote> [--json]
|
|
@@ -353,6 +356,9 @@ agency repo list [--json]
|
|
|
353
356
|
|
|
354
357
|
Registered workbases are stored in
|
|
355
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.
|
|
356
362
|
`repo add` creates a bare clone. `repo link` creates a symlink to an existing Git
|
|
357
363
|
repository. Alias names are then used by all documents and commands.
|
|
358
364
|
|
|
@@ -404,9 +410,17 @@ agency task create <id> --multi-phase
|
|
|
404
410
|
|
|
405
411
|
Agency never prompts when `--no-input` is set or stdin/stderr are not TTYs.
|
|
406
412
|
`--json` also disables prompts and selectors, even when a TTY is available.
|
|
407
|
-
Commands with explicit inputs continue normally.
|
|
408
|
-
|
|
409
|
-
|
|
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.
|
|
410
424
|
|
|
411
425
|
Inspect tasks:
|
|
412
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"
|
|
@@ -72,11 +73,9 @@ const CliLayer = Layer.mergeAll(
|
|
|
72
73
|
/**
|
|
73
74
|
* Run a command Effect with all services provided
|
|
74
75
|
*/
|
|
75
|
-
async function
|
|
76
|
-
effect: Effect.Effect<void, E, any>,
|
|
77
|
-
): Promise<void> {
|
|
76
|
+
async function runEffect<A, E>(effect: Effect.Effect<A, E, any>): Promise<A> {
|
|
78
77
|
const providedEffect = Effect.provide(effect, CliLayer) as Effect.Effect<
|
|
79
|
-
|
|
78
|
+
A,
|
|
80
79
|
E,
|
|
81
80
|
never
|
|
82
81
|
>
|
|
@@ -103,8 +102,44 @@ async function runCommand<E>(
|
|
|
103
102
|
),
|
|
104
103
|
)
|
|
105
104
|
if (Either.isLeft(result)) throw result.left
|
|
105
|
+
return result.right
|
|
106
106
|
}
|
|
107
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
|
+
|
|
108
143
|
// Read version from package.json
|
|
109
144
|
const packageJson = await Bun.file(
|
|
110
145
|
new URL("./package.json", import.meta.url),
|
|
@@ -129,6 +164,7 @@ const commands: Record<string, Command> = {
|
|
|
129
164
|
json: options.json,
|
|
130
165
|
silent: options.silent,
|
|
131
166
|
verbose: options.verbose,
|
|
167
|
+
cwd: options.cwd,
|
|
132
168
|
}),
|
|
133
169
|
)
|
|
134
170
|
},
|
|
@@ -146,6 +182,7 @@ const commands: Record<string, Command> = {
|
|
|
146
182
|
json: options.json,
|
|
147
183
|
silent: options.silent,
|
|
148
184
|
verbose: options.verbose,
|
|
185
|
+
cwd: options.cwd,
|
|
149
186
|
}),
|
|
150
187
|
)
|
|
151
188
|
},
|
|
@@ -164,6 +201,7 @@ const commands: Record<string, Command> = {
|
|
|
164
201
|
json: options.json,
|
|
165
202
|
silent: options.silent,
|
|
166
203
|
verbose: options.verbose,
|
|
204
|
+
cwd: options.cwd,
|
|
167
205
|
}),
|
|
168
206
|
)
|
|
169
207
|
},
|
|
@@ -180,6 +218,7 @@ const commands: Record<string, Command> = {
|
|
|
180
218
|
json: options.json,
|
|
181
219
|
silent: options.silent,
|
|
182
220
|
verbose: options.verbose,
|
|
221
|
+
cwd: options.cwd,
|
|
183
222
|
}),
|
|
184
223
|
)
|
|
185
224
|
},
|
|
@@ -200,6 +239,7 @@ const commands: Record<string, Command> = {
|
|
|
200
239
|
json: options.json,
|
|
201
240
|
silent: options.silent,
|
|
202
241
|
verbose: options.verbose,
|
|
242
|
+
cwd: options.cwd,
|
|
203
243
|
}),
|
|
204
244
|
)
|
|
205
245
|
},
|
|
@@ -220,6 +260,7 @@ const commands: Record<string, Command> = {
|
|
|
220
260
|
json: options.json,
|
|
221
261
|
silent: options.silent,
|
|
222
262
|
verbose: options.verbose,
|
|
263
|
+
cwd: options.cwd,
|
|
223
264
|
}),
|
|
224
265
|
)
|
|
225
266
|
},
|
|
@@ -241,6 +282,7 @@ const commands: Record<string, Command> = {
|
|
|
241
282
|
json: options.json,
|
|
242
283
|
silent: options.silent,
|
|
243
284
|
verbose: options.verbose,
|
|
285
|
+
cwd: options.cwd,
|
|
244
286
|
}),
|
|
245
287
|
)
|
|
246
288
|
},
|
|
@@ -258,6 +300,7 @@ const commands: Record<string, Command> = {
|
|
|
258
300
|
json: options.json,
|
|
259
301
|
silent: options.silent,
|
|
260
302
|
verbose: options.verbose,
|
|
303
|
+
cwd: options.cwd,
|
|
261
304
|
}),
|
|
262
305
|
)
|
|
263
306
|
},
|
|
@@ -273,8 +316,11 @@ const commands: Record<string, Command> = {
|
|
|
273
316
|
subcommand: args[0],
|
|
274
317
|
args: args.slice(1),
|
|
275
318
|
json: options.json,
|
|
319
|
+
name: options.name,
|
|
320
|
+
clear: options.clear,
|
|
276
321
|
silent: options.silent,
|
|
277
322
|
verbose: options.verbose,
|
|
323
|
+
cwd: options.cwd,
|
|
278
324
|
}),
|
|
279
325
|
)
|
|
280
326
|
},
|
|
@@ -291,6 +337,7 @@ const commands: Record<string, Command> = {
|
|
|
291
337
|
json: options.json,
|
|
292
338
|
silent: options.silent,
|
|
293
339
|
verbose: options.verbose,
|
|
340
|
+
cwd: options.cwd,
|
|
294
341
|
}),
|
|
295
342
|
)
|
|
296
343
|
},
|
|
@@ -308,6 +355,7 @@ const commands: Record<string, Command> = {
|
|
|
308
355
|
silent: options.silent,
|
|
309
356
|
verbose: options.verbose,
|
|
310
357
|
json: options.json,
|
|
358
|
+
cwd: options.cwd,
|
|
311
359
|
}),
|
|
312
360
|
)
|
|
313
361
|
},
|
|
@@ -334,6 +382,7 @@ const commands: Record<string, Command> = {
|
|
|
334
382
|
silent: options.silent,
|
|
335
383
|
verbose: options.verbose,
|
|
336
384
|
inputAllowed: options.inputAllowed,
|
|
385
|
+
cwd: options.cwd,
|
|
337
386
|
}),
|
|
338
387
|
)
|
|
339
388
|
},
|
|
@@ -357,6 +406,9 @@ const commands: Record<string, Command> = {
|
|
|
357
406
|
claude: options.claude,
|
|
358
407
|
force: options.force,
|
|
359
408
|
inputAllowed: options.inputAllowed,
|
|
409
|
+
cwd: options.cwd,
|
|
410
|
+
taskId: options.task,
|
|
411
|
+
phaseId: options.phase,
|
|
360
412
|
}),
|
|
361
413
|
)
|
|
362
414
|
},
|
|
@@ -385,6 +437,7 @@ const commands: Record<string, Command> = {
|
|
|
385
437
|
silent: options.silent,
|
|
386
438
|
verbose: options.verbose,
|
|
387
439
|
json: options.json,
|
|
440
|
+
cwd: options.cwd,
|
|
388
441
|
}),
|
|
389
442
|
)
|
|
390
443
|
},
|
|
@@ -402,6 +455,7 @@ const commands: Record<string, Command> = {
|
|
|
402
455
|
verbose: options.verbose,
|
|
403
456
|
json: options.json,
|
|
404
457
|
inputAllowed: options.inputAllowed,
|
|
458
|
+
cwd: options.cwd,
|
|
405
459
|
}),
|
|
406
460
|
)
|
|
407
461
|
},
|
|
@@ -414,11 +468,18 @@ const commands: Record<string, Command> = {
|
|
|
414
468
|
}
|
|
415
469
|
await runCommand(
|
|
416
470
|
context({
|
|
417
|
-
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],
|
|
418
478
|
compact: options.compact,
|
|
419
479
|
json: options.json,
|
|
420
480
|
silent: options.silent,
|
|
421
481
|
verbose: options.verbose,
|
|
482
|
+
cwd: options.cwd,
|
|
422
483
|
}),
|
|
423
484
|
)
|
|
424
485
|
},
|
|
@@ -441,6 +502,7 @@ const commands: Record<string, Command> = {
|
|
|
441
502
|
include: options.include,
|
|
442
503
|
silent: options.silent,
|
|
443
504
|
verbose: options.verbose,
|
|
505
|
+
cwd: options.cwd,
|
|
444
506
|
}),
|
|
445
507
|
)
|
|
446
508
|
},
|
|
@@ -458,6 +520,7 @@ const commands: Record<string, Command> = {
|
|
|
458
520
|
json: options.json,
|
|
459
521
|
silent: options.silent,
|
|
460
522
|
verbose: options.verbose,
|
|
523
|
+
cwd: options.cwd,
|
|
461
524
|
}),
|
|
462
525
|
)
|
|
463
526
|
},
|
|
@@ -497,6 +560,8 @@ Global Options:
|
|
|
497
560
|
-s, --silent Suppress output messages
|
|
498
561
|
-v, --verbose Show verbose output including detailed debugging info
|
|
499
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
|
|
500
565
|
|
|
501
566
|
Examples:
|
|
502
567
|
agency init # Initialize the current directory
|
|
@@ -538,15 +603,16 @@ try {
|
|
|
538
603
|
!values.json &&
|
|
539
604
|
!values["no-input"] &&
|
|
540
605
|
Boolean(process.stdin.isTTY && process.stderr.isTTY)
|
|
606
|
+
const cwd = await resolveInvocationCwd(commandName, values)
|
|
541
607
|
if (values.json || (values.jsonl && values.help)) {
|
|
542
608
|
const result = await collectCommandResult(() =>
|
|
543
|
-
command.run(commandArgs, { ...values, inputAllowed }),
|
|
609
|
+
command.run(commandArgs, { ...values, cwd, inputAllowed }),
|
|
544
610
|
)
|
|
545
611
|
writeEnvelope(successEnvelope(result))
|
|
546
612
|
} else if (values.jsonl) {
|
|
547
|
-
await command.run(commandArgs, { ...values, inputAllowed: false })
|
|
613
|
+
await command.run(commandArgs, { ...values, cwd, inputAllowed: false })
|
|
548
614
|
} else {
|
|
549
|
-
await command.run(commandArgs, { ...values, inputAllowed })
|
|
615
|
+
await command.run(commandArgs, { ...values, cwd, inputAllowed })
|
|
550
616
|
}
|
|
551
617
|
} catch (error) {
|
|
552
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
|
@@ -384,6 +384,80 @@ describe("strict CLI parsing", () => {
|
|
|
384
384
|
).toBe(true)
|
|
385
385
|
})
|
|
386
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
|
+
|
|
387
461
|
test("accepts grouped global short options before a command", () => {
|
|
388
462
|
const parsed = parseCli(["-sh", "task"])
|
|
389
463
|
expect(parsed.values).toMatchObject({ silent: true, help: true })
|