quacktionable 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.
Files changed (40) hide show
  1. package/README.md +116 -0
  2. package/bin/quack +2 -0
  3. package/bin/quacktionable +2 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +332 -0
  6. package/dist/copilot/copilotCli.d.ts +11 -0
  7. package/dist/copilot/copilotCli.js +93 -0
  8. package/dist/core/errors.d.ts +9 -0
  9. package/dist/core/errors.js +24 -0
  10. package/dist/core/types.d.ts +63 -0
  11. package/dist/core/types.js +2 -0
  12. package/dist/input/collectRepoContext.d.ts +1 -0
  13. package/dist/input/collectRepoContext.js +23 -0
  14. package/dist/input/parseLogs.d.ts +4 -0
  15. package/dist/input/parseLogs.js +16 -0
  16. package/dist/input/parsePrompt.d.ts +2 -0
  17. package/dist/input/parsePrompt.js +14 -0
  18. package/dist/llm/ollamaClient.d.ts +1 -0
  19. package/dist/llm/ollamaClient.js +35 -0
  20. package/dist/llm/prompts.d.ts +15 -0
  21. package/dist/llm/prompts.js +31 -0
  22. package/dist/llm/validateModelOutput.d.ts +2 -0
  23. package/dist/llm/validateModelOutput.js +39 -0
  24. package/dist/output/jsonRenderer.d.ts +2 -0
  25. package/dist/output/jsonRenderer.js +8 -0
  26. package/dist/output/schema.d.ts +7 -0
  27. package/dist/output/schema.js +11 -0
  28. package/dist/output/terminalRenderer.d.ts +2 -0
  29. package/dist/output/terminalRenderer.js +38 -0
  30. package/dist/tribunal/aggregateVotes.d.ts +2 -0
  31. package/dist/tribunal/aggregateVotes.js +33 -0
  32. package/dist/tribunal/confidence.d.ts +2 -0
  33. package/dist/tribunal/confidence.js +13 -0
  34. package/dist/tribunal/defaultDucks.d.ts +2 -0
  35. package/dist/tribunal/defaultDucks.js +23 -0
  36. package/dist/tribunal/runTribunal.d.ts +2 -0
  37. package/dist/tribunal/runTribunal.js +69 -0
  38. package/dist/tribunal/scoring.d.ts +3 -0
  39. package/dist/tribunal/scoring.js +21 -0
  40. package/package.json +56 -0
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+
2
+ ![quacktionable banner](assets/quack.png)
3
+
4
+ `quacktionable` is a playful terminal tool that helps you explain a bug, survive cross-examination by judge ducks, and get a voted root-cause verdict.
5
+
6
+ It is built around **GitHub Copilot CLI** as the reasoning engine.
7
+
8
+ ## Why this is useful
9
+
10
+ Most debugging stalls because we lock onto our first theory too early.
11
+
12
+ `quacktionable` forces structured skepticism:
13
+ - take your bug narrative,
14
+ - challenge it from multiple technical personas,
15
+ - and produce a final, confidence-scored quackdict.
16
+
17
+ ## Features
18
+
19
+ - Interactive intake mode with branded splash UI: `quack report`
20
+ - Fast sample run: `quack analyze --example`
21
+ - Flexible inputs: inline text, bug file, logs, and code paths
22
+ - Live terminal progress animation while the tribunal is thinking
23
+ - Terminal-friendly and JSON output modes
24
+ - Copilot CLI health check with `quack doctor`
25
+
26
+ ## Requirements
27
+
28
+ - Node.js `>=18.18`
29
+ - GitHub Copilot CLI installed and authenticated
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ npm install -g quacktionable
35
+ ```
36
+
37
+ If you prefer `npx` (no global install):
38
+
39
+ ```bash
40
+ npx quacktionable doctor
41
+ ```
42
+
43
+ ## Quick start
44
+
45
+ ```bash
46
+ quack doctor
47
+ quack report
48
+ ```
49
+
50
+ Or run a one-command demo:
51
+
52
+ ```bash
53
+ quack analyze --example
54
+ ```
55
+
56
+ ## Local development
57
+
58
+ ```bash
59
+ npm install
60
+ npm run build
61
+ npm link
62
+ ```
63
+
64
+ ## Command reference
65
+
66
+ - `quack doctor` — verifies Copilot CLI availability and prompt roundtrip
67
+ - `quack report` — interactive guided bug intake + analysis
68
+ - `quack analyze --example` — run with built-in sample bug/logs
69
+ - `quack analyze --bug "..." --log ... --path "a.ts,b.ts"` — explicit inputs
70
+ - `quack analyze --bug-file bug.txt --log app.log` — file-first workflow
71
+ - `quack analyze --json` — JSON output for scripts/automation
72
+
73
+ ### Analyze flags
74
+
75
+ - `--example`: auto-loads `examples/bug.txt` and `examples/logs.txt`
76
+ - `--bug`: bug narrative text
77
+ - `--bug-file`: path to bug narrative file
78
+ - `--log`: raw log text or a log file path
79
+ - `--paths` / `--path`: comma-separated code paths for extra context
80
+ - `--rounds`: tribunal rounds per duck (1-5, default 2)
81
+ - `--json`: emit machine-readable JSON only
82
+
83
+ ## What users see in terminal
84
+
85
+ ```text
86
+ 🦆⚖️ Rubber Duck Tribunal (Court is now in session)
87
+ 🎭 Opening statement: no bug survives cross-examination.
88
+ 🆔 Session: 5a077c56-b0fe-4f18-a26e-1a7408d56273
89
+ 🤖 Engine: gpt-5
90
+
91
+ 📝 Bug Tale
92
+ - 🐛 Users intermittently get a 500 error...
93
+
94
+ 🗳️ Judge Duck Votes
95
+ - 🦆 Detective Quackson: ...
96
+
97
+ 🏛️ Final Quackdict
98
+ - 🎯 root cause: ...
99
+ - 🔥 confidence: 92%
100
+ ```
101
+
102
+ ## Architecture
103
+
104
+ - Input parsing (`--bug`, `--bug-file`, stdin, `--log`, `--path[s]`)
105
+ - Evidence bundling + signal extraction from logs
106
+ - Copilot CLI cross-exam prompt + per-duck judgment prompts
107
+ - Vote aggregation and confidence scoring
108
+ - Pretty terminal renderer + JSON renderer
109
+
110
+ ## Development
111
+
112
+ ```bash
113
+ npm run lint
114
+ npm run build
115
+ npm run test
116
+ ```
package/bin/quack ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('../dist/cli.js');
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('../dist/cli.js');
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,332 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const node_fs_1 = require("node:fs");
8
+ const promises_1 = require("node:fs/promises");
9
+ const node_process_1 = __importDefault(require("node:process"));
10
+ const promises_2 = require("node:readline/promises");
11
+ const gradient_string_1 = __importDefault(require("gradient-string"));
12
+ const errors_1 = require("./core/errors");
13
+ const copilotCli_1 = require("./copilot/copilotCli");
14
+ const parsePrompt_1 = require("./input/parsePrompt");
15
+ const collectRepoContext_1 = require("./input/collectRepoContext");
16
+ const jsonRenderer_1 = require("./output/jsonRenderer");
17
+ const terminalRenderer_1 = require("./output/terminalRenderer");
18
+ const defaultDucks_1 = require("./tribunal/defaultDucks");
19
+ const runTribunal_1 = require("./tribunal/runTribunal");
20
+ const DEFAULT_BUG_FILE = 'examples/bug.txt';
21
+ const DEFAULT_LOG_FILE = 'examples/logs.txt';
22
+ const DEFAULT_EXAMPLE_PATHS = ['src/cli.ts', 'src/tribunal/runTribunal.ts'];
23
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
24
+ const hasFlag = (args, flag) => {
25
+ return args.includes(flag);
26
+ };
27
+ const getFlagValue = (args, flag) => {
28
+ const index = args.indexOf(flag);
29
+ if (index === -1 || index + 1 >= args.length) {
30
+ return undefined;
31
+ }
32
+ return args[index + 1];
33
+ };
34
+ const parsePaths = (raw) => {
35
+ if (!raw) {
36
+ return [];
37
+ }
38
+ return raw
39
+ .split(',')
40
+ .map((item) => item.trim())
41
+ .filter(Boolean);
42
+ };
43
+ const readTextFileIfExists = async (path) => {
44
+ if (!(0, node_fs_1.existsSync)(path)) {
45
+ return undefined;
46
+ }
47
+ return (0, promises_1.readFile)(path, 'utf8');
48
+ };
49
+ const readLogInput = async (raw) => {
50
+ if (!raw) {
51
+ return undefined;
52
+ }
53
+ if ((0, node_fs_1.existsSync)(raw)) {
54
+ return (0, promises_1.readFile)(raw, 'utf8');
55
+ }
56
+ return raw;
57
+ };
58
+ const readStdinText = async () => {
59
+ if (node_process_1.default.stdin.isTTY) {
60
+ return undefined;
61
+ }
62
+ const chunks = [];
63
+ for await (const chunk of node_process_1.default.stdin) {
64
+ chunks.push(Buffer.from(chunk));
65
+ }
66
+ const text = Buffer.concat(chunks).toString('utf8').trim();
67
+ return text.length > 0 ? text : undefined;
68
+ };
69
+ const usage = () => {
70
+ return [
71
+ 'Usage:',
72
+ ' quack doctor',
73
+ ' quack report',
74
+ ' quack analyze --example',
75
+ ' quack analyze --bug "..." [--log "..."] [--paths "a.ts,b.ts"] [--json] [--rounds 2]',
76
+ ' quack analyze --bug-file notes.txt [--log examples/logs.txt]',
77
+ '',
78
+ 'Notes:',
79
+ ' quack report starts an interactive guided bug intake.',
80
+ ' --example auto-loads examples/bug.txt and examples/logs.txt.',
81
+ ' If --bug is omitted and examples/bug.txt exists, it is used automatically.',
82
+ ' --bug-file reads bug narrative from a file.',
83
+ ' --log accepts raw text or a file path.',
84
+ ' --paths accepts comma-separated file paths.',
85
+ ' Copilot CLI is required at runtime.',
86
+ ' Keep calm and let the ducks do the judging.',
87
+ ].join('\n');
88
+ };
89
+ const parseAnalyzeInput = async (args) => {
90
+ const useExample = hasFlag(args, '--example');
91
+ const bugFilePath = getFlagValue(args, '--bug-file');
92
+ const bugFromFlag = getFlagValue(args, '--bug');
93
+ const bugFromFile = bugFilePath ? await readTextFileIfExists(bugFilePath) : undefined;
94
+ const bugFromExample = useExample ? await readTextFileIfExists(DEFAULT_BUG_FILE) : undefined;
95
+ const bugFromDefault = bugFromFlag || bugFromFile || bugFromExample ? undefined : await readTextFileIfExists(DEFAULT_BUG_FILE);
96
+ const bugFromStdin = await readStdinText();
97
+ const bug = bugFromFlag ?? bugFromFile ?? bugFromStdin ?? bugFromExample ?? bugFromDefault;
98
+ if (!bug) {
99
+ throw new errors_1.QuacktionableError('Missing bug description. Use --bug, --bug-file, --example, or pipe text via stdin.');
100
+ }
101
+ const roundsRaw = getFlagValue(args, '--rounds') ?? '2';
102
+ const rounds = Number(roundsRaw);
103
+ if (!Number.isInteger(rounds) || rounds < 1 || rounds > 5) {
104
+ throw new errors_1.QuacktionableError('--rounds must be an integer between 1 and 5.');
105
+ }
106
+ const logFromFlag = getFlagValue(args, '--log');
107
+ const pathsRaw = getFlagValue(args, '--paths') ?? getFlagValue(args, '--path');
108
+ const parsedPaths = parsePaths(pathsRaw);
109
+ const logFromExample = useExample && !logFromFlag ? await readTextFileIfExists(DEFAULT_LOG_FILE) : undefined;
110
+ const defaultPaths = useExample ? DEFAULT_EXAMPLE_PATHS : [];
111
+ return {
112
+ bug,
113
+ logText: (await readLogInput(logFromFlag)) ?? logFromExample,
114
+ paths: parsedPaths.length > 0 ? parsedPaths : defaultPaths,
115
+ jsonOutput: hasFlag(args, '--json'),
116
+ rounds,
117
+ };
118
+ };
119
+ const normalizeYes = (value) => {
120
+ return /^(y|yes|true|1)$/i.test(value.trim());
121
+ };
122
+ const ansi = {
123
+ reset: '\u001b[0m',
124
+ bold: '\u001b[1m',
125
+ dim: '\u001b[2m',
126
+ cyan: '\u001b[36m',
127
+ magenta: '\u001b[35m',
128
+ yellow: '\u001b[33m',
129
+ green: '\u001b[32m',
130
+ };
131
+ const colorize = (text, color, weight = '') => {
132
+ return `${weight}${color}${text}${ansi.reset}`;
133
+ };
134
+ const quackGradient = (0, gradient_string_1.default)(['#22d3ee', '#8b5cf6', '#f472b6']);
135
+ const renderInteractiveSplash = () => {
136
+ const logoRaw = [
137
+ ' ██████╗ ██╗ ██╗ █████╗ ██████╗██╗ ██╗',
138
+ '██╔═══██╗██║ ██║██╔══██╗██╔════╝██║ ██╔╝',
139
+ '██║ ██║██║ ██║███████║██║ █████╔╝ ',
140
+ '██║▄▄ ██║██║ ██║██╔══██║██║ ██╔═██╗ ',
141
+ '╚██████╔╝╚██████╔╝██║ ██║╚██████╗██║ ██╗',
142
+ ' ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝',
143
+ ].join('\n');
144
+ const logo = quackGradient.multiline(logoRaw);
145
+ const subtitleBox = [
146
+ '┌───────────────────────────────────────────────────────────────┐',
147
+ '│ 🦆 Rubber Duck Tribunal · Interactive Intake Console │',
148
+ '│ Explain the bug. The duck bench handles the drama. │',
149
+ '└───────────────────────────────────────────────────────────────┘',
150
+ ].join('\n');
151
+ const statusLines = [
152
+ `${colorize('⚖️ Court mode', ansi.magenta, ansi.bold)}: ${colorize('ENABLED', ansi.green, ansi.bold)}`,
153
+ `${colorize('🔍 Evidence mode', ansi.magenta, ansi.bold)}: ${colorize('TRACKING', ansi.green, ansi.bold)}`,
154
+ `${colorize('🎭 Vibes', ansi.magenta, ansi.bold)}: ${colorize('dramatic, but factual', ansi.yellow)}`,
155
+ ].join('\n');
156
+ return [
157
+ '',
158
+ logo,
159
+ '',
160
+ subtitleBox,
161
+ '',
162
+ statusLines,
163
+ '',
164
+ ].join('\n');
165
+ };
166
+ const createProgressReporter = (enabled) => {
167
+ if (!enabled) {
168
+ return {
169
+ update: () => undefined,
170
+ success: () => undefined,
171
+ fail: () => undefined,
172
+ };
173
+ }
174
+ if (!node_process_1.default.stderr.isTTY) {
175
+ return {
176
+ update: (message) => {
177
+ node_process_1.default.stderr.write(`⏳ ${message}\n`);
178
+ },
179
+ success: (message) => {
180
+ node_process_1.default.stderr.write(`✅ ${message}\n`);
181
+ },
182
+ fail: (message) => {
183
+ node_process_1.default.stderr.write(`❌ ${message}\n`);
184
+ },
185
+ };
186
+ }
187
+ let active = false;
188
+ let frameIndex = 0;
189
+ let currentMessage = 'Warming up the duck bench...';
190
+ let intervalId;
191
+ const render = () => {
192
+ const frame = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length];
193
+ frameIndex += 1;
194
+ node_process_1.default.stderr.write(`\r${frame} ${currentMessage} `);
195
+ };
196
+ const start = () => {
197
+ if (active) {
198
+ return;
199
+ }
200
+ active = true;
201
+ render();
202
+ intervalId = setInterval(render, 80);
203
+ };
204
+ const stop = (icon, message) => {
205
+ if (intervalId) {
206
+ clearInterval(intervalId);
207
+ intervalId = undefined;
208
+ }
209
+ if (active) {
210
+ node_process_1.default.stderr.write(`\r${icon} ${message} \n`);
211
+ active = false;
212
+ return;
213
+ }
214
+ node_process_1.default.stderr.write(`${icon} ${message}\n`);
215
+ };
216
+ return {
217
+ update: (message) => {
218
+ currentMessage = message;
219
+ start();
220
+ },
221
+ success: (message) => {
222
+ stop('✅', message);
223
+ },
224
+ fail: (message) => {
225
+ stop('❌', message);
226
+ },
227
+ };
228
+ };
229
+ const runAnalyzeWithInput = async (input) => {
230
+ const showProgress = !input.jsonOutput;
231
+ const reporter = createProgressReporter(showProgress);
232
+ const progress = reporter.update;
233
+ try {
234
+ progress('Gathering bug clues...');
235
+ const repoContext = await (0, collectRepoContext_1.collectRepoContext)(input.paths);
236
+ const report = {
237
+ summary: input.bug,
238
+ logs: input.logText,
239
+ filePaths: input.paths,
240
+ repoContext,
241
+ };
242
+ progress('Bundling evidence for the duck tribunal...');
243
+ const evidence = (0, parsePrompt_1.buildEvidenceBundle)(report);
244
+ const session = await (0, runTribunal_1.runTribunal)(evidence, report, {
245
+ ducks: defaultDucks_1.DEFAULT_DUCKS,
246
+ rounds: input.rounds,
247
+ requireCopilotCli: true,
248
+ onProgress: progress,
249
+ });
250
+ progress('Printing final quackdict...');
251
+ const output = input.jsonOutput ? (0, jsonRenderer_1.renderJson)(session) : (0, terminalRenderer_1.renderTerminal)(session);
252
+ node_process_1.default.stdout.write(`${output}\n`);
253
+ reporter.success('Tribunal complete. Verdict delivered.');
254
+ }
255
+ catch (error) {
256
+ reporter.fail('Tribunal interrupted before final verdict.');
257
+ throw error;
258
+ }
259
+ };
260
+ const runAnalyze = async (args) => {
261
+ const input = await parseAnalyzeInput(args);
262
+ await runAnalyzeWithInput(input);
263
+ };
264
+ const runReport = async () => {
265
+ const rl = (0, promises_2.createInterface)({
266
+ input: node_process_1.default.stdin,
267
+ output: node_process_1.default.stdout,
268
+ });
269
+ node_process_1.default.stdout.write(`${renderInteractiveSplash()}\n\n`);
270
+ node_process_1.default.stdout.write('🦆 Welcome, debugger. Let us file this bug with dramatic flair and technical rigor.\n\n');
271
+ try {
272
+ let bug = '';
273
+ while (!bug.trim()) {
274
+ bug = await rl.question('1) 📝 Describe the bug in plain language: ');
275
+ if (!bug.trim()) {
276
+ node_process_1.default.stdout.write(' 🕵️ The ducks need at least one clue. Please add a short description.\n');
277
+ }
278
+ }
279
+ const logInput = await rl.question('2) 📜 Optional log path or raw log line (enter to skip): ');
280
+ const pathsInput = await rl.question('3) 📂 Optional code paths (comma-separated, enter to skip): ');
281
+ const roundsInput = await rl.question('4) 🔁 Cross-exam rounds per duck [2]: ');
282
+ const jsonInput = await rl.question('5) 🧾 Output JSON only? [y/N]: ');
283
+ const roundsRaw = roundsInput.trim() || '2';
284
+ const rounds = Number(roundsRaw);
285
+ if (!Number.isInteger(rounds) || rounds < 1 || rounds > 5) {
286
+ throw new errors_1.QuacktionableError('Rounds must be an integer between 1 and 5.');
287
+ }
288
+ const input = {
289
+ bug: bug.trim(),
290
+ logText: await readLogInput(logInput.trim() || undefined),
291
+ paths: parsePaths(pathsInput.trim() || undefined),
292
+ jsonOutput: normalizeYes(jsonInput),
293
+ rounds,
294
+ };
295
+ node_process_1.default.stdout.write('\n🚀 Launching tribunal proceedings...\n\n');
296
+ await runAnalyzeWithInput(input);
297
+ }
298
+ finally {
299
+ rl.close();
300
+ }
301
+ };
302
+ const runDoctor = async () => {
303
+ const result = await (0, copilotCli_1.runCopilotDoctor)();
304
+ node_process_1.default.stdout.write('quack doctor 🩺🦆\n');
305
+ node_process_1.default.stdout.write(`- cliAvailable: ${result.cliAvailable}\n`);
306
+ node_process_1.default.stdout.write(`- promptRoundTripOk: ${result.promptRoundTripOk}\n`);
307
+ node_process_1.default.stdout.write(`- details: ${result.details}\n`);
308
+ };
309
+ const main = async () => {
310
+ const [, , command, ...args] = node_process_1.default.argv;
311
+ if (!command || command === '--help' || command === '-h') {
312
+ node_process_1.default.stdout.write(`${usage()}\n`);
313
+ return;
314
+ }
315
+ if (command === 'doctor') {
316
+ await runDoctor();
317
+ return;
318
+ }
319
+ if (command === 'report') {
320
+ await runReport();
321
+ return;
322
+ }
323
+ if (command !== 'analyze') {
324
+ throw new errors_1.QuacktionableError(`Unknown command: ${command}`);
325
+ }
326
+ await runAnalyze(args);
327
+ };
328
+ main().catch((error) => {
329
+ const message = error instanceof Error ? error.message : String(error);
330
+ node_process_1.default.stderr.write(`quacktionable error: ${message}\n`);
331
+ node_process_1.default.exitCode = 1;
332
+ });
@@ -0,0 +1,11 @@
1
+ import type { EvidenceBundle } from '../core/types';
2
+ export declare const assertCopilotCliAvailable: () => void;
3
+ export declare const runCopilotCrossExam: (evidence: EvidenceBundle) => Promise<string>;
4
+ export declare const runCopilotDuckJudgment: (prompt: string) => Promise<string>;
5
+ export interface CopilotDoctorResult {
6
+ cliAvailable: boolean;
7
+ promptRoundTripOk: boolean;
8
+ details: string;
9
+ }
10
+ export declare const runCopilotDoctor: () => Promise<CopilotDoctorResult>;
11
+ export declare const resolveCopilotModelLabel: () => Promise<string>;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveCopilotModelLabel = exports.runCopilotDoctor = exports.runCopilotDuckJudgment = exports.runCopilotCrossExam = exports.assertCopilotCliAvailable = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const promises_1 = require("node:fs/promises");
6
+ const node_os_1 = require("node:os");
7
+ const node_path_1 = require("node:path");
8
+ const node_util_1 = require("node:util");
9
+ const errors_1 = require("../core/errors");
10
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
11
+ const getCopilotCommand = () => {
12
+ return process.env.COPILOT_CLI_COMMAND ?? 'copilot';
13
+ };
14
+ const assertCopilotCliAvailable = () => {
15
+ const command = getCopilotCommand();
16
+ const check = (0, node_child_process_1.spawnSync)(command, ['--version'], { stdio: 'ignore' });
17
+ if (check.status !== 0) {
18
+ throw new errors_1.MissingDependencyError('GitHub Copilot CLI', `Install it and ensure '${command}' is available on PATH.`);
19
+ }
20
+ };
21
+ exports.assertCopilotCliAvailable = assertCopilotCliAvailable;
22
+ const runCopilotPrompt = async (prompt) => {
23
+ const command = getCopilotCommand();
24
+ try {
25
+ const { stdout } = await execFileAsync(command, ['-p', prompt], {
26
+ maxBuffer: 1024 * 1024,
27
+ });
28
+ const text = stdout.trim();
29
+ return text || 'No output returned by Copilot CLI.';
30
+ }
31
+ catch (error) {
32
+ const message = error instanceof Error ? error.message : String(error);
33
+ throw new errors_1.QuacktionableError(`Copilot CLI execution failed: ${message}`);
34
+ }
35
+ };
36
+ const runCopilotCrossExam = async (evidence) => {
37
+ const prompt = [
38
+ 'You are an external tribunal observer with sharp wit and sharp debugging instincts.',
39
+ 'Given bug evidence, provide up to 5 high-impact cross-exam questions and likely weak assumptions.',
40
+ 'Format as concise bullet points with light humor, but keep technical accuracy strict.',
41
+ '',
42
+ `Narrative: ${evidence.narrative || '<none>'}`,
43
+ `Logs: ${evidence.logs || '<none>'}`,
44
+ `Repo context: ${evidence.repoContext || '<none>'}`,
45
+ ].join('\n');
46
+ return runCopilotPrompt(prompt);
47
+ };
48
+ exports.runCopilotCrossExam = runCopilotCrossExam;
49
+ const runCopilotDuckJudgment = async (prompt) => {
50
+ return runCopilotPrompt(prompt);
51
+ };
52
+ exports.runCopilotDuckJudgment = runCopilotDuckJudgment;
53
+ const runCopilotDoctor = async () => {
54
+ (0, exports.assertCopilotCliAvailable)();
55
+ const probePrompt = [
56
+ 'Health-check probe.',
57
+ 'Reply with exactly one word: QUACK_OK',
58
+ ].join('\n');
59
+ const output = await runCopilotPrompt(probePrompt);
60
+ const roundTripOk = /\bQUACK_OK\b/i.test(output);
61
+ if (!roundTripOk) {
62
+ throw new errors_1.QuacktionableError(`Copilot CLI responded, but health probe token was missing. Received: ${output.slice(0, 120)}`);
63
+ }
64
+ return {
65
+ cliAvailable: true,
66
+ promptRoundTripOk: true,
67
+ details: 'Copilot CLI is online and beak-to-beak communication is healthy.',
68
+ };
69
+ };
70
+ exports.runCopilotDoctor = runCopilotDoctor;
71
+ const readCopilotConfig = async () => {
72
+ const configDir = process.env.COPILOT_CONFIG_DIR ?? (0, node_path_1.join)((0, node_os_1.homedir)(), '.copilot');
73
+ const configPath = (0, node_path_1.join)(configDir, 'config.json');
74
+ try {
75
+ const raw = await (0, promises_1.readFile)(configPath, 'utf8');
76
+ const parsed = JSON.parse(raw);
77
+ return parsed;
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ };
83
+ const resolveCopilotModelLabel = async () => {
84
+ if (process.env.QUACK_MODEL_LABEL) {
85
+ return process.env.QUACK_MODEL_LABEL;
86
+ }
87
+ const config = await readCopilotConfig();
88
+ if (config?.model) {
89
+ return config.model;
90
+ }
91
+ return 'default (configured by GitHub Copilot CLI)';
92
+ };
93
+ exports.resolveCopilotModelLabel = resolveCopilotModelLabel;
@@ -0,0 +1,9 @@
1
+ export declare class QuacktionableError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare class MissingDependencyError extends QuacktionableError {
5
+ constructor(dependencyName: string, hint: string);
6
+ }
7
+ export declare class InvalidModelOutputError extends QuacktionableError {
8
+ constructor(message: string);
9
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InvalidModelOutputError = exports.MissingDependencyError = exports.QuacktionableError = void 0;
4
+ class QuacktionableError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = 'QuacktionableError';
8
+ }
9
+ }
10
+ exports.QuacktionableError = QuacktionableError;
11
+ class MissingDependencyError extends QuacktionableError {
12
+ constructor(dependencyName, hint) {
13
+ super(`${dependencyName} is required: ${hint}`);
14
+ this.name = 'MissingDependencyError';
15
+ }
16
+ }
17
+ exports.MissingDependencyError = MissingDependencyError;
18
+ class InvalidModelOutputError extends QuacktionableError {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = 'InvalidModelOutputError';
22
+ }
23
+ }
24
+ exports.InvalidModelOutputError = InvalidModelOutputError;
@@ -0,0 +1,63 @@
1
+ export interface BugReport {
2
+ summary: string;
3
+ logs?: string;
4
+ filePaths: string[];
5
+ repoContext?: string;
6
+ }
7
+ export interface EvidenceBundle {
8
+ narrative: string;
9
+ logs: string;
10
+ repoContext: string;
11
+ extractedSignals: string[];
12
+ }
13
+ export interface DuckPersona {
14
+ id: string;
15
+ name: string;
16
+ style: string;
17
+ focus: string;
18
+ }
19
+ export interface CrossExamRound {
20
+ round: number;
21
+ question: string;
22
+ answer: string;
23
+ concern: string;
24
+ }
25
+ export interface Vote {
26
+ duckId: string;
27
+ duckName: string;
28
+ rootCause: string;
29
+ rationale: string;
30
+ confidence: number;
31
+ evidenceRefs: string[];
32
+ }
33
+ export interface Verdict {
34
+ rootCause: string;
35
+ confidence: number;
36
+ dissentingCauses: string[];
37
+ voteBreakdown: Vote[];
38
+ summary: string;
39
+ insufficientEvidence?: boolean;
40
+ }
41
+ export interface TribunalSession {
42
+ id: string;
43
+ createdAt: string;
44
+ model: string;
45
+ bugReport: BugReport;
46
+ evidence: EvidenceBundle;
47
+ roundsByDuck: Record<string, CrossExamRound[]>;
48
+ votes: Vote[];
49
+ verdict: Verdict;
50
+ }
51
+ export interface TribunalOptions {
52
+ ducks: DuckPersona[];
53
+ rounds: number;
54
+ requireCopilotCli: boolean;
55
+ onProgress?: (message: string) => void;
56
+ }
57
+ export interface AnalyzeInput {
58
+ bug: string;
59
+ logText?: string;
60
+ paths: string[];
61
+ jsonOutput: boolean;
62
+ rounds: number;
63
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export declare function collectRepoContext(paths: string[]): Promise<string>;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectRepoContext = collectRepoContext;
4
+ const promises_1 = require("node:fs/promises");
5
+ const MAX_FILE_CHARS = 8_000;
6
+ async function collectRepoContext(paths) {
7
+ if (paths.length === 0) {
8
+ return '';
9
+ }
10
+ const chunks = [];
11
+ for (const path of paths) {
12
+ try {
13
+ const content = await (0, promises_1.readFile)(path, 'utf8');
14
+ const trimmed = content.length > MAX_FILE_CHARS ? `${content.slice(0, MAX_FILE_CHARS)}\n...<truncated>` : content;
15
+ chunks.push(`### File: ${path}\n${trimmed}`);
16
+ }
17
+ catch (error) {
18
+ const message = error instanceof Error ? error.message : String(error);
19
+ chunks.push(`### File: ${path}\n<unreadable: ${message}>`);
20
+ }
21
+ }
22
+ return chunks.join('\n\n');
23
+ }
@@ -0,0 +1,4 @@
1
+ export declare const parseLogs: (logText?: string) => {
2
+ cleaned: string;
3
+ signals: string[];
4
+ };
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseLogs = void 0;
4
+ const parseLogs = (logText) => {
5
+ if (!logText) {
6
+ return { cleaned: '', signals: [] };
7
+ }
8
+ const cleaned = logText.trim();
9
+ const lines = cleaned.split(/\r?\n/);
10
+ const signals = lines
11
+ .filter((line) => /(error|exception|trace|failed|timeout|refused)/i.test(line))
12
+ .slice(0, 12)
13
+ .map((line) => line.trim());
14
+ return { cleaned, signals };
15
+ };
16
+ exports.parseLogs = parseLogs;
@@ -0,0 +1,2 @@
1
+ import type { BugReport, EvidenceBundle } from '../core/types';
2
+ export declare const buildEvidenceBundle: (report: BugReport) => EvidenceBundle;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildEvidenceBundle = void 0;
4
+ const parseLogs_1 = require("./parseLogs");
5
+ const buildEvidenceBundle = (report) => {
6
+ const { cleaned, signals } = (0, parseLogs_1.parseLogs)(report.logs);
7
+ return {
8
+ narrative: report.summary.trim(),
9
+ logs: cleaned,
10
+ repoContext: report.repoContext ?? '',
11
+ extractedSignals: signals,
12
+ };
13
+ };
14
+ exports.buildEvidenceBundle = buildEvidenceBundle;
@@ -0,0 +1 @@
1
+ export declare function runOllamaChat(prompt: string, model: string): Promise<string>;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runOllamaChat = runOllamaChat;
4
+ const errors_1 = require("../core/errors");
5
+ async function runOllamaChat(prompt, model) {
6
+ const baseUrl = process.env.OLLAMA_BASE_URL ?? 'http://127.0.0.1:11434';
7
+ const response = await fetch(`${baseUrl}/api/chat`, {
8
+ method: 'POST',
9
+ headers: {
10
+ 'Content-Type': 'application/json',
11
+ },
12
+ body: JSON.stringify({
13
+ model,
14
+ stream: false,
15
+ messages: [
16
+ {
17
+ role: 'user',
18
+ content: prompt,
19
+ },
20
+ ],
21
+ options: {
22
+ temperature: 0.2,
23
+ },
24
+ }),
25
+ });
26
+ if (!response.ok) {
27
+ throw new errors_1.QuacktionableError(`Ollama request failed (${response.status}). Ensure Ollama is running and model '${model}' is pulled.`);
28
+ }
29
+ const json = (await response.json());
30
+ const content = json.message?.content;
31
+ if (!content) {
32
+ throw new errors_1.QuacktionableError('Ollama returned an empty response.');
33
+ }
34
+ return content;
35
+ }
@@ -0,0 +1,15 @@
1
+ import type { DuckPersona, EvidenceBundle } from '../core/types';
2
+ export interface DuckModelResponse {
3
+ rounds: Array<{
4
+ question: string;
5
+ answer: string;
6
+ concern: string;
7
+ }>;
8
+ vote: {
9
+ rootCause: string;
10
+ rationale: string;
11
+ confidence: number;
12
+ evidenceRefs: string[];
13
+ };
14
+ }
15
+ export declare const buildDuckPrompt: (persona: DuckPersona, evidence: EvidenceBundle, rounds: number) => string;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildDuckPrompt = void 0;
4
+ const buildDuckPrompt = (persona, evidence, rounds) => {
5
+ return [
6
+ 'You are a judge duck in a debugging tribunal: witty but ruthlessly accurate.',
7
+ `Duck persona: ${persona.name}`,
8
+ `Style: ${persona.style}`,
9
+ `Focus: ${persona.focus}`,
10
+ '',
11
+ 'Task:',
12
+ `- Generate exactly ${rounds} cross-examination rounds.`,
13
+ '- Then cast one vote for the most likely root cause.',
14
+ '- Keep confidence as a number from 0 to 100.',
15
+ '- Use concrete evidence references from narrative, logs, or repo context.',
16
+ '- Light humor is welcome; hallucinations are not.',
17
+ '',
18
+ 'Respond ONLY in JSON with this shape:',
19
+ '{"rounds":[{"question":"","answer":"","concern":""}],"vote":{"rootCause":"","rationale":"","confidence":0,"evidenceRefs":[""]}}',
20
+ '',
21
+ 'Evidence narrative:',
22
+ evidence.narrative || '<none>',
23
+ '',
24
+ 'Evidence logs:',
25
+ evidence.logs || '<none>',
26
+ '',
27
+ 'Evidence repo context:',
28
+ evidence.repoContext || '<none>',
29
+ ].join('\n');
30
+ };
31
+ exports.buildDuckPrompt = buildDuckPrompt;
@@ -0,0 +1,2 @@
1
+ import type { DuckModelResponse } from './prompts';
2
+ export declare const parseDuckModelOutput: (raw: string, expectedRounds: number) => DuckModelResponse;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseDuckModelOutput = void 0;
4
+ const errors_1 = require("../core/errors");
5
+ const parseDuckModelOutput = (raw, expectedRounds) => {
6
+ let parsed;
7
+ try {
8
+ parsed = JSON.parse(raw);
9
+ }
10
+ catch {
11
+ throw new errors_1.InvalidModelOutputError('Model response was not valid JSON.');
12
+ }
13
+ if (!parsed || typeof parsed !== 'object') {
14
+ throw new errors_1.InvalidModelOutputError('Model response was not an object.');
15
+ }
16
+ const candidate = parsed;
17
+ if (!Array.isArray(candidate.rounds) || candidate.rounds.length !== expectedRounds) {
18
+ throw new errors_1.InvalidModelOutputError(`Model response must contain exactly ${expectedRounds} rounds.`);
19
+ }
20
+ for (const round of candidate.rounds) {
21
+ if (!round || typeof round.question !== 'string' || typeof round.answer !== 'string' || typeof round.concern !== 'string') {
22
+ throw new errors_1.InvalidModelOutputError('Each round must include question, answer, and concern strings.');
23
+ }
24
+ }
25
+ if (!candidate.vote || typeof candidate.vote !== 'object') {
26
+ throw new errors_1.InvalidModelOutputError('Model response must include a vote object.');
27
+ }
28
+ if (typeof candidate.vote.rootCause !== 'string' || typeof candidate.vote.rationale !== 'string') {
29
+ throw new errors_1.InvalidModelOutputError('Vote must include rootCause and rationale strings.');
30
+ }
31
+ if (typeof candidate.vote.confidence !== 'number' || Number.isNaN(candidate.vote.confidence)) {
32
+ throw new errors_1.InvalidModelOutputError('Vote confidence must be a number.');
33
+ }
34
+ if (!Array.isArray(candidate.vote.evidenceRefs) || !candidate.vote.evidenceRefs.every((item) => typeof item === 'string')) {
35
+ throw new errors_1.InvalidModelOutputError('Vote evidenceRefs must be an array of strings.');
36
+ }
37
+ return candidate;
38
+ };
39
+ exports.parseDuckModelOutput = parseDuckModelOutput;
@@ -0,0 +1,2 @@
1
+ import type { TribunalSession } from '../core/types';
2
+ export declare const renderJson: (session: TribunalSession) => string;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderJson = void 0;
4
+ const schema_1 = require("./schema");
5
+ const renderJson = (session) => {
6
+ return JSON.stringify((0, schema_1.toJsonOutput)(session), null, 2);
7
+ };
8
+ exports.renderJson = renderJson;
@@ -0,0 +1,7 @@
1
+ import type { TribunalSession } from '../core/types';
2
+ export declare const OUTPUT_SCHEMA_VERSION = "1.0.0";
3
+ export interface QuacktionableJsonOutput {
4
+ schemaVersion: string;
5
+ session: TribunalSession;
6
+ }
7
+ export declare const toJsonOutput: (session: TribunalSession) => QuacktionableJsonOutput;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toJsonOutput = exports.OUTPUT_SCHEMA_VERSION = void 0;
4
+ exports.OUTPUT_SCHEMA_VERSION = '1.0.0';
5
+ const toJsonOutput = (session) => {
6
+ return {
7
+ schemaVersion: exports.OUTPUT_SCHEMA_VERSION,
8
+ session,
9
+ };
10
+ };
11
+ exports.toJsonOutput = toJsonOutput;
@@ -0,0 +1,2 @@
1
+ import type { TribunalSession } from '../core/types';
2
+ export declare const renderTerminal: (session: TribunalSession) => string;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderTerminal = void 0;
4
+ const renderTerminal = (session) => {
5
+ const lines = [];
6
+ const confidenceEmoji = session.verdict.confidence >= 80 ? '🔥' : session.verdict.confidence >= 50 ? '🧠' : '🤔';
7
+ lines.push('🦆⚖️ Rubber Duck Tribunal (Court is now in session)');
8
+ lines.push('🎭 Opening statement: no bug survives cross-examination.');
9
+ lines.push(`🆔 Session: ${session.id}`);
10
+ lines.push(`🤖 Engine: ${session.model}`);
11
+ lines.push('');
12
+ lines.push('📝 Bug Tale');
13
+ lines.push(`- 🐛 ${session.bugReport.summary}`);
14
+ if (session.evidence.extractedSignals.length > 0) {
15
+ lines.push('');
16
+ lines.push('🚨 Suspicious Signals');
17
+ for (const signal of session.evidence.extractedSignals) {
18
+ lines.push(`- 📍 ${signal}`);
19
+ }
20
+ }
21
+ lines.push('');
22
+ lines.push('🗳️ Judge Duck Votes');
23
+ for (const vote of session.votes) {
24
+ lines.push(`- 🦆 ${vote.duckName}: ${vote.rootCause} (${vote.confidence}%)`);
25
+ lines.push(` 💬 rationale: ${vote.rationale}`);
26
+ }
27
+ lines.push('');
28
+ lines.push('🏛️ Final Quackdict');
29
+ lines.push(`- 🎯 root cause: ${session.verdict.rootCause}`);
30
+ lines.push(`- ${confidenceEmoji} confidence: ${session.verdict.confidence}%`);
31
+ if (session.verdict.dissentingCauses.length > 0) {
32
+ lines.push(`- 🙋 dissent: ${session.verdict.dissentingCauses.join(', ')}`);
33
+ }
34
+ lines.push(`- 🧾 summary: ${session.verdict.summary}`);
35
+ lines.push('🧃 Court snack break: hydrate, patch, and redeploy responsibly.');
36
+ return lines.join('\n');
37
+ };
38
+ exports.renderTerminal = renderTerminal;
@@ -0,0 +1,2 @@
1
+ import type { Verdict, Vote } from '../core/types';
2
+ export declare const aggregateVotes: (votes: Vote[]) => Verdict;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.aggregateVotes = void 0;
4
+ const confidence_1 = require("./confidence");
5
+ const scoring_1 = require("./scoring");
6
+ const aggregateVotes = (votes) => {
7
+ if (votes.length === 0) {
8
+ return {
9
+ rootCause: 'insufficient evidence',
10
+ confidence: 0,
11
+ dissentingCauses: [],
12
+ voteBreakdown: [],
13
+ summary: 'No valid votes were produced by the duck bench. The court requests more breadcrumbs.',
14
+ insufficientEvidence: true,
15
+ };
16
+ }
17
+ const totals = (0, scoring_1.scoreVotes)(votes);
18
+ const sorted = Object.entries(totals).sort((a, b) => b[1] - a[1]);
19
+ const [topCause] = sorted[0];
20
+ const topCauseVotes = votes.filter((vote) => (0, scoring_1.normalizeCause)(vote.rootCause) === topCause);
21
+ const confidence = (0, confidence_1.calibrateConfidence)(topCauseVotes, votes);
22
+ return {
23
+ rootCause: topCause,
24
+ confidence,
25
+ dissentingCauses: sorted.slice(1, 4).map(([cause]) => cause),
26
+ voteBreakdown: votes,
27
+ summary: confidence < 35
28
+ ? 'The ducks are unconvinced. Bring juicier logs and code clues before shipping a fix.'
29
+ : `The duck bench quacks in favor of '${topCause}' as the likely root cause.`,
30
+ insufficientEvidence: confidence < 25,
31
+ };
32
+ };
33
+ exports.aggregateVotes = aggregateVotes;
@@ -0,0 +1,2 @@
1
+ import type { Vote } from '../core/types';
2
+ export declare const calibrateConfidence: (topCauseVotes: Vote[], allVotes: Vote[]) => number;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calibrateConfidence = void 0;
4
+ const calibrateConfidence = (topCauseVotes, allVotes) => {
5
+ if (allVotes.length === 0) {
6
+ return 0;
7
+ }
8
+ const topShare = topCauseVotes.length / allVotes.length;
9
+ const avgTopConfidence = topCauseVotes.reduce((sum, vote) => sum + vote.confidence, 0) / Math.max(topCauseVotes.length, 1);
10
+ const calibrated = topShare * 70 + (avgTopConfidence / 100) * 30;
11
+ return Math.round(Math.max(0, Math.min(100, calibrated * 100)));
12
+ };
13
+ exports.calibrateConfidence = calibrateConfidence;
@@ -0,0 +1,2 @@
1
+ import type { DuckPersona } from '../core/types';
2
+ export declare const DEFAULT_DUCKS: DuckPersona[];
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_DUCKS = void 0;
4
+ exports.DEFAULT_DUCKS = [
5
+ {
6
+ id: 'duck-1',
7
+ name: 'Detective Quackson',
8
+ style: 'Methodical, skeptical, and mildly dramatic',
9
+ focus: 'Correlate logs with claim consistency and missing evidence',
10
+ },
11
+ {
12
+ id: 'duck-2',
13
+ name: 'Captain Crashbeard',
14
+ style: 'Pragmatic production engineer with chaos stories',
15
+ focus: 'Failure modes, environment drift, and configuration mismatches',
16
+ },
17
+ {
18
+ id: 'duck-3',
19
+ name: 'Null Pointer Patty',
20
+ style: 'Static-analysis minded and delightfully sarcastic',
21
+ focus: 'Control flow, nullability, race conditions, and edge paths',
22
+ },
23
+ ];
@@ -0,0 +1,2 @@
1
+ import type { TribunalSession, TribunalOptions } from '../core/types';
2
+ export declare const runTribunal: (evidence: TribunalSession["evidence"], bugReport: TribunalSession["bugReport"], options: TribunalOptions) => Promise<TribunalSession>;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runTribunal = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const copilotCli_1 = require("../copilot/copilotCli");
6
+ const prompts_1 = require("../llm/prompts");
7
+ const validateModelOutput_1 = require("../llm/validateModelOutput");
8
+ const aggregateVotes_1 = require("./aggregateVotes");
9
+ const stripFences = (text) => {
10
+ const trimmed = text.trim();
11
+ const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
12
+ return fenced ? fenced[1].trim() : trimmed;
13
+ };
14
+ const runTribunal = async (evidence, bugReport, options) => {
15
+ const modelLabel = await (0, copilotCli_1.resolveCopilotModelLabel)();
16
+ options.onProgress?.('Checking if Copilot CLI is awake...');
17
+ if (options.requireCopilotCli) {
18
+ (0, copilotCli_1.assertCopilotCliAvailable)();
19
+ }
20
+ options.onProgress?.('Asking Copilot for spicy cross-exam ammo...');
21
+ const copilotNotes = options.requireCopilotCli ? await (0, copilotCli_1.runCopilotCrossExam)(evidence) : '';
22
+ const enrichedEvidence = {
23
+ ...evidence,
24
+ narrative: `${evidence.narrative}\n\nCopilot external cross-exam notes:\n${copilotNotes}`,
25
+ };
26
+ options.onProgress?.('Calling the duck judges to the bench...');
27
+ const results = await Promise.all(options.ducks.map(async (duck) => {
28
+ options.onProgress?.(`🦆 ${duck.name} is cross-examining your theory...`);
29
+ const prompt = (0, prompts_1.buildDuckPrompt)(duck, enrichedEvidence, options.rounds);
30
+ const raw = await (0, copilotCli_1.runCopilotDuckJudgment)(prompt);
31
+ const parsed = (0, validateModelOutput_1.parseDuckModelOutput)(stripFences(raw), options.rounds);
32
+ const rounds = parsed.rounds.map((round, index) => ({
33
+ round: index + 1,
34
+ question: round.question,
35
+ answer: round.answer,
36
+ concern: round.concern,
37
+ }));
38
+ const vote = {
39
+ duckId: duck.id,
40
+ duckName: duck.name,
41
+ rootCause: parsed.vote.rootCause,
42
+ rationale: parsed.vote.rationale,
43
+ confidence: Math.max(0, Math.min(100, Math.round(parsed.vote.confidence))),
44
+ evidenceRefs: parsed.vote.evidenceRefs,
45
+ };
46
+ options.onProgress?.(`✅ ${duck.name} submitted a vote.`);
47
+ return { duckId: duck.id, rounds, vote };
48
+ }));
49
+ const roundsByDuck = {};
50
+ const votes = [];
51
+ for (const result of results) {
52
+ roundsByDuck[result.duckId] = result.rounds;
53
+ votes.push(result.vote);
54
+ }
55
+ options.onProgress?.('Counting quacks and tallying votes...');
56
+ const verdict = (0, aggregateVotes_1.aggregateVotes)(votes);
57
+ options.onProgress?.('Final quackdict ready. Court adjourned.');
58
+ return {
59
+ id: (0, node_crypto_1.randomUUID)(),
60
+ createdAt: new Date().toISOString(),
61
+ model: modelLabel,
62
+ bugReport,
63
+ evidence,
64
+ roundsByDuck,
65
+ votes,
66
+ verdict,
67
+ };
68
+ };
69
+ exports.runTribunal = runTribunal;
@@ -0,0 +1,3 @@
1
+ import type { Vote } from '../core/types';
2
+ export declare const scoreVotes: (votes: Vote[]) => Record<string, number>;
3
+ export declare const normalizeCause: (cause: string) => string;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeCause = exports.scoreVotes = void 0;
4
+ const scoreVote = (vote) => {
5
+ const evidenceBonus = Math.min(vote.evidenceRefs.length * 3, 12);
6
+ return Math.max(0, Math.min(100, vote.confidence + evidenceBonus));
7
+ };
8
+ const scoreVotes = (votes) => {
9
+ const totals = {};
10
+ for (const vote of votes) {
11
+ const cause = (0, exports.normalizeCause)(vote.rootCause);
12
+ const value = scoreVote(vote);
13
+ totals[cause] = (totals[cause] ?? 0) + value;
14
+ }
15
+ return totals;
16
+ };
17
+ exports.scoreVotes = scoreVotes;
18
+ const normalizeCause = (cause) => {
19
+ return cause.trim().toLowerCase().replace(/\s+/g, ' ');
20
+ };
21
+ exports.normalizeCause = normalizeCause;
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "quacktionable",
3
+ "version": "0.1.0",
4
+ "description": "Rubber Duck Tribunal: cross-examine bugs and vote on root cause.",
5
+ "main": "dist/cli.js",
6
+ "files": [
7
+ "dist",
8
+ "bin",
9
+ "README.md",
10
+ "CHANGELOG.md"
11
+ ],
12
+ "bin": {
13
+ "quack": "bin/quack",
14
+ "quacktionable": "bin/quacktionable"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/seshxn/quacktionable.git"
19
+ },
20
+ "homepage": "https://github.com/seshxn/quacktionable#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/seshxn/quacktionable/issues"
23
+ },
24
+ "type": "commonjs",
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "lint": "tsc --noEmit -p tsconfig.json",
28
+ "test": "vitest run",
29
+ "dev": "tsx src/cli.ts",
30
+ "prepack": "npm run build"
31
+ },
32
+ "keywords": [
33
+ "cli",
34
+ "debugging",
35
+ "rubber-duck",
36
+ "root-cause-analysis",
37
+ "copilot"
38
+ ],
39
+ "author": "",
40
+ "license": "MIT",
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "engines": {
45
+ "node": ">=18.18"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^22.13.10",
49
+ "tsx": "^4.19.3",
50
+ "typescript": "^5.8.2",
51
+ "vitest": "^3.0.8"
52
+ },
53
+ "dependencies": {
54
+ "gradient-string": "^3.0.0"
55
+ }
56
+ }