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

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 (56) 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 +507 -29
  8. package/dist/heart/daemon/daemon-entry.js +13 -5
  9. package/dist/heart/daemon/daemon.js +42 -9
  10. package/dist/heart/daemon/hatch-animation.js +35 -0
  11. package/dist/heart/daemon/hatch-flow.js +2 -11
  12. package/dist/heart/daemon/hatch-specialist.js +6 -1
  13. package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
  14. package/dist/heart/daemon/ouro-path-installer.js +177 -0
  15. package/dist/heart/daemon/ouro-uti.js +11 -2
  16. package/dist/heart/daemon/process-manager.js +1 -1
  17. package/dist/heart/daemon/runtime-logging.js +9 -5
  18. package/dist/heart/daemon/runtime-metadata.js +118 -0
  19. package/dist/heart/daemon/sense-manager.js +266 -0
  20. package/dist/heart/daemon/specialist-orchestrator.js +129 -0
  21. package/dist/heart/daemon/specialist-prompt.js +98 -0
  22. package/dist/heart/daemon/specialist-tools.js +237 -0
  23. package/dist/heart/daemon/subagent-installer.js +10 -1
  24. package/dist/heart/identity.js +77 -1
  25. package/dist/heart/providers/anthropic.js +19 -2
  26. package/dist/heart/sense-truth.js +61 -0
  27. package/dist/heart/streaming.js +99 -21
  28. package/dist/mind/bundle-manifest.js +58 -0
  29. package/dist/mind/friends/channel.js +8 -0
  30. package/dist/mind/friends/types.js +1 -1
  31. package/dist/mind/prompt.js +77 -3
  32. package/dist/nerves/cli-logging.js +15 -2
  33. package/dist/repertoire/ado-client.js +4 -2
  34. package/dist/repertoire/coding/feedback.js +134 -0
  35. package/dist/repertoire/coding/index.js +4 -1
  36. package/dist/repertoire/coding/manager.js +61 -2
  37. package/dist/repertoire/coding/spawner.js +3 -3
  38. package/dist/repertoire/coding/tools.js +41 -2
  39. package/dist/repertoire/data/ado-endpoints.json +188 -0
  40. package/dist/repertoire/tools-base.js +69 -5
  41. package/dist/repertoire/tools-teams.js +57 -4
  42. package/dist/repertoire/tools.js +44 -11
  43. package/dist/senses/bluebubbles-client.js +433 -0
  44. package/dist/senses/bluebubbles-entry.js +11 -0
  45. package/dist/senses/bluebubbles-media.js +244 -0
  46. package/dist/senses/bluebubbles-model.js +253 -0
  47. package/dist/senses/bluebubbles-mutation-log.js +76 -0
  48. package/dist/senses/bluebubbles.js +421 -0
  49. package/dist/senses/cli.js +293 -133
  50. package/dist/senses/debug-activity.js +107 -0
  51. package/dist/senses/teams.js +173 -54
  52. package/package.json +11 -4
  53. package/subagents/work-doer.md +26 -24
  54. package/subagents/work-merger.md +24 -30
  55. package/subagents/work-planner.md +34 -25
  56. package/dist/inner-worker-entry.js +0 -4
@@ -38,6 +38,8 @@ exports.handleSigint = handleSigint;
38
38
  exports.addHistory = addHistory;
39
39
  exports.renderMarkdown = renderMarkdown;
40
40
  exports.createCliCallbacks = createCliCallbacks;
41
+ exports.createDebouncedLines = createDebouncedLines;
42
+ exports.runCliSession = runCliSession;
41
43
  exports.main = main;
42
44
  const readline = __importStar(require("readline"));
43
45
  const os = __importStar(require("os"));
