@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.
- package/README.md +45 -16
- package/cli.ts +21 -0
- package/package.json +1 -1
- package/skills/agency/SKILL.md +31 -4
- package/src/cli.test.ts +1 -0
- package/src/commands/archive.test.ts +69 -0
- package/src/commands/archive.ts +70 -0
- package/src/commands/init.test.ts +3 -0
- package/src/commands/phase.ts +23 -1
- package/src/commands/pr.ts +1 -0
- package/src/commands/task-phase.test.ts +46 -1
- package/src/commands/task.test.ts +94 -0
- package/src/commands/task.ts +160 -17
- package/src/commands/work.test.ts +63 -34
- package/src/commands/work.ts +20 -3
- package/src/services/ArchiveService.test.ts +334 -0
- package/src/services/ArchiveService.ts +246 -0
- package/src/services/FileSystemService.ts +7 -2
- package/src/services/PhaseService.ts +28 -0
- package/src/services/PullRequestService.ts +3 -0
- package/src/services/TaskPhaseService.test.ts +79 -0
- package/src/services/TaskService.ts +31 -1
- package/src/services/WorkbaseService.test.ts +66 -0
- package/src/services/WorkbaseService.ts +32 -2
- package/src/services/WorktreeService.test.ts +210 -2
- package/src/services/WorktreeService.ts +143 -1
- package/src/test-utils.ts +2 -0
- package/src/utils/process.test.ts +38 -1
- package/src/utils/process.ts +30 -6
- package/src/utils/progress.test.ts +37 -0
- package/src/utils/progress.ts +36 -0
- package/src/workbase/AGENTS.md +3 -0
- package/src/workbase/opencode-file.ts +37 -0
- package/src/workbase/schemas.test.ts +50 -0
- package/src/workbase/schemas.ts +6 -2
- package/src/workbase/work-target.test.ts +9 -9
- package/src/workbase/work-target.ts +36 -6
|
@@ -212,6 +212,85 @@ describe("task and phase services", () => {
|
|
|
212
212
|
branch: "task/single",
|
|
213
213
|
base: "main",
|
|
214
214
|
pr: "https://github.com/example/agency/pull/42",
|
|
215
|
+
status: "open",
|
|
215
216
|
})
|
|
216
217
|
})
|
|
218
|
+
|
|
219
|
+
test("updates status on execution units", async () => {
|
|
220
|
+
const createdTask = await runTestEffect(
|
|
221
|
+
TaskService.pipe(
|
|
222
|
+
Effect.flatMap((service) =>
|
|
223
|
+
service.create(
|
|
224
|
+
{
|
|
225
|
+
id: "single-status",
|
|
226
|
+
ticketUrl: "https://example.com/task",
|
|
227
|
+
repo: "agency",
|
|
228
|
+
branch: "task/single-status",
|
|
229
|
+
base: "main",
|
|
230
|
+
},
|
|
231
|
+
root,
|
|
232
|
+
),
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
expect(createdTask.content).toContain("status: open")
|
|
237
|
+
const task = await runTestEffect(
|
|
238
|
+
TaskService.pipe(
|
|
239
|
+
Effect.flatMap((service) =>
|
|
240
|
+
service.setStatus("single-status", "done", root),
|
|
241
|
+
),
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
expect(task.data.status).toBe("done")
|
|
245
|
+
expect(task.content).toContain("status: done")
|
|
246
|
+
expect(task.content).toContain("Describe the task outcome.")
|
|
247
|
+
|
|
248
|
+
await runTestEffect(
|
|
249
|
+
TaskService.pipe(
|
|
250
|
+
Effect.flatMap((service) =>
|
|
251
|
+
service.create(
|
|
252
|
+
{
|
|
253
|
+
id: "multi-status",
|
|
254
|
+
ticketUrl: "https://example.com/task",
|
|
255
|
+
multiPhase: true,
|
|
256
|
+
},
|
|
257
|
+
root,
|
|
258
|
+
),
|
|
259
|
+
),
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
await runTestEffect(
|
|
263
|
+
PhaseService.pipe(
|
|
264
|
+
Effect.flatMap((service) =>
|
|
265
|
+
service.create(
|
|
266
|
+
{
|
|
267
|
+
taskId: "multi-status",
|
|
268
|
+
id: "implementation",
|
|
269
|
+
repo: "agency",
|
|
270
|
+
branch: "task/multi-status",
|
|
271
|
+
base: "main",
|
|
272
|
+
},
|
|
273
|
+
root,
|
|
274
|
+
),
|
|
275
|
+
),
|
|
276
|
+
),
|
|
277
|
+
)
|
|
278
|
+
const phase = await runTestEffect(
|
|
279
|
+
PhaseService.pipe(
|
|
280
|
+
Effect.flatMap((service) =>
|
|
281
|
+
service.setStatus("multi-status", "implementation", "dropped", root),
|
|
282
|
+
),
|
|
283
|
+
),
|
|
284
|
+
)
|
|
285
|
+
expect(phase.data.status).toBe("dropped")
|
|
286
|
+
await expect(
|
|
287
|
+
runTestEffect(
|
|
288
|
+
TaskService.pipe(
|
|
289
|
+
Effect.flatMap((service) =>
|
|
290
|
+
service.setStatus("multi-status", "done", root),
|
|
291
|
+
),
|
|
292
|
+
),
|
|
293
|
+
),
|
|
294
|
+
).rejects.toThrow("set status on a phase instead")
|
|
295
|
+
})
|
|
217
296
|
})
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
TaskFrontmatter,
|
|
10
10
|
type RepositoryReference,
|
|
11
11
|
type TaskFrontmatter as TaskData,
|
|
12
|
+
WorkStatus,
|
|
12
13
|
} from "../workbase/schemas"
|
|
13
14
|
import {
|
|
14
15
|
formatMarkdownDocument,
|
|
@@ -28,7 +29,7 @@ interface TaskRecord {
|
|
|
28
29
|
|
|
29
30
|
export interface CreateTaskInput {
|
|
30
31
|
readonly id: string
|
|
31
|
-
readonly ticketUrl: string
|
|
32
|
+
readonly ticketUrl: string | null
|
|
32
33
|
readonly description?: string
|
|
33
34
|
readonly epic?: string
|
|
34
35
|
readonly multiPhase?: boolean
|
|
@@ -57,6 +58,13 @@ const decodeId = (id: string) => {
|
|
|
57
58
|
: Effect.succeed(result.right)
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
const decodeStatus = (status: string) => {
|
|
62
|
+
const result = Schema.decodeUnknownEither(WorkStatus)(status)
|
|
63
|
+
return Either.isLeft(result)
|
|
64
|
+
? Effect.fail(new TaskError({ message: `Invalid work status '${status}'` }))
|
|
65
|
+
: Effect.succeed(result.right)
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
61
69
|
sync: () => ({
|
|
62
70
|
create: (input: CreateTaskInput, startPath: string = process.cwd()) =>
|
|
@@ -188,5 +196,27 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
188
196
|
}
|
|
189
197
|
return record
|
|
190
198
|
}),
|
|
199
|
+
|
|
200
|
+
setStatus: (
|
|
201
|
+
id: string,
|
|
202
|
+
status: string,
|
|
203
|
+
startPath: string = process.cwd(),
|
|
204
|
+
) =>
|
|
205
|
+
Effect.gen(function* () {
|
|
206
|
+
const fs = yield* FileSystemService
|
|
207
|
+
const service = yield* TaskService
|
|
208
|
+
const validStatus = yield* decodeStatus(status)
|
|
209
|
+
const record = yield* service.show(id, startPath)
|
|
210
|
+
if ("phases" in record.data) {
|
|
211
|
+
return yield* new TaskError({
|
|
212
|
+
message: `Task '${id}' has multiple phases; set status on a phase instead`,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
const parsed = yield* parseFrontmatter(record.content, record.path)
|
|
216
|
+
const data = { ...record.data, status: validStatus }
|
|
217
|
+
const content = formatMarkdownDocument(data, parsed.body)
|
|
218
|
+
yield* fs.writeFile(record.path, content)
|
|
219
|
+
return { ...record, content, data } satisfies TaskRecord
|
|
220
|
+
}),
|
|
191
221
|
}),
|
|
192
222
|
}) {}
|
|
@@ -5,6 +5,7 @@ import { mkdir } from "node:fs/promises"
|
|
|
5
5
|
import { dirname, join } from "node:path"
|
|
6
6
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
7
7
|
import { managedWorkbaseAgents } from "../workbase/agents-file"
|
|
8
|
+
import { managedWorkbaseOpencode } from "../workbase/opencode-file"
|
|
8
9
|
import { WorkbaseService } from "./WorkbaseService"
|
|
9
10
|
|
|
10
11
|
const write = async (root: string, path: string, content: string) => {
|
|
@@ -18,6 +19,11 @@ const managedAgents = (body: string) => {
|
|
|
18
19
|
return `<!-- agency-managed: sha256=${checksum} -->\n\n${body}`
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
const managedOpencode = (body: string) => {
|
|
23
|
+
const checksum = createHash("sha256").update(body).digest("hex")
|
|
24
|
+
return `// agency-managed: sha256=${checksum}\n\n${body}`
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
describe("WorkbaseService", () => {
|
|
22
28
|
let root: string
|
|
23
29
|
|
|
@@ -45,6 +51,66 @@ describe("WorkbaseService", () => {
|
|
|
45
51
|
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
46
52
|
managedWorkbaseAgents,
|
|
47
53
|
)
|
|
54
|
+
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
55
|
+
managedWorkbaseOpencode,
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("preserves an unmanaged workbase OpenCode config", async () => {
|
|
60
|
+
await write(root, "agency.json", '{"version":2}\n')
|
|
61
|
+
await write(root, ".opencode/opencode.jsonc", '{"model":"test/model"}\n')
|
|
62
|
+
|
|
63
|
+
await runTestEffect(
|
|
64
|
+
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
68
|
+
'{"model":"test/model"}\n',
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test("does not override an existing JSON OpenCode config", async () => {
|
|
73
|
+
await write(root, "agency.json", '{"version":2}\n')
|
|
74
|
+
await write(root, ".opencode/opencode.json", '{"model":"test/model"}\n')
|
|
75
|
+
|
|
76
|
+
await runTestEffect(
|
|
77
|
+
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
expect(
|
|
81
|
+
await Bun.file(join(root, ".opencode/opencode.jsonc")).exists(),
|
|
82
|
+
).toBe(false)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test("updates an unmodified managed workbase OpenCode config", async () => {
|
|
86
|
+
await write(root, "agency.json", '{"version":2}\n')
|
|
87
|
+
await write(
|
|
88
|
+
root,
|
|
89
|
+
".opencode/opencode.jsonc",
|
|
90
|
+
managedOpencode('{"references":{}}\n'),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
await runTestEffect(
|
|
94
|
+
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
98
|
+
managedWorkbaseOpencode,
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test("preserves a modified managed workbase OpenCode config", async () => {
|
|
103
|
+
await write(root, "agency.json", '{"version":2}\n')
|
|
104
|
+
const content = `${managedOpencode('{"references":{}}\n')}\n// User edit\n`
|
|
105
|
+
await write(root, ".opencode/opencode.jsonc", content)
|
|
106
|
+
|
|
107
|
+
await runTestEffect(
|
|
108
|
+
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
112
|
+
content,
|
|
113
|
+
)
|
|
48
114
|
})
|
|
49
115
|
|
|
50
116
|
test("preserves an unmanaged workbase AGENTS.md", async () => {
|
|
@@ -19,6 +19,10 @@ import {
|
|
|
19
19
|
canUpdateManagedWorkbaseAgents,
|
|
20
20
|
managedWorkbaseAgents,
|
|
21
21
|
} from "../workbase/agents-file"
|
|
22
|
+
import {
|
|
23
|
+
canUpdateManagedWorkbaseOpencode,
|
|
24
|
+
managedWorkbaseOpencode,
|
|
25
|
+
} from "../workbase/opencode-file"
|
|
22
26
|
|
|
23
27
|
class WorkbaseNotFoundError extends Data.TaggedError("WorkbaseNotFoundError")<{
|
|
24
28
|
readonly message: string
|
|
@@ -86,6 +90,32 @@ const ensureWorkbaseAgents = (root: string) =>
|
|
|
86
90
|
}
|
|
87
91
|
})
|
|
88
92
|
|
|
93
|
+
const ensureWorkbaseOpencode = (root: string) =>
|
|
94
|
+
Effect.gen(function* () {
|
|
95
|
+
const fs = yield* FileSystemService
|
|
96
|
+
const directory = join(root, ".opencode")
|
|
97
|
+
const path = join(directory, "opencode.jsonc")
|
|
98
|
+
if (!(yield* fs.exists(path))) {
|
|
99
|
+
if (yield* fs.exists(join(directory, "opencode.json"))) return
|
|
100
|
+
yield* fs.createDirectory(directory)
|
|
101
|
+
yield* fs.writeFile(path, managedWorkbaseOpencode)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const content = yield* fs.readFile(path)
|
|
106
|
+
if (
|
|
107
|
+
content !== managedWorkbaseOpencode &&
|
|
108
|
+
canUpdateManagedWorkbaseOpencode(content)
|
|
109
|
+
) {
|
|
110
|
+
yield* fs.writeFile(path, managedWorkbaseOpencode)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const ensureWorkbaseAgentFiles = (root: string) =>
|
|
115
|
+
Effect.all([ensureWorkbaseAgents(root), ensureWorkbaseOpencode(root)], {
|
|
116
|
+
concurrency: "unbounded",
|
|
117
|
+
})
|
|
118
|
+
|
|
89
119
|
const findCycles = (nodes: readonly Dependency[]): readonly string[] => {
|
|
90
120
|
const dependencies = new Map(
|
|
91
121
|
nodes.map((node) => [node.id, [...(node.dependsOn ?? [])]]),
|
|
@@ -164,7 +194,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
164
194
|
`${existing}${prefix}${missing.join("\n")}\n`,
|
|
165
195
|
)
|
|
166
196
|
}
|
|
167
|
-
yield*
|
|
197
|
+
yield* ensureWorkbaseAgentFiles(root)
|
|
168
198
|
|
|
169
199
|
return root
|
|
170
200
|
}),
|
|
@@ -221,7 +251,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
221
251
|
})
|
|
222
252
|
}
|
|
223
253
|
}
|
|
224
|
-
yield*
|
|
254
|
+
yield* ensureWorkbaseAgentFiles(current)
|
|
225
255
|
return current
|
|
226
256
|
}
|
|
227
257
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
-
import { mkdir } from "node:fs/promises"
|
|
3
|
+
import { mkdir, rm } from "node:fs/promises"
|
|
4
4
|
import { join } from "node:path"
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
captureLogs,
|
|
7
|
+
cleanupTempDir,
|
|
8
|
+
createTempDir,
|
|
9
|
+
runTestEffect,
|
|
10
|
+
} from "../test-utils"
|
|
6
11
|
import { TaskService } from "./TaskService"
|
|
7
12
|
import { PhaseService } from "./PhaseService"
|
|
8
13
|
import { WorktreeService } from "./WorktreeService"
|
|
@@ -127,6 +132,60 @@ describe("WorktreeService", () => {
|
|
|
127
132
|
).toBe("example\n")
|
|
128
133
|
})
|
|
129
134
|
|
|
135
|
+
test("logs the expanded configured command in verbose mode", async () => {
|
|
136
|
+
await Bun.write(
|
|
137
|
+
join(root, "agency.json"),
|
|
138
|
+
JSON.stringify({
|
|
139
|
+
version: 2,
|
|
140
|
+
worktreeCreateCommand: [
|
|
141
|
+
"sh",
|
|
142
|
+
"-c",
|
|
143
|
+
'git -C "$1" worktree add -b "$3" "$2" "$4" >/dev/null 2>&1',
|
|
144
|
+
"agency-worktree",
|
|
145
|
+
"{repo}",
|
|
146
|
+
"{worktree}",
|
|
147
|
+
"{branch}",
|
|
148
|
+
"{base}",
|
|
149
|
+
],
|
|
150
|
+
}),
|
|
151
|
+
)
|
|
152
|
+
await runTestEffect(
|
|
153
|
+
TaskService.pipe(
|
|
154
|
+
Effect.flatMap((service) =>
|
|
155
|
+
service.create(
|
|
156
|
+
{
|
|
157
|
+
id: "verbose-command",
|
|
158
|
+
ticketUrl: "https://example.com/task",
|
|
159
|
+
repo: "agency",
|
|
160
|
+
branch: "task/verbose-command",
|
|
161
|
+
base: "main",
|
|
162
|
+
},
|
|
163
|
+
root,
|
|
164
|
+
),
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
const logs = await captureLogs(() =>
|
|
170
|
+
runTestEffect(
|
|
171
|
+
WorktreeService.pipe(
|
|
172
|
+
Effect.flatMap((service) =>
|
|
173
|
+
service.materialize("verbose-command", undefined, root, {
|
|
174
|
+
verbose: true,
|
|
175
|
+
}),
|
|
176
|
+
),
|
|
177
|
+
),
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
expect(logs).toHaveLength(1)
|
|
182
|
+
expect(logs[0]).toStartWith("Running worktree command: sh -c ")
|
|
183
|
+
expect(logs[0]).toContain(join(root, "repos", "agency"))
|
|
184
|
+
expect(logs[0]).toContain(
|
|
185
|
+
join(root, "tasks", "verbose-command", "code", "agency"),
|
|
186
|
+
)
|
|
187
|
+
})
|
|
188
|
+
|
|
130
189
|
test("selects phases and rejects missing or unexpected phase IDs", async () => {
|
|
131
190
|
await runTestEffect(
|
|
132
191
|
TaskService.pipe(
|
|
@@ -541,6 +600,155 @@ pr: null
|
|
|
541
600
|
).rejects.toThrow("is attached to branch 'main'")
|
|
542
601
|
})
|
|
543
602
|
|
|
603
|
+
test("removes worktrees without deleting branches", async () => {
|
|
604
|
+
await runTestEffect(
|
|
605
|
+
TaskService.pipe(
|
|
606
|
+
Effect.flatMap((service) =>
|
|
607
|
+
service.create(
|
|
608
|
+
{
|
|
609
|
+
id: "removable",
|
|
610
|
+
ticketUrl: "https://example.com/task",
|
|
611
|
+
repo: "agency",
|
|
612
|
+
repos: [{ repo: "effect", ref: "main" }],
|
|
613
|
+
branch: "task/removable",
|
|
614
|
+
base: "main",
|
|
615
|
+
},
|
|
616
|
+
root,
|
|
617
|
+
),
|
|
618
|
+
),
|
|
619
|
+
),
|
|
620
|
+
)
|
|
621
|
+
const workspace = await runTestEffect(
|
|
622
|
+
WorktreeService.pipe(
|
|
623
|
+
Effect.flatMap((service) =>
|
|
624
|
+
service.materialize("removable", undefined, root),
|
|
625
|
+
),
|
|
626
|
+
),
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
const removed = await runTestEffect(
|
|
630
|
+
WorktreeService.pipe(
|
|
631
|
+
Effect.flatMap((service) =>
|
|
632
|
+
service.remove("removable", undefined, root),
|
|
633
|
+
),
|
|
634
|
+
),
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
expect(removed.sort()).toEqual(
|
|
638
|
+
[
|
|
639
|
+
join(workspace.codePath, "agency"),
|
|
640
|
+
join(workspace.codePath, "effect"),
|
|
641
|
+
].sort(),
|
|
642
|
+
)
|
|
643
|
+
expect(await Bun.file(workspace.codePath).exists()).toBe(false)
|
|
644
|
+
const branch = Bun.spawnSync([
|
|
645
|
+
"git",
|
|
646
|
+
"-C",
|
|
647
|
+
join(root, "repos/agency"),
|
|
648
|
+
"show-ref",
|
|
649
|
+
"--verify",
|
|
650
|
+
"refs/heads/task/removable",
|
|
651
|
+
])
|
|
652
|
+
expect(branch.exitCode).toBe(0)
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
test("refuses to remove a worktree with uncommitted changes", async () => {
|
|
656
|
+
await runTestEffect(
|
|
657
|
+
TaskService.pipe(
|
|
658
|
+
Effect.flatMap((service) =>
|
|
659
|
+
service.create(
|
|
660
|
+
{
|
|
661
|
+
id: "dirty",
|
|
662
|
+
ticketUrl: "https://example.com/task",
|
|
663
|
+
repo: "agency",
|
|
664
|
+
branch: "task/dirty",
|
|
665
|
+
base: "main",
|
|
666
|
+
},
|
|
667
|
+
root,
|
|
668
|
+
),
|
|
669
|
+
),
|
|
670
|
+
),
|
|
671
|
+
)
|
|
672
|
+
const workspace = await runTestEffect(
|
|
673
|
+
WorktreeService.pipe(
|
|
674
|
+
Effect.flatMap((service) =>
|
|
675
|
+
service.materialize("dirty", undefined, root),
|
|
676
|
+
),
|
|
677
|
+
),
|
|
678
|
+
)
|
|
679
|
+
await Bun.write(
|
|
680
|
+
join(workspace.writablePath, "uncommitted.txt"),
|
|
681
|
+
"keep me\n",
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
await expect(
|
|
685
|
+
runTestEffect(
|
|
686
|
+
WorktreeService.pipe(
|
|
687
|
+
Effect.flatMap((service) => service.remove("dirty", undefined, root)),
|
|
688
|
+
),
|
|
689
|
+
),
|
|
690
|
+
).rejects.toThrow("Failed to remove worktree for 'agency'")
|
|
691
|
+
expect(
|
|
692
|
+
await Bun.file(join(workspace.writablePath, "uncommitted.txt")).text(),
|
|
693
|
+
).toBe("keep me\n")
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
test("handles a missing checkout without deleting its branch", async () => {
|
|
697
|
+
await runTestEffect(
|
|
698
|
+
TaskService.pipe(
|
|
699
|
+
Effect.flatMap((service) =>
|
|
700
|
+
service.create(
|
|
701
|
+
{
|
|
702
|
+
id: "stale",
|
|
703
|
+
ticketUrl: "https://example.com/task",
|
|
704
|
+
repo: "agency",
|
|
705
|
+
branch: "task/stale",
|
|
706
|
+
base: "main",
|
|
707
|
+
},
|
|
708
|
+
root,
|
|
709
|
+
),
|
|
710
|
+
),
|
|
711
|
+
),
|
|
712
|
+
)
|
|
713
|
+
const workspace = await runTestEffect(
|
|
714
|
+
WorktreeService.pipe(
|
|
715
|
+
Effect.flatMap((service) =>
|
|
716
|
+
service.materialize("stale", undefined, root),
|
|
717
|
+
),
|
|
718
|
+
),
|
|
719
|
+
)
|
|
720
|
+
await rm(workspace.codePath, { recursive: true })
|
|
721
|
+
|
|
722
|
+
const removed = await runTestEffect(
|
|
723
|
+
WorktreeService.pipe(
|
|
724
|
+
Effect.flatMap((service) => service.remove("stale", undefined, root)),
|
|
725
|
+
),
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
expect(removed).toEqual([])
|
|
729
|
+
const worktrees = Bun.spawnSync([
|
|
730
|
+
"git",
|
|
731
|
+
"-C",
|
|
732
|
+
join(root, "repos/agency"),
|
|
733
|
+
"worktree",
|
|
734
|
+
"list",
|
|
735
|
+
"--porcelain",
|
|
736
|
+
])
|
|
737
|
+
expect(new TextDecoder().decode(worktrees.stdout)).not.toContain(
|
|
738
|
+
workspace.writablePath,
|
|
739
|
+
)
|
|
740
|
+
expect(
|
|
741
|
+
Bun.spawnSync([
|
|
742
|
+
"git",
|
|
743
|
+
"-C",
|
|
744
|
+
join(root, "repos/agency"),
|
|
745
|
+
"show-ref",
|
|
746
|
+
"--verify",
|
|
747
|
+
"refs/heads/task/stale",
|
|
748
|
+
]).exitCode,
|
|
749
|
+
).toBe(0)
|
|
750
|
+
})
|
|
751
|
+
|
|
544
752
|
test("supports Worktrunk as the configured command", async () => {
|
|
545
753
|
if (Bun.spawnSync(["which", "wt"], { stdout: "ignore" }).exitCode !== 0) {
|
|
546
754
|
return
|