@dotdrelle/wiki-manager 0.11.4 → 0.11.7

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.
@@ -95,7 +95,7 @@ services:
95
95
  - MAILER_REQUIRE_CONFIRMATION=${MAILER_REQUIRE_CONFIRMATION:-true}
96
96
  - MAILER_DRY_RUN=${MAILER_DRY_RUN:-false}
97
97
  - MCP_AUTH_TOKEN=${MAILER_MCP_AUTH_TOKEN:-}
98
- volumes:
99
- # Optional: mount a CA bundle and set MAILERSEND_CA_CERT to its container path.
100
- #- ${AGENTS_DATA_DIR:-./.agents-data}/certs:/certs:ro
98
+ # Optional: mount a CA bundle and set MAILERSEND_CA_CERT to its container path.
99
+ # volumes:
100
+ # - ${AGENTS_DATA_DIR:-./.agents-data}/certs:/certs:ro
101
101
  restart: unless-stopped
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdrelle/wiki-manager",
3
- "version": "0.11.4",
3
+ "version": "0.11.7",
4
4
  "description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "author": "dotrelle",
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "scripts": {
13
13
  "start": "bun ./bin/wiki-manager.js",
14
- "test": "node --test src/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/agentEvents.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/wikiWorkspace.test.js src/core/wikirc.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/commands/slash.test.js src/shell/repl.test.js src/runtime/store.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/auth.test.js",
14
+ "test": "node --test src/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/agentEvents.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/wikiWorkspace.test.js src/core/wikirc.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/commands/slash.test.js src/shell/repl.test.js src/runtime/store.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/donna-contract.test.js src/runtime/auth.test.js",
15
15
  "check-versions": "node scripts/check-versions.js",
16
16
  "prepack": "node scripts/check-versions.js",
17
17
  "prepublishOnly": "node scripts/check-versions.js",
@@ -1,3 +1,4 @@
1
+ import { join } from 'node:path';
1
2
  import { Annotation, END, START, StateGraph } from '@langchain/langgraph';
2
3
  import {
3
4
  buildLlmTools,
@@ -6,14 +7,17 @@ import {
6
7
  formatMcpToolsForAgent,
7
8
  parseToolCallName,
8
9
  } from '../core/mcp.js';
9
- import { formatSkillsForAgent } from '../core/skills.js';
10
+ import { formatSkillsForAgent, readOptionalText } from '../core/skills.js';
10
11
  import { handleSlashCommand } from '../commands/slash.js';
11
12
  import { extractActivity, formatActivitySummary, parseJsonText } from '../core/activity.js';
12
13
  import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
13
14
  import { enqueueProductionJob, ensureJobQueue, formatQueue, productionLockBusy } from '../core/jobQueue.js';
15
+ import { updateWorkspaceProfilePreference } from '../core/profile.js';
14
16
 
15
17
  const MAX_TOOL_ITERATIONS = 80;
16
18
  const MAX_SPINNER_ARG_LENGTH = 96;
19
+ const MAX_PROFILE_CHARS = 4000;
20
+
17
21
  const AGENT_SLASH_COMMANDS = new Set([
18
22
  'help',
19
23
  'version',
@@ -51,6 +55,51 @@ const SHELL_RUN_COMMAND_TOOL = {
51
55
  },
52
56
  };
53
57
 
58
+ const SHELL_READ_COMMAND_TOOL = {
59
+ type: 'function',
60
+ function: {
61
+ name: 'shell__read_command',
62
+ description: [
63
+ 'Run a read-only deterministic wiki-manager slash command inside the current shell session.',
64
+ 'Allowed commands: /help, /version, /config, /config list, /config status, /status, /services, /skills, /skills list, /skills show <name>, /uploads, /uploads list, /queue.',
65
+ 'Do not use for workspace creation/deletion, uploads conversion, service start/stop, MCP calls, wiki runs, or any mutation.',
66
+ ].join(' '),
67
+ parameters: {
68
+ type: 'object',
69
+ additionalProperties: false,
70
+ properties: {
71
+ command: {
72
+ type: 'string',
73
+ description: 'Read-only slash command to run, for example "/status", "/config status", or "/services".',
74
+ },
75
+ },
76
+ required: ['command'],
77
+ },
78
+ },
79
+ };
80
+
81
+ const SHELL_PROFILE_UPDATE_TOOL = {
82
+ type: 'function',
83
+ function: {
84
+ name: 'shell__profile_update',
85
+ description: [
86
+ 'Append one explicit durable user preference to the current workspace .wiki/profile.md.',
87
+ 'Use when the user explicitly asks to remember, persist, note, or update profile information and wiki__profile_update is not available.',
88
+ ].join(' '),
89
+ parameters: {
90
+ type: 'object',
91
+ additionalProperties: false,
92
+ properties: {
93
+ preference: {
94
+ type: 'string',
95
+ description: 'The durable preference to append, without Markdown bullet syntax.',
96
+ },
97
+ },
98
+ required: ['preference'],
99
+ },
100
+ },
101
+ };
102
+
54
103
  const WIKI_PLAN_SET_TOOL = {
55
104
  type: 'function',
56
105
  function: {
@@ -238,6 +287,24 @@ function assertAgentSlashCommandAllowed(commandLine) {
238
287
  }
239
288
  }
240
289
 
290
+ function assertAgentReadSlashCommandAllowed(commandLine) {
291
+ const parts = commandLine.slice(1).trim().split(/\s+/).filter(Boolean);
292
+ const command = parts[0] ?? '';
293
+ const subcommand = parts[1] ?? '';
294
+ const allowed =
295
+ command === 'help' ||
296
+ command === 'version' ||
297
+ command === 'status' ||
298
+ command === 'services' ||
299
+ command === 'queue' ||
300
+ (command === 'config' && ['', 'list', 'status'].includes(subcommand)) ||
301
+ (command === 'skills' && ['', 'list', 'show'].includes(subcommand)) ||
302
+ (command === 'uploads' && ['', 'list'].includes(subcommand));
303
+ if (!allowed) {
304
+ throw new Error(`Read-only command is not available to the agent: /${parts.join(' ')}`);
305
+ }
306
+ }
307
+
241
308
  function withActiveWorkspaceForExternalTool(session, server, tool, args) {
242
309
  const needsWorkspace =
243
310
  (server === 'documents' && tool.startsWith('documents_') && tool !== 'documents_status') ||
@@ -269,6 +336,21 @@ async function runShellCommandTool(session, commandLine) {
269
336
  return result.output ?? 'Command completed.';
270
337
  }
271
338
 
339
+ async function runShellReadCommandTool(session, commandLine) {
340
+ const command = normalizeShellCommand(commandLine);
341
+ assertAgentReadSlashCommandAllowed(command);
342
+ session._onStep?.(`Shell: ${command}`);
343
+ const result = await handleSlashCommand(command, {
344
+ packageJson: session.packageJson ?? { version: '0.0.0' },
345
+ session,
346
+ onStep: session._onStep,
347
+ });
348
+ if (result.exit) {
349
+ throw new Error('/exit is not available to the agent.');
350
+ }
351
+ return result.output ?? 'Command completed.';
352
+ }
353
+
272
354
  function rememberProductionProgress(session, payload, label) {
273
355
  const job = payload?.job;
274
356
  const jobId = payload?.jobId ?? job?.jobId;
@@ -423,6 +505,18 @@ function selectExecutorForStep(description, session) {
423
505
  return fallback;
424
506
  }
425
507
 
508
+ // The manager runs on the same host filesystem as the workspace directory
509
+ // (this is the same local file wiki__profile_update writes to via its
510
+ // volume-mounted container), so read it fresh on every turn instead of
511
+ // relying on the model proactively calling wiki__profile_read — profile
512
+ // content (tutoiement, formatting preferences, etc.) is meant to shape every
513
+ // reply, not just ones where the model happens to think to check it.
514
+ function loadWorkspaceProfile(workspacePath) {
515
+ if (!workspacePath) return null;
516
+ const content = readOptionalText(join(workspacePath, '.wiki', 'profile.md'));
517
+ return content ? content.slice(0, MAX_PROFILE_CHARS) : null;
518
+ }
519
+
426
520
  export function buildAgentSystemPrompt(state) {
427
521
  const workspace = state.session.workspace ?? 'no workspace selected';
428
522
  const wikirc = state.session.wikirc?.profile ?? 'no profile loaded';
@@ -430,10 +524,12 @@ export function buildAgentSystemPrompt(state) {
430
524
  const mcpTools = formatMcpToolsForAgent(state.session.mcp);
431
525
  const skills = formatSkillsForAgent(state.session);
432
526
  const customPrompt = state.session.systemPrompt ?? null;
527
+ const workspaceProfile = loadWorkspaceProfile(state.session.workspacePath);
433
528
 
434
529
  const agentContext = [
435
530
  'You are Donna, the terminal orchestrator agent for llm-wiki-manager.',
436
531
  'The shell is agent-first: every input without a leading slash is routed to you.',
532
+ 'Default to a plain conversational reply with no tool call. Only call a tool, create a plan, or start a job when the user\'s message clearly requests an action (ingest, build, export, configure, run a skill, check a concrete status, etc.). Greetings, small talk, thanks, and general questions do not warrant starting a job or calling a tool — just answer in text.',
437
533
  'Commands starting with / are deterministic primitives. You may run a safe subset through shell__run_command.',
438
534
  `Reply language: ${language}.`,
439
535
  `Current workspace: ${workspace}.`,
@@ -488,7 +584,11 @@ export function buildAgentSystemPrompt(state) {
488
584
  'If production_start_job is returned as queued/waiting by the manager, report that it is waiting in the local queue and return control. Do not continue as if the production job has started.',
489
585
  'For diagnostics, use /wiki run doctor when the user asks for doctor. Use /workspace init <name> [path] for low-level non-interactive workspace creation. In the interactive TUI, /new <name> opens the setup wizard. Use /wiki for index, or /wiki run index through the explicit backup hatch. Use /wiki run init only for explicit current-workspace llm-wiki init.',
490
586
  'If an action requires tools or skills not available yet, explain the limitation and name the expected primitive.',
491
- ].join('\n');
587
+ workspaceProfile
588
+ ? `Workspace profile (.wiki/profile.md) — durable user preferences, apply these to every reply (tone, tutoiement/vouvoiement, formatting, etc.):\n${workspaceProfile}`
589
+ : null,
590
+ 'When the user explicitly asks you to remember, persist, or update durable preference/profile information, call wiki__profile_update when it is available; otherwise call shell__profile_update. Do not just acknowledge in text without calling a profile update tool.',
591
+ ].filter(Boolean).join('\n');
492
592
 
493
593
  return customPrompt ? `${customPrompt}\n\n${agentContext}` : agentContext;
494
594
  }
@@ -512,12 +612,17 @@ export function buildLimitedAgentResponse(state, reason = 'no workspace loaded w
512
612
  ].join('\n');
513
613
  }
514
614
 
615
+ export function formatLlmUnavailableMessage(reason) {
616
+ const clean = String(reason ?? 'raison inconnue').replace(/\s+/g, ' ').trim();
617
+ return `⚠ LLM injoignable : ${clean || 'raison inconnue'}`;
618
+ }
619
+
515
620
  export function createAgentGraph(options = {}) {
516
621
  async function orchestratorNode(state) {
517
622
  const llm = state.session.llm ?? options.llm ?? null;
518
623
 
519
624
  if (!llm) {
520
- return { response: buildLimitedAgentResponse(state), pendingToolCalls: null, readyToStream: false };
625
+ return { response: formatLlmUnavailableMessage('aucun client LLM configure'), pendingToolCalls: null, readyToStream: false };
521
626
  }
522
627
 
523
628
  const iterations = state.toolIterations ?? 0;
@@ -535,12 +640,15 @@ export function createAgentGraph(options = {}) {
535
640
  state.session._onStep?.('Agent: planning next action…');
536
641
  }
537
642
 
538
- const tools = [
643
+ const allTools = [
539
644
  SHELL_RUN_COMMAND_TOOL,
645
+ SHELL_READ_COMMAND_TOOL,
646
+ SHELL_PROFILE_UPDATE_TOOL,
540
647
  WIKI_PLAN_SET_TOOL,
541
648
  WIKI_PLAN_DONE_TOOL,
542
649
  ...buildLlmTools(state.session.mcp),
543
650
  ];
651
+ const tools = allTools;
544
652
  const system = buildAgentSystemPrompt(state);
545
653
 
546
654
  // On iteration 0: prior history is in state.messages, user input must be appended.
@@ -619,7 +727,7 @@ export function createAgentGraph(options = {}) {
619
727
  } catch (err) {
620
728
  if (err.name === 'AbortError') throw err;
621
729
  const message = err instanceof Error ? err.message : String(err);
622
- return { response: buildLimitedAgentResponse(state, `LLM unavailable: ${message}`), pendingToolCalls: null, readyToStream: false };
730
+ return { response: formatLlmUnavailableMessage(message), pendingToolCalls: null, readyToStream: false };
623
731
  }
624
732
  }
625
733
 
@@ -663,6 +771,11 @@ export function createAgentGraph(options = {}) {
663
771
  } else if (server === 'shell' && tool === 'run_command') {
664
772
  await awaitRunApproval(state.session, { runId, tool: toolName });
665
773
  resultText = await runShellCommandTool(state.session, args.command);
774
+ } else if (server === 'shell' && tool === 'read_command') {
775
+ resultText = await runShellReadCommandTool(state.session, args.command);
776
+ } else if (server === 'shell' && tool === 'profile_update') {
777
+ const result = await updateWorkspaceProfilePreference(state.session, args.preference);
778
+ resultText = JSON.stringify(result, null, 2);
666
779
  } else if (server !== 'shell') {
667
780
  await awaitRunApproval(state.session, { runId, tool: toolName });
668
781
  await awaitToolApproval(state.session, {
@@ -1,6 +1,9 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
- import { createAgentGraph } from './graph.js';
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { buildAgentSystemPrompt, createAgentGraph } from './graph.js';
4
7
 
5
8
  function sessionBase(overrides = {}) {
6
9
  return {
@@ -99,6 +102,160 @@ test('agent graph waits for run-level approval before first MCP action', async (
99
102
  }
100
103
  });
101
104
 
105
+ test('agent graph reports LLM unavailable without Donna active boilerplate', async () => {
106
+ const agent = createAgentGraph();
107
+ const result = await agent.invoke({ input: 'salut', session: sessionBase({ llm: null }) });
108
+
109
+ assert.equal(result.response, '⚠ LLM injoignable : aucun client LLM configure');
110
+ assert.doesNotMatch(result.response, /Donna is active/);
111
+ });
112
+
113
+ test('agent graph binds the full toolset and lets Donna decide whether to call tools', async () => {
114
+ const seenTools = [];
115
+ const session = sessionBase({
116
+ llm: {
117
+ async completeWithTools({ tools }) {
118
+ seenTools.push(...tools.map((tool) => tool.function.name));
119
+ return {
120
+ content: 'Salut, je suis là.',
121
+ message: { role: 'assistant', content: 'Salut, je suis là.' },
122
+ tool_calls: null,
123
+ };
124
+ },
125
+ },
126
+ });
127
+
128
+ const agent = createAgentGraph();
129
+ const result = await agent.invoke({ input: 'salut', session });
130
+
131
+ assert.equal(result.response, 'Salut, je suis là.');
132
+ assert.ok(seenTools.length > 0);
133
+ assert.ok(seenTools.includes('shell__read_command'));
134
+ assert.ok(seenTools.includes('shell__run_command'));
135
+ assert.ok(seenTools.includes('shell__profile_update'));
136
+ assert.ok(seenTools.includes('wiki__plan_set'));
137
+ assert.ok(seenTools.includes('wiki__plan_done'));
138
+ assert.equal(session.headlessPlan ?? null, null);
139
+ assert.equal(Object.keys(session.activities ?? {}).length, 0);
140
+ });
141
+
142
+ test('agent graph does not pre-filter mutating MCP tools for config questions', async () => {
143
+ const seenTools = [];
144
+ const session = sessionBase({
145
+ mcp: {
146
+ production: {
147
+ status: 'connected',
148
+ url: 'http://127.0.0.1:3000/mcp/',
149
+ tools: [
150
+ {
151
+ name: 'production_start_job',
152
+ description: 'Start production job',
153
+ inputSchema: { type: 'object', properties: { type: { type: 'string' } } },
154
+ },
155
+ {
156
+ name: 'production_job_status',
157
+ description: 'Read production job status',
158
+ inputSchema: { type: 'object', properties: { jobId: { type: 'string' } } },
159
+ },
160
+ ],
161
+ },
162
+ },
163
+ llm: {
164
+ async completeWithTools({ tools }) {
165
+ seenTools.push(...tools.map((tool) => tool.function.name));
166
+ return {
167
+ content: 'Le profil actif est docs.',
168
+ message: { role: 'assistant', content: 'Le profil actif est docs.' },
169
+ tool_calls: null,
170
+ };
171
+ },
172
+ },
173
+ });
174
+
175
+ const agent = createAgentGraph();
176
+ const result = await agent.invoke({ input: 'quel est le profil actif ?', session });
177
+
178
+ assert.equal(result.response, 'Le profil actif est docs.');
179
+ assert.ok(seenTools.includes('shell__read_command'));
180
+ assert.ok(seenTools.includes('production__production_job_status'));
181
+ assert.ok(seenTools.includes('wiki__plan_set'));
182
+ assert.ok(seenTools.includes('production__production_start_job'));
183
+ assert.ok(seenTools.includes('shell__run_command'));
184
+ });
185
+
186
+ test('agent graph binds the full toolset for a "remember my preference" request, not just read-only tools', async () => {
187
+ const seenTools = [];
188
+ const session = sessionBase({
189
+ mcp: {
190
+ wiki: {
191
+ status: 'connected',
192
+ url: 'http://127.0.0.1:3001/mcp/',
193
+ tools: [
194
+ {
195
+ name: 'profile_read',
196
+ description: 'Read the workspace profile from .wiki/profile.md.',
197
+ inputSchema: { type: 'object', properties: {} },
198
+ },
199
+ {
200
+ name: 'profile_update',
201
+ description: 'Write the workspace profile to .wiki/profile.md.',
202
+ inputSchema: { type: 'object', properties: { content: { type: 'string' } } },
203
+ },
204
+ ],
205
+ },
206
+ },
207
+ llm: {
208
+ async completeWithTools({ tools }) {
209
+ seenTools.push(...tools.map((tool) => tool.function.name));
210
+ return {
211
+ content: 'Noté, je retiens cette préférence.',
212
+ message: { role: 'assistant', content: 'Noté, je retiens cette préférence.' },
213
+ tool_calls: null,
214
+ };
215
+ },
216
+ },
217
+ });
218
+
219
+ const agent = createAgentGraph();
220
+ const result = await agent.invoke({ input: 'retiens que je préfère des réponses courtes', session });
221
+
222
+ assert.equal(result.response, 'Noté, je retiens cette préférence.');
223
+ // profile_update is a write tool (doesn't match the read-only name pattern)
224
+ // and "retiens" doesn't appear in the config/status read-only phrasing —
225
+ // without action-intent coverage for remember/save/update requests, this
226
+ // tool would silently never be offered to the LLM at all.
227
+ assert.ok(seenTools.includes('wiki__profile_update'));
228
+ assert.ok(seenTools.includes('wiki__profile_read'));
229
+ assert.ok(seenTools.includes('shell__profile_update'));
230
+ });
231
+
232
+ test('buildAgentSystemPrompt includes .wiki/profile.md content so preferences apply without a tool call', () => {
233
+ const workspacePath = mkdtempSync(join(tmpdir(), 'donna-profile-'));
234
+ mkdirSync(join(workspacePath, '.wiki'), { recursive: true });
235
+ writeFileSync(
236
+ join(workspacePath, '.wiki', 'profile.md'),
237
+ '# Workspace Profile\n\n## User Preferences\n\n- Tutoiement : me tutoyer\n',
238
+ );
239
+ try {
240
+ const prompt = buildAgentSystemPrompt({
241
+ session: sessionBase({ workspacePath }),
242
+ });
243
+ assert.match(prompt, /Tutoiement : me tutoyer/);
244
+ } finally {
245
+ rmSync(workspacePath, { recursive: true, force: true });
246
+ }
247
+ });
248
+
249
+ test('buildAgentSystemPrompt omits the profile section when profile.md is missing or empty', () => {
250
+ const workspacePath = mkdtempSync(join(tmpdir(), 'donna-profile-empty-'));
251
+ try {
252
+ const prompt = buildAgentSystemPrompt({ session: sessionBase({ workspacePath }) });
253
+ assert.doesNotMatch(prompt, /Workspace profile \(\.wiki\/profile\.md\)/);
254
+ } finally {
255
+ rmSync(workspacePath, { recursive: true, force: true });
256
+ }
257
+ });
258
+
102
259
  test('agent graph waits for tool-level approval configured on endpoint', async () => {
103
260
  const originalFetch = globalThis.fetch;
104
261
  globalThis.fetch = async () => ({
@@ -29,6 +29,11 @@ function valueAfter(argv, flag) {
29
29
  return argv[index + 1];
30
30
  }
31
31
 
32
+ function unavailableRuntime(err) {
33
+ const reason = err instanceof Error ? err.message : String(err);
34
+ return { url: null, error: reason };
35
+ }
36
+
32
37
  function createSession() {
33
38
  return {
34
39
  workspace: null,
@@ -745,7 +750,8 @@ export async function runCli(argv) {
745
750
  const { ensureRuntime } = await import('../runtime/lifecycle.js');
746
751
  runtime = await ensureRuntime();
747
752
  } catch (err) {
748
- console.error(`Runtime unavailable: ${err instanceof Error ? err.message : String(err)}`);
753
+ runtime = unavailableRuntime(err);
754
+ console.error(`Runtime unavailable: ${runtime.error}`);
749
755
  }
750
756
  await runOpenTuiShell({ agent, packageJson, runtime });
751
757
  return;
@@ -757,7 +763,8 @@ export async function runCli(argv) {
757
763
  const { ensureRuntime } = await import('../runtime/lifecycle.js');
758
764
  runtime = await ensureRuntime();
759
765
  } catch (err) {
760
- console.error(`Runtime unavailable: ${err instanceof Error ? err.message : String(err)}`);
766
+ runtime = unavailableRuntime(err);
767
+ console.error(`Runtime unavailable: ${runtime.error}`);
761
768
  }
762
769
  }
763
770
  await runShell({ agent, packageJson, runtime });
@@ -655,15 +655,38 @@ export function printHelp(packageJson) {
655
655
  console.log(helpText(packageJson));
656
656
  }
657
657
 
658
+ function rawCommandAgentPrompt(command, output) {
659
+ return [
660
+ `L'utilisateur a lancé la commande shell ${command}.`,
661
+ 'Voici la sortie brute collectée par la commande déterministe. Ne relance pas la commande et ne modifie pas les données.',
662
+ 'Réponds à l’utilisateur à partir de ces faits, en appliquant le profil workspace et les préférences de présentation déjà chargés dans ton prompt système.',
663
+ '',
664
+ 'Sortie brute:',
665
+ '```text',
666
+ output || '(empty)',
667
+ '```',
668
+ ].join('\n');
669
+ }
670
+
671
+ function rawCommandResult(command, output) {
672
+ return {
673
+ output,
674
+ rawOutput: true,
675
+ agentTrigger: rawCommandAgentPrompt(command, output),
676
+ };
677
+ }
678
+
658
679
  export async function handleSlashCommand(line, context) {
659
680
  const args = line.slice(1).trim().split(/\s+/).filter(Boolean);
660
681
  const [command] = args;
661
682
  const step = context.onStep ?? (() => {});
662
- const runAgentCommand = async (fn, verb) => {
683
+ const runAgentCommand = async (fn, verb, commandLabel) => {
663
684
  try {
664
685
  step(`Agents: ${verb}ing external agents…`);
665
686
  const output = await fn();
666
- return { output: output || `Agents ${verb}ed.` };
687
+ return output
688
+ ? rawCommandResult(commandLabel, output)
689
+ : { output: `Agents ${verb}ed.` };
667
690
  } catch (err) {
668
691
  step(formatActivityError('agents', verb, err));
669
692
  return { output: err instanceof Error ? err.message : String(err) };
@@ -808,7 +831,8 @@ export async function handleSlashCommand(line, context) {
808
831
  try {
809
832
  step('Services: reading compose state…');
810
833
  await refreshMcpRuntimeStatus(context.session);
811
- return { output: await listServices(context.session) };
834
+ const output = await listServices(context.session);
835
+ return rawCommandResult('/services', output);
812
836
  } catch (err) {
813
837
  const message = err instanceof Error ? err.message : String(err);
814
838
  step(formatActivityError('services', 'list', err));
@@ -821,13 +845,13 @@ export async function handleSlashCommand(line, context) {
821
845
  // do not remap it to undefined, that bypasses any custom "all" target list and always
822
846
  // falls back to the hardcoded COMPOSE_SERVICES constant instead.
823
847
  const service = args[1];
824
- if (service === 'agents') return runAgentCommand(startAgents, 'start');
848
+ if (service === 'agents') return runAgentCommand(startAgents, 'start', '/start agents');
825
849
  try {
826
850
  step(`Services: starting ${service ?? 'workspace services'}…`);
827
851
  const output = await startService(context.session, service);
828
852
  step('Services: refreshing MCP runtime…');
829
853
  await refreshMcpRuntimeStatus(context.session);
830
- return { output };
854
+ return rawCommandResult(`/start${service ? ` ${service}` : ''}`, output);
831
855
  } catch (err) {
832
856
  const message = err instanceof Error ? err.message : String(err);
833
857
  step(formatActivityError('services', 'stop', err));
@@ -836,13 +860,13 @@ export async function handleSlashCommand(line, context) {
836
860
  }
837
861
  case 'stop': {
838
862
  const service = args[1];
839
- if (service === 'agents') return runAgentCommand(stopAgents, 'stop');
863
+ if (service === 'agents') return runAgentCommand(stopAgents, 'stop', '/stop agents');
840
864
  try {
841
865
  step(`Services: stopping ${service ?? 'workspace services'}…`);
842
866
  const output = await stopService(context.session, service);
843
867
  step('Services: refreshing MCP runtime…');
844
868
  await refreshMcpRuntimeStatus(context.session);
845
- return { output };
869
+ return rawCommandResult(`/stop${service ? ` ${service}` : ''}`, output);
846
870
  } catch (err) {
847
871
  const message = err instanceof Error ? err.message : String(err);
848
872
  step(formatActivityError('services', 'logs', err));
@@ -854,7 +878,8 @@ export async function handleSlashCommand(line, context) {
854
878
  const tail = args[2] ? Number(args[2]) : 120;
855
879
  try {
856
880
  step(`Services: reading logs for ${service ?? 'service'}…`);
857
- return { output: await serviceLogs(context.session, service, { tail }) };
881
+ const output = await serviceLogs(context.session, service, { tail });
882
+ return rawCommandResult(`/logs ${[service, args[2]].filter(Boolean).join(' ')}`.trim(), output);
858
883
  } catch (err) {
859
884
  const message = err instanceof Error ? err.message : String(err);
860
885
  return { output: message };
@@ -903,7 +928,7 @@ export async function handleSlashCommand(line, context) {
903
928
  }
904
929
  const activity = formatMcpCallActivity(serverName, toolName, output);
905
930
  if (activity) step(activity);
906
- return { output };
931
+ return rawCommandResult(`/mcp call ${serverName} ${toolName}`, output);
907
932
  } catch (err) {
908
933
  const message = err instanceof Error ? err.message : String(err);
909
934
  step(formatActivityError(serverName, toolName, err));
@@ -1056,7 +1081,7 @@ export async function handleSlashCommand(line, context) {
1056
1081
  });
1057
1082
  const activity = formatActivitySummary('wiki', 'index', output);
1058
1083
  if (activity) step(activity);
1059
- return { output };
1084
+ return rawCommandResult('/wiki', output);
1060
1085
  } catch (err) {
1061
1086
  const message = err instanceof Error ? err.message : String(err);
1062
1087
  step(formatActivityError('wiki', 'index', err));
@@ -1073,7 +1098,7 @@ export async function handleSlashCommand(line, context) {
1073
1098
  });
1074
1099
  const activity = formatActivitySummary('wiki', wikiArgs[0] ?? 'run', output);
1075
1100
  if (activity) step(activity);
1076
- return { output };
1101
+ return rawCommandResult(`/wiki run ${wikiArgs.join(' ')}`, output);
1077
1102
  }
1078
1103
  return {
1079
1104
  output: [
@@ -1,8 +1,8 @@
1
- import { buildAgentSystemPrompt, buildLimitedAgentResponse } from '../agent/graph.js';
1
+ import { buildAgentSystemPrompt, formatLlmUnavailableMessage } from '../agent/graph.js';
2
2
  import { createAgentEvent, dispatchAgentEvent } from './agentEvents.js';
3
3
  import { activitySnapshot, newNonTerminalActivities } from './activity.js';
4
4
  import { extractHeadlessPlan, formatCompletedActivities, formatPlanStatus } from './plan.js';
5
- import { formatReadyTaskPrompt, nextReadyPlanTask, readyPlanTasks } from './planPatch.js';
5
+ import { formatReadyTaskPrompt, nextReadyPlanTask, readyPlanTasks, sanitizePlanForExecution } from './planPatch.js';
6
6
 
7
7
  export function abortError(message = 'Agent run cancelled.') {
8
8
  const err = new Error(message);
@@ -31,7 +31,7 @@ export async function runAgentTurn(agent, session, input, {
31
31
  if (session._abortSignal === signal) delete session._abortSignal;
32
32
  }
33
33
  if (result.streamedInline) {
34
- return streamedContent.trim() || buildLimitedAgentResponse({ input, session }, 'LLM stream ended without content');
34
+ return streamedContent.trim() || formatLlmUnavailableMessage('flux vide');
35
35
  }
36
36
  if (result.response != null) return result.response;
37
37
  if (result.readyToStream && session.llm?.stream) {
@@ -44,9 +44,9 @@ export async function runAgentTurn(agent, session, input, {
44
44
  })) {
45
45
  content += delta;
46
46
  }
47
- return content.trim() || buildLimitedAgentResponse({ input, session }, 'LLM stream ended without content');
47
+ return content.trim() || formatLlmUnavailableMessage('flux vide');
48
48
  }
49
- return buildLimitedAgentResponse({ input, session });
49
+ return formatLlmUnavailableMessage('reponse vide');
50
50
  }
51
51
 
52
52
  export async function runAgenticLoop(agent, session, initialInput, {
@@ -109,6 +109,7 @@ export async function runAgenticLoop(agent, session, initialInput, {
109
109
  onPlanAlreadySet?.({ steps: session.headlessPlan });
110
110
  }
111
111
  }
112
+ sanitizeSessionPlan(session, { runId });
112
113
 
113
114
  const newPending = newNonTerminalActivities(snapshot, session);
114
115
  if (newPending.length === 0) {
@@ -158,6 +159,20 @@ export async function runAgenticLoop(agent, session, initialInput, {
158
159
  return { ok: false, maxTurns: true };
159
160
  }
160
161
 
162
+ function sanitizeSessionPlan(session, { runId = null } = {}) {
163
+ if (!session.headlessPlan) return;
164
+ const sanitized = sanitizePlanForExecution(session.headlessPlan);
165
+ if (sanitized.warnings.length === 0) return;
166
+ session.headlessPlan = sanitized.plan;
167
+ dispatchAgentEvent(session, createAgentEvent('runtime_log', {
168
+ origin: 'runtime',
169
+ runId,
170
+ payload: {
171
+ message: `plan warning: ${sanitized.warnings.join('; ')}`,
172
+ },
173
+ }));
174
+ }
175
+
161
176
  function pendingStepsPrompt(initialInput, plan, readyTask) {
162
177
  return [
163
178
  'Original task:',
@@ -1,7 +1,25 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
3
  import { createAgentEvent, dispatchAgentEvent } from './agentEvents.js';
4
- import { runAgenticLoop } from './agentLoop.js';
4
+ import { runAgentTurn, runAgenticLoop } from './agentLoop.js';
5
+
6
+ test('runAgentTurn returns a one-line LLM error on empty stream', async () => {
7
+ const session = {
8
+ commands: [],
9
+ llm: {
10
+ async *stream() {},
11
+ },
12
+ };
13
+ const agent = {
14
+ async invoke() {
15
+ return { readyToStream: true, streamContext: { messages: [] } };
16
+ },
17
+ };
18
+
19
+ const response = await runAgentTurn(agent, session, 'salut');
20
+
21
+ assert.equal(response, '⚠ LLM injoignable : flux vide');
22
+ });
5
23
 
6
24
  test('runAgenticLoop waits for new activities and continues with a completion summary', async () => {
7
25
  const session = {
package/src/core/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { managerEnvFile, managerMcpEndpointsFile, readEnvFile } from './env.js';
3
3
 
4
- const WIKI_MANAGER_VERSION = '0.11.4';
4
+ const WIKI_MANAGER_VERSION = '0.11.7';
5
5
 
6
6
  function envValue(key) {
7
7
  const filePath = managerEnvFile();