opencode-swarm 7.188.12 → 7.190.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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-4g963859.js → auto-checkpoint-qvk04fya.js} +9 -9
  7. package/dist/cli/{checkpoint-y9nbjper.js → checkpoint-9q2r5c3j.js} +18 -20
  8. package/dist/cli/{coder-settlement-q8ea2jkw.js → coder-settlement-xcparbf3.js} +13 -13
  9. package/dist/cli/{config-doctor-4v5m6v30.js → config-doctor-by0ak49m.js} +23 -20
  10. package/dist/cli/{core-879105xb.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-t7g3hamd.js → curator-d7b88mzr.js} +44 -44
  13. package/dist/cli/{curator-drift-satfdnp2.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-y3ft2v24.js → doctor-knowledge-receipt-phase-id-kwrvnps9.js} +9 -9
  16. package/dist/cli/{evidence-summary-service-56rqzmrf.js → evidence-summary-service-mzr2q4bn.js} +21 -22
  17. package/dist/cli/{gate-evidence-zzmgph9z.js → gate-evidence-78jes9kq.js} +8 -8
  18. package/dist/cli/guardrail-explain-8kt2mxmw.js +83 -0
  19. package/dist/cli/{guardrail-log-sxv7q0xw.js → guardrail-log-2sthykag.js} +8 -8
  20. package/dist/cli/{guardrail-reset-37cr442b.js → guardrail-reset-2n746rzv.js} +44 -44
  21. package/dist/cli/{hive-promoter-hyeta8d6.js → hive-promoter-s7a5gwhz.js} +44 -44
  22. package/dist/cli/{index-e638qt2d.js → index-0qc7a88d.js} +3 -3
  23. package/dist/cli/{index-kap0d52p.js → index-1s95jrgc.js} +3 -3
  24. package/dist/cli/{index-9tvjwv01.js → index-2fpv2wmc.js} +1 -1
  25. package/dist/cli/{index-jgr1rsr1.js → index-2kpqqcch.js} +4 -4
  26. package/dist/cli/{index-msvf6p5y.js → index-3t9t9q2q.js} +44 -44
  27. package/dist/cli/{index-8q8783g0.js → index-3zwhxk98.js} +1 -1
  28. package/dist/cli/{index-0b172h6r.js → index-49e65y51.js} +5 -5
  29. package/dist/cli/{index-5pafc9x6.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-84pwb6cz.js → index-9gcqmgbc.js} +5 -5
  33. package/dist/cli/{index-9sps637v.js → index-9xxqdzp4.js} +4 -4
  34. package/dist/cli/{index-j0qep2zr.js → index-a5ca62vt.js} +1 -1
  35. package/dist/cli/{index-xy2403g9.js → index-ajzgeb2s.js} +2 -2
  36. package/dist/cli/{index-8xabhfwr.js → index-b2j231jy.js} +2 -2
  37. package/dist/cli/{index-19yt5p2t.js → index-dmq7p8s9.js} +6 -6
  38. package/dist/cli/{index-95pzyv9a.js → index-ej1hdw16.js} +5 -5
  39. package/dist/cli/{index-1krdj1sc.js → index-ejk34dnw.js} +127 -13
  40. package/dist/cli/{index-tbyvvq50.js → index-fb5s32c2.js} +862 -1221
  41. package/dist/cli/{index-ntda6tje.js → index-feaxsjrt.js} +10 -2
  42. package/dist/cli/{index-94fgbb34.js → index-ffgcdf49.js} +22147 -15396
  43. package/dist/cli/{index-fhtcx5ag.js → index-h0xhec3d.js} +4 -4
  44. package/dist/cli/{index-kaqdzhj3.js → index-kfevm0n5.js} +2 -2
  45. package/dist/cli/{index-z7gakzb5.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-tmn4pf7q.js → index-mp1s7b0n.js} +7 -7
  49. package/dist/cli/{index-f0jn2nvs.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-8jy605yf.js → index-npe10823.js} +7 -7
  53. package/dist/cli/{index-yc0y5fjt.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-qha2p47y.js → index-rgkzcw2a.js} +5 -5
  57. package/dist/cli/{index-zfbxfm2m.js → index-s8b67e6h.js} +33 -12
  58. package/dist/cli/{index-m3hhaf61.js → index-s92a5ta3.js} +4 -4
  59. package/dist/cli/{index-dd6p8xb5.js → index-sm01zpqe.js} +46 -46
  60. package/dist/cli/{index-q9kefh33.js → index-sxj143a4.js} +1 -1
  61. package/dist/cli/{index-2h13hjbf.js → index-t0gj8s46.js} +77 -16
  62. package/dist/cli/{index-3dqazqk4.js → index-t73adn9b.js} +2 -2
  63. package/dist/cli/{index-5cksakjf.js → index-tkhenmms.js} +101 -13
  64. package/dist/cli/{index-zsrtt42s.js → index-v3bmwmqw.js} +25 -1
  65. package/dist/cli/{index-w9tfb0am.js → index-vhn4e6rm.js} +7 -7
  66. package/dist/cli/{index-yjjyapbf.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-7z2pxxye.js → index-zyg1m9d0.js} +4 -4
  71. package/dist/cli/index.js +45 -45
  72. package/dist/cli/{knowledge-escalator-dzd35eb3.js → knowledge-escalator-n1ezv1zp.js} +15 -15
  73. package/dist/cli/{knowledge-events-wkr5ne5f.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-sme6sqrb.js → knowledge-store-mnxqcyet.js} +6 -6
  76. package/dist/cli/{knowledge-validator-jgzeawa8.js → knowledge-validator-z30113mp.js} +8 -8
  77. package/dist/cli/{mcp-egrbh8na.js → mcp-mnbqw2s5.js} +1 -1
  78. package/dist/cli/{model-preflight-h7azh4m4.js → model-preflight-ex482fsy.js} +44 -44
  79. package/dist/cli/{pending-delegations-pg284vsk.js → pending-delegations-j18afz5z.js} +12 -12
  80. package/dist/cli/{pr-subscriptions-hzc4fg25.js → pr-subscriptions-yngda8mz.js} +12 -12
  81. package/dist/cli/{pr-workflow-gate-j3r3g3q7.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-zg0a56c1.js → scan-cursor-8pwfg5g9.js} +7 -7
  84. package/dist/cli/{schema-p246rqkt.js → schema-e58ytmqt.js} +14 -2
  85. package/dist/cli/{scope-persistence-7qtsmx5c.js → scope-persistence-9qfx6ext.js} +16 -16
  86. package/dist/cli/{server-twhpwhgq.js → server-wxcfwsdx.js} +45 -45
  87. package/dist/cli/{skill-generator-7vfbmyrw.js → skill-generator-tbd4tq9r.js} +17 -17
  88. package/dist/cli/{snapshot-coordination-init-050hf69t.js → snapshot-coordination-init-sckyzb94.js} +44 -44
  89. package/dist/cli/{speckit-checkoff-d3v522cb.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-yy1tp8t9.js → worktree-collision-ownership-3wwfc72t.js} +14 -14
  93. package/dist/cli/{worktree-isolation-e56skbfw.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-fx1qnpsh.js +0 -82
  164. package/dist/cli/guardrail-explain-g03manbh.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,209 @@
1
+ /**
2
+ * `/swarm epic close` and `/swarm close` finalization (Epic v2 C1a + C1b).
3
+ *
4
+ * 1. refuse `epic-incomplete` (pending tasks) / `epic-orphaned` /
5
+ * `epic-state-unreadable` unless `--abandon`;
6
+ * 2. epic-branch epics (C1b), not abandoning: landing preflight BEFORE any
7
+ * mutation (M-f) — refuse `dirty-worktree` (changes outside `.swarm/`),
8
+ * `original-branch-missing`, `detached-head` (a detached commit that
9
+ * switching would orphan),
10
+ * `epic-branch-missing`, `landing-git-failed`; detect an
11
+ * already-landed state so a resumed close is idempotent;
12
+ * 3. CAS the row to `closing` (an interrupted close resumes from here);
13
+ * 4. write the close report (`epic-report-v2`: lifecycle facts + the
14
+ * epic's scorecard, `scorecard.ts`) to
15
+ * `.swarm/epic/reports/<reportKey>.json` and
16
+ * `.swarm/epic-prior/reports/<reportKey>.json` (newest 50 kept —
17
+ * `epic-prior/` survives `/swarm close`; `/swarm epic report` reads
18
+ * them);
19
+ * 5. land (`--land squash|merge|none`, default squash; see
20
+ * `epic-branch.ts`). A conflict or failure is rolled back, recorded on
21
+ * the row, and the close STOPS with the row still `closing` — rerunning
22
+ * `/swarm epic close` resumes. `--abandon` never lands: it switches back
23
+ * to the original branch when the tree is clean and keeps the branch;
24
+ * 6. delete the epic's refs `refs/swarm/epics/<epicKey>/*` (Epic v2 C3;
25
+ * the report captured their values in step 4) unless
26
+ * `epic.retain_refs: true`; merge the epic's learning posterior
27
+ * into the project prior `.swarm/epic-prior/learning.json` (Epic v2 C6,
28
+ * any outcome, once per epic instance — the prior survives
29
+ * `/swarm close`); rewrite the report with the landing, ref and
30
+ * learning outcome, delete the row, then compare-and-delete the
31
+ * sentinel, under the lifecycle lock. A failed landing keeps the refs
32
+ * and merges nothing (the close resumes).
33
+ * `--abandon` on unreadable state deletes every lifecycle row without
34
+ * parsing it, then the sentinel.
35
+ */
36
+ import type { Plan } from '../config/plan-schema.js';
37
+ import { loadPlanJsonOnly } from '../plan/manager.js';
38
+ import { listCoderSettlementWalStates } from '../workflow/coder-settlement.js';
39
+ import { type EpicConfigSource } from './config.js';
40
+ import { type EpicLandingAfterState, leaveEpicBranchOnAbandon, performEpicLanding, preflightEpicLanding } from './epic-branch.js';
41
+ import { type EpicLearningSettings } from './learning.js';
42
+ import { type EpicPriorMergeResult, loadEpicLearningView, mergeEpicPosteriorIntoPrior } from './learning-store.js';
43
+ import { deleteEpicState, type EpicCloseOutcome, type EpicLandMode, type EpicRecordV1, inspectEpic, markEpicClosing, recordEpicLandingAttempt } from './lifecycle.js';
44
+ import { deleteEpicRefs, readEpicRefs } from './markers.js';
45
+ import { completedBeforeEpic } from './next-wave.js';
46
+ import { type EpicScorecardV1 } from './scorecard.js';
47
+ /** Newest close reports kept under `.swarm/epic-prior/reports/`. */
48
+ export declare const EPIC_PRIOR_REPORTS_KEEP = 50;
49
+ export declare const EPIC_REPORTS_RELATIVE_DIR: string;
50
+ export declare const EPIC_PRIOR_REPORTS_RELATIVE_DIR: string;
51
+ /** File names of close reports (`<reportKey>.json`). */
52
+ export declare const EPIC_REPORT_NAME_RE: RegExp;
53
+ /** Close-report schema (`report.ts` reads only this version). */
54
+ export declare const EPIC_REPORT_SCHEMA = "epic-report-v2";
55
+ export interface EpicTaskSummary {
56
+ total: number;
57
+ completed: number;
58
+ closed: number;
59
+ pending: string[];
60
+ }
61
+ /** What close did with the epic branch. */
62
+ export interface EpicLandingSummary {
63
+ /** Requested mode; null when abandoning (never lands). */
64
+ mode: EpicLandMode | null;
65
+ status: 'not-applicable' | 'pending' | 'landed' | 'already-landed' | 'nothing-to-land' | 'checked-out-original' | 'left-in-place' | 'conflict' | 'failed';
66
+ epicBranch: string | null;
67
+ originalBranch: string | null;
68
+ conflictFiles: string[];
69
+ detail: string;
70
+ /**
71
+ * Where the repository actually ended up after a landing attempt (null
72
+ * when no landing git command ran).
73
+ */
74
+ after: EpicLandingAfterState | null;
75
+ }
76
+ /**
77
+ * The close report (`epic-report-v2`): lifecycle facts, the raw waves /
78
+ * outcomes / phases, the landing, refs and learning outcome, and the
79
+ * epic's {@link EpicScorecardV1} (which carries `forced`, `sizingAtStart`
80
+ * and the inherited `priorDigest`). Epic v2 C8 superseded the C1 report of
81
+ * this unreleased branch in place; no earlier version was ever shipped.
82
+ */
83
+ export interface EpicCloseReport {
84
+ schema: typeof EPIC_REPORT_SCHEMA;
85
+ reportKey: string;
86
+ epicKey: string;
87
+ planId: string;
88
+ planKey: string;
89
+ outcome: EpicCloseOutcome;
90
+ startedAt: string;
91
+ closedAt: string;
92
+ startedBySession: string;
93
+ /** The epic's scorecard at close (`scorecard.ts`). */
94
+ scorecard: EpicScorecardV1;
95
+ config: EpicRecordV1['config'];
96
+ git: EpicRecordV1['git'] & {
97
+ headAtClose: string | null;
98
+ };
99
+ /** Null when the plan is gone or no longer the epic's plan (orphaned). */
100
+ tasks: EpicTaskSummary | null;
101
+ /** Waves issued by `epic_next_wave` (frozen scopes, heads, status). */
102
+ waves: EpicRecordV1['waves'];
103
+ /** Per-task outcomes recorded at wave close. */
104
+ taskOutcomes: EpicRecordV1['tasks'];
105
+ /** Phase lifecycle (review runs, verdicts). */
106
+ phases: EpicRecordV1['phases'];
107
+ landing: EpicLandingSummary;
108
+ /**
109
+ * The epic's refs (`refs/swarm/epics/<epicKey>/*`, name → sha) captured
110
+ * at close, and what close did with them. Null for non-git epics.
111
+ */
112
+ refs: {
113
+ entries: Record<string, string>;
114
+ retained: boolean;
115
+ /** Refs that could not be deleted (or listed); empty when clean. */
116
+ deleteFailures: string[];
117
+ /** Why the refs could not be captured, when they could not. */
118
+ captureError?: string;
119
+ } | null;
120
+ /**
121
+ * Epic v2 C6: what the epic's learning contributed to the project prior
122
+ * (`priorPath`, kept across `/swarm close`); null until the close
123
+ * completes the merge step.
124
+ */
125
+ learning: (EpicPriorMergeResult & {
126
+ priorPath: string;
127
+ }) | null;
128
+ }
129
+ export type EpicCloseResult = {
130
+ status: 'no-epic';
131
+ repairedSentinel: boolean;
132
+ } | {
133
+ status: 'refused';
134
+ reason: 'epic-incomplete' | 'epic-orphaned' | 'epic-state-unreadable' | 'coders-live' | 'dirty-worktree' | 'epic-branch-missing' | 'original-branch-missing' | 'detached-head' | 'landing-git-failed';
135
+ details: string[];
136
+ } | {
137
+ /** Landing conflicted / failed: rolled back, row stays `closing`. */
138
+ status: 'landing-failed';
139
+ report: EpicCloseReport;
140
+ reportPaths: string[];
141
+ landing: EpicLandingSummary;
142
+ } | {
143
+ status: 'repaired-unreadable';
144
+ rowsDeleted: string[];
145
+ sentinelDeleted: boolean;
146
+ } | {
147
+ status: 'closed';
148
+ report: EpicCloseReport;
149
+ reportPaths: string[];
150
+ sentinelDeleted: boolean;
151
+ };
152
+ export interface EpicCloseOptions {
153
+ directory: string;
154
+ abandon: boolean;
155
+ /**
156
+ * `--land` (epic-branch epics). Absent ⇒ the mode a resumed close
157
+ * recorded, else `squash`. Ignored with `abandon`.
158
+ */
159
+ land?: EpicLandMode;
160
+ /** Overrides the outcome label (swarm-close finalization). */
161
+ outcome?: EpicCloseOutcome;
162
+ /** `epic.retain_refs`: keep the epic's refs after close. */
163
+ retainRefs?: boolean;
164
+ /** `epic.learning.*` (absent ⇒ the schema defaults). */
165
+ learning?: EpicLearningSettings;
166
+ }
167
+ /** Unique per epic instance: `<epicKey>-<startedAt compact UTC>`. */
168
+ export declare function epicReportKey(record: EpicRecordV1): string;
169
+ export declare function summarizePlanTasks(plan: Plan): EpicTaskSummary;
170
+ /** Keep the newest {@link EPIC_PRIOR_REPORTS_KEEP} reports (by mtime). */
171
+ export declare function pruneEpicPriorReports(directory: string): number;
172
+ declare function writeReport(directory: string, report: EpicCloseReport): string[];
173
+ export declare function closeEpic(options: EpicCloseOptions): Promise<EpicCloseResult>;
174
+ /**
175
+ * `/swarm close` hook, run BEFORE the archive stage (MINOR 1): an open epic
176
+ * is closed as `abandoned-by-swarm-close` so its report lands in
177
+ * `.swarm/epic/` (archived) and `.swarm/epic-prior/reports/` (kept).
178
+ *
179
+ * Epic-gated on the config close already loaded: with
180
+ * `epic.mode.enabled !== true` it returns null with NO I/O, so a
181
+ * non-Epic close is byte-identical (leftover state is left for
182
+ * `/swarm epic close --abandon`, which works regardless of config). With the
183
+ * gate on it acts when the sentinel exists, or — sentinel lost — when a
184
+ * lifecycle row exists in an existing swarm.db. Never throws.
185
+ */
186
+ export declare function finalizeOpenEpicOnSwarmClose(directory: string, config: EpicConfigSource): Promise<string | null>;
187
+ /**
188
+ * DI seam (AGENTS.md invariant 7). Restore in `afterEach`.
189
+ */
190
+ export declare const _internals: {
191
+ loadPlanJsonOnly: typeof loadPlanJsonOnly;
192
+ inspectEpic: typeof inspectEpic;
193
+ markEpicClosing: typeof markEpicClosing;
194
+ recordEpicLandingAttempt: typeof recordEpicLandingAttempt;
195
+ deleteEpicState: typeof deleteEpicState;
196
+ preflightEpicLanding: typeof preflightEpicLanding;
197
+ performEpicLanding: typeof performEpicLanding;
198
+ leaveEpicBranchOnAbandon: typeof leaveEpicBranchOnAbandon;
199
+ writeReport: typeof writeReport;
200
+ readEpicRefs: typeof readEpicRefs;
201
+ deleteEpicRefs: typeof deleteEpicRefs;
202
+ mergeEpicPosteriorIntoPrior: typeof mergeEpicPosteriorIntoPrior;
203
+ loadEpicLearningView: typeof loadEpicLearningView;
204
+ listCoderSettlementWalStates: typeof listCoderSettlementWalStates;
205
+ completedBeforeEpic: typeof completedBeforeEpic;
206
+ gitExec: (args: string[], cwd: string) => string;
207
+ now: () => number;
208
+ };
209
+ export {};
@@ -12,9 +12,10 @@
12
12
  * not invoke it, nothing changes anywhere.
13
13
  *
14
14
  * Path / co-change name reconciliation:
15
- * - Scope paths may arrive normalized but absolute (`{projectRoot}/src/x.ts`)
16
- * because `src/turbo/lean/planner.ts:getValidatedFiles` prepends `directory`
17
- * to relative scopes.
15
+ * - Planner scope paths are repo-relative (`getValidatedFiles` in
16
+ * `src/turbo/lean/partition-common.ts` normalizes them relative to the
17
+ * project root), but other callers may still pass normalized absolute
18
+ * paths (`{projectRoot}/src/x.ts`).
18
19
  * - Co-change paths come from `git log --name-only` and are always
19
20
  * repo-relative (e.g. `src/x.ts`).
20
21
  * - We bridge with a boundary-aware suffix match: a scope path matches a
@@ -22,7 +23,7 @@
22
23
  * This handles both absolute and relative scope paths without needing the
23
24
  * project root.
24
25
  */
25
- import type { CoChangeEntry } from '../../tools/co-change-analyzer.js';
26
+ import type { CoChangeEntry } from '../tools/co-change-analyzer.js';
26
27
  /**
27
28
  * Threshold for treating a co-change pair as a conflict signal.
28
29
  *
@@ -27,7 +27,7 @@
27
27
  * `_internals` DI seam so tests can stub it without `mock.module`.
28
28
  */
29
29
  import * as child_process from 'node:child_process';
30
- import { type CoChangeEntry, _internals as coChangeAnalyzer } from '../../tools/co-change-analyzer.js';
30
+ import { type CoChangeEntry, _internals as coChangeAnalyzer } from '../tools/co-change-analyzer.js';
31
31
  declare const execFileAsync: typeof child_process.execFile.__promisify__;
32
32
  export interface GetCoChangePairsOptions {
33
33
  /** Maximum commits the analyzer scans. Defaults to 500. */
@@ -57,6 +57,15 @@ export interface CoChangeData {
57
57
  * Both cases are signal-absent.
58
58
  */
59
59
  export declare function getCoChangeData(directory: string, options?: GetCoChangePairsOptions): Promise<CoChangeData>;
60
+ /**
61
+ * The co-change data last computed for `directory` (any HEAD), WITHOUT git:
62
+ * no subprocess, no scan — a read of the in-memory cache `getCoChangeData`
63
+ * fills. Null when nothing is cached (cold: the caller plans path-only and
64
+ * says so). The entry may be for an older HEAD; it is a planning estimate.
65
+ * Used by the `save_plan` plan-shaping seam (Epic v2 C7), which must not
66
+ * spawn git on a plan save.
67
+ */
68
+ export declare function peekCoChangeData(directory: string): CoChangeData | null;
60
69
  /**
61
70
  * Back-compat wrapper kept for the M2 path (`/swarm coupling` only needs
62
71
  * `pairs`). Capability C uses `getCoChangeData` directly for the
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Epic v2 C5 — per-component parallelism (pure).
3
+ *
4
+ * The Epic wave planner. Over the pending tasks of ONE phase it builds the
5
+ * conflict graph, splits it into connected components, gives every
6
+ * component a mode, and picks the next wave ({@link planNextEpicWave}).
7
+ * `epic_next_wave` (`wave-select.ts`) issues the wave it picks;
8
+ * `/swarm epic start`'s sizing dry-run ({@link dryRunEpicPhase}) repeats
9
+ * that same step over the tasks still pending, wave after wave, so it
10
+ * replays exactly the waves `epic_next_wave` would issue while every live
11
+ * scope equals the planning scope (`files_touched` until declared).
12
+ *
13
+ * ONE conflict predicate — `epicPairConflict(...).conflict` (path ∪
14
+ * co-change, `cochange-conflict.ts`) — is the edge relation here (computed
15
+ * through a pair index that is the same relation, see `cochangePartners`)
16
+ * and the pair relation of THE wave verdict (`computeEpicWaveVerdict`
17
+ * in `gate-policy.ts` → `computeParallelVerdict`, which the delegation gate
18
+ * repeats at dispatch). Both are `epicPairConflict(scopeA, scopeB, pairs,
19
+ * threshold).conflict` over the tasks' scopes:
20
+ * - the scopes are the same strings: a task enters a multi-task wave only
21
+ * with a live declared scope, and that live scope is what the wave
22
+ * freezes and the verdict reads;
23
+ * - the co-change pairs the wave freezes (`cochangePairsWithin`) are a
24
+ * SUBSET of the pairs used here, and `epicPairConflict` is monotone in
25
+ * its pairs (fewer pairs ⇒ fewer conflicts), so no edge here ⇒ no
26
+ * conflict in the verdict;
27
+ * - a task with no usable scope (the verdict's `unknown`) is exclusive
28
+ * here, so it never shares a wave.
29
+ * - learned scope expansion (Epic v2 C6, `learning.ts`) only ADDS edges:
30
+ * the path half runs over scope*(t) = scope(t) ∪ the files tasks
31
+ * declaring scope(t) were learned to co-write, a superset of the
32
+ * declared scope the verdict reads (path overlap is monotone in the
33
+ * scopes); the co-change half keeps the declared scopes (its rule is
34
+ * not monotone in the scope). The planner is stricter than the verdict.
35
+ * Hence every wave of ≥ 2 tasks picked here is `all_disjoint` under the
36
+ * verdict (`epic_next_wave` still asserts it before issuing).
37
+ *
38
+ * Model:
39
+ * - exclusive task: its scope touches a global file or a protected path
40
+ * (`isGlobalFile` / `isProtectedPath`, classified by the shared
41
+ * partition preflight), has no usable scope, or its declared scope
42
+ * lists a learned hot file (`learning.ts`; exact normalized path). It is its own component
43
+ * (mode `exclusive`) and
44
+ * always runs ALONE, before any other ready task;
45
+ * - every other task is a vertex; E = pairs that conflict (above, over
46
+ * the learned scope* for the path half);
47
+ * C = connected components of (vertices, E) (union-find);
48
+ * - density d_C = |E_C| / (|C| choose 2) (0 for a single task);
49
+ * - mode(C) = `serial-component` when d_C > `epic.mode.
50
+ * activation_threshold` (default 0.3): a densely coupled cluster — a hub
51
+ * file most of its tasks touch — runs ONE task per wave; otherwise
52
+ * `parallel`: its tasks share a wave whenever they have no edge (a
53
+ * sparse chain is one component whose non-adjacent tasks still run
54
+ * together).
55
+ * A hub file therefore costs one serial cluster, never the whole phase:
56
+ * tasks outside the cluster keep filling waves in parallel.
57
+ *
58
+ * Next wave (readiness via the shared partition preflight + `getReadyTasks`,
59
+ * cycle-safe topological order):
60
+ * 1. a ready exclusive task, alone;
61
+ * 2. otherwise greedy over the ready tasks — oldest component first
62
+ * (waves since the component last had a task issued: no component
63
+ * starves), then topological order: add t when it has no edge to a
64
+ * chosen task, its component has no chosen task yet if the component
65
+ * is `serial-component`, and the wave is below `maxParallel`.
66
+ */
67
+ import type { LeanTurboConfig } from '../config/schema.js';
68
+ import type { CoChangeEntry } from '../tools/co-change-analyzer.js';
69
+ import { type PartitionPreflight, type PlanTask as PartitionTask } from '../turbo/lean/partition-common.js';
70
+ import type { CoChangeThreshold } from './cochange-conflict.js';
71
+ import { type EpicCoWriteIndex } from './learning.js';
72
+ /** Default intra-component density threshold (`mode.activation_threshold`). */
73
+ export declare const DEFAULT_EPIC_DENSITY_THRESHOLD = 0.3;
74
+ /** Max task → component entries recorded per wave (wave tasks always kept). */
75
+ export declare const MAX_WAVE_COMPONENT_TASKS = 256;
76
+ export type EpicComponentMode = 'exclusive' | 'serial-component' | 'parallel';
77
+ export type EpicExclusiveReason = 'global-file' | 'protected-path' | 'no-scope' | 'hot-file';
78
+ /** Co-change signal (null = disabled by config ⇒ path-only conflicts). */
79
+ export interface EpicCochangeSignal {
80
+ pairs: CoChangeEntry[];
81
+ threshold: CoChangeThreshold;
82
+ }
83
+ /** The components recorded with a wave (`EpicWaveRecord.components`). */
84
+ export interface EpicWaveComponents {
85
+ /** Pending task of the phase at issue time → its component id. */
86
+ byTask: Record<string, string>;
87
+ /** Component id → mode. */
88
+ modes: Record<string, EpicComponentMode>;
89
+ /** Component id → intra-component density d_C. */
90
+ density: Record<string, number>;
91
+ /** Component id → why its (single) task is exclusive. */
92
+ exclusive: Record<string, EpicExclusiveReason>;
93
+ /** The density threshold in force. */
94
+ threshold: number;
95
+ /** True when `byTask` was capped at {@link MAX_WAVE_COMPONENT_TASKS}. */
96
+ truncated: boolean;
97
+ }
98
+ /** What a past wave contributes to component ages. */
99
+ export interface EpicWaveHistoryEntry {
100
+ taskIds: readonly string[];
101
+ components?: Pick<EpicWaveComponents, 'byTask'>;
102
+ }
103
+ export interface EpicConflictGraphInput {
104
+ /** Project root (scope paths are validated relative to it; no I/O). */
105
+ directory: string;
106
+ /** The phase's pending tasks. */
107
+ tasks: readonly PartitionTask[];
108
+ /** Scope per task: the live declared scope, else `files_touched`. */
109
+ scopes: Record<string, string[]>;
110
+ /** Lean config (risk policy of the shared preflight). */
111
+ leanConfig: LeanTurboConfig;
112
+ /** Learned hot files (`learning.ts`): tasks declaring them are exclusive. */
113
+ hotFiles: readonly string[];
114
+ /**
115
+ * Learned co-writes (`learning.ts`): the path half of the conflict graph
116
+ * runs over each task's expanded scope*. Null ⇒ no expansion.
117
+ */
118
+ coWrites: EpicCoWriteIndex | null;
119
+ cochange: EpicCochangeSignal | null;
120
+ }
121
+ export interface EpicConflictGraph {
122
+ /** Shared preflight: classification + cycle-safe topological order. */
123
+ preflight: PartitionPreflight;
124
+ /** Exclusive tasks and why. */
125
+ exclusive: Map<string, EpicExclusiveReason>;
126
+ /** Conflict edges among the non-exclusive tasks (symmetric). */
127
+ adjacency: Map<string, Set<string>>;
128
+ /** Task id → position in the topological order. */
129
+ topoIndex: Map<string, number>;
130
+ }
131
+ /** Build the conflict graph of one phase's pending tasks. */
132
+ export declare function buildEpicConflictGraph(input: EpicConflictGraphInput): EpicConflictGraph;
133
+ export interface EpicComponent {
134
+ /** Smallest member task id. */
135
+ id: string;
136
+ /** Member task ids, sorted. */
137
+ members: string[];
138
+ mode: EpicComponentMode;
139
+ /** Conflict edges inside the component. */
140
+ edgeCount: number;
141
+ /** d_C = edgeCount / (|C| choose 2); 0 for a single task. */
142
+ density: number;
143
+ exclusiveReason?: EpicExclusiveReason;
144
+ }
145
+ export interface EpicComponentPartition {
146
+ components: EpicComponent[];
147
+ /** Task id → component id. */
148
+ componentOf: Map<string, string>;
149
+ threshold: number;
150
+ }
151
+ /**
152
+ * Components (union-find) of `taskIds` — a subset of the graph's tasks —
153
+ * with their modes under `densityThreshold`.
154
+ */
155
+ export declare function partitionEpicComponents(graph: EpicConflictGraph, taskIds: Iterable<string>, densityThreshold: number): EpicComponentPartition;
156
+ /**
157
+ * Waves since each component last had a task issued (`history` oldest
158
+ * first). A wave served component C when one of its issued tasks shared a
159
+ * recorded component with a current member of C (or was a member). Never
160
+ * served ⇒ `history.length`.
161
+ */
162
+ export declare function componentAges(partition: EpicComponentPartition, history: readonly EpicWaveHistoryEntry[]): Map<string, number>;
163
+ export interface EpicWaveChoice {
164
+ taskIds: string[];
165
+ kind: 'parallel' | 'exclusive' | 'serial-component';
166
+ }
167
+ /**
168
+ * The next wave among `readyIds` (see the module header); null when none is
169
+ * ready.
170
+ */
171
+ export declare function chooseEpicWave(graph: EpicConflictGraph, partition: EpicComponentPartition, readyIds: readonly string[], maxParallel: number, ages: Map<string, number>): EpicWaveChoice | null;
172
+ /** Ready task ids: every dependency in `done` or satisfied outside. */
173
+ export declare function readyEpicTasks(graph: EpicConflictGraph, done: ReadonlySet<string>, satisfiedOutside: (taskId: string) => boolean): string[];
174
+ /** The recorded form of a partition (wave tasks always kept when capped). */
175
+ export declare function toWaveComponents(partition: EpicComponentPartition, waveTaskIds: readonly string[]): EpicWaveComponents;
176
+ export interface EpicNextWaveInput extends EpicConflictGraphInput {
177
+ maxParallel: number;
178
+ densityThreshold: number;
179
+ /** A dependency outside `tasks` is satisfied (completed + evidence). */
180
+ satisfiedOutside: (taskId: string) => boolean;
181
+ /** This phase's earlier waves, oldest first (component ages). */
182
+ history: readonly EpicWaveHistoryEntry[];
183
+ }
184
+ /**
185
+ * THE next-wave step over `tasks` (the phase's runnable pending tasks):
186
+ * graph → components → ready tasks → wave. `epic_next_wave`
187
+ * (`wave-select.ts`) issues its choice; {@link dryRunEpicPhase} repeats it.
188
+ */
189
+ export declare function planNextEpicWave(input: EpicNextWaveInput): {
190
+ partition: EpicComponentPartition;
191
+ choice: EpicWaveChoice | null;
192
+ };
193
+ export interface EpicPhaseDryRun {
194
+ /** Waves in issue order. */
195
+ waves: EpicWaveChoice[];
196
+ /**
197
+ * Tasks never scheduled: `blocked` tasks, a dependency cycle, tasks
198
+ * downstream of either — and, when the run stopped at its work budget,
199
+ * every task still pending (each then counts as one serial step: a
200
+ * pessimistic L).
201
+ */
202
+ unscheduled: string[];
203
+ /** Work units spent ({@link epicWaveWork} summed over the waves). */
204
+ work: number;
205
+ /** True when `maxWork` stopped the run before every task was planned. */
206
+ aborted: boolean;
207
+ }
208
+ /** Work units per scope path per wave (the shared preflight resolves each). */
209
+ export declare const EPIC_WORK_PER_SCOPE_PATH = 8;
210
+ /** Work units per task pair per wave (graph + component bookkeeping). */
211
+ export declare const EPIC_WORK_PER_TASK_PAIR = 6;
212
+ /**
213
+ * Deterministic cost estimate (work units ≈ path comparisons) of ONE
214
+ * planning step over `remaining` tasks with these scope sizes, `history`
215
+ * waves old: per-path preflight + per-pair graph work + the pairwise path
216
+ * comparisons (Σ_{i<j} |a_i|·|a_j|) + component aging. Callers that must
217
+ * stay bounded (start sizing, plan shaping) spend a budget of these.
218
+ */
219
+ export declare function epicWaveWork(scopeSizes: readonly number[], history: number): number;
220
+ /**
221
+ * Replay `epic_next_wave` over one phase as if every issued wave completed
222
+ * (the `/swarm epic start` sizing dry-run): each step is
223
+ * {@link planNextEpicWave} over the tasks still pending — the graph and
224
+ * components rebuilt, the recorded components aging them — exactly the
225
+ * sequence `epic_next_wave` issues when every live scope equals the
226
+ * planning scope. `blocked` tasks never run (as in `epic_next_wave`), so
227
+ * they and their dependents are unscheduled. Dependencies outside the
228
+ * phase count as satisfied (phases run in order).
229
+ *
230
+ * `maxWork` bounds the run: before each step its {@link epicWaveWork} is
231
+ * charged; a step that would exceed the budget is not run (`aborted`, the
232
+ * rest unscheduled). Without it the run is unbounded (and identical).
233
+ */
234
+ export declare function dryRunEpicPhase(input: EpicConflictGraphInput & {
235
+ maxParallel: number;
236
+ densityThreshold: number;
237
+ maxWork?: number;
238
+ }): EpicPhaseDryRun;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Epic Mode config master gates.
3
+ *
4
+ * `epic.mode.enabled` and `epic.cochange.enabled` are documented as opt-in
5
+ * master gates (both default `false`). This module is the single source of
6
+ * truth for reading them so every Epic entry point (slash commands, architect
7
+ * tools, agent tool maps, the hot-path seams, phase-complete readiness)
8
+ * agrees. Both read the effective Epic block via `resolveEpicConfig`
9
+ * (`./config.ts`: top-level `epic`, legacy `turbo.epic` fallback).
10
+ *
11
+ * Pure predicates take an already-loaded `PluginConfig`; callers that only
12
+ * have a directory use the `*ForDirectory` variants, which load config via
13
+ * `loadPluginConfigWithMeta` and fail closed (return `false`) if loading
14
+ * throws.
15
+ */
16
+ import { loadPluginConfigWithMeta } from '../config/loader.js';
17
+ /** The pure gates live in `./config.ts`; re-exported here for gate callers. */
18
+ export { isEpicCochangeConfigEnabled, isEpicModeConfigEnabled, } from './config.js';
19
+ /** Remediation text shown when Epic Mode is used without the config opt-in. */
20
+ export declare const EPIC_MODE_CONFIG_DISABLED_MESSAGE: string;
21
+ export declare const _internals: {
22
+ loadPluginConfigWithMeta: typeof loadPluginConfigWithMeta;
23
+ };
24
+ /** Directory-keyed variant of {@link isEpicModeConfigEnabled}; fails closed. */
25
+ export declare function isEpicModeConfigEnabledForDirectory(directory: string): boolean;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Epic Mode config — the ONE place that knows where Epic settings live.
3
+ *
4
+ * Canonical path: the top-level `epic` block of `opencode-swarm.json`
5
+ * (`{ "epic": { "mode": { "enabled": true } } }`). It needs no `turbo` block:
6
+ * Epic Mode is its own mode, not a Turbo overlay.
7
+ *
8
+ * Legacy path: `turbo.epic` (Epic v1 and Epic v2 before C9). It is accepted
9
+ * PERMANENTLY — there is no removal date — and handled in two layers:
10
+ *
11
+ * 1. Raw config (the loader, `src/config/loader.ts` step 0, and the raw
12
+ * re-reads in `/swarm config doctor`): {@link migrateLegacyEpicConfig}
13
+ * moves `turbo.epic` to top-level `epic` in EACH config file BEFORE the
14
+ * user + project files are merged and before Zod parsing. Within one
15
+ * file the move is a per-key deep merge in which that file's top-level
16
+ * `epic` wins every key both set; across files the normal precedence
17
+ * applies afterwards (project over user), whichever path each file used.
18
+ * Doing it on the raw object is what makes the merge exact (after
19
+ * parsing, schema defaults are indistinguishable from keys the user
20
+ * wrote). The `turbo` block left behind is unchanged apart from losing
21
+ * `epic`, and is dropped when nothing else was in it — so a legacy
22
+ * `turbo.epic` no longer needs `turbo.strategy`. The loader warns once
23
+ * when any file carries `turbo.epic` ({@link LEGACY_EPIC_CONFIG_WARNING}),
24
+ * names recovered keys that came from it `… (from turbo.epic)`
25
+ * ({@link annotateLegacyEpicKeys}), and `/swarm config doctor` reports
26
+ * `legacy-epic-config-path` per file (report-only), naming validation
27
+ * issues at the path the user wrote ({@link legacyEpicIssuePath}).
28
+ * 2. Parsed config: {@link resolveEpicConfig} returns `config.epic`, falling
29
+ * back to `config.turbo.epic` for a config parsed WITHOUT the loader
30
+ * (direct `PluginConfigSchema.parse`, tests). When a parsed config carries
31
+ * both, the top-level block wins whole — the per-key merge needs the raw
32
+ * config and is the loader's job.
33
+ *
34
+ * Every Epic config reader goes through {@link resolveEpicConfig} (directly or
35
+ * via `config-gate.ts`). This module has no runtime imports, so the loader and
36
+ * shared files can use it without pulling in Epic code.
37
+ */
38
+ import type { EpicConfig } from '../config/schema.js';
39
+ /** Advisory printed once per config-advisory dedup window (process / session start). */
40
+ export declare const LEGACY_EPIC_CONFIG_WARNING: string;
41
+ /**
42
+ * The parts of a parsed config Epic settings can live in. Generic over the
43
+ * Epic block's shape so structural mini-configs (e.g. the Full-Auto
44
+ * classifier input) resolve through the same function as `PluginConfig`.
45
+ */
46
+ export type EpicConfigSource<E = EpicConfig> = {
47
+ epic?: E | null;
48
+ turbo?: {
49
+ epic?: E | null;
50
+ } | null;
51
+ } | null | undefined;
52
+ /**
53
+ * The effective Epic settings of a parsed config: top-level `epic`, else the
54
+ * legacy `turbo.epic` (see the module doc for the per-key merge). `undefined`
55
+ * when neither is set — every Epic gate then reads as off.
56
+ */
57
+ export declare function resolveEpicConfig<E = EpicConfig>(config: EpicConfigSource<E>): E | undefined;
58
+ /**
59
+ * Master gate: `epic.mode.enabled === true` (absent block ⇒ false). Pure; the
60
+ * directory-keyed variant and the disabled message live in `config-gate.ts`,
61
+ * which re-exports this.
62
+ */
63
+ export declare function isEpicModeConfigEnabled(config: EpicConfigSource<{
64
+ mode?: {
65
+ enabled?: boolean;
66
+ } | null;
67
+ }>): boolean;
68
+ /** Co-change gate: `epic.cochange.enabled === true` (absent block ⇒ false). */
69
+ export declare function isEpicCochangeConfigEnabled(config: EpicConfigSource<{
70
+ cochange?: {
71
+ enabled?: boolean;
72
+ } | null;
73
+ }>): boolean;
74
+ /** True when a raw (pre-parse) config carries the legacy `turbo.epic` key. */
75
+ export declare function hasLegacyEpicConfig(raw: unknown): boolean;
76
+ /**
77
+ * Move a raw config's legacy `turbo.epic` to top-level `epic` (module doc,
78
+ * layer 1). Returns the SAME object when there is no legacy key — a non-Epic
79
+ * config is never copied or changed. A non-object legacy value is moved as-is
80
+ * when top-level `epic` is absent (so validation reports it at `epic`), and
81
+ * dropped when top-level `epic` is set (top-level wins). Applied per config
82
+ * FILE, before the user and project files are merged.
83
+ */
84
+ export declare function migrateLegacyEpicConfig(raw: Record<string, unknown>): Record<string, unknown>;
85
+ /**
86
+ * The path a validation issue of a MIGRATED raw config file should be reported
87
+ * at, given the file as the user wrote it: an `epic.*` path maps back to
88
+ * `turbo.epic.*` when the file set only the legacy path (no top-level `epic`).
89
+ * Every other path — and every path of a file without `turbo.epic` — is
90
+ * returned unchanged.
91
+ */
92
+ export declare function legacyEpicIssuePath(raw: unknown, path: readonly PropertyKey[]): PropertyKey[];
93
+ /**
94
+ * Label recovered config keys that came from a legacy `turbo.epic` block:
95
+ * `epic.<k>` becomes `epic.<k> (from turbo.epic)` when `<k>` was written under
96
+ * `turbo.epic` in one of `rawFiles` (as the user wrote them, before
97
+ * migration) and under no top-level `epic`; the whole `epic` section gets
98
+ * the label when some file had `turbo.epic` and none had top-level `epic`.
99
+ * Returns `keys` itself when no file carries `turbo.epic`.
100
+ */
101
+ export declare function annotateLegacyEpicKeys(keys: string[], rawFiles: readonly unknown[]): string[];
@@ -16,7 +16,7 @@
16
16
  * question "what would Capability A say about this plan if I asked it
17
17
  * about every task pair?".
18
18
  */
19
- import type { CoChangeEntry } from '../../tools/co-change-analyzer.js';
19
+ import type { CoChangeEntry } from '../tools/co-change-analyzer.js';
20
20
  import { type CoChangeThreshold, type EpicPairVerdict } from './cochange-conflict.js';
21
21
  /** A task as `epic` mode sees it: identifier + declared file scope. */
22
22
  export interface CouplingTask {
@@ -54,6 +54,8 @@ export interface CouplingReport {
54
54
  /** Top-N modules with a human-readable rank line for each. */
55
55
  roadmap: string[];
56
56
  }
57
+ /** Whether the co-change conflict signal contributed to `p` (`/swarm coupling`). */
58
+ export type EpicCochangeSignalState = 'enabled' | 'disabled-by-config';
57
59
  export interface ComputeCouplingReportOptions {
58
60
  /** Cap on roadmap rank entries. Default 5. */
59
61
  roadmapTop?: number;