@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,542 @@
1
+ /**
2
+ * lib/capability/inventory.mjs — the agent's CAPABILITY MAP (SPEC §5.1).
3
+ *
4
+ * Exhaustively enumerates everything this agent can actually *do*, across the
5
+ * four planes of the execution ladder, and probes each one for reachability:
6
+ *
7
+ * builtin — Claude Code's own tools (Read/Write/Edit/Bash/Glob/Grep/Task/…)
8
+ * skill — plugin skills on disk (plugins/<plugin>/skills/*.md) + archetype
9
+ * workflows + the sub-agent roster (agents/<id>/agent.md)
10
+ * mcp — servers declared in .mcp.json, cross-read with the role's
11
+ * config/mcp-servers.yaml wish-list
12
+ * org — the curated Cohort protocol tools (lib/org/tool-surface.getOrgTools)
13
+ * plus the workspace's GRANTED integration tools from
14
+ * state/org/toolset.json
15
+ *
16
+ * The output is a durable, queryable artefact — `config/capability-manifest.json`
17
+ * (tracked in git, so a capability appearing or disappearing shows up in a diff)
18
+ * — that sessions, the schedule compiler and the execution router all consult.
19
+ *
20
+ * THE LAW this file exists to enforce: **an entry with `reachable:false` may not
21
+ * be cited by any obligation.** `lib/plan/compile.mjs` reads this manifest and
22
+ * refuses to emit an obligation whose `uses[]` is not reachable, so a schedule
23
+ * can never be built on a capability the agent does not have.
24
+ *
25
+ * Determinism: entries are emitted in a stable (plane, id) order and the manifest
26
+ * carries a `checksum` over the identity-bearing fields only (id/plane/kind/
27
+ * reachable), so re-running the inventory when nothing changed produces the same
28
+ * checksum even though `probedAt` moved. That is what lets the plan compiler
29
+ * decide "nothing on disk needs to move".
30
+ *
31
+ * Fail-open, never silent: every plane that cannot be enumerated or probed
32
+ * records a `degradations[]` entry (id + reason) AND logs. A plane that throws
33
+ * yields zero entries for that plane, not a failed setup.
34
+ *
35
+ * Pure-ish + injectable: all fs/network/clock effects arrive via `deps`.
36
+ *
37
+ * @module lib/capability/inventory
38
+ */
39
+
40
+ "use strict";
41
+
42
+ import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync, renameSync } from "node:fs";
43
+ import { join } from "node:path";
44
+ import { createHash } from "node:crypto";
45
+
46
+ import { probeSkillPlugin, probeMcpServer, probeOrgPlane, probeGrantedPluginTools, readJsonSafe, PLANES } from "./probe.mjs";
47
+
48
+ /** Relative path of the durable manifest (tracked in git). */
49
+ export const MANIFEST_REL = join("config", "capability-manifest.json");
50
+ /** Manifest schema version — bump when the entry shape changes. */
51
+ export const MANIFEST_VERSION = 1;
52
+
53
+ /**
54
+ * Claude Code's built-in tool surface. Always reachable (they ship with the
55
+ * binary) but still inventoried, because the execution router needs to know
56
+ * a rung-3 session can read/write the filesystem and a rung-0 call cannot.
57
+ * `blastRadius` follows the SPEC's ACTION_CLASSES vocabulary.
58
+ */
59
+ export const BUILTIN_TOOLS = Object.freeze([
60
+ { id: "Read", kind: "tool", blastRadius: "internal", costHint: "free" },
61
+ { id: "Write", kind: "tool", blastRadius: "irreversible", costHint: "free" },
62
+ { id: "Edit", kind: "tool", blastRadius: "irreversible", costHint: "free" },
63
+ { id: "Glob", kind: "tool", blastRadius: "internal", costHint: "free" },
64
+ { id: "Grep", kind: "tool", blastRadius: "internal", costHint: "free" },
65
+ { id: "Bash", kind: "tool", blastRadius: "irreversible", costHint: "cheap" },
66
+ { id: "WebFetch", kind: "tool", blastRadius: "external", costHint: "cheap" },
67
+ { id: "WebSearch", kind: "tool", blastRadius: "external", costHint: "cheap" },
68
+ { id: "Task", kind: "tool", blastRadius: "internal", costHint: "session" },
69
+ { id: "Skill", kind: "tool", blastRadius: "internal", costHint: "cheap" },
70
+ { id: "TodoWrite", kind: "tool", blastRadius: "internal", costHint: "free" },
71
+ ]);
72
+
73
+ /** Map an org tool descriptor onto the ACTION_CLASSES blast-radius vocabulary. */
74
+ export function blastRadiusOfOrgTool(t) {
75
+ if (!t) return "internal";
76
+ if (t.outbound) return "external";
77
+ if (t.access === "admin") return "irreversible";
78
+ if (t.access === "write") {
79
+ if (/^(email|messaging|calling)_/.test(t.name || "")) return "external";
80
+ if (/(books|payment|invoice|expense)/.test(t.name || "")) return "financial";
81
+ return "irreversible";
82
+ }
83
+ return "internal";
84
+ }
85
+
86
+ /** Cost hint for an org tool: reads are free, writes cheap, sessions expensive. */
87
+ export function costHintOfOrgTool(t) {
88
+ return t && t.access === "read" ? "free" : "cheap";
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Enumerators — one per plane. Each returns { entries, degradations }.
93
+ // ---------------------------------------------------------------------------
94
+
95
+ /** Built-in plane. Static; always reachable. */
96
+ export function enumerateBuiltins(at) {
97
+ return {
98
+ entries: BUILTIN_TOOLS.map((b) => ({
99
+ id: `builtin:${b.id}`,
100
+ plane: "builtin",
101
+ kind: b.kind,
102
+ invocation: b.id,
103
+ reachable: true,
104
+ probedAt: at,
105
+ probe: { name: "static", ok: true, detail: "ships with Claude Code" },
106
+ costHint: b.costHint,
107
+ blastRadius: b.blastRadius,
108
+ source: "claude-code",
109
+ })),
110
+ degradations: [],
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Skill plane: every `plugins/<plugin>/skills/*.md` under the agent repo AND the
116
+ * framework repo. A skill is reachable only when its plugin's marketplace is
117
+ * registered and the plugin is enabled — probed per plugin, not per skill.
118
+ *
119
+ * @param {object} o - { agentRoot, maestroRoot, at, repair? }
120
+ * @param {object} [deps] - fs + { homeDir }
121
+ */
122
+ export function enumerateSkills(o = {}, deps = {}) {
123
+ const { agentRoot, maestroRoot, at, repair = false } = o;
124
+ const ex = deps.existsSync || existsSync;
125
+ const rdd = deps.readdirSync || readdirSync;
126
+ const entries = [];
127
+ const degradations = [];
128
+ const repairs = [];
129
+ const seen = new Set();
130
+
131
+ for (const root of [agentRoot, maestroRoot].filter(Boolean)) {
132
+ const pluginsDir = join(root, "plugins");
133
+ if (!ex(pluginsDir)) continue;
134
+ let pluginIds = [];
135
+ try {
136
+ pluginIds = rdd(pluginsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
137
+ } catch (e) {
138
+ degradations.push({ plane: "skill", id: pluginsDir, reason: `unreadable plugins dir: ${msg(e)}` });
139
+ continue;
140
+ }
141
+ for (const pluginId of pluginIds) {
142
+ const pluginDir = join(pluginsDir, pluginId);
143
+ const probe = probeSkillPlugin({ pluginDir, pluginId, repair }, deps);
144
+ if (Array.isArray(probe.repaired) && probe.repaired.length) repairs.push(...probe.repaired);
145
+ if (!probe.ok) {
146
+ degradations.push({ plane: "skill", id: `plugin:${pluginId}`, reason: `${probe.degraded}: ${probe.detail}` });
147
+ }
148
+ const skillsDir = join(pluginDir, "skills");
149
+ if (!ex(skillsDir)) continue;
150
+ let files = [];
151
+ try {
152
+ files = rdd(skillsDir).filter((f) => f.endsWith(".md")).sort();
153
+ } catch (e) {
154
+ degradations.push({ plane: "skill", id: `plugin:${pluginId}`, reason: `unreadable skills dir: ${msg(e)}` });
155
+ continue;
156
+ }
157
+ for (const f of files) {
158
+ const name = f.replace(/\.md$/, "");
159
+ const id = `skill:${pluginId}:${name}`;
160
+ if (seen.has(id)) continue;
161
+ seen.add(id);
162
+ entries.push({
163
+ id,
164
+ plane: "skill",
165
+ kind: "skill",
166
+ invocation: `Skill(${pluginId}:${name})`,
167
+ reachable: probe.ok,
168
+ probedAt: at,
169
+ probe: { name: "marketplace+enabledPlugins", ok: probe.ok, detail: probe.detail },
170
+ costHint: "session",
171
+ blastRadius: "internal",
172
+ source: root === agentRoot ? "agent-repo" : "framework",
173
+ });
174
+ }
175
+ }
176
+ }
177
+ return { entries, degradations, repairs };
178
+ }
179
+
180
+ /**
181
+ * Skill plane, archetype leg: the function's recurring + event-driven workflows.
182
+ * These are procedures the agent is expected to run; they are reachable as soon
183
+ * as the capability pack resolved (they are prompts, not credentials).
184
+ * @param {object} o - { surface, at } surface = lib/capability.resolveCapabilitySurface()
185
+ */
186
+ export function enumerateWorkflows(o = {}) {
187
+ const { surface, at } = o;
188
+ const list = (surface && Array.isArray(surface.workflows) ? surface.workflows : []).filter((w) => w && w.id);
189
+ return {
190
+ entries: list.map((w) => ({
191
+ id: `workflow:${w.id}`,
192
+ plane: "skill",
193
+ kind: "workflow",
194
+ invocation: `workflows/${w.id}`,
195
+ reachable: true,
196
+ probedAt: at,
197
+ probe: { name: "capability-pack", ok: true, detail: `archetype workflow (${w.cadence || "event-driven"})` },
198
+ costHint: "session",
199
+ blastRadius: "internal",
200
+ source: "archetype",
201
+ cadence: w.cadence || null,
202
+ purpose: w.purpose || w.name || "",
203
+ })),
204
+ degradations: [],
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Skill plane, sub-agent leg: `agents/<id>/agent.md` on disk.
210
+ * @param {object} o - { agentRoot, at }
211
+ */
212
+ export function enumerateSubagents(o = {}, deps = {}) {
213
+ const { agentRoot, at } = o;
214
+ const ex = deps.existsSync || existsSync;
215
+ const rdd = deps.readdirSync || readdirSync;
216
+ const dir = join(agentRoot, "agents");
217
+ if (!ex(dir)) return { entries: [], degradations: [] };
218
+ let ids = [];
219
+ try {
220
+ ids = rdd(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
221
+ } catch (e) {
222
+ return { entries: [], degradations: [{ plane: "skill", id: "agents/", reason: `unreadable agents dir: ${msg(e)}` }] };
223
+ }
224
+ const entries = [];
225
+ for (const id of ids) {
226
+ if (!ex(join(dir, id, "agent.md"))) continue;
227
+ entries.push({
228
+ id: `subagent:${id}`,
229
+ plane: "skill",
230
+ kind: "subagent",
231
+ invocation: `Task(subagent_type: ${id})`,
232
+ reachable: true,
233
+ probedAt: at,
234
+ probe: { name: "agent.md-on-disk", ok: true, detail: `agents/${id}/agent.md` },
235
+ costHint: "session",
236
+ blastRadius: "internal",
237
+ source: "agent-repo",
238
+ });
239
+ }
240
+ return { entries, degradations: [] };
241
+ }
242
+
243
+ /**
244
+ * MCP plane: `.mcp.json` is the truth of what Claude Code will connect to;
245
+ * `config/mcp-servers.yaml` is the role's declared wish-list. A wished-for
246
+ * server that is NOT in `.mcp.json` is inventoried as unreachable with the
247
+ * reason spelled out, which is precisely the gap the old setup swallowed.
248
+ *
249
+ * @param {object} o - { agentRoot, at, env?, mcpJson?, wishlist? }
250
+ * @param {object} [deps] - fs + { handshake }
251
+ */
252
+ export function enumerateMcp(o = {}, deps = {}) {
253
+ const { agentRoot, at, env = process.env } = o;
254
+ const declaredDoc = o.mcpJson !== undefined ? o.mcpJson : readJsonSafe(join(agentRoot, ".mcp.json"), null, deps);
255
+ const declared = declaredDoc && declaredDoc.mcpServers && typeof declaredDoc.mcpServers === "object" ? declaredDoc.mcpServers : {};
256
+ const wishlist = Array.isArray(o.wishlist) ? o.wishlist : [];
257
+ const degradations = [];
258
+ if (!declaredDoc) degradations.push({ plane: "mcp", id: ".mcp.json", reason: "absent or unparseable — no MCP servers will be reachable" });
259
+
260
+ const names = new Set([...Object.keys(declared), ...wishlist.map((w) => w && w.id).filter(Boolean)]);
261
+ const entries = [];
262
+ for (const name of [...names].sort()) {
263
+ const wish = wishlist.find((w) => w && w.id === name) || null;
264
+ const probe = probeMcpServer(
265
+ {
266
+ name,
267
+ entry: declared[name] || null,
268
+ declared: Object.prototype.hasOwnProperty.call(declared, name),
269
+ requiredEnv: (wish && Array.isArray(wish.env) ? wish.env : []),
270
+ env,
271
+ },
272
+ deps,
273
+ );
274
+ if (!probe.ok) degradations.push({ plane: "mcp", id: `mcp:${name}`, reason: `${probe.degraded}: ${probe.detail}` });
275
+ entries.push({
276
+ id: `mcp:${name}`,
277
+ plane: "mcp",
278
+ kind: "mcp_server",
279
+ invocation: `mcp__${name}__*`,
280
+ reachable: probe.ok,
281
+ probedAt: at,
282
+ probe: { name: "declaration+credentials", ok: probe.ok, detail: probe.detail },
283
+ costHint: "cheap",
284
+ blastRadius: "external",
285
+ source: Object.prototype.hasOwnProperty.call(declared, name) ? ".mcp.json" : "config/mcp-servers.yaml",
286
+ optional: wish ? wish.optional !== false : false,
287
+ purpose: (wish && wish.purpose) || "",
288
+ });
289
+ }
290
+ return { entries, degradations };
291
+ }
292
+
293
+ /**
294
+ * Org plane: the curated protocol tools + the granted integration tools.
295
+ * Reachability of the curated set is one round-trip for the whole plane (they
296
+ * all ride the same transport + credential); granted plugin tools are reachable
297
+ * iff cached on disk.
298
+ *
299
+ * @param {object} o - { agentRoot, at, orgTools, orgProbe }
300
+ * orgTools — array from lib/org/tool-surface.getOrgTools()
301
+ * orgProbe — the resolved probeOrgPlane() verdict
302
+ */
303
+ export function enumerateOrgTools(o = {}, deps = {}) {
304
+ const { agentRoot, at, orgTools = [], orgProbe = { ok: false, detail: "not probed", degraded: "org-unprobed" } } = o;
305
+ const degradations = [];
306
+ if (!orgProbe.ok) degradations.push({ plane: "org", id: "org:*", reason: `${orgProbe.degraded || "unreachable"}: ${orgProbe.detail}` });
307
+ else if (orgProbe.degraded) degradations.push({ plane: "org", id: "org:mandate", reason: `${orgProbe.degraded}: ${orgProbe.detail}` });
308
+
309
+ const entries = [];
310
+ const seen = new Set();
311
+ for (const t of orgTools) {
312
+ if (!t || typeof t.name !== "string" || seen.has(t.name)) continue;
313
+ seen.add(t.name);
314
+ const granted = !!t.integration;
315
+ entries.push({
316
+ id: t.name,
317
+ plane: "org",
318
+ kind: granted ? "integration_tool" : "method_tool",
319
+ invocation: t.name,
320
+ reachable: orgProbe.ok,
321
+ probedAt: at,
322
+ probe: { name: granted ? "toolset-cache" : "protocol-round-trip", ok: orgProbe.ok, detail: orgProbe.detail },
323
+ costHint: costHintOfOrgTool(t),
324
+ blastRadius: blastRadiusOfOrgTool(t),
325
+ source: granted ? `integration:${t.integration}` : "protocol",
326
+ method: (t.binding && t.binding.method) || null,
327
+ access: t.access || "read",
328
+ });
329
+ }
330
+ // Surface the granted-plugin-tool cache state explicitly, even when getOrgTools
331
+ // already merged them: an empty cache is a real (loggable) degradation.
332
+ const grant = probeGrantedPluginTools(agentRoot, deps);
333
+ if (!grant.ok) degradations.push({ plane: "org", id: "org:granted-plugin-tools", reason: `${grant.degraded}: ${grant.detail}` });
334
+
335
+ return { entries, degradations, toolsetVersion: grant.toolsetVersion };
336
+ }
337
+
338
+ // ---------------------------------------------------------------------------
339
+ // The build
340
+ // ---------------------------------------------------------------------------
341
+
342
+ /**
343
+ * Build the full capability manifest.
344
+ *
345
+ * Everything is injectable so this runs hermetically in tests:
346
+ * deps.orgToolsImpl() → array of org tool descriptors (default: getOrgTools)
347
+ * deps.orgProbeImpl() → Promise<probe verdict> (default: probeOrgPlane)
348
+ * deps.capabilitySurface() → Promise<surface> (default: lib/capability)
349
+ * deps.wishlistImpl() → array of {id, env[], optional} (default: config/mcp-servers.yaml)
350
+ * deps.now() → ISO string (default: new Date())
351
+ * deps.logImpl(msg) → degradation logger (default: console.warn)
352
+ *
353
+ * @param {object} o - { agentRoot, maestroRoot, archetype?:{function,altitude}, repair?, env? }
354
+ * @param {object} [deps]
355
+ * @returns {Promise<object>} the manifest
356
+ */
357
+ export async function buildInventory(o = {}, deps = {}) {
358
+ const agentRoot = o.agentRoot || process.cwd();
359
+ const maestroRoot = o.maestroRoot || null;
360
+ const at = typeof deps.now === "function" ? deps.now() : new Date().toISOString();
361
+ const log = typeof deps.logImpl === "function" ? deps.logImpl : (m) => { try { console.warn(m); } catch { /* never throw from logging */ } };
362
+
363
+ const degradations = [];
364
+ const collect = (r) => {
365
+ if (!r) return [];
366
+ if (Array.isArray(r.degradations)) degradations.push(...r.degradations);
367
+ return Array.isArray(r.entries) ? r.entries : [];
368
+ };
369
+
370
+ const entries = [];
371
+ const repairs = [];
372
+
373
+ // ── builtin ──────────────────────────────────────────────────────────────
374
+ entries.push(...collect(enumerateBuiltins(at)));
375
+
376
+ // ── skill: plugin skills + archetype workflows + sub-agents ──────────────
377
+ const skillRes = enumerateSkills({ agentRoot, maestroRoot, at, repair: !!o.repair }, deps);
378
+ if (Array.isArray(skillRes.repairs)) repairs.push(...skillRes.repairs);
379
+ entries.push(...collect(skillRes));
380
+
381
+ let surface = null;
382
+ if (o.archetype && o.archetype.function && o.archetype.altitude) {
383
+ try {
384
+ surface = typeof deps.capabilitySurface === "function"
385
+ ? await deps.capabilitySurface(o.archetype)
386
+ : await (await import("../capability.mjs")).resolveCapabilitySurface(o.archetype);
387
+ } catch (e) {
388
+ degradations.push({ plane: "skill", id: "archetype:workflows", reason: `capability pack unresolved: ${msg(e)}` });
389
+ }
390
+ } else {
391
+ degradations.push({ plane: "skill", id: "archetype:workflows", reason: "no { function, altitude } supplied — archetype workflows not inventoried" });
392
+ }
393
+ entries.push(...collect(enumerateWorkflows({ surface, at })));
394
+ entries.push(...collect(enumerateSubagents({ agentRoot, at }, deps)));
395
+
396
+ // ── mcp ──────────────────────────────────────────────────────────────────
397
+ let wishlist = [];
398
+ try {
399
+ wishlist = typeof deps.wishlistImpl === "function" ? await deps.wishlistImpl(agentRoot) : await readMcpWishlist(agentRoot, deps);
400
+ } catch (e) {
401
+ degradations.push({ plane: "mcp", id: "config/mcp-servers.yaml", reason: `unreadable wish-list: ${msg(e)}` });
402
+ }
403
+ entries.push(...collect(enumerateMcp({ agentRoot, at, env: o.env || process.env, wishlist }, deps)));
404
+
405
+ // ── org ──────────────────────────────────────────────────────────────────
406
+ let orgTools = [];
407
+ try {
408
+ orgTools = typeof deps.orgToolsImpl === "function"
409
+ ? await deps.orgToolsImpl({ agentRoot })
410
+ : (await import("../org/tool-surface.mjs")).getOrgTools({ agentRoot });
411
+ } catch (e) {
412
+ degradations.push({ plane: "org", id: "org:tool-surface", reason: `tool surface unresolved: ${msg(e)}` });
413
+ }
414
+ let orgProbe;
415
+ try {
416
+ orgProbe = typeof deps.orgProbeImpl === "function" ? await deps.orgProbeImpl({ agentRoot }) : await defaultOrgProbe(agentRoot);
417
+ } catch (e) {
418
+ orgProbe = { ok: false, detail: `org probe threw: ${msg(e)}`, degraded: "org-probe-threw", mandateFamily: false };
419
+ }
420
+ const orgRes = enumerateOrgTools({ agentRoot, at, orgTools, orgProbe }, deps);
421
+ entries.push(...collect(orgRes));
422
+
423
+ // Stable order: plane (declaration order), then id.
424
+ const planeRank = new Map(PLANES.map((p, i) => [p, i]));
425
+ entries.sort((a, b) => (planeRank.get(a.plane) - planeRank.get(b.plane)) || a.id.localeCompare(b.id));
426
+
427
+ const counts = {};
428
+ for (const p of PLANES) counts[p] = entries.filter((e) => e.plane === p).length;
429
+ counts.reachable = entries.filter((e) => e.reachable).length;
430
+ counts.unreachable = entries.length - counts.reachable;
431
+
432
+ const manifest = {
433
+ schemaVersion: MANIFEST_VERSION,
434
+ generatedAt: at,
435
+ agentRoot,
436
+ archetype: o.archetype || null,
437
+ mandateFamily: !!(orgProbe && orgProbe.mandateFamily),
438
+ toolsetVersion: orgRes.toolsetVersion ?? null,
439
+ counts,
440
+ checksum: checksumEntries(entries),
441
+ entries,
442
+ degradations,
443
+ repairs,
444
+ };
445
+
446
+ // NEVER SILENT: every degradation is logged, once, with its plane + reason.
447
+ for (const d of degradations) log(`[capability-inventory] DEGRADED ${d.plane} ${d.id}: ${d.reason}`);
448
+ for (const r of repairs) log(`[capability-inventory] repaired: ${r}`);
449
+
450
+ return manifest;
451
+ }
452
+
453
+ /** sha256 over the identity-bearing fields only (id|plane|kind|reachable). */
454
+ export function checksumEntries(entries) {
455
+ const canon = (entries || [])
456
+ .map((e) => `${e.id}|${e.plane}|${e.kind}|${e.reachable ? 1 : 0}`)
457
+ .sort()
458
+ .join("\n");
459
+ return createHash("sha256").update(canon).digest("hex");
460
+ }
461
+
462
+ /** Default org probe: read config/org.yaml, then round-trip via lib/org/client. */
463
+ async function defaultOrgProbe(agentRoot) {
464
+ const client = await import("../org/client.mjs");
465
+ const cfg = client.loadOrgConfig(agentRoot);
466
+ const enabled = client.isEnabled(cfg);
467
+ const o = client.configFromAgent(cfg);
468
+ return probeOrgPlane({
469
+ enabled,
470
+ call: (m, p) => client.call(m, p, o),
471
+ read: (path) => client.read(path, o),
472
+ });
473
+ }
474
+
475
+ /** Parse `config/mcp-servers.yaml` into [{id, env[], optional, purpose}]. */
476
+ export async function readMcpWishlist(agentRoot, deps = {}) {
477
+ const ex = deps.existsSync || existsSync;
478
+ const rd = deps.readFileSync || readFileSync;
479
+ const p = join(agentRoot, "config", "mcp-servers.yaml");
480
+ if (!ex(p)) return [];
481
+ const mod = await import("js-yaml");
482
+ const yaml = mod && mod.default ? mod.default : mod;
483
+ const doc = yaml.load(rd(p, "utf-8"));
484
+ const list = doc && Array.isArray(doc.mcpServers) ? doc.mcpServers : [];
485
+ return list
486
+ .filter((s) => s && s.id)
487
+ .map((s) => ({ id: String(s.id), env: Array.isArray(s.env) ? s.env : [], optional: s.optional !== false, purpose: s.purpose || "" }));
488
+ }
489
+
490
+ // ---------------------------------------------------------------------------
491
+ // Persistence + query
492
+ // ---------------------------------------------------------------------------
493
+
494
+ /** Absolute path of the manifest for an agent root. */
495
+ export function manifestPath(agentRoot) {
496
+ return join(agentRoot, MANIFEST_REL);
497
+ }
498
+
499
+ /** Atomically write the manifest. @returns {string} the path written */
500
+ export function writeManifest(agentRoot, manifest, deps = {}) {
501
+ const wr = deps.writeFileSync || writeFileSync;
502
+ const mk = deps.mkdirSync || mkdirSync;
503
+ const mv = deps.renameSync || renameSync;
504
+ const p = manifestPath(agentRoot);
505
+ mk(join(agentRoot, "config"), { recursive: true });
506
+ const tmp = `${p}.${process.pid}.tmp`;
507
+ wr(tmp, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
508
+ mv(tmp, p);
509
+ return p;
510
+ }
511
+
512
+ /** Read the manifest (null when absent/unparseable). */
513
+ export function readManifest(agentRoot, deps = {}) {
514
+ return readJsonSafe(manifestPath(agentRoot), null, deps);
515
+ }
516
+
517
+ /** Set of ids that are reachable — the gate `lib/plan/compile.mjs` enforces. */
518
+ export function reachableIds(manifest) {
519
+ const out = new Set();
520
+ for (const e of (manifest && manifest.entries) || []) if (e && e.reachable) out.add(e.id);
521
+ return out;
522
+ }
523
+
524
+ /** Look up one capability entry by id (undefined when unknown). */
525
+ export function findCapability(manifest, id) {
526
+ return ((manifest && manifest.entries) || []).find((e) => e && e.id === id);
527
+ }
528
+
529
+ /** All entries on a plane (optionally reachable-only). */
530
+ export function byPlane(manifest, plane, { reachableOnly = false } = {}) {
531
+ return ((manifest && manifest.entries) || []).filter((e) => e && e.plane === plane && (!reachableOnly || e.reachable));
532
+ }
533
+
534
+ /** @param {*} e @returns {string} */
535
+ function msg(e) {
536
+ return e && e.message ? String(e.message) : String(e);
537
+ }
538
+
539
+ export default {
540
+ MANIFEST_REL, MANIFEST_VERSION, BUILTIN_TOOLS, buildInventory, writeManifest, readManifest,
541
+ manifestPath, reachableIds, findCapability, byPlane, checksumEntries, readMcpWishlist,
542
+ };