opencode-swarm 7.17.1 → 7.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -34,7 +34,7 @@ var package_default;
34
34
  var init_package = __esm(() => {
35
35
  package_default = {
36
36
  name: "opencode-swarm",
37
- version: "7.17.1",
37
+ version: "7.17.2",
38
38
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
39
39
  main: "dist/index.js",
40
40
  types: "dist/index.d.ts",
@@ -8,7 +8,7 @@
8
8
  * No I/O — fully unit-testable with mock inputs. All file reads/writes happen in
9
9
  * sibling modules (criteria-store, council-evidence-writer).
10
10
  */
11
- import type { CouncilConfig, CouncilCriteria, CouncilMemberVerdict, CouncilSynthesis, PhaseCouncilSynthesis } from './types';
11
+ import type { CouncilConfig, CouncilCriteria, CouncilMemberVerdict, CouncilSynthesis, FinalCouncilSynthesis, PhaseCouncilSynthesis } from './types';
12
12
  export declare function synthesizeCouncilVerdicts(taskId: string, swarmId: string, verdicts: CouncilMemberVerdict[], criteria: CouncilCriteria | null, roundNumber: number, config?: Partial<CouncilConfig>): CouncilSynthesis;
13
13
  /**
14
14
  * Synthesize phase-level council verdicts into a PhaseCouncilSynthesis.
@@ -19,3 +19,9 @@ export declare function synthesizeCouncilVerdicts(taskId: string, swarmId: strin
19
19
  * (see writePhaseCouncilEvidence in submit-phase-council-verdicts.ts).
20
20
  */
21
21
  export declare function synthesizePhaseCouncilAdvisory(phaseNumber: number, phaseSummary: string, verdicts: CouncilMemberVerdict[], roundNumber: number, config?: Partial<CouncilConfig>, _workingDir?: string): PhaseCouncilSynthesis;
22
+ /**
23
+ * Synthesize project-level final council verdicts into a FinalCouncilSynthesis.
24
+ * This uses the same five-member verdict semantics as phase council, but the
25
+ * output is scoped to completed-project review and the final_council gate.
26
+ */
27
+ export declare function synthesizeFinalCouncilAdvisory(projectSummary: string, verdicts: CouncilMemberVerdict[], roundNumber: number, config?: Partial<CouncilConfig>): FinalCouncilSynthesis;
@@ -87,6 +87,38 @@ export interface PhaseCouncilSynthesis {
87
87
  /** Summary of the phase being reviewed */
88
88
  phaseSummary?: string;
89
89
  }
90
+ /**
91
+ * Project-level final council synthesis result.
92
+ * Distinct from task-level and phase-level council results: this is the
93
+ * final project close gate and writes to .swarm/evidence/final-council.json.
94
+ */
95
+ export interface FinalCouncilSynthesis {
96
+ /** Always 'project' - distinguishes final council from task/phase councils */
97
+ scope: 'project';
98
+ /** ISO 8601 */
99
+ timestamp: string;
100
+ overallVerdict: CouncilVerdict;
101
+ vetoedBy: CouncilAgent[] | null;
102
+ memberVerdicts: CouncilMemberVerdict[];
103
+ unresolvedConflicts: string[];
104
+ /** Severity HIGH + MEDIUM from veto members */
105
+ requiredFixes: CouncilFinding[];
106
+ /** Severity LOW or from non-veto members */
107
+ advisoryFindings: CouncilFinding[];
108
+ /** Project-level advisory notes for the architect */
109
+ advisoryNotes: string[];
110
+ /** Single markdown document for final project review */
111
+ unifiedFeedbackMd: string;
112
+ /** 1-indexed */
113
+ roundNumber: number;
114
+ allCriteriaMet: boolean;
115
+ /** Distinct council members that produced verdicts */
116
+ quorumSize: number;
117
+ /** Path where evidence was written */
118
+ evidencePath: '.swarm/evidence/final-council.json';
119
+ /** Summary of the completed project being reviewed */
120
+ projectSummary: string;
121
+ }
90
122
  export interface CouncilCriteriaItem {
91
123
  id: string;
92
124
  description: string;
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.17.1",
36
+ version: "7.17.2",
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",
@@ -61439,7 +61439,7 @@ Present the eleven gates with their defaults (DEFAULT_QA_GATES) as a single user
61439
61439
  - 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)
61440
61440
  - 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.
61441
61441
  - 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)
61442
- - 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.
61442
+ - final_council (default: OFF) - when enabled, after all phases complete the architect dispatches the same five phase-council members (\`critic\`, \`reviewer\`, \`sme\`, \`test_engineer\`, \`explorer\`) at project scope, collects \`CouncilMemberVerdict\` objects, and calls \`write_final_council_evidence\`. This is not General Council mode and does not require \`council.general.enabled\`.
61443
61443
 
61444
61444
  One question, one message, defaults pre-stated. Wait for the user's answer.
61445
61445
 
@@ -63279,19 +63279,15 @@ The tool will automatically write the retrospective to \`.swarm/evidence/retro-{
63279
63279
  - \`.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
63280
63280
  If any required file is missing, run the missing gate first. Turbo mode skips all gates automatically.
63281
63281
  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.
63282
- 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.
63282
+ 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.
63283
63283
  If enabled AND this is the LAST phase in the plan (all other phases have status 'complete' and no more phases remain):
63284
- 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.
63285
- 2. Run 1-3 targeted \`web_search\` queries relevant to the project domain.
63286
- 3. Compile a RESEARCH CONTEXT block from search results.
63287
- 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."
63288
- 5. Collect all three JSON responses.
63289
- 6. Call \`convene_general_council\` with mode: 'general', the project summary as question, and the collected round1Responses.
63290
- 7. If disagreements exist, re-dispute as in MODE: COUNCIL step 5-6.
63291
- 8. Present the final synthesis to the user as a project-close summary.
63292
- 9. Write the final council result to \`.swarm/evidence/final-council.json\`.
63293
- 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.
63294
- If enabled but NOT the last phase, skip silently — final council only runs once, after all phases.
63284
+ 1. Build a PROJECT DOSSIER from the completed plan, all phase summaries, changed-file summaries, and all relevant evidence artifacts. This is a completed-project review, not General Council mode.
63285
+ 2. Dispatch \`{{AGENT_PREFIX}}critic\`, \`{{AGENT_PREFIX}}reviewer\`, \`{{AGENT_PREFIX}}sme\`, \`{{AGENT_PREFIX}}test_engineer\`, and \`{{AGENT_PREFIX}}explorer\` in PARALLEL with project-scoped context. Each member must review the entire completed body of work and return a \`CouncilMemberVerdict\` JSON object using \`agent\`, \`verdict\` (APPROVE|CONCERNS|REJECT), \`confidence\`, \`findings[]\`, \`criteriaAssessed[]\`, \`criteriaUnmet[]\`, and \`durationMs\`.
63286
+ 3. Collect the five returned verdict objects. Do NOT fabricate, infer, or substitute verdicts. If a member does not return valid JSON, re-dispatch that member.
63287
+ 4. Call \`write_final_council_evidence\` with \`phase\`, \`projectSummary\`, \`roundNumber\`, and the collected \`verdicts\` array. This writes \`.swarm/evidence/final-council.json\` with plan binding, member verdicts, and quorum metadata.
63288
+ 5. Do NOT call \`convene_general_council\`, do NOT dispatch \`council_generalist\`, \`council_skeptic\`, or \`council_domain_expert\`, and do NOT require \`council.general.enabled\` for this gate. \`final_council\` is the same five-member phase council rerun at project scope.
63289
+ 6. Do NOT call \`phase_complete\` or \`/swarm close\` until \`.swarm/evidence/final-council.json\` exists with an approved, plan-bound, quorumed final-council verdict. When \`final_council\` is enabled, \`phase_complete\` will block until that evidence exists.
63290
+ If enabled but NOT the last phase, skip silently - final council only runs once, after all phases.
63295
63291
  6. Summarize to user
63296
63292
  7. Ask: "Ready for Phase [N+1]?"
63297
63293
 
@@ -85356,6 +85352,99 @@ function buildPhaseCouncilFeedback(phaseNumber, phaseSummary, verdict, vetoedBy,
85356
85352
  return lines.join(`
85357
85353
  `);
85358
85354
  }
85355
+ function synthesizeFinalCouncilAdvisory(projectSummary, verdicts, roundNumber, config3 = {}) {
85356
+ const cfg = { ...COUNCIL_DEFAULTS, ...config3 };
85357
+ const timestamp = new Date().toISOString();
85358
+ const quorumSize = new Set(verdicts.map((v) => v.agent)).size;
85359
+ const rejectingMembers = verdicts.filter((v) => v.verdict === "REJECT").map((v) => v.agent);
85360
+ let overallVerdict;
85361
+ if (cfg.vetoPriority && rejectingMembers.length > 0) {
85362
+ overallVerdict = "REJECT";
85363
+ } else if (verdicts.some((v) => v.verdict === "CONCERNS") || !cfg.vetoPriority && rejectingMembers.length > 0) {
85364
+ overallVerdict = "CONCERNS";
85365
+ } else {
85366
+ overallVerdict = "APPROVE";
85367
+ }
85368
+ const unresolvedConflicts = detectConflicts(verdicts);
85369
+ const rejectingSet = new Set(rejectingMembers);
85370
+ const vetoFindings = verdicts.filter((v) => rejectingSet.has(v.agent)).flatMap((v) => v.findings);
85371
+ const requiredFixes = vetoFindings.filter((f) => f.severity === "HIGH" || f.severity === "MEDIUM");
85372
+ const advisoryFindings = [
85373
+ ...vetoFindings.filter((f) => f.severity === "LOW"),
85374
+ ...verdicts.filter((v) => !rejectingSet.has(v.agent)).flatMap((v) => v.findings)
85375
+ ];
85376
+ const advisoryNotes = [];
85377
+ if (advisoryFindings.length > 0) {
85378
+ advisoryNotes.push(`Final council found ${advisoryFindings.length} advisory finding(s). Review before project close.`);
85379
+ }
85380
+ if (quorumSize < 3) {
85381
+ advisoryNotes.push(`Final council quorum is ${quorumSize} members - dispatch additional project-scoped council members before closing the project.`);
85382
+ }
85383
+ const allUnmetIds = new Set(verdicts.flatMap((v) => v.criteriaUnmet));
85384
+ const allCriteriaMet = allUnmetIds.size === 0 && verdicts.length > 0;
85385
+ const unifiedFeedbackMd = buildFinalCouncilFeedback(projectSummary, overallVerdict, rejectingMembers, requiredFixes, advisoryFindings, unresolvedConflicts, roundNumber, cfg.maxRounds);
85386
+ return {
85387
+ scope: "project",
85388
+ timestamp,
85389
+ overallVerdict,
85390
+ vetoedBy: rejectingMembers.length > 0 ? rejectingMembers : null,
85391
+ memberVerdicts: verdicts,
85392
+ unresolvedConflicts,
85393
+ requiredFixes,
85394
+ advisoryFindings,
85395
+ advisoryNotes,
85396
+ unifiedFeedbackMd,
85397
+ roundNumber,
85398
+ allCriteriaMet,
85399
+ quorumSize,
85400
+ evidencePath: ".swarm/evidence/final-council.json",
85401
+ projectSummary
85402
+ };
85403
+ }
85404
+ function buildFinalCouncilFeedback(projectSummary, verdict, vetoedBy, requiredFixes, advisoryFindings, conflicts, roundNumber, maxRounds) {
85405
+ const lines = [
85406
+ `## Final Council Review - Round ${roundNumber}/${maxRounds}`,
85407
+ `**Scope:** completed project **Overall verdict:** ${verdict}`,
85408
+ ""
85409
+ ];
85410
+ if (projectSummary) {
85411
+ lines.push(`**Project Summary:** ${projectSummary}`);
85412
+ lines.push("");
85413
+ }
85414
+ if (vetoedBy.length > 0) {
85415
+ lines.push(`> BLOCKED: project close is blocked by ${vetoedBy.join(", ")}`);
85416
+ lines.push("");
85417
+ }
85418
+ if (requiredFixes.length > 0) {
85419
+ lines.push("### Required Fixes (must resolve before project close)");
85420
+ for (const f of requiredFixes) {
85421
+ lines.push(`- **[${f.severity}]** \`${f.location}\` - ${f.detail}`, ` _Evidence:_ ${f.evidence}`);
85422
+ }
85423
+ lines.push("");
85424
+ }
85425
+ if (conflicts.length > 0) {
85426
+ lines.push("### Conflicts to Resolve");
85427
+ lines.push("_The following council members gave contradictory project-close instructions. Architect must resolve before closing the project._");
85428
+ for (const c of conflicts) {
85429
+ lines.push(`- ${c}`);
85430
+ }
85431
+ lines.push("");
85432
+ }
85433
+ if (advisoryFindings.length > 0) {
85434
+ lines.push("### Advisory Findings (non-blocking)");
85435
+ for (const f of advisoryFindings) {
85436
+ lines.push(`- **[${f.severity}]** \`${f.location}\` - ${f.detail}`);
85437
+ }
85438
+ lines.push("");
85439
+ }
85440
+ if (verdict === "APPROVE") {
85441
+ lines.push("> Final council approved. Project may proceed to close.");
85442
+ } else if (roundNumber >= maxRounds) {
85443
+ lines.push(`> Max rounds (${maxRounds}) reached. Escalate to user - do not close the project automatically.`);
85444
+ }
85445
+ return lines.join(`
85446
+ `);
85447
+ }
85359
85448
 
85360
85449
  // src/council/criteria-store.ts
85361
85450
  import { existsSync as existsSync50, mkdirSync as mkdirSync23, readFileSync as readFileSync42, writeFileSync as writeFileSync16 } from "node:fs";
@@ -89823,6 +89912,41 @@ Advisory notes: ${advisoryNotes.join("; ")}` : "";
89823
89912
  }, null, 2);
89824
89913
  }
89825
89914
  }
89915
+ if (typeof entry.quorumSize !== "number" || !Number.isFinite(entry.quorumSize) || entry.quorumSize < 5) {
89916
+ return JSON.stringify({
89917
+ success: false,
89918
+ phase,
89919
+ status: "blocked",
89920
+ reason: "FINAL_COUNCIL_MISSING_QUORUM",
89921
+ message: `Phase ${phase} (last phase) cannot be completed: final council evidence is missing valid quorum metadata. Re-run the project-scoped five-member final council and call write_final_council_evidence to generate quorumed evidence.`,
89922
+ agentsDispatched,
89923
+ agentsMissing: [],
89924
+ warnings: []
89925
+ }, null, 2);
89926
+ }
89927
+ const requiredFinalCouncilMembers = [
89928
+ "critic",
89929
+ "reviewer",
89930
+ "sme",
89931
+ "test_engineer",
89932
+ "explorer"
89933
+ ];
89934
+ const membersVoted = Array.isArray(entry.membersVoted) ? entry.membersVoted.filter((member) => typeof member === "string") : [];
89935
+ const membersAbsent = Array.isArray(entry.membersAbsent) ? entry.membersAbsent.filter((member) => typeof member === "string") : [];
89936
+ const distinctMembersVoted = new Set(membersVoted);
89937
+ const hasAllRequiredMembers = requiredFinalCouncilMembers.every((member) => distinctMembersVoted.has(member)) && distinctMembersVoted.size === requiredFinalCouncilMembers.length && membersAbsent.length === 0;
89938
+ if (!hasAllRequiredMembers) {
89939
+ return JSON.stringify({
89940
+ success: false,
89941
+ phase,
89942
+ status: "blocked",
89943
+ reason: "FINAL_COUNCIL_MISSING_QUORUM",
89944
+ message: `Phase ${phase} (last phase) cannot be completed: final council evidence does not prove all five required members voted. Re-run the project-scoped five-member final council and call write_final_council_evidence to generate complete evidence.`,
89945
+ agentsDispatched,
89946
+ agentsMissing: [],
89947
+ warnings: []
89948
+ }, null, 2);
89949
+ }
89826
89950
  if (entry.verdict === "rejected" || entry.verdict === "REJECTED") {
89827
89951
  return JSON.stringify({
89828
89952
  success: false,
@@ -89862,11 +89986,11 @@ Advisory notes: ${advisoryNotes.join("; ")}` : "";
89862
89986
  status: "blocked",
89863
89987
  reason: "FINAL_COUNCIL_REQUIRED",
89864
89988
  final_council_required: true,
89865
- 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.`,
89989
+ message: `Phase ${phase} (last phase) cannot be completed: final_council is enabled and final council evidence not found at .swarm/evidence/final-council.json. Dispatch critic, reviewer, sme, test_engineer, and explorer with project-scoped context, collect their CouncilMemberVerdict JSON, and call write_final_council_evidence before completing the project. Do not use convene_general_council for this gate.`,
89866
89990
  agentsDispatched,
89867
89991
  agentsMissing: [],
89868
89992
  warnings: [
89869
- `Final council required convene a holistic project review using convene_general_council, then call write_final_council_evidence to persist the verdict.`
89993
+ `Final council required - dispatch the five project-scoped council members, then call write_final_council_evidence to persist quorumed evidence.`
89870
89994
  ]
89871
89995
  }, null, 2);
89872
89996
  }
@@ -97343,7 +97467,7 @@ var set_qa_gates = createSwarmTool({
97343
97467
  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."),
97344
97468
  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."),
97345
97469
  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."),
97346
- 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."),
97470
+ final_council: exports_external.boolean().optional().describe("Enable the final_council gate (default: off). When on, " + "after all phases complete the architect dispatches critic, reviewer, " + "sme, test_engineer, and explorer with project-scoped context, " + "collects their CouncilMemberVerdict objects, and calls " + "write_final_council_evidence. This is not General Council mode " + "and does not require council.general.enabled."),
97347
97471
  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.')
97348
97472
  },
97349
97473
  execute: async (args2, directory) => {
@@ -102301,59 +102425,94 @@ var write_drift_evidence = createSwarmTool({
102301
102425
  });
102302
102426
  // src/tools/write-final-council-evidence.ts
102303
102427
  init_zod();
102428
+ init_loader();
102429
+ import fs107 from "node:fs";
102430
+ import path135 from "node:path";
102304
102431
  init_utils2();
102305
102432
  init_manager();
102306
102433
  init_create_tool();
102307
- import fs107 from "node:fs";
102308
- import path135 from "node:path";
102309
- function normalizeVerdict2(verdict) {
102310
- switch (verdict) {
102311
- case "APPROVED":
102312
- return "approved";
102313
- case "NEEDS_REVISION":
102314
- return "rejected";
102315
- default:
102316
- throw new Error(`Invalid verdict: must be 'APPROVED' or 'NEEDS_REVISION', got '${verdict}'`);
102317
- }
102434
+ var FINAL_COUNCIL_MEMBERS = [
102435
+ "critic",
102436
+ "reviewer",
102437
+ "sme",
102438
+ "test_engineer",
102439
+ "explorer"
102440
+ ];
102441
+ var VerdictSchema3 = exports_external.object({
102442
+ agent: exports_external.enum(FINAL_COUNCIL_MEMBERS),
102443
+ verdict: exports_external.enum(["APPROVE", "CONCERNS", "REJECT"]),
102444
+ confidence: exports_external.number().min(0).max(1),
102445
+ findings: exports_external.array(exports_external.object({
102446
+ severity: exports_external.enum(["HIGH", "MEDIUM", "LOW"]),
102447
+ category: exports_external.string().min(1),
102448
+ location: exports_external.string(),
102449
+ detail: exports_external.string(),
102450
+ evidence: exports_external.string()
102451
+ })),
102452
+ criteriaAssessed: exports_external.array(exports_external.string()),
102453
+ criteriaUnmet: exports_external.array(exports_external.string()),
102454
+ durationMs: exports_external.number().nonnegative()
102455
+ });
102456
+ var ArgsSchema6 = exports_external.object({
102457
+ phase: exports_external.number().int().min(1),
102458
+ projectSummary: exports_external.string().min(1),
102459
+ roundNumber: exports_external.number().int().min(1).max(10).optional(),
102460
+ verdicts: exports_external.array(VerdictSchema3).min(1).max(5)
102461
+ });
102462
+ function normalizeFinalVerdict(verdict) {
102463
+ return verdict === "APPROVE" ? "approved" : "rejected";
102318
102464
  }
102319
102465
  async function executeWriteFinalCouncilEvidence(args2, directory) {
102320
- const phase = args2.phase;
102321
- if (!Number.isInteger(phase) || phase < 1) {
102322
- return JSON.stringify({
102323
- success: false,
102324
- phase,
102325
- message: "Invalid phase: must be a positive integer"
102326
- }, null, 2);
102327
- }
102328
- const validVerdicts = ["APPROVED", "NEEDS_REVISION"];
102329
- if (!validVerdicts.includes(args2.verdict)) {
102466
+ const parsed = ArgsSchema6.safeParse(args2);
102467
+ if (!parsed.success) {
102330
102468
  return JSON.stringify({
102331
102469
  success: false,
102332
- phase,
102333
- message: "Invalid verdict: must be 'APPROVED' or 'NEEDS_REVISION'"
102470
+ reason: "invalid arguments",
102471
+ errors: parsed.error.issues.map((i2) => ({
102472
+ path: i2.path.join("."),
102473
+ message: i2.message
102474
+ }))
102334
102475
  }, null, 2);
102335
102476
  }
102336
- const summary = args2.summary;
102337
- if (typeof summary !== "string" || summary.trim().length === 0) {
102477
+ const input = parsed.data;
102478
+ const config3 = loadPluginConfig(directory);
102479
+ const requiredMembers = FINAL_COUNCIL_MEMBERS.length;
102480
+ const distinctMembers = new Set(input.verdicts.map((v) => v.agent));
102481
+ const membersVoted = [...distinctMembers];
102482
+ const membersAbsent = FINAL_COUNCIL_MEMBERS.filter((m) => !distinctMembers.has(m));
102483
+ if (membersVoted.length < requiredMembers) {
102338
102484
  return JSON.stringify({
102339
102485
  success: false,
102340
- phase,
102341
- message: "Invalid summary: must be a non-empty string"
102486
+ reason: "insufficient_quorum",
102487
+ message: `Final council quorum not met: ${membersVoted.length} of ${requiredMembers} required members provided verdicts. ` + `Members voted: [${membersVoted.join(", ")}]. ` + `Members absent: [${membersAbsent.join(", ")}]. ` + `Dispatch the absent council members with project-scoped context and collect their verdicts before calling write_final_council_evidence.`,
102488
+ membersVoted,
102489
+ membersAbsent,
102490
+ quorumRequired: requiredMembers
102342
102491
  }, null, 2);
102343
102492
  }
102344
- const normalizedVerdict = normalizeVerdict2(args2.verdict);
102493
+ const synthesis = synthesizeFinalCouncilAdvisory(input.projectSummary.trim(), input.verdicts, input.roundNumber ?? 1, config3.council);
102345
102494
  const plan = await loadPlan(directory);
102346
102495
  const planId = plan ? derivePlanId(plan) : "unknown";
102496
+ const normalizedVerdict = normalizeFinalVerdict(synthesis.overallVerdict);
102347
102497
  const evidenceEntry = {
102348
102498
  type: "final-council",
102349
- phase,
102499
+ phase: input.phase,
102350
102500
  plan_id: planId,
102351
102501
  verdict: normalizedVerdict,
102352
- summary: summary.trim(),
102353
- timestamp: new Date().toISOString()
102354
- };
102355
- const evidenceContent = {
102356
- entries: [evidenceEntry]
102502
+ rawCouncilVerdict: synthesis.overallVerdict,
102503
+ quorumSize: synthesis.quorumSize,
102504
+ membersVoted,
102505
+ membersAbsent,
102506
+ requiredFixes: synthesis.requiredFixes,
102507
+ advisoryFindings: synthesis.advisoryFindings,
102508
+ advisoryNotes: synthesis.advisoryNotes,
102509
+ unresolvedConflicts: synthesis.unresolvedConflicts,
102510
+ roundNumber: synthesis.roundNumber,
102511
+ allCriteriaMet: synthesis.allCriteriaMet,
102512
+ memberVerdicts: synthesis.memberVerdicts,
102513
+ unifiedFeedbackMd: synthesis.unifiedFeedbackMd,
102514
+ projectSummary: synthesis.projectSummary,
102515
+ timestamp: synthesis.timestamp
102357
102516
  };
102358
102517
  const filename = "final-council.json";
102359
102518
  const relativePath = path135.join("evidence", filename);
@@ -102363,10 +102522,13 @@ async function executeWriteFinalCouncilEvidence(args2, directory) {
102363
102522
  } catch (error93) {
102364
102523
  return JSON.stringify({
102365
102524
  success: false,
102366
- phase,
102525
+ phase: input.phase,
102367
102526
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
102368
102527
  }, null, 2);
102369
102528
  }
102529
+ const evidenceContent = {
102530
+ entries: [evidenceEntry]
102531
+ };
102370
102532
  const evidenceDir = path135.dirname(validatedPath);
102371
102533
  try {
102372
102534
  await fs107.promises.mkdir(evidenceDir, { recursive: true });
@@ -102375,41 +102537,53 @@ async function executeWriteFinalCouncilEvidence(args2, directory) {
102375
102537
  await fs107.promises.rename(tempPath, validatedPath);
102376
102538
  return JSON.stringify({
102377
102539
  success: true,
102378
- phase,
102540
+ phase: input.phase,
102541
+ overallVerdict: synthesis.overallVerdict,
102379
102542
  verdict: normalizedVerdict,
102380
- message: `Final council evidence written to .swarm/evidence/final-council.json`
102543
+ vetoedBy: synthesis.vetoedBy,
102544
+ roundNumber: synthesis.roundNumber,
102545
+ allCriteriaMet: synthesis.allCriteriaMet,
102546
+ requiredFixesCount: synthesis.requiredFixes.length,
102547
+ advisoryFindingsCount: synthesis.advisoryFindings.length,
102548
+ unresolvedConflictsCount: synthesis.unresolvedConflicts.length,
102549
+ advisoryNotes: synthesis.advisoryNotes,
102550
+ membersVoted,
102551
+ membersAbsent,
102552
+ quorumSize: synthesis.quorumSize,
102553
+ quorumMet: true,
102554
+ evidencePath: synthesis.evidencePath,
102555
+ unifiedFeedbackMd: synthesis.unifiedFeedbackMd,
102556
+ message: "Final council evidence written to .swarm/evidence/final-council.json"
102381
102557
  }, null, 2);
102382
102558
  } catch (error93) {
102383
102559
  return JSON.stringify({
102384
102560
  success: false,
102385
- phase,
102561
+ phase: input.phase,
102386
102562
  message: error93 instanceof Error ? error93.message : String(error93)
102387
102563
  }, null, 2);
102388
102564
  }
102389
102565
  }
102390
102566
  var write_final_council_evidence = createSwarmTool({
102391
- 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.",
102567
+ description: "Write final council evidence for a completed project. This is not General Council mode and does not use convene_general_council. PREREQUISITE: dispatch critic, reviewer, sme, test_engineer, and explorer as project-scoped Agent tasks, collect their CouncilMemberVerdict JSON, then call this tool to synthesize and persist .swarm/evidence/final-council.json.",
102392
102568
  args: {
102393
- phase: exports_external.number().int().min(1).describe("The phase number for the final council verdict (e.g., 1, 2, 3)"),
102394
- verdict: exports_external.enum(["APPROVED", "NEEDS_REVISION"]).describe("Verdict of the final council: 'APPROVED' or 'NEEDS_REVISION'"),
102395
- summary: exports_external.string().describe("Human-readable summary of the final council verdict")
102569
+ phase: exports_external.number().int().min(1).describe("The final phase number for the project being reviewed"),
102570
+ projectSummary: exports_external.string().min(1).describe("Summary of the completed project and total work reviewed"),
102571
+ roundNumber: exports_external.number().int().min(1).max(10).optional().describe("1-indexed final council round number. Defaults to 1."),
102572
+ verdicts: exports_external.array(VerdictSchema3).min(1).max(5).describe("Collected CouncilMemberVerdict objects from critic, reviewer, sme, test_engineer, and explorer.")
102396
102573
  },
102397
102574
  execute: async (args2, directory) => {
102398
- const rawPhase = args2.phase !== undefined ? Number(args2.phase) : 0;
102399
- try {
102400
- const writeFinalCouncilEvidenceArgs = {
102401
- phase: Number(args2.phase),
102402
- verdict: String(args2.verdict),
102403
- summary: String(args2.summary ?? "")
102404
- };
102405
- return await executeWriteFinalCouncilEvidence(writeFinalCouncilEvidenceArgs, directory);
102406
- } catch (error93) {
102575
+ const parsed = ArgsSchema6.safeParse(args2);
102576
+ if (!parsed.success) {
102407
102577
  return JSON.stringify({
102408
102578
  success: false,
102409
- phase: rawPhase,
102410
- message: error93 instanceof Error ? error93.message : "Unknown error"
102579
+ reason: "invalid arguments",
102580
+ errors: parsed.error.issues.map((i2) => ({
102581
+ path: i2.path.join("."),
102582
+ message: i2.message
102583
+ }))
102411
102584
  }, null, 2);
102412
102585
  }
102586
+ return await executeWriteFinalCouncilEvidence(parsed.data, directory);
102413
102587
  }
102414
102588
  });
102415
102589
  // src/tools/write-hallucination-evidence.ts
@@ -102418,7 +102592,7 @@ init_utils2();
102418
102592
  init_create_tool();
102419
102593
  import fs108 from "node:fs";
102420
102594
  import path136 from "node:path";
102421
- function normalizeVerdict3(verdict) {
102595
+ function normalizeVerdict2(verdict) {
102422
102596
  switch (verdict) {
102423
102597
  case "APPROVED":
102424
102598
  return "approved";
@@ -102453,7 +102627,7 @@ async function executeWriteHallucinationEvidence(args2, directory) {
102453
102627
  message: "Invalid summary: must be a non-empty string"
102454
102628
  }, null, 2);
102455
102629
  }
102456
- const normalizedVerdict = normalizeVerdict3(args2.verdict);
102630
+ const normalizedVerdict = normalizeVerdict2(args2.verdict);
102457
102631
  const evidenceEntry = {
102458
102632
  type: "hallucination-verification",
102459
102633
  verdict: normalizedVerdict,
@@ -102529,7 +102703,7 @@ init_utils2();
102529
102703
  init_create_tool();
102530
102704
  import fs109 from "node:fs";
102531
102705
  import path137 from "node:path";
102532
- function normalizeVerdict4(verdict) {
102706
+ function normalizeVerdict3(verdict) {
102533
102707
  switch (verdict) {
102534
102708
  case "PASS":
102535
102709
  return "pass";
@@ -102586,7 +102760,7 @@ async function executeWriteMutationEvidence(args2, directory) {
102586
102760
  message: "Invalid summary: must be a non-empty string"
102587
102761
  }, null, 2);
102588
102762
  }
102589
- const normalizedVerdict = normalizeVerdict4(args2.verdict);
102763
+ const normalizedVerdict = normalizeVerdict3(args2.verdict);
102590
102764
  const evidenceEntry = {
102591
102765
  type: "mutation-gate",
102592
102766
  verdict: normalizedVerdict,
@@ -1,29 +1,66 @@
1
1
  /**
2
- * Write final council evidence tool for persisting final holistic council verdicts.
3
- * Accepts phase, verdict, and summary from the Architect and writes
4
- * a structured evidence file to the flat evidence root (not per-phase).
2
+ * Write final council evidence for the project-scoped final council gate.
3
+ *
4
+ * The final council is not General Council mode. It accepts the same
5
+ * five-member CouncilMemberVerdict objects used by phase council, synthesized
6
+ * at completed-project scope.
5
7
  */
6
8
  import type { ToolDefinition } from '@opencode-ai/plugin/tool';
9
+ import { z } from 'zod';
10
+ import type { CouncilMemberVerdict } from '../council/types';
11
+ export declare const ArgsSchema: z.ZodObject<{
12
+ phase: z.ZodNumber;
13
+ projectSummary: z.ZodString;
14
+ roundNumber: z.ZodOptional<z.ZodNumber>;
15
+ verdicts: z.ZodArray<z.ZodObject<{
16
+ agent: z.ZodEnum<{
17
+ reviewer: "reviewer";
18
+ test_engineer: "test_engineer";
19
+ explorer: "explorer";
20
+ sme: "sme";
21
+ critic: "critic";
22
+ }>;
23
+ verdict: z.ZodEnum<{
24
+ APPROVE: "APPROVE";
25
+ REJECT: "REJECT";
26
+ CONCERNS: "CONCERNS";
27
+ }>;
28
+ confidence: z.ZodNumber;
29
+ findings: z.ZodArray<z.ZodObject<{
30
+ severity: z.ZodEnum<{
31
+ HIGH: "HIGH";
32
+ MEDIUM: "MEDIUM";
33
+ LOW: "LOW";
34
+ }>;
35
+ category: z.ZodString;
36
+ location: z.ZodString;
37
+ detail: z.ZodString;
38
+ evidence: z.ZodString;
39
+ }, z.core.$strip>>;
40
+ criteriaAssessed: z.ZodArray<z.ZodString>;
41
+ criteriaUnmet: z.ZodArray<z.ZodString>;
42
+ durationMs: z.ZodNumber;
43
+ }, z.core.$strip>>;
44
+ }, z.core.$strip>;
7
45
  /**
8
- * Arguments for the write_final_council_evidence tool
46
+ * Arguments for the write_final_council_evidence tool.
9
47
  */
10
48
  export interface WriteFinalCouncilEvidenceArgs {
11
49
  /** The phase number for the final council verdict */
12
50
  phase: number;
13
- /** Verdict of the final council: 'APPROVED' or 'NEEDS_REVISION' */
14
- verdict: 'APPROVED' | 'NEEDS_REVISION';
15
- /** Human-readable summary of the final council verdict */
16
- summary: string;
51
+ /** Summary of the completed project being reviewed */
52
+ projectSummary: string;
53
+ /** 1-indexed final council round number */
54
+ roundNumber?: number;
55
+ /** Collected verdicts from critic, reviewer, sme, test_engineer, explorer */
56
+ verdicts: CouncilMemberVerdict[];
17
57
  }
18
58
  /**
19
59
  * Execute the write_final_council_evidence tool.
20
- * Validates input, builds an evidence entry, and writes to disk.
21
- * @param args - The write final council evidence arguments
22
- * @param directory - Working directory
23
- * @returns JSON string with success status and details
60
+ * Validates input, synthesizes project-scoped council evidence, and writes it.
24
61
  */
25
- export declare function executeWriteFinalCouncilEvidence(args: WriteFinalCouncilEvidenceArgs, directory: string): Promise<string>;
62
+ export declare function executeWriteFinalCouncilEvidence(args: unknown, directory: string): Promise<string>;
26
63
  /**
27
- * Tool definition for write_final_council_evidence
64
+ * Tool definition for write_final_council_evidence.
28
65
  */
29
66
  export declare const write_final_council_evidence: ToolDefinition;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.17.1",
3
+ "version": "7.17.2",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",