metheus-governance-mcp-cli 0.2.206 → 0.2.207

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,
@@ -8463,6 +8464,7 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8463
8464
  saveRunnerRouteState,
8464
8465
  startRunnerTypingHeartbeat,
8465
8466
  runRunnerAIExecution,
8467
+ explainExecutionFailureWithAI,
8466
8468
  performLocalBotDelivery,
8467
8469
  serializeRunnerTriggerPolicy,
8468
8470
  serializeRunnerArchivePolicy,
@@ -10833,6 +10835,7 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
10833
10835
  saveRunnerRouteState,
10834
10836
  startRunnerTypingHeartbeat,
10835
10837
  runRunnerAIExecution,
10838
+ explainExecutionFailureWithAI,
10836
10839
  performLocalBotDelivery,
10837
10840
  serializeRunnerTriggerPolicy,
10838
10841
  serializeRunnerArchivePolicy,
@@ -1269,6 +1269,29 @@ export function resolveResponderAdjudicatorModelDisplayName({
1269
1269
  ).trim();
1270
1270
  }
1271
1271
 
1272
+ export function resolveFailureExplainerModelDisplayName({
1273
+ client = "",
1274
+ model = "",
1275
+ env = process.env,
1276
+ } = {}) {
1277
+ const explainerClient = normalizeLocalAIClientName(
1278
+ String(
1279
+ client
1280
+ || env?.METHEUS_FAILURE_EXPLAINER_CLIENT
1281
+ || env?.METHEUS_INTENT_PARSER_CLIENT
1282
+ || "",
1283
+ ).trim(),
1284
+ "gpt",
1285
+ );
1286
+ return String(
1287
+ model
1288
+ || env?.METHEUS_FAILURE_EXPLAINER_MODEL
1289
+ || env?.METHEUS_INTENT_PARSER_MODEL
1290
+ || defaultAdjudicationModelForClient(explainerClient, env)
1291
+ || "",
1292
+ ).trim();
1293
+ }
1294
+
1272
1295
  export function resolveLocalAIExecutionModel(clientName, rawModelValue = "") {
1273
1296
  const modelValue = String(rawModelValue || "").trim();
1274
1297
  if (!modelValue) return "";
@@ -2492,6 +2515,100 @@ export function analyzeHumanConversationIntentWithAI({
2492
2515
  };
2493
2516
  }
2494
2517
 
2518
+ function buildExecutionFailureExplanationPrompt({
2519
+ botName = "",
2520
+ userMessageText = "",
2521
+ failureFacts = null,
2522
+ }) {
2523
+ const compactUserMessage = String(userMessageText || "").trim() || "(not available)";
2524
+ const compactBotName = String(botName || "").trim() || "the bot";
2525
+ return [
2526
+ "You explain runner execution failures to the user.",
2527
+ `The active bot name is: ${compactBotName}`,
2528
+ "Use only the supplied failure facts. Do not invent success, recovered work items, or missing facts.",
2529
+ "Classify the outcome and explain it briefly in the same language as the user's latest message when possible.",
2530
+ "If retryable is true, mention that a retry is reasonable.",
2531
+ "Return a JSON object only with keys:",
2532
+ '{"classification":"failed|retryable_failure|partial_success|needs_user_input|blocked","reply":"string","next_action":"string"}',
2533
+ "",
2534
+ "Latest user message:",
2535
+ compactUserMessage,
2536
+ "",
2537
+ "Failure facts JSON:",
2538
+ JSON.stringify(safeObject(failureFacts), null, 2),
2539
+ ].join("\n");
2540
+ }
2541
+
2542
+ export function explainExecutionFailureWithAI({
2543
+ failureFacts = null,
2544
+ userMessageText = "",
2545
+ botName = "",
2546
+ workspaceDir,
2547
+ client = "",
2548
+ model = "",
2549
+ env = process.env,
2550
+ }) {
2551
+ const explainerClient = normalizeLocalAIClientName(
2552
+ String(
2553
+ client
2554
+ || env?.METHEUS_FAILURE_EXPLAINER_CLIENT
2555
+ || env?.METHEUS_INTENT_PARSER_CLIENT
2556
+ || "",
2557
+ ).trim(),
2558
+ "gpt",
2559
+ );
2560
+ const explainerModel = resolveFailureExplainerModelDisplayName({
2561
+ client: explainerClient,
2562
+ model,
2563
+ env,
2564
+ });
2565
+ const rawText = runLocalAIPromptRawText({
2566
+ client: explainerClient,
2567
+ promptText: buildExecutionFailureExplanationPrompt({
2568
+ botName,
2569
+ userMessageText,
2570
+ failureFacts,
2571
+ }),
2572
+ workspaceDir,
2573
+ model: explainerModel,
2574
+ permissionMode: "read_only",
2575
+ reasoningEffort: String(env?.METHEUS_FAILURE_EXPLAINER_REASONING_EFFORT || "low").trim() || "low",
2576
+ env,
2577
+ });
2578
+ const parsed = tryJsonParse(rawText) || tryParseEmbeddedJsonObject(rawText);
2579
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2580
+ const classificationRaw = String(parsed.classification || "").trim().toLowerCase();
2581
+ const classification = [
2582
+ "failed",
2583
+ "retryable_failure",
2584
+ "partial_success",
2585
+ "needs_user_input",
2586
+ "blocked",
2587
+ ].includes(classificationRaw) ? classificationRaw : "failed";
2588
+ const reply = String(parsed.reply || parsed.message || "").trim();
2589
+ const nextAction = String(parsed.next_action || parsed.nextAction || "").trim();
2590
+ if (!reply) {
2591
+ throw new Error("failure explainer did not return reply text");
2592
+ }
2593
+ return {
2594
+ classification,
2595
+ reply,
2596
+ next_action: nextAction,
2597
+ raw: parsed,
2598
+ };
2599
+ }
2600
+ const plainReply = String(rawText || "").trim();
2601
+ if (!plainReply) {
2602
+ throw new Error("failure explainer returned empty output");
2603
+ }
2604
+ return {
2605
+ classification: "failed",
2606
+ reply: plainReply,
2607
+ next_action: "",
2608
+ raw: null,
2609
+ };
2610
+ }
2611
+
2495
2612
  function normalizeResponderAdjudicationSelectorList(values, allowedSelectors) {
2496
2613
  const allowed = new Set(ensureArray(allowedSelectors).map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase()).filter(Boolean));
