@ouro.bot/cli 0.1.0-alpha.16 → 0.1.0-alpha.160
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/AdoptionSpecialist.ouro/agent.json +3 -2
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +3 -3
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/README.md +186 -191
- package/changelog.json +1081 -0
- package/dist/heart/active-work.js +633 -0
- package/dist/heart/bridges/manager.js +358 -0
- package/dist/heart/bridges/state-machine.js +135 -0
- package/dist/heart/bridges/store.js +123 -0
- package/dist/heart/commitments.js +105 -0
- package/dist/heart/config.js +40 -30
- package/dist/heart/core.js +608 -123
- package/dist/heart/cross-chat-delivery.js +146 -0
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/agent-service.js +360 -0
- package/dist/heart/daemon/auth-flow.js +477 -0
- package/dist/heart/daemon/cadence.js +70 -0
- package/dist/heart/daemon/daemon-cli.js +2467 -205
- package/dist/heart/daemon/daemon-entry.js +131 -4
- package/dist/heart/daemon/daemon-health.js +136 -0
- package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
- package/dist/heart/daemon/daemon-tombstone.js +123 -0
- package/dist/heart/daemon/daemon.js +434 -17
- package/dist/heart/daemon/habit-migration.js +181 -0
- package/dist/heart/daemon/habit-parser.js +119 -0
- package/dist/heart/daemon/habit-scheduler.js +369 -0
- package/dist/heart/daemon/hatch-animation.js +10 -3
- package/dist/heart/daemon/hatch-flow.js +30 -111
- package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
- package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
- package/dist/heart/daemon/inner-status.js +89 -0
- package/dist/heart/daemon/launchd.js +159 -0
- package/dist/heart/daemon/log-tailer.js +4 -3
- package/dist/heart/daemon/mcp-server.js +591 -0
- package/dist/heart/daemon/message-router.js +17 -8
- package/dist/heart/daemon/os-cron-deps.js +134 -0
- package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
- package/dist/heart/daemon/ouro-path-installer.js +78 -34
- package/dist/heart/daemon/ouro-version-manager.js +171 -0
- package/dist/heart/daemon/outlook-http.js +307 -0
- package/dist/heart/daemon/outlook-read.js +1281 -0
- package/dist/heart/daemon/outlook-render.js +1032 -0
- package/dist/heart/daemon/outlook-types.js +7 -0
- package/dist/heart/daemon/outlook-view.js +194 -0
- package/dist/heart/daemon/process-manager.js +75 -0
- package/dist/heart/daemon/run-hooks.js +39 -0
- package/dist/heart/daemon/runtime-logging.js +58 -15
- package/dist/heart/daemon/runtime-metadata.js +219 -0
- package/dist/heart/daemon/runtime-mode.js +67 -0
- package/dist/heart/daemon/safe-mode.js +161 -0
- package/dist/heart/daemon/sense-manager.js +50 -2
- package/dist/heart/daemon/session-id-resolver.js +131 -0
- package/dist/heart/daemon/skill-management-installer.js +94 -0
- package/dist/heart/daemon/socket-client.js +202 -0
- package/dist/heart/daemon/specialist-orchestrator.js +2 -2
- package/dist/heart/daemon/specialist-prompt.js +13 -8
- package/dist/heart/daemon/specialist-tools.js +53 -4
- package/dist/heart/daemon/staged-restart.js +114 -0
- package/dist/heart/daemon/task-scheduler.js +3 -25
- package/dist/heart/daemon/thoughts.js +507 -0
- package/dist/heart/daemon/update-checker.js +111 -0
- package/dist/heart/daemon/update-hooks.js +138 -0
- package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/identity.js +136 -55
- package/dist/heart/kicks.js +2 -20
- package/dist/heart/migrate-config.js +127 -0
- package/dist/heart/model-capabilities.js +48 -0
- package/dist/heart/obligations.js +197 -0
- package/dist/heart/progress-story.js +42 -0
- package/dist/heart/provider-failover.js +88 -0
- package/dist/heart/provider-ping.js +172 -0
- package/dist/heart/providers/anthropic-token.js +163 -0
- package/dist/heart/providers/anthropic.js +196 -36
- package/dist/heart/providers/azure.js +116 -10
- package/dist/heart/providers/github-copilot.js +157 -0
- package/dist/heart/providers/minimax.js +33 -3
- package/dist/heart/providers/openai-codex.js +51 -16
- package/dist/heart/session-activity.js +172 -0
- package/dist/heart/session-recall.js +216 -0
- package/dist/heart/streaming.js +117 -33
- package/dist/heart/target-resolution.js +127 -0
- package/dist/heart/tool-activity-callbacks.js +36 -0
- package/dist/heart/tool-description.js +135 -0
- package/dist/heart/tool-loop.js +200 -0
- package/dist/heart/turn-coordinator.js +28 -0
- package/dist/mind/associative-recall.js +122 -32
- package/dist/mind/bundle-manifest.js +14 -0
- package/dist/mind/context.js +125 -14
- package/dist/mind/{memory.js → diary.js} +70 -56
- package/dist/mind/first-impressions.js +16 -2
- package/dist/mind/friends/channel.js +56 -0
- package/dist/mind/friends/group-context.js +144 -0
- package/dist/mind/friends/store-file.js +19 -0
- package/dist/mind/friends/trust-explanation.js +74 -0
- package/dist/mind/friends/types.js +8 -0
- package/dist/mind/journal-index.js +161 -0
- package/dist/mind/obligation-steering.js +221 -0
- package/dist/mind/obligations.js +134 -0
- package/dist/mind/pending.js +76 -9
- package/dist/mind/phrases.js +1 -0
- package/dist/mind/prompt.js +651 -91
- package/dist/mind/token-estimate.js +8 -12
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/nerves/event-buffer.js +111 -0
- package/dist/nerves/index.js +63 -3
- package/dist/nerves/observation.js +20 -0
- package/dist/nerves/runtime.js +5 -1
- package/dist/repertoire/ado-semantic.js +6 -0
- package/dist/repertoire/coding/codex-jsonl.js +64 -0
- package/dist/repertoire/coding/context-pack.js +254 -0
- package/dist/repertoire/coding/feedback.js +301 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +210 -4
- package/dist/repertoire/coding/spawner.js +58 -12
- package/dist/repertoire/coding/tools.js +176 -4
- package/dist/repertoire/guardrails.js +291 -0
- package/dist/repertoire/mcp-client.js +254 -0
- package/dist/repertoire/mcp-manager.js +198 -0
- package/dist/repertoire/skills.js +14 -23
- package/dist/repertoire/tasks/board.js +43 -5
- package/dist/repertoire/tasks/fix.js +182 -0
- package/dist/repertoire/tasks/index.js +28 -10
- package/dist/repertoire/tasks/lifecycle.js +2 -2
- package/dist/repertoire/tasks/parser.js +3 -2
- package/dist/repertoire/tasks/scanner.js +194 -37
- package/dist/repertoire/tasks/transitions.js +11 -28
- package/dist/repertoire/tools-base.js +992 -273
- package/dist/repertoire/tools-bluebubbles.js +94 -0
- package/dist/repertoire/tools-github.js +1 -6
- package/dist/repertoire/tools-teams.js +12 -59
- package/dist/repertoire/tools.js +281 -101
- package/dist/scripts/claude-code-hook.js +41 -0
- package/dist/scripts/claude-code-stop-hook.js +47 -0
- package/dist/senses/attention-queue.js +97 -0
- package/dist/senses/bluebubbles-client.js +258 -5
- package/dist/senses/bluebubbles-entry.js +2 -0
- package/dist/senses/bluebubbles-inbound-log.js +109 -0
- package/dist/senses/bluebubbles-media.js +339 -0
- package/dist/senses/bluebubbles-model.js +12 -4
- package/dist/senses/bluebubbles-mutation-log.js +45 -5
- package/dist/senses/bluebubbles-runtime-state.js +109 -0
- package/dist/senses/bluebubbles-session-cleanup.js +72 -0
- package/dist/senses/bluebubbles.js +1040 -45
- package/dist/senses/cli-entry.js +1 -1
- package/dist/senses/cli-layout.js +187 -0
- package/dist/senses/cli.js +419 -152
- package/dist/senses/commands.js +38 -1
- package/dist/senses/continuity.js +94 -0
- package/dist/senses/habit-turn-message.js +108 -0
- package/dist/senses/inner-dialog-worker.js +112 -19
- package/dist/senses/inner-dialog.js +592 -84
- package/dist/senses/pipeline.js +502 -0
- package/dist/senses/shared-turn.js +196 -0
- package/dist/senses/surface-tool.js +95 -0
- package/dist/senses/teams.js +667 -120
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +12 -4
- package/skills/configure-dev-tools.md +81 -0
- package/subagents/README.md +4 -70
- package/dist/heart/daemon/subagent-installer.js +0 -134
- package/subagents/work-doer.md +0 -233
- package/subagents/work-merger.md +0 -624
- package/subagents/work-planner.md +0 -373
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpClient = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const readline_1 = require("readline");
|
|
6
|
+
const runtime_1 = require("../nerves/runtime");
|
|
7
|
+
const MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
8
|
+
const DEFAULT_TOOL_CALL_TIMEOUT = 30_000;
|
|
9
|
+
class McpClient {
|
|
10
|
+
config;
|
|
11
|
+
process = null;
|
|
12
|
+
nextId = 1;
|
|
13
|
+
pending = new Map();
|
|
14
|
+
connected = false;
|
|
15
|
+
cachedTools = null;
|
|
16
|
+
onCloseCallback = null;
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
async connect() {
|
|
21
|
+
(0, runtime_1.emitNervesEvent)({
|
|
22
|
+
event: "mcp.connect_start",
|
|
23
|
+
component: "repertoire",
|
|
24
|
+
message: "starting MCP server connection",
|
|
25
|
+
meta: { command: this.config.command },
|
|
26
|
+
});
|
|
27
|
+
const env = { ...process.env, ...this.config.env };
|
|
28
|
+
this.process = (0, child_process_1.spawn)(this.config.command, this.config.args ?? [], {
|
|
29
|
+
env,
|
|
30
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
31
|
+
});
|
|
32
|
+
this.setupLineReader();
|
|
33
|
+
this.setupProcessHandlers();
|
|
34
|
+
try {
|
|
35
|
+
await this.initialize();
|
|
36
|
+
this.connected = true;
|
|
37
|
+
(0, runtime_1.emitNervesEvent)({
|
|
38
|
+
event: "mcp.connect_end",
|
|
39
|
+
component: "repertoire",
|
|
40
|
+
message: "MCP server connected",
|
|
41
|
+
meta: { command: this.config.command },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
this.connected = false;
|
|
46
|
+
(0, runtime_1.emitNervesEvent)({
|
|
47
|
+
level: "error",
|
|
48
|
+
event: "mcp.connect_error",
|
|
49
|
+
component: "repertoire",
|
|
50
|
+
message: "MCP server connection failed",
|
|
51
|
+
meta: {
|
|
52
|
+
command: this.config.command,
|
|
53
|
+
/* v8 ignore next -- defensive: spawn errors are always Error instances @preserve */
|
|
54
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async listTools() {
|
|
61
|
+
if (this.cachedTools) {
|
|
62
|
+
return this.cachedTools;
|
|
63
|
+
}
|
|
64
|
+
const allTools = [];
|
|
65
|
+
let cursor;
|
|
66
|
+
do {
|
|
67
|
+
const params = {};
|
|
68
|
+
if (cursor) {
|
|
69
|
+
params.cursor = cursor;
|
|
70
|
+
}
|
|
71
|
+
const result = await this.sendRequest("tools/list", params);
|
|
72
|
+
allTools.push(...result.tools);
|
|
73
|
+
cursor = result.nextCursor;
|
|
74
|
+
} while (cursor);
|
|
75
|
+
this.cachedTools = allTools;
|
|
76
|
+
return allTools;
|
|
77
|
+
}
|
|
78
|
+
async callTool(name, args, timeout = DEFAULT_TOOL_CALL_TIMEOUT) {
|
|
79
|
+
(0, runtime_1.emitNervesEvent)({
|
|
80
|
+
event: "mcp.tool_call_start",
|
|
81
|
+
component: "repertoire",
|
|
82
|
+
message: `calling MCP tool: ${name}`,
|
|
83
|
+
meta: { tool: name },
|
|
84
|
+
});
|
|
85
|
+
try {
|
|
86
|
+
const result = await this.sendRequest("tools/call", {
|
|
87
|
+
name,
|
|
88
|
+
arguments: args,
|
|
89
|
+
}, timeout);
|
|
90
|
+
(0, runtime_1.emitNervesEvent)({
|
|
91
|
+
event: "mcp.tool_call_end",
|
|
92
|
+
component: "repertoire",
|
|
93
|
+
message: `MCP tool call completed: ${name}`,
|
|
94
|
+
meta: { tool: name },
|
|
95
|
+
});
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
(0, runtime_1.emitNervesEvent)({
|
|
100
|
+
level: "error",
|
|
101
|
+
event: "mcp.tool_call_error",
|
|
102
|
+
component: "repertoire",
|
|
103
|
+
message: `MCP tool call failed: ${name}`,
|
|
104
|
+
meta: {
|
|
105
|
+
tool: name,
|
|
106
|
+
/* v8 ignore next -- defensive: callTool errors are always Error instances @preserve */
|
|
107
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
shutdown() {
|
|
114
|
+
this.connected = false;
|
|
115
|
+
this.rejectAllPending(new Error("Client shutdown"));
|
|
116
|
+
/* v8 ignore next -- defensive: process always exists during normal shutdown @preserve */
|
|
117
|
+
if (this.process && !this.process.killed) {
|
|
118
|
+
this.process.kill();
|
|
119
|
+
}
|
|
120
|
+
this.process = null;
|
|
121
|
+
}
|
|
122
|
+
isConnected() {
|
|
123
|
+
return this.connected;
|
|
124
|
+
}
|
|
125
|
+
onClose(callback) {
|
|
126
|
+
this.onCloseCallback = callback;
|
|
127
|
+
}
|
|
128
|
+
async initialize() {
|
|
129
|
+
const result = await this.sendRequest("initialize", {
|
|
130
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
131
|
+
clientInfo: { name: "ouroboros", version: "1.0" },
|
|
132
|
+
capabilities: {},
|
|
133
|
+
});
|
|
134
|
+
// Send initialized notification (no id, no response expected)
|
|
135
|
+
this.writeMessage({
|
|
136
|
+
jsonrpc: "2.0",
|
|
137
|
+
method: "initialized",
|
|
138
|
+
});
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
sendRequest(method, params, timeout) {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
if (!this.process || !this.connected && method !== "initialize") {
|
|
144
|
+
reject(new Error("MCP client is disconnected"));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const id = this.nextId++;
|
|
148
|
+
const pending = { resolve, reject };
|
|
149
|
+
if (timeout) {
|
|
150
|
+
pending.timer = setTimeout(() => {
|
|
151
|
+
this.pending.delete(id);
|
|
152
|
+
reject(new Error(`MCP request timeout after ${timeout}ms: ${method}`));
|
|
153
|
+
}, timeout);
|
|
154
|
+
}
|
|
155
|
+
this.pending.set(id, pending);
|
|
156
|
+
const request = {
|
|
157
|
+
jsonrpc: "2.0",
|
|
158
|
+
id,
|
|
159
|
+
method,
|
|
160
|
+
params,
|
|
161
|
+
};
|
|
162
|
+
this.writeMessage(request);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
writeMessage(message) {
|
|
166
|
+
/* v8 ignore next -- defensive: stdin always writable during active connection @preserve */
|
|
167
|
+
if (this.process?.stdin?.writable) {
|
|
168
|
+
this.process.stdin.write(JSON.stringify(message) + "\n");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
setupLineReader() {
|
|
172
|
+
/* v8 ignore next -- defensive: stdout always exists after spawn @preserve */
|
|
173
|
+
if (!this.process?.stdout)
|
|
174
|
+
return;
|
|
175
|
+
const rl = (0, readline_1.createInterface)({ input: this.process.stdout });
|
|
176
|
+
rl.on("line", (line) => {
|
|
177
|
+
this.handleLine(line);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
handleLine(line) {
|
|
181
|
+
let response;
|
|
182
|
+
try {
|
|
183
|
+
response = JSON.parse(line);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
(0, runtime_1.emitNervesEvent)({
|
|
187
|
+
level: "warn",
|
|
188
|
+
event: "mcp.connect_error",
|
|
189
|
+
component: "repertoire",
|
|
190
|
+
message: "received malformed JSON from MCP server",
|
|
191
|
+
meta: { line },
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (response.id === undefined || response.id === null) {
|
|
196
|
+
// Notification or invalid — ignore
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const pending = this.pending.get(response.id);
|
|
200
|
+
if (!pending)
|
|
201
|
+
return;
|
|
202
|
+
this.pending.delete(response.id);
|
|
203
|
+
if (pending.timer) {
|
|
204
|
+
clearTimeout(pending.timer);
|
|
205
|
+
}
|
|
206
|
+
if (response.error) {
|
|
207
|
+
pending.reject(new Error(response.error.message));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
pending.resolve(response.result);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
setupProcessHandlers() {
|
|
214
|
+
/* v8 ignore next -- defensive: process always exists after spawn @preserve */
|
|
215
|
+
if (!this.process)
|
|
216
|
+
return;
|
|
217
|
+
this.process.on("error", (error) => {
|
|
218
|
+
(0, runtime_1.emitNervesEvent)({
|
|
219
|
+
level: "error",
|
|
220
|
+
event: "mcp.connect_error",
|
|
221
|
+
component: "repertoire",
|
|
222
|
+
message: "MCP server process error",
|
|
223
|
+
meta: { reason: error.message },
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
this.process.on("close", (code) => {
|
|
227
|
+
const wasConnected = this.connected;
|
|
228
|
+
this.connected = false;
|
|
229
|
+
this.rejectAllPending(new Error(`MCP server process closed with code ${code}`));
|
|
230
|
+
if (wasConnected) {
|
|
231
|
+
(0, runtime_1.emitNervesEvent)({
|
|
232
|
+
level: "error",
|
|
233
|
+
event: "mcp.connect_error",
|
|
234
|
+
component: "repertoire",
|
|
235
|
+
message: "MCP server process exited unexpectedly",
|
|
236
|
+
meta: { exitCode: code },
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (this.onCloseCallback) {
|
|
240
|
+
this.onCloseCallback();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
rejectAllPending(error) {
|
|
245
|
+
for (const [id, pending] of this.pending) {
|
|
246
|
+
if (pending.timer) {
|
|
247
|
+
clearTimeout(pending.timer);
|
|
248
|
+
}
|
|
249
|
+
pending.reject(error);
|
|
250
|
+
this.pending.delete(id);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
exports.McpClient = McpClient;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpManager = void 0;
|
|
4
|
+
exports.getSharedMcpManager = getSharedMcpManager;
|
|
5
|
+
exports.shutdownSharedMcpManager = shutdownSharedMcpManager;
|
|
6
|
+
exports.resetSharedMcpManager = resetSharedMcpManager;
|
|
7
|
+
const mcp_client_1 = require("./mcp-client");
|
|
8
|
+
const identity_1 = require("../heart/identity");
|
|
9
|
+
const runtime_1 = require("../nerves/runtime");
|
|
10
|
+
const MAX_RESTART_RETRIES = 5;
|
|
11
|
+
const RESTART_DELAY_MS = 1000;
|
|
12
|
+
class McpManager {
|
|
13
|
+
servers = new Map();
|
|
14
|
+
shuttingDown = false;
|
|
15
|
+
async start(servers) {
|
|
16
|
+
(0, runtime_1.emitNervesEvent)({
|
|
17
|
+
event: "mcp.manager_start",
|
|
18
|
+
component: "repertoire",
|
|
19
|
+
message: "starting MCP manager",
|
|
20
|
+
meta: { serverCount: Object.keys(servers).length },
|
|
21
|
+
});
|
|
22
|
+
const entries = Object.entries(servers);
|
|
23
|
+
for (const [name, config] of entries) {
|
|
24
|
+
await this.connectServer(name, config);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
listAllTools() {
|
|
28
|
+
const result = [];
|
|
29
|
+
for (const [name, entry] of this.servers) {
|
|
30
|
+
result.push({ server: name, tools: entry.cachedTools });
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
async callTool(server, tool, args) {
|
|
35
|
+
const entry = this.servers.get(server);
|
|
36
|
+
if (!entry) {
|
|
37
|
+
throw new Error(`Unknown server: ${server}`);
|
|
38
|
+
}
|
|
39
|
+
if (!entry.client.isConnected()) {
|
|
40
|
+
throw new Error(`Server "${server}" is disconnected`);
|
|
41
|
+
}
|
|
42
|
+
return entry.client.callTool(tool, args);
|
|
43
|
+
}
|
|
44
|
+
shutdown() {
|
|
45
|
+
this.shuttingDown = true;
|
|
46
|
+
(0, runtime_1.emitNervesEvent)({
|
|
47
|
+
event: "mcp.manager_stop",
|
|
48
|
+
component: "repertoire",
|
|
49
|
+
message: "shutting down MCP manager",
|
|
50
|
+
meta: { serverCount: this.servers.size },
|
|
51
|
+
});
|
|
52
|
+
for (const [, entry] of this.servers) {
|
|
53
|
+
entry.client.shutdown();
|
|
54
|
+
}
|
|
55
|
+
this.servers.clear();
|
|
56
|
+
}
|
|
57
|
+
async connectServer(name, config) {
|
|
58
|
+
const client = new mcp_client_1.McpClient(config);
|
|
59
|
+
const entry = {
|
|
60
|
+
name,
|
|
61
|
+
config,
|
|
62
|
+
client,
|
|
63
|
+
cachedTools: [],
|
|
64
|
+
consecutiveFailures: 0,
|
|
65
|
+
};
|
|
66
|
+
this.servers.set(name, entry);
|
|
67
|
+
client.onClose(() => {
|
|
68
|
+
if (this.shuttingDown)
|
|
69
|
+
return;
|
|
70
|
+
this.handleServerCrash(name);
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
await client.connect();
|
|
74
|
+
const tools = await client.listTools();
|
|
75
|
+
entry.cachedTools = tools;
|
|
76
|
+
entry.consecutiveFailures = 0;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
80
|
+
(0, runtime_1.emitNervesEvent)({
|
|
81
|
+
level: "error",
|
|
82
|
+
event: "mcp.connect_error",
|
|
83
|
+
component: "repertoire",
|
|
84
|
+
message: `failed to connect MCP server "${name}" (command: ${config.command}). Check that the command exists and is properly configured. Reason: ${reason}`,
|
|
85
|
+
meta: {
|
|
86
|
+
server: name,
|
|
87
|
+
command: config.command,
|
|
88
|
+
args: config.args,
|
|
89
|
+
reason,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
handleServerCrash(name) {
|
|
95
|
+
const entry = this.servers.get(name);
|
|
96
|
+
/* v8 ignore next -- defensive: entry removed between close event and handler @preserve */
|
|
97
|
+
if (!entry)
|
|
98
|
+
return;
|
|
99
|
+
entry.consecutiveFailures++;
|
|
100
|
+
if (entry.consecutiveFailures > MAX_RESTART_RETRIES) {
|
|
101
|
+
(0, runtime_1.emitNervesEvent)({
|
|
102
|
+
level: "error",
|
|
103
|
+
event: "mcp.connect_error",
|
|
104
|
+
component: "repertoire",
|
|
105
|
+
message: `MCP server "${name}" exceeded max restart retries (${MAX_RESTART_RETRIES}). Giving up — check that "${entry.config.command}" exists and is properly configured in agent.json mcpServers.`,
|
|
106
|
+
meta: { server: name, command: entry.config.command, failures: entry.consecutiveFailures },
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
(0, runtime_1.emitNervesEvent)({
|
|
111
|
+
level: "warn",
|
|
112
|
+
event: "mcp.server_restart",
|
|
113
|
+
component: "repertoire",
|
|
114
|
+
message: `restarting crashed MCP server: ${name}`,
|
|
115
|
+
meta: { server: name, attempt: entry.consecutiveFailures },
|
|
116
|
+
});
|
|
117
|
+
/* v8 ignore start -- timer callback: covered by mcp-manager.test.ts via fake timers but v8 can't trace @preserve */
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
if (this.shuttingDown)
|
|
120
|
+
return;
|
|
121
|
+
this.restartServer(name).catch(() => {
|
|
122
|
+
// Error handling is inside restartServer
|
|
123
|
+
});
|
|
124
|
+
}, RESTART_DELAY_MS);
|
|
125
|
+
/* v8 ignore stop */
|
|
126
|
+
}
|
|
127
|
+
/* v8 ignore start -- called from timer callback: covered by mcp-manager.test.ts via fake timers but v8 can't trace @preserve */
|
|
128
|
+
async restartServer(name) {
|
|
129
|
+
const entry = this.servers.get(name);
|
|
130
|
+
if (!entry)
|
|
131
|
+
return;
|
|
132
|
+
// Remove old entry and reconnect
|
|
133
|
+
this.servers.delete(name);
|
|
134
|
+
await this.connectServer(name, entry.config);
|
|
135
|
+
// Preserve failure count
|
|
136
|
+
const newEntry = this.servers.get(name);
|
|
137
|
+
if (newEntry) {
|
|
138
|
+
newEntry.consecutiveFailures = entry.consecutiveFailures;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
exports.McpManager = McpManager;
|
|
143
|
+
let _sharedManager = null;
|
|
144
|
+
let _sharedManagerPromise = null;
|
|
145
|
+
/**
|
|
146
|
+
* Get or create a shared McpManager instance from the agent's config.
|
|
147
|
+
* Returns null if no mcpServers are configured.
|
|
148
|
+
* Safe to call from multiple senses — will only create one instance.
|
|
149
|
+
*/
|
|
150
|
+
async function getSharedMcpManager() {
|
|
151
|
+
if (_sharedManager)
|
|
152
|
+
return _sharedManager;
|
|
153
|
+
/* v8 ignore next -- race guard: deduplicates concurrent initialization calls @preserve */
|
|
154
|
+
if (_sharedManagerPromise)
|
|
155
|
+
return _sharedManagerPromise;
|
|
156
|
+
_sharedManagerPromise = (async () => {
|
|
157
|
+
try {
|
|
158
|
+
const config = (0, identity_1.loadAgentConfig)();
|
|
159
|
+
const servers = config.mcpServers;
|
|
160
|
+
if (!servers || Object.keys(servers).length === 0)
|
|
161
|
+
return null;
|
|
162
|
+
const manager = new McpManager();
|
|
163
|
+
await manager.start(servers);
|
|
164
|
+
_sharedManager = manager;
|
|
165
|
+
return manager;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
(0, runtime_1.emitNervesEvent)({
|
|
169
|
+
level: "error",
|
|
170
|
+
event: "mcp.manager_start",
|
|
171
|
+
component: "repertoire",
|
|
172
|
+
message: "failed to initialize shared MCP manager",
|
|
173
|
+
/* v8 ignore next -- both branches tested: Error in wiring test, non-Error is defensive @preserve */
|
|
174
|
+
meta: { reason: error instanceof Error ? error.message : String(error) },
|
|
175
|
+
});
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
_sharedManagerPromise = null;
|
|
180
|
+
}
|
|
181
|
+
})();
|
|
182
|
+
return _sharedManagerPromise;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Shut down the shared MCP manager and clear the singleton.
|
|
186
|
+
* Called during daemon/agent shutdown.
|
|
187
|
+
*/
|
|
188
|
+
function shutdownSharedMcpManager() {
|
|
189
|
+
if (_sharedManager) {
|
|
190
|
+
_sharedManager.shutdown();
|
|
191
|
+
_sharedManager = null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** Reset for testing only */
|
|
195
|
+
function resetSharedMcpManager() {
|
|
196
|
+
_sharedManager = null;
|
|
197
|
+
_sharedManagerPromise = null;
|
|
198
|
+
}
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.getSkillsDir = getSkillsDir;
|
|
37
|
+
exports.getHarnessSkillsDir = getHarnessSkillsDir;
|
|
37
38
|
exports.listSkills = listSkills;
|
|
38
39
|
exports.loadSkill = loadSkill;
|
|
39
40
|
exports.getLoadedSkills = getLoadedSkills;
|
|
@@ -50,9 +51,9 @@ function getSkillsDir() {
|
|
|
50
51
|
function getProtocolMirrorDir() {
|
|
51
52
|
return path.join(getSkillsDir(), "protocols");
|
|
52
53
|
}
|
|
53
|
-
//
|
|
54
|
-
function
|
|
55
|
-
return path.join((0, identity_1.getRepoRoot)(), "
|
|
54
|
+
// Harness-level skills live in {repoRoot}/skills/ directory.
|
|
55
|
+
function getHarnessSkillsDir() {
|
|
56
|
+
return path.join((0, identity_1.getRepoRoot)(), "skills");
|
|
56
57
|
}
|
|
57
58
|
function listMarkdownBasenames(dir) {
|
|
58
59
|
if (!fs.existsSync(dir))
|
|
@@ -74,8 +75,10 @@ function listSkills() {
|
|
|
74
75
|
});
|
|
75
76
|
const baseSkills = listMarkdownBasenames(getSkillsDir());
|
|
76
77
|
const protocolMirrors = listMarkdownBasenames(getProtocolMirrorDir());
|
|
77
|
-
const
|
|
78
|
-
|
|
78
|
+
const harnessSkills = listMarkdownBasenames(getHarnessSkillsDir());
|
|
79
|
+
// Agent skills (base + protocol) come first; harness skills are fallback.
|
|
80
|
+
// Set deduplicates by name — agent overrides harness.
|
|
81
|
+
const skills = [...new Set([...baseSkills, ...protocolMirrors, ...harnessSkills])].sort();
|
|
79
82
|
(0, runtime_1.emitNervesEvent)({
|
|
80
83
|
event: "repertoire.load_end",
|
|
81
84
|
component: "repertoire",
|
|
@@ -93,7 +96,7 @@ function loadSkill(skillName) {
|
|
|
93
96
|
});
|
|
94
97
|
const directSkillPath = path.join(getSkillsDir(), `${skillName}.md`);
|
|
95
98
|
const protocolMirrorPath = path.join(getProtocolMirrorDir(), `${skillName}.md`);
|
|
96
|
-
const
|
|
99
|
+
const harnessSkillPath = path.join(getHarnessSkillsDir(), `${skillName}.md`);
|
|
97
100
|
let resolvedPath = null;
|
|
98
101
|
// 1) Direct agent skill.
|
|
99
102
|
if (fs.existsSync(directSkillPath)) {
|
|
@@ -103,21 +106,9 @@ function loadSkill(skillName) {
|
|
|
103
106
|
else if (fs.existsSync(protocolMirrorPath)) {
|
|
104
107
|
resolvedPath = protocolMirrorPath;
|
|
105
108
|
}
|
|
106
|
-
// 3)
|
|
107
|
-
else if (fs.existsSync(
|
|
108
|
-
|
|
109
|
-
level: "warn",
|
|
110
|
-
event: "repertoire.error",
|
|
111
|
-
component: "repertoire",
|
|
112
|
-
message: "protocol mirror missing; using canonical fallback",
|
|
113
|
-
meta: {
|
|
114
|
-
operation: "loadSkill",
|
|
115
|
-
skill: skillName,
|
|
116
|
-
mirrorPath: protocolMirrorPath,
|
|
117
|
-
canonicalPath: canonicalProtocolPath,
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
resolvedPath = canonicalProtocolPath;
|
|
109
|
+
// 3) Harness-level skill (ships with npm package).
|
|
110
|
+
else if (fs.existsSync(harnessSkillPath)) {
|
|
111
|
+
resolvedPath = harnessSkillPath;
|
|
121
112
|
}
|
|
122
113
|
if (!resolvedPath) {
|
|
123
114
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -128,13 +119,13 @@ function loadSkill(skillName) {
|
|
|
128
119
|
meta: {
|
|
129
120
|
operation: "loadSkill",
|
|
130
121
|
skill: skillName,
|
|
131
|
-
checkedPaths: [directSkillPath, protocolMirrorPath,
|
|
122
|
+
checkedPaths: [directSkillPath, protocolMirrorPath, harnessSkillPath],
|
|
132
123
|
},
|
|
133
124
|
});
|
|
134
125
|
throw new Error(`skill '${skillName}' not found in:\n` +
|
|
135
126
|
`- ${directSkillPath}\n` +
|
|
136
127
|
`- ${protocolMirrorPath}\n` +
|
|
137
|
-
`- ${
|
|
128
|
+
`- ${harnessSkillPath}`);
|
|
138
129
|
}
|
|
139
130
|
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
140
131
|
if (!loadedSkills.includes(skillName)) {
|
|
@@ -7,14 +7,21 @@ exports.boardDeps = boardDeps;
|
|
|
7
7
|
exports.boardSessions = boardSessions;
|
|
8
8
|
const runtime_1 = require("../../nerves/runtime");
|
|
9
9
|
const transitions_1 = require("./transitions");
|
|
10
|
-
const
|
|
10
|
+
const BOARD_ACTIVE_ORDER = [
|
|
11
11
|
"blocked",
|
|
12
12
|
"processing",
|
|
13
13
|
"collaborating",
|
|
14
14
|
"drafting",
|
|
15
15
|
"validating",
|
|
16
16
|
"paused",
|
|
17
|
+
];
|
|
18
|
+
const BOARD_TERMINAL_ORDER = [
|
|
17
19
|
"done",
|
|
20
|
+
"cancelled",
|
|
21
|
+
];
|
|
22
|
+
const BOARD_STATUS_ORDER = [
|
|
23
|
+
...BOARD_ACTIVE_ORDER,
|
|
24
|
+
...BOARD_TERMINAL_ORDER,
|
|
18
25
|
];
|
|
19
26
|
function compactName(task) {
|
|
20
27
|
const suffix = task.stem.replace(/^\d{4}-\d{2}-\d{2}-\d{4}-/, "");
|
|
@@ -29,6 +36,7 @@ function groupByStatus(tasks) {
|
|
|
29
36
|
paused: [],
|
|
30
37
|
blocked: [],
|
|
31
38
|
done: [],
|
|
39
|
+
cancelled: [],
|
|
32
40
|
};
|
|
33
41
|
for (const task of tasks) {
|
|
34
42
|
grouped[task.status].push(compactName(task));
|
|
@@ -59,8 +67,26 @@ function activeSessionLines(tasks) {
|
|
|
59
67
|
});
|
|
60
68
|
return active.map((task) => task.stem).sort();
|
|
61
69
|
}
|
|
70
|
+
function activeBridgeLines(tasks) {
|
|
71
|
+
return tasks
|
|
72
|
+
.filter((task) => typeof task.frontmatter.active_bridge === "string" && String(task.frontmatter.active_bridge).trim())
|
|
73
|
+
.map((task) => `${task.stem} -> ${String(task.frontmatter.active_bridge).trim()}`)
|
|
74
|
+
.sort();
|
|
75
|
+
}
|
|
76
|
+
function buildHealthLine(issues) {
|
|
77
|
+
if (issues.length === 0)
|
|
78
|
+
return "health: clean";
|
|
79
|
+
const liveCount = issues.filter((i) => i.category === "live").length;
|
|
80
|
+
const migrationCount = issues.filter((i) => i.category === "migration").length;
|
|
81
|
+
const parts = [];
|
|
82
|
+
if (liveCount > 0)
|
|
83
|
+
parts.push(`${liveCount} live`);
|
|
84
|
+
if (migrationCount > 0)
|
|
85
|
+
parts.push(`${migrationCount} migration`);
|
|
86
|
+
return `health: ${parts.join(", ")}`;
|
|
87
|
+
}
|
|
62
88
|
function actionRequired(index, byStatus) {
|
|
63
|
-
const actions =
|
|
89
|
+
const actions = index.issues.map((issue) => `${issue.code}: ${issue.description} [${issue.target}]`);
|
|
64
90
|
if (byStatus.blocked.length > 0) {
|
|
65
91
|
actions.push(`blocked tasks: ${byStatus.blocked.join(", ")}`);
|
|
66
92
|
}
|
|
@@ -77,14 +103,19 @@ function buildTaskBoard(index) {
|
|
|
77
103
|
meta: { taskCount: index.tasks.length },
|
|
78
104
|
});
|
|
79
105
|
const byStatus = groupByStatus(index.tasks);
|
|
80
|
-
const
|
|
106
|
+
const activeCounts = BOARD_ACTIVE_ORDER.map((status) => `${status}:${byStatus[status].length}`).join(" ");
|
|
81
107
|
const processing = byStatus.processing.length > 0 ? `\n processing: ${byStatus.processing.join(", ")}` : "";
|
|
82
108
|
const blocked = byStatus.blocked.length > 0 ? `\n blocked: ${byStatus.blocked.join(", ")}` : "";
|
|
83
|
-
const
|
|
109
|
+
const terminalParts = BOARD_TERMINAL_ORDER
|
|
110
|
+
.filter((status) => byStatus[status].length > 0)
|
|
111
|
+
.map((status) => `${status}:${byStatus[status].length}`);
|
|
112
|
+
const terminalLine = terminalParts.length > 0 ? `\n terminal: ${terminalParts.join(" ")}` : "";
|
|
113
|
+
const healthLine = `\n ${buildHealthLine(index.issues)}`;
|
|
114
|
+
const compact = `[Tasks] ${activeCounts}${processing}${blocked}${terminalLine}${healthLine}`;
|
|
84
115
|
const fullLines = [];
|
|
85
116
|
for (const status of BOARD_STATUS_ORDER) {
|
|
86
117
|
const names = byStatus[status];
|
|
87
|
-
if (status === "done" && names.length === 0)
|
|
118
|
+
if ((status === "done" || status === "cancelled") && names.length === 0)
|
|
88
119
|
continue;
|
|
89
120
|
fullLines.push(`## ${status}`);
|
|
90
121
|
fullLines.push(names.length > 0 ? names.map((name) => `- ${name}`).join("\n") : "- (none)");
|
|
@@ -99,13 +130,20 @@ function buildTaskBoard(index) {
|
|
|
99
130
|
fullLines.push("## active sessions");
|
|
100
131
|
fullLines.push(active.map((line) => `- ${line}`).join("\n"));
|
|
101
132
|
}
|
|
133
|
+
const activeBridges = activeBridgeLines(index.tasks);
|
|
134
|
+
if (activeBridges.length > 0) {
|
|
135
|
+
fullLines.push("## active bridges");
|
|
136
|
+
fullLines.push(activeBridges.map((line) => `- ${line}`).join("\n"));
|
|
137
|
+
}
|
|
102
138
|
return {
|
|
103
139
|
compact,
|
|
104
140
|
full: fullLines.join("\n\n"),
|
|
105
141
|
byStatus,
|
|
142
|
+
issues: index.issues,
|
|
106
143
|
actionRequired: actionRequired(index, byStatus),
|
|
107
144
|
unresolvedDependencies: unresolved,
|
|
108
145
|
activeSessions: active,
|
|
146
|
+
activeBridges,
|
|
109
147
|
};
|
|
110
148
|
}
|
|
111
149
|
function boardStatus(board, status) {
|