@markjaquith/agency 2.52.1 → 2.53.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.52.1",
3
+ "version": "2.53.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -20,6 +20,7 @@
20
20
  "files": [
21
21
  "index.ts",
22
22
  "cli.ts",
23
+ "cli-main.ts",
23
24
  "src",
24
25
  "schemas",
25
26
  "fixtures/protocol",
@@ -292,7 +292,6 @@ describe("strict CLI parsing", () => {
292
292
  [["restore", "task", "one", "two"], "agency restore task"],
293
293
  [["restore", "phase", "one", "two", "three"], "agency restore phase"],
294
294
  [["work", "one", "two"], "agency work"],
295
- [["pr", "create", "one", "two", "three"], "agency pr create"],
296
295
  [["status", "extra"], "agency status"],
297
296
  [["validate", "one", "two"], "agency validate"],
298
297
  [["context", "one", "two"], "agency context"],
@@ -358,15 +357,20 @@ describe("strict CLI parsing", () => {
358
357
  commandName: "work",
359
358
  values: { force: true },
360
359
  })
361
- expect(parseCli(["pr", "create", "example", "--force"])).toMatchObject({
362
- commandName: "pr",
363
- values: { force: true },
364
- })
365
360
  expect(() => parseCli(["work", "prepare", "example", "--force"])).toThrow(
366
361
  "cannot be combined",
367
362
  )
368
363
  })
369
364
 
365
+ test("preserves every argument after pr without parsing it", () => {
366
+ const args = ["create", "--title", "two words", "--", "--literal"]
367
+ expect(parseCli(["--cwd", "/workbase", "pr", ...args])).toEqual({
368
+ commandName: "pr",
369
+ args,
370
+ values: { cwd: "/workbase" },
371
+ })
372
+ })
373
+
370
374
  test("parses reconciliation modes and rejects conflicting modes", () => {
371
375
  expect(parseCli(["sync", "--dry-run", "--json"])).toMatchObject({
372
376
  commandName: "sync",
package/src/cli-parser.ts CHANGED
@@ -897,22 +897,12 @@ const commands = {
897
897
  },
898
898
  },
899
899
  pr: {
900
- usage: "agency pr create <task-id> [phase-id]",
901
- options: {
902
- ...outputOptions,
903
- draft: { type: "boolean" },
904
- force: { type: "boolean" },
905
- task: { type: "string" },
906
- phase: { type: "string" },
907
- },
908
- subcommands: {
909
- create: {
910
- usage:
911
- "agency pr create <task-id> [phase-id] [--draft] [--force] [--json]",
912
- minArgs: 1,
913
- maxArgs: 2,
914
- options: ["draft", "force", "json", "task", "phase"],
915
- },
900
+ usage: "agency pr [args...]",
901
+ options: commonOptions,
902
+ command: {
903
+ usage: "agency pr [args...]",
904
+ minArgs: 0,
905
+ maxArgs: Number.POSITIVE_INFINITY,
916
906
  },
917
907
  },
918
908
  next: {
@@ -1103,7 +1093,6 @@ const targetSlots = (
1103
1093
  if (commandName === "worktree" && subcommand !== "list") {
1104
1094
  return ["task", "phase"]
1105
1095
  }
1106
- if (commandName === "pr" && subcommand === "create") return ["task", "phase"]
1107
1096
  return []
1108
1097
  }
1109
1098
 
@@ -1324,6 +1313,35 @@ export function parseCli(args: readonly string[]): ParsedCli {
1324
1313
  "agency <command> [options]",
1325
1314
  )
1326
1315
  }
1316
+ if (commandName === "pr") {
1317
+ const parsed = parse(
1318
+ args.slice(0, commandIndex),
1319
+ rootOptions,
1320
+ "agency <command> [options]",
1321
+ )
1322
+ assertNoDuplicateOptions(
1323
+ parsed.tokens,
1324
+ new Set(),
1325
+ "agency <command> [options]",
1326
+ )
1327
+ if (parsed.values.silent && parsed.values.verbose) {
1328
+ throw usageError(
1329
+ "Options '--silent' and '--verbose' cannot be combined.",
1330
+ "agency <command> [options]",
1331
+ )
1332
+ }
1333
+ if (parsed.values.workbase && parsed.values.cwd) {
1334
+ throw usageError(
1335
+ "Options '--workbase' and '--cwd' cannot be combined.",
1336
+ "agency <command> [options]",
1337
+ )
1338
+ }
1339
+ return {
1340
+ commandName,
1341
+ args: [...args.slice(commandIndex + 1)],
1342
+ values: parsed.values,
1343
+ }
1344
+ }
1327
1345
  const commandArgs = [
1328
1346
  ...args.slice(0, commandIndex),
1329
1347
  ...args.slice(commandIndex + 1),
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, stat, symlink } from "node:fs/promises"
2
+ import { access, chmod, 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"
@@ -502,7 +502,7 @@ describe("CLI", () => {
502
502
  ["archive", "Usage: agency archive"],
503
503
  ["restore", "Usage: agency restore"],
504
504
  ["work", "Usage: agency work"],
505
- ["pr", "Usage: agency pr"],
505
+ ["pr", "Work with GitHub pull requests."],
506
506
  ["status", "Usage: agency status"],
507
507
  ["validate", "Usage: agency validate"],
508
508
  ["context", "Usage: agency context"],
@@ -537,6 +537,34 @@ describe("CLI", () => {
537
537
  expect(after).toEqual({ exitCode: 0, stdout: "", stderr: "" })
538
538
  }, 30_000)
539
539
 
540
+ test("passes pr arguments and exit status through to gh", async () => {
541
+ const root = await createTempDir()
542
+ tempDirs.push(root)
543
+ const bin = join(root, "bin")
544
+ const capture = join(root, "capture")
545
+ await mkdir(bin)
546
+ await Bun.write(
547
+ join(bin, "gh"),
548
+ `#!/bin/sh
549
+ printf '%s\n' "$PWD" "$@" > "$GH_CAPTURE"
550
+ exit 23
551
+ `,
552
+ )
553
+ await chmod(join(bin, "gh"), 0o755)
554
+ const args = ["pr", "create", "--title", "two words", "--", "literal"]
555
+
556
+ const result = await runCli(args, root, {
557
+ PATH: `${bin}:${process.env.PATH ?? ""}`,
558
+ GH_CAPTURE: capture,
559
+ })
560
+
561
+ expect(result).toEqual({ exitCode: 23, stdout: "", stderr: "" })
562
+ expect((await Bun.file(capture).text()).trim().split("\n")).toEqual([
563
+ await realpath(root),
564
+ ...args,
565
+ ])
566
+ })
567
+
540
568
  test("lists ready work and exposes excluded blockers through one result", async () => {
541
569
  const root = await createTempDir()
542
570
  tempDirs.push(root)
@@ -570,20 +598,6 @@ describe("CLI", () => {
570
598
  blockers: [{ kind: "status", reason: "Task status is dropped" }],
571
599
  },
572
600
  ])
573
-
574
- const blockedPr = await runCli(["pr", "create", "finished", "--json"], root)
575
- expect(blockedPr.exitCode).toBe(1)
576
- expect(blockedPr.stderr).toBe("")
577
- expect(JSON.parse(blockedPr.stdout)).toMatchObject({
578
- ok: false,
579
- error: {
580
- code: "EXECUTION_BLOCKED",
581
- fields: {
582
- status: "dropped",
583
- blockers: [{ kind: "status", reason: "Task status is dropped" }],
584
- },
585
- },
586
- })
587
601
  })
588
602
 
589
603
  test("reports and synchronizes managed integration files", async () => {
@@ -1,45 +1,147 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { Effect } from "effect"
3
- import { captureLogs } from "../test-utils"
4
- import { PullRequestService } from "../services/PullRequestService"
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { chmod, mkdir, realpath } from "node:fs/promises"
3
+ import { dirname, join } from "node:path"
4
+ import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
5
5
  import { pr } from "./pr"
6
6
 
7
+ const write = async (root: string, path: string, content: string) => {
8
+ const fullPath = join(root, path)
9
+ await mkdir(dirname(fullPath), { recursive: true })
10
+ await Bun.write(fullPath, content)
11
+ }
12
+
7
13
  describe("pr command", () => {
8
- test("outputs the created pull request URL as JSON", async () => {
9
- const url = "https://github.com/markjaquith/agency/pull/123"
10
- let received: unknown[] = []
11
- const logs = await captureLogs(() =>
12
- Effect.runPromise(
13
- pr({
14
- subcommand: "create",
15
- taskId: "example",
16
- phaseId: "implementation",
17
- draft: true,
18
- force: true,
19
- cwd: "/workbase",
20
- json: true,
21
- }).pipe(
22
- Effect.provideService(PullRequestService, {
23
- create: (...args: unknown[]) => {
24
- received = args
25
- return Effect.succeed(url)
26
- },
27
- } as never),
28
- ) as Effect.Effect<void, unknown, never>,
29
- ),
14
+ let tempRoot: string
15
+ let capturePath: string
16
+ let originalPath: string | undefined
17
+
18
+ beforeEach(async () => {
19
+ tempRoot = await createTempDir()
20
+ capturePath = join(tempRoot, "capture")
21
+ const bin = join(tempRoot, "bin")
22
+ await mkdir(bin)
23
+ await Bun.write(
24
+ join(bin, "gh"),
25
+ `#!/bin/sh
26
+ printf '%s\n' "$PWD" "$@" > "$GH_CAPTURE"
27
+ exit "\${GH_EXIT:-0}"
28
+ `,
29
+ )
30
+ await chmod(join(bin, "gh"), 0o755)
31
+ originalPath = process.env.PATH
32
+ process.env.PATH = `${bin}:${originalPath ?? ""}`
33
+ process.env.GH_CAPTURE = capturePath
34
+ })
35
+
36
+ afterEach(async () => {
37
+ process.env.PATH = originalPath
38
+ delete process.env.GH_CAPTURE
39
+ delete process.env.GH_EXIT
40
+ await cleanupTempDir(tempRoot)
41
+ })
42
+
43
+ const captured = async () =>
44
+ (await Bun.file(capturePath).text()).trim().split("\n")
45
+
46
+ const createExecutionWorkbase = async () => {
47
+ const root = join(tempRoot, "workbase")
48
+ await write(root, "agency.json", '{"version":2}\n')
49
+ await mkdir(join(root, "repos/agency"), { recursive: true })
50
+ await mkdir(join(root, "tasks/single/code/agency"), { recursive: true })
51
+ await write(
52
+ root,
53
+ "tasks/single/TASK.md",
54
+ `---
55
+ ticketUrl: null
56
+ repo: agency
57
+ branch: feat/single
58
+ base: main
59
+ pr: null
60
+ status: open
61
+ ---
62
+
63
+ # Single
64
+ `,
30
65
  )
66
+ await mkdir(join(root, "tasks/multi/phases/build/code/agency"), {
67
+ recursive: true,
68
+ })
69
+ await write(
70
+ root,
71
+ "tasks/multi/TASK.md",
72
+ `---
73
+ ticketUrl: null
74
+ phases:
75
+ - id: build
76
+ ---
31
77
 
32
- expect(JSON.parse(logs[0]!)).toEqual({ url })
33
- expect(received).toEqual([
34
- "example",
35
- "implementation",
36
- true,
37
- "/workbase",
38
- expect.objectContaining({
39
- force: true,
40
- draft: true,
41
- json: true,
42
- }),
78
+ # Multi
79
+ `,
80
+ )
81
+ await write(
82
+ root,
83
+ "tasks/multi/phases/build/PHASE.md",
84
+ `---
85
+ repo: agency
86
+ branch: feat/build
87
+ base: main
88
+ pr: null
89
+ status: open
90
+ ---
91
+
92
+ # Build
93
+ `,
94
+ )
95
+ return root
96
+ }
97
+
98
+ test("focuses task and descendant invocations on the writable checkout", async () => {
99
+ const root = await createExecutionWorkbase()
100
+ const task = join(root, "tasks/single")
101
+ const descendant = join(task, "notes/deep")
102
+ await mkdir(descendant, { recursive: true })
103
+
104
+ for (const cwd of [task, descendant]) {
105
+ expect(await runTestEffect(pr(["view"], cwd))).toBe(0)
106
+ expect(await captured()).toEqual([
107
+ await realpath(join(task, "code/agency")),
108
+ "pr",
109
+ "view",
110
+ ])
111
+ }
112
+ })
113
+
114
+ test("focuses a phase invocation on its writable checkout", async () => {
115
+ const root = await createExecutionWorkbase()
116
+ const phase = join(root, "tasks/multi/phases/build")
117
+
118
+ expect(await runTestEffect(pr(["status"], phase))).toBe(0)
119
+ expect(await captured()).toEqual([
120
+ await realpath(join(phase, "code/agency")),
121
+ "pr",
122
+ "status",
43
123
  ])
44
124
  })
125
+
126
+ test("falls back to the invocation directory without execution authority", async () => {
127
+ const root = await createExecutionWorkbase()
128
+ const orchestration = join(root, "tasks/multi")
129
+ const outside = join(tempRoot, "outside")
130
+ await mkdir(outside)
131
+
132
+ for (const cwd of [orchestration, outside]) {
133
+ expect(await runTestEffect(pr(["list"], cwd))).toBe(0)
134
+ expect(await captured()).toEqual([await realpath(cwd), "pr", "list"])
135
+ }
136
+ })
137
+
138
+ test("forwards arguments unchanged and returns the gh exit code", async () => {
139
+ const outside = join(tempRoot, "outside")
140
+ await mkdir(outside)
141
+ process.env.GH_EXIT = "23"
142
+ const args = ["create", "--title", "two words", "--", "literal"]
143
+
144
+ expect(await runTestEffect(pr(args, outside))).toBe(23)
145
+ expect(await captured()).toEqual([await realpath(outside), "pr", ...args])
146
+ })
45
147
  })
@@ -1,43 +1,31 @@
1
1
  import { Effect } from "effect"
2
- import type { BaseCommandOptions } from "../utils/command"
3
- import { PullRequestService } from "../services/PullRequestService"
4
- import { createLoggers } from "../utils/effect"
2
+ import { resolve } from "node:path"
3
+ import { ContextService } from "../services/ContextService"
4
+ import { FileSystemService } from "../services/FileSystemService"
5
5
 
6
- interface PrOptions extends BaseCommandOptions {
7
- readonly subcommand?: string
8
- readonly taskId?: string
9
- readonly phaseId?: string
10
- readonly draft?: boolean
11
- readonly force?: boolean
12
- }
13
-
14
- export const pr = (options: PrOptions) =>
6
+ export const pr = (args: readonly string[], cwd: string = process.cwd()) =>
15
7
  Effect.gen(function* () {
16
- if (options.subcommand !== "create" || !options.taskId) {
17
- return yield* Effect.fail(
18
- new Error("Usage: agency pr create <task-id> [phase-id]"),
19
- )
20
- }
21
- const pullRequests = yield* PullRequestService
22
- const { log } = createLoggers(options)
23
- const url = yield* pullRequests.create(
24
- options.taskId,
25
- options.phaseId,
26
- options.draft,
27
- options.cwd ?? process.cwd(),
28
- options,
29
- )
30
- log(options.json ? JSON.stringify({ url }, null, 2) : url)
8
+ const contexts = yield* ContextService
9
+ const fs = yield* FileSystemService
10
+ const invocationCwd = resolve(cwd)
11
+ const context = yield* contexts
12
+ .get({ cwd: invocationCwd, target: ".", compact: true })
13
+ .pipe(Effect.catchAll(() => Effect.succeed(null)))
14
+ const writableCheckout =
15
+ context?.validation.valid && context.workspace?.writable?.materialized
16
+ ? context.authority.writable?.checkoutPath
17
+ : null
18
+ const focusedCwd = writableCheckout ?? invocationCwd
19
+ const result = yield* fs.runCommand(["gh", "pr", ...args], {
20
+ cwd: focusedCwd,
21
+ passthrough: true,
22
+ })
23
+ return result.exitCode
31
24
  })
32
25
 
33
26
  export const help = `
34
- Usage: agency pr create <task-id> [phase-id]
35
-
36
- Push the execution branch, create a pull request with the delivery provider,
37
- and update its task or phase document.
27
+ Usage: agency pr [args...]
38
28
 
39
- Options:
40
- --draft Create a draft pull request
41
- --force Override readiness and terminal-state guards
42
- --json Output the pull request URL as JSON
29
+ Run gh pr unchanged, focusing the writable repository checkout when invoked
30
+ from an Agency execution task or phase.
43
31
  `
@@ -218,25 +218,34 @@ export class FileSystemService extends Effect.Service<FileSystemService>()(
218
218
  readonly cwd?: string
219
219
  readonly captureOutput?: boolean
220
220
  readonly forwardOutput?: boolean
221
+ readonly passthrough?: boolean
221
222
  readonly env?: Record<string, string>
222
223
  },
223
224
  ) =>
224
225
  pipe(
225
226
  spawnProcess(args, {
226
227
  cwd: options?.cwd,
227
- stdin: "pipe",
228
- stdout: options?.forwardOutput
229
- ? "tee"
230
- : options?.captureOutput
231
- ? "pipe"
232
- : "inherit",
233
- stderr: options?.forwardOutput ? "tee" : "pipe",
228
+ stdin: options?.passthrough ? "inherit" : "pipe",
229
+ stdout: options?.passthrough
230
+ ? "inherit"
231
+ : options?.forwardOutput
232
+ ? "tee"
233
+ : options?.captureOutput
234
+ ? "pipe"
235
+ : "inherit",
236
+ stderr: options?.passthrough
237
+ ? "inherit"
238
+ : options?.forwardOutput
239
+ ? "tee"
240
+ : "pipe",
234
241
  env: options?.env,
235
242
  }),
236
243
  Effect.mapError(
237
244
  (processError) =>
238
245
  new FileSystemError({
239
- message: `Failed to run command: ${args.join(" ")}`,
246
+ message: options?.passthrough
247
+ ? processError.message
248
+ : `Failed to run command: ${args.join(" ")}`,
240
249
  cause: processError,
241
250
  }),
242
251
  ),
@@ -34,7 +34,7 @@ class TaskError extends Data.TaggedError("TaskError")<{
34
34
  readonly message: string
35
35
  }> {}
36
36
 
37
- interface TaskRecord {
37
+ export interface TaskRecord {
38
38
  readonly id: string
39
39
  readonly path: string
40
40
  readonly content: string
@@ -7,6 +7,7 @@ import { ClaimService } from "./ClaimService"
7
7
  import { TaskService } from "./TaskService"
8
8
  import { VcsMigrationService } from "./VcsMigrationService"
9
9
  import { WorktreeService } from "./WorktreeService"
10
+ import { runVcsStatusFast } from "../vcs-status-fast"
10
11
 
11
12
  const run = async (args: string[], cwd?: string) => {
12
13
  const child = Bun.spawn(args, { cwd, stdout: "pipe", stderr: "pipe" })
@@ -72,6 +73,51 @@ describe("VcsMigrationService", () => {
72
73
 
73
74
  afterEach(async () => cleanupTempDir(root))
74
75
 
76
+ test("reports same-backend status for materialized workspaces", async () => {
77
+ const status = await runTestEffect(
78
+ VcsMigrationService.pipe(
79
+ Effect.flatMap((service) => service.status(root)),
80
+ ),
81
+ )
82
+ expect(status).toMatchObject({
83
+ configured: "git",
84
+ source: "git",
85
+ target: "git",
86
+ workspaceCount: 2,
87
+ blockers: [],
88
+ })
89
+ })
90
+
91
+ test("fast jj status matches the validated service result", async () => {
92
+ if (!Bun.which("jj")) return
93
+ await runTestEffect(
94
+ VcsMigrationService.pipe(
95
+ Effect.flatMap((service) =>
96
+ service.migrate("jj", root, { apply: true }),
97
+ ),
98
+ ),
99
+ )
100
+ const taskPath = join(root, "tasks/example/TASK.md")
101
+ const content = await Bun.file(taskPath).text()
102
+ const withoutReference = content.replace(
103
+ /\nrepos:\n(?: .+\n)+(?=branch:)/,
104
+ "\n",
105
+ )
106
+ expect(withoutReference).not.toBe(content)
107
+ await Bun.write(taskPath, withoutReference)
108
+
109
+ const validated = await runTestEffect(
110
+ VcsMigrationService.pipe(
111
+ Effect.flatMap((service) => service.status(root)),
112
+ ),
113
+ )
114
+ const output: string[] = []
115
+ expect(
116
+ await runVcsStatusFast(true, root, (line) => output.push(line)),
117
+ ).toBe(true)
118
+ expect(JSON.parse(output.join("\n")).result).toEqual(validated)
119
+ })
120
+
75
121
  test("migrates Git worktrees to jj workspaces and back", async () => {
76
122
  if (!Bun.which("jj")) return
77
123
  const checkout = join(root, "tasks/example/code/agency")
@@ -8,7 +8,7 @@ import {
8
8
  runLifecycleTransaction,
9
9
  } from "./LifecycleTransaction"
10
10
  import { FileSystemService } from "./FileSystemService"
11
- import { PhaseService } from "./PhaseService"
11
+ import { PhaseService, type PhaseRecord } from "./PhaseService"
12
12
  import { RepositoryService } from "./RepositoryService"
13
13
  import { TaskService } from "./TaskService"
14
14
  import {
@@ -182,15 +182,19 @@ const executionRecords = (root: string) =>
182
182
  Effect.gen(function* () {
183
183
  const tasks = yield* TaskService
184
184
  const phases = yield* PhaseService
185
+ const taskRecords = yield* tasks.list(root)
186
+ const phasesByTask = new Map<string, readonly PhaseRecord[]>()
185
187
  const records: {
186
188
  taskId: string
187
189
  phaseId?: string
188
190
  status: WorkStatus
189
191
  claimActive: boolean
190
192
  }[] = []
191
- for (const task of yield* tasks.list(root)) {
193
+ for (const task of taskRecords) {
192
194
  if ("phases" in task.data) {
193
- for (const phase of yield* phases.list(task.id, root)) {
195
+ const phaseRecords = yield* phases.list(task.id, root)
196
+ phasesByTask.set(task.id, phaseRecords)
197
+ for (const phase of phaseRecords) {
194
198
  records.push({
195
199
  taskId: task.id,
196
200
  phaseId: phase.id,
@@ -206,7 +210,7 @@ const executionRecords = (root: string) =>
206
210
  })
207
211
  }
208
212
  }
209
- return records
213
+ return { records, taskRecords, phasesByTask }
210
214
  })
211
215
 
212
216
  const inspectMigration = (startPath: string, requestedTarget?: VcsKind) =>
@@ -241,7 +245,8 @@ const inspectMigration = (startPath: string, requestedTarget?: VcsKind) =>
241
245
  })
242
246
  }
243
247
 
244
- const records = yield* executionRecords(root)
248
+ const execution = yield* executionRecords(root)
249
+ const records = execution.records
245
250
  for (const record of records) {
246
251
  if (record.claimActive) {
247
252
  const label = record.phaseId
@@ -339,7 +344,15 @@ const inspectMigration = (startPath: string, requestedTarget?: VcsKind) =>
339
344
  }
340
345
 
341
346
  const workspacePlans: WorkspacePlan[] = []
342
- const inspected = yield* Effect.either(worktrees.list(root))
347
+ const inspected = yield* Effect.either(
348
+ worktrees.list(root, {
349
+ materializedOnly: !blockers.some(
350
+ (blocker) => blocker.kind === "repository",
351
+ ),
352
+ tasks: execution.taskRecords,
353
+ phasesByTask: execution.phasesByTask,
354
+ }),
355
+ )
343
356
  if (Either.isLeft(inspected)) {
344
357
  blockers.push({
345
358
  kind: "workspace-conflict",
@@ -377,26 +390,27 @@ const inspectMigration = (startPath: string, requestedTarget?: VcsKind) =>
377
390
  continue
378
391
  }
379
392
  const sourceName =
380
- source === "jj"
393
+ source !== target && source === "jj"
381
394
  ? ((yield* sourceBackend.listWorkspaces(
382
395
  join(root, "repos", checkout.repo),
383
396
  )).find((item) => item.path === checkout.registeredPath)
384
397
  ?.name ?? null)
385
398
  : null
386
- const previousBranchCommit = checkout.actualBranch
387
- ? yield* command(
388
- fs,
389
- [
390
- "git",
391
- "-C",
392
- join(root, "repos", checkout.repo),
393
- "rev-parse",
394
- "--verify",
395
- `${checkout.actualBranch}^{commit}`,
396
- ],
397
- `Failed to inspect branch '${checkout.actualBranch}'`,
398
- ).pipe(Effect.catchAll(() => Effect.succeed(null)))
399
- : null
399
+ const previousBranchCommit =
400
+ source !== target && checkout.actualBranch
401
+ ? yield* command(
402
+ fs,
403
+ [
404
+ "git",
405
+ "-C",
406
+ join(root, "repos", checkout.repo),
407
+ "rev-parse",
408
+ "--verify",
409
+ `${checkout.actualBranch}^{commit}`,
410
+ ],
411
+ `Failed to inspect branch '${checkout.actualBranch}'`,
412
+ ).pipe(Effect.catchAll(() => Effect.succeed(null)))
413
+ : null
400
414
  workspacePlans.push({
401
415
  taskId: inspection.owner.taskId,
402
416
  ...(inspection.owner.phaseId