@markjaquith/agency 2.22.0 → 2.24.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 CHANGED
@@ -400,6 +400,8 @@ repository. Alias names are then used by all documents and commands.
400
400
  Commands that print Agency-owned results accept `--json`, including initialization,
401
401
  integration inspection/sync, repository mutations, entity creation/list/show,
402
402
  status, validation, graph export, reconciliation, and PR creation.
403
+ Entity create, list, and show results include a stable SHA-256 `revision` of the
404
+ complete Markdown document.
403
405
 
404
406
  ### Epics
405
407
 
@@ -515,6 +517,13 @@ references as one rollback-capable mutation and refuse when a materialized
515
517
  worktree would make the directory move unsafe. Mutation JSON includes changed
516
518
  paths and the focused validation scope.
517
519
 
520
+ Epic, task, and phase update, rename, move, and dependency mutations accept
521
+ `--if-revision <hash>`. The option is optional for interactive human use. When
522
+ provided, Agency fails with a structured `REVISION_CONFLICT` containing the
523
+ expected and current revisions if the target changed. Multi-document mutations
524
+ also recheck every affected document after taking the mutation lock and before
525
+ writing anything.
526
+
518
527
  Single-phase tasks and phases store status in YAML. New execution units start
519
528
  `open`, and `agency work` marks the selected execution unit `working` immediately
520
529
  before launch. Use claims for coordinated ownership and the status subcommands
@@ -565,15 +574,24 @@ runner environment documented above for a later release or finish operation.
565
574
  ### Archive
566
575
 
567
576
  ```text
568
- agency archive epic <epic-id> [--json]
569
- agency archive task <task-id> [--json]
570
- agency archive phase <task-id> <phase-id> [--json]
577
+ agency archive list [--kind <kind>] [--status <status>] [--repository <alias>]
578
+ agency archive show <epic|task> <id>
579
+ agency archive show phase <task-id> <phase-id>
580
+ agency archive epic <epic-id> [--dry-run] [--json]
581
+ agency archive task <task-id> [--dry-run] [--json]
582
+ agency archive phase <task-id> <phase-id> [--dry-run] [--json]
583
+ agency restore epic <epic-id> [--dry-run] [--json]
584
+ agency restore task <task-id> [--dry-run] [--json]
585
+ agency restore phase <task-id> <phase-id> [--dry-run] [--json]
571
586
  ```
572
587
 
573
588
  Archived work keeps its hierarchy under `archive/`. Epic archiving includes its
574
589
  listed tasks. Task and phase archiving update the active parent document and
575
590
  reject items that active siblings depend on. Agency removes registered worktrees
576
- before moving files, refuses dirty worktrees, and preserves branches.
591
+ before moving files, refuses dirty worktrees, and preserves branches. Archive and
592
+ restore preflight all destinations and graph references before changing files.
593
+ Versioned lifecycle provenance preserves parent declarations and dependency edges
594
+ for restoration. Archived IDs are reserved until restored.
577
595
 
578
596
  ### Work and Pull Requests
579
597
 
package/cli.ts CHANGED
@@ -17,6 +17,7 @@ import { repo, help as repoHelp } from "./src/commands/repo"
17
17
  import { epic, help as epicHelp } from "./src/commands/epic"
18
18
  import { phase, help as phaseHelp } from "./src/commands/phase"
19
19
  import { archive, help as archiveHelp } from "./src/commands/archive"
20
+ import { restore, help as restoreHelp } from "./src/commands/restore"
20
21
  import { workbase, help as workbaseHelp } from "./src/commands/workbase"
