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
@@ -0,0 +1,198 @@
1
+ import { ERROR_CODES } from '../core/error-codes.js';
2
+ import { ApiWorkerProvider } from './api-worker-provider.js';
3
+ import { CliWorkerProvider } from './cli-worker-provider.js';
4
+ import type {
5
+ AppError,
6
+ ProviderCommandRunner,
7
+ ProviderSelection,
8
+ WorkerProvider,
9
+ } from './providers.js';
10
+ import { SUPPORTED_PROVIDERS, NullWorkerProvider } from './providers.js';
11
+ import {
12
+ ClaudeOutputParser,
13
+ CodexOutputParser,
14
+ GeminiOutputParser,
15
+ GenericCliOutputParser,
16
+ } from './output-parsers/index.js';
17
+
18
+ export type WorkerProviderMode = 'live' | 'stub';
19
+ export type ProviderCommandContext = 'run' | 'resume' | 'send' | 'attach';
20
+
21
+ export type WorkerMalformedOutputAction = 'block_feature' | 'fail_run';
22
+ export type WorkerNoProgressAction = 'block_feature' | 'fail_run';
23
+
24
+ export interface WorkerProviderFactoryPolicy {
25
+ require_live_provider_for_run: boolean;
26
+ }
27
+
28
+ export interface WorkerProviderFactoryRuntime {
29
+ worker_response_timeout_ms: number;
30
+ max_consecutive_no_progress_iterations: number;
31
+ }
32
+
33
+ export interface CreateWorkerProviderInput {
34
+ selection: ProviderSelection;
35
+ mode: WorkerProviderMode;
36
+ context: ProviderCommandContext;
37
+ policy: WorkerProviderFactoryPolicy;
38
+ runtime: WorkerProviderFactoryRuntime;
39
+ commandRunner?: ProviderCommandRunner;
40
+ }
41
+
42
+ export interface WorkerProviderFactory {
43
+ create(input: CreateWorkerProviderInput): WorkerProvider;
44
+ }
45
+
46
+ interface RuntimeConfigInput {
47
+ worker_response_timeout_ms?: unknown;
48
+ max_consecutive_no_progress_iterations?: unknown;
49
+ }
50
+
51
+ interface PolicyConfigInput {
52
+ require_live_provider_for_run?: unknown;
53
+ malformed_worker_output_action?: unknown;
54
+ no_progress_action?: unknown;
55
+ }
56
+
57
+ function asPositiveInteger(value: unknown, fallback: number): number {
58
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 1) {
59
+ return fallback;
60
+ }
61
+ return Math.floor(value);
62
+ }
63
+
64
+ function toAppError(code: string, message: string, details: Record<string, unknown>): AppError {
65
+ const error = new Error(message) as AppError;
66
+ error.code = code;
67
+ error.details = details;
68
+ return error;
69
+ }
70
+
71
+ function normalizeMode(value: unknown): WorkerProviderMode | null {
72
+ if (value === 'live' || value === 'stub') {
73
+ return value;
74
+ }
75
+ return null;
76
+ }
77
+
78
+ export function resolveWorkerProviderMode(
79
+ cliMode: unknown,
80
+ configuredMode: unknown,
81
+ fallback: WorkerProviderMode,
82
+ ): WorkerProviderMode {
83
+ const explicitCliMode = normalizeMode(cliMode);
84
+ if (explicitCliMode) {
85
+ return explicitCliMode;
86
+ }
87
+
88
+ const explicitConfiguredMode = normalizeMode(configuredMode);
89
+ if (explicitConfiguredMode) {
90
+ return explicitConfiguredMode;
91
+ }
92
+
93
+ return fallback;
94
+ }
95
+
96
+ export function resolveWorkerProviderRuntime(
97
+ input: RuntimeConfigInput | null | undefined,
98
+ ): WorkerProviderFactoryRuntime {
99
+ return {
100
+ worker_response_timeout_ms: asPositiveInteger(input?.worker_response_timeout_ms, 120_000),
101
+ max_consecutive_no_progress_iterations: asPositiveInteger(
102
+ input?.max_consecutive_no_progress_iterations,
103
+ 2,
104
+ ),
105
+ };
106
+ }
107
+
108
+ export function resolveWorkerProviderPolicy(
109
+ input: PolicyConfigInput | null | undefined,
110
+ ): WorkerProviderFactoryPolicy {
111
+ return {
112
+ require_live_provider_for_run: input?.require_live_provider_for_run !== false,
113
+ };
114
+ }
115
+
116
+ export function resolveMalformedWorkerOutputAction(
117
+ input: PolicyConfigInput | null | undefined,
118
+ ): WorkerMalformedOutputAction {
119
+ return input?.malformed_worker_output_action === 'fail_run' ? 'fail_run' : 'block_feature';
120
+ }
121
+
122
+ export function resolveNoProgressAction(
123
+ input: PolicyConfigInput | null | undefined,
124
+ ): WorkerNoProgressAction {
125
+ return input?.no_progress_action === 'fail_run' ? 'fail_run' : 'block_feature';
126
+ }
127
+
128
+ export class DefaultWorkerProviderFactory implements WorkerProviderFactory {
129
+ create(input: CreateWorkerProviderInput): WorkerProvider {
130
+ const { selection, mode, context, policy, runtime, commandRunner } = input;
131
+
132
+ if (!SUPPORTED_PROVIDERS.has(selection.provider)) {
133
+ throw toAppError(ERROR_CODES.UNSUPPORTED_AGENT_PROVIDER, 'Unsupported worker provider', {
134
+ provider: selection.provider,
135
+ });
136
+ }
137
+
138
+ if (mode === 'stub') {
139
+ if (
140
+ (context === 'run' || context === 'resume') &&
141
+ policy.require_live_provider_for_run === true
142
+ ) {
143
+ throw toAppError(
144
+ ERROR_CODES.PROVIDER_STUB_DISALLOWED,
145
+ 'Stub worker provider is disallowed for run/resume',
146
+ {
147
+ provider: selection.provider,
148
+ context,
149
+ },
150
+ );
151
+ }
152
+ return new NullWorkerProvider(selection, { commandRunner });
153
+ }
154
+
155
+ if (selection.provider === 'gemini') {
156
+ return new ApiWorkerProvider(selection, {
157
+ outputParser: new GeminiOutputParser(),
158
+ workerResponseTimeoutMs: runtime.worker_response_timeout_ms,
159
+ });
160
+ }
161
+
162
+ if (selection.provider === 'codex') {
163
+ return new CliWorkerProvider(selection, {
164
+ outputParser: new CodexOutputParser(),
165
+ commandRunner,
166
+ workerResponseTimeoutMs: runtime.worker_response_timeout_ms,
167
+ });
168
+ }
169
+
170
+ if (selection.provider === 'claude') {
171
+ return new CliWorkerProvider(selection, {
172
+ outputParser: new ClaudeOutputParser(),
173
+ commandRunner,
174
+ workerResponseTimeoutMs: runtime.worker_response_timeout_ms,
175
+ });
176
+ }
177
+
178
+ if (
179
+ selection.provider === 'kiro-cli' ||
180
+ selection.provider === 'copilot' ||
181
+ selection.provider === 'custom'
182
+ ) {
183
+ return new CliWorkerProvider(selection, {
184
+ outputParser: new GenericCliOutputParser(),
185
+ commandRunner,
186
+ workerResponseTimeoutMs: runtime.worker_response_timeout_ms,
187
+ });
188
+ }
189
+
190
+ throw toAppError(
191
+ ERROR_CODES.PROVIDER_RUNTIME_UNAVAILABLE,
192
+ 'No live worker provider adapter is available for selection',
193
+ {
194
+ provider: selection.provider,
195
+ },
196
+ );
197
+ }
198
+ }
@@ -1,10 +1,16 @@
1
1
  import { GATE_RESULT, STATUS, TOOLS } from '../core/constants.js';
