metheus-governance-mcp-cli 0.2.293 → 0.2.295

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/cli.mjs CHANGED
@@ -151,6 +151,9 @@ import {
151
151
  selectPendingArchiveComments,
152
152
  printRunnerResult,
153
153
  } from "./lib/runner-helpers.mjs";
154
+ import {
155
+ normalizeRunnerRecentLocalInboundReceiptMap,
156
+ } from "./lib/runner-local-inbound-receipts.mjs";
154
157
  import {
155
158
  normalizeRunnerConversationDecisionBundle,
156
159
  validateRunnerConversationDecisionBundle,
@@ -1966,88 +1969,6 @@ function prefersRunnerStateRecord(candidate, current) {
1966
1969
  return false;
1967
1970
  }
1968
1971
 
1969
- function normalizeRunnerRecentLocalInboundReceiptRecord(rawReceipt, fallbackKey = "") {
1970
- const receipt = safeObject(rawReceipt);
1971
- const chatID = String(receipt.chat_id || receipt.chatID || "").trim();
1972
- const messageID = intFromRawAllowZero(receipt.message_id ?? receipt.messageID, 0);
1973
- const receiptKey = String(fallbackKey || `${chatID}:${messageID}`).trim();
1974
- if (!receiptKey || !chatID || !(messageID > 0)) {
1975
- return null;
1976
- }
1977
- const normalized = {
1978
- chat_id: chatID,
1979
- message_id: messageID,
1980
- receipt_origin: firstNonEmptyString([
1981
- receipt.receipt_origin,
1982
- receipt.receiptOrigin,
1983
- receipt.source_origin,
1984
- receipt.sourceOrigin,
1985
- "local_telegram_inbound",
1986
- ]),
1987
- receipt_route_key: firstNonEmptyString([
1988
- receipt.receipt_route_key,
1989
- receipt.receiptRouteKey,
1990
- receipt.source_route_key,
1991
- receipt.sourceRouteKey,
1992
- ]),
1993
- receipt_bot_username: normalizeTelegramMentionUsername(
1994
- firstNonEmptyString([
1995
- receipt.receipt_bot_username,
1996
- receipt.receiptBotUsername,
1997
- receipt.source_bot_username,
1998
- receipt.sourceBotUsername,
1999
- ]),
2000
- ),
2001
- kind: firstNonEmptyString([receipt.kind, "telegram_message"]),
2002
- sender: firstNonEmptyString([receipt.sender, receipt.from_name, receipt.fromName]),
2003
- sender_username: firstNonEmptyString([
2004
- receipt.sender_username,
2005
- receipt.senderUsername,
2006
- receipt.from_username,
2007
- receipt.fromUsername,
2008
- ]),
2009
- sender_is_bot: Boolean(receipt.sender_is_bot ?? receipt.senderIsBot ?? false),
2010
- body: firstNonEmptyString([receipt.body, receipt.text]),
2011
- occurred_at: firstNonEmptyString([receipt.occurred_at, receipt.occurredAt]),
2012
- received_at: firstNonEmptyString([receipt.received_at, receipt.receivedAt, new Date().toISOString()]),
2013
- };
2014
- const senderID = firstNonEmptyString([receipt.sender_id, receipt.senderID, receipt.from_id, receipt.fromID]);
2015
- if (senderID) {
2016
- normalized.sender_id = senderID;
2017
- }
2018
- const updateID = intFromRawAllowZero(receipt.update_id ?? receipt.updateID, 0);
2019
- if (updateID > 0) {
2020
- normalized.update_id = updateID;
2021
- }
2022
- const messageThreadID = intFromRawAllowZero(receipt.message_thread_id ?? receipt.messageThreadID, 0);
2023
- if (messageThreadID > 0) {
2024
- normalized.message_thread_id = messageThreadID;
2025
- }
2026
- const replyToMessageID = intFromRawAllowZero(receipt.reply_to_message_id ?? receipt.replyToMessageID, 0);
2027
- if (replyToMessageID > 0) {
2028
- normalized.reply_to_message_id = replyToMessageID;
2029
- }
2030
- const chatType = firstNonEmptyString([receipt.chat_type, receipt.chatType]);
2031
- if (chatType) {
2032
- normalized.chat_type = chatType;
2033
- }
2034
- const chatTitle = firstNonEmptyString([receipt.chat_title, receipt.chatTitle]);
2035
- if (chatTitle) {
2036
- normalized.chat_title = chatTitle;
2037
- }
2038
- return [receiptKey, normalized];
2039
- }
2040
-
2041
- function normalizeRunnerRecentLocalInboundReceiptMap(rawReceipts) {
2042
- const normalizedEntries = [];
2043
- for (const [key, value] of Object.entries(safeObject(rawReceipts))) {
2044
- const normalizedEntry = normalizeRunnerRecentLocalInboundReceiptRecord(value, key);
2045
- if (!normalizedEntry) continue;
2046
- normalizedEntries.push(normalizedEntry);
2047
- }
2048
- return Object.fromEntries(normalizedEntries);
2049
- }
2050
-
2051
1972
  function mergeRunnerStateRecords(preferred, fallback) {
2052
1973
  const primary = safeObject(preferred);
2053
1974
  const secondary = safeObject(fallback);
@@ -10711,10 +10632,10 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10711
10632
  latestRunnerState.consumedComments || latestRunnerState.consumed_comments,
10712
10633
  );
10713
10634
  const requests = normalizeBotRunnerRequests(latestRunnerState.requests);
10714
- const commentsForPending = ensureArray(comments).filter((comment) => {
10715
- const normalizedComment = normalizeArchiveCommentRecord(comment, parseArchivedChatComment);
10716
- const commentID = String(normalizedComment.id || "").trim();
10717
- const parsed = safeObject(normalizedComment.parsedArchive);
10635
+ const commentsForPending = ensureArray(comments).filter((comment) => {
10636
+ const normalizedComment = normalizeArchiveCommentRecord(comment, parseArchivedChatComment);
10637
+ const commentID = String(normalizedComment.id || "").trim();
10638
+ const parsed = safeObject(normalizedComment.parsedArchive);
10718
10639
  const commentKind = String(parsed.kind || "").trim().toLowerCase();
10719
10640
  if (commentID && safeObject(excludedComments)[commentID]) {
10720
10641
  return false;
@@ -10742,22 +10663,45 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
10742
10663
  }
10743
10664
  const sessionMatch = findScopedConversationSessionState(latestRunnerState, normalizedRoute, conversationID);
10744
10665
  return sessionAllowsConversationResponder(sessionMatch.session, currentBotSelector);
10745
- }
10746
- return true;
10747
- });
10666
+ }
10667
+ return true;
10668
+ });
10669
+ const followupRequestsForPending = Object.values(requests).filter((entryRaw) => {
10670
+ const entry = safeObject(entryRaw);
10671
+ const nextExpectedResponders = ensureArray(entry.next_expected_responders)
10672
+ .map((value) => normalizeTelegramMentionUsername(value))
10673
+ .filter(Boolean);
10674
+ if (!nextExpectedResponders.includes(currentBotSelector)) {
10675
+ return false;
10676
+ }
10677
+ if (!isActiveRunnerRequestStatus(entry.status)) {
10678
+ return false;
10679
+ }
10680
+ if (
10681
+ String(entry.project_id || "").trim() !== String(normalizedRoute.projectID || "").trim()
10682
+ || String(entry.provider || "").trim() !== String(normalizedRoute.provider || "").trim()
10683
+ || String(entry.chat_id || "").trim() !== String(destination.chatID || "").trim()
10684
+ ) {
10685
+ return false;
10686
+ }
10687
+ const lastReplyEnvelope = safeObject(entry.last_reply_message_envelope);
10688
+ return intFromRawAllowZero(lastReplyEnvelope.message_id || lastReplyEnvelope.messageID, 0) > 0;
10689
+ });
10748
10690
  const pendingWork = selectRunnerPendingWork({
10749
10691
  comments: commentsForPending,
10750
10692
  importOutcome,
10751
10693
  refreshedState,
10752
- mode,
10694
+ mode,
10753
10695
  parseArchivedChatComment,
10754
10696
  pendingSelectionOptions: {
10755
10697
  maxPendingAgeMs: BOT_RUNNER_PENDING_COMMENT_MAX_AGE_MS,
10756
10698
  },
10757
- deps: {
10758
- applyPendingAgeSelection,
10759
- normalizeArchiveCommentRecord,
10760
- },
10699
+ deps: {
10700
+ applyPendingAgeSelection,
10701
+ normalizeArchiveCommentRecord,
10702
+ },
10703
+ followupRequests: followupRequestsForPending,
10704
+ currentBotSelector,
10761
10705
  });
