@ouro.bot/cli 0.1.0-alpha.3 → 0.1.0-alpha.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +70 -9
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
  3. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  4. package/AdoptionSpecialist.ouro/psyche/identities/python.md +30 -0
  5. package/assets/ouroboros.png +0 -0
  6. package/changelog.json +80 -0
  7. package/dist/heart/config.js +66 -4
  8. package/dist/heart/core.js +75 -2
  9. package/dist/heart/daemon/agent-discovery.js +81 -0
  10. package/dist/heart/daemon/daemon-cli.js +562 -64
  11. package/dist/heart/daemon/daemon-entry.js +14 -5
  12. package/dist/heart/daemon/daemon-runtime-sync.js +90 -0
  13. package/dist/heart/daemon/daemon.js +87 -9
  14. package/dist/heart/daemon/hatch-animation.js +35 -0
  15. package/dist/heart/daemon/hatch-flow.js +2 -11
  16. package/dist/heart/daemon/hatch-specialist.js +6 -1
  17. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  18. package/dist/heart/daemon/launchd.js +134 -0
  19. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  20. package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
  21. package/dist/heart/daemon/ouro-path-installer.js +178 -0
  22. package/dist/heart/daemon/ouro-uti.js +11 -2
  23. package/dist/heart/daemon/process-manager.js +1 -1
  24. package/dist/heart/daemon/run-hooks.js +37 -0
  25. package/dist/heart/daemon/runtime-logging.js +9 -5
  26. package/dist/heart/daemon/runtime-metadata.js +118 -0
  27. package/dist/heart/daemon/sense-manager.js +266 -0
  28. package/dist/heart/daemon/specialist-orchestrator.js +129 -0
  29. package/dist/heart/daemon/specialist-prompt.js +98 -0
  30. package/dist/heart/daemon/specialist-tools.js +237 -0
  31. package/dist/heart/daemon/staged-restart.js +114 -0
  32. package/dist/heart/daemon/subagent-installer.js +10 -1
  33. package/dist/heart/daemon/update-checker.js +103 -0
  34. package/dist/heart/daemon/update-hooks.js +138 -0
  35. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  36. package/dist/heart/identity.js +85 -1
  37. package/dist/heart/providers/anthropic.js +19 -2
  38. package/dist/heart/sense-truth.js +61 -0
  39. package/dist/heart/streaming.js +99 -21
  40. package/dist/mind/bundle-manifest.js +69 -0
  41. package/dist/mind/first-impressions.js +2 -1
  42. package/dist/mind/friends/channel.js +8 -0
  43. package/dist/mind/friends/types.js +1 -1
  44. package/dist/mind/phrases.js +1 -0
  45. package/dist/mind/prompt.js +94 -3
  46. package/dist/nerves/cli-logging.js +15 -2
  47. package/dist/repertoire/ado-client.js +4 -2
  48. package/dist/repertoire/coding/feedback.js +134 -0
  49. package/dist/repertoire/coding/index.js +4 -1
  50. package/dist/repertoire/coding/manager.js +61 -2
  51. package/dist/repertoire/coding/spawner.js +3 -3
  52. package/dist/repertoire/coding/tools.js +41 -2
  53. package/dist/repertoire/data/ado-endpoints.json +188 -0
  54. package/dist/repertoire/tools-base.js +69 -5
  55. package/dist/repertoire/tools-teams.js +57 -4
  56. package/dist/repertoire/tools.js +44 -11
  57. package/dist/senses/bluebubbles-client.js +434 -0
  58. package/dist/senses/bluebubbles-entry.js +11 -0
  59. package/dist/senses/bluebubbles-media.js +338 -0
  60. package/dist/senses/bluebubbles-model.js +251 -0
  61. package/dist/senses/bluebubbles-mutation-log.js +76 -0
  62. package/dist/senses/bluebubbles-session-cleanup.js +73 -0
  63. package/dist/senses/bluebubbles.js +449 -0
  64. package/dist/senses/cli.js +299 -133
  65. package/dist/senses/debug-activity.js +108 -0
  66. package/dist/senses/teams.js +173 -54
  67. package/package.json +15 -6
  68. package/subagents/work-doer.md +26 -24
  69. package/subagents/work-merger.md +24 -30
  70. package/subagents/work-planner.md +34 -25
  71. package/dist/inner-worker-entry.js +0 -4
