@ouro.bot/cli 0.1.0-alpha.13 → 0.1.0-alpha.131

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 (126) hide show
  1. package/AdoptionSpecialist.ouro/psyche/SOUL.md +2 -2
  2. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  3. package/README.md +147 -205
  4. package/changelog.json +814 -0
  5. package/dist/heart/active-work.js +622 -0
  6. package/dist/heart/bridges/manager.js +358 -0
  7. package/dist/heart/bridges/state-machine.js +135 -0
  8. package/dist/heart/bridges/store.js +123 -0
  9. package/dist/heart/commitments.js +105 -0
  10. package/dist/heart/config.js +66 -21
  11. package/dist/heart/core.js +518 -100
  12. package/dist/heart/cross-chat-delivery.js +146 -0
  13. package/dist/heart/daemon/agent-discovery.js +81 -0
  14. package/dist/heart/daemon/auth-flow.js +457 -0
  15. package/dist/heart/daemon/daemon-cli.js +1516 -195
  16. package/dist/heart/daemon/daemon-entry.js +43 -2
  17. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  18. package/dist/heart/daemon/daemon.js +261 -1
  19. package/dist/heart/daemon/hatch-animation.js +10 -3
  20. package/dist/heart/daemon/hatch-flow.js +7 -72
  21. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  22. package/dist/heart/daemon/launchd.js +159 -0
  23. package/dist/heart/daemon/log-tailer.js +4 -3
  24. package/dist/heart/daemon/message-router.js +17 -8
  25. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  26. package/dist/heart/daemon/ouro-path-installer.js +57 -29
  27. package/dist/heart/daemon/ouro-version-manager.js +171 -0
  28. package/dist/heart/daemon/process-manager.js +13 -0
  29. package/dist/heart/daemon/run-hooks.js +37 -0
  30. package/dist/heart/daemon/runtime-logging.js +58 -15
  31. package/dist/heart/daemon/runtime-metadata.js +219 -0
  32. package/dist/heart/daemon/runtime-mode.js +67 -0
  33. package/dist/heart/daemon/sense-manager.js +50 -2
  34. package/dist/heart/daemon/skill-management-installer.js +94 -0
  35. package/dist/heart/daemon/socket-client.js +202 -0
  36. package/dist/heart/daemon/specialist-orchestrator.js +2 -2
  37. package/dist/heart/daemon/specialist-prompt.js +7 -4
  38. package/dist/heart/daemon/specialist-tools.js +52 -3
  39. package/dist/heart/daemon/staged-restart.js +114 -0
  40. package/dist/heart/daemon/thoughts.js +507 -0
  41. package/dist/heart/daemon/update-checker.js +111 -0
  42. package/dist/heart/daemon/update-hooks.js +138 -0
  43. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  44. package/dist/heart/delegation.js +62 -0
  45. package/dist/heart/identity.js +64 -21
  46. package/dist/heart/kicks.js +1 -19
  47. package/dist/heart/model-capabilities.js +48 -0
  48. package/dist/heart/obligations.js +197 -0
  49. package/dist/heart/progress-story.js +42 -0
  50. package/dist/heart/provider-failover.js +88 -0
  51. package/dist/heart/provider-ping.js +159 -0
  52. package/dist/heart/providers/anthropic-token.js +163 -0
  53. package/dist/heart/providers/anthropic.js +195 -34
  54. package/dist/heart/providers/azure.js +115 -9
  55. package/dist/heart/providers/github-copilot.js +157 -0
  56. package/dist/heart/providers/minimax.js +33 -3
  57. package/dist/heart/providers/openai-codex.js +49 -14
  58. package/dist/heart/safe-workspace.js +381 -0
  59. package/dist/heart/session-activity.js +173 -0
  60. package/dist/heart/session-recall.js +216 -0
  61. package/dist/heart/streaming.js +108 -24
  62. package/dist/heart/target-resolution.js +123 -0
  63. package/dist/heart/tool-loop.js +194 -0
  64. package/dist/heart/turn-coordinator.js +28 -0
  65. package/dist/mind/associative-recall.js +14 -2
  66. package/dist/mind/bundle-manifest.js +12 -0
  67. package/dist/mind/context.js +60 -14
  68. package/dist/mind/first-impressions.js +16 -2
  69. package/dist/mind/friends/channel.js +35 -0
  70. package/dist/mind/friends/group-context.js +144 -0
  71. package/dist/mind/friends/store-file.js +19 -0
  72. package/dist/mind/friends/trust-explanation.js +74 -0
  73. package/dist/mind/friends/types.js +8 -0
  74. package/dist/mind/memory.js +27 -26
  75. package/dist/mind/obligation-steering.js +221 -0
  76. package/dist/mind/pending.js +76 -9
  77. package/dist/mind/phrases.js +1 -0
  78. package/dist/mind/prompt.js +456 -77
  79. package/dist/mind/token-estimate.js +8 -12
  80. package/dist/nerves/cli-logging.js +15 -2
  81. package/dist/nerves/coverage/run-artifacts.js +1 -1
  82. package/dist/nerves/index.js +12 -0
  83. package/dist/nerves/runtime.js +5 -1
  84. package/dist/repertoire/ado-client.js +4 -2
  85. package/dist/repertoire/coding/context-pack.js +254 -0
  86. package/dist/repertoire/coding/feedback.js +301 -0
  87. package/dist/repertoire/coding/index.js +4 -1
  88. package/dist/repertoire/coding/manager.js +210 -4
  89. package/dist/repertoire/coding/spawner.js +39 -9
  90. package/dist/repertoire/coding/tools.js +171 -4
  91. package/dist/repertoire/data/ado-endpoints.json +188 -0
  92. package/dist/repertoire/guardrails.js +290 -0
  93. package/dist/repertoire/mcp-client.js +254 -0
  94. package/dist/repertoire/mcp-manager.js +198 -0
  95. package/dist/repertoire/skills.js +3 -26
  96. package/dist/repertoire/tasks/board.js +12 -0
  97. package/dist/repertoire/tasks/index.js +23 -9
  98. package/dist/repertoire/tasks/transitions.js +1 -2
  99. package/dist/repertoire/tools-base.js +925 -250
  100. package/dist/repertoire/tools-bluebubbles.js +93 -0
  101. package/dist/repertoire/tools-teams.js +58 -25
  102. package/dist/repertoire/tools.js +106 -53
  103. package/dist/senses/bluebubbles-client.js +210 -5
  104. package/dist/senses/bluebubbles-entry.js +2 -0
  105. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  106. package/dist/senses/bluebubbles-media.js +339 -0
  107. package/dist/senses/bluebubbles-model.js +12 -4
  108. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  109. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  110. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  111. package/dist/senses/bluebubbles.js +915 -45
  112. package/dist/senses/cli-layout.js +187 -0
  113. package/dist/senses/cli.js +374 -131
  114. package/dist/senses/continuity.js +94 -0
  115. package/dist/senses/debug-activity.js +154 -0
  116. package/dist/senses/inner-dialog-worker.js +47 -18
  117. package/dist/senses/inner-dialog.js +388 -83
  118. package/dist/senses/pipeline.js +444 -0
  119. package/dist/senses/teams.js +607 -129
  120. package/dist/senses/trust-gate.js +112 -2
  121. package/package.json +9 -3
  122. package/subagents/README.md +4 -70
  123. package/dist/heart/daemon/subagent-installer.js +0 -134
  124. package/subagents/work-doer.md +0 -233
  125. package/subagents/work-merger.md +0 -624
  126. package/subagents/work-planner.md +0 -373
