@ouro.bot/cli 0.1.0-alpha.15 → 0.1.0-alpha.17

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.
@@ -41,8 +41,10 @@ exports.getMinimaxConfig = getMinimaxConfig;
41
41
  exports.getAnthropicConfig = getAnthropicConfig;
42
42
  exports.getOpenAICodexConfig = getOpenAICodexConfig;
43
43
  exports.getTeamsConfig = getTeamsConfig;
44
+ exports.getTeamsSecondaryConfig = getTeamsSecondaryConfig;
44
45
  exports.getContextConfig = getContextConfig;
45
46
  exports.getOAuthConfig = getOAuthConfig;
47
+ exports.resolveOAuthForTenant = resolveOAuthForTenant;
46
48
  exports.getTeamsChannelConfig = getTeamsChannelConfig;
47
49
  exports.getBlueBubblesConfig = getBlueBubblesConfig;
48
50
  exports.getBlueBubblesChannelConfig = getBlueBubblesChannelConfig;
@@ -84,12 +86,19 @@ const DEFAULT_SECRETS_TEMPLATE = {
84
86
  clientId: "",
85
87
  clientSecret: "",
86
88
  tenantId: "",
89
+ managedIdentityClientId: "",
87
90
  },
88
91
  oauth: {
89
92
  graphConnectionName: "graph",
90
93
  adoConnectionName: "ado",
91
94
  githubConnectionName: "",
92
95
  },
