opencode-swarm 7.60.0 → 7.61.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.
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ var package_default;
69
69
  var init_package = __esm(() => {
70
70
  package_default = {
71
71
  name: "opencode-swarm",
72
- version: "7.60.0",
72
+ version: "7.61.0",
73
73
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
74
74
  main: "dist/index.js",
75
75
  types: "dist/index.d.ts",
@@ -577,11 +577,11 @@ var init_tool_metadata = __esm(() => {
577
577
  ]
578
578
  },
579
579
  get_qa_gate_profile: {
580
- description: "retrieve the QA gate profile for the current plan: gates (reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test, council_general_review, drift_check, final_council), lock state, and profile hash. Read-only.",
580
+ description: "retrieve the QA gate profile for the current plan: gates (reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test, phase_council, drift_check, final_council), lock state, and profile hash. Read-only.",
581
581
  agents: ["architect"]
582
582
  },
583
583
  set_qa_gates: {
584
- description: "configure the QA gate profile for the current plan. Architect-only. Ratchet-tighter only — rejected once the profile is locked after critic approval. Supports: reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test, council_general_review, drift_check, final_council.",
584
+ description: "configure the QA gate profile for the current plan. Architect-only. Ratchet-tighter only — rejected once the profile is locked after critic approval. Supports: reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test, phase_council, drift_check, final_council.",
585
585
  agents: ["architect"]
586
586
  },
587
587
  web_search: {
@@ -15906,7 +15906,7 @@ var init_schema = __esm(() => {
15906
15906
  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."),
15907
15907
  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)."),
15908
15908
  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."),
15909
- phaseConcernsAllowComplete: exports_external.boolean().default(true).describe("When true, a phase-level council CONCERNS verdict does NOT block phase completion — the advisory notes are logged as warnings and the phase proceeds. When false, CONCERNS blocks like REJECT. Default: true (CONCERNS is advisory)."),
15909
+ phaseConcernsAllowComplete: exports_external.boolean().default(true).describe("When true, a phase-level council CONCERNS verdict with only MEDIUM/LOW findings does NOT block phase completion — the advisory notes are logged as warnings and the phase proceeds. When false, CONCERNS blocks like REJECT. Note: HIGH/CRITICAL findings from CONCERNS members are always promoted to requiredFixes and block at the tool level regardless of this setting. Default: true."),
15910
15910
  general: GeneralCouncilConfigSchema.optional()
15911
15911
  }).strict();
15912
15912
  ParallelizationConfigSchema = exports_external.object({
@@ -21783,11 +21783,26 @@ import { createHash as createHash3 } from "node:crypto";
21783
21783
  function rowToProfile(row) {
21784
21784
  let parsed = {};
21785
21785
  try {
21786
- parsed = JSON.parse(row.gates);
21786
+ const maybeGates = JSON.parse(row.gates);
21787
+ if (maybeGates && typeof maybeGates === "object" && !Array.isArray(maybeGates)) {
21788
+ parsed = maybeGates;
21789
+ }
21787
21790
  } catch {
21788
21791
  parsed = {};
21789
21792
  }
21790
- const gates = { ...DEFAULT_QA_GATES, ...parsed };
21793
+ const raw = parsed;
21794
+ if (raw.council_mode === true && raw.phase_council === undefined) {
21795
+ parsed.phase_council = true;
21796
+ parsed.council_mode = false;
21797
+ }
21798
+ const knownKeys = new Set(Object.keys(DEFAULT_QA_GATES));
21799
+ const filteredParsed = {};
21800
+ for (const key of Object.keys(parsed)) {
21801
+ if (knownKeys.has(key)) {
21802
+ filteredParsed[key] = parsed[key];
21803
+ }
21804
+ }
21805
+ const gates = { ...DEFAULT_QA_GATES, ...filteredParsed };
21791
21806
  return {
21792
21807
  id: row.id,
21793
21808
  plan_id: row.plan_id,
@@ -21910,7 +21925,7 @@ var init_qa_gate_profile = __esm(() => {
21910
21925
  hallucination_guard: false,
21911
21926
  sast_enabled: true,
21912
21927
  mutation_test: false,
21913
- council_general_review: false,
21928
+ phase_council: false,
21914
21929
  drift_check: true,
21915
21930
  final_council: false
21916
21931
  };
@@ -41939,7 +41954,8 @@ function createDelegationGateHook(config2, directory) {
41939
41954
  if (!session)
41940
41955
  return;
41941
41956
  const normalized = normalizeToolName(input.tool);
41942
- const councilActive = await isCouncilGateActive(directory, config2.council);
41957
+ const { qaGateSessionOverrides } = ensureAgentSession(input.sessionID);
41958
+ const councilActive = await isCouncilGateActive(directory, config2.council, qaGateSessionOverrides ?? {});
41943
41959
  if (normalized === "update_task_status") {
41944
41960
  const directArgs = input.args;
41945
41961
  const storedArgs = getStoredInputArgs(input.callID);
@@ -42049,86 +42065,88 @@ function createDelegationGateHook(config2, directory) {
42049
42065
  hasReviewer = true;
42050
42066
  if (targetAgent === "test_engineer")
42051
42067
  hasTestEngineer = true;
42052
- const stageBParallelEnabled = true;
42053
- if (stageBParallelEnabled) {
42054
- if ((targetAgent === "reviewer" || targetAgent === "test_engineer") && session.taskWorkflowStates) {
42055
- const stageBEligibleStates = [
42056
- "coder_delegated",
42057
- "pre_check_passed",
42058
- "reviewer_run"
42059
- ];
42060
- for (const [taskId, state] of session.taskWorkflowStates) {
42061
- if (!stageBEligibleStates.includes(state))
42062
- continue;
42063
- const eligibleState = state;
42064
- recordStageBCompletion(session, taskId, targetAgent);
42065
- if (hasBothStageBCompletions(session, taskId)) {
42066
- try {
42067
- if (eligibleState === "coder_delegated" || eligibleState === "pre_check_passed") {
42068
- advanceTaskState(session, taskId, "reviewer_run", {
42069
- telemetrySessionId: input.sessionID
42070
- });
42071
- }
42072
- advanceTaskState(session, taskId, "tests_run", {
42073
- telemetrySessionId: input.sessionID
42074
- });
42075
- } catch (err2) {
42076
- warn(`[delegation-gate] toolAfter stage-b-parallel: could not advance ${taskId} (${eligibleState}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
42077
- }
42078
- } else {
42079
- try {
42080
- if (targetAgent === "reviewer" && (eligibleState === "coder_delegated" || eligibleState === "pre_check_passed")) {
42081
- advanceTaskState(session, taskId, "reviewer_run", {
42082
- telemetrySessionId: input.sessionID
42083
- });
42084
- } else if (targetAgent === "test_engineer" && eligibleState === "reviewer_run") {
42085
- advanceTaskState(session, taskId, "tests_run", {
42086
- telemetrySessionId: input.sessionID
42087
- });
42088
- }
42089
- } catch (err2) {
42090
- warn(`[delegation-gate] toolAfter stage-b-parallel intermediate: could not advance ${taskId} (${eligibleState}) after ${targetAgent}: ${err2 instanceof Error ? err2.message : String(err2)}`);
42091
- }
42092
- }
42093
- }
42094
- const seedTaskId = getSeedTaskId(session);
42095
- if (seedTaskId) {
42096
- for (const [, otherSession] of swarmState.agentSessions) {
42097
- if (otherSession === session)
42098
- continue;
42099
- if (!otherSession.taskWorkflowStates)
42100
- continue;
42101
- if (!otherSession.taskWorkflowStates.has(seedTaskId)) {
42102
- otherSession.taskWorkflowStates.set(seedTaskId, "coder_delegated");
42103
- }
42104
- const seedState = otherSession.taskWorkflowStates.get(seedTaskId);
42105
- if (!seedState || !stageBEligibleStates.includes(seedState)) {
42068
+ if (!councilActive) {
42069
+ const stageBParallelEnabled = true;
42070
+ if (stageBParallelEnabled) {
42071
+ if ((targetAgent === "reviewer" || targetAgent === "test_engineer") && session.taskWorkflowStates) {
42072
+ const stageBEligibleStates = [
42073
+ "coder_delegated",
42074
+ "pre_check_passed",
42075
+ "reviewer_run"
42076
+ ];
42077
+ for (const [taskId, state] of session.taskWorkflowStates) {
42078
+ if (!stageBEligibleStates.includes(state))
42106
42079
  continue;
42107
- }
42108
- const seedEligibleState = seedState;
42109
- recordStageBCompletion(otherSession, seedTaskId, targetAgent);
42110
- if (hasBothStageBCompletions(otherSession, seedTaskId)) {
42080
+ const eligibleState = state;
42081
+ recordStageBCompletion(session, taskId, targetAgent);
42082
+ if (hasBothStageBCompletions(session, taskId)) {
42111
42083
  try {
42112
- if (seedEligibleState === "coder_delegated" || seedEligibleState === "pre_check_passed") {
42113
- advanceTaskState(otherSession, seedTaskId, "reviewer_run", { emitTelemetry: false });
42084
+ if (eligibleState === "coder_delegated" || eligibleState === "pre_check_passed") {
42085
+ advanceTaskState(session, taskId, "reviewer_run", {
42086
+ telemetrySessionId: input.sessionID
42087
+ });
42114
42088
  }
42115
- advanceTaskState(otherSession, seedTaskId, "tests_run", {
42116
- emitTelemetry: false
42089
+ advanceTaskState(session, taskId, "tests_run", {
42090
+ telemetrySessionId: input.sessionID
42117
42091
  });
42118
42092
  } catch (err2) {
42119
- warn(`[delegation-gate] toolAfter cross-session stage-b-parallel: could not advance ${seedTaskId} (${seedEligibleState}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
42093
+ warn(`[delegation-gate] toolAfter stage-b-parallel: could not advance ${taskId} (${eligibleState}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
42120
42094
  }
42121
42095
  } else {
42122
42096
  try {
42123
- if (targetAgent === "reviewer" && (seedEligibleState === "coder_delegated" || seedEligibleState === "pre_check_passed")) {
42124
- advanceTaskState(otherSession, seedTaskId, "reviewer_run", { emitTelemetry: false });
42125
- } else if (targetAgent === "test_engineer" && seedEligibleState === "reviewer_run") {
42097
+ if (targetAgent === "reviewer" && (eligibleState === "coder_delegated" || eligibleState === "pre_check_passed")) {
42098
+ advanceTaskState(session, taskId, "reviewer_run", {
42099
+ telemetrySessionId: input.sessionID
42100
+ });
42101
+ } else if (targetAgent === "test_engineer" && eligibleState === "reviewer_run") {
42102
+ advanceTaskState(session, taskId, "tests_run", {
42103
+ telemetrySessionId: input.sessionID
42104
+ });
42105
+ }
42106
+ } catch (err2) {
42107
+ warn(`[delegation-gate] toolAfter stage-b-parallel intermediate: could not advance ${taskId} (${eligibleState}) after ${targetAgent}: ${err2 instanceof Error ? err2.message : String(err2)}`);
42108
+ }
42109
+ }
42110
+ }
42111
+ const seedTaskId = getSeedTaskId(session);
42112
+ if (seedTaskId) {
42113
+ for (const [, otherSession] of swarmState.agentSessions) {
42114
+ if (otherSession === session)
42115
+ continue;
42116
+ if (!otherSession.taskWorkflowStates)
42117
+ continue;
42118
+ if (!otherSession.taskWorkflowStates.has(seedTaskId)) {
42119
+ otherSession.taskWorkflowStates.set(seedTaskId, "coder_delegated");
42120
+ }
42121
+ const seedState = otherSession.taskWorkflowStates.get(seedTaskId);
42122
+ if (!seedState || !stageBEligibleStates.includes(seedState)) {
42123
+ continue;
42124
+ }
42125
+ const seedEligibleState = seedState;
42126
+ recordStageBCompletion(otherSession, seedTaskId, targetAgent);
42127
+ if (hasBothStageBCompletions(otherSession, seedTaskId)) {
42128
+ try {
42129
+ if (seedEligibleState === "coder_delegated" || seedEligibleState === "pre_check_passed") {
42130
+ advanceTaskState(otherSession, seedTaskId, "reviewer_run", { emitTelemetry: false });
42131
+ }
42126
42132
  advanceTaskState(otherSession, seedTaskId, "tests_run", {
42127
42133
  emitTelemetry: false
42128
42134
  });
42135
+ } catch (err2) {
42136
+ warn(`[delegation-gate] toolAfter cross-session stage-b-parallel: could not advance ${seedTaskId} (${seedEligibleState}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
42137
+ }
42138
+ } else {
42139
+ try {
42140
+ if (targetAgent === "reviewer" && (seedEligibleState === "coder_delegated" || seedEligibleState === "pre_check_passed")) {
42141
+ advanceTaskState(otherSession, seedTaskId, "reviewer_run", { emitTelemetry: false });
42142
+ } else if (targetAgent === "test_engineer" && seedEligibleState === "reviewer_run") {
42143
+ advanceTaskState(otherSession, seedTaskId, "tests_run", {
42144
+ emitTelemetry: false
42145
+ });
42146
+ }
42147
+ } catch (err2) {
42148
+ warn(`[delegation-gate] toolAfter cross-session stage-b-parallel intermediate: could not advance ${seedTaskId} (${seedEligibleState}) after ${targetAgent}: ${err2 instanceof Error ? err2.message : String(err2)}`);
42129
42149
  }
42130
- } catch (err2) {
42131
- warn(`[delegation-gate] toolAfter cross-session stage-b-parallel intermediate: could not advance ${seedTaskId} (${seedEligibleState}) after ${targetAgent}: ${err2 instanceof Error ? err2.message : String(err2)}`);
42132
42150
  }
42133
42151
  }
42134
42152
  }
@@ -42167,7 +42185,7 @@ function createDelegationGateHook(config2, directory) {
42167
42185
  if (storedArgs !== undefined) {
42168
42186
  deleteStoredInputArgs(input.callID);
42169
42187
  }
42170
- if (!subagentType || !hasReviewer) {
42188
+ if (!subagentType || !hasReviewer || councilActive) {
42171
42189
  const delegationChain = swarmState.delegationChains.get(input.sessionID);
42172
42190
  if (delegationChain && delegationChain.length > 0) {
42173
42191
  let lastCoderIndex = -1;
@@ -42191,69 +42209,71 @@ function createDelegationGateHook(config2, directory) {
42191
42209
  session.qaSkipCount = 0;
42192
42210
  session.qaSkipTaskIds = [];
42193
42211
  }
42194
- if (lastCoderIndex !== -1 && hasReviewer && session.taskWorkflowStates) {
42195
- for (const [taskId, state] of session.taskWorkflowStates) {
42196
- if (state === "coder_delegated" || state === "pre_check_passed") {
42197
- try {
42198
- advanceTaskState(session, taskId, "reviewer_run");
42199
- } catch (err2) {
42200
- warn(`[delegation-gate] fallback: could not advance ${taskId} (${state}) → reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
42201
- }
42202
- }
42203
- }
42204
- }
42205
- if (lastCoderIndex !== -1 && hasReviewer && hasTestEngineer && session.taskWorkflowStates) {
42206
- for (const [taskId, state] of session.taskWorkflowStates) {
42207
- if (state === "reviewer_run") {
42208
- try {
42209
- advanceTaskState(session, taskId, "tests_run");
42210
- } catch (err2) {
42211
- warn(`[delegation-gate] fallback: could not advance ${taskId} (${state}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
42212
- }
42213
- }
42214
- }
42215
- }
42216
- if (lastCoderIndex !== -1 && hasReviewer) {
42217
- for (const [, otherSession] of swarmState.agentSessions) {
42218
- if (otherSession === session)
42219
- continue;
42220
- if (!otherSession.taskWorkflowStates)
42221
- continue;
42222
- const seedTaskId = getSeedTaskId(session);
42223
- if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
42224
- otherSession.taskWorkflowStates.set(seedTaskId, "coder_delegated");
42225
- }
42226
- for (const [taskId, state] of otherSession.taskWorkflowStates) {
42212
+ if (!councilActive) {
42213
+ if (lastCoderIndex !== -1 && hasReviewer && session.taskWorkflowStates) {
42214
+ for (const [taskId, state] of session.taskWorkflowStates) {
42227
42215
  if (state === "coder_delegated" || state === "pre_check_passed") {
42228
42216
  try {
42229
- advanceTaskState(otherSession, taskId, "reviewer_run", {
42230
- emitTelemetry: false
42231
- });
42217
+ advanceTaskState(session, taskId, "reviewer_run");
42232
42218
  } catch (err2) {
42233
- warn(`[delegation-gate] fallback cross-session: could not advance ${taskId} (${state}) → reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
42219
+ warn(`[delegation-gate] fallback: could not advance ${taskId} (${state}) → reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
42234
42220
  }
42235
42221
  }
42236
42222
  }
42237
42223
  }
42238
- }
42239
- if (lastCoderIndex !== -1 && hasReviewer && hasTestEngineer) {
42240
- for (const [, otherSession] of swarmState.agentSessions) {
42241
- if (otherSession === session)
42242
- continue;
42243
- if (!otherSession.taskWorkflowStates)
42244
- continue;
42245
- const seedTaskId = getSeedTaskId(session);
42246
- if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
42247
- otherSession.taskWorkflowStates.set(seedTaskId, "reviewer_run");
42248
- }
42249
- for (const [taskId, state] of otherSession.taskWorkflowStates) {
42224
+ if (lastCoderIndex !== -1 && hasReviewer && hasTestEngineer && session.taskWorkflowStates) {
42225
+ for (const [taskId, state] of session.taskWorkflowStates) {
42250
42226
  if (state === "reviewer_run") {
42251
42227
  try {
42252
- advanceTaskState(otherSession, taskId, "tests_run", {
42253
- emitTelemetry: false
42254
- });
42228
+ advanceTaskState(session, taskId, "tests_run");
42255
42229
  } catch (err2) {
42256
- warn(`[delegation-gate] fallback cross-session: could not advance ${taskId} (${state}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
42230
+ warn(`[delegation-gate] fallback: could not advance ${taskId} (${state}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
42231
+ }
42232
+ }
42233
+ }
42234
+ }
42235
+ if (lastCoderIndex !== -1 && hasReviewer) {
42236
+ for (const [, otherSession] of swarmState.agentSessions) {
42237
+ if (otherSession === session)
42238
+ continue;
42239
+ if (!otherSession.taskWorkflowStates)
42240
+ continue;
42241
+ const seedTaskId = getSeedTaskId(session);
42242
+ if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
42243
+ otherSession.taskWorkflowStates.set(seedTaskId, "coder_delegated");
42244
+ }
42245
+ for (const [taskId, state] of otherSession.taskWorkflowStates) {
42246
+ if (state === "coder_delegated" || state === "pre_check_passed") {
42247
+ try {
42248
+ advanceTaskState(otherSession, taskId, "reviewer_run", {
42249
+ emitTelemetry: false
42250
+ });
42251
+ } catch (err2) {
42252
+ warn(`[delegation-gate] fallback cross-session: could not advance ${taskId} (${state}) → reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
42253
+ }
42254
+ }
42255
+ }
42256
+ }
42257
+ }
42258
+ if (lastCoderIndex !== -1 && hasReviewer && hasTestEngineer) {
42259
+ for (const [, otherSession] of swarmState.agentSessions) {
42260
+ if (otherSession === session)
42261
+ continue;
42262
+ if (!otherSession.taskWorkflowStates)
42263
+ continue;
42264
+ const seedTaskId = getSeedTaskId(session);
42265
+ if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
42266
+ otherSession.taskWorkflowStates.set(seedTaskId, "reviewer_run");
42267
+ }
42268
+ for (const [taskId, state] of otherSession.taskWorkflowStates) {
42269
+ if (state === "reviewer_run") {
42270
+ try {
42271
+ advanceTaskState(otherSession, taskId, "tests_run", {
42272
+ emitTelemetry: false
42273
+ });
42274
+ } catch (err2) {
42275
+ warn(`[delegation-gate] fallback cross-session: could not advance ${taskId} (${state}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
42276
+ }
42257
42277
  }
42258
42278
  }
42259
42279
  }
@@ -43077,7 +43097,7 @@ function hasBothStageBCompletions(session, taskId) {
43077
43097
  return false;
43078
43098
  return completions.has("reviewer") && completions.has("test_engineer");
43079
43099
  }
43080
- async function isCouncilGateActive(directory, council) {
43100
+ async function isCouncilGateActive(directory, council, sessionOverrides = {}) {
43081
43101
  const enabled = council?.enabled === true;
43082
43102
  let plan = null;
43083
43103
  try {
@@ -43103,13 +43123,13 @@ async function isCouncilGateActive(directory, council) {
43103
43123
  if (!profile) {
43104
43124
  return false;
43105
43125
  }
43106
- const councilMode = profile.gates.council_mode === true;
43126
+ const councilMode = getEffectiveGates(profile, sessionOverrides).council_mode === true;
43107
43127
  if (enabled && councilMode) {
43108
43128
  return true;
43109
43129
  }
43110
43130
  if (enabled !== councilMode && !_councilDisagreementWarned.has(planId)) {
43111
43131
  _councilDisagreementWarned.add(planId);
43112
- warn(`[delegation-gate] Council mode mismatch for plan ${planId}: ` + `pluginConfig.council.enabled=${enabled}, QaGates.council_mode=${councilMode}. ` + "Falling back to Stage B (non-council) advancement.");
43132
+ warn(`[delegation-gate] Council mode mismatch for plan ${planId}: ` + `pluginConfig.council.enabled=${enabled}, QaGates.council_mode=${councilMode}. ` + "Falling back to Stage B (non-council) per-task advancement.");
43113
43133
  }
43114
43134
  return false;
43115
43135
  }
@@ -80755,7 +80775,7 @@ var init_qa_gates = __esm(() => {
80755
80775
  "hallucination_guard",
80756
80776
  "sast_enabled",
80757
80777
  "mutation_test",
80758
- "council_general_review",
80778
+ "phase_council",
80759
80779
  "drift_check",
80760
80780
  "final_council"
80761
80781
  ];
@@ -84128,7 +84148,7 @@ Subcommands:
84128
84148
  handler: (ctx) => handleQaGatesCommand(ctx.directory, ctx.args, ctx.sessionID),
84129
84149
  description: "View or modify QA gate profile for the current plan [enable|override <gate>...]",
84130
84150
  args: "[show|enable|override] <gate>...",
84131
- details: "show: display spec-level, session-override, and effective QA gates for the current plan. enable: persist gate(s) into the locked-once profile (architect; rejected after critic approval lock). override: session-only ratchet-tighter enable. Valid gates: reviewer, test_engineer, council_mode, sme_enabled, critic_pre_plan, hallucination_guard, sast_enabled, mutation_test, council_general_review, drift_check.",
84151
+ details: "show: display spec-level, session-override, and effective QA gates for the current plan. enable: persist gate(s) into the locked-once profile (architect; rejected after critic approval lock). override: session-only ratchet-tighter enable. Valid gates: reviewer, test_engineer, council_mode, sme_enabled, critic_pre_plan, hallucination_guard, sast_enabled, mutation_test, phase_council, drift_check, final_council.",
84132
84152
  category: "config"
84133
84153
  },
84134
84154
  promote: {
@@ -84371,24 +84391,87 @@ Do NOT dispatch the supervisor yourself as a reviewer of code — it is summary-
84371
84391
  function buildCouncilWorkflow(council) {
84372
84392
  if (council?.enabled !== true)
84373
84393
  return "";
84374
- return `## COUNCIL WORKFLOW (submit_phase_council_verdicts)
84394
+ return `## COUNCIL WORKFLOW
84395
+
84396
+ ANTI-CONFUSION: Do NOT confuse the three council modes:
84397
+ (1) \`council_mode\` — per-task full council replacing Stage B.
84398
+ (2) \`phase_council\` — phase-level holistic review at \`phase_complete\`.
84399
+ (3) \`final_council\` — project-level final review after all phases.
84400
+ None of these use the General Council (3-agent advisory). The General Council is an early workflow option gated by \`council.general.enabled\`, not a QA gate.
84401
+
84402
+ ## A. PER-TASK COUNCIL (when \`council_mode\` is ON)
84403
+
84404
+ When \`council_mode\` is enabled in the QA gate profile, Stage B (reviewer + test_engineer) is **replaced** by the full 5-member council (critic, reviewer, sme, test_engineer, explorer) per task. Stage A (\`pre_check_batch\`) still runs as the pre-review gate.
84405
+
84406
+ ### PREREQUISITES
84407
+ - \`declare_council_criteria\` must be called for each task before council dispatch.
84408
+
84409
+ ### MANDATORY SEQUENCE — never skip or reorder
84410
+
84411
+ #### STEP 1 — DISPATCH all 5 council members in parallel (task-scoped)
84412
+ After Stage A passes for a task, in a SINGLE message, dispatch \`critic\`, \`reviewer\`, \`sme\`, \`test_engineer\`, and \`explorer\` as parallel Agent tasks. Each member receives task-scoped context:
84413
+ - \`critic\` — task diff + task spec + approved-plan baseline (via \`get_approved_plan\`) + spec-intent drift analysis
84414
+ - \`reviewer\` — task semantic diff summary + blast radius across changed files
84415
+ - \`sme\` — task domain context + knowledge base entries relevant to the task
84416
+ - \`test_engineer\` — changed test files for the task + coverage delta + known mutation gaps
84417
+ - \`explorer\` — task diff + original task intent + prior slop findings
84418
+ (hunts for lazy implementations, hallucinated APIs, cargo-cult patterns,
84419
+ spec drift, lazy abstractions)
84420
+
84421
+ Wait for ALL dispatched agents to return their verdict objects before proceeding.
84422
+
84423
+ #### STEP 2 — COLLECT verdicts
84424
+ Read each agent's response and extract their \`CouncilMemberVerdict\` object.
84425
+ Each member must return: \`agent\`, \`verdict\` (APPROVE|CONCERNS|REJECT),
84426
+ \`confidence\` (0.0–1.0), \`findings[]\`, \`criteriaAssessed[]\`, \`criteriaUnmet[]\`,
84427
+ \`durationMs\`.
84428
+
84429
+ Do NOT fabricate, infer, or substitute a verdict. If an agent did not return
84430
+ a valid verdict object, re-dispatch that agent.
84431
+
84432
+ #### STEP 3 — CALL submit_council_verdicts (the per-task tool, NOT submit_phase_council_verdicts)
84433
+ ONLY after collecting real verdicts from all dispatched agents, call
84434
+ \`submit_council_verdicts\` with the collected verdicts. The per-task council
84435
+ verdict replaces the Stage B gate — APPROVE advances the task, REJECT blocks it.
84436
+
84437
+ #### STEP 4 — ACT on the verdict
84438
+ - **APPROVE**: Task passes. Proceed to the next task.
84439
+ If \`advisoryFindingsCount > 0\`, deliver \`unifiedFeedbackMd\` as a single
84440
+ non-blocking advisory note before proceeding.
84441
+ - **CONCERNS with \`success: false\` + \`reason: 'blocking_concerns_unresolved'\`**:
84442
+ The tool blocked because HIGH/CRITICAL findings from CONCERNS members were
84443
+ promoted to \`requiredFixes\`. No evidence was written. Send \`unifiedFeedbackMd\`
84444
+ to the coder — every \`requiredFix\` must be resolved. Increment \`roundNumber\`
84445
+ and re-convene council after fixes. This is tool-enforced: you cannot bypass it.
84446
+ - **CONCERNS with \`success: true\`**: Only MEDIUM/LOW advisory findings remain.
84447
+ Task passes — surface \`unifiedFeedbackMd\` as a non-blocking note.
84448
+ - **REJECT**: Block task advancement. Send \`unifiedFeedbackMd\` to the coder
84449
+ with the BLOCKING flag. The coder must resolve all \`requiredFixes\` before
84450
+ the council is re-convened. Maximum \`council.maxRounds\` rounds (default 3).
84451
+ If \`roundNumber >= maxRounds\` and verdict is still REJECT, surface
84452
+ \`unifiedFeedbackMd\` to the user and HALT — do NOT auto-advance.
84453
+
84454
+ ### ANTI-PATTERNS — per-task council bypass violations
84455
+ - ✗ Calling \`submit_council_verdicts\` without first dispatching all 5 members.
84456
+ - ✗ Passing verdicts inferred or fabricated rather than received from dispatched agents.
84457
+ - ✗ Claiming "Council APPROVED" when \`membersAbsent\` is non-empty.
84458
+ - ✗ Falling back to Stage B (reviewer + test_engineer only) when \`council_mode\` is ON — the full council replaces Stage B.
84459
+ - ✗ Skipping \`declare_council_criteria\` before dispatching council members.
84460
+ - ✗ Using \`submit_phase_council_verdicts\` for per-task verdicts — use \`submit_council_verdicts\`.
84461
+
84462
+ ## B. PHASE COUNCIL (when \`phase_council\` is ON)
84375
84463
 
84376
84464
  CRITICAL: \`submit_phase_council_verdicts\` does NOT run council members.
84377
84465
  It synthesizes verdicts that you must collect BEFORE calling it.
84378
84466
 
84379
- When \`council.enabled\` is true and \`council_mode\` is enabled in the QA gate
84380
- profile, a phase-level council review is required before calling \`phase_complete\`.
84381
- Stage B (reviewer + test_engineer) ALWAYS runs per-task as normal.
84382
- Stage B always runs per-task — council is an ADDITIONAL verification layer at PHASE LEVEL, never a replacement for Stage B.
84467
+ When \`phase_council\` is enabled in the QA gate profile, a phase-level council review is required before calling \`phase_complete\`. This is additive to whichever per-task mechanism is active — Stage B (reviewer + test_engineer) runs per task by default, or the full 5-member per-task council if \`council_mode\` is ON.
84383
84468
 
84384
- ### WHEN TO RUN COUNCIL
84469
+ ### WHEN TO RUN PHASE COUNCIL
84385
84470
  After ALL tasks in the current phase have been marked \`completed\` and their
84386
- Stage B gates have passed, and BEFORE calling \`phase_complete\`, convene the
84471
+ per-task gates have passed, and BEFORE calling \`phase_complete\`, convene the
84387
84472
  phase council for a Phase Dossier Assembly — a holistic review of cross-cutting concerns,
84388
84473
  behavioral cohesion, and the full body of work completed in the phase.
84389
84474
 
84390
- ## PHASE COUNCIL
84391
-
84392
84475
  ### MANDATORY SEQUENCE — never skip or reorder
84393
84476
 
84394
84477
  #### STEP 1 — DISPATCH all 5 council members in parallel (phase-scoped)
@@ -84437,10 +84520,13 @@ and re-call \`submit_phase_council_verdicts\`.
84437
84520
  - **APPROVE**: Call \`phase_complete\`. Gate 5 will pass.
84438
84521
  If \`advisoryFindingsCount > 0\`, deliver \`unifiedFeedbackMd\` as a single
84439
84522
  non-blocking advisory note to the team before proceeding.
84440
- - **CONCERNS**: Evaluate severity. Minor concerns → call \`phase_complete\` and
84441
- surface \`unifiedFeedbackMd\` as a non-blocking note. Significant concerns
84442
- send \`unifiedFeedbackMd\` to the coder as ONE coherent document for resolution
84443
- before calling \`phase_complete\`. Increment \`roundNumber\` on re-council.
84523
+ - **CONCERNS with \`success: false\` + \`reason: 'blocking_concerns_unresolved'\`**:
84524
+ The tool blocked because HIGH/CRITICAL findings from CONCERNS members were
84525
+ promoted to \`requiredFixes\`. No evidence was written. Send \`unifiedFeedbackMd\`
84526
+ to the coder — every \`requiredFix\` must be resolved. Increment \`roundNumber\`
84527
+ and re-convene council after fixes. This is tool-enforced: you cannot bypass it.
84528
+ - **CONCERNS with \`success: true\`**: Only MEDIUM/LOW advisory findings remain.
84529
+ Call \`phase_complete\` and surface \`unifiedFeedbackMd\` as a non-blocking note.
84444
84530
  - **REJECT**: Block advancement. Send \`unifiedFeedbackMd\` to the coder
84445
84531
  with the BLOCKING flag. The coder must resolve all \`requiredFixes\` before
84446
84532
  the phase council is re-convened. Maximum \`council.maxRounds\` rounds (default 3).
@@ -84451,10 +84537,11 @@ and re-call \`submit_phase_council_verdicts\`.
84451
84537
  - ✗ Calling \`submit_phase_council_verdicts\` without first dispatching all 5 members.
84452
84538
  - ✗ Passing verdicts inferred or fabricated rather than received from dispatched agents.
84453
84539
  - ✗ Claiming "Council APPROVED" when \`membersAbsent\` is non-empty.
84454
- - ✗ Omitting per-task review gates (reviewer + test_engineer) because council mode is onthese gates are mandatory regardless.
84540
+ - ✗ Skipping per-task gates because phase council will catch issuesper-task gates are mandatory regardless.
84455
84541
  - ✗ Calling \`phase_complete\` before council evidence has been written (Gate 5 will block you).
84456
84542
  - ✗ Treating a prior phase's council verdict as valid for a new phase.
84457
84543
  - ✗ Incrementing \`roundNumber\` without re-dispatching members for the new round.
84544
+ - ✗ Using \`submit_council_verdicts\` for phase verdicts — use \`submit_phase_council_verdicts\`.
84458
84545
 
84459
84546
  ### ROUND 2 DELIBERATION
84460
84547
  If round 1 produces REJECT or CONCERNS requiring re-work, every prior-round
@@ -84497,16 +84584,25 @@ Present the eleven gates with their defaults (DEFAULT_QA_GATES) as a single user
84497
84584
  - sme_enabled (default: ON) — SME consultation during planning/clarification
84498
84585
  - critic_pre_plan (default: ON) — critic review before plan finalization
84499
84586
  - sast_enabled (default: ON) — static security scanning
84500
- - council_mode (default: OFF) — multi-member council gate (recommended for high-impact architecture, public APIs, schema/data mutation, security-sensitive code)
84587
+ - council_mode (default: OFF) — replaces per-task Stage B (reviewer + test_engineer) with the full 5-member council (critic, reviewer, sme, test_engineer, explorer). When enabled, Stage A still runs, but after Stage A passes, all 5 council members review the task instead of just reviewer + test_engineer. Requires council.enabled: true in config. (recommended for high-impact architecture, public APIs, schema/data mutation, security-sensitive code)
84501
84588
  - hallucination_guard (default: OFF) — when enabled, mandatory per-phase API/signature/claim/citation verification via critic_hallucination_verifier at PHASE-WRAP; phase_complete will REJECT phase completion unless .swarm/evidence/{phase}/hallucination-guard.json exists with an APPROVED verdict (recommended for claim-heavy or research-heavy work)
84502
84589
  - mutation_test (default: OFF) — when enabled, runs mutation testing on source files touched this phase via generate_mutants + mutation_test + write_mutation_evidence at PHASE-WRAP; FAIL verdict blocks phase_complete; WARN is non-blocking (recommended for projects with coverage gaps or safety-critical code)
84503
- - council_general_review (default: OFF) — when enabled, MODE: SPECIFY runs convene_general_council on the draft spec before the critic-gate; the architect runs a curated web_search pass, dispatches council_generalist / council_skeptic / council_domain_expert in parallel with a shared RESEARCH CONTEXT block, deliberates on disagreements, and synthesizes the result directly into the spec (recommended for novel architecture, unclear best practices, or high-risk design decisions). Requires council.general.enabled: true and a configured search API key in the resolved config: global ~/.config/opencode/opencode-swarm.json, then project .opencode/opencode-swarm.json overrides.
84590
+ - phase_council (default: OFF) — when enabled, a full 5-member council (critic, reviewer, sme, test_engineer, explorer) reviews all work completed in a phase holistically at phase_complete time. This is additive to whichever per-task mechanism is active Stage B (reviewer + test_engineer) by default, or the full 5-member per-task council if council_mode is ON. Requires council.enabled: true in config.
84504
84591
  - drift_check (default: ON) — when enabled, mandatory per-phase drift verification via critic_drift_verifier at PHASE-WRAP; compares implemented changes against spec.md intent; hard-blocks phase_complete when spec.md exists and drift evidence is missing or REJECTED; advisory-only when no spec.md exists (recommended for all projects with a specification)
84505
- - final_council (default: OFF) - when enabled, after all phases complete the architect dispatches the same five phase-council members (\`critic\`, \`reviewer\`, \`sme\`, \`test_engineer\`, \`explorer\`) at project scope, collects \`CouncilMemberVerdict\` objects, and calls \`write_final_council_evidence\`. This is not General Council mode and does not require \`council.general.enabled\`.
84592
+ - final_council (default: OFF) when enabled, the full 5-member council (NOT the General Council) reviews the entire project after all phases complete. The architect dispatches the same five council members (\`critic\`, \`reviewer\`, \`sme\`, \`test_engineer\`, \`explorer\`) at project scope, collects \`CouncilMemberVerdict\` objects, and calls \`write_final_council_evidence\`. This is not General Council mode and does not require \`council.general.enabled\`.
84593
+
84594
+ Present all three items together in a single message. One message, defaults pre-stated. Wait for the user's answer to all three:
84506
84595
 
84507
- One question, one message, defaults pre-stated. Wait for the user's answer.
84596
+ **1. QA Gates** accept defaults or customize (the eleven gates listed above).
84508
84597
 
84509
- If the user answered the gate question, immediately follow up with ONE more question: "How many coders should run in parallel? (default: 1, range: 1-4)" — if the user says a number > 1, also write a \`## Pending Parallelization Config\` section to \`.swarm/context.md\` alongside the gate selection:
84598
+ **2. Parallel Coders** "How many coders should run in parallel? (default: 1, range: 1-4)"
84599
+
84600
+ **3. Commit Frequency** — "Commit frequency for completed tasks? (default: phase-level only; optional per-task checkpoint commit after each task completion)"
84601
+
84602
+ Wait for the user to answer all three in a single reply. Then apply:
84603
+
84604
+ - For gates: record the user's gate selections.
84605
+ - For parallel coders: if the user says a number > 1, write a \`## Pending Parallelization Config\` section to \`.swarm/context.md\` alongside the gate selection:
84510
84606
  \`\`\`
84511
84607
  ## Pending Parallelization Config
84512
84608
  - parallelization_enabled: true
@@ -84517,9 +84613,7 @@ If the user answered the gate question, immediately follow up with ONE more ques
84517
84613
  \`\`\`
84518
84614
  If the user accepts the default (1), skip writing this section entirely — serial execution is the default and needs no config.
84519
84615
 
84520
- After asking the parallelization question (regardless of whether the user chose serial or parallel), immediately follow up with ONE more question: "Commit frequency for completed tasks? (default: phase-level only; optional per-task checkpoint commit after each task completion)".
84521
-
84522
- If the user chooses per-task commits, write this section to \`.swarm/context.md\`:
84616
+ - For commit frequency: if the user chooses per-task commits, write this section to \`.swarm/context.md\`:
84523
84617
  \`\`\`
84524
84618
  ## Task Completion Commit Policy
84525
84619
  - commit_after_each_completed_task: true
@@ -84971,7 +85065,7 @@ TIER 3 — CRITICAL
84971
85065
  Pipeline: Full Stage A. Stage B = {{AGENT_PREFIX}}reviewer×2 + {{AGENT_PREFIX}}test_engineer×2.
84972
85066
  Rationale: Security paths need adversarial review.
84973
85067
 
84974
- Council mode is additive Stage B always runs per-task in both modes. The council runs holistically at phase end via \`submit_phase_council_verdicts\` before calling \`phase_complete\`. Council is supplemental; Stage B is mandatory in all modes.
85068
+ When \`council_mode\` is enabled, Stage B (reviewer + test_engineer) is replaced by the full 5-member council per task. When \`phase_council\` is enabled, a phase-level council review is additionally required before calling \`phase_complete\`.
84975
85069
 
84976
85070
  CLASSIFICATION RULES:
84977
85071
  - Multi-tier → use HIGHEST tier.
@@ -84998,7 +85092,7 @@ Stage B runs by default for TIER 1-3 classifications. Stage A passing does not s
84998
85092
  Stage B is where logic errors, security flaws, edge cases, and behavioral bugs are caught.
84999
85093
  You MUST delegate to each required Stage B agent. For the standard reviewer + test_engineer pair, dispatch both before waiting so Stage B actually runs in parallel.
85000
85094
 
85001
- Stage B (reviewer + test_engineer) **always runs per-task** regardless of council mode it is never replaced, never omitted, never deferred. When \`council_mode\` is enabled in the QA gate profile, a **phase-level** council review is additionally required before calling \`phase_complete\`: dispatch all 5 council members, collect their verdicts, call \`submit_phase_council_verdicts\`, then call \`phase_complete\` (Gate 5 validates the resulting \`phase-council.json\` evidence). Stage A (\`pre_check_batch\`) still runs as the pre-review gate for each task.
85095
+ When \`council_mode\` is enabled, Stage B (reviewer + test_engineer) is **replaced** by the full 5-member council (critic, reviewer, sme, test_engineer, explorer) per task. Stage A (\`pre_check_batch\`) still runs as the pre-review gate. When \`phase_council\` is enabled, a phase-level council review is additionally required before calling \`phase_complete\`: dispatch all 5 council members with phase-scoped context, collect their verdicts, call \`submit_phase_council_verdicts\`, then call \`phase_complete\` (Gate 5 validates the resulting \`phase-council.json\` evidence).
85002
85096
 
85003
85097
  A task is complete ONLY when BOTH stages pass.
85004
85098
 
@@ -85375,6 +85469,7 @@ HARD CONSTRAINTS:
85375
85469
 
85376
85470
  <!-- BEHAVIORAL_GUIDANCE_START -->
85377
85471
  - Treat brainstorm output as discovery material until the loaded skill transitions to SPECIFY or PLAN.
85472
+ - When council.general.enabled is true, the brainstorm skill offers the user a General Council advisory input option before spec writing. This is NOT a QA gate — it's an early workflow option. The convene_general_council tool must be available when council.general.enabled is true.
85378
85473
  <!-- BEHAVIORAL_GUIDANCE_END -->
85379
85474
 
85380
85475
  ### MODE: SPECIFY
@@ -85391,6 +85486,7 @@ HARD CONSTRAINTS:
85391
85486
 
85392
85487
  <!-- BEHAVIORAL_GUIDANCE_START -->
85393
85488
  - Follow the loaded skill's spec creation, clarification, and transition rules.
85489
+ - General Council advisory input is available via the /swarm council command at any time. It is NOT offered as a SPECIFY workflow step — it moved to BRAINSTORM Phase 1b as an early option before spec writing.
85394
85490
  <!-- BEHAVIORAL_GUIDANCE_END -->
85395
85491
 
85396
85492
  <!-- BEHAVIORAL_GUIDANCE_START -->
@@ -98130,7 +98226,7 @@ DELEGATION RULES:
98130
98226
  ${complianceHeader}
98131
98227
  - Reviewer delegation is MANDATORY for every coder task.
98132
98228
  - pre_check_batch is NOT a substitute for reviewer.
98133
- - Stage A (tools) + Stage B (agents) = BOTH required.
98229
+ - Stage A (automated tools) + Stage B/Council (agent review) = BOTH required.
98134
98230
  ${phaseNumber !== null && phaseNumber >= 4 ? `
98135
98231
  ⚠️ You are in Phase ${phaseNumber}. Compliance degrades with time. Do not skip reviewer or test_engineer.` : ""}
98136
98232
  </swarm_reminder>`;
@@ -111833,16 +111929,17 @@ function synthesizeCouncilVerdicts(taskId, swarmId, verdicts, criteria, roundNum
111833
111929
  const unresolvedConflicts = detectConflicts2(verdicts);
111834
111930
  const rejectingSet = new Set(rejectingMembers);
111835
111931
  const vetoFindings = verdicts.filter((v) => rejectingSet.has(v.agent)).flatMap((v) => v.findings);
111836
- const requiredFixes = vetoFindings.filter((f) => f.severity === "HIGH" || f.severity === "MEDIUM");
111932
+ const requiredFixes = vetoFindings.filter((f) => f.severity === "CRITICAL" || f.severity === "HIGH" || f.severity === "MEDIUM");
111837
111933
  const advisoryFindings = [
111838
111934
  ...vetoFindings.filter((f) => f.severity === "LOW"),
111839
111935
  ...verdicts.filter((v) => !rejectingSet.has(v.agent)).flatMap((v) => v.findings)
111840
111936
  ];
111937
+ const blockingConcernsCount = promoteBlockingConcerns(verdicts, rejectingSet, requiredFixes, advisoryFindings);
111841
111938
  const allAssessedIds = new Set(verdicts.flatMap((v) => v.criteriaAssessed));
111842
111939
  const allUnmetIds = new Set(verdicts.flatMap((v) => v.criteriaUnmet));
111843
111940
  const mandatoryIds = new Set((criteria?.criteria ?? []).filter((c) => c.mandatory).map((c) => c.id));
111844
111941
  const allCriteriaMet = [...mandatoryIds].every((id) => allAssessedIds.has(id) && !allUnmetIds.has(id));
111845
- const unifiedFeedbackMd = buildUnifiedFeedback(taskId, overallVerdict, rejectingMembers, requiredFixes, advisoryFindings, unresolvedConflicts, roundNumber, cfg.maxRounds);
111942
+ const unifiedFeedbackMd = buildUnifiedFeedback(taskId, overallVerdict, rejectingMembers, requiredFixes, advisoryFindings, unresolvedConflicts, roundNumber, cfg.maxRounds, blockingConcernsCount);
111846
111943
  return {
111847
111944
  taskId,
111848
111945
  swarmId,
@@ -111857,9 +111954,24 @@ function synthesizeCouncilVerdicts(taskId, swarmId, verdicts, criteria, roundNum
111857
111954
  roundNumber,
111858
111955
  allCriteriaMet,
111859
111956
  quorumSize,
111957
+ blockingConcernsCount,
111860
111958
  ...verdicts.length === 0 && { emptyVerdictsWarning: true }
111861
111959
  };
111862
111960
  }
111961
+ function promoteBlockingConcerns(verdicts, rejectingSet, requiredFixes, advisoryFindings) {
111962
+ const concernsFindings = verdicts.filter((v) => v.verdict === "CONCERNS" && !rejectingSet.has(v.agent)).flatMap((v) => v.findings);
111963
+ const blocking = concernsFindings.filter((f) => f.severity === "CRITICAL" || f.severity === "HIGH");
111964
+ if (blocking.length === 0)
111965
+ return 0;
111966
+ requiredFixes.push(...blocking);
111967
+ const blockingSet = new Set(blocking);
111968
+ for (let i2 = advisoryFindings.length - 1;i2 >= 0; i2--) {
111969
+ if (blockingSet.has(advisoryFindings[i2])) {
111970
+ advisoryFindings.splice(i2, 1);
111971
+ }
111972
+ }
111973
+ return blocking.length;
111974
+ }
111863
111975
  function detectConflicts2(verdicts) {
111864
111976
  const conflicts = [];
111865
111977
  const locationMap = new Map;
@@ -111889,7 +112001,7 @@ function detectConflicts2(verdicts) {
111889
112001
  }
111890
112002
  return conflicts;
111891
112003
  }
111892
- function buildUnifiedFeedback(taskId, verdict, vetoedBy, requiredFixes, advisoryFindings, conflicts, roundNumber, maxRounds) {
112004
+ function buildUnifiedFeedback(taskId, verdict, vetoedBy, requiredFixes, advisoryFindings, conflicts, roundNumber, maxRounds, blockingConcernsCount = 0) {
111893
112005
  const lines = [
111894
112006
  `## Work Complete Council — Round ${roundNumber}/${maxRounds}`,
111895
112007
  `**Task:** ${taskId} **Overall verdict:** ${verdict}`,
@@ -111899,6 +112011,10 @@ function buildUnifiedFeedback(taskId, verdict, vetoedBy, requiredFixes, advisory
111899
112011
  lines.push(`> ⛔ **BLOCKED** by: ${vetoedBy.join(", ")}`);
111900
112012
  lines.push("");
111901
112013
  }
112014
+ if (blockingConcernsCount > 0) {
112015
+ lines.push(`> ⚠️ **BLOCKING CONCERNS**: ${blockingConcernsCount} HIGH/CRITICAL finding(s) from CONCERNS members require investigation and resolution before advancement.`);
112016
+ lines.push("");
112017
+ }
111902
112018
  if (requiredFixes.length > 0) {
111903
112019
  lines.push("### Required Fixes (must resolve before re-submission)");
111904
112020
  for (const f of requiredFixes) {
@@ -111946,11 +112062,12 @@ function synthesizePhaseCouncilAdvisory(phaseNumber, phaseSummary, verdicts, rou
111946
112062
  const unresolvedConflicts = detectConflicts2(verdicts);
111947
112063
  const rejectingSet = new Set(rejectingMembers);
111948
112064
  const vetoFindings = verdicts.filter((v) => rejectingSet.has(v.agent)).flatMap((v) => v.findings);
111949
- const requiredFixes = vetoFindings.filter((f) => f.severity === "HIGH" || f.severity === "MEDIUM");
112065
+ const requiredFixes = vetoFindings.filter((f) => f.severity === "CRITICAL" || f.severity === "HIGH" || f.severity === "MEDIUM");
111950
112066
  const advisoryFindings = [
111951
112067
  ...vetoFindings.filter((f) => f.severity === "LOW"),
111952
112068
  ...verdicts.filter((v) => !rejectingSet.has(v.agent)).flatMap((v) => v.findings)
111953
112069
  ];
112070
+ const blockingConcernsCount = promoteBlockingConcerns(verdicts, rejectingSet, requiredFixes, advisoryFindings);
111954
112071
  const advisoryNotes = [];
111955
112072
  if (advisoryFindings.length > 0) {
111956
112073
  advisoryNotes.push(`Phase ${phaseNumber} council found ${advisoryFindings.length} advisory finding(s). Review before proceeding to next phase.`);
@@ -111960,7 +112077,7 @@ function synthesizePhaseCouncilAdvisory(phaseNumber, phaseSummary, verdicts, rou
111960
112077
  }
111961
112078
  const allUnmetIds = new Set(verdicts.flatMap((v) => v.criteriaUnmet));
111962
112079
  const allCriteriaMet = allUnmetIds.size === 0 && verdicts.length > 0;
111963
- const unifiedFeedbackMd = buildPhaseCouncilFeedback(phaseNumber, phaseSummary, overallVerdict, rejectingMembers, requiredFixes, advisoryFindings, unresolvedConflicts, roundNumber, cfg.maxRounds);
112080
+ const unifiedFeedbackMd = buildPhaseCouncilFeedback(phaseNumber, phaseSummary, overallVerdict, rejectingMembers, requiredFixes, advisoryFindings, unresolvedConflicts, roundNumber, cfg.maxRounds, blockingConcernsCount);
111964
112081
  const evidencePath = `.swarm/evidence/${phaseNumber}/phase-council.json`;
111965
112082
  return {
111966
112083
  phaseNumber,
@@ -111977,11 +112094,12 @@ function synthesizePhaseCouncilAdvisory(phaseNumber, phaseSummary, verdicts, rou
111977
112094
  roundNumber,
111978
112095
  allCriteriaMet,
111979
112096
  quorumSize,
112097
+ blockingConcernsCount,
111980
112098
  evidencePath,
111981
112099
  phaseSummary
111982
112100
  };
111983
112101
  }
111984
- function buildPhaseCouncilFeedback(phaseNumber, phaseSummary, verdict, vetoedBy, requiredFixes, advisoryFindings, conflicts, roundNumber, maxRounds) {
112102
+ function buildPhaseCouncilFeedback(phaseNumber, phaseSummary, verdict, vetoedBy, requiredFixes, advisoryFindings, conflicts, roundNumber, maxRounds, blockingConcernsCount = 0) {
111985
112103
  const lines = [
111986
112104
  `## Phase Council Review — Round ${roundNumber}/${maxRounds}`,
111987
112105
  `**Phase:** ${phaseNumber} **Overall verdict:** ${verdict}`,
@@ -111995,6 +112113,10 @@ function buildPhaseCouncilFeedback(phaseNumber, phaseSummary, verdict, vetoedBy,
111995
112113
  lines.push(`> ⛔ **BLOCKED** by: ${vetoedBy.join(", ")}`);
111996
112114
  lines.push("");
111997
112115
  }
112116
+ if (blockingConcernsCount > 0) {
112117
+ lines.push(`> ⚠️ **BLOCKING CONCERNS**: ${blockingConcernsCount} HIGH/CRITICAL finding(s) from CONCERNS members require investigation and resolution before phase completion.`);
112118
+ lines.push("");
112119
+ }
111998
112120
  if (requiredFixes.length > 0) {
111999
112121
  lines.push("### Required Fixes (must resolve before re-submission)");
112000
112122
  for (const f of requiredFixes) {
@@ -112041,11 +112163,12 @@ function synthesizeFinalCouncilAdvisory(projectSummary, verdicts, roundNumber, c
112041
112163
  const unresolvedConflicts = detectConflicts2(verdicts);
112042
112164
  const rejectingSet = new Set(rejectingMembers);
112043
112165
  const vetoFindings = verdicts.filter((v) => rejectingSet.has(v.agent)).flatMap((v) => v.findings);
112044
- const requiredFixes = vetoFindings.filter((f) => f.severity === "HIGH" || f.severity === "MEDIUM");
112166
+ const requiredFixes = vetoFindings.filter((f) => f.severity === "CRITICAL" || f.severity === "HIGH" || f.severity === "MEDIUM");
112045
112167
  const advisoryFindings = [
112046
112168
  ...vetoFindings.filter((f) => f.severity === "LOW"),
112047
112169
  ...verdicts.filter((v) => !rejectingSet.has(v.agent)).flatMap((v) => v.findings)
112048
112170
  ];
112171
+ const blockingConcernsCount = promoteBlockingConcerns(verdicts, rejectingSet, requiredFixes, advisoryFindings);
112049
112172
  const advisoryNotes = [];
112050
112173
  if (advisoryFindings.length > 0) {
112051
112174
  advisoryNotes.push(`Final council found ${advisoryFindings.length} advisory finding(s). Review before project close.`);
@@ -112055,7 +112178,7 @@ function synthesizeFinalCouncilAdvisory(projectSummary, verdicts, roundNumber, c
112055
112178
  }
112056
112179
  const allUnmetIds = new Set(verdicts.flatMap((v) => v.criteriaUnmet));
112057
112180
  const allCriteriaMet = allUnmetIds.size === 0 && verdicts.length > 0;
112058
- const unifiedFeedbackMd = buildFinalCouncilFeedback(projectSummary, overallVerdict, rejectingMembers, requiredFixes, advisoryFindings, unresolvedConflicts, roundNumber, cfg.maxRounds);
112181
+ const unifiedFeedbackMd = buildFinalCouncilFeedback(projectSummary, overallVerdict, rejectingMembers, requiredFixes, advisoryFindings, unresolvedConflicts, roundNumber, cfg.maxRounds, blockingConcernsCount);
112059
112182
  return {
112060
112183
  scope: "project",
112061
112184
  timestamp,
@@ -112071,10 +112194,11 @@ function synthesizeFinalCouncilAdvisory(projectSummary, verdicts, roundNumber, c
112071
112194
  allCriteriaMet,
112072
112195
  quorumSize,
112073
112196
  evidencePath: ".swarm/evidence/final-council.json",
112197
+ blockingConcernsCount,
112074
112198
  projectSummary
112075
112199
  };
112076
112200
  }
112077
- function buildFinalCouncilFeedback(projectSummary, verdict, vetoedBy, requiredFixes, advisoryFindings, conflicts, roundNumber, maxRounds) {
112201
+ function buildFinalCouncilFeedback(projectSummary, verdict, vetoedBy, requiredFixes, advisoryFindings, conflicts, roundNumber, maxRounds, blockingConcernsCount = 0) {
112078
112202
  const lines = [
112079
112203
  `## Final Council Review - Round ${roundNumber}/${maxRounds}`,
112080
112204
  `**Scope:** completed project **Overall verdict:** ${verdict}`,
@@ -112084,6 +112208,10 @@ function buildFinalCouncilFeedback(projectSummary, verdict, vetoedBy, requiredFi
112084
112208
  lines.push(`**Project Summary:** ${projectSummary}`);
112085
112209
  lines.push("");
112086
112210
  }
112211
+ if (blockingConcernsCount > 0) {
112212
+ lines.push(`> ⚠️ **BLOCKING CONCERNS**: ${blockingConcernsCount} HIGH/CRITICAL finding(s) from CONCERNS members require investigation and resolution before project close.`);
112213
+ lines.push("");
112214
+ }
112087
112215
  if (vetoedBy.length > 0) {
112088
112216
  lines.push(`> BLOCKED: project close is blocked by ${vetoedBy.join(", ")}`);
112089
112217
  lines.push("");
@@ -112162,7 +112290,7 @@ init_state();
112162
112290
  init_create_tool();
112163
112291
  init_resolve_working_directory();
112164
112292
  var FindingSchema = exports_external.object({
112165
- severity: exports_external.enum(["HIGH", "MEDIUM", "LOW"]),
112293
+ severity: exports_external.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW"]),
112166
112294
  category: exports_external.string().min(1),
112167
112295
  location: exports_external.string(),
112168
112296
  detail: exports_external.string(),
@@ -112203,7 +112331,7 @@ var submit_council_verdicts = createSwarmTool({
112203
112331
  verdictRound: exports_external.number().int().min(1).max(10).optional(),
112204
112332
  confidence: exports_external.number().min(0).max(1),
112205
112333
  findings: exports_external.array(exports_external.object({
112206
- severity: exports_external.enum(["HIGH", "MEDIUM", "LOW"]),
112334
+ severity: exports_external.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW"]),
112207
112335
  category: exports_external.string().min(1),
112208
112336
  location: exports_external.string(),
112209
112337
  detail: exports_external.string(),
@@ -112306,6 +112434,17 @@ var submit_council_verdicts = createSwarmTool({
112306
112434
  session.pendingCouncilRequirements.set(nextRoundKey, new Set(dissenters));
112307
112435
  }
112308
112436
  }
112437
+ if (synthesis.overallVerdict === "CONCERNS" && synthesis.blockingConcernsCount > 0) {
112438
+ return JSON.stringify({
112439
+ success: false,
112440
+ reason: "blocking_concerns_unresolved",
112441
+ overallVerdict: synthesis.overallVerdict,
112442
+ blockingConcernsCount: synthesis.blockingConcernsCount,
112443
+ requiredFixes: synthesis.requiredFixes,
112444
+ unifiedFeedbackMd: synthesis.unifiedFeedbackMd,
112445
+ message: `Council returned CONCERNS with ${synthesis.blockingConcernsCount} HIGH/CRITICAL finding(s) promoted to requiredFixes. These must be resolved before the task can advance. Do NOT write evidence or proceed — address every requiredFix and resubmit.`
112446
+ }, null, 2);
112447
+ }
112309
112448
  await writeCouncilEvidence(workingDir, synthesis);
112310
112449
  try {
112311
112450
  if (sessionID) {
@@ -119886,7 +120025,7 @@ async function runPhaseCouncilGate(ctx) {
119886
120025
  const session = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
119887
120026
  const overrides = session?.qaGateSessionOverrides ?? {};
119888
120027
  const effective = getEffectiveGates(profile, overrides);
119889
- if (effective.council_mode === true) {
120028
+ if (effective.phase_council === true && pluginConfig.council?.enabled === true) {
119890
120029
  councilModeEnabled = true;
119891
120030
  const pcPath = path148.join(dir, ".swarm", "evidence", String(phase), "phase-council.json");
119892
120031
  let pcVerdictFound = false;
@@ -119990,7 +120129,7 @@ Advisory notes: ${advisoryNotes.join("; ")}` : "";
119990
120129
  blocked: true,
119991
120130
  reason: "PHASE_COUNCIL_REQUIRED",
119992
120131
  phase_council_required: true,
119993
- message: `Phase ${phase} cannot be completed: council_mode is enabled and phase council evidence not found at .swarm/evidence/${phase}/phase-council.json. Convene a phase-level council (dispatch 5 members, collect verdicts, call submit_phase_council_verdicts) before completing the phase.`,
120132
+ message: `Phase ${phase} cannot be completed: phase_council is enabled and phase council evidence not found at .swarm/evidence/${phase}/phase-council.json. Convene a phase-level council (dispatch 5 members, collect verdicts, call submit_phase_council_verdicts) before completing the phase.`,
119994
120133
  agentsDispatched,
119995
120134
  agentsMissing: [],
119996
120135
  warnings: [
@@ -120046,7 +120185,7 @@ Advisory notes: ${advisoryNotes.join("; ")}` : "";
120046
120185
  return {
120047
120186
  blocked: true,
120048
120187
  reason: "PHASE_COUNCIL_ERROR",
120049
- message: `Phase ${phase} cannot be completed: phase council gate encountered an error when council_mode was enabled. Error: ${String(pcError)}`,
120188
+ message: `Phase ${phase} cannot be completed: phase council gate encountered an error when phase_council was enabled. Error: ${String(pcError)}`,
120050
120189
  agentsDispatched,
120051
120190
  agentsMissing: [],
120052
120191
  warnings: [`PHASE_COUNCIL_ERROR: ${String(pcError)}`]
@@ -128056,7 +128195,7 @@ async function executeSetQaGates(args2, directory) {
128056
128195
  "hallucination_guard",
128057
128196
  "sast_enabled",
128058
128197
  "mutation_test",
128059
- "council_general_review",
128198
+ "phase_council",
128060
128199
  "drift_check",
128061
128200
  "final_council"
128062
128201
  ]) {
@@ -128098,13 +128237,13 @@ var set_qa_gates = createSwarmTool({
128098
128237
  args: {
128099
128238
  reviewer: exports_external.boolean().optional().describe("Enable the reviewer gate (true) — cannot be disabled."),
128100
128239
  test_engineer: exports_external.boolean().optional().describe("Enable the test_engineer gate (true) — cannot be disabled once on."),
128101
- council_mode: exports_external.boolean().optional().describe("Enable council mode (multi-SME consensus on high-risk phases)."),
128240
+ council_mode: exports_external.boolean().optional().describe("Enable council mode — replaces per-task Stage B (reviewer + test_engineer) with the full 5-member council (critic, reviewer, sme, test_engineer, explorer) per task."),
128102
128241
  sme_enabled: exports_external.boolean().optional().describe("Enable SME consultation."),
128103
128242
  critic_pre_plan: exports_external.boolean().optional().describe("Enable critic_pre_plan review before plan approval."),
128104
128243
  hallucination_guard: exports_external.boolean().optional().describe("Enable hallucination_guard checks on plan and implementation claims."),
128105
128244
  sast_enabled: exports_external.boolean().optional().describe("Enable SAST scanning as a required QA gate."),
128106
128245
  mutation_test: exports_external.boolean().optional().describe("Enable the mutation-testing gate (default: off). Requires mutation " + "tests to achieve a passing kill rate before phase completion; " + "WARN verdict allows advancement, FAIL blocks."),
128107
- council_general_review: exports_external.boolean().optional().describe("Enable the council_general_review gate (default: off). When on, " + "MODE: SPECIFY runs convene_general_council on the draft spec " + "before the critic-gate, folding multi-model deliberation into " + "the spec. Requires council.general.enabled and a search API key " + "in the resolved config: global ~/.config/opencode/opencode-swarm.json, " + "then project .opencode/opencode-swarm.json overrides."),
128246
+ phase_council: exports_external.boolean().optional().describe("Enable the phase_council gate (default: off). When on, a full " + "5-member council (critic, reviewer, sme, test_engineer, explorer) " + "reviews all work completed in a phase holistically at phase_complete " + "time. Requires council.enabled: true in config."),
128108
128247
  drift_check: exports_external.boolean().optional().describe("Enable drift verification gate (default: on). Blocks phase_complete " + "until drift-verifier.json has an approved verdict. When disabled, " + "drift verification is skipped entirely."),
128109
128248
  final_council: exports_external.boolean().optional().describe("Enable the final_council gate (default: off). When on, " + "after all phases complete the architect dispatches critic, reviewer, " + "sme, test_engineer, and explorer with project-scoped context, " + "collects their CouncilMemberVerdict objects, and calls " + "write_final_council_evidence. This is not General Council mode " + "and does not require council.general.enabled."),
128110
128249
  project_type: exports_external.string().optional().describe('Project type label (e.g. "ts", "python"). Only applied when the profile is being created for the first time.')
@@ -128363,7 +128502,7 @@ var VerdictSchema2 = exports_external.object({
128363
128502
  verdictRound: exports_external.number().int().min(1).max(10).optional(),
128364
128503
  confidence: exports_external.number().min(0).max(1),
128365
128504
  findings: exports_external.array(exports_external.object({
128366
- severity: exports_external.enum(["HIGH", "MEDIUM", "LOW"]),
128505
+ severity: exports_external.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW"]),
128367
128506
  category: exports_external.string().min(1),
128368
128507
  location: exports_external.string(),
128369
128508
  detail: exports_external.string(),
@@ -128382,7 +128521,7 @@ var ArgsSchema4 = exports_external.object({
128382
128521
  working_directory: exports_external.string().optional()
128383
128522
  });
128384
128523
  var submit_phase_council_verdicts = createSwarmTool({
128385
- description: "Submit pre-collected council member verdicts for PHASE-LEVEL synthesis. " + "PREREQUISITE — you MUST dispatch each council member (critic, reviewer, sme, " + "test_engineer, explorer) as separate Agent tasks with PHASE-SCOPED context and " + "collect their verdict responses BEFORE calling this tool. This tool performs " + "synthesis only — it does NOT dispatch, invoke, or contact council members. " + "Writes .swarm/evidence/{phase}/phase-council.json which is required by " + "phase_complete Gate 5 when council_mode is enabled. " + "Architect-only. Config-gated via council.enabled.",
128524
+ description: "Submit pre-collected council member verdicts for PHASE-LEVEL synthesis. " + "PREREQUISITE — you MUST dispatch each council member (critic, reviewer, sme, " + "test_engineer, explorer) as separate Agent tasks with PHASE-SCOPED context and " + "collect their verdict responses BEFORE calling this tool. This tool performs " + "synthesis only — it does NOT dispatch, invoke, or contact council members. " + "Writes .swarm/evidence/{phase}/phase-council.json which is required by " + "phase_complete Gate 5 when phase_council is enabled. " + "Architect-only. Config-gated via council.enabled.",
128386
128525
  args: {
128387
128526
  phaseNumber: exports_external.number().int().min(1).describe("Phase number being reviewed (e.g. 1, 2, 3)"),
128388
128527
  swarmId: exports_external.string().min(1).describe('Swarm identifier, e.g. "mega"'),
@@ -128400,7 +128539,7 @@ var submit_phase_council_verdicts = createSwarmTool({
128400
128539
  verdictRound: exports_external.number().int().min(1).max(10).optional(),
128401
128540
  confidence: exports_external.number().min(0).max(1),
128402
128541
  findings: exports_external.array(exports_external.object({
128403
- severity: exports_external.enum(["HIGH", "MEDIUM", "LOW"]),
128542
+ severity: exports_external.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW"]),
128404
128543
  category: exports_external.string().min(1),
128405
128544
  location: exports_external.string(),
128406
128545
  detail: exports_external.string(),
@@ -128475,6 +128614,20 @@ var submit_phase_council_verdicts = createSwarmTool({
128475
128614
  const mutationGapFinding = existingMutationGapFinding ? null : getPhaseMutationGapFinding(input.phaseNumber, workingDir);
128476
128615
  if (mutationGapFinding) {
128477
128616
  addMutationGapFindingToSynthesis(synthesis, mutationGapFinding);
128617
+ if (mutationGapFinding.severity === "CRITICAL" || mutationGapFinding.severity === "HIGH") {
128618
+ synthesis.blockingConcernsCount++;
128619
+ }
128620
+ }
128621
+ if (synthesis.blockingConcernsCount > 0) {
128622
+ return JSON.stringify({
128623
+ success: false,
128624
+ reason: "blocking_concerns_unresolved",
128625
+ overallVerdict: synthesis.overallVerdict,
128626
+ blockingConcernsCount: synthesis.blockingConcernsCount,
128627
+ requiredFixes: synthesis.requiredFixes,
128628
+ unifiedFeedbackMd: synthesis.unifiedFeedbackMd,
128629
+ message: `Phase council returned CONCERNS with ${synthesis.blockingConcernsCount} HIGH/CRITICAL finding(s) promoted to requiredFixes. These must be resolved before the phase can complete. Do NOT write evidence or proceed — address every requiredFix and resubmit.`
128630
+ }, null, 2);
128478
128631
  }
128479
128632
  writePhaseCouncilEvidence(workingDir, synthesis);
128480
128633
  return JSON.stringify({
@@ -128604,7 +128757,7 @@ function writePhaseCouncilEvidence(workingDir, synthesis) {
128604
128757
  }
128605
128758
  }
128606
128759
  function addMutationGapFindingToSynthesis(synthesis, finding) {
128607
- if (finding.severity === "HIGH" || finding.severity === "MEDIUM") {
128760
+ if (finding.severity === "CRITICAL" || finding.severity === "HIGH" || finding.severity === "MEDIUM") {
128608
128761
  synthesis.requiredFixes.push(finding);
128609
128762
  } else {
128610
128763
  synthesis.advisoryFindings.push(finding);
@@ -130720,6 +130873,69 @@ function recoverTaskStateFromDelegations(taskId, directory) {
130720
130873
  }
130721
130874
  }
130722
130875
  }
130876
+ function checkCouncilGate(workingDirectory, taskId) {
130877
+ let councilEnabled = false;
130878
+ let effectiveMinimum = 3;
130879
+ try {
130880
+ const config3 = loadPluginConfig(workingDirectory);
130881
+ councilEnabled = config3.council?.enabled === true;
130882
+ effectiveMinimum = config3.council?.requireAllMembers ? 5 : config3.council?.minimumMembers ?? 3;
130883
+ } catch {
130884
+ return { blocked: false, reason: "", active: false };
130885
+ }
130886
+ if (!councilEnabled) {
130887
+ return { blocked: false, reason: "", active: false };
130888
+ }
130889
+ try {
130890
+ const planPath = path171.join(workingDirectory, ".swarm", "plan.json");
130891
+ const planRaw = fs128.readFileSync(planPath, "utf-8");
130892
+ const planObj = JSON.parse(planRaw);
130893
+ if (planObj.swarm && planObj.title) {
130894
+ const planId = derivePlanId(planObj);
130895
+ const profile = getProfile(workingDirectory, planId);
130896
+ if (!profile || !profile.gates.council_mode) {
130897
+ return { blocked: false, reason: "", active: false };
130898
+ }
130899
+ }
130900
+ } catch {
130901
+ return { blocked: false, reason: "", active: false };
130902
+ }
130903
+ let evidence;
130904
+ try {
130905
+ evidence = readTaskEvidenceRaw(workingDirectory, taskId);
130906
+ } catch {
130907
+ return {
130908
+ blocked: true,
130909
+ reason: "council gate required but not yet run — architect must call submit_council_verdicts before advancing this task",
130910
+ active: true
130911
+ };
130912
+ }
130913
+ const councilGate = evidence?.gates?.council;
130914
+ if (!councilGate) {
130915
+ return {
130916
+ blocked: true,
130917
+ reason: "council gate required but not yet run — architect must call submit_council_verdicts before advancing this task",
130918
+ active: true
130919
+ };
130920
+ }
130921
+ if (councilGate.verdict === "REJECT") {
130922
+ return {
130923
+ blocked: true,
130924
+ reason: "council gate blocked advancement — resolve requiredFixes and re-run submit_council_verdicts",
130925
+ active: true
130926
+ };
130927
+ }
130928
+ const rawQuorumSize = councilGate.quorumSize;
130929
+ const quorumSize = typeof rawQuorumSize === "number" && Number.isFinite(rawQuorumSize) && rawQuorumSize >= 1 ? rawQuorumSize : 1;
130930
+ if (quorumSize < effectiveMinimum) {
130931
+ return {
130932
+ blocked: true,
130933
+ reason: `council gate blocked advancement — recorded verdict has insufficient quorum (${quorumSize} of ${effectiveMinimum} required members). Re-run submit_council_verdicts with the missing council members.`,
130934
+ active: true
130935
+ };
130936
+ }
130937
+ return { blocked: false, reason: "", active: true };
130938
+ }
130723
130939
  async function executeUpdateTaskStatus(args2, fallbackDir, ctx) {
130724
130940
  const statusError = validateStatus(args2.status);
130725
130941
  if (statusError) {
@@ -130821,7 +131037,15 @@ async function executeUpdateTaskStatus(args2, fallbackDir, ctx) {
130821
131037
  phaseRequiresReviewer = false;
130822
131038
  }
130823
131039
  } catch {}
130824
- if (phaseRequiresReviewer) {
131040
+ const councilCheck = checkCouncilGate(directory, args2.task_id);
131041
+ if (councilCheck.blocked) {
131042
+ return {
131043
+ success: false,
131044
+ message: "Gate check failed: council gate not yet satisfied for task " + args2.task_id,
131045
+ errors: [councilCheck.reason]
131046
+ };
131047
+ }
131048
+ if (phaseRequiresReviewer && !councilCheck.active) {
130825
131049
  const reviewerCheck = await checkReviewerGateWithScope(args2.task_id, directory, ctx?.sessionID, fallbackDir);
130826
131050
  if (reviewerCheck.blocked) {
130827
131051
  return {
@@ -131601,7 +131825,7 @@ var VerdictSchema3 = exports_external.object({
131601
131825
  verdict: exports_external.enum(["APPROVE", "CONCERNS", "REJECT"]),
131602
131826
  confidence: exports_external.number().min(0).max(1),
131603
131827
  findings: exports_external.array(exports_external.object({
131604
- severity: exports_external.enum(["HIGH", "MEDIUM", "LOW"]),
131828
+ severity: exports_external.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW"]),
131605
131829
  category: exports_external.string().min(1),
131606
131830
  location: exports_external.string(),
131607
131831
  detail: exports_external.string(),
@@ -131655,6 +131879,17 @@ async function executeWriteFinalCouncilEvidence(args2, directory) {
131655
131879
  }, null, 2);
131656
131880
  }
131657
131881
  const synthesis = synthesizeFinalCouncilAdvisory(input.projectSummary.trim(), input.verdicts, input.roundNumber ?? 1, config3.council);
131882
+ if (synthesis.overallVerdict === "CONCERNS" && synthesis.blockingConcernsCount > 0) {
131883
+ return JSON.stringify({
131884
+ success: false,
131885
+ reason: "blocking_concerns_unresolved",
131886
+ overallVerdict: synthesis.overallVerdict,
131887
+ blockingConcernsCount: synthesis.blockingConcernsCount,
131888
+ requiredFixes: synthesis.requiredFixes,
131889
+ unifiedFeedbackMd: synthesis.unifiedFeedbackMd,
131890
+ message: `Final council returned CONCERNS with ${synthesis.blockingConcernsCount} HIGH/CRITICAL finding(s) promoted to requiredFixes. These must be resolved before the project can close. Do NOT write evidence or proceed — address every requiredFix and resubmit.`
131891
+ }, null, 2);
131892
+ }
131658
131893
  const plan = await loadPlan(directory);
131659
131894
  const planId = plan ? derivePlanId(plan) : "unknown";
131660
131895
  const normalizedVerdict = normalizeFinalVerdict(synthesis.overallVerdict, synthesis.requiredFixes.length);