opencode-swarm 7.17.0 → 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.0",
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",
@@ -15935,7 +15935,6 @@ async function handleAcknowledgeSpecDriftCommand(directory, _args) {
15935
15935
  return "Spec staleness file was corrupted. It has been removed.";
15936
15936
  }
15937
15937
  const { planTitle, phase } = stalenessData;
15938
- await fsPromises3.unlink(specStalenessPath);
15939
15938
  let currentHash = null;
15940
15939
  let planUpdateSkipped = false;
15941
15940
  try {
@@ -15949,6 +15948,9 @@ async function handleAcknowledgeSpecDriftCommand(directory, _args) {
15949
15948
  console.error("[acknowledge-spec-drift] Failed to update plan specHash:", planError instanceof Error ? planError.message : String(planError));
15950
15949
  planUpdateSkipped = true;
15951
15950
  }
15951
+ if (!planUpdateSkipped) {
15952
+ await fsPromises3.unlink(specStalenessPath);
15953
+ }
15952
15954
  const eventsPath = validateSwarmPath(directory, "events.jsonl");
15953
15955
  const acknowledgmentEvent = {
15954
15956
  type: "spec_drift_acknowledged",
@@ -35412,6 +35414,8 @@ async function quarantineEntry(directory, entryId, reason, reportedBy) {
35412
35414
  const remaining = entries.filter((e) => e.id !== entryId);
35413
35415
  const quarantined = {
35414
35416
  ...entry,
35417
+ status: "quarantined",
35418
+ original_status: entry.status,
35415
35419
  quarantine_reason: sanitizedReason,
35416
35420
  quarantined_at: new Date().toISOString(),
35417
35421
  reported_by: reportedBy
@@ -35470,7 +35474,24 @@ async function restoreEntry(directory, entryId) {
35470
35474
  return;
35471
35475
  }
35472
35476
  const remaining = quarantinedEntries.filter((e) => e.id !== entryId);
35473
- const { quarantine_reason, quarantined_at, reported_by, ...original } = entryToRestore;
35477
+ const {
35478
+ quarantine_reason,
35479
+ quarantined_at,
35480
+ reported_by,
35481
+ original_status,
35482
+ status: _quarantineStatus,
35483
+ ...rest
35484
+ } = entryToRestore;
35485
+ const original = { ...rest, status: original_status ?? "candidate" };
35486
+ const validation = validateLesson(original.lesson, [], {
35487
+ category: original.category,
35488
+ scope: original.scope,
35489
+ confidence: original.confidence
35490
+ });
35491
+ if (!validation.valid) {
35492
+ warn(`[knowledge-validator] restoreEntry: entry ${entryId} failed re-validation: ${validation.reason}`);
35493
+ return;
35494
+ }
35474
35495
  const jsonlContent = remaining.length > 0 ? `${remaining.map((e) => JSON.stringify(e)).join(`
35475
35496
  `)}
35476
35497
  ` : "";
@@ -46325,7 +46346,7 @@ async function selectEntryPoints2(dir) {
46325
46346
  `)) {
46326
46347
  const m = line.match(/=\s*['"]([^'":]+)/);
46327
46348
  if (m) {
46328
- const modPath = m[1].replace(/\./g, "/") + ".py";
46349
+ const modPath = `${m[1].replace(/\./g, "/")}.py`;
46329
46350
  points.add(modPath);
46330
46351
  }
46331
46352
  }
@@ -47508,7 +47529,7 @@ function clearDispatchCache() {
47508
47529
  manifestRootCache.clear();
47509
47530
  insertCounter = 0;
47510
47531
  }
47511
- var _internals22, cache, insertCounter = 0, MANIFEST_FILES, MANIFEST_SET, manifestRootCache;
47532
+ var _internals22, cache, insertCounter = 0, MANIFEST_FILES, _MANIFEST_SET, manifestRootCache;
47512
47533
  var init_dispatch = __esm(() => {
47513
47534
  init_backends();
47514
47535
  init_detector();
@@ -47540,7 +47561,7 @@ var init_dispatch = __esm(() => {
47540
47561
  "Gemfile",
47541
47562
  "composer.json"
47542
47563
  ];
47543
- MANIFEST_SET = new Set(MANIFEST_FILES);
47564
+ _MANIFEST_SET = new Set(MANIFEST_FILES);
47544
47565
  manifestRootCache = new Map;
47545
47566
  });
47546
47567
 
@@ -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;
@@ -9,7 +9,8 @@ import type { KnowledgeConfig, MessageWithParts } from './knowledge-types.js';
9
9
  /**
10
10
  * Creates a knowledge injection hook that injects relevant knowledge into the
11
11
  * architect's message context at phase start. Supports caching for re-injection
12
- * after compaction.
12
+ * after compaction. Cache is per-instance (bound to the returned hook closure),
13
+ * ensuring no cross-test pollution in Bun's shared test-runner process.
13
14
  *
14
15
  * @param directory - The project directory containing .swarm/
15
16
  * @param config - Knowledge system configuration
@@ -81,7 +81,7 @@ export interface KnowledgeEntryBase extends ActionableDirectiveFields {
81
81
  tags: string[];
82
82
  scope: string;
83
83
  confidence: number;
84
- status: 'candidate' | 'established' | 'promoted' | 'archived';
84
+ status: 'candidate' | 'established' | 'promoted' | 'archived' | 'quarantined';
85
85
  confirmed_by: PhaseConfirmationRecord[] | ProjectConfirmationRecord[];
86
86
  retrieval_outcomes: RetrievalOutcome;
87
87
  schema_version: number;
@@ -31,6 +31,7 @@ export declare function validateSkillPath(p: unknown): boolean;
31
31
  export declare function validateActionableFields(fields: ActionableDirectiveFields | undefined): ActionableValidationResult;
32
32
  export type { ActionableDirectiveFields, DirectivePriority };
33
33
  export interface QuarantinedEntry extends KnowledgeEntryBase {
34
+ original_status: string;
34
35
  quarantine_reason: string;
35
36
  quarantined_at: string;
36
37
  reported_by: 'architect' | 'user' | 'auto';
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.0",
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",
@@ -18445,7 +18445,6 @@ async function handleAcknowledgeSpecDriftCommand(directory, _args) {
18445
18445
  return "Spec staleness file was corrupted. It has been removed.";
18446
18446
  }
18447
18447
  const { planTitle, phase } = stalenessData;
18448
- await fsPromises4.unlink(specStalenessPath);
18449
18448
  let currentHash = null;
18450
18449
  let planUpdateSkipped = false;
18451
18450
  try {
@@ -18459,6 +18458,9 @@ async function handleAcknowledgeSpecDriftCommand(directory, _args) {
18459
18458
  console.error("[acknowledge-spec-drift] Failed to update plan specHash:", planError instanceof Error ? planError.message : String(planError));
18460
18459
  planUpdateSkipped = true;
18461
18460
  }
18461
+ if (!planUpdateSkipped) {
18462
+ await fsPromises4.unlink(specStalenessPath);
18463
+ }
18462
18464
  const eventsPath = validateSwarmPath(directory, "events.jsonl");
18463
18465
  const acknowledgmentEvent = {
18464
18466
  type: "spec_drift_acknowledged",
@@ -42470,6 +42472,8 @@ async function quarantineEntry(directory, entryId, reason, reportedBy) {
42470
42472
  const remaining = entries.filter((e) => e.id !== entryId);
42471
42473
  const quarantined = {
42472
42474
  ...entry,
42475
+ status: "quarantined",
42476
+ original_status: entry.status,
42473
42477
  quarantine_reason: sanitizedReason,
42474
42478
  quarantined_at: new Date().toISOString(),
42475
42479
  reported_by: reportedBy
@@ -42528,7 +42532,24 @@ async function restoreEntry(directory, entryId) {
42528
42532
  return;
42529
42533
  }
42530
42534
  const remaining = quarantinedEntries.filter((e) => e.id !== entryId);
42531
- const { quarantine_reason, quarantined_at, reported_by, ...original } = entryToRestore;
42535
+ const {
42536
+ quarantine_reason,
42537
+ quarantined_at,
42538
+ reported_by,
42539
+ original_status,
42540
+ status: _quarantineStatus,
42541
+ ...rest
42542
+ } = entryToRestore;
42543
+ const original = { ...rest, status: original_status ?? "candidate" };
42544
+ const validation = validateLesson(original.lesson, [], {
42545
+ category: original.category,
42546
+ scope: original.scope,
42547
+ confidence: original.confidence
42548
+ });
42549
+ if (!validation.valid) {
42550
+ warn(`[knowledge-validator] restoreEntry: entry ${entryId} failed re-validation: ${validation.reason}`);
42551
+ return;
42552
+ }
42532
42553
  const jsonlContent = remaining.length > 0 ? `${remaining.map((e) => JSON.stringify(e)).join(`
42533
42554
  `)}
42534
42555
  ` : "";
@@ -44359,7 +44380,9 @@ async function readMergedKnowledge(directory, config3, context) {
44359
44380
  const maxInject = config3.max_inject_count ?? 5;
44360
44381
  const topN = ranked.slice(0, maxInject);
44361
44382
  if (topN.length > 0 && context?.currentPhase) {
44362
- recordLessonsShown(directory, topN.map((e) => e.id), context.currentPhase).catch(() => {});
44383
+ recordLessonsShown(directory, topN.map((e) => e.id), context.currentPhase).catch((err2) => {
44384
+ warn("[knowledge-reader] recordLessonsShown unexpected rejection:", err2);
44385
+ });
44363
44386
  }
44364
44387
  return topN;
44365
44388
  }
@@ -54848,7 +54871,7 @@ async function selectEntryPoints2(dir) {
54848
54871
  `)) {
54849
54872
  const m = line.match(/=\s*['"]([^'":]+)/);
54850
54873
  if (m) {
54851
- const modPath = m[1].replace(/\./g, "/") + ".py";
54874
+ const modPath = `${m[1].replace(/\./g, "/")}.py`;
54852
54875
  points.add(modPath);
54853
54876
  }
54854
54877
  }
@@ -56031,7 +56054,7 @@ function clearDispatchCache() {
56031
56054
  manifestRootCache.clear();
56032
56055
  insertCounter = 0;
56033
56056
  }
56034
- var _internals28, cache, insertCounter = 0, MANIFEST_FILES, MANIFEST_SET, manifestRootCache;
56057
+ var _internals28, cache, insertCounter = 0, MANIFEST_FILES, _MANIFEST_SET, manifestRootCache;
56035
56058
  var init_dispatch = __esm(() => {
56036
56059
  init_backends();
56037
56060
  init_detector();
@@ -56063,7 +56086,7 @@ var init_dispatch = __esm(() => {
56063
56086
  "Gemfile",
56064
56087
  "composer.json"
56065
56088
  ];
56066
- MANIFEST_SET = new Set(MANIFEST_FILES);
56089
+ _MANIFEST_SET = new Set(MANIFEST_FILES);
56067
56090
  manifestRootCache = new Map;
56068
56091
  });
56069
56092
 
@@ -61416,7 +61439,7 @@ Present the eleven gates with their defaults (DEFAULT_QA_GATES) as a single user
61416
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)
61417
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.
61418
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)
61419
- - 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\`.
61420
61443
 
61421
61444
  One question, one message, defaults pre-stated. Wait for the user's answer.
61422
61445
 
@@ -63256,19 +63279,15 @@ The tool will automatically write the retrospective to \`.swarm/evidence/retro-{
63256
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
63257
63280
  If any required file is missing, run the missing gate first. Turbo mode skips all gates automatically.
63258
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.
63259
- 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.
63260
63283
  If enabled AND this is the LAST phase in the plan (all other phases have status 'complete' and no more phases remain):
63261
- 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.
63262
- 2. Run 1-3 targeted \`web_search\` queries relevant to the project domain.
63263
- 3. Compile a RESEARCH CONTEXT block from search results.
63264
- 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."
63265
- 5. Collect all three JSON responses.
63266
- 6. Call \`convene_general_council\` with mode: 'general', the project summary as question, and the collected round1Responses.
63267
- 7. If disagreements exist, re-dispute as in MODE: COUNCIL step 5-6.
63268
- 8. Present the final synthesis to the user as a project-close summary.
63269
- 9. Write the final council result to \`.swarm/evidence/final-council.json\`.
63270
- 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.
63271
- 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.
63272
63291
  6. Summarize to user
63273
63292
  7. Ask: "Ready for Phase [N+1]?"
63274
63293
 
@@ -71420,7 +71439,7 @@ function decisionFromVerdict(verdict, escalationNeeded) {
71420
71439
  if (verdict === "APPROVED" || verdict === "ANSWER")
71421
71440
  return "allow";
71422
71441
  if (verdict === "BLOCKED")
71423
- return "pause";
71442
+ return "deny";
71424
71443
  if (verdict === "PENDING")
71425
71444
  return "pending";
71426
71445
  return "deny";
@@ -80646,7 +80665,7 @@ function createFullAutoPermissionHook(options) {
80646
80665
  phase: effectivePhase,
80647
80666
  taskID: taskId ?? undefined,
80648
80667
  planID: runState.planID,
80649
- architectOutput: undefined,
80668
+ architectOutput: output.args ? JSON.stringify(output.args) : undefined,
80650
80669
  actionContext: {
80651
80670
  tool: toolName,
80652
80671
  ...decision.context ?? {}
@@ -81352,6 +81371,7 @@ init_extractors();
81352
81371
  init_knowledge_reader();
81353
81372
  init_knowledge_store();
81354
81373
  init_utils2();
81374
+ var INJECTION_SENTINEL = "‌[[KNOWLEDGE-INJECTED]]";
81355
81375
  function buildKnowledgeBlock(entries, charBudget, cfg, currentProject) {
81356
81376
  if (entries.length === 0)
81357
81377
  return null;
@@ -81444,7 +81464,7 @@ function isOrchestratorAgent(agentName) {
81444
81464
  function injectKnowledgeMessage(output, text) {
81445
81465
  if (!output.messages)
81446
81466
  return;
81447
- const alreadyInjected = output.messages.some((m) => m.parts?.some((p) => p.text?.includes("\uD83D\uDCDA Lessons:") || p.text?.includes("<drift_report>") || p.text?.includes("<curator_briefing>")));
81467
+ const alreadyInjected = output.messages.some((m) => m.parts?.some((p) => p.text?.includes(INJECTION_SENTINEL)));
81448
81468
  if (alreadyInjected)
81449
81469
  return;
81450
81470
  let insertIdx = output.messages.length - 1;
@@ -81456,14 +81476,11 @@ function injectKnowledgeMessage(output, text) {
81456
81476
  }
81457
81477
  const knowledgeMessage = {
81458
81478
  info: { role: "system" },
81459
- parts: [{ type: "text", text }]
81479
+ parts: [{ type: "text", text: `${INJECTION_SENTINEL}${text}` }]
81460
81480
  };
81461
81481
  output.messages.splice(insertIdx, 0, knowledgeMessage);
81462
81482
  }
81463
81483
  function createKnowledgeInjectorHook(directory, config3) {
81464
- let lastSeenCacheKey = null;
81465
- let cachedInjectionText = null;
81466
- let cachedShownIds = [];
81467
81484
  function buildContextCacheKey(phase, ctx) {
81468
81485
  const parts2 = [
81469
81486
  String(phase),
@@ -81475,6 +81492,9 @@ function createKnowledgeInjectorHook(directory, config3) {
81475
81492
  ].join("|");
81476
81493
  return createHash7("sha1").update(parts2).digest("hex").slice(0, 16);
81477
81494
  }
81495
+ let lastSeenCacheKey = null;
81496
+ let cachedInjectionText = null;
81497
+ let cachedShownIds = [];
81478
81498
  return safeHook(async (_input, output) => {
81479
81499
  if (!output.messages || output.messages.length === 0)
81480
81500
  return;
@@ -85332,6 +85352,99 @@ function buildPhaseCouncilFeedback(phaseNumber, phaseSummary, verdict, vetoedBy,
85332
85352
  return lines.join(`
85333
85353
  `);
85334
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
+ }
85335
85448
 
85336
85449
  // src/council/criteria-store.ts
85337
85450
  import { existsSync as existsSync50, mkdirSync as mkdirSync23, readFileSync as readFileSync42, writeFileSync as writeFileSync16 } from "node:fs";
@@ -87862,7 +87975,9 @@ var imports = createSwarmTool({
87862
87975
  });
87863
87976
  // src/tools/knowledge-ack.ts
87864
87977
  init_zod();
87978
+ import { randomUUID as randomUUID7 } from "node:crypto";
87865
87979
  init_state();
87980
+ init_logger();
87866
87981
  init_create_tool();
87867
87982
  var knowledge_ack = createSwarmTool({
87868
87983
  description: "Record an acknowledgment outcome (applied/ignored/violated) for a previously-injected knowledge directive. Updates retrieval-outcome counters and appends a record to .swarm/knowledge-application.jsonl.",
@@ -87890,7 +88005,11 @@ var knowledge_ack = createSwarmTool({
87890
88005
  result: a.result,
87891
88006
  reason: a.reason
87892
88007
  };
87893
- const sessionId = ctx?.sessionID ?? "unknown";
88008
+ let sessionId = ctx?.sessionID;
88009
+ if (!sessionId) {
88010
+ warn("[knowledge-ack] No sessionID in tool context — dedup disabled for this acknowledgment");
88011
+ sessionId = randomUUID7();
88012
+ }
87894
88013
  const dedupKey = buildAckDedupKey(sessionId, a.id, a.result);
87895
88014
  if (swarmState.knowledgeAckDedup.has(dedupKey)) {
87896
88015
  return JSON.stringify({
@@ -87919,7 +88038,7 @@ init_knowledge_store();
87919
88038
  init_knowledge_validator();
87920
88039
  init_manager();
87921
88040
  init_create_tool();
87922
- import { randomUUID as randomUUID7 } from "node:crypto";
88041
+ import { randomUUID as randomUUID8 } from "node:crypto";
87923
88042
  var VALID_CATEGORIES2 = [
87924
88043
  "process",
87925
88044
  "architecture",
@@ -87993,7 +88112,7 @@ var knowledge_add = createSwarmTool({
87993
88112
  project_name = plan?.title ?? "";
87994
88113
  } catch {}
87995
88114
  const entry = {
87996
- id: randomUUID7(),
88115
+ id: randomUUID8(),
87997
88116
  tier: "swarm",
87998
88117
  lesson,
87999
88118
  category,
@@ -89793,6 +89912,41 @@ Advisory notes: ${advisoryNotes.join("; ")}` : "";
89793
89912
  }, null, 2);
89794
89913
  }
89795
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
+ }
89796
89950
  if (entry.verdict === "rejected" || entry.verdict === "REJECTED") {
89797
89951
  return JSON.stringify({
89798
89952
  success: false,
@@ -89832,11 +89986,11 @@ Advisory notes: ${advisoryNotes.join("; ")}` : "";
89832
89986
  status: "blocked",
89833
89987
  reason: "FINAL_COUNCIL_REQUIRED",
89834
89988
  final_council_required: true,
89835
- 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.`,
89836
89990
  agentsDispatched,
89837
89991
  agentsMissing: [],
89838
89992
  warnings: [
89839
- `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.`
89840
89994
  ]
89841
89995
  }, null, 2);
89842
89996
  }
