@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.
- 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/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 +58 -22
- package/lib/org/client.test.mjs +3 -1
- 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 +17 -2
- package/lib/org/messaging.mjs +40 -4
- package/lib/org/messaging.test.mjs +40 -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 +105 -17
- package/lib/setup/enroll-from-cohort.test.mjs +68 -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 +8 -3
- 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 +36 -4
- package/scripts/daemon/cadence-handlers.mjs +145 -1
- 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/prompt-builder.mjs +41 -1
- 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/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,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
|
+
};
|