@ouro.bot/cli 0.1.0-alpha.16 → 0.1.0-alpha.18
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/dist/heart/core.js +1 -1
- package/dist/heart/daemon/specialist-prompt.js +1 -0
- package/dist/heart/daemon/specialist-tools.js +8 -28
- package/dist/heart/streaming.js +55 -1
- package/dist/mind/prompt.js +3 -3
- 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/tools-base.js +69 -5
- package/dist/repertoire/tools.js +32 -9
- package/dist/senses/bluebubbles-client.js +159 -5
- package/dist/senses/bluebubbles-media.js +244 -0
- package/dist/senses/bluebubbles.js +108 -19
- package/dist/senses/debug-activity.js +107 -0
- package/package.json +1 -1
- package/subagents/work-doer.md +26 -24
- package/subagents/work-merger.md +24 -30
- package/subagents/work-planner.md +34 -25
package/dist/heart/core.js
CHANGED
|
@@ -341,7 +341,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
341
341
|
}
|
|
342
342
|
catch { /* unsupported */ }
|
|
343
343
|
const toolPreferences = currentContext?.friend?.toolPreferences;
|
|
344
|
-
const baseTools = options?.tools ?? (0, tools_1.getToolsForChannel)(channel ? (0, channel_1.getChannelCapabilities)(channel) : undefined, toolPreferences && Object.keys(toolPreferences).length > 0 ? toolPreferences : undefined);
|
|
344
|
+
const baseTools = options?.tools ?? (0, tools_1.getToolsForChannel)(channel ? (0, channel_1.getChannelCapabilities)(channel) : undefined, toolPreferences && Object.keys(toolPreferences).length > 0 ? toolPreferences : undefined, currentContext);
|
|
345
345
|
// Rebase provider-owned turn state from canonical messages at user-turn start.
|
|
346
346
|
// This prevents stale provider caches from replaying prior-turn context.
|
|
347
347
|
providerRuntime.resetTurnState(messages);
|
|
@@ -87,6 +87,7 @@ function buildSpecialistSystemPrompt(soulText, identityText, existingBundles, co
|
|
|
87
87
|
"- `write_file`: Write a file to disk. Use this to write psyche files and agent.json to the temp directory.",
|
|
88
88
|
"- `read_file`: Read a file from disk. Useful for reviewing existing agent bundles or migration sources.",
|
|
89
89
|
"- `list_directory`: List directory contents. Useful for exploring existing agent bundles.",
|
|
90
|
+
"- I also have the normal local harness tools when useful here, including `shell`, task tools like `task_create` and `schedule_reminder`, memory tools, coding tools, and repo helpers.",
|
|
90
91
|
"- `complete_adoption`: Finalize the bundle. Validates, scaffolds structural dirs, moves to ~/AgentBundles/, writes secrets, plays hatch animation. I call this with `name` (PascalCase) and `handoff_message` (warm message for the human).",
|
|
91
92
|
"- `final_answer`: End the conversation with a final message. I call this after complete_adoption succeeds.",
|
|
92
93
|
"",
|
|
@@ -63,14 +63,11 @@ const completeAdoptionTool = {
|
|
|
63
63
|
},
|
|
64
64
|
},
|
|
65
65
|
};
|
|
66
|
-
const readFileTool = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === "read_file");
|
|
67
|
-
const writeFileTool = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === "write_file");
|
|
68
|
-
const listDirTool = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === "list_directory");
|
|
69
66
|
/**
|
|
70
67
|
* Returns the specialist's tool schema array.
|
|
71
68
|
*/
|
|
72
69
|
function getSpecialistTools() {
|
|
73
|
-
return [completeAdoptionTool, tools_base_1.finalAnswerTool,
|
|
70
|
+
return [completeAdoptionTool, tools_base_1.finalAnswerTool, ...tools_base_1.baseToolDefinitions.map((definition) => definition.tool)];
|
|
74
71
|
}
|
|
75
72
|
const PSYCHE_FILES = ["SOUL.md", "IDENTITY.md", "LORE.md", "TACIT.md", "ASPIRATIONS.md"];
|
|
76
73
|
function isPascalCase(name) {
|
|
@@ -199,31 +196,14 @@ function createSpecialistExecTool(deps) {
|
|
|
199
196
|
if (name === "complete_adoption") {
|
|
200
197
|
return execCompleteAdoption(args, deps);
|
|
201
198
|
}
|
|
202
|
-
|
|
199
|
+
const baseDefinition = tools_base_1.baseToolDefinitions.find((definition) => definition.tool.function.name === name);
|
|
200
|
+
if (baseDefinition) {
|
|
201
|
+
const toolContext = {
|
|
202
|
+
signin: async () => undefined,
|
|
203
|
+
};
|
|
203
204
|
try {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
catch (e) {
|
|
207
|
-
return `error: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive @preserve */ String(e)}`;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
if (name === "write_file") {
|
|
211
|
-
try {
|
|
212
|
-
const dir = path.dirname(args.path);
|
|
213
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
214
|
-
fs.writeFileSync(args.path, args.content, "utf-8");
|
|
215
|
-
return `wrote ${args.path}`;
|
|
216
|
-
}
|
|
217
|
-
catch (e) {
|
|
218
|
-
return `error: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive @preserve */ String(e)}`;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (name === "list_directory") {
|
|
222
|
-
try {
|
|
223
|
-
return fs
|
|
224
|
-
.readdirSync(args.path, { withFileTypes: true })
|
|
225
|
-
.map((e) => `${e.isDirectory() ? "d" : "-"} ${e.name}`)
|
|
226
|
-
.join("\n");
|
|
205
|
+
await toolContext.signin("specialist");
|
|
206
|
+
return await baseDefinition.handler(args, toolContext);
|
|
227
207
|
}
|
|
228
208
|
catch (e) {
|
|
229
209
|
return `error: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive @preserve */ String(e)}`;
|
package/dist/heart/streaming.js
CHANGED
|
@@ -77,6 +77,60 @@ class FinalAnswerParser {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
exports.FinalAnswerParser = FinalAnswerParser;
|
|
80
|
+
function toResponsesUserContent(content) {
|
|
81
|
+
if (typeof content === "string") {
|
|
82
|
+
return content;
|
|
83
|
+
}
|
|
84
|
+
if (!Array.isArray(content)) {
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
const parts = [];
|
|
88
|
+
for (const part of content) {
|
|
89
|
+
if (!part || typeof part !== "object") {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
93
|
+
parts.push({ type: "input_text", text: part.text });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (part.type === "image_url") {
|
|
97
|
+
const imageUrl = typeof part.image_url?.url === "string" ? part.image_url.url : "";
|
|
98
|
+
if (!imageUrl)
|
|
99
|
+
continue;
|
|
100
|
+
parts.push({
|
|
101
|
+
type: "input_image",
|
|
102
|
+
image_url: imageUrl,
|
|
103
|
+
detail: part.image_url?.detail ?? "auto",
|
|
104
|
+
});
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (part.type === "input_audio" &&
|
|
108
|
+
typeof part.input_audio?.data === "string" &&
|
|
109
|
+
(part.input_audio.format === "mp3" || part.input_audio.format === "wav")) {
|
|
110
|
+
parts.push({
|
|
111
|
+
type: "input_audio",
|
|
112
|
+
input_audio: {
|
|
113
|
+
data: part.input_audio.data,
|
|
114
|
+
format: part.input_audio.format,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (part.type === "file") {
|
|
120
|
+
const fileRecord = { type: "input_file" };
|
|
121
|
+
if (typeof part.file?.file_data === "string")
|
|
122
|
+
fileRecord.file_data = part.file.file_data;
|
|
123
|
+
if (typeof part.file?.file_id === "string")
|
|
124
|
+
fileRecord.file_id = part.file.file_id;
|
|
125
|
+
if (typeof part.file?.filename === "string")
|
|
126
|
+
fileRecord.filename = part.file.filename;
|
|
127
|
+
if (typeof part.file?.file_data === "string" || typeof part.file?.file_id === "string") {
|
|
128
|
+
parts.push(fileRecord);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return parts.length > 0 ? parts : "";
|
|
133
|
+
}
|
|
80
134
|
function toResponsesInput(messages) {
|
|
81
135
|
let instructions = "";
|
|
82
136
|
const input = [];
|
|
@@ -90,7 +144,7 @@ function toResponsesInput(messages) {
|
|
|
90
144
|
}
|
|
91
145
|
if (msg.role === "user") {
|
|
92
146
|
const u = msg;
|
|
93
|
-
input.push({ role: "user", content:
|
|
147
|
+
input.push({ role: "user", content: toResponsesUserContent(u.content) });
|
|
94
148
|
continue;
|
|
95
149
|
}
|
|
96
150
|
if (msg.role === "assistant") {
|
package/dist/mind/prompt.js
CHANGED
|
@@ -282,8 +282,8 @@ function dateSection() {
|
|
|
282
282
|
const today = new Date().toISOString().slice(0, 10);
|
|
283
283
|
return `current date: ${today}`;
|
|
284
284
|
}
|
|
285
|
-
function toolsSection(channel, options) {
|
|
286
|
-
const channelTools = (0, tools_1.getToolsForChannel)((0, channel_1.getChannelCapabilities)(channel));
|
|
285
|
+
function toolsSection(channel, options, context) {
|
|
286
|
+
const channelTools = (0, tools_1.getToolsForChannel)((0, channel_1.getChannelCapabilities)(channel), undefined, context);
|
|
287
287
|
const activeTools = (options?.toolChoiceRequired ?? true) ? [...channelTools, tools_1.finalAnswerTool] : channelTools;
|
|
288
288
|
const list = activeTools
|
|
289
289
|
.map((t) => `- ${t.function.name}: ${t.function.description}`)
|
|
@@ -399,7 +399,7 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
399
399
|
runtimeInfoSection(channel),
|
|
400
400
|
providerSection(),
|
|
401
401
|
dateSection(),
|
|
402
|
-
toolsSection(channel, options),
|
|
402
|
+
toolsSection(channel, options, context),
|
|
403
403
|
skillsSection(),
|
|
404
404
|
taskBoardSection(),
|
|
405
405
|
buildSessionSummary({
|
|
@@ -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) => {
|