@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.
Files changed (72) hide show
  1. package/.env.example +10 -3
  2. package/README.md +57 -0
  3. package/agent-runtimes.example.json +68 -0
  4. package/agents.docker-compose.yml +39 -1
  5. package/docker-compose.yml +3 -3
  6. package/package.json +3 -2
  7. package/src/activity/activityAggregator.test.js +2 -2
  8. package/src/agent/graph.js +13 -11
  9. package/src/agent/skillRecursion.test.js +13 -12
  10. package/src/cli/wiki-manager.js +125 -37
  11. package/src/cli/wiki-manager.test.js +16 -16
  12. package/src/commands/slash.js +59 -5
  13. package/src/contracts/schemas.js +67 -0
  14. package/src/core/activity.js +5 -0
  15. package/src/core/agentEvents.js +139 -25
  16. package/src/core/agentEvents.test.js +26 -1
  17. package/src/core/buildInfo.json +2 -2
  18. package/src/core/commandFailure.test.js +2 -2
  19. package/src/core/currentArtifact.test.js +5 -5
  20. package/src/core/dockerCompose.test.js +8 -40
  21. package/src/core/env.js +14 -0
  22. package/src/core/env.test.js +19 -0
  23. package/src/core/googleGrants.test.js +1 -1
  24. package/src/core/mcp.js +1 -1
  25. package/src/core/mcp.test.js +1 -1
  26. package/src/core/otherWorkspacesRunning.test.js +6 -6
  27. package/src/core/runtimeEventAdapter.js +81 -0
  28. package/src/core/runtimeEventAdapter.test.js +61 -0
  29. package/src/core/runtimeLog.js +35 -1
  30. package/src/core/runtimeLog.test.js +27 -2
  31. package/src/core/skillChainView.test.js +2 -2
  32. package/src/core/skillCompiler.test.js +1 -1
  33. package/src/core/skillInvocation.js +13 -8
  34. package/src/core/skillInvocation.test.js +1 -1
  35. package/src/core/startupCheck.js +58 -0
  36. package/src/core/startupCheck.test.js +29 -1
  37. package/src/core/wikiSetup.js +25 -0
  38. package/src/core/wikiSetup.test.js +35 -0
  39. package/src/core/wikirc.test.js +6 -6
  40. package/src/core/workspaceInherit.test.js +14 -14
  41. package/src/orchestrator/agentRegistry.js +1 -22
  42. package/src/orchestrator/agentRegistry.test.js +6 -6
  43. package/src/orchestrator/assignmentManager.js +16 -4
  44. package/src/orchestrator/capabilityRegistry.js +8 -1
  45. package/src/orchestrator/dispatcher.js +405 -2
  46. package/src/orchestrator/dispatcher.test.js +158 -4
  47. package/src/orchestrator/objectiveResolver.js +10 -6
  48. package/src/orchestrator/objectiveResolver.test.js +26 -27
  49. package/src/orchestrator/providers/deepAgentsProvider.js +168 -0
  50. package/src/orchestrator/providers/deepAgentsProvider.test.js +178 -0
  51. package/src/orchestrator/providers/dispatcherExternalRuntime.test.js +409 -0
  52. package/src/orchestrator/providers/fakeRuntimeProvider.js +164 -0
  53. package/src/orchestrator/providers/fakeRuntimeProvider.test.js +201 -0
  54. package/src/orchestrator/providers/runtimeProvider.js +101 -0
  55. package/src/orchestrator/providers/runtimeProviders.js +378 -0
  56. package/src/orchestrator/providers/runtimeProviders.test.js +384 -0
  57. package/src/orchestrator/resultAggregator.js +35 -2
  58. package/src/orchestrator/resultAggregator.test.js +62 -0
  59. package/src/orchestrator/scheduler.test.js +4 -4
  60. package/src/runtime/delegation.test.js +11 -11
  61. package/src/runtime/recoveryManager.js +70 -5
  62. package/src/runtime/runner.test.js +1 -1
  63. package/src/runtime/server.test.js +2 -2
  64. package/src/runtime/skillChain.e2e.test.js +2 -2
  65. package/src/runtime/store.test.js +8 -5
  66. package/src/runtime/supervisor.js +5 -10
  67. package/src/runtime/workspaceIsolation.test.js +26 -26
  68. package/src/shell/RightPane.tsx +23 -3
  69. package/src/shell/StartupScreen.tsx +44 -7
  70. package/src/shell/repl.js +24 -2
  71. package/src/shell/repl.test.js +13 -0
  72. package/wiki-workspace +53 -3
