opencouncil 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenCouncil Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # OpenCouncil Plugin for OpenCode
2
+
3
+ A multi-model council plugin for OpenCode that runs prompts across multiple LLMs and synthesizes their responses to identify consensus and discrepancies.
4
+
5
+ ## Installation
6
+
7
+ Add the plugin to your `opencode.json`:
8
+
9
+ ```json
10
+ {
11
+ "plugin": ["opencode-opencouncil"]
12
+ }
13
+ ```
14
+
15
+ Then restart OpenCode. The plugin will be automatically installed via Bun.
16
+
17
+ ## Configuration
18
+
19
+ Add the configuration to your `opencode.json`:
20
+
21
+ ```json
22
+ {
23
+ "plugin": ["opencode-opencouncil"],
24
+ "opencouncil": {
25
+ "defaultCouncil": "standard",
26
+ "councils": {
27
+ "standard": {
28
+ "members": [
29
+ { "name": "gemini", "model": "google/gemini-2.0-flash", "temperature": 0.2 },
30
+ { "name": "codex", "model": "openai/codex", "temperature": 0.2 },
31
+ { "name": "opus", "model": "anthropic/claude-opus-4", "temperature": 0.2 }
32
+ ]
33
+ }
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### Council Member Options
40
+
41
+ Each member supports:
42
+ - `name`: Identifier for the member
43
+ - `model`: Provider/model ID (format: `provider/model`)
44
+ - `temperature`: Sampling temperature (optional)
45
+ - `system`: Custom system prompt (optional)
46
+ - `options`: Additional model options (optional)
47
+
48
+ ## Usage
49
+
50
+ ### Tool: `opencouncil.run`
51
+
52
+ Run a prompt through the configured council:
53
+
54
+ ```
55
+ Use tool: opencouncil.run
56
+ Prompt: "Explain the benefits of TypeScript"
57
+ ```
58
+
59
+ Optional parameters:
60
+ - `council`: Specify a council name (defaults to `defaultCouncil`)
61
+
62
+ ### Tool: `opencouncil.init`
63
+
64
+ Initialize configuration and optionally create an agent file:
65
+
66
+ ```
67
+ Use tool: opencouncil.init
68
+ writeAgentFile: true
69
+ ```
70
+
71
+ This will:
72
+ 1. Print the configuration block to add to your `opencode.json`
73
+ 2. If `writeAgentFile` is true, create `.opencode/agents/opencouncil.md`
74
+
75
+ ### Agent: `@opencouncil`
76
+
77
+ If you ran `opencouncil.init` with `writeAgentFile: true`, you can use:
78
+
79
+ ```
80
+ @opencouncil "What are the best practices for error handling?"
81
+ ```
82
+
83
+ The agent will automatically call `opencouncil.run` and summarize the results.
84
+
85
+ ## Output Format
86
+
87
+ The plugin returns:
88
+ 1. **Member responses** - Individual answers from each model
89
+ 2. **Consensus** - Claims endorsed by multiple members
90
+ 3. **Discrepancies** - Claims mentioned by only one member
91
+ 4. **Uncertainties** - Each member's expressed uncertainties
92
+
93
+ ## Troubleshooting
94
+
95
+ ### Plugin installation stuck
96
+
97
+ Clear the cache and restart:
98
+ ```bash
99
+ rm -rf ~/.cache/opencode
100
+ ```
101
+
102
+ ### Model not found
103
+
104
+ Ensure the model IDs match those available in your OpenCode installation. Use OpenCode's CLI commands to discover available models.
105
+
106
+ ## License
107
+
108
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ import { makeRunTool } from "./tools/run.js";
2
+ import { makeInitTool } from "./tools/init.js";
3
+ const plugin = async (input) => {
4
+ return {
5
+ tool: {
6
+ "opencouncil.run": makeRunTool(input),
7
+ "opencouncil.init": makeInitTool(input)
8
+ }
9
+ };
10
+ };
11
+ export default plugin;
@@ -0,0 +1,13 @@
1
+ export function getCouncil(cfg, requested) {
2
+ const oc = cfg.opencouncil ?? {};
3
+ const councilName = requested ?? oc.defaultCouncil ?? "standard";
4
+ const council = oc.councils?.[councilName];
5
+ const members = council?.members?.length
6
+ ? council.members
7
+ : [
8
+ { name: "gemini", model: "google/gemini-2.0-flash", temperature: 0.2 },
9
+ { name: "codex", model: "openai/codex", temperature: 0.2 },
10
+ { name: "opus", model: "anthropic/claude-opus-4", temperature: 0.2 }
11
+ ];
12
+ return { councilName, members, rubric: council?.rubric };
13
+ }
@@ -0,0 +1,4 @@
1
+ export function splitModel(model) {
2
+ const [providerID, ...rest] = model.split("/");
3
+ return { providerID, modelID: rest.join("/") };
4
+ }
@@ -0,0 +1,14 @@
1
+ export const memberOutputFormat = {
2
+ type: "json_schema",
3
+ name: "member_response",
4
+ schema: {
5
+ type: "object",
6
+ additionalProperties: false,
7
+ properties: {
8
+ answer: { type: "string" },
9
+ claims: { type: "array", items: { type: "string" } },
10
+ uncertainties: { type: "array", items: { type: "string" } }
11
+ },
12
+ required: ["answer", "claims", "uncertainties"]
13
+ }
14
+ };
@@ -0,0 +1,20 @@
1
+ function normalize(s) {
2
+ return s.trim().toLowerCase();
3
+ }
4
+ export function synthesize(results) {
5
+ const claimIndex = new Map();
6
+ for (const r of results) {
7
+ for (const c of r.claims) {
8
+ const key = normalize(c);
9
+ const entry = claimIndex.get(key) ?? { claim: c, by: [] };
10
+ entry.by.push({ member: r.member, model: r.model });
11
+ claimIndex.set(key, entry);
12
+ }
13
+ }
14
+ const allClaims = [...claimIndex.values()].sort((a, b) => b.by.length - a.by.length);
15
+ const agreed = allClaims.filter(x => x.by.length > 1);
16
+ const disputedCandidates = allClaims.filter(x => x.by.length === 1);
17
+ // Youll improve this with semantic clustering / contradiction detection later.
18
+ const discrepancies = disputedCandidates.slice(0, 10);
19
+ return { agreed, discrepancies };
20
+ }
@@ -0,0 +1,51 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ export function makeInitTool(pluginInput) {
3
+ return tool({
4
+ description: "Prints (and optionally writes) starter config + agent file for OpenCouncil.",
5
+ args: {
6
+ writeAgentFile: tool.schema.boolean().optional().describe("If true, writes .opencode/agents/opencouncil.md")
7
+ },
8
+ async execute(args, ctx) {
9
+ const { $, directory } = pluginInput;
10
+ if (args.writeAgentFile) {
11
+ await $ `mkdir -p ${directory}/.opencode/agents`;
12
+ await $ `cat > ${directory}/.opencode/agents/opencouncil.md << 'EOF'
13
+ ---
14
+ description: Runs a multi-model council and synthesizes discrepancies
15
+ mode: primary
16
+ tools:
17
+ write: false
18
+ edit: false
19
+ bash: false
20
+ ---
21
+
22
+ When asked a question, call the tool opencouncil.run with the users prompt.
23
+ Then summarize consensus and list key discrepancies.
24
+ EOF`;
25
+ }
26
+ return [
27
+ "Add this to your opencode.json (top-level):",
28
+ "",
29
+ "```jsonc",
30
+ "{",
31
+ ' "plugin": ["opencode-opencouncil"],',
32
+ ' "opencouncil": {',
33
+ ' "defaultCouncil": "standard",',
34
+ ' "councils": {',
35
+ ' "standard": {',
36
+ ' "members": [',
37
+ ' { "name": "gemini", "model": "google/<your-gemini-model>", "temperature": 0.2 },',
38
+ ' { "name": "codex", "model": "openai/<your-codex-model>", "temperature": 0.2 },',
39
+ ' { "name": "opus", "model": "anthropic/<your-opus-model>", "temperature": 0.2 }',
40
+ " ]",
41
+ " }",
42
+ " }",
43
+ " }",
44
+ "}",
45
+ "```",
46
+ "",
47
+ "Then restart OpenCode (it will auto-install npm plugins)."
48
+ ].join("\n");
49
+ }
50
+ });
51
+ }
@@ -0,0 +1,116 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { getCouncil } from "../lib/config.js";
3
+ import { splitModel } from "../lib/models.js";
4
+ import { synthesize } from "../lib/synthesize.js";
5
+ function isTextPart(part) {
6
+ return (typeof part === "object" &&
7
+ part !== null &&
8
+ "type" in part &&
9
+ part.type === "text" &&
10
+ "text" in part &&
11
+ typeof part.text === "string");
12
+ }
13
+ const MEMBER_SYSTEM_PROMPT = `You are a council member evaluating a prompt. Respond with a JSON object containing:
14
+ - answer: your detailed response to the prompt
15
+ - claims: an array of key factual claims you make (as strings)
16
+ - uncertainties: an array of anything you're uncertain about (as strings)
17
+
18
+ Format your response as valid JSON only, no markdown code blocks.`;
19
+ export function makeRunTool(pluginInput) {
20
+ return tool({
21
+ description: "Runs multiple configured models on a prompt, then summarizes and identifies discrepancies.",
22
+ args: {
23
+ prompt: tool.schema.string().describe("The prompt to evaluate"),
24
+ council: tool.schema.string().optional().describe("Council name (defaults to configured defaultCouncil)")
25
+ },
26
+ async execute(args, ctx) {
27
+ const { client } = pluginInput;
28
+ const configResp = await client.config.get();
29
+ const cfg = configResp.data;
30
+ const { councilName, members } = getCouncil(cfg, args.council);
31
+ const results = await Promise.all(members.map(async (m) => {
32
+ const { providerID, modelID } = splitModel(m.model);
33
+ const systemPrompt = m.system?.trim()
34
+ ? `${m.system.trim()}\n\n${MEMBER_SYSTEM_PROMPT}`
35
+ : MEMBER_SYSTEM_PROMPT;
36
+ const res = await client.session.prompt({
37
+ path: { id: cfg.session?.id ?? "current" },
38
+ body: {
39
+ model: { providerID, modelID },
40
+ system: systemPrompt,
41
+ parts: [{ type: "text", text: args.prompt }]
42
+ }
43
+ });
44
+ const data = res.data;
45
+ if (!data) {
46
+ return {
47
+ member: m.name,
48
+ model: m.model,
49
+ answer: "",
50
+ claims: [],
51
+ uncertainties: []
52
+ };
53
+ }
54
+ // Use unknown to bypass TypeScript's strict Part union type checking
55
+ const parts = data.parts;
56
+ const textPart = parts.find((p) => isTextPart(p));
57
+ const text = textPart?.text ?? "{}";
58
+ let parsed;
59
+ try {
60
+ // Try to extract JSON from markdown code blocks if present
61
+ const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/)
62
+ || text.match(/\{[\s\S]*\}/);
63
+ const jsonText = jsonMatch ? jsonMatch[1] || jsonMatch[0] : text;
64
+ parsed = JSON.parse(jsonText);
65
+ }
66
+ catch {
67
+ parsed = {
68
+ answer: text,
69
+ claims: [],
70
+ uncertainties: ["Failed to parse structured response"]
71
+ };
72
+ }
73
+ return {
74
+ member: m.name,
75
+ model: m.model,
76
+ answer: parsed.answer ?? text ?? "",
77
+ claims: parsed.claims ?? [],
78
+ uncertainties: parsed.uncertainties ?? []
79
+ };
80
+ }));
81
+ const { agreed, discrepancies } = synthesize(results);
82
+ const lines = [];
83
+ lines.push(`# OpenCouncil (${councilName})`);
84
+ lines.push("");
85
+ lines.push("## Member responses");
86
+ for (const r of results) {
87
+ lines.push(`### ${r.member} (${r.model})`);
88
+ lines.push(r.answer || "_(no answer)_");
89
+ if (r.uncertainties?.length) {
90
+ lines.push("");
91
+ lines.push("**Uncertainties:**");
92
+ for (const u of r.uncertainties)
93
+ lines.push(`- ${u}`);
94
+ }
95
+ lines.push("");
96
+ }
97
+ lines.push("## Consensus (claims repeated by >1 member)");
98
+ if (!agreed.length)
99
+ lines.push("- _(none detected)_");
100
+ for (const a of agreed) {
101
+ lines.push(`- ${a.claim} _(endorsed by: ${a.by.map(x => x.member).join(", ")})_`);
102
+ }
103
+ lines.push("");
104
+ lines.push("## Discrepancies (claims only mentioned by one member)");
105
+ if (!discrepancies.length)
106
+ lines.push("- _(none detected)_");
107
+ for (const d of discrepancies) {
108
+ lines.push(`- ${d.claim} _(only: ${d.by[0].member})_`);
109
+ }
110
+ lines.push("");
111
+ lines.push("## Next improvement");
112
+ lines.push("- For higher quality discrepancy detection, add a second-pass judge prompt that clusters claims semantically and flags contradictions.");
113
+ return lines.join("\n");
114
+ }
115
+ });
116
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "opencouncil",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "exports": "./dist/index.js",
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "dependencies": {
13
+ "@opencode-ai/plugin": "^1.1.65"
14
+ },
15
+ "devDependencies": {
16
+ "typescript": "^5.5.0"
17
+ },
18
+ "keywords": [
19
+ "opencode",
20
+ "plugin",
21
+ "agents",
22
+ "llm"
23
+ ],
24
+ "license": "MIT",
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json"
27
+ }
28
+ }