@markjaquith/agency 0.5.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.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/cli.ts +569 -0
- package/index.ts +1 -0
- package/package.json +65 -0
- package/src/commands/base.test.ts +198 -0
- package/src/commands/base.ts +198 -0
- package/src/commands/clean.test.ts +299 -0
- package/src/commands/clean.ts +320 -0
- package/src/commands/emit.test.ts +412 -0
- package/src/commands/emit.ts +521 -0
- package/src/commands/emitted.test.ts +226 -0
- package/src/commands/emitted.ts +57 -0
- package/src/commands/init.test.ts +311 -0
- package/src/commands/init.ts +140 -0
- package/src/commands/merge.test.ts +365 -0
- package/src/commands/merge.ts +253 -0
- package/src/commands/pull.test.ts +385 -0
- package/src/commands/pull.ts +205 -0
- package/src/commands/push.test.ts +394 -0
- package/src/commands/push.ts +346 -0
- package/src/commands/save.test.ts +247 -0
- package/src/commands/save.ts +162 -0
- package/src/commands/source.test.ts +195 -0
- package/src/commands/source.ts +72 -0
- package/src/commands/status.test.ts +489 -0
- package/src/commands/status.ts +258 -0
- package/src/commands/switch.test.ts +194 -0
- package/src/commands/switch.ts +84 -0
- package/src/commands/task-branching.test.ts +334 -0
- package/src/commands/task-edit.test.ts +141 -0
- package/src/commands/task-main.test.ts +872 -0
- package/src/commands/task.ts +712 -0
- package/src/commands/tasks.test.ts +335 -0
- package/src/commands/tasks.ts +155 -0
- package/src/commands/template-delete.test.ts +178 -0
- package/src/commands/template-delete.ts +98 -0
- package/src/commands/template-list.test.ts +135 -0
- package/src/commands/template-list.ts +87 -0
- package/src/commands/template-view.test.ts +158 -0
- package/src/commands/template-view.ts +86 -0
- package/src/commands/template.test.ts +32 -0
- package/src/commands/template.ts +96 -0
- package/src/commands/use.test.ts +87 -0
- package/src/commands/use.ts +97 -0
- package/src/commands/work.test.ts +462 -0
- package/src/commands/work.ts +193 -0
- package/src/errors.ts +17 -0
- package/src/schemas.ts +33 -0
- package/src/services/AgencyMetadataService.ts +287 -0
- package/src/services/ClaudeService.test.ts +184 -0
- package/src/services/ClaudeService.ts +91 -0
- package/src/services/ConfigService.ts +115 -0
- package/src/services/FileSystemService.ts +222 -0
- package/src/services/GitService.ts +751 -0
- package/src/services/OpencodeService.ts +263 -0
- package/src/services/PromptService.ts +183 -0
- package/src/services/TemplateService.ts +75 -0
- package/src/test-utils.ts +362 -0
- package/src/types/native-exec.d.ts +8 -0
- package/src/types.ts +216 -0
- package/src/utils/colors.ts +178 -0
- package/src/utils/command.ts +17 -0
- package/src/utils/effect.ts +281 -0
- package/src/utils/exec.ts +48 -0
- package/src/utils/paths.ts +51 -0
- package/src/utils/pr-branch.test.ts +372 -0
- package/src/utils/pr-branch.ts +473 -0
- package/src/utils/process.ts +110 -0
- package/src/utils/spinner.ts +82 -0
- package/templates/AGENCY.md +20 -0
- package/templates/AGENTS.md +11 -0
- package/templates/CLAUDE.md +3 -0
- package/templates/TASK.md +5 -0
- package/templates/opencode.json +4 -0
|
@@ -0,0 +1,362 @@
|
|
|
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, ["checkout", branchName])
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Create a new branch and switch to it
|
|
187
|
+
*/
|
|
188
|
+
export async function createBranch(
|
|
189
|
+
cwd: string,
|
|
190
|
+
branchName: string,
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
await gitRun(cwd, ["checkout", "-b", branchName])
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Stage files and commit in a single operation
|
|
197
|
+
*/
|
|
198
|
+
export async function addAndCommit(
|
|
199
|
+
cwd: string,
|
|
200
|
+
files: string | string[],
|
|
201
|
+
message: string,
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
const fileList = Array.isArray(files) ? files : files.split(" ")
|
|
204
|
+
await gitRun(cwd, ["add", ...fileList])
|
|
205
|
+
await gitRun(cwd, ["commit", "--no-verify", "-m", message])
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Setup a remote and fetch in a single operation
|
|
210
|
+
*/
|
|
211
|
+
export async function setupRemote(
|
|
212
|
+
cwd: string,
|
|
213
|
+
remoteName: string,
|
|
214
|
+
remoteUrl: string,
|
|
215
|
+
): Promise<void> {
|
|
216
|
+
await gitRun(cwd, ["remote", "add", remoteName, remoteUrl])
|
|
217
|
+
await gitRun(cwd, ["fetch", remoteName])
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Delete a branch
|
|
222
|
+
*/
|
|
223
|
+
export async function deleteBranch(
|
|
224
|
+
cwd: string,
|
|
225
|
+
branchName: string,
|
|
226
|
+
force: boolean = false,
|
|
227
|
+
): Promise<void> {
|
|
228
|
+
const flag = force ? "-D" : "-d"
|
|
229
|
+
await gitRun(cwd, ["branch", flag, branchName])
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Rename current branch
|
|
234
|
+
*/
|
|
235
|
+
export async function renameBranch(
|
|
236
|
+
cwd: string,
|
|
237
|
+
newName: string,
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
await gitRun(cwd, ["branch", "-m", newName])
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Check if a branch exists in a git repository
|
|
244
|
+
*/
|
|
245
|
+
export async function branchExists(
|
|
246
|
+
cwd: string,
|
|
247
|
+
branch: string,
|
|
248
|
+
): Promise<boolean> {
|
|
249
|
+
const proc = Bun.spawn(["git", "rev-parse", "--verify", branch], {
|
|
250
|
+
cwd,
|
|
251
|
+
stdout: "pipe",
|
|
252
|
+
stderr: "pipe",
|
|
253
|
+
})
|
|
254
|
+
await proc.exited
|
|
255
|
+
return proc.exitCode === 0
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Initialize a repository with agency by setting a template in git config
|
|
260
|
+
*/
|
|
261
|
+
export async function initAgency(
|
|
262
|
+
cwd: string,
|
|
263
|
+
templateName: string,
|
|
264
|
+
): Promise<void> {
|
|
265
|
+
await Bun.spawn(
|
|
266
|
+
["git", "config", "--local", "agency.template", templateName],
|
|
267
|
+
{
|
|
268
|
+
cwd,
|
|
269
|
+
stdout: "pipe",
|
|
270
|
+
stderr: "pipe",
|
|
271
|
+
},
|
|
272
|
+
).exited
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get a git config value for testing
|
|
277
|
+
*/
|
|
278
|
+
export async function getGitConfig(
|
|
279
|
+
key: string,
|
|
280
|
+
gitRoot: string,
|
|
281
|
+
): Promise<string | null> {
|
|
282
|
+
try {
|
|
283
|
+
const proc = Bun.spawn(["git", "config", "--local", "--get", key], {
|
|
284
|
+
cwd: gitRoot,
|
|
285
|
+
stdout: "pipe",
|
|
286
|
+
stderr: "pipe",
|
|
287
|
+
})
|
|
288
|
+
await proc.exited // Must await before reading stdout to prevent hangs in concurrent tests
|
|
289
|
+
const output = await new Response(proc.stdout).text()
|
|
290
|
+
return output.trim() || null
|
|
291
|
+
} catch {
|
|
292
|
+
return null
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Run a git command
|
|
298
|
+
*/
|
|
299
|
+
export async function runGitCommand(
|
|
300
|
+
cwd: string,
|
|
301
|
+
args: string[],
|
|
302
|
+
): Promise<void> {
|
|
303
|
+
const proc = Bun.spawn(args, {
|
|
304
|
+
cwd,
|
|
305
|
+
stdout: "pipe",
|
|
306
|
+
stderr: "pipe",
|
|
307
|
+
})
|
|
308
|
+
await proc.exited
|
|
309
|
+
if (proc.exitCode !== 0) {
|
|
310
|
+
const stderr = await new Response(proc.stderr).text()
|
|
311
|
+
throw new Error(`Git command failed: ${args.join(" ")}\n${stderr}`)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Create a file with content
|
|
317
|
+
*/
|
|
318
|
+
export async function createFile(
|
|
319
|
+
cwd: string,
|
|
320
|
+
filename: string,
|
|
321
|
+
content: string,
|
|
322
|
+
): Promise<void> {
|
|
323
|
+
await Bun.write(join(cwd, filename), content)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Run an Effect in tests with all services provided
|
|
328
|
+
*/
|
|
329
|
+
import { Effect, Layer } from "effect"
|
|
330
|
+
import { GitService } from "./services/GitService"
|
|
331
|
+
import { ConfigService } from "./services/ConfigService"
|
|
332
|
+
import { FileSystemService } from "./services/FileSystemService"
|
|
333
|
+
import { PromptService } from "./services/PromptService"
|
|
334
|
+
import { TemplateService } from "./services/TemplateService"
|
|
335
|
+
import { OpencodeService } from "./services/OpencodeService"
|
|
336
|
+
import { ClaudeService } from "./services/ClaudeService"
|
|
337
|
+
|
|
338
|
+
// Create test layer with all services
|
|
339
|
+
const TestLayer = Layer.mergeAll(
|
|
340
|
+
GitService.Default,
|
|
341
|
+
ConfigService.Default,
|
|
342
|
+
FileSystemService.Default,
|
|
343
|
+
PromptService.Default,
|
|
344
|
+
TemplateService.Default,
|
|
345
|
+
OpencodeService.Default,
|
|
346
|
+
ClaudeService.Default,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
export async function runTestEffect<A, E>(
|
|
350
|
+
effect: Effect.Effect<A, E, any>,
|
|
351
|
+
): Promise<A> {
|
|
352
|
+
const providedEffect = Effect.provide(effect, TestLayer) as Effect.Effect<
|
|
353
|
+
A,
|
|
354
|
+
E,
|
|
355
|
+
never
|
|
356
|
+
>
|
|
357
|
+
const program = Effect.catchAllDefect(providedEffect, (defect) =>
|
|
358
|
+
Effect.fail(defect instanceof Error ? defect : new Error(String(defect))),
|
|
359
|
+
) as Effect.Effect<A, E | Error, never>
|
|
360
|
+
|
|
361
|
+
return await Effect.runPromise(program)
|
|
362
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
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 {
|
|
6
|
+
AgencyMetadataService,
|
|
7
|
+
AgencyMetadataServiceLive,
|
|
8
|
+
} from "./services/AgencyMetadataService"
|
|
9
|
+
import { FileSystemService } from "./services/FileSystemService"
|
|
10
|
+
import { GitService } from "./services/GitService"
|
|
11
|
+
|
|
12
|
+
export interface Command {
|
|
13
|
+
name: string
|
|
14
|
+
description: string
|
|
15
|
+
run: (args: string[], options: Record<string, any>) => Promise<void>
|
|
16
|
+
help?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Load template content from the templates directory.
|
|
21
|
+
* Falls back to inline defaults if files cannot be read (e.g., in bundled packages).
|
|
22
|
+
*/
|
|
23
|
+
async function loadTemplateContent(fileName: string): Promise<string> {
|
|
24
|
+
try {
|
|
25
|
+
// Try to load from the templates directory relative to this file's location
|
|
26
|
+
const templatePath = join(import.meta.dir, "..", "templates", fileName)
|
|
27
|
+
const file = Bun.file(templatePath)
|
|
28
|
+
if (await file.exists()) {
|
|
29
|
+
return await file.text()
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
// Fall through to defaults if file loading fails
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Return inline defaults as fallback
|
|
36
|
+
const defaults: Record<string, string> = {
|
|
37
|
+
"AGENCY.md": `# Agent Instructions
|
|
38
|
+
|
|
39
|
+
## TASK.md
|
|
40
|
+
|
|
41
|
+
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:
|
|
42
|
+
|
|
43
|
+
- What is being built or fixed
|
|
44
|
+
- Current progress and status
|
|
45
|
+
- Remaining work items
|
|
46
|
+
- Any important context or decisions
|
|
47
|
+
|
|
48
|
+
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.
|
|
49
|
+
|
|
50
|
+
See \`TASK.md\` for the current task description and progress.
|
|
51
|
+
`,
|
|
52
|
+
"AGENTS.md": `# Agency
|
|
53
|
+
|
|
54
|
+
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.
|
|
55
|
+
|
|
56
|
+
## Key Commands
|
|
57
|
+
|
|
58
|
+
- \`agency task\` - Initialize template files on a feature branch
|
|
59
|
+
- \`agency edit\` - Open TASK.md in system editor
|
|
60
|
+
- \`agency template save\` - Save current file versions back to a template
|
|
61
|
+
- \`agency template use\` - Switch to a different template
|
|
62
|
+
- \`agency emit\` - Create an emit branch with managed files reverted to their merge-base state
|
|
63
|
+
- \`agency switch\` - Toggle between feature and emit branches
|
|
64
|
+
- \`agency template source\` - Get the path to a template's source directory
|
|
65
|
+
- \`agency set-base\` - Update the saved base branch for emit creation
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
- **Template-based workflow** - Reusable templates stored in \`~/.config/agency/templates/\`
|
|
70
|
+
- **Git integration** - Saves template configuration in \`.git/config\`
|
|
71
|
+
- **Emit branch management** - Automatically creates clean emit branches without local modifications
|
|
72
|
+
- **Multi-file support** - Manages AGENTS.md, TASK.md, and opencode.json
|
|
73
|
+
`,
|
|
74
|
+
"TASK.md": `{task}
|
|
75
|
+
|
|
76
|
+
## Tasks
|
|
77
|
+
|
|
78
|
+
- [ ] Populate this list
|
|
79
|
+
`,
|
|
80
|
+
"opencode.json": JSON.stringify(
|
|
81
|
+
{
|
|
82
|
+
$schema: "https://opencode.ai/config.json",
|
|
83
|
+
instructions: ["AGENCY.md", "TASK.md"],
|
|
84
|
+
},
|
|
85
|
+
null,
|
|
86
|
+
2,
|
|
87
|
+
),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return defaults[fileName] || ""
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Initialize managed files with their default content.
|
|
95
|
+
* This is a synchronous function that returns a promise for the initialized files.
|
|
96
|
+
*/
|
|
97
|
+
export async function initializeManagedFiles(): Promise<ManagedFile[]> {
|
|
98
|
+
const files: ManagedFile[] = []
|
|
99
|
+
|
|
100
|
+
for (const fileName of [
|
|
101
|
+
"AGENCY.md",
|
|
102
|
+
"AGENTS.md",
|
|
103
|
+
"opencode.json",
|
|
104
|
+
"TASK.md",
|
|
105
|
+
]) {
|
|
106
|
+
const content = await loadTemplateContent(fileName)
|
|
107
|
+
files.push({
|
|
108
|
+
name: fileName,
|
|
109
|
+
defaultContent: content,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return files
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// This will be initialized by commands that need it
|
|
117
|
+
// For backward compatibility, export a variable that can be set
|
|
118
|
+
let MANAGED_FILES: ManagedFile[] = []
|
|
119
|
+
|
|
120
|
+
// Validation is now handled by Effect schemas in schemas.ts
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read agency.json metadata from a repository.
|
|
124
|
+
* @deprecated Use AgencyMetadataService.readFromDisk instead
|
|
125
|
+
*/
|
|
126
|
+
export async function readAgencyMetadata(
|
|
127
|
+
gitRoot: string,
|
|
128
|
+
): Promise<AgencyMetadata | null> {
|
|
129
|
+
const program = Effect.gen(function* () {
|
|
130
|
+
const metadataService = yield* AgencyMetadataService
|
|
131
|
+
return yield* metadataService.readFromDisk(gitRoot)
|
|
132
|
+
}).pipe(
|
|
133
|
+
Effect.provide(AgencyMetadataServiceLive),
|
|
134
|
+
Effect.provide(FileSystemService.Default),
|
|
135
|
+
Effect.provide(GitService.Default),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return Effect.runPromise(program)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Write agency.json metadata to a repository.
|
|
143
|
+
* @deprecated Use AgencyMetadataService.write instead
|
|
144
|
+
*/
|
|
145
|
+
export async function writeAgencyMetadata(
|
|
146
|
+
gitRoot: string,
|
|
147
|
+
metadata: AgencyMetadata,
|
|
148
|
+
): Promise<void> {
|
|
149
|
+
const program = Effect.gen(function* () {
|
|
150
|
+
const metadataService = yield* AgencyMetadataService
|
|
151
|
+
return yield* metadataService.write(gitRoot, metadata)
|
|
152
|
+
}).pipe(
|
|
153
|
+
Effect.provide(AgencyMetadataServiceLive),
|
|
154
|
+
Effect.provide(FileSystemService.Default),
|
|
155
|
+
Effect.provide(GitService.Default),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return Effect.runPromise(program)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get list of files to filter during PR/merge operations.
|
|
163
|
+
* Always includes TASK.md, AGENCY.md, and agency.json, plus any backpack files from metadata.
|
|
164
|
+
* @deprecated Use AgencyMetadataService.getFilesToFilter instead
|
|
165
|
+
*/
|
|
166
|
+
export async function getFilesToFilter(gitRoot: string): Promise<string[]> {
|
|
167
|
+
const program = Effect.gen(function* () {
|
|
168
|
+
const metadataService = yield* AgencyMetadataService
|
|
169
|
+
return yield* metadataService.getFilesToFilter(gitRoot)
|
|
170
|
+
}).pipe(
|
|
171
|
+
Effect.provide(AgencyMetadataServiceLive),
|
|
172
|
+
Effect.provide(FileSystemService.Default),
|
|
173
|
+
Effect.provide(GitService.Default),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return Effect.runPromise(program)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get the configured base branch from agency.json metadata.
|
|
181
|
+
* @deprecated Use AgencyMetadataService.getBaseBranch instead
|
|
182
|
+
*/
|
|
183
|
+
export async function getBaseBranchFromMetadata(
|
|
184
|
+
gitRoot: string,
|
|
185
|
+
): Promise<string | null> {
|
|
186
|
+
const program = Effect.gen(function* () {
|
|
187
|
+
const metadataService = yield* AgencyMetadataService
|
|
188
|
+
return yield* metadataService.getBaseBranch(gitRoot)
|
|
189
|
+
}).pipe(
|
|
190
|
+
Effect.provide(AgencyMetadataServiceLive),
|
|
191
|
+
Effect.provide(FileSystemService.Default),
|
|
192
|
+
Effect.provide(GitService.Default),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return Effect.runPromise(program)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Set the base branch in agency.json metadata.
|
|
200
|
+
* @deprecated Use AgencyMetadataService.setBaseBranch instead
|
|
201
|
+
*/
|
|
202
|
+
export async function setBaseBranchInMetadata(
|
|
203
|
+
gitRoot: string,
|
|
204
|
+
baseBranch: string,
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
const program = Effect.gen(function* () {
|
|
207
|
+
const metadataService = yield* AgencyMetadataService
|
|
208
|
+
return yield* metadataService.setBaseBranch(gitRoot, baseBranch)
|
|
209
|
+
}).pipe(
|
|
210
|
+
Effect.provide(AgencyMetadataServiceLive),
|
|
211
|
+
Effect.provide(FileSystemService.Default),
|
|
212
|
+
Effect.provide(GitService.Default),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return Effect.runPromise(program)
|
|
216
|
+
}
|