@dotdrelle/wiki-manager 0.11.10 → 0.12.0

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 (81) 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 +65 -30
  11. package/src/agent/graph.test.js +28 -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 +145 -0
  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 +1 -1
  25. package/src/core/plan.js +46 -4
  26. package/src/core/plan.test.js +16 -1
  27. package/src/core/planPatch.js +64 -3
  28. package/src/core/planPatch.test.js +110 -1
  29. package/src/core/queueStore.test.js +21 -0
  30. package/src/core/runtimeLog.js +119 -0
  31. package/src/core/runtimeLog.test.js +84 -0
  32. package/src/core/wikirc.js +22 -0
  33. package/src/core/wikirc.test.js +49 -1
  34. package/src/core/workflow.js +14 -3
  35. package/src/graph/graphAggregator.js +5 -0
  36. package/src/graph/graphPatch.js +8 -0
  37. package/src/graph/graphSnapshot.js +14 -0
  38. package/src/graph/graphVisibilityPolicy.js +40 -0
  39. package/src/graph/runGraphProjector.js +87 -0
  40. package/src/graph/runGraphProjector.test.js +111 -0
  41. package/src/orchestrator/agentRegistry.js +172 -0
  42. package/src/orchestrator/agentRegistry.test.js +114 -0
  43. package/src/orchestrator/approvalPolicy.js +126 -0
  44. package/src/orchestrator/approvalPolicy.test.js +78 -0
  45. package/src/orchestrator/assignmentManager.js +80 -0
  46. package/src/orchestrator/attemptManager.js +168 -0
  47. package/src/orchestrator/attemptManager.test.js +160 -0
  48. package/src/orchestrator/budgetManager.js +127 -0
  49. package/src/orchestrator/capabilityRegistry.js +64 -0
  50. package/src/orchestrator/capabilityRegistry.test.js +59 -0
  51. package/src/orchestrator/capabilityResolver.js +151 -0
  52. package/src/orchestrator/capabilityResolver.test.js +127 -0
  53. package/src/orchestrator/dependencyResolver.js +112 -0
  54. package/src/orchestrator/dispatcher.js +302 -0
  55. package/src/orchestrator/lockManager.js +51 -0
  56. package/src/orchestrator/planIntegrator.js +268 -0
  57. package/src/orchestrator/planIntegrator.test.js +213 -0
  58. package/src/orchestrator/planValidator.js +534 -0
  59. package/src/orchestrator/planValidator.test.js +262 -0
  60. package/src/orchestrator/resultAggregator.js +248 -0
  61. package/src/orchestrator/resultAggregator.test.js +211 -0
  62. package/src/orchestrator/scheduler.js +103 -0
  63. package/src/orchestrator/scheduler.test.js +138 -0
  64. package/src/runtime/approvals.js +130 -1
  65. package/src/runtime/donna-contract.test.js +293 -30
  66. package/src/runtime/recoveryManager.js +176 -0
  67. package/src/runtime/recoveryManager.test.js +162 -0
  68. package/src/runtime/runner.e2e.test.js +98 -12
  69. package/src/runtime/runner.js +261 -227
  70. package/src/runtime/runner.test.js +249 -452
  71. package/src/runtime/server.js +137 -20
  72. package/src/runtime/server.test.js +137 -7
  73. package/src/runtime/store.js +837 -2
  74. package/src/runtime/store.test.js +245 -4
  75. package/src/runtime/supervisor.js +33 -1
  76. package/src/runtime/supervisor.test.js +47 -1
  77. package/src/shell/RightPane.tsx +8 -5
  78. package/src/shell/repl.js +4 -9
  79. package/src/shell/repl.test.js +25 -4
  80. package/src/shell/tui.tsx +1 -0
  81. package/src/shell/useSession.ts +38 -10
@@ -1,6 +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
5
  import { projectWorkflow } from './workflow.js';
5
6
  import { validateContractInDev } from '../contracts/schemas.js';
