@markjaquith/agency 3.2.2 → 3.2.4
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 +30 -62
- package/cli-main.ts +20 -83
- package/fixtures/protocol/orchestration-recipes.json +9 -34
- package/package.json +1 -4
- package/schemas/agency-graph-v1.schema.json +2 -31
- package/src/cli-parser.test.ts +34 -132
- package/src/cli-parser.ts +45 -102
- package/src/cli.test.ts +60 -117
- package/src/commands/act.ts +2 -6
- package/src/commands/sync.ts +3 -3
- package/src/commands/validate.ts +3 -1
- package/src/commands/work.test.ts +70 -8
- package/src/commands/work.ts +15 -8
- package/src/graph-schema.ts +0 -2
- package/src/protocol.test.ts +0 -25
- package/src/protocol.ts +0 -11
- package/src/readiness.test.ts +2 -2
- package/src/services/ArchiveBulkService.test.ts +0 -88
- package/src/services/ArchiveService.ts +0 -32
- package/src/services/GraphMutationService.ts +0 -26
- package/src/services/GraphService.test.ts +1 -1
- package/src/services/IntegrationService.test.ts +7 -4
- package/src/services/LifecycleTransaction.ts +5 -1
- package/src/services/PhaseService.ts +1 -8
- package/src/services/ReadinessService.test.ts +0 -34
- package/src/services/ReadinessService.ts +1 -20
- package/src/services/ReviewService.test.ts +0 -64
- package/src/services/ReviewService.ts +0 -5
- package/src/services/SyncService.test.ts +75 -88
- package/src/services/SyncService.ts +52 -123
- package/src/services/TaskPhaseService.test.ts +13 -1
- package/src/services/TaskService.ts +1 -7
- package/src/services/WorkbaseService.ts +0 -3
- package/src/services/WorktreeService.test.ts +192 -1
- package/src/services/WorktreeService.ts +169 -34
- package/src/test-utils.ts +0 -2
- package/src/usage-log.test.ts +89 -5
- package/src/usage-log.ts +139 -31
- package/src/workbase/AGENTS.md +9 -15
- package/src/workbase/agent-command.test.ts +1 -3
- package/src/workbase/agent-command.ts +1 -6
- package/src/workbase/document-revision.ts +0 -4
- package/src/workbase/opencode-plugin-file.ts +1 -0
- package/src/workbase/schemas.test.ts +12 -25
- package/src/workbase/schemas.ts +0 -16
- package/src/commands/claim.ts +0 -122
- package/src/services/ClaimService.test.ts +0 -415
- package/src/services/ClaimService.ts +0 -608
|
@@ -224,40 +224,6 @@ describe("ReadinessService", () => {
|
|
|
224
224
|
readiness.guardWorkTarget("execution-unit:phase/ship/implement", root),
|
|
225
225
|
)
|
|
226
226
|
|
|
227
|
-
await write(
|
|
228
|
-
root,
|
|
229
|
-
"tasks/ship/phases/implement/PHASE.md",
|
|
230
|
-
`---
|
|
231
|
-
repo: agency
|
|
232
|
-
branch: feat/implement
|
|
233
|
-
base: main
|
|
234
|
-
pr: null
|
|
235
|
-
status: working
|
|
236
|
-
claim:
|
|
237
|
-
claimant: orchestrator
|
|
238
|
-
agent: opencode
|
|
239
|
-
sessionId: session-1
|
|
240
|
-
startedAt: 2026-07-20T00:00:00.000Z
|
|
241
|
-
targetRevision: ${"a".repeat(64)}
|
|
242
|
-
state: active
|
|
243
|
-
---
|
|
244
|
-
|
|
245
|
-
# Execution
|
|
246
|
-
`,
|
|
247
|
-
)
|
|
248
|
-
expect(
|
|
249
|
-
await service((readiness) => readiness.getWorkTargetIds(root)),
|
|
250
|
-
).not.toContain("execution-unit:phase/ship/implement")
|
|
251
|
-
await expect(
|
|
252
|
-
service((readiness) =>
|
|
253
|
-
readiness.guardWorkTarget(
|
|
254
|
-
"execution-unit:phase/ship/implement",
|
|
255
|
-
root,
|
|
256
|
-
true,
|
|
257
|
-
),
|
|
258
|
-
),
|
|
259
|
-
).rejects.toThrow("active claim")
|
|
260
|
-
|
|
261
227
|
const blocked = await service((readiness) =>
|
|
262
228
|
Effect.either(readiness.guardWorkTarget("phase:ship/verify", root)),
|
|
263
229
|
)
|
|
@@ -46,22 +46,13 @@ const executionNodeId = (taskId: string, phaseId?: string) =>
|
|
|
46
46
|
? `execution-unit:phase/${taskId}/${phaseId}`
|
|
47
47
|
: `execution-unit:task/${taskId}`
|
|
48
48
|
|
|
49
|
-
const hasActiveClaim = (node: GraphNode) =>
|
|
50
|
-
node.kind !== "epic" &&
|
|
51
|
-
node.kind !== "repository" &&
|
|
52
|
-
"claim" in node.data &&
|
|
53
|
-
node.data.claim?.state === "active"
|
|
54
|
-
|
|
55
49
|
const isResumableWork = (node: GraphNode) =>
|
|
56
50
|
node.kind !== "repository" &&
|
|
57
51
|
node.status === "working" &&
|
|
58
|
-
!hasActiveClaim(node) &&
|
|
59
52
|
node.readiness.blockers.every((blocker) => blocker.kind !== "validation")
|
|
60
53
|
|
|
61
54
|
const isWorkTarget = (node: GraphNode) =>
|
|
62
|
-
node.kind !== "repository" &&
|
|
63
|
-
!hasActiveClaim(node) &&
|
|
64
|
-
(node.readiness.ready || isResumableWork(node))
|
|
55
|
+
node.kind !== "repository" && (node.readiness.ready || isResumableWork(node))
|
|
65
56
|
|
|
66
57
|
const itemFor = (
|
|
67
58
|
node: ExecutionNode,
|
|
@@ -200,16 +191,6 @@ export class ReadinessService extends Effect.Service<ReadinessService>()(
|
|
|
200
191
|
blockers: [],
|
|
201
192
|
})
|
|
202
193
|
}
|
|
203
|
-
if (hasActiveClaim(node)) {
|
|
204
|
-
return yield* new ExecutionGuardError({
|
|
205
|
-
message: `Cannot work on '${node.key}': it has an active claim. Use agency release or agency finish first.`,
|
|
206
|
-
action: "work",
|
|
207
|
-
target,
|
|
208
|
-
status: node.status!,
|
|
209
|
-
blockedBy: node.readiness!.blockedBy,
|
|
210
|
-
blockers: node.readiness!.blockers,
|
|
211
|
-
})
|
|
212
|
-
}
|
|
213
194
|
if (override) return
|
|
214
195
|
if (!node.readiness.ready && !isResumableWork(node)) {
|
|
215
196
|
const item = {
|
|
@@ -11,7 +11,6 @@ import { GraphService } from "./GraphService"
|
|
|
11
11
|
import { PullRequestService } from "./PullRequestService"
|
|
12
12
|
import { PhaseService } from "./PhaseService"
|
|
13
13
|
import { ArchiveService } from "./ArchiveService"
|
|
14
|
-
import { ClaimService } from "./ClaimService"
|
|
15
14
|
import { SyncService } from "./SyncService"
|
|
16
15
|
import { task as taskCommand } from "../commands/task"
|
|
17
16
|
|
|
@@ -183,69 +182,6 @@ describe("ReviewService", () => {
|
|
|
183
182
|
).rejects.toThrow("cannot be converted to phases")
|
|
184
183
|
})
|
|
185
184
|
|
|
186
|
-
test("participates in claim lifecycle", async () => {
|
|
187
|
-
const task = await createReview()
|
|
188
|
-
const claimed = await runTestEffect(
|
|
189
|
-
ClaimService.pipe(
|
|
190
|
-
Effect.flatMap((service) =>
|
|
191
|
-
service.claim(
|
|
192
|
-
{
|
|
193
|
-
taskId: "review",
|
|
194
|
-
claimant: "reviewer",
|
|
195
|
-
agent: "opencode",
|
|
196
|
-
sessionId: "session",
|
|
197
|
-
revision: task.revision,
|
|
198
|
-
},
|
|
199
|
-
root,
|
|
200
|
-
),
|
|
201
|
-
),
|
|
202
|
-
),
|
|
203
|
-
)
|
|
204
|
-
expect(claimed.data.status).toBe("working")
|
|
205
|
-
const released = await runTestEffect(
|
|
206
|
-
ClaimService.pipe(
|
|
207
|
-
Effect.flatMap((service) =>
|
|
208
|
-
service.release(
|
|
209
|
-
{
|
|
210
|
-
taskId: "review",
|
|
211
|
-
sessionId: "session",
|
|
212
|
-
revision: claimed.revision,
|
|
213
|
-
},
|
|
214
|
-
root,
|
|
215
|
-
),
|
|
216
|
-
),
|
|
217
|
-
),
|
|
218
|
-
)
|
|
219
|
-
expect(released.data.status).toBe("open")
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
test("rejects refresh while actively claimed", async () => {
|
|
223
|
-
const task = await createReview()
|
|
224
|
-
await runTestEffect(
|
|
225
|
-
ClaimService.pipe(
|
|
226
|
-
Effect.flatMap((service) =>
|
|
227
|
-
service.claim(
|
|
228
|
-
{
|
|
229
|
-
taskId: "review",
|
|
230
|
-
claimant: "reviewer",
|
|
231
|
-
agent: "opencode",
|
|
232
|
-
sessionId: "active",
|
|
233
|
-
revision: task.revision,
|
|
234
|
-
},
|
|
235
|
-
root,
|
|
236
|
-
),
|
|
237
|
-
),
|
|
238
|
-
),
|
|
239
|
-
)
|
|
240
|
-
await expect(
|
|
241
|
-
runTestEffect(
|
|
242
|
-
ReviewService.pipe(
|
|
243
|
-
Effect.flatMap((service) => service.refresh("review", root)),
|
|
244
|
-
),
|
|
245
|
-
),
|
|
246
|
-
).rejects.toThrow("active claim")
|
|
247
|
-
})
|
|
248
|
-
|
|
249
185
|
test("rolls back document and checkout when pin advancement fails", async () => {
|
|
250
186
|
const created = await createReview()
|
|
251
187
|
const workspace = await runTestEffect(
|
|
@@ -302,11 +302,6 @@ export class ReviewService extends Effect.Service<ReviewService>()(
|
|
|
302
302
|
})
|
|
303
303
|
}
|
|
304
304
|
const previousReview = task.data.review
|
|
305
|
-
if (task.data.claim?.state === "active") {
|
|
306
|
-
return yield* new ReviewError({
|
|
307
|
-
message: `Review task '${taskId}' has an active claim; release or finish it before refreshing`,
|
|
308
|
-
})
|
|
309
|
-
}
|
|
310
305
|
if (ifRevision && task.revision !== ifRevision) {
|
|
311
306
|
return yield* new RevisionConflictError({
|
|
312
307
|
path: task.path,
|
|
@@ -3,7 +3,6 @@ import { Effect } from "effect"
|
|
|
3
3
|
import { chmod, mkdir, rm } from "node:fs/promises"
|
|
4
4
|
import { join } from "node:path"
|
|
5
5
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
6
|
-
import { ClaimService } from "./ClaimService"
|
|
7
6
|
import { PullRequestService } from "./PullRequestService"
|
|
8
7
|
import { SyncService } from "./SyncService"
|
|
9
8
|
import { TaskService } from "./TaskService"
|
|
@@ -143,30 +142,6 @@ pr: null
|
|
|
143
142
|
["remote", "set-url", "origin", "git@github.com:example/agency.git"],
|
|
144
143
|
join(root, "repos/agency"),
|
|
145
144
|
)
|
|
146
|
-
const inspected = await runTestEffect(
|
|
147
|
-
ClaimService.pipe(
|
|
148
|
-
Effect.flatMap((service) =>
|
|
149
|
-
service.inspect("example", undefined, root),
|
|
150
|
-
),
|
|
151
|
-
),
|
|
152
|
-
)
|
|
153
|
-
await runTestEffect(
|
|
154
|
-
ClaimService.pipe(
|
|
155
|
-
Effect.flatMap((service) =>
|
|
156
|
-
service.claim(
|
|
157
|
-
{
|
|
158
|
-
taskId: "example",
|
|
159
|
-
claimant: "orchestrator",
|
|
160
|
-
agent: "agent",
|
|
161
|
-
sessionId: "session-1",
|
|
162
|
-
revision: inspected.revision,
|
|
163
|
-
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
164
|
-
},
|
|
165
|
-
root,
|
|
166
|
-
),
|
|
167
|
-
),
|
|
168
|
-
),
|
|
169
|
-
)
|
|
170
145
|
await Bun.write(
|
|
171
146
|
join(workspace.codePath, "reference", "LOCAL.md"),
|
|
172
147
|
"dirty\n",
|
|
@@ -176,21 +151,13 @@ pr: null
|
|
|
176
151
|
const before = await Bun.file(taskPath).text()
|
|
177
152
|
const observed = await runTestEffect(
|
|
178
153
|
SyncService.pipe(
|
|
179
|
-
Effect.flatMap((service) =>
|
|
180
|
-
service.reconcile({ cwd: root, now: new Date("2100-01-02") }),
|
|
181
|
-
),
|
|
154
|
+
Effect.flatMap((service) => service.reconcile({ cwd: root })),
|
|
182
155
|
),
|
|
183
156
|
)
|
|
184
157
|
|
|
185
158
|
expect(observed.mode).toBe("dry-run")
|
|
186
|
-
expect(observed.warnings).
|
|
187
|
-
expect.objectContaining({
|
|
188
|
-
kind: "dirty-reference",
|
|
189
|
-
target: "task:example",
|
|
190
|
-
}),
|
|
191
|
-
)
|
|
159
|
+
expect(observed.warnings).toEqual([])
|
|
192
160
|
expect(observed.changes.map((change) => change.kind)).toEqual([
|
|
193
|
-
"release-stale-claim",
|
|
194
161
|
"record-pr",
|
|
195
162
|
"mark-done",
|
|
196
163
|
])
|
|
@@ -199,16 +166,11 @@ pr: null
|
|
|
199
166
|
const applied = await runTestEffect(
|
|
200
167
|
SyncService.pipe(
|
|
201
168
|
Effect.flatMap((service) =>
|
|
202
|
-
service.reconcile({
|
|
203
|
-
cwd: root,
|
|
204
|
-
apply: true,
|
|
205
|
-
now: new Date("2100-01-02"),
|
|
206
|
-
}),
|
|
169
|
+
service.reconcile({ cwd: root, apply: true }),
|
|
207
170
|
),
|
|
208
171
|
),
|
|
209
172
|
)
|
|
210
173
|
expect(applied.changes.map((change) => change.kind)).toEqual([
|
|
211
|
-
"release-stale-claim",
|
|
212
174
|
"record-pr",
|
|
213
175
|
"mark-done",
|
|
214
176
|
])
|
|
@@ -236,7 +198,6 @@ pr: null
|
|
|
236
198
|
baseBranch: "main",
|
|
237
199
|
mergeable: true,
|
|
238
200
|
},
|
|
239
|
-
claim: { state: "released", sessionId: "session-1" },
|
|
240
201
|
})
|
|
241
202
|
})
|
|
242
203
|
|
|
@@ -314,6 +275,70 @@ JSON
|
|
|
314
275
|
})
|
|
315
276
|
})
|
|
316
277
|
|
|
278
|
+
test("rejects a concurrent target change before recording a pull request", async () => {
|
|
279
|
+
await runTestEffect(
|
|
280
|
+
TaskService.pipe(
|
|
281
|
+
Effect.flatMap((service) =>
|
|
282
|
+
service.create(
|
|
283
|
+
{
|
|
284
|
+
id: "concurrent",
|
|
285
|
+
ticketUrl: null,
|
|
286
|
+
repo: "agency",
|
|
287
|
+
branch: "feat/example",
|
|
288
|
+
base: "main",
|
|
289
|
+
},
|
|
290
|
+
root,
|
|
291
|
+
),
|
|
292
|
+
),
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
await git(
|
|
296
|
+
["remote", "set-url", "origin", "git@github.com:example/agency.git"],
|
|
297
|
+
join(root, "repos/agency"),
|
|
298
|
+
)
|
|
299
|
+
const taskPath = join(root, "tasks/concurrent/TASK.md")
|
|
300
|
+
await Bun.write(
|
|
301
|
+
join(root, "bin", "gh"),
|
|
302
|
+
`#!/bin/sh
|
|
303
|
+
printf '\nConcurrent edit.\n' >> ${JSON.stringify(taskPath)}
|
|
304
|
+
cat <<'JSON'
|
|
305
|
+
[{"number":42,"state":"MERGED","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","headRepository":{"nameWithOwner":"example/agency"},"url":"https://github.com/example/agency/pull/42","mergedAt":"2100-01-01T00:00:00Z","mergeCommit":{"oid":"abc"},"mergeable":"MERGEABLE"}]
|
|
306
|
+
JSON
|
|
307
|
+
`,
|
|
308
|
+
)
|
|
309
|
+
await chmod(join(root, "bin", "gh"), 0o755)
|
|
310
|
+
|
|
311
|
+
const failure = await runTestEffect(
|
|
312
|
+
SyncService.pipe(
|
|
313
|
+
Effect.flatMap((service) =>
|
|
314
|
+
service.reconcile({ cwd: root, apply: true }),
|
|
315
|
+
),
|
|
316
|
+
Effect.flip,
|
|
317
|
+
),
|
|
318
|
+
)
|
|
319
|
+
expect(failure).toMatchObject({
|
|
320
|
+
_tag: "RevisionConflictError",
|
|
321
|
+
message: expect.stringContaining("Revision conflict"),
|
|
322
|
+
})
|
|
323
|
+
const content = await Bun.file(taskPath).text()
|
|
324
|
+
expect(content).toContain("Concurrent edit.")
|
|
325
|
+
expect(content).not.toContain("provider: github")
|
|
326
|
+
|
|
327
|
+
await Bun.write(join(root, ".agency-graph-mutation.lock"), "busy")
|
|
328
|
+
const transactionFailure = await runTestEffect(
|
|
329
|
+
SyncService.pipe(
|
|
330
|
+
Effect.flatMap((service) =>
|
|
331
|
+
service.reconcile({ cwd: root, apply: true }),
|
|
332
|
+
),
|
|
333
|
+
Effect.flip,
|
|
334
|
+
),
|
|
335
|
+
)
|
|
336
|
+
expect(transactionFailure).toMatchObject({
|
|
337
|
+
_tag: "SyncError",
|
|
338
|
+
message: expect.stringContaining("graph mutation is in progress"),
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
|
|
317
342
|
test("queries and records a configured delivery provider", async () => {
|
|
318
343
|
await runTestEffect(
|
|
319
344
|
TaskService.pipe(
|
|
@@ -400,13 +425,13 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
|
|
|
400
425
|
expect("pr" in task.data && task.data.pr).toEqual(record)
|
|
401
426
|
})
|
|
402
427
|
|
|
403
|
-
test("marks
|
|
428
|
+
test("marks working execution done after merge", async () => {
|
|
404
429
|
await runTestEffect(
|
|
405
430
|
TaskService.pipe(
|
|
406
431
|
Effect.flatMap((service) =>
|
|
407
432
|
service.create(
|
|
408
433
|
{
|
|
409
|
-
id: "
|
|
434
|
+
id: "working",
|
|
410
435
|
ticketUrl: null,
|
|
411
436
|
repo: "agency",
|
|
412
437
|
branch: "feat/example",
|
|
@@ -420,7 +445,7 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
|
|
|
420
445
|
await runTestEffect(
|
|
421
446
|
WorktreeService.pipe(
|
|
422
447
|
Effect.flatMap((service) =>
|
|
423
|
-
service.materialize("
|
|
448
|
+
service.materialize("working", undefined, root),
|
|
424
449
|
),
|
|
425
450
|
),
|
|
426
451
|
)
|
|
@@ -428,48 +453,13 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
|
|
|
428
453
|
["remote", "set-url", "origin", "git@github.com:example/agency.git"],
|
|
429
454
|
join(root, "repos/agency"),
|
|
430
455
|
)
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
Effect.flatMap((service) =>
|
|
434
|
-
service.inspect("finished-claim", undefined, root),
|
|
435
|
-
),
|
|
436
|
-
),
|
|
437
|
-
)
|
|
438
|
-
const claimed = await runTestEffect(
|
|
439
|
-
ClaimService.pipe(
|
|
440
|
-
Effect.flatMap((service) =>
|
|
441
|
-
service.claim(
|
|
442
|
-
{
|
|
443
|
-
taskId: "finished-claim",
|
|
444
|
-
claimant: "orchestrator",
|
|
445
|
-
agent: "agent",
|
|
446
|
-
sessionId: "session-1",
|
|
447
|
-
revision: initial.revision,
|
|
448
|
-
},
|
|
449
|
-
root,
|
|
450
|
-
),
|
|
451
|
-
),
|
|
452
|
-
),
|
|
453
|
-
)
|
|
454
|
-
const finished = await runTestEffect(
|
|
455
|
-
ClaimService.pipe(
|
|
456
|
+
await runTestEffect(
|
|
457
|
+
TaskService.pipe(
|
|
456
458
|
Effect.flatMap((service) =>
|
|
457
|
-
service.
|
|
458
|
-
{
|
|
459
|
-
taskId: "finished-claim",
|
|
460
|
-
sessionId: "session-1",
|
|
461
|
-
revision: claimed.revision,
|
|
462
|
-
outcome: "done",
|
|
463
|
-
},
|
|
464
|
-
root,
|
|
465
|
-
),
|
|
459
|
+
service.setStatus("working", "working", root),
|
|
466
460
|
),
|
|
467
461
|
),
|
|
468
462
|
)
|
|
469
|
-
expect(finished.data).toMatchObject({
|
|
470
|
-
status: "working",
|
|
471
|
-
claim: { state: "finished", outcome: "done" },
|
|
472
|
-
})
|
|
473
463
|
|
|
474
464
|
const synced = await runTestEffect(
|
|
475
465
|
SyncService.pipe(
|
|
@@ -482,14 +472,11 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
|
|
|
482
472
|
expect(
|
|
483
473
|
await runTestEffect(
|
|
484
474
|
TaskService.pipe(
|
|
485
|
-
Effect.flatMap((service) => service.show("
|
|
475
|
+
Effect.flatMap((service) => service.show("working", root)),
|
|
486
476
|
),
|
|
487
477
|
),
|
|
488
478
|
).toMatchObject({
|
|
489
|
-
data: {
|
|
490
|
-
status: "done",
|
|
491
|
-
claim: { state: "finished", outcome: "done" },
|
|
492
|
-
},
|
|
479
|
+
data: { status: "done" },
|
|
493
480
|
})
|
|
494
481
|
})
|
|
495
482
|
|