metheus-governance-mcp-cli 0.2.225 → 0.2.227

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
@@ -2073,13 +2073,38 @@ function writeTextFileAtomic(filePath, text) {
2073
2073
  `.${path.basename(normalizedPath)}.${process.pid}.${Date.now()}.tmp`,
2074
2074
  );
2075
2075
  fs.writeFileSync(tempPath, text, "utf8");
2076
+ const renameRetryDelaysMs = [0, 20, 50, 100, 200];
2077
+ const sleepBuffer = new SharedArrayBuffer(4);
2078
+ const sleepArray = new Int32Array(sleepBuffer);
2079
+ let lastRenameError = null;
2076
2080
  try {
2077
- fs.renameSync(tempPath, normalizedPath);
2078
- } catch (error) {
2079
- try {
2080
- fs.rmSync(normalizedPath, { force: true });
2081
- } catch {}
2082
- fs.renameSync(tempPath, normalizedPath);
2081
+ for (const delayMs of renameRetryDelaysMs) {
2082
+ if (delayMs > 0) {
2083
+ Atomics.wait(sleepArray, 0, 0, delayMs);
2084
+ }
2085
+ try {
2086
+ fs.renameSync(tempPath, normalizedPath);
2087
+ lastRenameError = null;
2088
+ break;
2089
+ } catch (error) {
2090
+ lastRenameError = error;
2091
+ try {
2092
+ fs.rmSync(normalizedPath, { force: true });
2093
+ fs.renameSync(tempPath, normalizedPath);
2094
+ lastRenameError = null;
2095
+ break;
2096
+ } catch (retryError) {
2097
+ lastRenameError = retryError;
2098
+ const errorCode = String(retryError?.code || error?.code || "").trim().toUpperCase();
2099
+ if (!["EPERM", "EBUSY", "ENOTEMPTY"].includes(errorCode)) {
2100
+ throw retryError;
2101
+ }
2102
+ }
2103
+ }
2104
+ }
2105
+ if (lastRenameError) {
2106
+ throw lastRenameError;
2107
+ }
2083
2108
  } finally {
2084
2109
  try {
2085
2110
  if (fs.existsSync(tempPath)) {
@@ -2397,6 +2422,11 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2397
2422
  response_contract_validation_targets: ensureArray(entry.response_contract_validation_targets || entry.responseContractValidationTargets)
2398
2423
  .map((value) => normalizeTelegramMentionUsername(value))
2399
2424
  .filter(Boolean),
2425
+ assignment_validation_status: String(entry.assignment_validation_status || entry.assignmentValidationStatus || "").trim().toLowerCase(),
2426
+ assignment_validation_reason: String(entry.assignment_validation_reason || entry.assignmentValidationReason || "").trim(),
2427
+ assignment_validation_modes: ensureArray(entry.assignment_validation_modes || entry.assignmentValidationModes)
2428
+ .map((value) => String(value || "").trim().toLowerCase())
2429
+ .filter(Boolean),
2400
2430
  delivery_status: String(entry.delivery_status || entry.deliveryStatus || "").trim().toLowerCase(),
2401
2431
  archive_status: String(entry.archive_status || entry.archiveStatus || "").trim().toLowerCase(),
2402
2432
  transport_error: String(entry.transport_error || entry.transportError || "").trim(),
@@ -2427,15 +2457,17 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
2427
2457
  function normalizeBotRunnerConsumedComments(rawConsumed, nowMs = Date.now()) {
2428
2458
  const normalized = {};
2429
2459
  for (const [commentIDRaw, entryRaw] of Object.entries(safeObject(rawConsumed))) {
2430
- const commentID = String(commentIDRaw || safeObject(entryRaw).comment_id || "").trim();
2431
- if (!commentID) continue;
2432
2460
  const entry = safeObject(entryRaw);
2461
+ const commentID = String(entry.comment_id || commentIDRaw || "").trim();
2462
+ if (!commentID) continue;
2463
+ const ledgerKey = String(commentIDRaw || commentID).trim();
2433
2464
  const consumedAt = firstNonEmptyString([entry.consumed_at, entry.consumedAt, entry.updated_at, entry.updatedAt]);
2434
2465
  const consumedAtMs = Date.parse(consumedAt);
2435
2466
  if (Number.isFinite(consumedAtMs) && nowMs - consumedAtMs > BOT_RUNNER_REQUEST_KEEP_MS) {
2436
2467
  continue;
2437
2468
  }
2438
- normalized[commentID] = {
2469
+ normalized[ledgerKey] = {
2470
+ ledger_key: ledgerKey,
2439
2471
  comment_id: commentID,
2440
2472
  project_id: String(entry.project_id || entry.projectID || "").trim(),
2441
2473
  provider: String(entry.provider || "").trim(),
@@ -2451,6 +2483,38 @@ function normalizeBotRunnerConsumedComments(rawConsumed, nowMs = Date.now()) {
2451
2483
  return normalized;
2452
2484
  }
2453
2485
 
2486
+ function buildRunnerConsumedCommentLedgerKey(commentIDRaw, routeKeyRaw = "", commentKindRaw = "") {
2487
+ const commentID = String(commentIDRaw || "").trim();
2488
+ if (!commentID) return "";
2489
+ const routeKey = String(routeKeyRaw || "").trim();
2490
+ const commentKind = String(commentKindRaw || "").trim().toLowerCase();
2491
+ if (commentKind === "bot_reply" && routeKey) {
2492
+ return `${commentID}::${routeKey}`;
2493
+ }
2494
+ return commentID;
2495
+ }
2496
+
2497
+ function findRunnerConsumedCommentEntry(rawConsumed, commentIDRaw, { routeKey = "", commentKind = "" } = {}) {
2498
+ const commentID = String(commentIDRaw || "").trim();
2499
+ if (!commentID) return {};
2500
+ const consumedComments = normalizeBotRunnerConsumedComments(rawConsumed);
2501
+ const ledgerKey = buildRunnerConsumedCommentLedgerKey(commentID, routeKey, commentKind);
2502
+ const directEntry = safeObject(consumedComments[ledgerKey]);
2503
+ if (Object.keys(directEntry).length) {
2504
+ return directEntry;
2505
+ }
2506
+ const normalizedKind = String(commentKind || "").trim().toLowerCase();
2507
+ if (normalizedKind === "bot_reply" && String(routeKey || "").trim()) {
2508
+ const routeEntry = Object.values(consumedComments).find((entryRaw) => {
2509
+ const entry = safeObject(entryRaw);
2510
+ return String(entry.comment_id || "").trim() === commentID
2511
+ && String(entry.route_key || "").trim() === String(routeKey || "").trim();
2512
+ });
2513
+ return safeObject(routeEntry);
2514
+ }
2515
+ return safeObject(consumedComments[commentID]);
2516
+ }
2517
+
2454
2518
  function runnerRouteMatchesProjectConversationScope(candidateRouteRaw, normalizedRouteRaw) {
2455
2519
  const candidateRoute = normalizeRunnerRoute(candidateRouteRaw);
2456
2520
  const normalizedRoute = normalizeRunnerRoute(normalizedRouteRaw);
@@ -3320,7 +3384,8 @@ function upsertRunnerConsumedComment(state, commentIDRaw, patch = {}) {
3320
3384
  const commentID = String(commentIDRaw || "").trim();
3321
3385
  const currentState = safeObject(state);
3322
3386
  const consumedComments = normalizeBotRunnerConsumedComments(currentState.consumedComments || currentState.consumed_comments);
3323
- const existing = safeObject(consumedComments[commentID]);
3387
+ const ledgerKey = buildRunnerConsumedCommentLedgerKey(commentID, patch.route_key, patch.comment_kind);
3388
+ const existing = safeObject(consumedComments[ledgerKey]);
3324
3389
  const nextEntry = {
3325
3390
  ...existing,
3326
3391
  ...safeObject(patch),
@@ -3330,7 +3395,7 @@ function upsertRunnerConsumedComment(state, commentIDRaw, patch = {}) {
3330
3395
  consumed_at: new Date().toISOString(),
3331
3396
  request_status: normalizeRunnerRequestStatus(patch.request_status || existing.request_status),
3332
3397
  };
3333
- consumedComments[commentID] = nextEntry;
3398
+ consumedComments[ledgerKey] = nextEntry;
3334
3399
  return {
3335
3400
  consumedComments,
3336
3401
  consumedComment: nextEntry,
@@ -4516,6 +4581,9 @@ function markRunnerRequestLifecycle({
4516
4581
  responseContractValidationStatus = "",
4517
4582
  responseContractValidationReason = "",
4518
4583
  responseContractValidationTargets = [],
4584
+ assignmentValidationStatus = "",
4585
+ assignmentValidationReason = "",
4586
+ assignmentValidationModes = [],
4519
4587
  deliveryStatus = "",
4520
4588
  archiveStatus = "",
4521
4589
  transportError = "",
@@ -4643,6 +4711,18 @@ function markRunnerRequestLifecycle({
4643
4711
  : existing.response_contract_validation_targets,
4644
4712
  normalizeTelegramMentionUsername,
4645
4713
  ),
4714
+ assignment_validation_status: String(
4715
+ assignmentValidationStatus || existing.assignment_validation_status || "",
4716
+ ).trim().toLowerCase(),
4717
+ assignment_validation_reason: String(
4718
+ assignmentValidationReason || existing.assignment_validation_reason || "",
4719
+ ).trim(),
4720
+ assignment_validation_modes: uniqueOrderedStrings(
4721
+ ensureArray(assignmentValidationModes).length
4722
+ ? assignmentValidationModes
4723
+ : existing.assignment_validation_modes,
4724
+ (value) => String(value || "").trim().toLowerCase(),
4725
+ ),
4646
4726
  delivery_status: String(deliveryStatus || existing.delivery_status || "").trim().toLowerCase(),
4647
4727
  archive_status: String(archiveStatus || existing.archive_status || "").trim().toLowerCase(),
4648
4728
  transport_error: String(transportError || existing.transport_error || "").trim(),
@@ -8074,14 +8154,18 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8074
8154
  const commentsForPending = ensureArray(comments).filter((comment) => {
8075
8155
  const normalizedComment = normalizeArchiveCommentRecord(comment, parseArchivedChatComment);
8076
8156
  const commentID = String(normalizedComment.id || "").trim();
8157
+ const parsed = safeObject(normalizedComment.parsedArchive);
8158
+ const commentKind = String(parsed.kind || "").trim().toLowerCase();
8077
8159
  if (commentID && safeObject(excludedComments)[commentID]) {
8078
8160
  return false;
8079
8161
  }
8080
- if (commentID && safeObject(consumedComments)[commentID]) {
8162
+ if (commentID && Object.keys(findRunnerConsumedCommentEntry(consumedComments, commentID, {
8163
+ routeKey,
8164
+ commentKind,
8165
+ })).length > 0) {
8081
8166
  return false;
8082
8167
  }
8083
- const parsed = safeObject(normalizedComment.parsedArchive);
8084
- if (String(parsed.kind || "").trim().toLowerCase() === "bot_reply") {
8168
+ if (commentKind === "bot_reply") {
8085
8169
  const conversationID = String(parsed.conversationID || "").trim();
8086
8170
  if (!conversationID) {
8087
8171
  return false;
@@ -8873,6 +8957,9 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
8873
8957
  responseContractValidationStatus: String(processed.result?.response_contract_validation_status || "").trim(),
8874
8958
  responseContractValidationReason: String(processed.result?.response_contract_validation_reason || "").trim(),
8875
8959
  responseContractValidationTargets: ensureArray(processed.result?.response_contract_validation_targets),
8960
+ assignmentValidationStatus: String(processed.result?.assignment_validation_status || "").trim(),
8961
+ assignmentValidationReason: String(processed.result?.assignment_validation_reason || "").trim(),
8962
+ assignmentValidationModes: ensureArray(processed.result?.assignment_validation_modes),
8876
8963
  deliveryStatus: String(processed.result?.delivery_status || "").trim(),
8877
8964
  archiveStatus: String(processed.result?.archive_status || "").trim(),
8878
8965
  transportError: String(processed.result?.transport_error || "").trim(),
@@ -11293,6 +11380,9 @@ async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
11293
11380
  responseContractValidationStatus: String(processed.result?.response_contract_validation_status || "").trim(),
11294
11381
  responseContractValidationReason: String(processed.result?.response_contract_validation_reason || "").trim(),
11295
11382
  responseContractValidationTargets: ensureArray(processed.result?.response_contract_validation_targets),
11383
+ assignmentValidationStatus: String(processed.result?.assignment_validation_status || "").trim(),
11384
+ assignmentValidationReason: String(processed.result?.assignment_validation_reason || "").trim(),
11385
+ assignmentValidationModes: ensureArray(processed.result?.assignment_validation_modes),
11296
11386
  deliveryStatus: String(processed.result?.delivery_status || "").trim(),
11297
11387
  archiveStatus: String(processed.result?.archive_status || "").trim(),
11298
11388
  transportError: String(processed.result?.transport_error || "").trim(),
@@ -1918,7 +1918,10 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1918
1918
  "Do not mention another managed bot unless the contract explicitly names that bot in assignments or next_responders.",
1919
1919
  "Without a matching contract, newly mentioned bots will not act.",
1920
1920
  "When delegating to another managed bot, use contract.type=\"delegation\" with actionable=true, assignments, and next_responders.",
1921
- "Delegation contract example: {\"type\":\"delegation\",\"actionable\":true,\"assignments\":[{\"target_bot\":\"ryoai2_bot\",\"task\":\"briefly greet in one line\"}],\"next_responders\":[\"ryoai2_bot\"]}.",
1921
+ "Each assignment must declare whether it is a conversational contribution or a real execution task.",
1922
+ "Use assignment.mode=\"conversation_contribution\" for opinions, discussion, review, comparison, synthesis, greetings, or other room-visible contributions that do not require workspace artifacts.",
1923
+ "Use assignment.mode=\"execution_task\" only when the delegated bot must change workspace files, create artifacts, update ctxpack, or produce other concrete project outputs. If it is an execution task, also set artifacts_required=true.",
1924
+ "Delegation contract example: {\"type\":\"delegation\",\"actionable\":true,\"assignments\":[{\"target_bot\":\"ryoai2_bot\",\"task\":\"briefly greet in one line\",\"mode\":\"conversation_contribution\",\"artifacts_required\":false}],\"next_responders\":[\"ryoai2_bot\"]}.",
1922
1925
  ensureArray(responseContract.required_delegation_targets).length > 0
1923
1926
  ? `This reply must delegate to these exact managed bots now: ${ensureArray(responseContract.required_delegation_targets).map((item) => `@${String(item || "").trim().replace(/^@+/, "")}`).join(", ")}.`
1924
1927
  : "",
@@ -1970,7 +1973,10 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1970
1973
  "Do not mention another managed bot unless the contract explicitly names that bot in assignments or next_responders.",
1971
1974
  "Without a matching contract, mentioned bots will not act.",
1972
1975
  "When delegating to another managed bot, use contract.type=\"delegation\" with actionable=true, assignments, and next_responders.",
1973
- "Delegation contract example: {\"type\":\"delegation\",\"actionable\":true,\"assignments\":[{\"target_bot\":\"ryoai2_bot\",\"task\":\"briefly greet in one line\"}],\"next_responders\":[\"ryoai2_bot\"]}.",
1976
+ "Each assignment must declare whether it is a conversational contribution or a real execution task.",
1977
+ "Use assignment.mode=\"conversation_contribution\" for opinions, discussion, review, comparison, synthesis, greetings, or other room-visible contributions that do not require workspace artifacts.",
1978
+ "Use assignment.mode=\"execution_task\" only when the delegated bot must change workspace files, create artifacts, update ctxpack, or produce other concrete project outputs. If it is an execution task, also set artifacts_required=true.",
1979
+ "Delegation contract example: {\"type\":\"delegation\",\"actionable\":true,\"assignments\":[{\"target_bot\":\"ryoai2_bot\",\"task\":\"briefly greet in one line\",\"mode\":\"conversation_contribution\",\"artifacts_required\":false}],\"next_responders\":[\"ryoai2_bot\"]}.",
1974
1980
  ensureArray(responseContract.required_delegation_targets).length > 0
1975
1981
  ? `This reply must delegate to these exact managed bots now: ${ensureArray(responseContract.required_delegation_targets).map((item) => `@${String(item || "").trim().replace(/^@+/, "")}`).join(", ")}.`
1976
1982
  : "",
@@ -2006,7 +2012,7 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
2006
2012
  isInternalExecutionStep
2007
2013
  ? "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\"}],\"ctxpack_files\":[{\"path\":\"relative/path.md\",\"content\":\"full document text\",\"doc_type\":\"guide|readme|agenda|rule|architecture|manifest\",\"operation\":\"create|update|delete\"}],\"work_items\":[{\"title\":\"short atomic task\",\"description\":\"useful implementation detail\"}],\"contract\":{\"type\":\"direct_result|summary_request|final_summary\",\"actionable\":true,\"summary_bot\":\"username\",\"next_responders\":[\"username\"]}}. Use ctxpack_files when ctxpack-backed guidance/instruction files must be authored. If execution_step.ctxpack_update_required is true, ctxpack_files must not be empty. Use artifacts: [] only if this step truly changes no project files, and use work_items: [] only if this step truly creates no governance tasks."
2008
2014
  : responseContract.is_current_bot_candidate === true
2009
- ? "Return JSON only in one line: {\"reply\":\"...\",\"artifacts\":[],\"ctxpack_files\":[],\"work_items\":[]} or {\"clarify\":\"...\"} or {\"skip\":true,\"reason\":\"...\"} or {\"reply\":\"...\",\"artifacts\":[],\"ctxpack_files\":[],\"work_items\":[],\"contract\":{\"type\":\"direct_result|delegation|summary_request|final_summary\",\"actionable\":true,\"assignments\":[{\"target_bot\":\"username\",\"task\":\"...\"}],\"summary_bot\":\"username\",\"next_responders\":[\"username\"]},\"context_suggestion\":{\"should_store\":true,\"title\":\"...\",\"body\":\"...\",\"category\":\"bot_role|operating_rule|project_fact|current_focus|risk|general\",\"importance\":\"low|normal|high|critical\"}}."
2015
+ ? "Return JSON only in one line: {\"reply\":\"...\",\"artifacts\":[],\"ctxpack_files\":[],\"work_items\":[]} or {\"clarify\":\"...\"} or {\"skip\":true,\"reason\":\"...\"} or {\"reply\":\"...\",\"artifacts\":[],\"ctxpack_files\":[],\"work_items\":[],\"contract\":{\"type\":\"direct_result|delegation|summary_request|final_summary\",\"actionable\":true,\"assignments\":[{\"target_bot\":\"username\",\"task\":\"...\",\"mode\":\"conversation_contribution|execution_task\",\"artifacts_required\":true|false}],\"summary_bot\":\"username\",\"next_responders\":[\"username\"]},\"context_suggestion\":{\"should_store\":true,\"title\":\"...\",\"body\":\"...\",\"category\":\"bot_role|operating_rule|project_fact|current_focus|risk|general\",\"importance\":\"low|normal|high|critical\"}}."
2010
2016
  : terse
2011
2017
  ? "Return JSON only in one line: {\"reply\":\"...\",\"artifacts\":[],\"ctxpack_files\":[],\"work_items\":[],\"context_suggestion\":{\"should_store\":true,\"title\":\"...\",\"body\":\"...\",\"category\":\"bot_role|operating_rule|project_fact|current_focus|risk|general\",\"importance\":\"low|normal|high|critical\"}} or {\"clarify\":\"...\"} or {\"skip\":true,\"reason\":\"...\"}."
2012
2018
  : "Return JSON only: {\"reply\":\"...\",\"artifacts\":[],\"ctxpack_files\":[],\"work_items\":[],\"context_suggestion\":{\"should_store\":true,\"title\":\"...\",\"body\":\"...\",\"category\":\"bot_role|operating_rule|project_fact|current_focus|risk|general\",\"importance\":\"low|normal|high|critical\"}} or {\"clarify\":\"...\"} or {\"skip\":true,\"reason\":\"...\"}. Keep the reply concise and directly useful in a group chat.",
@@ -2061,7 +2067,12 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
2061
2067
  }
2062
2068
  if (assignmentsForThisBot.length > 0) {
2063
2069
  lines.push(
2064
- `Current Assignment For This Bot: ${assignmentsForThisBot.map((item) => String(item.task || "").trim()).join(" | ")}`,
2070
+ `Current Assignment For This Bot: ${assignmentsForThisBot.map((item) => {
2071
+ const task = String(item.task || "").trim();
2072
+ const mode = String(item.mode || "").trim();
2073
+ const artifactsRequired = item.artifactsRequired === true || item.artifacts_required === true;
2074
+ return `${task}${mode ? ` [mode=${mode}]` : ""}${artifactsRequired ? " [artifacts_required=yes]" : ""}`;
2075
+ }).join(" | ")}`,
2065
2076
  );
2066
2077
  }
2067
2078
  if (String(currentExecutionContract.summary_bot || currentExecutionContract.summaryBot || "").trim()) {
@@ -2149,11 +2160,13 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
2149
2160
  "A valid follow-up may be a concise opinion, analysis, review, comparison, synthesis, or direct delegated result.",
2150
2161
  "Mentioning the lead bot for acknowledgment or handoff does not require a new delegation contract.",
2151
2162
  "Do not wake any third bot. Only the lead bot may delegate new public work in this conversation.",
2152
- "If you complete your assigned contribution and the lead bot should continue or synthesize, use contract.type=\"summary_request\" with the designated summary bot.",
2163
+ "If any other expected responder besides this bot still remains in the current execution contract, do not emit contract.type=\"summary_request\" yet.",
2164
+ "Only the final remaining contributor may hand control back to the designated summary bot with contract.type=\"summary_request\".",
2153
2165
  );
2154
2166
  } else {
2155
2167
  lines.push(
2156
- "If you complete the delegated work and want the lead bot to continue or summarize, include contract.type=\"summary_request\" with the summary_bot set to the designated summary bot.",
2168
+ "If any other expected responder besides this bot still remains in the current execution contract, do not emit contract.type=\"summary_request\" yet.",
2169
+ "Only the final remaining contributor may hand control back to the designated summary bot with contract.type=\"summary_request\".",
2157
2170
  "Do not wake any third bot. Only the lead bot may delegate work publicly in this conversation.",
2158
2171
  );
2159
2172
  }
@@ -710,6 +710,112 @@ function normalizeReplyExpectation(value, fallback = "informational") {
710
710
  return fallback;
711
711
  }
712
712
 
713
+ function boolFromRunnerRaw(raw, fallback = false) {
714
+ if (raw === true || raw === false) {
715
+ return raw;
716
+ }
717
+ const normalized = String(raw ?? "").trim().toLowerCase();
718
+ if (!normalized) return fallback;
719
+ if (["1", "true", "yes", "y", "on"].includes(normalized)) {
720
+ return true;
721
+ }
722
+ if (["0", "false", "no", "n", "off"].includes(normalized)) {
723
+ return false;
724
+ }
725
+ return fallback;
726
+ }
727
+
728
+ function normalizeExecutionAssignmentMode(rawValue, fallback = "conversation_contribution") {
729
+ const normalized = String(rawValue || "").trim().toLowerCase();
730
+ if (!normalized) {
731
+ return fallback;
732
+ }
733
+ if ([
734
+ "execution_task",
735
+ "workspace_action",
736
+ "artifact_work",
737
+ "artifact_task",
738
+ "file_work",
739
+ "ctxpack_update",
740
+ "workitem_task",
741
+ ].includes(normalized)) {
742
+ return "execution_task";
743
+ }
744
+ if ([
745
+ "conversation_contribution",
746
+ "discussion_contribution",
747
+ "opinion",
748
+ "analysis",
749
+ "review",
750
+ "comparison",
751
+ "summary_contribution",
752
+ "greeting",
753
+ "reply",
754
+ ].includes(normalized)) {
755
+ return "conversation_contribution";
756
+ }
757
+ return fallback;
758
+ }
759
+
760
+ function normalizeExecutionAssignment(rawAssignment, {
761
+ allowedResponderSet = null,
762
+ currentBotSelector = "",
763
+ excludeCurrentTargetAssignments = false,
764
+ } = {}) {
765
+ const assignment = safeObject(rawAssignment);
766
+ const targetBot = normalizeMentionSelector(
767
+ assignment.targetBot
768
+ || assignment.target_bot
769
+ || assignment.bot
770
+ || assignment.username,
771
+ );
772
+ const task = String(
773
+ assignment.task
774
+ || assignment.instruction
775
+ || assignment.assignment
776
+ || assignment.work
777
+ || assignment.description
778
+ || "",
779
+ ).trim();
780
+ if (!targetBot || !task) {
781
+ return null;
782
+ }
783
+ if (allowedResponderSet?.size && !allowedResponderSet.has(targetBot)) {
784
+ return null;
785
+ }
786
+ if (
787
+ excludeCurrentTargetAssignments === true
788
+ && currentBotSelector
789
+ && targetBot === normalizeMentionSelector(currentBotSelector)
790
+ ) {
791
+ return null;
792
+ }
793
+ const artifactsRequired = boolFromRunnerRaw(
794
+ assignment.artifactsRequired ?? assignment.artifacts_required,
795
+ false,
796
+ );
797
+ const workspaceAction = boolFromRunnerRaw(
798
+ assignment.workspaceAction ?? assignment.workspace_action,
799
+ false,
800
+ );
801
+ const mode = normalizeExecutionAssignmentMode(
802
+ assignment.mode
803
+ || assignment.assignment_mode
804
+ || assignment.kind
805
+ || assignment.type,
806
+ (artifactsRequired || workspaceAction) ? "execution_task" : "conversation_contribution",
807
+ );
808
+ const requiresExecution = mode === "execution_task" || artifactsRequired || workspaceAction;
809
+ return {
810
+ targetBot,
811
+ task,
812
+ mode,
813
+ artifactsRequired,
814
+ workspaceAction,
815
+ requiresExecution,
816
+ };
817
+ }
818
+
713
819
  function normalizeHumanIntentType(value, fallback = "") {
714
820
  const normalized = String(value || "").trim().toLowerCase();
715
821
  if ([
@@ -1811,14 +1917,84 @@ function buildFallbackArchivedBotReplyConversationContext({
1811
1917
  };
1812
1918
  }
1813
1919
 
1814
- function extractCurrentAssignmentTasks(conversationContext, currentBotSelector) {
1920
+ function extractCurrentAssignmentsForBot(conversationContext, currentBotSelector) {
1815
1921
  const currentSelector = normalizeMentionSelector(currentBotSelector);
1816
1922
  if (!currentSelector) return [];
1817
1923
  return ensureArray(safeObject(conversationContext?.executionContract).assignments)
1818
1924
  .map((item) => safeObject(item))
1819
1925
  .filter((item) => normalizeMentionSelector(item.targetBot || item.target_bot) === currentSelector)
1820
- .map((item) => String(item.task || item.instruction || "").trim())
1821
- .filter(Boolean);
1926
+ .map((item) => ({
1927
+ targetBot: normalizeMentionSelector(item.targetBot || item.target_bot),
1928
+ task: String(item.task || item.instruction || "").trim(),
1929
+ mode: normalizeExecutionAssignmentMode(
1930
+ item.mode || item.assignment_mode || item.kind || item.type,
1931
+ item.requiresExecution === true
1932
+ || item.artifactsRequired === true
1933
+ || item.workspaceAction === true
1934
+ || item.artifacts_required === true
1935
+ || item.workspace_action === true
1936
+ ? "execution_task"
1937
+ : "conversation_contribution",
1938
+ ),
1939
+ artifactsRequired: item.artifactsRequired === true || item.artifacts_required === true,
1940
+ workspaceAction: item.workspaceAction === true || item.workspace_action === true,
1941
+ requiresExecution: item.requiresExecution === true
1942
+ || item.artifactsRequired === true
1943
+ || item.workspaceAction === true
1944
+ || item.artifacts_required === true
1945
+ || item.workspace_action === true
1946
+ || normalizeExecutionAssignmentMode(
1947
+ item.mode || item.assignment_mode || item.kind || item.type,
1948
+ "conversation_contribution",
1949
+ ) === "execution_task",
1950
+ }))
1951
+ .filter((item) => item.task);
1952
+ }
1953
+
1954
+ function summarizeCurrentAssignmentExecutionValidation(conversationContext, currentBotSelector) {
1955
+ const assignments = extractCurrentAssignmentsForBot(conversationContext, currentBotSelector);
1956
+ const assignmentModes = uniqueOrdered(
1957
+ assignments.map((item) => String(item.mode || "").trim()).filter(Boolean),
1958
+ );
1959
+ const executionAssignments = assignments.filter((item) => item.requiresExecution === true);
1960
+ const conversationAssignments = assignments.filter((item) => item.requiresExecution !== true);
1961
+ if (!assignments.length) {
1962
+ return {
1963
+ status: "no_assignment",
1964
+ reason: "no assignment for current bot in the active execution contract",
1965
+ assignmentModes: [],
1966
+ assignments: [],
1967
+ executionAssignments: [],
1968
+ conversationAssignments: [],
1969
+ executionTasks: [],
1970
+ allTasks: [],
1971
+ requiresPlanner: false,
1972
+ };
1973
+ }
1974
+ if (executionAssignments.length > 0) {
1975
+ return {
1976
+ status: "execution_assignment",
1977
+ reason: "active assignment explicitly requires workspace/artifact execution",
1978
+ assignmentModes,
1979
+ assignments,
1980
+ executionAssignments,
1981
+ conversationAssignments,
1982
+ executionTasks: executionAssignments.map((item) => String(item.task || "").trim()).filter(Boolean),
1983
+ allTasks: assignments.map((item) => String(item.task || "").trim()).filter(Boolean),
1984
+ requiresPlanner: true,
1985
+ };
1986
+ }
1987
+ return {
1988
+ status: "conversation_assignment",
1989
+ reason: "active assignment is a conversational contribution and does not require planner/worker execution",
1990
+ assignmentModes,
1991
+ assignments,
1992
+ executionAssignments: [],
1993
+ conversationAssignments,
1994
+ executionTasks: [],
1995
+ allTasks: assignments.map((item) => String(item.task || "").trim()).filter(Boolean),
1996
+ requiresPlanner: false,
1997
+ };
1822
1998
  }
1823
1999
 
1824
2000
  function requiresArtifactsForExecutionStep(step) {
@@ -2557,6 +2733,7 @@ async function maybeExecuteDynamicRolePlan({
2557
2733
  saveRunnerRouteState,
2558
2734
  runRunnerAIExecution,
2559
2735
  validateWorkspaceArtifacts,
2736
+ assignmentExecutionValidation = null,
2560
2737
  }) {
2561
2738
  const planner = typeof executionDeps.planRoleExecutionWithAI === "function"
2562
2739
  ? executionDeps.planRoleExecutionWithAI
@@ -2576,8 +2753,12 @@ async function maybeExecuteDynamicRolePlan({
2576
2753
  if (!planner || !resolveRunnerExecutionPlanForRole) {
2577
2754
  return null;
2578
2755
  }
2579
- const currentBotSelector = normalizeMentionSelector(bot?.username || bot?.name);
2580
- const assignmentTasks = extractCurrentAssignmentTasks(conversationContext, currentBotSelector);
2756
+ const assignmentValidation = safeObject(assignmentExecutionValidation);
2757
+ const assignmentValidationStatus = String(assignmentValidation.status || "").trim();
2758
+ const assignmentValidationReason = String(assignmentValidation.reason || "").trim();
2759
+ const assignmentTasks = ensureArray(assignmentValidation.executionTasks)
2760
+ .map((item) => String(item || "").trim())
2761
+ .filter(Boolean);
2581
2762
  const humanIntentType = normalizeHumanIntentType(
2582
2763
  safeObject(directHumanResponseContract).intentType
2583
2764
  || safeObject(safeObject(humanIntentContext).humanIntent).intentType,
@@ -2610,7 +2791,7 @@ async function maybeExecuteDynamicRolePlan({
2610
2791
  ) {
2611
2792
  return null;
2612
2793
  }
2613
- const shouldPlanExecution = assignmentTasks.length > 0 || (
2794
+ const shouldPlanExecution = assignmentValidation.requiresPlanner === true || (
2614
2795
  triggerDecision.requiresDirectReply === true
2615
2796
  && humanIntentMode === "single_bot"
2616
2797
  && actionableReplyExpectation
@@ -2756,6 +2937,8 @@ async function maybeExecuteDynamicRolePlan({
2756
2937
  last_conversation_id: String(conversationContext?.id || "").trim(),
2757
2938
  last_conversation_stage: String(conversationContext?.stage || "").trim(),
2758
2939
  last_workspace_dir: String(executionPlan.workspaceDir || "").trim(),
2940
+ last_assignment_validation_status: assignmentValidationStatus,
2941
+ last_assignment_validation_reason: assignmentValidationReason,
2759
2942
  ...safeObject(intentStatePatch),
2760
2943
  }),
2761
2944
  );
@@ -2769,6 +2952,9 @@ async function maybeExecuteDynamicRolePlan({
2769
2952
  thread_id: archiveThread.threadID,
2770
2953
  comment_id: selectedRecord.id,
2771
2954
  trigger_kind: String(triggerDecision.trigger || "").trim(),
2955
+ assignment_validation_status: assignmentValidationStatus,
2956
+ assignment_validation_reason: assignmentValidationReason,
2957
+ assignment_validation_modes: ensureArray(assignmentValidation.assignmentModes),
2772
2958
  },
2773
2959
  };
2774
2960
  }
@@ -2786,6 +2972,8 @@ async function maybeExecuteDynamicRolePlan({
2786
2972
  last_conversation_id: String(conversationContext?.id || "").trim(),
2787
2973
  last_conversation_stage: String(conversationContext?.stage || "").trim(),
2788
2974
  last_workspace_dir: String(executionPlan.workspaceDir || "").trim(),
2975
+ last_assignment_validation_status: assignmentValidationStatus,
2976
+ last_assignment_validation_reason: assignmentValidationReason,
2789
2977
  ...safeObject(intentStatePatch),
2790
2978
  }),
2791
2979
  );
@@ -2799,6 +2987,9 @@ async function maybeExecuteDynamicRolePlan({
2799
2987
  thread_id: archiveThread.threadID,
2800
2988
  comment_id: selectedRecord.id,
2801
2989
  trigger_kind: String(triggerDecision.trigger || "").trim(),
2990
+ assignment_validation_status: assignmentValidationStatus,
2991
+ assignment_validation_reason: assignmentValidationReason,
2992
+ assignment_validation_modes: ensureArray(assignmentValidation.assignmentModes),
2802
2993
  },
2803
2994
  };
2804
2995
  }
@@ -2833,6 +3024,8 @@ async function maybeExecuteDynamicRolePlan({
2833
3024
  last_conversation_id: String(conversationContext?.id || "").trim(),
2834
3025
  last_conversation_stage: String(conversationContext?.stage || "").trim(),
2835
3026
  last_workspace_dir: String(executionPlan.workspaceDir || "").trim(),
3027
+ last_assignment_validation_status: assignmentValidationStatus,
3028
+ last_assignment_validation_reason: assignmentValidationReason,
2836
3029
  ...safeObject(intentStatePatch),
2837
3030
  }),
2838
3031
  );
@@ -2846,6 +3039,9 @@ async function maybeExecuteDynamicRolePlan({
2846
3039
  thread_id: archiveThread.threadID,
2847
3040
  comment_id: selectedRecord.id,
2848
3041
  trigger_kind: String(triggerDecision.trigger || "").trim(),
3042
+ assignment_validation_status: assignmentValidationStatus,
3043
+ assignment_validation_reason: assignmentValidationReason,
3044
+ assignment_validation_modes: ensureArray(assignmentValidation.assignmentModes),
2849
3045
  },
2850
3046
  };
2851
3047
  }
@@ -2862,6 +3058,8 @@ async function maybeExecuteDynamicRolePlan({
2862
3058
  last_conversation_id: String(conversationContext?.id || "").trim(),
2863
3059
  last_conversation_stage: String(conversationContext?.stage || "").trim(),
2864
3060
  last_workspace_dir: String(stepExecutionPlan.workspaceDir || executionPlan.workspaceDir || "").trim(),
3061
+ last_assignment_validation_status: assignmentValidationStatus,
3062
+ last_assignment_validation_reason: assignmentValidationReason,
2865
3063
  ...safeObject(intentStatePatch),
2866
3064
  }),
2867
3065
  );
@@ -2875,6 +3073,9 @@ async function maybeExecuteDynamicRolePlan({
2875
3073
  thread_id: archiveThread.threadID,
2876
3074
  comment_id: selectedRecord.id,
2877
3075
  trigger_kind: String(triggerDecision.trigger || "").trim(),
3076
+ assignment_validation_status: assignmentValidationStatus,
3077
+ assignment_validation_reason: assignmentValidationReason,
3078
+ assignment_validation_modes: ensureArray(assignmentValidation.assignmentModes),
2878
3079
  },
2879
3080
  };
2880
3081
  }
@@ -2918,6 +3119,8 @@ async function maybeExecuteDynamicRolePlan({
2918
3119
  last_conversation_id: String(conversationContext?.id || "").trim(),
2919
3120
  last_conversation_stage: String(conversationContext?.stage || "").trim(),
2920
3121
  last_workspace_dir: String(stepExecutionPlan.workspaceDir || executionPlan.workspaceDir || "").trim(),
3122
+ last_assignment_validation_status: assignmentValidationStatus,
3123
+ last_assignment_validation_reason: assignmentValidationReason,
2921
3124
  ...safeObject(intentStatePatch),
2922
3125
  }),
2923
3126
  );
@@ -2931,6 +3134,9 @@ async function maybeExecuteDynamicRolePlan({
2931
3134
  thread_id: archiveThread.threadID,
2932
3135
  comment_id: selectedRecord.id,
2933
3136
  trigger_kind: String(triggerDecision.trigger || "").trim(),
3137
+ assignment_validation_status: assignmentValidationStatus,
3138
+ assignment_validation_reason: assignmentValidationReason,
3139
+ assignment_validation_modes: ensureArray(assignmentValidation.assignmentModes),
2934
3140
  },
2935
3141
  };
2936
3142
  }
@@ -3057,6 +3263,8 @@ async function maybeExecuteDynamicRolePlan({
3057
3263
  last_artifact_errors: stepErrors,
3058
3264
  last_boundary_violations: stepBoundaryViolations,
3059
3265
  last_workspace_dir: String(stepExecutionPlan.workspaceDir || executionPlan.workspaceDir || "").trim(),
3266
+ last_assignment_validation_status: assignmentValidationStatus,
3267
+ last_assignment_validation_reason: assignmentValidationReason,
3060
3268
  ...safeObject(intentStatePatch),
3061
3269
  }),
3062
3270
  );
@@ -3073,6 +3281,9 @@ async function maybeExecuteDynamicRolePlan({
3073
3281
  artifact_validation: String(stepValidation.status || "").trim() || "execution_failed",
3074
3282
  artifact_paths: summarizeValidatedArtifactPaths(stepValidation),
3075
3283
  artifact_errors: stepErrors,
3284
+ assignment_validation_status: assignmentValidationStatus,
3285
+ assignment_validation_reason: assignmentValidationReason,
3286
+ assignment_validation_modes: ensureArray(assignmentValidation.assignmentModes),
3076
3287
  },
3077
3288
  };
3078
3289
  }
@@ -3117,21 +3328,13 @@ async function maybeExecuteDynamicRolePlan({
3117
3328
  mergedWorkItems,
3118
3329
  );
3119
3330
  const summaryBotSelector = normalizeMentionSelector(conversationContext?.summaryBotUsername || conversationContext?.leadBotUsername);
3120
- const finalContract = conversationContext?.mode === "public_multi_bot" && summaryBotSelector && summaryBotSelector !== currentBotSelector
3121
- ? {
3122
- type: "summary_request",
3123
- actionable: true,
3124
- assignments: [],
3125
- summaryBot: summaryBotSelector,
3126
- nextResponders: [summaryBotSelector],
3127
- }
3128
- : {
3129
- type: summaryRole === "approval" ? "final_summary" : "direct_result",
3130
- actionable: true,
3131
- assignments: [],
3132
- summaryBot: summaryBotSelector,
3133
- nextResponders: [],
3134
- };
3331
+ const finalContract = {
3332
+ type: summaryRole === "approval" ? "final_summary" : "direct_result",
3333
+ actionable: true,
3334
+ assignments: [],
3335
+ summaryBot: summaryBotSelector,
3336
+ nextResponders: [],
3337
+ };
3135
3338
  return {
3136
3339
  kind: "executed",
3137
3340
  aiResult: {
@@ -3380,47 +3583,21 @@ function normalizeConversationExecutionContract(
3380
3583
  || contract.contractType
3381
3584
  || "",
3382
3585
  ).trim().toLowerCase();
3383
- const normalizedType = [
3384
- "direct_result",
3385
- "delegation",
3386
- "summary_request",
3387
- "final_summary",
3388
- ].includes(type)
3389
- ? type
3390
- : "";
3391
- const assignments = ensureArray(contract.assignments)
3392
- .map((item) => {
3393
- const assignment = safeObject(item);
3394
- const targetBot = normalizeMentionSelector(
3395
- assignment.targetBot
3396
- || assignment.target_bot
3397
- || assignment.bot
3398
- || assignment.username,
3399
- );
3400
- const task = String(
3401
- assignment.task
3402
- || assignment.instruction
3403
- || assignment.assignment
3404
- || assignment.work
3405
- || assignment.description
3406
- || "",
3407
- ).trim();
3408
- if (!targetBot || !task) {
3409
- return null;
3410
- }
3411
- if (allowedResponderSet.size && !allowedResponderSet.has(targetBot)) {
3412
- return null;
3413
- }
3414
- if (
3415
- excludeCurrentTargetAssignments === true
3416
- && currentBotSelector
3417
- && targetBot === normalizeMentionSelector(currentBotSelector)
3418
- ) {
3419
- return null;
3420
- }
3421
- return { targetBot, task };
3422
- })
3423
- .filter(Boolean);
3586
+ const normalizedType = [
3587
+ "direct_result",
3588
+ "delegation",
3589
+ "summary_request",
3590
+ "final_summary",
3591
+ ].includes(type)
3592
+ ? type
3593
+ : "";
3594
+ const assignments = ensureArray(contract.assignments)
3595
+ .map((item) => normalizeExecutionAssignment(item, {
3596
+ allowedResponderSet,
3597
+ currentBotSelector,
3598
+ excludeCurrentTargetAssignments,
3599
+ }))
3600
+ .filter(Boolean);
3424
3601
  const summaryBot = normalizeMentionSelector(
3425
3602
  contract.summaryBot
3426
3603
  || contract.summary_bot
@@ -3483,30 +3660,10 @@ function normalizeResponseExecutionContract(rawContract, responseContract, { cur
3483
3660
  return null;
3484
3661
  }
3485
3662
  const assignments = ensureArray(contract.assignments)
3486
- .map((item) => {
3487
- const assignment = safeObject(item);
3488
- const targetBot = normalizeMentionSelector(
3489
- assignment.targetBot
3490
- || assignment.target_bot
3491
- || assignment.bot
3492
- || assignment.username,
3493
- );
3494
- const task = String(
3495
- assignment.task
3496
- || assignment.instruction
3497
- || assignment.assignment
3498
- || assignment.work
3499
- || assignment.description
3500
- || "",
3501
- ).trim();
3502
- if (!targetBot || !task) {
3503
- return null;
3504
- }
3505
- if (currentBotSelector && targetBot === currentBotSelector) {
3506
- return null;
3507
- }
3508
- return { targetBot, task };
3509
- })
3663
+ .map((item) => normalizeExecutionAssignment(item, {
3664
+ currentBotSelector,
3665
+ excludeCurrentTargetAssignments: true,
3666
+ }))
3510
3667
  .filter(Boolean);
3511
3668
  const summaryBot = normalizeMentionSelector(
3512
3669
  contract.summaryBot
@@ -4826,6 +4983,10 @@ export async function processRunnerSelectedRecord({
4826
4983
  directQueryReply: directInformationalReply,
4827
4984
  responderAdjudication,
4828
4985
  });
4986
+ const assignmentExecutionValidation = summarizeCurrentAssignmentExecutionValidation(
4987
+ conversationContext,
4988
+ currentBotSelector,
4989
+ );
4829
4990
  const emitRunnerStage = (phase, detail) => {
4830
4991
  if (!reportRunnerStage) return;
4831
4992
  try {
@@ -4981,6 +5142,7 @@ export async function processRunnerSelectedRecord({
4981
5142
  saveRunnerRouteState,
4982
5143
  runRunnerAIExecution,
4983
5144
  validateWorkspaceArtifacts,
5145
+ assignmentExecutionValidation,
4984
5146
  });
4985
5147
  if (dynamicRoleExecution?.kind === "error") {
4986
5148
  dynamicExecutionError = dynamicRoleExecution.result;
@@ -5317,13 +5479,19 @@ export async function processRunnerSelectedRecord({
5317
5479
  if (effectiveConversationContext?.mode === "public_multi_bot") {
5318
5480
  const currentBotSelector = normalizeMentionSelector(bot?.username || bot?.name);
5319
5481
  const currentSession = safeObject(effectiveConversationContext.session);
5482
+ const currentExecutionContract = safeObject(effectiveConversationContext.executionContract);
5320
5483
  const normalizedReplyFingerprint = normalizeConversationReplyFingerprint(sanitizedReplyText);
5321
5484
  const lastSpeaker = normalizeMentionSelector(currentSession.last_speaker_bot_username);
5322
5485
  const lastReplyFingerprintByBot = safeObject(currentSession.last_reply_fingerprint_by_bot);
5323
- if (lastSpeaker && lastSpeaker === currentBotSelector) {
5324
- const reason = "conversation guard blocked consecutive reply from the same bot";
5325
- saveRunnerRouteState(
5326
- routeKey,
5486
+ const contractAuthorizedResponders = uniqueOrdered([
5487
+ ...collectExecutionContractNextResponders(currentExecutionContract),
5488
+ normalizeMentionSelector(currentExecutionContract.summaryBot || currentExecutionContract.summary_bot),
5489
+ ].filter(Boolean));
5490
+ const contractAuthorizesCurrentBotReply = contractAuthorizedResponders.includes(currentBotSelector);
5491
+ if (lastSpeaker && lastSpeaker === currentBotSelector && !contractAuthorizesCurrentBotReply) {
5492
+ const reason = "conversation guard blocked consecutive reply from the same bot";
5493
+ saveRunnerRouteState(
5494
+ routeKey,
5327
5495
  buildRunnerRouteStateFromComment(selectedRecord, {
5328
5496
  last_action: "conversation_skipped",
5329
5497
  last_reason: reason,
@@ -5433,6 +5601,8 @@ export async function processRunnerSelectedRecord({
5433
5601
  last_contract_validation_status: String(responseContractValidation.status || "").trim(),
5434
5602
  last_contract_validation_reason: String(responseContractValidation.reason || "").trim(),
5435
5603
  last_contract_validation_targets: ensureArray(responseContractValidation.targets),
5604
+ last_assignment_validation_status: String(assignmentExecutionValidation.status || "").trim(),
5605
+ last_assignment_validation_reason: String(assignmentExecutionValidation.reason || "").trim(),
5436
5606
  last_speaker_bot_username: normalizeMentionSelector(bot?.username || bot?.name),
5437
5607
  last_workspace_dir: String(effectiveExecutionPlan.workspaceDir || "").trim(),
5438
5608
  ...intentStatePatch,
@@ -5474,6 +5644,9 @@ export async function processRunnerSelectedRecord({
5474
5644
  response_contract_validation_status: String(responseContractValidation.status || "").trim(),
5475
5645
  response_contract_validation_reason: String(responseContractValidation.reason || "").trim(),
5476
5646
  response_contract_validation_targets: ensureArray(responseContractValidation.targets),
5647
+ assignment_validation_status: String(assignmentExecutionValidation.status || "").trim(),
5648
+ assignment_validation_reason: String(assignmentExecutionValidation.reason || "").trim(),
5649
+ assignment_validation_modes: ensureArray(assignmentExecutionValidation.assignmentModes),
5477
5650
  reply_chars: String(sanitizedReplyText || "").length,
5478
5651
  execution_mode: effectiveExecutionPlan.mode,
5479
5652
  role_profile: effectiveExecutionPlan.roleProfileName,
@@ -5564,6 +5737,8 @@ export async function processRunnerSelectedRecord({
5564
5737
  last_contract_validation_status: String(responseContractValidation.status || "").trim(),
5565
5738
  last_contract_validation_reason: String(responseContractValidation.reason || "").trim(),
5566
5739
  last_contract_validation_targets: ensureArray(responseContractValidation.targets),
5740
+ last_assignment_validation_status: String(assignmentExecutionValidation.status || "").trim(),
5741
+ last_assignment_validation_reason: String(assignmentExecutionValidation.reason || "").trim(),
5567
5742
  last_trigger: String(effectiveTriggerDecision.trigger || "").trim(),
5568
5743
  last_reason: effectiveConversationContext?.mode === "public_multi_bot"
5569
5744
  ? [
@@ -5664,6 +5839,9 @@ export async function processRunnerSelectedRecord({
5664
5839
  response_contract_validation_status: String(responseContractValidation.status || "").trim(),
5665
5840
  response_contract_validation_reason: String(responseContractValidation.reason || "").trim(),
5666
5841
  response_contract_validation_targets: ensureArray(responseContractValidation.targets),
5842
+ assignment_validation_status: String(assignmentExecutionValidation.status || "").trim(),
5843
+ assignment_validation_reason: String(assignmentExecutionValidation.reason || "").trim(),
5844
+ assignment_validation_modes: ensureArray(assignmentExecutionValidation.assignmentModes),
5667
5845
  delivery_status: deliveryResult.delivery.dryRun ? "dry_run" : "delivered",
5668
5846
  execution_mode: effectiveExecutionPlan.mode,
5669
5847
  role_profile: effectiveExecutionPlan.roleProfileName,
@@ -5483,9 +5483,9 @@ export async function runSelftestRunnerScenarios(push, deps) {
5483
5483
  push("single_bot_execution_failure_uses_ai_failure_explainer_when_available", false, String(err?.message || err));
5484
5484
  }
5485
5485
 
5486
- try {
5487
- let aiCalls = 0;
5488
- const processed = await processRunnerSelectedRecord({
5486
+ try {
5487
+ let aiCalls = 0;
5488
+ const processed = await processRunnerSelectedRecord({
5489
5489
  routeKey: "single-bot-informational-human-request-key",
5490
5490
  normalizedRoute: normalizeRunnerRoute({
5491
5491
  name: "telegram-monitor-single-bot-informational",
@@ -9087,13 +9087,164 @@ export async function runSelftestRunnerScenarios(push, deps) {
9087
9087
  && String(processed.result?.execution_contract_type || "") === "summary_request",
9088
9088
  `kind=${String(processed.kind || "(none)")} ai_calls=${aiCalls} contract=${String(processed.result?.execution_contract_type || "(none)")} reason=${String(processed.skippedRecord?.reason || "(none)")}`,
9089
9089
  );
9090
- } catch (err) {
9091
- push("delegated_single_lead_lead_reply_can_wake_named_peer", false, String(err?.message || err));
9092
- }
9093
-
9094
- try {
9095
- let aiCalls = 0;
9096
- const deliveredConversation = [];
9090
+ } catch (err) {
9091
+ push("delegated_single_lead_lead_reply_can_wake_named_peer", false, String(err?.message || err));
9092
+ }
9093
+
9094
+ try {
9095
+ let aiCalls = 0;
9096
+ let plannerCalls = 0;
9097
+ const processed = await processRunnerSelectedRecord({
9098
+ routeKey: "delegated-single-lead-conversation-assignment-skips-planner-key",
9099
+ normalizedRoute: normalizeRunnerRoute({
9100
+ name: "telegram-monitor-delegated-single-lead-conversation-assignment-skips-planner",
9101
+ project_id: selftestProjectID,
9102
+ provider: "telegram",
9103
+ role: "monitor",
9104
+ role_profile: "monitor",
9105
+ destination_id: "dest-1",
9106
+ destination_label: "Main Room",
9107
+ server_bot_name: "RyoAI2_bot",
9108
+ server_bot_id: "bot-peer-1",
9109
+ trigger_policy: {
9110
+ mentions_only: true,
9111
+ direct_messages: true,
9112
+ reply_to_bot_messages: true,
9113
+ },
9114
+ archive_policy: {
9115
+ mirror_replies: true,
9116
+ dedupe_inbound: true,
9117
+ dedupe_outbound: true,
9118
+ skip_bot_messages: true,
9119
+ },
9120
+ dry_run_delivery: true,
9121
+ }),
9122
+ selectedRecord: {
9123
+ id: "comment-delegated-single-lead-conversation-assignment-skips-planner",
9124
+ createdAt: "2026-03-16T00:02:12.000Z",
9125
+ parsedArchive: {
9126
+ kind: "bot_reply",
9127
+ conversationID: "comment-delegated-single-lead-conversation-open",
9128
+ conversationMode: "public_multi_bot",
9129
+ conversationStage: "bot_reply",
9130
+ conversationIntentMode: "delegated_single_lead",
9131
+ conversationAllowBotToBot: true,
9132
+ conversationLeadBotUsername: "ryoai_bot",
9133
+ conversationParticipants: ["ryoai_bot", "ryoai2_bot", "ryoai3_bot"],
9134
+ conversationInitialResponders: ["ryoai_bot"],
9135
+ conversationAllowedResponders: ["ryoai_bot", "ryoai2_bot", "ryoai3_bot"],
9136
+ conversationSummaryBotUsername: "ryoai_bot",
9137
+ botUsername: "RyoAI_bot",
9138
+ botName: "RyoAI_bot",
9139
+ sender: "RyoAI_bot",
9140
+ body: "@RyoAI2_bot 관점 하나만 짧게 말해줘.",
9141
+ mentionUsernames: ["ryoai2_bot"],
9142
+ executionContract: {
9143
+ type: "delegation",
9144
+ actionable: true,
9145
+ assignments: [
9146
+ { target_bot: "ryoai2_bot", task: "관점 하나만 짧게 말해줘.", mode: "conversation_contribution", artifacts_required: false },
9147
+ ],
9148
+ summary_bot: "ryoai_bot",
9149
+ next_responders: ["ryoai2_bot"],
9150
+ },
9151
+ },
9152
+ },
9153
+ pendingOrdered: [],
9154
+ bot: {
9155
+ id: "bot-peer-1",
9156
+ name: "RyoAI2_bot",
9157
+ username: "RyoAI2_bot",
9158
+ role: "monitor",
9159
+ provider: "telegram",
9160
+ },
9161
+ destination: {
9162
+ id: "dest-1",
9163
+ label: "Main Room",
9164
+ provider: "telegram",
9165
+ chatID: "-100123",
9166
+ },
9167
+ archiveThread: {
9168
+ threadID: "thread-1",
9169
+ workItemID: "work-item-1",
9170
+ },
9171
+ executionPlan: {
9172
+ mode: "role_profile",
9173
+ roleProfileName: "monitor",
9174
+ roleProfile: {
9175
+ client: "sample",
9176
+ model: "",
9177
+ permissionMode: "read_only",
9178
+ reasoningEffort: "low",
9179
+ },
9180
+ workspaceDir: path.join(os.tmpdir(), "metheus-runner-selftest-conversation-assignment-skips-planner"),
9181
+ workspaceSource: "selftest",
9182
+ usedCommandFallback: false,
9183
+ },
9184
+ runtime: {
9185
+ baseURL: "https://example.test",
9186
+ token: "selftest-token",
9187
+ timeoutSeconds: 30,
9188
+ actor: { user_id: "user-1" },
9189
+ },
9190
+ deps: {
9191
+ saveRunnerRouteState: () => {},
9192
+ startRunnerTypingHeartbeat: () => ({ async stop() {} }),
9193
+ runRunnerAIExecution: async () => {
9194
+ aiCalls += 1;
9195
+ return {
9196
+ skip: false,
9197
+ reply: "제 관점에서는 범위와 우선순위를 먼저 고정하는 게 맞습니다.",
9198
+ contract: {
9199
+ type: "summary_request",
9200
+ actionable: true,
9201
+ assignments: [],
9202
+ summary_bot: "ryoai_bot",
9203
+ next_responders: ["ryoai_bot"],
9204
+ },
9205
+ };
9206
+ },
9207
+ performLocalBotDelivery: async () => ({
9208
+ delivery: { dryRun: true, body: {} },
9209
+ archive: {},
9210
+ }),
9211
+ serializeRunnerTriggerPolicy: (value) => value,
9212
+ serializeRunnerArchivePolicy: (value) => value,
9213
+ buildRunnerExecutionDeps: () => ({
9214
+ planRoleExecutionWithAI: async () => {
9215
+ plannerCalls += 1;
9216
+ return {
9217
+ requiresExecution: true,
9218
+ summaryRole: "worker",
9219
+ steps: [{ role: "worker", goal: "unexpected", artifactsRequired: true }],
9220
+ };
9221
+ },
9222
+ }),
9223
+ buildRunnerDeliveryDeps: () => ({}),
9224
+ buildRunnerRuntimeDeps: () => ({}),
9225
+ resolveConversationPeerBots: () => [
9226
+ { id: "bot-lead-1", name: "RyoAI_bot" },
9227
+ { id: "bot-peer-1", name: "RyoAI2_bot" },
9228
+ { id: "bot-peer-2", name: "RyoAI3_bot" },
9229
+ ],
9230
+ },
9231
+ });
9232
+ push(
9233
+ "delegated_single_lead_conversation_assignment_skips_planner",
9234
+ processed.kind === "replied"
9235
+ && aiCalls === 1
9236
+ && plannerCalls === 0
9237
+ && String(processed.result?.assignment_validation_status || "") === "conversation_assignment"
9238
+ && ensureArray(processed.result?.assignment_validation_modes).includes("conversation_contribution"),
9239
+ `kind=${String(processed.kind || "(none)")} ai_calls=${aiCalls} planner_calls=${plannerCalls} assignment_status=${String(processed.result?.assignment_validation_status || "(none)")} modes=${JSON.stringify(processed.result?.assignment_validation_modes || [])}`,
9240
+ );
9241
+ } catch (err) {
9242
+ push("delegated_single_lead_conversation_assignment_skips_planner", false, String(err?.message || err));
9243
+ }
9244
+
9245
+ try {
9246
+ let aiCalls = 0;
9247
+ const deliveredConversation = [];
9097
9248
  const processed = await processRunnerSelectedRecord({
9098
9249
  routeKey: "delegated-single-lead-restored-bot-reply-key",
9099
9250
  normalizedRoute: normalizeRunnerRoute({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.225",
3
+ "version": "0.2.227",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [