metheus-governance-mcp-cli 0.2.278 → 0.2.279

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.
@@ -9,6 +9,14 @@ function ensureArray(value) {
9
9
  return Array.isArray(value) ? value : [];
10
10
  }
11
11
 
12
+ function intFromRawAllowZero(raw, fallback = 0) {
13
+ if (raw === null || raw === undefined || raw === "") {
14
+ return fallback;
15
+ }
16
+ const parsed = Number.parseInt(String(raw).trim(), 10);
17
+ return Number.isFinite(parsed) ? parsed : fallback;
18
+ }
19
+
12
20
  function uniqueOrdered(values) {
13
21
  const seen = new Set();
14
22
  const ordered = [];
@@ -21,6 +29,104 @@ function uniqueOrdered(values) {
21
29
  return ordered;
22
30
  }
23
31
 
32
+ function normalizeSelectorList(values, normalizeMentionSelector) {
33
+ return uniqueOrdered(
34
+ ensureArray(values)
35
+ .map((item) => normalizeMentionSelector(item))
36
+ .filter(Boolean),
37
+ );
38
+ }
39
+
40
+ function buildImplicitPublicMultiBotExecutionContract({
41
+ conversationContext = null,
42
+ executionContract = null,
43
+ currentBotSelector = "",
44
+ normalizeMentionSelector,
45
+ }) {
46
+ const context = safeObject(conversationContext);
47
+ const normalizedCurrentBotSelector = normalizeMentionSelector(currentBotSelector);
48
+ if (
49
+ String(context.mode || "").trim() !== "public_multi_bot"
50
+ || String(context.intentMode || "").trim() !== "multi_bot_collab"
51
+ || context.allowBotToBot !== true
52
+ || !normalizedCurrentBotSelector
53
+ ) {
54
+ return safeObject(executionContract);
55
+ }
56
+ const normalizedAllowedResponders = normalizeSelectorList(
57
+ context.allowedResponderSelectors,
58
+ normalizeMentionSelector,
59
+ );
60
+ if (
61
+ normalizedAllowedResponders.length < 2
62
+ || !normalizedAllowedResponders.includes(normalizedCurrentBotSelector)
63
+ ) {
64
+ return safeObject(executionContract);
65
+ }
66
+
67
+ const explicitContract = safeObject(executionContract);
68
+ const explicitAssignments = normalizeSelectorList(
69
+ ensureArray(explicitContract.assignments).map((item) => (
70
+ safeObject(item).targetBot || safeObject(item).target_bot
71
+ )),
72
+ normalizeMentionSelector,
73
+ ).filter((item) => item !== normalizedCurrentBotSelector);
74
+ const explicitNextResponders = normalizeSelectorList(
75
+ explicitContract.nextResponders || explicitContract.next_responders || explicitContract.responders,
76
+ normalizeMentionSelector,
77
+ ).filter((item) => item !== normalizedCurrentBotSelector);
78
+ const explicitSummaryBot = normalizeMentionSelector(
79
+ explicitContract.summaryBot || explicitContract.summary_bot || context.summaryBotUsername,
80
+ );
81
+ const explicitType = String(explicitContract.type || "").trim().toLowerCase();
82
+ const hasExplicitFollowup = explicitAssignments.length > 0
83
+ || explicitNextResponders.length > 0
84
+ || (explicitType === "summary_request" && explicitSummaryBot && explicitSummaryBot !== normalizedCurrentBotSelector);
85
+ if (hasExplicitFollowup) {
86
+ return explicitContract;
87
+ }
88
+
89
+ const sessionSpeakerCounts = safeObject(safeObject(context.session).speaker_counts);
90
+ const projectedSpeakerCounts = {
91
+ ...sessionSpeakerCounts,
92
+ [normalizedCurrentBotSelector]: intFromRawAllowZero(sessionSpeakerCounts[normalizedCurrentBotSelector], 0) + 1,
93
+ };
94
+ const remainingPeerResponders = normalizedAllowedResponders.filter((selector) => (
95
+ selector
96
+ && selector !== normalizedCurrentBotSelector
97
+ && intFromRawAllowZero(projectedSpeakerCounts[selector], 0) <= 0
98
+ ));
99
+ if (remainingPeerResponders.length > 0) {
100
+ return {
101
+ type: "delegation",
102
+ actionable: true,
103
+ assignments: remainingPeerResponders.map((targetBot) => ({ targetBot })),
104
+ summaryBot: explicitSummaryBot,
105
+ nextResponders: remainingPeerResponders,
106
+ implicitFollowup: true,
107
+ };
108
+ }
109
+
110
+ const stage = String(context.stage || "").trim().toLowerCase();
111
+ if (
112
+ stage === "bot_reply"
113
+ && explicitSummaryBot
114
+ && explicitSummaryBot !== normalizedCurrentBotSelector
115
+ && normalizedAllowedResponders.includes(explicitSummaryBot)
116
+ ) {
117
+ return {
118
+ type: "summary_request",
119
+ actionable: true,
120
+ assignments: [],
121
+ summaryBot: explicitSummaryBot,
122
+ nextResponders: [explicitSummaryBot],
123
+ implicitFollowup: true,
124
+ };
125
+ }
126
+
127
+ return explicitContract;
128
+ }
129
+
24
130
  export async function prepareRunnerSelectedRecordContractContext({
25
131
  selectedRecord,
26
132
  routeState,
@@ -64,7 +170,7 @@ export async function prepareRunnerSelectedRecordContractContext({
64
170
  }
65
171
 
66
172
  const effectiveResponseContractPayload = safeObject(aiPayload?.response_contract);
67
- const executionContract = conversationContext?.mode === "public_multi_bot"
173
+ const normalizedExecutionContract = conversationContext?.mode === "public_multi_bot"
68
174
  ? normalizeConversationExecutionContract(
69
175
  aiResult?.contract,
70
176
  {
@@ -78,6 +184,12 @@ export async function prepareRunnerSelectedRecordContractContext({
78
184
  effectiveResponseContractPayload,
79
185
  { currentBotSelector },
80
186
  );
187
+ const executionContract = buildImplicitPublicMultiBotExecutionContract({
188
+ conversationContext,
189
+ executionContract: normalizedExecutionContract,
190
+ currentBotSelector,
191
+ normalizeMentionSelector,
192
+ });
81
193
  let allowedActionableTypes = new Set(
82
194
  ensureArray(effectiveResponseContractPayload.allowed_contract_types)
83
195
  .map((item) => String(item || "").trim().toLowerCase())
@@ -99,7 +211,6 @@ export async function prepareRunnerSelectedRecordContractContext({
99
211
  && String(conversationContext?.stage || "").trim() === "human_opening"
100
212
  && conversationContext?.allowBotToBot === true
101
213
  && Boolean(currentBotSelector)
102
- && normalizedConversationInitialResponders.length === 1
103
214
  && normalizedConversationInitialResponders.includes(currentBotSelector)
104
215
  && peerAllowedResponders.length > 0;
105
216
  const directHumanPeerMap = humanIntentContext?.peerMap instanceof Map
@@ -2951,6 +2951,50 @@ export async function runSelftestRunnerScenarios(push, deps) {
2951
2951
  `status=${String(continuedRequest?.status || "(none)")}`,
2952
2952
  );
2953
2953
 
2954
+ saveBotRunnerState({
2955
+ routes: {
2956
+ [requestRouteKey]: {},
2957
+ },
2958
+ sharedInboxes: {},
2959
+ excludedComments: {},
2960
+ requests: {
2961
+ "request-key-2c": {
2962
+ request_key: "request-key-2c",
2963
+ project_id: selftestProjectID,
2964
+ provider: "telegram",
2965
+ chat_id: "-100123",
2966
+ source_message_id: 721,
2967
+ conversation_id: "conv-request-2c",
2968
+ status: "running",
2969
+ claimed_by_route: requestRouteKey,
2970
+ },
2971
+ },
2972
+ consumedComments: {},
2973
+ });
2974
+ const continuedByNextExpected = markRunnerRequestLifecycle({
2975
+ normalizedRoute: requestRoute,
2976
+ requestKey: "request-key-2c",
2977
+ selectedRecord: {
2978
+ id: "comment-request-finish-2c",
2979
+ parsedArchive: {
2980
+ kind: "bot_reply",
2981
+ chatID: "-100123",
2982
+ messageID: 722,
2983
+ conversationID: "conv-request-2c",
2984
+ },
2985
+ },
2986
+ routeKey: requestRouteKey,
2987
+ outcome: "replied",
2988
+ currentBotSelector: "@RyoAI_bot",
2989
+ executionContractTargets: [],
2990
+ nextExpectedResponders: ["@RyoAI2_bot", "@RyoAI3_bot"],
2991
+ });
2992
+ push(
2993
+ "runner_request_lifecycle_reply_remains_running_with_next_expected_responders",
2994
+ String(continuedByNextExpected?.status || "") === "running",
2995
+ `status=${String(continuedByNextExpected?.status || "(none)")}`,
2996
+ );
2997
+
2954
2998
  saveBotRunnerState({
2955
2999
  routes: {
2956
3000
  [requestRouteKey]: {},
@@ -5450,16 +5494,162 @@ export async function runSelftestRunnerScenarios(push, deps) {
5450
5494
  && String(deliveredConversation[0]?.summaryBotUsername || "") === "ryoai_bot",
5451
5495
  `kind=${String(processed.kind || "(none)")} intent=${String(deliveredConversation[0]?.intentMode || "(none)")} summary=${String(deliveredConversation[0]?.summaryBotUsername || "(none)")}`,
5452
5496
  );
5453
- } catch (err) {
5454
- push("intent_fallback_uses_repeated_managed_mention_for_summary_target", false, String(err?.message || err));
5455
- }
5456
-
5457
- try {
5458
- const deliveredConversation = [];
5459
- const processed = await processRunnerSelectedRecord({
5460
- routeKey: "delegated-single-lead-human-open-key",
5461
- normalizedRoute: normalizeRunnerRoute({
5462
- name: "telegram-monitor-delegated-single-lead-open",
5497
+ } catch (err) {
5498
+ push("intent_fallback_uses_repeated_managed_mention_for_summary_target", false, String(err?.message || err));
5499
+ }
5500
+
5501
+ try {
5502
+ let aiCalls = 0;
5503
+ const deliveredConversation = [];
5504
+ const processed = await processRunnerSelectedRecord({
5505
+ routeKey: "multi-bot-collab-human-opening-synthesizes-next-turn-contract-key",
5506
+ normalizedRoute: normalizeRunnerRoute({
5507
+ name: "telegram-monitor-multi-bot-collab-human-opening-synthesizes-next-turn-contract",
5508
+ project_id: selftestProjectID,
5509
+ provider: "telegram",
5510
+ role: "monitor",
5511
+ role_profile: "monitor",
5512
+ destination_id: "dest-1",
5513
+ destination_label: "Main Room",
5514
+ server_bot_name: "RyoAI_bot",
5515
+ server_bot_id: "bot-lead-1",
5516
+ trigger_policy: {
5517
+ mentions_only: true,
5518
+ direct_messages: true,
5519
+ reply_to_bot_messages: true,
5520
+ },
5521
+ archive_policy: {
5522
+ mirror_replies: true,
5523
+ dedupe_inbound: true,
5524
+ dedupe_outbound: true,
5525
+ skip_bot_messages: true,
5526
+ },
5527
+ dry_run_delivery: true,
5528
+ }),
5529
+ selectedRecord: {
5530
+ id: "comment-multi-bot-collab-human-opening",
5531
+ body: "@RyoAI_bot @RyoAI2_bot @RyoAI3_bot 같이 논의해줘.",
5532
+ threadID: "thread-1",
5533
+ createdAt: "2026-03-17T00:00:00.000Z",
5534
+ updatedAt: "2026-03-17T00:00:00.000Z",
5535
+ parsedArchive: {
5536
+ kind: "telegram_message",
5537
+ body: "@RyoAI_bot @RyoAI2_bot @RyoAI3_bot 같이 논의해줘.",
5538
+ sender: "User",
5539
+ senderName: "User",
5540
+ senderIsBot: false,
5541
+ username: "user1",
5542
+ chatType: "supergroup",
5543
+ mentionUsernames: ["ryoai_bot", "ryoai2_bot", "ryoai3_bot"],
5544
+ messageID: 1901,
5545
+ chatID: "-100123",
5546
+ },
5547
+ },
5548
+ routeState: {},
5549
+ pendingOrdered: [],
5550
+ bot: {
5551
+ id: "bot-lead-1",
5552
+ name: "RyoAI_bot",
5553
+ username: "RyoAI_bot",
5554
+ },
5555
+ destination: {
5556
+ id: "dest-1",
5557
+ label: "Main Room",
5558
+ provider: "telegram",
5559
+ chatID: "-100123",
5560
+ },
5561
+ archiveThread: {
5562
+ threadID: "thread-1",
5563
+ workItemID: "work-item-1",
5564
+ },
5565
+ executionPlan: {
5566
+ mode: "role_profile",
5567
+ roleProfileName: "monitor",
5568
+ roleProfile: {
5569
+ client: "sample",
5570
+ model: "",
5571
+ permissionMode: "read_only",
5572
+ reasoningEffort: "low",
5573
+ },
5574
+ workspaceDir: path.join(os.tmpdir(), "metheus-runner-selftest-multi-bot-collab-kickoff"),
5575
+ workspaceSource: "selftest",
5576
+ usedCommandFallback: false,
5577
+ },
5578
+ runtime: {
5579
+ baseURL: "https://example.test",
5580
+ token: "selftest-token",
5581
+ timeoutSeconds: 30,
5582
+ actor: { user_id: "user-1" },
5583
+ },
5584
+ deps: {
5585
+ saveRunnerRouteState: () => {},
5586
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
5587
+ runRunnerAIExecution: async () => {
5588
+ aiCalls += 1;
5589
+ return {
5590
+ skip: false,
5591
+ reply: "좋아요. 먼저 제 관점을 짧게 정리하고, 다른 봇 의견도 이어서 받겠습니다.",
5592
+ replyToMessageID: 0,
5593
+ };
5594
+ },
5595
+ performLocalBotDelivery: async ({ archiveConversation, archiveConversationContext, archiveExecutionContract }) => {
5596
+ deliveredConversation.push(buildSelftestArchiveConversation({
5597
+ archiveConversation,
5598
+ archiveConversationContext,
5599
+ archiveExecutionContract,
5600
+ }));
5601
+ return {
5602
+ delivery: { dryRun: true, body: {} },
5603
+ archive: {},
5604
+ };
5605
+ },
5606
+ serializeRunnerTriggerPolicy: (value) => value,
5607
+ serializeRunnerArchivePolicy: (value) => value,
5608
+ buildRunnerExecutionDeps: () => ({
5609
+ analyzeHumanConversationIntentWithAI: async () => ({
5610
+ mode: "multi_bot_collab",
5611
+ intent_type: "explanation_query",
5612
+ lead_bot: "ryoai_bot",
5613
+ participants: ["ryoai_bot", "ryoai2_bot", "ryoai3_bot"],
5614
+ initial_responders: ["ryoai_bot", "ryoai2_bot", "ryoai3_bot"],
5615
+ allowed_responders: ["ryoai_bot", "ryoai2_bot", "ryoai3_bot"],
5616
+ summary_bot: "ryoai_bot",
5617
+ allow_bot_to_bot: true,
5618
+ reply_expectation: "informational",
5619
+ }),
5620
+ }),
5621
+ buildRunnerDeliveryDeps: () => ({}),
5622
+ buildRunnerRuntimeDeps: () => ({}),
5623
+ resolveConversationPeerBots: () => [
5624
+ { id: "bot-lead-1", name: "RyoAI_bot" },
5625
+ { id: "bot-peer-1", name: "RyoAI2_bot" },
5626
+ { id: "bot-peer-2", name: "RyoAI3_bot" },
5627
+ ],
5628
+ },
5629
+ });
5630
+ push(
5631
+ "multi_bot_collab_human_opening_synthesizes_shared_next_turn_contract",
5632
+ processed.kind === "replied"
5633
+ && aiCalls === 1
5634
+ && String(deliveredConversation[0]?.mode || "") === "public_multi_bot"
5635
+ && String(deliveredConversation[0]?.executionContract?.type || "") === "delegation"
5636
+ && ensureArray(deliveredConversation[0]?.executionContract?.nextResponders).includes("ryoai2_bot")
5637
+ && ensureArray(deliveredConversation[0]?.executionContract?.nextResponders).includes("ryoai3_bot")
5638
+ && ensureArray(processed.result?.next_expected_responders).includes("ryoai2_bot")
5639
+ && ensureArray(processed.result?.next_expected_responders).includes("ryoai3_bot")
5640
+ && String(processed.result?.response_contract_validation_status || "") === "valid",
5641
+ `kind=${String(processed.kind || "(none)")} ai_calls=${aiCalls} contract=${String(deliveredConversation[0]?.executionContract?.type || "(none)")} next=${JSON.stringify(processed.result?.next_expected_responders || [])} validation=${String(processed.result?.response_contract_validation_status || "(none)")}`,
5642
+ );
5643
+ } catch (err) {
5644
+ push("multi_bot_collab_human_opening_synthesizes_shared_next_turn_contract", false, String(err?.message || err));
5645
+ }
5646
+
5647
+ try {
5648
+ const deliveredConversation = [];
5649
+ const processed = await processRunnerSelectedRecord({
5650
+ routeKey: "delegated-single-lead-human-open-key",
5651
+ normalizedRoute: normalizeRunnerRoute({
5652
+ name: "telegram-monitor-delegated-single-lead-open",
5463
5653
  project_id: selftestProjectID,
5464
5654
  provider: "telegram",
5465
5655
  role: "monitor",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.278",
3
+ "version": "0.2.279",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [