@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
package/src/test-utils.ts CHANGED
@@ -1,454 +1,57 @@
1
- import { mkdtemp, rm, cp } from "fs/promises"
2
- import { tmpdir } from "os"
3
- import { join } from "path"
4
-
5
- // Cache a template git repository to speed up test setup
6
- let templateGitRepo: string | null = null
7
- let templateGitRepoPromise: Promise<string> | null = null
8
-
9
- async function getTemplateGitRepo(): Promise<string> {
10
- // Return cached result if available
11
- if (templateGitRepo) {
12
- return templateGitRepo
13
- }
14
-
15
- // Use a promise to prevent concurrent initialization (race condition fix)
16
- if (templateGitRepoPromise) {
17
- return templateGitRepoPromise
18
- }
19
-
20
- // Create and cache the initialization promise
21
- templateGitRepoPromise = (async () => {
22
- // Create a template git repo once and reuse it
23
- const tempDir = await mkdtemp(join(tmpdir(), "agency-template-"))
24
-
25
- const proc = Bun.spawn(["git", "init", "-b", "main"], {
26
- cwd: tempDir,
27
- stdout: "pipe",
28
- stderr: "pipe",
29
- })
30
- await proc.exited
31
-
32
- if (proc.exitCode !== 0) {
33
- throw new Error("Failed to initialize template git repository")
34
- }
35
-
36
- // Write config directly
37
- const configFile = Bun.file(join(tempDir, ".git", "config"))
38
- const existingConfig = await configFile.text()
39
- const newConfig =
40
- existingConfig +
41
- "\n[user]\n\temail = test@example.com\n\tname = Test User\n[core]\n\thooksPath = /dev/null\n"
42
- await Bun.write(join(tempDir, ".git", "config"), newConfig)
43
-
44
- // Create initial commit
45
- await Bun.write(join(tempDir, ".gitkeep"), "")
46
- await Bun.spawn(["git", "add", ".gitkeep"], {
47
- cwd: tempDir,
48
- stdout: "pipe",
49
- stderr: "pipe",
50
- }).exited
51
- await Bun.spawn(["git", "commit", "-m", "Initial commit"], {
52
- cwd: tempDir,
53
- stdout: "pipe",
54
- stderr: "pipe",
55
- }).exited
56
-
57
- templateGitRepo = tempDir
58
- return tempDir
59
- })()
60
-
61
- return templateGitRepoPromise
62
- }
63
-
64
- /**
65
- * Create a temporary directory for testing
66
- */
67
- export async function createTempDir(): Promise<string> {
68
- return await mkdtemp(join(tmpdir(), "agency-test-"))
69
- }
70
-
71
- /**
72
- * Clean up a temporary directory
73
- */
74
- export async function cleanupTempDir(path: string): Promise<void> {
75
- try {
76
- await rm(path, { recursive: true, force: true })
77
- } catch (error) {
78
- // Ignore errors during cleanup
79
- }
80
- }
81
-
82
- /**
83
- * Initialize a git repository in a directory
84
- * Uses a cached template repository for much faster setup
85
- */
86
- export async function initGitRepo(path: string): Promise<void> {
87
- const template = await getTemplateGitRepo()
88
-
89
- // Copy the template .git directory
90
- await cp(join(template, ".git"), join(path, ".git"), {
91
- recursive: true,
92
- })
93
-
94
- // Copy the .gitkeep file
95
- await cp(join(template, ".gitkeep"), join(path, ".gitkeep"))
96
- }
97
-
98
- /**
99
- * Create a subdirectory in a path
100
- */
101
- export async function createSubdir(
102
- basePath: string,
103
- name: string,
104
- ): Promise<string> {
105
- const subdirPath = join(basePath, name)
106
- await Bun.write(join(subdirPath, ".gitkeep"), "")
107
- return subdirPath
108
- }
109
-
110
- /**
111
- * Check if a file exists
112
- */
113
- export async function fileExists(path: string): Promise<boolean> {
114
- const file = Bun.file(path)
115
- return await file.exists()
116
- }
117
-
118
- /**
119
- * Read file content
120
- */
121
- export async function readFile(path: string): Promise<string> {
122
- const file = Bun.file(path)
123
- return await file.text()
124
- }
125
-
126
- /**
127
- * Execute a git command and return its output
128
- */
129
- export async function getGitOutput(
130
- cwd: string,
131
- args: string[],
132
- ): Promise<string> {
133
- const proc = Bun.spawn(["git", ...args], {
134
- cwd,
135
- stdout: "pipe",
136
- stderr: "pipe",
137
- })
138
- await proc.exited
139
- return await new Response(proc.stdout).text()
140
- }
141
-
142
- /**
143
- * Get the current branch name in a git repository
144
- */
145
- export async function getCurrentBranch(cwd: string): Promise<string> {
146
- const output = await getGitOutput(cwd, ["branch", "--show-current"])
147
- return output.trim()
148
- }
149
-
150
- /**
151
- * Run a git command directly - fire and forget version (fastest)
152
- */
153
- async function gitRun(cwd: string, args: string[]): Promise<void> {
154
- const proc = Bun.spawn(["git", ...args], {
155
- cwd,
156
- stdout: "pipe",
157
- stderr: "pipe",
158
- })
159
- await proc.exited
160
- }
161
-
162
- /**
163
- * Create a test commit in a git repository
164
- */
165
- export async function createCommit(
166
- cwd: string,
167
- message: string,
168
- ): Promise<void> {
169
- // Create a test file and commit it
170
- await Bun.write(join(cwd, "test.txt"), message)
171
- await gitRun(cwd, ["add", "test.txt"])
172
- await gitRun(cwd, ["commit", "--no-verify", "-m", message])
173
- }
174
-
175
- /**
176
- * Checkout a branch in a git repository
177
- */
178
- export async function checkoutBranch(
179
- cwd: string,
180
- branchName: string,
181
- ): Promise<void> {
182
- await gitRun(cwd, [
183
- "-c",
184
- "advice.detachedHead=false",
185
- "checkout",
186
- "-q",
187
- branchName,
188
- ])
189
- }
190
-
191
- /**
192
- * Create a new branch and switch to it
193
- */
194
- export async function createBranch(
195
- cwd: string,
196
- branchName: string,
197
- ): Promise<void> {
198
- await gitRun(cwd, ["checkout", "-q", "-b", branchName])
199
- }
200
-
201
- /**
202
- * Stage files and commit in a single operation
203
- */
204
- export async function addAndCommit(
205
- cwd: string,
206
- files: string | string[],
207
- message: string,
208
- ): Promise<void> {
209
- const fileList = Array.isArray(files) ? files : files.split(" ")
210
- await gitRun(cwd, ["add", ...fileList])
211
- await gitRun(cwd, ["commit", "--no-verify", "-m", message])
212
- }
213
-
214
- /**
215
- * Setup a remote and fetch in a single operation
216
- */
217
- export async function setupRemote(
218
- cwd: string,
219
- remoteName: string,
220
- remoteUrl: string,
221
- ): Promise<void> {
222
- await gitRun(cwd, ["remote", "add", remoteName, remoteUrl])
223
- await gitRun(cwd, ["fetch", remoteName])
224
- }
225
-
226
- /**
227
- * Delete a branch
228
- */
229
- export async function deleteBranch(
230
- cwd: string,
231
- branchName: string,
232
- force: boolean = false,
233
- ): Promise<void> {
234
- const flag = force ? "-D" : "-d"
235
- await gitRun(cwd, ["branch", flag, branchName])
236
- }
237
-
238
- /**
239
- * Reset a git repo to clean state for test reuse.
240
- * This is much faster than creating a new repo from scratch.
241
- * - Checks out main branch
242
- * - Removes all other branches
243
- * - Resets to initial commit
244
- * - Cleans untracked files
245
- * - Removes agency git config
246
- */
247
- export async function resetGitRepo(cwd: string): Promise<void> {
248
- // Checkout main
249
- await gitRun(cwd, ["checkout", "-q", "main"])
250
-
251
- // Delete all branches except main
252
- const branchOutput = await getGitOutput(cwd, ["branch", "--list"])
253
- const branches = branchOutput
254
- .split("\n")
255
- .map((b) => b.replace(/^\*?\s*/, "").trim())
256
- .filter((b) => b && b !== "main")
257
-
258
- for (const branch of branches) {
259
- await gitRun(cwd, ["branch", "-D", branch])
260
- }
261
-
262
- // Reset to first commit (the initial commit from template)
263
- const firstCommit = (
264
- await getGitOutput(cwd, ["rev-list", "--max-parents=0", "HEAD"])
265
- ).trim()
266
- await gitRun(cwd, ["reset", "--hard", firstCommit])
267
-
268
- // Clean untracked files and directories
269
- await gitRun(cwd, ["clean", "-fdx"])
270
-
271
- // Remove agency config
272
- try {
273
- await gitRun(cwd, ["config", "--unset", "agency.template"])
274
- } catch {
275
- // Ignore if not set
276
- }
277
- }
278
-
279
- /**
280
- * Rename current branch
281
- */
282
- export async function renameBranch(
283
- cwd: string,
284
- newName: string,
285
- ): Promise<void> {
286
- await gitRun(cwd, ["branch", "-m", newName])
287
- }
288
-
289
- /**
290
- * Check if a branch exists in a git repository
291
- */
292
- export async function branchExists(
293
- cwd: string,
294
- branch: string,
295
- ): Promise<boolean> {
296
- const proc = Bun.spawn(["git", "rev-parse", "--verify", branch], {
297
- cwd,
298
- stdout: "pipe",
299
- stderr: "pipe",
300
- })
301
- await proc.exited
302
- return proc.exitCode === 0
303
- }
304
-
305
- /**
306
- * Initialize a repository with agency by setting a template in git config
307
- */
308
- export async function initAgency(
309
- cwd: string,
310
- templateName: string,
311
- ): Promise<void> {
312
- await Bun.spawn(
313
- ["git", "config", "--local", "agency.template", templateName],
314
- {
315
- cwd,
316
- stdout: "pipe",
317
- stderr: "pipe",
318
- },
319
- ).exited
320
- }
321
-
322
- /**
323
- * Get a git config value for testing
324
- */
325
- export async function getGitConfig(
326
- key: string,
327
- gitRoot: string,
328
- ): Promise<string | null> {
329
- try {
330
- const proc = Bun.spawn(["git", "config", "--local", "--get", key], {
331
- cwd: gitRoot,
332
- stdout: "pipe",
333
- stderr: "pipe",
334
- })
335
- await proc.exited // Must await before reading stdout to prevent hangs in concurrent tests
336
- const output = await new Response(proc.stdout).text()
337
- return output.trim() || null
338
- } catch {
339
- return null
340
- }
341
- }
342
-
343
- /**
344
- * Run a git command
345
- */
346
- export async function runGitCommand(
347
- cwd: string,
348
- args: string[],
349
- ): Promise<void> {
350
- const proc = Bun.spawn(args, {
351
- cwd,
352
- stdout: "pipe",
353
- stderr: "pipe",
354
- })
355
- await proc.exited
356
- if (proc.exitCode !== 0) {
357
- const stderr = await new Response(proc.stderr).text()
358
- throw new Error(`Git command failed: ${args.join(" ")}\n${stderr}`)
359
- }
360
- }
361
-
362
- /**
363
- * Create a file with content
364
- */
365
- export async function createFile(
366
- cwd: string,
367
- filename: string,
368
- content: string,
369
- ): Promise<void> {
370
- await Bun.write(join(cwd, filename), content)
371
- }
372
-
373
- /**
374
- * Run an Effect in tests with all services provided
375
- */
1
+ import { spyOn } from "bun:test"
376
2
  import { Effect, Layer } from "effect"
