@dotdrelle/wiki-manager 0.15.74 → 0.15.76

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
@@ -23,11 +23,10 @@ deterministic dispatcher executes bounded tasks with idempotency, bounded
23
23
  approvals, per-run budgets and automatic recovery after restart. The chat
24
24
  stays available while runs execute; additional requests are queued.
25
25
 
26
- Scope note: this is a single-user deployment baseline. The multi-user model is
27
- specified in `llm-wiki/docs/industrialisation.md` and planned next. Until
28
- then, do not expose the runtime as a shared write surface; it binds to
29
- `127.0.0.1` by default, and `--host 0.0.0.0` must be an explicit deployment
30
- choice with bearer-token and network protection.
26
+ Scope note: this is a single-user deployment baseline. Do not expose the
27
+ runtime as a shared write surface; it binds to `127.0.0.1` by default, and
28
+ `--host 0.0.0.0` must be an explicit deployment choice with bearer-token and
29
+ network protection.
31
30
 
32
31
  ---
33
32
 
@@ -170,7 +169,7 @@ Projects compared: [OpenWiki](https://github.com/langchain-ai/openwiki),
170
169
  territory.
171
170
  - Serve a true multi-user instance with per-user identity and an attributed
172
171
  audit trail. This is a single-user deployment baseline (see the scope note
173
- above); multi-user is specified and planned next.
172
+ above).
174
173
  - Expose a graph-query API over the corpus the way GraphRAG does; retrieval is
175
174
  BM25 plus a vector index feeding generation.
176
175
  - Ship or host the multi-provider AI gateway — routing to several providers is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdrelle/wiki-manager",
3
- "version": "0.15.74",
3
+ "version": "0.15.76",
4
4
  "description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -702,8 +702,8 @@ async function runHeadless(argv, agent) {
702
702
  session._onStep = step;
703
703
 
704
704
  let input = prompt;
705
- // Plan §24: headless must resolve executable skills through the same
706
- // runtime resolver as the Shell and serve. Injecting the skill body into a
705
+ // Headless must resolve executable skills through the same runtime resolver
706
+ // as the Shell and serve. Injecting the skill body into a
707
707
  // local prompt bypasses the compiler, so a multi-capability skill such as
708
708
  // wiki-sync would collapse into a single run here while producing two
709
709
  // everywhere else — and the rewritten bodies are business intentions, not
@@ -396,8 +396,8 @@ test('argument extraction keeps a value the vocabulary allows', async () => {
396
396
  });
397
397
 
