@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,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
- })