@dotdrelle/wiki-manager 0.6.34 → 0.7.3

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 (42) hide show
  1. package/bin/wiki-manager.js +2 -2
  2. package/docker-compose.yml +25 -1
  3. package/package.json +2 -2
  4. package/src/agent/graph.js +2 -19
  5. package/src/cli/wiki-manager.js +236 -129
  6. package/src/commands/slash.js +17 -1
  7. package/src/commands/slash.test.js +78 -0
  8. package/src/core/activity.js +14 -0
  9. package/src/core/agentEvents.js +24 -2
  10. package/src/core/agentLoop.js +164 -0
  11. package/src/core/agentLoop.test.js +85 -0
  12. package/src/core/cacert.js +4 -3
  13. package/src/core/compose.js +4 -3
  14. package/src/core/dockerCompose.test.js +14 -0
  15. package/src/core/documentIntake.test.js +7 -7
  16. package/src/core/env.js +6 -0
  17. package/src/core/jobQueue.js +18 -4
  18. package/src/core/mcp.js +1 -1
  19. package/src/core/queueStore.js +27 -0
  20. package/src/core/queueStore.test.js +31 -0
  21. package/src/core/startupCheck.js +1 -1
  22. package/src/core/startupCheck.test.js +66 -0
  23. package/src/core/wikirc.js +2 -2
  24. package/src/core/workspaces.js +8 -1
  25. package/src/runtime/auth.js +35 -0
  26. package/src/runtime/auth.test.js +21 -0
  27. package/src/runtime/client.js +108 -0
  28. package/src/runtime/lifecycle.js +60 -0
  29. package/src/runtime/queueStore.js +6 -0
  30. package/src/runtime/runner.js +73 -0
  31. package/src/runtime/server.js +160 -0
  32. package/src/runtime/server.test.js +53 -0
  33. package/src/runtime/store.js +292 -0
  34. package/src/runtime/store.test.js +103 -0
  35. package/src/runtime/supervisor.js +98 -0
  36. package/src/runtime/supervisor.test.js +99 -0
  37. package/src/shell/repl.js +132 -17
  38. package/src/shell/repl.test.js +38 -0
  39. package/src/shell/tui.tsx +4 -1
  40. package/src/shell/useAgent.ts +20 -2
  41. package/src/shell/useSession.ts +146 -11
  42. package/wiki-workspace +39 -30