96
+ teamsSecondary: {
97
+ clientId: "",
98
+ clientSecret: "",
99
+ tenantId: "",
100
+ managedIdentityClientId: "",
101
+ },
93
102
  teamsChannel: {
94
103
  skipConfirmation: true,
95
104
  port: 3978,
@@ -118,6 +127,7 @@ function defaultRuntimeConfig() {
118
127
  "openai-codex": { ...DEFAULT_SECRETS_TEMPLATE.providers["openai-codex"] },
119
128
  },
120
129
  teams: { ...DEFAULT_SECRETS_TEMPLATE.teams },
130
+ teamsSecondary: { ...DEFAULT_SECRETS_TEMPLATE.teamsSecondary },
121
131
  oauth: { ...DEFAULT_SECRETS_TEMPLATE.oauth },
122
132
  context: { ...identity_1.DEFAULT_AGENT_CONTEXT },
123
133
  teamsChannel: { ...DEFAULT_SECRETS_TEMPLATE.teamsChannel },
@@ -262,6 +272,10 @@ function getTeamsConfig() {
262
272
  const config = loadConfig();
263
273
  return { ...config.teams };
264
274
  }
275
+ function getTeamsSecondaryConfig() {
276
+ const config = loadConfig();
277
+ return { ...config.teamsSecondary };
278
+ }
265
279
  function getContextConfig() {
266
280
  if (_testContextOverride) {
267
281
  return { ..._testContextOverride };
@@ -282,6 +296,16 @@ function getOAuthConfig() {
282
296
  const config = loadConfig();
283
297
  return { ...config.oauth };
284
298
  }
299
+ /** Resolve OAuth connection names for a specific tenant, falling back to defaults. */
300
+ function resolveOAuthForTenant(tenantId) {
301
+ const base = getOAuthConfig();
302
+ const overrides = tenantId ? base.tenantOverrides?.[tenantId] : undefined;
303
+ return {
304
+ graphConnectionName: overrides?.graphConnectionName ?? base.graphConnectionName,
305
+ adoConnectionName: overrides?.adoConnectionName ?? base.adoConnectionName,
306
+ githubConnectionName: overrides?.githubConnectionName ?? base.githubConnectionName,
307
+ };
308
+ }
285
309
  function getTeamsChannelConfig() {
286
310
  const config = loadConfig();
287
311
  const { skipConfirmation, flushIntervalMs, port } = config.teamsChannel;
@@ -321,7 +345,11 @@ function sanitizeKey(key) {
321
345
  return key.replace(/[/:]/g, "_");
322
346
  }
323
347
  function sessionPath(friendId, channel, key) {
324
- const dir = path.join(os.homedir(), ".agentstate", (0, identity_1.getAgentName)(), "sessions", friendId, channel);
348
+ // On Azure App Service, os.homedir() returns /root which is ephemeral.
349
+ // Use /home (persistent storage) when WEBSITE_SITE_NAME is set.
350
+ /* v8 ignore next -- Azure vs local path branch; environment-specific @preserve */
351
+ const homeBase = process.env.WEBSITE_SITE_NAME ? "/home" : os.homedir();
352
+ const dir = path.join(homeBase, ".agentstate", (0, identity_1.getAgentName)(), "sessions", friendId, channel);
325
353
  fs.mkdirSync(dir, { recursive: true });
326
354
  return path.join(dir, sanitizeKey(key) + ".json");
327
355
  }
@@ -8,6 +8,7 @@ exports.getProvider = getProvider;
8
8
  exports.createSummarize = createSummarize;
9
9
  exports.getProviderDisplayLabel = getProviderDisplayLabel;
10
10
  exports.stripLastToolCalls = stripLastToolCalls;
11
+ exports.repairOrphanedToolCalls = repairOrphanedToolCalls;
11
12
  exports.isTransientError = isTransientError;
12
13
  exports.classifyTransientError = classifyTransientError;
13
14
  exports.runAgent = runAgent;
@@ -160,6 +161,68 @@ function stripLastToolCalls(messages) {
160
161
  }
161
162
  }
162
163
  }
164
+ // Roles that end a tool-result scan. When scanning forward from an assistant
165
+ // message, stop at the next assistant or user message (tool results must be
166
+ // adjacent to their originating assistant message).
167
+ const TOOL_SCAN_BOUNDARY_ROLES = new Set(["assistant", "user"]);
168
+ // Repair orphaned tool_calls and tool results anywhere in the message history.
169
+ // 1. If an assistant message has tool_calls but missing tool results, inject synthetic error results.
170
+ // 2. If a tool result's tool_call_id doesn't match any tool_calls in a preceding assistant message, remove it.
171
+ // This prevents 400 errors from the API after an aborted turn.
172
+ function repairOrphanedToolCalls(messages) {
173
+ // Pass 1: collect all valid tool_call IDs from assistant messages
174
+ const validCallIds = new Set();
175
+ for (const msg of messages) {
176
+ if (msg.role === "assistant") {
177
+ const asst = msg;
178
+ if (asst.tool_calls) {
179
+ for (const tc of asst.tool_calls)
180
+ validCallIds.add(tc.id);
181
+ }
182
+ }
183
+ }
184
+ // Pass 2: remove orphaned tool results (tool_call_id not in any assistant's tool_calls)
185
+ for (let i = messages.length - 1; i >= 0; i--) {
186
+ if (messages[i].role === "tool") {
187
+ const toolMsg = messages[i];
188
+ if (!validCallIds.has(toolMsg.tool_call_id)) {
189
+ messages.splice(i, 1);
190
+ }
191
+ }
192
+ }
193
+ // Pass 3: inject synthetic results for tool_calls missing their tool results
194
+ for (let i = 0; i < messages.length; i++) {
195
+ const msg = messages[i];
196
+ if (msg.role !== "assistant")
197
+ continue;
198
+ const asst = msg;
199
+ if (!asst.tool_calls || asst.tool_calls.length === 0)
200
+ continue;
201
+ // Collect tool result IDs that follow this assistant message
202
+ const resultIds = new Set();
203
+ for (let j = i + 1; j < messages.length; j++) {
204
+ const following = messages[j];
205
+ if (following.role === "tool") {
206
+ resultIds.add(following.tool_call_id);
207
+ }
208
+ else if (TOOL_SCAN_BOUNDARY_ROLES.has(following.role)) {
209
+ break;
210
+ }
211
+ }
212
+ const missing = asst.tool_calls.filter((tc) => !resultIds.has(tc.id));
213
+ if (missing.length > 0) {
214
+ const syntheticResults = missing.map((tc) => ({
215
+ role: "tool",
216
+ tool_call_id: tc.id,
217
+ content: "error: tool call was interrupted (previous turn timed out or was aborted)",
218
+ }));
219
+ let insertAt = i + 1;
220
+ while (insertAt < messages.length && messages[insertAt].role === "tool")
221
+ insertAt++;
222
+ messages.splice(insertAt, 0, ...syntheticResults);
223
+ }
224
+ }
225
+ }
163
226
  // Detect context overflow errors from Azure or MiniMax
164
227
  function isContextOverflow(err) {
165
228
  if (!(err instanceof Error))
@@ -278,7 +341,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
278
341
  }
279
342
  catch { /* unsupported */ }
280
343
  const toolPreferences = currentContext?.friend?.toolPreferences;
281
- 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);
282
345
  // Rebase provider-owned turn state from canonical messages at user-turn start.
283
346
  // This prevents stale provider caches from replaying prior-turn context.
284
347
  providerRuntime.resetTurnState(messages);
@@ -571,7 +571,8 @@ function defaultListDiscoveredAgents() {
571
571
  return discovered.sort((left, right) => left.localeCompare(right));
572
572
  }
573
573
  async function defaultLinkFriendIdentity(command) {
574
- const friendStore = new store_file_1.FileFriendStore(path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`, "friends"));
574
+ const fp = path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`, "friends");
575
+ const friendStore = new store_file_1.FileFriendStore(fp);
575
576
  const current = await friendStore.get(command.friendId);
576
577
  if (!current) {
577
578
  return `friend not found: ${command.friendId}`;
@@ -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: typeof u.content === "string" ? u.content : "" });
147
+ input.push({ role: "user", content: toResponsesUserContent(u.content) });
94
148
  continue;
95
149
  }
96
150
  if (msg.role === "assistant") {
@@ -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({
@@ -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
- async function adoRequest(token, method, org, path, body) {
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 = `${ADO_BASE}/${org}${fullPath}`;
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.failure?.stdoutTail ?? "",
455
- stderrTail: normalizedSession.failure?.stderrTail ?? "",
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.write(`${prompt}\n`);
94
+ proc.stdin.end(`${prompt}\n`);
95
95
  (0, runtime_1.emitNervesEvent)({
96
96
  component: "repertoire",
97
97
  event: "repertoire.coding_spawn_end",