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.
- package/AGENTS.md +139 -0
- package/CLAUDE.md +12 -0
- package/agentic/orchestrator/agents.yaml +3 -0
- package/agentic/orchestrator/defaults/policy.defaults.yaml +3 -0
- package/agentic/orchestrator/policy.yaml +3 -0
- package/agentic/orchestrator/schemas/agents.schema.json +15 -0
- package/agentic/orchestrator/schemas/policy.schema.json +14 -0
- package/apps/control-plane/src/cli/cli-argument-parser.ts +7 -0
- package/apps/control-plane/src/cli/help-command-handler.ts +8 -0
- package/apps/control-plane/src/cli/init-command-handler.ts +6 -0
- package/apps/control-plane/src/cli/resume-command-handler.ts +31 -2
- package/apps/control-plane/src/cli/run-command-handler.ts +31 -3
- package/apps/control-plane/src/cli/types.ts +1 -0
- package/apps/control-plane/src/core/error-codes.ts +4 -0
- package/apps/control-plane/src/core/kernel.ts +3 -0
- package/apps/control-plane/src/index.ts +14 -0
- package/apps/control-plane/src/interfaces/cli/bootstrap.ts +25 -3
- package/apps/control-plane/src/providers/api-worker-provider.ts +115 -0
- package/apps/control-plane/src/providers/cli-worker-provider.ts +385 -0
- package/apps/control-plane/src/providers/output-parsers/generic-output-parser.ts +100 -0
- package/apps/control-plane/src/providers/output-parsers/index.ts +11 -0
- package/apps/control-plane/src/providers/output-parsers/types.ts +23 -0
- package/apps/control-plane/src/providers/providers.ts +19 -0
- package/apps/control-plane/src/providers/worker-provider-factory.ts +198 -0
- package/apps/control-plane/src/supervisor/build-wave-executor.ts +140 -3
- package/apps/control-plane/src/supervisor/planning-wave-executor.ts +125 -5
- package/apps/control-plane/src/supervisor/qa-wave-executor.ts +144 -2
- package/apps/control-plane/src/supervisor/runtime.ts +24 -0
- package/apps/control-plane/src/supervisor/worker-decision-loop.ts +134 -12
- package/apps/control-plane/test/cli.unit.spec.ts +36 -0
- package/apps/control-plane/test/dashboard-api.integration.spec.ts +2 -2
- package/apps/control-plane/test/resume-command.spec.ts +31 -1
- package/apps/control-plane/test/worker-decision-loop.spec.ts +3 -0
- package/apps/control-plane/test/worker-execution-policy.spec.ts +284 -0
- package/apps/control-plane/test/worker-provider-adapters.spec.ts +440 -0
- package/apps/control-plane/test/worker-provider-factory.spec.ts +151 -0
- package/config/agentic/orchestrator/agents.yaml +3 -0
- package/dist/apps/control-plane/cli/cli-argument-parser.js +7 -0
- package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
- package/dist/apps/control-plane/cli/help-command-handler.js +8 -0
- package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/init-command-handler.js +6 -0
- package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/resume-command-handler.d.ts +3 -0
- package/dist/apps/control-plane/cli/resume-command-handler.js +18 -2
- package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/run-command-handler.d.ts +3 -1
- package/dist/apps/control-plane/cli/run-command-handler.js +17 -3
- package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/types.d.ts +1 -0
- package/dist/apps/control-plane/core/error-codes.d.ts +4 -0
- package/dist/apps/control-plane/core/error-codes.js +4 -0
- package/dist/apps/control-plane/core/error-codes.js.map +1 -1
- package/dist/apps/control-plane/core/kernel.d.ts +3 -0
- package/dist/apps/control-plane/core/kernel.js.map +1 -1
- package/dist/apps/control-plane/index.d.ts +2 -0
- package/dist/apps/control-plane/index.js +1 -0
- package/dist/apps/control-plane/index.js.map +1 -1
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js +14 -2
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
- package/dist/apps/control-plane/providers/api-worker-provider.d.ts +31 -0
- package/dist/apps/control-plane/providers/api-worker-provider.js +73 -0
- package/dist/apps/control-plane/providers/api-worker-provider.js.map +1 -0
- package/dist/apps/control-plane/providers/cli-worker-provider.d.ts +46 -0
- package/dist/apps/control-plane/providers/cli-worker-provider.js +274 -0
- package/dist/apps/control-plane/providers/cli-worker-provider.js.map +1 -0
- package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.d.ts +10 -0
- package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.js +79 -0
- package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.js.map +1 -0
- package/dist/apps/control-plane/providers/output-parsers/index.d.ts +2 -0
- package/dist/apps/control-plane/providers/output-parsers/index.js +2 -0
- package/dist/apps/control-plane/providers/output-parsers/index.js.map +1 -0
- package/dist/apps/control-plane/providers/output-parsers/types.d.ts +21 -0
- package/dist/apps/control-plane/providers/output-parsers/types.js +2 -0
- package/dist/apps/control-plane/providers/output-parsers/types.js.map +1 -0
- package/dist/apps/control-plane/providers/providers.d.ts +4 -0
- package/dist/apps/control-plane/providers/providers.js +15 -0
- package/dist/apps/control-plane/providers/providers.js.map +1 -1
- package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +41 -0
- package/dist/apps/control-plane/providers/worker-provider-factory.js +104 -0
- package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -0
- package/dist/apps/control-plane/supervisor/build-wave-executor.d.ts +13 -0
- package/dist/apps/control-plane/supervisor/build-wave-executor.js +92 -3
- package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/planning-wave-executor.d.ts +12 -0
- package/dist/apps/control-plane/supervisor/planning-wave-executor.js +83 -5
- package/dist/apps/control-plane/supervisor/planning-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/qa-wave-executor.d.ts +13 -0
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js +91 -2
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/runtime.js +19 -0
- package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
- package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +10 -0
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js +113 -12
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
- package/package.json +2 -2
- package/packages/web-dashboard/next-env.d.ts +2 -1
- package/packages/web-dashboard/src/app/api/features/[id]/checkout/route.ts +4 -3
- package/packages/web-dashboard/src/app/api/features/[id]/diff/route.ts +6 -2
- package/packages/web-dashboard/src/app/api/features/[id]/evidence/[artifact]/route.ts +6 -5
- package/packages/web-dashboard/src/app/api/features/[id]/review/route.ts +5 -4
- package/packages/web-dashboard/src/app/api/features/[id]/route.ts +7 -3
- package/packages/web-dashboard/src/lib/aop-client.ts +2 -2
- package/packages/web-dashboard/src/lib/orchestrator-tools.ts +1 -1
- package/packages/web-dashboard/tsconfig.json +1 -0
- package/spec-files/outstanding/agentic_orchestrator_human_input_interaction_protocol_spec.md +590 -0
- package/spec-files/outstanding/agentic_orchestrator_real_worker_provider_execution_spec.md +616 -0
- 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
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
}
|