@@ -70,12 +72,14 @@ class Spinner {
70
72
  msg = "";
71
73
  phrases = null;
72
74
  lastPhrase = "";
75
+ stopped = false;
73
76
  constructor(m = "working", phrases) {
74
77
  this.msg = m;
75
78
  if (phrases && phrases.length > 0)
76
79
  this.phrases = phrases;
77
80
  }
78
81
  start() {
82
+ this.stopped = false;
79
83
  process.stderr.write("\r\x1b[K");
80
84
  this.spin();
81
85
  this.iv = setInterval(() => this.spin(), 80);
@@ -84,15 +88,23 @@ class Spinner {
84
88
  }
85
89
  }
86
90
  spin() {
87
- process.stderr.write(`\r${this.frames[this.i]} ${this.msg}... `);
91
+ // Guard: clearInterval can't prevent already-dequeued callbacks
92
+ /* v8 ignore next -- race guard: timer callback fires after stop() @preserve */
93
+ if (this.stopped)
94
+ return;
95
+ process.stderr.write(`\r\x1b[K${this.frames[this.i]} ${this.msg}... `);
88
96
  this.i = (this.i + 1) % this.frames.length;
89
97
  }
90
98
  rotatePhrase() {
99
+ /* v8 ignore next -- race guard: timer callback fires after stop() @preserve */
100
+ if (this.stopped)
101
+ return;
91
102
  const next = (0, phrases_1.pickPhrase)(this.phrases, this.lastPhrase);
92
103
  this.lastPhrase = next;
93
104
  this.msg = next;
94
105
  }
95
106
  stop(ok) {
107
+ this.stopped = true;
96
108
  if (this.iv) {
97
109
  clearInterval(this.iv);
98
110
  this.iv = null;
@@ -297,12 +309,25 @@ function createCliCallbacks() {
297
309
  currentSpinner.start();
298
310
  },
299
311
  onModelStreamStart: () => {
300
- currentSpinner?.stop();
301
- currentSpinner = null;
312
+ // No-op: content callbacks (onTextChunk, onReasoningChunk) handle
313
+ // stopping the spinner. onModelStreamStart fires too early and
314
+ // doesn't fire at all for final_answer tool streaming.
315
+ },
316
+ onClearText: () => {
317
+ streamer.reset();
302
318
  },
303
319
  onTextChunk: (text) => {
320
+ // Stop spinner if still running — final_answer streaming and Anthropic
321
+ // tool-only responses bypass onModelStreamStart, so the spinner would
322
+ // otherwise keep running (and its \r writes overwrite response text).
323
+ if (currentSpinner) {
324
+ currentSpinner.stop();
325
+ currentSpinner = null;
326
+ }
304
327
  if (hadReasoning) {
305
- process.stdout.write("\n\n");
328
+ // Single newline to separate reasoning from reply — reasoning
329
+ // output often ends with its own trailing newline(s)
330
+ process.stdout.write("\n");
306
331
  hadReasoning = false;
307
332
  }
308
333
  const rendered = streamer.push(text);
@@ -311,6 +336,10 @@ function createCliCallbacks() {
311
336
  textDirty = text.length > 0 && !text.endsWith("\n");
312
337
  },
313
338
  onReasoningChunk: (text) => {
339
+ if (currentSpinner) {
340
+ currentSpinner.stop();
341
+ currentSpinner = null;
342
+ }
314
343
  hadReasoning = true;
315
344
  process.stdout.write(`\x1b[2m${text}\x1b[0m`);
316
345
  textDirty = text.length > 0 && !text.endsWith("\n");
@@ -368,88 +397,86 @@ function createCliCallbacks() {
368
397
  },
369
398
  };
370
399
  }
371
- async function main(agentName, options) {
372
- if (agentName)
373
- (0, identity_1.setAgentName)(agentName);
374
- const pasteDebounceMs = options?.pasteDebounceMs ?? 50;
375
- // Fail fast if provider is misconfigured (triggers human-readable error + exit)
376
- (0, core_1.getProvider)();
377
- const registry = (0, commands_1.createCommandRegistry)();
378
- (0, commands_1.registerDefaultCommands)(registry);
379
- // Resolve context kernel (identity + channel) for CLI
380
- const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
381
- const friendStore = new store_file_1.FileFriendStore(friendsPath);
382
- const username = os.userInfo().username;
383
- const hostname = os.hostname();
384
- const localExternalId = `${username}@${hostname}`;
385
- const resolver = new resolver_1.FriendResolver(friendStore, {
386
- provider: "local",
387
- externalId: localExternalId,
388
- displayName: username,
389
- channel: "cli",
390
- });
391
- const resolvedContext = await resolver.resolve();
392
- const cliToolContext = {
393
- /* v8 ignore next -- CLI has no OAuth sign-in; this no-op satisfies the interface @preserve */
394
- signin: async () => undefined,
395
- context: resolvedContext,
396
- friendStore,
397
- summarize: (0, core_1.createSummarize)(),
398
- };
399
- const friendId = resolvedContext.friend.id;
400
- const agentConfig = (0, identity_1.loadAgentConfig)();
401
- (0, cli_logging_1.configureCliRuntimeLogger)(friendId, {
402
- level: agentConfig.logging?.level,
403
- sinks: agentConfig.logging?.sinks,
404
- });
405
- const sessPath = (0, config_1.sessionPath)(friendId, "cli", "session");
406
- let sessionLock = null;
407
- try {
408
- sessionLock = (0, session_lock_1.acquireSessionLock)(`${sessPath}.lock`, (0, identity_1.getAgentName)());
400
+ // Debounced line iterator: collects rapid-fire lines (paste) into a single input.
401
+ // When the debounce timeout wins the race, the pending iter.next() is saved
402
+ // and reused in the next iteration to prevent it from silently consuming input.
403
+ async function* createDebouncedLines(source, debounceMs) {
404
+ if (debounceMs <= 0) {
405
+ yield* source;
406
+ return;
409
407
  }
410
- catch (error) {
411
- /* v8 ignore start -- integration: main() is interactive, lock tested in session-lock.test.ts @preserve */
412
- if (error instanceof session_lock_1.SessionLockError) {
413
- process.stderr.write(`${error.message}\n`);
414
- return;
408
+ const iter = source[Symbol.asyncIterator]();
409
+ let pending = null;
410
+ while (true) {
411
+ const first = pending ? await pending : await iter.next();
412
+ pending = null;
413
+ if (first.done)
414
+ break;
415
+ const lines = [first.value];
416
+ let more = true;
417
+ while (more) {
418
+ const nextPromise = iter.next();
419
+ const raced = await Promise.race([
420
+ nextPromise.then((r) => ({ kind: "line", result: r })),
421
+ new Promise((r) => setTimeout(() => r({ kind: "timeout" }), debounceMs)),
422
+ ]);
423
+ if (raced.kind === "timeout") {
424
+ pending = nextPromise;
425
+ more = false;
426
+ }
427
+ else if (raced.result.done) {
428
+ more = false;
429
+ }
430
+ else {
431
+ lines.push(raced.result.value);
432
+ }
415
433
  }
416
- throw error;
417
- /* v8 ignore stop */
434
+ yield lines.join("\n");
418
435
  }
419
- // Load existing session or start fresh
420
- const existing = (0, context_1.loadSession)(sessPath);
421
- const messages = existing?.messages && existing.messages.length > 0
422
- ? existing.messages
423
- : [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", undefined, resolvedContext) }];
424
- // Pending queue drain: inject pending messages as harness-context + assistant-content pairs
425
- const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "cli", "session");
426
- const drainToMessages = () => {
427
- const pending = (0, pending_1.drainPending)(pendingDir);
428
- if (pending.length === 0)
429
- return 0;
430
- for (const msg of pending) {
431
- messages.push({ role: "user", name: "harness", content: `[proactive message from ${msg.from}]` });
432
- messages.push({ role: "assistant", content: msg.content });
433
- }
434
- return pending.length;
435
- };
436
- // Startup drain: deliver offline messages
437
- const startupCount = drainToMessages();
438
- if (startupCount > 0) {
439
- (0, context_1.saveSession)(sessPath, messages);
436
+ }
437
+ async function runCliSession(options) {
438
+ /* v8 ignore start -- integration: runCliSession is interactive, tested via E2E @preserve */
439
+ const pasteDebounceMs = options.pasteDebounceMs ?? 50;
440
+ const registry = (0, commands_1.createCommandRegistry)();
441
+ if (!options.disableCommands) {
442
+ (0, commands_1.registerDefaultCommands)(registry);
440
443
  }
444
+ const messages = options.messages
445
+ ?? [{ role: "system", content: await (0, prompt_1.buildSystem)("cli") }];
441
446
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
442
447
  const ctrl = new InputController(rl);
443
448
  let currentAbort = null;
444
449
  const history = [];
445
450
  let closed = false;
446
451
  rl.on("close", () => { closed = true; });
447
- // eslint-disable-next-line no-console -- terminal UX: startup banner
448
- console.log(`\n${(0, identity_1.getAgentName)()} (type /commands for help)\n`);
452
+ if (options.banner !== false) {
453
+ const bannerText = typeof options.banner === "string"
454
+ ? options.banner
455
+ : `${options.agentName} (type /commands for help)`;
456
+ // eslint-disable-next-line no-console -- terminal UX: startup banner
457
+ console.log(`\n${bannerText}\n`);
458
+ }
449
459
  const cliCallbacks = createCliCallbacks();
450
- process.stdout.write("\x1b[36m> \x1b[0m");
460
+ // exitOnToolCall machinery: wrap execTool to detect target tool
461
+ let exitToolResult;
462
+ let exitToolFired = false;
463
+ const resolvedExecTool = options.execTool;
464
+ const wrappedExecTool = options.exitOnToolCall && resolvedExecTool
465
+ ? async (name, args, ctx) => {
466
+ const result = await resolvedExecTool(name, args, ctx);
467
+ if (name === options.exitOnToolCall) {
468
+ exitToolResult = result;
469
+ exitToolFired = true;
470
+ // Abort immediately so the model doesn't generate more output
471
+ // (e.g. reasoning about calling final_answer after complete_adoption)
472
+ currentAbort?.abort();
473
+ }
474
+ return result;
475
+ }
476
+ : resolvedExecTool;
477
+ // Resolve toolChoiceRequired: use explicit option if set, else fall back to toggle
478
+ const getEffectiveToolChoiceRequired = () => options.toolChoiceRequired !== undefined ? options.toolChoiceRequired : (0, commands_1.getToolChoiceRequired)();
451
479
  // Ctrl-C at the input prompt: clear line or warn/exit
452
- // readline with terminal:true catches Ctrl-C in raw mode (no ^C echo)
453
480
  rl.on("SIGINT", () => {
454
481
  const rlInt = rl;
455
482
  const currentLine = rlInt.line || "";
@@ -470,38 +497,58 @@ async function main(agentName, options) {
470
497
  rl.close();
471
498
  }
472
499
  });
473
- // Debounced line iterator: collects rapid-fire lines (paste) into a single input
474
- async function* debouncedLines(source) {
475
- if (pasteDebounceMs <= 0) {
476
- yield* source;
477
- return;
500
+ const debouncedLines = (source) => createDebouncedLines(source, pasteDebounceMs);
501
+ (0, runtime_1.emitNervesEvent)({
502
+ component: "senses",
503
+ event: "senses.cli_session_start",
504
+ message: "runCliSession started",
505
+ meta: { agentName: options.agentName, hasExitOnToolCall: !!options.exitOnToolCall },
506
+ });
507
+ let exitReason = "user_quit";
508
+ // Auto-first-turn: process the last user message immediately so the agent
509
+ // speaks first (e.g. specialist greeting). Only triggers when explicitly opted in.
510
+ if (options.autoFirstTurn && messages.length > 0 && messages[messages.length - 1]?.role === "user") {
511
+ currentAbort = new AbortController();
512
+ const traceId = (0, nerves_1.createTraceId)();
513
+ ctrl.suppress(() => currentAbort.abort());
514
+ let result;
515
+ try {
516
+ result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
517
+ toolChoiceRequired: getEffectiveToolChoiceRequired(),
518
+ traceId,
519
+ tools: options.tools,
520
+ execTool: wrappedExecTool,
521
+ toolContext: options.toolContext,
522
+ });
478
523
  }
479
- const iter = source[Symbol.asyncIterator]();
480
- while (true) {
481
- const first = await iter.next();
482
- if (first.done)
483
- break;
484
- // Collect any lines that arrive within the debounce window (paste detection)
485
- const lines = [first.value];
486
- let more = true;
487
- while (more) {
488
- const raced = await Promise.race([
489
- iter.next().then((r) => ({ kind: "line", result: r })),
490
- new Promise((r) => setTimeout(() => r({ kind: "timeout" }), pasteDebounceMs)),
491
- ]);
492
- if (raced.kind === "timeout") {
493
- more = false;
494
- }
495
- else if (raced.result.done) {
496
- more = false;
497
- }
498
- else {
499
- lines.push(raced.result.value);
500
- }
524
+ catch (err) {
525
+ // AbortError (Ctrl-C) -- silently continue to prompt
526
+ // All other errors: show the user what happened
527
+ if (!(err instanceof DOMException && err.name === "AbortError")) {
528
+ process.stderr.write(`\x1b[31m${err instanceof Error ? err.message : String(err)}\x1b[0m\n`);
529
+ }
530
+ }
531
+ cliCallbacks.flushMarkdown();
532
+ ctrl.restore();
533
+ currentAbort = null;
534
+ if (exitToolFired) {
535
+ exitReason = "tool_exit";
536
+ rl.close();
537
+ }
538
+ else {
539
+ const lastMsg = messages[messages.length - 1];
540
+ if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
541
+ process.stderr.write("\x1b[33m(empty response)\x1b[0m\n");
542
+ }
543
+ process.stdout.write("\n\n");
544
+ if (options.onTurnEnd) {
545
+ await options.onTurnEnd(messages, result ?? { usage: undefined });
501
546
  }
502
- yield lines.join("\n");
503
547
  }
504
548
  }
549
+ if (!exitToolFired) {
550
+ process.stdout.write("\x1b[36m> \x1b[0m");
551
+ }
505
552
  try {
506
553
  for await (const input of debouncedLines(rl)) {
507
554
  if (closed)
@@ -510,20 +557,18 @@ async function main(agentName, options) {
510
557
  process.stdout.write("\x1b[36m> \x1b[0m");
511
558
  continue;
512
559
  }
513
- const trustGate = (0, trust_gate_1.enforceTrustGate)({
514
- friend: resolvedContext.friend,
515
- provider: "local",
516
- externalId: localExternalId,
517
- channel: "cli",
518
- });
519
- if (!trustGate.allowed) {
520
- if (trustGate.reason === "stranger_first_reply") {
521
- process.stdout.write(`${trustGate.autoReply}\n`);
560
+ // Optional input gate (e.g. trust gate in main)
561
+ if (options.onInput) {
562
+ const gate = options.onInput(input);
563
+ if (!gate.allowed) {
564
+ if (gate.reply) {
565
+ process.stdout.write(`${gate.reply}\n`);
566
+ }
567
+ if (closed)
568
+ break;
569
+ process.stdout.write("\x1b[36m> \x1b[0m");
570
+ continue;
522
571
  }
523
- if (closed)
524
- break;
525
- process.stdout.write("\x1b[36m> \x1b[0m");
526
- continue;
527
572
  }
528
573
  // Check for slash commands
529
574
  const parsed = (0, commands_1.parseSlashCommand)(input);
@@ -536,7 +581,7 @@ async function main(agentName, options) {
536
581
  else if (dispatchResult.result.action === "new") {
537
582
  messages.length = 0;
538
583
  messages.push({ role: "system", content: await (0, prompt_1.buildSystem)("cli") });
539
- (0, context_1.deleteSession)(sessPath);
584
+ await options.onNewSession?.();
540
585
  // eslint-disable-next-line no-console -- terminal UX: session cleared
541
586
  console.log("session cleared");
542
587
  process.stdout.write("\x1b[36m> \x1b[0m");
@@ -550,13 +595,12 @@ async function main(agentName, options) {
550
595
  }
551
596
  }
552
597
  }
553
- // Re-style the echoed input lines (readline terminal:true echoes each line)
554
- // For multiline paste, each line was echoed separately — erase them all
598
+ // Re-style the echoed input lines
555
599
  const cols = process.stdout.columns || 80;
556
600
  const inputLines = input.split("\n");
557
601
  let echoRows = 0;
558
602
  for (const line of inputLines) {
559
- echoRows += Math.ceil((2 + line.length) / cols); // "> " prefix + line content
603
+ echoRows += Math.ceil((2 + line.length) / cols);
560
604
  }
561
605
  process.stdout.write(`\x1b[${echoRows}A\x1b[K` + `\x1b[1m> ${inputLines[0]}${inputLines.length > 1 ? ` (+${inputLines.length - 1} lines)` : ""}\x1b[0m\n\n`);
562
606
  messages.push({ role: "user", content: input });
@@ -566,39 +610,155 @@ async function main(agentName, options) {
566
610
  ctrl.suppress(() => currentAbort.abort());
567
611
  let result;
568
612
  try {
569
- result = await (0, core_1.runAgent)(messages, cliCallbacks, "cli", currentAbort.signal, {
570
- toolChoiceRequired: (0, commands_1.getToolChoiceRequired)(),
571
- toolContext: cliToolContext,
613
+ result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
614
+ toolChoiceRequired: getEffectiveToolChoiceRequired(),
572
615
  traceId,
616
+ tools: options.tools,
617
+ execTool: wrappedExecTool,
618
+ toolContext: options.toolContext,
573
619
  });
574
620
  }
575
- catch {
576
- // AbortError silently return to prompt
621
+ catch (err) {
622
+ // AbortError (Ctrl-C) -- silently return to prompt
623
+ // All other errors: show the user what happened
624
+ if (!(err instanceof DOMException && err.name === "AbortError")) {
625
+ process.stderr.write(`\x1b[31m${err instanceof Error ? err.message : String(err)}\x1b[0m\n`);
626
+ }
577
627
  }
578
628
  cliCallbacks.flushMarkdown();
579
629
  ctrl.restore();
580
630
  currentAbort = null;
631
+ // Check if exit tool was fired during this turn
632
+ if (exitToolFired) {
633
+ exitReason = "tool_exit";
634
+ break;
635
+ }
581
636
  // Safety net: never silently swallow an empty response
582
637
  const lastMsg = messages[messages.length - 1];
583
638
  if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
584
639
  process.stderr.write("\x1b[33m(empty response)\x1b[0m\n");
585
640
  }
586
641
  process.stdout.write("\n\n");
587
- (0, context_1.postTurn)(messages, sessPath, result?.usage);
588
- await (0, tokens_1.accumulateFriendTokens)(friendStore, resolvedContext.friend.id, result?.usage);
589
- // Post-turn: drain any pending messages that arrived during runAgent
590
- drainToMessages();
591
- // Post-turn: refresh system prompt so active sessions metadata is current
592
- await (0, prompt_refresh_1.refreshSystemPrompt)(messages, "cli", undefined, resolvedContext);
642
+ // Post-turn hook (session persistence, pending drain, prompt refresh, etc.)
643
+ if (options.onTurnEnd) {
644
+ await options.onTurnEnd(messages, result ?? { usage: undefined });
645
+ }
593
646
  if (closed)
594
647
  break;
595
648
  process.stdout.write("\x1b[36m> \x1b[0m");
596
649
  }
597
650
  }
598
651
  finally {
599
- sessionLock?.release();
600
652
  rl.close();
601
- // eslint-disable-next-line no-console -- terminal UX: goodbye
602
- console.log("bye");
653
+ if (options.banner !== false) {
654
+ // eslint-disable-next-line no-console -- terminal UX: goodbye
655
+ console.log("bye");
656
+ }
657
+ }
658
+ /* v8 ignore stop */
659
+ return { exitReason, toolResult: exitToolResult };
660
+ }
661
+ async function main(agentName, options) {
662
+ if (agentName)
663
+ (0, identity_1.setAgentName)(agentName);
664
+ const pasteDebounceMs = options?.pasteDebounceMs ?? 50;
665
+ // Fail fast if provider is misconfigured (triggers human-readable error + exit)
666
+ (0, core_1.getProvider)();
667
+ // Resolve context kernel (identity + channel) for CLI
668
+ const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
669
+ const friendStore = new store_file_1.FileFriendStore(friendsPath);
670
+ const username = os.userInfo().username;
671
+ const hostname = os.hostname();
672
+ const localExternalId = `${username}@${hostname}`;
673
+ const resolver = new resolver_1.FriendResolver(friendStore, {
674
+ provider: "local",
675
+ externalId: localExternalId,
676
+ displayName: username,
677
+ channel: "cli",
678
+ });
679
+ const resolvedContext = await resolver.resolve();
680
+ const cliToolContext = {
681
+ /* v8 ignore next -- CLI has no OAuth sign-in; this no-op satisfies the interface @preserve */
682
+ signin: async () => undefined,
683
+ context: resolvedContext,
684
+ friendStore,
685
+ summarize: (0, core_1.createSummarize)(),
686
+ };
687
+ const friendId = resolvedContext.friend.id;
688
+ const agentConfig = (0, identity_1.loadAgentConfig)();
689
+ (0, cli_logging_1.configureCliRuntimeLogger)(friendId, {
690
+ level: agentConfig.logging?.level,
691
+ sinks: agentConfig.logging?.sinks,
692
+ });
693
+ const sessPath = (0, config_1.sessionPath)(friendId, "cli", "session");
694
+ let sessionLock = null;
695
+ try {
696
+ sessionLock = (0, session_lock_1.acquireSessionLock)(`${sessPath}.lock`, (0, identity_1.getAgentName)());
697
+ }
698
+ catch (error) {
699
+ /* v8 ignore start -- integration: main() is interactive, lock tested in session-lock.test.ts @preserve */
700
+ if (error instanceof session_lock_1.SessionLockError) {
701
+ process.stderr.write(`${error.message}\n`);
702
+ return;
703
+ }
704
+ throw error;
705
+ /* v8 ignore stop */
706
+ }
707
+ // Load existing session or start fresh
708
+ const existing = (0, context_1.loadSession)(sessPath);
709
+ const sessionMessages = existing?.messages && existing.messages.length > 0
710
+ ? existing.messages
711
+ : [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", undefined, resolvedContext) }];
712
+ // Pending queue drain: inject pending messages as harness-context + assistant-content pairs
713
+ const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "cli", "session");
714
+ const drainToMessages = () => {
715
+ const pending = (0, pending_1.drainPending)(pendingDir);
716
+ if (pending.length === 0)
717
+ return 0;
718
+ for (const msg of pending) {
719
+ sessionMessages.push({ role: "user", name: "harness", content: `[proactive message from ${msg.from}]` });
720
+ sessionMessages.push({ role: "assistant", content: msg.content });
721
+ }
722
+ return pending.length;
723
+ };
724
+ // Startup drain: deliver offline messages
725
+ const startupCount = drainToMessages();
726
+ if (startupCount > 0) {
727
+ (0, context_1.saveSession)(sessPath, sessionMessages);
728
+ }
729
+ try {
730
+ await runCliSession({
731
+ agentName: (0, identity_1.getAgentName)(),
732
+ pasteDebounceMs,
733
+ messages: sessionMessages,
734
+ toolContext: cliToolContext,
735
+ onInput: () => {
736
+ const trustGate = (0, trust_gate_1.enforceTrustGate)({
737
+ friend: resolvedContext.friend,
738
+ provider: "local",
739
+ externalId: localExternalId,
740
+ channel: "cli",
741
+ });
742
+ if (!trustGate.allowed) {
743
+ return {
744
+ allowed: false,
745
+ reply: trustGate.reason === "stranger_first_reply" ? trustGate.autoReply : undefined,
746
+ };
747
+ }
748
+ return { allowed: true };
749
+ },
750
+ onTurnEnd: async (msgs, result) => {
751
+ (0, context_1.postTurn)(msgs, sessPath, result.usage);
752
+ await (0, tokens_1.accumulateFriendTokens)(friendStore, resolvedContext.friend.id, result.usage);
753
+ drainToMessages();
754
+ await (0, prompt_refresh_1.refreshSystemPrompt)(msgs, "cli", undefined, resolvedContext);
755
+ },
756
+ onNewSession: () => {
757
+ (0, context_1.deleteSession)(sessPath);
758
+ },
759
+ });
760
+ }
761
+ finally {
762
+ sessionLock?.release();
603
763
  }
604
764
  }
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDebugActivityController = createDebugActivityController;
4
+ const format_1 = require("../mind/format");
5
+ const phrases_1 = require("../mind/phrases");
6
+ const runtime_1 = require("../nerves/runtime");
7
+ function createDebugActivityController(options) {
8
+ let queue = Promise.resolve();
9
+ let statusMessageGuid;
10
+ let typingActive = false;
11
+ let hadToolRun = false;
12
+ let followupShown = false;
13
+ let lastPhrase = "";
14
+ function reportTransportError(operation, error) {
15
+ (0, runtime_1.emitNervesEvent)({
16
+ level: "warn",
17
+ component: "senses",
18
+ event: "senses.debug_activity_transport_error",
19
+ message: "debug activity transport failed",
20
+ meta: {
21
+ operation,
22
+ reason: error instanceof Error ? error.message : String(error),
23
+ },
24
+ });
25
+ options.onTransportError?.(operation, error);
26
+ }
27
+ function enqueue(operation, task) {
28
+ queue = queue
29
+ .then(task)
30
+ .catch((error) => {
31
+ reportTransportError(operation, error);
32
+ });
33
+ }
34
+ function nextPhrase(pool) {
35
+ const phrase = (0, phrases_1.pickPhrase)(pool, lastPhrase);
36
+ lastPhrase = phrase;
37
+ return phrase;
38
+ }
39
+ function ensureTyping(active) {
40
+ if (typingActive === active) {
41
+ return;
42
+ }
43
+ typingActive = active;
44
+ enqueue(active ? "typing_start" : "typing_stop", async () => {
45
+ await options.transport.setTyping(active);
46
+ });
47
+ }
48
+ function setStatus(text) {
49
+ (0, runtime_1.emitNervesEvent)({
50
+ component: "senses",
51
+ event: "senses.debug_activity_update",
52
+ message: "debug activity status updated",
53
+ meta: {
54
+ hasStatusGuid: Boolean(statusMessageGuid),
55
+ textLength: text.length,
56
+ },
57
+ });
58
+ ensureTyping(true);
59
+ enqueue("status_update", async () => {
60
+ if (statusMessageGuid) {
61
+ await options.transport.editStatus(statusMessageGuid, text);
62
+ return;
63
+ }
64
+ statusMessageGuid = await options.transport.sendStatus(text);
65
+ });
66
+ }
67
+ return {
68
+ onModelStart() {
69
+ const pool = hadToolRun ? options.followupPhrases : options.thinkingPhrases;
70
+ setStatus(`${nextPhrase(pool)}...`);
71
+ },
72
+ onToolStart(name, args) {
73
+ hadToolRun = true;
74
+ followupShown = false;
75
+ const argSummary = Object.values(args).join(", ");
76
+ const detail = argSummary ? ` (${argSummary})` : "";
77
+ setStatus(`running ${name}${detail}...`);
78
+ },
79
+ onToolEnd(name, summary, success) {
80
+ hadToolRun = true;
81
+ followupShown = false;
82
+ setStatus((0, format_1.formatToolResult)(name, summary, success));
83
+ },
84
+ onTextChunk(text) {
85
+ if (!text || !hadToolRun || followupShown) {
86
+ return;
87
+ }
88
+ followupShown = true;
89
+ setStatus(`${nextPhrase(options.followupPhrases)}...`);
90
+ },
91
+ onError(error) {
92
+ setStatus((0, format_1.formatError)(error));
93
+ this.finish();
94
+ },
95
+ async drain() {
96
+ await queue;
97
+ },
98
+ async finish() {
99
+ if (!typingActive) {
100
+ await queue;
101
+ return;
102
+ }
103
+ ensureTyping(false);
104
+ await queue;
105
+ },
106
+ };
107
+ }