@@ -0,0 +1,384 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { capabilityRegistryForSession, createCapabilityRegistry } from '../capabilityRegistry.js';
7
+ import { CapabilityUnavailableError, resolve } from '../capabilityResolver.js';
8
+ import { createAssignmentManager } from '../assignmentManager.js';
9
+ import { resolveObjective } from '../objectiveResolver.js';
10
+ import { createFakeRuntimeProvider } from './fakeRuntimeProvider.js';
11
+ import {
12
+ discoverRuntimeProviderAgents,
13
+ discoverRuntimeProvidersOnce,
14
+ loadAgentRuntimesConfig,
15
+ resolveRuntimeProviders,
16
+ } from './runtimeProviders.js';
17
+
18
+ function mcpAgent(agentInstanceId, capabilityId) {
19
+ return {
20
+ agentInstanceId,
21
+ health: 'available',
22
+ description: {
23
+ contractVersion: '1',
24
+ agentType: agentInstanceId.split('-')[0],
25
+ agentInstanceId,
26
+ displayName: agentInstanceId,
27
+ capabilities: [{
28
+ id: capabilityId,
29
+ version: '1',
30
+ description: capabilityId,
31
+ inputSchema: {},
32
+ outputSchema: {},
33
+ supportedOperations: ['run'],
34
+ }],
35
+ },
36
+ };
37
+ }
38
+
39
+ test('discovery projects a live runtime into synthetic agents', async () => {
40
+ const provider = createFakeRuntimeProvider({
41
+ runtime: 'deepagents',
42
+ capabilities: [{ name: 'agent.review', operations: ['run'] }],
43
+ });
44
+
45
+ const { agents, unavailable } = await discoverRuntimeProviderAgents([
46
+ { id: 'deepagents', provider },
47
+ ]);
48
+
49
+ assert.equal(unavailable.length, 0);
50
+ assert.equal(agents.length, 1);
51
+ const agent = agents[0];
52
+ assert.equal(agent.agentInstanceId, 'deepagents::agent.review');
53
+ assert.equal(agent.providerKind, 'external-runtime');
54
+ assert.equal(agent.health, 'available');
55
+ assert.equal(agent.serverName, null);
56
+ assert.equal(agent.runtimeProvider, provider);
57
+ assert.equal(agent.description.capabilities[0].id, 'agent.review');
58
+ });
59
+
60
+ test('a down runtime yields no agents and is reported', async () => {
61
+ const provider = createFakeRuntimeProvider({ available: false });
62
+
63
+ const { agents, unavailable } = await discoverRuntimeProviderAgents([
64
+ { id: 'deepagents', provider },
65
+ ]);
66
+
67
+ assert.equal(agents.length, 0);
68
+ assert.equal(unavailable.length, 1);
69
+ assert.equal(unavailable[0].runtimeId, 'deepagents');
70
+ });
71
+
72
+ test('the capability registry carries providerKind and runtimeProvider for external entries', async () => {
73
+ const provider = createFakeRuntimeProvider({ capabilities: [{ name: 'agent.review', operations: ['run'] }] });
74
+ const { agents } = await discoverRuntimeProviderAgents([{ id: 'deepagents', provider }]);
75
+
76
+ const registry = createCapabilityRegistry({ agents });
77
+
78
+ const providers = registry.providersFor('agent.review');
79
+ assert.equal(providers.length, 1);
80
+ assert.equal(providers[0].providerKind, 'external-runtime');
81
+ assert.equal(providers[0].runtimeProvider, provider);
82
+ });
83
+
84
+ test('resolve routes agent.* to the external runtime agent', async () => {
85
+ const provider = createFakeRuntimeProvider({ capabilities: [{ name: 'agent.review', operations: ['run'] }] });
86
+ const { agents } = await discoverRuntimeProviderAgents([{ id: 'deepagents', provider }]);
87
+ const registry = createCapabilityRegistry({ agents });
88
+
89
+ const assignment = resolve('agent.review', { workspaceConfig: {}, registry });
90
+
91
+ assert.equal(assignment.agentInstanceId, 'deepagents::agent.review');
92
+ });
93
+
94
+ test('assignmentManager produces an external-runtime assignment with a null serverName', async () => {
95
+ const provider = createFakeRuntimeProvider({ capabilities: [{ name: 'agent.review', operations: ['run'] }] });
96
+ const { agents } = await discoverRuntimeProviderAgents([{ id: 'deepagents', provider }]);
97
+ const session = {
98
+ capabilityRegistry: createCapabilityRegistry({ agents }),
99
+ agentRegistrySnapshot: [],
100
+ agents: [],
101
+ };
102
+ const assign = createAssignmentManager({ session });
103
+
104
+ const assignment = await assign.assign({
105
+ id: 'review-a',
106
+ requiredCapability: 'agent.review',
107
+ operation: 'run',
108
+ arguments: {},
109
+ });
110
+
111
+ assert.equal(assignment.agentInstanceId, 'deepagents::agent.review');
112
+ assert.equal(assignment.serverName, null);
113
+ assert.equal(assignment.runtimeProvider, provider);
114
+ assert.equal(assignment.providerKind, 'external-runtime');
115
+ });
116
+
117
+ test('a down runtime is isolated: agent.* resolves nowhere while MCP capabilities still do', async () => {
118
+ const { agents } = await discoverRuntimeProviderAgents([
119
+ { id: 'deepagents', provider: createFakeRuntimeProvider({ available: false }) },
120
+ ]);
121
+ assert.equal(agents.length, 0);
122
+
123
+ const session = {
124
+ agentRegistry: { snapshot: () => [mcpAgent('production-main', 'knowledge.update')] },
125
+ agentRegistrySnapshot: [],
126
+ };
127
+ const registry = capabilityRegistryForSession(session);
128
+
129
+ // The external capability is absent because its runtime is down.
130
+ assert.throws(
131
+ () => resolve('agent.review', { workspaceConfig: {}, registry }),
132
+ (error) => error instanceof CapabilityUnavailableError && error.reason === 'capability_not_found',
133
+ );
134
+ // The MCP capability is unaffected.
135
+ const mcpAssignment = resolve('knowledge.update', { workspaceConfig: {}, registry });
136
+ assert.equal(mcpAssignment.agentInstanceId, 'production-main');
137
+ });
138
+
139
+ function sessionWithEvents() {
140
+ return { workspace: 'test', mcp: {}, agentEvents: [], activities: {} };
141
+ }
142
+
143
+ test('resolveRuntimeProviders maps enabled entries and skips unknown types', () => {
144
+ const { providers, skipped } = resolveRuntimeProviders([
145
+ { id: 'fake-1', type: 'fake', capabilities: [{ name: 'agent.echo', operations: ['run'] }] },
146
+ { id: 'mystery', type: 'mystery-runtime' },
147
+ { id: 'off', type: 'fake', enabled: false },
148
+ ]);
149
+
150
+ assert.equal(providers.length, 1);
151
+ assert.equal(providers[0].id, 'fake-1');
152
+ assert.equal(providers[0].type, 'fake');
153
+ assert.equal(skipped.length, 1);
154
+ assert.equal(skipped[0].id, 'mystery');
155
+ });
156
+
157
+ test('loadAgentRuntimesConfig reads both array and object forms', () => {
158
+ const dir = mkdtempSync(join(tmpdir(), 'agent-runtimes-'));
159
+ try {
160
+ writeFileSync(join(dir, 'agent-runtimes.json'), JSON.stringify({ runtimes: [{ id: 'a', type: 'fake' }] }));
161
+ assert.deepEqual(loadAgentRuntimesConfig({ stateDir: dir }), [{ id: 'a', type: 'fake' }]);
162
+ } finally {
163
+ rmSync(dir, { recursive: true, force: true });
164
+ }
165
+ });
166
+
167
+ test('loadAgentRuntimesConfig returns [] for a missing or unreadable file', () => {
168
+ const dir = mkdtempSync(join(tmpdir(), 'agent-runtimes-'));
169
+ try {
170
+ assert.deepEqual(loadAgentRuntimesConfig({ stateDir: dir, env: {} }), []);
171
+ writeFileSync(join(dir, 'agent-runtimes.json'), '{not json');
172
+ const logs = [];
173
+ assert.deepEqual(loadAgentRuntimesConfig({ stateDir: dir, log: (message) => logs.push(message), env: {} }), []);
174
+ assert.equal(logs.length, 1);
175
+ } finally {
176
+ rmSync(dir, { recursive: true, force: true });
177
+ }
178
+ });
179
+
180
+ test('GATEWAY_ENABLED implies the deepagents runtime with the bearer token (one switch)', () => {
181
+ const dir = mkdtempSync(join(tmpdir(), 'agent-runtimes-'));
182
+ try {
183
+ const config = loadAgentRuntimesConfig({
184
+ stateDir: dir,
185
+ env: { GATEWAY_ENABLED: 'true', GATEWAY_PORT: '7789', GATEWAY_AUTH_TOKEN: 'tok-1' },
186
+ });
187
+ const implied = config.find((entry) => entry.type === 'deepagents');
188
+ assert.ok(implied, 'the gateway is declared when GATEWAY_ENABLED is set');
189
+ assert.equal(implied.enabled, true);
190
+ assert.equal(implied.endpoint, 'http://localhost:7789');
191
+ assert.deepEqual(implied.headers, { Authorization: 'Bearer tok-1' });
192
+ } finally {
193
+ rmSync(dir, { recursive: true, force: true });
194
+ }
195
+ });
196
+
197
+ test('the local gateway bearer is injected only into the manager-owned host-local entry', () => {
198
+ const dir = mkdtempSync(join(tmpdir(), 'agent-runtimes-'));
199
+ try {
200
+ writeFileSync(join(dir, 'agent-runtimes.json'), JSON.stringify({
201
+ runtimes: [
202
+ { id: 'local', type: 'deepagents', endpoint: 'http://127.0.0.1:7789', enabled: true },
203
+ { id: 'shared', type: 'deepagents', endpoint: 'https://gateway.partner.example', enabled: true },
204
+ { id: 'otherport', type: 'deepagents', endpoint: 'http://localhost:9999', enabled: true },
205
+ ],
206
+ }));
207
+ const config = loadAgentRuntimesConfig({
208
+ stateDir: dir,
209
+ env: { GATEWAY_ENABLED: 'true', GATEWAY_PORT: '7789', GATEWAY_AUTH_TOKEN: 'tok-local' },
210
+ });
211
+ const byId = Object.fromEntries(config.map((entry) => [entry.id, entry]));
212
+ assert.deepEqual(byId.local.headers, { Authorization: 'Bearer tok-local' });
213
+ assert.equal(byId.shared.headers, undefined, 'a foreign gateway host must never receive the local token');
214
+ assert.equal(byId.otherport.headers, undefined, 'a non-gateway port is not the manager-owned gateway');
215
+ } finally {
216
+ rmSync(dir, { recursive: true, force: true });
217
+ }
218
+ });
219
+
220
+ test('an explicit enabled deepagents entry wins over the implied one', () => {
221
+ const dir = mkdtempSync(join(tmpdir(), 'agent-runtimes-'));
222
+ try {
223
+ writeFileSync(join(dir, 'agent-runtimes.json'), JSON.stringify({
224
+ runtimes: [{ id: 'deepagents', type: 'deepagents', endpoint: 'http://custom:9000', enabled: true }],
225
+ }));
226
+ const config = loadAgentRuntimesConfig({ stateDir: dir, env: { GATEWAY_ENABLED: 'true' } });
227
+ assert.equal(config.length, 1);
228
+ assert.equal(config[0].endpoint, 'http://custom:9000');
229
+ } finally {
230
+ rmSync(dir, { recursive: true, force: true });
231
+ }
232
+ });
233
+
234
+ test('discoverRuntimeProvidersOnce populates session.runtimeProviderAgents and announces down runtimes', async () => {
235
+ const session = sessionWithEvents();
236
+
237
+ const agents = await discoverRuntimeProvidersOnce(session, {
238
+ config: [
239
+ { id: 'fake', type: 'fake', capabilities: [{ name: 'agent.echo', operations: ['run'] }] },
240
+ { id: 'down', type: 'fake', available: false },
241
+ ],
242
+ });
243
+
244
+ assert.equal(agents.length, 1);
245
+ assert.equal(session.runtimeProviderAgents.length, 1);
246
+ assert.equal(session.runtimeProviderAgents[0].agentInstanceId, 'fake::agent.echo');
247
+
248
+ const logs = (session.agentEvents ?? []).filter((event) => event.type === 'runtime_log');
249
+ assert.ok(
250
+ logs.some((event) => String(event.payload?.message ?? '').includes('down')
251
+ && String(event.payload?.message ?? '').includes('unavailable')),
252
+ 'a down runtime is announced in the journal',
253
+ );
254
+ });
255
+
256
+ test('discoverRuntimeProvidersOnce announces a down runtime only once across re-scans', async () => {
257
+ const session = sessionWithEvents();
258
+ const config = [{ id: 'down', type: 'fake', available: false }];
259
+
260
+ await discoverRuntimeProvidersOnce(session, { config });
261
+ await discoverRuntimeProvidersOnce(session, { config });
262
+
263
+ const logs = (session.agentEvents ?? []).filter((event) => event.type === 'runtime_log'
264
+ && String(event.payload?.message ?? '').includes('unavailable'));
265
+ assert.equal(logs.length, 1, 'the degradation is announced once, not on every re-scan');
266
+ });
267
+
268
+ test('a transient probe failure keeps the last-known capability set (a failed probe is not a lost agent)', async () => {
269
+ const session = sessionWithEvents();
270
+ const up = [{ id: 'gw', type: 'fake', capabilities: [{ name: 'agent.review', operations: ['run'] }] }];
271
+ const down = [{ id: 'gw', type: 'fake', available: false }];
272
+
273
+ await discoverRuntimeProvidersOnce(session, { config: up });
274
+ assert.equal(session.runtimeProviderAgents.length, 1);
275
+
276
+ // Network blip during a periodic re-scan: discovery returns nothing.
277
+ await discoverRuntimeProvidersOnce(session, { config: down });
278
+ assert.equal(session.runtimeProviderAgents.length, 1, 'the capability survives the blip');
279
+ assert.equal(session.runtimeProviderAgents[0].agentInstanceId, 'gw::agent.review');
280
+
281
+ const logs = (session.agentEvents ?? []).filter((event) => event.type === 'runtime_log'
282
+ && String(event.payload?.message ?? '').includes('keeping'));
283
+ assert.equal(logs.length, 1, 'the preservation is announced');
284
+
285
+ // Recovery: a fresh answer is authoritative again.
286
+ await discoverRuntimeProvidersOnce(session, { config: up });
287
+ assert.equal(session.runtimeProviderAgents.length, 1);
288
+ });
289
+
290
+ test('a healthy runtime that answers is authoritative — its set replaces, never merges with, the last-known one', async () => {
291
+ const session = sessionWithEvents();
292
+ await discoverRuntimeProvidersOnce(session, {
293
+ config: [{ id: 'gw', type: 'fake', capabilities: [{ name: 'agent.review', operations: ['run'] }] }],
294
+ });
295
+ assert.deepEqual(session.runtimeProviderAgents.map((a) => a.agentInstanceId), ['gw::agent.review']);
296
+
297
+ await discoverRuntimeProvidersOnce(session, {
298
+ config: [{ id: 'gw', type: 'fake', capabilities: [{ name: 'agent.consistency', operations: ['run'] }] }],
299
+ });
300
+ assert.deepEqual(
301
+ session.runtimeProviderAgents.map((a) => a.agentInstanceId),
302
+ ['gw::agent.consistency'],
303
+ 'the stale agent.review is gone, not kept alongside',
304
+ );
305
+ });
306
+
307
+ test('GATEWAY_ENABLED inherits the disabled entry\'s declared capabilities', () => {
308
+ const dir = mkdtempSync(join(tmpdir(), 'agent-runtimes-'));
309
+ try {
310
+ writeFileSync(join(dir, 'agent-runtimes.json'), JSON.stringify({
311
+ runtimes: [{
312
+ id: 'deepagents',
313
+ type: 'deepagents',
314
+ endpoint: 'http://agent-runtime:7789',
315
+ enabled: false,
316
+ capabilities: [
317
+ { name: 'agent.notify', operations: ['run'], defaultRequiresApproval: true },
318
+ ],
319
+ }],
320
+ }));
321
+ const config = loadAgentRuntimesConfig({ stateDir: dir, env: { GATEWAY_ENABLED: 'true' } });
322
+ const implied = config.find((entry) => entry.type === 'deepagents');
323
+ assert.ok(implied);
324
+ assert.equal(implied.enabled, true);
325
+ assert.equal(implied.endpoint, 'http://localhost:7789', 'the endpoint stays host-local');
326
+ assert.deepEqual(implied.capabilities, [
327
+ { name: 'agent.notify', operations: ['run'], defaultRequiresApproval: true },
328
+ ], 'the shipped approval metadata is not discarded');
329
+ assert.equal(config.filter((entry) => entry.type === 'deepagents').length, 1, 'no duplicate entry');
330
+ } finally {
331
+ rmSync(dir, { recursive: true, force: true });
332
+ }
333
+ });
334
+
335
+ test('external runtime capabilities carry aliases through to the registry', async () => {
336
+ const provider = createFakeRuntimeProvider({
337
+ capabilities: [{ name: 'agent.review', operations: ['run'], aliases: ['audit', 'review', 'analyze'] }],
338
+ });
339
+ const { agents } = await discoverRuntimeProviderAgents([{ id: 'deepagents', provider }]);
340
+
341
+ assert.deepEqual(agents[0].description.capabilities[0].aliases, ['audit', 'review', 'analyze']);
342
+ assert.equal(agents[0].description.orchestration.canPlan, false);
343
+ assert.equal(agents[0].description.orchestration.singleTaskOnly, true, 'external runtimes are executor-only single-task');
344
+ });
345
+
346
+ test('external runtime capabilities carry mutationClass and defaultRequiresApproval through', async () => {
347
+ const provider = createFakeRuntimeProvider({
348
+ capabilities: [
349
+ { name: 'agent.research', operations: ['run'], mutationClass: 'ingest' },
350
+ { name: 'agent.notify', operations: ['run'], defaultRequiresApproval: true },
351
+ ],
352
+ });
353
+ const { agents } = await discoverRuntimeProviderAgents([{ id: 'deepagents', provider }]);
354
+
355
+ const byName = new Map(
356
+ agents.map((agent) => [agent.description.capabilities[0].id, agent.description.capabilities[0]]),
357
+ );
358
+ assert.equal(byName.get('agent.research').mutationClass, 'ingest');
359
+ assert.equal(byName.get('agent.research').defaultRequiresApproval, undefined);
360
+ assert.equal(byName.get('agent.notify').defaultRequiresApproval, true);
361
+ assert.equal(byName.get('agent.notify').mutationClass, undefined);
362
+ });
363
+
364
+ test('external runtime capabilities carry aliasOperations through', async () => {
365
+ const provider = createFakeRuntimeProvider({
366
+ capabilities: [{ name: 'agent.plan', operations: ['plan', 'run'], aliasOperations: { plan: 'plan', apply: 'run' } }],
367
+ });
368
+ const { agents } = await discoverRuntimeProviderAgents([{ id: 'deepagents', provider }]);
369
+
370
+ assert.deepEqual(agents[0].description.capabilities[0].aliasOperations, { plan: 'plan', apply: 'run' });
371
+ });
372
+
373
+ test('resolveObjective deterministically routes "audit" to agent.review via aliases (no LLM)', async () => {
374
+ const provider = createFakeRuntimeProvider({
375
+ capabilities: [{ name: 'agent.review', operations: ['run'], aliases: ['audit', 'review', 'analyze'] }],
376
+ });
377
+ const { agents } = await discoverRuntimeProviderAgents([{ id: 'deepagents', provider }]);
378
+ const session = { runtimeProviderAgents: agents };
379
+
380
+ const selection = await resolveObjective('audit la couverture conceptuelle', session);
381
+
382
+ assert.equal(selection.capability, 'agent.review');
383
+ assert.equal(selection.operation, 'run');
384
+ });
@@ -2,6 +2,7 @@ import { validateContract } from '../contracts/schemas.js';
2
2
  import { parseJsonText } from '../core/activity.js';
