@dotdrelle/wiki-manager 0.11.10 → 0.12.1

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 (85) hide show
  1. package/README.md +32 -7
  2. package/agents.docker-compose.yml +3 -0
  3. package/docker-compose.yml +1 -0
  4. package/package.json +3 -2
  5. package/src/activity/activityAggregator.js +109 -0
  6. package/src/activity/activityAggregator.test.js +60 -0
  7. package/src/activity/activityDeduplicator.js +50 -0
  8. package/src/activity/progressCalculator.js +61 -0
  9. package/src/activity/runSynthesis.js +15 -0
  10. package/src/agent/graph.js +101 -38
  11. package/src/agent/graph.test.js +75 -5
  12. package/src/cli/wiki-manager.js +22 -0
  13. package/src/contracts/schemas.js +249 -13
  14. package/src/contracts/schemas.test.js +147 -2
  15. package/src/core/activity.js +8 -5
  16. package/src/core/agentEvents.js +260 -23
  17. package/src/core/agentEvents.test.js +53 -0
  18. package/src/core/compose.js +6 -1
  19. package/src/core/dockerCompose.test.js +12 -0
  20. package/src/core/documentIntake.js +33 -38
  21. package/src/core/documentIntake.test.js +25 -0
  22. package/src/core/env.js +11 -1
  23. package/src/core/jobQueue.js +26 -2
  24. package/src/core/mcp.js +30 -3
  25. package/src/core/mcp.test.js +69 -1
  26. package/src/core/plan.js +46 -4
  27. package/src/core/plan.test.js +16 -1
  28. package/src/core/planPatch.js +64 -3
  29. package/src/core/planPatch.test.js +110 -1
  30. package/src/core/queueStore.test.js +21 -0
  31. package/src/core/runtimeLog.js +119 -0
  32. package/src/core/runtimeLog.test.js +84 -0
  33. package/src/core/wikirc.js +22 -0
  34. package/src/core/wikirc.test.js +49 -1
  35. package/src/core/workflow.js +14 -3
  36. package/src/graph/graphAggregator.js +5 -0
  37. package/src/graph/graphPatch.js +8 -0
  38. package/src/graph/graphSnapshot.js +14 -0
  39. package/src/graph/graphVisibilityPolicy.js +40 -0
  40. package/src/graph/runGraphProjector.js +87 -0
  41. package/src/graph/runGraphProjector.test.js +111 -0
  42. package/src/orchestrator/agentRegistry.js +172 -0
  43. package/src/orchestrator/agentRegistry.test.js +114 -0
  44. package/src/orchestrator/approvalPolicy.js +126 -0
  45. package/src/orchestrator/approvalPolicy.test.js +78 -0
  46. package/src/orchestrator/assignmentManager.js +80 -0
  47. package/src/orchestrator/attemptManager.js +168 -0
  48. package/src/orchestrator/attemptManager.test.js +160 -0
  49. package/src/orchestrator/budgetManager.js +127 -0
  50. package/src/orchestrator/capabilityRegistry.js +64 -0
  51. package/src/orchestrator/capabilityRegistry.test.js +59 -0
  52. package/src/orchestrator/capabilityResolver.js +151 -0
  53. package/src/orchestrator/capabilityResolver.test.js +127 -0
  54. package/src/orchestrator/dependencyResolver.js +112 -0
  55. package/src/orchestrator/dispatcher.js +302 -0
  56. package/src/orchestrator/lockManager.js +51 -0
  57. package/src/orchestrator/planIntegrator.js +268 -0
  58. package/src/orchestrator/planIntegrator.test.js +213 -0
  59. package/src/orchestrator/planValidator.js +534 -0
  60. package/src/orchestrator/planValidator.test.js +262 -0
  61. package/src/orchestrator/resultAggregator.js +248 -0
  62. package/src/orchestrator/resultAggregator.test.js +211 -0
  63. package/src/orchestrator/scheduler.js +103 -0
  64. package/src/orchestrator/scheduler.test.js +138 -0
  65. package/src/runtime/approvals.js +130 -1
  66. package/src/runtime/controlMessages.js +43 -0
  67. package/src/runtime/controlMessages.test.js +21 -0
  68. package/src/runtime/donna-contract.test.js +296 -33
  69. package/src/runtime/recoveryManager.js +176 -0
  70. package/src/runtime/recoveryManager.test.js +162 -0
  71. package/src/runtime/runner.e2e.test.js +98 -12
  72. package/src/runtime/runner.js +261 -227
  73. package/src/runtime/runner.test.js +249 -452
  74. package/src/runtime/server.js +142 -24
  75. package/src/runtime/server.test.js +192 -62
  76. package/src/runtime/store.js +837 -2
  77. package/src/runtime/store.test.js +269 -28
  78. package/src/runtime/supervisor.js +33 -1
  79. package/src/runtime/supervisor.test.js +47 -1
  80. package/src/shell/RightPane.tsx +8 -5
  81. package/src/shell/repl.js +7 -11
  82. package/src/shell/repl.test.js +26 -5
  83. package/src/shell/tui.tsx +1 -0
  84. package/src/shell/useAgent.ts +5 -1
  85. package/src/shell/useSession.ts +75 -19
