opencode-swarm 6.65.0 → 6.66.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.
@@ -8,4 +8,21 @@ export interface AdversarialTestingConfig {
8
8
  enabled: boolean;
9
9
  scope: 'all' | 'security-only';
10
10
  }
11
- export declare function createArchitectAgent(model: string, customPrompt?: string, customAppendPrompt?: string, adversarialTesting?: AdversarialTestingConfig): AgentDefinition;
11
+ /**
12
+ * Subset of PluginConfig.council needed to gate the Work Complete Council
13
+ * workflow block in the architect prompt. Only `enabled` is consumed here —
14
+ * runtime behavior (maxRounds, timeout, veto priority) is enforced elsewhere
15
+ * via the council tools and config. Keeping this shape narrow avoids pulling
16
+ * the full PluginConfig type into the agent-prompt layer.
17
+ */
18
+ export interface CouncilWorkflowConfig {
19
+ enabled?: boolean;
20
+ }
21
+ /**
22
+ * Build the Work Complete Council four-phase workflow block. Returns the full
23
+ * block text when council.enabled === true, otherwise the empty string. The
24
+ * empty-string return path guarantees byte-for-byte non-regression when the
25
+ * council feature is off or the config key is absent.
26
+ */
27
+ export declare function buildCouncilWorkflow(council?: CouncilWorkflowConfig): string;
28
+ export declare function createArchitectAgent(model: string, customPrompt?: string, customAppendPrompt?: string, adversarialTesting?: AdversarialTestingConfig, council?: CouncilWorkflowConfig): AgentDefinition;
package/dist/cli/index.js CHANGED
@@ -14825,7 +14825,14 @@ async function savePlan(directory, plan, options) {
14825
14825
  const planId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
14826
14826
  const planHashForInit = computePlanHash(validated);
14827
14827
  if (!await ledgerExists(directory)) {
14828
- await initLedger(directory, planId, planHashForInit, validated);
14828
+ try {
14829
+ await initLedger(directory, planId, planHashForInit, validated);
14830
+ } catch (initErr) {
14831
+ const msg = initErr instanceof Error ? initErr.message : String(initErr);
14832
+ if (!/already initialized/i.test(msg)) {
14833
+ throw initErr;
14834
+ }
14835
+ }
14829
14836
  } else {
14830
14837
  const existingEvents = await readLedgerEvents(directory);
14831
14838
  if (existingEvents.length > 0 && existingEvents[0].plan_id !== planId) {
@@ -18470,6 +18477,8 @@ var TOOL_NAMES = [
18470
18477
  "evidence_check",
18471
18478
  "check_gate_status",
18472
18479
  "completion_verify",
18480
+ "convene_council",
18481
+ "declare_council_criteria",
18473
18482
  "sbom_generate",
18474
18483
  "checkpoint",
18475
18484
  "pkg_audit",
@@ -18526,6 +18535,8 @@ var AGENT_TOOL_MAP = {
18526
18535
  "checkpoint",
18527
18536
  "check_gate_status",
18528
18537
  "completion_verify",
18538
+ "convene_council",
18539
+ "declare_council_criteria",
18529
18540
  "complexity_hotspots",
18530
18541
  "detect_domains",
18531
18542
  "evidence_check",
@@ -19182,6 +19193,14 @@ var AuthorityConfigSchema = exports_external.object({
19182
19193
  enabled: exports_external.boolean().default(true),
19183
19194
  rules: exports_external.record(exports_external.string(), AgentAuthorityRuleSchema).default({})
19184
19195
  });
19196
+ var CouncilConfigSchema = exports_external.object({
19197
+ enabled: exports_external.boolean().default(false),
19198
+ maxRounds: exports_external.number().int().min(1).max(10).default(3),
19199
+ parallelTimeoutMs: exports_external.number().int().min(5000).max(120000).default(30000),
19200
+ vetoPriority: exports_external.boolean().default(true),
19201
+ requireAllMembers: exports_external.boolean().default(false).describe("When true, convene_council rejects if fewer than 5 member verdicts are provided."),
19202
+ escalateOnMaxRounds: exports_external.string().optional().describe("Optional webhook URL or handler name invoked when maxRounds is reached without APPROVE. Declared for forward compatibility; no behavior is implemented yet.")
19203
+ }).strict();
19185
19204
  var PluginConfigSchema = exports_external.object({
19186
19205
  agents: exports_external.record(exports_external.string(), AgentOverrideConfigSchema).optional(),
19187
19206
  swarms: exports_external.record(exports_external.string(), SwarmConfigSchema).optional(),
@@ -19229,6 +19248,7 @@ var PluginConfigSchema = exports_external.object({
19229
19248
  }).optional(),
19230
19249
  incremental_verify: IncrementalVerifyConfigSchema.optional(),
19231
19250
  compaction_service: CompactionConfigSchema.optional(),
19251
+ council: CouncilConfigSchema.optional(),
19232
19252
  turbo_mode: exports_external.boolean().default(false).optional(),
19233
19253
  full_auto: exports_external.object({
19234
19254
  enabled: exports_external.boolean().default(false),
@@ -511,6 +511,15 @@ export declare const AuthorityConfigSchema: z.ZodObject<{
511
511
  }, z.core.$strip>>>;
512
512
  }, z.core.$strip>;
513
513
  export type AuthorityConfig = z.infer<typeof AuthorityConfigSchema>;
514
+ export declare const CouncilConfigSchema: z.ZodObject<{
515
+ enabled: z.ZodDefault<z.ZodBoolean>;
516
+ maxRounds: z.ZodDefault<z.ZodNumber>;
517
+ parallelTimeoutMs: z.ZodDefault<z.ZodNumber>;
518
+ vetoPriority: z.ZodDefault<z.ZodBoolean>;
519
+ requireAllMembers: z.ZodDefault<z.ZodBoolean>;
520
+ escalateOnMaxRounds: z.ZodOptional<z.ZodString>;
521
+ }, z.core.$strict>;
522
+ export type CouncilConfig = z.infer<typeof CouncilConfigSchema>;
514
523
  export declare const PluginConfigSchema: z.ZodObject<{
515
524
  agents: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
516
525
  model: z.ZodOptional<z.ZodString>;
@@ -854,6 +863,14 @@ export declare const PluginConfigSchema: z.ZodObject<{
854
863
  emergencyThreshold: z.ZodDefault<z.ZodNumber>;
855
864
  preserveLastNTurns: z.ZodDefault<z.ZodNumber>;
856
865
  }, z.core.$strip>>;
866
+ council: z.ZodOptional<z.ZodObject<{
867
+ enabled: z.ZodDefault<z.ZodBoolean>;
868
+ maxRounds: z.ZodDefault<z.ZodNumber>;
869
+ parallelTimeoutMs: z.ZodDefault<z.ZodNumber>;
870
+ vetoPriority: z.ZodDefault<z.ZodBoolean>;
871
+ requireAllMembers: z.ZodDefault<z.ZodBoolean>;
872
+ escalateOnMaxRounds: z.ZodOptional<z.ZodString>;
873
+ }, z.core.$strict>>;
857
874
  turbo_mode: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
858
875
  full_auto: z.ZodDefault<z.ZodOptional<z.ZodObject<{
859
876
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Work Complete Council — advisory routing helper.
3
+ *
4
+ * Purpose:
5
+ * Routes a CouncilSynthesis (unifiedFeedbackMd + verdict metadata) into the
6
+ * architect's non-blocking advisory queue (`session.pendingAdvisoryMessages`).
7
+ * The guardrails `messagesTransform` hook drains that queue into an
8
+ * [ADVISORIES] block prepended to the architect's first SYSTEM message on
9
+ * the next turn.
10
+ *
11
+ * Runtime call site:
12
+ * `src/tools/convene-council.ts` invokes this helper after writing evidence,
13
+ * guarded by `ctx?.sessionID` and `getAgentSession`. Missing session, missing
14
+ * sessionID, or any thrown error silently skip — the advisory is never
15
+ * critical-path. The helper is also re-exported for direct use by any future
16
+ * caller that wants to push synthesis output into an advisory queue.
17
+ *
18
+ * Scope and known limitation:
19
+ * The advisory queue is READ by the architect session on its next turn (self
20
+ * echo). It is NOT a programmatic architect→coder delivery channel — the
21
+ * architect still has to render `unifiedFeedbackMd` into the coder's
22
+ * delegation payload manually, per the prompt's four-phase workflow. A
23
+ * dedicated architect→coder advisory primitive is future work.
24
+ *
25
+ * Dedup semantics:
26
+ * Dedup key is `council:${taskId}:${roundNumber}`. If the queue already
27
+ * contains a string whose content includes that key, the push is a no-op.
28
+ * Different rounds or tasks push distinct entries.
29
+ *
30
+ * Blocking signal (metadata only):
31
+ * - REJECT → header declares `blocking=true`. Council vetoed the candidate.
32
+ * - CONCERNS→ `blocking=false`. Architect should weigh fixes but is not vetoed.
33
+ * - APPROVE → `blocking=false`. Helper skips push entirely when there are no
34
+ * advisoryFindings (nothing useful to surface).
35
+ */
36
+ import type { AgentSessionState } from '../state';
37
+ import type { CouncilSynthesis } from './types';
38
+ /**
39
+ * Push a CouncilSynthesis into the given session's advisory queue so the
40
+ * architect will see it as an [ADVISORIES] block on the next messagesTransform.
41
+ *
42
+ * Idempotent per (taskId, roundNumber): repeated calls with the same key
43
+ * leave the queue unchanged. Safe to call on APPROVE — it is a no-op when
44
+ * there are no advisoryFindings.
45
+ */
46
+ export declare function pushCouncilAdvisory(session: Pick<AgentSessionState, 'pendingAdvisoryMessages'>, synthesis: CouncilSynthesis): void;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Work Complete Council — evidence writer.
3
+ *
4
+ * Stamps the council synthesis result into `.swarm/evidence/{taskId}.json`
5
+ * under `gates.council`, matching the shape other gate writers use and the
6
+ * shape that `check_gate_status` and `update_task_status` consume (they read
7
+ * `evidence.gates[gateName]`). Council-specific fields (verdict, vetoedBy,
8
+ * roundNumber, allCriteriaMet) are stored alongside the standard GateInfo
9
+ * fields (sessionId, timestamp, agent); existing consumers only check
10
+ * `gates.council != null`, so the extras are compatible.
11
+ *
12
+ * Existing fields in the evidence file — top-level keys AND other `gates[*]`
13
+ * entries — are preserved across the write. The raw taskId is used as the
14
+ * filename; defense-in-depth regex validation rejects malformed IDs before
15
+ * any filesystem op.
16
+ */
17
+ import type { CouncilSynthesis } from './types';
18
+ export declare function writeCouncilEvidence(workingDir: string, synthesis: CouncilSynthesis): void;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Work Complete Council — pure synthesis service.
3
+ *
4
+ * Given the verdicts of council members (critic, reviewer, sme, test_engineer),
5
+ * compute the overall verdict, classify findings, detect conflicts, and build a
6
+ * single unified feedback document for the coder.
7
+ *
8
+ * No I/O — fully unit-testable with mock inputs. All file reads/writes happen in
9
+ * sibling modules (criteria-store, council-evidence-writer).
10
+ */
11
+ import type { CouncilConfig, CouncilCriteria, CouncilMemberVerdict, CouncilSynthesis } from './types';
12
+ export declare function synthesizeCouncilVerdicts(taskId: string, swarmId: string, verdicts: CouncilMemberVerdict[], criteria: CouncilCriteria | null, roundNumber: number, config?: Partial<CouncilConfig>): CouncilSynthesis;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Work Complete Council — pre-declaration criteria writer/reader.
3
+ *
4
+ * Stores acceptance criteria under .swarm/council/{safeId}.json so they can be
5
+ * read back during council evaluation.
6
+ */
7
+ import type { CouncilCriteria, CouncilCriteriaItem } from './types';
8
+ export declare function writeCriteria(workingDir: string, taskId: string, criteria: CouncilCriteriaItem[]): void;
9
+ export declare function readCriteria(workingDir: string, taskId: string): CouncilCriteria | null;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Work Complete Council — data contracts.
3
+ *
4
+ * Flat, stable schema — no nested generics. Designed for reliable LLM output.
5
+ * No business logic, no I/O. Only types, interfaces, and defaults.
6
+ */
7
+ export type CouncilVerdict = 'APPROVE' | 'CONCERNS' | 'REJECT';
8
+ export type CouncilFindingSeverity = 'HIGH' | 'MEDIUM' | 'LOW';
9
+ export type CouncilFindingCategory = 'logic' | 'edge_case' | 'error_handling' | 'spec_compliance' | 'security' | 'maintainability' | 'naming' | 'domain' | 'test_gap' | 'test_quality' | 'mutation_gap' | 'adversarial_gap' | 'slop_pattern' | 'hallucinated_api' | 'lazy_abstraction' | 'cargo_cult' | 'spec_drift' | 'other';
10
+ export type CouncilAgent = 'critic' | 'reviewer' | 'sme' | 'test_engineer' | 'explorer';
11
+ export interface CouncilFinding {
12
+ severity: CouncilFindingSeverity;
13
+ category: CouncilFindingCategory;
14
+ /** e.g. "src/tools/convene-council.ts:42" */
15
+ location: string;
16
+ /** Human-readable explanation */
17
+ detail: string;
18
+ /** Concrete quote or line reference */
19
+ evidence: string;
20
+ }
21
+ export interface CouncilMemberVerdict {
22
+ agent: CouncilAgent;
23
+ verdict: CouncilVerdict;
24
+ /** Confidence 0.0–1.0 */
25
+ confidence: number;
26
+ findings: CouncilFinding[];
27
+ /** Criteria IDs from pre-declaration (e.g. ["C1","C3"]) */
28
+ criteriaAssessed: string[];
29
+ /** Criteria IDs that failed */
30
+ criteriaUnmet: string[];
31
+ durationMs: number;
32
+ }
33
+ export interface CouncilSynthesis {
34
+ taskId: string;
35
+ swarmId: string;
36
+ /** ISO 8601 */
37
+ timestamp: string;
38
+ overallVerdict: CouncilVerdict;
39
+ vetoedBy: CouncilAgent[] | null;
40
+ memberVerdicts: CouncilMemberVerdict[];
41
+ unresolvedConflicts: string[];
42
+ /** Severity HIGH + MEDIUM from veto members */
43
+ requiredFixes: CouncilFinding[];
44
+ /** Severity LOW or from non-veto members */
45
+ advisoryFindings: CouncilFinding[];
46
+ /** Single markdown document sent to coder */
47
+ unifiedFeedbackMd: string;
48
+ /** 1-indexed */
49
+ roundNumber: number;
50
+ allCriteriaMet: boolean;
51
+ }
52
+ export interface CouncilCriteriaItem {
53
+ id: string;
54
+ description: string;
55
+ mandatory: boolean;
56
+ }
57
+ export interface CouncilCriteria {
58
+ taskId: string;
59
+ criteria: CouncilCriteriaItem[];
60
+ /** ISO 8601 */
61
+ declaredAt: string;
62
+ }
63
+ /** Config shape — matched in schema.ts via CouncilConfigSchema. */
64
+ export interface CouncilConfig {
65
+ enabled: boolean;
66
+ /** Default 3 */
67
+ maxRounds: number;
68
+ /** Default 30_000 */
69
+ parallelTimeoutMs: number;
70
+ /** Default true — any REJECT blocks */
71
+ vetoPriority: boolean;
72
+ /** Default false — when true, convene_council rejects unless all 5 member verdicts are provided */
73
+ requireAllMembers: boolean;
74
+ /** Optional webhook URL or handler name invoked when maxRounds is reached without APPROVE. Declared for forward compatibility; no behavior is implemented yet. */
75
+ escalateOnMaxRounds?: string;
76
+ }
77
+ export declare const COUNCIL_DEFAULTS: CouncilConfig;
package/dist/index.js CHANGED
@@ -49,6 +49,8 @@ var init_tool_names = __esm(() => {
49
49
  "evidence_check",
50
50
  "check_gate_status",
51
51
  "completion_verify",
52
+ "convene_council",
53
+ "declare_council_criteria",
52
54
  "sbom_generate",
53
55
  "checkpoint",
54
56
  "pkg_audit",
@@ -173,6 +175,8 @@ var init_constants = __esm(() => {
173
175
  "checkpoint",
174
176
  "check_gate_status",
175
177
  "completion_verify",
178
+ "convene_council",
179
+ "declare_council_criteria",
176
180
  "complexity_hotspots",
177
181
  "detect_domains",
178
182
  "evidence_check",
@@ -394,6 +398,8 @@ var init_constants = __esm(() => {
394
398
  co_change_analyzer: "detect hidden couplings by analyzing git history",
395
399
  check_gate_status: "check the gate status of a specific task",
396
400
  completion_verify: "verify completed tasks have required evidence",
401
+ convene_council: "convene the Work Complete Council \u2014 parallel veto-aware verification gate across critic, reviewer, sme, test_engineer, and explorer verdicts",
402
+ declare_council_criteria: "pre-declare acceptance criteria for a task before the coder starts work; criteria are read back during council evaluation",
397
403
  detect_domains: "detect which SME domains are relevant for a given text",
398
404
  extract_code_blocks: "extract code blocks from text content and save them to files",
399
405
  gitingest: "fetch a GitHub repository full content via gitingest.com",
@@ -14628,7 +14634,7 @@ function resolveGuardrailsConfig(config2, agentName) {
14628
14634
  };
14629
14635
  return resolved;
14630
14636
  }
14631
- var KNOWN_SWARM_PREFIXES, SEPARATORS, AgentOverrideConfigSchema, SwarmConfigSchema, HooksConfigSchema, ScoringWeightsSchema, DecisionDecaySchema, TokenRatiosSchema, ScoringConfigSchema, ContextBudgetConfigSchema, EvidenceConfigSchema, GateFeatureSchema, PlaceholderScanConfigSchema, QualityBudgetConfigSchema, GateConfigSchema, PipelineConfigSchema, PhaseCompleteConfigSchema, SummaryConfigSchema, ReviewPassesConfigSchema, AdversarialDetectionConfigSchema, AdversarialTestingConfigSchemaBase, AdversarialTestingConfigSchema, IntegrationAnalysisConfigSchema, DocsConfigSchema, UIReviewConfigSchema, CompactionAdvisoryConfigSchema, LintConfigSchema, SecretscanConfigSchema, GuardrailsProfileSchema, DEFAULT_AGENT_PROFILES, DEFAULT_ARCHITECT_PROFILE, GuardrailsConfigSchema, WatchdogConfigSchema, SelfReviewConfigSchema, ToolFilterConfigSchema, PlanCursorConfigSchema, CheckpointConfigSchema, AutomationModeSchema, AutomationCapabilitiesSchema, AutomationConfigSchemaBase, AutomationConfigSchema, KnowledgeConfigSchema, CuratorConfigSchema, SlopDetectorConfigSchema, IncrementalVerifyConfigSchema, CompactionConfigSchema, AgentAuthorityRuleSchema, AuthorityConfigSchema, PluginConfigSchema;
14637
+ var KNOWN_SWARM_PREFIXES, SEPARATORS, AgentOverrideConfigSchema, SwarmConfigSchema, HooksConfigSchema, ScoringWeightsSchema, DecisionDecaySchema, TokenRatiosSchema, ScoringConfigSchema, ContextBudgetConfigSchema, EvidenceConfigSchema, GateFeatureSchema, PlaceholderScanConfigSchema, QualityBudgetConfigSchema, GateConfigSchema, PipelineConfigSchema, PhaseCompleteConfigSchema, SummaryConfigSchema, ReviewPassesConfigSchema, AdversarialDetectionConfigSchema, AdversarialTestingConfigSchemaBase, AdversarialTestingConfigSchema, IntegrationAnalysisConfigSchema, DocsConfigSchema, UIReviewConfigSchema, CompactionAdvisoryConfigSchema, LintConfigSchema, SecretscanConfigSchema, GuardrailsProfileSchema, DEFAULT_AGENT_PROFILES, DEFAULT_ARCHITECT_PROFILE, GuardrailsConfigSchema, WatchdogConfigSchema, SelfReviewConfigSchema, ToolFilterConfigSchema, PlanCursorConfigSchema, CheckpointConfigSchema, AutomationModeSchema, AutomationCapabilitiesSchema, AutomationConfigSchemaBase, AutomationConfigSchema, KnowledgeConfigSchema, CuratorConfigSchema, SlopDetectorConfigSchema, IncrementalVerifyConfigSchema, CompactionConfigSchema, AgentAuthorityRuleSchema, AuthorityConfigSchema, CouncilConfigSchema, PluginConfigSchema;
14632
14638
  var init_schema = __esm(() => {
14633
14639
  init_zod();
14634
14640
  init_constants();
@@ -15120,6 +15126,14 @@ var init_schema = __esm(() => {
15120
15126
  enabled: exports_external.boolean().default(true),
15121
15127
  rules: exports_external.record(exports_external.string(), AgentAuthorityRuleSchema).default({})
15122
15128
  });
15129
+ CouncilConfigSchema = exports_external.object({
15130
+ enabled: exports_external.boolean().default(false),
15131
+ maxRounds: exports_external.number().int().min(1).max(10).default(3),
15132
+ parallelTimeoutMs: exports_external.number().int().min(5000).max(120000).default(30000),
15133
+ vetoPriority: exports_external.boolean().default(true),
15134
+ requireAllMembers: exports_external.boolean().default(false).describe("When true, convene_council rejects if fewer than 5 member verdicts are provided."),
15135
+ escalateOnMaxRounds: exports_external.string().optional().describe("Optional webhook URL or handler name invoked when maxRounds is reached without APPROVE. Declared for forward compatibility; no behavior is implemented yet.")
15136
+ }).strict();
15123
15137
  PluginConfigSchema = exports_external.object({
15124
15138
  agents: exports_external.record(exports_external.string(), AgentOverrideConfigSchema).optional(),
15125
15139
  swarms: exports_external.record(exports_external.string(), SwarmConfigSchema).optional(),
@@ -15167,6 +15181,7 @@ var init_schema = __esm(() => {
15167
15181
  }).optional(),
15168
15182
  incremental_verify: IncrementalVerifyConfigSchema.optional(),
15169
15183
  compaction_service: CompactionConfigSchema.optional(),
15184
+ council: CouncilConfigSchema.optional(),
15170
15185
  turbo_mode: exports_external.boolean().default(false).optional(),
15171
15186
  full_auto: exports_external.object({
15172
15187
  enabled: exports_external.boolean().default(false),
@@ -16418,7 +16433,14 @@ async function savePlan(directory, plan, options) {
16418
16433
  const planId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16419
16434
  const planHashForInit = computePlanHash(validated);
16420
16435
  if (!await ledgerExists(directory)) {
16421
- await initLedger(directory, planId, planHashForInit, validated);
16436
+ try {
16437
+ await initLedger(directory, planId, planHashForInit, validated);
16438
+ } catch (initErr) {
16439
+ const msg = initErr instanceof Error ? initErr.message : String(initErr);
16440
+ if (!/already initialized/i.test(msg)) {
16441
+ throw initErr;
16442
+ }
16443
+ }
16422
16444
  } else {
16423
16445
  const existingEvents = await readLedgerEvents(directory);
16424
16446
  if (existingEvents.length > 0 && existingEvents[0].plan_id !== planId) {
@@ -45342,8 +45364,8 @@ async function loadGrammar(languageId) {
45342
45364
  const parser = new Parser;
45343
45365
  const wasmFileName = getWasmFileName(normalizedId);
45344
45366
  const wasmPath = path66.join(getGrammarsDirAbsolute(), wasmFileName);
45345
- const { existsSync: existsSync37 } = await import("fs");
45346
- if (!existsSync37(wasmPath)) {
45367
+ const { existsSync: existsSync39 } = await import("fs");
45368
+ if (!existsSync39(wasmPath)) {
45347
45369
  throw new Error(`Grammar file not found for ${languageId}: ${wasmPath}
45348
45370
  Make sure to run 'bun run build' to copy grammar files to dist/lang/grammars/`);
45349
45371
  }
@@ -53684,6 +53706,8 @@ NOT valid completion signals:
53684
53706
 
53685
53707
  The ONLY valid completion signal is: all required gate agents returned positive verdicts.
53686
53708
 
53709
+ {{COUNCIL_WORKFLOW}}
53710
+
53687
53711
  Emit 'architect_loop_detected' when triggering sounding board for 3rd time on same impasse.
53688
53712
 
53689
53713
  6g. **META.SUMMARY CONVENTION** \u2014 When emitting state updates to .swarm/ files or events.jsonl, include:
@@ -54496,6 +54520,68 @@ Swarm: {{SWARM_ID}}
54496
54520
  \`\`\`
54497
54521
 
54498
54522
  `;
54523
+ function buildCouncilWorkflow(council) {
54524
+ if (council?.enabled !== true)
54525
+ return "";
54526
+ return `## Work Complete Council (when enabled)
54527
+
54528
+ When \`council.enabled\` is true, every task goes through a four-phase verification
54529
+ gate before advancing to \`complete\`. This supplements \u2014 does NOT replace \u2014 the
54530
+ existing precheckbatch / reviewer / test_engineer gate sequence.
54531
+
54532
+ ### Phase 0 \u2014 Pre-declare criteria (at plan time, BEFORE dispatching the coder)
54533
+ Call \`declare_council_criteria\` for each task with at least 3 concrete,
54534
+ testable acceptance criteria. Mark functional/correctness criteria
54535
+ \`mandatory: true\`; style/naming criteria \`mandatory: false\`. Criterion ids
54536
+ follow the pattern \`C1\`, \`C2\`, etc. The criteria are persisted to
54537
+ \`.swarm/council/{taskId}.json\` and read back automatically at synthesis time.
54538
+
54539
+ ### Phase 1 \u2014 Parallel dispatch (when the coder signals the task is complete)
54540
+ Dispatch all FIVE council members IN PARALLEL \u2014 do not run them sequentially.
54541
+ Each receives ONLY their role-relevant context, not the full conversation:
54542
+ - \`critic\` \u2014 original task spec + acceptance criteria + code diff + test results
54543
+ - \`reviewer\` \u2014 semantic diff summary + blast radius (files importing changed files) + style guide
54544
+ - \`sme\` \u2014 task domain context + relevant knowledge base entries
54545
+ - \`test_engineer\` \u2014 changed test files + coverage delta + known mutation gaps
54546
+ - \`explorer\` \u2014 full diff + original task intent + any prior slop findings
54547
+ (explorer hunts for lazy implementations, hallucinated APIs,
54548
+ cargo-cult patterns, spec drift, lazy abstractions)
54549
+
54550
+ Each member must return a \`CouncilMemberVerdict\` with all fields populated:
54551
+ \`agent\`, \`verdict\` (APPROVE|CONCERNS|REJECT), \`confidence\` (0.0\u20131.0),
54552
+ \`findings[]\`, \`criteriaAssessed[]\`, \`criteriaUnmet[]\`, \`durationMs\`.
54553
+
54554
+ ### Phase 2 \u2014 Synthesize
54555
+ Call \`convene_council\` with all 5 verdicts, the task id, swarm id, and the
54556
+ current round number (1-indexed). The tool returns:
54557
+ \`overallVerdict\`, \`vetoedBy\`, \`unifiedFeedbackMd\`, \`requiredFixesCount\`,
54558
+ \`allCriteriaMet\`.
54559
+
54560
+ ### Phase 3 \u2014 Act on the result
54561
+ - **APPROVE**: Advance task to complete via \`update_task_status\`. If
54562
+ advisoryFindingsCount > 0, deliver \`unifiedFeedbackMd\` as a
54563
+ single non-blocking note. Otherwise, advance silently.
54564
+ - **CONCERNS**: Send \`unifiedFeedbackMd\` to the coder as ONE coherent document.
54565
+ Do NOT enumerate individual member verdicts. Increment
54566
+ roundNumber on the next council call. CONCERNS does not block
54567
+ advancement at the update_task_status level \u2014 decide per
54568
+ severity whether to advance or retry.
54569
+ - **REJECT**: Block advancement. Send \`unifiedFeedbackMd\` to the coder with
54570
+ the BLOCKING flag. The coder must resolve all \`requiredFixes\`
54571
+ before re-submitting. Maximum \`council.maxRounds\` rounds
54572
+ (default 3). If roundNumber >= maxRounds and verdict is still
54573
+ REJECT, surface \`unifiedFeedbackMd\` to the user and HALT \u2014
54574
+ do NOT auto-advance.
54575
+
54576
+ ### Retry protocol
54577
+ On re-submission after REJECT or CONCERNS, the council reads the same
54578
+ pre-declared criteria and receives (a) the previous synthesis findings plus
54579
+ (b) the diff of what changed since the last round. Council members verify
54580
+ prior findings are resolved without re-reviewing unchanged code. The
54581
+ architect resolves any \`unresolvedConflicts\` in \`unifiedFeedbackMd\` BEFORE
54582
+ sending it to the coder \u2014 the coder never sees contradictory instructions
54583
+ from different members.`;
54584
+ }
54499
54585
  function buildYourToolsList() {
54500
54586
  const tools = AGENT_TOOL_MAP.architect ?? [];
54501
54587
  const sorted = [...tools].sort();
@@ -54636,7 +54722,7 @@ function buildSlashCommandsList() {
54636
54722
  return lines.join(`
54637
54723
  `);
54638
54724
  }
54639
- function createArchitectAgent(model, customPrompt, customAppendPrompt, adversarialTesting) {
54725
+ function createArchitectAgent(model, customPrompt, customAppendPrompt, adversarialTesting, council) {
54640
54726
  let prompt = ARCHITECT_PROMPT;
54641
54727
  if (customPrompt) {
54642
54728
  prompt = customPrompt;
@@ -54646,6 +54732,18 @@ function createArchitectAgent(model, customPrompt, customAppendPrompt, adversari
54646
54732
  ${customAppendPrompt}`;
54647
54733
  }
54648
54734
  prompt = prompt?.replace("{{YOUR_TOOLS}}", buildYourToolsList())?.replace("{{AVAILABLE_TOOLS}}", buildAvailableToolsList())?.replace("{{SLASH_COMMANDS}}", buildSlashCommandsList());
54735
+ const councilBlock = buildCouncilWorkflow(council);
54736
+ if (councilBlock === "") {
54737
+ prompt = prompt?.replace(`
54738
+
54739
+ {{COUNCIL_WORKFLOW}}
54740
+
54741
+ `, `
54742
+
54743
+ `);
54744
+ } else {
54745
+ prompt = prompt?.replace("{{COUNCIL_WORKFLOW}}", councilBlock);
54746
+ }
54649
54747
  const advEnabled = adversarialTesting?.enabled ?? true;
54650
54748
  const advScope = adversarialTesting?.scope ?? "all";
54651
54749
  if (!advEnabled) {
@@ -56241,7 +56339,7 @@ function createSwarmAgents(swarmId, swarmConfig, isDefault, pluginConfig) {
56241
56339
  const prefixName = (name2) => `${prefix}${name2}`;
56242
56340
  if (!isAgentDisabled("architect", swarmAgents, swarmPrefix)) {
56243
56341
  const architectPrompts = getPrompts("architect");
56244
- const architect = createArchitectAgent(getModel("architect"), architectPrompts.prompt, architectPrompts.appendPrompt, pluginConfig?.adversarial_testing);
56342
+ const architect = createArchitectAgent(getModel("architect"), architectPrompts.prompt, architectPrompts.appendPrompt, pluginConfig?.adversarial_testing, pluginConfig?.council);
56245
56343
  architect.name = prefixName("architect");
56246
56344
  const swarmName = swarmConfig.name || swarmId;
56247
56345
  const swarmIdentity = isDefault ? "default" : swarmId;
@@ -68250,6 +68348,345 @@ var complexity_hotspots = createSwarmTool({
68250
68348
  }
68251
68349
  }
68252
68350
  });
68351
+ // src/tools/convene-council.ts
68352
+ init_dist();
68353
+ init_zod();
68354
+ init_loader();
68355
+
68356
+ // src/council/council-advisory.ts
68357
+ function pushCouncilAdvisory(session, synthesis) {
68358
+ const dedupKey = `council:${synthesis.taskId}:${synthesis.roundNumber}`;
68359
+ if (synthesis.overallVerdict === "APPROVE" && synthesis.advisoryFindings.length === 0) {
68360
+ return;
68361
+ }
68362
+ session.pendingAdvisoryMessages ??= [];
68363
+ if (session.pendingAdvisoryMessages.some((m) => m.includes(dedupKey))) {
68364
+ return;
68365
+ }
68366
+ const blocking = synthesis.overallVerdict === "REJECT";
68367
+ const header = `[${dedupKey}] (priority=HIGH, blocking=${blocking})`;
68368
+ const body2 = synthesis.unifiedFeedbackMd;
68369
+ session.pendingAdvisoryMessages.push(`${header}
68370
+ ${body2}`);
68371
+ }
68372
+
68373
+ // src/council/council-evidence-writer.ts
68374
+ import { existsSync as existsSync36, mkdirSync as mkdirSync16, readFileSync as readFileSync35, writeFileSync as writeFileSync11 } from "fs";
68375
+ import { join as join59 } from "path";
68376
+ var EVIDENCE_DIR2 = ".swarm/evidence";
68377
+ var VALID_TASK_ID = /^\d+\.\d+(\.\d+)*$/;
68378
+ var COUNCIL_GATE_NAME = "council";
68379
+ var COUNCIL_AGENT_ID = "architect";
68380
+ var FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
68381
+ function safeAssignOwnProps(target, source) {
68382
+ for (const key of Object.keys(source)) {
68383
+ if (FORBIDDEN_KEYS.has(key))
68384
+ continue;
68385
+ target[key] = source[key];
68386
+ }
68387
+ return target;
68388
+ }
68389
+ function writeCouncilEvidence(workingDir, synthesis) {
68390
+ if (!VALID_TASK_ID.test(synthesis.taskId)) {
68391
+ throw new Error(`writeCouncilEvidence: invalid taskId "${synthesis.taskId}" \u2014 must match N.M or N.M.P format`);
68392
+ }
68393
+ const dir = join59(workingDir, EVIDENCE_DIR2);
68394
+ mkdirSync16(dir, { recursive: true });
68395
+ const filePath = join59(dir, `${synthesis.taskId}.json`);
68396
+ const existingRoot = Object.create(null);
68397
+ if (existsSync36(filePath)) {
68398
+ try {
68399
+ const parsed = JSON.parse(readFileSync35(filePath, "utf-8"));
68400
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
68401
+ safeAssignOwnProps(existingRoot, parsed);
68402
+ }
68403
+ } catch {}
68404
+ }
68405
+ const existingGatesRaw = existingRoot.gates;
68406
+ const mergedGates = Object.create(null);
68407
+ if (existingGatesRaw && typeof existingGatesRaw === "object" && !Array.isArray(existingGatesRaw)) {
68408
+ safeAssignOwnProps(mergedGates, existingGatesRaw);
68409
+ }
68410
+ mergedGates[COUNCIL_GATE_NAME] = {
68411
+ sessionId: synthesis.swarmId,
68412
+ timestamp: synthesis.timestamp,
68413
+ agent: COUNCIL_AGENT_ID,
68414
+ verdict: synthesis.overallVerdict,
68415
+ vetoedBy: synthesis.vetoedBy,
68416
+ roundNumber: synthesis.roundNumber,
68417
+ allCriteriaMet: synthesis.allCriteriaMet
68418
+ };
68419
+ const updated = Object.create(null);
68420
+ safeAssignOwnProps(updated, existingRoot);
68421
+ updated.gates = mergedGates;
68422
+ writeFileSync11(filePath, JSON.stringify(updated, null, 2));
68423
+ }
68424
+
68425
+ // src/council/types.ts
68426
+ var COUNCIL_DEFAULTS = {
68427
+ enabled: false,
68428
+ maxRounds: 3,
68429
+ parallelTimeoutMs: 30000,
68430
+ vetoPriority: true,
68431
+ requireAllMembers: false
68432
+ };
68433
+
68434
+ // src/council/council-service.ts
68435
+ function synthesizeCouncilVerdicts(taskId, swarmId, verdicts, criteria, roundNumber, config3 = {}) {
68436
+ const cfg = { ...COUNCIL_DEFAULTS, ...config3 };
68437
+ const timestamp = new Date().toISOString();
68438
+ const rejectingMembers = verdicts.filter((v) => v.verdict === "REJECT").map((v) => v.agent);
68439
+ let overallVerdict;
68440
+ if (cfg.vetoPriority && rejectingMembers.length > 0) {
68441
+ overallVerdict = "REJECT";
68442
+ } else if (verdicts.some((v) => v.verdict === "CONCERNS") || !cfg.vetoPriority && rejectingMembers.length > 0) {
68443
+ overallVerdict = "CONCERNS";
68444
+ } else {
68445
+ overallVerdict = "APPROVE";
68446
+ }
68447
+ const unresolvedConflicts = detectConflicts(verdicts);
68448
+ const rejectingSet = new Set(rejectingMembers);
68449
+ const vetoFindings = verdicts.filter((v) => rejectingSet.has(v.agent)).flatMap((v) => v.findings);
68450
+ const requiredFixes = vetoFindings.filter((f) => f.severity === "HIGH" || f.severity === "MEDIUM");
68451
+ const advisoryFindings = [
68452
+ ...vetoFindings.filter((f) => f.severity === "LOW"),
68453
+ ...verdicts.filter((v) => !rejectingSet.has(v.agent)).flatMap((v) => v.findings)
68454
+ ];
68455
+ const allAssessedIds = new Set(verdicts.flatMap((v) => v.criteriaAssessed));
68456
+ const allUnmetIds = new Set(verdicts.flatMap((v) => v.criteriaUnmet));
68457
+ const mandatoryIds = new Set((criteria?.criteria ?? []).filter((c) => c.mandatory).map((c) => c.id));
68458
+ const allCriteriaMet = [...mandatoryIds].every((id) => allAssessedIds.has(id) && !allUnmetIds.has(id));
68459
+ const unifiedFeedbackMd = buildUnifiedFeedback(taskId, overallVerdict, rejectingMembers, requiredFixes, advisoryFindings, unresolvedConflicts, roundNumber, cfg.maxRounds);
68460
+ return {
68461
+ taskId,
68462
+ swarmId,
68463
+ timestamp,
68464
+ overallVerdict,
68465
+ vetoedBy: rejectingMembers.length > 0 ? rejectingMembers : null,
68466
+ memberVerdicts: verdicts,
68467
+ unresolvedConflicts,
68468
+ requiredFixes,
68469
+ advisoryFindings,
68470
+ unifiedFeedbackMd,
68471
+ roundNumber,
68472
+ allCriteriaMet
68473
+ };
68474
+ }
68475
+ function detectConflicts(verdicts) {
68476
+ const conflicts = [];
68477
+ const locationMap = new Map;
68478
+ for (const verdict of verdicts) {
68479
+ for (const finding of verdict.findings) {
68480
+ const key = finding.location.toLowerCase();
68481
+ if (!key)
68482
+ continue;
68483
+ const entries = locationMap.get(key);
68484
+ if (entries) {
68485
+ entries.push({ agent: verdict.agent, detail: finding.detail });
68486
+ } else {
68487
+ locationMap.set(key, [
68488
+ { agent: verdict.agent, detail: finding.detail }
68489
+ ]);
68490
+ }
68491
+ }
68492
+ }
68493
+ for (const [location, entries] of locationMap) {
68494
+ if (entries.length < 2)
68495
+ continue;
68496
+ const addDirectives = entries.filter((e) => /\badd\b|\binclude\b|\binsert\b/i.test(e.detail));
68497
+ const removeDirectives = entries.filter((e) => /\bremove\b|\bdelete\b|\beliminate\b/i.test(e.detail));
68498
+ if (addDirectives.length > 0 && removeDirectives.length > 0) {
68499
+ conflicts.push(`Conflict at ${location}: ${addDirectives.map((e) => `${e.agent} says "${e.detail}"`).join(", ")} vs ${removeDirectives.map((e) => `${e.agent} says "${e.detail}"`).join(", ")}`);
68500
+ }
68501
+ }
68502
+ return conflicts;
68503
+ }
68504
+ function buildUnifiedFeedback(taskId, verdict, vetoedBy, requiredFixes, advisoryFindings, conflicts, roundNumber, maxRounds) {
68505
+ const lines = [
68506
+ `## Work Complete Council \u2014 Round ${roundNumber}/${maxRounds}`,
68507
+ `**Task:** ${taskId} **Overall verdict:** ${verdict}`,
68508
+ ""
68509
+ ];
68510
+ if (vetoedBy.length > 0) {
68511
+ lines.push(`> \u26D4 **BLOCKED** by: ${vetoedBy.join(", ")}`);
68512
+ lines.push("");
68513
+ }
68514
+ if (requiredFixes.length > 0) {
68515
+ lines.push("### Required Fixes (must resolve before re-submission)");
68516
+ for (const f of requiredFixes) {
68517
+ lines.push(`- **[${f.severity}]** \`${f.location}\` \u2014 ${f.detail}`, ` _Evidence:_ ${f.evidence}`);
68518
+ }
68519
+ lines.push("");
68520
+ }
68521
+ if (conflicts.length > 0) {
68522
+ lines.push("### Conflicts to Resolve");
68523
+ lines.push("_The following reviewers gave contradictory instructions. Architect must resolve before sending to coder._");
68524
+ for (const c of conflicts) {
68525
+ lines.push(`- ${c}`);
68526
+ }
68527
+ lines.push("");
68528
+ }
68529
+ if (advisoryFindings.length > 0) {
68530
+ lines.push("### Advisory Findings (non-blocking)");
68531
+ for (const f of advisoryFindings) {
68532
+ lines.push(`- **[${f.severity}]** \`${f.location}\` \u2014 ${f.detail}`);
68533
+ }
68534
+ lines.push("");
68535
+ }
68536
+ if (verdict === "APPROVE") {
68537
+ lines.push("> \u2705 **All council members approved.** Work may advance to `complete`.");
68538
+ } else if (roundNumber >= maxRounds) {
68539
+ lines.push(`> \u26A0\uFE0F **Max rounds (${maxRounds}) reached.** Escalate to user \u2014 do not auto-advance.`);
68540
+ }
68541
+ return lines.join(`
68542
+ `);
68543
+ }
68544
+
68545
+ // src/council/criteria-store.ts
68546
+ import { existsSync as existsSync37, mkdirSync as mkdirSync17, readFileSync as readFileSync36, writeFileSync as writeFileSync12 } from "fs";
68547
+ import { join as join60 } from "path";
68548
+ var COUNCIL_DIR = ".swarm/council";
68549
+ function writeCriteria(workingDir, taskId, criteria) {
68550
+ const dir = join60(workingDir, COUNCIL_DIR);
68551
+ mkdirSync17(dir, { recursive: true });
68552
+ const payload = {
68553
+ taskId,
68554
+ criteria,
68555
+ declaredAt: new Date().toISOString()
68556
+ };
68557
+ writeFileSync12(join60(dir, `${safeId(taskId)}.json`), JSON.stringify(payload, null, 2));
68558
+ }
68559
+ function readCriteria(workingDir, taskId) {
68560
+ const filePath = join60(workingDir, COUNCIL_DIR, `${safeId(taskId)}.json`);
68561
+ if (!existsSync37(filePath))
68562
+ return null;
68563
+ try {
68564
+ const parsed = JSON.parse(readFileSync36(filePath, "utf-8"));
68565
+ if (parsed && typeof parsed === "object" && typeof parsed.taskId === "string" && Array.isArray(parsed.criteria)) {
68566
+ return parsed;
68567
+ }
68568
+ return null;
68569
+ } catch {
68570
+ return null;
68571
+ }
68572
+ }
68573
+ function safeId(id) {
68574
+ return id.replace(/[^a-zA-Z0-9_-]/g, "_");
68575
+ }
68576
+
68577
+ // src/tools/convene-council.ts
68578
+ init_state();
68579
+ init_create_tool();
68580
+ init_resolve_working_directory();
68581
+ var FindingSchema = exports_external.object({
68582
+ severity: exports_external.enum(["HIGH", "MEDIUM", "LOW"]),
68583
+ category: exports_external.string().min(1),
68584
+ location: exports_external.string(),
68585
+ detail: exports_external.string(),
68586
+ evidence: exports_external.string()
68587
+ });
68588
+ var VerdictSchema = exports_external.object({
68589
+ agent: exports_external.enum(["critic", "reviewer", "sme", "test_engineer", "explorer"]),
68590
+ verdict: exports_external.enum(["APPROVE", "CONCERNS", "REJECT"]),
68591
+ confidence: exports_external.number().min(0).max(1),
68592
+ findings: exports_external.array(FindingSchema),
68593
+ criteriaAssessed: exports_external.array(exports_external.string()),
68594
+ criteriaUnmet: exports_external.array(exports_external.string()),
68595
+ durationMs: exports_external.number().nonnegative()
68596
+ });
68597
+ var ArgsSchema = exports_external.object({
68598
+ taskId: exports_external.string().min(1).regex(/^\d+\.\d+(\.\d+)*$/, 'Task ID must be in N.M or N.M.P format (e.g. "1.1")'),
68599
+ swarmId: exports_external.string().min(1),
68600
+ roundNumber: exports_external.number().int().min(1).max(10).default(1),
68601
+ verdicts: exports_external.array(VerdictSchema).min(1).max(5),
68602
+ working_directory: exports_external.string().optional()
68603
+ });
68604
+ var convene_council = createSwarmTool({
68605
+ description: "Convene the Work Complete Council. Accepts parallel verdicts from critic, " + "reviewer, sme, test_engineer, and explorer (anti-slop specialist). Returns " + "a synthesized assessment with a veto-aware overall verdict, required fixes, " + "and a single unified feedback document. Architect-only. Config-gated via " + "council.enabled.",
68606
+ args: {
68607
+ taskId: tool.schema.string().min(1).regex(/^\d+\.\d+(\.\d+)*$/, "Task ID must be in N.M or N.M.P format").describe('Task ID being evaluated, e.g. "1.1", "1.2.3"'),
68608
+ swarmId: tool.schema.string().min(1).describe('Swarm identifier, e.g. "mega"'),
68609
+ roundNumber: tool.schema.number().int().min(1).max(10).optional().describe("1-indexed round number. Defaults to 1."),
68610
+ verdicts: tool.schema.array(tool.schema.object({
68611
+ agent: tool.schema.enum([
68612
+ "critic",
68613
+ "reviewer",
68614
+ "sme",
68615
+ "test_engineer",
68616
+ "explorer"
68617
+ ]),
68618
+ verdict: tool.schema.enum(["APPROVE", "CONCERNS", "REJECT"]),
68619
+ confidence: tool.schema.number().min(0).max(1),
68620
+ findings: tool.schema.array(tool.schema.object({
68621
+ severity: tool.schema.enum(["HIGH", "MEDIUM", "LOW"]),
68622
+ category: tool.schema.string().min(1),
68623
+ location: tool.schema.string(),
68624
+ detail: tool.schema.string(),
68625
+ evidence: tool.schema.string()
68626
+ })),
68627
+ criteriaAssessed: tool.schema.array(tool.schema.string()),
68628
+ criteriaUnmet: tool.schema.array(tool.schema.string()),
68629
+ durationMs: tool.schema.number()
68630
+ })).min(1).max(5).describe("Array of CouncilMemberVerdict objects. Must include between 1 and 5 entries, one per participating member (critic, reviewer, sme, test_engineer, explorer)."),
68631
+ working_directory: tool.schema.string().optional().describe("Explicit project root directory. When provided, .swarm/council/ and .swarm/evidence/ are resolved relative to this path instead of the plugin context directory.")
68632
+ },
68633
+ async execute(args2, directory, ctx) {
68634
+ const parsed = ArgsSchema.safeParse(args2);
68635
+ if (!parsed.success) {
68636
+ return JSON.stringify({
68637
+ success: false,
68638
+ reason: "invalid arguments",
68639
+ errors: parsed.error.issues.map((i2) => ({
68640
+ path: i2.path.join("."),
68641
+ message: i2.message
68642
+ }))
68643
+ }, null, 2);
68644
+ }
68645
+ const input = parsed.data;
68646
+ const dirResult = resolveWorkingDirectory(input.working_directory, directory);
68647
+ if (!dirResult.success) {
68648
+ return JSON.stringify({ success: false, reason: dirResult.message }, null, 2);
68649
+ }
68650
+ const workingDir = dirResult.directory;
68651
+ const config3 = loadPluginConfig(workingDir);
68652
+ if (!config3.council?.enabled) {
68653
+ return JSON.stringify({
68654
+ success: false,
68655
+ reason: "council feature is disabled \u2014 set council.enabled: true in .opencode/opencode-swarm.json to enable"
68656
+ }, null, 2);
68657
+ }
68658
+ if (config3.council?.requireAllMembers && input.verdicts.length < 5) {
68659
+ return JSON.stringify({
68660
+ success: false,
68661
+ reason: `council.requireAllMembers is true but only ${input.verdicts.length} of 5 member verdicts were provided`
68662
+ }, null, 2);
68663
+ }
68664
+ const criteria = readCriteria(workingDir, input.taskId);
68665
+ const verdicts = input.verdicts;
68666
+ const synthesis = synthesizeCouncilVerdicts(input.taskId, input.swarmId, verdicts, criteria, input.roundNumber, config3.council);
68667
+ writeCouncilEvidence(workingDir, synthesis);
68668
+ try {
68669
+ const sessionID = ctx?.sessionID;
68670
+ if (sessionID) {
68671
+ const session = getAgentSession(sessionID);
68672
+ if (session) {
68673
+ pushCouncilAdvisory(session, synthesis);
68674
+ }
68675
+ }
68676
+ } catch {}
68677
+ return JSON.stringify({
68678
+ success: true,
68679
+ overallVerdict: synthesis.overallVerdict,
68680
+ vetoedBy: synthesis.vetoedBy,
68681
+ roundNumber: synthesis.roundNumber,
68682
+ allCriteriaMet: synthesis.allCriteriaMet,
68683
+ requiredFixesCount: synthesis.requiredFixes.length,
68684
+ advisoryFindingsCount: synthesis.advisoryFindings.length,
68685
+ unresolvedConflictsCount: synthesis.unresolvedConflicts.length,
68686
+ unifiedFeedbackMd: synthesis.unifiedFeedbackMd
68687
+ }, null, 2);
68688
+ }
68689
+ });
68253
68690
  // src/tools/curator-analyze.ts
68254
68691
  init_dist();
68255
68692
  init_config();
@@ -68367,6 +68804,88 @@ var curator_analyze = createSwarmTool({
68367
68804
  }
68368
68805
  }
68369
68806
  });
68807
+ // src/tools/declare-council-criteria.ts
68808
+ init_dist();
68809
+ init_zod();
68810
+ init_loader();
68811
+ init_create_tool();
68812
+ init_resolve_working_directory();
68813
+ var CriteriaItemSchema = exports_external.object({
68814
+ id: exports_external.string().min(1).max(20).regex(/^C\d+$/, 'Criterion id must match C\\d+ (e.g. "C1", "C12")'),
68815
+ description: exports_external.string().min(10).max(500),
68816
+ mandatory: exports_external.boolean()
68817
+ });
68818
+ var ArgsSchema2 = exports_external.object({
68819
+ taskId: exports_external.string().min(1).regex(/^\d+\.\d+(\.\d+)*$/, "Task ID must be in N.M or N.M.P format"),
68820
+ criteria: exports_external.array(CriteriaItemSchema).min(1).max(20),
68821
+ working_directory: exports_external.string().optional()
68822
+ });
68823
+ var declare_council_criteria = createSwarmTool({
68824
+ description: "Pre-declare acceptance criteria for a task before the coder starts work. " + "Criteria are persisted under .swarm/council/ and read back during council " + "evaluation so reviewers assess a stable, pre-committed contract. " + "Architect-only. Config-gated via council.enabled.",
68825
+ args: {
68826
+ taskId: tool.schema.string().min(1).regex(/^\d+\.\d+(\.\d+)*$/, "Task ID must be in N.M or N.M.P format").describe('Task ID for which criteria are declared, e.g. "1.1", "1.2.3"'),
68827
+ criteria: tool.schema.array(tool.schema.object({
68828
+ id: tool.schema.string().min(1).max(20).regex(/^C\d+$/, "Criterion id must match C\\d+").describe('Criterion identifier, e.g. "C1", "C12"'),
68829
+ description: tool.schema.string().min(10).max(500).describe("Human-readable description of the criterion"),
68830
+ mandatory: tool.schema.boolean().describe("Whether the criterion is mandatory. Mandatory criteria block APPROVE when unmet.")
68831
+ })).min(1).max(20).describe("Array of acceptance criteria items. Must contain between 1 and 20 entries with unique ids."),
68832
+ working_directory: tool.schema.string().optional().describe("Explicit project root directory. When provided, .swarm/council/ is resolved relative to this path instead of the plugin context directory.")
68833
+ },
68834
+ async execute(args2, directory) {
68835
+ const parsed = ArgsSchema2.safeParse(args2);
68836
+ if (!parsed.success) {
68837
+ return JSON.stringify({
68838
+ success: false,
68839
+ reason: "invalid arguments",
68840
+ errors: parsed.error.issues.map((i2) => ({
68841
+ path: i2.path.join("."),
68842
+ message: i2.message
68843
+ }))
68844
+ }, null, 2);
68845
+ }
68846
+ const input = parsed.data;
68847
+ const dirResult = resolveWorkingDirectory(input.working_directory, directory);
68848
+ if (!dirResult.success) {
68849
+ return JSON.stringify({ success: false, reason: dirResult.message }, null, 2);
68850
+ }
68851
+ const workingDir = dirResult.directory;
68852
+ const config3 = loadPluginConfig(workingDir);
68853
+ if (!config3.council?.enabled) {
68854
+ return JSON.stringify({
68855
+ success: false,
68856
+ reason: "council feature is disabled \u2014 set council.enabled: true in .opencode/opencode-swarm.json to enable"
68857
+ }, null, 2);
68858
+ }
68859
+ const ids = input.criteria.map((c) => c.id);
68860
+ const idSet = new Set(ids);
68861
+ if (idSet.size < ids.length) {
68862
+ const seen = new Set;
68863
+ const duplicates = [];
68864
+ for (const id of ids) {
68865
+ if (seen.has(id) && !duplicates.includes(id)) {
68866
+ duplicates.push(id);
68867
+ }
68868
+ seen.add(id);
68869
+ }
68870
+ return JSON.stringify({
68871
+ success: false,
68872
+ reason: "duplicate criterion ids",
68873
+ errors: duplicates
68874
+ }, null, 2);
68875
+ }
68876
+ const existing = readCriteria(workingDir, input.taskId);
68877
+ const replaced = existing !== null;
68878
+ writeCriteria(workingDir, input.taskId, input.criteria);
68879
+ return JSON.stringify({
68880
+ success: true,
68881
+ taskId: input.taskId,
68882
+ criteriaCount: input.criteria.length,
68883
+ mandatoryCount: input.criteria.filter((c) => c.mandatory).length,
68884
+ declaredAt: new Date().toISOString(),
68885
+ replaced
68886
+ }, null, 2);
68887
+ }
68888
+ });
68370
68889
  // src/tools/declare-scope.ts
68371
68890
  init_tool();
68372
68891
  init_state();
@@ -69247,7 +69766,7 @@ import * as fs52 from "fs";
69247
69766
  import * as path67 from "path";
69248
69767
  var MAX_FILE_SIZE_BYTES6 = 1024 * 1024;
69249
69768
  var MAX_EVIDENCE_FILES = 1000;
69250
- var EVIDENCE_DIR2 = ".swarm/evidence";
69769
+ var EVIDENCE_DIR3 = ".swarm/evidence";
69251
69770
  var PLAN_FILE = ".swarm/plan.md";
69252
69771
  var SHELL_METACHAR_REGEX2 = /[;&|%$`\\]/;
69253
69772
  var VALID_EVIDENCE_FILENAME_REGEX = /^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*\.json$/;
@@ -69456,7 +69975,7 @@ var evidence_check = createSwarmTool({
69456
69975
  };
69457
69976
  return JSON.stringify(result2, null, 2);
69458
69977
  }
69459
- const evidenceDir = path67.join(cwd, EVIDENCE_DIR2);
69978
+ const evidenceDir = path67.join(cwd, EVIDENCE_DIR3);
69460
69979
  const evidence = readEvidenceFiles(evidenceDir, cwd);
69461
69980
  const { tasksWithFullEvidence, gaps } = analyzeGaps(completedTasks, evidence, requiredTypes);
69462
69981
  const completeness = completedTasks.length > 0 ? Math.round(tasksWithFullEvidence.length / completedTasks.length * 100) / 100 : 1;
@@ -70260,7 +70779,7 @@ init_dist();
70260
70779
  init_config();
70261
70780
  init_knowledge_store();
70262
70781
  init_create_tool();
70263
- import { existsSync as existsSync40 } from "fs";
70782
+ import { existsSync as existsSync42 } from "fs";
70264
70783
  var DEFAULT_LIMIT = 10;
70265
70784
  var MAX_LESSON_LENGTH = 200;
70266
70785
  var VALID_CATEGORIES3 = [
@@ -70329,14 +70848,14 @@ function validateLimit(limit) {
70329
70848
  }
70330
70849
  async function readSwarmKnowledge(directory) {
70331
70850
  const swarmPath = resolveSwarmKnowledgePath(directory);
70332
- if (!existsSync40(swarmPath)) {
70851
+ if (!existsSync42(swarmPath)) {
70333
70852
  return [];
70334
70853
  }
70335
70854
  return readKnowledge(swarmPath);
70336
70855
  }
70337
70856
  async function readHiveKnowledge() {
70338
70857
  const hivePath = resolveHiveKnowledgePath();
70339
- if (!existsSync40(hivePath)) {
70858
+ if (!existsSync42(hivePath)) {
70340
70859
  return [];
70341
70860
  }
70342
70861
  return readKnowledge(hivePath);
@@ -75186,7 +75705,7 @@ init_create_tool();
75186
75705
  import * as fs60 from "fs";
75187
75706
  import * as path76 from "path";
75188
75707
  var SPEC_FILE = ".swarm/spec.md";
75189
- var EVIDENCE_DIR3 = ".swarm/evidence";
75708
+ var EVIDENCE_DIR4 = ".swarm/evidence";
75190
75709
  var OBLIGATION_KEYWORDS = ["MUST", "SHOULD", "SHALL"];
75191
75710
  var MAX_FILE_SIZE_BYTES9 = 1024 * 1024;
75192
75711
  function extractRequirements(specContent) {
@@ -75482,7 +76001,7 @@ var req_coverage = createSwarmTool({
75482
76001
  message: "No FR requirements found in spec.md"
75483
76002
  }, null, 2);
75484
76003
  }
75485
- const evidenceDir = path76.join(cwd, EVIDENCE_DIR3);
76004
+ const evidenceDir = path76.join(cwd, EVIDENCE_DIR4);
75486
76005
  const touchedFiles = readTouchedFiles(evidenceDir, phase, cwd);
75487
76006
  const analyzedRequirements = [];
75488
76007
  let coveredCount = 0;
@@ -78498,6 +79017,7 @@ var todo_extract = createSwarmTool({
78498
79017
  });
78499
79018
  // src/tools/update-task-status.ts
78500
79019
  init_tool();
79020
+ init_loader();
78501
79021
  init_schema();
78502
79022
  init_gate_evidence();
78503
79023
  import * as fs70 from "fs";
@@ -78844,6 +79364,41 @@ function recoverTaskStateFromDelegations(taskId) {
78844
79364
  }
78845
79365
  }
78846
79366
  }
79367
+ function checkCouncilGate(workingDirectory, taskId) {
79368
+ let councilEnabled = false;
79369
+ try {
79370
+ const config3 = loadPluginConfig(workingDirectory);
79371
+ councilEnabled = config3.council?.enabled === true;
79372
+ } catch {
79373
+ return { blocked: false, reason: "" };
79374
+ }
79375
+ if (!councilEnabled) {
79376
+ return { blocked: false, reason: "" };
79377
+ }
79378
+ let evidence;
79379
+ try {
79380
+ evidence = readTaskEvidenceRaw(workingDirectory, taskId);
79381
+ } catch {
79382
+ return {
79383
+ blocked: true,
79384
+ reason: "council gate required but not yet run \u2014 architect must call convene_council before advancing this task"
79385
+ };
79386
+ }
79387
+ const councilGate = evidence?.gates?.council;
79388
+ if (!councilGate) {
79389
+ return {
79390
+ blocked: true,
79391
+ reason: "council gate required but not yet run \u2014 architect must call convene_council before advancing this task"
79392
+ };
79393
+ }
79394
+ if (councilGate.verdict === "REJECT") {
79395
+ return {
79396
+ blocked: true,
79397
+ reason: "council gate blocked advancement \u2014 resolve requiredFixes and re-run convene_council"
79398
+ };
79399
+ }
79400
+ return { blocked: false, reason: "" };
79401
+ }
78847
79402
  async function executeUpdateTaskStatus(args2, fallbackDir) {
78848
79403
  const statusError = validateStatus(args2.status);
78849
79404
  if (statusError) {
@@ -78980,6 +79535,14 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
78980
79535
  };
78981
79536
  }
78982
79537
  }
79538
+ const councilCheck = checkCouncilGate(directory, args2.task_id);
79539
+ if (councilCheck.blocked) {
79540
+ return {
79541
+ success: false,
79542
+ message: councilCheck.reason,
79543
+ errors: [councilCheck.reason]
79544
+ };
79545
+ }
78983
79546
  }
78984
79547
  const lockTaskId = `update-task-status-${args2.task_id}-${Date.now()}`;
78985
79548
  const planFilePath = "plan.json";
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Work Complete Council — architect-only tool.
3
+ *
4
+ * Accepts parallel verdicts from critic, reviewer, sme, and test_engineer,
5
+ * then synthesizes them into a veto-aware overall verdict with required fixes
6
+ * and a single unified feedback document.
7
+ *
8
+ * Config-gated (council.enabled must be true) and architect-only via
9
+ * AGENT_TOOL_MAP. Follows the check-gate-status.ts pattern.
10
+ */
11
+ import { tool } from '@opencode-ai/plugin';
12
+ import { z } from 'zod';
13
+ export declare const ArgsSchema: z.ZodObject<{
14
+ taskId: z.ZodString;
15
+ swarmId: z.ZodString;
16
+ roundNumber: z.ZodDefault<z.ZodNumber>;
17
+ verdicts: z.ZodArray<z.ZodObject<{
18
+ agent: z.ZodEnum<{
19
+ reviewer: "reviewer";
20
+ test_engineer: "test_engineer";
21
+ explorer: "explorer";
22
+ sme: "sme";
23
+ critic: "critic";
24
+ }>;
25
+ verdict: z.ZodEnum<{
26
+ APPROVE: "APPROVE";
27
+ CONCERNS: "CONCERNS";
28
+ REJECT: "REJECT";
29
+ }>;
30
+ confidence: z.ZodNumber;
31
+ findings: z.ZodArray<z.ZodObject<{
32
+ severity: z.ZodEnum<{
33
+ HIGH: "HIGH";
34
+ MEDIUM: "MEDIUM";
35
+ LOW: "LOW";
36
+ }>;
37
+ category: z.ZodString;
38
+ location: z.ZodString;
39
+ detail: z.ZodString;
40
+ evidence: z.ZodString;
41
+ }, z.core.$strip>>;
42
+ criteriaAssessed: z.ZodArray<z.ZodString>;
43
+ criteriaUnmet: z.ZodArray<z.ZodString>;
44
+ durationMs: z.ZodNumber;
45
+ }, z.core.$strip>>;
46
+ working_directory: z.ZodOptional<z.ZodString>;
47
+ }, z.core.$strip>;
48
+ export declare const convene_council: ReturnType<typeof tool>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Work Complete Council — pre-declaration tool.
3
+ *
4
+ * Lets the architect declare acceptance criteria at plan time, before the
5
+ * coder starts work. Criteria are persisted to .swarm/council/{safeId}.json
6
+ * and later read back during council evaluation (convene_council) so that
7
+ * reviewers assess a stable, pre-committed contract rather than whatever
8
+ * criteria happen to be invented at review time.
9
+ *
10
+ * Config-gated (council.enabled must be true) and architect-only via
11
+ * AGENT_TOOL_MAP. Follows the convene-council.ts pattern.
12
+ */
13
+ import { tool } from '@opencode-ai/plugin';
14
+ export declare const declare_council_criteria: ReturnType<typeof tool>;
@@ -5,7 +5,9 @@ export { checkpoint } from './checkpoint';
5
5
  export { co_change_analyzer } from './co-change-analyzer';
6
6
  export { completion_verify } from './completion-verify';
7
7
  export { complexity_hotspots } from './complexity-hotspots';
8
+ export { convene_council } from './convene-council';
8
9
  export { curator_analyze } from './curator-analyze';
10
+ export { declare_council_criteria } from './declare-council-criteria';
9
11
  export { declare_scope } from './declare-scope';
10
12
  export { type DiffErrorResult, type DiffResult, diff } from './diff';
11
13
  export { doc_extract, doc_scan } from './doc-scan';
@@ -3,7 +3,7 @@
3
3
  * Used for constants and agent setup references.
4
4
  */
5
5
  /** Union type of all valid tool names */
6
- export type ToolName = 'diff' | 'syntax_check' | 'placeholder_scan' | 'imports' | 'lint' | 'secretscan' | 'sast_scan' | 'build_check' | 'pre_check_batch' | 'quality_budget' | 'symbols' | 'complexity_hotspots' | 'schema_drift' | 'todo_extract' | 'evidence_check' | 'check_gate_status' | 'completion_verify' | 'sbom_generate' | 'checkpoint' | 'pkg_audit' | 'test_runner' | 'detect_domains' | 'gitingest' | 'retrieve_summary' | 'extract_code_blocks' | 'phase_complete' | 'save_plan' | 'update_task_status' | 'lint_spec' | 'write_retro' | 'write_drift_evidence' | 'declare_scope' | 'knowledge_query' | 'doc_scan' | 'doc_extract' | 'curator_analyze' | 'knowledge_add' | 'knowledge_recall' | 'knowledge_remove' | 'co_change_analyzer' | 'search' | 'batch_symbols' | 'suggest_patch' | 'req_coverage' | 'get_approved_plan' | 'repo_map';
6
+ export type ToolName = 'diff' | 'syntax_check' | 'placeholder_scan' | 'imports' | 'lint' | 'secretscan' | 'sast_scan' | 'build_check' | 'pre_check_batch' | 'quality_budget' | 'symbols' | 'complexity_hotspots' | 'schema_drift' | 'todo_extract' | 'evidence_check' | 'check_gate_status' | 'completion_verify' | 'convene_council' | 'declare_council_criteria' | 'sbom_generate' | 'checkpoint' | 'pkg_audit' | 'test_runner' | 'detect_domains' | 'gitingest' | 'retrieve_summary' | 'extract_code_blocks' | 'phase_complete' | 'save_plan' | 'update_task_status' | 'lint_spec' | 'write_retro' | 'write_drift_evidence' | 'declare_scope' | 'knowledge_query' | 'doc_scan' | 'doc_extract' | 'curator_analyze' | 'knowledge_add' | 'knowledge_recall' | 'knowledge_remove' | 'co_change_analyzer' | 'search' | 'batch_symbols' | 'suggest_patch' | 'req_coverage' | 'get_approved_plan' | 'repo_map';
7
7
  /** Readonly array of all tool names */
8
8
  export declare const TOOL_NAMES: readonly ToolName[];
9
9
  /** Set for O(1) tool name validation */
@@ -71,6 +71,25 @@ export declare function checkReviewerGateWithScope(taskId: string, workingDirect
71
71
  * @param taskId - The task ID to recover state for
72
72
  */
73
73
  export declare function recoverTaskStateFromDelegations(taskId: string): void;
74
+ /**
75
+ * Result of the council-gate check used when transitioning to 'completed'.
76
+ *
77
+ * - When council.enabled is false, {blocked:false} is always returned (no regression).
78
+ * - When council.enabled is true, requires evidence.gates.council to exist and
79
+ * its verdict to be APPROVE or CONCERNS. A missing gate or REJECT verdict blocks.
80
+ */
81
+ export interface CouncilGateResult {
82
+ blocked: boolean;
83
+ reason: string;
84
+ }
85
+ /**
86
+ * Check the council gate for a completion transition. Pure — reads config and
87
+ * evidence only, no state mutation. Exported for focused unit testing.
88
+ *
89
+ * @param workingDirectory - Validated project root (contains .swarm/evidence/)
90
+ * @param taskId - Task ID in N.M or N.M.P format
91
+ */
92
+ export declare function checkCouncilGate(workingDirectory: string, taskId: string): CouncilGateResult;
74
93
  /**
75
94
  * Execute the update_task_status tool.
76
95
  * Validates the task_id and status, then updates the task status in the plan.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "6.65.0",
3
+ "version": "6.66.0",
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",