@@ -97313,7 +97467,7 @@ var set_qa_gates = createSwarmTool({
97313
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."),
97314
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."),
97315
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."),
97316
- 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."),
97317
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.')
97318
97472
  },
97319
97473
  execute: async (args2, directory) => {
@@ -99561,7 +99715,7 @@ ${fileList}
99561
99715
  async _withStateLock(fn) {
99562
99716
  const timeoutMs = 1e4;
99563
99717
  let timeoutId;
99564
- const withTimeout2 = new Promise((resolve47, reject) => {
99718
+ const withTimeout2 = new Promise((_resolve, reject) => {
99565
99719
  timeoutId = setTimeout(() => {
99566
99720
  reject(new Error(`_withStateLock timed out after ${timeoutMs}ms — state update will not block subsequent operations`));
99567
99721
  }, timeoutMs);
@@ -102271,59 +102425,94 @@ var write_drift_evidence = createSwarmTool({
102271
102425
  });
102272
102426
  // src/tools/write-final-council-evidence.ts
102273
102427
  init_zod();
102428
+ init_loader();
102429
+ import fs107 from "node:fs";
102430
+ import path135 from "node:path";
102274
102431
  init_utils2();
102275
102432
  init_manager();
102276
102433
  init_create_tool();
102277
- import fs107 from "node:fs";
102278
- import path135 from "node:path";
102279
- function normalizeVerdict2(verdict) {
102280
- switch (verdict) {
102281
- case "APPROVED":
102282
- return "approved";
102283
- case "NEEDS_REVISION":
102284
- return "rejected";
102285
- default:
102286
- throw new Error(`Invalid verdict: must be 'APPROVED' or 'NEEDS_REVISION', got '${verdict}'`);
102287
- }
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";
102288
102464
  }
102289
102465
  async function executeWriteFinalCouncilEvidence(args2, directory) {
102290
- const phase = args2.phase;
102291
- if (!Number.isInteger(phase) || phase < 1) {
102292
- return JSON.stringify({
102293
- success: false,
102294
- phase,
102295
- message: "Invalid phase: must be a positive integer"
102296
- }, null, 2);
102297
- }
102298
- const validVerdicts = ["APPROVED", "NEEDS_REVISION"];
102299
- if (!validVerdicts.includes(args2.verdict)) {
102466
+ const parsed = ArgsSchema6.safeParse(args2);
102467
+ if (!parsed.success) {
102300
102468
  return JSON.stringify({
102301
102469
  success: false,
102302
- phase,
102303
- 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
+ }))
102304
102475
  }, null, 2);
102305
102476
  }
102306
- const summary = args2.summary;
102307
- 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) {
102308
102484
  return JSON.stringify({
102309
102485
  success: false,
102310
- phase,
102311
- 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
102312
102491
  }, null, 2);
102313
102492
  }
102314
- const normalizedVerdict = normalizeVerdict2(args2.verdict);
102493
+ const synthesis = synthesizeFinalCouncilAdvisory(input.projectSummary.trim(), input.verdicts, input.roundNumber ?? 1, config3.council);
102315
102494
  const plan = await loadPlan(directory);
102316
102495
  const planId = plan ? derivePlanId(plan) : "unknown";
102496
+ const normalizedVerdict = normalizeFinalVerdict(synthesis.overallVerdict);
102317
102497
  const evidenceEntry = {
102318
102498
  type: "final-council",
102319
- phase,
102499
+ phase: input.phase,
102320
102500
  plan_id: planId,
102321
102501
  verdict: normalizedVerdict,
102322
- summary: summary.trim(),
102323
- timestamp: new Date().toISOString()
102324
- };
102325
- const evidenceContent = {
102326
- 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
102327
102516
  };
102328
102517
  const filename = "final-council.json";
102329
102518
  const relativePath = path135.join("evidence", filename);
@@ -102333,10 +102522,13 @@ async function executeWriteFinalCouncilEvidence(args2, directory) {
102333
102522
  } catch (error93) {
102334
102523
  return JSON.stringify({
102335
102524
  success: false,
102336
- phase,
102525
+ phase: input.phase,
102337
102526
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
102338
102527
  }, null, 2);
102339
102528
  }
102529
+ const evidenceContent = {
102530
+ entries: [evidenceEntry]
102531
+ };
102340
102532
  const evidenceDir = path135.dirname(validatedPath);
102341
102533
  try {
102342
102534
  await fs107.promises.mkdir(evidenceDir, { recursive: true });
@@ -102345,41 +102537,53 @@ async function executeWriteFinalCouncilEvidence(args2, directory) {
102345
102537
  await fs107.promises.rename(tempPath, validatedPath);
102346
102538
  return JSON.stringify({
102347
102539
  success: true,
102348
- phase,
102540
+ phase: input.phase,
102541
+ overallVerdict: synthesis.overallVerdict,
102349
102542
  verdict: normalizedVerdict,
102350
- 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"
102351
102557
  }, null, 2);
102352
102558
  } catch (error93) {
102353
102559
  return JSON.stringify({
102354
102560
  success: false,
102355
- phase,
102561
+ phase: input.phase,
102356
102562
  message: error93 instanceof Error ? error93.message : String(error93)
102357
102563
  }, null, 2);
102358
102564
  }
102359
102565
  }
102360
102566
  var write_final_council_evidence = createSwarmTool({
102361
- 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.",
102362
102568
  args: {
102363
- phase: exports_external.number().int().min(1).describe("The phase number for the final council verdict (e.g., 1, 2, 3)"),
102364
- verdict: exports_external.enum(["APPROVED", "NEEDS_REVISION"]).describe("Verdict of the final council: 'APPROVED' or 'NEEDS_REVISION'"),
102365
- 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.")
102366
102573
  },
102367
102574
  execute: async (args2, directory) => {
102368
- const rawPhase = args2.phase !== undefined ? Number(args2.phase) : 0;
102369
- try {
102370
- const writeFinalCouncilEvidenceArgs = {
102371
- phase: Number(args2.phase),
102372
- verdict: String(args2.verdict),
102373
- summary: String(args2.summary ?? "")
102374
- };
102375
- return await executeWriteFinalCouncilEvidence(writeFinalCouncilEvidenceArgs, directory);
102376
- } catch (error93) {
102575
+ const parsed = ArgsSchema6.safeParse(args2);
102576
+ if (!parsed.success) {
102377
102577
  return JSON.stringify({
102378
102578
  success: false,
102379
- phase: rawPhase,
102380
- 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
+ }))
102381
102584
  }, null, 2);
102382
102585
  }
102586
+ return await executeWriteFinalCouncilEvidence(parsed.data, directory);
102383
102587
  }
102384
102588
  });
102385
102589
  // src/tools/write-hallucination-evidence.ts
@@ -102388,7 +102592,7 @@ init_utils2();
102388
102592
  init_create_tool();
102389
102593
  import fs108 from "node:fs";
102390
102594
  import path136 from "node:path";
102391
- function normalizeVerdict3(verdict) {
102595
+ function normalizeVerdict2(verdict) {
102392
102596
  switch (verdict) {
102393
102597
  case "APPROVED":
102394
102598
  return "approved";
@@ -102423,7 +102627,7 @@ async function executeWriteHallucinationEvidence(args2, directory) {
102423
102627
  message: "Invalid summary: must be a non-empty string"
102424
102628
  }, null, 2);
102425
102629
  }
102426
- const normalizedVerdict = normalizeVerdict3(args2.verdict);
102630
+ const normalizedVerdict = normalizeVerdict2(args2.verdict);
102427
102631
  const evidenceEntry = {
102428
102632
  type: "hallucination-verification",
102429
102633
  verdict: normalizedVerdict,
@@ -102499,7 +102703,7 @@ init_utils2();
102499
102703
  init_create_tool();
102500
102704
  import fs109 from "node:fs";
102501
102705
  import path137 from "node:path";
102502
- function normalizeVerdict4(verdict) {
102706
+ function normalizeVerdict3(verdict) {
102503
102707
  switch (verdict) {
102504
102708
  case "PASS":
102505
102709
  return "pass";
@@ -102556,7 +102760,7 @@ async function executeWriteMutationEvidence(args2, directory) {
102556
102760
  message: "Invalid summary: must be a non-empty string"
102557
102761
  }, null, 2);
102558
102762
  }
102559
- const normalizedVerdict = normalizeVerdict4(args2.verdict);
102763
+ const normalizedVerdict = normalizeVerdict3(args2.verdict);
102560
102764
  const evidenceEntry = {
102561
102765
  type: "mutation-gate",
102562
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.0",
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",