opencode-swarm 7.6.0 → 7.7.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
@@ -33,7 +33,7 @@ var package_default;
33
33
  var init_package = __esm(() => {
34
34
  package_default = {
35
35
  name: "opencode-swarm",
36
- version: "7.6.0",
36
+ version: "7.7.0",
37
37
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
38
38
  main: "dist/index.js",
39
39
  types: "dist/index.d.ts",
@@ -164,7 +164,8 @@ var init_tool_names = __esm(() => {
164
164
  "get_qa_gate_profile",
165
165
  "set_qa_gates",
166
166
  "web_search",
167
- "convene_general_council"
167
+ "convene_general_council",
168
+ "write_final_council_evidence"
168
169
  ];
169
170
  TOOL_NAME_SET = new Set(TOOL_NAMES);
170
171
  });
@@ -464,7 +465,8 @@ var init_constants = __esm(() => {
464
465
  "get_qa_gate_profile",
465
466
  "set_qa_gates",
466
467
  "convene_general_council",
467
- "web_search"
468
+ "web_search",
469
+ "write_final_council_evidence"
468
470
  ],
469
471
  explorer: [
470
472
  "complexity_hotspots",
@@ -662,6 +664,7 @@ var init_constants = __esm(() => {
662
664
  write_retro: "document phase retrospectives via phase_complete workflow, capture lessons learned",
663
665
  write_drift_evidence: "write drift verification evidence for a completed phase",
664
666
  write_hallucination_evidence: "write hallucination verification evidence for a completed phase",
667
+ write_final_council_evidence: "write final council evidence for project completion",
665
668
  declare_scope: "declare file scope for next coder delegation",
666
669
  phase_complete: "mark a phase as complete and track dispatched agents",
667
670
  save_plan: "save a structured implementation plan",
@@ -690,8 +693,8 @@ var init_constants = __esm(() => {
690
693
  lint_spec: "validate .swarm/spec.md format and required fields",
691
694
  get_approved_plan: "retrieve the last critic-approved immutable plan snapshot for baseline drift comparison",
692
695
  repo_map: "query the repo code graph: importers, dependencies, blast radius, and localization context for structural awareness before refactoring",
693
- get_qa_gate_profile: "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), lock state, and profile hash. Read-only.",
694
- set_qa_gates: "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.",
696
+ get_qa_gate_profile: "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.",
697
+ set_qa_gates: "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.",
695
698
  req_coverage: "query requirement coverage status for tracked functional requirements"
696
699
  };
697
700
  for (const [agentName, tools] of Object.entries(AGENT_TOOL_MAP)) {
@@ -15487,12 +15490,7 @@ var init_schema = __esm(() => {
15487
15490
  maxConcurrentTasks: exports_external.number().int().min(1).max(64).default(1),
15488
15491
  evidenceLockTimeoutMs: exports_external.number().int().min(1000).max(300000).default(60000),
15489
15492
  max_coders: exports_external.number().int().min(1).max(16).default(3),
15490
- max_reviewers: exports_external.number().int().min(1).max(16).default(2),
15491
- stageB: exports_external.object({
15492
- parallel: exports_external.object({
15493
- enabled: exports_external.boolean().default(false)
15494
- }).default({ enabled: false })
15495
- }).default({ parallel: { enabled: false } })
15493
+ max_reviewers: exports_external.number().int().min(1).max(16).default(2)
15496
15494
  });
15497
15495
  PluginConfigSchema = exports_external.object({
15498
15496
  agents: exports_external.record(exports_external.string(), AgentOverrideConfigSchema).optional(),
@@ -16751,6 +16749,11 @@ var init_spec_hash = __esm(() => {
16751
16749
  };
16752
16750
  });
16753
16751
 
16752
+ // src/plan/utils.ts
16753
+ function derivePlanId(plan) {
16754
+ return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16755
+ }
16756
+
16754
16757
  // src/plan/ledger.ts
16755
16758
  import * as crypto2 from "node:crypto";
16756
16759
  import * as fs4 from "node:fs";
@@ -16939,7 +16942,7 @@ async function takeSnapshotEvent(directory, plan, options) {
16939
16942
  if (options?.approvalMetadata) {
16940
16943
  snapshotPayload.approval = options.approvalMetadata;
16941
16944
  }
16942
- const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16945
+ const planId = derivePlanId(plan);
16943
16946
  return appendLedgerEvent(directory, {
16944
16947
  event_type: "snapshot",
16945
16948
  source: options?.source ?? "takeSnapshotEvent",
@@ -17134,7 +17137,7 @@ async function loadLastApprovedPlan(directory, expectedPlanId) {
17134
17137
  continue;
17135
17138
  }
17136
17139
  if (expectedPlanId !== undefined) {
17137
- const payloadPlanId = `${payload.plan.swarm}-${payload.plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
17140
+ const payloadPlanId = derivePlanId(payload.plan);
17138
17141
  if (payloadPlanId !== expectedPlanId) {
17139
17142
  continue;
17140
17143
  }
@@ -17335,7 +17338,7 @@ async function loadPlan(directory) {
17335
17338
  if (!startupLedgerCheckedWorkspaces.has(resolvedWorkspace)) {
17336
17339
  startupLedgerCheckedWorkspaces.add(resolvedWorkspace);
17337
17340
  if (ledgerHash !== "" && planHash !== ledgerHash) {
17338
- const currentPlanId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
17341
+ const currentPlanId = derivePlanId(validated);
17339
17342
  const ledgerEvents = await readLedgerEvents(directory);
17340
17343
  const firstEvent = ledgerEvents.length > 0 ? ledgerEvents[0] : null;
17341
17344
  if (firstEvent && firstEvent.plan_id !== currentPlanId) {
@@ -17418,7 +17421,7 @@ async function loadPlan(directory) {
17418
17421
  try {
17419
17422
  const rawParsed = JSON.parse(planJsonContent);
17420
17423
  if (typeof rawParsed?.swarm === "string" && typeof rawParsed?.title === "string") {
17421
- rawPlanId = `${rawParsed.swarm}-${rawParsed.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
17424
+ rawPlanId = derivePlanId(rawParsed);
17422
17425
  }
17423
17426
  } catch {}
17424
17427
  if (await ledgerExists(directory)) {
@@ -17544,7 +17547,7 @@ async function savePlan(directory, plan, options) {
17544
17547
  }
17545
17548
  }
17546
17549
  const currentPlan = await _internals6.loadPlanJsonOnly(directory);
17547
- const planId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
17550
+ const planId = derivePlanId(validated);
17548
17551
  const planHashForInit = computePlanHash(validated);
17549
17552
  if (!await ledgerExists(directory)) {
17550
17553
  try {
@@ -17645,7 +17648,7 @@ async function savePlan(directory, plan, options) {
17645
17648
  const oldTask = oldTaskMap.get(task.id);
17646
17649
  if (oldTask && oldTask.status !== task.status) {
17647
17650
  const eventInput = {
17648
- plan_id: `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_"),
17651
+ plan_id: derivePlanId(validated),
17649
17652
  event_type: "task_status_changed",
17650
17653
  task_id: task.id,
17651
17654
  phase_id: phase.id,
@@ -20548,7 +20551,8 @@ var init_qa_gate_profile = __esm(() => {
20548
20551
  sast_enabled: true,
20549
20552
  mutation_test: false,
20550
20553
  council_general_review: false,
20551
- drift_check: true
20554
+ drift_check: true,
20555
+ final_council: false
20552
20556
  };
20553
20557
  });
20554
20558
 
@@ -25820,7 +25824,7 @@ function createDelegationGateHook(config2, directory) {
25820
25824
  hasReviewer = true;
25821
25825
  if (targetAgent === "test_engineer")
25822
25826
  hasTestEngineer = true;
25823
- const stageBParallelEnabled = config2.parallelization?.stageB?.parallel?.enabled === true;
25827
+ const stageBParallelEnabled = true;
25824
25828
  if (stageBParallelEnabled) {
25825
25829
  if ((targetAgent === "reviewer" || targetAgent === "test_engineer") && session.taskWorkflowStates) {
25826
25830
  const stageBEligibleStates = [
@@ -25846,6 +25850,20 @@ function createDelegationGateHook(config2, directory) {
25846
25850
  } catch (err2) {
25847
25851
  warn(`[delegation-gate] toolAfter stage-b-parallel: could not advance ${taskId} (${eligibleState}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
25848
25852
  }
25853
+ } else {
25854
+ try {
25855
+ if (targetAgent === "reviewer" && (eligibleState === "coder_delegated" || eligibleState === "pre_check_passed")) {
25856
+ advanceTaskState(session, taskId, "reviewer_run", {
25857
+ telemetrySessionId: input.sessionID
25858
+ });
25859
+ } else if (targetAgent === "test_engineer" && eligibleState === "reviewer_run") {
25860
+ advanceTaskState(session, taskId, "tests_run", {
25861
+ telemetrySessionId: input.sessionID
25862
+ });
25863
+ }
25864
+ } catch (err2) {
25865
+ warn(`[delegation-gate] toolAfter stage-b-parallel intermediate: could not advance ${taskId} (${eligibleState}) after ${targetAgent}: ${err2 instanceof Error ? err2.message : String(err2)}`);
25866
+ }
25849
25867
  }
25850
25868
  }
25851
25869
  const seedTaskId = getSeedTaskId(session);
@@ -25875,74 +25893,17 @@ function createDelegationGateHook(config2, directory) {
25875
25893
  } catch (err2) {
25876
25894
  warn(`[delegation-gate] toolAfter cross-session stage-b-parallel: could not advance ${seedTaskId} (${seedEligibleState}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
25877
25895
  }
25878
- }
25879
- }
25880
- }
25881
- }
25882
- } else {
25883
- if (targetAgent === "reviewer" && session.taskWorkflowStates) {
25884
- for (const [taskId, state] of session.taskWorkflowStates) {
25885
- if (state === "coder_delegated" || state === "pre_check_passed") {
25886
- try {
25887
- advanceTaskState(session, taskId, "reviewer_run", {
25888
- telemetrySessionId: input.sessionID
25889
- });
25890
- } catch (err2) {
25891
- warn(`[delegation-gate] toolAfter: could not advance ${taskId} (${state}) → reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
25892
- }
25893
- }
25894
- }
25895
- }
25896
- if (targetAgent === "test_engineer" && session.taskWorkflowStates) {
25897
- for (const [taskId, state] of session.taskWorkflowStates) {
25898
- if (state === "reviewer_run") {
25899
- try {
25900
- advanceTaskState(session, taskId, "tests_run", {
25901
- telemetrySessionId: input.sessionID
25902
- });
25903
- } catch (err2) {
25904
- warn(`[delegation-gate] toolAfter: could not advance ${taskId} (${state}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
25905
- }
25906
- }
25907
- }
25908
- }
25909
- if (targetAgent === "reviewer" || targetAgent === "test_engineer") {
25910
- for (const [, otherSession] of swarmState.agentSessions) {
25911
- if (otherSession === session)
25912
- continue;
25913
- if (!otherSession.taskWorkflowStates)
25914
- continue;
25915
- if (targetAgent === "reviewer") {
25916
- const seedTaskId = getSeedTaskId(session);
25917
- if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
25918
- otherSession.taskWorkflowStates.set(seedTaskId, "coder_delegated");
25919
- }
25920
- for (const [taskId, state] of otherSession.taskWorkflowStates) {
25921
- if (state === "coder_delegated" || state === "pre_check_passed") {
25922
- try {
25923
- advanceTaskState(otherSession, taskId, "reviewer_run", {
25924
- emitTelemetry: false
25925
- });
25926
- } catch (err2) {
25927
- warn(`[delegation-gate] toolAfter cross-session: could not advance ${taskId} (${state}) → reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
25928
- }
25929
- }
25930
- }
25931
- }
25932
- if (targetAgent === "test_engineer") {
25933
- const seedTaskId = getSeedTaskId(session);
25934
- if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
25935
- otherSession.taskWorkflowStates.set(seedTaskId, "reviewer_run");
25936
- }
25937
- for (const [taskId, state] of otherSession.taskWorkflowStates) {
25938
- if (state === "reviewer_run") {
25939
- try {
25940
- advanceTaskState(otherSession, taskId, "tests_run", {
25896
+ } else {
25897
+ try {
25898
+ if (targetAgent === "reviewer" && (seedEligibleState === "coder_delegated" || seedEligibleState === "pre_check_passed")) {
25899
+ advanceTaskState(otherSession, seedTaskId, "reviewer_run", { emitTelemetry: false });
25900
+ } else if (targetAgent === "test_engineer" && seedEligibleState === "reviewer_run") {
25901
+ advanceTaskState(otherSession, seedTaskId, "tests_run", {
25941
25902
  emitTelemetry: false
25942
25903
  });
25943
- } catch (err2) {
25944
- warn(`[delegation-gate] toolAfter cross-session: could not advance ${taskId} (${state}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
25945
25904
  }
25905
+ } catch (err2) {
25906
+ warn(`[delegation-gate] toolAfter cross-session stage-b-parallel intermediate: could not advance ${seedTaskId} (${seedEligibleState}) after ${targetAgent}: ${err2 instanceof Error ? err2.message : String(err2)}`);
25946
25907
  }
25947
25908
  }
25948
25909
  }
@@ -25953,7 +25914,7 @@ function createDelegationGateHook(config2, directory) {
25953
25914
  if (typeof subagentType === "string") {
25954
25915
  try {
25955
25916
  const rawTaskId = directArgs?.task_id;
25956
- const evidenceTaskId = typeof rawTaskId === "string" && rawTaskId.length <= 20 && /^\d+\.\d+$/.test(rawTaskId.trim()) ? rawTaskId.trim() : await getEvidenceTaskId(session, directory);
25917
+ const evidenceTaskId = typeof rawTaskId === "string" && rawTaskId.length <= 20 && isStrictTaskId(rawTaskId.trim()) ? rawTaskId.trim() : await getEvidenceTaskId(session, directory);
25957
25918
  if (evidenceTaskId) {
25958
25919
  const turbo = hasActiveTurboMode(input.sessionID);
25959
25920
  const gateAgents = [
@@ -26076,7 +26037,7 @@ function createDelegationGateHook(config2, directory) {
26076
26037
  }
26077
26038
  try {
26078
26039
  const rawTaskId = directArgs?.task_id;
26079
- const evidenceTaskId = typeof rawTaskId === "string" && rawTaskId.length <= 20 && /^\d+\.\d+$/.test(rawTaskId.trim()) ? rawTaskId.trim() : await getEvidenceTaskId(session, directory);
26040
+ const evidenceTaskId = typeof rawTaskId === "string" && rawTaskId.length <= 20 && isStrictTaskId(rawTaskId.trim()) ? rawTaskId.trim() : await getEvidenceTaskId(session, directory);
26080
26041
  if (evidenceTaskId) {
26081
26042
  const turbo = hasActiveTurboMode(input.sessionID);
26082
26043
  if (hasReviewer) {
@@ -26322,6 +26283,7 @@ var init_delegation_gate = __esm(() => {
26322
26283
  init_state();
26323
26284
  init_telemetry();
26324
26285
  init_logger();
26286
+ init_task_id();
26325
26287
  init_guardrails();
26326
26288
  init_normalize_tool_name();
26327
26289
  init_utils2();
@@ -26796,9 +26758,6 @@ function hasBothStageBCompletions(session, taskId) {
26796
26758
  return false;
26797
26759
  return completions.has("reviewer") && completions.has("test_engineer");
26798
26760
  }
26799
- function derivePlanIdFromPlan(plan) {
26800
- return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
26801
- }
26802
26761
  async function isCouncilGateActive(directory, council) {
26803
26762
  const enabled = council?.enabled === true;
26804
26763
  let plan = null;
@@ -26810,7 +26769,7 @@ async function isCouncilGateActive(directory, council) {
26810
26769
  if (!plan) {
26811
26770
  return false;
26812
26771
  }
26813
- const planId = derivePlanIdFromPlan(plan);
26772
+ const planId = derivePlanId(plan);
26814
26773
  let profile = null;
26815
26774
  try {
26816
26775
  profile = getProfile(directory, planId);
@@ -53956,9 +53915,6 @@ var init_promote = __esm(() => {
53956
53915
  });
53957
53916
 
53958
53917
  // src/commands/qa-gates.ts
53959
- function derivePlanId(plan) {
53960
- return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
53961
- }
53962
53918
  function isGateName(name2) {
53963
53919
  return ALL_GATE_NAMES.includes(name2);
53964
53920
  }
@@ -54084,7 +54040,8 @@ var init_qa_gates = __esm(() => {
54084
54040
  "sast_enabled",
54085
54041
  "mutation_test",
54086
54042
  "council_general_review",
54087
- "drift_check"
54043
+ "drift_check",
54044
+ "final_council"
54088
54045
  ];
54089
54046
  });
54090
54047
 
@@ -55148,7 +55105,7 @@ async function handleRollbackCommand(directory, args2) {
55148
55105
  if (fs27.existsSync(planJsonPath)) {
55149
55106
  const planRaw = fs27.readFileSync(planJsonPath, "utf-8");
55150
55107
  const plan = PlanSchema.parse(JSON.parse(planRaw));
55151
- const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
55108
+ const planId = derivePlanId(plan);
55152
55109
  const planHash = computePlanHash(plan);
55153
55110
  await initLedger(directory, planId, planHash, plan);
55154
55111
  await appendLedgerEvent(directory, {
@@ -56680,7 +56637,7 @@ function buildQaGateSelectionDialogue(modeLabel) {
56680
56637
  const leadIn = modeLabel === "BRAINSTORM" ? "Now ask the user which QA gates to enable for this plan — do not select on their behalf." : modeLabel === "SPECIFY" ? "Ask the user which QA gates to enable for this plan before suggesting the next step." : "No pending gate selection found in `.swarm/context.md`. Ask the user inline now.";
56681
56638
  return `${leadIn}
56682
56639
 
56683
- Present the ten gates with their defaults (DEFAULT_QA_GATES) as a single user-facing question. Offer the user a one-shot choice: accept defaults, or customize. The ten gates are:
56640
+ Present the eleven gates with their defaults (DEFAULT_QA_GATES) as a single user-facing question. Offer the user a one-shot choice: accept defaults, or customize. The eleven gates are:
56684
56641
  - reviewer (default: ON) — code review of coder output
56685
56642
  - test_engineer (default: ON) — test verification of coder output
56686
56643
  - sme_enabled (default: ON) — SME consultation during planning/clarification
@@ -56691,8 +56648,20 @@ Present the ten gates with their defaults (DEFAULT_QA_GATES) as a single user-fa
56691
56648
  - 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)
56692
56649
  - 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.
56693
56650
  - 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)
56651
+ - final_council (default: OFF) — when enabled, after all phases complete the architect convenes a holistic general council review of the entire body of work before project close. Requires council.general.enabled: true in plugin config. Recommended for multi-phase projects with high architectural complexity.
56652
+
56653
+ One question, one message, defaults pre-stated. Wait for the user's answer.
56694
56654
 
56695
- One question, one message, defaults pre-stated. Wait for the user's answer.`;
56655
+ 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:
56656
+ \`\`\`
56657
+ ## Pending Parallelization Config
56658
+ - parallelization_enabled: true
56659
+ - max_concurrent_tasks: <user's number>
56660
+ - council_parallel: false
56661
+ - locked: true
56662
+ - recorded_at: <ISO timestamp>
56663
+ \`\`\`
56664
+ If the user accepts the default (1), skip writing this section entirely — serial execution is the default and needs no config.`;
56696
56665
  }
56697
56666
  function buildAvailableToolsList(council) {
56698
56667
  const tools = AGENT_TOOL_MAP.architect ?? [];
@@ -57529,6 +57498,7 @@ Do NOT call \`set_qa_gates\` yet — \`plan.json\` does not exist at this point.
57529
57498
  - mutation_test: <true|false>
57530
57499
  - council_general_review: <true|false>
57531
57500
  - drift_check: <true|false>
57501
+ - final_council: <true|false>
57532
57502
  - recorded_at: <ISO timestamp>
57533
57503
  \`\`\`
57534
57504
  MODE: PLAN applies these after \`save_plan\` succeeds via \`set_qa_gates\`.
@@ -57607,6 +57577,7 @@ Do NOT call \`set_qa_gates\` yet — \`plan.json\` does not exist at this point.
57607
57577
  - mutation_test: <true|false>
57608
57578
  - council_general_review: <true|false>
57609
57579
  - drift_check: <true|false>
57580
+ - final_council: <true|false>
57610
57581
  - recorded_at: <ISO timestamp>
57611
57582
  \`\`\`
57612
57583
  MODE: PLAN will read this section after \`save_plan\` succeeds and persist via \`set_qa_gates\`.
@@ -57989,6 +57960,7 @@ save_plan({
57989
57960
  **POST-SAVE_PLAN: APPLY QA GATE SELECTION.**
57990
57961
  After \`save_plan\` succeeds, read \`.swarm/context.md\`:
57991
57962
  - If a \`## Pending QA Gate Selection\` section exists: parse the gate values, call \`set_qa_gates\` with those flags, confirm with the user ("QA gates applied: <list>"), then remove the section from context.md.
57963
+ - If a \`## Pending Parallelization Config\` section also exists: parse the values and call \`save_plan\` again with \`execution_profile\` set to \`{ parallelization_enabled: <parsed>, max_concurrent_tasks: <parsed>, council_parallel: false, locked: true }\`. Then remove the section from context.md. If the plan already had \`execution_profile.locked: true\`, skip this step — the profile is already locked and immutable.
57992
57964
  - If no pending section exists: {{QA_GATE_DIALOGUE_PLAN}}
57993
57965
  <!-- BEHAVIORAL_GUIDANCE_START -->
57994
57966
  INLINE GATE SELECTION — no pending section found in context.md. You MUST ask now.
@@ -58343,6 +58315,19 @@ The tool will automatically write the retrospective to \`.swarm/evidence/retro-{
58343
58315
  - \`.swarm/evidence/{phase}/mutation-gate.json\` exists with verdict 'pass' or 'warn' (written by YOU via the \`write_mutation_evidence\` tool after step 5.56) — ONLY required when \`mutation_test\` is enabled in the QA gate profile
58344
58316
  If any required file is missing, run the missing gate first. Turbo mode skips all gates automatically.
58345
58317
  NOTE: Steps 5.5, 5.55, and 5.56 are enforced by runtime hooks. If \`hallucination_guard\` is enabled and you skip the critic_hallucination_verifier delegation (or fail to call \`write_hallucination_evidence\`), phase_complete will be BLOCKED by the plugin. Similarly, if \`mutation_test\` is enabled and you skip step 5.56 (or fail to call \`write_mutation_evidence\`), phase_complete will be BLOCKED. These are not suggestions — they are hard enforcement mechanisms.
58318
+ 5.7. **Final Council (conditional on QA gate — last phase only)**: Check whether \`final_council\` is enabled in the effective QA gate profile (visible via \`get_qa_gate_profile\`). If disabled, skip silently and proceed to step 6.
58319
+ If enabled AND this is the LAST phase in the plan (all other phases have status 'complete' and no more phases remain):
58320
+ 1. Verify \`council.general.enabled: true\` in plugin config. If not enabled, warn the user: "final_council gate is enabled but General Council is not configured. Skipping final council." Then proceed to step 6. Check that \`convene_general_council\` is available in your tool list. If the tool is unavailable (filtered by config), warn the user and skip.
58321
+ 2. Run 1-3 targeted \`web_search\` queries relevant to the project domain.
58322
+ 3. Compile a RESEARCH CONTEXT block from search results.
58323
+ 4. Dispatch \`{{AGENT_PREFIX}}council_generalist\`, \`{{AGENT_PREFIX}}council_skeptic\`, and \`{{AGENT_PREFIX}}council_domain_expert\` in PARALLEL. Pass: the full body of work (all phase summaries, all evidence artifacts), the RESEARCH CONTEXT block, round number 1. Instruction: "Review the entire body of work holistically. Identify cross-cutting issues, architectural coherence, and overall quality."
58324
+ 5. Collect all three JSON responses.
58325
+ 6. Call \`convene_general_council\` with mode: 'general', the project summary as question, and the collected round1Responses.
58326
+ 7. If disagreements exist, re-dispute as in MODE: COUNCIL step 5-6.
58327
+ 8. Present the final synthesis to the user as a project-close summary.
58328
+ 9. Write the final council result to \`.swarm/evidence/final-council.json\`.
58329
+ 10. Do NOT call \`/swarm close\` until the final council completes (if enabled). The evidence file \`.swarm/evidence/final-council.json\` must exist with an APPROVED verdict before \`/swarm close\` is permitted when final_council is enabled.
58330
+ If enabled but NOT the last phase, skip silently — final council only runs once, after all phases.
58346
58331
  6. Summarize to user
58347
58332
  7. Ask: "Ready for Phase [N+1]?"
58348
58333
 
@@ -65662,7 +65647,7 @@ var init_curator_drift = __esm(() => {
65662
65647
  init_package();
65663
65648
  init_agents2();
65664
65649
  init_critic();
65665
- import * as path115 from "node:path";
65650
+ import * as path116 from "node:path";
65666
65651
 
65667
65652
  // src/background/index.ts
65668
65653
  init_event_bus();
@@ -78855,7 +78840,7 @@ var diff = createSwarmTool({
78855
78840
  encoding: "utf-8",
78856
78841
  timeout: 3000,
78857
78842
  cwd: directory,
78858
- stdio: "pipe"
78843
+ stdio: ["ignore", "pipe", "pipe"]
78859
78844
  });
78860
78845
  return true;
78861
78846
  } catch (e) {
@@ -78869,9 +78854,9 @@ var diff = createSwarmTool({
78869
78854
  }, getContentFromRef = function(refPath) {
78870
78855
  return child_process7.execFileSync("git", ["show", refPath], {
78871
78856
  encoding: "utf-8",
78872
- timeout: 5000,
78857
+ timeout: 15000,
78873
78858
  cwd: directory,
78874
- stdio: "pipe"
78859
+ stdio: ["ignore", "pipe", "pipe"]
78875
78860
  });
78876
78861
  };
78877
78862
  if (!directory || typeof directory !== "string" || directory.trim() === "") {
@@ -78922,13 +78907,15 @@ var diff = createSwarmTool({
78922
78907
  encoding: "utf-8",
78923
78908
  timeout: DIFF_TIMEOUT_MS,
78924
78909
  maxBuffer: MAX_BUFFER_BYTES,
78925
- cwd: directory
78910
+ cwd: directory,
78911
+ stdio: ["ignore", "pipe", "pipe"]
78926
78912
  });
78927
78913
  const fullDiffOutput = child_process7.execFileSync("git", fullDiffArgs, {
78928
78914
  encoding: "utf-8",
78929
78915
  timeout: DIFF_TIMEOUT_MS,
78930
78916
  maxBuffer: MAX_BUFFER_BYTES,
78931
- cwd: directory
78917
+ cwd: directory,
78918
+ stdio: ["ignore", "pipe", "pipe"]
78932
78919
  });
78933
78920
  const files = [];
78934
78921
  const numstatLines = numstatOutput.split(`
@@ -79697,9 +79684,6 @@ function summarizePlan(plan) {
79697
79684
  }))
79698
79685
  };
79699
79686
  }
79700
- function derivePlanId2(plan) {
79701
- return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
79702
- }
79703
79687
  async function executeGetApprovedPlan(args2, directory) {
79704
79688
  const currentPlan = await loadPlanJsonOnly(directory);
79705
79689
  if (!currentPlan) {
@@ -79718,7 +79702,7 @@ async function executeGetApprovedPlan(args2, directory) {
79718
79702
  reason: "no_approved_snapshot"
79719
79703
  };
79720
79704
  }
79721
- const expectedPlanId = derivePlanId2(currentPlan);
79705
+ const expectedPlanId = derivePlanId(currentPlan);
79722
79706
  const profile = getProfile(directory, expectedPlanId);
79723
79707
  const qaProfileHash = profile ? computeProfileHash(profile) : null;
79724
79708
  const approved = await loadLastApprovedPlan(directory, expectedPlanId);
@@ -79777,9 +79761,6 @@ var get_approved_plan = createSwarmTool({
79777
79761
  init_qa_gate_profile();
79778
79762
  init_manager();
79779
79763
  init_create_tool();
79780
- function derivePlanId3(plan) {
79781
- return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
79782
- }
79783
79764
  async function executeGetQaGateProfile(_args, directory) {
79784
79765
  const plan = await loadPlanJsonOnly(directory);
79785
79766
  if (!plan) {
@@ -79788,7 +79769,7 @@ async function executeGetQaGateProfile(_args, directory) {
79788
79769
  reason: "plan_json_unavailable"
79789
79770
  };
79790
79771
  }
79791
- const planId = derivePlanId3(plan);
79772
+ const planId = derivePlanId(plan);
79792
79773
  const profile = getProfile(directory, planId);
79793
79774
  if (!profile) {
79794
79775
  return {
@@ -80960,7 +80941,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
80960
80941
  }, null, 2);
80961
80942
  }
80962
80943
  if (hasActiveTurboMode(sessionID)) {
80963
- console.warn(`[phase_complete] Turbo mode active — skipping completion-verify, drift-verifier, hallucination-guard, mutation-gate, and phase-council gates for phase ${phase}`);
80944
+ console.warn(`[phase_complete] Turbo mode active — skipping completion-verify, drift-verifier, hallucination-guard, mutation-gate, phase-council, and final-council gates for phase ${phase}`);
80964
80945
  } else {
80965
80946
  try {
80966
80947
  const completionResultRaw = await executeCompletionVerify({ phase }, dir);
@@ -80989,7 +80970,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
80989
80970
  driftHasSpecMd = fs69.existsSync(specMdPath);
80990
80971
  const gatePlan = await loadPlan(dir);
80991
80972
  if (gatePlan) {
80992
- const gatePlanId = `${gatePlan.swarm}-${gatePlan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
80973
+ const gatePlanId = derivePlanId(gatePlan);
80993
80974
  const gateProfile = getProfile(dir, gatePlanId);
80994
80975
  if (gateProfile) {
80995
80976
  const gateSession = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
@@ -81118,7 +81099,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
81118
81099
  try {
81119
81100
  const plan = await loadPlan(dir);
81120
81101
  if (plan) {
81121
- const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
81102
+ const planId = derivePlanId(plan);
81122
81103
  const profile = getProfile(dir, planId);
81123
81104
  if (profile) {
81124
81105
  const session2 = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
@@ -81190,7 +81171,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
81190
81171
  try {
81191
81172
  const plan = await loadPlan(dir);
81192
81173
  if (plan) {
81193
- const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
81174
+ const planId = derivePlanId(plan);
81194
81175
  const profile = getProfile(dir, planId);
81195
81176
  if (profile) {
81196
81177
  const session2 = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
@@ -81263,7 +81244,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
81263
81244
  try {
81264
81245
  const plan = await loadPlan(dir);
81265
81246
  if (plan) {
81266
- const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
81247
+ const planId = derivePlanId(plan);
81267
81248
  const profile = getProfile(dir, planId);
81268
81249
  if (profile) {
81269
81250
  const session2 = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
@@ -81464,6 +81445,127 @@ Advisory notes: ${advisoryNotes.join("; ")}` : "";
81464
81445
  }
81465
81446
  }
81466
81447
  }
81448
+ if (!hasActiveTurboMode(sessionID)) {
81449
+ let finalCouncilEnabled = false;
81450
+ try {
81451
+ const plan = await loadPlan(dir);
81452
+ if (plan) {
81453
+ const lastPhaseId = plan.phases[plan.phases.length - 1]?.id;
81454
+ if (lastPhaseId !== undefined && phase === lastPhaseId) {
81455
+ const planId = derivePlanId(plan);
81456
+ const profile = getProfile(dir, planId);
81457
+ if (profile) {
81458
+ const session2 = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
81459
+ const overrides = session2?.qaGateSessionOverrides ?? {};
81460
+ const effective = getEffectiveGates(profile, overrides);
81461
+ if (effective.final_council === true) {
81462
+ finalCouncilEnabled = true;
81463
+ const fcPath = path90.join(dir, ".swarm", "evidence", "final-council.json");
81464
+ let fcVerdictFound = false;
81465
+ let _fcVerdict;
81466
+ try {
81467
+ const fcContent = fs69.readFileSync(fcPath, "utf-8");
81468
+ const fcBundle = JSON.parse(fcContent);
81469
+ for (const entry of fcBundle.entries ?? []) {
81470
+ if (typeof entry.type === "string" && entry.type === "final-council" && typeof entry.verdict === "string") {
81471
+ fcVerdictFound = true;
81472
+ _fcVerdict = entry.verdict;
81473
+ if (plan) {
81474
+ const currentPlanId = derivePlanId(plan);
81475
+ if (entry.plan_id && entry.plan_id !== currentPlanId) {
81476
+ return JSON.stringify({
81477
+ success: false,
81478
+ phase,
81479
+ status: "blocked",
81480
+ reason: "final_council_plan_mismatch",
81481
+ message: `Final council evidence belongs to a different plan (evidence: ${entry.plan_id}, current: ${currentPlanId}). Re-run the final council.`,
81482
+ agentsDispatched,
81483
+ agentsMissing: [],
81484
+ warnings: []
81485
+ }, null, 2);
81486
+ }
81487
+ if (!entry.plan_id) {
81488
+ return JSON.stringify({
81489
+ success: false,
81490
+ phase,
81491
+ status: "blocked",
81492
+ reason: "FINAL_COUNCIL_PLAN_ID_REQUIRED",
81493
+ message: `Phase ${phase} (last phase) cannot be completed: final council evidence is missing plan_id binding. Re-run the final council to generate evidence with plan identity.`,
81494
+ agentsDispatched,
81495
+ agentsMissing: [],
81496
+ warnings: []
81497
+ }, null, 2);
81498
+ }
81499
+ }
81500
+ if (entry.verdict === "rejected" || entry.verdict === "REJECTED") {
81501
+ return JSON.stringify({
81502
+ success: false,
81503
+ phase,
81504
+ status: "blocked",
81505
+ reason: "FINAL_COUNCIL_REJECTED",
81506
+ message: `Phase ${phase} (last phase) cannot be completed: final council returned verdict 'REJECTED'. Address the required fixes before completing the project.`,
81507
+ agentsDispatched,
81508
+ agentsMissing: [],
81509
+ warnings: []
81510
+ }, null, 2);
81511
+ }
81512
+ if (entry.verdict !== "approved" && entry.verdict !== "APPROVED") {
81513
+ return JSON.stringify({
81514
+ success: false,
81515
+ phase,
81516
+ status: "blocked",
81517
+ reason: "FINAL_COUNCIL_INVALID_VERDICT",
81518
+ message: `Phase ${phase} (last phase) cannot be completed: final council evidence contains unrecognized verdict '${entry.verdict}'. Expected 'approved'.`,
81519
+ agentsDispatched,
81520
+ agentsMissing: [],
81521
+ warnings: []
81522
+ }, null, 2);
81523
+ }
81524
+ }
81525
+ }
81526
+ } catch (readErr) {
81527
+ if (readErr.code !== "ENOENT") {
81528
+ safeWarn(`[phase_complete] Final council evidence unreadable:`, readErr);
81529
+ }
81530
+ fcVerdictFound = false;
81531
+ }
81532
+ if (!fcVerdictFound) {
81533
+ return JSON.stringify({
81534
+ success: false,
81535
+ phase,
81536
+ status: "blocked",
81537
+ reason: "FINAL_COUNCIL_REQUIRED",
81538
+ final_council_required: true,
81539
+ message: `Phase ${phase} (last phase) cannot be completed: final_council is enabled and final council evidence not found at .swarm/evidence/final-council.json. Convene a final holistic council (use convene_general_council with mode 'general') and call write_final_council_evidence to persist the verdict before completing the project.`,
81540
+ agentsDispatched,
81541
+ agentsMissing: [],
81542
+ warnings: [
81543
+ `Final council required — convene a holistic project review using convene_general_council, then call write_final_council_evidence to persist the verdict.`
81544
+ ]
81545
+ }, null, 2);
81546
+ }
81547
+ }
81548
+ }
81549
+ }
81550
+ }
81551
+ } catch (fcError) {
81552
+ if (finalCouncilEnabled) {
81553
+ warnings.push(`FINAL_COUNCIL_ERROR: ${String(fcError)}`);
81554
+ return JSON.stringify({
81555
+ success: false,
81556
+ phase,
81557
+ status: "blocked",
81558
+ reason: "FINAL_COUNCIL_ERROR",
81559
+ message: `Phase ${phase} (last phase) cannot be completed: final council gate encountered an error. Error: ${String(fcError)}`,
81560
+ agentsDispatched,
81561
+ agentsMissing: [],
81562
+ warnings: [`FINAL_COUNCIL_ERROR: ${String(fcError)}`]
81563
+ }, null, 2);
81564
+ } else {
81565
+ safeWarn(`[phase_complete] Final council gate error (non-blocking):`, fcError);
81566
+ }
81567
+ }
81568
+ }
81467
81569
  let knowledgeConfig;
81468
81570
  try {
81469
81571
  knowledgeConfig = KnowledgeConfigSchema.parse(config3.knowledge ?? {});
@@ -86800,7 +86902,10 @@ async function executeSavePlan(args2, fallbackDir) {
86800
86902
  } catch {}
86801
86903
  const hasPendingSection = contextContent.includes("## Pending QA Gate Selection");
86802
86904
  if (!hasPendingSection) {
86803
- const candidatePlanId = `${args2.swarm_id}-${args2.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
86905
+ const candidatePlanId = derivePlanId({
86906
+ swarm: args2.swarm_id,
86907
+ title: args2.title
86908
+ });
86804
86909
  let existingProfile = null;
86805
86910
  try {
86806
86911
  existingProfile = getProfile(targetWorkspace, candidatePlanId);
@@ -86921,7 +87026,7 @@ async function executeSavePlan(args2, fallbackDir) {
86921
87026
  await takeSnapshotEvent(dir, savedPlan).catch(() => {});
86922
87027
  }
86923
87028
  if (resolvedProfile !== undefined && savedPlan) {
86924
- const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
87029
+ const planId = derivePlanId(plan);
86925
87030
  const planHashAfter = computePlanHash(savedPlan);
86926
87031
  const profileChanged = JSON.stringify(resolvedProfile) !== JSON.stringify(preservedExecutionProfile);
86927
87032
  if (profileChanged) {
@@ -88803,9 +88908,6 @@ init_zod();
88803
88908
  init_qa_gate_profile();
88804
88909
  init_manager();
88805
88910
  init_create_tool();
88806
- function derivePlanId4(plan) {
88807
- return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
88808
- }
88809
88911
  async function executeSetQaGates(args2, directory) {
88810
88912
  const plan = await loadPlanJsonOnly(directory);
88811
88913
  if (!plan) {
@@ -88815,7 +88917,7 @@ async function executeSetQaGates(args2, directory) {
88815
88917
  message: "Cannot configure QA gates: plan.json is missing or invalid. " + "Create a plan first (e.g. via /swarm specify or save_plan)."
88816
88918
  };
88817
88919
  }
88818
- const planId = derivePlanId4(plan);
88920
+ const planId = derivePlanId(plan);
88819
88921
  getOrCreateProfile(directory, planId, args2.project_type);
88820
88922
  const partial3 = {};
88821
88923
  for (const key of [
@@ -88828,7 +88930,8 @@ async function executeSetQaGates(args2, directory) {
88828
88930
  "sast_enabled",
88829
88931
  "mutation_test",
88830
88932
  "council_general_review",
88831
- "drift_check"
88933
+ "drift_check",
88934
+ "final_council"
88832
88935
  ]) {
88833
88936
  if (args2[key] !== undefined)
88834
88937
  partial3[key] = args2[key];
@@ -88876,6 +88979,7 @@ var set_qa_gates = createSwarmTool({
88876
88979
  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."),
88877
88980
  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."),
88878
88981
  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."),
88982
+ final_council: exports_external.boolean().optional().describe("Enable the final_council gate (default: off). When on, " + "after all phases complete the architect runs a holistic " + "general council review against the entire body of work. " + "Requires council.general.enabled: true in plugin config."),
88879
88983
  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.')
88880
88984
  },
88881
88985
  execute: async (args2, directory) => {
@@ -90974,6 +91078,7 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
90974
91078
  } catch {}
90975
91079
  }
90976
91080
  const resolvedDir = workingDirectory ?? process.cwd();
91081
+ let evidenceIncompleteReason = null;
90977
91082
  try {
90978
91083
  const evidence = readTaskEvidenceRaw(resolvedDir, taskId);
90979
91084
  if (evidence === null) {} else if (evidence.required_gates && Array.isArray(evidence.required_gates) && evidence.gates) {
@@ -90982,11 +91087,7 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
90982
91087
  return { blocked: false, reason: "" };
90983
91088
  }
90984
91089
  const missingGates = evidence.required_gates.filter((gate) => evidence.gates[gate] == null);
90985
- telemetry.gateFailed("", "qa_gate", taskId, `Missing gates: [${missingGates.join(", ")}]`);
90986
- return {
90987
- blocked: true,
90988
- reason: `Task ${taskId} is missing required gates: [${missingGates.join(", ")}]. ` + `Required: [${evidence.required_gates.join(", ")}]. ` + `Completed: [${Object.keys(evidence.gates).join(", ")}]. ` + `Delegate the missing gate agents before marking task as completed.`
90989
- };
91090
+ evidenceIncompleteReason = `Task ${taskId} is missing required gates: [${missingGates.join(", ")}]. ` + `Required: [${evidence.required_gates.join(", ")}]. ` + `Completed: [${Object.keys(evidence.gates).join(", ")}]. ` + `Delegate the missing gate agents before marking task as completed.`;
90990
91091
  }
90991
91092
  } catch (error93) {
90992
91093
  console.warn(`[gate-evidence] Evidence file for task ${taskId} is corrupt or unreadable:`, error93 instanceof Error ? error93.message : String(error93));
@@ -91076,23 +91177,18 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
91076
91177
  }
91077
91178
  }
91078
91179
  const currentStateStr = stateEntries.length > 0 ? stateEntries.join(", ") : "no active sessions";
91079
- telemetry.gateFailed("", "qa_gate", taskId, `Missing state: tests_run or complete`);
91180
+ const finalReason = evidenceIncompleteReason ?? `Task ${taskId} has not passed QA gates. Current state by session: [${currentStateStr}]. Missing required state: tests_run or complete in at least one valid session. Do not write directly to plan files — use update_task_status after running the reviewer and test_engineer agents.`;
91181
+ telemetry.gateFailed("", "qa_gate", taskId, evidenceIncompleteReason ? `Missing gates: evidence incomplete` : `Missing state: tests_run or complete`);
91080
91182
  return {
91081
91183
  blocked: true,
91082
- reason: `Task ${taskId} has not passed QA gates. Current state by session: [${currentStateStr}]. Missing required state: tests_run or complete in at least one valid session. Do not write directly to plan files — use update_task_status after running the reviewer and test_engineer agents.`
91184
+ reason: finalReason
91083
91185
  };
91084
91186
  } catch {
91085
91187
  return { blocked: false, reason: "" };
91086
91188
  }
91087
91189
  }
91088
91190
  async function checkReviewerGateWithScope(taskId, workingDirectory) {
91089
- let stageBParallelEnabled = false;
91090
- if (workingDirectory) {
91091
- try {
91092
- const cfg = await loadPluginConfig(workingDirectory);
91093
- stageBParallelEnabled = cfg.parallelization?.stageB?.parallel?.enabled === true;
91094
- } catch {}
91095
- }
91191
+ const stageBParallelEnabled = true;
91096
91192
  const result = checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled);
91097
91193
  const scopeWarning = await validateDiffScope(taskId, workingDirectory).catch(() => null);
91098
91194
  if (!scopeWarning)
@@ -91599,9 +91695,6 @@ init_manager();
91599
91695
  init_create_tool();
91600
91696
  import fs89 from "node:fs";
91601
91697
  import path112 from "node:path";
91602
- function derivePlanId5(plan) {
91603
- return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
91604
- }
91605
91698
  function normalizeVerdict(verdict) {
91606
91699
  switch (verdict) {
91607
91700
  case "APPROVED":
@@ -91688,7 +91781,7 @@ async function executeWriteDriftEvidence(args2, directory) {
91688
91781
  timestamp: snapshotEvent.timestamp
91689
91782
  };
91690
91783
  try {
91691
- const planId = derivePlanId5(currentPlan);
91784
+ const planId = derivePlanId(currentPlan);
91692
91785
  const locked = lockProfile(directory, planId, snapshotEvent.seq);
91693
91786
  qaProfileLocked = {
91694
91787
  plan_id: planId,
@@ -91755,9 +91848,10 @@ var write_drift_evidence = createSwarmTool({
91755
91848
  }
91756
91849
  }
91757
91850
  });
91758
- // src/tools/write-hallucination-evidence.ts
91851
+ // src/tools/write-final-council-evidence.ts
91759
91852
  init_zod();
91760
91853
  init_utils2();
91854
+ init_manager();
91761
91855
  init_create_tool();
91762
91856
  import fs90 from "node:fs";
91763
91857
  import path113 from "node:path";
@@ -91771,7 +91865,7 @@ function normalizeVerdict2(verdict) {
91771
91865
  throw new Error(`Invalid verdict: must be 'APPROVED' or 'NEEDS_REVISION', got '${verdict}'`);
91772
91866
  }
91773
91867
  }
91774
- async function executeWriteHallucinationEvidence(args2, directory) {
91868
+ async function executeWriteFinalCouncilEvidence(args2, directory) {
91775
91869
  const phase = args2.phase;
91776
91870
  if (!Number.isInteger(phase) || phase < 1) {
91777
91871
  return JSON.stringify({
@@ -91797,18 +91891,21 @@ async function executeWriteHallucinationEvidence(args2, directory) {
91797
91891
  }, null, 2);
91798
91892
  }
91799
91893
  const normalizedVerdict = normalizeVerdict2(args2.verdict);
91894
+ const plan = await loadPlan(directory);
91895
+ const planId = plan ? derivePlanId(plan) : "unknown";
91800
91896
  const evidenceEntry = {
91801
- type: "hallucination-verification",
91897
+ type: "final-council",
91898
+ phase,
91899
+ plan_id: planId,
91802
91900
  verdict: normalizedVerdict,
91803
91901
  summary: summary.trim(),
91804
- timestamp: new Date().toISOString(),
91805
- findings: args2.findings
91902
+ timestamp: new Date().toISOString()
91806
91903
  };
91807
91904
  const evidenceContent = {
91808
91905
  entries: [evidenceEntry]
91809
91906
  };
91810
- const filename = "hallucination-guard.json";
91811
- const relativePath = path113.join("evidence", String(phase), filename);
91907
+ const filename = "final-council.json";
91908
+ const relativePath = path113.join("evidence", filename);
91812
91909
  let validatedPath;
91813
91910
  try {
91814
91911
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -91825,6 +91922,115 @@ async function executeWriteHallucinationEvidence(args2, directory) {
91825
91922
  const tempPath = path113.join(evidenceDir, `.${filename}.tmp`);
91826
91923
  await fs90.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
91827
91924
  await fs90.promises.rename(tempPath, validatedPath);
91925
+ return JSON.stringify({
91926
+ success: true,
91927
+ phase,
91928
+ verdict: normalizedVerdict,
91929
+ message: `Final council evidence written to .swarm/evidence/final-council.json`
91930
+ }, null, 2);
91931
+ } catch (error93) {
91932
+ return JSON.stringify({
91933
+ success: false,
91934
+ phase,
91935
+ message: error93 instanceof Error ? error93.message : String(error93)
91936
+ }, null, 2);
91937
+ }
91938
+ }
91939
+ var write_final_council_evidence = createSwarmTool({
91940
+ description: "Write final council evidence for a completed project. Accepts phase, verdict (APPROVED/NEEDS_REVISION), summary, and writes structured evidence to .swarm/evidence/final-council.json. Normalizes verdict to lowercase. Use this after convening a final holistic council to persist the verdict.",
91941
+ args: {
91942
+ phase: exports_external.number().int().min(1).describe("The phase number for the final council verdict (e.g., 1, 2, 3)"),
91943
+ verdict: exports_external.enum(["APPROVED", "NEEDS_REVISION"]).describe("Verdict of the final council: 'APPROVED' or 'NEEDS_REVISION'"),
91944
+ summary: exports_external.string().describe("Human-readable summary of the final council verdict")
91945
+ },
91946
+ execute: async (args2, directory) => {
91947
+ const rawPhase = args2.phase !== undefined ? Number(args2.phase) : 0;
91948
+ try {
91949
+ const writeFinalCouncilEvidenceArgs = {
91950
+ phase: Number(args2.phase),
91951
+ verdict: String(args2.verdict),
91952
+ summary: String(args2.summary ?? "")
91953
+ };
91954
+ return await executeWriteFinalCouncilEvidence(writeFinalCouncilEvidenceArgs, directory);
91955
+ } catch (error93) {
91956
+ return JSON.stringify({
91957
+ success: false,
91958
+ phase: rawPhase,
91959
+ message: error93 instanceof Error ? error93.message : "Unknown error"
91960
+ }, null, 2);
91961
+ }
91962
+ }
91963
+ });
91964
+ // src/tools/write-hallucination-evidence.ts
91965
+ init_zod();
91966
+ init_utils2();
91967
+ init_create_tool();
91968
+ import fs91 from "node:fs";
91969
+ import path114 from "node:path";
91970
+ function normalizeVerdict3(verdict) {
91971
+ switch (verdict) {
91972
+ case "APPROVED":
91973
+ return "approved";
91974
+ case "NEEDS_REVISION":
91975
+ return "rejected";
91976
+ default:
91977
+ throw new Error(`Invalid verdict: must be 'APPROVED' or 'NEEDS_REVISION', got '${verdict}'`);
91978
+ }
91979
+ }
91980
+ async function executeWriteHallucinationEvidence(args2, directory) {
91981
+ const phase = args2.phase;
91982
+ if (!Number.isInteger(phase) || phase < 1) {
91983
+ return JSON.stringify({
91984
+ success: false,
91985
+ phase,
91986
+ message: "Invalid phase: must be a positive integer"
91987
+ }, null, 2);
91988
+ }
91989
+ const validVerdicts = ["APPROVED", "NEEDS_REVISION"];
91990
+ if (!validVerdicts.includes(args2.verdict)) {
91991
+ return JSON.stringify({
91992
+ success: false,
91993
+ phase,
91994
+ message: "Invalid verdict: must be 'APPROVED' or 'NEEDS_REVISION'"
91995
+ }, null, 2);
91996
+ }
91997
+ const summary = args2.summary;
91998
+ if (typeof summary !== "string" || summary.trim().length === 0) {
91999
+ return JSON.stringify({
92000
+ success: false,
92001
+ phase,
92002
+ message: "Invalid summary: must be a non-empty string"
92003
+ }, null, 2);
92004
+ }
92005
+ const normalizedVerdict = normalizeVerdict3(args2.verdict);
92006
+ const evidenceEntry = {
92007
+ type: "hallucination-verification",
92008
+ verdict: normalizedVerdict,
92009
+ summary: summary.trim(),
92010
+ timestamp: new Date().toISOString(),
92011
+ findings: args2.findings
92012
+ };
92013
+ const evidenceContent = {
92014
+ entries: [evidenceEntry]
92015
+ };
92016
+ const filename = "hallucination-guard.json";
92017
+ const relativePath = path114.join("evidence", String(phase), filename);
92018
+ let validatedPath;
92019
+ try {
92020
+ validatedPath = validateSwarmPath(directory, relativePath);
92021
+ } catch (error93) {
92022
+ return JSON.stringify({
92023
+ success: false,
92024
+ phase,
92025
+ message: error93 instanceof Error ? error93.message : "Failed to validate path"
92026
+ }, null, 2);
92027
+ }
92028
+ const evidenceDir = path114.dirname(validatedPath);
92029
+ try {
92030
+ await fs91.promises.mkdir(evidenceDir, { recursive: true });
92031
+ const tempPath = path114.join(evidenceDir, `.${filename}.tmp`);
92032
+ await fs91.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
92033
+ await fs91.promises.rename(tempPath, validatedPath);
91828
92034
  return JSON.stringify({
91829
92035
  success: true,
91830
92036
  phase,
@@ -91870,9 +92076,9 @@ var write_hallucination_evidence = createSwarmTool({
91870
92076
  init_zod();
91871
92077
  init_utils2();
91872
92078
  init_create_tool();
91873
- import fs91 from "node:fs";
91874
- import path114 from "node:path";
91875
- function normalizeVerdict3(verdict) {
92079
+ import fs92 from "node:fs";
92080
+ import path115 from "node:path";
92081
+ function normalizeVerdict4(verdict) {
91876
92082
  switch (verdict) {
91877
92083
  case "PASS":
91878
92084
  return "pass";
@@ -91929,7 +92135,7 @@ async function executeWriteMutationEvidence(args2, directory) {
91929
92135
  message: "Invalid summary: must be a non-empty string"
91930
92136
  }, null, 2);
91931
92137
  }
91932
- const normalizedVerdict = normalizeVerdict3(args2.verdict);
92138
+ const normalizedVerdict = normalizeVerdict4(args2.verdict);
91933
92139
  const evidenceEntry = {
91934
92140
  type: "mutation-gate",
91935
92141
  verdict: normalizedVerdict,
@@ -91945,7 +92151,7 @@ async function executeWriteMutationEvidence(args2, directory) {
91945
92151
  entries: [evidenceEntry]
91946
92152
  };
91947
92153
  const filename = "mutation-gate.json";
91948
- const relativePath = path114.join("evidence", String(phase), filename);
92154
+ const relativePath = path115.join("evidence", String(phase), filename);
91949
92155
  let validatedPath;
91950
92156
  try {
91951
92157
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -91956,12 +92162,12 @@ async function executeWriteMutationEvidence(args2, directory) {
91956
92162
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
91957
92163
  }, null, 2);
91958
92164
  }
91959
- const evidenceDir = path114.dirname(validatedPath);
92165
+ const evidenceDir = path115.dirname(validatedPath);
91960
92166
  try {
91961
- await fs91.promises.mkdir(evidenceDir, { recursive: true });
91962
- const tempPath = path114.join(evidenceDir, `.${filename}.tmp`);
91963
- await fs91.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
91964
- await fs91.promises.rename(tempPath, validatedPath);
92167
+ await fs92.promises.mkdir(evidenceDir, { recursive: true });
92168
+ const tempPath = path115.join(evidenceDir, `.${filename}.tmp`);
92169
+ await fs92.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
92170
+ await fs92.promises.rename(tempPath, validatedPath);
91965
92171
  return JSON.stringify({
91966
92172
  success: true,
91967
92173
  phase,
@@ -92253,7 +92459,7 @@ async function initializeOpenCodeSwarm(ctx) {
92253
92459
  const { PreflightTriggerManager: PTM } = await Promise.resolve().then(() => (init_trigger(), exports_trigger));
92254
92460
  preflightTriggerManager = new PTM(automationConfig);
92255
92461
  const { AutomationStatusArtifact: ASA } = await Promise.resolve().then(() => (init_status_artifact(), exports_status_artifact));
92256
- const swarmDir = path115.resolve(ctx.directory, ".swarm");
92462
+ const swarmDir = path116.resolve(ctx.directory, ".swarm");
92257
92463
  statusArtifact = new ASA(swarmDir);
92258
92464
  statusArtifact.updateConfig(automationConfig.mode, automationConfig.capabilities);
92259
92465
  if (automationConfig.capabilities?.evidence_auto_summaries === true) {
@@ -92412,6 +92618,7 @@ async function initializeOpenCodeSwarm(ctx) {
92412
92618
  write_drift_evidence,
92413
92619
  write_hallucination_evidence,
92414
92620
  write_mutation_evidence,
92621
+ write_final_council_evidence,
92415
92622
  declare_scope
92416
92623
  },
92417
92624
  config: async (opencodeConfig) => {