nexus-agents 3.3.2 → 3.4.0

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.
Files changed (25) hide show
  1. package/dist/{chunk-UNDYHLRR.js → chunk-2KVTXC7Y.js} +6 -1
  2. package/dist/{chunk-UNDYHLRR.js.map → chunk-2KVTXC7Y.js.map} +1 -1
  3. package/dist/{chunk-VSFWQHRX.js → chunk-CFK4VK2M.js} +6 -6
  4. package/dist/{chunk-LVFYELQU.js → chunk-FHTU4BSP.js} +3 -3
  5. package/dist/{chunk-3T3TZZKG.js → chunk-JEV6RUUH.js} +2 -2
  6. package/dist/{chunk-RA5L4BVA.js → chunk-QAEBYGSP.js} +2 -2
  7. package/dist/{chunk-RAYKASM4.js → chunk-XL6T3HTN.js} +261 -31
  8. package/dist/chunk-XL6T3HTN.js.map +1 -0
  9. package/dist/cli.d.ts +1 -1
  10. package/dist/cli.js +7 -7
  11. package/dist/{consensus-vote-UF73LBG6.js → consensus-vote-EIEPPKAD.js} +3 -3
  12. package/dist/{consensus-vote-types-C7kiyyqD.d.ts → consensus-vote-types-jDyddCzg.d.ts} +27 -0
  13. package/dist/{improvement-review-B7DR4Y3C.js → improvement-review-YYHQ2D5L.js} +3 -3
  14. package/dist/index.d.ts +4 -2
  15. package/dist/index.js +6 -6
  16. package/dist/{setup-command-COUBH2NS.js → setup-command-CYZO22RO.js} +3 -3
  17. package/package.json +1 -1
  18. package/dist/chunk-RAYKASM4.js.map +0 -1
  19. /package/dist/{chunk-VSFWQHRX.js.map → chunk-CFK4VK2M.js.map} +0 -0
  20. /package/dist/{chunk-LVFYELQU.js.map → chunk-FHTU4BSP.js.map} +0 -0
  21. /package/dist/{chunk-3T3TZZKG.js.map → chunk-JEV6RUUH.js.map} +0 -0
  22. /package/dist/{chunk-RA5L4BVA.js.map → chunk-QAEBYGSP.js.map} +0 -0
  23. /package/dist/{consensus-vote-UF73LBG6.js.map → consensus-vote-EIEPPKAD.js.map} +0 -0
  24. /package/dist/{improvement-review-B7DR4Y3C.js.map → improvement-review-YYHQ2D5L.js.map} +0 -0
  25. /package/dist/{setup-command-COUBH2NS.js.map → setup-command-CYZO22RO.js.map} +0 -0
@@ -21,7 +21,7 @@ import {
21
21
  DEFAULT_TASK_TTL_MS,
22
22
  DEFAULT_TOOL_RATE_LIMITS,
23
23
  clampTaskTtl
24
- } from "./chunk-LVFYELQU.js";
24
+ } from "./chunk-FHTU4BSP.js";
25
25
  import {
26
26
  executeExpert
27
27
  } from "./chunk-H3B6BSBZ.js";
