agentic-orchestrator 0.1.13 → 0.1.15

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 (108) hide show
  1. package/AGENTS.md +139 -0
  2. package/CLAUDE.md +12 -0
  3. package/agentic/orchestrator/agents.yaml +3 -0
  4. package/agentic/orchestrator/defaults/policy.defaults.yaml +3 -0
  5. package/agentic/orchestrator/policy.yaml +3 -0
  6. package/agentic/orchestrator/schemas/agents.schema.json +15 -0
  7. package/agentic/orchestrator/schemas/policy.schema.json +14 -0
  8. package/apps/control-plane/src/cli/cli-argument-parser.ts +7 -0
  9. package/apps/control-plane/src/cli/help-command-handler.ts +8 -0
  10. package/apps/control-plane/src/cli/init-command-handler.ts +6 -0
  11. package/apps/control-plane/src/cli/resume-command-handler.ts +31 -2
  12. package/apps/control-plane/src/cli/run-command-handler.ts +31 -3
  13. package/apps/control-plane/src/cli/types.ts +1 -0
  14. package/apps/control-plane/src/core/error-codes.ts +4 -0
  15. package/apps/control-plane/src/core/kernel.ts +3 -0
  16. package/apps/control-plane/src/index.ts +14 -0
  17. package/apps/control-plane/src/interfaces/cli/bootstrap.ts +25 -3
  18. package/apps/control-plane/src/providers/api-worker-provider.ts +115 -0
  19. package/apps/control-plane/src/providers/cli-worker-provider.ts +385 -0
  20. package/apps/control-plane/src/providers/output-parsers/generic-output-parser.ts +100 -0
  21. package/apps/control-plane/src/providers/output-parsers/index.ts +11 -0
  22. package/apps/control-plane/src/providers/output-parsers/types.ts +23 -0
  23. package/apps/control-plane/src/providers/providers.ts +19 -0
  24. package/apps/control-plane/src/providers/worker-provider-factory.ts +198 -0
  25. package/apps/control-plane/src/supervisor/build-wave-executor.ts +140 -3
  26. package/apps/control-plane/src/supervisor/planning-wave-executor.ts +125 -5
  27. package/apps/control-plane/src/supervisor/qa-wave-executor.ts +144 -2
  28. package/apps/control-plane/src/supervisor/runtime.ts +24 -0
  29. package/apps/control-plane/src/supervisor/worker-decision-loop.ts +134 -12
  30. package/apps/control-plane/test/cli.unit.spec.ts +36 -0
  31. package/apps/control-plane/test/dashboard-api.integration.spec.ts +2 -2
  32. package/apps/control-plane/test/resume-command.spec.ts +31 -1
  33. package/apps/control-plane/test/worker-decision-loop.spec.ts +3 -0
  34. package/apps/control-plane/test/worker-execution-policy.spec.ts +284 -0
  35. package/apps/control-plane/test/worker-provider-adapters.spec.ts +440 -0
  36. package/apps/control-plane/test/worker-provider-factory.spec.ts +151 -0
  37. package/config/agentic/orchestrator/agents.yaml +3 -0
  38. package/dist/apps/control-plane/cli/cli-argument-parser.js +7 -0
  39. package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
  40. package/dist/apps/control-plane/cli/help-command-handler.js +8 -0
  41. package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
  42. package/dist/apps/control-plane/cli/init-command-handler.js +6 -0
  43. package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
  44. package/dist/apps/control-plane/cli/resume-command-handler.d.ts +3 -0
  45. package/dist/apps/control-plane/cli/resume-command-handler.js +18 -2
  46. package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
  47. package/dist/apps/control-plane/cli/run-command-handler.d.ts +3 -1
  48. package/dist/apps/control-plane/cli/run-command-handler.js +17 -3
  49. package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
  50. package/dist/apps/control-plane/cli/types.d.ts +1 -0
  51. package/dist/apps/control-plane/core/error-codes.d.ts +4 -0
  52. package/dist/apps/control-plane/core/error-codes.js +4 -0
  53. package/dist/apps/control-plane/core/error-codes.js.map +1 -1
  54. package/dist/apps/control-plane/core/kernel.d.ts +3 -0
  55. package/dist/apps/control-plane/core/kernel.js.map +1 -1
  56. package/dist/apps/control-plane/index.d.ts +2 -0
  57. package/dist/apps/control-plane/index.js +1 -0
  58. package/dist/apps/control-plane/index.js.map +1 -1
  59. package/dist/apps/control-plane/interfaces/cli/bootstrap.js +14 -2
  60. package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
  61. package/dist/apps/control-plane/providers/api-worker-provider.d.ts +31 -0
  62. package/dist/apps/control-plane/providers/api-worker-provider.js +73 -0
  63. package/dist/apps/control-plane/providers/api-worker-provider.js.map +1 -0
  64. package/dist/apps/control-plane/providers/cli-worker-provider.d.ts +46 -0
  65. package/dist/apps/control-plane/providers/cli-worker-provider.js +274 -0
  66. package/dist/apps/control-plane/providers/cli-worker-provider.js.map +1 -0
  67. package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.d.ts +10 -0
  68. package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.js +79 -0
  69. package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.js.map +1 -0
  70. package/dist/apps/control-plane/providers/output-parsers/index.d.ts +2 -0
  71. package/dist/apps/control-plane/providers/output-parsers/index.js +2 -0
  72. package/dist/apps/control-plane/providers/output-parsers/index.js.map +1 -0
  73. package/dist/apps/control-plane/providers/output-parsers/types.d.ts +21 -0
  74. package/dist/apps/control-plane/providers/output-parsers/types.js +2 -0
  75. package/dist/apps/control-plane/providers/output-parsers/types.js.map +1 -0
  76. package/dist/apps/control-plane/providers/providers.d.ts +4 -0
  77. package/dist/apps/control-plane/providers/providers.js +15 -0
  78. package/dist/apps/control-plane/providers/providers.js.map +1 -1
  79. package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +41 -0
  80. package/dist/apps/control-plane/providers/worker-provider-factory.js +104 -0
  81. package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -0
  82. package/dist/apps/control-plane/supervisor/build-wave-executor.d.ts +13 -0
  83. package/dist/apps/control-plane/supervisor/build-wave-executor.js +92 -3
  84. package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
  85. package/dist/apps/control-plane/supervisor/planning-wave-executor.d.ts +12 -0
  86. package/dist/apps/control-plane/supervisor/planning-wave-executor.js +83 -5
  87. package/dist/apps/control-plane/supervisor/planning-wave-executor.js.map +1 -1
  88. package/dist/apps/control-plane/supervisor/qa-wave-executor.d.ts +13 -0
  89. package/dist/apps/control-plane/supervisor/qa-wave-executor.js +91 -2
  90. package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
  91. package/dist/apps/control-plane/supervisor/runtime.js +19 -0
  92. package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
  93. package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +10 -0
  94. package/dist/apps/control-plane/supervisor/worker-decision-loop.js +113 -12
  95. package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
  96. package/package.json +2 -2
  97. package/packages/web-dashboard/next-env.d.ts +2 -1
  98. package/packages/web-dashboard/src/app/api/features/[id]/checkout/route.ts +4 -3
  99. package/packages/web-dashboard/src/app/api/features/[id]/diff/route.ts +6 -2
  100. package/packages/web-dashboard/src/app/api/features/[id]/evidence/[artifact]/route.ts +6 -5
  101. package/packages/web-dashboard/src/app/api/features/[id]/review/route.ts +5 -4
  102. package/packages/web-dashboard/src/app/api/features/[id]/route.ts +7 -3
  103. package/packages/web-dashboard/src/lib/aop-client.ts +2 -2
  104. package/packages/web-dashboard/src/lib/orchestrator-tools.ts +1 -1
  105. package/packages/web-dashboard/tsconfig.json +1 -0
  106. package/spec-files/outstanding/agentic_orchestrator_human_input_interaction_protocol_spec.md +590 -0
  107. package/spec-files/outstanding/agentic_orchestrator_real_worker_provider_execution_spec.md +616 -0
  108. package/spec-files/progress.md +91 -0