@@ -59,6 +59,10 @@ test('applyRuntimeStateToShellSession projects runtime state into shell session'
59
59
 
60
60
  assert.equal(applied, true);
61
61
  assert.equal(session.agentProjection.status, 'running');
62
+ assert.deepEqual(session.agentProjection.conversation, [
63
+ { role: 'user', content: 'Build docs' },
64
+ { role: 'assistant', content: 'Working.' },
65
+ ]);
62
66
  assert.equal(session.headlessPlan[0].description, 'Build');
63
67
  assert.equal(session.activities['production:job-1'].status, 'running');
64
68
  assert.equal(session.productionActivity.jobId, 'job-1');
@@ -66,10 +70,7 @@ test('applyRuntimeStateToShellSession projects runtime state into shell session'
66
70
  assert.equal(session.workflow.nodes[0].id, 'task:build');
67
71
  assert.equal(session.workflow.relations[0].type, 'contains');
68
72
  assert.deepEqual(session.workflow.waitingReasons, ['queue:q-1']);
69
- assert.deepEqual(conversationMessages(session), [
70
- { role: 'user', content: 'Build docs' },
71
- { role: 'donna', content: 'Working.' },
72
- ]);
73
+ assert.deepEqual(conversationMessages(session), []);
73
74
  });
74
75
 
75
76
  test('submitRuntimeRun reports acceptance without throwing', async () => {
@@ -108,6 +109,26 @@ test('submitRuntimeRun routes busy runtime input through the control lane', asyn
108
109
  }
109
110
  });
110
111
 
