@ouro.bot/cli 0.1.0-alpha.2 → 0.1.0-alpha.21

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 (58) 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/assets/ouroboros.png +0 -0
  5. package/dist/heart/config.js +66 -4
  6. package/dist/heart/core.js +75 -2
  7. package/dist/heart/daemon/daemon-cli.js +523 -33
  8. package/dist/heart/daemon/daemon-entry.js +13 -5
  9. package/dist/heart/daemon/daemon-runtime-sync.js +90 -0
  10. package/dist/heart/daemon/daemon.js +42 -9
  11. package/dist/heart/daemon/hatch-animation.js +35 -0
  12. package/dist/heart/daemon/hatch-flow.js +2 -11
  13. package/dist/heart/daemon/hatch-specialist.js +6 -1
  14. package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
  15. package/dist/heart/daemon/ouro-path-installer.js +178 -0
  16. package/dist/heart/daemon/ouro-uti.js +11 -2
  17. package/dist/heart/daemon/process-manager.js +1 -1
  18. package/dist/heart/daemon/runtime-logging.js +9 -5
  19. package/dist/heart/daemon/runtime-metadata.js +118 -0
  20. package/dist/heart/daemon/sense-manager.js +266 -0
  21. package/dist/heart/daemon/specialist-orchestrator.js +129 -0
  22. package/dist/heart/daemon/specialist-prompt.js +98 -0
  23. package/dist/heart/daemon/specialist-tools.js +237 -0
  24. package/dist/heart/daemon/subagent-installer.js +10 -1
  25. package/dist/heart/daemon/wrapper-publish-guard.js +48 -0
  26. package/dist/heart/identity.js +77 -1
  27. package/dist/heart/providers/anthropic.js +19 -2
  28. package/dist/heart/sense-truth.js +61 -0
  29. package/dist/heart/streaming.js +99 -21
  30. package/dist/mind/bundle-manifest.js +58 -0
  31. package/dist/mind/friends/channel.js +8 -0
  32. package/dist/mind/friends/types.js +1 -1
  33. package/dist/mind/prompt.js +77 -3
  34. package/dist/nerves/cli-logging.js +15 -2
  35. package/dist/repertoire/ado-client.js +4 -2
  36. package/dist/repertoire/coding/feedback.js +134 -0
  37. package/dist/repertoire/coding/index.js +4 -1
  38. package/dist/repertoire/coding/manager.js +61 -2
  39. package/dist/repertoire/coding/spawner.js +3 -3
  40. package/dist/repertoire/coding/tools.js +41 -2
  41. package/dist/repertoire/data/ado-endpoints.json +188 -0
  42. package/dist/repertoire/tools-base.js +69 -5
  43. package/dist/repertoire/tools-teams.js +57 -4
  44. package/dist/repertoire/tools.js +44 -11
  45. package/dist/senses/bluebubbles-client.js +433 -0
  46. package/dist/senses/bluebubbles-entry.js +11 -0
  47. package/dist/senses/bluebubbles-media.js +244 -0
  48. package/dist/senses/bluebubbles-model.js +253 -0
  49. package/dist/senses/bluebubbles-mutation-log.js +76 -0
  50. package/dist/senses/bluebubbles.js +421 -0
  51. package/dist/senses/cli.js +293 -133
  52. package/dist/senses/debug-activity.js +107 -0
  53. package/dist/senses/teams.js +173 -54
  54. package/package.json +11 -4
  55. package/subagents/work-doer.md +26 -24
  56. package/subagents/work-merger.md +24 -30
  57. package/subagents/work-planner.md +34 -25
  58. package/dist/inner-worker-entry.js +0 -4
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FinalAnswerParser = void 0;
3
+ exports.FinalAnswerStreamer = exports.FinalAnswerParser = void 0;
4
4
  exports.toResponsesInput = toResponsesInput;
5
5
  exports.toResponsesTools = toResponsesTools;
6
6
  exports.streamChatCompletion = streamChatCompletion;
@@ -77,6 +77,89 @@ class FinalAnswerParser {
77
77
  }
78
78
  }
79
79
  exports.FinalAnswerParser = FinalAnswerParser;