@@ -1,4 +1,5 @@
1
1
  import { GATE_RESULT, STATUS, TOOLS } from '../core/constants.js';
2
+ import { ERROR_CODES } from '../core/error-codes.js';
2
3
  import type { WorkerProvider } from '../providers/providers.js';
3
4
  import type {
4
5
  AgentPromptProvider,
@@ -14,6 +15,11 @@ import type {
14
15
  GateRepairContext,
15
16
  ReactionsService,
16
17
  } from '../application/services/reactions-service.js';
18
+ import type {
19
+ WorkerMalformedOutputAction,
20
+ WorkerNoProgressAction,
21
+ WorkerProviderMode,
22
+ } from '../providers/worker-provider-factory.js';
17
23
 
18
24
  interface GatesRunData {
19
25
  overall?: string;
@@ -29,6 +35,10 @@ interface QaWaveExecutorDependencies {
29
35
  state: SupervisorRuntimeState;
30
36
  workerDecisionRunner?: WorkerDecisionRunner;
31
37
  reactionsService?: ReactionsService;
38
+ providerMode?: WorkerProviderMode;
39
+ noProgressLimit?: number;
40
+ noProgressAction?: WorkerNoProgressAction;
41
+ malformedOutputAction?: WorkerMalformedOutputAction;
32
42
  }
33
43
 
34
44
  function isRetryableToolError(error: unknown): boolean {
@@ -42,6 +52,13 @@ function isRetryableToolError(error: unknown): boolean {
42
52
  return (details as { retryable?: unknown }).retryable === true;
43
53
  }
44
54
 
55
+ function asVersion(value: unknown): number | null {
56
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
57
+ return null;
58
+ }
59
+ return Math.floor(value);
60
+ }
61
+
45
62
  export class QaWaveExecutor {
46
63
  private readonly kernel: FeatureOrchestrationPort;
47
64
  private readonly provider: WorkerProvider;
@@ -51,6 +68,12 @@ export class QaWaveExecutor {
51
68
  private readonly state: SupervisorRuntimeState;
52
69
  private readonly workerDecisionRunner: WorkerDecisionRunner;
53
70
  private readonly reactionsService: ReactionsService | undefined;
71
+ private readonly providerMode: WorkerProviderMode;
72
+ private readonly noProgressLimit: number;
73
+ private readonly noProgressAction: WorkerNoProgressAction;
74
+ private readonly malformedOutputAction: WorkerMalformedOutputAction;
75
+ private readonly noProgressByFeature = new Map<string, number>();
76
+ private readonly blockedForCurrentCycle = new Set<string>();
54
77
 
55
78
  constructor(dependencies: QaWaveExecutorDependencies) {
56
79
  this.kernel = dependencies.kernel;
@@ -61,9 +84,17 @@ export class QaWaveExecutor {
61
84
  this.state = dependencies.state;
62
85
  this.workerDecisionRunner = dependencies.workerDecisionRunner ?? NOOP_WORKER_DECISION_RUNNER;
63
86
  this.reactionsService = dependencies.reactionsService;
87
+ this.providerMode = dependencies.providerMode ?? 'stub';
88
+ this.noProgressLimit =
89
+ typeof dependencies.noProgressLimit === 'number' && dependencies.noProgressLimit > 0
90
+ ? Math.floor(dependencies.noProgressLimit)
91
+ : 2;
92
+ this.noProgressAction = dependencies.noProgressAction ?? 'block_feature';
93
+ this.malformedOutputAction = dependencies.malformedOutputAction ?? 'block_feature';
64
94
  }
65
95
 
66
96
  async run(featureIds: string[], maxParallelGateRuns: number): Promise<void> {
97
+ this.blockedForCurrentCycle.clear();
67
98
  const batch: string[] = [];
68
99
  for (const featureId of featureIds) {
69
100
  const state = await this.toolCaller.callTool<FeatureStatePayload>(
@@ -83,13 +114,18 @@ export class QaWaveExecutor {
83
114
  const context = await this.toolCaller.callTool('qa', TOOLS.FEATURE_GET_CONTEXT, {
84
115
  feature_id: featureId,
85
116
  });
86
- await this.workerDecisionRunner.execute({
117
+ const decision = await this.workerDecisionRunner.execute({
87
118
  role: 'qa',
88
119
  featureId,
89
120
  contextBundle: context.data,
90
121
  instructions:
91
122
  'Emit PATCH outputs for deterministic QA remediations, NOTE outputs for findings, and REQUEST outputs for lock/context needs.',
92
123
  });
124
+ const shouldSkip = await this.enforceWorkerExecutionPolicy(featureId, decision, 'qa');
125
+ if (shouldSkip) {
126
+ continue;
127
+ }
128
+ this.noProgressByFeature.delete(featureId);
93
129
 
94
130
  const stateForRetry = await this.toolCaller.callTool<FeatureStatePayload>(
95
131
  'qa',
@@ -154,12 +190,21 @@ export class QaWaveExecutor {
154
190
  failureHistory,
155
191
  };
156
192
  const repairPrompt = this.reactionsService.buildRepairPrompt(ctx);
157
- await this.workerDecisionRunner.execute({
193
+ const repairDecision = await this.workerDecisionRunner.execute({
158
194
  role: 'qa',
159
195
  featureId,
160
196
  contextBundle: context.data,
161
197
  instructions: repairPrompt,
162
198
  });
199
+ const shouldSkipRepair = await this.enforceWorkerExecutionPolicy(
200
+ featureId,
201
+ repairDecision,
202
+ 'qa',
203
+ );
204
+ if (shouldSkipRepair) {
205
+ break;
206
+ }
207
+ this.noProgressByFeature.delete(featureId);
163
208
 
164
209
  try {
165
210
  const retryResult = await this.toolCaller.callTool<GatesRunData>(
@@ -246,4 +291,101 @@ export class QaWaveExecutor {
246
291
  });
247
292
  }
248
293
  }
294
+
295
+ private async enforceWorkerExecutionPolicy(
296
+ featureId: string,
297
+ decision: {
298
+ invalidOutput: boolean;
299
+ noProgress: boolean;
300
+ },
301
+ phase: 'qa',
302
+ ): Promise<boolean> {
303
+ if (this.providerMode !== 'live') {
304
+ this.noProgressByFeature.delete(featureId);
305
+ return false;
306
+ }
307
+
308
+ if (decision.invalidOutput) {
309
+ await this.applyWorkerPolicyAction(
310
+ featureId,
311
+ this.malformedOutputAction,
312
+ ERROR_CODES.PROVIDER_OUTPUT_INVALID,
313
+ 'QA emitted malformed worker outputs',
314
+ phase,
315
+ );
316
+ this.blockedForCurrentCycle.add(featureId);
317
+ return true;
318
+ }
319
+
320
+ if (!decision.noProgress) {
321
+ this.noProgressByFeature.delete(featureId);
322
+ return false;
323
+ }
324
+
325
+ const currentCount = (this.noProgressByFeature.get(featureId) ?? 0) + 1;
326
+ this.noProgressByFeature.set(featureId, currentCount);
327
+ this.blockedForCurrentCycle.add(featureId);
328
+ if (currentCount < this.noProgressLimit) {
329
+ return true;
330
+ }
331
+
332
+ await this.applyWorkerPolicyAction(
333
+ featureId,
334
+ this.noProgressAction,
335
+ ERROR_CODES.PROVIDER_NO_PROGRESS,
336
+ `QA made no actionable progress in ${currentCount} consecutive iterations`,
337
+ phase,
338
+ );
339
+ return true;
340
+ }
341
+
342
+ private async applyWorkerPolicyAction(
343
+ featureId: string,
344
+ action: WorkerNoProgressAction | WorkerMalformedOutputAction,
345
+ errorCode: string,
346
+ message: string,
347
+ phase: 'qa',
348
+ ): Promise<void> {
349
+ if (action === 'fail_run') {
350
+ const error = new Error(message) as Error & {
351
+ code?: string;
352
+ details?: Record<string, unknown>;
353
+ };
354
+ error.code = errorCode;
355
+ error.details = {
356
+ feature_id: featureId,
357
+ retryable: false,
358
+ requires_human: true,
359
+ };
360
+ throw error;
361
+ }
362
+
363
+ const state = await this.toolCaller.callTool<FeatureStatePayload>(
364
+ 'orchestrator',
365
+ TOOLS.FEATURE_STATE_GET,
366
+ {
367
+ feature_id: featureId,
368
+ },
369
+ );
370
+ const expectedVersion = asVersion(state.data.front_matter.version);
371
+ await this.toolCaller.callTool('orchestrator', TOOLS.FEATURE_STATE_PATCH, {
372
+ feature_id: featureId,
373
+ expected_version: expectedVersion,
374
+ patch: {
375
+ front_matter: {
376
+ status: STATUS.BLOCKED,
377
+ status_reason: `${errorCode}: ${message}`,
378
+ },
379
+ },
380
+ });
381
+ await this.toolCaller.callTool('orchestrator', TOOLS.FEATURE_LOG_APPEND, {
382
+ feature_id: featureId,
383
+ note: JSON.stringify({
384
+ phase,
385
+ decision: 'blocked',
386
+ error_code: errorCode,
387
+ reason: message,
388
+ }),
389
+ });
390
+ }
249
391
  }
@@ -16,6 +16,11 @@ import { ReactionsService } from '../application/services/reactions-service.js';
16
16
  import { ActivityMonitorService } from '../application/services/activity-monitor-service.js';
17
17
  import { PrMonitorService, createGhRunner } from '../application/services/pr-monitor-service.js';
18
18
  import { createIssueTracker } from '../application/services/issue-tracker-service.js';
19
+ import {
20
+ resolveMalformedWorkerOutputAction,
21
+ resolveNoProgressAction,
22
+ resolveWorkerProviderRuntime,
23
+ } from '../providers/worker-provider-factory.js';
19
24
  import {
20
25
  globalAdapterRegistry,
21
26
  NOTIFICATION_CHANNEL_SLOT,
@@ -190,23 +195,38 @@ export class SupervisorRuntime
190
195
  ? (rawIssueTrackerConfig as { type: string; config?: Record<string, string> })
191
196
  : undefined;
192
197
  const issueTracker = createIssueTracker(issueTrackerConfig);
198
+ const workerRuntimeConfig = resolveWorkerProviderRuntime(this.kernel.getAgentsConfig().runtime);
199
+ const executionPolicy =
200
+ policy.execution && typeof policy.execution === 'object'
201
+ ? (policy.execution as Record<string, unknown>)
202
+ : null;
193
203
 
194
204
  this.workerDecisionLoop = new WorkerDecisionLoop({
195
205
  provider: this.provider,
196
206
  toolCaller: this,
197
207
  activityMonitor,
208
+ repoRoot: this.kernel.getRepoRoot(),
209
+ runId: () => this.state.runId,
198
210
  });
199
211
 
200
212
  this.planningWaveExecutor = new PlanningWaveExecutor({
201
213
  toolCaller: this,
202
214
  planGenerator: this,
203
215
  workerDecisionRunner: this.workerDecisionLoop,
216
+ providerMode: this.provider.mode,
217
+ noProgressLimit: workerRuntimeConfig.max_consecutive_no_progress_iterations,
218
+ noProgressAction: resolveNoProgressAction(executionPolicy),
219
+ malformedOutputAction: resolveMalformedWorkerOutputAction(executionPolicy),
204
220
  });
205
221
 
206
222
  this.buildWaveExecutor = new BuildWaveExecutor({
207
223
  toolCaller: this,
208
224
  workerDecisionRunner: this.workerDecisionLoop,
209
225
  reactionsService,
226
+ providerMode: this.provider.mode,
227
+ noProgressLimit: workerRuntimeConfig.max_consecutive_no_progress_iterations,
228
+ noProgressAction: resolveNoProgressAction(executionPolicy),
229
+ malformedOutputAction: resolveMalformedWorkerOutputAction(executionPolicy),
210
230
  });
211
231
 
212
232
  this.qaWaveExecutor = new QaWaveExecutor({
@@ -218,6 +238,10 @@ export class SupervisorRuntime
218
238
  state: this.state,
219
239
  workerDecisionRunner: this.workerDecisionLoop,
220
240
  reactionsService,
241
+ providerMode: this.provider.mode,
242
+ noProgressLimit: workerRuntimeConfig.max_consecutive_no_progress_iterations,
243
+ noProgressAction: resolveNoProgressAction(executionPolicy),
244
+ malformedOutputAction: resolveMalformedWorkerOutputAction(executionPolicy),
221
245
  });
222
246
 
223
247
  this.leaseHeartbeatService = new LeaseHeartbeatService({
@@ -1,4 +1,7 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
1
3
  import { TOOLS } from '../core/constants.js';
4
+ import { ERROR_CODES } from '../core/error-codes.js';
2
5
  import type { WorkerProvider } from '../providers/providers.js';
3
6
  import type { RuntimeRole, SupervisorToolCaller } from './types.js';
4
7
  import type { ActivityMonitorService } from '../application/services/activity-monitor-service.js';
@@ -18,6 +21,9 @@ export interface WorkerDecisionResult {
18
21
  patchApplied: boolean;
19
22
  noteLogged: boolean;
20
23
  requestHandled: boolean;
24
+ invalidOutput: boolean;
25
+ noProgress: boolean;
26
+ outputTypes: string[];
21
27
  priorityOrder: string[];
22
28
  toolResults: AnyRecord[];
23
29
  }
@@ -73,6 +79,9 @@ function makeEmptyResult(): WorkerDecisionResult {
73
79
  patchApplied: false,
74
80
  noteLogged: false,
75
81
  requestHandled: false,
82
+ invalidOutput: false,
83
+ noProgress: false,
84
+ outputTypes: [],
76
85
  priorityOrder: [],
77
86
  toolResults: [],
78
87
  };
@@ -86,17 +95,23 @@ interface WorkerDecisionLoopDependencies {
86
95
  provider: WorkerProvider;
87
96
  toolCaller: SupervisorToolCaller;
88
97
  activityMonitor?: ActivityMonitorService;
98
+ repoRoot?: string;
99
+ runId?: string | (() => string);
89
100
  }
90
101
 
91
102
  export class WorkerDecisionLoop implements WorkerDecisionRunner {
92
103
  private readonly provider: WorkerProvider;
93
104
  private readonly toolCaller: SupervisorToolCaller;
94
105
  private readonly activityMonitor: ActivityMonitorService | undefined;
106
+ private readonly repoRoot: string | null;
107
+ private readonly runId: string | (() => string) | null;
95
108
 
96
109
  constructor(dependencies: WorkerDecisionLoopDependencies) {
97
110
  this.provider = dependencies.provider;
98
111
  this.toolCaller = dependencies.toolCaller;
99
112
  this.activityMonitor = dependencies.activityMonitor;
113
+ this.repoRoot = dependencies.repoRoot ?? null;
114
+ this.runId = dependencies.runId ?? null;
100
115
  }
101
116
 
102
117
  async execute(input: WorkerDecisionInput): Promise<WorkerDecisionResult> {
@@ -105,43 +120,93 @@ export class WorkerDecisionLoop implements WorkerDecisionRunner {
105
120
  return result;
106
121
  }
107
122
 
108
- const workerOutput = await this.provider.runWorker({
109
- role: input.role,
110
- feature_id: input.featureId,
111
- context_bundle: input.contextBundle,
112
- instructions: input.instructions,
113
- last_tool_results: input.lastToolResults ?? [],
114
- runtime_selection: {
115
- provider: this.provider.selection.provider,
116
- model: this.provider.selection.model,
117
- provider_config_ref: this.provider.selection.provider_config_ref,
118
- },
119
- });
123
+ let workerOutput: Record<string, unknown>;
124
+ try {
125
+ workerOutput = await this.provider.runWorker({
126
+ role: input.role,
127
+ feature_id: input.featureId,
128
+ context_bundle: input.contextBundle,
129
+ instructions: input.instructions,
130
+ last_tool_results: input.lastToolResults ?? [],
131
+ runtime_selection: {
132
+ provider: this.provider.selection.provider,
133
+ model: this.provider.selection.model,
134
+ provider_config_ref: this.provider.selection.provider_config_ref,
135
+ },
136
+ });
137
+ } catch (error) {
138
+ const typed = error as { code?: string };
139
+ await this.appendWorkerEvent({
140
+ featureId: input.featureId,
141
+ role: input.role,
142
+ outputTypes: [],
143
+ patchCount: 0,
144
+ planSubmissionCount: 0,
145
+ requestCount: 0,
146
+ noteCount: 0,
147
+ valid: false,
148
+ errorCode:
149
+ typeof typed.code === 'string' && typed.code.length > 0
150
+ ? typed.code
151
+ : ERROR_CODES.PROVIDER_RUNTIME_UNAVAILABLE,
152
+ });
153
+ throw error;
154
+ }
120
155
 
121
156
  const outputs = asOutputs(workerOutput);
157
+ const allowedTypes = new Set(['PLAN_SUBMISSION', 'PATCH', 'NOTE', 'REQUEST']);
158
+ let planSubmissionCount = 0;
159
+ let patchCount = 0;
160
+ let requestCount = 0;
161
+ let noteCount = 0;
122
162
  for (const output of outputs) {
123
163
  const outputType = asNonEmptyString(output.type)?.toUpperCase();
124
164
  if (!outputType) {
165
+ result.invalidOutput = true;
166
+ continue;
167
+ }
168
+ result.outputTypes.push(outputType);
169
+ if (!allowedTypes.has(outputType)) {
170
+ result.invalidOutput = true;
125
171
  continue;
126
172
  }
127
173
 
128
174
  if (outputType === 'PLAN_SUBMISSION') {
175
+ planSubmissionCount += 1;
129
176
  await this.routePlanSubmission(input, output, result);
130
177
  continue;
131
178
  }
132
179
  if (outputType === 'PATCH') {
180
+ patchCount += 1;
133
181
  await this.routePatch(input, output, result);
134
182
  continue;
135
183
  }
136
184
  if (outputType === 'NOTE') {
185
+ noteCount += 1;
137
186
  await this.routeNote(input.featureId, output, result);
138
187
  continue;
139
188
  }
140
189
  if (outputType === 'REQUEST') {
190
+ requestCount += 1;
141
191
  await this.routeRequest(input, output, result);
142
192
  }
143
193
  }
144
194
 
195
+ const hasProgress = result.planSubmission || result.patchApplied || result.requestHandled;
196
+ result.noProgress = !hasProgress;
197
+
198
+ await this.appendWorkerEvent({
199
+ featureId: input.featureId,
200
+ role: input.role,
201
+ outputTypes: result.outputTypes,
202
+ patchCount,
203
+ planSubmissionCount,
204
+ requestCount,
205
+ noteCount,
206
+ valid: !result.invalidOutput,
207
+ errorCode: result.invalidOutput ? ERROR_CODES.PROVIDER_OUTPUT_INVALID : null,
208
+ });
209
+
145
210
  if (this.activityMonitor) {
146
211
  await this.activityMonitor.checkAndNotifyStuck(input.featureId);
147
212
  }
@@ -314,4 +379,61 @@ export class WorkerDecisionLoop implements WorkerDecisionRunner {
314
379
  result.planSubmission = true;
315
380
  }
316
381
  }
382
+
383
+ private resolveRunId(): string {
384
+ if (typeof this.runId === 'function') {
385
+ const value = this.runId();
386
+ return value && value.length > 0 ? value : `run:unknown:${Date.now()}`;
387
+ }
388
+ if (typeof this.runId === 'string' && this.runId.length > 0) {
389
+ return this.runId;
390
+ }
391
+ return `run:unknown:${Date.now()}`;
392
+ }
393
+
394
+ private sanitizeRunId(runId: string): string {
395
+ return runId.replace(/[^a-zA-Z0-9._:-]/g, '_');
396
+ }
397
+
398
+ private async appendWorkerEvent(event: {
399
+ featureId: string;
400
+ role: RuntimeRole;
401
+ outputTypes: string[];
402
+ patchCount: number;
403
+ planSubmissionCount: number;
404
+ requestCount: number;
405
+ noteCount: number;
406
+ valid: boolean;
407
+ errorCode: string | null;
408
+ }): Promise<void> {
409
+ if (!this.repoRoot) {
410
+ return;
411
+ }
412
+ const runId = this.resolveRunId();
413
+ const workerEventsDir = path.join(this.repoRoot, '.aop', 'runtime', 'worker-events');
414
+ const filePath = path.join(workerEventsDir, `${this.sanitizeRunId(runId)}.jsonl`);
415
+
416
+ const payload = {
417
+ ts: new Date().toISOString(),
418
+ run_id: runId,
419
+ feature_id: event.featureId,
420
+ role: event.role,
421
+ output_types: event.outputTypes,
422
+ patch_count: event.patchCount,
423
+ plan_submission_count: event.planSubmissionCount,
424
+ request_count: event.requestCount,
425
+ note_count: event.noteCount,
426
+ valid: event.valid,
427
+ error_code: event.errorCode,
428
+ provider: this.provider.selection.provider,
429
+ model: this.provider.selection.model,
430
+ };
431
+
432
+ try {
433
+ await fs.mkdir(workerEventsDir, { recursive: true });
434
+ await fs.appendFile(filePath, `${JSON.stringify(payload)}\n`, 'utf8');
435
+ } catch {
436
+ // Diagnostic logging is best-effort and must not alter wave behavior.
437
+ }
438
+ }
317
439
  }
@@ -97,6 +97,10 @@ vi.mock('../src/core/kernel.js', () => {
97
97
  return {};
98
98
  }
99
99
 
100
+ getPolicySnapshot() {
101
+ return {};
102
+ }
103
+
100
104
  async readIndex() {
101
105
  return await readIndexMock();
102
106
  }
@@ -124,6 +128,7 @@ vi.mock('../src/mcp/runtime-factory.js', () => ({
124
128
 
125
129
  vi.mock('../src/providers/providers.js', () => ({
126
130
  resolveProviderSelection: resolveProviderSelectionMock,
131
+ SUPPORTED_PROVIDERS: new Set(['codex', 'claude', 'gemini', 'custom', 'kiro-cli', 'copilot']),
127
132
  NullWorkerProvider: class NullWorkerProvider {
128
133
  selection: unknown;
129
134
 
@@ -133,6 +138,37 @@ vi.mock('../src/providers/providers.js', () => ({
133
138
  },
134
139
  }));
135
140
 
141
+ vi.mock('../src/providers/worker-provider-factory.js', () => ({
142
+ DefaultWorkerProviderFactory: class DefaultWorkerProviderFactory {
143
+ create(input: { selection: unknown }) {
144
+ return {
145
+ mode: 'live',
146
+ selection: input.selection,
147
+ async createSession() {
148
+ return { session_id: 'mock-session' };
149
+ },
150
+ async reattachSession() {
151
+ return null;
152
+ },
153
+ async closeSession() {
154
+ return { closed: true };
155
+ },
156
+ async runWorker() {
157
+ return { outputs: [{ type: 'NOTE', content: 'mock' }] };
158
+ },
159
+ };
160
+ }
161
+ },
162
+ resolveWorkerProviderMode: vi.fn((_cliMode, _configuredMode, fallback) => fallback ?? 'live'),
163
+ resolveWorkerProviderPolicy: vi.fn(() => ({ require_live_provider_for_run: true })),
164
+ resolveWorkerProviderRuntime: vi.fn(() => ({
165
+ worker_response_timeout_ms: 120000,
166
+ max_consecutive_no_progress_iterations: 2,
167
+ })),
168
+ resolveMalformedWorkerOutputAction: vi.fn(() => 'block_feature'),
169
+ resolveNoProgressAction: vi.fn(() => 'block_feature'),
170
+ }));
171
+
136
172
  vi.mock('../src/supervisor/runtime.js', () => {
137
173
  class SupervisorRuntime {
138
174
  private readonly options: unknown;
@@ -190,7 +190,7 @@ describe('dashboard api integration', () => {
190
190
  method: 'POST',
191
191
  body: JSON.stringify({ action: 'checkout', stash_changes: false }),
192
192
  }),
193
- { params: { id: 'feature_checkout' } },
193
+ { params: Promise.resolve({ id: 'feature_checkout' }) },
194
194
  );
195
195
  const checkoutBody = (await checkoutResponse.json()) as {
196
196
  ok: boolean;
@@ -208,7 +208,7 @@ describe('dashboard api integration', () => {
208
208
  method: 'POST',
209
209
  body: JSON.stringify({ action: 'restore' }),
210
210
  }),
211
- { params: { id: 'feature_checkout' } },
211
+ { params: Promise.resolve({ id: 'feature_checkout' }) },
212
212
  );
213
213
  const restoreBody = (await restoreResponse.json()) as {
214
214
  ok: boolean;
@@ -2,10 +2,11 @@ import { describe, it, expect, vi } from 'vitest';
2
2
 
3
3
  vi.mock('../src/providers/providers.js', () => ({
4
4
  resolveProviderSelection: vi.fn(() => ({
5
- provider: 'null',
5
+ provider: 'custom',
6
6
  model: 'none',
7
7
  provider_config_ref: null,
8
8
  })),
9
+ SUPPORTED_PROVIDERS: new Set(['codex', 'claude', 'gemini', 'custom', 'kiro-cli', 'copilot']),
9
10
  NullWorkerProvider: class NullWorkerProvider {
10
11
  selection: unknown;
11
12
 
@@ -27,6 +28,35 @@ vi.mock('../src/providers/providers.js', () => ({
27
28
  },
28
29
  }));
29
30
 
31
+ vi.mock('../src/providers/worker-provider-factory.js', () => ({
32
+ DefaultWorkerProviderFactory: class DefaultWorkerProviderFactory {
33
+ create(input: { selection: unknown }) {
34
+ return {
35
+ mode: 'live',
36
+ selection: input.selection,
37
+ async createSession() {
38
+ return { session_id: 'mock-session' };
39
+ },
40
+ async reattachSession() {
41
+ return null;
42
+ },
43
+ async closeSession() {
44
+ return { closed: true };
45
+ },
46
+ async runWorker() {
47
+ return { outputs: [{ type: 'NOTE', content: 'mock' }] };
48
+ },
49
+ };
50
+ }
51
+ },
52
+ resolveWorkerProviderMode: vi.fn(() => 'live'),
53
+ resolveWorkerProviderPolicy: vi.fn(() => ({ require_live_provider_for_run: true })),
54
+ resolveWorkerProviderRuntime: vi.fn(() => ({
55
+ worker_response_timeout_ms: 120000,
56
+ max_consecutive_no_progress_iterations: 2,
57
+ })),
58
+ }));
59
+
30
60
  vi.mock('../src/supervisor/runtime.js', () => ({
31
61
  SupervisorRuntime: class SupervisorRuntime {
32
62
  async start(features: unknown[]) {
@@ -41,6 +41,9 @@ describe('WorkerDecisionLoop', () => {
41
41
  patchApplied: false,
42
42
  noteLogged: false,
43
43
  requestHandled: false,
44
+ invalidOutput: false,
45
+ noProgress: false,
46
+ outputTypes: [],
44
47
  priorityOrder: [],
45
48
  toolResults: [],
46
49
  });