@markjaquith/agency 2.12.0 → 2.14.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 +46 -7
- package/cli.ts +72 -3
- package/package.json +1 -1
- package/schemas/agency-graph-v1.schema.json +28 -1
- package/skills/agency/SKILL.md +14 -3
- package/src/cli-parser.test.ts +97 -0
- package/src/cli-parser.ts +100 -4
- package/src/cli.test.ts +155 -0
- package/src/commands/claim.ts +97 -0
- package/src/commands/phase.ts +1 -1
- package/src/commands/task-phase.test.ts +4 -4
- package/src/commands/task.ts +1 -1
- package/src/commands/work.test.ts +65 -1
- package/src/commands/work.ts +94 -5
- package/src/protocol.test.ts +25 -0
- package/src/protocol.ts +16 -0
- package/src/services/ClaimService.test.ts +270 -0
- package/src/services/ClaimService.ts +446 -0
- package/src/services/PhaseService.ts +13 -0
- package/src/services/TaskPhaseService.test.ts +10 -9
- package/src/services/TaskService.ts +11 -0
- package/src/services/WorktreeService.test.ts +82 -7
- package/src/services/WorktreeService.ts +199 -41
- package/src/test-utils.ts +2 -0
- package/src/utils/command.ts +1 -0
- package/src/workbase/AGENTS.md +2 -2
- package/src/workbase/document-revision.ts +2 -0
- package/src/workbase/frontmatter.ts +59 -53
- package/src/workbase/schemas.test.ts +27 -0
- package/src/workbase/schemas.ts +21 -0
|
@@ -193,6 +193,8 @@ export class PhaseService extends Effect.Service<PhaseService>()(
|
|
|
193
193
|
branch: task.data.branch,
|
|
194
194
|
base: task.data.base,
|
|
195
195
|
pr: task.data.pr,
|
|
196
|
+
status: task.data.status,
|
|
197
|
+
...(task.data.claim ? { claim: task.data.claim } : {}),
|
|
196
198
|
})
|
|
197
199
|
const firstTitle = firstPhaseId!
|
|
198
200
|
.split("-")
|
|
@@ -340,7 +342,18 @@ export class PhaseService extends Effect.Service<PhaseService>()(
|
|
|
340
342
|
const fs = yield* FileSystemService
|
|
341
343
|
const service = yield* PhaseService
|
|
342
344
|
const validStatus = yield* decodeStatus(status)
|
|
345
|
+
if (validStatus === "working" || validStatus === "delegated") {
|
|
346
|
+
return yield* new PhaseError({
|
|
347
|
+
message:
|
|
348
|
+
"Active work and delegation require explicit ownership; use 'agency claim'",
|
|
349
|
+
})
|
|
350
|
+
}
|
|
343
351
|
const record = yield* service.show(taskId, id, startPath)
|
|
352
|
+
if (record.data.claim?.state === "active") {
|
|
353
|
+
return yield* new PhaseError({
|
|
354
|
+
message: `Phase '${id}' has an active claim; use agency release or agency finish`,
|
|
355
|
+
})
|
|
356
|
+
}
|
|
344
357
|
if (!canTransitionStatus(record.data.status, validStatus)) {
|
|
345
358
|
return yield* new PhaseError({
|
|
346
359
|
message: `Cannot transition phase '${id}' from ${record.data.status} to ${validStatus}; reopen it first`,
|
|
@@ -234,16 +234,17 @@ describe("task and phase services", () => {
|
|
|
234
234
|
),
|
|
235
235
|
)
|
|
236
236
|
expect(createdTask.content).toContain("status: open")
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
for (const status of ["working", "delegated"]) {
|
|
238
|
+
await expect(
|
|
239
|
+
runTestEffect(
|
|
240
|
+
TaskService.pipe(
|
|
241
|
+
Effect.flatMap((service) =>
|
|
242
|
+
service.setStatus("single-status", status, root),
|
|
243
|
+
),
|
|
244
|
+
),
|
|
241
245
|
),
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
expect(task.data.status).toBe("delegated")
|
|
245
|
-
expect(task.content).toContain("status: delegated")
|
|
246
|
-
expect(task.content).toContain("Describe the task outcome.")
|
|
246
|
+
).rejects.toThrow("require explicit ownership")
|
|
247
|
+
}
|
|
247
248
|
await runTestEffect(
|
|
248
249
|
TaskService.pipe(
|
|
249
250
|
Effect.flatMap((service) =>
|
|
@@ -207,12 +207,23 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
207
207
|
const fs = yield* FileSystemService
|
|
208
208
|
const service = yield* TaskService
|
|
209
209
|
const validStatus = yield* decodeStatus(status)
|
|
210
|
+
if (validStatus === "working" || validStatus === "delegated") {
|
|
211
|
+
return yield* new TaskError({
|
|
212
|
+
message:
|
|
213
|
+
"Active work and delegation require explicit ownership; use 'agency claim'",
|
|
214
|
+
})
|
|
215
|
+
}
|
|
210
216
|
const record = yield* service.show(id, startPath)
|
|
211
217
|
if ("phases" in record.data) {
|
|
212
218
|
return yield* new TaskError({
|
|
213
219
|
message: `Task '${id}' has multiple phases; set status on a phase instead`,
|
|
214
220
|
})
|
|
215
221
|
}
|
|
222
|
+
if (record.data.claim?.state === "active") {
|
|
223
|
+
return yield* new TaskError({
|
|
224
|
+
message: `Task '${id}' has an active claim; use agency release or agency finish`,
|
|
225
|
+
})
|
|
226
|
+
}
|
|
216
227
|
if (!canTransitionStatus(record.data.status, validStatus)) {
|
|
217
228
|
return yield* new TaskError({
|
|
218
229
|
message: `Cannot transition task '${id}' from ${record.data.status} to ${validStatus}; reopen it first`,
|
|
@@ -113,15 +113,17 @@ describe("WorktreeService", () => {
|
|
|
113
113
|
["remote", "set-url", "origin", join(root, "missing")],
|
|
114
114
|
join(root, "repos/agency"),
|
|
115
115
|
)
|
|
116
|
-
await
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
service.materialize("existing", undefined, root),
|
|
121
|
-
),
|
|
116
|
+
const reused = await runTestEffect(
|
|
117
|
+
WorktreeService.pipe(
|
|
118
|
+
Effect.flatMap((service) =>
|
|
119
|
+
service.materialize("existing", undefined, root),
|
|
122
120
|
),
|
|
123
121
|
),
|
|
124
|
-
)
|
|
122
|
+
)
|
|
123
|
+
expect(reused.checkouts).toEqual([
|
|
124
|
+
expect.objectContaining({ repo: "agency", action: "reused" }),
|
|
125
|
+
])
|
|
126
|
+
expect(reused.operations).toEqual([])
|
|
125
127
|
})
|
|
126
128
|
|
|
127
129
|
test("reuses an immutable reference checkout without fetching", async () => {
|
|
@@ -771,6 +773,79 @@ pr: null
|
|
|
771
773
|
"refs/heads/task/removable",
|
|
772
774
|
])
|
|
773
775
|
expect(branch.exitCode).toBe(0)
|
|
776
|
+
expect(workspace.checkouts).toEqual([
|
|
777
|
+
expect.objectContaining({
|
|
778
|
+
repo: "agency",
|
|
779
|
+
kind: "writable",
|
|
780
|
+
action: "created",
|
|
781
|
+
resolvedCommit: expect.stringMatching(/^[0-9a-f]{40}$/),
|
|
782
|
+
}),
|
|
783
|
+
expect.objectContaining({
|
|
784
|
+
repo: "effect",
|
|
785
|
+
kind: "reference",
|
|
786
|
+
action: "created",
|
|
787
|
+
resolvedCommit: expect.stringMatching(/^[0-9a-f]{40}$/),
|
|
788
|
+
}),
|
|
789
|
+
])
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
test("reports dry-run fetch and worktree changes without mutating", async () => {
|
|
793
|
+
await runTestEffect(
|
|
794
|
+
TaskService.pipe(
|
|
795
|
+
Effect.flatMap((service) =>
|
|
796
|
+
service.create(
|
|
797
|
+
{
|
|
798
|
+
id: "planned",
|
|
799
|
+
ticketUrl: "https://example.com/task",
|
|
800
|
+
repo: "agency",
|
|
801
|
+
repos: [{ repo: "effect", ref: "main" }],
|
|
802
|
+
branch: "task/planned",
|
|
803
|
+
base: "main",
|
|
804
|
+
},
|
|
805
|
+
root,
|
|
806
|
+
),
|
|
807
|
+
),
|
|
808
|
+
),
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
const workspace = await runTestEffect(
|
|
812
|
+
WorktreeService.pipe(
|
|
813
|
+
Effect.flatMap((service) =>
|
|
814
|
+
service.materialize("planned", undefined, root, { dryRun: true }),
|
|
815
|
+
),
|
|
816
|
+
),
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
expect(workspace.dryRun).toBe(true)
|
|
820
|
+
expect(
|
|
821
|
+
workspace.checkouts.every(({ resolvedCommit }) => resolvedCommit),
|
|
822
|
+
).toBe(true)
|
|
823
|
+
expect(
|
|
824
|
+
workspace.checkouts.map(({ repo, action }) => ({ repo, action })),
|
|
825
|
+
).toEqual([
|
|
826
|
+
{ repo: "agency", action: "created" },
|
|
827
|
+
{ repo: "effect", action: "created" },
|
|
828
|
+
])
|
|
829
|
+
expect(workspace.operations).toEqual(
|
|
830
|
+
expect.arrayContaining([
|
|
831
|
+
expect.objectContaining({ action: "fetch", status: "planned" }),
|
|
832
|
+
expect.objectContaining({
|
|
833
|
+
action: "create-worktree",
|
|
834
|
+
status: "planned",
|
|
835
|
+
}),
|
|
836
|
+
]),
|
|
837
|
+
)
|
|
838
|
+
expect(await Bun.file(workspace.codePath).exists()).toBe(false)
|
|
839
|
+
expect(
|
|
840
|
+
Bun.spawnSync([
|
|
841
|
+
"git",
|
|
842
|
+
"-C",
|
|
843
|
+
join(root, "repos/agency"),
|
|
844
|
+
"show-ref",
|
|
845
|
+
"--verify",
|
|
846
|
+
"refs/heads/task/planned",
|
|
847
|
+
]).exitCode,
|
|
848
|
+
).not.toBe(0)
|
|
774
849
|
})
|
|
775
850
|
|
|
776
851
|
test("refuses to remove a worktree with uncommitted changes", async () => {
|
|
@@ -16,6 +16,22 @@ class WorktreeError extends Data.TaggedError("WorktreeError")<{
|
|
|
16
16
|
readonly message: string
|
|
17
17
|
}> {}
|
|
18
18
|
|
|
19
|
+
interface WorkspaceOperation {
|
|
20
|
+
readonly action: "fetch" | "create-branch" | "create-worktree"
|
|
21
|
+
readonly repo: string
|
|
22
|
+
readonly command: readonly string[]
|
|
23
|
+
readonly status: "planned" | "completed"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface WorkspaceCheckout {
|
|
27
|
+
readonly repo: string
|
|
28
|
+
readonly kind: "writable" | "reference"
|
|
29
|
+
readonly path: string
|
|
30
|
+
readonly requestedRef: string
|
|
31
|
+
readonly resolvedCommit: string | null
|
|
32
|
+
readonly action: "created" | "reused"
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
interface ExecutionWorkspace {
|
|
20
36
|
readonly root: string
|
|
21
37
|
readonly taskPath: string
|
|
@@ -24,6 +40,9 @@ interface ExecutionWorkspace {
|
|
|
24
40
|
readonly writablePath: string
|
|
25
41
|
readonly repo: string
|
|
26
42
|
readonly repos: readonly RepositoryReference[]
|
|
43
|
+
readonly dryRun: boolean
|
|
44
|
+
readonly checkouts: readonly WorkspaceCheckout[]
|
|
45
|
+
readonly operations: readonly WorkspaceOperation[]
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
interface GitWorktree {
|
|
@@ -121,7 +140,9 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
121
140
|
codePath = join(dirname(task.path), "code")
|
|
122
141
|
}
|
|
123
142
|
|
|
124
|
-
yield* fs.createDirectory(codePath)
|
|
143
|
+
if (!options.dryRun) yield* fs.createDirectory(codePath)
|
|
144
|
+
const operations: WorkspaceOperation[] = []
|
|
145
|
+
const checkoutReports: WorkspaceCheckout[] = []
|
|
125
146
|
const checkouts: readonly (
|
|
126
147
|
| { readonly repo: string; readonly branch: string }
|
|
127
148
|
| RepositoryReference
|
|
@@ -146,23 +167,38 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
146
167
|
{ captureOutput: true },
|
|
147
168
|
)
|
|
148
169
|
if (remote.exitCode !== 0) return false
|
|
170
|
+
const command = [
|
|
171
|
+
"git",
|
|
172
|
+
"-C",
|
|
173
|
+
repositoryPath,
|
|
174
|
+
"fetch",
|
|
175
|
+
"origin",
|
|
176
|
+
...(ref ? [ref] : []),
|
|
177
|
+
]
|
|
178
|
+
if (options.dryRun) {
|
|
179
|
+
operations.push({
|
|
180
|
+
action: "fetch",
|
|
181
|
+
repo: alias,
|
|
182
|
+
command,
|
|
183
|
+
status: "planned",
|
|
184
|
+
})
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
149
187
|
|
|
150
|
-
const fetch = yield* fs.runCommand(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"-C",
|
|
154
|
-
repositoryPath,
|
|
155
|
-
"fetch",
|
|
156
|
-
"origin",
|
|
157
|
-
...(ref ? [ref] : []),
|
|
158
|
-
],
|
|
159
|
-
{ captureOutput: true },
|
|
160
|
-
)
|
|
188
|
+
const fetch = yield* fs.runCommand(command, {
|
|
189
|
+
captureOutput: true,
|
|
190
|
+
})
|
|
161
191
|
if (fetch.exitCode !== 0) {
|
|
162
192
|
return yield* new WorktreeError({
|
|
163
193
|
message: `Failed to fetch '${alias}': ${fetch.stderr}`,
|
|
164
194
|
})
|
|
165
195
|
}
|
|
196
|
+
operations.push({
|
|
197
|
+
action: "fetch",
|
|
198
|
+
repo: alias,
|
|
199
|
+
command,
|
|
200
|
+
status: "completed",
|
|
201
|
+
})
|
|
166
202
|
return true
|
|
167
203
|
})
|
|
168
204
|
|
|
@@ -183,7 +219,9 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
183
219
|
message: `Failed to inspect worktrees for '${alias}': ${listed.stderr}`,
|
|
184
220
|
})
|
|
185
221
|
}
|
|
186
|
-
const canonicalCodePath = yield* fs.
|
|
222
|
+
const canonicalCodePath = (yield* fs.exists(codePath))
|
|
223
|
+
? yield* fs.realPath(codePath)
|
|
224
|
+
: resolve(codePath)
|
|
187
225
|
const canonicalCheckoutPath = join(canonicalCodePath, alias)
|
|
188
226
|
const worktrees: GitWorktree[] = []
|
|
189
227
|
for (const worktree of parseWorktreeList(listed.stdout)) {
|
|
@@ -212,7 +250,17 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
212
250
|
})
|
|
213
251
|
}
|
|
214
252
|
if (yield* fs.isDirectory(checkoutPath)) {
|
|
215
|
-
if (registeredAtPath?.branch === branchRef)
|
|
253
|
+
if (registeredAtPath?.branch === branchRef) {
|
|
254
|
+
checkoutReports.push({
|
|
255
|
+
repo: alias,
|
|
256
|
+
kind: "writable",
|
|
257
|
+
path: checkoutPath,
|
|
258
|
+
requestedRef: checkout.branch,
|
|
259
|
+
resolvedCommit: registeredAtPath.head ?? null,
|
|
260
|
+
action: "reused",
|
|
261
|
+
})
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
216
264
|
return yield* new WorktreeError({
|
|
217
265
|
message: `Existing checkout ${checkoutPath} is not registered to branch '${checkout.branch}'`,
|
|
218
266
|
})
|
|
@@ -260,21 +308,29 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
260
308
|
{ captureOutput: true },
|
|
261
309
|
)
|
|
262
310
|
if (branchExists.exitCode !== 0) {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
311
|
+
const command = [
|
|
312
|
+
"git",
|
|
313
|
+
"-C",
|
|
314
|
+
repositoryPath,
|
|
315
|
+
"branch",
|
|
316
|
+
checkout.branch,
|
|
317
|
+
execution.base,
|
|
318
|
+
]
|
|
319
|
+
operations.push({
|
|
320
|
+
action: "create-branch",
|
|
321
|
+
repo: alias,
|
|
322
|
+
command,
|
|
323
|
+
status: options.dryRun ? "planned" : "completed",
|
|
324
|
+
})
|
|
325
|
+
if (!options.dryRun) {
|
|
326
|
+
const createBranch = yield* fs.runCommand(command, {
|
|
327
|
+
captureOutput: true,
|
|
277
328
|
})
|
|
329
|
+
if (createBranch.exitCode !== 0) {
|
|
330
|
+
return yield* new WorktreeError({
|
|
331
|
+
message: `Failed to create branch '${checkout.branch}': ${createBranch.stderr}`,
|
|
332
|
+
})
|
|
333
|
+
}
|
|
278
334
|
}
|
|
279
335
|
}
|
|
280
336
|
args = [
|
|
@@ -287,6 +343,48 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
287
343
|
checkout.branch,
|
|
288
344
|
]
|
|
289
345
|
}
|
|
346
|
+
if (options.dryRun) {
|
|
347
|
+
operations.push({
|
|
348
|
+
action: "create-worktree",
|
|
349
|
+
repo: alias,
|
|
350
|
+
command: args,
|
|
351
|
+
status: "planned",
|
|
352
|
+
})
|
|
353
|
+
let resolved = yield* fs.runCommand(
|
|
354
|
+
[
|
|
355
|
+
"git",
|
|
356
|
+
"-C",
|
|
357
|
+
repositoryPath,
|
|
358
|
+
"rev-parse",
|
|
359
|
+
"--verify",
|
|
360
|
+
`${checkout.branch}^{commit}`,
|
|
361
|
+
],
|
|
362
|
+
{ captureOutput: true },
|
|
363
|
+
)
|
|
364
|
+
if (resolved.exitCode !== 0) {
|
|
365
|
+
resolved = yield* fs.runCommand(
|
|
366
|
+
[
|
|
367
|
+
"git",
|
|
368
|
+
"-C",
|
|
369
|
+
repositoryPath,
|
|
370
|
+
"rev-parse",
|
|
371
|
+
"--verify",
|
|
372
|
+
`${execution.base}^{commit}`,
|
|
373
|
+
],
|
|
374
|
+
{ captureOutput: true },
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
checkoutReports.push({
|
|
378
|
+
repo: alias,
|
|
379
|
+
kind: "writable",
|
|
380
|
+
path: checkoutPath,
|
|
381
|
+
requestedRef: checkout.branch,
|
|
382
|
+
resolvedCommit:
|
|
383
|
+
resolved.exitCode === 0 ? resolved.stdout.trim() : null,
|
|
384
|
+
action: "created",
|
|
385
|
+
})
|
|
386
|
+
continue
|
|
387
|
+
}
|
|
290
388
|
|
|
291
389
|
if (config.worktreeCreateCommand) {
|
|
292
390
|
verboseLog(`Running worktree command: ${formatCommand(args)}`)
|
|
@@ -308,6 +406,24 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
308
406
|
message: `Worktree command did not create ${checkoutPath}`,
|
|
309
407
|
})
|
|
310
408
|
}
|
|
409
|
+
operations.push({
|
|
410
|
+
action: "create-worktree",
|
|
411
|
+
repo: alias,
|
|
412
|
+
command: args,
|
|
413
|
+
status: "completed",
|
|
414
|
+
})
|
|
415
|
+
const head = yield* fs.runCommand(
|
|
416
|
+
["git", "-C", checkoutPath, "rev-parse", "HEAD"],
|
|
417
|
+
{ captureOutput: true },
|
|
418
|
+
)
|
|
419
|
+
checkoutReports.push({
|
|
420
|
+
repo: alias,
|
|
421
|
+
kind: "writable",
|
|
422
|
+
path: checkoutPath,
|
|
423
|
+
requestedRef: checkout.branch,
|
|
424
|
+
resolvedCommit: head.exitCode === 0 ? head.stdout.trim() : null,
|
|
425
|
+
action: "created",
|
|
426
|
+
})
|
|
311
427
|
} else {
|
|
312
428
|
const fetched = isCommitId(checkout.ref)
|
|
313
429
|
? false
|
|
@@ -349,6 +465,14 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
349
465
|
currentHead.exitCode === 0 &&
|
|
350
466
|
currentHead.stdout.trim() === commit
|
|
351
467
|
) {
|
|
468
|
+
checkoutReports.push({
|
|
469
|
+
repo: alias,
|
|
470
|
+
kind: "reference",
|
|
471
|
+
path: checkoutPath,
|
|
472
|
+
requestedRef: checkout.ref,
|
|
473
|
+
resolvedCommit: commit,
|
|
474
|
+
action: "reused",
|
|
475
|
+
})
|
|
352
476
|
continue
|
|
353
477
|
}
|
|
354
478
|
return yield* new WorktreeError({
|
|
@@ -360,24 +484,55 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
360
484
|
message: `Worktree registry contains a missing checkout at ${checkoutPath}`,
|
|
361
485
|
})
|
|
362
486
|
}
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
{
|
|
375
|
-
|
|
487
|
+
const command = [
|
|
488
|
+
"git",
|
|
489
|
+
"-C",
|
|
490
|
+
repositoryPath,
|
|
491
|
+
"worktree",
|
|
492
|
+
"add",
|
|
493
|
+
"--detach",
|
|
494
|
+
checkoutPath,
|
|
495
|
+
commit,
|
|
496
|
+
]
|
|
497
|
+
if (options.dryRun) {
|
|
498
|
+
operations.push({
|
|
499
|
+
action: "create-worktree",
|
|
500
|
+
repo: alias,
|
|
501
|
+
command,
|
|
502
|
+
status: "planned",
|
|
503
|
+
})
|
|
504
|
+
checkoutReports.push({
|
|
505
|
+
repo: alias,
|
|
506
|
+
kind: "reference",
|
|
507
|
+
path: checkoutPath,
|
|
508
|
+
requestedRef: checkout.ref,
|
|
509
|
+
resolvedCommit: commit,
|
|
510
|
+
action: "created",
|
|
511
|
+
})
|
|
512
|
+
continue
|
|
513
|
+
}
|
|
514
|
+
const result = yield* fs.runCommand(command, {
|
|
515
|
+
captureOutput: true,
|
|
516
|
+
})
|
|
376
517
|
if (result.exitCode !== 0) {
|
|
377
518
|
return yield* new WorktreeError({
|
|
378
519
|
message: `Failed to create worktree for '${alias}': ${result.stderr}`,
|
|
379
520
|
})
|
|
380
521
|
}
|
|
522
|
+
operations.push({
|
|
523
|
+
action: "create-worktree",
|
|
524
|
+
repo: alias,
|
|
525
|
+
command,
|
|
526
|
+
status: "completed",
|
|
527
|
+
})
|
|
528
|
+
checkoutReports.push({
|
|
529
|
+
repo: alias,
|
|
530
|
+
kind: "reference",
|
|
531
|
+
path: checkoutPath,
|
|
532
|
+
requestedRef: checkout.ref,
|
|
533
|
+
resolvedCommit: commit,
|
|
534
|
+
action: "created",
|
|
535
|
+
})
|
|
381
536
|
}
|
|
382
537
|
}
|
|
383
538
|
|
|
@@ -389,6 +544,9 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
389
544
|
writablePath: join(codePath, execution.repo),
|
|
390
545
|
repo: execution.repo,
|
|
391
546
|
repos: execution.repos ?? [],
|
|
547
|
+
dryRun: options.dryRun === true,
|
|
548
|
+
checkouts: checkoutReports,
|
|
549
|
+
operations,
|
|
392
550
|
} satisfies ExecutionWorkspace
|
|
393
551
|
}),
|
|
394
552
|
|
package/src/test-utils.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { ArchiveService } from "./services/ArchiveService"
|
|
|
15
15
|
import { IntegrationService } from "./services/IntegrationService"
|
|
16
16
|
import { ContextService } from "./services/ContextService"
|
|
17
17
|
import { GraphService } from "./services/GraphService"
|
|
18
|
+
import { ClaimService } from "./services/ClaimService"
|
|
18
19
|
|
|
19
20
|
export const createTempDir = () => mkdtemp(join(tmpdir(), "agency-test-"))
|
|
20
21
|
|
|
@@ -34,6 +35,7 @@ const TestLayer = Layer.mergeAll(
|
|
|
34
35
|
IntegrationService.Default,
|
|
35
36
|
ContextService.Default,
|
|
36
37
|
GraphService.Default,
|
|
38
|
+
ClaimService.Default,
|
|
37
39
|
)
|
|
38
40
|
|
|
39
41
|
export async function runTestEffect<A, E>(
|
package/src/utils/command.ts
CHANGED
package/src/workbase/AGENTS.md
CHANGED
|
@@ -26,8 +26,8 @@ field. Repositories listed in plural `repos` are read-only references.
|
|
|
26
26
|
`agency integration sync` to update managed agent files explicitly.
|
|
27
27
|
- Keep task-level decisions in `TASK.md` and phase-specific delivery context in
|
|
28
28
|
`PHASE.md`.
|
|
29
|
-
-
|
|
30
|
-
`agency
|
|
29
|
+
- Coordinate execution ownership with `agency claim`, `agency release`, and
|
|
30
|
+
`agency finish`; `agency work` claims execution units before launch.
|
|
31
31
|
- Do not manually create, move, or remove worktrees under `code/`.
|
|
32
32
|
- Use `agency archive`, rather than moving work item folders manually.
|
|
33
33
|
- Do not edit bare repositories or repository symlinks under `repos/`.
|
|
@@ -12,65 +12,71 @@ interface ParsedFrontmatter {
|
|
|
12
12
|
readonly body: string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
export const parseFrontmatterSync = (
|
|
16
|
+
content: string,
|
|
17
|
+
path: string,
|
|
18
|
+
): ParsedFrontmatter => {
|
|
19
|
+
try {
|
|
20
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)
|
|
21
|
+
if (!match) {
|
|
22
|
+
throw new Error("Markdown file must begin with YAML frontmatter")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const document = parseDocument(match[1]!, {
|
|
26
|
+
customTags: [],
|
|
27
|
+
merge: false,
|
|
28
|
+
schema: "core",
|
|
29
|
+
strict: true,
|
|
30
|
+
uniqueKeys: true,
|
|
31
|
+
version: "1.2",
|
|
32
|
+
})
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
strict: true,
|
|
28
|
-
uniqueKeys: true,
|
|
29
|
-
version: "1.2",
|
|
30
|
-
})
|
|
34
|
+
const parseMessages = [...document.errors, ...document.warnings]
|
|
35
|
+
if (parseMessages.length > 0) {
|
|
36
|
+
throw new Error(parseMessages.map((error) => error.message).join("; "))
|
|
37
|
+
}
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
let unsupportedFeature: string | null = null
|
|
40
|
+
visit(document, {
|
|
41
|
+
Alias: () => {
|
|
42
|
+
unsupportedFeature = "YAML aliases are not supported"
|
|
43
|
+
},
|
|
44
|
+
Node: (_key, node) => {
|
|
45
|
+
if (node.anchor) {
|
|
46
|
+
unsupportedFeature = "YAML anchors are not supported"
|
|
47
|
+
} else if (node.tag && !node.tag.startsWith("tag:yaml.org,2002:")) {
|
|
48
|
+
unsupportedFeature = "Custom YAML tags are not supported"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
})
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
unsupportedFeature = "YAML aliases are not supported"
|
|
41
|
-
},
|
|
42
|
-
Node: (_key, node) => {
|
|
43
|
-
if (node.anchor) {
|
|
44
|
-
unsupportedFeature = "YAML anchors are not supported"
|
|
45
|
-
} else if (node.tag && !node.tag.startsWith("tag:yaml.org,2002:")) {
|
|
46
|
-
unsupportedFeature = "Custom YAML tags are not supported"
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
})
|
|
53
|
+
if (unsupportedFeature) {
|
|
54
|
+
throw new Error(unsupportedFeature)
|
|
55
|
+
}
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
const data = document.toJS({ maxAliasCount: 0 })
|
|
58
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
59
|
+
throw new Error("YAML frontmatter must be a mapping")
|
|
60
|
+
}
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
return {
|
|
63
|
+
data,
|
|
64
|
+
body: content.slice(match[0].length),
|
|
65
|
+
}
|
|
66
|
+
} catch (cause) {
|
|
67
|
+
throw new FrontmatterParseError({
|
|
68
|
+
path,
|
|
69
|
+
message:
|
|
70
|
+
cause instanceof Error ? cause.message : "Failed to parse frontmatter",
|
|
71
|
+
cause,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
}
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
},
|
|
65
|
-
catch: (cause) =>
|
|
66
|
-
new FrontmatterParseError({
|
|
67
|
-
path,
|
|
68
|
-
message:
|
|
69
|
-
cause instanceof Error
|
|
70
|
-
? cause.message
|
|
71
|
-
: "Failed to parse frontmatter",
|
|
72
|
-
cause,
|
|
73
|
-
}),
|
|
76
|
+
export const parseFrontmatter = (content: string, path: string) =>
|
|
77
|
+
Effect.try({
|
|
78
|
+
try: () => parseFrontmatterSync(content, path),
|
|
79
|
+
catch: (error) => error as FrontmatterParseError,
|
|
74
80
|
})
|
|
75
81
|
|
|
76
82
|
export const formatMarkdownDocument = (data: object, body: string) =>
|