@markjaquith/agency 2.4.0 → 2.6.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 +56 -20
- package/cli.ts +43 -2
- package/package.json +1 -1
- package/skills/agency/SKILL.md +41 -5
- package/src/cli.test.ts +31 -3
- package/src/commands/archive.test.ts +69 -0
- package/src/commands/archive.ts +70 -0
- package/src/commands/init.test.ts +3 -0
- package/src/commands/phase.ts +23 -1
- package/src/commands/task-phase.test.ts +46 -1
- package/src/commands/task.test.ts +94 -0
- package/src/commands/task.ts +160 -17
- package/src/commands/validate.test.ts +65 -0
- package/src/commands/validate.ts +19 -5
- package/src/commands/work.test.ts +100 -34
- package/src/commands/work.ts +30 -10
- package/src/commands/workbase.test.ts +62 -0
- package/src/commands/workbase.ts +58 -0
- package/src/services/ArchiveService.test.ts +334 -0
- package/src/services/ArchiveService.ts +246 -0
- package/src/services/PhaseService.ts +28 -0
- package/src/services/TaskPhaseService.test.ts +79 -0
- package/src/services/TaskService.ts +31 -1
- package/src/services/WorkbaseService.test.ts +99 -1
- package/src/services/WorkbaseService.ts +101 -2
- package/src/services/WorktreeService.test.ts +150 -1
- package/src/services/WorktreeService.ts +123 -1
- package/src/test-utils.ts +2 -0
- package/src/utils/progress.test.ts +37 -0
- package/src/utils/progress.ts +36 -0
- package/src/workbase/AGENTS.md +3 -0
- package/src/workbase/opencode-file.ts +37 -0
- package/src/workbase/schemas.test.ts +114 -0
- package/src/workbase/schemas.ts +18 -2
- package/src/workbase/work-target.test.ts +16 -9
- package/src/workbase/work-target.ts +37 -6
- package/src/workbase/workbase-choice.ts +71 -0
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ frontmatter; prose below it supplies human and agent context.
|
|
|
43
43
|
```text
|
|
44
44
|
workbase/
|
|
45
45
|
AGENTS.md # managed workbase instructions
|
|
46
|
+
.opencode/
|
|
47
|
+
opencode.jsonc # managed task and epic references
|
|
46
48
|
agency.json
|
|
47
49
|
repos/
|
|
48
50
|
frontend/ # bare Git repository or symlink
|
|
@@ -69,10 +71,11 @@ workbase/
|
|
|
69
71
|
backend/
|
|
70
72
|
```
|
|
71
73
|
|
|
72
|
-
Agency creates `AGENTS.md`
|
|
73
|
-
the workbase is discovered.
|
|
74
|
-
|
|
75
|
-
files
|
|
74
|
+
Agency creates `AGENTS.md` and `.opencode/opencode.jsonc` during initialization
|
|
75
|
+
and ensures they exist whenever the workbase is discovered. The OpenCode config
|
|
76
|
+
grants external-directory access to task and epic references. Checksums in the
|
|
77
|
+
generated files let newer Agency versions refresh unmodified content while
|
|
78
|
+
preserving custom or edited files.
|
|
76
79
|
|
|
77
80
|
Repository metadata comes directly from Git under `repos/{alias}`. Workbase
|
|
78
81
|
configuration may provide a custom writable-worktree creation command.
|
|
@@ -177,6 +180,7 @@ repos:
|
|
|
177
180
|
branch: task/refresh-copy
|
|
178
181
|
base: main
|
|
179
182
|
pr: null
|
|
183
|
+
status: open
|
|
180
184
|
---
|
|
181
185
|
```
|
|
182
186
|
|
|
@@ -207,6 +211,7 @@ repos:
|
|
|
207
211
|
branch: task/checkout-ui
|
|
208
212
|
base: task/checkout-api
|
|
209
213
|
pr: null
|
|
214
|
+
status: open
|
|
210
215
|
---
|
|
211
216
|
```
|
|
212
217
|
|
|
@@ -222,13 +227,7 @@ cd ~/work
|
|
|
222
227
|
agency repo add frontend git@github.com:example/frontend.git
|
|
223
228
|
agency repo link backend ~/Dev/backend
|
|
224
229
|
|
|
225
|
-
agency task
|
|
226
|
-
--ticket-url https://example.com/tickets/refresh-copy \
|
|
227
|
-
--description "Refresh user-facing checkout copy" \
|
|
228
|
-
--repo frontend \
|
|
229
|
-
--reference backend:main \
|
|
230
|
-
--branch task/refresh-copy \
|
|
231
|
-
--base main
|
|
230
|
+
agency task new
|
|
232
231
|
|
|
233
232
|
agency validate
|
|
234
233
|
agency work refresh-copy
|
|
@@ -241,11 +240,15 @@ agency pr create refresh-copy
|
|
|
241
240
|
|
|
242
241
|
```text
|
|
243
242
|
agency init [path] [--json]
|
|
243
|
+
agency workbase add <path> [--json]
|
|
244
|
+
agency workbase list [--json]
|
|
244
245
|
agency repo add <alias> <remote> [--json]
|
|
245
246
|
agency repo link <alias> <path> [--json]
|
|
246
247
|
agency repo list [--json]
|
|
247
248
|
```
|
|
248
249
|
|
|
250
|
+
Registered workbases are stored in
|
|
251
|
+
`$XDG_CONFIG_HOME/agency/workbases.json` (or `~/.config/agency/workbases.json`).
|
|
249
252
|
`repo add` creates a bare clone. `repo link` creates a symlink to an existing Git
|
|
250
253
|
repository. Alias names are then used by all documents and commands.
|
|
251
254
|
|
|
@@ -266,19 +269,28 @@ back-reference.
|
|
|
266
269
|
|
|
267
270
|
### Tasks
|
|
268
271
|
|
|
272
|
+
Create a task interactively. Text prompts identify optional values, and known
|
|
273
|
+
choices use fzf:
|
|
274
|
+
|
|
275
|
+
```text
|
|
276
|
+
agency task new [id]
|
|
277
|
+
```
|
|
278
|
+
|
|
269
279
|
Create a single-phase task:
|
|
270
280
|
|
|
271
281
|
```text
|
|
272
|
-
agency task create <id> --
|
|
273
|
-
--
|
|
274
|
-
[--
|
|
282
|
+
agency task create <id> --repo <alias>
|
|
283
|
+
[--ticket-url <url>] [--description <text>] [--epic <id>]
|
|
284
|
+
[--reference <alias>:<ref>...] [--branch <name>] [--base <name>] [--json]
|
|
275
285
|
```
|
|
276
286
|
|
|
287
|
+
The branch defaults to `task/<id>` and the base defaults to `main`.
|
|
288
|
+
|
|
277
289
|
Create a multi-phase task container:
|
|
278
290
|
|
|
279
291
|
```text
|
|
280
|
-
agency task create <id> --
|
|
281
|
-
[--description <text>] [--epic <id>] [--json]
|
|
292
|
+
agency task create <id> --multi-phase
|
|
293
|
+
[--ticket-url <url>] [--description <text>] [--epic <id>] [--json]
|
|
282
294
|
```
|
|
283
295
|
|
|
284
296
|
Inspect tasks:
|
|
@@ -286,6 +298,7 @@ Inspect tasks:
|
|
|
286
298
|
```text
|
|
287
299
|
agency task list [--json]
|
|
288
300
|
agency task show <id> [--json]
|
|
301
|
+
agency task status <id> <open|working|delegated|done|dropped> [--json]
|
|
289
302
|
```
|
|
290
303
|
|
|
291
304
|
To add a phase to an existing single-phase task, name the phase that will own
|
|
@@ -312,8 +325,28 @@ agency phase create <task-id> <phase-id>
|
|
|
312
325
|
|
|
313
326
|
agency phase list <task-id> [--json]
|
|
314
327
|
agency phase show <task-id> <phase-id> [--json]
|
|
328
|
+
agency phase status <task-id> <phase-id> <open|working|delegated|done|dropped> [--json]
|
|
315
329
|
```
|
|
316
330
|
|
|
331
|
+
Single-phase tasks and phases store status in YAML. New execution units start
|
|
332
|
+
`open`, and `agency work` marks the selected execution unit `working` immediately
|
|
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.
|
|
336
|
+
|
|
337
|
+
### Archive
|
|
338
|
+
|
|
339
|
+
```text
|
|
340
|
+
agency archive epic <epic-id> [--json]
|
|
341
|
+
agency archive task <task-id> [--json]
|
|
342
|
+
agency archive phase <task-id> <phase-id> [--json]
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Archived work keeps its hierarchy under `archive/`. Epic archiving includes its
|
|
346
|
+
listed tasks. Task and phase archiving update the active parent document and
|
|
347
|
+
reject items that active siblings depend on. Agency removes registered worktrees
|
|
348
|
+
before moving files, refuses dirty worktrees, and preserves branches.
|
|
349
|
+
|
|
317
350
|
### Work and Pull Requests
|
|
318
351
|
|
|
319
352
|
```text
|
|
@@ -322,8 +355,10 @@ agency pr create <task-id> [phase-id] [--draft] [--json]
|
|
|
322
355
|
```
|
|
323
356
|
|
|
324
357
|
`agency work` infers an epic, task, or phase from the current directory. From
|
|
325
|
-
elsewhere in the workbase, it presents the full hierarchy in `fzf`.
|
|
326
|
-
|
|
358
|
+
elsewhere in the workbase, it presents the full hierarchy in `fzf`. Outside a
|
|
359
|
+
workbase, it 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 target.
|
|
327
362
|
|
|
328
363
|
Epic and multi-phase task targets launch orchestration agents beside their
|
|
329
364
|
documents. Single-phase tasks and phases fetch repositories, create or reuse
|
|
@@ -351,13 +386,14 @@ the owning `TASK.md` or `PHASE.md`.
|
|
|
351
386
|
|
|
352
387
|
```text
|
|
353
388
|
agency status [--json]
|
|
354
|
-
agency validate [--json]
|
|
389
|
+
agency validate [path] [--json]
|
|
355
390
|
```
|
|
356
391
|
|
|
357
392
|
Validation checks JSON and YAML parsing, Effect Schema conformance, repository
|
|
358
393
|
aliases, parent/child backlinks, phase directories, duplicate references,
|
|
359
394
|
unknown dependencies, and dependency cycles. YAML duplicate keys, anchors,
|
|
360
|
-
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.
|
|
361
397
|
|
|
362
398
|
## Agent Skill
|
|
363
399
|
|
package/cli.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { validate, help as validateHelp } from "./src/commands/validate"
|
|
|
11
11
|
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
|
+
import { archive, help as archiveHelp } from "./src/commands/archive"
|
|
15
|
+
import { workbase, help as workbaseHelp } from "./src/commands/workbase"
|
|
14
16
|
import type { Command } from "./src/types"
|
|
15
17
|
import { FileSystemService } from "./src/services/FileSystemService"
|
|
16
18
|
import { WorkbaseService } from "./src/services/WorkbaseService"
|
|
@@ -20,6 +22,7 @@ import { TaskService } from "./src/services/TaskService"
|
|
|
20
22
|
import { PhaseService } from "./src/services/PhaseService"
|
|
21
23
|
import { WorktreeService } from "./src/services/WorktreeService"
|
|
22
24
|
import { PullRequestService } from "./src/services/PullRequestService"
|
|
25
|
+
import { ArchiveService } from "./src/services/ArchiveService"
|
|
23
26
|
|
|
24
27
|
// Create CLI layer with all services
|
|
25
28
|
const CliLayer = Layer.mergeAll(
|
|
@@ -31,6 +34,7 @@ const CliLayer = Layer.mergeAll(
|
|
|
31
34
|
PhaseService.Default,
|
|
32
35
|
WorktreeService.Default,
|
|
33
36
|
PullRequestService.Default,
|
|
37
|
+
ArchiveService.Default,
|
|
34
38
|
)
|
|
35
39
|
|
|
36
40
|
/**
|
|
@@ -153,6 +157,40 @@ const commands: Record<string, Command> = {
|
|
|
153
157
|
)
|
|
154
158
|
},
|
|
155
159
|
},
|
|
160
|
+
archive: {
|
|
161
|
+
run: async (args: string[], options: Record<string, any>) => {
|
|
162
|
+
if (options.help) {
|
|
163
|
+
console.log(archiveHelp)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
await runCommand(
|
|
167
|
+
archive({
|
|
168
|
+
type: args[0],
|
|
169
|
+
args: args.slice(1),
|
|
170
|
+
json: options.json,
|
|
171
|
+
silent: options.silent,
|
|
172
|
+
verbose: options.verbose,
|
|
173
|
+
}),
|
|
174
|
+
)
|
|
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
|
+
},
|
|
156
194
|
repo: {
|
|
157
195
|
run: async (args: string[], options: Record<string, any>) => {
|
|
158
196
|
if (options.help) {
|
|
@@ -231,13 +269,14 @@ const commands: Record<string, Command> = {
|
|
|
231
269
|
},
|
|
232
270
|
},
|
|
233
271
|
validate: {
|
|
234
|
-
run: async (
|
|
272
|
+
run: async (args: string[], options: Record<string, any>) => {
|
|
235
273
|
if (options.help) {
|
|
236
274
|
console.log(validateHelp)
|
|
237
275
|
return
|
|
238
276
|
}
|
|
239
277
|
await runCommand(
|
|
240
278
|
validate({
|
|
279
|
+
path: args[0],
|
|
241
280
|
silent: options.silent,
|
|
242
281
|
verbose: options.verbose,
|
|
243
282
|
json: options.json,
|
|
@@ -255,14 +294,16 @@ Usage: agency <command> [options]
|
|
|
255
294
|
|
|
256
295
|
Commands:
|
|
257
296
|
init [path] Initialize an Agency workbase
|
|
297
|
+
workbase <subcommand> Manage registered workbases
|
|
258
298
|
epic <subcommand> Manage epics
|
|
259
299
|
phase <subcommand> Manage task phases
|
|
300
|
+
archive <type> Archive a work item
|
|
260
301
|
task <subcommand> Manage tasks
|
|
261
302
|
work [task] [phase] Work on an epic, task, or phase
|
|
262
303
|
pr create Create a pull request for an execution unit
|
|
263
304
|
repo <subcommand> Manage workbase repositories
|
|
264
305
|
status Show status for the current workbase
|
|
265
|
-
validate
|
|
306
|
+
validate [path] Validate a workbase
|
|
266
307
|
|
|
267
308
|
Global Options:
|
|
268
309
|
-h, --help Show help for a command
|
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:
|
|
@@ -100,17 +107,22 @@ Epic task ordering and dependencies live in `EPIC.md`. Creating a task with
|
|
|
100
107
|
|
|
101
108
|
## Create Single-Phase Tasks
|
|
102
109
|
|
|
110
|
+
For guided creation, run `agency task new`. It prompts for text input, uses fzf
|
|
111
|
+
for known choices, and allows optional inputs to be skipped.
|
|
112
|
+
|
|
103
113
|
```bash
|
|
104
114
|
agency task create <id> \
|
|
105
|
-
--ticket-url <url> \
|
|
115
|
+
[--ticket-url <url>] \
|
|
106
116
|
[--description <text>] \
|
|
107
117
|
[--epic <epic-id>] \
|
|
108
118
|
--repo <writable-alias> \
|
|
109
119
|
[--reference <read-only-alias>:<ref>] \
|
|
110
|
-
--branch <branch> \
|
|
111
|
-
--base <base>
|
|
120
|
+
[--branch <branch>] \
|
|
121
|
+
[--base <base>]
|
|
112
122
|
```
|
|
113
123
|
|
|
124
|
+
The branch defaults to `task/<id>` and the base defaults to `main`.
|
|
125
|
+
|
|
114
126
|
The task itself is the execution unit. Its worktrees live under
|
|
115
127
|
`tasks/{id}/code/{alias}`.
|
|
116
128
|
|
|
@@ -120,7 +132,7 @@ Create the task container:
|
|
|
120
132
|
|
|
121
133
|
```bash
|
|
122
134
|
agency task create <id> \
|
|
123
|
-
--ticket-url <url> \
|
|
135
|
+
[--ticket-url <url>] \
|
|
124
136
|
[--description <text>] \
|
|
125
137
|
[--epic <epic-id>] \
|
|
126
138
|
--multi-phase
|
|
@@ -170,6 +182,7 @@ repos:
|
|
|
170
182
|
branch: task/example
|
|
171
183
|
base: main
|
|
172
184
|
pr: null
|
|
185
|
+
status: open
|
|
173
186
|
---
|
|
174
187
|
```
|
|
175
188
|
|
|
@@ -189,6 +202,8 @@ output, including mutations, entity inspection, status, validation, and PR creat
|
|
|
189
202
|
- Multi-phase task frontmatter owns the phase dependency graph.
|
|
190
203
|
- Epic frontmatter owns the child-task dependency graph.
|
|
191
204
|
- `pr` is either a GitHub PR URL string or `null`.
|
|
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.
|
|
192
207
|
- Keep directory IDs stable; encode sequencing with `dependsOn`, not numeric
|
|
193
208
|
directory prefixes.
|
|
194
209
|
- Do not use YAML duplicate keys, anchors, aliases, or custom tags.
|
|
@@ -196,16 +211,36 @@ output, including mutations, entity inspection, status, validation, and PR creat
|
|
|
196
211
|
Prefer Agency commands for creation. When manually editing dependencies or
|
|
197
212
|
prose, preserve backlinks and run validation immediately afterward.
|
|
198
213
|
|
|
214
|
+
Update execution status with:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
agency task status <task-id> <open|working|delegated|done|dropped>
|
|
218
|
+
agency phase status <task-id> <phase-id> <open|working|delegated|done|dropped>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Archive Completed Work
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
agency archive epic <epic-id>
|
|
225
|
+
agency archive task <task-id>
|
|
226
|
+
agency archive phase <task-id> <phase-id>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Use these commands instead of moving work item folders manually. Agency mirrors
|
|
230
|
+
their hierarchy under `archive/`, removes registered worktrees first, and keeps
|
|
231
|
+
branches. It refuses dirty worktrees and active sibling dependencies.
|
|
232
|
+
|
|
199
233
|
## Validate Every Structural Change
|
|
200
234
|
|
|
201
235
|
```bash
|
|
202
|
-
agency validate
|
|
236
|
+
agency validate [path]
|
|
203
237
|
```
|
|
204
238
|
|
|
205
239
|
Use `--json` when diagnostics will be consumed programmatically. Resolve all
|
|
206
240
|
validation errors before materializing worktrees or creating PRs. Validation
|
|
207
241
|
checks schemas, aliases, backlinks, phase directories, duplicate references,
|
|
208
242
|
duplicate writable branch ownership, unknown dependencies, and dependency cycles.
|
|
243
|
+
Outside a workbase, omitting path opens the registered-workbase picker.
|
|
209
244
|
|
|
210
245
|
## Worktrees And Agent Launch
|
|
211
246
|
|
|
@@ -220,6 +255,7 @@ repositories for execution targets, creates or reuses their worktrees, and
|
|
|
220
255
|
replaces the current process with the selected agent. It infers the nearest
|
|
221
256
|
epic, task, or phase from the current directory; otherwise it opens an `fzf`
|
|
222
257
|
picker containing the workbase hierarchy.
|
|
258
|
+
Outside a workbase, it first opens a picker containing registered workbases.
|
|
223
259
|
|
|
224
260
|
Epic and multi-phase task targets are orchestration sessions launched beside
|
|
225
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,10 +81,12 @@ 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"],
|
|
82
88
|
["phase", "Usage: agency phase"],
|
|
89
|
+
["archive", "Usage: agency archive"],
|
|
83
90
|
["work", "Usage: agency work"],
|
|
84
91
|
["pr", "Usage: agency pr"],
|
|
85
92
|
["status", "Usage: agency status"],
|
|
@@ -105,6 +112,25 @@ describe("CLI", () => {
|
|
|
105
112
|
expect(after).toEqual({ exitCode: 0, stdout: "", stderr: "" })
|
|
106
113
|
})
|
|
107
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
|
+
|
|
108
134
|
test("runs a multi-phase domain workflow through subprocesses", async () => {
|
|
109
135
|
const parent = await createTempDir()
|
|
110
136
|
tempDirs.push(parent)
|
|
@@ -277,9 +303,11 @@ describe("CLI", () => {
|
|
|
277
303
|
issues: [],
|
|
278
304
|
})
|
|
279
305
|
|
|
280
|
-
const validation = parseJson(
|
|
306
|
+
const validation = parseJson(
|
|
307
|
+
await runCli(["validate", root, "--json"], parent),
|
|
308
|
+
)
|
|
281
309
|
expect(validation).toEqual({
|
|
282
|
-
root
|
|
310
|
+
root,
|
|
283
311
|
issues: [],
|
|
284
312
|
epicCount: 1,
|
|
285
313
|
taskCount: 1,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import {
|
|
4
|
+
captureLogs,
|
|
5
|
+
cleanupTempDir,
|
|
6
|
+
createTempDir,
|
|
7
|
+
runTestEffect,
|
|
8
|
+
} from "../test-utils"
|
|
9
|
+
import { archive } from "./archive"
|
|
10
|
+
import { task } from "./task"
|
|
11
|
+
|
|
12
|
+
describe("archive command", () => {
|
|
13
|
+
let root: string
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
root = await createTempDir()
|
|
17
|
+
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
18
|
+
const initialized = Bun.spawnSync([
|
|
19
|
+
"git",
|
|
20
|
+
"init",
|
|
21
|
+
"--bare",
|
|
22
|
+
join(root, "repos/agency"),
|
|
23
|
+
])
|
|
24
|
+
if (initialized.exitCode !== 0) {
|
|
25
|
+
throw new Error(new TextDecoder().decode(initialized.stderr))
|
|
26
|
+
}
|
|
27
|
+
await runTestEffect(
|
|
28
|
+
task({
|
|
29
|
+
subcommand: "create",
|
|
30
|
+
args: ["example"],
|
|
31
|
+
ticketUrl: "https://example.com/task",
|
|
32
|
+
repo: "agency",
|
|
33
|
+
branch: "task/example",
|
|
34
|
+
base: "main",
|
|
35
|
+
cwd: root,
|
|
36
|
+
silent: true,
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
afterEach(async () => cleanupTempDir(root))
|
|
42
|
+
|
|
43
|
+
test("archives a task and outputs the result as JSON", async () => {
|
|
44
|
+
const logs = await captureLogs(() =>
|
|
45
|
+
runTestEffect(
|
|
46
|
+
archive({
|
|
47
|
+
type: "task",
|
|
48
|
+
args: ["example"],
|
|
49
|
+
cwd: root,
|
|
50
|
+
json: true,
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
expect(JSON.parse(logs[0]!)).toEqual({
|
|
56
|
+
kind: "task",
|
|
57
|
+
id: "example",
|
|
58
|
+
path: join(root, "archive/tasks/example"),
|
|
59
|
+
archivedPaths: [join(root, "archive/tasks/example")],
|
|
60
|
+
removedWorktrees: [],
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test("requires a supported work item type", async () => {
|
|
65
|
+
await expect(
|
|
66
|
+
runTestEffect(archive({ args: [], cwd: root, silent: true })),
|
|
67
|
+
).rejects.toThrow("Available: epic, task, phase")
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { ArchiveService } from "../services/ArchiveService"
|
|
3
|
+
import type { BaseCommandOptions } from "../utils/command"
|
|
4
|
+
import { createLoggers } from "../utils/effect"
|
|
5
|
+
|
|
6
|
+
interface ArchiveOptions extends BaseCommandOptions {
|
|
7
|
+
readonly type?: string
|
|
8
|
+
readonly args: readonly string[]
|
|
9
|
+
readonly json?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const archive = (options: ArchiveOptions) =>
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const archives = yield* ArchiveService
|
|
15
|
+
const { log } = createLoggers(options)
|
|
16
|
+
const cwd = options.cwd ?? process.cwd()
|
|
17
|
+
const [id, phaseId] = options.args
|
|
18
|
+
|
|
19
|
+
let result
|
|
20
|
+
switch (options.type) {
|
|
21
|
+
case "epic":
|
|
22
|
+
if (!id) {
|
|
23
|
+
return yield* Effect.fail(
|
|
24
|
+
new Error("Usage: agency archive epic <epic-id>"),
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
result = yield* archives.archiveEpic(id, cwd)
|
|
28
|
+
break
|
|
29
|
+
case "task":
|
|
30
|
+
if (!id) {
|
|
31
|
+
return yield* Effect.fail(
|
|
32
|
+
new Error("Usage: agency archive task <task-id>"),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
result = yield* archives.archiveTask(id, cwd)
|
|
36
|
+
break
|
|
37
|
+
case "phase":
|
|
38
|
+
if (!id || !phaseId) {
|
|
39
|
+
return yield* Effect.fail(
|
|
40
|
+
new Error("Usage: agency archive phase <task-id> <phase-id>"),
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
result = yield* archives.archivePhase(id, phaseId, cwd)
|
|
44
|
+
break
|
|
45
|
+
default:
|
|
46
|
+
return yield* Effect.fail(
|
|
47
|
+
new Error("Work item type is required. Available: epic, task, phase"),
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
log(
|
|
52
|
+
options.json
|
|
53
|
+
? JSON.stringify(result, null, 2)
|
|
54
|
+
: `Archived ${result.kind} '${result.id}' to ${result.path}`,
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export const help = `
|
|
59
|
+
Usage: agency archive <type> <id>
|
|
60
|
+
|
|
61
|
+
Archive a work item after removing its worktrees. Branches are preserved.
|
|
62
|
+
|
|
63
|
+
Types:
|
|
64
|
+
epic <epic-id> Archive an epic and its tasks
|
|
65
|
+
task <task-id> Archive a task
|
|
66
|
+
phase <task-id> <phase-id> Archive a phase
|
|
67
|
+
|
|
68
|
+
Options:
|
|
69
|
+
--json Output results as JSON
|
|
70
|
+
`
|
|
@@ -36,6 +36,9 @@ describe("init command", () => {
|
|
|
36
36
|
expect(await Bun.file(join(root, "AGENTS.md")).text()).toContain(
|
|
37
37
|
"# Agency Workbase",
|
|
38
38
|
)
|
|
39
|
+
expect(
|
|
40
|
+
await Bun.file(join(root, ".opencode/opencode.jsonc")).text(),
|
|
41
|
+
).toContain('"path": "../tasks"')
|
|
39
42
|
})
|
|
40
43
|
|
|
41
44
|
test("preserves existing gitignore entries", async () => {
|
package/src/commands/phase.ts
CHANGED
|
@@ -90,9 +90,29 @@ export const phase = (options: PhaseOptions) =>
|
|
|
90
90
|
)
|
|
91
91
|
return
|
|
92
92
|
}
|
|
93
|
+
case "status": {
|
|
94
|
+
const status = options.args[2]
|
|
95
|
+
if (!taskId || !phaseId || !status) {
|
|
96
|
+
return yield* Effect.fail(
|
|
97
|
+
new Error(
|
|
98
|
+
"Usage: agency phase status <task-id> <phase-id> <status>",
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
const record = yield* phases.setStatus(taskId, phaseId, status, cwd)
|
|
103
|
+
const { content: _, ...output } = record
|
|
104
|
+
log(
|
|
105
|
+
options.json
|
|
106
|
+
? JSON.stringify(output, null, 2)
|
|
107
|
+
: `Marked phase '${phaseId}' as ${record.data.status}`,
|
|
108
|
+
)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
93
111
|
default:
|
|
94
112
|
return yield* Effect.fail(
|
|
95
|
-
new Error(
|
|
113
|
+
new Error(
|
|
114
|
+
"Subcommand is required. Available: create, list, show, status",
|
|
115
|
+
),
|
|
96
116
|
)
|
|
97
117
|
}
|
|
98
118
|
})
|
|
@@ -104,6 +124,8 @@ Subcommands:
|
|
|
104
124
|
create <task> <phase> Create a phase
|
|
105
125
|
list <task> List task phases
|
|
106
126
|
show <task> <phase> Show a phase
|
|
127
|
+
status <task> <phase> <status>
|
|
128
|
+
Set open, working, delegated, done, or dropped
|
|
107
129
|
|
|
108
130
|
Create options:
|
|
109
131
|
--description <text> Short description of the phase
|