@cohortapp/agent-sdk 2.5.1 → 2.6.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/bin/maestro.mjs +185 -88
- package/bin/maestro.test.mjs +175 -48
- package/docs/runbooks/backup-restore.md +65 -33
- package/framework-features.json +4 -4
- package/lib/backup/policy.mjs +710 -0
- package/lib/backup/policy.test.mjs +305 -0
- package/lib/budget-escalate.mjs +133 -0
- package/lib/budget-escalate.test.mjs +232 -0
- package/lib/budget-guard.envelope.test.mjs +476 -0
- package/lib/budget-guard.mjs +853 -75
- package/lib/budget-guard.test.mjs +91 -42
- package/lib/cadences.mjs +33 -0
- package/lib/channels/orgmail/adapter.mjs +88 -3
- package/lib/channels/orgmail/adapter.test.mjs +137 -0
- package/lib/channels/repeat-suppressor.mjs +198 -0
- package/lib/channels/repeat-suppressor.test.mjs +134 -0
- package/lib/comms/receipts.mjs +297 -0
- package/lib/cost/ledger-row.mjs +333 -0
- package/lib/cost/ledger-row.test.mjs +183 -0
- package/lib/execution/drive.mjs +28 -1
- package/lib/execution/effects.mjs +191 -12
- package/lib/execution/effects.test.mjs +50 -11
- package/lib/goals/admission.mjs +13 -1
- package/lib/goals/admission.test.mjs +26 -1
- package/lib/goals/loop.mjs +13 -0
- package/lib/kpi-sensors.test.mjs +3 -0
- package/lib/mandate/cache.mjs +13 -5
- package/lib/mandate/derive.mjs +146 -21
- package/lib/mandate/derive.test.mjs +50 -6
- package/lib/mandate/model.mjs +32 -4
- package/lib/mandate/refresh.test.mjs +16 -2
- package/lib/mcp/server.test.mjs +12 -3
- package/lib/model-router/economics.mjs +107 -76
- package/lib/model-router/economics.test.mjs +64 -46
- package/lib/model-router/integration-coverage.test.mjs +39 -37
- package/lib/model-router/ledger.mjs +75 -22
- package/lib/model-router/ledger.test.mjs +35 -2
- package/lib/org/client.mjs +14 -0
- package/lib/org/cost-sync.mjs +16 -2
- package/lib/org/doctor.mjs +62 -1
- package/lib/org/doctor.test.mjs +36 -3
- package/lib/org/email-remedy.mjs +49 -0
- package/lib/org/engagement-ledger.mjs +376 -0
- package/lib/org/engagement-ledger.test.mjs +112 -0
- package/lib/org/engagement.mjs +1056 -0
- package/lib/org/engagement.test.mjs +739 -0
- package/lib/org/messaging.mjs +230 -3
- package/lib/org/messaging.test.mjs +110 -1
- package/lib/org/param-contract.mjs +56 -2
- package/lib/org/param-contract.test.mjs +26 -0
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +5 -0
- package/lib/org/protocol.test.mjs +7 -1
- package/lib/org/tool-surface.mjs +506 -10
- package/lib/org/tool-surface.test.mjs +191 -7
- package/lib/org/ui-parity.mjs +333 -6
- package/lib/org/ui-parity.test.mjs +96 -3
- package/lib/org/work-ledger.mjs +241 -0
- package/lib/org/work-ledger.test.mjs +237 -0
- package/lib/plan/adoption-e2e.test.mjs +366 -0
- package/lib/plan/budget-enforcement.test.mjs +400 -0
- package/lib/plan/budget-runtime.mjs +215 -0
- package/lib/plan/compile.mjs +201 -5
- package/lib/plan/compile.test.mjs +19 -5
- package/lib/plan/emit.mjs +8 -0
- package/lib/plan/emit.test.mjs +18 -0
- package/lib/resource-governor.mjs +58 -12
- package/lib/resource-governor.test.mjs +41 -1
- package/lib/security/audit-engine.mjs +45 -8
- package/lib/security/audit-engine.test.mjs +35 -0
- package/lib/setup/enroll-from-cohort.mjs +14 -1
- package/lib/setup/sections/mandate.mjs +48 -7
- package/lib/setup/sections/mandate.test.mjs +17 -2
- package/lib/setup/sections/orgmail.mjs +10 -2
- package/lib/setup/state.mjs +83 -2
- package/lib/telemetry/collect.mjs +360 -20
- package/lib/telemetry/collect.test.mjs +266 -0
- package/package.json +1 -1
- package/scripts/cost/track-claude-usage.mjs +207 -48
- package/scripts/cost/track-claude-usage.test.mjs +148 -0
- package/scripts/daemon/agent-daemon.mjs +315 -17
- package/scripts/daemon/assurance-e2e.test.mjs +421 -0
- package/scripts/daemon/assurance.mjs +944 -0
- package/scripts/daemon/assurance.test.mjs +668 -0
- package/scripts/daemon/cadence-consumer-governance.test.mjs +56 -0
- package/scripts/daemon/cadence-consumer.mjs +147 -9
- package/scripts/daemon/cadence-consumer.test.mjs +6 -0
- package/scripts/daemon/cadence-handlers.mjs +158 -0
- package/scripts/daemon/cadence-handlers.test.mjs +64 -0
- package/scripts/daemon/deliver.mjs +314 -0
- package/scripts/daemon/dispatcher-governance.test.mjs +10 -0
- package/scripts/daemon/dispatcher.mjs +64 -6
- package/scripts/daemon/responder-cost.test.mjs +68 -0
- package/scripts/daemon/responder.mjs +351 -298
- package/scripts/local-triggers/generate-plists.test.mjs +7 -4
- package/scripts/maintenance/backup-run.mjs +415 -0
- package/scripts/maintenance/backup-to-cloud.sh +16 -116
- package/scripts/org/send-orgmail.mjs +16 -0
- package/scripts/record-receipt.sh +63 -0
- package/scripts/restore-from-backup.sh +14 -3
- package/scripts/restore-from-backup.test.mjs +8 -5
- package/scripts/send-email-threaded.py +47 -0
- package/scripts/send-sms.sh +4 -0
- package/scripts/send-whatsapp.sh +4 -0
- package/scripts/setup/init-backup.mjs +93 -38
- package/scripts/slack-send.sh +12 -0
|
@@ -6,12 +6,16 @@
|
|
|
6
6
|
* routing happens at dispatch/spawn time only, never mid-session (cache prefixes
|
|
7
7
|
* are model-scoped; a mid-session switch silently re-bills the entire prefix, G8).
|
|
8
8
|
*
|
|
9
|
-
* 1. budgetLadder(spentUSD, capUSD) -> { band:0|
|
|
10
|
-
* A LADDER, not a cliff (§6.4)
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* 1. budgetLadder(spentUSD, capUSD) -> { band:0|100|125|150, degrade }
|
|
10
|
+
* A LADDER, not a cliff (§6.4) — banded against the SEAT ENVELOPE hq
|
|
11
|
+
* published, not a local config cap, and rung-for-rung identical to
|
|
12
|
+
* `lib/budget-guard.postureForBand`:
|
|
13
|
+
* <100% → policy as written (80% is a supervisor notice, not a routing
|
|
14
|
+
* change — see budgetLadder)
|
|
15
|
+
* ≥100% → cheapest class only, batch deferrable, fan-out capped at 1,
|
|
16
|
+
* self-directed work stopped
|
|
17
|
+
* ≥125% → essential-only (still answers inbox DMs)
|
|
18
|
+
* ≥150% → the spawn gate refuses everything but a human reply
|
|
15
19
|
*
|
|
16
20
|
* 2. degradeChain(chain, band) — a resolveChain `opts` hook that TIGHTENS a
|
|
17
21
|
* candidate chain per band (drops/reorders refs). Subordinate to the gates
|
|
@@ -49,59 +53,100 @@ import { writeJsonAtomic, appendJsonl } from "../fs-atomic.mjs";
|
|
|
49
53
|
// ---------------------------------------------------------------------------
|
|
50
54
|
|
|
51
55
|
/**
|
|
52
|
-
* The economics bands.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
56
|
+
* The economics bands. These are now THE SAME BANDS the governance ladder uses
|
|
57
|
+
* (`lib/budget-guard.BANDS` = [80,100,125,150]) rather than a second, coarser
|
|
58
|
+
* set — two ladders with different rungs meant "degraded" in the router and
|
|
59
|
+
* "degraded" in the governor were different states, and neither matched the
|
|
60
|
+
* decision. 80% is the supervisor NOTICE rung: it is deliberately NOT a routing
|
|
61
|
+
* change, so `budgetLadder` collapses it back to band 0 for routing purposes and
|
|
62
|
+
* the first routing degradation happens at 100% — the rung where the seat has
|
|
63
|
+
* spent what the org actually funded.
|
|
57
64
|
*/
|
|
58
|
-
export const LADDER_BANDS = Object.freeze([0,
|
|
65
|
+
export const LADDER_BANDS = Object.freeze([0, 100, 125, 150]);
|
|
59
66
|
|
|
60
67
|
/** Map a spend percentage onto an economics band. */
|
|
61
68
|
function ladderBandForPct(pct) {
|
|
69
|
+
if (pct >= 150) return 150;
|
|
70
|
+
if (pct >= 125) return 125;
|
|
62
71
|
if (pct >= 100) return 100;
|
|
63
|
-
if (pct >= 90) return 90;
|
|
64
|
-
if (pct >= 75) return 75;
|
|
65
72
|
return 0;
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
/**
|
|
69
76
|
* Compute the budget degradation for the day's spend. PURE. NEVER throws.
|
|
70
77
|
*
|
|
71
|
-
* `degrade` is the structured instruction the resolver / dispatcher consume
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* - band
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
+
* `degrade` is the structured instruction the resolver / dispatcher consume, and
|
|
79
|
+
* it mirrors `lib/budget-guard.postureForBand` rung for rung:
|
|
80
|
+
* - band 0 → nothing. (80% is a supervisor notice, not a routing change.)
|
|
81
|
+
* - band 100 → cheapOnly + batchDeferrable + fan-out capped at 1 + no
|
|
82
|
+
* self-directed work. THE DEGRADE RUNG: inbox DMs are still
|
|
83
|
+
* answered, on the cheapest qualifying row.
|
|
84
|
+
* - band 125 → essentialOnly on top (OUTCOME obligations are suspended
|
|
85
|
+
* elsewhere; here it means non-inbox work stops being routed).
|
|
86
|
+
* - band 150 → the spawn gate refuses anything that is not a human reply;
|
|
87
|
+
* routing stays cheap-only for what does get through.
|
|
88
|
+
*
|
|
89
|
+
* `preferCheaper` (the old one-tier downgrade at 75%) is retained as the band-100
|
|
90
|
+
* behaviour's companion rather than a rung of its own: there is no longer a band
|
|
91
|
+
* between "fine" and "spent the envelope", because the old 75/90 rungs were
|
|
92
|
+
* measured against a local config file rather than the org's funding.
|
|
78
93
|
*
|
|
79
94
|
* @param {number} spentUSD today's spend (>= 0)
|
|
80
|
-
* @param {number} capUSD the daily
|
|
81
|
-
* @returns {{ band:0|
|
|
95
|
+
* @param {number} capUSD the binding daily allowance (> 0; <= 0 ⇒ band 0)
|
|
96
|
+
* @returns {{ band:0|100|125|150, pct:number, spentUSD:number, capUSD:number,
|
|
82
97
|
* degrade:{ preferCheaper:boolean, cheapOnly:boolean,
|
|
83
98
|
* batchDeferrable:boolean, essentialOnly:boolean,
|
|
99
|
+
* maxFanout:number|null, selfDirected:boolean,
|
|
84
100
|
* downgradeTiers:number } }}
|
|
85
101
|
*/
|
|
86
|
-
export function budgetLadder(spentUSD, capUSD) {
|
|
102
|
+
export function budgetLadder(spentUSD, capUSD, opts = {}) {
|
|
87
103
|
const spent = numOr0(spentUSD);
|
|
88
104
|
const cap = Number(capUSD);
|
|
89
105
|
const hasCap = Number.isFinite(cap) && cap > 0;
|
|
90
|
-
|
|
91
|
-
|
|
106
|
+
|
|
107
|
+
// ── THE GOVERNOR'S BAND WINS, WHEN IT IS SUPPLIED ─────────────────────────
|
|
108
|
+
// Callers used to re-derive the band here from (spent, cap), which silently
|
|
109
|
+
// INVERTED the ladder once `dailyAllowance` started returning 0 for a seat
|
|
110
|
+
// whose monthly envelope was spent out: cap<=0 collapsed to band 0, so the
|
|
111
|
+
// further past its envelope a seat was, the LESS the router degraded. A seat
|
|
112
|
+
// at 200 % of its monthly envelope still routed the full frontier chain.
|
|
113
|
+
// `lib/budget-guard.dailyStatus` has already reconciled day vs month vs
|
|
114
|
+
// blindness; when it hands us its band we use it verbatim.
|
|
115
|
+
const supplied = Number(opts && opts.band);
|
|
116
|
+
if (Number.isFinite(supplied)) {
|
|
117
|
+
const band = ladderBandForPct(supplied);
|
|
118
|
+
return buildLadder(band, hasCap ? round2((spent / cap) * 100) : (spent > 0 ? 100 : 0), spent, hasCap ? cap : 0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// No cap AND money already spent is not "no ceiling" — it is a ceiling of
|
|
122
|
+
// zero, i.e. fully consumed. Band 0 there is the same "a zero that means
|
|
123
|
+
// unmeasured" defect in percentage form.
|
|
124
|
+
const pct = hasCap ? round2((spent / cap) * 100) : (spent > 0 ? Infinity : 0);
|
|
125
|
+
const band = hasCap ? ladderBandForPct(pct) : (spent > 0 ? 150 : 0);
|
|
126
|
+
return buildLadder(band, Number.isFinite(pct) ? pct : 100, spent, hasCap ? cap : 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Shared tail of `budgetLadder` — the structured instruction for a band. */
|
|
130
|
+
function buildLadder(band, pct, spent, cap) {
|
|
92
131
|
|
|
93
132
|
const degrade = {
|
|
94
|
-
preferCheaper: band >=
|
|
95
|
-
cheapOnly: band >=
|
|
96
|
-
batchDeferrable: band >=
|
|
97
|
-
essentialOnly: band >=
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
133
|
+
preferCheaper: band >= 100,
|
|
134
|
+
cheapOnly: band >= 100,
|
|
135
|
+
batchDeferrable: band >= 100,
|
|
136
|
+
essentialOnly: band >= 125,
|
|
137
|
+
// 100%: sub-agent fan-out capped at 1 (the `team` rung becomes unavailable)
|
|
138
|
+
// and self-directed D3 work stops. Both are read by the daemon, not by the
|
|
139
|
+
// chain resolver, but they belong to the same instruction so a caller cannot
|
|
140
|
+
// apply half the rung.
|
|
141
|
+
maxFanout: band >= 100 ? 1 : null,
|
|
142
|
+
selfDirected: band < 100,
|
|
143
|
+
// band 100+ collapses to cheap/fast directly (degradeChain drops the
|
|
144
|
+
// frontier/default head), so the tier count is the explain-string figure
|
|
145
|
+
// rather than a second knob.
|
|
146
|
+
downgradeTiers: band >= 100 ? 99 : 0,
|
|
102
147
|
};
|
|
103
148
|
|
|
104
|
-
return { band, pct, spentUSD: round6(spent), capUSD:
|
|
149
|
+
return { band, pct, spentUSD: round6(spent), capUSD: cap, degrade };
|
|
105
150
|
}
|
|
106
151
|
|
|
107
152
|
// ---------------------------------------------------------------------------
|
|
@@ -132,72 +177,58 @@ function inferTier(member) {
|
|
|
132
177
|
* Tighten a resolved candidate chain for a budget band. PURE. NEVER throws.
|
|
133
178
|
*
|
|
134
179
|
* Contract (the `band` argument is the economics band from budgetLadder):
|
|
135
|
-
* - band <
|
|
136
|
-
* - band ≥
|
|
137
|
-
* (frontier→default→fast→cheap); the rest pass through. The
|
|
138
|
-
* resulting chain is de-duplicated, preserving order.
|
|
139
|
-
* - band ≥ 90 → frontier+default head members are DROPPED entirely (cheap/fast
|
|
180
|
+
* - band < 100 → the chain is returned unchanged (80 % is a notice rung).
|
|
181
|
+
* - band ≥ 100 → frontier+default head members are DROPPED entirely (cheap/fast
|
|
140
182
|
* only); a chain that would empty out keeps its cheapest member
|
|
141
183
|
* so the caller always has at least one candidate.
|
|
142
|
-
* - `exempt` (pin or critical) → unchanged at any band below
|
|
184
|
+
* - `exempt` (pin or critical) → unchanged at any band below 125.
|
|
143
185
|
*
|
|
144
186
|
* Members are `{ ref, alias?, ... }` (resolveChain's chain shape) or bare ref
|
|
145
187
|
* strings. The returned members keep their original shape, only `ref`/`alias`
|
|
146
188
|
* shift on a downgrade. A `rules`/sentinel member is always preserved as a tail.
|
|
147
189
|
*
|
|
148
190
|
* @param {Array<object|string>} chain resolved chain (alias-tagged members)
|
|
149
|
-
* @param {0|
|
|
191
|
+
* @param {0|100|125|150} band
|
|
150
192
|
* @param {object} [opts]
|
|
151
193
|
* @param {boolean} [opts.exempt] pin / critical — bypass degradation
|
|
152
|
-
* @param {Record<string,string>} [opts.aliases]
|
|
153
|
-
*
|
|
194
|
+
* @param {Record<string,string>} [opts.aliases] accepted and ignored — kept so
|
|
195
|
+
* callers built for the old one-rung downgrade
|
|
196
|
+
* do not have to change shape
|
|
154
197
|
* @returns {Array<object|string>} the tightened chain
|
|
155
198
|
*/
|
|
156
199
|
export function degradeChain(chain, band, opts = {}) {
|
|
157
200
|
const members = Array.isArray(chain) ? chain : [];
|
|
158
201
|
if (!members.length) return members;
|
|
159
202
|
const b = Number(band) || 0;
|
|
160
|
-
if (b <
|
|
161
|
-
|
|
162
|
-
const aliases = (opts && opts.aliases && typeof opts.aliases === "object") ? opts.aliases : {};
|
|
203
|
+
if (b < 100 || opts.exempt) return members.slice();
|
|
163
204
|
|
|
164
205
|
const isSentinel = (m) =>
|
|
165
206
|
(typeof m === "object" && m && (m.isRules || m.ref === "rules")) ||
|
|
166
207
|
m === "rules";
|
|
167
208
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.filter((m) => !isSentinel(m))
|
|
179
|
-
.sort((a, z) => (TIER_RANK[inferTier(z)] ?? -1) - (TIER_RANK[inferTier(a)] ?? -1))[0];
|
|
180
|
-
return cheapest ? [cheapest] : members.slice();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// band >= 75: downgrade each tier-tagged member one rung.
|
|
184
|
-
const next = members.map((m) => {
|
|
185
|
-
if (isSentinel(m)) return m;
|
|
209
|
+
// Cheap/fast only — drop the frontier/default head.
|
|
210
|
+
//
|
|
211
|
+
// There is no longer a one-rung "prefer cheaper" step below this. It existed
|
|
212
|
+
// for the old 75% rung, which was measured against a local config cap; on the
|
|
213
|
+
// funded-envelope ladder the band below this one is 80%, which is a supervisor
|
|
214
|
+
// NOTICE and deliberately changes no routing. Keeping a branch no band can
|
|
215
|
+
// reach would be worse than not having the rung: it reads as enforcement that
|
|
216
|
+
// is in fact dead code.
|
|
217
|
+
const kept = members.filter((m) => {
|
|
218
|
+
if (isSentinel(m)) return true;
|
|
186
219
|
const tier = inferTier(m);
|
|
187
|
-
|
|
188
|
-
const i = TIER_RANK[tier];
|
|
189
|
-
const downAlias = TIER_ORDER[Math.min(TIER_ORDER.length - 1, i + 1)];
|
|
190
|
-
if (downAlias === tier) return m; // already cheapest
|
|
191
|
-
// Retarget to the cheaper alias's ref when the caller supplied the alias map.
|
|
192
|
-
if (typeof m === "object" && m) {
|
|
193
|
-
const ref = aliases[downAlias] || m.ref;
|
|
194
|
-
return { ...m, ref, alias: downAlias };
|
|
195
|
-
}
|
|
196
|
-
return aliases[downAlias] || m;
|
|
220
|
+
return tier == null || TIER_RANK[tier] >= TIER_RANK.fast;
|
|
197
221
|
});
|
|
198
|
-
return dedupeByRef(
|
|
222
|
+
if (kept.length) return dedupeByRef(kept);
|
|
223
|
+
// Everything was frontier/default: keep the single cheapest as a safety net,
|
|
224
|
+
// because a caller with an empty chain has no candidate at all.
|
|
225
|
+
const cheapest = members
|
|
226
|
+
.filter((m) => !isSentinel(m))
|
|
227
|
+
.sort((a, z) => (TIER_RANK[inferTier(z)] ?? -1) - (TIER_RANK[inferTier(a)] ?? -1))[0];
|
|
228
|
+
return cheapest ? [cheapest] : members.slice();
|
|
199
229
|
}
|
|
200
230
|
|
|
231
|
+
/** Collapse chain members that resolved onto the same ref, preserving order. */
|
|
201
232
|
function dedupeByRef(members) {
|
|
202
233
|
const seen = new Set();
|
|
203
234
|
const out = [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for lib/model-router/economics.mjs — the router economics levers:
|
|
3
|
-
* - budgetLadder transitions at
|
|
3
|
+
* - budgetLadder transitions at 100 / 125 / 150 (a ladder, not a cliff) —
|
|
4
|
+
* the SAME rungs lib/budget-guard bands the seat envelope on
|
|
4
5
|
* - degradeChain tightens a candidate chain per band
|
|
5
6
|
* - batchLaneFor classifies deferrable vs realtime work
|
|
6
7
|
* - cacheAffinity pin / lookupPin / resolvePin override (+ pin_overridden audit)
|
|
@@ -24,11 +25,11 @@ import {
|
|
|
24
25
|
} from "./economics.mjs";
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
|
-
// budgetLadder — transitions at
|
|
28
|
+
// budgetLadder — transitions at 100/125/150 (the governance ladder's rungs)
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
|
|
30
|
-
test("budgetLadder: below
|
|
31
|
-
for (const spent of [0, 10, 37,
|
|
31
|
+
test("budgetLadder: below 100% is band 0, no degradation", () => {
|
|
32
|
+
for (const spent of [0, 10, 37, 80, 99.9]) {
|
|
32
33
|
const r = budgetLadder(spent, 100);
|
|
33
34
|
assert.equal(r.band, 0, `spent ${spent} → band 0`);
|
|
34
35
|
assert.equal(r.degrade.preferCheaper, false);
|
|
@@ -37,45 +38,65 @@ test("budgetLadder: below 75% is band 0, no degradation", () => {
|
|
|
37
38
|
}
|
|
38
39
|
});
|
|
39
40
|
|
|
40
|
-
test("budgetLadder:
|
|
41
|
-
// budget-guard notifies at
|
|
42
|
-
|
|
41
|
+
test("budgetLadder: the 80% supervisor notice does NOT degrade routing (band 0)", () => {
|
|
42
|
+
// budget-guard notifies the SUPERVISOR at 80%, but a notice is not a routing
|
|
43
|
+
// change — the first degradation is at 100%, where the seat has spent what the
|
|
44
|
+
// org actually funded.
|
|
45
|
+
const r = budgetLadder(80, 100);
|
|
43
46
|
assert.equal(r.band, 0);
|
|
44
47
|
assert.equal(r.degrade.downgradeTiers, 0);
|
|
48
|
+
assert.equal(r.degrade.selfDirected, true);
|
|
45
49
|
});
|
|
46
50
|
|
|
47
|
-
test("budgetLadder:
|
|
48
|
-
const r = budgetLadder(
|
|
49
|
-
assert.equal(r.band,
|
|
50
|
-
assert.equal(r.degrade.preferCheaper, true);
|
|
51
|
-
assert.equal(r.degrade.cheapOnly, false);
|
|
52
|
-
assert.equal(r.degrade.batchDeferrable, false);
|
|
53
|
-
assert.equal(r.degrade.essentialOnly, false);
|
|
54
|
-
assert.equal(r.degrade.downgradeTiers, 1);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("budgetLadder: 90% → cheap-only + batch deferrable", () => {
|
|
58
|
-
const r = budgetLadder(90, 100);
|
|
59
|
-
assert.equal(r.band, 90);
|
|
51
|
+
test("budgetLadder: 100% → cheapest class, fan-out 1, no self-directed work", () => {
|
|
52
|
+
const r = budgetLadder(100, 100);
|
|
53
|
+
assert.equal(r.band, 100);
|
|
60
54
|
assert.equal(r.degrade.preferCheaper, true);
|
|
61
55
|
assert.equal(r.degrade.cheapOnly, true);
|
|
62
56
|
assert.equal(r.degrade.batchDeferrable, true);
|
|
63
|
-
assert.equal(r.degrade.
|
|
57
|
+
assert.equal(r.degrade.maxFanout, 1);
|
|
58
|
+
assert.equal(r.degrade.selfDirected, false, "D3 stops here");
|
|
59
|
+
assert.equal(r.degrade.essentialOnly, false, "inbox + obligations still run — this rung is not a cliff");
|
|
64
60
|
});
|
|
65
61
|
|
|
66
|
-
test("budgetLadder:
|
|
67
|
-
const r = budgetLadder(
|
|
68
|
-
assert.equal(r.band,
|
|
62
|
+
test("budgetLadder: 125% → essential-only on top of the degrade rung", () => {
|
|
63
|
+
const r = budgetLadder(125, 100);
|
|
64
|
+
assert.equal(r.band, 125);
|
|
69
65
|
assert.equal(r.degrade.essentialOnly, true);
|
|
70
66
|
assert.equal(r.degrade.cheapOnly, true);
|
|
71
|
-
// over-cap also stays band 100
|
|
72
|
-
assert.equal(budgetLadder(250, 100).band, 100);
|
|
73
67
|
});
|
|
74
68
|
|
|
75
|
-
test("budgetLadder:
|
|
76
|
-
assert.equal(budgetLadder(
|
|
77
|
-
assert.equal(budgetLadder(
|
|
78
|
-
assert.equal(budgetLadder(
|
|
69
|
+
test("budgetLadder: past 150% stays band 150", () => {
|
|
70
|
+
assert.equal(budgetLadder(150, 100).band, 150);
|
|
71
|
+
assert.equal(budgetLadder(250, 100).band, 150);
|
|
72
|
+
assert.equal(budgetLadder(250, 100).degrade.essentialOnly, true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("budgetLadder: a cap of ZERO with money spent is EXHAUSTED, not uncapped", () => {
|
|
76
|
+
// This test used to assert band 0 for every cap<=0, and it was correct when
|
|
77
|
+
// capUSD could only be a local config number > 0. `dailyAllowance` now returns
|
|
78
|
+
// 0 for a seat whose MONTHLY envelope is spent out — so cap===0 became a
|
|
79
|
+
// reachable production value meaning "there is no room at all", and collapsing
|
|
80
|
+
// it to band 0 inverted the ladder: the further past its envelope a seat was,
|
|
81
|
+
// the less the router degraded.
|
|
82
|
+
assert.equal(budgetLadder(500, 0).band, 150, "$500 spent against a $0 allowance is fully over");
|
|
83
|
+
assert.equal(budgetLadder(500, -1).band, 150);
|
|
84
|
+
assert.equal(budgetLadder(500, NaN).band, 150);
|
|
85
|
+
// Nothing spent and no cap is genuinely unknown — do not degrade on it.
|
|
86
|
+
assert.equal(budgetLadder(0, 0).band, 0);
|
|
87
|
+
assert.equal(budgetLadder(0, NaN).band, 0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("budgetLadder: the governor's band wins over any re-derivation", () => {
|
|
91
|
+
// The dispatcher and the cadence consumer pass `dailyStatus().band`, which has
|
|
92
|
+
// already reconciled day vs month vs blindness. Re-deriving from (spent, cap)
|
|
93
|
+
// is what let a seat at 109% of its MONTH route the full frontier chain
|
|
94
|
+
// because today happened to look quiet.
|
|
95
|
+
const r = budgetLadder(3, 30, { band: 125 });
|
|
96
|
+
assert.equal(r.band, 125);
|
|
97
|
+
assert.equal(r.degrade.cheapOnly, true);
|
|
98
|
+
assert.equal(r.degrade.essentialOnly, true);
|
|
99
|
+
assert.equal(budgetLadder(3, 30).band, 0, "and without it, the local reading still applies");
|
|
79
100
|
});
|
|
80
101
|
|
|
81
102
|
test("budgetLadder: pct is rounded and exposed", () => {
|
|
@@ -99,35 +120,33 @@ function aliasChain(...names) {
|
|
|
99
120
|
return names.map((n) => ({ ref: ALIASES[n] || n, alias: ALIASES[n] ? n : null }));
|
|
100
121
|
}
|
|
101
122
|
|
|
102
|
-
test("degradeChain: band <
|
|
123
|
+
test("degradeChain: band < 100 returns the chain unchanged", () => {
|
|
103
124
|
const chain = aliasChain("frontier", "default");
|
|
104
125
|
const out = degradeChain(chain, 0, { aliases: ALIASES });
|
|
105
126
|
assert.deepEqual(out, chain);
|
|
106
127
|
assert.notEqual(out, chain, "returns a copy, not the same reference");
|
|
107
128
|
});
|
|
108
129
|
|
|
109
|
-
test("degradeChain:
|
|
130
|
+
test("degradeChain: the 80% notice rung changes no routing at all", () => {
|
|
131
|
+
// There is deliberately no one-rung "prefer cheaper" step any more: the band
|
|
132
|
+
// below the drop rung is a supervisor NOTICE, and a notice must not silently
|
|
133
|
+
// re-route the seat onto a worse model.
|
|
110
134
|
const chain = aliasChain("frontier", "default");
|
|
111
|
-
|
|
112
|
-
// frontier→default, default→fast
|
|
113
|
-
assert.equal(out[0].alias, "default");
|
|
114
|
-
assert.equal(out[0].ref, ALIASES.default);
|
|
115
|
-
assert.equal(out[1].alias, "fast");
|
|
116
|
-
assert.equal(out[1].ref, ALIASES.fast);
|
|
135
|
+
assert.deepEqual(degradeChain(chain, 80, { aliases: ALIASES }), chain);
|
|
117
136
|
});
|
|
118
137
|
|
|
119
|
-
test("degradeChain: band
|
|
138
|
+
test("degradeChain: band 100 drops frontier/default head, keeps cheap/fast", () => {
|
|
120
139
|
const chain = aliasChain("frontier", "default", "fast", "cheap");
|
|
121
|
-
const out = degradeChain(chain,
|
|
140
|
+
const out = degradeChain(chain, 100, { aliases: ALIASES });
|
|
122
141
|
const aliasesOut = out.map((m) => m.alias);
|
|
123
142
|
assert.ok(!aliasesOut.includes("frontier"));
|
|
124
143
|
assert.ok(!aliasesOut.includes("default"));
|
|
125
144
|
assert.deepEqual(aliasesOut, ["fast", "cheap"]);
|
|
126
145
|
});
|
|
127
146
|
|
|
128
|
-
test("degradeChain: band
|
|
147
|
+
test("degradeChain: band 100 with an all-frontier chain keeps the single cheapest as a safety net", () => {
|
|
129
148
|
const chain = aliasChain("frontier", "default");
|
|
130
|
-
const out = degradeChain(chain,
|
|
149
|
+
const out = degradeChain(chain, 100, { aliases: ALIASES });
|
|
131
150
|
assert.equal(out.length, 1);
|
|
132
151
|
// 'default' is cheaper than 'frontier', so it's the safety-net survivor.
|
|
133
152
|
assert.equal(out[0].alias, "default");
|
|
@@ -141,22 +160,21 @@ test("degradeChain: exempt (pin/critical) bypasses degradation", () => {
|
|
|
141
160
|
|
|
142
161
|
test("degradeChain: preserves a rules sentinel as a tail at any band", () => {
|
|
143
162
|
const chain = [...aliasChain("frontier"), { ref: "rules", isRules: true }];
|
|
144
|
-
const out90 = degradeChain(chain,
|
|
163
|
+
const out90 = degradeChain(chain, 100, { aliases: ALIASES });
|
|
145
164
|
assert.ok(out90.some((m) => m.ref === "rules"));
|
|
146
165
|
});
|
|
147
166
|
|
|
148
167
|
test("degradeChain: infers tier from literal refs without alias tags", () => {
|
|
149
168
|
const chain = [{ ref: "anthropic/claude-opus-4-8" }, { ref: "anthropic/claude-haiku-4-5" }];
|
|
150
|
-
const out = degradeChain(chain,
|
|
169
|
+
const out = degradeChain(chain, 100, { aliases: ALIASES });
|
|
151
170
|
// opus (frontier) dropped; haiku (fast) kept.
|
|
152
171
|
assert.equal(out.length, 1);
|
|
153
172
|
assert.match(out[0].ref, /haiku/);
|
|
154
173
|
});
|
|
155
174
|
|
|
156
175
|
test("degradeChain: de-dupes refs that collapse onto the same model", () => {
|
|
157
|
-
// default + fast at band 75 → fast + cheap; but if both map to fast they dedupe.
|
|
158
176
|
const chain = aliasChain("fast", "fast");
|
|
159
|
-
const out = degradeChain(chain,
|
|
177
|
+
const out = degradeChain(chain, 100, { aliases: ALIASES });
|
|
160
178
|
assert.equal(out.length, 1);
|
|
161
179
|
});
|
|
162
180
|
|
|
@@ -440,38 +440,42 @@ test("2b. context_overflow NEVER fails over (terminal, opens no breaker) inside
|
|
|
440
440
|
});
|
|
441
441
|
|
|
442
442
|
// ===========================================================================
|
|
443
|
-
// 3. budgetLadder: band
|
|
444
|
-
// degradeChain tightens the resolved chain.
|
|
443
|
+
// 3. budgetLadder: band 100 forces the cheapest class; band 125 essential-only;
|
|
444
|
+
// degradeChain tightens the resolved chain. The bands are the governance
|
|
445
|
+
// ladder's own rungs (lib/budget-guard.BANDS), measured against the seat's
|
|
446
|
+
// hq-funded envelope rather than a local config cap.
|
|
445
447
|
// ===========================================================================
|
|
446
448
|
|
|
447
449
|
test("3. budgetLadder bands + degradeChain tighten the resolved chain", () => {
|
|
448
450
|
// ── budgetLadder maps spend% onto the economics bands. ───────────────────
|
|
449
|
-
assert.deepEqual(LADDER_BANDS, [0,
|
|
451
|
+
assert.deepEqual(LADDER_BANDS, [0, 100, 125, 150]);
|
|
450
452
|
|
|
451
453
|
const low = budgetLadder(40, 100); // 40%
|
|
452
|
-
assert.equal(low.band, 0, "below
|
|
454
|
+
assert.equal(low.band, 0, "below 100% is band 0 (policy as written)");
|
|
453
455
|
assert.equal(low.degrade.preferCheaper, false);
|
|
454
456
|
|
|
455
|
-
//
|
|
456
|
-
assert.equal(budgetLadder(
|
|
457
|
+
// 80% is a budget-guard SUPERVISOR NOTICE, not a routing change.
|
|
458
|
+
assert.equal(budgetLadder(80, 100).band, 0, "the 80% notice does not degrade routing");
|
|
457
459
|
|
|
458
|
-
const
|
|
459
|
-
assert.equal(
|
|
460
|
-
assert.equal(
|
|
461
|
-
assert.equal(
|
|
462
|
-
assert.equal(
|
|
460
|
+
const b100 = budgetLadder(100, 100);
|
|
461
|
+
assert.equal(b100.band, 100, "≥100% — the seat has spent what the org funded");
|
|
462
|
+
assert.equal(b100.degrade.cheapOnly, true, "band 100 is cheapest-class only");
|
|
463
|
+
assert.equal(b100.degrade.batchDeferrable, true);
|
|
464
|
+
assert.equal(b100.degrade.maxFanout, 1, "sub-agent fan-out capped at 1");
|
|
465
|
+
assert.equal(b100.degrade.selfDirected, false, "self-directed D3 work stops");
|
|
466
|
+
assert.equal(b100.degrade.essentialOnly, false, "but inbox + obligations still run");
|
|
463
467
|
|
|
464
|
-
const
|
|
465
|
-
assert.equal(
|
|
466
|
-
assert.equal(
|
|
467
|
-
assert.equal(b90.degrade.batchDeferrable, true, "band 90 defers batch-eligible work");
|
|
468
|
+
const b125 = budgetLadder(130, 100);
|
|
469
|
+
assert.equal(b125.band, 125, "≥125% suspends the obligations");
|
|
470
|
+
assert.equal(b125.degrade.essentialOnly, true, "essential-only (still answers inbox DMs)");
|
|
468
471
|
|
|
469
|
-
const
|
|
470
|
-
assert.equal(
|
|
471
|
-
assert.equal(b100.degrade.essentialOnly, true, "band 100 is essential-only (still answers inbox DMs)");
|
|
472
|
+
const b150 = budgetLadder(200, 100);
|
|
473
|
+
assert.equal(b150.band, 150, "past 150% the spawn gate refuses non-human-reply work");
|
|
472
474
|
|
|
473
|
-
//
|
|
474
|
-
|
|
475
|
+
// A cap of 0 with money already spent is an EXHAUSTED envelope, not an unknown
|
|
476
|
+
// one — see economics.test.mjs for why this assertion flipped.
|
|
477
|
+
assert.equal(budgetLadder(500, 0).band, 150, "spend against a $0 allowance is fully over");
|
|
478
|
+
assert.equal(budgetLadder(0, 0).band, 0, "but nothing spent under no cap stays band 0");
|
|
475
479
|
|
|
476
480
|
// ── degradeChain tightens the alias-tagged chain that resolveChain built. ──
|
|
477
481
|
const aliases = {
|
|
@@ -486,38 +490,36 @@ test("3. budgetLadder bands + degradeChain tighten the resolved chain", () => {
|
|
|
486
490
|
{ ref: aliases.default, alias: "default" },
|
|
487
491
|
];
|
|
488
492
|
|
|
489
|
-
//
|
|
493
|
+
// Below the drop rung → unchanged. Includes the 80% notice band.
|
|
490
494
|
assert.deepEqual(degradeChain(chain, 0, { aliases }).map((m) => m.ref), [aliases.frontier, aliases.default]);
|
|
495
|
+
assert.deepEqual(degradeChain(chain, 80, { aliases }).map((m) => m.ref), [aliases.frontier, aliases.default]);
|
|
491
496
|
|
|
492
|
-
// band
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
tightened75.map((m) => m.alias),
|
|
497
|
-
["default", "fast"],
|
|
498
|
-
"band 75 downgrades frontier→default and default→fast"
|
|
499
|
-
);
|
|
500
|
-
assert.deepEqual(tightened75.map((m) => m.ref), [aliases.default, aliases.fast]);
|
|
501
|
-
|
|
502
|
-
// band 90 → cheap/fast only: the frontier/default head is DROPPED.
|
|
503
|
-
const tightened90 = degradeChain(
|
|
497
|
+
// band 100 → cheap/fast only: the frontier/default head is DROPPED. An
|
|
498
|
+
// all-frontier chain keeps its single cheapest member so the caller is never
|
|
499
|
+
// left with no candidate at all.
|
|
500
|
+
const tightened100 = degradeChain(
|
|
504
501
|
[
|
|
505
502
|
{ ref: aliases.frontier, alias: "frontier" },
|
|
506
503
|
{ ref: aliases.default, alias: "default" },
|
|
507
504
|
{ ref: aliases.fast, alias: "fast" },
|
|
508
505
|
{ ref: aliases.cheap, alias: "cheap" },
|
|
509
506
|
],
|
|
510
|
-
|
|
507
|
+
100,
|
|
511
508
|
{ aliases }
|
|
512
509
|
);
|
|
513
510
|
assert.deepEqual(
|
|
514
|
-
|
|
511
|
+
tightened100.map((m) => m.alias),
|
|
515
512
|
["fast", "cheap"],
|
|
516
|
-
"band
|
|
513
|
+
"band 100 drops the frontier/default head, keeps cheap/fast"
|
|
514
|
+
);
|
|
515
|
+
assert.deepEqual(
|
|
516
|
+
degradeChain(chain, 100, { aliases }).map((m) => m.alias),
|
|
517
|
+
["default"],
|
|
518
|
+
"a frontier+default chain keeps its CHEAPEST member as a safety net"
|
|
517
519
|
);
|
|
518
520
|
|
|
519
521
|
// exempt (pin / critical) → unchanged even at a degrading band.
|
|
520
|
-
const exempt = degradeChain(chain,
|
|
522
|
+
const exempt = degradeChain(chain, 100, { aliases, exempt: true });
|
|
521
523
|
assert.deepEqual(exempt.map((m) => m.ref), [aliases.frontier, aliases.default], "pin/critical is exempt from degradation");
|
|
522
524
|
|
|
523
525
|
// batchLaneFor + spawnKnobsFor (the other §8 economics levers) are wired too.
|