@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.
Files changed (71) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +70 -9
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
  3. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  4. package/AdoptionSpecialist.ouro/psyche/identities/python.md +30 -0
  5. package/assets/ouroboros.png +0 -0
  6. package/changelog.json +87 -0
  7. package/dist/heart/config.js +66 -4
  8. package/dist/heart/core.js +75 -2
  9. package/dist/heart/daemon/agent-discovery.js +81 -0
  10. package/dist/heart/daemon/daemon-cli.js +562 -64
  11. package/dist/heart/daemon/daemon-entry.js +14 -5
  12. package/dist/heart/daemon/daemon-runtime-sync.js +90 -0
  13. package/dist/heart/daemon/daemon.js +87 -9
  14. package/dist/heart/daemon/hatch-animation.js +35 -0
  15. package/dist/heart/daemon/hatch-flow.js +2 -11
  16. package/dist/heart/daemon/hatch-specialist.js +6 -1
  17. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  18. package/dist/heart/daemon/launchd.js +134 -0
  19. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  20. package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
  21. package/dist/heart/daemon/ouro-path-installer.js +178 -0
  22. package/dist/heart/daemon/ouro-uti.js +11 -2
  23. package/dist/heart/daemon/process-manager.js +1 -1
  24. package/dist/heart/daemon/run-hooks.js +37 -0
  25. package/dist/heart/daemon/runtime-logging.js +9 -5
  26. package/dist/heart/daemon/runtime-metadata.js +118 -0
  27. package/dist/heart/daemon/sense-manager.js +266 -0
  28. package/dist/heart/daemon/specialist-orchestrator.js +129 -0
  29. package/dist/heart/daemon/specialist-prompt.js +98 -0
  30. package/dist/heart/daemon/specialist-tools.js +237 -0
  31. package/dist/heart/daemon/staged-restart.js +114 -0
  32. package/dist/heart/daemon/subagent-installer.js +10 -1
  33. package/dist/heart/daemon/update-checker.js +103 -0
  34. package/dist/heart/daemon/update-hooks.js +138 -0
  35. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  36. package/dist/heart/identity.js +85 -1
  37. package/dist/heart/providers/anthropic.js +19 -2
  38. package/dist/heart/sense-truth.js +61 -0
  39. package/dist/heart/streaming.js +99 -21
  40. package/dist/mind/bundle-manifest.js +69 -0
  41. package/dist/mind/first-impressions.js +2 -1
  42. package/dist/mind/friends/channel.js +8 -0
  43. package/dist/mind/friends/types.js +1 -1
  44. package/dist/mind/phrases.js +1 -0
  45. package/dist/mind/prompt.js +94 -3
  46. package/dist/nerves/cli-logging.js +15 -2
  47. package/dist/repertoire/ado-client.js +4 -2
  48. package/dist/repertoire/coding/feedback.js +134 -0
  49. package/dist/repertoire/coding/index.js +4 -1
  50. package/dist/repertoire/coding/manager.js +61 -2
  51. package/dist/repertoire/coding/spawner.js +3 -3
  52. package/dist/repertoire/coding/tools.js +41 -2
  53. package/dist/repertoire/data/ado-endpoints.json +188 -0
  54. package/dist/repertoire/tools-base.js +69 -5
  55. package/dist/repertoire/tools-teams.js +57 -4
  56. package/dist/repertoire/tools.js +44 -11
  57. package/dist/senses/bluebubbles-client.js +434 -0
  58. package/dist/senses/bluebubbles-entry.js +11 -0
  59. package/dist/senses/bluebubbles-media.js +338 -0
  60. package/dist/senses/bluebubbles-model.js +251 -0
  61. package/dist/senses/bluebubbles-mutation-log.js +76 -0
  62. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  63. package/dist/senses/bluebubbles.js +449 -0
  64. package/dist/senses/cli.js +299 -133
  65. package/dist/senses/debug-activity.js +108 -0
  66. package/dist/senses/teams.js +173 -54
  67. package/package.json +15 -6
  68. package/subagents/work-doer.md +26 -24
  69. package/subagents/work-merger.md +24 -30
  70. package/subagents/work-planner.md +34 -25
  71. 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",
@@ -16,6 +16,7 @@ function getPhrases() {
16
16
  message: "loading phrase pools",
17
17
  meta: {},
18
18
  });
19
+ (0, identity_1.resetAgentConfigCache)();
19
20
  const phrases = (0, identity_1.loadAgentConfig)().phrases;
20
21
  (0, runtime_1.emitNervesEvent)({
21
22
  event: "repertoire.load_end",
@@ -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
- return (0, nerves_1.createTerminalSink)();
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: options.level ?? "info",
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
- 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",
@@ -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 session = await (0, index_1.getCodingSessionManager)().spawnSession(request);
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) => {