opencode-swarm 7.5.3 → 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.5.3",
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",
@@ -518,6 +520,7 @@ var init_constants = __esm(() => {
518
520
  "imports",
519
521
  "retrieve_summary",
520
522
  "schema_drift",
523
+ "search",
521
524
  "symbols",
522
525
  "knowledge_recall"
523
526
  ],
@@ -603,6 +606,7 @@ var init_constants = __esm(() => {
603
606
  "imports",
604
607
  "retrieve_summary",
605
608
  "schema_drift",
609
+ "search",
606
610
  "symbols",
607
611
  "todo_extract",
608
612
  "knowledge_recall"
@@ -610,6 +614,7 @@ var init_constants = __esm(() => {
610
614
  designer: [
611
615
  "extract_code_blocks",
612
616
  "retrieve_summary",
617
+ "search",
613
618
  "symbols",
614
619
  "knowledge_recall"
615
620
  ],
@@ -659,6 +664,7 @@ var init_constants = __esm(() => {
659
664
  write_retro: "document phase retrospectives via phase_complete workflow, capture lessons learned",
660
665
  write_drift_evidence: "write drift verification evidence for a completed phase",
661
666
  write_hallucination_evidence: "write hallucination verification evidence for a completed phase",
667
+ write_final_council_evidence: "write final council evidence for project completion",
662
668
  declare_scope: "declare file scope for next coder delegation",
663
669
  phase_complete: "mark a phase as complete and track dispatched agents",
664
670
  save_plan: "save a structured implementation plan",
@@ -687,8 +693,8 @@ var init_constants = __esm(() => {
687
693
  lint_spec: "validate .swarm/spec.md format and required fields",
688
694
  get_approved_plan: "retrieve the last critic-approved immutable plan snapshot for baseline drift comparison",
689
695
  repo_map: "query the repo code graph: importers, dependencies, blast radius, and localization context for structural awareness before refactoring",
690
- 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.",
691
- 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.",
692
698
  req_coverage: "query requirement coverage status for tracked functional requirements"
693
699
  };
694
700
  for (const [agentName, tools] of Object.entries(AGENT_TOOL_MAP)) {
@@ -15484,12 +15490,7 @@ var init_schema = __esm(() => {
15484
15490
  maxConcurrentTasks: exports_external.number().int().min(1).max(64).default(1),
15485
15491
  evidenceLockTimeoutMs: exports_external.number().int().min(1000).max(300000).default(60000),
15486
15492
  max_coders: exports_external.number().int().min(1).max(16).default(3),
15487
- max_reviewers: exports_external.number().int().min(1).max(16).default(2),
15488
- stageB: exports_external.object({
15489
- parallel: exports_external.object({
15490
- enabled: exports_external.boolean().default(false)
15491
- }).default({ enabled: false })
15492
- }).default({ parallel: { enabled: false } })
15493
+ max_reviewers: exports_external.number().int().min(1).max(16).default(2)
15493
15494
  });
15494
15495
  PluginConfigSchema = exports_external.object({
15495
15496
  agents: exports_external.record(exports_external.string(), AgentOverrideConfigSchema).optional(),
@@ -16748,6 +16749,11 @@ var init_spec_hash = __esm(() => {
16748
16749
  };
16749
16750
  });
16750
16751
 
16752
+ // src/plan/utils.ts
16753
+ function derivePlanId(plan) {
16754
+ return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16755
+ }
16756
+
16751
16757
  // src/plan/ledger.ts
16752
16758
  import * as crypto2 from "node:crypto";
16753
16759
  import * as fs4 from "node:fs";
@@ -16936,7 +16942,7 @@ async function takeSnapshotEvent(directory, plan, options) {
16936
16942
  if (options?.approvalMetadata) {
16937
16943
  snapshotPayload.approval = options.approvalMetadata;
16938
16944
  }
16939
- const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16945
+ const planId = derivePlanId(plan);
16940
16946
  return appendLedgerEvent(directory, {
16941
16947
  event_type: "snapshot",
16942
16948
  source: options?.source ?? "takeSnapshotEvent",
@@ -17131,7 +17137,7 @@ async function loadLastApprovedPlan(directory, expectedPlanId) {
17131
17137
  continue;
17132
17138
  }
17133
17139
  if (expectedPlanId !== undefined) {
17134
- const payloadPlanId = `${payload.plan.swarm}-${payload.plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
17140
+ const payloadPlanId = derivePlanId(payload.plan);
17135
17141
  if (payloadPlanId !== expectedPlanId) {
17136
17142
  continue;
17137
17143
  }
@@ -17332,7 +17338,7 @@ async function loadPlan(directory) {
17332
17338
  if (!startupLedgerCheckedWorkspaces.has(resolvedWorkspace)) {
17333
17339
  startupLedgerCheckedWorkspaces.add(resolvedWorkspace);
17334
17340
  if (ledgerHash !== "" && planHash !== ledgerHash) {
17335
- const currentPlanId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
17341
+ const currentPlanId = derivePlanId(validated);
17336
17342
  const ledgerEvents = await readLedgerEvents(directory);
17337
17343
  const firstEvent = ledgerEvents.length > 0 ? ledgerEvents[0] : null;
17338
17344
  if (firstEvent && firstEvent.plan_id !== currentPlanId) {
@@ -17415,7 +17421,7 @@ async function loadPlan(directory) {
17415
17421
  try {
17416
17422
  const rawParsed = JSON.parse(planJsonContent);
17417
17423
  if (typeof rawParsed?.swarm === "string" && typeof rawParsed?.title === "string") {
17418
- rawPlanId = `${rawParsed.swarm}-${rawParsed.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
17424
+ rawPlanId = derivePlanId(rawParsed);
17419
17425
  }
17420
17426
  } catch {}
17421
17427
  if (await ledgerExists(directory)) {
@@ -17541,7 +17547,7 @@ async function savePlan(directory, plan, options) {
17541
17547
  }
17542
17548
  }
17543
17549
  const currentPlan = await _internals6.loadPlanJsonOnly(directory);
17544
- const planId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
17550
+ const planId = derivePlanId(validated);
17545
17551
  const planHashForInit = computePlanHash(validated);
17546
17552
  if (!await ledgerExists(directory)) {
17547
17553
  try {
@@ -17642,7 +17648,7 @@ async function savePlan(directory, plan, options) {
17642
17648
  const oldTask = oldTaskMap.get(task.id);
17643
17649
  if (oldTask && oldTask.status !== task.status) {
17644
17650
  const eventInput = {
17645
- plan_id: `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_"),
17651
+ plan_id: derivePlanId(validated),
17646
17652
  event_type: "task_status_changed",
17647
17653
  task_id: task.id,
17648
17654
  phase_id: phase.id,
@@ -20545,7 +20551,8 @@ var init_qa_gate_profile = __esm(() => {
20545
20551
  sast_enabled: true,
20546
20552
  mutation_test: false,
20547
20553
  council_general_review: false,
20548
- drift_check: true
20554
+ drift_check: true,
20555
+ final_council: false
20549
20556
  };
20550
20557
  });
20551
20558
 
@@ -25817,7 +25824,7 @@ function createDelegationGateHook(config2, directory) {
25817
25824
  hasReviewer = true;
25818
25825
  if (targetAgent === "test_engineer")
25819
25826
  hasTestEngineer = true;
25820
- const stageBParallelEnabled = config2.parallelization?.stageB?.parallel?.enabled === true;
25827
+ const stageBParallelEnabled = true;
25821
25828
  if (stageBParallelEnabled) {
25822
25829
  if ((targetAgent === "reviewer" || targetAgent === "test_engineer") && session.taskWorkflowStates) {
25823
25830
  const stageBEligibleStates = [
@@ -25843,6 +25850,20 @@ function createDelegationGateHook(config2, directory) {
25843
25850
  } catch (err2) {
25844
25851
  warn(`[delegation-gate] toolAfter stage-b-parallel: could not advance ${taskId} (${eligibleState}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
25845
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
+ }
25846
25867
  }
25847
25868
  }
25848
25869
  const seedTaskId = getSeedTaskId(session);
@@ -25872,74 +25893,17 @@ function createDelegationGateHook(config2, directory) {
25872
25893
  } catch (err2) {
25873
25894
  warn(`[delegation-gate] toolAfter cross-session stage-b-parallel: could not advance ${seedTaskId} (${seedEligibleState}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
25874
25895
  }
25875
- }
25876
- }
25877
- }
25878
- }
25879
- } else {
25880
- if (targetAgent === "reviewer" && session.taskWorkflowStates) {
25881
- for (const [taskId, state] of session.taskWorkflowStates) {
25882
- if (state === "coder_delegated" || state === "pre_check_passed") {
25883
- try {
25884
- advanceTaskState(session, taskId, "reviewer_run", {
25885
- telemetrySessionId: input.sessionID
25886
- });
25887
- } catch (err2) {
25888
- warn(`[delegation-gate] toolAfter: could not advance ${taskId} (${state}) → reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
25889
- }
25890
- }
25891
- }
25892
- }
25893
- if (targetAgent === "test_engineer" && session.taskWorkflowStates) {
25894
- for (const [taskId, state] of session.taskWorkflowStates) {
25895
- if (state === "reviewer_run") {
25896
- try {
25897
- advanceTaskState(session, taskId, "tests_run", {
25898
- telemetrySessionId: input.sessionID
25899
- });
25900
- } catch (err2) {
25901
- warn(`[delegation-gate] toolAfter: could not advance ${taskId} (${state}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
25902
- }
25903
- }
25904
- }
25905
- }
25906
- if (targetAgent === "reviewer" || targetAgent === "test_engineer") {
25907
- for (const [, otherSession] of swarmState.agentSessions) {
25908
- if (otherSession === session)
25909
- continue;
25910
- if (!otherSession.taskWorkflowStates)
25911
- continue;
25912
- if (targetAgent === "reviewer") {
25913
- const seedTaskId = getSeedTaskId(session);
25914
- if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
25915
- otherSession.taskWorkflowStates.set(seedTaskId, "coder_delegated");
25916
- }
25917
- for (const [taskId, state] of otherSession.taskWorkflowStates) {
25918
- if (state === "coder_delegated" || state === "pre_check_passed") {
25919
- try {
25920
- advanceTaskState(otherSession, taskId, "reviewer_run", {
25921
- emitTelemetry: false
25922
- });
25923
- } catch (err2) {
25924
- warn(`[delegation-gate] toolAfter cross-session: could not advance ${taskId} (${state}) → reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
25925
- }
25926
- }
25927
- }
25928
- }
25929
- if (targetAgent === "test_engineer") {
25930
- const seedTaskId = getSeedTaskId(session);
25931
- if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
25932
- otherSession.taskWorkflowStates.set(seedTaskId, "reviewer_run");
25933
- }
25934
- for (const [taskId, state] of otherSession.taskWorkflowStates) {
25935
- if (state === "reviewer_run") {
25936
- try {
25937
- 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", {
25938
25902
  emitTelemetry: false
25939
25903
  });
25940
- } catch (err2) {
25941
- warn(`[delegation-gate] toolAfter cross-session: could not advance ${taskId} (${state}) → tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
25942
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)}`);
25943
25907
  }
25944
25908
  }
25945
25909
  }
@@ -25950,7 +25914,7 @@ function createDelegationGateHook(config2, directory) {
25950
25914
  if (typeof subagentType === "string") {
25951
25915
  try {
25952
25916
  const rawTaskId = directArgs?.task_id;
25953
- 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);
25954
25918
  if (evidenceTaskId) {
25955
25919
  const turbo = hasActiveTurboMode(input.sessionID);
25956
25920
  const gateAgents = [
@@ -26073,7 +26037,7 @@ function createDelegationGateHook(config2, directory) {
26073
26037
  }
26074
26038
  try {
26075
26039
  const rawTaskId = directArgs?.task_id;
26076
- 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);
26077
26041
  if (evidenceTaskId) {
26078
26042
  const turbo = hasActiveTurboMode(input.sessionID);
26079
26043
  if (hasReviewer) {
@@ -26319,6 +26283,7 @@ var init_delegation_gate = __esm(() => {
26319
26283
  init_state();
26320
26284
  init_telemetry();
26321
26285
  init_logger();
26286
+ init_task_id();
26322
26287
  init_guardrails();
26323
26288
  init_normalize_tool_name();
26324
26289
  init_utils2();
@@ -26474,7 +26439,7 @@ function startAgentSession(sessionId, agentName, staleDurationMs = 7200000, dire
26474
26439
  if (directory) {
26475
26440
  let rehydrationPromise;
26476
26441
  rehydrationPromise = _internals9.rehydrateSessionFromDisk(directory, sessionState).catch((err2) => {
26477
- console.warn("[state] Rehydration failed:", err2 instanceof Error ? err2.message : String(err2));
26442
+ warn("[state] Rehydration failed:", err2 instanceof Error ? err2.message : String(err2));
26478
26443
  }).finally(() => {
26479
26444
  swarmState.pendingRehydrations.delete(rehydrationPromise);
26480
26445
  });
@@ -26760,7 +26725,7 @@ async function advanceTaskStateAndPersist(session, taskId, newState, directory,
26760
26725
  try {
26761
26726
  await updateTaskStatus(directory, taskId, planStatus);
26762
26727
  } catch (err2) {
26763
- console.warn(`[advanceTaskStateAndPersist] persist ${taskId} → ${planStatus} failed (in-memory state still advanced): ${err2 instanceof Error ? err2.message : String(err2)}`);
26728
+ warn(`[advanceTaskStateAndPersist] persist ${taskId} → ${planStatus} failed (in-memory state still advanced): ${err2 instanceof Error ? err2.message : String(err2)}`);
26764
26729
  }
26765
26730
  }
26766
26731
  function getTaskState(session, taskId) {
@@ -26793,9 +26758,6 @@ function hasBothStageBCompletions(session, taskId) {
26793
26758
  return false;
26794
26759
  return completions.has("reviewer") && completions.has("test_engineer");
26795
26760
  }
26796
- function derivePlanIdFromPlan(plan) {
26797
- return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
26798
- }
26799
26761
  async function isCouncilGateActive(directory, council) {
26800
26762
  const enabled = council?.enabled === true;
26801
26763
  let plan = null;
@@ -26807,7 +26769,7 @@ async function isCouncilGateActive(directory, council) {
26807
26769
  if (!plan) {
26808
26770
  return false;
26809
26771
  }
26810
- const planId = derivePlanIdFromPlan(plan);
26772
+ const planId = derivePlanId(plan);
26811
26773
  let profile = null;
26812
26774
  try {
26813
26775
  profile = getProfile(directory, planId);
@@ -26815,7 +26777,7 @@ async function isCouncilGateActive(directory, council) {
26815
26777
  const msg = err2 instanceof Error ? err2.message : String(err2);
26816
26778
  const isBenign = msg.includes("SQLITE_CANTOPEN") || msg.includes("ENOENT");
26817
26779
  if (!isBenign) {
26818
- console.warn(`[isCouncilGateActive] getProfile threw unexpectedly for plan ${planId}: ${msg}. Treating council as inactive.`);
26780
+ warn(`[isCouncilGateActive] getProfile threw unexpectedly for plan ${planId}: ${msg}. Treating council as inactive.`);
26819
26781
  }
26820
26782
  profile = null;
26821
26783
  }
@@ -26828,7 +26790,7 @@ async function isCouncilGateActive(directory, council) {
26828
26790
  }
26829
26791
  if (enabled !== councilMode && !_councilDisagreementWarned.has(planId)) {
26830
26792
  _councilDisagreementWarned.add(planId);
26831
- console.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.");
26793
+ 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.");
26832
26794
  }
26833
26795
  return false;
26834
26796
  }
@@ -27038,6 +27000,7 @@ var init_state = __esm(() => {
27038
27000
  init_delegation_gate();
27039
27001
  init_manager();
27040
27002
  init_telemetry();
27003
+ init_logger();
27041
27004
  _councilDisagreementWarned = new Set;
27042
27005
  _toolAggregates = new Map;
27043
27006
  defaultRunContext = new AgentRunContext("default", _toolAggregates);
@@ -53952,9 +53915,6 @@ var init_promote = __esm(() => {
53952
53915
  });
53953
53916
 
53954
53917
  // src/commands/qa-gates.ts
53955
- function derivePlanId(plan) {
53956
- return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
53957
- }
53958
53918
  function isGateName(name2) {
53959
53919
  return ALL_GATE_NAMES.includes(name2);
53960
53920
  }
@@ -54080,7 +54040,8 @@ var init_qa_gates = __esm(() => {
54080
54040
  "sast_enabled",
54081
54041
  "mutation_test",
54082
54042
  "council_general_review",
54083
- "drift_check"
54043
+ "drift_check",
54044
+ "final_council"
54084
54045
  ];
54085
54046
  });
54086
54047
 
@@ -55144,7 +55105,7 @@ async function handleRollbackCommand(directory, args2) {
55144
55105
  if (fs27.existsSync(planJsonPath)) {
55145
55106
  const planRaw = fs27.readFileSync(planJsonPath, "utf-8");
55146
55107
  const plan = PlanSchema.parse(JSON.parse(planRaw));
55147
- const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
55108
+ const planId = derivePlanId(plan);
55148
55109
  const planHash = computePlanHash(plan);
55149
55110
  await initLedger(directory, planId, planHash, plan);
55150
55111
  await appendLedgerEvent(directory, {
@@ -56676,7 +56637,7 @@ function buildQaGateSelectionDialogue(modeLabel) {
56676
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.";
56677
56638
  return `${leadIn}
56678
56639
 
56679
- 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:
56680
56641
  - reviewer (default: ON) — code review of coder output
56681
56642
  - test_engineer (default: ON) — test verification of coder output
56682
56643
  - sme_enabled (default: ON) — SME consultation during planning/clarification
@@ -56687,8 +56648,20 @@ Present the ten gates with their defaults (DEFAULT_QA_GATES) as a single user-fa
56687
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)
56688
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.
56689
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.
56690
56654
 
56691
- 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.`;
56692
56665
  }
56693
56666
  function buildAvailableToolsList(council) {
56694
56667
  const tools = AGENT_TOOL_MAP.architect ?? [];
@@ -57252,6 +57225,63 @@ SECURITY_KEYWORDS: password, secret, token, credential, auth, login, encryption,
57252
57225
  {{AGENT_PREFIX}}docs - Documentation updates (README, API docs, guides — NOT .swarm/ files)
57253
57226
  {{AGENT_PREFIX}}designer - UI/UX design specs (scaffold generation for UI components — runs BEFORE coder on UI tasks)
57254
57227
 
57228
+ ## SKILLS PROPAGATION
57229
+
57230
+ Subagents run in isolated contexts. Any project-specific skill constraints loaded into your session (e.g. \`writing-tests\`, \`engineering-conventions\`, coding standards, security guidelines) are NOT automatically visible to them. Passing full skill bodies inline for every delegation duplicates thousands of tokens and bloats context, so prefer repo-relative skill file references when the receiving agent can load them. Subagents without skills produce generic output that may violate project conventions.
57231
+
57232
+ ### Step 1 — Discover available skills (once per session)
57233
+
57234
+ At session start, before your first delegation:
57235
+ 1. Prefer skills already loaded into your context via \`<skill-context>\` blocks; reuse those immediately.
57236
+ 2. When you need to inspect on-disk skills, use the \`search\` tool with \`include\` patterns like \`.opencode/skills/*/SKILL.md,.claude/skills/*/SKILL.md\` and frontmatter queries such as \`^name:\` / \`^description:\` so you only read the YAML lines you need.
57237
+ 3. Write a brief skill index to \`.swarm/context.md\` under \`## Available Skills\`:
57238
+ - writing-tests: Guidelines for writing tests (bun:test, mock isolation, CI) → test_engineer, coder
57239
+ - engineering-conventions: Engineering invariants for this repo → coder, reviewer, test_engineer
57240
+ - [name]: [description] → [applicable agents]
57241
+ 4. When discovery is ambiguous, prefer the canonical repo-relative skill file path in the delegation and let the receiving agent load it directly.
57242
+
57243
+ ### Step 2 — Route skills to agents
57244
+
57245
+ Include a skill in a delegation when ANY of the following match:
57246
+
57247
+ | Skill description / name contains… | Pass to agents… |
57248
+ |---------------------------------------------------|---------------------------------------|
57249
+ | "test", "testing", "test files", "writing tests" | test_engineer, coder |
57250
+ | "engineering", "conventions", "invariants", "rules" | coder, reviewer, test_engineer |
57251
+ | "code", "implementation", "coding standards" | coder, reviewer |
57252
+ | "review", "security audit", "security guidelines" | reviewer |
57253
+ | "documentation", "docs", "writing docs" | docs |
57254
+ | "architecture", "design patterns", "ui" | designer, sme |
57255
+ | domain-specific (database, cloud, mobile, etc.) | sme |
57256
+
57257
+ When uncertain: pass the skill. Subagents ignore irrelevant content. A missing applicable skill degrades output quality.
57258
+
57259
+ ### Step 3 — Include skill references in delegations
57260
+
57261
+ Add a \`SKILLS:\` field to every delegation that goes to an implementation or review agent (coder, reviewer, test_engineer, sme, docs, designer). Use one of:
57262
+
57263
+ - \`SKILLS: none\` — only when no project-specific skill applies to that delegation
57264
+ - \`SKILLS: file:.claude/skills/writing-tests/SKILL.md\` — preferred for skills that exist on disk; use repo-relative \`file:\` references, comma-separated when multiple skills apply
57265
+ - Inline block fallback:
57266
+ SKILLS:
57267
+ --- [skill-name] ---
57268
+ [full SKILL.md body content pasted here]
57269
+ --- [skill-name-2] ---
57270
+ [full SKILL.md body content pasted here]
57271
+
57272
+ Default to repo-relative \`file:\` references for coder, reviewer, test_engineer, and sme. Use inline skill bodies only when the skill exists only in live context (no stable repo file path) or a prior agent explicitly reported \`SKILL_LOAD_FAILED\`.
57273
+
57274
+ **SKILL_LOAD_FAILED recovery:** If a subagent reports SKILL_LOAD_FAILED for a \`file:\` reference, do NOT retry with the same reference. Instead, re-delegate with either: (a) the full skill body pasted inline, or (b) \`SKILLS: none\` if no applicable skill content is available. Never re-use a file: reference that has already failed.
57275
+
57276
+ **Mandatory for coding tasks:** Always provide \`writing-tests\` to test_engineer and \`engineering-conventions\` to coder + reviewer when those skills are present in the project. Prefer \`file:\` references when the files exist.
57277
+
57278
+ ### ANTI-RATIONALIZATION
57279
+ - ✗ "The coder already knows these conventions" → Skills contain project-specific rules the model cannot know from training. Always pass.
57280
+ - ✗ "It's a simple task, skills aren't needed" → A short \`file:\` reference is cheap. Missing skill constraints cause convention drift. Always pass.
57281
+ - ✗ "I don't know which skill is relevant" → When uncertain, pass ALL discovered skills. Subagents discard inapplicable content.
57282
+ - ✗ "The skill was loaded earlier so the agent knows it" → Each subagent Task call is a fresh context. Skills do NOT persist across Task boundaries.
57283
+ - ✗ "I'll paste the whole skill body every time just to be safe" → Inline bodies are fallback only. Prefer \`file:\` references to avoid unnecessary context bloat.
57284
+
57255
57285
  ## SLASH COMMANDS
57256
57286
  {{SLASH_COMMANDS}}
57257
57287
  Commands above are documented with args and behavioral details. Run commands via /swarm <command> [args].
@@ -57265,15 +57295,13 @@ Available Tools: {{AVAILABLE_TOOLS}}
57265
57295
 
57266
57296
  Delegations are performed ONLY by calling the **Task** tool. Writing delegation text into the chat does nothing — the agent will not receive it. Every delegation below is the content you pass to the Task tool, not text you output to the conversation.
57267
57297
 
57268
- All delegations MUST use this exact structure (MANDATORY malformed delegations will be rejected):
57298
+ All delegations MUST follow the receiving agent's INPUT FORMAT exactly. Do NOT invent fields, omit required fields, or force one agent's schema onto another. Every delegation MUST begin with the agent name, include \`TASK:\`, and include \`SKILLS:\` when that agent prompt supports skills.
57269
57299
  Do NOT add conversational preamble before the agent prefix. Begin directly with the agent name.
57270
57300
 
57271
57301
  {{AGENT_PREFIX}}[agent]
57272
57302
  TASK: [single objective]
57273
- FILE: [path] (if applicable)
57274
- INPUT: [what to analyze/use]
57275
- OUTPUT: [expected deliverable format]
57276
- CONSTRAINT: [what NOT to do]
57303
+ [agent-specific fields required by that agent's INPUT FORMAT]
57304
+ SKILLS: [either "none", repo-relative file: references, or inline skill bodies — see SKILLS PROPAGATION; use "none" only when no project-specific skill applies]
57277
57305
 
57278
57306
  Examples:
57279
57307
 
@@ -57281,6 +57309,7 @@ Examples:
57281
57309
  TASK: Analyze codebase for auth implementation
57282
57310
  INPUT: Focus on src/auth/, src/middleware/
57283
57311
  OUTPUT: Structure, frameworks, key files, relevant domains
57312
+ SKILLS: none
57284
57313
 
57285
57314
  {{AGENT_PREFIX}}sme
57286
57315
  TASK: Review auth token patterns
@@ -57288,12 +57317,14 @@ DOMAIN: security
57288
57317
  INPUT: src/auth/login.ts uses JWT with RS256
57289
57318
  OUTPUT: Security considerations, recommended patterns
57290
57319
  CONSTRAINT: Focus on auth only, not general code style
57320
+ SKILLS: none
57291
57321
 
57292
57322
  {{AGENT_PREFIX}}sme
57293
57323
  TASK: Advise on state management approach
57294
57324
  DOMAIN: ios
57295
57325
  INPUT: Building a SwiftUI app with offline-first sync
57296
57326
  OUTPUT: Recommended patterns, frameworks, gotchas
57327
+ SKILLS: none
57297
57328
 
57298
57329
  PRE-STEP (required): call \`declare_scope({ taskId, files })\` BEFORE writing any {{AGENT_PREFIX}}coder delegation. See Rule 1a.
57299
57330
 
@@ -57303,6 +57334,7 @@ FILE: src/auth/login.ts
57303
57334
  INPUT: Validate email format, password >= 8 chars
57304
57335
  OUTPUT: Modified file
57305
57336
  CONSTRAINT: Do not modify other functions
57337
+ SKILLS: file:.claude/skills/engineering-conventions/SKILL.md
57306
57338
 
57307
57339
  {{AGENT_PREFIX}}reviewer
57308
57340
  TASK: Review login validation
@@ -57310,17 +57342,20 @@ FILE: src/auth/login.ts
57310
57342
  CHECK: [security, correctness, edge-cases]
57311
57343
  GATES: lint=PASS, sast_scan=PASS, secretscan=PASS
57312
57344
  OUTPUT: VERDICT + RISK + ISSUES
57345
+ SKILLS: file:.claude/skills/engineering-conventions/SKILL.md
57313
57346
 
57314
57347
  {{AGENT_PREFIX}}test_engineer
57315
57348
  TASK: Generate and run login validation tests
57316
57349
  FILE: src/auth/login.ts
57317
57350
  OUTPUT: Test file at src/auth/login.test.ts + VERDICT: PASS/FAIL with failure details
57351
+ SKILLS: file:.claude/skills/writing-tests/SKILL.md
57318
57352
 
57319
57353
  {{AGENT_PREFIX}}critic
57320
57354
  TASK: Review plan for user authentication feature
57321
57355
  PLAN: [paste the plan.md content]
57322
57356
  CONTEXT: [codebase summary from explorer]
57323
57357
  OUTPUT: VERDICT + CONFIDENCE + ISSUES + SUMMARY
57358
+ SKILLS: none
57324
57359
 
57325
57360
  {{AGENT_PREFIX}}reviewer
57326
57361
  TASK: Security-only review of login validation
@@ -57328,18 +57363,21 @@ FILE: src/auth/login.ts
57328
57363
  CHECK: [security-only] — evaluate against OWASP Top 10, scan for hardcoded secrets, injection vectors, insecure crypto, missing input validation
57329
57364
  GATES: lint=PASS, sast_scan=PASS, secretscan=PASS
57330
57365
  OUTPUT: VERDICT + RISK + SECURITY ISSUES ONLY
57366
+ SKILLS: file:.claude/skills/engineering-conventions/SKILL.md
57331
57367
 
57332
57368
  {{AGENT_PREFIX}}test_engineer
57333
57369
  TASK: Adversarial security testing
57334
57370
  FILE: src/auth/login.ts
57335
57371
  CONSTRAINT: ONLY attack vectors — malformed inputs, oversized payloads, injection attempts, auth bypass, boundary violations
57336
57372
  OUTPUT: Test file + VERDICT: PASS/FAIL
57373
+ SKILLS: file:.claude/skills/writing-tests/SKILL.md
57337
57374
 
57338
57375
  {{AGENT_PREFIX}}explorer
57339
57376
  TASK: Integration impact analysis
57340
57377
  INPUT: Contract changes detected: [list from diff tool]
57341
57378
  OUTPUT: BREAKING_CHANGES + COMPATIBLE_CHANGES + CONSUMERS_AFFECTED + COMPATIBILITY SIGNALS: [COMPATIBLE | INCOMPATIBLE | UNCERTAIN] + MIGRATION_SURFACE: [yes — list of affected call signatures | no]
57342
57379
  CONSTRAINT: Read-only. use search to find imports/usages of changed exports.
57380
+ SKILLS: none
57343
57381
 
57344
57382
  {{AGENT_PREFIX}}docs
57345
57383
  TASK: Update documentation for Phase 2 changes
@@ -57350,6 +57388,7 @@ CHANGES SUMMARY:
57350
57388
  - Added UserSession interface with refreshToken field
57351
57389
  DOC FILES: README.md, docs/api.md, docs/installation.md
57352
57390
  OUTPUT: Updated doc files + SUMMARY
57391
+ SKILLS: none
57353
57392
 
57354
57393
  {{AGENT_PREFIX}}designer
57355
57394
  TASK: Design specification for user settings page
@@ -57357,6 +57396,7 @@ CONTEXT: Users need to update profile info, change password, manage notification
57357
57396
  FRAMEWORK: React (TSX)
57358
57397
  EXISTING PATTERNS: All forms use react-hook-form, validation with zod, toast notifications for success/error
57359
57398
  OUTPUT: Code scaffold for src/pages/Settings.tsx with component tree, typed props, layout, and accessibility
57399
+ SKILLS: none
57360
57400
 
57361
57401
  ## WORKFLOW
57362
57402
 
@@ -57458,6 +57498,7 @@ Do NOT call \`set_qa_gates\` yet — \`plan.json\` does not exist at this point.
57458
57498
  - mutation_test: <true|false>
57459
57499
  - council_general_review: <true|false>
57460
57500
  - drift_check: <true|false>
57501
+ - final_council: <true|false>
57461
57502
  - recorded_at: <ISO timestamp>
57462
57503
  \`\`\`
57463
57504
  MODE: PLAN applies these after \`save_plan\` succeeds via \`set_qa_gates\`.
@@ -57536,6 +57577,7 @@ Do NOT call \`set_qa_gates\` yet — \`plan.json\` does not exist at this point.
57536
57577
  - mutation_test: <true|false>
57537
57578
  - council_general_review: <true|false>
57538
57579
  - drift_check: <true|false>
57580
+ - final_council: <true|false>
57539
57581
  - recorded_at: <ISO timestamp>
57540
57582
  \`\`\`
57541
57583
  MODE: PLAN will read this section after \`save_plan\` succeeds and persist via \`set_qa_gates\`.
@@ -57918,6 +57960,7 @@ save_plan({
57918
57960
  **POST-SAVE_PLAN: APPLY QA GATE SELECTION.**
57919
57961
  After \`save_plan\` succeeds, read \`.swarm/context.md\`:
57920
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.
57921
57964
  - If no pending section exists: {{QA_GATE_DIALOGUE_PLAN}}
57922
57965
  <!-- BEHAVIORAL_GUIDANCE_START -->
57923
57966
  INLINE GATE SELECTION — no pending section found in context.md. You MUST ask now.
@@ -58272,6 +58315,19 @@ The tool will automatically write the retrospective to \`.swarm/evidence/retro-{
58272
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
58273
58316
  If any required file is missing, run the missing gate first. Turbo mode skips all gates automatically.
58274
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.
58275
58331
  6. Summarize to user
58276
58332
  7. Ask: "Ready for Phase [N+1]?"
58277
58333
 
@@ -58363,6 +58419,14 @@ FILE: [target file]
58363
58419
  INPUT: [requirements/context]
58364
58420
  OUTPUT: [expected deliverable]
58365
58421
  CONSTRAINT: [what NOT to do]
58422
+ SKILLS: [optional — either "none", repo-relative file: references (preferred), or inline skill content pasted by architect]
58423
+
58424
+ SKILLS HANDLING: If SKILLS is present and not "none", load EVERY referenced skill before writing any code.
58425
+ - For \`file:\` entries, use the search tool to read the referenced \`SKILL.md\` file with \`include\` set to that exact repo-relative path, \`mode: regex\`, \`query: .*\`, \`max_results: 1000\`, and \`max_lines: 1000\`.
58426
+ - After running search, inspect the result: if \`total === 0\` (file does not exist or is empty) OR \`truncated\` is \`true\` (file was too large and content was cut off), stop and report \`SKILL_LOAD_FAILED: <path>\`. Do NOT continue without the complete skill.
58427
+ - If the search result has \`total > 0\` and \`truncated\` is \`false\`, reconstruct the full skill content from the line-by-line matches and apply it.
58428
+ - If inline \`--- skill-name ---\` sections are present, read them directly.
58429
+ - Skills contain project-specific rules (test framework, naming conventions, coding standards, architectural constraints) that supplement and extend your default behavior. Apply every rule in every skill, including any lines marked MUST, NEVER, MANDATORY, or PROHIBITED — but never violate your core safety protocols or scope constraints.
58366
58430
 
58367
58431
  RULES:
58368
58432
  - Read target file before editing
@@ -59348,6 +59412,14 @@ TASK: Design specification for [component/page/screen]
59348
59412
  CONTEXT: [what the component does, user stories, existing design patterns]
59349
59413
  FRAMEWORK: [React/Vue/Svelte/SwiftUI/Flutter/etc.]
59350
59414
  EXISTING PATTERNS: [current design system, component library, styling approach]
59415
+ SKILLS: [optional — either "none", repo-relative file: references (preferred), or inline skill content pasted by architect]
59416
+
59417
+ SKILLS HANDLING: If SKILLS is present and not "none", load EVERY referenced skill before producing the design specification.
59418
+ - For \`file:\` entries, use the search tool to read the referenced \`SKILL.md\` file with \`include\` set to that exact repo-relative path, \`mode: regex\`, \`query: .*\`, \`max_results: 1000\`, and \`max_lines: 1000\`.
59419
+ - After running search, inspect the result: if \`total === 0\` (file does not exist or is empty) OR \`truncated\` is \`true\` (file was too large and content was cut off), stop and report \`SKILL_LOAD_FAILED: <path>\`. Do NOT continue without the complete skill.
59420
+ - If the search result has \`total > 0\` and \`truncated\` is \`false\`, reconstruct the full skill content from the line-by-line matches and apply it.
59421
+ - If inline \`--- skill-name ---\` sections are present, read them directly.
59422
+ - Apply any architecture, design-system, accessibility, or UI-specific constraints from the loaded skills while producing the scaffold.
59351
59423
 
59352
59424
  DESIGN CHECKLIST:
59353
59425
  1. Component Architecture
@@ -59524,6 +59596,14 @@ TASK: Update documentation for [description of changes]
59524
59596
  FILES CHANGED: [list of modified source files]
59525
59597
  CHANGES SUMMARY: [what was added/modified/removed]
59526
59598
  DOC FILES: [list of documentation files to update]
59599
+ SKILLS: [optional — either "none", repo-relative file: references (preferred), or inline skill content pasted by architect]
59600
+
59601
+ SKILLS HANDLING: If SKILLS is present and not "none", load EVERY referenced skill before updating docs.
59602
+ - For \`file:\` entries, use the search tool to read the referenced \`SKILL.md\` file with \`include\` set to that exact repo-relative path, \`mode: regex\`, \`query: .*\`, \`max_results: 1000\`, and \`max_lines: 1000\`.
59603
+ - After running search, inspect the result: if \`total === 0\` (file does not exist or is empty) OR \`truncated\` is \`true\` (file was too large and content was cut off), stop and report \`SKILL_LOAD_FAILED: <path>\`. Do NOT continue without the complete skill.
59604
+ - If the search result has \`total > 0\` and \`truncated\` is \`false\`, reconstruct the full skill content from the line-by-line matches and apply it.
59605
+ - If inline \`--- skill-name ---\` sections are present, read them directly.
59606
+ - Apply any documentation, release-note, or style constraints from the loaded skills while updating documentation.
59527
59607
 
59528
59608
  SCOPE:
59529
59609
  - README.md (project description, usage, examples)
@@ -59823,6 +59903,14 @@ DIFF: [changed files/functions, or "infer from FILE" if omitted]
59823
59903
  AFFECTS: [callers/consumers/dependents to inspect, or "infer from diff"]
59824
59904
  CHECK: [list of dimensions to evaluate]
59825
59905
  GATES: [pre-completed gate results (lint, SAST, secretscan, etc.), or "none" if unavailable]
59906
+ SKILLS: [optional — either "none", repo-relative file: references (preferred), or inline skill content pasted by architect]
59907
+
59908
+ SKILLS HANDLING: If SKILLS is present and not "none", load EVERY referenced skill before beginning your review.
59909
+ - For \`file:\` entries, use the search tool to read the referenced \`SKILL.md\` file with \`include\` set to that exact repo-relative path, \`mode: regex\`, \`query: .*\`, \`max_results: 1000\`, and \`max_lines: 1000\`.
59910
+ - After running search, inspect the result: if \`total === 0\` (file does not exist or is empty) OR \`truncated\` is \`true\` (file was too large and content was cut off), stop and report \`SKILL_LOAD_FAILED: <path>\`. Do NOT continue without the complete skill.
59911
+ - If the search result has \`total > 0\` and \`truncated\` is \`false\`, reconstruct the full skill content from the line-by-line matches and apply it.
59912
+ - If inline \`--- skill-name ---\` sections are present, read them directly.
59913
+ - Skills contain project-specific constraints (coding standards, architectural invariants, security requirements) that supplement and may extend your normal review dimensions. Flag any violation of a skill rule at the same severity as a logic error.
59826
59914
 
59827
59915
  PROCESSING: If GATES is provided and includes passing results for lint, SAST, placeholder-scan, or secret-scan: skip the corresponding Tier 2 checks that those gates already cover. Focus Tier 2 time on checks NOT covered by automated gates.
59828
59916
 
@@ -59930,6 +60018,14 @@ Match response length to confidence and complexity. HIGH confidence on simple lo
59930
60018
  TASK: [what guidance is needed]
59931
60019
  DOMAIN: [the domain - e.g., security, ios, android, rust, kubernetes]
59932
60020
  INPUT: [context/requirements]
60021
+ SKILLS: [optional — either "none", repo-relative file: references (preferred), or inline skill content pasted by architect]
60022
+
60023
+ SKILLS HANDLING: If SKILLS is present and not "none", load EVERY referenced skill before formulating your recommendation.
60024
+ - For \`file:\` entries, use the search tool to read the referenced \`SKILL.md\` file with \`include\` set to that exact repo-relative path, \`mode: regex\`, \`query: .*\`, \`max_results: 1000\`, and \`max_lines: 1000\`.
60025
+ - After running search, inspect the result: if \`total === 0\` (file does not exist or is empty) OR \`truncated\` is \`true\` (file was too large and content was cut off), stop and report \`SKILL_LOAD_FAILED: <path>\`. Do NOT continue without the complete skill.
60026
+ - If the search result has \`total > 0\` and \`truncated\` is \`false\`, reconstruct the full skill content from the line-by-line matches and apply it.
60027
+ - If inline \`--- skill-name ---\` sections are present, read them directly.
60028
+ - Skills may contain project-specific constraints relevant to your domain (e.g. security rules, platform requirements, coding standards). Where skills add constraints to your recommendation, list them explicitly in your APPROACH and GOTCHAS.
59933
60029
 
59934
60030
  ## OUTPUT FORMAT (MANDATORY — deviations will be rejected)
59935
60031
  Begin directly with CONFIDENCE. Do NOT prepend "Here's my research..." or any conversational preamble.
@@ -60049,6 +60145,14 @@ INPUT FORMAT:
60049
60145
  TASK: Generate tests for [description]
60050
60146
  FILE: [source file path]
60051
60147
  OUTPUT: [test file path]
60148
+ SKILLS: [optional — either "none", repo-relative file: references (preferred), or inline skill content pasted by architect]
60149
+
60150
+ SKILLS HANDLING: If SKILLS is present and not "none", load EVERY referenced skill before writing any test code.
60151
+ - For \`file:\` entries, use the search tool to read the referenced \`SKILL.md\` file with \`include\` set to that exact repo-relative path, \`mode: regex\`, \`query: .*\`, \`max_results: 1000\`, and \`max_lines: 1000\`.
60152
+ - After running search, inspect the result: if \`total === 0\` (file does not exist or is empty) OR \`truncated\` is \`true\` (file was too large and content was cut off), stop and report \`SKILL_LOAD_FAILED: <path>\`. Do NOT continue without the complete skill.
60153
+ - If the search result has \`total > 0\` and \`truncated\` is \`false\`, reconstruct the full skill content from the line-by-line matches and apply it.
60154
+ - If inline \`--- skill-name ---\` sections are present, read them directly.
60155
+ - Skills override your default framework choices, mock patterns, file placement conventions, and CI rules. Apply every MUST, NEVER, MANDATORY, and PROHIBITED rule precisely.
60052
60156
 
60053
60157
  COVERAGE:
60054
60158
  - Happy path: normal inputs
@@ -65543,7 +65647,7 @@ var init_curator_drift = __esm(() => {
65543
65647
  init_package();
65544
65648
  init_agents2();
65545
65649
  init_critic();
65546
- import * as path115 from "node:path";
65650
+ import * as path116 from "node:path";
65547
65651
 
65548
65652
  // src/background/index.ts
65549
65653
  init_event_bus();
@@ -78736,7 +78840,7 @@ var diff = createSwarmTool({
78736
78840
  encoding: "utf-8",
78737
78841
  timeout: 3000,
78738
78842
  cwd: directory,
78739
- stdio: "pipe"
78843
+ stdio: ["ignore", "pipe", "pipe"]
78740
78844
  });
78741
78845
  return true;
78742
78846
  } catch (e) {
@@ -78750,9 +78854,9 @@ var diff = createSwarmTool({
78750
78854
  }, getContentFromRef = function(refPath) {
78751
78855
  return child_process7.execFileSync("git", ["show", refPath], {
78752
78856
  encoding: "utf-8",
78753
- timeout: 5000,
78857
+ timeout: 15000,
78754
78858
  cwd: directory,
78755
- stdio: "pipe"
78859
+ stdio: ["ignore", "pipe", "pipe"]
78756
78860
  });
78757
78861
  };
78758
78862
  if (!directory || typeof directory !== "string" || directory.trim() === "") {
@@ -78803,13 +78907,15 @@ var diff = createSwarmTool({
78803
78907
  encoding: "utf-8",
78804
78908
  timeout: DIFF_TIMEOUT_MS,
78805
78909
  maxBuffer: MAX_BUFFER_BYTES,
78806
- cwd: directory
78910
+ cwd: directory,
78911
+ stdio: ["ignore", "pipe", "pipe"]
78807
78912
  });
78808
78913
  const fullDiffOutput = child_process7.execFileSync("git", fullDiffArgs, {
78809
78914
  encoding: "utf-8",
78810
78915
  timeout: DIFF_TIMEOUT_MS,
78811
78916
  maxBuffer: MAX_BUFFER_BYTES,
78812
- cwd: directory
78917
+ cwd: directory,
78918
+ stdio: ["ignore", "pipe", "pipe"]
78813
78919
  });
78814
78920
  const files = [];
78815
78921
  const numstatLines = numstatOutput.split(`
@@ -79578,9 +79684,6 @@ function summarizePlan(plan) {
79578
79684
  }))
79579
79685
  };
79580
79686
  }
79581
- function derivePlanId2(plan) {
79582
- return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
79583
- }
79584
79687
  async function executeGetApprovedPlan(args2, directory) {
79585
79688
  const currentPlan = await loadPlanJsonOnly(directory);
79586
79689
  if (!currentPlan) {
@@ -79599,7 +79702,7 @@ async function executeGetApprovedPlan(args2, directory) {
79599
79702
  reason: "no_approved_snapshot"
79600
79703
  };
79601
79704
  }
79602
- const expectedPlanId = derivePlanId2(currentPlan);
79705
+ const expectedPlanId = derivePlanId(currentPlan);
79603
79706
  const profile = getProfile(directory, expectedPlanId);
79604
79707
  const qaProfileHash = profile ? computeProfileHash(profile) : null;
79605
79708
  const approved = await loadLastApprovedPlan(directory, expectedPlanId);
@@ -79658,9 +79761,6 @@ var get_approved_plan = createSwarmTool({
79658
79761
  init_qa_gate_profile();
79659
79762
  init_manager();
79660
79763
  init_create_tool();
79661
- function derivePlanId3(plan) {
79662
- return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
79663
- }
79664
79764
  async function executeGetQaGateProfile(_args, directory) {
79665
79765
  const plan = await loadPlanJsonOnly(directory);
79666
79766
  if (!plan) {
@@ -79669,7 +79769,7 @@ async function executeGetQaGateProfile(_args, directory) {
79669
79769
  reason: "plan_json_unavailable"
79670
79770
  };
79671
79771
  }
79672
- const planId = derivePlanId3(plan);
79772
+ const planId = derivePlanId(plan);
79673
79773
  const profile = getProfile(directory, planId);
79674
79774
  if (!profile) {
79675
79775
  return {
@@ -80841,7 +80941,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
80841
80941
  }, null, 2);
80842
80942
  }
80843
80943
  if (hasActiveTurboMode(sessionID)) {
80844
- 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}`);
80845
80945
  } else {
80846
80946
  try {
80847
80947
  const completionResultRaw = await executeCompletionVerify({ phase }, dir);
@@ -80870,7 +80970,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
80870
80970
  driftHasSpecMd = fs69.existsSync(specMdPath);
80871
80971
  const gatePlan = await loadPlan(dir);
80872
80972
  if (gatePlan) {
80873
- const gatePlanId = `${gatePlan.swarm}-${gatePlan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
80973
+ const gatePlanId = derivePlanId(gatePlan);
80874
80974
  const gateProfile = getProfile(dir, gatePlanId);
80875
80975
  if (gateProfile) {
80876
80976
  const gateSession = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
@@ -80999,7 +81099,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
80999
81099
  try {
81000
81100
  const plan = await loadPlan(dir);
81001
81101
  if (plan) {
81002
- const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
81102
+ const planId = derivePlanId(plan);
81003
81103
  const profile = getProfile(dir, planId);
81004
81104
  if (profile) {
81005
81105
  const session2 = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
@@ -81071,7 +81171,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
81071
81171
  try {
81072
81172
  const plan = await loadPlan(dir);
81073
81173
  if (plan) {
81074
- const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
81174
+ const planId = derivePlanId(plan);
81075
81175
  const profile = getProfile(dir, planId);
81076
81176
  if (profile) {
81077
81177
  const session2 = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
@@ -81144,7 +81244,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
81144
81244
  try {
81145
81245
  const plan = await loadPlan(dir);
81146
81246
  if (plan) {
81147
- const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
81247
+ const planId = derivePlanId(plan);
81148
81248
  const profile = getProfile(dir, planId);
81149
81249
  if (profile) {
81150
81250
  const session2 = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
@@ -81345,6 +81445,127 @@ Advisory notes: ${advisoryNotes.join("; ")}` : "";
81345
81445
  }
81346
81446
  }
81347
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
+ }
81348
81569
  let knowledgeConfig;
81349
81570
  try {
81350
81571
  knowledgeConfig = KnowledgeConfigSchema.parse(config3.knowledge ?? {});
@@ -86681,7 +86902,10 @@ async function executeSavePlan(args2, fallbackDir) {
86681
86902
  } catch {}
86682
86903
  const hasPendingSection = contextContent.includes("## Pending QA Gate Selection");
86683
86904
  if (!hasPendingSection) {
86684
- 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
+ });
86685
86909
  let existingProfile = null;
86686
86910
  try {
86687
86911
  existingProfile = getProfile(targetWorkspace, candidatePlanId);
@@ -86802,7 +87026,7 @@ async function executeSavePlan(args2, fallbackDir) {
86802
87026
  await takeSnapshotEvent(dir, savedPlan).catch(() => {});
86803
87027
  }
86804
87028
  if (resolvedProfile !== undefined && savedPlan) {
86805
- const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
87029
+ const planId = derivePlanId(plan);
86806
87030
  const planHashAfter = computePlanHash(savedPlan);
86807
87031
  const profileChanged = JSON.stringify(resolvedProfile) !== JSON.stringify(preservedExecutionProfile);
86808
87032
  if (profileChanged) {
@@ -88684,9 +88908,6 @@ init_zod();
88684
88908
  init_qa_gate_profile();
88685
88909
  init_manager();
88686
88910
  init_create_tool();
88687
- function derivePlanId4(plan) {
88688
- return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
88689
- }
88690
88911
  async function executeSetQaGates(args2, directory) {
88691
88912
  const plan = await loadPlanJsonOnly(directory);
88692
88913
  if (!plan) {
@@ -88696,7 +88917,7 @@ async function executeSetQaGates(args2, directory) {
88696
88917
  message: "Cannot configure QA gates: plan.json is missing or invalid. " + "Create a plan first (e.g. via /swarm specify or save_plan)."
88697
88918
  };
88698
88919
  }
88699
- const planId = derivePlanId4(plan);
88920
+ const planId = derivePlanId(plan);
88700
88921
  getOrCreateProfile(directory, planId, args2.project_type);
88701
88922
  const partial3 = {};
88702
88923
  for (const key of [
@@ -88709,7 +88930,8 @@ async function executeSetQaGates(args2, directory) {
88709
88930
  "sast_enabled",
88710
88931
  "mutation_test",
88711
88932
  "council_general_review",
88712
- "drift_check"
88933
+ "drift_check",
88934
+ "final_council"
88713
88935
  ]) {
88714
88936
  if (args2[key] !== undefined)
88715
88937
  partial3[key] = args2[key];
@@ -88757,6 +88979,7 @@ var set_qa_gates = createSwarmTool({
88757
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."),
88758
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."),
88759
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."),
88760
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.')
88761
88984
  },
88762
88985
  execute: async (args2, directory) => {
@@ -90855,6 +91078,7 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
90855
91078
  } catch {}
90856
91079
  }
90857
91080
  const resolvedDir = workingDirectory ?? process.cwd();
91081
+ let evidenceIncompleteReason = null;
90858
91082
  try {
90859
91083
  const evidence = readTaskEvidenceRaw(resolvedDir, taskId);
90860
91084
  if (evidence === null) {} else if (evidence.required_gates && Array.isArray(evidence.required_gates) && evidence.gates) {
@@ -90863,11 +91087,7 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
90863
91087
  return { blocked: false, reason: "" };
90864
91088
  }
90865
91089
  const missingGates = evidence.required_gates.filter((gate) => evidence.gates[gate] == null);
90866
- telemetry.gateFailed("", "qa_gate", taskId, `Missing gates: [${missingGates.join(", ")}]`);
90867
- return {
90868
- blocked: true,
90869
- 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.`
90870
- };
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.`;
90871
91091
  }
90872
91092
  } catch (error93) {
90873
91093
  console.warn(`[gate-evidence] Evidence file for task ${taskId} is corrupt or unreadable:`, error93 instanceof Error ? error93.message : String(error93));
@@ -90957,23 +91177,18 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
90957
91177
  }
90958
91178
  }
90959
91179
  const currentStateStr = stateEntries.length > 0 ? stateEntries.join(", ") : "no active sessions";
90960
- 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`);
90961
91182
  return {
90962
91183
  blocked: true,
90963
- 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
90964
91185
  };
90965
91186
  } catch {
90966
91187
  return { blocked: false, reason: "" };
90967
91188
  }
90968
91189
  }
90969
91190
  async function checkReviewerGateWithScope(taskId, workingDirectory) {
90970
- let stageBParallelEnabled = false;
90971
- if (workingDirectory) {
90972
- try {
90973
- const cfg = await loadPluginConfig(workingDirectory);
90974
- stageBParallelEnabled = cfg.parallelization?.stageB?.parallel?.enabled === true;
90975
- } catch {}
90976
- }
91191
+ const stageBParallelEnabled = true;
90977
91192
  const result = checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled);
90978
91193
  const scopeWarning = await validateDiffScope(taskId, workingDirectory).catch(() => null);
90979
91194
  if (!scopeWarning)
@@ -91480,9 +91695,6 @@ init_manager();
91480
91695
  init_create_tool();
91481
91696
  import fs89 from "node:fs";
91482
91697
  import path112 from "node:path";
91483
- function derivePlanId5(plan) {
91484
- return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
91485
- }
91486
91698
  function normalizeVerdict(verdict) {
91487
91699
  switch (verdict) {
91488
91700
  case "APPROVED":
@@ -91569,7 +91781,7 @@ async function executeWriteDriftEvidence(args2, directory) {
91569
91781
  timestamp: snapshotEvent.timestamp
91570
91782
  };
91571
91783
  try {
91572
- const planId = derivePlanId5(currentPlan);
91784
+ const planId = derivePlanId(currentPlan);
91573
91785
  const locked = lockProfile(directory, planId, snapshotEvent.seq);
91574
91786
  qaProfileLocked = {
91575
91787
  plan_id: planId,
@@ -91636,9 +91848,10 @@ var write_drift_evidence = createSwarmTool({
91636
91848
  }
91637
91849
  }
91638
91850
  });
91639
- // src/tools/write-hallucination-evidence.ts
91851
+ // src/tools/write-final-council-evidence.ts
91640
91852
  init_zod();
91641
91853
  init_utils2();
91854
+ init_manager();
91642
91855
  init_create_tool();
91643
91856
  import fs90 from "node:fs";
91644
91857
  import path113 from "node:path";
@@ -91652,7 +91865,7 @@ function normalizeVerdict2(verdict) {
91652
91865
  throw new Error(`Invalid verdict: must be 'APPROVED' or 'NEEDS_REVISION', got '${verdict}'`);
91653
91866
  }
91654
91867
  }
91655
- async function executeWriteHallucinationEvidence(args2, directory) {
91868
+ async function executeWriteFinalCouncilEvidence(args2, directory) {
91656
91869
  const phase = args2.phase;
91657
91870
  if (!Number.isInteger(phase) || phase < 1) {
91658
91871
  return JSON.stringify({
@@ -91678,18 +91891,21 @@ async function executeWriteHallucinationEvidence(args2, directory) {
91678
91891
  }, null, 2);
91679
91892
  }
91680
91893
  const normalizedVerdict = normalizeVerdict2(args2.verdict);
91894
+ const plan = await loadPlan(directory);
91895
+ const planId = plan ? derivePlanId(plan) : "unknown";
91681
91896
  const evidenceEntry = {
91682
- type: "hallucination-verification",
91897
+ type: "final-council",
91898
+ phase,
91899
+ plan_id: planId,
91683
91900
  verdict: normalizedVerdict,
91684
91901
  summary: summary.trim(),
91685
- timestamp: new Date().toISOString(),
91686
- findings: args2.findings
91902
+ timestamp: new Date().toISOString()
91687
91903
  };
91688
91904
  const evidenceContent = {
91689
91905
  entries: [evidenceEntry]
91690
91906
  };
91691
- const filename = "hallucination-guard.json";
91692
- const relativePath = path113.join("evidence", String(phase), filename);
91907
+ const filename = "final-council.json";
91908
+ const relativePath = path113.join("evidence", filename);
91693
91909
  let validatedPath;
91694
91910
  try {
91695
91911
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -91706,6 +91922,115 @@ async function executeWriteHallucinationEvidence(args2, directory) {
91706
91922
  const tempPath = path113.join(evidenceDir, `.${filename}.tmp`);
91707
91923
  await fs90.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
91708
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);
91709
92034
  return JSON.stringify({
91710
92035
  success: true,
91711
92036
  phase,
@@ -91751,9 +92076,9 @@ var write_hallucination_evidence = createSwarmTool({
91751
92076
  init_zod();
91752
92077
  init_utils2();
91753
92078
  init_create_tool();
91754
- import fs91 from "node:fs";
91755
- import path114 from "node:path";
91756
- function normalizeVerdict3(verdict) {
92079
+ import fs92 from "node:fs";
92080
+ import path115 from "node:path";
92081
+ function normalizeVerdict4(verdict) {
91757
92082
  switch (verdict) {
91758
92083
  case "PASS":
91759
92084
  return "pass";
@@ -91810,7 +92135,7 @@ async function executeWriteMutationEvidence(args2, directory) {
91810
92135
  message: "Invalid summary: must be a non-empty string"
91811
92136
  }, null, 2);
91812
92137
  }
91813
- const normalizedVerdict = normalizeVerdict3(args2.verdict);
92138
+ const normalizedVerdict = normalizeVerdict4(args2.verdict);
91814
92139
  const evidenceEntry = {
91815
92140
  type: "mutation-gate",
91816
92141
  verdict: normalizedVerdict,
@@ -91826,7 +92151,7 @@ async function executeWriteMutationEvidence(args2, directory) {
91826
92151
  entries: [evidenceEntry]
91827
92152
  };
91828
92153
  const filename = "mutation-gate.json";
91829
- const relativePath = path114.join("evidence", String(phase), filename);
92154
+ const relativePath = path115.join("evidence", String(phase), filename);
91830
92155
  let validatedPath;
91831
92156
  try {
91832
92157
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -91837,12 +92162,12 @@ async function executeWriteMutationEvidence(args2, directory) {
91837
92162
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
91838
92163
  }, null, 2);
91839
92164
  }
91840
- const evidenceDir = path114.dirname(validatedPath);
92165
+ const evidenceDir = path115.dirname(validatedPath);
91841
92166
  try {
91842
- await fs91.promises.mkdir(evidenceDir, { recursive: true });
91843
- const tempPath = path114.join(evidenceDir, `.${filename}.tmp`);
91844
- await fs91.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
91845
- 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);
91846
92171
  return JSON.stringify({
91847
92172
  success: true,
91848
92173
  phase,
@@ -92134,7 +92459,7 @@ async function initializeOpenCodeSwarm(ctx) {
92134
92459
  const { PreflightTriggerManager: PTM } = await Promise.resolve().then(() => (init_trigger(), exports_trigger));
92135
92460
  preflightTriggerManager = new PTM(automationConfig);
92136
92461
  const { AutomationStatusArtifact: ASA } = await Promise.resolve().then(() => (init_status_artifact(), exports_status_artifact));
92137
- const swarmDir = path115.resolve(ctx.directory, ".swarm");
92462
+ const swarmDir = path116.resolve(ctx.directory, ".swarm");
92138
92463
  statusArtifact = new ASA(swarmDir);
92139
92464
  statusArtifact.updateConfig(automationConfig.mode, automationConfig.capabilities);
92140
92465
  if (automationConfig.capabilities?.evidence_auto_summaries === true) {
@@ -92293,6 +92618,7 @@ async function initializeOpenCodeSwarm(ctx) {
92293
92618
  write_drift_evidence,
92294
92619
  write_hallucination_evidence,
92295
92620
  write_mutation_evidence,
92621
+ write_final_council_evidence,
92296
92622
  declare_scope
92297
92623
  },
92298
92624
  config: async (opencodeConfig) => {