@markjaquith/agency 1.11.0 → 2.0.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 (130) hide show
  1. package/README.md +302 -113
  2. package/cli.ts +123 -415
  3. package/index.ts +1 -1
  4. package/package.json +12 -17
  5. package/skills/agency/SKILL.md +258 -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 +172 -419
  24. package/src/commands/work.ts +47 -166
  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/worktree-command.test.ts +61 -0
  53. package/src/workbase/worktree-command.ts +52 -0
  54. package/src/commands/base.test.ts +0 -198
  55. package/src/commands/base.ts +0 -198
  56. package/src/commands/clean.test.ts +0 -299
  57. package/src/commands/clean.ts +0 -274
  58. package/src/commands/completions.test.ts +0 -51
  59. package/src/commands/completions.ts +0 -308
  60. package/src/commands/emit.integration.test.ts +0 -549
  61. package/src/commands/emit.test.ts +0 -576
  62. package/src/commands/emit.ts +0 -700
  63. package/src/commands/emitted.test.ts +0 -226
  64. package/src/commands/emitted.ts +0 -57
  65. package/src/commands/loop.test.ts +0 -621
  66. package/src/commands/loop.ts +0 -406
  67. package/src/commands/merge.integration.test.ts +0 -195
  68. package/src/commands/merge.test.ts +0 -246
  69. package/src/commands/merge.ts +0 -244
  70. package/src/commands/pull.test.ts +0 -395
  71. package/src/commands/pull.ts +0 -205
  72. package/src/commands/push.test.ts +0 -518
  73. package/src/commands/push.ts +0 -434
  74. package/src/commands/rebase.test.ts +0 -521
  75. package/src/commands/rebase.ts +0 -232
  76. package/src/commands/save.test.ts +0 -247
  77. package/src/commands/save.ts +0 -162
  78. package/src/commands/source.test.ts +0 -195
  79. package/src/commands/source.ts +0 -72
  80. package/src/commands/switch.test.ts +0 -194
  81. package/src/commands/switch.ts +0 -84
  82. package/src/commands/task-branching.test.ts +0 -633
  83. package/src/commands/task-continue.test.ts +0 -311
  84. package/src/commands/task-edit.test.ts +0 -235
  85. package/src/commands/task-main.test.ts +0 -1066
  86. package/src/commands/task-squash.test.ts +0 -266
  87. package/src/commands/tasks.test.ts +0 -391
  88. package/src/commands/tasks.ts +0 -161
  89. package/src/commands/template-delete.test.ts +0 -178
  90. package/src/commands/template-delete.ts +0 -98
  91. package/src/commands/template-list.test.ts +0 -135
  92. package/src/commands/template-list.ts +0 -87
  93. package/src/commands/template-view.test.ts +0 -158
  94. package/src/commands/template-view.ts +0 -86
  95. package/src/commands/template.test.ts +0 -32
  96. package/src/commands/template.ts +0 -96
  97. package/src/commands/use.test.ts +0 -87
  98. package/src/commands/use.ts +0 -97
  99. package/src/constants.ts +0 -10
  100. package/src/errors.ts +0 -17
  101. package/src/schemas.ts +0 -33
  102. package/src/services/AgencyMetadataService.ts +0 -311
  103. package/src/services/ClaudeService.test.ts +0 -184
  104. package/src/services/ClaudeService.ts +0 -91
  105. package/src/services/ConfigService.ts +0 -115
  106. package/src/services/FilterRepoService.ts +0 -164
  107. package/src/services/FormatterService.test.ts +0 -432
  108. package/src/services/FormatterService.ts +0 -219
  109. package/src/services/GitService.ts +0 -1183
  110. package/src/services/MockFilterRepoService.ts +0 -140
  111. package/src/services/OpencodeService.ts +0 -263
  112. package/src/services/PromptService.ts +0 -183
  113. package/src/services/TemplateService.ts +0 -81
  114. package/src/types/native-exec.d.ts +0 -8
  115. package/src/utils/colors.ts +0 -178
  116. package/src/utils/git-path.ts +0 -6
  117. package/src/utils/glob.test.ts +0 -161
  118. package/src/utils/glob.ts +0 -78
  119. package/src/utils/paths.ts +0 -51
  120. package/src/utils/pr-branch.test.ts +0 -376
  121. package/src/utils/pr-branch.ts +0 -508
  122. package/src/utils/spinner.test.ts +0 -45
  123. package/src/utils/spinner.ts +0 -96
  124. package/src/utils/task-parser.test.ts +0 -172
  125. package/src/utils/task-parser.ts +0 -93
  126. package/templates/AGENCY.md +0 -22
  127. package/templates/AGENTS.md +0 -11
  128. package/templates/CLAUDE.md +0 -3
  129. package/templates/TASK.md +0 -5
  130. 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
- })