metheus-governance-mcp-cli 0.2.280 → 0.2.281

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
@@ -3919,7 +3919,6 @@ function runnerRequestPreferredExecutionContractType(entryRaw) {
3919
3919
  return String(
3920
3920
  decisionBundle.execution_contract_type
3921
3921
  || entry.execution_contract_type
3922
- || entry.followup_execution_contract_type
3923
3922
  || entry.root_execution_contract_type
3924
3923
  || "",
3925
3924
  ).trim().toLowerCase();
@@ -3939,14 +3938,12 @@ function runnerRequestPreferredExecutionContractTargets(entryRaw) {
3939
3938
  ? decisionBundle.execution_contract_targets
3940
3939
  : ensureArray(entry.execution_contract_targets).length
3941
3940
  ? entry.execution_contract_targets
3942
- : ensureArray(entry.followup_execution_contract_targets).length
3943
- ? entry.followup_execution_contract_targets
3944
- : ensureArray(entry.root_execution_contract_targets).length
3945
- ? entry.root_execution_contract_targets
3946
- : [],
3947
- normalizeTelegramMentionUsername,
3948
- );
3949
- }
3941
+ : ensureArray(entry.root_execution_contract_targets).length
3942
+ ? entry.root_execution_contract_targets
3943
+ : [],
3944
+ normalizeTelegramMentionUsername,
3945
+ );
3946
+ }
3950
3947
 
3951
3948
  function runnerRequestPreferredNextExpectedResponders(entryRaw) {
3952
3949
  const entry = safeObject(entryRaw);
@@ -3956,14 +3953,12 @@ function runnerRequestPreferredNextExpectedResponders(entryRaw) {
3956
3953
  ? decisionBundle.next_expected_responders
3957
3954
  : ensureArray(entry.next_expected_responders).length
3958
3955
  ? entry.next_expected_responders
3959
- : ensureArray(entry.followup_next_expected_responders).length
3960
- ? entry.followup_next_expected_responders
3961
- : ensureArray(entry.root_next_expected_responders).length
3962
- ? entry.root_next_expected_responders
3963
- : [],
3964
- normalizeTelegramMentionUsername,
3965
- );
3966
- }
3956
+ : ensureArray(entry.root_next_expected_responders).length
3957
+ ? entry.root_next_expected_responders
3958
+ : [],
3959
+ normalizeTelegramMentionUsername,
3960
+ );
3961
+ }
3967
3962
 
