@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 +22 -4
- package/cli.ts +27 -0
- package/package.json +1 -1
- package/skills/agency/SKILL.md +9 -1
- package/src/cli-parser.test.ts +57 -0
- package/src/cli-parser.ts +85 -17
- package/src/cli.test.ts +36 -0
- package/src/commands/archive.test.ts +21 -3
- package/src/commands/archive.ts +86 -18
- package/src/commands/epic.test.ts +12 -3
- package/src/commands/epic.ts +11 -1
- package/src/commands/phase.ts +13 -1
- package/src/commands/restore.test.ts +98 -0
- package/src/commands/restore.ts +73 -0
- package/src/commands/task-phase.test.ts +16 -4
- package/src/commands/task.ts +13 -1
- package/src/protocol.test.ts +19 -0
- package/src/services/ArchiveService.test.ts +520 -1
- package/src/services/ArchiveService.ts +964 -84
- package/src/services/ClaimService.ts +27 -21
- package/src/services/ContextService.ts +4 -6
- package/src/services/EpicService.ts +22 -2
- package/src/services/GraphMutationService.test.ts +31 -0
- package/src/services/GraphMutationService.ts +126 -11
- package/src/services/GraphService.ts +2 -4
- package/src/services/PhaseService.ts +32 -4
- package/src/services/TaskService.ts +28 -3
- package/src/services/WorktreeService.ts +35 -5
- package/src/work-view.ts +2 -0
- package/src/workbase/archive.ts +18 -0
- package/src/workbase/document-revision.ts +16 -0
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
|
|
569
|
-
agency archive task <
|
|
570
|
-
agency archive phase <task-id> <phase-id>
|
|
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
package/skills/agency/SKILL.md
CHANGED
|
@@ -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
|
|
package/src/cli-parser.test.ts
CHANGED
|
@@ -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: {
|
|
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 (
|
|
842
|
+
if (["archive", "restore"].includes(commandName))
|
|
789
843
|
return subcommand === "epic"
|
|
790
844
|
? ["epic"]
|
|
791
845
|
: subcommand === "task"
|
|
792
846
|
? ["task"]
|
|
793
|
-
:
|
|
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(
|
|
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]!)).
|
|
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
|
-
|
|
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
|
})
|
package/src/commands/archive.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
:
|
|
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 <
|
|
120
|
+
Usage: agency archive <list|show|epic|task|phase>
|
|
60
121
|
|
|
61
|
-
|
|
122
|
+
Browse or archive work items after preflighting worktrees and graph references.
|
|
62
123
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
phase <task-id> <phase-id>
|
|
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
|
-
--
|
|
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
|
`
|