@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,311 +0,0 @@
1
- import { join, dirname, resolve, relative } from "node:path"
2
- import { Data, Effect } from "effect"
3
- import { Schema } from "@effect/schema"
4
- import { AgencyMetadata } from "../schemas"
5
- import { FileSystemService } from "./FileSystemService"
6
- import { GitService } from "./GitService"
7
- import { expandGlobs } from "../utils/glob"
8
-
9
- /**
10
- * Error type for AgencyMetadata operations
11
- */
12
- class AgencyMetadataError extends Data.TaggedError("AgencyMetadataError")<{
13
- message: string
14
- cause?: unknown
15
- }> {}
16
-
17
- /**
18
- * Parse and validate agency.json content from a JSON string.
19
- * Returns null if the content is invalid.
20
- */
21
- const parseAgencyMetadata = (content: string) =>
22
- Effect.gen(function* () {
23
- const data = yield* Effect.try({
24
- try: () => JSON.parse(content),
25
- catch: () => new Error("Failed to parse agency.json"),
26
- })
27
-
28
- // Validate version
29
- if (typeof data.version !== "number" || data.version !== 1) {
30
- return null
31
- }
32
-
33
- // Parse and validate using Effect schema
34
- const metadata = yield* Effect.try({
35
- try: () => Schema.decodeUnknownSync(AgencyMetadata)(data),
36
- catch: () => new Error("Invalid agency.json format"),
37
- })
38
-
39
- return metadata
40
- }).pipe(Effect.catchAll(() => Effect.succeed(null)))
41
-
42
- /**
43
- * Resolve symlink targets for a list of files.
44
- * For each file, if it's a symlink, also adds the target path to the result.
45
- * This ensures that when filtering files from git history, both the symlink
46
- * and its target are filtered (important when CLAUDE.md → AGENTS.md, for example).
47
- */
48
- const resolveSymlinkTargets = (
49
- fs: FileSystemService,
50
- gitRoot: string,
51
- files: string[],
52
- ) =>
53
- Effect.gen(function* () {
54
- const result = new Set<string>(files)
55
-
56
- for (const file of files) {
57
- const fullPath = join(gitRoot, file)
58
- const target = yield* fs.readSymlinkTarget(fullPath)
59
-
60
- if (target) {
61
- // The target path could be relative or absolute
62
- // If relative, it's relative to the symlink's location
63
- // We need to normalize it to be relative to gitRoot
64
- let targetPath: string
65
- if (target.startsWith("/")) {
66
- // Absolute path - make it relative to gitRoot
67
- targetPath = relative(gitRoot, target)
68
- } else {
69
- // Relative path - resolve from the symlink's directory
70
- const symlinkDir = dirname(fullPath)
71
- const absoluteTarget = resolve(symlinkDir, target)
72
- targetPath = relative(gitRoot, absoluteTarget)
73
- }
74
-
75
- // Only add if it's within the gitRoot (not escaping the repo)
76
- if (!targetPath.startsWith("..")) {
77
- result.add(targetPath)
78
- }
79
- }
80
- }
81
-
82
- return Array.from(result)
83
- })
84
-
85
- /**
86
- * Service for managing agency.json metadata operations
87
- */
88
- export class AgencyMetadataService extends Effect.Service<AgencyMetadataService>()(
89
- "AgencyMetadataService",
90
- {
91
- sync: () => ({
92
- /**
93
- * Read agency.json from disk in the repository root.
94
- * Returns null if the file doesn't exist or is invalid.
95
- */
96
- readFromDisk: (gitRoot: string) =>
97
- Effect.gen(function* () {
98
- const fs = yield* FileSystemService
99
- const metadataPath = join(gitRoot, "agency.json")
100
-
101
- const exists = yield* fs.exists(metadataPath)
102
- if (!exists) {
103
- return null
104
- }
105
-
106
- const content = yield* fs.readFile(metadataPath)
107
- return yield* parseAgencyMetadata(content)
108
- }).pipe(Effect.catchAll(() => Effect.succeed(null))),
109
-
110
- /**
111
- * Read agency.json from a specific git branch using git show.
112
- * Returns null if the file doesn't exist or is invalid.
113
- */
114
- readFromBranch: (gitRoot: string, branch: string) =>
115
- Effect.gen(function* () {
116
- const git = yield* GitService
117
-
118
- // Try to read agency.json from the branch using git show
119
- const content = yield* git.getFileAtRef(
120
- gitRoot,
121
- branch,
122
- "agency.json",
123
- )
124
-
125
- if (!content) {
126
- return null
127
- }
128
-
129
- return yield* parseAgencyMetadata(content)
130
- }).pipe(Effect.catchAll(() => Effect.succeed(null))),
131
-
132
- /**
133
- * Write agency.json to disk in the repository root.
134
- */
135
- write: (gitRoot: string, metadata: AgencyMetadata) =>
136
- Effect.gen(function* () {
137
- const fs = yield* FileSystemService
138
- const metadataPath = join(gitRoot, "agency.json")
139
-
140
- const content = JSON.stringify(metadata, null, 2) + "\n"
141
- yield* fs.writeFile(metadataPath, content).pipe(
142
- Effect.mapError(
143
- (error) =>
144
- new AgencyMetadataError({
145
- message: `Failed to write agency.json: ${error}`,
146
- cause: error,
147
- }),
148
- ),
149
- )
150
- }),
151
-
152
- /**
153
- * Get list of files to filter during PR/merge operations.
154
- * Always includes TASK.md, AGENCY.md, and agency.json, plus any injectedFiles from metadata.
155
- */
156
- getFilesToFilter: (gitRoot: string) =>
157
- Effect.gen(function* () {
158
- const fs = yield* FileSystemService
159
- const metadataPath = join(gitRoot, "agency.json")
160
-
161
- const exists = yield* fs.exists(metadataPath)
162
- const baseFiles = ["TASK.md", "AGENCY.md", "agency.json"]
163
-
164
- if (!exists) {
165
- // Resolve symlinks even for base files
166
- return yield* resolveSymlinkTargets(fs, gitRoot, baseFiles)
167
- }
168
-
169
- const content = yield* fs
170
- .readFile(metadataPath)
171
- .pipe(Effect.catchAll(() => Effect.succeed("")))
172
-
173
- if (!content) {
174
- return yield* resolveSymlinkTargets(fs, gitRoot, baseFiles)
175
- }
176
-
177
- const metadata = yield* parseAgencyMetadata(content)
178
-
179
- if (!metadata) {
180
- return yield* resolveSymlinkTargets(fs, gitRoot, baseFiles)
181
- }
182
-
183
- // Expand any glob patterns in injectedFiles to actual file paths
184
- const expandedFiles = yield* Effect.tryPromise({
185
- try: () =>
186
- expandGlobs([...baseFiles, ...metadata.injectedFiles], gitRoot),
187
- catch: () => new Error("Failed to expand glob patterns"),
188
- })
189
-
190
- // Resolve symlinks and add their targets
191
- return yield* resolveSymlinkTargets(fs, gitRoot, expandedFiles)
192
- }).pipe(
193
- Effect.catchAll(() =>
194
- Effect.succeed(["TASK.md", "AGENCY.md", "agency.json"]),
195
- ),
196
- ),
197
-
198
- /**
199
- * Get the configured base branch from agency.json metadata.
200
- * Returns null if no metadata exists or no base branch is configured.
201
- */
202
- getBaseBranch: (gitRoot: string) =>
203
- Effect.gen(function* () {
204
- const fs = yield* FileSystemService
205
- const metadataPath = join(gitRoot, "agency.json")
206
-
207
- const exists = yield* fs.exists(metadataPath)
208
- if (!exists) {
209
- return null
210
- }
211
-
212
- const content = yield* fs
213
- .readFile(metadataPath)
214
- .pipe(Effect.catchAll(() => Effect.succeed("")))
215
-
216
- if (!content) {
217
- return null
218
- }
219
-
220
- const metadata = yield* parseAgencyMetadata(content)
221
- return metadata?.baseBranch || null
222
- }).pipe(Effect.catchAll(() => Effect.succeed(null))),
223
-
224
- /**
225
- * Set the base branch in agency.json metadata.
226
- */
227
- setBaseBranch: (gitRoot: string, baseBranch: string) =>
228
- Effect.gen(function* () {
229
- const fs = yield* FileSystemService
230
- const metadataPath = join(gitRoot, "agency.json")
231
-
232
- const exists = yield* fs.exists(metadataPath).pipe(
233
- Effect.mapError(
234
- (error) =>
235
- new AgencyMetadataError({
236
- message: `Failed to check agency.json: ${error}`,
237
- cause: error,
238
- }),
239
- ),
240
- )
241
-
242
- if (!exists) {
243
- return yield* Effect.fail(
244
- new AgencyMetadataError({
245
- message:
246
- "agency.json not found. Please run 'agency task' first to initialize backpack files.",
247
- }),
248
- )
249
- }
250
-
251
- const content = yield* fs.readFile(metadataPath).pipe(
252
- Effect.mapError(
253
- (error) =>
254
- new AgencyMetadataError({
255
- message: `Failed to read agency.json: ${error}`,
256
- cause: error,
257
- }),
258
- ),
259
- )
260
-
261
- const metadata = yield* parseAgencyMetadata(content).pipe(
262
- Effect.flatMap((m) =>
263
- m
264
- ? Effect.succeed(m)
265
- : Effect.fail(
266
- new AgencyMetadataError({
267
- message:
268
- "agency.json is invalid. Please run 'agency task' first to initialize backpack files.",
269
- }),
270
- ),
271
- ),
272
- )
273
-
274
- // Create a new metadata instance with the updated baseBranch
275
- const updatedMetadata = new AgencyMetadata({
276
- version: metadata.version,
277
- injectedFiles: metadata.injectedFiles,
278
- template: metadata.template,
279
- createdAt: metadata.createdAt,
280
- baseBranch,
281
- emitBranch: metadata.emitBranch,
282
- })
283
-
284
- const outputContent = JSON.stringify(updatedMetadata, null, 2) + "\n"
285
- yield* fs.writeFile(metadataPath, outputContent).pipe(
286
- Effect.mapError(
287
- (error) =>
288
- new AgencyMetadataError({
289
- message: `Failed to write agency.json: ${error}`,
290
- cause: error,
291
- }),
292
- ),
293
- )
294
- return void 0
295
- }),
296
-
297
- /**
298
- * Parse and validate agency.json content from a JSON string.
299
- * Returns null if the content is invalid.
300
- */
301
- parse: parseAgencyMetadata,
302
- }),
303
- },
304
- ) {}
305
-
306
- /**
307
- * Parse and validate agency.json content from a JSON string.
308
- * Returns null if the content is invalid.
309
- * This is exported as a standalone function for convenience since it has no dependencies.
310
- */
311
- export { parseAgencyMetadata }
@@ -1,184 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from "bun:test"
2
- import { Effect } from "effect"
3
- import { join } from "path"
4
- import {
5
- createTempDir,
6
- cleanupTempDir,
7
- initGitRepo,
8
- runTestEffect,
9
- fileExists,
10
- readFile,
11
- createFile,
12
- } from "../test-utils"
13
- import { ClaudeService } from "./ClaudeService"
14
-
15
- describe("ClaudeService", () => {
16
- let tempDir: string
17
-
18
- beforeEach(async () => {
19
- tempDir = await createTempDir()
20
- await initGitRepo(tempDir)
21
- })
22
-
23
- afterEach(async () => {
24
- await cleanupTempDir(tempDir)
25
- })
26
-
27
- describe("claudeFileExists", () => {
28
- test("returns false when CLAUDE.md does not exist", async () => {
29
- const result = await runTestEffect(
30
- Effect.gen(function* () {
31
- const claudeService = yield* ClaudeService
32
- return yield* claudeService.claudeFileExists(tempDir)
33
- }),
34
- )
35
-
36
- expect(result).toBe(false)
37
- })
38
-
39
- test("returns true when CLAUDE.md exists", async () => {
40
- await createFile(tempDir, "CLAUDE.md", "# Claude Code\n")
41
-
42
- const result = await runTestEffect(
43
- Effect.gen(function* () {
44
- const claudeService = yield* ClaudeService
45
- return yield* claudeService.claudeFileExists(tempDir)
46
- }),
47
- )
48
-
49
- expect(result).toBe(true)
50
- })
51
- })
52
-
53
- describe("hasAgencySection", () => {
54
- test("returns false when content does not have agency section", async () => {
55
- const content = "# Claude Code\n\nSome instructions\n"
56
-
57
- const result = await runTestEffect(
58
- Effect.gen(function* () {
59
- const claudeService = yield* ClaudeService
60
- return yield* claudeService.hasAgencySection(content)
61
- }),
62
- )
63
-
64
- expect(result).toBe(false)
65
- })
66
-
67
- test("returns true when content has both @AGENCY.md and @TASK.md", async () => {
68
- const content = `# Claude Code
69
-
70
- Some instructions
71
-
72
- ## Agency
73
-
74
- @AGENCY.md
75
- @TASK.md
76
- `
77
-
78
- const result = await runTestEffect(
79
- Effect.gen(function* () {
80
- const claudeService = yield* ClaudeService
81
- return yield* claudeService.hasAgencySection(content)
82
- }),
83
- )
84
-
85
- expect(result).toBe(true)
86
- })
87
- })
88
-
89
- describe("injectAgencySection", () => {
90
- test("creates CLAUDE.md with agency section when file does not exist", async () => {
91
- const result = await runTestEffect(
92
- Effect.gen(function* () {
93
- const claudeService = yield* ClaudeService
94
- return yield* claudeService.injectAgencySection(tempDir)
95
- }),
96
- )
97
-
98
- expect(result.created).toBe(true)
99
- expect(result.modified).toBe(true)
100
-
101
- const claudePath = join(tempDir, "CLAUDE.md")
102
- expect(await fileExists(claudePath)).toBe(true)
103
-
104
- const content = await readFile(claudePath)
105
- expect(content).toContain("@AGENCY.md")
106
- expect(content).toContain("@TASK.md")
107
- expect(content.indexOf("@AGENCY.md")).toBeLessThan(
108
- content.indexOf("@TASK.md"),
109
- )
110
- })
111
-
112
- test("appends agency section when file exists without references", async () => {
113
- const initialContent = "# Claude Code\n\nSome existing instructions\n"
114
- await createFile(tempDir, "CLAUDE.md", initialContent)
115
-
116
- const result = await runTestEffect(
117
- Effect.gen(function* () {
118
- const claudeService = yield* ClaudeService
119
- return yield* claudeService.injectAgencySection(tempDir)
120
- }),
121
- )
122
-
123
- expect(result.created).toBe(false)
124
- expect(result.modified).toBe(true)
125
-
126
- const content = await readFile(join(tempDir, "CLAUDE.md"))
127
- expect(content).toContain("Some existing instructions")
128
- expect(content).toContain("@AGENCY.md")
129
- expect(content).toContain("@TASK.md")
130
- })
131
-
132
- test("does not modify file when agency section already exists in correct order", async () => {
133
- const contentWithSection = `# Claude Code
134
-
135
- Some instructions
136
-
137
- ## Agency
138
-
139
- @AGENCY.md
140
- @TASK.md
141
- `
142
- await createFile(tempDir, "CLAUDE.md", contentWithSection)
143
-
144
- const result = await runTestEffect(
145
- Effect.gen(function* () {
146
- const claudeService = yield* ClaudeService
147
- return yield* claudeService.injectAgencySection(tempDir)
148
- }),
149
- )
150
-
151
- expect(result.created).toBe(false)
152
- expect(result.modified).toBe(false)
153
-
154
- const content = await readFile(join(tempDir, "CLAUDE.md"))
155
- expect(content).toBe(contentWithSection)
156
- })
157
-
158
- test("re-adds references when they exist in wrong order", async () => {
159
- const contentWithWrongOrder = `# Claude Code
160
-
161
- Some instructions
162
-
163
- @TASK.md
164
- @AGENCY.md
165
- `
166
- await createFile(tempDir, "CLAUDE.md", contentWithWrongOrder)
167
-
168
- const result = await runTestEffect(
169
- Effect.gen(function* () {
170
- const claudeService = yield* ClaudeService
171
- return yield* claudeService.injectAgencySection(tempDir)
172
- }),
173
- )
174
-
175
- expect(result.created).toBe(false)
176
- expect(result.modified).toBe(true)
177
-
178
- const content = await readFile(join(tempDir, "CLAUDE.md"))
179
- // Should have the section appended with correct order
180
- expect(content).toContain("@AGENCY.md")
181
- expect(content).toContain("@TASK.md")
182
- })
183
- })
184
- })
@@ -1,91 +0,0 @@
1
- import { Effect } from "effect"
2
- import { FileSystemService } from "./FileSystemService"
3
-
4
- const AGENCY_SECTION = `
5
- ## Agency
6
-
7
- @AGENCY.md
8
- @TASK.md`
9
-
10
- /**
11
- * Service for handling CLAUDE.md files with @-reference injection.
12
- */
13
- export class ClaudeService extends Effect.Service<ClaudeService>()(
14
- "ClaudeService",
15
- {
16
- sync: () => ({
17
- /**
18
- * Check if CLAUDE.md exists in the git root.
19
- */
20
- claudeFileExists: (gitRoot: string) =>
21
- Effect.gen(function* () {
22
- const fs = yield* FileSystemService
23
- const claudePath = `${gitRoot}/CLAUDE.md`
24
- return yield* fs.exists(claudePath)
25
- }),
26
-
27
- /**
28
- * Check if the CLAUDE.md file already contains the agency section.
29
- */
30
- hasAgencySection: (content: string) =>
31
- Effect.sync(() => {
32
- // Check if both @AGENCY.md and @TASK.md exist in the content
33
- return content.includes("@AGENCY.md") && content.includes("@TASK.md")
34
- }),
35
-
36
- /**
37
- * Inject or ensure the agency section exists in CLAUDE.md.
38
- * If the file doesn't exist, create it with the basic template.
39
- * If it exists but doesn't have the references, append them.
40
- * Returns true if the file was modified, false if no changes needed.
41
- */
42
- injectAgencySection: (gitRoot: string) =>
43
- Effect.gen(function* () {
44
- const fs = yield* FileSystemService
45
- const claudePath = `${gitRoot}/CLAUDE.md`
46
-
47
- // Check if file exists
48
- const exists = yield* fs.exists(claudePath)
49
-
50
- if (!exists) {
51
- // Create new CLAUDE.md with basic template
52
- const content = `# Claude Code Instructions
53
-
54
- This project uses the agency CLI for managing development tasks and templates.
55
- ${AGENCY_SECTION}
56
- `
57
- yield* fs.writeFile(claudePath, content)
58
- return { modified: true, created: true }
59
- }
60
-
61
- // Read existing content
62
- const content = yield* fs.readFile(claudePath)
63
-
64
- // Check if agency section already exists
65
- const hasSection = yield* Effect.sync(() => {
66
- return (
67
- content.includes("@AGENCY.md") && content.includes("@TASK.md")
68
- )
69
- })
70
-
71
- if (hasSection) {
72
- // Check if they're in the correct order
73
- const agencyIndex = content.indexOf("@AGENCY.md")
74
- const taskIndex = content.indexOf("@TASK.md")
75
-
76
- if (agencyIndex < taskIndex) {
77
- // Already has the section in correct order
78
- return { modified: false, created: false }
79
- }
80
-
81
- // They exist but in wrong order - we'll re-add them
82
- }
83
-
84
- // Append the agency section
85
- const newContent = content.trimEnd() + "\n" + AGENCY_SECTION + "\n"
86
- yield* fs.writeFile(claudePath, newContent)
87
- return { modified: true, created: false }
88
- }),
89
- }),
90
- },
91
- ) {}
@@ -1,115 +0,0 @@
1
- import { Effect, Data } from "effect"
2
- import { Schema } from "@effect/schema"
3
- import { mkdir } from "node:fs/promises"
4
- import { AgencyConfig } from "../schemas"
5
- import { getAgencyConfigDir, getAgencyConfigPath } from "../utils/paths"
6
-
7
- // Error types for Config operations
8
- class ConfigError extends Data.TaggedError("ConfigError")<{
9
- message: string
10
- cause?: unknown
11
- }> {}
12
-
13
- class ConfigWriteError extends Data.TaggedError("ConfigWriteError")<{
14
- path: string
15
- cause?: unknown
16
- }> {}
17
-
18
- const DEFAULT_CONFIG: AgencyConfig = new AgencyConfig({
19
- sourceBranchPattern: "agency--%branch%",
20
- emitBranch: "%branch%",
21
- })
22
-
23
- // Config Service using Effect.Service pattern
24
- export class ConfigService extends Effect.Service<ConfigService>()(
25
- "ConfigService",
26
- {
27
- sync: () => ({
28
- getConfigDir: () => Effect.sync(() => getAgencyConfigDir()),
29
-
30
- getConfigPath: () => Effect.sync(() => getAgencyConfigPath()),
31
-
32
- loadConfig: (configPath?: string) =>
33
- Effect.gen(function* () {
34
- const path =
35
- configPath || (yield* Effect.sync(() => getAgencyConfigPath()))
36
-
37
- // Check if file exists
38
- const file = Bun.file(path)
39
- const exists = yield* Effect.tryPromise({
40
- try: () => file.exists(),
41
- catch: () =>
42
- new ConfigError({
43
- message: `Failed to check if config exists at ${path}`,
44
- }),
45
- })
46
-
47
- if (!exists) {
48
- return DEFAULT_CONFIG
49
- }
50
-
51
- // Try to read and parse the config
52
- const data = yield* Effect.tryPromise({
53
- try: () => file.json(),
54
- catch: (error) =>
55
- new ConfigError({
56
- message: `Failed to read config file at ${path}`,
57
- cause: error,
58
- }),
59
- })
60
-
61
- // Parse with schema, but fall back to defaults on error
62
- const parsed = yield* Effect.try({
63
- try: () => Schema.decodeUnknownSync(AgencyConfig)(data),
64
- catch: (error) =>
65
- new ConfigError({
66
- message: `Invalid config format at ${path}`,
67
- cause: error,
68
- }),
69
- })
70
-
71
- // If we get here, parsing succeeded
72
- return parsed || DEFAULT_CONFIG
73
- }),
74
-
75
- saveConfig: (config: AgencyConfig, configPath?: string) =>
76
- Effect.gen(function* () {
77
- const path =
78
- configPath || (yield* Effect.sync(() => getAgencyConfigPath()))
79
-
80
- // Ensure the config directory exists
81
- const configDir = yield* Effect.sync(() => getAgencyConfigDir())
82
-
83
- yield* Effect.tryPromise({
84
- try: () => mkdir(configDir, { recursive: true }),
85
- catch: (error) =>
86
- new ConfigWriteError({
87
- path: configDir,
88
- cause: error,
89
- }),
90
- })
91
-
92
- // Encode and write the config
93
- const encoded = yield* Effect.try({
94
- try: () => Schema.encodeSync(AgencyConfig)(config),
95
- catch: (error) =>
96
- new ConfigWriteError({
97
- path,
98
- cause: error,
99
- }),
100
- })
101
-
102
- yield* Effect.tryPromise({
103
- try: () => Bun.write(path, JSON.stringify(encoded, null, 2) + "\n"),
104
- catch: (error) =>
105
- new ConfigWriteError({
106
- path,
107
- cause: error,
108
- }),
109
- })
110
- }),
111
-
112
- getDefaultConfig: () => Effect.succeed(DEFAULT_CONFIG),
113
- }),
114
- },
115
- ) {}