@dotdrelle/wiki-manager 0.11.9 → 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
@@ -300,7 +300,7 @@ test('agent graph waits for tool-level approval configured on endpoint', async (
300
300
  }
301
301
  });
302
302
 
303
- test('agent graph accepts structured wiki plan steps and selects MCP executors', async () => {
303
+ test('agent graph accepts structured wiki plan steps without selecting MCP executors implicitly', async () => {
304
304
  let calls = 0;
305
305
  const session = sessionBase({
306
306
  mcp: {
@@ -337,8 +337,20 @@ test('agent graph accepts structured wiki plan steps and selects MCP executors',
337
337
  name: 'wiki__plan_set',
338
338
  arguments: JSON.stringify({
339
339
  steps: [
340
- { id: 'cme-export', description: 'Export CME pages', outputRefs: ['raw/untracked'] },
341
- { id: 'build', description: 'Run production build', dependsOn: ['cme-export'] },
340
+ {
341
+ id: 'cme-export',
342
+ description: 'Export CME pages',
343
+ requiredCapability: 'external-source.export',
344
+ executor: 'cme.cme_export_run',
345
+ executorQuery: { capability: 'legacy export' },
346
+ outputRefs: ['raw/untracked'],
347
+ },
348
+ {
349
+ id: 'build',
350
+ description: 'Run production build',
351
+ requiredCapability: 'knowledge.pipeline',
352
+ dependsOn: ['cme-export'],
353
+ },
342
354
  ],
343
355
  }),
344
356
  },
@@ -359,8 +371,19 @@ test('agent graph accepts structured wiki plan steps and selects MCP executors',
359
371
 
360
372
  assert.equal(result.response, 'Plan ready.');
361
373
  assert.deepEqual(session.headlessPlan.map((step) => step.id), ['cme-export', 'build']);
362
- assert.equal(session.headlessPlan[0].executor, 'cme.cme_export_run');
363
- assert.equal(session.headlessPlan[1].executor, 'production.production_start_job');
374
+ assert.deepEqual(session.headlessPlan.map((step) => step.requiredCapability), ['external-source.export', 'knowledge.pipeline']);
375
+ assert.equal(session.headlessPlan[0].executor, null);
376
+ assert.equal(session.headlessPlan[0].executorQuery, null);
377
+ assert.equal(session.headlessPlan[1].executor, null);
364
378
  assert.deepEqual(session.headlessPlan[1].dependsOn, ['cme-export']);
365
379
  assert.deepEqual(session.headlessPlan[0].outputRefs, ['raw/untracked']);
366
380
  });
381
+
382
+ test('buildAgentSystemPrompt forbids inventing slash commands or arguments', () => {
383
+ const prompt = buildAgentSystemPrompt({ session: sessionBase({ commands: ['status', 'services'] }) });
384
+ assert.match(prompt, /Available primitives: \/status, \/services\./);
385
+ assert.match(prompt, /Do not invent command names, subcommands, or arguments/);
386
+ assert.doesNotMatch(prompt, /\/restart serve/);
387
+ assert.doesNotMatch(prompt, /executorQuery/);
388
+ assert.doesNotMatch(prompt, /executor:"/);
389
+ });
@@ -294,6 +294,7 @@ async function runRuntime(argv, agent) {
294
294
  }
295
295
  const { defaultRuntimeStateDir, openRuntimeStore, RECOVERABLE_QUEUE_STATUSES } = await import('../runtime/store.js');
296
296
  const { startRuntimeServer } = await import('../runtime/server.js');
297
+ const { recoverActiveRuns } = await import('../runtime/recoveryManager.js');
297
298
  const { emitRuntimeLog, startActivitySupervisor } = await import('../runtime/supervisor.js');
298
299
  const { resolveRuntimeAuthToken } = await import('../runtime/auth.js');
299
300
  const { createSqliteQueueStore } = await import('../runtime/queueStore.js');
@@ -484,6 +485,27 @@ async function runRuntime(argv, agent) {
484
485
  reason: `MCP unavailable: ${gaps.join(', ')}`,
485
486
  };
486
487
  }
488
+ const taskRecovery = await recoverActiveRuns({
489
+ store,
490
+ session: context.session,
491
+ workspace: context.workspace,
492
+ callTool: callMcpTool,
493
+ });
494
+ if (!taskRecovery.ok) {
495
+ const interrupted = store.interruptRuns({ workspace: context.workspace });
496
+ return {
497
+ workspace: context.workspace ?? workspace ?? null,
498
+ resumed: false,
499
+ interrupted,
500
+ reason: `Task recovery failed: ${taskRecovery.errors.map((item) => `${item.taskId}: ${item.error}`).join('; ')}`,
501
+ };
502
+ }
503
+ if (taskRecovery.recovered.length > 0 || taskRecovery.rescheduled.length > 0) {
504
+ emitRuntimeLog(
505
+ context.session,
506
+ `runtime: recovery attached ${taskRecovery.recovered.length} job(s), requeued ${taskRecovery.rescheduled.length} task(s)`,
507
+ );
508
+ }
487
509
  const recoverableRuns = store.listRecoverableRuns({ workspace: context.workspace });
488
510
  const runningRun = recoverableRuns.find((run) => run.status === 'running');
489
511
  const activeActivities = activeNonTerminalActivities(context.session);
@@ -3,6 +3,7 @@ const STATUS_VALUES = [
3
3
  'queued',
4
4
  'running',
5
5
  'waiting',
6
+ 'waiting_approval',
6
7
  'pending_approval',
7
8
  'done',
8
9
  'failed',
@@ -18,6 +19,7 @@ const STATUS_VALUES = [
18
19
 
19
20
  const nullableString = { type: ['string', 'null'] };
20
21
  const nullableObject = { type: ['object', 'null'], additionalProperties: true };
22
+ const stringArraySchema = { type: 'array', items: { type: 'string' } };
21
23
  const outputReferenceSchema = {
22
24
  $id: 'https://dotdrelle.dev/wiki-manager/contracts/output-reference/v1',
23
25
  title: 'OutputReference',
@@ -38,6 +40,241 @@ const outputReferenceSchema = {
38
40
  ],
39
41
  };
40
42
 
43
+ const retryPolicySchema = {
44
+ $id: 'https://dotdrelle.dev/wiki-manager/contracts/retry-policy/v1',
45
+ title: 'RetryPolicy',
46
+ schemaVersion: '1',
47
+ type: 'object',
48
+ required: ['maxAttempts', 'retryableErrors', 'allowAgentFallback'],
49
+ additionalProperties: false,
50
+ properties: {
51
+ maxAttempts: { type: 'integer', minimum: 1 },
52
+ retryableErrors: stringArraySchema,
53
+ allowAgentFallback: { type: 'boolean' },
54
+ },
55
+ };
56
+
57
+ const jsonSchemaSchema = { type: 'object', additionalProperties: true };
58
+ const capabilityDescriptionSchema = {
59
+ $id: 'https://dotdrelle.dev/wiki-manager/contracts/capability-description/v1',
60
+ title: 'CapabilityDescription',
61
+ schemaVersion: '1',
62
+ type: 'object',
63
+ required: ['id', 'version', 'description', 'inputSchema', 'outputSchema', 'supportedOperations'],
64
+ additionalProperties: true,
65
+ properties: {
66
+ id: { type: 'string', minLength: 1 },
67
+ version: { type: 'string', minLength: 1 },
68
+ description: { type: 'string' },
69
+ inputSchema: jsonSchemaSchema,
70
+ outputSchema: jsonSchemaSchema,
71
+ supportedOperations: stringArraySchema,
72
+ mutationClass: { type: 'string' },
73
+ defaultRequiresApproval: { type: 'boolean' },
74
+ estimatedCost: {
75
+ type: 'object',
76
+ additionalProperties: false,
77
+ properties: {
78
+ llmCalls: { type: 'number', minimum: 0 },
79
+ tokenRange: { type: 'array', items: { type: 'number' } },
80
+ },
81
+ },
82
+ },
83
+ };
84
+
85
+ const agentDescriptionSchema = {
86
+ $id: 'https://dotdrelle.dev/wiki-manager/contracts/agent-description/v1',
87
+ title: 'AgentDescription',
88
+ schemaVersion: '1',
89
+ type: 'object',
90
+ required: ['contractVersion', 'agentType', 'agentInstanceId', 'displayName', 'capabilities', 'orchestration', 'limits', 'health'],
91
+ additionalProperties: true,
92
+ properties: {
93
+ contractVersion: { type: 'string', minLength: 1 },
94
+ agentType: { type: 'string', minLength: 1 },
95
+ agentInstanceId: { type: 'string', minLength: 1 },
96
+ displayName: { type: 'string', minLength: 1 },
97
+ capabilities: { type: 'array', items: capabilityDescriptionSchema },
98
+ orchestration: {
99
+ type: 'object',
100
+ required: ['canPlan', 'canExpandPlan', 'canExecute', 'canCancel', 'canResume', 'supportsIdempotency', 'supportsParallelWorkers'],
101
+ additionalProperties: true,
102
+ properties: {
103
+ canPlan: { type: 'boolean' },
104
+ canExpandPlan: { type: 'boolean' },
105
+ canExecute: { type: 'boolean' },
106
+ canCancel: { type: 'boolean' },
107
+ canResume: { type: 'boolean' },
108
+ supportsIdempotency: { type: 'boolean' },
109
+ supportsParallelWorkers: { type: 'boolean' },
110
+ },
111
+ },
112
+ limits: {
113
+ type: 'object',
114
+ required: ['recommendedConcurrency', 'maxConcurrency'],
115
+ additionalProperties: true,
116
+ properties: {
117
+ recommendedConcurrency: { type: 'number', minimum: 0 },
118
+ maxConcurrency: { type: 'number', minimum: 0 },
119
+ maxTasksPerPlan: { type: 'number', minimum: 0 },
120
+ maxTaskDurationMs: { type: 'number', minimum: 0 },
121
+ },
122
+ },
123
+ health: {
124
+ type: 'object',
125
+ required: ['status'],
126
+ additionalProperties: false,
127
+ properties: {
128
+ status: { type: 'string', enum: ['available', 'degraded', 'unavailable'] },
129
+ },
130
+ },
131
+ },
132
+ };
133
+
134
+ const taskGroupSchema = {
135
+ $id: 'https://dotdrelle.dev/wiki-manager/contracts/task-group/v1',
136
+ title: 'TaskGroup',
137
+ schemaVersion: '1',
138
+ type: 'object',
139
+ required: ['id', 'label'],
140
+ additionalProperties: false,
141
+ properties: {
142
+ id: { type: 'string', minLength: 1 },
143
+ label: { type: 'string', minLength: 1 },
144
+ recommendedConcurrency: { type: 'integer', minimum: 1 },
145
+ progressWeight: { type: 'number', minimum: 0 },
146
+ },
147
+ };
148
+
149
+ const plannedTaskSchema = {
150
+ $id: 'https://dotdrelle.dev/wiki-manager/contracts/planned-task/v1',
151
+ title: 'PlannedTask',
152
+ schemaVersion: '1',
153
+ type: 'object',
154
+ required: [
155
+ 'id',
156
+ 'label',
157
+ 'requiredCapability',
158
+ 'operation',
159
+ 'dependsOn',
160
+ 'parallelizable',
161
+ 'inputRefs',
162
+ 'locks',
163
+ 'requiresApproval',
164
+ 'idempotencyKey',
165
+ 'progressWeight',
166
+ ],
167
+ additionalProperties: false,
168
+ properties: {
169
+ id: { type: 'string', minLength: 1 },
170
+ label: { type: 'string', minLength: 1 },
171
+ requiredCapability: { type: 'string', minLength: 1 },
172
+ operation: { type: 'string', minLength: 1 },
173
+ arguments: { type: 'object', additionalProperties: true },
174
+ groupId: { type: 'string' },
175
+ dependsOn: stringArraySchema,
176
+ dependsOnGroup: { type: 'string' },
177
+ barrier: { type: 'boolean' },
178
+ parallelizable: { type: 'boolean' },
179
+ recommendedConcurrency: { type: 'integer', minimum: 1 },
180
+ inputRefs: { type: 'array', items: outputReferenceSchema },
181
+ expectedOutputRefs: { type: 'array', items: outputReferenceSchema },
182
+ locks: stringArraySchema,
183
+ requiresApproval: { type: 'boolean' },
184
+ approvalClass: { type: 'string' },
185
+ approvalSummary: { type: 'string' },
186
+ idempotencyKey: { type: ['string', 'null'] },
187
+ progressWeight: { type: 'number', minimum: 0 },
188
+ priority: { type: 'number' },
189
+ retryPolicy: retryPolicySchema,
190
+ },
191
+ };
192
+
193
+ const taskGraphFragmentSchema = {
194
+ $id: 'https://dotdrelle.dev/wiki-manager/contracts/task-graph-fragment/v1',
195
+ title: 'TaskGraphFragment',
196
+ schemaVersion: '1',
197
+ type: 'object',
198
+ required: ['contractVersion', 'agentInstanceId', 'capability', 'summary', 'groups', 'tasks'],
199
+ additionalProperties: false,
200
+ properties: {
201
+ contractVersion: { type: 'string', minLength: 1 },
202
+ agentInstanceId: { type: 'string', minLength: 1 },
203
+ capability: { type: 'string', minLength: 1 },
204
+ summary: {
205
+ type: 'object',
206
+ required: ['label', 'initialSynthesis', 'estimatedTasks'],
207
+ additionalProperties: false,
208
+ properties: {
209
+ label: { type: 'string', minLength: 1 },
210
+ initialSynthesis: stringArraySchema,
211
+ estimatedTasks: { type: 'integer', minimum: 0 },
212
+ },
213
+ },
214
+ groups: { type: 'array', items: taskGroupSchema },
215
+ tasks: { type: 'array', items: plannedTaskSchema },
216
+ expectedOutputs: { type: 'array', items: outputReferenceSchema },
217
+ },
218
+ };
219
+
220
+ const planExpansionRequestSchema = {
221
+ $id: 'https://dotdrelle.dev/wiki-manager/contracts/plan-expansion-request/v1',
222
+ title: 'PlanExpansionRequest',
223
+ schemaVersion: '1',
224
+ type: 'object',
225
+ required: ['capability'],
226
+ additionalProperties: true,
227
+ properties: {
228
+ capability: { type: 'string', minLength: 1 },
229
+ operation: nullableString,
230
+ objective: nullableString,
231
+ reason: nullableString,
232
+ arguments: { type: 'object', additionalProperties: true },
233
+ workspace: { type: 'object', additionalProperties: true },
234
+ constraints: {
235
+ type: 'object',
236
+ additionalProperties: true,
237
+ properties: {
238
+ maxTasks: { type: 'integer', minimum: 1 },
239
+ maxConcurrency: { type: 'integer', minimum: 1 },
240
+ maxDepth: { type: 'integer', minimum: 1 },
241
+ requireApprovalForMutations: { type: 'boolean' },
242
+ },
243
+ },
244
+ insertBeforeTasks: stringArraySchema,
245
+ insertAfterTasks: stringArraySchema,
246
+ },
247
+ };
248
+
249
+ // Shared by planStepSchema and patchTaskSchema, which differ only in their
250
+ // `id`/`description` requiredness — every other field is identical.
251
+ const taskFieldsSchema = {
252
+ label: { type: 'string' },
253
+ requiredCapability: { type: ['string', 'null'] },
254
+ operation: { type: ['string', 'null'] },
255
+ arguments: { type: 'object', additionalProperties: true },
256
+ groupId: nullableString,
257
+ status: { type: 'string' },
258
+ dependsOn: { type: 'array', items: { type: 'string' } },
259
+ dependsOnGroup: nullableString,
260
+ barrier: { type: 'boolean' },
261
+ parallelizable: { type: 'boolean' },
262
+ recommendedConcurrency: { type: 'integer', minimum: 1 },
263
+ executor: nullableString,
264
+ executorQuery: nullableObject,
265
+ inputRefs: { type: 'array', items: outputReferenceSchema },
266
+ expectedOutputRefs: { type: 'array', items: outputReferenceSchema },
267
+ locks: stringArraySchema,
268
+ requiresApproval: { type: 'boolean' },
269
+ approvalClass: nullableString,
270
+ approvalSummary: nullableString,
271
+ idempotencyKey: { type: ['string', 'null'] },
272
+ progressWeight: { type: 'number', minimum: 0 },
273
+ priority: { type: 'number' },
274
+ retryPolicy: retryPolicySchema,
275
+ outputRefs: { type: 'array', items: outputReferenceSchema },
276
+ };
277
+
41
278
  const planStepSchema = {
42
279
  type: 'object',
43
280
  required: ['description', 'status', 'dependsOn', 'outputRefs'],
@@ -46,11 +283,7 @@ const planStepSchema = {
46
283
  step: { type: 'number' },
47
284
  id: nullableString,
48
285
  description: { type: 'string' },
49
- status: { type: 'string' },
50
- dependsOn: { type: 'array', items: { type: 'string' } },
51
- executor: nullableString,
52
- executorQuery: nullableObject,
53
- outputRefs: { type: 'array', items: outputReferenceSchema },
286
+ ...taskFieldsSchema,
54
287
  },
55
288
  };
56
289
 
@@ -61,11 +294,7 @@ const patchTaskSchema = {
61
294
  properties: {
62
295
  id: { type: 'string' },
63
296
  description: { type: 'string' },
64
- status: { type: 'string' },
65
- dependsOn: { type: 'array', items: { type: 'string' } },
66
- executor: nullableString,
67
- executorQuery: nullableObject,
68
- outputRefs: { type: 'array', items: outputReferenceSchema },
297
+ ...taskFieldsSchema,
69
298
  },
70
299
  };
71
300
 
@@ -145,7 +374,7 @@ export const contractSchemas = {
145
374
  message: { type: 'string' },
146
375
  prompt: { type: 'string' },
147
376
  request: { type: 'string' },
148
- intent: { type: 'string', enum: ['observe', 'converse', 'mutate', 'enqueue'] },
377
+ intent: { type: 'string', enum: ['observe', 'converse', 'mutate', 'enqueue', 'approve', 'modify_run', 'enqueue_run', 'cancel', 'ambiguous'] },
149
378
  workspace: nullableString,
150
379
  patchId: { type: 'string' },
151
380
  id: { type: 'string' },
@@ -157,7 +386,7 @@ export const contractSchemas = {
157
386
  title: 'StructuredPlan',
158
387
  schemaVersion: '1',
159
388
  type: 'array',
160
- items: planStepSchema,
389
+ items: { anyOf: [planStepSchema, plannedTaskSchema] },
161
390
  },
162
391
  planPatch: {
163
392
  $id: 'https://dotdrelle.dev/wiki-manager/contracts/plan-patch/v1',
@@ -179,7 +408,7 @@ export const contractSchemas = {
179
408
  additionalProperties: true,
180
409
  properties: {
181
410
  op: { type: 'string', enum: ['add_task', 'add_dependency', 'remove_dependency', 'cancel_task', 'replace_executor', 'request_approval'] },
182
- task: patchTaskSchema,
411
+ task: { anyOf: [patchTaskSchema, plannedTaskSchema] },
183
412
  taskId: { type: 'string' },
184
413
  targetTaskId: { type: 'string' },
185
414
  dependencyId: { type: 'string' },
@@ -193,6 +422,13 @@ export const contractSchemas = {
193
422
  },
194
423
  },
195
424
  outputReference: outputReferenceSchema,
425
+ capabilityDescription: capabilityDescriptionSchema,
426
+ agentDescription: agentDescriptionSchema,
427
+ retryPolicy: retryPolicySchema,
428
+ taskGroup: taskGroupSchema,
429
+ plannedTask: plannedTaskSchema,
430
+ taskGraphFragment: taskGraphFragmentSchema,
431
+ planExpansionRequest: planExpansionRequestSchema,
196
432
  };
197
433
 
198
434
  export function validateContract(name, value) {
@@ -91,3 +91,148 @@ test('plan and patch contracts cover structured dependencies and output refs', (
91
91
  assert.equal(validateContract('plan', plan).ok, true);
92
92
  assert.equal(validateContract('planPatch', patch).ok, true);
93
93
  });
94
+
95
+ test('task graph fragment contract carries planned tasks and groups round-trip', () => {
96
+ const fragment = {
97
+ contractVersion: '1',
98
+ agentInstanceId: 'production-main',
99
+ capability: 'knowledge.update',
100
+ summary: {
101
+ label: 'Update knowledge',
102
+ initialSynthesis: ['Two files will be ingested.'],
103
+ estimatedTasks: 1,
104
+ },
105
+ groups: [{
106
+ id: 'ingest',
107
+ label: 'Ingest sources',
108
+ recommendedConcurrency: 2,
109
+ progressWeight: 2,
110
+ }],
111
+ tasks: [{
112
+ id: 'ingest-a',
113
+ label: 'Ingest source A',
114
+ requiredCapability: 'knowledge.update',
115
+ operation: 'ingest',
116
+ arguments: { files: ['raw/untracked/a.md'] },
117
+ groupId: 'ingest',
118
+ dependsOn: [],
119
+ parallelizable: true,
120
+ recommendedConcurrency: 2,
121
+ inputRefs: [{ type: 'file', ref: 'raw/untracked/a.md', label: 'A' }],
122
+ expectedOutputRefs: ['wiki/a.md'],
123
+ locks: ['workspace:wiki'],
124
+ requiresApproval: false,
125
+ idempotencyKey: null,
126
+ progressWeight: 1,
127
+ priority: 10,
128
+ retryPolicy: {
129
+ maxAttempts: 2,
130
+ retryableErrors: ['timeout'],
131
+ allowAgentFallback: false,
132
+ },
133
+ }],
134
+ expectedOutputs: ['wiki/a.md'],
135
+ };
136
+
137
+ const roundTrip = JSON.parse(JSON.stringify(fragment));
138
+ assert.deepEqual(roundTrip, fragment);
139
+ assert.equal(validateContract('taskGraphFragment', roundTrip).ok, true);
140
+ assert.equal(validateContract('plannedTask', roundTrip.tasks[0]).ok, true);
141
+ assert.equal(validateContract('taskGroup', roundTrip.groups[0]).ok, true);
142
+ assert.equal(validateContract('retryPolicy', roundTrip.tasks[0].retryPolicy).ok, true);
143
+ });
144
+
145
+ test('strict planned task contract rejects invalid fields and types', () => {
146
+ const invalid = {
147
+ id: 'bad',
148
+ label: 'Bad task',
149
+ requiredCapability: 'knowledge.update',
150
+ operation: 'ingest',
151
+ dependsOn: [],
152
+ parallelizable: false,
153
+ inputRefs: [],
154
+ locks: [],
155
+ requiresApproval: false,
156
+ idempotencyKey: null,
157
+ progressWeight: 1,
158
+ executor: 'legacy-tool',
159
+ };
160
+
161
+ const result = validateContract('plannedTask', invalid);
162
+ assert.equal(result.ok, false);
163
+ assert.ok(result.errors.some((error) => error.includes('executor is not allowed')));
164
+ assert.throws(() => assertContract('plannedTask', invalid), /executor is not allowed/);
165
+ });
166
+
167
+ test('plan and plan patch contracts accept new planned task fields without rejecting legacy tasks', () => {
168
+ const plannedTask = {
169
+ id: 'publish',
170
+ label: 'Publish deliverable',
171
+ requiredCapability: 'document.publish',
172
+ operation: 'publish',
173
+ arguments: { target: 'deliverables/report.md' },
174
+ dependsOn: ['build'],
175
+ parallelizable: false,
176
+ inputRefs: ['deliverables/report.md'],
177
+ locks: ['deliverable:report.md'],
178
+ requiresApproval: true,
179
+ approvalClass: 'mutation',
180
+ approvalSummary: 'Publish report',
181
+ idempotencyKey: 'idem-1',
182
+ progressWeight: 1,
183
+ };
184
+ const legacyTask = {
185
+ step: 1,
186
+ id: 'build',
187
+ description: 'Build report',
188
+ status: 'done',
189
+ dependsOn: [],
190
+ outputRefs: ['deliverables/report.md'],
191
+ };
192
+
193
+ assert.equal(validateContract('plan', [legacyTask, plannedTask]).ok, true);
194
+ assert.equal(validateContract('planPatch', {
195
+ basePlanRevision: 1,
196
+ operations: [{ op: 'add_task', task: plannedTask }],
197
+ }).ok, true);
198
+ });
199
+
200
+ test('agent description contract validates orchestrable agent capabilities', () => {
201
+ const description = {
202
+ contractVersion: '1',
203
+ agentType: 'production',
204
+ agentInstanceId: 'production-main',
205
+ displayName: 'Production',
206
+ capabilities: [{
207
+ id: 'knowledge.update',
208
+ version: '1',
209
+ description: 'Update wiki knowledge',
210
+ inputSchema: { type: 'object' },
211
+ outputSchema: { type: 'object' },
212
+ supportedOperations: ['ingest', 'pipeline'],
213
+ mutationClass: 'workspace',
214
+ defaultRequiresApproval: true,
215
+ estimatedCost: { llmCalls: 2, tokenRange: [1000, 3000] },
216
+ }],
217
+ orchestration: {
218
+ canPlan: true,
219
+ canExpandPlan: false,
220
+ canExecute: true,
221
+ canCancel: true,
222
+ canResume: false,
223
+ supportsIdempotency: false,
224
+ supportsParallelWorkers: true,
225
+ },
226
+ limits: {
227
+ recommendedConcurrency: 2,
228
+ maxConcurrency: 4,
229
+ maxTasksPerPlan: 50,
230
+ maxTaskDurationMs: 600000,
231
+ },
232
+ health: { status: 'available' },
233
+ };
234
+
235
+ assert.equal(validateContract('agentDescription', description).ok, true);
236
+ assert.equal(validateContract('capabilityDescription', description.capabilities[0]).ok, true);
237
+ assert.equal(validateContract('agentDescription', { ...description, health: { status: 'offline' } }).ok, false);
238
+ });
@@ -100,11 +100,12 @@ export function normalizeActivity(activity, fallback = {}) {
100
100
  return normalized;
101
101
  }
102
102
 
103
- function productionActivityFromPayload(payload) {
103
+ function productionActivityFromPayload(payload, context = {}) {
104
104
  const progress = payload?.progress;
105
105
  const job = payload?.job;
106
106
  const jobId = payload?.jobId ?? job?.jobId;
107
107
  if (!progress && !job && !jobId) return null;
108
+ const result = payload?.result && typeof payload.result === 'object' ? payload.result : null;
108
109
  const status = job?.status ?? payload?.status ?? progress?.status ?? 'running';
109
110
  const percent = Number.isFinite(Number(progress?.percent)) ? Number(progress.percent) : null;
110
111
  const sourceCount = Number(progress?.sourceCount);
@@ -139,21 +140,23 @@ function productionActivityFromPayload(payload) {
139
140
  return normalizeActivity({
140
141
  id: jobId,
141
142
  source: 'production',
142
- kind: job?.type ?? payload?.type ?? progress?.phase ?? progress?.currentStep ?? 'job',
143
+ kind: job?.type ?? payload?.operation ?? payload?.type ?? progress?.phase ?? progress?.currentStep ?? 'job',
143
144
  label: detail ? `Production: ${detail}` : `Production: ${status}`,
144
145
  status,
145
146
  progress: {
146
147
  ...(progress ?? {}),
147
148
  ...(percent !== null ? { percent } : {}),
148
149
  step,
150
+ ...(payload?.taskId ? { stepId: String(payload.taskId) } : {}),
149
151
  },
150
152
  poll: jobId ? {
151
153
  server: 'production',
152
- tool: 'production_job_status',
154
+ tool: context.tool === 'agent_status' ? 'agent_status' : 'production_job_status',
153
155
  args: { jobId },
154
156
  intervalMs: 2500,
155
157
  } : null,
156
- error: job?.error ?? payload?.error ?? null,
158
+ outputRefs: Array.isArray(result?.outputRefs) ? result.outputRefs : [],
159
+ error: job?.error ?? result?.error?.message ?? payload?.error ?? null,
157
160
  });
158
161
  }
159
162
 
@@ -163,7 +166,7 @@ export function extractActivity(payload, context = {}) {
163
166
  return normalizeActivity(payload._activity, { source: context.server });
164
167
  }
165
168
  if (context.server === 'production') {
166
- return productionActivityFromPayload(payload);
169
+ return productionActivityFromPayload(payload, context);
167
170
  }
168
171
  return null;
169
172
  }