@markjaquith/agency 2.74.0 → 3.1.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 (59) hide show
  1. package/README.md +29 -122
  2. package/cli-main.ts +10 -28
  3. package/cli.ts +1 -17
  4. package/package.json +1 -2
  5. package/schemas/agency-graph-v1.schema.json +1 -1
  6. package/src/cli-parser.test.ts +5 -19
  7. package/src/cli-parser.ts +12 -30
  8. package/src/cli.test.ts +6 -6
  9. package/src/commands/archive.test.ts +73 -1
  10. package/src/commands/archive.ts +18 -7
  11. package/src/commands/context.test.ts +0 -67
  12. package/src/commands/init.test.ts +1 -2
  13. package/src/commands/pr.test.ts +2 -87
  14. package/src/commands/pr.ts +2 -79
  15. package/src/commands/push.test.ts +1 -2
  16. package/src/graph-schema.ts +1 -1
  17. package/src/services/ArchiveBulkService.test.ts +1 -142
  18. package/src/services/ArchiveService.test.ts +1 -172
  19. package/src/services/ArchiveService.ts +59 -27
  20. package/src/services/ContextService.ts +3 -47
  21. package/src/services/DoctorService.ts +8 -24
  22. package/src/services/GraphService.test.ts +1 -0
  23. package/src/services/GraphService.ts +2 -5
  24. package/src/services/PhaseService.ts +70 -191
  25. package/src/services/PullRequestService.test.ts +1 -45
  26. package/src/services/PullRequestService.ts +1 -22
  27. package/src/services/PushService.test.ts +29 -295
  28. package/src/services/PushService.ts +10 -239
  29. package/src/services/RepositoryService.test.ts +1 -61
  30. package/src/services/RepositoryService.ts +2 -13
  31. package/src/services/ReviewService.test.ts +1 -84
  32. package/src/services/ReviewService.ts +9 -48
  33. package/src/services/SyncService.test.ts +0 -81
  34. package/src/services/SyncService.ts +12 -39
  35. package/src/services/TaskPhaseService.test.ts +0 -80
  36. package/src/services/TaskService.ts +1 -9
  37. package/src/services/VersionControlService.test.ts +4 -117
  38. package/src/services/VersionControlService.ts +3 -426
  39. package/src/services/WorkbaseService.test.ts +0 -20
  40. package/src/services/WorkbaseService.ts +1 -18
  41. package/src/services/WorktreeService.test.ts +1 -705
  42. package/src/services/WorktreeService.ts +395 -1604
  43. package/src/services/push-validation.ts +0 -7
  44. package/src/test-utils.ts +0 -4
  45. package/src/workbase/AGENTS.md +4 -5
  46. package/src/workbase/checkout-command.test.ts +1 -1
  47. package/src/workbase/checkout-command.ts +1 -1
  48. package/src/workbase/delivery-command.test.ts +4 -35
  49. package/src/workbase/delivery-command.ts +2 -15
  50. package/src/workbase/schemas.test.ts +0 -19
  51. package/src/workbase/schemas.ts +1 -2
  52. package/src/commands/vcs.test.ts +0 -75
  53. package/src/commands/vcs.ts +0 -72
  54. package/src/services/VcsMigrationService.test.ts +0 -348
  55. package/src/services/VcsMigrationService.ts +0 -857
  56. package/src/vcs-status-fast.ts +0 -310
  57. package/src/workbase/version-control.ts +0 -5
  58. package/src/workbase/workspace-command.test.ts +0 -70
  59. package/src/workbase/workspace-command.ts +0 -63
@@ -219,73 +219,6 @@ Phase prose.
219
219
  )
220
220
  })
221
221
 
