metheus-governance-mcp-cli 0.2.146 → 0.2.147

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
@@ -14,6 +14,7 @@ import {
14
14
  analyzeHumanConversationIntentWithAI,
15
15
  auditDirectHumanReplyWithAI,
16
16
  normalizeExecutionArtifacts,
17
+ planRoleExecutionWithAI,
17
18
  resolveLocalAIExecutionModel,
18
19
  resolveGeminiReasoningConfig,
19
20
  suggestLocalAIModelDisplayName,
@@ -143,6 +144,7 @@ import {
143
144
  } from "./lib/runner-delivery.mjs";
144
145
  import {
145
146
  resolveRunnerExecutionPlan,
147
+ resolveRunnerExecutionPlanForRole,
146
148
  resolveRunnerRoleProfile,
147
149
  resolveRunnerWorkspaceSelection,
148
150
  runRunnerAIExecution,
@@ -3000,16 +3002,19 @@ function buildRunnerExecutionDeps() {
3000
3002
  return {
3001
3003
  analyzeHumanConversationIntentWithAI,
3002
3004
  auditDirectHumanReplyWithAI,
3005
+ planRoleExecutionWithAI,
3003
3006
  normalizeRunnerRoleProfileName,
3004
3007
  normalizeRunnerRoleProfile,
3005
3008
  normalizeBotRunnerProjectMapping,
3006
3009
  sanitizeWorkspaceCandidate,
3007
3010
  loadProviderEnvConfig,
3011
+ loadBotRunnerConfig,
3008
3012
  canUseLegacyRunnerCommand,
3009
3013
  hasLegacyRunnerCommand,
3010
3014
  isLegacyRunnerCommandOptInEnabled,
3011
3015
  legacyRunnerCommandDisabledMessage,
3012
3016
  normalizeExecutionArtifacts,
3017
+ resolveRunnerExecutionPlanForRole,
3013
3018
  validateWorkspaceArtifacts,
3014
3019
  tryJsonParse,
3015
3020
  };
@@ -718,6 +718,50 @@ function normalizeExecutionContract(rawContract) {
718
718
  };
719
719
  }
720
720
 
721
+ const RUNNER_EXECUTION_STEP_ROLES = new Set(["monitor", "worker", "review", "approval"]);
722
+
723
+ function normalizeRoleExecutionStepRole(rawValue) {
724
+ const normalized = String(rawValue || "").trim().toLowerCase();
725
+ return RUNNER_EXECUTION_STEP_ROLES.has(normalized) ? normalized : "";
726
+ }
727
+
728
+ function normalizeRoleExecutionStep(rawStep) {
729
+ const step = safeObject(rawStep);
730
+ const role = normalizeRoleExecutionStepRole(step.role || step.profile || step.role_profile || step.roleProfile);
731
+ const goal = firstNonEmptyString([
732
+ step.goal,
733
+ step.task,
734
+ step.objective,
735
+ step.description,
736
+ ]);
737
+ if (!role || !goal) {
738
+ return null;
739
+ }
740
+ return {
741
+ role,
742
+ goal,
743
+ artifactsRequired: step.artifacts_required === true || step.artifactsRequired === true,
744
+ };
745
+ }
746
+
747
+ export function normalizeRoleExecutionPlan(rawPlan) {
748
+ const plan = safeObject(rawPlan);
749
+ if (!Object.keys(plan).length) {
750
+ return null;
751
+ }
752
+ const steps = ensureArray(plan.steps)
753
+ .map((item) => normalizeRoleExecutionStep(item))
754
+ .filter(Boolean);
755
+ const summaryRole = normalizeRoleExecutionStepRole(plan.summary_role || plan.summaryRole);
756
+ const requiresExecution = plan.requires_execution === true || plan.requiresExecution === true;
757
+ return {
758
+ requiresExecution,
759
+ summaryRole: summaryRole || (steps.length > 0 ? steps[steps.length - 1].role : ""),
760
+ steps,
761
+ reason: String(plan.reason || "").trim(),
762
+ };
763
+ }
764
+
721
765
  function normalizeModelAliasText(rawValue) {
722
766
  return String(rawValue || "")
723
767
  .trim()
@@ -1070,6 +1114,7 @@ function inferCurrentTurnPurpose({ trigger, conversation, selfBotUsername, other
1070
1114
 
1071
1115
  export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1072
1116
  const safePayload = payload && typeof payload === "object" ? payload : {};
1117
+ const taskName = String(safePayload.task || "").trim();
1073
1118
  const trigger = safePayload.trigger && typeof safePayload.trigger === "object" ? safePayload.trigger : {};
1074
1119
  const responseContract = safePayload.response_contract && typeof safePayload.response_contract === "object"
1075
1120
  ? safePayload.response_contract
@@ -1156,9 +1201,14 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1156
1201
  const assignmentsForThisBot = currentExecutionAssignments.filter((item) => (
1157
1202
  String(item.target_bot || item.targetBot || "").trim().replace(/^@+/, "").toLowerCase() === selfSelector.toLowerCase()
1158
1203
  ));
1204
+ const executionStep = safeObject(safePayload.execution_step);
1205
+ const executionPlan = safeObject(safePayload.execution_plan);
1206
+ const executionProgress = safeObject(safePayload.execution_progress);
1207
+ const isInternalExecutionStep = taskName === "execute_project_role_step";
1159
1208
 
1160
1209
  const lines = [
1161
1210
  "You are a Metheus local bot runner.",
1211
+ `Task: ${taskName || "reply_to_project_chat_message"}`,
1162
1212
  `Provider: ${provider}`,
1163
1213
  `Role: ${role}`,
1164
1214
  `Role Profile: ${roleProfile || "-"}`,
@@ -1189,6 +1239,32 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1189
1239
  recentBotMessages.length ? recentBotMessages.map(formatContextCommentLine).join("\n") : "- none",
1190
1240
  "",
1191
1241
  ];
1242
+ if (isInternalExecutionStep) {
1243
+ const stepIndex = intFromRawAllowZero(executionStep.index, 0);
1244
+ const stepTotal = intFromRawAllowZero(executionStep.total, 0);
1245
+ const stepRole = firstNonEmptyString([executionStep.role, roleProfile, role]);
1246
+ const stepGoal = String(executionStep.goal || "").trim();
1247
+ const priorArtifacts = ensureArray(executionProgress.produced_artifacts)
1248
+ .map((item) => firstNonEmptyString([safeObject(item).relativePath, safeObject(item).path]))
1249
+ .filter(Boolean);
1250
+ lines.push(
1251
+ "Internal execution step is active.",
1252
+ "This is not a placeholder chat reply task. Complete the assigned step now.",
1253
+ `Execution Step: ${stepIndex > 0 ? stepIndex : "-"} / ${stepTotal > 0 ? stepTotal : "-"}`,
1254
+ `Execution Step Role: ${stepRole || "-"}`,
1255
+ `Execution Step Goal: ${stepGoal || "-"}`,
1256
+ `Execution Step Requires Artifacts: ${executionStep.artifacts_required === true || executionStep.artifactsRequired === true ? "yes" : "no"}`,
1257
+ `Execution Plan Requires Execution: ${executionPlan.requires_execution === true || executionPlan.requiresExecution === true ? "yes" : "no"}`,
1258
+ `Execution Plan Summary Role: ${String(executionPlan.summary_role || executionPlan.summaryRole || "").trim() || "-"}`,
1259
+ `Prior Produced Artifacts: ${priorArtifacts.length ? priorArtifacts.join(", ") : "-"}`,
1260
+ "Do the work for this step now. Do not say you will start later, wait, come back later, or plan first.",
1261
+ "If this is a worker step, actually create/update the required project artifacts now and include them in artifacts.",
1262
+ "If this is a review step, provide the review findings now.",
1263
+ "If this is an approval step, provide the approval decision now.",
1264
+ "If this is the final step, the reply will be shown to humans. If it is not the final step, the reply is still an internal execution note and must describe what was actually completed in this step.",
1265
+ "",
1266
+ );
1267
+ }
1192
1268
  if (responseContract.must_reply === true) {
1193
1269
  lines.push(
1194
1270
  "This bot was explicitly addressed by the trigger message.",
@@ -1232,7 +1308,9 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1232
1308
  "- If no project files changed, return artifacts: [].",
1233
1309
  "- Do not claim that a file, plan, document, or code change is complete unless the corresponding artifact path is present in artifacts.",
1234
1310
  "",
1235
- responseContract.must_reply === true
1311
+ isInternalExecutionStep
1312
+ ? "Return JSON only in one line: {\"reply\":\"what was completed in this step\",\"artifacts\":[{\"path\":\"relative/or/absolute/path\",\"kind\":\"plan|code|doc|spec|test\",\"operation\":\"create|update|delete\"}],\"contract\":{\"type\":\"direct_result|summary_request|final_summary\",\"actionable\":true,\"summary_bot\":\"username\",\"next_responders\":[\"username\"]}}. Use artifacts: [] only if this step truly changes no project files."
1313
+ : responseContract.must_reply === true
1236
1314
  ? "Return JSON only in one line: {\"reply\":\"...\",\"artifacts\":[]} or {\"reply\":\"...\",\"artifacts\":[],\"contract\":{\"type\":\"direct_result|delegation|summary_request|final_summary\",\"actionable\":true,\"assignments\":[{\"target_bot\":\"username\",\"task\":\"...\"}],\"summary_bot\":\"username\",\"next_responders\":[\"username\"]}}."
1237
1315
  : terse
1238
1316
  ? "Return JSON only in one line: {\"reply\":\"...\",\"artifacts\":[]} or {\"skip\":true,\"reason\":\"...\"}."
@@ -1426,6 +1504,57 @@ function buildConversationIntentAnalysisPrompt({
1426
1504
  ].join("\n");
1427
1505
  }
1428
1506
 
1507
+ function buildRoleExecutionPlanPrompt({
1508
+ messageText,
1509
+ currentBotUsername,
1510
+ currentBotRole,
1511
+ managedBots,
1512
+ workspaceDir,
1513
+ currentAssignmentTasks = [],
1514
+ contextComments = [],
1515
+ conversationMode = "",
1516
+ }) {
1517
+ const bots = ensureArray(managedBots).map((item) => {
1518
+ const bot = safeObject(item);
1519
+ return {
1520
+ username: String(bot.username || "").trim().replace(/^@+/, ""),
1521
+ display_name: String(bot.display_name || bot.displayName || bot.username || "").trim(),
1522
+ };
1523
+ }).filter((item) => item.username);
1524
+ const normalizedContext = ensureArray(contextComments)
1525
+ .slice(0, 10)
1526
+ .map((item) => formatContextCommentLine(item));
1527
+ return [
1528
+ "You are an execution planner for managed local Telegram bots.",
1529
+ "Read the human request and recent room context, then decide whether actual project execution is required now.",
1530
+ "Judge by meaning, not by keywords or @mentions alone.",
1531
+ "If the request only asks for status, explanation, location, or clarification, then requires_execution should be false.",
1532
+ "If the request asks the bot to create/update/delete project files, write plans/docs/code, delegate concrete tasks, continue execution, or otherwise do work now, then requires_execution should be true.",
1533
+ "When requires_execution=true, return the minimal role step plan using only these roles: monitor, worker, review, approval.",
1534
+ "Role guidance:",
1535
+ "- monitor: understand context, scope the work, plan, coordinate, or summarize.",
1536
+ "- worker: actually create/update/delete project artifacts now.",
1537
+ "- review: inspect completed work and report findings, issues, or improvements.",
1538
+ "- approval: make a go/no-go, approval, or explicit decision when the request calls for it.",
1539
+ "worker is required if project artifacts must be created or updated now.",
1540
+ "review is required if the request implies checking or validating the created result before finalizing.",
1541
+ "approval is required only when the request asks for approval, release, confirmation, or a formal decision.",
1542
+ "",
1543
+ "Return strict JSON only with this shape:",
1544
+ "{\"requires_execution\":true|false,\"summary_role\":\"monitor|worker|review|approval\",\"reason\":\"short explanation\",\"steps\":[{\"role\":\"monitor|worker|review|approval\",\"goal\":\"short goal\",\"artifacts_required\":true|false}]}",
1545
+ "",
1546
+ `current_bot_username=${JSON.stringify(String(currentBotUsername || "").trim().replace(/^@+/, ""))}`,
1547
+ `current_bot_route_role=${JSON.stringify(String(currentBotRole || "").trim().toLowerCase())}`,
1548
+ `conversation_mode=${JSON.stringify(String(conversationMode || "").trim())}`,
1549
+ `workspace_dir=${JSON.stringify(String(workspaceDir || "").trim())}`,
1550
+ `managed_bots=${JSON.stringify(bots)}`,
1551
+ `current_assignment_tasks=${JSON.stringify(ensureArray(currentAssignmentTasks).map((item) => String(item || "").trim()).filter(Boolean))}`,
1552
+ `human_message=${JSON.stringify(String(messageText || "").trim())}`,
1553
+ "recent_context=",
1554
+ normalizedContext.length ? normalizedContext.join("\n") : "- none",
1555
+ ].join("\n");
1556
+ }
1557
+
1429
1558
  function buildDirectReplyGuardrailPrompt({
1430
1559
  humanMessageText,
1431
1560
  botReplyText,
@@ -1557,6 +1686,59 @@ export function auditDirectHumanReplyWithAI({
1557
1686
  };
1558
1687
  }
1559
1688
 
1689
+ export function planRoleExecutionWithAI({
1690
+ messageText,
1691
+ currentBotUsername,
1692
+ currentBotRole = "",
1693
+ managedBots,
1694
+ workspaceDir,
1695
+ currentAssignmentTasks = [],
1696
+ contextComments = [],
1697
+ conversationMode = "",
1698
+ client = "",
1699
+ model = "",
1700
+ env = process.env,
1701
+ }) {
1702
+ const bots = ensureArray(managedBots);
1703
+ if (!bots.length) {
1704
+ return null;
1705
+ }
1706
+ const plannerClient = normalizeLocalAIClientName(
1707
+ String(client || env?.METHEUS_ROLE_PLANNER_CLIENT || env?.METHEUS_INTENT_PARSER_CLIENT || "").trim(),
1708
+ "gpt",
1709
+ );
1710
+ const plannerModel = String(
1711
+ model
1712
+ || env?.METHEUS_ROLE_PLANNER_MODEL
1713
+ || env?.METHEUS_INTENT_PARSER_MODEL
1714
+ || suggestLocalAIModelDisplayName(plannerClient, "")
1715
+ || "",
1716
+ ).trim();
1717
+ const rawText = runLocalAIPromptRawText({
1718
+ client: plannerClient,
1719
+ promptText: buildRoleExecutionPlanPrompt({
1720
+ messageText,
1721
+ currentBotUsername,
1722
+ currentBotRole,
1723
+ managedBots: bots,
1724
+ workspaceDir,
1725
+ currentAssignmentTasks,
1726
+ contextComments,
1727
+ conversationMode,
1728
+ }),
1729
+ workspaceDir,
1730
+ model: plannerModel,
1731
+ permissionMode: "read_only",
1732
+ reasoningEffort: String(env?.METHEUS_ROLE_PLANNER_REASONING_EFFORT || "medium").trim() || "medium",
1733
+ env,
1734
+ });
1735
+ const parsed = tryJsonParse(rawText) || tryParseEmbeddedJsonObject(rawText);
1736
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1737
+ throw new Error("role execution planner did not return a JSON object");
1738
+ }
1739
+ return normalizeRoleExecutionPlan(parsed);
1740
+ }
1741
+
1560
1742
  export function runLocalAIClient({
1561
1743
  client,
1562
1744
  inputPayload,
@@ -241,6 +241,23 @@ export function resolveRunnerExecutionPlan(route, bot, config, deps) {
241
241
  throw new Error(detail.join("; "));
242
242
  }
243
243
 
244
+ export function resolveRunnerExecutionPlanForRole(route, bot, roleName, config, deps) {
245
+ const normalizedRoleName = String(roleName || "").trim().toLowerCase();
246
+ if (!normalizedRoleName) {
247
+ throw new Error("roleName is required to resolve a role execution plan");
248
+ }
249
+ return resolveRunnerExecutionPlan(
250
+ {
251
+ ...safeObject(route),
252
+ role: normalizedRoleName,
253
+ roleProfile: normalizedRoleName,
254
+ },
255
+ bot,
256
+ config,
257
+ deps,
258
+ );
259
+ }
260
+
244
261
  function buildRunnerExecutionEnv({ route, destination, executionPlan }) {
245
262
  return {
246
263
  ...process.env,
@@ -431,6 +431,406 @@ function buildDirectHumanResponseContract({
431
431
  allowedContractTypes,
432
432
  };
433
433
  }
434
+
435
+ function extractCurrentAssignmentTasks(conversationContext, currentBotSelector) {
436
+ const currentSelector = normalizeMentionSelector(currentBotSelector);
437
+ if (!currentSelector) return [];
438
+ return ensureArray(safeObject(conversationContext?.executionContract).assignments)
439
+ .map((item) => safeObject(item))
440
+ .filter((item) => normalizeMentionSelector(item.targetBot || item.target_bot) === currentSelector)
441
+ .map((item) => String(item.task || item.instruction || "").trim())
442
+ .filter(Boolean);
443
+ }
444
+
445
+ function requiresArtifactsForExecutionStep(step) {
446
+ const role = String(safeObject(step).role || "").trim().toLowerCase();
447
+ return safeObject(step).artifactsRequired === true || role === "worker";
448
+ }
449
+
450
+ function summarizeExecutedRolePlan(plan, stepResults) {
451
+ return {
452
+ requires_execution: safeObject(plan).requiresExecution === true,
453
+ summary_role: String(safeObject(plan).summaryRole || "").trim(),
454
+ steps: ensureArray(safeObject(plan).steps).map((item, index) => ({
455
+ index: index + 1,
456
+ role: String(safeObject(item).role || "").trim(),
457
+ goal: String(safeObject(item).goal || "").trim(),
458
+ artifacts_required: safeObject(item).artifactsRequired === true,
459
+ })),
460
+ step_results: ensureArray(stepResults).map((item) => ({
461
+ role: String(safeObject(item).role || "").trim(),
462
+ goal: String(safeObject(item).goal || "").trim(),
463
+ artifact_paths: ensureArray(safeObject(item).artifactPaths),
464
+ artifact_validation: String(safeObject(item).artifactValidation || "").trim(),
465
+ reply_chars: intFromRawAllowZero(String(safeObject(item).reply || "").length, 0),
466
+ })),
467
+ };
468
+ }
469
+
470
+ function buildRoleExecutionStepPayload({
471
+ aiPayload,
472
+ step,
473
+ stepIndex,
474
+ stepTotal,
475
+ stepExecutionPlan,
476
+ executedPlan,
477
+ priorStepResults,
478
+ producedArtifacts,
479
+ }) {
480
+ return {
481
+ ...aiPayload,
482
+ task: "execute_project_role_step",
483
+ role: String(step.role || "").trim(),
484
+ requested_role: String(step.role || "").trim(),
485
+ role_profile: String(stepExecutionPlan.roleProfileName || step.role || "").trim(),
486
+ execution: {
487
+ mode: String(stepExecutionPlan.mode || "").trim(),
488
+ role_profile: String(stepExecutionPlan.roleProfileName || step.role || "").trim(),
489
+ client: String(stepExecutionPlan.roleProfile?.client || "").trim(),
490
+ model: String(stepExecutionPlan.roleProfile?.model || "").trim(),
491
+ permission_mode: String(stepExecutionPlan.roleProfile?.permissionMode || "").trim(),
492
+ reasoning_effort: String(stepExecutionPlan.roleProfile?.reasoningEffort || "").trim(),
493
+ workspace_dir: String(stepExecutionPlan.workspaceDir || "").trim(),
494
+ workspace_source: String(stepExecutionPlan.workspaceSource || "").trim(),
495
+ command_fallback: stepExecutionPlan.usedCommandFallback === true,
496
+ dry_run_delivery: Boolean(safeObject(aiPayload.execution).dry_run_delivery),
497
+ },
498
+ execution_plan: {
499
+ requires_execution: safeObject(executedPlan).requiresExecution === true,
500
+ summary_role: String(safeObject(executedPlan).summaryRole || "").trim(),
501
+ steps: ensureArray(safeObject(executedPlan).steps).map((item) => ({
502
+ role: String(safeObject(item).role || "").trim(),
503
+ goal: String(safeObject(item).goal || "").trim(),
504
+ artifacts_required: safeObject(item).artifactsRequired === true,
505
+ })),
506
+ reason: String(safeObject(executedPlan).reason || "").trim(),
507
+ },
508
+ execution_step: {
509
+ index: stepIndex,
510
+ total: stepTotal,
511
+ role: String(step.role || "").trim(),
512
+ goal: String(step.goal || "").trim(),
513
+ artifacts_required: safeObject(step).artifactsRequired === true,
514
+ is_final_step: stepIndex === stepTotal,
515
+ },
516
+ execution_progress: {
517
+ completed_steps: ensureArray(priorStepResults).map((item) => ({
518
+ role: String(safeObject(item).role || "").trim(),
519
+ goal: String(safeObject(item).goal || "").trim(),
520
+ reply: String(safeObject(item).reply || "").trim(),
521
+ artifact_paths: ensureArray(safeObject(item).artifactPaths),
522
+ })),
523
+ produced_artifacts: ensureArray(producedArtifacts).map((item) => ({
524
+ path: String(safeObject(item).path || "").trim(),
525
+ relative_path: String(safeObject(item).relativePath || "").trim(),
526
+ kind: String(safeObject(item).kind || "").trim(),
527
+ operation: String(safeObject(item).operation || "").trim(),
528
+ })),
529
+ },
530
+ response_contract: {
531
+ ...safeObject(aiPayload.response_contract),
532
+ allow_skip: false,
533
+ must_reply: true,
534
+ require_actionable_contract: true,
535
+ allowed_contract_types: ["direct_result", "summary_request", "final_summary"],
536
+ },
537
+ };
538
+ }
539
+
540
+ async function maybeExecuteDynamicRolePlan({
541
+ aiPayload,
542
+ routeKey,
543
+ normalizedRoute,
544
+ routeState,
545
+ selectedRecord,
546
+ pendingOrdered,
547
+ bot,
548
+ destination,
549
+ archiveThread,
550
+ executionPlan,
551
+ runtime,
552
+ executionDeps,
553
+ triggerDecision,
554
+ humanIntentContext,
555
+ conversationContext,
556
+ saveRunnerRouteState,
557
+ runRunnerAIExecution,
558
+ validateWorkspaceArtifacts,
559
+ }) {
560
+ const planner = typeof executionDeps.planRoleExecutionWithAI === "function"
561
+ ? executionDeps.planRoleExecutionWithAI
562
+ : null;
563
+ const resolveRunnerExecutionPlanForRole = typeof executionDeps.resolveRunnerExecutionPlanForRole === "function"
564
+ ? executionDeps.resolveRunnerExecutionPlanForRole
565
+ : null;
566
+ const loadBotRunnerConfig = typeof executionDeps.loadBotRunnerConfig === "function"
567
+ ? executionDeps.loadBotRunnerConfig
568
+ : null;
569
+ if (!planner || !resolveRunnerExecutionPlanForRole) {
570
+ return null;
571
+ }
572
+ const currentBotSelector = normalizeMentionSelector(bot?.username || bot?.name);
573
+ const assignmentTasks = extractCurrentAssignmentTasks(conversationContext, currentBotSelector);
574
+ const shouldPlanExecution = triggerDecision.requiresDirectReply === true || assignmentTasks.length > 0;
575
+ if (!shouldPlanExecution) {
576
+ return null;
577
+ }
578
+ const peerMap = humanIntentContext?.peerMap instanceof Map
579
+ ? humanIntentContext.peerMap
580
+ : buildConversationPeerMap(bot, normalizedRoute, executionDeps);
581
+ const managedBots = buildConversationParticipantViews(Array.from(peerMap.keys()), peerMap);
582
+ let executedPlan;
583
+ try {
584
+ executedPlan = await planner({
585
+ messageText: String(safeObject(selectedRecord?.parsedArchive).body || "").trim(),
586
+ currentBotUsername: String(bot?.username || bot?.name || "").trim(),
587
+ currentBotRole: String(executionPlan.roleProfileName || normalizedRoute.role || "").trim(),
588
+ managedBots,
589
+ workspaceDir: String(executionPlan.workspaceDir || process.cwd()).trim() || process.cwd(),
590
+ currentAssignmentTasks: assignmentTasks,
591
+ contextComments: buildRunnerContextWindow(pendingOrdered, selectedRecord, normalizedRoute.contextComments),
592
+ conversationMode: String(conversationContext?.mode || "").trim(),
593
+ client: String(executionPlan.roleProfile?.client || "").trim(),
594
+ model: String(executionPlan.roleProfile?.model || "").trim(),
595
+ });
596
+ } catch {
597
+ return null;
598
+ }
599
+ if (!safeObject(executedPlan).requiresExecution) {
600
+ return null;
601
+ }
602
+ const plannedSteps = ensureArray(safeObject(executedPlan).steps).filter((item) => String(safeObject(item).role || "").trim());
603
+ if (!plannedSteps.length) {
604
+ const reason = "execution planner required execution but did not return any executable steps";
605
+ saveRunnerRouteState(
606
+ routeKey,
607
+ buildRunnerRouteStateFromComment(selectedRecord, {
608
+ last_action: "execution_failed",
609
+ last_reason: reason,
610
+ last_conversation_id: String(conversationContext?.id || "").trim(),
611
+ last_conversation_stage: String(conversationContext?.stage || "").trim(),
612
+ last_workspace_dir: String(executionPlan.workspaceDir || "").trim(),
613
+ }),
614
+ );
615
+ return {
616
+ kind: "error",
617
+ result: {
618
+ route_key: routeKey,
619
+ route_name: normalizedRoute.name,
620
+ outcome: "execution_failed",
621
+ detail: reason,
622
+ thread_id: archiveThread.threadID,
623
+ comment_id: selectedRecord.id,
624
+ trigger_kind: String(triggerDecision.trigger || "").trim(),
625
+ },
626
+ };
627
+ }
628
+ const config = loadBotRunnerConfig ? loadBotRunnerConfig() : { roleProfiles: {}, botBindings: {}, routes: [], projectMappings: {} };
629
+ const stepResults = [];
630
+ const mergedArtifacts = [];
631
+ const mergedArtifactPaths = new Set();
632
+ const mergedBoundaryViolations = [];
633
+ const totalSteps = plannedSteps.length;
634
+ for (const [index, rawStep] of plannedSteps.entries()) {
635
+ const step = safeObject(rawStep);
636
+ let stepExecutionPlan;
637
+ try {
638
+ stepExecutionPlan = resolveRunnerExecutionPlanForRole(
639
+ normalizedRoute,
640
+ bot,
641
+ String(step.role || "").trim(),
642
+ config,
643
+ executionDeps,
644
+ );
645
+ } catch (err) {
646
+ const reason = `failed to resolve execution step "${String(step.role || "").trim()}": ${String(err?.message || err)}`;
647
+ saveRunnerRouteState(
648
+ routeKey,
649
+ buildRunnerRouteStateFromComment(selectedRecord, {
650
+ last_action: "execution_failed",
651
+ last_reason: reason,
652
+ last_conversation_id: String(conversationContext?.id || "").trim(),
653
+ last_conversation_stage: String(conversationContext?.stage || "").trim(),
654
+ last_workspace_dir: String(executionPlan.workspaceDir || "").trim(),
655
+ }),
656
+ );
657
+ return {
658
+ kind: "error",
659
+ result: {
660
+ route_key: routeKey,
661
+ route_name: normalizedRoute.name,
662
+ outcome: "execution_failed",
663
+ detail: reason,
664
+ thread_id: archiveThread.threadID,
665
+ comment_id: selectedRecord.id,
666
+ trigger_kind: String(triggerDecision.trigger || "").trim(),
667
+ },
668
+ };
669
+ }
670
+ const stepPayload = buildRoleExecutionStepPayload({
671
+ aiPayload,
672
+ step,
673
+ stepIndex: index + 1,
674
+ stepTotal: totalSteps,
675
+ stepExecutionPlan,
676
+ executedPlan,
677
+ priorStepResults: stepResults,
678
+ producedArtifacts: mergedArtifacts,
679
+ });
680
+ const stepResult = await runRunnerAIExecution({
681
+ inputPayload: stepPayload,
682
+ route: {
683
+ ...normalizedRoute,
684
+ role: String(step.role || "").trim(),
685
+ roleProfile: String(stepExecutionPlan.roleProfileName || step.role || "").trim(),
686
+ },
687
+ destination,
688
+ executionPlan: stepExecutionPlan,
689
+ deps: executionDeps,
690
+ });
691
+ if (stepResult.skip) {
692
+ const reason = `${String(step.role || "").trim()} step skipped: ${String(stepResult.reason || "execution step returned skip").trim() || "execution step returned skip"}`;
693
+ saveRunnerRouteState(
694
+ routeKey,
695
+ buildRunnerRouteStateFromComment(selectedRecord, {
696
+ last_action: "execution_failed",
697
+ last_reason: reason,
698
+ last_conversation_id: String(conversationContext?.id || "").trim(),
699
+ last_conversation_stage: String(conversationContext?.stage || "").trim(),
700
+ last_workspace_dir: String(stepExecutionPlan.workspaceDir || executionPlan.workspaceDir || "").trim(),
701
+ }),
702
+ );
703
+ return {
704
+ kind: "error",
705
+ result: {
706
+ route_key: routeKey,
707
+ route_name: normalizedRoute.name,
708
+ outcome: "execution_failed",
709
+ detail: reason,
710
+ thread_id: archiveThread.threadID,
711
+ comment_id: selectedRecord.id,
712
+ trigger_kind: String(triggerDecision.trigger || "").trim(),
713
+ },
714
+ };
715
+ }
716
+ const stepValidation = validateWorkspaceArtifacts && String(stepExecutionPlan.workspaceDir || "").trim()
717
+ ? validateWorkspaceArtifacts(
718
+ ensureArray(stepResult?.artifacts),
719
+ stepExecutionPlan.workspaceDir,
720
+ {
721
+ permissionMode: String(stepExecutionPlan.roleProfile?.permissionMode || "workspace_write").trim() || "workspace_write",
722
+ },
723
+ )
724
+ : {
725
+ ok: true,
726
+ status: "none",
727
+ artifacts: ensureArray(stepResult?.artifacts),
728
+ errors: [],
729
+ };
730
+ const stepBoundaryViolations = normalizeBoundaryViolations(stepResult?.boundaryViolations);
731
+ const stepErrors = [
732
+ ...ensureArray(stepValidation.errors).map((item) => String(item || "").trim()).filter(Boolean),
733
+ ...stepBoundaryViolations.map((item) => `${item.detail}${item.path ? `: ${item.path}` : ""}`),
734
+ ];
735
+ if (requiresArtifactsForExecutionStep(step) && ensureArray(stepValidation.artifacts).length === 0) {
736
+ stepErrors.push(`${String(step.role || "").trim()} step completed without any validated project artifacts`);
737
+ }
738
+ if (stepErrors.length > 0) {
739
+ const reason = stepErrors.join("; ");
740
+ saveRunnerRouteState(
741
+ routeKey,
742
+ buildRunnerRouteStateFromComment(selectedRecord, {
743
+ last_action: "execution_failed",
744
+ last_reason: reason,
745
+ last_conversation_id: String(conversationContext?.id || "").trim(),
746
+ last_conversation_stage: String(conversationContext?.stage || "").trim(),
747
+ last_artifact_validation: String(stepValidation.status || "").trim() || "execution_failed",
748
+ last_artifact_paths: ensureArray(stepValidation.artifacts).map((item) => String(item.path || item.relativePath || "").trim()).filter(Boolean),
749
+ last_artifact_errors: stepErrors,
750
+ last_boundary_violations: stepBoundaryViolations,
751
+ last_workspace_dir: String(stepExecutionPlan.workspaceDir || executionPlan.workspaceDir || "").trim(),
752
+ }),
753
+ );
754
+ return {
755
+ kind: "error",
756
+ result: {
757
+ route_key: routeKey,
758
+ route_name: normalizedRoute.name,
759
+ outcome: "execution_failed",
760
+ detail: reason,
761
+ thread_id: archiveThread.threadID,
762
+ comment_id: selectedRecord.id,
763
+ trigger_kind: String(triggerDecision.trigger || "").trim(),
764
+ artifact_validation: String(stepValidation.status || "").trim() || "execution_failed",
765
+ artifact_paths: ensureArray(stepValidation.artifacts).map((item) => String(item.path || item.relativePath || "").trim()).filter(Boolean),
766
+ artifact_errors: stepErrors,
767
+ },
768
+ };
769
+ }
770
+ ensureArray(stepValidation.artifacts).forEach((item) => {
771
+ const artifact = safeObject(item);
772
+ const artifactPath = String(artifact.path || artifact.relativePath || "").trim();
773
+ if (!artifactPath || mergedArtifactPaths.has(artifactPath)) {
774
+ return;
775
+ }
776
+ mergedArtifactPaths.add(artifactPath);
777
+ mergedArtifacts.push(artifact);
778
+ });
779
+ mergedBoundaryViolations.push(...stepBoundaryViolations);
780
+ stepResults.push({
781
+ role: String(step.role || "").trim(),
782
+ goal: String(step.goal || "").trim(),
783
+ reply: String(stepResult.reply || "").trim(),
784
+ artifactPaths: ensureArray(stepValidation.artifacts).map((item) => String(safeObject(item).relativePath || safeObject(item).path || "").trim()).filter(Boolean),
785
+ artifactValidation: String(stepValidation.status || "").trim() || "none",
786
+ });
787
+ }
788
+ const finalStepResult = stepResults[stepResults.length - 1] || {};
789
+ const summaryRole = String(safeObject(executedPlan).summaryRole || "").trim().toLowerCase();
790
+ const summaryStepResult = stepResults.findLast
791
+ ? stepResults.findLast((item) => String(safeObject(item).role || "").trim().toLowerCase() === summaryRole)
792
+ : [...stepResults].reverse().find((item) => String(safeObject(item).role || "").trim().toLowerCase() === summaryRole);
793
+ const finalReply = String((summaryStepResult || finalStepResult).reply || "").trim();
794
+ const summaryBotSelector = normalizeMentionSelector(conversationContext?.summaryBotUsername || conversationContext?.leadBotUsername);
795
+ const finalContract = conversationContext?.mode === "public_multi_bot" && summaryBotSelector && summaryBotSelector !== currentBotSelector
796
+ ? {
797
+ type: "summary_request",
798
+ actionable: true,
799
+ assignments: [],
800
+ summaryBot: summaryBotSelector,
801
+ nextResponders: [summaryBotSelector],
802
+ }
803
+ : {
804
+ type: summaryRole === "approval" ? "final_summary" : "direct_result",
805
+ actionable: true,
806
+ assignments: [],
807
+ summaryBot: summaryBotSelector,
808
+ nextResponders: [],
809
+ };
810
+ return {
811
+ kind: "executed",
812
+ aiResult: {
813
+ skip: false,
814
+ reply: finalReply,
815
+ replyToMessageID: 0,
816
+ artifacts: mergedArtifacts.map((item) => ({
817
+ path: String(safeObject(item).path || safeObject(item).relativePath || "").trim(),
818
+ kind: String(safeObject(item).kind || "").trim(),
819
+ operation: String(safeObject(item).operation || "").trim(),
820
+ })),
821
+ boundaryViolations: mergedBoundaryViolations,
822
+ contract: finalContract,
823
+ executionPlanSummary: summarizeExecutedRolePlan(executedPlan, stepResults),
824
+ },
825
+ artifactValidation: {
826
+ ok: true,
827
+ status: mergedArtifacts.length > 0 ? "validated" : "none",
828
+ artifacts: mergedArtifacts,
829
+ errors: [],
830
+ },
831
+ executedPlan: summarizeExecutedRolePlan(executedPlan, stepResults),
832
+ };
833
+ }
434
834
 
435
835
  function buildConversationPeerMap(bot, normalizedRoute, deps) {
436
836
  const peers = typeof deps?.resolveConversationPeerBots === "function"
@@ -1456,22 +1856,60 @@ export async function processRunnerSelectedRecord({
1456
1856
  intervalMs: 4000,
1457
1857
  deps: runtimeDeps,
1458
1858
  });
1459
- try {
1460
- await Promise.resolve(typingHeartbeat.ready);
1461
- } catch {}
1462
- let aiResult;
1463
- try {
1464
- aiResult = await runRunnerAIExecution({
1465
- inputPayload: aiPayload,
1466
- route: normalizedRoute,
1467
- destination,
1468
- executionPlan,
1469
- deps: executionDeps,
1470
- });
1471
- } finally {
1472
- await typingHeartbeat.stop();
1473
- }
1474
-
1859
+ try {
1860
+ await Promise.resolve(typingHeartbeat.ready);
1861
+ } catch {}
1862
+ let aiResult;
1863
+ let dynamicRoleExecution = null;
1864
+ let dynamicExecutionError = null;
1865
+ let artifactValidationOverride = null;
1866
+ let executedRolePlan = null;
1867
+ try {
1868
+ dynamicRoleExecution = await maybeExecuteDynamicRolePlan({
1869
+ aiPayload,
1870
+ routeKey,
1871
+ normalizedRoute,
1872
+ routeState,
1873
+ selectedRecord,
1874
+ pendingOrdered,
1875
+ bot,
1876
+ destination,
1877
+ archiveThread,
1878
+ executionPlan,
1879
+ runtime,
1880
+ executionDeps,
1881
+ triggerDecision,
1882
+ humanIntentContext,
1883
+ conversationContext,
1884
+ saveRunnerRouteState,
1885
+ runRunnerAIExecution,
1886
+ validateWorkspaceArtifacts,
1887
+ });
1888
+ if (dynamicRoleExecution?.kind === "error") {
1889
+ dynamicExecutionError = dynamicRoleExecution.result;
1890
+ } else if (dynamicRoleExecution?.kind === "executed") {
1891
+ aiResult = dynamicRoleExecution.aiResult;
1892
+ artifactValidationOverride = safeObject(dynamicRoleExecution.artifactValidation);
1893
+ executedRolePlan = safeObject(dynamicRoleExecution.executedPlan);
1894
+ } else {
1895
+ aiResult = await runRunnerAIExecution({
1896
+ inputPayload: aiPayload,
1897
+ route: normalizedRoute,
1898
+ destination,
1899
+ executionPlan,
1900
+ deps: executionDeps,
1901
+ });
1902
+ }
1903
+ } finally {
1904
+ await typingHeartbeat.stop();
1905
+ }
1906
+ if (dynamicExecutionError) {
1907
+ return {
1908
+ kind: "error",
1909
+ result: dynamicExecutionError,
1910
+ };
1911
+ }
1912
+
1475
1913
  if (aiResult.skip) {
1476
1914
  if (triggerDecision.requiresDirectReply) {
1477
1915
  const forcedReplyPayload = {
@@ -1524,20 +1962,22 @@ export async function processRunnerSelectedRecord({
1524
1962
  };
1525
1963
  }
1526
1964
 
1527
- const artifactValidation = validateWorkspaceArtifacts && String(executionPlan.workspaceDir || "").trim()
1528
- ? validateWorkspaceArtifacts(
1529
- ensureArray(aiResult?.artifacts),
1530
- executionPlan.workspaceDir,
1531
- {
1532
- permissionMode: String(executionPlan.roleProfile?.permissionMode || "workspace_write").trim() || "workspace_write",
1533
- },
1534
- )
1535
- : {
1536
- ok: true,
1537
- status: "none",
1538
- artifacts: ensureArray(aiResult?.artifacts),
1539
- errors: [],
1540
- };
1965
+ const artifactValidation = artifactValidationOverride && Object.keys(artifactValidationOverride).length > 0
1966
+ ? artifactValidationOverride
1967
+ : validateWorkspaceArtifacts && String(executionPlan.workspaceDir || "").trim()
1968
+ ? validateWorkspaceArtifacts(
1969
+ ensureArray(aiResult?.artifacts),
1970
+ executionPlan.workspaceDir,
1971
+ {
1972
+ permissionMode: String(executionPlan.roleProfile?.permissionMode || "workspace_write").trim() || "workspace_write",
1973
+ },
1974
+ )
1975
+ : {
1976
+ ok: true,
1977
+ status: "none",
1978
+ artifacts: ensureArray(aiResult?.artifacts),
1979
+ errors: [],
1980
+ };
1541
1981
  const boundaryViolations = normalizeBoundaryViolations(aiResult?.boundaryViolations);
1542
1982
  const artifactErrors = [
1543
1983
  ...ensureArray(artifactValidation.errors).map((item) => String(item || "").trim()).filter(Boolean),
@@ -1931,6 +2371,7 @@ export async function processRunnerSelectedRecord({
1931
2371
  last_artifact_errors: [],
1932
2372
  last_boundary_violations: [],
1933
2373
  last_workspace_dir: String(executionPlan.workspaceDir || "").trim(),
2374
+ last_execution_plan: executedRolePlan && Object.keys(executedRolePlan).length > 0 ? executedRolePlan : undefined,
1934
2375
  }),
1935
2376
  ...(nextConversationSessions ? { conversation_sessions: nextConversationSessions } : {}),
1936
2377
  },
@@ -1973,9 +2414,10 @@ export async function processRunnerSelectedRecord({
1973
2414
  reply_chars: String(sanitizedReplyText || "").length,
1974
2415
  execution_mode: executionPlan.mode,
1975
2416
  role_profile: executionPlan.roleProfileName,
1976
- archive_status: deliveryResult.archive?.dry_run
1977
- ? "dry_run"
1978
- : deliveryResult.archive?.deduped
2417
+ executed_role_plan: executedRolePlan && Object.keys(executedRolePlan).length > 0 ? executedRolePlan : undefined,
2418
+ archive_status: deliveryResult.archive?.dry_run
2419
+ ? "dry_run"
2420
+ : deliveryResult.archive?.deduped
1979
2421
  ? "deduped"
1980
2422
  : deliveryResult.archive?.ok === false
1981
2423
  ? "archive_error"
@@ -2731,6 +2731,355 @@ export async function runSelftestRunnerScenarios(push, deps) {
2731
2731
  push("single_bot_human_request_guardrail_audit_upgrades_to_actionable", false, String(err?.message || err));
2732
2732
  }
2733
2733
 
2734
+ try {
2735
+ let deliveryCalls = 0;
2736
+ let aiCalls = 0;
2737
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-selftest-multi-step-success-"));
2738
+ const processed = await processRunnerSelectedRecord({
2739
+ routeKey: "single-bot-multi-step-execution-success-key",
2740
+ normalizedRoute: normalizeRunnerRoute({
2741
+ name: "telegram-monitor-single-bot-multi-step-success",
2742
+ project_id: selftestProjectID,
2743
+ provider: "telegram",
2744
+ role: "monitor",
2745
+ role_profile: "monitor",
2746
+ destination_id: "dest-1",
2747
+ destination_label: "Main Room",
2748
+ server_bot_name: "RyoAI_bot",
2749
+ server_bot_id: "bot-lead-1",
2750
+ trigger_policy: {
2751
+ mentions_only: true,
2752
+ direct_messages: true,
2753
+ reply_to_bot_messages: true,
2754
+ },
2755
+ archive_policy: {
2756
+ mirror_replies: true,
2757
+ dedupe_inbound: true,
2758
+ dedupe_outbound: true,
2759
+ skip_bot_messages: true,
2760
+ },
2761
+ dry_run_delivery: true,
2762
+ }),
2763
+ selectedRecord: {
2764
+ id: "comment-single-bot-multi-step-execution-success",
2765
+ createdAt: "2026-03-17T01:00:00.000Z",
2766
+ parsedArchive: {
2767
+ kind: "telegram_message",
2768
+ chatID: "-100123",
2769
+ chatType: "supergroup",
2770
+ senderIsBot: false,
2771
+ body: "@RyoAI_bot create the project README now.",
2772
+ mentionUsernames: ["RyoAI_bot"],
2773
+ messageID: 1301,
2774
+ },
2775
+ },
2776
+ pendingOrdered: [],
2777
+ bot: {
2778
+ id: "bot-lead-1",
2779
+ name: "RyoAI_bot",
2780
+ username: "RyoAI_bot",
2781
+ role: "monitor",
2782
+ provider: "telegram",
2783
+ },
2784
+ destination: {
2785
+ id: "dest-1",
2786
+ label: "Main Room",
2787
+ provider: "telegram",
2788
+ chatID: "-100123",
2789
+ },
2790
+ archiveThread: {
2791
+ threadID: "thread-1",
2792
+ workItemID: "work-item-1",
2793
+ },
2794
+ executionPlan: {
2795
+ mode: "role_profile",
2796
+ roleProfileName: "monitor",
2797
+ roleProfile: {
2798
+ client: "sample",
2799
+ model: "",
2800
+ permissionMode: "read_only",
2801
+ reasoningEffort: "low",
2802
+ },
2803
+ workspaceDir,
2804
+ workspaceSource: "selftest",
2805
+ usedCommandFallback: false,
2806
+ },
2807
+ runtime: {
2808
+ baseURL: "https://example.test",
2809
+ token: "selftest-token",
2810
+ timeoutSeconds: 30,
2811
+ actor: { user_id: "user-1" },
2812
+ },
2813
+ deps: {
2814
+ saveRunnerRouteState: () => {},
2815
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
2816
+ runRunnerAIExecution: async ({ inputPayload }) => {
2817
+ aiCalls += 1;
2818
+ if (String(inputPayload?.task || "").trim() !== "execute_project_role_step") {
2819
+ throw new Error(`unexpected task ${String(inputPayload?.task || "(none)")}`);
2820
+ }
2821
+ const stepRole = String(inputPayload?.execution_step?.role || "").trim();
2822
+ if (stepRole === "monitor") {
2823
+ return {
2824
+ skip: false,
2825
+ reply: "Inspected the project requirements and confirmed the target README scope.",
2826
+ };
2827
+ }
2828
+ if (stepRole === "worker") {
2829
+ const readmePath = path.join(workspaceDir, "README.md");
2830
+ fs.writeFileSync(readmePath, "# README\n", "utf8");
2831
+ return {
2832
+ skip: false,
2833
+ reply: "Created the README document.",
2834
+ artifacts: [{ path: "README.md", kind: "document", operation: "create" }],
2835
+ };
2836
+ }
2837
+ if (stepRole === "review") {
2838
+ return {
2839
+ skip: false,
2840
+ reply: "Reviewed the README and confirmed it is ready to share.",
2841
+ };
2842
+ }
2843
+ throw new Error(`unexpected step role ${stepRole}`);
2844
+ },
2845
+ performLocalBotDelivery: async () => {
2846
+ deliveryCalls += 1;
2847
+ return {
2848
+ delivery: { dryRun: true, body: {} },
2849
+ archive: {},
2850
+ };
2851
+ },
2852
+ serializeRunnerTriggerPolicy: (value) => value,
2853
+ serializeRunnerArchivePolicy: (value) => value,
2854
+ buildRunnerExecutionDeps: () => ({
2855
+ validateWorkspaceArtifacts,
2856
+ analyzeHumanConversationIntentWithAI: async () => ({
2857
+ mode: "single_bot",
2858
+ lead_bot: "ryoai_bot",
2859
+ participants: ["ryoai_bot"],
2860
+ initial_responders: ["ryoai_bot"],
2861
+ allowed_responders: ["ryoai_bot"],
2862
+ summary_bot: "",
2863
+ allow_bot_to_bot: false,
2864
+ reply_expectation: "actionable",
2865
+ }),
2866
+ planRoleExecutionWithAI: async () => ({
2867
+ requiresExecution: true,
2868
+ summaryRole: "review",
2869
+ steps: [
2870
+ { role: "monitor", goal: "Inspect project requirements" },
2871
+ { role: "worker", goal: "Create README", artifactsRequired: true },
2872
+ { role: "review", goal: "Review generated README" },
2873
+ ],
2874
+ }),
2875
+ resolveRunnerExecutionPlanForRole: (_route, _bot, roleName) => ({
2876
+ mode: "role_profile",
2877
+ roleProfileName: String(roleName || "").trim(),
2878
+ roleProfile: {
2879
+ client: "sample",
2880
+ model: "",
2881
+ permissionMode: String(roleName || "").trim() === "worker" ? "workspace_write" : "read_only",
2882
+ reasoningEffort: String(roleName || "").trim() === "worker" ? "medium" : "low",
2883
+ },
2884
+ workspaceDir,
2885
+ workspaceSource: "selftest",
2886
+ usedCommandFallback: false,
2887
+ }),
2888
+ loadBotRunnerConfig: () => ({
2889
+ roleProfiles: {},
2890
+ botBindings: {},
2891
+ routes: [],
2892
+ projectMappings: {},
2893
+ }),
2894
+ }),
2895
+ buildRunnerDeliveryDeps: () => ({}),
2896
+ buildRunnerRuntimeDeps: () => ({}),
2897
+ resolveConversationPeerBots: () => [
2898
+ { id: "bot-lead-1", name: "RyoAI_bot" },
2899
+ ],
2900
+ },
2901
+ });
2902
+ push(
2903
+ "single_bot_human_execution_request_runs_multi_role_plan",
2904
+ processed.kind === "replied"
2905
+ && aiCalls === 3
2906
+ && deliveryCalls === 1
2907
+ && ensureArray(processed.result?.artifact_paths).includes("README.md")
2908
+ && ensureArray(processed.result?.executed_role_plan?.steps).length === 3,
2909
+ `kind=${String(processed.kind || "(none)")} ai_calls=${aiCalls} delivery_calls=${deliveryCalls} artifacts=${ensureArray(processed.result?.artifact_paths).join(",")} steps=${ensureArray(processed.result?.executed_role_plan?.steps).length}`,
2910
+ );
2911
+ } catch (err) {
2912
+ push("single_bot_human_execution_request_runs_multi_role_plan", false, String(err?.message || err));
2913
+ }
2914
+
2915
+ try {
2916
+ let deliveryCalls = 0;
2917
+ let aiCalls = 0;
2918
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-selftest-multi-step-worker-fail-"));
2919
+ const processed = await processRunnerSelectedRecord({
2920
+ routeKey: "single-bot-multi-step-worker-artifact-required-key",
2921
+ normalizedRoute: normalizeRunnerRoute({
2922
+ name: "telegram-monitor-single-bot-multi-step-worker-fail",
2923
+ project_id: selftestProjectID,
2924
+ provider: "telegram",
2925
+ role: "monitor",
2926
+ role_profile: "monitor",
2927
+ destination_id: "dest-1",
2928
+ destination_label: "Main Room",
2929
+ server_bot_name: "RyoAI_bot",
2930
+ server_bot_id: "bot-lead-1",
2931
+ trigger_policy: {
2932
+ mentions_only: true,
2933
+ direct_messages: true,
2934
+ reply_to_bot_messages: true,
2935
+ },
2936
+ archive_policy: {
2937
+ mirror_replies: true,
2938
+ dedupe_inbound: true,
2939
+ dedupe_outbound: true,
2940
+ skip_bot_messages: true,
2941
+ },
2942
+ dry_run_delivery: true,
2943
+ }),
2944
+ selectedRecord: {
2945
+ id: "comment-single-bot-multi-step-worker-artifact-required",
2946
+ createdAt: "2026-03-17T01:00:01.000Z",
2947
+ parsedArchive: {
2948
+ kind: "telegram_message",
2949
+ chatID: "-100123",
2950
+ chatType: "supergroup",
2951
+ senderIsBot: false,
2952
+ body: "@RyoAI_bot write the README file now.",
2953
+ mentionUsernames: ["RyoAI_bot"],
2954
+ messageID: 1302,
2955
+ },
2956
+ },
2957
+ pendingOrdered: [],
2958
+ bot: {
2959
+ id: "bot-lead-1",
2960
+ name: "RyoAI_bot",
2961
+ username: "RyoAI_bot",
2962
+ role: "monitor",
2963
+ provider: "telegram",
2964
+ },
2965
+ destination: {
2966
+ id: "dest-1",
2967
+ label: "Main Room",
2968
+ provider: "telegram",
2969
+ chatID: "-100123",
2970
+ },
2971
+ archiveThread: {
2972
+ threadID: "thread-1",
2973
+ workItemID: "work-item-1",
2974
+ },
2975
+ executionPlan: {
2976
+ mode: "role_profile",
2977
+ roleProfileName: "monitor",
2978
+ roleProfile: {
2979
+ client: "sample",
2980
+ model: "",
2981
+ permissionMode: "read_only",
2982
+ reasoningEffort: "low",
2983
+ },
2984
+ workspaceDir,
2985
+ workspaceSource: "selftest",
2986
+ usedCommandFallback: false,
2987
+ },
2988
+ runtime: {
2989
+ baseURL: "https://example.test",
2990
+ token: "selftest-token",
2991
+ timeoutSeconds: 30,
2992
+ actor: { user_id: "user-1" },
2993
+ },
2994
+ deps: {
2995
+ saveRunnerRouteState: () => {},
2996
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
2997
+ runRunnerAIExecution: async ({ inputPayload }) => {
2998
+ aiCalls += 1;
2999
+ const stepRole = String(inputPayload?.execution_step?.role || "").trim();
3000
+ if (stepRole === "monitor") {
3001
+ return {
3002
+ skip: false,
3003
+ reply: "Inspected the request.",
3004
+ };
3005
+ }
3006
+ if (stepRole === "worker") {
3007
+ return {
3008
+ skip: false,
3009
+ reply: "Started the README work.",
3010
+ artifacts: [],
3011
+ };
3012
+ }
3013
+ throw new Error(`unexpected step role ${stepRole}`);
3014
+ },
3015
+ performLocalBotDelivery: async () => {
3016
+ deliveryCalls += 1;
3017
+ return {
3018
+ delivery: { dryRun: true, body: {} },
3019
+ archive: {},
3020
+ };
3021
+ },
3022
+ serializeRunnerTriggerPolicy: (value) => value,
3023
+ serializeRunnerArchivePolicy: (value) => value,
3024
+ buildRunnerExecutionDeps: () => ({
3025
+ validateWorkspaceArtifacts,
3026
+ analyzeHumanConversationIntentWithAI: async () => ({
3027
+ mode: "single_bot",
3028
+ lead_bot: "ryoai_bot",
3029
+ participants: ["ryoai_bot"],
3030
+ initial_responders: ["ryoai_bot"],
3031
+ allowed_responders: ["ryoai_bot"],
3032
+ summary_bot: "",
3033
+ allow_bot_to_bot: false,
3034
+ reply_expectation: "actionable",
3035
+ }),
3036
+ planRoleExecutionWithAI: async () => ({
3037
+ requiresExecution: true,
3038
+ summaryRole: "monitor",
3039
+ steps: [
3040
+ { role: "monitor", goal: "Inspect request" },
3041
+ { role: "worker", goal: "Write README", artifactsRequired: true },
3042
+ ],
3043
+ }),
3044
+ resolveRunnerExecutionPlanForRole: (_route, _bot, roleName) => ({
3045
+ mode: "role_profile",
3046
+ roleProfileName: String(roleName || "").trim(),
3047
+ roleProfile: {
3048
+ client: "sample",
3049
+ model: "",
3050
+ permissionMode: String(roleName || "").trim() === "worker" ? "workspace_write" : "read_only",
3051
+ reasoningEffort: "low",
3052
+ },
3053
+ workspaceDir,
3054
+ workspaceSource: "selftest",
3055
+ usedCommandFallback: false,
3056
+ }),
3057
+ loadBotRunnerConfig: () => ({
3058
+ roleProfiles: {},
3059
+ botBindings: {},
3060
+ routes: [],
3061
+ projectMappings: {},
3062
+ }),
3063
+ }),
3064
+ buildRunnerDeliveryDeps: () => ({}),
3065
+ buildRunnerRuntimeDeps: () => ({}),
3066
+ resolveConversationPeerBots: () => [
3067
+ { id: "bot-lead-1", name: "RyoAI_bot" },
3068
+ ],
3069
+ },
3070
+ });
3071
+ push(
3072
+ "single_bot_worker_step_requires_artifacts_in_multi_role_plan",
3073
+ processed.kind === "error"
3074
+ && String(processed.result?.outcome || "") === "execution_failed"
3075
+ && deliveryCalls === 0
3076
+ && /validated project artifacts/i.test(String(processed.result?.detail || "")),
3077
+ `kind=${String(processed.kind || "(none)")} outcome=${String(processed.result?.outcome || "(none)")} ai_calls=${aiCalls} delivery_calls=${deliveryCalls} detail=${String(processed.result?.detail || "(none)")}`,
3078
+ );
3079
+ } catch (err) {
3080
+ push("single_bot_worker_step_requires_artifacts_in_multi_role_plan", false, String(err?.message || err));
3081
+ }
3082
+
2734
3083
  try {
2735
3084
  let aiCalls = 0;
2736
3085
  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.146",
3
+ "version": "0.2.147",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [