opencode-swarm 7.86.0 → 7.87.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/cli/{capability-probe-jevmgwmf.js → capability-probe-wsjzcp48.js} +2 -2
  2. package/dist/cli/{config-doctor-zejarrr6.js → config-doctor-6h64pn8n.js} +4 -4
  3. package/dist/cli/{dispatch-k86d928w.js → dispatch-kb69qw40.js} +3 -3
  4. package/dist/cli/{evidence-summary-service-g2znnd33.js → evidence-summary-service-gg5m9z57.js} +4 -4
  5. package/dist/cli/{guardrail-explain-rtd1x26f.js → guardrail-explain-scym5r5y.js} +13 -13
  6. package/dist/cli/{guardrail-log-80116wmz.js → guardrail-log-eegabqcp.js} +5 -5
  7. package/dist/cli/{index-0sxvwjt0.js → index-1cb4wxnm.js} +2 -2
  8. package/dist/cli/{index-zfsbaaqh.js → index-5e4e2hvv.js} +1 -1
  9. package/dist/cli/{index-vq2321gg.js → index-5hvbw5xh.js} +2 -2
  10. package/dist/cli/{index-5cb86007.js → index-5vpe6vq9.js} +1 -1
  11. package/dist/cli/{index-red8fm8p.js → index-89xjr3h4.js} +1162 -214
  12. package/dist/cli/{index-f8r50m3h.js → index-adz3nk9b.js} +2 -2
  13. package/dist/cli/{index-jwz50183.js → index-dsjyfd3g.js} +14 -14
  14. package/dist/cli/{index-ckntc5gf.js → index-gn8n22th.js} +2 -2
  15. package/dist/cli/{index-5q66xc88.js → index-gwzpy671.js} +2699 -1403
  16. package/dist/cli/{index-hw9b2xng.js → index-q9h0wb04.js} +36 -3
  17. package/dist/cli/{index-d9fbxaqd.js → index-s8bj492g.js} +1 -1
  18. package/dist/cli/{index-7r2b453y.js → index-ts2j1wjr.js} +2 -2
  19. package/dist/cli/{index-hz59hg4h.js → index-v4fcn4tr.js} +1 -1
  20. package/dist/cli/{index-eb85wtx9.js → index-vqyfscxd.js} +2 -2
  21. package/dist/cli/{index-yx44zd0p.js → index-zgwm4ryv.js} +9 -1
  22. package/dist/cli/index.js +12 -12
  23. package/dist/cli/{pending-delegations-rd40tv9s.js → pending-delegations-35fvcj7z.js} +3 -3
  24. package/dist/cli/{pr-subscriptions-y1nn36e5.js → pr-subscriptions-b18n1yd8.js} +4 -4
  25. package/dist/cli/{schema-8d32b2v6.js → schema-84146tvk.js} +3 -1
  26. package/dist/cli/{skill-generator-a5ehggyg.js → skill-generator-3pvpk4y2.js} +2 -2
  27. package/dist/commands/coupling.d.ts +36 -0
  28. package/dist/commands/epic.d.ts +52 -0
  29. package/dist/commands/registry.d.ts +18 -2
  30. package/dist/config/constants.d.ts +1 -0
  31. package/dist/config/schema.d.ts +145 -0
  32. package/dist/git/branch.d.ts +22 -1
  33. package/dist/hooks/delegation-gate/worktree-merge-status.d.ts +86 -0
  34. package/dist/index.js +8577 -5858
  35. package/dist/memory/schema.d.ts +3 -3
  36. package/dist/memory/scoring.d.ts +18 -0
  37. package/dist/memory/sqlite-provider.d.ts +10 -0
  38. package/dist/plan/manager.d.ts +10 -0
  39. package/dist/state.d.ts +16 -0
  40. package/dist/tools/epic-plan-waves.d.ts +79 -0
  41. package/dist/tools/epic-record-divergence.d.ts +73 -0
  42. package/dist/tools/epic-run-phase.d.ts +179 -0
  43. package/dist/tools/index.d.ts +3 -0
  44. package/dist/tools/manifest.d.ts +3 -0
  45. package/dist/tools/tool-metadata.d.ts +12 -0
  46. package/dist/turbo/epic/activation.d.ts +193 -0
  47. package/dist/turbo/epic/calibration-engine.d.ts +88 -0
  48. package/dist/turbo/epic/calibration.d.ts +65 -0
  49. package/dist/turbo/epic/cochange-conflict.d.ts +79 -0
  50. package/dist/turbo/epic/cochange-source.d.ts +80 -0
  51. package/dist/turbo/epic/coupling-report.d.ts +85 -0
  52. package/dist/turbo/epic/divergence-recorder.d.ts +112 -0
  53. package/dist/turbo/epic/index.d.ts +24 -0
  54. package/dist/turbo/epic/promotion-evidence.d.ts +42 -0
  55. package/dist/turbo/epic/state.d.ts +85 -0
  56. package/dist/turbo/epic/task-commit.d.ts +110 -0
  57. package/dist/turbo/epic/upstream-commits.d.ts +82 -0
  58. package/dist/turbo/epic/wave-planner.d.ts +83 -0
  59. package/dist/turbo/lean/partition-common.d.ts +85 -0
  60. package/dist/turbo/lean/planner.d.ts +12 -20
  61. package/dist/utils/index.d.ts +1 -1
  62. package/dist/utils/logger.d.ts +19 -0
  63. package/package.json +1 -1
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Epic Mode activation decision (Capability C).
3
+ *
4
+ * `decideEpicActivation(...)` is the pure heart of M3: given a plan, a
5
+ * co-change pair list, and the activation thresholds, it returns a
6
+ * structured `promote | demote` verdict with the rationale fields a
7
+ * caller can persist for audit. Pure function — no I/O.
8
+ *
9
+ * Three independent gates must all pass for promotion:
10
+ *
11
+ * 1. **p-threshold gate.** Compute the coupling coefficient `p` over
12
+ * the plan's task graph using Capability A's `epicPairConflict` (via
13
+ * Capability B's `computeCouplingReport`). Promote only when
14
+ * `p <= activation_threshold`.
15
+ *
16
+ * 2. **Hot-module gate.** No task in scope may touch a Lean Turbo
17
+ * "global" or "protected" path — these are the same lists Lean
18
+ * Turbo already maintains (reused by import; not duplicated).
19
+ * Touching a hot module forces serial regardless of `p`.
20
+ *
21
+ * 3. **Greenfield gate.** If the co-change history is sparse (fewer
22
+ * than `min_commits_for_signal` distinct commits across the
23
+ * analyzer output), the signal is too weak to trust per brief §4.2's
24
+ * greenfield rule. Force serial.
25
+ *
26
+ * Default-serial-promote-on-proof (brief §4.2): when any gate fails or
27
+ * the data is missing, the decision is `demote`. Promotion requires
28
+ * positive evidence on every gate.
29
+ */
30
+ import type { CoChangeEntry } from '../../tools/co-change-analyzer.js';
31
+ import type { CouplingTask } from './coupling-report.js';
32
+ /** Thresholds the caller supplies (typically derived from EpicConfigSchema). */
33
+ export interface EpicActivationOptions {
34
+ /** Plan-wide p ceiling. Plans with p > activationThreshold are demoted. */
35
+ activationThreshold: number;
36
+ /** Greenfield floor on the analyzer's commit window. */
37
+ minCommitsForSignal: number;
38
+ /** NPMI floor for the co-change conflict signal — passed through to coupling. */
39
+ cochangeNpmiThreshold: number;
40
+ /** Minimum raw co-change count for the conflict signal. */
41
+ cochangeMinCoChanges: number;
42
+ /**
43
+ * Capability D (calibration) additions to the hot-module list. The static
44
+ * Lean Turbo predicates (`isGlobalFile` / `isProtectedPath`) always apply;
45
+ * these are normalised paths the calibration loop has promoted after
46
+ * observing divergent writes against the static set. Optional — falsy or
47
+ * empty means "no calibration overrides". Path matching is exact (post-
48
+ * `normalizePath`); callers compute that via `effectiveHotModules` in
49
+ * `./calibration-engine.ts`.
50
+ */
51
+ extraHotModules?: readonly string[];
52
+ /**
53
+ * Greenfield-smart Rule 1: whether the project is under git version control.
54
+ * The greenfield gate exists because co-change signals require git history
55
+ * to compute. When the project is not a git repo, there is no signal type
56
+ * to evaluate — the gate's premise is absent, so it passes trivially
57
+ * rather than fail-closed. Callers (typically `epic_run_phase`) resolve
58
+ * this via `isGitRepo(directory)` from `src/git/branch.ts`.
59
+ *
60
+ * Backward-compat: omitted or `undefined` reverts to legacy behavior
61
+ * (apply the `commitsObserved >= minCommitsForSignal` floor
62
+ * unconditionally). Callers should pass an explicit boolean.
63
+ */
64
+ isGitProject?: boolean;
65
+ /**
66
+ * Phase 13 (B20): task IDs the architect declared in `depends:` that
67
+ * don't resolve to ANY task in the plan. Typically an LLM typo. The
68
+ * gate fails closed with a dedicated `phantom dep` blocking reason so
69
+ * the architect sees the actual bad ID instead of being misled into
70
+ * hunting a non-existent cross-phase upstream. Pass alongside
71
+ * `crossPhaseUpstreams` (the two lists are disjoint).
72
+ */
73
+ phantomDeps?: readonly string[];
74
+ /**
75
+ * Phase 10 — predecessor-evidence gate redesign.
76
+ *
77
+ * Cross-phase upstream task IDs for the phase being decided: every
78
+ * task that lives in a strictly-prior phase AND is depended on by a
79
+ * task in the current phase. The gate verifies each one has a
80
+ * `swarm(task <id>):` marker in git log via `isUpstreamCommitted`.
81
+ *
82
+ * Empty array (the legacy default) ⇒ no cross-phase deps to check;
83
+ * predecessor evidence is vacuously satisfied. This is correct for
84
+ * Phase 1 (no prior phase), single-phase projects, and phases the
85
+ * architect explicitly declared as independent.
86
+ *
87
+ * Why this replaces the `commitsObserved >= minCommitsForSignal`
88
+ * floor: the floor was a statistical proxy for "do we have enough
89
+ * history to trust `p`?", but in small projects it permanently
90
+ * blocked parallelism (a 12-task project never reaches 20 commits).
91
+ * The structural check asks the actually-relevant question — "are
92
+ * the things this phase depends on actually in git?" — directly,
93
+ * regardless of project size. The architect's declared dep graph IS
94
+ * the parallelism specification (Lamport happens-before); Rule 2's
95
+ * commits ARE the synchronization point; this check ties them
96
+ * together.
97
+ *
98
+ * Callers (`epic_run_phase`) compute this from the plan's dep graph.
99
+ */
100
+ crossPhaseUpstreams?: readonly string[];
101
+ /**
102
+ * Predicate for the predecessor-evidence check above. Returns true
103
+ * when the given taskId has a `swarm(task <id>):` marker in git
104
+ * history. Same predicate Rule 3 uses at the lane planner — share
105
+ * one source of truth.
106
+ *
107
+ * Omitted ⇒ the gate treats every cross-phase upstream as
108
+ * uncommitted (fail-closed). Pair `crossPhaseUpstreams` with this
109
+ * predicate, or pass neither.
110
+ */
111
+ isUpstreamCommitted?: (taskId: string) => boolean;
112
+ }
113
+ /** Each gate's pass/fail outcome plus the evidence behind it. */
114
+ export interface EpicActivationRationale {
115
+ pCheck: {
116
+ passed: boolean;
117
+ p: number;
118
+ threshold: number;
119
+ };
120
+ hotModuleCheck: {
121
+ passed: boolean;
122
+ touchedHotModules: string[];
123
+ };
124
+ greenfieldCheck: {
125
+ passed: boolean;
126
+ commitsObserved: number;
127
+ minCommits: number;
128
+ /**
129
+ * `true` when the caller flagged the project as non-git
130
+ * (`options.isGitProject === false`). In that case the gate is
131
+ * bypassed (`passed: true`) because the co-change signal does not
132
+ * apply — not because the history floor was met. Surfaced for audit
133
+ * so reviewers can distinguish "bypassed" from "satisfied".
134
+ */
135
+ bypassedNoGit?: boolean;
136
+ /**
137
+ * Phase 10: cross-phase upstream task IDs the gate consulted.
138
+ * Empty when the current phase has no cross-phase deps (Phase 1,
139
+ * single-phase plans, declared-independent phases).
140
+ *
141
+ * Phase 13 (B19): optional because pre-Phase-10 records on disk
142
+ * (`.swarm/evidence/epic-promotions.jsonl`) lack this field.
143
+ * Renderers MUST default to `[]` when reading historical records.
144
+ */
145
+ crossPhaseUpstreams?: string[];
146
+ /**
147
+ * Phase 10: cross-phase upstreams the predicate reported as NOT
148
+ * yet committed. Non-empty ⇒ the gate failed; the architect
149
+ * needs to wait for those tasks to commit before re-deciding.
150
+ *
151
+ * Phase 13 (B19): optional, same reason as above.
152
+ */
153
+ missingUpstreams?: string[];
154
+ /**
155
+ * Phase 13 (B20): dep IDs the architect declared that don't
156
+ * resolve to any task in the plan. Usually an LLM typo; the gate
157
+ * fails CLOSED so the architect can see the bad ID and fix the
158
+ * declaration. Distinct from `missingUpstreams` because phantom
159
+ * IDs aren't tasks that need to be "committed" — they don't
160
+ * exist at all, and the remediation is "fix the dep ID", not
161
+ * "wait for the upstream to land".
162
+ */
163
+ phantomDeps?: string[];
164
+ };
165
+ }
166
+ /** The verdict `decideEpicActivation` returns. */
167
+ export interface EpicActivationVerdict {
168
+ decision: 'promote' | 'demote';
169
+ p: number;
170
+ rationale: EpicActivationRationale;
171
+ /** Plain-English reasons the verdict went the way it did — for logs and UI. */
172
+ blockingReasons: string[];
173
+ }
174
+ /**
175
+ * Decide whether the given tasks should be promoted to parallel execution
176
+ * via Lean Turbo's lane planner.
177
+ *
178
+ * Inputs are pre-resolved by the caller:
179
+ * - `tasks`: every task in scope (typically the whole plan), with the
180
+ * same `{ id, scope }` shape Capability B consumes. The caller
181
+ * handles `readTaskScopes` / `files_touched` resolution and any
182
+ * completed-task filtering.
183
+ * - `cochangePairs`: the analyzer's output (unfiltered) plus the
184
+ * `commitsObserved` count from `parseGitLog`. The greenfield gate
185
+ * consults the count directly so the function stays pure.
186
+ * - `options`: thresholds (typically read from
187
+ * `turbo.epic.mode.*` + `turbo.epic.cochange.*`).
188
+ *
189
+ * Output: structured verdict the caller persists to
190
+ * `.swarm/evidence/epic-promotions.jsonl` and surfaces via
191
+ * `/swarm epic status`.
192
+ */
193
+ export declare function decideEpicActivation(tasks: CouplingTask[], cochangePairs: CoChangeEntry[], commitsObserved: number, options: EpicActivationOptions): EpicActivationVerdict;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Calibration engine for Epic Mode Capability D.
3
+ *
4
+ * `applyCalibration(state, newRecords, options) → newState` is a pure
5
+ * function that walks fresh divergence records (those not yet processed)
6
+ * and produces an updated calibration state. The wiring code is responsible
7
+ * for: (a) reading the prior state, (b) filtering history to the
8
+ * unprocessed tail, (c) writing the new state.
9
+ *
10
+ * Hard design rules (brief §4.5-D + the user's ratified §20.3 defaults):
11
+ *
12
+ * 1. **Auto-tighten only on threshold.** A divergent task tightens
13
+ * `activationThresholdOverride` toward zero by `tightenStep`,
14
+ * bounded by `floorThreshold` (we never tighten below the floor —
15
+ * a value too small makes Epic Mode never promote anything).
16
+ *
17
+ * 2. **Hot-module list monotonically grows.** Files in a divergent
18
+ * task's `undeclared` set are added to `hotModuleAdditions`. The
19
+ * list is NEVER auto-shrunk; loosening a hot-module promotion
20
+ * requires manual intervention. This is a one-way ratchet.
21
+ *
22
+ * 3. **Loosen only after `loosenWindow` consecutive clean tasks.**
23
+ * Every clean (divergenceRatio === 0) task increments
24
+ * `consecutiveCleanCount`; every divergent task resets it to zero.
25
+ * When the counter reaches `loosenWindow`, the threshold is loosened
26
+ * by `loosenStep` toward the static config value (never past it) and
27
+ * the counter resets to zero. The hot-module list is NOT shrunk by
28
+ * loosening — only the threshold relaxes.
29
+ *
30
+ * 4. **No oscillation.** A clean-then-divergent-then-clean sequence
31
+ * cannot swing the threshold back and forth: every divergent task
32
+ * resets the clean-counter and tightens by `tightenStep`, so the
33
+ * threshold only moves toward zero in the divergent direction and
34
+ * toward the static value (with the clean-window gate) in the
35
+ * clean direction. Tests assert this invariant.
36
+ */
37
+ import type { CalibrationState } from './calibration.js';
38
+ import type { DivergenceRecord } from './divergence-recorder.js';
39
+ export interface ApplyCalibrationOptions {
40
+ /** Static config value — the absolute ceiling for the threshold. */
41
+ staticThreshold: number;
42
+ /**
43
+ * Floor for the threshold — calibration never tightens past this.
44
+ * Default 0.05 — below that, Epic Mode effectively never promotes
45
+ * even on highly-decoupled plans.
46
+ */
47
+ floorThreshold?: number;
48
+ /** Per-divergent-task tightening step. Default 0.02. */
49
+ tightenStep?: number;
50
+ /** Per-loosening-event step (toward static). Default 0.01. */
51
+ loosenStep?: number;
52
+ /** Consecutive-clean-tasks required before any loosening. Default 10. */
53
+ loosenWindow?: number;
54
+ }
55
+ /**
56
+ * Apply the calibration rules to a chronological list of NEW divergence
57
+ * records. Pure — no I/O, no side effects, deterministic for a given
58
+ * (state, newRecords, options) triple.
59
+ *
60
+ * The caller is responsible for tracking which records are "new" (typically
61
+ * by reading the full divergence history and slicing past the
62
+ * `processedRecords` count from the current state). This function does
63
+ * not deduplicate — feeding the same record twice will double-count its
64
+ * effect.
65
+ */
66
+ export declare function applyCalibration(state: CalibrationState, newRecords: readonly DivergenceRecord[], options: ApplyCalibrationOptions): CalibrationState;
67
+ /**
68
+ * Resolve the effective activation threshold by combining the static config
69
+ * value with the calibration override (if any). The override is always a
70
+ * tighter or equal value to the static — calibration can never relax past
71
+ * static. Returns the static value when calibration state is null
72
+ * (fail-closed mode) or when no override is set.
73
+ *
74
+ * Belt-and-braces clamps:
75
+ * - upper bound: static ceiling (calibration never relaxes past it)
76
+ * - lower bound: 0 (a negative or absurdly small override — e.g. from a
77
+ * hand-edited calibration.json — would make Epic Mode never promote
78
+ * anything; treat 0 as the absolute floor here; callers that pass a
79
+ * higher `floor_threshold` to the engine get that bound at WRITE time,
80
+ * so this clamp only matters for corrupt on-disk values)
81
+ */
82
+ export declare function effectiveActivationThreshold(staticThreshold: number, state: CalibrationState | null): number;
83
+ /**
84
+ * Union the static hot-module list (Lean Turbo's globals + protected) with
85
+ * the calibration's learned additions. Returns a fresh array; callers can
86
+ * pass this to `decideEpicActivation`'s effective-hot-module check.
87
+ */
88
+ export declare function effectiveHotModules(staticHotModules: readonly string[], state: CalibrationState | null): string[];
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Durable calibration state for Epic Mode Capability D.
3
+ *
4
+ * Persists the LEARNED knob overrides that `decideEpicActivation` consults
5
+ * at runtime — the activation-threshold override (tighter than the static
6
+ * config when divergence has been observed) and the auto-added hot-module
7
+ * list (monotonically grows; never auto-shrinks per design).
8
+ *
9
+ * Lives at `<projectRoot>/.swarm/epic/calibration.json`. Pattern mirrors
10
+ * `src/turbo/epic/state.ts` exactly — atomic `tmp + rename`, per-directory
11
+ * fail-closed marker on malformed file, repair seam.
12
+ *
13
+ * No imports from `src/turbo/lean/` — purely additive to the Epic namespace.
14
+ */
15
+ /** Persisted shape of `.swarm/epic/calibration.json`. */
16
+ export interface CalibrationState {
17
+ version: 1;
18
+ updatedAt: string;
19
+ /**
20
+ * Effective activation threshold override. When set, supersedes the
21
+ * static `turbo.epic.mode.activation_threshold` config value (which is
22
+ * always the absolute ceiling — calibration can tighten, never loosen
23
+ * past, the static value). Range: same as the static config — [0, 1].
24
+ */
25
+ activationThresholdOverride?: number;
26
+ /**
27
+ * Modules promoted to the hot-module list by observed divergence.
28
+ * Monotonically grows — never auto-shrinks (loosening the hot-module
29
+ * list requires manual intervention; the calibration loop only adds).
30
+ * Sorted lexicographically for stable diffs.
31
+ */
32
+ hotModuleAdditions: string[];
33
+ /**
34
+ * Running counter of consecutive clean (divergenceRatio === 0) task
35
+ * outcomes since the last divergent task or the last loosening event.
36
+ * Drives the loosen-rule (loosen only after `loosenWindow` consecutive
37
+ * clean tasks). Cleared by the engine after any loosening or divergence.
38
+ */
39
+ consecutiveCleanCount: number;
40
+ /** ISO 8601 timestamp of the most recent calibration-engine invocation. */
41
+ lastCalibrationAt?: string;
42
+ /** Number of divergence records processed by the engine so far. */
43
+ processedRecords: number;
44
+ }
45
+ export declare function emptyCalibrationState(): CalibrationState;
46
+ export declare function isCalibrationStateUnreadable(directory: string): boolean;
47
+ export declare function repairCalibrationUnreadable(directory: string): void;
48
+ /**
49
+ * Read calibration state from disk. Seeds an empty file on first access so
50
+ * subsequent writes do not race on directory creation. Returns null when
51
+ * the file is malformed (fail-closed via `stateUnreadableMap`).
52
+ *
53
+ * Self-healing: when the in-memory unreadable marker is set, this function
54
+ * first attempts to re-validate the on-disk file. If a user (or another
55
+ * process) has repaired the file out-of-band, the marker auto-clears and the
56
+ * normal read proceeds. Without this, a long-lived plugin process would keep
57
+ * returning null until manually told to repair (adversarial review H2).
58
+ */
59
+ export declare function loadCalibrationState(directory: string): CalibrationState | null;
60
+ /**
61
+ * Atomic write of calibration state. `tmp + rename` pattern with random
62
+ * suffix to avoid concurrent-collision; tmp file is best-effort cleaned up
63
+ * on rename failure so a failed write does not leave orphans.
64
+ */
65
+ export declare function saveCalibrationState(directory: string, state: CalibrationState): void;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Co-change-aware pair conflict predicate for Epic mode.
3
+ *
4
+ * Combines Lean Turbo's existing path-based conflict signal (imported from
5
+ * `../lean/conflicts`) with a git co-change signal (from the
6
+ * `co_change_analyzer` output, sourced via `./cochange-source`).
7
+ *
8
+ * Conservative combination rule (design notes §15.2 step 4):
9
+ * - The co-change signal may only ESCALATE a verdict, never DOWNGRADE it.
10
+ * - `conflict = pathConflict || cochangeConflict`.
11
+ * - This module never modifies Lean Turbo's behavior — when its caller does
12
+ * not invoke it, nothing changes anywhere.
13
+ *
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.
18
+ * - Co-change paths come from `git log --name-only` and are always
19
+ * repo-relative (e.g. `src/x.ts`).
20
+ * - We bridge with a boundary-aware suffix match: a scope path matches a
21
+ * co-change path if they are equal OR the scope ends with `'/' + cochange`.
22
+ * This handles both absolute and relative scope paths without needing the
23
+ * project root.
24
+ */
25
+ import type { CoChangeEntry } from '../../tools/co-change-analyzer.js';
26
+ /**
27
+ * Threshold for treating a co-change pair as a conflict signal.
28
+ *
29
+ * `npmi` and `minCoChanges` directly correspond to fields on
30
+ * `CoChangeEntry`. Both must be satisfied for a pair to contribute a signal.
31
+ * Defaults live in `EpicConfigSchema` (`src/config/schema.ts`) and are
32
+ * deliberately stricter than `co_change_analyzer`'s discovery defaults.
33
+ */
34
+ export interface CoChangeThreshold {
35
+ /** Minimum NPMI in [-1, 1]. */
36
+ npmi: number;
37
+ /** Minimum raw co-change count, to suppress small-sample noise. */
38
+ minCoChanges: number;
39
+ }
40
+ /** Detailed verdict from `epicPairConflict`. */
41
+ export interface EpicPairVerdict {
42
+ /** True iff the two scopes conflict under the combined signal. */
43
+ conflict: boolean;
44
+ /** Which signal(s) fired. `'none'` only when `conflict === false`. */
45
+ reason: 'path' | 'cochange' | 'both' | 'none';
46
+ /** Concrete pairs that drove the verdict. Empty arrays when no signal fired. */
47
+ evidence: {
48
+ /** Path-overlapping pairs (each entry: `[scopeApath, scopeBpath]`). */
49
+ pathPairs: Array<[string, string]>;
50
+ /** Co-change pairs (each entry references files as they appear in CoChangeEntry). */
51
+ cochangePairs: Array<{
52
+ a: string;
53
+ b: string;
54
+ npmi: number;
55
+ coChangeCount: number;
56
+ }>;
57
+ };
58
+ }
59
+ /**
60
+ * Decide whether two task scopes conflict, combining path-based and co-change
61
+ * signals. Pure function — no I/O, no side effects.
62
+ *
63
+ * @param scopeA Files task 1 declares. Paths may be absolute or relative.
64
+ * @param scopeB Files task 2 declares.
65
+ * @param cochangePairs Unfiltered co-change entries from `./cochange-source`.
66
+ * This function applies the threshold internally so callers
67
+ * can pass the analyzer's output verbatim.
68
+ * @param threshold NPMI floor + min co-change count.
69
+ *
70
+ * Behavioral invariants verified by tests:
71
+ * - Empty `cochangePairs` (greenfield / signal absent) → verdict is exactly
72
+ * the path-only result. This is the "feature disabled" guarantee from
73
+ * design notes §15.6.
74
+ * - Co-change-only conflict promotes `'none'` to `'cochange'`.
75
+ * - Path-only conflict is unaffected by co-change input.
76
+ * - Both signals present → reason `'both'`.
77
+ * - Empty scopes (either side) → no conflict (no pairs to evaluate).
78
+ */
79
+ export declare function epicPairConflict(scopeA: string[], scopeB: string[], cochangePairs: CoChangeEntry[], threshold: CoChangeThreshold): EpicPairVerdict;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Co-change pair source for Epic mode.
3
+ *
4
+ * Composes the existing `co_change_analyzer` primitives (`parseGitLog`,
5
+ * `buildCoChangeMatrix`) to produce a full, unfiltered list of file-pair
6
+ * co-change entries the Epic conflict module can threshold itself.
7
+ *
8
+ * Why not call `detectDarkMatter` directly?
9
+ * - `detectDarkMatter` caps output at the top 20 pairs by NPMI and excludes
10
+ * pairs that already have a static import edge. That filter is correct for
11
+ * *dark-matter discovery* (its purpose) but wrong for lane-conflict signal:
12
+ * a high-NPMI pair with a static edge is still a real coupling signal the
13
+ * lane planner should know about.
14
+ *
15
+ * Caching:
16
+ * - One entry per project directory, keyed on `git rev-parse HEAD`.
17
+ * - Same HEAD → cache hit, no git re-scan.
18
+ * - Different HEAD or different directory → recompute.
19
+ * - FIFO eviction at `MAX_TRACKED_DIRS` so multi-project usage stays bounded
20
+ * (AGENTS.md invariant 8: module-level state needs explicit eviction).
21
+ *
22
+ * Reuse vs reimplementation:
23
+ * - This module never copies analyzer logic. It only orchestrates calls to
24
+ * the analyzer's existing exported `_internals` (per AGENTS.md invariant
25
+ * on composition).
26
+ * - The git HEAD read is a separate bounded subprocess, exposed via the
27
+ * `_internals` DI seam so tests can stub it without `mock.module`.
28
+ */
29
+ import * as child_process from 'node:child_process';
30
+ import { type CoChangeEntry, _internals as coChangeAnalyzer } from '../../tools/co-change-analyzer.js';
31
+ declare const execFileAsync: typeof child_process.execFile.__promisify__;
32
+ export interface GetCoChangePairsOptions {
33
+ /** Maximum commits the analyzer scans. Defaults to 500. */
34
+ maxCommitsToAnalyze?: number;
35
+ }
36
+ /**
37
+ * Output of `getCoChangeData`. Carries the same `pairs` returned by
38
+ * `getCoChangePairs`, plus the commit count the analyzer actually observed
39
+ * — which Capability C's greenfield gate needs to decide whether the
40
+ * signal is dense enough to trust.
41
+ */
42
+ export interface CoChangeData {
43
+ pairs: CoChangeEntry[];
44
+ commitsObserved: number;
45
+ }
46
+ /**
47
+ * Return the full co-change data for the given directory at the current
48
+ * git HEAD. The returned `pairs` are unfiltered (every pair the analyzer
49
+ * recorded, with NPMI / lift / counts / static-edge fields); `commitsObserved`
50
+ * is the number of distinct commits the analyzer scanned. Capability A
51
+ * applies the NPMI threshold to `pairs`; Capability C's greenfield gate
52
+ * inspects `commitsObserved`.
53
+ *
54
+ * Returns `{ pairs: [], commitsObserved: 0 }` when:
55
+ * - Directory is not a git repo, or `git` is unavailable / times out.
56
+ * - The analyzer's commit map is empty (greenfield repo).
57
+ * Both cases are signal-absent.
58
+ */
59
+ export declare function getCoChangeData(directory: string, options?: GetCoChangePairsOptions): Promise<CoChangeData>;
60
+ /**
61
+ * Back-compat wrapper kept for the M2 path (`/swarm coupling` only needs
62
+ * `pairs`). Capability C uses `getCoChangeData` directly for the
63
+ * greenfield gate.
64
+ */
65
+ export declare function getCoChangePairs(directory: string, options?: GetCoChangePairsOptions): Promise<CoChangeEntry[]>;
66
+ /** Test-only: drop all cache entries. */
67
+ export declare function _clearCache(): void;
68
+ /** Test-only: cache size, for asserting eviction behavior. */
69
+ export declare function _cacheSize(): number;
70
+ /**
71
+ * Test-only DI seam. Mutating this object is file-scoped and trivially
72
+ * restorable via afterEach, avoiding Bun's cross-file `mock.module` leak
73
+ * (AGENTS.md invariant 7).
74
+ */
75
+ export declare const _internals: {
76
+ execFile: typeof execFileAsync;
77
+ parseGitLog: typeof coChangeAnalyzer.parseGitLog;
78
+ buildCoChangeMatrix: typeof coChangeAnalyzer.buildCoChangeMatrix;
79
+ };
80
+ export {};
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Coupling report computation for Epic mode (Capability B).
3
+ *
4
+ * Given a plan, computes:
5
+ * - `p` — the coupling coefficient (fraction of task pairs that conflict
6
+ * under the combined path + co-change signal from Capability A).
7
+ * - `perModule` — for each file/module that caused at least one conflict,
8
+ * the count of conflicting pairs it appeared in.
9
+ * - `roadmap` — the modules ranked by conflict-contribution, with each
10
+ * one's share of total detected conflicts. The team can use this
11
+ * as a decoupling priority order.
12
+ *
13
+ * Read-only — this module changes no execution behavior. It composes
14
+ * Capability A's `epicPairConflict` (the same predicate the future epic
15
+ * mode will use when scheduling), so the report answers exactly the
16
+ * question "what would Capability A say about this plan if I asked it
17
+ * about every task pair?".
18
+ */
19
+ import type { CoChangeEntry } from '../../tools/co-change-analyzer.js';
20
+ import { type CoChangeThreshold, type EpicPairVerdict } from './cochange-conflict.js';
21
+ /** A task as `epic` mode sees it: identifier + declared file scope. */
22
+ export interface CouplingTask {
23
+ id: string;
24
+ scope: string[];
25
+ }
26
+ /** One conflicting pair in the report. */
27
+ export interface ConflictingPair {
28
+ a: string;
29
+ b: string;
30
+ reason: EpicPairVerdict['reason'];
31
+ cochangeMatches: number;
32
+ pathMatches: number;
33
+ }
34
+ /** Per-module conflict contribution. */
35
+ export interface ModuleContention {
36
+ module: string;
37
+ conflicts: number;
38
+ share: number;
39
+ }
40
+ /** Output of `computeCouplingReport`. */
41
+ export interface CouplingReport {
42
+ /** Number of tasks considered. */
43
+ taskCount: number;
44
+ /** Number of unordered task pairs evaluated (`n*(n-1)/2`). */
45
+ totalPairs: number;
46
+ /** Pairs the combined signal flagged as conflicting. */
47
+ conflictingPairCount: number;
48
+ /** Coupling coefficient `p` = conflictingPairCount / totalPairs (0 when totalPairs == 0). */
49
+ p: number;
50
+ /** Each conflicting pair, with the per-pair verdict reason and evidence counts. */
51
+ conflictingPairs: ConflictingPair[];
52
+ /** Per-module contention table, sorted by `conflicts` descending. */
53
+ perModule: ModuleContention[];
54
+ /** Top-N modules with a human-readable rank line for each. */
55
+ roadmap: string[];
56
+ }
57
+ export interface ComputeCouplingReportOptions {
58
+ /** Cap on roadmap rank entries. Default 5. */
59
+ roadmapTop?: number;
60
+ }
61
+ /**
62
+ * Compute the coupling report over a set of tasks.
63
+ *
64
+ * Inputs:
65
+ * - `tasks`: the tasks to consider. The caller decides scoping (whole
66
+ * plan vs a single phase) and any filtering (pending vs all). Empty
67
+ * array is valid and produces `p = 0`.
68
+ * - `cochangePairs`: typically the output of
69
+ * `getCoChangePairs(directory)` — passed in so this function stays
70
+ * pure (no I/O) and trivially testable. Empty array is valid (the
71
+ * Capability A predicate falls back to path-only verdicts).
72
+ * - `threshold`: NPMI + min-co-changes floor, same shape Capability A
73
+ * consumes.
74
+ * - `options.roadmapTop`: how many modules to list in the roadmap
75
+ * (default 5).
76
+ *
77
+ * Pure function — no file I/O, no side effects.
78
+ */
79
+ export declare function computeCouplingReport(tasks: CouplingTask[], cochangePairs: CoChangeEntry[], threshold: CoChangeThreshold, options?: ComputeCouplingReportOptions): CouplingReport;
80
+ /**
81
+ * Render a `CouplingReport` as a markdown document. Output shape is
82
+ * stable so downstream tools can parse it; the JSON form (via
83
+ * `JSON.stringify(report)`) is the better target for programmatic use.
84
+ */
85
+ export declare function formatCouplingReportMarkdown(report: CouplingReport): string;