metheus-governance-mcp-cli 0.2.211 → 0.2.213

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
@@ -2358,6 +2358,16 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2358
2358
  next_expected_responders: ensureArray(entry.next_expected_responders || entry.nextExpectedResponders)
2359
2359
  .map((value) => normalizeTelegramMentionUsername(value))
2360
2360
  .filter(Boolean),
2361
+ ai_reply_generated: boolFromRaw(
2362
+ entry.ai_reply_generated ?? entry.aiReplyGenerated,
2363
+ false,
2364
+ ),
2365
+ ai_reply_generated_at: firstNonEmptyString([entry.ai_reply_generated_at, entry.aiReplyGeneratedAt]),
2366
+ ai_reply_preview: String(entry.ai_reply_preview || entry.aiReplyPreview || "").trim(),
2367
+ delivery_status: String(entry.delivery_status || entry.deliveryStatus || "").trim().toLowerCase(),
2368
+ archive_status: String(entry.archive_status || entry.archiveStatus || "").trim().toLowerCase(),
2369
+ transport_error: String(entry.transport_error || entry.transportError || "").trim(),
2370
+ archive_error: String(entry.archive_error || entry.archiveError || "").trim(),
2361
2371
  normalized_intent: String(entry.normalized_intent || entry.normalizedIntent || "").trim().toLowerCase(),
2362
2372
  status,
2363
2373
  claimed_by_route: String(entry.claimed_by_route || entry.claimedByRoute || "").trim(),
@@ -2764,6 +2774,62 @@ function sessionAllowsConversationResponder(sessionRaw, responderSelectorRaw = "
2764
2774
  return allowedResponders.includes(responderSelector);
2765
2775
  }
2766
2776
 
