metheus-governance-mcp-cli 0.2.145 → 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/README.md CHANGED
@@ -27,6 +27,7 @@ Install creates local provider settings templates here:
27
27
  - `~/.metheus/slack.env`
28
28
  - `~/.metheus/kakaotalk.env`
29
29
  - `~/.metheus/bot-runner.json`
30
+ - `~/.metheus/project-workspaces.json`
30
31
 
31
32
  These files are for local provider bot secrets, local transport options, and optional per-bot local AI binding.
32
33
 
@@ -181,6 +182,7 @@ metheus-governance-mcp-cli setup --project-id <project_uuid> --ctxpack-key "<ctx
181
182
  - `~/.metheus/slack.env`
182
183
  - `~/.metheus/kakaotalk.env`
183
184
  - `~/.metheus/bot-runner.json`
185
+ - `~/.metheus/project-workspaces.json`
184
186
 
185
187
  Bootstrap or setup does not create one file per server bot automatically. Per-bot Telegram files are created later when you run `bot add`, `bot verify`, or `bot edit` against a real server bot identity.
186
188
 
@@ -195,10 +197,13 @@ Fill provider bot secrets and provider-local transport options locally. Project
195
197
  `~/.metheus/bot-runner.json` is the local automation profile for:
196
198
  - which project to watch
197
199
  - which provider/role bot profile to use
198
- - which `project_id -> workspace_dir` mapping to apply locally
199
200
  - which role profile maps to which local CLI/model/permission/reasoning policy
200
201
  - which server bot maps to which local LLM execution profile via `telegram-bots/global.env`, `telegram-bots/*.env`, or fallback `bot_bindings`
201
202
 
203
+ `~/.metheus/project-workspaces.json` is the local project workspace registry for:
204
+ - which `project_id -> workspace_dir` mapping to apply locally
205
+ - which workspace source updated that mapping last
206
+
202
207
  Built-in helper command for legacy fallback/testing:
203
208
 
204
209
  ```bash
package/cli.mjs CHANGED
@@ -12,7 +12,9 @@ import https from "node:https";
12
12
  import {
13
13
  DEFAULT_LOCAL_AI_CLIENT,
14
14
  analyzeHumanConversationIntentWithAI,
15
+ auditDirectHumanReplyWithAI,
15
16
  normalizeExecutionArtifacts,
17
+ planRoleExecutionWithAI,
16
18
  resolveLocalAIExecutionModel,
17
19
  resolveGeminiReasoningConfig,
18
20
  suggestLocalAIModelDisplayName,
@@ -142,6 +144,7 @@ import {
142
144
  } from "./lib/runner-delivery.mjs";
143
145
  import {
144
146
  resolveRunnerExecutionPlan,
147
+ resolveRunnerExecutionPlanForRole,
145
148
  resolveRunnerRoleProfile,
146
149
  resolveRunnerWorkspaceSelection,
147
150
  runRunnerAIExecution,
@@ -2998,16 +3001,20 @@ function buildRunnerDeliveryDeps() {
2998
3001
  function buildRunnerExecutionDeps() {
2999
3002
  return {
3000
3003
  analyzeHumanConversationIntentWithAI,
3004
+ auditDirectHumanReplyWithAI,
3005
+ planRoleExecutionWithAI,
3001
3006
  normalizeRunnerRoleProfileName,
3002
3007
  normalizeRunnerRoleProfile,
3003
3008
  normalizeBotRunnerProjectMapping,
3004
3009
  sanitizeWorkspaceCandidate,
3005
3010
  loadProviderEnvConfig,
3011
+ loadBotRunnerConfig,
3006
3012
  canUseLegacyRunnerCommand,
3007
3013
  hasLegacyRunnerCommand,
3008
3014
  isLegacyRunnerCommandOptInEnabled,
3009
3015
  legacyRunnerCommandDisabledMessage,
3010
3016
  normalizeExecutionArtifacts,
3017
+ resolveRunnerExecutionPlanForRole,
3011
3018
  validateWorkspaceArtifacts,
3012
3019
  tryJsonParse,
3013
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.",
@@ -1209,7 +1285,10 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1209
1285
  ? `A previous attempt incorrectly skipped the reply. Previous skip reason: ${String(responseContract.previous_skip_reason || "").trim() || "-"}.`
1210
1286
  : "Skip is not allowed for this message.",
1211
1287
  responseContract.require_actionable_contract === true
1212
- ? "This reply must include an actionable execution contract. Do not promise future work without immediate delegation or an immediate result."
1288
+ ? "The human intent requires immediate execution, not a placeholder status update. This reply must include an actionable execution contract. Do not promise future work without immediate delegation or an immediate result."
1289
+ : "",
1290
+ responseContract.require_actionable_contract === true && Array.isArray(responseContract.allowed_contract_types) && responseContract.allowed_contract_types.length > 0
1291
+ ? `Allowed execution contract types for this reply: ${responseContract.allowed_contract_types.join(", ")}.`
1213
1292
  : "",
1214
1293
  "",
1215
1294
  );
@@ -1229,7 +1308,9 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1229
1308
  "- If no project files changed, return artifacts: [].",
1230
1309
  "- Do not claim that a file, plan, document, or code change is complete unless the corresponding artifact path is present in artifacts.",
1231
1310
  "",
1232
- 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
1233
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\"]}}."
1234
1315
  : terse
1235
1316
  ? "Return JSON only in one line: {\"reply\":\"...\",\"artifacts\":[]} or {\"skip\":true,\"reason\":\"...\"}."
@@ -1397,9 +1478,10 @@ function buildConversationIntentAnalysisPrompt({
1397
1478
  "Infer the human's intended bot participation contract from the human message only.",
1398
1479
  "Do not infer from prior bot replies. Do not invent bots outside managed_bots.",
1399
1480
  "Be conservative. If the request is ambiguous, choose single_bot and keep only the directly addressed bot as the responder.",
1481
+ "Also decide whether the human is asking for immediate execution/work now or only asking for information/explanation.",
1400
1482
  "",
1401
1483
  "Return strict JSON only with this shape:",
1402
- "{\"mode\":\"single_bot|delegated_single_lead|multi_bot_collab|multi_bot_direct\",\"lead_bot\":\"<username or empty>\",\"participants\":[\"...\"],\"initial_responders\":[\"...\"],\"allowed_responders\":[\"...\"],\"summary_bot\":\"<username or empty>\",\"allow_bot_to_bot\":true|false}",
1484
+ "{\"mode\":\"single_bot|delegated_single_lead|multi_bot_collab|multi_bot_direct\",\"lead_bot\":\"<username or empty>\",\"participants\":[\"...\"],\"initial_responders\":[\"...\"],\"allowed_responders\":[\"...\"],\"summary_bot\":\"<username or empty>\",\"allow_bot_to_bot\":true|false,\"reply_expectation\":\"informational|actionable\"}",
1403
1485
  "",
1404
1486
  "Mode definitions:",
1405
1487
  "- single_bot: only one directly addressed bot should respond. No bot-to-bot relay.",
@@ -1414,12 +1496,96 @@ function buildConversationIntentAnalysisPrompt({
1414
1496
  "- If mode is single_bot, initial_responders and allowed_responders should contain only that bot.",
1415
1497
  "- If mode is delegated_single_lead, lead_bot must be set and initial_responders should contain only the lead bot.",
1416
1498
  "- If the human explicitly asks one bot to summarize or finalize, set summary_bot to that bot.",
1499
+ "- reply_expectation=actionable when the human is asking the bot(s) to actually do work now, produce concrete results, create/update files, delegate concrete tasks, or otherwise execute immediately.",
1500
+ "- reply_expectation=informational when the human is only asking for explanation, status, location, clarification, or other non-execution information.",
1417
1501
  "",
1418
1502
  `managed_bots=${JSON.stringify(bots)}`,
1419
1503
  `human_message=${JSON.stringify(String(messageText || "").trim())}`,
1420
1504
  ].join("\n");
1421
1505
  }
1422
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
+
1558
+ function buildDirectReplyGuardrailPrompt({
1559
+ humanMessageText,
1560
+ botReplyText,
1561
+ currentBotUsername,
1562
+ managedBots,
1563
+ }) {
1564
+ const bots = ensureArray(managedBots).map((item) => {
1565
+ const bot = safeObject(item);
1566
+ return {
1567
+ username: String(bot.username || "").trim().replace(/^@+/, ""),
1568
+ display_name: String(bot.display_name || bot.displayName || bot.username || "").trim(),
1569
+ };
1570
+ }).filter((item) => item.username);
1571
+ return [
1572
+ "You are an execution guardrail auditor for managed Telegram bots.",
1573
+ "Decide whether the human message requires immediate execution/action now, or whether a plain informational reply is sufficient.",
1574
+ "Then decide whether the candidate bot reply actually satisfies that requirement.",
1575
+ "Judge by meaning, not by keywords.",
1576
+ "A placeholder or status-only reply such as 'I will start', 'please wait', 'I will analyze and come back', or 'I will plan first' does NOT satisfy an actionable request.",
1577
+ "If the human is asking the bot to do work now, create/update files now, delegate concrete tasks now, or otherwise execute immediately, then requires_actionable_contract should be true.",
1578
+ "",
1579
+ "Return strict JSON only with this shape:",
1580
+ "{\"requires_actionable_contract\":true|false,\"reply_satisfies_request\":true|false,\"reason\":\"short explanation\"}",
1581
+ "",
1582
+ `current_bot_username=${JSON.stringify(String(currentBotUsername || "").trim().replace(/^@+/, ""))}`,
1583
+ `managed_bots=${JSON.stringify(bots)}`,
1584
+ `human_message=${JSON.stringify(String(humanMessageText || "").trim())}`,
1585
+ `candidate_reply=${JSON.stringify(String(botReplyText || "").trim())}`,
1586
+ ].join("\n");
1587
+ }
1588
+
1423
1589
  function normalizeIntentContractSelectorList(values, managedSelectorSet) {
1424
1590
  return Array.from(new Set(ensureArray(values)
1425
1591
  .map((value) => String(value || "").trim().replace(/^@+/, "").toLowerCase())
@@ -1472,9 +1638,107 @@ export function analyzeHumanConversationIntentWithAI({
1472
1638
  allowed_responders: normalizeIntentContractSelectorList(parsed.allowed_responders || parsed.allowedResponders, managedSelectorSet),
1473
1639
  summary_bot: String(parsed.summary_bot || parsed.summaryBot || "").trim().replace(/^@+/, "").toLowerCase(),
1474
1640
  allow_bot_to_bot: parsed.allow_bot_to_bot === true || parsed.allowBotToBot === true,
1641
+ reply_expectation: String(parsed.reply_expectation || parsed.replyExpectation || "").trim().toLowerCase(),
1642
+ };
1643
+ }
1644
+
1645
+ export function auditDirectHumanReplyWithAI({
1646
+ humanMessageText,
1647
+ botReplyText,
1648
+ currentBotUsername,
1649
+ managedBots,
1650
+ workspaceDir,
1651
+ client = "",
1652
+ model = "",
1653
+ env = process.env,
1654
+ }) {
1655
+ const bots = ensureArray(managedBots);
1656
+ if (!bots.length) {
1657
+ return null;
1658
+ }
1659
+ const parserClient = normalizeLocalAIClientName(
1660
+ String(client || env?.METHEUS_INTENT_PARSER_CLIENT || "").trim(),
1661
+ "gpt",
1662
+ );
1663
+ const parserModel = String(model || env?.METHEUS_INTENT_PARSER_MODEL || suggestLocalAIModelDisplayName(parserClient, "") || "").trim();
1664
+ const rawText = runLocalAIPromptRawText({
1665
+ client: parserClient,
1666
+ promptText: buildDirectReplyGuardrailPrompt({
1667
+ humanMessageText,
1668
+ botReplyText,
1669
+ currentBotUsername,
1670
+ managedBots: bots,
1671
+ }),
1672
+ workspaceDir,
1673
+ model: parserModel,
1674
+ permissionMode: "read_only",
1675
+ reasoningEffort: String(env?.METHEUS_INTENT_PARSER_REASONING_EFFORT || "low").trim() || "low",
1676
+ env,
1677
+ });
1678
+ const parsed = tryJsonParse(rawText) || tryParseEmbeddedJsonObject(rawText);
1679
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1680
+ throw new Error("direct reply guardrail auditor did not return a JSON object");
1681
+ }
1682
+ return {
1683
+ requires_actionable_contract: parsed.requires_actionable_contract === true || parsed.requiresActionableContract === true,
1684
+ reply_satisfies_request: parsed.reply_satisfies_request === true || parsed.replySatisfiesRequest === true,
1685
+ reason: String(parsed.reason || "").trim(),
1475
1686
  };
1476
1687
  }
1477
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
+
1478
1742
  export function runLocalAIClient({
1479
1743
  client,
1480
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,