opencode-swarm 7.189.0 → 7.190.1

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-26m3h5f4.js} +9 -9
  7. package/dist/cli/{checkpoint-m7q9886q.js → checkpoint-7a0achpv.js} +18 -20
  8. package/dist/cli/{coder-settlement-hh81z2f7.js → coder-settlement-dsb1t39t.js} +13 -13
  9. package/dist/cli/{config-doctor-0rexy3xt.js → config-doctor-9txawzng.js} +23 -20
  10. package/dist/cli/{core-ays6stz4.js → core-kv7jz9ty.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-26yzg8wp.js} +44 -44
  13. package/dist/cli/{curator-drift-nts5atks.js → curator-drift-kxn5sk3z.js} +9 -9
  14. package/dist/cli/curator-llm-factory-hs2y9kmw.js +82 -0
  15. package/dist/cli/{doctor-knowledge-receipt-phase-id-ec40k5hn.js → doctor-knowledge-receipt-phase-id-z8wea03e.js} +9 -9
  16. package/dist/cli/{evidence-summary-service-vgtm72g5.js → evidence-summary-service-bg5zq78s.js} +21 -22
  17. package/dist/cli/{gate-evidence-vmn65kxe.js → gate-evidence-0beg9c7r.js} +8 -8
  18. package/dist/cli/guardrail-explain-n8bm0hpm.js +83 -0
  19. package/dist/cli/{guardrail-log-hc8fjm4x.js → guardrail-log-r2zazmbn.js} +8 -8
  20. package/dist/cli/{guardrail-reset-ct00w300.js → guardrail-reset-0q54t44w.js} +44 -44
  21. package/dist/cli/{hive-promoter-2xax6van.js → hive-promoter-7dr5b9zv.js} +44 -44
  22. package/dist/cli/{index-9gqb5nkj.js → index-0gve0mkj.js} +4 -4
  23. package/dist/cli/{index-34vsyj15.js → index-0yrg10jx.js} +5 -5
  24. package/dist/cli/{index-mfxef20k.js → index-0z8vdkvg.js} +4 -4
  25. package/dist/cli/{index-2nexf5vz.js → index-1f084tqn.js} +22223 -15395
  26. package/dist/cli/{index-qx3g1bbe.js → index-26e2s6r1.js} +1 -1
  27. package/dist/cli/{index-kqegvngt.js → index-2c47efpt.js} +4 -4
  28. package/dist/cli/{index-9tvjwv01.js → index-2fpv2wmc.js} +1 -1
  29. package/dist/cli/{index-8q8783g0.js → index-3zwhxk98.js} +1 -1
  30. package/dist/cli/{index-9j3ybyfs.js → index-4a15347d.js} +7 -7
  31. package/dist/cli/{index-5cfhwd65.js → index-67a6mk20.js} +2 -2
  32. package/dist/cli/{index-d3rjvm9e.js → index-7vnr5cmp.js} +2 -2
  33. package/dist/cli/{index-twb3f4tk.js → index-8bx6vhzk.js} +1 -1
  34. package/dist/cli/{index-0gejy4cx.js → index-ay8ggtgr.js} +2 -2
  35. package/dist/cli/{index-8xabhfwr.js → index-b2j231jy.js} +2 -2
  36. package/dist/cli/{index-cbqkag7q.js → index-bcwmn4d7.js} +3 -3
  37. package/dist/cli/{index-fbgjj7h5.js → index-bfyh3gpe.js} +101 -13
  38. package/dist/cli/{index-skdpgjar.js → index-cewpxcwm.js} +862 -1221
  39. package/dist/cli/{index-bzbxn24k.js → index-dbdkh7pq.js} +5 -5
  40. package/dist/cli/{index-w4zscz6p.js → index-dnp4qtpf.js} +5 -5
  41. package/dist/cli/{index-waxvpwbv.js → index-efztf4yw.js} +127 -13
  42. package/dist/cli/{index-4p3jh14t.js → index-enq166kw.js} +4 -4
  43. package/dist/cli/{index-g03m5jhw.js → index-f46dbyew.js} +3 -3
  44. package/dist/cli/{index-ntda6tje.js → index-feaxsjrt.js} +10 -2
  45. package/dist/cli/{index-89f56ej4.js → index-fthhppnc.js} +46 -46
  46. package/dist/cli/{index-wqa31xnf.js → index-gqb8etv4.js} +7 -7
  47. package/dist/cli/{index-1cwmj1gs.js → index-ha0w0a7t.js} +77 -16
  48. package/dist/cli/{index-nhsafbrh.js → index-hx1fmywk.js} +5 -5
  49. package/dist/cli/{index-kaqdzhj3.js → index-kfevm0n5.js} +2 -2
  50. package/dist/cli/{index-z29hksyw.js → index-m28dzyy8.js} +26 -10
  51. package/dist/cli/{index-x1eh2636.js → index-mcv0v2dp.js} +1 -1
  52. package/dist/cli/{index-9czp2dcg.js → index-mjfpaxw3.js} +7 -7
  53. package/dist/cli/{index-f6exny9m.js → index-n8q43eff.js} +2 -2
  54. package/dist/cli/{index-7hr5m7yp.js → index-nkgzkt7c.js} +1 -1
  55. package/dist/cli/{index-p2tqzwh6.js → index-qs9myayh.js} +5 -10
  56. package/dist/cli/{index-hh7pa3yk.js → index-r9yvpr2m.js} +2 -2
  57. package/dist/cli/{index-cdjxzs2r.js → index-rjzn7hf0.js} +7 -7
  58. package/dist/cli/{index-zfbxfm2m.js → index-s8b67e6h.js} +33 -12
  59. package/dist/cli/{index-bskex45z.js → index-s9cgnd2z.js} +2 -2
  60. package/dist/cli/{index-cj4mp0fm.js → index-sc7t1tev.js} +6 -6
  61. package/dist/cli/{index-3dx0c7ve.js → index-sfjpbnqs.js} +44 -44
  62. package/dist/cli/{index-16m2m4ah.js → index-spgfs5y7.js} +1 -1
  63. package/dist/cli/{index-q9kefh33.js → index-sxj143a4.js} +1 -1
  64. package/dist/cli/{index-zsrtt42s.js → index-v3bmwmqw.js} +25 -1
  65. package/dist/cli/{index-5v09zw1q.js → index-v3j9fk16.js} +18 -8
  66. package/dist/cli/{index-2btybj8w.js → index-xdq6mak1.js} +4 -4
  67. package/dist/cli/{index-k8sarme1.js → index-y9v41185.js} +1 -1
  68. package/dist/cli/{index-4a7j5f60.js → index-yy5yyyst.js} +4 -4
  69. package/dist/cli/{index-1xa8wq76.js → index-z3ahy9c2.js} +2 -2
  70. package/dist/cli/{index-wrdktgpy.js → index-zk6ttjkt.js} +2 -2
  71. package/dist/cli/index.js +45 -45
  72. package/dist/cli/{knowledge-escalator-v3c8w86g.js → knowledge-escalator-hdrqt5pj.js} +15 -15
  73. package/dist/cli/{knowledge-events-kagaccj9.js → knowledge-events-ttebc8gn.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-8cxv6w2n.js} +6 -6
  76. package/dist/cli/{knowledge-validator-210gkpec.js → knowledge-validator-kr5xmqpa.js} +8 -8
  77. package/dist/cli/{mcp-58hgx6av.js → mcp-jtk79ygp.js} +1 -1
  78. package/dist/cli/{model-preflight-e69b48n0.js → model-preflight-d6tt7479.js} +44 -44
  79. package/dist/cli/{pending-delegations-g24d3qa9.js → pending-delegations-ttk09f50.js} +12 -12
  80. package/dist/cli/{pr-subscriptions-qprh2zsm.js → pr-subscriptions-2kbeezsr.js} +12 -12
  81. package/dist/cli/{pr-workflow-gate-pj7dhkv4.js → pr-workflow-gate-3ra6p7k4.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-zzccy117.js} +7 -7
  84. package/dist/cli/{schema-smgngq9m.js → schema-9prswg1x.js} +14 -2
  85. package/dist/cli/{scope-persistence-tbv2ar4w.js → scope-persistence-eqm8vwnx.js} +16 -16
  86. package/dist/cli/{server-7p2ds5w2.js → server-swjre81h.js} +45 -45
  87. package/dist/cli/{skill-generator-r4hsne71.js → skill-generator-d8dk9vps.js} +17 -17
  88. package/dist/cli/{snapshot-coordination-init-ydjwh56d.js → snapshot-coordination-init-f49y0zr0.js} +44 -44
  89. package/dist/cli/{speckit-checkoff-aj75vmjq.js → speckit-checkoff-pnb6q9gd.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-3c7xyc4p.js} +14 -14
  93. package/dist/cli/{worktree-isolation-h0c22pm2.js → worktree-isolation-jggdf28m.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
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Epic v2 C6 — what an epic learns from its own outcomes (pure).
3
+ *
4
+ * Two signals, both learned from the per-task outcomes `epic_next_wave`
5
+ * records when it closes a wave (`EpicTaskOutcome`, see `wave-close.ts`),
6
+ * and both used ONLY by the component planner (`components.ts`) — they are
7
+ * analysis, never write authorization:
8
+ *
9
+ * 1. Learned scope expansion. A task that declared D and wrote f without
10
+ * declaring it is evidence that work on D also touches f:
11
+ * w(d → f) += 1 for every d ∈ D. The planner's conflict graph uses
12
+ * scope*(t) = scope(t) ∪ { f : Σ_{d ∈ scope(t)} w(d → f) ≥ 1 }.
13
+ * scope* only ADDS path-conflict edges; the wave still freezes the
14
+ * DECLARED scopes, which the dispatch gate checks for drift and its
15
+ * parallel verdict reads. Because the planner is stricter than the
16
+ * verdict (a superset of its edges), every multi-task wave it picks
17
+ * stays `all_disjoint` under the verdict (property-tested). Co-change
18
+ * coupling keeps using the declared scopes: its "exclusively owns one
19
+ * side" rule is not monotone in the scope, so expanding it could REMOVE
20
+ * a verdict conflict.
21
+ *
22
+ * 2. Decaying hot set. Per file, incidents α_f and exposures β_f:
23
+ * incidents — undeclared write 1.0 (the written file); merge-back
24
+ * failure 0.5, Stage B failure 0.25 per failure, rework
25
+ * 0.25 × min(generation − 1, 4) and reopen 0.5 per reopen, each
26
+ * charged to every declared FILE entry of the task (the merge-status
27
+ * registry records no conflict files, so a merge failure is charged
28
+ * to the declared files);
29
+ * exposures — β += 1 per declared FILE entry of every resolved task.
30
+ * A declared entry that is a directory (on disk, or covering another
31
+ * path of the same outcome) is never charged or exposed: one troubled
32
+ * `src`-scoped task must not make everything under `src/` hot.
33
+ * A task run again after a reopen is charged only the DELTA of its
34
+ * counters over its previous outcome (`previous`), when that previous
35
+ * outcome was already learned.
36
+ * Beta prior with mean m0 = 0.1 and strength 2 (a0 = 0.2, b0 = 1.8):
37
+ * r(f) = (a0 + α_f) / (a0 + b0 + α_f + β_f).
38
+ * Strongest-co-writer discount: α'_f = α_f − m_f when the strongest
39
+ * learned co-writer m_f = max_d w(d → f) ≥ 1 (an active expansion edge
40
+ * already keeps that declarer apart from f's owners), else α'_f = α_f.
41
+ * f is HOT ⇔ α'_f ≥ 1 ∧ r(α'_f, β_f) − m0 > hot_excess (default 0.25). A task
42
+ * whose declared scope lists a hot file (exact normalized path) runs
43
+ * alone (exclusive). Hot
44
+ * needs EXCESS evidence: an empty or clean history has no hot file and
45
+ * no expansion, so the cold start is neutral (no R_C product).
46
+ *
47
+ * Decay keeps both bounded and current: × `decay_per_epic` (0.7) of the
48
+ * project prior at every epic close that learned something, and age decay
49
+ * in WHOLE half-lives — × 0.5^floor(Δdays / `half_life_days` (60)) since
50
+ * the state was last written — so an observation keeps its full weight for
51
+ * a whole half-life; entries below 0.05 are dropped and at most
52
+ * {@link MAX_EPIC_LEARNING_FILES} files (lowest α + β evicted) and
53
+ * {@link MAX_EPIC_LEARNING_EDGES} co-write edges (lowest weight evicted) are
54
+ * kept. Persistence (project prior + epic posterior) is `learning-store.ts`.
55
+ */
56
+ import { type EpicConfigSource } from './config.js';
57
+ import type { EpicTaskOutcome } from './lifecycle.js';
58
+ /** Files with learned statistics kept (lowest α + β evicted first). */
59
+ export declare const MAX_EPIC_LEARNING_FILES = 2000;
60
+ /** Learned co-write edges kept (lowest weight evicted first). */
61
+ export declare const MAX_EPIC_LEARNING_EDGES = 2000;
62
+ /** Entries whose mass falls below this after decay are dropped. */
63
+ export declare const EPIC_LEARNING_DROP_BELOW = 0.05;
64
+ /** Prior mean incident rate m0 and its strength (a0 + b0). */
65
+ export declare const EPIC_LEARNING_PRIOR_MEAN = 0.1;
66
+ export declare const EPIC_LEARNING_PRIOR_STRENGTH = 2;
67
+ /** Minimum incident mass before a file can be hot. */
68
+ export declare const EPIC_HOT_MIN_ALPHA = 1;
69
+ /** Summed co-write weight at which a learned file joins scope*. */
70
+ export declare const EPIC_EXPANSION_MIN_WEIGHT = 1;
71
+ export declare const EPIC_LEARNING_INCIDENT_WEIGHTS: Readonly<{
72
+ undeclared: 1;
73
+ mergeFailurePerDeclared: 0.5;
74
+ stageBFailure: 0.25;
75
+ reworkPerGeneration: 0.25;
76
+ reworkMaxGenerations: 4;
77
+ reopen: 0.5;
78
+ }>;
79
+ /** Resolved `epic.learning.*` settings. */
80
+ export interface EpicLearningSettings {
81
+ enabled: boolean;
82
+ decayPerEpic: number;
83
+ halfLifeDays: number;
84
+ hotExcess: number;
85
+ }
86
+ export declare const DEFAULT_EPIC_LEARNING_SETTINGS: EpicLearningSettings;
87
+ export declare function resolveEpicLearningSettings(config: EpicConfigSource): EpicLearningSettings;
88
+ export interface EpicFileStats {
89
+ /** Incident mass α_f. */
90
+ alpha: number;
91
+ /** Exposure mass β_f. */
92
+ beta: number;
93
+ }
94
+ /** In-memory learning statistics (paths normalized, never `.swarm/`). */
95
+ export interface EpicLearningStats {
96
+ files: Map<string, EpicFileStats>;
97
+ /** from (declared) → to (co-written undeclared) → weight. */
98
+ edges: Map<string, Map<string, number>>;
99
+ }
100
+ /** Learned co-write weights, read-only (the planner's view). */
101
+ export type EpicCoWriteIndex = ReadonlyMap<string, ReadonlyMap<string, number>>;
102
+ export declare function emptyEpicLearning(): EpicLearningStats;
103
+ export declare function isEpicLearningEmpty(stats: EpicLearningStats): boolean;
104
+ export declare function cloneEpicLearning(stats: EpicLearningStats): EpicLearningStats;
105
+ /** Normalized, unique paths learning may keep (never `.swarm/` or absolute). */
106
+ export declare function learnablePaths(files: readonly string[]): string[];
107
+ /**
108
+ * Files written that the declared scope does not cover (a declared
109
+ * directory covers every file beneath it, segment-aware). Sorted, unique,
110
+ * normalized.
111
+ */
112
+ export declare function undeclaredFiles(declaredScope: readonly string[], actualFiles: readonly string[]): string[];
113
+ export interface EpicLearningOptions {
114
+ /**
115
+ * Whether a declared entry is a directory on disk (never charged or
116
+ * exposed). Absent ⇒ only entries covering another path of the same
117
+ * outcome count as directories.
118
+ */
119
+ isDirectory?: (path: string) => boolean;
120
+ /**
121
+ * Waves up to this seq were already learned: a re-run task whose
122
+ * `previous` outcome is among them is charged only the delta of its
123
+ * counters. Absent ⇒ every outcome is charged in full (the record
124
+ * fallback, where the earlier outcome was never learned separately).
125
+ */
126
+ learnedThroughWaveSeq?: number;
127
+ }
128
+ /**
129
+ * The learning increments of resolved task outcomes (removed tasks never
130
+ * ran and teach nothing). Pure (the directory probe is injected); the
131
+ * caller merges the result.
132
+ */
133
+ export declare function epicLearningFromOutcomes(outcomes: readonly EpicTaskOutcome[], options?: EpicLearningOptions): EpicLearningStats;
134
+ /** a ⊕ b (new object; inputs untouched). */
135
+ export declare function mergeEpicLearning(a: EpicLearningStats, b: EpicLearningStats): EpicLearningStats;
136
+ /** Every mass × factor (new object). */
137
+ export declare function scaleEpicLearning(stats: EpicLearningStats, factor: number): EpicLearningStats;
138
+ /**
139
+ * Age decay in WHOLE half-lives: 0.5^floor(Δdays / halfLifeDays) — an
140
+ * observation keeps its full weight for a whole half-life (a single
141
+ * co-write keeps expanding scopes until then). 1 for a future or unknown
142
+ * timestamp.
143
+ */
144
+ export declare function epicTimeDecayFactor(updatedAtIso: string | null | undefined, nowMs: number, halfLifeDays: number): number;
145
+ /**
146
+ * Drop entries below {@link EPIC_LEARNING_DROP_BELOW} and keep at most the
147
+ * file / edge caps (lowest mass evicted; ties by path). New object.
148
+ */
149
+ export declare function boundEpicLearning(stats: EpicLearningStats): EpicLearningStats;
150
+ /** r(f) = (a0 + α) / (a0 + b0 + α + β). */
151
+ export declare function epicIncidentRate(stats: EpicFileStats): number;
152
+ export declare function isEpicHotFile(stats: EpicFileStats, hotExcess: number): boolean;
153
+ /**
154
+ * The incident mass the hot predicate judges (the strongest-co-writer
155
+ * discount): α' = max(0, α − m) when the file's strongest learned co-writer
156
+ * m = max_d w(d → f) is an ACTIVE scope-expansion edge (m ≥
157
+ * {@link EPIC_EXPANSION_MIN_WEIGHT}), else α. Undeclared writes that one
158
+ * declared file keeps explaining are already handled by scope expansion
159
+ * (the declarer now conflicts with the file's owners), so they must not
160
+ * ALSO make the file hot and serialize everyone declaring it; writes from
161
+ * several different declarers (a file that attracts undeclared writes
162
+ * from all over) and the other incidents still count.
163
+ */
164
+ export declare function epicHotIncidentMass(stats: EpicFileStats, strongestCoWriter: number): number;
165
+ /** The hot predicate's evidence for one file (`/swarm epic learning`). */
166
+ export interface EpicHotEvidence {
167
+ /** Raw incident mass α. */
168
+ alpha: number;
169
+ /** α' after the strongest-co-writer discount. */
170
+ countedAlpha: number;
171
+ beta: number;
172
+ /** r(α', β). */
173
+ rate: number;
174
+ }
175
+ export declare function epicHotEvidence(stats: EpicLearningStats, file: string): EpicHotEvidence;
176
+ /**
177
+ * Hot files, hottest first (ties by path) — THE hot predicate (planner,
178
+ * sizing, shaping, report and commands all use it): {@link isEpicHotFile}
179
+ * over the discounted incident mass ({@link epicHotIncidentMass}).
180
+ */
181
+ export declare function epicHotFiles(stats: EpicLearningStats, hotExcess: number): string[];
182
+ /**
183
+ * scope*(t) = scope(t) ∪ { f : Σ_{d ∈ scope(t)} w(d → f) ≥ 1 } (normalized;
184
+ * declared entries first, learned files appended sorted). Analysis only.
185
+ */
186
+ export declare function expandEpicScope(scope: readonly string[], coWrites: EpicCoWriteIndex | null): string[];
187
+ export interface EpicLearningSummary {
188
+ files: number;
189
+ edges: number;
190
+ hotFiles: string[];
191
+ topCoWrites: Array<{
192
+ from: string;
193
+ to: string;
194
+ weight: number;
195
+ }>;
196
+ }
197
+ /** A bounded human summary (status / commands). */
198
+ export declare function summarizeEpicLearning(stats: EpicLearningStats, hotExcess: number, limit?: number): EpicLearningSummary;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * One-time retirement of Epic v1 session state (Epic v2 C1a).
3
+ *
4
+ * Epic v1 kept a per-session "Epic on" flag in the coordination namespace
5
+ * `turbo.epic.session` with `.swarm/epic-state.json` as its projection. v2
6
+ * epics are plan-scoped and opened only by `/swarm epic start`, so v1 state
7
+ * is never imported as an open epic: it is retired here — rows deleted, the
8
+ * projection renamed to `.imported` — and any session that was still "on"
9
+ * yields an advisory to run `/swarm epic start`.
10
+ *
11
+ * Runs only inside `/swarm epic status` (never on a probe/hot path). Opens
12
+ * swarm.db only when it already exists. Idempotent: once retired there is
13
+ * nothing left to find.
14
+ */
15
+ /** The retired v1 coordination namespace. */
16
+ export declare const LEGACY_EPIC_SESSION_NAMESPACE = "turbo.epic.session";
17
+ export interface LegacyEpicMigrationResult {
18
+ rowsRemoved: number;
19
+ /** Sessions that still had Epic v1 switched on. */
20
+ activeSessions: number;
21
+ fileArchivedTo: string | null;
22
+ errors: string[];
23
+ }
24
+ export declare function retireLegacyEpicSessionState(directory: string): LegacyEpicMigrationResult;
25
+ /** Status lines for a non-empty migration result (empty otherwise). */
26
+ export declare function describeLegacyEpicMigration(result: LegacyEpicMigrationResult): string[];
@@ -0,0 +1,437 @@
1
+ /**
2
+ * Epic v2 lifecycle (C1a): one open epic per project, bound to one plan.
3
+ *
4
+ * Authority: the coordination row in namespace {@link EPIC_LIFECYCLE_NAMESPACE}
5
+ * (entity key = epicKey). The sentinel `.swarm/epic/epic.json` is a JSON
6
+ * projection of that row whose only job is to make "Epic is off" cost exactly
7
+ * ONE `existsSync` on every hot-path seam: a project that never opened an
8
+ * epic has no sentinel, so no probe ever opens swarm.db, reads config, or
9
+ * reads the plan.
10
+ *
11
+ * Probe order (`getOpenEpic`): sentinel → row → config → identity. No memo,
12
+ * no migration, no writes. A row whose plan identity no longer matches the
13
+ * current plan (plan renamed or replaced — a new ledger root) is ORPHANED:
14
+ * the probe answers "no open epic"; `/swarm epic status` explains and
15
+ * `/swarm epic close --abandon` repairs.
16
+ *
17
+ * Lifecycle lock (M-d): every sentinel mutation runs INSIDE the coordination
18
+ * store's `BEGIN IMMEDIATE` transaction (`withCoordinationTransaction`), so
19
+ * the row write and the sentinel write/delete are serialized against every
20
+ * other lifecycle writer in every process:
21
+ * - start: only the CAS winner (row created with expectedRevision null)
22
+ * writes the sentinel; a sentinel-write failure rolls the row back.
23
+ * - close/repair: the row is deleted (revision CAS) and the sentinel is
24
+ * removed only when it still names the same epicKey + token
25
+ * (compare-and-delete), so a late close can never delete a newer epic's
26
+ * sentinel.
27
+ */
28
+ import type { EpicWaveComponents } from './components.js';
29
+ import { isEpicModeConfigEnabledForDirectory } from './config-gate.js';
30
+ import type { EpicSizingVerdict } from './sizing.js';
31
+ /** Coordination namespace of the authoritative epic row. */
32
+ export declare const EPIC_LIFECYCLE_NAMESPACE = "turbo.epic.lifecycle";
33
+ /** Project-relative path of the sentinel projection. */
34
+ export declare const EPIC_SENTINEL_RELATIVE_PATH: string;
35
+ export type EpicLifecycleStatus = 'open' | 'closing';
36
+ /** One co-change pair recorded with a wave (threshold-passing, in-wave). */
37
+ export interface EpicCochangePair {
38
+ fileA: string;
39
+ fileB: string;
40
+ npmi: number;
41
+ coChangeCount: number;
42
+ }
43
+ /** Snapshot of a worktree merge-back failure observed for a wave task. */
44
+ export interface EpicMergeFailureSnapshot {
45
+ outcome: string;
46
+ stage: string;
47
+ message: string;
48
+ at: number | null;
49
+ }
50
+ /**
51
+ * One wave issued by `epic_next_wave` (Epic v2 C2). `files` are the FROZEN
52
+ * declared scopes of the wave's tasks at issue time; later commits compare
53
+ * against them (divergence now, the dispatch gate in C4).
54
+ */
55
+ export interface EpicWaveRecord {
56
+ seq: number;
57
+ phase: number;
58
+ /**
59
+ * `parallel` (tasks of conflict-free components), `exclusive` (a task
60
+ * that runs alone), or `serial-component` (one task of a densely coupled
61
+ * component; no other ready task could join it). See `components.ts`.
62
+ */
63
+ kind: 'parallel' | 'exclusive' | 'serial-component';
64
+ taskIds: string[];
65
+ files: Record<string, string[]>;
66
+ /** ≤ 256 threshold-passing pairs within the wave's files; null = co-change off. */
67
+ cochange: {
68
+ pairs: EpicCochangePair[];
69
+ threshold: {
70
+ npmi: number;
71
+ minCoChanges: number;
72
+ };
73
+ } | null;
74
+ /**
75
+ * Epic v2 C5: the phase's components when the wave was issued (pending
76
+ * task → component, component modes/densities). Absent on waves issued
77
+ * before C5.
78
+ */
79
+ components?: EpicWaveComponents;
80
+ /** HEAD when the wave was issued (null: non-git / unborn). */
81
+ baseHead: string | null;
82
+ issuedAt: string;
83
+ closedAt?: string;
84
+ closeHead?: string | null;
85
+ status: 'issued' | 'closed' | 'aborted';
86
+ abortReason?: string;
87
+ /** Merge-back failures observed while the wave was blocked (snapshots). */
88
+ mergeFailures?: Record<string, EpicMergeFailureSnapshot>;
89
+ /**
90
+ * Wave-level undeclared files from the git fallback (changed since
91
+ * `baseHead`, not declared by any wave task, not attributed to a task).
92
+ */
93
+ undeclared?: string[];
94
+ }
95
+ /** How one wave task was resolved, recorded when its wave closes. */
96
+ export interface EpicTaskOutcome {
97
+ taskId: string;
98
+ phase: number;
99
+ waveSeq: number;
100
+ resolution: 'completed' | 'closed' | 'removed';
101
+ resolvedAt: string;
102
+ /** Evidence workflow generation (uncapped; 0 = no evidence). */
103
+ generation: number;
104
+ /**
105
+ * LOWER BOUNDS: `stage_a_failed` / `stage_b_failed` entries in the
106
+ * evidence workflow `retryHistory`, which keeps only the last 3 entries.
107
+ */
108
+ stageAFailures: number;
109
+ stageBFailures: number;
110
+ /** Merge-back failure snapshot observed while the wave was blocked. */
111
+ mergeFailure: {
112
+ outcome: string;
113
+ stage: string;
114
+ } | null;
115
+ declared: string[];
116
+ undeclared: string[];
117
+ /** Where `undeclared` came from. */
118
+ attribution: 'session' | 'git-single-task' | 'unavailable' | 'no-git';
119
+ /** Ledger transitions out of `completed` for this task since the epic started. */
120
+ reopened: number;
121
+ /**
122
+ * Epic v2 C6: the counters of this task's earlier outcome when it ran
123
+ * again (reopened) — learning charges only the delta.
124
+ */
125
+ previous?: {
126
+ waveSeq: number;
127
+ generation: number;
128
+ stageBFailures: number;
129
+ reopened: number;
130
+ };
131
+ /**
132
+ * The commit proving the task's work is on the epic branch (Epic v2 C3),
133
+ * mirrored to `ref` (`refs/swarm/epics/<epicKey>/tasks/<id>`, see
134
+ * `markers.ts`): the task's newest `swarm(task <id>):` commit for this
135
+ * plan inside the wave (`landing-commit` — its coder's landing merge or a
136
+ * residue commit), else HEAD at wave close (`wave-close-head`), or a
137
+ * commit re-adopted by `/swarm epic status --repair-refs` (`repaired`).
138
+ * Completed tasks only; `no-git` epics record no commit.
139
+ */
140
+ marker: {
141
+ ref: string | null;
142
+ sha: string | null;
143
+ provenance: EpicTaskMarkerProvenance;
144
+ } | null;
145
+ }
146
+ export type EpicTaskMarkerProvenance = 'landing-commit' | 'wave-close-head' | 'repaired' | 'no-git';
147
+ /** Per-phase lifecycle as seen by `epic_next_wave` / `epic_phase_review`. */
148
+ export interface EpicPhaseRecord {
149
+ status: 'active' | 'review' | 'complete';
150
+ /** True when the phase was already finished when the epic started. */
151
+ completeAtStart?: boolean;
152
+ reviewRuns: number;
153
+ /** Newest last; at most {@link EPIC_PHASE_VERDICTS_KEEP}. */
154
+ verdicts: string[];
155
+ }
156
+ /** Phase-review verdict strings kept per phase in the record. */
157
+ export declare const EPIC_PHASE_VERDICTS_KEEP = 20;
158
+ /**
159
+ * Where an epic's commits go (`epic.commit_policy`, default
160
+ * `epic-branch`): a dedicated `swarm/epic/<epicKey>` branch checked out at
161
+ * start and landed at close, or the branch that was current at start.
162
+ */
163
+ export type EpicCommitPolicy = 'epic-branch' | 'current-branch';
164
+ /** How `/swarm epic close` lands the epic branch (default `squash`). */
165
+ export type EpicLandMode = 'squash' | 'merge' | 'none';
166
+ export interface EpicRecordConfig {
167
+ /** Non-git epics always record 'current-branch' (nothing to branch). */
168
+ commitPolicy: EpicCommitPolicy;
169
+ isolation: 'worktree' | 'main-tree-nogit';
170
+ /** Wave width cap honoured by `epic_next_wave` (1 for non-git, M-i). */
171
+ maxParallel: number;
172
+ }
173
+ export interface EpicRecordGit {
174
+ isRepo: boolean;
175
+ baseCommit: string | null;
176
+ originalBranch: string | null;
177
+ /**
178
+ * `swarm/epic/<epicKey>` — written ONLY after `git checkout -b` succeeded
179
+ * (M-e); null for current-branch / non-git epics.
180
+ */
181
+ epicBranch: string | null;
182
+ }
183
+ /** A landing attempt that did not land (close stays resumable). */
184
+ export interface EpicLandingAttempt {
185
+ mode: EpicLandMode;
186
+ status: 'conflict' | 'failed';
187
+ at: string;
188
+ conflictFiles: string[];
189
+ detail: string;
190
+ }
191
+ export interface EpicClosingInfo {
192
+ requestedAt: string;
193
+ outcome: EpicCloseOutcome;
194
+ /** Landing mode of the latest close request (null: abandon / no branch). */
195
+ land: EpicLandMode | null;
196
+ /** Last landing attempt that failed; null until one fails. */
197
+ lastLandingAttempt: EpicLandingAttempt | null;
198
+ }
199
+ export type EpicCloseOutcome = 'completed' | 'abandoned' | 'abandoned-by-swarm-close';
200
+ export interface EpicRecordV1 {
201
+ schema: 'epic-record-v1';
202
+ epicKey: string;
203
+ /** Per-start nonce; the sentinel compare-and-delete matches it too. */
204
+ token: string;
205
+ planId: string;
206
+ planIdentityHash: string;
207
+ planEpoch: string | null;
208
+ planKey: string;
209
+ /** sha256 of the plan ledger's root line (sync epoch proxy); null = no ledger. */
210
+ ledgerRootDigest: string | null;
211
+ status: EpicLifecycleStatus;
212
+ startedAt: string;
213
+ startedBySession: string;
214
+ forced: boolean;
215
+ structureHashAtStart: string;
216
+ config: EpicRecordConfig;
217
+ git: EpicRecordGit;
218
+ sizing: EpicSizingVerdict;
219
+ /**
220
+ * Epic v2 C6: sha256 of the project prior (`.swarm/epic-prior/
221
+ * learning.json`) the epic's posterior was copied from at start; null
222
+ * when there was no readable prior or learning is disabled.
223
+ */
224
+ priorDigest: string | null;
225
+ closing: EpicClosingInfo | null;
226
+ /** Waves issued by `epic_next_wave`, in issue order (Epic v2 C2). */
227
+ waves: EpicWaveRecord[];
228
+ /** Seq of the wave currently issued and not yet closed. */
229
+ activeWaveSeq: number | null;
230
+ /** Outcome of every task resolved through a closed wave, by task id. */
231
+ tasks: Record<string, EpicTaskOutcome>;
232
+ /** Phase lifecycle keyed by phase id (string). */
233
+ phases: Record<string, EpicPhaseRecord>;
234
+ }
235
+ /** Sentinel projection: enough to identify the row it mirrors. */
236
+ export interface EpicSentinel {
237
+ schema: 'epic-sentinel-v1';
238
+ epicKey: string;
239
+ token: string;
240
+ planId: string;
241
+ startedAt: string;
242
+ }
243
+ /** Raised when the lifecycle row exists but cannot be trusted. */
244
+ export declare class EpicStateUnreadableError extends Error {
245
+ constructor(message: string);
246
+ }
247
+ /**
248
+ * DI seam (AGENTS.md invariant 7). Restore in `afterEach`.
249
+ */
250
+ export declare const _internals: {
251
+ isEpicModeConfigEnabledForDirectory: typeof isEpicModeConfigEnabledForDirectory;
252
+ now: () => number;
253
+ /** Bounded warn-once set so a corrupt row does not spam every probe. */
254
+ warnedDirectories: Set<string>;
255
+ };
256
+ export declare function epicSentinelPath(directory: string): string;
257
+ /**
258
+ * The one hot-path check: does the sentinel exist? Exactly one `existsSync`.
259
+ * Never follows raw `..` segments into an ancestor's `.swarm/`.
260
+ */
261
+ export declare function epicSentinelExists(directory: string): boolean;
262
+ /** Ref-safe, filename-safe epic key: `<planId≤40>-<planKey≤12>`. */
263
+ export declare function computeEpicKey(planId: string, planKey: string): string;
264
+ /** Identity fields of the current plan used for the orphan check. */
265
+ export interface EpicPlanIdentity {
266
+ planId: string;
267
+ planIdentityHash: string;
268
+ }
269
+ export declare function planIdentityOf(plan: {
270
+ swarm: string;
271
+ title: string;
272
+ }): EpicPlanIdentity;
273
+ /**
274
+ * sha256 of the plan ledger's first line. The ledger is append-only and a
275
+ * new plan (save_plan of a different plan, reset, recovery re-root) starts a
276
+ * new root line, so the digest is a synchronous proxy for the plan epoch.
277
+ * Returns null when there is no ledger.
278
+ */
279
+ export declare function readLedgerRootDigest(directory: string): string | null;
280
+ /** Synchronous, bounded read of the current plan identity from plan.json. */
281
+ export declare function readCurrentPlanIdentity(directory: string): EpicPlanIdentity | null;
282
+ export declare function parseEpicRecord(payload: string): EpicRecordV1;
283
+ /** Read and validate the sentinel; null when absent, invalid, or oversized. */
284
+ export declare function readEpicSentinel(directory: string): EpicSentinel | null;
285
+ /** Why a row-backed epic is not usable for the current plan. */
286
+ export type EpicOrphanReason = 'plan-missing' | 'plan-renamed-or-replaced' | 'plan-ledger-replaced';
287
+ /** Pure identity comparison (exported for tests and status). */
288
+ export declare function evaluateEpicIdentity(record: EpicRecordV1, current: EpicPlanIdentity | null, ledgerRootDigest: string | null): EpicOrphanReason | null;
289
+ /** Full diagnostic view used by `/swarm epic status` / close / repair. */
290
+ export interface EpicInspection {
291
+ sentinelPresent: boolean;
292
+ sentinel: EpicSentinel | null;
293
+ /** Validated record (null when no row, or when `unreadable` is set). */
294
+ record: EpicRecordV1 | null;
295
+ /** Row entity keys present (even when unparseable). */
296
+ rowKeys: string[];
297
+ unreadable: string | null;
298
+ orphanReason: EpicOrphanReason | null;
299
+ configEnabled: boolean;
300
+ }
301
+ /**
302
+ * Inspect every piece of lifecycle state without writing. Unlike the probe
303
+ * it reads rows even when the sentinel is absent (repair needs that), so it
304
+ * is only for `/swarm epic *` and `/swarm close` finalization. Opens the DB
305
+ * only when swarm.db already exists.
306
+ */
307
+ export declare function inspectEpic(directory: string, plan?: {
308
+ swarm: string;
309
+ title: string;
310
+ } | null): EpicInspection;
311
+ /**
312
+ * The open epic for the current plan, or null. Probe order: sentinel → row →
313
+ * config → identity; no memo, no writes. Throws {@link EpicStateUnreadableError}
314
+ * when the sentinel exists and the row is present but untrustworthy.
315
+ *
316
+ * @param plan - when supplied, its identity is used instead of reading
317
+ * `.swarm/plan.json`.
318
+ */
319
+ export declare function getOpenEpic(directory: string, plan?: {
320
+ swarm: string;
321
+ title: string;
322
+ } | null): EpicRecordV1 | null;
323
+ /**
324
+ * Boolean probe for hot-path seams. Sentinel-first (one `existsSync` when
325
+ * Epic is off); corrupt state ⇒ false + one critical warning per directory.
326
+ */
327
+ export declare function isEpicOpenForProject(directory: string): boolean;
328
+ export type EpicCreateOutcome = {
329
+ outcome: 'created';
330
+ record: EpicRecordV1;
331
+ } | {
332
+ outcome: 'exists';
333
+ existingKeys: string[];
334
+ };
335
+ /**
336
+ * CAS-create the epic row and (as the winner only) write the sentinel, both
337
+ * under the lifecycle lock. A sentinel-write failure rolls the row back.
338
+ */
339
+ export declare function createEpicRecord(directory: string, record: EpicRecordV1): EpicCreateOutcome;
340
+ /**
341
+ * The epic row would outgrow the store payload cap. Distinct from a generic
342
+ * store failure so callers can surface the remedy instead of the raw
343
+ * `payload must contain 1..1048576 characters`.
344
+ */
345
+ export declare class EpicRecordTooLargeError extends Error {
346
+ constructor(epicKey: string);
347
+ }
348
+ /** True when `record` would serialize past {@link MAX_EPIC_RECORD_PAYLOAD_CHARS}. */
349
+ export declare function epicRecordPayloadTooLarge(record: EpicRecordV1): boolean;
350
+ /**
351
+ * Revision-checked read-modify-write of the epic row. Returns the updated
352
+ * record, or null when the row vanished or (with `expectedToken`) belongs to
353
+ * a different start of the same epicKey. Throws on contention/corruption, and
354
+ * throws {@link EpicRecordTooLargeError} when the row would outgrow the store
355
+ * payload cap.
356
+ */
357
+ export declare function updateEpicRecord(directory: string, epicKey: string, mutate: (record: EpicRecordV1) => EpicRecordV1, expectedToken?: string | null): EpicRecordV1 | null;
358
+ /** A phase as the Epic phase order sees it (id + plan status). */
359
+ export interface EpicPhaseRef {
360
+ id: number;
361
+ status?: string;
362
+ }
363
+ /**
364
+ * Whether `phase` is done for Epic: recorded complete on the epic (by
365
+ * `phase_complete`, or finished before the epic started), or closed in the
366
+ * plan. The plan's own `complete` status is NOT used: it turns `complete`
367
+ * as soon as every task completes, before the phase review ran.
368
+ */
369
+ export declare function isEpicPhaseDone(record: Pick<EpicRecordV1, 'phases'>, phase: EpicPhaseRef): boolean;
370
+ /** The epic's current phase: the first phase (plan order) not done. */
371
+ export declare function epicCurrentPhase(record: Pick<EpicRecordV1, 'phases'>, phases: readonly EpicPhaseRef[]): number | null;
372
+ /** Bounded read of the plan's phase order from `.swarm/plan.json`. */
373
+ export declare function readPlanPhaseRefs(directory: string): EpicPhaseRef[] | null;
374
+ /** A plan task as Epic's hot-path seams see it. */
375
+ export interface EpicPlanTaskRef {
376
+ id: string;
377
+ phase: number;
378
+ description: string | undefined;
379
+ files: string[];
380
+ }
381
+ /**
382
+ * Bounded synchronous lookup of one task in `.swarm/plan.json` (the derived
383
+ * projection — enough for membership and the commit subject). Null when the
384
+ * plan is unreadable or the task is not in it.
385
+ */
386
+ export declare function readPlanTaskRef(directory: string, taskId: string): EpicPlanTaskRef | null;
387
+ export type EpicPhaseCompleteOutcome = {
388
+ outcome: 'recorded' | 'already-complete';
389
+ record: EpicRecordV1;
390
+ } | {
391
+ outcome: 'no-open-epic';
392
+ } | {
393
+ outcome: 'not-current-phase';
394
+ currentPhase: number | null;
395
+ };
396
+ /**
397
+ * Record `phase` complete on the open epic (Epic v2 C2; called by
398
+ * `phase_complete` on success while an epic is open). Phases are
399
+ * iterations: only the epic's CURRENT phase can be completed — completing
400
+ * another phase out of order is refused (`not-current-phase`) so it cannot
401
+ * skip the current phase's waves. Idempotent for an already-complete phase.
402
+ * Throws on unreadable state / contention.
403
+ */
404
+ export declare function markEpicPhaseComplete(directory: string, phase: number, planPhases?: readonly EpicPhaseRef[]): EpicPhaseCompleteOutcome;
405
+ /**
406
+ * CAS the row to `closing` (first close step; idempotent). A resumed close
407
+ * keeps the original request time and outcome — except that an explicit
408
+ * abandon upgrades a `completed` close whose landing never finished — and
409
+ * records the landing mode of the latest request.
410
+ */
411
+ export declare function markEpicClosing(directory: string, epicKey: string, outcome: EpicCloseOutcome, expectedToken?: string | null, land?: EpicLandMode | null): EpicRecordV1 | null;
412
+ /**
413
+ * Record the epic branch once `git checkout -b` succeeded (M-e). Returns the
414
+ * updated record, or null when the row vanished / belongs to another start.
415
+ */
416
+ export declare function recordEpicBranch(directory: string, epicKey: string, expectedToken: string, epicBranch: string): EpicRecordV1 | null;
417
+ /** Record a failed landing attempt on a `closing` row (close stays resumable). */
418
+ export declare function recordEpicLandingAttempt(directory: string, epicKey: string, expectedToken: string, attempt: EpicLandingAttempt): EpicRecordV1 | null;
419
+ export interface EpicDeleteResult {
420
+ rowsDeleted: string[];
421
+ sentinelDeleted: boolean;
422
+ }
423
+ /**
424
+ * Delete the epic row(s) then the sentinel (compare-and-delete), under the
425
+ * lifecycle lock. `epicKey` null deletes every lifecycle row WITHOUT parsing
426
+ * payloads (corrupt-state repair for `close --abandon`).
427
+ */
428
+ export declare function deleteEpicState(directory: string, epicKey: string | null, token: string | null): EpicDeleteResult;
429
+ export type EpicSentinelRepair = 'none' | 'removed-stale-sentinel' | 'restored-sentinel' | 'rewrote-mismatched-sentinel';
430
+ /**
431
+ * Reconcile the sentinel with the authoritative row under the lifecycle lock:
432
+ * - sentinel without any row → remove the sentinel (row confirmed absent);
433
+ * - open row without sentinel → restore the sentinel from the row;
434
+ * - sentinel naming another epic than the row → rewrite it from the row.
435
+ * Corrupt rows are left alone (repair is `close --abandon`).
436
+ */
437
+ export declare function repairEpicSentinel(directory: string): EpicSentinelRepair;