@markjaquith/agency 2.56.1 → 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 +35 -9
- 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/pr.test.ts +40 -1
- package/src/commands/pr.ts +6 -0
- 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/ContextService.ts +27 -10
- package/src/services/DoctorService.ts +6 -11
- package/src/services/GraphService.ts +60 -102
- package/src/services/PullRequestService.test.ts +45 -1
- package/src/services/PullRequestService.ts +18 -1
- package/src/services/PushService.test.ts +10 -13
- package/src/services/RepositoryService.test.ts +6 -1
- package/src/services/RepositoryService.ts +57 -114
- package/src/services/ReviewService.test.ts +2 -2
- package/src/services/ReviewService.ts +47 -28
- package/src/services/SyncService.test.ts +17 -2
- package/src/services/SyncService.ts +41 -18
- package/src/services/TaskService.ts +18 -2
- package/src/services/VcsMigrationService.test.ts +58 -1
- package/src/services/VcsMigrationService.ts +9 -0
- package/src/services/VersionControlService.test.ts +38 -0
- package/src/services/VersionControlService.ts +211 -2
- package/src/services/WorktreeService.test.ts +4 -0
- package/src/services/WorktreeService.ts +1 -5
- package/src/vcs-status-fast.ts +3 -8
- package/src/workbase/delivery-command.test.ts +11 -2
- package/src/workbase/delivery-command.ts +5 -1
package/README.md
CHANGED
|
@@ -142,9 +142,17 @@ or credential:
|
|
|
142
142
|
|
|
143
143
|
New workbases select `"jj"` when the `jj` executable is available and otherwise
|
|
144
144
|
select `"git"`. Existing version 2 workbases without `vcs` remain Git workbases.
|
|
145
|
-
The selected backend is a workbase-level invariant: jj workbases
|
|
146
|
-
managed clones
|
|
147
|
-
|
|
145
|
+
The selected backend is a workbase-level invariant: jj workbases create
|
|
146
|
+
non-colocated managed clones and jj workspaces, while Git workbases continue to
|
|
147
|
+
use bare repositories and Git worktrees. Existing colocated jj repositories are
|
|
148
|
+
also supported.
|
|
149
|
+
|
|
150
|
+
Agency uses jj-native commands for repository validity, workspace registration,
|
|
151
|
+
working-copy state, revisions, bookmarks, ancestry, fetch, and push. Raw Git is
|
|
152
|
+
limited to backing-store plumbing such as durable review refs and integrations
|
|
153
|
+
that require Git, including GitHub CLI commit discovery. For those operations,
|
|
154
|
+
Agency obtains the backing repository with `jj git root` and supplies it as
|
|
155
|
+
`GIT_DIR`; it does not use raw Git to infer jj workspace state.
|
|
148
156
|
|
|
149
157
|
Inspect the current backend and migration readiness with:
|
|
150
158
|
|
|
@@ -162,6 +170,9 @@ target backend, and updates `agency.json` last. Migration from jj to Git also
|
|
|
162
170
|
blocks jj-only heads that are not preserved by a bookmark or workspace. Failed
|
|
163
171
|
migrations restore the source repositories and workspaces when rollback is
|
|
164
172
|
possible and report explicit manual recovery paths otherwise.
|
|
173
|
+
Converting a non-colocated jj workbase to Git is currently blocked before any
|
|
174
|
+
mutation because its backing Git object store is inside `.jj`; first convert the
|
|
175
|
+
repositories to colocated jj so the Git store survives metadata removal.
|
|
165
176
|
|
|
166
177
|
Existing version 2 workbases without `repositories` remain valid. Run
|
|
167
178
|
`agency repo setup` to preview deterministic adoption of legacy local aliases;
|
|
@@ -848,6 +859,7 @@ agency archive show <epic|task> <id>
|
|
|
848
859
|
agency archive show phase <task-id> <phase-id>
|
|
849
860
|
agency archive epic <epic-id> [--dry-run] [--json]
|
|
850
861
|
agency archive task <task-id> [--dry-run] [--json]
|
|
862
|
+
agency archive tasks [--dry-run] [--json]
|
|
851
863
|
agency archive phase <task-id> <phase-id> [--dry-run] [--json]
|
|
852
864
|
agency restore epic <epic-id> [--dry-run] [--json]
|
|
853
865
|
agency restore task <task-id> [--dry-run] [--json]
|
|
@@ -855,12 +867,26 @@ agency restore phase <task-id> <phase-id> [--dry-run] [--json]
|
|
|
855
867
|
```
|
|
856
868
|
|
|
857
869
|
Archived work keeps its hierarchy under `archive/`. Epic archiving includes its
|
|
858
|
-
listed tasks.
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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.
|
|
864
890
|
|
|
865
891
|
### Work, Publication, and Pull Requests
|
|
866
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:
|
package/src/commands/pr.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
-
import { chmod, mkdir, realpath } from "node:fs/promises"
|
|
3
|
+
import { chmod, mkdir, realpath, rm } from "node:fs/promises"
|
|
4
4
|
import { dirname, join } from "node:path"
|
|
5
5
|
import { PullRequestService } from "../services/PullRequestService"
|
|
6
6
|
import {
|
|
@@ -31,6 +31,7 @@ describe("pr command", () => {
|
|
|
31
31
|
join(bin, "gh"),
|
|
32
32
|
`#!/bin/sh
|
|
33
33
|
printf '%s\n' "$PWD" "$@" > "$GH_CAPTURE"
|
|
34
|
+
if [ -n "$GIT_DIR" ]; then printf 'GIT_DIR=%s\n' "$GIT_DIR" >> "$GH_CAPTURE"; fi
|
|
34
35
|
exit "\${GH_EXIT:-0}"
|
|
35
36
|
`,
|
|
36
37
|
)
|
|
@@ -109,6 +110,41 @@ status: open
|
|
|
109
110
|
# Build
|
|
110
111
|
`,
|
|
111
112
|
)
|
|
113
|
+
if (vcs === "jj") {
|
|
114
|
+
const repository = join(root, "repos/agency")
|
|
115
|
+
await rm(join(root, "tasks/single/code/agency"), { recursive: true })
|
|
116
|
+
await rm(join(root, "tasks/multi/phases/build/code/agency"), {
|
|
117
|
+
recursive: true,
|
|
118
|
+
})
|
|
119
|
+
const initialized = Bun.spawn(
|
|
120
|
+
["jj", "git", "init", "--no-colocate", repository],
|
|
121
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
122
|
+
)
|
|
123
|
+
if ((await initialized.exited) !== 0)
|
|
124
|
+
throw new Error(await new Response(initialized.stderr).text())
|
|
125
|
+
for (const [name, path] of [
|
|
126
|
+
["single", join(root, "tasks/single/code/agency")],
|
|
127
|
+
["build", join(root, "tasks/multi/phases/build/code/agency")],
|
|
128
|
+
] as const) {
|
|
129
|
+
const workspace = Bun.spawn(
|
|
130
|
+
[
|
|
131
|
+
"jj",
|
|
132
|
+
"-R",
|
|
133
|
+
repository,
|
|
134
|
+
"workspace",
|
|
135
|
+
"add",
|
|
136
|
+
"--name",
|
|
137
|
+
name,
|
|
138
|
+
"-r",
|
|
139
|
+
"root()",
|
|
140
|
+
path,
|
|
141
|
+
],
|
|
142
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
143
|
+
)
|
|
144
|
+
if ((await workspace.exited) !== 0)
|
|
145
|
+
throw new Error(await new Response(workspace.stderr).text())
|
|
146
|
+
}
|
|
147
|
+
}
|
|
112
148
|
return root
|
|
113
149
|
}
|
|
114
150
|
|
|
@@ -187,6 +223,7 @@ status: open
|
|
|
187
223
|
"--repo",
|
|
188
224
|
"example/agency",
|
|
189
225
|
"--web",
|
|
226
|
+
expect.stringMatching(/^GIT_DIR=.*\.jj/),
|
|
190
227
|
])
|
|
191
228
|
|
|
192
229
|
expect(
|
|
@@ -202,6 +239,7 @@ status: open
|
|
|
202
239
|
"example/agency",
|
|
203
240
|
"--title",
|
|
204
241
|
"Example",
|
|
242
|
+
expect.stringMatching(/^GIT_DIR=.*\.jj/),
|
|
205
243
|
])
|
|
206
244
|
})
|
|
207
245
|
|
|
@@ -215,6 +253,7 @@ status: open
|
|
|
215
253
|
await realpath(join(task, "code/agency")),
|
|
216
254
|
"pr",
|
|
217
255
|
...args,
|
|
256
|
+
expect.stringMatching(/^GIT_DIR=.*\.jj/),
|
|
218
257
|
])
|
|
219
258
|
})
|
|
220
259
|
|
package/src/commands/pr.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { resolve } from "node:path"
|
|
|
3
3
|
import { ContextService } from "../services/ContextService"
|
|
4
4
|
import { FileSystemService } from "../services/FileSystemService"
|
|
5
5
|
import { PullRequestService } from "../services/PullRequestService"
|
|
6
|
+
import { VersionControlService } from "../services/VersionControlService"
|
|
6
7
|
import { WorkbaseService } from "../services/WorkbaseService"
|
|
7
8
|
import type { BaseCommandOptions } from "../utils/command"
|
|
8
9
|
import { createLoggers } from "../utils/effect"
|
|
@@ -84,6 +85,7 @@ export const pr = (args: readonly string[], cwd: string = process.cwd()) =>
|
|
|
84
85
|
const contexts = yield* ContextService
|
|
85
86
|
const fs = yield* FileSystemService
|
|
86
87
|
const workbase = yield* WorkbaseService
|
|
88
|
+
const versionControl = yield* VersionControlService
|
|
87
89
|
const invocationCwd = resolve(cwd)
|
|
88
90
|
const context = yield* contexts
|
|
89
91
|
.get({ cwd: invocationCwd, target: ".", compact: true })
|
|
@@ -94,6 +96,7 @@ export const pr = (args: readonly string[], cwd: string = process.cwd()) =>
|
|
|
94
96
|
: null
|
|
95
97
|
const focusedCwd = writableCheckout ?? invocationCwd
|
|
96
98
|
let forwardedArgs = args
|
|
99
|
+
let environment: Record<string, string> = {}
|
|
97
100
|
if (
|
|
98
101
|
writableCheckout &&
|
|
99
102
|
context?.workbase.vcs === "jj" &&
|
|
@@ -111,10 +114,13 @@ export const pr = (args: readonly string[], cwd: string = process.cwd()) =>
|
|
|
111
114
|
repositoryFromRemote(remote),
|
|
112
115
|
)
|
|
113
116
|
}
|
|
117
|
+
const backend = yield* versionControl.forWorkbase(context.workbase.root)
|
|
118
|
+
environment = yield* backend.gitEnvironment(writableCheckout)
|
|
114
119
|
}
|
|
115
120
|
const result = yield* fs.runCommand(["gh", "pr", ...forwardedArgs], {
|
|
116
121
|
cwd: focusedCwd,
|
|
117
122
|
passthrough: true,
|
|
123
|
+
env: environment,
|
|
118
124
|
})
|
|
119
125
|
return result.exitCode
|
|
120
126
|
})
|
|
@@ -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
|
)
|