@ouro.bot/cli 0.1.0-alpha.3 → 0.1.0-alpha.31
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 +70 -9
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/AdoptionSpecialist.ouro/psyche/identities/python.md +30 -0
- package/assets/ouroboros.png +0 -0
- package/changelog.json +87 -0
- package/dist/heart/config.js +66 -4
- package/dist/heart/core.js +75 -2
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/daemon-cli.js +562 -64
- package/dist/heart/daemon/daemon-entry.js +14 -5
- package/dist/heart/daemon/daemon-runtime-sync.js +90 -0
- package/dist/heart/daemon/daemon.js +87 -9
- package/dist/heart/daemon/hatch-animation.js +35 -0
- package/dist/heart/daemon/hatch-flow.js +2 -11
- package/dist/heart/daemon/hatch-specialist.js +6 -1
- package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
- package/dist/heart/daemon/launchd.js +134 -0
- package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
- package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
- package/dist/heart/daemon/ouro-path-installer.js +178 -0
- package/dist/heart/daemon/ouro-uti.js +11 -2
- package/dist/heart/daemon/process-manager.js +1 -1
- package/dist/heart/daemon/run-hooks.js +37 -0
- package/dist/heart/daemon/runtime-logging.js +9 -5
- package/dist/heart/daemon/runtime-metadata.js +118 -0
- package/dist/heart/daemon/sense-manager.js +266 -0
- package/dist/heart/daemon/specialist-orchestrator.js +129 -0
- package/dist/heart/daemon/specialist-prompt.js +98 -0
- package/dist/heart/daemon/specialist-tools.js +237 -0
- package/dist/heart/daemon/staged-restart.js +114 -0
- package/dist/heart/daemon/subagent-installer.js +10 -1
- package/dist/heart/daemon/update-checker.js +103 -0
- package/dist/heart/daemon/update-hooks.js +138 -0
- package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
- package/dist/heart/identity.js +85 -1
- package/dist/heart/providers/anthropic.js +19 -2
- package/dist/heart/sense-truth.js +61 -0
- package/dist/heart/streaming.js +99 -21
- package/dist/mind/bundle-manifest.js +69 -0
- package/dist/mind/first-impressions.js +2 -1
- package/dist/mind/friends/channel.js +8 -0
- package/dist/mind/friends/types.js +1 -1
- package/dist/mind/phrases.js +1 -0
- package/dist/mind/prompt.js +94 -3
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/coding/feedback.js +134 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +61 -2
- package/dist/repertoire/coding/spawner.js +3 -3
- package/dist/repertoire/coding/tools.js +41 -2
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/tools-base.js +69 -5
- package/dist/repertoire/tools-teams.js +57 -4
- package/dist/repertoire/tools.js +44 -11
- package/dist/senses/bluebubbles-client.js +434 -0
- package/dist/senses/bluebubbles-entry.js +11 -0
- package/dist/senses/bluebubbles-media.js +338 -0
- package/dist/senses/bluebubbles-model.js +251 -0
- package/dist/senses/bluebubbles-mutation-log.js +76 -0
- package/dist/senses/bluebubbles-session-cleanup.js +72 -0
- package/dist/senses/bluebubbles.js +449 -0
- package/dist/senses/cli.js +299 -133
- package/dist/senses/debug-activity.js +108 -0
- package/dist/senses/teams.js +173 -54
- package/package.json +15 -6
- package/subagents/work-doer.md +26 -24
- package/subagents/work-merger.md +24 -30
- package/subagents/work-planner.md +34 -25
- package/dist/inner-worker-entry.js +0 -4
|
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
5
5
|
exports.isIdentityProvider = isIdentityProvider;
|
|
6
6
|
exports.isIntegration = isIntegration;
|
|
7
7
|
const runtime_1 = require("../../nerves/runtime");
|
|
8
|
-
const IDENTITY_PROVIDERS = new Set(["aad", "local", "teams-conversation"]);
|
|
8
|
+
const IDENTITY_PROVIDERS = new Set(["aad", "local", "teams-conversation", "imessage-handle"]);
|
|
9
9
|
function isIdentityProvider(value) {
|
|
10
10
|
(0, runtime_1.emitNervesEvent)({
|
|
11
11
|
component: "friends",
|
package/dist/mind/phrases.js
CHANGED
package/dist/mind/prompt.js
CHANGED
|
@@ -47,10 +47,12 @@ const identity_1 = require("../heart/identity");
|
|
|
47
47
|
const os = __importStar(require("os"));
|
|
48
48
|
const channel_1 = require("./friends/channel");
|
|
49
49
|
const runtime_1 = require("../nerves/runtime");
|
|
50
|
+
const bundle_manifest_1 = require("./bundle-manifest");
|
|
50
51
|
const first_impressions_1 = require("./first-impressions");
|
|
51
52
|
const tasks_1 = require("../repertoire/tasks");
|
|
52
53
|
// Lazy-loaded psyche text cache
|
|
53
54
|
let _psycheCache = null;
|
|
55
|
+
let _senseStatusLinesCache = null;
|
|
54
56
|
function loadPsycheFile(name) {
|
|
55
57
|
try {
|
|
56
58
|
const psycheDir = path.join((0, identity_1.getAgentRoot)(), "psyche");
|
|
@@ -74,6 +76,7 @@ function loadPsyche() {
|
|
|
74
76
|
}
|
|
75
77
|
function resetPsycheCache() {
|
|
76
78
|
_psycheCache = null;
|
|
79
|
+
_senseStatusLinesCache = null;
|
|
77
80
|
}
|
|
78
81
|
const DEFAULT_ACTIVE_THRESHOLD_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
79
82
|
function resolveFriendName(friendId, friendsDir, agentName) {
|
|
@@ -187,22 +190,108 @@ function aspirationsSection() {
|
|
|
187
190
|
return "";
|
|
188
191
|
return `## my aspirations\n${text}`;
|
|
189
192
|
}
|
|
193
|
+
function readBundleMeta() {
|
|
194
|
+
try {
|
|
195
|
+
const metaPath = path.join((0, identity_1.getAgentRoot)(), "bundle-meta.json");
|
|
196
|
+
const raw = fs.readFileSync(metaPath, "utf-8");
|
|
197
|
+
return JSON.parse(raw);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
190
203
|
function runtimeInfoSection(channel) {
|
|
191
204
|
const lines = [];
|
|
192
205
|
const agentName = (0, identity_1.getAgentName)();
|
|
206
|
+
const currentVersion = (0, bundle_manifest_1.getPackageVersion)();
|
|
193
207
|
lines.push(`## runtime`);
|
|
194
208
|
lines.push(`agent: ${agentName}`);
|
|
209
|
+
lines.push(`runtime version: ${currentVersion}`);
|
|
210
|
+
const bundleMeta = readBundleMeta();
|
|
211
|
+
if (bundleMeta?.previousRuntimeVersion && bundleMeta.previousRuntimeVersion !== currentVersion) {
|
|
212
|
+
lines.push(`previously: ${bundleMeta.previousRuntimeVersion}`);
|
|
213
|
+
}
|
|
214
|
+
lines.push(`changelog available at: ${(0, bundle_manifest_1.getChangelogPath)()}`);
|
|
195
215
|
lines.push(`cwd: ${process.cwd()}`);
|
|
196
216
|
lines.push(`channel: ${channel}`);
|
|
217
|
+
lines.push(`current sense: ${channel}`);
|
|
197
218
|
lines.push(`i can read and modify my own source code.`);
|
|
198
219
|
if (channel === "cli") {
|
|
199
220
|
lines.push("i introduce myself on boot with a fun random greeting.");
|
|
200
221
|
}
|
|
222
|
+
else if (channel === "bluebubbles") {
|
|
223
|
+
lines.push("i am responding in iMessage through BlueBubbles. i keep replies short and phone-native. i do not use markdown. i do not introduce myself on boot.");
|
|
224
|
+
}
|
|
201
225
|
else {
|
|
202
226
|
lines.push("i am responding in Microsoft Teams. i keep responses concise. i use markdown formatting. i do not introduce myself on boot.");
|
|
203
227
|
}
|
|
228
|
+
lines.push("");
|
|
229
|
+
lines.push(...senseRuntimeGuidance(channel));
|
|
204
230
|
return lines.join("\n");
|
|
205
231
|
}
|
|
232
|
+
function hasTextField(record, key) {
|
|
233
|
+
return typeof record?.[key] === "string" && record[key].trim().length > 0;
|
|
234
|
+
}
|
|
235
|
+
function localSenseStatusLines() {
|
|
236
|
+
if (_senseStatusLinesCache) {
|
|
237
|
+
return [..._senseStatusLinesCache];
|
|
238
|
+
}
|
|
239
|
+
const config = (0, identity_1.loadAgentConfig)();
|
|
240
|
+
const senses = config.senses ?? {
|
|
241
|
+
cli: { enabled: true },
|
|
242
|
+
teams: { enabled: false },
|
|
243
|
+
bluebubbles: { enabled: false },
|
|
244
|
+
};
|
|
245
|
+
let payload = {};
|
|
246
|
+
try {
|
|
247
|
+
const raw = fs.readFileSync((0, identity_1.getAgentSecretsPath)(), "utf-8");
|
|
248
|
+
const parsed = JSON.parse(raw);
|
|
249
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
250
|
+
payload = parsed;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
payload = {};
|
|
255
|
+
}
|
|
256
|
+
const teams = payload.teams;
|
|
257
|
+
const bluebubbles = payload.bluebubbles;
|
|
258
|
+
const configured = {
|
|
259
|
+
cli: true,
|
|
260
|
+
teams: hasTextField(teams, "clientId") && hasTextField(teams, "clientSecret") && hasTextField(teams, "tenantId"),
|
|
261
|
+
bluebubbles: hasTextField(bluebubbles, "serverUrl") && hasTextField(bluebubbles, "password"),
|
|
262
|
+
};
|
|
263
|
+
const rows = [
|
|
264
|
+
{ label: "CLI", status: "interactive" },
|
|
265
|
+
{
|
|
266
|
+
label: "Teams",
|
|
267
|
+
status: !senses.teams.enabled ? "disabled" : configured.teams ? "ready" : "needs_config",
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
label: "BlueBubbles",
|
|
271
|
+
status: !senses.bluebubbles.enabled ? "disabled" : configured.bluebubbles ? "ready" : "needs_config",
|
|
272
|
+
},
|
|
273
|
+
];
|
|
274
|
+
_senseStatusLinesCache = rows.map((row) => `- ${row.label}: ${row.status}`);
|
|
275
|
+
return [..._senseStatusLinesCache];
|
|
276
|
+
}
|
|
277
|
+
function senseRuntimeGuidance(channel) {
|
|
278
|
+
const lines = ["available senses:"];
|
|
279
|
+
lines.push(...localSenseStatusLines());
|
|
280
|
+
lines.push("sense states:");
|
|
281
|
+
lines.push("- interactive = available when opened by the user instead of kept running by the daemon");
|
|
282
|
+
lines.push("- disabled = turned off in agent.json");
|
|
283
|
+
lines.push("- needs_config = enabled but missing required secrets.json values");
|
|
284
|
+
lines.push("- ready = enabled and configured; `ouro up` should bring it online");
|
|
285
|
+
lines.push("- running = enabled and currently active");
|
|
286
|
+
lines.push("- error = enabled but unhealthy");
|
|
287
|
+
lines.push("If asked how to enable another sense, I explain the relevant agent.json senses entry and required secrets.json fields instead of guessing.");
|
|
288
|
+
lines.push("teams setup truth: enable `senses.teams.enabled`, then provide `teams.clientId`, `teams.clientSecret`, and `teams.tenantId` in secrets.json.");
|
|
289
|
+
lines.push("bluebubbles setup truth: enable `senses.bluebubbles.enabled`, then provide `bluebubbles.serverUrl` and `bluebubbles.password` in secrets.json.");
|
|
290
|
+
if (channel === "cli") {
|
|
291
|
+
lines.push("cli is interactive: it is available when the user opens it, not something `ouro up` daemonizes.");
|
|
292
|
+
}
|
|
293
|
+
return lines;
|
|
294
|
+
}
|
|
206
295
|
function providerSection() {
|
|
207
296
|
return `## my provider\n${(0, core_1.getProviderDisplayLabel)()}`;
|
|
208
297
|
}
|
|
@@ -210,8 +299,8 @@ function dateSection() {
|
|
|
210
299
|
const today = new Date().toISOString().slice(0, 10);
|
|
211
300
|
return `current date: ${today}`;
|
|
212
301
|
}
|
|
213
|
-
function toolsSection(channel, options) {
|
|
214
|
-
const channelTools = (0, tools_1.getToolsForChannel)((0, channel_1.getChannelCapabilities)(channel));
|
|
302
|
+
function toolsSection(channel, options, context) {
|
|
303
|
+
const channelTools = (0, tools_1.getToolsForChannel)((0, channel_1.getChannelCapabilities)(channel), undefined, context);
|
|
215
304
|
const activeTools = (options?.toolChoiceRequired ?? true) ? [...channelTools, tools_1.finalAnswerTool] : channelTools;
|
|
216
305
|
const list = activeTools
|
|
217
306
|
.map((t) => `- ${t.function.name}: ${t.function.description}`)
|
|
@@ -316,6 +405,8 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
316
405
|
message: "buildSystem started",
|
|
317
406
|
meta: { channel, has_context: Boolean(context), tool_choice_required: Boolean(options?.toolChoiceRequired) },
|
|
318
407
|
});
|
|
408
|
+
// Backfill bundle-meta.json for existing agents that don't have one
|
|
409
|
+
(0, bundle_manifest_1.backfillBundleMeta)((0, identity_1.getAgentRoot)());
|
|
319
410
|
const system = [
|
|
320
411
|
soulSection(),
|
|
321
412
|
identitySection(),
|
|
@@ -325,7 +416,7 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
325
416
|
runtimeInfoSection(channel),
|
|
326
417
|
providerSection(),
|
|
327
418
|
dateSection(),
|
|
328
|
-
toolsSection(channel, options),
|
|
419
|
+
toolsSection(channel, options, context),
|
|
329
420
|
skillsSection(),
|
|
330
421
|
taskBoardSection(),
|
|
331
422
|
buildSessionSummary({
|
|
@@ -5,20 +5,33 @@ const config_1 = require("../heart/config");
|
|
|
5
5
|
const nerves_1 = require("../nerves");
|
|
6
6
|
const runtime_1 = require("./runtime");
|
|
7
7
|
const runtime_2 = require("./runtime");
|
|
8
|
+
const LEVEL_PRIORITY = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
9
|
+
/** Wrap a sink so it only receives events at or above the given level. */
|
|
10
|
+
/* v8 ignore start -- internal filter plumbing, exercised via integration @preserve */
|
|
11
|
+
function filterSink(sink, minLevel) {
|
|
12
|
+
const minPriority = LEVEL_PRIORITY[minLevel] ?? 0;
|
|
13
|
+
return (entry) => {
|
|
14
|
+
if ((LEVEL_PRIORITY[entry.level] ?? 0) >= minPriority)
|
|
15
|
+
sink(entry);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
8
18
|
function resolveCliSinks(sinks) {
|
|
9
19
|
const requested = sinks && sinks.length > 0 ? sinks : ["terminal", "ndjson"];
|
|
10
20
|
return [...new Set(requested)];
|
|
11
21
|
}
|
|
12
22
|
function configureCliRuntimeLogger(_friendId, options = {}) {
|
|
13
23
|
const sinkKinds = resolveCliSinks(options.sinks);
|
|
24
|
+
const level = options.level ?? "info";
|
|
14
25
|
const sinks = sinkKinds.map((sinkKind) => {
|
|
15
26
|
if (sinkKind === "terminal") {
|
|
16
|
-
|
|
27
|
+
// Terminal only shows warnings and errors — INFO is too noisy
|
|
28
|
+
// for an interactive session. Full detail goes to the ndjson file.
|
|
29
|
+
return filterSink((0, nerves_1.createTerminalSink)(), "warn");
|
|
17
30
|
}
|
|
18
31
|
return (0, nerves_1.createNdjsonFileSink)((0, config_1.logPath)("cli", "runtime"));
|
|
19
32
|
});
|
|
20
33
|
const logger = (0, nerves_1.createLogger)({
|
|
21
|
-
level
|
|
34
|
+
level,
|
|
22
35
|
sinks,
|
|
23
36
|
});
|
|
24
37
|
(0, runtime_2.setRuntimeLogger)(logger);
|
|
@@ -28,8 +28,10 @@ function resolveContentType(method, path) {
|
|
|
28
28
|
: "application/json";
|
|
29
29
|
}
|
|
30
30
|
// Generic ADO API request. Returns response body as pretty-printed JSON string.
|
|
31
|
-
|
|
31
|
+
// `host` overrides the base URL for non-standard APIs (e.g. "vsapm.dev.azure.com", "vssps.dev.azure.com").
|
|
32
|
+
async function adoRequest(token, method, org, path, body, host) {
|
|
32
33
|
try {
|
|
34
|
+
const base = host ? `https://${host}/${org}` : `${ADO_BASE}/${org}`;
|
|
33
35
|
(0, runtime_1.emitNervesEvent)({
|
|
34
36
|
event: "client.request_start",
|
|
35
37
|
component: "clients",
|
|
@@ -37,7 +39,7 @@ async function adoRequest(token, method, org, path, body) {
|
|
|
37
39
|
meta: { client: "ado", method, org, path },
|
|
38
40
|
});
|
|
39
41
|
const fullPath = ensureApiVersion(path);
|
|
40
|
-
const url = `${
|
|
42
|
+
const url = `${base}${fullPath}`;
|
|
41
43
|
const contentType = resolveContentType(method, path);
|
|
42
44
|
const opts = {
|
|
43
45
|
method,
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatCodingTail = formatCodingTail;
|
|
4
|
+
exports.attachCodingSessionFeedback = attachCodingSessionFeedback;
|
|
5
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
6
|
+
const TERMINAL_UPDATE_KINDS = new Set(["completed", "failed", "killed"]);
|
|
7
|
+
function clip(text, maxLength = 280) {
|
|
8
|
+
const trimmed = text.trim();
|
|
9
|
+
if (trimmed.length <= maxLength)
|
|
10
|
+
return trimmed;
|
|
11
|
+
return `${trimmed.slice(0, maxLength - 3)}...`;
|
|
12
|
+
}
|
|
13
|
+
function isNoiseLine(line) {
|
|
14
|
+
return (/^-+$/.test(line)
|
|
15
|
+
|| /^Reading prompt from stdin/i.test(line)
|
|
16
|
+
|| /^OpenAI Codex v/i.test(line)
|
|
17
|
+
|| /^workdir:/i.test(line)
|
|
18
|
+
|| /^model:/i.test(line)
|
|
19
|
+
|| /^provider:/i.test(line)
|
|
20
|
+
|| /^approval:/i.test(line)
|
|
21
|
+
|| /^sandbox:/i.test(line)
|
|
22
|
+
|| /^reasoning effort:/i.test(line)
|
|
23
|
+
|| /^reasoning summaries:/i.test(line)
|
|
24
|
+
|| /^session id:/i.test(line)
|
|
25
|
+
|| /^mcp startup:/i.test(line)
|
|
26
|
+
|| /^tokens used$/i.test(line)
|
|
27
|
+
|| /^\d{1,3}(,\d{3})*$/.test(line)
|
|
28
|
+
|| /^\d{4}-\d{2}-\d{2}T.*\bWARN\b/.test(line)
|
|
29
|
+
|| line === "user"
|
|
30
|
+
|| line === "codex");
|
|
31
|
+
}
|
|
32
|
+
function lastMeaningfulLine(text) {
|
|
33
|
+
if (!text)
|
|
34
|
+
return null;
|
|
35
|
+
const lines = text
|
|
36
|
+
.split(/\r?\n/)
|
|
37
|
+
.map((line) => line.trim())
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.filter((line) => !isNoiseLine(line));
|
|
40
|
+
if (lines.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
return clip(lines.at(-1));
|
|
43
|
+
}
|
|
44
|
+
function formatSessionLabel(session) {
|
|
45
|
+
return `${session.runner} ${session.id}`;
|
|
46
|
+
}
|
|
47
|
+
function isSafeProgressSnippet(snippet) {
|
|
48
|
+
const wordCount = snippet.split(/\s+/).filter(Boolean).length;
|
|
49
|
+
return (snippet.length <= 80
|
|
50
|
+
&& wordCount <= 8
|
|
51
|
+
&& !snippet.includes(":")
|
|
52
|
+
&& !snippet.startsWith("**")
|
|
53
|
+
&& !/^Respond with\b/i.test(snippet)
|
|
54
|
+
&& !/^Coding session metadata\b/i.test(snippet)
|
|
55
|
+
&& !/^sessionId\b/i.test(snippet)
|
|
56
|
+
&& !/^taskRef\b/i.test(snippet)
|
|
57
|
+
&& !/^parentAgent\b/i.test(snippet));
|
|
58
|
+
}
|
|
59
|
+
function pickUpdateSnippet(update) {
|
|
60
|
+
return (lastMeaningfulLine(update.text)
|
|
61
|
+
?? lastMeaningfulLine(update.session.stderrTail)
|
|
62
|
+
?? lastMeaningfulLine(update.session.stdoutTail));
|
|
63
|
+
}
|
|
64
|
+
function formatUpdateMessage(update) {
|
|
65
|
+
const label = formatSessionLabel(update.session);
|
|
66
|
+
const snippet = pickUpdateSnippet(update);
|
|
67
|
+
switch (update.kind) {
|
|
68
|
+
case "progress":
|
|
69
|
+
return snippet && isSafeProgressSnippet(snippet) ? `${label}: ${snippet}` : null;
|
|
70
|
+
case "waiting_input":
|
|
71
|
+
return snippet ? `${label} waiting: ${snippet}` : `${label} waiting`;
|
|
72
|
+
case "stalled":
|
|
73
|
+
return snippet ? `${label} stalled: ${snippet}` : `${label} stalled`;
|
|
74
|
+
case "completed":
|
|
75
|
+
return snippet ? `${label} completed: ${snippet}` : `${label} completed`;
|
|
76
|
+
case "failed":
|
|
77
|
+
return snippet ? `${label} failed: ${snippet}` : `${label} failed`;
|
|
78
|
+
case "killed":
|
|
79
|
+
return `${label} killed`;
|
|
80
|
+
case "spawned":
|
|
81
|
+
return `${label} started`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function formatCodingTail(session) {
|
|
85
|
+
const stdout = session.stdoutTail.trim() || "(empty)";
|
|
86
|
+
const stderr = session.stderrTail.trim() || "(empty)";
|
|
87
|
+
return [
|
|
88
|
+
`sessionId: ${session.id}`,
|
|
89
|
+
`runner: ${session.runner}`,
|
|
90
|
+
`status: ${session.status}`,
|
|
91
|
+
`workdir: ${session.workdir}`,
|
|
92
|
+
"",
|
|
93
|
+
"[stdout]",
|
|
94
|
+
stdout,
|
|
95
|
+
"",
|
|
96
|
+
"[stderr]",
|
|
97
|
+
stderr,
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
function attachCodingSessionFeedback(manager, session, target) {
|
|
101
|
+
let lastMessage = "";
|
|
102
|
+
let closed = false;
|
|
103
|
+
let unsubscribe = () => { };
|
|
104
|
+
const sendMessage = (message) => {
|
|
105
|
+
if (closed || !message || message === lastMessage) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
lastMessage = message;
|
|
109
|
+
void Promise.resolve(target.send(message)).catch((error) => {
|
|
110
|
+
(0, runtime_1.emitNervesEvent)({
|
|
111
|
+
level: "warn",
|
|
112
|
+
component: "repertoire",
|
|
113
|
+
event: "repertoire.coding_feedback_error",
|
|
114
|
+
message: "coding feedback transport failed",
|
|
115
|
+
meta: {
|
|
116
|
+
sessionId: session.id,
|
|
117
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
sendMessage(formatUpdateMessage({ kind: "spawned", session }));
|
|
123
|
+
unsubscribe = manager.subscribe(session.id, async (update) => {
|
|
124
|
+
sendMessage(formatUpdateMessage(update));
|
|
125
|
+
if (TERMINAL_UPDATE_KINDS.has(update.kind)) {
|
|
126
|
+
closed = true;
|
|
127
|
+
unsubscribe();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return () => {
|
|
131
|
+
closed = true;
|
|
132
|
+
unsubscribe();
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.formatCodingMonitorReport = exports.CodingSessionMonitor = exports.CodingSessionManager = void 0;
|
|
3
|
+
exports.formatCodingTail = exports.attachCodingSessionFeedback = exports.formatCodingMonitorReport = exports.CodingSessionMonitor = exports.CodingSessionManager = void 0;
|
|
4
4
|
exports.getCodingSessionManager = getCodingSessionManager;
|
|
5
5
|
exports.resetCodingSessionManager = resetCodingSessionManager;
|
|
6
6
|
const runtime_1 = require("../../nerves/runtime");
|
|
@@ -34,3 +34,6 @@ var monitor_1 = require("./monitor");
|
|
|
34
34
|
Object.defineProperty(exports, "CodingSessionMonitor", { enumerable: true, get: function () { return monitor_1.CodingSessionMonitor; } });
|
|
35
35
|
var reporter_1 = require("./reporter");
|
|
36
36
|
Object.defineProperty(exports, "formatCodingMonitorReport", { enumerable: true, get: function () { return reporter_1.formatCodingMonitorReport; } });
|
|
37
|
+
var feedback_1 = require("./feedback");
|
|
38
|
+
Object.defineProperty(exports, "attachCodingSessionFeedback", { enumerable: true, get: function () { return feedback_1.attachCodingSessionFeedback; } });
|
|
39
|
+
Object.defineProperty(exports, "formatCodingTail", { enumerable: true, get: function () { return feedback_1.formatCodingTail; } });
|
|
@@ -63,6 +63,8 @@ function isPidAlive(pid) {
|
|
|
63
63
|
function cloneSession(session) {
|
|
64
64
|
return {
|
|
65
65
|
...session,
|
|
66
|
+
stdoutTail: session.stdoutTail,
|
|
67
|
+
stderrTail: session.stderrTail,
|
|
66
68
|
failure: session.failure
|
|
67
69
|
? {
|
|
68
70
|
...session.failure,
|
|
@@ -115,6 +117,7 @@ function defaultFailureDiagnostics(code, signal, command, args, stdoutTail, stde
|
|
|
115
117
|
}
|
|
116
118
|
class CodingSessionManager {
|
|
117
119
|
records = new Map();
|
|
120
|
+
listeners = new Map();
|
|
118
121
|
spawnProcess;
|
|
119
122
|
nowIso;
|
|
120
123
|
maxRestarts;
|
|
@@ -158,6 +161,8 @@ class CodingSessionManager {
|
|
|
158
161
|
scopeFile: normalizedRequest.scopeFile,
|
|
159
162
|
stateFile: normalizedRequest.stateFile,
|
|
160
163
|
status: "spawning",
|
|
164
|
+
stdoutTail: "",
|
|
165
|
+
stderrTail: "",
|
|
161
166
|
pid: null,
|
|
162
167
|
startedAt: now,
|
|
163
168
|
lastActivityAt: now,
|
|
@@ -188,6 +193,7 @@ class CodingSessionManager {
|
|
|
188
193
|
meta: { id, runner: normalizedRequest.runner, pid: session.pid },
|
|
189
194
|
});
|
|
190
195
|
this.persistState();
|
|
196
|
+
this.notifyListeners(id, { kind: "spawned", session: cloneSession(session) });
|
|
191
197
|
return cloneSession(session);
|
|
192
198
|
}
|
|
193
199
|
listSessions() {
|
|
@@ -199,6 +205,20 @@ class CodingSessionManager {
|
|
|
199
205
|
const record = this.records.get(sessionId);
|
|
200
206
|
return record ? cloneSession(record.session) : null;
|
|
201
207
|
}
|
|
208
|
+
subscribe(sessionId, listener) {
|
|
209
|
+
const listeners = this.listeners.get(sessionId) ?? new Set();
|
|
210
|
+
listeners.add(listener);
|
|
211
|
+
this.listeners.set(sessionId, listeners);
|
|
212
|
+
return () => {
|
|
213
|
+
const current = this.listeners.get(sessionId);
|
|
214
|
+
if (!current)
|
|
215
|
+
return;
|
|
216
|
+
current.delete(listener);
|
|
217
|
+
if (current.size === 0) {
|
|
218
|
+
this.listeners.delete(sessionId);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
202
222
|
sendInput(sessionId, input) {
|
|
203
223
|
const record = this.records.get(sessionId);
|
|
204
224
|
if (!record || !record.process) {
|
|
@@ -234,6 +254,7 @@ class CodingSessionManager {
|
|
|
234
254
|
meta: { id: sessionId },
|
|
235
255
|
});
|
|
236
256
|
this.persistState();
|
|
257
|
+
this.notifyListeners(sessionId, { kind: "killed", session: cloneSession(record.session) });
|
|
237
258
|
return { ok: true, message: `killed ${sessionId}` };
|
|
238
259
|
}
|
|
239
260
|
checkStalls(nowMs = Date.now()) {
|
|
@@ -254,6 +275,7 @@ class CodingSessionManager {
|
|
|
254
275
|
message: "coding session stalled",
|
|
255
276
|
meta: { id: record.session.id, elapsedMs: elapsed },
|
|
256
277
|
});
|
|
278
|
+
this.notifyListeners(record.session.id, { kind: "stalled", session: cloneSession(record.session) });
|
|
257
279
|
if (record.request.autoRestartOnStall !== false && record.session.restartCount < this.maxRestarts) {
|
|
258
280
|
this.restartSession(record, "stalled");
|
|
259
281
|
}
|
|
@@ -297,18 +319,23 @@ class CodingSessionManager {
|
|
|
297
319
|
}
|
|
298
320
|
onOutput(record, text, stream) {
|
|
299
321
|
record.session.lastActivityAt = this.nowIso();
|
|
322
|
+
let updateKind = "progress";
|
|
300
323
|
if (stream === "stdout") {
|
|
301
324
|
record.stdoutTail = appendTail(record.stdoutTail, text);
|
|
325
|
+
record.session.stdoutTail = record.stdoutTail;
|
|
302
326
|
}
|
|
303
327
|
else {
|
|
304
328
|
record.stderrTail = appendTail(record.stderrTail, text);
|
|
329
|
+
record.session.stderrTail = record.stderrTail;
|
|
305
330
|
}
|
|
306
331
|
if (text.includes("status: NEEDS_REVIEW") || text.includes("❌ blocked")) {
|
|
307
332
|
record.session.status = "waiting_input";
|
|
333
|
+
updateKind = "waiting_input";
|
|
308
334
|
}
|
|
309
335
|
if (text.includes("✅ all units complete")) {
|
|
310
336
|
record.session.status = "completed";
|
|
311
337
|
record.session.endedAt = this.nowIso();
|
|
338
|
+
updateKind = "completed";
|
|
312
339
|
}
|
|
313
340
|
(0, runtime_1.emitNervesEvent)({
|
|
314
341
|
component: "repertoire",
|
|
@@ -317,6 +344,12 @@ class CodingSessionManager {
|
|
|
317
344
|
meta: { id: record.session.id, status: record.session.status },
|
|
318
345
|
});
|
|
319
346
|
this.persistState();
|
|
347
|
+
this.notifyListeners(record.session.id, {
|
|
348
|
+
kind: updateKind,
|
|
349
|
+
session: cloneSession(record.session),
|
|
350
|
+
stream,
|
|
351
|
+
text,
|
|
352
|
+
});
|
|
320
353
|
}
|
|
321
354
|
onExit(record, code, signal) {
|
|
322
355
|
if (!record.process)
|
|
@@ -334,6 +367,7 @@ class CodingSessionManager {
|
|
|
334
367
|
record.session.status = "completed";
|
|
335
368
|
record.session.endedAt = this.nowIso();
|
|
336
369
|
this.persistState();
|
|
370
|
+
this.notifyListeners(record.session.id, { kind: "completed", session: cloneSession(record.session) });
|
|
337
371
|
return;
|
|
338
372
|
}
|
|
339
373
|
if (record.request.autoRestartOnCrash !== false && record.session.restartCount < this.maxRestarts) {
|
|
@@ -351,6 +385,7 @@ class CodingSessionManager {
|
|
|
351
385
|
meta: { id: record.session.id, code, signal, command: record.command },
|
|
352
386
|
});
|
|
353
387
|
this.persistState();
|
|
388
|
+
this.notifyListeners(record.session.id, { kind: "failed", session: cloneSession(record.session) });
|
|
354
389
|
}
|
|
355
390
|
restartSession(record, reason) {
|
|
356
391
|
const replacement = normalizeSpawnResult(this.spawnProcess(record.request));
|
|
@@ -359,6 +394,8 @@ class CodingSessionManager {
|
|
|
359
394
|
record.args = [...replacement.args];
|
|
360
395
|
record.stdoutTail = "";
|
|
361
396
|
record.stderrTail = "";
|
|
397
|
+
record.session.stdoutTail = "";
|
|
398
|
+
record.session.stderrTail = "";
|
|
362
399
|
record.session.pid = replacement.process.pid ?? null;
|
|
363
400
|
record.session.restartCount += 1;
|
|
364
401
|
record.session.status = "running";
|
|
@@ -375,6 +412,26 @@ class CodingSessionManager {
|
|
|
375
412
|
});
|
|
376
413
|
this.persistState();
|
|
377
414
|
}
|
|
415
|
+
notifyListeners(sessionId, update) {
|
|
416
|
+
const listeners = this.listeners.get(sessionId);
|
|
417
|
+
if (!listeners || listeners.size === 0)
|
|
418
|
+
return;
|
|
419
|
+
for (const listener of listeners) {
|
|
420
|
+
void Promise.resolve(listener(update)).catch((error) => {
|
|
421
|
+
(0, runtime_1.emitNervesEvent)({
|
|
422
|
+
level: "warn",
|
|
423
|
+
component: "repertoire",
|
|
424
|
+
event: "repertoire.coding_feedback_listener_error",
|
|
425
|
+
message: "coding session listener failed",
|
|
426
|
+
meta: {
|
|
427
|
+
sessionId,
|
|
428
|
+
kind: update.kind,
|
|
429
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
378
435
|
loadPersistedState() {
|
|
379
436
|
if (!this.existsSync(this.stateFilePath)) {
|
|
380
437
|
return;
|
|
@@ -433,6 +490,8 @@ class CodingSessionManager {
|
|
|
433
490
|
...session,
|
|
434
491
|
taskRef: session.taskRef ?? normalizedRequest.taskRef,
|
|
435
492
|
failure: session.failure ?? null,
|
|
493
|
+
stdoutTail: session.stdoutTail ?? session.failure?.stdoutTail ?? "",
|
|
494
|
+
stderrTail: session.stderrTail ?? session.failure?.stderrTail ?? "",
|
|
436
495
|
};
|
|
437
496
|
if (typeof normalizedSession.pid === "number") {
|
|
438
497
|
const alive = this.pidAlive(normalizedSession.pid);
|
|
@@ -451,8 +510,8 @@ class CodingSessionManager {
|
|
|
451
510
|
process: null,
|
|
452
511
|
command: normalizedSession.failure?.command ?? "restored",
|
|
453
512
|
args: normalizedSession.failure ? [...normalizedSession.failure.args] : [],
|
|
454
|
-
stdoutTail: normalizedSession.
|
|
455
|
-
stderrTail: normalizedSession.
|
|
513
|
+
stdoutTail: normalizedSession.stdoutTail,
|
|
514
|
+
stderrTail: normalizedSession.stderrTail,
|
|
456
515
|
});
|
|
457
516
|
this.sequence = Math.max(this.sequence, extractSequence(normalizedSession.id));
|
|
458
517
|
}
|
|
@@ -43,11 +43,11 @@ function buildCommandArgs(runner, workdir) {
|
|
|
43
43
|
command: "claude",
|
|
44
44
|
args: [
|
|
45
45
|
"-p",
|
|
46
|
+
"--verbose",
|
|
47
|
+
"--no-session-persistence",
|
|
46
48
|
"--dangerously-skip-permissions",
|
|
47
49
|
"--add-dir",
|
|
48
50
|
workdir,
|
|
49
|
-
"--input-format",
|
|
50
|
-
"stream-json",
|
|
51
51
|
"--output-format",
|
|
52
52
|
"stream-json",
|
|
53
53
|
],
|
|
@@ -91,7 +91,7 @@ function spawnCodingProcess(request, deps = {}) {
|
|
|
91
91
|
cwd: request.workdir,
|
|
92
92
|
stdio: ["pipe", "pipe", "pipe"],
|
|
93
93
|
});
|
|
94
|
-
proc.stdin.
|
|
94
|
+
proc.stdin.end(`${prompt}\n`);
|
|
95
95
|
(0, runtime_1.emitNervesEvent)({
|
|
96
96
|
component: "repertoire",
|
|
97
97
|
event: "repertoire.coding_spawn_end",
|
|
@@ -61,6 +61,20 @@ const codingStatusTool = {
|
|
|
61
61
|
},
|
|
62
62
|
},
|
|
63
63
|
};
|
|
64
|
+
const codingTailTool = {
|
|
65
|
+
type: "function",
|
|
66
|
+
function: {
|
|
67
|
+
name: "coding_tail",
|
|
68
|
+
description: "show recent stdout/stderr tail for a coding session in a readable format",
|
|
69
|
+
parameters: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
sessionId: { type: "string" },
|
|
73
|
+
},
|
|
74
|
+
required: ["sessionId"],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
64
78
|
const codingSendInputTool = {
|
|
65
79
|
type: "function",
|
|
66
80
|
function: {
|
|
@@ -93,7 +107,7 @@ const codingKillTool = {
|
|
|
93
107
|
exports.codingToolDefinitions = [
|
|
94
108
|
{
|
|
95
109
|
tool: codingSpawnTool,
|
|
96
|
-
handler: async (args) => {
|
|
110
|
+
handler: async (args, ctx) => {
|
|
97
111
|
emitCodingToolEvent("coding_spawn");
|
|
98
112
|
const rawRunner = requireArg(args, "runner");
|
|
99
113
|
if (!rawRunner)
|
|
@@ -122,7 +136,19 @@ exports.codingToolDefinitions = [
|
|
|
122
136
|
const stateFile = optionalArg(args, "stateFile");
|
|
123
137
|
if (stateFile)
|
|
124
138
|
request.stateFile = stateFile;
|
|
125
|
-
const
|
|
139
|
+
const manager = (0, index_1.getCodingSessionManager)();
|
|
140
|
+
const session = await manager.spawnSession(request);
|
|
141
|
+
if (args.runner === "codex" && args.taskRef) {
|
|
142
|
+
(0, runtime_1.emitNervesEvent)({
|
|
143
|
+
component: "repertoire",
|
|
144
|
+
event: "repertoire.coding_codex_spawned",
|
|
145
|
+
message: "spawned codex coding session",
|
|
146
|
+
meta: { sessionId: session.id, taskRef: args.taskRef },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (ctx?.codingFeedback) {
|
|
150
|
+
(0, index_1.attachCodingSessionFeedback)(manager, session, ctx.codingFeedback);
|
|
151
|
+
}
|
|
126
152
|
return JSON.stringify(session);
|
|
127
153
|
},
|
|
128
154
|
},
|
|
@@ -141,6 +167,19 @@ exports.codingToolDefinitions = [
|
|
|
141
167
|
return JSON.stringify(session);
|
|
142
168
|
},
|
|
143
169
|
},
|
|
170
|
+
{
|
|
171
|
+
tool: codingTailTool,
|
|
172
|
+
handler: (args) => {
|
|
173
|
+
emitCodingToolEvent("coding_tail");
|
|
174
|
+
const sessionId = requireArg(args, "sessionId");
|
|
175
|
+
if (!sessionId)
|
|
176
|
+
return "sessionId is required";
|
|
177
|
+
const session = (0, index_1.getCodingSessionManager)().getSession(sessionId);
|
|
178
|
+
if (!session)
|
|
179
|
+
return `session not found: ${sessionId}`;
|
|
180
|
+
return (0, index_1.formatCodingTail)(session);
|
|
181
|
+
},
|
|
182
|
+
},
|
|
144
183
|
{
|
|
145
184
|
tool: codingSendInputTool,
|
|
146
185
|
handler: (args) => {
|