@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.
- package/README.md +302 -113
- package/cli.ts +123 -415
- package/index.ts +1 -1
- package/package.json +12 -17
- package/skills/agency/SKILL.md +258 -0
- package/src/cli.test.ts +290 -0
- package/src/commands/description.test.ts +103 -0
- package/src/commands/epic.test.ts +103 -0
- package/src/commands/epic.ts +101 -0
- package/src/commands/init.test.ts +42 -290
- package/src/commands/init.ts +15 -123
- package/src/commands/phase.ts +120 -0
- package/src/commands/pr.test.ts +19 -170
- package/src/commands/pr.ts +23 -179
- package/src/commands/repo.test.ts +80 -0
- package/src/commands/repo.ts +83 -0
- package/src/commands/status.test.ts +35 -477
- package/src/commands/status.ts +24 -213
- package/src/commands/task-phase.test.ts +206 -0
- package/src/commands/task.ts +83 -1285
- package/src/commands/validate.test.ts +91 -0
- package/src/commands/validate.ts +50 -0
- package/src/commands/work.test.ts +172 -419
- package/src/commands/work.ts +47 -166
- package/src/services/EpicService.test.ts +71 -0
- package/src/services/EpicService.ts +135 -0
- package/src/services/FileSystemService.ts +61 -92
- package/src/services/PhaseService.ts +323 -0
- package/src/services/PullRequestService.test.ts +386 -0
- package/src/services/PullRequestService.ts +143 -0
- package/src/services/RepositoryService.test.ts +106 -0
- package/src/services/RepositoryService.ts +159 -0
- package/src/services/TaskPhaseService.test.ts +217 -0
- package/src/services/TaskService.ts +192 -0
- package/src/services/WorkbaseService.test.ts +446 -0
- package/src/services/WorkbaseService.ts +493 -0
- package/src/services/WorktreeService.test.ts +599 -0
- package/src/services/WorktreeService.ts +357 -0
- package/src/test-utils.ts +41 -438
- package/src/types.ts +1 -215
- package/src/utils/command.ts +1 -5
- package/src/utils/effect.test.ts +24 -0
- package/src/utils/effect.ts +3 -273
- package/src/utils/process.test.ts +0 -15
- package/src/utils/process.ts +1 -60
- package/src/workbase/frontmatter.test.ts +50 -0
- package/src/workbase/frontmatter.ts +77 -0
- package/src/workbase/repository-reference.test.ts +25 -0
- package/src/workbase/repository-reference.ts +18 -0
- package/src/workbase/schemas.test.ts +146 -0
- package/src/workbase/schemas.ts +78 -0
- package/src/workbase/worktree-command.test.ts +61 -0
- package/src/workbase/worktree-command.ts +52 -0
- package/src/commands/base.test.ts +0 -198
- package/src/commands/base.ts +0 -198
- package/src/commands/clean.test.ts +0 -299
- package/src/commands/clean.ts +0 -274
- package/src/commands/completions.test.ts +0 -51
- package/src/commands/completions.ts +0 -308
- package/src/commands/emit.integration.test.ts +0 -549
- package/src/commands/emit.test.ts +0 -576
- package/src/commands/emit.ts +0 -700
- package/src/commands/emitted.test.ts +0 -226
- package/src/commands/emitted.ts +0 -57
- package/src/commands/loop.test.ts +0 -621
- package/src/commands/loop.ts +0 -406
- package/src/commands/merge.integration.test.ts +0 -195
- package/src/commands/merge.test.ts +0 -246
- package/src/commands/merge.ts +0 -244
- package/src/commands/pull.test.ts +0 -395
- package/src/commands/pull.ts +0 -205
- package/src/commands/push.test.ts +0 -518
- package/src/commands/push.ts +0 -434
- package/src/commands/rebase.test.ts +0 -521
- package/src/commands/rebase.ts +0 -232
- package/src/commands/save.test.ts +0 -247
- package/src/commands/save.ts +0 -162
- package/src/commands/source.test.ts +0 -195
- package/src/commands/source.ts +0 -72
- package/src/commands/switch.test.ts +0 -194
- package/src/commands/switch.ts +0 -84
- package/src/commands/task-branching.test.ts +0 -633
- package/src/commands/task-continue.test.ts +0 -311
- package/src/commands/task-edit.test.ts +0 -235
- package/src/commands/task-main.test.ts +0 -1066
- package/src/commands/task-squash.test.ts +0 -266
- package/src/commands/tasks.test.ts +0 -391
- package/src/commands/tasks.ts +0 -161
- package/src/commands/template-delete.test.ts +0 -178
- package/src/commands/template-delete.ts +0 -98
- package/src/commands/template-list.test.ts +0 -135
- package/src/commands/template-list.ts +0 -87
- package/src/commands/template-view.test.ts +0 -158
- package/src/commands/template-view.ts +0 -86
- package/src/commands/template.test.ts +0 -32
- package/src/commands/template.ts +0 -96
- package/src/commands/use.test.ts +0 -87
- package/src/commands/use.ts +0 -97
- package/src/constants.ts +0 -10
- package/src/errors.ts +0 -17
- package/src/schemas.ts +0 -33
- package/src/services/AgencyMetadataService.ts +0 -311
- package/src/services/ClaudeService.test.ts +0 -184
- package/src/services/ClaudeService.ts +0 -91
- package/src/services/ConfigService.ts +0 -115
- package/src/services/FilterRepoService.ts +0 -164
- package/src/services/FormatterService.test.ts +0 -432
- package/src/services/FormatterService.ts +0 -219
- package/src/services/GitService.ts +0 -1183
- package/src/services/MockFilterRepoService.ts +0 -140
- package/src/services/OpencodeService.ts +0 -263
- package/src/services/PromptService.ts +0 -183
- package/src/services/TemplateService.ts +0 -81
- package/src/types/native-exec.d.ts +0 -8
- package/src/utils/colors.ts +0 -178
- package/src/utils/git-path.ts +0 -6
- package/src/utils/glob.test.ts +0 -161
- package/src/utils/glob.ts +0 -78
- package/src/utils/paths.ts +0 -51
- package/src/utils/pr-branch.test.ts +0 -376
- package/src/utils/pr-branch.ts +0 -508
- package/src/utils/spinner.test.ts +0 -45
- package/src/utils/spinner.ts +0 -96
- package/src/utils/task-parser.test.ts +0 -172
- package/src/utils/task-parser.ts +0 -93
- package/templates/AGENCY.md +0 -22
- package/templates/AGENTS.md +0 -11
- package/templates/CLAUDE.md +0 -3
- package/templates/TASK.md +0 -5
- package/templates/opencode.json +0 -4
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { Data, Effect } from "effect"
|
|
2
|
+
import { dirname, join, resolve } from "node:path"
|
|
3
|
+
import { FileSystemService } from "./FileSystemService"
|
|
4
|
+
import { WorkbaseService } from "./WorkbaseService"
|
|
5
|
+
import { TaskService } from "./TaskService"
|
|
6
|
+
import { PhaseService } from "./PhaseService"
|
|
7
|
+
import {
|
|
8
|
+
expandWorktreeCreateCommand,
|
|
9
|
+
worktreeCommandEnvironment,
|
|
10
|
+
} from "../workbase/worktree-command"
|
|
11
|
+
import type { RepositoryReference } from "../workbase/schemas"
|
|
12
|
+
|
|
13
|
+
class WorktreeError extends Data.TaggedError("WorktreeError")<{
|
|
14
|
+
readonly message: string
|
|
15
|
+
}> {}
|
|
16
|
+
|
|
17
|
+
interface ExecutionWorkspace {
|
|
18
|
+
readonly root: string
|
|
19
|
+
readonly taskPath: string
|
|
20
|
+
readonly phasePath: string | null
|
|
21
|
+
readonly codePath: string
|
|
22
|
+
readonly writablePath: string
|
|
23
|
+
readonly repo: string
|
|
24
|
+
readonly repos: readonly RepositoryReference[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface GitWorktree {
|
|
28
|
+
readonly path: string
|
|
29
|
+
readonly head?: string
|
|
30
|
+
readonly branch?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parseWorktreeList = (output: string): readonly GitWorktree[] => {
|
|
34
|
+
const worktrees: GitWorktree[] = []
|
|
35
|
+
let current: { path: string; head?: string; branch?: string } | undefined
|
|
36
|
+
|
|
37
|
+
for (const field of output.split("\0")) {
|
|
38
|
+
if (field.startsWith("worktree ")) {
|
|
39
|
+
if (current) worktrees.push(current)
|
|
40
|
+
current = { path: field.slice("worktree ".length) }
|
|
41
|
+
} else if (current && field.startsWith("HEAD ")) {
|
|
42
|
+
current.head = field.slice("HEAD ".length)
|
|
43
|
+
} else if (current && field.startsWith("branch ")) {
|
|
44
|
+
current.branch = field.slice("branch ".length)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (current) worktrees.push(current)
|
|
48
|
+
|
|
49
|
+
return worktrees
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
53
|
+
"WorktreeService",
|
|
54
|
+
{
|
|
55
|
+
sync: () => ({
|
|
56
|
+
materialize: (
|
|
57
|
+
taskId: string,
|
|
58
|
+
phaseId?: string,
|
|
59
|
+
startPath: string = process.cwd(),
|
|
60
|
+
) =>
|
|
61
|
+
Effect.gen(function* () {
|
|
62
|
+
const fs = yield* FileSystemService
|
|
63
|
+
const workbase = yield* WorkbaseService
|
|
64
|
+
const tasks = yield* TaskService
|
|
65
|
+
const phases = yield* PhaseService
|
|
66
|
+
const { root, config } = yield* workbase.loadConfig(startPath)
|
|
67
|
+
const report = yield* workbase.validate(root)
|
|
68
|
+
const ownershipIssue = report.issues.find((issue) =>
|
|
69
|
+
issue.message.startsWith("Writable branch "),
|
|
70
|
+
)
|
|
71
|
+
if (ownershipIssue) {
|
|
72
|
+
return yield* new WorktreeError({
|
|
73
|
+
message: `${ownershipIssue.path}: ${ownershipIssue.message}`,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
const task = yield* tasks.show(taskId, root)
|
|
77
|
+
|
|
78
|
+
let execution: {
|
|
79
|
+
repo: string
|
|
80
|
+
repos?: readonly RepositoryReference[]
|
|
81
|
+
branch: string
|
|
82
|
+
base: string
|
|
83
|
+
}
|
|
84
|
+
let phasePath: string | null = null
|
|
85
|
+
let codePath: string
|
|
86
|
+
if ("phases" in task.data) {
|
|
87
|
+
if (!phaseId) {
|
|
88
|
+
return yield* new WorktreeError({
|
|
89
|
+
message: `Task '${taskId}' has multiple phases; phase ID is required`,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
const phase = yield* phases.show(taskId, phaseId, root)
|
|
93
|
+
execution = phase.data
|
|
94
|
+
phasePath = phase.path
|
|
95
|
+
codePath = join(dirname(phase.path), "code")
|
|
96
|
+
} else {
|
|
97
|
+
if (phaseId) {
|
|
98
|
+
return yield* new WorktreeError({
|
|
99
|
+
message: `Task '${taskId}' is single-phase and does not accept a phase ID`,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
execution = task.data
|
|
103
|
+
codePath = join(dirname(task.path), "code")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
yield* fs.createDirectory(codePath)
|
|
107
|
+
const checkouts: readonly (
|
|
108
|
+
| { readonly repo: string; readonly branch: string }
|
|
109
|
+
| RepositoryReference
|
|
110
|
+
)[] = [
|
|
111
|
+
{ repo: execution.repo, branch: execution.branch },
|
|
112
|
+
...(execution.repos ?? []),
|
|
113
|
+
]
|
|
114
|
+
for (const checkout of checkouts) {
|
|
115
|
+
const alias = checkout.repo
|
|
116
|
+
const repositoryPath = join(root, "repos", alias)
|
|
117
|
+
const checkoutPath = join(codePath, alias)
|
|
118
|
+
if (!(yield* fs.exists(repositoryPath))) {
|
|
119
|
+
return yield* new WorktreeError({
|
|
120
|
+
message: `Repository alias '${alias}' does not exist`,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const remote = yield* fs.runCommand(
|
|
125
|
+
["git", "-C", repositoryPath, "remote", "get-url", "origin"],
|
|
126
|
+
{ captureOutput: true },
|
|
127
|
+
)
|
|
128
|
+
if (remote.exitCode === 0) {
|
|
129
|
+
const fetch = yield* fs.runCommand(
|
|
130
|
+
["git", "-C", repositoryPath, "fetch", "origin"],
|
|
131
|
+
{ captureOutput: true },
|
|
132
|
+
)
|
|
133
|
+
if (fetch.exitCode !== 0) {
|
|
134
|
+
return yield* new WorktreeError({
|
|
135
|
+
message: `Failed to fetch '${alias}': ${fetch.stderr}`,
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const listed = yield* fs.runCommand(
|
|
141
|
+
[
|
|
142
|
+
"git",
|
|
143
|
+
"-C",
|
|
144
|
+
repositoryPath,
|
|
145
|
+
"worktree",
|
|
146
|
+
"list",
|
|
147
|
+
"--porcelain",
|
|
148
|
+
"-z",
|
|
149
|
+
],
|
|
150
|
+
{ captureOutput: true },
|
|
151
|
+
)
|
|
152
|
+
if (listed.exitCode !== 0) {
|
|
153
|
+
return yield* new WorktreeError({
|
|
154
|
+
message: `Failed to inspect worktrees for '${alias}': ${listed.stderr}`,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
const canonicalCodePath = yield* fs.realPath(codePath)
|
|
158
|
+
const canonicalCheckoutPath = join(canonicalCodePath, alias)
|
|
159
|
+
const worktrees: GitWorktree[] = []
|
|
160
|
+
for (const worktree of parseWorktreeList(listed.stdout)) {
|
|
161
|
+
worktrees.push({
|
|
162
|
+
...worktree,
|
|
163
|
+
path: (yield* fs.exists(worktree.path))
|
|
164
|
+
? yield* fs.realPath(worktree.path)
|
|
165
|
+
: resolve(worktree.path),
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
const registeredAtPath = worktrees.find(
|
|
169
|
+
(worktree) => worktree.path === canonicalCheckoutPath,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if ("branch" in checkout) {
|
|
173
|
+
const branchRef = `refs/heads/${checkout.branch}`
|
|
174
|
+
const branchWorktree = worktrees.find(
|
|
175
|
+
(worktree) => worktree.branch === branchRef,
|
|
176
|
+
)
|
|
177
|
+
if (
|
|
178
|
+
branchWorktree &&
|
|
179
|
+
branchWorktree.path !== canonicalCheckoutPath
|
|
180
|
+
) {
|
|
181
|
+
return yield* new WorktreeError({
|
|
182
|
+
message: `Branch '${checkout.branch}' for repository '${alias}' is already checked out at ${branchWorktree.path}`,
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
if (yield* fs.isDirectory(checkoutPath)) {
|
|
186
|
+
if (registeredAtPath?.branch === branchRef) continue
|
|
187
|
+
return yield* new WorktreeError({
|
|
188
|
+
message: `Existing checkout ${checkoutPath} is not registered to branch '${checkout.branch}'`,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
if (registeredAtPath) {
|
|
192
|
+
return yield* new WorktreeError({
|
|
193
|
+
message: `Worktree registry contains a missing checkout at ${checkoutPath}`,
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let args: string[]
|
|
198
|
+
let env: Record<string, string> | undefined
|
|
199
|
+
if (config.worktreeCreateCommand) {
|
|
200
|
+
const variables = {
|
|
201
|
+
repo: repositoryPath,
|
|
202
|
+
worktree: checkoutPath,
|
|
203
|
+
branch: checkout.branch,
|
|
204
|
+
base: execution.base,
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
args = expandWorktreeCreateCommand(
|
|
208
|
+
config.worktreeCreateCommand,
|
|
209
|
+
variables,
|
|
210
|
+
)
|
|
211
|
+
} catch (cause) {
|
|
212
|
+
return yield* new WorktreeError({
|
|
213
|
+
message:
|
|
214
|
+
cause instanceof Error
|
|
215
|
+
? cause.message
|
|
216
|
+
: "Invalid worktreeCreateCommand",
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
env = worktreeCommandEnvironment(variables)
|
|
220
|
+
} else {
|
|
221
|
+
const branchExists = yield* fs.runCommand(
|
|
222
|
+
[
|
|
223
|
+
"git",
|
|
224
|
+
"-C",
|
|
225
|
+
repositoryPath,
|
|
226
|
+
"show-ref",
|
|
227
|
+
"--verify",
|
|
228
|
+
branchRef,
|
|
229
|
+
],
|
|
230
|
+
{ captureOutput: true },
|
|
231
|
+
)
|
|
232
|
+
if (branchExists.exitCode !== 0) {
|
|
233
|
+
const createBranch = yield* fs.runCommand(
|
|
234
|
+
[
|
|
235
|
+
"git",
|
|
236
|
+
"-C",
|
|
237
|
+
repositoryPath,
|
|
238
|
+
"branch",
|
|
239
|
+
checkout.branch,
|
|
240
|
+
execution.base,
|
|
241
|
+
],
|
|
242
|
+
{ captureOutput: true },
|
|
243
|
+
)
|
|
244
|
+
if (createBranch.exitCode !== 0) {
|
|
245
|
+
return yield* new WorktreeError({
|
|
246
|
+
message: `Failed to create branch '${checkout.branch}': ${createBranch.stderr}`,
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
args = [
|
|
251
|
+
"git",
|
|
252
|
+
"-C",
|
|
253
|
+
repositoryPath,
|
|
254
|
+
"worktree",
|
|
255
|
+
"add",
|
|
256
|
+
checkoutPath,
|
|
257
|
+
checkout.branch,
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const result = yield* fs.runCommand(args, {
|
|
262
|
+
cwd: repositoryPath,
|
|
263
|
+
captureOutput: true,
|
|
264
|
+
env,
|
|
265
|
+
})
|
|
266
|
+
if (result.exitCode !== 0) {
|
|
267
|
+
return yield* new WorktreeError({
|
|
268
|
+
message: `Failed to create worktree for '${alias}': ${result.stderr}`,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
if (!(yield* fs.isDirectory(checkoutPath))) {
|
|
272
|
+
return yield* new WorktreeError({
|
|
273
|
+
message: `Worktree command did not create ${checkoutPath}`,
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
const resolvedRef = yield* fs.runCommand(
|
|
278
|
+
[
|
|
279
|
+
"git",
|
|
280
|
+
"-C",
|
|
281
|
+
repositoryPath,
|
|
282
|
+
"rev-parse",
|
|
283
|
+
"--verify",
|
|
284
|
+
`${checkout.ref}^{commit}`,
|
|
285
|
+
],
|
|
286
|
+
{ captureOutput: true },
|
|
287
|
+
)
|
|
288
|
+
if (resolvedRef.exitCode !== 0) {
|
|
289
|
+
return yield* new WorktreeError({
|
|
290
|
+
message: `Reference '${checkout.ref}' for repository '${alias}' does not resolve to a commit`,
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
const commit = resolvedRef.stdout.trim()
|
|
294
|
+
if (yield* fs.isDirectory(checkoutPath)) {
|
|
295
|
+
if (!registeredAtPath) {
|
|
296
|
+
return yield* new WorktreeError({
|
|
297
|
+
message: `Existing checkout ${checkoutPath} is not registered as a Git worktree`,
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
if (registeredAtPath.branch) {
|
|
301
|
+
return yield* new WorktreeError({
|
|
302
|
+
message: `Reference checkout ${checkoutPath} is attached to branch '${registeredAtPath.branch.replace(/^refs\/heads\//, "")}'`,
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
const currentHead = yield* fs.runCommand(
|
|
306
|
+
["git", "-C", checkoutPath, "rev-parse", "HEAD"],
|
|
307
|
+
{ captureOutput: true },
|
|
308
|
+
)
|
|
309
|
+
if (
|
|
310
|
+
currentHead.exitCode === 0 &&
|
|
311
|
+
currentHead.stdout.trim() === commit
|
|
312
|
+
) {
|
|
313
|
+
continue
|
|
314
|
+
}
|
|
315
|
+
return yield* new WorktreeError({
|
|
316
|
+
message: `Existing checkout ${checkoutPath} does not match reference '${checkout.ref}' (${commit})`,
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
if (registeredAtPath) {
|
|
320
|
+
return yield* new WorktreeError({
|
|
321
|
+
message: `Worktree registry contains a missing checkout at ${checkoutPath}`,
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
const result = yield* fs.runCommand(
|
|
325
|
+
[
|
|
326
|
+
"git",
|
|
327
|
+
"-C",
|
|
328
|
+
repositoryPath,
|
|
329
|
+
"worktree",
|
|
330
|
+
"add",
|
|
331
|
+
"--detach",
|
|
332
|
+
checkoutPath,
|
|
333
|
+
commit,
|
|
334
|
+
],
|
|
335
|
+
{ captureOutput: true },
|
|
336
|
+
)
|
|
337
|
+
if (result.exitCode !== 0) {
|
|
338
|
+
return yield* new WorktreeError({
|
|
339
|
+
message: `Failed to create worktree for '${alias}': ${result.stderr}`,
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
root,
|
|
347
|
+
taskPath: task.path,
|
|
348
|
+
phasePath,
|
|
349
|
+
codePath,
|
|
350
|
+
writablePath: join(codePath, execution.repo),
|
|
351
|
+
repo: execution.repo,
|
|
352
|
+
repos: execution.repos ?? [],
|
|
353
|
+
} satisfies ExecutionWorkspace
|
|
354
|
+
}),
|
|
355
|
+
}),
|
|
356
|
+
},
|
|
357
|
+
) {}
|