@dotdrelle/wiki-manager 0.15.38 → 0.15.40
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 +5 -0
- package/docker-compose.yml +1 -1
- package/package.json +1 -1
- package/src/cli/wiki-manager.js +5 -3
- package/src/core/agentEvents.js +1 -0
- package/src/core/agentEvents.test.js +32 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/dockerCompose.test.js +8 -0
- package/src/core/mcp.js +1 -1
- package/src/orchestrator/dispatcher.js +4 -2
- package/src/orchestrator/dispatcher.test.js +33 -0
- package/src/runtime/server.js +24 -1
- package/src/runtime/server.test.js +113 -0
package/README.md
CHANGED
|
@@ -771,6 +771,11 @@ capabilityRouting:
|
|
|
771
771
|
allowedAgents: [connectors]
|
|
772
772
|
```
|
|
773
773
|
|
|
774
|
+
The production agent also advertises `workspace.restore` for Git-backed
|
|
775
|
+
rollback. It is workspace-scoped and remains subject to the normal runtime
|
|
776
|
+
approval and lock checks; it can be pinned in the same way when several agents
|
|
777
|
+
provide that capability.
|
|
778
|
+
|
|
774
779
|
#### Compose overrides — optional agents, proxies, local fixes
|
|
775
780
|
|
|
776
781
|
Two override files sit under **`.wiki/compose/`**, one per stack:
|
package/docker-compose.yml
CHANGED
|
@@ -124,7 +124,7 @@ services:
|
|
|
124
124
|
- WORKSPACE_NAME=${WORKSPACE_NAME:-workspace}
|
|
125
125
|
- WIKI_WORKSPACE_PATH=/workspace
|
|
126
126
|
- WIKI_CONFIG_PATH=${WIKI_CONFIG_PATH:-}
|
|
127
|
-
- PRODUCTION_ALLOWED_STEPS=${PRODUCTION_ALLOWED_STEPS:-doctor,ingest,ingest_plan,ingest_apply,build,export,polish,pipeline}
|
|
127
|
+
- PRODUCTION_ALLOWED_STEPS=${PRODUCTION_ALLOWED_STEPS:-doctor,ingest,ingest_plan,ingest_apply,build,export,polish,restore,pipeline}
|
|
128
128
|
- PRODUCTION_REQUIRE_CONFIRMATION=${PRODUCTION_REQUIRE_CONFIRMATION:-false}
|
|
129
129
|
# Parallelism levers — effective concurrency ≈ recommendedConcurrency.
|
|
130
130
|
# Intermediate defaults (4/8). Low profile 2/4, high profile 8/16.
|
package/package.json
CHANGED
package/src/cli/wiki-manager.js
CHANGED
|
@@ -1231,9 +1231,11 @@ async function runRuntime(argv, agent) {
|
|
|
1231
1231
|
),
|
|
1232
1232
|
requireApprovalForMutations: body.capabilityPlan.requireApproval !== false,
|
|
1233
1233
|
},
|
|
1234
|
-
...(
|
|
1235
|
-
? { arguments:
|
|
1236
|
-
:
|
|
1234
|
+
...(body.capabilityPlan.arguments && typeof body.capabilityPlan.arguments === 'object'
|
|
1235
|
+
? { arguments: body.capabilityPlan.arguments }
|
|
1236
|
+
: Array.isArray(body.capabilityPlan.inputs) && body.capabilityPlan.inputs.length > 0
|
|
1237
|
+
? { arguments: { inputs: body.capabilityPlan.inputs } }
|
|
1238
|
+
: {}),
|
|
1237
1239
|
})));
|
|
1238
1240
|
if (!Array.isArray(fragment?.tasks) || fragment.tasks.length === 0) {
|
|
1239
1241
|
dispatchAgentEvent(session, createAgentEvent('assistant_message', {
|
package/src/core/agentEvents.js
CHANGED
|
@@ -556,6 +556,7 @@ function applyEvent(state, event) {
|
|
|
556
556
|
status: 'queued',
|
|
557
557
|
createdAt: event.payload?.createdAt ?? event.ts,
|
|
558
558
|
updatedAt: event.ts,
|
|
559
|
+
...(event.payload?.capabilityPlan !== undefined ? { capabilityPlan: event.payload.capabilityPlan } : {}),
|
|
559
560
|
});
|
|
560
561
|
return;
|
|
561
562
|
case 'control_started':
|
|
@@ -364,6 +364,38 @@ test('reduceAgentEvents: control queue is event sourced and follows run status',
|
|
|
364
364
|
assert.equal(projection.controlQueue[1].status, 'cancelled');
|
|
365
365
|
});
|
|
366
366
|
|
|
367
|
+
test('reduceAgentEvents: control_enqueued preserves a structured capabilityPlan across replay', () => {
|
|
368
|
+
const capabilityPlan = {
|
|
369
|
+
capability: 'workspace.restore',
|
|
370
|
+
operation: 'restore',
|
|
371
|
+
arguments: { run: 'abc123' },
|
|
372
|
+
requireApproval: true,
|
|
373
|
+
};
|
|
374
|
+
const projection = reduceAgentEvents([
|
|
375
|
+
createAgentEvent('control_enqueued', {
|
|
376
|
+
origin: 'runtime',
|
|
377
|
+
workspace: 'docs',
|
|
378
|
+
payload: {
|
|
379
|
+
id: 'control-restore',
|
|
380
|
+
workspace: 'docs',
|
|
381
|
+
input: 'restore',
|
|
382
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
383
|
+
capabilityPlan,
|
|
384
|
+
},
|
|
385
|
+
}),
|
|
386
|
+
createAgentEvent('control_started', {
|
|
387
|
+
origin: 'runtime',
|
|
388
|
+
runId: 'run-control-restore',
|
|
389
|
+
workspace: 'docs',
|
|
390
|
+
payload: { id: 'control-restore', runId: 'run-control-restore' },
|
|
391
|
+
}),
|
|
392
|
+
]);
|
|
393
|
+
|
|
394
|
+
const item = projection.controlQueue.find((entry) => entry.id === 'control-restore');
|
|
395
|
+
assert.deepEqual(item.capabilityPlan, capabilityPlan);
|
|
396
|
+
assert.equal(item.status, 'running');
|
|
397
|
+
});
|
|
398
|
+
|
|
367
399
|
test('reduceAgentEvents: activity-owned plan is used when no orchestrator plan exists', () => {
|
|
368
400
|
const projection = reduceAgentEvents([
|
|
369
401
|
createAgentEvent('activity_upserted', {
|
package/src/core/buildInfo.json
CHANGED
|
@@ -19,6 +19,14 @@ test('workspace compose does not start a per-workspace agent runtime', async ()
|
|
|
19
19
|
);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
test('workspace production agent enables restore by default', async () => {
|
|
23
|
+
const raw = await readFile(new URL('../../docker-compose.yml', import.meta.url), 'utf8');
|
|
24
|
+
const compose = YAML.parse(raw);
|
|
25
|
+
const allowed = compose.services['production-mcp'].environment
|
|
26
|
+
.find((entry) => String(entry).startsWith('PRODUCTION_ALLOWED_STEPS='));
|
|
27
|
+
assert.match(String(allowed), /(?:^|,)restore(?:,|})/);
|
|
28
|
+
});
|
|
29
|
+
|
|
22
30
|
test('shipped compose files never carry a build context', async () => {
|
|
23
31
|
// Ces deux fichiers partent dans le paquet npm, où les dépôts frères
|
|
24
32
|
// (`../agent-external/…`) n'existent pas : un `build:` y rend toute commande
|
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.40';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|
|
@@ -53,7 +53,7 @@ export async function execute(task, assignment, {
|
|
|
53
53
|
session.mcp,
|
|
54
54
|
serverName,
|
|
55
55
|
executeTool,
|
|
56
|
-
executeRequest(task, session),
|
|
56
|
+
executeRequest(task, session, runId),
|
|
57
57
|
signal,
|
|
58
58
|
));
|
|
59
59
|
if (accepted?.accepted === false || accepted?.ok === false) {
|
|
@@ -157,9 +157,11 @@ export async function execute(task, assignment, {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
function executeRequest(task, session) {
|
|
160
|
+
function executeRequest(task, session, runId) {
|
|
161
161
|
return {
|
|
162
162
|
taskId: String(task.id ?? task.step),
|
|
163
|
+
...(runId ? { runId: String(runId) } : {}),
|
|
164
|
+
...(task.requiredCapability ? { capability: String(task.requiredCapability) } : {}),
|
|
163
165
|
idempotencyKey: task.idempotencyKey ?? undefined,
|
|
164
166
|
operation: task.operation,
|
|
165
167
|
workspace: workspaceRequest(session),
|
|
@@ -33,6 +33,39 @@ test('dispatcher returns a retryable logical failure when agent_execute reports
|
|
|
33
33
|
assert.equal(result.error.retryable, true);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
test('dispatcher forwards the orchestration run and required capability to agent_execute', async () => {
|
|
37
|
+
let executeArgs;
|
|
38
|
+
const session = {
|
|
39
|
+
workspace: 'test',
|
|
40
|
+
mcp: {
|
|
41
|
+
production: {
|
|
42
|
+
tools: [{ name: 'agent_execute' }, { name: 'agent_status' }, { name: 'agent_cancel' }],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
activities: {},
|
|
46
|
+
};
|
|
47
|
+
const dispatcher = createDispatcher({
|
|
48
|
+
session,
|
|
49
|
+
pollIntervalMs: 1,
|
|
50
|
+
callTool: async (_mcp, _server, tool, args) => {
|
|
51
|
+
if (tool === 'agent_execute') {
|
|
52
|
+
executeArgs = args;
|
|
53
|
+
return { accepted: true, jobId: 'job-build', status: 'queued' };
|
|
54
|
+
}
|
|
55
|
+
return { jobId: 'job-build', status: 'succeeded', terminal: true, result: { status: 'succeeded' } };
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await dispatcher.execute(
|
|
60
|
+
{ id: 'build-a', requiredCapability: 'document.build', operation: 'build', arguments: {} },
|
|
61
|
+
{ serverName: 'production', agentInstanceId: 'production-main' },
|
|
62
|
+
{ runId: 'run-donna-1', attempt: { attemptId: 'build-a:attempt-1', locks: [], release() {} } },
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
assert.equal(executeArgs.runId, 'run-donna-1');
|
|
66
|
+
assert.equal(executeArgs.capability, 'document.build');
|
|
67
|
+
});
|
|
68
|
+
|
|
36
69
|
test('dispatcher completes when an executor-only agent reports succeeded', async () => {
|
|
37
70
|
const session = {
|
|
38
71
|
workspace: 'test',
|
package/src/runtime/server.js
CHANGED
|
@@ -283,6 +283,27 @@ export function startRuntimeServer({
|
|
|
283
283
|
}
|
|
284
284
|
validateContractInDev('runRequest', { ...body, input });
|
|
285
285
|
if (context.running) {
|
|
286
|
+
// A structured capability plan must never degrade into a free-text
|
|
287
|
+
// control message: its exact arguments and approval policy would
|
|
288
|
+
// otherwise be discarded by the control lane. It can still be
|
|
289
|
+
// queued as a future run when the caller explicitly opts in via
|
|
290
|
+
// intent: 'enqueue' — the control queue carries capabilityPlan
|
|
291
|
+
// through untouched (see enqueueControlRequest/
|
|
292
|
+
// startNextControlRequest) instead of collapsing it to text.
|
|
293
|
+
if (body.capabilityPlan) {
|
|
294
|
+
if (body.intent === 'enqueue') {
|
|
295
|
+
const item = enqueueControlRequest(context, input, { capabilityPlan: body.capabilityPlan });
|
|
296
|
+
void startNextControlRequest(context);
|
|
297
|
+
sendJson(response, 202, {
|
|
298
|
+
accepted: true,
|
|
299
|
+
item,
|
|
300
|
+
...controlStatus(context, store),
|
|
301
|
+
});
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
sendJson(response, 409, { error: 'run_active' });
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
286
307
|
// Blind enqueueing made every message typed during a run
|
|
287
308
|
// invisible until the run ended (serve UI symptom: "no result
|
|
288
309
|
// until the job finished or was stopped"). Classify instead:
|
|
@@ -630,6 +651,7 @@ export function startRuntimeServer({
|
|
|
630
651
|
startRuntimeRun(context, {
|
|
631
652
|
input: item.input,
|
|
632
653
|
workspace: item.workspace ?? context.workspace ?? null,
|
|
654
|
+
...(item.capabilityPlan !== undefined ? { capabilityPlan: item.capabilityPlan } : {}),
|
|
633
655
|
}, { controlItemId: item.id });
|
|
634
656
|
return true;
|
|
635
657
|
}
|
|
@@ -843,7 +865,7 @@ async function handleControlMessage(context, store, input, { intent = null, star
|
|
|
843
865
|
: controlMessage(context?.session, 'converse_while_idle'));
|
|
844
866
|
}
|
|
845
867
|
|
|
846
|
-
function enqueueControlRequest(context, input) {
|
|
868
|
+
function enqueueControlRequest(context, input, { capabilityPlan } = {}) {
|
|
847
869
|
const now = new Date().toISOString();
|
|
848
870
|
const item = {
|
|
849
871
|
id: `control-${randomUUID()}`,
|
|
@@ -853,6 +875,7 @@ function enqueueControlRequest(context, input) {
|
|
|
853
875
|
status: 'queued',
|
|
854
876
|
createdAt: now,
|
|
855
877
|
updatedAt: now,
|
|
878
|
+
...(capabilityPlan !== undefined ? { capabilityPlan } : {}),
|
|
856
879
|
};
|
|
857
880
|
dispatchAgentEvent(context.session, createAgentEvent('control_enqueued', {
|
|
858
881
|
origin: 'runtime',
|
|
@@ -347,6 +347,119 @@ test('runtime server accepts only one active run', async (t) => {
|
|
|
347
347
|
}
|
|
348
348
|
});
|
|
349
349
|
|
|
350
|
+
test('runtime server rejects a structured capability plan while a run is active', async (t) => {
|
|
351
|
+
let releaseRun;
|
|
352
|
+
let handle;
|
|
353
|
+
try {
|
|
354
|
+
handle = await startRuntimeServer({
|
|
355
|
+
host: '127.0.0.1',
|
|
356
|
+
port: 0,
|
|
357
|
+
store: {
|
|
358
|
+
dbPath: ':memory:',
|
|
359
|
+
getState: () => ({ status: 'idle' }),
|
|
360
|
+
listEvents: () => [],
|
|
361
|
+
},
|
|
362
|
+
session: {},
|
|
363
|
+
run: async () => new Promise((resolve) => { releaseRun = resolve; }),
|
|
364
|
+
});
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (err?.code === 'EPERM') {
|
|
367
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
throw err;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const url = `http://127.0.0.1:${handle.port}/run`;
|
|
375
|
+
const first = await fetch(url, {
|
|
376
|
+
method: 'POST',
|
|
377
|
+
headers: { 'Content-Type': 'application/json' },
|
|
378
|
+
body: JSON.stringify({ input: 'active build' }),
|
|
379
|
+
});
|
|
380
|
+
assert.equal(first.status, 202);
|
|
381
|
+
|
|
382
|
+
const restore = await fetch(url, {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
headers: { 'Content-Type': 'application/json' },
|
|
385
|
+
body: JSON.stringify({
|
|
386
|
+
input: 'restore',
|
|
387
|
+
capabilityPlan: {
|
|
388
|
+
capability: 'workspace.restore',
|
|
389
|
+
operation: 'restore',
|
|
390
|
+
arguments: { run: 'abc123' },
|
|
391
|
+
requireApproval: true,
|
|
392
|
+
},
|
|
393
|
+
}),
|
|
394
|
+
});
|
|
395
|
+
assert.equal(restore.status, 409);
|
|
396
|
+
assert.deepEqual(await restore.json(), { error: 'run_active' });
|
|
397
|
+
} finally {
|
|
398
|
+
releaseRun?.();
|
|
399
|
+
await handle.close();
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('runtime server queues a structured capability plan when intent is enqueue', async (t) => {
|
|
404
|
+
let releaseRun;
|
|
405
|
+
let handle;
|
|
406
|
+
try {
|
|
407
|
+
handle = await startRuntimeServer({
|
|
408
|
+
host: '127.0.0.1',
|
|
409
|
+
port: 0,
|
|
410
|
+
store: {
|
|
411
|
+
dbPath: ':memory:',
|
|
412
|
+
getState: () => ({ status: 'idle' }),
|
|
413
|
+
listEvents: () => [],
|
|
414
|
+
},
|
|
415
|
+
session: {},
|
|
416
|
+
run: async () => new Promise((resolve) => { releaseRun = resolve; }),
|
|
417
|
+
});
|
|
418
|
+
} catch (err) {
|
|
419
|
+
if (err?.code === 'EPERM') {
|
|
420
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
throw err;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const url = `http://127.0.0.1:${handle.port}/run`;
|
|
428
|
+
const first = await fetch(url, {
|
|
429
|
+
method: 'POST',
|
|
430
|
+
headers: { 'Content-Type': 'application/json' },
|
|
431
|
+
body: JSON.stringify({ input: 'active build' }),
|
|
432
|
+
});
|
|
433
|
+
assert.equal(first.status, 202);
|
|
434
|
+
|
|
435
|
+
const capabilityPlan = {
|
|
436
|
+
capability: 'workspace.restore',
|
|
437
|
+
operation: 'restore',
|
|
438
|
+
arguments: { run: 'abc123' },
|
|
439
|
+
requireApproval: true,
|
|
440
|
+
};
|
|
441
|
+
const restore = await fetch(url, {
|
|
442
|
+
method: 'POST',
|
|
443
|
+
headers: { 'Content-Type': 'application/json' },
|
|
444
|
+
body: JSON.stringify({ input: 'restore', intent: 'enqueue', capabilityPlan }),
|
|
445
|
+
});
|
|
446
|
+
assert.equal(restore.status, 202);
|
|
447
|
+
const restoreBody = await restore.json();
|
|
448
|
+
assert.equal(restoreBody.accepted, true);
|
|
449
|
+
assert.deepEqual(restoreBody.item.capabilityPlan, capabilityPlan);
|
|
450
|
+
assert.equal(restoreBody.item.status, 'queued');
|
|
451
|
+
|
|
452
|
+
const status = await fetch(`http://127.0.0.1:${handle.port}/control`);
|
|
453
|
+
const statusBody = await status.json();
|
|
454
|
+
const queued = statusBody.controlQueue.find((item) => item.id === restoreBody.item.id);
|
|
455
|
+
assert.ok(queued, 'queued item should be visible in control status');
|
|
456
|
+
assert.deepEqual(queued.capabilityPlan, capabilityPlan);
|
|
457
|
+
} finally {
|
|
458
|
+
releaseRun?.();
|
|
459
|
+
await handle.close();
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
350
463
|
test('runtime server returns the accepted run id and passes it to the runner', async (t) => {
|
|
351
464
|
let receivedBody = null;
|
|
352
465
|
let handle;
|