80
+ // Shared helper: wraps FinalAnswerParser with onClearText + onTextChunk wiring.
81
+ // Used by all streaming providers (Chat Completions, Responses API, Anthropic)
82
+ // so the eager-match streaming pattern lives in one place.
83
+ class FinalAnswerStreamer {
84
+ parser = new FinalAnswerParser();
85
+ _detected = false;
86
+ callbacks;
87
+ constructor(callbacks) {
88
+ this.callbacks = callbacks;
89
+ }
90
+ get detected() { return this._detected; }
91
+ get streamed() { return this.parser.active; }
92
+ /** Mark final_answer as detected. Calls onClearText on the callbacks. */
93
+ activate() {
94
+ if (this._detected)
95
+ return;
96
+ this._detected = true;
97
+ this.callbacks.onClearText?.();
98
+ }
99
+ /** Feed an argument delta through the parser. Emits text via onTextChunk. */
100
+ processDelta(delta) {
101
+ if (!this._detected)
102
+ return;
103
+ const text = this.parser.process(delta);
104
+ if (text)
105
+ this.callbacks.onTextChunk(text);
106
+ }
107
+ }
108
+ exports.FinalAnswerStreamer = FinalAnswerStreamer;
109
+ function toResponsesUserContent(content) {
110
+ if (typeof content === "string") {
111
+ return content;
112
+ }
113
+ if (!Array.isArray(content)) {
114
+ return "";
115
+ }
116
+ const parts = [];
117
+ for (const part of content) {
118
+ if (!part || typeof part !== "object") {
119
+ continue;
120
+ }
121
+ if (part.type === "text" && typeof part.text === "string") {
122
+ parts.push({ type: "input_text", text: part.text });
123
+ continue;
124
+ }
125
+ if (part.type === "image_url") {
126
+ const imageUrl = typeof part.image_url?.url === "string" ? part.image_url.url : "";
127
+ if (!imageUrl)
128
+ continue;
129
+ parts.push({
130
+ type: "input_image",
131
+ image_url: imageUrl,
132
+ detail: part.image_url?.detail ?? "auto",
133
+ });
134
+ continue;
135
+ }
136
+ if (part.type === "input_audio" &&
137
+ typeof part.input_audio?.data === "string" &&
138
+ (part.input_audio.format === "mp3" || part.input_audio.format === "wav")) {
139
+ parts.push({
140
+ type: "input_audio",
141
+ input_audio: {
142
+ data: part.input_audio.data,
143
+ format: part.input_audio.format,
144
+ },
145
+ });
146
+ continue;
147
+ }
148
+ if (part.type === "file") {
149
+ const fileRecord = { type: "input_file" };
150
+ if (typeof part.file?.file_data === "string")
151
+ fileRecord.file_data = part.file.file_data;
152
+ if (typeof part.file?.file_id === "string")
153
+ fileRecord.file_id = part.file.file_id;
154
+ if (typeof part.file?.filename === "string")
155
+ fileRecord.filename = part.file.filename;
156
+ if (typeof part.file?.file_data === "string" || typeof part.file?.file_id === "string") {
157
+ parts.push(fileRecord);
158
+ }
159
+ }
160
+ }
161
+ return parts.length > 0 ? parts : "";
162
+ }
80
163
  function toResponsesInput(messages) {
81
164
  let instructions = "";
82
165
  const input = [];
@@ -90,7 +173,7 @@ function toResponsesInput(messages) {
90
173
  }
91
174
  if (msg.role === "user") {
92
175
  const u = msg;
93
- input.push({ role: "user", content: typeof u.content === "string" ? u.content : "" });
176
+ input.push({ role: "user", content: toResponsesUserContent(u.content) });
94
177
  continue;
95
178
  }
96
179
  if (msg.role === "assistant") {
@@ -106,6 +189,9 @@ function toResponsesInput(messages) {
106
189
  }
107
190
  if (a.tool_calls) {
108
191
  for (const tc of a.tool_calls) {
192
+ /* v8 ignore next -- type narrowing: OpenAI SDK only emits function tool_calls @preserve */
193
+ if (tc.type !== "function")
194
+ continue;
109
195
  input.push({
110
196
  type: "function_call",
111
197
  call_id: tc.id,
@@ -152,8 +238,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
152
238
  let toolCalls = {};
153
239
  let streamStarted = false;
154
240
  let usage;
155
- const answerParser = new FinalAnswerParser();
156
- let finalAnswerDetected = false;
241
+ const answerStreamer = new FinalAnswerStreamer(callbacks);
157
242
  // State machine for parsing inline <think> tags (MiniMax pattern)
158
243
  let contentBuf = "";
159
244
  let inThinkTag = false;
@@ -271,21 +356,18 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
271
356
  // Detect final_answer tool call on first name delta.
272
357
  // Only activate streaming if this is the sole tool call (index 0
273
358
  // and no other indices seen). Mixed calls are rejected by core.ts.
274
- if (tc.function.name === "final_answer" && !finalAnswerDetected
359
+ if (tc.function.name === "final_answer" && !answerStreamer.detected
275
360
  && tc.index === 0 && Object.keys(toolCalls).length === 1) {
276
- finalAnswerDetected = true;
277
- callbacks.onClearText?.();
361
+ answerStreamer.activate();
278
362
  }
279
363
  }
280
364
  if (tc.function?.arguments) {
281
365
  toolCalls[tc.index].arguments += tc.function.arguments;
282
366
  // Feed final_answer argument deltas to the parser for progressive
283
367
  // streaming, but only when it appears to be the sole tool call.
284
- if (finalAnswerDetected && toolCalls[tc.index].name === "final_answer"
368
+ if (answerStreamer.detected && toolCalls[tc.index].name === "final_answer"
285
369
  && Object.keys(toolCalls).length === 1) {
286
- const text = answerParser.process(tc.function.arguments);
287
- if (text)
288
- callbacks.onTextChunk(text);
370
+ answerStreamer.processDelta(tc.function.arguments);
289
371
  }
290
372
  }
291
373
  }
@@ -299,7 +381,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
299
381
  toolCalls: Object.values(toolCalls),
300
382
  outputItems: [],
301
383
  usage,
302
- finalAnswerStreamed: answerParser.active,
384
+ finalAnswerStreamed: answerStreamer.streamed,
303
385
  };
304
386
  }
305
387
  async function streamResponsesApi(client, createParams, callbacks, signal) {
@@ -317,9 +399,8 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
317
399
  const outputItems = [];
318
400
  let currentToolCall = null;
319
401
  let usage;
320
- const answerParser = new FinalAnswerParser();
402
+ const answerStreamer = new FinalAnswerStreamer(callbacks);
321
403
  let functionCallCount = 0;
322
- let finalAnswerDetected = false;
323
404
  for await (const event of response) {
324
405
  if (signal?.aborted)
325
406
  break;
@@ -352,8 +433,7 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
352
433
  // Only activate when this is the first (and so far only) function call.
353
434
  // Mixed calls are rejected by core.ts; no need to stream their args.
354
435
  if (String(event.item.name) === "final_answer" && functionCallCount === 1) {
355
- finalAnswerDetected = true;
356
- callbacks.onClearText?.();
436
+ answerStreamer.activate();
357
437
  }
358
438
  }
359
439
  break;
@@ -363,11 +443,9 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
363
443
  currentToolCall.arguments += event.delta;
364
444
  // Feed final_answer argument deltas to the parser for progressive
365
445
  // streaming, but only when it appears to be the sole function call.
366
- if (finalAnswerDetected && currentToolCall.name === "final_answer"
446
+ if (answerStreamer.detected && currentToolCall.name === "final_answer"
367
447
  && functionCallCount === 1) {
368
- const text = answerParser.process(String(event.delta));
369
- if (text)
370
- callbacks.onTextChunk(text);
448
+ answerStreamer.processDelta(String(event.delta));
371
449
  }
372
450
  }
373
451
  break;
@@ -407,6 +485,6 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
407
485
  toolCalls,
408
486
  outputItems,
409
487
  usage,
410
- finalAnswerStreamed: answerParser.active,
488
+ finalAnswerStreamed: answerStreamer.streamed,
411
489
  };
412
490
  }
@@ -34,6 +34,10 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CANONICAL_BUNDLE_MANIFEST = void 0;
37
+ exports.getPackageVersion = getPackageVersion;
38
+ exports.createBundleMeta = createBundleMeta;
39
+ exports.backfillBundleMeta = backfillBundleMeta;
40
+ exports.resetBackfillTracking = resetBackfillTracking;
37
41
  exports.isCanonicalBundlePath = isCanonicalBundlePath;
38
42
  exports.findNonCanonicalBundlePaths = findNonCanonicalBundlePaths;
39
43
  const fs = __importStar(require("fs"));
@@ -41,6 +45,7 @@ const path = __importStar(require("path"));
41
45
  const runtime_1 = require("../nerves/runtime");
42
46
  exports.CANONICAL_BUNDLE_MANIFEST = [
43
47
  { path: "agent.json", kind: "file" },
48
+ { path: "bundle-meta.json", kind: "file" },
44
49
  { path: "psyche/SOUL.md", kind: "file" },
45
50
  { path: "psyche/IDENTITY.md", kind: "file" },
46
51
  { path: "psyche/LORE.md", kind: "file" },
@@ -53,6 +58,59 @@ exports.CANONICAL_BUNDLE_MANIFEST = [
53
58
  { path: "senses", kind: "dir" },
54
59
  { path: "senses/teams", kind: "dir" },
55
60
  ];
61
+ function getPackageVersion() {
62
+ const packageJsonPath = path.resolve(__dirname, "../../package.json");
63
+ const raw = fs.readFileSync(packageJsonPath, "utf-8");
64
+ const parsed = JSON.parse(raw);
65
+ (0, runtime_1.emitNervesEvent)({
66
+ component: "mind",
67
+ event: "mind.package_version_read",
68
+ message: "read package version",
69
+ meta: { version: parsed.version },
70
+ });
71
+ return parsed.version;
72
+ }
73
+ function createBundleMeta() {
74
+ return {
75
+ runtimeVersion: getPackageVersion(),
76
+ bundleSchemaVersion: 1,
77
+ lastUpdated: new Date().toISOString(),
78
+ };
79
+ }
80
+ const _backfilledRoots = new Set();
81
+ /**
82
+ * If bundle-meta.json is missing from the agent root, create it with current runtime version.
83
+ * This backfills existing agent bundles that were created before bundle-meta.json was introduced.
84
+ * Only attempts once per bundleRoot per process.
85
+ */
86
+ function backfillBundleMeta(bundleRoot) {
87
+ if (_backfilledRoots.has(bundleRoot))
88
+ return;
89
+ _backfilledRoots.add(bundleRoot);
90
+ const metaPath = path.join(bundleRoot, "bundle-meta.json");
91
+ try {
92
+ if (fs.existsSync(metaPath)) {
93
+ return;
94
+ }
95
+ const meta = createBundleMeta();
96
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
97
+ (0, runtime_1.emitNervesEvent)({
98
+ component: "mind",
99
+ event: "mind.bundle_meta_backfill",
100
+ message: "backfilled missing bundle-meta.json",
101
+ meta: { bundleRoot },
102
+ });
103
+ }
104
+ catch {
105
+ // Non-blocking: if we can't write, that's okay
106
+ }
107
+ }
108
+ /**
109
+ * Reset the backfill tracking set. Used in tests.
110
+ */
111
+ function resetBackfillTracking() {
112
+ _backfilledRoots.clear();
113
+ }
56
114
  const CANONICAL_FILE_PATHS = new Set(exports.CANONICAL_BUNDLE_MANIFEST
57
115
  .filter((entry) => entry.kind === "file")
58
116
  .map((entry) => entry.path));
@@ -21,6 +21,14 @@ const CHANNEL_CAPABILITIES = {
21
21
  supportsRichCards: true,
22
22
  maxMessageLength: Infinity,
23
23
  },
24
+ bluebubbles: {
25
+ channel: "bluebubbles",
26
+ availableIntegrations: [],
27
+ supportsMarkdown: false,
28
+ supportsStreaming: false,
29
+ supportsRichCards: false,
30
+ maxMessageLength: Infinity,
31
+ },
24
32
  };
25
33
  const DEFAULT_CAPABILITIES = {
26
34
  channel: "cli",
@@ -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",
@@ -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) {
@@ -194,15 +197,84 @@ function runtimeInfoSection(channel) {
194
197
  lines.push(`agent: ${agentName}`);
195
198
  lines.push(`cwd: ${process.cwd()}`);
196
199
  lines.push(`channel: ${channel}`);
200
+ lines.push(`current sense: ${channel}`);
197
201
  lines.push(`i can read and modify my own source code.`);
198
202
  if (channel === "cli") {
199
203
  lines.push("i introduce myself on boot with a fun random greeting.");
200
204
  }
205
+ else if (channel === "bluebubbles") {
206
+ 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.");
207
+ }
201
208
  else {
202
209
  lines.push("i am responding in Microsoft Teams. i keep responses concise. i use markdown formatting. i do not introduce myself on boot.");
203
210
  }
211
+ lines.push("");
212
+ lines.push(...senseRuntimeGuidance(channel));
204
213
  return lines.join("\n");
205
214
  }
215
+ function hasTextField(record, key) {
216
+ return typeof record?.[key] === "string" && record[key].trim().length > 0;
217
+ }
218
+ function localSenseStatusLines() {
219
+ if (_senseStatusLinesCache) {
220
+ return [..._senseStatusLinesCache];
221
+ }
222
+ const config = (0, identity_1.loadAgentConfig)();
223
+ const senses = config.senses ?? {
224
+ cli: { enabled: true },
225
+ teams: { enabled: false },
226
+ bluebubbles: { enabled: false },
227
+ };
228
+ let payload = {};
229
+ try {
230
+ const raw = fs.readFileSync((0, identity_1.getAgentSecretsPath)(), "utf-8");
231
+ const parsed = JSON.parse(raw);
232
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
233
+ payload = parsed;
234
+ }
235
+ }
236
+ catch {
237
+ payload = {};
238
+ }
239
+ const teams = payload.teams;
240
+ const bluebubbles = payload.bluebubbles;
241
+ const configured = {
242
+ cli: true,
243
+ teams: hasTextField(teams, "clientId") && hasTextField(teams, "clientSecret") && hasTextField(teams, "tenantId"),
244
+ bluebubbles: hasTextField(bluebubbles, "serverUrl") && hasTextField(bluebubbles, "password"),
245
+ };
246
+ const rows = [
247
+ { label: "CLI", status: "interactive" },
248
+ {
249
+ label: "Teams",
250
+ status: !senses.teams.enabled ? "disabled" : configured.teams ? "ready" : "needs_config",
251
+ },
252
+ {
253
+ label: "BlueBubbles",
254
+ status: !senses.bluebubbles.enabled ? "disabled" : configured.bluebubbles ? "ready" : "needs_config",
255
+ },
256
+ ];
257
+ _senseStatusLinesCache = rows.map((row) => `- ${row.label}: ${row.status}`);
258
+ return [..._senseStatusLinesCache];
259
+ }
260
+ function senseRuntimeGuidance(channel) {
261
+ const lines = ["available senses:"];
262
+ lines.push(...localSenseStatusLines());
263
+ lines.push("sense states:");
264
+ lines.push("- interactive = available when opened by the user instead of kept running by the daemon");
265
+ lines.push("- disabled = turned off in agent.json");
266
+ lines.push("- needs_config = enabled but missing required secrets.json values");
267
+ lines.push("- ready = enabled and configured; `ouro up` should bring it online");
268
+ lines.push("- running = enabled and currently active");
269
+ lines.push("- error = enabled but unhealthy");
270
+ 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.");
271
+ lines.push("teams setup truth: enable `senses.teams.enabled`, then provide `teams.clientId`, `teams.clientSecret`, and `teams.tenantId` in secrets.json.");
272
+ lines.push("bluebubbles setup truth: enable `senses.bluebubbles.enabled`, then provide `bluebubbles.serverUrl` and `bluebubbles.password` in secrets.json.");
273
+ if (channel === "cli") {
274
+ lines.push("cli is interactive: it is available when the user opens it, not something `ouro up` daemonizes.");
275
+ }
276
+ return lines;
277
+ }
206
278
  function providerSection() {
207
279
  return `## my provider\n${(0, core_1.getProviderDisplayLabel)()}`;
208
280
  }
@@ -210,8 +282,8 @@ function dateSection() {
210
282
  const today = new Date().toISOString().slice(0, 10);
211
283
  return `current date: ${today}`;
212
284
  }
213
- function toolsSection(channel, options) {
214
- 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);
215
287
  const activeTools = (options?.toolChoiceRequired ?? true) ? [...channelTools, tools_1.finalAnswerTool] : channelTools;
216
288
  const list = activeTools
217
289
  .map((t) => `- ${t.function.name}: ${t.function.description}`)
@@ -316,6 +388,8 @@ async function buildSystem(channel = "cli", options, context) {
316
388
  message: "buildSystem started",
317
389
  meta: { channel, has_context: Boolean(context), tool_choice_required: Boolean(options?.toolChoiceRequired) },
318
390
  });
391
+ // Backfill bundle-meta.json for existing agents that don't have one
392
+ (0, bundle_manifest_1.backfillBundleMeta)((0, identity_1.getAgentRoot)());
319
393
  const system = [
320
394
  soulSection(),
321
395
  identitySection(),
@@ -325,7 +399,7 @@ async function buildSystem(channel = "cli", options, context) {
325
399
  runtimeInfoSection(channel),
326
400
  providerSection(),
327
401
  dateSection(),
328
- toolsSection(channel, options),
402
+ toolsSection(channel, options, context),
329
403
  skillsSection(),
330
404
  taskBoardSection(),
331
405
  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; } });