@dotdrelle/wiki-manager 0.15.66 → 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/.env.example +10 -3
- package/README.md +57 -0
- package/agent-runtimes.example.json +68 -0
- package/agents.docker-compose.yml +39 -1
- package/docker-compose.yml +3 -3
- package/package.json +3 -2
- package/src/activity/activityAggregator.test.js +2 -2
- package/src/agent/graph.js +13 -11
- package/src/agent/skillRecursion.test.js +13 -12
- package/src/cli/wiki-manager.js +125 -37
- package/src/cli/wiki-manager.test.js +16 -16
- package/src/commands/slash.js +59 -5
- package/src/contracts/schemas.js +67 -0
- package/src/core/activity.js +5 -0
- package/src/core/agentEvents.js +139 -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/dockerCompose.test.js +8 -40
- package/src/core/env.js +14 -0
- package/src/core/env.test.js +19 -0
- package/src/core/googleGrants.test.js +1 -1
- 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/runtimeEventAdapter.js +81 -0
- package/src/core/runtimeEventAdapter.test.js +61 -0
- package/src/core/runtimeLog.js +35 -1
- package/src/core/runtimeLog.test.js +27 -2
- package/src/core/skillChainView.test.js +2 -2
- package/src/core/skillCompiler.test.js +1 -1
- package/src/core/skillInvocation.js +13 -8
- package/src/core/skillInvocation.test.js +1 -1
- package/src/core/startupCheck.js +58 -0
- package/src/core/startupCheck.test.js +29 -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.js +1 -22
- package/src/orchestrator/agentRegistry.test.js +6 -6
- package/src/orchestrator/assignmentManager.js +16 -4
- package/src/orchestrator/capabilityRegistry.js +8 -1
- package/src/orchestrator/dispatcher.js +405 -2
- package/src/orchestrator/dispatcher.test.js +158 -4
- package/src/orchestrator/objectiveResolver.js +10 -6
- package/src/orchestrator/objectiveResolver.test.js +26 -27
- package/src/orchestrator/providers/deepAgentsProvider.js +168 -0
- package/src/orchestrator/providers/deepAgentsProvider.test.js +178 -0
- package/src/orchestrator/providers/dispatcherExternalRuntime.test.js +409 -0
- package/src/orchestrator/providers/fakeRuntimeProvider.js +164 -0
- package/src/orchestrator/providers/fakeRuntimeProvider.test.js +201 -0
- package/src/orchestrator/providers/runtimeProvider.js +101 -0
- package/src/orchestrator/providers/runtimeProviders.js +378 -0
- package/src/orchestrator/providers/runtimeProviders.test.js +384 -0
- package/src/orchestrator/resultAggregator.js +35 -2
- package/src/orchestrator/resultAggregator.test.js +62 -0
- package/src/orchestrator/scheduler.test.js +4 -4
- package/src/runtime/delegation.test.js +11 -11
- package/src/runtime/recoveryManager.js +70 -5
- package/src/runtime/runner.test.js +1 -1
- package/src/runtime/server.test.js +2 -2
- package/src/runtime/skillChain.e2e.test.js +2 -2
- package/src/runtime/store.test.js +8 -5
- package/src/runtime/supervisor.js +5 -10
- package/src/runtime/workspaceIsolation.test.js +26 -26
- package/src/shell/RightPane.tsx +23 -3
- package/src/shell/StartupScreen.tsx +44 -7
- package/src/shell/repl.js +24 -2
- package/src/shell/repl.test.js +13 -0
- package/wiki-workspace +53 -3
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 } 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';
|
|
@@ -108,6 +108,23 @@ export function dispatchAgentEvent(session, event) {
|
|
|
108
108
|
return normalized;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// Shared `runtime_log` shaping — was independently reimplemented byte-for-byte
|
|
112
|
+
// in runtime/supervisor.js, orchestrator/agentRegistry.js and
|
|
113
|
+
// orchestrator/providers/runtimeProviders.js (each citing the same "avoid a
|
|
114
|
+
// cycle with supervisor.js" reason, even where no such cycle existed). This
|
|
115
|
+
// module already sits below all three, so it is the one safe common home.
|
|
116
|
+
export function dispatchRuntimeLog(session, message) {
|
|
117
|
+
if (!session) return;
|
|
118
|
+
const payload = normalizeRuntimeLog(message, { session });
|
|
119
|
+
return dispatchAgentEvent(session, createAgentEvent('runtime_log', {
|
|
120
|
+
origin: 'runtime',
|
|
121
|
+
runId: payload.runId ?? null,
|
|
122
|
+
taskId: payload.taskId ?? null,
|
|
123
|
+
workspace: payload.workspaceId ?? null,
|
|
124
|
+
payload,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
|
|
111
128
|
// Full in-memory projection reset for a session. The runtime keeps the live
|
|
112
129
|
// projection in memory (session.agentProjection) and serves it from /state, so
|
|
113
130
|
// interrupting runs is not enough to clear the PLAN/ACTIVITY/LOGS panels — the
|
|
@@ -312,16 +329,13 @@ function applyEvent(state, event) {
|
|
|
312
329
|
: state.planRevision + 1;
|
|
313
330
|
return;
|
|
314
331
|
case 'plan.received':
|
|
315
|
-
state.
|
|
316
|
-
state.logs = state.logs.slice(-200);
|
|
332
|
+
appendLog(state, `${logTime(event.ts)} Plan received for run ${String(event.runId ?? event.payload?.runId ?? '')}`.trim());
|
|
317
333
|
return;
|
|
318
334
|
case 'plan.validated':
|
|
319
|
-
state.
|
|
320
|
-
state.logs = state.logs.slice(-200);
|
|
335
|
+
appendLog(state, `${logTime(event.ts)} Plan validated for run ${String(event.runId ?? event.payload?.runId ?? '')}`.trim());
|
|
321
336
|
return;
|
|
322
337
|
case 'plan.rejected':
|
|
323
|
-
state.
|
|
324
|
-
state.logs = state.logs.slice(-200);
|
|
338
|
+
appendLog(state, `${logTime(event.ts)} Plan rejected: ${formatPlanErrors(event.payload?.errors)}`.trim());
|
|
325
339
|
return;
|
|
326
340
|
case 'task_group.created':
|
|
327
341
|
return;
|
|
@@ -329,28 +343,24 @@ function applyEvent(state, event) {
|
|
|
329
343
|
appendCreatedTask(state, event.payload?.task);
|
|
330
344
|
return;
|
|
331
345
|
case 'task.assigned':
|
|
332
|
-
state
|
|
333
|
-
state.logs = state.logs.slice(-200);
|
|
346
|
+
appendLog(state, taskLogLine(state, event, 'assigned'));
|
|
334
347
|
return;
|
|
335
348
|
case 'task.started':
|
|
336
|
-
|
|
337
|
-
|
|
349
|
+
// Silent: always follows `task.assigned` (same task, milliseconds apart),
|
|
350
|
+
// which already printed the "started" line.
|
|
338
351
|
return;
|
|
339
352
|
case 'task.retry_scheduled':
|
|
340
|
-
state
|
|
341
|
-
state.logs = state.logs.slice(-200);
|
|
353
|
+
appendLog(state, taskLogLine(state, event, 'retry'));
|
|
342
354
|
return;
|
|
343
355
|
case 'task.result_returned':
|
|
344
|
-
|
|
345
|
-
|
|
356
|
+
// Silent: an internal transition immediately followed by
|
|
357
|
+
// `task.completed`/`task.failed`, which carry the same result.
|
|
346
358
|
return;
|
|
347
359
|
case 'task.completed':
|
|
348
|
-
state
|
|
349
|
-
state.logs = state.logs.slice(-200);
|
|
360
|
+
appendLog(state, taskLogLine(state, event, 'completed'));
|
|
350
361
|
return;
|
|
351
362
|
case 'task.failed':
|
|
352
|
-
state
|
|
353
|
-
state.logs = state.logs.slice(-200);
|
|
363
|
+
appendLog(state, taskLogLine(state, event, 'failed'));
|
|
354
364
|
return;
|
|
355
365
|
case 'plan.revision_changed':
|
|
356
366
|
if (Array.isArray(event.payload?.tasks)) {
|
|
@@ -364,12 +374,11 @@ function applyEvent(state, event) {
|
|
|
364
374
|
// référence au state : `updatePlanStep` reste une fonction sur un
|
|
365
375
|
// plan, et le journal reste la responsabilité de l'appelant.
|
|
366
376
|
const anomaly = updatePlanStep(state.plan, event.payload ?? {});
|
|
367
|
-
if (anomaly) state.
|
|
377
|
+
if (anomaly) appendLog(state, `${logTime(event.ts)} ${anomaly}`.trim());
|
|
368
378
|
}
|
|
369
379
|
return;
|
|
370
380
|
case 'control_message_received':
|
|
371
|
-
state.
|
|
372
|
-
state.logs = state.logs.slice(-200);
|
|
381
|
+
appendLog(state, `${logTime(event.ts)} Control message: ${String(event.payload?.input ?? '')}`.trim());
|
|
373
382
|
return;
|
|
374
383
|
case 'plan_patch_proposed':
|
|
375
384
|
upsertPlanPatch(state, {
|
|
@@ -551,7 +560,7 @@ function applyEvent(state, event) {
|
|
|
551
560
|
return;
|
|
552
561
|
case 'run_cancelled':
|
|
553
562
|
state.status = 'cancelled';
|
|
554
|
-
state.
|
|
563
|
+
appendLog(state, `${logTime(event.ts)} ${String(event.payload?.message ?? 'Agent run cancelled.')}`.trim());
|
|
555
564
|
// A cancelled run must not leave its plan steps "running/pending" and
|
|
556
565
|
// its activities spinning in the panels: mark every non-terminal one
|
|
557
566
|
// cancelled so the display reflects reality immediately.
|
|
@@ -573,7 +582,7 @@ function applyEvent(state, event) {
|
|
|
573
582
|
already in hand. What ends a run is essential by construction; the
|
|
574
583
|
prefix states that instead of hoping the wording says so.
|
|
575
584
|
*/
|
|
576
|
-
state.
|
|
585
|
+
appendLog(state, `${logTime(event.ts)} Run failed: ${String(event.payload?.message ?? 'Agent run failed.')}`.trim());
|
|
577
586
|
// A dead run must not leave "pending" plan steps and spinning
|
|
578
587
|
// activities in the persisted projection: they reappeared as ghosts
|
|
579
588
|
// at every relaunch ("des trucs dans le plan qui n'existent pas") and
|
|
@@ -650,7 +659,7 @@ function applyEvent(state, event) {
|
|
|
650
659
|
}, event.ts);
|
|
651
660
|
return;
|
|
652
661
|
case 'runtime_log':
|
|
653
|
-
state
|
|
662
|
+
appendLog(state, formatRuntimeLogPayload(event.payload ?? {}, event.ts));
|
|
654
663
|
return;
|
|
655
664
|
default:
|
|
656
665
|
return;
|
|
@@ -1035,6 +1044,111 @@ function formatPlanErrors(errors) {
|
|
|
1035
1044
|
: 'unknown';
|
|
1036
1045
|
}
|
|
1037
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
|
+
|
|
1038
1152
|
function cloneRef(value) {
|
|
1039
1153
|
return value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : String(value);
|
|
1040
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');
|
|
@@ -27,52 +27,20 @@ test('workspace production agent enables restore by default', async () => {
|
|
|
27
27
|
assert.match(String(allowed), /(?:^|,)restore(?:,|})/);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
test('
|
|
30
|
+
test('the shipped default carries no step the engine retired in 0.15.66', async () => {
|
|
31
31
|
/*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
avait corrigé côté moteur. Le défaut de Python porte `taxonomy` ; un compose
|
|
39
|
-
POSITIONNE toujours la variable, donc ce défaut ne s'applique jamais là.
|
|
32
|
+
0.15.66 a retiré du moteur llm-wiki les commandes `wiki concepts`,
|
|
33
|
+
`reclassify-concepts` et `taxonomy` (simplification : le concept EST le
|
|
34
|
+
dossier). Un défaut livré qui les autoriserait encore ferait échouer le
|
|
35
|
+
pipeline à la première de ces étapes — `unknown command 'concepts'` — sans
|
|
36
|
+
jamais les exécuter. Ces trois étapes ne doivent donc figurer dans AUCUN
|
|
37
|
+
défaut livré, ni ici ni dans agent-production.
|
|
40
38
|
*/
|
|
41
39
|
const raw = await readFile(new URL('../../docker-compose.yml', import.meta.url), 'utf8');
|
|
42
40
|
const compose = YAML.parse(raw);
|
|
43
41
|
const allowed = compose.services['production-mcp'].environment
|
|
44
42
|
.find((entry) => String(entry).startsWith('PRODUCTION_ALLOWED_STEPS='));
|
|
45
|
-
assert.
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('every shipped default allows the concepts step', async () => {
|
|
49
|
-
/*
|
|
50
|
-
Same silent-omission risk as `taxonomy` above, one lot earlier: without
|
|
51
|
-
`concepts`, `wiki concepts --apply` (which writes wiki/concepts-grid.md) is
|
|
52
|
-
never reachable through /wiki-sync, /pipeline, or any orchestrated flow.
|
|
53
|
-
Every ingest then files every concept page under the reserved
|
|
54
|
-
`unclassified` class forever, with nothing surfacing why.
|
|
55
|
-
*/
|
|
56
|
-
const raw = await readFile(new URL('../../docker-compose.yml', import.meta.url), 'utf8');
|
|
57
|
-
const compose = YAML.parse(raw);
|
|
58
|
-
const allowed = compose.services['production-mcp'].environment
|
|
59
|
-
.find((entry) => String(entry).startsWith('PRODUCTION_ALLOWED_STEPS='));
|
|
60
|
-
assert.match(String(allowed), /(?:^|,)concepts(?:,|})/);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('every shipped default allows the reclassify-concepts step', async () => {
|
|
64
|
-
/*
|
|
65
|
-
One step further than `concepts`: without `reclassify-concepts`, a page
|
|
66
|
-
already stuck under wiki/concepts/unclassified stays there even after a
|
|
67
|
-
grid exists — re-ingesting its source is not a reliable fix, since the
|
|
68
|
-
ingest prompt updates an existing leaf at its existing path instead of
|
|
69
|
-
moving it.
|
|
70
|
-
*/
|
|
71
|
-
const raw = await readFile(new URL('../../docker-compose.yml', import.meta.url), 'utf8');
|
|
72
|
-
const compose = YAML.parse(raw);
|
|
73
|
-
const allowed = compose.services['production-mcp'].environment
|
|
74
|
-
.find((entry) => String(entry).startsWith('PRODUCTION_ALLOWED_STEPS='));
|
|
75
|
-
assert.match(String(allowed), /(?:^|,)reclassify-concepts(?:,|})/);
|
|
43
|
+
assert.doesNotMatch(String(allowed), /(?:^|,)(?:concepts|reclassify-concepts|taxonomy)(?:,|})/);
|
|
76
44
|
});
|
|
77
45
|
|
|
78
46
|
test('shipped compose files never carry a build context', async () => {
|
package/src/core/env.js
CHANGED
|
@@ -49,6 +49,10 @@ export function managerMcpEndpointsFile() {
|
|
|
49
49
|
return join(managerStateDir(), 'mcp.endpoints.json');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export function managerAgentRuntimesFile() {
|
|
53
|
+
return join(managerStateDir(), 'agent-runtimes.json');
|
|
54
|
+
}
|
|
55
|
+
|
|
52
56
|
// User-owned compose overrides, one per stack. `.wiki/compose` is persistent
|
|
53
57
|
// operator configuration; `.wiki/runtime` is generated state rewritten by
|
|
54
58
|
// compose commands. Keeping those directories separate makes the lifecycle
|
|
@@ -164,6 +168,16 @@ export function ensureManagerScaffold({ log = () => {} } = {}) {
|
|
|
164
168
|
}
|
|
165
169
|
}
|
|
166
170
|
}
|
|
171
|
+
// External agent runtimes (RFC § 37) are optional and DISABLED in the
|
|
172
|
+
// packaged example: seeding a live endpoint would make every fresh install
|
|
173
|
+
// probe a runtime that does not ship. Copy once, never rewrite — an existing
|
|
174
|
+
// file is the operator's.
|
|
175
|
+
const runtimesFile = managerAgentRuntimesFile();
|
|
176
|
+
const runtimesExample = join(packageRoot, 'agent-runtimes.example.json');
|
|
177
|
+
if (existsSync(runtimesExample) && !existsSync(runtimesFile)) {
|
|
178
|
+
copyFileSync(runtimesExample, runtimesFile);
|
|
179
|
+
created.push('agent-runtimes.json');
|
|
180
|
+
}
|
|
167
181
|
const envFile = managerEnvFile();
|
|
168
182
|
const envExample = join(packageRoot, '.env.example');
|
|
169
183
|
if (!existsSync(envFile) && existsSync(envExample)) {
|
package/src/core/env.test.js
CHANGED
|
@@ -37,6 +37,25 @@ test('scaffold copies the packaged examples into a fresh directory', () => {
|
|
|
37
37
|
});
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
test('scaffold seeds an enabled agent-runtimes example and never rewrites an existing one', () => {
|
|
41
|
+
withTempManagerDir((dir) => {
|
|
42
|
+
const created = ensureManagerScaffold();
|
|
43
|
+
assert.ok(created.includes('agent-runtimes.json'));
|
|
44
|
+
const runtimes = JSON.parse(readFileSync(join(dir, 'agent-runtimes.json'), 'utf8'));
|
|
45
|
+
assert.ok(Array.isArray(runtimes.runtimes));
|
|
46
|
+
assert.equal(runtimes.runtimes[0].enabled, true, 'the scaffold ships the gateway enabled (GATEWAY_ENABLED=true)');
|
|
47
|
+
|
|
48
|
+
// Operator-owned: once present, the file is never touched again.
|
|
49
|
+
writeFileSync(join(dir, 'agent-runtimes.json'), JSON.stringify({ runtimes: [] }));
|
|
50
|
+
const second = ensureManagerScaffold();
|
|
51
|
+
assert.ok(!second.includes('agent-runtimes.json'));
|
|
52
|
+
assert.deepEqual(
|
|
53
|
+
JSON.parse(readFileSync(join(dir, 'agent-runtimes.json'), 'utf8')),
|
|
54
|
+
{ runtimes: [] },
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
40
59
|
test('an install predating the required runtime host receives it on its placeholder', () => {
|
|
41
60
|
withTempManagerDir((dir) => {
|
|
42
61
|
const envFile = join(dir, '.env');
|
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { GOOGLE_GRANTS, GOOGLE_GRANT_LABELS, defaultGoogleGrants } from './googleGrants.js';
|
|
6
6
|
|
|
7
7
|
// `agent-connectors` n'est pas cloné par le CI du manager, qui ne tire que
|
|
8
|
-
// llm-wiki, agent-
|
|
8
|
+
// llm-wiki, agent-production, agent-cme et agent-documents (voir
|
|
9
9
|
// check-versions.js). Les contrôles de cohérence croisée ci-dessous lisent la
|
|
10
10
|
// source de l'agent connectors : sans elle, on les saute plutôt que d'échouer
|
|
11
11
|
// sur un ENOENT. Le flux de release complet (build-and-push.sh) la fournit,
|
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, [
|
|
@@ -27,25 +27,25 @@ function workspaceEntry(root, name) {
|
|
|
27
27
|
|
|
28
28
|
test('the current workspace is never counted as another one', async () => {
|
|
29
29
|
const root = mkdtempSync(join(tmpdir(), 'ws-running-self-'));
|
|
30
|
-
const
|
|
30
|
+
const acme = workspaceEntry(root, 'acme');
|
|
31
31
|
|
|
32
|
-
const busy = await otherWorkspacesRunning({ workspace: '
|
|
32
|
+
const busy = await otherWorkspacesRunning({ workspace: 'acme' }, [acme]);
|
|
33
33
|
|
|
34
34
|
assert.deepEqual(busy, [], 'stopping a workspace must not be blocked by itself');
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
test('an unqueryable workspace does not hold the shared agents hostage', async () => {
|
|
38
38
|
const root = mkdtempSync(join(tmpdir(), 'ws-running-unknown-'));
|
|
39
|
-
const workspaces = [workspaceEntry(root, '
|
|
39
|
+
const workspaces = [workspaceEntry(root, 'acme'), workspaceEntry(root, 'stale')];
|
|
40
40
|
|
|
41
|
-
const busy = await otherWorkspacesRunning({ workspace: '
|
|
41
|
+
const busy = await otherWorkspacesRunning({ workspace: 'acme' }, workspaces);
|
|
42
42
|
|
|
43
43
|
assert.deepEqual(busy, []);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
test('a single workspace, or none at all, never blocks', async () => {
|
|
47
|
-
assert.deepEqual(await otherWorkspacesRunning({ workspace: '
|
|
47
|
+
assert.deepEqual(await otherWorkspacesRunning({ workspace: 'acme' }, []), []);
|
|
48
48
|
assert.deepEqual(await otherWorkspacesRunning({}, []), []);
|
|
49
49
|
// Entries without a name are registry noise, not workspaces.
|
|
50
|
-
assert.deepEqual(await otherWorkspacesRunning({ workspace: '
|
|
50
|
+
assert.deepEqual(await otherWorkspacesRunning({ workspace: 'acme' }, [{}, null]), []);
|
|
51
51
|
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter RuntimeEvent -> événements natifs du manager (RFC § 16).
|
|
3
|
+
*
|
|
4
|
+
* Le flux d'événements d'un runtime externe est traduit ici dans le
|
|
5
|
+
* vocabulaire du reducer (`core/agentEvents.js`) SANS aucune refonte de l'UI :
|
|
6
|
+
*
|
|
7
|
+
* - `message` devient un `assistant_message` : c'est ce que Donna affiche ;
|
|
8
|
+
* - les événements d'action (`tool_*`, `subagent_*`, `approval_required`)
|
|
9
|
+
* deviennent des lignes de journal structurées (`runtime_log`) ;
|
|
10
|
+
* - le raisonnement privé (`agent_thinking`) n'est jamais ré-émis (RFC § 15) ;
|
|
11
|
+
* - les événements terminaux (`run_completed`/`run_failed`/`run_cancelled`)
|
|
12
|
+
* ne sont pas ré-émis : ils sont déjà portés par le poll `status()` du
|
|
13
|
+
* dispatcher, qui construit le résultat de tâche à partir de là.
|
|
14
|
+
*
|
|
15
|
+
* La fonction est pure et déterministe : un événement produit zéro ou
|
|
16
|
+
* plusieurs descripteurs `{ type, payload }`. Le dispatcher porte l'identité
|
|
17
|
+
* run/task au moment de la dépêche.
|
|
18
|
+
*/
|
|
19
|
+
export function mapRuntimeEvent(event) {
|
|
20
|
+
const type = String(event?.type ?? '');
|
|
21
|
+
switch (type) {
|
|
22
|
+
case 'message': {
|
|
23
|
+
const content = String(event?.content ?? event?.message ?? '').trim();
|
|
24
|
+
return content ? [{ type: 'assistant_message', payload: { content } }] : [];
|
|
25
|
+
}
|
|
26
|
+
case 'tool_started':
|
|
27
|
+
return log(`tool ${toolLabel(event)} started`);
|
|
28
|
+
case 'tool_finished': {
|
|
29
|
+
const duration = Number.isFinite(Number(event?.durationMs))
|
|
30
|
+
? ` (${Math.round(Number(event.durationMs))}ms)`
|
|
31
|
+
: '';
|
|
32
|
+
const summary = String(event?.resultSummary ?? '').trim();
|
|
33
|
+
const error = String(event?.error ?? '').trim();
|
|
34
|
+
if (error) return log(`tool ${toolLabel(event)} failed: ${error}${duration}`);
|
|
35
|
+
return log(`tool ${toolLabel(event)} done${duration}${summary ? ` — ${summary}` : ''}`);
|
|
36
|
+
}
|
|
37
|
+
case 'subagent_started':
|
|
38
|
+
return log(`subagent ${subagentLabel(event)} started`);
|
|
39
|
+
case 'subagent_finished':
|
|
40
|
+
return log(`subagent ${subagentLabel(event)} finished`);
|
|
41
|
+
case 'approval_required': {
|
|
42
|
+
// Human-in-the-loop du runtime (RFC § 14) : l'analyse pré-exécution
|
|
43
|
+
// devient une demande d'approbation native. Les mutations annoncées
|
|
44
|
+
// deviennent les classes d'approbation ; le dispatcher attend qu'un
|
|
45
|
+
// grant humain les couvre avant de débloquer le runtime.
|
|
46
|
+
const proposal = event?.proposal && typeof event.proposal === 'object' ? event.proposal : {};
|
|
47
|
+
const mutations = Array.isArray(proposal?.mutations) ? proposal.mutations : [];
|
|
48
|
+
const classes = [...new Set(mutations.map((mutation) => String(mutation?.kind ?? '').trim()).filter(Boolean))];
|
|
49
|
+
return [{
|
|
50
|
+
type: 'approval.requested',
|
|
51
|
+
payload: {
|
|
52
|
+
approvalId: String(event?.approvalId ?? 'runtime-approval'),
|
|
53
|
+
scope: 'run',
|
|
54
|
+
approvalClasses: classes,
|
|
55
|
+
reason: String(event?.reason ?? proposal?.summary ?? ''),
|
|
56
|
+
proposal,
|
|
57
|
+
},
|
|
58
|
+
}];
|
|
59
|
+
}
|
|
60
|
+
case 'run_started':
|
|
61
|
+
case 'run_created':
|
|
62
|
+
case 'agent_thinking':
|
|
63
|
+
case 'run_completed':
|
|
64
|
+
case 'run_failed':
|
|
65
|
+
case 'run_cancelled':
|
|
66
|
+
default:
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function log(message) {
|
|
72
|
+
return [{ type: 'runtime_log', payload: { message } }];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toolLabel(event) {
|
|
76
|
+
return String(event?.tool ?? event?.name ?? 'tool');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function subagentLabel(event) {
|
|
80
|
+
return String(event?.subagent ?? event?.tool ?? event?.name ?? 'subagent');
|
|
81
|
+
}
|