@ouro.bot/cli 0.1.0-alpha.8 → 0.1.0-alpha.81

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 (127) 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/README.md +147 -205
  5. package/assets/ouroboros.png +0 -0
  6. package/changelog.json +468 -0
  7. package/dist/heart/active-work.js +218 -0
  8. package/dist/heart/bridges/manager.js +358 -0
  9. package/dist/heart/bridges/state-machine.js +135 -0
  10. package/dist/heart/bridges/store.js +123 -0
  11. package/dist/heart/commitments.js +89 -0
  12. package/dist/heart/config.js +68 -23
  13. package/dist/heart/core.js +452 -93
  14. package/dist/heart/cross-chat-delivery.js +146 -0
  15. package/dist/heart/daemon/agent-discovery.js +81 -0
  16. package/dist/heart/daemon/auth-flow.js +430 -0
  17. package/dist/heart/daemon/daemon-cli.js +1779 -247
  18. package/dist/heart/daemon/daemon-entry.js +55 -6
  19. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  20. package/dist/heart/daemon/daemon.js +216 -10
  21. package/dist/heart/daemon/hatch-animation.js +10 -3
  22. package/dist/heart/daemon/hatch-flow.js +7 -82
  23. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  24. package/dist/heart/daemon/launchd.js +159 -0
  25. package/dist/heart/daemon/log-tailer.js +4 -3
  26. package/dist/heart/daemon/message-router.js +17 -8
  27. package/dist/heart/daemon/ouro-bot-entry.js +0 -0
  28. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  29. package/dist/heart/daemon/ouro-entry.js +0 -0
  30. package/dist/heart/daemon/ouro-path-installer.js +260 -0
  31. package/dist/heart/daemon/ouro-uti.js +11 -2
  32. package/dist/heart/daemon/ouro-version-manager.js +164 -0
  33. package/dist/heart/daemon/process-manager.js +14 -1
  34. package/dist/heart/daemon/run-hooks.js +37 -0
  35. package/dist/heart/daemon/runtime-logging.js +58 -15
  36. package/dist/heart/daemon/runtime-metadata.js +219 -0
  37. package/dist/heart/daemon/runtime-mode.js +67 -0
  38. package/dist/heart/daemon/sense-manager.js +307 -0
  39. package/dist/heart/daemon/skill-management-installer.js +94 -0
  40. package/dist/heart/daemon/socket-client.js +202 -0
  41. package/dist/heart/daemon/specialist-orchestrator.js +53 -84
  42. package/dist/heart/daemon/specialist-prompt.js +63 -11
  43. package/dist/heart/daemon/specialist-tools.js +211 -60
  44. package/dist/heart/daemon/staged-restart.js +114 -0
  45. package/dist/heart/daemon/thoughts.js +507 -0
  46. package/dist/heart/daemon/update-checker.js +111 -0
  47. package/dist/heart/daemon/update-hooks.js +138 -0
  48. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  49. package/dist/heart/delegation.js +62 -0
  50. package/dist/heart/identity.js +126 -21
  51. package/dist/heart/kicks.js +1 -19
  52. package/dist/heart/model-capabilities.js +48 -0
  53. package/dist/heart/obligations.js +141 -0
  54. package/dist/heart/progress-story.js +42 -0
  55. package/dist/heart/providers/anthropic.js +74 -9
  56. package/dist/heart/providers/azure.js +86 -7
  57. package/dist/heart/providers/github-copilot.js +149 -0
  58. package/dist/heart/providers/minimax.js +4 -0
  59. package/dist/heart/providers/openai-codex.js +12 -3
  60. package/dist/heart/safe-workspace.js +228 -0
  61. package/dist/heart/sense-truth.js +61 -0
  62. package/dist/heart/session-activity.js +169 -0
  63. package/dist/heart/session-recall.js +116 -0
  64. package/dist/heart/streaming.js +100 -22
  65. package/dist/heart/target-resolution.js +123 -0
  66. package/dist/heart/turn-coordinator.js +28 -0
  67. package/dist/mind/associative-recall.js +14 -2
  68. package/dist/mind/bundle-manifest.js +70 -0
  69. package/dist/mind/context.js +27 -11
  70. package/dist/mind/first-impressions.js +16 -2
  71. package/dist/mind/friends/channel.js +35 -0
  72. package/dist/mind/friends/group-context.js +144 -0
  73. package/dist/mind/friends/store-file.js +19 -0
  74. package/dist/mind/friends/trust-explanation.js +74 -0
  75. package/dist/mind/friends/types.js +8 -0
  76. package/dist/mind/memory.js +27 -26
  77. package/dist/mind/pending.js +76 -9
  78. package/dist/mind/phrases.js +1 -0
  79. package/dist/mind/prompt.js +445 -77
  80. package/dist/mind/token-estimate.js +8 -12
  81. package/dist/nerves/cli-logging.js +15 -2
  82. package/dist/nerves/coverage/run-artifacts.js +1 -1
  83. package/dist/nerves/index.js +12 -0
  84. package/dist/repertoire/ado-client.js +4 -2
  85. package/dist/repertoire/coding/feedback.js +134 -0
  86. package/dist/repertoire/coding/index.js +4 -1
  87. package/dist/repertoire/coding/manager.js +62 -4
  88. package/dist/repertoire/coding/spawner.js +3 -3
  89. package/dist/repertoire/coding/tools.js +41 -2
  90. package/dist/repertoire/data/ado-endpoints.json +188 -0
  91. package/dist/repertoire/guardrails.js +290 -0
  92. package/dist/repertoire/mcp-client.js +254 -0
  93. package/dist/repertoire/mcp-manager.js +195 -0
  94. package/dist/repertoire/skills.js +3 -26
  95. package/dist/repertoire/tasks/board.js +12 -0
  96. package/dist/repertoire/tasks/index.js +23 -9
  97. package/dist/repertoire/tasks/transitions.js +1 -2
  98. package/dist/repertoire/tools-base.js +686 -251
  99. package/dist/repertoire/tools-bluebubbles.js +93 -0
  100. package/dist/repertoire/tools-teams.js +58 -25
  101. package/dist/repertoire/tools.js +95 -53
  102. package/dist/senses/bluebubbles-client.js +210 -5
  103. package/dist/senses/bluebubbles-entry.js +2 -0
  104. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  105. package/dist/senses/bluebubbles-media.js +339 -0
  106. package/dist/senses/bluebubbles-model.js +12 -4
  107. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  108. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  109. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  110. package/dist/senses/bluebubbles.js +894 -45
  111. package/dist/senses/cli-layout.js +187 -0
  112. package/dist/senses/cli.js +405 -156
  113. package/dist/senses/continuity.js +94 -0
  114. package/dist/senses/debug-activity.js +154 -0
  115. package/dist/senses/inner-dialog-worker.js +47 -18
  116. package/dist/senses/inner-dialog.js +377 -83
  117. package/dist/senses/pipeline.js +307 -0
  118. package/dist/senses/teams.js +573 -129
  119. package/dist/senses/trust-gate.js +112 -2
  120. package/package.json +14 -3
  121. package/subagents/README.md +4 -70
  122. package/dist/heart/daemon/specialist-session.js +0 -142
  123. package/dist/heart/daemon/subagent-installer.js +0 -125
  124. package/dist/inner-worker-entry.js +0 -4
  125. package/subagents/work-doer.md +0 -233
  126. package/subagents/work-merger.md +0 -624
  127. package/subagents/work-planner.md +0 -373
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.__internal = void 0;
4
3
  exports.estimateTokensForMessage = estimateTokensForMessage;
5
4
  exports.estimateTokensForMessages = estimateTokensForMessages;
6
5
  const runtime_1 = require("../nerves/runtime");
@@ -53,7 +52,7 @@ function countCharsInContent(content) {
53
52
  return c.text.length;
54
53
  if (typeof c.content === "string")
55
54
  return c.content.length;
56
- return safeStringify(content).length;
55
+ return safeStringify(c).length;
57
56
  }
58
57
  return 0;
59
58
  }
@@ -70,12 +69,13 @@ function countCharsInToolCalls(toolCalls) {
70
69
  if (typeof t.type === "string")
71
70
  total += t.type.length;
72
71
  if (t.function && typeof t.function === "object") {
73
- if (typeof t.function.name === "string")
74
- total += t.function.name.length;
75
- if (typeof t.function.arguments === "string")
76
- total += t.function.arguments.length;
77
- else if (t.function.arguments != null)
78
- total += safeStringify(t.function.arguments).length;
72
+ const fn = t.function;
73
+ if (typeof fn.name === "string")
74
+ total += fn.name.length;
75
+ if (typeof fn.arguments === "string")
76
+ total += fn.arguments.length;
77
+ else if (fn.arguments != null)
78
+ total += safeStringify(fn.arguments).length;
79
79
  }
80
80
  }