10762
10706
  const pending = pendingWork.pending;
10763
10707
  if (pending.staleSkippedLatest) {
@@ -2143,6 +2143,11 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
2143
2143
  "Do not mention another managed bot unless the contract explicitly names that bot in assignments or next_responders.",
2144
2144
  "Without a matching contract, mentioned bots will not act.",
2145
2145
  "When delegating to another managed bot, use contract.type=\"delegation\" with actionable=true, assignments, and next_responders.",
2146
+ "Decision bundle consistency rules:",
2147
+ "- If execution_contract_type is \"delegation\", conversation_intent_mode must be \"delegated_single_lead\".",
2148
+ "- If execution_contract_type is \"delegation\", initial_responders must contain only the lead bot. Delegated target bots belong in next_expected_responders, not initial_responders.",
2149
+ "- If multiple bots should each answer the opening human message, do not set should_close_after_reply=true on the first reply. Keep should_close_after_reply=false and list the remaining bots in next_expected_responders until the multi-bot opening is complete.",
2150
+ "- Never combine conversation_intent_mode=\"multi_bot_direct\" with execution_contract_type=\"delegation\".",
2146
2151
  "Each assignment must declare whether it is a conversational contribution or a real execution task.",
2147
2152
  "Use assignment.mode=\"conversation_contribution\" for opinions, discussion, review, comparison, synthesis, greetings, or other room-visible contributions that do not require workspace artifacts.",
2148
2153
  "Use assignment.mode=\"execution_task\" only when the delegated bot must change workspace files, create artifacts, update ctxpack, or produce other concrete project outputs. If it is an execution task, also set artifacts_required=true.",
@@ -0,0 +1,212 @@
1
+ import {
2
+ buildCanonicalHumanInboundKey,
3
+ } from "./runner-helpers.mjs";
4
+
5
+ function safeObject(value) {
6
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
7
+ return {};
8
+ }
9
+ return value;
10
+ }
11
+
12
+ function ensureArray(value) {
13
+ return Array.isArray(value) ? value : [];
14
+ }
15
+
16
+ function intFromRawAllowZero(raw, fallback = 0) {
17
+ if (raw === null || raw === undefined || raw === "") {
18
+ return fallback;
19
+ }
20
+ const parsed = Number.parseInt(String(raw).trim(), 10);
21
+ return Number.isFinite(parsed) ? parsed : fallback;
22
+ }
23
+
24
+ function firstNonEmptyString(values) {
25
+ for (const value of ensureArray(values)) {
26
+ const normalized = String(value ?? "").trim();
27
+ if (normalized) return normalized;
28
+ }
29
+ return "";
30
+ }
31
+
32
+ function normalizeMentionSelector(value) {
33
+ return String(value || "").trim().replace(/^@+/, "").toLowerCase();
34
+ }
35
+
36
+ function buildRunnerInboundTargetSelectorSuffix(selector) {
37
+ const normalizedSelector = normalizeMentionSelector(selector);
38
+ return normalizedSelector ? `:${normalizedSelector}` : "";
39
+ }
40
+
41
+ export function buildRunnerRecentLocalInboundReceiptKey(chatID, messageID) {
42
+ return `${String(chatID || "").trim()}:${intFromRawAllowZero(messageID, 0)}`;
43
+ }
44
+
45
+ export function normalizeRunnerRecentLocalInboundReceiptEntry(rawReceipt, fallbackKey = "") {
46
+ const receipt = safeObject(rawReceipt);
47
+ const chatID = String(receipt.chat_id || receipt.chatID || "").trim();
48
+ const messageID = intFromRawAllowZero(receipt.message_id ?? receipt.messageID, 0);
49
+ const receiptKey = String(fallbackKey || buildRunnerRecentLocalInboundReceiptKey(chatID, messageID)).trim();
50
+ if (!receiptKey || !chatID || !(messageID > 0)) {
51
+ return null;
52
+ }
53
+ const normalized = {
54
+ chat_id: chatID,
55
+ message_id: messageID,
56
+ receipt_origin: firstNonEmptyString([
57
+ receipt.receipt_origin,
58
+ receipt.receiptOrigin,
59
+ receipt.source_origin,
60
+ receipt.sourceOrigin,
61
+ "local_telegram_inbound",
62
+ ]),
63
+ receipt_route_key: firstNonEmptyString([
64
+ receipt.receipt_route_key,
65
+ receipt.receiptRouteKey,
66
+ receipt.source_route_key,
67
+ receipt.sourceRouteKey,
68
+ ]),
69
+ receipt_bot_username: normalizeMentionSelector(
70
+ firstNonEmptyString([
71
+ receipt.receipt_bot_username,
72
+ receipt.receiptBotUsername,
73
+ receipt.source_bot_username,
74
+ receipt.sourceBotUsername,
75
+ ]),
76
+ ),
77
+ kind: firstNonEmptyString([receipt.kind, "telegram_message"]),
78
+ sender: firstNonEmptyString([receipt.sender, receipt.from_name, receipt.fromName]),
79
+ sender_username: firstNonEmptyString([
80
+ receipt.sender_username,
81
+ receipt.senderUsername,
82
+ receipt.from_username,
83
+ receipt.fromUsername,
84
+ ]),
85
+ sender_is_bot: Boolean(receipt.sender_is_bot ?? receipt.senderIsBot ?? false),
86
+ body: firstNonEmptyString([receipt.body, receipt.text]),
87
+ occurred_at: firstNonEmptyString([receipt.occurred_at, receipt.occurredAt]),
88
+ received_at: firstNonEmptyString([receipt.received_at, receipt.receivedAt, new Date().toISOString()]),
89
+ };
90
+ const senderID = firstNonEmptyString([receipt.sender_id, receipt.senderID, receipt.from_id, receipt.fromID]);
91
+ if (senderID) {
92
+ normalized.sender_id = senderID;
93
+ }
94
+ const updateID = intFromRawAllowZero(receipt.update_id ?? receipt.updateID, 0);
95
+ if (updateID > 0) {
96
+ normalized.update_id = updateID;
97
+ }
98
+ const messageThreadID = intFromRawAllowZero(receipt.message_thread_id ?? receipt.messageThreadID, 0);
99
+ if (messageThreadID > 0) {
100
+ normalized.message_thread_id = messageThreadID;
101
+ }
102
+ const replyToMessageID = intFromRawAllowZero(receipt.reply_to_message_id ?? receipt.replyToMessageID, 0);
103
+ if (replyToMessageID > 0) {
104
+ normalized.reply_to_message_id = replyToMessageID;
105
+ }
106
+ const replyToFromUsername = firstNonEmptyString([
107
+ receipt.reply_to_from_username,
108
+ receipt.replyToFromUsername,
109
+ receipt.reply_to_username,
110
+ receipt.replyToUsername,
111
+ ]);
112
+ if (replyToFromUsername) {
113
+ normalized.reply_to_from_username = replyToFromUsername;
114
+ }
115
+ if (receipt.reply_to_from_is_bot === true || receipt.replyToFromIsBot === true) {
116
+ normalized.reply_to_from_is_bot = true;
117
+ }
118
+ const chatType = firstNonEmptyString([receipt.chat_type, receipt.chatType]);
119
+ if (chatType) {
120
+ normalized.chat_type = chatType;
121
+ }
122
+ const chatTitle = firstNonEmptyString([receipt.chat_title, receipt.chatTitle]);
123
+ if (chatTitle) {
124
+ normalized.chat_title = chatTitle;
125
+ }
126
+ const canonicalHumanMessageKey = String(buildCanonicalHumanInboundKey({
127
+ chat_id: normalized.chat_id,
128
+ message_id: normalized.message_id,
129
+ message_thread_id: normalized.message_thread_id,
130
+ reply_to_message_id: normalized.reply_to_message_id,
131
+ kind: normalized.kind,
132
+ sender_id: normalized.sender_id,
133
+ sender_username: normalized.sender_username,
134
+ sender_is_bot: normalized.sender_is_bot === true,
135
+ body: normalized.body,
136
+ occurred_at: normalized.occurred_at,
137
+ canonical_human_message_key: firstNonEmptyString([
138
+ receipt.canonical_human_message_key,
139
+ receipt.canonicalHumanMessageKey,
140
+ ]),
141
+ }) || "").trim();
142
+ if (canonicalHumanMessageKey) {
143
+ normalized.canonical_human_message_key = canonicalHumanMessageKey;
144
+ }
145
+ return [receiptKey, normalized];
146
+ }
147
+
148
+ export function normalizeRunnerRecentLocalInboundReceipt(rawReceipt, fallbackKey = "") {
149
+ const normalizedEntry = normalizeRunnerRecentLocalInboundReceiptEntry(rawReceipt, fallbackKey);
150
+ return normalizedEntry ? safeObject(normalizedEntry[1]) : {};
151
+ }
152
+
153
+ export function normalizeRunnerRecentLocalInboundReceiptMap(rawReceipts) {
154
+ const normalizedEntries = [];
155
+ for (const [key, value] of Object.entries(safeObject(rawReceipts))) {
156
+ const normalizedEntry = normalizeRunnerRecentLocalInboundReceiptEntry(value, key);
157
+ if (!normalizedEntry) continue;
158
+ normalizedEntries.push(normalizedEntry);
159
+ }
160
+ return Object.fromEntries(normalizedEntries);
161
+ }
162
+
163
+ export function buildRunnerLocalInboundReceiptKey(receiptRaw, { resolveTargetSelector = null } = {}) {
164
+ const receipt = normalizeRunnerRecentLocalInboundReceipt(receiptRaw);
165
+ const canonicalHumanKey = String(receipt.canonical_human_message_key || "").trim();
166
+ if (canonicalHumanKey) {
167
+ return `human:${canonicalHumanKey}`;
168
+ }
169
+ const chatID = String(receipt.chat_id || "").trim();
170
+ const messageID = intFromRawAllowZero(receipt.message_id, 0);
171
+ if (!chatID || !(messageID > 0)) {
172
+ return "";
173
+ }
174
+ const targetSelector = typeof resolveTargetSelector === "function"
175
+ ? normalizeMentionSelector(resolveTargetSelector(receiptRaw))
176
+ : normalizeMentionSelector(receipt.receipt_bot_username || "");
177
+ return `${chatID}:${messageID}${buildRunnerInboundTargetSelectorSuffix(targetSelector)}`;
178
+ }
179
+
180
+ export function findRecentTelegramInboundReceipt(rawMap, {
181
+ chatID = "",
182
+ messageID = 0,
183
+ canonicalHumanMessageKey = "",
184
+ } = {}) {
185
+ const normalizedChatID = String(chatID || "").trim();
186
+ const normalizedMessageID = intFromRawAllowZero(messageID, 0);
187
+ const normalizedCanonicalHumanMessageKey = String(canonicalHumanMessageKey || "").trim();
188
+ if (!normalizedChatID || (!(normalizedMessageID > 0) && !normalizedCanonicalHumanMessageKey)) {
189
+ return {};
190
+ }
191
+ if (normalizedMessageID > 0) {
192
+ const exactReceipt = normalizeRunnerRecentLocalInboundReceipt(
193
+ safeObject(safeObject(rawMap)[buildRunnerRecentLocalInboundReceiptKey(normalizedChatID, normalizedMessageID)]),
194
+ );
195
+ if (Object.keys(exactReceipt).length > 0) {
196
+ return exactReceipt;
197
+ }
198
+ }
199
+ if (!normalizedCanonicalHumanMessageKey) {
200
+ return {};
201
+ }
202
+ for (const value of Object.values(safeObject(rawMap))) {
203
+ const normalizedReceipt = normalizeRunnerRecentLocalInboundReceipt(value);
204
+ if (
205
+ String(normalizedReceipt.chat_id || "").trim() === normalizedChatID
206
+ && String(normalizedReceipt.canonical_human_message_key || "").trim() === normalizedCanonicalHumanMessageKey
207
+ ) {
208
+ return normalizedReceipt;
209
+ }
210
+ }
211
+ return {};
212
+ }
@@ -132,6 +132,14 @@ export function validateRunnerConversationDecisionBundle(bundleRaw) {
132
132
  bundle,
133
133
  };