377
- import { GitService } from "./services/GitService"
378
- import { ConfigService } from "./services/ConfigService"
3
+ import { mkdtemp, rm } from "node:fs/promises"
4
+ import { tmpdir } from "node:os"
5
+ import { join } from "node:path"
379
6
  import { FileSystemService } from "./services/FileSystemService"
380
- import { PromptService } from "./services/PromptService"
381
- import { TemplateService } from "./services/TemplateService"
382
- import { OpencodeService } from "./services/OpencodeService"
383
- import { ClaudeService } from "./services/ClaudeService"
384
- import { FilterRepoService } from "./services/FilterRepoService"
385
- import { MockFilterRepoService } from "./services/MockFilterRepoService"
386
- import { FormatterService } from "./services/FormatterService"
7
+ import { WorkbaseService } from "./services/WorkbaseService"
8
+ import { RepositoryService } from "./services/RepositoryService"
9
+ import { EpicService } from "./services/EpicService"
10
+ import { TaskService } from "./services/TaskService"
11
+ import { PhaseService } from "./services/PhaseService"
12
+ import { WorktreeService } from "./services/WorktreeService"
13
+ import { PullRequestService } from "./services/PullRequestService"
387
14
 
388
- // Re-export mock utilities for tests
389
- export {
390
- clearCapturedFilterRepoCalls,
391
- getCapturedFilterRepoCalls,
392
- getLastCapturedFilterRepoCall,
393
- } from "./services/MockFilterRepoService"
394
- export type { CapturedFilterRepoCall } from "./services/MockFilterRepoService"
15
+ export const createTempDir = () => mkdtemp(join(tmpdir(), "agency-test-"))
395
16
 
396
- // Create test layer with all services (real filter-repo)
397
- const TestLayer = Layer.mergeAll(
398
- GitService.Default,
399
- ConfigService.Default,
400
- FileSystemService.Default,
401
- PromptService.Default,
402
- TemplateService.Default,
403
- OpencodeService.Default,
404
- ClaudeService.Default,
405
- FilterRepoService.Default,
406
- FormatterService.Default,
407
- )
17
+ export const cleanupTempDir = (path: string) =>
18
+ rm(path, { recursive: true, force: true })
408
19
 
409
- // Create test layer with mock filter-repo (for tests that don't need real filtering)
410
- const TestLayerWithMockFilterRepo = Layer.mergeAll(
411
- GitService.Default,
412
- ConfigService.Default,
20
+ const TestLayer = Layer.mergeAll(
413
21
  FileSystemService.Default,
414
- PromptService.Default,
415
- TemplateService.Default,
416
- OpencodeService.Default,
417
- ClaudeService.Default,
418
- MockFilterRepoService.Default,
419
- FormatterService.Default,
22
+ WorkbaseService.Default,
23
+ RepositoryService.Default,
24
+ EpicService.Default,
25
+ TaskService.Default,
26
+ PhaseService.Default,
27
+ WorktreeService.Default,
28
+ PullRequestService.Default,
420
29
  )
421
30
 
422
31
  export async function runTestEffect<A, E>(
423
32
  effect: Effect.Effect<A, E, any>,
424
33
  ): Promise<A> {
425
- const providedEffect = Effect.provide(effect, TestLayer) as Effect.Effect<
426
- A,
427
- E,
428
- never
429
- >
430
- const program = Effect.catchAllDefect(providedEffect, (defect) =>
431
- Effect.fail(defect instanceof Error ? defect : new Error(String(defect))),
34
+ const program = effect.pipe(
35
+ Effect.provide(TestLayer),
36
+ Effect.catchAllDefect((defect) =>
37
+ Effect.fail(defect instanceof Error ? defect : new Error(String(defect))),
38
+ ),
432
39
  ) as Effect.Effect<A, E | Error, never>
433
40
 
434
- return await Effect.runPromise(program)
41
+ return Effect.runPromise(program)
435
42
  }
436
43
 
437
- /**
438
- * Run a test effect with MockFilterRepoService instead of the real one.
439
- * Use this for tests that need to verify filter-repo command construction
440
- * without actually running git-filter-repo.
441
- */
442
- export async function runTestEffectWithMockFilterRepo<A, E>(
443
- effect: Effect.Effect<A, E, any>,
444
- ): Promise<A> {
445
- const providedEffect = Effect.provide(
446
- effect,
447
- TestLayerWithMockFilterRepo,
448
- ) as Effect.Effect<A, E, never>
449
- const program = Effect.catchAllDefect(providedEffect, (defect) =>
450
- Effect.fail(defect instanceof Error ? defect : new Error(String(defect))),
451
- ) as Effect.Effect<A, E | Error, never>
452
-
453
- return await Effect.runPromise(program)
44
+ export async function captureLogs(
45
+ run: () => Promise<unknown>,
46
+ ): Promise<string[]> {
47
+ const logs: string[] = []
48
+ const log = spyOn(console, "log").mockImplementation((...args) => {
49
+ logs.push(args.join(" "))
50
+ })
51
+ try {
52
+ await run()
53
+ return logs
54
+ } finally {
55
+ log.mockRestore()
56
+ }
454
57
  }
package/src/types.ts CHANGED
@@ -1,217 +1,3 @@
1
- import { join } from "node:path"
2
- import { Effect } from "effect"
3
- import { Schema } from "@effect/schema"
4
- import { ManagedFile, AgencyMetadata } from "./schemas"
5
- import { AgencyMetadataService } from "./services/AgencyMetadataService"
6
- import { FileSystemService } from "./services/FileSystemService"
7
- import { GitService } from "./services/GitService"
8
-
9
1
  export interface Command {
10
- name: string
11
- description: string
12
- run: (
13
- args: string[],
14
- options: Record<string, any>,
15
- rawArgs?: string[],
16
- ) => Promise<void>
17
- help?: string
18
- }
19
-
20
- /**
21
- * Load template content from the templates directory.
22
- * Falls back to inline defaults if files cannot be read (e.g., in bundled packages).
23
- */
24
- async function loadTemplateContent(fileName: string): Promise<string> {
25
- try {
26
- // Try to load from the templates directory relative to this file's location
27
- const templatePath = join(import.meta.dir, "..", "templates", fileName)
28
- const file = Bun.file(templatePath)
29
- if (await file.exists()) {
30
- return await file.text()
31
- }
32
- } catch (error) {
33
- // Fall through to defaults if file loading fails
34
- }
35
-
36
- // Return inline defaults as fallback
37
- const defaults: Record<string, string> = {
38
- "AGENCY.md": `# Agent Instructions
39
-
40
- ## TASK.md
41
-
42
- The \`TASK.md\` file describes the task being performed and should be kept updated as work progresses. This file serves as a living record of:
43
-
44
- - What is being built or fixed
45
- - Current progress and status
46
- - Remaining work items
47
- - Any important context or decisions
48
-
49
- All work on this repository should begin by reading and understanding \`TASK.md\`. Whenever any significant progress is made, \`TASK.md\` should be updated to reflect the current state of work.
50
-
51
- See \`TASK.md\` for the current task description and progress.
52
- `,
53
- "AGENTS.md": `# Agency
54
-
55
- Agency is a CLI tool for managing \`AGENTS.md\`, \`TASK.md\`, and \`opencode.json\` files in git repositories. It helps coordinate work across multiple branches and templates.
56
-
57
- ## Key Commands
58
-
59
- - \`agency task <branch-name>\` - Create feature branch and initialize template files
60
- - \`agency edit\` - Open TASK.md in system editor
61
- - \`agency template save\` - Save current file versions back to a template
62
- - \`agency template use\` - Switch to a different template
63
- - \`agency emit\` - Create an emit branch with managed files reverted to their merge-base state
64
- - \`agency switch\` - Toggle between feature and emit branches
65
- - \`agency template source\` - Get the path to a template's source directory
66
- - \`agency set-base\` - Update the saved base branch for emit creation
67
-
68
- ## Features
69
-
70
- - **Template-based workflow** - Reusable templates stored in \`~/.config/agency/templates/\`
71
- - **Git integration** - Saves template configuration in \`.git/config\`
72
- - **Emit branch management** - Automatically creates clean emit branches without local modifications
73
- - **Multi-file support** - Manages AGENTS.md, TASK.md, and opencode.json
74
- `,
75
- "TASK.md": `{task}
76
-
77
- ## Tasks
78
-
79
- - [ ] Populate this list
80
- `,
81
- "opencode.json": JSON.stringify(
82
- {
83
- $schema: "https://opencode.ai/config.json",
84
- instructions: ["AGENCY.md", "TASK.md"],
85
- },
86
- null,
87
- 2,
88
- ),
89
- }
90
-
91
- return defaults[fileName] || ""
92
- }
93
-
94
- /**
95
- * Initialize managed files with their default content.
96
- * This is a synchronous function that returns a promise for the initialized files.
97
- */
98
- export async function initializeManagedFiles(): Promise<ManagedFile[]> {
99
- const files: ManagedFile[] = []
100
-
101
- for (const fileName of [
102
- "AGENCY.md",
103
- "AGENTS.md",
104
- "opencode.json",
105
- "TASK.md",
106
- ]) {
107
- const content = await loadTemplateContent(fileName)
108
- files.push({
109
- name: fileName,
110
- defaultContent: content,
111
- })
112
- }
113
-
114
- return files
115
- }
116
-
117
- // This will be initialized by commands that need it
118
- // For backward compatibility, export a variable that can be set
119
- let MANAGED_FILES: ManagedFile[] = []
120
-
121
- // Validation is now handled by Effect schemas in schemas.ts
122
-
123
- /**
124
- * Read agency.json metadata from a repository.
125
- * @deprecated Use AgencyMetadataService.readFromDisk instead
126
- */
127
- export async function readAgencyMetadata(
128
- gitRoot: string,
129
- ): Promise<AgencyMetadata | null> {
130
- const program = Effect.gen(function* () {
131
- const metadataService = yield* AgencyMetadataService
132
- return yield* metadataService.readFromDisk(gitRoot)
133
- }).pipe(
134
- Effect.provide(AgencyMetadataService.Default),
135
- Effect.provide(FileSystemService.Default),
136
- Effect.provide(GitService.Default),
137
- )
138
-
139
- return Effect.runPromise(program)
140
- }
141
-
142
- /**
143
- * Write agency.json metadata to a repository.
144
- * @deprecated Use AgencyMetadataService.write instead
145
- */
146
- export async function writeAgencyMetadata(
147
- gitRoot: string,
148
- metadata: AgencyMetadata,
149
- ): Promise<void> {
150
- const program = Effect.gen(function* () {
151
- const metadataService = yield* AgencyMetadataService
152
- return yield* metadataService.write(gitRoot, metadata)
153
- }).pipe(
154
- Effect.provide(AgencyMetadataService.Default),
155
- Effect.provide(FileSystemService.Default),
156
- Effect.provide(GitService.Default),
157
- )
158
-
159
- return Effect.runPromise(program)
160
- }
161
-
162
- /**
163
- * Get list of files to filter during PR/merge operations.
164
- * Always includes TASK.md, AGENCY.md, CLAUDE.md, and agency.json, plus any backpack files from metadata.
165
- * @deprecated Use AgencyMetadataService.getFilesToFilter instead
166
- */
167
- export async function getFilesToFilter(gitRoot: string): Promise<string[]> {
168
- const program = Effect.gen(function* () {
169
- const metadataService = yield* AgencyMetadataService
170
- return yield* metadataService.getFilesToFilter(gitRoot)
171
- }).pipe(
172
- Effect.provide(AgencyMetadataService.Default),
173
- Effect.provide(FileSystemService.Default),
174
- Effect.provide(GitService.Default),
175
- )
176
-
177
- return Effect.runPromise(program)
178
- }
179
-
180
- /**
181
- * Get the configured base branch from agency.json metadata.
182
- * @deprecated Use AgencyMetadataService.getBaseBranch instead
183
- */
184
- export async function getBaseBranchFromMetadata(
185
- gitRoot: string,
186
- ): Promise<string | null> {
187
- const program = Effect.gen(function* () {
188
- const metadataService = yield* AgencyMetadataService
189
- return yield* metadataService.getBaseBranch(gitRoot)
190
- }).pipe(
191
- Effect.provide(AgencyMetadataService.Default),
192
- Effect.provide(FileSystemService.Default),
193
- Effect.provide(GitService.Default),
194
- )
195
-
196
- return Effect.runPromise(program)
197
- }
198
-
199
- /**
200
- * Set the base branch in agency.json metadata.
201
- * @deprecated Use AgencyMetadataService.setBaseBranch instead
202
- */
203
- export async function setBaseBranchInMetadata(
204
- gitRoot: string,
205
- baseBranch: string,
206
- ): Promise<void> {
207
- const program = Effect.gen(function* () {
208
- const metadataService = yield* AgencyMetadataService
209
- return yield* metadataService.setBaseBranch(gitRoot, baseBranch)
210
- }).pipe(
211
- Effect.provide(AgencyMetadataService.Default),
212
- Effect.provide(FileSystemService.Default),
213
- Effect.provide(GitService.Default),
214
- )
215
-
216
- return Effect.runPromise(program)
2
+ run: (args: string[], options: Record<string, any>) => Promise<void>
217
3
  }