@cohortapp/agent-sdk 2.5.1 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/maestro.mjs +305 -89
- package/bin/maestro.test.mjs +357 -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/classifier.test.mjs +18 -9
- 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
package/lib/mandate/derive.mjs
CHANGED
|
@@ -70,6 +70,34 @@ export const SENSOR_HINTS = Object.freeze([
|
|
|
70
70
|
/** Default review rhythm per objective kind. */
|
|
71
71
|
const DEFAULT_CADENCE = { PILLAR: "quarterly", OBJECTIVE: "weekly", GOAL: "monthly" };
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Could a human adopt this node TODAY?
|
|
75
|
+
*
|
|
76
|
+
* The client-side mirror of hq's `assessAdmissibility(..., "adopt")`. It is not
|
|
77
|
+
* a gate — derivation proposes, it never adopts — it is a REPORT. The single
|
|
78
|
+
* most useful number a seat can print about its own mandate is "of the N things
|
|
79
|
+
* I proposed, M are actually adoptable", because the difference is always
|
|
80
|
+
* measurement debt somebody has to pay, and it is invisible otherwise.
|
|
81
|
+
*
|
|
82
|
+
* Funding and the slate caps are deliberately NOT checked here: they are facts
|
|
83
|
+
* about the seat, not about the objective, and only hq holds them.
|
|
84
|
+
*/
|
|
85
|
+
export function isAdoptable(o) {
|
|
86
|
+
if (!o) return false;
|
|
87
|
+
if (String(o.kind).toUpperCase() === "PILLAR") return !!o.charterSectionId;
|
|
88
|
+
return (
|
|
89
|
+
!!o.parentKey &&
|
|
90
|
+
!!o.charterSectionId &&
|
|
91
|
+
!!o.metric &&
|
|
92
|
+
Number.isFinite(Number(o.target)) &&
|
|
93
|
+
o.target != null &&
|
|
94
|
+
!!o.cadence &&
|
|
95
|
+
!!o.sensor &&
|
|
96
|
+
o.sensor.source === "method" &&
|
|
97
|
+
!!o.sensor.capability
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
73
101
|
/** Deterministic, stable, url-safe slug. Collisions are disambiguated by caller. */
|
|
74
102
|
export function slug(text, max = 48) {
|
|
75
103
|
const s = String(text || "")
|
|
@@ -138,13 +166,25 @@ export function deriveMandate(input = {}) {
|
|
|
138
166
|
const degradations = [];
|
|
139
167
|
const used = new Set();
|
|
140
168
|
|
|
141
|
-
/**
|
|
169
|
+
/**
|
|
170
|
+
* Reserve a key, or report that it is ALREADY TAKEN.
|
|
171
|
+
*
|
|
172
|
+
* This used to mint `x-2` on a collision. That is where the OBJECTIVE/GOAL
|
|
173
|
+
* mirror came from: an archetype kpiCategory and a config/priorities.yaml
|
|
174
|
+
* entry that reduce to the same slug produced TWO rows — `x` (OBJECTIVE,
|
|
175
|
+
* weekly) and `x-2` (GOAL, monthly) — describing one thing, with one gap, one
|
|
176
|
+
* sensor and two obligations. Ten of A016's 35 proposals were exactly that
|
|
177
|
+
* pair. A slug collision means the same accountability was named twice; the
|
|
178
|
+
* honest resolution is to keep the first and SAY the second was folded in, not
|
|
179
|
+
* to invent a second objective by suffixing a digit onto its name.
|
|
180
|
+
*
|
|
181
|
+
* @returns {{key:string, taken:boolean}}
|
|
182
|
+
*/
|
|
142
183
|
const key = (base) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
while (used.has(k)) k = `${slug(base)}-${n++}`;
|
|
184
|
+
const k = slug(base);
|
|
185
|
+
if (used.has(k)) return { key: k, taken: true };
|
|
146
186
|
used.add(k);
|
|
147
|
-
return k;
|
|
187
|
+
return { key: k, taken: false };
|
|
148
188
|
};
|
|
149
189
|
|
|
150
190
|
// ── 1. PILLARs from the charter ─────────────────────────────────────────
|
|
@@ -155,7 +195,11 @@ export function deriveMandate(input = {}) {
|
|
|
155
195
|
|
|
156
196
|
for (const s of pillars) {
|
|
157
197
|
const text = firstClause(s.title || s.body);
|
|
158
|
-
const k = key(text);
|
|
198
|
+
const { key: k, taken } = key(text);
|
|
199
|
+
if (taken) {
|
|
200
|
+
degradations.push({ kind: "duplicate_key", key: k, reason: `two charter sections reduce to the slug "${k}" — the second is skipped` });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
159
203
|
objectives.push({
|
|
160
204
|
key: k,
|
|
161
205
|
kind: "PILLAR",
|
|
@@ -174,8 +218,21 @@ export function deriveMandate(input = {}) {
|
|
|
174
218
|
degradations.push({ kind: "no_charter_pillars", reason: "config/agent.json has no charter.sections of kind PILLAR/CAPABILITY — objectives will have no charter provenance" });
|
|
175
219
|
}
|
|
176
220
|
|
|
177
|
-
/** Attach an objective to the most textually-related pillar, else the first. */
|
|
178
221
|
const pillarKeys = objectives.filter((o) => o.kind === "PILLAR").map((o) => ({ key: o.key, text: o.text }));
|
|
222
|
+
/**
|
|
223
|
+
* Attach an objective to the most textually-related pillar — or to NOTHING.
|
|
224
|
+
*
|
|
225
|
+
* The old fallback was `pillarKeys[0].key`: any objective that overlapped no
|
|
226
|
+
* pillar was parented onto whichever pillar happened to be first. On A016 that
|
|
227
|
+
* put 33 of 35 objectives — `global-footprint` and `capital-formation-aum-growth`
|
|
228
|
+
* among them — under `own-the-agent-platform-substrate`. The edge existed, the
|
|
229
|
+
* tree rendered, the provenance walk returned a chain, and every bit of it was
|
|
230
|
+
* fiction. A fabricated edge is worse than a missing one, because a missing one
|
|
231
|
+
* is visible.
|
|
232
|
+
*
|
|
233
|
+
* Now: no overlap, no parent. The caller drops the node and records WHY, which
|
|
234
|
+
* is a fact an operator can act on ("this charter has no pillar for revenue").
|
|
235
|
+
*/
|
|
179
236
|
const parentFor = (text) => {
|
|
180
237
|
if (!pillarKeys.length) return null;
|
|
181
238
|
const t = String(text || "").toLowerCase();
|
|
@@ -185,16 +242,68 @@ export function deriveMandate(input = {}) {
|
|
|
185
242
|
const score = overlapScore(t, p.text.toLowerCase());
|
|
186
243
|
if (score > bestScore) { bestScore = score; best = p.key; }
|
|
187
244
|
}
|
|
188
|
-
return bestScore > 0 ? best :
|
|
245
|
+
return bestScore > 0 ? best : null;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/** The charter section a pillar key carries, for provenance inheritance. */
|
|
249
|
+
const sectionForPillar = new Map(
|
|
250
|
+
objectives.filter((o) => o.kind === "PILLAR").map((o) => [o.key, o.charterSectionId || null])
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The one place a derived OBJECTIVE/GOAL enters the tree.
|
|
255
|
+
*
|
|
256
|
+
* Two different failures, deliberately handled two different ways:
|
|
257
|
+
*
|
|
258
|
+
* MIRROR (`taken`) — dropped. A slug collision means one accountability
|
|
259
|
+
* was named twice; keeping both would produce two
|
|
260
|
+
* obligations, two sensors and two gaps for one thing.
|
|
261
|
+
* There is nothing to preserve.
|
|
262
|
+
* NO PILLAR — KEPT, with `parentKey: null` and a named degradation.
|
|
263
|
+
* The node is real; what is missing is a human decision
|
|
264
|
+
* about where it belongs. Dropping it would delete work
|
|
265
|
+
* somebody asked for; fabricating an edge (the old
|
|
266
|
+
* `pillarKeys[0]` fallback) would answer that decision
|
|
267
|
+
* with a coin flip. So it is carried, unplaced, and
|
|
268
|
+
* `isAdoptable` returns false for it until a human
|
|
269
|
+
* places it.
|
|
270
|
+
*
|
|
271
|
+
* Neither path is ever silent. `counts.adoptable` and `degradations[]` are
|
|
272
|
+
* what an operator reads to see the difference between "I have 35 objectives"
|
|
273
|
+
* and "I am accountable for 4".
|
|
274
|
+
*/
|
|
275
|
+
const record = (obj, { taken, base }) => {
|
|
276
|
+
if (taken) {
|
|
277
|
+
degradations.push({
|
|
278
|
+
kind: "duplicate_key",
|
|
279
|
+
key: obj.key,
|
|
280
|
+
reason: `"${base}" reduces to the already-used slug "${obj.key}" — folded into the existing objective rather than minting an "${obj.key}-2" mirror row`,
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
objectives.push(obj);
|
|
285
|
+
if (!obj.parentKey) {
|
|
286
|
+
degradations.push({
|
|
287
|
+
kind: "no_pillar_trace",
|
|
288
|
+
key: obj.key,
|
|
289
|
+
reason: `"${obj.text}" overlaps no PILLAR/CAPABILITY section on this seat's charter — it is carried UNPLACED and is not adoptable until a human parents it. It is NOT auto-attached to the first pillar: a fabricated provenance edge is worse than a missing one, because a missing one is visible.`,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
189
292
|
};
|
|
190
293
|
|
|
191
294
|
// ── 2. OBJECTIVEs from the org's strategy streams ───────────────────────
|
|
192
295
|
for (const stream of strategyStreams(strategy)) {
|
|
193
296
|
const text = firstClause(stream.title || stream.name || stream.code);
|
|
194
|
-
const k = key(text);
|
|
297
|
+
const { key: k, taken } = key(text);
|
|
195
298
|
const sensor = pickSensor(`${text} ${stream.detail || ""}`, reachable);
|
|
196
|
-
|
|
197
|
-
|
|
299
|
+
const parentKey = parentFor(text);
|
|
300
|
+
record(
|
|
301
|
+
buildObjective({ k, text, parentKey, charterSectionId: sectionForPillar.get(parentKey) || null, sensor, cadence: "weekly", origin: "strategy", ref: stream.code || stream.id || text, overrides }),
|
|
302
|
+
{ taken, base: text }
|
|
303
|
+
);
|
|
304
|
+
// Placement and measurability are INDEPENDENT debts. Reporting the sensor
|
|
305
|
+
// debt only for placed nodes would hide it exactly where it is worst.
|
|
306
|
+
if (!taken && sensor.source === "human") degradations.push({ kind: "no_reachable_sensor", key: k, reason: `no reachable capability measures "${text}" (considered: ${sensor.considered.join(", ") || "none"})` });
|
|
198
307
|
}
|
|
199
308
|
|
|
200
309
|
// ── 3. OBJECTIVEs from the archetype's KPI categories ───────────────────
|
|
@@ -202,13 +311,14 @@ export function deriveMandate(input = {}) {
|
|
|
202
311
|
if (!cat || !cat.id) continue;
|
|
203
312
|
const text = cat.name || cat.id;
|
|
204
313
|
const example = Array.isArray(cat.examples) && cat.examples.length ? cat.examples[0] : "";
|
|
205
|
-
const k = key(cat.id);
|
|
314
|
+
const { key: k, taken } = key(cat.id);
|
|
206
315
|
const sensor = pickSensor(`${text} ${example}`, reachable);
|
|
207
|
-
|
|
208
|
-
|
|
316
|
+
const parentKey = parentFor(`${text} ${example}`);
|
|
317
|
+
record(buildObjective({
|
|
318
|
+
k, text, parentKey, charterSectionId: sectionForPillar.get(parentKey) || null, sensor, cadence: "weekly",
|
|
209
319
|
origin: "archetype", ref: `kpiCategory:${cat.id}`, metric: example ? slug(example).replace(/-/g, "_") : undefined, overrides,
|
|
210
|
-
}));
|
|
211
|
-
if (sensor.source === "human") degradations.push({ kind: "no_reachable_sensor", key: k, reason: `no reachable capability measures "${text}" (considered: ${sensor.considered.join(", ") || "none"})` });
|
|
320
|
+
}), { taken, base: cat.id });
|
|
321
|
+
if (!taken && sensor.source === "human") degradations.push({ kind: "no_reachable_sensor", key: k, reason: `no reachable capability measures "${text}" (considered: ${sensor.considered.join(", ") || "none"})` });
|
|
212
322
|
}
|
|
213
323
|
if (kpiCategories.length === 0) {
|
|
214
324
|
degradations.push({ kind: "no_kpi_categories", reason: "archetype profile carried no kpiCategories — the mandate has no role-derived objectives" });
|
|
@@ -218,10 +328,13 @@ export function deriveMandate(input = {}) {
|
|
|
218
328
|
for (const p of priorities) {
|
|
219
329
|
const text = firstClause(p && (p.title || p.text || p.id));
|
|
220
330
|
if (!text) continue;
|
|
221
|
-
const k = key(text);
|
|
331
|
+
const { key: k, taken } = key(text);
|
|
222
332
|
const sensor = pickSensor(text, reachable);
|
|
223
333
|
const parent = parentFor(text);
|
|
224
|
-
|
|
334
|
+
record(
|
|
335
|
+
buildObjective({ k, text, parentKey: parent, charterSectionId: sectionForPillar.get(parent) || null, sensor, cadence: "monthly", origin: "priorities", ref: (p && p.id) || text, kind: "GOAL", overrides }),
|
|
336
|
+
{ taken, base: text }
|
|
337
|
+
);
|
|
225
338
|
}
|
|
226
339
|
|
|
227
340
|
const counts = {
|
|
@@ -230,6 +343,12 @@ export function deriveMandate(input = {}) {
|
|
|
230
343
|
goals: objectives.filter((o) => o.kind === "GOAL").length,
|
|
231
344
|
withMethodSensor: objectives.filter((o) => o.sensor && o.sensor.source === "method").length,
|
|
232
345
|
proposed: objectives.length,
|
|
346
|
+
// What a human could ACTUALLY adopt today. This is the number that matters:
|
|
347
|
+
// "35 proposed / 0 adoptable" is the report A016 should have been producing
|
|
348
|
+
// for months, and "0 of 35 adopted" is what it produced instead.
|
|
349
|
+
adoptable: objectives.filter((o) => isAdoptable(o)).length,
|
|
350
|
+
// Named refusals, so the gap between the two numbers is never a mystery.
|
|
351
|
+
refused: degradations.filter((d) => d.kind === "no_pillar_trace" || d.kind === "duplicate_key").length,
|
|
233
352
|
};
|
|
234
353
|
|
|
235
354
|
return { objectives, degradations, counts };
|
|
@@ -289,14 +408,20 @@ export function toMandateBody(o = {}) {
|
|
|
289
408
|
// helpers
|
|
290
409
|
// ---------------------------------------------------------------------------
|
|
291
410
|
|
|
292
|
-
function buildObjective({ k, text, parentKey, sensor, cadence, origin, ref, metric, kind = "OBJECTIVE", overrides = {} }) {
|
|
411
|
+
function buildObjective({ k, text, parentKey, charterSectionId = null, sensor, cadence, origin, ref, metric, kind = "OBJECTIVE", overrides = {} }) {
|
|
293
412
|
const ov = overrides[k] || {};
|
|
294
413
|
const obj = {
|
|
295
414
|
key: k,
|
|
296
415
|
kind,
|
|
297
416
|
text,
|
|
298
417
|
parentKey: parentKey || null,
|
|
299
|
-
|
|
418
|
+
// INHERITED from the pillar this node hangs off. It used to be hardcoded
|
|
419
|
+
// null, and `lib/setup/sections/mandate.mjs` then posted `parentKey` to
|
|
420
|
+
// `mandate.propose` — which takes `parentId`/`charterSectionId` and ignores
|
|
421
|
+
// unknown params. So every proposal reached hq with BOTH provenance columns
|
|
422
|
+
// null, and hq's tree could never trace one back to a charter clause. Two
|
|
423
|
+
// silent drops, one on each side of the wire.
|
|
424
|
+
charterSectionId: charterSectionId || null,
|
|
300
425
|
state: "proposed",
|
|
301
426
|
metric: ov.metric || metric || `${k.replace(/-/g, "_")}`,
|
|
302
427
|
unit: ov.unit || sensor.unit || null,
|
|
@@ -358,4 +483,4 @@ function overlapScore(a, b) {
|
|
|
358
483
|
return n;
|
|
359
484
|
}
|
|
360
485
|
|
|
361
|
-
export default { deriveMandate, toMandateBody, pickSensor, slug, firstClause, SENSOR_HINTS, OBJECTIVE_KINDS, DIRECTIONS, CADENCES };
|
|
486
|
+
export default { deriveMandate, toMandateBody, pickSensor, isAdoptable, slug, firstClause, SENSOR_HINTS, OBJECTIVE_KINDS, DIRECTIONS, CADENCES };
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { test } from "node:test";
|
|
17
17
|
import assert from "node:assert/strict";
|
|
18
18
|
|
|
19
|
-
import { deriveMandate, toMandateBody, pickSensor, slug, firstClause } from "./derive.mjs";
|
|
19
|
+
import { deriveMandate, toMandateBody, pickSensor, slug, firstClause, isAdoptable } from "./derive.mjs";
|
|
20
20
|
import { validateMandateBody, pillars, ancestry, provenanceOf } from "./model.mjs";
|
|
21
21
|
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
@@ -83,8 +83,10 @@ test("charter PILLAR sections become PILLAR nodes carrying their charterSectionI
|
|
|
83
83
|
test("EVERYTHING derived is proposed — derivation can never adopt", () => {
|
|
84
84
|
const r = deriveMandate({
|
|
85
85
|
charterSections: CHARTER,
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
// Both of these overlap a pillar's own words, so both are PLACEABLE — see
|
|
87
|
+
// the unplaced-node test below for the other half.
|
|
88
|
+
kpiCategories: [{ id: "revenue-coverage", name: "Revenue pipeline coverage", examples: ["pipeline_coverage_x"] }],
|
|
89
|
+
priorities: [{ id: "p1", title: "Lift retention among customers we win" }],
|
|
88
90
|
manifest: MANIFEST,
|
|
89
91
|
});
|
|
90
92
|
assert.ok(r.objectives.length >= 4);
|
|
@@ -101,7 +103,7 @@ test("the derived tree is structurally valid: every objective hangs off a pillar
|
|
|
101
103
|
const r = deriveMandate({
|
|
102
104
|
charterSections: CHARTER,
|
|
103
105
|
kpiCategories: [
|
|
104
|
-
{ id: "
|
|
106
|
+
{ id: "revenue-coverage", name: "Revenue pipeline coverage", examples: ["pipeline_coverage_x"] },
|
|
105
107
|
{ id: "churn", name: "Customer retention rate", examples: ["logo_retention_pct"] },
|
|
106
108
|
],
|
|
107
109
|
manifest: MANIFEST,
|
|
@@ -122,7 +124,7 @@ test("the derived tree is structurally valid: every objective hangs off a pillar
|
|
|
122
124
|
test("provenance walks objective → pillar → charter clause, offline", () => {
|
|
123
125
|
const r = deriveMandate({
|
|
124
126
|
charterSections: CHARTER,
|
|
125
|
-
kpiCategories: [{ id: "
|
|
127
|
+
kpiCategories: [{ id: "revenue-coverage", name: "Revenue pipeline coverage", examples: ["pipeline_coverage_x"] }],
|
|
126
128
|
manifest: MANIFEST,
|
|
127
129
|
});
|
|
128
130
|
const obj = r.objectives.find((o) => o.kind === "OBJECTIVE");
|
|
@@ -130,13 +132,55 @@ test("provenance walks objective → pillar → charter clause, offline", () =>
|
|
|
130
132
|
assert.ok(prov.pillar, "an objective must resolve to a pillar");
|
|
131
133
|
assert.match(prov.clause, /^cs_/, "and the pillar must carry the charter clause id");
|
|
132
134
|
assert.ok(prov.chain.includes(obj.key));
|
|
135
|
+
// The clause id is also stamped on the OBJECTIVE ITSELF, not only on its
|
|
136
|
+
// pillar. hq's `mandate.propose` takes `charterSectionId`; it has no idea what
|
|
137
|
+
// a `parentKey` is, so a node that carried provenance only through its parent
|
|
138
|
+
// reached the server with BOTH provenance columns null — which is why all 35
|
|
139
|
+
// of A016's proposals had `charterSectionId: null`.
|
|
140
|
+
assert.equal(obj.charterSectionId, prov.clause);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("an objective that overlaps NO pillar is carried UNPLACED, never auto-attached to the first one", () => {
|
|
144
|
+
// The old fallback parented anything unmatched onto `pillarKeys[0]`. On A016
|
|
145
|
+
// that put `global-footprint` and `capital-formation-aum-growth` under
|
|
146
|
+
// `own-the-agent-platform-substrate`: an edge that existed, rendered, and was
|
|
147
|
+
// fiction.
|
|
148
|
+
const r = deriveMandate({
|
|
149
|
+
charterSections: CHARTER,
|
|
150
|
+
kpiCategories: [{ id: "warehouse-latency", name: "Warehouse latency", examples: ["p95_ms"] }],
|
|
151
|
+
manifest: MANIFEST,
|
|
152
|
+
});
|
|
153
|
+
const orphan = r.objectives.find((o) => o.key === "warehouse-latency");
|
|
154
|
+
assert.ok(orphan, "the node is KEPT — dropping it would delete work somebody asked for");
|
|
155
|
+
assert.equal(orphan.parentKey, null, "and it is NOT attached to an unrelated pillar");
|
|
156
|
+
assert.equal(orphan.charterSectionId, null);
|
|
157
|
+
assert.ok(
|
|
158
|
+
r.degradations.some((d) => d.kind === "no_pillar_trace" && d.key === "warehouse-latency"),
|
|
159
|
+
`expected a no_pillar_trace degradation; got ${JSON.stringify(r.degradations)}`
|
|
160
|
+
);
|
|
161
|
+
assert.equal(isAdoptable(orphan), false, "an unplaced objective is not adoptable");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("a slug collision folds into the existing objective instead of minting an `x-2` mirror", () => {
|
|
165
|
+
// Ten of A016's 35 proposals were `x` (OBJECTIVE/weekly) + `x-2` (GOAL/monthly)
|
|
166
|
+
// pairs: one accountability, two rows, two obligations, two gaps.
|
|
167
|
+
const r = deriveMandate({
|
|
168
|
+
charterSections: CHARTER,
|
|
169
|
+
kpiCategories: [{ id: "revenue-coverage", name: "Revenue pipeline coverage", examples: ["pipeline_coverage_x"] }],
|
|
170
|
+
priorities: [{ id: "p1", title: "Revenue coverage" }],
|
|
171
|
+
manifest: MANIFEST,
|
|
172
|
+
});
|
|
173
|
+
const keys = r.objectives.map((o) => o.key);
|
|
174
|
+
assert.equal(keys.filter((k) => k === "revenue-coverage").length, 1);
|
|
175
|
+
assert.ok(!keys.some((k) => /-\d+$/.test(k)), `no mirror key may be minted; got ${keys.join(", ")}`);
|
|
176
|
+
assert.ok(r.degradations.some((d) => d.kind === "duplicate_key"));
|
|
133
177
|
});
|
|
134
178
|
|
|
135
179
|
test("an objective with no reachable sensor is a recorded DEGRADATION, not a silent human task", () => {
|
|
136
180
|
const r = deriveMandate({
|
|
137
181
|
charterSections: CHARTER,
|
|
138
182
|
// A KPI category nothing in the manifest can measure.
|
|
139
|
-
kpiCategories: [{ id: "brand-sentiment", name: "
|
|
183
|
+
kpiCategories: [{ id: "brand-sentiment", name: "Customer brand sentiment", examples: ["nps_score"] }],
|
|
140
184
|
manifest: MANIFEST,
|
|
141
185
|
});
|
|
142
186
|
const obj = r.objectives.find((o) => o.key === "brand-sentiment");
|
package/lib/mandate/model.mjs
CHANGED
|
@@ -327,16 +327,43 @@ export function adoptedObjectives(source, opts = {}) {
|
|
|
327
327
|
if (!o || bad.has(o.key)) return false;
|
|
328
328
|
if (!MEASURABLE_KINDS.includes(String(o.kind).toUpperCase())) return false;
|
|
329
329
|
if (o.state !== "active") return false;
|
|
330
|
-
if (
|
|
330
|
+
if (adoptionDefect(o, owner)) return false;
|
|
331
331
|
return true;
|
|
332
332
|
});
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Why this objective's adoption cannot be trusted, or null when it can.
|
|
337
|
+
*
|
|
338
|
+
* TWO defects, and the second one used to FAIL OPEN — which mattered, because it
|
|
339
|
+
* was the cheaper attack. The stated threat model is "an agent handed a
|
|
340
|
+
* hand-edited state file"; forging a plausible supervisor id is work, DELETING
|
|
341
|
+
* the `adoptedById` field is one keystroke, and `isSelfAdopted` returned false
|
|
342
|
+
* for a missing field, so the row sailed through and compiled a live OUTCOME
|
|
343
|
+
* obligation with no adopter at all.
|
|
344
|
+
*
|
|
345
|
+
* "missing_adoption_provenance" — `state:"active"` with no `adoptedById`. hq
|
|
346
|
+
* stamps that column on every adopt, so an active row without one is
|
|
347
|
+
* either a tampered cache or an hq bug. Both mean the same thing offline:
|
|
348
|
+
* nobody is on record as having handed this seat the number, so it may not
|
|
349
|
+
* drive graded work. Fail CLOSED, and name it.
|
|
350
|
+
* "self_adopted" — the owner adopted its own target.
|
|
351
|
+
*
|
|
352
|
+
* @param {object} objective
|
|
353
|
+
* @param {string|null} [ownerMemberId]
|
|
354
|
+
* @returns {"missing_adoption_provenance"|"self_adopted"|null}
|
|
355
|
+
*/
|
|
356
|
+
export function adoptionDefect(objective, ownerMemberId) {
|
|
357
|
+
if (!objective) return null;
|
|
358
|
+
if (!objective.adoptedById) return "missing_adoption_provenance";
|
|
359
|
+
const owner = ownerMemberId || objective.ownerMemberId || objective.memberId || null;
|
|
360
|
+
if (owner && objective.adoptedById === owner) return "self_adopted";
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
335
364
|
/** True when a node was adopted by its own owning seat (the Goodhart hole). */
|
|
336
365
|
export function isSelfAdopted(objective, ownerMemberId) {
|
|
337
|
-
|
|
338
|
-
const owner = ownerMemberId || objective.ownerMemberId || objective.memberId || null;
|
|
339
|
-
return !!owner && objective.adoptedById === owner;
|
|
366
|
+
return adoptionDefect(objective, ownerMemberId) === "self_adopted";
|
|
340
367
|
}
|
|
341
368
|
|
|
342
369
|
export default {
|
|
@@ -357,5 +384,6 @@ export default {
|
|
|
357
384
|
provenanceOf,
|
|
358
385
|
pillars,
|
|
359
386
|
adoptedObjectives,
|
|
387
|
+
adoptionDefect,
|
|
360
388
|
isSelfAdopted,
|
|
361
389
|
};
|
|
@@ -110,8 +110,22 @@ test("the staleness ladder degrades on a ramp, not a cliff", () => {
|
|
|
110
110
|
assert.equal(permissionsForTier("expired").react, "none");
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
test("adoptedObjectives from the cache
|
|
114
|
-
|
|
113
|
+
test("adoptedObjectives from the cache carries the WHOLE law, not just the state column", () => {
|
|
114
|
+
// cache.mjs used to export a second, law-free implementation that filtered on
|
|
115
|
+
// `state === "active"` alone — so whichever module a caller happened to import
|
|
116
|
+
// decided whether the anti-Goodhart law applied. It now delegates to
|
|
117
|
+
// lib/mandate/model.adoptedObjectives. One law, one implementation.
|
|
118
|
+
const rec = {
|
|
119
|
+
body: {
|
|
120
|
+
memberId: "M-OWNER",
|
|
121
|
+
objectives: [
|
|
122
|
+
{ key: "a", kind: "OBJECTIVE", state: "active", adoptedById: "M-SUPERVISOR" },
|
|
123
|
+
{ key: "b", kind: "OBJECTIVE", state: "proposed", adoptedById: "M-SUPERVISOR" },
|
|
124
|
+
{ key: "c", kind: "OBJECTIVE", state: "active", adoptedById: "M-OWNER" }, // self-adopted
|
|
125
|
+
{ key: "d", kind: "OBJECTIVE", state: "active" }, // no provenance
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
};
|
|
115
129
|
assert.deepEqual(adoptedObjectives(rec).map((o) => o.key), ["a"]);
|
|
116
130
|
assert.deepEqual(adoptedObjectives(null), []);
|
|
117
131
|
});
|
package/lib/mcp/server.test.mjs
CHANGED
|
@@ -145,7 +145,16 @@ test("tools/list: full active surface (email tools present — family is vendore
|
|
|
145
145
|
const r = await h.request("tools/list", {});
|
|
146
146
|
const tools = r.result.tools;
|
|
147
147
|
assert.equal(tools.length, getOrgTools().length, "one MCP tool per active table entry");
|
|
148
|
-
|
|
148
|
+
// 108 → 117: the conversation-parity delta (+4 messaging per-message actions,
|
|
149
|
+
// +2 huddle start/join) and the macro/parity delta (+org_pulse, +org_huddle_end,
|
|
150
|
+
// +memory_recall).
|
|
151
|
+
// 117 → 122: the engagement + work-visibility delta. Three room primitives
|
|
152
|
+
// (messaging_open_group / messaging_create_channel / messaging_add_to_channel)
|
|
153
|
+
// are what let an agent START a conversation rather than only answer one;
|
|
154
|
+
// engage_colleagues is the judged version of that; work_track puts the ask on
|
|
155
|
+
// the board and moves it. The line above is the real invariant — this one just
|
|
156
|
+
// pins the number so a silent table change is visible in review.
|
|
157
|
+
assert.equal(tools.length, 122, "email + artifact + desk families vendored → 122 tools");
|
|
149
158
|
const names = tools.map((t) => t.name);
|
|
150
159
|
for (const expected of ["org_whoami", "org_describe", "org_rpc", "org_read", "messaging_send", "task_assign", "board_ready", "email_send", "email_inbox", "artifact_create", "artifact_act", "artifact_catalog", "email_mailboxes", "email_draft_send", "files_list", "calendar_find_a_time", "crm_list_deals", "books_reports", "meetings_recap_file"]) {
|
|
151
160
|
assert.ok(names.includes(expected), `${expected} listed`);
|
|
@@ -154,10 +163,10 @@ test("tools/list: full active surface (email tools present — family is vendore
|
|
|
154
163
|
h.server.stop();
|
|
155
164
|
});
|
|
156
165
|
|
|
157
|
-
test("tools/list honours the email gate (emailAvailable:false →
|
|
166
|
+
test("tools/list honours the email gate (emailAvailable:false → 117 tools)", async () => {
|
|
158
167
|
const h = harness({ emailAvailable: false });
|
|
159
168
|
const r = await h.request("tools/list", {});
|
|
160
|
-
assert.equal(r.result.tools.length,
|
|
169
|
+
assert.equal(r.result.tools.length, 117, "the 5 own-mailbox email tools drop out");
|
|
161
170
|
const names = new Set(r.result.tools.map((t) => t.name));
|
|
162
171
|
// The five own-mailbox tools (email:true) are gated out …
|
|
163
172
|
for (const gated of ["email_send", "email_inbox", "email_message", "email_thread", "email_mark_read"]) {
|