3
3
  import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
4
4
  import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
5
+ import { capabilityRegistryForSession } from './capabilityRegistry.js';
5
6
  import { resolve as resolveCapability } from './capabilityResolver.js';
6
7
  import { integrate } from './planIntegrator.js';
7
8
  import { validateFragment } from './planValidator.js';
@@ -112,7 +113,11 @@ async function maybeExpandPlan(result, {
112
113
  return rejectExpansion({ session, runId, taskId, store, errors: requestValidation.errors.map((message) => ({ code: 'invalid_plan_expansion_request', message })) });
113
114
  }
114
115
 
115
- const effectiveRegistry = registry ?? session.capabilityRegistry;
116
+ // session.capabilityRegistry is never assigned anywhere in production;
117
+ // falling back to it here meant every planExpansionRequest resolved
118
+ // against `undefined` and was unconditionally rejected as
119
+ // capability_unavailable, regardless of whether the capability existed.
120
+ const effectiveRegistry = registry ?? capabilityRegistryForSession(session);
116
121
  let resolved;
117
122
  try {
118
123
  resolved = resolveCapability(request.capability, {
@@ -151,6 +156,24 @@ async function maybeExpandPlan(result, {
151
156
  const toolName = toolNameFor(session, serverName, 'agent_plan');
152
157
  const planRequest = agentPlanRequest(request, session);
153
158
  const fragment = parseToolPayload(await callTool(session.mcp, serverName, toolName, planRequest));
159
+ // A planner that REFUSES answers { ok: false, error } (the production agent
160
+ // does, e.g. "knowledge.update cannot plan operation: doctor"). Feeding that
161
+ // envelope to validateFragment turned the planner's actual sentence into
162
+ // contract noise ("taskGraphFragment.ok is not allowed") — the human saw a
163
+ // schema violation instead of the reason. Surface the planner's words.
164
+ if (fragment && typeof fragment === 'object' && !Array.isArray(fragment) && fragment.ok === false) {
165
+ return rejectExpansion({
166
+ session,
167
+ runId,
168
+ taskId,
169
+ store,
170
+ errors: [{
171
+ code: 'planner_rejected',
172
+ message: String(fragment.error ?? 'the planner rejected the objective without a reason'),
173
+ details: { capability: request.capability, operation: request.operation ?? null },
174
+ }],
175
+ });
176
+ }
154
177
  const validation = validateFragment(fragment, {
155
178
  registry: effectiveRegistry,
156
179
  run: { plannerAgentInstanceId: resolved.agentInstanceId },
@@ -211,7 +234,17 @@ function agentPlanRequest(request, session) {
211
234
  objective: request.objective ?? request.reason ?? undefined,
212
235
  workspace: request.workspace ?? workspaceRequest(session),
213
236
  arguments: request.arguments && typeof request.arguments === 'object' ? request.arguments : {},
214
- constraints: request.constraints && typeof request.constraints === 'object' ? request.constraints : {},
237
+ constraints: {
238
+ ...(request.constraints && typeof request.constraints === 'object' ? request.constraints : {}),
239
+ // Default governance, same as prepareDelegation: mutations wait for a
240
+ // human grant. Omitting it here let the planner answer mutating tasks
241
+ // with requiresApproval:false — the scheduler dispatched an ingest
242
+ // plan with no gate, and the production agent's confirmation guard
243
+ // then rejected every job ("requires confirm=true").
244
+ // requireApprovalForMutations: false from the proposal itself remains
245
+ // an explicit opt-out (a runtime declaring its own policy).
246
+ requireApprovalForMutations: request.constraints?.requireApprovalForMutations !== false,
247
+ },
215
248
  };
216
249
  }
217
250
 
@@ -209,3 +209,65 @@ function expansionRegistry() {
209
209
  },
210
210
  };
211
211
  }
212
+
213
+ test('resultAggregator surfaces the planner rejection instead of contract noise', async () => {
214
+ const session = sessionWithPlan();
215
+ session.approvals = [{ id: 'approval-run', scope: 'run', status: 'approved', runId: 'run-expansion' }];
216
+
217
+ const result = await accept(taskResultWithExpansion(), {
218
+ session,
219
+ runId: 'run-expansion',
220
+ task: session.headlessPlan[0],
221
+ assignment: { agentInstanceId: 'production-main', serverName: 'production' },
222
+ registry: expansionRegistry(),
223
+ callTool: async () => ({ ok: false, error: 'knowledge.update cannot plan operation: doctor' }),
224
+ });
225
+
226
+ assert.equal(result.ok, true, 'the producing task itself completed');
227
+ assert.equal(result.expansion.ok, false);
228
+ assert.equal(result.expansion.errors[0].code, 'planner_rejected');
229
+ assert.match(result.expansion.errors[0].message, /cannot plan operation: doctor/);
230
+ });
231
+
232
+ test('resultAggregator defaults requireApprovalForMutations to true on the plan request', async () => {
233
+ const session = sessionWithPlan();
234
+ session.approvals = [{ id: 'approval-run', scope: 'run', status: 'approved', runId: 'run-expansion' }];
235
+ const calls = [];
236
+
237
+ await accept(taskResultWithExpansion(), {
238
+ session,
239
+ runId: 'run-expansion',
240
+ task: session.headlessPlan[0],
241
+ assignment: { agentInstanceId: 'production-main', serverName: 'production' },
242
+ registry: expansionRegistry(),
243
+ callTool: async (_mcp, serverName, toolName, args) => {
244
+ calls.push(args);
245
+ return expansionFragment();
246
+ },
247
+ });
248
+
249
+ assert.equal(calls[0].constraints.requireApprovalForMutations, true);
250
+ });
251
+
252
+ test('resultAggregator honours an explicit opt-out from the proposal constraints', async () => {
253
+ const session = sessionWithPlan();
254
+ session.approvals = [{ id: 'approval-run', scope: 'run', status: 'approved', runId: 'run-expansion' }];
255
+ const calls = [];
256
+ const expansion = taskResultWithExpansion();
257
+ expansion.planExpansionRequest.constraints = { requireApprovalForMutations: false, maxTasks: 2 };
258
+
259
+ await accept(expansion, {
260
+ session,
261
+ runId: 'run-expansion',
262
+ task: session.headlessPlan[0],
263
+ assignment: { agentInstanceId: 'production-main', serverName: 'production' },
264
+ registry: expansionRegistry(),
265
+ callTool: async (_mcp, serverName, toolName, args) => {
266
+ calls.push(args);
267
+ return expansionFragment();
268
+ },
269
+ });
270
+
271
+ assert.equal(calls[0].constraints.requireApprovalForMutations, false);
272
+ assert.equal(calls[0].constraints.maxTasks, 2);
273
+ });
@@ -246,7 +246,7 @@ function task(id, overrides = {}) {
246
246
  }
247
247
 
248
248
  /*
249
- Cas observé le 2026-08-22 (workspace acpi) : `/wiki-ingest` planifie 13
249
+ Cas observé le 2026-08-22 (workspace acme) : `/wiki-ingest` planifie 13
250
250
  ingest_plan (groupe `ingest`) + 13 ingest_apply (groupe `apply`, sérialisés
251
251
  sur le lock `workspace-write`, derrière la barrière `ingest`) + 1 taxonomy
252
252
  (barrière `apply`). Le grant run-scope émis par le bouton Approve est « nu » :
@@ -260,7 +260,7 @@ function task(id, overrides = {}) {
260
260
  test('un grant run-scope « nu » (sans classes ni révision) débloque les apply derrière une barrière', () => {
261
261
  const plan = {
262
262
  runId: 'run-1',
263
- workspace: 'acpi',
263
+ workspace: 'acme',
264
264
  planRevision: 1,
265
265
  tasks: [
266
266
  // 13 ingest_plan du groupe ingest, tous done.
@@ -307,7 +307,7 @@ test('un grant run-scope « nu » (sans classes ni révision) débloque les appl
307
307
  status: 'approved',
308
308
  scope: 'run',
309
309
  runId: 'run-1',
310
- workspaceId: 'acpi',
310
+ workspaceId: 'acme',
311
311
  planRevision: null,
312
312
  approvalClasses: [],
313
313
  }],
@@ -322,7 +322,7 @@ test('un grant run-scope « nu » (sans classes ni révision) débloque les appl
322
322
  });
323
323
 
324
324
  /*
325
- Cas observé le 2026-08-04 (workspace juno) : une ingestion de dix fichiers,
325
+ Cas observé le 2026-08-04 (workspace demo) : une ingestion de dix fichiers,
326
326
  neuf réussis, le dixième en échec sur du JSON malformé. La barrière de groupe
327
327
  exigeait que TOUS les membres soient `done` : elle ne s'est jamais ouverte, le
328
328
  planificateur n'a plus trouvé de tâche prête, et le run est resté `running`
@@ -47,11 +47,11 @@ function preparedDelegation(taskId) {
47
47
 
48
48
  function runningSession(runId = 'run-conversational') {
49
49
  return {
50
- workspace: 'juno',
50
+ workspace: 'demo',
51
51
  activities: {},
52
52
  agentEvents: [],
53
53
  headlessPlan: null,
54
- _currentRunIdentity: { runId, workspace: 'juno' },
54
+ _currentRunIdentity: { runId, workspace: 'demo' },
55
55
  };
56
56
  }
57
57
 
@@ -98,9 +98,9 @@ test('une demande de build délègue dans le run courant, sans en démarrer un s
98
98
  const session = runningSession('run-fr-build');
99
99
  const started = [];
100
100
 
101
- const result = await delegateWithinRun(session, 'construis les livrables du workspace juno', {
101
+ const result = await delegateWithinRun(session, 'build the deliverables of the demo workspace', {
102
102
  prepare: async ({ objective }) => {
103
- assert.match(objective, /construis les livrables/);
103
+ assert.match(objective, /build the deliverables/);
104
104
  return preparedDelegation('build-fr');
105
105
  },
106
106
  registry: registryDouble(),
@@ -118,10 +118,10 @@ test('une demande de build délègue dans le run courant, sans en démarrer un s
118
118
  test('la délégation interne exige un run actif', async () => {
119
119
  // Hors run, il n'y a pas d'exécution à transformer : c'est un vrai démarrage
120
120
  // de run, et il doit passer par le chemin normal plutôt que par ici.
121
- const session = { workspace: 'juno', agentEvents: [] };
121
+ const session = { workspace: 'demo', agentEvents: [] };
122
122
 
123
123
  await assert.rejects(
124
- () => delegateWithinRun(session, 'construis les livrables', {
124
+ () => delegateWithinRun(session, 'build the deliverables', {
125
125
  prepare: async () => preparedDelegation(),
126
126
  registry: registryDouble(),
127
127
  }),
@@ -184,12 +184,12 @@ test('une demande française délègue une fois et bascule, sans jamais duplique
184
184
  const { runRuntimeAgenticWorkflow } = await import('./runner.js');
185
185
  const runId = 'run-fr-integration';
186
186
  const session = {
187
- workspace: 'juno',
187
+ workspace: 'demo',
188
188
  activities: {},
189
189
  agentEvents: [],
190
190
  headlessPlan: null,
191
191
  // Posée par executeRun avant l'appel au workflow, comme en production.
192
- _currentRunIdentity: { runId, workspace: 'juno' },
192
+ _currentRunIdentity: { runId, workspace: 'demo' },
193
193
  llm: { async completeWithTools() { assert.fail('aucune évaluation ne doit avoir lieu ici'); } },
194
194
  };
195
195
  const fiveTasks = {
@@ -205,7 +205,7 @@ test('une demande française délègue une fois et bascule, sans jamais duplique
205
205
  async invoke({ session: turnSession }) {
206
206
  conversationalTurns += 1;
207
207
  // Le tour conversationnel appelle l'outil de délégation, comme Donna.
208
- const result = await delegateWithinRun(turnSession, 'construis les livrables du workspace juno', {
208
+ const result = await delegateWithinRun(turnSession, 'build the deliverables of the demo workspace', {
209
209
  prepare: async () => {
210
210
  delegations += 1;
211
211
  return { ...preparedDelegation(), fragment: fiveTasks };
@@ -242,7 +242,7 @@ test('une demande française délègue une fois et bascule, sans jamais duplique
242
242
  // Borne l'attente d'approbation comme en headless : un test ne doit jamais
243
243
  // pouvoir se figer sur une décision humaine qui ne viendra pas.
244
244
  session._approvalTimeoutMs = 200;
245
- await runRuntimeAgenticWorkflow(agent, session, 'construis les livrables du workspace juno', {
245
+ await runRuntimeAgenticWorkflow(agent, session, 'build the deliverables of the demo workspace', {
246
246
  runId,
247
247
  timeoutMs: 2000,
248
248
  maxTurns: 4,
@@ -272,7 +272,7 @@ test('un run qui porte déjà un plan validé refuse une seconde délégation',
272
272
  session.headlessPlan = [task('build-a')];
273
273
 
274
274
  await assert.rejects(
275
- () => delegateWithinRun(session, 'construis les livrables', {
275
+ () => delegateWithinRun(session, 'build the deliverables', {
276
276
  prepare: async () => preparedDelegation(),
277
277
  registry: registryDouble(),
278
278
  }),