6
7
 
@@ -14,6 +15,18 @@ const SESSION_PROJECTION_EVENTS = new Set([
14
15
  'plan_patch_applied',
15
16
  'plan_patch_rebased',
16
17
  'plan_patch_rejected',
18
+ 'plan.received',
19
+ 'plan.validated',
20
+ 'plan.rejected',
21
+ 'task_group.created',
22
+ 'task.created',
23
+ 'task.assigned',
24
+ 'task.started',
25
+ 'task.retry_scheduled',
26
+ 'task.result_returned',
27
+ 'task.completed',
28
+ 'task.failed',
29
+ 'plan.revision_changed',
17
30
  'activity_upserted',
18
31
  'run_evaluated',
19
32
  'run_replanned',
@@ -21,8 +34,13 @@ const SESSION_PROJECTION_EVENTS = new Set([
21
34
  'run_approved',
22
35
  'tool_pending_approval',
23
36
  'tool_approved',
37
+ 'approval.requested',
38
+ 'approval.granted',
39
+ 'approval.rejected',
24
40
  'control_enqueued',
25
41
  'control_started',
42
+ 'agent.registered',
43
+ 'agent.health_changed',
26
44
  'run_done',
27
45
  'run_error',
28
46
  'run_cancelled',
@@ -36,6 +54,7 @@ const PLAN_MUTATING_EVENTS = new Set([
36
54
  'run_started',
37
55
  'plan_set',
38
56
  'plan_patch_applied',
57
+ 'task.created',
39
58
  'plan_step_updated',
40
59
  'activity_upserted',
41
60
  'run_done',
@@ -120,6 +139,7 @@ function createProjectionState() {
120
139
  planRevision: 0,
121
140
  planPatches: [],
122
141
  controlQueue: [],
142
+ agents: {},
123
143
  summary: null,
124
144
  status: 'idle',
125
145
  };
@@ -142,6 +162,9 @@ function publicProjection(state) {
142
162
  patch: patch.patch ? { ...patch.patch, operations: (patch.patch.operations ?? []).map((operation) => ({ ...operation })) } : null,
143
163
  })),
144
164
  controlQueue: state.controlQueue.map((item) => ({ ...item })),
165
+ agents: Object.values(state.agents)
166
+ .map((agent) => ({ ...agent, description: cloneJson(agent.description) }))
167
+ .sort((a, b) => a.agentInstanceId.localeCompare(b.agentInstanceId)),
145
168
  summary: state.summary,
146
169
  status: state.status,
147
170
  };
@@ -155,6 +178,7 @@ export function applyAgentProjectionToSession(session, projection) {
155
178
  session.headlessPlan = projection.plan ? projection.plan.map((step) => ({ ...step })) : null;
156
179
  session.activities = Object.fromEntries((projection.activities ?? []).map((activity) => [activity.key, { ...activity }]));
157
180
  session.controlQueue = (projection.controlQueue ?? []).map((item) => ({ ...item }));
181
+ session.agents = (projection.agents ?? []).map((agent) => ({ ...agent, description: cloneJson(agent.description) }));
158
182
  session.planRevision = projection.planRevision ?? 0;
159
183
  session.planPatches = (projection.planPatches ?? []).map((patch) => ({ ...patch }));
160
184
  session.workflow = projection.workflow ? { ...projection.workflow } : null;
@@ -220,6 +244,53 @@ function applyEvent(state, event) {
220
244
  ? normalizePlanRevision(event.payload.planRevision)
221
245
  : state.planRevision + 1;
222
246
  return;
247
+ case 'plan.received':
248
+ state.logs.push(`Plan received for run ${String(event.runId ?? event.payload?.runId ?? '')}`.trim());
249
+ state.logs = state.logs.slice(-200);
250
+ return;
251
+ case 'plan.validated':
252
+ state.logs.push(`Plan validated for run ${String(event.runId ?? event.payload?.runId ?? '')}`.trim());
253
+ state.logs = state.logs.slice(-200);
254
+ return;
255
+ case 'plan.rejected':
256
+ state.logs.push(`Plan rejected: ${formatPlanErrors(event.payload?.errors)}`);
257
+ state.logs = state.logs.slice(-200);
258
+ return;
259
+ case 'task_group.created':
260
+ return;
261
+ case 'task.created':
262
+ appendCreatedTask(state, event.payload?.task);
263
+ return;
264
+ case 'task.assigned':
265
+ state.logs.push(`Task assigned: ${String(event.taskId ?? event.payload?.taskId ?? '')}`.trim());
266
+ state.logs = state.logs.slice(-200);
267
+ return;
268
+ case 'task.started':
269
+ state.logs.push(`Task started: ${String(event.taskId ?? event.payload?.taskId ?? '')}`.trim());
270
+ state.logs = state.logs.slice(-200);
271
+ return;
272
+ case 'task.retry_scheduled':
273
+ state.logs.push(`Task retry scheduled: ${String(event.taskId ?? event.payload?.taskId ?? '')}`.trim());
274
+ state.logs = state.logs.slice(-200);
275
+ return;
276
+ case 'task.result_returned':
277
+ state.logs.push(`Task result returned: ${String(event.taskId ?? event.payload?.taskId ?? '')}`.trim());
278
+ state.logs = state.logs.slice(-200);
279
+ return;
280
+ case 'task.completed':
281
+ state.logs.push(`Task completed: ${String(event.taskId ?? event.payload?.taskId ?? '')}`.trim());
282
+ state.logs = state.logs.slice(-200);
283
+ return;
284
+ case 'task.failed':
285
+ state.logs.push(`Task failed: ${String(event.taskId ?? event.payload?.taskId ?? '')}`.trim());
286
+ state.logs = state.logs.slice(-200);
287
+ return;
288
+ case 'plan.revision_changed':
289
+ if (Array.isArray(event.payload?.tasks)) {
290
+ state.plan = normalizePlan(event.payload.tasks, { owner: 'orchestrator', planRevision: state.planRevision });
291
+ }
292
+ state.planRevision = normalizePlanRevision(event.payload?.planRevision ?? state.planRevision + 1);
293
+ return;
223
294
  case 'plan_step_updated':
224
295
  updatePlanStep(state.plan, event.payload ?? {});
225
296
  return;
@@ -346,6 +417,60 @@ function applyEvent(state, event) {
346
417
  approvedAt: event.ts,
347
418
  });
348
419
  return;
420
+ case 'approval.requested':
421
+ upsertApproval(state, {
422
+ id: approvalProjectionId(event),
423
+ approvalId: event.payload?.approvalId ?? event.payload?.id ?? null,
424
+ scope: event.payload?.scope ?? 'task',
425
+ status: 'pending_approval',
426
+ runId: event.runId ?? event.payload?.runId ?? null,
427
+ workspaceId: event.workspace ?? event.payload?.workspaceId ?? event.payload?.workspace ?? null,
428
+ planRevision: event.payload?.planRevision ?? null,
429
+ taskId: event.taskId ?? event.payload?.taskId ?? null,
430
+ itemId: event.payload?.itemId ?? event.payload?.taskId ?? null,
431
+ groupId: event.payload?.groupId ?? null,
432
+ approvalClasses: normalizeApprovalClasses(event.payload?.approvalClasses ?? event.payload?.approvalClass),
433
+ reason: event.payload?.reason ?? null,
434
+ createdAt: event.ts,
435
+ });
436
+ return;
437
+ case 'approval.granted': {
438
+ const grant = {
439
+ id: approvalProjectionId(event),
440
+ approvalId: event.payload?.approvalId ?? event.payload?.id ?? null,
441
+ scope: event.payload?.scope ?? 'run',
442
+ status: 'approved',
443
+ runId: event.runId ?? event.payload?.runId ?? null,
444
+ workspaceId: event.workspace ?? event.payload?.workspaceId ?? event.payload?.workspace ?? null,
445
+ planRevision: event.payload?.planRevision ?? null,
446
+ taskId: event.taskId ?? event.payload?.taskId ?? null,
447
+ itemId: event.payload?.itemId ?? event.payload?.taskId ?? null,
448
+ groupId: event.payload?.groupId ?? null,
449
+ approvalClasses: normalizeApprovalClasses(event.payload?.approvalClasses ?? event.payload?.approvalClass),
450
+ reason: event.payload?.reason ?? null,
451
+ approvedAt: event.ts,
452
+ };
453
+ upsertApproval(state, grant);
454
+ markCoveredApprovalsApproved(state.approvals, grant, event.ts);
455
+ return;
456
+ }
457
+ case 'approval.rejected':
458
+ upsertApproval(state, {
459
+ id: approvalProjectionId(event),
460
+ approvalId: event.payload?.approvalId ?? event.payload?.id ?? null,
461
+ scope: event.payload?.scope ?? 'run',
462
+ status: 'rejected',
463
+ runId: event.runId ?? event.payload?.runId ?? null,
464
+ workspaceId: event.workspace ?? event.payload?.workspaceId ?? event.payload?.workspace ?? null,
465
+ planRevision: event.payload?.planRevision ?? null,
466
+ taskId: event.taskId ?? event.payload?.taskId ?? null,
467
+ itemId: event.payload?.itemId ?? event.payload?.taskId ?? null,
468
+ groupId: event.payload?.groupId ?? null,
469
+ approvalClasses: normalizeApprovalClasses(event.payload?.approvalClasses ?? event.payload?.approvalClass),
470
+ reason: event.payload?.reason ?? null,
471
+ rejectedAt: event.ts,
472
+ });
473
+ return;
349
474
  case 'run_done':
350
475
  state.status = 'done';
351
476
  finishPendingPlanSteps(state.plan);
@@ -380,15 +505,36 @@ function applyEvent(state, event) {
380
505
  updatedAt: event.ts,
381
506
  });
382
507
  return;
508
+ case 'agent.registered':
509
+ upsertAgent(state, event.payload?.agent, event.ts);
510
+ return;
511
+ case 'agent.health_changed':
512
+ upsertAgent(state, {
513
+ ...(event.payload?.agent ?? {}),
514
+ agentInstanceId: event.payload?.agentInstanceId ?? event.payload?.agent?.agentInstanceId,
515
+ health: event.payload?.health ?? event.payload?.agent?.health,
516
+ }, event.ts);
517
+ return;
383
518
  case 'runtime_log':
384
- state.logs.push(String(event.payload?.message ?? ''));
385
- state.logs = state.logs.slice(-200);
519
+ state.logs.push(formatRuntimeLogPayload(event.payload ?? {}, event.ts));
386
520
  return;
387
521
  default:
388
522
  return;
389
523
  }
390
524
  }
391
525
 
526
+ function upsertAgent(state, rawAgent, ts) {
527
+ if (!rawAgent?.agentInstanceId) return;
528
+ const existing = state.agents[rawAgent.agentInstanceId] ?? {};
529
+ state.agents[rawAgent.agentInstanceId] = {
530
+ ...existing,
531
+ ...rawAgent,
532
+ description: cloneJson(rawAgent.description ?? existing.description ?? null),
533
+ health: rawAgent.health ?? rawAgent.description?.health?.status ?? existing.health ?? 'unavailable',
534
+ lastSeenAt: rawAgent.lastSeenAt ?? ts,
535
+ };
536
+ }
537
+
392
538
  function upsertById(list, next) {
393
539
  const index = list.findIndex((item) => item.id === next.id);
394
540
  if (index === -1) {
@@ -411,6 +557,14 @@ function upsertPlanPatch(state, next) {
411
557
  upsertById(state.planPatches, next);
412
558
  }
413
559
 
560
+ function appendCreatedTask(state, rawTask) {
561
+ if (!rawTask?.id) return;
562
+ state.plan ??= [];
563
+ const existing = state.plan.find((task) => String(task.id ?? task.step) === String(rawTask.id));
564
+ if (existing) return;
565
+ state.plan.push(normalizePlanTask(rawTask, state.plan.length, { owner: 'orchestrator' }));
566
+ }
567
+
414
568
  function patchIdFromEvent(event) {
415
569
  return event.payload?.id ?? event.payload?.patchId ?? event.id;
416
570
  }
@@ -462,6 +616,42 @@ function upsertApproval(state, next) {
462
616
  upsertById(state.approvals, next);
463
617
  }
464
618
 
619
+ function approvalProjectionId(event) {
620
+ return event.payload?.id
621
+ ?? event.payload?.approvalId
622
+ ?? event.payload?.taskId
623
+ ?? event.payload?.itemId
624
+ ?? event.payload?.groupId
625
+ ?? event.payload?.runId
626
+ ?? event.id;
627
+ }
628
+
629
+ function normalizeApprovalClasses(value) {
630
+ if (value == null) return [];
631
+ const values = Array.isArray(value) ? value : [value];
632
+ return [...new Set(values.map((item) => String(item).trim()).filter(Boolean))];
633
+ }
634
+
635
+ function markCoveredApprovalsApproved(approvals, grant, ts) {
636
+ const classes = normalizeApprovalClasses(grant.approvalClasses);
637
+ for (const approval of approvals) {
638
+ if (approval.status !== 'pending_approval') continue;
639
+ if (grant.runId != null && approval.runId != null && String(grant.runId) !== String(approval.runId)) continue;
640
+ if (grant.workspaceId != null && approval.workspaceId != null && String(grant.workspaceId) !== String(approval.workspaceId)) continue;
641
+ if (grant.planRevision != null && approval.planRevision != null && Number(grant.planRevision) !== Number(approval.planRevision)) continue;
642
+ const approvalClasses = normalizeApprovalClasses(approval.approvalClasses);
643
+ if (classes.length > 0 && approvalClasses.length > 0 && !approvalClasses.some((item) => classes.includes(item))) continue;
644
+ if (grant.scope === 'group' && String(grant.groupId ?? '') !== String(approval.groupId ?? '')) continue;
645
+ if (grant.scope === 'task' || grant.scope === 'tool') {
646
+ const grantIds = new Set([grant.taskId, grant.itemId, grant.approvalId, grant.id].filter(Boolean).map(String));
647
+ const approvalIds = [approval.taskId, approval.itemId, approval.approvalId, approval.id].filter(Boolean).map(String);
648
+ if (!approvalIds.some((id) => grantIds.has(id))) continue;
649
+ }
650
+ approval.status = 'approved';
651
+ approval.approvedAt = ts;
652
+ }
653
+ }
654
+
465
655
  function finishPendingPlanSteps(plan) {
466
656
  for (const step of plan ?? []) {
467
657
  if (step.status === 'running' || step.status === 'pending') {
@@ -524,30 +714,56 @@ function normalizePlan(steps, payload = {}) {
524
714
  if (!Array.isArray(steps)) return null;
525
715
  const owner = payload.owner ?? 'orchestrator';
526
716
  const ownerActivityKey = payload.ownerActivityKey ?? payload.activityKey ?? null;
527
- const plan = steps.map((raw, i) => {
528
- const item = typeof raw === 'string' ? { description: raw } : (raw ?? {});
529
- return {
530
- step: Number(item.step ?? i + 1),
531
- id: item.id ?? null,
532
- description: String(item.description ?? item.label ?? item.name ?? `Step ${i + 1}`),
533
- status: item.status ?? 'pending',
534
- dependsOn: Array.isArray(item.dependsOn) ? item.dependsOn.map(String) : [],
535
- executor: item.executor ?? null,
536
- executorQuery: item.executorQuery ?? null,
537
- outputRefs: Array.isArray(item.outputRefs) ? item.outputRefs.map(String) : [],
538
- locks: Array.isArray(item.locks) ? item.locks.map(String) : item.locks ?? null,
539
- writeLocks: Array.isArray(item.writeLocks) ? item.writeLocks.map(String) : item.writeLocks ?? null,
540
- deliverableWrites: Array.isArray(item.deliverableWrites) ? item.deliverableWrites.map(String) : [],
541
- wikiPageWrites: Array.isArray(item.wikiPageWrites) ? item.wikiPageWrites.map(String) : [],
542
- workspaceWrite: item.workspaceWrite === true,
543
- owner: item.owner ?? owner,
544
- ownerActivityKey: item.ownerActivityKey ?? ownerActivityKey,
545
- _activityKey: item._activityKey ?? payload.activityKey ?? null,
546
- };
547
- });
717
+ const plan = steps.map((raw, i) => normalizePlanTask(raw, i, { owner, ownerActivityKey, activityKey: payload.activityKey ?? null }));
548
718
  return validateContractInDev('plan', plan);
549
719
  }
550
720
 
721
+ function normalizePlanTask(raw, index, { owner = 'orchestrator', ownerActivityKey = null, activityKey = null } = {}) {
722
+ const item = typeof raw === 'string' ? { description: raw } : (raw ?? {});
723
+ const task = {
724
+ step: Number(item.step ?? index + 1),
725
+ id: item.id ?? null,
726
+ label: item.label ?? item.description ?? item.name ?? `Step ${index + 1}`,
727
+ description: String(item.description ?? item.label ?? item.name ?? `Step ${index + 1}`),
728
+ status: item.status ?? 'pending',
729
+ dependsOn: Array.isArray(item.dependsOn) ? item.dependsOn.map(String) : [],
730
+ requiredCapability: item.requiredCapability ?? null,
731
+ operation: item.operation ?? null,
732
+ arguments: item.arguments && typeof item.arguments === 'object' && !Array.isArray(item.arguments) ? { ...item.arguments } : {},
733
+ groupId: item.groupId ?? null,
734
+ dependsOnGroup: item.dependsOnGroup ?? null,
735
+ barrier: item.barrier === true,
736
+ parallelizable: item.parallelizable === true,
737
+ recommendedConcurrency: item.recommendedConcurrency,
738
+ inputRefs: Array.isArray(item.inputRefs) ? item.inputRefs.map(cloneRef) : [],
739
+ expectedOutputRefs: Array.isArray(item.expectedOutputRefs) ? item.expectedOutputRefs.map(cloneRef) : [],
740
+ executor: item.executor ?? null,
741
+ executorQuery: item.executorQuery ?? null,
742
+ outputRefs: Array.isArray(item.outputRefs) ? item.outputRefs.map(String) : [],
743
+ locks: Array.isArray(item.locks) ? item.locks.map(String) : [],
744
+ requiresApproval: item.requiresApproval === true,
745
+ approvalClass: item.approvalClass ?? null,
746
+ approvalSummary: item.approvalSummary ?? null,
747
+ idempotencyKey: item.idempotencyKey ?? null,
748
+ progressWeight: Number.isFinite(Number(item.progressWeight)) ? Number(item.progressWeight) : 1,
749
+ priority: item.priority,
750
+ retryPolicy: item.retryPolicy ? cloneJson(item.retryPolicy) : undefined,
751
+ retryState: item.retryState ? cloneJson(item.retryState) : undefined,
752
+ retryAssignment: item.retryAssignment ? cloneJson(item.retryAssignment) : undefined,
753
+ writeLocks: Array.isArray(item.writeLocks) ? item.writeLocks.map(String) : item.writeLocks ?? null,
754
+ deliverableWrites: Array.isArray(item.deliverableWrites) ? item.deliverableWrites.map(String) : [],
755
+ wikiPageWrites: Array.isArray(item.wikiPageWrites) ? item.wikiPageWrites.map(String) : [],
756
+ workspaceWrite: item.workspaceWrite === true,
757
+ owner: item.owner ?? owner,
758
+ ownerActivityKey: item.ownerActivityKey ?? ownerActivityKey,
759
+ _activityKey: item._activityKey ?? activityKey,
760
+ };
761
+ for (const key of ['recommendedConcurrency', 'priority', 'retryPolicy']) {
762
+ if (task[key] === undefined) delete task[key];
763
+ }
764
+ return task;
765
+ }
766
+
551
767
  function updatePlanStep(plan, payload) {
552
768
  if (!plan) return;
553
769
  const requestedTaskId = payload.taskId ?? payload.id ?? payload.targetTaskId;
@@ -557,12 +773,33 @@ function updatePlanStep(plan, payload) {
557
773
  if (!step) return;
558
774
  if (payload.status === 'failed') step.status = 'failed';
559
775
  else if (payload.status === 'running') step.status = 'running';
776
+ else if (payload.status === 'pending') step.status = 'pending';
777
+ else if (payload.status === 'pending_approval') step.status = 'pending_approval';
778
+ else if (payload.status === 'waiting_approval') step.status = 'waiting_approval';
560
779
  else if (payload.status === 'cancelled') step.status = 'cancelled';
561
780
  else step.status = 'done';
562
781
  if (payload.activityKey) step.activityKey = payload.activityKey;
782
+ if (Array.isArray(payload.outputRefs)) step.outputRefs = payload.outputRefs.map(cloneRef);
783
+ if (payload.result) step.result = cloneJson(payload.result);
784
+ if (payload.retryState) step.retryState = cloneJson(payload.retryState);
785
+ if (payload.retryAssignment) step.retryAssignment = cloneJson(payload.retryAssignment);
786
+ }
787
+
788
+ function formatPlanErrors(errors) {
789
+ return Array.isArray(errors) && errors.length > 0
790
+ ? errors.map((error) => error.code ?? error.message ?? String(error)).join(', ')
791
+ : 'unknown';
792
+ }
793
+
794
+ function cloneRef(value) {
795
+ return value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : String(value);
563
796
  }
564
797
 
565
798
  function sortedActivities(activities) {
566
799
  return Object.values(activities ?? {})
567
800
  .sort((a, b) => String(a.updatedAt ?? '').localeCompare(String(b.updatedAt ?? '')));
568
801
  }
802
+
803
+ function cloneJson(value) {
804
+ return value == null ? value : JSON.parse(JSON.stringify(value));
805
+ }
@@ -214,6 +214,59 @@ test('reduceAgentEvents: approvals move from pending to approved', () => {
214
214
  assert.deepEqual(projection.approvals[0].plan, ['Build']);
215
215
  });
216
216
 
217
+ test('reduceAgentEvents: bounded approval grant covers matching pending requests only', () => {
218
+ const projection = reduceAgentEvents([
219
+ createAgentEvent('approval.requested', {
220
+ origin: 'orchestrator',
221
+ runId: 'run-1',
222
+ workspace: 'docs',
223
+ taskId: 'run-1:a',
224
+ payload: {
225
+ id: 'approval-a',
226
+ scope: 'task',
227
+ runId: 'run-1',
228
+ workspaceId: 'docs',
229
+ planRevision: 2,
230
+ taskId: 'run-1:a',
231
+ approvalClasses: ['workspace-write'],
232
+ },
233
+ }),
234
+ createAgentEvent('approval.requested', {
235
+ origin: 'orchestrator',
236
+ runId: 'run-1',
237
+ workspace: 'docs',
238
+ taskId: 'run-1:b',
239
+ payload: {
240
+ id: 'approval-b',
241
+ scope: 'task',
242
+ runId: 'run-1',
243
+ workspaceId: 'docs',
244
+ planRevision: 2,
245
+ taskId: 'run-1:b',
246
+ approvalClasses: ['publish'],
247
+ },
248
+ }),
249
+ createAgentEvent('approval.granted', {
250
+ origin: 'runtime',
251
+ runId: 'run-1',
252
+ workspace: 'docs',
253
+ payload: {
254
+ id: 'grant-1',
255
+ scope: 'run',
256
+ runId: 'run-1',
257
+ workspaceId: 'docs',
258
+ planRevision: 2,
259
+ approvalClasses: ['workspace-write'],
260
+ },
261
+ }),
262
+ ]);
263
+
264
+ const byId = Object.fromEntries(projection.approvals.map((approval) => [approval.id, approval]));
265
+ assert.equal(byId['approval-a'].status, 'approved');
266
+ assert.equal(byId['approval-b'].status, 'pending_approval');
267
+ assert.equal(byId['grant-1'].status, 'approved');
268
+ });
269
+
217
270
  test('reduceAgentEvents: control queue is event sourced and follows run status', () => {
218
271
  const projection = reduceAgentEvents([
219
272
  createAgentEvent('control_enqueued', {
@@ -4,7 +4,7 @@ import { join } from 'node:path';
4
4
  import { promisify } from 'node:util';
5
5
  import YAML from 'yaml';
6
6
  import { cacertEnv, ensureCacertComposeOverride } from './cacert.js';
7
- import { managerEnvFile, readEnvFile } from './env.js';
7
+ import { managerEnvFile, readEnvFile, resolveAgentsDataDir } from './env.js';
8
8
  import { managerRoot } from './workspaces.js';
9
9
 
10
10
  const execFileAsync = promisify(execFile);
@@ -106,11 +106,16 @@ function composeBaseArgs(session) {
106
106
  }
107
107
 
108
108
  function composeEnv(session) {
109
+ const uid = typeof process.getuid === 'function' ? String(process.getuid()) : (process.env.UID ?? '1000');
110
+ const gid = typeof process.getgid === 'function' ? String(process.getgid()) : (process.env.GID ?? '1000');
109
111
  return {
110
112
  ...process.env,
111
113
  ...readManagerEnv(),
112
114
  ...(session.workspaceEnv ?? {}),
113
115
  ...cacertEnv(),
116
+ UID: uid,
117
+ GID: gid,
118
+ AGENTS_DATA_DIR: resolveAgentsDataDir(session),
114
119
  WORKSPACE_NAME: session.workspace,
115
120
  WIKI_WORKSPACE_PATH: session.workspacePath,
116
121
  ...(session.wikirc?.fileName && { WIKI_CONFIG_PATH: session.wikirc.fileName }),
@@ -12,3 +12,15 @@ test('workspace compose does not start a per-workspace agent runtime', async ()
12
12
  assert.deepEqual(aliases.all.targets, ['serve', 'mcp-http', 'production-mcp']);
13
13
  assert.equal(aliases.runtime, undefined);
14
14
  });
15
+
16
+ test('agent compose services run as the host uid and gid', async () => {
17
+ const workspaceRaw = await readFile(new URL('../../docker-compose.yml', import.meta.url), 'utf8');
18
+ const workspaceCompose = YAML.parse(workspaceRaw);
19
+ assert.equal(workspaceCompose.services['production-mcp'].user, '${UID:-1000}:${GID:-1000}');
20
+
21
+ const agentsRaw = await readFile(new URL('../../agents.docker-compose.yml', import.meta.url), 'utf8');
22
+ const agentsCompose = YAML.parse(agentsRaw);
23
+ assert.equal(agentsCompose.services.cme.user, '${UID:-1000}:${GID:-1000}');
24
+ assert.equal(agentsCompose.services.documents.user, '${UID:-1000}:${GID:-1000}');
25
+ assert.equal(agentsCompose.services.mailer.user, '${UID:-1000}:${GID:-1000}');
26
+ });