@markjaquith/agency 2.57.0 → 2.58.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 -6
- package/package.json +1 -1
- package/src/cli-parser.test.ts +8 -0
- package/src/cli-parser.ts +7 -1
- package/src/commands/archive.test.ts +39 -1
- package/src/commands/archive.ts +25 -2
- package/src/commands/restore.test.ts +8 -0
- package/src/services/ArchiveBulkService.test.ts +485 -0
- package/src/services/ArchiveService.test.ts +164 -1
- package/src/services/ArchiveService.ts +447 -29
- package/src/services/WorktreeService.test.ts +4 -0
- package/src/services/WorktreeService.ts +1 -5
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.
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
package/src/cli-parser.test.ts
CHANGED
|
@@ -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 () => {
|
package/src/commands/archive.ts
CHANGED
|
@@ -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
|
)
|