opencode-swarm 7.189.0 → 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.
Files changed (177) hide show
  1. package/README.md +42 -6
  2. package/binaries/win32-arm64/swarm-sandbox-runner.exe +0 -0
  3. package/binaries/win32-x64/swarm-sandbox-runner.exe +0 -0
  4. package/dist/agents/architect.d.ts +1 -1
  5. package/dist/background/completion-observer.d.ts +6 -0
  6. package/dist/cli/{auto-checkpoint-m7c4x8bk.js → auto-checkpoint-qvk04fya.js} +9 -9
  7. package/dist/cli/{checkpoint-m7q9886q.js → checkpoint-9q2r5c3j.js} +18 -20
  8. package/dist/cli/{coder-settlement-hh81z2f7.js → coder-settlement-xcparbf3.js} +13 -13
  9. package/dist/cli/{config-doctor-0rexy3xt.js → config-doctor-by0ak49m.js} +23 -20
  10. package/dist/cli/{core-ays6stz4.js → core-b1b3kxzy.js} +4 -4
  11. package/dist/cli/{curation-policy-65ckmmd5.js → curation-policy-6p6fcfjb.js} +6 -6
  12. package/dist/cli/{curator-dfzkfgs0.js → curator-d7b88mzr.js} +44 -44
  13. package/dist/cli/{curator-drift-nts5atks.js → curator-drift-6v604220.js} +9 -9
  14. package/dist/cli/curator-llm-factory-yvbktznk.js +82 -0
  15. package/dist/cli/{doctor-knowledge-receipt-phase-id-ec40k5hn.js → doctor-knowledge-receipt-phase-id-kwrvnps9.js} +9 -9
  16. package/dist/cli/{evidence-summary-service-vgtm72g5.js → evidence-summary-service-mzr2q4bn.js} +21 -22
  17. package/dist/cli/{gate-evidence-vmn65kxe.js → gate-evidence-78jes9kq.js} +8 -8
  18. package/dist/cli/guardrail-explain-8kt2mxmw.js +83 -0
  19. package/dist/cli/{guardrail-log-hc8fjm4x.js → guardrail-log-2sthykag.js} +8 -8
  20. package/dist/cli/{guardrail-reset-ct00w300.js → guardrail-reset-2n746rzv.js} +44 -44
  21. package/dist/cli/{hive-promoter-2xax6van.js → hive-promoter-s7a5gwhz.js} +44 -44
  22. package/dist/cli/{index-cbqkag7q.js → index-0qc7a88d.js} +3 -3
  23. package/dist/cli/{index-g03m5jhw.js → index-1s95jrgc.js} +3 -3
  24. package/dist/cli/{index-9tvjwv01.js → index-2fpv2wmc.js} +1 -1
  25. package/dist/cli/{index-2btybj8w.js → index-2kpqqcch.js} +4 -4
  26. package/dist/cli/{index-3dx0c7ve.js → index-3t9t9q2q.js} +44 -44
  27. package/dist/cli/{index-8q8783g0.js → index-3zwhxk98.js} +1 -1
  28. package/dist/cli/{index-34vsyj15.js → index-49e65y51.js} +5 -5
  29. package/dist/cli/{index-5v09zw1q.js → index-69tgdm9t.js} +18 -8
  30. package/dist/cli/{index-d3rjvm9e.js → index-7vnr5cmp.js} +2 -2
  31. package/dist/cli/{index-twb3f4tk.js → index-8bx6vhzk.js} +1 -1
  32. package/dist/cli/{index-nhsafbrh.js → index-9gcqmgbc.js} +5 -5
  33. package/dist/cli/{index-4p3jh14t.js → index-9xxqdzp4.js} +4 -4
  34. package/dist/cli/{index-qx3g1bbe.js → index-a5ca62vt.js} +1 -1
  35. package/dist/cli/{index-bskex45z.js → index-ajzgeb2s.js} +2 -2
  36. package/dist/cli/{index-8xabhfwr.js → index-b2j231jy.js} +2 -2
  37. package/dist/cli/{index-cj4mp0fm.js → index-dmq7p8s9.js} +6 -6
  38. package/dist/cli/{index-bzbxn24k.js → index-ej1hdw16.js} +5 -5
  39. package/dist/cli/{index-waxvpwbv.js → index-ejk34dnw.js} +127 -13
  40. package/dist/cli/{index-skdpgjar.js → index-fb5s32c2.js} +862 -1221
  41. package/dist/cli/{index-ntda6tje.js → index-feaxsjrt.js} +10 -2
  42. package/dist/cli/{index-2nexf5vz.js → index-ffgcdf49.js} +22147 -15396
  43. package/dist/cli/{index-kqegvngt.js → index-h0xhec3d.js} +4 -4
  44. package/dist/cli/{index-kaqdzhj3.js → index-kfevm0n5.js} +2 -2
  45. package/dist/cli/{index-4a7j5f60.js → index-kp8k95v9.js} +4 -4
  46. package/dist/cli/{index-x1eh2636.js → index-mcv0v2dp.js} +1 -1
  47. package/dist/cli/{index-9czp2dcg.js → index-mjfpaxw3.js} +7 -7
  48. package/dist/cli/{index-cdjxzs2r.js → index-mp1s7b0n.js} +7 -7
  49. package/dist/cli/{index-z29hksyw.js → index-mseerehd.js} +26 -10
  50. package/dist/cli/{index-f6exny9m.js → index-n8q43eff.js} +2 -2
  51. package/dist/cli/{index-7hr5m7yp.js → index-nkgzkt7c.js} +1 -1
  52. package/dist/cli/{index-9j3ybyfs.js → index-npe10823.js} +7 -7
  53. package/dist/cli/{index-16m2m4ah.js → index-p69k8v0e.js} +1 -1
  54. package/dist/cli/{index-p2tqzwh6.js → index-qs9myayh.js} +5 -10
  55. package/dist/cli/{index-hh7pa3yk.js → index-r9yvpr2m.js} +2 -2
  56. package/dist/cli/{index-w4zscz6p.js → index-rgkzcw2a.js} +5 -5
  57. package/dist/cli/{index-zfbxfm2m.js → index-s8b67e6h.js} +33 -12
  58. package/dist/cli/{index-9gqb5nkj.js → index-s92a5ta3.js} +4 -4
  59. package/dist/cli/{index-89f56ej4.js → index-sm01zpqe.js} +46 -46
  60. package/dist/cli/{index-q9kefh33.js → index-sxj143a4.js} +1 -1
  61. package/dist/cli/{index-1cwmj1gs.js → index-t0gj8s46.js} +77 -16
  62. package/dist/cli/{index-0gejy4cx.js → index-t73adn9b.js} +2 -2
  63. package/dist/cli/{index-fbgjj7h5.js → index-tkhenmms.js} +101 -13
  64. package/dist/cli/{index-zsrtt42s.js → index-v3bmwmqw.js} +25 -1
  65. package/dist/cli/{index-wqa31xnf.js → index-vhn4e6rm.js} +7 -7
  66. package/dist/cli/{index-5cfhwd65.js → index-xyvrfram.js} +2 -2
  67. package/dist/cli/{index-k8sarme1.js → index-y9v41185.js} +1 -1
  68. package/dist/cli/{index-1xa8wq76.js → index-z3ahy9c2.js} +2 -2
  69. package/dist/cli/{index-wrdktgpy.js → index-zk6ttjkt.js} +2 -2
  70. package/dist/cli/{index-mfxef20k.js → index-zyg1m9d0.js} +4 -4
  71. package/dist/cli/index.js +45 -45
  72. package/dist/cli/{knowledge-escalator-v3c8w86g.js → knowledge-escalator-n1ezv1zp.js} +15 -15
  73. package/dist/cli/{knowledge-events-kagaccj9.js → knowledge-events-nyqk3mr7.js} +13 -13
  74. package/dist/cli/{knowledge-link-h4pdcnya.js → knowledge-link-6hw3z6wn.js} +5 -5
  75. package/dist/cli/{knowledge-store-ywxjk5pb.js → knowledge-store-mnxqcyet.js} +6 -6
  76. package/dist/cli/{knowledge-validator-210gkpec.js → knowledge-validator-z30113mp.js} +8 -8
  77. package/dist/cli/{mcp-58hgx6av.js → mcp-mnbqw2s5.js} +1 -1
  78. package/dist/cli/{model-preflight-e69b48n0.js → model-preflight-ex482fsy.js} +44 -44
  79. package/dist/cli/{pending-delegations-g24d3qa9.js → pending-delegations-j18afz5z.js} +12 -12
  80. package/dist/cli/{pr-subscriptions-qprh2zsm.js → pr-subscriptions-yngda8mz.js} +12 -12
  81. package/dist/cli/{pr-workflow-gate-pj7dhkv4.js → pr-workflow-gate-0vt614f0.js} +44 -44
  82. package/dist/cli/{runner-4hfmp1ag.js → runner-ssnsje35.js} +6 -6
  83. package/dist/cli/{scan-cursor-s5wsxxd1.js → scan-cursor-8pwfg5g9.js} +7 -7
  84. package/dist/cli/{schema-smgngq9m.js → schema-e58ytmqt.js} +14 -2
  85. package/dist/cli/{scope-persistence-tbv2ar4w.js → scope-persistence-9qfx6ext.js} +16 -16
  86. package/dist/cli/{server-7p2ds5w2.js → server-wxcfwsdx.js} +45 -45
  87. package/dist/cli/{skill-generator-r4hsne71.js → skill-generator-tbd4tq9r.js} +17 -17
  88. package/dist/cli/{snapshot-coordination-init-ydjwh56d.js → snapshot-coordination-init-sckyzb94.js} +44 -44
  89. package/dist/cli/{speckit-checkoff-aj75vmjq.js → speckit-checkoff-k5jm0abh.js} +5 -5
  90. package/dist/cli/{telemetry-kvhe94gs.js → telemetry-9szfdfqv.js} +1 -1
  91. package/dist/cli/{workspace-snapshot-30kdf018.js → workspace-snapshot-9f12p1s1.js} +3 -3
  92. package/dist/cli/{worktree-collision-ownership-0bxj9f0t.js → worktree-collision-ownership-3wwfc72t.js} +14 -14
  93. package/dist/cli/{worktree-isolation-h0c22pm2.js → worktree-isolation-zfk7zk6a.js} +46 -44
  94. package/dist/commands/coupling.d.ts +21 -4
  95. package/dist/commands/epic-learning.d.ts +28 -0
  96. package/dist/commands/epic.d.ts +81 -36
  97. package/dist/commands/registry.d.ts +8 -8
  98. package/dist/commands/turbo.d.ts +16 -1
  99. package/dist/config/constants.d.ts +11 -1
  100. package/dist/config/loader.d.ts +2 -0
  101. package/dist/config/schema.d.ts +291 -77
  102. package/dist/db/coordination-store.d.ts +6 -0
  103. package/dist/epic/close.d.ts +209 -0
  104. package/dist/{turbo/epic → epic}/cochange-conflict.d.ts +5 -4
  105. package/dist/{turbo/epic → epic}/cochange-source.d.ts +10 -1
  106. package/dist/epic/components.d.ts +238 -0
  107. package/dist/epic/config-gate.d.ts +25 -0
  108. package/dist/epic/config.d.ts +101 -0
  109. package/dist/{turbo/epic → epic}/coupling-report.d.ts +3 -1
  110. package/dist/epic/declared-scopes.d.ts +37 -0
  111. package/dist/epic/epic-branch.d.ts +157 -0
  112. package/dist/epic/gate-policy.d.ts +114 -0
  113. package/dist/epic/git-once.d.ts +26 -0
  114. package/dist/epic/learning-store.d.ts +176 -0
  115. package/dist/epic/learning.d.ts +198 -0
  116. package/dist/epic/legacy-migration.d.ts +26 -0
  117. package/dist/epic/lifecycle.d.ts +437 -0
  118. package/dist/epic/markers.d.ts +123 -0
  119. package/dist/epic/merge-epoch.d.ts +90 -0
  120. package/dist/epic/next-wave-format.d.ts +101 -0
  121. package/dist/epic/next-wave.d.ts +102 -0
  122. package/dist/epic/phase-readiness.d.ts +221 -0
  123. package/dist/epic/plan-key.d.ts +104 -0
  124. package/dist/epic/plan-shaping-seam.d.ts +112 -0
  125. package/dist/epic/planning-signals.d.ts +38 -0
  126. package/dist/epic/report.d.ts +152 -0
  127. package/dist/epic/residue-commit.d.ts +151 -0
  128. package/dist/epic/scorecard.d.ts +130 -0
  129. package/dist/epic/shaping-sizing.d.ts +108 -0
  130. package/dist/epic/shaping-suggestions.d.ts +172 -0
  131. package/dist/epic/shaping.d.ts +112 -0
  132. package/dist/epic/sizing.d.ts +60 -0
  133. package/dist/epic/start.d.ts +187 -0
  134. package/dist/epic/task-landing.d.ts +83 -0
  135. package/dist/epic/wave-close.d.ts +112 -0
  136. package/dist/epic/wave-select.d.ts +95 -0
  137. package/dist/evaluation/contracts.d.ts +1 -1
  138. package/dist/evaluation/public-api.d.ts +1 -1
  139. package/dist/full-auto/policy.d.ts +12 -1
  140. package/dist/hooks/delegation-gate/worktree-isolation.d.ts +16 -0
  141. package/dist/hooks/delegation-gate.d.ts +11 -0
  142. package/dist/hooks/guardrails/index.d.ts +7 -0
  143. package/dist/hooks/promotion-evidence-store.d.ts +2 -2
  144. package/dist/index.js +814 -807
  145. package/dist/plan/manager.d.ts +6 -8
  146. package/dist/plan/parallel-verdict.d.ts +13 -2
  147. package/dist/retention/caps.d.ts +1 -1
  148. package/dist/review/contracts.d.ts +8 -1
  149. package/dist/services/config-doctor.d.ts +38 -0
  150. package/dist/session/snapshot-reader.d.ts +2 -0
  151. package/dist/session/snapshot-writer.d.ts +13 -2
  152. package/dist/state.d.ts +13 -18
  153. package/dist/tools/epic-next-wave.d.ts +20 -0
  154. package/dist/tools/epic-phase-review.d.ts +43 -0
  155. package/dist/tools/index.d.ts +2 -3
  156. package/dist/tools/manifest.d.ts +2 -3
  157. package/dist/tools/phase-complete.d.ts +5 -1
  158. package/dist/tools/save-plan.d.ts +7 -0
  159. package/dist/tools/tool-metadata.d.ts +4 -8
  160. package/dist/worktree/merge.d.ts +14 -1
  161. package/opencode-swarm.schema.json +273 -28
  162. package/package.json +1 -1
  163. package/dist/cli/curator-llm-factory-nrn6n2p1.js +0 -82
  164. package/dist/cli/guardrail-explain-9jmr8ezt.js +0 -83
  165. package/dist/tools/epic-plan-waves.d.ts +0 -79
  166. package/dist/tools/epic-record-divergence.d.ts +0 -73
  167. package/dist/tools/epic-run-phase.d.ts +0 -179
  168. package/dist/turbo/epic/activation.d.ts +0 -193
  169. package/dist/turbo/epic/calibration-engine.d.ts +0 -88
  170. package/dist/turbo/epic/calibration.d.ts +0 -80
  171. package/dist/turbo/epic/divergence-recorder.d.ts +0 -123
  172. package/dist/turbo/epic/index.d.ts +0 -24
  173. package/dist/turbo/epic/promotion-evidence.d.ts +0 -42
  174. package/dist/turbo/epic/state.d.ts +0 -69
  175. package/dist/turbo/epic/task-commit.d.ts +0 -110
  176. package/dist/turbo/epic/upstream-commits.d.ts +0 -82
  177. 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-16m2m4ah.js";