112
+ test('submitRuntimeRun reports queued runs from non-blocking runtime', async () => {
113
+ const restore = stubFetch(async (url) => {
114
+ assert.equal(pathOf(url), '/run');
115
+ return jsonResponse(202, {
116
+ accepted: true,
117
+ queued: true,
118
+ kind: 'enqueue_run',
119
+ item: { id: 'control-1', status: 'queued' },
120
+ });
121
+ });
122
+ try {
123
+ const session = createSession();
124
+ const outcome = await submitRuntimeRun('build the doc', { runtime: { url: 'http://runtime.test' }, session });
125
+ assert.equal(outcome.kind, 'queued');
126
+ assert.equal(outcome.result.item.id, 'control-1');
127
+ } finally {
128
+ restore();
129
+ }
130
+ });
131
+
111
132
  test('submitRuntimeRun reports a non-409 error without throwing or calling /control', async () => {
112
133
  let controlCalled = false;
113
134
  const restore = stubFetch(async (url) => {
@@ -162,7 +183,7 @@ test('agent mode without runtime records a visible error instead of falling back
162
183
 
163
184
  test('runtime status exposes the disconnected reason', () => {
164
185
  assert.equal(
165
- runtimeStatusLine({ error: 'token mismatch' }, { workspace: 'juno' }),
186
+ runtimeStatusLine({ error: 'token mismatch' }, { workspace: 'acme' }),
166
187
  'runtime: disconnected: token mismatch',
167
188
  );
168
189
  assert.equal(
package/src/shell/tui.tsx CHANGED
@@ -336,6 +336,7 @@ function App(props: {
336
336
  queueItems={state.queueItems()}
337
337
  queueInfo={state.queueInfo()}
338
338
  activeTab={state.rightTab()}
339
+ logFilter={state.runtimeLogFilter()}
339
340
  onTabClick={state.selectRightTab}
340
341
  />
341
342
  <SlashDialog context={state.activeEditor() ? null : state.slash()} />
@@ -31,7 +31,11 @@ export function useAgent(props: { agent: unknown; packageJson: Record<string, un
31
31
  props.onRuntimeAccepted?.();
32
32
  props.addLog('runtime: run accepted');
33
33
  } else if (outcome.kind === 'queued') {
34
- conversationMessages(props.session).push({ role: 'command', content: 'Runtime is busy — request added to the control queue, it will start automatically.' });
34
+ // The server localizes control-lane acknowledgements from the
35
+ // session language (src/runtime/controlMessages.js) — always prefer
36
+ // its explanation over a local hardcoded string.
37
+ const explanation = (outcome as any).result?.explanation ?? 'Request added to the queue.';
38
+ conversationMessages(props.session).push({ role: 'command', content: String(explanation) });
35
39
  props.addLog('runtime: control queued');
36
40
  } else {
37
41
  conversationMessages(props.session).push({ role: 'command', content: `Runtime error: ${outcome.message}` });
@@ -3,10 +3,11 @@ import { isAbsolute, relative, resolve, sep } from 'node:path';
3
3
  import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
4
4
  import { formatMcpToolResult, callMcpTool } from '../core/mcp.js';
5
5
  import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
6
- import { formatPlanStatus, formatCompletedActivities } from '../core/plan.js';
6
+ import { formatPlanStatus, formatCompletedActivities, formatPlanStep } from '../core/plan.js';
7
7
  import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
8
8
  import { projectQueue, queueCounts, startNextQueuedJob, syncQueueWithActivity } from '../core/jobQueue.js';
9
9
  import { queueStoreFor } from '../core/queueStore.js';
10
+ import { filterRuntimeLogs } from '../core/runtimeLog.js';
10
11
  import { fetchRuntimeState, streamRuntimeEvents } from '../runtime/client.js';
11
12
  import type { ActiveFileEditor } from './FileEditorDialog';
12
13
  import {
@@ -99,11 +100,13 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
99
100
  },
100
101
  });
101
102
  const runtimeRunStatus = createMemo(() => String(runtimeState()?.status ?? 'idle'));
102
- const agentBusy = createMemo(() =>
103
+ const conversationBusy = createMemo(() => agent.busy());
104
+ const executionActive = createMemo(() =>
103
105
  props.runtime?.url
104
- ? runtimeRunStatus() === 'running' || agent.busy()
105
- : agent.busy(),
106
+ ? runtimeRunStatus() === 'running'
107
+ : sessionActivities(session).some((activity) => !activity.terminal),
106
108
  );
109
+ const agentBusy = conversationBusy;
107
110
  const localFallbackActive = createMemo(() => !props.runtime?.url || runtimeStatus() === 'disconnected');
108
111
 
109
112
  // Runtime conversation entries are merged into conversationMessages(session)
@@ -182,6 +185,20 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
182
185
  let lastVisiblePlan: Array<{ step: number; description: string; status: string }> | null = null;
183
186
  const activities = createMemo(() => {
184
187
  version();
188
+ const activityLines = nonEmptyRuntimeArray(runtimeState()?.workflow?.activity?.lines);
189
+ if (activityLines) {
190
+ return activityLines.map((line: any, index: number) => ({
191
+ key: line.id ?? `activity-line-${index}`,
192
+ id: line.id ?? `activity-line-${index}`,
193
+ label: line.label,
194
+ status: line.status ?? 'running',
195
+ progress: line.progress ?? null,
196
+ terminal: ['done', 'failed', 'cancelled'].includes(String(line.status)),
197
+ _runtime: true,
198
+ _workflow: true,
199
+ _aggregated: true,
200
+ }));
201
+ }
185
202
  const workflowActivities = nonEmptyRuntimeArray(runtimeState()?.workflow?.nodes?.filter((node: any) => node.type === 'activity'));
186
203
  if (workflowActivities) {
187
204
  return workflowActivities.map((node: any) => ({
@@ -207,36 +224,64 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
207
224
  ? lastVisibleActivities.map((activity) => ({ ...activity }))
208
225
  : current;
209
226
  });
227
+ // Control-lane run requests (user messages queued while the runtime is
228
+ // busy) live in state.controlQueue, a separate lane from the MCP job
229
+ // queue. They must appear in the Queue panel too — otherwise a queued run
230
+ // request is invisible (Queue (0)) even though it is registered and will
231
+ // start automatically.
232
+ const controlQueueItems = createMemo(() => {
233
+ version();
234
+ const controlQueue = runtimeState()?.controlQueue;
235
+ if (!Array.isArray(controlQueue)) return [] as any[];
236
+ return controlQueue
237
+ .filter((item: any) => ['queued', 'running', 'starting'].includes(String(item.status ?? '').toLowerCase()))
238
+ .map((item: any) => ({
239
+ ...item,
240
+ id: item.id,
241
+ label: `run: ${String(item.input ?? '').slice(0, 64)}`,
242
+ status: item.status,
243
+ _runtime: true,
244
+ _control: true,
245
+ }));
246
+ });
210
247
  const queueItems = createMemo(() => {
211
248
  version();
249
+ const control = controlQueueItems();
212
250
  const workflowQueue = nonEmptyRuntimeArray(runtimeState()?.workflow?.nodes?.filter((node: any) => node.type === 'queue'));
213
251
  if (workflowQueue) {
214
- return workflowQueue.map((node: any) => ({ ...(node.raw ?? {}), id: node.itemId ?? node.id, label: node.label, status: node.status, _runtime: true, _workflow: true }));
252
+ return [...control, ...workflowQueue.map((node: any) => ({ ...(node.raw ?? {}), id: node.itemId ?? node.id, label: node.label, status: node.status, _runtime: true, _workflow: true }))];
215
253
  }
216
254
  const runtimeQueue = nonEmptyRuntimeArray(runtimeState()?.queue);
217
- if (runtimeQueue) return runtimeQueue.map((item: any) => ({ ...item, _runtime: true }));
218
- return projectQueue((session as any).headlessPlan, (session as any).jobQueue ?? [], { workspace: (session as any).workspace ?? null })
219
- .map((item: any) => ({ ...item }));
255
+ if (runtimeQueue) return [...control, ...runtimeQueue.map((item: any) => ({ ...item, _runtime: true }))];
256
+ return [
257
+ ...control,
258
+ ...projectQueue((session as any).headlessPlan, (session as any).jobQueue ?? [], { workspace: (session as any).workspace ?? null })
259
+ .map((item: any) => ({ ...item })),
260
+ ];
220
261
  });
221
262
  const queueInfo = createMemo(() => {
222
263
  version();
264
+ const control = controlQueueItems();
265
+ const controlActive = control.length;
266
+ const controlCurrent = control.filter((item: any) => ['starting', 'running'].includes(String(item.status ?? '').toLowerCase())).length;
223
267
  const workflowQueue = runtimeState()?.workflow?.nodes?.filter((node: any) => node.type === 'queue');
224
268
  if (Array.isArray(workflowQueue)) {
225
269
  return {
226
- active: workflowQueue.filter((item: any) => ['waiting', 'starting', 'running', 'queued', 'pending', 'pending_approval'].includes(String(item.status ?? '').toLowerCase())).length,
227
- current: workflowQueue.filter((item: any) => ['starting', 'running'].includes(String(item.status ?? '').toLowerCase())).length,
270
+ active: controlActive + workflowQueue.filter((item: any) => ['waiting', 'starting', 'running', 'queued', 'pending', 'pending_approval'].includes(String(item.status ?? '').toLowerCase())).length,
271
+ current: controlCurrent + workflowQueue.filter((item: any) => ['starting', 'running'].includes(String(item.status ?? '').toLowerCase())).length,
228
272
  frozen: 0,
229
273
  };
230
274
  }
231
275
  const runtimeQueue = runtimeState()?.queue;
232
276
  if (Array.isArray(runtimeQueue)) {
233
277
  return {
234
- active: runtimeQueue.filter((item: any) => ['waiting', 'starting', 'running', 'queued', 'pending', 'pending_approval'].includes(String(item.status ?? '').toLowerCase())).length,
235
- current: runtimeQueue.filter((item: any) => ['starting', 'running'].includes(String(item.status ?? '').toLowerCase())).length,
278
+ active: controlActive + runtimeQueue.filter((item: any) => ['waiting', 'starting', 'running', 'queued', 'pending', 'pending_approval'].includes(String(item.status ?? '').toLowerCase())).length,
279
+ current: controlCurrent + runtimeQueue.filter((item: any) => ['starting', 'running'].includes(String(item.status ?? '').toLowerCase())).length,
236
280
  frozen: 0,
237
281
  };
238
282
  }
239
- return queueCounts(session);
283
+ const base = queueCounts(session);
284
+ return { ...base, active: (base.active ?? 0) + controlActive, current: (base.current ?? 0) + controlCurrent };
240
285
  });
241
286
  const plan = createMemo(() => {
242
287
  version();
@@ -246,7 +291,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
246
291
  .sort((a: any, b: any) => Number(a.step ?? 0) - Number(b.step ?? 0))
247
292
  .map((node: any, index: number) => ({
248
293
  step: Number(node.step ?? index + 1),
249
- description: String(node.description ?? node.label ?? `Step ${index + 1}`),
294
+ description: formatPlanStep(node) || `Step ${index + 1}`,
250
295
  status: String(node.status ?? 'pending'),
251
296
  }));
252
297
  }
@@ -254,7 +299,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
254
299
  if (runtimePlan) {
255
300
  return runtimePlan.map((step: any, index: number) => ({
256
301
  step: Number(step.step ?? index + 1),
257
- description: String(step.description ?? step.label ?? step.name ?? `Step ${index + 1}`),
302
+ description: formatPlanStep(step) || `Step ${index + 1}`,
258
303
  status: String(step.status ?? 'pending'),
259
304
  }));
260
305
  }
@@ -272,8 +317,9 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
272
317
  version();
273
318
  const runtimeLogs = runtimeState()?.logs;
274
319
  if (!Array.isArray(runtimeLogs) || runtimeLogs.length === 0) return logs();
275
- const tagged = runtimeLogs.slice(-80).map((line: any) => `runtime ${String(line)}`);
276
- return [...logs(), ...tagged].slice(-200);
320
+ const filter = String((session as any).runtimeLogFilter ?? '').trim();
321
+ const tagged = filterRuntimeLogs(runtimeLogs.map((line: any) => `runtime ${String(line)}`), filter);
322
+ return [...logs(), ...tagged];
277
323
  });
278
324
 
279
325
  // Index-aligned with each workspace's runtimeConversation array — not just
@@ -291,6 +337,13 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
291
337
  const target = conversationMessages(session);
292
338
  const backed = runtimeConversationRefsByWorkspace.get(workspace) ?? [];
293
339
  runtimeConversationRefsByWorkspace.set(workspace, backed);
340
+ if (backed.length === 0 && target.length === 0 && !runtimeConversation.some((message: any) => message?._pending)) {
341
+ backed.push(...runtimeConversation.map((message: any) => ({
342
+ role: message.role === 'assistant' ? 'donna' : message.role,
343
+ content: String(message.content ?? ''),
344
+ })));
345
+ return;
346
+ }
294
347
  for (let i = 0; i < runtimeConversation.length; i += 1) {
295
348
  const message = runtimeConversation[i];
296
349
  const content = String(message.content ?? '');
@@ -437,7 +490,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
437
490
  const plan = (session as any).headlessPlan;
438
491
  const stillRunning = sessionActivities(session).filter((a) => !a.terminal && a.poll);
439
492
  const pendingSteps = (plan ?? []).filter((s: any) => s.status === 'pending');
440
- if (stillRunning.length === 0 && pendingSteps.length > 0 && !agentBusy()) {
493
+ if (stillRunning.length === 0 && pendingSteps.length > 0 && !conversationBusy()) {
441
494
  const completedAll = sessionActivities(session).filter((a) => a.terminal);
442
495
  const prompt = buildContinuationPrompt(session, completedAll);
443
496
  void agent.submit(prompt);
@@ -469,7 +522,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
469
522
 
470
523
  async function submitInput(submittedValue?: string) {
471
524
  const line = typeof submittedValue === 'string' && submittedValue.trim() ? submittedValue : input();
472
- if (agentBusy()) return { exit: false, busy: true };
525
+ if (conversationBusy()) return { exit: false, busy: true };
473
526
  setInput('');
474
527
  setConversationScroll(0);
475
528
  if (line.trim()) {
@@ -593,6 +646,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
593
646
  session,
594
647
  messages,
595
648
  logs: visibleLogs,
649
+ runtimeLogFilter: () => String((session as any).runtimeLogFilter ?? ''),
596
650
  input,
597
651
  setInput: updateInput,
598
652
  title,
@@ -613,6 +667,8 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
613
667
  plan,
614
668
  conversationScroll,
615
669
  scrollConversation,
670
+ conversationBusy,
671
+ executionActive,
616
672
  busy: agentBusy,
617
673
  abort,
618
674
  submitInput,