@markjaquith/agency 2.7.1 → 2.7.3

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.
@@ -81,14 +81,16 @@ export const task = (
81
81
  const cwd = options.cwd ?? process.cwd()
82
82
 
83
83
  switch (options.subcommand) {
84
- case "new":
85
- case "create": {
86
- const interactive = options.subcommand === "new" || !options.args[0]
84
+ case "new": {
85
+ if (options.inputAllowed === false) {
86
+ return yield* Effect.fail(
87
+ new Error(
88
+ "task new requires interactive input; use 'agency task create <id> --repo <alias>' for automation",
89
+ ),
90
+ )
91
+ }
87
92
  const id =
88
- options.args[0] ??
89
- (interactive
90
- ? (yield* interaction.text("Task ID: ")).trim()
91
- : undefined)
93
+ options.args[0] ?? (yield* interaction.text("Task ID: ")).trim()
92
94
  if (!id) {
93
95
  return yield* Effect.fail(new Error("Task ID is required"))
94
96
  }
@@ -99,42 +101,39 @@ export const task = (
99
101
  let multiPhase = options.multiPhase ?? false
100
102
  let repo = options.repo
101
103
 
102
- if (interactive) {
103
- if (options.ticketUrl === undefined) {
104
- ticketUrl =
105
- (yield* interaction.text("Ticket URL (optional): ")).trim() ||
106
- null
107
- }
108
- if (options.description === undefined) {
109
- description =
110
- (yield* interaction.text("Description (optional): ")).trim() ||
111
- undefined
112
- }
113
- if (options.epic === undefined) {
114
- const epicRecords = yield* epics.list(cwd)
115
- if (epicRecords.length > 0) {
116
- const none = "(none)"
117
- const selected = yield* interaction.select("Parent epic", [
118
- none,
119
- ...epicRecords.map((record) => record.id),
120
- ])
121
- if (selected === null) {
122
- return yield* Effect.fail(new Error("Task creation cancelled"))
123
- }
124
- epic = selected === none ? undefined : selected
125
- }
126
- }
127
- if (options.multiPhase === undefined) {
128
- const selected = yield* interaction.select("Task type", [
129
- "single-phase",
130
- "multi-phase",
104
+ if (options.ticketUrl === undefined) {
105
+ ticketUrl =
106
+ (yield* interaction.text("Ticket URL (optional): ")).trim() || null
107
+ }
108
+ if (options.description === undefined) {
109
+ description =
110
+ (yield* interaction.text("Description (optional): ")).trim() ||
111
+ undefined
112
+ }
113
+ if (options.epic === undefined) {
114
+ const epicRecords = yield* epics.list(cwd)
115
+ if (epicRecords.length > 0) {
116
+ const none = "(none)"
117
+ const selected = yield* interaction.select("Parent epic", [
118
+ none,
119
+ ...epicRecords.map((record) => record.id),
131
120
  ])
132
121
  if (selected === null) {
133
122
  return yield* Effect.fail(new Error("Task creation cancelled"))
134
123
  }
135
- multiPhase = selected === "multi-phase"
124
+ epic = selected === none ? undefined : selected
136
125
  }
137
126
  }
127
+ if (options.multiPhase === undefined) {
128
+ const selected = yield* interaction.select("Task type", [
129
+ "single-phase",
130
+ "multi-phase",
131
+ ])
132
+ if (selected === null) {
133
+ return yield* Effect.fail(new Error("Task creation cancelled"))
134
+ }
135
+ multiPhase = selected === "multi-phase"
136
+ }
138
137
 
139
138
  if (!multiPhase && !repo) {
140
139
  const records = yield* repositories.list(cwd)
@@ -177,6 +176,39 @@ export const task = (
177
176
  )
178
177
  return
179
178
  }
179
+ case "create": {
180
+ const id = options.args[0]
181
+ if (!id) {
182
+ return yield* Effect.fail(new Error("Task ID is required"))
183
+ }
184
+ const multiPhase = options.multiPhase ?? false
185
+ if (!multiPhase && !options.repo) {
186
+ return yield* Effect.fail(
187
+ new Error("Writable repository is required for task create"),
188
+ )
189
+ }
190
+ const record = yield* tasks.create(
191
+ {
192
+ id,
193
+ ticketUrl: options.ticketUrl?.trim() || null,
194
+ description: options.description?.trim() || undefined,
195
+ epic: options.epic,
196
+ multiPhase,
197
+ repo: options.repo,
198
+ repos: parseRepositoryReferences(options.references),
199
+ branch: multiPhase ? undefined : (options.branch ?? `task/${id}`),
200
+ base: multiPhase ? undefined : (options.base ?? "main"),
201
+ },
202
+ cwd,
203
+ )
204
+ const { content: _, ...output } = record
205
+ log(
206
+ options.json
207
+ ? JSON.stringify(output, null, 2)
208
+ : `Created task '${record.id}'`,
209
+ )
210
+ return
211
+ }
180
212
  case "list": {
181
213
  const records = yield* tasks.list(cwd)
182
214
  if (options.json) {
@@ -234,7 +266,7 @@ Usage: agency task <subcommand>
234
266
 
235
267
  Subcommands:
236
268
  new [id] Create a task with guided input
237
- create <id> Create a task; omitted metadata uses defaults
269
+ create <id> Create a task without prompting
238
270
  list List tasks
239
271
  show <id> Show a task
240
272
  status <id> <status> Set open, working, delegated, done, or dropped
@@ -250,6 +282,11 @@ Create options:
250
282
  --base <name> Base branch (default: main)
251
283
  --multi-phase Create a task container for phases
252
284
 
285
+ Task creation is noninteractive. Single-phase tasks require --repo; use
286
+ --multi-phase instead for a task container. Guided input is available only
287
+ through task new, which fails when --no-input is set or no TTY is available.
288
+
253
289
  Options:
254
290
  --json Output results as JSON
291
+ --no-input Never open interactive task creation
255
292
  `
@@ -108,6 +108,34 @@ pr: null
108
108
  expect(discovered).toEqual(["/outside", "/selected"])
109
109
  })
110
110
 
111
+ test("does not select a registered workbase when input is disabled", async () => {
112
+ const workbase = {
113
+ discover: () =>
114
+ Effect.fail({
115
+ _tag: "WorkbaseNotFoundError" as const,
116
+ message: "No Agency workbase found from /outside",
117
+ }),
118
+ listRegistered: () => Effect.succeed(["/selected"]),
119
+ }
120
+ const fs = {
121
+ runCommand: () => Effect.fail(new Error("unexpected fzf probe")),
122
+ }
123
+ const pick: PickWorkbase = () =>
124
+ Effect.fail(new Error("unexpected workbase selection"))
125
+
126
+ await expect(
127
+ Effect.runPromise(
128
+ validate(
129
+ { cwd: "/outside", silent: true, inputAllowed: false },
130
+ pick,
131
+ ).pipe(
132
+ Effect.provideService(WorkbaseService, workbase as never),
133
+ Effect.provideService(FileSystemService, fs as never),
134
+ ) as Effect.Effect<void, unknown, never>,
135
+ ),
136
+ ).rejects.toThrow("provide an explicit path or run Agency from a workbase")
137
+ })
138
+
111
139
  test("outputs the validation report as JSON", async () => {
112
140
  await Bun.write(
113
141
  join(root, "tasks/example/TASK.md"),
@@ -27,7 +27,12 @@ export const validate = (
27
27
  const startPath = options.path ?? options.cwd ?? process.cwd()
28
28
  const root = options.path
29
29
  ? yield* workbase.discover(startPath)
30
- : yield* resolveWorkbase(startPath, log, pick)
30
+ : yield* resolveWorkbase(
31
+ startPath,
32
+ log,
33
+ pick,
34
+ options.inputAllowed ?? true,
35
+ )
31
36
  if (!root) return
32
37
  const report = yield* workbase.validate(root)
33
38
 
@@ -61,4 +66,8 @@ graphs. The current or selected registered workbase is used when path is omitted
61
66
 
62
67
  Options:
63
68
  --json Output the validation report as JSON
69
+ --no-input Never open an interactive workbase selector
70
+
71
+ When interactive input is unavailable, pass a path or run this command from a
72
+ workbase. Registered-workbase selection otherwise fails.
64
73
  `
@@ -193,7 +193,12 @@ describe("work command", () => {
193
193
  expect(harness.events).toEqual(["probe:opencode", "launch:opencode"])
194
194
  expect(harness.launches[0]).toEqual({
195
195
  cli: "opencode",
196
- args: ["opencode", "--continue"],
196
+ args: [
197
+ "opencode",
198
+ "--continue",
199
+ "--prompt",
200
+ "Work on the epic. Read /workbase/epics/delivery/EPIC.md.",
201
+ ],
197
202
  cwd: "/workbase/epics/delivery",
198
203
  })
199
204
  })
@@ -210,7 +215,7 @@ describe("work command", () => {
210
215
  })
211
216
 
212
217
  expect(harness.shownTasks).toEqual(["delivery"])
213
- expect(harness.launches[0]?.cwd).toBe("/workbase/tasks/delivery")
218
+ expect(harness.launches[0]?.cwd).toBe(singlePhaseWorkspace.writablePath)
214
219
  })
215
220
 
216
221
  test("treats a positional value as a task ID when it is not a directory", async () => {
@@ -223,7 +228,7 @@ describe("work command", () => {
223
228
  })
224
229
 
225
230
  expect(harness.shownTasks).toEqual(["delivery"])
226
- expect(harness.launches[0]?.cwd).toBe("/workbase/tasks/delivery")
231
+ expect(harness.launches[0]?.cwd).toBe(singlePhaseWorkspace.writablePath)
227
232
  })
228
233
 
229
234
  test("launches a multi-phase task agent without materializing", async () => {
@@ -238,7 +243,12 @@ describe("work command", () => {
238
243
  expect(harness.events).toEqual(["probe:opencode", "launch:opencode"])
239
244
  expect(harness.launches[0]).toEqual({
240
245
  cli: "opencode",
241
- args: ["opencode", "--continue"],
246
+ args: [
247
+ "opencode",
248
+ "--continue",
249
+ "--prompt",
250
+ "Work on the task. Read /workbase/tasks/delivery/TASK.md.",
251
+ ],
242
252
  cwd: "/workbase/tasks/delivery",
243
253
  })
244
254
  })
@@ -257,7 +267,16 @@ describe("work command", () => {
257
267
  "probe:opencode",
258
268
  "launch:opencode",
259
269
  ])
260
- 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
+ })
261
280
  expect(harness.statusUpdates).toEqual([
262
281
  "phase:example:implementation:working",
263
282
  ])
@@ -273,7 +292,7 @@ describe("work command", () => {
273
292
  })
274
293
 
275
294
  expect(harness.events[0]).toBe("materialize")
276
- expect(harness.launches[0]?.cwd).toBe("/workbase/tasks/example")
295
+ expect(harness.launches[0]?.cwd).toBe(singlePhaseWorkspace.writablePath)
277
296
  })
278
297
 
279
298
  test("selects a target with fzf when no directory is provided", async () => {
@@ -307,9 +326,37 @@ describe("work command", () => {
307
326
  "probe:opencode",
308
327
  "launch:opencode",
309
328
  ])
310
- expect(harness.launches[0]?.cwd).toBe(
311
- "/workbase/tasks/delivery/phases/build",
312
- )
329
+ expect(harness.launches[0]?.cwd).toBe(multiPhaseWorkspace.writablePath)
330
+ })
331
+
332
+ test("requires an explicit target when input is disabled", async () => {
333
+ const harness = createHarness()
334
+
335
+ await expect(
336
+ harness.run({
337
+ cwd: "/workbase",
338
+ opencode: true,
339
+ inputAllowed: false,
340
+ }),
341
+ ).rejects.toThrow("provide a directory, task ID, or --epic")
342
+ expect(harness.events).toEqual([])
343
+ })
344
+
345
+ test("runs an explicit target when input is disabled", async () => {
346
+ const harness = createHarness({ existingDirectories: [] })
347
+
348
+ await harness.run({
349
+ cwd: "/workbase",
350
+ directory: "example",
351
+ opencode: true,
352
+ inputAllowed: false,
353
+ })
354
+
355
+ expect(harness.events).toEqual([
356
+ "materialize",
357
+ "probe:opencode",
358
+ "launch:opencode",
359
+ ])
313
360
  })
314
361
 
315
362
  test("selects a registered workbase when local discovery fails", async () => {
@@ -338,6 +385,24 @@ describe("work command", () => {
338
385
  ])
339
386
  })
340
387
 
388
+ test("does not select a registered workbase when input is disabled", async () => {
389
+ const harness = createHarness({
390
+ outsideWorkbase: true,
391
+ registeredWorkbases: ["/workbase"],
392
+ existingDirectories: [],
393
+ })
394
+
395
+ await expect(
396
+ harness.run({
397
+ cwd: "/outside",
398
+ directory: "example",
399
+ opencode: true,
400
+ inputAllowed: false,
401
+ }),
402
+ ).rejects.toThrow("provide an explicit path or run Agency from a workbase")
403
+ expect(harness.events).toEqual([])
404
+ })
405
+
341
406
  test("explains how to register a workbase when none are known", async () => {
342
407
  const harness = createHarness({ outsideWorkbase: true })
343
408
 
@@ -387,7 +452,7 @@ describe("work command", () => {
387
452
  expect(harness.events).toEqual([])
388
453
  })
389
454
 
390
- test("launches OpenCode in the task directory and continues its session", async () => {
455
+ test("launches OpenCode in the writable checkout with explicit context", async () => {
391
456
  const harness = createHarness()
392
457
 
393
458
  await harness.run({ taskId: "example", opencode: true })
@@ -400,8 +465,13 @@ describe("work command", () => {
400
465
  expect(harness.launches).toEqual([
401
466
  {
402
467
  cli: "opencode",
403
- args: ["opencode", "--continue"],
404
- cwd: "/workbase/tasks/example",
468
+ args: [
469
+ "opencode",
470
+ "--continue",
471
+ "--prompt",
472
+ "Start the task. Read /workbase/tasks/example/TASK.md.",
473
+ ],
474
+ cwd: singlePhaseWorkspace.writablePath,
405
475
  },
406
476
  ])
407
477
  expect(harness.statusUpdates).toEqual(["task:example:working"])
@@ -411,7 +481,7 @@ describe("work command", () => {
411
481
  ])
412
482
  })
413
483
 
414
- test("continues OpenCode for a multi-phase task", async () => {
484
+ test("continues OpenCode with explicit task and phase context", async () => {
415
485
  const harness = createHarness({ workspace: multiPhaseWorkspace })
416
486
 
417
487
  await harness.run({
@@ -420,7 +490,16 @@ describe("work command", () => {
420
490
  opencode: true,
421
491
  })
422
492
 
423
- expect(harness.launches[0]?.args).toEqual(["opencode", "--continue"])
493
+ expect(harness.launches[0]).toEqual({
494
+ cli: "opencode",
495
+ args: [
496
+ "opencode",
497
+ "--continue",
498
+ "--prompt",
499
+ "Start the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
500
+ ],
501
+ cwd: multiPhaseWorkspace.writablePath,
502
+ })
424
503
  })
425
504
 
426
505
  test("automatically falls back to Claude", async () => {
@@ -432,7 +511,7 @@ describe("work command", () => {
432
511
  expect(harness.launches[0]).toEqual({
433
512
  cli: "claude",
434
513
  args: ["claude", "Start the task. Read /workbase/tasks/example/TASK.md."],
435
- cwd: "/workbase/tasks/example",
514
+ cwd: singlePhaseWorkspace.writablePath,
436
515
  })
437
516
  })
438
517
 
@@ -456,7 +535,7 @@ describe("work command", () => {
456
535
  expect(harness.launches[0]).toEqual({
457
536
  cli: "claude",
458
537
  args: ["claude", "Start the task. Read /workbase/tasks/example/TASK.md."],
459
- cwd: "/workbase/tasks/example",
538
+ cwd: singlePhaseWorkspace.writablePath,
460
539
  })
461
540
  })
462
541
 
@@ -493,7 +572,7 @@ describe("work command", () => {
493
572
  verboseHarness.run({ taskId: "example", verbose: true }),
494
573
  )
495
574
  expect(verboseLogs).toEqual([
496
- "Launching command: opencode --continue (cwd: /workbase/tasks/example)",
575
+ "Launching command: opencode --continue --prompt 'Start the task. Read /workbase/tasks/example/TASK.md.' (cwd: /workbase/tasks/example/code/agency)",
497
576
  ])
498
577
  expect(verboseHarness.materializeOptions[0]?.verbose).toBe(true)
499
578
 
@@ -84,7 +84,8 @@ export const work = (
84
84
  ? yield* fs.isDirectory(directoryPath)
85
85
  : false
86
86
  const startPath = isDirectory && directoryPath ? directoryPath : cwd
87
- const root = yield* resolveWorkbase(startPath, log, pickBase)
87
+ const inputAllowed = options.inputAllowed ?? true
88
+ const root = yield* resolveWorkbase(startPath, log, pickBase, inputAllowed)
88
89
  if (!root) return
89
90
 
90
91
  let target: WorkTarget | null = null
@@ -148,6 +149,13 @@ export const work = (
148
149
  }
149
150
 
150
151
  if (!target) {
152
+ if (!inputAllowed) {
153
+ return yield* Effect.fail(
154
+ new Error(
155
+ "Work target selection requires interactive input; provide a directory, task ID, or --epic <id>",
156
+ ),
157
+ )
158
+ }
151
159
  const epicRecords = yield* epics.list(root)
152
160
  const taskRecords = yield* tasks.list(root)
153
161
  const phaseRecords = []
@@ -206,7 +214,7 @@ export const work = (
206
214
  prompt = workspace.phasePath
207
215
  ? `Start the task. Read ${workspace.taskPath} and ${workspace.phasePath}.`
208
216
  : `Start the task. Read ${workspace.taskPath}.`
209
- launchPath = dirname(target.path)
217
+ launchPath = workspace.writablePath
210
218
  }
211
219
 
212
220
  const requested = options.claude ? "claude" : "opencode"
@@ -227,7 +235,8 @@ export const work = (
227
235
  yield* tasks.setStatus(target.taskId, "working", root)
228
236
  }
229
237
 
230
- const args = cli === "opencode" ? ["--continue"] : [prompt]
238
+ const args =
239
+ cli === "opencode" ? ["--continue", "--prompt", prompt] : [prompt]
231
240
  verboseLog(
232
241
  `Launching command: ${formatCommand([cli, ...args])} (cwd: ${launchPath})`,
233
242
  )
@@ -246,4 +255,8 @@ Options:
246
255
  --epic <id> Work on an epic
247
256
  --opencode Require OpenCode
248
257
  --claude Require Claude Code
258
+ --no-input Never open an interactive selector
259
+
260
+ Without interactive input, provide a directory, task ID, or --epic and run the
261
+ command from a workbase. Workbase and target selection otherwise fail.
249
262
  `
@@ -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 () => {