@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.
Files changed (132) hide show
  1. package/README.md +313 -117
  2. package/cli.ts +124 -420
  3. package/index.ts +1 -1
  4. package/package.json +12 -17
  5. package/skills/agency/SKILL.md +266 -0
  6. package/src/cli.test.ts +290 -0
  7. package/src/commands/description.test.ts +103 -0
  8. package/src/commands/epic.test.ts +103 -0
  9. package/src/commands/epic.ts +101 -0
  10. package/src/commands/init.test.ts +42 -290
  11. package/src/commands/init.ts +15 -123
  12. package/src/commands/phase.ts +120 -0
  13. package/src/commands/pr.test.ts +19 -170
  14. package/src/commands/pr.ts +23 -179
  15. package/src/commands/repo.test.ts +80 -0
  16. package/src/commands/repo.ts +83 -0
  17. package/src/commands/status.test.ts +35 -477
  18. package/src/commands/status.ts +24 -213
  19. package/src/commands/task-phase.test.ts +206 -0
  20. package/src/commands/task.ts +83 -1285
  21. package/src/commands/validate.test.ts +91 -0
  22. package/src/commands/validate.ts +50 -0
  23. package/src/commands/work.test.ts +326 -399
  24. package/src/commands/work.ts +153 -153
  25. package/src/services/EpicService.test.ts +71 -0
  26. package/src/services/EpicService.ts +135 -0
  27. package/src/services/FileSystemService.ts +61 -92
  28. package/src/services/PhaseService.ts +323 -0
  29. package/src/services/PullRequestService.test.ts +386 -0
  30. package/src/services/PullRequestService.ts +143 -0
  31. package/src/services/RepositoryService.test.ts +106 -0
  32. package/src/services/RepositoryService.ts +159 -0
  33. package/src/services/TaskPhaseService.test.ts +217 -0
  34. package/src/services/TaskService.ts +192 -0
  35. package/src/services/WorkbaseService.test.ts +446 -0
  36. package/src/services/WorkbaseService.ts +493 -0
  37. package/src/services/WorktreeService.test.ts +599 -0
  38. package/src/services/WorktreeService.ts +357 -0
  39. package/src/test-utils.ts +41 -438
  40. package/src/types.ts +1 -215
  41. package/src/utils/command.ts +1 -5
  42. package/src/utils/effect.test.ts +24 -0
  43. package/src/utils/effect.ts +3 -273
  44. package/src/utils/process.test.ts +0 -15
  45. package/src/utils/process.ts +1 -60
  46. package/src/workbase/frontmatter.test.ts +50 -0
  47. package/src/workbase/frontmatter.ts +77 -0
  48. package/src/workbase/repository-reference.test.ts +25 -0
  49. package/src/workbase/repository-reference.ts +18 -0
  50. package/src/workbase/schemas.test.ts +146 -0
  51. package/src/workbase/schemas.ts +78 -0
  52. package/src/workbase/work-target.test.ts +77 -0
  53. package/src/workbase/work-target.ts +180 -0
  54. package/src/workbase/worktree-command.test.ts +61 -0
  55. package/src/workbase/worktree-command.ts +52 -0
  56. package/src/commands/base.test.ts +0 -198
  57. package/src/commands/base.ts +0 -198
  58. package/src/commands/clean.test.ts +0 -299
  59. package/src/commands/clean.ts +0 -274
  60. package/src/commands/completions.test.ts +0 -51
  61. package/src/commands/completions.ts +0 -308
  62. package/src/commands/emit.integration.test.ts +0 -549
  63. package/src/commands/emit.test.ts +0 -576
  64. package/src/commands/emit.ts +0 -700
  65. package/src/commands/emitted.test.ts +0 -226
  66. package/src/commands/emitted.ts +0 -57
  67. package/src/commands/loop.test.ts +0 -621
  68. package/src/commands/loop.ts +0 -406
  69. package/src/commands/merge.integration.test.ts +0 -195
  70. package/src/commands/merge.test.ts +0 -246
  71. package/src/commands/merge.ts +0 -244
  72. package/src/commands/pull.test.ts +0 -395
  73. package/src/commands/pull.ts +0 -205
  74. package/src/commands/push.test.ts +0 -641
  75. package/src/commands/push.ts +0 -482
  76. package/src/commands/rebase.test.ts +0 -521
  77. package/src/commands/rebase.ts +0 -232
  78. package/src/commands/save.test.ts +0 -247
  79. package/src/commands/save.ts +0 -162
  80. package/src/commands/source.test.ts +0 -195
  81. package/src/commands/source.ts +0 -72
  82. package/src/commands/switch.test.ts +0 -194
  83. package/src/commands/switch.ts +0 -84
  84. package/src/commands/task-branching.test.ts +0 -633
  85. package/src/commands/task-continue.test.ts +0 -311
  86. package/src/commands/task-edit.test.ts +0 -235
  87. package/src/commands/task-main.test.ts +0 -1066
  88. package/src/commands/task-squash.test.ts +0 -266
  89. package/src/commands/tasks.test.ts +0 -391
  90. package/src/commands/tasks.ts +0 -161
  91. package/src/commands/template-delete.test.ts +0 -178
  92. package/src/commands/template-delete.ts +0 -98
  93. package/src/commands/template-list.test.ts +0 -135
  94. package/src/commands/template-list.ts +0 -87
  95. package/src/commands/template-view.test.ts +0 -158
  96. package/src/commands/template-view.ts +0 -86
  97. package/src/commands/template.test.ts +0 -32
  98. package/src/commands/template.ts +0 -96
  99. package/src/commands/use.test.ts +0 -87
  100. package/src/commands/use.ts +0 -97
  101. package/src/constants.ts +0 -10
  102. package/src/errors.ts +0 -17
  103. package/src/schemas.ts +0 -33
  104. package/src/services/AgencyMetadataService.ts +0 -311
  105. package/src/services/ClaudeService.test.ts +0 -184
  106. package/src/services/ClaudeService.ts +0 -91
  107. package/src/services/ConfigService.ts +0 -115
  108. package/src/services/FilterRepoService.ts +0 -164
  109. package/src/services/FormatterService.test.ts +0 -432
  110. package/src/services/FormatterService.ts +0 -219
  111. package/src/services/GitService.ts +0 -1187
  112. package/src/services/MockFilterRepoService.ts +0 -140
  113. package/src/services/OpencodeService.ts +0 -263
  114. package/src/services/PromptService.ts +0 -183
  115. package/src/services/TemplateService.ts +0 -81
  116. package/src/types/native-exec.d.ts +0 -8
  117. package/src/utils/colors.ts +0 -178
  118. package/src/utils/git-path.ts +0 -6
  119. package/src/utils/glob.test.ts +0 -161
  120. package/src/utils/glob.ts +0 -78
  121. package/src/utils/paths.ts +0 -51
  122. package/src/utils/pr-branch.test.ts +0 -376
  123. package/src/utils/pr-branch.ts +0 -508
  124. package/src/utils/spinner.test.ts +0 -45
  125. package/src/utils/spinner.ts +0 -96
  126. package/src/utils/task-parser.test.ts +0 -172
  127. package/src/utils/task-parser.ts +0 -93
  128. package/templates/AGENCY.md +0 -22
  129. package/templates/AGENTS.md +0 -11
  130. package/templates/CLAUDE.md +0 -3
  131. package/templates/TASK.md +0 -5
  132. package/templates/opencode.json +0 -4
