e3d-pilot 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  **Multi-model AI problem solving: independent answers → critique → convergence → human decision → execution → review → provenance.**
4
4
 
5
- ![e3d-debate: independent AI models answer a question, critique each other across rounds, converge on a synthesis with consensus and named dissent, then hand off to a human decision, execution, and provenance](docs/images/debate-convergence.svg)
5
+ ![e3d-debate: independent AI models answer a question, critique each other across rounds, converge on a synthesis with consensus and named dissent, then hand off to a human decision, execution, and provenance](https://raw.githubusercontent.com/spacepacket1/e3d-pilot/main/docs/images/debate-convergence.svg)
6
6
 
7
7
  e3d-pilot is an open-source system for getting multiple AI models to work on the same problem instead of trusting a single model's answer — from a one-off question to a fully executed, human-gated software change.
8
8
 
9
- It starts simply. Ask several configured models (Claude, Codex, Grok, Devin, or others you wire in) the same question. Let them answer independently, see and critique each other's positions, revise, expose real dissent, and converge on the strongest answer. That's `bin/e3d-debate` — a standalone tool, no repo or pipeline required.
9
+ It starts simply. Ask several configured models (Claude, Codex, Grok, Devin, or others you wire in) the same question. Let them answer independently, see and critique each other's positions, revise, expose real dissent, and converge on the strongest answer. That's `bin/e3d-debate` — a standalone tool, no repo or pipeline required. It's also available inside your editor as an MCP server: [use it from GitHub Copilot, Claude Code, or Cursor](#use-e3d-pilot-from-github-copilot-claude-code-and-cursor-mcp) with one `npx` line.
10
10
 
11
11
  Then e3d-pilot takes the same instinct further, into real software work: `discover → ideate → approve → draft → negotiate → execute → review → publish → outcome`. Every step is preserved in an append-only ledger, so the system (or a human, or a fresh AI session) can later reconstruct where an idea came from, which models or people influenced it, what it produced, and what happened afterward.
12
12
 
@@ -70,13 +70,34 @@ demand instead — a real, named disagreement the tool didn't paper over.
70
70
 
71
71
  You don't need the rest of e3d-pilot to use `e3d-debate` on its own — see [e3d-debate](#e3d-debate-experimental) below for the full flag reference. Keep reading for the part that takes a debate's answer all the way to a shipped, reviewed, provenance-tracked change.
72
72
 
73
- ## Use e3d-debate from GitHub Copilot (MCP)
73
+ ## Use e3d-pilot from GitHub Copilot, Claude Code, and Cursor (MCP)
74
74
 
75
- `bin/e3d-mcp` is a zero-dependency [Model Context Protocol](https://modelcontextprotocol.io) server, so Copilot agent mode in VS Code (and any other MCP client: Claude Code, Cursor, ...) can ask for a multi-model second opinion mid-task.
75
+ `e3d-mcp` is a zero-dependency [Model Context Protocol](https://modelcontextprotocol.io) server. It lets an AI coding assistant ask for a multi-model second opinion mid-task, instead of committing to the first answer one model gives it. It's published on npm as [`e3d-pilot`](https://www.npmjs.com/package/e3d-pilot) and listed in the official MCP registry as `io.github.spacepacket1/e3d-pilot`.
76
76
 
77
- Tools: `e3d_debate` (answer + consensus + named dissent + transcript), `e3d_list_providers`, and read-only `e3d_provenance_trace` / `e3d_ideas_list` ("why does this change exist?").
77
+ | Tool | What it does |
78
+ | --- | --- |
79
+ | `e3d_debate` | Several independent models answer and a synthesizer returns a decisive answer, a consensus level (`full` / `majority` / `split`), any named dissent, and each model's final position, as both readable text and structured data (`dissent.present`, `dissent.summary`, `positions`). `mode: "quick"` (default) is one round and takes seconds; `mode: "thorough"` adds a critique round where models rebut each other. The full transcript is opt-in (`include_transcript`). |
80
+ | `e3d_list_providers` | Shows which model providers are available on this machine. A debate needs at least two. |
81
+ | `e3d_provenance_trace` | Read-only: traces why a change exists, from its origin through debates, approvals, execution, review, and outcome. |
82
+ | `e3d_ideas_list` | Read-only: lists the ideas e3d-pilot tracks in a repo, to find an id for the trace. |
83
+
84
+ A real result in `quick` mode (11 seconds, two models), from asking whether a small CLI should default to a flat JSON file or SQLite:
85
+
86
+ ```text
87
+ FINAL ANSWER: Default to a flat JSON file for a small CLI. It has no dependencies, is
88
+ human-readable and diffable, and is easy to hand-edit and debug. Write it atomically
89
+ (temp file, then rename). Move to SQLite once you need indexed queries, concurrent
90
+ writers, or data beyond a few MB.
91
+ CONSENSUS: split
92
+ DISSENT: Codex prefers SQLite as the default for any persistent mutable state because
93
+ it gives transactions, concurrency safety, and schema evolution at low cost.
94
+ ```
95
+
96
+ The disagreement is the point: you get the answer and the strongest case against it, instead of one model's confident guess.
97
+
98
+ ### Install
78
99
 
79
- **Install (VS Code / Copilot agent mode):** [one-click install](https://insiders.vscode.dev/redirect/mcp/install?name=e3d-pilot&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22-p%22%2C%22e3d-pilot%22%2C%22e3d-mcp%22%5D%7D), or add this to `.vscode/mcp.json`:
100
+ **VS Code (GitHub Copilot agent mode):** [one-click install](https://insiders.vscode.dev/redirect/mcp/install?name=e3d-pilot&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22-p%22%2C%22e3d-pilot%22%2C%22e3d-mcp%22%5D%7D), or add this to `.vscode/mcp.json`:
80
101
 
81
102
  ```json
82
103
  {
@@ -86,14 +107,37 @@ Tools: `e3d_debate` (answer + consensus + named dissent + transcript), `e3d_list
86
107
  }
87
108
  ```
88
109
 
89
- Prerequisites: Node 18+, `jq`, bash 4+ (macOS: `brew install bash jq`), and at least two model CLIs logged in (e.g. `claude` and `codex`). Check them all with `npx -y -p e3d-pilot e3d-mcp --doctor`. From a clone, `bin/e3d-mcp --print-config --local` prints a config pointing at your checkout.
110
+ **Claude Code:**
111
+
112
+ ```bash
113
+ claude mcp add e3d-pilot -- npx -y -p e3d-pilot e3d-mcp
114
+ ```
115
+
116
+ **Cursor** (`~/.cursor/mcp.json`) and other clients that use the `mcpServers` shape:
117
+
118
+ ```json
119
+ { "mcpServers": { "e3d-pilot": { "command": "npx", "args": ["-y", "-p", "e3d-pilot", "e3d-mcp"] } } }
120
+ ```
121
+
122
+ Any MCP client that can launch a stdio server should work; the shapes above are each client's standard format.
123
+
124
+ **Prerequisites:** Node 18+, `jq`, bash 4+ (macOS ships 3.2: `brew install bash jq`), and at least two model CLIs logged in (e.g. `claude` and `codex`). Check them all at once:
125
+
126
+ ```bash
127
+ npx -y -p e3d-pilot e3d-mcp --doctor
128
+ ```
129
+
130
+ `--doctor` reports what's missing and how to fix it. It does not need the full pipeline's `e3d-code` executor (it only notes whether that is installed). From a clone, `bin/e3d-mcp --print-config --local` prints a config that points at your checkout.
90
131
 
91
- Then, in Copilot agent mode: *"Before you pick a storage layer, run e3d_debate on SQLite vs flat JSON for this CLI."* To make Copilot do that unprompted for contestable design choices, copy [examples/copilot-instructions.md](examples/copilot-instructions.md) into `.github/copilot-instructions.md` or `AGENTS.md`.
132
+ ### Using it
92
133
 
93
- Notes:
94
- - Debates are slow (minutes) and spend **your** model credits. The server caps rounds/providers/concurrency: `E3D_MCP_MAX_ROUNDS` (3), `E3D_MCP_MAX_PROVIDERS` (4), `E3D_MCP_MAX_CONCURRENT` (2), `E3D_MCP_TIMEOUT_SEC` (900).
95
- - It uses whatever provider CLIs/keys are in the server's environment (`claude`, `codex`, ... see `e3d-pilot providers list`). Needs at least two available.
96
- - The Copilot *coding agent* runs on GitHub-hosted runners, where those provider CLIs are not installed; use the local (stdio) setup above for now.
134
+ In Copilot agent mode (or any client), ask: *"Before you pick a storage layer, run e3d_debate on SQLite vs flat JSON for this CLI."* To make the assistant do this unprompted for contestable design choices, copy [examples/copilot-instructions.md](examples/copilot-instructions.md) into `.github/copilot-instructions.md` or `AGENTS.md`. Don't use it for lookups or questions with an obvious answer; it is slow and costs money.
135
+
136
+ ### Cost and limits
137
+
138
+ - `quick` debates take seconds to a minute and `thorough` ones a few minutes; both spend **your** model credits. The server caps `e3d_debate` with `E3D_MCP_MAX_ROUNDS` (3), `E3D_MCP_MAX_PROVIDERS` (4), `E3D_MCP_MAX_CONCURRENT` (2), and `E3D_MCP_TIMEOUT_SEC` (900).
139
+ - It uses whatever provider CLIs and keys are in the server's environment (see `e3d-pilot providers list`).
140
+ - The Copilot *coding agent* runs on GitHub-hosted runners, where those provider CLIs aren't installed. Use the local (stdio) setup above for now.
97
141
  - The server never writes to a repo. Recording a verdict into provenance is still `e3d-debate --repo --idea`.
98
142
 
99
143
  ## Why we built this
@@ -209,11 +253,18 @@ Start from [`examples/sample-fleet-config-with-ops.json`](examples/sample-fleet-
209
253
  Install Bash, Git, `jq`, and `curl`; the full pipeline's execute stage also needs [`e3d-code`](https://github.com/spacepacket1/e3d-code) (formerly `spec2code`; the debate and MCP tools do not). Install and authenticate whichever model CLIs and forge CLI your configuration uses (`claude`, `codex`, `devin`, a local OpenAI-compatible endpoint, and/or `grok` for `grok-build`). Add this repository's `bin` directory to `PATH`:
210
254
 
211
255
  ```bash
256
+ # From npm (also installs e3d-code, the execute-stage dependency, when it can):
257
+ npm install -g e3d-pilot
258
+ e3d-pilot --help
259
+
260
+ # Or from a clone:
212
261
  git clone https://github.com/spacepacket1/e3d-pilot.git
213
262
  export PATH="$PWD/e3d-pilot/bin:$PATH"
214
263
  e3d-pilot --help
215
264
  ```
216
265
 
266
+ The npm package provides three commands: `e3d-pilot`, `e3d-debate`, and `e3d-mcp`.
267
+
217
268
  Node.js is only needed for the optional `e3d-pilot web` dashboard (see [Web UI](#web-ui-optional) below) — every other command needs only Bash, Git, `jq`, and `curl`.
218
269
 
219
270
  ## Configure a repository
@@ -578,6 +629,8 @@ Both `E3D_PILOT_WEB_AUTH_USER` and `E3D_PILOT_WEB_AUTH_PASS` are required; there
578
629
 
579
630
  ## e3d-debate (experimental)
580
631
 
632
+ > Want this inside your editor? `e3d-debate` is also exposed as an [MCP tool](#use-e3d-pilot-from-github-copilot-claude-code-and-cursor-mcp) for Copilot, Claude Code, and Cursor.
633
+
581
634
  `bin/e3d-debate` is a separate, standalone script — not wired into `bin/e3d-pilot`'s subcommand tree or `config.schema.json`, the same way `bin/e3d-backend-benchmark` and `bin/e3d-grok-workers` already aren't — for the "ask several models a question and see whether they actually agree" loop, applied to any question rather than repo work specifically. It reuses the same `lib/providers/<name>` adapters and the `lib/negotiate/convergence` primitive that discover/ideate/negotiate/review already depend on. "Experimental" here means still standalone, not wired into the pipeline proper — not unused or untested: `tests/phase34.sh` covers the provenance-linking flags below, and it has already been the deciding mechanism behind several real architecture and product-direction calls for this project, not just a demo.
582
635
 
583
636
  ```bash
package/bin/e3d-mcp CHANGED
@@ -22,7 +22,7 @@ import { fileURLToPath } from 'node:url';
22
22
  import { createInterface } from 'node:readline';
23
23
 
24
24
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
25
- const SERVER_INFO = { name: 'e3d-pilot', version: '0.1.0' };
25
+ const SERVER_INFO = { name: 'e3d-pilot', version: '0.1.1' };
26
26
  const SUPPORTED_PROTOCOLS = ['2025-06-18', '2025-03-26', '2024-11-05'];
27
27
 
28
28
  const envInt = (name, dflt) => {
@@ -63,26 +63,79 @@ async function configuredProviders() {
63
63
 
64
64
  const NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
65
65
 
66
+ // quick = one round of independent answers + one synthesis call (fast, no cross-critique);
67
+ // thorough = the debate as originally shipped (2 rounds, models see and rebut each other).
68
+ const MODES = { quick: { rounds: 1, maxWords: 80 }, thorough: { rounds: 2, maxWords: 200 } };
69
+
70
+ // e3d-debate prints "FINAL ANSWER: ...\nCONSENSUS: ...\nDISSENT: ..." -- turn that into fields.
71
+ function parseVerdict(out) {
72
+ const raw = out.match(/FINAL ANSWER:[\s\S]*$/)?.[0]?.trim();
73
+ if (!raw) return null;
74
+ const m = raw.match(/^FINAL ANSWER:\s*([\s\S]*?)\s*\n\s*CONSENSUS:\s*([^\n]*)\n\s*DISSENT:\s*([\s\S]*)$/);
75
+ if (!m) return { final_answer: raw.replace(/^FINAL ANSWER:\s*/, ''), consensus: 'unknown', dissent: { present: null, summary: 'Verdict could not be parsed; read the answer text in full.' } };
76
+ const consensus = ['full', 'majority', 'split'].find((c) => m[2].toLowerCase().includes(c)) ?? 'unknown';
77
+ const dissentText = m[3].trim();
78
+ const none = /^["'`]*none[."'`]*$/i.test(dissentText);
79
+ return { final_answer: m[1].trim(), consensus, dissent: { present: !none, summary: none ? 'none' : dissentText } };
80
+ }
81
+
82
+ // Each model's answer in the last round, from transcript.md ("## Round N -- provider").
83
+ function parsePositions(transcript) {
84
+ const sections = transcript.split(/\n(?=## )/).map((c) => c.match(/^## Round (\d+) -- (\S+)\n\n([\s\S]*)$/)).filter(Boolean)
85
+ .map((m) => ({ round: Number(m[1]), provider: m[2], body: m[3].trim() }));
86
+ const last = Math.max(0, ...sections.map((x) => x.round));
87
+ return {
88
+ roundsRan: last,
89
+ positions: sections.filter((x) => x.round === last).map((x) => ({
90
+ provider: x.provider,
91
+ text: x.body.replace(/\n*status: (approved|revise)\s*$/i, '').replace(/\n*POSITION:[^\n]*$/, '').trim(),
92
+ })),
93
+ };
94
+ }
95
+
96
+ function renderDebate(d) {
97
+ const label = d.consensus === 'full' ? 'FULL CONSENSUS' : d.consensus === 'majority' ? 'MAJORITY (some dissent)' : d.consensus === 'split' ? 'SPLIT' : 'CONSENSUS UNKNOWN';
98
+ const lines = [
99
+ `# Debate verdict: ${label}`,
100
+ `_${d.providers.join(', ')} | ${d.mode} mode | ${d.rounds_ran} round${d.rounds_ran === 1 ? '' : 's'}_`,
101
+ '', `**Answer:** ${d.final_answer}`, '',
102
+ ];
103
+ if (d.dissent.present === false) lines.push('**Dissent:** none', '');
104
+ else lines.push(`**Dissent (report this to the user, do not drop it when summarizing):** ${d.dissent.summary}`, '');
105
+ if (d.positions.length) {
106
+ lines.push('**Each model\'s final position:**');
107
+ for (const p of d.positions) lines.push('', `**${p.provider}:**`, ...p.text.split('\n').map((l) => `> ${l}`));
108
+ }
109
+ if (d.transcript) lines.push('', '<details><summary>Full transcript</summary>', '', d.transcript, '', '</details>');
110
+ return lines.join('\n');
111
+ }
112
+
66
113
  async function toolDebate(args, progress) {
67
114
  const question = typeof args.question === 'string' ? args.question.trim() : '';
68
115
  if (!question) return text('error: `question` is required', true);
69
116
  if (question.length > LIMITS.questionChars) return text(`error: question exceeds ${LIMITS.questionChars} characters`, true);
70
117
 
71
- const rounds = args.rounds ?? 2;
118
+ const mode = args.mode ?? 'quick';
119
+ if (!MODES[mode]) return text('error: mode must be "quick" or "thorough"', true);
120
+ const rounds = args.rounds ?? MODES[mode].rounds;
72
121
  if (!Number.isInteger(rounds) || rounds < 1 || rounds > LIMITS.rounds) return text(`error: rounds must be an integer 1..${LIMITS.rounds}`, true);
73
- const maxWords = args.max_words ?? 200;
122
+ const maxWords = args.max_words ?? MODES[mode].maxWords;
74
123
  if (!Number.isInteger(maxWords) || maxWords < 20 || maxWords > LIMITS.maxWords) return text(`error: max_words must be an integer 20..${LIMITS.maxWords}`, true);
124
+ if (args.include_transcript !== undefined && typeof args.include_transcript !== 'boolean') return text('error: include_transcript must be a boolean', true);
75
125
 
76
- const known = await configuredProviders();
126
+ // E3D_MCP_DEBATE_BIN swaps in another debate executable (used by tests); provider checks are then that binary's job.
127
+ const debateBin = process.env.E3D_MCP_DEBATE_BIN || join(ROOT, 'bin/e3d-debate');
128
+ const stubbed = Boolean(process.env.E3D_MCP_DEBATE_BIN);
129
+ const known = stubbed ? [] : await configuredProviders();
77
130
  let providers = args.providers;
78
131
  if (providers === undefined) {
79
- providers = known.filter((p) => p.available && ['claude', 'codex'].includes(p.name)).map((p) => p.name);
132
+ providers = stubbed ? ['claude', 'codex'] : known.filter((p) => p.available && ['claude', 'codex'].includes(p.name)).map((p) => p.name);
80
133
  }
81
134
  if (!Array.isArray(providers) || providers.some((p) => typeof p !== 'string' || !NAME_RE.test(p))) return text('error: providers must be an array of provider names', true);
82
135
  providers = [...new Set(providers)];
83
136
  if (providers.length < 2) return text('error: need at least 2 available providers to debate (call e3d_list_providers); only found: ' + (providers.join(', ') || 'none'), true);
84
137
  if (providers.length > LIMITS.providers) return text(`error: at most ${LIMITS.providers} providers per debate`, true);
85
- const unavailable = providers.filter((p) => !known.find((k) => k.name === p && k.available));
138
+ const unavailable = stubbed ? [] : providers.filter((p) => !known.find((k) => k.name === p && k.available));
86
139
  if (unavailable.length) return text(`error: provider(s) not available on this machine: ${unavailable.join(', ')}`, true);
87
140
 
88
141
  if (activeDebates >= LIMITS.concurrent) return text(`error: ${LIMITS.concurrent} debates already running; retry shortly`, true);
@@ -94,21 +147,23 @@ async function toolDebate(args, progress) {
94
147
  const cliArgs = ['--question-file', qfile, '--providers', providers.join(','), '--rounds', String(rounds),
95
148
  '--max-words', String(maxWords), '--out-dir', join(dir, 'out')];
96
149
  let step = 0;
97
- const r = await run(join(ROOT, 'bin/e3d-debate'), cliArgs, {
150
+ const r = await run(debateBin, cliArgs, {
98
151
  timeoutMs: LIMITS.timeoutMs,
99
152
  onStderr: (s) => { for (const m of s.matchAll(/^=== (.+?) ===$/gm)) progress?.(++step, m[1]); },
100
153
  });
101
154
  if (r.timedOut) return text(`error: debate timed out after ${LIMITS.timeoutMs / 1000}s`, true);
102
- const verdict = r.out.match(/FINAL ANSWER:[\s\S]*$/)?.[0]?.trim();
155
+ const verdict = parseVerdict(r.out);
103
156
  if (r.code !== 0 || !verdict) return text(`error: debate failed (exit ${r.code})\n${r.err.slice(-1500)}`, true);
104
157
 
105
- const transcript = { out: await readFile(join(dir, 'out', 'transcript.md'), 'utf8') };
106
- const body = [
107
- `# Debate verdict (${providers.join(', ')}; up to ${rounds} rounds)`,
108
- '', verdict, '',
109
- '<details><summary>Full transcript</summary>', '', transcript.out.trim(), '', '</details>',
110
- ].join('\n');
111
- return text(body);
158
+ const transcript = await readFile(join(dir, 'out', 'transcript.md'), 'utf8').catch(() => '');
159
+ const { roundsRan, positions } = parsePositions(transcript);
160
+ const result = {
161
+ ...verdict,
162
+ mode, providers, rounds_requested: rounds, rounds_ran: roundsRan || rounds,
163
+ positions,
164
+ ...(args.include_transcript ? { transcript: transcript.trim() } : {}),
165
+ };
166
+ return { content: [{ type: 'text', text: renderDebate(result) }], structuredContent: result };
112
167
  } finally {
113
168
  activeDebates--;
114
169
  rm(dir, { recursive: true, force: true }).catch(() => {});
@@ -157,18 +212,39 @@ async function toolIdeasList(args) {
157
212
  const TOOLS = [
158
213
  {
159
214
  name: 'e3d_debate',
160
- description: 'Get a multi-model second opinion on a decision or design question. Several independent AI models answer, critique each other across rounds, and converge; returns a decisive final answer, a consensus level (full/majority/split), and any named dissent, plus the full transcript. Use for architectural choices, tradeoffs, and contestable judgment calls -- not for lookups or questions with an obvious answer. Slow (minutes) and spends model credits.',
215
+ description: 'Get a multi-model second opinion on a decision or design question. Independent AI models answer, and a synthesizer returns a final answer, a consensus level (full/majority/split), and any dissent, plus each model\'s final position. ALWAYS report the dissent and consensus level to the user -- the disagreement is the point; never present a split verdict as settled. Write a self-contained question: the models cannot see your code. mode "quick" (default): one round, usually well under a minute. mode "thorough": models also critique each other, a few minutes. Use for architectural choices, tradeoffs, and contestable judgment calls -- not for lookups or questions with an obvious answer. Spends the user\'s model credits.',
161
216
  inputSchema: {
162
217
  type: 'object',
163
218
  properties: {
164
219
  question: { type: 'string', description: 'The decision or question, with enough context for models that cannot see your code.' },
220
+ mode: { type: 'string', enum: ['quick', 'thorough'], description: 'quick (default): 1 round, short answers, no cross-critique. thorough: 2 rounds where models see and rebut each other.' },
165
221
  providers: { type: 'array', items: { type: 'string' }, description: 'Provider names (see e3d_list_providers). Default: claude + codex.' },
166
- rounds: { type: 'integer', minimum: 1, maximum: LIMITS.rounds, description: 'Max debate rounds; stops early on convergence. Default 2.' },
167
- max_words: { type: 'integer', minimum: 20, maximum: LIMITS.maxWords, description: 'Per-provider per-round answer cap. Default 200.' },
222
+ rounds: { type: 'integer', minimum: 1, maximum: LIMITS.rounds, description: 'Override the mode\'s round count. Stops early on convergence.' },
223
+ max_words: { type: 'integer', minimum: 20, maximum: LIMITS.maxWords, description: 'Override the mode\'s per-provider per-round answer cap.' },
224
+ include_transcript: { type: 'boolean', description: 'Also return the full transcript (large). Default false.' },
168
225
  },
169
226
  required: ['question'],
170
227
  additionalProperties: false,
171
228
  },
229
+ outputSchema: {
230
+ type: 'object',
231
+ properties: {
232
+ final_answer: { type: 'string' },
233
+ consensus: { type: 'string', enum: ['full', 'majority', 'split', 'unknown'] },
234
+ dissent: {
235
+ type: 'object',
236
+ properties: { present: { type: ['boolean', 'null'] }, summary: { type: 'string', description: '"none" when there is no dissent.' } },
237
+ required: ['present', 'summary'],
238
+ },
239
+ positions: { type: 'array', items: { type: 'object', properties: { provider: { type: 'string' }, text: { type: 'string' } }, required: ['provider', 'text'] } },
240
+ mode: { type: 'string' },
241
+ providers: { type: 'array', items: { type: 'string' } },
242
+ rounds_requested: { type: 'integer' },
243
+ rounds_ran: { type: 'integer' },
244
+ transcript: { type: 'string' },
245
+ },
246
+ required: ['final_answer', 'consensus', 'dissent', 'positions', 'mode', 'providers', 'rounds_ran'],
247
+ },
172
248
  annotations: { title: 'Multi-model debate', readOnlyHint: true, openWorldHint: true, idempotentHint: false },
173
249
  },
174
250
  {
package/lib/web/render.js CHANGED
@@ -71,7 +71,6 @@ export function renderIdeasList(rows, filters, repoAliases) {
71
71
  .map(
72
72
  (r) => `<tr>
73
73
  <td>${escapeHtml(r.repoAlias)}</td>
74
- <td><code>${escapeHtml(r.idea_id)}</code></td>
75
74
  <td><a href="/ideas/${encodeURIComponent(r.repoAlias)}/${encodeURIComponent(r.idea_id)}">${escapeHtml(r.title)}</a></td>
76
75
  <td>${statusBadge(r.status)}</td>
77
76
  <td>${escapeHtml(r.scores?.revenue ?? '-')} / ${escapeHtml(r.scores?.attraction ?? '-')} / ${escapeHtml(r.scores?.retention ?? '-')}</td>
@@ -88,8 +87,8 @@ export function renderIdeasList(rows, filters, repoAliases) {
88
87
  <button type="submit">Filter</button>
89
88
  </form>
90
89
  <table>
91
- <thead><tr><th>Workspace</th><th>ID</th><th>Title</th><th>Status</th><th>Rev/Attr/Ret</th><th>Effort</th><th>Updated</th></tr></thead>
92
- <tbody>${tableRows || '<tr><td colspan="7">No ideas found.</td></tr>'}</tbody>
90
+ <thead><tr><th>Workspace</th><th>Title</th><th>Status</th><th>Rev/Attr/Ret</th><th>Effort</th><th>Updated</th></tr></thead>
91
+ <tbody>${tableRows || '<tr><td colspan="6">No ideas found.</td></tr>'}</tbody>
93
92
  </table>`;
94
93
  }
95
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "e3d-pilot",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Multi-model debate and decision provenance for AI coding work. Includes an MCP server (e3d-mcp) for GitHub Copilot, Claude Code, Cursor and other MCP clients.",
5
5
  "type": "module",
6
6
  "license": "MIT",