@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,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/plan/compile.mjs — the PLAN COMPILER (SPEC §5.6).
|
|
3
|
+
*
|
|
4
|
+
* `(mandate, capabilityManifest, archetypeProfile, orgContext) → obligations[]`
|
|
5
|
+
*
|
|
6
|
+
* This is the load-bearing function of the whole design and it is deliberately
|
|
7
|
+
* BORING: a pure, deterministic, offline reduction over structured JSON. Not an
|
|
8
|
+
* LLM pass over prose (that makes `obligationsHash` non-deterministic and
|
|
9
|
+
* manufactures phantom drift), and not a server planning service (that makes a
|
|
10
|
+
* partitioned agent go dark). Same inputs → byte-identical output, forever.
|
|
11
|
+
*
|
|
12
|
+
* Emission rules, in order:
|
|
13
|
+
* 1. the 19 STANDARD_CADENCES → SCHEDULE, origin "standard",
|
|
14
|
+
* triggers copied VERBATIM so existing
|
|
15
|
+
* launchd plists do not churn
|
|
16
|
+
* 2. archetype recurring workflows
|
|
17
|
+
* + ALTITUDE_CADENCES → SCHEDULE, origin "archetype"
|
|
18
|
+
* 3. each ADOPTED objective w/ sensor → SCHEDULE `schedule.measure.<key>`
|
|
19
|
+
* + OUTCOME `outcome.<key>`
|
|
20
|
+
* 4. each adopted objective with a
|
|
21
|
+
* REVIEWER collaborator → SCHEDULE `schedule.review.<key>`
|
|
22
|
+
* 5. mandate `reactsTo[]` + the four
|
|
23
|
+
* standard directTo sources → REACT
|
|
24
|
+
*
|
|
25
|
+
* Then, for every obligation: `uses[]` is resolved against the capability
|
|
26
|
+
* manifest and `allowed_tools[]` is COMPUTED from `uses[]`. That is what closes
|
|
27
|
+
* the "archetype cadences get {mode, prompt} and a --dangerously-skip-permissions
|
|
28
|
+
* session" hole — a plan-derived cadence gets a tool scope by construction.
|
|
29
|
+
*
|
|
30
|
+
* PROPOSED objectives compile NOTHING. Until a human adopts in hq, the agent runs
|
|
31
|
+
* exactly today's standard + archetype cadences, which is why this ships
|
|
32
|
+
* incrementally instead of lockstep.
|
|
33
|
+
*
|
|
34
|
+
* Nothing here reads the filesystem, the clock, the network, or Math.random.
|
|
35
|
+
*
|
|
36
|
+
* @module lib/plan/compile
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
"use strict";
|
|
40
|
+
|
|
41
|
+
import { createHash } from "node:crypto";
|
|
42
|
+
import { canonical } from "../mandate/cache.mjs";
|
|
43
|
+
|
|
44
|
+
/** Bump when emission rules change — it is part of `inputsHash`. */
|
|
45
|
+
export const COMPILER_VERSION = 1;
|
|
46
|
+
|
|
47
|
+
/** Read primitives every session-spawning obligation may use. */
|
|
48
|
+
export const BASELINE_TOOLS = Object.freeze(["Read", "Grep", "Glob", "LS"]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Capabilities each STANDARD cadence genuinely needs. Single-sourced here so the
|
|
52
|
+
* emitted `allowedTools` is auditable in one place instead of drifting across
|
|
53
|
+
* the registry. Entries absent from this table (the deterministic inline
|
|
54
|
+
* cadences) legitimately need no tools and spawn no session.
|
|
55
|
+
*/
|
|
56
|
+
export const STANDARD_CADENCE_USES = Object.freeze({
|
|
57
|
+
"inbox-processor": ["email_inbox", "email_triage", "messaging_history", "task_create"],
|
|
58
|
+
"backlog-executor": ["board_ready", "board_claim", "board_complete", "task_update"],
|
|
59
|
+
"meeting-prep": ["calendar_list", "calendar_get", "knowledge_search"],
|
|
60
|
+
"meeting-action-capture": ["meetings_recap_file", "task_create"],
|
|
61
|
+
"daily-morning-brief": ["calendar_list", "email_inbox", "board_ready"],
|
|
62
|
+
"daily-midday-sweep": ["board_ready", "messaging_history"],
|
|
63
|
+
"daily-evening-wrap": ["board_ready", "task_update", "knowledge_append"],
|
|
64
|
+
"quarterly-self-assessment": ["knowledge_search", "memory_author"],
|
|
65
|
+
"org-pulse": ["org_snapshot", "org_events_tail"],
|
|
66
|
+
"dynamic-jobs": ["task_create", "task_update"],
|
|
67
|
+
"messaging-inbound": ["messaging_channels", "messaging_history", "messaging_send"],
|
|
68
|
+
"directory-hygiene": [
|
|
69
|
+
"directory_search", "directory_list_people", "directory_list_captures",
|
|
70
|
+
"directory_list_merges", "directory_propose_capture", "directory_propose_merge",
|
|
71
|
+
],
|
|
72
|
+
"brand-steward": ["design_foundation", "design_list_templates"],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The four EXISTING `directTo` sources the SSE lane already classifies. These
|
|
77
|
+
* become REACT obligations for every agent so a directed event always has a
|
|
78
|
+
* matching obligation (an unmatched T0/T1 event is drift, not routine).
|
|
79
|
+
*/
|
|
80
|
+
export const STANDARD_REACTS = Object.freeze([
|
|
81
|
+
{ key: "react.email.received", topic: "email", kind: "received", uses: ["email_message", "email_thread", "email_send"], offline_safe: false, mode: "escalate" },
|
|
82
|
+
{ key: "react.handoff.offered", topic: "handoff", kind: "offered", uses: ["board_ready", "board_claim"], offline_safe: true, mode: "escalate" },
|
|
83
|
+
{ key: "react.task.assigned", topic: "task", kind: "assigned", uses: ["board_ready", "board_claim", "task_update"], offline_safe: true, mode: "escalate" },
|
|
84
|
+
{ key: "react.approval.requested", topic: "approval", kind: "requested", uses: ["approval_wait", "decision_list"], offline_safe: true, mode: "escalate" },
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
/** Deterministic tie-break rank for an obligation key. */
|
|
88
|
+
export function rankOf(key) {
|
|
89
|
+
return createHash("sha256").update(String(key)).digest("hex");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** sha256 of a canonical value. */
|
|
93
|
+
function sha(value) {
|
|
94
|
+
return createHash("sha256").update(typeof value === "string" ? value : canonical(value)).digest("hex");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Map a capability id onto the session tool name the `--allowedTools` flag and
|
|
99
|
+
* the cadence registry understand.
|
|
100
|
+
* @param {string} id @returns {string|null}
|
|
101
|
+
*/
|
|
102
|
+
export function toolNameFor(id) {
|
|
103
|
+
const s = String(id || "");
|
|
104
|
+
if (s.startsWith("builtin:")) return s.slice("builtin:".length);
|
|
105
|
+
if (s.startsWith("skill:")) return "Skill";
|
|
106
|
+
if (s.startsWith("workflow:") || s.startsWith("subagent:")) return "Task";
|
|
107
|
+
if (s.startsWith("mcp:")) return `mcp__${s.slice("mcp:".length)}__*`;
|
|
108
|
+
return s || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** allowed_tools[] computed from uses[] — never hand-listed. */
|
|
112
|
+
export function allowedToolsFor(uses, { baseline = BASELINE_TOOLS } = {}) {
|
|
113
|
+
const out = new Set(baseline);
|
|
114
|
+
for (const u of uses || []) {
|
|
115
|
+
const t = toolNameFor(u);
|
|
116
|
+
if (t) out.add(t);
|
|
117
|
+
}
|
|
118
|
+
return [...out].sort();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Union of the blast-radius classes of the cited capabilities. */
|
|
122
|
+
export function actionClassesFor(uses, manifest) {
|
|
123
|
+
const byId = new Map((((manifest && manifest.entries) || [])).map((e) => [e.id, e]));
|
|
124
|
+
const out = new Set();
|
|
125
|
+
for (const u of uses || []) {
|
|
126
|
+
const e = byId.get(u);
|
|
127
|
+
if (e && e.blastRadius && e.blastRadius !== "internal") out.add(e.blastRadius);
|
|
128
|
+
}
|
|
129
|
+
return [...out].sort();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Deterministic slot solver
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
/** Candidate slot spaces per review rhythm. Deterministic + ordered. */
|
|
137
|
+
function candidates(cadence) {
|
|
138
|
+
const out = [];
|
|
139
|
+
if (cadence === "weekly") {
|
|
140
|
+
for (let wd = 1; wd <= 5; wd++) for (let h = 9; h <= 16; h++) for (const m of [0, 30]) out.push({ weekday: wd, hour: h, minute: m });
|
|
141
|
+
} else if (cadence === "monthly") {
|
|
142
|
+
for (let d = 1; d <= 5; d++) for (let h = 9; h <= 15; h++) out.push({ day: d, hour: h, minute: 0 });
|
|
143
|
+
} else if (cadence === "quarterly") {
|
|
144
|
+
for (let h = 9; h <= 15; h++) out.push({ day: 1, hour: h, minute: 0 });
|
|
145
|
+
} else {
|
|
146
|
+
for (let h = 8; h <= 17; h++) for (const m of [0, 30]) out.push({ hour: h, minute: m });
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A slot's identity for concurrency accounting. */
|
|
152
|
+
function slotKey(c) {
|
|
153
|
+
return `${c.weekday ?? "-"}:${c.day ?? "-"}:${c.hour}:${c.minute}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Pick a calendar slot deterministically: start from an offset derived from
|
|
158
|
+
* sha256(key), scan the candidate space in order, skip slots the principal is
|
|
159
|
+
* busy in and slots already at the concurrency cap. Same inputs → same schedule;
|
|
160
|
+
* a different mandate (different keys) → a different schedule. This REPLACES the
|
|
161
|
+
* old `slotOf(id)` hash-stagger, which knew nothing about the calendar.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} key
|
|
164
|
+
* @param {string} cadence weekly|monthly|quarterly|daily
|
|
165
|
+
* @param {{busy?:Set<string>, taken?:Map<string,number>, concurrency?:number}} state
|
|
166
|
+
* @returns {{calendar:object, collided:boolean}}
|
|
167
|
+
*/
|
|
168
|
+
export function solveSlot(key, cadence, state = {}) {
|
|
169
|
+
const busy = state.busy instanceof Set ? state.busy : new Set();
|
|
170
|
+
const taken = state.taken instanceof Map ? state.taken : new Map();
|
|
171
|
+
const cap = Number.isFinite(state.concurrency) ? state.concurrency : 2;
|
|
172
|
+
|
|
173
|
+
const space = candidates(cadence);
|
|
174
|
+
const start = parseInt(rankOf(key).slice(0, 8), 16) % space.length;
|
|
175
|
+
let fallback = null;
|
|
176
|
+
for (let i = 0; i < space.length; i++) {
|
|
177
|
+
const c = space[(start + i) % space.length];
|
|
178
|
+
const sk = slotKey(c);
|
|
179
|
+
if (fallback === null) fallback = c;
|
|
180
|
+
if (busy.has(`${c.weekday ?? "*"}:${c.hour}`)) continue;
|
|
181
|
+
if ((taken.get(sk) || 0) >= cap) continue;
|
|
182
|
+
taken.set(sk, (taken.get(sk) || 0) + 1);
|
|
183
|
+
return { calendar: c, collided: i !== 0 };
|
|
184
|
+
}
|
|
185
|
+
// Every slot is busy or capped — take the deterministic fallback and let the
|
|
186
|
+
// caller record the drift rather than silently dropping the obligation.
|
|
187
|
+
const sk = slotKey(fallback);
|
|
188
|
+
taken.set(sk, (taken.get(sk) || 0) + 1);
|
|
189
|
+
return { calendar: fallback, collided: true, saturated: true };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Busy windows from the principal's calendar events (weekday:hour keys). */
|
|
193
|
+
export function busyWindows(events = []) {
|
|
194
|
+
const out = new Set();
|
|
195
|
+
for (const e of events || []) {
|
|
196
|
+
if (!e) continue;
|
|
197
|
+
const wd = Number.isFinite(e.weekday) ? e.weekday : null;
|
|
198
|
+
const h = Number.isFinite(e.hour) ? e.hour : null;
|
|
199
|
+
if (h === null) continue;
|
|
200
|
+
out.add(`${wd ?? "*"}:${h}`);
|
|
201
|
+
if (wd !== null) out.add(`*:${h}`);
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// The compiler
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Compile the plan.
|
|
212
|
+
*
|
|
213
|
+
* @param {object} input
|
|
214
|
+
* @param {object} input.mandate the cached mandate BODY ({objectives, budgetCentsPerPeriod, reactsTo, collaborators})
|
|
215
|
+
* @param {number} [input.mandateVersion]
|
|
216
|
+
* @param {object} input.manifest the capability manifest
|
|
217
|
+
* @param {object} [input.profile] resolved archetype profile (for the hash + altitude)
|
|
218
|
+
* @param {object} [input.orgContext] org-context.json (strategy is part of the hash)
|
|
219
|
+
* @param {object[]} input.standardCadences lib/cadences.STANDARD_CADENCES
|
|
220
|
+
* @param {object[]} [input.archetypeCadences] resolved function+altitude cadences (scope !== "standard")
|
|
221
|
+
* @param {object[]} [input.calendarEvents] [{weekday,hour}] busy windows
|
|
222
|
+
* @param {number} [input.concurrency=2]
|
|
223
|
+
* @returns {{obligations:object[], inputsHash:string, obligationsHash:string, compilerVersion:number, mandateVersion:number, drift:object[], counts:object}}
|
|
224
|
+
*/
|
|
225
|
+
export function compilePlan(input = {}) {
|
|
226
|
+
const mandate = input.mandate && typeof input.mandate === "object" ? input.mandate : {};
|
|
227
|
+
const manifest = input.manifest || { entries: [] };
|
|
228
|
+
const reachable = new Set(((manifest.entries) || []).filter((e) => e && e.reachable).map((e) => e.id));
|
|
229
|
+
const standardCadences = Array.isArray(input.standardCadences) ? input.standardCadences : [];
|
|
230
|
+
const archetypeCadences = Array.isArray(input.archetypeCadences) ? input.archetypeCadences : [];
|
|
231
|
+
const drift = [];
|
|
232
|
+
const obligations = [];
|
|
233
|
+
|
|
234
|
+
const defaultBudget = Number.isFinite(mandate.budgetCentsPerPeriod) ? mandate.budgetCentsPerPeriod : 500;
|
|
235
|
+
const seatEnvelope = Number.isFinite(input.seatBudgetCents) ? input.seatBudgetCents : defaultBudget * 20;
|
|
236
|
+
|
|
237
|
+
/** Keep only reachable capabilities; record the rest as drift. */
|
|
238
|
+
const resolveUses = (wanted, obKey) => {
|
|
239
|
+
const kept = [];
|
|
240
|
+
for (const id of wanted || []) {
|
|
241
|
+
if (reachable.has(id)) kept.push(id);
|
|
242
|
+
else drift.push({ kind: "unreachable_capability", key: obKey, detail: { capability: id } });
|
|
243
|
+
}
|
|
244
|
+
return kept;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const push = (ob) => {
|
|
248
|
+
ob.allowed_tools = ob.mode === "inline" ? [] : allowedToolsFor(ob.uses);
|
|
249
|
+
ob.action_classes = actionClassesFor(ob.uses, manifest);
|
|
250
|
+
obligations.push(ob);
|
|
251
|
+
return ob;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// ── 1. standard cadences — triggers copied VERBATIM ─────────────────────
|
|
255
|
+
for (const c of standardCadences) {
|
|
256
|
+
if (!c || !c.id) continue;
|
|
257
|
+
const key = `schedule.${c.id}`;
|
|
258
|
+
const uses = resolveUses(STANDARD_CADENCE_USES[c.id] || [], key);
|
|
259
|
+
push({
|
|
260
|
+
key,
|
|
261
|
+
kind: "SCHEDULE",
|
|
262
|
+
cadence_id: c.id,
|
|
263
|
+
scope: "standard",
|
|
264
|
+
source: { origin: "standard", ref: c.id },
|
|
265
|
+
mode: c.mode || "escalate",
|
|
266
|
+
prompt: c.prompt || null,
|
|
267
|
+
guard_module: null,
|
|
268
|
+
schedule: c.interval != null ? { interval: c.interval } : { calendar: { ...c.calendar } },
|
|
269
|
+
uses,
|
|
270
|
+
budget_cents_per_period: defaultBudget,
|
|
271
|
+
offline_safe: c.mode === "inline",
|
|
272
|
+
status: "active",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── 2. archetype + altitude cadences ────────────────────────────────────
|
|
277
|
+
for (const c of archetypeCadences) {
|
|
278
|
+
if (!c || !c.id) continue;
|
|
279
|
+
if (c.scope === "standard") continue; // already emitted verbatim above
|
|
280
|
+
const key = `schedule.${c.id}`;
|
|
281
|
+
if (obligations.some((o) => o.key === key)) continue;
|
|
282
|
+
const uses = resolveUses(usesForArchetypeCadence(c, reachable), key);
|
|
283
|
+
push({
|
|
284
|
+
key,
|
|
285
|
+
kind: "SCHEDULE",
|
|
286
|
+
cadence_id: c.id,
|
|
287
|
+
scope: c.scope || "archetype",
|
|
288
|
+
source: { origin: "archetype", ref: c.id },
|
|
289
|
+
mode: c.mode || "escalate",
|
|
290
|
+
prompt: c.prompt || `schedules/triggers/${c.id}.md`,
|
|
291
|
+
guard_module: null,
|
|
292
|
+
schedule: c.interval != null ? { interval: c.interval } : { calendar: { ...c.calendar } },
|
|
293
|
+
uses,
|
|
294
|
+
cadence: c.cadence || null,
|
|
295
|
+
purpose: c.purpose || "",
|
|
296
|
+
budget_cents_per_period: defaultBudget,
|
|
297
|
+
offline_safe: false,
|
|
298
|
+
status: "active",
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── 3/4. adopted objectives → measure + outcome (+ review) ──────────────
|
|
303
|
+
const adopted = (Array.isArray(mandate.objectives) ? mandate.objectives : [])
|
|
304
|
+
.filter((o) => o && o.state === "active" && o.kind !== "PILLAR");
|
|
305
|
+
const proposedCount = (Array.isArray(mandate.objectives) ? mandate.objectives : []).filter((o) => o && o.state === "proposed").length;
|
|
306
|
+
|
|
307
|
+
// Budget envelope: highest-weight objectives get the envelope first; the rest
|
|
308
|
+
// are emitted SUSPENDED with an explicit breach record. Never a silent drop.
|
|
309
|
+
const ordered = adopted.slice().sort((a, b) => (b.weight || 1) - (a.weight || 1) || rankOf(a.key).localeCompare(rankOf(b.key)));
|
|
310
|
+
const slotState = { busy: busyWindows(input.calendarEvents), taken: new Map(), concurrency: input.concurrency };
|
|
311
|
+
let spent = 0;
|
|
312
|
+
|
|
313
|
+
for (const obj of ordered) {
|
|
314
|
+
const okey = String(obj.key || "");
|
|
315
|
+
if (!okey) { drift.push({ kind: "schedule_missing", key: null, detail: { reason: "adopted objective has no key" } }); continue; }
|
|
316
|
+
|
|
317
|
+
const sensorOk = obj.sensor && obj.sensor.source === "method" && obj.sensor.capability && reachable.has(obj.sensor.capability);
|
|
318
|
+
if (!sensorOk) {
|
|
319
|
+
drift.push({
|
|
320
|
+
kind: obj.sensor && obj.sensor.capability ? "unreachable_capability" : "stale_sensor",
|
|
321
|
+
key: `outcome.${okey}`,
|
|
322
|
+
detail: { objectiveKey: okey, sensor: (obj.sensor && obj.sensor.capability) || null, source: (obj.sensor && obj.sensor.source) || "none" },
|
|
323
|
+
});
|
|
324
|
+
continue; // an OUTCOME without a reachable method sensor is not emitted
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const budget = Number.isFinite(obj.budgetCentsPerPeriod) ? obj.budgetCentsPerPeriod : defaultBudget;
|
|
328
|
+
const withinEnvelope = spent + budget <= seatEnvelope;
|
|
329
|
+
if (!withinEnvelope) drift.push({ kind: "budget_breach", key: `outcome.${okey}`, detail: { seatEnvelope, spent, requested: budget } });
|
|
330
|
+
else spent += budget;
|
|
331
|
+
const status = withinEnvelope ? "active" : "suspended";
|
|
332
|
+
|
|
333
|
+
const measureKey = `schedule.measure.${okey}`;
|
|
334
|
+
const measureUses = resolveUses([obj.sensor.capability], measureKey);
|
|
335
|
+
const slot = solveSlot(measureKey, obj.cadence || "weekly", slotState);
|
|
336
|
+
if (slot.saturated) drift.push({ kind: "schedule_missing", key: measureKey, detail: { reason: "no free calendar slot; placed deterministically anyway" } });
|
|
337
|
+
push({
|
|
338
|
+
key: measureKey,
|
|
339
|
+
kind: "SCHEDULE",
|
|
340
|
+
cadence_id: `measure-${okey}`,
|
|
341
|
+
scope: "mandate",
|
|
342
|
+
source: { origin: "mandate", ref: obj.id || okey, charter_section: obj.charterSectionId || null },
|
|
343
|
+
objective_id: obj.id || okey,
|
|
344
|
+
mode: "guarded",
|
|
345
|
+
prompt: `schedules/triggers/measure-${okey}.md`,
|
|
346
|
+
guard_module: "lib/goals/loop.mjs",
|
|
347
|
+
schedule: { calendar: slot.calendar },
|
|
348
|
+
cadence: obj.cadence || "weekly",
|
|
349
|
+
uses: measureUses,
|
|
350
|
+
budget_cents_per_period: budget,
|
|
351
|
+
offline_safe: true,
|
|
352
|
+
status,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
push({
|
|
356
|
+
key: `outcome.${okey}`,
|
|
357
|
+
kind: "OUTCOME",
|
|
358
|
+
scope: "mandate",
|
|
359
|
+
source: { origin: "mandate", ref: obj.id || okey, charter_section: obj.charterSectionId || null },
|
|
360
|
+
objective_id: obj.id || okey,
|
|
361
|
+
metric: obj.metric || okey.replace(/-/g, "_"),
|
|
362
|
+
target: Number.isFinite(obj.target) ? obj.target : null,
|
|
363
|
+
direction: obj.direction || "up",
|
|
364
|
+
tolerance: Number.isFinite(obj.tolerance) ? obj.tolerance : 0,
|
|
365
|
+
cadence: obj.cadence || "weekly",
|
|
366
|
+
sensor: { capability: obj.sensor.capability, params: obj.sensor.params || {}, source: "method" },
|
|
367
|
+
collaborators: Array.isArray(obj.collaborators) ? obj.collaborators : [],
|
|
368
|
+
mode: "guarded",
|
|
369
|
+
uses: measureUses,
|
|
370
|
+
budget_cents_per_period: budget,
|
|
371
|
+
offline_safe: true,
|
|
372
|
+
status,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const reviewers = (Array.isArray(obj.collaborators) ? obj.collaborators : []).filter((c) => c && String(c.role).toUpperCase() === "REVIEWER");
|
|
376
|
+
if (reviewers.length) {
|
|
377
|
+
const reviewKey = `schedule.review.${okey}`;
|
|
378
|
+
const reviewUses = resolveUses(["board_ready", "messaging_send"], reviewKey);
|
|
379
|
+
const rslot = solveSlot(reviewKey, obj.cadence || "weekly", slotState);
|
|
380
|
+
push({
|
|
381
|
+
key: reviewKey,
|
|
382
|
+
kind: "SCHEDULE",
|
|
383
|
+
cadence_id: `review-${okey}`,
|
|
384
|
+
scope: "mandate",
|
|
385
|
+
source: { origin: "mandate", ref: obj.id || okey, charter_section: obj.charterSectionId || null },
|
|
386
|
+
objective_id: obj.id || okey,
|
|
387
|
+
mode: "escalate",
|
|
388
|
+
prompt: `schedules/triggers/review-${okey}.md`,
|
|
389
|
+
guard_module: null,
|
|
390
|
+
schedule: { calendar: rslot.calendar },
|
|
391
|
+
cadence: obj.cadence || "weekly",
|
|
392
|
+
collaborators: reviewers,
|
|
393
|
+
uses: reviewUses,
|
|
394
|
+
budget_cents_per_period: budget,
|
|
395
|
+
offline_safe: false,
|
|
396
|
+
status,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── 5. REACT obligations ────────────────────────────────────────────────
|
|
402
|
+
const reacts = [
|
|
403
|
+
...STANDARD_REACTS.map((r) => ({ ...r, origin: "react-standard" })),
|
|
404
|
+
...(Array.isArray(mandate.reactsTo) ? mandate.reactsTo : []).map((r) => ({
|
|
405
|
+
key: r && r.key ? String(r.key) : `react.${(r && r.topic) || "unknown"}.${(r && r.kind) || "any"}`,
|
|
406
|
+
topic: r && r.topic,
|
|
407
|
+
kind: r && r.kind,
|
|
408
|
+
predicate: r && r.predicate,
|
|
409
|
+
uses: (r && r.uses) || [],
|
|
410
|
+
offline_safe: !!(r && r.offlineSafe),
|
|
411
|
+
mode: (r && r.mode) || "escalate",
|
|
412
|
+
origin: "mandate",
|
|
413
|
+
objective_id: r && r.objectiveId,
|
|
414
|
+
})),
|
|
415
|
+
];
|
|
416
|
+
for (const r of reacts) {
|
|
417
|
+
if (!r.topic) continue;
|
|
418
|
+
if (obligations.some((o) => o.key === r.key)) continue;
|
|
419
|
+
const uses = resolveUses(r.uses, r.key);
|
|
420
|
+
push({
|
|
421
|
+
key: r.key,
|
|
422
|
+
kind: "REACT",
|
|
423
|
+
scope: r.origin === "mandate" ? "mandate" : "standard",
|
|
424
|
+
source: { origin: r.origin, ref: `${r.topic}.${r.kind || "*"}` },
|
|
425
|
+
objective_id: r.objective_id || null,
|
|
426
|
+
trigger: { topic: r.topic, kind: r.kind || null, predicate: r.predicate || null },
|
|
427
|
+
mode: r.mode,
|
|
428
|
+
uses,
|
|
429
|
+
budget_cents_per_period: defaultBudget,
|
|
430
|
+
offline_safe: !!r.offline_safe,
|
|
431
|
+
status: "active",
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Stable output order: kind rank, then key.
|
|
436
|
+
const kindRank = { SCHEDULE: 0, OUTCOME: 1, REACT: 2 };
|
|
437
|
+
obligations.sort((a, b) => (kindRank[a.kind] - kindRank[b.kind]) || a.key.localeCompare(b.key));
|
|
438
|
+
|
|
439
|
+
const inputsHash = sha([
|
|
440
|
+
canonical(mandate),
|
|
441
|
+
canonical(stripVolatile(manifest)),
|
|
442
|
+
sha(canonical(input.profile || null)),
|
|
443
|
+
canonical((input.orgContext && input.orgContext.strategy) || null),
|
|
444
|
+
`compiler:${COMPILER_VERSION}`,
|
|
445
|
+
].join(""));
|
|
446
|
+
|
|
447
|
+
const obligationsHash = sha(canonical(obligations.map(identityOf)));
|
|
448
|
+
|
|
449
|
+
const counts = {
|
|
450
|
+
SCHEDULE: obligations.filter((o) => o.kind === "SCHEDULE").length,
|
|
451
|
+
OUTCOME: obligations.filter((o) => o.kind === "OUTCOME").length,
|
|
452
|
+
REACT: obligations.filter((o) => o.kind === "REACT").length,
|
|
453
|
+
total: obligations.length,
|
|
454
|
+
adoptedObjectives: adopted.length,
|
|
455
|
+
proposedObjectives: proposedCount,
|
|
456
|
+
suspended: obligations.filter((o) => o.status !== "active").length,
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
obligations,
|
|
461
|
+
inputsHash,
|
|
462
|
+
obligationsHash,
|
|
463
|
+
compilerVersion: COMPILER_VERSION,
|
|
464
|
+
mandateVersion: Number.isFinite(input.mandateVersion) ? input.mandateVersion : 0,
|
|
465
|
+
drift,
|
|
466
|
+
counts,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** The identity-bearing projection an obligationsHash is taken over. */
|
|
471
|
+
function identityOf(ob) {
|
|
472
|
+
return {
|
|
473
|
+
key: ob.key,
|
|
474
|
+
kind: ob.kind,
|
|
475
|
+
status: ob.status,
|
|
476
|
+
schedule: ob.schedule || null,
|
|
477
|
+
trigger: ob.trigger || null,
|
|
478
|
+
objective_id: ob.objective_id || null,
|
|
479
|
+
uses: ob.uses || [],
|
|
480
|
+
allowed_tools: ob.allowed_tools || [],
|
|
481
|
+
mode: ob.mode || null,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Drop probe timestamps so a re-probe that changed nothing changes no hash. */
|
|
486
|
+
function stripVolatile(manifest) {
|
|
487
|
+
return {
|
|
488
|
+
schemaVersion: manifest && manifest.schemaVersion,
|
|
489
|
+
checksum: manifest && manifest.checksum,
|
|
490
|
+
entries: (((manifest && manifest.entries) || [])).map((e) => ({ id: e.id, plane: e.plane, kind: e.kind, reachable: !!e.reachable, blastRadius: e.blastRadius || null })),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Best-effort capability set for a non-standard cadence (reachable-filtered). */
|
|
495
|
+
function usesForArchetypeCadence(c, reachable) {
|
|
496
|
+
const text = `${c.id} ${c.purpose || ""}`.toLowerCase();
|
|
497
|
+
const guesses = [];
|
|
498
|
+
if (/review|memo|priorit|execution|operating/.test(text)) guesses.push("board_ready", "knowledge_search", "task_update");
|
|
499
|
+
if (/runway|burn|invest|capital|cost/.test(text)) guesses.push("books_reports");
|
|
500
|
+
if (/board|investor|stakeholder/.test(text)) guesses.push("knowledge_search", "files_list");
|
|
501
|
+
if (/commitment|sweep|action/.test(text)) guesses.push("board_ready", "task_update");
|
|
502
|
+
if (/inbox|mail/.test(text)) guesses.push("email_inbox", "email_triage");
|
|
503
|
+
if (!guesses.length) guesses.push("knowledge_search");
|
|
504
|
+
return [...new Set(guesses)].filter((g) => reachable.has(g));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export default {
|
|
508
|
+
COMPILER_VERSION, BASELINE_TOOLS, STANDARD_CADENCE_USES, STANDARD_REACTS,
|
|
509
|
+
compilePlan, solveSlot, busyWindows, allowedToolsFor, actionClassesFor, toolNameFor, rankOf,
|
|
510
|
+
};
|