palabre 0.6.0 → 0.6.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
@@ -50,6 +50,7 @@ palabre -s "Compare ces deux approches" -t 2
50
50
  palabre codex-claude "Relis cette architecture" --context src docs
51
51
  palabre claude-ollama "Critique ce fichier" --files README.md
52
52
  palabre codex-claude "Preview" --context src --show-prompt
53
+ palabre context scan src docs --json
53
54
  ```
54
55
 
55
56
  ### Agents supportés
@@ -63,10 +64,22 @@ palabre codex-claude "Preview" --context src --show-prompt
63
64
 
64
65
  PALABRE ne liste pas les modèles : ils changent souvent et dépendent de chaque CLI ou compte utilisateur. `--model-a`, `--model-b` et `--summary-model` transmettent simplement la valeur brute à l'agent concerné.
65
66
 
67
+ ### Intégrations
68
+
69
+ PALABRE expose des sorties JSON versionnées pour les clients externes :
70
+
71
+ - `palabre presets --json` pour lire les paires d'agents disponibles ;
72
+ - `palabre context scan --json` pour prévisualiser le contexte que `--context` retiendrait ;
73
+ - `--renderer ndjson` ou `--json` pour suivre un débat événement par événement.
74
+
75
+ Le flux NDJSON v1 est traité comme une API publique d'intégration. Les ajouts compatibles se font sans casser v1 ; les changements cassants doivent changer le champ `v`.
76
+
66
77
  ### Confidentialité
67
78
 
68
79
  PALABRE tourne localement et n'envoie aucune donnée à un serveur appartenant à PALABRE. Les données envoyées aux agents dépendent des outils que vous utilisez : vérifiez les politiques de confidentialité de Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode, Ollama ou de tout autre agent configuré.
69
80
 
81
+ Si un agent échoue pendant le débat ou la synthèse, PALABRE conserve l'export Markdown partiel avec une section d'interruption quand c'est possible.
82
+
70
83
  ### Développement local
71
84
 
72
85
  ```bash
@@ -97,7 +110,7 @@ It does not replace your tools: it drives them. You keep your subscriptions, def
97
110
  - https://palab.re
98
111
  - https://palabre.netlify.app
99
112
 
