@markjaquith/agency 2.7.0 → 2.7.2

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/cli.ts CHANGED
@@ -240,7 +240,9 @@ const commands: Record<string, Command> = {
240
240
  return
241
241
  }
242
242
  if (args.length > 1) {
243
- throw new Error("Usage: agency work [<directory> | --epic <epic-id>]")
243
+ throw new Error(
244
+ "Usage: agency work [<directory-or-task-id> | --epic <epic-id>]",
245
+ )
244
246
  }
245
247
 
246
248
  await runCommand(
@@ -301,7 +303,7 @@ Commands:
301
303
  phase <subcommand> Manage task phases
302
304
  archive <type> Archive a work item
303
305
  task <subcommand> Manage tasks
304
- work [directory] Work on an epic, task, or phase
306
+ work [directory|task] Work on an epic, task, or phase
305
307
  pr create Create a pull request for an execution unit
306
308
  repo <subcommand> Manage workbase repositories
307
309
  status Show status for the current workbase
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.7.0",
3
+ "version": "2.7.2",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -46,12 +46,14 @@ interface HarnessOptions {
46
46
  readonly phaseRecords?: readonly any[]
47
47
  readonly outsideWorkbase?: boolean
48
48
  readonly registeredWorkbases?: readonly string[]
49
+ readonly existingDirectories?: readonly string[]
49
50
  }
50
51
 
51
52
  const createHarness = (options: HarnessOptions = {}) => {
52
53
  const events: string[] = []
53
54
  const probes: string[] = []
54
55
  const statusUpdates: string[] = []
56
+ const shownTasks: string[] = []
55
57
  const progressUpdates: string[] = []
56
58
  const launches: Array<{
57
59
  cli: string
@@ -95,14 +97,16 @@ const createHarness = (options: HarnessOptions = {}) => {
95
97
  list: () => Effect.succeed(options.epicRecords ?? []),
96
98
  }
97
99
  const tasks = {
98
- show: (id: string) =>
99
- Effect.succeed({
100
+ show: (id: string) => {
101
+ shownTasks.push(id)
102
+ return Effect.succeed({
100
103
  id,
101
104
  path: `/workbase/tasks/${id}/TASK.md`,
102
105
  data: options.multiPhaseTasks?.includes(id)
103
106
  ? { phases: [] }
104
107
  : { repo: "agency", branch: `task/${id}`, base: "main" },
105
- }),
108
+ })
109
+ },
106
110
  list: () => Effect.succeed(options.taskRecords ?? []),
107
111
  setStatus: (id: string, status: string) => {
108
112
  statusUpdates.push(`task:${id}:${status}`)
@@ -124,7 +128,8 @@ const createHarness = (options: HarnessOptions = {}) => {
124
128
  },
125
129
  }
126
130
  const fs = {
127
- isDirectory: () => Effect.succeed(true),
131
+ isDirectory: (path: string) =>
132
+ Effect.succeed(options.existingDirectories?.includes(path) ?? true),
128
133
  runCommand: (args: readonly string[]) => {
129
134
  const cli = args[1] as "opencode" | "claude"
130
135
  events.push(`probe:${cli}`)
@@ -169,6 +174,7 @@ const createHarness = (options: HarnessOptions = {}) => {
169
174
  launches,
170
175
  materializeOptions,
171
176
  statusUpdates,
177
+ shownTasks,
172
178
  progressUpdates,
173
179
  run,
174
180
  }
@@ -187,11 +193,44 @@ describe("work command", () => {
187
193
  expect(harness.events).toEqual(["probe:opencode", "launch:opencode"])
188
194
  expect(harness.launches[0]).toEqual({
189
195
  cli: "opencode",
190
- args: ["opencode", "--continue"],
196
+ args: [
197
+ "opencode",
198
+ "--continue",
199
+ "--prompt",
200
+ "Work on the epic. Read /workbase/epics/delivery/EPIC.md.",
201
+ ],
191
202
  cwd: "/workbase/epics/delivery",
192
203
  })
193
204
  })
194
205
 
206
+ test("resolves an existing positional path before treating it as a task ID", async () => {
207
+ const harness = createHarness({
208
+ existingDirectories: ["/workbase/tasks/delivery"],
209
+ })
210
+
211
+ await harness.run({
212
+ cwd: "/workbase/tasks/delivery",
213
+ directory: ".",
214
+ opencode: true,
215
+ })
216
+
217
+ expect(harness.shownTasks).toEqual(["delivery"])
218
+ expect(harness.launches[0]?.cwd).toBe(singlePhaseWorkspace.writablePath)
219
+ })
220
+
221
+ test("treats a positional value as a task ID when it is not a directory", async () => {
222
+ const harness = createHarness({ existingDirectories: [] })
223
+
224
+ await harness.run({
225
+ cwd: "/workbase",
226
+ directory: "delivery",
227
+ opencode: true,
228
+ })
229
+
230
+ expect(harness.shownTasks).toEqual(["delivery"])
231
+ expect(harness.launches[0]?.cwd).toBe(singlePhaseWorkspace.writablePath)
232
+ })
233
+
195
234
  test("launches a multi-phase task agent without materializing", async () => {
196
235
  const harness = createHarness({ multiPhaseTasks: ["delivery"] })
197
236
 
@@ -204,7 +243,12 @@ describe("work command", () => {
204
243
  expect(harness.events).toEqual(["probe:opencode", "launch:opencode"])
205
244
  expect(harness.launches[0]).toEqual({
206
245
  cli: "opencode",
207
- args: ["opencode", "--continue"],
246
+ args: [
247
+ "opencode",
248
+ "--continue",
249
+ "--prompt",
250
+ "Work on the task. Read /workbase/tasks/delivery/TASK.md.",
251
+ ],
208
252
  cwd: "/workbase/tasks/delivery",
209
253
  })
210
254
  })
@@ -223,7 +267,16 @@ describe("work command", () => {
223
267
  "probe:opencode",
224
268
  "launch:opencode",
225
269
  ])
226
- expect(harness.launches[0]?.args).toEqual(["opencode", "--continue"])
270
+ expect(harness.launches[0]).toEqual({
271
+ cli: "opencode",
272
+ args: [
273
+ "opencode",
274
+ "--continue",
275
+ "--prompt",
276
+ "Start the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
277
+ ],
278
+ cwd: multiPhaseWorkspace.writablePath,
279
+ })
227
280
  expect(harness.statusUpdates).toEqual([
228
281
  "phase:example:implementation:working",
229
282
  ])
@@ -239,7 +292,7 @@ describe("work command", () => {
239
292
  })
240
293
 
241
294
  expect(harness.events[0]).toBe("materialize")
242
- expect(harness.launches[0]?.cwd).toBe("/workbase/tasks/example")
295
+ expect(harness.launches[0]?.cwd).toBe(singlePhaseWorkspace.writablePath)
243
296
  })
244
297
 
245
298
  test("selects a target with fzf when no directory is provided", async () => {
@@ -273,9 +326,7 @@ describe("work command", () => {
273
326
  "probe:opencode",
274
327
  "launch:opencode",
275
328
  ])
276
- expect(harness.launches[0]?.cwd).toBe(
277
- "/workbase/tasks/delivery/phases/build",
278
- )
329
+ expect(harness.launches[0]?.cwd).toBe(multiPhaseWorkspace.writablePath)
279
330
  })
280
331
 
281
332
  test("selects a registered workbase when local discovery fails", async () => {
@@ -353,7 +404,7 @@ describe("work command", () => {
353
404
  expect(harness.events).toEqual([])
354
405
  })
355
406
 
356
- test("launches OpenCode in the task directory and continues its session", async () => {
407
+ test("launches OpenCode in the writable checkout with explicit context", async () => {
357
408
  const harness = createHarness()
358
409
 
359
410
  await harness.run({ taskId: "example", opencode: true })
@@ -366,8 +417,13 @@ describe("work command", () => {
366
417
  expect(harness.launches).toEqual([
367
418
  {
368
419
  cli: "opencode",
369
- args: ["opencode", "--continue"],
370
- cwd: "/workbase/tasks/example",
420
+ args: [
421
+ "opencode",
422
+ "--continue",
423
+ "--prompt",
424
+ "Start the task. Read /workbase/tasks/example/TASK.md.",
425
+ ],
426
+ cwd: singlePhaseWorkspace.writablePath,
371
427
  },
372
428
  ])
373
429
  expect(harness.statusUpdates).toEqual(["task:example:working"])
@@ -377,7 +433,7 @@ describe("work command", () => {
377
433
  ])
378
434
  })
379
435
 
380
- test("continues OpenCode for a multi-phase task", async () => {
436
+ test("continues OpenCode with explicit task and phase context", async () => {
381
437
  const harness = createHarness({ workspace: multiPhaseWorkspace })
382
438
 
383
439
  await harness.run({
@@ -386,7 +442,16 @@ describe("work command", () => {
386
442
  opencode: true,
387
443
  })
388
444
 
389
- expect(harness.launches[0]?.args).toEqual(["opencode", "--continue"])
445
+ expect(harness.launches[0]).toEqual({
446
+ cli: "opencode",
447
+ args: [
448
+ "opencode",
449
+ "--continue",
450
+ "--prompt",
451
+ "Start the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
452
+ ],
453
+ cwd: multiPhaseWorkspace.writablePath,
454
+ })
390
455
  })
391
456
 
392
457
  test("automatically falls back to Claude", async () => {
@@ -398,7 +463,7 @@ describe("work command", () => {
398
463
  expect(harness.launches[0]).toEqual({
399
464
  cli: "claude",
400
465
  args: ["claude", "Start the task. Read /workbase/tasks/example/TASK.md."],
401
- cwd: "/workbase/tasks/example",
466
+ cwd: singlePhaseWorkspace.writablePath,
402
467
  })
403
468
  })
404
469
 
@@ -422,7 +487,7 @@ describe("work command", () => {
422
487
  expect(harness.launches[0]).toEqual({
423
488
  cli: "claude",
424
489
  args: ["claude", "Start the task. Read /workbase/tasks/example/TASK.md."],
425
- cwd: "/workbase/tasks/example",
490
+ cwd: singlePhaseWorkspace.writablePath,
426
491
  })
427
492
  })
428
493
 
@@ -459,7 +524,7 @@ describe("work command", () => {
459
524
  verboseHarness.run({ taskId: "example", verbose: true }),
460
525
  )
461
526
  expect(verboseLogs).toEqual([
462
- "Launching command: opencode --continue (cwd: /workbase/tasks/example)",
527
+ "Launching command: opencode --continue --prompt 'Start the task. Read /workbase/tasks/example/TASK.md.' (cwd: /workbase/tasks/example/code/agency)",
463
528
  ])
464
529
  expect(verboseHarness.materializeOptions[0]?.verbose).toBe(true)
465
530
 
@@ -77,12 +77,13 @@ export const work = (
77
77
  const phases = yield* PhaseService
78
78
  const { log, verboseLog } = createLoggers(options)
79
79
  const cwd = options.cwd ?? process.cwd()
80
- const startPath = options.directory ? resolve(cwd, options.directory) : cwd
81
- if (options.directory && !(yield* fs.isDirectory(startPath))) {
82
- return yield* Effect.fail(
83
- new Error(`Work directory does not exist: ${startPath}`),
84
- )
85
- }
80
+ const directoryPath = options.directory
81
+ ? resolve(cwd, options.directory)
82
+ : undefined
83
+ const isDirectory = directoryPath
84
+ ? yield* fs.isDirectory(directoryPath)
85
+ : false
86
+ const startPath = isDirectory && directoryPath ? directoryPath : cwd
86
87
  const root = yield* resolveWorkbase(startPath, log, pickBase)
87
88
  if (!root) return
88
89
 
@@ -108,7 +109,15 @@ export const work = (
108
109
  multiPhase: "phases" in task.data,
109
110
  }
110
111
  }
111
- } else if (options.directory) {
112
+ } else if (options.directory && !isDirectory) {
113
+ const task = yield* tasks.show(options.directory, root)
114
+ target = {
115
+ kind: "task",
116
+ taskId: task.id,
117
+ path: task.path,
118
+ multiPhase: "phases" in task.data,
119
+ }
120
+ } else if (directoryPath) {
112
121
  const path = relative(root, startPath)
113
122
  const parts =
114
123
  !path || isAbsolute(path) || path.startsWith(`..${sep}`)
@@ -197,7 +206,7 @@ export const work = (
197
206
  prompt = workspace.phasePath
198
207
  ? `Start the task. Read ${workspace.taskPath} and ${workspace.phasePath}.`
199
208
  : `Start the task. Read ${workspace.taskPath}.`
200
- launchPath = dirname(target.path)
209
+ launchPath = workspace.writablePath
201
210
  }
202
211
 
203
212
  const requested = options.claude ? "claude" : "opencode"
@@ -218,7 +227,8 @@ export const work = (
218
227
  yield* tasks.setStatus(target.taskId, "working", root)
219
228
  }
220
229
 
221
- const args = cli === "opencode" ? ["--continue"] : [prompt]
230
+ const args =
231
+ cli === "opencode" ? ["--continue", "--prompt", prompt] : [prompt]
222
232
  verboseLog(
223
233
  `Launching command: ${formatCommand([cli, ...args])} (cwd: ${launchPath})`,
224
234
  )
@@ -226,11 +236,12 @@ export const work = (
226
236
  })
227
237
 
228
238
  export const help = `
229
- Usage: agency work [<directory> | --epic <epic-id>]
239
+ Usage: agency work [<directory-or-task-id> | --epic <epic-id>]
230
240
 
231
241
  Launch an agent for an epic, task, or phase. With no directory, select one
232
- with fzf. Use '.' for the current directory. Outside a workbase, select a
233
- registered workbase first.
242
+ with fzf. A positional argument resolves as a directory first, then as a task
243
+ ID. Use '.' for the current directory. Outside a workbase, select a registered
244
+ workbase first.
234
245
 
235
246
  Options:
236
247
  --epic <id> Work on an epic
@@ -314,6 +314,20 @@ process.exit(${exitCode})
314
314
  expect(await Bun.file(ghCallPath).exists()).toBe(false)
315
315
  })
316
316
 
317
+ test("blocks PR creation when workbase validation fails", async () => {
318
+ await createTask()
319
+ await materialize()
320
+ await mkdir(join(root, "tasks", "missing-document"), { recursive: true })
321
+ await writeFakeGh({ stdout: "https://github.com/example/agency/pull/45" })
322
+
323
+ await expect(createPullRequest()).rejects.toThrow(
324
+ "Required document is missing",
325
+ )
326
+
327
+ await expectRemoteBranch("task/example", false)
328
+ expect(await Bun.file(ghCallPath).exists()).toBe(false)
329
+ })
330
+
317
331
  test("reports push failure and does not invoke gh", async () => {
318
332
  await createTask()
319
333
  await materialize()
@@ -85,7 +85,7 @@ describe("WorktreeService", () => {
85
85
  expect(new TextDecoder().decode(branch.stdout).trim()).toBe("task/example")
86
86
  })
87
87
 
88
- test("does not fetch origins for existing worktrees", async () => {
88
+ test("does not fetch the origin for an existing writable worktree", async () => {
89
89
  await runTestEffect(
90
90
  TaskService.pipe(
91
91
  Effect.flatMap((service) =>
@@ -94,7 +94,6 @@ describe("WorktreeService", () => {
94
94
  id: "existing",
95
95
  ticketUrl: "https://example.com/task",
96
96
  repo: "agency",
97
- repos: [{ repo: "effect", ref: "main" }],
98
97
  branch: "task/existing",
99
98
  base: "main",
100
99
  },
@@ -114,6 +113,50 @@ describe("WorktreeService", () => {
114
113
  ["remote", "set-url", "origin", join(root, "missing")],
115
114
  join(root, "repos/agency"),
116
115
  )
116
+ await expect(
117
+ runTestEffect(
118
+ WorktreeService.pipe(
119
+ Effect.flatMap((service) =>
120
+ service.materialize("existing", undefined, root),
121
+ ),
122
+ ),
123
+ ),
124
+ ).resolves.toMatchObject({ repo: "agency" })
125
+ })
126
+
127
+ test("reuses an immutable reference checkout without fetching", async () => {
128
+ const commit = new TextDecoder()
129
+ .decode(
130
+ Bun.spawnSync(
131
+ ["git", "-C", join(root, "repos/effect"), "rev-parse", "main"],
132
+ { stdout: "pipe" },
133
+ ).stdout,
134
+ )
135
+ .trim()
136
+ await runTestEffect(
137
+ TaskService.pipe(
138
+ Effect.flatMap((service) =>
139
+ service.create(
140
+ {
141
+ id: "immutable-reference",
142
+ ticketUrl: "https://example.com/task",
143
+ repo: "agency",
144
+ repos: [{ repo: "effect", ref: commit }],
145
+ branch: "task/immutable-reference",
146
+ base: "main",
147
+ },
148
+ root,
149
+ ),
150
+ ),
151
+ ),
152
+ )
153
+ await runTestEffect(
154
+ WorktreeService.pipe(
155
+ Effect.flatMap((service) =>
156
+ service.materialize("immutable-reference", undefined, root),
157
+ ),
158
+ ),
159
+ )
117
160
  await git(
118
161
  ["remote", "set-url", "origin", join(root, "missing")],
119
162
  join(root, "repos/effect"),
@@ -123,11 +166,44 @@ describe("WorktreeService", () => {
123
166
  runTestEffect(
124
167
  WorktreeService.pipe(
125
168
  Effect.flatMap((service) =>
126
- service.materialize("existing", undefined, root),
169
+ service.materialize("immutable-reference", undefined, root),
127
170
  ),
128
171
  ),
129
172
  ),
130
- ).resolves.toMatchObject({ repo: "agency" })
173
+ ).resolves.toMatchObject({ repos: [{ repo: "effect", ref: commit }] })
174
+ })
175
+
176
+ test("stops before materializing when workbase validation fails", async () => {
177
+ await runTestEffect(
178
+ TaskService.pipe(
179
+ Effect.flatMap((service) =>
180
+ service.create(
181
+ {
182
+ id: "valid-target",
183
+ ticketUrl: "https://example.com/task",
184
+ repo: "agency",
185
+ branch: "task/valid-target",
186
+ base: "main",
187
+ },
188
+ root,
189
+ ),
190
+ ),
191
+ ),
192
+ )
193
+ await mkdir(join(root, "tasks", "missing-document"), { recursive: true })
194
+
195
+ await expect(
196
+ runTestEffect(
197
+ WorktreeService.pipe(
198
+ Effect.flatMap((service) =>
199
+ service.materialize("valid-target", undefined, root),
200
+ ),
201
+ ),
202
+ ),
203
+ ).rejects.toThrow("Required document is missing")
204
+ expect(
205
+ await Bun.file(join(root, "tasks", "valid-target", "code")).exists(),
206
+ ).toBe(false)
131
207
  })
132
208
 
133
209
  test("uses a configured worktree creation command", async () => {
@@ -330,7 +406,7 @@ describe("WorktreeService", () => {
330
406
  repo: "missing",
331
407
  base: "main",
332
408
  config: { version: 2 },
333
- expected: "Repository alias 'missing' does not exist",
409
+ expected: "Unknown repository alias 'missing'",
334
410
  },
335
411
  {
336
412
  id: "bad-base",
@@ -392,6 +468,7 @@ pr: null
392
468
  ),
393
469
  ),
394
470
  ).rejects.toThrow(fixture.expected)
471
+ await rm(taskDirectory, { recursive: true, force: true })
395
472
  }
396
473
  })
397
474
 
@@ -561,7 +638,7 @@ pr: null
561
638
  ).rejects.toThrow("is not registered to branch 'task/expected'")
562
639
  })
563
640
 
564
- test("rejects a reused reference checkout after its ref advances", async () => {
641
+ test("fetches a moving ref before checking a reused reference checkout", async () => {
565
642
  await runTestEffect(
566
643
  TaskService.pipe(
567
644
  Effect.flatMap((service) =>
@@ -570,7 +647,7 @@ pr: null
570
647
  id: "pinned-reference",
571
648
  ticketUrl: "https://example.com/task",
572
649
  repo: "agency",
573
- repos: [{ repo: "effect", ref: "main" }],
650
+ repos: [{ repo: "effect", ref: "origin/main" }],
574
651
  branch: "task/pinned-reference",
575
652
  base: "main",
576
653
  },
@@ -590,7 +667,6 @@ pr: null
590
667
  await Bun.write(join(source, "README.md"), "updated\n")
591
668
  await git(["add", "README.md"], source)
592
669
  await git(["-c", "commit.gpgsign=false", "commit", "-m", "update"], source)
593
- await git(["push", join(root, "repos/effect"), "main"], source)
594
670
 
595
671
  await expect(
596
672
  runTestEffect(
@@ -600,7 +676,7 @@ pr: null
600
676
  ),
601
677
  ),
602
678
  ),
603
- ).rejects.toThrow("does not match reference 'main'")
679
+ ).rejects.toThrow("does not match reference 'origin/main'")
604
680
  })
605
681
 
606
682
  test("rejects a reference checkout attached to a branch", async () => {
@@ -60,6 +60,11 @@ const formatCommand = (args: readonly string[]) =>
60
60
  )
61
61
  .join(" ")
62
62
 
63
+ const isCommitId = (ref: string) => /^[0-9a-f]{40,64}$/i.test(ref)
64
+
65
+ const originRef = (ref: string) =>
66
+ ref.replace(/^refs\/remotes\/origin\//, "").replace(/^origin\//, "")
67
+
63
68
  export class WorktreeService extends Effect.Service<WorktreeService>()(
64
69
  "WorktreeService",
65
70
  {
@@ -80,12 +85,10 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
80
85
  options.verbose === true && !options.silent && !options.json
81
86
  const { root, config } = yield* workbase.loadConfig(startPath)
82
87
  const report = yield* workbase.validate(root)
83
- const ownershipIssue = report.issues.find((issue) =>
84
- issue.message.startsWith("Writable branch "),
85
- )
86
- if (ownershipIssue) {
88
+ const validationIssue = report.issues[0]
89
+ if (validationIssue) {
87
90
  return yield* new WorktreeError({
88
- message: `${ownershipIssue.path}: ${ownershipIssue.message}`,
91
+ message: `${validationIssue.path}: ${validationIssue.message}`,
89
92
  })
90
93
  }
91
94
  const task = yield* tasks.show(taskId, root)
@@ -136,23 +139,32 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
136
139
  })
137
140
  }
138
141
 
139
- const fetchOrigin = Effect.gen(function* () {
140
- const remote = yield* fs.runCommand(
141
- ["git", "-C", repositoryPath, "remote", "get-url", "origin"],
142
- { captureOutput: true },
143
- )
144
- if (remote.exitCode !== 0) return
142
+ const fetchOrigin = (ref?: string) =>
143
+ Effect.gen(function* () {
144
+ const remote = yield* fs.runCommand(
145
+ ["git", "-C", repositoryPath, "remote", "get-url", "origin"],
146
+ { captureOutput: true },
147
+ )
148
+ if (remote.exitCode !== 0) return false
145
149
 
146
- const fetch = yield* fs.runCommand(
147
- ["git", "-C", repositoryPath, "fetch", "origin"],
148
- { captureOutput: true },
149
- )
150
- if (fetch.exitCode !== 0) {
151
- return yield* new WorktreeError({
152
- message: `Failed to fetch '${alias}': ${fetch.stderr}`,
153
- })
154
- }
155
- })
150
+ const fetch = yield* fs.runCommand(
151
+ [
152
+ "git",
153
+ "-C",
154
+ repositoryPath,
155
+ "fetch",
156
+ "origin",
157
+ ...(ref ? [ref] : []),
158
+ ],
159
+ { captureOutput: true },
160
+ )
161
+ if (fetch.exitCode !== 0) {
162
+ return yield* new WorktreeError({
163
+ message: `Failed to fetch '${alias}': ${fetch.stderr}`,
164
+ })
165
+ }
166
+ return true
167
+ })
156
168
 
157
169
  const listed = yield* fs.runCommand(
158
170
  [
@@ -210,7 +222,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
210
222
  message: `Worktree registry contains a missing checkout at ${checkoutPath}`,
211
223
  })
212
224
  }
213
- yield* fetchOrigin
225
+ yield* fetchOrigin()
214
226
 
215
227
  let args: string[]
216
228
  let env: Record<string, string> | undefined
@@ -297,6 +309,10 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
297
309
  })
298
310
  }
299
311
  } else {
312
+ const fetched = isCommitId(checkout.ref)
313
+ ? false
314
+ : yield* fetchOrigin(originRef(checkout.ref))
315
+ const resolvedRefName = fetched ? "FETCH_HEAD" : checkout.ref
300
316
  const resolvedRef = yield* fs.runCommand(
301
317
  [
302
318
  "git",
@@ -304,7 +320,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
304
320
  repositoryPath,
305
321
  "rev-parse",
306
322
  "--verify",
307
- `${checkout.ref}^{commit}`,
323
+ `${resolvedRefName}^{commit}`,
308
324
  ],
309
325
  { captureOutput: true },
310
326
  )
@@ -344,7 +360,6 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
344
360
  message: `Worktree registry contains a missing checkout at ${checkoutPath}`,
345
361
  })
346
362
  }
347
- yield* fetchOrigin
348
363
  const result = yield* fs.runCommand(
349
364
  [
350
365
  "git",