@ouro.bot/cli 0.1.0-alpha.2 → 0.1.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AdoptionSpecialist.ouro/agent.json +70 -9
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/assets/ouroboros.png +0 -0
- package/dist/heart/config.js +66 -4
- package/dist/heart/core.js +75 -2
- package/dist/heart/daemon/daemon-cli.js +523 -33
- package/dist/heart/daemon/daemon-entry.js +13 -5
- package/dist/heart/daemon/daemon-runtime-sync.js +90 -0
- package/dist/heart/daemon/daemon.js +42 -9
- package/dist/heart/daemon/hatch-animation.js +35 -0
- package/dist/heart/daemon/hatch-flow.js +2 -11
- package/dist/heart/daemon/hatch-specialist.js +6 -1
- package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
- package/dist/heart/daemon/ouro-path-installer.js +178 -0
- package/dist/heart/daemon/ouro-uti.js +11 -2
- package/dist/heart/daemon/process-manager.js +1 -1
- package/dist/heart/daemon/runtime-logging.js +9 -5
- package/dist/heart/daemon/runtime-metadata.js +118 -0
- package/dist/heart/daemon/sense-manager.js +266 -0
- package/dist/heart/daemon/specialist-orchestrator.js +129 -0
- package/dist/heart/daemon/specialist-prompt.js +98 -0
- package/dist/heart/daemon/specialist-tools.js +237 -0
- package/dist/heart/daemon/subagent-installer.js +10 -1
- package/dist/heart/daemon/wrapper-publish-guard.js +48 -0
- package/dist/heart/identity.js +77 -1
- package/dist/heart/providers/anthropic.js +19 -2
- package/dist/heart/sense-truth.js +61 -0
- package/dist/heart/streaming.js +99 -21
- package/dist/mind/bundle-manifest.js +58 -0
- package/dist/mind/friends/channel.js +8 -0
- package/dist/mind/friends/types.js +1 -1
- package/dist/mind/prompt.js +77 -3
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/coding/feedback.js +134 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +61 -2
- package/dist/repertoire/coding/spawner.js +3 -3
- package/dist/repertoire/coding/tools.js +41 -2
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/tools-base.js +69 -5
- package/dist/repertoire/tools-teams.js +57 -4
- package/dist/repertoire/tools.js +44 -11
- package/dist/senses/bluebubbles-client.js +433 -0
- package/dist/senses/bluebubbles-entry.js +11 -0
- package/dist/senses/bluebubbles-media.js +244 -0
- package/dist/senses/bluebubbles-model.js +253 -0
- package/dist/senses/bluebubbles-mutation-log.js +76 -0
- package/dist/senses/bluebubbles.js +421 -0
- package/dist/senses/cli.js +293 -133
- package/dist/senses/debug-activity.js +107 -0
- package/dist/senses/teams.js +173 -54
- package/package.json +11 -4
- package/subagents/work-doer.md +26 -24
- package/subagents/work-merger.md +24 -30
- package/subagents/work-planner.md +34 -25
- package/dist/inner-worker-entry.js +0 -4
package/dist/senses/teams.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
523
|
-
if (allContent.includes("AUTH_REQUIRED:ado"))
|
|
524
|
-
await teamsContext.signin(
|
|
525
|
-
if (allContent.includes("AUTH_REQUIRED:github"))
|
|
526
|
-
await teamsContext.signin(
|
|
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
|
-
//
|
|
537
|
-
//
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
690
|
-
|
|
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
|
-
|
|
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,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.21",
|
|
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
8
|
"ouro.bot": "dist/heart/daemon/ouro-bot-entry.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"dist/",
|
|
11
12
|
"AdoptionSpecialist.ouro/",
|
|
12
|
-
"subagents/"
|
|
13
|
+
"subagents/",
|
|
14
|
+
"assets/"
|
|
13
15
|
],
|
|
14
16
|
"exports": {
|
|
15
|
-
".": "./dist/heart/daemon/
|
|
17
|
+
".": "./dist/heart/daemon/daemon-cli.js",
|
|
16
18
|
"./runOuroCli": "./dist/heart/daemon/daemon-cli.js"
|
|
17
19
|
},
|
|
18
20
|
"scripts": {
|
|
@@ -20,6 +22,7 @@
|
|
|
20
22
|
"daemon": "tsc && node dist/heart/daemon/daemon-entry.js",
|
|
21
23
|
"ouro": "tsc && node dist/heart/daemon/ouro-entry.js",
|
|
22
24
|
"teams": "tsc && node dist/senses/teams-entry.js --agent ouroboros",
|
|
25
|
+
"bluebubbles": "tsc && node dist/senses/bluebubbles-entry.js --agent ouroboros",
|
|
23
26
|
"test": "vitest run",
|
|
24
27
|
"test:coverage:vitest": "vitest run --coverage",
|
|
25
28
|
"test:coverage": "node scripts/run-coverage-gate.cjs",
|
|
@@ -31,7 +34,11 @@
|
|
|
31
34
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
32
35
|
"@microsoft/teams.apps": "^2.0.5",
|
|
33
36
|
"@microsoft/teams.dev": "^2.0.5",
|
|
34
|
-
"openai": "^
|
|
37
|
+
"openai": "^6.27.0"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/ouroborosbot/ouroboros"
|
|
35
42
|
},
|
|
36
43
|
"devDependencies": {
|
|
37
44
|
"@vitest/coverage-v8": "^4.0.18",
|
package/subagents/work-doer.md
CHANGED
|
@@ -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
|
|
12
|
-
2.
|
|
13
|
-
3. If
|
|
14
|
-
4.
|
|
15
|
-
5. **
|
|
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
|
-
|
|
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
|
-
|
|
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. **
|
|
218
|
-
3. **
|
|
219
|
-
4. **
|
|
220
|
-
5. **
|
|
221
|
-
6. **
|
|
222
|
-
7. **
|
|
223
|
-
8. **
|
|
224
|
-
9. **
|
|
225
|
-
10. **
|
|
226
|
-
11. **
|
|
227
|
-
12. **
|
|
228
|
-
13.
|
|
229
|
-
14.
|
|
230
|
-
15. **
|
|
231
|
-
16. **
|
|
232
|
-
17. **
|
|
233
|
-
18. **
|
|
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.
|
package/subagents/work-merger.md
CHANGED
|
@@ -19,12 +19,16 @@ The branch follows the `<agent>/<slug>` convention (e.g., `ouroboros/context-ker
|
|
|
19
19
|
|
|
20
20
|
Do not hardcode agent names. Derive `<agent>` from the branch at runtime.
|
|
21
21
|
|
|
22
|
+
### 1a. Determine the project-defined task-doc directory
|
|
23
|
+
|
|
24
|
+
Read project instructions (for example `AGENTS.md`) to determine where this repo keeps planning/doing docs. Set `TASK_DIR` to that project-defined location. Do not assume task docs live in the repo.
|
|
25
|
+
|
|
22
26
|
### 2. Find own doing doc
|
|
23
27
|
|
|
24
|
-
The caller provides the doing doc path (
|
|
28
|
+
The caller provides the doing doc path. If not provided, read project instructions (for example `AGENTS.md`) to find the project-defined task-doc directory, then find the most recent doing doc there:
|
|
25
29
|
|
|
26
30
|
```bash
|
|
27
|
-
ls -t ${
|
|
31
|
+
ls -t "${TASK_DIR}"/*-doing-*.md | head -1
|
|
28
32
|
```
|
|
29
33
|
|
|
30
34
|
Read this doing doc to understand what was just implemented. You will need it for conflict resolution context.
|
|
@@ -134,35 +138,28 @@ You already have the path from On Startup. Read the doing doc to understand:
|
|
|
134
138
|
- The objective, completion criteria, and unit descriptions
|
|
135
139
|
- What files were changed and why
|
|
136
140
|
|
|
137
|
-
### Step 2:
|
|
138
|
-
|
|
139
|
-
Find exactly which doing docs landed on main since this branch diverged. Do NOT scan by filename timestamps or guess -- use git history:
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
git log origin/main --not HEAD --name-only --diff-filter=A -- '*/tasks/*-doing-*.md'
|
|
143
|
-
```
|
|
141
|
+
### Step 2: Gather incoming-main intent (git-informed)
|
|
144
142
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
If no doing docs are found with `--diff-filter=A` (added), also check for modifications:
|
|
143
|
+
Do not assume task docs live in this repo. Instead, use git history and diffs to understand what landed on `main` since this branch diverged:
|
|
148
144
|
|
|
149
145
|
```bash
|
|
150
|
-
git log origin/main --not HEAD --
|
|
146
|
+
git log origin/main --not HEAD --oneline
|
|
147
|
+
git diff --name-only HEAD...origin/main
|
|
151
148
|
```
|
|
152
149
|
|
|
153
|
-
|
|
154
|
-
- Timestamp-sorted filename scans can miss relevant docs or include irrelevant ones
|
|
155
|
-
- Git history tells you exactly what landed on main since you branched
|
|
156
|
-
- This is deterministic and correct regardless of doc naming or timing
|
|
150
|
+
If a clearly relevant local task doc exists outside the repo (for example in another local bundle/worktree task directory), you may read it for extra context. Treat that as optional context, not a required precondition.
|
|
157
151
|
|
|
158
|
-
|
|
152
|
+
**Why this is the primary source of truth:**
|
|
153
|
+
- Task docs may live outside the repo entirely
|
|
154
|
+
- Git history tells you exactly what changed on `main` since you branched
|
|
155
|
+
- This keeps work-merger generic instead of assuming one repo's task-doc layout
|
|
159
156
|
|
|
160
|
-
|
|
161
|
-
- What the other agent implemented
|
|
162
|
-
- Their objective and completion criteria
|
|
163
|
-
- Which files they changed and why
|
|
157
|
+
### Step 3: Combine own task intent with incoming-main changes
|
|
164
158
|
|
|
165
|
-
|
|
159
|
+
Use:
|
|
160
|
+
- your own doing doc as the source of truth for this branch's intent
|
|
161
|
+
- incoming git commits/diffs as the source of truth for what landed on `main`
|
|
162
|
+
- any optional local task docs only when they materially clarify a conflict
|
|
166
163
|
|
|
167
164
|
### Step 4: Resolve conflicts
|
|
168
165
|
|
|
@@ -182,7 +179,7 @@ With both intents understood, resolve each conflict:
|
|
|
182
179
|
If the merge was syntactically clean but tests fail (Case B):
|
|
183
180
|
|
|
184
181
|
1. Read the test failure output to identify which tests broke
|
|
185
|
-
2. Cross-reference with
|
|
182
|
+
2. Cross-reference with your doing doc plus the incoming git changes to understand the conflict
|
|
186
183
|
3. Fix the code to satisfy both agents' intents
|
|
187
184
|
4. Re-run tests: `npm test`
|
|
188
185
|
5. Repeat until tests pass
|
|
@@ -204,7 +201,7 @@ git commit -m "fix: resolve semantic conflicts after merging main"
|
|
|
204
201
|
npm test
|
|
205
202
|
```
|
|
206
203
|
|
|
207
|
-
All tests must pass before proceeding to PR Workflow. If tests still fail after resolution, re-examine
|
|
204
|
+
All tests must pass before proceeding to PR Workflow. If tests still fail after resolution, re-examine your doing doc, the incoming git changes, and any optional supporting task docs, then try again. If genuinely stuck after multiple attempts, escalate to the user (see **Escalation**).
|
|
208
205
|
|
|
209
206
|
---
|
|
210
207
|
|
|
@@ -227,20 +224,17 @@ git push --force-with-lease origin ${BRANCH}
|
|
|
227
224
|
|
|
228
225
|
### Step 2: Create the pull request
|
|
229
226
|
|
|
230
|
-
Before creating the PR, build a comprehensive description of **all** changes on this branch relative to main — not just the most recent task. Use git to understand the full scope:
|
|
227
|
+
Before creating the PR, build a comprehensive description of **all** changes on this branch relative to main — not just the most recent task. Use your doing doc plus git to understand the full scope:
|
|
231
228
|
|
|
232
229
|
```bash
|
|
233
230
|
# All commits on this branch not on main
|
|
234
231
|
git log origin/main..HEAD --oneline
|
|
235
232
|
|
|
236
|
-
# All doing docs on this branch (completed tasks)
|
|
237
|
-
git log origin/main..HEAD --name-only --diff-filter=A -- '*/tasks/*-doing-*.md'
|
|
238
|
-
|
|
239
233
|
# Summary of all files changed
|
|
240
234
|
git diff origin/main --stat
|
|
241
235
|
```
|
|
242
236
|
|
|
243
|
-
Read
|
|
237
|
+
Read the doing doc you are executing, plus any other explicitly provided task docs for this branch. The PR body should summarize every completed task on the branch, grouped logically when needed. Include:
|
|
244
238
|
- A section per task (or group of related tasks) with a brief summary of what was implemented
|
|
245
239
|
- A final "Files changed" summary (e.g., "164 files changed — new context kernel, codebase restructure, sync-and-merge system")
|
|
246
240
|
|