@markjaquith/agency 2.57.0 → 2.58.1

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
@@ -859,6 +859,7 @@ agency archive show <epic|task> <id>
859
859
  agency archive show phase <task-id> <phase-id>
860
860
  agency archive epic <epic-id> [--dry-run] [--json]
861
861
  agency archive task <task-id> [--dry-run] [--json]
862
+ agency archive tasks [--dry-run] [--json]
862
863
  agency archive phase <task-id> <phase-id> [--dry-run] [--json]
863
864
  agency restore epic <epic-id> [--dry-run] [--json]
864
865
  agency restore task <task-id> [--dry-run] [--json]
@@ -866,12 +867,26 @@ agency restore phase <task-id> <phase-id> [--dry-run] [--json]
866
867
  ```
867
868
 
868
869
  Archived work keeps its hierarchy under `archive/`. Epic archiving includes its
869
- listed tasks. Task and phase archiving update the active parent document and
870
- reject items that active siblings depend on. Agency removes registered worktrees
871
- before moving files, refuses dirty worktrees, and preserves branches. Archive and
872
- restore preflight all destinations and graph references before changing files.
873
- Versioned lifecycle provenance preserves parent declarations and dependency edges
874
- for restoration. Archived IDs are reserved until restored.
870
+ listed tasks. A task can be archived only when its effective status is terminal
871
+ (`done` or `dropped`). Multi-phase task status is derived from its phases, every
872
+ phase must be terminal, and a task with no phases is not eligible.
873
+
874
+ `archive tasks` plans the maximal safe cohort of terminal tasks and applies by
875
+ default; `--dry-run` performs the same preflight without mutation. A dependency
876
+ is work required by a candidate, while a dependent is work that requires the
877
+ candidate. Dependencies within the selected cohort can be archived together,
878
+ but a retained dependent excludes its dependency, including exclusions that
879
+ propagate through a dependency chain. Active claims, dirty or otherwise unsafe
880
+ managed checkouts, and occupied archive destinations are reported as per-task
881
+ skips. Invalid workbase structure and infrastructure failures abort the command.
882
+
883
+ Task and phase archiving update active parent documents. Agency removes
884
+ registered worktrees before moving files, refuses dirty worktrees, and preserves
885
+ branches. Bulk application updates shared parents once and archives the entire
886
+ selected cohort in one rollback-capable transaction; it never archives an empty
887
+ parent epic implicitly. Versioned lifecycle provenance preserves parent
888
+ declarations and dependency edges for restoration. Archived IDs are reserved
889
+ until restored.
875
890
 
876
891
  ### Work, Publication, and Pull Requests
877
892
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.57.0",
3
+ "version": "2.58.1",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -282,6 +282,7 @@ describe("strict CLI parsing", () => {
282
282
  ],
283
283
  [["archive", "epic", "one", "two"], "agency archive epic"],
284
284
  [["archive", "task", "one", "two"], "agency archive task"],
285
+ [["archive", "tasks", "one"], "agency archive tasks"],
285
286
  [["archive", "phase", "one", "two", "three"], "agency archive phase"],
286
287
  [["archive", "list", "extra"], "agency archive list"],
287
288
  [
@@ -414,6 +415,13 @@ describe("strict CLI parsing", () => {
414
415
  values: { "dry-run": true },
415
416
  },
416
417
  )
418
+ expect(parseCli(["archive", "tasks", "--dry-run", "--json"])).toMatchObject(
419
+ {
420
+ commandName: "archive",
421
+ args: ["tasks"],
422
+ values: { "dry-run": true, json: true },
423
+ },
424
+ )
417
425
  })
418
426
 
419
427
  test("validates revision-guarded claim operations", () => {
package/src/cli-parser.ts CHANGED
@@ -691,7 +691,7 @@ const commands = {
691
691
  },
692
692
  },
693
693
  archive: {
694
- usage: "agency archive <list|show|epic|task|phase>",
694
+ usage: "agency archive <list|show|epic|task|tasks|phase>",
695
695
  options: {
696
696
  ...outputOptions,
697
697
  ...entitySelectorOptions,
@@ -728,6 +728,12 @@ const commands = {
728
728
  maxArgs: 1,
729
729
  options: ["dry-run", "json", "task"],
730
730
  },
731
+ tasks: {
732
+ usage: "agency archive tasks [--dry-run] [--json]",
733
+ minArgs: 0,
734
+ maxArgs: 0,
735
+ options: ["dry-run", "json"],
736
+ },
731
737
  phase: {
732
738
  usage: "agency archive phase <task-id> <phase-id> [--dry-run] [--json]",
733
739
  minArgs: 2,
@@ -36,6 +36,14 @@ describe("archive command", () => {
36
36
  silent: true,
37
37
  }),
38
38
  )
39
+ await runTestEffect(
40
+ task({
41
+ subcommand: "status",
42
+ args: ["example", "dropped"],
43
+ cwd: root,
44
+ silent: true,
45
+ }),
46
+ )
39
47
  })
40
48
 
41
49
  afterEach(async () => cleanupTempDir(root))
@@ -85,10 +93,40 @@ describe("archive command", () => {
85
93
  )
86
94
  })
87
95
 
96
+ test("outputs one deterministic bulk archive object and a concise human summary", async () => {
97
+ const jsonLogs = await captureLogs(() =>
98
+ runTestEffect(
99
+ archive({
100
+ type: "tasks",
101
+ args: [],
102
+ cwd: root,
103
+ dryRun: true,
104
+ json: true,
105
+ }),
106
+ ),
107
+ )
108
+ expect(jsonLogs).toHaveLength(1)
109
+ expect(JSON.parse(jsonLogs[0]!)).toMatchObject({
110
+ operation: "archive",
111
+ kind: "tasks",
112
+ dryRun: true,
113
+ tasks: [{ id: "example", disposition: "planned" }],
114
+ })
115
+
116
+ const humanLogs = await captureLogs(() =>
117
+ runTestEffect(
118
+ archive({ type: "tasks", args: [], cwd: root, dryRun: true }),
119
+ ),
120
+ )
121
+ expect(humanLogs).toEqual([
122
+ "Would archive 1 task: example\nSkipped 0 tasks",
123
+ ])
124
+ })
125
+
88
126
  test("requires a supported work item type", async () => {
89
127
  await expect(
90
128
  runTestEffect(archive({ args: [], cwd: root, silent: true })),
91
- ).rejects.toThrow("Available: list, show, epic, task, phase")
129
+ ).rejects.toThrow("Available: list, show, epic, task, tasks, phase")
92
130
  })
93
131
 
94
132
  test("rejects an extra archive show identifier", async () => {
@@ -92,6 +92,11 @@ export const archive = (options: ArchiveOptions) =>
92
92
  dryRun: options.dryRun,
93
93
  })
94
94
  break
95
+ case "tasks":
96
+ result = yield* archives.archiveTasks(cwd, {
97
+ dryRun: options.dryRun,
98
+ })
99
+ break
95
100
  case "phase":
96
101
  if (!id || !phaseId)
97
102
  return yield* Effect.fail(
@@ -104,11 +109,28 @@ export const archive = (options: ArchiveOptions) =>
104
109
  default:
105
110
  return yield* Effect.fail(
106
111
  new Error(
107
- "Archive operation is required. Available: list, show, epic, task, phase",
112
+ "Archive operation is required. Available: list, show, epic, task, tasks, phase",
108
113
  ),
109
114
  )
110
115
  }
111
116
 
117
+ if (result.kind === "tasks") {
118
+ const selected = result.tasks.filter(
119
+ (task) => task.disposition !== "skipped",
120
+ )
121
+ const skipped = result.tasks.filter(
122
+ (task) => task.disposition === "skipped",
123
+ )
124
+ log(
125
+ options.json
126
+ ? JSON.stringify(result, null, 2)
127
+ : [
128
+ `${result.dryRun ? "Would archive" : "Archived"} ${selected.length} task${selected.length === 1 ? "" : "s"}${selected.length ? `: ${selected.map((task) => task.id).join(", ")}` : ""}`,
129
+ `Skipped ${skipped.length} task${skipped.length === 1 ? "" : "s"}${skipped.length ? `: ${skipped.map((task) => `${task.id} (${task.reason!.code})`).join(", ")}` : ""}`,
130
+ ].join("\n"),
131
+ )
132
+ return
133
+ }
112
134
  log(
113
135
  options.json
114
136
  ? JSON.stringify(result, null, 2)
@@ -119,7 +141,7 @@ export const archive = (options: ArchiveOptions) =>
119
141
  })
120
142
 
121
143
  export const help = `
122
- Usage: agency archive <list|show|epic|task|phase>
144
+ Usage: agency archive <list|show|epic|task|tasks|phase>
123
145
 
124
146
  Browse or archive work items after preflighting worktrees and graph references.
125
147
 
@@ -129,6 +151,7 @@ Commands:
129
151
  show phase <task-id> <phase-id> Show an archived phase
130
152
  epic <epic-id> Archive an epic and its tasks
131
153
  task <task-id> Archive a task
154
+ tasks Archive all eligible terminal tasks
132
155
  phase <task-id> <phase-id> Archive a phase
133
156
 
134
157
  Options:
@@ -35,6 +35,14 @@ describe("restore command", () => {
35
35
  silent: true,
36
36
  }),
37
37
  )
38
+ await runTestEffect(
39
+ task({
40
+ subcommand: "status",
41
+ args: ["example", "dropped"],
42
+ cwd: root,
43
+ silent: true,
44
+ }),
45
+ )
38
46
  await runTestEffect(
39
47
  archive({ type: "task", args: ["example"], cwd: root, silent: true }),
40
48
  )