@@ -80,8 +80,8 @@ async function main() {
80
80
  // Fallback for already-bootstrapped direct invocations; the shell wrapper
81
81
  // exports these before Bun starts.
82
82
  if (parsed.cacert) Object.assign(process.env, cacertEnvVars(parsed.cacert));
83
- await import('@opentui/solid/preload');
84
- const interactive = process.stdout.isTTY && process.stdin.isTTY && !argv.includes('--setup-wizard') && !argv.includes('--headless') && !argv.includes('--once') && !argv.includes('--version') && !argv.includes('-v') && !argv.includes('--help') && !argv.includes('-h');
83
+ const interactive = process.stdout.isTTY && process.stdin.isTTY && argv[0] !== 'runtime' && !argv.includes('--setup-wizard') && !argv.includes('--headless') && !argv.includes('--once') && !argv.includes('--version') && !argv.includes('-v') && !argv.includes('--help') && !argv.includes('-h');
84
+ if (interactive || argv.includes('--setup-wizard')) await import('@opentui/solid/preload');
85
85
  if (interactive) process.stdout.write('Starting wiki-manager…\r');
86
86
  const { runCli } = await import('../src/cli/wiki-manager.js');
87
87
  await runCli(argv);
@@ -62,6 +62,8 @@ services:
62
62
  - DOCUMENT_MAX_UPLOAD_BYTES=${DOCUMENT_MAX_UPLOAD_BYTES:-52428800}
63
63
  - WIKI_MCP_PROXY_URL=http://host.docker.internal:${WIKI_MCP_PORT:-3101}/mcp
64
64
  - PRODUCTION_MCP_PROXY_URL=http://host.docker.internal:${PRODUCTION_MCP_PORT:-3102}/mcp/
65
+ - WIKI_MANAGER_RUNTIME_URL=http://host.docker.internal:7788
66
+ - WIKI_MANAGER_RUNTIME_TOKEN=${WIKI_MANAGER_RUNTIME_TOKEN:-}
65
67
  # HTTPS — set paths inside the container (e.g. /certs/server.crt) and uncomment the volume above
66
68
  #- WIKI_SERVE_TLS_CERT_PATH=/certs/server.crt
67
69
  #- WIKI_SERVE_TLS_KEY_PATH=/certs/server.key
@@ -95,6 +97,25 @@ services:
95
97
  - host.docker.internal:host-gateway
96
98
  restart: unless-stopped
97
99
 
100
+ # ── wiki-manager runtime ─────────────────────────────────────────────────
101
+
102
+ agent-runtime:
103
+ image: dotdrelle/llm-wiki-manager:latest
104
+ labels:
105
+ wiki-manager.description: "Agentic runtime — runs, plan, activities, queue."
106
+ command: runtime --host 0.0.0.0 --port 7788 --state-dir /state
107
+ volumes:
108
+ - ./.wiki-manager:/state
109
+ - ${WIKI_WORKSPACES_DIR:-./workspaces}:/workspaces
110
+ environment:
111
+ - WIKI_WORKSPACES_DIR=/workspaces
112
+ - WIKI_MANAGER_RUNTIME_TOKEN=${WIKI_MANAGER_RUNTIME_TOKEN:-}
113
+ ports:
114
+ - '127.0.0.1:7788:7788'
115
+ extra_hosts:
116
+ - host.docker.internal:host-gateway
117
+ restart: unless-stopped
118
+
98
119
  # ── agent-wiki-production ─────────────────────────────────────────────────
99
120
 
100
121
  production-mcp:
@@ -119,7 +140,7 @@ services:
119
140
  x-wiki-manager:
120
141
  service-aliases:
121
142
  all:
122
- targets: [serve, mcp-http, production-mcp]
143
+ targets: [serve, mcp-http, agent-runtime, production-mcp]
123
144
  description: "Full workspace service set."
124
145
  ui:
125
146
  targets: [serve]
@@ -130,6 +151,9 @@ x-wiki-manager:
130
151
  mcp:
131
152
  targets: [mcp-http]
132
153
  description: "Alias for mcp-http: wiki MCP server."
154
+ runtime:
155
+ targets: [agent-runtime]
156
+ description: "Alias for agent-runtime: agentic runtime."
133
157
  production:
134
158
  targets: [production-mcp]
135
159
  description: "Alias for production-mcp: production jobs."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdrelle/wiki-manager",
3
- "version": "0.6.34",
3
+ "version": "0.7.3",
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/core/activity.test.js src/core/agentEvents.test.js src/core/plan.test.js src/core/mcp.test.js src/core/documentIntake.test.js src/core/wikirc.test.js src/core/modelFetch.test.js src/commands/slash.test.js",
14
+ "test": "node --test src/core/activity.test.js src/core/agentEvents.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/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/auth.test.js",
15
15
  "check": "bun ./bin/wiki-manager.js --version && bun ./bin/wiki-manager.js --help && bun ./bin/wiki-manager.js --once \"verifie le mode agent\""
16
16
  },
