palabre 0.1.4

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.
@@ -0,0 +1,98 @@
1
+ const presets = {
2
+ "codex-claude": {
3
+ agentA: "codex",
4
+ agentB: "claude"
5
+ },
6
+ "claude-codex": {
7
+ agentA: "claude",
8
+ agentB: "codex"
9
+ },
10
+ "codex-opencode": {
11
+ agentA: "codex",
12
+ agentB: "opencode"
13
+ },
14
+ "opencode-codex": {
15
+ agentA: "opencode",
16
+ agentB: "codex"
17
+ },
18
+ "claude-opencode": {
19
+ agentA: "claude",
20
+ agentB: "opencode"
21
+ },
22
+ "opencode-claude": {
23
+ agentA: "opencode",
24
+ agentB: "claude"
25
+ },
26
+ "gemini-opencode": {
27
+ agentA: "gemini",
28
+ agentB: "opencode"
29
+ },
30
+ "opencode-gemini": {
31
+ agentA: "opencode",
32
+ agentB: "gemini"
33
+ },
34
+ "opencode-ollama": {
35
+ agentA: "opencode",
36
+ agentB: "ollama-local"
37
+ },
38
+ "ollama-opencode": {
39
+ agentA: "ollama-local",
40
+ agentB: "opencode"
41
+ },
42
+ "codex-ollama": {
43
+ agentA: "codex",
44
+ agentB: "ollama-local"
45
+ },
46
+ "ollama-codex": {
47
+ agentA: "ollama-local",
48
+ agentB: "codex"
49
+ },
50
+ "claude-ollama": {
51
+ agentA: "claude",
52
+ agentB: "ollama-local"
53
+ },
54
+ "ollama-claude": {
55
+ agentA: "ollama-local",
56
+ agentB: "claude"
57
+ },
58
+ "gemini-ollama": {
59
+ agentA: "gemini",
60
+ agentB: "ollama-local"
61
+ },
62
+ "ollama-gemini": {
63
+ agentA: "ollama-local",
64
+ agentB: "gemini"
65
+ },
66
+ "codex-gemini": {
67
+ agentA: "codex",
68
+ agentB: "gemini"
69
+ },
70
+ "gemini-codex": {
71
+ agentA: "gemini",
72
+ agentB: "codex"
73
+ },
74
+ "claude-gemini": {
75
+ agentA: "claude",
76
+ agentB: "gemini"
77
+ },
78
+ "gemini-claude": {
79
+ agentA: "gemini",
80
+ agentB: "claude"
81
+ }
82
+ };
83
+ /** Retourne la paire d'agents pour `name`. Lève une erreur avec la liste des presets disponibles si inconnu. */
84
+ export function resolvePreset(name) {
85
+ const preset = presets[name];
86
+ if (!preset) {
87
+ throw new Error(`Preset inconnu: ${name}. Presets disponibles: ${Object.keys(presets).join(", ")}`);
88
+ }
89
+ return preset;
90
+ }
91
+ /** Retourne la liste de tous les noms de presets disponibles, dans l'ordre de déclaration. */
92
+ export function listPresetNames() {
93
+ return Object.keys(presets);
94
+ }
95
+ /** Recherche inverse : retourne le nom du preset correspondant à une paire `(agentA, agentB)`, ou `undefined`. */
96
+ export function findPresetNameForPair(agentA, agentB) {
97
+ return Object.entries(presets).find(([, preset]) => preset.agentA === agentA && preset.agentB === agentB)?.[0];
98
+ }
package/dist/prompt.js ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Formate le prompt complet transmis à l'adapter.
3
+ * Dispatche vers le format de synthèse si `input.mode === "summary"`, sinon construit le prompt de débat standard.
4
+ */
5
+ export function formatAgentPrompt(input) {
6
+ if (input.mode === "summary") {
7
+ return formatSummaryPrompt(input);
8
+ }
9
+ const transcript = formatTranscript(input.transcript);
10
+ return [
11
+ `Sujet: ${input.topic}`,
12
+ "",
13
+ `Tu es ${input.selfName}. Tu reponds au tour ${input.turn}.`,
14
+ `Ton interlocuteur est ${input.peerName}.`,
15
+ `Role de ${input.selfName}: ${input.selfRole}.`,
16
+ roleInstruction(input.selfRole),
17
+ "",
18
+ "Contexte de session PALABRE:",
19
+ "- Source: fourni par PALABRE et visible par tous les agents de ce debat.",
20
+ `- Date locale: ${input.session.localDate}`,
21
+ `- Fuseau horaire: ${input.session.timeZone}`,
22
+ `- Dossier courant: ${input.session.cwd}`,
23
+ `- Session demarree a: ${input.session.startedAt}`,
24
+ "",
25
+ "Objectif:",
26
+ "- Apporte une reponse utile, concrete et courte.",
27
+ "- Reagis aux arguments precedents au lieu de repartir de zero.",
28
+ "- Signale les incertitudes ou les points a trancher.",
29
+ "- Respecte ton role sans ignorer les faits du transcript.",
30
+ "",
31
+ input.files.length > 0 ? "Contexte fichiers:" : "",
32
+ formatFileContext(input.files),
33
+ input.files.length > 0 ? "" : "",
34
+ transcript.length > 0 ? "Historique:" : "Historique: aucun message pour le moment.",
35
+ transcript,
36
+ "",
37
+ "Ta reponse:"
38
+ ]
39
+ .filter(Boolean)
40
+ .join("\n");
41
+ }
42
+ /** Formate le prompt de synthèse finale. Impose un format structuré : Consensus / Désaccords / Actions / Conclusion. */
43
+ function formatSummaryPrompt(input) {
44
+ const transcript = formatTranscript(input.transcript);
45
+ return [
46
+ `Sujet: ${input.topic}`,
47
+ "",
48
+ `Tu es ${input.selfName}. Tu produis la synthese finale du debat.`,
49
+ `Role de ${input.selfName}: ${input.selfRole}.`,
50
+ roleInstruction("summarizer"),
51
+ "",
52
+ "Contexte de session PALABRE:",
53
+ "- Source: fourni par PALABRE et visible par tous les agents de ce debat.",
54
+ `- Date locale: ${input.session.localDate}`,
55
+ `- Fuseau horaire: ${input.session.timeZone}`,
56
+ `- Dossier courant: ${input.session.cwd}`,
57
+ `- Session demarree a: ${input.session.startedAt}`,
58
+ "",
59
+ "Objectif:",
60
+ "- Resume le consensus en points concrets.",
61
+ "- Liste les desaccords ou incertitudes qui restent.",
62
+ "- Propose les prochaines actions techniques.",
63
+ "- Termine par une conclusion courte en prose, bien ecrite, qui explique rapidement ce qu'il faut retenir.",
64
+ "- Reste concis et exploitable.",
65
+ "",
66
+ input.files.length > 0 ? "Contexte fichiers:" : "",
67
+ formatFileContext(input.files),
68
+ input.files.length > 0 ? "" : "",
69
+ "Transcript du debat:",
70
+ transcript || "Aucun message.",
71
+ "",
72
+ "Format attendu:",
73
+ "### Consensus",
74
+ "",
75
+ "### Desaccords / incertitudes",
76
+ "",
77
+ "### Actions proposees",
78
+ "",
79
+ "### Conclusion",
80
+ "",
81
+ "Un court paragraphe de synthese en prose, sans liste, qui resume le sens general du debat et la decision ou direction la plus raisonnable.",
82
+ "",
83
+ "Synthese:"
84
+ ]
85
+ .filter(Boolean)
86
+ .join("\n");
87
+ }
88
+ function roleInstruction(role) {
89
+ const instructions = {
90
+ implementer: "Consigne de role: propose une solution concrete, executable et sobrement justifiee.",
91
+ reviewer: "Consigne de role: cherche les risques, regressions, angles morts et tests manquants.",
92
+ architect: "Consigne de role: structure les options techniques, compromis et frontieres du systeme.",
93
+ scout: "Consigne de role: explore rapidement le terrain, releve les pistes utiles et les inconnues.",
94
+ critic: "Consigne de role: challenge les hypotheses, pointe les faiblesses et demande les preuves utiles.",
95
+ summarizer: "Consigne de role: synthetise fidelement le transcript sans ajouter de nouvelles hypotheses non signalees."
96
+ };
97
+ return instructions[role];
98
+ }
99
+ /** Formate les fichiers projet en blocs de code annotés pour l'injection dans le prompt. */
100
+ function formatFileContext(files) {
101
+ return files
102
+ .map((file) => {
103
+ return [
104
+ `--- ${file.path} (${file.sizeBytes} bytes)`,
105
+ "```",
106
+ file.content.trimEnd(),
107
+ "```"
108
+ ].join("\n");
109
+ })
110
+ .join("\n\n");
111
+ }
112
+ /** Formate le transcript en sections lisibles. Retourne une chaîne vide si aucun message. */
113
+ export function formatTranscript(messages) {
114
+ return messages
115
+ .map((message) => {
116
+ return [
117
+ `--- ${message.agent} (${message.role})`,
118
+ message.content.trim()
119
+ ].join("\n");
120
+ })
121
+ .join("\n\n");
122
+ }
@@ -0,0 +1,171 @@
1
+ const supportsColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
2
+ const supportsInteractiveOutput = Boolean(process.stdout.isTTY);
3
+ /** Instancie le renderer adapté : pretty (spinner, couleurs ANSI, sections) ou plain (logs bruts). */
4
+ export function createConsoleRenderer(plain) {
5
+ return plain ? new PlainConsoleRenderer() : new PrettyConsoleRenderer(supportsColor, supportsInteractiveOutput);
6
+ }
7
+ class PrettyConsoleRenderer {
8
+ color;
9
+ interactive;
10
+ spinner;
11
+ spinnerFrame = 0;
12
+ renderingSummary = false;
13
+ frames = ["-", "\\", "|", "/"];
14
+ constructor(color, interactive) {
15
+ this.color = color;
16
+ this.interactive = interactive;
17
+ }
18
+ start(options, agents = []) {
19
+ const title = "PALABRE";
20
+ process.stdout.write([
21
+ "",
22
+ this.c("cyan", `┌─ ${title} ${"─".repeat(Math.max(1, 54 - title.length))}`),
23
+ this.c("cyan", `│`) + ` Sujet: ${options.topic}`,
24
+ this.c("cyan", `│`) + ` Agents: ${formatAgentPair(options, agents)}`,
25
+ this.c("cyan", `│`) + ` Réponses: ${options.turns} | Synthèse: ${formatSummary(options)}`,
26
+ this.c("cyan", `│`) + ` Contexte: ${formatContext(options)}`,
27
+ this.c("cyan", `│`) + ` Options: arrêt anticipé ${options.earlyStopOnAgreement ? "activé" : "désactivé"}, auto-pull Ollama ${options.pullModels ? "activé" : "désactivé"}`,
28
+ this.c("cyan", `└${"─".repeat(57)}`),
29
+ ""
30
+ ].join("\n"));
31
+ }
32
+ warning(message) {
33
+ process.stderr.write(`${this.c("yellow", "Warning:")} ${message}\n`);
34
+ }
35
+ notice(message) {
36
+ process.stdout.write(`${this.c("green", "Info:")} ${message}\n`);
37
+ }
38
+ turnStart(turn, totalTurns, agent, role) {
39
+ this.renderingSummary = false;
40
+ process.stdout.write([
41
+ "",
42
+ this.c("blue", `◆ ${agent}`) + this.dim(` · ${role} · tour ${turn}/${totalTurns}`),
43
+ this.dim("─".repeat(60)),
44
+ ""
45
+ ].join("\n"));
46
+ }
47
+ thinkingStart(agent, role) {
48
+ this.thinkingEnd();
49
+ const text = `${agent} (${role}) reflechit`;
50
+ if (!this.interactive) {
51
+ process.stdout.write(`${this.dim(`${text}...`)}\n`);
52
+ return;
53
+ }
54
+ const render = () => {
55
+ const frame = this.frames[this.spinnerFrame % this.frames.length];
56
+ this.spinnerFrame += 1;
57
+ process.stdout.write(`\r${this.c("cyan", frame)} ${this.dim(`${text}...`)}`);
58
+ };
59
+ render();
60
+ this.spinner = setInterval(render, 120);
61
+ }
62
+ thinkingEnd() {
63
+ if (this.spinner) {
64
+ clearInterval(this.spinner);
65
+ this.spinner = undefined;
66
+ }
67
+ if (this.interactive) {
68
+ process.stdout.write("\r\u001b[2K");
69
+ }
70
+ }
71
+ message(content) {
72
+ const trimmed = content.trim();
73
+ process.stdout.write(`${this.renderingSummary ? this.formatSummaryMessage(trimmed) : trimmed}\n`);
74
+ }
75
+ summaryStart(agent, role) {
76
+ this.renderingSummary = true;
77
+ process.stdout.write([
78
+ "",
79
+ this.c("magenta", `◆ Synthese`) + this.dim(` · ${agent} · ${role}`),
80
+ this.dim("─".repeat(60)),
81
+ ""
82
+ ].join("\n"));
83
+ }
84
+ done(outputPath) {
85
+ process.stdout.write(`\n${this.c("green", "Debat exporte:")} ${outputPath}\n`);
86
+ }
87
+ formatSummaryMessage(content) {
88
+ return content
89
+ .split(/\r?\n/)
90
+ .map((line) => {
91
+ const heading = line.match(/^###\s+(.+)$/);
92
+ if (!heading)
93
+ return line;
94
+ return [
95
+ "",
96
+ this.c("magenta", heading[1] ?? line),
97
+ this.dim("─".repeat(40))
98
+ ].join("\n");
99
+ })
100
+ .join("\n")
101
+ .trimStart();
102
+ }
103
+ c(color, value) {
104
+ if (!this.color)
105
+ return value;
106
+ return `${codes[color]}${value}${codes.reset}`;
107
+ }
108
+ dim(value) {
109
+ if (!this.color)
110
+ return value;
111
+ return `${codes.dim}${value}${codes.reset}`;
112
+ }
113
+ }
114
+ class PlainConsoleRenderer {
115
+ start(options, agents = []) {
116
+ process.stdout.write(`Sujet: ${options.topic}` + "\n");
117
+ process.stdout.write(`Agents: ${formatAgentPair(options, agents)}` + "\n");
118
+ process.stdout.write(`Réponses: ${options.turns} | Synthèse: ${formatSummary(options)} | Contexte: ${formatContext(options)}` + "\n");
119
+ }
120
+ warning(message) {
121
+ process.stderr.write(`Warning: ${message}\n`);
122
+ }
123
+ notice(message) {
124
+ process.stdout.write(`Info: ${message}\n`);
125
+ }
126
+ turnStart(turn, totalTurns, agent, role) {
127
+ process.stdout.write(`\n[${turn}/${totalTurns}] ${agent} (${role})...\n`);
128
+ }
129
+ thinkingStart(_agent, _role) { }
130
+ thinkingEnd() { }
131
+ message(content) {
132
+ process.stdout.write(`${content.trim()}\n`);
133
+ }
134
+ summaryStart(agent, role) {
135
+ process.stdout.write(`\n[Synthese] ${agent} (${role})...\n`);
136
+ }
137
+ done(outputPath) {
138
+ process.stdout.write(`\nDebat exporte: ${outputPath}\n`);
139
+ }
140
+ }
141
+ function formatAgentPair(options, agents) {
142
+ if (agents.length >= 2) {
143
+ return `${formatAgentLabel(agents[0])} <-> ${formatAgentLabel(agents[1])}`;
144
+ }
145
+ return `${options.agentA} <-> ${options.agentB}`;
146
+ }
147
+ function formatAgentLabel(agent) {
148
+ if (!agent) {
149
+ return "?";
150
+ }
151
+ return `${agent.name} (${agent.role}, ${agent.type})`;
152
+ }
153
+ function formatSummary(options) {
154
+ return options.summaryEnabled ? options.summaryAgent ?? options.agentB : "désactivée";
155
+ }
156
+ function formatContext(options) {
157
+ const count = options.files.length;
158
+ if (count === 0) {
159
+ return "aucun fichier injecté";
160
+ }
161
+ return `${count} fichier${count > 1 ? "s" : ""} injecté${count > 1 ? "s" : ""}`;
162
+ }
163
+ const codes = {
164
+ reset: "\u001b[0m",
165
+ dim: "\u001b[2m",
166
+ blue: "\u001b[34m",
167
+ cyan: "\u001b[36m",
168
+ green: "\u001b[32m",
169
+ magenta: "\u001b[35m",
170
+ yellow: "\u001b[33m"
171
+ };
@@ -0,0 +1,35 @@
1
+ import path from "node:path";
2
+ /**
3
+ * Construit le contexte de session partagé par tous les agents pour la durée du débat.
4
+ * `cwd` et `now` sont injectables pour la testabilité.
5
+ */
6
+ export function createSessionContext(cwd = process.cwd(), now = new Date()) {
7
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown";
8
+ return {
9
+ startedAt: now.toISOString(),
10
+ localDate: formatDateInTimeZone(now, timeZone),
11
+ timeZone,
12
+ cwd: path.resolve(cwd)
13
+ };
14
+ }
15
+ /** Formate une date dans le fuseau local via `Intl.DateTimeFormat`. Retombe sur la date UTC ISO si le runtime ne supporte pas le fuseau. */
16
+ function formatDateInTimeZone(date, timeZone) {
17
+ try {
18
+ const parts = new Intl.DateTimeFormat("en-CA", {
19
+ timeZone,
20
+ year: "numeric",
21
+ month: "2-digit",
22
+ day: "2-digit"
23
+ }).formatToParts(date);
24
+ const year = parts.find((part) => part.type === "year")?.value;
25
+ const month = parts.find((part) => part.type === "month")?.value;
26
+ const day = parts.find((part) => part.type === "day")?.value;
27
+ if (year && month && day) {
28
+ return `${year}-${month}-${day}`;
29
+ }
30
+ }
31
+ catch {
32
+ // Fall back to UTC ISO date if the runtime cannot format the local timezone.
33
+ }
34
+ return date.toISOString().slice(0, 10);
35
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/update.js ADDED
@@ -0,0 +1,68 @@
1
+ import { spawn } from "node:child_process";
2
+ import { access } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ /** Détecte le mode d'installation (source ou package) à partir de la présence d'un dossier `.git`. */
6
+ export async function getUpdateInfo(version) {
7
+ const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ return {
9
+ version,
10
+ projectRoot,
11
+ sourceCheckout: await exists(path.join(projectRoot, ".git"))
12
+ };
13
+ }
14
+ /** Génère les instructions de mise à jour adaptées au mode d'installation détecté dans `info`. */
15
+ export function formatUpdateInstructions(info) {
16
+ const lines = [
17
+ `PALABRE ${info.version}`,
18
+ "",
19
+ "Mise a jour recommandee:"
20
+ ];
21
+ if (info.sourceCheckout) {
22
+ lines.push("", "Installation depuis le repo source detectee.", "", ` cd "${info.projectRoot}"`, " git pull --ff-only", " pnpm install", " pnpm build", " pnpm link --global", "", "Pour executer ces etapes automatiquement:", "", " palabre update --apply");
23
+ }
24
+ else {
25
+ lines.push("", "Installation package detectee.", "", " pnpm add --global palabre@latest", "", "Si tu utilises npm:", "", " npm install --global palabre@latest");
26
+ }
27
+ return lines.join("\n");
28
+ }
29
+ /**
30
+ * Exécute `git pull`, `pnpm install`, `pnpm build`, `pnpm link --global` dans le répertoire du projet.
31
+ * @throws {Error} si `info.sourceCheckout` est faux — la mise à jour automatique ne s'applique qu'aux checkouts git.
32
+ */
33
+ export async function applySourceUpdate(info) {
34
+ if (!info.sourceCheckout) {
35
+ throw new Error("Mise a jour automatique disponible seulement depuis un checkout git. Utilise pnpm add --global palabre@latest.");
36
+ }
37
+ await runStep("git", ["pull", "--ff-only"], info.projectRoot);
38
+ await runStep("pnpm", ["install"], info.projectRoot);
39
+ await runStep("pnpm", ["build"], info.projectRoot);
40
+ await runStep("pnpm", ["link", "--global"], info.projectRoot);
41
+ }
42
+ async function exists(targetPath) {
43
+ try {
44
+ await access(targetPath);
45
+ return true;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ function runStep(command, args, cwd) {
52
+ return new Promise((resolve, reject) => {
53
+ const child = spawn(command, args, {
54
+ cwd,
55
+ shell: process.platform === "win32",
56
+ stdio: "inherit",
57
+ windowsHide: true
58
+ });
59
+ child.on("error", reject);
60
+ child.on("close", (exitCode) => {
61
+ if (exitCode === 0) {
62
+ resolve();
63
+ return;
64
+ }
65
+ reject(new Error(`${command} ${args.join(" ")} a echoue avec le code ${exitCode ?? "inconnu"}.`));
66
+ });
67
+ });
68
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "palabre",
3
+ "version": "0.1.4",
4
+ "description": "Orchestrateur de debat entre agents IA locaux, CLIs et Ollama.",
5
+ "type": "module",
6
+ "homepage": "https://github.com/JuReyms/Palabre#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/JuReyms/Palabre.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/JuReyms/Palabre/issues"
13
+ },
14
+ "keywords": [
15
+ "ai",
16
+ "cli",
17
+ "agents",
18
+ "ollama",
19
+ "codex",
20
+ "claude",
21
+ "gemini",
22
+ "opencode"
23
+ ],
24
+ "bin": {
25
+ "palabre": "./dist/index.js"
26
+ },
27
+ "files": [
28
+ "dist/",
29
+ "README.md",
30
+ "palabre.config.example.json"
31
+ ],
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^20.12.0",
37
+ "typescript": "^5.4.0"
38
+ },
39
+ "scripts": {
40
+ "build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
41
+ "check": "tsc -p tsconfig.json --noEmit",
42
+ "start": "node ./dist/index.js",
43
+ "test": "pnpm build:test && node --test .tmp/test-dist/tests/*.test.js",
44
+ "build:test": "node -e \"fs.rmSync('.tmp/test-dist',{recursive:true,force:true})\" && tsc -p tsconfig.test.json"
45
+ }
46
+ }
@@ -0,0 +1,81 @@
1
+ {
2
+ "outputDir": ".",
3
+ "defaults": {
4
+ "agentA": "codex",
5
+ "agentB": "claude",
6
+ "summaryAgent": "claude",
7
+ "turns": 4
8
+ },
9
+ "agents": {
10
+ "codex": {
11
+ "type": "cli",
12
+ "command": "codex",
13
+ "args": [
14
+ "exec",
15
+ "--skip-git-repo-check",
16
+ "--color",
17
+ "never",
18
+ "--sandbox",
19
+ "read-only",
20
+ "-"
21
+ ],
22
+ "promptMode": "stdin",
23
+ "shell": true,
24
+ "role": "implementer",
25
+ "tier": "primary"
26
+ },
27
+ "claude": {
28
+ "type": "cli",
29
+ "command": "claude.exe",
30
+ "args": [
31
+ "--print",
32
+ "--output-format",
33
+ "text",
34
+ "--no-session-persistence"
35
+ ],
36
+ "promptMode": "stdin",
37
+ "shell": false,
38
+ "role": "reviewer",
39
+ "tier": "primary"
40
+ },
41
+ "gemini": {
42
+ "type": "cli",
43
+ "command": "gemini",
44
+ "args": [
45
+ "--output-format",
46
+ "text",
47
+ "--approval-mode",
48
+ "plan",
49
+ "--skip-trust",
50
+ "--prompt",
51
+ "-"
52
+ ],
53
+ "promptMode": "stdin",
54
+ "shell": true,
55
+ "role": "reviewer",
56
+ "tier": "primary"
57
+ },
58
+ "opencode": {
59
+ "type": "cli",
60
+ "command": "opencode",
61
+ "args": [
62
+ "run"
63
+ ],
64
+ "promptMode": "stdin",
65
+ "modelArg": "--model",
66
+ "shell": true,
67
+ "role": "reviewer",
68
+ "tier": "primary"
69
+ },
70
+ "ollama-local": {
71
+ "type": "ollama",
72
+ "baseUrl": "http://localhost:11434",
73
+ "model": "nemotron-3-nano:4b",
74
+ "role": "critic",
75
+ "tier": "local",
76
+ "temperature": 0.2,
77
+ "validateModel": true,
78
+ "unloadOtherModels": true
79
+ }
80
+ }
81
+ }