@markjaquith/agency 2.3.0 → 2.5.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 (37) hide show
  1. package/README.md +45 -16
  2. package/cli.ts +21 -0
  3. package/package.json +1 -1
  4. package/skills/agency/SKILL.md +31 -4
  5. package/src/cli.test.ts +1 -0
  6. package/src/commands/archive.test.ts +69 -0
  7. package/src/commands/archive.ts +70 -0
  8. package/src/commands/init.test.ts +3 -0
  9. package/src/commands/phase.ts +23 -1
  10. package/src/commands/pr.ts +1 -0
  11. package/src/commands/task-phase.test.ts +46 -1
  12. package/src/commands/task.test.ts +94 -0
  13. package/src/commands/task.ts +160 -17
  14. package/src/commands/work.test.ts +63 -34
  15. package/src/commands/work.ts +20 -3
  16. package/src/services/ArchiveService.test.ts +334 -0
  17. package/src/services/ArchiveService.ts +246 -0
  18. package/src/services/FileSystemService.ts +7 -2
  19. package/src/services/PhaseService.ts +28 -0
  20. package/src/services/PullRequestService.ts +3 -0
  21. package/src/services/TaskPhaseService.test.ts +79 -0
  22. package/src/services/TaskService.ts +31 -1
  23. package/src/services/WorkbaseService.test.ts +66 -0
  24. package/src/services/WorkbaseService.ts +32 -2
  25. package/src/services/WorktreeService.test.ts +210 -2
  26. package/src/services/WorktreeService.ts +143 -1
  27. package/src/test-utils.ts +2 -0
  28. package/src/utils/process.test.ts +38 -1
  29. package/src/utils/process.ts +30 -6
  30. package/src/utils/progress.test.ts +37 -0
  31. package/src/utils/progress.ts +36 -0
  32. package/src/workbase/AGENTS.md +3 -0
  33. package/src/workbase/opencode-file.ts +37 -0
  34. package/src/workbase/schemas.test.ts +50 -0
  35. package/src/workbase/schemas.ts +6 -2
  36. package/src/workbase/work-target.test.ts +9 -9
  37. package/src/workbase/work-target.ts +36 -6
