@markjaquith/agency 1.11.1 → 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 -420
  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 -641
  73. package/src/commands/push.ts +0 -482
  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 -1187
  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,376 +0,0 @@
1
- import { describe, test, expect, afterEach } from "bun:test"
2
- import {
3
- makeSourceBranchName,
4
- extractCleanBranch,
5
- makeEmitBranchName,
6
- extractCleanFromEmit,
7
- resolveBranchPairWithAgencyJson,
8
- } from "./pr-branch"
9
- import {
10
- createTempDir,
11
- cleanupTempDir,
12
- initGitRepo,
13
- runTestEffect,
14
- } from "../test-utils"
15
- import { join } from "path"
16
-
17
- describe("makeSourceBranchName", () => {
18
- test("replaces %branch% placeholder with branch name", () => {
19
- expect(makeSourceBranchName("main", "agency--%branch%")).toBe(
20
- "agency--main",
21
- )
22
- expect(makeSourceBranchName("feature-foo", "wip/%branch%")).toBe(
23
- "wip/feature-foo",
24
- )
25
- expect(makeSourceBranchName("main", "%branch%/dev")).toBe("main/dev")
26
- })
27
-
28
- test("treats pattern as prefix when %branch% is not present", () => {
29
- expect(makeSourceBranchName("main", "agency--")).toBe("agency--main")
30
- expect(makeSourceBranchName("feature-foo", "wip-")).toBe("wip-feature-foo")
31
- })
32
-
33
- test("handles empty branch name", () => {
34
- expect(makeSourceBranchName("", "agency--%branch%")).toBe("agency--")
35
- expect(makeSourceBranchName("", "agency--")).toBe("agency--")
36
- })
37
- })
38
-
39
- describe("extractCleanBranch", () => {
40
- describe("with %branch% placeholder", () => {
41
- test("extracts clean branch from source branch name", () => {
42
- expect(extractCleanBranch("agency--main", "agency--%branch%")).toBe(
43
- "main",
44
- )
45
- expect(extractCleanBranch("wip/feature-foo", "wip/%branch%")).toBe(
46
- "feature-foo",
47
- )
48
- expect(extractCleanBranch("main/dev", "%branch%/dev")).toBe("main")
49
- })
50
-
51
- test("returns null when source branch name doesn't match pattern", () => {
52
- expect(extractCleanBranch("main", "agency--%branch%")).toBeNull()
53
- expect(extractCleanBranch("feature-foo", "wip/%branch%")).toBeNull()
54
- expect(extractCleanBranch("agency--main", "wip/%branch%")).toBeNull()
55
- })
56
-
57
- test("returns null for empty clean branch", () => {
58
- expect(extractCleanBranch("agency--", "agency--%branch%")).toBeNull()
59
- expect(extractCleanBranch("wip/", "wip/%branch%")).toBeNull()
60
- })
61
-
62
- test("handles complex patterns", () => {
63
- expect(
64
- extractCleanBranch("pr-feature-foo-ready", "pr-%branch%-ready"),
65
- ).toBe("feature-foo")
66
- expect(
67
- extractCleanBranch("PR/feature/foo/ready", "PR/%branch%/ready"),
68
- ).toBe("feature/foo")
69
- })
70
- })
71
-
72
- describe("without %branch% placeholder (prefix mode)", () => {
73
- test("extracts clean branch by removing prefix", () => {
74
- expect(extractCleanBranch("agency--main", "agency--")).toBe("main")
75
- expect(extractCleanBranch("wip-feature-foo", "wip-")).toBe("feature-foo")
76
- })
77
-
78
- test("returns null when branch doesn't start with prefix", () => {
79
- expect(extractCleanBranch("main", "agency--")).toBeNull()
80
- expect(extractCleanBranch("feature-foo", "wip-")).toBeNull()
81
- })
82
-
83
- test("returns null for empty clean branch", () => {
84
- expect(extractCleanBranch("agency--", "agency--")).toBeNull()
85
- expect(extractCleanBranch("wip-", "wip-")).toBeNull()
86
- })
87
- })
88
- })
89
-
90
- describe("makeEmitBranchName", () => {
91
- test("returns clean branch when pattern is %branch%", () => {
92
- expect(makeEmitBranchName("main", "%branch%")).toBe("main")
93
- expect(makeEmitBranchName("feature-foo", "%branch%")).toBe("feature-foo")
94
- })
95
-
96
- test("replaces %branch% placeholder with branch name", () => {
97
- expect(makeEmitBranchName("feature-foo", "%branch%--PR")).toBe(
98
- "feature-foo--PR",
99
- )
100
- expect(makeEmitBranchName("feature-foo", "PR/%branch%")).toBe(
101
- "PR/feature-foo",
102
- )
103
- })
104
-
105
- test("treats pattern as suffix when %branch% is not present", () => {
106
- expect(makeEmitBranchName("feature-foo", "--PR")).toBe("feature-foo--PR")
107
- expect(makeEmitBranchName("feature-foo", "-pr")).toBe("feature-foo-pr")
108
- })
109
-
110
- test("handles empty branch name", () => {
111
- expect(makeEmitBranchName("", "%branch%")).toBe("")
112
- expect(makeEmitBranchName("", "%branch%--PR")).toBe("--PR")
113
- expect(makeEmitBranchName("", "--PR")).toBe("--PR")
114
- })
115
- })
116
-
117
- describe("extractCleanFromEmit", () => {
118
- test("returns emit branch when pattern is %branch%", () => {
119
- expect(extractCleanFromEmit("main", "%branch%")).toBe("main")
120
- expect(extractCleanFromEmit("feature-foo", "%branch%")).toBe("feature-foo")
121
- })
122
-
123
- describe("with %branch% placeholder", () => {
124
- test("extracts clean branch from emit branch name", () => {
125
- expect(extractCleanFromEmit("feature-foo--PR", "%branch%--PR")).toBe(
126
- "feature-foo",
127
- )
128
- expect(extractCleanFromEmit("PR/feature-foo", "PR/%branch%")).toBe(
129
- "feature-foo",
130
- )
131
- })
132
-
133
- test("returns null when emit branch name doesn't match pattern", () => {
134
- expect(extractCleanFromEmit("feature-foo", "%branch%--PR")).toBeNull()
135
- expect(extractCleanFromEmit("feature-foo--PR", "PR/%branch%")).toBeNull()
136
- expect(extractCleanFromEmit("main", "%branch%--PR")).toBeNull()
137
- })
138
-
139
- test("returns null for empty clean branch", () => {
140
- expect(extractCleanFromEmit("--PR", "%branch%--PR")).toBeNull()
141
- expect(extractCleanFromEmit("PR/", "PR/%branch%")).toBeNull()
142
- })
143
-
144
- test("handles complex patterns", () => {
145
- expect(
146
- extractCleanFromEmit("pr-feature-foo-ready", "pr-%branch%-ready"),
147
- ).toBe("feature-foo")
148
- expect(
149
- extractCleanFromEmit("PR/feature/foo/ready", "PR/%branch%/ready"),
150
- ).toBe("feature/foo")
151
- })
152
- })
153
-
154
- describe("without %branch% placeholder (suffix mode)", () => {
155
- test("extracts clean branch by removing suffix", () => {
156
- expect(extractCleanFromEmit("feature-foo--PR", "--PR")).toBe(
157
- "feature-foo",
158
- )
159
- expect(extractCleanFromEmit("feature-foo-pr", "-pr")).toBe("feature-foo")
160
- })
161
-
162
- test("returns null when branch doesn't end with suffix", () => {
163
- expect(extractCleanFromEmit("feature-foo", "--PR")).toBeNull()
164
- expect(extractCleanFromEmit("PR-feature-foo", "--PR")).toBeNull()
165
- })
166
-
167
- test("returns null for empty clean branch", () => {
168
- expect(extractCleanFromEmit("--PR", "--PR")).toBeNull()
169
- expect(extractCleanFromEmit("-pr", "-pr")).toBeNull()
170
- })
171
- })
172
- })
173
-
174
- describe("resolveBranchPairWithAgencyJson", () => {
175
- let tempDir: string
176
-
177
- afterEach(async () => {
178
- if (tempDir) {
179
- await cleanupTempDir(tempDir)
180
- }
181
- })
182
-
183
- test("uses agency.json emitBranch when on source branch", async () => {
184
- tempDir = await createTempDir()
185
- await initGitRepo(tempDir)
186
-
187
- // Create source branch with agency.json
188
- await Bun.spawn(["git", "checkout", "-b", "agency--main"], {
189
- cwd: tempDir,
190
- stdout: "pipe",
191
- stderr: "pipe",
192
- }).exited
193
-
194
- const agencyJson = {
195
- version: 1,
196
- injectedFiles: [],
197
- template: "test",
198
- createdAt: new Date().toISOString(),
199
- emitBranch: "main",
200
- }
201
- await Bun.write(join(tempDir, "agency.json"), JSON.stringify(agencyJson))
202
-
203
- const result = await runTestEffect(
204
- resolveBranchPairWithAgencyJson(
205
- tempDir,
206
- "agency--main",
207
- "agency--%branch%",
208
- "%branch%",
209
- ),
210
- )
211
-
212
- expect(result.sourceBranch).toBe("agency--main")
213
- expect(result.emitBranch).toBe("main")
214
- expect(result.isOnEmitBranch).toBe(false)
215
- })
216
-
217
- test("finds source branch by searching for matching emitBranch", async () => {
218
- tempDir = await createTempDir()
219
- await initGitRepo(tempDir)
220
-
221
- // Create a source branch with agency.json
222
- await Bun.spawn(["git", "checkout", "-b", "agency--feature-bar"], {
223
- cwd: tempDir,
224
- stdout: "pipe",
225
- stderr: "pipe",
226
- }).exited
227
-
228
- const agencyJson = {
229
- version: 1,
230
- injectedFiles: [],
231
- template: "test",
232
- createdAt: new Date().toISOString(),
233
- emitBranch: "feature-bar",
234
- }
235
- await Bun.write(join(tempDir, "agency.json"), JSON.stringify(agencyJson))
236
- await Bun.spawn(["git", "add", "agency.json"], {
237
- cwd: tempDir,
238
- stdout: "pipe",
239
- stderr: "pipe",
240
- }).exited
241
- await Bun.spawn(["git", "commit", "-m", "Add agency.json"], {
242
- cwd: tempDir,
243
- stdout: "pipe",
244
- stderr: "pipe",
245
- }).exited
246
-
247
- // Create the emit branch from main (so it doesn't have agency.json)
248
- await Bun.spawn(["git", "checkout", "main"], {
249
- cwd: tempDir,
250
- stdout: "pipe",
251
- stderr: "pipe",
252
- }).exited
253
- await Bun.spawn(["git", "checkout", "-b", "feature-bar"], {
254
- cwd: tempDir,
255
- stdout: "pipe",
256
- stderr: "pipe",
257
- }).exited
258
-
259
- const result = await runTestEffect(
260
- resolveBranchPairWithAgencyJson(
261
- tempDir,
262
- "feature-bar",
263
- "agency--%branch%",
264
- "%branch%",
265
- ),
266
- )
267
-
268
- expect(result.sourceBranch).toBe("agency--feature-bar")
269
- expect(result.emitBranch).toBe("feature-bar")
270
- expect(result.isOnEmitBranch).toBe(true)
271
- })
272
-
273
- test("falls back to pattern-based resolution when agency.json not found", async () => {
274
- tempDir = await createTempDir()
275
- await initGitRepo(tempDir)
276
-
277
- // Create source branch without agency.json
278
- await Bun.spawn(["git", "checkout", "-b", "agency--feature-baz"], {
279
- cwd: tempDir,
280
- stdout: "pipe",
281
- stderr: "pipe",
282
- }).exited
283
-
284
- // No agency.json, should fall back to pattern-based resolution
285
- const result = await runTestEffect(
286
- resolveBranchPairWithAgencyJson(
287
- tempDir,
288
- "agency--feature-baz",
289
- "agency--%branch%",
290
- "%branch%",
291
- ),
292
- )
293
-
294
- expect(result.sourceBranch).toBe("agency--feature-baz")
295
- expect(result.emitBranch).toBe("feature-baz")
296
- expect(result.isOnEmitBranch).toBe(false)
297
- })
298
-
299
- test("treats branch as legacy source when no matching pattern and no source branch exists", async () => {
300
- tempDir = await createTempDir()
301
- await initGitRepo(tempDir)
302
-
303
- // Create a branch without the source pattern (legacy branch)
304
- // No agency--feature-qux exists, so this is a legacy source branch
305
- await Bun.spawn(["git", "checkout", "-b", "feature-qux"], {
306
- cwd: tempDir,
307
- stdout: "pipe",
308
- stderr: "pipe",
309
- }).exited
310
-
311
- const result = await runTestEffect(
312
- resolveBranchPairWithAgencyJson(
313
- tempDir,
314
- "feature-qux",
315
- "agency--%branch%",
316
- "%branch%",
317
- ),
318
- )
319
-
320
- // Legacy branch is treated as source, emit is the same name (with %branch% pattern)
321
- expect(result.sourceBranch).toBe("feature-qux")
322
- expect(result.emitBranch).toBe("feature-qux")
323
- expect(result.isOnEmitBranch).toBe(false)
324
- })
325
-
326
- test("handles branches with no agency.json on current branch", async () => {
327
- tempDir = await createTempDir()
328
- await initGitRepo(tempDir)
329
-
330
- // Create source branch, no agency.json
331
- await Bun.spawn(["git", "checkout", "-b", "agency--main"], {
332
- cwd: tempDir,
333
- stdout: "pipe",
334
- stderr: "pipe",
335
- }).exited
336
-
337
- // Just test pattern-based resolution with no agency.json anywhere
338
- const result = await runTestEffect(
339
- resolveBranchPairWithAgencyJson(
340
- tempDir,
341
- "agency--main",
342
- "agency--%branch%",
343
- "%branch%",
344
- ),
345
- )
346
-
347
- expect(result.sourceBranch).toBe("agency--main")
348
- expect(result.emitBranch).toBe("main")
349
- expect(result.isOnEmitBranch).toBe(false)
350
- })
351
-
352
- test("handles emit pattern with suffix", async () => {
353
- tempDir = await createTempDir()
354
- await initGitRepo(tempDir)
355
-
356
- // Test with custom emit pattern that adds a suffix
357
- await Bun.spawn(["git", "checkout", "-b", "agency--feature"], {
358
- cwd: tempDir,
359
- stdout: "pipe",
360
- stderr: "pipe",
361
- }).exited
362
-
363
- const result = await runTestEffect(
364
- resolveBranchPairWithAgencyJson(
365
- tempDir,
366
- "agency--feature",
367
- "agency--%branch%",
368
- "%branch%--PR",
369
- ),
370
- )
371
-
372
- expect(result.sourceBranch).toBe("agency--feature")
373
- expect(result.emitBranch).toBe("feature--PR")
374
- expect(result.isOnEmitBranch).toBe(false)
375
- })
376
- })