@cohortapp/agent-sdk 2.3.2 → 2.4.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/framework-features.json +30 -0
- package/lib/backlog.mjs +136 -0
- package/lib/cadences.mjs +63 -2
- package/lib/cadences.test.mjs +105 -0
- package/lib/capability/inventory.mjs +542 -0
- package/lib/capability/inventory.test.mjs +232 -0
- package/lib/capability/probe.mjs +255 -0
- package/lib/channels/contract.mjs +37 -1
- package/lib/channels/contract.test.mjs +25 -1
- package/lib/claude-bin.mjs +37 -3
- package/lib/claude-bin.test.mjs +42 -8
- package/lib/execution/disposition.mjs +501 -0
- package/lib/execution/disposition.test.mjs +482 -0
- package/lib/execution/drive.mjs +352 -0
- package/lib/execution/drive.test.mjs +270 -0
- package/lib/execution/effects.mjs +340 -0
- package/lib/execution/effects.test.mjs +193 -0
- package/lib/execution/index.mjs +152 -0
- package/lib/execution/intake.mjs +581 -0
- package/lib/execution/intake.test.mjs +343 -0
- package/lib/execution/journal.mjs +374 -0
- package/lib/execution/journal.test.mjs +261 -0
- package/lib/execution/match.mjs +331 -0
- package/lib/execution/match.test.mjs +235 -0
- package/lib/execution/pipeline.mjs +341 -0
- package/lib/execution/pipeline.test.mjs +389 -0
- package/lib/execution/route.mjs +332 -0
- package/lib/execution/route.test.mjs +186 -0
- package/lib/execution/surface-policy.mjs +446 -0
- package/lib/execution/surface-policy.test.mjs +162 -0
- package/lib/goals/admission.mjs +209 -0
- package/lib/goals/admission.test.mjs +139 -0
- package/lib/goals/classify.mjs +206 -0
- package/lib/goals/classify.test.mjs +109 -0
- package/lib/goals/collaborate.mjs +415 -0
- package/lib/goals/collaborate.test.mjs +324 -0
- package/lib/goals/gaps.mjs +111 -0
- package/lib/goals/gaps.test.mjs +284 -0
- package/lib/goals/loop.mjs +537 -0
- package/lib/goals/loop.test.mjs +719 -0
- package/lib/identity/persona.mjs +247 -0
- package/lib/identity/persona.test.mjs +117 -0
- package/lib/kpi.mjs +469 -0
- package/lib/kpi.test.mjs +244 -0
- package/lib/mandate/audit.mjs +168 -0
- package/lib/mandate/audit.test.mjs +195 -0
- package/lib/mandate/cache.mjs +162 -0
- package/lib/mandate/derive.mjs +317 -0
- package/lib/mandate/derive.test.mjs +224 -0
- package/lib/mandate/model.mjs +352 -0
- package/lib/mandate/model.test.mjs +145 -0
- package/lib/mandate/refresh.mjs +187 -0
- package/lib/mandate/refresh.test.mjs +293 -0
- package/lib/mcp/server.test.mjs +4 -4
- package/lib/org/approvals.mjs +14 -2
- package/lib/org/client.mjs +58 -22
- package/lib/org/client.test.mjs +3 -1
- package/lib/org/inbound/directedness.mjs +720 -0
- package/lib/org/inbound/directedness.test.mjs +543 -0
- package/lib/org/inbound/facts.mjs +501 -0
- package/lib/org/inbound/facts.test.mjs +375 -0
- package/lib/org/inbound/hydrate.mjs +535 -0
- package/lib/org/inbound/hydrate.test.mjs +326 -0
- package/lib/org/inbound/index.mjs +233 -0
- package/lib/org/inbound/index.test.mjs +324 -0
- package/lib/org/inbound/io.mjs +141 -0
- package/lib/org/inbound/project.mjs +201 -0
- package/lib/org/inbound/project.test.mjs +287 -0
- package/lib/org/inbound/surfaces.mjs +257 -0
- package/lib/org/knowledge.mjs +10 -1
- package/lib/org/knowledge.test.mjs +8 -1
- package/lib/org/leases.mjs +5 -0
- package/lib/org/mesh.mjs +17 -2
- package/lib/org/messaging.mjs +40 -4
- package/lib/org/messaging.test.mjs +40 -0
- package/lib/org/param-contract.mjs +694 -0
- package/lib/org/param-contract.test.mjs +451 -0
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +8 -0
- package/lib/org/protocol.test.mjs +5 -1
- package/lib/org/push.mjs +1025 -0
- package/lib/org/push.test.mjs +690 -0
- package/lib/org/tool-surface.mjs +138 -38
- package/lib/org/tool-surface.test.mjs +13 -8
- package/lib/org/typing.mjs +341 -0
- package/lib/org/typing.test.mjs +291 -0
- package/lib/plan/compile.mjs +510 -0
- package/lib/plan/compile.test.mjs +286 -0
- package/lib/plan/emit.mjs +256 -0
- package/lib/plan/emit.test.mjs +246 -0
- package/lib/plan/explain.mjs +226 -0
- package/lib/plan/explain.test.mjs +188 -0
- package/lib/plan/schema.mjs +140 -0
- package/lib/resource-governor.mjs +47 -1
- package/lib/resource-governor.test.mjs +21 -1
- package/lib/setup/enroll-from-cohort.mjs +84 -16
- package/lib/setup/enroll-from-cohort.test.mjs +43 -1
- package/lib/setup/sections/identity.mjs +15 -4
- package/lib/setup/sections/identity.test.mjs +94 -0
- package/lib/setup/sections/inventory.mjs +178 -0
- package/lib/setup/sections/inventory.test.mjs +198 -0
- package/lib/setup/sections/mandate.mjs +392 -0
- package/lib/setup/sections/mandate.test.mjs +373 -0
- package/lib/setup/sections/subagents.mjs +427 -0
- package/lib/setup/sections/subagents.test.mjs +429 -0
- package/lib/setup/sections/verify.mjs +121 -0
- package/lib/setup/sections/verify.test.mjs +175 -0
- package/lib/setup/sot.mjs +2 -0
- package/lib/subagents/cli.mjs +463 -0
- package/lib/subagents/cli.test.mjs +389 -0
- package/lib/subagents/client.mjs +373 -0
- package/lib/subagents/client.test.mjs +309 -0
- package/lib/subagents/gap.mjs +268 -0
- package/lib/subagents/gap.test.mjs +234 -0
- package/lib/subagents/lock.mjs +296 -0
- package/lib/subagents/lock.test.mjs +248 -0
- package/lib/subagents/manifest.mjs +224 -0
- package/lib/subagents/manifest.test.mjs +175 -0
- package/lib/subagents/refs.mjs +274 -0
- package/lib/subagents/refs.test.mjs +204 -0
- package/lib/subagents/resolve.mjs +455 -0
- package/lib/subagents/resolve.test.mjs +422 -0
- package/lib/subagents/schema.mjs +467 -0
- package/lib/subagents/schema.test.mjs +306 -0
- package/package.json +8 -3
- package/plugins/maestro-skills/.claude-plugin/marketplace.json +16 -0
- package/policies/ai-disclosure.yaml +42 -2
- package/scaffold/CLAUDE.md +16 -2
- package/schedules/triggers/goal-steward.md +79 -0
- package/scripts/ci/conformance-org-api.mjs +792 -0
- package/scripts/ci/conformance-org-api.test.mjs +417 -0
- package/scripts/daemon/agent-daemon.mjs +36 -4
- package/scripts/daemon/cadence-handlers.mjs +145 -1
- package/scripts/daemon/goal-steward-cadence.test.mjs +243 -0
- package/scripts/daemon/inbox-deferral.mjs +45 -2
- package/scripts/daemon/inbox-deferral.test.mjs +56 -0
- package/scripts/daemon/inbox-wake.mjs +282 -0
- package/scripts/daemon/inbox-wake.test.mjs +199 -0
- package/scripts/daemon/prompt-builder.mjs +41 -1
- package/scripts/daemon/typing-registry.mjs +55 -2
- package/scripts/daemon/typing-registry.test.mjs +25 -0
- package/scripts/local-triggers/generate-plists.test.mjs +5 -5
- package/scripts/setup/gen-subagent-manifest.mjs +95 -0
- package/scripts/setup/gen-subagent-manifest.test.mjs +124 -0
- package/scripts/setup/generate-plan.mjs +108 -0
- package/scripts/setup/init-capability-manifest.mjs +70 -0
- package/scripts/setup/init-skill-marketplace.mjs +155 -0
- package/scripts/setup/init-skill-marketplace.test.mjs +193 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* emit.test.mjs — obligations → the files the runtime already reads (SPEC §5.7).
|
|
3
|
+
* Run: node --test lib/plan/emit.test.mjs
|
|
4
|
+
*
|
|
5
|
+
* The contract under test is compatibility: cadences.yaml, .cadence-registry.json
|
|
6
|
+
* and .cadence-plists.tsv must keep the EXACT shapes their existing consumers
|
|
7
|
+
* (cadence-handlers.mjs, session-permissions.mjs, generate-plists.sh) already
|
|
8
|
+
* parse — new producer, unchanged formats.
|
|
9
|
+
*/
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
import { test } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { mkdtempSync, readFileSync, existsSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import yaml from "js-yaml";
|
|
18
|
+
|
|
19
|
+
import { emitPlan, toCadenceEntries, toRegistry, toTsvLines, scheduleXml, readLock, PLAN_REL, CADENCES_REL, REGISTRY_REL, TSV_REL, LOCK_REL } from "./emit.mjs";
|
|
20
|
+
import { compilePlan } from "./compile.mjs";
|
|
21
|
+
|
|
22
|
+
const AT = "2026-08-11T09:00:00.000Z";
|
|
23
|
+
|
|
24
|
+
function manifest() {
|
|
25
|
+
return {
|
|
26
|
+
schemaVersion: 1,
|
|
27
|
+
checksum: "fixture",
|
|
28
|
+
entries: ["board_ready", "task_create", "task_update", "board_claim", "board_complete", "crm_list_deals", "knowledge_search", "email_inbox", "email_triage", "messaging_history", "messaging_send", "email_message", "email_thread", "email_send", "approval_wait", "decision_list"]
|
|
29
|
+
.map((id) => ({ id, plane: "org", kind: "method_tool", reachable: true, blastRadius: "internal" })),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const STANDARD = [
|
|
34
|
+
{ id: "inbox-processor", scope: "standard", mode: "guarded", interval: 300, prompt: "schedules/triggers/inbox-processor.md" },
|
|
35
|
+
{ id: "daily-morning-brief", scope: "standard", mode: "escalate", calendar: { hour: 6, minute: 30 }, prompt: "schedules/triggers/daily-morning-brief.md" },
|
|
36
|
+
];
|
|
37
|
+
const ARCHETYPE = [
|
|
38
|
+
{ id: "weekly-operating-review", scope: "function", mode: "escalate", cadence: "weekly", calendar: { weekday: 2, hour: 10, minute: 0 }, prompt: "schedules/triggers/weekly-operating-review.md", purpose: "Operating review" },
|
|
39
|
+
{ id: "org-cost-sync-fn", scope: "function", mode: "inline", interval: 3600 },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function adoptedObjective() {
|
|
43
|
+
return {
|
|
44
|
+
key: "pipeline-coverage", kind: "OBJECTIVE", text: "Pipeline coverage", state: "active", id: "obj_7Kx",
|
|
45
|
+
metric: "pipeline_coverage_x", direction: "up", target: 3, tolerance: 0.2, weight: 1, cadence: "weekly",
|
|
46
|
+
sensor: { capability: "crm_list_deals", params: { stage: "open" }, source: "method" }, collaborators: [],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function planWith(objectives = []) {
|
|
51
|
+
return compilePlan({ mandate: { objectives }, manifest: manifest(), standardCadences: STANDARD, archetypeCadences: ARCHETYPE });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function tmpRoot() { return mkdtempSync(join(tmpdir(), "maestro-emit-")); }
|
|
55
|
+
|
|
56
|
+
test("emitPlan writes all five artefacts", async () => {
|
|
57
|
+
const root = tmpRoot();
|
|
58
|
+
const r = await emitPlan({ agentRoot: root, plan: planWith([adoptedObjective()]), manifest: manifest(), archetype: "executive-operator@c-suite", at: AT });
|
|
59
|
+
assert.equal(r.ok, true, r.errors.join("; "));
|
|
60
|
+
for (const rel of [PLAN_REL, CADENCES_REL, REGISTRY_REL, TSV_REL, LOCK_REL]) {
|
|
61
|
+
assert.ok(existsSync(join(root, rel)), `${rel} written`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("config/cadences.yaml keeps the shape cadence-handlers already parses", async () => {
|
|
66
|
+
const root = tmpRoot();
|
|
67
|
+
await emitPlan({ agentRoot: root, plan: planWith([adoptedObjective()]), manifest: manifest(), archetype: "executive-operator@c-suite", at: AT });
|
|
68
|
+
const doc = yaml.load(readFileSync(join(root, CADENCES_REL), "utf8"));
|
|
69
|
+
assert.equal(doc.archetype, "executive-operator@c-suite");
|
|
70
|
+
const inbox = doc.cadences.find((c) => c.id === "inbox-processor");
|
|
71
|
+
assert.equal(inbox.interval, 300);
|
|
72
|
+
assert.equal(inbox.mode, "guarded");
|
|
73
|
+
assert.equal(inbox.prompt, "schedules/triggers/inbox-processor.md");
|
|
74
|
+
assert.equal(inbox.scope, "standard");
|
|
75
|
+
// the plan-derived measure cadence is a first-class cadence id
|
|
76
|
+
assert.ok(doc.cadences.some((c) => c.id === "measure-pipeline-coverage"));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test(".cadence-registry.json carries the EXTENDED shape session-permissions can scope on", async () => {
|
|
80
|
+
const root = tmpRoot();
|
|
81
|
+
await emitPlan({ agentRoot: root, plan: planWith([adoptedObjective()]), manifest: manifest(), at: AT });
|
|
82
|
+
const reg = JSON.parse(readFileSync(join(root, REGISTRY_REL), "utf8"));
|
|
83
|
+
const m = reg["measure-pipeline-coverage"];
|
|
84
|
+
assert.equal(m.mode, "guarded");
|
|
85
|
+
assert.equal(m.guardModule, "lib/goals/loop.mjs");
|
|
86
|
+
assert.equal(m.obligationKey, "schedule.measure.pipeline-coverage");
|
|
87
|
+
assert.ok(Number.isFinite(m.budgetCents));
|
|
88
|
+
assert.ok(Array.isArray(m.allowedTools) && m.allowedTools.includes("crm_list_deals"));
|
|
89
|
+
// Back-compat: {mode, prompt} still present for every entry.
|
|
90
|
+
for (const [id, def] of Object.entries(reg)) {
|
|
91
|
+
assert.ok(def.mode, `${id} has a mode`);
|
|
92
|
+
assert.ok(def.prompt, `${id} has a prompt`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test(".cadence-plists.tsv is pipe-delimited, EXCLUDES standard cadences, and decodes to launchd XML", async () => {
|
|
97
|
+
const root = tmpRoot();
|
|
98
|
+
await emitPlan({ agentRoot: root, plan: planWith([adoptedObjective()]), manifest: manifest(), at: AT });
|
|
99
|
+
const lines = readFileSync(join(root, TSV_REL), "utf8").trim().split("\n");
|
|
100
|
+
assert.ok(lines.length > 0);
|
|
101
|
+
assert.ok(!lines.some((l) => l.startsWith("inbox-processor|")), "generate-plists.sh emits the standard set itself");
|
|
102
|
+
const interval = lines.find((l) => l.startsWith("org-cost-sync-fn|"));
|
|
103
|
+
assert.equal(interval, "org-cost-sync-fn|3600|");
|
|
104
|
+
const cal = lines.find((l) => l.startsWith("weekly-operating-review|"));
|
|
105
|
+
const [, empty, b64] = cal.split("|");
|
|
106
|
+
assert.equal(empty, "");
|
|
107
|
+
const xml = Buffer.from(b64, "base64").toString("utf8");
|
|
108
|
+
assert.match(xml, /<key>Weekday<\/key>\n\s+<integer>2<\/integer>/);
|
|
109
|
+
assert.match(xml, /<key>Hour<\/key>\n\s+<integer>10<\/integer>/);
|
|
110
|
+
// the derived measure cadence becomes a REAL launchd job
|
|
111
|
+
assert.ok(lines.some((l) => l.startsWith("measure-pipeline-coverage||")));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("scheduleXml output is byte-identical to the legacy generator's shape", () => {
|
|
115
|
+
assert.equal(
|
|
116
|
+
scheduleXml({ weekday: 2, hour: 10, minute: 0 }),
|
|
117
|
+
" <key>Weekday</key>\n <integer>2</integer>\n <key>Hour</key>\n <integer>10</integer>\n <key>Minute</key>\n <integer>0</integer>",
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("IDEMPOTENT: re-emitting an unchanged plan produces byte-identical files (no plist churn)", async () => {
|
|
122
|
+
const root = tmpRoot();
|
|
123
|
+
const plan = planWith([adoptedObjective()]);
|
|
124
|
+
await emitPlan({ agentRoot: root, plan, manifest: manifest(), at: AT });
|
|
125
|
+
const before = [CADENCES_REL, REGISTRY_REL, TSV_REL].map((r) => readFileSync(join(root, r), "utf8"));
|
|
126
|
+
await emitPlan({ agentRoot: root, plan: planWith([adoptedObjective()]), manifest: manifest(), at: AT });
|
|
127
|
+
const after = [CADENCES_REL, REGISTRY_REL, TSV_REL].map((r) => readFileSync(join(root, r), "utf8"));
|
|
128
|
+
assert.deepEqual(after, before);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("FAIL-CLOSED: an invalid plan writes NOTHING", async () => {
|
|
132
|
+
const root = tmpRoot();
|
|
133
|
+
const bad = {
|
|
134
|
+
compilerVersion: 1, mandateVersion: 0, inputsHash: "x", obligationsHash: "y", counts: {},
|
|
135
|
+
obligations: [{
|
|
136
|
+
key: "schedule.ghost", kind: "SCHEDULE", source: { origin: "archetype", ref: "ghost" }, status: "active",
|
|
137
|
+
mode: "escalate", schedule: { interval: 600 }, cadence_id: "ghost",
|
|
138
|
+
uses: ["ghost_tool"], allowed_tools: ["Read"], action_classes: [], offline_safe: false,
|
|
139
|
+
}],
|
|
140
|
+
};
|
|
141
|
+
const r = await emitPlan({ agentRoot: root, plan: bad, manifest: manifest(), at: AT });
|
|
142
|
+
assert.equal(r.ok, false);
|
|
143
|
+
assert.ok(r.errors.some((e) => /not reachable:true/.test(e)));
|
|
144
|
+
for (const rel of [PLAN_REL, CADENCES_REL, REGISTRY_REL, TSV_REL, LOCK_REL]) {
|
|
145
|
+
assert.equal(existsSync(join(root, rel)), false, `${rel} must NOT exist after a failed validation`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("compile.lock carries the hashes verify() checks", async () => {
|
|
150
|
+
const root = tmpRoot();
|
|
151
|
+
const plan = planWith([adoptedObjective()]);
|
|
152
|
+
await emitPlan({ agentRoot: root, plan, manifest: manifest(), at: AT });
|
|
153
|
+
const lock = readLock(root);
|
|
154
|
+
assert.equal(lock.obligationsHash, plan.obligationsHash);
|
|
155
|
+
assert.equal(lock.inputsHash, plan.inputsHash);
|
|
156
|
+
assert.equal(lock.compilerVersion, plan.compilerVersion);
|
|
157
|
+
assert.equal(lock.at, AT);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("config/plan.yaml is the reviewable obligation set (provenance + sensor + tool scope)", async () => {
|
|
161
|
+
const root = tmpRoot();
|
|
162
|
+
await emitPlan({ agentRoot: root, plan: planWith([adoptedObjective()]), manifest: manifest(), enforcement: "observe-only", at: AT });
|
|
163
|
+
const doc = yaml.load(readFileSync(join(root, PLAN_REL), "utf8"));
|
|
164
|
+
assert.equal(doc.enforcement, "observe-only");
|
|
165
|
+
const outcome = doc.obligations.find((o) => o.key === "outcome.pipeline-coverage");
|
|
166
|
+
assert.equal(outcome.objective_id, "obj_7Kx");
|
|
167
|
+
assert.equal(outcome.sensor.capability, "crm_list_deals");
|
|
168
|
+
assert.equal(outcome.source.origin, "mandate");
|
|
169
|
+
assert.ok(Array.isArray(outcome.allowed_tools));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("SUSPENDED obligations are in plan.yaml but never in the schedule artefacts", () => {
|
|
173
|
+
const obs = [
|
|
174
|
+
{ key: "schedule.a", kind: "SCHEDULE", cadence_id: "a", scope: "mandate", mode: "guarded", prompt: "p.md", schedule: { interval: 60 }, status: "suspended", uses: [], allowed_tools: ["Read"], action_classes: [], offline_safe: true, source: { origin: "mandate", ref: "a" }, budget_cents_per_period: 100 },
|
|
175
|
+
{ key: "schedule.b", kind: "SCHEDULE", cadence_id: "b", scope: "mandate", mode: "guarded", prompt: "p.md", schedule: { interval: 60 }, status: "active", uses: [], allowed_tools: ["Read"], action_classes: [], offline_safe: true, source: { origin: "mandate", ref: "b" }, budget_cents_per_period: 100 },
|
|
176
|
+
];
|
|
177
|
+
assert.deepEqual(toCadenceEntries(obs).map((c) => c.id), ["b"]);
|
|
178
|
+
assert.deepEqual(Object.keys(toRegistry(obs)), ["b"]);
|
|
179
|
+
assert.deepEqual(toTsvLines(obs), ["b|60|"]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// The no-op guard (SPEC §6.4: "if obligationsHash is unchanged, nothing on disk
|
|
184
|
+
// moves"). config/plan.yaml is TRACKED, so a recompile that changes nothing must
|
|
185
|
+
// not rewrite it — otherwise the one artefact meant to make a schedule change
|
|
186
|
+
// legible in a PR diff churns on every org-pulse tick and proves nothing.
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
test("a re-emit with identical inputs moves NOTHING on disk", async () => {
|
|
190
|
+
const root = tmpRoot();
|
|
191
|
+
const opts = { agentRoot: root, manifest: manifest(), archetype: "executive-operator@c-suite" };
|
|
192
|
+
|
|
193
|
+
const first = await emitPlan({ ...opts, plan: planWith([adoptedObjective()]), at: AT });
|
|
194
|
+
assert.equal(first.ok, true);
|
|
195
|
+
assert.equal(first.files.length, 5);
|
|
196
|
+
const before = [PLAN_REL, CADENCES_REL, REGISTRY_REL, TSV_REL].map((rel) => readFileSync(join(root, rel), "utf-8"));
|
|
197
|
+
|
|
198
|
+
// Same inputs, LATER wall clock — the only thing that would otherwise differ.
|
|
199
|
+
const second = await emitPlan({ ...opts, plan: planWith([adoptedObjective()]), at: "2026-09-01T12:00:00.000Z" });
|
|
200
|
+
assert.equal(second.ok, true);
|
|
201
|
+
assert.equal(second.unchanged, true, "the re-emit is reported as a no-op");
|
|
202
|
+
assert.deepEqual(second.files, [], "no file was rewritten");
|
|
203
|
+
|
|
204
|
+
const after = [PLAN_REL, CADENCES_REL, REGISTRY_REL, TSV_REL].map((rel) => readFileSync(join(root, rel), "utf-8"));
|
|
205
|
+
assert.deepEqual(after, before, "every artefact is byte-identical");
|
|
206
|
+
// The derived views are still returned, so a caller can report on them.
|
|
207
|
+
assert.ok(second.cadences.length > 0 && Object.keys(second.registry).length > 0);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("a CHANGED mandate still re-emits (the guard is not a freeze)", async () => {
|
|
211
|
+
const root = tmpRoot();
|
|
212
|
+
const opts = { agentRoot: root, manifest: manifest(), archetype: "executive-operator@c-suite", at: AT };
|
|
213
|
+
|
|
214
|
+
await emitPlan({ ...opts, plan: planWith([]) });
|
|
215
|
+
const beforeTsv = readFileSync(join(root, TSV_REL), "utf-8");
|
|
216
|
+
|
|
217
|
+
// Adopting an objective adds measure/outcome obligations → new hashes.
|
|
218
|
+
const changed = await emitPlan({ ...opts, plan: planWith([adoptedObjective()]) });
|
|
219
|
+
assert.equal(changed.unchanged, undefined, "a real change is not a no-op");
|
|
220
|
+
assert.equal(changed.files.length, 5);
|
|
221
|
+
assert.notEqual(readFileSync(join(root, TSV_REL), "utf-8"), beforeTsv, "the launchd TSV was updated");
|
|
222
|
+
assert.equal(readLock(root).obligationsHash, planWith([adoptedObjective()]).obligationsHash);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("the guard re-emits when a tracked artefact was deleted underneath it", async () => {
|
|
226
|
+
const root = tmpRoot();
|
|
227
|
+
const opts = { agentRoot: root, manifest: manifest(), at: AT };
|
|
228
|
+
await emitPlan({ ...opts, plan: planWith([adoptedObjective()]) });
|
|
229
|
+
|
|
230
|
+
// Hashes still match, but the file is gone — a missing launchd TSV must be
|
|
231
|
+
// rebuilt, not skipped because the lock looks satisfied.
|
|
232
|
+
const { rmSync } = await import("node:fs");
|
|
233
|
+
rmSync(join(root, TSV_REL));
|
|
234
|
+
const res = await emitPlan({ ...opts, plan: planWith([adoptedObjective()]) });
|
|
235
|
+
assert.equal(res.unchanged, undefined);
|
|
236
|
+
assert.ok(existsSync(join(root, TSV_REL)), "the deleted artefact is restored");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("force:true overrides the guard", async () => {
|
|
240
|
+
const root = tmpRoot();
|
|
241
|
+
const opts = { agentRoot: root, manifest: manifest(), at: AT };
|
|
242
|
+
await emitPlan({ ...opts, plan: planWith([adoptedObjective()]) });
|
|
243
|
+
const res = await emitPlan({ ...opts, plan: planWith([adoptedObjective()]), force: true });
|
|
244
|
+
assert.equal(res.unchanged, undefined);
|
|
245
|
+
assert.equal(res.files.length, 5);
|
|
246
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/plan/explain.mjs — answer "why does this job exist?" OFFLINE (SPEC §8).
|
|
3
|
+
*
|
|
4
|
+
* Every scheduled job this agent runs is supposed to be traceable to an outcome
|
|
5
|
+
* a human adopted. That claim is worthless unless it can be CHECKED, so this
|
|
6
|
+
* module walks the chain from a launchd tick all the way back to the clause that
|
|
7
|
+
* justifies it, using only files already on disk:
|
|
8
|
+
*
|
|
9
|
+
* cadence id / obligation key
|
|
10
|
+
* → config/plan.yaml the obligation (what, when, with which tools)
|
|
11
|
+
* → objective_id the outcome it serves
|
|
12
|
+
* → state/mandate/cache.json the objective: metric, target, state, adopter
|
|
13
|
+
* → charterSectionId the charter clause the objective came from
|
|
14
|
+
* → config/agent.json the clause text
|
|
15
|
+
* → adoptedById WHO adopted it (never the agent itself)
|
|
16
|
+
*
|
|
17
|
+
* No network, no LLM, no hq round-trip. A partitioned laptop can still answer
|
|
18
|
+
* the governance question, which is the entire point of the agent-sovereign
|
|
19
|
+
* split — hq owns the mandate, the agent owns the plan, and both halves of the
|
|
20
|
+
* provenance chain are cached locally.
|
|
21
|
+
*
|
|
22
|
+
* Nothing here throws on missing inputs: an un-adopted agent legitimately has no
|
|
23
|
+
* mandate cache. It returns a `provenance` chain that stops early with an
|
|
24
|
+
* explicit `gap` — a chain that cannot be completed is REPORTED, never faked.
|
|
25
|
+
*
|
|
26
|
+
* @module lib/plan/explain
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
"use strict";
|
|
30
|
+
|
|
31
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { PLAN_REL } from "./emit.mjs";
|
|
34
|
+
|
|
35
|
+
/** Load config/plan.yaml. Returns null when absent/unparseable (with a reason). */
|
|
36
|
+
export function readPlan(agentRoot, deps = {}) {
|
|
37
|
+
const ex = deps.existsSync || existsSync;
|
|
38
|
+
const rd = deps.readFileSync || readFileSync;
|
|
39
|
+
const p = join(agentRoot, PLAN_REL);
|
|
40
|
+
if (!ex(p)) return null;
|
|
41
|
+
try {
|
|
42
|
+
const yaml = deps.yaml;
|
|
43
|
+
if (!yaml) throw new Error("explain: a yaml loader must be injected (or use explainAsync)");
|
|
44
|
+
return yaml.load(rd(p, "utf-8"));
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Load config/agent.json (for charter section text). */
|
|
51
|
+
function readAgentCfg(agentRoot, deps = {}) {
|
|
52
|
+
const ex = deps.existsSync || existsSync;
|
|
53
|
+
const rd = deps.readFileSync || readFileSync;
|
|
54
|
+
const p = join(agentRoot, "config", "agent.json");
|
|
55
|
+
if (!ex(p)) return null;
|
|
56
|
+
try { return JSON.parse(rd(p, "utf-8")); } catch { return null; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Find the obligation a lookup key names. Accepts either the obligation key
|
|
61
|
+
* (`schedule.measure.pipeline-coverage`) or the cadence id (`measure-pipeline-
|
|
62
|
+
* coverage`) — a launchd job only knows the latter.
|
|
63
|
+
* @param {object} plan @param {string} key
|
|
64
|
+
*/
|
|
65
|
+
export function findObligation(plan, key) {
|
|
66
|
+
const list = (plan && plan.obligations) || [];
|
|
67
|
+
const k = String(key || "");
|
|
68
|
+
return list.find((o) => o.key === k) || list.find((o) => o.cadence_id === k) || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Find an objective in a cached mandate body by id or key. */
|
|
72
|
+
export function findObjective(mandateBody, id) {
|
|
73
|
+
const list = (mandateBody && mandateBody.objectives) || [];
|
|
74
|
+
const k = String(id || "");
|
|
75
|
+
return list.find((o) => o.id === k) || list.find((o) => o.key === k) || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Find a charter section by id in config/agent.json. */
|
|
79
|
+
export function findCharterSection(agentCfg, sectionId) {
|
|
80
|
+
const sections = (agentCfg && agentCfg.charter && agentCfg.charter.sections) || [];
|
|
81
|
+
const k = String(sectionId || "");
|
|
82
|
+
return sections.find((s) => s && (s.id === k || s.key === k)) || null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Explain one obligation.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} o - { agentRoot, key, plan?, mandate?, agentCfg? }
|
|
89
|
+
* @param {object} [deps] - { existsSync, readFileSync, yaml }
|
|
90
|
+
* @returns {{found:boolean, key:string, provenance:object[], gap:string|null, summary:string}}
|
|
91
|
+
*/
|
|
92
|
+
export function explain(o = {}, deps = {}) {
|
|
93
|
+
const key = String(o.key || "");
|
|
94
|
+
const plan = o.plan !== undefined ? o.plan : readPlan(o.agentRoot, deps);
|
|
95
|
+
const provenance = [];
|
|
96
|
+
|
|
97
|
+
if (!plan) {
|
|
98
|
+
return { found: false, key, provenance, gap: "no-plan", summary: `no ${PLAN_REL} on disk — run \`maestro setup --only mandate\`` };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ob = findObligation(plan, key);
|
|
102
|
+
if (!ob) {
|
|
103
|
+
return { found: false, key, provenance, gap: "no-obligation", summary: `no obligation named "${key}" in ${PLAN_REL}` };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
provenance.push({
|
|
107
|
+
step: "obligation",
|
|
108
|
+
key: ob.key,
|
|
109
|
+
kind: ob.kind,
|
|
110
|
+
status: ob.status,
|
|
111
|
+
origin: (ob.source && ob.source.origin) || null,
|
|
112
|
+
when: ob.schedule
|
|
113
|
+
? (Number.isFinite(ob.schedule.interval) ? `every ${ob.schedule.interval}s` : JSON.stringify(ob.schedule.calendar))
|
|
114
|
+
: (ob.trigger ? `on ${ob.trigger.topic}.${ob.trigger.kind || "*"}` : null),
|
|
115
|
+
uses: ob.uses || [],
|
|
116
|
+
allowedTools: ob.allowed_tools || [],
|
|
117
|
+
budgetCents: ob.budget_cents_per_period ?? null,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// A standard/archetype obligation is legitimately not mandate-backed: it is
|
|
121
|
+
// the floor every agent runs. Say so plainly rather than reporting a gap.
|
|
122
|
+
const origin = (ob.source && ob.source.origin) || "";
|
|
123
|
+
if (!ob.objective_id) {
|
|
124
|
+
const floor = origin === "standard" || origin === "archetype" || origin === "react-standard";
|
|
125
|
+
return {
|
|
126
|
+
found: true, key: ob.key, provenance,
|
|
127
|
+
gap: floor ? null : "no-objective",
|
|
128
|
+
summary: floor
|
|
129
|
+
? `"${ob.key}" is a ${origin} obligation — the baseline every ${origin === "archetype" ? "seat of this archetype" : "agent"} runs. It serves no specific adopted objective.`
|
|
130
|
+
: `"${ob.key}" cites no objective_id, but its origin is "${origin}" — this obligation is unjustified and should be reported as drift.`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const mandate = o.mandate !== undefined ? o.mandate : null;
|
|
135
|
+
const body = mandate && (mandate.body || mandate);
|
|
136
|
+
const objective = findObjective(body, ob.objective_id);
|
|
137
|
+
if (!objective) {
|
|
138
|
+
return {
|
|
139
|
+
found: true, key: ob.key, provenance, gap: "no-mandate-cache",
|
|
140
|
+
summary: `"${ob.key}" serves objective ${ob.objective_id}, but it is not in the local mandate cache — refresh with \`mandate.get\` (the chain cannot be completed offline).`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
provenance.push({
|
|
145
|
+
step: "objective",
|
|
146
|
+
id: objective.id || objective.key,
|
|
147
|
+
key: objective.key,
|
|
148
|
+
text: objective.text || null,
|
|
149
|
+
metric: objective.metric || null,
|
|
150
|
+
target: objective.target ?? null,
|
|
151
|
+
direction: objective.direction || "up",
|
|
152
|
+
cadence: objective.cadence || null,
|
|
153
|
+
state: objective.state,
|
|
154
|
+
sensor: objective.sensor || null,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Adoption: the anti-Goodhart law made visible. An objective adopted by its
|
|
158
|
+
// own owner is a governance violation, and this is where a human sees it.
|
|
159
|
+
const adoptedBy = objective.adoptedById || null;
|
|
160
|
+
const owner = objective.memberId || (body && body.memberId) || null;
|
|
161
|
+
const selfAdopted = !!(adoptedBy && owner && adoptedBy === owner);
|
|
162
|
+
provenance.push({
|
|
163
|
+
step: "adoption",
|
|
164
|
+
state: objective.state,
|
|
165
|
+
adoptedById: adoptedBy,
|
|
166
|
+
adoptedAt: objective.adoptedAt || null,
|
|
167
|
+
ownerId: owner,
|
|
168
|
+
selfAdopted,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (objective.charterSectionId) {
|
|
172
|
+
const agentCfg = o.agentCfg !== undefined ? o.agentCfg : readAgentCfg(o.agentRoot, deps);
|
|
173
|
+
const section = findCharterSection(agentCfg, objective.charterSectionId);
|
|
174
|
+
provenance.push({
|
|
175
|
+
step: "charter",
|
|
176
|
+
sectionId: objective.charterSectionId,
|
|
177
|
+
kind: section ? section.kind : null,
|
|
178
|
+
title: section ? (section.title || section.heading || null) : null,
|
|
179
|
+
text: section ? (section.body || section.text || null) : null,
|
|
180
|
+
resolved: !!section,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const gap = selfAdopted ? "self-adopted" : (objective.state !== "active" ? "not-adopted" : null);
|
|
185
|
+
const target = objective.target != null ? ` toward ${objective.metric || objective.key} ${objective.direction === "down" ? "≤" : "≥"} ${objective.target}` : "";
|
|
186
|
+
const who = adoptedBy ? ` adopted by ${adoptedBy}` : " (not yet adopted)";
|
|
187
|
+
const summary = selfAdopted
|
|
188
|
+
? `GOVERNANCE VIOLATION: "${ob.key}" serves objective "${objective.key}", which was adopted by its own owner (${adoptedBy}). A seat may not define, measure and be graded on the same number.`
|
|
189
|
+
: `"${ob.key}" exists to serve "${objective.text || objective.key}"${target}${who}.`;
|
|
190
|
+
|
|
191
|
+
return { found: true, key: ob.key, provenance, gap, summary };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Convenience wrapper that loads plan.yaml + the mandate cache itself.
|
|
196
|
+
* @param {object} o - { agentRoot, key }
|
|
197
|
+
* @returns {Promise<object>}
|
|
198
|
+
*/
|
|
199
|
+
export async function explainAsync(o = {}, deps = {}) {
|
|
200
|
+
const yaml = deps.yaml || (await import("js-yaml").then((m) => m.default || m));
|
|
201
|
+
let mandate = o.mandate;
|
|
202
|
+
if (mandate === undefined) {
|
|
203
|
+
try {
|
|
204
|
+
const cache = await import("../mandate/cache.mjs");
|
|
205
|
+
mandate = cache.readCache(o.agentRoot);
|
|
206
|
+
} catch {
|
|
207
|
+
mandate = null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return explain({ ...o, mandate }, { ...deps, yaml });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Render an explanation as indented text for a CLI / SessionStart banner. */
|
|
214
|
+
export function renderExplanation(res) {
|
|
215
|
+
const lines = [res.summary];
|
|
216
|
+
for (const p of res.provenance || []) {
|
|
217
|
+
if (p.step === "obligation") lines.push(` obligation ${p.key} [${p.kind}/${p.status}] ${p.when || ""} — tools: ${(p.allowedTools || []).join(", ") || "none"}`);
|
|
218
|
+
else if (p.step === "objective") lines.push(` objective ${p.key} — ${p.metric || "?"} target ${p.target ?? "?"} (${p.state}), sensor ${p.sensor && p.sensor.capability ? p.sensor.capability : "none"}`);
|
|
219
|
+
else if (p.step === "adoption") lines.push(` adoption ${p.adoptedById ? `by ${p.adoptedById}${p.adoptedAt ? ` at ${p.adoptedAt}` : ""}` : "NOT ADOPTED"}${p.selfAdopted ? " ← SELF-ADOPTED (invalid)" : ""}`);
|
|
220
|
+
else if (p.step === "charter") lines.push(` charter ${p.sectionId}${p.title ? ` — ${p.title}` : ""}${p.resolved ? "" : " (clause not found locally)"}`);
|
|
221
|
+
}
|
|
222
|
+
if (res.gap) lines.push(` gap ${res.gap}`);
|
|
223
|
+
return lines.join("\n");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export default { explain, explainAsync, findObligation, findObjective, findCharterSection, readPlan, renderExplanation };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* explain.test.mjs — the OFFLINE provenance walk (SPEC §8).
|
|
3
|
+
* Run: node --test lib/plan/explain.test.mjs
|
|
4
|
+
*
|
|
5
|
+
* The claim this module has to defend: every scheduled job an agent runs can be
|
|
6
|
+
* traced back to an outcome a human adopted, WITHOUT calling hq. These tests
|
|
7
|
+
* inject the whole world (plan, mandate cache, agent.json) so the walk is proven
|
|
8
|
+
* to touch no network and to REPORT an incomplete chain rather than fake one.
|
|
9
|
+
*/
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
import { test } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
|
|
15
|
+
import { explain, findObligation, findObjective, findCharterSection, renderExplanation } from "./explain.mjs";
|
|
16
|
+
|
|
17
|
+
const OBLIGATION = {
|
|
18
|
+
key: "schedule.measure.pipeline-coverage",
|
|
19
|
+
kind: "SCHEDULE",
|
|
20
|
+
cadence_id: "measure-pipeline-coverage",
|
|
21
|
+
status: "active",
|
|
22
|
+
source: { origin: "mandate", ref: "obj_7Kx", charter_section: "cs_12" },
|
|
23
|
+
objective_id: "obj_7Kx",
|
|
24
|
+
schedule: { calendar: { weekday: 2, hour: 14, minute: 30 } },
|
|
25
|
+
uses: ["crm_list_deals"],
|
|
26
|
+
allowed_tools: ["Glob", "Grep", "LS", "Read", "crm_list_deals"],
|
|
27
|
+
budget_cents_per_period: 500,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const STANDARD_OBLIGATION = {
|
|
31
|
+
key: "schedule.inbox-processor",
|
|
32
|
+
kind: "SCHEDULE",
|
|
33
|
+
cadence_id: "inbox-processor",
|
|
34
|
+
status: "active",
|
|
35
|
+
source: { origin: "standard", ref: "inbox-processor" },
|
|
36
|
+
schedule: { interval: 300 },
|
|
37
|
+
uses: ["email_inbox"],
|
|
38
|
+
allowed_tools: ["Read", "email_inbox"],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const PLAN = { schemaVersion: 1, obligations: [OBLIGATION, STANDARD_OBLIGATION] };
|
|
42
|
+
|
|
43
|
+
function mandate(over = {}) {
|
|
44
|
+
return {
|
|
45
|
+
version: 3,
|
|
46
|
+
body: {
|
|
47
|
+
memberId: "mem_SELF",
|
|
48
|
+
objectives: [{
|
|
49
|
+
id: "obj_7Kx",
|
|
50
|
+
key: "pipeline-coverage",
|
|
51
|
+
text: "Keep qualified pipeline at 3x the quarterly target",
|
|
52
|
+
metric: "pipeline_coverage_x",
|
|
53
|
+
target: 3,
|
|
54
|
+
direction: "up",
|
|
55
|
+
cadence: "weekly",
|
|
56
|
+
state: "active",
|
|
57
|
+
memberId: "mem_SELF",
|
|
58
|
+
adoptedById: "mem_HUMAN",
|
|
59
|
+
adoptedAt: "2026-08-01T10:00:00.000Z",
|
|
60
|
+
charterSectionId: "cs_12",
|
|
61
|
+
sensor: { capability: "crm_list_deals", source: "method" },
|
|
62
|
+
...over,
|
|
63
|
+
}],
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const AGENT_CFG = {
|
|
69
|
+
charter: {
|
|
70
|
+
sections: [{ id: "cs_12", kind: "PILLAR", title: "Revenue durability", body: "Sustain a predictable commercial engine." }],
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
test("explain walks obligation → objective → adoption → charter clause", () => {
|
|
75
|
+
const res = explain({ agentRoot: "/agent", key: "schedule.measure.pipeline-coverage", plan: PLAN, mandate: mandate(), agentCfg: AGENT_CFG });
|
|
76
|
+
|
|
77
|
+
assert.equal(res.found, true);
|
|
78
|
+
assert.equal(res.gap, null);
|
|
79
|
+
const steps = res.provenance.map((p) => p.step);
|
|
80
|
+
assert.deepEqual(steps, ["obligation", "objective", "adoption", "charter"]);
|
|
81
|
+
|
|
82
|
+
const [ob, obj, adopt, charter] = res.provenance;
|
|
83
|
+
assert.equal(ob.kind, "SCHEDULE");
|
|
84
|
+
assert.deepEqual(ob.allowedTools, ["Glob", "Grep", "LS", "Read", "crm_list_deals"]);
|
|
85
|
+
assert.equal(obj.metric, "pipeline_coverage_x");
|
|
86
|
+
assert.equal(obj.target, 3);
|
|
87
|
+
assert.equal(adopt.adoptedById, "mem_HUMAN");
|
|
88
|
+
assert.equal(adopt.selfAdopted, false);
|
|
89
|
+
assert.equal(charter.resolved, true);
|
|
90
|
+
assert.equal(charter.title, "Revenue durability");
|
|
91
|
+
assert.match(res.summary, /Keep qualified pipeline/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("explain resolves a launchd CADENCE ID, not just the obligation key", () => {
|
|
95
|
+
// A launchd job only knows `measure-pipeline-coverage`.
|
|
96
|
+
const res = explain({ key: "measure-pipeline-coverage", plan: PLAN, mandate: mandate(), agentCfg: AGENT_CFG });
|
|
97
|
+
assert.equal(res.found, true);
|
|
98
|
+
assert.equal(res.key, "schedule.measure.pipeline-coverage");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("a standard cadence is honestly reported as the floor, not as a gap", () => {
|
|
102
|
+
const res = explain({ key: "inbox-processor", plan: PLAN, mandate: mandate(), agentCfg: AGENT_CFG });
|
|
103
|
+
assert.equal(res.found, true);
|
|
104
|
+
assert.equal(res.gap, null, "the baseline set is not drift");
|
|
105
|
+
assert.match(res.summary, /baseline every agent runs/);
|
|
106
|
+
assert.equal(res.provenance.length, 1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("a mandate-origin obligation with no objective_id is flagged as unjustified", () => {
|
|
110
|
+
const orphan = { ...OBLIGATION, key: "schedule.orphan", cadence_id: "orphan", objective_id: undefined, source: { origin: "mandate" } };
|
|
111
|
+
const res = explain({ key: "schedule.orphan", plan: { obligations: [orphan] }, mandate: mandate(), agentCfg: AGENT_CFG });
|
|
112
|
+
assert.equal(res.gap, "no-objective");
|
|
113
|
+
assert.match(res.summary, /unjustified/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("SELF-ADOPTION is surfaced as a governance violation (the anti-Goodhart law)", () => {
|
|
117
|
+
// adoptedById === the objective's own owner: a seat defining, measuring and
|
|
118
|
+
// being graded on the same number.
|
|
119
|
+
const res = explain({ key: "schedule.measure.pipeline-coverage", plan: PLAN, mandate: mandate({ adoptedById: "mem_SELF" }), agentCfg: AGENT_CFG });
|
|
120
|
+
assert.equal(res.gap, "self-adopted");
|
|
121
|
+
assert.match(res.summary, /GOVERNANCE VIOLATION/);
|
|
122
|
+
assert.equal(res.provenance.find((p) => p.step === "adoption").selfAdopted, true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("a proposed (un-adopted) objective reports the not-adopted gap", () => {
|
|
126
|
+
const res = explain({ key: "schedule.measure.pipeline-coverage", plan: PLAN, mandate: mandate({ state: "proposed", adoptedById: null, adoptedAt: null }), agentCfg: AGENT_CFG });
|
|
127
|
+
assert.equal(res.gap, "not-adopted");
|
|
128
|
+
assert.match(res.summary, /not yet adopted/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("an incomplete chain is REPORTED, never faked", () => {
|
|
132
|
+
// No mandate cache at all (an offline agent that never synced).
|
|
133
|
+
const noCache = explain({ key: "schedule.measure.pipeline-coverage", plan: PLAN, mandate: null, agentCfg: AGENT_CFG });
|
|
134
|
+
assert.equal(noCache.found, true);
|
|
135
|
+
assert.equal(noCache.gap, "no-mandate-cache");
|
|
136
|
+
assert.equal(noCache.provenance.length, 1, "the walk stops where the evidence stops");
|
|
137
|
+
|
|
138
|
+
// No plan at all.
|
|
139
|
+
const noPlan = explain({ key: "whatever", plan: null });
|
|
140
|
+
assert.equal(noPlan.found, false);
|
|
141
|
+
assert.equal(noPlan.gap, "no-plan");
|
|
142
|
+
|
|
143
|
+
// Plan present, key unknown.
|
|
144
|
+
const noOb = explain({ key: "schedule.nonexistent", plan: PLAN, mandate: mandate() });
|
|
145
|
+
assert.equal(noOb.found, false);
|
|
146
|
+
assert.equal(noOb.gap, "no-obligation");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("an unresolvable charter clause is marked unresolved rather than dropped", () => {
|
|
150
|
+
const res = explain({ key: "schedule.measure.pipeline-coverage", plan: PLAN, mandate: mandate(), agentCfg: { charter: { sections: [] } } });
|
|
151
|
+
const charter = res.provenance.find((p) => p.step === "charter");
|
|
152
|
+
assert.ok(charter, "the charter step is still emitted");
|
|
153
|
+
assert.equal(charter.resolved, false);
|
|
154
|
+
assert.equal(charter.sectionId, "cs_12");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("lookup helpers match on either id or key", () => {
|
|
158
|
+
assert.equal(findObligation(PLAN, "measure-pipeline-coverage").key, "schedule.measure.pipeline-coverage");
|
|
159
|
+
assert.equal(findObligation(PLAN, "nope"), null);
|
|
160
|
+
const body = mandate().body;
|
|
161
|
+
assert.equal(findObjective(body, "obj_7Kx").key, "pipeline-coverage");
|
|
162
|
+
assert.equal(findObjective(body, "pipeline-coverage").id, "obj_7Kx");
|
|
163
|
+
assert.equal(findObjective(body, "nope"), null);
|
|
164
|
+
assert.equal(findCharterSection(AGENT_CFG, "cs_12").kind, "PILLAR");
|
|
165
|
+
assert.equal(findCharterSection(AGENT_CFG, "cs_99"), null);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("renderExplanation prints every step it walked", () => {
|
|
169
|
+
const res = explain({ key: "schedule.measure.pipeline-coverage", plan: PLAN, mandate: mandate(), agentCfg: AGENT_CFG });
|
|
170
|
+
const text = renderExplanation(res);
|
|
171
|
+
assert.match(text, /obligation {2}schedule\.measure\.pipeline-coverage/);
|
|
172
|
+
assert.match(text, /objective {3}pipeline-coverage/);
|
|
173
|
+
assert.match(text, /adoption {4}by mem_HUMAN/);
|
|
174
|
+
assert.match(text, /charter {5}cs_12 — Revenue durability/);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("explain touches no network and no clock (pure over injected inputs)", () => {
|
|
178
|
+
// Same inputs, twice, with fetch sabotaged: identical output.
|
|
179
|
+
const realFetch = globalThis.fetch;
|
|
180
|
+
globalThis.fetch = () => { throw new Error("explain must not call the network"); };
|
|
181
|
+
try {
|
|
182
|
+
const a = explain({ key: "schedule.measure.pipeline-coverage", plan: PLAN, mandate: mandate(), agentCfg: AGENT_CFG });
|
|
183
|
+
const b = explain({ key: "schedule.measure.pipeline-coverage", plan: PLAN, mandate: mandate(), agentCfg: AGENT_CFG });
|
|
184
|
+
assert.deepEqual(a, b);
|
|
185
|
+
} finally {
|
|
186
|
+
globalThis.fetch = realFetch;
|
|
187
|
+
}
|
|
188
|
+
});
|