100
- Useful pages: [Installation](https://palab.re/fr/get-started/installation), [Configuration](https://palab.re/fr/get-started/configuration), [First debate](https://palab.re/fr/get-started/first-debate), [CLI reference](https://palab.re/fr/reference/cli), [Troubleshooting](https://palab.re/fr/troubleshooting), [Roadmap](https://palab.re/fr/roadmap).
113
+ Useful pages: [Installation](https://palab.re/en/get-started/installation), [Configuration](https://palab.re/en/get-started/configuration), [First debate](https://palab.re/en/get-started/first-debate), [CLI reference](https://palab.re/en/reference/cli), [Troubleshooting](https://palab.re/en/troubleshooting), [Roadmap](https://palab.re/en/roadmap).
101
114
 
102
115
  ### Installation
103
116
 
@@ -125,6 +138,7 @@ palabre -s "Compare these two approaches" -t 2
125
138
  palabre codex-claude "Review this architecture" --context src docs
126
139
  palabre claude-ollama "Review this file" --files README.md
127
140
  palabre codex-claude "Preview" --context src --show-prompt
141
+ palabre context scan src docs --json
128
142
  ```
129
143
 
130
144
  ### Supported Agents
@@ -138,10 +152,22 @@ palabre codex-claude "Preview" --context src --show-prompt
138
152
 
139
153
  PALABRE does not list models: they change often and depend on each CLI or user account. `--model-a`, `--model-b`, and `--summary-model` simply pass the raw value to the selected agent.
140
154
 
155
+ ### Integrations
156
+
157
+ PALABRE exposes versioned JSON outputs for external clients:
158
+
159
+ - `palabre presets --json` to read available agent pairs;
160
+ - `palabre context scan --json` to preview the context `--context` would retain;
161
+ - `--renderer ndjson` or `--json` to follow a debate event by event.
162
+
163
+ The NDJSON v1 stream is treated as a public integration API. Compatible additions do not break v1; breaking changes must change the `v` field.
164
+
141
165
  ### Privacy
142
166
 
143
167
  PALABRE runs locally and does not send data to a PALABRE-owned server. Data sent to agents depends on the tools you use: check the privacy policies of Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode, Ollama, or any custom agent you configure.
144
168
 
169
+ If an agent fails during the debate or final summary, PALABRE keeps the partial Markdown export with an interruption section whenever possible.
170
+
145
171
  ### Local Development
146
172
 
147
173
  ```bash
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { AdapterError } from "../errors.js";
5
5
  import { formatAgentPrompt } from "../prompt.js";
6
6
  import { cleanTerminalOutput } from "./terminal.js";
7
+ const DEFAULT_MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
7
8
  /**
8
9
  * Adapter pour les CLIs qui exigent un vrai terminal.
9
10
  * Contrairement à `CliAdapter`, stdout/stderr sont fusionnés dans le flux PTY.
@@ -45,11 +46,13 @@ export class CliPtyAdapter {
45
46
  : baseArgs;
46
47
  return new Promise((resolve, reject) => {
47
48
  let output = "";
49
+ let outputBytes = 0;
48
50
  let settled = false;
49
51
  let hardTimer;
50
52
  let term;
51
53
  let dataSubscription;
52
54
  let exitSubscription;
55
+ const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
53
56
  const finish = (error, exitCode, kill = true) => {
54
57
  if (settled)
55
58
  return;
@@ -109,6 +112,14 @@ export class CliPtyAdapter {
109
112
  }));
110
113
  }, this.config.timeoutMs ?? 180_000);
111
114
  dataSubscription = term.onData((chunk) => {
115
+ outputBytes += Buffer.byteLength(chunk, "utf8");
116
+ if (outputBytes > maxOutputBytes) {
117
+ finish(new AdapterError("output-too-large", this.name, `${this.name} produced more than ${maxOutputBytes} bytes of PTY output`, {
118
+ maxOutputBytes,
119
+ outputBytes
120
+ }));
121
+ return;
122
+ }
112
123
  output += chunk;
113
124
  });
114
125
  exitSubscription = term.onExit(({ exitCode }) => {
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { AdapterError } from "../errors.js";
3
3
  import { formatAgentPrompt } from "../prompt.js";
4
4
  import { cleanTerminalOutput } from "./terminal.js";
5
+ const DEFAULT_MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
5
6
  /**
6
7
  * Adapter pour les CLIs batch (Codex, Claude, Gemini…).
7
8
  * Lance un sous-processus, injecte le prompt via stdin ou argument, capture stdout.
@@ -50,8 +51,10 @@ export class CliAdapter {
50
51
  let stdout = "";
51
52
  let stderr = "";
52
53
  let settled = false;
54
+ let outputBytes = 0;
53
55
  let hardTimer;
54
56
  let idleTimer;
57
+ const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
55
58
  const finish = (error) => {
56
59
  if (settled)
57
60
  return;
@@ -94,10 +97,28 @@ export class CliAdapter {
94
97
  };
95
98
  bumpIdleTimer();
96
99
  child.stdout.on("data", (chunk) => {
100
+ outputBytes += chunk.length;
101
+ if (outputBytes > maxOutputBytes) {
102
+ child.kill();
103
+ finish(new AdapterError("output-too-large", this.name, `${this.name} produced more than ${maxOutputBytes} bytes of output`, {
104
+ maxOutputBytes,
105
+ outputBytes
106
+ }));
107
+ return;
108
+ }
97
109
  stdout += chunk.toString("utf8");
98
110
  bumpIdleTimer();
99
111
  });
100
112
  child.stderr.on("data", (chunk) => {
113
+ outputBytes += chunk.length;
114
+ if (outputBytes > maxOutputBytes) {
115
+ child.kill();
116
+ finish(new AdapterError("output-too-large", this.name, `${this.name} produced more than ${maxOutputBytes} bytes of output`, {
117
+ maxOutputBytes,
118
+ outputBytes
119
+ }));
120
+ return;
121
+ }
101
122
  stderr += chunk.toString("utf8");
102
123
  bumpIdleTimer();
103
124
  });
@@ -0,0 +1,48 @@
1
+ import path from "node:path";
2
+ import { loadProjectInputs } from "./context.js";
3
+ import { createTranslator } from "./i18n.js";
4
+ /**
5
+ * Builds the machine-readable context preview used by integrations.
6
+ *
7
+ * The scan intentionally reuses the same tolerant loader as `--context`, so
8
+ * the returned files are the files Palabre would actually inject into a debate.
9
+ */
10
+ export async function buildContextScan(scanPaths, cwd = process.cwd(), messages = createTranslator("fr")) {
11
+ const effectiveScanPaths = scanPaths.length > 0 ? scanPaths : ["."];
12
+ const result = await loadProjectInputs([], effectiveScanPaths, cwd, messages);
13
+ const files = result.files.map((file) => ({
14
+ kind: "file",
15
+ path: file.path,
16
+ absolutePath: file.absolutePath,
17
+ sizeBytes: file.sizeBytes
18
+ }));
19
+ const folders = collectContextFolders(files.map((file) => file.path), cwd);
20
+ return {
21
+ v: 1,
22
+ root: cwd,
23
+ scanned: effectiveScanPaths,
24
+ items: [...folders, ...files],
25
+ warnings: result.warnings
26
+ };
27
+ }
28
+ function collectContextFolders(filePaths, cwd) {
29
+ const counts = new Map();
30
+ if (filePaths.length > 0) {
31
+ counts.set(".", filePaths.length);
32
+ }
33
+ for (const filePath of filePaths) {
34
+ const parts = filePath.split("/").filter(Boolean);
35
+ for (let index = 1; index < parts.length; index += 1) {
36
+ const folder = parts.slice(0, index).join("/");
37
+ counts.set(folder, (counts.get(folder) ?? 0) + 1);
38
+ }
39
+ }
40
+ return [...counts.entries()]
41
+ .sort(([left], [right]) => left === "." ? -1 : right === "." ? 1 : left.localeCompare(right))
42
+ .map(([folder, filesCount]) => ({
43
+ kind: "folder",
44
+ path: folder,
45
+ absolutePath: path.resolve(cwd, folder),
46
+ filesCount
47
+ }));
48
+ }
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { configExists, createConfigFromDiscovery, DEFAULT_CONFIG_PATH, GLOBAL_CONFIG_PATH, loadConfig, resolveDefaultConfigPath, resolveOutputDir, writeExampleConfig } from "./config.js";
6
6
  import { loadProjectInputs } from "./context.js";
7
+ import { buildContextScan } from "./contextScan.js";
7
8
  import { discoverLocalTools } from "./discovery.js";
8
9
  import { runDoctor } from "./doctor.js";
9
10
  import { AdapterError, formatAdapterError } from "./errors.js";
@@ -51,6 +52,10 @@ async function main() {
51
52
  await runPresetsCommand(parsed.flags);
52
53
  return;
53
54
  }
55
+ if (parsed.command === "context") {
56
+ await runContextCommand(parsed.flags, parsed.positionals);
57
+ return;
58
+ }
54
59
  if (parsed.command === "update") {
55
60
  const info = await getUpdateInfo(await getPackageVersion());
56
61
  const updateConfigPath = optionalString(parsed.flags.config) ?? await resolveDefaultConfigPath();
@@ -168,8 +173,11 @@ async function main() {
168
173
  const renderer = createRendererFromFlags(parsed.flags, options.plainOutput, messages);
169
174
  context.warnings.forEach((warning) => renderer.warning(warning));
170
175
  const result = await runDebate(config, options, renderer, messages);
171
- const outputPath = await writeDebateMarkdown(resolveOutputDir(config.outputDir), result.options, result.messages, result.summary, result.stopReason, messages);
176
+ const outputPath = await writeDebateMarkdown(resolveOutputDir(config.outputDir), result.options, result.messages, result.summary, result.stopReason, messages, result.failure);
172
177
  renderer.done(outputPath);
178
+ if (result.failure) {
179
+ process.exitCode = 1;
180
+ }
173
181
  }
174
182
  /**
175
183
  * Exécute la commande `agents` : charge la config et affiche les agents déclarés avec leur état de détection.
@@ -436,6 +444,31 @@ async function runPresetsCommand(flags) {
436
444
  console.log("");
437
445
  console.log(messages.presets.total(presets.length));
438
446
  }
447
+ async function runContextCommand(flags, positionals) {
448
+ const language = resolveLanguage({ explicitLanguage: optionalString(flags.language) });
449
+ const messages = createTranslator(language);
450
+ const subcommand = positionals[0] ?? "scan";
451
+ if (subcommand !== "scan") {
452
+ throw new Error(messages.common.unknownCommand(`context ${subcommand}`, "context scan"));
453
+ }
454
+ const paths = positionals.slice(1);
455
+ const result = await buildContextScan(paths, process.cwd(), messages);
456
+ const folders = result.items.filter((item) => item.kind === "folder");
457
+ const files = result.items.filter((item) => item.kind === "file");
458
+ if (flags.json) {
459
+ console.log(JSON.stringify(result, null, 2));
460
+ return;
461
+ }
462
+ for (const folder of folders) {
463
+ console.log(`[folder] ${folder.path}`);
464
+ }
465
+ for (const file of files) {
466
+ console.log(`[file] ${file.path} (${file.sizeBytes} bytes)`);
467
+ }
468
+ for (const warning of result.warnings) {
469
+ console.error(`${messages.renderers.warningPrefix} ${warning}`);
470
+ }
471
+ }
439
472
  /**
440
473
  * Parse `process.argv` en une structure typée `ParsedArgs`.
441
474
  * Gère les flags courts (-h, -v, -s, -t, -a), les flags longs (--topic, --agent-a…),
@@ -448,7 +481,7 @@ function parseArgs(args, messages) {
448
481
  let command = "run";
449
482
  let commandExplicit = false;
450
483
  const positionals = [];
451
- const commands = new Set(["run", "new", "init", "setup", "help", "version", "update", "doctor", "config", "agent", "agents", "preset", "presets"]);
484
+ const commands = new Set(["run", "new", "init", "setup", "help", "version", "update", "doctor", "config", "agent", "agents", "preset", "presets", "context"]);
452
485
  const presets = new Set(listPresetNames());
453
486
  for (let index = 0; index < args.length; index += 1) {
454
487
  const value = args[index];
@@ -545,7 +578,7 @@ function parseArgs(args, messages) {
545
578
  if (command === "run") {
546
579
  applyRunPositionals(positionals, flags, presets, commandExplicit, commands, messages);
547
580
  }
548
- return { command, commandExplicit, flags };
581
+ return { command, commandExplicit, positionals, flags };
549
582
  }
550
583
  /**
551
584
  * Détecte si une valeur ressemble à une faute de frappe d'une commande connue
@@ -3,6 +3,7 @@ const frHints = {
3
3
  "spawn-failed": "Sur Windows, essaye le wrapper .cmd ou active \"shell\": true dans la config agent.",
4
4
  timeout: "Augmente timeoutMs ou teste la commande directement dans le terminal.",
5
5
  "idle-timeout": "Desactive idleTimeoutMs pour les CLIs IA qui restent silencieuses pendant la generation.",
6
+ "output-too-large": "Reduis le contexte, le nombre de tours ou configure maxOutputBytes pour cet agent si ce volume est attendu.",
6
7
  "empty-output": "Teste la commande en dehors de Palabre et verifie que le prompt est bien lu via stdin ou argument.",
7
8
  "usage-limit": "Attends la fenetre indiquee par la CLI, change de modele ou relance avec un autre agent/preset disponible.",
8
9
  "non-zero-exit": "Teste la commande directement, puis ajuste args, permissions, modele ou authentification de la CLI.",
@@ -16,6 +17,7 @@ const enHints = {
16
17
  "spawn-failed": "On Windows, try the .cmd wrapper or enable \"shell\": true in the agent config.",
17
18
  timeout: "Increase timeoutMs or test the command directly in the terminal.",
18
19
  "idle-timeout": "Disable idleTimeoutMs for AI CLIs that stay silent while generating.",
20
+ "output-too-large": "Reduce context, turn count, or configure maxOutputBytes for this agent if this volume is expected.",
19
21
  "empty-output": "Test the command outside Palabre and check that the prompt is read through stdin or an argument.",
20
22
  "usage-limit": "Wait for the window indicated by the CLI, change model, or run again with another available agent/preset.",
21
23
  "non-zero-exit": "Test the command directly, then adjust args, permissions, model, or CLI authentication.",
@@ -29,6 +29,16 @@ Usage:
29
29
  Flags:
30
30
  --json sortie structuree pour integrations
31
31
  --config <path> chemin de config explicite
32
+ `,
33
+ context: `
34
+ Scanne le contexte projet avec les memes regles que --context.
35
+
36
+ Usage:
37
+ palabre context scan [paths...] [flags]
38
+
39
+ Flags:
40
+ --json sortie structuree pour integrations
41
+ --language <fr|en> force la langue des avertissements
32
42
  `,
33
43
  config: `
34
44
  Configure les agents par defaut, la synthese, le nombre de reponses et la langue.
@@ -124,6 +134,16 @@ Usage:
124
134
  Flags:
125
135
  --json structured output for integrations
126
136
  --config <path> explicit config path
137
+ `,
138
+ context: `
139
+ Scans project context with the same rules as --context.
140
+
141
+ Usage:
142
+ palabre context scan [paths...] [flags]
143
+
144
+ Flags:
145
+ --json structured output for integrations
146
+ --language <fr|en> forces warning language
127
147
  `,
128
148
  config: `
129
149
  Configures default agents, summary, response count, and language.
@@ -212,6 +232,7 @@ Commandes:
212
232
  new Assistant interactif de debat
213
233
  agents Lister les agents configures
214
234
  presets Lister les presets disponibles
235
+ context Scanner le contexte projet
215
236
  config Modifier les parametres par defaut
216
237
  doctor Verifier la config et les outils locaux
217
238
  update Afficher ou appliquer les etapes de mise a jour
@@ -256,6 +277,7 @@ Commands:
256
277
  new Interactive debate assistant
257
278
  agents List configured agents
258
279
  presets List available presets
280
+ context Scan project context
259
281
  config Edit default settings
260
282
  doctor Check config and local tools
261
283
  update Show or apply update steps
@@ -3,6 +3,7 @@ export const outputMessages = {
3
3
  title: "# PALABRE Debate",
4
4
  contextTitle: "## Contexte",
5
5
  exchangesTitle: "## Echanges",
6
+ failureTitle: "## Interruption",
6
7
  finalSummaryTitle: "## Synthese finale",
7
8
  tableField: "Champ",
8
9
  tableValue: "Valeur",
@@ -27,13 +28,19 @@ export const outputMessages = {
27
28
  sessionStartedAt: "Session demarree a",
28
29
  agent: "Agent",
29
30
  role: "Role",
30
- date: "Date"
31
+ date: "Date",
32
+ failurePhase: "Phase",
33
+ failureAgent: "Agent",
34
+ failureTurn: "Tour",
35
+ failureKind: "Type d'erreur",
36
+ failureMessage: "Message"
31
37
  }
32
38
  },
33
39
  en: {
34
40
  title: "# PALABRE Debate",
35
41
  contextTitle: "## Context",
36
42
  exchangesTitle: "## Exchanges",
43
+ failureTitle: "## Interruption",
37
44
  finalSummaryTitle: "## Final summary",
38
45
  tableField: "Field",
39
46
  tableValue: "Value",
@@ -58,7 +65,12 @@ export const outputMessages = {
58
65
  sessionStartedAt: "Session started at",
59
66
  agent: "Agent",
60
67
  role: "Role",
61
- date: "Date"
68
+ date: "Date",
69
+ failurePhase: "Phase",
70
+ failureAgent: "Agent",
71
+ failureTurn: "Turn",
72
+ failureKind: "Error kind",
73
+ failureMessage: "Message"
62
74
  }
63
75
  }
64
76
  };
@@ -1,4 +1,5 @@
1
1
  import { createAgent } from "./adapters/index.js";
2
+ import { AdapterError } from "./errors.js";
2
3
  import { createTranslator } from "./i18n.js";
3
4
  /**
4
5
  * Point d'entrée de l'orchestration.
@@ -36,18 +37,39 @@ export async function runDebate(config, options, renderer, messages = createTran
36
37
  const turn = index + 1;
37
38
  renderer?.turnStart(turn, options.turns, current.name, current.role);
38
39
  renderer?.thinkingStart(current.name, current.role);
39
- const response = await current.generate({
40
- topic: options.topic,
41
- turn,
42
- totalTurns: options.turns,
43
- selfName: current.name,
44
- peerName: peer.name,
45
- selfRole: current.role,
46
- language: options.language,
47
- session: options.session,
48
- files: options.files,
49
- transcript
50
- }).finally(() => renderer?.thinkingEnd());
40
+ let response;
41
+ try {
42
+ response = await current.generate({
43
+ topic: options.topic,
44
+ turn,
45
+ totalTurns: options.turns,
46
+ selfName: current.name,
47
+ peerName: peer.name,
48
+ selfRole: current.role,
49
+ language: options.language,
50
+ session: options.session,
51
+ files: options.files,
52
+ transcript
53
+ });
54
+ }
55
+ catch (error) {
56
+ const failure = toDebateFailure(error, {
57
+ phase: "debate",
58
+ agent: current.name,
59
+ role: current.role,
60
+ turn
61
+ });
62
+ renderer?.error(failure);
63
+ return {
64
+ options,
65
+ messages: transcript,
66
+ stopReason,
67
+ failure
68
+ };
69
+ }
70
+ finally {
71
+ renderer?.thinkingEnd();
72
+ }
51
73
  const message = {
52
74
  agent: current.name,
53
75
  role: current.role,
@@ -62,14 +84,27 @@ export async function runDebate(config, options, renderer, messages = createTran
62
84
  break;
63
85
  }
64
86
  }
65
- const summary = options.summaryEnabled
66
- ? await generateSummary(config, options, transcript, renderer, messages)
67
- : undefined;
87
+ let summary;
88
+ let failure;
89
+ if (options.summaryEnabled) {
90
+ try {
91
+ summary = await generateSummary(config, options, transcript, renderer, messages);
92
+ }
93
+ catch (error) {
94
+ failure = toDebateFailure(error, {
95
+ phase: "summary",
96
+ agent: options.summaryAgent ?? options.agentB,
97
+ turn: transcript.length + 1
98
+ });
99
+ renderer?.error(failure);
100
+ }
101
+ }
68
102
  return {
69
103
  options,
70
104
  messages: transcript,
71
105
  summary,
72
- stopReason
106
+ stopReason,
107
+ failure
73
108
  };
74
109
  }
75
110
  /**
@@ -164,6 +199,27 @@ async function generateSummary(config, options, transcript, renderer, messages =
164
199
  renderer?.message(summary.content);
165
200
  return summary;
166
201
  }
202
+ function toDebateFailure(error, context) {
203
+ if (error instanceof AdapterError) {
204
+ return {
205
+ phase: context.phase,
206
+ agent: context.agent ?? error.adapterName,
207
+ role: context.role,
208
+ turn: context.turn,
209
+ kind: error.kind,
210
+ message: error.message,
211
+ details: error.details
212
+ };
213
+ }
214
+ return {
215
+ phase: context.phase,
216
+ agent: context.agent,
217
+ role: context.role,
218
+ turn: context.turn,
219
+ kind: "unknown",
220
+ message: error instanceof Error ? error.message : String(error)
221
+ };
222
+ }
167
223
  /** Résout le model override pour un agent donné. Retourne `undefined` si l'agent n'est ni A ni B. */
168
224
  function modelForAgent(options, agent) {
169
225
  if (agent === options.agentA) {
package/dist/output.js CHANGED
@@ -5,12 +5,12 @@ import { createTranslator } from "./i18n.js";
5
5
  * Écrit le débat au format Markdown dans `outputDir`.
6
6
  * Crée le répertoire si absent. Retourne le chemin absolu du fichier créé.
7
7
  */
8
- export async function writeDebateMarkdown(outputDir, options, debateMessages, summary, stopReason, messages = createTranslator("fr")) {
8
+ export async function writeDebateMarkdown(outputDir, options, debateMessages, summary, stopReason, messages = createTranslator("fr"), failure) {
9
9
  const safeDate = new Date().toISOString().replace(/[:.]/g, "-");
10
10
  const fileName = `palabre-${slugifyTopic(options.topic)}-${safeDate}.debate.md`;
11
11
  const filePath = path.resolve(outputDir, fileName);
12
12
  await mkdir(path.dirname(filePath), { recursive: true });
13
- await writeFile(filePath, renderDebateMarkdown(options, debateMessages, summary, stopReason, messages), "utf8");
13
+ await writeFile(filePath, renderDebateMarkdown(options, debateMessages, summary, stopReason, messages, failure), "utf8");
14
14
  return filePath;
15
15
  }
16
16
  function slugifyTopic(topic) {
@@ -28,7 +28,7 @@ function slugifyTopic(topic) {
28
28
  * Produit la représentation Markdown complète du débat.
29
29
  * Fonction pure : aucun effet de bord sur le filesystem.
30
30
  */
31
- export function renderDebateMarkdown(options, debateMessages, summary, stopReason, messages = createTranslator("fr")) {
31
+ export function renderDebateMarkdown(options, debateMessages, summary, stopReason, messages = createTranslator("fr"), failure) {
32
32
  const lines = [
33
33
  messages.output.title,
34
34
  "",
@@ -44,6 +44,9 @@ export function renderDebateMarkdown(options, debateMessages, summary, stopReaso
44
44
  for (const message of debateMessages) {
45
45
  lines.push(`### ${message.agent} (${message.role})`, "", normalizeMarkdownForWindowsPreview(message.content.trim()), "");
46
46
  }
47
+ if (failure) {
48
+ lines.push("---", "", messages.output.failureTitle, "", ...renderFailureBlock(failure, messages), "");
49
+ }
47
50
  lines.push("---", "", messages.output.finalSummaryTitle, "", ...renderSummaryBlock(options, summary, messages));
48
51
  return `${lines.join("\n")}\n`;
49
52
  }
@@ -67,6 +70,20 @@ function renderSummaryBlock(options, summary, messages) {
67
70
  ""
68
71
  ];
69
72
  }
73
+ function renderFailureBlock(failure, messages) {
74
+ const rows = [
75
+ [messages.output.fields.failurePhase, failure.phase],
76
+ [messages.output.fields.failureAgent, failure.agent ?? messages.output.no],
77
+ [messages.output.fields.failureTurn, failure.turn === undefined ? messages.output.no : String(failure.turn)],
78
+ [messages.output.fields.failureKind, failure.kind],
79
+ [messages.output.fields.failureMessage, failure.message]
80
+ ];
81
+ return [
82
+ `| ${messages.output.tableField} | ${messages.output.tableValue} |`,
83
+ "| --- | --- |",
84
+ ...rows.map(([label, value]) => `| ${escapeTableCell(label)} | ${escapeTableCell(value)} |`)
85
+ ];
86
+ }
70
87
  function normalizeMarkdownForWindowsPreview(content) {
71
88
  return content.replace(/:\*\*/g, "&#58;**");
72
89
  }
@@ -99,6 +99,10 @@ class PrettyConsoleRenderer {
99
99
  ""
100
100
  ].join("\n"));
101
101
  }
102
+ error(failure) {
103
+ this.thinkingEnd();
104
+ process.stderr.write(`\n${this.c("red", this.messages.common.errorPrefix)} ${formatFailureLocation(failure, this.messages)}: ${failure.message}\n`);
105
+ }
102
106
  /** Affiche le chemin du fichier de sortie en vert à la fin du débat. */
103
107
  done(outputPath) {
104
108
  process.stdout.write(`\n\n${this.c("green", this.messages.renderers.exported(outputPath))}\n\n`);
@@ -175,6 +179,9 @@ class PlainConsoleRenderer {
175
179
  summaryStart(agent, role) {
176
180
  process.stdout.write(`\n[${this.messages.renderers.summaryTitle}] ${agent} (${role})...\n`);
177
181
  }
182
+ error(failure) {
183
+ process.stderr.write(`\n${this.messages.common.errorPrefix}: ${formatFailureLocation(failure, this.messages)}: ${failure.message}\n`);
184
+ }
178
185
  /** Affiche le chemin du fichier de sortie à la fin du débat. */
179
186
  done(outputPath) {
180
187
  process.stdout.write(`\n${this.messages.renderers.exported(outputPath)}\n`);
@@ -220,6 +227,13 @@ function formatContext(options, messages) {
220
227
  }
221
228
  return messages.renderers.injectedFiles(count);
222
229
  }
230
+ function formatFailureLocation(failure, messages) {
231
+ if (failure.phase === "summary") {
232
+ return messages.renderers.summaryTitle;
233
+ }
234
+ const turn = failure.turn === undefined ? "" : `, turn ${failure.turn}`;
235
+ return `${failure.agent ?? "?"} (${failure.role ?? "?"}${turn})`;
236
+ }
223
237
  /** Codes d'échappement ANSI utilisés par `PrettyConsoleRenderer`. */
224
238
  const codes = {
225
239
  reset: "\u001b[0m",
@@ -228,6 +242,7 @@ const codes = {
228
242
  cyan: "\u001b[36m",
229
243
  green: "\u001b[32m",
230
244
  magenta: "\u001b[35m",
245
+ red: "\u001b[31m",
231
246
  yellow: "\u001b[33m",
232
247
  orange: "\u001b[38;5;208m",
233
248
  pink: "\u001b[38;5;205m"
@@ -101,6 +101,10 @@ export class NdjsonRenderer {
101
101
  this.currentRole = role;
102
102
  this.emit({ type: "summary-start", agent, role });
103
103
  }
104
+ /** Émet une erreur runtime structurée. */
105
+ error(failure) {
106
+ this.emit({ type: "error", ...failure });
107
+ }
104
108
  /** Émet `done` avec le chemin du `.debate.md` écrit. */
105
109
  done(outputPath) {
106
110
  this.emit({ type: "done", outputPath });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palabre",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Orchestrateur de debat entre agents IA locaux, CLIs et Ollama.",
5
5
  "license": "MIT",
6
6
  "type": "module",