@markjaquith/agency 2.5.0 → 2.7.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 +21 -14
- package/cli.ts +28 -6
- package/package.json +1 -1
- package/skills/agency/SKILL.md +18 -9
- package/src/cli.test.ts +30 -3
- package/src/commands/init.test.ts +3 -0
- package/src/commands/phase.ts +1 -1
- package/src/commands/task-phase.test.ts +4 -4
- package/src/commands/task.ts +1 -1
- package/src/commands/validate.test.ts +65 -0
- package/src/commands/validate.ts +19 -5
- package/src/commands/work.test.ts +65 -7
- package/src/commands/work.ts +39 -10
- package/src/commands/workbase.test.ts +62 -0
- package/src/commands/workbase.ts +58 -0
- package/src/services/TaskPhaseService.test.ts +3 -3
- package/src/services/WorkbaseService.test.ts +35 -3
- package/src/services/WorkbaseService.ts +73 -6
- package/src/services/WorktreeService.test.ts +45 -0
- package/src/services/WorktreeService.ts +10 -6
- package/src/workbase/opencode-file.ts +25 -16
- package/src/workbase/schemas.test.ts +74 -10
- package/src/workbase/schemas.ts +13 -1
- package/src/workbase/work-target.test.ts +7 -0
- package/src/workbase/work-target.ts +1 -0
- package/src/workbase/workbase-choice.ts +71 -0
package/README.md
CHANGED
|
@@ -230,7 +230,7 @@ agency repo link backend ~/Dev/backend
|
|
|
230
230
|
agency task new
|
|
231
231
|
|
|
232
232
|
agency validate
|
|
233
|
-
agency work refresh-copy
|
|
233
|
+
agency work tasks/refresh-copy
|
|
234
234
|
agency pr create refresh-copy
|
|
235
235
|
```
|
|
236
236
|
|
|
@@ -240,11 +240,15 @@ agency pr create refresh-copy
|
|
|
240
240
|
|
|
241
241
|
```text
|
|
242
242
|
agency init [path] [--json]
|
|
243
|
+
agency workbase add <path> [--json]
|
|
244
|
+
agency workbase list [--json]
|
|
243
245
|
agency repo add <alias> <remote> [--json]
|
|
244
246
|
agency repo link <alias> <path> [--json]
|
|
245
247
|
agency repo list [--json]
|
|
246
248
|
```
|
|
247
249
|
|
|
250
|
+
Registered workbases are stored in
|
|
251
|
+
`$XDG_CONFIG_HOME/agency/workbases.json` (or `~/.config/agency/workbases.json`).
|
|
248
252
|
`repo add` creates a bare clone. `repo link` creates a symlink to an existing Git
|
|
249
253
|
repository. Alias names are then used by all documents and commands.
|
|
250
254
|
|
|
@@ -294,7 +298,7 @@ Inspect tasks:
|
|
|
294
298
|
```text
|
|
295
299
|
agency task list [--json]
|
|
296
300
|
agency task show <id> [--json]
|
|
297
|
-
agency task status <id> <open|working|done|dropped> [--json]
|
|
301
|
+
agency task status <id> <open|working|delegated|done|dropped> [--json]
|
|
298
302
|
```
|
|
299
303
|
|
|
300
304
|
To add a phase to an existing single-phase task, name the phase that will own
|
|
@@ -321,14 +325,14 @@ agency phase create <task-id> <phase-id>
|
|
|
321
325
|
|
|
322
326
|
agency phase list <task-id> [--json]
|
|
323
327
|
agency phase show <task-id> <phase-id> [--json]
|
|
324
|
-
agency phase status <task-id> <phase-id> <open|working|done|dropped> [--json]
|
|
328
|
+
agency phase status <task-id> <phase-id> <open|working|delegated|done|dropped> [--json]
|
|
325
329
|
```
|
|
326
330
|
|
|
327
331
|
Single-phase tasks and phases store status in YAML. New execution units start
|
|
328
332
|
`open`, and `agency work` marks the selected execution unit `working` immediately
|
|
329
|
-
before launch. Use the status subcommands to mark work `
|
|
330
|
-
it again. The interactive work selector displays status
|
|
331
|
-
units.
|
|
333
|
+
before launch. Use the status subcommands to mark work `delegated`, `done`,
|
|
334
|
+
`dropped`, or open it again. The interactive work selector displays status
|
|
335
|
+
markers before execution units.
|
|
332
336
|
|
|
333
337
|
### Archive
|
|
334
338
|
|
|
@@ -346,19 +350,21 @@ before moving files, refuses dirty worktrees, and preserves branches.
|
|
|
346
350
|
### Work and Pull Requests
|
|
347
351
|
|
|
348
352
|
```text
|
|
349
|
-
agency work [<
|
|
353
|
+
agency work [<directory> | --epic <epic-id>] [--opencode | --claude]
|
|
350
354
|
agency pr create <task-id> [phase-id] [--draft] [--json]
|
|
351
355
|
```
|
|
352
356
|
|
|
353
|
-
`agency work`
|
|
354
|
-
|
|
355
|
-
|
|
357
|
+
`agency work` presents the full hierarchy in `fzf`. Pass a directory, including
|
|
358
|
+
`.` for the current directory, to infer its epic, task, or phase. Outside a
|
|
359
|
+
workbase, Agency first presents the registered workbases, then the selected
|
|
360
|
+
workbase's hierarchy. If `fzf` is not installed, Agency prints the available
|
|
361
|
+
choices and asks for an explicit directory.
|
|
356
362
|
|
|
357
363
|
Epic and multi-phase task targets launch orchestration agents beside their
|
|
358
364
|
documents. Single-phase tasks and phases fetch repositories, create or reuse
|
|
359
365
|
worktrees under `code/`, and launch an execution agent in the writable checkout
|
|
360
|
-
with absolute context paths.
|
|
361
|
-
|
|
366
|
+
with absolute context paths. An explicit directory or `--epic` target bypasses
|
|
367
|
+
the hierarchy chooser.
|
|
362
368
|
|
|
363
369
|
Each writable `(repo, branch)` pair may belong to only one task or phase. Agency
|
|
364
370
|
validation reports duplicate ownership, and `agency work` checks Git's worktree
|
|
@@ -380,13 +386,14 @@ the owning `TASK.md` or `PHASE.md`.
|
|
|
380
386
|
|
|
381
387
|
```text
|
|
382
388
|
agency status [--json]
|
|
383
|
-
agency validate [--json]
|
|
389
|
+
agency validate [path] [--json]
|
|
384
390
|
```
|
|
385
391
|
|
|
386
392
|
Validation checks JSON and YAML parsing, Effect Schema conformance, repository
|
|
387
393
|
aliases, parent/child backlinks, phase directories, duplicate references,
|
|
388
394
|
unknown dependencies, and dependency cycles. YAML duplicate keys, anchors,
|
|
389
|
-
aliases, and custom tags are rejected.
|
|
395
|
+
aliases, and custom tags are rejected. When path is omitted outside a workbase,
|
|
396
|
+
Agency prompts for a registered workbase.
|
|
390
397
|
|
|
391
398
|
## Agent Skill
|
|
392
399
|
|
package/cli.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { repo, help as repoHelp } from "./src/commands/repo"
|
|
|
12
12
|
import { epic, help as epicHelp } from "./src/commands/epic"
|
|
13
13
|
import { phase, help as phaseHelp } from "./src/commands/phase"
|
|
14
14
|
import { archive, help as archiveHelp } from "./src/commands/archive"
|
|
15
|
+
import { workbase, help as workbaseHelp } from "./src/commands/workbase"
|
|
15
16
|
import type { Command } from "./src/types"
|
|
16
17
|
import { FileSystemService } from "./src/services/FileSystemService"
|
|
17
18
|
import { WorkbaseService } from "./src/services/WorkbaseService"
|
|
@@ -173,6 +174,23 @@ const commands: Record<string, Command> = {
|
|
|
173
174
|
)
|
|
174
175
|
},
|
|
175
176
|
},
|
|
177
|
+
workbase: {
|
|
178
|
+
run: async (args: string[], options: Record<string, any>) => {
|
|
179
|
+
if (options.help) {
|
|
180
|
+
console.log(workbaseHelp)
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
await runCommand(
|
|
184
|
+
workbase({
|
|
185
|
+
subcommand: args[0],
|
|
186
|
+
args: args.slice(1),
|
|
187
|
+
json: options.json,
|
|
188
|
+
silent: options.silent,
|
|
189
|
+
verbose: options.verbose,
|
|
190
|
+
}),
|
|
191
|
+
)
|
|
192
|
+
},
|
|
193
|
+
},
|
|
176
194
|
repo: {
|
|
177
195
|
run: async (args: string[], options: Record<string, any>) => {
|
|
178
196
|
if (options.help) {
|
|
@@ -221,11 +239,13 @@ const commands: Record<string, Command> = {
|
|
|
221
239
|
console.log(workHelp)
|
|
222
240
|
return
|
|
223
241
|
}
|
|
242
|
+
if (args.length > 1) {
|
|
243
|
+
throw new Error("Usage: agency work [<directory> | --epic <epic-id>]")
|
|
244
|
+
}
|
|
224
245
|
|
|
225
246
|
await runCommand(
|
|
226
247
|
work({
|
|
227
|
-
|
|
228
|
-
phaseId: args[1],
|
|
248
|
+
directory: args[0],
|
|
229
249
|
epicId: options.epic,
|
|
230
250
|
silent: options.silent,
|
|
231
251
|
verbose: options.verbose,
|
|
@@ -251,13 +271,14 @@ const commands: Record<string, Command> = {
|
|
|
251
271
|
},
|
|
252
272
|
},
|
|
253
273
|
validate: {
|
|
254
|
-
run: async (
|
|
274
|
+
run: async (args: string[], options: Record<string, any>) => {
|
|
255
275
|
if (options.help) {
|
|
256
276
|
console.log(validateHelp)
|
|
257
277
|
return
|
|
258
278
|
}
|
|
259
279
|
await runCommand(
|
|
260
280
|
validate({
|
|
281
|
+
path: args[0],
|
|
261
282
|
silent: options.silent,
|
|
262
283
|
verbose: options.verbose,
|
|
263
284
|
json: options.json,
|
|
@@ -275,15 +296,16 @@ Usage: agency <command> [options]
|
|
|
275
296
|
|
|
276
297
|
Commands:
|
|
277
298
|
init [path] Initialize an Agency workbase
|
|
299
|
+
workbase <subcommand> Manage registered workbases
|
|
278
300
|
epic <subcommand> Manage epics
|
|
279
301
|
phase <subcommand> Manage task phases
|
|
280
302
|
archive <type> Archive a work item
|
|
281
303
|
task <subcommand> Manage tasks
|
|
282
|
-
work [
|
|
304
|
+
work [directory] Work on an epic, task, or phase
|
|
283
305
|
pr create Create a pull request for an execution unit
|
|
284
306
|
repo <subcommand> Manage workbase repositories
|
|
285
307
|
status Show status for the current workbase
|
|
286
|
-
validate
|
|
308
|
+
validate [path] Validate a workbase
|
|
287
309
|
|
|
288
310
|
Global Options:
|
|
289
311
|
-h, --help Show help for a command
|
|
@@ -294,7 +316,7 @@ Global Options:
|
|
|
294
316
|
Examples:
|
|
295
317
|
agency init # Initialize the current directory
|
|
296
318
|
agency task list # List tasks
|
|
297
|
-
agency work refresh-cli-copy
|
|
319
|
+
agency work tasks/refresh-cli-copy # Start working on a task
|
|
298
320
|
|
|
299
321
|
For more information about a command, run:
|
|
300
322
|
agency <command> --help
|
package/package.json
CHANGED
package/skills/agency/SKILL.md
CHANGED
|
@@ -58,6 +58,13 @@ If no workbase is found, do not initialize one without user intent. When asked:
|
|
|
58
58
|
agency init [path]
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
Register known workbases so `agency work` can select one when run elsewhere:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
agency workbase add <path>
|
|
65
|
+
agency workbase list
|
|
66
|
+
```
|
|
67
|
+
|
|
61
68
|
## Repository Aliases
|
|
62
69
|
|
|
63
70
|
Add a remote as an Agency-managed bare repository:
|
|
@@ -195,8 +202,8 @@ output, including mutations, entity inspection, status, validation, and PR creat
|
|
|
195
202
|
- Multi-phase task frontmatter owns the phase dependency graph.
|
|
196
203
|
- Epic frontmatter owns the child-task dependency graph.
|
|
197
204
|
- `pr` is either a GitHub PR URL string or `null`.
|
|
198
|
-
- Execution-unit `status` is `open`, `working`, `done`, or `dropped`.
|
|
199
|
-
starts open, and `agency work` marks it working before agent launch.
|
|
205
|
+
- Execution-unit `status` is `open`, `working`, `delegated`, `done`, or `dropped`.
|
|
206
|
+
New work starts open, and `agency work` marks it working before agent launch.
|
|
200
207
|
- Keep directory IDs stable; encode sequencing with `dependsOn`, not numeric
|
|
201
208
|
directory prefixes.
|
|
202
209
|
- Do not use YAML duplicate keys, anchors, aliases, or custom tags.
|
|
@@ -207,8 +214,8 @@ prose, preserve backlinks and run validation immediately afterward.
|
|
|
207
214
|
Update execution status with:
|
|
208
215
|
|
|
209
216
|
```bash
|
|
210
|
-
agency task status <task-id> <open|working|done|dropped>
|
|
211
|
-
agency phase status <task-id> <phase-id> <open|working|done|dropped>
|
|
217
|
+
agency task status <task-id> <open|working|delegated|done|dropped>
|
|
218
|
+
agency phase status <task-id> <phase-id> <open|working|delegated|done|dropped>
|
|
212
219
|
```
|
|
213
220
|
|
|
214
221
|
## Archive Completed Work
|
|
@@ -226,27 +233,29 @@ branches. It refuses dirty worktrees and active sibling dependencies.
|
|
|
226
233
|
## Validate Every Structural Change
|
|
227
234
|
|
|
228
235
|
```bash
|
|
229
|
-
agency validate
|
|
236
|
+
agency validate [path]
|
|
230
237
|
```
|
|
231
238
|
|
|
232
239
|
Use `--json` when diagnostics will be consumed programmatically. Resolve all
|
|
233
240
|
validation errors before materializing worktrees or creating PRs. Validation
|
|
234
241
|
checks schemas, aliases, backlinks, phase directories, duplicate references,
|
|
235
242
|
duplicate writable branch ownership, unknown dependencies, and dependency cycles.
|
|
243
|
+
Outside a workbase, omitting path opens the registered-workbase picker.
|
|
236
244
|
|
|
237
245
|
## Worktrees And Agent Launch
|
|
238
246
|
|
|
239
247
|
```bash
|
|
240
248
|
agency work
|
|
241
|
-
agency work <
|
|
249
|
+
agency work <directory>
|
|
242
250
|
agency work --epic <epic-id>
|
|
243
251
|
```
|
|
244
252
|
|
|
245
253
|
Use `--opencode` or `--claude` to require a specific agent. This command fetches
|
|
246
254
|
repositories for execution targets, creates or reuses their worktrees, and
|
|
247
|
-
replaces the current process with the selected agent.
|
|
248
|
-
|
|
249
|
-
|
|
255
|
+
replaces the current process with the selected agent. With no directory it opens
|
|
256
|
+
an `fzf` picker containing the workbase hierarchy. Pass `.`, or another
|
|
257
|
+
directory, to infer the nearest epic, task, or phase.
|
|
258
|
+
Outside a workbase, it first opens a picker containing registered workbases.
|
|
250
259
|
|
|
251
260
|
Epic and multi-phase task targets are orchestration sessions launched beside
|
|
252
261
|
their documents. Single-phase tasks and phases are execution sessions launched
|
package/src/cli.test.ts
CHANGED
|
@@ -12,9 +12,14 @@ interface CliResult {
|
|
|
12
12
|
stderr: string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
async function runCli(
|
|
15
|
+
async function runCli(
|
|
16
|
+
args: string[],
|
|
17
|
+
cwd = projectRoot,
|
|
18
|
+
env?: Record<string, string>,
|
|
19
|
+
): Promise<CliResult> {
|
|
16
20
|
const subprocess = Bun.spawn([process.execPath, cliPath, ...args], {
|
|
17
21
|
cwd,
|
|
22
|
+
env: env ? { ...process.env, ...env } : undefined,
|
|
18
23
|
stdout: "pipe",
|
|
19
24
|
stderr: "pipe",
|
|
20
25
|
})
|
|
@@ -76,6 +81,7 @@ describe("CLI", () => {
|
|
|
76
81
|
test("routes command help and global options on either side of commands", async () => {
|
|
77
82
|
for (const [command, usage] of [
|
|
78
83
|
["init", "Usage: agency init"],
|
|
84
|
+
["workbase", "Usage: agency workbase"],
|
|
79
85
|
["repo", "Usage: agency repo"],
|
|
80
86
|
["epic", "Usage: agency epic"],
|
|
81
87
|
["task", "Usage: agency task"],
|
|
@@ -106,6 +112,25 @@ describe("CLI", () => {
|
|
|
106
112
|
expect(after).toEqual({ exitCode: 0, stdout: "", stderr: "" })
|
|
107
113
|
})
|
|
108
114
|
|
|
115
|
+
test("registers and lists workbases", async () => {
|
|
116
|
+
const parent = await createTempDir()
|
|
117
|
+
tempDirs.push(parent)
|
|
118
|
+
const root = join(parent, "workbase")
|
|
119
|
+
const env = { XDG_CONFIG_HOME: join(parent, "config") }
|
|
120
|
+
|
|
121
|
+
expect(
|
|
122
|
+
parseJson(await runCli(["init", root, "--json"], parent, env)),
|
|
123
|
+
).toEqual({
|
|
124
|
+
root,
|
|
125
|
+
})
|
|
126
|
+
expect(
|
|
127
|
+
parseJson(await runCli(["workbase", "add", root, "--json"], parent, env)),
|
|
128
|
+
).toEqual({ path: await realpath(root) })
|
|
129
|
+
expect(
|
|
130
|
+
parseJson(await runCli(["workbase", "list", "--json"], parent, env)),
|
|
131
|
+
).toEqual([await realpath(root)])
|
|
132
|
+
})
|
|
133
|
+
|
|
109
134
|
test("runs a multi-phase domain workflow through subprocesses", async () => {
|
|
110
135
|
const parent = await createTempDir()
|
|
111
136
|
tempDirs.push(parent)
|
|
@@ -278,9 +303,11 @@ describe("CLI", () => {
|
|
|
278
303
|
issues: [],
|
|
279
304
|
})
|
|
280
305
|
|
|
281
|
-
const validation = parseJson(
|
|
306
|
+
const validation = parseJson(
|
|
307
|
+
await runCli(["validate", root, "--json"], parent),
|
|
308
|
+
)
|
|
282
309
|
expect(validation).toEqual({
|
|
283
|
-
root
|
|
310
|
+
root,
|
|
284
311
|
issues: [],
|
|
285
312
|
epicCount: 1,
|
|
286
313
|
taskCount: 1,
|
|
@@ -39,6 +39,9 @@ describe("init command", () => {
|
|
|
39
39
|
expect(
|
|
40
40
|
await Bun.file(join(root, ".opencode/opencode.jsonc")).text(),
|
|
41
41
|
).toContain('"path": "../tasks"')
|
|
42
|
+
expect(
|
|
43
|
+
await Bun.file(join(root, ".opencode/opencode.jsonc")).text(),
|
|
44
|
+
).toContain(`"${join(root, "tasks")}/*": "allow"`)
|
|
42
45
|
})
|
|
43
46
|
|
|
44
47
|
test("preserves existing gitignore entries", async () => {
|
package/src/commands/phase.ts
CHANGED
|
@@ -125,7 +125,7 @@ Subcommands:
|
|
|
125
125
|
list <task> List task phases
|
|
126
126
|
show <task> <phase> Show a phase
|
|
127
127
|
status <task> <phase> <status>
|
|
128
|
-
Set open, working, done, or dropped
|
|
128
|
+
Set open, working, delegated, done, or dropped
|
|
129
129
|
|
|
130
130
|
Create options:
|
|
131
131
|
--description <text> Short description of the phase
|
|
@@ -172,13 +172,13 @@ describe("task and phase command JSON output", () => {
|
|
|
172
172
|
runTestEffect(
|
|
173
173
|
phase({
|
|
174
174
|
subcommand: "status",
|
|
175
|
-
args: ["multi", "first", "
|
|
175
|
+
args: ["multi", "first", "delegated"],
|
|
176
176
|
cwd: root,
|
|
177
177
|
json: true,
|
|
178
178
|
}),
|
|
179
179
|
),
|
|
180
180
|
)
|
|
181
|
-
expect(JSON.parse(phaseLogs[0]!).data.status).toBe("
|
|
181
|
+
expect(JSON.parse(phaseLogs[0]!).data.status).toBe("delegated")
|
|
182
182
|
|
|
183
183
|
await runTestEffect(
|
|
184
184
|
task({
|
|
@@ -196,13 +196,13 @@ describe("task and phase command JSON output", () => {
|
|
|
196
196
|
runTestEffect(
|
|
197
197
|
task({
|
|
198
198
|
subcommand: "status",
|
|
199
|
-
args: ["single-status", "
|
|
199
|
+
args: ["single-status", "delegated"],
|
|
200
200
|
cwd: root,
|
|
201
201
|
json: true,
|
|
202
202
|
}),
|
|
203
203
|
),
|
|
204
204
|
)
|
|
205
|
-
expect(JSON.parse(taskLogs[0]!).data.status).toBe("
|
|
205
|
+
expect(JSON.parse(taskLogs[0]!).data.status).toBe("delegated")
|
|
206
206
|
})
|
|
207
207
|
|
|
208
208
|
test("converts a single-phase task with an explicit first phase ID", async () => {
|
package/src/commands/task.ts
CHANGED
|
@@ -237,7 +237,7 @@ Subcommands:
|
|
|
237
237
|
create <id> Create a task; omitted metadata uses defaults
|
|
238
238
|
list List tasks
|
|
239
239
|
show <id> Show a task
|
|
240
|
-
status <id> <status> Set open, working, done, or dropped
|
|
240
|
+
status <id> <status> Set open, working, delegated, done, or dropped
|
|
241
241
|
|
|
242
242
|
Create options:
|
|
243
243
|
--ticket-url <url> External ticket URL (optional)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { Effect } from "effect"
|
|
2
3
|
import { mkdir } from "node:fs/promises"
|
|
3
4
|
import { join } from "node:path"
|
|
4
5
|
import {
|
|
@@ -7,6 +8,9 @@ import {
|
|
|
7
8
|
createTempDir,
|
|
8
9
|
runTestEffect,
|
|
9
10
|
} from "../test-utils"
|
|
11
|
+
import { FileSystemService } from "../services/FileSystemService"
|
|
12
|
+
import { WorkbaseService } from "../services/WorkbaseService"
|
|
13
|
+
import type { PickWorkbase } from "../workbase/workbase-choice"
|
|
10
14
|
import { validate } from "./validate"
|
|
11
15
|
|
|
12
16
|
describe("validate command", () => {
|
|
@@ -43,6 +47,67 @@ pr: null
|
|
|
43
47
|
).resolves.toBeUndefined()
|
|
44
48
|
})
|
|
45
49
|
|
|
50
|
+
test("validates an explicit workbase path", async () => {
|
|
51
|
+
await Bun.write(
|
|
52
|
+
join(root, "tasks/example/TASK.md"),
|
|
53
|
+
`---
|
|
54
|
+
ticketUrl: null
|
|
55
|
+
repo: agency
|
|
56
|
+
branch: task/example
|
|
57
|
+
base: main
|
|
58
|
+
pr: null
|
|
59
|
+
---
|
|
60
|
+
`,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
await expect(
|
|
64
|
+
runTestEffect(
|
|
65
|
+
validate({ path: root, cwd: join(root, "outside"), silent: true }),
|
|
66
|
+
),
|
|
67
|
+
).resolves.toBeUndefined()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test("selects a registered workbase when local discovery fails", async () => {
|
|
71
|
+
const discovered: string[] = []
|
|
72
|
+
const workbase = {
|
|
73
|
+
discover: (path: string) => {
|
|
74
|
+
discovered.push(path)
|
|
75
|
+
return path === "/outside"
|
|
76
|
+
? Effect.fail({
|
|
77
|
+
_tag: "WorkbaseNotFoundError" as const,
|
|
78
|
+
message: "No Agency workbase found from /outside",
|
|
79
|
+
})
|
|
80
|
+
: Effect.succeed(path)
|
|
81
|
+
},
|
|
82
|
+
listRegistered: () => Effect.succeed(["/first", "/selected"]),
|
|
83
|
+
validate: (path: string) =>
|
|
84
|
+
Effect.succeed({
|
|
85
|
+
root: path,
|
|
86
|
+
issues: [],
|
|
87
|
+
epicCount: 0,
|
|
88
|
+
taskCount: 0,
|
|
89
|
+
phaseCount: 0,
|
|
90
|
+
valid: true,
|
|
91
|
+
}),
|
|
92
|
+
}
|
|
93
|
+
const fs = {
|
|
94
|
+
runCommand: () => Effect.succeed({ exitCode: 0, stdout: "", stderr: "" }),
|
|
95
|
+
}
|
|
96
|
+
const pick: PickWorkbase = (workbases) => {
|
|
97
|
+
expect(workbases).toEqual(["/first", "/selected"])
|
|
98
|
+
return Effect.succeed("/selected")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await Effect.runPromise(
|
|
102
|
+
validate({ cwd: "/outside", silent: true }, pick).pipe(
|
|
103
|
+
Effect.provideService(WorkbaseService, workbase as never),
|
|
104
|
+
Effect.provideService(FileSystemService, fs as never),
|
|
105
|
+
) as Effect.Effect<void, unknown, never>,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
expect(discovered).toEqual(["/outside", "/selected"])
|
|
109
|
+
})
|
|
110
|
+
|
|
46
111
|
test("outputs the validation report as JSON", async () => {
|
|
47
112
|
await Bun.write(
|
|
48
113
|
join(root, "tasks/example/TASK.md"),
|
package/src/commands/validate.ts
CHANGED
|
@@ -2,8 +2,14 @@ import { Data, Effect } from "effect"
|
|
|
2
2
|
import type { BaseCommandOptions } from "../utils/command"
|
|
3
3
|
import { WorkbaseService } from "../services/WorkbaseService"
|
|
4
4
|
import { createLoggers } from "../utils/effect"
|
|
5
|
+
import {
|
|
6
|
+
pickWorkbase,
|
|
7
|
+
resolveWorkbase,
|
|
8
|
+
type PickWorkbase,
|
|
9
|
+
} from "../workbase/workbase-choice"
|
|
5
10
|
|
|
6
11
|
interface ValidateOptions extends BaseCommandOptions {
|
|
12
|
+
readonly path?: string
|
|
7
13
|
readonly json?: boolean
|
|
8
14
|
}
|
|
9
15
|
|
|
@@ -11,11 +17,19 @@ class ValidationFailedError extends Data.TaggedError("ValidationFailedError")<{
|
|
|
11
17
|
readonly message: string
|
|
12
18
|
}> {}
|
|
13
19
|
|
|
14
|
-
export const validate = (
|
|
20
|
+
export const validate = (
|
|
21
|
+
options: ValidateOptions = {},
|
|
22
|
+
pick: PickWorkbase = pickWorkbase,
|
|
23
|
+
) =>
|
|
15
24
|
Effect.gen(function* () {
|
|
16
25
|
const workbase = yield* WorkbaseService
|
|
17
26
|
const { log } = createLoggers(options)
|
|
18
|
-
const
|
|
27
|
+
const startPath = options.path ?? options.cwd ?? process.cwd()
|
|
28
|
+
const root = options.path
|
|
29
|
+
? yield* workbase.discover(startPath)
|
|
30
|
+
: yield* resolveWorkbase(startPath, log, pick)
|
|
31
|
+
if (!root) return
|
|
32
|
+
const report = yield* workbase.validate(root)
|
|
19
33
|
|
|
20
34
|
if (options.json) {
|
|
21
35
|
log(JSON.stringify(report, null, 2))
|
|
@@ -40,10 +54,10 @@ export const validate = (options: ValidateOptions = {}) =>
|
|
|
40
54
|
})
|
|
41
55
|
|
|
42
56
|
export const help = `
|
|
43
|
-
Usage: agency validate [options]
|
|
57
|
+
Usage: agency validate [path] [options]
|
|
44
58
|
|
|
45
|
-
Validate
|
|
46
|
-
|
|
59
|
+
Validate a workbase's configuration, frontmatter, references, and dependency
|
|
60
|
+
graphs. The current or selected registered workbase is used when path is omitted.
|
|
47
61
|
|
|
48
62
|
Options:
|
|
49
63
|
--json Output the validation report as JSON
|