opencode-swarm 7.86.0 → 7.87.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.
- package/dist/cli/{capability-probe-jevmgwmf.js → capability-probe-wsjzcp48.js} +2 -2
- package/dist/cli/{config-doctor-zejarrr6.js → config-doctor-6h64pn8n.js} +4 -4
- package/dist/cli/{dispatch-k86d928w.js → dispatch-kb69qw40.js} +3 -3
- package/dist/cli/{evidence-summary-service-g2znnd33.js → evidence-summary-service-gg5m9z57.js} +4 -4
- package/dist/cli/{guardrail-explain-rtd1x26f.js → guardrail-explain-wb1cj312.js} +13 -13
- package/dist/cli/{guardrail-log-80116wmz.js → guardrail-log-eegabqcp.js} +5 -5
- package/dist/cli/{index-jwz50183.js → index-0m44n5qv.js} +14 -14
- package/dist/cli/{index-0sxvwjt0.js → index-1cb4wxnm.js} +2 -2
- package/dist/cli/{index-zfsbaaqh.js → index-5e4e2hvv.js} +1 -1
- package/dist/cli/{index-vq2321gg.js → index-5hvbw5xh.js} +2 -2
- package/dist/cli/{index-5cb86007.js → index-5vpe6vq9.js} +1 -1
- package/dist/cli/{index-red8fm8p.js → index-89xjr3h4.js} +1162 -214
- package/dist/cli/{index-f8r50m3h.js → index-adz3nk9b.js} +2 -2
- package/dist/cli/{index-7r2b453y.js → index-f13d3b69.js} +2 -2
- package/dist/cli/{index-ckntc5gf.js → index-gn8n22th.js} +2 -2
- package/dist/cli/{index-hw9b2xng.js → index-q9h0wb04.js} +36 -3
- package/dist/cli/{index-d9fbxaqd.js → index-s8bj492g.js} +1 -1
- package/dist/cli/{index-hz59hg4h.js → index-v4fcn4tr.js} +1 -1
- package/dist/cli/{index-eb85wtx9.js → index-vqyfscxd.js} +2 -2
- package/dist/cli/{index-5q66xc88.js → index-wv2yj8ka.js} +2598 -1406
- package/dist/cli/{index-yx44zd0p.js → index-zgwm4ryv.js} +9 -1
- package/dist/cli/index.js +12 -12
- package/dist/cli/{pending-delegations-rd40tv9s.js → pending-delegations-35fvcj7z.js} +3 -3
- package/dist/cli/{pr-subscriptions-y1nn36e5.js → pr-subscriptions-b18n1yd8.js} +4 -4
- package/dist/cli/{schema-8d32b2v6.js → schema-84146tvk.js} +3 -1
- package/dist/cli/{skill-generator-a5ehggyg.js → skill-generator-3pvpk4y2.js} +2 -2
- package/dist/commands/coupling.d.ts +36 -0
- package/dist/commands/epic.d.ts +52 -0
- package/dist/commands/registry.d.ts +18 -2
- package/dist/config/constants.d.ts +1 -0
- package/dist/config/schema.d.ts +145 -0
- package/dist/git/branch.d.ts +22 -1
- package/dist/hooks/delegation-gate/worktree-merge-status.d.ts +86 -0
- package/dist/index.js +8401 -5792
- package/dist/memory/schema.d.ts +3 -3
- package/dist/plan/manager.d.ts +10 -0
- package/dist/state.d.ts +16 -0
- package/dist/tools/epic-plan-waves.d.ts +79 -0
- package/dist/tools/epic-record-divergence.d.ts +73 -0
- package/dist/tools/epic-run-phase.d.ts +179 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/manifest.d.ts +3 -0
- package/dist/tools/tool-metadata.d.ts +12 -0
- package/dist/turbo/epic/activation.d.ts +193 -0
- package/dist/turbo/epic/calibration-engine.d.ts +88 -0
- package/dist/turbo/epic/calibration.d.ts +65 -0
- package/dist/turbo/epic/cochange-conflict.d.ts +79 -0
- package/dist/turbo/epic/cochange-source.d.ts +80 -0
- package/dist/turbo/epic/coupling-report.d.ts +85 -0
- package/dist/turbo/epic/divergence-recorder.d.ts +112 -0
- package/dist/turbo/epic/index.d.ts +24 -0
- package/dist/turbo/epic/promotion-evidence.d.ts +42 -0
- package/dist/turbo/epic/state.d.ts +85 -0
- package/dist/turbo/epic/task-commit.d.ts +110 -0
- package/dist/turbo/epic/upstream-commits.d.ts +82 -0
- package/dist/turbo/epic/wave-planner.d.ts +83 -0
- package/dist/turbo/lean/partition-common.d.ts +85 -0
- package/dist/turbo/lean/planner.d.ts +12 -20
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/logger.d.ts +19 -0
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Divergence recorder for Epic Mode Capability D (self-calibration).
|
|
3
|
+
*
|
|
4
|
+
* After every task transitions to `completed`, this module:
|
|
5
|
+
* 1. Compares the task's DECLARED scope (from
|
|
6
|
+
* `.swarm/scopes/scope-{taskId}.json` — what the coder said it would
|
|
7
|
+
* touch) against the ACTUAL files modified during the task
|
|
8
|
+
* (`session.modifiedFilesThisCoderTask` — what the guardrails hook
|
|
9
|
+
* observed the coder writing to).
|
|
10
|
+
* 2. Computes divergence — undeclared writes (actual − declared), unused
|
|
11
|
+
* declarations (declared − actual), and a per-task divergence ratio
|
|
12
|
+
* (undeclared / max(1, actual)).
|
|
13
|
+
* 3. Appends one record to `.swarm/epic/divergence.jsonl`.
|
|
14
|
+
*
|
|
15
|
+
* The calibration engine (`./calibration-engine.ts`) reads this history on
|
|
16
|
+
* the next `epic_decide_phase` invocation and uses it to adjust the
|
|
17
|
+
* activation threshold and hot-module list. This module just records.
|
|
18
|
+
*
|
|
19
|
+
* Pure I/O: never throws to the caller. Failures are logged and swallowed
|
|
20
|
+
* so the task-completion path is never blocked by an audit write.
|
|
21
|
+
*/
|
|
22
|
+
/** One record per task completion. */
|
|
23
|
+
export interface DivergenceRecord {
|
|
24
|
+
/** ISO 8601. */
|
|
25
|
+
timestamp: string;
|
|
26
|
+
sessionID: string;
|
|
27
|
+
taskId: string;
|
|
28
|
+
/** Phase the task belonged to, when known. */
|
|
29
|
+
phaseNumber?: number;
|
|
30
|
+
/** Normalised paths declared via `declare_scope` or files_touched fallback. */
|
|
31
|
+
declaredScope: string[];
|
|
32
|
+
/** Normalised paths the guardrails hook observed the coder write to. */
|
|
33
|
+
actualFiles: string[];
|
|
34
|
+
/** Files in `actualFiles` not present in `declaredScope`. */
|
|
35
|
+
undeclared: string[];
|
|
36
|
+
/** Files in `declaredScope` not present in `actualFiles`. */
|
|
37
|
+
unused: string[];
|
|
38
|
+
/** undeclared.length / max(1, actualFiles.length). 0 ⇒ fully declared. */
|
|
39
|
+
divergenceRatio: number;
|
|
40
|
+
/** True when divergenceRatio === 0 (no undeclared writes). */
|
|
41
|
+
isClean: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Compute the divergence between a declared scope and the files actually
|
|
45
|
+
* modified. Pure — no I/O, no side effects. Returns the diff sets plus the
|
|
46
|
+
* ratio used by the calibration engine.
|
|
47
|
+
*
|
|
48
|
+
* Path comparison uses `normalizePath` (POSIX-style, no trailing slash,
|
|
49
|
+
* Windows-lowercased) from Lean Turbo's conflicts module so the comparison
|
|
50
|
+
* is consistent with everything else in the lane planner.
|
|
51
|
+
*/
|
|
52
|
+
export declare function computeDivergence(declaredScope: readonly string[], actualFiles: readonly string[]): {
|
|
53
|
+
declared: string[];
|
|
54
|
+
actual: string[];
|
|
55
|
+
undeclared: string[];
|
|
56
|
+
unused: string[];
|
|
57
|
+
divergenceRatio: number;
|
|
58
|
+
};
|
|
59
|
+
interface RecordTaskDivergenceArgs {
|
|
60
|
+
directory: string;
|
|
61
|
+
sessionID: string;
|
|
62
|
+
taskId: string;
|
|
63
|
+
phaseNumber?: number;
|
|
64
|
+
declaredScope: readonly string[];
|
|
65
|
+
actualFiles: readonly string[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Append one divergence record to the JSONL audit file.
|
|
69
|
+
*
|
|
70
|
+
* Append-only, line-delimited so partial writes are tolerable (the calibration
|
|
71
|
+
* reader skips malformed lines). Best-effort — never throws to caller:
|
|
72
|
+
* - Directory-creation failure → log and return null.
|
|
73
|
+
* - Append write failure → log and return null.
|
|
74
|
+
* Either keeps the task-completion path moving even if the audit subsystem
|
|
75
|
+
* is broken (audit miss is not a correctness issue; blocking task completion
|
|
76
|
+
* would be).
|
|
77
|
+
*/
|
|
78
|
+
export declare function recordTaskDivergence(args: RecordTaskDivergenceArgs): {
|
|
79
|
+
path: string;
|
|
80
|
+
record: DivergenceRecord;
|
|
81
|
+
} | null;
|
|
82
|
+
export interface ReadDivergenceHistoryOptions {
|
|
83
|
+
/** Read at most this many of the most recent records. */
|
|
84
|
+
limit?: number;
|
|
85
|
+
/** Filter to this session (default: all sessions). */
|
|
86
|
+
sessionID?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Maximum bytes to read from the tail of the file. Defaults to
|
|
89
|
+
* `MAX_TAIL_BYTES` (16 MiB) — large enough to hold thousands of
|
|
90
|
+
* records, small enough to avoid OOMing on a runaway audit log.
|
|
91
|
+
* Pass `Infinity` to disable the bound (callers that truly need the
|
|
92
|
+
* whole history — adversarial review H3).
|
|
93
|
+
*/
|
|
94
|
+
maxBytes?: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Read divergence records from disk, oldest-to-newest within the read
|
|
98
|
+
* window. Malformed lines (rare — could occur on partial write) are
|
|
99
|
+
* silently skipped — they do not corrupt the well-formed records before or
|
|
100
|
+
* after them. Returns `[]` when the file does not exist.
|
|
101
|
+
*
|
|
102
|
+
* Tail-bounded: by default reads at most the last `MAX_TAIL_BYTES`. When
|
|
103
|
+
* the file is larger, the read starts mid-file and the FIRST encountered
|
|
104
|
+
* line (which is almost certainly a partial record split by the byte
|
|
105
|
+
* boundary) is discarded. This means very old records are not returned by
|
|
106
|
+
* a default-bounded read — the calibration engine consumes the tail
|
|
107
|
+
* incrementally via `processedRecords`, so it never needs the full history
|
|
108
|
+
* in memory at once. For full-history audit reads (tests, ad-hoc tooling),
|
|
109
|
+
* pass `maxBytes: Infinity`.
|
|
110
|
+
*/
|
|
111
|
+
export declare function readDivergenceHistory(directory: string, options?: ReadDivergenceHistoryOptions): DivergenceRecord[];
|
|
112
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Epic mode — barrel export.
|
|
3
|
+
*
|
|
4
|
+
* Epic mode is a new, additive execution mode that composes Lean Turbo without
|
|
5
|
+
* modifying it. Capabilities:
|
|
6
|
+
* - A: co-change-aware pair conflict (`epicPairConflict`).
|
|
7
|
+
* - B: coupling KPI + decoupling roadmap (`computeCouplingReport`).
|
|
8
|
+
* - C: per-plan activation decision (`decideEpicActivation`).
|
|
9
|
+
*
|
|
10
|
+
* Dependency direction is one-way: `epic` depends on `lean`; `lean` never
|
|
11
|
+
* depends on `epic`. All Lean Turbo files stay byte-for-byte untouched.
|
|
12
|
+
*/
|
|
13
|
+
export type { EpicActivationOptions, EpicActivationRationale, EpicActivationVerdict, } from './activation.js';
|
|
14
|
+
export { decideEpicActivation } from './activation.js';
|
|
15
|
+
export type { CoChangeThreshold, EpicPairVerdict, } from './cochange-conflict.js';
|
|
16
|
+
export { epicPairConflict } from './cochange-conflict.js';
|
|
17
|
+
export type { CoChangeData, GetCoChangePairsOptions, } from './cochange-source.js';
|
|
18
|
+
export { getCoChangeData, getCoChangePairs } from './cochange-source.js';
|
|
19
|
+
export type { ComputeCouplingReportOptions, ConflictingPair, CouplingReport, CouplingTask, ModuleContention, } from './coupling-report.js';
|
|
20
|
+
export { computeCouplingReport, formatCouplingReportMarkdown, } from './coupling-report.js';
|
|
21
|
+
export type { PromotionEvidenceRecord } from './promotion-evidence.js';
|
|
22
|
+
export { appendPromotionEvidence, readPromotionEvidence, } from './promotion-evidence.js';
|
|
23
|
+
export type { EpicLastDecision, EpicPersistedState, EpicSessionState, } from './state.js';
|
|
24
|
+
export { disableEpicMode, emptyPersisted as emptyEpicPersisted, emptySessionState as emptyEpicSessionState, enableEpicMode, isEpicModeActive, isStateUnreadable as isEpicStateUnreadable, loadEpicSessionState, recordEpicDecision, repairStateUnreadable as repairEpicStateUnreadable, resetEpicSession, saveEpicSessionState, } from './state.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promotion-evidence writer for Epic Mode (Capability C).
|
|
3
|
+
*
|
|
4
|
+
* Each activation decision (one per phase per session) appends a single
|
|
5
|
+
* JSON line to `.swarm/evidence/epic-promotions.jsonl`. The file is
|
|
6
|
+
* line-delimited so partial writes are tolerable — readers can skip a
|
|
7
|
+
* truncated trailing line and continue.
|
|
8
|
+
*
|
|
9
|
+
* The write is intentionally append-mode (not atomic-rename) because
|
|
10
|
+
* each line is a self-contained record and the file is monotonically
|
|
11
|
+
* growing. Per AGENTS.md invariant 4, writes go under `ctx.directory`,
|
|
12
|
+
* never `process.cwd()`.
|
|
13
|
+
*/
|
|
14
|
+
import type { EpicActivationVerdict } from './activation.js';
|
|
15
|
+
/** What `appendPromotionEvidence` writes per decision. */
|
|
16
|
+
export interface PromotionEvidenceRecord {
|
|
17
|
+
/** ISO 8601 timestamp of the decision. */
|
|
18
|
+
timestamp: string;
|
|
19
|
+
/** Session that made the decision (so multi-session usage stays auditable). */
|
|
20
|
+
sessionID: string;
|
|
21
|
+
/** Phase the decision applied to (Capability C operates per-plan but each
|
|
22
|
+
* phase invokes the runner; we record per phase for granular telemetry). */
|
|
23
|
+
phase?: number;
|
|
24
|
+
/** The decision and the rationale, copied from the activation verdict. */
|
|
25
|
+
verdict: EpicActivationVerdict;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Append one decision record to `.swarm/evidence/epic-promotions.jsonl`.
|
|
29
|
+
*
|
|
30
|
+
* Returns the absolute path of the file written. On any I/O failure the
|
|
31
|
+
* error is rethrown — the caller decides whether to surface a warning to
|
|
32
|
+
* the user or fail closed. Returns null when the directory itself cannot
|
|
33
|
+
* be created (best-effort fail-soft so a missing `.swarm/` does not break
|
|
34
|
+
* the activation flow's primary verdict).
|
|
35
|
+
*/
|
|
36
|
+
export declare function appendPromotionEvidence(directory: string, record: PromotionEvidenceRecord): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Read all evidence records from the JSONL file. Convenience for
|
|
39
|
+
* `/swarm epic status` and tests. Skips malformed lines (best-effort
|
|
40
|
+
* tolerance for the rare partial-write case).
|
|
41
|
+
*/
|
|
42
|
+
export declare function readPromotionEvidence(directory: string): PromotionEvidenceRecord[];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable Epic Mode session state (Capability C).
|
|
3
|
+
*
|
|
4
|
+
* Persists per-session Epic Mode activation state under
|
|
5
|
+
* `<projectRoot>/.swarm/epic-state.json` so toggling survives process
|
|
6
|
+
* restarts. Mirrors the pattern in `src/turbo/lean/state.ts` (atomic
|
|
7
|
+
* `tmp + rename`, per-directory `stateUnreadableMap` for fail-closed
|
|
8
|
+
* semantics, sessions-keyed shape) — without modifying that file.
|
|
9
|
+
*
|
|
10
|
+
* Dependency direction is one-way: this module imports nothing from
|
|
11
|
+
* `src/turbo/lean/`. The shape is parallel but independent.
|
|
12
|
+
*/
|
|
13
|
+
/** Top-level state for a single session. */
|
|
14
|
+
export interface EpicSessionState {
|
|
15
|
+
sessionID: string;
|
|
16
|
+
/** When epic mode was last enabled for this session (ISO 8601). */
|
|
17
|
+
enabledAt?: string;
|
|
18
|
+
/** When epic mode was last disabled for this session (ISO 8601). */
|
|
19
|
+
disabledAt?: string;
|
|
20
|
+
/** Most recent activation decision recorded for this session, if any. */
|
|
21
|
+
lastDecision?: EpicLastDecision;
|
|
22
|
+
/** Whether epic mode is currently active for this session. */
|
|
23
|
+
active: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Minimal snapshot of the last activation decision. */
|
|
26
|
+
export interface EpicLastDecision {
|
|
27
|
+
decidedAt: string;
|
|
28
|
+
phase?: number;
|
|
29
|
+
decision: 'promote' | 'demote';
|
|
30
|
+
p: number;
|
|
31
|
+
blockingReasons: string[];
|
|
32
|
+
}
|
|
33
|
+
/** Persisted shape of `.swarm/epic-state.json`. */
|
|
34
|
+
export interface EpicPersistedState {
|
|
35
|
+
version: 1;
|
|
36
|
+
updatedAt: string;
|
|
37
|
+
sessions: Record<string, EpicSessionState>;
|
|
38
|
+
}
|
|
39
|
+
export declare function emptyPersisted(): EpicPersistedState;
|
|
40
|
+
export declare function emptySessionState(sessionID: string): EpicSessionState;
|
|
41
|
+
export declare function isStateUnreadable(directory: string): boolean;
|
|
42
|
+
export declare function repairStateUnreadable(directory: string): void;
|
|
43
|
+
/** Read this session's state, or null if not yet recorded. */
|
|
44
|
+
export declare function loadEpicSessionState(directory: string, sessionID: string): EpicSessionState | null;
|
|
45
|
+
/** Write the given session state, replacing any prior entry for that sessionID. */
|
|
46
|
+
export declare function saveEpicSessionState(directory: string, state: EpicSessionState): void;
|
|
47
|
+
/** True iff epic mode is currently active for the given session. */
|
|
48
|
+
export declare function isEpicModeActive(directory: string, sessionID: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* True iff epic mode is currently active for ANY session in the project.
|
|
51
|
+
*
|
|
52
|
+
* Use this when a code path needs to know "is the project running under
|
|
53
|
+
* Epic Mode right now" without caring which session toggled it. The
|
|
54
|
+
* session-scoped `isEpicModeActive` answers "did THIS session toggle it" —
|
|
55
|
+
* a different question with a different answer.
|
|
56
|
+
*
|
|
57
|
+
* The architect's session enables Epic via `/swarm epic on`; sub-agents
|
|
58
|
+
* (coders, reviewers) dispatched through the `Task` tool run in their own
|
|
59
|
+
* sessions and have no record of that toggle. Asking the project-scoped
|
|
60
|
+
* check is the only correct way to honor Epic Mode from those flows.
|
|
61
|
+
* Rule 2's auto-commit (centralized in Phase 5) is the canonical caller.
|
|
62
|
+
*
|
|
63
|
+
* Fail-closed: returns `false` on unreadable state, matching the rest of
|
|
64
|
+
* this module's defaults.
|
|
65
|
+
*/
|
|
66
|
+
export declare function isEpicModeActiveForProject(directory: string): boolean;
|
|
67
|
+
/** Enable epic mode for the session; records `enabledAt`. */
|
|
68
|
+
export declare function enableEpicMode(directory: string, sessionID: string): void;
|
|
69
|
+
/** Disable epic mode for the session; records `disabledAt`. */
|
|
70
|
+
export declare function disableEpicMode(directory: string, sessionID: string): void;
|
|
71
|
+
/** Reset the session's state entry entirely. */
|
|
72
|
+
export declare function resetEpicSession(directory: string, sessionID: string): void;
|
|
73
|
+
/**
|
|
74
|
+
* Update the session's `lastDecision` field. Used by the runner after each
|
|
75
|
+
* activation evaluation so `/swarm epic status` can show the most recent
|
|
76
|
+
* decision rationale without re-reading the evidence JSONL.
|
|
77
|
+
*
|
|
78
|
+
* Precondition: the session must already have an entry (i.e. the caller has
|
|
79
|
+
* called `enableEpicMode` previously). This is intentional — recording a
|
|
80
|
+
* decision for a never-toggled session would produce phantom state that
|
|
81
|
+
* `/swarm epic status` could not distinguish from a legitimately-active
|
|
82
|
+
* session. Callers that reach this function should have already verified
|
|
83
|
+
* `isEpicModeActive(...)` returned `true`. Throws if no session entry exists.
|
|
84
|
+
*/
|
|
85
|
+
export declare function recordEpicDecision(directory: string, sessionID: string, decision: EpicLastDecision): void;
|