@cohortapp/agent-sdk 2.3.2 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/framework-features.json +30 -0
  2. package/lib/backlog.mjs +136 -0
  3. package/lib/cadences.mjs +63 -2
  4. package/lib/cadences.test.mjs +105 -0
  5. package/lib/capability/inventory.mjs +542 -0
  6. package/lib/capability/inventory.test.mjs +232 -0
  7. package/lib/capability/probe.mjs +255 -0
  8. package/lib/channels/contract.mjs +37 -1
  9. package/lib/channels/contract.test.mjs +25 -1
  10. package/lib/claude-bin.mjs +37 -3
  11. package/lib/claude-bin.test.mjs +42 -8
  12. package/lib/execution/disposition.mjs +501 -0
  13. package/lib/execution/disposition.test.mjs +482 -0
  14. package/lib/execution/drive.mjs +352 -0
  15. package/lib/execution/drive.test.mjs +270 -0
  16. package/lib/execution/effects.mjs +340 -0
  17. package/lib/execution/effects.test.mjs +193 -0
  18. package/lib/execution/index.mjs +152 -0
  19. package/lib/execution/intake.mjs +581 -0
  20. package/lib/execution/intake.test.mjs +343 -0
  21. package/lib/execution/journal.mjs +374 -0
  22. package/lib/execution/journal.test.mjs +261 -0
  23. package/lib/execution/match.mjs +331 -0
  24. package/lib/execution/match.test.mjs +235 -0
  25. package/lib/execution/pipeline.mjs +341 -0
  26. package/lib/execution/pipeline.test.mjs +389 -0
  27. package/lib/execution/route.mjs +332 -0
  28. package/lib/execution/route.test.mjs +186 -0
  29. package/lib/execution/surface-policy.mjs +446 -0
  30. package/lib/execution/surface-policy.test.mjs +162 -0
  31. package/lib/goals/admission.mjs +209 -0
  32. package/lib/goals/admission.test.mjs +139 -0
  33. package/lib/goals/classify.mjs +206 -0
  34. package/lib/goals/classify.test.mjs +109 -0
  35. package/lib/goals/collaborate.mjs +415 -0
  36. package/lib/goals/collaborate.test.mjs +324 -0
  37. package/lib/goals/gaps.mjs +111 -0
  38. package/lib/goals/gaps.test.mjs +284 -0
  39. package/lib/goals/loop.mjs +537 -0
  40. package/lib/goals/loop.test.mjs +719 -0
  41. package/lib/identity/persona.mjs +247 -0
  42. package/lib/identity/persona.test.mjs +117 -0
  43. package/lib/kpi.mjs +469 -0
  44. package/lib/kpi.test.mjs +244 -0
  45. package/lib/mandate/audit.mjs +168 -0
  46. package/lib/mandate/audit.test.mjs +195 -0
  47. package/lib/mandate/cache.mjs +162 -0
  48. package/lib/mandate/derive.mjs +317 -0
  49. package/lib/mandate/derive.test.mjs +224 -0
  50. package/lib/mandate/model.mjs +352 -0
  51. package/lib/mandate/model.test.mjs +145 -0
  52. package/lib/mandate/refresh.mjs +187 -0
  53. package/lib/mandate/refresh.test.mjs +293 -0
  54. package/lib/mcp/server.test.mjs +4 -4
  55. package/lib/org/approvals.mjs +14 -2
  56. package/lib/org/client.mjs +58 -22
  57. package/lib/org/client.test.mjs +3 -1
  58. package/lib/org/inbound/directedness.mjs +720 -0
  59. package/lib/org/inbound/directedness.test.mjs +543 -0
  60. package/lib/org/inbound/facts.mjs +501 -0
  61. package/lib/org/inbound/facts.test.mjs +375 -0
  62. package/lib/org/inbound/hydrate.mjs +535 -0
  63. package/lib/org/inbound/hydrate.test.mjs +326 -0
  64. package/lib/org/inbound/index.mjs +233 -0
  65. package/lib/org/inbound/index.test.mjs +324 -0
  66. package/lib/org/inbound/io.mjs +141 -0
  67. package/lib/org/inbound/project.mjs +201 -0
  68. package/lib/org/inbound/project.test.mjs +287 -0
  69. package/lib/org/inbound/surfaces.mjs +257 -0
  70. package/lib/org/knowledge.mjs +10 -1
  71. package/lib/org/knowledge.test.mjs +8 -1
  72. package/lib/org/leases.mjs +5 -0
  73. package/lib/org/mesh.mjs +17 -2
  74. package/lib/org/messaging.mjs +40 -4
  75. package/lib/org/messaging.test.mjs +40 -0
  76. package/lib/org/param-contract.mjs +694 -0
  77. package/lib/org/param-contract.test.mjs +451 -0
  78. package/lib/org/protocol.checksum +1 -1
  79. package/lib/org/protocol.mjs +8 -0
  80. package/lib/org/protocol.test.mjs +5 -1
  81. package/lib/org/push.mjs +1025 -0
  82. package/lib/org/push.test.mjs +690 -0
  83. package/lib/org/tool-surface.mjs +138 -38
  84. package/lib/org/tool-surface.test.mjs +13 -8
  85. package/lib/org/typing.mjs +341 -0
  86. package/lib/org/typing.test.mjs +291 -0
  87. package/lib/plan/compile.mjs +510 -0
  88. package/lib/plan/compile.test.mjs +286 -0
  89. package/lib/plan/emit.mjs +256 -0
  90. package/lib/plan/emit.test.mjs +246 -0
  91. package/lib/plan/explain.mjs +226 -0
  92. package/lib/plan/explain.test.mjs +188 -0
  93. package/lib/plan/schema.mjs +140 -0
  94. package/lib/resource-governor.mjs +47 -1
  95. package/lib/resource-governor.test.mjs +21 -1
  96. package/lib/setup/enroll-from-cohort.mjs +105 -17
  97. package/lib/setup/enroll-from-cohort.test.mjs +68 -1
  98. package/lib/setup/sections/identity.mjs +15 -4
  99. package/lib/setup/sections/identity.test.mjs +94 -0
  100. package/lib/setup/sections/inventory.mjs +178 -0
  101. package/lib/setup/sections/inventory.test.mjs +198 -0
  102. package/lib/setup/sections/mandate.mjs +392 -0
  103. package/lib/setup/sections/mandate.test.mjs +373 -0
  104. package/lib/setup/sections/subagents.mjs +427 -0
  105. package/lib/setup/sections/subagents.test.mjs +429 -0
  106. package/lib/setup/sections/verify.mjs +121 -0
  107. package/lib/setup/sections/verify.test.mjs +175 -0
  108. package/lib/setup/sot.mjs +2 -0
  109. package/lib/subagents/cli.mjs +463 -0
  110. package/lib/subagents/cli.test.mjs +389 -0
  111. package/lib/subagents/client.mjs +373 -0
  112. package/lib/subagents/client.test.mjs +309 -0
  113. package/lib/subagents/gap.mjs +268 -0
  114. package/lib/subagents/gap.test.mjs +234 -0
  115. package/lib/subagents/lock.mjs +296 -0
  116. package/lib/subagents/lock.test.mjs +248 -0
  117. package/lib/subagents/manifest.mjs +224 -0
  118. package/lib/subagents/manifest.test.mjs +175 -0
  119. package/lib/subagents/refs.mjs +274 -0
  120. package/lib/subagents/refs.test.mjs +204 -0
  121. package/lib/subagents/resolve.mjs +455 -0
  122. package/lib/subagents/resolve.test.mjs +422 -0
  123. package/lib/subagents/schema.mjs +467 -0
  124. package/lib/subagents/schema.test.mjs +306 -0
  125. package/package.json +8 -3
  126. package/plugins/maestro-skills/.claude-plugin/marketplace.json +16 -0
  127. package/policies/ai-disclosure.yaml +42 -2
  128. package/scaffold/CLAUDE.md +16 -2
  129. package/schedules/triggers/goal-steward.md +79 -0
  130. package/scripts/ci/conformance-org-api.mjs +792 -0
  131. package/scripts/ci/conformance-org-api.test.mjs +417 -0
  132. package/scripts/daemon/agent-daemon.mjs +36 -4
  133. package/scripts/daemon/cadence-handlers.mjs +145 -1
  134. package/scripts/daemon/goal-steward-cadence.test.mjs +243 -0
  135. package/scripts/daemon/inbox-deferral.mjs +45 -2
  136. package/scripts/daemon/inbox-deferral.test.mjs +56 -0
  137. package/scripts/daemon/inbox-wake.mjs +282 -0
  138. package/scripts/daemon/inbox-wake.test.mjs +199 -0
  139. package/scripts/daemon/prompt-builder.mjs +41 -1
  140. package/scripts/daemon/typing-registry.mjs +55 -2
  141. package/scripts/daemon/typing-registry.test.mjs +25 -0
  142. package/scripts/local-triggers/generate-plists.test.mjs +5 -5
  143. package/scripts/setup/gen-subagent-manifest.mjs +95 -0
  144. package/scripts/setup/gen-subagent-manifest.test.mjs +124 -0
  145. package/scripts/setup/generate-plan.mjs +108 -0
  146. package/scripts/setup/init-capability-manifest.mjs +70 -0
  147. package/scripts/setup/init-skill-marketplace.mjs +155 -0
  148. 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 };