@@ -33,11 +33,18 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.MarkdownStreamer = exports.InputController = exports.Spinner = void 0;
36
+ exports.MarkdownStreamer = exports.InputController = exports.Spinner = exports.StreamingWordWrapper = exports.wrapCliText = exports.formatEchoedInputSummary = void 0;
37
+ exports.formatPendingPrefix = formatPendingPrefix;
38
+ exports.getCliContinuityIngressTexts = getCliContinuityIngressTexts;
39
+ exports.writeCliAsyncAssistantMessage = writeCliAsyncAssistantMessage;
40
+ exports.pauseActiveSpinner = pauseActiveSpinner;
41
+ exports.resumeActiveSpinner = resumeActiveSpinner;
42
+ exports.setActiveSpinner = setActiveSpinner;
37
43
  exports.handleSigint = handleSigint;
38
44
  exports.addHistory = addHistory;
39
45
  exports.renderMarkdown = renderMarkdown;
40
46
  exports.createCliCallbacks = createCliCallbacks;
47
+ exports.createDebouncedLines = createDebouncedLines;
41
48
  exports.runCliSession = runCliSession;
42
49
  exports.main = main;
43
50
  const readline = __importStar(require("readline"));
@@ -50,9 +57,9 @@ const format_1 = require("../mind/format");
50
57
  const config_1 = require("../heart/config");
51
58
  const context_1 = require("../mind/context");
52
59
  const pending_1 = require("../mind/pending");
53
- const prompt_refresh_1 = require("../mind/prompt-refresh");
54
60
  const commands_1 = require("./commands");
55
61
  const identity_1 = require("../heart/identity");
62
+ const mcp_manager_1 = require("../repertoire/mcp-manager");
56
63
  const nerves_1 = require("../nerves");
57
64
  const store_file_1 = require("../mind/friends/store-file");
58
65
  const resolver_1 = require("../mind/friends/resolver");
@@ -60,7 +67,57 @@ const tokens_1 = require("../mind/friends/tokens");
60
67
  const cli_logging_1 = require("../nerves/cli-logging");
61
68
  const runtime_1 = require("../nerves/runtime");
62
69
  const trust_gate_1 = require("./trust-gate");
70
+ const pipeline_1 = require("./pipeline");
71
+ const channel_1 = require("../mind/friends/channel");
63
72
  const session_lock_1 = require("./session-lock");