81
81
  return total;
@@ -113,7 +113,3 @@ function estimateTokensForMessages(msgs) {
113
113
  total += estimateTokensForMessage(msg);
114
114
  return total;
115
115
  }
116
- exports.__internal = {
117
- CHARS_PER_TOKEN,
118
- PER_MESSAGE_OVERHEAD_TOKENS,
119
- };
@@ -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);
@@ -14,7 +14,7 @@ const path_1 = require("path");
14
14
  const os_1 = require("os");
15
15
  exports.REPO_SLUG = "ouroboros-agent-harness";
16
16
  function getTestRunsRoot(repoSlug = exports.REPO_SLUG) {
17
- return (0, path_1.join)((0, os_1.homedir)(), ".agentstate", "test-runs", repoSlug);
17
+ return (0, path_1.join)((0, os_1.tmpdir)(), "ouroboros-test-runs", repoSlug);
18
18
  }
19
19
  function createRunId(now = new Date()) {
20
20
  return now.toISOString().replace(/[:.]/g, "-");
@@ -4,6 +4,7 @@ exports.createTraceId = createTraceId;
4
4
  exports.ensureTraceId = ensureTraceId;
5
5
  exports.createFanoutSink = createFanoutSink;
6
6
  exports.formatTerminalEntry = formatTerminalEntry;
7
+ exports.registerSpinnerHooks = registerSpinnerHooks;
7
8
  exports.createTerminalSink = createTerminalSink;
8
9
  exports.createStderrSink = createStderrSink;
9
10
  exports.createNdjsonFileSink = createNdjsonFileSink;
@@ -73,15 +74,26 @@ function formatTerminalEntry(entry) {
73
74
  const level = entry.level.toUpperCase();
74
75
  return `${formatTerminalTime(entry.ts)} ${level} [${entry.component}] ${entry.message}${formatTerminalMeta(entry.meta)}`;
75
76
  }
77
+ // Spinner coordination: the CLI sense registers these so log output
78
+ // doesn't interleave with the active spinner animation.
79
+ let _pauseSpinner = null;
80
+ let _resumeSpinner = null;
81
+ function registerSpinnerHooks(pause, resume) {
82
+ _pauseSpinner = pause;
83
+ _resumeSpinner = resume;
84
+ }
76
85
  function createTerminalSink(write = (chunk) => process.stderr.write(chunk), colorize = true) {
77
86
  return (entry) => {
87
+ _pauseSpinner?.();
78
88
  const line = formatTerminalEntry(entry);
79
89
  if (!colorize) {
80
90
  write(`${line}\n`);
91
+ _resumeSpinner?.();
81
92
  return;
82
93
  }
83
94
  const prefix = LEVEL_COLORS[entry.level];
84
95
  write(`${prefix}${line}\x1b[0m\n`);
96
+ _resumeSpinner?.();
85
97
  };
86
98
  }
87
99
  function createStderrSink(write = (chunk) => process.stderr.write(chunk)) {
@@ -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; } });
@@ -35,7 +35,6 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CodingSessionManager = void 0;
37
37
  const fs = __importStar(require("fs"));
38
- const os = __importStar(require("os"));
39
38
  const path = __importStar(require("path"));
40
39
  const identity_1 = require("../../heart/identity");
41
40
  const runtime_1 = require("../../nerves/runtime");
@@ -49,7 +48,7 @@ function safeAgentName() {
49
48
  }
50
49
  }
51
50
  function defaultStateFilePath(agentName) {
52
- return path.join(os.homedir(), ".agentstate", agentName, "coding", "sessions.json");
51
+ return path.join((0, identity_1.getAgentRoot)(agentName), "state", "coding", "sessions.json");
53
52
  }