222
- test("reports the current jj change without inventing a missing bookmark", async () => {
223
- if (!Bun.which("jj")) return
224
- await cleanupTempDir(root)
225
- root = await createTempDir()
226
- const remote = join(root, "remote.git")
227
- const seed = join(root, "seed")
228
- const repository = join(root, "repos/agency")
229
- const checkout = join(root, "tasks/example/code/agency")
230
- await mkdir(join(root, "repos"), { recursive: true })
231
- await write(root, "agency.json", '{"version":2,"vcs":"jj"}\n')
232
- await run(root, ["git", "init", "--bare", "--initial-branch=main", remote])
233
- await run(root, ["git", "init", "--initial-branch=main", seed])
234
- await run(seed, ["git", "config", "user.email", "test@example.com"])
235
- await run(seed, ["git", "config", "user.name", "Test"])
236
- await Bun.write(join(seed, "README.md"), "example\n")
237
- await run(seed, ["git", "add", "README.md"])
238
- await run(seed, ["git", "commit", "-m", "initial"])
239
- await run(seed, ["git", "remote", "add", "origin", remote])
240
- await run(seed, ["git", "push", "origin", "main"])
241
- await run(root, ["jj", "git", "clone", "--no-colocate", remote, repository])
242
- await write(
243
- root,
244
- "tasks/example/TASK.md",
245
- `---
246
- ticketUrl: null
247
- repo: agency
248
- branch: task/example
249
- base: main
250
- pr: null
251
- status: working
252
- ---
253
-
254
- # Example
255
- `,
256
- )
257
- await mkdir(dirname(checkout), { recursive: true })
258
- await run(repository, [
259
- "jj",
260
- "workspace",
261
- "add",
262
- "--name",
263
- "task-example",
264
- "-r",
265
- "main",
266
- checkout,
267
- ])
268
- await Bun.write(join(checkout, "feature.txt"), "current change\n")
269
- const current = Bun.spawn(
270
- ["jj", "log", "--no-graph", "-r", "@", "-T", "commit_id"],
271
- { cwd: checkout, stdout: "pipe", stderr: "pipe" },
272
- )
273
- const currentCommit = (await new Response(current.stdout).text()).trim()
274
- expect(await current.exited).toBe(0)
275
-
276
- const result = await readContext(root, "tasks/example", false, true)
277
- expect(result.workspace.writable).toMatchObject({
278
- branchCommit: null,
279
- checkoutCommit: currentCommit,
280
- checkoutBranch: null,
281
- detached: true,
282
- dirty: false,
283
- })
284
- expect(result.workspace.warnings).toContain(
285
- `Unable to resolve branch 'task/example' in ${repository}`,
286
- )
287
- })
288
-
289
222
  test("defaults to compact while preserving explicit compact compatibility", async () => {
290
223
  const result = await readContext(
291
224
  root,
@@ -8,7 +8,6 @@ import {
8
8
  runTestEffect,
9
9
  } from "../test-utils"
10
10
  import { init } from "./init"
11
- import { preferredVersionControl } from "../workbase/version-control"
12
11
 
13
12
  describe("init command", () => {
14
13
  let parent: string
@@ -27,7 +26,7 @@ describe("init command", () => {
27
26
 
28
27
  expect(await Bun.file(join(root, "agency.json")).json()).toEqual({
29
28
  version: 2,
30
- vcs: preferredVersionControl(),
29
+ vcs: "git",
31
30
  })
32
31
  for (const directory of ["repos", "epics", "tasks"]) {
33
32
  expect((await stat(join(root, directory))).isDirectory()).toBe(true)
@@ -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, rm } from "node:fs/promises"
3
+ import { chmod, mkdir, realpath } from "node:fs/promises"
4
4
  import { dirname, join } from "node:path"
5
5
  import { PullRequestService } from "../services/PullRequestService"
6
6
  import {
@@ -31,7 +31,6 @@ 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
35
34
  exit "\${GH_EXIT:-0}"
36
35
  `,
37
36
  )
@@ -51,14 +50,13 @@ exit "\${GH_EXIT:-0}"
51
50
  const captured = async () =>
52
51
  (await Bun.file(capturePath).text()).trim().split("\n")
53
52
 
54
- const createExecutionWorkbase = async (vcs: "git" | "jj" = "git") => {
53
+ const createExecutionWorkbase = async () => {
55
54
  const root = join(tempRoot, "workbase")
56
55
  await write(
57
56
  root,
58
57
  "agency.json",
59
58
  `${JSON.stringify({
60
59
  version: 2,
61
- vcs,
62
60
  repositories: {
63
61
  agency: { remote: "https://github.com/example/agency.git" },
64
62
  },
@@ -110,41 +108,6 @@ status: open
110
108
  # Build
111
109
  `,
112
110
  )
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
- }
148
111
  return root
149
112
  }
150
113
 
@@ -261,54 +224,6 @@ status: working
261
224
  ])
262
225
  })
263
226
 
264
- test("injects the execution branch and repository for jj targets", async () => {
265
- const root = await createExecutionWorkbase("jj")
266
- const task = join(root, "tasks/single")
267
- const phase = join(root, "tasks/multi/phases/build")
268
-
269
- expect(await runTestEffect(pr(["view", "--web"], task))).toBe(0)
270
- expect(await captured()).toEqual([
271
- await realpath(join(task, "code/agency")),
272
- "pr",
273
- "view",
274
- "feat/single",
275
- "--repo",
276
- "example/agency",
277
- "--web",
278
- expect.stringMatching(/^GIT_DIR=.*\.jj/),
279
- ])
280
-
281
- expect(
282
- await runTestEffect(pr(["create", "--title", "Example"], phase)),
283
- ).toBe(0)
284
- expect(await captured()).toEqual([
285
- await realpath(join(phase, "code/agency")),
286
- "pr",
287
- "create",
288
- "--head",
289
- "feat/build",
290
- "--repo",
291
- "example/agency",
292
- "--title",
293
- "Example",
294
- expect.stringMatching(/^GIT_DIR=.*\.jj/),
295
- ])
296
- })
297
-
298
- test("preserves explicit jj PR and repository targets", async () => {
299
- const root = await createExecutionWorkbase("jj")
300
- const task = join(root, "tasks/single")
301
- const args = ["view", "123", "--repo", "other/repository"]
302
-
303
- expect(await runTestEffect(pr(args, task))).toBe(0)
304
- expect(await captured()).toEqual([
305
- await realpath(join(task, "code/agency")),
306
- "pr",
307
- ...args,
308
- expect.stringMatching(/^GIT_DIR=.*\.jj/),
309
- ])
310
- })
311
-
312
227
  test("falls back to the invocation directory without execution authority", async () => {
313
228
  const root = await createExecutionWorkbase()
314
229
  const orchestration = join(root, "tasks/multi")
@@ -3,11 +3,8 @@ 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"
7
- import { WorkbaseService } from "../services/WorkbaseService"
8
6
  import type { BaseCommandOptions } from "../utils/command"
9
7
  import { createLoggers } from "../utils/effect"
10
- import { repositoryFromRemote } from "../workbase/delivery-command"
11
8
 
12
9
  interface PrCreateOptions extends BaseCommandOptions {
13
10
  readonly taskId: string
@@ -20,56 +17,6 @@ interface PrCreateOptions extends BaseCommandOptions {
20
17
  readonly labels?: readonly string[]
21
18
  }
22
19
 
23
- const branchTargetCommands = new Set([
24
- "checkout",
25
- "checks",
26
- "close",
27
- "comment",
28
- "diff",
29
- "edit",
30
- "lock",
31
- "merge",
32
- "ready",
33
- "reopen",
34
- "revert",
35
- "review",
36
- "unlock",
37
- "update-branch",
38
- "view",
39
- ])
40
-
41
- const hasOption = (args: readonly string[], short: string, long: string) =>
42
- args.some(
43
- (argument) =>
44
- argument === short ||
45
- argument.startsWith(`${short}=`) ||
46
- argument === long ||
47
- argument.startsWith(`${long}=`),
48
- )
49
-
50
- const withJjContext = (
51
- args: readonly string[],
52
- branch: string,
53
- repository: string,
54
- ) => {
55
- const [command, ...rest] = args
56
- if (!command) return args
57
-
58
- const branchArgs =
59
- (command === "create" || command === "new") &&
60
- !hasOption(rest, "-H", "--head")
61
- ? ["--head", branch]
62
- : branchTargetCommands.has(command) &&
63
- (rest.length === 0 || rest[0]?.startsWith("-"))
64
- ? [branch]
65
- : []
66
- const repositoryArgs = hasOption(rest, "-R", "--repo")
67
- ? []
68
- : ["--repo", repository]
69
-
70
- return [command, ...branchArgs, ...repositoryArgs, ...rest]
71
- }
72
-
73
20
  export const prCreate = (options: PrCreateOptions) =>
74
21
  Effect.gen(function* () {
75
22
  const pullRequests = yield* PullRequestService
@@ -88,8 +35,6 @@ export const pr = (args: readonly string[], cwd: string = process.cwd()) =>
88
35
  Effect.gen(function* () {
89
36
  const contexts = yield* ContextService
90
37
  const fs = yield* FileSystemService
91
- const workbase = yield* WorkbaseService
92
- const versionControl = yield* VersionControlService
93
38
  const invocationCwd = resolve(cwd)
94
39
  const context = yield* contexts
95
40
  .get({ cwd: invocationCwd, target: ".", compact: true })
@@ -104,30 +49,9 @@ export const pr = (args: readonly string[], cwd: string = process.cwd()) =>
104
49
  : null
105
50
  const focusedCheckout = writableCheckout ?? reviewCheckout
106
51
  const focusedCwd = focusedCheckout ?? invocationCwd
107
- let forwardedArgs = args
108
- let environment: Record<string, string> = {}
109
- if (focusedCheckout && context?.workbase.vcs === "jj") {
110
- if (writableCheckout && context.authority.writable) {
111
- const execution =
112
- context.documents?.phase?.data ?? context.documents?.task?.data
113
- const { config } = yield* workbase.loadConfig(context.workbase.root)
114
- const remote =
115
- config.repositories?.[context.authority.writable.repo]?.remote
116
- if (execution && "branch" in execution && remote) {
117
- forwardedArgs = withJjContext(
118
- args,
119
- execution.branch,
120
- repositoryFromRemote(remote),
121
- )
122
- }
123
- }
124
- const backend = yield* versionControl.forWorkbase(context.workbase.root)
125
- environment = yield* backend.gitEnvironment(focusedCheckout)
126
- }
127
- const result = yield* fs.runCommand(["gh", "pr", ...forwardedArgs], {
52
+ const result = yield* fs.runCommand(["gh", "pr", ...args], {
128
53
  cwd: focusedCwd,
129
54
  passthrough: true,
130
- env: environment,
131
55
  })
132
56
  return result.exitCode
133
57
  })
@@ -139,8 +63,7 @@ Usage: agency pr create <task-id> [phase-id] [options]
139
63
  Create records a pull request for an Agency execution unit and accepts the
140
64
  options listed below. Invocations without an Agency task target run gh pr
141
65
  unchanged, focusing the writable repository checkout when invoked from
142
- an Agency execution task or phase. In jj workbases, Agency supplies the declared
143
- branch and repository to gh subcommands that would otherwise infer Git context.
66
+ an Agency execution task or phase.
144
67
 
145
68
  Options:
146
69
  --draft Create the pull request as a draft
@@ -14,13 +14,12 @@ describe("push command", () => {
14
14
  fail: (message) => updates.push(`fail:${message}`),
15
15
  }
16
16
  const result = {
17
- vcs: "jj" as const,
17
+ vcs: "git" as const,
18
18
  taskId: "example",
19
19
  branch: "task/example",
20
20
  base: "main",
21
21
  remote: "origin",
22
22
  tip: "abc123",
23
- changeId: "change",
24
23
  }
25
24
  const logs = await captureLogs(() =>
26
25
  Effect.runPromise(
@@ -57,7 +57,7 @@ export const GraphReadiness = Schema.Struct({
57
57
 
58
58
  export const GraphWorkbase = Schema.Struct({
59
59
  version: Schema.Literal(2),
60
- vcs: Schema.optional(Schema.Literal("git", "jj")),
60
+ vcs: Schema.optional(Schema.Literal("git")),
61
61
  root: Schema.optional(Schema.String),
62
62
  })
63
63
 
@@ -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, rm } from "node:fs/promises"
3
+ import { chmod, mkdir } from "node:fs/promises"
4
4
  import { join } from "node:path"
5
5
  import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
6
  import { ArchiveService } from "./ArchiveService"
@@ -17,13 +17,6 @@ const git = (args: readonly string[], cwd?: string) => {
17
17
  }
18
18
  }
19
19
 
20
- const jj = (args: readonly string[], cwd?: string) => {
21
- const result = Bun.spawnSync(["jj", ...args], { cwd })
22
- if (result.exitCode !== 0) {
23
- throw new Error(new TextDecoder().decode(result.stderr))
24
- }
25
- }
26
-
27
20
  describe("ArchiveService bulk task archive", () => {
28
21
  let root: string
29
22
  let source: string
@@ -430,140 +423,6 @@ claim:
430
423
  ])
431
424
  })
432
425
 
433
- test("skips a dirty jj workspace", async () => {
434
- if (!Bun.which("jj")) return
435
- const repository = join(root, "repos/agency")
436
- await rm(repository, { recursive: true, force: true })
437
- git(["clone", source, repository])
438
- const initialized = Bun.spawnSync([
439
- "jj",
440
- "git",
441
- "init",
442
- "--colocate",
443
- repository,
444
- ])
445
- if (initialized.exitCode !== 0) {
446
- throw new Error(new TextDecoder().decode(initialized.stderr))
447
- }
448
- await Bun.write(
449
- join(root, "agency.json"),
450
- JSON.stringify({ version: 2, vcs: "jj" }),
451
- )
452
- await createTask("jj-dirty")
453
- await dropTask("jj-dirty")
454
- const workspace = await runTestEffect(
455
- WorktreeService.pipe(
456
- Effect.flatMap((service) =>
457
- service.materialize("jj-dirty", undefined, root),
458
- ),
459
- ),
460
- )
461
- await Bun.write(join(workspace.writablePath!, "dirty.txt"), "keep\n")
462
-
463
- const result = await archiveTasks()
464
-
465
- expect(result.tasks[0]).toMatchObject({
466
- id: "jj-dirty",
467
- disposition: "skipped",
468
- reason: { code: "dirty-worktree" },
469
- })
470
- expect(
471
- await Bun.file(join(workspace.writablePath!, "dirty.txt")).exists(),
472
- ).toBe(true)
473
- })
474
-
475
- test("archives a jj working-copy commit preserved by its task bookmark", async () => {
476
- if (!Bun.which("jj")) return
477
- const repository = join(root, "repos/agency")
478
- await rm(repository, { recursive: true, force: true })
479
- git(["clone", source, repository])
480
- jj(["git", "init", "--colocate", repository])
481
- await Bun.write(
482
- join(root, "agency.json"),
483
- JSON.stringify({ version: 2, vcs: "jj" }),
484
- )
485
- await createTask("jj-bookmarked")
486
- await dropTask("jj-bookmarked")
487
- const workspace = await runTestEffect(
488
- WorktreeService.pipe(
489
- Effect.flatMap((service) =>
490
- service.materialize("jj-bookmarked", undefined, root),
491
- ),
492
- ),
493
- )
494
- await Bun.write(join(workspace.writablePath!, "preserved.txt"), "keep\n")
495
- jj(
496
- ["bookmark", "set", "task/jj-bookmarked", "-r", "@"],
497
- workspace.writablePath!,
498
- )
499
-
500
- const preview = await archiveTasks(true)
501
- expect(preview.tasks[0]).toMatchObject({
502
- id: "jj-bookmarked",
503
- disposition: "planned",
504
- removedWorktrees: [workspace.writablePath!],
505
- })
506
- expect(
507
- await Bun.file(join(workspace.writablePath!, "preserved.txt")).text(),
508
- ).toBe("keep\n")
509
-
510
- const result = await archiveTasks()
511
- expect(result.tasks[0]).toMatchObject({
512
- id: "jj-bookmarked",
513
- disposition: "archived",
514
- })
515
- expect(await Bun.file(workspace.writablePath!).exists()).toBe(false)
516
- const preserved = Bun.spawnSync([
517
- "jj",
518
- "-R",
519
- repository,
520
- "file",
521
- "show",
522
- "-r",
523
- "task/jj-bookmarked",
524
- 'root:"preserved.txt"',
525
- ])
526
- if (preserved.exitCode !== 0) {
527
- throw new Error(preserved.stderr.toString())
528
- }
529
- expect(preserved.stdout.toString()).toBe("keep\n")
530
- })
531
-
532
- test("archives a forgotten jj workspace preserved by its task bookmark", async () => {
533
- if (!Bun.which("jj")) return
534
- const repository = join(root, "repos/agency")
535
- await rm(repository, { recursive: true, force: true })
536
- git(["clone", source, repository])
537
- jj(["git", "init", "--colocate", repository])
538
- await Bun.write(
539
- join(root, "agency.json"),
540
- JSON.stringify({ version: 2, vcs: "jj" }),
541
- )
542
- await createTask("jj-stale")
543
- await dropTask("jj-stale")
544
- const workspace = await runTestEffect(
545
- WorktreeService.pipe(
546
- Effect.flatMap((service) =>
547
- service.materialize("jj-stale", undefined, root),
548
- ),
549
- ),
550
- )
551
- await Bun.write(join(workspace.writablePath!, "preserved.txt"), "keep\n")
552
- jj(["bookmark", "set", "task/jj-stale", "-r", "@"], workspace.writablePath!)
553
- jj(["-R", repository, "workspace", "forget", "agency-jj-stale-task-agency"])
554
-
555
- const result = await archiveTasks(true)
556
-
557
- expect(result.tasks[0]).toMatchObject({
558
- id: "jj-stale",
559
- disposition: "planned",
560
- removedWorktrees: [workspace.writablePath!],
561
- })
562
- expect(
563
- await Bun.file(join(workspace.writablePath!, "preserved.txt")).text(),
564
- ).toBe("keep\n")
565
- })
566
-
567
426
  test("rolls back the entire cohort when application fails", async () => {
568
427
  await runTestEffect(
569
428
  EpicService.pipe(