@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,406 +0,0 @@
1
- import { join } from "node:path"
2
- import { Effect } from "effect"
3
- import type { BaseCommandOptions } from "../utils/command"
4
- import { FileSystemService } from "../services/FileSystemService"
5
- import { GitService } from "../services/GitService"
6
- import { createLoggers, ensureGitRepo } from "../utils/effect"
7
- import { spawnProcess } from "../utils/process"
8
- import {
9
- parseTaskItems,
10
- countTasks,
11
- areAllTasksComplete,
12
- extractCompletionPromise,
13
- validateCompletion,
14
- } from "../utils/task-parser"
15
- import highlight, { done, info } from "../utils/colors"
16
-
17
- interface LoopOptions extends BaseCommandOptions {
18
- /**
19
- * Maximum number of iterations (undefined = unlimited)
20
- */
21
- maxLoops?: number
22
- /**
23
- * Minimum number of iterations to enforce
24
- */
25
- minLoops?: number
26
- /**
27
- * Force use of OpenCode CLI
28
- */
29
- opencode?: boolean
30
- /**
31
- * Force use of Claude Code CLI
32
- */
33
- claude?: boolean
34
- /**
35
- * Additional arguments to pass to the CLI tool
36
- */
37
- extraArgs?: string[]
38
- }
39
-
40
- interface LoopState {
41
- iteration: number
42
- tasksCompleted: number
43
- tasksIncomplete: number
44
- tasksTotalAtStart: number
45
- iterationStartTime: number
46
- harnessFailed: boolean
47
- }
48
-
49
- export const loop = (options: LoopOptions = {}) =>
50
- Effect.gen(function* () {
51
- const { log, verboseLog } = createLoggers(options)
52
- const fs = yield* FileSystemService
53
-
54
- const gitRoot = yield* ensureGitRepo()
55
-
56
- // Check if TASK.md exists
57
- const taskPath = join(gitRoot, "TASK.md")
58
- const taskExists = yield* fs.exists(taskPath)
59
- if (!taskExists) {
60
- return yield* Effect.fail(
61
- new Error("TASK.md not found. Run 'agency task' first to create it."),
62
- )
63
- }
64
-
65
- verboseLog(`Found TASK.md at: ${taskPath}`)
66
-
67
- // Change to git root before executing and restore afterwards
68
- // Do not change global cwd; pass gitRoot explicitly to subprocesses
69
-
70
- // Check for conflicting flags
71
- if (options.opencode && options.claude) {
72
- return yield* Effect.fail(
73
- new Error(
74
- "Cannot use both --opencode and --claude flags together. Choose one.",
75
- ),
76
- )
77
- }
78
-
79
- // Validate loop bounds
80
- if (
81
- typeof options.minLoops === "number" &&
82
- typeof options.maxLoops === "number" &&
83
- options.minLoops > options.maxLoops
84
- ) {
85
- return yield* Effect.fail(
86
- new Error("--min-loops cannot be greater than --max-loops."),
87
- )
88
- }
89
-
90
- // Check which CLI tool is available
91
- const hasOpencode = yield* Effect.tryPromise({
92
- try: async () => {
93
- const result = Bun.spawnSync(["which", "opencode"], {
94
- stdout: "ignore",
95
- stderr: "ignore",
96
- })
97
- return result.exitCode === 0
98
- },
99
- catch: () => false,
100
- })
101
-
102
- const hasClaude = yield* Effect.tryPromise({
103
- try: async () => {
104
- const result = Bun.spawnSync(["which", "claude"], {
105
- stdout: "ignore",
106
- stderr: "ignore",
107
- })
108
- return result.exitCode === 0
109
- },
110
- catch: () => false,
111
- })
112
-
113
- // Determine which CLI to use based on flags or auto-detection
114
- let useOpencode: boolean
115
- if (options.opencode) {
116
- if (!hasOpencode) {
117
- return yield* Effect.fail(
118
- new Error(
119
- "opencode CLI tool not found. Please install OpenCode or remove the --opencode flag.",
120
- ),
121
- )
122
- }
123
- useOpencode = true
124
- verboseLog("Using opencode (explicitly requested)")
125
- } else if (options.claude) {
126
- if (!hasClaude) {
127
- return yield* Effect.fail(
128
- new Error(
129
- "claude CLI tool not found. Please install Claude Code or remove the --claude flag.",
130
- ),
131
- )
132
- }
133
- useOpencode = false
134
- verboseLog("Using claude (explicitly requested)")
135
- } else {
136
- // Auto-detect
137
- if (!hasOpencode && !hasClaude) {
138
- return yield* Effect.fail(
139
- new Error(
140
- "Neither opencode nor claude CLI tool found. Please install OpenCode or Claude Code.",
141
- ),
142
- )
143
- }
144
- useOpencode = hasOpencode
145
- verboseLog(`Using ${useOpencode ? "opencode" : "claude"} (auto-detected)`)
146
- }
147
-
148
- const cliName = useOpencode ? "opencode" : "claude"
149
-
150
- // Parse initial task state
151
- let taskContent = yield* fs.readFile(taskPath)
152
- let taskItems = parseTaskItems(taskContent)
153
- let taskCounts = countTasks(taskItems)
154
- const tasksTotalAtStart = taskCounts.total
155
-
156
- // If there are no tasks at all, treat as complete but still respect minLoops
157
- const noTasks = tasksTotalAtStart === 0
158
-
159
- verboseLog(
160
- `Initial state: ${taskCounts.completed}/${taskCounts.total} tasks complete`,
161
- )
162
-
163
- const loopStartTime = Date.now()
164
- let loopState: LoopState = {
165
- iteration: 0,
166
- tasksCompleted: taskCounts.completed,
167
- tasksIncomplete: taskCounts.incomplete,
168
- tasksTotalAtStart,
169
- iterationStartTime: loopStartTime,
170
- harnessFailed: false,
171
- }
172
-
173
- // Main loop
174
- while (true) {
175
- loopState.iteration++
176
- loopState.iterationStartTime = Date.now()
177
- loopState.harnessFailed = false
178
-
179
- // Check if we've exceeded max loops
180
- if (options.maxLoops && loopState.iteration > options.maxLoops) {
181
- break
182
- }
183
-
184
- // Log iteration start
185
- const maxStr = options.maxLoops ? `/${options.maxLoops}` : ""
186
- const minStr = options.minLoops ? ` (min: ${options.minLoops})` : ""
187
- log(
188
- info(
189
- `Loop ${loopState.iteration}${maxStr}${minStr} - ${loopState.tasksCompleted}/${loopState.tasksTotalAtStart} tasks complete`,
190
- ),
191
- )
192
-
193
- // Prepare harness command
194
- const harnessPassed = yield* runHarness(
195
- cliName,
196
- useOpencode,
197
- gitRoot,
198
- options.verbose ?? false,
199
- verboseLog,
200
- options.extraArgs,
201
- ).pipe(
202
- Effect.map(() => true),
203
- Effect.catchAll(() => Effect.succeed(false)),
204
- )
205
-
206
- if (!harnessPassed) {
207
- verboseLog(
208
- `Harness failed on iteration ${loopState.iteration}, retrying...`,
209
- )
210
- loopState.harnessFailed = true
211
- // Retry once
212
- const harnessPassed2 = yield* runHarness(
213
- cliName,
214
- useOpencode,
215
- gitRoot,
216
- options.verbose ?? false,
217
- verboseLog,
218
- options.extraArgs,
219
- ).pipe(
220
- Effect.map(() => true),
221
- Effect.catchAll(() => Effect.succeed(false)),
222
- )
223
-
224
- if (!harnessPassed2) {
225
- return yield* Effect.fail(
226
- new Error(
227
- `Harness failed twice on iteration ${loopState.iteration}. Stopping loop.`,
228
- ),
229
- )
230
- }
231
- }
232
-
233
- // Re-read TASK.md and check status
234
- taskContent = yield* fs.readFile(taskPath)
235
- taskItems = parseTaskItems(taskContent)
236
- taskCounts = countTasks(taskItems)
237
-
238
- loopState.tasksCompleted = taskCounts.completed
239
- loopState.tasksIncomplete = taskCounts.incomplete
240
-
241
- const allComplete = noTasks ? true : areAllTasksComplete(taskItems)
242
- const completionPromise = extractCompletionPromise(taskContent)
243
-
244
- // Validate completion claim if agent says it's done
245
- if (completionPromise) {
246
- try {
247
- validateCompletion(completionPromise, allComplete)
248
- } catch (error) {
249
- return yield* Effect.fail(error)
250
- }
251
- }
252
-
253
- // Log iteration summary
254
- const iterationDuration = Date.now() - loopState.iterationStartTime
255
- const durationSecs = (iterationDuration / 1000).toFixed(1)
256
- log(
257
- done(
258
- `Loop ${loopState.iteration} complete (${durationSecs}s) - ${loopState.tasksCompleted}/${loopState.tasksTotalAtStart} tasks complete`,
259
- ),
260
- )
261
-
262
- // Check if we should exit
263
- if (
264
- allComplete &&
265
- (!options.minLoops || loopState.iteration >= options.minLoops)
266
- ) {
267
- // Ensure TASK.md does not contain the completion promise
268
- const fs = yield* FileSystemService
269
- const taskPath = `${gitRoot}/TASK.md`
270
- if (yield* fs.exists(taskPath)) {
271
- const contents = yield* fs.readFile(taskPath)
272
- const cleaned =
273
- contents.replace(/<promise>[^<]*<\/promise>/gi, "").trimEnd() + "\n"
274
- if (cleaned !== contents) {
275
- yield* fs.writeFile(taskPath, cleaned)
276
- // Commit cleanup so final TASK.md is clean
277
- const git = yield* GitService
278
- yield* git.gitAdd(["TASK.md"], gitRoot)
279
- yield* git.gitCommit(
280
- "fix(loop): remove completion promise from TASK.md",
281
- gitRoot,
282
- )
283
- }
284
- }
285
- log(done(`All tasks complete!`))
286
- break
287
- }
288
-
289
- // Check if we've hit max loops
290
- if (options.maxLoops && loopState.iteration >= options.maxLoops) {
291
- if (!allComplete) {
292
- log(
293
- info(
294
- `Max loops (${options.maxLoops}) reached but tasks remain. Stopping.`,
295
- ),
296
- )
297
- }
298
- break
299
- }
300
- }
301
-
302
- // Final summary
303
- const totalDuration = Date.now() - loopStartTime
304
- const totalDurationSecs = (totalDuration / 1000).toFixed(1)
305
- log(
306
- done(
307
- `Loop completed in ${loopState.iteration} iteration${loopState.iteration === 1 ? "" : "s"} (${totalDurationSecs}s)`,
308
- ),
309
- )
310
- log(
311
- done(
312
- `Final state: ${loopState.tasksCompleted}/${loopState.tasksTotalAtStart} tasks complete`,
313
- ),
314
- )
315
- })
316
-
317
- /**
318
- * Run the harness (opencode or claude run) for a single iteration
319
- */
320
- function runHarness(
321
- cliName: string,
322
- useOpencode: boolean,
323
- gitRoot: string,
324
- verbose: boolean,
325
- verboseLog: (msg: string) => void,
326
- extraArgs?: string[],
327
- ): Effect.Effect<void, Error> {
328
- return Effect.gen(function* () {
329
- const prompt =
330
- "Find the next logical task from TASK.md and work on it until complete. Update TASK.md with progress, and commit your changes. Once all tasks are done, output <promise>COMPLETE</promise>"
331
-
332
- // opencode: use `opencode run "prompt"` for non-interactive mode
333
- // claude: use `claude "prompt"` (positional argument)
334
- const baseArgs = useOpencode ? [cliName, "run", prompt] : [cliName, prompt]
335
-
336
- // Append extra args if provided
337
- const cliArgs =
338
- extraArgs && extraArgs.length > 0 ? [...baseArgs, ...extraArgs] : baseArgs
339
-
340
- // Log the command being run when verbose
341
- // Quote arguments that contain spaces for proper display
342
- const quotedArgs = cliArgs.map((arg) =>
343
- arg.includes(" ") ? `"${arg}"` : arg,
344
- )
345
- verboseLog(`Running: ${quotedArgs.join(" ")}`)
346
-
347
- // When verbose, stream output directly to the terminal
348
- // When not verbose, capture output silently
349
- // Always inherit stdin so the process doesn't hang waiting for input
350
- const result = yield* spawnProcess(cliArgs, {
351
- cwd: gitRoot,
352
- stdin: "inherit",
353
- stdout: verbose ? "inherit" : "pipe",
354
- stderr: verbose ? "inherit" : "pipe",
355
- }).pipe(
356
- Effect.catchAll((error) =>
357
- Effect.fail(new Error(`${cliName} exited with error: ${error.stderr}`)),
358
- ),
359
- )
360
-
361
- if (result.exitCode !== 0) {
362
- return yield* Effect.fail(
363
- new Error(`${cliName} exited with code ${result.exitCode}`),
364
- )
365
- }
366
- })
367
- }
368
-
369
- export const help = `
370
- Usage: agency loop [options] [-- extra-args...]
371
-
372
- Run a Ralph Wiggum loop that repeatedly invokes the harness (opencode or claude)
373
- to work on tasks defined in TASK.md until all tasks are complete.
374
-
375
- Each iteration:
376
- 1. Invokes the harness with a prompt to work on the next task
377
- 2. Checks TASK.md for task completion status
378
- 3. Validates completion claims against actual task status
379
- 4. Commits changes if any were made
380
- 5. Repeats until all tasks are done or max loops reached
381
-
382
- Options:
383
- --max-loops <num> Maximum number of iterations (default: unlimited)
384
- --min-loops <num> Minimum number of iterations (safety check)
385
- --opencode Force use of OpenCode CLI
386
- --claude Force use of Claude Code CLI
387
- -h, --help Show this help message
388
- -s, --silent Suppress output messages
389
- -v, --verbose Stream harness output to terminal
390
-
391
- Pass-through Arguments:
392
- Use -- to pass additional arguments to the underlying CLI tool.
393
- Everything after -- will be forwarded to opencode or claude.
394
-
395
- Examples:
396
- agency loop # Run until all tasks are done
397
- agency loop --max-loops 5 # Stop after 5 iterations
398
- agency loop -- --agent deep # Pass --agent deep to harness
399
-
400
- Notes:
401
- - Requires TASK.md to exist (run 'agency task' first)
402
- - Each iteration calls the harness which handles its own commit
403
- - The loop validates that completion claims match actual task status
404
- - Use --min-loops as a safeguard if you know the minimum work required
405
- - Arguments after -- are passed directly to the underlying tool
406
- `
@@ -1,195 +0,0 @@
1
- import { test, expect, describe, beforeEach, afterEach } from "bun:test"
2
- import { join } from "path"
3
- import { merge } from "./merge"
4
- import { emit } from "./emit"
5
- import { task } from "./task"
6
- import {
7
- createTempDir,
8
- cleanupTempDir,
9
- initGitRepo,
10
- initAgency,
11
- getGitOutput,
12
- getCurrentBranch,
13
- createCommit,
14
- branchExists,
15
- checkoutBranch,
16
- createBranch,
17
- addAndCommit,
18
- setupRemote,
19
- deleteBranch,
20
- runTestEffect,
21
- } from "../test-utils"
22
-
23
- // Cache the git-filter-repo availability check (it doesn't change during test run)
24
- let hasGitFilterRepoCache: boolean | null = null
25
- async function checkGitFilterRepo(): Promise<boolean> {
26
- if (hasGitFilterRepoCache === null) {
27
- const proc = Bun.spawn(["which", "git-filter-repo"], {
28
- stdout: "pipe",
29
- stderr: "pipe",
30
- })
31
- await proc.exited
32
- hasGitFilterRepoCache = proc.exitCode === 0
33
- }
34
- return hasGitFilterRepoCache
35
- }
36
-
37
- describe("merge command - integration tests (requires git-filter-repo)", () => {
38
- let tempDir: string
39
- let originalCwd: string
40
- let hasGitFilterRepo: boolean
41
-
42
- beforeEach(async () => {
43
- tempDir = await createTempDir()
44
- originalCwd = process.cwd()
45
- process.chdir(tempDir)
46
-
47
- // Set config path to non-existent file to use defaults
48
- process.env.AGENCY_CONFIG_PATH = join(tempDir, "non-existent-config.json")
49
- // Set config dir to temp dir to avoid picking up user's config files
50
- process.env.AGENCY_CONFIG_DIR = await createTempDir()
51
-
52
- // Check if git-filter-repo is available (cached)
53
- hasGitFilterRepo = await checkGitFilterRepo()
54
-
55
- // Initialize git repo with main branch (already includes initial commit)
56
- await initGitRepo(tempDir)
57
-
58
- // Set up origin for git-filter-repo
59
- await setupRemote(tempDir, "origin", tempDir)
60
-
61
- // Create a source branch (with agency-- prefix per new default config)
62
- await createBranch(tempDir, "agency--feature")
63
-
64
- // Initialize AGENTS.md on feature branch
65
- await initAgency(tempDir, "test")
66
-
67
- await runTestEffect(task({ silent: true, fromCurrent: true }))
68
-
69
- // Ensure agency.json has baseBranch set (task should auto-detect it, but ensure it's there)
70
- const agencyJsonPath = join(tempDir, "agency.json")
71
- const agencyJson = await Bun.file(agencyJsonPath).json()
72
- if (!agencyJson.baseBranch) {
73
- agencyJson.baseBranch = "origin/main"
74
- await Bun.write(
75
- agencyJsonPath,
76
- JSON.stringify(agencyJson, null, 2) + "\n",
77
- )
78
- }
79
-
80
- await addAndCommit(tempDir, "AGENTS.md agency.json", "Add AGENTS.md")
81
-
82
- // Create another commit on feature branch
83
- await createCommit(tempDir, "Feature work")
84
- })
85
-
86
- afterEach(async () => {
87
- process.chdir(originalCwd)
88
- delete process.env.AGENCY_CONFIG_PATH
89
- if (process.env.AGENCY_CONFIG_DIR) {
90
- await cleanupTempDir(process.env.AGENCY_CONFIG_DIR)
91
- delete process.env.AGENCY_CONFIG_DIR
92
- }
93
- await cleanupTempDir(tempDir)
94
- })
95
-
96
- describe("merge from source branch", () => {
97
- test("creates emit branch and merges when run from source branch", async () => {
98
- if (!hasGitFilterRepo) {
99
- console.log("Skipping test: git-filter-repo not installed")
100
- return
101
- }
102
-
103
- // We're on feature branch (source)
104
- const currentBranch = await getCurrentBranch(tempDir)
105
- expect(currentBranch).toBe("agency--feature")
106
-
107
- // Run merge - should create feature--PR and merge it to main
108
- await runTestEffect(merge({ silent: true }))
109
-
110
- // Should be on main branch after merge
111
- const afterMergeBranch = await getCurrentBranch(tempDir)
112
- expect(afterMergeBranch).toBe("main")
113
-
114
- // emit branch should exist
115
- const prExists = await branchExists(tempDir, "feature")
116
- expect(prExists).toBe(true)
117
-
118
- // Main should have the feature work but not AGENTS.md
119
- const files = await getGitOutput(tempDir, ["ls-files"])
120
- expect(files).not.toContain("AGENTS.md")
121
- })
122
-
123
- test("recreates emit branch if it already exists", async () => {
124
- if (!hasGitFilterRepo) {
125
- console.log("Skipping test: git-filter-repo not installed")
126
- return
127
- }
128
-
129
- // Create emit branch first
130
- await runTestEffect(emit({ silent: true }))
131
-
132
- // Go back to feature branch
133
- await checkoutBranch(tempDir, "agency--feature")
134
-
135
- // Make additional changes
136
- await createCommit(tempDir, "More feature work")
137
-
138
- // Run merge - should recreate emit branch with new changes
139
- await runTestEffect(merge({ silent: true }))
140
-
141
- // Should be on main after merge
142
- const currentBranch = await getCurrentBranch(tempDir)
143
- expect(currentBranch).toBe("main")
144
- })
145
- })
146
-
147
- describe("merge from emit branch", () => {
148
- test("merges emit branch directly when run from emit branch", async () => {
149
- if (!hasGitFilterRepo) {
150
- console.log("Skipping test: git-filter-repo not installed")
151
- return
152
- }
153
-
154
- // Create emit branch
155
- await runTestEffect(emit({ silent: true }))
156
-
157
- // emit() now stays on source branch, so we need to checkout to emit branch
158
- await checkoutBranch(tempDir, "feature")
159
-
160
- // We're on feature--PR now
161
- const currentBranch = await getCurrentBranch(tempDir)
162
- expect(currentBranch).toBe("feature")
163
-
164
- // Run merge - should merge feature--PR to main
165
- await runTestEffect(merge({ silent: true }))
166
-
167
- // Should be on main after merge
168
- const afterMergeBranch = await getCurrentBranch(tempDir)
169
- expect(afterMergeBranch).toBe("main")
170
-
171
- // Main should have the feature work but not AGENTS.md
172
- const files = await getGitOutput(tempDir, ["ls-files"])
173
- expect(files).not.toContain("AGENTS.md")
174
- })
175
-
176
- test("throws error if emit branch has no corresponding source branch", async () => {
177
- if (!hasGitFilterRepo) {
178
- console.log("Skipping test: git-filter-repo not installed")
179
- return
180
- }
181
-
182
- // Create emit branch
183
- await runTestEffect(emit({ silent: true }))
184
-
185
- // pr() now stays on source branch, so checkout to emit branch
186
- await checkoutBranch(tempDir, "feature")
187
-
188
- // Delete the source branch
189
- await deleteBranch(tempDir, "agency--feature", true)
190
-
191
- // Try to merge - should fail (error message may vary since source branch is deleted)
192
- await expect(runTestEffect(merge({ silent: true }))).rejects.toThrow()
193
- })
194
- })
195
- })