@dotdrelle/wiki-manager 0.15.93 → 0.15.96

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 (48) hide show
  1. package/README.md +39 -28
  2. package/docker-compose.yml +1 -1
  3. package/package.json +2 -2
  4. package/src/agent/graph.js +78 -26
  5. package/src/agent/graph.test.js +28 -2
  6. package/src/cli/wiki-manager.js +138 -18
  7. package/src/commands/slash.js +38 -4
  8. package/src/commands/slash.test.js +11 -1
  9. package/src/core/agentEvents.js +24 -3
  10. package/src/core/agentEvents.test.js +37 -0
  11. package/src/core/buildInfo.json +2 -2
  12. package/src/core/dockerCompose.test.js +14 -0
  13. package/src/core/googleGrants.js +0 -3
  14. package/src/core/json.js +9 -0
  15. package/src/core/mcp.js +1 -1
  16. package/src/core/plan.js +0 -4
  17. package/src/core/progressNotes.js +0 -4
  18. package/src/core/skillChainView.js +3 -1
  19. package/src/core/skillCompiler.test.js +21 -1
  20. package/src/core/toolLoop.js +56 -2
  21. package/src/core/toolLoop.test.js +35 -4
  22. package/src/orchestrator/agentRegistry.js +1 -3
  23. package/src/orchestrator/dependencyResolver.js +0 -3
  24. package/src/orchestrator/objectiveResolver.test.js +27 -0
  25. package/src/orchestrator/planValidator.js +1 -3
  26. package/src/orchestrator/providers/runtimeProvider.js +0 -14
  27. package/src/orchestrator/taskStatuses.js +8 -0
  28. package/src/runtime/client.js +0 -16
  29. package/src/runtime/controlDrain.js +6 -3
  30. package/src/runtime/deltaCoalescer.js +53 -0
  31. package/src/runtime/deltaCoalescer.test.js +56 -0
  32. package/src/runtime/loginPage.js +129 -0
  33. package/src/runtime/loginRoutes.test.js +131 -0
  34. package/src/runtime/loginSession.js +223 -0
  35. package/src/runtime/loginSession.test.js +143 -0
  36. package/src/runtime/qrCode.js +15 -0
  37. package/src/runtime/runner.js +29 -3
  38. package/src/runtime/runner.test.js +90 -1
  39. package/src/runtime/server.js +240 -1
  40. package/src/runtime/server.test.js +87 -0
  41. package/src/runtime/skillRun.js +1 -1
  42. package/src/runtime/skillRun.test.js +3 -0
  43. package/src/runtime/totp.js +87 -0
  44. package/src/runtime/totp.test.js +80 -0
  45. package/src/runtime/totpLogin.js +123 -0
  46. package/src/runtime/vendor/qrcode.cjs +2297 -0
  47. package/src/shell/repl.js +7 -3
  48. package/src/orchestrator/.fuse_hidden0000001c00000001 +0 -316
package/README.md CHANGED
@@ -121,47 +121,60 @@ in isolated workspaces.
121
121
 
122
122
  ## How wikiLLM compares
123
123
 
124
- Several open projects now build a Markdown wiki with an LLM. They target
124
+ Several open projects now build an LLM-maintained knowledge layer over your
125
+ sources — some as a wiki, some as a retrieval and answer platform. They target
125
126
  **different problems** — the useful questions are *what goes in, what comes out,
126
- and who operates it*. Snapshot as of 2026; all of these move quickly.
127
+ and who operates it*. Snapshot as of **September 2026**; all of these move quickly.
127
128
 
128
129
  ✅ first-class · 🟡 partial or indirect · ❌ not a goal
129
130
 