134
134
  }
135
+ if (bundle.should_close_after_reply === true && bundle.initial_responders.length > 1) {
136
+ return {
137
+ ok: false,
138
+ status: "contradictory_multi_initial_close_state",
139
+ reason: "should_close_after_reply cannot be true when multiple initial_responders are expected to answer the opening turn",
140
+ bundle,
141
+ };
142
+ }
135
143
  if (
136
144
  bundle.conversation_intent_mode === "single_bot"
137
145
  && bundle.allowed_responders.length > 1
@@ -154,6 +162,28 @@ export function validateRunnerConversationDecisionBundle(bundleRaw) {
154
162
  bundle,
155
163
  };
156
164
  }
165
+ if (
166
+ bundle.conversation_intent_mode === "delegated_single_lead"
167
+ && bundle.initial_responders.length !== 1
168
+ ) {
169
+ return {
170
+ ok: false,
171
+ status: "invalid_delegated_single_lead_initial_responders",
172
+ reason: "delegated_single_lead requires exactly one initial responder",
173
+ bundle,
174
+ };
175
+ }
176
+ if (
177
+ bundle.execution_contract_type === "delegation"
178
+ && bundle.conversation_intent_mode === "multi_bot_direct"
179
+ ) {
180
+ return {
181
+ ok: false,
182
+ status: "contradictory_delegation_intent_mode",
183
+ reason: "multi_bot_direct cannot be combined with a delegation contract",
184
+ bundle,
185
+ };
186
+ }
157
187
  if (bundle.visible_handoff_required === true && bundle.visible_handoff_targets.length === 0) {
158
188
  return {
159
189
  ok: false,