@@ -0,0 +1,108 @@
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 setStatus(text) {
40
+ (0, runtime_1.emitNervesEvent)({
41
+ component: "senses",
42
+ event: "senses.debug_activity_update",
43
+ message: "debug activity status updated",
44
+ meta: {
45
+ hasStatusGuid: Boolean(statusMessageGuid),
46
+ textLength: text.length,
47
+ },
48
+ });
49
+ const shouldStartTyping = !typingActive;
50
+ if (shouldStartTyping) {
51
+ typingActive = true;
52
+ }
53
+ enqueue("status_update", async () => {
54
+ if (statusMessageGuid) {
55
+ await options.transport.editStatus(statusMessageGuid, text);
56
+ }
57
+ else {
58
+ statusMessageGuid = await options.transport.sendStatus(text);
59
+ }
60
+ if (shouldStartTyping) {
61
+ await options.transport.setTyping(true);
62
+ }
63
+ });
64
+ }
65
+ return {
66
+ onModelStart() {
67
+ const pool = hadToolRun ? options.followupPhrases : options.thinkingPhrases;
68
+ setStatus(`${nextPhrase(pool)}...`);
69
+ },
70
+ onToolStart(name, args) {
71
+ hadToolRun = true;
72
+ followupShown = false;
73
+ const argSummary = Object.values(args).join(", ");
74
+ const detail = argSummary ? ` (${argSummary})` : "";
75
+ setStatus(`running ${name}${detail}...`);
76
+ },
77
+ onToolEnd(name, summary, success) {
78
+ hadToolRun = true;
79
+ followupShown = false;
80
+ setStatus((0, format_1.formatToolResult)(name, summary, success));
81
+ },
82
+ onTextChunk(text) {
83
+ if (!text || !hadToolRun || followupShown) {
84
+ return;
85
+ }
86
+ followupShown = true;
87
+ setStatus(`${nextPhrase(options.followupPhrases)}...`);
88
+ },
89
+ onError(error) {
90
+ setStatus((0, format_1.formatError)(error));
91
+ this.finish();
92
+ },
93
+ async drain() {
94
+ await queue;
95
+ },
96
+ async finish() {
97
+ if (!typingActive) {
98
+ await queue;
99
+ return;
100
+ }
101
+ typingActive = false;
102
+ enqueue("typing_stop", async () => {
103
+ await options.transport.setTyping(false);
104
+ });
105
+ await queue;
106
+ },
107
+ };
108
+ }
@@ -44,6 +44,8 @@ exports.startTeamsApp = startTeamsApp;
44
44
  const teams_apps_1 = require("@microsoft/teams.apps");
45
45
  const teams_dev_1 = require("@microsoft/teams.dev");
46
46
  const core_1 = require("../heart/core");
47
+ const tools_1 = require("../repertoire/tools");
48
+ const channel_1 = require("../mind/friends/channel");
47
49
  const config_1 = require("../heart/config");
48
50
  const prompt_1 = require("../mind/prompt");
49
51
  const phrases_1 = require("../mind/phrases");
@@ -58,6 +60,7 @@ const resolver_1 = require("../mind/friends/resolver");
58
60
  const tokens_1 = require("../mind/friends/tokens");
59
61
  const turn_coordinator_1 = require("../heart/turn-coordinator");
60
62
  const identity_1 = require("../heart/identity");
63
+ const http = __importStar(require("http"));
61
64
  const path = __importStar(require("path"));
62
65
  const trust_gate_1 = require("./trust-gate");
63
66
  // Strip @mention markup from incoming messages.
@@ -317,7 +320,14 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
317
320
  onToolStart: (name, args) => {
318
321
  stopPhraseRotation();
319
322
  flushTextBuffer();
320
- const argSummary = Object.values(args).join(", ");
323
+ // Emit a placeholder to satisfy the 15s Copilot timeout for initial
324
+ // stream.emit(). Without this, long tool chains (e.g. ADO batch ops)
325
+ // never emit before the timeout and the user sees "this response was
326
+ // stopped". The placeholder is replaced by actual content on next emit.
327
+ // https://learn.microsoft.com/en-us/answers/questions/2288017/m365-custom-engine-agents-timeout-message-after-15
328
+ if (!streamHasContent)
329
+ safeEmit("⏳");
330
+ const argSummary = (0, tools_1.summarizeArgs)(name, args) || Object.keys(args).join(", ");
321
331
  safeUpdate(`running ${name} (${argSummary})...`);
322
332
  hadToolRun = true;
323
333
  },
