@markjaquith/agency 2.49.0 → 2.51.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 +56 -9
- package/cli.ts +56 -1
- package/package.json +1 -1
- package/schemas/agency-graph-v1.schema.json +142 -38
- package/src/cli-parser.test.ts +81 -0
- package/src/cli-parser.ts +78 -1
- package/src/cli.test.ts +4 -4
- package/src/commands/init.test.ts +2 -0
- package/src/commands/review.ts +38 -0
- package/src/commands/task.ts +27 -5
- package/src/commands/vcs.test.ts +75 -0
- package/src/commands/vcs.ts +72 -0
- package/src/commands/work.test.ts +2 -0
- package/src/commands/work.ts +2 -2
- package/src/commands/worktree.ts +1 -1
- package/src/graph-schema.test.ts +1 -1
- package/src/graph-schema.ts +36 -13
- package/src/protocol.ts +1 -0
- package/src/services/ArchiveService.test.ts +2 -2
- package/src/services/ArchiveService.ts +24 -0
- package/src/services/ClaimService.ts +3 -2
- package/src/services/ContextService.ts +118 -17
- package/src/services/DoctorService.ts +58 -7
- package/src/services/GraphMutationService.ts +5 -0
- package/src/services/GraphService.ts +119 -54
- package/src/services/PhaseService.ts +196 -70
- package/src/services/PullRequestService.test.ts +2 -2
- package/src/services/PullRequestService.ts +37 -33
- package/src/services/ReadinessService.ts +7 -3
- package/src/services/RepositoryService.test.ts +31 -1
- package/src/services/RepositoryService.ts +63 -31
- package/src/services/ReviewService.test.ts +472 -0
- package/src/services/ReviewService.ts +427 -0
- package/src/services/SyncService.test.ts +69 -2
- package/src/services/SyncService.ts +161 -90
- package/src/services/TaskPhaseService.test.ts +80 -0
- package/src/services/TaskService.ts +82 -9
- package/src/services/VcsMigrationService.test.ts +211 -0
- package/src/services/VcsMigrationService.ts +816 -0
- package/src/services/VersionControlService.test.ts +100 -0
- package/src/services/VersionControlService.ts +479 -0
- package/src/services/WorkbaseService.ts +7 -2
- package/src/services/WorktreeService.test.ts +88 -17
- package/src/services/WorktreeService.ts +865 -329
- package/src/test-utils.ts +12 -0
- package/src/work-view.ts +14 -6
- package/src/workbase/AGENTS.md +2 -2
- package/src/workbase/schemas.test.ts +57 -0
- package/src/workbase/schemas.ts +57 -0
- package/src/workbase/version-control.ts +5 -0
package/src/cli-parser.ts
CHANGED
|
@@ -82,6 +82,9 @@ const taskCreateOptions = {
|
|
|
82
82
|
branch: { type: "string" },
|
|
83
83
|
base: { type: "string" },
|
|
84
84
|
"multi-phase": { type: "boolean" },
|
|
85
|
+
review: { type: "string" },
|
|
86
|
+
"pull-request": { type: "string" },
|
|
87
|
+
ref: { type: "string" },
|
|
85
88
|
} satisfies OptionConfig
|
|
86
89
|
|
|
87
90
|
const phaseCreateOptions = {
|
|
@@ -393,7 +396,7 @@ const commands = {
|
|
|
393
396
|
},
|
|
394
397
|
create: {
|
|
395
398
|
usage:
|
|
396
|
-
"agency task create <id> (--repo <alias> | --multi-phase) [options]",
|
|
399
|
+
"agency task create <id> (--repo <alias> | --multi-phase | --review <alias> (--pull-request <value> | --ref <remote-ref>)) [options]",
|
|
397
400
|
minArgs: 1,
|
|
398
401
|
maxArgs: 1,
|
|
399
402
|
options: [
|
|
@@ -405,6 +408,9 @@ const commands = {
|
|
|
405
408
|
"branch",
|
|
406
409
|
"base",
|
|
407
410
|
"multi-phase",
|
|
411
|
+
"review",
|
|
412
|
+
"pull-request",
|
|
413
|
+
"ref",
|
|
408
414
|
"json",
|
|
409
415
|
],
|
|
410
416
|
repeatable: ["reference"],
|
|
@@ -760,6 +766,22 @@ const commands = {
|
|
|
760
766
|
},
|
|
761
767
|
},
|
|
762
768
|
},
|
|
769
|
+
review: {
|
|
770
|
+
usage: "agency review refresh <task-id> [--if-revision <hash>] [--json]",
|
|
771
|
+
options: {
|
|
772
|
+
...outputOptions,
|
|
773
|
+
"if-revision": { type: "string" },
|
|
774
|
+
},
|
|
775
|
+
subcommands: {
|
|
776
|
+
refresh: {
|
|
777
|
+
usage:
|
|
778
|
+
"agency review refresh <task-id> [--if-revision <hash>] [--json]",
|
|
779
|
+
minArgs: 1,
|
|
780
|
+
maxArgs: 1,
|
|
781
|
+
options: ["if-revision", "json"],
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
},
|
|
763
785
|
worktree: {
|
|
764
786
|
usage: "agency worktree <list|inspect|prepare|remove|rebuild|repair>",
|
|
765
787
|
options: {
|
|
@@ -810,6 +832,29 @@ const commands = {
|
|
|
810
832
|
},
|
|
811
833
|
},
|
|
812
834
|
},
|
|
835
|
+
vcs: {
|
|
836
|
+
usage: "agency vcs <status|migrate>",
|
|
837
|
+
options: {
|
|
838
|
+
...outputOptions,
|
|
839
|
+
apply: { type: "boolean" },
|
|
840
|
+
"dry-run": { type: "boolean" },
|
|
841
|
+
},
|
|
842
|
+
subcommands: {
|
|
843
|
+
status: {
|
|
844
|
+
usage: "agency vcs status [--json]",
|
|
845
|
+
minArgs: 0,
|
|
846
|
+
maxArgs: 0,
|
|
847
|
+
options: ["json"],
|
|
848
|
+
},
|
|
849
|
+
migrate: {
|
|
850
|
+
usage: "agency vcs migrate <git|jj> [--dry-run | --apply] [--json]",
|
|
851
|
+
minArgs: 1,
|
|
852
|
+
maxArgs: 1,
|
|
853
|
+
options: ["apply", "dry-run", "json"],
|
|
854
|
+
conflicts: [["apply", "dry-run"]],
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
},
|
|
813
858
|
work: {
|
|
814
859
|
usage:
|
|
815
860
|
"agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>] [--auto] | agency work prepare [target] [--dry-run] [--json]",
|
|
@@ -1161,6 +1206,38 @@ function validateTaskCreate(
|
|
|
1161
1206
|
spec: LeafCommand,
|
|
1162
1207
|
requireRepo: boolean,
|
|
1163
1208
|
) {
|
|
1209
|
+
const review = values.review !== undefined
|
|
1210
|
+
const pullRequest = values["pull-request"] !== undefined
|
|
1211
|
+
const ref = values.ref !== undefined
|
|
1212
|
+
if (review) {
|
|
1213
|
+
if (pullRequest === ref) {
|
|
1214
|
+
throw usageError(
|
|
1215
|
+
"Option '--review' requires exactly one of '--pull-request' or '--ref'.",
|
|
1216
|
+
spec.usage,
|
|
1217
|
+
)
|
|
1218
|
+
}
|
|
1219
|
+
for (const option of [
|
|
1220
|
+
"repo",
|
|
1221
|
+
"reference",
|
|
1222
|
+
"branch",
|
|
1223
|
+
"base",
|
|
1224
|
+
"multi-phase",
|
|
1225
|
+
] as const) {
|
|
1226
|
+
if (values[option] !== undefined) {
|
|
1227
|
+
throw usageError(
|
|
1228
|
+
`Option '--review' cannot be combined with '${optionLabel(option)}'.`,
|
|
1229
|
+
spec.usage,
|
|
1230
|
+
)
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
return
|
|
1234
|
+
}
|
|
1235
|
+
if (pullRequest || ref) {
|
|
1236
|
+
throw usageError(
|
|
1237
|
+
`Option '${pullRequest ? "--pull-request" : "--ref"}' requires '--review'.`,
|
|
1238
|
+
spec.usage,
|
|
1239
|
+
)
|
|
1240
|
+
}
|
|
1164
1241
|
if (values["multi-phase"]) {
|
|
1165
1242
|
for (const option of ["repo", "reference", "branch", "base"] as const) {
|
|
1166
1243
|
if (values[option] !== undefined) {
|
package/src/cli.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterAll, afterEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { access, mkdir, realpath, symlink } from "node:fs/promises"
|
|
2
|
+
import { access, mkdir, realpath, stat, symlink } from "node:fs/promises"
|
|
3
3
|
import { join, sep } from "node:path"
|
|
4
4
|
import errorFixture from "../fixtures/protocol/error.json"
|
|
5
5
|
import successFixture from "../fixtures/protocol/success.json"
|
|
@@ -1762,9 +1762,9 @@ status: open
|
|
|
1762
1762
|
alias: "agency",
|
|
1763
1763
|
status: "applied",
|
|
1764
1764
|
})
|
|
1765
|
-
expect(
|
|
1766
|
-
|
|
1767
|
-
)
|
|
1765
|
+
expect(
|
|
1766
|
+
(await stat(join(restored, "repos/agency/.jj"))).isDirectory(),
|
|
1767
|
+
).toBe(true)
|
|
1768
1768
|
|
|
1769
1769
|
const prepared = parseJson(
|
|
1770
1770
|
await runCli(["work", "prepare", "portable", "--json"], restored),
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
runTestEffect,
|
|
9
9
|
} from "../test-utils"
|
|
10
10
|
import { init } from "./init"
|
|
11
|
+
import { preferredVersionControl } from "../workbase/version-control"
|
|
11
12
|
|
|
12
13
|
describe("init command", () => {
|
|
13
14
|
let parent: string
|
|
@@ -26,6 +27,7 @@ describe("init command", () => {
|
|
|
26
27
|
|
|
27
28
|
expect(await Bun.file(join(root, "agency.json")).json()).toEqual({
|
|
28
29
|
version: 2,
|
|
30
|
+
vcs: preferredVersionControl(),
|
|
29
31
|
})
|
|
30
32
|
for (const directory of ["repos", "epics", "tasks"]) {
|
|
31
33
|
expect((await stat(join(root, directory))).isDirectory()).toBe(true)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { ReviewService } from "../services/ReviewService"
|
|
3
|
+
import type { BaseCommandOptions } from "../utils/command"
|
|
4
|
+
import { createLoggers } from "../utils/effect"
|
|
5
|
+
|
|
6
|
+
interface ReviewOptions extends BaseCommandOptions {
|
|
7
|
+
readonly subcommand?: string
|
|
8
|
+
readonly taskId?: string
|
|
9
|
+
readonly ifRevision?: string
|
|
10
|
+
readonly json?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const review = (options: ReviewOptions) =>
|
|
14
|
+
Effect.gen(function* () {
|
|
15
|
+
if (options.subcommand !== "refresh" || !options.taskId) {
|
|
16
|
+
return yield* Effect.fail(
|
|
17
|
+
new Error("Usage: agency review refresh <task>"),
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
const result = yield* (yield* ReviewService).refresh(
|
|
21
|
+
options.taskId,
|
|
22
|
+
options.cwd,
|
|
23
|
+
options.ifRevision,
|
|
24
|
+
)
|
|
25
|
+
const { log } = createLoggers(options)
|
|
26
|
+
log(
|
|
27
|
+
options.json
|
|
28
|
+
? JSON.stringify(result, null, 2)
|
|
29
|
+
: `Refreshed review '${options.taskId}' at ${result.commit}`,
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export const help = `
|
|
34
|
+
Usage: agency review refresh <task-id> [--if-revision <hash>] [--json]
|
|
35
|
+
|
|
36
|
+
Fetch the review source explicitly and replace the pinned commit and any clean,
|
|
37
|
+
detached review checkout. Review sources never move implicitly.
|
|
38
|
+
`
|
package/src/commands/task.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { formatTable } from "../utils/table"
|
|
|
11
11
|
import { getWorkViews } from "../work-view"
|
|
12
12
|
import { GraphMutationService } from "../services/GraphMutationService"
|
|
13
13
|
import { work as startWork, type StartWork } from "./work"
|
|
14
|
+
import { ReviewService } from "../services/ReviewService"
|
|
14
15
|
|
|
15
16
|
interface TaskOptions extends BaseCommandOptions {
|
|
16
17
|
readonly subcommand?: string
|
|
@@ -30,6 +31,9 @@ interface TaskOptions extends BaseCommandOptions {
|
|
|
30
31
|
readonly ifRevision?: string
|
|
31
32
|
readonly noEpic?: boolean
|
|
32
33
|
readonly multiPhase?: boolean
|
|
34
|
+
readonly review?: string
|
|
35
|
+
readonly pullRequest?: string
|
|
36
|
+
readonly ref?: string
|
|
33
37
|
readonly json?: boolean
|
|
34
38
|
readonly statuses?: readonly string[]
|
|
35
39
|
readonly repositories?: readonly string[]
|
|
@@ -85,6 +89,7 @@ export const task = (
|
|
|
85
89
|
const repositories = yield* RepositoryService
|
|
86
90
|
const workbase = yield* WorkbaseService
|
|
87
91
|
const mutations = yield* GraphMutationService
|
|
92
|
+
const reviews = yield* ReviewService
|
|
88
93
|
const { log } = createLoggers(options)
|
|
89
94
|
const cwd = options.cwd ?? process.cwd()
|
|
90
95
|
|
|
@@ -211,11 +216,18 @@ export const task = (
|
|
|
211
216
|
return yield* Effect.fail(new Error("Task ID is required"))
|
|
212
217
|
}
|
|
213
218
|
const multiPhase = options.multiPhase ?? false
|
|
214
|
-
if (!multiPhase && !options.repo) {
|
|
219
|
+
if (!multiPhase && !options.repo && !options.review) {
|
|
215
220
|
return yield* Effect.fail(
|
|
216
221
|
new Error("Writable repository is required for task create"),
|
|
217
222
|
)
|
|
218
223
|
}
|
|
224
|
+
const review = options.review
|
|
225
|
+
? yield* reviews.resolve(
|
|
226
|
+
options.review,
|
|
227
|
+
{ pullRequest: options.pullRequest, ref: options.ref },
|
|
228
|
+
cwd,
|
|
229
|
+
)
|
|
230
|
+
: undefined
|
|
219
231
|
const record = yield* tasks.create(
|
|
220
232
|
{
|
|
221
233
|
id,
|
|
@@ -223,10 +235,16 @@ export const task = (
|
|
|
223
235
|
description: options.description?.trim() || undefined,
|
|
224
236
|
epic: options.epic,
|
|
225
237
|
multiPhase,
|
|
238
|
+
review,
|
|
226
239
|
repo: options.repo,
|
|
227
|
-
repos:
|
|
228
|
-
|
|
229
|
-
|
|
240
|
+
repos: review
|
|
241
|
+
? undefined
|
|
242
|
+
: parseRepositoryReferences(options.references),
|
|
243
|
+
branch:
|
|
244
|
+
multiPhase || review
|
|
245
|
+
? undefined
|
|
246
|
+
: (options.branch ?? `task/${id}`),
|
|
247
|
+
base: multiPhase || review ? undefined : (options.base ?? "main"),
|
|
230
248
|
},
|
|
231
249
|
cwd,
|
|
232
250
|
)
|
|
@@ -462,7 +480,11 @@ Create options:
|
|
|
462
480
|
Read-only repository reference; repeatable
|
|
463
481
|
--branch <name> Working branch (default: task/<id>)
|
|
464
482
|
--base <name> Base branch (default: main)
|
|
465
|
-
|
|
483
|
+
--multi-phase Create a task container for phases
|
|
484
|
+
--review <alias> Create a pinned read-only review task
|
|
485
|
+
--pull-request <url-or-number>
|
|
486
|
+
Review a GitHub pull request from the alias origin
|
|
487
|
+
--ref <remote-ref> Review a branch or other fetchable origin ref
|
|
466
488
|
--work Start work on the new task after creating it
|
|
467
489
|
--auto Pass --auto to work; requires --work
|
|
468
490
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
captureLogs,
|
|
4
|
+
cleanupTempDir,
|
|
5
|
+
createTempDir,
|
|
6
|
+
runTestEffect,
|
|
7
|
+
} from "../test-utils"
|
|
8
|
+
import { vcs } from "./vcs"
|
|
9
|
+
|
|
10
|
+
describe("vcs command", () => {
|
|
11
|
+
let root: string
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
root = await createTempDir()
|
|
15
|
+
await Bun.write(
|
|
16
|
+
`${root}/agency.json`,
|
|
17
|
+
JSON.stringify({ version: 2, vcs: "git" }),
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(async () => cleanupTempDir(root))
|
|
22
|
+
|
|
23
|
+
test("reports structured backend status", async () => {
|
|
24
|
+
const logs = await captureLogs(() =>
|
|
25
|
+
runTestEffect(vcs({ subcommand: "status", cwd: root, json: true })),
|
|
26
|
+
)
|
|
27
|
+
expect(JSON.parse(logs[0]!)).toMatchObject({
|
|
28
|
+
root,
|
|
29
|
+
configured: "git",
|
|
30
|
+
source: "git",
|
|
31
|
+
target: "git",
|
|
32
|
+
workspaceCount: 0,
|
|
33
|
+
blockers: [],
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test("persists the inferred Git backend for a legacy workbase", async () => {
|
|
38
|
+
await Bun.write(`${root}/agency.json`, JSON.stringify({ version: 2 }))
|
|
39
|
+
await captureLogs(() =>
|
|
40
|
+
runTestEffect(
|
|
41
|
+
vcs({
|
|
42
|
+
subcommand: "migrate",
|
|
43
|
+
target: "git",
|
|
44
|
+
cwd: root,
|
|
45
|
+
apply: true,
|
|
46
|
+
json: true,
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
expect(await Bun.file(`${root}/agency.json`).json()).toEqual({
|
|
51
|
+
version: 2,
|
|
52
|
+
vcs: "git",
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("treats migration to the configured backend as a no-op", async () => {
|
|
57
|
+
const logs = await captureLogs(() =>
|
|
58
|
+
runTestEffect(
|
|
59
|
+
vcs({
|
|
60
|
+
subcommand: "migrate",
|
|
61
|
+
target: "git",
|
|
62
|
+
cwd: root,
|
|
63
|
+
apply: true,
|
|
64
|
+
json: true,
|
|
65
|
+
}),
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
expect(JSON.parse(logs[0]!)).toMatchObject({
|
|
69
|
+
source: "git",
|
|
70
|
+
target: "git",
|
|
71
|
+
mode: "apply",
|
|
72
|
+
actions: [],
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { VcsMigrationService } from "../services/VcsMigrationService"
|
|
3
|
+
import type { BaseCommandOptions } from "../utils/command"
|
|
4
|
+
import { createLoggers } from "../utils/effect"
|
|
5
|
+
|
|
6
|
+
interface VcsOptions extends BaseCommandOptions {
|
|
7
|
+
readonly subcommand?: string
|
|
8
|
+
readonly target?: string
|
|
9
|
+
readonly apply?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const vcs = (options: VcsOptions = {}) =>
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const migrations = yield* VcsMigrationService
|
|
15
|
+
const { log } = createLoggers(options)
|
|
16
|
+
const root = options.cwd ?? process.cwd()
|
|
17
|
+
if (options.subcommand === "status") {
|
|
18
|
+
const status = yield* migrations.status(root)
|
|
19
|
+
if (options.json) return log(JSON.stringify(status, null, 2))
|
|
20
|
+
log(
|
|
21
|
+
`Version control: ${status.source}${status.configured ? "" : " (inferred for legacy workbase)"}`,
|
|
22
|
+
)
|
|
23
|
+
log(
|
|
24
|
+
`Tools: git=${status.available.git ? "available" : "missing"} jj=${status.available.jj ? "available" : "missing"}`,
|
|
25
|
+
)
|
|
26
|
+
log(
|
|
27
|
+
`Repositories: ${status.repositories.length}; managed workspaces: ${status.workspaceCount}; blockers: ${status.blockers.length}`,
|
|
28
|
+
)
|
|
29
|
+
for (const blocker of status.blockers)
|
|
30
|
+
log(`blocker ${blocker.kind} ${blocker.target}: ${blocker.message}`)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
if (options.subcommand === "migrate") {
|
|
34
|
+
if (options.target !== "git" && options.target !== "jj") {
|
|
35
|
+
return yield* Effect.fail(
|
|
36
|
+
new Error("VCS migration target must be 'git' or 'jj'"),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
const result = yield* migrations.migrate(options.target, root, {
|
|
40
|
+
apply: options.apply,
|
|
41
|
+
})
|
|
42
|
+
if (options.json) return log(JSON.stringify(result, null, 2))
|
|
43
|
+
log(
|
|
44
|
+
result.mode === "apply"
|
|
45
|
+
? `Migrated workbase to ${options.target}`
|
|
46
|
+
: `Migration plan: ${result.source} -> ${options.target}`,
|
|
47
|
+
)
|
|
48
|
+
for (const action of result.actions) log(`- ${action}`)
|
|
49
|
+
if (result.mode === "dry-run" && result.actions.length > 0)
|
|
50
|
+
log("Run again with --apply to perform the migration.")
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
return yield* Effect.fail(
|
|
54
|
+
new Error(`Unknown vcs subcommand '${options.subcommand ?? ""}'`),
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export const help = `
|
|
59
|
+
Usage: agency vcs <status|migrate>
|
|
60
|
+
|
|
61
|
+
Inspect or migrate the workbase-wide version-control backend.
|
|
62
|
+
|
|
63
|
+
Commands:
|
|
64
|
+
status Show the configured backend, tools, repositories, and blockers
|
|
65
|
+
migrate <git|jj> [--dry-run | --apply]
|
|
66
|
+
Preview or apply a clean, transactional backend migration
|
|
67
|
+
|
|
68
|
+
Options:
|
|
69
|
+
--apply Apply the migration; omission performs a dry run
|
|
70
|
+
--dry-run Explicitly preview the migration without applying it
|
|
71
|
+
--json Print structured output
|
|
72
|
+
`
|
|
@@ -24,6 +24,7 @@ const singlePhaseWorkspace: ExecutionWorkspace = {
|
|
|
24
24
|
phasePath: null,
|
|
25
25
|
codePath: "/workbase/tasks/example/code",
|
|
26
26
|
writablePath: "/workbase/tasks/example/code/agency",
|
|
27
|
+
reviewPath: null,
|
|
27
28
|
repo: "agency",
|
|
28
29
|
repos: [],
|
|
29
30
|
dryRun: false,
|
|
@@ -37,6 +38,7 @@ const multiPhaseWorkspace: ExecutionWorkspace = {
|
|
|
37
38
|
phasePath: "/workbase/tasks/example/phases/implementation/PHASE.md",
|
|
38
39
|
codePath: "/workbase/tasks/example/phases/implementation/code",
|
|
39
40
|
writablePath: "/workbase/tasks/example/phases/implementation/code/agency",
|
|
41
|
+
reviewPath: null,
|
|
40
42
|
repo: "agency",
|
|
41
43
|
repos: [],
|
|
42
44
|
dryRun: false,
|
package/src/commands/work.ts
CHANGED
|
@@ -262,7 +262,7 @@ export const work = (
|
|
|
262
262
|
? `${action} the task. Read ${workspace.taskPath} and ${workspace.phasePath}.`
|
|
263
263
|
: `${action} the task. Read ${workspace.taskPath}.`
|
|
264
264
|
launchPath = dirname(workspace.phasePath ?? workspace.taskPath)
|
|
265
|
-
writablePath = workspace.writablePath
|
|
265
|
+
writablePath = workspace.writablePath ?? undefined
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
const explicitlyRequested = Boolean(
|
|
@@ -468,7 +468,7 @@ export const workPrepare = (options: WorkOptions = {}) =>
|
|
|
468
468
|
log(JSON.stringify(workspace, null, 2))
|
|
469
469
|
} else {
|
|
470
470
|
log(
|
|
471
|
-
`${workspace.dryRun ? "Workspace plan" : "Workspace ready"}: ${workspace.writablePath}`,
|
|
471
|
+
`${workspace.dryRun ? "Workspace plan" : "Workspace ready"}: ${workspace.writablePath ?? workspace.reviewPath}`,
|
|
472
472
|
)
|
|
473
473
|
}
|
|
474
474
|
})
|
package/src/commands/worktree.ts
CHANGED
|
@@ -120,7 +120,7 @@ export const worktree = (options: WorktreeOptions = {}) =>
|
|
|
120
120
|
export const help = `
|
|
121
121
|
Usage: agency worktree <list|inspect|prepare|remove|rebuild|repair>
|
|
122
122
|
|
|
123
|
-
Inspect and maintain Agency-managed writable and reference
|
|
123
|
+
Inspect and maintain Agency-managed writable and reference workspaces.
|
|
124
124
|
|
|
125
125
|
Commands:
|
|
126
126
|
list List every managed checkout
|
package/src/graph-schema.test.ts
CHANGED
package/src/graph-schema.ts
CHANGED
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
EpicFrontmatter,
|
|
4
4
|
PhaseFrontmatter,
|
|
5
5
|
PullRequestRecord,
|
|
6
|
+
ClaimRecord,
|
|
7
|
+
ReviewRecord,
|
|
6
8
|
TaskFrontmatter,
|
|
7
9
|
WorkStatus,
|
|
8
10
|
} from "./workbase/schemas"
|
|
@@ -54,6 +56,7 @@ export const GraphReadiness = Schema.Struct({
|
|
|
54
56
|
|
|
55
57
|
export const GraphWorkbase = Schema.Struct({
|
|
56
58
|
version: Schema.Literal(2),
|
|
59
|
+
vcs: Schema.optional(Schema.Literal("git", "jj")),
|
|
57
60
|
root: Schema.optional(Schema.String),
|
|
58
61
|
})
|
|
59
62
|
|
|
@@ -69,13 +72,24 @@ const DocumentHash = Schema.Struct({ sha256: Schema.String })
|
|
|
69
72
|
export const GraphEpicData = Schema.extend(EpicFrontmatter, DocumentHash)
|
|
70
73
|
export const GraphTaskData = Schema.extend(TaskFrontmatter, DocumentHash)
|
|
71
74
|
export const GraphPhaseData = Schema.extend(PhaseFrontmatter, DocumentHash)
|
|
72
|
-
export const GraphExecutionData = Schema.
|
|
73
|
-
|
|
75
|
+
export const GraphExecutionData = Schema.Union(
|
|
76
|
+
Schema.extend(
|
|
77
|
+
PhaseFrontmatter,
|
|
78
|
+
Schema.Struct({
|
|
79
|
+
taskId: Schema.String,
|
|
80
|
+
phaseId: Schema.optional(Schema.String),
|
|
81
|
+
ticketUrl: Schema.optional(Schema.NullOr(Schema.String)),
|
|
82
|
+
epic: Schema.optional(Schema.String),
|
|
83
|
+
}),
|
|
84
|
+
),
|
|
74
85
|
Schema.Struct({
|
|
75
86
|
taskId: Schema.String,
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
ticketUrl: Schema.NullOr(Schema.String),
|
|
88
|
+
description: Schema.optional(Schema.String),
|
|
78
89
|
epic: Schema.optional(Schema.String),
|
|
90
|
+
review: ReviewRecord,
|
|
91
|
+
status: WorkStatus,
|
|
92
|
+
claim: Schema.optional(ClaimRecord),
|
|
79
93
|
}),
|
|
80
94
|
)
|
|
81
95
|
|
|
@@ -98,15 +112,24 @@ export const GraphRepositoryGit = Schema.Struct({
|
|
|
98
112
|
head: Schema.NullOr(Schema.String),
|
|
99
113
|
branch: Schema.NullOr(Schema.String),
|
|
100
114
|
})
|
|
101
|
-
export const GraphExecutionGit = Schema.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
export const GraphExecutionGit = Schema.Union(
|
|
116
|
+
Schema.Struct({
|
|
117
|
+
branch: Schema.String,
|
|
118
|
+
base: Schema.String,
|
|
119
|
+
branchCommit: Schema.NullOr(Schema.String),
|
|
120
|
+
baseCommit: Schema.NullOr(Schema.String),
|
|
121
|
+
checkoutCommit: Schema.NullOr(Schema.String),
|
|
122
|
+
checkoutBranch: Schema.NullOr(Schema.String),
|
|
123
|
+
dirty: Schema.NullOr(Schema.Boolean),
|
|
124
|
+
}),
|
|
125
|
+
Schema.Struct({
|
|
126
|
+
pinnedCommit: Schema.String,
|
|
127
|
+
sourceCommit: Schema.NullOr(Schema.String),
|
|
128
|
+
checkoutCommit: Schema.NullOr(Schema.String),
|
|
129
|
+
checkoutBranch: Schema.NullOr(Schema.String),
|
|
130
|
+
dirty: Schema.NullOr(Schema.Boolean),
|
|
131
|
+
}),
|
|
132
|
+
)
|
|
110
133
|
export const GraphPr = Schema.Union(
|
|
111
134
|
Schema.Struct({ url: Schema.Null, state: Schema.Literal("none") }),
|
|
112
135
|
Schema.Struct({ url: Schema.String, state: Schema.Literal("unavailable") }),
|
package/src/protocol.ts
CHANGED
|
@@ -100,6 +100,7 @@ const errorMetadata: Readonly<Record<string, ErrorMetadata>> = {
|
|
|
100
100
|
ArchiveError: { code: "ARCHIVE_ERROR", retryable: false },
|
|
101
101
|
WorktreeError: { code: "WORKTREE_ERROR", retryable: false },
|
|
102
102
|
PullRequestError: { code: "PULL_REQUEST_ERROR", retryable: false },
|
|
103
|
+
ReviewError: { code: "REVIEW_ERROR", retryable: false },
|
|
103
104
|
ContextError: {
|
|
104
105
|
code: "CONTEXT_ERROR",
|
|
105
106
|
retryable: false,
|
|
@@ -315,7 +315,7 @@ describe("ArchiveService", () => {
|
|
|
315
315
|
),
|
|
316
316
|
),
|
|
317
317
|
)
|
|
318
|
-
await Bun.write(join(workspace.writablePath
|
|
318
|
+
await Bun.write(join(workspace.writablePath!, "dirty.txt"), "keep me\n")
|
|
319
319
|
|
|
320
320
|
await expect(
|
|
321
321
|
runTestEffect(
|
|
@@ -747,7 +747,7 @@ describe("ArchiveService", () => {
|
|
|
747
747
|
),
|
|
748
748
|
).rejects.toThrow("Another archive or restore operation")
|
|
749
749
|
expect(
|
|
750
|
-
await Bun.file(join(workspace.writablePath
|
|
750
|
+
await Bun.file(join(workspace.writablePath!, "README.md")).exists(),
|
|
751
751
|
).toBe(true)
|
|
752
752
|
expect(await Bun.file(join(root, "tasks/locked/TASK.md")).exists()).toBe(
|
|
753
753
|
true,
|
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
WorktreeService,
|
|
12
12
|
type WorktreeRemovalSnapshot,
|
|
13
13
|
} from "./WorktreeService"
|
|
14
|
+
import {
|
|
15
|
+
GitVersionControlService,
|
|
16
|
+
JjVersionControlService,
|
|
17
|
+
VersionControlService,
|
|
18
|
+
} from "./VersionControlService"
|
|
14
19
|
import {
|
|
15
20
|
formatMarkdownDocument,
|
|
16
21
|
parseFrontmatter,
|
|
@@ -268,6 +273,9 @@ const applyMutation = (moves: readonly Move[], writes: readonly Write[]) =>
|
|
|
268
273
|
const WorktreeLayer = Layer.mergeAll(
|
|
269
274
|
FileSystemService.Default,
|
|
270
275
|
WorkbaseService.Default,
|
|
276
|
+
GitVersionControlService.Default,
|
|
277
|
+
JjVersionControlService.Default,
|
|
278
|
+
VersionControlService.Default,
|
|
271
279
|
TaskService.Default,
|
|
272
280
|
PhaseService.Default,
|
|
273
281
|
WorktreeService.Default,
|
|
@@ -298,6 +306,21 @@ const restoreWorktreeSnapshots = async (
|
|
|
298
306
|
continue
|
|
299
307
|
} catch {}
|
|
300
308
|
await mkdir(dirname(snapshot.path), { recursive: true })
|
|
309
|
+
if (snapshot.vcs === "jj") {
|
|
310
|
+
await runGit([
|
|
311
|
+
"jj",
|
|
312
|
+
"-R",
|
|
313
|
+
snapshot.repositoryPath,
|
|
314
|
+
"workspace",
|
|
315
|
+
"add",
|
|
316
|
+
"--name",
|
|
317
|
+
snapshot.workspaceName!,
|
|
318
|
+
"-r",
|
|
319
|
+
snapshot.head,
|
|
320
|
+
snapshot.path,
|
|
321
|
+
])
|
|
322
|
+
continue
|
|
323
|
+
}
|
|
301
324
|
await runGit(
|
|
302
325
|
snapshot.branch
|
|
303
326
|
? [
|
|
@@ -367,6 +390,7 @@ const repositoriesFor = (record: ArchivedRecord) => {
|
|
|
367
390
|
...(record.data.repos ?? []).map((reference) => reference.repo),
|
|
368
391
|
]
|
|
369
392
|
}
|
|
393
|
+
if ("review" in record.data) return [record.data.review.repo]
|
|
370
394
|
return []
|
|
371
395
|
}
|
|
372
396
|
|
|
@@ -108,7 +108,8 @@ interface ReconcileInput {
|
|
|
108
108
|
const PR_URL = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+\/?$/
|
|
109
109
|
|
|
110
110
|
type SingleTaskData = Extract<TaskData, { readonly repo: string }>
|
|
111
|
-
type
|
|
111
|
+
type ReviewTaskData = Extract<TaskData, { readonly review: unknown }>
|
|
112
|
+
type ExecutionData = SingleTaskData | ReviewTaskData | PhaseData
|
|
112
113
|
|
|
113
114
|
const isTaggedClaimError = (
|
|
114
115
|
error: unknown,
|
|
@@ -541,7 +542,7 @@ export class ClaimService extends Effect.Service<ClaimService>()(
|
|
|
541
542
|
message: `Session '${input.sessionId}' does not own ${inspected.target.label}`,
|
|
542
543
|
})
|
|
543
544
|
}
|
|
544
|
-
if (input.nonPrCompletion && data.pr !== null) {
|
|
545
|
+
if (input.nonPrCompletion && "pr" in data && data.pr !== null) {
|
|
545
546
|
throw new ClaimError({
|
|
546
547
|
target: inspected.target.label,
|
|
547
548
|
message:
|