metheus-governance-mcp-cli 0.2.207 → 0.2.209

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
@@ -2623,7 +2623,7 @@ function inferRunnerRequestClaimIntent(selectedRecord) {
2623
2623
  if (looksLikeRunnerClaimQuestion(rawText) && normalizedText.split(/\s+/).filter(Boolean).length <= 8) {
2624
2624
  return "status_query";
2625
2625
  }
2626
- return "general_execution";
2626
+ return "";
2627
2627
  }
2628
2628
 
2629
2629
  function resolveRunnerRequestClaimIntent({
@@ -2634,7 +2634,8 @@ function resolveRunnerRequestClaimIntent({
2634
2634
  if (explicitIntent) {
2635
2635
  return explicitIntent;
2636
2636
  }
2637
- return inferRunnerRequestClaimIntent(selectedRecord);
2637
+ const inferredIntent = inferRunnerRequestClaimIntent(selectedRecord);
2638
+ return inferredIntent === "status_query" ? inferredIntent : "";
2638
2639
  }
2639
2640
 
2640
2641
  function buildSyntheticReplyChainConversationID(normalizedRoute, chatID, anchorMessageID) {
@@ -2647,6 +2648,16 @@ function buildSyntheticReplyChainConversationID(normalizedRoute, chatID, anchorM
2647
2648
  return `reply_chain:${provider}:${normalizedChatID}:${normalizedAnchorMessageID}`;
2648
2649
  }
2649
2650
 
2651
+ function buildSyntheticHumanOpeningConversationID(normalizedRoute, chatID, messageID) {
2652
+ const provider = String(normalizedRoute?.provider || "").trim() || "unknown";
2653
+ const normalizedChatID = String(chatID || "").trim() || "-";
2654
+ const normalizedMessageID = intFromRawAllowZero(messageID, 0);
2655
+ if (normalizedMessageID <= 0) {
2656
+ return "";
2657
+ }
2658
+ return `human_opening:${provider}:${normalizedChatID}:${normalizedMessageID}`;
2659
+ }
2660
+
2650
2661
  function findRunnerRequestsForScope(state, normalizedRoute, selectors = {}) {
2651
2662
  const requests = normalizeBotRunnerRequests(state?.requests);
2652
2663
  const projectID = String(normalizedRoute?.projectID || "").trim();
@@ -3259,13 +3270,17 @@ async function claimRunnerRequestForHumanComment({
3259
3270
  normalizedIntent,
3260
3271
  selectedRecord,
3261
3272
  });
3262
- const requestKey = buildRunnerRequestKey({
3273
+ let stateForClaim = safeObject(replyChainResolution.state);
3274
+ const normalizedSharedHumanIntent = safeObject(sharedHumanIntent);
3275
+ const provisionalNormalizedIntent = String(
3276
+ normalizedSharedHumanIntent.intentType || resolvedNormalizedIntent || "",
3277
+ ).trim().toLowerCase();
3278
+ const provisionalRequestKey = buildRunnerRequestKey({
3263
3279
  normalizedRoute,
3264
3280
  selectedRecord,
3265
3281
  selectedBotUsernames,
3266
- normalizedIntent: resolvedNormalizedIntent,
3282
+ normalizedIntent: provisionalNormalizedIntent,
3267
3283
  });
3268
- let stateForClaim = safeObject(replyChainResolution.state);
3269
3284
  const baseConversationID = String(
3270
3285
  parsed.conversationID
3271
3286
  || replyChainContext.conversationID
@@ -3285,9 +3300,6 @@ async function claimRunnerRequestForHumanComment({
3285
3300
  requests: backfilled.requests,
3286
3301
  };
3287
3302
  }
3288
- const requests = normalizeBotRunnerRequests(stateForClaim.requests);
3289
- const existing = safeObject(requests[requestKey]);
3290
- const normalizedSharedHumanIntent = safeObject(sharedHumanIntent);
3291
3303
  const currentMessageID = intFromRawAllowZero(parsed.messageID, 0);
3292
3304
  let sharedConversationSource = currentMessageID > 0
3293
3305
  ? pickRunnerSharedConversationSourceRequest(
@@ -3295,7 +3307,7 @@ async function claimRunnerRequestForHumanComment({
3295
3307
  chatID: String(parsed.chatID || parsed.chatId || "").trim(),
3296
3308
  messageID: currentMessageID,
3297
3309
  }),
3298
- requestKey,
3310
+ provisionalRequestKey,
3299
3311
  )
3300
3312
  : {};
3301
3313
  if (
@@ -3309,14 +3321,36 @@ async function claimRunnerRequestForHumanComment({
3309
3321
  runtime,
3310
3322
  chatID: String(parsed.chatID || parsed.chatId || "").trim(),
3311
3323
  messageID: currentMessageID,
3312
- excludeRequestKey: requestKey,
3324
+ excludeRequestKey: provisionalRequestKey,
3313
3325
  }));
3314
3326
  }
3315
3327
  const resolvedConversationID = String(
3316
3328
  baseConversationID
3317
3329
  || sharedConversationSource.conversation_id
3330
+ || (
3331
+ Object.keys(normalizedSharedHumanIntent).length > 0
3332
+ ? buildSyntheticHumanOpeningConversationID(
3333
+ normalizedRoute,
3334
+ String(parsed.chatID || parsed.chatId || "").trim(),
3335
+ currentMessageID,
3336
+ )
3337
+ : ""
3338
+ )
3318
3339
  || "",
3319
3340
  ).trim();
3341
+ const preferredNormalizedIntent = String(
3342
+ normalizedSharedHumanIntent.intentType
3343
+ || resolvedNormalizedIntent
3344
+ || "",
3345
+ ).trim().toLowerCase();
3346
+ const requestKey = buildRunnerRequestKey({
3347
+ normalizedRoute,
3348
+ selectedRecord,
3349
+ selectedBotUsernames,
3350
+ normalizedIntent: preferredNormalizedIntent,
3351
+ });
3352
+ const requests = normalizeBotRunnerRequests(stateForClaim.requests);
3353
+ const existing = safeObject(requests[requestKey]);
3320
3354
  if (isFinalRunnerRequestStatus(existing.status)) {
3321
3355
  return {
3322
3356
  ok: false,
@@ -3346,50 +3380,70 @@ async function claimRunnerRequestForHumanComment({
3346
3380
  conversation_id: resolvedConversationID,
3347
3381
  selected_bot_usernames: uniqueOrderedStrings(selectedBotUsernames, normalizeTelegramMentionUsername),
3348
3382
  conversation_intent_mode: String(
3349
- existing.conversation_intent_mode || sharedConversationSource.conversation_intent_mode || referencedRequest.conversation_intent_mode || normalizedSharedHumanIntent.intentMode || "",
3383
+ normalizedSharedHumanIntent.intentMode
3384
+ || existing.conversation_intent_mode
3385
+ || sharedConversationSource.conversation_intent_mode
3386
+ || referencedRequest.conversation_intent_mode
3387
+ || "",
3350
3388
  ).trim().toLowerCase(),
3351
3389
  conversation_lead_bot: normalizeTelegramMentionUsername(
3352
- existing.conversation_lead_bot || sharedConversationSource.conversation_lead_bot || referencedRequest.conversation_lead_bot || normalizedSharedHumanIntent.leadBotSelector,
3390
+ normalizedSharedHumanIntent.leadBotSelector
3391
+ || existing.conversation_lead_bot
3392
+ || sharedConversationSource.conversation_lead_bot
3393
+ || referencedRequest.conversation_lead_bot,
3353
3394
  ),
3354
3395
  conversation_summary_bot: normalizeTelegramMentionUsername(
3355
- existing.conversation_summary_bot || sharedConversationSource.conversation_summary_bot || referencedRequest.conversation_summary_bot || normalizedSharedHumanIntent.summaryBotSelector,
3396
+ normalizedSharedHumanIntent.summaryBotSelector
3397
+ || existing.conversation_summary_bot
3398
+ || sharedConversationSource.conversation_summary_bot
3399
+ || referencedRequest.conversation_summary_bot,
3356
3400
  ),
3357
3401
  conversation_participants: uniqueOrderedStrings(
3358
- ensureArray(existing.conversation_participants).length
3359
- ? existing.conversation_participants
3360
- : ensureArray(sharedConversationSource.conversation_participants).length
3361
- ? sharedConversationSource.conversation_participants
3362
- : ensureArray(referencedRequest.conversation_participants).length
3363
- ? referencedRequest.conversation_participants
3364
- : normalizedSharedHumanIntent.participantSelectors,
3402
+ ensureArray(normalizedSharedHumanIntent.participantSelectors).length
3403
+ ? normalizedSharedHumanIntent.participantSelectors
3404
+ : ensureArray(existing.conversation_participants).length
3405
+ ? existing.conversation_participants
3406
+ : ensureArray(sharedConversationSource.conversation_participants).length
3407
+ ? sharedConversationSource.conversation_participants
3408
+ : ensureArray(referencedRequest.conversation_participants).length
3409
+ ? referencedRequest.conversation_participants
3410
+ : [],
3365
3411
  normalizeTelegramMentionUsername,
3366
3412
  ),
3367
3413
  conversation_initial_responders: uniqueOrderedStrings(
3368
- ensureArray(existing.conversation_initial_responders).length
3369
- ? existing.conversation_initial_responders
3370
- : ensureArray(sharedConversationSource.conversation_initial_responders).length
3371
- ? sharedConversationSource.conversation_initial_responders
3372
- : ensureArray(referencedRequest.conversation_initial_responders).length
3373
- ? referencedRequest.conversation_initial_responders
3374
- : normalizedSharedHumanIntent.initialResponderSelectors,
3414
+ ensureArray(normalizedSharedHumanIntent.initialResponderSelectors).length
3415
+ ? normalizedSharedHumanIntent.initialResponderSelectors
3416
+ : ensureArray(existing.conversation_initial_responders).length
3417
+ ? existing.conversation_initial_responders
3418
+ : ensureArray(sharedConversationSource.conversation_initial_responders).length
3419
+ ? sharedConversationSource.conversation_initial_responders
3420
+ : ensureArray(referencedRequest.conversation_initial_responders).length
3421
+ ? referencedRequest.conversation_initial_responders
3422
+ : [],
3375
3423
  normalizeTelegramMentionUsername,
3376
3424
  ),
3377
3425
  conversation_allowed_responders: uniqueOrderedStrings(
3378
- ensureArray(existing.conversation_allowed_responders).length
3379
- ? existing.conversation_allowed_responders
3380
- : ensureArray(sharedConversationSource.conversation_allowed_responders).length
3381
- ? sharedConversationSource.conversation_allowed_responders
3382
- : ensureArray(referencedRequest.conversation_allowed_responders).length
3383
- ? referencedRequest.conversation_allowed_responders
3384
- : normalizedSharedHumanIntent.allowedResponderSelectors,
3426
+ ensureArray(normalizedSharedHumanIntent.allowedResponderSelectors).length
3427
+ ? normalizedSharedHumanIntent.allowedResponderSelectors
3428
+ : ensureArray(existing.conversation_allowed_responders).length
3429
+ ? existing.conversation_allowed_responders
3430
+ : ensureArray(sharedConversationSource.conversation_allowed_responders).length
3431
+ ? sharedConversationSource.conversation_allowed_responders
3432
+ : ensureArray(referencedRequest.conversation_allowed_responders).length
3433
+ ? referencedRequest.conversation_allowed_responders
3434
+ : [],
3385
3435
  normalizeTelegramMentionUsername,
3386
3436
  ),
3387
- conversation_allow_bot_to_bot: existing.conversation_allow_bot_to_bot === true
3437
+ conversation_allow_bot_to_bot: normalizedSharedHumanIntent.allowBotToBot === true
3438
+ || existing.conversation_allow_bot_to_bot === true
3388
3439
  || sharedConversationSource.conversation_allow_bot_to_bot === true
3389
- || referencedRequest.conversation_allow_bot_to_bot === true
3390
- || normalizedSharedHumanIntent.allowBotToBot === true,
3440
+ || referencedRequest.conversation_allow_bot_to_bot === true,
3391
3441
  conversation_reply_expectation: String(
3392
- existing.conversation_reply_expectation || sharedConversationSource.conversation_reply_expectation || referencedRequest.conversation_reply_expectation || normalizedSharedHumanIntent.replyExpectation || "",
3442
+ normalizedSharedHumanIntent.replyExpectation
3443
+ || existing.conversation_reply_expectation
3444
+ || sharedConversationSource.conversation_reply_expectation
3445
+ || referencedRequest.conversation_reply_expectation
3446
+ || "",
3393
3447
  ).trim().toLowerCase(),
3394
3448
  execution_contract_type: String(
3395
3449
  existing.execution_contract_type || sharedConversationSource.execution_contract_type || referencedRequest.execution_contract_type || "",
@@ -3412,10 +3466,10 @@ async function claimRunnerRequestForHumanComment({
3412
3466
  ? sharedConversationSource.next_expected_responders
3413
3467
  : ensureArray(referencedRequest.next_expected_responders).length
3414
3468
  ? referencedRequest.next_expected_responders
3415
- : normalizedSharedHumanIntent.initialResponderSelectors,
3469
+ : [],
3416
3470
  normalizeTelegramMentionUsername,
3417
3471
  ),
3418
- normalized_intent: resolvedNormalizedIntent,
3472
+ normalized_intent: String(preferredNormalizedIntent || existing.normalized_intent || "").trim().toLowerCase(),
3419
3473
  status: "claimed",
3420
3474
  claimed_by_route: String(routeKey || "").trim(),
3421
3475
  claimed_at: firstNonEmptyString([existing.claimed_at, nowISO]) || nowISO,
@@ -3466,6 +3520,89 @@ function isActionableRunnerRequestIntent(rawIntent) {
3466
3520
  return Boolean(normalizedIntent) && !isInformationalRunnerRequestIntent(normalizedIntent);
3467
3521
  }
3468
3522
 
3523
+ function runnerRequestHasConversationContract(requestRaw) {
3524
+ const request = safeObject(requestRaw);
3525
+ return Boolean(
3526
+ String(request.conversation_id || "").trim()
3527
+ || String(request.conversation_intent_mode || "").trim()
3528
+ || String(request.conversation_reply_expectation || "").trim()
3529
+ || ensureArray(request.conversation_participants).length
3530
+ || ensureArray(request.conversation_initial_responders).length
3531
+ || ensureArray(request.conversation_allowed_responders).length
3532
+ );
3533
+ }
3534
+
3535
+ function runnerRequestHasCompleteConversationContract(requestRaw) {
3536
+ const request = safeObject(requestRaw);
3537
+ const intentMode = String(request.conversation_intent_mode || "").trim().toLowerCase();
3538
+ const conversationID = String(request.conversation_id || "").trim();
3539
+ const replyExpectation = String(request.conversation_reply_expectation || "").trim().toLowerCase();
3540
+ const participants = ensureArray(request.conversation_participants);
3541
+ const initialResponders = ensureArray(request.conversation_initial_responders);
3542
+ const allowedResponders = ensureArray(request.conversation_allowed_responders);
3543
+ if (!runnerRequestHasConversationContract(request)) {
3544
+ return false;
3545
+ }
3546
+ if (!conversationID || !intentMode || !replyExpectation) {
3547
+ return false;
3548
+ }
3549
+ if (intentMode === "single_bot") {
3550
+ return allowedResponders.length > 0 || participants.length > 0;
3551
+ }
3552
+ return participants.length > 0 && initialResponders.length > 0 && allowedResponders.length > 0;
3553
+ }
3554
+
3555
+ function runnerRequestHasExecutionContract(requestRaw) {
3556
+ const request = safeObject(requestRaw);
3557
+ return Boolean(
3558
+ String(request.execution_contract_type || "").trim()
3559
+ || request.execution_contract_actionable === true
3560
+ || ensureArray(request.execution_contract_targets).length
3561
+ || ensureArray(request.next_expected_responders).length
3562
+ );
3563
+ }
3564
+
3565
+ function runnerRequestHasCompleteExecutionContract(requestRaw) {
3566
+ const request = safeObject(requestRaw);
3567
+ const executionContractType = String(request.execution_contract_type || "").trim().toLowerCase();
3568
+ const executionTargets = ensureArray(request.execution_contract_targets);
3569
+ const nextExpectedResponders = ensureArray(request.next_expected_responders);
3570
+ if (!runnerRequestHasExecutionContract(request)) {
3571
+ return false;
3572
+ }
3573
+ if (!executionContractType) {
3574
+ return false;
3575
+ }
3576
+ if (executionContractType === "delegation") {
3577
+ return executionTargets.length > 0 || nextExpectedResponders.length > 0;
3578
+ }
3579
+ if (executionContractType === "summary_request") {
3580
+ return nextExpectedResponders.length > 0 || executionTargets.length > 0;
3581
+ }
3582
+ return true;
3583
+ }
3584
+
3585
+ function runnerRequestHasContractSignals(requestRaw) {
3586
+ const request = safeObject(requestRaw);
3587
+ return runnerRequestHasConversationContract(request) || runnerRequestHasExecutionContract(request);
3588
+ }
3589
+
3590
+ function runnerRequestRequiresActionableContract(requestRaw) {
3591
+ const request = safeObject(requestRaw);
3592
+ const replyExpectation = String(request.conversation_reply_expectation || "").trim().toLowerCase();
3593
+ const executionContractType = String(request.execution_contract_type || "").trim().toLowerCase();
3594
+ if (request.execution_contract_actionable === true) {
3595
+ return true;
3596
+ }
3597
+ if (replyExpectation === "actionable") {
3598
+ return true;
3599
+ }
3600
+ if (["delegation", "direct_result"].includes(executionContractType)) {
3601
+ return true;
3602
+ }
3603
+ return false;
3604
+ }
3605
+
3469
3606
  function loadRunnerRequestByKey(requestKey) {
3470
3607
  const key = String(requestKey || "").trim();
3471
3608
  if (!key) {
@@ -3477,7 +3614,7 @@ function loadRunnerRequestByKey(requestKey) {
3477
3614
  function actionableRunnerRequestMissingRootWorkItem(requestRaw) {
3478
3615
  const request = safeObject(requestRaw);
3479
3616
  return (
3480
- isActionableRunnerRequestIntent(request.normalized_intent)
3617
+ runnerRequestRequiresActionableContract(request)
3481
3618
  && !String(request.root_work_item_id || "").trim()
3482
3619
  );
3483
3620
  }
@@ -3789,7 +3926,7 @@ async function ensureRunnerRootWorkItemForRequest({
3789
3926
  requestKey: key,
3790
3927
  };
3791
3928
  }
3792
- if (!isActionableRunnerRequestIntent(existing.normalized_intent)) {
3929
+ if (!runnerRequestRequiresActionableContract(existing)) {
3793
3930
  return {
3794
3931
  ok: true,
3795
3932
  requestKey: key,
@@ -4181,7 +4318,7 @@ function resolveRunnerContinuationRequestForBotReply({
4181
4318
  execution_contract_actionable: session.last_execution_contract_actionable === true,
4182
4319
  execution_contract_targets: ensureArray(session.last_execution_contract_targets),
4183
4320
  next_expected_responders: ensureArray(session.next_expected_responders),
4184
- normalized_intent: String(safeObject(sessionMatch.routeState).last_intent_type || "").trim().toLowerCase(),
4321
+ normalized_intent: String(fallbackRequest.normalized_intent || "").trim().toLowerCase(),
4185
4322
  status: "running",
4186
4323
  claimed_by_route: String(sessionMatch.routeKey || "").trim(),
4187
4324
  claimed_at: firstNonEmptyString([session.started_at, nowISO]),
@@ -4298,6 +4435,26 @@ function markRunnerRequestLifecycle({
4298
4435
  normalizeTelegramMentionUsername,
4299
4436
  ).filter((selector) => selector && selector !== normalizedCurrentBotSelector);
4300
4437
  const shouldRemainRunningAfterReply = continuationSelectors.length > 0;
4438
+ const nextConversationIntentMode = String(
4439
+ conversationIntentMode
4440
+ || existing.conversation_intent_mode
4441
+ || "",
4442
+ ).trim().toLowerCase();
4443
+ const nextExecutionContractType = String(
4444
+ executionContractType
4445
+ || existing.execution_contract_type
4446
+ || "",
4447
+ ).trim().toLowerCase();
4448
+ const nextNormalizedIntent = (() => {
4449
+ const explicitIntent = String(normalizedIntent || "").trim().toLowerCase();
4450
+ if (explicitIntent) {
4451
+ return explicitIntent;
4452
+ }
4453
+ if (nextConversationIntentMode && !nextExecutionContractType) {
4454
+ return "";
4455
+ }
4456
+ return String(existing.normalized_intent || "").trim().toLowerCase();
4457
+ })();
4301
4458
  const normalizedOutcome = String(outcome || "").trim().toLowerCase();
4302
4459
  const nextStatus = (() => {
4303
4460
  if (normalizedOutcome === "claimed") return "claimed";
@@ -4329,11 +4486,7 @@ function markRunnerRequestLifecycle({
4329
4486
  ensureArray(allowedResponders).length ? allowedResponders : existing.conversation_allowed_responders,
4330
4487
  normalizeTelegramMentionUsername,
4331
4488
  ),
4332
- conversation_intent_mode: String(
4333
- conversationIntentMode
4334
- || existing.conversation_intent_mode
4335
- || "",
4336
- ).trim().toLowerCase(),
4489
+ conversation_intent_mode: nextConversationIntentMode,
4337
4490
  conversation_lead_bot: normalizeTelegramMentionUsername(
4338
4491
  conversationLeadBot
4339
4492
  || existing.conversation_lead_bot
@@ -4351,11 +4504,7 @@ function markRunnerRequestLifecycle({
4351
4504
  || existing.conversation_reply_expectation
4352
4505
  || "",
4353
4506
  ).trim().toLowerCase(),
4354
- execution_contract_type: String(
4355
- executionContractType
4356
- || existing.execution_contract_type
4357
- || "",
4358
- ).trim().toLowerCase(),
4507
+ execution_contract_type: nextExecutionContractType,
4359
4508
  execution_contract_actionable: executionContractActionable === true
4360
4509
  || existing.execution_contract_actionable === true,
4361
4510
  execution_contract_targets: uniqueOrderedStrings(
@@ -4366,7 +4515,7 @@ function markRunnerRequestLifecycle({
4366
4515
  ensureArray(nextExpectedResponders).length ? nextExpectedResponders : existing.next_expected_responders,
4367
4516
  normalizeTelegramMentionUsername,
4368
4517
  ),
4369
- normalized_intent: String(normalizedIntent || existing.normalized_intent || "").trim().toLowerCase(),
4518
+ normalized_intent: nextNormalizedIntent,
4370
4519
  status: nextStatus,
4371
4520
  started_at: firstNonEmptyString([existing.started_at, nowISO]),
4372
4521
  completed_at: nextStatus === "completed" ? nowISO : String(existing.completed_at || "").trim(),
@@ -4807,7 +4956,7 @@ async function syncRunnerRequestLedgerForProjectToServer({ normalizedRoute, runt
4807
4956
  })[String(request.request_key || "").trim()]
4808
4957
  : request;
4809
4958
  if (
4810
- isActionableRunnerRequestIntent(recoveredRequest.normalized_intent)
4959
+ runnerRequestRequiresActionableContract(recoveredRequest)
4811
4960
  && !String(recoveredRequest.root_work_item_id || "").trim()
4812
4961
  ) {
4813
4962
  continue;
@@ -949,7 +949,7 @@ function normalizeExecutionContract(rawContract) {
949
949
  || contract.target_summary_bot
950
950
  || "",
951
951
  ).trim().replace(/^@+/, "").toLowerCase();
952
- const nextResponders = uniqueOrdered(
952
+ const explicitNextResponders = uniqueOrdered(
953
953
  ensureArray(contract.next_responders || contract.nextResponders || contract.responders)
954
954
  .map((item) => String(item || "").trim().replace(/^@+/, "").toLowerCase())
955
955
  .filter(Boolean),
@@ -962,6 +962,13 @@ function normalizeExecutionContract(rawContract) {
962
962
  ].includes(type)
963
963
  ? type
964
964
  : "";
965
+ const nextResponders = explicitNextResponders.length > 0
966
+ ? explicitNextResponders
967
+ : normalizedType === "delegation"
968
+ ? uniqueOrdered(assignments.map((item) => String(item.target_bot || item.targetBot || "").trim().replace(/^@+/, "").toLowerCase()).filter(Boolean))
969
+ : normalizedType === "summary_request" && summaryBot
970
+ ? [summaryBot]
971
+ : [];
965
972
  const actionable = contract.actionable === true
966
973
  || (normalizedType === "delegation" && assignments.length > 0)
967
974
  || (normalizedType === "summary_request" && Boolean(summaryBot))
@@ -2078,7 +2085,9 @@ export function serializeLocalAIResult(result) {
2078
2085
  function buildConversationIntentAnalysisPrompt({
2079
2086
  messageText,
2080
2087
  managedBots,
2088
+ contractGuardrail = null,
2081
2089
  }) {
2090
+ const guardrail = safeObject(contractGuardrail);
2082
2091
  const bots = ensureArray(managedBots).map((item) => {
2083
2092
  const bot = safeObject(item);
2084
2093
  return {
@@ -2122,6 +2131,15 @@ function buildConversationIntentAnalysisPrompt({
2122
2131
  "- If the human asks bots to discuss, debate, review, brainstorm, compare opinions, or hold a conversation about a topic, default to reply_expectation=informational unless they explicitly ask for concrete execution/output now.",
2123
2132
  "- reply_expectation=actionable when the human is asking the bot(s) to actually do work now, produce concrete results, create/update files, delegate concrete tasks, or otherwise execute immediately.",
2124
2133
  "- reply_expectation=informational when the human is only asking for explanation, status, location, clarification, discussion, review, brainstorming, or other non-execution information.",
2134
+ guardrail.require_complete_contract === true
2135
+ ? "- Contract guardrail: do not leave intent_type, reply_expectation, participants, initial_responders, or allowed_responders empty when the message clearly addresses managed bots."
2136
+ : "",
2137
+ guardrail.require_explicit_actionable_intent === true
2138
+ ? "- Contract guardrail: if reply_expectation=actionable, intent_type must be exactly one of ctxpack_mutation, workitem_mutation, or general_execution."
2139
+ : "",
2140
+ String(guardrail.reason || "").trim()
2141
+ ? `- Contract guardrail reason: ${String(guardrail.reason || "").trim()}`
2142
+ : "",
2125
2143
  "",
2126
2144
  `managed_bots=${JSON.stringify(bots)}`,
2127
2145
  `human_message=${JSON.stringify(String(messageText || "").trim())}`,
@@ -2464,6 +2482,7 @@ export function analyzeHumanConversationIntentWithAI({
2464
2482
  messageText,
2465
2483
  managedBots,
2466
2484
  workspaceDir,
2485
+ contractGuardrail = null,
2467
2486
  client = "",
2468
2487
  model = "",
2469
2488
  env = process.env,
@@ -2491,6 +2510,7 @@ export function analyzeHumanConversationIntentWithAI({
2491
2510
  promptText: buildConversationIntentAnalysisPrompt({
2492
2511
  messageText,
2493
2512
  managedBots: bots,
2513
+ contractGuardrail,
2494
2514
  }),
2495
2515
  workspaceDir,
2496
2516
  model: parserModel,
@@ -101,11 +101,18 @@ function normalizeExecutionContractForArchive(rawContract) {
101
101
  })
102
102
  .filter(Boolean);
103
103
  const summaryBot = normalizeMentionUsername(contract.summary_bot || contract.summaryBot || "");
104
- const nextResponders = Array.from(new Set(
104
+ const explicitNextResponders = Array.from(new Set(
105
105
  ensureArray(contract.next_responders || contract.nextResponders || contract.responders)
106
106
  .map((item) => normalizeMentionUsername(item))
107
107
  .filter(Boolean),
108
108
  ));
109
+ const nextResponders = explicitNextResponders.length > 0
110
+ ? explicitNextResponders
111
+ : type === "delegation"
112
+ ? Array.from(new Set(assignments.map((item) => normalizeMentionUsername(item.target_bot)).filter(Boolean)))
113
+ : type === "summary_request" && summaryBot
114
+ ? [summaryBot]
115
+ : [];
109
116
  const actionable = contract.actionable === true;
110
117
  if (!type && !assignments.length && !summaryBot && !nextResponders.length && !actionable) {
111
118
  return null;