@@ -421,7 +431,12 @@ async function withConversationLock(convId, fn) {
421
431
  // Create a fresh friend store per request so mkdirSync re-runs if directories
422
432
  // are deleted while the process is alive.
423
433
  function getFriendStore() {
424
- const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
434
+ // On Azure App Service, os.homedir() returns /root which is ephemeral.
435
+ // Use /home/.agentstate/ (persistent) when WEBSITE_SITE_NAME is set.
436
+ /* v8 ignore next 3 -- Azure vs local path branch; environment-specific @preserve */
437
+ const friendsPath = process.env.WEBSITE_SITE_NAME
438
+ ? path.join("/home", ".agentstate", (0, identity_1.getAgentName)(), "friends")
439
+ : path.join((0, identity_1.getAgentRoot)(), "friends");
425
440
  return new store_file_1.FileFriendStore(friendsPath);
426
441
  }
427
442
  // Handle an incoming Teams message
@@ -445,6 +460,8 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
445
460
  signin: teamsContext.signin,
446
461
  friendStore: store,
447
462
  summarize: (0, core_1.createSummarize)(),
463
+ tenantId: teamsContext.tenantId,
464
+ botApi: teamsContext.botApi,
448
465
  } : undefined;
449
466
  if (toolContext) {
450
467
  const resolver = new resolver_1.FriendResolver(store, {
@@ -497,6 +514,8 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
497
514
  const messages = existing?.messages && existing.messages.length > 0
498
515
  ? existing.messages
499
516
  : [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", undefined, toolContext?.context) }];
517
+ // Repair any orphaned tool calls from a previous aborted turn
518
+ (0, core_1.repairOrphanedToolCalls)(messages);
500
519
  // Push user message
501
520
  messages.push({ role: "user", content: text });
502
521
  // Run agent
@@ -518,12 +537,12 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
518
537
  // This must happen after the stream is done so the OAuth card renders properly.
519
538
  if (teamsContext) {
520
539
  const allContent = messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
521
- if (allContent.includes("AUTH_REQUIRED:graph"))
522
- await teamsContext.signin("graph");
523
- if (allContent.includes("AUTH_REQUIRED:ado"))
524
- await teamsContext.signin("ado");
525
- if (allContent.includes("AUTH_REQUIRED:github"))
526
- await teamsContext.signin("github");
540
+ if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
541
+ await teamsContext.signin(teamsContext.graphConnectionName);
542
+ if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
543
+ await teamsContext.signin(teamsContext.adoConnectionName);
544
+ if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
545
+ await teamsContext.signin(teamsContext.githubConnectionName);
527
546
  }
528
547
  // Trim context and save session
529
548
  (0, context_1.postTurn)(messages, sessPath, result.usage);
@@ -533,49 +552,85 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
533
552
  }
534
553
  // SDK auto-closes the stream after our handler returns (app.process.js)
535
554
  }
536
- // Start the Teams app in DevtoolsPlugin mode (local dev) or Bot Service mode (real Teams).
537
- // Mode is determined by getTeamsConfig().clientId.
538
- // Text is always accumulated in textBuffer and flushed periodically (chunked streaming).
539
- function startTeamsApp() {
555
+ // Internal port for the secondary bot App (not exposed externally).
556
+ // The primary app proxies /api/messages-secondary → localhost:SECONDARY_PORT/api/messages.
557
+ const SECONDARY_INTERNAL_PORT = 3979;
558
+ // Collect all unique OAuth connection names across top-level config and tenant overrides.
559
+ /* v8 ignore start -- runtime Teams SDK config; no unit-testable surface @preserve */
560
+ function allOAuthConnectionNames() {
561
+ const oauthConfig = (0, config_1.getOAuthConfig)();
562
+ const names = new Set();
563
+ if (oauthConfig.graphConnectionName)
564
+ names.add(oauthConfig.graphConnectionName);
565
+ if (oauthConfig.adoConnectionName)
566
+ names.add(oauthConfig.adoConnectionName);
567
+ if (oauthConfig.githubConnectionName)
568
+ names.add(oauthConfig.githubConnectionName);
569
+ if (oauthConfig.tenantOverrides) {
570
+ for (const ov of Object.values(oauthConfig.tenantOverrides)) {
571
+ if (ov.graphConnectionName)
572
+ names.add(ov.graphConnectionName);
573
+ if (ov.adoConnectionName)
574
+ names.add(ov.adoConnectionName);
575
+ if (ov.githubConnectionName)
576
+ names.add(ov.githubConnectionName);
577
+ }
578
+ }
579
+ return [...names];
580
+ }
581
+ // Create an App instance from a TeamsConfig. Returns { app, mode }.
582
+ function createBotApp(teamsConfig) {
540
583
  const mentionStripping = { activity: { mentions: { stripText: true } } };
541
- const teamsConfig = (0, config_2.getTeamsConfig)();
542
- let app;
543
- let mode;
544
584
  const oauthConfig = (0, config_1.getOAuthConfig)();
545
- if (teamsConfig.clientId) {
546
- // Bot Service mode -- real Teams connection with SingleTenant credentials
547
- app = new teams_apps_1.App({
548
- clientId: teamsConfig.clientId,
549
- clientSecret: teamsConfig.clientSecret,
550
- tenantId: teamsConfig.tenantId,
551
- oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
552
- ...mentionStripping,
553
- });
554
- mode = "Bot Service";
585
+ if (teamsConfig.clientId && teamsConfig.clientSecret) {
586
+ return {
587
+ app: new teams_apps_1.App({
588
+ clientId: teamsConfig.clientId,
589
+ clientSecret: teamsConfig.clientSecret,
590
+ tenantId: teamsConfig.tenantId,
591
+ oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
592
+ ...mentionStripping,
593
+ }),
594
+ mode: "Bot Service (client secret)",
595
+ };
596
+ }
597
+ else if (teamsConfig.clientId) {
598
+ return {
599
+ app: new teams_apps_1.App({
600
+ clientId: teamsConfig.clientId,
601
+ tenantId: teamsConfig.tenantId,
602
+ ...(teamsConfig.managedIdentityClientId ? { managedIdentityClientId: teamsConfig.managedIdentityClientId } : {}),
603
+ oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
604
+ ...mentionStripping,
605
+ }),
606
+ mode: "Bot Service (managed identity)",
607
+ };
555
608
  }
556
609
  else {
557
- // DevtoolsPlugin mode -- local development with Teams DevtoolsPlugin UI
558
- app = new teams_apps_1.App({
559
- plugins: [new teams_dev_1.DevtoolsPlugin()],
560
- ...mentionStripping,
561
- });
562
- mode = "DevtoolsPlugin";
610
+ return {
611
+ app: new teams_apps_1.App({
612
+ plugins: [new teams_dev_1.DevtoolsPlugin()],
613
+ ...mentionStripping,
614
+ }),
615
+ mode: "DevtoolsPlugin",
616
+ };
563
617
  }
618
+ }
619
+ /* v8 ignore stop */
620
+ // Register message, verify-state, and error handlers on an App instance.
621
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
622
+ function registerBotHandlers(app, label) {
623
+ const connectionNames = allOAuthConnectionNames();
564
624
  // Override default OAuth verify-state handler. The SDK's built-in handler
565
625
  // uses a single defaultConnectionName, which breaks multi-connection setups
566
626
  // (graph + ado + github). The verifyState activity only carries a `state`
567
627
  // code with no connectionName, so we try each configured connection until
568
628
  // one succeeds.
569
- const allConnectionNames = [
570
- oauthConfig.graphConnectionName,
571
- oauthConfig.adoConnectionName,
572
- oauthConfig.githubConnectionName,
573
- ].filter(Boolean);
574
629
  app.on("signin.verify-state", async (ctx) => {
575
630
  const { api, activity } = ctx;
576
631
  if (!activity.value?.state)
577
632
  return { status: 404 };
578
- for (const cn of allConnectionNames) {
633
+ for (const cn of connectionNames) {
579
634
  try {
580
635
  await api.users.token.get({
581
636
  channelId: activity.channelId,
@@ -583,12 +638,12 @@ function startTeamsApp() {
583
638
  connectionName: cn,
584
639
  code: activity.value.state,
585
640
  });
586
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.verify_state", component: "channels", message: `verify-state succeeded for connection "${cn}"`, meta: { connectionName: cn } });
641
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state succeeded for connection "${cn}"`, meta: { connectionName: cn } });
587
642
  return { status: 200 };
588
643
  }
589
644
  catch { /* try next */ }
590
645
  }
591
- (0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message: "verify-state failed for all connections", meta: {} });
646
+ (0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state failed for all connections`, meta: {} });
592
647
  return { status: 412 };
593
648
  });
594
649
  app.on("message", async (ctx) => {
@@ -598,16 +653,12 @@ function startTeamsApp() {
598
653
  const turnKey = teamsTurnKey(convId);
599
654
  const userId = activity.from?.id || "";
600
655
  const channelId = activity.channelId || "msteams";
601
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: "incoming teams message", meta: { userId: userId.slice(0, 12), conversationId: convId.slice(0, 20) } });
656
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: `[${label}] incoming teams message`, meta: { userId: userId.slice(0, 12), conversationId: convId.slice(0, 20) } });
602
657
  // Resolve pending confirmations IMMEDIATELY — before token fetches or
603
658
  // the conversation lock. The original message holds the lock while
604
659
  // awaiting confirmation, so acquiring it here would deadlock. Token
605
660
  // fetches are also unnecessary (and slow) for a simple yes/no reply.
606
661
  if (resolvePendingConfirmation(convId, text)) {
607
- // Don't emit on this stream — the original message's stream is still
608
- // active. Opening a second streaming response in the same conversation
609
- // can corrupt the first. The original stream will show tool progress
610
- // once the confirmation Promise resolves.
611
662
  return;
612
663
  }
613
664
  // If this conversation already has an active turn, steer follow-up input
@@ -621,27 +672,30 @@ function startTeamsApp() {
621
672
  return;
622
673
  }
623
674
  try {
675
+ // Resolve OAuth connection names for this user's tenant (supports per-tenant overrides).
676
+ const tenantId = activity.conversation?.tenantId;
677
+ const tenantOAuth = (0, config_1.resolveOAuthForTenant)(tenantId);
624
678
  // Fetch tokens for both OAuth connections independently.
625
679
  // Failures are silently caught -- the tool handler will request signin if needed.
626
680
  let graphToken;
627
681
  let adoToken;
628
682
  let githubToken;
629
683
  try {
630
- const graphRes = await api.users.token.get({ userId, connectionName: oauthConfig.graphConnectionName, channelId });
684
+ const graphRes = await api.users.token.get({ userId, connectionName: tenantOAuth.graphConnectionName, channelId });
631
685
  graphToken = graphRes?.token;
632
686
  }
633
687
  catch { /* no token yet — tool handler will trigger signin */ }
634
688
  try {
635
- const adoRes = await api.users.token.get({ userId, connectionName: oauthConfig.adoConnectionName, channelId });
689
+ const adoRes = await api.users.token.get({ userId, connectionName: tenantOAuth.adoConnectionName, channelId });
636
690
  adoToken = adoRes?.token;
637
691
  }
638
692
  catch { /* no token yet — tool handler will trigger signin */ }
639
693
  try {
640
- const githubRes = await api.users.token.get({ userId, connectionName: oauthConfig.githubConnectionName, channelId });
694
+ const githubRes = await api.users.token.get({ userId, connectionName: tenantOAuth.githubConnectionName, channelId });
641
695
  githubToken = githubRes?.token;
642
696
  }
643
697
  catch { /* no token yet — tool handler will trigger signin */ }
644
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken } });
698
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken, tenantId } });
645
699
  const teamsContext = {
646
700
  graphToken,
647
701
  adoToken,
@@ -661,6 +715,11 @@ function startTeamsApp() {
661
715
  aadObjectId: activity.from?.aadObjectId,
662
716
  tenantId: activity.conversation?.tenantId,
663
717
  displayName: activity.from?.name,
718
+ graphConnectionName: tenantOAuth.graphConnectionName,
719
+ adoConnectionName: tenantOAuth.adoConnectionName,
720
+ githubConnectionName: tenantOAuth.githubConnectionName,
721
+ /* v8 ignore next -- bot API availability branch; requires live SDK context @preserve */
722
+ botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
664
723
  };
665
724
  /* v8 ignore next 5 -- bot-framework integration callback; tested via handleTeamsMessage sendMessage path @preserve */
666
725
  const ctxSend = async (t) => {
@@ -678,6 +737,23 @@ function startTeamsApp() {
678
737
  _turnCoordinator.endTurn(turnKey);
679
738
  }
680
739
  });
740
+ app.event("error", ({ error }) => {
741
+ const msg = error instanceof Error ? error.message : String(error);
742
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: `[${label}] ${msg}`, meta: {} });
743
+ });
744
+ }
745
+ // Start the Teams app in DevtoolsPlugin mode (local dev) or Bot Service mode (real Teams).
746
+ // Mode is determined by getTeamsConfig().clientId.
747
+ // Text is always accumulated in textBuffer and flushed periodically (chunked streaming).
748
+ //
749
+ // Dual-bot support: if teamsSecondary is configured with a clientId, a second App
750
+ // instance starts on an internal port and the primary app proxies requests from
751
+ // /api/messages-secondary to it. This lets a single App Service serve two bot
752
+ // registrations (e.g. one per tenant) without SDK modifications.
753
+ function startTeamsApp() {
754
+ const teamsConfig = (0, config_2.getTeamsConfig)();
755
+ const { app, mode } = createBotApp(teamsConfig);
756
+ registerBotHandlers(app, "primary");
681
757
  if (!process.listeners("unhandledRejection").some((l) => l.__agentHandler)) {
682
758
  const handler = (err) => {
683
759
  const msg = err instanceof Error ? err.message : String(err);
@@ -686,11 +762,54 @@ function startTeamsApp() {
686
762
  handler.__agentHandler = true;
687
763
  process.on("unhandledRejection", handler);
688
764
  }
689
- app.event("error", ({ error }) => {
690
- const msg = error instanceof Error ? error.message : String(error);
691
- (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: msg, meta: {} });
692
- });
693
- const port = (0, config_2.getTeamsChannelConfig)().port;
765
+ /* v8 ignore next -- PORT env branch; runtime-only @preserve */
766
+ const port = process.env.PORT ? Number(process.env.PORT) : (0, config_2.getTeamsChannelConfig)().port;
694
767
  app.start(port);
695
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Teams bot started on port ${port} with ${mode} (chunked streaming)`, meta: { port, mode } });
768
+ // Diagnostic: log tool count at startup to verify deploy
769
+ const startupTools = (0, tools_1.getToolsForChannel)((0, channel_1.getChannelCapabilities)("teams"));
770
+ const toolNames = startupTools.map((t) => t.function.name);
771
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Teams bot started on port ${port} with ${mode} (chunked streaming)`, meta: { port, mode, toolCount: toolNames.length, hasProactive: toolNames.includes("teams_send_message") } });
772
+ // --- Secondary bot (dual-bot support) ---
773
+ // If teamsSecondary has a clientId, start a second App on an internal port
774
+ // and proxy /api/messages-secondary on the primary app to it.
775
+ /* v8 ignore start -- dual-bot proxy wiring; requires live Teams SDK + HTTP @preserve */
776
+ const secondaryConfig = (0, config_1.getTeamsSecondaryConfig)();
777
+ if (secondaryConfig.clientId) {
778
+ const { app: secondaryApp, mode: secondaryMode } = createBotApp(secondaryConfig);
779
+ registerBotHandlers(secondaryApp, "secondary");
780
+ secondaryApp.start(SECONDARY_INTERNAL_PORT);
781
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Secondary bot started on internal port ${SECONDARY_INTERNAL_PORT} with ${secondaryMode}`, meta: { port: SECONDARY_INTERNAL_PORT, mode: secondaryMode } });
782
+ // Proxy: forward /api/messages-secondary on the primary app's Express
783
+ // to localhost:SECONDARY_INTERNAL_PORT/api/messages.
784
+ // The SDK's HttpPlugin exposes .post() bound to its Express instance.
785
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
786
+ const httpPlugin = app.http;
787
+ httpPlugin.post("/api/messages-secondary", (req, res) => {
788
+ const body = JSON.stringify(req.body);
789
+ const proxyReq = http.request({
790
+ hostname: "127.0.0.1",
791
+ port: SECONDARY_INTERNAL_PORT,
792
+ path: "/api/messages",
793
+ method: "POST",
794
+ headers: {
795
+ ...req.headers,
796
+ "content-length": Buffer.byteLength(body).toString(),
797
+ },
798
+ }, (proxyRes) => {
799
+ res.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
800
+ proxyRes.pipe(res);
801
+ });
802
+ proxyReq.on("error", (err) => {
803
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.proxy_error", component: "channels", message: `secondary proxy error: ${err.message}`, meta: {} });
804
+ if (!res.headersSent) {
805
+ res.writeHead(502);
806
+ res.end("Bad Gateway");
807
+ }
808
+ });
809
+ proxyReq.write(body);
810
+ proxyReq.end();
811
+ });
812
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.proxy_ready", component: "channels", message: "proxy /api/messages-secondary → secondary bot ready", meta: {} });
813
+ }
814
+ /* v8 ignore stop */
696
815
  }
package/package.json CHANGED
@@ -1,19 +1,21 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.30",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
+ "cli": "dist/heart/daemon/ouro-bot-entry.js",
6
7
  "ouro": "dist/heart/daemon/ouro-entry.js",
7
- "ouro.bot": "dist/heart/daemon/ouro-bot-entry.js",
8
- "cli": "dist/heart/daemon/ouro-entry.js"
8
+ "ouro.bot": "dist/heart/daemon/ouro-bot-entry.js"
9
9
  },
10
10
  "files": [
11
11
  "dist/",
12
12
  "AdoptionSpecialist.ouro/",
13
- "subagents/"
13
+ "subagents/",
14
+ "assets/",
15
+ "changelog.json"
14
16
  ],
15
17
  "exports": {
16
- ".": "./dist/heart/daemon/ouro-entry.js",
18
+ ".": "./dist/heart/daemon/daemon-cli.js",
17
19
  "./runOuroCli": "./dist/heart/daemon/daemon-cli.js"
18
20
  },
19
21
  "scripts": {
@@ -21,6 +23,7 @@
21
23
  "daemon": "tsc && node dist/heart/daemon/daemon-entry.js",
22
24
  "ouro": "tsc && node dist/heart/daemon/ouro-entry.js",
23
25
  "teams": "tsc && node dist/senses/teams-entry.js --agent ouroboros",
26
+ "bluebubbles": "tsc && node dist/senses/bluebubbles-entry.js --agent ouroboros",
24
27
  "test": "vitest run",
25
28
  "test:coverage:vitest": "vitest run --coverage",
26
29
  "test:coverage": "node scripts/run-coverage-gate.cjs",
@@ -32,9 +35,15 @@
32
35
  "@anthropic-ai/sdk": "^0.78.0",
33
36
  "@microsoft/teams.apps": "^2.0.5",
34
37
  "@microsoft/teams.dev": "^2.0.5",
35
- "openai": "^4.78.0"
38
+ "openai": "^6.27.0",
39
+ "semver": "^7.7.4"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/ouroborosbot/ouroboros"
36
44
  },
37
45
  "devDependencies": {
46
+ "@types/semver": "^7.7.1",
38
47
  "@vitest/coverage-v8": "^4.0.18",
39
48
  "eslint": "^10.0.2",
40
49
  "typescript": "^5.7.0",
@@ -8,17 +8,18 @@ You are a task executor. Read a doing.md file and execute all units sequentially
8
8
 
9
9
  ## On Startup
10
10
 
11
- 1. **Find doing doc**: Look for `YYYY-MM-DD-HHMM-doing-*.md` in repo root
12
- 2. If multiple found, ask which one
13
- 3. If none found, ask user for location
14
- 4. **Check execution_mode**: Read the doing doc's `Execution Mode` field
15
- 5. **Verify artifacts directory exists**: `{task-name}/` next to `{task-name}.md`
11
+ 1. **Find task-doc directory**: Read project instructions (for example `AGENTS.md`) to determine where planning/doing docs live for this repo
12
+ 2. **Find doing doc**: Look for `YYYY-MM-DD-HHMM-doing-*.md` in that project-defined task-doc directory
13
+ 3. If multiple found, ask which one
14
+ 4. If none found, ask user for location
15
+ 5. **Check execution_mode**: Read the doing doc's `Execution Mode` field
16
+ 6. **Verify artifacts directory exists**: `{task-name}/` next to `{task-name}.md`
16
17
  - If missing, create it: `mkdir {task-name}`
17
- 6. **Detect resume vs fresh start:**
18
+ 7. **Detect resume vs fresh start:**
18
19
  - Count completed units (✅) vs total units
19
20
  - Check git status for uncommitted changes
20
21
 
21
- 7. **Announce status clearly:**
22
+ 8. **Announce status clearly:**
22
23
 
23
24
  **If fresh start (0 units complete):**
24
25
  ```
@@ -214,20 +215,21 @@ When all units are `✅`:
214
215
  ## Rules
215
216
 
216
217
  1. **File naming**: Expect `YYYY-MM-DD-HHMM-doing-{name}.md` format
217
- 2. **Artifacts directory**: Use `{task-name}/` for all outputs, logs, data
218
- 3. **Execution mode**: Honor `pending | spawn | direct` from doing doc
219
- 4. **TDD strictly enforced** tests before implementation, always
220
- 5. **100% coverage** — no exceptions, no exclude attributes
221
- 6. **Atomic commits** — one logical unit per commit, push after each
222
- 7. **Timestamps from git** — `git log -1 --format="%Y-%m-%d %H:%M"`
223
- 8. **Push after each unit phase complete**
224
- 9. **Update doing.md after each unit** status and progress log
225
- 10. **Spawn sub-agents for fixes** — don't ask, just do it
226
- 11. **Update docs immediately** — when decisions made, commit right away
227
- 12. **Stop on actual blocker** — unclear requirements or need user input
228
- 13. **/compact proactively** — preserve context between units
229
- 14. **No warnings** — treat warnings as errors
230
- 15. **Run full test suite** — before marking unit complete, not just new tests
231
- 16. **Always compile** — run the project's build command after every implementation/refactor unit. Tests passing is necessary but not sufficient.
232
- 17. **Checklist hygiene is mandatory** — keep doing/planning `Completion Criteria` checklists synchronized with verified completion evidence.
233
- 18. **Verify APIs before importing** — before writing `import { Foo } from './bar'`, use `grep` or `read_file` to confirm `Foo` is actually exported from that module. Never assume an export exists — always check the source first. This prevents wasted cycles on "module has no exported member" errors.
218
+ 2. **Location**: Read and update doing docs in the project-defined task-doc directory, which may live outside the repo
219
+ 3. **Artifacts directory**: Use `{task-name}/` for all outputs, logs, data
220
+ 4. **Execution mode**: Honor `pending | spawn | direct` from doing doc
221
+ 5. **TDD strictly enforced** — tests before implementation, always
222
+ 6. **100% coverage** — no exceptions, no exclude attributes
223
+ 7. **Atomic commits** — one logical unit per commit, push after each
224
+ 8. **Timestamps from git** `git log -1 --format="%Y-%m-%d %H:%M"`
225
+ 9. **Push after each unit phase complete**
226
+ 10. **Update doing.md after each unit** — status and progress log
227
+ 11. **Spawn sub-agents for fixes** — don't ask, just do it
228
+ 12. **Update docs immediately** — when decisions made, commit right away
229
+ 13. **Stop on actual blocker** — unclear requirements or need user input
230
+ 14. **/compact proactively** — preserve context between units
231
+ 15. **No warnings** — treat warnings as errors
232
+ 16. **Run full test suite** — before marking unit complete, not just new tests
233
+ 17. **Always compile** — run the project's build command after every implementation/refactor unit. Tests passing is necessary but not sufficient.
234
+ 18. **Checklist hygiene is mandatory** — keep doing/planning `Completion Criteria` checklists synchronized with verified completion evidence.
235
+ 19. **Verify APIs before importing** — before writing `import { Foo } from './bar'`, use `grep` or `read_file` to confirm `Foo` is actually exported from that module. Never assume an export exists — always check the source first. This prevents wasted cycles on "module has no exported member" errors.