@@ -45,7 +45,7 @@ import {
45
45
  simulationDeniedResult,
46
46
  toJobSummary,
47
47
  writeJobCancelled
48
- } from "./chunk-RAYKASM4.js";
48
+ } from "./chunk-XL6T3HTN.js";
49
49
  import {
50
50
  normalizeTopicToCanonical,
51
51
  synthesizeResearch
@@ -59,7 +59,7 @@ import {
59
59
  } from "./chunk-G2C4WNK7.js";
60
60
  import {
61
61
  withPrerequisite
62
- } from "./chunk-3T3TZZKG.js";
62
+ } from "./chunk-JEV6RUUH.js";
63
63
  import {
64
64
  NOOP_NOTIFIER,
65
65
  RateLimiter,
@@ -82,7 +82,7 @@ import {
82
82
  withAuditTrail,
83
83
  withProgressHeartbeat,
84
84
  wrapToolWithTimeout
85
- } from "./chunk-UNDYHLRR.js";
85
+ } from "./chunk-2KVTXC7Y.js";
86
86
  import {
87
87
  createAuditTrail,
88
88
  createGraphAuditBridge,
@@ -45787,7 +45787,7 @@ ${contextBlock}`;
45787
45787
  const strategy = config.votingStrategy ?? "higher_order";
45788
45788
  await postProgress(config, "Vote", `Running consensus with ${strategy} strategy...`);
45789
45789
  try {
45790
- const { executeVoting } = await import("./consensus-vote-UF73LBG6.js");
45790
+ const { executeVoting } = await import("./consensus-vote-EIEPPKAD.js");
45791
45791
  const votingResult = await executeVoting(
45792
45792
  {
45793
45793
  proposal: buildVoteProposal(plan, research),
@@ -54166,4 +54166,4 @@ export {
54166
54166
  shutdownFeedbackSubscriber,
54167
54167
  createEventBusBridge
54168
54168
  };
54169
- //# sourceMappingURL=chunk-VSFWQHRX.js.map
54169
+ //# sourceMappingURL=chunk-CFK4VK2M.js.map
@@ -49,7 +49,7 @@ import {
49
49
  } from "./chunk-OQLFRJCW.js";
50
50
 
51
51
  // src/version.ts
52
- var VERSION = true ? "3.3.2" : "dev";
52
+ var VERSION = true ? "3.4.0" : "dev";
53
53
 
54
54
  // src/config/schemas-core.ts
55
55
  import { z } from "zod";
@@ -2253,7 +2253,7 @@ async function runDoctorFix(result) {
2253
2253
  writeLine2("\u2500".repeat(40));
2254
2254
  let fixCount = 0;
2255
2255
  if (!result.dataDirectory.rootExists || result.dataDirectory.subdirectories.some((d) => !d.exists || !d.writable)) {
2256
- const { runSetup } = await import("./setup-command-COUBH2NS.js");
2256
+ const { runSetup } = await import("./setup-command-CYZO22RO.js");
2257
2257
  const setupResult = runSetup({
2258
2258
  skipMcp: true,
2259
2259
  skipRules: true,
@@ -2366,4 +2366,4 @@ export {
2366
2366
  startStdioServer,
2367
2367
  closeServer
2368
2368
  };
2369
- //# sourceMappingURL=chunk-LVFYELQU.js.map
2369
+ //# sourceMappingURL=chunk-FHTU4BSP.js.map
@@ -7,7 +7,7 @@ import {
7
7
  toolStructuredError,
8
8
  toolSuccessStructured,
9
9
  wrapToolWithTimeout
10
- } from "./chunk-UNDYHLRR.js";
10
+ } from "./chunk-2KVTXC7Y.js";
11
11
  import {
12
12
  JsonlStore,
13
13
  SECURITY_KEYWORDS,
@@ -2132,4 +2132,4 @@ export {
2132
2132
  runImprovementReview,
2133
2133
  registerImprovementReviewTool
2134
2134
  };
2135
- //# sourceMappingURL=chunk-3T3TZZKG.js.map
2135
+ //# sourceMappingURL=chunk-JEV6RUUH.js.map
@@ -8,7 +8,7 @@ import {
8
8
  checkSqlite,
9
9
  defaultConfig,
10
10
  initDataDirectories
11
- } from "./chunk-LVFYELQU.js";
11
+ } from "./chunk-FHTU4BSP.js";
12
12
  import {
13
13
  BUILT_IN_EXPERTS
14
14
  } from "./chunk-YPNQPT2F.js";
@@ -2001,4 +2001,4 @@ export {
2001
2001
  setupCommand,
2002
2002
  setupCommandAsync
2003
2003
  };
2004
- //# sourceMappingURL=chunk-RA5L4BVA.js.map
2004
+ //# sourceMappingURL=chunk-QAEBYGSP.js.map
@@ -18,7 +18,7 @@ import {
18
18
  toolSuccess,
19
19
  withProgressHeartbeat,
20
20
  wrapToolWithTimeout
21
- } from "./chunk-UNDYHLRR.js";
21
+ } from "./chunk-2KVTXC7Y.js";
22
22
  import {
23
23
  createEvent,
24
24
  getGlobalRegistry
@@ -369,6 +369,57 @@ var SIMULATED_VOTE_REASONING = {
369
369
 
370
370
  // src/cli/voter-response.ts
371
371
  import { z } from "zod";
372
+
373
+ // src/consensus/option-tally.ts
374
+ var SUPERMAJORITY_SHARE = 2 / 3;
375
+ var MAJORITY_SHARE = 0.5;
376
+ function matchDeclaredOption(raw, declaredOptions) {
377
+ if (raw === void 0) return void 0;
378
+ const normalized = raw.trim().toLowerCase();
379
+ if (normalized === "") return void 0;
380
+ return declaredOptions.find((option) => option.trim().toLowerCase() === normalized);
381
+ }
382
+ function tallyOptions(votes, declaredOptions) {
383
+ const approvers = votes.filter((v) => v.decision === "approve");
384
+ const counts = /* @__PURE__ */ new Map();
385
+ let selectedCount = 0;
386
+ for (const vote of approvers) {
387
+ const matched = matchDeclaredOption(vote.selectedOption, declaredOptions);
388
+ if (matched === void 0) continue;
389
+ selectedCount += 1;
390
+ counts.set(matched, (counts.get(matched) ?? 0) + 1);
391
+ }
392
+ const tally = [...counts.entries()].map(([option, count]) => ({ option, count })).sort((a, b) => b.count !== a.count ? b.count - a.count : a.option.localeCompare(b.option));
393
+ const leader = tally[0];
394
+ const approverCount = approvers.length;
395
+ const leadingCount = leader?.count ?? 0;
396
+ return {
397
+ tally,
398
+ ...leader !== void 0 ? { leadingOption: leader.option } : {},
399
+ leadingCount,
400
+ approverCount,
401
+ selectedCount,
402
+ unattributedApprovals: approverCount - selectedCount,
403
+ leadingShare: approverCount > 0 ? leadingCount / approverCount : 0
404
+ };
405
+ }
406
+ function evaluateOptionThreshold(tally, threshold) {
407
+ const approved = clears(tally, threshold);
408
+ return { ...tally, threshold, approved };
409
+ }
410
+ function clears(tally, threshold) {
411
+ if (tally.approverCount === 0 || tally.leadingCount === 0) return false;
412
+ switch (threshold) {
413
+ case "unanimous":
414
+ return tally.leadingCount === tally.approverCount;
415
+ case "supermajority":
416
+ return tally.leadingShare >= SUPERMAJORITY_SHARE;
417
+ case "majority":
418
+ return tally.leadingShare > MAJORITY_SHARE;
419
+ }
420
+ }
421
+
422
+ // src/cli/voter-response.ts
372
423
  var SyntheticVoteError = class extends Error {
373
424
  constructor(reason, rawOutput) {
374
425
  super(`Vote response parsing failed: ${reason}`);
@@ -408,7 +459,11 @@ var VoteResponseSchema = z.object({
408
459
  ).optional().describe("Rejection reason categories when decision is reject"),
409
460
  /** Top-level structured findings for PR-review mode (#2245 v4 follow-up).
410
461
  * Replaces the YAML-in-reasoning encoding that proved lossy. */
411
- findings: z.array(RawFindingSchema).optional().describe("Structured findings (PR review only)")
462
+ findings: z.array(RawFindingSchema).optional().describe("Structured findings (PR review only)"),
463
+ /** Chosen option when the proposal declared `options` (#4472). Validated
464
+ * against the declared list by the caller; an unmatched value is dropped
465
+ * rather than recorded, so a parse miss surfaces as absent. */
466
+ selectedOption: z.string().optional().describe("Which declared option you choose")
412
467
  });
413
468
  var VOTE_JSON_SCHEMA = {
414
469
  type: "object",
@@ -437,6 +492,12 @@ var VOTE_JSON_SCHEMA = {
437
492
  items: { type: "string" },
438
493
  description: "Optional conditions for approval"
439
494
  },
495
+ // #4472: without this, `additionalProperties: false` makes it impossible
496
+ // for a structured-output voter to emit a selection at all.
497
+ selectedOption: {
498
+ type: "string",
499
+ description: "Which declared option you choose (multi-option proposals only)"
500
+ },
440
501
  rejectionCategories: {
441
502
  type: "array",
442
503
  items: {
@@ -544,12 +605,12 @@ Example PR-review request_changes response with structured findings:
544
605
  }
545
606
  ]
546
607
  }`;
547
- function buildVotePrompt(proposal) {
608
+ function buildVotePrompt(proposal, options) {
548
609
  return `Evaluate the following proposal and provide your vote.
549
610
 
550
611
  PROPOSAL:
551
612
  ${proposal}
552
-
613
+ ${buildOptionsBlock(options)}
553
614
  In addition to your role-specific criteria, assess these workflow-test dimensions:
554
615
  - Testability: Can the proposed changes be verified with automated tests?
555
616
  - Workflow integration: Does this fit into existing CI/make/test workflows?
@@ -562,9 +623,21 @@ Respond with a JSON object containing:
562
623
  - conditions: Optional array of conditions for approval
563
624
  - rejectionCategories: Required when rejecting. Array of categories from: YAGNI, DRY_VIOLATION, OVER_ENGINEERING, SCOPE_CREEP, SECURITY_RISK, MISALIGNED, INSUFFICIENT_EVIDENCE
564
625
  - findings: PR-REVIEW MODE ONLY. Optional top-level array of structured findings \u2014 see "PR-review mode" in the system prompt. OMIT this field entirely when reviewing a non-diff proposal or when approving a diff.
565
-
626
+ ${buildSelectionFieldDoc(options)}
566
627
  ${VOTE_PROMPT_EXAMPLES}`;
567
628
  }
629
+ function buildOptionsBlock(options) {
630
+ if (options === void 0 || options.length === 0) return "";
631
+ const list = options.map((o) => `- ${o}`).join("\n");
632
+ return `
633
+ OPTIONS \u2014 choose exactly ONE of these and name it verbatim in \`selectedOption\`:
634
+ ${list}
635
+ `;
636
+ }
637
+ function buildSelectionFieldDoc(options) {
638
+ if (options === void 0 || options.length === 0) return "";
639
+ return "- selectedOption: REQUIRED. Exactly one option from the OPTIONS list above, copied verbatim. Do not invent an option, combine two, or leave it blank \u2014 your selection is what the threshold is measured over.";
640
+ }
568
641
  function stepInString(state, ch) {
569
642
  if (ch === "\\") state.escaped = true;
570
643
  else if (ch === '"') state.inString = false;
@@ -647,7 +720,8 @@ function clampOversizeVoteStrings(parsed) {
647
720
  }
648
721
  return obj;
649
722
  }
650
- function buildParsedVote(data) {
723
+ function buildParsedVote(data, options) {
724
+ const selectedOption = options === void 0 ? void 0 : matchDeclaredOption(data.selectedOption, options);
651
725
  return {
652
726
  decision: data.decision,
653
727
  reasoning: data.reasoning,
@@ -655,16 +729,17 @@ function buildParsedVote(data) {
655
729
  ...data.conditions !== void 0 ? { conditions: data.conditions } : {},
656
730
  ...data.rejectionCategories !== void 0 ? { rejectionCategories: data.rejectionCategories } : {},
657
731
  ...data.findings !== void 0 ? { findings: data.findings } : {},
732
+ ...selectedOption !== void 0 ? { selectedOption } : {},
658
733
  source: "parsed"
659
734
  };
660
735
  }
661
- function parseVoteResponse(output, _role) {
736
+ function parseVoteResponse(output, _role, options) {
662
737
  try {
663
738
  const jsonStr = extractJsonFromResponse(output);
664
739
  const parsed = JSON.parse(jsonStr);
665
740
  const validated = VoteResponseSchema.safeParse(clampOversizeVoteStrings(parsed));
666
741
  if (validated.success) {
667
- return buildParsedVote(validated.data);
742
+ return buildParsedVote(validated.data, options);
668
743
  }
669
744
  const reason = `Validation failed: ${validated.error.issues.map((e) => e.message).join(", ")}`;
670
745
  throw new SyntheticVoteError(reason, output);
@@ -773,11 +848,11 @@ function extractTextFromResponse(content) {
773
848
  }
774
849
  return String(content);
775
850
  }
776
- function buildVoteRequest(role, proposal, timeoutMs, withResponseFormat) {
851
+ function buildVoteRequest(role, proposal, timeoutMs, withResponseFormat, options) {
777
852
  const base = {
778
853
  messages: [
779
854
  { role: "system", content: VOTER_SYSTEM_PROMPTS[role] },
780
- { role: "user", content: buildVotePrompt(proposal) }
855
+ { role: "user", content: buildVotePrompt(proposal, options) }
781
856
  ],
782
857
  // 4000 (#4131): headroom so a findings-bearing verdict (JSON envelope +
783
858
  // reasoning + structured findings) isn't cut mid-JSON by the token cap and
@@ -800,8 +875,15 @@ function isStructuredOutputUnsupported(errorMessage) {
800
875
  function readTokenCount(value) {
801
876
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
802
877
  }
803
- async function runVoteCompletion(role, proposal, adapter, timeoutMs, withResponseFormat) {
804
- const request = buildVoteRequest(role, proposal, timeoutMs, withResponseFormat);
878
+ async function runVoteCompletion({
879
+ role,
880
+ proposal,
881
+ adapter,
882
+ timeoutMs,
883
+ withResponseFormat,
884
+ options
885
+ }) {
886
+ const request = buildVoteRequest(role, proposal, timeoutMs, withResponseFormat, options);
805
887
  const timeoutResult = await withTimeout(
806
888
  adapter.complete(request),
807
889
  timeoutMs,
@@ -821,14 +903,15 @@ async function runVoteCompletion(role, proposal, adapter, timeoutMs, withRespons
821
903
  };
822
904
  return { ok: true, output: extractTextFromResponse(response.value.content), usage };
823
905
  }
824
- async function executeSingleVoteAttempt(role, proposal, adapter, timeoutMs) {
825
- let completion = await runVoteCompletion(role, proposal, adapter, timeoutMs, true);
906
+ async function executeSingleVoteAttempt(role, proposal, adapter, timeoutMs, options) {
907
+ const completionArgs = { role, proposal, adapter, timeoutMs, options };
908
+ let completion = await runVoteCompletion({ ...completionArgs, withResponseFormat: true });
826
909
  if (!completion.ok && isStructuredOutputUnsupported(completion.error)) {
827
- completion = await runVoteCompletion(role, proposal, adapter, timeoutMs, false);
910
+ completion = await runVoteCompletion({ ...completionArgs, withResponseFormat: false });
828
911
  }
829
912
  if (!completion.ok) return { ok: false, error: completion.error };
830
913
  try {
831
- const vote = parseVoteResponse(completion.output, role);
914
+ const vote = parseVoteResponse(completion.output, role, options);
832
915
  return { ok: true, vote, output: completion.output, usage: completion.usage };
833
916
  } catch (error) {
834
917
  if (error instanceof SyntheticVoteError) {
@@ -838,7 +921,7 @@ async function executeSingleVoteAttempt(role, proposal, adapter, timeoutMs) {
838
921
  }
839
922
  }
840
923
  async function executeWithRetries(opts) {
841
- const { role, proposal, adapter, logger: logger10, timeoutMs, maxRetries } = opts;
924
+ const { role, proposal, adapter, logger: logger10, timeoutMs, maxRetries, options } = opts;
842
925
  let lastError = "";
843
926
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
844
927
  if (attempt > 0) {
@@ -849,7 +932,7 @@ async function executeWithRetries(opts) {
849
932
  await delay(delayMs);
850
933
  }
851
934
  const attemptStart = Date.now();
852
- const result = await executeSingleVoteAttempt(role, proposal, adapter, timeoutMs);
935
+ const result = await executeSingleVoteAttempt(role, proposal, adapter, timeoutMs, options);
853
936
  const attemptMs = Date.now() - attemptStart;
854
937
  if (result.ok) {
855
938
  logger10.info("Vote attempt timing", {
@@ -982,17 +1065,27 @@ function buildLlmVoteResult(role, vote, usage, adapter, processingTimeMs) {
982
1065
  source: "llm",
983
1066
  cli: adapter.providerId,
984
1067
  model: adapter.modelId,
1068
+ // #4472: surface the voter's choice at the result level, where the tally
1069
+ // and the record read it. Absent when no options were declared or the
1070
+ // selection matched none of them.
1071
+ ...vote.selectedOption !== void 0 ? { selectedOption: vote.selectedOption } : {},
985
1072
  ...usage.inputTokens !== void 0 ? { inputTokens: usage.inputTokens } : {},
986
1073
  ...usage.outputTokens !== void 0 ? { outputTokens: usage.outputTokens } : {},
987
1074
  ...usage.cachedInputTokens !== void 0 ? { cachedInputTokens: usage.cachedInputTokens } : {},
988
1075
  ...usage.cacheCreationInputTokens !== void 0 ? { cacheCreationInputTokens: usage.cacheCreationInputTokens } : {}
989
1076
  };
990
1077
  }
1078
+ function resolveVoteExecution(options) {
1079
+ return {
1080
+ timeoutMs: options?.timeoutMs ?? resolveVoteTimeout(),
1081
+ maxRetries: options?.maxRetries ?? VOTE_TIMEOUTS.maxRetries,
1082
+ allowSimulation: options?.allowSimulation ?? false,
1083
+ declaredOptions: options?.declaredOptions
1084
+ };
1085
+ }
991
1086
  async function executeAgentVote(role, proposal, adapter, logger10, options) {
992
1087
  const start = getTimeProvider().now();
993
- const timeoutMs = options?.timeoutMs ?? resolveVoteTimeout();
994
- const maxRetries = options?.maxRetries ?? VOTE_TIMEOUTS.maxRetries;
995
- const allowSimulation = options?.allowSimulation ?? false;
1088
+ const { timeoutMs, maxRetries, allowSimulation, declaredOptions } = resolveVoteExecution(options);
996
1089
  logger10.info("Executing vote", { role, model: adapter.modelId, provider: adapter.providerId });
997
1090
  const result = await executeWithRetries({
998
1091
  role,
@@ -1000,7 +1093,8 @@ async function executeAgentVote(role, proposal, adapter, logger10, options) {
1000
1093
  adapter,
1001
1094
  logger: logger10,
1002
1095
  timeoutMs,
1003
- maxRetries
1096
+ maxRetries,
1097
+ options: declaredOptions
1004
1098
  });
1005
1099
  const processingTimeMs = getTimeProvider().now() - start;
1006
1100
  if (result.ok) {
@@ -1191,7 +1285,14 @@ async function collectRealVotes(options) {
1191
1285
  }
1192
1286
  const roleAdapters = options.adapter !== void 0 ? assignUniformAdapter(roles, adapterResult.adapter) : await resolveDiverseAdapters(roles, logger10, adapterResult.adapter, options.gatewayAdapters);
1193
1287
  warnIfCodexConcurrencyExceeded(roleAdapters, logger10);
1194
- const voteOptions = { timeoutMs, maxRetries, allowSimulation: allowSimulation ?? false };
1288
+ const voteOptions = {
1289
+ timeoutMs,
1290
+ maxRetries,
1291
+ allowSimulation: allowSimulation ?? false,
1292
+ // #4472: reaches executeAgentVote, which puts the declared options in the
1293
+ // prompt and resolves each voter's selection against them.
1294
+ declaredOptions: options.declaredOptions
1295
+ };
1195
1296
  const interDelay = options.interAgentDelayMs ?? DEFAULT_INTER_AGENT_DELAY_MS;
1196
1297
  return launchStaggeredVotes({
1197
1298
  roles,
@@ -1204,6 +1305,43 @@ async function collectRealVotes(options) {
1204
1305
  });
1205
1306
  }
1206
1307
 
1308
+ // src/mcp/tools/consensus-vote-option-gate.ts
1309
+ var STRATEGY_TO_OPTION_THRESHOLD = {
1310
+ unanimous: "unanimous",
1311
+ supermajority: "supermajority",
1312
+ simple_majority: "majority",
1313
+ // higher_order and proof_of_learning aggregate differently; they are gated at
1314
+ // the majority bar so a declared-option split still cannot pass unnoticed.
1315
+ higher_order: "majority",
1316
+ proof_of_learning: "majority",
1317
+ opinion_wise: "majority"
1318
+ };
1319
+ function optionThresholdFor(strategy, legacyThreshold) {
1320
+ if (legacyThreshold === "unanimous") return "unanimous";
1321
+ if (legacyThreshold === "supermajority") return "supermajority";
1322
+ return STRATEGY_TO_OPTION_THRESHOLD[strategy] ?? "majority";
1323
+ }
1324
+ function evaluateOptionGate(votes, declaredOptions, threshold, approvedByStrategy) {
1325
+ const tally = tallyOptions(
1326
+ votes.map((v) => ({
1327
+ decision: v.vote.decision,
1328
+ ...v.selectedOption !== void 0 ? { selectedOption: v.selectedOption } : {}
1329
+ })),
1330
+ declaredOptions
1331
+ );
1332
+ const verdict = evaluateOptionThreshold(tally, threshold);
1333
+ if (!approvedByStrategy || verdict.approved) {
1334
+ return { verdict, vetoed: false };
1335
+ }
1336
+ return { verdict, vetoed: true, reason: describeVeto(verdict) };
1337
+ }
1338
+ function describeVeto(verdict) {
1339
+ const pct = (verdict.leadingShare * 100).toFixed(1);
1340
+ const leader = verdict.leadingOption !== void 0 ? `leading option "${verdict.leadingOption}" held ${String(verdict.leadingCount)}/${String(verdict.approverCount)} approvers (${pct}%)` : "no voter selected a declared option";
1341
+ const coverage = verdict.unattributedApprovals > 0 ? ` ${String(verdict.unattributedApprovals)} of ${String(verdict.approverCount)} approvers recorded no usable selection, which lowers the leading share; their choice is unmeasured, not counted as dissent.` : "";
1342
+ return `Approvals cleared the ${verdict.threshold} bar, but the option split did not: ${leader}.${coverage}`;
1343
+ }
1344
+
1207
1345
  // src/consensus/types-voting-protocol.ts
1208
1346
  import { z as z2 } from "zod";
1209
1347
  var VOTING_ROUND_DEFAULT_TIMEOUT_MS = OPERATION_CLASSES["multi-llm-panel"].guardMs;
@@ -4322,7 +4460,10 @@ function getDefaultErrorPolicy(strategy) {
4322
4460
  }
4323
4461
  var ConsensusVoteInputSchema = z6.object({
4324
4462
  proposal: z6.string().min(1).max(MAX_PROPOSAL_LENGTH).describe(
4325
- "Proposal text to vote on. IMPORTANT (#4452): the tally records approve/reject/abstain ONLY. If your proposal asks voters to choose among named options (A/B/C), every voter who engages returns `approve`, so the result records as unanimous even when the panel disagreed about WHICH option \u2014 a 6-1 split persists as 7-0, 100%. Threshold semantics invert too: `unanimous` becomes trivially easy to clear, because everyone approves while choosing different things. Prefer a single yes/no question; if you must offer options, read each voter's `reasoning` to recover the real split and do NOT report the recorded percentage as agreement."
4463
+ "Proposal text to vote on. If the proposal asks voters to choose among named alternatives, you MUST declare them in `options` (#4472). Without `options` the tally records approve/reject/abstain only, so every voter who engages returns `approve` and a 6-1 split on WHICH option persists as 7-0, 100% (#4452)."
4464
+ ),
4465
+ options: z6.array(z6.string().min(1).max(200)).min(2).max(10).optional().describe(
4466
+ "Named alternatives for a multi-option proposal (#4472). When present, the threshold must ALSO be cleared by the leading option, in addition to the ordinary approve/reject bar: `unanimous` requires every approver to have chosen the SAME option, and `supermajority`/`majority` measure the leading option's share of approvers. An approving voter whose selection is absent or matches no declared option stays in the denominator and credits no option, so a degraded response can only lower the leading share, never raise it. Omit for an ordinary yes/no vote \u2014 behaviour is then unchanged."
4326
4467
  ),
4327
4468
  threshold: VoteThresholdSchema.optional().describe(
4328
4469
  "Voting threshold (legacy): majority, supermajority, unanimous. Use strategy instead."
@@ -4500,6 +4641,18 @@ function buildResponse(input, result, costSummary, voteRecord) {
4500
4641
  if (voteRecord !== void 0 && !voteRecord.persisted) {
4501
4642
  response.voteRecordNote = voteRecord.detail;
4502
4643
  }
4644
+ if (result.optionGate !== void 0) {
4645
+ const g = result.optionGate;
4646
+ response.optionOutcome = {
4647
+ tally: g.tally.map((t) => ({ option: t.option, count: t.count })),
4648
+ ...g.leadingOption !== void 0 ? { leadingOption: g.leadingOption } : {},
4649
+ leadingShare: g.leadingShare,
4650
+ approverCount: g.approverCount,
4651
+ selectedCount: g.selectedCount,
4652
+ unattributedApprovals: g.unattributedApprovals,
4653
+ thresholdMet: g.approved
4654
+ };
4655
+ }
4503
4656
  applyOptionalResponseFields(response, input, result, errorCount, costSummary);
4504
4657
  applyAbsoluteQuorumTelemetry(response, input, decision, degradeReason);
4505
4658
  return response;
@@ -4580,6 +4733,14 @@ import { dirname, isAbsolute, relative, resolve, sep } from "path";
4580
4733
  import * as crypto from "crypto";
4581
4734
  import { z as z7 } from "zod";
4582
4735
  var VoteRecordDecisionSchema = z7.enum(["approved", "rejected", "no_quorum"]);
4736
+ var VoteRecordOptionCoverageSchema = z7.object({
4737
+ /** Every approving voter, selecting or not — the tally's denominator. */
4738
+ approverCount: z7.number().int().nonnegative(),
4739
+ /** Approvers whose selection matched a declared option. */
4740
+ selectedCount: z7.number().int().nonnegative(),
4741
+ /** Approvers with no usable selection; their choice is unmeasured. */
4742
+ unattributedApprovals: z7.number().int().nonnegative()
4743
+ }).strict();
4583
4744
  var VoterSummarySchema = z7.object({
4584
4745
  role: z7.string().min(1).max(100),
4585
4746
  decision: z7.enum(["approve", "reject", "abstain"]),
@@ -4605,7 +4766,7 @@ var VoteRecordSchema = z7.object({
4605
4766
  * `ratifies` is folded into the self-hash ONLY when present (see
4606
4767
  * {@link computeVoteRecordHash}).
4607
4768
  */
4608
- version: z7.enum(["1.1", "1.2", "1.3"]),
4769
+ version: z7.enum(["1.1", "1.2", "1.3", "1.4"]),
4609
4770
  /** Unique record id (also usable as a `ratificationVoteRef`). */
4610
4771
  id: z7.string().min(1),
4611
4772
  /**
@@ -4659,6 +4820,16 @@ var VoteRecordSchema = z7.object({
4659
4820
  * choosing different things.
4660
4821
  */
4661
4822
  optionTally: z7.array(VoteRecordOptionCountSchema).min(1).optional(),
4823
+ /**
4824
+ * Selection coverage for a multi-option vote (#4472, schema 1.4).
4825
+ *
4826
+ * `optionTally` alone cannot distinguish dissent from absence: `4 pick X
4827
+ * + 3 unreadable` and a genuine 4/3 split both leave a leading share of
4828
+ * 57%. Recording how many approvers produced no usable selection is what
4829
+ * makes a partial measurement legible AS partial — the condition every
4830
+ * voter attached to adopting the "credit no option" rule.
4831
+ */
4832
+ optionCoverage: VoteRecordOptionCoverageSchema.optional(),
4662
4833
  /**
4663
4834
  * The loop/strategy subject this vote RATIFIES (#3927 item 1). Present only on
4664
4835
  * a ratification vote; set at vote time and bound into the self-hash (so it is
@@ -4708,8 +4879,16 @@ function computeVoteRecordHash(payload) {
4708
4879
  ...base,
4709
4880
  optionTally: payload.optionTally.map((o) => ({ option: o.option, count: o.count }))
4710
4881
  } : base;
4882
+ const withCoverage = payload.optionCoverage !== void 0 ? {
4883
+ ...withTally,
4884
+ optionCoverage: {
4885
+ approverCount: payload.optionCoverage.approverCount,
4886
+ selectedCount: payload.optionCoverage.selectedCount,
4887
+ unattributedApprovals: payload.optionCoverage.unattributedApprovals
4888
+ }
4889
+ } : withTally;
4711
4890
  const canonical = JSON.stringify(
4712
- payload.ratifies !== void 0 ? { ...withTally, ratifies: payload.ratifies } : withTally
4891
+ payload.ratifies !== void 0 ? { ...withCoverage, ratifies: payload.ratifies } : withCoverage
4713
4892
  );
4714
4893
  return crypto.createHash("sha256").update(canonical).digest("hex");
4715
4894
  }
@@ -4741,6 +4920,26 @@ function toVoterSummaries(votes) {
4741
4920
  }
4742
4921
  return summaries;
4743
4922
  }
4923
+ function deriveOptionFields(votes) {
4924
+ const optionTally = tallySelectedOptions(votes);
4925
+ return {
4926
+ optionTally,
4927
+ optionCoverage: optionTally === void 0 ? void 0 : coverageOf(votes)
4928
+ };
4929
+ }
4930
+ function recordVersion(optionTally, optionCoverage) {
4931
+ if (optionCoverage !== void 0) return "1.4";
4932
+ return optionTally !== void 0 ? "1.3" : "1.2";
4933
+ }
4934
+ function coverageOf(votes) {
4935
+ const approvers = votes.filter((v) => v.vote.decision === "approve");
4936
+ const selectedCount = approvers.filter((v) => v.selectedOption !== void 0).length;
4937
+ return {
4938
+ approverCount: approvers.length,
4939
+ selectedCount,
4940
+ unattributedApprovals: approvers.length - selectedCount
4941
+ };
4942
+ }
4744
4943
  function tallySelectedOptions(votes) {
4745
4944
  const counts = /* @__PURE__ */ new Map();
4746
4945
  for (const v of votes) {
@@ -4752,9 +4951,9 @@ function tallySelectedOptions(votes) {
4752
4951
  }
4753
4952
  function buildVoteRecord(input) {
4754
4953
  const proposalTruncated = input.proposal.length > MAX_PROPOSAL_RECORD_CHARS ? input.proposal.slice(0, MAX_PROPOSAL_RECORD_CHARS) + "..." : input.proposal;
4755
- const optionTally = tallySelectedOptions(input.votes);
4954
+ const { optionTally, optionCoverage } = deriveOptionFields(input.votes);
4756
4955
  const payload = {
4757
- version: optionTally !== void 0 ? "1.3" : "1.2",
4956
+ version: recordVersion(optionTally, optionCoverage),
4758
4957
  id: input.id,
4759
4958
  sequence: input.sequence ?? 0,
4760
4959
  recordedAt: input.recordedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
@@ -4773,6 +4972,7 @@ function buildVoteRecord(input) {
4773
4972
  ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {},
4774
4973
  ...input.ratifies !== void 0 ? { ratifies: input.ratifies } : {},
4775
4974
  ...optionTally !== void 0 ? { optionTally } : {},
4975
+ ...optionCoverage !== void 0 ? { optionCoverage } : {},
4776
4976
  ...input.previousHash !== void 0 ? { previousHash: input.previousHash } : {}
4777
4977
  };
4778
4978
  return { ...payload, hash: computeVoteRecordHash(payload) };
@@ -6060,9 +6260,26 @@ async function maybeEscalateContrarian(input, outcome, ctx, logger10, opts) {
6060
6260
  async function executeVoting(input, logger10, opts) {
6061
6261
  const result = await executeVotingInner(input, logger10, opts);
6062
6262
  const errorCount = result.votes.filter((v) => v.source === "error").length;
6263
+ applyOptionGate(input, result);
6063
6264
  result.decision = resolveVoteDecision(input, result, errorCount).decision;
6064
6265
  return result;
6065
6266
  }
6267
+ function applyOptionGate(input, result) {
6268
+ const declaredOptions = input.options;
6269
+ if (declaredOptions === void 0 || declaredOptions.length === 0) return;
6270
+ const outcome = evaluateOptionGate(
6271
+ result.votes,
6272
+ declaredOptions,
6273
+ optionThresholdFor(result.strategy, input.threshold),
6274
+ result.result.outcome === "approved"
6275
+ );
6276
+ result.optionGate = outcome.verdict;
6277
+ if (!outcome.vetoed) return;
6278
+ result.result.outcome = "rejected";
6279
+ if (result.policyReason === void 0 && outcome.reason !== void 0) {
6280
+ result.policyReason = outcome.reason;
6281
+ }
6282
+ }
6066
6283
  async function executeVotingInner(input, logger10, opts) {
6067
6284
  const strategy = resolveStrategy(input);
6068
6285
  const algorithm = strategyToAlgorithm(strategy);
@@ -6080,7 +6297,8 @@ async function executeVotingInner(input, logger10, opts) {
6080
6297
  proposal: input.proposal,
6081
6298
  simulate: input.simulateVotes,
6082
6299
  ...opts?.voteTimeoutMs !== void 0 && { timeoutMs: opts.voteTimeoutMs },
6083
- ...opts?.gatewayAdapters !== void 0 && { gatewayAdapters: opts.gatewayAdapters }
6300
+ ...opts?.gatewayAdapters !== void 0 && { gatewayAdapters: opts.gatewayAdapters },
6301
+ declaredOptions: input.options
6084
6302
  });
6085
6303
  const policyDecision = applyErrorPolicy(votes, errorPolicy);
6086
6304
  if (policyDecision.shortCircuit) {
@@ -6379,7 +6597,19 @@ var CONSENSUS_VOTE_OUTPUT_SCHEMA = {
6379
6597
  // #3855: per-decision cost rollup. Same omission as panelWarning (#4032) — it
6380
6598
  // is present on the response whenever cost recording succeeds (common in `api`
6381
6599
  // billing mode). Shared schema lives with the type (single source of truth).
6382
- costSummary: DecisionCostSummarySchema.optional()
6600
+ costSummary: DecisionCostSummarySchema.optional(),
6601
+ // #4472: declared-option outcome. Present whenever the proposal declared
6602
+ // `options`. `unattributedApprovals` is part of the contract, not telemetry:
6603
+ // without it a caller cannot tell a real split from unreadable responses.
6604
+ optionOutcome: z10.object({
6605
+ tally: z10.array(z10.object({ option: z10.string(), count: z10.number().int().nonnegative() })),
6606
+ leadingOption: z10.string().optional(),
6607
+ leadingShare: z10.number().min(0).max(1),
6608
+ approverCount: z10.number().int().nonnegative(),
6609
+ selectedCount: z10.number().int().nonnegative(),
6610
+ unattributedApprovals: z10.number().int().nonnegative(),
6611
+ thresholdMet: z10.boolean()
6612
+ }).optional()
6383
6613
  };
6384
6614
  var CONSENSUS_VOTE_TOOL_SCHEMA = {
6385
6615
  proposal: z10.string().min(1).max(MAX_PROPOSAL_LENGTH).describe("Proposal text to vote on"),
@@ -6503,4 +6733,4 @@ export {
6503
6733
  CONSENSUS_VOTE_OUTPUT_SCHEMA,
6504
6734
  registerConsensusVoteTool
6505
6735
  };
6506
- //# sourceMappingURL=chunk-RAYKASM4.js.map
6736
+ //# sourceMappingURL=chunk-XL6T3HTN.js.map