@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,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/mandate/cache.mjs — the local mandate cache + the staleness ladder.
|
|
3
|
+
*
|
|
4
|
+
* hq owns the MANDATE (what outcomes exist, who owns them, what the target is);
|
|
5
|
+
* the agent owns the PLAN. The plan is compiled locally from a CACHED mandate, so
|
|
6
|
+
* a partitioned agent keeps working from cache on a defined ramp instead of going
|
|
7
|
+
* dark. This module is that cache and that ramp (SPEC §2.3 / §6.4).
|
|
8
|
+
*
|
|
9
|
+
* state/mandate/cache.json → { version, checksum, body, fetchedAt, source }
|
|
10
|
+
*
|
|
11
|
+
* `source` is load-bearing and is NOT cosmetic:
|
|
12
|
+
* "server" — fetched from hq via mandate.get (adopted, authoritative)
|
|
13
|
+
* "local" — derived locally from charter/strategy/archetype, every objective
|
|
14
|
+
* state:'proposed'. Proposed objectives compile NO obligations and
|
|
15
|
+
* admit NO backlog; the agent runs exactly today's archetype
|
|
16
|
+
* cadences until a human adopts in hq.
|
|
17
|
+
*
|
|
18
|
+
* Staleness tiers (SPEC §6.4) — degradation is a ramp, not a cliff:
|
|
19
|
+
* fresh (<24h) full REACT / full SCHEDULE / full OUTCOME
|
|
20
|
+
* aging (24-72h) full / full / OUTCOME suspended
|
|
21
|
+
* stale (>72h) offline_safe REACT only / inline+guarded SCHEDULE / suspended
|
|
22
|
+
* expired (>7d) nothing but one drift record per day
|
|
23
|
+
*
|
|
24
|
+
* Pure + injectable: the clock arrives as a parameter, never `Date.now()` inside
|
|
25
|
+
* a decision function, so the ladder is testable and deterministic.
|
|
26
|
+
*
|
|
27
|
+
* @module lib/mandate/cache
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
"use strict";
|
|
31
|
+
|
|
32
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
33
|
+
import { join } from "node:path";
|
|
34
|
+
import { createHash } from "node:crypto";
|
|
35
|
+
|
|
36
|
+
/** Relative path of the cache (git-ignored runtime state). */
|
|
37
|
+
export const CACHE_REL = join("state", "mandate", "cache.json");
|
|
38
|
+
|
|
39
|
+
/** Staleness tier boundaries, in seconds. */
|
|
40
|
+
export const TIERS = Object.freeze({ fresh: 24 * 3600, aging: 72 * 3600, stale: 7 * 24 * 3600 });
|
|
41
|
+
|
|
42
|
+
/** Absolute cache path for an agent root. */
|
|
43
|
+
export function cachePath(agentRoot) {
|
|
44
|
+
return join(agentRoot, CACHE_REL);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Canonical JSON (stable key order) — the checksum + hash substrate. */
|
|
48
|
+
export function canonical(value) {
|
|
49
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value === undefined ? null : value);
|
|
50
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
51
|
+
const keys = Object.keys(value).sort();
|
|
52
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`).join(",")}}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** sha256 of the canonicalised value. */
|
|
56
|
+
export function checksumOf(value) {
|
|
57
|
+
return createHash("sha256").update(canonical(value)).digest("hex");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Write the cache atomically.
|
|
62
|
+
* @param {string} agentRoot
|
|
63
|
+
* @param {{version?:number, body:object, source?:string, fetchedAt?:string}} snapshot
|
|
64
|
+
* @param {object} [deps] - { writeFileSync, mkdirSync, renameSync, now }
|
|
65
|
+
* @returns {object} the persisted record
|
|
66
|
+
*/
|
|
67
|
+
export function writeCache(agentRoot, snapshot = {}, deps = {}) {
|
|
68
|
+
const wr = deps.writeFileSync || writeFileSync;
|
|
69
|
+
const mk = deps.mkdirSync || mkdirSync;
|
|
70
|
+
const mv = deps.renameSync || renameSync;
|
|
71
|
+
const now = typeof deps.now === "function" ? deps.now() : new Date().toISOString();
|
|
72
|
+
const body = snapshot.body || {};
|
|
73
|
+
const record = {
|
|
74
|
+
version: Number.isFinite(snapshot.version) ? snapshot.version : 0,
|
|
75
|
+
checksum: snapshot.checksum || checksumOf(body),
|
|
76
|
+
source: snapshot.source || "local",
|
|
77
|
+
fetchedAt: snapshot.fetchedAt || now,
|
|
78
|
+
body,
|
|
79
|
+
};
|
|
80
|
+
const p = cachePath(agentRoot);
|
|
81
|
+
mk(join(agentRoot, "state", "mandate"), { recursive: true });
|
|
82
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
83
|
+
wr(tmp, JSON.stringify(record, null, 2) + "\n", "utf-8");
|
|
84
|
+
mv(tmp, p);
|
|
85
|
+
return record;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read the cache. Returns null when absent; a PARSE FAILURE is returned as a
|
|
90
|
+
* degraded record (never silently null) so callers can log the difference
|
|
91
|
+
* between "never fetched" and "corrupt on disk".
|
|
92
|
+
* @returns {object|null}
|
|
93
|
+
*/
|
|
94
|
+
export function readCache(agentRoot, deps = {}) {
|
|
95
|
+
const ex = deps.existsSync || existsSync;
|
|
96
|
+
const rd = deps.readFileSync || readFileSync;
|
|
97
|
+
const p = cachePath(agentRoot);
|
|
98
|
+
if (!ex(p)) return null;
|
|
99
|
+
try {
|
|
100
|
+
const doc = JSON.parse(rd(p, "utf-8"));
|
|
101
|
+
return doc && typeof doc === "object" ? doc : { corrupt: true, reason: "cache is not an object", body: {} };
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return { corrupt: true, reason: e && e.message ? e.message : String(e), body: {} };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Seconds since the cache was fetched. `Infinity` when never fetched.
|
|
109
|
+
* @param {object|null} record @param {number|string|Date} nowMs
|
|
110
|
+
*/
|
|
111
|
+
export function staleSeconds(record, nowMs = Date.now()) {
|
|
112
|
+
if (!record || !record.fetchedAt) return Infinity;
|
|
113
|
+
const then = Date.parse(record.fetchedAt);
|
|
114
|
+
const now = typeof nowMs === "number" ? nowMs : Date.parse(String(nowMs));
|
|
115
|
+
if (!Number.isFinite(then) || !Number.isFinite(now)) return Infinity;
|
|
116
|
+
return Math.max(0, (now - then) / 1000);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The staleness tier for a cache record.
|
|
121
|
+
* @returns {"fresh"|"aging"|"stale"|"expired"}
|
|
122
|
+
*/
|
|
123
|
+
export function stalenessTier(record, nowMs = Date.now()) {
|
|
124
|
+
const s = staleSeconds(record, nowMs);
|
|
125
|
+
if (s < TIERS.fresh) return "fresh";
|
|
126
|
+
if (s < TIERS.aging) return "aging";
|
|
127
|
+
if (s < TIERS.stale) return "stale";
|
|
128
|
+
return "expired";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* What a given tier permits. The loops consult this rather than re-deriving the
|
|
133
|
+
* ramp; that is why the ladder can only be wrong in one place.
|
|
134
|
+
* @param {string} tier
|
|
135
|
+
* @returns {{react:"full"|"offline_safe"|"none", schedule:"full"|"guarded"|"none", outcome:"full"|"suspended"}}
|
|
136
|
+
*/
|
|
137
|
+
export function permissionsForTier(tier) {
|
|
138
|
+
switch (tier) {
|
|
139
|
+
case "fresh": return { react: "full", schedule: "full", outcome: "full" };
|
|
140
|
+
case "aging": return { react: "full", schedule: "full", outcome: "suspended" };
|
|
141
|
+
case "stale": return { react: "offline_safe", schedule: "guarded", outcome: "suspended" };
|
|
142
|
+
default: return { react: "none", schedule: "none", outcome: "suspended" };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Are the adopted objectives in this cache usable to compile obligations? */
|
|
147
|
+
export function adoptedObjectives(record) {
|
|
148
|
+
const body = (record && record.body) || {};
|
|
149
|
+
const list = Array.isArray(body.objectives) ? body.objectives : [];
|
|
150
|
+
return list.filter((o) => o && o.state === "active");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Every objective in the cache, adopted or not (the proposal surface). */
|
|
154
|
+
export function allObjectives(record) {
|
|
155
|
+
const body = (record && record.body) || {};
|
|
156
|
+
return Array.isArray(body.objectives) ? body.objectives : [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export default {
|
|
160
|
+
CACHE_REL, TIERS, cachePath, canonical, checksumOf, writeCache, readCache,
|
|
161
|
+
staleSeconds, stalenessTier, permissionsForTier, adoptedObjectives, allObjectives,
|
|
162
|
+
};
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/mandate/derive.mjs — derive a PROPOSED objective tree for this seat.
|
|
3
|
+
*
|
|
4
|
+
* This is the "what is this agent actually for" step of init (SPEC §5.5). It is
|
|
5
|
+
* deterministic and offline: prose → structured mandate happens ONCE, here, from
|
|
6
|
+
* artefacts the agent already has, in the SPEC's precedence order:
|
|
7
|
+
*
|
|
8
|
+
* 1. hq `charter.sections` PILLAR / CAPABILITY entries (config/agent.json)
|
|
9
|
+
* 2. `org-context.json` strategy streams (the org's own plan)
|
|
10
|
+
* 3. `profile.kpiCategories` from the archetype (the role's KPI tree)
|
|
11
|
+
* 4. `config/priorities.yaml` (the seat's own list)
|
|
12
|
+
*
|
|
13
|
+
* Every node comes out `state:'proposed'`. **Nothing is adopted here** — an agent
|
|
14
|
+
* may propose an objective but a human/manager adopts it in hq, and
|
|
15
|
+
* `mandate.adopt` refuses when the actor is the objective's owner. That is the
|
|
16
|
+
* anti-Goodhart law: a seat cannot define, measure and be graded on the same
|
|
17
|
+
* number. Proposed objectives compile no obligations, so until adoption the
|
|
18
|
+
* agent runs exactly today's archetype cadences.
|
|
19
|
+
*
|
|
20
|
+
* SENSORS are chosen from the capability manifest, never invented: a metric is
|
|
21
|
+
* bound to a `reachable:true` capability or it is proposed with
|
|
22
|
+
* `sensor.source:"human"` and flagged. An objective with an unreachable sensor
|
|
23
|
+
* is a measurement debt the operator can see, not a silent no-op.
|
|
24
|
+
*
|
|
25
|
+
* PURE: no fs, no network, no clock, no randomness. Same inputs → same tree.
|
|
26
|
+
*
|
|
27
|
+
* @module lib/mandate/derive
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
"use strict";
|
|
31
|
+
|
|
32
|
+
import { createHash } from "node:crypto";
|
|
33
|
+
|
|
34
|
+
/** Objective kinds (mirrors hq's ObjectiveKind enum). */
|
|
35
|
+
export const OBJECTIVE_KINDS = Object.freeze(["PILLAR", "OBJECTIVE", "GOAL"]);
|
|
36
|
+
/** Metric directions (mirrors hq's MetricDirection enum). */
|
|
37
|
+
export const DIRECTIONS = Object.freeze(["up", "down", "hold", "band"]);
|
|
38
|
+
/** Review/measure rhythms we know how to schedule. */
|
|
39
|
+
export const CADENCES = Object.freeze(["weekly", "monthly", "quarterly"]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Metric-keyword → candidate sensor capabilities, most specific first. The
|
|
43
|
+
* derivation walks this table and takes the FIRST capability that is reachable
|
|
44
|
+
* in the manifest; nothing else picks sensors, so the mapping is auditable in
|
|
45
|
+
* one place.
|
|
46
|
+
* @type {Array<{re:RegExp, caps:string[], params?:object, direction?:string, unit?:string}>}
|
|
47
|
+
*/
|
|
48
|
+
export const SENSOR_HINTS = Object.freeze([
|
|
49
|
+
{ re: /(pipeline|deal|bookings|revenue|win rate|arr|mrr)/i, caps: ["crm_list_deals", "crm_next_best_action"], params: { stage: "open" }, unit: "x", direction: "up" },
|
|
50
|
+
{ re: /(runway|burn|cash|invoice|billing|tax|spend|cost)/i, caps: ["books_reports", "books_invoices"], unit: "months", direction: "up" },
|
|
51
|
+
{ re: /(commitment|action item|follow-?through|closure|backlog|task|execution)/i, caps: ["board_ready", "task_update"], unit: "%", direction: "up" },
|
|
52
|
+
{ re: /(decision|governance|approval)/i, caps: ["decision_list", "approval_wait"], unit: "days", direction: "down" },
|
|
53
|
+
{ re: /(meeting|calendar|cadence|agenda)/i, caps: ["calendar_list", "meetings_recap_file"], unit: "%", direction: "up" },
|
|
54
|
+
{ re: /(inbox|email|response|reply|correspondence)/i, caps: ["email_inbox", "email_triage"], unit: "hours", direction: "down" },
|
|
55
|
+
{ re: /(message|channel|comms|alignment|broadcast)/i, caps: ["messaging_history", "messaging_channels"], unit: "%", direction: "up" },
|
|
56
|
+
{ re: /(escalation|blocker|risk|incident)/i, caps: ["crm_list_escalations", "escalation_raise"], unit: "count", direction: "down" },
|
|
57
|
+
{ re: /(directory|contact|crm hygiene|record)/i, caps: ["directory_list_captures", "directory_search"], unit: "%", direction: "up" },
|
|
58
|
+
{ re: /(knowledge|memory|documentation|institutional)/i, caps: ["knowledge_search", "files_list"], unit: "%", direction: "up" },
|
|
59
|
+
{ re: /(brand|design|template|asset)/i, caps: ["design_list_templates", "design_foundation"], unit: "%", direction: "up" },
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/** Default review rhythm per objective kind. */
|
|
63
|
+
const DEFAULT_CADENCE = { PILLAR: "quarterly", OBJECTIVE: "weekly", GOAL: "monthly" };
|
|
64
|
+
|
|
65
|
+
/** Deterministic, stable, url-safe slug. Collisions are disambiguated by caller. */
|
|
66
|
+
export function slug(text, max = 48) {
|
|
67
|
+
const s = String(text || "")
|
|
68
|
+
.toLowerCase()
|
|
69
|
+
.replace(/^\s*\d+[).:\-]\s*/, "") // strip "1) " / "3. " list prefixes
|
|
70
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
71
|
+
.replace(/^-+|-+$/g, "")
|
|
72
|
+
.slice(0, max)
|
|
73
|
+
.replace(/-+$/g, "");
|
|
74
|
+
return s || `k-${createHash("sha256").update(String(text || "")).digest("hex").slice(0, 8)}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** First sentence / clause of a body, for a short objective title. */
|
|
78
|
+
export function firstClause(body, max = 120) {
|
|
79
|
+
const t = String(body || "").replace(/\s+/g, " ").trim().replace(/^\d+[).:\-]\s*/, "");
|
|
80
|
+
const cut = t.split(/(?<=[.;—])\s/)[0] || t;
|
|
81
|
+
return cut.length > max ? `${cut.slice(0, max - 1).trimEnd()}…` : cut;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Pick a sensor for a metric from the capability manifest.
|
|
86
|
+
* @param {string} text the objective/metric text to classify
|
|
87
|
+
* @param {Set<string>} reachable reachable capability ids
|
|
88
|
+
* @returns {{capability:string|null, params?:object, source:"method"|"human", unit?:string, direction?:string, considered:string[]}}
|
|
89
|
+
*/
|
|
90
|
+
export function pickSensor(text, reachable = new Set()) {
|
|
91
|
+
for (const hint of SENSOR_HINTS) {
|
|
92
|
+
if (!hint.re.test(String(text || ""))) continue;
|
|
93
|
+
const cap = hint.caps.find((c) => reachable.has(c));
|
|
94
|
+
if (cap) {
|
|
95
|
+
const s = { capability: cap, source: "method", considered: hint.caps.slice() };
|
|
96
|
+
if (hint.params) s.params = hint.params;
|
|
97
|
+
if (hint.unit) s.unit = hint.unit;
|
|
98
|
+
if (hint.direction) s.direction = hint.direction;
|
|
99
|
+
return s;
|
|
100
|
+
}
|
|
101
|
+
// Matched a family but nothing in it is reachable — that is a measurement
|
|
102
|
+
// debt, and it must be visible rather than silently downgraded.
|
|
103
|
+
return { capability: null, source: "human", considered: hint.caps.slice(), unit: hint.unit, direction: hint.direction };
|
|
104
|
+
}
|
|
105
|
+
return { capability: null, source: "human", considered: [] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Derive the proposed objective tree.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} input
|
|
112
|
+
* @param {Array<{kind:string, order?:number, title?:string, body:string, id?:string}>} [input.charterSections]
|
|
113
|
+
* @param {object} [input.strategy] org-context.json `strategy`
|
|
114
|
+
* @param {Array<{id:string,name:string,examples?:string[]}>} [input.kpiCategories] archetype profile
|
|
115
|
+
* @param {Array<{id?:string,title?:string,text?:string}>} [input.priorities] config/priorities.yaml
|
|
116
|
+
* @param {object} [input.manifest] capability manifest (for sensor binding)
|
|
117
|
+
* @param {string} [input.memberId]
|
|
118
|
+
* @param {object} [input.overrides] key → { target, tolerance, cadence, unit, direction, metric }
|
|
119
|
+
* @returns {{objectives:object[], degradations:object[], counts:object}}
|
|
120
|
+
*/
|
|
121
|
+
export function deriveMandate(input = {}) {
|
|
122
|
+
const charterSections = Array.isArray(input.charterSections) ? input.charterSections : [];
|
|
123
|
+
const kpiCategories = Array.isArray(input.kpiCategories) ? input.kpiCategories : [];
|
|
124
|
+
const priorities = Array.isArray(input.priorities) ? input.priorities : [];
|
|
125
|
+
const strategy = input.strategy && typeof input.strategy === "object" ? input.strategy : null;
|
|
126
|
+
const overrides = input.overrides && typeof input.overrides === "object" ? input.overrides : {};
|
|
127
|
+
const reachable = reachableSet(input.manifest);
|
|
128
|
+
|
|
129
|
+
const objectives = [];
|
|
130
|
+
const degradations = [];
|
|
131
|
+
const used = new Set();
|
|
132
|
+
|
|
133
|
+
/** Reserve a unique key. */
|
|
134
|
+
const key = (base) => {
|
|
135
|
+
let k = slug(base);
|
|
136
|
+
let n = 2;
|
|
137
|
+
while (used.has(k)) k = `${slug(base)}-${n++}`;
|
|
138
|
+
used.add(k);
|
|
139
|
+
return k;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// ── 1. PILLARs from the charter ─────────────────────────────────────────
|
|
143
|
+
const pillars = charterSections
|
|
144
|
+
.filter((s) => s && (String(s.kind).toUpperCase() === "PILLAR" || String(s.kind).toUpperCase() === "CAPABILITY"))
|
|
145
|
+
.slice()
|
|
146
|
+
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
147
|
+
|
|
148
|
+
for (const s of pillars) {
|
|
149
|
+
const text = firstClause(s.title || s.body);
|
|
150
|
+
const k = key(text);
|
|
151
|
+
objectives.push({
|
|
152
|
+
key: k,
|
|
153
|
+
kind: "PILLAR",
|
|
154
|
+
text,
|
|
155
|
+
parentKey: null,
|
|
156
|
+
charterSectionId: s.id || null,
|
|
157
|
+
state: "proposed",
|
|
158
|
+
direction: "up",
|
|
159
|
+
weight: 1,
|
|
160
|
+
cadence: DEFAULT_CADENCE.PILLAR,
|
|
161
|
+
sensor: null,
|
|
162
|
+
source: { origin: "charter", ref: s.id || `order:${s.order ?? 0}` },
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (pillars.length === 0) {
|
|
166
|
+
degradations.push({ kind: "no_charter_pillars", reason: "config/agent.json has no charter.sections of kind PILLAR/CAPABILITY — objectives will have no charter provenance" });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Attach an objective to the most textually-related pillar, else the first. */
|
|
170
|
+
const pillarKeys = objectives.filter((o) => o.kind === "PILLAR").map((o) => ({ key: o.key, text: o.text }));
|
|
171
|
+
const parentFor = (text) => {
|
|
172
|
+
if (!pillarKeys.length) return null;
|
|
173
|
+
const t = String(text || "").toLowerCase();
|
|
174
|
+
let best = null;
|
|
175
|
+
let bestScore = 0;
|
|
176
|
+
for (const p of pillarKeys) {
|
|
177
|
+
const score = overlapScore(t, p.text.toLowerCase());
|
|
178
|
+
if (score > bestScore) { bestScore = score; best = p.key; }
|
|
179
|
+
}
|
|
180
|
+
return bestScore > 0 ? best : pillarKeys[0].key;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// ── 2. OBJECTIVEs from the org's strategy streams ───────────────────────
|
|
184
|
+
for (const stream of strategyStreams(strategy)) {
|
|
185
|
+
const text = firstClause(stream.title || stream.name || stream.code);
|
|
186
|
+
const k = key(text);
|
|
187
|
+
const sensor = pickSensor(`${text} ${stream.detail || ""}`, reachable);
|
|
188
|
+
objectives.push(buildObjective({ k, text, parentKey: parentFor(text), sensor, cadence: "weekly", origin: "strategy", ref: stream.code || stream.id || text, overrides }));
|
|
189
|
+
if (sensor.source === "human") degradations.push({ kind: "no_reachable_sensor", key: k, reason: `no reachable capability measures "${text}" (considered: ${sensor.considered.join(", ") || "none"})` });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── 3. OBJECTIVEs from the archetype's KPI categories ───────────────────
|
|
193
|
+
for (const cat of kpiCategories) {
|
|
194
|
+
if (!cat || !cat.id) continue;
|
|
195
|
+
const text = cat.name || cat.id;
|
|
196
|
+
const example = Array.isArray(cat.examples) && cat.examples.length ? cat.examples[0] : "";
|
|
197
|
+
const k = key(cat.id);
|
|
198
|
+
const sensor = pickSensor(`${text} ${example}`, reachable);
|
|
199
|
+
objectives.push(buildObjective({
|
|
200
|
+
k, text, parentKey: parentFor(`${text} ${example}`), sensor, cadence: "weekly",
|
|
201
|
+
origin: "archetype", ref: `kpiCategory:${cat.id}`, metric: example ? slug(example).replace(/-/g, "_") : undefined, overrides,
|
|
202
|
+
}));
|
|
203
|
+
if (sensor.source === "human") degradations.push({ kind: "no_reachable_sensor", key: k, reason: `no reachable capability measures "${text}" (considered: ${sensor.considered.join(", ") || "none"})` });
|
|
204
|
+
}
|
|
205
|
+
if (kpiCategories.length === 0) {
|
|
206
|
+
degradations.push({ kind: "no_kpi_categories", reason: "archetype profile carried no kpiCategories — the mandate has no role-derived objectives" });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── 4. GOALs from the seat's own priorities ─────────────────────────────
|
|
210
|
+
for (const p of priorities) {
|
|
211
|
+
const text = firstClause(p && (p.title || p.text || p.id));
|
|
212
|
+
if (!text) continue;
|
|
213
|
+
const k = key(text);
|
|
214
|
+
const sensor = pickSensor(text, reachable);
|
|
215
|
+
const parent = parentFor(text);
|
|
216
|
+
objectives.push(buildObjective({ k, text, parentKey: parent, sensor, cadence: "monthly", origin: "priorities", ref: (p && p.id) || text, kind: "GOAL", overrides }));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const counts = {
|
|
220
|
+
pillars: objectives.filter((o) => o.kind === "PILLAR").length,
|
|
221
|
+
objectives: objectives.filter((o) => o.kind === "OBJECTIVE").length,
|
|
222
|
+
goals: objectives.filter((o) => o.kind === "GOAL").length,
|
|
223
|
+
withMethodSensor: objectives.filter((o) => o.sensor && o.sensor.source === "method").length,
|
|
224
|
+
proposed: objectives.length,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return { objectives, degradations, counts };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Wrap the derived tree in the mandate BODY shape the cache + compiler consume.
|
|
232
|
+
* @param {object} o - { memberId, objectives, budgetCentsPerPeriod?, collaborators?, reactsTo? }
|
|
233
|
+
*/
|
|
234
|
+
export function toMandateBody(o = {}) {
|
|
235
|
+
return {
|
|
236
|
+
memberId: o.memberId || null,
|
|
237
|
+
objectives: Array.isArray(o.objectives) ? o.objectives : [],
|
|
238
|
+
budgetCentsPerPeriod: Number.isFinite(o.budgetCentsPerPeriod) ? o.budgetCentsPerPeriod : 500,
|
|
239
|
+
collaborators: Array.isArray(o.collaborators) ? o.collaborators : [],
|
|
240
|
+
reactsTo: Array.isArray(o.reactsTo) ? o.reactsTo : [],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// helpers
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
function buildObjective({ k, text, parentKey, sensor, cadence, origin, ref, metric, kind = "OBJECTIVE", overrides = {} }) {
|
|
249
|
+
const ov = overrides[k] || {};
|
|
250
|
+
const obj = {
|
|
251
|
+
key: k,
|
|
252
|
+
kind,
|
|
253
|
+
text,
|
|
254
|
+
parentKey: parentKey || null,
|
|
255
|
+
charterSectionId: null,
|
|
256
|
+
state: "proposed",
|
|
257
|
+
metric: ov.metric || metric || `${k.replace(/-/g, "_")}`,
|
|
258
|
+
unit: ov.unit || sensor.unit || null,
|
|
259
|
+
direction: DIRECTIONS.includes(ov.direction) ? ov.direction : (sensor.direction || "up"),
|
|
260
|
+
baseline: Number.isFinite(ov.baseline) ? ov.baseline : null,
|
|
261
|
+
target: Number.isFinite(ov.target) ? ov.target : null,
|
|
262
|
+
tolerance: Number.isFinite(ov.tolerance) ? ov.tolerance : 0,
|
|
263
|
+
weight: Number.isFinite(ov.weight) ? ov.weight : 1,
|
|
264
|
+
cadence: CADENCES.includes(ov.cadence) ? ov.cadence : cadence,
|
|
265
|
+
sensor: sensor.capability
|
|
266
|
+
? { capability: sensor.capability, params: sensor.params || {}, source: "method" }
|
|
267
|
+
: { capability: null, params: {}, source: "human", considered: sensor.considered || [] },
|
|
268
|
+
source: { origin, ref: String(ref || "") },
|
|
269
|
+
};
|
|
270
|
+
return obj;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Reachable capability ids from a manifest (empty set when none). */
|
|
274
|
+
function reachableSet(manifest) {
|
|
275
|
+
const out = new Set();
|
|
276
|
+
if (!manifest) return out;
|
|
277
|
+
// Tolerate every shape the manifest legitimately arrives in. `buildInventory`
|
|
278
|
+
// returns `{entries:[…]}`, but a bare entry ARRAY is the shape the spec's
|
|
279
|
+
// state-file table describes and the shape a hand-written fixture reaches for
|
|
280
|
+
// — and `[].entries` is Array.prototype.entries, a FUNCTION, so the naive
|
|
281
|
+
// `manifest.entries || []` walk threw "function is not iterable" and took the
|
|
282
|
+
// whole derivation down with it. A Set of ids is accepted too, since that is
|
|
283
|
+
// what `pickSensor` itself takes.
|
|
284
|
+
if (manifest instanceof Set) return new Set(manifest);
|
|
285
|
+
const list = Array.isArray(manifest)
|
|
286
|
+
? manifest
|
|
287
|
+
: Array.isArray(manifest.entries)
|
|
288
|
+
? manifest.entries
|
|
289
|
+
: [];
|
|
290
|
+
for (const e of list) {
|
|
291
|
+
if (!e) continue;
|
|
292
|
+
if (typeof e === "string") { out.add(e); continue; }
|
|
293
|
+
if (e.reachable) out.add(e.id);
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Strategy streams, tolerating the several shapes org-context has carried. */
|
|
299
|
+
function strategyStreams(strategy) {
|
|
300
|
+
if (!strategy) return [];
|
|
301
|
+
const raw = Array.isArray(strategy.streams) ? strategy.streams
|
|
302
|
+
: Array.isArray(strategy.workstreams) ? strategy.workstreams
|
|
303
|
+
: [];
|
|
304
|
+
return raw.filter((s) => s && (s.title || s.name || s.code || s.id));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Cheap token-overlap score (deterministic; stopwords dropped). */
|
|
308
|
+
const STOP = new Set(["the", "a", "an", "and", "or", "of", "to", "for", "with", "on", "in", "own", "every", "its"]);
|
|
309
|
+
function overlapScore(a, b) {
|
|
310
|
+
const ta = new Set(String(a).split(/[^a-z0-9]+/).filter((w) => w.length > 3 && !STOP.has(w)));
|
|
311
|
+
const tb = new Set(String(b).split(/[^a-z0-9]+/).filter((w) => w.length > 3 && !STOP.has(w)));
|
|
312
|
+
let n = 0;
|
|
313
|
+
for (const w of ta) if (tb.has(w)) n++;
|
|
314
|
+
return n;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export default { deriveMandate, toMandateBody, pickSensor, slug, firstClause, SENSOR_HINTS, OBJECTIVE_KINDS, DIRECTIONS, CADENCES };
|