@markjaquith/agency 1.11.1 → 2.1.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 +313 -117
- package/cli.ts +124 -420
- package/index.ts +1 -1
- package/package.json +12 -17
- package/skills/agency/SKILL.md +266 -0
- package/src/cli.test.ts +290 -0
- package/src/commands/description.test.ts +103 -0
- package/src/commands/epic.test.ts +103 -0
- package/src/commands/epic.ts +101 -0
- package/src/commands/init.test.ts +42 -290
- package/src/commands/init.ts +15 -123
- package/src/commands/phase.ts +120 -0
- package/src/commands/pr.test.ts +19 -170
- package/src/commands/pr.ts +23 -179
- package/src/commands/repo.test.ts +80 -0
- package/src/commands/repo.ts +83 -0
- package/src/commands/status.test.ts +35 -477
- package/src/commands/status.ts +24 -213
- package/src/commands/task-phase.test.ts +206 -0
- package/src/commands/task.ts +83 -1285
- package/src/commands/validate.test.ts +91 -0
- package/src/commands/validate.ts +50 -0
- package/src/commands/work.test.ts +326 -399
- package/src/commands/work.ts +153 -153
- package/src/services/EpicService.test.ts +71 -0
- package/src/services/EpicService.ts +135 -0
- package/src/services/FileSystemService.ts +61 -92
- package/src/services/PhaseService.ts +323 -0
- package/src/services/PullRequestService.test.ts +386 -0
- package/src/services/PullRequestService.ts +143 -0
- package/src/services/RepositoryService.test.ts +106 -0
- package/src/services/RepositoryService.ts +159 -0
- package/src/services/TaskPhaseService.test.ts +217 -0
- package/src/services/TaskService.ts +192 -0
- package/src/services/WorkbaseService.test.ts +446 -0
- package/src/services/WorkbaseService.ts +493 -0
- package/src/services/WorktreeService.test.ts +599 -0
- package/src/services/WorktreeService.ts +357 -0
- package/src/test-utils.ts +41 -438
- package/src/types.ts +1 -215
- package/src/utils/command.ts +1 -5
- package/src/utils/effect.test.ts +24 -0
- package/src/utils/effect.ts +3 -273
- package/src/utils/process.test.ts +0 -15
- package/src/utils/process.ts +1 -60
- package/src/workbase/frontmatter.test.ts +50 -0
- package/src/workbase/frontmatter.ts +77 -0
- package/src/workbase/repository-reference.test.ts +25 -0
- package/src/workbase/repository-reference.ts +18 -0
- package/src/workbase/schemas.test.ts +146 -0
- package/src/workbase/schemas.ts +78 -0
- package/src/workbase/work-target.test.ts +77 -0
- package/src/workbase/work-target.ts +180 -0
- package/src/workbase/worktree-command.test.ts +61 -0
- package/src/workbase/worktree-command.ts +52 -0
- package/src/commands/base.test.ts +0 -198
- package/src/commands/base.ts +0 -198
- package/src/commands/clean.test.ts +0 -299
- package/src/commands/clean.ts +0 -274
- package/src/commands/completions.test.ts +0 -51
- package/src/commands/completions.ts +0 -308
- package/src/commands/emit.integration.test.ts +0 -549
- package/src/commands/emit.test.ts +0 -576
- package/src/commands/emit.ts +0 -700
- package/src/commands/emitted.test.ts +0 -226
- package/src/commands/emitted.ts +0 -57
- package/src/commands/loop.test.ts +0 -621
- package/src/commands/loop.ts +0 -406
- package/src/commands/merge.integration.test.ts +0 -195
- package/src/commands/merge.test.ts +0 -246
- package/src/commands/merge.ts +0 -244
- package/src/commands/pull.test.ts +0 -395
- package/src/commands/pull.ts +0 -205
- package/src/commands/push.test.ts +0 -641
- package/src/commands/push.ts +0 -482
- package/src/commands/rebase.test.ts +0 -521
- package/src/commands/rebase.ts +0 -232
- package/src/commands/save.test.ts +0 -247
- package/src/commands/save.ts +0 -162
- package/src/commands/source.test.ts +0 -195
- package/src/commands/source.ts +0 -72
- package/src/commands/switch.test.ts +0 -194
- package/src/commands/switch.ts +0 -84
- package/src/commands/task-branching.test.ts +0 -633
- package/src/commands/task-continue.test.ts +0 -311
- package/src/commands/task-edit.test.ts +0 -235
- package/src/commands/task-main.test.ts +0 -1066
- package/src/commands/task-squash.test.ts +0 -266
- package/src/commands/tasks.test.ts +0 -391
- package/src/commands/tasks.ts +0 -161
- package/src/commands/template-delete.test.ts +0 -178
- package/src/commands/template-delete.ts +0 -98
- package/src/commands/template-list.test.ts +0 -135
- package/src/commands/template-list.ts +0 -87
- package/src/commands/template-view.test.ts +0 -158
- package/src/commands/template-view.ts +0 -86
- package/src/commands/template.test.ts +0 -32
- package/src/commands/template.ts +0 -96
- package/src/commands/use.test.ts +0 -87
- package/src/commands/use.ts +0 -97
- package/src/constants.ts +0 -10
- package/src/errors.ts +0 -17
- package/src/schemas.ts +0 -33
- package/src/services/AgencyMetadataService.ts +0 -311
- package/src/services/ClaudeService.test.ts +0 -184
- package/src/services/ClaudeService.ts +0 -91
- package/src/services/ConfigService.ts +0 -115
- package/src/services/FilterRepoService.ts +0 -164
- package/src/services/FormatterService.test.ts +0 -432
- package/src/services/FormatterService.ts +0 -219
- package/src/services/GitService.ts +0 -1187
- package/src/services/MockFilterRepoService.ts +0 -140
- package/src/services/OpencodeService.ts +0 -263
- package/src/services/PromptService.ts +0 -183
- package/src/services/TemplateService.ts +0 -81
- package/src/types/native-exec.d.ts +0 -8
- package/src/utils/colors.ts +0 -178
- package/src/utils/git-path.ts +0 -6
- package/src/utils/glob.test.ts +0 -161
- package/src/utils/glob.ts +0 -78
- package/src/utils/paths.ts +0 -51
- package/src/utils/pr-branch.test.ts +0 -376
- package/src/utils/pr-branch.ts +0 -508
- package/src/utils/spinner.test.ts +0 -45
- package/src/utils/spinner.ts +0 -96
- package/src/utils/task-parser.test.ts +0 -172
- package/src/utils/task-parser.ts +0 -93
- package/templates/AGENCY.md +0 -22
- package/templates/AGENTS.md +0 -11
- package/templates/CLAUDE.md +0 -3
- package/templates/TASK.md +0 -5
- package/templates/opencode.json +0 -4
|
@@ -1,1066 +0,0 @@
|
|
|
1
|
-
import { test, expect, describe, beforeEach, afterEach } from "bun:test"
|
|
2
|
-
import { join } from "path"
|
|
3
|
-
import { symlink } from "fs/promises"
|
|
4
|
-
import { task } from "../commands/task"
|
|
5
|
-
import {
|
|
6
|
-
createTempDir,
|
|
7
|
-
cleanupTempDir,
|
|
8
|
-
initGitRepo,
|
|
9
|
-
initAgency,
|
|
10
|
-
createSubdir,
|
|
11
|
-
fileExists,
|
|
12
|
-
readFile,
|
|
13
|
-
runTestEffect,
|
|
14
|
-
runGitCommand,
|
|
15
|
-
getCurrentBranch,
|
|
16
|
-
getGitOutput,
|
|
17
|
-
addAndCommit,
|
|
18
|
-
} from "../test-utils"
|
|
19
|
-
|
|
20
|
-
describe("task command", () => {
|
|
21
|
-
let tempDir: string
|
|
22
|
-
let originalCwd: string
|
|
23
|
-
let originalConfigDir: string | undefined
|
|
24
|
-
|
|
25
|
-
beforeEach(async () => {
|
|
26
|
-
tempDir = await createTempDir()
|
|
27
|
-
originalCwd = process.cwd()
|
|
28
|
-
originalConfigDir = process.env.AGENCY_CONFIG_DIR
|
|
29
|
-
// Use a temp config dir to avoid interference from user's actual config
|
|
30
|
-
process.env.AGENCY_CONFIG_DIR = await createTempDir()
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
afterEach(async () => {
|
|
34
|
-
process.chdir(originalCwd)
|
|
35
|
-
if (originalConfigDir !== undefined) {
|
|
36
|
-
process.env.AGENCY_CONFIG_DIR = originalConfigDir
|
|
37
|
-
} else {
|
|
38
|
-
delete process.env.AGENCY_CONFIG_DIR
|
|
39
|
-
}
|
|
40
|
-
if (
|
|
41
|
-
process.env.AGENCY_CONFIG_DIR &&
|
|
42
|
-
process.env.AGENCY_CONFIG_DIR !== originalConfigDir
|
|
43
|
-
) {
|
|
44
|
-
await cleanupTempDir(process.env.AGENCY_CONFIG_DIR)
|
|
45
|
-
}
|
|
46
|
-
await cleanupTempDir(tempDir)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
describe("without path argument", () => {
|
|
50
|
-
test("creates AGENTS.md at git root", async () => {
|
|
51
|
-
await initGitRepo(tempDir)
|
|
52
|
-
process.chdir(tempDir)
|
|
53
|
-
|
|
54
|
-
await initAgency(tempDir, "test")
|
|
55
|
-
|
|
56
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
57
|
-
|
|
58
|
-
expect(await fileExists(join(tempDir, "AGENTS.md"))).toBe(true)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
test("creates AGENTS.md with default content", async () => {
|
|
62
|
-
await initGitRepo(tempDir)
|
|
63
|
-
process.chdir(tempDir)
|
|
64
|
-
|
|
65
|
-
await initAgency(tempDir, "test")
|
|
66
|
-
|
|
67
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
68
|
-
|
|
69
|
-
const content = await readFile(join(tempDir, "AGENTS.md"))
|
|
70
|
-
expect(content).toContain("Repo Instructions")
|
|
71
|
-
expect(content).toContain("AGENCY.md")
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
test("creates file at git root even when run from subdirectory", async () => {
|
|
75
|
-
await initGitRepo(tempDir)
|
|
76
|
-
const subdir = await createSubdir(tempDir, "subdir")
|
|
77
|
-
process.chdir(subdir)
|
|
78
|
-
|
|
79
|
-
await initAgency(tempDir, "test")
|
|
80
|
-
|
|
81
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
82
|
-
|
|
83
|
-
expect(await fileExists(join(tempDir, "AGENTS.md"))).toBe(true)
|
|
84
|
-
expect(await fileExists(join(subdir, "AGENTS.md"))).toBe(false)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
test("does not overwrite existing AGENTS.md", async () => {
|
|
88
|
-
await initGitRepo(tempDir)
|
|
89
|
-
process.chdir(tempDir)
|
|
90
|
-
|
|
91
|
-
const existingContent = "# Existing content"
|
|
92
|
-
await Bun.write(join(tempDir, "AGENTS.md"), existingContent)
|
|
93
|
-
|
|
94
|
-
await initAgency(tempDir, "test")
|
|
95
|
-
|
|
96
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
97
|
-
|
|
98
|
-
const content = await readFile(join(tempDir, "AGENTS.md"))
|
|
99
|
-
expect(content).toBe(existingContent)
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
test("throws error when not in a git repository", async () => {
|
|
103
|
-
process.chdir(tempDir)
|
|
104
|
-
|
|
105
|
-
await expect(runTestEffect(task({ silent: true }))).rejects.toThrow(
|
|
106
|
-
"Not in a git repository",
|
|
107
|
-
)
|
|
108
|
-
})
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
describe("with path argument", () => {
|
|
112
|
-
test("creates file at specified git root", async () => {
|
|
113
|
-
await initGitRepo(tempDir)
|
|
114
|
-
|
|
115
|
-
await initAgency(tempDir, "test")
|
|
116
|
-
await runTestEffect(
|
|
117
|
-
task({
|
|
118
|
-
path: tempDir,
|
|
119
|
-
silent: true,
|
|
120
|
-
emit: "test-feature",
|
|
121
|
-
}),
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
expect(await fileExists(join(tempDir, "AGENTS.md"))).toBe(true)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
test("throws error when path is not a git repository root", async () => {
|
|
128
|
-
await initGitRepo(tempDir)
|
|
129
|
-
const subdir = await createSubdir(tempDir, "subdir")
|
|
130
|
-
|
|
131
|
-
expect(
|
|
132
|
-
runTestEffect(task({ path: subdir, silent: true })),
|
|
133
|
-
).rejects.toThrow("not the root of a git repository")
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
test("throws error when path is not a git repository at all", async () => {
|
|
137
|
-
expect(
|
|
138
|
-
runTestEffect(task({ path: tempDir, silent: true })),
|
|
139
|
-
).rejects.toThrow("not the root of a git repository")
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
test("resolves relative paths correctly", async () => {
|
|
143
|
-
await initGitRepo(tempDir)
|
|
144
|
-
const subdir = await createSubdir(tempDir, "subdir")
|
|
145
|
-
process.chdir(subdir)
|
|
146
|
-
|
|
147
|
-
await initAgency(tempDir, "test")
|
|
148
|
-
await runTestEffect(
|
|
149
|
-
task({
|
|
150
|
-
path: "..",
|
|
151
|
-
silent: true,
|
|
152
|
-
emit: "test-feature",
|
|
153
|
-
}),
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
expect(await fileExists(join(tempDir, "AGENTS.md"))).toBe(true)
|
|
157
|
-
})
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
describe("opencode.json support", () => {
|
|
161
|
-
test("creates opencode.json at git root", async () => {
|
|
162
|
-
await initGitRepo(tempDir)
|
|
163
|
-
process.chdir(tempDir)
|
|
164
|
-
|
|
165
|
-
await initAgency(tempDir, "test")
|
|
166
|
-
|
|
167
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
168
|
-
|
|
169
|
-
expect(await fileExists(join(tempDir, "opencode.json"))).toBe(true)
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
test("creates opencode.json with default content", async () => {
|
|
173
|
-
await initGitRepo(tempDir)
|
|
174
|
-
process.chdir(tempDir)
|
|
175
|
-
|
|
176
|
-
await initAgency(tempDir, "test")
|
|
177
|
-
|
|
178
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
179
|
-
|
|
180
|
-
const content = await readFile(join(tempDir, "opencode.json"))
|
|
181
|
-
const parsed = JSON.parse(content)
|
|
182
|
-
|
|
183
|
-
expect(parsed.$schema).toBe("https://opencode.ai/config.json")
|
|
184
|
-
expect(parsed.instructions).toEqual(["AGENCY.md", "TASK.md"])
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
test("merges with existing opencode.json", async () => {
|
|
188
|
-
await initGitRepo(tempDir)
|
|
189
|
-
process.chdir(tempDir)
|
|
190
|
-
|
|
191
|
-
const existingConfig = {
|
|
192
|
-
custom: "config",
|
|
193
|
-
}
|
|
194
|
-
await Bun.write(
|
|
195
|
-
join(tempDir, "opencode.json"),
|
|
196
|
-
JSON.stringify(existingConfig),
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
await initAgency(tempDir, "test")
|
|
200
|
-
|
|
201
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
202
|
-
|
|
203
|
-
const content = await readFile(join(tempDir, "opencode.json"))
|
|
204
|
-
const parsed = JSON.parse(content)
|
|
205
|
-
|
|
206
|
-
// Should preserve existing properties
|
|
207
|
-
expect(parsed.custom).toBe("config")
|
|
208
|
-
|
|
209
|
-
// Should add our instructions
|
|
210
|
-
expect(parsed.instructions).toContain("AGENCY.md")
|
|
211
|
-
expect(parsed.instructions).toContain("TASK.md")
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
test("uses opencode.json from template directory if it exists", async () => {
|
|
215
|
-
await initGitRepo(tempDir)
|
|
216
|
-
process.chdir(tempDir)
|
|
217
|
-
|
|
218
|
-
const configDir = process.env.AGENCY_CONFIG_DIR!
|
|
219
|
-
const templateDir = join(configDir, "templates", "custom-template")
|
|
220
|
-
await Bun.spawn(["mkdir", "-p", templateDir], {
|
|
221
|
-
stdout: "pipe",
|
|
222
|
-
stderr: "pipe",
|
|
223
|
-
}).exited
|
|
224
|
-
|
|
225
|
-
const customConfig = JSON.stringify({
|
|
226
|
-
$schema: "https://opencode.ai/config.json",
|
|
227
|
-
instructions: ["CUSTOM.md"],
|
|
228
|
-
})
|
|
229
|
-
await Bun.write(join(templateDir, "opencode.json"), customConfig)
|
|
230
|
-
|
|
231
|
-
await initAgency(tempDir, "custom-template")
|
|
232
|
-
|
|
233
|
-
await runTestEffect(
|
|
234
|
-
task({
|
|
235
|
-
silent: true,
|
|
236
|
-
emit: "test-feature",
|
|
237
|
-
}),
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
const content = await readFile(join(tempDir, "opencode.json"))
|
|
241
|
-
expect(content).toBe(customConfig)
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
test("merges with existing opencode.jsonc", async () => {
|
|
245
|
-
await initGitRepo(tempDir)
|
|
246
|
-
process.chdir(tempDir)
|
|
247
|
-
|
|
248
|
-
const existingConfig = {
|
|
249
|
-
custom: "config",
|
|
250
|
-
existing: "property",
|
|
251
|
-
}
|
|
252
|
-
await Bun.write(
|
|
253
|
-
join(tempDir, "opencode.jsonc"),
|
|
254
|
-
JSON.stringify(existingConfig, null, 2),
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
await initAgency(tempDir, "test")
|
|
258
|
-
|
|
259
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
260
|
-
|
|
261
|
-
// Should update opencode.jsonc, not create opencode.json
|
|
262
|
-
expect(await fileExists(join(tempDir, "opencode.jsonc"))).toBe(true)
|
|
263
|
-
expect(await fileExists(join(tempDir, "opencode.json"))).toBe(false)
|
|
264
|
-
|
|
265
|
-
const content = await readFile(join(tempDir, "opencode.jsonc"))
|
|
266
|
-
const parsed = JSON.parse(content)
|
|
267
|
-
|
|
268
|
-
// Should preserve existing properties
|
|
269
|
-
expect(parsed.custom).toBe("config")
|
|
270
|
-
expect(parsed.existing).toBe("property")
|
|
271
|
-
|
|
272
|
-
// Should add our instructions
|
|
273
|
-
expect(parsed.instructions).toContain("AGENCY.md")
|
|
274
|
-
expect(parsed.instructions).toContain("TASK.md")
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
test("prefers opencode.jsonc over opencode.json when both exist", async () => {
|
|
278
|
-
await initGitRepo(tempDir)
|
|
279
|
-
process.chdir(tempDir)
|
|
280
|
-
|
|
281
|
-
// Create both files
|
|
282
|
-
const jsoncConfig = {
|
|
283
|
-
custom: "jsonc-config",
|
|
284
|
-
}
|
|
285
|
-
const jsonConfig = {
|
|
286
|
-
custom: "json-config",
|
|
287
|
-
}
|
|
288
|
-
await Bun.write(
|
|
289
|
-
join(tempDir, "opencode.jsonc"),
|
|
290
|
-
JSON.stringify(jsoncConfig, null, 2),
|
|
291
|
-
)
|
|
292
|
-
await Bun.write(
|
|
293
|
-
join(tempDir, "opencode.json"),
|
|
294
|
-
JSON.stringify(jsonConfig, null, 2),
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
await initAgency(tempDir, "test")
|
|
298
|
-
|
|
299
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
300
|
-
|
|
301
|
-
// Should merge with opencode.jsonc (not opencode.json)
|
|
302
|
-
const jsoncContent = await readFile(join(tempDir, "opencode.jsonc"))
|
|
303
|
-
const jsoncParsed = JSON.parse(jsoncContent)
|
|
304
|
-
|
|
305
|
-
expect(jsoncParsed.custom).toBe("jsonc-config")
|
|
306
|
-
expect(jsoncParsed.instructions).toContain("AGENCY.md")
|
|
307
|
-
|
|
308
|
-
// opencode.json should remain unchanged
|
|
309
|
-
const jsonContent = await readFile(join(tempDir, "opencode.json"))
|
|
310
|
-
expect(jsonContent).toBe(JSON.stringify(jsonConfig, null, 2))
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
test("handles opencode.jsonc with comments", async () => {
|
|
314
|
-
await initGitRepo(tempDir)
|
|
315
|
-
process.chdir(tempDir)
|
|
316
|
-
|
|
317
|
-
const jsoncWithComments = `{
|
|
318
|
-
// This is a comment
|
|
319
|
-
"custom": "config",
|
|
320
|
-
/* Multi-line
|
|
321
|
-
comment */
|
|
322
|
-
"existing": "property"
|
|
323
|
-
}`
|
|
324
|
-
await Bun.write(join(tempDir, "opencode.jsonc"), jsoncWithComments)
|
|
325
|
-
|
|
326
|
-
await initAgency(tempDir, "test")
|
|
327
|
-
|
|
328
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
329
|
-
|
|
330
|
-
const content = await readFile(join(tempDir, "opencode.jsonc"))
|
|
331
|
-
const parsed = JSON.parse(content)
|
|
332
|
-
|
|
333
|
-
// Should preserve existing properties
|
|
334
|
-
expect(parsed.custom).toBe("config")
|
|
335
|
-
expect(parsed.existing).toBe("property")
|
|
336
|
-
|
|
337
|
-
// Should add our instructions
|
|
338
|
-
expect(parsed.instructions).toContain("AGENCY.md")
|
|
339
|
-
expect(parsed.instructions).toContain("TASK.md")
|
|
340
|
-
})
|
|
341
|
-
|
|
342
|
-
test("merges with .opencode/opencode.json", async () => {
|
|
343
|
-
await initGitRepo(tempDir)
|
|
344
|
-
process.chdir(tempDir)
|
|
345
|
-
|
|
346
|
-
// Create .opencode directory with opencode.json
|
|
347
|
-
const dotOpencodeDir = join(tempDir, ".opencode")
|
|
348
|
-
await Bun.spawn(["mkdir", "-p", dotOpencodeDir], {
|
|
349
|
-
stdout: "pipe",
|
|
350
|
-
stderr: "pipe",
|
|
351
|
-
}).exited
|
|
352
|
-
|
|
353
|
-
const existingConfig = {
|
|
354
|
-
custom: "config-in-dotdir",
|
|
355
|
-
}
|
|
356
|
-
await Bun.write(
|
|
357
|
-
join(dotOpencodeDir, "opencode.json"),
|
|
358
|
-
JSON.stringify(existingConfig, null, 2),
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
await initAgency(tempDir, "test")
|
|
362
|
-
|
|
363
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
364
|
-
|
|
365
|
-
// Should update .opencode/opencode.json, not create root opencode.json
|
|
366
|
-
expect(await fileExists(join(dotOpencodeDir, "opencode.json"))).toBe(true)
|
|
367
|
-
expect(await fileExists(join(tempDir, "opencode.json"))).toBe(false)
|
|
368
|
-
|
|
369
|
-
const content = await readFile(join(dotOpencodeDir, "opencode.json"))
|
|
370
|
-
const parsed = JSON.parse(content)
|
|
371
|
-
|
|
372
|
-
// Should preserve existing properties
|
|
373
|
-
expect(parsed.custom).toBe("config-in-dotdir")
|
|
374
|
-
|
|
375
|
-
// Should add our instructions
|
|
376
|
-
expect(parsed.instructions).toContain("AGENCY.md")
|
|
377
|
-
expect(parsed.instructions).toContain("TASK.md")
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
test("merges with .opencode/opencode.jsonc", async () => {
|
|
381
|
-
await initGitRepo(tempDir)
|
|
382
|
-
process.chdir(tempDir)
|
|
383
|
-
|
|
384
|
-
// Create .opencode directory with opencode.jsonc
|
|
385
|
-
const dotOpencodeDir = join(tempDir, ".opencode")
|
|
386
|
-
await Bun.spawn(["mkdir", "-p", dotOpencodeDir], {
|
|
387
|
-
stdout: "pipe",
|
|
388
|
-
stderr: "pipe",
|
|
389
|
-
}).exited
|
|
390
|
-
|
|
391
|
-
const existingConfig = {
|
|
392
|
-
custom: "jsonc-in-dotdir",
|
|
393
|
-
}
|
|
394
|
-
await Bun.write(
|
|
395
|
-
join(dotOpencodeDir, "opencode.jsonc"),
|
|
396
|
-
JSON.stringify(existingConfig, null, 2),
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
await initAgency(tempDir, "test")
|
|
400
|
-
|
|
401
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
402
|
-
|
|
403
|
-
// Should update .opencode/opencode.jsonc, not create root opencode.json
|
|
404
|
-
expect(await fileExists(join(dotOpencodeDir, "opencode.jsonc"))).toBe(
|
|
405
|
-
true,
|
|
406
|
-
)
|
|
407
|
-
expect(await fileExists(join(tempDir, "opencode.json"))).toBe(false)
|
|
408
|
-
|
|
409
|
-
const content = await readFile(join(dotOpencodeDir, "opencode.jsonc"))
|
|
410
|
-
const parsed = JSON.parse(content)
|
|
411
|
-
|
|
412
|
-
// Should preserve existing properties
|
|
413
|
-
expect(parsed.custom).toBe("jsonc-in-dotdir")
|
|
414
|
-
|
|
415
|
-
// Should add our instructions
|
|
416
|
-
expect(parsed.instructions).toContain("AGENCY.md")
|
|
417
|
-
expect(parsed.instructions).toContain("TASK.md")
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
test("prefers .opencode/opencode.jsonc over root opencode.json", async () => {
|
|
421
|
-
await initGitRepo(tempDir)
|
|
422
|
-
process.chdir(tempDir)
|
|
423
|
-
|
|
424
|
-
// Create .opencode directory with opencode.jsonc
|
|
425
|
-
const dotOpencodeDir = join(tempDir, ".opencode")
|
|
426
|
-
await Bun.spawn(["mkdir", "-p", dotOpencodeDir], {
|
|
427
|
-
stdout: "pipe",
|
|
428
|
-
stderr: "pipe",
|
|
429
|
-
}).exited
|
|
430
|
-
|
|
431
|
-
const dotOpencodeConfig = {
|
|
432
|
-
custom: "dotdir-jsonc",
|
|
433
|
-
}
|
|
434
|
-
const rootJsonConfig = {
|
|
435
|
-
custom: "root-json",
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
await Bun.write(
|
|
439
|
-
join(dotOpencodeDir, "opencode.jsonc"),
|
|
440
|
-
JSON.stringify(dotOpencodeConfig, null, 2),
|
|
441
|
-
)
|
|
442
|
-
await Bun.write(
|
|
443
|
-
join(tempDir, "opencode.json"),
|
|
444
|
-
JSON.stringify(rootJsonConfig, null, 2),
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
await initAgency(tempDir, "test")
|
|
448
|
-
|
|
449
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
450
|
-
|
|
451
|
-
// Should merge with .opencode/opencode.jsonc (not root opencode.json)
|
|
452
|
-
const dotDirContent = await readFile(
|
|
453
|
-
join(dotOpencodeDir, "opencode.jsonc"),
|
|
454
|
-
)
|
|
455
|
-
const dotDirParsed = JSON.parse(dotDirContent)
|
|
456
|
-
|
|
457
|
-
expect(dotDirParsed.custom).toBe("dotdir-jsonc")
|
|
458
|
-
expect(dotDirParsed.instructions).toContain("AGENCY.md")
|
|
459
|
-
|
|
460
|
-
// root opencode.json should remain unchanged
|
|
461
|
-
const rootContent = await readFile(join(tempDir, "opencode.json"))
|
|
462
|
-
expect(rootContent).toBe(JSON.stringify(rootJsonConfig, null, 2))
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
test("prefers .opencode/opencode.json over root opencode.jsonc", async () => {
|
|
466
|
-
await initGitRepo(tempDir)
|
|
467
|
-
process.chdir(tempDir)
|
|
468
|
-
|
|
469
|
-
// Create .opencode directory with opencode.json
|
|
470
|
-
const dotOpencodeDir = join(tempDir, ".opencode")
|
|
471
|
-
await Bun.spawn(["mkdir", "-p", dotOpencodeDir], {
|
|
472
|
-
stdout: "pipe",
|
|
473
|
-
stderr: "pipe",
|
|
474
|
-
}).exited
|
|
475
|
-
|
|
476
|
-
const dotOpencodeConfig = {
|
|
477
|
-
custom: "dotdir-json",
|
|
478
|
-
}
|
|
479
|
-
const rootJsoncConfig = {
|
|
480
|
-
custom: "root-jsonc",
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
await Bun.write(
|
|
484
|
-
join(dotOpencodeDir, "opencode.json"),
|
|
485
|
-
JSON.stringify(dotOpencodeConfig, null, 2),
|
|
486
|
-
)
|
|
487
|
-
await Bun.write(
|
|
488
|
-
join(tempDir, "opencode.jsonc"),
|
|
489
|
-
JSON.stringify(rootJsoncConfig, null, 2),
|
|
490
|
-
)
|
|
491
|
-
|
|
492
|
-
await initAgency(tempDir, "test")
|
|
493
|
-
|
|
494
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
495
|
-
|
|
496
|
-
// Should merge with .opencode/opencode.json (not root opencode.jsonc)
|
|
497
|
-
const dotDirContent = await readFile(
|
|
498
|
-
join(dotOpencodeDir, "opencode.json"),
|
|
499
|
-
)
|
|
500
|
-
const dotDirParsed = JSON.parse(dotDirContent)
|
|
501
|
-
|
|
502
|
-
expect(dotDirParsed.custom).toBe("dotdir-json")
|
|
503
|
-
expect(dotDirParsed.instructions).toContain("AGENCY.md")
|
|
504
|
-
|
|
505
|
-
// root opencode.jsonc should remain unchanged
|
|
506
|
-
const rootContent = await readFile(join(tempDir, "opencode.jsonc"))
|
|
507
|
-
expect(rootContent).toBe(JSON.stringify(rootJsoncConfig, null, 2))
|
|
508
|
-
})
|
|
509
|
-
|
|
510
|
-
test("adds .opencode/opencode.json to injectedFiles", async () => {
|
|
511
|
-
await initGitRepo(tempDir)
|
|
512
|
-
process.chdir(tempDir)
|
|
513
|
-
|
|
514
|
-
// Create .opencode directory with opencode.json
|
|
515
|
-
const dotOpencodeDir = join(tempDir, ".opencode")
|
|
516
|
-
await Bun.spawn(["mkdir", "-p", dotOpencodeDir], {
|
|
517
|
-
stdout: "pipe",
|
|
518
|
-
stderr: "pipe",
|
|
519
|
-
}).exited
|
|
520
|
-
|
|
521
|
-
await Bun.write(
|
|
522
|
-
join(dotOpencodeDir, "opencode.json"),
|
|
523
|
-
JSON.stringify({ custom: "config" }, null, 2),
|
|
524
|
-
)
|
|
525
|
-
|
|
526
|
-
await initAgency(tempDir, "test")
|
|
527
|
-
|
|
528
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
529
|
-
|
|
530
|
-
// Check agency.json to verify .opencode/opencode.json is in injectedFiles
|
|
531
|
-
const agencyJsonContent = await readFile(join(tempDir, "agency.json"))
|
|
532
|
-
const metadata = JSON.parse(agencyJsonContent)
|
|
533
|
-
|
|
534
|
-
expect(metadata.injectedFiles).toContain(".opencode/opencode.json")
|
|
535
|
-
})
|
|
536
|
-
})
|
|
537
|
-
|
|
538
|
-
describe("TASK.md support", () => {
|
|
539
|
-
test("creates TASK.md at git root", async () => {
|
|
540
|
-
await initGitRepo(tempDir)
|
|
541
|
-
process.chdir(tempDir)
|
|
542
|
-
|
|
543
|
-
await initAgency(tempDir, "test")
|
|
544
|
-
|
|
545
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
546
|
-
|
|
547
|
-
expect(await fileExists(join(tempDir, "TASK.md"))).toBe(true)
|
|
548
|
-
})
|
|
549
|
-
|
|
550
|
-
test("creates TASK.md with placeholder when no task provided", async () => {
|
|
551
|
-
await initGitRepo(tempDir)
|
|
552
|
-
process.chdir(tempDir)
|
|
553
|
-
|
|
554
|
-
await initAgency(tempDir, "test")
|
|
555
|
-
|
|
556
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
557
|
-
|
|
558
|
-
const content = await readFile(join(tempDir, "TASK.md"))
|
|
559
|
-
expect(content).toContain("{task}")
|
|
560
|
-
})
|
|
561
|
-
|
|
562
|
-
test("creates TASK.md with task description when provided", async () => {
|
|
563
|
-
await initGitRepo(tempDir)
|
|
564
|
-
process.chdir(tempDir)
|
|
565
|
-
|
|
566
|
-
await initAgency(tempDir, "test")
|
|
567
|
-
await runTestEffect(
|
|
568
|
-
task({
|
|
569
|
-
silent: true,
|
|
570
|
-
task: "Build new feature",
|
|
571
|
-
emit: "test-feature",
|
|
572
|
-
}),
|
|
573
|
-
)
|
|
574
|
-
|
|
575
|
-
const content = await readFile(join(tempDir, "TASK.md"))
|
|
576
|
-
expect(content).toContain("Build new feature")
|
|
577
|
-
expect(content).not.toContain("{task}")
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
test("skips TASK.md if it already exists", async () => {
|
|
581
|
-
await initGitRepo(tempDir)
|
|
582
|
-
process.chdir(tempDir)
|
|
583
|
-
|
|
584
|
-
const existingContent = "# Existing TASK"
|
|
585
|
-
await Bun.write(join(tempDir, "TASK.md"), existingContent)
|
|
586
|
-
|
|
587
|
-
await initAgency(tempDir, "test")
|
|
588
|
-
|
|
589
|
-
// Should succeed but skip TASK.md
|
|
590
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
591
|
-
|
|
592
|
-
// TASK.md should not be overwritten
|
|
593
|
-
const content = await readFile(join(tempDir, "TASK.md"))
|
|
594
|
-
expect(content).toBe(existingContent)
|
|
595
|
-
|
|
596
|
-
// Other files should still be created
|
|
597
|
-
expect(await fileExists(join(tempDir, "AGENTS.md"))).toBe(true)
|
|
598
|
-
})
|
|
599
|
-
|
|
600
|
-
test("uses TASK.md from template directory if it exists", async () => {
|
|
601
|
-
await initGitRepo(tempDir)
|
|
602
|
-
process.chdir(tempDir)
|
|
603
|
-
|
|
604
|
-
const configDir = process.env.AGENCY_CONFIG_DIR!
|
|
605
|
-
const templateDir = join(configDir, "templates", "custom-template")
|
|
606
|
-
await Bun.spawn(["mkdir", "-p", templateDir], {
|
|
607
|
-
stdout: "pipe",
|
|
608
|
-
stderr: "pipe",
|
|
609
|
-
}).exited
|
|
610
|
-
|
|
611
|
-
const customTask = "# Custom Task Content"
|
|
612
|
-
await Bun.write(join(templateDir, "TASK.md"), customTask)
|
|
613
|
-
|
|
614
|
-
await initAgency(tempDir, "custom-template")
|
|
615
|
-
|
|
616
|
-
await runTestEffect(
|
|
617
|
-
task({
|
|
618
|
-
silent: true,
|
|
619
|
-
emit: "test-feature",
|
|
620
|
-
}),
|
|
621
|
-
)
|
|
622
|
-
|
|
623
|
-
const content = await readFile(join(tempDir, "TASK.md"))
|
|
624
|
-
expect(content).toBe(customTask)
|
|
625
|
-
})
|
|
626
|
-
})
|
|
627
|
-
|
|
628
|
-
describe("silent mode", () => {
|
|
629
|
-
test("silent flag suppresses output", async () => {
|
|
630
|
-
await initGitRepo(tempDir)
|
|
631
|
-
process.chdir(tempDir)
|
|
632
|
-
|
|
633
|
-
// Capture console.log calls
|
|
634
|
-
const logs: string[] = []
|
|
635
|
-
const originalLog = console.log
|
|
636
|
-
console.log = (...args: any[]) => logs.push(args.join(" "))
|
|
637
|
-
|
|
638
|
-
await initAgency(tempDir, "test")
|
|
639
|
-
|
|
640
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
641
|
-
|
|
642
|
-
console.log = originalLog
|
|
643
|
-
|
|
644
|
-
expect(logs.length).toBe(0)
|
|
645
|
-
expect(await fileExists(join(tempDir, "AGENTS.md"))).toBe(true)
|
|
646
|
-
})
|
|
647
|
-
|
|
648
|
-
test("without silent flag produces output", async () => {
|
|
649
|
-
await initGitRepo(tempDir)
|
|
650
|
-
process.chdir(tempDir)
|
|
651
|
-
|
|
652
|
-
// Create a feature branch to avoid the main branch check
|
|
653
|
-
await Bun.spawn(["git", "checkout", "-b", "test-feature"], {
|
|
654
|
-
cwd: tempDir,
|
|
655
|
-
stdout: "pipe",
|
|
656
|
-
stderr: "pipe",
|
|
657
|
-
}).exited
|
|
658
|
-
|
|
659
|
-
// Capture console.log calls
|
|
660
|
-
const logs: string[] = []
|
|
661
|
-
const originalLog = console.log
|
|
662
|
-
console.log = (...args: any[]) => logs.push(args.join(" "))
|
|
663
|
-
|
|
664
|
-
// Provide task to avoid interactive prompt
|
|
665
|
-
await initAgency(tempDir, "test")
|
|
666
|
-
await runTestEffect(
|
|
667
|
-
task({
|
|
668
|
-
silent: false,
|
|
669
|
-
task: "Test task",
|
|
670
|
-
emit: "test-feature",
|
|
671
|
-
}),
|
|
672
|
-
)
|
|
673
|
-
|
|
674
|
-
console.log = originalLog
|
|
675
|
-
|
|
676
|
-
expect(logs.length).toBeGreaterThan(0)
|
|
677
|
-
expect(logs.some((log) => log.includes("Created"))).toBe(true)
|
|
678
|
-
})
|
|
679
|
-
})
|
|
680
|
-
|
|
681
|
-
describe("branch handling", () => {
|
|
682
|
-
test("fails when no branch name provided (silent mode)", async () => {
|
|
683
|
-
await initGitRepo(tempDir)
|
|
684
|
-
process.chdir(tempDir)
|
|
685
|
-
|
|
686
|
-
await initAgency(tempDir, "test")
|
|
687
|
-
await expect(runTestEffect(task({ silent: true }))).rejects.toThrow(
|
|
688
|
-
"Branch name is required",
|
|
689
|
-
)
|
|
690
|
-
})
|
|
691
|
-
|
|
692
|
-
test("creates branch when on main branch with branch name provided", async () => {
|
|
693
|
-
await initGitRepo(tempDir)
|
|
694
|
-
process.chdir(tempDir)
|
|
695
|
-
|
|
696
|
-
await initAgency(tempDir, "test")
|
|
697
|
-
|
|
698
|
-
await runTestEffect(task({ silent: true, emit: "my-feature" }))
|
|
699
|
-
|
|
700
|
-
// Verify we're now on the new branch
|
|
701
|
-
const proc = Bun.spawn(["git", "branch", "--show-current"], {
|
|
702
|
-
cwd: tempDir,
|
|
703
|
-
stdout: "pipe",
|
|
704
|
-
stderr: "pipe",
|
|
705
|
-
})
|
|
706
|
-
await proc.exited
|
|
707
|
-
const currentBranch = await new Response(proc.stdout).text()
|
|
708
|
-
expect(currentBranch.trim()).toBe("agency--my-feature")
|
|
709
|
-
|
|
710
|
-
// Verify files were created
|
|
711
|
-
expect(await fileExists(join(tempDir, "AGENTS.md"))).toBe(true)
|
|
712
|
-
})
|
|
713
|
-
|
|
714
|
-
test("fails early when branch already exists", async () => {
|
|
715
|
-
await initGitRepo(tempDir)
|
|
716
|
-
process.chdir(tempDir)
|
|
717
|
-
|
|
718
|
-
// Create a feature branch first (using source pattern)
|
|
719
|
-
await Bun.spawn(["git", "checkout", "-b", "agency--existing-branch"], {
|
|
720
|
-
cwd: tempDir,
|
|
721
|
-
stdout: "pipe",
|
|
722
|
-
stderr: "pipe",
|
|
723
|
-
}).exited
|
|
724
|
-
|
|
725
|
-
// Switch back to main
|
|
726
|
-
await Bun.spawn(["git", "checkout", "main"], {
|
|
727
|
-
cwd: tempDir,
|
|
728
|
-
stdout: "pipe",
|
|
729
|
-
stderr: "pipe",
|
|
730
|
-
}).exited
|
|
731
|
-
|
|
732
|
-
// Try to create a branch with the same name
|
|
733
|
-
await expect(
|
|
734
|
-
(async () => {
|
|
735
|
-
await initAgency(tempDir, "test")
|
|
736
|
-
return await runTestEffect(
|
|
737
|
-
task({
|
|
738
|
-
silent: true,
|
|
739
|
-
emit: "existing-branch",
|
|
740
|
-
task: "This should not be asked for",
|
|
741
|
-
}),
|
|
742
|
-
)
|
|
743
|
-
})(),
|
|
744
|
-
).rejects.toThrow("already exists")
|
|
745
|
-
})
|
|
746
|
-
|
|
747
|
-
test("succeeds when already on a feature branch", async () => {
|
|
748
|
-
await initGitRepo(tempDir)
|
|
749
|
-
process.chdir(tempDir)
|
|
750
|
-
|
|
751
|
-
// Create and switch to feature branch
|
|
752
|
-
await Bun.spawn(["git", "checkout", "-b", "feature-branch"], {
|
|
753
|
-
cwd: tempDir,
|
|
754
|
-
stdout: "pipe",
|
|
755
|
-
stderr: "pipe",
|
|
756
|
-
}).exited
|
|
757
|
-
|
|
758
|
-
// Should succeed without needing branch option
|
|
759
|
-
await initAgency(tempDir, "test")
|
|
760
|
-
|
|
761
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
762
|
-
|
|
763
|
-
expect(await fileExists(join(tempDir, "AGENTS.md"))).toBe(true)
|
|
764
|
-
})
|
|
765
|
-
|
|
766
|
-
test("fails when on agency source branch without branch name in silent mode", async () => {
|
|
767
|
-
await initGitRepo(tempDir)
|
|
768
|
-
process.chdir(tempDir)
|
|
769
|
-
|
|
770
|
-
await initAgency(tempDir, "test")
|
|
771
|
-
|
|
772
|
-
// Create a task branch with agency files
|
|
773
|
-
await runTestEffect(task({ silent: true, emit: "original-feature" }))
|
|
774
|
-
|
|
775
|
-
// Verify we're on the source branch with agency.json
|
|
776
|
-
const currentBranch = await getCurrentBranch(tempDir)
|
|
777
|
-
expect(currentBranch).toBe("agency--original-feature")
|
|
778
|
-
expect(await fileExists(join(tempDir, "agency.json"))).toBe(true)
|
|
779
|
-
|
|
780
|
-
// Try to run agency task again without providing a branch name
|
|
781
|
-
// This should fail because we're on an agency source branch
|
|
782
|
-
await expect(runTestEffect(task({ silent: true }))).rejects.toThrow(
|
|
783
|
-
"agency source branch",
|
|
784
|
-
)
|
|
785
|
-
})
|
|
786
|
-
|
|
787
|
-
test("prompts for branch name when on agency source branch", async () => {
|
|
788
|
-
await initGitRepo(tempDir)
|
|
789
|
-
process.chdir(tempDir)
|
|
790
|
-
|
|
791
|
-
await initAgency(tempDir, "test")
|
|
792
|
-
|
|
793
|
-
// Create a task branch with agency files
|
|
794
|
-
await runTestEffect(task({ silent: true, emit: "original-feature" }))
|
|
795
|
-
|
|
796
|
-
// Verify we're on the source branch with agency.json
|
|
797
|
-
const currentBranch = await getCurrentBranch(tempDir)
|
|
798
|
-
expect(currentBranch).toBe("agency--original-feature")
|
|
799
|
-
expect(await fileExists(join(tempDir, "agency.json"))).toBe(true)
|
|
800
|
-
|
|
801
|
-
// With a branch name provided, it should work
|
|
802
|
-
await runTestEffect(task({ silent: true, emit: "new-feature" }))
|
|
803
|
-
|
|
804
|
-
// Verify we're now on the new branch
|
|
805
|
-
const newBranch = await getCurrentBranch(tempDir)
|
|
806
|
-
expect(newBranch).toBe("agency--new-feature")
|
|
807
|
-
})
|
|
808
|
-
})
|
|
809
|
-
|
|
810
|
-
describe("template-based source files", () => {
|
|
811
|
-
let configDir: string
|
|
812
|
-
let originalEnv: string | undefined
|
|
813
|
-
|
|
814
|
-
beforeEach(async () => {
|
|
815
|
-
// Create a temporary config directory
|
|
816
|
-
configDir = await createTempDir()
|
|
817
|
-
originalEnv = process.env.AGENCY_CONFIG_DIR
|
|
818
|
-
process.env.AGENCY_CONFIG_DIR = configDir
|
|
819
|
-
})
|
|
820
|
-
|
|
821
|
-
afterEach(async () => {
|
|
822
|
-
// Restore original env
|
|
823
|
-
if (originalEnv !== undefined) {
|
|
824
|
-
process.env.AGENCY_CONFIG_DIR = originalEnv
|
|
825
|
-
} else {
|
|
826
|
-
delete process.env.AGENCY_CONFIG_DIR
|
|
827
|
-
}
|
|
828
|
-
await cleanupTempDir(configDir)
|
|
829
|
-
})
|
|
830
|
-
|
|
831
|
-
test("copies files in subdirectories from template", async () => {
|
|
832
|
-
await initGitRepo(tempDir)
|
|
833
|
-
process.chdir(tempDir)
|
|
834
|
-
|
|
835
|
-
// Create template with files in subdirectories (including dotdirectories)
|
|
836
|
-
const templateDir = join(configDir, "templates", "custom")
|
|
837
|
-
await Bun.spawn(
|
|
838
|
-
["mkdir", "-p", join(templateDir, ".agents", "skills", "my-skill")],
|
|
839
|
-
{
|
|
840
|
-
stdout: "pipe",
|
|
841
|
-
stderr: "pipe",
|
|
842
|
-
},
|
|
843
|
-
).exited
|
|
844
|
-
await Bun.spawn(["mkdir", "-p", join(templateDir, "docs")], {
|
|
845
|
-
stdout: "pipe",
|
|
846
|
-
stderr: "pipe",
|
|
847
|
-
}).exited
|
|
848
|
-
|
|
849
|
-
const skillContent = "# My Skill\n\nSkill instructions here"
|
|
850
|
-
const docContent = "# Docs\n\nDocs content"
|
|
851
|
-
await Bun.write(
|
|
852
|
-
join(templateDir, ".agents", "skills", "my-skill", "SKILL.md"),
|
|
853
|
-
skillContent,
|
|
854
|
-
)
|
|
855
|
-
await Bun.write(join(templateDir, "docs", "guide.md"), docContent)
|
|
856
|
-
|
|
857
|
-
await initAgency(tempDir, "custom")
|
|
858
|
-
|
|
859
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
860
|
-
|
|
861
|
-
// Files in dotdirectory should be created
|
|
862
|
-
expect(
|
|
863
|
-
await fileExists(
|
|
864
|
-
join(tempDir, ".agents", "skills", "my-skill", "SKILL.md"),
|
|
865
|
-
),
|
|
866
|
-
).toBe(true)
|
|
867
|
-
const skillFileContent = await readFile(
|
|
868
|
-
join(tempDir, ".agents", "skills", "my-skill", "SKILL.md"),
|
|
869
|
-
)
|
|
870
|
-
expect(skillFileContent).toBe(skillContent)
|
|
871
|
-
|
|
872
|
-
// Files in regular subdirectory should also be created
|
|
873
|
-
expect(await fileExists(join(tempDir, "docs", "guide.md"))).toBe(true)
|
|
874
|
-
const docFileContent = await readFile(join(tempDir, "docs", "guide.md"))
|
|
875
|
-
expect(docFileContent).toBe(docContent)
|
|
876
|
-
})
|
|
877
|
-
|
|
878
|
-
test("uses AGENTS.md from template directory if it exists", async () => {
|
|
879
|
-
await initGitRepo(tempDir)
|
|
880
|
-
process.chdir(tempDir)
|
|
881
|
-
|
|
882
|
-
// Create template with custom content
|
|
883
|
-
const templateDir = join(configDir, "templates", "custom")
|
|
884
|
-
await Bun.spawn(["mkdir", "-p", templateDir], {
|
|
885
|
-
stdout: "pipe",
|
|
886
|
-
stderr: "pipe",
|
|
887
|
-
}).exited
|
|
888
|
-
|
|
889
|
-
const sourceContent = "# Custom AGENTS.md content\nThis is from template"
|
|
890
|
-
await Bun.write(join(templateDir, "AGENTS.md"), sourceContent)
|
|
891
|
-
|
|
892
|
-
await initAgency(tempDir, "custom")
|
|
893
|
-
|
|
894
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
895
|
-
|
|
896
|
-
const content = await readFile(join(tempDir, "AGENTS.md"))
|
|
897
|
-
expect(content).toBe(sourceContent)
|
|
898
|
-
})
|
|
899
|
-
|
|
900
|
-
test("uses default content when template doesn't exist yet", async () => {
|
|
901
|
-
await initGitRepo(tempDir)
|
|
902
|
-
process.chdir(tempDir)
|
|
903
|
-
|
|
904
|
-
await initAgency(tempDir, "custom-template")
|
|
905
|
-
|
|
906
|
-
await runTestEffect(
|
|
907
|
-
task({
|
|
908
|
-
silent: true,
|
|
909
|
-
emit: "test-feature",
|
|
910
|
-
}),
|
|
911
|
-
)
|
|
912
|
-
|
|
913
|
-
const agentsContent = await readFile(join(tempDir, "AGENTS.md"))
|
|
914
|
-
|
|
915
|
-
expect(agentsContent).toContain("Repo Instructions")
|
|
916
|
-
})
|
|
917
|
-
|
|
918
|
-
test("does not populate template directory automatically on first use", async () => {
|
|
919
|
-
await initGitRepo(tempDir)
|
|
920
|
-
process.chdir(tempDir)
|
|
921
|
-
|
|
922
|
-
await initAgency(tempDir, "auto-created")
|
|
923
|
-
|
|
924
|
-
await runTestEffect(
|
|
925
|
-
task({
|
|
926
|
-
silent: true,
|
|
927
|
-
emit: "test-feature",
|
|
928
|
-
}),
|
|
929
|
-
)
|
|
930
|
-
|
|
931
|
-
// Template should NOT have AGENTS.md (not populated automatically)
|
|
932
|
-
const templateDir = join(configDir, "templates", "auto-created")
|
|
933
|
-
const templateAgentsExists = await fileExists(
|
|
934
|
-
join(templateDir, "AGENTS.md"),
|
|
935
|
-
)
|
|
936
|
-
expect(templateAgentsExists).toBe(false)
|
|
937
|
-
|
|
938
|
-
// Files in repo should use default content (not from template)
|
|
939
|
-
const agentsContent = await readFile(join(tempDir, "AGENTS.md"))
|
|
940
|
-
expect(agentsContent).toContain("Repo Instructions")
|
|
941
|
-
})
|
|
942
|
-
|
|
943
|
-
test("excludes AGENCY.md and TASK.md from injectedFiles metadata", async () => {
|
|
944
|
-
await initGitRepo(tempDir)
|
|
945
|
-
process.chdir(tempDir)
|
|
946
|
-
|
|
947
|
-
await initAgency(tempDir, "test")
|
|
948
|
-
|
|
949
|
-
await runTestEffect(
|
|
950
|
-
task({
|
|
951
|
-
silent: true,
|
|
952
|
-
emit: "test-feature",
|
|
953
|
-
}),
|
|
954
|
-
)
|
|
955
|
-
|
|
956
|
-
// Read agency.json metadata
|
|
957
|
-
const metadata = JSON.parse(await readFile(join(tempDir, "agency.json")))
|
|
958
|
-
|
|
959
|
-
// AGENCY.md and TASK.md should NOT be in injectedFiles
|
|
960
|
-
expect(metadata.injectedFiles).not.toContain("AGENCY.md")
|
|
961
|
-
expect(metadata.injectedFiles).not.toContain("TASK.md")
|
|
962
|
-
|
|
963
|
-
// opencode.json should be in injectedFiles
|
|
964
|
-
expect(metadata.injectedFiles).toContain("opencode.json")
|
|
965
|
-
})
|
|
966
|
-
|
|
967
|
-
test("supports deprecated --branch flag for backward compatibility", async () => {
|
|
968
|
-
await initGitRepo(tempDir)
|
|
969
|
-
process.chdir(tempDir)
|
|
970
|
-
|
|
971
|
-
await initAgency(tempDir, "test")
|
|
972
|
-
|
|
973
|
-
// Use deprecated --branch flag instead of --emit
|
|
974
|
-
await runTestEffect(
|
|
975
|
-
task({
|
|
976
|
-
silent: true,
|
|
977
|
-
branch: "backward-compat-test",
|
|
978
|
-
}),
|
|
979
|
-
)
|
|
980
|
-
|
|
981
|
-
// Should have created the branch with agency-- prefix
|
|
982
|
-
const currentBranch = await getCurrentBranch(tempDir)
|
|
983
|
-
expect(currentBranch).toBe("agency--backward-compat-test")
|
|
984
|
-
|
|
985
|
-
// Should have correct emitBranch in agency.json
|
|
986
|
-
const metadata = JSON.parse(await readFile(join(tempDir, "agency.json")))
|
|
987
|
-
expect(metadata.emitBranch).toBe("backward-compat-test")
|
|
988
|
-
})
|
|
989
|
-
})
|
|
990
|
-
|
|
991
|
-
describe("CLAUDE.md symlink handling", () => {
|
|
992
|
-
test("commits CLAUDE.md modification when it is a symlink to another file", async () => {
|
|
993
|
-
await initGitRepo(tempDir)
|
|
994
|
-
process.chdir(tempDir)
|
|
995
|
-
|
|
996
|
-
// Create a pre-existing AGENTS.md and CLAUDE.md as a symlink to it
|
|
997
|
-
await Bun.write(
|
|
998
|
-
join(tempDir, "AGENTS.md"),
|
|
999
|
-
"# Agents\n\nOriginal content",
|
|
1000
|
-
)
|
|
1001
|
-
await symlink("AGENTS.md", join(tempDir, "CLAUDE.md"))
|
|
1002
|
-
await addAndCommit(
|
|
1003
|
-
tempDir,
|
|
1004
|
-
["AGENTS.md", "CLAUDE.md"],
|
|
1005
|
-
"Add AGENTS.md and CLAUDE.md symlink",
|
|
1006
|
-
)
|
|
1007
|
-
|
|
1008
|
-
await initAgency(tempDir, "test")
|
|
1009
|
-
|
|
1010
|
-
// Run task - this will modify CLAUDE.md (which is a symlink to AGENTS.md)
|
|
1011
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
1012
|
-
|
|
1013
|
-
// Check that the commit was created and includes both files
|
|
1014
|
-
const commitLog = await getGitOutput(tempDir, [
|
|
1015
|
-
"log",
|
|
1016
|
-
"--oneline",
|
|
1017
|
-
"-n",
|
|
1018
|
-
"2",
|
|
1019
|
-
])
|
|
1020
|
-
|
|
1021
|
-
// Should have the CLAUDE.md edit commit
|
|
1022
|
-
expect(commitLog).toContain("agency edit CLAUDE.md")
|
|
1023
|
-
|
|
1024
|
-
// Verify that both AGENTS.md and CLAUDE.md were staged in the CLAUDE.md commit
|
|
1025
|
-
// by checking the diff of the last commit
|
|
1026
|
-
const lastCommitDiff = await getGitOutput(tempDir, [
|
|
1027
|
-
"diff-tree",
|
|
1028
|
-
"--no-commit-id",
|
|
1029
|
-
"--name-only",
|
|
1030
|
-
"-r",
|
|
1031
|
-
"HEAD",
|
|
1032
|
-
])
|
|
1033
|
-
|
|
1034
|
-
// The CLAUDE.md commit should include AGENTS.md (the symlink target)
|
|
1035
|
-
expect(lastCommitDiff.trim().split("\n")).toContain("AGENTS.md")
|
|
1036
|
-
})
|
|
1037
|
-
|
|
1038
|
-
test("commits CLAUDE.md modification normally when not a symlink", async () => {
|
|
1039
|
-
await initGitRepo(tempDir)
|
|
1040
|
-
process.chdir(tempDir)
|
|
1041
|
-
|
|
1042
|
-
// Create a pre-existing CLAUDE.md (not a symlink)
|
|
1043
|
-
await Bun.write(
|
|
1044
|
-
join(tempDir, "CLAUDE.md"),
|
|
1045
|
-
"# Claude\n\nOriginal content",
|
|
1046
|
-
)
|
|
1047
|
-
await addAndCommit(tempDir, "CLAUDE.md", "Add CLAUDE.md")
|
|
1048
|
-
|
|
1049
|
-
await initAgency(tempDir, "test")
|
|
1050
|
-
|
|
1051
|
-
// Run task - this will modify CLAUDE.md
|
|
1052
|
-
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
1053
|
-
|
|
1054
|
-
// Check that the commit was created
|
|
1055
|
-
const commitLog = await getGitOutput(tempDir, [
|
|
1056
|
-
"log",
|
|
1057
|
-
"--oneline",
|
|
1058
|
-
"-n",
|
|
1059
|
-
"2",
|
|
1060
|
-
])
|
|
1061
|
-
|
|
1062
|
-
// Should have the CLAUDE.md edit commit
|
|
1063
|
-
expect(commitLog).toContain("agency edit CLAUDE.md")
|
|
1064
|
-
})
|
|
1065
|
-
})
|
|
1066
|
-
})
|