@@ -0,0 +1,334 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { Effect } from "effect"
3
+ import { mkdir } from "node:fs/promises"
4
+ import { join } from "node:path"
5
+ import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
+ import { ArchiveService } from "./ArchiveService"
7
+ import { EpicService } from "./EpicService"
8
+ import { PhaseService } from "./PhaseService"
9
+ import { TaskService } from "./TaskService"
10
+ import { WorkbaseService } from "./WorkbaseService"
11
+ import { WorktreeService } from "./WorktreeService"
12
+
13
+ const git = async (args: string[], cwd?: string) => {
14
+ const process = Bun.spawn(["git", ...args], {
15
+ cwd,
16
+ stdout: "pipe",
17
+ stderr: "pipe",
18
+ })
19
+ await process.exited
20
+ if (process.exitCode !== 0) {
21
+ throw new Error(await new Response(process.stderr).text())
22
+ }
23
+ }
24
+
25
+ describe("ArchiveService", () => {
26
+ let root: string
27
+
28
+ beforeEach(async () => {
29
+ root = await createTempDir()
30
+ await Bun.write(join(root, "agency.json"), '{"version":2}\n')
31
+ const source = join(root, "source")
32
+ await mkdir(source, { recursive: true })
33
+ await git(["init", "--initial-branch=main"], source)
34
+ await git(["config", "user.email", "test@example.com"], source)
35
+ await git(["config", "user.name", "Test"], source)
36
+ await Bun.write(join(source, "README.md"), "example\n")
37
+ await git(["add", "README.md"], source)
38
+ await git(["-c", "commit.gpgsign=false", "commit", "-m", "initial"], source)
39
+ await mkdir(join(root, "repos"), { recursive: true })
40
+ await git(["clone", "--bare", source, join(root, "repos/agency")])
41
+ })
42
+
43
+ afterEach(async () => cleanupTempDir(root))
44
+
45
+ test("archives a task after removing its worktree and preserves its branch", async () => {
46
+ await runTestEffect(
47
+ EpicService.pipe(
48
+ Effect.flatMap((service) =>
49
+ service.create(
50
+ "parent",
51
+ "https://example.com/epic",
52
+ [{ repo: "agency", ref: "main" }],
53
+ root,
54
+ ),
55
+ ),
56
+ ),
57
+ )
58
+ await runTestEffect(
59
+ TaskService.pipe(
60
+ Effect.flatMap((service) =>
61
+ service.create(
62
+ {
63
+ id: "child",
64
+ ticketUrl: "https://example.com/task",
65
+ epic: "parent",
66
+ repo: "agency",
67
+ branch: "task/child",
68
+ base: "main",
69
+ },
70
+ root,
71
+ ),
72
+ ),
73
+ ),
74
+ )
75
+ await runTestEffect(
76
+ WorktreeService.pipe(
77
+ Effect.flatMap((service) =>
78
+ service.materialize("child", undefined, root),
79
+ ),
80
+ ),
81
+ )
82
+
83
+ const result = await runTestEffect(
84
+ ArchiveService.pipe(
85
+ Effect.flatMap((service) => service.archiveTask("child", root)),
86
+ ),
87
+ )
88
+
89
+ expect(result.path).toBe(join(root, "archive/tasks/child"))
90
+ expect(await Bun.file(join(root, "tasks/child/TASK.md")).exists()).toBe(
91
+ false,
92
+ )
93
+ expect(await Bun.file(join(result.path, "TASK.md")).exists()).toBe(true)
94
+ expect(await Bun.file(join(result.path, "code")).exists()).toBe(false)
95
+ const epic = await runTestEffect(
96
+ EpicService.pipe(
97
+ Effect.flatMap((service) => service.show("parent", root)),
98
+ ),
99
+ )
100
+ expect(epic.data.tasks).toEqual([])
101
+ const report = await runTestEffect(
102
+ WorkbaseService.pipe(Effect.flatMap((service) => service.validate(root))),
103
+ )
104
+ expect(report.valid).toBe(true)
105
+ expect(
106
+ Bun.spawnSync([
107
+ "git",
108
+ "-C",
109
+ join(root, "repos/agency"),
110
+ "show-ref",
111
+ "--verify",
112
+ "refs/heads/task/child",
113
+ ]).exitCode,
114
+ ).toBe(0)
115
+ })
116
+
117
+ test("archives a phase in the mirrored task hierarchy", async () => {
118
+ await runTestEffect(
119
+ TaskService.pipe(
120
+ Effect.flatMap((service) =>
121
+ service.create(
122
+ {
123
+ id: "multi",
124
+ ticketUrl: "https://example.com/task",
125
+ multiPhase: true,
126
+ },
127
+ root,
128
+ ),
129
+ ),
130
+ ),
131
+ )
132
+ for (const id of ["first", "second"]) {
133
+ await runTestEffect(
134
+ PhaseService.pipe(
135
+ Effect.flatMap((service) =>
136
+ service.create(
137
+ {
138
+ taskId: "multi",
139
+ id,
140
+ repo: "agency",
141
+ branch: `task/${id}`,
142
+ base: "main",
143
+ },
144
+ root,
145
+ ),
146
+ ),
147
+ ),
148
+ )
149
+ }
150
+ await runTestEffect(
151
+ WorktreeService.pipe(
152
+ Effect.flatMap((service) =>
153
+ service.materialize("multi", "second", root),
154
+ ),
155
+ ),
156
+ )
157
+
158
+ const result = await runTestEffect(
159
+ ArchiveService.pipe(
160
+ Effect.flatMap((service) =>
161
+ service.archivePhase("multi", "second", root),
162
+ ),
163
+ ),
164
+ )
165
+
166
+ expect(result.path).toBe(join(root, "archive/tasks/multi/phases/second"))
167
+ expect(await Bun.file(join(result.path, "PHASE.md")).exists()).toBe(true)
168
+ expect(await Bun.file(join(result.path, "code")).exists()).toBe(false)
169
+ const task = await runTestEffect(
170
+ TaskService.pipe(
171
+ Effect.flatMap((service) => service.show("multi", root)),
172
+ ),
173
+ )
174
+ expect("phases" in task.data && task.data.phases).toEqual([{ id: "first" }])
175
+ const report = await runTestEffect(
176
+ WorkbaseService.pipe(Effect.flatMap((service) => service.validate(root))),
177
+ )
178
+ expect(report.valid).toBe(true)
179
+ })
180
+
181
+ test("archives an epic and all of its child task folders", async () => {
182
+ await runTestEffect(
183
+ EpicService.pipe(
184
+ Effect.flatMap((service) =>
185
+ service.create(
186
+ "initiative",
187
+ "https://example.com/epic",
188
+ [{ repo: "agency", ref: "main" }],
189
+ root,
190
+ ),
191
+ ),
192
+ ),
193
+ )
194
+ await runTestEffect(
195
+ TaskService.pipe(
196
+ Effect.flatMap((service) =>
197
+ service.create(
198
+ {
199
+ id: "delivery",
200
+ ticketUrl: "https://example.com/task",
201
+ epic: "initiative",
202
+ repo: "agency",
203
+ branch: "task/delivery",
204
+ base: "main",
205
+ },
206
+ root,
207
+ ),
208
+ ),
209
+ ),
210
+ )
211
+
212
+ const result = await runTestEffect(
213
+ ArchiveService.pipe(
214
+ Effect.flatMap((service) => service.archiveEpic("initiative", root)),
215
+ ),
216
+ )
217
+
218
+ expect(result.archivedPaths).toEqual([
219
+ join(root, "archive/tasks/delivery"),
220
+ join(root, "archive/epics/initiative"),
221
+ ])
222
+ expect(
223
+ await Bun.file(join(root, "archive/epics/initiative/EPIC.md")).exists(),
224
+ ).toBe(true)
225
+ expect(
226
+ await Bun.file(join(root, "archive/tasks/delivery/TASK.md")).exists(),
227
+ ).toBe(true)
228
+ const report = await runTestEffect(
229
+ WorkbaseService.pipe(Effect.flatMap((service) => service.validate(root))),
230
+ )
231
+ expect(report.valid).toBe(true)
232
+ })
233
+
234
+ test("rejects archiving an item required by an active sibling", async () => {
235
+ await runTestEffect(
236
+ TaskService.pipe(
237
+ Effect.flatMap((service) =>
238
+ service.create(
239
+ {
240
+ id: "multi",
241
+ ticketUrl: "https://example.com/task",
242
+ multiPhase: true,
243
+ },
244
+ root,
245
+ ),
246
+ ),
247
+ ),
248
+ )
249
+ await runTestEffect(
250
+ PhaseService.pipe(
251
+ Effect.flatMap((service) =>
252
+ service.create(
253
+ {
254
+ taskId: "multi",
255
+ id: "first",
256
+ repo: "agency",
257
+ branch: "task/first",
258
+ base: "main",
259
+ },
260
+ root,
261
+ ),
262
+ ),
263
+ ),
264
+ )
265
+ await runTestEffect(
266
+ PhaseService.pipe(
267
+ Effect.flatMap((service) =>
268
+ service.create(
269
+ {
270
+ taskId: "multi",
271
+ id: "second",
272
+ repo: "agency",
273
+ branch: "task/second",
274
+ base: "main",
275
+ dependsOn: ["first"],
276
+ },
277
+ root,
278
+ ),
279
+ ),
280
+ ),
281
+ )
282
+
283
+ await expect(
284
+ runTestEffect(
285
+ ArchiveService.pipe(
286
+ Effect.flatMap((service) =>
287
+ service.archivePhase("multi", "first", root),
288
+ ),
289
+ ),
290
+ ),
291
+ ).rejects.toThrow("phase 'second' depends on it")
292
+ })
293
+
294
+ test("does not move a task when its worktree is dirty", async () => {
295
+ await runTestEffect(
296
+ TaskService.pipe(
297
+ Effect.flatMap((service) =>
298
+ service.create(
299
+ {
300
+ id: "dirty",
301
+ ticketUrl: "https://example.com/task",
302
+ repo: "agency",
303
+ branch: "task/dirty",
304
+ base: "main",
305
+ },
306
+ root,
307
+ ),
308
+ ),
309
+ ),
310
+ )
311
+ const workspace = await runTestEffect(
312
+ WorktreeService.pipe(
313
+ Effect.flatMap((service) =>
314
+ service.materialize("dirty", undefined, root),
315
+ ),
316
+ ),
317
+ )
318
+ await Bun.write(join(workspace.writablePath, "dirty.txt"), "keep me\n")
319
+
320
+ await expect(
321
+ runTestEffect(
322
+ ArchiveService.pipe(
323
+ Effect.flatMap((service) => service.archiveTask("dirty", root)),
324
+ ),
325
+ ),
326
+ ).rejects.toThrow("Failed to remove worktree")
327
+ expect(await Bun.file(join(root, "tasks/dirty/TASK.md")).exists()).toBe(
328
+ true,
329
+ )
330
+ expect(await Bun.file(join(root, "archive/tasks/dirty")).exists()).toBe(
331
+ false,
332
+ )
333
+ })
334
+ })
@@ -0,0 +1,246 @@
1
+ import { Data, Effect } from "effect"
2
+ import { dirname, join } from "node:path"
3
+ import { EpicService, type EpicRecord } from "./EpicService"
4
+ import { FileSystemService } from "./FileSystemService"
5
+ import { PhaseService } from "./PhaseService"
6
+ import { TaskService } from "./TaskService"
7
+ import { WorkbaseService } from "./WorkbaseService"
8
+ import { WorktreeService } from "./WorktreeService"
9
+ import {
10
+ formatMarkdownDocument,
11
+ parseFrontmatter,
12
+ } from "../workbase/frontmatter"
13
+ import type { TaskFrontmatter as TaskData } from "../workbase/schemas"
14
+
15
+ class ArchiveError extends Data.TaggedError("ArchiveError")<{
16
+ readonly message: string
17
+ }> {}
18
+
19
+ interface ArchiveResult {
20
+ readonly kind: "epic" | "task" | "phase"
21
+ readonly id: string
22
+ readonly taskId?: string
23
+ readonly path: string
24
+ readonly archivedPaths: readonly string[]
25
+ readonly removedWorktrees: readonly string[]
26
+ }
27
+
28
+ interface TaskRecord {
29
+ readonly id: string
30
+ readonly path: string
31
+ readonly content: string
32
+ readonly data: TaskData
33
+ }
34
+
35
+ const rejectExistingDestination = (path: string) =>
36
+ Effect.gen(function* () {
37
+ const fs = yield* FileSystemService
38
+ if (yield* fs.exists(path)) {
39
+ return yield* new ArchiveError({
40
+ message: `Archive destination already exists: ${path}`,
41
+ })
42
+ }
43
+ })
44
+
45
+ export class ArchiveService extends Effect.Service<ArchiveService>()(
46
+ "ArchiveService",
47
+ {
48
+ sync: () => ({
49
+ archiveEpic: (id: string, startPath: string = process.cwd()) =>
50
+ Effect.gen(function* () {
51
+ const fs = yield* FileSystemService
52
+ const workbase = yield* WorkbaseService
53
+ const epics = yield* EpicService
54
+ const tasks = yield* TaskService
55
+ const worktrees = yield* WorktreeService
56
+ const root = yield* workbase.discover(startPath)
57
+ const epic = yield* epics.show(id, root)
58
+ const taskRecords: TaskRecord[] = []
59
+ for (const child of epic.data.tasks) {
60
+ const task = yield* tasks.show(child.id, root)
61
+ if (task.data.epic !== id) {
62
+ return yield* new ArchiveError({
63
+ message: `Task '${task.id}' does not reference epic '${id}'`,
64
+ })
65
+ }
66
+ taskRecords.push(task)
67
+ }
68
+
69
+ const epicDestination = join(root, "archive", "epics", id)
70
+ yield* rejectExistingDestination(epicDestination)
71
+ for (const task of taskRecords) {
72
+ yield* rejectExistingDestination(
73
+ join(root, "archive", "tasks", task.id),
74
+ )
75
+ }
76
+
77
+ const removedWorktrees: string[] = []
78
+ for (const task of taskRecords) {
79
+ if ("phases" in task.data) {
80
+ for (const phase of task.data.phases) {
81
+ removedWorktrees.push(
82
+ ...(yield* worktrees.remove(task.id, phase.id, root)),
83
+ )
84
+ }
85
+ } else {
86
+ removedWorktrees.push(
87
+ ...(yield* worktrees.remove(task.id, undefined, root)),
88
+ )
89
+ }
90
+ }
91
+
92
+ const archivedPaths: string[] = []
93
+ for (const task of taskRecords) {
94
+ const destination = join(root, "archive", "tasks", task.id)
95
+ yield* fs.createDirectory(dirname(destination))
96
+ yield* fs.moveDirectory(dirname(task.path), destination)
97
+ archivedPaths.push(destination)
98
+ }
99
+ yield* fs.createDirectory(dirname(epicDestination))
100
+ yield* fs.moveDirectory(dirname(epic.path), epicDestination)
101
+ archivedPaths.push(epicDestination)
102
+
103
+ return {
104
+ kind: "epic",
105
+ id,
106
+ path: epicDestination,
107
+ archivedPaths,
108
+ removedWorktrees,
109
+ } satisfies ArchiveResult
110
+ }),
111
+
112
+ archiveTask: (id: string, startPath: string = process.cwd()) =>
113
+ Effect.gen(function* () {
114
+ const fs = yield* FileSystemService
115
+ const workbase = yield* WorkbaseService
116
+ const epics = yield* EpicService
117
+ const tasks = yield* TaskService
118
+ const worktrees = yield* WorktreeService
119
+ const root = yield* workbase.discover(startPath)
120
+ const task = yield* tasks.show(id, root)
121
+ const destination = join(root, "archive", "tasks", id)
122
+ yield* rejectExistingDestination(destination)
123
+
124
+ let parentEpic: EpicRecord | undefined
125
+ if (task.data.epic) {
126
+ parentEpic = yield* epics.show(task.data.epic, root)
127
+ const dependent = parentEpic.data.tasks.find((child) =>
128
+ child.dependsOn?.includes(id),
129
+ )
130
+ if (dependent) {
131
+ return yield* new ArchiveError({
132
+ message: `Cannot archive task '${id}'; task '${dependent.id}' depends on it`,
133
+ })
134
+ }
135
+ }
136
+
137
+ const removedWorktrees: string[] = []
138
+ if ("phases" in task.data) {
139
+ for (const phase of task.data.phases) {
140
+ removedWorktrees.push(
141
+ ...(yield* worktrees.remove(id, phase.id, root)),
142
+ )
143
+ }
144
+ } else {
145
+ removedWorktrees.push(
146
+ ...(yield* worktrees.remove(id, undefined, root)),
147
+ )
148
+ }
149
+
150
+ if (parentEpic) {
151
+ const parsed = yield* parseFrontmatter(
152
+ parentEpic.content,
153
+ parentEpic.path,
154
+ )
155
+ yield* fs.writeFile(
156
+ parentEpic.path,
157
+ formatMarkdownDocument(
158
+ {
159
+ ...parentEpic.data,
160
+ tasks: parentEpic.data.tasks.filter(
161
+ (child) => child.id !== id,
162
+ ),
163
+ },
164
+ parsed.body,
165
+ ),
166
+ )
167
+ }
168
+
169
+ yield* fs.createDirectory(dirname(destination))
170
+ yield* fs.moveDirectory(dirname(task.path), destination)
171
+ return {
172
+ kind: "task",
173
+ id,
174
+ path: destination,
175
+ archivedPaths: [destination],
176
+ removedWorktrees,
177
+ } satisfies ArchiveResult
178
+ }),
179
+
180
+ archivePhase: (
181
+ taskId: string,
182
+ id: string,
183
+ startPath: string = process.cwd(),
184
+ ) =>
185
+ Effect.gen(function* () {
186
+ const fs = yield* FileSystemService
187
+ const workbase = yield* WorkbaseService
188
+ const tasks = yield* TaskService
189
+ const phases = yield* PhaseService
190
+ const worktrees = yield* WorktreeService
191
+ const root = yield* workbase.discover(startPath)
192
+ const task = yield* tasks.show(taskId, root)
193
+ if (!("phases" in task.data)) {
194
+ return yield* new ArchiveError({
195
+ message: `Task '${taskId}' is single-phase and does not contain phases`,
196
+ })
197
+ }
198
+ const phase = yield* phases.show(taskId, id, root)
199
+ const dependent = task.data.phases.find((candidate) =>
200
+ candidate.dependsOn?.includes(id),
201
+ )
202
+ if (dependent) {
203
+ return yield* new ArchiveError({
204
+ message: `Cannot archive phase '${id}'; phase '${dependent.id}' depends on it`,
205
+ })
206
+ }
207
+
208
+ const destination = join(
209
+ root,
210
+ "archive",
211
+ "tasks",
212
+ taskId,
213
+ "phases",
214
+ id,
215
+ )
216
+ yield* rejectExistingDestination(destination)
217
+ const removedWorktrees = yield* worktrees.remove(taskId, id, root)
218
+
219
+ const parsed = yield* parseFrontmatter(task.content, task.path)
220
+ yield* fs.writeFile(
221
+ task.path,
222
+ formatMarkdownDocument(
223
+ {
224
+ ...task.data,
225
+ phases: task.data.phases.filter(
226
+ (candidate) => candidate.id !== id,
227
+ ),
228
+ },
229
+ parsed.body,
230
+ ),
231
+ )
232
+ yield* fs.createDirectory(dirname(destination))
233
+ yield* fs.moveDirectory(dirname(phase.path), destination)
234
+
235
+ return {
236
+ kind: "phase",
237
+ id,
238
+ taskId,
239
+ path: destination,
240
+ archivedPaths: [destination],
241
+ removedWorktrees,
242
+ } satisfies ArchiveResult
243
+ }),
244
+ }),
245
+ },
246
+ ) {}
@@ -180,6 +180,7 @@ export class FileSystemService extends Effect.Service<FileSystemService>()(
180
180
  options?: {
181
181
  readonly cwd?: string
182
182
  readonly captureOutput?: boolean
183
+ readonly forwardOutput?: boolean
183
184
  readonly env?: Record<string, string>
184
185
  },
185
186
  ) =>