3968
3963
  function runnerRequestPreferredAuthoritySelectedBotUsernames(entryRaw) {
3969
3964
  const entry = safeObject(entryRaw);
@@ -5182,6 +5177,7 @@ async function claimRunnerRequestForHumanComment({
5182
5177
  ensureArray(authoritativeDecisionBundle.allowed_responders),
5183
5178
  normalizeTelegramMentionUsername,
5184
5179
  );
5180
+ const hasAuthoritativeConversationDecision = Object.keys(authoritativeDecisionBundle).length > 0;
5185
5181
  const { requests: nextRequests, request } = upsertRunnerRequest(stateForClaim, requestKey, {
5186
5182
  project_id: String(normalizedRoute?.projectID || "").trim(),
5187
5183
  provider: String(normalizedRoute?.provider || "").trim(),
@@ -5202,26 +5198,32 @@ async function claimRunnerRequestForHumanComment({
5202
5198
  decision_bundle_validation_status: String(decisionBundleValidation.status || "").trim(),
5203
5199
  decision_bundle_validation_reason: String(decisionBundleValidation.reason || "").trim(),
5204
5200
  conversation_intent_mode: String(
5205
- authoritativeDecisionBundle.conversation_intent_mode
5201
+ (hasAuthoritativeConversationDecision
5202
+ ? authoritativeDecisionBundle.conversation_intent_mode
5203
+ : "")
5206
5204
  || normalizedSharedHumanIntent.intentMode
5207
5205
  || existing.conversation_intent_mode
5208
5206
  || authoritySource.conversation_intent_mode
5209
5207
  || "",
5210
5208
  ).trim().toLowerCase(),
5211
5209
  conversation_lead_bot: normalizeTelegramMentionUsername(
5212
- authoritativeDecisionBundle.conversation_lead_bot
5210
+ (hasAuthoritativeConversationDecision
5211
+ ? authoritativeDecisionBundle.conversation_lead_bot
5212
+ : "")
5213
5213
  || normalizedSharedHumanIntent.leadBotSelector
5214
5214
  || existing.conversation_lead_bot
5215
5215
  || authoritySource.conversation_lead_bot,
5216
5216
  ),
5217
5217
  conversation_summary_bot: normalizeTelegramMentionUsername(
5218
- authoritativeDecisionBundle.conversation_summary_bot
5218
+ (hasAuthoritativeConversationDecision
5219
+ ? authoritativeDecisionBundle.conversation_summary_bot
5220
+ : "")
5219
5221
  || normalizedSharedHumanIntent.summaryBotSelector
5220
5222
  || existing.conversation_summary_bot
5221
5223
  || authoritySource.conversation_summary_bot,
5222
5224
  ),
5223
5225
  conversation_participants: uniqueOrderedStrings(
5224
- decisionConversationParticipants.length
5226
+ hasAuthoritativeConversationDecision && decisionConversationParticipants.length
5225
5227
  ? decisionConversationParticipants
5226
5228
  : ensureArray(normalizedSharedHumanIntent.participantSelectors).length
5227
5229
  ? normalizedSharedHumanIntent.participantSelectors
@@ -5233,7 +5235,7 @@ async function claimRunnerRequestForHumanComment({
5233
5235
  normalizeTelegramMentionUsername,
5234
5236
  ),
5235
5237
  conversation_initial_responders: uniqueOrderedStrings(
5236
- decisionInitialResponders.length
5238
+ hasAuthoritativeConversationDecision && decisionInitialResponders.length
5237
5239
  ? decisionInitialResponders
5238
5240
  : ensureArray(normalizedSharedHumanIntent.initialResponderSelectors).length
5239
5241
  ? normalizedSharedHumanIntent.initialResponderSelectors
@@ -5245,7 +5247,7 @@ async function claimRunnerRequestForHumanComment({
5245
5247
  normalizeTelegramMentionUsername,
5246
5248
  ),
5247
5249
  conversation_allowed_responders: uniqueOrderedStrings(
5248
- decisionAllowedResponders.length
5250
+ hasAuthoritativeConversationDecision && decisionAllowedResponders.length
5249
5251
  ? decisionAllowedResponders
5250
5252
  : ensureArray(normalizedSharedHumanIntent.allowedResponderSelectors).length
5251
5253
  ? normalizedSharedHumanIntent.allowedResponderSelectors
@@ -5254,30 +5256,38 @@ async function claimRunnerRequestForHumanComment({
5254
5256
  : ensureArray(authoritySource.conversation_allowed_responders).length
5255
5257
  ? authoritySource.conversation_allowed_responders
5256
5258
  : [],
5257
- normalizeTelegramMentionUsername,
5259
+ normalizeTelegramMentionUsername,
5258
5260
  ),
5259
- conversation_allow_bot_to_bot: authoritativeDecisionBundle.allow_bot_to_bot === true
5261
+ conversation_allow_bot_to_bot: (hasAuthoritativeConversationDecision
5262
+ ? authoritativeDecisionBundle.allow_bot_to_bot === true
5263
+ : false)
5260
5264
  || normalizedSharedHumanIntent.allowBotToBot === true
5261
5265
  || existing.conversation_allow_bot_to_bot === true
5262
5266
  || authoritySource.conversation_allow_bot_to_bot === true,
5263
5267
  conversation_reply_expectation: String(
5264
- authoritativeDecisionBundle.conversation_reply_expectation
5268
+ (hasAuthoritativeConversationDecision
5269
+ ? authoritativeDecisionBundle.conversation_reply_expectation
5270
+ : "")
5265
5271
  || normalizedSharedHumanIntent.replyExpectation
5266
5272
  || existing.conversation_reply_expectation
5267
5273
  || authoritySource.conversation_reply_expectation
5268
5274
  || "",
5269
5275
  ).trim().toLowerCase(),
5270
5276
  execution_contract_type: String(
5271
- authoritativeDecisionBundle.execution_contract_type
5277
+ (hasAuthoritativeConversationDecision
5278
+ ? authoritativeDecisionBundle.execution_contract_type
5279
+ : "")
5272
5280
  || runnerRequestPreferredExecutionContractType(existing)
5273
5281
  || runnerRequestPreferredExecutionContractType(authoritySource)
5274
5282
  || "",
5275
5283
  ).trim().toLowerCase(),
5276
- execution_contract_actionable: authoritativeDecisionBundle.execution_contract_actionable === true
5284
+ execution_contract_actionable: (hasAuthoritativeConversationDecision
5285
+ ? authoritativeDecisionBundle.execution_contract_actionable === true
5286
+ : false)
5277
5287
  || runnerRequestPreferredExecutionContractActionable(existing)
5278
5288
  || runnerRequestPreferredExecutionContractActionable(authoritySource),
5279
5289
  execution_contract_targets: uniqueOrderedStrings(
5280
- ensureArray(authoritativeDecisionBundle.execution_contract_targets).length
5290
+ hasAuthoritativeConversationDecision && ensureArray(authoritativeDecisionBundle.execution_contract_targets).length
5281
5291
  ? authoritativeDecisionBundle.execution_contract_targets
5282
5292
  : runnerRequestPreferredExecutionContractTargets(existing).length
5283
5293
  ? runnerRequestPreferredExecutionContractTargets(existing)
@@ -5285,7 +5295,7 @@ async function claimRunnerRequestForHumanComment({
5285
5295
  normalizeTelegramMentionUsername,
5286
5296
  ),
5287
5297
  next_expected_responders: uniqueOrderedStrings(
5288
- ensureArray(authoritativeDecisionBundle.next_expected_responders).length
5298
+ hasAuthoritativeConversationDecision && ensureArray(authoritativeDecisionBundle.next_expected_responders).length
5289
5299
  ? authoritativeDecisionBundle.next_expected_responders
5290
5300
  : runnerRequestPreferredNextExpectedResponders(existing).length
5291
5301
  ? runnerRequestPreferredNextExpectedResponders(existing)
@@ -6428,23 +6438,33 @@ function markRunnerRequestLifecycle({
6428
6438
  ],
6429
6439
  normalizeTelegramMentionUsername,
6430
6440
  ).filter((selector) => selector && selector !== normalizedCurrentBotSelector);
6441
+ const nextExecutionContractType = String(
6442
+ authoritativeDecisionBundle.execution_contract_type
6443
+ || executionContractType
6444
+ || existing.execution_contract_type
6445
+ || "",
6446
+ ).trim().toLowerCase();
6447
+ const normalizedOutcome = String(outcome || "").trim().toLowerCase();
6431
6448
  const shouldRemainRunningAfterReply = authoritativeDecisionBundle.should_close_after_reply === true
6432
6449
  ? false
6433
6450
  : authoritativeDecisionBundle.should_close_after_reply === false
6434
6451
  ? true
6435
6452
  : continuationSelectors.length > 0;
6453
+ const shouldRemainRunningAfterSkip = normalizedOutcome === "skipped"
6454
+ && parsedKind === "bot_reply"
6455
+ && authoritativeDecisionBundle.should_close_after_reply !== true
6456
+ && (
6457
+ nextExecutionContractType === "delegation"
6458
+ || rootEffectiveExecutionContractTargets.length > 0
6459
+ || rootEffectiveNextExpectedResponders.length > 0
6460
+ || continuationSelectors.length > 0
6461
+ );
6436
6462
  const nextConversationIntentMode = String(
6437
6463
  authoritativeDecisionBundle.conversation_intent_mode
6438
6464
  || conversationIntentMode
6439
6465
  || existing.conversation_intent_mode
6440
6466
  || "",
6441
6467
  ).trim().toLowerCase();
6442
- const nextExecutionContractType = String(
6443
- authoritativeDecisionBundle.execution_contract_type
6444
- || executionContractType
6445
- || existing.execution_contract_type
6446
- || "",
6447
- ).trim().toLowerCase();
6448
6468
  const nextNormalizedIntent = (() => {
6449
6469
  const explicitIntent = String(
6450
6470
  authoritativeDecisionBundle.normalized_intent || normalizedIntent || "",
@@ -6454,10 +6474,9 @@ function markRunnerRequestLifecycle({
6454
6474
  }
6455
6475
  if (nextConversationIntentMode && !nextExecutionContractType) {
6456
6476
  return "";
6457
- }
6458
- return String(existing.normalized_intent || "").trim().toLowerCase();
6459
- })();
6460
- const normalizedOutcome = String(outcome || "").trim().toLowerCase();
6477
+ }
6478
+ return String(existing.normalized_intent || "").trim().toLowerCase();
6479
+ })();
6461
6480
  const nextStatus = (() => {
6462
6481
  if (normalizedOutcome === "claimed") return "claimed";
6463
6482
  if (normalizedOutcome === "running") return "running";
@@ -6467,9 +6486,11 @@ function markRunnerRequestLifecycle({
6467
6486
  }
6468
6487
  if (normalizedOutcome === "loop_closed") return "loop_closed";
6469
6488
  if (normalizedOutcome === "expired") return "expired";
6489
+ if (normalizedOutcome === "skipped") {
6490
+ return shouldRemainRunningAfterSkip ? "running" : "closed";
6491
+ }
6470
6492
  if (
6471
6493
  normalizedOutcome === "error"
6472
- || normalizedOutcome === "skipped"
6473
6494
  || normalizedOutcome === "closed"
6474
6495
  || normalizedOutcome === "execution_failed"
6475
6496
  || normalizedOutcome === "policy_violation"
@@ -931,62 +931,57 @@ function normalizeCliOutput(rawText) {
931
931
  throw new Error("AI client returned empty output");
932
932
  }
933
933
  const parsed = tryJsonParse(text) || tryParseEmbeddedJsonObject(text);
934
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
935
- if (parsed.skip === true) {
936
- return {
937
- skip: true,
938
- reason: firstNonEmptyString([parsed.reason, parsed.message, parsed.detail]),
939
- raw: parsed,
940
- };
941
- }
942
- const clarifyText = firstNonEmptyString([
943
- typeof parsed.clarify === "string" ? parsed.clarify : "",
944
- typeof parsed.clarification === "string" ? parsed.clarification : "",
945
- parsed.clarify === true ? parsed.reply : "",
946
- parsed.clarify === true ? parsed.message : "",
947
- ]);
948
- if (clarifyText) {
949
- return {
950
- skip: false,
951
- clarify: true,
952
- reply: clarifyText,
953
- replyToMessageID: intFromRawAllowZero(parsed.reply_to_message_id ?? parsed.replyToMessageID, 0),
954
- contract: null,
955
- artifacts: [],
956
- ctxpackFiles: [],
957
- workItems: [],
958
- raw: parsed,
959
- };
960
- }
961
- const reply = firstNonEmptyString([parsed.reply, parsed.text, parsed.content, parsed.message]);
962
- if (!reply) {
963
- throw new Error("AI client returned JSON without reply text");
964
- }
965
- const contract = normalizeExecutionContract(
966
- parsed.contract
967
- ?? parsed.execution_contract
968
- ?? parsed.executionContract,
969
- );
934
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
935
+ throw new Error("AI client did not return a JSON object");
936
+ }
937
+ const decisionBundle = parsed.decision_bundle ?? parsed.decisionBundle ?? null;
938
+ if (parsed.skip === true) {
939
+ return {
940
+ skip: true,
941
+ reason: firstNonEmptyString([parsed.reason, parsed.message, parsed.detail]),
942
+ decisionBundle,
943
+ raw: parsed,
944
+ };
945
+ }
946
+ const clarifyText = firstNonEmptyString([
947
+ typeof parsed.clarify === "string" ? parsed.clarify : "",
948
+ typeof parsed.clarification === "string" ? parsed.clarification : "",
949
+ parsed.clarify === true ? parsed.reply : "",
950
+ parsed.clarify === true ? parsed.message : "",
951
+ ]);
952
+ if (clarifyText) {
970
953
  return {
971
954
  skip: false,
972
- reply,
955
+ clarify: true,
956
+ reply: clarifyText,
973
957
  replyToMessageID: intFromRawAllowZero(parsed.reply_to_message_id ?? parsed.replyToMessageID, 0),
974
- contract,
975
- artifacts: normalizeExecutionArtifacts(parsed.artifacts ?? parsed.files ?? parsed.outputs),
976
- ctxpackFiles: normalizeExecutionCtxpackFiles(parsed.ctxpack_files ?? parsed.ctxpackFiles),
977
- workItems: normalizeExecutionWorkItems(parsed.work_items ?? parsed.workItems),
958
+ contract: null,
959
+ decisionBundle,
960
+ artifacts: [],
961
+ ctxpackFiles: [],
962
+ workItems: [],
978
963
  raw: parsed,
979
964
  };
980
965
  }
966
+ const reply = firstNonEmptyString([parsed.reply, parsed.text, parsed.content, parsed.message]);
967
+ if (!reply) {
968
+ throw new Error("AI client returned JSON without reply text");
969
+ }
970
+ const contract = normalizeExecutionContract(
971
+ parsed.contract
972
+ ?? parsed.execution_contract
973
+ ?? parsed.executionContract,
974
+ );
981
975
  return {
982
976
  skip: false,
983
- reply: text,
984
- replyToMessageID: 0,
985
- contract: null,
986
- artifacts: [],
987
- ctxpackFiles: [],
988
- workItems: [],
989
- raw: text,
977
+ reply,
978
+ replyToMessageID: intFromRawAllowZero(parsed.reply_to_message_id ?? parsed.replyToMessageID, 0),
979
+ contract,
980
+ decisionBundle,
981
+ artifacts: normalizeExecutionArtifacts(parsed.artifacts ?? parsed.files ?? parsed.outputs),
982
+ ctxpackFiles: normalizeExecutionCtxpackFiles(parsed.ctxpack_files ?? parsed.ctxpackFiles),
983
+ workItems: normalizeExecutionWorkItems(parsed.work_items ?? parsed.workItems),
984
+ raw: parsed,
990
985
  };
991
986
  }
992
987
 
@@ -1629,6 +1624,8 @@ function runGeminiAdapter({ promptText, workspaceDir, model, permissionMode, rea
1629
1624
 
1630
1625
  function runSampleAdapter(payload) {
1631
1626
  const trigger = payload && typeof payload === "object" ? payload.trigger || {} : {};
1627
+ const responseContract = payload && typeof payload === "object" ? safeObject(payload.response_contract) : {};
1628
+ const selfBotUsername = String(responseContract.self_bot_username || "").trim().replace(/^@+/, "").toLowerCase();
1632
1629
  const body = String(trigger.body || "").trim();
1633
1630
  if (!body) {
1634
1631
  return {
@@ -1641,6 +1638,26 @@ function runSampleAdapter(payload) {
1641
1638
  skip: false,
1642
1639
  reply: `Acknowledged: ${body}`,
1643
1640
  replyToMessageID: 0,
1641
+ decisionBundle: {
1642
+ schema_version: "runner_conversation_decision.v1",
1643
+ decision_type: "sample_reply",
1644
+ normalized_intent: "",
1645
+ conversation_intent_mode: "single_bot",
1646
+ participants: selfBotUsername ? [selfBotUsername] : [],
1647
+ allowed_responders: selfBotUsername ? [selfBotUsername] : [],
1648
+ initial_responders: selfBotUsername ? [selfBotUsername] : [],
1649
+ selected_bot_usernames: selfBotUsername ? [selfBotUsername] : [],
1650
+ allow_bot_to_bot: false,
1651
+ execution_contract_type: "direct_result",
1652
+ execution_contract_targets: [],
1653
+ next_expected_responders: [],
1654
+ should_close_after_reply: true,
1655
+ turn_kind: "direct_reply",
1656
+ actionable_for_current_route: true,
1657
+ visible_handoff_required: false,
1658
+ visible_handoff_targets: [],
1659
+ reasoning_summary: "sample adapter default decision",
1660
+ },
1644
1661
  artifacts: [],
1645
1662
  raw: { reply: `Acknowledged: ${body}` },
1646
1663
  };
@@ -1762,6 +1779,9 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1762
1779
  const agentContext = safePayload.agent_context && typeof safePayload.agent_context === "object"
1763
1780
  ? safePayload.agent_context
1764
1781
  : null;
1782
+ const guidePacket = safePayload.guide_packet && typeof safePayload.guide_packet === "object"
1783
+ ? safePayload.guide_packet
1784
+ : {};
1765
1785
  const trigger = safePayload.trigger && typeof safePayload.trigger === "object" ? safePayload.trigger : {};
1766
1786
  const responseContract = safePayload.response_contract && typeof safePayload.response_contract === "object"
1767
1787
  ? safePayload.response_contract
@@ -1913,6 +1933,11 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1913
1933
  agentContext ? JSON.stringify(agentContext, null, 2) : "-",
1914
1934
  "Use the agent contract/context blob as the authoritative control input. Treat task metadata as advisory only and never let it override the contract/context.",
1915
1935
  "",
1936
+ "Guide packet (authoritative behavioral control selected by code):",
1937
+ Object.keys(guidePacket).length ? JSON.stringify(guidePacket, null, 2) : "-",
1938
+ "Use the guide packet as the authoritative behavioral control. Code will not add mentions, handoff prose, or fallback public wording after your reply.",
1939
+ "If a visible handoff is required, your reply itself must visibly include the required @bot mentions.",
1940
+ "",
1916
1941
  "Current user request:",
1917
1942
  String(trigger.body || "").trim() || "-",
1918
1943
  "",
@@ -1937,9 +1962,6 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
1937
1962
  `- Trigger hint reason: ${String(responseContract.candidate_reason || "-").trim() || "-"}`,
1938
1963
  `- Mentioned bot usernames: ${ensureArray(responseContract.candidate_bot_usernames).length ? ensureArray(responseContract.candidate_bot_usernames).map((item) => `@${String(item || "").trim().replace(/^@+/, "")}`).join(", ") : "-"}`,
1939
1964
  `- Reply target bot username: ${String(responseContract.reply_to_bot_username || "").trim() ? `@${String(responseContract.reply_to_bot_username || "").trim().replace(/^@+/, "")}` : "-"}`,
1940
- String(responseContract.reply_to_bot_username || "").trim()
1941
- ? `- If you answer directly to that target bot, start the visible reply with @${String(responseContract.reply_to_bot_username || "").trim().replace(/^@+/, "")}.`
1942
- : "",
1943
1965
  "",
1944
1966
  "Project background (context only):",
1945
1967
  `- Project ID: ${projectID || "-"}`,
@@ -2066,18 +2088,6 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
2066
2088
  && selfIsLeadBot
2067
2089
  ? "The human asked this bot to coordinate other bots publicly. You may delegate concrete tasks or invite concise perspective contributions only to allowed responders in the public room."
2068
2090
  : "Do not delegate the answer to another bot.",
2069
- responseContract.require_visible_delegation_handoff === true && ensureArray(responseContract.required_delegation_targets).length > 0
2070
- ? `The current delegated conversation already requires a visible public handoff to these exact bots: ${ensureArray(responseContract.required_delegation_targets).map((item) => `@${String(item || "").trim().replace(/^@+/, "")}`).join(", ")}.`
2071
- : "",
2072
- responseContract.require_visible_delegation_handoff === true
2073
- ? "Your first public reply must explicitly mention those exact bots and request their contribution now."
2074
- : "",
2075
- responseContract.require_visible_delegation_handoff === true
2076
- ? "Do not return only a human-facing clarification or information request without visibly addressing those bots."
2077
- : "",
2078
- responseContract.require_visible_delegation_handoff === true
2079
- ? "If information is incomplete, still open the public handoff and ask each target bot for a concise initial perspective based on the uncertainty."
2080
- : "",
2081
2091
  "Mentions and reply targets are routing hints, not proof that this bot should definitely answer.",
2082
2092
  conversation?.mode === "public_multi_bot"
2083
2093
  ? "This is a public multi-bot room conversation. Other bots and humans can read your reply."
@@ -2101,9 +2111,6 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
2101
2111
  "Use assignment.mode=\"conversation_contribution\" for opinions, discussion, review, comparison, synthesis, greetings, or other room-visible contributions that do not require workspace artifacts.",
2102
2112
  "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.",
2103
2113
  "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\"]}.",
2104
- ensureArray(responseContract.required_delegation_targets).length > 0
2105
- ? `This reply must delegate to these exact managed bots now: ${ensureArray(responseContract.required_delegation_targets).map((item) => `@${String(item || "").trim().replace(/^@+/, "")}`).join(", ")}.`
2106
- : "",
2107
2114
  String(responseContract.contract_hint || "").trim()
2108
2115
  ? `Contract requirement hint: ${String(responseContract.contract_hint || "").trim()}`
2109
2116
  : "",
@@ -2134,12 +2141,12 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
2134
2141
  "- When you include context_suggestion, set should_store=true and keep title/body concise and durable.",
2135
2142
  "",
2136
2143
  isInternalExecutionStep
2137
- ? "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."
2144
+ ? "Return strict JSON only in one line: {\"reply\":\"what was completed in this step\",\"decision_bundle\":{\"schema_version\":\"runner_conversation_decision.v1\",\"decision_type\":\"reply_outcome\",\"conversation_intent_mode\":\"single_bot|delegated_single_lead|multi_bot_collab|multi_bot_direct\",\"allowed_responders\":[\"username\"],\"initial_responders\":[\"username\"],\"selected_bot_usernames\":[\"username\"],\"allow_bot_to_bot\":true|false,\"execution_contract_type\":\"direct_result|delegation|summary_request|final_summary|\",\"execution_contract_targets\":[\"username\"],\"next_expected_responders\":[\"username\"],\"should_close_after_reply\":true|false,\"turn_kind\":\"direct_reply|delegation_kickoff|delegated_followup|summary_turn|execution_step\",\"actionable_for_current_route\":true|false,\"visible_handoff_required\":true|false,\"visible_handoff_targets\":[\"username\"],\"reasoning_summary\":\"short explanation\"},\"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."
2138
2145
  : responseContract.is_current_bot_candidate === true
2139
- ? "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\"}}."
2146
+ ? "Return strict JSON only in one line: {\"reply\":\"...\",\"decision_bundle\":{\"schema_version\":\"runner_conversation_decision.v1\",\"decision_type\":\"reply_outcome\",\"conversation_intent_mode\":\"single_bot|delegated_single_lead|multi_bot_collab|multi_bot_direct\",\"allowed_responders\":[\"username\"],\"initial_responders\":[\"username\"],\"selected_bot_usernames\":[\"username\"],\"allow_bot_to_bot\":true|false,\"execution_contract_type\":\"direct_result|delegation|summary_request|final_summary|\",\"execution_contract_targets\":[\"username\"],\"next_expected_responders\":[\"username\"],\"should_close_after_reply\":true|false,\"turn_kind\":\"direct_reply|delegation_kickoff|delegated_followup|summary_turn|clarify_turn\",\"actionable_for_current_route\":true|false,\"visible_handoff_required\":true|false,\"visible_handoff_targets\":[\"username\"],\"reasoning_summary\":\"short explanation\"},\"artifacts\":[],\"ctxpack_files\":[],\"work_items\":[]} or {\"clarify\":\"...\",\"decision_bundle\":{\"schema_version\":\"runner_conversation_decision.v1\",\"decision_type\":\"clarify\",\"conversation_intent_mode\":\"single_bot|delegated_single_lead|multi_bot_collab|multi_bot_direct\",\"allowed_responders\":[\"username\"],\"initial_responders\":[\"username\"],\"selected_bot_usernames\":[\"username\"],\"allow_bot_to_bot\":true|false,\"execution_contract_type\":\"|delegation|summary_request|direct_result|final_summary\",\"execution_contract_targets\":[\"username\"],\"next_expected_responders\":[\"username\"],\"should_close_after_reply\":false,\"turn_kind\":\"clarify_turn\",\"actionable_for_current_route\":true|false,\"visible_handoff_required\":true|false,\"visible_handoff_targets\":[\"username\"],\"reasoning_summary\":\"short explanation\"}} or {\"skip\":true,\"reason\":\"...\",\"decision_bundle\":{\"schema_version\":\"runner_conversation_decision.v1\",\"decision_type\":\"skip\",\"conversation_intent_mode\":\"single_bot|delegated_single_lead|multi_bot_collab|multi_bot_direct\",\"allowed_responders\":[\"username\"],\"initial_responders\":[\"username\"],\"selected_bot_usernames\":[\"username\"],\"allow_bot_to_bot\":true|false,\"execution_contract_type\":\"|delegation|summary_request|direct_result|final_summary\",\"execution_contract_targets\":[\"username\"],\"next_expected_responders\":[\"username\"],\"should_close_after_reply\":false,\"turn_kind\":\"skip\",\"actionable_for_current_route\":false,\"visible_handoff_required\":true|false,\"visible_handoff_targets\":[\"username\"],\"reasoning_summary\":\"short explanation\"}} or {\"reply\":\"...\",\"decision_bundle\":{\"schema_version\":\"runner_conversation_decision.v1\",\"decision_type\":\"reply_outcome\",\"conversation_intent_mode\":\"single_bot|delegated_single_lead|multi_bot_collab|multi_bot_direct\",\"allowed_responders\":[\"username\"],\"initial_responders\":[\"username\"],\"selected_bot_usernames\":[\"username\"],\"allow_bot_to_bot\":true|false,\"execution_contract_type\":\"direct_result|delegation|summary_request|final_summary|\",\"execution_contract_targets\":[\"username\"],\"next_expected_responders\":[\"username\"],\"should_close_after_reply\":true|false,\"turn_kind\":\"direct_reply|delegation_kickoff|delegated_followup|summary_turn\",\"actionable_for_current_route\":true|false,\"visible_handoff_required\":true|false,\"visible_handoff_targets\":[\"username\"],\"reasoning_summary\":\"short explanation\"},\"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\"}}."
2140
2147
  : terse
2141
- ? "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\":\"...\"}."
2142
- : "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.",
2148
+ ? "Return strict JSON only in one line: {\"reply\":\"...\",\"decision_bundle\":{\"schema_version\":\"runner_conversation_decision.v1\",\"decision_type\":\"reply_outcome\",\"conversation_intent_mode\":\"single_bot|delegated_single_lead|multi_bot_collab|multi_bot_direct\",\"allowed_responders\":[\"username\"],\"initial_responders\":[\"username\"],\"selected_bot_usernames\":[\"username\"],\"allow_bot_to_bot\":true|false,\"execution_contract_type\":\"|delegation|summary_request|direct_result|final_summary\",\"execution_contract_targets\":[\"username\"],\"next_expected_responders\":[\"username\"],\"should_close_after_reply\":true|false,\"turn_kind\":\"direct_reply|delegation_kickoff|delegated_followup|summary_turn\",\"actionable_for_current_route\":true|false,\"visible_handoff_required\":true|false,\"visible_handoff_targets\":[\"username\"],\"reasoning_summary\":\"short explanation\"},\"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\":\"...\",\"decision_bundle\":{\"schema_version\":\"runner_conversation_decision.v1\",\"decision_type\":\"clarify\",\"conversation_intent_mode\":\"single_bot|delegated_single_lead|multi_bot_collab|multi_bot_direct\",\"allowed_responders\":[\"username\"],\"initial_responders\":[\"username\"],\"selected_bot_usernames\":[\"username\"],\"allow_bot_to_bot\":true|false,\"execution_contract_type\":\"|delegation|summary_request|direct_result|final_summary\",\"execution_contract_targets\":[\"username\"],\"next_expected_responders\":[\"username\"],\"should_close_after_reply\":false,\"turn_kind\":\"clarify_turn\",\"actionable_for_current_route\":true|false,\"visible_handoff_required\":true|false,\"visible_handoff_targets\":[\"username\"],\"reasoning_summary\":\"short explanation\"}} or {\"skip\":true,\"reason\":\"...\",\"decision_bundle\":{\"schema_version\":\"runner_conversation_decision.v1\",\"decision_type\":\"skip\",\"conversation_intent_mode\":\"single_bot|delegated_single_lead|multi_bot_collab|multi_bot_direct\",\"allowed_responders\":[\"username\"],\"initial_responders\":[\"username\"],\"selected_bot_usernames\":[\"username\"],\"allow_bot_to_bot\":true|false,\"execution_contract_type\":\"|delegation|summary_request|direct_result|final_summary\",\"execution_contract_targets\":[\"username\"],\"next_expected_responders\":[\"username\"],\"should_close_after_reply\":false,\"turn_kind\":\"skip\",\"actionable_for_current_route\":false,\"visible_handoff_required\":true|false,\"visible_handoff_targets\":[\"username\"],\"reasoning_summary\":\"short explanation\"}}."
2149
+ : "Return strict JSON only. Every non-skip response must include a decision_bundle. Do not rely on code to rewrite your reply or add visible handoff mentions after generation.",
2143
2150
  );
2144
2151
  if (conversation?.mode === "public_multi_bot") {
2145
2152
  const roleGuidance = {
@@ -37,6 +37,13 @@ function firstNonBlankString(values) {
37
37
  return "";
38
38
  }
39
39
 
40
+ function normalizeBoolean(value, fallback = false) {
41
+ if (typeof value === "boolean") {
42
+ return value;
43
+ }
44
+ return fallback;
45
+ }
46
+
40
47
  function normalizeContractAssignments(assignmentsRaw) {
41
48
  return uniqueOrderedSelectors(
42
49
  ensureArray(assignmentsRaw).map((item) => {
@@ -48,31 +55,14 @@ function normalizeContractAssignments(assignmentsRaw) {
48
55
 
49
56
  export function normalizeRunnerConversationDecisionBundle(bundleRaw) {
50
57
  const bundle = safeObject(bundleRaw);
51
- const executionContractTargets = uniqueOrderedSelectors([
52
- ...ensureArray(bundle.execution_contract_targets),
53
- ...normalizeContractAssignments(safeObject(bundle.execution_contract).assignments),
54
- ]);
55
- const nextExpectedResponders = uniqueOrderedSelectors([
56
- ...ensureArray(bundle.next_expected_responders),
57
- ...ensureArray(safeObject(bundle.execution_contract).nextResponders),
58
- ...ensureArray(safeObject(bundle.execution_contract).next_responders),
59
- ...ensureArray(safeObject(bundle.execution_contract).responders),
60
- ...(
61
- executionContractTargets.length > 0
62
- && ["delegation", "summary_request", "direct_result"].includes(
63
- String(bundle.execution_contract_type || safeObject(bundle.execution_contract).type || "").trim().toLowerCase(),
64
- )
65
- ? executionContractTargets
66
- : []
67
- ),
68
- ]);
58
+ const executionContractTargets = uniqueOrderedSelectors(ensureArray(bundle.execution_contract_targets));
59
+ const nextExpectedResponders = uniqueOrderedSelectors(ensureArray(bundle.next_expected_responders));
69
60
  const executionContractType = String(
70
61
  bundle.execution_contract_type
71
62
  || safeObject(bundle.execution_contract).type
72
- || (executionContractTargets.length > 0 ? "delegation" : "")
73
63
  || "",
74
64
  ).trim().toLowerCase();
75
- const allowBotToBot = bundle.allow_bot_to_bot === true;
65
+ const allowBotToBot = normalizeBoolean(bundle.allow_bot_to_bot, false);
76
66
  const allowedResponders = uniqueOrderedSelectors(bundle.allowed_responders);
77
67
  const initialResponders = uniqueOrderedSelectors(bundle.initial_responders);
78
68
  const selectedBotUsernames = uniqueOrderedSelectors(
@@ -94,14 +84,16 @@ export function normalizeRunnerConversationDecisionBundle(bundleRaw) {
94
84
  allow_bot_to_bot: allowBotToBot,
95
85
  conversation_reply_expectation: String(bundle.conversation_reply_expectation || "").trim().toLowerCase(),
96
86
  execution_contract_type: executionContractType,
97
- execution_contract_actionable: bundle.execution_contract_actionable === true
98
- || executionContractTargets.length > 0
99
- || nextExpectedResponders.length > 0,
87
+ execution_contract_actionable: normalizeBoolean(bundle.execution_contract_actionable, false),
100
88
  execution_contract_targets: executionContractTargets,
101
89
  next_expected_responders: nextExpectedResponders,
102
90
  should_close_after_reply: typeof bundle.should_close_after_reply === "boolean"
103
91
  ? bundle.should_close_after_reply
104
- : nextExpectedResponders.length === 0,
92
+ : false,
93
+ turn_kind: String(bundle.turn_kind || "").trim().toLowerCase(),
94
+ actionable_for_current_route: normalizeBoolean(bundle.actionable_for_current_route, false),
95
+ visible_handoff_required: normalizeBoolean(bundle.visible_handoff_required, false),
96
+ visible_handoff_targets: uniqueOrderedSelectors(bundle.visible_handoff_targets),
105
97
  reasoning_summary: firstNonBlankString([bundle.reasoning_summary]),
106
98
  };
107
99
  }
@@ -162,6 +154,14 @@ export function validateRunnerConversationDecisionBundle(bundleRaw) {
162
154
  bundle,
163
155
  };
164
156
  }
157
+ if (bundle.visible_handoff_required === true && bundle.visible_handoff_targets.length === 0) {
158
+ return {
159
+ ok: false,
160
+ status: "missing_visible_handoff_targets",
161
+ reason: "visible_handoff_required needs visible_handoff_targets",
162
+ bundle,
163
+ };
164
+ }
165
165
  return {
166
166
  ok: true,
167
167
  status: "valid",
@@ -171,6 +171,7 @@ export function validateRunnerConversationDecisionBundle(bundleRaw) {
171
171
  }
172
172
 
173
173
  export function buildRunnerConversationDecisionBundle({
174
+ decisionBundle = null,
174
175
  decisionType = "",
175
176
  normalizedIntent = "",
176
177
  humanIntent = null,
@@ -184,6 +185,9 @@ export function buildRunnerConversationDecisionBundle({
184
185
  shouldCloseAfterReply,
185
186
  reasoningSummary = "",
186
187
  }) {
188
+ if (Object.keys(safeObject(decisionBundle)).length > 0) {
189
+ return normalizeRunnerConversationDecisionBundle(decisionBundle);
190
+ }
187
191
  const intent = safeObject(humanIntent);
188
192
  const conversation = safeObject(conversationContext);
189
193
  const contract = safeObject(executionContract);
@@ -244,7 +248,11 @@ export function buildRunnerConversationDecisionBundle({
244
248
  }
245
249
 
246
250
  export function resolveRunnerConversationDecisionBundle(options = {}) {
247
- const bundle = buildRunnerConversationDecisionBundle(options);
251
+ const explicitBundle = safeObject(options.decisionBundle || options.decision_bundle);
252
+ const bundle = buildRunnerConversationDecisionBundle({
253
+ ...options,
254
+ decisionBundle: Object.keys(explicitBundle).length ? explicitBundle : null,
255
+ });
248
256
  const validation = validateRunnerConversationDecisionBundle(bundle);
249
257
  return {
250
258
  decisionBundle: validation.ok ? validation.bundle : bundle,