@cohortapp/agent-sdk 2.3.1 → 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/bin/maestro.mjs +37 -50
- 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/channels/inbox-item.mjs +20 -0
- 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 +79 -25
- package/lib/org/client.test.mjs +54 -1
- package/lib/org/doctor.mjs +64 -0
- package/lib/org/doctor.test.mjs +31 -2
- 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 +45 -2
- package/lib/org/mesh.test.mjs +55 -0
- package/lib/org/messaging.mjs +180 -15
- package/lib/org/messaging.test.mjs +117 -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 +9 -4
- 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 +70 -11
- package/scripts/daemon/cadence-handlers.mjs +187 -5
- 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/maestro-daemon.mjs +23 -0
- package/scripts/daemon/prompt-builder.mjs +41 -1
- package/scripts/daemon/responder.mjs +56 -0
- 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/poller/inbox-scan-poller.mjs +26 -1
- package/scripts/poller/inbox-scan-poller.test.mjs +64 -0
- package/scripts/poller/slack-cloud-relay-client.mjs +5 -0
- package/scripts/poller/slack-poller.mjs +32 -0
- package/scripts/poller/slack-socket-mode.mjs +27 -1
- package/scripts/poller/slack-socket-mode.test.mjs +52 -0
- package/scripts/poller/utils.mjs +47 -0
- 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,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/plan/schema.mjs — the OBLIGATION schema + a fail-closed validator.
|
|
3
|
+
*
|
|
4
|
+
* The plan is the agent's half of the ownership split: hq owns the mandate, the
|
|
5
|
+
* agent owns the obligations, the schedule and the backlog. An obligation is the
|
|
6
|
+
* unit of that plan — one of three kinds:
|
|
7
|
+
*
|
|
8
|
+
* REACT — "when <topic/kind> arrives and <predicate> holds, do this"
|
|
9
|
+
* SCHEDULE — "at <interval|calendar>, run this" (compiles to a launchd plist)
|
|
10
|
+
* OUTCOME — "this number, measured by this sensor, must reach this target"
|
|
11
|
+
*
|
|
12
|
+
* Validation is dependency-free and FAIL-CLOSED (mirrors lib/archetype.mjs's
|
|
13
|
+
* validateProfile): a plan that does not validate is not written, because a
|
|
14
|
+
* half-valid plan silently emits half a schedule, and a missing launchd job is
|
|
15
|
+
* exactly the class of silent failure this system keeps growing.
|
|
16
|
+
*
|
|
17
|
+
* Two structural laws are enforced here rather than by convention:
|
|
18
|
+
* 1. every `uses[]` id must be REACHABLE in the capability manifest
|
|
19
|
+
* 2. every OUTCOME obligation must carry a sensor, and a `source:"llm"` sensor
|
|
20
|
+
* is never allowed to close a loop (it is advisory only)
|
|
21
|
+
*
|
|
22
|
+
* PURE. No fs, no clock, no network.
|
|
23
|
+
*
|
|
24
|
+
* @module lib/plan/schema
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
"use strict";
|
|
28
|
+
|
|
29
|
+
/** The three obligation kinds. */
|
|
30
|
+
export const OBLIGATION_KINDS = Object.freeze(["REACT", "SCHEDULE", "OUTCOME"]);
|
|
31
|
+
/** Where an obligation came from (its provenance origin). */
|
|
32
|
+
export const ORIGINS = Object.freeze(["standard", "archetype", "mandate", "react-standard"]);
|
|
33
|
+
/** Execution modes the cadence consumer understands (unchanged vocabulary). */
|
|
34
|
+
export const MODES = Object.freeze(["inline", "guarded", "escalate"]);
|
|
35
|
+
/** Blast-radius classes that force an approval before any rung executes. */
|
|
36
|
+
export const ACTION_CLASSES = Object.freeze(["internal", "external", "irreversible", "financial"]);
|
|
37
|
+
/** Sensor provenance — the honesty gate. */
|
|
38
|
+
export const SENSOR_SOURCES = Object.freeze(["method", "human", "llm"]);
|
|
39
|
+
/** Statuses an obligation can hold locally. */
|
|
40
|
+
export const STATUSES = Object.freeze(["active", "suspended", "proposed"]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate ONE obligation.
|
|
44
|
+
* @param {object} ob
|
|
45
|
+
* @param {{reachable?:Set<string>}} [o]
|
|
46
|
+
* @returns {string[]} error strings (empty = valid)
|
|
47
|
+
*/
|
|
48
|
+
export function validateObligation(ob, o = {}) {
|
|
49
|
+
const errs = [];
|
|
50
|
+
const at = (m) => `obligation ${(ob && ob.key) || "<no key>"}: ${m}`;
|
|
51
|
+
if (!ob || typeof ob !== "object") return ["obligation is not an object"];
|
|
52
|
+
|
|
53
|
+
if (!ob.key || typeof ob.key !== "string") errs.push(at("missing key"));
|
|
54
|
+
else if (!/^[a-z0-9][a-z0-9._-]*$/.test(ob.key)) errs.push(at(`key "${ob.key}" is not a stable slug ([a-z0-9._-])`));
|
|
55
|
+
|
|
56
|
+
if (!OBLIGATION_KINDS.includes(ob.kind)) errs.push(at(`kind must be one of ${OBLIGATION_KINDS.join("|")} (got ${JSON.stringify(ob.kind)})`));
|
|
57
|
+
|
|
58
|
+
if (!ob.source || typeof ob.source !== "object") errs.push(at("missing source provenance"));
|
|
59
|
+
else if (!ORIGINS.includes(ob.source.origin)) errs.push(at(`source.origin must be one of ${ORIGINS.join("|")}`));
|
|
60
|
+
|
|
61
|
+
if (!STATUSES.includes(ob.status)) errs.push(at(`status must be one of ${STATUSES.join("|")}`));
|
|
62
|
+
|
|
63
|
+
if (!Array.isArray(ob.uses)) errs.push(at("uses[] must be an array"));
|
|
64
|
+
if (!Array.isArray(ob.allowed_tools)) errs.push(at("allowed_tools[] must be an array"));
|
|
65
|
+
if (!Array.isArray(ob.action_classes)) errs.push(at("action_classes[] must be an array"));
|
|
66
|
+
else for (const c of ob.action_classes) if (!ACTION_CLASSES.includes(c)) errs.push(at(`unknown action class "${c}"`));
|
|
67
|
+
|
|
68
|
+
if (typeof ob.offline_safe !== "boolean") errs.push(at("offline_safe must be a boolean"));
|
|
69
|
+
if (ob.budget_cents_per_period != null && !Number.isFinite(ob.budget_cents_per_period)) errs.push(at("budget_cents_per_period must be a number"));
|
|
70
|
+
|
|
71
|
+
// Kind-specific structure.
|
|
72
|
+
if (ob.kind === "SCHEDULE") {
|
|
73
|
+
const hasInterval = ob.schedule && Number.isFinite(ob.schedule.interval);
|
|
74
|
+
const hasCalendar = ob.schedule && ob.schedule.calendar && typeof ob.schedule.calendar === "object";
|
|
75
|
+
if (!hasInterval && !hasCalendar) errs.push(at("SCHEDULE needs schedule.interval or schedule.calendar"));
|
|
76
|
+
if (!MODES.includes(ob.mode)) errs.push(at(`SCHEDULE needs a mode (${MODES.join("|")})`));
|
|
77
|
+
// The hole this closes: an archetype cadence used to get {mode, prompt} and a
|
|
78
|
+
// blanket-permission session. An empty allowed_tools list is legal ONLY for
|
|
79
|
+
// an inline (deterministic, no-session) cadence.
|
|
80
|
+
if (ob.mode !== "inline" && (!Array.isArray(ob.allowed_tools) || ob.allowed_tools.length === 0)) {
|
|
81
|
+
errs.push(at("a session-spawning SCHEDULE must declare a non-empty allowed_tools list"));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (ob.kind === "OUTCOME") {
|
|
85
|
+
if (!ob.objective_id) errs.push(at("OUTCOME must be provenance-linked to an objective_id"));
|
|
86
|
+
if (!ob.sensor || typeof ob.sensor !== "object") errs.push(at("OUTCOME must carry a sensor"));
|
|
87
|
+
else {
|
|
88
|
+
if (!SENSOR_SOURCES.includes(ob.sensor.source)) errs.push(at(`sensor.source must be one of ${SENSOR_SOURCES.join("|")}`));
|
|
89
|
+
if (ob.sensor.source === "llm") errs.push(at('sensor.source "llm" cannot back an OUTCOME obligation (advisory only)'));
|
|
90
|
+
if (ob.sensor.source === "method" && !ob.sensor.capability) errs.push(at('sensor.source "method" requires a capability'));
|
|
91
|
+
}
|
|
92
|
+
if (ob.target != null && !Number.isFinite(ob.target)) errs.push(at("target must be a number when set"));
|
|
93
|
+
}
|
|
94
|
+
if (ob.kind === "REACT") {
|
|
95
|
+
if (!ob.trigger || typeof ob.trigger !== "object" || !ob.trigger.topic) errs.push(at("REACT needs trigger.topic"));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The capability law.
|
|
99
|
+
if (o.reachable instanceof Set && Array.isArray(ob.uses)) {
|
|
100
|
+
for (const id of ob.uses) {
|
|
101
|
+
if (!o.reachable.has(id)) errs.push(at(`cites capability "${id}" which is not reachable:true in the capability manifest`));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return errs;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Validate a whole plan. Fail-closed: `ok:false` means DO NOT WRITE.
|
|
109
|
+
* @param {{obligations:object[]}} plan
|
|
110
|
+
* @param {{manifest?:object, reachable?:Set<string>}} [o]
|
|
111
|
+
* @returns {{ok:boolean, errors:string[], counts:object}}
|
|
112
|
+
*/
|
|
113
|
+
export function validatePlan(plan, o = {}) {
|
|
114
|
+
const errors = [];
|
|
115
|
+
const obligations = plan && Array.isArray(plan.obligations) ? plan.obligations : null;
|
|
116
|
+
if (!obligations) return { ok: false, errors: ["plan.obligations must be an array"], counts: {} };
|
|
117
|
+
|
|
118
|
+
const reachable = o.reachable instanceof Set
|
|
119
|
+
? o.reachable
|
|
120
|
+
: o.manifest
|
|
121
|
+
? new Set(((o.manifest.entries) || []).filter((e) => e && e.reachable).map((e) => e.id))
|
|
122
|
+
: null;
|
|
123
|
+
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
for (const ob of obligations) {
|
|
126
|
+
if (ob && ob.key) {
|
|
127
|
+
if (seen.has(ob.key)) errors.push(`duplicate obligation key "${ob.key}"`);
|
|
128
|
+
seen.add(ob.key);
|
|
129
|
+
}
|
|
130
|
+
errors.push(...validateObligation(ob, { reachable }));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const counts = {};
|
|
134
|
+
for (const k of OBLIGATION_KINDS) counts[k] = obligations.filter((ob) => ob && ob.kind === k).length;
|
|
135
|
+
counts.total = obligations.length;
|
|
136
|
+
|
|
137
|
+
return { ok: errors.length === 0, errors, counts };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default { OBLIGATION_KINDS, ORIGINS, MODES, ACTION_CLASSES, SENSOR_SOURCES, STATUSES, validateObligation, validatePlan };
|
|
@@ -169,11 +169,57 @@ export function _resetLiveCache() { _liveCache = { at: 0, value: null }; }
|
|
|
169
169
|
* Build the real-world deps snapshot the governor reasons over. Tests bypass
|
|
170
170
|
* this entirely by passing their own `deps`.
|
|
171
171
|
*/
|
|
172
|
+
// How long an availableMemoryBytes() probe is reused. vm_stat costs a few ms and
|
|
173
|
+
// the governor is consulted on every admission decision; memory pressure does not
|
|
174
|
+
// meaningfully move inside a 2s window.
|
|
175
|
+
const AVAIL_CACHE_MS = 2000;
|
|
176
|
+
let availCache = { at: 0, bytes: 0 };
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Bytes of memory the OS can actually hand to a new session.
|
|
180
|
+
*
|
|
181
|
+
* On Darwin `os.freemem()` reports ONLY vm_stat's "Pages free" — it excludes
|
|
182
|
+
* inactive, speculative and purgeable pages, every one of which macOS reclaims on
|
|
183
|
+
* demand. Any warm Mac therefore reports near-zero free memory forever (measured:
|
|
184
|
+
* 263MB "free" on a 24GB box that `memory_pressure` simultaneously called 76%
|
|
185
|
+
* free), so a percentage floor could never be satisfied and the governor deferred
|
|
186
|
+
* every spawn indefinitely. Counting the reclaimable pages is what macOS itself
|
|
187
|
+
* means by available memory.
|
|
188
|
+
*
|
|
189
|
+
* Falls back to `os.freemem()` on non-Darwin platforms and on any probe failure —
|
|
190
|
+
* a governor that cannot measure must not silently admit everything.
|
|
191
|
+
*
|
|
192
|
+
* @param {number} [now] injectable clock (tests)
|
|
193
|
+
* @returns {number} bytes
|
|
194
|
+
*/
|
|
195
|
+
export function availableMemoryBytes(now = Date.now()) {
|
|
196
|
+
if (process.platform !== "darwin") return os.freemem();
|
|
197
|
+
if (availCache.at && now - availCache.at < AVAIL_CACHE_MS) return availCache.bytes;
|
|
198
|
+
|
|
199
|
+
let bytes = os.freemem();
|
|
200
|
+
try {
|
|
201
|
+
const out = execFileSync("vm_stat", { encoding: "utf8", timeout: 2000 });
|
|
202
|
+
const pageSize = Number(/page size of (\d+) bytes/.exec(out)?.[1]) || 4096;
|
|
203
|
+
const pages = (label) => {
|
|
204
|
+
const m = new RegExp(`^${label}:\\s+(\\d+)\\.`, "m").exec(out);
|
|
205
|
+
return m ? Number(m[1]) : 0;
|
|
206
|
+
};
|
|
207
|
+
const reclaimable =
|
|
208
|
+
pages("Pages free") + pages("Pages inactive") + pages("Pages speculative") + pages("Pages purgeable");
|
|
209
|
+
if (reclaimable > 0) bytes = reclaimable * pageSize;
|
|
210
|
+
} catch {
|
|
211
|
+
// vm_stat missing or unparseable — keep the os.freemem() reading.
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
availCache = { at: now, bytes };
|
|
215
|
+
return bytes;
|
|
216
|
+
}
|
|
217
|
+
|
|
172
218
|
export function defaultDeps(extra = {}) {
|
|
173
219
|
const live = liveClaudeStats();
|
|
174
220
|
const throttle = readThrottle(extra);
|
|
175
221
|
return {
|
|
176
|
-
freemem:
|
|
222
|
+
freemem: availableMemoryBytes(),
|
|
177
223
|
totalmem: os.totalmem(),
|
|
178
224
|
loadavg: os.loadavg(),
|
|
179
225
|
cpus: os.cpus().length || 1,
|
|
@@ -11,7 +11,7 @@ import { test } from "node:test";
|
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
12
|
import { promises as fsp } from "node:fs";
|
|
13
13
|
import { writeFileSync } from "node:fs";
|
|
14
|
-
import { tmpdir } from "node:os";
|
|
14
|
+
import { tmpdir, freemem as osFreemem, totalmem as totalmemBytes } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
|
|
17
17
|
import {
|
|
@@ -21,8 +21,28 @@ import {
|
|
|
21
21
|
PER_SESSION_MB,
|
|
22
22
|
RAM_FRACTION,
|
|
23
23
|
DECISIONS,
|
|
24
|
+
availableMemoryBytes,
|
|
25
|
+
FREEMEM_FLOOR_FRACTION,
|
|
24
26
|
} from "./resource-governor.mjs";
|
|
25
27
|
|
|
28
|
+
test("availableMemoryBytes counts reclaimable pages, not just free ones", () => {
|
|
29
|
+
const bytes = availableMemoryBytes();
|
|
30
|
+
assert.ok(Number.isFinite(bytes) && bytes > 0, "always a usable number");
|
|
31
|
+
|
|
32
|
+
if (process.platform !== "darwin") {
|
|
33
|
+
assert.equal(bytes, osFreemem(), "non-Darwin defers to os.freemem()");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The regression: os.freemem() on macOS reports only "Pages free", which sits
|
|
38
|
+
// near zero on any warm box and pinned the governor at DEFER. Availability must
|
|
39
|
+
// include the inactive/speculative/purgeable pages macOS reclaims on demand,
|
|
40
|
+
// so it is always >= the raw reading and should clear the floor on an idle host.
|
|
41
|
+
assert.ok(bytes >= osFreemem(), "availability is never below raw free memory");
|
|
42
|
+
const floor = totalmemBytes() * FREEMEM_FLOOR_FRACTION;
|
|
43
|
+
assert.ok(bytes > floor, `an idle Mac must clear its own floor (${bytes} vs ${floor})`);
|
|
44
|
+
});
|
|
45
|
+
|
|
26
46
|
const GB = 1024 * 1024 * 1024;
|
|
27
47
|
const MB = 1024 * 1024;
|
|
28
48
|
|
|
@@ -40,15 +40,20 @@ import { isPlaceholder } from "./completeness.mjs";
|
|
|
40
40
|
// Archetype axes the identity section also offers. Keep these in sync with
|
|
41
41
|
// lib/setup/sections/identity.mjs FUNCTIONS/ALTITUDES — a derived value MUST be
|
|
42
42
|
// one the wizard's select() can render and the archetype resolver accepts.
|
|
43
|
+
// These are the seven archetypes that exist on disk. `people-leader`/`compliance-leader`
|
|
44
|
+
// were derivable here but resolve to no archetype file — a pull could therefore
|
|
45
|
+
// write an un-resolvable function into config/agent.json and make the identity
|
|
46
|
+
// section's apply() fatal. They map onto `operations-leader`/`compliance-officer`.
|
|
43
47
|
const FUNCTIONS = Object.freeze([
|
|
44
48
|
"executive-operator",
|
|
45
49
|
"technical-leader",
|
|
46
50
|
"commercial-leader",
|
|
47
|
-
"
|
|
51
|
+
"product-leader",
|
|
52
|
+
"operations-leader",
|
|
48
53
|
"finance-leader",
|
|
49
|
-
"compliance-
|
|
54
|
+
"compliance-officer",
|
|
50
55
|
]);
|
|
51
|
-
const ALTITUDES = Object.freeze(["c-suite", "svp", "vp", "senior-manager"]);
|
|
56
|
+
const ALTITUDES = Object.freeze(["founder", "c-suite", "svp", "vp", "senior-manager"]);
|
|
52
57
|
|
|
53
58
|
const DEFAULT_FUNCTION = "executive-operator";
|
|
54
59
|
const DEFAULT_ALTITUDE = "c-suite";
|
|
@@ -69,9 +74,50 @@ export function cohortPullEnv(env = process.env) {
|
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
function str(v) {
|
|
72
|
-
|
|
77
|
+
if (v == null) return "";
|
|
78
|
+
// NEVER stringify an object/array leaf. The live MemberProfileDTO nests
|
|
79
|
+
// role/team as objects; `String({})` yields the TRUTHY literal
|
|
80
|
+
// "[object Object]", which used to satisfy every `if (value)` guard below and
|
|
81
|
+
// then poison the keyword derivations — a silent fall-back to defaults rather
|
|
82
|
+
// than an honest empty. Nested shapes are read via the accessors below.
|
|
83
|
+
if (typeof v === "object") return "";
|
|
84
|
+
return String(v).trim();
|
|
73
85
|
}
|
|
74
86
|
|
|
87
|
+
/**
|
|
88
|
+
* The live Cohort MemberProfileDTO nests the fields the mapper needs:
|
|
89
|
+
* role: { title, level, status } team: { slug, name, shortLabel }
|
|
90
|
+
* Older/flattened payloads (and the wizard's own fixtures) carry them as plain
|
|
91
|
+
* strings. These accessors read BOTH shapes so a pull maps identically either
|
|
92
|
+
* way. Each returns "" when the profile genuinely carries nothing.
|
|
93
|
+
*/
|
|
94
|
+
function profileTitle(p) {
|
|
95
|
+
return str(p.title) || str(p.role?.title);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function profileRoleText(p) {
|
|
99
|
+
return str(p.role) || str(p.role?.title) || str(p.role?.level);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function profileTeamText(p) {
|
|
103
|
+
return str(p.team) || str(p.team?.name) || str(p.team?.shortLabel) || str(p.team?.slug);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The org's explicit seniority band, when it ships one (`role.level`). */
|
|
107
|
+
function profileLevel(p) {
|
|
108
|
+
return str(p.level) || str(p.role?.level);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Cohort's `role.level` enum → the archetype `altitude` axis. An EXPLICIT band
|
|
112
|
+
// from the org beats guessing at the title string, so this is consulted first.
|
|
113
|
+
const LEVEL_TO_ALTITUDE = Object.freeze({
|
|
114
|
+
FOUNDER: "founder",
|
|
115
|
+
C_SUITE: "c-suite",
|
|
116
|
+
SVP: "svp",
|
|
117
|
+
VP: "vp",
|
|
118
|
+
SENIOR_MANAGER: "senior-manager",
|
|
119
|
+
});
|
|
120
|
+
|
|
75
121
|
/**
|
|
76
122
|
* Map a Cohort MemberProfileDTO to the config/agent.json field subset it
|
|
77
123
|
* authoritatively provides. Only sets a field when the profile carries a real
|
|
@@ -88,6 +134,7 @@ function str(v) {
|
|
|
88
134
|
* operatingPrinciples ← profileSections (operating-principles section)
|
|
89
135
|
* communication.defaultTone ← profileSections (communication/tone section)
|
|
90
136
|
* responsibilities ← profileSections (responsibilities section)
|
|
137
|
+
* persona ← profileSections (persona/background/bio section)
|
|
91
138
|
*
|
|
92
139
|
* @param {object|null} profile MemberProfileDTO
|
|
93
140
|
* @returns {object} the config/agent.json patch (deep-mergeable)
|
|
@@ -104,7 +151,7 @@ export function mapProfileToAgentConfig(profile) {
|
|
|
104
151
|
const fullName = display || [first, last].filter(Boolean).join(" ").trim();
|
|
105
152
|
if (fullName) patch.fullName = fullName;
|
|
106
153
|
|
|
107
|
-
const title =
|
|
154
|
+
const title = profileTitle(p);
|
|
108
155
|
if (title) patch.title = title;
|
|
109
156
|
|
|
110
157
|
const email = str(p.agentEmail) || str(p.email);
|
|
@@ -115,7 +162,7 @@ export function mapProfileToAgentConfig(profile) {
|
|
|
115
162
|
// leaving the wizard to prompt). When signal IS present, derivation always
|
|
116
163
|
// resolves to a VALID axis (safe default when unmappable) so the wizard renders
|
|
117
164
|
// + confirms it rather than failing the pull.
|
|
118
|
-
const hasAxisSignal = !!(
|
|
165
|
+
const hasAxisSignal = !!(profileRoleText(p) || profileTeamText(p) || profileTitle(p) || profileLevel(p));
|
|
119
166
|
if (hasAxisSignal) {
|
|
120
167
|
patch.function = deriveFunction(p);
|
|
121
168
|
patch.altitude = deriveAltitude(p);
|
|
@@ -135,6 +182,12 @@ export function mapProfileToAgentConfig(profile) {
|
|
|
135
182
|
if (responsibilities.length) patch.responsibilities = responsibilities;
|
|
136
183
|
const tone = sectionText(sections, ["communication", "tone", "communication-style"]);
|
|
137
184
|
if (tone) patch.communication = { defaultTone: tone };
|
|
185
|
+
// persona/background — free prose describing who this member is. Rendered into
|
|
186
|
+
// the "Background" section of the prompt persona block (lib/identity/persona.mjs)
|
|
187
|
+
// so the agent writes with its own history behind it rather than as a generic
|
|
188
|
+
// seat-holder. The org is the SoT for this; we never synthesise one.
|
|
189
|
+
const persona = sectionText(sections, ["persona", "background", "bio", "about", "profile"]);
|
|
190
|
+
if (persona) patch.persona = persona;
|
|
138
191
|
|
|
139
192
|
// charter — the Cohort-authored operating charter (CharterDTO:
|
|
140
193
|
// approvedByLine/gatesText/sections). When present this is the SOURCE OF TRUTH
|
|
@@ -218,14 +271,17 @@ function mapSupervisor(sup) {
|
|
|
218
271
|
}
|
|
219
272
|
if (typeof sup === "object") {
|
|
220
273
|
const fullName = str(sup.displayName) || str(sup.fullName) || [str(sup.firstName), str(sup.lastName)].filter(Boolean).join(" ").trim();
|
|
221
|
-
|
|
274
|
+
// A supervisor DTO nests its title under `role`, exactly as the member does.
|
|
275
|
+
const title = profileTitle(sup);
|
|
276
|
+
const email = str(sup.email) || str(sup.agentEmail);
|
|
277
|
+
if (!fullName && !email && !title) return null;
|
|
222
278
|
const [pf, ...pr] = fullName.split(/\s+/);
|
|
223
279
|
return {
|
|
224
280
|
firstName: str(sup.firstName) || pf || "",
|
|
225
281
|
lastName: str(sup.lastName) || pr.join(" "),
|
|
226
282
|
fullName,
|
|
227
|
-
title
|
|
228
|
-
email
|
|
283
|
+
title,
|
|
284
|
+
email,
|
|
229
285
|
};
|
|
230
286
|
}
|
|
231
287
|
return null;
|
|
@@ -239,14 +295,16 @@ function mapSupervisor(sup) {
|
|
|
239
295
|
* @returns {string} one of FUNCTIONS
|
|
240
296
|
*/
|
|
241
297
|
export function deriveFunction(p) {
|
|
242
|
-
const hay = `${
|
|
298
|
+
const hay = `${profileRoleText(p)} ${profileTeamText(p)} ${profileTitle(p)}`.toLowerCase();
|
|
243
299
|
const rules = [
|
|
244
300
|
[/\b(eng|engineer|platform|infra|technical|cto|devops|software|sre)\b/, "technical-leader"],
|
|
301
|
+
[/\b(product|design|ux|research|pm|cpo)\b/, "product-leader"],
|
|
245
302
|
[/\b(sales|revenue|gtm|growth|commercial|marketing|bd|biz dev|partnerships)\b/, "commercial-leader"],
|
|
246
|
-
[/\b(people|hr|talent|recruit|org design|human resources)\b/, "
|
|
303
|
+
[/\b(people|hr|talent|recruit|org design|human resources)\b/, "operations-leader"],
|
|
247
304
|
[/\b(finance|fp&a|accounting|treasury|capital|corp dev|cfo)\b/, "finance-leader"],
|
|
248
|
-
[/\b(compliance|legal|risk|regulatory|counsel|audit|dfsa)\b/, "compliance-
|
|
249
|
-
[/\b(ops|operations|
|
|
305
|
+
[/\b(compliance|legal|risk|regulatory|counsel|audit|dfsa)\b/, "compliance-officer"],
|
|
306
|
+
[/\b(ops|operations|delivery|supply|logistics)\b/, "operations-leader"],
|
|
307
|
+
[/\b(chief of staff|coo|executive|operator)\b/, "executive-operator"],
|
|
250
308
|
];
|
|
251
309
|
for (const [re, fn] of rules) if (re.test(hay)) return fn;
|
|
252
310
|
return DEFAULT_FUNCTION;
|
|
@@ -259,9 +317,19 @@ export function deriveFunction(p) {
|
|
|
259
317
|
* @returns {string} one of ALTITUDES
|
|
260
318
|
*/
|
|
261
319
|
export function deriveAltitude(p) {
|
|
262
|
-
|
|
263
|
-
//
|
|
264
|
-
|
|
320
|
+
// An explicit `role.level` from the org is authoritative — prefer it over
|
|
321
|
+
// guessing at prose. (An SVP reporting to a CPTO reads as "chief" to the
|
|
322
|
+
// keyword pass below only because their manager's title is nearby; the band
|
|
323
|
+
// removes that whole class of error.)
|
|
324
|
+
const level = profileLevel(p).toUpperCase().replace(/[\s-]+/g, "_");
|
|
325
|
+
if (LEVEL_TO_ALTITUDE[level]) return LEVEL_TO_ALTITUDE[level];
|
|
326
|
+
|
|
327
|
+
const hay = `${profileRoleText(p)} ${profileTitle(p)}`.toLowerCase();
|
|
328
|
+
// Order matters: most-senior cue wins (check founder/c-suite/SVP before
|
|
329
|
+
// VP/manager). `founder` is its own altitude — it resolves the runway-review
|
|
330
|
+
// and investor-update cadences a generic c-suite seat does not get.
|
|
331
|
+
if (/\b(founder|co-?founder)\b/.test(hay)) return "founder";
|
|
332
|
+
if (/\b(c-?suite|chief|ceo|cto|cfo|coo|cmo|cpo)\b/.test(hay)) return "c-suite";
|
|
265
333
|
if (/\bsvp\b|senior vice president/.test(hay)) return "svp";
|
|
266
334
|
if (/\bvp\b|vice president|head of|director/.test(hay)) return "vp";
|
|
267
335
|
if (/\b(senior manager|sr\.? manager|manager|lead)\b/.test(hay)) return "senior-manager";
|
|
@@ -52,6 +52,46 @@ const PROFILE = {
|
|
|
52
52
|
],
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
// The shape the LIVE org-data API actually returns: role/team are objects, and
|
|
56
|
+
// the supervisor nests its title the same way. The flat PROFILE above is the
|
|
57
|
+
// legacy/wizard shape — both must map identically. Regression fixture for the
|
|
58
|
+
// enrolment that shipped an agent with title:"", altitude:"c-suite" and a
|
|
59
|
+
// principal with no title, because every field below was read one level too high.
|
|
60
|
+
const NESTED_PROFILE = {
|
|
61
|
+
slug: "A016",
|
|
62
|
+
kind: "AI_AGENT",
|
|
63
|
+
status: "ACTIVE",
|
|
64
|
+
displayName: "Agent One",
|
|
65
|
+
firstName: "Agent",
|
|
66
|
+
lastName: "One",
|
|
67
|
+
team: { slug: "ai-systems-agent-platform", name: "AI Systems & Agent Platform", shortLabel: "AI Systems" },
|
|
68
|
+
role: { title: "SVP AI Systems & Agent Platform", level: "SVP", status: "ACTIVE" },
|
|
69
|
+
agentEmail: "agent-one@example.test",
|
|
70
|
+
supervisor: {
|
|
71
|
+
slug: "A002",
|
|
72
|
+
displayName: "Samir Al Sulaiti",
|
|
73
|
+
role: { title: "Chief Product & Technology Officer", level: "C_SUITE", status: "ACTIVE" },
|
|
74
|
+
team: { slug: "product-technology-platform", name: "Product, Technology & Platform" },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
test("mapProfileToAgentConfig reads the nested (live) DTO shape", () => {
|
|
79
|
+
const patch = mapProfileToAgentConfig(NESTED_PROFILE);
|
|
80
|
+
assert.equal(patch.title, "SVP AI Systems & Agent Platform", "title comes from role.title");
|
|
81
|
+
assert.equal(patch.function, "technical-leader", "derived from the platform/AI-systems team");
|
|
82
|
+
assert.equal(patch.altitude, "svp", "role.level beats keyword-guessing the title");
|
|
83
|
+
assert.equal(patch.principal.fullName, "Samir Al Sulaiti");
|
|
84
|
+
assert.equal(patch.principal.title, "Chief Product & Technology Officer", "supervisor title comes from role.title");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("an object leaf never stringifies into a derivation", () => {
|
|
88
|
+
// `String({})` is the truthy "[object Object]"; letting it through made every
|
|
89
|
+
// derivation silently fall back to its default instead of reporting no signal.
|
|
90
|
+
assert.equal(mapProfileToAgentConfig({ role: {}, team: {} }).function, undefined, "no axis signal → no axis emitted");
|
|
91
|
+
assert.equal(deriveFunction({ role: { title: "Staff Engineer" } }), "technical-leader");
|
|
92
|
+
assert.equal(deriveAltitude({ role: { level: "SENIOR_MANAGER" } }), "senior-manager");
|
|
93
|
+
});
|
|
94
|
+
|
|
55
95
|
test("mapProfileToAgentConfig maps the authoritative fields", () => {
|
|
56
96
|
const patch = mapProfileToAgentConfig(PROFILE);
|
|
57
97
|
assert.equal(patch.firstName, "Robin");
|
|
@@ -130,7 +170,9 @@ test("charter/orgProfile absent → no charter/orgProfile keys (empty patch stay
|
|
|
130
170
|
test("deriveFunction + deriveAltitude resolve to valid axes (safe default)", () => {
|
|
131
171
|
assert.ok(FUNCTIONS.includes(deriveFunction({ role: "sales lead", team: "Revenue" })));
|
|
132
172
|
assert.equal(deriveFunction({ role: "sales lead", team: "Revenue" }), "commercial-leader");
|
|
133
|
-
|
|
173
|
+
// `compliance-leader` resolves to no archetype on disk; the rule map returns
|
|
174
|
+
// the archetype that does exist. The expectation, not the code, was stale.
|
|
175
|
+
assert.equal(deriveFunction({ role: "general counsel", team: "Legal" }), "compliance-officer");
|
|
134
176
|
assert.equal(deriveFunction({ role: "weird-unknown-role" }), "executive-operator");
|
|
135
177
|
assert.ok(ALTITUDES.includes(deriveAltitude({ title: "Chief Technology Officer" })));
|
|
136
178
|
assert.equal(deriveAltitude({ title: "Chief Technology Officer" }), "c-suite");
|
|
@@ -20,15 +20,26 @@ import { runGenerator } from "../run-generator.mjs";
|
|
|
20
20
|
import { cohortPullEnv, pullAndPlan } from "../enroll-from-cohort.mjs";
|
|
21
21
|
import { fetchSelfProfile } from "../../org/client.mjs";
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// The seven archetypes that ACTUALLY EXIST on disk (archetypes/*.yaml). This
|
|
24
|
+
// list used to offer `people-leader` and `compliance-leader`, neither of which
|
|
25
|
+
// has an archetype file — picking either resolved to nothing and made apply()
|
|
26
|
+
// fatal, and it omitted `operations-leader` and `product-leader`, which do
|
|
27
|
+
// exist. The whole init pipeline downstream (capability pack → cadences →
|
|
28
|
+
// plan compiler) keys off this value, so it must name a real archetype.
|
|
29
|
+
// Keep in sync with lib/setup/enroll-from-cohort.mjs FUNCTIONS.
|
|
30
|
+
export const FUNCTIONS = [
|
|
24
31
|
{ value: "executive-operator", label: "Executive operator (chief of staff / COO-style)" },
|
|
25
32
|
{ value: "technical-leader", label: "Technical leader (eng / platform / CTO-style)" },
|
|
26
33
|
{ value: "commercial-leader", label: "Commercial leader (sales / GTM / revenue)" },
|
|
27
|
-
{ value: "
|
|
34
|
+
{ value: "product-leader", label: "Product leader (product / design / research)" },
|
|
35
|
+
{ value: "operations-leader", label: "Operations leader (ops / people / delivery)" },
|
|
28
36
|
{ value: "finance-leader", label: "Finance leader (finance / corp dev / capital)" },
|
|
29
|
-
{ value: "compliance-
|
|
37
|
+
{ value: "compliance-officer", label: "Compliance officer (legal / risk / regulatory)" },
|
|
30
38
|
];
|
|
31
|
-
|
|
39
|
+
// `founder` carries its own ALTITUDE_CADENCES entry (runway review + investor
|
|
40
|
+
// update) in lib/cadences.mjs but was unreachable from the wizard.
|
|
41
|
+
export const ALTITUDES = [
|
|
42
|
+
{ value: "founder", label: "Founder" },
|
|
32
43
|
{ value: "c-suite", label: "C-suite" },
|
|
33
44
|
{ value: "svp", label: "SVP" },
|
|
34
45
|
{ value: "vp", label: "VP" },
|
|
@@ -138,3 +138,97 @@ test("pull is fail-open: a fetch error leaves config untouched", async () => {
|
|
|
138
138
|
);
|
|
139
139
|
assert.equal(readAgentJson(root).firstName, "UNCONFIGURED");
|
|
140
140
|
});
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// ARCHETYPE AXES (the fatal apply() regression).
|
|
144
|
+
//
|
|
145
|
+
// The wizard used to offer `people-leader` and `compliance-leader`, neither of
|
|
146
|
+
// which has an archetype file on disk. Picking either resolved to nothing and
|
|
147
|
+
// made apply() throw — and because the whole init pipeline downstream
|
|
148
|
+
// (capability pack -> cadences -> plan compiler) keys off this value, a bad axis
|
|
149
|
+
// poisons everything after it. These assert the invariant against the real
|
|
150
|
+
// filesystem, so adding an archetype without offering it (or offering one that
|
|
151
|
+
// does not exist) fails here rather than in a user's setup run.
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
import { FUNCTIONS, ALTITUDES } from "./identity.mjs";
|
|
155
|
+
import { listFunctions, listAltitudes, resolveArchetype } from "../../archetype.mjs";
|
|
156
|
+
import { FUNCTIONS as ENROLL_FUNCTIONS, ALTITUDES as ENROLL_ALTITUDES, deriveFunction, deriveAltitude } from "../enroll-from-cohort.mjs";
|
|
157
|
+
import { loadSections } from "../runner.mjs";
|
|
158
|
+
|
|
159
|
+
test("every function the wizard offers has an archetype file on disk", () => {
|
|
160
|
+
const onDisk = listFunctions();
|
|
161
|
+
for (const f of FUNCTIONS) {
|
|
162
|
+
assert.ok(onDisk.includes(f.value), `wizard offers "${f.value}" but archetypes/functions/${f.value}.yaml does not exist`);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("every archetype on disk is offered by the wizard (no orphaned archetype)", () => {
|
|
167
|
+
const offered = new Set(FUNCTIONS.map((f) => f.value));
|
|
168
|
+
for (const f of listFunctions()) {
|
|
169
|
+
assert.ok(offered.has(f), `archetypes/functions/${f}.yaml exists but the wizard never offers it`);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("every altitude the wizard offers has an archetype file, and vice versa", () => {
|
|
174
|
+
const onDisk = listAltitudes();
|
|
175
|
+
for (const a of ALTITUDES) assert.ok(onDisk.includes(a.value), `wizard offers altitude "${a.value}" with no archetypes/altitudes/${a.value}.yaml`);
|
|
176
|
+
const offered = new Set(ALTITUDES.map((a) => a.value));
|
|
177
|
+
for (const a of onDisk) assert.ok(offered.has(a), `archetypes/altitudes/${a}.yaml exists but the wizard never offers it`);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("`founder` is reachable from the wizard (it owns runway + investor cadences)", () => {
|
|
181
|
+
assert.ok(ALTITUDES.some((a) => a.value === "founder"), "founder altitude is offered");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("the pull path can only derive an axis the wizard can render", () => {
|
|
185
|
+
// enroll-from-cohort writes function/altitude straight into config/agent.json,
|
|
186
|
+
// so a value it can derive but the wizard cannot render is an un-resolvable seat.
|
|
187
|
+
const offeredFns = new Set(FUNCTIONS.map((f) => f.value));
|
|
188
|
+
for (const f of ENROLL_FUNCTIONS) assert.ok(offeredFns.has(f), `enroll can derive "${f}" which the wizard cannot render`);
|
|
189
|
+
const offeredAlts = new Set(ALTITUDES.map((a) => a.value));
|
|
190
|
+
for (const a of ENROLL_ALTITUDES) assert.ok(offeredAlts.has(a), `enroll can derive altitude "${a}" which the wizard cannot render`);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("the retired axes are gone from BOTH lists", () => {
|
|
194
|
+
for (const dead of ["people-leader", "compliance-leader"]) {
|
|
195
|
+
assert.ok(!FUNCTIONS.some((f) => f.value === dead), `wizard still offers the non-existent "${dead}"`);
|
|
196
|
+
assert.ok(!ENROLL_FUNCTIONS.includes(dead), `enroll can still derive the non-existent "${dead}"`);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("the roles that used to derive a dead axis now derive a real one", () => {
|
|
201
|
+
assert.equal(deriveFunction({ role: "Head of People", team: "HR" }), "operations-leader");
|
|
202
|
+
assert.equal(deriveFunction({ role: "general counsel", team: "Legal" }), "compliance-officer");
|
|
203
|
+
assert.equal(deriveFunction({ role: "Head of Product", team: "Product" }), "product-leader");
|
|
204
|
+
assert.equal(deriveAltitude({ title: "Co-Founder & CEO" }), "founder");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("every offered (function, altitude) pair actually resolves to an archetype", async () => {
|
|
208
|
+
// The real end-to-end guarantee: no combination the wizard can produce throws.
|
|
209
|
+
for (const f of FUNCTIONS) {
|
|
210
|
+
for (const a of ALTITUDES) {
|
|
211
|
+
const arch = await resolveArchetype({ function: f.value, altitude: a.value });
|
|
212
|
+
assert.ok(arch && typeof arch === "object", `resolveArchetype failed for ${f.value}/${a.value}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("every derivable pull result resolves to an archetype too", async () => {
|
|
218
|
+
for (const f of ENROLL_FUNCTIONS) {
|
|
219
|
+
for (const a of ENROLL_ALTITUDES) {
|
|
220
|
+
const arch = await resolveArchetype({ function: f, altitude: a });
|
|
221
|
+
assert.ok(arch && typeof arch === "object", `resolveArchetype failed for pulled ${f}/${a}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("the identity section is registered by the directory glob at order 10", async () => {
|
|
227
|
+
const sections = await loadSections();
|
|
228
|
+
const s = sections.find((x) => x.id === "identity");
|
|
229
|
+
assert.ok(s, "identity is discovered by loadSections()");
|
|
230
|
+
assert.equal(s.order, 10);
|
|
231
|
+
assert.equal(s.order, identity.order);
|
|
232
|
+
// It must run before the capability inventory, which reads its archetype.
|
|
233
|
+
assert.ok(s.order < sections.find((x) => x.id === "inventory").order);
|
|
234
|
+
});
|