2497
2614
  return uniqueOrdered(
@@ -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);
@@ -4176,6 +4325,9 @@ export async function processRunnerSelectedRecord({
4176
4325
  ...safeObject(buildRunnerExecutionDeps()),
4177
4326
  ...safeObject(deps),
4178
4327
  };
4328
+ const explainExecutionFailureWithAI = typeof executionDeps.explainExecutionFailureWithAI === "function"
4329
+ ? executionDeps.explainExecutionFailureWithAI
4330
+ : null;
4179
4331
  const normalizedPrecomputedHumanIntentContext = safeObject(precomputedHumanIntentContext);
4180
4332
  const normalizedPrecomputedHumanIntent = safeObject(normalizedPrecomputedHumanIntentContext.humanIntent);
4181
4333
  const validateWorkspaceArtifacts = typeof executionDeps.validateWorkspaceArtifacts === "function"
@@ -4413,9 +4565,36 @@ export async function processRunnerSelectedRecord({
4413
4565
  if (!shouldSendExecutionFailureReply({ triggerDecision: effectiveTriggerDecision, selectedRecord })) {
4414
4566
  return null;
4415
4567
  }
4416
- const replyText = buildExecutionFailureReplyText(detail, {
4568
+ const failureFacts = buildExecutionFailureFacts(detail, {
4417
4569
  intentType: resolvedIntentType,
4570
+ executionMode: effectiveExecutionPlan.mode,
4571
+ roleProfileName: effectiveExecutionPlan.roleProfileName,
4572
+ conversationID: String(conversationContext?.id || "").trim(),
4573
+ rootWorkItemID: firstNonEmptyString([
4574
+ routeState?.active_root_work_item_id,
4575
+ routeState?.last_root_work_item_id,
4576
+ ]),
4577
+ messageID: selectedRecord?.parsedArchive?.messageID,
4578
+ userMessageText: selectedRecord?.parsedArchive?.body,
4418
4579
  });
4580
+ let replyText = "";
4581
+ if (explainExecutionFailureWithAI) {
4582
+ try {
4583
+ const explanation = await Promise.resolve(explainExecutionFailureWithAI({
4584
+ failureFacts,
4585
+ userMessageText: String(selectedRecord?.parsedArchive?.body || "").trim(),
4586
+ botName: String(bot?.name || bot?.username || bot?.id || "").trim(),
4587
+ workspaceDir: String(effectiveExecutionPlan.workspaceDir || "").trim(),
4588
+ env: process.env,
4589
+ }));
4590
+ replyText = String(safeObject(explanation).reply || safeObject(explanation).message || "").trim();
4591
+ } catch {}
4592
+ }
4593
+ if (!replyText) {
4594
+ replyText = buildExecutionFailureReplyText(detail, {
4595
+ intentType: resolvedIntentType,
4596
+ });
4597
+ }
4419
4598
  if (!replyText) {
4420
4599
  return null;
4421
4600
  }
@@ -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.207",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [