metheus-governance-mcp-cli 0.2.206 → 0.2.208

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
@@ -16,6 +16,7 @@ import {
16
16
  analyzeHumanConversationIntentWithAI,
17
17
  auditRoleExecutionPlanWithAI,
18
18
  auditDirectHumanReplyWithAI,
19
+ explainExecutionFailureWithAI,
19
20
  normalizeExecutionArtifacts,
20
21
  planRoleExecutionWithAI,
21
22
  repairRoleExecutionPlanWithAI,
@@ -2622,7 +2623,7 @@ function inferRunnerRequestClaimIntent(selectedRecord) {
2622
2623
  if (looksLikeRunnerClaimQuestion(rawText) && normalizedText.split(/\s+/).filter(Boolean).length <= 8) {
2623
2624
  return "status_query";
2624
2625
  }
2625
- return "general_execution";
2626
+ return "";
2626
2627
  }
2627
2628
 
2628
2629
  function resolveRunnerRequestClaimIntent({
@@ -2646,6 +2647,16 @@ function buildSyntheticReplyChainConversationID(normalizedRoute, chatID, anchorM
2646
2647
  return `reply_chain:${provider}:${normalizedChatID}:${normalizedAnchorMessageID}`;
2647
2648
  }
2648
2649
 
2650
+ function buildSyntheticHumanOpeningConversationID(normalizedRoute, chatID, messageID) {
2651
+ const provider = String(normalizedRoute?.provider || "").trim() || "unknown";
2652
+ const normalizedChatID = String(chatID || "").trim() || "-";
2653
+ const normalizedMessageID = intFromRawAllowZero(messageID, 0);
2654
+ if (normalizedMessageID <= 0) {
2655
+ return "";
2656
+ }
2657
+ return `human_opening:${provider}:${normalizedChatID}:${normalizedMessageID}`;
2658
+ }
2659
+
2649
2660
  function findRunnerRequestsForScope(state, normalizedRoute, selectors = {}) {
2650
2661
  const requests = normalizeBotRunnerRequests(state?.requests);
2651
2662
  const projectID = String(normalizedRoute?.projectID || "").trim();
@@ -3258,13 +3269,17 @@ async function claimRunnerRequestForHumanComment({
3258
3269
  normalizedIntent,
3259
3270
  selectedRecord,
3260
3271
  });
3261
- const requestKey = buildRunnerRequestKey({
3272
+ let stateForClaim = safeObject(replyChainResolution.state);
3273
+ const normalizedSharedHumanIntent = safeObject(sharedHumanIntent);
3274
+ const provisionalNormalizedIntent = String(
3275
+ normalizedSharedHumanIntent.intentType || resolvedNormalizedIntent || "",
3276
+ ).trim().toLowerCase();
3277
+ const provisionalRequestKey = buildRunnerRequestKey({
3262
3278
  normalizedRoute,
3263
3279
  selectedRecord,
3264
3280
  selectedBotUsernames,
3265
- normalizedIntent: resolvedNormalizedIntent,
3281
+ normalizedIntent: provisionalNormalizedIntent,
3266
3282
  });
3267
- let stateForClaim = safeObject(replyChainResolution.state);
3268
3283
  const baseConversationID = String(
3269
3284
  parsed.conversationID
3270
3285
  || replyChainContext.conversationID
@@ -3284,9 +3299,6 @@ async function claimRunnerRequestForHumanComment({
3284
3299
  requests: backfilled.requests,
3285
3300
  };
3286
3301
  }
3287
- const requests = normalizeBotRunnerRequests(stateForClaim.requests);
3288
- const existing = safeObject(requests[requestKey]);
3289
- const normalizedSharedHumanIntent = safeObject(sharedHumanIntent);
3290
3302
  const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
3291
3303
  let sharedConversationSource = currentMessageID > 0
3292
3304
  ? pickRunnerSharedConversationSourceRequest(
@@ -3294,7 +3306,7 @@ async function claimRunnerRequestForHumanComment({
3294
3306
  chatID: String(parsed.chatID || parsed.chatId || "").trim(),
3295
3307
  messageID: currentMessageID,
3296
3308
  }),
3297
- requestKey,
3309
+ provisionalRequestKey,
3298
3310
  )
3299
3311
  : {};
3300
3312
  if (
@@ -3308,14 +3320,38 @@ async function claimRunnerRequestForHumanComment({
3308
3320
  runtime,
3309
3321
  chatID: String(parsed.chatID || parsed.chatId || "").trim(),
3310
3322
  messageID: currentMessageID,
3311
- excludeRequestKey: requestKey,
3323
+ excludeRequestKey: provisionalRequestKey,
3312
3324
  }));
3313
3325
  }
3314
3326
  const resolvedConversationID = String(
3315
3327
  baseConversationID
3316
3328
  || sharedConversationSource.conversation_id
3329
+ || (
3330
+ Object.keys(normalizedSharedHumanIntent).length > 0
3331
+ ? buildSyntheticHumanOpeningConversationID(
3332
+ normalizedRoute,
3333
+ String(parsed.chatID || parsed.chatId || "").trim(),
3334
+ currentMessageID,
3335
+ )
3336
+ : ""
3337
+ )
3317
3338
  || "",
3318
3339
  ).trim();
3340
+ const preferredNormalizedIntent = String(
3341
+ normalizedSharedHumanIntent.intentType
3342
+ || sharedConversationSource.normalized_intent
3343
+ || referencedRequest.normalized_intent
3344
+ || resolvedNormalizedIntent
3345
+ || "",
3346
+ ).trim().toLowerCase();
3347
+ const requestKey = buildRunnerRequestKey({
3348
+ normalizedRoute,
3349
+ selectedRecord,
3350
+ selectedBotUsernames,
3351
+ normalizedIntent: preferredNormalizedIntent,
3352
+ });
3353
+ const requests = normalizeBotRunnerRequests(stateForClaim.requests);
3354
+ const existing = safeObject(requests[requestKey]);
3319
3355
  if (isFinalRunnerRequestStatus(existing.status)) {
3320
3356
  return {
3321
3357
  ok: false,
@@ -3411,10 +3447,10 @@ async function claimRunnerRequestForHumanComment({
3411
3447
  ? sharedConversationSource.next_expected_responders
3412
3448
  : ensureArray(referencedRequest.next_expected_responders).length
3413
3449
  ? referencedRequest.next_expected_responders
3414
- : normalizedSharedHumanIntent.initialResponderSelectors,
3450
+ : [],
3415
3451
  normalizeTelegramMentionUsername,
3416
3452
  ),
3417
- normalized_intent: resolvedNormalizedIntent,
3453
+ normalized_intent: preferredNormalizedIntent,
3418
3454
  status: "claimed",
3419
3455
  claimed_by_route: String(routeKey || "").trim(),
3420
3456
  claimed_at: firstNonEmptyString([existing.claimed_at, nowISO]) || nowISO,
@@ -8463,6 +8499,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8463
8499
  saveRunnerRouteState,
8464
8500
  startRunnerTypingHeartbeat,
8465
8501
  runRunnerAIExecution,
8502
+ explainExecutionFailureWithAI,
8466
8503
  performLocalBotDelivery,
8467
8504
  serializeRunnerTriggerPolicy,
8468
8505
  serializeRunnerArchivePolicy,
@@ -10833,6 +10870,7 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
10833
10870
  saveRunnerRouteState,
10834
10871
  startRunnerTypingHeartbeat,
10835
10872
  runRunnerAIExecution,
10873
+ explainExecutionFailureWithAI,
10836
10874
  performLocalBotDelivery,
10837
10875
  serializeRunnerTriggerPolicy,
10838
10876
  serializeRunnerArchivePolicy,
@@ -949,7 +949,7 @@ function normalizeExecutionContract(rawContract) {
949
949
  || contract.target_summary_bot
950
950
  || "",
951
951
  ).trim().replace(/^@+/, "").toLowerCase();
952
- const nextResponders = uniqueOrdered(
952
+ const explicitNextResponders = uniqueOrdered(
953
953
  ensureArray(contract.next_responders || contract.nextResponders || contract.responders)
954
954
  .map((item) => String(item || "").trim().replace(/^@+/, "").toLowerCase())
955
955
  .filter(Boolean),
@@ -962,6 +962,13 @@ function normalizeExecutionContract(rawContract) {
962
962
  ].includes(type)
963
963
  ? type
964
964
  : "";
965
+ const nextResponders = explicitNextResponders.length > 0
966
+ ? explicitNextResponders
967
+ : normalizedType === "delegation"
968
+ ? uniqueOrdered(assignments.map((item) => String(item.target_bot || item.targetBot || "").trim().replace(/^@+/, "").toLowerCase()).filter(Boolean))
969
+ : normalizedType === "summary_request" && summaryBot
970
+ ? [summaryBot]
971
+ : [];
965
972
  const actionable = contract.actionable === true
966
973
  || (normalizedType === "delegation" && assignments.length > 0)
967
974
  || (normalizedType === "summary_request" && Boolean(summaryBot))
@@ -1269,6 +1276,29 @@ export function resolveResponderAdjudicatorModelDisplayName({
1269
1276
  ).trim();
1270
1277
  }
1271
1278
 
1279
+ export function resolveFailureExplainerModelDisplayName({
1280
+ client = "",
1281
+ model = "",
1282
+ env = process.env,
1283
+ } = {}) {
1284
+ const explainerClient = normalizeLocalAIClientName(
1285
+ String(
1286
+ client
1287
+ || env?.METHEUS_FAILURE_EXPLAINER_CLIENT
1288
+ || env?.METHEUS_INTENT_PARSER_CLIENT
1289
+ || "",
1290
+ ).trim(),
1291
+ "gpt",
1292
+ );
1293
+ return String(
1294
+ model
1295
+ || env?.METHEUS_FAILURE_EXPLAINER_MODEL
1296
+ || env?.METHEUS_INTENT_PARSER_MODEL
1297
+ || defaultAdjudicationModelForClient(explainerClient, env)
1298
+ || "",
1299
+ ).trim();
1300
+ }
1301
+
1272
1302
  export function resolveLocalAIExecutionModel(clientName, rawModelValue = "") {
1273
1303
  const modelValue = String(rawModelValue || "").trim();
1274
1304
  if (!modelValue) return "";
@@ -2492,6 +2522,100 @@ export function analyzeHumanConversationIntentWithAI({
2492
2522
  };
2493
2523
  }
2494
2524
 
2525
+ function buildExecutionFailureExplanationPrompt({
2526
+ botName = "",
2527
+ userMessageText = "",
2528
+ failureFacts = null,
2529
+ }) {
2530
+ const compactUserMessage = String(userMessageText || "").trim() || "(not available)";
2531
+ const compactBotName = String(botName || "").trim() || "the bot";
2532
+ return [
2533
+ "You explain runner execution failures to the user.",
2534
+ `The active bot name is: ${compactBotName}`,
2535
+ "Use only the supplied failure facts. Do not invent success, recovered work items, or missing facts.",
2536
+ "Classify the outcome and explain it briefly in the same language as the user's latest message when possible.",
2537
+ "If retryable is true, mention that a retry is reasonable.",
2538
+ "Return a JSON object only with keys:",
2539
+ '{"classification":"failed|retryable_failure|partial_success|needs_user_input|blocked","reply":"string","next_action":"string"}',
2540
+ "",
2541
+ "Latest user message:",
2542
+ compactUserMessage,
2543
+ "",
2544
+ "Failure facts JSON:",
2545
+ JSON.stringify(safeObject(failureFacts), null, 2),
2546
+ ].join("\n");
2547
+ }
2548
+
2549
+ export function explainExecutionFailureWithAI({
2550
+ failureFacts = null,
2551
+ userMessageText = "",
2552
+ botName = "",
2553
+ workspaceDir,
2554
+ client = "",
2555
+ model = "",
2556
+ env = process.env,
2557
+ }) {
2558
+ const explainerClient = normalizeLocalAIClientName(
2559
+ String(
2560
+ client
2561
+ || env?.METHEUS_FAILURE_EXPLAINER_CLIENT
2562
+ || env?.METHEUS_INTENT_PARSER_CLIENT
2563
+ || "",
2564
+ ).trim(),
2565
+ "gpt",
2566
+ );
2567
+ const explainerModel = resolveFailureExplainerModelDisplayName({
2568
+ client: explainerClient,
2569
+ model,
2570
+ env,
2571
+ });
2572
+ const rawText = runLocalAIPromptRawText({
2573
+ client: explainerClient,
2574
+ promptText: buildExecutionFailureExplanationPrompt({
2575
+ botName,
2576
+ userMessageText,
2577
+ failureFacts,
2578
+ }),
2579
+ workspaceDir,
2580
+ model: explainerModel,
2581
+ permissionMode: "read_only",
2582
+ reasoningEffort: String(env?.METHEUS_FAILURE_EXPLAINER_REASONING_EFFORT || "low").trim() || "low",
2583
+ env,
2584
+ });
2585
+ const parsed = tryJsonParse(rawText) || tryParseEmbeddedJsonObject(rawText);
2586
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2587
+ const classificationRaw = String(parsed.classification || "").trim().toLowerCase();
2588
+ const classification = [
2589
+ "failed",
2590
+ "retryable_failure",
2591
+ "partial_success",
2592
+ "needs_user_input",
2593
+ "blocked",
2594
+ ].includes(classificationRaw) ? classificationRaw : "failed";
2595
+ const reply = String(parsed.reply || parsed.message || "").trim();
2596
+ const nextAction = String(parsed.next_action || parsed.nextAction || "").trim();
2597
+ if (!reply) {
2598
+ throw new Error("failure explainer did not return reply text");
2599
+ }
2600
+ return {
2601
+ classification,
2602
+ reply,
2603
+ next_action: nextAction,
2604
+ raw: parsed,
2605
+ };
2606
+ }
2607
+ const plainReply = String(rawText || "").trim();
2608
+ if (!plainReply) {
2609
+ throw new Error("failure explainer returned empty output");
2610
+ }
2611
+ return {
2612
+ classification: "failed",
2613
+ reply: plainReply,
2614
+ next_action: "",
2615
+ raw: null,
2616
+ };
2617
+ }
2618
+
2495
2619
  function normalizeResponderAdjudicationSelectorList(values, allowedSelectors) {
2496
2620
  const allowed = new Set(ensureArray(allowedSelectors).map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase()).filter(Boolean));
2497
2621
  return uniqueOrdered(
@@ -101,11 +101,18 @@ function normalizeExecutionContractForArchive(rawContract) {
101
101
  })
102
102
  .filter(Boolean);
103
103
  const summaryBot = normalizeMentionUsername(contract.summary_bot || contract.summaryBot || "");
104
- const nextResponders = Array.from(new Set(
104
+ const explicitNextResponders = Array.from(new Set(
105
105
  ensureArray(contract.next_responders || contract.nextResponders || contract.responders)
106
106
  .map((item) => normalizeMentionUsername(item))
107
107
  .filter(Boolean),
108
108
  ));
109
+ const nextResponders = explicitNextResponders.length > 0
110
+ ? explicitNextResponders
111
+ : type === "delegation"
112
+ ? Array.from(new Set(assignments.map((item) => normalizeMentionUsername(item.target_bot)).filter(Boolean)))
113
+ : type === "summary_request" && summaryBot
114
+ ? [summaryBot]
115
+ : [];
109
116
  const actionable = contract.actionable === true;
110
117
  if (!type && !assignments.length && !summaryBot && !nextResponders.length && !actionable) {
111
118
  return null;
@@ -431,6 +431,155 @@ function shouldSendExecutionFailureReply({ triggerDecision, selectedRecord }) {
431
431
  && safeObject(selectedRecord?.parsedArchive).senderIsBot !== true;
432
432
  }
433
433
 
434
+ function classifyExecutionFailureFacts(detail) {
435
+ const normalizedDetail = String(detail || "").trim();
436
+ const networkReset = /ECONNRESET|socket hang up|read ECONNRESET/i.test(normalizedDetail);
437
+ const networkTimeout = /ETIMEDOUT|http timeout|ECONNABORTED|aborted/i.test(normalizedDetail);
438
+ const retryable = networkReset || networkTimeout;
439
+ const base = {
440
+ stage: "execution",
441
+ operation: "runner_execution",
442
+ errorType: retryable
443
+ ? (networkTimeout ? "network_timeout" : "network_reset")
444
+ : "execution_failed",
445
+ retryable,
446
+ artifactCreated: null,
447
+ workItemCreated: null,
448
+ ctxpackUpdated: null,
449
+ partialSuccess: false,
450
+ };
451
+ if (!normalizedDetail) {
452
+ return base;
453
+ }
454
+ if (/permission_mode=read_only|read[_ -]?only/i.test(normalizedDetail)) {
455
+ return {
456
+ ...base,
457
+ stage: "permission_check",
458
+ operation: "route_permission_check",
459
+ errorType: "read_only_route",
460
+ retryable: false,
461
+ };
462
+ }
463
+ if (/reply did not produce an actionable execution contract/i.test(normalizedDetail)) {
464
+ return {
465
+ ...base,
466
+ stage: "execution_contract",
467
+ operation: "response_contract_validation",
468
+ errorType: "missing_actionable_contract",
469
+ retryable: false,
470
+ };
471
+ }
472
+ if (/failed to create work item/i.test(normalizedDetail)) {
473
+ return {
474
+ ...base,
475
+ stage: "work_item_create",
476
+ operation: "workitem.push",
477
+ errorType: retryable
478
+ ? (networkTimeout ? "network_timeout" : "network_reset")
479
+ : "work_item_create_failed",
480
+ workItemCreated: false,
481
+ };
482
+ }
483
+ if (/governance work items/i.test(normalizedDetail)) {
484
+ return {
485
+ ...base,
486
+ stage: "work_item_create",
487
+ operation: "governance_work_item_validation",
488
+ errorType: "governance_work_items_missing",
489
+ retryable: false,
490
+ workItemCreated: false,
491
+ };
492
+ }
493
+ if (/validated project artifacts|reported project artifacts that were not observed|artifact path does not exist/i.test(normalizedDetail)) {
494
+ return {
495
+ ...base,
496
+ stage: "artifact_validation",
497
+ operation: "workspace_artifact_validation",
498
+ errorType: "artifact_validation_failed",
499
+ retryable: false,
500
+ artifactCreated: false,
501
+ };
502
+ }
503
+ if (/thread not found/i.test(normalizedDetail)) {
504
+ return {
505
+ ...base,
506
+ stage: "archive_thread",
507
+ operation: "archive_thread_lookup",
508
+ errorType: "archive_thread_missing",
509
+ retryable,
510
+ };
511
+ }
512
+ if (/ctxpack version_id is missing/i.test(normalizedDetail)) {
513
+ return {
514
+ ...base,
515
+ stage: "ctxpack_update",
516
+ operation: "ctxpack.update",
517
+ errorType: "ctxpack_version_missing",
518
+ retryable: false,
519
+ ctxpackUpdated: false,
520
+ };
521
+ }
522
+ if (/ctxpack\.update requires project ctxpack write access|ctxpack write access|ctxpack update permission|forbidden/i.test(normalizedDetail) && /ctxpack/i.test(normalizedDetail)) {
523
+ return {
524
+ ...base,
525
+ stage: "ctxpack_update",
526
+ operation: "ctxpack.update",
527
+ errorType: "ctxpack_permission_denied",
528
+ retryable: false,
529
+ ctxpackUpdated: false,
530
+ };
531
+ }
532
+ if (/ctxpack/i.test(normalizedDetail) && retryable) {
533
+ return {
534
+ ...base,
535
+ stage: "ctxpack_update",
536
+ operation: "ctxpack.update",
537
+ errorType: networkTimeout ? "network_timeout" : "network_reset",
538
+ retryable: true,
539
+ ctxpackUpdated: false,
540
+ };
541
+ }
542
+ if (/ctxpack/i.test(normalizedDetail)) {
543
+ return {
544
+ ...base,
545
+ stage: "ctxpack_update",
546
+ operation: "ctxpack.update",
547
+ errorType: "ctxpack_update_failed",
548
+ retryable: false,
549
+ ctxpackUpdated: false,
550
+ };
551
+ }
552
+ return base;
553
+ }
554
+
555
+ function buildExecutionFailureFacts(detail, options = {}) {
556
+ const normalizedDetail = String(detail || "").trim();
557
+ const compactDetail = normalizedDetail.replace(/\s+/g, " ").trim();
558
+ const normalizedOptions = safeObject(options);
559
+ const intentType = normalizeHumanIntentType(normalizedOptions.intentType);
560
+ const classified = classifyExecutionFailureFacts(compactDetail);
561
+ return {
562
+ stage: classified.stage,
563
+ operation: classified.operation,
564
+ status: "failed",
565
+ error_type: classified.errorType,
566
+ error_message: compactDetail.slice(0, 400),
567
+ retryable: classified.retryable === true,
568
+ artifact_created: classified.artifactCreated,
569
+ work_item_created: classified.workItemCreated,
570
+ ctxpack_updated: classified.ctxpackUpdated,
571
+ partial_success: classified.partialSuccess === true,
572
+ intent_type: intentType,
573
+ informational_request: isInformationalHumanIntentType(intentType),
574
+ execution_mode: String(normalizedOptions.executionMode || "").trim(),
575
+ role_profile: String(normalizedOptions.roleProfileName || "").trim(),
576
+ conversation_id: String(normalizedOptions.conversationID || "").trim(),
577
+ root_work_item_id: String(normalizedOptions.rootWorkItemID || "").trim(),
578
+ request_message_id: intFromRawAllowZero(normalizedOptions.messageID, 0),
579
+ request_text: String(normalizedOptions.userMessageText || "").trim(),
580
+ };
581
+ }
582
+
434
583
  function buildExecutionFailureReplyText(detail, options = {}) {
435
584
  const normalizedDetail = String(detail || "").trim();
436
585
  const intentType = normalizeHumanIntentType(safeObject(options).intentType);
@@ -960,14 +1109,26 @@ function buildHumanIntentFromPersistedRunnerRequest({
960
1109
  .filter((item) => item && (managedSelectors.size === 0 || managedSelectors.has(item))),
961
1110
  );
962
1111
  const intentMode = String(persistedRequest.conversation_intent_mode || "").trim();
963
- const intentType = normalizeHumanIntentType(
1112
+ const persistedReplyExpectation = normalizeReplyExpectation(
1113
+ persistedRequest.conversation_reply_expectation,
1114
+ "",
1115
+ );
1116
+ const persistedExecutionActionable = persistedRequest.execution_contract_actionable === true;
1117
+ const persistedNormalizedIntent = normalizeHumanIntentType(
964
1118
  persistedRequest.normalized_intent || persistedRequest.intent_type,
965
1119
  "",
966
1120
  );
967
- const replyExpectation = normalizeReplyExpectation(
968
- persistedRequest.conversation_reply_expectation,
1121
+ const intentType = normalizeHumanIntentType(
1122
+ (
1123
+ persistedNormalizedIntent === "general_execution"
1124
+ && persistedReplyExpectation !== "actionable"
1125
+ && !persistedExecutionActionable
1126
+ )
1127
+ ? ""
1128
+ : persistedNormalizedIntent,
969
1129
  "",
970
1130
  );
1131
+ const replyExpectation = persistedReplyExpectation;
971
1132
  const participantSelectors = normalizeManagedSelectors(persistedRequest.conversation_participants);
972
1133
  const initialResponderSelectors = normalizeManagedSelectors(persistedRequest.conversation_initial_responders);
973
1134
  const allowedResponderSelectors = normalizeManagedSelectors(persistedRequest.conversation_allowed_responders);
@@ -1112,7 +1273,7 @@ function buildDirectHumanResponseContract({
1112
1273
  }
1113
1274
  }
1114
1275
  const requiresActionableContract = (
1115
- (replyExpectation === "actionable" || intentMode === "delegated_single_lead")
1276
+ replyExpectation === "actionable"
1116
1277
  && Boolean(currentBotSelector)
1117
1278
  && initialResponderSelectors.includes(currentBotSelector)
1118
1279
  );
@@ -3158,11 +3319,18 @@ function normalizeConversationExecutionContract(
3158
3319
  || contract.summary_bot
3159
3320
  || conversationContext?.summaryBotUsername,
3160
3321
  );
3161
- const nextResponders = uniqueOrdered(
3162
- ensureArray(contract.nextResponders || contract.next_responders || contract.responders)
3163
- .map((item) => normalizeMentionSelector(item))
3164
- .filter((item) => !allowedResponderSet.size || allowedResponderSet.has(item)),
3165
- );
3322
+ const explicitNextResponders = uniqueOrdered(
3323
+ ensureArray(contract.nextResponders || contract.next_responders || contract.responders)
3324
+ .map((item) => normalizeMentionSelector(item))
3325
+ .filter((item) => !allowedResponderSet.size || allowedResponderSet.has(item)),
3326
+ );
3327
+ const nextResponders = explicitNextResponders.length > 0
3328
+ ? explicitNextResponders
3329
+ : normalizedType === "delegation"
3330
+ ? uniqueOrdered(assignments.map((item) => normalizeMentionSelector(item.targetBot)).filter(Boolean))
3331
+ : normalizedType === "summary_request" && summaryBot
3332
+ ? [summaryBot]
3333
+ : [];
3166
3334
  const actionable = contract.actionable === true
3167
3335
  || (normalizedType === "delegation" && assignments.length > 0)
3168
3336
  || (normalizedType === "summary_request" && Boolean(summaryBot || nextResponders.length))
@@ -3237,11 +3405,18 @@ function normalizeResponseExecutionContract(rawContract, responseContract, { cur
3237
3405
  contract.summaryBot
3238
3406
  || contract.summary_bot,
3239
3407
  );
3240
- const nextResponders = uniqueOrdered(
3408
+ const explicitNextResponders = uniqueOrdered(
3241
3409
  ensureArray(contract.nextResponders || contract.next_responders || contract.responders)
3242
3410
  .map((item) => normalizeMentionSelector(item))
3243
3411
  .filter(Boolean),
3244
3412
  );
3413
+ const nextResponders = explicitNextResponders.length > 0
3414
+ ? explicitNextResponders
3415
+ : normalizedType === "delegation"
3416
+ ? uniqueOrdered(assignments.map((item) => normalizeMentionSelector(item.targetBot)).filter(Boolean))
3417
+ : normalizedType === "summary_request" && summaryBot
3418
+ ? [summaryBot]
3419
+ : [];
3245
3420
  const actionable = contract.actionable === true
3246
3421
  || (normalizedType === "delegation" && assignments.length > 0)
3247
3422
  || (normalizedType === "summary_request" && Boolean(summaryBot || nextResponders.length))
@@ -3262,7 +3437,7 @@ function normalizeResponseExecutionContract(rawContract, responseContract, { cur
3262
3437
  };
3263
3438
  }
3264
3439
 
3265
- function buildImplicitSummaryRequestContract(conversationContext, currentBotSelector) {
3440
+ function buildImplicitSummaryRequestContract(conversationContext, currentBotSelector) {
3266
3441
  if (String(conversationContext?.mode || "").trim() !== "public_multi_bot") {
3267
3442
  return null;
3268
3443
  }
@@ -3277,15 +3452,45 @@ function buildImplicitSummaryRequestContract(conversationContext, currentBotSele
3277
3452
  if (!summaryBotSelector || !currentSelector || summaryBotSelector === currentSelector) {
3278
3453
  return null;
3279
3454
  }
3280
- return {
3281
- type: "summary_request",
3282
- actionable: true,
3283
- assignments: [],
3284
- summaryBot: summaryBotSelector,
3285
- nextResponders: [summaryBotSelector],
3286
- implicit: true,
3287
- };
3288
- }
3455
+ return {
3456
+ type: "summary_request",
3457
+ actionable: true,
3458
+ assignments: [],
3459
+ summaryBot: summaryBotSelector,
3460
+ nextResponders: [summaryBotSelector],
3461
+ implicit: true,
3462
+ };
3463
+ }
3464
+
3465
+ function collectExecutionContractNextResponders(executionContract) {
3466
+ const contract = safeObject(executionContract);
3467
+ const normalizedType = String(
3468
+ contract.type
3469
+ || contract.contract_type
3470
+ || contract.contractType
3471
+ || "",
3472
+ ).trim().toLowerCase();
3473
+ const explicitNextResponders = uniqueOrdered(
3474
+ ensureArray(contract.nextResponders || contract.next_responders || contract.responders)
3475
+ .map((item) => normalizeMentionSelector(item))
3476
+ .filter(Boolean),
3477
+ );
3478
+ if (explicitNextResponders.length > 0) {
3479
+ return explicitNextResponders;
3480
+ }
3481
+ if (normalizedType === "delegation") {
3482
+ return uniqueOrdered(
3483
+ ensureArray(contract.assignments)
3484
+ .map((item) => normalizeMentionSelector(safeObject(item).targetBot || safeObject(item).target_bot))
3485
+ .filter(Boolean),
3486
+ );
3487
+ }
3488
+ const summaryBot = normalizeMentionSelector(contract.summaryBot || contract.summary_bot);
3489
+ if (normalizedType === "summary_request" && summaryBot) {
3490
+ return [summaryBot];
3491
+ }
3492
+ return [];
3493
+ }
3289
3494
 
3290
3495
  function conversationSessionIsExpired(session, policy, nowMs = Date.now()) {
3291
3496
  const expiresAtMs = parseISOStringMs(safeObject(session).expires_at);
@@ -4176,6 +4381,9 @@ export async function processRunnerSelectedRecord({
4176
4381
  ...safeObject(buildRunnerExecutionDeps()),
4177
4382
  ...safeObject(deps),
4178
4383
  };
4384
+ const explainExecutionFailureWithAI = typeof executionDeps.explainExecutionFailureWithAI === "function"
4385
+ ? executionDeps.explainExecutionFailureWithAI
4386
+ : null;
4179
4387
  const normalizedPrecomputedHumanIntentContext = safeObject(precomputedHumanIntentContext);
4180
4388
  const normalizedPrecomputedHumanIntent = safeObject(normalizedPrecomputedHumanIntentContext.humanIntent);
4181
4389
  const validateWorkspaceArtifacts = typeof executionDeps.validateWorkspaceArtifacts === "function"
@@ -4413,9 +4621,36 @@ export async function processRunnerSelectedRecord({
4413
4621
  if (!shouldSendExecutionFailureReply({ triggerDecision: effectiveTriggerDecision, selectedRecord })) {
4414
4622
  return null;
4415
4623
  }
4416
- const replyText = buildExecutionFailureReplyText(detail, {
4624
+ const failureFacts = buildExecutionFailureFacts(detail, {
4417
4625
  intentType: resolvedIntentType,
4626
+ executionMode: effectiveExecutionPlan.mode,
4627
+ roleProfileName: effectiveExecutionPlan.roleProfileName,
4628
+ conversationID: String(conversationContext?.id || "").trim(),
4629
+ rootWorkItemID: firstNonEmptyString([
4630
+ routeState?.active_root_work_item_id,
4631
+ routeState?.last_root_work_item_id,
4632
+ ]),
4633
+ messageID: selectedRecord?.parsedArchive?.messageID,
4634
+ userMessageText: selectedRecord?.parsedArchive?.body,
4418
4635
  });
4636
+ let replyText = "";
4637
+ if (explainExecutionFailureWithAI) {
4638
+ try {
4639
+ const explanation = await Promise.resolve(explainExecutionFailureWithAI({
4640
+ failureFacts,
4641
+ userMessageText: String(selectedRecord?.parsedArchive?.body || "").trim(),
4642
+ botName: String(bot?.name || bot?.username || bot?.id || "").trim(),
4643
+ workspaceDir: String(effectiveExecutionPlan.workspaceDir || "").trim(),
4644
+ env: process.env,
4645
+ }));
4646
+ replyText = String(safeObject(explanation).reply || safeObject(explanation).message || "").trim();
4647
+ } catch {}
4648
+ }
4649
+ if (!replyText) {
4650
+ replyText = buildExecutionFailureReplyText(detail, {
4651
+ intentType: resolvedIntentType,
4652
+ });
4653
+ }
4419
4654
  if (!replyText) {
4420
4655
  return null;
4421
4656
  }
@@ -4687,6 +4922,24 @@ export async function processRunnerSelectedRecord({
4687
4922
  .filter(Boolean),
4688
4923
  );
4689
4924
  let requiresActionableContract = safeObject(aiPayload.response_contract).require_actionable_contract === true;
4925
+ const normalizedConversationInitialResponders = uniqueOrdered(
4926
+ ensureArray(conversationContext?.initialResponderSelectors)
4927
+ .map((item) => normalizeMentionSelector(item))
4928
+ .filter(Boolean),
4929
+ );
4930
+ const normalizedConversationAllowedResponders = uniqueOrdered(
4931
+ ensureArray(conversationContext?.allowedResponderSelectors)
4932
+ .map((item) => normalizeMentionSelector(item))
4933
+ .filter(Boolean),
4934
+ );
4935
+ const peerAllowedResponders = normalizedConversationAllowedResponders.filter((item) => item && item !== currentBotSelector);
4936
+ const requiresPeerDelegationContract = conversationContext?.mode === "public_multi_bot"
4937
+ && String(conversationContext?.stage || "").trim() === "human_opening"
4938
+ && conversationContext?.allowBotToBot === true
4939
+ && Boolean(currentBotSelector)
4940
+ && normalizedConversationInitialResponders.length === 1
4941
+ && normalizedConversationInitialResponders.includes(currentBotSelector)
4942
+ && peerAllowedResponders.length > 0;
4690
4943
  const directHumanPeerMap = humanIntentContext?.peerMap instanceof Map
4691
4944
  ? humanIntentContext.peerMap
4692
4945
  : buildConversationPeerMap(bot, normalizedRoute, executionDeps);
@@ -4704,6 +4957,10 @@ export async function processRunnerSelectedRecord({
4704
4957
  if (delegatedBotMentions.length > 0) {
4705
4958
  allowedActionableTypes = new Set(["delegation"]);
4706
4959
  }
4960
+ if (requiresPeerDelegationContract) {
4961
+ allowedActionableTypes = new Set(["delegation"]);
4962
+ requiresActionableContract = true;
4963
+ }
4707
4964
  let hasValidActionableContract = Boolean(
4708
4965
  executionContract
4709
4966
  && executionContract.actionable === true
@@ -4744,7 +5001,7 @@ export async function processRunnerSelectedRecord({
4744
5001
  allow_skip: false,
4745
5002
  must_reply: true,
4746
5003
  require_actionable_contract: true,
4747
- human_intent_mode: effectiveResponseContractPayload.human_intent_mode || (delegatedBotMentions.length > 0 ? "delegated_single_lead" : ""),
5004
+ human_intent_mode: effectiveResponseContractPayload.human_intent_mode || ((delegatedBotMentions.length > 0 || requiresPeerDelegationContract) ? "delegated_single_lead" : ""),
4748
5005
  human_lead_bot: delegatedBotMentions.length > 0
4749
5006
  ? currentBotSelector
4750
5007
  : String(effectiveResponseContractPayload.human_lead_bot || "").trim(),
@@ -4755,10 +5012,10 @@ export async function processRunnerSelectedRecord({
4755
5012
  ? ensureArray(effectiveResponseContractPayload.human_allowed_responders)
4756
5013
  : uniqueOrdered([
4757
5014
  currentBotSelector,
4758
- ...delegatedBotMentions,
5015
+ ...(delegatedBotMentions.length > 0 ? delegatedBotMentions : peerAllowedResponders),
4759
5016
  ].filter(Boolean)),
4760
- allow_bot_to_bot: delegatedBotMentions.length > 0 || effectiveResponseContractPayload.human_intent_mode === "delegated_single_lead",
4761
- allowed_contract_types: delegatedBotMentions.length > 0
5017
+ allow_bot_to_bot: delegatedBotMentions.length > 0 || requiresPeerDelegationContract || effectiveResponseContractPayload.human_intent_mode === "delegated_single_lead",
5018
+ allowed_contract_types: delegatedBotMentions.length > 0 || requiresPeerDelegationContract
4762
5019
  ? ["delegation", "summary_request", "final_summary"]
4763
5020
  : ensureArray(effectiveResponseContractPayload.allowed_contract_types).length
4764
5021
  ? ensureArray(effectiveResponseContractPayload.allowed_contract_types)
@@ -5077,10 +5334,7 @@ export async function processRunnerSelectedRecord({
5077
5334
  last_execution_contract_type: String(executionContract?.type || "").trim(),
5078
5335
  last_execution_contract_actionable: executionContract?.actionable === true,
5079
5336
  last_execution_contract_targets: ensureArray(executionContract?.assignments).map((item) => normalizeMentionSelector(item.targetBot)).filter(Boolean),
5080
- next_expected_responders: uniqueOrdered([
5081
- ...ensureArray(executionContract?.nextResponders).map((item) => normalizeMentionSelector(item)).filter(Boolean),
5082
- normalizeMentionSelector(executionContract?.summaryBot),
5083
- ]),
5337
+ next_expected_responders: collectExecutionContractNextResponders(executionContract),
5084
5338
  last_speaker_bot_username: currentBotSelector,
5085
5339
  speaker_counts: speakerCounts,
5086
5340
  last_sender_bot_username: String(effectiveConversationContext?.senderBotUsername || "").trim(),
@@ -5176,10 +5430,7 @@ export async function processRunnerSelectedRecord({
5176
5430
  execution_contract_type: String(executionContract?.type || "").trim(),
5177
5431
  execution_contract_actionable: executionContract?.actionable === true,
5178
5432
  execution_contract_targets: ensureArray(executionContract?.assignments).map((item) => normalizeMentionSelector(item.targetBot)).filter(Boolean),
5179
- next_expected_responders: uniqueOrdered([
5180
- ...ensureArray(executionContract?.nextResponders).map((item) => normalizeMentionSelector(item)).filter(Boolean),
5181
- normalizeMentionSelector(executionContract?.summaryBot),
5182
- ]),
5433
+ next_expected_responders: collectExecutionContractNextResponders(executionContract),
5183
5434
  artifact_validation: String(artifactValidation.status || "").trim() || "none",
5184
5435
  artifact_paths: summarizeValidatedArtifactPaths(artifactValidation),
5185
5436
  ctxpack_version_id: String(safeObject(aiResult?.ctxpackUpdate).version_id || "").trim(),
@@ -1569,9 +1569,9 @@ export async function runSelftestRunnerScenarios(push, deps) {
1569
1569
  });
1570
1570
  const inferredExecutionRequest = safeObject(safeObject(loadBotRunnerState().requests)[inferredExecutionClaim.requestKey]);
1571
1571
  push(
1572
- "runner_request_claim_defaults_actionable_messages_to_general_execution",
1572
+ "runner_request_claim_leaves_unclassified_messages_unresolved",
1573
1573
  inferredExecutionClaim.ok === true
1574
- && String(inferredExecutionRequest.normalized_intent || "") === "general_execution",
1574
+ && String(inferredExecutionRequest.normalized_intent || "") === "",
1575
1575
  `intent=${String(inferredExecutionRequest.normalized_intent || "(none)")} request=${String(inferredExecutionClaim.requestKey || "(none)")}`,
1576
1576
  );
1577
1577
 
@@ -4785,7 +4785,7 @@ export async function runSelftestRunnerScenarios(push, deps) {
4785
4785
  push(
4786
4786
  "direct_human_public_delegate_fallback_uses_response_allowed_responders",
4787
4787
  processed.kind === "replied"
4788
- && aiCalls === 1
4788
+ && aiCalls === 2
4789
4789
  && String(deliveredConversation[0]?.mode || "") === "public_multi_bot"
4790
4790
  && Array.isArray(deliveredConversation[0]?.allowedResponderSelectors)
4791
4791
  && deliveredConversation[0].allowedResponderSelectors.includes("ryoai3_bot")
@@ -5064,14 +5064,14 @@ export async function runSelftestRunnerScenarios(push, deps) {
5064
5064
  },
5065
5065
  });
5066
5066
  push(
5067
- "delegated_single_lead_lead_opening_requires_actionable_contract",
5067
+ "delegated_single_lead_lead_opening_requires_delegation_contract_for_peer_followup",
5068
5068
  processed.kind === "skipped"
5069
5069
  && aiCalls === 2
5070
- && /actionable execution contract/i.test(String(processed.skippedRecord?.reason || "")),
5071
- `kind=${String(processed.kind || "(none)")} ai_calls=${aiCalls} reason=${String(processed.skippedRecord?.reason || "(none)")}`,
5070
+ && !String(deliveredText || "").trim(),
5071
+ `kind=${String(processed.kind || "(none)")} ai_calls=${aiCalls} reply=${String(deliveredText || "(none)")}`,
5072
5072
  );
5073
5073
  } catch (err) {
5074
- push("delegated_single_lead_lead_opening_requires_actionable_contract", false, String(err?.message || err));
5074
+ push("delegated_single_lead_lead_opening_requires_delegation_contract_for_peer_followup", false, String(err?.message || err));
5075
5075
  }
5076
5076
 
5077
5077
  try {
@@ -5207,6 +5207,148 @@ export async function runSelftestRunnerScenarios(push, deps) {
5207
5207
  push("single_bot_human_work_request_requires_actionable_contract", false, String(err?.message || err));
5208
5208
  }
5209
5209
 
5210
+ try {
5211
+ let aiCalls = 0;
5212
+ let deliveryCalls = 0;
5213
+ let deliveredText = "";
5214
+ let capturedFailureFacts = null;
5215
+ const processed = await processRunnerSelectedRecord({
5216
+ routeKey: "single-bot-human-work-request-ai-failure-explainer-key",
5217
+ normalizedRoute: normalizeRunnerRoute({
5218
+ name: "telegram-monitor-single-bot-human-work-request-ai-failure-explainer",
5219
+ project_id: selftestProjectID,
5220
+ provider: "telegram",
5221
+ role: "monitor",
5222
+ role_profile: "monitor",
5223
+ destination_id: "dest-1",
5224
+ destination_label: "Main Room",
5225
+ server_bot_name: "RyoAI_bot",
5226
+ server_bot_id: "bot-lead-1",
5227
+ trigger_policy: {
5228
+ mentions_only: true,
5229
+ direct_messages: true,
5230
+ reply_to_bot_messages: true,
5231
+ },
5232
+ archive_policy: {
5233
+ mirror_replies: true,
5234
+ dedupe_inbound: true,
5235
+ dedupe_outbound: true,
5236
+ skip_bot_messages: true,
5237
+ },
5238
+ dry_run_delivery: true,
5239
+ }),
5240
+ selectedRecord: {
5241
+ id: "comment-single-bot-human-work-request-ai-failure-explainer",
5242
+ createdAt: "2026-03-16T00:02:05.500Z",
5243
+ parsedArchive: {
5244
+ kind: "telegram_message",
5245
+ chatID: "-100123",
5246
+ chatType: "supergroup",
5247
+ senderIsBot: false,
5248
+ body: "@RyoAI_bot update the implementation guide now.",
5249
+ mentionUsernames: ["RyoAI_bot"],
5250
+ messageID: 1206,
5251
+ },
5252
+ },
5253
+ pendingOrdered: [],
5254
+ bot: {
5255
+ id: "bot-lead-1",
5256
+ name: "RyoAI_bot",
5257
+ username: "RyoAI_bot",
5258
+ role: "monitor",
5259
+ provider: "telegram",
5260
+ },
5261
+ destination: {
5262
+ id: "dest-1",
5263
+ label: "Main Room",
5264
+ provider: "telegram",
5265
+ chatID: "-100123",
5266
+ },
5267
+ archiveThread: {
5268
+ threadID: "thread-1",
5269
+ workItemID: "work-item-1",
5270
+ },
5271
+ executionPlan: {
5272
+ mode: "role_profile",
5273
+ roleProfileName: "monitor",
5274
+ roleProfile: {
5275
+ client: "sample",
5276
+ model: "",
5277
+ permissionMode: "read_only",
5278
+ reasoningEffort: "low",
5279
+ },
5280
+ workspaceDir: process.cwd(),
5281
+ workspaceSource: "selftest",
5282
+ usedCommandFallback: false,
5283
+ },
5284
+ runtime: {
5285
+ baseURL: "https://example.test",
5286
+ token: "selftest-token",
5287
+ timeoutSeconds: 30,
5288
+ actor: { user_id: "user-1" },
5289
+ },
5290
+ deps: {
5291
+ saveRunnerRouteState: () => {},
5292
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
5293
+ runRunnerAIExecution: async () => {
5294
+ aiCalls += 1;
5295
+ return {
5296
+ skip: false,
5297
+ reply: "I reviewed the request but did not produce a contract.",
5298
+ contract: null,
5299
+ };
5300
+ },
5301
+ explainExecutionFailureWithAI: ({ failureFacts }) => {
5302
+ capturedFailureFacts = safeObject(failureFacts);
5303
+ return {
5304
+ classification: "failed",
5305
+ reply: "AI failure summary",
5306
+ next_action: "retry",
5307
+ };
5308
+ },
5309
+ performLocalBotDelivery: async ({ text }) => {
5310
+ deliveryCalls += 1;
5311
+ deliveredText = String(text || "");
5312
+ return {
5313
+ delivery: { dryRun: true, body: {} },
5314
+ archive: {},
5315
+ };
5316
+ },
5317
+ serializeRunnerTriggerPolicy: (value) => value,
5318
+ serializeRunnerArchivePolicy: (value) => value,
5319
+ buildRunnerExecutionDeps: () => ({
5320
+ analyzeHumanConversationIntentWithAI: async () => ({
5321
+ mode: "single_bot",
5322
+ lead_bot: "ryoai_bot",
5323
+ participants: ["ryoai_bot"],
5324
+ initial_responders: ["ryoai_bot"],
5325
+ allowed_responders: ["ryoai_bot"],
5326
+ summary_bot: "",
5327
+ allow_bot_to_bot: false,
5328
+ reply_expectation: "actionable",
5329
+ }),
5330
+ }),
5331
+ buildRunnerDeliveryDeps: () => ({}),
5332
+ buildRunnerRuntimeDeps: () => ({}),
5333
+ resolveConversationPeerBots: () => [
5334
+ { id: "bot-lead-1", name: "RyoAI_bot" },
5335
+ ],
5336
+ },
5337
+ });
5338
+ push(
5339
+ "single_bot_execution_failure_uses_ai_failure_explainer_when_available",
5340
+ processed.kind === "error"
5341
+ && aiCalls >= 1
5342
+ && deliveryCalls === 1
5343
+ && deliveredText === "AI failure summary"
5344
+ && String(capturedFailureFacts?.error_type || "") === "missing_actionable_contract"
5345
+ && String(capturedFailureFacts?.stage || "") === "execution_contract",
5346
+ `kind=${String(processed.kind || "(none)")} outcome=${String(processed.result?.outcome || "(none)")} ai_calls=${aiCalls} delivery_calls=${deliveryCalls} delivered=${deliveredText} failure_type=${String(capturedFailureFacts?.error_type || "(none)")} stage=${String(capturedFailureFacts?.stage || "(none)")}`,
5347
+ );
5348
+ } catch (err) {
5349
+ push("single_bot_execution_failure_uses_ai_failure_explainer_when_available", false, String(err?.message || err));
5350
+ }
5351
+
5210
5352
  try {
5211
5353
  let aiCalls = 0;
5212
5354
  const processed = await processRunnerSelectedRecord({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.206",
3
+ "version": "0.2.208",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [