opencode-swarm 7.188.12 → 7.190.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 +42 -6
- package/binaries/win32-arm64/swarm-sandbox-runner.exe +0 -0
- package/binaries/win32-x64/swarm-sandbox-runner.exe +0 -0
- package/dist/agents/architect.d.ts +1 -1
- package/dist/background/completion-observer.d.ts +6 -0
- package/dist/cli/{auto-checkpoint-4g963859.js → auto-checkpoint-qvk04fya.js} +9 -9
- package/dist/cli/{checkpoint-y9nbjper.js → checkpoint-9q2r5c3j.js} +18 -20
- package/dist/cli/{coder-settlement-q8ea2jkw.js → coder-settlement-xcparbf3.js} +13 -13
- package/dist/cli/{config-doctor-4v5m6v30.js → config-doctor-by0ak49m.js} +23 -20
- package/dist/cli/{core-879105xb.js → core-b1b3kxzy.js} +4 -4
- package/dist/cli/{curation-policy-65ckmmd5.js → curation-policy-6p6fcfjb.js} +6 -6
- package/dist/cli/{curator-t7g3hamd.js → curator-d7b88mzr.js} +44 -44
- package/dist/cli/{curator-drift-satfdnp2.js → curator-drift-6v604220.js} +9 -9
- package/dist/cli/curator-llm-factory-yvbktznk.js +82 -0
- package/dist/cli/{doctor-knowledge-receipt-phase-id-y3ft2v24.js → doctor-knowledge-receipt-phase-id-kwrvnps9.js} +9 -9
- package/dist/cli/{evidence-summary-service-56rqzmrf.js → evidence-summary-service-mzr2q4bn.js} +21 -22
- package/dist/cli/{gate-evidence-zzmgph9z.js → gate-evidence-78jes9kq.js} +8 -8
- package/dist/cli/guardrail-explain-8kt2mxmw.js +83 -0
- package/dist/cli/{guardrail-log-sxv7q0xw.js → guardrail-log-2sthykag.js} +8 -8
- package/dist/cli/{guardrail-reset-37cr442b.js → guardrail-reset-2n746rzv.js} +44 -44
- package/dist/cli/{hive-promoter-hyeta8d6.js → hive-promoter-s7a5gwhz.js} +44 -44
- package/dist/cli/{index-e638qt2d.js → index-0qc7a88d.js} +3 -3
- package/dist/cli/{index-kap0d52p.js → index-1s95jrgc.js} +3 -3
- package/dist/cli/{index-9tvjwv01.js → index-2fpv2wmc.js} +1 -1
- package/dist/cli/{index-jgr1rsr1.js → index-2kpqqcch.js} +4 -4
- package/dist/cli/{index-msvf6p5y.js → index-3t9t9q2q.js} +44 -44
- package/dist/cli/{index-8q8783g0.js → index-3zwhxk98.js} +1 -1
- package/dist/cli/{index-0b172h6r.js → index-49e65y51.js} +5 -5
- package/dist/cli/{index-5pafc9x6.js → index-69tgdm9t.js} +18 -8
- package/dist/cli/{index-d3rjvm9e.js → index-7vnr5cmp.js} +2 -2
- package/dist/cli/{index-twb3f4tk.js → index-8bx6vhzk.js} +1 -1
- package/dist/cli/{index-84pwb6cz.js → index-9gcqmgbc.js} +5 -5
- package/dist/cli/{index-9sps637v.js → index-9xxqdzp4.js} +4 -4
- package/dist/cli/{index-j0qep2zr.js → index-a5ca62vt.js} +1 -1
- package/dist/cli/{index-xy2403g9.js → index-ajzgeb2s.js} +2 -2
- package/dist/cli/{index-8xabhfwr.js → index-b2j231jy.js} +2 -2
- package/dist/cli/{index-19yt5p2t.js → index-dmq7p8s9.js} +6 -6
- package/dist/cli/{index-95pzyv9a.js → index-ej1hdw16.js} +5 -5
- package/dist/cli/{index-1krdj1sc.js → index-ejk34dnw.js} +127 -13
- package/dist/cli/{index-tbyvvq50.js → index-fb5s32c2.js} +862 -1221
- package/dist/cli/{index-ntda6tje.js → index-feaxsjrt.js} +10 -2
- package/dist/cli/{index-94fgbb34.js → index-ffgcdf49.js} +22147 -15396
- package/dist/cli/{index-fhtcx5ag.js → index-h0xhec3d.js} +4 -4
- package/dist/cli/{index-kaqdzhj3.js → index-kfevm0n5.js} +2 -2
- package/dist/cli/{index-z7gakzb5.js → index-kp8k95v9.js} +4 -4
- package/dist/cli/{index-x1eh2636.js → index-mcv0v2dp.js} +1 -1
- package/dist/cli/{index-9czp2dcg.js → index-mjfpaxw3.js} +7 -7
- package/dist/cli/{index-tmn4pf7q.js → index-mp1s7b0n.js} +7 -7
- package/dist/cli/{index-f0jn2nvs.js → index-mseerehd.js} +26 -10
- package/dist/cli/{index-f6exny9m.js → index-n8q43eff.js} +2 -2
- package/dist/cli/{index-7hr5m7yp.js → index-nkgzkt7c.js} +1 -1
- package/dist/cli/{index-8jy605yf.js → index-npe10823.js} +7 -7
- package/dist/cli/{index-yc0y5fjt.js → index-p69k8v0e.js} +1 -1
- package/dist/cli/{index-p2tqzwh6.js → index-qs9myayh.js} +5 -10
- package/dist/cli/{index-hh7pa3yk.js → index-r9yvpr2m.js} +2 -2
- package/dist/cli/{index-qha2p47y.js → index-rgkzcw2a.js} +5 -5
- package/dist/cli/{index-zfbxfm2m.js → index-s8b67e6h.js} +33 -12
- package/dist/cli/{index-m3hhaf61.js → index-s92a5ta3.js} +4 -4
- package/dist/cli/{index-dd6p8xb5.js → index-sm01zpqe.js} +46 -46
- package/dist/cli/{index-q9kefh33.js → index-sxj143a4.js} +1 -1
- package/dist/cli/{index-2h13hjbf.js → index-t0gj8s46.js} +77 -16
- package/dist/cli/{index-3dqazqk4.js → index-t73adn9b.js} +2 -2
- package/dist/cli/{index-5cksakjf.js → index-tkhenmms.js} +101 -13
- package/dist/cli/{index-zsrtt42s.js → index-v3bmwmqw.js} +25 -1
- package/dist/cli/{index-w9tfb0am.js → index-vhn4e6rm.js} +7 -7
- package/dist/cli/{index-yjjyapbf.js → index-xyvrfram.js} +2 -2
- package/dist/cli/{index-k8sarme1.js → index-y9v41185.js} +1 -1
- package/dist/cli/{index-1xa8wq76.js → index-z3ahy9c2.js} +2 -2
- package/dist/cli/{index-wrdktgpy.js → index-zk6ttjkt.js} +2 -2
- package/dist/cli/{index-7z2pxxye.js → index-zyg1m9d0.js} +4 -4
- package/dist/cli/index.js +45 -45
- package/dist/cli/{knowledge-escalator-dzd35eb3.js → knowledge-escalator-n1ezv1zp.js} +15 -15
- package/dist/cli/{knowledge-events-wkr5ne5f.js → knowledge-events-nyqk3mr7.js} +13 -13
- package/dist/cli/{knowledge-link-h4pdcnya.js → knowledge-link-6hw3z6wn.js} +5 -5
- package/dist/cli/{knowledge-store-sme6sqrb.js → knowledge-store-mnxqcyet.js} +6 -6
- package/dist/cli/{knowledge-validator-jgzeawa8.js → knowledge-validator-z30113mp.js} +8 -8
- package/dist/cli/{mcp-egrbh8na.js → mcp-mnbqw2s5.js} +1 -1
- package/dist/cli/{model-preflight-h7azh4m4.js → model-preflight-ex482fsy.js} +44 -44
- package/dist/cli/{pending-delegations-pg284vsk.js → pending-delegations-j18afz5z.js} +12 -12
- package/dist/cli/{pr-subscriptions-hzc4fg25.js → pr-subscriptions-yngda8mz.js} +12 -12
- package/dist/cli/{pr-workflow-gate-j3r3g3q7.js → pr-workflow-gate-0vt614f0.js} +44 -44
- package/dist/cli/{runner-4hfmp1ag.js → runner-ssnsje35.js} +6 -6
- package/dist/cli/{scan-cursor-zg0a56c1.js → scan-cursor-8pwfg5g9.js} +7 -7
- package/dist/cli/{schema-p246rqkt.js → schema-e58ytmqt.js} +14 -2
- package/dist/cli/{scope-persistence-7qtsmx5c.js → scope-persistence-9qfx6ext.js} +16 -16
- package/dist/cli/{server-twhpwhgq.js → server-wxcfwsdx.js} +45 -45
- package/dist/cli/{skill-generator-7vfbmyrw.js → skill-generator-tbd4tq9r.js} +17 -17
- package/dist/cli/{snapshot-coordination-init-050hf69t.js → snapshot-coordination-init-sckyzb94.js} +44 -44
- package/dist/cli/{speckit-checkoff-d3v522cb.js → speckit-checkoff-k5jm0abh.js} +5 -5
- package/dist/cli/{telemetry-kvhe94gs.js → telemetry-9szfdfqv.js} +1 -1
- package/dist/cli/{workspace-snapshot-30kdf018.js → workspace-snapshot-9f12p1s1.js} +3 -3
- package/dist/cli/{worktree-collision-ownership-yy1tp8t9.js → worktree-collision-ownership-3wwfc72t.js} +14 -14
- package/dist/cli/{worktree-isolation-e56skbfw.js → worktree-isolation-zfk7zk6a.js} +46 -44
- package/dist/commands/coupling.d.ts +21 -4
- package/dist/commands/epic-learning.d.ts +28 -0
- package/dist/commands/epic.d.ts +81 -36
- package/dist/commands/registry.d.ts +8 -8
- package/dist/commands/turbo.d.ts +16 -1
- package/dist/config/constants.d.ts +11 -1
- package/dist/config/loader.d.ts +2 -0
- package/dist/config/schema.d.ts +291 -77
- package/dist/db/coordination-store.d.ts +6 -0
- package/dist/epic/close.d.ts +209 -0
- package/dist/{turbo/epic → epic}/cochange-conflict.d.ts +5 -4
- package/dist/{turbo/epic → epic}/cochange-source.d.ts +10 -1
- package/dist/epic/components.d.ts +238 -0
- package/dist/epic/config-gate.d.ts +25 -0
- package/dist/epic/config.d.ts +101 -0
- package/dist/{turbo/epic → epic}/coupling-report.d.ts +3 -1
- package/dist/epic/declared-scopes.d.ts +37 -0
- package/dist/epic/epic-branch.d.ts +157 -0
- package/dist/epic/gate-policy.d.ts +114 -0
- package/dist/epic/git-once.d.ts +26 -0
- package/dist/epic/learning-store.d.ts +176 -0
- package/dist/epic/learning.d.ts +198 -0
- package/dist/epic/legacy-migration.d.ts +26 -0
- package/dist/epic/lifecycle.d.ts +437 -0
- package/dist/epic/markers.d.ts +123 -0
- package/dist/epic/merge-epoch.d.ts +90 -0
- package/dist/epic/next-wave-format.d.ts +101 -0
- package/dist/epic/next-wave.d.ts +102 -0
- package/dist/epic/phase-readiness.d.ts +221 -0
- package/dist/epic/plan-key.d.ts +104 -0
- package/dist/epic/plan-shaping-seam.d.ts +112 -0
- package/dist/epic/planning-signals.d.ts +38 -0
- package/dist/epic/report.d.ts +152 -0
- package/dist/epic/residue-commit.d.ts +151 -0
- package/dist/epic/scorecard.d.ts +130 -0
- package/dist/epic/shaping-sizing.d.ts +108 -0
- package/dist/epic/shaping-suggestions.d.ts +172 -0
- package/dist/epic/shaping.d.ts +112 -0
- package/dist/epic/sizing.d.ts +60 -0
- package/dist/epic/start.d.ts +187 -0
- package/dist/epic/task-landing.d.ts +83 -0
- package/dist/epic/wave-close.d.ts +112 -0
- package/dist/epic/wave-select.d.ts +95 -0
- package/dist/evaluation/contracts.d.ts +1 -1
- package/dist/evaluation/public-api.d.ts +1 -1
- package/dist/full-auto/policy.d.ts +12 -1
- package/dist/hooks/delegation-gate/worktree-isolation.d.ts +16 -0
- package/dist/hooks/delegation-gate.d.ts +11 -0
- package/dist/hooks/guardrails/index.d.ts +7 -0
- package/dist/hooks/promotion-evidence-store.d.ts +2 -2
- package/dist/index.js +814 -807
- package/dist/plan/manager.d.ts +6 -8
- package/dist/plan/parallel-verdict.d.ts +13 -2
- package/dist/retention/caps.d.ts +1 -1
- package/dist/review/contracts.d.ts +8 -1
- package/dist/services/config-doctor.d.ts +38 -0
- package/dist/session/snapshot-reader.d.ts +2 -0
- package/dist/session/snapshot-writer.d.ts +13 -2
- package/dist/state.d.ts +13 -18
- package/dist/tools/epic-next-wave.d.ts +20 -0
- package/dist/tools/epic-phase-review.d.ts +43 -0
- package/dist/tools/index.d.ts +2 -3
- package/dist/tools/manifest.d.ts +2 -3
- package/dist/tools/phase-complete.d.ts +5 -1
- package/dist/tools/save-plan.d.ts +7 -0
- package/dist/tools/tool-metadata.d.ts +4 -8
- package/dist/worktree/merge.d.ts +14 -1
- package/opencode-swarm.schema.json +273 -28
- package/package.json +1 -1
- package/dist/cli/curator-llm-factory-fx1qnpsh.js +0 -82
- package/dist/cli/guardrail-explain-g03manbh.js +0 -83
- package/dist/tools/epic-plan-waves.d.ts +0 -79
- package/dist/tools/epic-record-divergence.d.ts +0 -73
- package/dist/tools/epic-run-phase.d.ts +0 -179
- package/dist/turbo/epic/activation.d.ts +0 -193
- package/dist/turbo/epic/calibration-engine.d.ts +0 -88
- package/dist/turbo/epic/calibration.d.ts +0 -80
- package/dist/turbo/epic/divergence-recorder.d.ts +0 -123
- package/dist/turbo/epic/index.d.ts +0 -24
- package/dist/turbo/epic/promotion-evidence.d.ts +0 -42
- package/dist/turbo/epic/state.d.ts +0 -69
- package/dist/turbo/epic/task-commit.d.ts +0 -110
- package/dist/turbo/epic/upstream-commits.d.ts +0 -82
- package/dist/turbo/epic/wave-planner.d.ts +0 -83
|
@@ -4,17 +4,14 @@ import {
|
|
|
4
4
|
} from "./index-6b295cas.js";
|
|
5
5
|
import {
|
|
6
6
|
readEffectiveSpecSync
|
|
7
|
-
} from "./index-
|
|
7
|
+
} from "./index-p69k8v0e.js";
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
isSpawnCwdMissing,
|
|
16
|
-
isSpawnCwdUnreadable
|
|
17
|
-
} from "./index-g4fs6z4e.js";
|
|
9
|
+
_internals,
|
|
10
|
+
clearWorktreeMergeStatus,
|
|
11
|
+
getWorktreeMergeFailure,
|
|
12
|
+
initDurableStatusPath,
|
|
13
|
+
scanWorktreeMergeFailuresForRecovery
|
|
14
|
+
} from "./index-v3bmwmqw.js";
|
|
18
15
|
import {
|
|
19
16
|
sanitizeContextText
|
|
20
17
|
} from "./index-m9zzj4ee.js";
|
|
@@ -23,11 +20,10 @@ import {
|
|
|
23
20
|
estimateTokens,
|
|
24
21
|
readSwarmFileAsync,
|
|
25
22
|
validateSwarmPath
|
|
26
|
-
} from "./index-
|
|
23
|
+
} from "./index-nkgzkt7c.js";
|
|
27
24
|
import {
|
|
28
25
|
bunHash,
|
|
29
|
-
bunWrite
|
|
30
|
-
mergeEnvForChild
|
|
26
|
+
bunWrite
|
|
31
27
|
} from "./index-2s7dvzff.js";
|
|
32
28
|
import {
|
|
33
29
|
LedgerStaleWriterError,
|
|
@@ -47,22 +43,25 @@ import {
|
|
|
47
43
|
replayFromLedgerWithStatus,
|
|
48
44
|
takeSnapshotEvent,
|
|
49
45
|
takeSnapshotWithRetry
|
|
50
|
-
} from "./index-
|
|
46
|
+
} from "./index-ej1hdw16.js";
|
|
51
47
|
import {
|
|
52
48
|
appendCoreEventSync
|
|
53
|
-
} from "./index-
|
|
49
|
+
} from "./index-zk6ttjkt.js";
|
|
54
50
|
import {
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
deleteCoordinationStateWithinTransaction,
|
|
52
|
+
getCoordinationStateRaw,
|
|
53
|
+
listCoordinationStateKeys,
|
|
57
54
|
listCoordinationStates,
|
|
58
|
-
transitionCoordinationState
|
|
59
|
-
|
|
55
|
+
transitionCoordinationState,
|
|
56
|
+
transitionCoordinationStateWithinTransaction,
|
|
57
|
+
withCoordinationTransaction
|
|
58
|
+
} from "./index-feaxsjrt.js";
|
|
60
59
|
import {
|
|
61
60
|
DURABILITY_CLASSES,
|
|
62
61
|
getProjectDb,
|
|
63
62
|
projectDbExists,
|
|
64
63
|
withDurabilityClass
|
|
65
|
-
} from "./index-
|
|
64
|
+
} from "./index-2fpv2wmc.js";
|
|
66
65
|
import {
|
|
67
66
|
derivePlanId,
|
|
68
67
|
derivePlanIdentityHash
|
|
@@ -72,7 +71,7 @@ import {
|
|
|
72
71
|
} from "./index-t7hmqgjy.js";
|
|
73
72
|
import {
|
|
74
73
|
emit
|
|
75
|
-
} from "./index-
|
|
74
|
+
} from "./index-7vnr5cmp.js";
|
|
76
75
|
import {
|
|
77
76
|
canonicalRootKeyFresh
|
|
78
77
|
} from "./index-fzpt2djt.js";
|
|
@@ -83,26 +82,24 @@ import {
|
|
|
83
82
|
atomicWriteSwarmFileSync,
|
|
84
83
|
invalidateCachedArtifact,
|
|
85
84
|
readCachedParsedFile
|
|
86
|
-
} from "./index-
|
|
85
|
+
} from "./index-qs9myayh.js";
|
|
87
86
|
import {
|
|
88
87
|
PlanSchema,
|
|
89
88
|
normalizeCurrentPhaseInPlace,
|
|
90
89
|
resolveActivePhaseId
|
|
91
90
|
} from "./index-gx2vp8gx.js";
|
|
92
91
|
import {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
} from "./index-
|
|
92
|
+
init_config,
|
|
93
|
+
init_loader,
|
|
94
|
+
isEpicModeConfigEnabled,
|
|
95
|
+
loadPluginConfigWithMeta
|
|
96
|
+
} from "./index-ejk34dnw.js";
|
|
98
97
|
import {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
} from "./index-wvghb3x6.js";
|
|
98
|
+
exports_external,
|
|
99
|
+
init_zod
|
|
100
|
+
} from "./index-gknz3bv1.js";
|
|
103
101
|
import {
|
|
104
102
|
criticalWarn,
|
|
105
|
-
error,
|
|
106
103
|
init_logger,
|
|
107
104
|
warn
|
|
108
105
|
} from "./index-zgwm4ryv.js";
|
|
@@ -267,680 +264,772 @@ function updateTaskCheckpointReceipt(directory, descriptor, state, sha) {
|
|
|
267
264
|
return rowAfter;
|
|
268
265
|
}
|
|
269
266
|
|
|
270
|
-
// src/utils/transient-retry.ts
|
|
271
|
-
var MAX_TRANSIENT_RETRIES = 5;
|
|
272
|
-
var RETRY_BASE_DELAY_MS = 200;
|
|
273
|
-
function isTransientSpawnError(error2) {
|
|
274
|
-
if (!error2 || typeof error2 !== "object")
|
|
275
|
-
return false;
|
|
276
|
-
const code = error2.code;
|
|
277
|
-
return code === "ETIMEDOUT";
|
|
278
|
-
}
|
|
279
|
-
function transientBackoff(attempt) {
|
|
280
|
-
const delay = RETRY_BASE_DELAY_MS * 2 ** attempt;
|
|
281
|
-
try {
|
|
282
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
|
|
283
|
-
} catch {
|
|
284
|
-
const start = Date.now();
|
|
285
|
-
while (Date.now() - start < delay) {}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
267
|
// src/plan/manager.ts
|
|
290
268
|
import {
|
|
291
|
-
closeSync,
|
|
269
|
+
closeSync as closeSync2,
|
|
292
270
|
fsyncSync,
|
|
293
271
|
mkdirSync as mkdirSync2,
|
|
294
|
-
openSync,
|
|
295
|
-
renameSync
|
|
296
|
-
unlinkSync,
|
|
272
|
+
openSync as openSync2,
|
|
273
|
+
renameSync,
|
|
274
|
+
unlinkSync as unlinkSync2,
|
|
297
275
|
writeFileSync
|
|
298
276
|
} from "fs";
|
|
299
277
|
import * as fsPromises from "fs/promises";
|
|
300
278
|
import * as path3 from "path";
|
|
301
279
|
|
|
302
|
-
// src/
|
|
303
|
-
|
|
304
|
-
import * as child_process from "child_process";
|
|
305
|
-
import * as fsSync from "fs";
|
|
306
|
-
import path from "path";
|
|
307
|
-
init_git_executable();
|
|
280
|
+
// src/epic/merge-epoch.ts
|
|
281
|
+
import * as path2 from "path";
|
|
308
282
|
init_logger();
|
|
309
283
|
|
|
310
|
-
// src/
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
284
|
+
// src/epic/lifecycle.ts
|
|
285
|
+
init_zod();
|
|
286
|
+
import { createHash as createHash2 } from "crypto";
|
|
287
|
+
import * as fs from "fs";
|
|
288
|
+
import * as path from "path";
|
|
289
|
+
init_logger();
|
|
290
|
+
|
|
291
|
+
// src/epic/config-gate.ts
|
|
292
|
+
init_loader();
|
|
293
|
+
init_config();
|
|
294
|
+
init_config();
|
|
295
|
+
var EPIC_MODE_CONFIG_DISABLED_MESSAGE = "Epic Mode is disabled by config. Set `epic.mode.enabled: true` in " + ".opencode/opencode-swarm.json (or your user config) to opt in, then retry. " + 'Example: `{ "epic": { "mode": { "enabled": true } } }` (no `turbo` block ' + "is needed).";
|
|
296
|
+
var _internals2 = {
|
|
297
|
+
loadPluginConfigWithMeta
|
|
298
|
+
};
|
|
299
|
+
function isEpicModeConfigEnabledForDirectory(directory) {
|
|
300
|
+
try {
|
|
301
|
+
return isEpicModeConfigEnabled(_internals2.loadPluginConfigWithMeta(directory).config);
|
|
302
|
+
} catch {
|
|
323
303
|
return false;
|
|
324
|
-
for (const side of value.split("..")) {
|
|
325
|
-
if (side.startsWith("-"))
|
|
326
|
-
return false;
|
|
327
|
-
}
|
|
328
|
-
return true;
|
|
329
|
-
}
|
|
330
|
-
function assertSafeGitRefArg(value, context) {
|
|
331
|
-
if (!isSafeGitRefArg(value)) {
|
|
332
|
-
throw new UnsafeGitRefError(value, context);
|
|
333
304
|
}
|
|
334
|
-
return value;
|
|
335
305
|
}
|
|
336
306
|
|
|
337
|
-
// src/
|
|
338
|
-
var
|
|
339
|
-
var
|
|
340
|
-
var
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
307
|
+
// src/epic/lifecycle.ts
|
|
308
|
+
var EPIC_LIFECYCLE_NAMESPACE = "turbo.epic.lifecycle";
|
|
309
|
+
var EPIC_SENTINEL_RELATIVE_PATH = path.join(".swarm", "epic", "epic.json");
|
|
310
|
+
var PLAN_LEDGER_RELATIVE_PATH = path.join(".swarm", "plan-ledger.jsonl");
|
|
311
|
+
var PLAN_JSON_RELATIVE_PATH = path.join(".swarm", "plan.json");
|
|
312
|
+
var MAX_PLAN_JSON_BYTES = 8 * 1024 * 1024;
|
|
313
|
+
var MAX_LEDGER_ROOT_LINE_BYTES = 1024 * 1024;
|
|
314
|
+
var MAX_SENTINEL_BYTES = 64 * 1024;
|
|
315
|
+
var LIFECYCLE_ROW_LIST_LIMIT = 8;
|
|
316
|
+
class EpicStateUnreadableError extends Error {
|
|
317
|
+
constructor(message) {
|
|
318
|
+
super(message);
|
|
319
|
+
this.name = "EpicStateUnreadableError";
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
var sizingSchema = exports_external.object({
|
|
323
|
+
epicSized: exports_external.boolean(),
|
|
324
|
+
reasons: exports_external.array(exports_external.string())
|
|
325
|
+
}).passthrough();
|
|
326
|
+
var waveSchema = exports_external.object({
|
|
327
|
+
seq: exports_external.number().int().min(1),
|
|
328
|
+
phase: exports_external.number().int(),
|
|
329
|
+
kind: exports_external.enum(["parallel", "exclusive", "serial-component"]),
|
|
330
|
+
taskIds: exports_external.array(exports_external.string()),
|
|
331
|
+
files: exports_external.record(exports_external.string(), exports_external.array(exports_external.string())),
|
|
332
|
+
cochange: exports_external.object({
|
|
333
|
+
pairs: exports_external.array(exports_external.object({
|
|
334
|
+
fileA: exports_external.string(),
|
|
335
|
+
fileB: exports_external.string(),
|
|
336
|
+
npmi: exports_external.number(),
|
|
337
|
+
coChangeCount: exports_external.number()
|
|
338
|
+
}).passthrough()),
|
|
339
|
+
threshold: exports_external.object({ npmi: exports_external.number(), minCoChanges: exports_external.number() })
|
|
340
|
+
}).nullable(),
|
|
341
|
+
components: exports_external.object({
|
|
342
|
+
byTask: exports_external.record(exports_external.string(), exports_external.string()),
|
|
343
|
+
modes: exports_external.record(exports_external.string(), exports_external.enum(["exclusive", "serial-component", "parallel"])),
|
|
344
|
+
density: exports_external.record(exports_external.string(), exports_external.number()),
|
|
345
|
+
exclusive: exports_external.record(exports_external.string(), exports_external.enum([
|
|
346
|
+
"global-file",
|
|
347
|
+
"protected-path",
|
|
348
|
+
"no-scope",
|
|
349
|
+
"hot-file",
|
|
350
|
+
"hot-module"
|
|
351
|
+
]).transform((reason) => reason === "hot-module" ? "hot-file" : reason)),
|
|
352
|
+
threshold: exports_external.number(),
|
|
353
|
+
truncated: exports_external.boolean()
|
|
354
|
+
}).passthrough().optional(),
|
|
355
|
+
baseHead: exports_external.string().nullable(),
|
|
356
|
+
issuedAt: exports_external.string(),
|
|
357
|
+
closedAt: exports_external.string().optional(),
|
|
358
|
+
closeHead: exports_external.string().nullable().optional(),
|
|
359
|
+
status: exports_external.enum(["issued", "closed", "aborted"]),
|
|
360
|
+
abortReason: exports_external.string().optional(),
|
|
361
|
+
mergeFailures: exports_external.record(exports_external.string(), exports_external.object({
|
|
362
|
+
outcome: exports_external.string(),
|
|
363
|
+
stage: exports_external.string(),
|
|
364
|
+
message: exports_external.string(),
|
|
365
|
+
at: exports_external.number().nullable()
|
|
366
|
+
}).passthrough()).optional(),
|
|
367
|
+
undeclared: exports_external.array(exports_external.string()).optional()
|
|
368
|
+
}).passthrough();
|
|
369
|
+
var taskOutcomeSchema = exports_external.object({
|
|
370
|
+
taskId: exports_external.string(),
|
|
371
|
+
phase: exports_external.number().int(),
|
|
372
|
+
waveSeq: exports_external.number().int(),
|
|
373
|
+
resolution: exports_external.enum(["completed", "closed", "removed"]),
|
|
374
|
+
resolvedAt: exports_external.string(),
|
|
375
|
+
generation: exports_external.number(),
|
|
376
|
+
stageAFailures: exports_external.number(),
|
|
377
|
+
stageBFailures: exports_external.number(),
|
|
378
|
+
mergeFailure: exports_external.object({
|
|
379
|
+
outcome: exports_external.string(),
|
|
380
|
+
stage: exports_external.string()
|
|
381
|
+
}).passthrough().nullable(),
|
|
382
|
+
declared: exports_external.array(exports_external.string()),
|
|
383
|
+
undeclared: exports_external.array(exports_external.string()),
|
|
384
|
+
attribution: exports_external.enum([
|
|
385
|
+
"session",
|
|
386
|
+
"git-single-task",
|
|
387
|
+
"unavailable",
|
|
388
|
+
"no-git"
|
|
389
|
+
]),
|
|
390
|
+
reopened: exports_external.number(),
|
|
391
|
+
previous: exports_external.object({
|
|
392
|
+
waveSeq: exports_external.number(),
|
|
393
|
+
generation: exports_external.number(),
|
|
394
|
+
stageBFailures: exports_external.number(),
|
|
395
|
+
reopened: exports_external.number()
|
|
396
|
+
}).optional(),
|
|
397
|
+
marker: exports_external.object({
|
|
398
|
+
ref: exports_external.string().nullable(),
|
|
399
|
+
sha: exports_external.string().nullable(),
|
|
400
|
+
provenance: exports_external.enum([
|
|
401
|
+
"landing-commit",
|
|
402
|
+
"wave-close-head",
|
|
403
|
+
"repaired",
|
|
404
|
+
"no-git"
|
|
405
|
+
])
|
|
406
|
+
}).nullable()
|
|
407
|
+
}).passthrough();
|
|
408
|
+
var phaseRecordSchema = exports_external.object({
|
|
409
|
+
status: exports_external.enum(["active", "review", "complete"]),
|
|
410
|
+
completeAtStart: exports_external.boolean().optional(),
|
|
411
|
+
reviewRuns: exports_external.number().int().min(0),
|
|
412
|
+
verdicts: exports_external.array(exports_external.string())
|
|
413
|
+
}).passthrough();
|
|
414
|
+
var recordSchema = exports_external.object({
|
|
415
|
+
schema: exports_external.literal("epic-record-v1"),
|
|
416
|
+
epicKey: exports_external.string().min(1),
|
|
417
|
+
token: exports_external.string().min(1),
|
|
418
|
+
planId: exports_external.string().min(1),
|
|
419
|
+
planIdentityHash: exports_external.string().min(1),
|
|
420
|
+
planEpoch: exports_external.string().nullable(),
|
|
421
|
+
planKey: exports_external.string().min(1),
|
|
422
|
+
ledgerRootDigest: exports_external.string().nullable(),
|
|
423
|
+
status: exports_external.enum(["open", "closing"]),
|
|
424
|
+
startedAt: exports_external.string().min(1),
|
|
425
|
+
startedBySession: exports_external.string(),
|
|
426
|
+
forced: exports_external.boolean(),
|
|
427
|
+
structureHashAtStart: exports_external.string(),
|
|
428
|
+
config: exports_external.object({
|
|
429
|
+
commitPolicy: exports_external.enum(["epic-branch", "current-branch"]),
|
|
430
|
+
isolation: exports_external.enum(["worktree", "main-tree-nogit"]),
|
|
431
|
+
maxParallel: exports_external.number().int().min(1)
|
|
432
|
+
}).passthrough(),
|
|
433
|
+
git: exports_external.object({
|
|
434
|
+
isRepo: exports_external.boolean(),
|
|
435
|
+
baseCommit: exports_external.string().nullable(),
|
|
436
|
+
originalBranch: exports_external.string().nullable(),
|
|
437
|
+
epicBranch: exports_external.string().nullable().default(null)
|
|
438
|
+
}).passthrough(),
|
|
439
|
+
sizing: sizingSchema,
|
|
440
|
+
priorDigest: exports_external.string().nullable().default(null),
|
|
441
|
+
closing: exports_external.object({
|
|
442
|
+
requestedAt: exports_external.string(),
|
|
443
|
+
outcome: exports_external.enum(["completed", "abandoned", "abandoned-by-swarm-close"]),
|
|
444
|
+
land: exports_external.enum(["squash", "merge", "none"]).nullable().default(null),
|
|
445
|
+
lastLandingAttempt: exports_external.object({
|
|
446
|
+
mode: exports_external.enum(["squash", "merge", "none"]),
|
|
447
|
+
status: exports_external.enum(["conflict", "failed"]),
|
|
448
|
+
at: exports_external.string(),
|
|
449
|
+
conflictFiles: exports_external.array(exports_external.string()),
|
|
450
|
+
detail: exports_external.string()
|
|
451
|
+
}).nullable().default(null)
|
|
452
|
+
}).nullable(),
|
|
453
|
+
waves: exports_external.array(waveSchema).default([]),
|
|
454
|
+
activeWaveSeq: exports_external.number().int().nullable().default(null),
|
|
455
|
+
tasks: exports_external.record(exports_external.string(), taskOutcomeSchema).default({}),
|
|
456
|
+
phases: exports_external.record(exports_external.string(), phaseRecordSchema).default({})
|
|
457
|
+
}).passthrough();
|
|
458
|
+
var sentinelSchema = exports_external.object({
|
|
459
|
+
schema: exports_external.literal("epic-sentinel-v1"),
|
|
460
|
+
epicKey: exports_external.string().min(1),
|
|
461
|
+
token: exports_external.string().min(1),
|
|
462
|
+
planId: exports_external.string(),
|
|
463
|
+
startedAt: exports_external.string()
|
|
464
|
+
}).passthrough();
|
|
465
|
+
var _internals3 = {
|
|
466
|
+
isEpicModeConfigEnabledForDirectory,
|
|
467
|
+
now: () => Date.now(),
|
|
468
|
+
warnedDirectories: new Set
|
|
359
469
|
};
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
470
|
+
var MAX_WARNED_DIRECTORIES = 64;
|
|
471
|
+
function hasTraversalSegment(directory) {
|
|
472
|
+
return directory.split(/[\\/]/).includes("..");
|
|
473
|
+
}
|
|
474
|
+
function epicSentinelPath(directory) {
|
|
475
|
+
return path.join(directory, EPIC_SENTINEL_RELATIVE_PATH);
|
|
476
|
+
}
|
|
477
|
+
function epicSentinelExists(directory) {
|
|
478
|
+
if (typeof directory !== "string" || !directory)
|
|
479
|
+
return false;
|
|
480
|
+
if (hasTraversalSegment(directory))
|
|
481
|
+
return false;
|
|
482
|
+
return fs.existsSync(epicSentinelPath(directory));
|
|
483
|
+
}
|
|
484
|
+
function computeEpicKey(planId, planKey) {
|
|
485
|
+
const safeId = planId.replace(/[^A-Za-z0-9_-]/g, "_").replace(/^[-_.]+/, "").slice(0, 40);
|
|
486
|
+
return `${safeId.length > 0 ? safeId : "plan"}-${planKey.slice(0, 12)}`;
|
|
487
|
+
}
|
|
488
|
+
function planIdentityOf(plan) {
|
|
489
|
+
return {
|
|
490
|
+
planId: derivePlanId(plan),
|
|
491
|
+
planIdentityHash: derivePlanIdentityHash(plan)
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function readBoundedUtf8(filePath, maxBytes) {
|
|
495
|
+
let fd = null;
|
|
363
496
|
try {
|
|
364
|
-
|
|
497
|
+
fd = fs.openSync(filePath, "r");
|
|
498
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
499
|
+
const read = fs.readSync(fd, buffer, 0, maxBytes, 0);
|
|
500
|
+
return buffer.subarray(0, read).toString("utf-8");
|
|
365
501
|
} catch {
|
|
366
|
-
return
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
continue;
|
|
374
|
-
const eqIdx = line.indexOf("=");
|
|
375
|
-
if (eqIdx < 0)
|
|
376
|
-
continue;
|
|
377
|
-
const k = line.slice(0, eqIdx);
|
|
378
|
-
const v = line.slice(eqIdx + 1);
|
|
379
|
-
if (!isValidEnvKey(k))
|
|
380
|
-
continue;
|
|
381
|
-
if (isUntrustedEnvKey(k))
|
|
382
|
-
continue;
|
|
383
|
-
result[k] = v;
|
|
384
|
-
}
|
|
385
|
-
return result;
|
|
386
|
-
}
|
|
387
|
-
function gitBinaryMissingMessage(cwd) {
|
|
388
|
-
const resolution = describeGitResolution();
|
|
389
|
-
const lines = [
|
|
390
|
-
`git executable is not available: spawning "${resolution.resolvedPath ?? "git"}" failed, and the working directory ${cwd} exists and is a directory.`
|
|
391
|
-
];
|
|
392
|
-
if (resolution.attempts.length > 0) {
|
|
393
|
-
lines.push("Candidates tried:");
|
|
394
|
-
const shown = resolution.attempts.slice(0, MAX_RENDERED_GIT_CANDIDATES);
|
|
395
|
-
for (const attempt of shown) {
|
|
396
|
-
lines.push(` - [${attempt.source}] ${attempt.candidate}: ${attempt.accepted ? "accepted" : attempt.reason ?? "rejected"}`);
|
|
397
|
-
}
|
|
398
|
-
const omitted = resolution.attempts.length - shown.length;
|
|
399
|
-
if (omitted > 0)
|
|
400
|
-
lines.push(` ... and ${omitted} more`);
|
|
401
|
-
} else {
|
|
402
|
-
lines.push("No candidate probe results are recorded for this process (the resolver was pre-seeded or has not probed yet).");
|
|
502
|
+
return null;
|
|
503
|
+
} finally {
|
|
504
|
+
if (fd !== null) {
|
|
505
|
+
try {
|
|
506
|
+
fs.closeSync(fd);
|
|
507
|
+
} catch {}
|
|
508
|
+
}
|
|
403
509
|
}
|
|
404
|
-
|
|
405
|
-
|
|
510
|
+
}
|
|
511
|
+
function readLedgerRootDigest(directory) {
|
|
512
|
+
const ledgerPath = path.join(directory, PLAN_LEDGER_RELATIVE_PATH);
|
|
513
|
+
if (!fs.existsSync(ledgerPath))
|
|
514
|
+
return null;
|
|
515
|
+
const head = readBoundedUtf8(ledgerPath, MAX_LEDGER_ROOT_LINE_BYTES);
|
|
516
|
+
if (head === null)
|
|
517
|
+
return "unreadable";
|
|
518
|
+
const newline = head.indexOf(`
|
|
406
519
|
`);
|
|
520
|
+
const rootLine = (newline >= 0 ? head.slice(0, newline) : head).replace(/\r$/, "");
|
|
521
|
+
if (rootLine.trim().length === 0)
|
|
522
|
+
return null;
|
|
523
|
+
return createHash2("sha256").update(rootLine, "utf8").digest("hex");
|
|
407
524
|
}
|
|
408
|
-
function
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
timeout: GIT_TIMEOUT_MS,
|
|
418
|
-
windowsHide: true,
|
|
419
|
-
maxBuffer: GIT_MAX_BUFFER_BYTES,
|
|
420
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
421
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
422
|
-
envOverrides: resolvedLaneEnv
|
|
423
|
-
});
|
|
424
|
-
if (!result.error && result.status === 0) {
|
|
425
|
-
return result.stdout;
|
|
426
|
-
}
|
|
427
|
-
if (result.error) {
|
|
428
|
-
if (isSpawnCwdMissing(result.error, cwd)) {
|
|
429
|
-
throw new GitSpawnCwdError(cwd, "missing", { cause: result.error });
|
|
430
|
-
}
|
|
431
|
-
if (isSpawnCwdUnreadable(result.error, cwd)) {
|
|
432
|
-
throw new GitSpawnCwdError(cwd, "unreadable", {
|
|
433
|
-
cause: result.error
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
if (isGitBinaryMissing(result.error, cwd)) {
|
|
437
|
-
throw new GitBinaryMissingError(gitBinaryMissingMessage(cwd), {
|
|
438
|
-
cause: result.error
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
if (isTransientSpawnError(result.error) && attempt < MAX_TRANSIENT_RETRIES - 1) {
|
|
442
|
-
transientBackoff(attempt);
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
445
|
-
throw new Error(errorMessage(result.error));
|
|
446
|
-
}
|
|
447
|
-
if (result.status !== 0) {
|
|
448
|
-
throw new Error(result.stderr || result.stdout || `git exited with ${result.status}`);
|
|
525
|
+
function readCurrentPlanIdentity(directory) {
|
|
526
|
+
const planPath = path.join(directory, PLAN_JSON_RELATIVE_PATH);
|
|
527
|
+
try {
|
|
528
|
+
const stat = fs.statSync(planPath);
|
|
529
|
+
if (!stat.isFile() || stat.size > MAX_PLAN_JSON_BYTES)
|
|
530
|
+
return null;
|
|
531
|
+
const parsed = JSON.parse(fs.readFileSync(planPath, "utf-8"));
|
|
532
|
+
if (typeof parsed !== "object" || parsed === null || typeof parsed.swarm !== "string" || typeof parsed.title !== "string") {
|
|
533
|
+
return null;
|
|
449
534
|
}
|
|
535
|
+
return planIdentityOf(parsed);
|
|
536
|
+
} catch {
|
|
537
|
+
return null;
|
|
450
538
|
}
|
|
451
|
-
throw new GitBinaryMissingError(process.platform === "win32" ? "git executable is not available on PATH or common Windows install locations" : "git executable is not available on PATH");
|
|
452
539
|
}
|
|
453
|
-
function
|
|
540
|
+
function parseEpicRecord(payload) {
|
|
541
|
+
let raw;
|
|
454
542
|
try {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if (err instanceof GitBinaryMissingError) {
|
|
459
|
-
return {
|
|
460
|
-
isRepo: false,
|
|
461
|
-
reason: "git_unavailable",
|
|
462
|
-
message: err.message
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
const message = errorMessage(err);
|
|
466
|
-
return {
|
|
467
|
-
isRepo: false,
|
|
468
|
-
reason: isNotGitRepositoryMessage(message) ? "not_git_repo" : "git_error",
|
|
469
|
-
message
|
|
470
|
-
};
|
|
543
|
+
raw = JSON.parse(payload);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
throw new EpicStateUnreadableError(`Epic lifecycle row is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
471
546
|
}
|
|
547
|
+
const parsed = recordSchema.safeParse(raw);
|
|
548
|
+
if (!parsed.success) {
|
|
549
|
+
throw new EpicStateUnreadableError(`Epic lifecycle row has an unexpected shape: ${parsed.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`);
|
|
550
|
+
}
|
|
551
|
+
const { lastDecision: _retired, ...record } = parsed.data;
|
|
552
|
+
return record;
|
|
472
553
|
}
|
|
473
|
-
function
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const output = gitExec(["rev-parse", "--abbrev-ref", "HEAD"], cwd, laneEnv, laneIndex);
|
|
478
|
-
return output.trim();
|
|
479
|
-
}
|
|
480
|
-
function getDefaultBaseBranch(cwd) {
|
|
554
|
+
function readEpicSentinel(directory) {
|
|
555
|
+
const text = readBoundedUtf8(epicSentinelPath(directory), MAX_SENTINEL_BYTES);
|
|
556
|
+
if (text === null)
|
|
557
|
+
return null;
|
|
481
558
|
try {
|
|
482
|
-
|
|
483
|
-
return
|
|
559
|
+
const parsed = sentinelSchema.safeParse(JSON.parse(text));
|
|
560
|
+
return parsed.success ? parsed.data : null;
|
|
484
561
|
} catch {
|
|
485
|
-
|
|
486
|
-
gitExec(["rev-parse", "--verify", "origin/master"], cwd);
|
|
487
|
-
return "origin/master";
|
|
488
|
-
} catch {
|
|
489
|
-
return "origin/main";
|
|
490
|
-
}
|
|
562
|
+
return null;
|
|
491
563
|
}
|
|
492
564
|
}
|
|
493
|
-
function
|
|
494
|
-
|
|
495
|
-
|
|
565
|
+
function listLifecycleRows(directory) {
|
|
566
|
+
try {
|
|
567
|
+
return listCoordinationStates(directory, EPIC_LIFECYCLE_NAMESPACE, LIFECYCLE_ROW_LIST_LIMIT);
|
|
568
|
+
} catch (error) {
|
|
569
|
+
throw new EpicStateUnreadableError(`Epic lifecycle rows could not be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
570
|
+
}
|
|
496
571
|
}
|
|
497
|
-
function
|
|
572
|
+
function evaluateEpicIdentity(record, current, ledgerRootDigest) {
|
|
573
|
+
if (current === null)
|
|
574
|
+
return "plan-missing";
|
|
575
|
+
if (current.planIdentityHash !== record.planIdentityHash) {
|
|
576
|
+
return "plan-renamed-or-replaced";
|
|
577
|
+
}
|
|
578
|
+
if (ledgerRootDigest !== record.ledgerRootDigest) {
|
|
579
|
+
return "plan-ledger-replaced";
|
|
580
|
+
}
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
function inspectEpic(directory, plan) {
|
|
584
|
+
const sentinelPresent = epicSentinelExists(directory);
|
|
585
|
+
const sentinel = sentinelPresent ? readEpicSentinel(directory) : null;
|
|
586
|
+
const inspection = {
|
|
587
|
+
sentinelPresent,
|
|
588
|
+
sentinel,
|
|
589
|
+
record: null,
|
|
590
|
+
rowKeys: [],
|
|
591
|
+
unreadable: null,
|
|
592
|
+
orphanReason: null,
|
|
593
|
+
configEnabled: _internals3.isEpicModeConfigEnabledForDirectory(directory)
|
|
594
|
+
};
|
|
595
|
+
if (hasTraversalSegment(directory) || !projectDbExists(directory)) {
|
|
596
|
+
return inspection;
|
|
597
|
+
}
|
|
598
|
+
let rows;
|
|
498
599
|
try {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
600
|
+
rows = listLifecycleRows(directory);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
inspection.unreadable = error instanceof Error ? error.message : String(error);
|
|
603
|
+
try {
|
|
604
|
+
inspection.rowKeys = listLifecycleRowsRawKeys(directory);
|
|
605
|
+
} catch {}
|
|
606
|
+
return inspection;
|
|
607
|
+
}
|
|
608
|
+
inspection.rowKeys = rows.map((row) => row.entityKey);
|
|
609
|
+
if (rows.length === 0)
|
|
610
|
+
return inspection;
|
|
611
|
+
if (rows.length > 1) {
|
|
612
|
+
inspection.unreadable = `multiple Epic lifecycle rows present (${inspection.rowKeys.join(", ")})`;
|
|
613
|
+
return inspection;
|
|
614
|
+
}
|
|
505
615
|
try {
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
return branch;
|
|
616
|
+
const record = parseEpicRecord(rows[0].payload);
|
|
617
|
+
if (record.epicKey !== rows[0].entityKey) {
|
|
618
|
+
throw new EpicStateUnreadableError(`Epic lifecycle row key ${rows[0].entityKey} does not match its payload epicKey ${record.epicKey}`);
|
|
510
619
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
620
|
+
inspection.record = record;
|
|
621
|
+
inspection.orphanReason = evaluateEpicIdentity(record, plan ? planIdentityOf(plan) : readCurrentPlanIdentity(directory), readLedgerRootDigest(directory));
|
|
622
|
+
} catch (error) {
|
|
623
|
+
inspection.unreadable = error instanceof Error ? error.message : String(error);
|
|
624
|
+
}
|
|
625
|
+
return inspection;
|
|
626
|
+
}
|
|
627
|
+
function getOpenEpic(directory, plan) {
|
|
628
|
+
if (!epicSentinelExists(directory))
|
|
629
|
+
return null;
|
|
630
|
+
if (!projectDbExists(directory))
|
|
631
|
+
return null;
|
|
632
|
+
const rows = listLifecycleRows(directory);
|
|
633
|
+
if (rows.length === 0)
|
|
520
634
|
return null;
|
|
635
|
+
if (rows.length > 1) {
|
|
636
|
+
throw new EpicStateUnreadableError(`multiple Epic lifecycle rows present (${rows.map((row) => row.entityKey).join(", ")})`);
|
|
637
|
+
}
|
|
638
|
+
const record = parseEpicRecord(rows[0].payload);
|
|
639
|
+
if (record.epicKey !== rows[0].entityKey) {
|
|
640
|
+
throw new EpicStateUnreadableError(`Epic lifecycle row key ${rows[0].entityKey} does not match its payload epicKey ${record.epicKey}`);
|
|
641
|
+
}
|
|
642
|
+
if (record.status !== "open")
|
|
643
|
+
return null;
|
|
644
|
+
if (!_internals3.isEpicModeConfigEnabledForDirectory(directory))
|
|
645
|
+
return null;
|
|
646
|
+
const orphan = evaluateEpicIdentity(record, plan ? planIdentityOf(plan) : readCurrentPlanIdentity(directory), readLedgerRootDigest(directory));
|
|
647
|
+
return orphan === null ? record : null;
|
|
648
|
+
}
|
|
649
|
+
function warnUnreadableOnce(directory, message) {
|
|
650
|
+
const key = path.resolve(directory);
|
|
651
|
+
if (_internals3.warnedDirectories.has(key))
|
|
652
|
+
return;
|
|
653
|
+
if (_internals3.warnedDirectories.size >= MAX_WARNED_DIRECTORIES) {
|
|
654
|
+
const oldest = _internals3.warnedDirectories.values().next().value;
|
|
655
|
+
if (oldest !== undefined)
|
|
656
|
+
_internals3.warnedDirectories.delete(oldest);
|
|
521
657
|
}
|
|
658
|
+
_internals3.warnedDirectories.add(key);
|
|
659
|
+
criticalWarn(`[epic] Epic lifecycle state is unreadable for ${directory}: ${message}. Epic behaviour is OFF until repaired \u2014 run \`/swarm epic status\` (diagnose) or \`/swarm epic close --abandon\` (repair).`);
|
|
522
660
|
}
|
|
523
|
-
|
|
524
|
-
const warnings = [];
|
|
525
|
-
const prunedBranches = [];
|
|
661
|
+
function isEpicOpenForProject(directory) {
|
|
526
662
|
try {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
663
|
+
return getOpenEpic(directory) !== null;
|
|
664
|
+
} catch (error) {
|
|
665
|
+
warnUnreadableOnce(directory, error instanceof Error ? error.message : String(error));
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function writeSentinelFile(directory, sentinel) {
|
|
670
|
+
const target = epicSentinelPath(directory);
|
|
671
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
672
|
+
atomicWriteSwarmFileSync(target, `${JSON.stringify(sentinel, null, 2)}
|
|
673
|
+
`);
|
|
674
|
+
}
|
|
675
|
+
function sentinelFor(record) {
|
|
676
|
+
return {
|
|
677
|
+
schema: "epic-sentinel-v1",
|
|
678
|
+
epicKey: record.epicKey,
|
|
679
|
+
token: record.token,
|
|
680
|
+
planId: record.planId,
|
|
681
|
+
startedAt: record.startedAt
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function unlinkSentinel(directory) {
|
|
685
|
+
try {
|
|
686
|
+
fs.unlinkSync(epicSentinelPath(directory));
|
|
687
|
+
} catch (error) {
|
|
688
|
+
if (error.code !== "ENOENT")
|
|
689
|
+
throw error;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function deleteSentinelIfMatches(directory, epicKey, token) {
|
|
693
|
+
const current = readEpicSentinel(directory);
|
|
694
|
+
const present = epicSentinelExists(directory);
|
|
695
|
+
if (!present)
|
|
696
|
+
return false;
|
|
697
|
+
if (current !== null) {
|
|
698
|
+
if (current.epicKey !== epicKey)
|
|
699
|
+
return false;
|
|
700
|
+
if (token !== null && current.token !== token)
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
unlinkSentinel(directory);
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
function createEpicRecord(directory, record) {
|
|
707
|
+
return withCoordinationTransaction(directory, () => {
|
|
708
|
+
const existing = listLifecycleRowsRawKeys(directory);
|
|
709
|
+
if (existing.length > 0) {
|
|
710
|
+
return { outcome: "exists", existingKeys: existing };
|
|
711
|
+
}
|
|
712
|
+
const result = transitionCoordinationStateWithinTransaction(directory, {
|
|
713
|
+
namespace: EPIC_LIFECYCLE_NAMESPACE,
|
|
714
|
+
entityKey: record.epicKey,
|
|
715
|
+
expectedRevision: null,
|
|
716
|
+
generation: 1,
|
|
717
|
+
status: record.status,
|
|
718
|
+
payload: JSON.stringify(record)
|
|
719
|
+
});
|
|
720
|
+
if (result.outcome !== "applied") {
|
|
721
|
+
return { outcome: "exists", existingKeys: [record.epicKey] };
|
|
539
722
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
723
|
+
writeSentinelFile(directory, sentinelFor(record));
|
|
724
|
+
return { outcome: "created", record };
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
function listLifecycleRowsRawKeys(directory) {
|
|
728
|
+
return listCoordinationStateKeys(directory, EPIC_LIFECYCLE_NAMESPACE, LIFECYCLE_ROW_LIST_LIMIT);
|
|
729
|
+
}
|
|
730
|
+
var MAX_RECORD_UPDATE_ATTEMPTS = 5;
|
|
731
|
+
|
|
732
|
+
class EpicRecordTooLargeError extends Error {
|
|
733
|
+
constructor(epicKey) {
|
|
734
|
+
super(`Epic \`${epicKey}\`'s lifecycle row is near its size limit (over ${MAX_EPIC_RECORD_PAYLOAD_CHARS} serialized characters), so no further wave can be recorded. Close the epic (\`/swarm epic close\`) to land or abandon it, then start a new epic for the remaining work.`);
|
|
735
|
+
this.name = "EpicRecordTooLargeError";
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
var MAX_EPIC_RECORD_PAYLOAD_CHARS = 786432;
|
|
739
|
+
function epicRecordPayloadTooLarge(record) {
|
|
740
|
+
return JSON.stringify(record).length > MAX_EPIC_RECORD_PAYLOAD_CHARS;
|
|
741
|
+
}
|
|
742
|
+
function updateEpicRecord(directory, epicKey, mutate, expectedToken = null) {
|
|
743
|
+
for (let attempt = 0;attempt < MAX_RECORD_UPDATE_ATTEMPTS; attempt += 1) {
|
|
744
|
+
const current = getCoordinationStateRaw(directory, EPIC_LIFECYCLE_NAMESPACE, epicKey);
|
|
745
|
+
if (!current)
|
|
746
|
+
return null;
|
|
747
|
+
const record = parseEpicRecord(current.payload);
|
|
748
|
+
if (expectedToken !== null && record.token !== expectedToken)
|
|
749
|
+
return null;
|
|
750
|
+
const next = mutate(record);
|
|
751
|
+
if (epicRecordPayloadTooLarge(next)) {
|
|
752
|
+
throw new EpicRecordTooLargeError(epicKey);
|
|
553
753
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
754
|
+
const result = transitionCoordinationState(directory, {
|
|
755
|
+
namespace: EPIC_LIFECYCLE_NAMESPACE,
|
|
756
|
+
entityKey: epicKey,
|
|
757
|
+
expectedRevision: current.revision,
|
|
758
|
+
generation: current.generation + 1,
|
|
759
|
+
status: next.status,
|
|
760
|
+
payload: JSON.stringify(next)
|
|
761
|
+
});
|
|
762
|
+
if (result.outcome === "applied")
|
|
763
|
+
return next;
|
|
764
|
+
if (result.outcome !== "revision_conflict" && result.outcome !== "stale_generation") {
|
|
765
|
+
throw new Error(`Epic record update failed: ${result.outcome}`);
|
|
564
766
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
767
|
+
}
|
|
768
|
+
throw new Error(`Epic record update failed: contention on ${epicKey}`);
|
|
769
|
+
}
|
|
770
|
+
function readPlanTaskRef(directory, taskId) {
|
|
771
|
+
const planPath = path.join(directory, PLAN_JSON_RELATIVE_PATH);
|
|
772
|
+
try {
|
|
773
|
+
const stat = fs.statSync(planPath);
|
|
774
|
+
if (!stat.isFile() || stat.size > MAX_PLAN_JSON_BYTES)
|
|
775
|
+
return null;
|
|
776
|
+
const parsed = JSON.parse(fs.readFileSync(planPath, "utf-8"));
|
|
777
|
+
if (!Array.isArray(parsed?.phases))
|
|
778
|
+
return null;
|
|
779
|
+
for (const phase of parsed.phases) {
|
|
780
|
+
if (!phase || !Array.isArray(phase.tasks))
|
|
781
|
+
continue;
|
|
782
|
+
for (const task of phase.tasks) {
|
|
783
|
+
if (task?.id !== taskId)
|
|
784
|
+
continue;
|
|
568
785
|
return {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
alreadyAligned: false,
|
|
574
|
-
prunedBranches: [],
|
|
575
|
-
warnings: []
|
|
786
|
+
id: taskId,
|
|
787
|
+
phase: typeof phase.id === "number" ? phase.id : 0,
|
|
788
|
+
description: typeof task.description === "string" ? task.description : undefined,
|
|
789
|
+
files: Array.isArray(task.files_touched) ? task.files_touched.filter((f) => typeof f === "string") : []
|
|
576
790
|
};
|
|
577
791
|
}
|
|
578
|
-
} catch {}
|
|
579
|
-
try {
|
|
580
|
-
gitExec(["fetch", "--prune", "origin"], cwd);
|
|
581
|
-
} catch (err) {
|
|
582
|
-
return {
|
|
583
|
-
success: false,
|
|
584
|
-
targetBranch,
|
|
585
|
-
localBranch: currentBranch,
|
|
586
|
-
message: `Fetch failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
587
|
-
alreadyAligned: false,
|
|
588
|
-
prunedBranches: [],
|
|
589
|
-
warnings: []
|
|
590
|
-
};
|
|
591
792
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
gitExec(["checkout", currentBranch], cwd);
|
|
607
|
-
} catch (err) {
|
|
608
|
-
return {
|
|
609
|
-
success: false,
|
|
610
|
-
targetBranch,
|
|
611
|
-
localBranch: currentBranch,
|
|
612
|
-
message: `Checkout failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
613
|
-
alreadyAligned: false,
|
|
614
|
-
prunedBranches: [],
|
|
615
|
-
warnings: []
|
|
616
|
-
};
|
|
793
|
+
return null;
|
|
794
|
+
} catch {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function markEpicClosing(directory, epicKey, outcome, expectedToken = null, land = null) {
|
|
799
|
+
return updateEpicRecord(directory, epicKey, (record) => record.status === "closing" && record.closing ? resumeClosing(record, record.closing, outcome, land) : {
|
|
800
|
+
...record,
|
|
801
|
+
status: "closing",
|
|
802
|
+
closing: {
|
|
803
|
+
requestedAt: new Date(_internals3.now()).toISOString(),
|
|
804
|
+
outcome,
|
|
805
|
+
land,
|
|
806
|
+
lastLandingAttempt: null
|
|
617
807
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
808
|
+
}, expectedToken);
|
|
809
|
+
}
|
|
810
|
+
function resumeClosing(record, closing, outcome, land) {
|
|
811
|
+
const nextOutcome = closing.outcome === "completed" && outcome !== "completed" ? outcome : closing.outcome;
|
|
812
|
+
if (closing.land === land && closing.outcome === nextOutcome)
|
|
813
|
+
return record;
|
|
814
|
+
return { ...record, closing: { ...closing, land, outcome: nextOutcome } };
|
|
815
|
+
}
|
|
816
|
+
function recordEpicBranch(directory, epicKey, expectedToken, epicBranch) {
|
|
817
|
+
return updateEpicRecord(directory, epicKey, (record) => ({ ...record, git: { ...record.git, epicBranch } }), expectedToken);
|
|
818
|
+
}
|
|
819
|
+
function recordEpicLandingAttempt(directory, epicKey, expectedToken, attempt) {
|
|
820
|
+
return updateEpicRecord(directory, epicKey, (record) => record.closing ? {
|
|
821
|
+
...record,
|
|
822
|
+
closing: { ...record.closing, lastLandingAttempt: attempt }
|
|
823
|
+
} : record, expectedToken);
|
|
824
|
+
}
|
|
825
|
+
function payloadToken(payload) {
|
|
826
|
+
try {
|
|
827
|
+
const parsed = JSON.parse(payload);
|
|
828
|
+
return typeof parsed?.token === "string" ? parsed.token : null;
|
|
829
|
+
} catch {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function deleteEpicState(directory, epicKey, token) {
|
|
834
|
+
const result = { rowsDeleted: [], sentinelDeleted: false };
|
|
835
|
+
if (!projectDbExists(directory)) {
|
|
836
|
+
const sentinel = readEpicSentinel(directory);
|
|
837
|
+
result.sentinelDeleted = deleteSentinelIfMatches(directory, epicKey ?? sentinel?.epicKey ?? "", epicKey === null ? null : token);
|
|
838
|
+
return result;
|
|
839
|
+
}
|
|
840
|
+
return withCoordinationTransaction(directory, () => {
|
|
841
|
+
const keys = epicKey === null ? listLifecycleRowsRawKeys(directory) : [epicKey];
|
|
842
|
+
for (const key of keys) {
|
|
843
|
+
const raw = getCoordinationStateRaw(directory, EPIC_LIFECYCLE_NAMESPACE, key);
|
|
844
|
+
if (!raw)
|
|
845
|
+
continue;
|
|
846
|
+
if (epicKey !== null && token !== null && payloadToken(raw.payload) !== token) {
|
|
847
|
+
continue;
|
|
623
848
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
resetSucceeded = true;
|
|
627
|
-
break;
|
|
628
|
-
} catch (err) {
|
|
629
|
-
lastError = err;
|
|
849
|
+
if (deleteCoordinationStateWithinTransaction(directory, EPIC_LIFECYCLE_NAMESPACE, key, raw.revision)) {
|
|
850
|
+
result.rowsDeleted.push(key);
|
|
630
851
|
}
|
|
631
852
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
message: `Reset failed: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
|
|
638
|
-
alreadyAligned: false,
|
|
639
|
-
prunedBranches: [],
|
|
640
|
-
warnings: []
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
if (options?.pruneBranches) {
|
|
644
|
-
try {
|
|
645
|
-
const mergedOutput = gitExec(["branch", "--merged", targetBranch], cwd);
|
|
646
|
-
const mergedLines = mergedOutput.split(`
|
|
647
|
-
`);
|
|
648
|
-
for (const line of mergedLines) {
|
|
649
|
-
const trimmedLine = line.trim();
|
|
650
|
-
if (!trimmedLine || trimmedLine.startsWith("*")) {
|
|
651
|
-
continue;
|
|
652
|
-
}
|
|
653
|
-
try {
|
|
654
|
-
gitExec([
|
|
655
|
-
"branch",
|
|
656
|
-
"-d",
|
|
657
|
-
"--",
|
|
658
|
-
assertSafeGitRefArg(trimmedLine, "resetToRemoteBranch prune merged branch")
|
|
659
|
-
], cwd);
|
|
660
|
-
prunedBranches.push(trimmedLine);
|
|
661
|
-
} catch {
|
|
662
|
-
warnings.push(`Could not safely delete branch: ${trimmedLine}`);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
} catch (err) {
|
|
666
|
-
warnings.push(`Failed to get merged branches: ${err instanceof Error ? err.message : String(err)}`);
|
|
667
|
-
}
|
|
668
|
-
try {
|
|
669
|
-
const branchVvOutput = gitExec(["branch", "-vv"], cwd);
|
|
670
|
-
const vvLines = branchVvOutput.split(`
|
|
671
|
-
`);
|
|
672
|
-
for (const line of vvLines) {
|
|
673
|
-
const trimmedLine = line.trim();
|
|
674
|
-
if (!trimmedLine || trimmedLine.startsWith("*")) {
|
|
675
|
-
continue;
|
|
676
|
-
}
|
|
677
|
-
if (trimmedLine.includes(": gone]")) {
|
|
678
|
-
const parts = trimmedLine.split(/\s+/);
|
|
679
|
-
const branchName = parts[0];
|
|
680
|
-
try {
|
|
681
|
-
gitExec([
|
|
682
|
-
"branch",
|
|
683
|
-
"-d",
|
|
684
|
-
"--",
|
|
685
|
-
assertSafeGitRefArg(branchName, "resetToRemoteBranch prune gone branch")
|
|
686
|
-
], cwd);
|
|
687
|
-
prunedBranches.push(branchName);
|
|
688
|
-
} catch {
|
|
689
|
-
warnings.push(`Could not delete gone branch: ${branchName}`);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
} catch (err) {
|
|
694
|
-
warnings.push(`Failed to prune gone branches: ${err instanceof Error ? err.message : String(err)}`);
|
|
695
|
-
}
|
|
853
|
+
const sentinel = readEpicSentinel(directory);
|
|
854
|
+
if (epicKey === null) {
|
|
855
|
+
result.sentinelDeleted = deleteSentinelIfMatches(directory, sentinel?.epicKey ?? "", null);
|
|
856
|
+
} else {
|
|
857
|
+
result.sentinelDeleted = deleteSentinelIfMatches(directory, epicKey, token);
|
|
696
858
|
}
|
|
697
|
-
return
|
|
698
|
-
|
|
699
|
-
targetBranch,
|
|
700
|
-
localBranch: currentBranch,
|
|
701
|
-
message: "Successfully reset to remote branch",
|
|
702
|
-
alreadyAligned: false,
|
|
703
|
-
prunedBranches,
|
|
704
|
-
warnings
|
|
705
|
-
};
|
|
706
|
-
} catch (err) {
|
|
707
|
-
return {
|
|
708
|
-
success: false,
|
|
709
|
-
targetBranch: "",
|
|
710
|
-
localBranch: "",
|
|
711
|
-
message: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,
|
|
712
|
-
alreadyAligned: false,
|
|
713
|
-
prunedBranches: [],
|
|
714
|
-
warnings: []
|
|
715
|
-
};
|
|
716
|
-
}
|
|
859
|
+
return result;
|
|
860
|
+
});
|
|
717
861
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (!defaultBranch) {
|
|
724
|
-
return {
|
|
725
|
-
success: false,
|
|
726
|
-
targetBranch: "",
|
|
727
|
-
previousBranch: "",
|
|
728
|
-
message: "Could not detect default remote branch",
|
|
729
|
-
branchDeleted: false,
|
|
730
|
-
changesDiscarded: false,
|
|
731
|
-
warnings
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
const currentBranch = getCurrentBranch(cwd);
|
|
735
|
-
assertSafeGitRefArg(currentBranch, "resetToMainAfterMerge currentBranch");
|
|
736
|
-
assertSafeGitRefArg(defaultBranch, "resetToMainAfterMerge defaultBranch");
|
|
737
|
-
const targetBranch = `origin/${defaultBranch}`;
|
|
738
|
-
if (currentBranch === "HEAD") {
|
|
739
|
-
return {
|
|
740
|
-
success: false,
|
|
741
|
-
targetBranch,
|
|
742
|
-
previousBranch: "HEAD",
|
|
743
|
-
message: "Cannot reset: detached HEAD state",
|
|
744
|
-
branchDeleted: false,
|
|
745
|
-
changesDiscarded: false,
|
|
746
|
-
warnings
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
if (currentBranch === defaultBranch) {
|
|
750
|
-
try {
|
|
751
|
-
const logOutput = _internals.gitExec(["log", `${targetBranch}..HEAD`, "--oneline"], cwd);
|
|
752
|
-
if (logOutput.trim().length > 0) {
|
|
753
|
-
return {
|
|
754
|
-
success: false,
|
|
755
|
-
targetBranch,
|
|
756
|
-
previousBranch: currentBranch,
|
|
757
|
-
message: `Cannot reset: ${defaultBranch} has unpushed commits. Push them first.`,
|
|
758
|
-
branchDeleted: false,
|
|
759
|
-
changesDiscarded: false,
|
|
760
|
-
warnings
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
} catch {}
|
|
764
|
-
} else {
|
|
765
|
-
try {
|
|
766
|
-
_internals.gitExec(["rev-parse", "--abbrev-ref", `${currentBranch}@{upstream}`], cwd);
|
|
767
|
-
} catch {
|
|
768
|
-
try {
|
|
769
|
-
const localSha = _internals.gitExec(["rev-parse", "HEAD"], cwd).trim();
|
|
770
|
-
const remoteSha = _internals.gitExec(["rev-parse", targetBranch], cwd).trim();
|
|
771
|
-
if (localSha !== remoteSha) {
|
|
772
|
-
return {
|
|
773
|
-
success: false,
|
|
774
|
-
targetBranch,
|
|
775
|
-
previousBranch: currentBranch,
|
|
776
|
-
message: `Cannot reset: branch ${currentBranch} is local-only and diverges from ${defaultBranch}. Push or check manually.`,
|
|
777
|
-
branchDeleted: false,
|
|
778
|
-
changesDiscarded: false,
|
|
779
|
-
warnings
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
} catch {
|
|
783
|
-
return {
|
|
784
|
-
success: false,
|
|
785
|
-
targetBranch,
|
|
786
|
-
previousBranch: currentBranch,
|
|
787
|
-
message: `Cannot reset: unable to compare ${currentBranch} with ${defaultBranch}`,
|
|
788
|
-
branchDeleted: false,
|
|
789
|
-
changesDiscarded: false,
|
|
790
|
-
warnings
|
|
791
|
-
};
|
|
792
|
-
}
|
|
793
|
-
}
|
|
862
|
+
function repairEpicSentinel(directory) {
|
|
863
|
+
if (!projectDbExists(directory)) {
|
|
864
|
+
if (epicSentinelExists(directory)) {
|
|
865
|
+
unlinkSentinel(directory);
|
|
866
|
+
return "removed-stale-sentinel";
|
|
794
867
|
}
|
|
868
|
+
return "none";
|
|
869
|
+
}
|
|
870
|
+
return withCoordinationTransaction(directory, () => {
|
|
871
|
+
let rows;
|
|
795
872
|
try {
|
|
796
|
-
|
|
797
|
-
} catch
|
|
798
|
-
return
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
let
|
|
810
|
-
if (currentBranch !== defaultBranch) {
|
|
811
|
-
try {
|
|
812
|
-
_internals.gitExec(["checkout", defaultBranch], cwd);
|
|
813
|
-
switchedBranch = true;
|
|
814
|
-
} catch (err) {
|
|
815
|
-
return {
|
|
816
|
-
success: false,
|
|
817
|
-
targetBranch,
|
|
818
|
-
previousBranch,
|
|
819
|
-
message: `Checkout to ${defaultBranch} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
820
|
-
branchDeleted: false,
|
|
821
|
-
changesDiscarded: false,
|
|
822
|
-
warnings
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
}
|
|
873
|
+
rows = listLifecycleRows(directory);
|
|
874
|
+
} catch {
|
|
875
|
+
return "none";
|
|
876
|
+
}
|
|
877
|
+
const present = epicSentinelExists(directory);
|
|
878
|
+
if (rows.length === 0) {
|
|
879
|
+
if (!present)
|
|
880
|
+
return "none";
|
|
881
|
+
unlinkSentinel(directory);
|
|
882
|
+
return "removed-stale-sentinel";
|
|
883
|
+
}
|
|
884
|
+
if (rows.length > 1)
|
|
885
|
+
return "none";
|
|
886
|
+
let record;
|
|
826
887
|
try {
|
|
827
|
-
|
|
828
|
-
} catch
|
|
829
|
-
return
|
|
830
|
-
success: false,
|
|
831
|
-
targetBranch,
|
|
832
|
-
previousBranch,
|
|
833
|
-
message: `Reset to ${targetBranch} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
834
|
-
branchDeleted: false,
|
|
835
|
-
changesDiscarded: false,
|
|
836
|
-
warnings
|
|
837
|
-
};
|
|
888
|
+
record = parseEpicRecord(rows[0].payload);
|
|
889
|
+
} catch {
|
|
890
|
+
return "none";
|
|
838
891
|
}
|
|
839
|
-
|
|
840
|
-
if (
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
}
|
|
846
|
-
try {
|
|
847
|
-
_internals.gitExec(["checkout", "--", "."], cwd);
|
|
848
|
-
discardSucceeded = true;
|
|
849
|
-
break;
|
|
850
|
-
} catch {}
|
|
851
|
-
}
|
|
852
|
-
if (!discardSucceeded) {
|
|
853
|
-
warnings.push("Could not discard all uncommitted changes after reset");
|
|
854
|
-
}
|
|
855
|
-
changesDiscarded = discardSucceeded;
|
|
892
|
+
const sentinel = present ? readEpicSentinel(directory) : null;
|
|
893
|
+
if (!present) {
|
|
894
|
+
if (record.status !== "open")
|
|
895
|
+
return "none";
|
|
896
|
+
writeSentinelFile(directory, sentinelFor(record));
|
|
897
|
+
return "restored-sentinel";
|
|
856
898
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
warnings.push("Could not clean build artifacts");
|
|
899
|
+
if (sentinel === null || sentinel.epicKey !== record.epicKey || sentinel.token !== record.token) {
|
|
900
|
+
writeSentinelFile(directory, sentinelFor(record));
|
|
901
|
+
return "rewrote-mismatched-sentinel";
|
|
861
902
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
903
|
+
return "none";
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// src/epic/merge-epoch.ts
|
|
908
|
+
function classifyMergeFailure(failure, sinceMs) {
|
|
909
|
+
const at = failure.completedAt ?? failure.queuedAt;
|
|
910
|
+
if (typeof at !== "number" || !Number.isFinite(at))
|
|
911
|
+
return "undated";
|
|
912
|
+
return at >= sinceMs ? "current" : "stale";
|
|
913
|
+
}
|
|
914
|
+
function relevantMergeFailure(taskId, sinceMs) {
|
|
915
|
+
const failure = _internals4.getWorktreeMergeFailure(taskId);
|
|
916
|
+
if (!failure)
|
|
917
|
+
return;
|
|
918
|
+
return classifyMergeFailure(failure, sinceMs) === "stale" ? undefined : failure;
|
|
919
|
+
}
|
|
920
|
+
function boundStatusPath() {
|
|
921
|
+
try {
|
|
922
|
+
return _internals4.getBoundStatusPath();
|
|
923
|
+
} catch {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
function relevantMergeFailureForProject(directory, taskId, sinceMs) {
|
|
928
|
+
const bound = boundStatusPath();
|
|
929
|
+
const ours = path2.join(directory, ".swarm", "worktree-merge-status.json");
|
|
930
|
+
if (bound !== null && path2.resolve(bound) === path2.resolve(ours)) {
|
|
931
|
+
const live = relevantMergeFailure(taskId, sinceMs);
|
|
932
|
+
if (live)
|
|
933
|
+
return live;
|
|
934
|
+
}
|
|
935
|
+
const scan = _internals4.scanWorktreeMergeFailuresForRecovery(directory);
|
|
936
|
+
if (scan.status !== "ok")
|
|
937
|
+
return;
|
|
938
|
+
const durable = scan.failures.find(([id]) => id === taskId)?.[1];
|
|
939
|
+
if (!durable)
|
|
940
|
+
return;
|
|
941
|
+
return classifyMergeFailure(durable, sinceMs) === "stale" ? undefined : durable;
|
|
942
|
+
}
|
|
943
|
+
function epicMergeFailureSkipsCheckpoint(directory, taskId) {
|
|
944
|
+
try {
|
|
945
|
+
if (!_internals4.epicSentinelExists(directory))
|
|
946
|
+
return false;
|
|
947
|
+
const epic = _internals4.getOpenEpic(directory);
|
|
948
|
+
if (!epic)
|
|
949
|
+
return false;
|
|
950
|
+
const started = Date.parse(epic.startedAt);
|
|
951
|
+
const failure = relevantMergeFailureForProject(directory, taskId, Number.isFinite(started) ? started : 0);
|
|
952
|
+
if (!failure)
|
|
953
|
+
return false;
|
|
954
|
+
criticalWarn(`[epic] auto-checkpoint SKIPPED for ${taskId}: its worktree merge-back ${failure.outcome} at stage '${failure.stage}', so its work is not on the epic branch. Resolve the preserved worktree (\`/swarm lanes\`) and re-dispatch the task. Detail: ${failure.message}`);
|
|
955
|
+
return true;
|
|
956
|
+
} catch {
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function describeMergeFailuresForStatus(directory, sinceMs) {
|
|
961
|
+
const scan = _internals4.scanWorktreeMergeFailuresForRecovery(directory);
|
|
962
|
+
if (scan.status === "uncertain") {
|
|
963
|
+
return [
|
|
964
|
+
"",
|
|
965
|
+
"### Worktree merge failures",
|
|
966
|
+
`- Could not read \`.swarm/worktree-merge-status.json\`: ${scan.reason}. Epic falls back to the in-memory registry; repair or remove the file (no swarm session running) if this persists.`
|
|
967
|
+
];
|
|
968
|
+
}
|
|
969
|
+
if (scan.failures.length === 0)
|
|
970
|
+
return [];
|
|
971
|
+
const lines = ["", "### Worktree merge failures"];
|
|
972
|
+
const rootLabel = sinceMs === null ? "unknown (no plan ledger)" : new Date(sinceMs).toISOString();
|
|
973
|
+
lines.push(`Current plan root: ${rootLabel}`);
|
|
974
|
+
let blocking = 0;
|
|
975
|
+
for (const [taskId, failure] of scan.failures) {
|
|
976
|
+
const relevance = classifyMergeFailure(failure, sinceMs ?? Number.NEGATIVE_INFINITY);
|
|
977
|
+
const what = `${failure.outcome} at '${failure.stage}'`;
|
|
978
|
+
if (relevance === "stale") {
|
|
979
|
+
lines.push(`- ${taskId}: ${what} \u2014 stale (recorded before the current plan); ignored by Epic.`);
|
|
980
|
+
continue;
|
|
882
981
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
982
|
+
blocking += 1;
|
|
983
|
+
if (relevance === "undated") {
|
|
984
|
+
lines.push(`- ${taskId}: ${what} \u2014 NO timestamp, so it cannot be dated against the current plan; treated as BLOCKING (fail closed): an epic wave holding task ${taskId} will not close.`);
|
|
985
|
+
} else {
|
|
986
|
+
lines.push(`- ${taskId}: ${what} \u2014 BLOCKING: an epic wave holding task ${taskId} will not close.`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (blocking > 0) {
|
|
990
|
+
lines.push("Remedy: resolve the preserved worktree (`/swarm lanes`) and re-dispatch the task \u2014 a clean merge-back clears its record. If the work already landed, or an undated record belongs to an earlier plan, clear it with `/swarm epic clear-merge-failure <taskId> --confirm`.");
|
|
991
|
+
}
|
|
992
|
+
return lines;
|
|
993
|
+
}
|
|
994
|
+
function clearMergeFailureCommand(directory, args) {
|
|
995
|
+
const taskId = args.find((a) => !a.startsWith("--"))?.trim();
|
|
996
|
+
const confirm = args.includes("--confirm");
|
|
997
|
+
if (!taskId) {
|
|
998
|
+
return "Usage: /swarm epic clear-merge-failure <taskId> [--confirm]";
|
|
999
|
+
}
|
|
1000
|
+
const scan = _internals4.scanWorktreeMergeFailuresForRecovery(directory);
|
|
1001
|
+
const onDisk = scan.status === "ok" ? scan.failures.find(([id]) => id === taskId)?.[1] : undefined;
|
|
1002
|
+
const failure = onDisk ?? _internals4.getWorktreeMergeFailure(taskId);
|
|
1003
|
+
if (!failure) {
|
|
1004
|
+
return `No worktree merge failure is recorded for task ${taskId}; nothing to clear.`;
|
|
1005
|
+
}
|
|
1006
|
+
const at = failure.completedAt ?? failure.queuedAt;
|
|
1007
|
+
const detail = `${failure.outcome} at '${failure.stage}' (${typeof at === "number" && Number.isFinite(at) ? `recorded ${new Date(at).toISOString()}` : "no timestamp"}): ${failure.message}${failure.worktreePath ? ` \u2014 preserved worktree ${failure.worktreePath}` : ""}`;
|
|
1008
|
+
if (!confirm) {
|
|
1009
|
+
return [
|
|
1010
|
+
`Task ${taskId} has a recorded worktree merge failure: ${detail}.`,
|
|
1011
|
+
`Clearing it lets the epic wave holding task ${taskId} close, treating the task's work as landed. Clear it only if the task's work is on the epic branch (or the record belongs to an earlier plan); otherwise resolve the preserved worktree first (\`/swarm lanes\`).`,
|
|
1012
|
+
`To clear, run: /swarm epic clear-merge-failure ${taskId} --confirm`
|
|
1013
|
+
].join(`
|
|
887
1014
|
`);
|
|
888
|
-
for (const line of mergedLines) {
|
|
889
|
-
const trimmedLine = line.trim();
|
|
890
|
-
if (!trimmedLine || trimmedLine.startsWith("*") || trimmedLine === defaultBranch) {
|
|
891
|
-
continue;
|
|
892
|
-
}
|
|
893
|
-
try {
|
|
894
|
-
_internals.gitExec([
|
|
895
|
-
"branch",
|
|
896
|
-
"-d",
|
|
897
|
-
"--",
|
|
898
|
-
assertSafeGitRefArg(trimmedLine, "resetToMainAfterMerge prune branch")
|
|
899
|
-
], cwd);
|
|
900
|
-
} catch {
|
|
901
|
-
warnings.push(`Could not prune branch: ${trimmedLine}`);
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
} catch (err) {
|
|
905
|
-
warnings.push(`Prune failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
return {
|
|
909
|
-
success: true,
|
|
910
|
-
targetBranch,
|
|
911
|
-
previousBranch,
|
|
912
|
-
message: branchDeleted ? `Reset to ${defaultBranch} and deleted branch ${previousBranch}` : `Reset to ${defaultBranch}`,
|
|
913
|
-
branchDeleted,
|
|
914
|
-
changesDiscarded,
|
|
915
|
-
warnings
|
|
916
|
-
};
|
|
917
|
-
} catch (err) {
|
|
918
|
-
return {
|
|
919
|
-
success: false,
|
|
920
|
-
targetBranch: "",
|
|
921
|
-
previousBranch: "",
|
|
922
|
-
message: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,
|
|
923
|
-
branchDeleted: false,
|
|
924
|
-
changesDiscarded: false,
|
|
925
|
-
warnings
|
|
926
|
-
};
|
|
927
1015
|
}
|
|
1016
|
+
_internals4.initDurableStatusPath(directory);
|
|
1017
|
+
_internals4.clearWorktreeMergeStatus(taskId);
|
|
1018
|
+
return `Cleared the recorded worktree merge failure for task ${taskId} (${detail}). The next \`epic_next_wave\` may close its wave.`;
|
|
928
1019
|
}
|
|
929
|
-
var
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
readLaneEnvFileFromDiskSync,
|
|
938
|
-
resolveGitExecutable
|
|
1020
|
+
var _internals4 = {
|
|
1021
|
+
epicSentinelExists,
|
|
1022
|
+
getOpenEpic,
|
|
1023
|
+
clearWorktreeMergeStatus,
|
|
1024
|
+
initDurableStatusPath,
|
|
1025
|
+
getWorktreeMergeFailure,
|
|
1026
|
+
scanWorktreeMergeFailuresForRecovery,
|
|
1027
|
+
getBoundStatusPath: () => _internals.getDurableStatusPath()
|
|
939
1028
|
};
|
|
940
1029
|
|
|
941
1030
|
// src/services/run-memory.ts
|
|
942
1031
|
import * as crypto from "crypto";
|
|
943
|
-
import * as
|
|
1032
|
+
import * as fs2 from "fs/promises";
|
|
944
1033
|
init_logger();
|
|
945
1034
|
var RUN_MEMORY_FILENAME = "run-memory.jsonl";
|
|
946
1035
|
var MAX_SUMMARY_TOKENS = 500;
|
|
@@ -954,7 +1043,7 @@ async function recordOutcome(directory, entry) {
|
|
|
954
1043
|
const resolvedPath = validateSwarmPath(directory, RUN_MEMORY_FILENAME);
|
|
955
1044
|
const line = `${JSON.stringify(entry)}
|
|
956
1045
|
`;
|
|
957
|
-
await
|
|
1046
|
+
await fs2.appendFile(resolvedPath, line, { encoding: "utf-8" });
|
|
958
1047
|
}
|
|
959
1048
|
async function getTaskHistory(directory, taskId) {
|
|
960
1049
|
validateWorkspaceRoot(directory);
|
|
@@ -981,14 +1070,14 @@ async function recordTaskAttempt(directory, input) {
|
|
|
981
1070
|
try {
|
|
982
1071
|
let history = [];
|
|
983
1072
|
try {
|
|
984
|
-
history = await
|
|
1073
|
+
history = await _internals5.getTaskHistory(directory, input.taskId);
|
|
985
1074
|
} catch (err) {
|
|
986
1075
|
warn(`[run-memory] history read for task ${input.taskId} failed; recording with attemptNumber 1: ${err instanceof Error ? err.message : String(err)}`);
|
|
987
1076
|
}
|
|
988
1077
|
const entry = {
|
|
989
1078
|
timestamp: new Date().toISOString(),
|
|
990
1079
|
taskId: input.taskId,
|
|
991
|
-
taskFingerprint:
|
|
1080
|
+
taskFingerprint: _internals5.generateTaskFingerprint(input.taskId, input.fileTargets ?? []),
|
|
992
1081
|
agent: input.agent,
|
|
993
1082
|
outcome: input.outcome,
|
|
994
1083
|
attemptNumber: history.length + 1
|
|
@@ -1002,7 +1091,7 @@ async function recordTaskAttempt(directory, input) {
|
|
|
1002
1091
|
if (typeof input.durationMs === "number") {
|
|
1003
1092
|
entry.durationMs = input.durationMs;
|
|
1004
1093
|
}
|
|
1005
|
-
await
|
|
1094
|
+
await _internals5.recordOutcome(directory, entry);
|
|
1006
1095
|
} catch (err) {
|
|
1007
1096
|
warn(`[run-memory] failed to record ${input.outcome} for task ${input.taskId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1008
1097
|
}
|
|
@@ -1066,10 +1155,10 @@ async function getRunMemorySummary(directory) {
|
|
|
1066
1155
|
if (entries.length === 0) {
|
|
1067
1156
|
return null;
|
|
1068
1157
|
}
|
|
1069
|
-
const groups =
|
|
1158
|
+
const groups = _internals5.groupByTaskId(entries);
|
|
1070
1159
|
const summaries = [];
|
|
1071
1160
|
for (const [taskId, taskEntries] of groups) {
|
|
1072
|
-
const summary =
|
|
1161
|
+
const summary = _internals5.summarizeTask(taskId, taskEntries);
|
|
1073
1162
|
if (summary) {
|
|
1074
1163
|
summaries.push(summary);
|
|
1075
1164
|
}
|
|
@@ -1099,7 +1188,7 @@ Use this data to avoid repeating known failure patterns.`;
|
|
|
1099
1188
|
}
|
|
1100
1189
|
return prefix + summaryText + suffix;
|
|
1101
1190
|
}
|
|
1102
|
-
var
|
|
1191
|
+
var _internals5 = {
|
|
1103
1192
|
generateTaskFingerprint,
|
|
1104
1193
|
recordOutcome,
|
|
1105
1194
|
recordTaskAttempt,
|
|
@@ -1109,437 +1198,18 @@ var _internals2 = {
|
|
|
1109
1198
|
summarizeTask
|
|
1110
1199
|
};
|
|
1111
1200
|
|
|
1112
|
-
// src/turbo/epic/state.ts
|
|
1113
|
-
import { createHash as createHash3 } from "crypto";
|
|
1114
|
-
import * as fs2 from "fs";
|
|
1115
|
-
import * as path2 from "path";
|
|
1116
|
-
init_logger();
|
|
1117
|
-
var STATE_FILE = "epic-state.json";
|
|
1118
|
-
var COORDINATION_NAMESPACE = "turbo.epic.session";
|
|
1119
|
-
var MAX_SESSION_WRITE_ATTEMPTS = 5;
|
|
1120
|
-
function nowISO() {
|
|
1121
|
-
return new Date().toISOString();
|
|
1122
|
-
}
|
|
1123
|
-
function ensureSwarmDir(directory) {
|
|
1124
|
-
const swarmDir = path2.resolve(directory, ".swarm");
|
|
1125
|
-
if (!fs2.existsSync(swarmDir)) {
|
|
1126
|
-
fs2.mkdirSync(swarmDir, { recursive: true });
|
|
1127
|
-
}
|
|
1128
|
-
return swarmDir;
|
|
1129
|
-
}
|
|
1130
|
-
function stateFilePath(directory) {
|
|
1131
|
-
return path2.join(directory, ".swarm", STATE_FILE);
|
|
1132
|
-
}
|
|
1133
|
-
function importedStateFilePath(directory) {
|
|
1134
|
-
return `${stateFilePath(directory)}.imported`;
|
|
1135
|
-
}
|
|
1136
|
-
function archiveStateFileWithoutOverwrite(directory) {
|
|
1137
|
-
const filePath = stateFilePath(directory);
|
|
1138
|
-
if (!fs2.existsSync(filePath))
|
|
1139
|
-
return;
|
|
1140
|
-
const canonical = importedStateFilePath(directory);
|
|
1141
|
-
if (!fs2.existsSync(canonical)) {
|
|
1142
|
-
fs2.renameSync(filePath, canonical);
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
for (let suffix = 1;suffix <= 1000; suffix += 1) {
|
|
1146
|
-
const candidate = `${canonical}.${suffix}`;
|
|
1147
|
-
if (fs2.existsSync(candidate))
|
|
1148
|
-
continue;
|
|
1149
|
-
fs2.renameSync(filePath, candidate);
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
throw new Error("Epic state legacy archive collision limit exceeded");
|
|
1153
|
-
}
|
|
1154
|
-
function isRecord(value) {
|
|
1155
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1156
|
-
}
|
|
1157
|
-
function validatePersistedShape(parsed) {
|
|
1158
|
-
return isRecord(parsed) && parsed.version === 1 && isRecord(parsed.sessions);
|
|
1159
|
-
}
|
|
1160
|
-
function parsePersistedJson(raw) {
|
|
1161
|
-
const parsed = JSON.parse(raw);
|
|
1162
|
-
if (!validatePersistedShape(parsed)) {
|
|
1163
|
-
const maybe = parsed;
|
|
1164
|
-
throw new Error(`malformed shape (version=${maybe?.version}, sessions type=${Array.isArray(maybe?.sessions) ? "array" : typeof maybe?.sessions})`);
|
|
1165
|
-
}
|
|
1166
|
-
return {
|
|
1167
|
-
version: 1,
|
|
1168
|
-
updatedAt: typeof parsed.updatedAt === "string" && parsed.updatedAt.length > 0 ? parsed.updatedAt : nowISO(),
|
|
1169
|
-
sessions: parsed.sessions
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
function parseSessionPayload(payload, entityKey) {
|
|
1173
|
-
const parsed = JSON.parse(payload);
|
|
1174
|
-
if (!isRecord(parsed) || parsed.sessionID !== entityKey) {
|
|
1175
|
-
throw new Error(`session payload malformed for ${entityKey}: sessionID mismatch`);
|
|
1176
|
-
}
|
|
1177
|
-
return parsed;
|
|
1178
|
-
}
|
|
1179
|
-
function buildPersistedFromCoordination(directory) {
|
|
1180
|
-
const rows = listCoordinationStates(directory, COORDINATION_NAMESPACE);
|
|
1181
|
-
if (rows.length === 0)
|
|
1182
|
-
return null;
|
|
1183
|
-
const sessions = {};
|
|
1184
|
-
let updatedAt = "";
|
|
1185
|
-
for (const row of rows) {
|
|
1186
|
-
sessions[row.entityKey] = parseSessionPayload(row.payload, row.entityKey);
|
|
1187
|
-
if (!updatedAt || row.updatedAt > updatedAt)
|
|
1188
|
-
updatedAt = row.updatedAt;
|
|
1189
|
-
}
|
|
1190
|
-
return {
|
|
1191
|
-
version: 1,
|
|
1192
|
-
updatedAt: updatedAt || nowISO(),
|
|
1193
|
-
sessions
|
|
1194
|
-
};
|
|
1195
|
-
}
|
|
1196
|
-
function readLegacyPersisted(directory) {
|
|
1197
|
-
const filePath = stateFilePath(directory);
|
|
1198
|
-
if (!fs2.existsSync(filePath))
|
|
1199
|
-
return null;
|
|
1200
|
-
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
1201
|
-
return {
|
|
1202
|
-
persisted: parsePersistedJson(raw),
|
|
1203
|
-
sourceDigest: createHash3("sha256").update(raw).digest("hex")
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
function writeProjection(directory, persisted) {
|
|
1207
|
-
ensureSwarmDir(directory);
|
|
1208
|
-
const filePath = stateFilePath(directory);
|
|
1209
|
-
if (fs2.existsSync(filePath) && fs2.lstatSync(filePath).isDirectory()) {
|
|
1210
|
-
throw new Error(`${STATE_FILE} is a directory`);
|
|
1211
|
-
}
|
|
1212
|
-
const payload = `${JSON.stringify(persisted, null, 2)}
|
|
1213
|
-
`;
|
|
1214
|
-
atomicWriteSwarmFileSync(filePath, payload);
|
|
1215
|
-
}
|
|
1216
|
-
function seedProjectionBestEffort(directory, persisted) {
|
|
1217
|
-
try {
|
|
1218
|
-
writeProjection(directory, persisted);
|
|
1219
|
-
} catch {}
|
|
1220
|
-
}
|
|
1221
|
-
function preflightProjectionTarget(directory) {
|
|
1222
|
-
ensureSwarmDir(directory);
|
|
1223
|
-
const filePath = stateFilePath(directory);
|
|
1224
|
-
if (fs2.existsSync(filePath) && fs2.lstatSync(filePath).isDirectory()) {
|
|
1225
|
-
throw new Error(`Epic state persistence prepare failed: ${STATE_FILE} is a directory`);
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
function importLegacyStateIfNeeded(directory) {
|
|
1229
|
-
const legacy = readLegacyPersisted(directory);
|
|
1230
|
-
if (!legacy)
|
|
1231
|
-
return null;
|
|
1232
|
-
const outcome = importCoordinationOnce(directory, {
|
|
1233
|
-
source: STATE_FILE,
|
|
1234
|
-
sourceDigest: legacy.sourceDigest,
|
|
1235
|
-
rowCount: Object.keys(legacy.persisted.sessions).length,
|
|
1236
|
-
emptyNamespace: COORDINATION_NAMESPACE
|
|
1237
|
-
}, () => {
|
|
1238
|
-
for (const [sessionID, state] of Object.entries(legacy.persisted.sessions)) {
|
|
1239
|
-
const result = transitionCoordinationState(directory, {
|
|
1240
|
-
namespace: COORDINATION_NAMESPACE,
|
|
1241
|
-
entityKey: sessionID,
|
|
1242
|
-
expectedRevision: null,
|
|
1243
|
-
generation: 1,
|
|
1244
|
-
status: state.active ? "active" : "inactive",
|
|
1245
|
-
payload: JSON.stringify(state)
|
|
1246
|
-
});
|
|
1247
|
-
if (result.outcome !== "applied") {
|
|
1248
|
-
throw new Error("Epic legacy import conflict");
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
});
|
|
1252
|
-
if (outcome === "imported") {
|
|
1253
|
-
archiveStateFileWithoutOverwrite(directory);
|
|
1254
|
-
}
|
|
1255
|
-
const authoritative = buildPersistedFromCoordination(directory) ?? emptyPersisted();
|
|
1256
|
-
seedProjectionBestEffort(directory, authoritative);
|
|
1257
|
-
return authoritative;
|
|
1258
|
-
}
|
|
1259
|
-
function unreadableStateError(directory) {
|
|
1260
|
-
return new Error(`Epic state is unreadable for ${directory}. Repair .swarm/${STATE_FILE} before continuing.`);
|
|
1261
|
-
}
|
|
1262
|
-
function ensureReadableState(directory) {
|
|
1263
|
-
if (stateUnreadableMap.get(stateKey(directory)))
|
|
1264
|
-
throw unreadableStateError(directory);
|
|
1265
|
-
if (!readPersisted(directory))
|
|
1266
|
-
throw unreadableStateError(directory);
|
|
1267
|
-
}
|
|
1268
|
-
function refreshProjectionFromCoordination(directory) {
|
|
1269
|
-
const persisted = buildPersistedFromCoordination(directory) ?? emptyPersisted();
|
|
1270
|
-
writeProjection(directory, persisted);
|
|
1271
|
-
}
|
|
1272
|
-
function saveSessionRowAtomic(directory, state) {
|
|
1273
|
-
preflightProjectionTarget(directory);
|
|
1274
|
-
ensureReadableState(directory);
|
|
1275
|
-
for (let attempt = 0;attempt < MAX_SESSION_WRITE_ATTEMPTS; attempt++) {
|
|
1276
|
-
const current = getCoordinationState(directory, COORDINATION_NAMESPACE, state.sessionID);
|
|
1277
|
-
const result = transitionCoordinationState(directory, {
|
|
1278
|
-
namespace: COORDINATION_NAMESPACE,
|
|
1279
|
-
entityKey: state.sessionID,
|
|
1280
|
-
expectedRevision: current?.revision ?? null,
|
|
1281
|
-
generation: (current?.generation ?? 0) + 1,
|
|
1282
|
-
status: state.active ? "active" : "inactive",
|
|
1283
|
-
payload: JSON.stringify(state)
|
|
1284
|
-
});
|
|
1285
|
-
if (result.outcome === "applied") {
|
|
1286
|
-
refreshProjectionFromCoordination(directory);
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
if (result.outcome === "revision_conflict" || result.outcome === "stale_generation") {
|
|
1290
|
-
continue;
|
|
1291
|
-
}
|
|
1292
|
-
throw new Error(`Epic state persistence failed: ${result.outcome} for ${state.sessionID}`);
|
|
1293
|
-
}
|
|
1294
|
-
throw new Error(`Epic state persistence failed: contention for ${state.sessionID}`);
|
|
1295
|
-
}
|
|
1296
|
-
function mutateSessionRowAtomic(directory, sessionID, mutate) {
|
|
1297
|
-
preflightProjectionTarget(directory);
|
|
1298
|
-
ensureReadableState(directory);
|
|
1299
|
-
for (let attempt = 0;attempt < MAX_SESSION_WRITE_ATTEMPTS; attempt++) {
|
|
1300
|
-
const current = getCoordinationState(directory, COORDINATION_NAMESPACE, sessionID);
|
|
1301
|
-
if (!current)
|
|
1302
|
-
return null;
|
|
1303
|
-
const nextState = parseSessionPayload(current.payload, sessionID);
|
|
1304
|
-
mutate(nextState);
|
|
1305
|
-
const result = transitionCoordinationState(directory, {
|
|
1306
|
-
namespace: COORDINATION_NAMESPACE,
|
|
1307
|
-
entityKey: sessionID,
|
|
1308
|
-
expectedRevision: current.revision,
|
|
1309
|
-
generation: current.generation + 1,
|
|
1310
|
-
status: nextState.active ? "active" : "inactive",
|
|
1311
|
-
payload: JSON.stringify(nextState)
|
|
1312
|
-
});
|
|
1313
|
-
if (result.outcome === "applied") {
|
|
1314
|
-
refreshProjectionFromCoordination(directory);
|
|
1315
|
-
return nextState;
|
|
1316
|
-
}
|
|
1317
|
-
if (result.outcome === "revision_conflict" || result.outcome === "stale_generation") {
|
|
1318
|
-
continue;
|
|
1319
|
-
}
|
|
1320
|
-
throw new Error(`Epic state persistence failed: ${result.outcome} for ${sessionID}`);
|
|
1321
|
-
}
|
|
1322
|
-
throw new Error(`Epic state persistence failed: contention for ${sessionID}`);
|
|
1323
|
-
}
|
|
1324
|
-
function emptyPersisted() {
|
|
1325
|
-
return { version: 1, updatedAt: nowISO(), sessions: {} };
|
|
1326
|
-
}
|
|
1327
|
-
function emptySessionState(sessionID) {
|
|
1328
|
-
return { sessionID, active: false };
|
|
1329
|
-
}
|
|
1330
|
-
var stateUnreadableMap = new Map;
|
|
1331
|
-
function stateKey(directory) {
|
|
1332
|
-
return canonicalRootKeyFresh(directory);
|
|
1333
|
-
}
|
|
1334
|
-
function isStateUnreadable(directory) {
|
|
1335
|
-
return stateUnreadableMap.get(stateKey(directory)) ?? false;
|
|
1336
|
-
}
|
|
1337
|
-
function markStateUnreadable(directory, reason) {
|
|
1338
|
-
stateUnreadableMap.set(stateKey(directory), true);
|
|
1339
|
-
error(`[turbo/epic/state] state unreadable for ${directory}: ${reason} \u2014 failing closed`);
|
|
1340
|
-
}
|
|
1341
|
-
function readPersisted(directory) {
|
|
1342
|
-
try {
|
|
1343
|
-
const coordinated = buildPersistedFromCoordination(directory);
|
|
1344
|
-
if (coordinated) {
|
|
1345
|
-
const filePath = stateFilePath(directory);
|
|
1346
|
-
if (fs2.existsSync(filePath)) {
|
|
1347
|
-
let matches = false;
|
|
1348
|
-
try {
|
|
1349
|
-
matches = JSON.stringify(parsePersistedJson(fs2.readFileSync(filePath, "utf-8"))) === JSON.stringify(coordinated);
|
|
1350
|
-
} catch {
|
|
1351
|
-
matches = false;
|
|
1352
|
-
}
|
|
1353
|
-
if (!matches)
|
|
1354
|
-
archiveStateFileWithoutOverwrite(directory);
|
|
1355
|
-
}
|
|
1356
|
-
seedProjectionBestEffort(directory, coordinated);
|
|
1357
|
-
return coordinated;
|
|
1358
|
-
}
|
|
1359
|
-
const imported = importLegacyStateIfNeeded(directory);
|
|
1360
|
-
if (imported)
|
|
1361
|
-
return imported;
|
|
1362
|
-
const seed = emptyPersisted();
|
|
1363
|
-
seedProjectionBestEffort(directory, seed);
|
|
1364
|
-
return seed;
|
|
1365
|
-
} catch (error2) {
|
|
1366
|
-
const reason = error2 instanceof Error ? error2.message : String(error2);
|
|
1367
|
-
markStateUnreadable(directory, reason);
|
|
1368
|
-
return null;
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
function loadEpicSessionState(directory, sessionID) {
|
|
1372
|
-
if (stateUnreadableMap.get(stateKey(directory)))
|
|
1373
|
-
return null;
|
|
1374
|
-
const persisted = readPersisted(directory);
|
|
1375
|
-
if (!persisted)
|
|
1376
|
-
return null;
|
|
1377
|
-
return persisted.sessions[sessionID] ?? null;
|
|
1378
|
-
}
|
|
1379
|
-
function isEpicModeActive(directory, sessionID) {
|
|
1380
|
-
const state = loadEpicSessionState(directory, sessionID);
|
|
1381
|
-
return state?.active === true;
|
|
1382
|
-
}
|
|
1383
|
-
function isEpicModeActiveForProject(directory) {
|
|
1384
|
-
if (directory.split(/[\\/]/).includes(".."))
|
|
1385
|
-
return false;
|
|
1386
|
-
if (stateUnreadableMap.get(stateKey(directory)))
|
|
1387
|
-
return false;
|
|
1388
|
-
const hasLegacyFile = fs2.existsSync(stateFilePath(directory));
|
|
1389
|
-
let hasCoordinationRows = false;
|
|
1390
|
-
try {
|
|
1391
|
-
hasCoordinationRows = projectDbExists(directory) && listCoordinationStates(directory, COORDINATION_NAMESPACE, 1).length > 0;
|
|
1392
|
-
} catch {
|
|
1393
|
-
return false;
|
|
1394
|
-
}
|
|
1395
|
-
if (!hasLegacyFile && !hasCoordinationRows) {
|
|
1396
|
-
return false;
|
|
1397
|
-
}
|
|
1398
|
-
const persisted = readPersisted(directory);
|
|
1399
|
-
if (!persisted)
|
|
1400
|
-
return false;
|
|
1401
|
-
for (const session of Object.values(persisted.sessions)) {
|
|
1402
|
-
if (session?.active === true)
|
|
1403
|
-
return true;
|
|
1404
|
-
}
|
|
1405
|
-
return false;
|
|
1406
|
-
}
|
|
1407
|
-
function enableEpicMode(directory, sessionID) {
|
|
1408
|
-
const current = loadEpicSessionState(directory, sessionID);
|
|
1409
|
-
if (!current) {
|
|
1410
|
-
saveSessionRowAtomic(directory, {
|
|
1411
|
-
...emptySessionState(sessionID),
|
|
1412
|
-
active: true,
|
|
1413
|
-
enabledAt: nowISO(),
|
|
1414
|
-
disabledAt: undefined
|
|
1415
|
-
});
|
|
1416
|
-
return;
|
|
1417
|
-
}
|
|
1418
|
-
mutateSessionRowAtomic(directory, sessionID, (state) => {
|
|
1419
|
-
state.active = true;
|
|
1420
|
-
state.enabledAt = nowISO();
|
|
1421
|
-
state.disabledAt = undefined;
|
|
1422
|
-
});
|
|
1423
|
-
}
|
|
1424
|
-
function disableEpicMode(directory, sessionID) {
|
|
1425
|
-
const current = loadEpicSessionState(directory, sessionID);
|
|
1426
|
-
if (!current) {
|
|
1427
|
-
saveSessionRowAtomic(directory, {
|
|
1428
|
-
...emptySessionState(sessionID),
|
|
1429
|
-
disabledAt: nowISO()
|
|
1430
|
-
});
|
|
1431
|
-
return;
|
|
1432
|
-
}
|
|
1433
|
-
mutateSessionRowAtomic(directory, sessionID, (state) => {
|
|
1434
|
-
state.active = false;
|
|
1435
|
-
state.disabledAt = nowISO();
|
|
1436
|
-
});
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
// src/turbo/epic/task-commit.ts
|
|
1440
|
-
init_logger();
|
|
1441
|
-
function scrubTaskIdForGitSubject(taskId) {
|
|
1442
|
-
return taskId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
1443
|
-
}
|
|
1444
|
-
function formatTaskCommitMessage(taskId, description) {
|
|
1445
|
-
const safeId = scrubTaskIdForGitSubject(taskId);
|
|
1446
|
-
const summary = (description ?? "completed").replace(/\s+/g, " ").trim();
|
|
1447
|
-
const truncated = summary.length > 60 ? `${summary.slice(0, 57)}...` : summary;
|
|
1448
|
-
return `swarm(task ${safeId}): ${truncated || "completed"}`;
|
|
1449
|
-
}
|
|
1450
|
-
var INDEX_LOCK_BACKOFF_MS = [100, 200, 400, 800];
|
|
1451
|
-
var INDEX_LOCK_ERROR_RE = /index\.lock|unable to create.*\.lock/i;
|
|
1452
|
-
function isLockContentionError(err) {
|
|
1453
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1454
|
-
return INDEX_LOCK_ERROR_RE.test(msg);
|
|
1455
|
-
}
|
|
1456
|
-
async function commitTaskCompletion(directory, taskId, description, scopePaths) {
|
|
1457
|
-
if (!_internals3.isGitRepo(directory)) {
|
|
1458
|
-
return { committed: false, reason: "no-git" };
|
|
1459
|
-
}
|
|
1460
|
-
try {
|
|
1461
|
-
if (_internals3.hasExistingTaskCommit(directory, taskId)) {
|
|
1462
|
-
return { committed: true, reason: "idempotent-skip" };
|
|
1463
|
-
}
|
|
1464
|
-
} catch {}
|
|
1465
|
-
const rawPaths = (scopePaths ?? []).filter((p) => typeof p === "string" && p.trim().length > 0);
|
|
1466
|
-
const droppedMagic = [];
|
|
1467
|
-
const paths = rawPaths.filter((p) => {
|
|
1468
|
-
if (p.startsWith(":")) {
|
|
1469
|
-
droppedMagic.push(p);
|
|
1470
|
-
return false;
|
|
1471
|
-
}
|
|
1472
|
-
return true;
|
|
1473
|
-
});
|
|
1474
|
-
if (droppedMagic.length > 0) {
|
|
1475
|
-
criticalWarn(`[epic:task-commit] dropped ${droppedMagic.length} scope path(s) starting with ':' (git pathspec magic, not allowed): ${droppedMagic.slice(0, 5).join(", ")}${droppedMagic.length > 5 ? `, +${droppedMagic.length - 5} more` : ""}. Architect should declare literal file paths only.`);
|
|
1476
|
-
}
|
|
1477
|
-
const message = formatTaskCommitMessage(taskId, description);
|
|
1478
|
-
let lastError = null;
|
|
1479
|
-
for (let attempt = 0;attempt <= INDEX_LOCK_BACKOFF_MS.length; attempt++) {
|
|
1480
|
-
try {
|
|
1481
|
-
if (paths.length > 0) {
|
|
1482
|
-
_internals3.stageScopedPaths(directory, paths);
|
|
1483
|
-
}
|
|
1484
|
-
_internals3.commitAllowEmpty(directory, message);
|
|
1485
|
-
const sha = _internals3.gitHeadSha(directory);
|
|
1486
|
-
return { committed: true, reason: "success", sha };
|
|
1487
|
-
} catch (err) {
|
|
1488
|
-
lastError = err;
|
|
1489
|
-
if (attempt < INDEX_LOCK_BACKOFF_MS.length && isLockContentionError(err)) {
|
|
1490
|
-
await _internals3.sleep(INDEX_LOCK_BACKOFF_MS[attempt]);
|
|
1491
|
-
continue;
|
|
1492
|
-
}
|
|
1493
|
-
break;
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
const msg = lastError instanceof Error ? lastError.message : String(lastError);
|
|
1497
|
-
criticalWarn(`[epic:task-commit] commit for task ${taskId} failed (non-fatal): ${msg}`);
|
|
1498
|
-
return { committed: false, reason: "commit-failed", error: msg };
|
|
1499
|
-
}
|
|
1500
|
-
var _internals3 = {
|
|
1501
|
-
isGitRepo: (cwd) => isGitRepo(cwd),
|
|
1502
|
-
stageScopedPaths: (cwd, paths) => {
|
|
1503
|
-
const CHUNK = 200;
|
|
1504
|
-
for (let i = 0;i < paths.length; i += CHUNK) {
|
|
1505
|
-
const chunk = paths.slice(i, i + CHUNK);
|
|
1506
|
-
_internals.gitExec(["add", "--", ...chunk, ":(exclude,glob)**/.swarm/**"], cwd);
|
|
1507
|
-
}
|
|
1508
|
-
},
|
|
1509
|
-
commitAllowEmpty: (cwd, message) => {
|
|
1510
|
-
_internals.gitExec(["commit", "--allow-empty", "--no-verify", "-m", message], cwd);
|
|
1511
|
-
},
|
|
1512
|
-
gitHeadSha: (cwd) => {
|
|
1513
|
-
return _internals.gitExec(["rev-parse", "HEAD"], cwd).trim();
|
|
1514
|
-
},
|
|
1515
|
-
sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)),
|
|
1516
|
-
hasExistingTaskCommit: (cwd, taskId) => {
|
|
1517
|
-
const safeId = scrubTaskIdForGitSubject(taskId);
|
|
1518
|
-
const escaped = safeId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1519
|
-
const output = _internals.gitExec([
|
|
1520
|
-
"log",
|
|
1521
|
-
"--extended-regexp",
|
|
1522
|
-
`--grep=^swarm\\(task ${escaped}\\):`,
|
|
1523
|
-
"--pretty=format:%H",
|
|
1524
|
-
"-n",
|
|
1525
|
-
"1"
|
|
1526
|
-
], cwd);
|
|
1527
|
-
return output.trim().length > 0;
|
|
1528
|
-
}
|
|
1529
|
-
};
|
|
1530
|
-
|
|
1531
1201
|
// src/utils/spec-hash.ts
|
|
1532
1202
|
import { createHash as createHash4 } from "crypto";
|
|
1533
|
-
import * as
|
|
1534
|
-
import { join as
|
|
1203
|
+
import * as fsSync from "fs";
|
|
1204
|
+
import { join as join3 } from "path";
|
|
1535
1205
|
async function computeSpecHash(directory) {
|
|
1536
|
-
const spec =
|
|
1206
|
+
const spec = _internals6.readEffectiveSpecSync(directory);
|
|
1537
1207
|
if (!spec)
|
|
1538
1208
|
return null;
|
|
1539
1209
|
return createHash4("sha256").update(spec.content, "utf-8").digest("hex");
|
|
1540
1210
|
}
|
|
1541
1211
|
async function isSpecStale(directory, plan) {
|
|
1542
|
-
const currentHash = await
|
|
1212
|
+
const currentHash = await _internals6.computeSpecHash(directory);
|
|
1543
1213
|
if (!plan.specHash) {
|
|
1544
1214
|
return { stale: false };
|
|
1545
1215
|
}
|
|
@@ -1560,15 +1230,15 @@ async function isSpecStale(directory, plan) {
|
|
|
1560
1230
|
return { stale: false };
|
|
1561
1231
|
}
|
|
1562
1232
|
function isObligationPreserving(directory) {
|
|
1563
|
-
const snapshotPath =
|
|
1233
|
+
const snapshotPath = join3(directory, ".swarm", "spec-snapshot.md");
|
|
1564
1234
|
try {
|
|
1565
1235
|
let recordedContent;
|
|
1566
1236
|
try {
|
|
1567
|
-
recordedContent =
|
|
1237
|
+
recordedContent = fsSync.readFileSync(snapshotPath, "utf-8");
|
|
1568
1238
|
} catch {
|
|
1569
1239
|
return false;
|
|
1570
1240
|
}
|
|
1571
|
-
const currentResult =
|
|
1241
|
+
const currentResult = _internals6.readEffectiveSpecSync(directory);
|
|
1572
1242
|
if (!currentResult || !currentResult.content) {
|
|
1573
1243
|
return false;
|
|
1574
1244
|
}
|
|
@@ -1598,7 +1268,7 @@ function isObligationPreserving(directory) {
|
|
|
1598
1268
|
return false;
|
|
1599
1269
|
}
|
|
1600
1270
|
}
|
|
1601
|
-
var
|
|
1271
|
+
var _internals6 = {
|
|
1602
1272
|
computeSpecDiff,
|
|
1603
1273
|
computeSpecHash,
|
|
1604
1274
|
isObligationPreserving,
|
|
@@ -1606,19 +1276,19 @@ var _internals4 = {
|
|
|
1606
1276
|
readEffectiveSpecSync
|
|
1607
1277
|
};
|
|
1608
1278
|
function computeSpecDiff(directory) {
|
|
1609
|
-
const snapshotPath =
|
|
1279
|
+
const snapshotPath = join3(directory, ".swarm", "spec-snapshot.md");
|
|
1610
1280
|
let recordedContent;
|
|
1611
1281
|
try {
|
|
1612
|
-
const snapshotSize =
|
|
1282
|
+
const snapshotSize = fsSync.statSync(snapshotPath).size;
|
|
1613
1283
|
const MAX_SNAPSHOT_BYTES = 512 * 1024;
|
|
1614
1284
|
if (snapshotSize > MAX_SNAPSHOT_BYTES) {
|
|
1615
1285
|
return null;
|
|
1616
1286
|
}
|
|
1617
|
-
recordedContent =
|
|
1287
|
+
recordedContent = fsSync.readFileSync(snapshotPath, "utf-8");
|
|
1618
1288
|
} catch {
|
|
1619
1289
|
return null;
|
|
1620
1290
|
}
|
|
1621
|
-
const spec =
|
|
1291
|
+
const spec = _internals6.readEffectiveSpecSync(directory);
|
|
1622
1292
|
if (!spec) {
|
|
1623
1293
|
return null;
|
|
1624
1294
|
}
|
|
@@ -1719,7 +1389,7 @@ var startupLedgerCheckedWorkspaces = new Set;
|
|
|
1719
1389
|
var ledgerStaleWorkspaces = new Set;
|
|
1720
1390
|
var recoveryMutexes = new Map;
|
|
1721
1391
|
var PLAN_JSON_CACHE_NAMESPACE = "plan-json:validated:v1";
|
|
1722
|
-
var
|
|
1392
|
+
var _internals7 = {
|
|
1723
1393
|
loadPlan,
|
|
1724
1394
|
loadPlanJsonOnly,
|
|
1725
1395
|
readPlanJsonUtf8,
|
|
@@ -1731,10 +1401,7 @@ var _internals5 = {
|
|
|
1731
1401
|
loadLastApprovedPlan,
|
|
1732
1402
|
readLedgerEventsWithIntegrity,
|
|
1733
1403
|
regeneratePlanMarkdown,
|
|
1734
|
-
|
|
1735
|
-
isEpicModeActiveForProject,
|
|
1736
|
-
commitTaskCompletion,
|
|
1737
|
-
getWorktreeMergeFailure,
|
|
1404
|
+
epicMergeFailureSkipsCheckpoint,
|
|
1738
1405
|
recordTaskAttempt,
|
|
1739
1406
|
maybeSaveAutoCheckpoint: defaultMaybeSaveAutoCheckpoint
|
|
1740
1407
|
};
|
|
@@ -1813,9 +1480,9 @@ async function retryCasWithBackoff(directory, eventInput, options) {
|
|
|
1813
1480
|
planHashAfter: options.planHashAfter,
|
|
1814
1481
|
preCommitCheck: options.preCommitCheck
|
|
1815
1482
|
});
|
|
1816
|
-
} catch (
|
|
1817
|
-
if (!(
|
|
1818
|
-
throw
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
if (!(error instanceof LedgerStaleWriterError) || attempt >= maxRetries) {
|
|
1485
|
+
throw error;
|
|
1819
1486
|
}
|
|
1820
1487
|
attempt++;
|
|
1821
1488
|
const base = Math.min(CAS_BACKOFF_START_MS * 2 ** (attempt - 1), CAS_BACKOFF_CAP_MS);
|
|
@@ -1826,7 +1493,7 @@ async function retryCasWithBackoff(directory, eventInput, options) {
|
|
|
1826
1493
|
expectedHashPrefix: currentExpected.slice(0, 8),
|
|
1827
1494
|
delayMs
|
|
1828
1495
|
});
|
|
1829
|
-
await new Promise((
|
|
1496
|
+
await new Promise((resolve4) => setTimeout(resolve4, delayMs));
|
|
1830
1497
|
if (options.verifyValid) {
|
|
1831
1498
|
const stillValid = await options.verifyValid();
|
|
1832
1499
|
if (!stillValid)
|
|
@@ -1839,8 +1506,8 @@ async function retryCasWithBackoff(directory, eventInput, options) {
|
|
|
1839
1506
|
async function loadPlanJsonOnly(directory) {
|
|
1840
1507
|
try {
|
|
1841
1508
|
return await parsePlanJsonCached(directory);
|
|
1842
|
-
} catch (
|
|
1843
|
-
warn(`Plan validation failed for .swarm/plan.json: ${
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
warn(`Plan validation failed for .swarm/plan.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
1844
1511
|
}
|
|
1845
1512
|
return null;
|
|
1846
1513
|
}
|
|
@@ -1881,9 +1548,9 @@ async function surfaceLedgerStaleIfPersisted(directory, plan, options) {
|
|
|
1881
1548
|
ledgerStaleWorkspaces.delete(resolvedWorkspace);
|
|
1882
1549
|
return plan;
|
|
1883
1550
|
}
|
|
1884
|
-
} catch (
|
|
1885
|
-
if (
|
|
1886
|
-
throw
|
|
1551
|
+
} catch (error) {
|
|
1552
|
+
if (error instanceof PlanRecoverySupersededError)
|
|
1553
|
+
throw error;
|
|
1887
1554
|
}
|
|
1888
1555
|
plan._ledgerReplayStale = true;
|
|
1889
1556
|
if (typeof plan._ledgerReplayStaleReason !== "string" || plan._ledgerReplayStaleReason.length === 0) {
|
|
@@ -1893,7 +1560,7 @@ async function surfaceLedgerStaleIfPersisted(directory, plan, options) {
|
|
|
1893
1560
|
}
|
|
1894
1561
|
async function parsePlanJsonCached(directory) {
|
|
1895
1562
|
const planJsonPath = path3.resolve(directory, ".swarm", "plan.json");
|
|
1896
|
-
return readCachedParsedFile(planJsonPath, PLAN_JSON_CACHE_NAMESPACE, () =>
|
|
1563
|
+
return readCachedParsedFile(planJsonPath, PLAN_JSON_CACHE_NAMESPACE, () => _internals7.readPlanJsonUtf8(directory), (planJsonContent) => {
|
|
1897
1564
|
if (planJsonContent.includes("\x00")) {
|
|
1898
1565
|
throw new Error("Plan rejected: .swarm/plan.json contains null bytes");
|
|
1899
1566
|
}
|
|
@@ -1902,7 +1569,7 @@ async function parsePlanJsonCached(directory) {
|
|
|
1902
1569
|
});
|
|
1903
1570
|
}
|
|
1904
1571
|
async function readPlanJsonUtf8(directory) {
|
|
1905
|
-
return readSwarmFileAsync(directory, "plan.json", undefined, (filePath) =>
|
|
1572
|
+
return readSwarmFileAsync(directory, "plan.json", undefined, (filePath) => _internals7.readPlanFileUtf8(filePath), false);
|
|
1906
1573
|
}
|
|
1907
1574
|
async function readPlanFileUtf8(filePath) {
|
|
1908
1575
|
return new TextDecoder("utf-8", { fatal: true }).decode(await fsPromises.readFile(filePath));
|
|
@@ -1970,10 +1637,10 @@ ${markdown}`;
|
|
|
1970
1637
|
try {
|
|
1971
1638
|
await bunWrite(mdTempPath, markdownWithHash);
|
|
1972
1639
|
options?.preCommitCheck?.();
|
|
1973
|
-
|
|
1640
|
+
renameSync(mdTempPath, mdPath);
|
|
1974
1641
|
} finally {
|
|
1975
1642
|
try {
|
|
1976
|
-
|
|
1643
|
+
unlinkSync2(mdTempPath);
|
|
1977
1644
|
} catch {}
|
|
1978
1645
|
}
|
|
1979
1646
|
invalidateCachedArtifact(mdPath);
|
|
@@ -1983,9 +1650,9 @@ async function loadPlan(directory, cache, options) {
|
|
|
1983
1650
|
let planJsonContent = null;
|
|
1984
1651
|
let claimedStartupWorkspace = null;
|
|
1985
1652
|
try {
|
|
1986
|
-
planJsonContent = await
|
|
1987
|
-
} catch (
|
|
1988
|
-
warn(`Plan rejected: .swarm/plan.json could not be decoded as valid UTF-8: ${
|
|
1653
|
+
planJsonContent = await _internals7.readPlanJsonUtf8(directory);
|
|
1654
|
+
} catch (error) {
|
|
1655
|
+
warn(`Plan rejected: .swarm/plan.json could not be decoded as valid UTF-8: ${error instanceof Error ? error.message : String(error)}`);
|
|
1989
1656
|
}
|
|
1990
1657
|
if (planJsonContent !== null) {
|
|
1991
1658
|
if (planJsonContent.includes("\x00")) {
|
|
@@ -1999,7 +1666,7 @@ async function loadPlan(directory, cache, options) {
|
|
|
1999
1666
|
const inSync = await isPlanMdInSync(directory, validated, cache);
|
|
2000
1667
|
if (!inSync) {
|
|
2001
1668
|
try {
|
|
2002
|
-
await
|
|
1669
|
+
await _internals7.regeneratePlanMarkdown(directory, validated, {
|
|
2003
1670
|
preCommitCheck: options?.preCommitCheck
|
|
2004
1671
|
});
|
|
2005
1672
|
} catch (regenError) {
|
|
@@ -2115,9 +1782,9 @@ async function loadPlan(directory, cache, options) {
|
|
|
2115
1782
|
changedSections: diffInfo?.changedSections ?? []
|
|
2116
1783
|
}, null, 2), options?.preCommitCheck, "spec-staleness");
|
|
2117
1784
|
invalidateCachedArtifact(specStalenessPath);
|
|
2118
|
-
} catch (
|
|
2119
|
-
if (
|
|
2120
|
-
throw
|
|
1785
|
+
} catch (error) {
|
|
1786
|
+
if (error instanceof PlanRecoverySupersededError)
|
|
1787
|
+
throw error;
|
|
2121
1788
|
}
|
|
2122
1789
|
try {
|
|
2123
1790
|
assertProjectRoot(directory);
|
|
@@ -2132,23 +1799,23 @@ async function loadPlan(directory, cache, options) {
|
|
|
2132
1799
|
};
|
|
2133
1800
|
options?.preCommitCheck?.();
|
|
2134
1801
|
appendCoreEventSync(directory, { ...event });
|
|
2135
|
-
} catch (
|
|
2136
|
-
if (
|
|
2137
|
-
throw
|
|
1802
|
+
} catch (error) {
|
|
1803
|
+
if (error instanceof PlanRecoverySupersededError)
|
|
1804
|
+
throw error;
|
|
2138
1805
|
}
|
|
2139
1806
|
}
|
|
2140
1807
|
}
|
|
2141
1808
|
}
|
|
2142
1809
|
return await surfaceLedgerStaleIfPersisted(directory, validated, options);
|
|
2143
1810
|
}
|
|
2144
|
-
} catch (
|
|
2145
|
-
if (
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
if (error instanceof PlanRecoverySupersededError) {
|
|
2146
1813
|
if (claimedStartupWorkspace !== null) {
|
|
2147
1814
|
startupLedgerCheckedWorkspaces.delete(claimedStartupWorkspace);
|
|
2148
1815
|
}
|
|
2149
|
-
throw
|
|
1816
|
+
throw error;
|
|
2150
1817
|
}
|
|
2151
|
-
warn(`[loadPlan] plan.json validation failed: ${
|
|
1818
|
+
warn(`[loadPlan] plan.json validation failed: ${error instanceof Error ? error.message : String(error)}. Attempting rebuild from ledger. If rebuild fails, check .swarm/plan-export/SWARM_PLAN.md for a checkpoint.`);
|
|
2152
1819
|
let rawPlanId = null;
|
|
2153
1820
|
let rawPlanJsonParseFailed = false;
|
|
2154
1821
|
try {
|
|
@@ -2159,8 +1826,8 @@ async function loadPlan(directory, cache, options) {
|
|
|
2159
1826
|
} catch {
|
|
2160
1827
|
rawPlanJsonParseFailed = true;
|
|
2161
1828
|
}
|
|
2162
|
-
if (await
|
|
2163
|
-
const catchIntegrity = await
|
|
1829
|
+
if (await _internals7.ledgerExists(directory)) {
|
|
1830
|
+
const catchIntegrity = await _internals7.readLedgerEventsWithIntegrity(directory);
|
|
2164
1831
|
const ledgerEventsForCatch = catchIntegrity.events;
|
|
2165
1832
|
const catchFirstEvent = ledgerEventsForCatch.length > 0 ? ledgerEventsForCatch[0] : null;
|
|
2166
1833
|
const identityMatch = rawPlanId === null || catchFirstEvent === null || catchFirstEvent.plan_id === rawPlanId;
|
|
@@ -2169,7 +1836,7 @@ async function loadPlan(directory, cache, options) {
|
|
|
2169
1836
|
} else if (catchFirstEvent !== null && (rawPlanId !== null || rawPlanJsonParseFailed && !catchIntegrity.truncated)) {
|
|
2170
1837
|
let rebuilt = null;
|
|
2171
1838
|
try {
|
|
2172
|
-
rebuilt = await
|
|
1839
|
+
rebuilt = await _internals7.replayFromLedger(directory, {
|
|
2173
1840
|
preCommitCheck: options?.preCommitCheck
|
|
2174
1841
|
});
|
|
2175
1842
|
} catch (replayError) {
|
|
@@ -2186,7 +1853,7 @@ async function loadPlan(directory, cache, options) {
|
|
|
2186
1853
|
return rebuilt;
|
|
2187
1854
|
}
|
|
2188
1855
|
try {
|
|
2189
|
-
const approved = await
|
|
1856
|
+
const approved = await _internals7.loadLastApprovedPlan(directory, catchFirstEvent.plan_id);
|
|
2190
1857
|
if (approved) {
|
|
2191
1858
|
const { removedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, approved.plan, "load_plan_recovery_from_approved_snapshot", "restore from critic-approved snapshot", { preCommitCheck: options?.preCommitCheck });
|
|
2192
1859
|
if (removedCount > 0) {
|
|
@@ -2232,12 +1899,12 @@ async function loadPlan(directory, cache, options) {
|
|
|
2232
1899
|
}
|
|
2233
1900
|
}
|
|
2234
1901
|
}
|
|
2235
|
-
if (await
|
|
1902
|
+
if (await _internals7.ledgerExists(directory)) {
|
|
2236
1903
|
const resolvedDir = canonicalRootKeyFresh(directory);
|
|
2237
1904
|
const existingMutex = recoveryMutexes.get(resolvedDir);
|
|
2238
1905
|
if (existingMutex) {
|
|
2239
1906
|
await existingMutex;
|
|
2240
|
-
const postRecoveryPlan = await
|
|
1907
|
+
const postRecoveryPlan = await _internals7.loadPlanJsonOnly(directory);
|
|
2241
1908
|
if (postRecoveryPlan)
|
|
2242
1909
|
return postRecoveryPlan;
|
|
2243
1910
|
}
|
|
@@ -2249,7 +1916,7 @@ async function loadPlan(directory, cache, options) {
|
|
|
2249
1916
|
try {
|
|
2250
1917
|
let rebuilt = null;
|
|
2251
1918
|
try {
|
|
2252
|
-
rebuilt = await
|
|
1919
|
+
rebuilt = await _internals7.replayFromLedger(directory, {
|
|
2253
1920
|
preCommitCheck: options?.preCommitCheck
|
|
2254
1921
|
});
|
|
2255
1922
|
} catch (replayError) {
|
|
@@ -2268,14 +1935,14 @@ async function loadPlan(directory, cache, options) {
|
|
|
2268
1935
|
return rebuilt;
|
|
2269
1936
|
}
|
|
2270
1937
|
try {
|
|
2271
|
-
const anchorIntegrity = await
|
|
1938
|
+
const anchorIntegrity = await _internals7.readLedgerEventsWithIntegrity(directory);
|
|
2272
1939
|
const anchorEvents = anchorIntegrity.events;
|
|
2273
1940
|
if (anchorEvents.length === 0) {
|
|
2274
1941
|
warn("[loadPlan] Ledger present but no events readable \u2014 refusing approved-snapshot recovery (cannot verify plan identity).");
|
|
2275
1942
|
return null;
|
|
2276
1943
|
}
|
|
2277
1944
|
const expectedPlanId = anchorEvents[0].plan_id;
|
|
2278
|
-
const approved = await
|
|
1945
|
+
const approved = await _internals7.loadLastApprovedPlan(directory, expectedPlanId);
|
|
2279
1946
|
if (approved) {
|
|
2280
1947
|
const approvedPhase = approved.approval && typeof approved.approval === "object" && "phase" in approved.approval ? approved.approval.phase : undefined;
|
|
2281
1948
|
warn(`[loadPlan] Ledger replay returned no plan \u2014 recovered from critic-approved snapshot seq=${approved.seq} timestamp=${approved.timestamp} (approval phase=${approvedPhase ?? "unknown"}). This may roll the plan back to an earlier phase \u2014 verify before continuing.`);
|
|
@@ -2325,7 +1992,7 @@ async function loadPlan(directory, cache, options) {
|
|
|
2325
1992
|
return null;
|
|
2326
1993
|
}
|
|
2327
1994
|
async function savePlanWithAutoAcknowledgedRemovals(directory, plan, source, reason, options) {
|
|
2328
|
-
const existing = await
|
|
1995
|
+
const existing = await _internals7.loadPlanJsonOnly(directory);
|
|
2329
1996
|
const newIds = new Set;
|
|
2330
1997
|
for (const phase of plan.phases) {
|
|
2331
1998
|
for (const task of phase.tasks)
|
|
@@ -2372,14 +2039,14 @@ var PLAN_WRITE_VERIFY_READ_BACKOFF_MS = 200;
|
|
|
2372
2039
|
async function verifyWrittenPlanJson(directory, expected) {
|
|
2373
2040
|
let content;
|
|
2374
2041
|
try {
|
|
2375
|
-
content = await readSwarmFileAsync(directory, "plan.json", undefined, (filePath) =>
|
|
2376
|
-
} catch (
|
|
2377
|
-
throw new PlanWriteVerificationError(`PLAN_WRITE_VERIFICATION_FAILED: plan.json read-back could not be read (${
|
|
2042
|
+
content = await readSwarmFileAsync(directory, "plan.json", undefined, (filePath) => _internals7.readPlanFileUtf8(filePath), false);
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
throw new PlanWriteVerificationError(`PLAN_WRITE_VERIFICATION_FAILED: plan.json read-back could not be read (${error instanceof Error ? error.message : String(error)})`);
|
|
2378
2045
|
}
|
|
2379
2046
|
for (let attempt = 0;content === null && attempt < PLAN_WRITE_VERIFY_READ_RETRIES; attempt++) {
|
|
2380
|
-
await new Promise((
|
|
2047
|
+
await new Promise((resolve4) => setTimeout(resolve4, PLAN_WRITE_VERIFY_READ_BACKOFF_MS * (attempt + 1)));
|
|
2381
2048
|
try {
|
|
2382
|
-
content = await readSwarmFileAsync(directory, "plan.json", undefined, (filePath) =>
|
|
2049
|
+
content = await readSwarmFileAsync(directory, "plan.json", undefined, (filePath) => _internals7.readPlanFileUtf8(filePath), false);
|
|
2383
2050
|
} catch {}
|
|
2384
2051
|
}
|
|
2385
2052
|
if (content === null) {
|
|
@@ -2388,8 +2055,8 @@ async function verifyWrittenPlanJson(directory, expected) {
|
|
|
2388
2055
|
let parsed;
|
|
2389
2056
|
try {
|
|
2390
2057
|
parsed = JSON.parse(content);
|
|
2391
|
-
} catch (
|
|
2392
|
-
throw new PlanWriteVerificationError(`PLAN_WRITE_VERIFICATION_FAILED: plan.json read-back did not parse (${
|
|
2058
|
+
} catch (error) {
|
|
2059
|
+
throw new PlanWriteVerificationError(`PLAN_WRITE_VERIFICATION_FAILED: plan.json read-back did not parse (${error instanceof Error ? error.message : String(error)})`);
|
|
2393
2060
|
}
|
|
2394
2061
|
if (JSON.stringify(parsed) !== JSON.stringify(expected)) {
|
|
2395
2062
|
throw new PlanWriteVerificationError("PLAN_WRITE_VERIFICATION_FAILED: plan.json read-back content differs from the projected plan");
|
|
@@ -2408,10 +2075,10 @@ async function appendMigrationProvenanceEvent(directory, plan, preCommitCheck) {
|
|
|
2408
2075
|
tasks_count: plan.phases.reduce((sum, phase) => sum + phase.tasks.length, 0)
|
|
2409
2076
|
}
|
|
2410
2077
|
}, { preCommitCheck });
|
|
2411
|
-
} catch (
|
|
2412
|
-
if (
|
|
2413
|
-
throw
|
|
2414
|
-
warn(`[loadPlan] Markdown-migration provenance event append failed (plan remains migrated): ${
|
|
2078
|
+
} catch (error) {
|
|
2079
|
+
if (error instanceof PlanRecoverySupersededError)
|
|
2080
|
+
throw error;
|
|
2081
|
+
warn(`[loadPlan] Markdown-migration provenance event append failed (plan remains migrated): ${error instanceof Error ? error.message : String(error)}`);
|
|
2415
2082
|
}
|
|
2416
2083
|
}
|
|
2417
2084
|
async function savePlan(directory, plan, options) {
|
|
@@ -2442,7 +2109,7 @@ async function savePlan(directory, plan, options) {
|
|
|
2442
2109
|
}
|
|
2443
2110
|
if (options?.preserveCompletedStatuses !== false) {
|
|
2444
2111
|
try {
|
|
2445
|
-
const currentPlan2 = await
|
|
2112
|
+
const currentPlan2 = await _internals7.loadPlanJsonOnly(directory);
|
|
2446
2113
|
if (currentPlan2) {
|
|
2447
2114
|
const completedTaskIds = new Set;
|
|
2448
2115
|
for (const phase of currentPlan2.phases) {
|
|
@@ -2465,7 +2132,7 @@ async function savePlan(directory, plan, options) {
|
|
|
2465
2132
|
}
|
|
2466
2133
|
derivePhaseStatusesInPlace(validated);
|
|
2467
2134
|
normalizeCurrentPhaseInPlace(validated);
|
|
2468
|
-
const currentPlan = await
|
|
2135
|
+
const currentPlan = await _internals7.loadPlanJsonOnly(directory);
|
|
2469
2136
|
const planId = derivePlanId(validated);
|
|
2470
2137
|
const planHashForInit = computePlanLedgerHash(validated);
|
|
2471
2138
|
if (!await ledgerExists(directory)) {
|
|
@@ -2504,7 +2171,7 @@ async function savePlan(directory, plan, options) {
|
|
|
2504
2171
|
throw new PlanConcurrentModificationError("RECONCILE_LEDGER_PROJECTION_STALE: the ledger identity or tail changed before the recovery snapshot could be appended.");
|
|
2505
2172
|
}
|
|
2506
2173
|
try {
|
|
2507
|
-
const integrity = await
|
|
2174
|
+
const integrity = await _internals7.readLedgerEventsWithIntegrity(directory);
|
|
2508
2175
|
if (integrity.truncated) {
|
|
2509
2176
|
await replaceTruncatedLedgerWithRecoveryRoot(directory, validated, {
|
|
2510
2177
|
seq: expected.expectedSeq,
|
|
@@ -2518,11 +2185,11 @@ async function savePlan(directory, plan, options) {
|
|
|
2518
2185
|
expectedLedgerHash: expected.expectedLedgerHash
|
|
2519
2186
|
});
|
|
2520
2187
|
}
|
|
2521
|
-
} catch (
|
|
2522
|
-
if (
|
|
2523
|
-
throw new PlanConcurrentModificationError(`RECONCILE_LEDGER_PROJECTION_STALE: ${
|
|
2188
|
+
} catch (error) {
|
|
2189
|
+
if (error instanceof LedgerStaleWriterError) {
|
|
2190
|
+
throw new PlanConcurrentModificationError(`RECONCILE_LEDGER_PROJECTION_STALE: ${error.message}`);
|
|
2524
2191
|
}
|
|
2525
|
-
throw
|
|
2192
|
+
throw error;
|
|
2526
2193
|
}
|
|
2527
2194
|
}
|
|
2528
2195
|
const currentHash = computeCurrentPlanHash(directory);
|
|
@@ -2585,7 +2252,7 @@ async function savePlan(directory, plan, options) {
|
|
|
2585
2252
|
planHashAfter: hashAfter,
|
|
2586
2253
|
preCommitCheck: options?.preCommitCheck,
|
|
2587
2254
|
verifyValid: async () => {
|
|
2588
|
-
const onDisk = await
|
|
2255
|
+
const onDisk = await _internals7.loadPlanJsonOnly(directory);
|
|
2589
2256
|
if (!onDisk)
|
|
2590
2257
|
return true;
|
|
2591
2258
|
for (const p of onDisk.phases) {
|
|
@@ -2596,11 +2263,11 @@ async function savePlan(directory, plan, options) {
|
|
|
2596
2263
|
}
|
|
2597
2264
|
});
|
|
2598
2265
|
}
|
|
2599
|
-
} catch (
|
|
2600
|
-
if (
|
|
2601
|
-
throw new PlanConcurrentModificationError(`Concurrent plan modification detected after retries: ${
|
|
2266
|
+
} catch (error) {
|
|
2267
|
+
if (error instanceof LedgerStaleWriterError) {
|
|
2268
|
+
throw new PlanConcurrentModificationError(`Concurrent plan modification detected after retries: ${error.message}. Please retry the operation.`);
|
|
2602
2269
|
}
|
|
2603
|
-
throw
|
|
2270
|
+
throw error;
|
|
2604
2271
|
}
|
|
2605
2272
|
}
|
|
2606
2273
|
try {
|
|
@@ -2625,7 +2292,7 @@ async function savePlan(directory, plan, options) {
|
|
|
2625
2292
|
planHashAfter: hashAfter,
|
|
2626
2293
|
preCommitCheck: options?.preCommitCheck,
|
|
2627
2294
|
verifyValid: async () => {
|
|
2628
|
-
const onDisk = await
|
|
2295
|
+
const onDisk = await _internals7.loadPlanJsonOnly(directory);
|
|
2629
2296
|
if (!onDisk)
|
|
2630
2297
|
return true;
|
|
2631
2298
|
for (const p of onDisk.phases) {
|
|
@@ -2640,15 +2307,15 @@ async function savePlan(directory, plan, options) {
|
|
|
2640
2307
|
}
|
|
2641
2308
|
}
|
|
2642
2309
|
}
|
|
2643
|
-
} catch (
|
|
2644
|
-
if (
|
|
2645
|
-
throw new PlanConcurrentModificationError(`Concurrent plan modification detected after retries: ${
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
if (error instanceof LedgerStaleWriterError) {
|
|
2312
|
+
throw new PlanConcurrentModificationError(`Concurrent plan modification detected after retries: ${error.message}. Please retry the operation.`);
|
|
2646
2313
|
}
|
|
2647
|
-
throw
|
|
2314
|
+
throw error;
|
|
2648
2315
|
}
|
|
2649
2316
|
}
|
|
2650
2317
|
const ledgerStatusTaskIds = collectLedgerStatusTaskIds(await readLedgerEvents(directory));
|
|
2651
|
-
const replayedBeforeProjection = await
|
|
2318
|
+
const replayedBeforeProjection = await _internals7.replayFromLedger(directory, {
|
|
2652
2319
|
preCommitCheck: options?.preCommitCheck
|
|
2653
2320
|
});
|
|
2654
2321
|
const projectionCandidate = replayedBeforeProjection ? mergeStatusesTakingPrecedence(validated, replayedBeforeProjection, ledgerStatusTaskIds) : validated;
|
|
@@ -2675,14 +2342,14 @@ async function savePlan(directory, plan, options) {
|
|
|
2675
2342
|
try {
|
|
2676
2343
|
await bunWrite(tempPath, JSON.stringify(projectedPlan, null, 2));
|
|
2677
2344
|
options?.preCommitCheck?.();
|
|
2678
|
-
|
|
2345
|
+
renameSync(tempPath, planPath);
|
|
2679
2346
|
} finally {
|
|
2680
2347
|
try {
|
|
2681
|
-
|
|
2348
|
+
unlinkSync2(tempPath);
|
|
2682
2349
|
} catch {}
|
|
2683
2350
|
}
|
|
2684
2351
|
invalidateCachedArtifact(planPath);
|
|
2685
|
-
await
|
|
2352
|
+
await _internals7.verifyWrittenPlanJson(directory, projectedPlan);
|
|
2686
2353
|
try {
|
|
2687
2354
|
const markerPath = path3.join(swarmDir, ".plan-write-marker");
|
|
2688
2355
|
const inProgressMarker = JSON.stringify({
|
|
@@ -2693,9 +2360,9 @@ async function savePlan(directory, plan, options) {
|
|
|
2693
2360
|
in_progress: true
|
|
2694
2361
|
});
|
|
2695
2362
|
await commitAsyncPreparedFile(markerPath, inProgressMarker, options?.preCommitCheck, "plan-write-marker");
|
|
2696
|
-
} catch (
|
|
2697
|
-
if (
|
|
2698
|
-
throw
|
|
2363
|
+
} catch (error) {
|
|
2364
|
+
if (error instanceof PlanRecoverySupersededError)
|
|
2365
|
+
throw error;
|
|
2699
2366
|
}
|
|
2700
2367
|
let mdWriteError;
|
|
2701
2368
|
try {
|
|
@@ -2708,10 +2375,10 @@ ${markdown}`;
|
|
|
2708
2375
|
try {
|
|
2709
2376
|
await bunWrite(mdTempPath, markdownWithHash);
|
|
2710
2377
|
options?.preCommitCheck?.();
|
|
2711
|
-
|
|
2378
|
+
renameSync(mdTempPath, mdPath);
|
|
2712
2379
|
} finally {
|
|
2713
2380
|
try {
|
|
2714
|
-
|
|
2381
|
+
unlinkSync2(mdTempPath);
|
|
2715
2382
|
} catch {}
|
|
2716
2383
|
}
|
|
2717
2384
|
invalidateCachedArtifact(mdPath);
|
|
@@ -2740,9 +2407,9 @@ ${markdown}`;
|
|
|
2740
2407
|
in_progress: false
|
|
2741
2408
|
});
|
|
2742
2409
|
await commitAsyncPreparedFile(markerPath, marker, options?.preCommitCheck, "plan-write-marker");
|
|
2743
|
-
} catch (
|
|
2744
|
-
if (
|
|
2745
|
-
throw
|
|
2410
|
+
} catch (error) {
|
|
2411
|
+
if (error instanceof PlanRecoverySupersededError)
|
|
2412
|
+
throw error;
|
|
2746
2413
|
}
|
|
2747
2414
|
if (currentPlan) {
|
|
2748
2415
|
try {
|
|
@@ -2793,12 +2460,12 @@ async function commitAsyncPreparedFile(targetPath, content, preCommitCheck, temp
|
|
|
2793
2460
|
try {
|
|
2794
2461
|
await bunWrite(tempPath, content);
|
|
2795
2462
|
preCommitCheck?.();
|
|
2796
|
-
|
|
2797
|
-
} catch (
|
|
2463
|
+
renameSync(tempPath, targetPath);
|
|
2464
|
+
} catch (error) {
|
|
2798
2465
|
try {
|
|
2799
|
-
|
|
2466
|
+
unlinkSync2(tempPath);
|
|
2800
2467
|
} catch {}
|
|
2801
|
-
throw
|
|
2468
|
+
throw error;
|
|
2802
2469
|
}
|
|
2803
2470
|
}
|
|
2804
2471
|
async function writeRebuildPlanMarkdown(tempPath, content) {
|
|
@@ -2819,20 +2486,20 @@ async function rebuildPlan(directory, plan, options) {
|
|
|
2819
2486
|
const tempPlanPath = path3.join(swarmDir, `plan.json.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
2820
2487
|
try {
|
|
2821
2488
|
{
|
|
2822
|
-
const fd =
|
|
2489
|
+
const fd = openSync2(tempPlanPath, "w");
|
|
2823
2490
|
try {
|
|
2824
2491
|
writeFileSync(fd, JSON.stringify(targetPlan, null, 2), "utf8");
|
|
2825
2492
|
fsyncSync(fd);
|
|
2826
2493
|
} finally {
|
|
2827
|
-
|
|
2494
|
+
closeSync2(fd);
|
|
2828
2495
|
}
|
|
2829
2496
|
}
|
|
2830
2497
|
options?.preCommitCheck?.();
|
|
2831
|
-
|
|
2498
|
+
renameSync(tempPlanPath, planPath);
|
|
2832
2499
|
invalidateCachedArtifact(planPath);
|
|
2833
2500
|
} finally {
|
|
2834
2501
|
try {
|
|
2835
|
-
|
|
2502
|
+
unlinkSync2(tempPlanPath);
|
|
2836
2503
|
} catch {}
|
|
2837
2504
|
}
|
|
2838
2505
|
try {
|
|
@@ -2845,9 +2512,9 @@ async function rebuildPlan(directory, plan, options) {
|
|
|
2845
2512
|
in_progress: true
|
|
2846
2513
|
});
|
|
2847
2514
|
await commitAsyncPreparedFile(markerPath, inProgressMarker, options?.preCommitCheck, "rebuild");
|
|
2848
|
-
} catch (
|
|
2849
|
-
if (
|
|
2850
|
-
throw
|
|
2515
|
+
} catch (error) {
|
|
2516
|
+
if (error instanceof PlanRecoverySupersededError)
|
|
2517
|
+
throw error;
|
|
2851
2518
|
}
|
|
2852
2519
|
let markdownWriteFailed = false;
|
|
2853
2520
|
let markdownWriteError;
|
|
@@ -2859,18 +2526,18 @@ async function rebuildPlan(directory, plan, options) {
|
|
|
2859
2526
|
${markdown}`;
|
|
2860
2527
|
const tempMdPath = path3.join(swarmDir, `plan.md.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
2861
2528
|
try {
|
|
2862
|
-
await
|
|
2529
|
+
await _internals7.writeRebuildPlanMarkdown(tempMdPath, markdownWithHash);
|
|
2863
2530
|
options?.preCommitCheck?.();
|
|
2864
|
-
|
|
2531
|
+
renameSync(tempMdPath, mdPath);
|
|
2865
2532
|
invalidateCachedArtifact(mdPath);
|
|
2866
2533
|
} finally {
|
|
2867
2534
|
try {
|
|
2868
|
-
|
|
2535
|
+
unlinkSync2(tempMdPath);
|
|
2869
2536
|
} catch {}
|
|
2870
2537
|
}
|
|
2871
|
-
} catch (
|
|
2538
|
+
} catch (error) {
|
|
2872
2539
|
markdownWriteFailed = true;
|
|
2873
|
-
markdownWriteError =
|
|
2540
|
+
markdownWriteError = error;
|
|
2874
2541
|
} finally {
|
|
2875
2542
|
try {
|
|
2876
2543
|
const markerPath = path3.join(swarmDir, ".plan-write-marker");
|
|
@@ -2883,9 +2550,9 @@ ${markdown}`;
|
|
|
2883
2550
|
in_progress: false
|
|
2884
2551
|
});
|
|
2885
2552
|
await commitAsyncPreparedFile(markerPath, marker, options?.preCommitCheck, "rebuild");
|
|
2886
|
-
} catch (
|
|
2887
|
-
if (
|
|
2888
|
-
markerSupersededError =
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
if (error instanceof PlanRecoverySupersededError) {
|
|
2555
|
+
markerSupersededError = error;
|
|
2889
2556
|
}
|
|
2890
2557
|
}
|
|
2891
2558
|
}
|
|
@@ -2911,9 +2578,9 @@ ${markdown}`;
|
|
|
2911
2578
|
tasks_count: targetPlan.phases.reduce((sum, p) => sum + p.tasks.length, 0)
|
|
2912
2579
|
}
|
|
2913
2580
|
}, { planHashAfter, preCommitCheck: options?.preCommitCheck });
|
|
2914
|
-
} catch (
|
|
2915
|
-
if (
|
|
2916
|
-
throw
|
|
2581
|
+
} catch (error) {
|
|
2582
|
+
if (error instanceof PlanRecoverySupersededError)
|
|
2583
|
+
throw error;
|
|
2917
2584
|
}
|
|
2918
2585
|
return targetPlan;
|
|
2919
2586
|
}
|
|
@@ -2958,7 +2625,7 @@ async function closePlanTerminalState(directory, plan, options) {
|
|
|
2958
2625
|
const planPath = path3.join(swarmDir, "plan.json");
|
|
2959
2626
|
const tempPlanPath = path3.join(swarmDir, `plan.json.close.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
2960
2627
|
await bunWrite(tempPlanPath, JSON.stringify(validated, null, 2));
|
|
2961
|
-
|
|
2628
|
+
renameSync(tempPlanPath, planPath);
|
|
2962
2629
|
invalidateCachedArtifact(planPath);
|
|
2963
2630
|
try {
|
|
2964
2631
|
const markerPath = path3.join(swarmDir, ".plan-write-marker");
|
|
@@ -2979,7 +2646,7 @@ async function closePlanTerminalState(directory, plan, options) {
|
|
|
2979
2646
|
${markdown}`;
|
|
2980
2647
|
const mdTempPath = path3.join(swarmDir, `plan.md.close.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
2981
2648
|
await bunWrite(mdTempPath, markdownWithHash);
|
|
2982
|
-
|
|
2649
|
+
renameSync(mdTempPath, mdPath);
|
|
2983
2650
|
invalidateCachedArtifact(mdPath);
|
|
2984
2651
|
} finally {
|
|
2985
2652
|
try {
|
|
@@ -2997,7 +2664,7 @@ ${markdown}`;
|
|
|
2997
2664
|
}
|
|
2998
2665
|
}
|
|
2999
2666
|
async function defaultMaybeSaveAutoCheckpoint(directory, plan) {
|
|
3000
|
-
const { maybeSaveAutoCheckpoint } = await import("./auto-checkpoint-
|
|
2667
|
+
const { maybeSaveAutoCheckpoint } = await import("./auto-checkpoint-qvk04fya.js");
|
|
3001
2668
|
return maybeSaveAutoCheckpoint(directory, plan);
|
|
3002
2669
|
}
|
|
3003
2670
|
async function updateTaskStatus(directory, taskId, status, options) {
|
|
@@ -3015,7 +2682,7 @@ async function updateTaskStatus(directory, taskId, status, options) {
|
|
|
3015
2682
|
return "pending";
|
|
3016
2683
|
};
|
|
3017
2684
|
{
|
|
3018
|
-
const currentPlan = await
|
|
2685
|
+
const currentPlan = await _internals7.loadPlanJsonOnly(directory);
|
|
3019
2686
|
if (currentPlan) {
|
|
3020
2687
|
const currentTask = currentPlan.phases.flatMap((p) => p.tasks).find((t) => t.id === taskId);
|
|
3021
2688
|
const settled = currentTask && currentTask.status !== "pending" && currentTask.status !== "in_progress";
|
|
@@ -3028,7 +2695,7 @@ async function updateTaskStatus(directory, taskId, status, options) {
|
|
|
3028
2695
|
}
|
|
3029
2696
|
const MAX_OUTER_RETRIES = 1;
|
|
3030
2697
|
for (let attempt = 0;attempt <= MAX_OUTER_RETRIES; attempt++) {
|
|
3031
|
-
const plan = await
|
|
2698
|
+
const plan = await _internals7.loadPlan(directory);
|
|
3032
2699
|
if (plan === null) {
|
|
3033
2700
|
throw new Error(`Plan not found in directory: ${directory}`);
|
|
3034
2701
|
}
|
|
@@ -3059,7 +2726,7 @@ async function updateTaskStatus(directory, taskId, status, options) {
|
|
|
3059
2726
|
if (status === "completed" || status === "blocked") {
|
|
3060
2727
|
const recordedTask = updatedPlan.phases.flatMap((phase) => phase.tasks).find((candidate) => candidate.id === taskId);
|
|
3061
2728
|
try {
|
|
3062
|
-
await
|
|
2729
|
+
await _internals7.recordTaskAttempt(directory, {
|
|
3063
2730
|
taskId,
|
|
3064
2731
|
agent: "plan-status",
|
|
3065
2732
|
outcome: status === "completed" ? "pass" : "fail",
|
|
@@ -3073,7 +2740,7 @@ async function updateTaskStatus(directory, taskId, status, options) {
|
|
|
3073
2740
|
if (status === "completed") {
|
|
3074
2741
|
try {
|
|
3075
2742
|
const completedTask = updatedPlan.phases.flatMap((phase) => phase.tasks).find((candidate) => candidate.id === taskId);
|
|
3076
|
-
const { maybePropagateSpeckitCheckoff } = await import("./speckit-checkoff-
|
|
2743
|
+
const { maybePropagateSpeckitCheckoff } = await import("./speckit-checkoff-k5jm0abh.js");
|
|
3077
2744
|
await maybePropagateSpeckitCheckoff(directory, {
|
|
3078
2745
|
taskId,
|
|
3079
2746
|
frRefs: completedTask?.fr_refs ?? [],
|
|
@@ -3083,35 +2750,9 @@ async function updateTaskStatus(directory, taskId, status, options) {
|
|
|
3083
2750
|
warn(`[plan/manager] speckit check-off for ${taskId} failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
3084
2751
|
}
|
|
3085
2752
|
}
|
|
3086
|
-
if (status === "completed" &&
|
|
3087
|
-
const mergeFailure = _internals5.getWorktreeMergeFailure(taskId);
|
|
3088
|
-
if (mergeFailure) {
|
|
3089
|
-
criticalWarn(`[plan/manager] Rule 2 auto-commit SKIPPED for ${taskId}: worktree merge-back ${mergeFailure.outcome} at stage '${mergeFailure.stage}'. The task's changes are NOT in the main tree, so no completion marker is written (Rule 3 must not treat this task as satisfied). Resolve the preserved worktree, then re-run the task. Detail: ${mergeFailure.message}`);
|
|
3090
|
-
return updatedPlan;
|
|
3091
|
-
}
|
|
3092
|
-
try {
|
|
3093
|
-
let taskDescription;
|
|
3094
|
-
for (const phase of updatedPlan.phases) {
|
|
3095
|
-
const found = phase.tasks.find((t) => t.id === taskId);
|
|
3096
|
-
if (found) {
|
|
3097
|
-
taskDescription = found.description;
|
|
3098
|
-
break;
|
|
3099
|
-
}
|
|
3100
|
-
}
|
|
3101
|
-
const { readDeclaredScopeFilesFromBindings } = await import("./scope-persistence-7qtsmx5c.js");
|
|
3102
|
-
const canonicalScope = readDeclaredScopeFilesFromBindings({
|
|
3103
|
-
directory,
|
|
3104
|
-
taskId,
|
|
3105
|
-
plan: updatedPlan
|
|
3106
|
-
});
|
|
3107
|
-
await _internals5.commitTaskCompletion(directory, taskId, taskDescription, canonicalScope ?? undefined);
|
|
3108
|
-
} catch (commitErr) {
|
|
3109
|
-
criticalWarn(`[plan/manager] Rule 2 auto-commit for ${taskId} threw (non-fatal): ${commitErr instanceof Error ? commitErr.message : String(commitErr)}`);
|
|
3110
|
-
}
|
|
3111
|
-
}
|
|
3112
|
-
if (status === "completed") {
|
|
2753
|
+
if (status === "completed" && !_internals7.epicMergeFailureSkipsCheckpoint(directory, taskId)) {
|
|
3113
2754
|
try {
|
|
3114
|
-
const outcome = await
|
|
2755
|
+
const outcome = await _internals7.maybeSaveAutoCheckpoint(directory, updatedPlan);
|
|
3115
2756
|
if (outcome && outcome.saved === false && outcome.warning) {
|
|
3116
2757
|
criticalWarn(`[plan/manager] auto-checkpoint for ${taskId} was not saved (non-fatal): ${outcome.warning}`);
|
|
3117
2758
|
}
|
|
@@ -3120,11 +2761,11 @@ async function updateTaskStatus(directory, taskId, status, options) {
|
|
|
3120
2761
|
}
|
|
3121
2762
|
}
|
|
3122
2763
|
return updatedPlan;
|
|
3123
|
-
} catch (
|
|
3124
|
-
if (
|
|
2764
|
+
} catch (error) {
|
|
2765
|
+
if (error instanceof PlanConcurrentModificationError && attempt < MAX_OUTER_RETRIES) {
|
|
3125
2766
|
continue;
|
|
3126
2767
|
}
|
|
3127
|
-
throw
|
|
2768
|
+
throw error;
|
|
3128
2769
|
}
|
|
3129
2770
|
}
|
|
3130
2771
|
throw new Error("updateTaskStatus: unexpected loop exit");
|
|
@@ -3439,4 +3080,4 @@ function migrateLegacyPlan(planContent, swarmId) {
|
|
|
3439
3080
|
return plan;
|
|
3440
3081
|
}
|
|
3441
3082
|
|
|
3442
|
-
export {
|
|
3083
|
+
export { EPIC_MODE_CONFIG_DISABLED_MESSAGE, isEpicModeConfigEnabledForDirectory, epicSentinelExists, computeEpicKey, planIdentityOf, readLedgerRootDigest, inspectEpic, getOpenEpic, isEpicOpenForProject, createEpicRecord, updateEpicRecord, readPlanTaskRef, markEpicClosing, recordEpicBranch, recordEpicLandingAttempt, deleteEpicState, repairEpicSentinel, buildTaskCompletionDescriptor, ensureTaskCheckpointReceipt, updateTaskCheckpointReceipt, relevantMergeFailureForProject, describeMergeFailuresForStatus, clearMergeFailureCommand, getRunMemorySummary, PlanRecoverySupersededError, loadPlanJsonOnly, loadPlan, withPlanLifecycleLock, savePlan, closePlanTerminalState, updateTaskStatus, derivePlanMarkdown, getCurrentTaskId };
|