@@ -187,8 +188,12 @@ export class FileSystemService extends Effect.Service<FileSystemService>()(
187
188
  spawnProcess(args, {
188
189
  cwd: options?.cwd,
189
190
  stdin: "pipe",
190
- stdout: options?.captureOutput ? "pipe" : "inherit",
191
- stderr: "pipe",
191
+ stdout: options?.forwardOutput
192
+ ? "tee"
193
+ : options?.captureOutput
194
+ ? "pipe"
195
+ : "inherit",
196
+ stderr: options?.forwardOutput ? "tee" : "pipe",
192
197
  env: options?.env,
193
198
  }),
194
199
  Effect.mapError(
@@ -9,6 +9,7 @@ import {
9
9
  PhaseFrontmatter,
10
10
  type PhaseFrontmatter as PhaseData,
11
11
  type RepositoryReference,
12
+ WorkStatus,
12
13
  } from "../workbase/schemas"
13
14
  import {
14
15
  formatMarkdownDocument,
@@ -58,6 +59,15 @@ const decodePhase = (input: unknown) => {
58
59
  : Effect.succeed(result.right)
59
60
  }
60
61
 
62
+ const decodeStatus = (status: string) => {
63
+ const result = Schema.decodeUnknownEither(WorkStatus)(status)
64
+ return Either.isLeft(result)
65
+ ? Effect.fail(
66
+ new PhaseError({ message: `Invalid work status '${status}'` }),
67
+ )
68
+ : Effect.succeed(result.right)
69
+ }
70
+
61
71
  export class PhaseService extends Effect.Service<PhaseService>()(
62
72
  "PhaseService",
63
73
  {
@@ -318,6 +328,24 @@ export class PhaseService extends Effect.Service<PhaseService>()(
318
328
  }
319
329
  return record
320
330
  }),
331
+
332
+ setStatus: (
333
+ taskId: string,
334
+ id: string,
335
+ status: string,
336
+ startPath: string = process.cwd(),
337
+ ) =>
338
+ Effect.gen(function* () {
339
+ const fs = yield* FileSystemService
340
+ const service = yield* PhaseService
341
+ const validStatus = yield* decodeStatus(status)
342
+ const record = yield* service.show(taskId, id, startPath)
343
+ const parsed = yield* parseFrontmatter(record.content, record.path)
344
+ const data = { ...record.data, status: validStatus }
345
+ const content = formatMarkdownDocument(data, parsed.body)
346
+ yield* fs.writeFile(record.path, content)
347
+ return { ...record, content, data } satisfies PhaseRecord
348
+ }),
321
349
  }),
322
350
  },
323
351
  ) {}
@@ -2,6 +2,7 @@ import { Data, Effect } from "effect"
2
2
  import { FileSystemService } from "./FileSystemService"
3
3
  import { WorkbaseService } from "./WorkbaseService"
4
4
  import { WorktreeService } from "./WorktreeService"
5
+ import type { BaseCommandOptions } from "../utils/command"
5
6
  import { TaskService } from "./TaskService"
6
7
  import { PhaseService } from "./PhaseService"
7
8
  import {
@@ -59,6 +60,7 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
59
60
  phaseId?: string,
60
61
  draft = false,
61
62
  startPath: string = process.cwd(),
63
+ options: BaseCommandOptions = {},
62
64
  ) =>
63
65
  Effect.gen(function* () {
64
66
  const service = yield* PullRequestService
@@ -70,6 +72,7 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
70
72
  taskId,
71
73
  phaseId,
72
74
  startPath,
75
+ options,
73
76
  )
74
77
  const task = yield* tasks.show(taskId, workspace.root)
75
78
  const execution =