54
53
  function isPidAlive(pid) {
55
54
  try {
@@ -63,6 +62,8 @@ function isPidAlive(pid) {
63
62
  function cloneSession(session) {
64
63
  return {
65
64
  ...session,
65
+ stdoutTail: session.stdoutTail,
66
+ stderrTail: session.stderrTail,
66
67
  failure: session.failure
67
68
  ? {
68
69
  ...session.failure,
@@ -115,6 +116,7 @@ function defaultFailureDiagnostics(code, signal, command, args, stdoutTail, stde
115
116
  }
116
117
  class CodingSessionManager {
117
118
  records = new Map();
119
+ listeners = new Map();
118
120
  spawnProcess;
119
121
  nowIso;
120
122
  maxRestarts;
@@ -158,6 +160,8 @@ class CodingSessionManager {
158
160
  scopeFile: normalizedRequest.scopeFile,
159
161
  stateFile: normalizedRequest.stateFile,
160
162
  status: "spawning",
163
+ stdoutTail: "",
164
+ stderrTail: "",
161
165
  pid: null,
162
166
  startedAt: now,
163
167
  lastActivityAt: now,
@@ -188,6 +192,7 @@ class CodingSessionManager {
188
192
  meta: { id, runner: normalizedRequest.runner, pid: session.pid },
189
193
  });
190
194
  this.persistState();
195
+ this.notifyListeners(id, { kind: "spawned", session: cloneSession(session) });
191
196
  return cloneSession(session);
192
197
  }
193
198
  listSessions() {
@@ -199,6 +204,20 @@ class CodingSessionManager {
199
204
  const record = this.records.get(sessionId);
200
205
  return record ? cloneSession(record.session) : null;
201
206
  }
207
+ subscribe(sessionId, listener) {
208
+ const listeners = this.listeners.get(sessionId) ?? new Set();
209
+ listeners.add(listener);
210
+ this.listeners.set(sessionId, listeners);
211
+ return () => {
212
+ const current = this.listeners.get(sessionId);
213
+ if (!current)
214
+ return;
215
+ current.delete(listener);
216
+ if (current.size === 0) {
217
+ this.listeners.delete(sessionId);
218
+ }
219
+ };
220
+ }
202
221
  sendInput(sessionId, input) {
203
222
  const record = this.records.get(sessionId);
204
223
  if (!record || !record.process) {
@@ -234,6 +253,7 @@ class CodingSessionManager {
234
253
  meta: { id: sessionId },
235
254
  });
236
255
  this.persistState();
256
+ this.notifyListeners(sessionId, { kind: "killed", session: cloneSession(record.session) });
237
257
  return { ok: true, message: `killed ${sessionId}` };
238
258
  }
239
259
  checkStalls(nowMs = Date.now()) {
@@ -254,6 +274,7 @@ class CodingSessionManager {
254
274
  message: "coding session stalled",
255
275
  meta: { id: record.session.id, elapsedMs: elapsed },
256
276
  });
277
+ this.notifyListeners(record.session.id, { kind: "stalled", session: cloneSession(record.session) });
257
278
  if (record.request.autoRestartOnStall !== false && record.session.restartCount < this.maxRestarts) {
258
279
  this.restartSession(record, "stalled");
259
280
  }
@@ -297,18 +318,23 @@ class CodingSessionManager {
297
318
  }
298
319
  onOutput(record, text, stream) {
299
320
  record.session.lastActivityAt = this.nowIso();
321
+ let updateKind = "progress";
300
322
  if (stream === "stdout") {
301
323
  record.stdoutTail = appendTail(record.stdoutTail, text);
324
+ record.session.stdoutTail = record.stdoutTail;
302
325
  }
303
326
  else {
304
327
  record.stderrTail = appendTail(record.stderrTail, text);
328
+ record.session.stderrTail = record.stderrTail;
305
329
  }
306
330
  if (text.includes("status: NEEDS_REVIEW") || text.includes("❌ blocked")) {
307
331
  record.session.status = "waiting_input";
332
+ updateKind = "waiting_input";
308
333
  }
309
334
  if (text.includes("✅ all units complete")) {
310
335
  record.session.status = "completed";
311
336
  record.session.endedAt = this.nowIso();
337
+ updateKind = "completed";
312
338
  }
313
339
  (0, runtime_1.emitNervesEvent)({
314
340
  component: "repertoire",
@@ -317,6 +343,12 @@ class CodingSessionManager {
317
343
  meta: { id: record.session.id, status: record.session.status },
318
344
  });
319
345
  this.persistState();
346
+ this.notifyListeners(record.session.id, {
347
+ kind: updateKind,
348
+ session: cloneSession(record.session),
349
+ stream,
350
+ text,
351
+ });
320
352
  }
321
353
  onExit(record, code, signal) {
322
354
  if (!record.process)
@@ -334,6 +366,7 @@ class CodingSessionManager {
334
366
  record.session.status = "completed";
335
367
  record.session.endedAt = this.nowIso();
336
368
  this.persistState();
369
+ this.notifyListeners(record.session.id, { kind: "completed", session: cloneSession(record.session) });
337
370
  return;
338
371
  }
339
372
  if (record.request.autoRestartOnCrash !== false && record.session.restartCount < this.maxRestarts) {
@@ -351,6 +384,7 @@ class CodingSessionManager {
351
384
  meta: { id: record.session.id, code, signal, command: record.command },
352
385
  });
353
386
  this.persistState();
387
+ this.notifyListeners(record.session.id, { kind: "failed", session: cloneSession(record.session) });
354
388
  }
355
389
  restartSession(record, reason) {
356
390
  const replacement = normalizeSpawnResult(this.spawnProcess(record.request));
@@ -359,6 +393,8 @@ class CodingSessionManager {
359
393
  record.args = [...replacement.args];
360
394
  record.stdoutTail = "";
361
395
  record.stderrTail = "";
396
+ record.session.stdoutTail = "";
397
+ record.session.stderrTail = "";
362
398
  record.session.pid = replacement.process.pid ?? null;
363
399
  record.session.restartCount += 1;
364
400
  record.session.status = "running";
@@ -375,6 +411,26 @@ class CodingSessionManager {
375
411
  });
376
412
  this.persistState();
377
413
  }
414
+ notifyListeners(sessionId, update) {
415
+ const listeners = this.listeners.get(sessionId);
416
+ if (!listeners || listeners.size === 0)
417
+ return;
418
+ for (const listener of listeners) {
419
+ void Promise.resolve(listener(update)).catch((error) => {
420
+ (0, runtime_1.emitNervesEvent)({
421
+ level: "warn",
422
+ component: "repertoire",
423
+ event: "repertoire.coding_feedback_listener_error",
424
+ message: "coding session listener failed",
425
+ meta: {
426
+ sessionId,
427
+ kind: update.kind,
428
+ reason: error instanceof Error ? error.message : String(error),
429
+ },
430
+ });
431
+ });
432
+ }
433
+ }
378
434
  loadPersistedState() {
379
435
  if (!this.existsSync(this.stateFilePath)) {
380
436
  return;
@@ -433,6 +489,8 @@ class CodingSessionManager {
433
489
  ...session,
434
490
  taskRef: session.taskRef ?? normalizedRequest.taskRef,
435
491
  failure: session.failure ?? null,
492
+ stdoutTail: session.stdoutTail ?? session.failure?.stdoutTail ?? "",
493
+ stderrTail: session.stderrTail ?? session.failure?.stderrTail ?? "",
436
494
  };
437
495
  if (typeof normalizedSession.pid === "number") {
438
496
  const alive = this.pidAlive(normalizedSession.pid);
@@ -451,8 +509,8 @@ class CodingSessionManager {
451
509
  process: null,
452
510
  command: normalizedSession.failure?.command ?? "restored",
453
511
  args: normalizedSession.failure ? [...normalizedSession.failure.args] : [],
454
- stdoutTail: normalizedSession.failure?.stdoutTail ?? "",
455
- stderrTail: normalizedSession.failure?.stderrTail ?? "",
512
+ stdoutTail: normalizedSession.stdoutTail,
513
+ stderrTail: normalizedSession.stderrTail,
456
514
  });
457
515
  this.sequence = Math.max(this.sequence, extractSequence(normalizedSession.id));
458
516
  }
@@ -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) => {