21
22
  import {
22
23
  integration,
@@ -238,6 +239,7 @@ const commands: Record<string, Command> = {
238
239
  ticketUrl: options["ticket-url"],
239
240
  description: options.description,
240
241
  clearDescription: options["clear-description"],
242
+ ifRevision: options["if-revision"],
241
243
  repos: options.repo,
242
244
  json: options.json,
243
245
  statuses: options.status,
@@ -289,6 +291,7 @@ const commands: Record<string, Command> = {
289
291
  clearReferences: options["clear-references"],
290
292
  prUrl: options["pr-url"],
291
293
  clearPr: options["clear-pr"],
294
+ ifRevision: options["if-revision"],
292
295
  dependsOn: options["depends-on"],
293
296
  firstPhase: options["first-phase"],
294
297
  json: options.json,
@@ -315,6 +318,29 @@ const commands: Record<string, Command> = {
315
318
  type: args[0],
316
319
  args: args.slice(1),
317
320
  json: options.json,
321
+ dryRun: options["dry-run"],
322
+ kinds: options.kind,
323
+ statuses: options.status,
324
+ repositories: options.repository,
325
+ silent: options.silent,
326
+ verbose: options.verbose,
327
+ cwd: options.cwd,
328
+ }),
329
+ )
330
+ },
331
+ },
332
+ restore: {
333
+ run: async (args: string[], options: Record<string, any>) => {
334
+ if (options.help) {
335
+ console.log(restoreHelp)
336
+ return
337
+ }
338
+ await runCommand(
339
+ restore({
340
+ type: args[0],
341
+ args: args.slice(1),
342
+ json: options.json,
343
+ dryRun: options["dry-run"],
318
344
  silent: options.silent,
319
345
  verbose: options.verbose,
320
346
  cwd: options.cwd,
@@ -399,6 +425,7 @@ const commands: Record<string, Command> = {
399
425
  clearReferences: options["clear-references"],
400
426
  prUrl: options["pr-url"],
401
427
  clearPr: options["clear-pr"],
428
+ ifRevision: options["if-revision"],
402
429
  noEpic: options["no-epic"],
403
430
  multiPhase: options["multi-phase"],
404
431
  json: options.json,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.22.0",
3
+ "version": "2.24.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -248,14 +248,22 @@ agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outco
248
248
  ## Archive Completed Work
249
249
 
250
250
  ```bash
251
+ agency archive list
252
+ agency archive show <epic|task> <id>
251
253
  agency archive epic <epic-id>
252
254
  agency archive task <task-id>
253
255
  agency archive phase <task-id> <phase-id>
256
+ agency restore epic <epic-id>
257
+ agency restore task <task-id>
258
+ agency restore phase <task-id> <phase-id>
254
259
  ```
255
260
 
256
261
  Use these commands instead of moving work item folders manually. Agency mirrors
257
262
  their hierarchy under `archive/`, removes registered worktrees first, and keeps
258
- branches. It refuses dirty worktrees and active sibling dependencies.
263
+ branches. It refuses dirty worktrees and active sibling dependencies. Use
264
+ `--dry-run` to preflight archive or restore. Restore uses lifecycle provenance to
265
+ recreate parent declarations and rejects ID, backlink, dependency, and destination
266
+ conflicts. Archived IDs remain reserved until restored.
259
267
 
260
268
  ## Validate Every Structural Change
261
269
 
@@ -75,6 +75,27 @@ describe("strict CLI parsing", () => {
75
75
  reference: ["two:main", "three:main"],
76
76
  "depends-on": ["first", "second"],
77
77
  })
78
+ expect(
79
+ parseCli([
80
+ "archive",
81
+ "list",
82
+ "--kind",
83
+ "task",
84
+ "--kind",
85
+ "phase",
86
+ "--status",
87
+ "done",
88
+ "--repository",
89
+ "agency",
90
+ ]).values,
91
+ ).toMatchObject({
92
+ kind: ["task", "phase"],
93
+ status: ["done"],
94
+ repository: ["agency"],
95
+ })
96
+ expect(
97
+ parseCli(["restore", "task", "example", "--dry-run"]).values,
98
+ ).toMatchObject({ "dry-run": true })
78
99
  })
79
100
 
80
101
  test("parses composable view filters", () => {
@@ -110,6 +131,9 @@ describe("strict CLI parsing", () => {
110
131
  expect(() => parseCli(["status", "--pr", "--no-pr"])).toThrow(
111
132
  "cannot be combined",
112
133
  )
134
+ expect(() => parseCli(["archive", "list", "--status", "invalid"])).toThrow(
135
+ "Invalid '--status' value",
136
+ )
113
137
  })
114
138
 
115
139
  test("enforces exact maximum positional arity for every leaf command", () => {
@@ -170,6 +194,14 @@ describe("strict CLI parsing", () => {
170
194
  [["archive", "epic", "one", "two"], "agency archive epic"],
171
195
  [["archive", "task", "one", "two"], "agency archive task"],
172
196
  [["archive", "phase", "one", "two", "three"], "agency archive phase"],
197
+ [["archive", "list", "extra"], "agency archive list"],
198
+ [
199
+ ["archive", "show", "task", "one", "extra", "more"],
200
+ "agency archive show",
201
+ ],
202
+ [["restore", "epic", "one", "two"], "agency restore epic"],
203
+ [["restore", "task", "one", "two"], "agency restore task"],
204
+ [["restore", "phase", "one", "two", "three"], "agency restore phase"],
173
205
  [["work", "one", "two"], "agency work"],
174
206
  [["pr", "create", "one", "two", "three"], "agency pr create"],
175
207
  [["status", "extra"], "agency status"],
@@ -296,6 +328,31 @@ describe("strict CLI parsing", () => {
296
328
  ).toThrow("must be 'done' or 'dropped'")
297
329
  })
298
330
 
331
+ test("accepts valid mutation revisions and rejects malformed hashes", () => {
332
+ const revision = "a".repeat(64)
333
+ expect(
334
+ parseCli([
335
+ "task",
336
+ "move",
337
+ "example",
338
+ "--no-epic",
339
+ "--if-revision",
340
+ revision,
341
+ ]),
342
+ ).toMatchObject({ values: { "if-revision": revision } })
343
+ expect(() =>
344
+ parseCli([
345
+ "phase",
346
+ "rename",
347
+ "task",
348
+ "phase",
349
+ "renamed",
350
+ "--if-revision",
351
+ "stale",
352
+ ]),
353
+ ).toThrow("64-character SHA-256 hash")
354
+ })
355
+
299
356
  test("accepts context projections and keeps compact command-local", () => {
300
357
  expect(parseCli(["context", ".", "--json", "--compact"])).toMatchObject({
301
358
  commandName: "context",
package/src/cli-parser.ts CHANGED
@@ -91,6 +91,7 @@ const phaseCreateOptions = {
91
91
  } satisfies OptionConfig
92
92
 
93
93
  const mutationOptions = {
94
+ "if-revision": { type: "string" },
94
95
  "clear-description": { type: "boolean" },
95
96
  "clear-ticket": { type: "boolean" },
96
97
  "clear-references": { type: "boolean" },
@@ -249,6 +250,7 @@ const commands = {
249
250
  "clear-description",
250
251
  "ticket-url",
251
252
  "repo",
253
+ "if-revision",
252
254
  "json",
253
255
  ],
254
256
  repeatable: ["repo"],
@@ -258,7 +260,7 @@ const commands = {
258
260
  usage: "agency epic rename <id> <new-id> [--json]",
259
261
  minArgs: 2,
260
262
  maxArgs: 2,
261
- options: ["json"],
263
+ options: ["if-revision", "json"],
262
264
  },
263
265
  },
264
266
  },
@@ -344,6 +346,7 @@ const commands = {
344
346
  "base",
345
347
  "pr-url",
346
348
  "clear-pr",
349
+ "if-revision",
347
350
  "json",
348
351
  ],
349
352
  repeatable: ["reference"],
@@ -358,13 +361,13 @@ const commands = {
358
361
  usage: "agency task rename <id> <new-id> [--json]",
359
362
  minArgs: 2,
360
363
  maxArgs: 2,
361
- options: ["json"],
364
+ options: ["if-revision", "json"],
362
365
  },
363
366
  move: {
364
367
  usage: "agency task move <id> (--epic <id> | --no-epic) [--json]",
365
368
  minArgs: 1,
366
369
  maxArgs: 1,
367
- options: ["epic", "no-epic", "json"],
370
+ options: ["epic", "no-epic", "if-revision", "json"],
368
371
  conflicts: [["epic", "no-epic"]],
369
372
  },
370
373
  dependency: {
@@ -372,7 +375,7 @@ const commands = {
372
375
  "agency task dependency <add|remove> <task-id> <dependency-id> [--json]",
373
376
  minArgs: 3,
374
377
  maxArgs: 3,
375
- options: ["json"],
378
+ options: ["if-revision", "json"],
376
379
  },
377
380
  },
378
381
  },
@@ -441,6 +444,7 @@ const commands = {
441
444
  "base",
442
445
  "pr-url",
443
446
  "clear-pr",
447
+ "if-revision",
444
448
  "json",
445
449
  ],
446
450
  repeatable: ["reference"],
@@ -454,14 +458,14 @@ const commands = {
454
458
  usage: "agency phase rename <task-id> <phase-id> <new-id> [--json]",
455
459
  minArgs: 3,
456
460
  maxArgs: 3,
457
- options: ["json"],
461
+ options: ["if-revision", "json"],
458
462
  },
459
463
  dependency: {
460
464
  usage:
461
465
  "agency phase dependency <add|remove> <task-id> <phase-id> <dependency-id> [--json]",
462
466
  minArgs: 4,
463
467
  maxArgs: 4,
464
- options: ["json"],
468
+ options: ["if-revision", "json"],
465
469
  },
466
470
  },
467
471
  },
@@ -539,26 +543,76 @@ const commands = {
539
543
  },
540
544
  },
541
545
  archive: {
542
- usage: "agency archive <epic|task|phase>",
543
- options: { ...outputOptions, ...entitySelectorOptions },
546
+ usage: "agency archive <list|show|epic|task|phase>",
547
+ options: {
548
+ ...outputOptions,
549
+ ...entitySelectorOptions,
550
+ "dry-run": { type: "boolean" },
551
+ kind: { type: "string", multiple: true },
552
+ status: { type: "string", multiple: true },
553
+ repository: { type: "string", multiple: true },
554
+ },
544
555
  subcommands: {
556
+ list: {
557
+ usage:
558
+ "agency archive list [--kind <kind>] [--status <status>] [--repository <alias>] [--json]",
559
+ minArgs: 0,
560
+ maxArgs: 0,
561
+ options: ["kind", "status", "repository", "json"],
562
+ repeatable: ["kind", "status", "repository"],
563
+ },
564
+ show: {
565
+ usage:
566
+ "agency archive show <epic|task> <id> | phase <task-id> <phase-id> [--json]",
567
+ minArgs: 2,
568
+ maxArgs: 3,
569
+ options: ["json"],
570
+ },
545
571
  epic: {
546
- usage: "agency archive epic <epic-id> [--json]",
572
+ usage: "agency archive epic <epic-id> [--dry-run] [--json]",
547
573
  minArgs: 1,
548
574
  maxArgs: 1,
549
- options: ["json", "epic"],
575
+ options: ["dry-run", "json", "epic"],
550
576
  },
551
577
  task: {
552
- usage: "agency archive task <task-id> [--json]",
578
+ usage: "agency archive task <task-id> [--dry-run] [--json]",
553
579
  minArgs: 1,
554
580
  maxArgs: 1,
555
- options: ["json", "task"],
581
+ options: ["dry-run", "json", "task"],
556
582
  },
557
583
  phase: {
558
- usage: "agency archive phase <task-id> <phase-id> [--json]",
584
+ usage: "agency archive phase <task-id> <phase-id> [--dry-run] [--json]",
559
585
  minArgs: 2,
560
586
  maxArgs: 2,
561
- options: ["json", "task", "phase"],
587
+ options: ["dry-run", "json", "task", "phase"],
588
+ },
589
+ },
590
+ },
591
+ restore: {
592
+ usage: "agency restore <epic|task|phase>",
593
+ options: {
594
+ ...outputOptions,
595
+ ...entitySelectorOptions,
596
+ "dry-run": { type: "boolean" },
597
+ },
598
+ subcommands: {
599
+ epic: {
600
+ usage: "agency restore epic <epic-id> [--dry-run] [--json]",
601
+ minArgs: 1,
602
+ maxArgs: 1,
603
+ options: ["dry-run", "json", "epic"],
604
+ },
605
+ task: {
606
+ usage: "agency restore task <task-id> [--dry-run] [--json]",
607
+ minArgs: 1,
608
+ maxArgs: 1,
609
+ options: ["dry-run", "json", "task"],
610
+ },
611
+ phase: {
612
+ usage: "agency restore phase <task-id> <phase-id> [--dry-run] [--json]",
613
+ minArgs: 2,
614
+ maxArgs: 2,
615
+ options: ["dry-run", "json", "task", "phase"],
562
616
  },
563
617
  },
564
618
  },
@@ -785,12 +839,14 @@ const targetSlots = (
785
839
  return subcommand === "list" ? ["task"] : ["task", "phase"]
786
840
  if (["claim", "release", "finish"].includes(commandName))
787
841
  return ["task", "phase"]
788
- if (commandName === "archive")
842
+ if (["archive", "restore"].includes(commandName))
789
843
  return subcommand === "epic"
790
844
  ? ["epic"]
791
845
  : subcommand === "task"
792
846
  ? ["task"]
793
- : ["task", "phase"]
847
+ : subcommand === "phase"
848
+ ? ["task", "phase"]
849
+ : []
794
850
  if (commandName === "pr" && subcommand === "create") return ["task", "phase"]
795
851
  return []
796
852
  }
@@ -1125,16 +1181,28 @@ export function parseCli(args: readonly string[]): ParsedCli {
1125
1181
  ["epic", "task", "phase"].includes(commandName) &&
1126
1182
  subcommand === "update"
1127
1183
  ) {
1128
- const mutationNames = (spec.options ?? []).filter((name) => name !== "json")
1184
+ const mutationNames = (spec.options ?? []).filter(
1185
+ (name) => name !== "json" && name !== "if-revision",
1186
+ )
1129
1187
  if (!mutationNames.some((name) => parsed.values[name] !== undefined)) {
1130
1188
  throw usageError("At least one update option is required.", spec.usage)
1131
1189
  }
1132
1190
  }
1191
+ if (
1192
+ typeof parsed.values["if-revision"] === "string" &&
1193
+ !/^[a-f0-9]{64}$/.test(parsed.values["if-revision"])
1194
+ ) {
1195
+ throw usageError(
1196
+ "Option '--if-revision' must be a 64-character SHA-256 hash.",
1197
+ spec.usage,
1198
+ )
1199
+ }
1133
1200
  if (commandName === "graph") {
1134
1201
  validateGraphOptions(parsed.values, spec)
1135
1202
  }
1136
1203
  if (
1137
1204
  commandName === "status" ||
1205
+ (commandName === "archive" && subcommand === "list") ||
1138
1206
  (["epic", "task", "phase"].includes(commandName) && subcommand === "list")
1139
1207
  ) {
1140
1208
  validateViewOptions(parsed.values, spec)
package/src/cli.test.ts CHANGED
@@ -203,6 +203,41 @@ describe("CLI", () => {
203
203
  ),
204
204
  )
205
205
  }
206
+ const observed = parseJson(
207
+ await runCli(["task", "show", "second", "--json"], root),
208
+ )
209
+ const taskPath = join(root, "tasks/second/TASK.md")
210
+ await Bun.write(
211
+ taskPath,
212
+ `${await Bun.file(taskPath).text()}\nConcurrent edit.\n`,
213
+ )
214
+ const conflict = await runCli(
215
+ [
216
+ "task",
217
+ "update",
218
+ "second",
219
+ "--description",
220
+ "Stale update",
221
+ "--if-revision",
222
+ observed.revision,
223
+ "--json",
224
+ ],
225
+ root,
226
+ )
227
+ expect(conflict.exitCode).toBe(1)
228
+ expect(JSON.parse(conflict.stdout)).toMatchObject({
229
+ ok: false,
230
+ error: {
231
+ code: "REVISION_CONFLICT",
232
+ retryable: true,
233
+ fields: {
234
+ path: "tasks/second/TASK.md",
235
+ expectedRevision: observed.revision,
236
+ currentRevision: expect.stringMatching(/^[a-f0-9]{64}$/),
237
+ },
238
+ },
239
+ })
240
+ expect(await Bun.file(taskPath).text()).toContain("Concurrent edit.")
206
241
 
207
242
  const updated = parseJson(
208
243
  await runCli(
@@ -310,6 +345,7 @@ describe("CLI", () => {
310
345
  ["task", "Usage: agency task"],
311
346
  ["phase", "Usage: agency phase"],
312
347
  ["archive", "Usage: agency archive"],
348
+ ["restore", "Usage: agency restore"],
313
349
  ["work", "Usage: agency work"],
314
350
  ["pr", "Usage: agency pr"],
315
351
  ["status", "Usage: agency status"],
@@ -52,18 +52,36 @@ describe("archive command", () => {
52
52
  ),
53
53
  )
54
54
 
55
- expect(JSON.parse(logs[0]!)).toEqual({
55
+ expect(JSON.parse(logs[0]!)).toMatchObject({
56
+ operation: "archive",
56
57
  kind: "task",
57
58
  id: "example",
58
59
  path: join(root, "archive/tasks/example"),
59
- archivedPaths: [join(root, "archive/tasks/example")],
60
+ affectedPaths: [join(root, "archive/tasks/example")],
60
61
  removedWorktrees: [],
62
+ dryRun: false,
61
63
  })
62
64
  })
63
65
 
64
66
  test("requires a supported work item type", async () => {
65
67
  await expect(
66
68
  runTestEffect(archive({ args: [], cwd: root, silent: true })),
67
- ).rejects.toThrow("Available: epic, task, phase")
69
+ ).rejects.toThrow("Available: list, show, epic, task, phase")
70
+ })
71
+
72
+ test("rejects an extra archive show identifier", async () => {
73
+ await runTestEffect(
74
+ archive({ type: "task", args: ["example"], cwd: root, silent: true }),
75
+ )
76
+ await expect(
77
+ runTestEffect(
78
+ archive({
79
+ type: "show",
80
+ args: ["task", "example", "extra"],
81
+ cwd: root,
82
+ silent: true,
83
+ }),
84
+ ),
85
+ ).rejects.toThrow("Usage: agency archive show")
68
86
  })
69
87
  })
@@ -7,6 +7,15 @@ interface ArchiveOptions extends BaseCommandOptions {
7
7
  readonly type?: string
8
8
  readonly args: readonly string[]
9
9
  readonly json?: boolean
10
+ readonly dryRun?: boolean
11
+ readonly kinds?: readonly string[]
12
+ readonly statuses?: readonly string[]
13
+ readonly repositories?: readonly string[]
14
+ }
15
+
16
+ const archiveKind = (value: string | undefined) => {
17
+ if (value === "epic" || value === "task" || value === "phase") return value
18
+ return undefined
10
19
  }
11
20
 
12
21
  export const archive = (options: ArchiveOptions) =>
@@ -16,55 +25,114 @@ export const archive = (options: ArchiveOptions) =>
16
25
  const cwd = options.cwd ?? process.cwd()
17
26
  const [id, phaseId] = options.args
18
27
 
28
+ if (options.type === "list") {
29
+ const records = yield* archives.list(
30
+ {
31
+ kinds: options.kinds,
32
+ statuses: options.statuses,
33
+ repositories: options.repositories,
34
+ },
35
+ cwd,
36
+ )
37
+ log(
38
+ options.json
39
+ ? JSON.stringify(records, null, 2)
40
+ : records
41
+ .map((record) =>
42
+ record.kind === "phase"
43
+ ? `phase\t${record.taskId}/${record.id}`
44
+ : `${record.kind}\t${record.id}`,
45
+ )
46
+ .join("\n"),
47
+ )
48
+ return
49
+ }
50
+
51
+ if (options.type === "show") {
52
+ const [kindValue, firstId, secondId] = options.args
53
+ const kind = archiveKind(kindValue)
54
+ if (
55
+ !kind ||
56
+ !firstId ||
57
+ (kind === "phase" ? !secondId : secondId !== undefined)
58
+ ) {
59
+ return yield* Effect.fail(
60
+ new Error(
61
+ "Usage: agency archive show <epic|task> <id> | phase <task-id> <phase-id>",
62
+ ),
63
+ )
64
+ }
65
+ const record = yield* archives.show(
66
+ kind,
67
+ kind === "phase" ? secondId! : firstId,
68
+ kind === "phase" ? firstId : undefined,
69
+ cwd,
70
+ )
71
+ log(options.json ? JSON.stringify(record, null, 2) : record.content)
72
+ return
73
+ }
74
+
19
75
  let result
20
76
  switch (options.type) {
21
77
  case "epic":
22
- if (!id) {
78
+ if (!id)
23
79
  return yield* Effect.fail(
24
80
  new Error("Usage: agency archive epic <epic-id>"),
25
81
  )
26
- }
27
- result = yield* archives.archiveEpic(id, cwd)
82
+ result = yield* archives.archiveEpic(id, cwd, {
83
+ dryRun: options.dryRun,
84
+ })
28
85
  break
29
86
  case "task":
30
- if (!id) {
87
+ if (!id)
31
88
  return yield* Effect.fail(
32
89
  new Error("Usage: agency archive task <task-id>"),
33
90
  )
34
- }
35
- result = yield* archives.archiveTask(id, cwd)
91
+ result = yield* archives.archiveTask(id, cwd, {
92
+ dryRun: options.dryRun,
93
+ })
36
94
  break
37
95
  case "phase":
38
- if (!id || !phaseId) {
96
+ if (!id || !phaseId)
39
97
  return yield* Effect.fail(
40
98
  new Error("Usage: agency archive phase <task-id> <phase-id>"),
41
99
  )
42
- }
43
- result = yield* archives.archivePhase(id, phaseId, cwd)
100
+ result = yield* archives.archivePhase(id, phaseId, cwd, {
101
+ dryRun: options.dryRun,
102
+ })
44
103
  break
45
104
  default:
46
105
  return yield* Effect.fail(
47
- new Error("Work item type is required. Available: epic, task, phase"),
106
+ new Error(
107
+ "Archive operation is required. Available: list, show, epic, task, phase",
108
+ ),
48
109
  )
49
110
  }
50
111
 
51
112
  log(
52
113
  options.json
53
114
  ? JSON.stringify(result, null, 2)
54
- : `Archived ${result.kind} '${result.id}' to ${result.path}`,
115
+ : `${result.dryRun ? "Would archive" : "Archived"} ${result.kind} '${result.id}' to ${result.path}`,
55
116
  )
56
117
  })
57
118
 
58
119
  export const help = `
59
- Usage: agency archive <type> <id>
120
+ Usage: agency archive <list|show|epic|task|phase>
60
121
 
61
- Archive a work item after removing its worktrees. Branches are preserved.
122
+ Browse or archive work items after preflighting worktrees and graph references.
62
123
 
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
124
+ Commands:
125
+ list [filters] List archived work
126
+ show <type> <id> Show an archived epic or task
127
+ show phase <task-id> <phase-id> Show an archived phase
128
+ epic <epic-id> Archive an epic and its tasks
129
+ task <task-id> Archive a task
130
+ phase <task-id> <phase-id> Archive a phase
67
131
 
68
132
  Options:
69
- --json Output results as JSON
133
+ --kind <kind> Filter list by kind (repeatable)
134
+ --status <status> Filter list by status (repeatable)
135
+ --repository <alias> Filter list by repository (repeatable)
136
+ --dry-run Preflight without changing files
137
+ --json Output results as JSON
70
138
  `