@markjaquith/agency 3.2.9 → 3.2.11
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/package.json +1 -1
- package/src/services/ArchiveService.ts +80 -86
- package/src/services/LifecycleTransaction.test.ts +50 -12
- package/src/services/LifecycleTransaction.ts +152 -133
- package/src/services/PhaseService.ts +7 -6
- package/src/services/RepositoryService.ts +150 -115
- package/src/services/ReviewService.ts +30 -35
- package/src/services/TaskService.ts +23 -21
- package/src/utils/process.test.ts +56 -1
- package/src/utils/process.ts +159 -99
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema, TreeFormatter } from "@effect/schema"
|
|
2
|
-
import { Data, Effect, Either
|
|
2
|
+
import { Data, Effect, Either } from "effect"
|
|
3
3
|
import { lstat, mkdir, open, rename, rm } from "node:fs/promises"
|
|
4
4
|
import { dirname, join, relative, resolve, sep } from "node:path"
|
|
5
5
|
import { EpicService, type EpicRecord } from "./EpicService"
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
directoryMoveStep,
|
|
41
41
|
documentWriteStep,
|
|
42
42
|
runLifecycleTransaction,
|
|
43
|
+
transactionEffect,
|
|
43
44
|
type TransactionStep,
|
|
44
45
|
} from "./LifecycleTransaction"
|
|
45
46
|
import { withWorktreeLocks } from "./WorktreeLock"
|
|
@@ -358,21 +359,6 @@ const applyMutation = (moves: readonly Move[], writes: readonly Write[]) =>
|
|
|
358
359
|
}),
|
|
359
360
|
})
|
|
360
361
|
|
|
361
|
-
const WorktreeLayer = Layer.mergeAll(
|
|
362
|
-
FileSystemService.Default,
|
|
363
|
-
WorkbaseService.Default,
|
|
364
|
-
GitVersionControlService.Default,
|
|
365
|
-
VersionControlService.Default,
|
|
366
|
-
TaskService.Default,
|
|
367
|
-
PhaseService.Default,
|
|
368
|
-
WorktreeService.Default,
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
const runWorktreeEffect = <A, E>(effect: Effect.Effect<A, E, any>) =>
|
|
372
|
-
Effect.runPromise(
|
|
373
|
-
effect.pipe(Effect.provide(WorktreeLayer)) as Effect.Effect<A, E, never>,
|
|
374
|
-
)
|
|
375
|
-
|
|
376
362
|
const runGit = async (args: readonly string[]) => {
|
|
377
363
|
const process = Bun.spawn([...args], { stdout: "pipe", stderr: "pipe" })
|
|
378
364
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
@@ -791,25 +777,26 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
|
|
|
791
777
|
})
|
|
792
778
|
if (!options.dryRun) {
|
|
793
779
|
const snapshots: WorktreeRemovalSnapshot[] = []
|
|
794
|
-
const steps: TransactionStep[] = [
|
|
780
|
+
const steps: TransactionStep<any>[] = [
|
|
795
781
|
documentWriteStep(root, writes),
|
|
796
782
|
{
|
|
797
783
|
label: `remove worktrees for epic ${id}`,
|
|
798
|
-
apply:
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
784
|
+
apply: Effect.gen(function* () {
|
|
785
|
+
for (const unit of executionUnits)
|
|
786
|
+
yield* worktrees.remove(unit.taskId, unit.phaseId, root, {
|
|
787
|
+
snapshots,
|
|
788
|
+
lockHeld: true,
|
|
789
|
+
})
|
|
790
|
+
}).pipe(
|
|
791
|
+
Effect.catchAllCause((cause) =>
|
|
792
|
+
transactionEffect(() =>
|
|
793
|
+
restoreWorktreeSnapshots(snapshots),
|
|
794
|
+
).pipe(Effect.zipRight(Effect.failCause(cause))),
|
|
795
|
+
),
|
|
796
|
+
),
|
|
797
|
+
rollback: transactionEffect(() =>
|
|
798
|
+
restoreWorktreeSnapshots(snapshots),
|
|
799
|
+
),
|
|
813
800
|
manualRecovery: `Run agency work prepare for each execution unit in epic '${id}'`,
|
|
814
801
|
},
|
|
815
802
|
]
|
|
@@ -1074,34 +1061,33 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
|
|
|
1074
1061
|
)
|
|
1075
1062
|
if (!options.dryRun && selected.length > 0) {
|
|
1076
1063
|
const snapshots: WorktreeRemovalSnapshot[] = []
|
|
1077
|
-
const steps: TransactionStep[] = [
|
|
1064
|
+
const steps: TransactionStep<any>[] = [
|
|
1078
1065
|
documentWriteStep(root, writes),
|
|
1079
1066
|
{
|
|
1080
1067
|
label: "remove worktrees for task archive cohort",
|
|
1081
|
-
apply:
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
(phase) => phase.id === unit.phaseId,
|
|
1094
|
-
)
|
|
1095
|
-
: undefined,
|
|
1096
|
-
}),
|
|
1097
|
-
)
|
|
1098
|
-
}
|
|
1099
|
-
} catch (cause) {
|
|
1100
|
-
await restoreWorktreeSnapshots(snapshots)
|
|
1101
|
-
throw cause
|
|
1068
|
+
apply: Effect.gen(function* () {
|
|
1069
|
+
for (const unit of executionUnits) {
|
|
1070
|
+
yield* worktrees.remove(unit.taskId, unit.phaseId, root, {
|
|
1071
|
+
snapshots,
|
|
1072
|
+
lockHeld: true,
|
|
1073
|
+
task: contexts.get(unit.taskId)!.task,
|
|
1074
|
+
phase: unit.phaseId
|
|
1075
|
+
? contexts
|
|
1076
|
+
.get(unit.taskId)!
|
|
1077
|
+
.phases.find((phase) => phase.id === unit.phaseId)
|
|
1078
|
+
: undefined,
|
|
1079
|
+
})
|
|
1102
1080
|
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1081
|
+
}).pipe(
|
|
1082
|
+
Effect.catchAllCause((cause) =>
|
|
1083
|
+
transactionEffect(() =>
|
|
1084
|
+
restoreWorktreeSnapshots(snapshots),
|
|
1085
|
+
).pipe(Effect.zipRight(Effect.failCause(cause))),
|
|
1086
|
+
),
|
|
1087
|
+
),
|
|
1088
|
+
rollback: transactionEffect(() =>
|
|
1089
|
+
restoreWorktreeSnapshots(snapshots),
|
|
1090
|
+
),
|
|
1105
1091
|
manualRecovery:
|
|
1106
1092
|
"Run agency work prepare for each archived execution unit",
|
|
1107
1093
|
},
|
|
@@ -1281,30 +1267,31 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
|
|
|
1281
1267
|
})
|
|
1282
1268
|
if (!options.dryRun) {
|
|
1283
1269
|
const snapshots: WorktreeRemovalSnapshot[] = []
|
|
1284
|
-
const steps: TransactionStep[] = [
|
|
1270
|
+
const steps: TransactionStep<any>[] = [
|
|
1285
1271
|
documentWriteStep(root, writes),
|
|
1286
1272
|
{
|
|
1287
1273
|
label: `remove worktrees for task ${id}`,
|
|
1288
|
-
apply:
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1274
|
+
apply: Effect.gen(function* () {
|
|
1275
|
+
for (const unit of executionUnits)
|
|
1276
|
+
yield* worktrees.remove(
|
|
1277
|
+
unit.taskId,
|
|
1278
|
+
executionPhaseId(unit),
|
|
1279
|
+
root,
|
|
1280
|
+
{
|
|
1281
|
+
snapshots,
|
|
1282
|
+
lockHeld: true,
|
|
1283
|
+
},
|
|
1284
|
+
)
|
|
1285
|
+
}).pipe(
|
|
1286
|
+
Effect.catchAllCause((cause) =>
|
|
1287
|
+
transactionEffect(() =>
|
|
1288
|
+
restoreWorktreeSnapshots(snapshots),
|
|
1289
|
+
).pipe(Effect.zipRight(Effect.failCause(cause))),
|
|
1290
|
+
),
|
|
1291
|
+
),
|
|
1292
|
+
rollback: transactionEffect(() =>
|
|
1293
|
+
restoreWorktreeSnapshots(snapshots),
|
|
1294
|
+
),
|
|
1308
1295
|
manualRecovery: `Run agency work prepare for task '${id}'`,
|
|
1309
1296
|
},
|
|
1310
1297
|
directoryMoveStep(root, dirname(task.path), destination),
|
|
@@ -1426,15 +1413,22 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
|
|
|
1426
1413
|
documentWriteStep(root, writes),
|
|
1427
1414
|
{
|
|
1428
1415
|
label: `remove worktrees for phase ${taskId}/${id}`,
|
|
1429
|
-
apply:
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1416
|
+
apply: worktrees
|
|
1417
|
+
.remove(taskId, id, root, {
|
|
1418
|
+
snapshots,
|
|
1419
|
+
lockHeld: true,
|
|
1420
|
+
})
|
|
1421
|
+
.pipe(
|
|
1422
|
+
Effect.asVoid,
|
|
1423
|
+
Effect.catchAllCause((cause) =>
|
|
1424
|
+
transactionEffect(() =>
|
|
1425
|
+
restoreWorktreeSnapshots(snapshots),
|
|
1426
|
+
).pipe(Effect.zipRight(Effect.failCause(cause))),
|
|
1427
|
+
),
|
|
1428
|
+
),
|
|
1429
|
+
rollback: transactionEffect(() =>
|
|
1430
|
+
restoreWorktreeSnapshots(snapshots),
|
|
1431
|
+
),
|
|
1438
1432
|
manualRecovery: `Run agency work prepare for phase '${taskId}/${id}'`,
|
|
1439
1433
|
},
|
|
1440
1434
|
directoryMoveStep(root, dirname(phase.path), destination),
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { Effect } from "effect"
|
|
2
|
+
import { Deferred, Effect, Fiber } from "effect"
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
import { cleanupTempDir, createTempDir } from "../test-utils"
|
|
5
5
|
import {
|
|
6
6
|
documentWriteStep,
|
|
7
7
|
runLifecycleTransaction,
|
|
8
|
+
transactionEffect,
|
|
8
9
|
} from "./LifecycleTransaction"
|
|
9
10
|
|
|
10
11
|
describe("lifecycle transactions", () => {
|
|
@@ -25,14 +26,16 @@ describe("lifecycle transactions", () => {
|
|
|
25
26
|
steps: [
|
|
26
27
|
{
|
|
27
28
|
label: "write marker",
|
|
28
|
-
apply: (
|
|
29
|
+
apply: transactionEffect(async () => {
|
|
30
|
+
await Bun.write(marker, "applied")
|
|
31
|
+
}),
|
|
29
32
|
},
|
|
30
33
|
{
|
|
31
34
|
label: "reject plan",
|
|
32
|
-
preflight:
|
|
35
|
+
preflight: Effect.sync(() => {
|
|
33
36
|
throw new Error("preflight rejected")
|
|
34
|
-
},
|
|
35
|
-
apply:
|
|
37
|
+
}),
|
|
38
|
+
apply: Effect.void,
|
|
36
39
|
},
|
|
37
40
|
],
|
|
38
41
|
}),
|
|
@@ -57,9 +60,9 @@ describe("lifecycle transactions", () => {
|
|
|
57
60
|
]),
|
|
58
61
|
{
|
|
59
62
|
label: "fail after documents",
|
|
60
|
-
apply:
|
|
63
|
+
apply: Effect.sync(() => {
|
|
61
64
|
throw new Error("injected failure")
|
|
62
|
-
},
|
|
65
|
+
}),
|
|
63
66
|
},
|
|
64
67
|
],
|
|
65
68
|
}).pipe(Effect.either),
|
|
@@ -83,17 +86,17 @@ describe("lifecycle transactions", () => {
|
|
|
83
86
|
steps: [
|
|
84
87
|
{
|
|
85
88
|
label: "external mutation",
|
|
86
|
-
apply:
|
|
87
|
-
rollback:
|
|
89
|
+
apply: Effect.void,
|
|
90
|
+
rollback: Effect.sync(() => {
|
|
88
91
|
throw new Error("rollback failed")
|
|
89
|
-
},
|
|
92
|
+
}),
|
|
90
93
|
manualRecovery: "undo external mutation",
|
|
91
94
|
},
|
|
92
95
|
{
|
|
93
96
|
label: "injected failure",
|
|
94
|
-
apply:
|
|
97
|
+
apply: Effect.sync(() => {
|
|
95
98
|
throw new Error("apply failed")
|
|
96
|
-
},
|
|
99
|
+
}),
|
|
97
100
|
},
|
|
98
101
|
],
|
|
99
102
|
}).pipe(Effect.either),
|
|
@@ -104,4 +107,39 @@ describe("lifecycle transactions", () => {
|
|
|
104
107
|
expect(failure.rolledBack).toEqual([])
|
|
105
108
|
expect(failure.manualRecovery).toEqual(["undo external mutation"])
|
|
106
109
|
})
|
|
110
|
+
|
|
111
|
+
test("waits for rollback and lock release when interrupted", async () => {
|
|
112
|
+
const events: string[] = []
|
|
113
|
+
const blocked = await Effect.runPromise(Deferred.make<void>())
|
|
114
|
+
const fiber = Effect.runFork(
|
|
115
|
+
runLifecycleTransaction({
|
|
116
|
+
root,
|
|
117
|
+
steps: [
|
|
118
|
+
{
|
|
119
|
+
label: "external mutation",
|
|
120
|
+
apply: Effect.sync(() => events.push("applied")),
|
|
121
|
+
rollback: transactionEffect(async () => {
|
|
122
|
+
events.push("rollback started")
|
|
123
|
+
await Bun.sleep(20)
|
|
124
|
+
events.push("rollback finished")
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
label: "block",
|
|
129
|
+
apply: Deferred.succeed(blocked, undefined).pipe(
|
|
130
|
+
Effect.zipRight(Effect.never),
|
|
131
|
+
),
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
await Effect.runPromise(Deferred.await(blocked))
|
|
137
|
+
|
|
138
|
+
await Effect.runPromise(Fiber.interrupt(fiber))
|
|
139
|
+
|
|
140
|
+
expect(events).toEqual(["applied", "rollback started", "rollback finished"])
|
|
141
|
+
expect(
|
|
142
|
+
await Bun.file(join(root, ".agency-graph-mutation.lock")).exists(),
|
|
143
|
+
).toBe(false)
|
|
144
|
+
})
|
|
107
145
|
})
|