73
+ const update_hooks_1 = require("../heart/daemon/update-hooks");
74
+ const bundle_meta_1 = require("../heart/daemon/hooks/bundle-meta");
75
+ const bundle_manifest_1 = require("../mind/bundle-manifest");
76
+ const cli_layout_1 = require("./cli-layout");
77
+ var cli_layout_2 = require("./cli-layout");
78
+ Object.defineProperty(exports, "formatEchoedInputSummary", { enumerable: true, get: function () { return cli_layout_2.formatEchoedInputSummary; } });
79
+ Object.defineProperty(exports, "wrapCliText", { enumerable: true, get: function () { return cli_layout_2.wrapCliText; } });
80
+ Object.defineProperty(exports, "StreamingWordWrapper", { enumerable: true, get: function () { return cli_layout_2.StreamingWordWrapper; } });
81
+ /**
82
+ * Format pending messages as content-prefix strings for injection into
83
+ * the next user message. Self-messages (from === agentName) become
84
+ * `[inner thought: {content}]`, inter-agent messages become
85
+ * `[message from {name}: {content}]`.
86
+ */
87
+ function formatPendingPrefix(messages, agentName) {
88
+ return messages
89
+ .map((msg) => msg.from === agentName
90
+ ? `[inner thought: ${msg.content}]`
91
+ : `[message from ${msg.from}: ${msg.content}]`)
92
+ .join("\n");
93
+ }
94
+ function getCliContinuityIngressTexts(input) {
95
+ const trimmed = input.trim();
96
+ return trimmed ? [trimmed] : [];
97
+ }
98
+ const CLI_PROMPT = "\x1b[36m> \x1b[0m";
99
+ function writeCliAsyncAssistantMessage(rl, message, stdout = process.stdout) {
100
+ const rlInt = rl;
101
+ const currentLine = rlInt.line ?? "";
102
+ const currentCursor = rlInt.cursor ?? currentLine.length;
103
+ stdout.write("\r\x1b[K");
104
+ stdout.write(`${renderMarkdown(message)}\n`);
105
+ stdout.write(CLI_PROMPT);
106
+ if (!currentLine)
107
+ return;
108
+ stdout.write(currentLine);
109
+ if (currentCursor < currentLine.length) {
110
+ readline.cursorTo(process.stdout, 2 + currentCursor);
111
+ }
112
+ }
113
+ // Module-level active spinner for log coordination.
114
+ // The terminal log sink calls these to avoid interleaving with spinner output.
115
+ let _activeSpinner = null;
116
+ /* v8 ignore start -- spinner coordination: exercised at runtime, not unit-testable without real terminal @preserve */
117
+ function pauseActiveSpinner() { _activeSpinner?.pause(); }
118
+ function resumeActiveSpinner() { _activeSpinner?.resume(); }
119
+ /* v8 ignore stop */
120
+ function setActiveSpinner(s) { _activeSpinner = s; }
64
121
  // spinner that only touches stderr, cleans up after itself
65
122
  // exported for direct testability (stop-without-start branch)