7
+ } from "./index-p69k8v0e.js";
8
8
  import {
9
- getWorktreeMergeFailure
10
- } from "./index-zsrtt42s.js";
11
- import {
12
- GitBinaryMissingError,
13
- GitSpawnCwdError,
14
- isGitBinaryMissing,
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-7hr5m7yp.js";
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-bzbxn24k.js";
46
+ } from "./index-ej1hdw16.js";
51
47
  import {
52
48
  appendCoreEventSync
53
- } from "./index-wrdktgpy.js";
49
+ } from "./index-zk6ttjkt.js";
54
50
  import {
55
- getCoordinationState,
56
- importCoordinationOnce,
51
+ deleteCoordinationStateWithinTransaction,
52
+ getCoordinationStateRaw,
53
+ listCoordinationStateKeys,
57
54
  listCoordinationStates,
58
- transitionCoordinationState
59
- } from "./index-ntda6tje.js";
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-9tvjwv01.js";
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-d3rjvm9e.js";
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-p2tqzwh6.js";
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
- GIT_BINARY_ENV_VAR,
94
- describeGitResolution,
95
- init_git_executable,
96
- resolveGitExecutable
97
- } from "./index-zxdawwc7.js";
92
+ init_config,
93
+ init_loader,
94
+ isEpicModeConfigEnabled,
95
+ loadPluginConfigWithMeta
96
+ } from "./index-ejk34dnw.js";
98
97
  import {
99
- init_executor,
100
- isUntrustedEnvKey,
101
- isValidEnvKey
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 as renameSync2,
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/git/branch.ts
303
- init_executor();
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/git/safe-ref.ts
311
- class UnsafeGitRefError extends Error {
312
- ref;
313
- context;
314
- constructor(ref, context) {
315
- super(`Refused unsafe git ref "${ref}" at ${context}: a ref argument ` + 'starting with "-" can be parsed by git as an option (issue #2265/#2476)');
316
- this.ref = ref;
317
- this.context = context;
318
- this.name = "UnsafeGitRefError";
319
- }
320
- }
321
- function isSafeGitRefArg(value) {
322
- if (value.length === 0)
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/git/branch.ts
338
- var GIT_TIMEOUT_MS = 30000;
339
- var GIT_MAX_BUFFER_BYTES = 5 * 1024 * 1024;
340
- var MAX_RENDERED_GIT_CANDIDATES = 10;
341
- function errorMessage(err) {
342
- return err instanceof Error ? err.message : String(err);
343
- }
344
- function isNotGitRepositoryMessage(message) {
345
- const lower = message.toLowerCase();
346
- return lower.includes("not a git repository") || lower.includes("not a git repo");
347
- }
348
- function getChildProcessSpawnSync() {
349
- return child_process.spawnSync;
350
- }
351
- var __spawnSyncSeam = {
352
- spawnSync: (cmd, args, options) => {
353
- const mergedEnv = mergeEnvForChild(options?.env, options?.envOverrides);
354
- return getChildProcessSpawnSync()(cmd, args, {
355
- ...options,
356
- env: mergedEnv
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
- function readLaneEnvFileFromDiskSync(worktreePath, laneIndex) {
361
- const envPath = path.join(worktreePath, ".swarm", "lanes", `${laneIndex}.env`);
362
- let content;
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
- content = fsSync.readFileSync(envPath, "utf-8");
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
- const result = {};
369
- for (const rawLine of content.split(`
370
- `)) {
371
- const line = rawLine.trim();
372
- if (!line || line.startsWith("#"))
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
- lines.push(resolution.overrideValue ? `The configured override (${resolution.overrideSource ?? "unknown"}) "${resolution.overrideValue}" did not work \u2014 point the ${GIT_BINARY_ENV_VAR} environment variable, or the "git.binary" value in your USER config (a project .opencode config is ignored for this key), at a working git executable.` : `No override is configured. Set the ${GIT_BINARY_ENV_VAR} environment variable, or the "git.binary" value in your USER config (a project .opencode config is ignored for this key), to point at a working git executable.`);
405
- return lines.join(`
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 gitExec(args, cwd, laneEnv, laneIndex) {
409
- const resolvedLaneEnv = laneEnv ?? (laneIndex !== undefined ? readLaneEnvFileFromDiskSync(cwd, laneIndex) : undefined);
410
- const subcommand = args[0];
411
- const hardenedArgs = subcommand === "commit" || subcommand === "tag" ? ["-c", "commit.gpgsign=false", "-c", "tag.gpgsign=false", ...args] : args;
412
- const command = _internals.resolveGitExecutable();
413
- for (let attempt = 0;attempt < MAX_TRANSIENT_RETRIES; attempt++) {
414
- const result = _internals.spawnSync(command, hardenedArgs, {
415
- cwd,
416
- encoding: "utf-8",
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 getGitRepositoryStatus(cwd) {
540
+ function parseEpicRecord(payload) {
541
+ let raw;
454
542
  try {
455
- gitExec(["rev-parse", "--git-dir"], cwd);
456
- return { isRepo: true };
457
- } catch (err) {
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 isGitRepo(cwd) {
474
- return getGitRepositoryStatus(cwd).isRepo;
475
- }
476
- function getCurrentBranch(cwd, laneEnv, laneIndex) {
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
- gitExec(["rev-parse", "--verify", "origin/main"], cwd);
483
- return "origin/main";
559
+ const parsed = sentinelSchema.safeParse(JSON.parse(text));
560
+ return parsed.success ? parsed.data : null;
484
561
  } catch {
485
- try {
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 hasUncommittedChanges(cwd, laneEnv, laneIndex) {
494
- const status = gitExec(["status", "--porcelain"], cwd, laneEnv, laneIndex);
495
- return status.trim().length > 0;
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 detectDefaultRemoteBranch(cwd) {
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
- const output = gitExec(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd);
500
- const trimmed = output.trim();
501
- if (trimmed.startsWith("refs/remotes/origin/")) {
502
- return trimmed.slice("refs/remotes/origin/".length);
503
- }
504
- } catch {}
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 output = gitExec(["config", "init.defaultBranch"], cwd);
507
- const branch = output.trim();
508
- if (branch) {
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
- } catch {}
512
- try {
513
- gitExec(["rev-parse", "--verify", "origin/main"], cwd);
514
- return "main";
515
- } catch {}
516
- try {
517
- gitExec(["rev-parse", "--verify", "origin/master"], cwd);
518
- return "master";
519
- } catch {
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
- async function resetToRemoteBranch(cwd, options) {
524
- const warnings = [];
525
- const prunedBranches = [];
661
+ function isEpicOpenForProject(directory) {
526
662
  try {
527
- const currentBranch = getCurrentBranch(cwd);
528
- const defaultRemoteBranch = _internals.detectDefaultRemoteBranch(cwd);
529
- if (!defaultRemoteBranch) {
530
- return {
531
- success: false,
532
- targetBranch: "",
533
- localBranch: currentBranch,
534
- message: "Could not detect default remote branch",
535
- alreadyAligned: false,
536
- prunedBranches: [],
537
- warnings: []
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
- assertSafeGitRefArg(currentBranch, "resetToRemoteBranch currentBranch");
541
- assertSafeGitRefArg(defaultRemoteBranch, "resetToRemoteBranch defaultRemoteBranch");
542
- const targetBranch = `origin/${defaultRemoteBranch}`;
543
- if (currentBranch === "HEAD") {
544
- return {
545
- success: false,
546
- targetBranch,
547
- localBranch: "HEAD",
548
- message: "Cannot reset: detached HEAD state",
549
- alreadyAligned: false,
550
- prunedBranches: [],
551
- warnings: []
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
- if (hasUncommittedChanges(cwd)) {
555
- return {
556
- success: false,
557
- targetBranch,
558
- localBranch: currentBranch,
559
- message: "Cannot reset: uncommitted changes in working tree",
560
- alreadyAligned: false,
561
- prunedBranches: [],
562
- warnings: []
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
- try {
566
- const logOutput = gitExec(["log", `${targetBranch}..HEAD`, "--oneline"], cwd);
567
- if (logOutput.trim().length > 0) {
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
- success: false,
570
- targetBranch,
571
- localBranch: currentBranch,
572
- message: "Cannot reset: unpushed commits",
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
- const headSha = gitExec(["rev-parse", "HEAD"], cwd).trim();
593
- const remoteSha = gitExec(["rev-parse", `${targetBranch}`], cwd).trim();
594
- if (headSha === remoteSha) {
595
- return {
596
- success: true,
597
- targetBranch,
598
- localBranch: currentBranch,
599
- message: "Already aligned with remote",
600
- alreadyAligned: true,
601
- prunedBranches: [],
602
- warnings: []
603
- };
604
- }
605
- try {
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
- let resetSucceeded = false;
619
- let lastError;
620
- for (let retry = 0;retry < 4; retry++) {
621
- if (retry > 0 && process.platform === "win32") {
622
- await new Promise((resolve) => setTimeout(resolve, 500));
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
- try {
625
- gitExec(["reset", "--hard", targetBranch], cwd);
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
- if (!resetSucceeded) {
633
- return {
634
- success: false,
635
- targetBranch,
636
- localBranch: currentBranch,
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
- success: true,
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
- var GITIGNORED_BUILD_ARTIFACTS = ["dist"];
719
- async function resetToMainAfterMerge(cwd, options) {
720
- const warnings = [];
721
- try {
722
- const defaultBranch = _internals.detectDefaultRemoteBranch(cwd);
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
- _internals.gitExec(["fetch", "--prune", "origin"], cwd);
797
- } catch (err) {
798
- return {
799
- success: false,
800
- targetBranch,
801
- previousBranch: currentBranch,
802
- message: `Cannot reset: fetch failed \u2014 ${err instanceof Error ? err.message : String(err)}`,
803
- branchDeleted: false,
804
- changesDiscarded: false,
805
- warnings
806
- };
807
- }
808
- const previousBranch = currentBranch;
809
- let switchedBranch = false;
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
- _internals.gitExec(["reset", "--hard", targetBranch], cwd);
828
- } catch (err) {
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
- let changesDiscarded = false;
840
- if (hasUncommittedChanges(cwd)) {
841
- let discardSucceeded = false;
842
- for (let retry = 0;retry < 4; retry++) {
843
- if (retry > 0 && process.platform === "win32") {
844
- await new Promise((resolve) => setTimeout(resolve, 500));
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
- try {
858
- _internals.gitExec(["clean", "-fdX", "--", ...GITIGNORED_BUILD_ARTIFACTS], cwd);
859
- } catch {
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
- let branchDeleted = false;
863
- if (switchedBranch && previousBranch !== defaultBranch) {
864
- try {
865
- const mergedOutput = _internals.gitExec(["branch", "--merged", defaultBranch], cwd);
866
- const isMerged = mergedOutput.split(`
867
- `).some((line) => line.trim() === previousBranch || line.trim() === `* ${previousBranch}`);
868
- if (isMerged) {
869
- _internals.gitExec([
870
- "branch",
871
- "-d",
872
- "--",
873
- assertSafeGitRefArg(previousBranch, "resetToMainAfterMerge previousBranch")
874
- ], cwd);
875
- branchDeleted = true;
876
- } else {
877
- warnings.push(`Branch ${previousBranch} is not merged into ${defaultBranch} \u2014 keeping it`);
878
- }
879
- } catch {
880
- warnings.push(`Could not delete branch ${previousBranch}`);
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
- if (options?.pruneBranches) {
884
- try {
885
- const mergedOutput = _internals.gitExec(["branch", "--merged", defaultBranch], cwd);
886
- const mergedLines = mergedOutput.split(`
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 _internals = {
930
- gitExec,
931
- detectDefaultRemoteBranch,
932
- getDefaultBaseBranch,
933
- getGitRepositoryStatus,
934
- resetToRemoteBranch,
935
- resetToMainAfterMerge,
936
- spawnSync: __spawnSyncSeam.spawnSync,
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 fs from "fs/promises";
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 fs.appendFile(resolvedPath, line, { encoding: "utf-8" });
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 _internals2.getTaskHistory(directory, input.taskId);
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: _internals2.generateTaskFingerprint(input.taskId, input.fileTargets ?? []),
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 _internals2.recordOutcome(directory, entry);
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 = _internals2.groupByTaskId(entries);
1158
+ const groups = _internals5.groupByTaskId(entries);
1070
1159
  const summaries = [];
1071
1160
  for (const [taskId, taskEntries] of groups) {
1072
- const summary = _internals2.summarizeTask(taskId, taskEntries);
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 _internals2 = {
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 fsSync2 from "fs";
1534
- import { join as join2 } from "path";
1203
+ import * as fsSync from "fs";
1204
+ import { join as join3 } from "path";
1535
1205
  async function computeSpecHash(directory) {
1536
- const spec = _internals4.readEffectiveSpecSync(directory);
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 _internals4.computeSpecHash(directory);
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 = join2(directory, ".swarm", "spec-snapshot.md");
1233
+ const snapshotPath = join3(directory, ".swarm", "spec-snapshot.md");
1564
1234
  try {
1565
1235
  let recordedContent;
1566
1236
  try {
1567
- recordedContent = fsSync2.readFileSync(snapshotPath, "utf-8");
1237
+ recordedContent = fsSync.readFileSync(snapshotPath, "utf-8");
1568
1238
  } catch {
1569
1239
  return false;
1570
1240
  }
1571
- const currentResult = _internals4.readEffectiveSpecSync(directory);
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 _internals4 = {
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 = join2(directory, ".swarm", "spec-snapshot.md");
1279
+ const snapshotPath = join3(directory, ".swarm", "spec-snapshot.md");
1610
1280
  let recordedContent;
1611
1281
  try {
1612
- const snapshotSize = fsSync2.statSync(snapshotPath).size;
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 = fsSync2.readFileSync(snapshotPath, "utf-8");
1287
+ recordedContent = fsSync.readFileSync(snapshotPath, "utf-8");
1618
1288
  } catch {
1619
1289
  return null;
1620
1290
  }
1621
- const spec = _internals4.readEffectiveSpecSync(directory);
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 _internals5 = {
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
- isGitRepo,
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 (error2) {
1817
- if (!(error2 instanceof LedgerStaleWriterError) || attempt >= maxRetries) {
1818
- throw error2;
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((resolve3) => setTimeout(resolve3, delayMs));
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 (error2) {
1843
- warn(`Plan validation failed for .swarm/plan.json: ${error2 instanceof Error ? error2.message : String(error2)}`);
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 (error2) {
1885
- if (error2 instanceof PlanRecoverySupersededError)
1886
- throw error2;
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, () => _internals5.readPlanJsonUtf8(directory), (planJsonContent) => {
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) => _internals5.readPlanFileUtf8(filePath), false);
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
- renameSync2(mdTempPath, mdPath);
1640
+ renameSync(mdTempPath, mdPath);
1974
1641
  } finally {
1975
1642
  try {
1976
- unlinkSync(mdTempPath);
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 _internals5.readPlanJsonUtf8(directory);
1987
- } catch (error2) {
1988
- warn(`Plan rejected: .swarm/plan.json could not be decoded as valid UTF-8: ${error2 instanceof Error ? error2.message : String(error2)}`);
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 _internals5.regeneratePlanMarkdown(directory, validated, {
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 (error2) {
2119
- if (error2 instanceof PlanRecoverySupersededError)
2120
- throw error2;
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 (error2) {
2136
- if (error2 instanceof PlanRecoverySupersededError)
2137
- throw error2;
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 (error2) {
2145
- if (error2 instanceof PlanRecoverySupersededError) {
1811
+ } catch (error) {
1812
+ if (error instanceof PlanRecoverySupersededError) {
2146
1813
  if (claimedStartupWorkspace !== null) {
2147
1814
  startupLedgerCheckedWorkspaces.delete(claimedStartupWorkspace);
2148
1815
  }
2149
- throw error2;
1816
+ throw error;
2150
1817
  }
2151
- warn(`[loadPlan] plan.json validation failed: ${error2 instanceof Error ? error2.message : String(error2)}. Attempting rebuild from ledger. If rebuild fails, check .swarm/plan-export/SWARM_PLAN.md for a checkpoint.`);
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 _internals5.ledgerExists(directory)) {
2163
- const catchIntegrity = await _internals5.readLedgerEventsWithIntegrity(directory);
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 _internals5.replayFromLedger(directory, {
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 _internals5.loadLastApprovedPlan(directory, catchFirstEvent.plan_id);
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 _internals5.ledgerExists(directory)) {
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 _internals5.loadPlanJsonOnly(directory);
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 _internals5.replayFromLedger(directory, {
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 _internals5.readLedgerEventsWithIntegrity(directory);
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 _internals5.loadLastApprovedPlan(directory, expectedPlanId);
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 _internals5.loadPlanJsonOnly(directory);
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) => _internals5.readPlanFileUtf8(filePath), false);
2376
- } catch (error2) {
2377
- throw new PlanWriteVerificationError(`PLAN_WRITE_VERIFICATION_FAILED: plan.json read-back could not be read (${error2 instanceof Error ? error2.message : String(error2)})`);
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((resolve3) => setTimeout(resolve3, PLAN_WRITE_VERIFY_READ_BACKOFF_MS * (attempt + 1)));
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) => _internals5.readPlanFileUtf8(filePath), false);
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 (error2) {
2392
- throw new PlanWriteVerificationError(`PLAN_WRITE_VERIFICATION_FAILED: plan.json read-back did not parse (${error2 instanceof Error ? error2.message : String(error2)})`);
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 (error2) {
2412
- if (error2 instanceof PlanRecoverySupersededError)
2413
- throw error2;
2414
- warn(`[loadPlan] Markdown-migration provenance event append failed (plan remains migrated): ${error2 instanceof Error ? error2.message : String(error2)}`);
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 _internals5.loadPlanJsonOnly(directory);
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 _internals5.loadPlanJsonOnly(directory);
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 _internals5.readLedgerEventsWithIntegrity(directory);
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 (error2) {
2522
- if (error2 instanceof LedgerStaleWriterError) {
2523
- throw new PlanConcurrentModificationError(`RECONCILE_LEDGER_PROJECTION_STALE: ${error2.message}`);
2188
+ } catch (error) {
2189
+ if (error instanceof LedgerStaleWriterError) {
2190
+ throw new PlanConcurrentModificationError(`RECONCILE_LEDGER_PROJECTION_STALE: ${error.message}`);
2524
2191
  }
2525
- throw error2;
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 _internals5.loadPlanJsonOnly(directory);
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 (error2) {
2600
- if (error2 instanceof LedgerStaleWriterError) {
2601
- throw new PlanConcurrentModificationError(`Concurrent plan modification detected after retries: ${error2.message}. Please retry the operation.`);
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 error2;
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 _internals5.loadPlanJsonOnly(directory);
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 (error2) {
2644
- if (error2 instanceof LedgerStaleWriterError) {
2645
- throw new PlanConcurrentModificationError(`Concurrent plan modification detected after retries: ${error2.message}. Please retry the operation.`);
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 error2;
2314
+ throw error;
2648
2315
  }
2649
2316
  }
2650
2317
  const ledgerStatusTaskIds = collectLedgerStatusTaskIds(await readLedgerEvents(directory));
2651
- const replayedBeforeProjection = await _internals5.replayFromLedger(directory, {
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
- renameSync2(tempPath, planPath);
2345
+ renameSync(tempPath, planPath);
2679
2346
  } finally {
2680
2347
  try {
2681
- unlinkSync(tempPath);
2348
+ unlinkSync2(tempPath);
2682
2349
  } catch {}
2683
2350
  }
2684
2351
  invalidateCachedArtifact(planPath);
2685
- await _internals5.verifyWrittenPlanJson(directory, projectedPlan);
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 (error2) {
2697
- if (error2 instanceof PlanRecoverySupersededError)
2698
- throw error2;
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
- renameSync2(mdTempPath, mdPath);
2378
+ renameSync(mdTempPath, mdPath);
2712
2379
  } finally {
2713
2380
  try {
2714
- unlinkSync(mdTempPath);
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 (error2) {
2744
- if (error2 instanceof PlanRecoverySupersededError)
2745
- throw error2;
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
- renameSync2(tempPath, targetPath);
2797
- } catch (error2) {
2463
+ renameSync(tempPath, targetPath);
2464
+ } catch (error) {
2798
2465
  try {
2799
- unlinkSync(tempPath);
2466
+ unlinkSync2(tempPath);
2800
2467
  } catch {}
2801
- throw error2;
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 = openSync(tempPlanPath, "w");
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
- closeSync(fd);
2494
+ closeSync2(fd);
2828
2495
  }
2829
2496
  }
2830
2497
  options?.preCommitCheck?.();
2831
- renameSync2(tempPlanPath, planPath);
2498
+ renameSync(tempPlanPath, planPath);
2832
2499
  invalidateCachedArtifact(planPath);
2833
2500
  } finally {
2834
2501
  try {
2835
- unlinkSync(tempPlanPath);
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 (error2) {
2849
- if (error2 instanceof PlanRecoverySupersededError)
2850
- throw error2;
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 _internals5.writeRebuildPlanMarkdown(tempMdPath, markdownWithHash);
2529
+ await _internals7.writeRebuildPlanMarkdown(tempMdPath, markdownWithHash);
2863
2530
  options?.preCommitCheck?.();
2864
- renameSync2(tempMdPath, mdPath);
2531
+ renameSync(tempMdPath, mdPath);
2865
2532
  invalidateCachedArtifact(mdPath);
2866
2533
  } finally {
2867
2534
  try {
2868
- unlinkSync(tempMdPath);
2535
+ unlinkSync2(tempMdPath);
2869
2536
  } catch {}
2870
2537
  }
2871
- } catch (error2) {
2538
+ } catch (error) {
2872
2539
  markdownWriteFailed = true;
2873
- markdownWriteError = error2;
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 (error2) {
2887
- if (error2 instanceof PlanRecoverySupersededError) {
2888
- markerSupersededError = error2;
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 (error2) {
2915
- if (error2 instanceof PlanRecoverySupersededError)
2916
- throw error2;
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
- renameSync2(tempPlanPath, planPath);
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
- renameSync2(mdTempPath, mdPath);
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-m7c4x8bk.js");
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 _internals5.loadPlanJsonOnly(directory);
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 _internals5.loadPlan(directory);
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 _internals5.recordTaskAttempt(directory, {
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-aj75vmjq.js");
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" && _internals5.isGitRepo(directory) && _internals5.isEpicModeActiveForProject(directory)) {
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-tbv2ar4w.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 _internals5.maybeSaveAutoCheckpoint(directory, updatedPlan);
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 (error2) {
3124
- if (error2 instanceof PlanConcurrentModificationError && attempt < MAX_OUTER_RETRIES) {
2764
+ } catch (error) {
2765
+ if (error instanceof PlanConcurrentModificationError && attempt < MAX_OUTER_RETRIES) {
3125
2766
  continue;
3126
2767
  }
3127
- throw error2;
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 { buildTaskCompletionDescriptor, ensureTaskCheckpointReceipt, updateTaskCheckpointReceipt, MAX_TRANSIENT_RETRIES, isTransientSpawnError, transientBackoff, readLaneEnvFileFromDiskSync, getGitRepositoryStatus, isGitRepo, getDefaultBaseBranch, detectDefaultRemoteBranch, resetToRemoteBranch, resetToMainAfterMerge, getRunMemorySummary, isStateUnreadable, loadEpicSessionState, isEpicModeActive, enableEpicMode, disableEpicMode, PlanRecoverySupersededError, loadPlanJsonOnly, loadPlan, withPlanLifecycleLock, savePlan, closePlanTerminalState, updateTaskStatus, derivePlanMarkdown, getCurrentTaskId };
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 };