@dotdrelle/wiki-manager 0.15.70 → 0.15.71
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 +24 -21
- package/agents.docker-compose.yml +5 -1
- package/package.json +1 -1
- package/src/activity/activityAggregator.test.js +2 -2
- package/src/agent/graph.js +1 -0
- package/src/cli/wiki-manager.js +1 -1
- package/src/cli/wiki-manager.test.js +16 -16
- package/src/commands/slash.js +24 -5
- package/src/core/agentEvents.js +122 -25
- package/src/core/agentEvents.test.js +26 -1
- package/src/core/buildInfo.json +2 -2
- package/src/core/commandFailure.test.js +2 -2
- package/src/core/currentArtifact.test.js +5 -5
- package/src/core/mcp.js +1 -1
- package/src/core/mcp.test.js +1 -1
- package/src/core/otherWorkspacesRunning.test.js +6 -6
- package/src/core/runtimeLog.js +35 -1
- package/src/core/runtimeLog.test.js +27 -2
- package/src/core/skillInvocation.test.js +1 -1
- package/src/core/wikiSetup.js +25 -0
- package/src/core/wikiSetup.test.js +35 -0
- package/src/core/wikirc.test.js +6 -6
- package/src/core/workspaceInherit.test.js +14 -14
- package/src/orchestrator/agentRegistry.test.js +6 -6
- package/src/orchestrator/dispatcher.js +70 -26
- package/src/orchestrator/dispatcher.test.js +46 -3
- package/src/orchestrator/providers/deepAgentsProvider.test.js +2 -2
- package/src/orchestrator/providers/runtimeProviders.js +58 -5
- package/src/orchestrator/providers/runtimeProviders.test.js +23 -0
- package/src/orchestrator/scheduler.test.js +4 -4
- package/src/runtime/delegation.test.js +11 -11
- package/src/runtime/runner.test.js +1 -1
- package/src/runtime/server.test.js +2 -2
- package/src/runtime/store.test.js +8 -5
- package/src/runtime/workspaceIsolation.test.js +26 -26
- package/src/shell/RightPane.tsx +14 -2
- package/src/shell/repl.js +24 -2
- package/wiki-workspace +34 -0
package/README.md
CHANGED
|
@@ -126,27 +126,30 @@ Several open projects now build a Markdown wiki with an LLM. They target
|
|
|
126
126
|
**different problems** — the useful questions are *what goes in, what comes out,
|
|
127
127
|
and who operates it*. Snapshot as of 2026; all of these move quickly.
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
129
|
+
✅ first-class · 🟡 partial or indirect · ❌ not a goal
|
|
130
|
+
|
|
131
|
+
Projects compared: [OpenWiki](https://github.com/langchain-ai/openwiki),
|
|
132
|
+
[DeepWiki-Open](https://github.com/asyncfuncai/deepwiki-open),
|
|
133
|
+
[GraphRAG](https://github.com/microsoft/graphrag).
|
|
134
|
+
|
|
135
|
+
<table>
|
|
136
|
+
<thead>
|
|
137
|
+
<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>
|
|
138
|
+
</thead>
|
|
139
|
+
<tbody>
|
|
140
|
+
<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>
|
|
141
|
+
<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>
|
|
142
|
+
<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>
|
|
143
|
+
<tr><td><small>Evidence & 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>
|
|
144
|
+
<tr><td><small>Corpus-wide Q&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>
|
|
145
|
+
<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>
|
|
146
|
+
<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>
|
|
147
|
+
<tr><td><small>Orchestration & 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>
|
|
148
|
+
<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>
|
|
149
|
+
<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>
|
|
150
|
+
<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>
|
|
151
|
+
</tbody>
|
|
152
|
+
</table>
|
|
150
153
|
|
|
151
154
|
**The short version:**
|
|
152
155
|
|
|
@@ -164,7 +164,11 @@ services:
|
|
|
164
164
|
# One file, two readers: the manager routes on agent-runtimes.json, the
|
|
165
165
|
# gateway serves the capabilities of its own entry from the same file.
|
|
166
166
|
# Absolute path, computed by wiki-workspace from the manager state dir.
|
|
167
|
-
|
|
167
|
+
# Declared AFTER the /config data mount: for overlapping bind mounts the
|
|
168
|
+
# child mount wins, and the reverse order silently hid the file under
|
|
169
|
+
# the directory — the gateway degraded to its built-in default
|
|
170
|
+
# capabilities on every start.
|
|
168
171
|
- ${AGENTS_DATA_DIR:-./.agents-data}/gateway:/config
|
|
172
|
+
- ${AGENT_RUNTIMES_FILE:?Set AGENT_RUNTIMES_FILE}:/config/agent-runtimes.json:ro
|
|
169
173
|
- ${WORKSPACES_ROOT:?Set WORKSPACES_ROOT to the directory containing all workspace folders}:/workspaces
|
|
170
174
|
restart: unless-stopped
|
package/package.json
CHANGED
|
@@ -144,12 +144,12 @@ test('aggregateActivity keeps a healthy active task out of the error color when
|
|
|
144
144
|
key: 'activity-b',
|
|
145
145
|
status: 'running',
|
|
146
146
|
terminal: false,
|
|
147
|
-
progress: { percent: 35, stepId: 'running-b', label: 'Ingest application
|
|
147
|
+
progress: { percent: 35, stepId: 'running-b', label: 'Ingest sample-application.md', detail: 'LLM running' },
|
|
148
148
|
}],
|
|
149
149
|
}, []);
|
|
150
150
|
|
|
151
151
|
const line = aggregated.lines[0];
|
|
152
152
|
assert.equal(line.status, '35 %');
|
|
153
153
|
assert.match(line.label, /^\[\.\.\.\]/);
|
|
154
|
-
assert.equal(line.progress.label, 'Ingest application
|
|
154
|
+
assert.equal(line.progress.label, 'Ingest sample-application.md');
|
|
155
155
|
});
|
package/src/agent/graph.js
CHANGED
|
@@ -1248,6 +1248,7 @@ export function buildAgentSystemPrompt(state) {
|
|
|
1248
1248
|
'When an action fails or is refused for lack of an authorization grant or scope (rather than a missing capability), say exactly that and name the primitive that grants it. Do not describe the feature as unavailable.',
|
|
1249
1249
|
'For an action with no matching direct tool, call runtime__delegate with the user objective only. The runtime chooses the capability, operation, agent and plan, including a validated single task for executor-only agents. Never choose those identifiers yourself. Never call <provider>__agent_plan, <provider>__agent_execute, legacy production__production_start_job, wiki__plan_set, or wiki__plan_done from interactive chat.',
|
|
1250
1250
|
'Do not ask the user which sources, files, connectors, or templates to use for an ingest, build, or export: the specialized agent discovers them from the workspace. When the objective is clear (e.g. "lance une ingestion"), delegate it as stated, without a clarifying question.',
|
|
1251
|
+
'The concept map is not a deliverable and has no build, rebuild, reclassify, group or taxonomy pass of its own. Concepts are the folders produced by ingestion itself (wiki/concepts/<concept>/<subject>.md — the concept IS the folder), and the /graph taxonomy derives from them. So "rebuild / refresh / redo the concepts" means run an ingestion (the wiki-ingest skill, or a delegated ingest objective) — never a build or export, and never a separate concept step. Do not offer "rebuild the concepts" alongside build/export as if it were the same family of action.',
|
|
1251
1252
|
'Templates are instruction-only specs and deliverables are regenerated from them. When asked to change what a generated document says, edit the underlying wiki content (wiki_write_page) or the template\'s [[INSTRUCTION: ...]] sections — never write finished prose into a template, because a build copies it verbatim and it can no longer be refreshed from the wiki. template_write refuses prose outside an instruction block, so keep every sentence inside one.',
|
|
1252
1253
|
'Promise only what the resolved capability actually exposes in its declared contract (the input schema the specialized agent publishes for that capability). When the user requests an execution parameter — a batch or chunk size, a count "N at a time", concurrency, ordering, priority, or any tuning knob — apply it only if that parameter exists in the target capability\'s published input schema. Otherwise do not confirm or promise it: delegate the objective, and if the user explicitly asked for that parameter, say plainly in one line that you started the work but do not control that aspect (the runtime and the specialized agent decide it). Never state or imply a parameter was applied when the agent contract cannot enforce it.',
|
|
1253
1254
|
'If runtime__delegate returns a blocker or no specialized provider is available, report only that concrete blocker concisely. Never replace the missing execution path with a suggested slash command, skill, MCP tool name, manual file move, administrator escalation, or alternative workflow unless the user explicitly asks for alternatives.',
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -170,7 +170,7 @@ export async function resolveExecutorArguments({
|
|
|
170
170
|
'Only fill a field when the objective explicitly states or clearly implies its value.',
|
|
171
171
|
'Omit every field that is not stated. Never invent identifiers, queries, filters or counts.',
|
|
172
172
|
// The objective almost always names the workspace ("export the pages of
|
|
173
|
-
// workspace
|
|
173
|
+
// workspace acme"), and the orchestrator already binds it out of band. With
|
|
174
174
|
// one free-text field in the schema and no field for the workspace, a model
|
|
175
175
|
// reliably misbinds the two — that is how a workspace name ended up as a
|
|
176
176
|
// source name and failed the task.
|
|
@@ -66,7 +66,7 @@ test('interactive runtime refreshes configured MCP endpoints that started late',
|
|
|
66
66
|
test('executor-only capabilities receive one manager-authored executable task', () => {
|
|
67
67
|
const fragment = buildExecutorOnlyFragment({
|
|
68
68
|
objective: 'donne-moi mes derniers mails',
|
|
69
|
-
workspace: '
|
|
69
|
+
workspace: 'demo',
|
|
70
70
|
selection: {
|
|
71
71
|
capability: 'external-source.collect',
|
|
72
72
|
operation: 'collect',
|
|
@@ -91,7 +91,7 @@ test('executor-only capabilities receive one manager-authored executable task',
|
|
|
91
91
|
assert.equal(fragment.tasks[0].requiredCapability, 'external-source.collect');
|
|
92
92
|
assert.equal(fragment.tasks[0].operation, 'collect');
|
|
93
93
|
assert.deepEqual(fragment.tasks[0].arguments, { maxMessages: 10 });
|
|
94
|
-
assert.deepEqual(fragment.tasks[0].locks, ['external-source.collect:
|
|
94
|
+
assert.deepEqual(fragment.tasks[0].locks, ['external-source.collect:demo']);
|
|
95
95
|
assert.equal(fragment.tasks[0].requiresApproval, true);
|
|
96
96
|
assert.equal(fragment.tasks[0].approvalClass, 'external-source');
|
|
97
97
|
assert.match(fragment.tasks[0].idempotencyKey, /^[0-9a-f-]{36}$/);
|
|
@@ -263,14 +263,14 @@ const EXPORT_CAPABILITY = {
|
|
|
263
263
|
};
|
|
264
264
|
|
|
265
265
|
test('argument extraction drops a value that only echoes the active workspace', async () => {
|
|
266
|
-
// Regression: "
|
|
266
|
+
// Regression: "export the Confluence pages of the acme workspace" against a
|
|
267
267
|
// schema whose single free-text field is source_name. The model binds the
|
|
268
|
-
// workspace name to it, and the executor fails with "source '
|
|
268
|
+
// workspace name to it, and the executor fails with "source 'acme' not
|
|
269
269
|
// found". The workspace is already bound out of band, so the echo is noise.
|
|
270
270
|
const llm = {
|
|
271
271
|
completeWithTools: async () => ({
|
|
272
272
|
tool_calls: [{
|
|
273
|
-
function: { name: 'set_task_arguments', arguments: JSON.stringify({ source_name: '
|
|
273
|
+
function: { name: 'set_task_arguments', arguments: JSON.stringify({ source_name: 'acme' }) },
|
|
274
274
|
}],
|
|
275
275
|
}),
|
|
276
276
|
};
|
|
@@ -278,9 +278,9 @@ test('argument extraction drops a value that only echoes the active workspace',
|
|
|
278
278
|
assert.deepEqual(
|
|
279
279
|
await resolveExecutorArguments({
|
|
280
280
|
llm,
|
|
281
|
-
objective: '
|
|
281
|
+
objective: 'export the Confluence pages of the acme workspace',
|
|
282
282
|
capability: EXPORT_CAPABILITY,
|
|
283
|
-
workspace: '
|
|
283
|
+
workspace: 'acme',
|
|
284
284
|
}),
|
|
285
285
|
{},
|
|
286
286
|
);
|
|
@@ -303,7 +303,7 @@ test('argument extraction keeps a real value that is not the workspace name', as
|
|
|
303
303
|
llm,
|
|
304
304
|
objective: 'exporter la source EAS_Avant_projet_ACPI',
|
|
305
305
|
capability: EXPORT_CAPABILITY,
|
|
306
|
-
workspace: '
|
|
306
|
+
workspace: 'acme',
|
|
307
307
|
}),
|
|
308
308
|
{ source_name: 'EAS_Avant_projet_ACPI' },
|
|
309
309
|
);
|
|
@@ -320,17 +320,17 @@ test('argument extraction tells the model the workspace is already bound', async
|
|
|
320
320
|
|
|
321
321
|
await resolveExecutorArguments({
|
|
322
322
|
llm,
|
|
323
|
-
objective: '
|
|
323
|
+
objective: 'export the pages of the acme workspace',
|
|
324
324
|
capability: EXPORT_CAPABILITY,
|
|
325
|
-
workspace: '
|
|
325
|
+
workspace: 'acme',
|
|
326
326
|
});
|
|
327
327
|
|
|
328
|
-
assert.match(seenSystem, /already runs against workspace "
|
|
328
|
+
assert.match(seenSystem, /already runs against workspace "acme"/);
|
|
329
329
|
});
|
|
330
330
|
|
|
331
331
|
test('argument extraction rejects a value outside the closed vocabulary', async () => {
|
|
332
|
-
// Regression: "
|
|
333
|
-
// workspace guard removes "
|
|
332
|
+
// Regression: "export the Confluence pages of the demo workspace" — the
|
|
333
|
+
// workspace guard removes "demo", so the model reaches for the next noun and
|
|
334
334
|
// emits "Confluence". Only the agent knows the valid names; once it publishes
|
|
335
335
|
// them as an enum, the orchestrator can check without guessing at meaning.
|
|
336
336
|
const capability = {
|
|
@@ -354,9 +354,9 @@ test('argument extraction rejects a value outside the closed vocabulary', async
|
|
|
354
354
|
assert.deepEqual(
|
|
355
355
|
await resolveExecutorArguments({
|
|
356
356
|
llm,
|
|
357
|
-
objective: '
|
|
357
|
+
objective: 'export the Confluence pages of the demo workspace',
|
|
358
358
|
capability,
|
|
359
|
-
workspace: '
|
|
359
|
+
workspace: 'demo',
|
|
360
360
|
}),
|
|
361
361
|
{},
|
|
362
362
|
);
|
|
@@ -389,7 +389,7 @@ test('argument extraction keeps a value the vocabulary allows', async () => {
|
|
|
389
389
|
llm,
|
|
390
390
|
objective: 'exporter la source EAS_Avant_projet_ACPI',
|
|
391
391
|
capability,
|
|
392
|
-
workspace: '
|
|
392
|
+
workspace: 'acme',
|
|
393
393
|
}),
|
|
394
394
|
{ source_name: 'EAS_Avant_projet_ACPI' },
|
|
395
395
|
);
|
package/src/commands/slash.js
CHANGED
|
@@ -674,10 +674,14 @@ function runtimeProvidersSection(session) {
|
|
|
674
674
|
for (const [runtimeId, list] of byRuntime) {
|
|
675
675
|
const health = list[0]?.health ?? 'unknown';
|
|
676
676
|
const capabilities = list
|
|
677
|
-
.map((agent) => agent.description?.capabilities?.[0]?.id ?? agent.agentInstanceId)
|
|
678
|
-
.join(', ');
|
|
677
|
+
.map((agent) => agent.description?.capabilities?.[0]?.id ?? agent.agentInstanceId);
|
|
679
678
|
lines.push(`${runtimeId}: ${health}`);
|
|
680
|
-
|
|
679
|
+
// One capability per line: the status pane is a fixed-width box that
|
|
680
|
+
// TRUNCATES long lines, and a joined list ended mid-word
|
|
681
|
+
// ("capabilities: agent.review, agent.research, agent"). A per-line list
|
|
682
|
+
// never truncates and stays scannable.
|
|
683
|
+
lines.push('capabilities:');
|
|
684
|
+
for (const capability of capabilities) lines.push(` ${capability}`);
|
|
681
685
|
}
|
|
682
686
|
return sectionBlock('Agentic runtime', lines);
|
|
683
687
|
}
|
|
@@ -882,12 +886,13 @@ function componentInstallAction(missingImages) {
|
|
|
882
886
|
return missingImages.length > 0 ? 'downloaded-and-installed-missing-components' : null;
|
|
883
887
|
}
|
|
884
888
|
|
|
885
|
-
export function localizedOperationResult({ operation, target, status = 'succeeded', componentAction = null, images = [] }) {
|
|
889
|
+
export function localizedOperationResult({ operation, target, status = 'succeeded', componentAction = null, images = [], detail = null }) {
|
|
886
890
|
const facts = JSON.stringify({
|
|
887
891
|
operation,
|
|
888
892
|
target,
|
|
889
893
|
status,
|
|
890
894
|
...(componentAction ? { componentAction, images } : {}),
|
|
895
|
+
...(detail ? { detail } : {}),
|
|
891
896
|
});
|
|
892
897
|
return {
|
|
893
898
|
output: facts,
|
|
@@ -969,7 +974,10 @@ export async function handleSlashCommand(line, context) {
|
|
|
969
974
|
const runAgentCommand = async (fn, verb) => {
|
|
970
975
|
try {
|
|
971
976
|
step(`Agents: ${verb}ing external agents…`);
|
|
972
|
-
|
|
977
|
+
let outcome = null;
|
|
978
|
+
const missingImages = await collectMissingImages(step, async (options) => {
|
|
979
|
+
outcome = await fn(options);
|
|
980
|
+
});
|
|
973
981
|
if (verb === 'start') {
|
|
974
982
|
// `wiki-workspace agents up` generates the agent tokens into the
|
|
975
983
|
// manager .env and adds the profiled entries to mcp.endpoints.json.
|
|
@@ -983,6 +991,17 @@ export async function handleSlashCommand(line, context) {
|
|
|
983
991
|
loadManagerEnv({ override: true });
|
|
984
992
|
await refreshMcpRuntimeStatus(context.session);
|
|
985
993
|
}
|
|
994
|
+
if (outcome?.degraded) {
|
|
995
|
+
step('Agents: stack started degraded — one optional agent failed; the base agents are up.');
|
|
996
|
+
return localizedOperationResult({
|
|
997
|
+
operation: verb,
|
|
998
|
+
target: 'agents',
|
|
999
|
+
status: 'degraded',
|
|
1000
|
+
componentAction: componentInstallAction(missingImages),
|
|
1001
|
+
images: missingImages,
|
|
1002
|
+
detail: rawFailureText(outcome.degradedError),
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
986
1005
|
return localizedOperationResult({
|
|
987
1006
|
operation: verb,
|
|
988
1007
|
target: 'agents',
|
package/src/core/agentEvents.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { normalizeActivity } from './activity.js';
|
|
2
2
|
import { attachActivityToExistingPlan, syncActivitiesToPlan } from './plan.js';
|
|
3
3
|
import { applyPlanPatch, normalizePlanPatch, normalizePlanRevision, rebasePlanPatch } from './planPatch.js';
|
|
4
|
-
import { formatRuntimeLogPayload, normalizeRuntimeLog } from './runtimeLog.js';
|
|
4
|
+
import { formatRuntimeLogPayload, isDispatchPlumbingLine, normalizeRuntimeLog, shortTaskLabel } from './runtimeLog.js';
|
|
5
5
|
import { projectSkillChains, TERMINAL as CONTROL_TERMINAL_STATUSES } from './skillChainView.js';
|
|
6
6
|
import { projectWorkflow } from './workflow.js';
|
|
7
7
|
import { validateContractInDev } from '../contracts/schemas.js';
|
|
@@ -329,16 +329,13 @@ function applyEvent(state, event) {
|
|
|
329
329
|
: state.planRevision + 1;
|
|
330
330
|
return;
|
|
331
331
|
case 'plan.received':
|
|
332
|
-
state.
|
|
333
|
-
state.logs = state.logs.slice(-200);
|
|
332
|
+
appendLog(state, `${logTime(event.ts)} Plan received for run ${String(event.runId ?? event.payload?.runId ?? '')}`.trim());
|
|
334
333
|
return;
|
|
335
334
|
case 'plan.validated':
|
|
336
|
-
state.
|
|
337
|
-
state.logs = state.logs.slice(-200);
|
|
335
|
+
appendLog(state, `${logTime(event.ts)} Plan validated for run ${String(event.runId ?? event.payload?.runId ?? '')}`.trim());
|
|
338
336
|
return;
|
|
339
337
|
case 'plan.rejected':
|
|
340
|
-
state.
|
|
341
|
-
state.logs = state.logs.slice(-200);
|
|
338
|
+
appendLog(state, `${logTime(event.ts)} Plan rejected: ${formatPlanErrors(event.payload?.errors)}`.trim());
|
|
342
339
|
return;
|
|
343
340
|
case 'task_group.created':
|
|
344
341
|
return;
|
|
@@ -346,28 +343,24 @@ function applyEvent(state, event) {
|
|
|
346
343
|
appendCreatedTask(state, event.payload?.task);
|
|
347
344
|
return;
|
|
348
345
|
case 'task.assigned':
|
|
349
|
-
state
|
|
350
|
-
state.logs = state.logs.slice(-200);
|
|
346
|
+
appendLog(state, taskLogLine(state, event, 'assigned'));
|
|
351
347
|
return;
|
|
352
348
|
case 'task.started':
|
|
353
|
-
|
|
354
|
-
|
|
349
|
+
// Silent: always follows `task.assigned` (same task, milliseconds apart),
|
|
350
|
+
// which already printed the "started" line.
|
|
355
351
|
return;
|
|
356
352
|
case 'task.retry_scheduled':
|
|
357
|
-
state
|
|
358
|
-
state.logs = state.logs.slice(-200);
|
|
353
|
+
appendLog(state, taskLogLine(state, event, 'retry'));
|
|
359
354
|
return;
|
|
360
355
|
case 'task.result_returned':
|
|
361
|
-
|
|
362
|
-
|
|
356
|
+
// Silent: an internal transition immediately followed by
|
|
357
|
+
// `task.completed`/`task.failed`, which carry the same result.
|
|
363
358
|
return;
|
|
364
359
|
case 'task.completed':
|
|
365
|
-
state
|
|
366
|
-
state.logs = state.logs.slice(-200);
|
|
360
|
+
appendLog(state, taskLogLine(state, event, 'completed'));
|
|
367
361
|
return;
|
|
368
362
|
case 'task.failed':
|
|
369
|
-
state
|
|
370
|
-
state.logs = state.logs.slice(-200);
|
|
363
|
+
appendLog(state, taskLogLine(state, event, 'failed'));
|
|
371
364
|
return;
|
|
372
365
|
case 'plan.revision_changed':
|
|
373
366
|
if (Array.isArray(event.payload?.tasks)) {
|
|
@@ -381,12 +374,11 @@ function applyEvent(state, event) {
|
|
|
381
374
|
// référence au state : `updatePlanStep` reste une fonction sur un
|
|
382
375
|
// plan, et le journal reste la responsabilité de l'appelant.
|
|
383
376
|
const anomaly = updatePlanStep(state.plan, event.payload ?? {});
|
|
384
|
-
if (anomaly) state.
|
|
377
|
+
if (anomaly) appendLog(state, `${logTime(event.ts)} ${anomaly}`.trim());
|
|
385
378
|
}
|
|
386
379
|
return;
|
|
387
380
|
case 'control_message_received':
|
|
388
|
-
state.
|
|
389
|
-
state.logs = state.logs.slice(-200);
|
|
381
|
+
appendLog(state, `${logTime(event.ts)} Control message: ${String(event.payload?.input ?? '')}`.trim());
|
|
390
382
|
return;
|
|
391
383
|
case 'plan_patch_proposed':
|
|
392
384
|
upsertPlanPatch(state, {
|
|
@@ -568,7 +560,7 @@ function applyEvent(state, event) {
|
|
|
568
560
|
return;
|
|
569
561
|
case 'run_cancelled':
|
|
570
562
|
state.status = 'cancelled';
|
|
571
|
-
state.
|
|
563
|
+
appendLog(state, `${logTime(event.ts)} ${String(event.payload?.message ?? 'Agent run cancelled.')}`.trim());
|
|
572
564
|
// A cancelled run must not leave its plan steps "running/pending" and
|
|
573
565
|
// its activities spinning in the panels: mark every non-terminal one
|
|
574
566
|
// cancelled so the display reflects reality immediately.
|
|
@@ -590,7 +582,7 @@ function applyEvent(state, event) {
|
|
|
590
582
|
already in hand. What ends a run is essential by construction; the
|
|
591
583
|
prefix states that instead of hoping the wording says so.
|
|
592
584
|
*/
|
|
593
|
-
state.
|
|
585
|
+
appendLog(state, `${logTime(event.ts)} Run failed: ${String(event.payload?.message ?? 'Agent run failed.')}`.trim());
|
|
594
586
|
// A dead run must not leave "pending" plan steps and spinning
|
|
595
587
|
// activities in the persisted projection: they reappeared as ghosts
|
|
596
588
|
// at every relaunch ("des trucs dans le plan qui n'existent pas") and
|
|
@@ -667,7 +659,7 @@ function applyEvent(state, event) {
|
|
|
667
659
|
}, event.ts);
|
|
668
660
|
return;
|
|
669
661
|
case 'runtime_log':
|
|
670
|
-
state
|
|
662
|
+
appendLog(state, formatRuntimeLogPayload(event.payload ?? {}, event.ts));
|
|
671
663
|
return;
|
|
672
664
|
default:
|
|
673
665
|
return;
|
|
@@ -1052,6 +1044,111 @@ function formatPlanErrors(errors) {
|
|
|
1052
1044
|
: 'unknown';
|
|
1053
1045
|
}
|
|
1054
1046
|
|
|
1047
|
+
const LOG_TIME_PREFIX = /^\d{2}:\d{2}:\d{2}\s+/;
|
|
1048
|
+
const LOG_REPEAT_SUFFIX = / \(×\d+\)$/;
|
|
1049
|
+
|
|
1050
|
+
/*
|
|
1051
|
+
The single writer into `state.logs` — every push goes through here.
|
|
1052
|
+
|
|
1053
|
+
Two jobs the ad-hoc `push(...); logs = logs.slice(-200)` pairs did unevenly:
|
|
1054
|
+
the 200-entry cap is now applied on every path (the `runtime_log` case never
|
|
1055
|
+
capped and grew without bound during a long run), and a *plumbing* entry
|
|
1056
|
+
(isDispatchPlumbingLine) identical to the one before it — once the HH:MM:SS
|
|
1057
|
+
prefix is dropped — is collapsed into a `(×N)` counter instead of being
|
|
1058
|
+
printed again. `agent_status` polling and repeated progress ticks otherwise
|
|
1059
|
+
bury every readable event under dozens of identical rows. A business line (a
|
|
1060
|
+
▸/✓/✗/↻ transition, "Run failed:", a control message) is never collapsed, so
|
|
1061
|
+
a second genuine failure and its timing are never folded away.
|
|
1062
|
+
*/
|
|
1063
|
+
function appendLog(state, line) {
|
|
1064
|
+
const text = String(line ?? '').trim();
|
|
1065
|
+
if (!text) return;
|
|
1066
|
+
const last = state.logs.at(-1);
|
|
1067
|
+
if (last != null && isDispatchPlumbingLine(text) && isDispatchPlumbingLine(last)) {
|
|
1068
|
+
const bare = (value) => String(value).replace(LOG_TIME_PREFIX, '').replace(LOG_REPEAT_SUFFIX, '');
|
|
1069
|
+
if (bare(last) === bare(text)) {
|
|
1070
|
+
const count = Number(String(last).match(/ \(×(\d+)\)$/)?.[1] ?? '1') + 1;
|
|
1071
|
+
// Keep the LATEST timestamp so the panel shows when it last repeated.
|
|
1072
|
+
state.logs[state.logs.length - 1] = `${text.replace(LOG_REPEAT_SUFFIX, '')} (×${count})`;
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
state.logs.push(text);
|
|
1077
|
+
if (state.logs.length > 200) state.logs = state.logs.slice(-200);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function logTime(ts) {
|
|
1081
|
+
const date = ts ? new Date(ts) : new Date();
|
|
1082
|
+
return Number.isNaN(date.getTime()) ? '' : date.toISOString().slice(11, 19);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function planTaskById(state, taskId) {
|
|
1086
|
+
const id = String(taskId ?? '');
|
|
1087
|
+
if (!id) return null;
|
|
1088
|
+
return (state.plan ?? []).find((step) => String(step.id ?? step.step) === id) ?? null;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/*
|
|
1092
|
+
The persisted taskId is `<runId-uuid>:<slug>-<hash8>` — neither the UUID nor
|
|
1093
|
+
the trailing hash means anything to a reader. Prefer the plan step's business
|
|
1094
|
+
label ("Polish deliverable: proposition/presentation"), then its description,
|
|
1095
|
+
and only fall back to a de-slugified task name when the plan carries neither.
|
|
1096
|
+
*/
|
|
1097
|
+
function taskLabelFor(state, taskId) {
|
|
1098
|
+
const step = planTaskById(state, taskId);
|
|
1099
|
+
const label = step?.label ?? step?.description ?? null;
|
|
1100
|
+
if (label && !/^Step \d+$/.test(label)) return label;
|
|
1101
|
+
return shortTaskLabel(taskId) || String(taskId ?? '') || 'task';
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// One readable line per real task transition. `task.started` and
|
|
1105
|
+
// `task.result_returned` are deliberately silent in the reducer — each is an
|
|
1106
|
+
// internal step between two lines this function already prints (`assigned`
|
|
1107
|
+
// then `completed`/`failed`), and printing them doubled every task in the
|
|
1108
|
+
// Runtime panel.
|
|
1109
|
+
function taskLogLine(state, event, kind) {
|
|
1110
|
+
const payload = event.payload ?? {};
|
|
1111
|
+
const taskId = event.taskId ?? payload.taskId ?? '';
|
|
1112
|
+
const label = taskLabelFor(state, taskId);
|
|
1113
|
+
const time = logTime(event.ts);
|
|
1114
|
+
const step = planTaskById(state, taskId);
|
|
1115
|
+
const capability = payload.assignment?.capability ?? step?.requiredCapability ?? null;
|
|
1116
|
+
const agent = payload.assignment?.agentInstanceId
|
|
1117
|
+
?? payload.agentInstanceId
|
|
1118
|
+
?? payload.result?.assignment?.agentInstanceId
|
|
1119
|
+
?? payload.assignment?.agentId
|
|
1120
|
+
?? null;
|
|
1121
|
+
|
|
1122
|
+
if (kind === 'assigned') {
|
|
1123
|
+
const context = [capability, agent && `→ ${agent}`].filter(Boolean).join(' ');
|
|
1124
|
+
return `${time} ▸ ${label} — started${context ? ` (${context})` : ''}`.trim();
|
|
1125
|
+
}
|
|
1126
|
+
if (kind === 'retry') {
|
|
1127
|
+
const attempt = payload.attempts ?? payload.attempt ?? null;
|
|
1128
|
+
const max = payload.maxAttempts ?? null;
|
|
1129
|
+
const reason = payload.reason
|
|
1130
|
+
?? payload.error?.code
|
|
1131
|
+
?? payload.error?.message
|
|
1132
|
+
?? 'retryable error';
|
|
1133
|
+
const nth = attempt != null ? ` ${attempt}${max ? `/${max}` : ''}` : '';
|
|
1134
|
+
return `${time} ↻ ${label} — retry${nth} (${reason})`.trim();
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const result = payload.result ?? {};
|
|
1138
|
+
if (kind === 'failed') {
|
|
1139
|
+
const error = result.error?.code
|
|
1140
|
+
?? result.error?.message
|
|
1141
|
+
?? payload.error?.code
|
|
1142
|
+
?? payload.error?.message
|
|
1143
|
+
?? (result.status && result.status !== 'succeeded' ? result.status : null);
|
|
1144
|
+
return `${time} ✗ ${label} — failed${error ? `: ${error}` : ''}`.trim();
|
|
1145
|
+
}
|
|
1146
|
+
// completed
|
|
1147
|
+
const outputs = result.outputRefs ?? result.result?.outputRefs ?? [];
|
|
1148
|
+
const count = Array.isArray(outputs) ? outputs.length : 0;
|
|
1149
|
+
return `${time} ✓ ${label} — done${count ? ` (${count} output${count > 1 ? 's' : ''})` : ''}`.trim();
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1055
1152
|
function cloneRef(value) {
|
|
1056
1153
|
return value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : String(value);
|
|
1057
1154
|
}
|
|
@@ -720,8 +720,33 @@ test('run_error names the failure so the essential journal cannot filter it out'
|
|
|
720
720
|
|
|
721
721
|
assert.equal(state.status, 'error');
|
|
722
722
|
const line = state.logs.at(-1);
|
|
723
|
-
assert.match(line,
|
|
723
|
+
assert.match(line, /^\d{2}:\d{2}:\d{2} Run failed: /);
|
|
724
724
|
assert.match(line, /workspace\.restore/);
|
|
725
725
|
// Le mot qui rend l'entrée « essentielle » pour le journal serve.
|
|
726
726
|
assert.match(line, /failed/i);
|
|
727
727
|
});
|
|
728
|
+
|
|
729
|
+
test('appendLog collapses repeated dispatch plumbing but never a repeated business failure', () => {
|
|
730
|
+
const ev = (type, payload, ts, taskId = null) => ({ id: `${type}-${ts}`, ts, type, payload, taskId });
|
|
731
|
+
// Two identical agent_status polls, seconds apart → one (×2) row.
|
|
732
|
+
const polling = reduceAgentEvents([
|
|
733
|
+
ev('runtime_log', { event: 'agent_status', runId: 'r', taskId: 't', detail: 'production_status' }, '2026-07-08T14:00:01.000Z'),
|
|
734
|
+
ev('runtime_log', { event: 'agent_status', runId: 'r', taskId: 't', detail: 'production_status' }, '2026-07-08T14:00:04.000Z'),
|
|
735
|
+
]);
|
|
736
|
+
assert.equal(polling.logs.length, 1);
|
|
737
|
+
assert.match(polling.logs[0], / \(×2\)$/);
|
|
738
|
+
assert.match(polling.logs[0], /^14:00:04 /, 'the counter keeps the most recent timestamp');
|
|
739
|
+
|
|
740
|
+
// A retry that fails the SAME way twice produces two byte-identical failure
|
|
741
|
+
// lines (bar the timestamp) — both must stay visible, a degradation must
|
|
742
|
+
// announce itself (root CLAUDE.md). The guard is that ✗ lines are not
|
|
743
|
+
// plumbing, not that the text differs.
|
|
744
|
+
const failPayload = { taskId: 'a', result: { status: 'failed', error: { code: 'rate_limit' } } };
|
|
745
|
+
const failures = reduceAgentEvents([
|
|
746
|
+
ev('task.failed', failPayload, '2026-07-08T14:00:01.000Z', 'a'),
|
|
747
|
+
ev('task.failed', failPayload, '2026-07-08T14:05:09.000Z', 'a'),
|
|
748
|
+
]);
|
|
749
|
+
const failLines = failures.logs.filter((l) => /✗ a — failed: rate_limit/.test(l));
|
|
750
|
+
assert.equal(failLines.length, 2);
|
|
751
|
+
assert.equal(failLines.some((l) => / \(×\d+\)$/.test(l)), false);
|
|
752
|
+
});
|
package/src/core/buildInfo.json
CHANGED
|
@@ -28,7 +28,7 @@ test('the fallback hint drops the command echo, compose warnings and host paths'
|
|
|
28
28
|
message: [
|
|
29
29
|
COMPOSE_COMMAND,
|
|
30
30
|
'time="2026-07-28T11:37:41+02:00" level=warning msg="The \\"CONNECTORS_MCP_PORT\\" variable is not set."',
|
|
31
|
-
'error while creating mount source path /mnt/c/Users/p/Documents/docker/llm-wiki/workspaces/
|
|
31
|
+
'error while creating mount source path /mnt/c/Users/p/Documents/docker/llm-wiki/workspaces/demo: denied',
|
|
32
32
|
].join('\n'),
|
|
33
33
|
};
|
|
34
34
|
|
|
@@ -39,7 +39,7 @@ test('the fallback hint drops the command echo, compose warnings and host paths'
|
|
|
39
39
|
// Only the basename survives: absolute paths describe this machine's install
|
|
40
40
|
// layout and mean nothing to the person reading the answer.
|
|
41
41
|
assert.doesNotMatch(hint, /\/mnt\/c/);
|
|
42
|
-
assert.match(hint, /
|
|
42
|
+
assert.match(hint, /demo/);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
test('a failed operation reaches Donna as facts, never as docker output', () => {
|
|
@@ -38,10 +38,10 @@ test('artifactFromToolCall ignores read tools and tools without a path', () => {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
test('currentArtifactFor is workspace-scoped', () => {
|
|
41
|
-
const artifact = { workspace: '
|
|
42
|
-
assert.equal(currentArtifactFor({ workspace: '
|
|
41
|
+
const artifact = { workspace: 'acme', path: 'templates/notes/basic.md', kind: 'template' };
|
|
42
|
+
assert.equal(currentArtifactFor({ workspace: 'acme', currentArtifact: artifact }), artifact);
|
|
43
43
|
assert.equal(currentArtifactFor({ workspace: 'other', currentArtifact: artifact }), null);
|
|
44
|
-
assert.equal(currentArtifactFor({ workspace: '
|
|
44
|
+
assert.equal(currentArtifactFor({ workspace: 'acme' }), null);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
test('currentArtifactPromptLine names the artifact for follow-up edits', () => {
|
|
@@ -52,10 +52,10 @@ test('currentArtifactPromptLine names the artifact for follow-up edits', () => {
|
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
test('rememberArtifact records a workspace-scoped artifact and ignores empty paths', () => {
|
|
55
|
-
const session = { workspace: '
|
|
55
|
+
const session = { workspace: 'acme' };
|
|
56
56
|
rememberArtifact(session, { path: 'templates/notes/basic.md', kind: 'template' });
|
|
57
57
|
assert.equal(session.currentArtifact.path, 'templates/notes/basic.md');
|
|
58
|
-
assert.equal(session.currentArtifact.workspace, '
|
|
58
|
+
assert.equal(session.currentArtifact.workspace, 'acme');
|
|
59
59
|
assert.equal(session.currentArtifact.kind, 'template');
|
|
60
60
|
rememberArtifact(session, { path: ' ', kind: 'template' });
|
|
61
61
|
assert.equal(session.currentArtifact.path, 'templates/notes/basic.md');
|
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.
|
|
4
|
+
const WIKI_MANAGER_VERSION = '0.15.71';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|
package/src/core/mcp.test.js
CHANGED
|
@@ -830,7 +830,7 @@ test('callMcpTool re-negotiates and replays once when the agent drops the sessio
|
|
|
830
830
|
|
|
831
831
|
try {
|
|
832
832
|
const endpoint = { status: 'connected', url: 'http://127.0.0.1:3336/mcp/' };
|
|
833
|
-
const result = await callMcpTool({ cme: endpoint }, 'cme', 'cme_status', { workspace: '
|
|
833
|
+
const result = await callMcpTool({ cme: endpoint }, 'cme', 'cme_status', { workspace: 'demo' });
|
|
834
834
|
|
|
835
835
|
assert.equal(result.content[0].text, 'status: configured');
|
|
836
836
|
assert.deepEqual(requests, [
|