17
17
  "engines": {
@@ -43,7 +43,7 @@ const SHELL_RUN_COMMAND_TOOL = {
43
43
  properties: {
44
44
  command: {
45
45
  type: 'string',
46
- description: 'Slash command to run, for example "/workspace list", "/workspace init demo", or "/use juno".',
46
+ description: 'Slash command to run, for example "/workspace list", "/workspace init <name>", or "/use <workspace>".',
47
47
  },
48
48
  },
49
49
  required: ['command'],
@@ -366,23 +366,6 @@ export function buildAgentSystemPrompt(state) {
366
366
  export function buildLimitedAgentResponse(state, reason = 'no workspace loaded with .wikirc.yaml') {
367
367
  const workspace = state.session.workspace ?? 'no workspace selected';
368
368
  const wikirc = state.session.wikirc?.profile ?? 'no profile loaded';
369
- const language = state.session.language ?? 'en-US';
370
- if (language.toLowerCase().startsWith('fr')) {
371
- return [
372
- `Donna est active. Workspace courant: ${workspace}.`,
373
- `Profil wikirc courant: ${wikirc}.`,
374
- '',
375
- "Je suis le mode agent du shell: utilise `/agent` pour router les entrees libres vers ce graphe LangGraph, ou `/chat` pour revenir au chat direct.",
376
- `Connexion LLM: mode limite (${reason}).`,
377
- `Primitives disponibles maintenant: ${commandList(state.session)}.`,
378
- '',
379
- 'Outils MCP connectes:',
380
- formatMcpToolsForAgent(state.session.mcp),
381
- '',
382
- 'Mode limite: workspace, Docker Compose, appels MCP, echappatoire /wiki, decouverte skills et mode headless sont branches.',
383
- "Utilise `/help` pour voir les commandes deterministes disponibles.",
384
- ].join('\n');
385
- }
386
369
  return [
387
370
  `Donna is active. Current workspace: ${workspace}.`,
388
371
  `Current wikirc profile: ${wikirc}.`,
@@ -506,7 +489,7 @@ export function createAgentGraph(options = {}) {
506
489
  } catch (err) {
507
490
  if (err.name === 'AbortError') throw err;
508
491
  const message = err instanceof Error ? err.message : String(err);
509
- return { response: buildLimitedAgentResponse(state, `LLM indisponible: ${message}`), pendingToolCalls: null, readyToStream: false };
492
+ return { response: buildLimitedAgentResponse(state, `LLM unavailable: ${message}`), pendingToolCalls: null, readyToStream: false };
510
493
  }
511
494
  }
512
495
 
@@ -1,17 +1,21 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { readFileSync } from 'node:fs';
2
3
  import { mkdir, writeFile } from 'node:fs/promises';
3
4
  import { dirname, join, resolve } from 'node:path';
4
5
  import { fileURLToPath } from 'node:url';
5
6
  import { loadManagerEnv } from '../core/env.js';
6
7
  loadManagerEnv();
7
- import { createAgentGraph, buildAgentSystemPrompt, buildLimitedAgentResponse } from '../agent/graph.js';
8
+ import { createAgentGraph } from '../agent/graph.js';
8
9
  import { handleSlashCommand, printHelp, printVersion } from '../commands/slash.js';
9
10
  import { runShell } from '../shell/repl.js';
10
11
  import { runChecks } from '../core/startupCheck.js';
11
12
  import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
12
- import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
13
- import { extractHeadlessPlan, syncActivitiesToPlan, formatPlanStatus, formatCompletedActivities } from '../core/plan.js';
13
+ import { extractActivity, parseJsonText, sessionActivities, terminalFailures } from '../core/activity.js';
14
+ import { syncActivitiesToPlan, formatPlanStatus } from '../core/plan.js';
14
15
  import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
16
+ import { runAgentTurn, runAgenticLoop } from '../core/agentLoop.js';
17
+ // Runtime modules use node:sqlite (Node.js built-in unavailable in Bun).
18
+ // They are imported dynamically so the shell / TUI path never loads them.
15
19
 
16
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
21
  const packageJsonPath = resolve(__dirname, '../../package.json');
@@ -39,6 +43,7 @@ function createSession() {
39
43
  packageJson,
40
44
  conversations: { __global__: [] },
41
45
  activities: {},
46
+ jobQueue: [],
42
47
  productionActivity: null,
43
48
  headlessPlan: null,
44
49
  };
@@ -57,20 +62,6 @@ async function writeHeadlessLog(session, lines, explicitPath) {
57
62
  return logPath;
58
63
  }
59
64
 
60
- function activitySnapshot(session) {
61
- return new Set(sessionActivities(session).map((a) => a.key));
62
- }
63
-
64
- function newNonTerminalActivities(snapshotBefore, session) {
65
- return sessionActivities(session).filter((a) => !snapshotBefore.has(a.key) && !a.terminal);
66
- }
67
-
68
- function terminalFailures(activities) {
69
- return activities.filter(
70
- (a) => a.terminal && ['failed', 'error', 'cancelled', 'canceled'].includes(String(a.status).toLowerCase()),
71
- );
72
- }
73
-
74
65
 
75
66
  async function runHeadlessActivityLoop(session, log, { wait, timeoutMs }) {
76
67
  if (!wait) return { exitCode: 0, completed: [], timedOut: false };
@@ -150,118 +141,54 @@ async function runHeadlessActivityLoop(session, log, { wait, timeoutMs }) {
150
141
  return { exitCode: 1, completed, timedOut: true };
151
142
  }
152
143
 
153
- async function runHeadlessAgentTurn(agent, session, input, log, messages = []) {
154
- session.packageJson = packageJson;
155
- let streamedContent = '';
156
- session._onStream = (delta) => { streamedContent += delta; };
157
- session._onStreamReset = () => { streamedContent = ''; };
158
- let result;
159
- try {
160
- result = await agent.invoke({ input, session, messages });
161
- } finally {
162
- delete session._onStream;
163
- delete session._onStreamReset;
164
- }
165
- if (result.streamedInline) {
166
- return streamedContent.trim() || buildLimitedAgentResponse({ input, session }, 'LLM stream ended without content');
167
- }
168
- if (result.response != null) return result.response;
169
- if (result.readyToStream && session.llm?.stream) {
170
- const { system, messages: streamMessages = [] } = result.streamContext ?? {};
171
- let content = '';
172
- for await (const delta of session.llm.stream({
173
- system: system ?? buildAgentSystemPrompt({ input, session }),
174
- messages: streamMessages,
175
- })) {
176
- content += delta;
177
- }
178
- return content.trim() || buildLimitedAgentResponse({ input, session }, 'LLM stream ended without content');
179
- }
180
- return buildLimitedAgentResponse({ input, session });
181
- }
182
-
183
144
  async function runHeadlessAgenticLoop(agent, session, initialInput, log, { timeoutMs, maxTurns }) {
184
- const conversationHistory = [];
185
- let currentInput = initialInput;
186
-
187
- for (let turn = 1; turn <= maxTurns; turn++) {
188
- log.push(`agentic-loop: turn ${turn}/${maxTurns}`);
189
- console.log(`[headless] Agent turn ${turn}/${maxTurns}…`);
190
-
191
- const snapshot = activitySnapshot(session);
192
-
193
- const response = await runHeadlessAgentTurn(agent, session, currentInput, log, conversationHistory);
194
- log.push(`agentic-loop: turn ${turn} response:`);
195
- log.push(response);
196
- console.log(response);
197
-
198
- conversationHistory.push(
199
- { role: 'user', content: currentInput },
200
- { role: 'assistant', content: response },
201
- );
202
-
203
- // session.headlessPlan is set authoritatively by wiki__plan_set tool call.
204
- // Fall back to text extraction only if the agent didn't call the tool.
205
- if (turn === 1 && session.headlessPlan === null) {
206
- const extractedPlan = extractHeadlessPlan(response);
207
- if (extractedPlan) {
208
- dispatchAgentEvent(session, createAgentEvent('plan_set', {
209
- origin: 'llm',
210
- payload: { steps: extractedPlan },
211
- }));
212
- log.push(`agentic-loop: plan extracted from text (${session.headlessPlan.length} steps, fallback)`);
213
- }
214
- } else if (turn === 1 && session.headlessPlan) {
215
- log.push(`agentic-loop: plan set via tool (${session.headlessPlan.length} steps)`);
216
- }
217
-
218
- const newPending = newNonTerminalActivities(snapshot, session);
219
- if (newPending.length === 0) {
220
- const pendingSteps = (session.headlessPlan ?? []).filter((s) => s.status === 'pending');
221
- if (pendingSteps.length === 0) {
222
- log.push('agentic-loop: no new non-terminal activities — plan complete');
223
- console.log('[headless] Plan complete.');
224
- return { exitCode: 0 };
225
- }
145
+ const result = await runAgenticLoop(agent, session, initialInput, {
146
+ timeoutMs,
147
+ maxTurns,
148
+ waitForActivities: async (turnSession, _startedActivities, waitOptions) => {
149
+ const waitResult = await runHeadlessActivityLoop(turnSession, log, { wait: true, timeoutMs: waitOptions.timeoutMs });
150
+ return {
151
+ ok: waitResult.exitCode === 0 && !waitResult.timedOut,
152
+ ...waitResult,
153
+ };
154
+ },
155
+ onTurnStart: ({ turn, maxTurns: totalTurns }) => {
156
+ log.push(`agentic-loop: turn ${turn}/${totalTurns}`);
157
+ console.log(`[headless] Agent turn ${turn}/${totalTurns}…`);
158
+ },
159
+ onTurnResponse: ({ turn, response }) => {
160
+ log.push(`agentic-loop: turn ${turn} response:`);
161
+ log.push(response);
162
+ console.log(response);
163
+ },
164
+ onPlanExtracted: ({ steps }) => {
165
+ log.push(`agentic-loop: plan extracted from text (${steps.length} steps, fallback)`);
166
+ },
167
+ onPlanAlreadySet: ({ steps }) => {
168
+ log.push(`agentic-loop: plan set via tool (${steps.length} steps)`);
169
+ },
170
+ onComplete: () => {
171
+ log.push('agentic-loop: no new non-terminal activities — plan complete');
172
+ console.log('[headless] Plan complete.');
173
+ },
174
+ onPendingSteps: ({ pendingSteps }) => {
226
175
  log.push(`agentic-loop: no new async activity, ${pendingSteps.length} pending step(s) remain`);
227
176
  if (session.headlessPlan) log.push(`agentic-loop: plan status:\n${formatPlanStatus(session.headlessPlan)}`);
228
- currentInput = [
229
- 'Original task:',
230
- initialInput,
231
- '',
232
- `Plan status:\n${formatPlanStatus(session.headlessPlan)}`,
233
- '',
234
- 'No new background activity was started in the previous turn.',
235
- 'Continue the original plan. Start the next pending step only.',
236
- 'If required information is missing and cannot be inferred, stop with a clear blocker.',
237
- ].join('\n');
238
- continue;
239
- }
240
-
241
- log.push(`agentic-loop: ${newPending.length} new job(s) started, waiting…`);
242
- const { exitCode, completed, timedOut } = await runHeadlessActivityLoop(session, log, { wait: true, timeoutMs });
243
-
244
- if (timedOut || exitCode !== 0) return { exitCode };
245
-
246
- const summary = formatCompletedActivities(completed);
247
- log.push(`agentic-loop: completed activities:\n${summary}`);
248
- if (session.headlessPlan) log.push(`agentic-loop: plan status:\n${formatPlanStatus(session.headlessPlan)}`);
249
- currentInput = [
250
- 'Original task:',
251
- initialInput,
252
- '',
253
- session.headlessPlan ? `Plan status:\n${formatPlanStatus(session.headlessPlan)}\n` : null,
254
- 'Completed activities:',
255
- summary,
256
- '',
257
- 'Continue the original plan. Start the next required step only.',
258
- 'If all steps are complete, provide the final summary.',
259
- ].filter(Boolean).join('\n');
260
- }
261
-
262
- log.push(`agentic-loop: max turns (${maxTurns}) reached without completing`);
263
- console.error(`[headless] Max agent turns (${maxTurns}) reached.`);
264
- return { exitCode: 1 };
177
+ },
178
+ onActivitiesStarted: ({ activities }) => {
179
+ log.push(`agentic-loop: ${activities.length} new job(s) started, waiting…`);
180
+ },
181
+ onActivitiesCompleted: ({ summary }) => {
182
+ log.push(`agentic-loop: completed activities:\n${summary}`);
183
+ if (session.headlessPlan) log.push(`agentic-loop: plan status:\n${formatPlanStatus(session.headlessPlan)}`);
184
+ },
185
+ onMaxTurns: ({ maxTurns: totalTurns }) => {
186
+ log.push(`agentic-loop: max turns (${totalTurns}) reached without completing`);
187
+ console.error(`[headless] Max agent turns (${totalTurns}) reached.`);
188
+ },
189
+ });
190
+
191
+ return { exitCode: result.ok ? 0 : (result.waitResult?.exitCode ?? 1) };
265
192
  }
266
193
 
267
194
  async function runHeadless(argv, agent) {
@@ -326,7 +253,7 @@ async function runHeadless(argv, agent) {
326
253
  if (useAgenticLoop) {
327
254
  ({ exitCode } = await runHeadlessAgenticLoop(agent, session, input, log, { timeoutMs, maxTurns }));
328
255
  } else {
329
- const response = await runHeadlessAgentTurn(agent, session, input, log);
256
+ const response = await runAgentTurn(agent, session, input);
330
257
  log.push('response:');
331
258
  log.push(response);
332
259
  console.log(response);
@@ -345,7 +272,171 @@ async function runHeadless(argv, agent) {
345
272
  }
346
273
  }
347
274
 
275
+ async function runRuntime(argv, agent) {
276
+ if (argv.includes('--help') || argv.includes('-h')) {
277
+ console.log([
278
+ 'Usage: wiki-manager runtime [--host <host>] [--port <port>] [--state-dir <dir>]',
279
+ '',
280
+ 'Runs the local agentic runtime used by wiki-manager Shell and llm-wiki serve.',
281
+ '',
282
+ 'Defaults:',
283
+ ' --host 0.0.0.0',
284
+ ' --port 7788',
285
+ ' --state-dir .wiki-manager',
286
+ ].join('\n'));
287
+ return;
288
+ }
289
+ const { defaultRuntimeStateDir, openRuntimeStore } = await import('../runtime/store.js');
290
+ const { startRuntimeServer } = await import('../runtime/server.js');
291
+ const { emitRuntimeLog, startActivitySupervisor } = await import('../runtime/supervisor.js');
292
+ const { resolveRuntimeAuthToken } = await import('../runtime/auth.js');
293
+ const { createSqliteQueueStore } = await import('../runtime/queueStore.js');
294
+ const { runRuntimeAgenticLoop } = await import('../runtime/runner.js');
295
+
296
+ const host = valueAfter(argv, '--host') ?? process.env.WIKI_MANAGER_RUNTIME_HOST ?? '0.0.0.0';
297
+ const port = Number(valueAfter(argv, '--port') ?? process.env.WIKI_MANAGER_RUNTIME_PORT ?? 7788);
298
+ const stateDir = valueAfter(argv, '--state-dir') ?? defaultRuntimeStateDir();
299
+ const auth = resolveRuntimeAuthToken({ host, stateDir });
300
+ if (auth.token) process.env.WIKI_MANAGER_RUNTIME_TOKEN = auth.token;
301
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
302
+ throw new Error(`Invalid runtime port: ${port}`);
303
+ }
304
+
305
+ const session = createSession();
306
+ session.headless = true;
307
+ session.chatMode = false;
308
+ session.packageJson = packageJson;
309
+
310
+ const store = openRuntimeStore({ stateDir });
311
+ store.hydrateSession(session);
312
+ session.queueStore = createSqliteQueueStore(store, session);
313
+
314
+ let serverHandle = null;
315
+ session._onAgentEvent = (event) => {
316
+ store.persistEvent(event);
317
+ serverHandle?.publish(event);
318
+ };
319
+ session._onRuntimeError = (err) => {
320
+ const message = err instanceof Error ? err.message : String(err);
321
+ dispatchAgentEvent(session, createAgentEvent('run_error', {
322
+ origin: 'runtime',
323
+ payload: { message },
324
+ }));
325
+ };
326
+
327
+ let supervisor = null;
328
+
329
+ async function executeRun(body, { signal } = {}) {
330
+ const input = String(body.input ?? body.prompt ?? '').trim();
331
+ const workspace = body.workspace ? String(body.workspace).trim() : null;
332
+ const timeoutMs = (Number.isFinite(Number(body.timeout)) ? Math.max(1, Number(body.timeout)) : 3600) * 1000;
333
+ const maxTurns = Number.isFinite(Number(body.maxTurns)) ? Math.max(1, Number(body.maxTurns)) : 20;
334
+ const runId = randomUUID();
335
+ try {
336
+ if (workspace && session.workspace !== workspace) {
337
+ const result = await handleSlashCommand(`/use ${workspace}`, { packageJson, session });
338
+ if (!session.workspacePath) throw new Error(result.output || `Workspace not loaded: ${workspace}`);
339
+ }
340
+ dispatchAgentEvent(session, createAgentEvent('run_started', {
341
+ origin: 'runtime',
342
+ runId,
343
+ payload: { input, workspace },
344
+ }));
345
+ dispatchAgentEvent(session, createAgentEvent('user_message', {
346
+ origin: 'user',
347
+ runId,
348
+ payload: { content: input },
349
+ }));
350
+ session._abortSignal = signal ?? null;
351
+ supervisor?.setRunSignal(signal);
352
+ session._onStep = (message) => emitRuntimeLog(session, message);
353
+ const result = await runRuntimeAgenticLoop(agent, session, input, {
354
+ signal,
355
+ timeoutMs,
356
+ maxTurns,
357
+ runId,
358
+ pollBusy: supervisor?.pollBusy,
359
+ });
360
+ if (!result.ok) {
361
+ dispatchAgentEvent(session, createAgentEvent('run_error', {
362
+ origin: 'runtime',
363
+ runId,
364
+ payload: {
365
+ runId,
366
+ message: result.timedOut
367
+ ? 'Runtime agentic loop timed out.'
368
+ : result.maxTurns
369
+ ? `Runtime agentic loop reached max turns (${maxTurns}).`
370
+ : 'Runtime agentic loop failed.',
371
+ },
372
+ }));
373
+ return;
374
+ }
375
+ dispatchAgentEvent(session, createAgentEvent('run_done', {
376
+ origin: 'runtime',
377
+ runId,
378
+ payload: { runId },
379
+ }));
380
+ } catch (err) {
381
+ if (err?.name === 'AbortError') {
382
+ dispatchAgentEvent(session, createAgentEvent('run_cancelled', {
383
+ origin: 'runtime',
384
+ runId,
385
+ payload: {
386
+ runId,
387
+ message: 'Agent run cancelled.',
388
+ },
389
+ }));
390
+ return;
391
+ }
392
+ dispatchAgentEvent(session, createAgentEvent('run_error', {
393
+ origin: 'runtime',
394
+ runId,
395
+ payload: {
396
+ runId,
397
+ message: err instanceof Error ? err.message : String(err),
398
+ },
399
+ }));
400
+ } finally {
401
+ supervisor?.setRunSignal(null);
402
+ delete session._abortSignal;
403
+ delete session._onStep;
404
+ }
405
+ }
406
+
407
+ serverHandle = await startRuntimeServer({
408
+ host,
409
+ port,
410
+ store,
411
+ session,
412
+ run: executeRun,
413
+ cancel: () => emitRuntimeLog(session, 'runtime: cancel requested'),
414
+ token: auth.token,
415
+ });
416
+ supervisor = startActivitySupervisor(session);
417
+
418
+ console.log(`wiki-manager runtime listening on http://${host}:${port}`);
419
+ console.log(`runtime state: ${store.dbPath}`);
420
+ if (auth.tokenPath) console.log(`runtime token: ${auth.tokenPath}`);
421
+
422
+ const shutdown = async () => {
423
+ supervisor?.stop();
424
+ await serverHandle.close();
425
+ store.close();
426
+ process.exit(0);
427
+ };
428
+ process.once('SIGINT', shutdown);
429
+ process.once('SIGTERM', shutdown);
430
+ await new Promise(() => {});
431
+ }
432
+
348
433
  export async function runCli(argv) {
434
+ if (argv[0] === 'runtime') {
435
+ const agent = createAgentGraph();
436
+ await runRuntime(argv.slice(1), agent);
437
+ return;
438
+ }
439
+
349
440
  if (argv.includes('--setup-wizard')) {
350
441
  if (!process.versions.bun) {
351
442
  throw new Error('Setup wizard requires Bun. Run: bun ./bin/wiki-manager.js --setup-wizard');
@@ -396,9 +487,25 @@ export async function runCli(argv) {
396
487
  const { runOpenTuiShell, runStartupWizard } = await import('../shell/tui.tsx');
397
488
  const gaps = await runChecks();
398
489
  if (gaps.length > 0) await runStartupWizard(gaps);
399
- await runOpenTuiShell({ agent, packageJson });
490
+ let runtime = null;
491
+ try {
492
+ const { ensureRuntime } = await import('../runtime/lifecycle.js');
493
+ runtime = await ensureRuntime();
494
+ } catch (err) {
495
+ console.error(`Runtime unavailable: ${err instanceof Error ? err.message : String(err)}`);
496
+ }
497
+ await runOpenTuiShell({ agent, packageJson, runtime });
400
498
  return;
401
499
  }
402
500
 
403
- await runShell({ agent, packageJson });
501
+ let runtime = null;
502
+ if (process.stdin.isTTY && process.stdout.isTTY) {
503
+ try {
504
+ const { ensureRuntime } = await import('../runtime/lifecycle.js');
505
+ runtime = await ensureRuntime();
506
+ } catch (err) {
507
+ console.error(`Runtime unavailable: ${err instanceof Error ? err.message : String(err)}`);
508
+ }
509
+ }
510
+ await runShell({ agent, packageJson, runtime });
404
511
  }
@@ -282,6 +282,13 @@ function workspaceStatsText(stats) {
282
282
  }
283
283
 
284
284
  function workspaceLoadedText(workspace, summary, session) {
285
+ const profiles = listWikircProfiles(workspace.workspacePath);
286
+ const profileLines = profiles.length > 0
287
+ ? profiles.map((profile) => {
288
+ const marker = profile.name === summary.profile ? '*' : ' ';
289
+ return `${marker} ${profile.name}\t${profile.fileName}`;
290
+ })
291
+ : ['No .wikirc.yaml profile found.'];
285
292
  return [
286
293
  `Workspace: ${workspace.name}`,
287
294
  '',
@@ -300,6 +307,12 @@ function workspaceLoadedText(workspace, summary, session) {
300
307
  `vector: ${summary.vectorEnabled ? 'enabled' : 'disabled'}`,
301
308
  `embedding: ${summary.embeddingModel ?? '-'}`,
302
309
  '',
310
+ 'Available configs',
311
+ '',
312
+ ...profileLines,
313
+ '',
314
+ `Switch config: /config use <profile>`,
315
+ '',
303
316
  'Session',
304
317
  '',
305
318
  `llm: ${session.llm ? 'configured' : 'missing config'}`,
@@ -647,7 +660,7 @@ Modes:
647
660
  Status:
648
661
  Agent-first shell is installed with workspace services, MCP calls, wiki CLI, skill discovery, and headless runs.
649
662
  Shell UI is English. Agent exchange language is read from the active .wikirc.yaml.
650
- LLM config is intentionally workspace-scoped and will be read from .wikirc.yaml after /use <workspace>.
663
+ LLM config is intentionally workspace-scoped and is read from .wikirc.yaml after /use <workspace>.
651
664
  Headless mode supports one-shot workspace prompts and skill runs with log output.
652
665
  `;
653
666
  }
@@ -690,6 +703,9 @@ export async function handleSlashCommand(line, context) {
690
703
  case 'use': {
691
704
  const workspaceName = args[1];
692
705
  if (!workspaceName) {
706
+ return { output: formatWorkspaceList(listWorkspaces(), context.session) };
707
+ }
708
+ if (args[2]) {
693
709
  return { output: 'Usage: /use <workspace>' };
694
710
  }
695
711
  const workspace = findWorkspace(workspaceName);