398
398
  test('headless waits for every run of a skill chain, not just the first', async () => {
399
- // wiki-sync compiles into two sequential runs: returning as soon as the
400
- // export finishes would report success before the ingest had started.
399
+ // A multi-step user skill compiles into sequential runs: returning as soon as
400
+ // the first finishes would report success before the rest had started.
401
401
  const client = chainClient([
402
402
  { controlQueue: [chainItem(0, 'running', { runId: 'run-a' }), chainItem(1, 'queued')] },
403
403
  { controlQueue: [chainItem(0, 'done'), chainItem(1, 'running', { runId: 'run-b' })] },
@@ -894,7 +894,7 @@ function componentInstallAction(missingImages) {
894
894
  return missingImages.length > 0 ? 'downloaded-and-installed-missing-components' : null;
895
895
  }
896
896
 
897
- export function localizedOperationResult({ operation, target, status = 'succeeded', componentAction = null, images = [], detail = null }) {
897
+ export function localizedOperationResult({ operation, target, status = 'succeeded', componentAction = null, images = [], detail = null, style = 'compact' }) {
898
898
  const facts = JSON.stringify({
899
899
  operation,
900
900
  target,
@@ -902,15 +902,21 @@ export function localizedOperationResult({ operation, target, status = 'succeede
902
902
  ...(componentAction ? { componentAction, images } : {}),
903
903
  ...(detail ? { detail } : {}),
904
904
  });
905
+ const instructions = style === 'report'
906
+ ? [
907
+ 'Formule le résultat structuré suivant dans la langue et le ton demandés par le profil du workspace.',
908
+ "Décris ce qui a été fait : ce qui a été démarré, et l'état actuel de chaque service (en marche, en échec ou inconnu).",
909
+ 'Réponds en deux ou trois phrases. Ne cite aucune commande, syntaxe shell, chemin de fichier, sortie docker ou identifiant.',
910
+ ]
911
+ : [
912
+ 'Formule le résultat structuré suivant dans la langue et le ton demandés par le profil du workspace.',
913
+ 'Réponds par une seule phrase humaine et naturelle.',
914
+ 'Ne mentionne aucune commande, syntaxe shell, étape suivante ou détail technique.',
915
+ ];
905
916
  return {
906
917
  output: facts,
907
918
  rawOutput: true,
908
- agentTrigger: [
909
- 'Formule le résultat structuré suivant dans la langue et le ton demandés par le profil du workspace.',
910
- 'Réponds par une seule phrase humaine et naturelle.',
911
- 'Ne mentionne aucune commande, syntaxe shell, étape suivante ou détail technique.',
912
- `Résultat: ${facts}`,
913
- ].join('\n'),
919
+ agentTrigger: [...instructions, `Résultat: ${facts}`].join('\n'),
914
920
  };
915
921
  }
916
922
 
@@ -1197,6 +1203,7 @@ export async function handleSlashCommand(line, context) {
1197
1203
  const startsAgents = service === 'all';
1198
1204
  const target = service === 'services' ? undefined : service;
1199
1205
  try {
1206
+ let agentsResult = null;
1200
1207
  if (startsAgents) {
1201
1208
  // Validate the workspace half before mutating the global agents
1202
1209
  // stack. Otherwise `/start all` with no active workspace starts the
@@ -1204,18 +1211,48 @@ export async function handleSlashCommand(line, context) {
1204
1211
  if (!context.session.workspace || !context.session.workspacePath || !context.session.workspaceEnv?.WORKSPACE_NAME) {
1205
1212
  throw new Error('No workspace loaded. Use /use <workspace>.');
1206
1213
  }
1207
- const agentsResult = await runAgentCommand(startAgents, 'start');
1214
+ agentsResult = await runAgentCommand(startAgents, 'start');
1208
1215
  if (agentsResult?.failed) return agentsResult;
1209
1216
  }
1210
1217
  step(`Services: starting ${target ?? 'workspace services'}…`);
1211
- const missingImages = await collectMissingImages(step, (opts) => startService(context.session, target, opts));
1218
+ const started = [];
1219
+ const missingImages = await collectMissingImages(step, async (opts) => {
1220
+ const result = await startService(context.session, target, opts);
1221
+ if (Array.isArray(result?.targets)) started.push(...result.targets);
1222
+ });
1212
1223
  step('Services: refreshing MCP runtime…');
1213
1224
  await refreshMcpRuntimeStatus(context.session);
1225
+ // The report is about what was done, so it names what started and how
1226
+ // each service ended up. A failed state check says so instead of
1227
+ // leaving every state "unknown" without a word.
1228
+ let states = null;
1229
+ try {
1230
+ states = await serviceStates(context.session);
1231
+ } catch (err) {
1232
+ step(`Services: state check after start unavailable — ${rawFailureText(err)}`);
1233
+ }
1234
+ const agentsFacts = agentsResult ? parseJsonText(agentsResult.output) : null;
1235
+ const detail = {
1236
+ ...(agentsFacts && typeof agentsFacts === 'object' && !Array.isArray(agentsFacts) ? { agents: agentsFacts } : {}),
1237
+ ...(started.length > 0
1238
+ ? {
1239
+ services: {
1240
+ started,
1241
+ states: started.map((name) => ({
1242
+ service: name,
1243
+ running: states?.[name] ? states[name].running : null,
1244
+ })),
1245
+ },
1246
+ }
1247
+ : {}),
1248
+ };
1214
1249
  return localizedOperationResult({
1215
1250
  operation: 'start',
1216
1251
  target: startsAgents ? 'all-services-and-agents' : (target || 'workspace-services'),
1217
1252
  componentAction: componentInstallAction(missingImages),
1218
1253
  images: missingImages,
1254
+ detail: Object.keys(detail).length > 0 ? detail : null,
1255
+ style: 'report',
1219
1256
  });
1220
1257
  } catch (err) {
1221
1258
  step(formatActivityError('services', 'start', err));
@@ -128,6 +128,34 @@ test('a started agent stack reloads the env and the MCP endpoints the script rew
128
128
  assert.match(runAgent, /await refreshMcpRuntimeStatus\(context\.session\)/);
129
129
  });
130
130
 
131
+ test('start report style asks Donna to describe what was done, not a bare sentence', () => {
132
+ const result = localizedOperationResult({
133
+ operation: 'start',
134
+ target: 'wiki',
135
+ style: 'report',
136
+ detail: { services: { started: ['wiki'], states: [{ service: 'wiki', running: true }] } },
137
+ });
138
+ assert.deepEqual(JSON.parse(result.output).detail, {
139
+ services: { started: ['wiki'], states: [{ service: 'wiki', running: true }] },
140
+ });
141
+ assert.match(result.agentTrigger, /Décris ce qui a été fait/);
142
+ assert.match(result.agentTrigger, /en marche, en échec ou inconnu/);
143
+ assert.doesNotMatch(result.agentTrigger, /une seule phrase/);
144
+ assert.doesNotMatch(result.agentTrigger, /\/start|Docker|compose/);
145
+ });
146
+
147
+ test('/start captures what actually started and their resulting states', async () => {
148
+ const { readFile } = await import('node:fs/promises');
149
+ const source = await readFile(new URL('./slash.js', import.meta.url), 'utf8');
150
+ const start = source.slice(source.indexOf("case 'start': {"), source.indexOf("case 'stop': {"));
151
+
152
+ assert.match(start, /const started = \[\];/);
153
+ assert.match(start, /result\?\.targets/);
154
+ assert.match(start, /states = await serviceStates\(context\.session\)/);
155
+ assert.match(start, /running: states\?\.\[name\] \? states\[name\]\.running : null/);
156
+ assert.match(start, /style: 'report'/);
157
+ });
158
+
131
159
  test('/start completes to the three documented targets', async () => {
132
160
  const { completionDescription } = await import('../shell/repl.js');
133
161
  const { matches } = completionContext('/start ', {});
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "0.15.74",
3
- "commit": "4ca493b"
2
+ "version": "0.15.76",
3
+ "commit": "2504dc9"
4
4
  }
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.15.74';
4
+ const WIKI_MANAGER_VERSION = '0.15.76';
5
5
 
6
6
  function envValue(key) {
7
7
  const filePath = managerEnvFile();
@@ -756,8 +756,7 @@ export function parseToolCallName(name) {
756
756
  // tool name, route to it and report `normalized: true`; otherwise return
757
757
  // `server: null` with the list of candidate servers so the caller can raise
758
758
  // an explicit error. This is name normalization, never fuzzy matching — do
759
- // not extend it to description/similarity-based selection (plan directeur
760
- // §20 forbids that).
759
+ // not extend it to description/similarity-based selection.
761
760
  export function resolveToolCallName(mcpStatus, name, extraServers = {}) {
762
761
  const parsed = parseToolCallName(name);
763
762
  if (parsed.server) return { ...parsed, normalized: false, candidates: [] };
@@ -30,8 +30,8 @@ test('validation rejects technical routing details', () => {
30
30
  assert.throws(() => validateCompiledObjectives([{ text: 'agent: cme' }]), { code: 'skill_compile_failed' });
31
31
  });
32
32
 
33
- test('scaffold skills preserve existing capabilities and split only wiki-sync', async () => {
34
- const expected = { pipeline: 1, 'wiki-ingest': 1, 'wiki-build': 1, deliver: 1, diagnose: 1, status: 1, 'new-template': 1, 'wiki-sync': 2 };
33
+ test('every shipped scaffold skill compiles to a single intention', async () => {
34
+ const expected = { pipeline: 1, 'wiki-sync': 1, 'wiki-ingest': 1, 'wiki-build': 1, deliver: 1, diagnose: 1, status: 1, 'new-template': 1 };
35
35
  for (const [name, count] of Object.entries(expected)) {
36
36
  const raw = readFileSync(resolve('../llm-wiki/scaffold/workspace/.wiki/skills', `${name}.md`), 'utf8');
37
37
  const { meta, body } = parseFrontmatter(raw);
@@ -53,7 +53,7 @@ test('every objective of a chain carries the user parameters, not just the last'
53
53
  // Appending the parameters before splitting attached `source` to the ingest
54
54
  // step and left the export step — the one that consumes it — without it.
55
55
  const skill = {
56
- name: 'wiki-sync',
56
+ name: 'collect-then-ingest',
57
57
  params: ['source'],
58
58
  body: 'Export the requested source.\n\nThen ingest what was exported.',
59
59
  };
@@ -14,13 +14,11 @@ import { isTerminal } from '../orchestrator/taskStatuses.js';
14
14
 
15
15
  const RUNNING_STATUSES = new Set(['running', 'starting', 'queued', 'waiting', 'pending_approval']);
16
16
 
17
- // Canonical workflow projection for 0.9.6.
17
+ // Canonical workflow projection.
18
18
  //
19
- // Decision: projectWorkflow consumes the existing event-sourced agentProjection
20
- // instead of replacing it in this release. agentProjection remains the
21
- // compatibility reducer/hydration format; workflow is the canonical read model
22
- // for Serve and ShellTUI. Future releases can move the reducer internals behind
23
- // this module without changing UI contracts.
19
+ // projectWorkflow consumes the existing event-sourced agentProjection instead
20
+ // of replacing it: agentProjection remains the compatibility reducer/hydration
21
+ // format; workflow is the canonical read model for Serve and ShellTUI.
24
22
  export function projectWorkflow(state = {}, events = []) {
25
23
  const run = currentRun(state, events);
26
24
  const plan = Array.isArray(state.plan) ? state.plan : [];
@@ -92,7 +92,7 @@ test('resolveObjective disambiguates "export" of a Confluence source via alias,
92
92
  assert.equal(result.provider.agentInstanceId, 'cme-1');
93
93
  });
94
94
 
95
- test('resolveObjective resolves the ingest step of wiki-sync deterministically despite notification and guardrails', async () => {
95
+ test('resolveObjective resolves an ingest objective deterministically despite notification and guardrails', async () => {
96
96
  const session = sessionWith([
97
97
  provider('production-1', knowledge),
98
98
  provider('production-2', publish),
@@ -1422,9 +1422,8 @@ function rejectPlanPatch(context, store, patchId, reason) {
1422
1422
  };
1423
1423
  }
1424
1424
 
1425
- // Classifier for control §4.2 of the plan directeur. The plan expects an
1426
- // LLM-backed classification — "the classification LLM se trompera" — and this
1427
- // is that, now: the only deterministic matches left are the runtime's own
1425
+ // Classifier for the control lane's free-text messages. The classification is
1426
+ // LLM-backed: the only deterministic matches left are the runtime's own
1428
1427
  // control verbs (cancel, an explicit "later/queue", status and plan-change
1429
1428
  // wording). Deciding "is this a NEW task to queue vs plain conversation" is a
1430
1429
  // semantic judgement about the workspace's domain, so it is never a keyword
@@ -1606,7 +1606,7 @@ test('runtime server handle drains a pre-existing hydrated control request', asy
1606
1606
  test('POST /run compiles a workspace skill into a sequential runtime chain', async (t) => {
1607
1607
  const root = mkdtempSync(join(tmpdir(), 'runtime-skill-'));
1608
1608
  mkdirSync(join(root, '.wiki', 'skills'), { recursive: true });
1609
- writeFileSync(join(root, '.wiki', 'skills', 'wiki-sync.md'), '---\nname: wiki-sync\nparams:\n - source\n---\nExport the source.\n\nThen ingest the files.');
1609
+ writeFileSync(join(root, '.wiki', 'skills', 'sync-ingest.md'), '---\nname: sync-ingest\nparams:\n - source\n---\nExport the source.\n\nThen ingest the files.');
1610
1610
  const session = { workspace: 'acme', workspacePath: root, controlQueue: [] };
1611
1611
  const context = { workspace: 'acme', session, running: false, currentAbortController: null };
1612
1612
  let startedBody = null;
@@ -1623,7 +1623,7 @@ test('POST /run compiles a workspace skill into a sequential runtime chain', asy
1623
1623
  throw err;
1624
1624
  }
1625
1625
  try {
1626
- const response = await fetch(`http://127.0.0.1:${handle.port}/run?workspace=acme`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ input: '/wiki-sync docs' }) });
1626
+ const response = await fetch(`http://127.0.0.1:${handle.port}/run?workspace=acme`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ input: '/sync-ingest docs' }) });
1627
1627
  const body = await response.json();
1628
1628
  assert.equal(response.status, 202);
1629
1629
  assert.equal(body.kind, 'skill_chain');
@@ -1633,15 +1633,15 @@ test('POST /run compiles a workspace skill into a sequential runtime chain', asy
1633
1633
  assert.equal(session.controlQueue[1].status, 'queued');
1634
1634
  assert.equal(session.controlQueue[0].chainId, session.controlQueue[1].chainId);
1635
1635
  assert.equal('capabilityPlan' in session.controlQueue[0], false);
1636
- assert.equal(session.controlQueue[0].input, '/wiki-sync source="docs"');
1636
+ assert.equal(session.controlQueue[0].input, '/sync-ingest source="docs"');
1637
1637
  assert.equal(session.controlQueue[0].skillExecution, 'orchestrated');
1638
1638
  assert.match(startedBody.input, /Export the source/);
1639
- assert.equal(startedBody.publicInput, '/wiki-sync source="docs"');
1639
+ assert.equal(startedBody.publicInput, '/sync-ingest source="docs"');
1640
1640
  assert.equal(startedBody.skillChain.execution, 'orchestrated');
1641
1641
  assert.equal(startedBody.requireApproval, true);
1642
1642
  assert.notEqual(startedBody.autoApprove, true);
1643
1643
  assert.deepEqual(session.agentProjection.conversation, [
1644
- { role: 'user', content: '/wiki-sync docs' },
1644
+ { role: 'user', content: '/sync-ingest docs' },
1645
1645
  ]);
1646
1646
  assert.doesNotMatch(JSON.stringify(session.agentEvents), /Export the source|Then ingest the files/);
1647
1647
  } finally {
@@ -132,26 +132,31 @@ test('E2E-001 pipeline: one objective, one run, internal plan left untouched', a
132
132
  assert.equal(env.chain().length, 1);
133
133
  });
134
134
 
135
- test('E2E-002 wiki-sync: two objectives, two ordered runs, one chainId', async (t) => {
136
- const env = await harness(t, { skills: { 'wiki-sync': null } });
135
+ test('E2E-002 multi-step skill: two objectives, two ordered runs, one chainId', async (t) => {
136
+ // No shipped skill splits any more (each is one intention); a user-authored
137
+ // body with a strong connector still must.
138
+ const env = await harness(t, {
139
+ skills: {
140
+ 'sync-ingest': '---\nname: sync-ingest\nparams: []\n---\nExport every configured Confluence source.\n\nThen ingest the exported Markdown into the wiki.',
141
+ },
142
+ });
137
143
  if (!env) return;
138
144
 
139
- const { body } = await env.post('/run?workspace=acme', { input: '/wiki-sync' });
145
+ const { body } = await env.post('/run?workspace=acme', { input: '/sync-ingest' });
140
146
  await env.settle();
141
147
 
142
148
  assert.equal(body.objectives, 2);
143
149
  assert.equal(env.runs.length, 2, 'the second objective must run after the first');
144
150
  assert.match(env.runs[0].input, /^Export every configured Confluence source/);
145
- assert.match(env.runs[1].input, /^Run the production pipeline step ingest over the newly exported Markdown/);
146
- // CME first, Production second — the skill carries no source parameter, so
147
- // no run may receive a source selector (the export step must stay "all").
151
+ assert.match(env.runs[1].input, /^Ingest the exported Markdown/);
152
+ // The skill carries no parameter, so no run may receive a selector.
148
153
  for (const run of env.runs) assert.doesNotMatch(run.input, /User parameters:/);
149
154
  const items = env.chain();
150
155
  assert.equal(items.length, 2);
151
156
  assert.equal(items[0].chainId, items[1].chainId);
152
157
  assert.equal(items[0].chainId, body.chainId);
153
158
  assert.deepEqual(items.map((item) => item.status), ['done', 'done']);
154
- assert.deepEqual(items.map((item) => item.skillName), ['wiki-sync', 'wiki-sync']);
159
+ assert.deepEqual(items.map((item) => item.skillName), ['sync-ingest', 'sync-ingest']);
155
160
  });
156
161
 
157
162
  test('E2E-002b parameters reach every objective of a multi-step skill', async (t) => {
@@ -220,13 +225,13 @@ test('E2E-003 cancel: the running step and its chain stop, unrelated queue survi
220
225
  // that silently fragments would show up as extra runs, not as extra objectives.
221
226
  const PERFORMANCE_TABLE = {
222
227
  pipeline: 1,
228
+ 'wiki-sync': 1,
223
229
  'wiki-ingest': 1,
224
230
  'wiki-build': 1,
225
231
  deliver: 1,
226
232
  diagnose: 1,
227
233
  status: 1,
228
234
  'new-template': 1,
229
- 'wiki-sync': 2,
230
235
  };
231
236
 
232
237
  for (const [name, expected] of Object.entries(PERFORMANCE_TABLE)) {