2777
+ function shouldBypassRunnerStartupLoopForContractFollowup({
2778
+ selectedRecord,
2779
+ bot,
2780
+ conversationSessionFacts = null,
2781
+ }) {
2782
+ const parsed = safeObject(selectedRecord?.parsedArchive);
2783
+ if (String(parsed.kind || "").trim().toLowerCase() !== "bot_reply") {
2784
+ return false;
2785
+ }
2786
+ const currentBotSelector = normalizeTelegramMentionUsername(bot?.username || bot?.name);
2787
+ if (!currentBotSelector) {
2788
+ return false;
2789
+ }
2790
+ const conversationID = String(parsed.conversationID || "").trim();
2791
+ if (!conversationID) {
2792
+ return false;
2793
+ }
2794
+ const executionContractType = String(parsed.executionContractType || "").trim().toLowerCase();
2795
+ if (executionContractType !== "delegation") {
2796
+ return false;
2797
+ }
2798
+ const senderBotSelector = normalizeTelegramMentionUsername(
2799
+ parsed.botUsername
2800
+ || parsed.botName
2801
+ || parsed.username
2802
+ || parsed.sender,
2803
+ );
2804
+ if (!senderBotSelector || senderBotSelector === currentBotSelector) {
2805
+ return false;
2806
+ }
2807
+ const assignmentTargets = ensureArray(parsed.executionContractAssignments)
2808
+ .map((item) => normalizeTelegramMentionUsername(safeObject(item).targetBot || safeObject(item).target_bot))
2809
+ .filter(Boolean);
2810
+ const nextResponders = ensureArray(parsed.executionContractNextResponders)
2811
+ .map((item) => normalizeTelegramMentionUsername(item))
2812
+ .filter(Boolean);
2813
+ const conversationTargetBot = normalizeTelegramMentionUsername(parsed.conversationTargetBotUsername);
2814
+ const currentBotIsExpectedResponder = assignmentTargets.includes(currentBotSelector)
2815
+ || nextResponders.includes(currentBotSelector)
2816
+ || conversationTargetBot === currentBotSelector;
2817
+ if (!currentBotIsExpectedResponder) {
2818
+ return false;
2819
+ }
2820
+ const sessionFacts = safeObject(conversationSessionFacts);
2821
+ const sessionExpectedResponders = ensureArray(sessionFacts.next_expected_responders)
2822
+ .map((item) => normalizeTelegramMentionUsername(item))
2823
+ .filter(Boolean);
2824
+ if (sessionExpectedResponders.length > 0 && !sessionExpectedResponders.includes(currentBotSelector)) {
2825
+ return false;
2826
+ }
2827
+ if (sessionFacts.any_closed === true && sessionFacts.any_open !== true) {
2828
+ return false;
2829
+ }
2830
+ return true;
2831
+ }
2832
+
2767
2833
  function findRunnerRequestsForMessageID(state, normalizedRoute, selectors = {}) {
2768
2834
  const chatID = String(selectors.chatID || "").trim();
2769
2835
  const messageID = intFromRawAllowZero(selectors.messageID, 0);
@@ -4426,6 +4492,13 @@ function markRunnerRequestLifecycle({
4426
4492
  conversationReplyExpectation = "",
4427
4493
  normalizedIntent = "",
4428
4494
  closedReason = "",
4495
+ aiReplyGenerated = false,
4496
+ aiReplyGeneratedAt = "",
4497
+ aiReplyPreview = "",
4498
+ deliveryStatus = "",
4499
+ archiveStatus = "",
4500
+ transportError = "",
4501
+ archiveError = "",
4429
4502
  }) {
4430
4503
  const key = String(requestKey || "").trim();
4431
4504
  if (!key) return null;
@@ -4475,6 +4548,7 @@ function markRunnerRequestLifecycle({
4475
4548
  const nextStatus = (() => {
4476
4549
  if (normalizedOutcome === "claimed") return "claimed";
4477
4550
  if (normalizedOutcome === "running") return "running";
4551
+ if (normalizedOutcome === "delivery_failed_after_generation") return "running";
4478
4552
  if (normalizedOutcome === "replied") {
4479
4553
  return shouldRemainRunningAfterReply ? "running" : "completed";
4480
4554
  }
@@ -4531,6 +4605,15 @@ function markRunnerRequestLifecycle({
4531
4605
  ensureArray(nextExpectedResponders).length ? nextExpectedResponders : existing.next_expected_responders,
4532
4606
  normalizeTelegramMentionUsername,
4533
4607
  ),
4608
+ ai_reply_generated: aiReplyGenerated === true || existing.ai_reply_generated === true,
4609
+ ai_reply_generated_at: aiReplyGenerated === true
4610
+ ? firstNonEmptyString([aiReplyGeneratedAt, existing.ai_reply_generated_at, nowISO])
4611
+ : String(existing.ai_reply_generated_at || "").trim(),
4612
+ ai_reply_preview: String(aiReplyPreview || existing.ai_reply_preview || "").trim(),
4613
+ delivery_status: String(deliveryStatus || existing.delivery_status || "").trim().toLowerCase(),
4614
+ archive_status: String(archiveStatus || existing.archive_status || "").trim().toLowerCase(),
4615
+ transport_error: String(transportError || existing.transport_error || "").trim(),
4616
+ archive_error: String(archiveError || existing.archive_error || "").trim(),
4534
4617
  normalized_intent: nextNormalizedIntent,
4535
4618
  status: nextStatus,
4536
4619
  started_at: firstNonEmptyString([existing.started_at, nowISO]),
@@ -7707,6 +7790,13 @@ async function maybeHandleRunnerStartupLoopCandidate({
7707
7790
  }
7708
7791
  const conversationID = String(parsed.conversationID || "").trim();
7709
7792
  const conversationSessionFacts = collectBotRunnerConversationSessionFacts(normalizedRoute, conversationID);
7793
+ if (shouldBypassRunnerStartupLoopForContractFollowup({
7794
+ selectedRecord,
7795
+ bot,
7796
+ conversationSessionFacts,
7797
+ })) {
7798
+ return null;
7799
+ }
7710
7800
  const adjudication = await resolveRunnerStartupLoopAdjudication({
7711
7801
  selectedRecord,
7712
7802
  pendingOrdered,
@@ -8678,7 +8768,9 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8678
8768
  requestKey: requestClaim.requestKey,
8679
8769
  selectedRecord,
8680
8770
  routeKey,
8681
- outcome: String(processed.result?.outcome || "replied").trim().toLowerCase(),
8771
+ outcome: processed.kind === "delivery_failed"
8772
+ ? "delivery_failed_after_generation"
8773
+ : String(processed.result?.outcome || "replied").trim().toLowerCase(),
8682
8774
  conversationIDRaw: String(processed.result?.conversation_id || "").trim(),
8683
8775
  conversationParticipants: ensureArray(processed.result?.conversation_participants),
8684
8776
  conversationInitialResponders: ensureArray(processed.result?.conversation_initial_responders),
@@ -8694,14 +8786,23 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8694
8786
  currentBotSelector,
8695
8787
  conversationIntentMode: String(processed.result?.conversation_intent_mode || "").trim(),
8696
8788
  normalizedIntent: resolvedIntentType,
8789
+ aiReplyGenerated: processed.result?.ai_reply_generated === true,
8790
+ aiReplyGeneratedAt: String(processed.result?.ai_reply_generated_at || "").trim(),
8791
+ aiReplyPreview: String(processed.result?.ai_reply_preview || "").trim(),
8792
+ deliveryStatus: String(processed.result?.delivery_status || "").trim(),
8793
+ archiveStatus: String(processed.result?.archive_status || "").trim(),
8794
+ transportError: String(processed.result?.transport_error || "").trim(),
8795
+ archiveError: String(processed.result?.archive_error || "").trim(),
8697
8796
  });
8698
- await ensureRunnerRootWorkItemForRequest({
8699
- normalizedRoute,
8700
- routeKey,
8701
- selectedRecord,
8702
- runtime,
8703
- requestKey: requestClaim.requestKey,
8704
- });
8797
+ if (processed.kind !== "delivery_failed") {
8798
+ await ensureRunnerRootWorkItemForRequest({
8799
+ normalizedRoute,
8800
+ routeKey,
8801
+ selectedRecord,
8802
+ runtime,
8803
+ requestKey: requestClaim.requestKey,
8804
+ });
8805
+ }
8705
8806
  const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
8706
8807
  normalizedRoute,
8707
8808
  runtime,
@@ -11080,7 +11181,9 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
11080
11181
  requestKey: deferredExecution.requestKey,
11081
11182
  selectedRecord: deferredExecution.selectedRecord,
11082
11183
  routeKey: deferredExecution.routeKey,
11083
- outcome: String(processed.result?.outcome || "replied").trim().toLowerCase(),
11184
+ outcome: processed.kind === "delivery_failed"
11185
+ ? "delivery_failed_after_generation"
11186
+ : String(processed.result?.outcome || "replied").trim().toLowerCase(),
11084
11187
  conversationIDRaw: String(processed.result?.conversation_id || "").trim(),
11085
11188
  conversationParticipants: ensureArray(processed.result?.conversation_participants),
11086
11189
  conversationInitialResponders: ensureArray(processed.result?.conversation_initial_responders),
@@ -11098,14 +11201,23 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
11098
11201
  ),
11099
11202
  conversationIntentMode: String(processed.result?.conversation_intent_mode || "").trim(),
11100
11203
  normalizedIntent: resolvedIntentType,
11204
+ aiReplyGenerated: processed.result?.ai_reply_generated === true,
11205
+ aiReplyGeneratedAt: String(processed.result?.ai_reply_generated_at || "").trim(),
11206
+ aiReplyPreview: String(processed.result?.ai_reply_preview || "").trim(),
11207
+ deliveryStatus: String(processed.result?.delivery_status || "").trim(),
11208
+ archiveStatus: String(processed.result?.archive_status || "").trim(),
11209
+ transportError: String(processed.result?.transport_error || "").trim(),
11210
+ archiveError: String(processed.result?.archive_error || "").trim(),
11101
11211
  });
11102
- await ensureRunnerRootWorkItemForRequest({
11103
- normalizedRoute: deferredExecution.normalizedRoute,
11104
- routeKey: deferredExecution.routeKey,
11105
- selectedRecord: deferredExecution.selectedRecord,
11106
- runtime: deferredExecution.runtime,
11107
- requestKey: deferredExecution.requestKey,
11108
- });
11212
+ if (processed.kind !== "delivery_failed") {
11213
+ await ensureRunnerRootWorkItemForRequest({
11214
+ normalizedRoute: deferredExecution.normalizedRoute,
11215
+ routeKey: deferredExecution.routeKey,
11216
+ selectedRecord: deferredExecution.selectedRecord,
11217
+ runtime: deferredExecution.runtime,
11218
+ requestKey: deferredExecution.requestKey,
11219
+ });
11220
+ }
11109
11221
  const rootWorkItemSync = await syncRunnerRequestRootWorkItemForOutcome({
11110
11222
  normalizedRoute: deferredExecution.normalizedRoute,
11111
11223
  runtime: deferredExecution.runtime,
@@ -15488,6 +15600,7 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
15488
15600
  selectRunnerPendingWork,
15489
15601
  processRunnerSelectedRecord,
15490
15602
  resolveRunnerStartupLoopAdjudication,
15603
+ shouldBypassRunnerStartupLoopForContractFollowup,
15491
15604
  claimRunnerRequestForHumanComment,
15492
15605
  markRunnerRequestLifecycle,
15493
15606
  resolveRunnerContinuationRequestForBotReply,
@@ -1657,6 +1657,9 @@ function inferCurrentTurnPurpose({ trigger, conversation, selfBotUsername, other
1657
1657
  export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1658
1658
  const safePayload = payload && typeof payload === "object" ? payload : {};
1659
1659
  const taskName = String(safePayload.task || "").trim();
1660
+ const agentContext = safePayload.agent_context && typeof safePayload.agent_context === "object"
1661
+ ? safePayload.agent_context
1662
+ : null;
1660
1663
  const trigger = safePayload.trigger && typeof safePayload.trigger === "object" ? safePayload.trigger : {};
1661
1664
  const responseContract = safePayload.response_contract && typeof safePayload.response_contract === "object"
1662
1665
  ? safePayload.response_contract
@@ -1786,6 +1789,9 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1786
1789
  "Current turn purpose:",
1787
1790
  currentTurnPurpose,
1788
1791
  "",
1792
+ "Agent contract/context blob (primary control and state input):",
1793
+ agentContext ? JSON.stringify(agentContext, null, 2) : "-",
1794
+ "",
1789
1795
  "Current user request:",
1790
1796
  String(trigger.body || "").trim() || "-",
1791
1797
  "",
@@ -5406,55 +5406,114 @@ export async function processRunnerSelectedRecord({
5406
5406
  messageID: intFromRawAllowZero(selectedRecord?.parsedArchive?.messageID, 0),
5407
5407
  },
5408
5408
  };
5409
- }
5410
- }
5409
+ }
5410
+ }
5411
+ const aiReplyGeneratedAt = new Date().toISOString();
5412
+ const aiReplyPreview = String(sanitizedReplyText || "").replace(/\s+/g, " ").trim().slice(0, 280);
5411
5413
  emitRunnerStage("delivering", "sending reply and mirroring archive");
5412
- const deliveryResult = await performLocalBotDelivery({
5413
- siteBaseURL: runtime.baseURL,
5414
- token: runtime.token,
5415
- timeoutSeconds: runtime.timeoutSeconds,
5416
- actorUserID: runtime.actor.user_id,
5417
- bot: {
5418
- id: bot.id,
5419
- name: bot.name,
5420
- username: bot.username,
5421
- role: bot.role,
5422
- provider: bot.provider,
5423
- },
5424
- projectID: normalizedRoute.projectID,
5425
- provider: normalizedRoute.provider,
5426
- destinationSelectors: {
5427
- destinationID: destination.id,
5428
- destinationLabel: destination.label,
5429
- },
5430
- text: sanitizedReplyText,
5431
- disableWebPagePreview: true,
5432
- replyToMessageID,
5433
- archiveReplies: normalizedRoute.archivePolicy.mirrorReplies,
5434
- archiveDedupeOutbound: normalizedRoute.archivePolicy.dedupeOutbound,
5435
- archiveThreadID: archiveThread.threadID,
5436
- archiveWorkItemID: archiveThread.workItemID,
5437
- archiveConversation: effectiveConversationContext
5438
- ? {
5439
- id: effectiveConversationContext.id,
5440
- mode: effectiveConversationContext.mode,
5441
- stage: effectiveConversationContext.stage,
5442
- intentMode: effectiveConversationContext.intentMode,
5443
- allowBotToBot: effectiveConversationContext.allowBotToBot === true,
5444
- leadBotUsername: effectiveConversationContext.leadBotUsername,
5445
- summaryBotUsername: effectiveConversationContext.summaryBotUsername,
5446
- participants: effectiveConversationContext.participantSelectors,
5447
- participantSelectors: effectiveConversationContext.participantSelectors,
5448
- initialResponders: effectiveConversationContext.initialResponderSelectors,
5449
- initialResponderSelectors: effectiveConversationContext.initialResponderSelectors,
5450
- allowedResponders: effectiveConversationContext.allowedResponderSelectors,
5451
- allowedResponderSelectors: effectiveConversationContext.allowedResponderSelectors,
5452
- executionContract,
5453
- }
5454
- : null,
5455
- dryRun: normalizedRoute.dryRunDelivery,
5456
- deps: buildRunnerDeliveryDeps(),
5457
- });
5414
+ let deliveryResult;
5415
+ try {
5416
+ deliveryResult = await performLocalBotDelivery({
5417
+ siteBaseURL: runtime.baseURL,
5418
+ token: runtime.token,
5419
+ timeoutSeconds: runtime.timeoutSeconds,
5420
+ actorUserID: runtime.actor.user_id,
5421
+ bot: {
5422
+ id: bot.id,
5423
+ name: bot.name,
5424
+ username: bot.username,
5425
+ role: bot.role,
5426
+ provider: bot.provider,
5427
+ },
5428
+ projectID: normalizedRoute.projectID,
5429
+ provider: normalizedRoute.provider,
5430
+ destinationSelectors: {
5431
+ destinationID: destination.id,
5432
+ destinationLabel: destination.label,
5433
+ },
5434
+ text: sanitizedReplyText,
5435
+ disableWebPagePreview: true,
5436
+ replyToMessageID,
5437
+ archiveReplies: normalizedRoute.archivePolicy.mirrorReplies,
5438
+ archiveDedupeOutbound: normalizedRoute.archivePolicy.dedupeOutbound,
5439
+ archiveThreadID: archiveThread.threadID,
5440
+ archiveWorkItemID: archiveThread.workItemID,
5441
+ archiveConversation: effectiveConversationContext
5442
+ ? {
5443
+ id: effectiveConversationContext.id,
5444
+ mode: effectiveConversationContext.mode,
5445
+ stage: effectiveConversationContext.stage,
5446
+ intentMode: effectiveConversationContext.intentMode,
5447
+ allowBotToBot: effectiveConversationContext.allowBotToBot === true,
5448
+ leadBotUsername: effectiveConversationContext.leadBotUsername,
5449
+ summaryBotUsername: effectiveConversationContext.summaryBotUsername,
5450
+ participants: effectiveConversationContext.participantSelectors,
5451
+ participantSelectors: effectiveConversationContext.participantSelectors,
5452
+ initialResponders: effectiveConversationContext.initialResponderSelectors,
5453
+ initialResponderSelectors: effectiveConversationContext.initialResponderSelectors,
5454
+ allowedResponders: effectiveConversationContext.allowedResponderSelectors,
5455
+ allowedResponderSelectors: effectiveConversationContext.allowedResponderSelectors,
5456
+ executionContract,
5457
+ }
5458
+ : null,
5459
+ dryRun: normalizedRoute.dryRunDelivery,
5460
+ deps: buildRunnerDeliveryDeps(),
5461
+ });
5462
+ } catch (err) {
5463
+ const transportError = String(err?.message || err).trim() || "delivery_failed";
5464
+ saveRunnerRouteState(
5465
+ routeKey,
5466
+ buildRunnerRouteStateFromComment(selectedRecord, {
5467
+ last_action: "delivery_failed_after_generation",
5468
+ last_reason: transportError,
5469
+ last_trigger: String(effectiveTriggerDecision.trigger || "").trim(),
5470
+ last_conversation_id: String(effectiveConversationContext?.id || "").trim(),
5471
+ last_conversation_stage: String(effectiveConversationContext?.stage || "").trim(),
5472
+ last_speaker_bot_username: normalizeMentionSelector(bot?.username || bot?.name),
5473
+ last_workspace_dir: String(effectiveExecutionPlan.workspaceDir || "").trim(),
5474
+ ...intentStatePatch,
5475
+ }),
5476
+ );
5477
+ return {
5478
+ kind: "delivery_failed",
5479
+ result: {
5480
+ route_key: routeKey,
5481
+ route_name: normalizedRoute.name,
5482
+ outcome: "delivery_failed_after_generation",
5483
+ detail: `ai reply generated but delivery failed (${transportError})`,
5484
+ thread_id: archiveThread.threadID,
5485
+ comment_id: selectedRecord.id,
5486
+ trigger_kind: String(effectiveTriggerDecision.trigger || "").trim(),
5487
+ conversation_id: String(effectiveConversationContext?.id || "").trim(),
5488
+ conversation_stage: String(effectiveConversationContext?.stage || "").trim(),
5489
+ conversation_intent_mode: String(effectiveConversationContext?.intentMode || "").trim(),
5490
+ conversation_lead_bot: String(effectiveConversationContext?.leadBotUsername || "").trim(),
5491
+ conversation_summary_bot: String(effectiveConversationContext?.summaryBotUsername || "").trim(),
5492
+ conversation_participants: ensureArray(effectiveConversationContext?.participantSelectors),
5493
+ conversation_initial_responders: ensureArray(effectiveConversationContext?.initialResponderSelectors),
5494
+ conversation_allowed_responders: ensureArray(effectiveConversationContext?.allowedResponderSelectors),
5495
+ conversation_allow_bot_to_bot: effectiveConversationContext?.allowBotToBot === true,
5496
+ conversation_reply_expectation: String(effectiveConversationContext?.replyExpectation || "").trim(),
5497
+ execution_contract_type: String(executionContract?.type || "").trim(),
5498
+ execution_contract_actionable: executionContract?.actionable === true,
5499
+ execution_contract_targets: ensureArray(executionContract?.assignments).map((item) => normalizeMentionSelector(item.targetBot)).filter(Boolean),
5500
+ next_expected_responders: collectExecutionContractNextResponders(executionContract),
5501
+ artifact_validation: String(artifactValidation.status || "").trim() || "none",
5502
+ artifact_paths: summarizeValidatedArtifactPaths(artifactValidation),
5503
+ ai_reply_generated: true,
5504
+ ai_reply_generated_at: aiReplyGeneratedAt,
5505
+ ai_reply_preview: aiReplyPreview,
5506
+ delivery_status: "failed_transport",
5507
+ archive_status: "not_attempted",
5508
+ transport_error: transportError,
5509
+ archive_error: "",
5510
+ reply_chars: String(sanitizedReplyText || "").length,
5511
+ execution_mode: effectiveExecutionPlan.mode,
5512
+ role_profile: effectiveExecutionPlan.roleProfileName,
5513
+ executed_role_plan: executedRolePlan && Object.keys(executedRolePlan).length > 0 ? executedRolePlan : undefined,
5514
+ },
5515
+ };
5516
+ }
5458
5517
  emitRunnerStage("context_suggesting", "evaluating shared project context suggestion");
5459
5518
  const projectContextSuggestion = await maybeCreateSuggestedProjectContext({
5460
5519
  aiResult,
@@ -5629,6 +5688,10 @@ export async function processRunnerSelectedRecord({
5629
5688
  evidence_ids: ensureArray(aiResult?.evidenceItems).map((item) => String(safeObject(item).id || "").trim()).filter(Boolean),
5630
5689
  evidence_paths: ensureArray(aiResult?.evidenceItems).map((item) => String(safeObject(item).path || "").trim()).filter(Boolean),
5631
5690
  reply_chars: String(sanitizedReplyText || "").length,
5691
+ ai_reply_generated: true,
5692
+ ai_reply_generated_at: aiReplyGeneratedAt,
5693
+ ai_reply_preview: aiReplyPreview,
5694
+ delivery_status: deliveryResult.delivery.dryRun ? "dry_run" : "delivered",
5632
5695
  execution_mode: effectiveExecutionPlan.mode,
5633
5696
  role_profile: effectiveExecutionPlan.roleProfileName,
5634
5697
  executed_role_plan: executedRolePlan && Object.keys(executedRolePlan).length > 0 ? executedRolePlan : undefined,
@@ -94,19 +94,7 @@ function buildTriggerCandidateBotUsernames(parsedArchive) {
94
94
  function buildRunnerTaskName(humanIntentType, humanIntentContract = null) {
95
95
  const normalizedIntentType = String(humanIntentType || "").trim().toLowerCase();
96
96
  const contract = safeObject(humanIntentContract);
97
- const replyExpectation = String(contract.replyExpectation || contract.reply_expectation || "").trim().toLowerCase();
98
97
  const executionContractType = String(contract.executionContractType || contract.execution_contract_type || "").trim().toLowerCase();
99
- const intentMode = String(contract.intentMode || contract.intent_mode || "").trim().toLowerCase();
100
- const executionIntentRequested = [
101
- "general_execution",
102
- "ctxpack_mutation",
103
- "workitem_mutation",
104
- ].includes(normalizedIntentType);
105
- const collaborativeIntentMode = [
106
- "delegated_single_lead",
107
- "multi_bot_collab",
108
- "multi_bot_direct",
109
- ].includes(intentMode);
110
98
  if (normalizedIntentType === "status_query") return "answer_project_status_query";
111
99
  if (normalizedIntentType === "workspace_query") return "answer_project_workspace_query";
112
100
  if (normalizedIntentType === "bot_role_query") return "answer_project_bot_role_query";
@@ -115,12 +103,6 @@ function buildRunnerTaskName(humanIntentType, humanIntentContract = null) {
115
103
  if (executionContractType) {
116
104
  return "reply_to_project_actionable_message";
117
105
  }
118
- if (collaborativeIntentMode && replyExpectation === "actionable") {
119
- return "reply_to_project_chat_message";
120
- }
121
- if (executionIntentRequested) {
122
- return "reply_to_project_actionable_message";
123
- }
124
106
  return "reply_to_project_chat_message";
125
107
  }
126
108
 
@@ -199,6 +181,9 @@ export function buildRunnerInputPayload({
199
181
  const directHumanIntent = safeObject(humanIntentContract);
200
182
  const safeDirectQueryReply = safeObject(directQueryReply);
201
183
  const candidateBotUsernames = buildTriggerCandidateBotUsernames(parsed);
184
+ const executionContractType = String(
185
+ directHumanIntent.executionContractType || directHumanIntent.execution_contract_type || "",
186
+ ).trim().toLowerCase();
202
187
  return {
203
188
  task: buildRunnerTaskName(directHumanIntent.intentType, directHumanIntent),
204
189
  project_id: String(route?.projectID || "").trim(),
@@ -279,6 +264,39 @@ export function buildRunnerInputPayload({
279
264
  require_actionable_contract: directHumanIntent.requiresActionableContract === true,
280
265
  allowed_contract_types: ensureArray(directHumanIntent.allowedContractTypes),
281
266
  },
267
+ agent_context: {
268
+ bot_identity: {
269
+ id: String(bot?.id || "").trim(),
270
+ name: String(bot?.name || "").trim(),
271
+ username: String(bot?.username || "").trim(),
272
+ role: String(bot?.role || route?.role || "").trim(),
273
+ },
274
+ request_facts: {
275
+ comment_id: selectedRecord?.id,
276
+ trigger_kind: String(triggerDecision?.trigger || "").trim(),
277
+ body: String(parsed.body || "").trim(),
278
+ message_id: Number.parseInt(String(parsed.messageID ?? 0), 10) || 0,
279
+ reply_to_message_id: Number.parseInt(String(parsed.replyToMessageID ?? 0), 10) || 0,
280
+ },
281
+ conversation_contract: {
282
+ conversation_id: String(directHumanIntent.conversationID || directHumanIntent.conversation_id || parsed.conversationID || "").trim(),
283
+ mode: String(directHumanIntent.intentMode || directHumanIntent.intent_mode || "").trim(),
284
+ intent_type: String(directHumanIntent.intentType || directHumanIntent.intent_type || "").trim(),
285
+ lead_bot: String(directHumanIntent.leadBotSelector || directHumanIntent.lead_bot || "").trim(),
286
+ summary_bot: String(directHumanIntent.summaryBotSelector || directHumanIntent.summary_bot || "").trim(),
287
+ participants: ensureArray(directHumanIntent.participantSelectors || directHumanIntent.participants),
288
+ initial_responders: ensureArray(directHumanIntent.initialResponderSelectors || directHumanIntent.initial_responders),
289
+ allowed_responders: ensureArray(directHumanIntent.allowedResponderSelectors || directHumanIntent.allowed_responders),
290
+ reply_expectation: String(directHumanIntent.replyExpectation || directHumanIntent.reply_expectation || "").trim(),
291
+ execution_contract_type: executionContractType,
292
+ },
293
+ validation_contract: {
294
+ current_bot_is_candidate: true,
295
+ require_actionable_contract: directHumanIntent.requiresActionableContract === true,
296
+ allowed_contract_types: ensureArray(directHumanIntent.allowedContractTypes),
297
+ candidate_bot_usernames: candidateBotUsernames,
298
+ },
299
+ },
282
300
  conversation: conversationContext
283
301
  ? {
284
302
  mode: String(conversationContext.mode || "").trim(),
@@ -118,16 +118,17 @@ export async function runSelftestRunnerScenarios(push, deps) {
118
118
  const safeObject = requireDependency(deps, "safeObject");
119
119
  const normalizeRunnerTriggerPolicy = requireDependency(deps, "normalizeRunnerTriggerPolicy");
120
120
  const evaluateTelegramRunnerTrigger = requireDependency(deps, "evaluateTelegramRunnerTrigger");
121
- const resolveRunnerResponderAdjudication = requireDependency(deps, "resolveRunnerResponderAdjudication");
122
- const selectPendingArchiveComments = requireDependency(deps, "selectPendingArchiveComments");
123
- const selectRunnerPendingWork = requireDependency(deps, "selectRunnerPendingWork");
124
- const processRunnerSelectedRecord = requireDependency(deps, "processRunnerSelectedRecord");
125
- const resolveRunnerStartupLoopAdjudication = requireDependency(deps, "resolveRunnerStartupLoopAdjudication");
126
- const runRunnerAIExecution = requireDependency(deps, "runRunnerAIExecution");
127
- const formatBotReplyArchiveComment = requireDependency(deps, "formatBotReplyArchiveComment");
128
- const findArchivedBotReplyRecord = requireDependency(deps, "findArchivedBotReplyRecord");
129
- const parseArchivedChatComment = requireDependency(deps, "parseArchivedChatComment");
130
- const validateWorkspaceArtifacts = requireDependency(deps, "validateWorkspaceArtifacts");
121
+ const resolveRunnerResponderAdjudication = requireDependency(deps, "resolveRunnerResponderAdjudication");
122
+ const selectPendingArchiveComments = requireDependency(deps, "selectPendingArchiveComments");
123
+ const selectRunnerPendingWork = requireDependency(deps, "selectRunnerPendingWork");
124
+ const processRunnerSelectedRecord = requireDependency(deps, "processRunnerSelectedRecord");
125
+ const resolveRunnerStartupLoopAdjudication = requireDependency(deps, "resolveRunnerStartupLoopAdjudication");
126
+ const shouldBypassRunnerStartupLoopForContractFollowup = requireDependency(deps, "shouldBypassRunnerStartupLoopForContractFollowup");
127
+ const runRunnerAIExecution = requireDependency(deps, "runRunnerAIExecution");
128
+ const formatBotReplyArchiveComment = requireDependency(deps, "formatBotReplyArchiveComment");
129
+ const findArchivedBotReplyRecord = requireDependency(deps, "findArchivedBotReplyRecord");
130
+ const parseArchivedChatComment = requireDependency(deps, "parseArchivedChatComment");
131
+ const validateWorkspaceArtifacts = requireDependency(deps, "validateWorkspaceArtifacts");
131
132
  const buildLocalBotPrompt = requireDependency(deps, "buildLocalBotPrompt");
132
133
 
133
134
  try {
@@ -367,13 +368,53 @@ export async function runSelftestRunnerScenarios(push, deps) {
367
368
  && adjudication?.startup_facts?.repeated_same_delegation_without_new_human === true,
368
369
  `decision=${String(adjudication?.decision || "")} action=${String(adjudication?.action || "")} imported=${String(adjudication?.startup_facts?.imported_on_current_poll)} repeated_delegation=${String(adjudication?.startup_facts?.repeated_same_delegation_without_new_human)}`,
369
370
  );
370
- } catch (err) {
371
- push(
372
- "runner_loop_adjudication_detects_repeated_same_delegation_without_new_human_input",
373
- false,
374
- String(err?.message || err),
375
- );
376
- }
371
+ } catch (err) {
372
+ push(
373
+ "runner_loop_adjudication_detects_repeated_same_delegation_without_new_human_input",
374
+ false,
375
+ String(err?.message || err),
376
+ );
377
+ }
378
+
379
+ try {
380
+ const shouldBypass = shouldBypassRunnerStartupLoopForContractFollowup({
381
+ selectedRecord: {
382
+ id: "bot-reply-4",
383
+ parsedArchive: {
384
+ kind: "bot_reply",
385
+ messageID: 52,
386
+ senderIsBot: true,
387
+ botUsername: "@bot-a",
388
+ conversationID: "conv-4",
389
+ executionContractType: "delegation",
390
+ executionContractAssignments: [
391
+ { targetBot: "@bot-b", task: "say hello" },
392
+ ],
393
+ executionContractNextResponders: ["@bot-b"],
394
+ conversationTargetBotUsername: "@bot-b",
395
+ },
396
+ },
397
+ bot: {
398
+ username: "@bot-b",
399
+ },
400
+ conversationSessionFacts: {
401
+ any_open: true,
402
+ any_closed: false,
403
+ next_expected_responders: ["bot-b"],
404
+ },
405
+ });
406
+ push(
407
+ "runner_startup_loop_bypasses_expected_delegation_followup_for_target_bot",
408
+ shouldBypass === true,
409
+ `should_bypass=${String(shouldBypass)}`,
410
+ );
411
+ } catch (err) {
412
+ push(
413
+ "runner_startup_loop_bypasses_expected_delegation_followup_for_target_bot",
414
+ false,
415
+ String(err?.message || err),
416
+ );
417
+ }
377
418
 
378
419
  try {
379
420
  const pendingSelection = selectPendingArchiveComments(
@@ -11944,12 +11985,133 @@ export async function runSelftestRunnerScenarios(push, deps) {
11944
11985
  && !/project-operating-guide\.md/i.test(deliveredText),
11945
11986
  `kind=${String(processed.kind || "(none)")} outcome=${String(processed.result?.outcome || "(none)")} delivery_calls=${deliveryCalls} delivered=${deliveredText} detail=${String(processed.result?.detail || "(none)")}`,
11946
11987
  );
11947
- } catch (err) {
11948
- push("runner_direct_failure_reply_uses_generic_message_for_unobserved_reported_artifacts", false, String(err?.message || err));
11949
- }
11950
-
11951
- try {
11952
- const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-selftest-observed-artifacts-"));
11988
+ } catch (err) {
11989
+ push("runner_direct_failure_reply_uses_generic_message_for_unobserved_reported_artifacts", false, String(err?.message || err));
11990
+ }
11991
+
11992
+ try {
11993
+ const processed = await processRunnerSelectedRecord({
11994
+ routeKey: "delivery-failed-after-generation-key",
11995
+ normalizedRoute: normalizeRunnerRoute({
11996
+ name: "telegram-monitor-delivery-failed-after-generation",
11997
+ project_id: selftestProjectID,
11998
+ provider: "telegram",
11999
+ role: "monitor",
12000
+ role_profile: "monitor",
12001
+ destination_id: "dest-1",
12002
+ destination_label: "Main Room",
12003
+ server_bot_name: "RyoAI_bot",
12004
+ server_bot_id: "bot-1",
12005
+ trigger_policy: {
12006
+ mentions_only: true,
12007
+ direct_messages: true,
12008
+ reply_to_bot_messages: true,
12009
+ },
12010
+ archive_policy: {
12011
+ mirror_replies: true,
12012
+ dedupe_inbound: true,
12013
+ dedupe_outbound: true,
12014
+ skip_bot_messages: true,
12015
+ },
12016
+ dry_run_delivery: false,
12017
+ }),
12018
+ selectedRecord: {
12019
+ id: "comment-delivery-failed-after-generation",
12020
+ createdAt: "2026-03-17T00:00:21.000Z",
12021
+ parsedArchive: {
12022
+ kind: "telegram_message",
12023
+ chatID: "-100123",
12024
+ chatType: "supergroup",
12025
+ body: "@RyoAI_bot hi",
12026
+ messageID: 126,
12027
+ sender: "human",
12028
+ senderIsBot: false,
12029
+ mentionUsernames: ["ryoai_bot"],
12030
+ },
12031
+ },
12032
+ pendingOrdered: [],
12033
+ bot: {
12034
+ id: "bot-1",
12035
+ name: "RyoAI_bot",
12036
+ username: "RyoAI_bot",
12037
+ role: "monitor",
12038
+ provider: "telegram",
12039
+ },
12040
+ destination: {
12041
+ id: "dest-1",
12042
+ label: "Main Room",
12043
+ provider: "telegram",
12044
+ chatID: "-100123",
12045
+ },
12046
+ archiveThread: {
12047
+ threadID: "thread-1",
12048
+ workItemID: "work-item-1",
12049
+ },
12050
+ executionPlan: {
12051
+ mode: "role_profile",
12052
+ roleProfileName: "monitor",
12053
+ roleProfile: {
12054
+ client: "sample",
12055
+ model: "",
12056
+ permissionMode: "read_only",
12057
+ reasoningEffort: "low",
12058
+ },
12059
+ workspaceDir: "",
12060
+ workspaceSource: "selftest",
12061
+ usedCommandFallback: false,
12062
+ },
12063
+ runtime: {
12064
+ baseURL: "https://example.test",
12065
+ token: "selftest-token",
12066
+ timeoutSeconds: 30,
12067
+ actor: { user_id: "user-1" },
12068
+ },
12069
+ deps: {
12070
+ saveRunnerRouteState: () => {},
12071
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
12072
+ runRunnerAIExecution: async () => ({
12073
+ skip: false,
12074
+ reply: "Hello from RyoAI_bot.",
12075
+ }),
12076
+ performLocalBotDelivery: async () => {
12077
+ throw new Error("read ECONNRESET");
12078
+ },
12079
+ serializeRunnerTriggerPolicy: (value) => value,
12080
+ serializeRunnerArchivePolicy: (value) => value,
12081
+ buildRunnerExecutionDeps: () => ({
12082
+ validateWorkspaceArtifacts,
12083
+ analyzeHumanConversationIntentWithAI: async () => ({
12084
+ mode: "single_bot",
12085
+ lead_bot: "ryoai_bot",
12086
+ participants: ["ryoai_bot"],
12087
+ initial_responders: ["ryoai_bot"],
12088
+ allowed_responders: ["ryoai_bot"],
12089
+ summary_bot: "",
12090
+ allow_bot_to_bot: false,
12091
+ reply_expectation: "informational",
12092
+ intent_type: "small_talk",
12093
+ }),
12094
+ }),
12095
+ buildRunnerDeliveryDeps: () => ({}),
12096
+ buildRunnerRuntimeDeps: () => ({}),
12097
+ resolveConversationPeerBots: () => [],
12098
+ },
12099
+ });
12100
+ push(
12101
+ "runner_delivery_failure_after_generation_records_ai_state_without_execution_error",
12102
+ processed.kind === "delivery_failed"
12103
+ && String(processed.result?.outcome || "") === "delivery_failed_after_generation"
12104
+ && processed.result?.ai_reply_generated === true
12105
+ && String(processed.result?.delivery_status || "") === "failed_transport"
12106
+ && /ECONNRESET/i.test(String(processed.result?.transport_error || "")),
12107
+ `kind=${String(processed.kind || "(none)")} outcome=${String(processed.result?.outcome || "(none)")} ai_generated=${String(processed.result?.ai_reply_generated || false)} delivery=${String(processed.result?.delivery_status || "(none)")} transport=${String(processed.result?.transport_error || "(none)")}`,
12108
+ );
12109
+ } catch (err) {
12110
+ push("runner_delivery_failure_after_generation_records_ai_state_without_execution_error", false, String(err?.message || err));
12111
+ }
12112
+
12113
+ try {
12114
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-selftest-observed-artifacts-"));
11953
12115
  const scriptDir = path.join(workspaceDir, ".metheus", "runner-runtime", "local-ai-scratch");
11954
12116
  fs.mkdirSync(scriptDir, { recursive: true });
11955
12117
  const scriptPath = path.join(scriptDir, "emit-artifact.js");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.211",
3
+ "version": "0.2.213",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [