@cohortapp/agent-sdk 2.3.2 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +84 -16
- package/lib/setup/enroll-from-cohort.test.mjs +43 -1
- package/lib/setup/sections/identity.mjs +15 -4
- package/lib/setup/sections/identity.test.mjs +94 -0
- package/lib/setup/sections/inventory.mjs +178 -0
- package/lib/setup/sections/inventory.test.mjs +198 -0
- package/lib/setup/sections/mandate.mjs +392 -0
- package/lib/setup/sections/mandate.test.mjs +373 -0
- package/lib/setup/sections/subagents.mjs +427 -0
- package/lib/setup/sections/subagents.test.mjs +429 -0
- package/lib/setup/sections/verify.mjs +121 -0
- package/lib/setup/sections/verify.test.mjs +175 -0
- package/lib/setup/sot.mjs +2 -0
- package/lib/subagents/cli.mjs +463 -0
- package/lib/subagents/cli.test.mjs +389 -0
- package/lib/subagents/client.mjs +373 -0
- package/lib/subagents/client.test.mjs +309 -0
- package/lib/subagents/gap.mjs +268 -0
- package/lib/subagents/gap.test.mjs +234 -0
- package/lib/subagents/lock.mjs +296 -0
- package/lib/subagents/lock.test.mjs +248 -0
- package/lib/subagents/manifest.mjs +224 -0
- package/lib/subagents/manifest.test.mjs +175 -0
- package/lib/subagents/refs.mjs +274 -0
- package/lib/subagents/refs.test.mjs +204 -0
- package/lib/subagents/resolve.mjs +455 -0
- package/lib/subagents/resolve.test.mjs +422 -0
- package/lib/subagents/schema.mjs +467 -0
- package/lib/subagents/schema.test.mjs +306 -0
- package/package.json +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,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inventory.test.mjs — the capability inventory (SPEC §5.1).
|
|
3
|
+
* Run: node --test lib/capability/inventory.test.mjs
|
|
4
|
+
*
|
|
5
|
+
* Everything external is injected: a fake HOME for ~/.claude/settings.json, a
|
|
6
|
+
* fake org tool table, a fake org probe, a fake capability surface, a fake
|
|
7
|
+
* clock. No network, no real settings file, no real MCP spawn.
|
|
8
|
+
*/
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
import { test } from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
buildInventory, writeManifest, readManifest, reachableIds, findCapability, byPlane,
|
|
19
|
+
checksumEntries, blastRadiusOfOrgTool, enumerateSkills, enumerateMcp, MANIFEST_REL,
|
|
20
|
+
} from "./inventory.mjs";
|
|
21
|
+
import { probeSkillPlugin, probeMcpServer, probeOrgPlane, probeGrantedPluginTools } from "./probe.mjs";
|
|
22
|
+
|
|
23
|
+
const AT = "2026-08-11T09:00:00.000Z";
|
|
24
|
+
|
|
25
|
+
/** A tmp agent repo + a tmp HOME, so nothing touches the real machine. */
|
|
26
|
+
function fixture({ withMarketplace = false, withEnabled = false, mcp = { cohort: { command: "cohort-mcp" } }, skills = ["morning-brief", "inbox-triage"] } = {}) {
|
|
27
|
+
const root = mkdtempSync(join(tmpdir(), "maestro-inv-"));
|
|
28
|
+
const home = mkdtempSync(join(tmpdir(), "maestro-home-"));
|
|
29
|
+
mkdirSync(join(root, "config"), { recursive: true });
|
|
30
|
+
|
|
31
|
+
if (skills) {
|
|
32
|
+
mkdirSync(join(root, "plugins", "agent-skills", "skills"), { recursive: true });
|
|
33
|
+
for (const s of skills) writeFileSync(join(root, "plugins", "agent-skills", "skills", `${s}.md`), `# ${s}\n`);
|
|
34
|
+
}
|
|
35
|
+
if (withMarketplace) {
|
|
36
|
+
mkdirSync(join(root, "plugins", "agent-skills", ".claude-plugin"), { recursive: true });
|
|
37
|
+
writeFileSync(join(root, "plugins", "agent-skills", ".claude-plugin", "marketplace.json"), JSON.stringify({ name: "agent-skills", plugins: [] }));
|
|
38
|
+
}
|
|
39
|
+
if (withEnabled) {
|
|
40
|
+
mkdirSync(join(home, ".claude"), { recursive: true });
|
|
41
|
+
writeFileSync(join(home, ".claude", "settings.json"), JSON.stringify({ enabledPlugins: { "agent-skills@agent-skills": true } }));
|
|
42
|
+
}
|
|
43
|
+
if (mcp) writeFileSync(join(root, ".mcp.json"), JSON.stringify({ mcpServers: mcp }));
|
|
44
|
+
return { root, home };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const FAKE_ORG_TOOLS = [
|
|
48
|
+
{ name: "board_ready", access: "read", binding: { kind: "rpc", method: "board.ready" } },
|
|
49
|
+
{ name: "task_create", access: "write", binding: { kind: "rpc", method: "task.create" } },
|
|
50
|
+
{ name: "messaging_send", access: "write", outbound: true, binding: { kind: "rpc", method: "messaging.send" } },
|
|
51
|
+
{ name: "books_invoice_send", access: "write", binding: { kind: "rpc", method: "books.invoiceSend" } },
|
|
52
|
+
{ name: "linear_create_issue", access: "write", integration: "linear", binding: { kind: "rpc" } },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
function deps({ home, orgOk = true, mandateFamily = true, logs = [] }) {
|
|
56
|
+
return {
|
|
57
|
+
homeDir: home,
|
|
58
|
+
now: () => AT,
|
|
59
|
+
logImpl: (m) => logs.push(m),
|
|
60
|
+
orgToolsImpl: async () => FAKE_ORG_TOOLS,
|
|
61
|
+
orgProbeImpl: async () => (orgOk
|
|
62
|
+
? { ok: true, detail: "mandate.version round-trip ok", mandateFamily }
|
|
63
|
+
: { ok: false, detail: "org.cohort is not enabled", degraded: "org-not-enrolled", mandateFamily: false }),
|
|
64
|
+
capabilitySurface: async () => ({ workflows: [{ id: "weekly-operating-review", cadence: "weekly", purpose: "run the operating cadence" }] }),
|
|
65
|
+
wishlistImpl: async () => ([
|
|
66
|
+
{ id: "cohort", env: [], optional: false, purpose: "org spine" },
|
|
67
|
+
{ id: "notion", env: ["NOTION_API_KEY"], optional: true, purpose: "decision log" },
|
|
68
|
+
]),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
test("buildInventory: enumerates all four planes and marks unregistered skills UNREACHABLE", async () => {
|
|
73
|
+
const { root, home } = fixture();
|
|
74
|
+
const logs = [];
|
|
75
|
+
const m = await buildInventory({ agentRoot: root, archetype: { function: "executive-operator", altitude: "c-suite" } }, deps({ home, logs }));
|
|
76
|
+
|
|
77
|
+
assert.equal(m.schemaVersion, 1);
|
|
78
|
+
assert.equal(m.generatedAt, AT);
|
|
79
|
+
assert.ok(m.counts.builtin > 5, "builtins inventoried");
|
|
80
|
+
assert.ok(m.counts.skill >= 3, "2 skill files + 1 workflow");
|
|
81
|
+
assert.ok(m.counts.mcp >= 2, "cohort (declared) + notion (wished-for)");
|
|
82
|
+
assert.equal(m.counts.org, FAKE_ORG_TOOLS.length);
|
|
83
|
+
|
|
84
|
+
// The whole point: skills whose marketplace is unregistered are NOT reachable.
|
|
85
|
+
const skill = findCapability(m, "skill:agent-skills:morning-brief");
|
|
86
|
+
assert.ok(skill, "skill discovered");
|
|
87
|
+
assert.equal(skill.reachable, false);
|
|
88
|
+
assert.match(skill.probe.detail, /marketplace/);
|
|
89
|
+
|
|
90
|
+
// ...and it is LOGGED, not swallowed.
|
|
91
|
+
assert.ok(logs.some((l) => /DEGRADED skill plugin:agent-skills/.test(l)), `expected a logged degradation, got: ${logs.join(" | ")}`);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("buildInventory: registered + enabled marketplace makes skills reachable", async () => {
|
|
95
|
+
const { root, home } = fixture({ withMarketplace: true, withEnabled: true });
|
|
96
|
+
const m = await buildInventory({ agentRoot: root, archetype: { function: "executive-operator", altitude: "c-suite" } }, deps({ home }));
|
|
97
|
+
const skill = findCapability(m, "skill:agent-skills:inbox-triage");
|
|
98
|
+
assert.equal(skill.reachable, true);
|
|
99
|
+
assert.ok(reachableIds(m).has("skill:agent-skills:inbox-triage"));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("repair:true registers the marketplace and enables the plugin (rung 1 stops being a lie)", async () => {
|
|
103
|
+
const { root, home } = fixture();
|
|
104
|
+
const logs = [];
|
|
105
|
+
const m = await buildInventory({ agentRoot: root, repair: true, archetype: { function: "executive-operator", altitude: "c-suite" } }, deps({ home, logs }));
|
|
106
|
+
assert.ok(existsSync(join(root, "plugins", "agent-skills", ".claude-plugin", "marketplace.json")), "marketplace written");
|
|
107
|
+
const settings = JSON.parse(readFileSync(join(home, ".claude", "settings.json"), "utf8"));
|
|
108
|
+
assert.ok(Object.keys(settings.enabledPlugins).some((k) => k.startsWith("agent-skills")), "plugin enabled");
|
|
109
|
+
assert.equal(findCapability(m, "skill:agent-skills:morning-brief").reachable, true);
|
|
110
|
+
assert.ok(m.repairs.length >= 2 && logs.some((l) => /repaired/.test(l)));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("mcp plane: wished-for server absent from .mcp.json is unreachable with a stated reason", async () => {
|
|
114
|
+
const { root, home } = fixture();
|
|
115
|
+
const m = await buildInventory({ agentRoot: root, archetype: { function: "executive-operator", altitude: "c-suite" } }, deps({ home }));
|
|
116
|
+
const notion = findCapability(m, "mcp:notion");
|
|
117
|
+
assert.equal(notion.reachable, false);
|
|
118
|
+
assert.match(notion.probe.detail, /absent from \.mcp\.json|credential env/);
|
|
119
|
+
const cohort = findCapability(m, "mcp:cohort");
|
|
120
|
+
assert.equal(cohort.reachable, true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("org plane: an un-enrolled agent inventories the tools but NONE are reachable", async () => {
|
|
124
|
+
const { root, home } = fixture();
|
|
125
|
+
const logs = [];
|
|
126
|
+
const m = await buildInventory({ agentRoot: root, archetype: { function: "executive-operator", altitude: "c-suite" } }, deps({ home, orgOk: false, logs }));
|
|
127
|
+
const org = byPlane(m, "org");
|
|
128
|
+
assert.equal(org.length, FAKE_ORG_TOOLS.length);
|
|
129
|
+
assert.equal(org.filter((e) => e.reachable).length, 0);
|
|
130
|
+
assert.equal(m.mandateFamily, false);
|
|
131
|
+
assert.ok(logs.some((l) => /org-not-enrolled/.test(l)));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("blast radius: outbound → external, financial verbs → financial, reads → internal", () => {
|
|
135
|
+
assert.equal(blastRadiusOfOrgTool({ name: "messaging_send", access: "write", outbound: true }), "external");
|
|
136
|
+
assert.equal(blastRadiusOfOrgTool({ name: "books_invoice_send", access: "write" }), "financial");
|
|
137
|
+
assert.equal(blastRadiusOfOrgTool({ name: "board_ready", access: "read" }), "internal");
|
|
138
|
+
assert.equal(blastRadiusOfOrgTool({ name: "task_create", access: "write" }), "irreversible");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("determinism: checksum is stable across runs and ignores probedAt", async () => {
|
|
142
|
+
const { root, home } = fixture({ withMarketplace: true, withEnabled: true });
|
|
143
|
+
const a = await buildInventory({ agentRoot: root, archetype: { function: "executive-operator", altitude: "c-suite" } }, deps({ home }));
|
|
144
|
+
const b = await buildInventory({ agentRoot: root, archetype: { function: "executive-operator", altitude: "c-suite" } }, { ...deps({ home }), now: () => "2027-01-01T00:00:00.000Z" });
|
|
145
|
+
assert.equal(a.checksum, b.checksum, "same capabilities → same checksum despite a different clock");
|
|
146
|
+
assert.notEqual(a.generatedAt, b.generatedAt);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("checksum CHANGES when a capability's reachability flips", () => {
|
|
150
|
+
const base = [{ id: "x", plane: "org", kind: "method_tool", reachable: true }];
|
|
151
|
+
const flipped = [{ id: "x", plane: "org", kind: "method_tool", reachable: false }];
|
|
152
|
+
assert.notEqual(checksumEntries(base), checksumEntries(flipped));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("write/read round-trip is atomic and queryable", async () => {
|
|
156
|
+
const { root, home } = fixture();
|
|
157
|
+
const m = await buildInventory({ agentRoot: root, archetype: { function: "executive-operator", altitude: "c-suite" } }, deps({ home }));
|
|
158
|
+
const p = writeManifest(root, m);
|
|
159
|
+
assert.equal(p, join(root, MANIFEST_REL));
|
|
160
|
+
const back = readManifest(root);
|
|
161
|
+
assert.equal(back.checksum, m.checksum);
|
|
162
|
+
assert.equal(byPlane(back, "builtin").length, byPlane(m, "builtin").length);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("degradation is recorded when no archetype is supplied (never silent)", async () => {
|
|
166
|
+
const { root, home } = fixture();
|
|
167
|
+
const logs = [];
|
|
168
|
+
const m = await buildInventory({ agentRoot: root }, deps({ home, logs }));
|
|
169
|
+
assert.ok(m.degradations.some((d) => /archetype/.test(d.id)));
|
|
170
|
+
assert.ok(logs.some((l) => /archetype:workflows/.test(l)));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ── probe-level units ────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
test("probeMcpServer: missing credential env fails closed with the key named", () => {
|
|
176
|
+
const r = probeMcpServer({ name: "notion", declared: true, requiredEnv: ["NOTION_API_KEY"], env: {} });
|
|
177
|
+
assert.equal(r.ok, false);
|
|
178
|
+
assert.equal(r.degraded, "mcp-credential-missing");
|
|
179
|
+
assert.match(r.detail, /NOTION_API_KEY/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("probeMcpServer: an injected handshake decides the verdict", () => {
|
|
183
|
+
const ok = probeMcpServer({ name: "cohort", declared: true, env: {} }, { handshake: () => ({ ok: true, detail: "hello" }) });
|
|
184
|
+
assert.equal(ok.ok, true);
|
|
185
|
+
const bad = probeMcpServer({ name: "cohort", declared: true, env: {} }, { handshake: () => { throw new Error("spawn ENOENT"); } });
|
|
186
|
+
assert.equal(bad.ok, false);
|
|
187
|
+
assert.equal(bad.degraded, "mcp-handshake-threw");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("probeOrgPlane: NOT_FOUND on mandate.version means transport LIVE, mandate family absent", async () => {
|
|
191
|
+
const r = await probeOrgPlane({ enabled: true, call: async () => ({ ok: false, error: { code: "NOT_FOUND" } }) });
|
|
192
|
+
assert.equal(r.ok, true, "the transport answered, so the plane is reachable");
|
|
193
|
+
assert.equal(r.mandateFamily, false);
|
|
194
|
+
assert.equal(r.degraded, "mandate-family-absent");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("probeOrgPlane: falls back to a snapshot read, and a dead server is unreachable", async () => {
|
|
198
|
+
const ok = await probeOrgPlane({ enabled: true, call: async () => ({ ok: false, error: { code: "BAD_REQUEST" } }), read: async () => ({ ok: true }) });
|
|
199
|
+
assert.equal(ok.ok, true);
|
|
200
|
+
const dead = await probeOrgPlane({ enabled: true, call: async () => ({ ok: false, error: { code: "BAD_REQUEST" } }), read: async () => ({ ok: false, status: 503 }) });
|
|
201
|
+
assert.equal(dead.ok, false);
|
|
202
|
+
assert.equal(dead.degraded, "org-unreachable");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("probeGrantedPluginTools: an absent toolset cache is reported, not swallowed", () => {
|
|
206
|
+
const { root } = fixture();
|
|
207
|
+
const r = probeGrantedPluginTools(root);
|
|
208
|
+
assert.equal(r.ok, false);
|
|
209
|
+
assert.equal(r.degraded, "toolset-absent");
|
|
210
|
+
mkdirSync(join(root, "state", "org"), { recursive: true });
|
|
211
|
+
writeFileSync(join(root, "state", "org", "toolset.json"), JSON.stringify({ version: 7, tools: [{ name: "linear_create_issue" }] }));
|
|
212
|
+
const r2 = probeGrantedPluginTools(root);
|
|
213
|
+
assert.equal(r2.ok, true);
|
|
214
|
+
assert.equal(r2.toolsetVersion, 7);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("probeSkillPlugin: a plugin dir with no marketplace fails closed and names the path", () => {
|
|
218
|
+
const { root, home } = fixture();
|
|
219
|
+
const r = probeSkillPlugin({ pluginDir: join(root, "plugins", "agent-skills"), pluginId: "agent-skills" }, { homeDir: home });
|
|
220
|
+
assert.equal(r.ok, false);
|
|
221
|
+
assert.equal(r.degraded, "marketplace-unregistered");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("enumerateSkills/enumerateMcp are pure over injected fs state", () => {
|
|
225
|
+
const { root, home } = fixture({ withMarketplace: true, withEnabled: true, skills: ["a", "b", "c"] });
|
|
226
|
+
const s = enumerateSkills({ agentRoot: root, at: AT }, { homeDir: home });
|
|
227
|
+
assert.equal(s.entries.length, 3);
|
|
228
|
+
assert.ok(s.entries.every((e) => e.plane === "skill" && e.reachable));
|
|
229
|
+
|
|
230
|
+
const m = enumerateMcp({ agentRoot: root, at: AT, env: {}, wishlist: [] }, {});
|
|
231
|
+
assert.deepEqual(m.entries.map((e) => e.id), ["mcp:cohort"]);
|
|
232
|
+
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/capability/probe.mjs — cheap REACHABILITY probes, one per capability plane.
|
|
3
|
+
*
|
|
4
|
+
* The inventory step (SPEC §5.1) is only worth anything if it reports what is
|
|
5
|
+
* actually *reachable*, not what is merely declared. A skill file on disk that
|
|
6
|
+
* belongs to an unregistered marketplace is not a capability; an MCP server in
|
|
7
|
+
* `.mcp.json` whose credential env is unset is not a capability; the 107 curated
|
|
8
|
+
* org tools are not capabilities until the org client is enrolled and answering.
|
|
9
|
+
*
|
|
10
|
+
* So every plane gets a probe:
|
|
11
|
+
*
|
|
12
|
+
* skill → is the plugin's marketplace registered AND the plugin enabled in
|
|
13
|
+
* ~/.claude/settings.json `enabledPlugins`? (optionally repaired)
|
|
14
|
+
* mcp → does the server declaration hand-shake (injected), or at minimum
|
|
15
|
+
* is it declared in .mcp.json with its required env present?
|
|
16
|
+
* org → does a protocol round-trip come back? (`mandate.version`, falling
|
|
17
|
+
* back to a plain read, so this works before the mandate family ships)
|
|
18
|
+
* plugin → is the granted tool present in state/org/toolset.json?
|
|
19
|
+
*
|
|
20
|
+
* DOCTRINE: fail-open but NEVER silent. Every probe returns
|
|
21
|
+
* `{ ok, detail, degraded? }` and never throws; a degradation is returned to the
|
|
22
|
+
* caller so the inventory can log it and record `reachable:false` — which is the
|
|
23
|
+
* whole point, since "an entry with reachable:false may not be cited by any
|
|
24
|
+
* obligation".
|
|
25
|
+
*
|
|
26
|
+
* Pure-ish + injectable: every side effect (fs, network, clock) arrives through
|
|
27
|
+
* `deps` so the tests run hermetically with fakes.
|
|
28
|
+
*
|
|
29
|
+
* @module lib/capability/probe
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
"use strict";
|
|
33
|
+
|
|
34
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
35
|
+
import { join, dirname } from "node:path";
|
|
36
|
+
import { homedir } from "node:os";
|
|
37
|
+
|
|
38
|
+
/** The four capability planes an agent can act through. */
|
|
39
|
+
export const PLANES = Object.freeze(["builtin", "skill", "mcp", "org"]);
|
|
40
|
+
|
|
41
|
+
/** Probe verdict helper. @returns {{ok:boolean, detail:string, degraded?:string}} */
|
|
42
|
+
function verdict(ok, detail, degraded) {
|
|
43
|
+
const v = { ok: !!ok, detail: String(detail || "") };
|
|
44
|
+
if (degraded) v.degraded = String(degraded);
|
|
45
|
+
return v;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Read + parse a JSON file; returns `fallback` (never throws). */
|
|
49
|
+
export function readJsonSafe(path, fallback = null, deps = {}) {
|
|
50
|
+
const ex = deps.existsSync || existsSync;
|
|
51
|
+
const rd = deps.readFileSync || readFileSync;
|
|
52
|
+
try {
|
|
53
|
+
if (!ex(path)) return fallback;
|
|
54
|
+
return JSON.parse(rd(path, "utf-8"));
|
|
55
|
+
} catch {
|
|
56
|
+
return fallback;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Plane: skill (Claude Code plugins + their skills)
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Path to the Claude Code user settings that carries `enabledPlugins`.
|
|
66
|
+
* @param {object} [deps] - { homeDir }
|
|
67
|
+
*/
|
|
68
|
+
export function claudeSettingsPath(deps = {}) {
|
|
69
|
+
return join(deps.homeDir || homedir(), ".claude", "settings.json");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Probe the skill plane: a plugin's skills are reachable only when the plugin's
|
|
74
|
+
* marketplace manifest exists AND the plugin is listed in `enabledPlugins`.
|
|
75
|
+
*
|
|
76
|
+
* With `repair:true` this *fixes* both halves (writes the missing
|
|
77
|
+
* `.claude-plugin/marketplace.json`, appends the enabledPlugins entry) — SPEC
|
|
78
|
+
* §5.1 explicitly allows the probe to register the marketplace, because rung 1
|
|
79
|
+
* of the execution ladder is a lie until it is.
|
|
80
|
+
*
|
|
81
|
+
* @param {object} o
|
|
82
|
+
* @param {string} o.pluginDir absolute path to the plugin directory
|
|
83
|
+
* @param {string} o.pluginId the plugin id (directory basename)
|
|
84
|
+
* @param {boolean} [o.repair=false] write the marketplace manifest / enable entry
|
|
85
|
+
* @param {object} [deps] - { existsSync, readFileSync, writeFileSync, mkdirSync, homeDir }
|
|
86
|
+
* @returns {{ok:boolean, detail:string, degraded?:string, repaired?:string[]}}
|
|
87
|
+
*/
|
|
88
|
+
export function probeSkillPlugin(o = {}, deps = {}) {
|
|
89
|
+
const ex = deps.existsSync || existsSync;
|
|
90
|
+
const rd = deps.readFileSync || readFileSync;
|
|
91
|
+
const wr = deps.writeFileSync || writeFileSync;
|
|
92
|
+
const mk = deps.mkdirSync || mkdirSync;
|
|
93
|
+
const { pluginDir, pluginId, repair = false } = o;
|
|
94
|
+
if (!pluginDir || !pluginId) return verdict(false, "probeSkillPlugin: pluginDir + pluginId required", "bad-input");
|
|
95
|
+
|
|
96
|
+
const repaired = [];
|
|
97
|
+
const marketplacePath = join(pluginDir, ".claude-plugin", "marketplace.json");
|
|
98
|
+
let marketplaceOk = ex(marketplacePath);
|
|
99
|
+
if (!marketplaceOk && repair) {
|
|
100
|
+
try {
|
|
101
|
+
mk(dirname(marketplacePath), { recursive: true });
|
|
102
|
+
const manifest = {
|
|
103
|
+
name: pluginId,
|
|
104
|
+
owner: { name: "maestro" },
|
|
105
|
+
plugins: [{ name: pluginId, source: "./", description: `maestro ${pluginId} skill pack` }],
|
|
106
|
+
};
|
|
107
|
+
wr(marketplacePath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
108
|
+
marketplaceOk = true;
|
|
109
|
+
repaired.push(`wrote ${marketplacePath}`);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
return { ...verdict(false, `marketplace manifest missing and could not be written: ${msg(e)}`, "marketplace-unwritable"), repaired };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!marketplaceOk) {
|
|
115
|
+
return { ...verdict(false, `no .claude-plugin/marketplace.json under ${pluginDir}`, "marketplace-unregistered"), repaired };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const settingsPath = claudeSettingsPath(deps);
|
|
119
|
+
let settings = readJsonSafe(settingsPath, null, { existsSync: ex, readFileSync: rd });
|
|
120
|
+
const enabled = settings && settings.enabledPlugins && typeof settings.enabledPlugins === "object"
|
|
121
|
+
? settings.enabledPlugins
|
|
122
|
+
: null;
|
|
123
|
+
const keyed = enabled ? Object.keys(enabled).some((k) => k === pluginId || k.startsWith(`${pluginId}@`)) : false;
|
|
124
|
+
const on = keyed && Object.entries(enabled).some(([k, v]) => (k === pluginId || k.startsWith(`${pluginId}@`)) && v !== false);
|
|
125
|
+
|
|
126
|
+
if (!on && repair) {
|
|
127
|
+
try {
|
|
128
|
+
settings = settings && typeof settings === "object" ? settings : {};
|
|
129
|
+
settings.enabledPlugins = settings.enabledPlugins && typeof settings.enabledPlugins === "object" ? settings.enabledPlugins : {};
|
|
130
|
+
settings.enabledPlugins[`${pluginId}@${pluginId}`] = true;
|
|
131
|
+
mk(dirname(settingsPath), { recursive: true });
|
|
132
|
+
wr(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
133
|
+
repaired.push(`enabled ${pluginId} in ${settingsPath}`);
|
|
134
|
+
return { ...verdict(true, "marketplace registered + plugin enabled (repaired)"), repaired };
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return { ...verdict(false, `plugin not enabled and settings unwritable: ${msg(e)}`, "settings-unwritable"), repaired };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!on) {
|
|
140
|
+
return { ...verdict(false, `plugin "${pluginId}" not in enabledPlugins (${settingsPath})`, "plugin-disabled"), repaired };
|
|
141
|
+
}
|
|
142
|
+
return { ...verdict(true, "marketplace registered + plugin enabled"), repaired };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Plane: mcp
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Probe one MCP server. The default probe is declaration + credential presence
|
|
151
|
+
* (cheap, offline, deterministic). A real hand-shake can be injected as
|
|
152
|
+
* `deps.handshake(name, entry) -> {ok, detail}` — the inventory passes one only
|
|
153
|
+
* when the operator asked for a live probe, because spawning every MCP server
|
|
154
|
+
* during setup is neither cheap nor safe.
|
|
155
|
+
*
|
|
156
|
+
* @param {object} o - { name, entry, declared, requiredEnv?, env? }
|
|
157
|
+
* @param {object} [deps] - { handshake }
|
|
158
|
+
* @returns {{ok:boolean, detail:string, degraded?:string}}
|
|
159
|
+
*/
|
|
160
|
+
export function probeMcpServer(o = {}, deps = {}) {
|
|
161
|
+
const { name, entry, declared, requiredEnv = [], env = process.env } = o;
|
|
162
|
+
if (!name) return verdict(false, "probeMcpServer: name required", "bad-input");
|
|
163
|
+
if (!declared) return verdict(false, `"${name}" is declared in config/mcp-servers.yaml but absent from .mcp.json`, "mcp-undeclared");
|
|
164
|
+
|
|
165
|
+
const missingEnv = (requiredEnv || []).filter((k) => !env || !env[k]);
|
|
166
|
+
if (missingEnv.length) {
|
|
167
|
+
return verdict(false, `"${name}" missing credential env: ${missingEnv.join(", ")}`, "mcp-credential-missing");
|
|
168
|
+
}
|
|
169
|
+
if (typeof deps.handshake === "function") {
|
|
170
|
+
try {
|
|
171
|
+
const r = deps.handshake(name, entry);
|
|
172
|
+
if (r && typeof r.then === "function") return verdict(false, `"${name}" handshake returned a promise; use probeMcpServerAsync`, "mcp-handshake-async");
|
|
173
|
+
return r && r.ok ? verdict(true, r.detail || "handshake ok") : verdict(false, (r && r.detail) || "handshake failed", "mcp-handshake-failed");
|
|
174
|
+
} catch (e) {
|
|
175
|
+
return verdict(false, `"${name}" handshake threw: ${msg(e)}`, "mcp-handshake-threw");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return verdict(true, `"${name}" declared in .mcp.json with credentials present`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Plane: org (protocol methods + granted integration tools)
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Probe the org plane with one round-trip. Tries `mandate.version` first (the
|
|
187
|
+
* cheap ETag poll the SPEC defines); an `unknown method` reply still proves the
|
|
188
|
+
* transport + credential are live, so it counts as reachable-but-old-server and
|
|
189
|
+
* is reported as such. Falls back to a `snapshot` read.
|
|
190
|
+
*
|
|
191
|
+
* @param {object} o - { enabled, call?, read? } (call/read are lib/org/client shapes)
|
|
192
|
+
* @returns {Promise<{ok:boolean, detail:string, degraded?:string, mandateFamily:boolean}>}
|
|
193
|
+
*/
|
|
194
|
+
export async function probeOrgPlane(o = {}) {
|
|
195
|
+
const { enabled, call, read } = o;
|
|
196
|
+
if (!enabled) return { ...verdict(false, "org.cohort is not enabled in config/org.yaml", "org-not-enrolled"), mandateFamily: false };
|
|
197
|
+
|
|
198
|
+
if (typeof call === "function") {
|
|
199
|
+
try {
|
|
200
|
+
const r = await call("mandate.version", {});
|
|
201
|
+
if (r && r.ok) return { ...verdict(true, "mandate.version round-trip ok"), mandateFamily: true };
|
|
202
|
+
const code = r && r.error && r.error.code ? r.error.code : "";
|
|
203
|
+
if (code === "NOT_FOUND") {
|
|
204
|
+
// The transport answered — the server (or the vendored protocol) simply
|
|
205
|
+
// predates the mandate family. Reachable, but the mandate spine is not.
|
|
206
|
+
return {
|
|
207
|
+
...verdict(true, "org transport live; mandate family not present on this server/protocol", "mandate-family-absent"),
|
|
208
|
+
mandateFamily: false,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// fall through to the read probe before declaring the plane dead
|
|
212
|
+
} catch (e) {
|
|
213
|
+
return { ...verdict(false, `mandate.version threw: ${msg(e)}`, "org-call-threw"), mandateFamily: false };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (typeof read === "function") {
|
|
218
|
+
try {
|
|
219
|
+
const r = await read("snapshot");
|
|
220
|
+
if (r && r.ok) return { ...verdict(true, "snapshot read ok"), mandateFamily: false };
|
|
221
|
+
return { ...verdict(false, `snapshot read failed (${(r && r.status) || "?"})`, "org-unreachable"), mandateFamily: false };
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return { ...verdict(false, `snapshot read threw: ${msg(e)}`, "org-call-threw"), mandateFamily: false };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { ...verdict(false, "no org call/read impl supplied", "org-no-impl"), mandateFamily: false };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Probe granted org PLUGIN tools: they are reachable iff they are present in the
|
|
231
|
+
* shared disk cache the daemon and MCP plane both read.
|
|
232
|
+
* @param {string} agentRoot
|
|
233
|
+
* @param {object} [deps] - { existsSync, readFileSync }
|
|
234
|
+
* @returns {{ok:boolean, detail:string, degraded?:string, tools:object[], toolsetVersion:number|null}}
|
|
235
|
+
*/
|
|
236
|
+
export function probeGrantedPluginTools(agentRoot, deps = {}) {
|
|
237
|
+
const p = join(agentRoot, "state", "org", "toolset.json");
|
|
238
|
+
const doc = readJsonSafe(p, null, deps);
|
|
239
|
+
if (!doc) {
|
|
240
|
+
return { ...verdict(false, `no state/org/toolset.json (no granted plugin tools cached)`, "toolset-absent"), tools: [], toolsetVersion: null };
|
|
241
|
+
}
|
|
242
|
+
const tools = Array.isArray(doc.tools) ? doc.tools.filter((t) => t && typeof t.name === "string") : [];
|
|
243
|
+
return {
|
|
244
|
+
...verdict(tools.length > 0, `${tools.length} granted plugin tool(s) in state/org/toolset.json`, tools.length ? undefined : "toolset-empty"),
|
|
245
|
+
tools,
|
|
246
|
+
toolsetVersion: typeof doc.version === "number" ? doc.version : null,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** @param {*} e @returns {string} */
|
|
251
|
+
function msg(e) {
|
|
252
|
+
return e && e.message ? String(e.message) : String(e);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export default { PLANES, probeSkillPlugin, probeMcpServer, probeOrgPlane, probeGrantedPluginTools, claudeSettingsPath, readJsonSafe };
|
|
@@ -32,13 +32,38 @@
|
|
|
32
32
|
// ---------------------------------------------------------------------------
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* @typedef {"message"|"reaction"|"channel_cc"|"voice_note"|"call"
|
|
35
|
+
* @typedef {"message"|"reaction"|"channel_cc"|"voice_note"|"call"
|
|
36
|
+
* |"mention"|"thread_reply"|"task_assigned"|"task_comment"
|
|
37
|
+
* |"file_comment"|"approval"|"decision"|"escalation"|"handoff"
|
|
38
|
+
* |"email"} EventKind
|
|
36
39
|
*
|
|
40
|
+
* Chat kinds:
|
|
37
41
|
* - message a normal directed message (DM, @mention, thread reply)
|
|
38
42
|
* - reaction an emoji reaction add/remove (rarely action-required)
|
|
39
43
|
* - channel_cc a non-directed message in a watched/cc channel (observe, don't reply)
|
|
40
44
|
* - voice_note a voice note whose transcript becomes `text` (+ a voice_note attachment)
|
|
41
45
|
* - call a live phone/voice call turn
|
|
46
|
+
*
|
|
47
|
+
* WORK-SURFACE kinds (added for wide org inbound — lib/org/inbound/surfaces.mjs).
|
|
48
|
+
* An org agent is addressed through far more than chat: hq's own responder reacts
|
|
49
|
+
* to eleven inbound triggers, and an SDK agent that tails only `messaging` is
|
|
50
|
+
* deaf to nine of them. These kinds are the ROUTING HINT the daemon classifier
|
|
51
|
+
* reads off the inbox item, so a board assignment and an @mention do not have to
|
|
52
|
+
* be told apart by parsing prose:
|
|
53
|
+
* - mention an @mention of this agent in a public/private space
|
|
54
|
+
* - thread_reply a reply in a thread this agent is part of
|
|
55
|
+
* - task_assigned a board item assigned to / claimed for this agent
|
|
56
|
+
* - task_comment a comment, block or move on an item this agent owns or reviews
|
|
57
|
+
* - file_comment a comment on a chat file or workspace doc this agent owns/shares
|
|
58
|
+
* - approval an approval on this agent's desk, or the outcome of its request
|
|
59
|
+
* - decision a decision this agent proposed, commented on, or must act on
|
|
60
|
+
* - escalation an escalation on this agent's task / room / name
|
|
61
|
+
* - handoff a delegation offered to this agent, or its offer resolved
|
|
62
|
+
* - email an inbound email routed to this agent's mailbox
|
|
63
|
+
*
|
|
64
|
+
* `classifyByKind` (scripts/daemon/classify-kind.mjs) deliberately returns null
|
|
65
|
+
* for every work-surface kind, so they fall through to the normal LLM classifier
|
|
66
|
+
* — unlike `reaction`/`channel_cc`, a task assignment genuinely needs triage.
|
|
42
67
|
*/
|
|
43
68
|
export const EVENT_KINDS = Object.freeze([
|
|
44
69
|
"message",
|
|
@@ -46,6 +71,17 @@ export const EVENT_KINDS = Object.freeze([
|
|
|
46
71
|
"channel_cc",
|
|
47
72
|
"voice_note",
|
|
48
73
|
"call",
|
|
74
|
+
// Work surfaces (org inbound).
|
|
75
|
+
"mention",
|
|
76
|
+
"thread_reply",
|
|
77
|
+
"task_assigned",
|
|
78
|
+
"task_comment",
|
|
79
|
+
"file_comment",
|
|
80
|
+
"approval",
|
|
81
|
+
"decision",
|
|
82
|
+
"escalation",
|
|
83
|
+
"handoff",
|
|
84
|
+
"email",
|
|
49
85
|
]);
|
|
50
86
|
|
|
51
87
|
/** The default kind when an event/serialized item does not specify one. */
|
|
@@ -25,7 +25,31 @@ import {
|
|
|
25
25
|
import { ChannelMessage } from "./index.mjs";
|
|
26
26
|
|
|
27
27
|
test("EVENT_KINDS is the closed set from the design", () => {
|
|
28
|
-
|
|
28
|
+
// Two groups. The five CHAT kinds are the original design set and must not
|
|
29
|
+
// move — `message` in particular is the default and the byte-compat anchor.
|
|
30
|
+
// The ten WORK-SURFACE kinds were added for wide org inbound
|
|
31
|
+
// (lib/org/inbound/surfaces.mjs): an org agent is addressed through board
|
|
32
|
+
// items, approvals, decisions, docs and mail as well as chat, and `kind` is
|
|
33
|
+
// the routing hint the daemon classifier reads. Adding one here without a
|
|
34
|
+
// matching SURFACES entry (or vice versa) is caught by
|
|
35
|
+
// lib/org/inbound/project.test.mjs.
|
|
36
|
+
assert.deepEqual([...EVENT_KINDS].sort(), [
|
|
37
|
+
"approval",
|
|
38
|
+
"call",
|
|
39
|
+
"channel_cc",
|
|
40
|
+
"decision",
|
|
41
|
+
"email",
|
|
42
|
+
"escalation",
|
|
43
|
+
"file_comment",
|
|
44
|
+
"handoff",
|
|
45
|
+
"mention",
|
|
46
|
+
"message",
|
|
47
|
+
"reaction",
|
|
48
|
+
"task_assigned",
|
|
49
|
+
"task_comment",
|
|
50
|
+
"thread_reply",
|
|
51
|
+
"voice_note",
|
|
52
|
+
]);
|
|
29
53
|
assert.equal(DEFAULT_EVENT_KIND, "message");
|
|
30
54
|
});
|
|
31
55
|
|
package/lib/claude-bin.mjs
CHANGED
|
@@ -105,11 +105,45 @@ export function resolveClaudeBin() {
|
|
|
105
105
|
*
|
|
106
106
|
* Operators who want full MCP support on daemon spawns can set
|
|
107
107
|
* DAEMON_LOAD_MCPS=1 in .env; the spawn sites will skip these flags.
|
|
108
|
+
*
|
|
109
|
+
* --mcp-config <AGENT_ROOT>/.mcp.json (added when present)
|
|
110
|
+
* `--strict-mcp-config` ALONE is a blanket "load no MCP servers at all".
|
|
111
|
+
* That was correct for skipping Serena/Playwright, but it also killed the
|
|
112
|
+
* agent's OWN curated org MCP server — the one that carries the ~107 Cohort
|
|
113
|
+
* protocol tools. Rung 0 of the execution ladder ("one protocol method,
|
|
114
|
+
* params fully known") is a lie while every daemon spawn runs with zero org
|
|
115
|
+
* tools loaded, and the capability manifest's `org` plane would be claiming
|
|
116
|
+
* reachability for tools no session can actually call.
|
|
117
|
+
*
|
|
118
|
+
* Pairing `--mcp-config <file>` WITH `--strict-mcp-config` is the precise
|
|
119
|
+
* statement we want: load exactly this server set, and nothing globally
|
|
120
|
+
* installed. The agent's own tools come back; the IDE-grade plugins stay out.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} [agentRoot] - defaults to $AGENT_ROOT, then cwd. Callers that
|
|
123
|
+
* pass nothing keep working; the flag is only added when the file exists, so a
|
|
124
|
+
* scaffold without an .mcp.json degrades to exactly the previous behaviour.
|
|
125
|
+
* @param {{existsSync?:Function, env?:object}} [deps]
|
|
126
|
+
* @returns {string[]}
|
|
108
127
|
*/
|
|
109
|
-
export function daemonClaudeArgs() {
|
|
110
|
-
|
|
128
|
+
export function daemonClaudeArgs(agentRoot, deps = {}) {
|
|
129
|
+
const env = deps.env || process.env;
|
|
130
|
+
if (env.DAEMON_LOAD_MCPS === "1") return [];
|
|
131
|
+
const ex = deps.existsSync || existsSync;
|
|
132
|
+
|
|
111
133
|
const args = ["--strict-mcp-config"];
|
|
112
|
-
|
|
134
|
+
|
|
135
|
+
const root = agentRoot || env.AGENT_ROOT || process.cwd();
|
|
136
|
+
try {
|
|
137
|
+
const cfg = join(root, ".mcp.json");
|
|
138
|
+
// Opt-out for an operator who genuinely wants the old blanket behaviour.
|
|
139
|
+
if (env.DAEMON_SKIP_ORG_MCP !== "1" && ex(cfg)) args.unshift("--mcp-config", cfg);
|
|
140
|
+
} catch {
|
|
141
|
+
// A non-string root (bad caller) must not take the daemon down: we still
|
|
142
|
+
// return a usable arg list. Nothing is silent — the absence of the flag is
|
|
143
|
+
// visible in the spawn line every caller already logs.
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (env.DAEMON_BARE_MODE === "1") args.unshift("--bare");
|
|
113
147
|
return args;
|
|
114
148
|
}
|
|
115
149
|
|