66
123
  class Spinner {
@@ -71,12 +128,14 @@ class Spinner {
71
128
  msg = "";
72
129
  phrases = null;
73
130
  lastPhrase = "";
131
+ stopped = false;
74
132
  constructor(m = "working", phrases) {
75
133
  this.msg = m;
76
134
  if (phrases && phrases.length > 0)
77
135
  this.phrases = phrases;
78
136
  }
79
137
  start() {
138
+ this.stopped = false;
80
139
  process.stderr.write("\r\x1b[K");
81
140
  this.spin();
82
141
  this.iv = setInterval(() => this.spin(), 80);
@@ -85,15 +144,37 @@ class Spinner {
85
144
  }
86
145
  }
87
146
  spin() {
88
- process.stderr.write(`\r${this.frames[this.i]} ${this.msg}... `);
147
+ // Guard: clearInterval can't prevent already-dequeued callbacks
148
+ /* v8 ignore next -- race guard: timer callback fires after stop() @preserve */
149
+ if (this.stopped)
150
+ return;
151
+ process.stderr.write(`\r\x1b[K${this.frames[this.i]} ${this.msg}... `);
89
152
  this.i = (this.i + 1) % this.frames.length;
90
153
  }
91
154
  rotatePhrase() {
155
+ /* v8 ignore next -- race guard: timer callback fires after stop() @preserve */
156
+ if (this.stopped)
157
+ return;
92
158
  const next = (0, phrases_1.pickPhrase)(this.phrases, this.lastPhrase);
93
159
  this.lastPhrase = next;
94
160
  this.msg = next;
95
161
  }
162
+ /* v8 ignore start -- pause/resume: exercised at runtime via log sink coordination @preserve */
163
+ /** Clear the spinner line temporarily so other output can print cleanly. */
164
+ pause() {
165
+ if (this.stopped)
166
+ return;
167
+ process.stderr.write("\r\x1b[K");
168
+ }
169
+ /** Restore the spinner line after a pause. */
170
+ resume() {
171
+ if (this.stopped)
172
+ return;
173
+ this.spin();
174
+ }
175
+ /* v8 ignore stop */
96
176
  stop(ok) {
177
+ this.stopped = true;
97
178
  if (this.iv) {
98
179
  clearInterval(this.iv);
99
180
  this.iv = null;
@@ -280,41 +361,54 @@ function createCliCallbacks() {
280
361
  meta: {},
281
362
  });
282
363
  let currentSpinner = null;
283
- let hadReasoning = false;
364
+ function setSpinner(s) { currentSpinner = s; setActiveSpinner(s); }
284
365
  let hadToolRun = false;
285
366
  let textDirty = false; // true when text/reasoning was written without a trailing newline
286
367
  const streamer = new MarkdownStreamer();
368
+ const wrapper = new cli_layout_1.StreamingWordWrapper();
287
369
  return {
288
370
  onModelStart: () => {
289
371
  currentSpinner?.stop();
290
- currentSpinner = null;
291
- hadReasoning = false;
372
+ setSpinner(null);
292
373
  textDirty = false;
293
374
  streamer.reset();
375
+ wrapper.reset();
294
376
  const phrases = (0, phrases_1.getPhrases)();
295
377
  const pool = hadToolRun ? phrases.followup : phrases.thinking;
296
378
  const first = (0, phrases_1.pickPhrase)(pool);
297
- currentSpinner = new Spinner(first, pool);
379
+ setSpinner(new Spinner(first, pool));
298
380
  currentSpinner.start();
299
381
  },
300
382
  onModelStreamStart: () => {
301
- currentSpinner?.stop();
302
- currentSpinner = null;
383
+ // No-op: content callbacks (onTextChunk, onReasoningChunk) handle
384
+ // stopping the spinner. onModelStreamStart fires too early and
385
+ // doesn't fire at all for final_answer tool streaming.
386
+ },
387
+ onClearText: () => {
388
+ streamer.reset();
389
+ wrapper.reset();
303
390
  },
304
391
  onTextChunk: (text) => {
305
- if (hadReasoning) {
306
- process.stdout.write("\n\n");
307
- hadReasoning = false;
392
+ // Stop spinner if still running — final_answer streaming and Anthropic
393
+ // tool-only responses bypass onModelStreamStart, so the spinner would
394
+ // otherwise keep running (and its \r writes overwrite response text).
395
+ if (currentSpinner) {
396
+ currentSpinner.stop();
397
+ setSpinner(null);
308
398
  }
309
399
  const rendered = streamer.push(text);
310
- if (rendered)
311
- process.stdout.write(rendered);
400
+ /* v8 ignore start -- wrapper integration: tested via cli.test.ts onTextChunk tests @preserve */
401
+ if (rendered) {
402
+ const wrapped = wrapper.push(rendered);
403
+ if (wrapped)
404
+ process.stdout.write(wrapped);
405
+ }
406
+ /* v8 ignore stop */
312
407
  textDirty = text.length > 0 && !text.endsWith("\n");
313
408
  },
314
- onReasoningChunk: (text) => {
315
- hadReasoning = true;
316
- process.stdout.write(`\x1b[2m${text}\x1b[0m`);
317
- textDirty = text.length > 0 && !text.endsWith("\n");
409
+ onReasoningChunk: (_text) => {
410
+ // Keep reasoning private in the CLI surface. The spinner continues to
411
+ // represent active thinking until actual tool or answer output arrives.
318
412
  },
319
413
  onToolStart: (_name, _args) => {
320
414
  // Stop the model-start spinner: when the model returns only tool calls
@@ -329,13 +423,13 @@ function createCliCallbacks() {
329
423
  }
330
424
  const toolPhrases = (0, phrases_1.getPhrases)().tool;
331
425
  const first = (0, phrases_1.pickPhrase)(toolPhrases);
332
- currentSpinner = new Spinner(first, toolPhrases);
426
+ setSpinner(new Spinner(first, toolPhrases));
333
427
  currentSpinner.start();
334
428
  hadToolRun = true;
335
429
  },
336
430
  onToolEnd: (name, argSummary, success) => {
337
431
  currentSpinner?.stop();
338
- currentSpinner = null;
432
+ setSpinner(null);
339
433
  const msg = (0, format_1.formatToolResult)(name, argSummary, success);
340
434
  const color = success ? "\x1b[32m" : "\x1b[31m";
341
435
  process.stderr.write(`${color}${msg}\x1b[0m\n`);
@@ -343,17 +437,17 @@ function createCliCallbacks() {
343
437
  onError: (error, severity) => {
344
438
  if (severity === "transient") {
345
439
  currentSpinner?.fail(error.message);
346
- currentSpinner = null;
440
+ setSpinner(null);
347
441
  }
348
442
  else {
349
443
  currentSpinner?.stop();
350
- currentSpinner = null;
444
+ setSpinner(null);
351
445
  process.stderr.write(`\x1b[31m${(0, format_1.formatError)(error)}\x1b[0m\n`);
352
446
  }
353
447
  },
354
448
  onKick: () => {
355
449
  currentSpinner?.stop();
356
- currentSpinner = null;
450
+ setSpinner(null);
357
451
  if (textDirty) {
358
452
  process.stdout.write("\n");
359
453
  textDirty = false;
@@ -362,17 +456,65 @@ function createCliCallbacks() {
362
456
  },
363
457
  flushMarkdown: () => {
364
458
  currentSpinner?.stop();
365
- currentSpinner = null;
459
+ setSpinner(null);
460
+ /* v8 ignore start -- wrapper flush: tested via cli.test.ts flushMarkdown tests @preserve */
366
461
  const remaining = streamer.flush();
367
- if (remaining)
368
- process.stdout.write(remaining);
462
+ if (remaining) {
463
+ const wrapped = wrapper.push(remaining);
464
+ if (wrapped)
465
+ process.stdout.write(wrapped);
466
+ }
467
+ const tail = wrapper.flush();
468
+ if (tail)
469
+ process.stdout.write(tail);
470
+ /* v8 ignore stop */
369
471
  },
370
472
  };
371
473
  }
474
+ // Debounced line iterator: collects rapid-fire lines (paste) into a single input.
475
+ // When the debounce timeout wins the race, the pending iter.next() is saved
476
+ // and reused in the next iteration to prevent it from silently consuming input.
477
+ async function* createDebouncedLines(source, debounceMs) {
478
+ if (debounceMs <= 0) {
479
+ yield* source;
480
+ return;
481
+ }
482
+ const iter = source[Symbol.asyncIterator]();
483
+ let pending = null;
484
+ while (true) {
485
+ const first = pending ? await pending : await iter.next();
486
+ pending = null;
487
+ if (first.done)
488
+ break;
489
+ const lines = [first.value];
490
+ let more = true;
491
+ while (more) {
492
+ const nextPromise = iter.next();
493
+ const raced = await Promise.race([
494
+ nextPromise.then((r) => ({ kind: "line", result: r })),
495
+ new Promise((r) => setTimeout(() => r({ kind: "timeout" }), debounceMs)),
496
+ ]);
497
+ if (raced.kind === "timeout") {
498
+ pending = nextPromise;
499
+ more = false;
500
+ }
501
+ else if (raced.result.done) {
502
+ more = false;
503
+ }
504
+ else {
505
+ lines.push(raced.result.value);
506
+ }
507
+ }
508
+ yield lines.join("\n");
509
+ }
510
+ }
372
511
  async function runCliSession(options) {
512
+ /* v8 ignore start -- integration: runCliSession is interactive, tested via E2E @preserve */
373
513
  const pasteDebounceMs = options.pasteDebounceMs ?? 50;
374
514
  const registry = (0, commands_1.createCommandRegistry)();
375
- (0, commands_1.registerDefaultCommands)(registry);
515
+ if (!options.disableCommands) {
516
+ (0, commands_1.registerDefaultCommands)(registry);
517
+ }
376
518
  const messages = options.messages
377
519
  ?? [{ role: "system", content: await (0, prompt_1.buildSystem)("cli") }];
378
520
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
@@ -381,9 +523,30 @@ async function runCliSession(options) {
381
523
  const history = [];
382
524
  let closed = false;
383
525
  rl.on("close", () => { closed = true; });
384
- // eslint-disable-next-line no-console -- terminal UX: startup banner
385
- console.log(`\n${options.agentName} (type /commands for help)\n`);
526
+ if (options.banner !== false) {
527
+ const bannerText = typeof options.banner === "string"
528
+ ? options.banner
529
+ : `${options.agentName} (type /commands for help)`;
530
+ // eslint-disable-next-line no-console -- terminal UX: startup banner
531
+ console.log(`\n${bannerText}\n`);
532
+ }
386
533
  const cliCallbacks = createCliCallbacks();
534
+ const effectiveToolContext = {
535
+ signin: options.toolContext?.signin ?? (async () => undefined),
536
+ ...options.toolContext,
537
+ codingFeedback: {
538
+ send: async (message) => {
539
+ const assistantMessage = {
540
+ role: "assistant",
541
+ content: message,
542
+ };
543
+ messages.push(assistantMessage);
544
+ await options.onAsyncAssistantMessage?.(messages, assistantMessage);
545
+ writeCliAsyncAssistantMessage(rl, message);
546
+ await options.toolContext?.codingFeedback?.send(message);
547
+ },
548
+ },
549
+ };
387
550
  // exitOnToolCall machinery: wrap execTool to detect target tool
388
551
  let exitToolResult;
389
552
  let exitToolFired = false;
@@ -394,13 +557,15 @@ async function runCliSession(options) {
394
557
  if (name === options.exitOnToolCall) {
395
558
  exitToolResult = result;
396
559
  exitToolFired = true;
560
+ // Abort immediately so the model doesn't generate more output
561
+ // (e.g. reasoning about calling final_answer after complete_adoption)
562
+ currentAbort?.abort();
397
563
  }
398
564
  return result;
399
565
  }
400
566
  : resolvedExecTool;
401
567
  // Resolve toolChoiceRequired: use explicit option if set, else fall back to toggle
402
568
  const getEffectiveToolChoiceRequired = () => options.toolChoiceRequired !== undefined ? options.toolChoiceRequired : (0, commands_1.getToolChoiceRequired)();
403
- process.stdout.write("\x1b[36m> \x1b[0m");
404
569
  // Ctrl-C at the input prompt: clear line or warn/exit
405
570
  rl.on("SIGINT", () => {
406
571
  const rlInt = rl;
@@ -409,50 +574,20 @@ async function runCliSession(options) {
409
574
  if (result === "clear") {
410
575
  rlInt.line = "";
411
576
  rlInt.cursor = 0;
412
- process.stdout.write("\r\x1b[K\x1b[36m> \x1b[0m");
577
+ process.stdout.write(`\r\x1b[K${CLI_PROMPT}`);
413
578
  }
414
579
  else if (result === "warn") {
415
580
  rlInt.line = "";
416
581
  rlInt.cursor = 0;
417
582
  process.stdout.write("\r\x1b[K");
418
583
  process.stderr.write("press Ctrl-C again to exit\n");
419
- process.stdout.write("\x1b[36m> \x1b[0m");
584
+ process.stdout.write(CLI_PROMPT);
420
585
  }
421
586
  else {
422
587
  rl.close();
423
588
  }
424
589
  });
425
- // Debounced line iterator: collects rapid-fire lines (paste) into a single input
426
- async function* debouncedLines(source) {
427
- if (pasteDebounceMs <= 0) {
428
- yield* source;
429
- return;
430
- }
431
- const iter = source[Symbol.asyncIterator]();
432
- while (true) {
433
- const first = await iter.next();
434
- if (first.done)
435
- break;
436
- const lines = [first.value];
437
- let more = true;
438
- while (more) {
439
- const raced = await Promise.race([
440
- iter.next().then((r) => ({ kind: "line", result: r })),
441
- new Promise((r) => setTimeout(() => r({ kind: "timeout" }), pasteDebounceMs)),
442
- ]);
443
- if (raced.kind === "timeout") {
444
- more = false;
445
- }
446
- else if (raced.result.done) {
447
- more = false;
448
- }
449
- else {
450
- lines.push(raced.result.value);
451
- }
452
- }
453
- yield lines.join("\n");
454
- }
455
- }
590
+ const debouncedLines = (source) => createDebouncedLines(source, pasteDebounceMs);
456
591
  (0, runtime_1.emitNervesEvent)({
457
592
  component: "senses",
458
593
  event: "senses.cli_session_start",
@@ -460,12 +595,56 @@ async function runCliSession(options) {
460
595
  meta: { agentName: options.agentName, hasExitOnToolCall: !!options.exitOnToolCall },
461
596
  });
462
597
  let exitReason = "user_quit";
598
+ // Auto-first-turn: process the last user message immediately so the agent
599
+ // speaks first (e.g. specialist greeting). Only triggers when explicitly opted in.
600
+ if (options.autoFirstTurn && messages.length > 0 && messages[messages.length - 1]?.role === "user") {
601
+ currentAbort = new AbortController();
602
+ const traceId = (0, nerves_1.createTraceId)();
603
+ ctrl.suppress(() => currentAbort.abort());
604
+ let result;
605
+ try {
606
+ result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
607
+ toolChoiceRequired: getEffectiveToolChoiceRequired(),
608
+ traceId,
609
+ tools: options.tools,
610
+ execTool: wrappedExecTool,
611
+ toolContext: effectiveToolContext,
612
+ });
613
+ }
614
+ catch (err) {
615
+ // AbortError (Ctrl-C) -- silently continue to prompt
616
+ // All other errors: show the user what happened
617
+ if (!(err instanceof DOMException && err.name === "AbortError")) {
618
+ process.stderr.write(`\x1b[31m${err instanceof Error ? err.message : String(err)}\x1b[0m\n`);
619
+ }
620
+ }
621
+ cliCallbacks.flushMarkdown();
622
+ ctrl.restore();
623
+ currentAbort = null;
624
+ if (exitToolFired) {
625
+ exitReason = "tool_exit";
626
+ rl.close();
627
+ }
628
+ else {
629
+ const lastMsg = messages[messages.length - 1];
630
+ if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
631
+ process.stderr.write("\x1b[33m(empty response)\x1b[0m\n");
632
+ }
633
+ process.stdout.write("\n\n");
634
+ if (options.onTurnEnd) {
635
+ await options.onTurnEnd(messages, result ?? { usage: undefined });
636
+ }
637
+ }
638
+ }
639
+ if (!exitToolFired) {
640
+ process.stdout.write(CLI_PROMPT);
641
+ }
463
642
  try {
464
643
  for await (const input of debouncedLines(rl)) {
465
644
  if (closed)
466
645
  break;
467
646
  if (!input.trim()) {
468
- process.stdout.write("\x1b[36m> \x1b[0m");
647
+ process.stdout.write(CLI_PROMPT);
469
648
  continue;
470
649
  }
471
650
  // Optional input gate (e.g. trust gate in main)
@@ -477,7 +656,7 @@ async function runCliSession(options) {
477
656
  }
478
657
  if (closed)
479
658
  break;
480
- process.stdout.write("\x1b[36m> \x1b[0m");
659
+ process.stdout.write(CLI_PROMPT);
481
660
  continue;
482
661
  }
483
662
  }
@@ -495,42 +674,50 @@ async function runCliSession(options) {
495
674
  await options.onNewSession?.();
496
675
  // eslint-disable-next-line no-console -- terminal UX: session cleared
497
676
  console.log("session cleared");
498
- process.stdout.write("\x1b[36m> \x1b[0m");
677
+ process.stdout.write(CLI_PROMPT);
499
678
  continue;
500
679
  }
501
680
  else if (dispatchResult.result.action === "response") {
502
681
  // eslint-disable-next-line no-console -- terminal UX: command dispatch result
503
682
  console.log(dispatchResult.result.message || "");
504
- process.stdout.write("\x1b[36m> \x1b[0m");
683
+ process.stdout.write(CLI_PROMPT);
505
684
  continue;
506
685
  }
507
686
  }
508
687
  }
509
- // Re-style the echoed input lines
688
+ // Re-style the echoed input lines without leaving wrapped paste remnants behind.
510
689
  const cols = process.stdout.columns || 80;
511
- const inputLines = input.split("\n");
512
- let echoRows = 0;
513
- for (const line of inputLines) {
514
- echoRows += Math.ceil((2 + line.length) / cols);
515
- }
516
- process.stdout.write(`\x1b[${echoRows}A\x1b[K` + `\x1b[1m> ${inputLines[0]}${inputLines.length > 1 ? ` (+${inputLines.length - 1} lines)` : ""}\x1b[0m\n\n`);
517
- messages.push({ role: "user", content: input });
690
+ process.stdout.write((0, cli_layout_1.formatEchoedInputSummary)(input, cols));
518
691
  addHistory(history, input);
519
692
  currentAbort = new AbortController();
520
- const traceId = (0, nerves_1.createTraceId)();
521
693
  ctrl.suppress(() => currentAbort.abort());
522
694
  let result;
523
695
  try {
524
- result = await (0, core_1.runAgent)(messages, cliCallbacks, "cli", currentAbort.signal, {
525
- toolChoiceRequired: getEffectiveToolChoiceRequired(),
526
- traceId,
527
- tools: options.tools,
528
- execTool: wrappedExecTool,
529
- toolContext: options.toolContext,
530
- });
696
+ if (options.runTurn) {
697
+ // Pipeline-based turn: the runTurn callback handles user message assembly,
698
+ // pending drain, trust gate, runAgent, postTurn, and token accumulation.
699
+ result = await options.runTurn(messages, input, cliCallbacks, currentAbort.signal, effectiveToolContext);
700
+ }
701
+ else {
702
+ // Legacy path: inline runAgent (used by adoption specialist and tests)
703
+ const prefix = options.getContentPrefix?.();
704
+ messages.push({ role: "user", content: prefix ? `${prefix}\n\n${input}` : input });
705
+ const traceId = (0, nerves_1.createTraceId)();
706
+ result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
707
+ toolChoiceRequired: getEffectiveToolChoiceRequired(),
708
+ traceId,
709
+ tools: options.tools,
710
+ execTool: wrappedExecTool,
711
+ toolContext: effectiveToolContext,
712
+ });
713
+ }
531
714
  }
532
- catch {
533
- // AbortError -- silently return to prompt
715
+ catch (err) {
716
+ // AbortError (Ctrl-C) -- silently return to prompt
717
+ // All other errors: show the user what happened
718
+ if (!(err instanceof DOMException && err.name === "AbortError")) {
719
+ process.stderr.write(`\x1b[31m${err instanceof Error ? err.message : String(err)}\x1b[0m\n`);
720
+ }
534
721
  }
535
722
  cliCallbacks.flushMarkdown();
536
723
  ctrl.restore();
@@ -552,20 +739,28 @@ async function runCliSession(options) {
552
739
  }
553
740
  if (closed)
554
741
  break;
555
- process.stdout.write("\x1b[36m> \x1b[0m");
742
+ process.stdout.write(CLI_PROMPT);
556
743
  }
557
744
  }
558
745
  finally {
559
746
  rl.close();
560
- // eslint-disable-next-line no-console -- terminal UX: goodbye
561
- console.log("bye");
747
+ if (options.banner !== false) {
748
+ // eslint-disable-next-line no-console -- terminal UX: goodbye
749
+ console.log("bye");
750
+ }
562
751
  }
752
+ /* v8 ignore stop */
563
753
  return { exitReason, toolResult: exitToolResult };
564
754
  }
565
755
  async function main(agentName, options) {
566
756
  if (agentName)
567
757
  (0, identity_1.setAgentName)(agentName);
568
758
  const pasteDebounceMs = options?.pasteDebounceMs ?? 50;
759
+ // Register spinner hooks so log output clears the spinner before printing
760
+ (0, nerves_1.registerSpinnerHooks)(pauseActiveSpinner, resumeActiveSpinner);
761
+ // Fallback: apply pending updates for daemon-less direct CLI usage
762
+ (0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
763
+ await (0, update_hooks_1.applyPendingUpdates)((0, identity_1.getAgentBundlesRoot)(), (0, bundle_manifest_1.getPackageVersion)());
569
764
  // Fail fast if provider is misconfigured (triggers human-readable error + exit)
570
765
  (0, core_1.getProvider)();
571
766
  // Resolve context kernel (identity + channel) for CLI
@@ -581,13 +776,6 @@ async function main(agentName, options) {
581
776
  channel: "cli",
582
777
  });
583
778
  const resolvedContext = await resolver.resolve();
584
- const cliToolContext = {
585
- /* v8 ignore next -- CLI has no OAuth sign-in; this no-op satisfies the interface @preserve */
586
- signin: async () => undefined,
587
- context: resolvedContext,
588
- friendStore,
589
- summarize: (0, core_1.createSummarize)(),
590
- };
591
779
  const friendId = resolvedContext.friend.id;
592
780
  const agentConfig = (0, identity_1.loadAgentConfig)();
593
781
  (0, cli_logging_1.configureCliRuntimeLogger)(friendId, {
@@ -610,52 +798,107 @@ async function main(agentName, options) {
610
798
  }
611
799
  // Load existing session or start fresh
612
800
  const existing = (0, context_1.loadSession)(sessPath);
801
+ let sessionState = existing?.state;
802
+ const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
613
803
  const sessionMessages = existing?.messages && existing.messages.length > 0
614
804
  ? existing.messages
615
- : [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", undefined, resolvedContext) }];
616
- // Pending queue drain: inject pending messages as harness-context + assistant-content pairs
617
- const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "cli", "session");
618
- const drainToMessages = () => {
619
- const pending = (0, pending_1.drainPending)(pendingDir);
620
- if (pending.length === 0)
621
- return 0;
622
- for (const msg of pending) {
623
- sessionMessages.push({ role: "user", name: "harness", content: `[proactive message from ${msg.from}]` });
624
- sessionMessages.push({ role: "assistant", content: msg.content });
625
- }
626
- return pending.length;
627
- };
628
- // Startup drain: deliver offline messages
629
- const startupCount = drainToMessages();
630
- if (startupCount > 0) {
631
- (0, context_1.saveSession)(sessPath, sessionMessages);
632
- }
805
+ : [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", { mcpManager }, resolvedContext) }];
806
+ // Per-turn pipeline input: CLI capabilities and pending dir
807
+ const cliCapabilities = (0, channel_1.getChannelCapabilities)("cli");
808
+ const currentAgentName = (0, identity_1.getAgentName)();
809
+ const pendingDir = (0, pending_1.getPendingDir)(currentAgentName, friendId, "cli", "session");
810
+ const summarize = (0, core_1.createSummarize)();
811
+ const cliFailoverState = { pending: null };
633
812
  try {
634
813
  await runCliSession({
635
- agentName: (0, identity_1.getAgentName)(),
814
+ agentName: currentAgentName,
636
815
  pasteDebounceMs,
637
816
  messages: sessionMessages,
638
- toolContext: cliToolContext,
639
- onInput: () => {
640
- const trustGate = (0, trust_gate_1.enforceTrustGate)({
641
- friend: resolvedContext.friend,
817
+ onAsyncAssistantMessage: async (messages, _assistantMessage) => {
818
+ (0, context_1.postTurn)(messages, sessPath, undefined, undefined, sessionState);
819
+ },
820
+ runTurn: async (messages, userInput, callbacks, signal, toolContext) => {
821
+ // Run the full per-turn pipeline: resolve -> gate -> session -> drain -> runAgent -> postTurn -> tokens
822
+ // User message passed via input.messages so the pipeline can prepend pending messages to it.
823
+ const failoverState = cliFailoverState;
824
+ // Capture terminal errors instead of displaying immediately — the failover
825
+ // message replaces the raw error if failover triggers successfully.
826
+ let capturedTerminalError = null;
827
+ /* v8 ignore start -- failover-aware callback wrapper: tested via pipeline integration @preserve */
828
+ const failoverAwareCallbacks = {
829
+ ...callbacks,
830
+ onError: (error, severity) => {
831
+ if (severity === "terminal" && failoverState) {
832
+ capturedTerminalError = error;
833
+ callbacks.onError(new Error(""), "transient");
834
+ return;
835
+ }
836
+ callbacks.onError(error, severity);
837
+ },
838
+ };
839
+ /* v8 ignore stop */
840
+ const result = await (0, pipeline_1.handleInboundTurn)({
841
+ channel: "cli",
842
+ sessionKey: "session",
843
+ capabilities: cliCapabilities,
844
+ messages: [{ role: "user", content: userInput }],
845
+ continuityIngressTexts: getCliContinuityIngressTexts(userInput),
846
+ callbacks: failoverAwareCallbacks,
847
+ friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
848
+ sessionLoader: {
849
+ loadOrCreate: () => Promise.resolve({
850
+ messages,
851
+ sessionPath: sessPath,
852
+ state: sessionState,
853
+ }),
854
+ },
855
+ pendingDir,
856
+ friendStore,
642
857
  provider: "local",
643
858
  externalId: localExternalId,
644
- channel: "cli",
859
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
860
+ drainPending: pending_1.drainPending,
861
+ drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(currentAgentName, deferredFriendId),
862
+ runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
863
+ ...opts,
864
+ toolContext: {
865
+ /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
866
+ signin: async () => undefined,
867
+ ...opts?.toolContext,
868
+ summarize,
869
+ },
870
+ }),
871
+ postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
872
+ (0, context_1.postTurn)(turnMessages, sessionPathArg, usage, hooks, state);
873
+ sessionState = state;
874
+ },
875
+ accumulateFriendTokens: tokens_1.accumulateFriendTokens,
876
+ signal,
877
+ runAgentOptions: {
878
+ toolChoiceRequired: (0, commands_1.getToolChoiceRequired)(),
879
+ traceId: (0, nerves_1.createTraceId)(),
880
+ mcpManager,
881
+ toolContext,
882
+ },
883
+ failoverState,
645
884
  });
646
- if (!trustGate.allowed) {
647
- return {
648
- allowed: false,
649
- reply: trustGate.reason === "stranger_first_reply" ? trustGate.autoReply : undefined,
650
- };
885
+ /* v8 ignore start -- failover display: tested via pipeline integration tests @preserve */
886
+ if (result.failoverMessage) {
887
+ // Failover handled it — show the actionable message instead of the raw error
888
+ process.stdout.write(`\x1b[33m${result.failoverMessage}\x1b[0m\n`);
651
889
  }
652
- return { allowed: true };
653
- },
654
- onTurnEnd: async (msgs, result) => {
655
- (0, context_1.postTurn)(msgs, sessPath, result.usage);
656
- await (0, tokens_1.accumulateFriendTokens)(friendStore, resolvedContext.friend.id, result.usage);
657
- drainToMessages();
658
- await (0, prompt_refresh_1.refreshSystemPrompt)(msgs, "cli", undefined, resolvedContext);
890
+ else if (capturedTerminalError) {
891
+ // Failover didn't trigger (no failoverState, or sequence failed) — show the raw error
892
+ process.stderr.write(`\x1b[31m${(0, format_1.formatError)(capturedTerminalError)}\x1b[0m\n`);
893
+ }
894
+ /* v8 ignore stop */
895
+ // Handle gate rejection: display auto-reply if present
896
+ if (!result.gateResult.allowed) {
897
+ if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
898
+ process.stdout.write(`${result.gateResult.autoReply}\n`);
899
+ }
900
+ }
901
+ return { usage: result.usage };
659
902
  },
660
903
  onNewSession: () => {
661
904
  (0, context_1.deleteSession)(sessPath);