@@ -1,576 +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 { emit } from "../commands/emit"
5
- import { task } from "../commands/task"
6
- import { resolveGitInternalPath } from "../utils/git-path"
7
- import {
8
- createTempDir,
9
- cleanupTempDir,
10
- initGitRepo,
11
- initAgency,
12
- fileExists,
13
- getGitOutput,
14
- getCurrentBranch,
15
- createCommit,
16
- checkoutBranch,
17
- createBranch,
18
- addAndCommit,
19
- setupRemote,
20
- runTestEffect,
21
- runTestEffectWithMockFilterRepo,
22
- clearCapturedFilterRepoCalls,
23
- getLastCapturedFilterRepoCall,
24
- } from "../test-utils"
25
-
26
- // Cache the git-filter-repo availability check (it doesn't change during test run)
27
- let hasGitFilterRepoCache: boolean | null = null
28
- async function checkGitFilterRepo(): Promise<boolean> {
29
- if (hasGitFilterRepoCache === null) {
30
- const proc = Bun.spawn(["which", "git-filter-repo"], {
31
- stdout: "pipe",
32
- stderr: "pipe",
33
- })
34
- await proc.exited
35
- hasGitFilterRepoCache = proc.exitCode === 0
36
- }
37
- return hasGitFilterRepoCache
38
- }
39
-
40
- describe("emit command", () => {
41
- let tempDir: string
42
- let originalCwd: string
43
-
44
- beforeEach(async () => {
45
- tempDir = await createTempDir()
46
- originalCwd = process.cwd()
47
- process.chdir(tempDir)
48
-
49
- // Set config path to non-existent file to use defaults
50
- process.env.AGENCY_CONFIG_PATH = join(tempDir, "non-existent-config.json")
51
- // Set config dir to temp dir to avoid picking up user's config files
52
- process.env.AGENCY_CONFIG_DIR = await createTempDir()
53
-
54
- // Initialize git repo with main branch (already includes initial commit)
55
- await initGitRepo(tempDir)
56
-
57
- // Create a source branch (with agency-- prefix)
58
- await createBranch(tempDir, "agency--test-feature")
59
-
60
- // Initialize AGENTS.md and commit in one go
61
- await initAgency(tempDir, "test")
62
-
63
- await runTestEffect(task({ silent: true, fromCurrent: true }))
64
- await addAndCommit(tempDir, "AGENTS.md", "Add AGENTS.md")
65
-
66
- // Set up origin/main for git-filter-repo
67
- await setupRemote(tempDir, "origin", tempDir)
68
- })
69
-
70
- afterEach(async () => {
71
- process.chdir(originalCwd)
72
- delete process.env.AGENCY_CONFIG_PATH
73
- if (process.env.AGENCY_CONFIG_DIR) {
74
- await cleanupTempDir(process.env.AGENCY_CONFIG_DIR)
75
- delete process.env.AGENCY_CONFIG_DIR
76
- }
77
- await cleanupTempDir(tempDir)
78
- })
79
-
80
- describe("basic functionality", () => {
81
- test("fails when there are uncommitted changes", async () => {
82
- // Create uncommitted changes
83
- await Bun.write(join(tempDir, "uncommitted.txt"), "uncommitted\n")
84
-
85
- expect(
86
- runTestEffect(emit({ silent: true, skipFilter: true })),
87
- ).rejects.toThrow(/uncommitted changes/)
88
- })
89
-
90
- test("fails when there are staged but uncommitted changes", async () => {
91
- // Create a staged but uncommitted change
92
- await Bun.write(join(tempDir, "staged.txt"), "staged\n")
93
- await Bun.spawn(["git", "add", "staged.txt"], {
94
- cwd: tempDir,
95
- stdout: "pipe",
96
- stderr: "pipe",
97
- }).exited
98
-
99
- expect(
100
- runTestEffect(emit({ silent: true, skipFilter: true })),
101
- ).rejects.toThrow(/uncommitted changes/)
102
- })
103
-
104
- test("throws error when git-filter-repo is not installed", async () => {
105
- const hasGitFilterRepo = await checkGitFilterRepo()
106
- if (hasGitFilterRepo) {
107
- // Skip this test if git-filter-repo IS installed
108
- return
109
- }
110
-
111
- await createBranch(tempDir, "agency--feature")
112
- await createCommit(tempDir, "Feature commit")
113
-
114
- expect(runTestEffect(emit({ silent: true }))).rejects.toThrow(
115
- "git-filter-repo is not installed",
116
- )
117
- })
118
-
119
- test("creates emit branch with default name", async () => {
120
- // Go back to main and create a fresh source branch (no inherited agency.json)
121
- await checkoutBranch(tempDir, "main")
122
- await createBranch(tempDir, "agency--feature")
123
- // Create agency.json for this branch
124
- await Bun.write(
125
- join(tempDir, "agency.json"),
126
- JSON.stringify({
127
- version: 1,
128
- injectedFiles: ["AGENTS.md"],
129
- template: "test",
130
- createdAt: new Date().toISOString(),
131
- }),
132
- )
133
- await addAndCommit(tempDir, "agency.json", "Feature commit")
134
-
135
- // Create emit branch (skip filter for speed)
136
- await runTestEffect(emit({ silent: true, skipFilter: true }))
137
-
138
- // Check that emit branch exists (default pattern is %branch%, so emit is just "feature")
139
- const branches = await getGitOutput(tempDir, [
140
- "branch",
141
- "--list",
142
- "feature",
143
- ])
144
- expect(branches.trim()).toContain("feature")
145
-
146
- // Check we're still on the source branch
147
- const currentBranch = await getCurrentBranch(tempDir)
148
- expect(currentBranch).toBe("agency--feature")
149
- })
150
-
151
- test("sets emit branch upstream when remote branch exists", async () => {
152
- await checkoutBranch(tempDir, "main")
153
- await createBranch(tempDir, "agency--feature")
154
- await Bun.write(
155
- join(tempDir, "agency.json"),
156
- JSON.stringify({
157
- version: 1,
158
- injectedFiles: ["AGENTS.md"],
159
- template: "test",
160
- createdAt: new Date().toISOString(),
161
- }),
162
- )
163
- await addAndCommit(tempDir, "agency.json", "Feature commit")
164
-
165
- await getGitOutput(tempDir, [
166
- "update-ref",
167
- "refs/remotes/origin/feature",
168
- "HEAD",
169
- ])
170
-
171
- await runTestEffect(emit({ silent: true, skipFilter: true }))
172
-
173
- const remote = await getGitOutput(tempDir, [
174
- "config",
175
- "--get",
176
- "branch.feature.remote",
177
- ])
178
- const merge = await getGitOutput(tempDir, [
179
- "config",
180
- "--get",
181
- "branch.feature.merge",
182
- ])
183
- expect(remote.trim()).toBe("origin")
184
- expect(merge.trim()).toBe("refs/heads/feature")
185
-
186
- await checkoutBranch(tempDir, "feature")
187
- const pushRef = await getGitOutput(tempDir, [
188
- "rev-parse",
189
- "--abbrev-ref",
190
- "@{push}",
191
- ])
192
- expect(pushRef.trim()).toBe("origin/feature")
193
- })
194
-
195
- test("creates emit branch with custom name", async () => {
196
- await createBranch(tempDir, "agency--feature")
197
- await createCommit(tempDir, "Feature commit")
198
-
199
- // Skip filter for speed - we're just testing branch creation
200
- await runTestEffect(
201
- emit({ emit: "custom-pr", silent: true, skipFilter: true }),
202
- )
203
-
204
- const branches = await getGitOutput(tempDir, [
205
- "branch",
206
- "--list",
207
- "custom-pr",
208
- ])
209
- expect(branches.trim()).toContain("custom-pr")
210
-
211
- // Check we're still on the source branch
212
- const currentBranch = await getCurrentBranch(tempDir)
213
- expect(currentBranch).toBe("agency--feature")
214
- })
215
-
216
- test("completes emit workflow successfully", async () => {
217
- await createBranch(tempDir, "agency--feature")
218
- await createCommit(tempDir, "Feature commit")
219
-
220
- // Should complete without throwing (skip filter for speed)
221
- await runTestEffect(emit({ silent: true, skipFilter: true }))
222
-
223
- // Should still be on source branch
224
- const currentBranch = await getCurrentBranch(tempDir)
225
- expect(currentBranch).toBe("agency--feature")
226
- })
227
-
228
- test("preserves files on source branch after emit", async () => {
229
- await createBranch(tempDir, "agency--feature")
230
- await createCommit(tempDir, "Feature commit")
231
-
232
- // Skip filter for speed - we're testing source branch preservation
233
- await runTestEffect(emit({ silent: true, skipFilter: true }))
234
-
235
- // Should still be on source branch
236
- const currentBranch = await getCurrentBranch(tempDir)
237
- expect(currentBranch).toBe("agency--feature")
238
-
239
- // Check that test file still exists on source branch
240
- expect(await fileExists(join(tempDir, "test.txt"))).toBe(true)
241
-
242
- const files = await getGitOutput(tempDir, ["ls-files"])
243
- expect(files).toContain("test.txt")
244
- })
245
-
246
- test("original branch remains untouched", async () => {
247
- await createBranch(tempDir, "agency--feature")
248
- await createCommit(tempDir, "Feature commit")
249
-
250
- // Create emit branch (skip filter for speed)
251
- await runTestEffect(emit({ silent: true, skipFilter: true }))
252
-
253
- // Should still be on feature branch
254
- const currentBranch = await getCurrentBranch(tempDir)
255
- expect(currentBranch).toBe("agency--feature")
256
-
257
- // Check that AGENTS.md still exist on original branch
258
- const files = await getGitOutput(tempDir, ["ls-files"])
259
- expect(files).toContain("AGENTS.md")
260
- })
261
-
262
- test("works correctly when run multiple times (recreates emit branch)", async () => {
263
- await createBranch(tempDir, "agency--feature")
264
-
265
- // Modify AGENTS.md on feature branch
266
- await Bun.write(
267
- join(tempDir, "AGENTS.md"),
268
- "# Modified by feature branch\n",
269
- )
270
- await addAndCommit(tempDir, "AGENTS.md", "Modify AGENTS.md")
271
-
272
- // Run emit command first time (skip filter for speed)
273
- await runTestEffect(emit({ silent: true, skipFilter: true }))
274
-
275
- // Switch back to feature branch
276
- await checkoutBranch(tempDir, "agency--feature")
277
-
278
- // Make another commit
279
- await createCommit(tempDir, "Another feature commit")
280
-
281
- // Run emit command second time (skip filter for speed)
282
- await runTestEffect(emit({ silent: true, skipFilter: true }))
283
-
284
- // Should complete successfully without interactive prompts and stay on source branch
285
- const currentBranch = await getCurrentBranch(tempDir)
286
- expect(currentBranch).toBe("agency--feature")
287
- })
288
-
289
- test("accepts explicit base branch argument", async () => {
290
- await createBranch(tempDir, "agency--feature")
291
- await createCommit(tempDir, "Feature commit")
292
-
293
- // Create emit branch with explicit base branch (skip filter for speed)
294
- await runTestEffect(
295
- emit({ baseBranch: "main", silent: true, skipFilter: true }),
296
- )
297
-
298
- // Should stay on source branch
299
- const currentBranch = await getCurrentBranch(tempDir)
300
- expect(currentBranch).toBe("agency--feature")
301
- })
302
-
303
- test("throws error if provided base branch does not exist", async () => {
304
- await createBranch(tempDir, "agency--feature")
305
- await createCommit(tempDir, "Feature commit")
306
-
307
- // This should fail even with skipFilter since base branch validation happens first
308
- expect(
309
- runTestEffect(
310
- emit({ baseBranch: "nonexistent", silent: true, skipFilter: true }),
311
- ),
312
- ).rejects.toThrow("does not exist")
313
- })
314
- })
315
-
316
- describe("error handling", () => {
317
- test("throws error when not in a git repository", async () => {
318
- const nonGitDir = await createTempDir()
319
- process.chdir(nonGitDir)
320
-
321
- expect(runTestEffect(emit({ silent: true }))).rejects.toThrow(
322
- "Not in a git repository",
323
- )
324
-
325
- await cleanupTempDir(nonGitDir)
326
- })
327
- })
328
-
329
- describe("silent mode", () => {
330
- test("silent flag suppresses output", async () => {
331
- await createBranch(tempDir, "agency--feature")
332
- await createCommit(tempDir, "Feature commit")
333
-
334
- const logs: string[] = []
335
- const originalLog = console.log
336
- console.log = (...args: any[]) => logs.push(args.join(" "))
337
-
338
- // Skip filter for speed
339
- await runTestEffect(emit({ silent: true, skipFilter: true }))
340
-
341
- console.log = originalLog
342
-
343
- expect(logs.length).toBe(0)
344
- })
345
- })
346
-
347
- describe("filter-repo command construction (with mock)", () => {
348
- beforeEach(() => {
349
- clearCapturedFilterRepoCalls()
350
- })
351
-
352
- test("throws when emitted CLAUDE.md references AGENCY.md", async () => {
353
- await checkoutBranch(tempDir, "main")
354
- await createBranch(tempDir, "agency--claude-reference-test")
355
-
356
- await Bun.write(
357
- join(tempDir, "agency.json"),
358
- JSON.stringify({
359
- version: 1,
360
- injectedFiles: ["AGENTS.md"],
361
- template: "test",
362
- createdAt: new Date().toISOString(),
363
- }),
364
- )
365
- await Bun.write(
366
- join(tempDir, "CLAUDE.md"),
367
- "# Instructions\n\n@AGENCY.md\n",
368
- )
369
- await addAndCommit(
370
- tempDir,
371
- ["agency.json", "CLAUDE.md"],
372
- "Add Claude agency reference",
373
- )
374
-
375
- await expect(
376
- runTestEffectWithMockFilterRepo(emit({ silent: true })),
377
- ).rejects.toThrow(
378
- "CLAUDE.md on emitted branch claude-reference-test still references @AGENCY.md",
379
- )
380
- })
381
-
382
- test("constructs correct filter-repo arguments", async () => {
383
- // Set up fresh branch with agency.json
384
- await checkoutBranch(tempDir, "main")
385
- await createBranch(tempDir, "agency--filter-test")
386
-
387
- // Create agency.json with injected files
388
- await Bun.write(
389
- join(tempDir, "agency.json"),
390
- JSON.stringify({
391
- version: 1,
392
- injectedFiles: ["AGENTS.md"],
393
- template: "test",
394
- createdAt: new Date().toISOString(),
395
- }),
396
- )
397
- await addAndCommit(tempDir, "agency.json", "Add agency.json")
398
-
399
- // Run emit with mock filter-repo (not skipFilter!)
400
- await runTestEffectWithMockFilterRepo(emit({ silent: true }))
401
-
402
- // Verify filter-repo was called with correct arguments
403
- const lastCall = getLastCapturedFilterRepoCall()
404
- expect(lastCall).toBeDefined()
405
-
406
- // Should include paths for base files (TASK.md, AGENCY.md, agency.json)
407
- // Note: CLAUDE.md is NOT in base files - it's only filtered if it was created
408
- // by agency (in which case it would be in injectedFiles)
409
- // and injected files (AGENTS.md)
410
- expect(lastCall!.args).toContain("--path")
411
- expect(lastCall!.args).toContain("TASK.md")
412
- expect(lastCall!.args).toContain("AGENCY.md")
413
- expect(lastCall!.args).toContain("agency.json")
414
- expect(lastCall!.args).toContain("AGENTS.md")
415
-
416
- // Should include invert-paths and force flags
417
- expect(lastCall!.args).toContain("--invert-paths")
418
- expect(lastCall!.args).toContain("--force")
419
-
420
- // Should include refs for the commit range
421
- expect(lastCall!.args).toContain("--refs")
422
-
423
- // Should have GIT_CONFIG_GLOBAL env set
424
- expect(lastCall!.env?.GIT_CONFIG_GLOBAL).toBe("")
425
- })
426
-
427
- test("streams git-filter-repo output in verbose mode", async () => {
428
- await checkoutBranch(tempDir, "main")
429
- await createBranch(tempDir, "agency--verbose-filter-test")
430
-
431
- await Bun.write(
432
- join(tempDir, "agency.json"),
433
- JSON.stringify({
434
- version: 1,
435
- injectedFiles: ["AGENTS.md"],
436
- template: "test",
437
- createdAt: new Date().toISOString(),
438
- }),
439
- )
440
- await addAndCommit(tempDir, "agency.json", "Add agency.json")
441
-
442
- await runTestEffectWithMockFilterRepo(
443
- emit({ silent: true, verbose: true }),
444
- )
445
-
446
- const lastCall = getLastCapturedFilterRepoCall()
447
- expect(lastCall).toBeDefined()
448
- expect(lastCall!.streamOutput).toBe(true)
449
- expect(lastCall!.progressIntervalMs).toBe(5000)
450
- })
451
-
452
- test("logs filter preflight diagnostics in verbose mode", async () => {
453
- await checkoutBranch(tempDir, "main")
454
- await createBranch(tempDir, "agency--filter-diagnostics-test")
455
-
456
- await Bun.write(
457
- join(tempDir, "agency.json"),
458
- JSON.stringify({
459
- version: 1,
460
- injectedFiles: ["AGENTS.md"],
461
- template: "test",
462
- createdAt: new Date().toISOString(),
463
- }),
464
- )
465
- await addAndCommit(tempDir, "agency.json", "Add agency.json")
466
-
467
- const logs: string[] = []
468
- const originalLog = console.log
469
- console.log = (...args: any[]) => logs.push(args.join(" "))
470
-
471
- try {
472
- await runTestEffectWithMockFilterRepo(emit({ verbose: true }))
473
- } finally {
474
- console.log = originalLog
475
- }
476
-
477
- expect(logs.join("\n")).toContain("Commit range contains")
478
- expect(logs.join("\n")).toContain("Filtered paths are touched by")
479
- expect(logs.join("\n")).toContain("Filtered path diff contains")
480
- })
481
-
482
- test("includes symlink targets in files to filter", async () => {
483
- // Set up fresh branch
484
- await checkoutBranch(tempDir, "main")
485
- await createBranch(tempDir, "agency--symlink-test")
486
-
487
- // Create AGENTS.md as the real file
488
- await Bun.write(join(tempDir, "AGENTS.md"), "# Real file\n")
489
-
490
- // Create CLAUDE.md as a symlink to AGENTS.md
491
- await symlink("AGENTS.md", join(tempDir, "CLAUDE.md"))
492
-
493
- // Create agency.json with CLAUDE.md in injectedFiles (since it's no longer in baseFiles)
494
- await Bun.write(
495
- join(tempDir, "agency.json"),
496
- JSON.stringify({
497
- version: 1,
498
- injectedFiles: ["CLAUDE.md"],
499
- template: "test",
500
- createdAt: new Date().toISOString(),
501
- }),
502
- )
503
- await addAndCommit(
504
- tempDir,
505
- ["AGENTS.md", "CLAUDE.md", "agency.json"],
506
- "Add files with symlink",
507
- )
508
-
509
- // Run emit with mock filter-repo
510
- await runTestEffectWithMockFilterRepo(emit({ silent: true }))
511
-
512
- // Verify filter-repo was called with both CLAUDE.md AND AGENTS.md
513
- const lastCall = getLastCapturedFilterRepoCall()
514
- expect(lastCall).toBeDefined()
515
-
516
- // Should include CLAUDE.md (the symlink)
517
- expect(lastCall!.args).toContain("CLAUDE.md")
518
-
519
- // Should also include AGENTS.md (the symlink target)
520
- // This is the key assertion - the symlink target should be filtered too
521
- expect(lastCall!.args).toContain("AGENTS.md")
522
- })
523
-
524
- test("expands injected dot-directory globs into filter paths", async () => {
525
- await checkoutBranch(tempDir, "main")
526
- await createBranch(tempDir, "agency--dotdir-test")
527
-
528
- await Bun.spawn(["mkdir", "-p", join(tempDir, ".agents", "foo")], {
529
- stdout: "pipe",
530
- stderr: "pipe",
531
- }).exited
532
-
533
- await Bun.write(
534
- join(tempDir, "agency.json"),
535
- JSON.stringify({
536
- version: 1,
537
- injectedFiles: [".agents/**"],
538
- template: "test",
539
- createdAt: new Date().toISOString(),
540
- }),
541
- )
542
- await Bun.write(join(tempDir, ".agents", "foo", "bar.whatever"), "test\n")
543
- await addAndCommit(
544
- tempDir,
545
- ["agency.json", ".agents/foo/bar.whatever"],
546
- "Add template dot-directory file",
547
- )
548
-
549
- await runTestEffectWithMockFilterRepo(emit({ silent: true }))
550
-
551
- const lastCall = getLastCapturedFilterRepoCall()
552
- expect(lastCall).toBeDefined()
553
- expect(lastCall!.args).toContain(".agents/foo/bar.whatever")
554
- })
555
- })
556
-
557
- describe("git path resolution", () => {
558
- test("resolves relative git internal paths from git root", () => {
559
- expect(
560
- resolveGitInternalPath(
561
- "/repo/worktree",
562
- "../.git/worktrees/worktree/filter-repo",
563
- ),
564
- ).toBe("/repo/.git/worktrees/worktree/filter-repo")
565
- })
566
-
567
- test("preserves absolute git internal paths", () => {
568
- expect(
569
- resolveGitInternalPath(
570
- "/repo/worktree",
571
- "/repo/.git/worktrees/worktree/filter-repo",
572
- ),
573
- ).toBe("/repo/.git/worktrees/worktree/filter-repo")
574
- })
575
- })
576
- })