opencode-swarm 6.86.6 → 6.86.7

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/dist/cli/index.js CHANGED
@@ -18580,7 +18580,7 @@ import * as path32 from "path";
18580
18580
  // package.json
18581
18581
  var package_default = {
18582
18582
  name: "opencode-swarm",
18583
- version: "6.86.6",
18583
+ version: "6.86.7",
18584
18584
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
18585
18585
  main: "dist/index.js",
18586
18586
  types: "dist/index.d.ts",
@@ -18742,7 +18742,7 @@ var TOOL_NAMES = [
18742
18742
  "evidence_check",
18743
18743
  "check_gate_status",
18744
18744
  "completion_verify",
18745
- "convene_council",
18745
+ "submit_council_verdicts",
18746
18746
  "declare_council_criteria",
18747
18747
  "sbom_generate",
18748
18748
  "checkpoint",
@@ -18822,7 +18822,7 @@ var AGENT_TOOL_MAP = {
18822
18822
  "check_gate_status",
18823
18823
  "completion_verify",
18824
18824
  "complexity_hotspots",
18825
- "convene_council",
18825
+ "submit_council_verdicts",
18826
18826
  "declare_council_criteria",
18827
18827
  "detect_domains",
18828
18828
  "evidence_check",
@@ -19624,7 +19624,8 @@ var CouncilConfigSchema = exports_external.object({
19624
19624
  maxRounds: exports_external.number().int().min(1).max(10).default(3),
19625
19625
  parallelTimeoutMs: exports_external.number().int().min(5000).max(120000).default(30000),
19626
19626
  vetoPriority: exports_external.boolean().default(true),
19627
- requireAllMembers: exports_external.boolean().default(false).describe("When true, convene_council rejects if fewer than 5 member verdicts are provided."),
19627
+ requireAllMembers: exports_external.boolean().default(false).describe("When true, submit_council_verdicts rejects if fewer than 5 member verdicts are provided. Equivalent to minimumMembers: 5."),
19628
+ minimumMembers: exports_external.number().int().min(1).max(5).default(3).describe("Minimum distinct council member verdicts required for synthesis. Default 3. Set to 1 to disable quorum enforcement. requireAllMembers: true overrides this to 5 (stricter constraint wins)."),
19628
19629
  escalateOnMaxRounds: exports_external.string().optional().describe("Optional webhook URL or handler name invoked when maxRounds is reached without APPROVE. Declared for forward compatibility; no behavior is implemented yet."),
19629
19630
  general: GeneralCouncilConfigSchema.optional()
19630
19631
  }).strict();
@@ -576,6 +576,7 @@ export declare const CouncilConfigSchema: z.ZodObject<{
576
576
  parallelTimeoutMs: z.ZodDefault<z.ZodNumber>;
577
577
  vetoPriority: z.ZodDefault<z.ZodBoolean>;
578
578
  requireAllMembers: z.ZodDefault<z.ZodBoolean>;
579
+ minimumMembers: z.ZodDefault<z.ZodNumber>;
579
580
  escalateOnMaxRounds: z.ZodOptional<z.ZodString>;
580
581
  general: z.ZodOptional<z.ZodObject<{
581
582
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -996,6 +997,7 @@ export declare const PluginConfigSchema: z.ZodObject<{
996
997
  parallelTimeoutMs: z.ZodDefault<z.ZodNumber>;
997
998
  vetoPriority: z.ZodDefault<z.ZodBoolean>;
998
999
  requireAllMembers: z.ZodDefault<z.ZodBoolean>;
1000
+ minimumMembers: z.ZodDefault<z.ZodNumber>;
999
1001
  escalateOnMaxRounds: z.ZodOptional<z.ZodString>;
1000
1002
  general: z.ZodOptional<z.ZodObject<{
1001
1003
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -48,6 +48,8 @@ export interface CouncilSynthesis {
48
48
  /** 1-indexed */
49
49
  roundNumber: number;
50
50
  allCriteriaMet: boolean;
51
+ /** Distinct council members that produced verdicts (deduplicated count). */
52
+ quorumSize: number;
51
53
  /** true when called with an empty verdicts array — the APPROVE is vacuous */
52
54
  emptyVerdictsWarning?: boolean;
53
55
  }
@@ -71,8 +73,10 @@ export interface CouncilConfig {
71
73
  parallelTimeoutMs: number;
72
74
  /** Default true — any REJECT blocks */
73
75
  vetoPriority: boolean;
74
- /** Default false — when true, convene_council rejects unless all 5 member verdicts are provided */
76
+ /** Default false — when true, submit_council_verdicts rejects unless all 5 member verdicts are provided */
75
77
  requireAllMembers: boolean;
78
+ /** Default 3 — minimum distinct council members required for quorum. requireAllMembers: true overrides this to 5. */
79
+ minimumMembers: number;
76
80
  /**
77
81
  * Optional webhook URL or handler name for auto-escalation when maxRounds is
78
82
  * reached without APPROVE. Reserved for forward compatibility — NOT yet
@@ -85,7 +85,7 @@ export declare function computeProfileHash(profile: QaGateProfile): string;
85
85
  * machine; blocks coder→next-coder advancement until reviewer + test_engineer
86
86
  * delegations observed).
87
87
  * - council_mode — src/state.ts isCouncilGateActive + src/hooks/delegation-gate.ts
88
- * (Stage B replaced by convene_council verdict).
88
+ * (Stage B replaced by submit_council_verdicts verdict).
89
89
  * - sme_enabled — consumed during MODE: BRAINSTORM/SPECIFY architect dialogue.
90
90
  * - critic_pre_plan — consumed by MODE: PLAN critic delegation before save_plan.
91
91
  * - sast_enabled — consumed inside pre_check_batch tool.
package/dist/index.js CHANGED
@@ -50,7 +50,7 @@ var init_tool_names = __esm(() => {
50
50
  "evidence_check",
51
51
  "check_gate_status",
52
52
  "completion_verify",
53
- "convene_council",
53
+ "submit_council_verdicts",
54
54
  "declare_council_criteria",
55
55
  "sbom_generate",
56
56
  "checkpoint",
@@ -196,7 +196,7 @@ var init_constants = __esm(() => {
196
196
  "check_gate_status",
197
197
  "completion_verify",
198
198
  "complexity_hotspots",
199
- "convene_council",
199
+ "submit_council_verdicts",
200
200
  "declare_council_criteria",
201
201
  "detect_domains",
202
202
  "evidence_check",
@@ -453,7 +453,7 @@ var init_constants = __esm(() => {
453
453
  co_change_analyzer: "detect hidden couplings by analyzing git history",
454
454
  check_gate_status: "check the gate status of a specific task",
455
455
  completion_verify: "verify completed tasks have required evidence",
456
- convene_council: "convene the Work Complete Council \u2014 parallel veto-aware verification gate across critic, reviewer, sme, test_engineer, and explorer verdicts",
456
+ submit_council_verdicts: "submit pre-collected council member verdicts for synthesis (architect MUST dispatch critic/reviewer/sme/test_engineer/explorer as Agent tasks first; this tool synthesizes only, it does not contact members)",
457
457
  declare_council_criteria: "pre-declare acceptance criteria for a task before the coder starts work; criteria are read back during council evaluation",
458
458
  detect_domains: "detect which SME domains are relevant for a given text",
459
459
  extract_code_blocks: "extract code blocks from text content and save them to files",
@@ -15258,7 +15258,8 @@ var init_schema = __esm(() => {
15258
15258
  maxRounds: exports_external.number().int().min(1).max(10).default(3),
15259
15259
  parallelTimeoutMs: exports_external.number().int().min(5000).max(120000).default(30000),
15260
15260
  vetoPriority: exports_external.boolean().default(true),
15261
- requireAllMembers: exports_external.boolean().default(false).describe("When true, convene_council rejects if fewer than 5 member verdicts are provided."),
15261
+ requireAllMembers: exports_external.boolean().default(false).describe("When true, submit_council_verdicts rejects if fewer than 5 member verdicts are provided. Equivalent to minimumMembers: 5."),
15262
+ minimumMembers: exports_external.number().int().min(1).max(5).default(3).describe("Minimum distinct council member verdicts required for synthesis. Default 3. Set to 1 to disable quorum enforcement. requireAllMembers: true overrides this to 5 (stricter constraint wins)."),
15262
15263
  escalateOnMaxRounds: exports_external.string().optional().describe("Optional webhook URL or handler name invoked when maxRounds is reached without APPROVE. Declared for forward compatibility; no behavior is implemented yet."),
15263
15264
  general: GeneralCouncilConfigSchema.optional()
15264
15265
  }).strict();
@@ -25058,7 +25059,7 @@ function createDelegationGateHook(config2, directory) {
25058
25059
  return;
25059
25060
  const normalized = normalizeToolName(input.tool);
25060
25061
  const councilActive = await isCouncilGateActive(directory, config2.council);
25061
- if (normalized === "convene_council") {
25062
+ if (normalized === "submit_council_verdicts") {
25062
25063
  try {
25063
25064
  const parsed = typeof _output === "string" ? JSON.parse(_output) : _output;
25064
25065
  const result = parsed;
@@ -25072,19 +25073,20 @@ function createDelegationGateHook(config2, directory) {
25072
25073
  session.taskCouncilApproved = new Map;
25073
25074
  session.taskCouncilApproved.set(taskId, {
25074
25075
  verdict: result.overallVerdict,
25075
- roundNumber: typeof result.roundNumber === "number" ? result.roundNumber : 1
25076
+ roundNumber: typeof result.roundNumber === "number" ? result.roundNumber : 1,
25077
+ quorumSize: typeof result.quorumSize === "number" ? result.quorumSize : 1
25076
25078
  });
25077
25079
  if (councilActive && result.overallVerdict === "APPROVE" && result.allCriteriaMet === true && (result.requiredFixesCount ?? 0) === 0) {
25078
25080
  try {
25079
- await advanceTaskStateAndPersist(session, taskId, "complete", directory);
25081
+ await advanceTaskStateAndPersist(session, taskId, "complete", directory, config2.council);
25080
25082
  } catch (err2) {
25081
- console.warn(`[delegation-gate] toolAfter convene_council: could not advance ${taskId} \u2192 complete: ${err2 instanceof Error ? err2.message : String(err2)}`);
25083
+ console.warn(`[delegation-gate] toolAfter submit_council_verdicts: could not advance ${taskId} \u2192 complete: ${err2 instanceof Error ? err2.message : String(err2)}`);
25082
25084
  }
25083
25085
  }
25084
25086
  }
25085
25087
  }
25086
25088
  } catch (err2) {
25087
- console.warn(`[delegation-gate] toolAfter convene_council: failed to parse output: ${err2 instanceof Error ? err2.message : String(err2)}`);
25089
+ console.warn(`[delegation-gate] toolAfter submit_council_verdicts: failed to parse output: ${err2 instanceof Error ? err2.message : String(err2)}`);
25088
25090
  }
25089
25091
  return;
25090
25092
  }
@@ -25988,7 +25990,7 @@ function isValidTaskId2(taskId) {
25988
25990
  const trimmed = taskId.trim();
25989
25991
  return trimmed.length > 0;
25990
25992
  }
25991
- function advanceTaskState(session, taskId, newState) {
25993
+ function advanceTaskState(session, taskId, newState, councilConfig) {
25992
25994
  if (!isValidTaskId2(taskId)) {
25993
25995
  return;
25994
25996
  }
@@ -26011,7 +26013,8 @@ function advanceTaskState(session, taskId, newState) {
26011
26013
  }
26012
26014
  if (newState === "complete" && current !== "tests_run") {
26013
26015
  const councilEntry = session.taskCouncilApproved?.get(taskId);
26014
- const councilApproved = councilEntry?.verdict === "APPROVE";
26016
+ const effectiveMinimum = councilConfig?.requireAllMembers ? 5 : councilConfig?.minimumMembers ?? 3;
26017
+ const councilApproved = councilEntry?.verdict === "APPROVE" && (councilEntry.quorumSize ?? 0) >= effectiveMinimum;
26015
26018
  const pastPreCheck = currentIndex >= STATE_ORDER.indexOf("pre_check_passed");
26016
26019
  if (!councilApproved || !pastPreCheck) {
26017
26020
  throw new Error(`INVALID_TASK_STATE_TRANSITION: ${taskId} cannot reach complete from ${current} \u2014 must pass through tests_run first (or have council APPROVE after pre_check)`);
@@ -26020,8 +26023,8 @@ function advanceTaskState(session, taskId, newState) {
26020
26023
  session.taskWorkflowStates.set(taskId, newState);
26021
26024
  telemetry.taskStateChanged(session.agentName, taskId, newState, current);
26022
26025
  }
26023
- async function advanceTaskStateAndPersist(session, taskId, newState, directory) {
26024
- advanceTaskState(session, taskId, newState);
26026
+ async function advanceTaskStateAndPersist(session, taskId, newState, directory, councilConfig) {
26027
+ advanceTaskState(session, taskId, newState, councilConfig);
26025
26028
  if (newState !== "coder_delegated" && newState !== "complete") {
26026
26029
  return;
26027
26030
  }
@@ -26244,9 +26247,12 @@ function applyRehydrationCache(session) {
26244
26247
  if (typeof roundNumber !== "number" || !Number.isFinite(roundNumber)) {
26245
26248
  roundNumber = 1;
26246
26249
  }
26250
+ const rawQuorumSize = council.quorumSize;
26251
+ const quorumSize = typeof rawQuorumSize === "number" && Number.isFinite(rawQuorumSize) && rawQuorumSize >= 1 ? rawQuorumSize : 1;
26247
26252
  session.taskCouncilApproved.set(taskId, {
26248
26253
  verdict,
26249
- roundNumber
26254
+ roundNumber,
26255
+ quorumSize
26250
26256
  });
26251
26257
  }
26252
26258
  }
@@ -43807,7 +43813,7 @@ var package_default;
43807
43813
  var init_package = __esm(() => {
43808
43814
  package_default = {
43809
43815
  name: "opencode-swarm",
43810
- version: "6.86.6",
43816
+ version: "6.86.7",
43811
43817
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
43812
43818
  main: "dist/index.js",
43813
43819
  types: "dist/index.d.ts",
@@ -54348,10 +54354,15 @@ var init_registry = __esm(() => {
54348
54354
  function buildCouncilWorkflow(council) {
54349
54355
  if (council?.enabled !== true)
54350
54356
  return "";
54351
- return `## Work Complete Council (when enabled)
54357
+ return `## COUNCIL WORKFLOW (submit_council_verdicts)
54352
54358
 
54353
- When \`council.enabled\` is true, every task goes through a four-phase verification
54354
- gate before advancing to \`complete\`. When council is authoritative, this REPLACES Stage B (reviewer + test_engineer as standalone delegations). Stage A (precheckbatch) still runs as the pre-review gate; Phase 1 dispatch of reviewer and test_engineer is the sole review pass for this task.
54359
+ CRITICAL: \`submit_council_verdicts\` does NOT run council members.
54360
+ It synthesizes verdicts that you must collect BEFORE calling it.
54361
+
54362
+ When \`council.enabled\` is true, every task goes through this verification
54363
+ gate before advancing to \`complete\`. The council REPLACES Stage B
54364
+ (reviewer + test_engineer as standalone delegations). Stage A
54365
+ (\`pre_check_batch\`) still runs as the pre-review gate.
54355
54366
 
54356
54367
  ### Phase 0 \u2014 Pre-declare criteria (at plan time, BEFORE dispatching the coder)
54357
54368
  Call \`declare_council_criteria\` for each task with at least 3 concrete,
@@ -54360,9 +54371,13 @@ testable acceptance criteria. Mark functional/correctness criteria
54360
54371
  follow the pattern \`C1\`, \`C2\`, etc. The criteria are persisted to
54361
54372
  \`.swarm/council/{taskId}.json\` and read back automatically at synthesis time.
54362
54373
 
54363
- ### Phase 1 \u2014 Parallel dispatch (when the coder signals the task is complete)
54364
- Dispatch all FIVE council members IN PARALLEL \u2014 do not run them sequentially.
54365
- Each receives ONLY their role-relevant context, not the full conversation:
54374
+ ### MANDATORY SEQUENCE \u2014 never skip or reorder
54375
+
54376
+ #### STEP 1 \u2014 DISPATCH all council members as parallel Agent tasks
54377
+ Dispatch \`critic\`, \`reviewer\`, \`sme\`, \`test_engineer\`, and \`explorer\`
54378
+ (or at minimum \`council.minimumMembers\` distinct members; default 3) in a
54379
+ SINGLE message using parallel Agent tool calls. Provide each member with
54380
+ their role-specific scope:
54366
54381
  - \`critic\` \u2014 original task spec + acceptance criteria + code diff + test results + approved-plan baseline comparison (via \`get_approved_plan\`) and spec-intent drift analysis against the approved baseline
54367
54382
  - \`reviewer\` \u2014 semantic diff summary + blast radius (files importing changed files) + style guide
54368
54383
  - \`sme\` \u2014 task domain context + relevant knowledge base entries
@@ -54371,31 +54386,65 @@ Each receives ONLY their role-relevant context, not the full conversation:
54371
54386
  (explorer hunts for lazy implementations, hallucinated APIs,
54372
54387
  cargo-cult patterns, spec drift, lazy abstractions)
54373
54388
 
54374
- Each member must return a \`CouncilMemberVerdict\` with all fields populated:
54375
- \`agent\`, \`verdict\` (APPROVE|CONCERNS|REJECT), \`confidence\` (0.0\u20131.0),
54376
- \`findings[]\`, \`criteriaAssessed[]\`, \`criteriaUnmet[]\`, \`durationMs\`.
54389
+ Wait for ALL dispatched agents to return their verdict objects.
54390
+
54391
+ #### STEP 2 \u2014 COLLECT verdicts
54392
+ Read each agent's response and extract their \`CouncilMemberVerdict\` object.
54393
+ Each member must return all fields: \`agent\`, \`verdict\` (APPROVE|CONCERNS|REJECT),
54394
+ \`confidence\` (0.0\u20131.0), \`findings[]\`, \`criteriaAssessed[]\`, \`criteriaUnmet[]\`,
54395
+ \`durationMs\`.
54396
+
54397
+ Do NOT fabricate, infer, or substitute a verdict. If an agent did not return
54398
+ a valid verdict, re-dispatch that agent.
54399
+
54400
+ #### STEP 3 \u2014 CALL submit_council_verdicts
54401
+ ONLY after collecting real verdicts from real agent dispatches, call
54402
+ \`submit_council_verdicts\` with the collected verdicts array, the task id,
54403
+ swarm id, and current round number (1-indexed).
54404
+
54405
+ #### STEP 4 \u2014 READ the response
54406
+ Inspect \`membersAbsent\` in the response. If \`membersAbsent\` is non-empty,
54407
+ the council is incomplete \u2014 dispatch the missing members and re-collect.
54408
+ Inspect \`overallVerdict\`. APPROVE is valid only when \`membersAbsent\` is
54409
+ empty (or fewer members than \`council.minimumMembers\` are absent).
54410
+
54411
+ The response also includes: \`vetoedBy\`, \`unifiedFeedbackMd\`,
54412
+ \`requiredFixesCount\`, \`advisoryFindingsCount\`, \`allCriteriaMet\`,
54413
+ \`quorumSize\`, \`quorumMet\`.
54377
54414
 
54378
- ### Phase 2 \u2014 Synthesize
54379
- Call \`convene_council\` with all 5 verdicts, the task id, swarm id, and the
54380
- current round number (1-indexed). The tool returns:
54381
- \`overallVerdict\`, \`vetoedBy\`, \`unifiedFeedbackMd\`, \`requiredFixesCount\`,
54382
- \`allCriteriaMet\`.
54415
+ If \`success: false\` and \`reason: 'insufficient_quorum'\`, the response
54416
+ includes \`membersVoted\`, \`membersAbsent\`, and \`quorumRequired\` \u2014 dispatch
54417
+ the absent members and re-call the tool.
54383
54418
 
54384
- ### Phase 3 \u2014 Act on the result
54419
+ #### STEP 5 \u2014 ACT on the verdict
54385
54420
  - **APPROVE**: Advance task to complete via \`update_task_status\`. If
54386
- advisoryFindingsCount > 0, deliver \`unifiedFeedbackMd\` as a
54387
- single non-blocking note. Otherwise, advance silently.
54388
- - **CONCERNS**: Send \`unifiedFeedbackMd\` to the coder as ONE coherent document.
54389
- Do NOT enumerate individual member verdicts. Increment
54390
- roundNumber on the next council call. CONCERNS does not block
54391
- advancement at the update_task_status level \u2014 decide per
54392
- severity whether to advance or retry.
54393
- - **REJECT**: Block advancement. Send \`unifiedFeedbackMd\` to the coder with
54394
- the BLOCKING flag. The coder must resolve all \`requiredFixes\`
54395
- before re-submitting. Maximum \`council.maxRounds\` rounds
54396
- (default 3). If roundNumber >= maxRounds and verdict is still
54397
- REJECT, surface \`unifiedFeedbackMd\` to the user and HALT \u2014
54398
- do NOT auto-advance.
54421
+ \`advisoryFindingsCount > 0\`, deliver \`unifiedFeedbackMd\` as
54422
+ a single non-blocking note. Otherwise, advance silently.
54423
+ - **CONCERNS**: Send \`unifiedFeedbackMd\` to the coder as ONE coherent
54424
+ document. Do NOT enumerate individual member verdicts.
54425
+ Increment \`roundNumber\` on the next council call. CONCERNS
54426
+ does not block advancement at the update_task_status level \u2014
54427
+ decide per severity whether to advance or retry.
54428
+ - **REJECT**: Block advancement. Send \`unifiedFeedbackMd\` to the coder
54429
+ with the BLOCKING flag. The coder must resolve all
54430
+ \`requiredFixes\` before re-submitting. Maximum
54431
+ \`council.maxRounds\` rounds (default 3). If
54432
+ \`roundNumber >= maxRounds\` and verdict is still REJECT,
54433
+ surface \`unifiedFeedbackMd\` to the user and HALT \u2014 do NOT
54434
+ auto-advance.
54435
+
54436
+ ### ANTI-PATTERNS \u2014 any of these are council bypass violations
54437
+ - \u2717 Calling \`submit_council_verdicts\` without first dispatching council members.
54438
+ - \u2717 Passing a verdict you inferred or fabricated rather than received from a dispatched agent.
54439
+ - \u2717 Claiming "Council APPROVED" when \`membersAbsent\` is non-empty.
54440
+ - \u2717 Treating a prior round's APPROVE as valid for a new task or new round.
54441
+ - \u2717 Incrementing \`roundNumber\` without re-dispatching all members for the new round.
54442
+
54443
+ ### ROUND 2 DELIBERATION
54444
+ If round 1 produces REJECT or CONCERNS, dispatch only the disputing members
54445
+ for round 2 focused on the specific disagreement areas. Round 2 must produce
54446
+ NEW agent responses \u2014 do NOT reuse round 1 verdicts with a higher
54447
+ \`roundNumber\`.
54399
54448
 
54400
54449
  ### Retry protocol
54401
54450
  On re-submission after REJECT or CONCERNS, the council reads the same
@@ -54412,7 +54461,7 @@ function buildYourToolsList(council) {
54412
54461
  const qaCouncilEnabled = council?.enabled === true;
54413
54462
  const generalCouncilEnabled = council?.general?.enabled === true;
54414
54463
  const filtered = sorted.filter((t) => {
54415
- if (!qaCouncilEnabled && (t === "convene_council" || t === "declare_council_criteria")) {
54464
+ if (!qaCouncilEnabled && (t === "submit_council_verdicts" || t === "declare_council_criteria")) {
54416
54465
  return false;
54417
54466
  }
54418
54467
  if (!generalCouncilEnabled && t === "convene_general_council") {
@@ -54445,7 +54494,7 @@ function buildAvailableToolsList(council) {
54445
54494
  const qaCouncilEnabled = council?.enabled === true;
54446
54495
  const generalCouncilEnabled = council?.general?.enabled === true;
54447
54496
  const filtered = sorted.filter((t) => {
54448
- if (!qaCouncilEnabled && (t === "convene_council" || t === "declare_council_criteria")) {
54497
+ if (!qaCouncilEnabled && (t === "submit_council_verdicts" || t === "declare_council_criteria")) {
54449
54498
  return false;
54450
54499
  }
54451
54500
  if (!generalCouncilEnabled && t === "convene_general_council") {
@@ -58105,14 +58154,20 @@ function getAgentConfigs(config3, directory, sessionId) {
58105
58154
  allowedTools = AGENT_TOOL_MAP[baseAgentName];
58106
58155
  }
58107
58156
  if (baseAgentName === "architect" && config3?.council?.enabled === true && override !== undefined) {
58108
- const required3 = ["declare_council_criteria", "convene_council"];
58157
+ const required3 = [
58158
+ "declare_council_criteria",
58159
+ "submit_council_verdicts"
58160
+ ];
58109
58161
  const missing = required3.filter((t) => !override.includes(t));
58110
58162
  if (missing.length > 0) {
58111
58163
  throw new Error(`[opencode-swarm] Conflicting config: council.enabled=true but tool_filter.overrides.architect omits ${missing.join(", ")}. ` + `Either set council.enabled=false, remove the architect override entirely to fall back on AGENT_TOOL_MAP, or add the missing council tools to the override. ` + `Refusing to silently override your explicit tool_filter.overrides.architect.`);
58112
58164
  }
58113
58165
  }
58114
58166
  if (baseAgentName === "architect" && config3?.council?.enabled !== true && override !== undefined) {
58115
- const councilTools = ["declare_council_criteria", "convene_council"];
58167
+ const councilTools = [
58168
+ "declare_council_criteria",
58169
+ "submit_council_verdicts"
58170
+ ];
58116
58171
  const present = councilTools.filter((t) => override.includes(t));
58117
58172
  if (present.length > 0 && !quiet) {
58118
58173
  console.warn(`[opencode-swarm] tool_filter.overrides.architect includes ${present.join(", ")} but council.enabled is not true. ` + `The runtime gate will reject these calls. Either set council.enabled=true, or remove ${present.join(", ")} from the architect override.`);
@@ -74438,7 +74493,8 @@ function writeCouncilEvidence(workingDir, synthesis) {
74438
74493
  verdict: synthesis.overallVerdict,
74439
74494
  vetoedBy: synthesis.vetoedBy,
74440
74495
  roundNumber: synthesis.roundNumber,
74441
- allCriteriaMet: synthesis.allCriteriaMet
74496
+ allCriteriaMet: synthesis.allCriteriaMet,
74497
+ quorumSize: synthesis.quorumSize
74442
74498
  };
74443
74499
  const updated = Object.create(null);
74444
74500
  safeAssignOwnProps(updated, existingRoot);
@@ -74470,13 +74526,15 @@ var COUNCIL_DEFAULTS = {
74470
74526
  maxRounds: 3,
74471
74527
  parallelTimeoutMs: 30000,
74472
74528
  vetoPriority: true,
74473
- requireAllMembers: false
74529
+ requireAllMembers: false,
74530
+ minimumMembers: 3
74474
74531
  };
74475
74532
 
74476
74533
  // src/council/council-service.ts
74477
74534
  function synthesizeCouncilVerdicts(taskId, swarmId, verdicts, criteria, roundNumber, config3 = {}) {
74478
74535
  const cfg = { ...COUNCIL_DEFAULTS, ...config3 };
74479
74536
  const timestamp = new Date().toISOString();
74537
+ const quorumSize = new Set(verdicts.map((v) => v.agent)).size;
74480
74538
  const rejectingMembers = verdicts.filter((v) => v.verdict === "REJECT").map((v) => v.agent);
74481
74539
  let overallVerdict;
74482
74540
  if (cfg.vetoPriority && rejectingMembers.length > 0) {
@@ -74512,6 +74570,7 @@ function synthesizeCouncilVerdicts(taskId, swarmId, verdicts, criteria, roundNum
74512
74570
  unifiedFeedbackMd,
74513
74571
  roundNumber,
74514
74572
  allCriteriaMet,
74573
+ quorumSize,
74515
74574
  ...verdicts.length === 0 && { emptyVerdictsWarning: true }
74516
74575
  };
74517
74576
  }
@@ -74644,8 +74703,8 @@ var ArgsSchema = exports_external.object({
74644
74703
  verdicts: exports_external.array(VerdictSchema).min(1).max(5),
74645
74704
  working_directory: exports_external.string().optional()
74646
74705
  });
74647
- var convene_council = createSwarmTool({
74648
- description: "Convene the Work Complete Council. Accepts parallel verdicts from critic, " + "reviewer, sme, test_engineer, and explorer (anti-slop specialist). Returns " + "a synthesized assessment with a veto-aware overall verdict, required fixes, " + "and a single unified feedback document. Architect-only. Config-gated via " + "council.enabled.",
74706
+ var submit_council_verdicts = createSwarmTool({
74707
+ description: "Submit pre-collected council member verdicts for synthesis. PREREQUISITE \u2014 " + "you MUST dispatch each council member (critic, reviewer, sme, test_engineer, " + "explorer) as separate Agent tasks and collect their verdict responses BEFORE " + "calling this tool. This tool performs synthesis only \u2014 it does NOT dispatch, " + "invoke, or contact council members. Calling this tool without first " + "collecting real verdicts from dispatched agents constitutes a council bypass. " + "Returns the synthesized verdict, required fixes, and a quorum report showing " + "which members voted and which were absent. Architect-only. Config-gated via " + "council.enabled.",
74649
74708
  args: {
74650
74709
  taskId: exports_external.string().min(1).regex(/^\d+\.\d+(\.\d+)*$/, "Task ID must be in N.M or N.M.P format").describe('Task ID being evaluated, e.g. "1.1", "1.2.3"'),
74651
74710
  swarmId: exports_external.string().min(1).describe('Swarm identifier, e.g. "mega"'),
@@ -74698,10 +74757,25 @@ var convene_council = createSwarmTool({
74698
74757
  reason: "council feature is disabled \u2014 set council.enabled: true in .opencode/opencode-swarm.json to enable"
74699
74758
  }, null, 2);
74700
74759
  }
74701
- if (config3.council?.requireAllMembers && input.verdicts.length < 5) {
74760
+ const effectiveMinimum = config3.council?.requireAllMembers ? 5 : config3.council?.minimumMembers ?? 3;
74761
+ const ALL_MEMBERS = [
74762
+ "critic",
74763
+ "reviewer",
74764
+ "sme",
74765
+ "test_engineer",
74766
+ "explorer"
74767
+ ];
74768
+ const distinctMembers = new Set(input.verdicts.map((v) => v.agent));
74769
+ const membersVoted = [...distinctMembers];
74770
+ const membersAbsent = ALL_MEMBERS.filter((m) => !distinctMembers.has(m));
74771
+ if (membersVoted.length < effectiveMinimum) {
74702
74772
  return JSON.stringify({
74703
74773
  success: false,
74704
- reason: `council.requireAllMembers is true but only ${input.verdicts.length} of 5 member verdicts were provided`
74774
+ reason: "insufficient_quorum",
74775
+ message: `Council quorum not met: ${membersVoted.length} of ${effectiveMinimum} required members provided verdicts. ` + `Members voted: [${membersVoted.join(", ")}]. ` + `Members absent: [${membersAbsent.join(", ")}]. ` + `Dispatch the absent council members as Agent tasks and collect their verdicts before calling submit_council_verdicts.`,
74776
+ membersVoted,
74777
+ membersAbsent,
74778
+ quorumRequired: effectiveMinimum
74705
74779
  }, null, 2);
74706
74780
  }
74707
74781
  const criteria = readCriteria(workingDir, input.taskId);
@@ -74726,6 +74800,10 @@ var convene_council = createSwarmTool({
74726
74800
  requiredFixesCount: synthesis.requiredFixes.length,
74727
74801
  advisoryFindingsCount: synthesis.advisoryFindings.length,
74728
74802
  unresolvedConflictsCount: synthesis.unresolvedConflicts.length,
74803
+ membersVoted,
74804
+ membersAbsent,
74805
+ quorumSize: membersVoted.length,
74806
+ quorumMet: true,
74729
74807
  unifiedFeedbackMd: synthesis.unifiedFeedbackMd
74730
74808
  }, null, 2);
74731
74809
  }
@@ -87457,9 +87535,11 @@ function recoverTaskStateFromDelegations(taskId) {
87457
87535
  }
87458
87536
  function checkCouncilGate(workingDirectory, taskId) {
87459
87537
  let councilEnabled = false;
87538
+ let effectiveMinimum = 3;
87460
87539
  try {
87461
87540
  const config3 = loadPluginConfig(workingDirectory);
87462
87541
  councilEnabled = config3.council?.enabled === true;
87542
+ effectiveMinimum = config3.council?.requireAllMembers ? 5 : config3.council?.minimumMembers ?? 3;
87463
87543
  } catch {
87464
87544
  return { blocked: false, reason: "" };
87465
87545
  }
@@ -87486,20 +87566,28 @@ function checkCouncilGate(workingDirectory, taskId) {
87486
87566
  } catch {
87487
87567
  return {
87488
87568
  blocked: true,
87489
- reason: "council gate required but not yet run \u2014 architect must call convene_council before advancing this task"
87569
+ reason: "council gate required but not yet run \u2014 architect must call submit_council_verdicts before advancing this task"
87490
87570
  };
87491
87571
  }
87492
87572
  const councilGate = evidence?.gates?.council;
87493
87573
  if (!councilGate) {
87494
87574
  return {
87495
87575
  blocked: true,
87496
- reason: "council gate required but not yet run \u2014 architect must call convene_council before advancing this task"
87576
+ reason: "council gate required but not yet run \u2014 architect must call submit_council_verdicts before advancing this task"
87497
87577
  };
87498
87578
  }
87499
87579
  if (councilGate.verdict === "REJECT") {
87500
87580
  return {
87501
87581
  blocked: true,
87502
- reason: "council gate blocked advancement \u2014 resolve requiredFixes and re-run convene_council"
87582
+ reason: "council gate blocked advancement \u2014 resolve requiredFixes and re-run submit_council_verdicts"
87583
+ };
87584
+ }
87585
+ const rawQuorumSize = councilGate.quorumSize;
87586
+ const quorumSize = typeof rawQuorumSize === "number" && Number.isFinite(rawQuorumSize) && rawQuorumSize >= 1 ? rawQuorumSize : 1;
87587
+ if (quorumSize < effectiveMinimum) {
87588
+ return {
87589
+ blocked: true,
87590
+ reason: `council gate blocked advancement \u2014 recorded verdict has insufficient quorum (${quorumSize} of ${effectiveMinimum} required members). Re-run submit_council_verdicts with the missing council members.`
87503
87591
  };
87504
87592
  }
87505
87593
  return { blocked: false, reason: "" };
@@ -88717,7 +88805,7 @@ var OpenCodeSwarm = async (ctx) => {
88717
88805
  checkpoint,
88718
88806
  completion_verify,
88719
88807
  complexity_hotspots,
88720
- convene_council,
88808
+ submit_council_verdicts,
88721
88809
  convene_general_council,
88722
88810
  curator_analyze,
88723
88811
  declare_council_criteria,
package/dist/state.d.ts CHANGED
@@ -112,10 +112,17 @@ export interface AgentSessionState {
112
112
  * Only populated when parallelization.stageB.parallel.enabled = true.
113
113
  */
114
114
  stageBCompletion?: Map<string, Set<'reviewer' | 'test_engineer'>>;
115
- /** v6.71+ Council mode: per-task council verdict, recorded by delegation-gate when convene_council resolves. */
115
+ /** v6.71+ Council mode: per-task council verdict, recorded by delegation-gate when submit_council_verdicts resolves. */
116
116
  taskCouncilApproved?: Map<string, {
117
117
  verdict: 'APPROVE' | 'REJECT' | 'CONCERNS';
118
118
  roundNumber: number;
119
+ /**
120
+ * Distinct council members that voted on this verdict.
121
+ * Validated by the council fast-path against `council.minimumMembers`
122
+ * (default 3). Old evidence files without this field rehydrate as
123
+ * quorumSize: 1 — conservative; forces a fresh council run.
124
+ */
125
+ quorumSize: number;
119
126
  }>;
120
127
  /** Last gate outcome for deliberation preamble injection */
121
128
  lastGateOutcome: {
@@ -359,7 +366,10 @@ export declare function recordPhaseAgentDispatch(sessionId: string, agentName: s
359
366
  * @param taskId - The task identifier
360
367
  * @param newState - The requested new state
361
368
  */
362
- export declare function advanceTaskState(session: AgentSessionState, taskId: string, newState: TaskWorkflowState): void;
369
+ export declare function advanceTaskState(session: AgentSessionState, taskId: string, newState: TaskWorkflowState, councilConfig?: {
370
+ minimumMembers?: number;
371
+ requireAllMembers?: boolean;
372
+ }): void;
363
373
  /**
364
374
  * Advance the per-task workflow state machine AND persist the corresponding
365
375
  * plan.json status at meaningful workflow boundaries.
@@ -377,7 +387,10 @@ export declare function advanceTaskState(session: AgentSessionState, taskId: str
377
387
  * not break the in-memory state machine — matches the existing defensive
378
388
  * pattern around advanceTaskState call sites.
379
389
  */
380
- export declare function advanceTaskStateAndPersist(session: AgentSessionState, taskId: string, newState: TaskWorkflowState, directory: string): Promise<void>;
390
+ export declare function advanceTaskStateAndPersist(session: AgentSessionState, taskId: string, newState: TaskWorkflowState, directory: string, councilConfig?: {
391
+ minimumMembers?: number;
392
+ requireAllMembers?: boolean;
393
+ }): Promise<void>;
381
394
  /**
382
395
  * Get the current workflow state for a task.
383
396
  * Returns 'idle' if no entry exists.
@@ -1,9 +1,14 @@
1
1
  /**
2
- * Work Complete Council — architect-only tool.
2
+ * Submit Council Verdicts — architect-only tool.
3
3
  *
4
- * Accepts parallel verdicts from critic, reviewer, sme, and test_engineer,
5
- * then synthesizes them into a veto-aware overall verdict with required fixes
6
- * and a single unified feedback document.
4
+ * Accepts pre-collected parallel verdicts from critic, reviewer, sme,
5
+ * test_engineer, and explorer, then synthesizes them into a veto-aware
6
+ * overall verdict with required fixes and a single unified feedback document.
7
+ *
8
+ * PREREQUISITE: The architect must dispatch each council member as a separate
9
+ * Agent task and collect the resulting CouncilMemberVerdict objects BEFORE
10
+ * calling this tool. This tool performs synthesis only — it does NOT dispatch,
11
+ * invoke, or contact council members.
7
12
  *
8
13
  * Config-gated (council.enabled must be true) and architect-only via
9
14
  * AGENT_TOOL_MAP. Follows the check-gate-status.ts pattern.
@@ -45,4 +50,4 @@ export declare const ArgsSchema: z.ZodObject<{
45
50
  }, z.core.$strip>>;
46
51
  working_directory: z.ZodOptional<z.ZodString>;
47
52
  }, z.core.$strip>;
48
- export declare const convene_council: ReturnType<typeof tool>;
53
+ export declare const submit_council_verdicts: ReturnType<typeof tool>;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Lets the architect declare acceptance criteria at plan time, before the
5
5
  * coder starts work. Criteria are persisted to .swarm/council/{safeId}.json
6
- * and later read back during council evaluation (convene_council) so that
6
+ * and later read back during council evaluation (submit_council_verdicts) so that
7
7
  * reviewers assess a stable, pre-committed contract rather than whatever
8
8
  * criteria happen to be invented at review time.
9
9
  *
@@ -5,7 +5,7 @@ export { checkpoint } from './checkpoint';
5
5
  export { co_change_analyzer } from './co-change-analyzer';
6
6
  export { completion_verify } from './completion-verify';
7
7
  export { complexity_hotspots } from './complexity-hotspots';
8
- export { convene_council } from './convene-council';
8
+ export { submit_council_verdicts } from './convene-council';
9
9
  export { convene_general_council } from './convene-general-council';
10
10
  export { curator_analyze } from './curator-analyze';
11
11
  export { declare_council_criteria } from './declare-council-criteria';
@@ -3,7 +3,7 @@
3
3
  * Used for constants and agent setup references.
4
4
  */
5
5
  /** Union type of all valid tool names */
6
- export type ToolName = 'diff' | 'diff_summary' | 'syntax_check' | 'placeholder_scan' | 'imports' | 'lint' | 'secretscan' | 'sast_scan' | 'build_check' | 'pre_check_batch' | 'quality_budget' | 'symbols' | 'complexity_hotspots' | 'schema_drift' | 'todo_extract' | 'evidence_check' | 'check_gate_status' | 'completion_verify' | 'convene_council' | 'declare_council_criteria' | 'sbom_generate' | 'checkpoint' | 'pkg_audit' | 'test_runner' | 'test_impact' | 'mutation_test' | 'generate_mutants' | 'detect_domains' | 'gitingest' | 'retrieve_summary' | 'extract_code_blocks' | 'phase_complete' | 'save_plan' | 'update_task_status' | 'lint_spec' | 'write_retro' | 'write_drift_evidence' | 'write_hallucination_evidence' | 'write_mutation_evidence' | 'declare_scope' | 'knowledge_query' | 'doc_scan' | 'doc_extract' | 'curator_analyze' | 'knowledge_add' | 'knowledge_recall' | 'knowledge_remove' | 'co_change_analyzer' | 'search' | 'batch_symbols' | 'suggest_patch' | 'req_coverage' | 'get_approved_plan' | 'repo_map' | 'get_qa_gate_profile' | 'set_qa_gates' | 'web_search' | 'convene_general_council';
6
+ export type ToolName = 'diff' | 'diff_summary' | 'syntax_check' | 'placeholder_scan' | 'imports' | 'lint' | 'secretscan' | 'sast_scan' | 'build_check' | 'pre_check_batch' | 'quality_budget' | 'symbols' | 'complexity_hotspots' | 'schema_drift' | 'todo_extract' | 'evidence_check' | 'check_gate_status' | 'completion_verify' | 'submit_council_verdicts' | 'declare_council_criteria' | 'sbom_generate' | 'checkpoint' | 'pkg_audit' | 'test_runner' | 'test_impact' | 'mutation_test' | 'generate_mutants' | 'detect_domains' | 'gitingest' | 'retrieve_summary' | 'extract_code_blocks' | 'phase_complete' | 'save_plan' | 'update_task_status' | 'lint_spec' | 'write_retro' | 'write_drift_evidence' | 'write_hallucination_evidence' | 'write_mutation_evidence' | 'declare_scope' | 'knowledge_query' | 'doc_scan' | 'doc_extract' | 'curator_analyze' | 'knowledge_add' | 'knowledge_recall' | 'knowledge_remove' | 'co_change_analyzer' | 'search' | 'batch_symbols' | 'suggest_patch' | 'req_coverage' | 'get_approved_plan' | 'repo_map' | 'get_qa_gate_profile' | 'set_qa_gates' | 'web_search' | 'convene_general_council';
7
7
  /** Readonly array of all tool names */
8
8
  export declare const TOOL_NAMES: readonly ToolName[];
9
9
  /** Set for O(1) tool name validation */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "6.86.6",
3
+ "version": "6.86.7",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",