@markjaquith/agency 3.2.2 → 3.2.3
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 +20 -58
- package/cli-main.ts +0 -72
- 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 +13 -132
- package/src/cli-parser.ts +10 -101
- package/src/cli.test.ts +12 -111
- 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 +4 -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/workbase/AGENTS.md +9 -15
- package/src/workbase/agent-command.test.ts +0 -3
- package/src/workbase/agent-command.ts +0 -6
- package/src/workbase/document-revision.ts +0 -4
- 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
|
@@ -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
|
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { Data, Effect, Either } from "effect"
|
|
2
2
|
import { dirname, join, resolve } from "node:path"
|
|
3
3
|
import type {
|
|
4
|
-
ClaimRecord,
|
|
5
4
|
PhaseFrontmatter,
|
|
6
5
|
RepositoryReference,
|
|
7
6
|
TaskFrontmatter,
|
|
8
7
|
WorkStatus,
|
|
9
8
|
PullRequestRecord,
|
|
10
9
|
} from "../workbase/schemas"
|
|
10
|
+
import { documentRevision } from "../workbase/document-revision"
|
|
11
|
+
import {
|
|
12
|
+
formatMarkdownDocument,
|
|
13
|
+
parseFrontmatterSync,
|
|
14
|
+
} from "../workbase/frontmatter"
|
|
11
15
|
import {
|
|
12
16
|
normalizePullRequestRecord,
|
|
13
17
|
parseOptionalPullRequestRecord,
|
|
14
18
|
recordFromGitHubJson,
|
|
15
19
|
resolveDeliveryCommand,
|
|
16
20
|
} from "../workbase/delivery-command"
|
|
17
|
-
import { ClaimService } from "./ClaimService"
|
|
18
21
|
import { FileSystemService } from "./FileSystemService"
|
|
22
|
+
import {
|
|
23
|
+
documentWriteStep,
|
|
24
|
+
runLifecycleTransaction,
|
|
25
|
+
} from "./LifecycleTransaction"
|
|
19
26
|
import { WorkbaseService } from "./WorkbaseService"
|
|
20
27
|
import { WorktreeService } from "./WorktreeService"
|
|
21
28
|
import {
|
|
@@ -37,6 +44,7 @@ interface ExecutionRecord {
|
|
|
37
44
|
readonly taskId: string
|
|
38
45
|
readonly phaseId?: string
|
|
39
46
|
readonly path: string
|
|
47
|
+
readonly content: string
|
|
40
48
|
readonly revision: string
|
|
41
49
|
readonly data: ExecutionData
|
|
42
50
|
}
|
|
@@ -49,11 +57,7 @@ interface RegisteredWorktree {
|
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
interface SyncChange {
|
|
52
|
-
readonly kind:
|
|
53
|
-
| "materialize-workspace"
|
|
54
|
-
| "release-stale-claim"
|
|
55
|
-
| "record-pr"
|
|
56
|
-
| "mark-done"
|
|
60
|
+
readonly kind: "materialize-workspace" | "record-pr" | "mark-done"
|
|
57
61
|
readonly target: string
|
|
58
62
|
readonly message: string
|
|
59
63
|
readonly status: "planned" | "applied"
|
|
@@ -84,7 +88,6 @@ interface ExecutionSyncState {
|
|
|
84
88
|
readonly status: WorkStatus
|
|
85
89
|
readonly branch: string | null
|
|
86
90
|
readonly base: string | null
|
|
87
|
-
readonly claim: ClaimRecord | null
|
|
88
91
|
readonly checkouts: readonly CheckoutState[]
|
|
89
92
|
readonly pr: Record<string, unknown>
|
|
90
93
|
readonly review?: {
|
|
@@ -140,11 +143,6 @@ const parseJson = <T>(value: string, fallback: T): T => {
|
|
|
140
143
|
}
|
|
141
144
|
}
|
|
142
145
|
|
|
143
|
-
const isExpired = (claim: ClaimRecord | undefined, now: Date) =>
|
|
144
|
-
claim?.state === "active" &&
|
|
145
|
-
claim.expiresAt !== undefined &&
|
|
146
|
-
Date.parse(claim.expiresAt) <= now.getTime()
|
|
147
|
-
|
|
148
146
|
const isCommitId = (ref: string) => /^[0-9a-f]{40,64}$/i.test(ref)
|
|
149
147
|
|
|
150
148
|
const originRef = (ref: string) =>
|
|
@@ -199,13 +197,32 @@ const mergedPullRequestFromGitHub = (
|
|
|
199
197
|
return current
|
|
200
198
|
}
|
|
201
199
|
|
|
200
|
+
const mutateExecution = (
|
|
201
|
+
root: string,
|
|
202
|
+
record: ExecutionRecord,
|
|
203
|
+
revision: string,
|
|
204
|
+
data: ExecutionData,
|
|
205
|
+
) => {
|
|
206
|
+
const parsed = parseFrontmatterSync(record.content, record.path)
|
|
207
|
+
const content = formatMarkdownDocument(data, parsed.body)
|
|
208
|
+
return runLifecycleTransaction({
|
|
209
|
+
root,
|
|
210
|
+
preconditions: [{ path: record.path, revision }],
|
|
211
|
+
steps: [documentWriteStep(root, [{ path: record.path, content }])],
|
|
212
|
+
}).pipe(
|
|
213
|
+
Effect.as({ data, revision: documentRevision(content) }),
|
|
214
|
+
Effect.catchTag("LifecycleTransactionError", (error) =>
|
|
215
|
+
Effect.fail(new SyncError({ message: error.message })),
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
202
220
|
export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
203
221
|
sync: () => ({
|
|
204
222
|
reconcile: (
|
|
205
223
|
options: {
|
|
206
224
|
readonly cwd?: string
|
|
207
225
|
readonly apply?: boolean
|
|
208
|
-
readonly now?: Date
|
|
209
226
|
readonly onProgress?: (progress: SyncProgress) => void
|
|
210
227
|
readonly taskId?: string
|
|
211
228
|
readonly phaseId?: string
|
|
@@ -215,7 +232,6 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
215
232
|
const fs = yield* FileSystemService
|
|
216
233
|
const workbase = yield* WorkbaseService
|
|
217
234
|
const worktrees = yield* WorktreeService
|
|
218
|
-
const claims = yield* ClaimService
|
|
219
235
|
const repositories = yield* RepositoryService
|
|
220
236
|
const versionControl = yield* VersionControlService
|
|
221
237
|
const { root, config } = yield* workbase.loadConfig(options.cwd)
|
|
@@ -255,6 +271,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
255
271
|
taskId: task.id,
|
|
256
272
|
phaseId: phase.id,
|
|
257
273
|
path: phase.path,
|
|
274
|
+
content: phase.content,
|
|
258
275
|
revision: phase.revision,
|
|
259
276
|
data: phase.data,
|
|
260
277
|
})
|
|
@@ -264,6 +281,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
264
281
|
key: `task:${task.id}`,
|
|
265
282
|
taskId: task.id,
|
|
266
283
|
path: task.path,
|
|
284
|
+
content: task.content,
|
|
267
285
|
revision: task.revision,
|
|
268
286
|
data: task.data,
|
|
269
287
|
})
|
|
@@ -299,7 +317,6 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
299
317
|
})
|
|
300
318
|
|
|
301
319
|
const apply = options.apply === true
|
|
302
|
-
const now = options.now ?? new Date()
|
|
303
320
|
const changes: SyncChange[] = []
|
|
304
321
|
const warnings: SyncNotice[] = []
|
|
305
322
|
const unresolved: SyncNotice[] = []
|
|
@@ -500,8 +517,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
500
517
|
? null
|
|
501
518
|
: mergedPullRequestFromGitHub(data, query)
|
|
502
519
|
const skipCheckoutReconciliation =
|
|
503
|
-
Boolean(data.completion) ||
|
|
504
|
-
(remoteMergedPr !== null && data.claim?.state !== "active")
|
|
520
|
+
Boolean(data.completion) || remoteMergedPr !== null
|
|
505
521
|
let materialize = false
|
|
506
522
|
let workspaceConflict = false
|
|
507
523
|
const declared: readonly (
|
|
@@ -764,57 +780,12 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
764
780
|
})
|
|
765
781
|
}
|
|
766
782
|
|
|
767
|
-
if (
|
|
768
|
-
isExpired(data.claim, now) &&
|
|
769
|
-
(data.status === "working" || data.status === "delegated")
|
|
770
|
-
) {
|
|
771
|
-
const sessionId = data.claim!.sessionId
|
|
772
|
-
if (apply) {
|
|
773
|
-
const expired = yield* claims.expire(
|
|
774
|
-
{
|
|
775
|
-
taskId: record.taskId,
|
|
776
|
-
phaseId: record.phaseId,
|
|
777
|
-
revision,
|
|
778
|
-
now,
|
|
779
|
-
},
|
|
780
|
-
root,
|
|
781
|
-
)
|
|
782
|
-
data = expired.data as ExecutionData
|
|
783
|
-
revision = expired.revision
|
|
784
|
-
} else {
|
|
785
|
-
const claim: ClaimRecord = {
|
|
786
|
-
...data.claim!,
|
|
787
|
-
state: "released",
|
|
788
|
-
releasedAt: now.toISOString(),
|
|
789
|
-
}
|
|
790
|
-
data = { ...data, status: "open", claim }
|
|
791
|
-
}
|
|
792
|
-
changes.push({
|
|
793
|
-
kind: "release-stale-claim",
|
|
794
|
-
target: record.key,
|
|
795
|
-
message: `Release expired claim '${sessionId}'`,
|
|
796
|
-
status: apply ? "applied" : "planned",
|
|
797
|
-
})
|
|
798
|
-
} else if (
|
|
799
|
-
data.claim?.state === "active" &&
|
|
800
|
-
data.status !== "working" &&
|
|
801
|
-
data.status !== "delegated"
|
|
802
|
-
) {
|
|
803
|
-
unresolved.push({
|
|
804
|
-
kind: "claim-status-conflict",
|
|
805
|
-
target: record.key,
|
|
806
|
-
message: `Active claim conflicts with '${data.status}' status`,
|
|
807
|
-
action: "Release or finish the claim explicitly",
|
|
808
|
-
})
|
|
809
|
-
}
|
|
810
|
-
|
|
811
783
|
if (data.completion) {
|
|
812
784
|
executions.push({
|
|
813
785
|
target: record.key,
|
|
814
786
|
status: data.status,
|
|
815
787
|
branch: data.branch,
|
|
816
788
|
base: data.base,
|
|
817
|
-
claim: data.claim ?? null,
|
|
818
789
|
checkouts: checkoutStates,
|
|
819
790
|
pr: { url: null, state: "none" },
|
|
820
791
|
})
|
|
@@ -976,16 +947,11 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
976
947
|
|
|
977
948
|
if (current && JSON.stringify(current) !== JSON.stringify(existing)) {
|
|
978
949
|
if (apply) {
|
|
979
|
-
const recorded = yield*
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
pr: current,
|
|
985
|
-
},
|
|
986
|
-
root,
|
|
987
|
-
)
|
|
988
|
-
data = recorded.data as ExecutionData
|
|
950
|
+
const recorded = yield* mutateExecution(root, record, revision, {
|
|
951
|
+
...data,
|
|
952
|
+
pr: current,
|
|
953
|
+
})
|
|
954
|
+
data = recorded.data
|
|
989
955
|
revision = recorded.revision
|
|
990
956
|
}
|
|
991
957
|
changes.push({
|
|
@@ -1002,35 +968,20 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
1002
968
|
data.status !== "done" &&
|
|
1003
969
|
data.status !== "dropped"
|
|
1004
970
|
) {
|
|
1005
|
-
if (
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
message:
|
|
1010
|
-
"Pull request is merged while the execution unit remains claimed",
|
|
1011
|
-
action: "Finish or release the active claim",
|
|
1012
|
-
})
|
|
1013
|
-
} else {
|
|
1014
|
-
if (apply) {
|
|
1015
|
-
const completed = yield* claims.reconcile(
|
|
1016
|
-
{
|
|
1017
|
-
taskId: record.taskId,
|
|
1018
|
-
phaseId: record.phaseId,
|
|
1019
|
-
revision,
|
|
1020
|
-
status: "done",
|
|
1021
|
-
},
|
|
1022
|
-
root,
|
|
1023
|
-
)
|
|
1024
|
-
data = completed.data as ExecutionData
|
|
1025
|
-
revision = completed.revision
|
|
1026
|
-
}
|
|
1027
|
-
changes.push({
|
|
1028
|
-
kind: "mark-done",
|
|
1029
|
-
target: record.key,
|
|
1030
|
-
message: "Mark execution unit done from merged pull request",
|
|
1031
|
-
status: apply ? "applied" : "planned",
|
|
971
|
+
if (apply) {
|
|
972
|
+
const completed = yield* mutateExecution(root, record, revision, {
|
|
973
|
+
...data,
|
|
974
|
+
status: "done",
|
|
1032
975
|
})
|
|
976
|
+
data = completed.data
|
|
977
|
+
revision = completed.revision
|
|
1033
978
|
}
|
|
979
|
+
changes.push({
|
|
980
|
+
kind: "mark-done",
|
|
981
|
+
target: record.key,
|
|
982
|
+
message: "Mark execution unit done from merged pull request",
|
|
983
|
+
status: apply ? "applied" : "planned",
|
|
984
|
+
})
|
|
1034
985
|
}
|
|
1035
986
|
|
|
1036
987
|
executions.push({
|
|
@@ -1038,7 +989,6 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
1038
989
|
status: data.status,
|
|
1039
990
|
branch: data.branch,
|
|
1040
991
|
base: data.base,
|
|
1041
|
-
claim: data.claim ?? null,
|
|
1042
992
|
checkouts: checkoutStates,
|
|
1043
993
|
pr,
|
|
1044
994
|
})
|
|
@@ -1047,27 +997,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
1047
997
|
|
|
1048
998
|
for (const task of reviewRecords) {
|
|
1049
999
|
if (!("review" in task.data)) continue
|
|
1050
|
-
|
|
1051
|
-
let revision = task.revision
|
|
1052
|
-
if (
|
|
1053
|
-
isExpired(data.claim, now) &&
|
|
1054
|
-
(data.status === "working" || data.status === "delegated")
|
|
1055
|
-
) {
|
|
1056
|
-
if (apply) {
|
|
1057
|
-
const expired = yield* claims.expire(
|
|
1058
|
-
{ taskId: task.id, revision, now },
|
|
1059
|
-
root,
|
|
1060
|
-
)
|
|
1061
|
-
if ("review" in expired.data) data = expired.data
|
|
1062
|
-
revision = expired.revision
|
|
1063
|
-
}
|
|
1064
|
-
changes.push({
|
|
1065
|
-
kind: "release-stale-claim",
|
|
1066
|
-
target: `task:${task.id}`,
|
|
1067
|
-
message: `Release expired claim '${data.claim?.sessionId ?? "unknown"}'`,
|
|
1068
|
-
status: apply ? "applied" : "planned",
|
|
1069
|
-
})
|
|
1070
|
-
}
|
|
1000
|
+
const data = task.data
|
|
1071
1001
|
const inspection = yield* worktrees.inspect(task.id, undefined, root)
|
|
1072
1002
|
for (const conflict of inspection.conflicts) {
|
|
1073
1003
|
unresolved.push({
|
|
@@ -1118,7 +1048,6 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
1118
1048
|
status: data.status,
|
|
1119
1049
|
branch: null,
|
|
1120
1050
|
base: null,
|
|
1121
|
-
claim: data.claim ?? null,
|
|
1122
1051
|
checkouts: checkout
|
|
1123
1052
|
? [
|
|
1124
1053
|
{
|
|
@@ -615,7 +615,19 @@ describe("task and phase services", () => {
|
|
|
615
615
|
),
|
|
616
616
|
),
|
|
617
617
|
),
|
|
618
|
-
).rejects.toThrow("
|
|
618
|
+
).rejects.toThrow("Delegated status cannot be set directly")
|
|
619
|
+
await Bun.write(
|
|
620
|
+
createdTask.path,
|
|
621
|
+
workingTask.content.replace("status: working", "status: delegated"),
|
|
622
|
+
)
|
|
623
|
+
const reopenedDelegatedTask = await runTestEffect(
|
|
624
|
+
TaskService.pipe(
|
|
625
|
+
Effect.flatMap((service) =>
|
|
626
|
+
service.setStatus("single-status", "open", root),
|
|
627
|
+
),
|
|
628
|
+
),
|
|
629
|
+
)
|
|
630
|
+
expect(reopenedDelegatedTask.data.status).toBe("open")
|
|
619
631
|
await expect(
|
|
620
632
|
runTestEffect(
|
|
621
633
|
TaskService.pipe(
|
|
@@ -610,8 +610,7 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
610
610
|
const validStatus = yield* decodeStatus(status)
|
|
611
611
|
if (validStatus === "delegated") {
|
|
612
612
|
return yield* new TaskError({
|
|
613
|
-
message:
|
|
614
|
-
"Delegation requires explicit ownership; use 'agency claim'",
|
|
613
|
+
message: "Delegated status cannot be set directly",
|
|
615
614
|
})
|
|
616
615
|
}
|
|
617
616
|
const record = yield* service.show(id, startPath)
|
|
@@ -620,11 +619,6 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
620
619
|
message: `Task '${id}' has multiple phases; set status on a phase instead`,
|
|
621
620
|
})
|
|
622
621
|
}
|
|
623
|
-
if (record.data.claim?.state === "active") {
|
|
624
|
-
return yield* new TaskError({
|
|
625
|
-
message: `Task '${id}' has an active claim; use agency release or agency finish`,
|
|
626
|
-
})
|
|
627
|
-
}
|
|
628
622
|
if (nonPrCompletion && validStatus !== "done") {
|
|
629
623
|
return yield* new TaskError({
|
|
630
624
|
message: "Non-PR completion is valid only with a done status",
|
|
@@ -870,9 +870,6 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
870
870
|
"Non-PR completion cannot have a recorded pull request",
|
|
871
871
|
)
|
|
872
872
|
}
|
|
873
|
-
if (record.data.claim?.state === "active") {
|
|
874
|
-
issue(record.path, "Completed work cannot have an active claim")
|
|
875
|
-
}
|
|
876
873
|
}
|
|
877
874
|
|
|
878
875
|
for (const epic of epics.values()) {
|