2
+ import { ERROR_CODES } from '../core/error-codes.js';
2
3
  import type { FeatureStatePayload, SupervisorToolCaller } from './types.js';
3
4
  import { NOOP_WORKER_DECISION_RUNNER, type WorkerDecisionRunner } from './worker-decision-loop.js';
4
5
  import type {
5
6
  GateRepairContext,
6
7
  ReactionsService,
7
8
  } from '../application/services/reactions-service.js';
9
+ import type {
10
+ WorkerMalformedOutputAction,
11
+ WorkerNoProgressAction,
12
+ WorkerProviderMode,
13
+ } from '../providers/worker-provider-factory.js';
8
14
 
9
15
  interface GatesRunData {
10
16
  overall?: string;
@@ -15,6 +21,10 @@ interface BuildWaveExecutorDependencies {
15
21
  toolCaller: SupervisorToolCaller;
16
22
  workerDecisionRunner?: WorkerDecisionRunner;
17
23
  reactionsService?: ReactionsService;
24
+ providerMode?: WorkerProviderMode;
25
+ noProgressLimit?: number;
26
+ noProgressAction?: WorkerNoProgressAction;
27
+ malformedOutputAction?: WorkerMalformedOutputAction;
18
28
  }
19
29
 
20
30
  function isRetryableToolError(error: unknown): boolean {
@@ -28,18 +38,39 @@ function isRetryableToolError(error: unknown): boolean {
28
38
  return (details as { retryable?: unknown }).retryable === true;
29
39
  }
30
40
 
41
+ function asVersion(value: unknown): number | null {
42
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
43
+ return null;
44
+ }
45
+ return Math.floor(value);
46
+ }
47
+
31
48
  export class BuildWaveExecutor {
32
49
  private readonly toolCaller: SupervisorToolCaller;
33
50
  private readonly workerDecisionRunner: WorkerDecisionRunner;
34
51
  private readonly reactionsService: ReactionsService | undefined;
52
+ private readonly providerMode: WorkerProviderMode;
53
+ private readonly noProgressLimit: number;
54
+ private readonly noProgressAction: WorkerNoProgressAction;
55
+ private readonly malformedOutputAction: WorkerMalformedOutputAction;
56
+ private readonly noProgressByFeature = new Map<string, number>();
57
+ private readonly blockedForCurrentCycle = new Set<string>();
35
58
 
36
59
  constructor(dependencies: BuildWaveExecutorDependencies) {
37
60
  this.toolCaller = dependencies.toolCaller;
38
61
  this.workerDecisionRunner = dependencies.workerDecisionRunner ?? NOOP_WORKER_DECISION_RUNNER;
39
62
  this.reactionsService = dependencies.reactionsService;
63
+ this.providerMode = dependencies.providerMode ?? 'stub';
64
+ this.noProgressLimit =
65
+ typeof dependencies.noProgressLimit === 'number' && dependencies.noProgressLimit > 0
66
+ ? Math.floor(dependencies.noProgressLimit)
67
+ : 2;
68
+ this.noProgressAction = dependencies.noProgressAction ?? 'block_feature';
69
+ this.malformedOutputAction = dependencies.malformedOutputAction ?? 'block_feature';
40
70
  }
41
71
 
42
72
  async run(featureIds: string[], maxParallelGateRuns: number): Promise<void> {
73
+ this.blockedForCurrentCycle.clear();
43
74
  const batch: string[] = [];
44
75
  for (const featureId of featureIds) {
45
76
  const state = await this.toolCaller.callTool<FeatureStatePayload>(
@@ -60,16 +91,23 @@ export class BuildWaveExecutor {
60
91
  const context = await this.toolCaller.callTool('builder', TOOLS.FEATURE_GET_CONTEXT, {
61
92
  feature_id: featureId,
62
93
  });
63
- await this.workerDecisionRunner.execute({
94
+ const decision = await this.workerDecisionRunner.execute({
64
95
  role: 'builder',
65
96
  featureId,
66
97
  contextBundle: context.data,
67
98
  instructions:
68
99
  'Emit PATCH outputs for code changes, NOTE outputs for progress context, and REQUEST outputs for lock/context needs.',
69
100
  });
101
+ const shouldSkip = await this.enforceWorkerExecutionPolicy(featureId, decision);
102
+ if (!shouldSkip) {
103
+ this.noProgressByFeature.delete(featureId);
104
+ }
70
105
  }
71
106
 
72
- const executing = selected.map(async (featureId) => {
107
+ const gateCandidates = selected.filter(
108
+ (featureId) => !this.blockedForCurrentCycle.has(featureId),
109
+ );
110
+ const executing = gateCandidates.map(async (featureId) => {
73
111
  const stateForRetry = await this.toolCaller.callTool<FeatureStatePayload>(
74
112
  'builder',
75
113
  TOOLS.FEATURE_STATE_GET,
@@ -140,12 +178,17 @@ export class BuildWaveExecutor {
140
178
  failureHistory,
141
179
  };
142
180
  const repairPrompt = this.reactionsService.buildRepairPrompt(ctx);
143
- await this.workerDecisionRunner.execute({
181
+ const repairDecision = await this.workerDecisionRunner.execute({
144
182
  role: 'builder',
145
183
  featureId,
146
184
  contextBundle: context.data,
147
185
  instructions: repairPrompt,
148
186
  });
187
+ const shouldSkip = await this.enforceWorkerExecutionPolicy(featureId, repairDecision);
188
+ if (shouldSkip) {
189
+ break;
190
+ }
191
+ this.noProgressByFeature.delete(featureId);
149
192
 
150
193
  try {
151
194
  const retryResult = await this.toolCaller.callTool<GatesRunData>(
@@ -209,4 +252,98 @@ export class BuildWaveExecutor {
209
252
 
210
253
  await Promise.allSettled(executing);
211
254
  }
255
+
256
+ private async enforceWorkerExecutionPolicy(
257
+ featureId: string,
258
+ decision: {
259
+ invalidOutput: boolean;
260
+ noProgress: boolean;
261
+ },
262
+ ): Promise<boolean> {
263
+ if (this.providerMode !== 'live') {
264
+ this.noProgressByFeature.delete(featureId);
265
+ return false;
266
+ }
267
+
268
+ if (decision.invalidOutput) {
269
+ await this.applyWorkerPolicyAction(
270
+ featureId,
271
+ this.malformedOutputAction,
272
+ ERROR_CODES.PROVIDER_OUTPUT_INVALID,
273
+ 'Builder emitted malformed worker outputs',
274
+ );
275
+ this.blockedForCurrentCycle.add(featureId);
276
+ return true;
277
+ }
278
+
279
+ if (!decision.noProgress) {
280
+ this.noProgressByFeature.delete(featureId);
281
+ return false;
282
+ }
283
+
284
+ const currentCount = (this.noProgressByFeature.get(featureId) ?? 0) + 1;
285
+ this.noProgressByFeature.set(featureId, currentCount);
286
+ this.blockedForCurrentCycle.add(featureId);
287
+
288
+ if (currentCount < this.noProgressLimit) {
289
+ return true;
290
+ }
291
+
292
+ await this.applyWorkerPolicyAction(
293
+ featureId,
294
+ this.noProgressAction,
295
+ ERROR_CODES.PROVIDER_NO_PROGRESS,
296
+ `Builder made no actionable progress in ${currentCount} consecutive iterations`,
297
+ );
298
+ return true;
299
+ }
300
+
301
+ private async applyWorkerPolicyAction(
302
+ featureId: string,
303
+ action: WorkerNoProgressAction | WorkerMalformedOutputAction,
304
+ errorCode: string,
305
+ message: string,
306
+ ): Promise<void> {
307
+ if (action === 'fail_run') {
308
+ const error = new Error(message) as Error & {
309
+ code?: string;
310
+ details?: Record<string, unknown>;
311
+ };
312
+ error.code = errorCode;
313
+ error.details = {
314
+ feature_id: featureId,
315
+ retryable: false,
316
+ requires_human: true,
317
+ };
318
+ throw error;
319
+ }
320
+
321
+ const state = await this.toolCaller.callTool<FeatureStatePayload>(
322
+ 'orchestrator',
323
+ TOOLS.FEATURE_STATE_GET,
324
+ {
325
+ feature_id: featureId,
326
+ },
327
+ );
328
+ const expectedVersion = asVersion(state.data.front_matter.version);
329
+ await this.toolCaller.callTool('orchestrator', TOOLS.FEATURE_STATE_PATCH, {
330
+ feature_id: featureId,
331
+ expected_version: expectedVersion,
332
+ patch: {
333
+ front_matter: {
334
+ status: STATUS.BLOCKED,
335
+ status_reason: `${errorCode}: ${message}`,
336
+ },
337
+ },
338
+ });
339
+ await this.toolCaller.callTool('orchestrator', TOOLS.FEATURE_LOG_APPEND, {
340
+ feature_id: featureId,
341
+ note: JSON.stringify({
342
+ phase: 'building',
343
+ decision: 'blocked',
344
+ error_code: errorCode,
345
+ reason: message,
346
+ }),
347
+ });
348
+ }
212
349
  }
@@ -1,6 +1,12 @@
1
1
  import { STATUS, TOOLS } from '../core/constants.js';
2
+ import { ERROR_CODES } from '../core/error-codes.js';
2
3
  import type { FeatureContextPayload, InitialPlanGenerator, SupervisorToolCaller } from './types.js';
3
4
  import { NOOP_WORKER_DECISION_RUNNER, type WorkerDecisionRunner } from './worker-decision-loop.js';
5
+ import type {
6
+ WorkerMalformedOutputAction,
7
+ WorkerNoProgressAction,
8
+ WorkerProviderMode,
9
+ } from '../providers/worker-provider-factory.js';
4
10
 
5
11
  type AnyRecord = Record<string, unknown>;
6
12
 
@@ -8,6 +14,10 @@ interface PlanningWaveExecutorDependencies {
8
14
  toolCaller: SupervisorToolCaller;
9
15
  planGenerator: InitialPlanGenerator;
10
16
  workerDecisionRunner?: WorkerDecisionRunner;
17
+ providerMode?: WorkerProviderMode;
18
+ noProgressLimit?: number;
19
+ noProgressAction?: WorkerNoProgressAction;
20
+ malformedOutputAction?: WorkerMalformedOutputAction;
11
21
  }
12
22
 
13
23
  interface ReconciliationDecision {
@@ -110,11 +120,23 @@ export class PlanningWaveExecutor {
110
120
  private readonly toolCaller: SupervisorToolCaller;
111
121
  private readonly planGenerator: InitialPlanGenerator;
112
122
  private readonly workerDecisionRunner: WorkerDecisionRunner;
123
+ private readonly providerMode: WorkerProviderMode;
124
+ private readonly noProgressLimit: number;
125
+ private readonly noProgressAction: WorkerNoProgressAction;
126
+ private readonly malformedOutputAction: WorkerMalformedOutputAction;
127
+ private readonly noProgressByFeature = new Map<string, number>();
113
128
 
114
129
  constructor(dependencies: PlanningWaveExecutorDependencies) {
115
130
  this.toolCaller = dependencies.toolCaller;
116
131
  this.planGenerator = dependencies.planGenerator;
117
132
  this.workerDecisionRunner = dependencies.workerDecisionRunner ?? NOOP_WORKER_DECISION_RUNNER;
133
+ this.providerMode = dependencies.providerMode ?? 'stub';
134
+ this.noProgressLimit =
135
+ typeof dependencies.noProgressLimit === 'number' && dependencies.noProgressLimit > 0
136
+ ? Math.floor(dependencies.noProgressLimit)
137
+ : 2;
138
+ this.noProgressAction = dependencies.noProgressAction ?? 'block_feature';
139
+ this.malformedOutputAction = dependencies.malformedOutputAction ?? 'block_feature';
118
140
  }
119
141
 
120
142
  async run(featureIds: string[]): Promise<void> {
@@ -141,15 +163,22 @@ export class PlanningWaveExecutor {
141
163
  'Review feature context and emit PLAN_SUBMISSION/REQUEST/NOTE outputs to advance planning deterministically.',
142
164
  });
143
165
 
166
+ const shouldSkip = await this.enforceWorkerExecutionPolicy(featureId, decision);
167
+ if (shouldSkip) {
168
+ continue;
169
+ }
170
+
144
171
  if (decision.planSubmission || existingPlan) {
145
172
  continue;
146
173
  }
147
174
 
148
- const plan = await this.planGenerator.generateInitialPlan(featureId);
149
- await this.toolCaller.callTool('planner', TOOLS.PLAN_SUBMIT, {
150
- feature_id: featureId,
151
- plan_json: plan,
152
- });
175
+ if (this.providerMode === 'stub') {
176
+ const plan = await this.planGenerator.generateInitialPlan(featureId);
177
+ await this.toolCaller.callTool('planner', TOOLS.PLAN_SUBMIT, {
178
+ feature_id: featureId,
179
+ plan_json: plan,
180
+ });
181
+ }
153
182
  }
154
183
  }
155
184
 
@@ -326,4 +355,95 @@ export class PlanningWaveExecutor {
326
355
  note: JSON.stringify(note),
327
356
  });
328
357
  }
358
+
359
+ private async enforceWorkerExecutionPolicy(
360
+ featureId: string,
361
+ decision: {
362
+ invalidOutput: boolean;
363
+ noProgress: boolean;
364
+ },
365
+ ): Promise<boolean> {
366
+ if (this.providerMode !== 'live') {
367
+ this.noProgressByFeature.delete(featureId);
368
+ return false;
369
+ }
370
+
371
+ if (decision.invalidOutput) {
372
+ await this.applyWorkerPolicyAction(
373
+ featureId,
374
+ this.malformedOutputAction,
375
+ ERROR_CODES.PROVIDER_OUTPUT_INVALID,
376
+ 'Planner emitted malformed worker outputs',
377
+ );
378
+ return true;
379
+ }
380
+
381
+ if (!decision.noProgress) {
382
+ this.noProgressByFeature.delete(featureId);
383
+ return false;
384
+ }
385
+
386
+ const currentCount = (this.noProgressByFeature.get(featureId) ?? 0) + 1;
387
+ this.noProgressByFeature.set(featureId, currentCount);
388
+ if (currentCount < this.noProgressLimit) {
389
+ return true;
390
+ }
391
+
392
+ await this.applyWorkerPolicyAction(
393
+ featureId,
394
+ this.noProgressAction,
395
+ ERROR_CODES.PROVIDER_NO_PROGRESS,
396
+ `Planner made no actionable progress in ${currentCount} consecutive iterations`,
397
+ );
398
+ return true;
399
+ }
400
+
401
+ private async applyWorkerPolicyAction(
402
+ featureId: string,
403
+ action: WorkerNoProgressAction | WorkerMalformedOutputAction,
404
+ errorCode: string,
405
+ message: string,
406
+ ): Promise<void> {
407
+ if (action === 'fail_run') {
408
+ const error = new Error(message) as Error & {
409
+ code?: string;
410
+ details?: Record<string, unknown>;
411
+ };
412
+ error.code = errorCode;
413
+ error.details = {
414
+ feature_id: featureId,
415
+ retryable: false,
416
+ requires_human: true,
417
+ };
418
+ throw error;
419
+ }
420
+
421
+ const state = await this.toolCaller.callTool('orchestrator', TOOLS.FEATURE_STATE_GET, {
422
+ feature_id: featureId,
423
+ });
424
+ const frontMatter = asRecord(state.data.front_matter);
425
+ const expectedVersion =
426
+ typeof frontMatter.version === 'number' && Number.isFinite(frontMatter.version)
427
+ ? Math.floor(frontMatter.version)
428
+ : null;
429
+ await this.toolCaller.callTool('orchestrator', TOOLS.FEATURE_STATE_PATCH, {
430
+ feature_id: featureId,
431
+ expected_version: expectedVersion,
432
+ patch: {
433
+ front_matter: {
434
+ status: STATUS.BLOCKED,
435
+ status_reason: `${errorCode}: ${message}`,
436
+ },
437
+ },
438
+ });
439
+ await this.toolCaller.callTool('orchestrator', TOOLS.FEATURE_LOG_APPEND, {
440
+ feature_id: featureId,
441
+ note: JSON.stringify({
442
+ phase: 'planning',
443
+ decision: 'blocked',
444
+ error_code: errorCode,
445
+ reason: message,
446
+ }),
447
+ });
448
+ }
329
449
  }