@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.
Files changed (37) hide show
  1. package/README.md +56 -20
  2. package/cli.ts +43 -2
  3. package/package.json +1 -1
  4. package/skills/agency/SKILL.md +41 -5
  5. package/src/cli.test.ts +31 -3
  6. package/src/commands/archive.test.ts +69 -0
  7. package/src/commands/archive.ts +70 -0
  8. package/src/commands/init.test.ts +3 -0
  9. package/src/commands/phase.ts +23 -1
  10. package/src/commands/task-phase.test.ts +46 -1
  11. package/src/commands/task.test.ts +94 -0
  12. package/src/commands/task.ts +160 -17
  13. package/src/commands/validate.test.ts +65 -0
  14. package/src/commands/validate.ts +19 -5
  15. package/src/commands/work.test.ts +100 -34
  16. package/src/commands/work.ts +30 -10
  17. package/src/commands/workbase.test.ts +62 -0
  18. package/src/commands/workbase.ts +58 -0
  19. package/src/services/ArchiveService.test.ts +334 -0
  20. package/src/services/ArchiveService.ts +246 -0
  21. package/src/services/PhaseService.ts +28 -0
  22. package/src/services/TaskPhaseService.test.ts +79 -0
  23. package/src/services/TaskService.ts +31 -1
  24. package/src/services/WorkbaseService.test.ts +99 -1
  25. package/src/services/WorkbaseService.ts +101 -2
  26. package/src/services/WorktreeService.test.ts +150 -1
  27. package/src/services/WorktreeService.ts +123 -1
  28. package/src/test-utils.ts +2 -0
  29. package/src/utils/progress.test.ts +37 -0
  30. package/src/utils/progress.ts +36 -0
  31. package/src/workbase/AGENTS.md +3 -0
  32. package/src/workbase/opencode-file.ts +37 -0
  33. package/src/workbase/schemas.test.ts +114 -0
  34. package/src/workbase/schemas.ts +18 -2
  35. package/src/workbase/work-target.test.ts +16 -9
  36. package/src/workbase/work-target.ts +37 -6
  37. 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` during initialization and ensures it exists whenever
73
- the workbase is discovered. A checksum in the generated file lets newer Agency
74
- versions refresh unmodified instructions while preserving custom or edited
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 create refresh-copy \
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> --ticket-url <url> --repo <alias>
273
- --branch <name> --base <name>
274
- [--description <text>] [--epic <id>] [--reference <alias>:<ref>...] [--json]
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> --ticket-url <url> --multi-phase
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`. If `fzf` is
326
- not installed, Agency prints the hierarchy and asks for an explicit target.
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 (_args: string[], options: Record<string, any>) => {
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 Validate the current workbase
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -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(args: string[], cwd = projectRoot): Promise<CliResult> {
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(await runCli(["validate", "--json"], root))
306
+ const validation = parseJson(
307
+ await runCli(["validate", root, "--json"], parent),
308
+ )
281
309
  expect(validation).toEqual({
282
- root: workbaseRoot,
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 () => {
@@ -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("Subcommand is required. Available: create, list, show"),
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