@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.
Files changed (107) hide show
  1. package/bin/maestro.mjs +305 -89
  2. package/bin/maestro.test.mjs +357 -48
  3. package/docs/runbooks/backup-restore.md +65 -33
  4. package/framework-features.json +4 -4
  5. package/lib/backup/policy.mjs +710 -0
  6. package/lib/backup/policy.test.mjs +305 -0
  7. package/lib/budget-escalate.mjs +133 -0
  8. package/lib/budget-escalate.test.mjs +232 -0
  9. package/lib/budget-guard.envelope.test.mjs +476 -0
  10. package/lib/budget-guard.mjs +853 -75
  11. package/lib/budget-guard.test.mjs +91 -42
  12. package/lib/cadences.mjs +33 -0
  13. package/lib/channels/orgmail/adapter.mjs +88 -3
  14. package/lib/channels/orgmail/adapter.test.mjs +137 -0
  15. package/lib/channels/repeat-suppressor.mjs +198 -0
  16. package/lib/channels/repeat-suppressor.test.mjs +134 -0
  17. package/lib/comms/receipts.mjs +297 -0
  18. package/lib/cost/ledger-row.mjs +333 -0
  19. package/lib/cost/ledger-row.test.mjs +183 -0
  20. package/lib/execution/drive.mjs +28 -1
  21. package/lib/execution/effects.mjs +191 -12
  22. package/lib/execution/effects.test.mjs +50 -11
  23. package/lib/goals/admission.mjs +13 -1
  24. package/lib/goals/admission.test.mjs +26 -1
  25. package/lib/goals/loop.mjs +13 -0
  26. package/lib/kpi-sensors.test.mjs +3 -0
  27. package/lib/mandate/cache.mjs +13 -5
  28. package/lib/mandate/derive.mjs +146 -21
  29. package/lib/mandate/derive.test.mjs +50 -6
  30. package/lib/mandate/model.mjs +32 -4
  31. package/lib/mandate/refresh.test.mjs +16 -2
  32. package/lib/mcp/server.test.mjs +12 -3
  33. package/lib/model-router/economics.mjs +107 -76
  34. package/lib/model-router/economics.test.mjs +64 -46
  35. package/lib/model-router/integration-coverage.test.mjs +39 -37
  36. package/lib/model-router/ledger.mjs +75 -22
  37. package/lib/model-router/ledger.test.mjs +35 -2
  38. package/lib/org/client.mjs +14 -0
  39. package/lib/org/cost-sync.mjs +16 -2
  40. package/lib/org/doctor.mjs +62 -1
  41. package/lib/org/doctor.test.mjs +36 -3
  42. package/lib/org/email-remedy.mjs +49 -0
  43. package/lib/org/engagement-ledger.mjs +376 -0
  44. package/lib/org/engagement-ledger.test.mjs +112 -0
  45. package/lib/org/engagement.mjs +1056 -0
  46. package/lib/org/engagement.test.mjs +739 -0
  47. package/lib/org/messaging.mjs +230 -3
  48. package/lib/org/messaging.test.mjs +110 -1
  49. package/lib/org/param-contract.mjs +56 -2
  50. package/lib/org/param-contract.test.mjs +26 -0
  51. package/lib/org/protocol.checksum +1 -1
  52. package/lib/org/protocol.mjs +5 -0
  53. package/lib/org/protocol.test.mjs +7 -1
  54. package/lib/org/tool-surface.mjs +506 -10
  55. package/lib/org/tool-surface.test.mjs +191 -7
  56. package/lib/org/ui-parity.mjs +333 -6
  57. package/lib/org/ui-parity.test.mjs +96 -3
  58. package/lib/org/work-ledger.mjs +241 -0
  59. package/lib/org/work-ledger.test.mjs +237 -0
  60. package/lib/plan/adoption-e2e.test.mjs +366 -0
  61. package/lib/plan/budget-enforcement.test.mjs +400 -0
  62. package/lib/plan/budget-runtime.mjs +215 -0
  63. package/lib/plan/compile.mjs +201 -5
  64. package/lib/plan/compile.test.mjs +19 -5
  65. package/lib/plan/emit.mjs +8 -0
  66. package/lib/plan/emit.test.mjs +18 -0
  67. package/lib/resource-governor.mjs +58 -12
  68. package/lib/resource-governor.test.mjs +41 -1
  69. package/lib/security/audit-engine.mjs +45 -8
  70. package/lib/security/audit-engine.test.mjs +35 -0
  71. package/lib/setup/enroll-from-cohort.mjs +14 -1
  72. package/lib/setup/sections/mandate.mjs +48 -7
  73. package/lib/setup/sections/mandate.test.mjs +17 -2
  74. package/lib/setup/sections/orgmail.mjs +10 -2
  75. package/lib/setup/state.mjs +83 -2
  76. package/lib/telemetry/collect.mjs +360 -20
  77. package/lib/telemetry/collect.test.mjs +266 -0
  78. package/package.json +1 -1
  79. package/scripts/cost/track-claude-usage.mjs +207 -48
  80. package/scripts/cost/track-claude-usage.test.mjs +148 -0
  81. package/scripts/daemon/agent-daemon.mjs +315 -17
  82. package/scripts/daemon/assurance-e2e.test.mjs +421 -0
  83. package/scripts/daemon/assurance.mjs +944 -0
  84. package/scripts/daemon/assurance.test.mjs +668 -0
  85. package/scripts/daemon/cadence-consumer-governance.test.mjs +56 -0
  86. package/scripts/daemon/cadence-consumer.mjs +147 -9
  87. package/scripts/daemon/cadence-consumer.test.mjs +6 -0
  88. package/scripts/daemon/cadence-handlers.mjs +158 -0
  89. package/scripts/daemon/cadence-handlers.test.mjs +64 -0
  90. package/scripts/daemon/classifier.test.mjs +18 -9
  91. package/scripts/daemon/deliver.mjs +314 -0
  92. package/scripts/daemon/dispatcher-governance.test.mjs +10 -0
  93. package/scripts/daemon/dispatcher.mjs +64 -6
  94. package/scripts/daemon/responder-cost.test.mjs +68 -0
  95. package/scripts/daemon/responder.mjs +351 -298
  96. package/scripts/local-triggers/generate-plists.test.mjs +7 -4
  97. package/scripts/maintenance/backup-run.mjs +415 -0
  98. package/scripts/maintenance/backup-to-cloud.sh +16 -116
  99. package/scripts/org/send-orgmail.mjs +16 -0
  100. package/scripts/record-receipt.sh +63 -0
  101. package/scripts/restore-from-backup.sh +14 -3
  102. package/scripts/restore-from-backup.test.mjs +8 -5
  103. package/scripts/send-email-threaded.py +47 -0
  104. package/scripts/send-sms.sh +4 -0
  105. package/scripts/send-whatsapp.sh +4 -0
  106. package/scripts/setup/init-backup.mjs +93 -38
  107. package/scripts/slack-send.sh +12 -0
@@ -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
- /** Reserve a unique key. */
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
- let k = slug(base);
144
- let n = 2;
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 : pillarKeys[0].key;
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
- objectives.push(buildObjective({ k, text, parentKey: parentFor(text), sensor, cadence: "weekly", origin: "strategy", ref: stream.code || stream.id || text, overrides }));
197
- if (sensor.source === "human") degradations.push({ kind: "no_reachable_sensor", key: k, reason: `no reachable capability measures "${text}" (considered: ${sensor.considered.join(", ") || "none"})` });
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
- objectives.push(buildObjective({
208
- k, text, parentKey: parentFor(`${text} ${example}`), sensor, cadence: "weekly",
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
- objectives.push(buildObjective({ k, text, parentKey: parent, sensor, cadence: "monthly", origin: "priorities", ref: (p && p.id) || text, kind: "GOAL", overrides }));
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
- charterSectionId: null,
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
- kpiCategories: [{ id: "pipeline", name: "Pipeline coverage", examples: ["pipeline_coverage_x"] }],
87
- priorities: [{ id: "p1", title: "Land three lighthouse accounts" }],
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: "pipeline", name: "Pipeline coverage", examples: ["pipeline_coverage_x"] },
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: "pipeline", name: "Pipeline coverage", examples: ["pipeline_coverage_x"] }],
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: "Brand sentiment", examples: ["nps_score"] }],
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");
@@ -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 (isSelfAdopted(o, owner)) return false;
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
- if (!objective || !objective.adoptedById) return false;
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 admits only active nodes", () => {
114
- const rec = { body: { objectives: [{ key: "a", state: "active" }, { key: "b", state: "proposed" }] } };
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
  });
@@ -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
- assert.equal(tools.length, 108, "email + artifact + desk families vendored → 108 tools");
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 → 103 tools)", async () => {
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, 103, "the 5 own-mailbox email tools drop out");
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"]) {