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.
- package/README.md +42 -6
- package/binaries/win32-arm64/swarm-sandbox-runner.exe +0 -0
- package/binaries/win32-x64/swarm-sandbox-runner.exe +0 -0
- package/dist/agents/architect.d.ts +1 -1
- package/dist/background/completion-observer.d.ts +6 -0
- package/dist/cli/{auto-checkpoint-m7c4x8bk.js → auto-checkpoint-26m3h5f4.js} +9 -9
- package/dist/cli/{checkpoint-m7q9886q.js → checkpoint-7a0achpv.js} +18 -20
- package/dist/cli/{coder-settlement-hh81z2f7.js → coder-settlement-dsb1t39t.js} +13 -13
- package/dist/cli/{config-doctor-0rexy3xt.js → config-doctor-9txawzng.js} +23 -20
- package/dist/cli/{core-ays6stz4.js → core-kv7jz9ty.js} +4 -4
- package/dist/cli/{curation-policy-65ckmmd5.js → curation-policy-6p6fcfjb.js} +6 -6
- package/dist/cli/{curator-dfzkfgs0.js → curator-26yzg8wp.js} +44 -44
- package/dist/cli/{curator-drift-nts5atks.js → curator-drift-kxn5sk3z.js} +9 -9
- package/dist/cli/curator-llm-factory-hs2y9kmw.js +82 -0
- package/dist/cli/{doctor-knowledge-receipt-phase-id-ec40k5hn.js → doctor-knowledge-receipt-phase-id-z8wea03e.js} +9 -9
- package/dist/cli/{evidence-summary-service-vgtm72g5.js → evidence-summary-service-bg5zq78s.js} +21 -22
- package/dist/cli/{gate-evidence-vmn65kxe.js → gate-evidence-0beg9c7r.js} +8 -8
- package/dist/cli/guardrail-explain-n8bm0hpm.js +83 -0
- package/dist/cli/{guardrail-log-hc8fjm4x.js → guardrail-log-r2zazmbn.js} +8 -8
- package/dist/cli/{guardrail-reset-ct00w300.js → guardrail-reset-0q54t44w.js} +44 -44
- package/dist/cli/{hive-promoter-2xax6van.js → hive-promoter-7dr5b9zv.js} +44 -44
- package/dist/cli/{index-9gqb5nkj.js → index-0gve0mkj.js} +4 -4
- package/dist/cli/{index-34vsyj15.js → index-0yrg10jx.js} +5 -5
- package/dist/cli/{index-mfxef20k.js → index-0z8vdkvg.js} +4 -4
- package/dist/cli/{index-2nexf5vz.js → index-1f084tqn.js} +22223 -15395
- package/dist/cli/{index-qx3g1bbe.js → index-26e2s6r1.js} +1 -1
- package/dist/cli/{index-kqegvngt.js → index-2c47efpt.js} +4 -4
- package/dist/cli/{index-9tvjwv01.js → index-2fpv2wmc.js} +1 -1
- package/dist/cli/{index-8q8783g0.js → index-3zwhxk98.js} +1 -1
- package/dist/cli/{index-9j3ybyfs.js → index-4a15347d.js} +7 -7
- package/dist/cli/{index-5cfhwd65.js → index-67a6mk20.js} +2 -2
- package/dist/cli/{index-d3rjvm9e.js → index-7vnr5cmp.js} +2 -2
- package/dist/cli/{index-twb3f4tk.js → index-8bx6vhzk.js} +1 -1
- package/dist/cli/{index-0gejy4cx.js → index-ay8ggtgr.js} +2 -2
- package/dist/cli/{index-8xabhfwr.js → index-b2j231jy.js} +2 -2
- package/dist/cli/{index-cbqkag7q.js → index-bcwmn4d7.js} +3 -3
- package/dist/cli/{index-fbgjj7h5.js → index-bfyh3gpe.js} +101 -13
- package/dist/cli/{index-skdpgjar.js → index-cewpxcwm.js} +862 -1221
- package/dist/cli/{index-bzbxn24k.js → index-dbdkh7pq.js} +5 -5
- package/dist/cli/{index-w4zscz6p.js → index-dnp4qtpf.js} +5 -5
- package/dist/cli/{index-waxvpwbv.js → index-efztf4yw.js} +127 -13
- package/dist/cli/{index-4p3jh14t.js → index-enq166kw.js} +4 -4
- package/dist/cli/{index-g03m5jhw.js → index-f46dbyew.js} +3 -3
- package/dist/cli/{index-ntda6tje.js → index-feaxsjrt.js} +10 -2
- package/dist/cli/{index-89f56ej4.js → index-fthhppnc.js} +46 -46
- package/dist/cli/{index-wqa31xnf.js → index-gqb8etv4.js} +7 -7
- package/dist/cli/{index-1cwmj1gs.js → index-ha0w0a7t.js} +77 -16
- package/dist/cli/{index-nhsafbrh.js → index-hx1fmywk.js} +5 -5
- package/dist/cli/{index-kaqdzhj3.js → index-kfevm0n5.js} +2 -2
- package/dist/cli/{index-z29hksyw.js → index-m28dzyy8.js} +26 -10
- package/dist/cli/{index-x1eh2636.js → index-mcv0v2dp.js} +1 -1
- package/dist/cli/{index-9czp2dcg.js → index-mjfpaxw3.js} +7 -7
- package/dist/cli/{index-f6exny9m.js → index-n8q43eff.js} +2 -2
- package/dist/cli/{index-7hr5m7yp.js → index-nkgzkt7c.js} +1 -1
- package/dist/cli/{index-p2tqzwh6.js → index-qs9myayh.js} +5 -10
- package/dist/cli/{index-hh7pa3yk.js → index-r9yvpr2m.js} +2 -2
- package/dist/cli/{index-cdjxzs2r.js → index-rjzn7hf0.js} +7 -7
- package/dist/cli/{index-zfbxfm2m.js → index-s8b67e6h.js} +33 -12
- package/dist/cli/{index-bskex45z.js → index-s9cgnd2z.js} +2 -2
- package/dist/cli/{index-cj4mp0fm.js → index-sc7t1tev.js} +6 -6
- package/dist/cli/{index-3dx0c7ve.js → index-sfjpbnqs.js} +44 -44
- package/dist/cli/{index-16m2m4ah.js → index-spgfs5y7.js} +1 -1
- package/dist/cli/{index-q9kefh33.js → index-sxj143a4.js} +1 -1
- package/dist/cli/{index-zsrtt42s.js → index-v3bmwmqw.js} +25 -1
- package/dist/cli/{index-5v09zw1q.js → index-v3j9fk16.js} +18 -8
- package/dist/cli/{index-2btybj8w.js → index-xdq6mak1.js} +4 -4
- package/dist/cli/{index-k8sarme1.js → index-y9v41185.js} +1 -1
- package/dist/cli/{index-4a7j5f60.js → index-yy5yyyst.js} +4 -4
- package/dist/cli/{index-1xa8wq76.js → index-z3ahy9c2.js} +2 -2
- package/dist/cli/{index-wrdktgpy.js → index-zk6ttjkt.js} +2 -2
- package/dist/cli/index.js +45 -45
- package/dist/cli/{knowledge-escalator-v3c8w86g.js → knowledge-escalator-hdrqt5pj.js} +15 -15
- package/dist/cli/{knowledge-events-kagaccj9.js → knowledge-events-ttebc8gn.js} +13 -13
- package/dist/cli/{knowledge-link-h4pdcnya.js → knowledge-link-6hw3z6wn.js} +5 -5
- package/dist/cli/{knowledge-store-ywxjk5pb.js → knowledge-store-8cxv6w2n.js} +6 -6
- package/dist/cli/{knowledge-validator-210gkpec.js → knowledge-validator-kr5xmqpa.js} +8 -8
- package/dist/cli/{mcp-58hgx6av.js → mcp-jtk79ygp.js} +1 -1
- package/dist/cli/{model-preflight-e69b48n0.js → model-preflight-d6tt7479.js} +44 -44
- package/dist/cli/{pending-delegations-g24d3qa9.js → pending-delegations-ttk09f50.js} +12 -12
- package/dist/cli/{pr-subscriptions-qprh2zsm.js → pr-subscriptions-2kbeezsr.js} +12 -12
- package/dist/cli/{pr-workflow-gate-pj7dhkv4.js → pr-workflow-gate-3ra6p7k4.js} +44 -44
- package/dist/cli/{runner-4hfmp1ag.js → runner-ssnsje35.js} +6 -6
- package/dist/cli/{scan-cursor-s5wsxxd1.js → scan-cursor-zzccy117.js} +7 -7
- package/dist/cli/{schema-smgngq9m.js → schema-9prswg1x.js} +14 -2
- package/dist/cli/{scope-persistence-tbv2ar4w.js → scope-persistence-eqm8vwnx.js} +16 -16
- package/dist/cli/{server-7p2ds5w2.js → server-swjre81h.js} +45 -45
- package/dist/cli/{skill-generator-r4hsne71.js → skill-generator-d8dk9vps.js} +17 -17
- package/dist/cli/{snapshot-coordination-init-ydjwh56d.js → snapshot-coordination-init-f49y0zr0.js} +44 -44
- package/dist/cli/{speckit-checkoff-aj75vmjq.js → speckit-checkoff-pnb6q9gd.js} +5 -5
- package/dist/cli/{telemetry-kvhe94gs.js → telemetry-9szfdfqv.js} +1 -1
- package/dist/cli/{workspace-snapshot-30kdf018.js → workspace-snapshot-9f12p1s1.js} +3 -3
- package/dist/cli/{worktree-collision-ownership-0bxj9f0t.js → worktree-collision-ownership-3c7xyc4p.js} +14 -14
- package/dist/cli/{worktree-isolation-h0c22pm2.js → worktree-isolation-jggdf28m.js} +46 -44
- package/dist/commands/coupling.d.ts +21 -4
- package/dist/commands/epic-learning.d.ts +28 -0
- package/dist/commands/epic.d.ts +81 -36
- package/dist/commands/registry.d.ts +8 -8
- package/dist/commands/turbo.d.ts +16 -1
- package/dist/config/constants.d.ts +11 -1
- package/dist/config/loader.d.ts +2 -0
- package/dist/config/schema.d.ts +291 -77
- package/dist/db/coordination-store.d.ts +6 -0
- package/dist/epic/close.d.ts +209 -0
- package/dist/{turbo/epic → epic}/cochange-conflict.d.ts +5 -4
- package/dist/{turbo/epic → epic}/cochange-source.d.ts +10 -1
- package/dist/epic/components.d.ts +238 -0
- package/dist/epic/config-gate.d.ts +25 -0
- package/dist/epic/config.d.ts +101 -0
- package/dist/{turbo/epic → epic}/coupling-report.d.ts +3 -1
- package/dist/epic/declared-scopes.d.ts +37 -0
- package/dist/epic/epic-branch.d.ts +157 -0
- package/dist/epic/gate-policy.d.ts +114 -0
- package/dist/epic/git-once.d.ts +26 -0
- package/dist/epic/learning-store.d.ts +176 -0
- package/dist/epic/learning.d.ts +198 -0
- package/dist/epic/legacy-migration.d.ts +26 -0
- package/dist/epic/lifecycle.d.ts +437 -0
- package/dist/epic/markers.d.ts +123 -0
- package/dist/epic/merge-epoch.d.ts +90 -0
- package/dist/epic/next-wave-format.d.ts +101 -0
- package/dist/epic/next-wave.d.ts +102 -0
- package/dist/epic/phase-readiness.d.ts +221 -0
- package/dist/epic/plan-key.d.ts +104 -0
- package/dist/epic/plan-shaping-seam.d.ts +112 -0
- package/dist/epic/planning-signals.d.ts +38 -0
- package/dist/epic/report.d.ts +152 -0
- package/dist/epic/residue-commit.d.ts +151 -0
- package/dist/epic/scorecard.d.ts +130 -0
- package/dist/epic/shaping-sizing.d.ts +108 -0
- package/dist/epic/shaping-suggestions.d.ts +172 -0
- package/dist/epic/shaping.d.ts +112 -0
- package/dist/epic/sizing.d.ts +60 -0
- package/dist/epic/start.d.ts +187 -0
- package/dist/epic/task-landing.d.ts +83 -0
- package/dist/epic/wave-close.d.ts +112 -0
- package/dist/epic/wave-select.d.ts +95 -0
- package/dist/evaluation/contracts.d.ts +1 -1
- package/dist/evaluation/public-api.d.ts +1 -1
- package/dist/full-auto/policy.d.ts +12 -1
- package/dist/hooks/delegation-gate/worktree-isolation.d.ts +16 -0
- package/dist/hooks/delegation-gate.d.ts +11 -0
- package/dist/hooks/guardrails/index.d.ts +7 -0
- package/dist/hooks/promotion-evidence-store.d.ts +2 -2
- package/dist/index.js +814 -807
- package/dist/plan/manager.d.ts +6 -8
- package/dist/plan/parallel-verdict.d.ts +13 -2
- package/dist/retention/caps.d.ts +1 -1
- package/dist/review/contracts.d.ts +8 -1
- package/dist/services/config-doctor.d.ts +38 -0
- package/dist/session/snapshot-reader.d.ts +2 -0
- package/dist/session/snapshot-writer.d.ts +13 -2
- package/dist/state.d.ts +13 -18
- package/dist/tools/epic-next-wave.d.ts +20 -0
- package/dist/tools/epic-phase-review.d.ts +43 -0
- package/dist/tools/index.d.ts +2 -3
- package/dist/tools/manifest.d.ts +2 -3
- package/dist/tools/phase-complete.d.ts +5 -1
- package/dist/tools/save-plan.d.ts +7 -0
- package/dist/tools/tool-metadata.d.ts +4 -8
- package/dist/worktree/merge.d.ts +14 -1
- package/opencode-swarm.schema.json +273 -28
- package/package.json +1 -1
- package/dist/cli/curator-llm-factory-nrn6n2p1.js +0 -82
- package/dist/cli/guardrail-explain-9jmr8ezt.js +0 -83
- package/dist/tools/epic-plan-waves.d.ts +0 -79
- package/dist/tools/epic-record-divergence.d.ts +0 -73
- package/dist/tools/epic-run-phase.d.ts +0 -179
- package/dist/turbo/epic/activation.d.ts +0 -193
- package/dist/turbo/epic/calibration-engine.d.ts +0 -88
- package/dist/turbo/epic/calibration.d.ts +0 -80
- package/dist/turbo/epic/divergence-recorder.d.ts +0 -123
- package/dist/turbo/epic/index.d.ts +0 -24
- package/dist/turbo/epic/promotion-evidence.d.ts +0 -42
- package/dist/turbo/epic/state.d.ts +0 -69
- package/dist/turbo/epic/task-commit.d.ts +0 -110
- package/dist/turbo/epic/upstream-commits.d.ts +0 -82
- package/dist/turbo/epic/wave-planner.d.ts +0 -83
|
@@ -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
|
-
* -
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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;
|