130
- Projects compared: [OpenWiki](https://github.com/langchain-ai/openwiki),
131
- [DeepWiki-Open](https://github.com/asyncfuncai/deepwiki-open),
131
+ Projects compared: [OpenWiki](https://github.com/langchain-ai/openwiki) and
132
+ [DeepWiki-Open](https://github.com/asyncfuncai/deepwiki-open) (code / knowledge
133
+ wikis), [RAGFlow](https://github.com/infiniflow/ragflow) and
134
+ [Onyx](https://github.com/onyx-dot-app/onyx) (RAG platforms), and
132
135
  [GraphRAG](https://github.com/microsoft/graphrag).
133
136
 
134
137
  <table>
135
138
  <thead>
136
- <tr><th><small>Need</small></th><th><small><strong>wikiLLM</strong></small></th><th><small><strong>OpenWiki</strong></small></th><th><small><strong>DeepWiki-Open</strong></small></th><th><small><strong>GraphRAG</strong></small></th></tr>
139
+ <tr>
140
+ <th><small>Need</small></th>
141
+ <th><small><strong>wikiLLM</strong></small></th>
142
+ <th><small><strong>Code / knowledge wikis</strong><br>OpenWiki · DeepWiki-Open</small></th>
143
+ <th><small><strong>RAG platforms</strong><br>RAGFlow · Onyx</small></th>
144
+ <th><small><strong>Graph pipeline</strong><br>GraphRAG</small></th>
145
+ </tr>
137
146
  </thead>
138
147
  <tbody>
139
- <tr><td><small>Input</small></td><td><small>✅ Business docs — Confluence, PDF, Office, SaaS</small></td><td><small>✅ Codebase (code mode)</small></td><td><small>✅ Code repo → diagrams</small></td><td><small>🟡 Plain-text corpus only</small></td></tr>
140
- <tr><td><small>Output</small></td><td><small>✅ Maintained wiki <strong>+ deliverables</strong> from your templates</small></td><td><small>🟡 Wiki about the code, for agents</small></td><td><small>🟡 Interactive wiki + diagrams</small></td><td><small>❌ Entity graph + summaries — no wiki</small></td></tr>
141
- <tr><td><small>Keep current</small></td><td><small>✅ Re-ingest / scheduled</small></td><td><small>✅ <code>--update</code>, CI action</small></td><td><small>🟡 Regenerated per run</small></td><td><small>✅ <code>graphrag update</code> (delta)</small></td></tr>
142
- <tr><td><small>Evidence &amp; citations</small></td><td><small>🟡 Cites retrieved context</small></td><td><small>✅ Claims tied to versioned source</small></td><td><small>🟡 RAG-cited answers</small></td><td><small>✅ Citations to text units</small></td></tr>
143
- <tr><td><small>Corpus-wide Q&amp;A</small></td><td><small>🟡 BM25 + vector feeding generation</small></td><td><small>❌</small></td><td><small>🟡 Repo-scoped RAG chat</small></td><td><small>✅ Local/global community search</small></td></tr>
144
- <tr><td><small>Team UI</small></td><td><small>✅ Web console — wiki, graph, chat, runs (single-user today)</small></td><td><small>🟡 Local viewer + CLI chat</small></td><td><small>✅ Self-hosted web app + RAG</small></td><td><small>❌ Library / CLI</small></td></tr>
145
- <tr><td><small>Multi-project isolation</small></td><td><small>✅ Workspaces, own services, ports, secrets</small></td><td><small>❌ One wiki per run</small></td><td><small>❌ One wiki per repo</small></td><td><small>❌ One index per corpus</small></td></tr>
146
- <tr><td><small>Orchestration &amp; governance</small></td><td><small>✅ Approval-gated dispatcher, budgets, idempotent writes, crash recovery</small></td><td><small>❌ One agent loop</small></td><td><small>❌ One generation pipeline</small></td><td><small>❌ Indexing pipeline</small></td></tr>
147
- <tr><td><small>Connectors as services</small></td><td><small>✅ Independent MCP agents (Confluence, docs, e-mail…)</small></td><td><small>🟡 Built-in connector set</small></td><td><small>❌</small></td><td><small>❌</small></td></tr>
148
- <tr><td><small>Offline / local models</small></td><td><small>✅ Per-workspace OpenAI-compatible or gateway (Ollama, vLLM, MLX…)</small></td><td><small>✅ 13+ providers</small></td><td><small>✅ Ollama</small></td><td><small>✅ Any OpenAI-compatible</small></td></tr>
149
- <tr><td><small>License</small></td><td><small>❌ PolyForm <strong>Noncommercial</strong></small></td><td><small>✅ MIT</small></td><td><small>✅ MIT</small></td><td><small>✅ MIT</small></td></tr>
148
+ <tr><td><small>Input</small></td><td><small>✅ Business docs — Confluence, PDF, Office, SaaS</small></td><td><small>✅ Code repo; OpenWiki also personal sources (Notion, Gmail, Slack…)</small></td><td><small>✅ PDF/Office/images, Confluence, S3, 50+ connectors</small></td><td><small>🟡 Plain text</small></td></tr>
149
+ <tr><td><small>Output</small></td><td><small>✅ Maintained wiki <strong>+ deliverables</strong></small></td><td><small>✅ Linked Markdown wiki (+ graph / diagrams)</small></td><td><small>🟡 Grounded RAG answers, not a wiki</small></td><td><small>❌ Entity graph + summaries</small></td></tr>
150
+ <tr><td><small>Agents &amp; actions</small></td><td><small>✅ Capability dispatcher, external agentic runtime, MCP tools</small></td><td><small>✅ Doc-generation agents, MCP connectors</small></td><td><small>✅ Agentic workflow, MCP actions, deep research</small></td><td><small>❌ Indexing pipeline</small></td></tr>
151
+ <tr><td><small>Keep current</small></td><td><small>✅ Re-ingest / scheduled</small></td><td><small>✅ <code>--update</code> / regenerate, CI</small></td><td><small>✅ Scheduled connector sync</small></td><td><small>🟡 Maintenance mode</small></td></tr>
152
+ <tr><td><small>Team UI</small></td><td><small>✅ Web console — wiki, graph, chat, runs</small></td><td><small>🟡 CLI + visualizer; DeepWiki-Open web app</small></td><td><small>✅ Self-hosted web app</small></td><td><small>❌ Library / CLI</small></td></tr>
153
+ <tr><td><small>Multi-project isolation</small></td><td><small>✅ Workspaces, own services, ports, secrets</small></td><td><small>❌ One wiki per repo / directory</small></td><td><small>🟡 Datasets / teams, not isolated services</small></td><td><small>❌ One index per corpus</small></td></tr>
154
+ <tr><td><small>Human approval &amp; governance</small></td><td><small>✅ Approval-gated dispatcher, budgets, crash recovery</small></td><td><small>❌ PR review only</small></td><td><small>🟡 RBAC / SSO (Onyx enterprise)</small></td><td><small>❌</small></td></tr>
155
+ <tr><td><small>License</small></td><td><small>❌ PolyForm <strong>Noncommercial</strong></small></td><td><small>✅ MIT</small></td><td><small>✅ Apache-2.0 / MIT CE</small></td><td><small>✅ MIT</small></td></tr>
150
156
  </tbody>
151
157
  </table>
152
158
 
159
+ <small>Per-project details: OpenWiki emits OKF v0.2 and maintains a versioned
160
+ evidence graph of “grounded claims”; GraphRAG is a research project now in
161
+ maintenance mode; Onyx’s SSO/RBAC live in its enterprise edition.</small>
162
+
153
163
  **The short version:**
154
164
 
155
- - **OpenWiki** and **DeepWiki-Open** document *source code*. Point wikiLLM at a
156
- repository and there is nothing for it to ingest; point either of them at a
157
- stack of Confluence pages and a Word document and that is not their job.
158
- - **GraphRAG** builds *retrieval structure*, not a wiki you read or deliverables
159
- you ship — it is a strong back end for corpus-wide Q&A, and complementary
160
- rather than competing.
161
- - **wikiLLM** is the only one of the four whose output is *both* a browsable wiki
162
- *and* regenerated business documents, and the only one with the operational
163
- layer — isolated projects, bounded approvals, automatic recovery, a web
164
- console — that a shared internal tool needs.
165
+ - **OpenWiki** and **DeepWiki-Open** build wikis from a **code repository**
166
+ (OpenWiki also ingests personal sources — Notion, Gmail, Slack…). Their input
167
+ is code or personal feeds, not a stack of business documents feeding your
168
+ deliverable templates.
169
+ - **RAGFlow** and **Onyx** are RAG platforms: they index sources and answer
170
+ questions from them, with agents and connectors, but the output is an answer,
171
+ not a wiki you browse or a document you ship.
172
+ - **GraphRAG** builds retrieval structure and is now in maintenance mode — a
173
+ complementary back end, not a wiki.
174
+ - **wikiLLM** is the only one whose output is *both* a browsable wiki *and*
175
+ regenerated business documents, and the only one with the operational layer —
176
+ isolated projects, bounded approvals, automatic recovery, a web console —
177
+ that a shared internal tool needs.
165
178
 
166
179
  **What wikiLLM does *not* try to do (today):**
167
180
 
@@ -170,8 +183,6 @@ Projects compared: [OpenWiki](https://github.com/langchain-ai/openwiki),
170
183
  - Serve a true multi-user instance with per-user identity and an attributed
171
184
  audit trail. This is a single-user deployment baseline (see the scope note
172
185
  above).
173
- - Expose a graph-query API over the corpus the way GraphRAG does; retrieval is
174
- BM25 plus a vector index feeding generation.
175
186
  - Ship or host the multi-provider AI gateway — routing to several providers is
176
187
  supported, the gateway itself is infrastructure you bring.
177
188
 
@@ -130,7 +130,7 @@ services:
130
130
  # error. Every compose-deployed ingest then ran without the Lot 4 barrier
131
131
  # and left the published map stale — the very defect that work fixed.
132
132
  # `copy` stays out on purpose: it is the legacy step, opt-in only.
133
- - PRODUCTION_ALLOWED_STEPS=${PRODUCTION_ALLOWED_STEPS:-doctor,doctor_apply,ingest,ingest_plan,ingest_apply,build,export,polish,restore,pipeline}
133
+ - PRODUCTION_ALLOWED_STEPS=${PRODUCTION_ALLOWED_STEPS:-doctor,doctor_apply,copy,ingest,ingest_plan,ingest_apply,ingest_rebuild,build,export,polish,restore,pipeline,lint}
134
134
  - PRODUCTION_REQUIRE_CONFIRMATION=${PRODUCTION_REQUIRE_CONFIRMATION:-false}
135
135
  # Parallelism levers — effective concurrency ≈ recommendedConcurrency.
136
136
  # Intermediate defaults (4/8). Low profile 2/4, high profile 8/16.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdrelle/wiki-manager",
3
- "version": "0.15.93",
3
+ "version": "0.15.96",
4
4
  "description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "scripts": {
21
21
  "start": "bun ./bin/wiki-manager.js",
22
- "test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/skillRecursion.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/workspaceProfile.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/core/runtimeEventAdapter.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.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/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikiWorkspaceStart.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/currentArtifact.test.js src/core/googleGrants.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/taskStatuses.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/orchestrator/providers/fakeRuntimeProvider.test.js src/orchestrator/providers/runtimeProviders.test.js src/orchestrator/providers/dispatcherExternalRuntime.test.js src/orchestrator/providers/deepAgentsProvider.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.test.js src/runtime/store.test.js src/runtime/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js",
22
+ "test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/skillRecursion.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/workspaceProfile.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/core/runtimeEventAdapter.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.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/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikiWorkspaceStart.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/currentArtifact.test.js src/core/googleGrants.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/taskStatuses.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/orchestrator/providers/fakeRuntimeProvider.test.js src/orchestrator/providers/runtimeProviders.test.js src/orchestrator/providers/dispatcherExternalRuntime.test.js src/orchestrator/providers/deepAgentsProvider.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.test.js src/runtime/store.test.js src/runtime/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/deltaCoalescer.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js src/runtime/totp.test.js src/runtime/loginSession.test.js src/runtime/loginRoutes.test.js",
23
23
  "check-versions": "node scripts/check-versions.js",
24
24
  "prepack": "node scripts/check-versions.js",
25
25
  "prepublishOnly": "node scripts/check-versions.js",
@@ -31,6 +31,7 @@ import { artifactFromToolCall, currentArtifactFor, currentArtifactPromptLine, re
31
31
  import { capabilityRegistryForSession } from '../orchestrator/capabilityRegistry.js';
32
32
  import { objectiveForResolution } from '../orchestrator/objectiveResolver.js';
33
33
  import { fetchRuntimeState, postRuntimeCancel, postRuntimeControl, postRuntimeDelegate, postRuntimeKill, postRuntimeSkill } from '../runtime/client.js';
34
+ import { formatPublicSkillInvocation, generateSkillAcknowledgment } from '../runtime/skillRun.js';
34
35
 
35
36
  const MAX_TOOL_ITERATIONS = 80;
36
37
  /**
@@ -555,6 +556,27 @@ function isUnresolvedTargetFailure(rawFailure) {
555
556
  return /file does not exist|does not exist|no files match/i.test(rawFailure);
556
557
  }
557
558
 
559
+ // An agent that plans zero tasks is not a failure: it found nothing to act on
560
+ // (no pending source, no template, no deliverable, no archived source to
561
+ // re-file) and says so in its own synthesis sentence. Relaying that outcome is
562
+ // the whole point — the old generic path called it "could not be started" and
563
+ // sent the reader hunting a connectivity problem that did not exist.
564
+ export function nothingToDoForDonna(rawReason) {
565
+ const reason = String(rawReason ?? '')
566
+ .replace(/\s*Available capabilities:\s*[\s\S]*$/i, '')
567
+ .replace(/\b(?:provider|endpoint)=[^\s,]+/g, '')
568
+ .replace(/\s{2,}/g, ' ')
569
+ .trim()
570
+ .slice(0, 400);
571
+ return JSON.stringify({
572
+ delegated: false,
573
+ nothingToDo: true,
574
+ reason,
575
+ instruction:
576
+ 'The agent ran its planning and found nothing to act on. Answer the user naturally in their language, stating that plainly (use the reason above), and do not call it a failure, do not suggest restarting a service, and do not retry the same action. Do not expose exception names, capability identifiers, tool names, UUIDs, or internal routing details.',
577
+ });
578
+ }
579
+
558
580
  function unresolvedTargetForDonna(rawFailure) {
559
581
  const cleaned = String(rawFailure ?? '')
560
582
  .replace(/\b(?:provider|endpoint)=[^\s,]+/g, '')
@@ -941,14 +963,19 @@ export async function handleRuntimeControlTool(session, tool, args = {}) {
941
963
  });
942
964
  }
943
965
  const result = await postRuntimeDelegate(objective, { url, workspace });
944
- return result?.runId
945
- ? JSON.stringify({
946
- delegated: true,
947
- runId: result.runId,
948
- summary: result.delegation ?? null,
949
- message: `Action started (${String(result.runId).slice(0, 8)}) after real-plan validation: ${result.delegation?.tasks ?? 0} task(s), ${result.delegation?.agent ?? 'resolved agent'}. Execution in progress.`,
950
- })
951
- : `Delegation refused: ${result?.error ?? JSON.stringify(result)}`;
966
+ if (result?.runId) {
967
+ return JSON.stringify({
968
+ delegated: true,
969
+ runId: result.runId,
970
+ summary: result.delegation ?? null,
971
+ message: `Action started (${String(result.runId).slice(0, 8)}) after real-plan validation: ${result.delegation?.tasks ?? 0} task(s), ${result.delegation?.agent ?? 'resolved agent'}. Execution in progress.`,
972
+ });
973
+ }
974
+ const refused = String(result?.error ?? JSON.stringify(result));
975
+ // Same EMPTY_PLAN outcome as the in-run path above: relay it, don't call
976
+ // it a failure.
977
+ if (/^EMPTY_PLAN:\s*/i.test(refused)) return nothingToDoForDonna(refused.replace(/^EMPTY_PLAN:\s*/i, ''));
978
+ return `Delegation refused: ${refused}`;
952
979
  }
953
980
  if (tool === 'run_skill') {
954
981
  const skillName = String(args.skillName ?? '').trim();
@@ -1351,24 +1378,6 @@ export function buildAgentSystemPrompt(state) {
1351
1378
  return customPrompt ? `${customPrompt}\n\n${agentContext}` : agentContext;
1352
1379
  }
1353
1380
 
1354
- export function buildLimitedAgentResponse(state, reason = 'no workspace loaded with .wikirc.yaml') {
1355
- const workspace = state.session.workspace ?? 'no workspace selected';
1356
- const wikirc = state.session.wikirc?.profile ?? 'no profile loaded';
1357
- return [
1358
- `Donna is active. Current workspace: ${workspace}.`,
1359
- `Current wikirc profile: ${wikirc}.`,
1360
- '',
1361
- 'I am the shell agent mode: use `/agent` to route free text through this LangGraph graph, or `/chat` for direct chat.',
1362
- `LLM connection: limited mode (${reason}).`,
1363
- `Available primitives: ${commandList(state.session)}.`,
1364
- '',
1365
- 'Connected MCP tools:',
1366
- formatMcpToolsForAgent(state.session.mcp),
1367
- '',
1368
- 'Limited mode: workspace, Docker Compose tools, MCP calls, /wiki fallback, skill discovery, and headless mode are wired.',
1369
- 'Use `/help` to see deterministic shell commands.',
1370
- ].join('\n');
1371
- }
1372
1381
 
1373
1382
  export function formatLlmUnavailableMessage(reason) {
1374
1383
  const clean = String(reason ?? 'unknown reason').replace(/\s+/g, ' ').trim();
@@ -1877,6 +1886,7 @@ export function createAgentGraph(options = {}) {
1877
1886
  const toolCalls = state.pendingToolCalls ?? [];
1878
1887
  const toolResultMessages = [];
1879
1888
  let terminalFailure = null;
1889
+ let skillLaunch = null;
1880
1890
 
1881
1891
  for (const call of toolCalls) {
1882
1892
  const resolved = resolveToolCallName(state.session.mcp, call.function.name, INTERNAL_TOOL_SERVERS);
@@ -1991,6 +2001,19 @@ export function createAgentGraph(options = {}) {
1991
2001
  if (skillResult?.terminal === true) {
1992
2002
  terminalFailure = skillResult.code ?? 'skill_failed';
1993
2003
  ok = false;
2004
+ } else if (skillResult?.accepted === true) {
2005
+ // The skill owns execution from here. Without this the turn kept
2006
+ // going and the model could re-delegate or refuse the very
2007
+ // objective it had just launched — the "launched, then no agent
2008
+ // capable" contradiction. End the turn with the launch
2009
+ // acknowledgement instead of another tool round.
2010
+ skillLaunch = {
2011
+ publicInput: formatPublicSkillInvocation(
2012
+ skillResult.skill ?? args.skillName ?? '',
2013
+ args.arguments && typeof args.arguments === 'object' ? args.arguments : {},
2014
+ ),
2015
+ objectives: Number(skillResult.objectiveCount ?? skillResult.objectives ?? 1) || 1,
2016
+ };
1994
2017
  }
1995
2018
  }
1996
2019
  if (tool === 'delegate' && /^Runtime control error \(delegate\):/i.test(resultText)) {
@@ -2009,6 +2032,14 @@ export function createAgentGraph(options = {}) {
2009
2032
  missingRequiredFields: needsInput[1].split(',').map((item) => item.trim()).filter(Boolean),
2010
2033
  instruction: 'Ask the user for the missing required information. Do not expose internal validation details.',
2011
2034
  });
2035
+ } else if (/^EMPTY_PLAN:\s*/i.test(delegationFailure)) {
2036
+ // Valid planning, nothing to do (no pending source/template/
2037
+ // deliverable). Not a failure and not retryable — Donna relays
2038
+ // the agent's own sentence instead of a connectivity-sounding
2039
+ // generic error. Checked before the target heuristic: a plan
2040
+ // reason is authoritative and could mention a missing file
2041
+ // without meaning "unresolved target".
2042
+ resultText = nothingToDoForDonna(delegationFailure.replace(/^EMPTY_PLAN:\s*/i, ''));
2012
2043
  } else if (isUnresolvedTargetFailure(delegationFailure)) {
2013
2044
  // A named target that resolves to nothing is not an unsupported
2014
2045
  // action: Donna can look it up and retry (or ask), so the turn
@@ -2147,6 +2178,24 @@ export function createAgentGraph(options = {}) {
2147
2178
  invalidResponseRetries: 0,
2148
2179
  };
2149
2180
  }
2181
+ if (skillLaunch) {
2182
+ // Deterministic, localized acknowledgement, generated in the session
2183
+ // language like the `/turn` skill path. No further model turn: the skill
2184
+ // is launched and owns execution, so nothing can contradict it.
2185
+ const response = await generateSkillAcknowledgment(state.session, skillLaunch).catch(
2186
+ () => `Started ${skillLaunch.publicInput} — ${skillLaunch.objectives} step(s) in progress.`,
2187
+ );
2188
+ return {
2189
+ messages: toolResultMessages,
2190
+ pendingToolCalls: null,
2191
+ response,
2192
+ forceDelegation: false,
2193
+ retryWithoutTool: false,
2194
+ terminalToolFailure: false,
2195
+ invalidToolCallRetries: 0,
2196
+ invalidResponseRetries: 0,
2197
+ };
2198
+ }
2150
2199
  return {
2151
2200
  messages: toolResultMessages,
2152
2201
  pendingToolCalls: null,
@@ -2159,6 +2208,9 @@ export function createAgentGraph(options = {}) {
2159
2208
  }
2160
2209
 
2161
2210
  function routeToolExecutor(state) {
2211
+ // A skill launch already produced its final acknowledgement: end the turn
2212
+ // instead of routing back for another model call that could contradict it.
2213
+ if (state.response != null) return END;
2162
2214
  return 'orchestrator';
2163
2215
  }
2164
2216
 
@@ -3,7 +3,7 @@ import test from 'node:test';
3
3
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
- import { bareToolCallJson, buildAgentSystemPrompt, connectorConfigurationTarget, createAgentGraph, invalidSuggestedSlashCommands, invalidUserFacingToolNames, isOrchestrationBypassTool, knownCapabilityIds, normalizeToolArgumentsFromSchema } from './graph.js';
6
+ import { bareToolCallJson, buildAgentSystemPrompt, connectorConfigurationTarget, createAgentGraph, invalidSuggestedSlashCommands, invalidUserFacingToolNames, isOrchestrationBypassTool, knownCapabilityIds, normalizeToolArgumentsFromSchema, nothingToDoForDonna } from './graph.js';
7
7
 
8
8
  test('user-facing response guard hides MCP identifiers generically', () => {
9
9
  const session = sessionBase();
@@ -13,6 +13,29 @@ test('user-facing response guard hides MCP identifiers generically', () => {
13
13
  );
14
14
  });
15
15
 
16
+ test('an empty agent plan is relayed as "nothing to do", not as a failure', () => {
17
+ // The agent planned zero tasks and said why: no pending source, no template,
18
+ // no deliverable. That is an outcome. The old generic path called it "could
19
+ // not be started" and sent the reader after a connectivity problem that did
20
+ // not exist.
21
+ const payload = JSON.parse(nothingToDoForDonna(
22
+ 'No Markdown files were found in raw/untracked; no ingest operation was planned.',
23
+ ));
24
+ assert.equal(payload.delegated, false);
25
+ assert.equal(payload.nothingToDo, true);
26
+ assert.match(payload.reason, /No Markdown files were found in raw\/untracked/);
27
+ assert.match(payload.instruction, /do not call it a failure/i);
28
+ assert.doesNotMatch(payload.instruction, /EMPTY_PLAN/);
29
+ });
30
+
31
+ test('the nothing-to-do reason strips internal routing details', () => {
32
+ const payload = JSON.parse(nothingToDoForDonna(
33
+ 'No templates were found. Available capabilities: document.build, knowledge.update provider=production endpoint=http://host:3202',
34
+ ));
35
+ assert.doesNotMatch(payload.reason, /Available capabilities/);
36
+ assert.doesNotMatch(payload.reason, /provider=|endpoint=/);
37
+ });
38
+
16
39
  test('CME setup stays direct while CME export execution stays orchestrated', () => {
17
40
  assert.equal(isOrchestrationBypassTool('cme__cme_export_run'), true);
18
41
  assert.equal(isOrchestrationBypassTool('cme__cme_setup'), false);
@@ -601,7 +624,10 @@ test('an explicitly selected skill runs through the intra-runtime path with name
601
624
  },
602
625
  });
603
626
  const result = await createAgentGraph().invoke({ input: 'lance le skill deliver avec le template Quarterly report', session });
604
- assert.equal(result.response, 'Skill mis en file.');
627
+ // Launching a skill ends the turn: the acknowledgement is generated once and
628
+ // the model is not given a second chance to re-delegate or contradict it.
629
+ assert.equal(result.response, 'Started /deliver deliverable="Quarterly report" — 1 step(s) in progress.');
630
+ assert.equal(mainCalls, 1);
605
631
  // `skillStack` accompagne désormais la demande : le run imbriqué démarre après
606
632
  // le nettoyage de celui-ci, et c'est le seul canal par lequel il peut savoir
607
633
  // quelles compétences sont déjà ouvertes au-dessus de lui.
@@ -11,6 +11,7 @@ import { randomUUID } from 'node:crypto';
11
11
  import { spawnSync } from 'node:child_process';
12
12
  import { readFileSync } from 'node:fs';
13
13
  import { mkdir, writeFile } from 'node:fs/promises';
14
+ import { createInterface } from 'node:readline/promises';
14
15
  import { dirname, join, resolve } from 'node:path';
15
16
  import { fileURLToPath } from 'node:url';
16
17
  import { ensureManagerScaffold, loadManagerEnv } from '../core/env.js';
@@ -28,6 +29,8 @@ import { extractActivity, mergePolledActivity, parseJsonText, sessionActivities,
28
29
  import { syncActivitiesToPlan, formatPlanStatus } from '../core/plan.js';
29
30
  import { createAgentEvent, dispatchAgentEvent, reduceAgentEvents } from '../core/agentEvents.js';
30
31
  import { runAgentTurn, runAgenticLoop } from '../core/agentLoop.js';
32
+ import { createDeltaCoalescer } from '../runtime/deltaCoalescer.js';
33
+ import { TERMINAL_STATUS_SET } from '../orchestrator/taskStatuses.js';
31
34
  import { resolveCapabilityConcurrency } from '../orchestrator/scheduler.js';
32
35
  import { capabilityRegistryForSession } from '../orchestrator/capabilityRegistry.js';
33
36
  import { CapabilityUnavailableError, resolve as resolveCapability } from '../orchestrator/capabilityResolver.js';
@@ -49,6 +52,17 @@ function valueAfter(argv, flag) {
49
52
  return argv[index + 1];
50
53
  }
51
54
 
55
+ async function confirmOnTty(question, defaultValue) {
56
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return defaultValue;
57
+ const readline = createInterface({ input: process.stdin, output: process.stdout });
58
+ try {
59
+ const answer = (await readline.question(`${question} [y/N] `)).trim().toLowerCase();
60
+ return answer === 'y' || answer === 'yes';
61
+ } finally {
62
+ readline.close();
63
+ }
64
+ }
65
+
52
66
  function errorDiagnostic(err) {
53
67
  const parts = [];
54
68
  let current = err;
@@ -815,7 +829,7 @@ async function runHeadless(argv, agent) {
815
829
  // export finished, before the ingest had even started. The control queue is the
816
830
  // only place where the whole chain is observable, so the wait is scoped to
817
831
  // chainId and ends when every item of that chain is terminal.
818
- const CHAIN_TERMINAL_STATUSES = new Set(['done', 'failed', 'cancelled', 'skipped']);
832
+ const CHAIN_TERMINAL_STATUSES = TERMINAL_STATUS_SET;
819
833
 
820
834
  export async function waitForRuntimeChain(session, log, {
821
835
  chainId,
@@ -949,6 +963,11 @@ async function runRuntime(argv, agent) {
949
963
  const host = valueAfter(argv, '--host') ?? process.env.WIKI_MANAGER_RUNTIME_HOST ?? '0.0.0.0';
950
964
  const port = Number(valueAfter(argv, '--port') ?? process.env.WIKI_MANAGER_RUNTIME_PORT ?? 7788);
951
965
  const stateDir = valueAfter(argv, '--state-dir') ?? defaultRuntimeStateDir();
966
+ // The TOTP login modules resolve their state directory from the ENV, not
967
+ // from the CLI option: without this, `runtime --state-dir X` would store
968
+ // totp.json/session.json in the DEFAULT manager state dir while the rest of
969
+ // the runtime uses X — sessions issued on one side invisible to the other.
970
+ process.env.WIKI_MANAGER_STATE_DIR = stateDir;
952
971
  const auth = resolveRuntimeAuthToken({ host, stateDir });
953
972
  if (auth.token) process.env.WIKI_MANAGER_RUNTIME_TOKEN = auth.token;
954
973
  if (!Number.isInteger(port) || port <= 0 || port > 65535) {
@@ -1351,7 +1370,15 @@ async function runRuntime(argv, agent) {
1351
1370
  );
1352
1371
  }
1353
1372
  if (!Array.isArray(fragment?.tasks) || fragment.tasks.length === 0) {
1354
- throw new Error(fragment?.summary?.initialSynthesis?.[0] ?? `No task was planned for ${selection.capability}/${selection.operation}.`);
1373
+ // An agent may legitimately plan nothing: no pending source to ingest, no
1374
+ // template, no deliverable, no archived source to re-file. That is an
1375
+ // OUTCOME, not a failure — the agent carries its own human-readable
1376
+ // sentence in summary.initialSynthesis. The stable EMPTY_PLAN sentinel
1377
+ // tells graph.js to relay that outcome instead of the generic "could not
1378
+ // be started" failure (which read as a connectivity problem).
1379
+ const plannedReason = String(fragment?.summary?.initialSynthesis?.[0] ?? '').replace(/\s+/g, ' ').trim();
1380
+ const reason = plannedReason || `${selection.capability} planned no task for ${selection.operation}.`;
1381
+ throw new Error(`EMPTY_PLAN: ${reason}`);
1355
1382
  }
1356
1383
  const validation = validateFragment(fragment, {
1357
1384
  registry: capabilityRegistryForSession(session),
@@ -1714,7 +1741,21 @@ async function runRuntime(argv, agent) {
1714
1741
  workspace: context.workspace ?? ephemeral.workspace ?? null,
1715
1742
  }));
1716
1743
  const messages = conversationSeed({ agentProjection: persistedProjection }, input);
1744
+ // Streaming fragments are coalesced before they are persisted and pushed —
1745
+ // one synchronous SQLite insert (plus one SSE write) per token stalled the
1746
+ // event loop, freezing both chats (serve and ShellUI) while a long answer
1747
+ // was still being produced. Flushed before any non-delta event so ordering
1748
+ // and the final tail are preserved.
1749
+ const deltaCoalescer = createDeltaCoalescer((delta) => dispatchAgentEvent(ephemeral, createAgentEvent('assistant_delta', {
1750
+ origin: 'runtime_turn',
1751
+ turnId,
1752
+ workspace: context.workspace ?? null,
1753
+ payload: { delta },
1754
+ })), { intervalMs: 80 });
1717
1755
  ephemeral._onAgentEvent = (event) => {
1756
+ // Never let a final message (or any other event) overtake the buffered
1757
+ // fragments that precede it: flush them first, in order.
1758
+ if (event.type !== 'assistant_delta' && event.type !== 'assistant_delta_reset') deltaCoalescer.flush();
1718
1759
  const interactiveEvent = {
1719
1760
  ...event,
1720
1761
  origin: 'runtime_turn',
@@ -1760,28 +1801,30 @@ async function runRuntime(argv, agent) {
1760
1801
  response = await runHeadlessChatTurn(ephemeral, input, {
1761
1802
  history,
1762
1803
  onStep: ephemeral._onStep,
1763
- // Fragments de réponse publiés au fil de l'eau. Le réducteur les
1764
- // agrège dans la dernière entrée de conversation (`assistant_delta`),
1765
- // que `assistant_message` vient ensuite figer : les deux interfaces
1766
- // voient la réponse s'écrire, au lieu d'attendre le tour complet.
1767
- onTextDelta: (delta) => dispatchAgentEvent(ephemeral, createAgentEvent('assistant_delta', {
1768
- origin: 'runtime_turn',
1769
- turnId,
1770
- workspace: context.workspace ?? null,
1771
- payload: { delta },
1772
- })),
1773
- onTextReset: () => dispatchAgentEvent(ephemeral, createAgentEvent('assistant_delta_reset', {
1774
- origin: 'runtime_turn',
1775
- turnId,
1776
- workspace: context.workspace ?? null,
1777
- payload: {},
1778
- })),
1804
+ // Fragments de réponse publiés au fil de l'eau, coalescés (voir
1805
+ // deltaCoalescer ci-dessus). Le réducteur les agrège dans la dernière
1806
+ // entrée de conversation (`assistant_delta`), que `assistant_message`
1807
+ // vient ensuite figer : les deux interfaces voient la réponse s'écrire
1808
+ // sans qu'un insert SQLite par token ne bloque le flux.
1809
+ onTextDelta: (delta) => deltaCoalescer.push(delta),
1810
+ onTextReset: () => {
1811
+ deltaCoalescer.reset();
1812
+ dispatchAgentEvent(ephemeral, createAgentEvent('assistant_delta_reset', {
1813
+ origin: 'runtime_turn',
1814
+ turnId,
1815
+ workspace: context.workspace ?? null,
1816
+ payload: {},
1817
+ }));
1818
+ },
1779
1819
  openWikiPages,
1780
1820
  });
1781
1821
  } else {
1782
1822
  ephemeral.openWikiPages = openWikiPages;
1783
1823
  response = await runAgentTurn(agent, ephemeral, input, { messages, signal });
1784
1824
  }
1825
+ // Flush the tail before the turn is finalized, then stop the timer.
1826
+ deltaCoalescer.flush();
1827
+ deltaCoalescer.dispose();
1785
1828
  // Persist the artifact the turn may have opened/edited (template_write,
1786
1829
  // template_read, …) back onto the long-lived session, so the next /turn —
1787
1830
  // chat or agent — sees it. The ephemeral session is otherwise discarded.
@@ -1898,6 +1941,65 @@ export async function runCli(argv) {
1898
1941
  return;
1899
1942
  }
1900
1943
 
1944
+ if (argv[0] === 'login' || argv[0] === 'logout') {
1945
+ const { ensureRuntime } = await import('../runtime/lifecycle.js');
1946
+ const { fetchLoginStatus, requestTotpSession, revokeRuntimeSession } = await import('../runtime/totpLogin.js');
1947
+ let runtime = null;
1948
+ try {
1949
+ runtime = await ensureRuntime();
1950
+ } catch (err) {
1951
+ runtime = { url: null, error: err instanceof Error ? err.message : String(err) };
1952
+ }
1953
+ if (!runtime.url) throw new Error(`Runtime unavailable: ${runtime.error}`);
1954
+ if (argv[0] === 'logout') {
1955
+ const { currentSessionToken } = await import('../runtime/loginSession.js');
1956
+ const revoked = await revokeRuntimeSession(runtime, currentSessionToken());
1957
+ console.log(revoked ? 'Session revoked.' : 'No active session to revoke (or the runtime is not answering).');
1958
+ return;
1959
+ }
1960
+ if (argv.includes('--reset')) {
1961
+ // Re-enrollment after a lost authenticator: wipes the secret and the
1962
+ // active session (local files, not an HTTP route) then runs the normal
1963
+ // login flow, which lands on a fresh enrollment QR code.
1964
+ const confirmed = argv.includes('--yes') || (await confirmOnTty(
1965
+ 'Reset the TOTP enrollment? This revokes the active session.\nContinue?',
1966
+ false,
1967
+ ));
1968
+ if (!confirmed) {
1969
+ console.log('Aborted.');
1970
+ return;
1971
+ }
1972
+ const { resetTotpEnrollment } = await import('../runtime/loginSession.js');
1973
+ resetTotpEnrollment();
1974
+ console.log('TOTP enrollment and the active session were reset — enrolling a new authenticator now.');
1975
+ }
1976
+ const status = await fetchLoginStatus(runtime);
1977
+ if (status && !status.enabled) {
1978
+ console.log('TOTP login is disabled (WIKI_MANAGER_TOTP=off).');
1979
+ return;
1980
+ }
1981
+ if (!argv.includes('--reset') && status?.sessionActive) {
1982
+ // A session already exists, but the *browser* only gets the shared
1983
+ // wiki_session cookie the moment it verifies a code on the runtime's
1984
+ // origin. Re-open the page when asked so a browser that has none can
1985
+ // seed it (and then reuse it on serve, same host) without waiting for
1986
+ // the session to expire first.
1987
+ if (!argv.includes('--no-open')) {
1988
+ const { loginPageUrl, openBrowser } = await import('../runtime/totpLogin.js');
1989
+ try {
1990
+ await openBrowser(loginPageUrl(runtime));
1991
+ } catch {
1992
+ console.log(`Open ${loginPageUrl(runtime)} to seed this browser's session.`);
1993
+ }
1994
+ }
1995
+ console.log(`Session already active until ${new Date(status.sessionExpiresAt).toLocaleString()}.`);
1996
+ return;
1997
+ }
1998
+ const result = await requestTotpSession(runtime, { open: !argv.includes('--no-open') });
1999
+ if (!result.ok) throw new Error(result.error);
2000
+ return;
2001
+ }
2002
+
1901
2003
  if (argv.includes('--setup-wizard')) {
1902
2004
  if (!process.versions.bun) {
1903
2005
  throw new Error('Setup wizard requires Bun. Run: bun ./bin/wiki-manager.js --setup-wizard');
@@ -2008,6 +2110,15 @@ export async function runCli(argv) {
2008
2110
  runtime = unavailableRuntime(err);
2009
2111
  console.error(`Runtime unavailable: ${runtime.error}`);
2010
2112
  }
2113
+ if (runtime.url) {
2114
+ const { requestTotpSession } = await import('../runtime/totpLogin.js');
2115
+ const gate = await requestTotpSession(runtime);
2116
+ if (!gate.ok) {
2117
+ console.error(gate.error);
2118
+ process.exitCode = 1;
2119
+ return;
2120
+ }
2121
+ }
2011
2122
  preflight = withRuntimePreflight(preflight, runtime);
2012
2123
  // render() resolves at mount; the TUI owns renderer teardown. The shared
2013
2124
  // runtime deliberately survives shell exit because `serve` may use it.
@@ -2030,6 +2141,15 @@ export async function runCli(argv) {
2030
2141
  runtime = unavailableRuntime(err);
2031
2142
  console.error(`Runtime unavailable: ${runtime.error}`);
2032
2143
  }
2144
+ if (runtime.url) {
2145
+ const { requestTotpSession } = await import('../runtime/totpLogin.js');
2146
+ const gate = await requestTotpSession(runtime);
2147
+ if (!gate.ok) {
2148
+ console.error(gate.error);
2149
+ process.exitCode = 1;
2150
+ return;
2151
+ }
2152
+ }
2033
2153
  }
2034
2154
  await runShell({ agent, packageJson, runtime });
2035
2155
  }