@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.
Files changed (50) hide show
  1. package/README.md +56 -9
  2. package/cli.ts +56 -1
  3. package/package.json +1 -1
  4. package/schemas/agency-graph-v1.schema.json +142 -38
  5. package/src/cli-parser.test.ts +81 -0
  6. package/src/cli-parser.ts +78 -1
  7. package/src/cli.test.ts +4 -4
  8. package/src/commands/init.test.ts +2 -0
  9. package/src/commands/review.ts +38 -0
  10. package/src/commands/task.ts +27 -5
  11. package/src/commands/vcs.test.ts +75 -0
  12. package/src/commands/vcs.ts +72 -0
  13. package/src/commands/work.test.ts +2 -0
  14. package/src/commands/work.ts +2 -2
  15. package/src/commands/worktree.ts +1 -1
  16. package/src/graph-schema.test.ts +1 -1
  17. package/src/graph-schema.ts +36 -13
  18. package/src/protocol.ts +1 -0
  19. package/src/services/ArchiveService.test.ts +2 -2
  20. package/src/services/ArchiveService.ts +24 -0
  21. package/src/services/ClaimService.ts +3 -2
  22. package/src/services/ContextService.ts +118 -17
  23. package/src/services/DoctorService.ts +58 -7
  24. package/src/services/GraphMutationService.ts +5 -0
  25. package/src/services/GraphService.ts +119 -54
  26. package/src/services/PhaseService.ts +196 -70
  27. package/src/services/PullRequestService.test.ts +2 -2
  28. package/src/services/PullRequestService.ts +37 -33
  29. package/src/services/ReadinessService.ts +7 -3
  30. package/src/services/RepositoryService.test.ts +31 -1
  31. package/src/services/RepositoryService.ts +63 -31
  32. package/src/services/ReviewService.test.ts +472 -0
  33. package/src/services/ReviewService.ts +427 -0
  34. package/src/services/SyncService.test.ts +69 -2
  35. package/src/services/SyncService.ts +161 -90
  36. package/src/services/TaskPhaseService.test.ts +80 -0
  37. package/src/services/TaskService.ts +82 -9
  38. package/src/services/VcsMigrationService.test.ts +211 -0
  39. package/src/services/VcsMigrationService.ts +816 -0
  40. package/src/services/VersionControlService.test.ts +100 -0
  41. package/src/services/VersionControlService.ts +479 -0
  42. package/src/services/WorkbaseService.ts +7 -2
  43. package/src/services/WorktreeService.test.ts +88 -17
  44. package/src/services/WorktreeService.ts +865 -329
  45. package/src/test-utils.ts +12 -0
  46. package/src/work-view.ts +14 -6
  47. package/src/workbase/AGENTS.md +2 -2
  48. package/src/workbase/schemas.test.ts +57 -0
  49. package/src/workbase/schemas.ts +57 -0
  50. 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(await Bun.file(join(restored, "repos/agency/HEAD")).exists()).toBe(
1766
- true,
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
+ `
@@ -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: parseRepositoryReferences(options.references),
228
- branch: multiPhase ? undefined : (options.branch ?? `task/${id}`),
229
- base: multiPhase ? undefined : (options.base ?? "main"),
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
- --multi-phase Create a task container for phases
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,
@@ -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
  })
@@ -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 worktrees.
123
+ Inspect and maintain Agency-managed writable and reference workspaces.
124
124
 
125
125
  Commands:
126
126
  list List every managed checkout
@@ -64,7 +64,7 @@ describe("graph contract", () => {
64
64
  ]
65
65
  const graph = {
66
66
  version: 1,
67
- workbase: { version: 2 },
67
+ workbase: { version: 2, vcs: "git" },
68
68
  filters: {
69
69
  ready: null,
70
70
  blocked: null,
@@ -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.extend(
73
- PhaseFrontmatter,
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
- phaseId: Schema.optional(Schema.String),
77
- ticketUrl: Schema.optional(Schema.NullOr(Schema.String)),
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.Struct({
102
- branch: Schema.String,
103
- base: Schema.String,
104
- branchCommit: Schema.NullOr(Schema.String),
105
- baseCommit: Schema.NullOr(Schema.String),
106
- checkoutCommit: Schema.NullOr(Schema.String),
107
- checkoutBranch: Schema.NullOr(Schema.String),
108
- dirty: Schema.NullOr(Schema.Boolean),
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, "dirty.txt"), "keep me\n")
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, "README.md")).exists(),
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 ExecutionData = SingleTaskData | PhaseData
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: