instar 1.3.456 → 1.3.458

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 (38) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +42 -1
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  5. package/dist/core/PostUpdateMigrator.js +16 -0
  6. package/dist/core/PostUpdateMigrator.js.map +1 -1
  7. package/dist/core/ProactiveSwapMonitor.d.ts +149 -0
  8. package/dist/core/ProactiveSwapMonitor.d.ts.map +1 -0
  9. package/dist/core/ProactiveSwapMonitor.js +208 -0
  10. package/dist/core/ProactiveSwapMonitor.js.map +1 -0
  11. package/dist/core/devGatedFeatures.d.ts +41 -0
  12. package/dist/core/devGatedFeatures.d.ts.map +1 -0
  13. package/dist/core/devGatedFeatures.js +76 -0
  14. package/dist/core/devGatedFeatures.js.map +1 -0
  15. package/dist/core/types.d.ts +27 -0
  16. package/dist/core/types.d.ts.map +1 -1
  17. package/dist/core/types.js.map +1 -1
  18. package/dist/scaffold/templates.d.ts.map +1 -1
  19. package/dist/scaffold/templates.js +1 -0
  20. package/dist/scaffold/templates.js.map +1 -1
  21. package/dist/server/AgentServer.d.ts +1 -0
  22. package/dist/server/AgentServer.d.ts.map +1 -1
  23. package/dist/server/AgentServer.js +1 -0
  24. package/dist/server/AgentServer.js.map +1 -1
  25. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  26. package/dist/server/CapabilityIndex.js +4 -1
  27. package/dist/server/CapabilityIndex.js.map +1 -1
  28. package/dist/server/routes.d.ts +3 -0
  29. package/dist/server/routes.d.ts.map +1 -1
  30. package/dist/server/routes.js +27 -0
  31. package/dist/server/routes.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/data/builtin-manifest.json +64 -64
  34. package/src/scaffold/templates.ts +1 -0
  35. package/upgrades/1.3.457.md +45 -0
  36. package/upgrades/1.3.458.md +62 -0
  37. package/upgrades/side-effects/dev-gated-features-registry.md +70 -0
  38. package/upgrades/side-effects/proactive-prelimit-swap.md +64 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * ProactiveSwapMonitor — the PRE-LIMIT half of the Subscription & Auth Standard's
3
+ * continuity guarantee (P1.3).
4
+ *
5
+ * The reactive path (`autoSwapOnRateLimit`) only fires AFTER a session hits its
6
+ * account's wall: a RateLimitSentinel escalation on an ACTUAL throttle signal.
7
+ * So a session blips at the wall before recovering — and an UNTAGGED session
8
+ * (one with no `subscriptionAccountId`, e.g. the primary interactive session
9
+ * running on the default config) has nothing for the swap engine to grab, so it
10
+ * just wedges until an operator manually swaps the default login. That is the
11
+ * exact 2026-06-09 failure that motivated this monitor.
12
+ *
13
+ * This monitor moves a session OFF an account BEFORE it walls, at a lag-aware
14
+ * measured threshold below the real limit.
15
+ *
16
+ * ── Why a separate, lower threshold (lag-aware) ──
17
+ * The QuotaPoller reads utilization periodically, so our reading TRAILS the real
18
+ * usage (measured live: our ~90% == Anthropic's ~95% on the same account). A
19
+ * pre-emptive swap therefore triggers at a LOWER measured threshold (default 80)
20
+ * to leave margin for the lag — by the time we read 80%, real is higher, and the
21
+ * swap completes before the wall.
22
+ *
23
+ * ── Effective-account resolution (covers the session you actually use) ──
24
+ * A session carries `subscriptionAccountId` only if it was pinned at spawn. The
25
+ * primary interactive session usually runs on the DEFAULT config (untagged), so
26
+ * the monitor resolves an untagged session's effective account from the default
27
+ * config's live login (InUseAccountResolver). Without this, the session a user
28
+ * is actively in would be invisible to the swap engine and wedge at the wall.
29
+ *
30
+ * ── Bounded, non-storming ──
31
+ * Per evaluation: only accounts AT pressure that have a sub-threshold ALTERNATE
32
+ * are sources; candidates are sorted newest-first (the just-(re)started
33
+ * interactive session ranks first) and capped per cycle; each swapped session
34
+ * enters a cooldown so a slow restart isn't double-swapped. Near the wall the
35
+ * monitor triggers a fresh poll so a fast burn isn't missed between the
36
+ * low-frequency baseline polls.
37
+ *
38
+ * Gated OFF by default (moving live sessions is real authority — same authority
39
+ * as `autoSwapOnRateLimit`, just an earlier trigger). The decision core is pure
40
+ * (injected deps) so it tests with zero sessions and zero network.
41
+ */
42
+ import type { SubscriptionAccount } from './SubscriptionPool.js';
43
+ /** A running, swap-eligible session as the monitor sees it. */
44
+ export interface ProactiveSwapSession {
45
+ /** tmux session name (what the swap path keys on). */
46
+ sessionName: string;
47
+ /** The pool account this session is tagged with, or null if untagged
48
+ * (untagged ⇒ running on the default config ⇒ resolved via the default login). */
49
+ accountId: string | null;
50
+ /** ISO start time — newest-first ordering proxy for "most recently active". */
51
+ startedAt?: string;
52
+ }
53
+ /** The shape returned by the injected swap (a subset of QuotaAwareScheduler.SwapResult). */
54
+ export interface ProactiveSwapOutcome {
55
+ swapped: boolean;
56
+ toAccountId: string | null;
57
+ }
58
+ export interface ProactiveSwapMonitorConfig {
59
+ /** Current pool accounts (e.g. () => pool.list()). */
60
+ listAccounts: () => SubscriptionAccount[];
61
+ /** Currently-running, swap-eligible (claude-code) sessions. */
62
+ listRunningSessions: () => ProactiveSwapSession[];
63
+ /** The pool account the DEFAULT config is logged into right now (or null).
64
+ * Untagged sessions run here; from InUseAccountResolver in production. */
65
+ resolveDefaultAccountId: () => Promise<string | null>;
66
+ /** Performs the actual swap (wraps QuotaAwareScheduler.onQuotaPressure). */
67
+ swap: (args: {
68
+ sessionName: string;
69
+ exhaustedAccountId: string;
70
+ nowMs: number;
71
+ }) => Promise<ProactiveSwapOutcome>;
72
+ /** Optional fresh-poll trigger, awaited when an account is in the watch zone. */
73
+ triggerPoll?: () => Promise<unknown>;
74
+ /** Measured binding-window utilization % that triggers a pre-emptive swap. Default 80. */
75
+ thresholdPct?: number;
76
+ /** When an at-risk account is within this many points of the threshold, the
77
+ * monitor refreshes the poll before deciding (so a fast burn isn't missed
78
+ * between baseline polls). Default 15 (i.e. watch zone starts at 65%). */
79
+ watchMarginPct?: number;
80
+ /** Max sessions swapped per evaluation cycle (storm guard). Default 3. */
81
+ maxSwapsPerCycle?: number;
82
+ /** Per-session cooldown after a successful swap before it's eligible again.
83
+ * Default 600000 (10m) — must exceed the swap+restart time. */
84
+ cooldownMs?: number;
85
+ /** Monitor tick cadence. Default 180000 (3m). */
86
+ tickMs?: number;
87
+ /** Injected for tests. */
88
+ now?: () => number;
89
+ logger?: {
90
+ log: (m: string) => void;
91
+ warn: (m: string) => void;
92
+ };
93
+ }
94
+ export interface ProactiveSwapTickResult {
95
+ /** Session names that were actually swapped this pass. */
96
+ swapped: string[];
97
+ /** How many sessions were eligible (at-pressure, has-alternate, off-cooldown). */
98
+ considered: number;
99
+ /** Whether a fresh poll was triggered before the decision (watch zone). */
100
+ refreshed: boolean;
101
+ }
102
+ export declare class ProactiveSwapMonitor {
103
+ private readonly cfg;
104
+ private readonly thresholdPct;
105
+ private readonly watchMarginPct;
106
+ private readonly maxSwapsPerCycle;
107
+ private readonly cooldownMs;
108
+ private readonly tickMs;
109
+ private readonly now;
110
+ private readonly logger;
111
+ /** Last successful-swap timestamp per session (cooldown bookkeeping). */
112
+ private readonly lastSwapAt;
113
+ private timer;
114
+ private ticking;
115
+ private lastResult;
116
+ constructor(cfg: ProactiveSwapMonitorConfig);
117
+ start(): void;
118
+ stop(): void;
119
+ /** Status for the read route (never throws). */
120
+ status(): {
121
+ thresholdPct: number;
122
+ watchPct: number;
123
+ maxSwapsPerCycle: number;
124
+ cooldownMs: number;
125
+ tickMs: number;
126
+ running: boolean;
127
+ lastResult: ProactiveSwapTickResult | null;
128
+ };
129
+ /**
130
+ * One monitor pass: if any at-risk account is in the watch zone, refresh the
131
+ * quota poll first (fresh data near the wall), then evaluate + swap.
132
+ */
133
+ tick(): Promise<ProactiveSwapTickResult>;
134
+ /**
135
+ * Pure-ish decision + swap on the CURRENT snapshots (no poll refresh). Exposed
136
+ * for tests and the on-demand check route.
137
+ */
138
+ evaluate(): Promise<{
139
+ swapped: string[];
140
+ considered: number;
141
+ }>;
142
+ /**
143
+ * Map running sessions to candidates whose EFFECTIVE account is at/over minPct.
144
+ * Effective account = the session's tag, else the default-config login (so the
145
+ * untagged interactive session is visible). Resolves the default login once.
146
+ */
147
+ private mapCandidates;
148
+ }
149
+ //# sourceMappingURL=ProactiveSwapMonitor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProactiveSwapMonitor.d.ts","sourceRoot":"","sources":["../../src/core/ProactiveSwapMonitor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,208 @@
1
+ /**
2
+ * ProactiveSwapMonitor — the PRE-LIMIT half of the Subscription & Auth Standard's
3
+ * continuity guarantee (P1.3).
4
+ *
5
+ * The reactive path (`autoSwapOnRateLimit`) only fires AFTER a session hits its
6
+ * account's wall: a RateLimitSentinel escalation on an ACTUAL throttle signal.
7
+ * So a session blips at the wall before recovering — and an UNTAGGED session
8
+ * (one with no `subscriptionAccountId`, e.g. the primary interactive session
9
+ * running on the default config) has nothing for the swap engine to grab, so it
10
+ * just wedges until an operator manually swaps the default login. That is the
11
+ * exact 2026-06-09 failure that motivated this monitor.
12
+ *
13
+ * This monitor moves a session OFF an account BEFORE it walls, at a lag-aware
14
+ * measured threshold below the real limit.
15
+ *
16
+ * ── Why a separate, lower threshold (lag-aware) ──
17
+ * The QuotaPoller reads utilization periodically, so our reading TRAILS the real
18
+ * usage (measured live: our ~90% == Anthropic's ~95% on the same account). A
19
+ * pre-emptive swap therefore triggers at a LOWER measured threshold (default 80)
20
+ * to leave margin for the lag — by the time we read 80%, real is higher, and the
21
+ * swap completes before the wall.
22
+ *
23
+ * ── Effective-account resolution (covers the session you actually use) ──
24
+ * A session carries `subscriptionAccountId` only if it was pinned at spawn. The
25
+ * primary interactive session usually runs on the DEFAULT config (untagged), so
26
+ * the monitor resolves an untagged session's effective account from the default
27
+ * config's live login (InUseAccountResolver). Without this, the session a user
28
+ * is actively in would be invisible to the swap engine and wedge at the wall.
29
+ *
30
+ * ── Bounded, non-storming ──
31
+ * Per evaluation: only accounts AT pressure that have a sub-threshold ALTERNATE
32
+ * are sources; candidates are sorted newest-first (the just-(re)started
33
+ * interactive session ranks first) and capped per cycle; each swapped session
34
+ * enters a cooldown so a slow restart isn't double-swapped. Near the wall the
35
+ * monitor triggers a fresh poll so a fast burn isn't missed between the
36
+ * low-frequency baseline polls.
37
+ *
38
+ * Gated OFF by default (moving live sessions is real authority — same authority
39
+ * as `autoSwapOnRateLimit`, just an earlier trigger). The decision core is pure
40
+ * (injected deps) so it tests with zero sessions and zero network.
41
+ */
42
+ import { selectAccount, accountAtPressure, } from './QuotaAwareScheduler.js';
43
+ export class ProactiveSwapMonitor {
44
+ cfg;
45
+ thresholdPct;
46
+ watchMarginPct;
47
+ maxSwapsPerCycle;
48
+ cooldownMs;
49
+ tickMs;
50
+ now;
51
+ logger;
52
+ /** Last successful-swap timestamp per session (cooldown bookkeeping). */
53
+ lastSwapAt = new Map();
54
+ timer = null;
55
+ ticking = false;
56
+ lastResult = null;
57
+ constructor(cfg) {
58
+ this.cfg = cfg;
59
+ this.thresholdPct = cfg.thresholdPct ?? 80;
60
+ this.watchMarginPct = cfg.watchMarginPct ?? 15;
61
+ this.maxSwapsPerCycle = cfg.maxSwapsPerCycle ?? 3;
62
+ this.cooldownMs = cfg.cooldownMs ?? 600_000;
63
+ this.tickMs = cfg.tickMs ?? 180_000;
64
+ this.now = cfg.now ?? (() => Date.now());
65
+ this.logger = cfg.logger ?? { log: () => { }, warn: () => { } };
66
+ }
67
+ start() {
68
+ if (this.timer)
69
+ return;
70
+ this.timer = setInterval(() => {
71
+ if (this.ticking)
72
+ return; // never overlap ticks
73
+ void this.tick();
74
+ }, this.tickMs);
75
+ this.timer.unref?.();
76
+ }
77
+ stop() {
78
+ if (this.timer) {
79
+ clearInterval(this.timer);
80
+ this.timer = null;
81
+ }
82
+ }
83
+ /** Status for the read route (never throws). */
84
+ status() {
85
+ return {
86
+ thresholdPct: this.thresholdPct,
87
+ watchPct: Math.max(0, this.thresholdPct - this.watchMarginPct),
88
+ maxSwapsPerCycle: this.maxSwapsPerCycle,
89
+ cooldownMs: this.cooldownMs,
90
+ tickMs: this.tickMs,
91
+ running: this.timer !== null,
92
+ lastResult: this.lastResult,
93
+ };
94
+ }
95
+ /**
96
+ * One monitor pass: if any at-risk account is in the watch zone, refresh the
97
+ * quota poll first (fresh data near the wall), then evaluate + swap.
98
+ */
99
+ async tick() {
100
+ if (this.ticking) {
101
+ return this.lastResult ?? { swapped: [], considered: 0, refreshed: false };
102
+ }
103
+ this.ticking = true;
104
+ try {
105
+ let refreshed = false;
106
+ if (this.cfg.triggerPoll) {
107
+ const watchPct = Math.max(0, this.thresholdPct - this.watchMarginPct);
108
+ const near = (await this.mapCandidates(watchPct)).length > 0;
109
+ if (near) {
110
+ try {
111
+ await this.cfg.triggerPoll();
112
+ refreshed = true;
113
+ }
114
+ catch {
115
+ // @silent-fallback-ok: a poll blip just means we decide on prior data
116
+ }
117
+ }
118
+ }
119
+ const evaluated = await this.evaluate();
120
+ const result = { ...evaluated, refreshed };
121
+ this.lastResult = result;
122
+ return result;
123
+ }
124
+ finally {
125
+ this.ticking = false;
126
+ }
127
+ }
128
+ /**
129
+ * Pure-ish decision + swap on the CURRENT snapshots (no poll refresh). Exposed
130
+ * for tests and the on-demand check route.
131
+ */
132
+ async evaluate() {
133
+ const nowMs = this.now();
134
+ const accounts = this.cfg.listAccounts();
135
+ const atPressure = await this.mapCandidates(this.thresholdPct);
136
+ const eligible = atPressure.filter((c) => {
137
+ const last = this.lastSwapAt.get(c.sessionName);
138
+ if (last !== undefined && nowMs - last < this.cooldownMs)
139
+ return false;
140
+ // Only swap when there's an alternate BELOW the proactive threshold — never
141
+ // move a session onto an account that is itself nearly full (anti-thrash).
142
+ const alt = selectAccount(accounts, { softThresholdPct: this.thresholdPct, nowMs }, c.accountId);
143
+ return alt !== null;
144
+ });
145
+ // Newest-(re)started first: the interactive session a user is actively in
146
+ // (it just restarted on compaction/recovery) ranks ahead of idle background
147
+ // sessions, so under the per-cycle cap it is rescued first.
148
+ eligible.sort((a, b) => b.startedMs - a.startedMs);
149
+ const toSwap = eligible.slice(0, this.maxSwapsPerCycle);
150
+ const swapped = [];
151
+ for (const c of toSwap) {
152
+ let outcome;
153
+ try {
154
+ outcome = await this.cfg.swap({
155
+ sessionName: c.sessionName,
156
+ exhaustedAccountId: c.accountId,
157
+ nowMs,
158
+ });
159
+ }
160
+ catch {
161
+ // @silent-fallback-ok: a swap failure is retried next cycle (no cooldown set)
162
+ continue;
163
+ }
164
+ if (outcome.swapped) {
165
+ this.lastSwapAt.set(c.sessionName, nowMs);
166
+ swapped.push(c.sessionName);
167
+ this.logger.log(`[ProactiveSwap] ${c.sessionName}: pre-emptively swapped off ${c.accountId} → ${outcome.toAccountId} ` +
168
+ `(account ≥${this.thresholdPct}% measured — moved before the wall, conversation preserved)`);
169
+ }
170
+ }
171
+ return { swapped, considered: eligible.length };
172
+ }
173
+ /**
174
+ * Map running sessions to candidates whose EFFECTIVE account is at/over minPct.
175
+ * Effective account = the session's tag, else the default-config login (so the
176
+ * untagged interactive session is visible). Resolves the default login once.
177
+ */
178
+ async mapCandidates(minPct) {
179
+ const accounts = this.cfg.listAccounts();
180
+ const byId = new Map(accounts.map((a) => [a.id, a]));
181
+ let defaultAcctId = null;
182
+ try {
183
+ defaultAcctId = await this.cfg.resolveDefaultAccountId();
184
+ }
185
+ catch {
186
+ defaultAcctId = null; // @silent-fallback-ok: unknown default login → tagged-only
187
+ }
188
+ const out = [];
189
+ for (const s of this.cfg.listRunningSessions()) {
190
+ const eff = s.accountId ?? defaultAcctId;
191
+ if (!eff)
192
+ continue;
193
+ const acct = byId.get(eff);
194
+ if (!acct)
195
+ continue;
196
+ if (!accountAtPressure(acct, minPct))
197
+ continue;
198
+ const startedMs = s.startedAt ? Date.parse(s.startedAt) : NaN;
199
+ out.push({
200
+ sessionName: s.sessionName,
201
+ accountId: eff,
202
+ startedMs: Number.isFinite(startedMs) ? startedMs : 0,
203
+ });
204
+ }
205
+ return out;
206
+ }
207
+ }
208
+ //# sourceMappingURL=ProactiveSwapMonitor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProactiveSwapMonitor.js","sourceRoot":"","sources":["../../src/core/ProactiveSwapMonitor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * devGatedFeatures.ts — the registry of features that follow the
3
+ * standard_development_agent_dark_feature_gate convention: config OMITS
4
+ * `enabled`, the runtime resolves it via `resolveDevAgentGate` (live on a
5
+ * development agent, dark on the fleet).
6
+ *
7
+ * WHY (DEV-AGENT-DARK-GATE-CONFORMANCE-SPEC, Slice 2): Slice 1's lint catches a
8
+ * hand-rolled gate and a hardcoded `enabled: false` under a marker comment, but
9
+ * it cannot prove that a feature's *actual config + construction* resolves live
10
+ * on a dev agent. This registry drives the both-sides wiring test
11
+ * (`tests/unit/devGatedFeatures-wiring.test.ts`): for each entry, the REAL
12
+ * ConfigDefaults are applied and `resolveDevAgentGate(<configPath>)` must be
13
+ * true under a dev-agent config and false under a fleet config. A feature whose
14
+ * default hardcodes `enabled: false` (the literal #1001 mechanism — `applyDefaults`
15
+ * would inject the `false`) fails the test. Adding a dev-gated feature here is
16
+ * the natural checklist step; the test then guards it permanently.
17
+ *
18
+ * NOT every site that calls `resolveDevAgentGate` belongs here — only features
19
+ * whose intent is "dark fleet / LIVE on dev". Deliberately EXCLUDED:
20
+ * - `monitoring.mcpProcessReaper` — destructive (kills processes); ships OFF +
21
+ * dry-run for EVERYONE incl. dev agents by design (`enabled: false` default).
22
+ * - `monitoring.resourceLedger` — the ledger itself defaults `enabled: true`
23
+ * (on for everyone); only its sampling rides the gate off the same key, so
24
+ * it is not cleanly a dark-on-fleet feature.
25
+ */
26
+ /** A feature governed by the developmentAgent dark-feature gate. */
27
+ export interface DevGatedFeature {
28
+ /** Stable identifier (matches the feature's name in code/docs). */
29
+ name: string;
30
+ /** Dotted path to the feature's `enabled` flag in the agent config. */
31
+ configPath: string;
32
+ /** One-line description of what runs live on a dev agent. */
33
+ description: string;
34
+ }
35
+ export declare const DEV_GATED_FEATURES: DevGatedFeature[];
36
+ /**
37
+ * Read a dotted path off a config object, returning the value or undefined.
38
+ * Used by the wiring test and the spec-intent cross-check (Slice 3).
39
+ */
40
+ export declare function getConfigByPath(config: unknown, dottedPath: string): unknown;
41
+ //# sourceMappingURL=devGatedFeatures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devGatedFeatures.d.ts","sourceRoot":"","sources":["../../src/core/devGatedFeatures.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,oEAAoE;AACpE,MAAM,WAAW,eAAe;IAC9B,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,kBAAkB,EAAE,eAAe,EAoC/C,CAAC;AAEF;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAO5E"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * devGatedFeatures.ts — the registry of features that follow the
3
+ * standard_development_agent_dark_feature_gate convention: config OMITS
4
+ * `enabled`, the runtime resolves it via `resolveDevAgentGate` (live on a
5
+ * development agent, dark on the fleet).
6
+ *
7
+ * WHY (DEV-AGENT-DARK-GATE-CONFORMANCE-SPEC, Slice 2): Slice 1's lint catches a
8
+ * hand-rolled gate and a hardcoded `enabled: false` under a marker comment, but
9
+ * it cannot prove that a feature's *actual config + construction* resolves live
10
+ * on a dev agent. This registry drives the both-sides wiring test
11
+ * (`tests/unit/devGatedFeatures-wiring.test.ts`): for each entry, the REAL
12
+ * ConfigDefaults are applied and `resolveDevAgentGate(<configPath>)` must be
13
+ * true under a dev-agent config and false under a fleet config. A feature whose
14
+ * default hardcodes `enabled: false` (the literal #1001 mechanism — `applyDefaults`
15
+ * would inject the `false`) fails the test. Adding a dev-gated feature here is
16
+ * the natural checklist step; the test then guards it permanently.
17
+ *
18
+ * NOT every site that calls `resolveDevAgentGate` belongs here — only features
19
+ * whose intent is "dark fleet / LIVE on dev". Deliberately EXCLUDED:
20
+ * - `monitoring.mcpProcessReaper` — destructive (kills processes); ships OFF +
21
+ * dry-run for EVERYONE incl. dev agents by design (`enabled: false` default).
22
+ * - `monitoring.resourceLedger` — the ledger itself defaults `enabled: true`
23
+ * (on for everyone); only its sampling rides the gate off the same key, so
24
+ * it is not cleanly a dark-on-fleet feature.
25
+ */
26
+ export const DEV_GATED_FEATURES = [
27
+ {
28
+ name: 'growthAnalyst',
29
+ configPath: 'monitoring.growthAnalyst.enabled',
30
+ description: 'Proactive growth & milestone analyst (/growth/*).',
31
+ },
32
+ {
33
+ name: 'coherenceJournal',
34
+ configPath: 'multiMachine.coherenceJournal.enabled',
35
+ description: 'Cross-machine coherence journal.',
36
+ },
37
+ {
38
+ name: 'warmSessionA2A',
39
+ configPath: 'threadline.warmSessionA2A.enabled',
40
+ description: 'Warm-session pool for agent-to-agent delivery.',
41
+ },
42
+ {
43
+ name: 'secretSync',
44
+ configPath: 'multiMachine.secretSync.enabled',
45
+ description: 'Cross-machine secret sync (receive side).',
46
+ },
47
+ {
48
+ name: 'geminiLoopDriver',
49
+ configPath: 'autonomousSessions.geminiLoopDriver.enabled',
50
+ description: 'Gemini autonomous-loop driver.',
51
+ },
52
+ {
53
+ name: 'respawnBuildContext',
54
+ configPath: 'sessions.respawnBuildContext.enabled',
55
+ description: 'Respawn build-context capture on session restart.',
56
+ },
57
+ {
58
+ name: 'selfKnowledgeSessionContext',
59
+ configPath: 'selfKnowledge.sessionContext.enabled',
60
+ description: 'Session-boot self-knowledge context injection.',
61
+ },
62
+ ];
63
+ /**
64
+ * Read a dotted path off a config object, returning the value or undefined.
65
+ * Used by the wiring test and the spec-intent cross-check (Slice 3).
66
+ */
67
+ export function getConfigByPath(config, dottedPath) {
68
+ let cur = config;
69
+ for (const key of dottedPath.split('.')) {
70
+ if (cur == null || typeof cur !== 'object')
71
+ return undefined;
72
+ cur = cur[key];
73
+ }
74
+ return cur;
75
+ }
76
+ //# sourceMappingURL=devGatedFeatures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devGatedFeatures.js","sourceRoot":"","sources":["../../src/core/devGatedFeatures.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAYH,MAAM,CAAC,MAAM,kBAAkB,GAAsB;IACnD;QACE,IAAI,EAAE,eAAe;QACrB,UAAU,EAAE,kCAAkC;QAC9C,WAAW,EAAE,mDAAmD;KACjE;IACD;QACE,IAAI,EAAE,kBAAkB;QACxB,UAAU,EAAE,uCAAuC;QACnD,WAAW,EAAE,kCAAkC;KAChD;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,UAAU,EAAE,mCAAmC;QAC/C,WAAW,EAAE,gDAAgD;KAC9D;IACD;QACE,IAAI,EAAE,YAAY;QAClB,UAAU,EAAE,iCAAiC;QAC7C,WAAW,EAAE,2CAA2C;KACzD;IACD;QACE,IAAI,EAAE,kBAAkB;QACxB,UAAU,EAAE,6CAA6C;QACzD,WAAW,EAAE,gCAAgC;KAC9C;IACD;QACE,IAAI,EAAE,qBAAqB;QAC3B,UAAU,EAAE,sCAAsC;QAClD,WAAW,EAAE,mDAAmD;KACjE;IACD;QACE,IAAI,EAAE,6BAA6B;QACnC,UAAU,EAAE,sCAAsC;QAClD,WAAW,EAAE,gDAAgD;KAC9D;CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,MAAe,EAAE,UAAkB;IACjE,IAAI,GAAG,GAAY,MAAM,CAAC;IAC1B,KAAK,MAAM,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACxC,IAAI,GAAG,IAAI,IAAI,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QAC7D,GAAG,GAAI,GAA+B,CAAC,GAAG,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -2409,6 +2409,33 @@ export interface InstarConfig {
2409
2409
  * auto-swap functional (a session must carry which account it's on for the
2410
2410
  * swap engine to move it). Unset → spawns use the default config (no-op). */
2411
2411
  pinSessionsToPool?: boolean;
2412
+ /**
2413
+ * DARK by default: the PRE-LIMIT swap. When enabled, the ProactiveSwapMonitor
2414
+ * moves a session OFF an account that crosses `thresholdPct` on its binding
2415
+ * window BEFORE it walls — vs. the reactive `autoSwapOnRateLimit`, which only
2416
+ * fires AFTER the wall. Same authority as the reactive swap (it moves live
2417
+ * sessions), earlier trigger → opt-in. Covers untagged sessions too by
2418
+ * resolving the default-config login (so the primary interactive session is
2419
+ * swap-visible instead of wedging at the wall).
2420
+ */
2421
+ proactiveSwap?: {
2422
+ /** Master switch. Default false (dark). */
2423
+ enabled?: boolean;
2424
+ /** Measured binding-window utilization % that triggers a pre-emptive swap.
2425
+ * Default 80 — below the real wall to absorb poll-lag (measured trails
2426
+ * real ~5%), so the swap completes with margin. */
2427
+ thresholdPct?: number;
2428
+ /** Refresh the poll before deciding when an at-risk account is within this
2429
+ * many points of the threshold (catch a fast burn between baseline polls).
2430
+ * Default 15. */
2431
+ watchMarginPct?: number;
2432
+ /** Max sessions swapped per evaluation cycle (storm guard). Default 3. */
2433
+ maxSwapsPerCycle?: number;
2434
+ /** Per-session cooldown (ms) after a successful swap. Default 600000 (10m). */
2435
+ cooldownMs?: number;
2436
+ /** Monitor tick cadence (ms). Default 180000 (3m). */
2437
+ tickMs?: number;
2438
+ };
2412
2439
  /** P2.1 enrollment wizard knobs (all optional). */
2413
2440
  enrollment?: {
2414
2441
  /** Per-framework login command override (defaults: claude-code →