ai-maestro 1.0.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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +15 -0
- package/README.md +727 -0
- package/bin/maestro.mjs +246 -0
- package/package.json +29 -0
- package/src/checkpoint.mjs +102 -0
- package/src/codex-diagnostics.mjs +682 -0
- package/src/codex-home-inspect.mjs +252 -0
- package/src/codex-home.mjs +258 -0
- package/src/commands.mjs +809 -0
- package/src/config.mjs +11 -0
- package/src/debug.mjs +164 -0
- package/src/encoding-guard.mjs +125 -0
- package/src/engines/ai-router-engine.mjs +37 -0
- package/src/engines/codex-engine.mjs +21 -0
- package/src/engines/engine.mjs +21 -0
- package/src/engines/registry.mjs +29 -0
- package/src/files.mjs +65 -0
- package/src/format.mjs +132 -0
- package/src/lock.mjs +439 -0
- package/src/memory-log.mjs +18 -0
- package/src/memory.mjs +1 -0
- package/src/orchestration/attempt-chain-runtime.mjs +627 -0
- package/src/orchestration/budget-manager.mjs +121 -0
- package/src/orchestration/capability-registry.mjs +108 -0
- package/src/orchestration/consolidator.mjs +772 -0
- package/src/orchestration/delegation-executor.mjs +262 -0
- package/src/orchestration/delegation-manager.mjs +116 -0
- package/src/orchestration/deployment-intent.mjs +36 -0
- package/src/orchestration/engine-history.mjs +110 -0
- package/src/orchestration/engine-policy.mjs +73 -0
- package/src/orchestration/engine-selector.mjs +187 -0
- package/src/orchestration/execution-context.mjs +45 -0
- package/src/orchestration/failure-classifier.mjs +136 -0
- package/src/orchestration/failure-evidence.mjs +237 -0
- package/src/orchestration/fallback-chain-lock.mjs +217 -0
- package/src/orchestration/fallback-chain-trail.mjs +218 -0
- package/src/orchestration/fallback-executor.mjs +146 -0
- package/src/orchestration/fallback-graph.mjs +106 -0
- package/src/orchestration/fallback-recommendation.mjs +73 -0
- package/src/orchestration/file-conflict-detector.mjs +126 -0
- package/src/orchestration/host-validation.mjs +241 -0
- package/src/orchestration/orchestration-loop.mjs +1971 -0
- package/src/orchestration/orchestration-runtime.mjs +1019 -0
- package/src/orchestration/orchestration-scheduler.mjs +135 -0
- package/src/orchestration/orchestration-trail.mjs +192 -0
- package/src/orchestration/preflight.mjs +212 -0
- package/src/orchestration/provider-health.mjs +566 -0
- package/src/orchestration/provider-router.mjs +352 -0
- package/src/orchestration/rc-check-adapters.mjs +817 -0
- package/src/orchestration/rc-check.mjs +544 -0
- package/src/orchestration/rc-policy.mjs +200 -0
- package/src/orchestration/runtime-gate.mjs +146 -0
- package/src/orchestration/sector-managers.mjs +215 -0
- package/src/orchestration/self-hosting-canary.mjs +231 -0
- package/src/orchestration/self-hosting-readiness.mjs +244 -0
- package/src/orchestration/spec-planner.mjs +877 -0
- package/src/orchestration/subtask-planner.mjs +176 -0
- package/src/orchestration/task-classifier.mjs +241 -0
- package/src/orchestration/verifier.mjs +1608 -0
- package/src/orchestration-commands.mjs +279 -0
- package/src/planner.mjs +38 -0
- package/src/project.mjs +1 -0
- package/src/run-diagnostics.mjs +641 -0
- package/src/runner.mjs +243 -0
- package/src/safety.mjs +116 -0
- package/src/schema.mjs +371 -0
- package/src/session-commands.mjs +521 -0
- package/src/shell.mjs +93 -0
- package/src/smart-planner.mjs +249 -0
- package/src/task-commands.mjs +1182 -0
- package/src/tasks.mjs +134 -0
- package/src/ui/commands.mjs +76 -0
- package/src/ui/events.mjs +45 -0
- package/src/ui/public/app.js +600 -0
- package/src/ui/public/index.html +88 -0
- package/src/ui/public/style.css +460 -0
- package/src/ui/server.mjs +112 -0
- package/src/ui/state.mjs +504 -0
- package/src/usage.mjs +178 -0
- package/src/workspace-diff.mjs +228 -0
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
import { runTask, withAccountingLock } from '../task-commands.mjs';
|
|
2
|
+
import { addTask, getTaskById, getTasks, updateTaskStatus } from '../tasks.mjs';
|
|
3
|
+
import { getLatestRunForTask } from '../runner.mjs';
|
|
4
|
+
import { ensureSafetyConfig } from '../safety.mjs';
|
|
5
|
+
import { redactCredentials, redactObject } from '../format.mjs';
|
|
6
|
+
import { evaluateTaskRun, countRunsForTask } from './runtime-gate.mjs';
|
|
7
|
+
import { buildDelegationPlan } from './delegation-manager.mjs';
|
|
8
|
+
import { runOrchestrationLoop } from './orchestration-loop.mjs';
|
|
9
|
+
import { consolidateResults } from './consolidator.mjs';
|
|
10
|
+
import { verifyConsolidatedResult } from './verifier.mjs';
|
|
11
|
+
import { appendOrchestrationEvent, getOrchestrationEvents } from './orchestration-trail.mjs';
|
|
12
|
+
import { computeWaves, planWaveExecution } from './orchestration-scheduler.mjs';
|
|
13
|
+
import { detectFileConflicts } from './file-conflict-detector.mjs';
|
|
14
|
+
|
|
15
|
+
const SCHEMA_VERSION = 1;
|
|
16
|
+
const DEFAULT_LIMITS = Object.freeze({ maxDepth: 2, maxSubtasks: 8, maxAgentsPerManager: 4, maxTransitions: 40 });
|
|
17
|
+
const TERMINAL_STATUSES = new Set(['completed', 'completed_with_warnings', 'failed', 'blocked', 'needs_human', 'archived']);
|
|
18
|
+
const ACCEPTABLE_DEPENDENCY_STATUSES = new Set(['completed', 'completed_with_warnings']);
|
|
19
|
+
const SUCCESS_PARENT_STATUSES = new Set(['accepted', 'accepted_with_warnings']);
|
|
20
|
+
const SUCCESS_VERIFICATION_OUTCOMES = new Set(['verified', 'verified_with_warnings']);
|
|
21
|
+
const runtimeChains = new Map();
|
|
22
|
+
|
|
23
|
+
function isPlainObject(value) {
|
|
24
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function cloneData(value) {
|
|
28
|
+
if (value === undefined) return undefined;
|
|
29
|
+
return JSON.parse(JSON.stringify(value));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeMode(mode) {
|
|
33
|
+
return mode === 'real' ? 'real' : 'dry-run';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeTaskId(taskId) {
|
|
37
|
+
if (taskId === undefined || taskId === null || String(taskId).trim().length === 0) {
|
|
38
|
+
throw new TypeError('taskId is required');
|
|
39
|
+
}
|
|
40
|
+
return String(taskId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function mergeDefined(base, extra) {
|
|
44
|
+
const next = { ...base };
|
|
45
|
+
for (const [key, value] of Object.entries(extra || {})) {
|
|
46
|
+
if (value !== undefined) next[key] = value;
|
|
47
|
+
}
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildResult({ taskId, mode, executed = false, plan = null, executionPlan = undefined, children = [], consolidation = null, verification = null, parentStatus = null, statusOverrideReason = null, trace = [], warnings = [], errors = [], summary = '' }) {
|
|
52
|
+
return redactObject({
|
|
53
|
+
schemaVersion: SCHEMA_VERSION,
|
|
54
|
+
taskId: String(taskId),
|
|
55
|
+
mode,
|
|
56
|
+
executed,
|
|
57
|
+
plan: cloneData(plan),
|
|
58
|
+
...(executionPlan !== undefined ? { executionPlan: cloneData(executionPlan) } : {}),
|
|
59
|
+
children: cloneData(children),
|
|
60
|
+
consolidation: cloneData(consolidation),
|
|
61
|
+
verification: cloneData(verification),
|
|
62
|
+
parentStatus,
|
|
63
|
+
statusOverrideReason,
|
|
64
|
+
trace: cloneData(trace),
|
|
65
|
+
warnings: [...warnings],
|
|
66
|
+
errors: errors.map(error => typeof error === 'string' ? error : (error && error.message) || String(error)),
|
|
67
|
+
summary
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function runSerialized(taskId, fn) {
|
|
72
|
+
const key = String(taskId);
|
|
73
|
+
const previous = runtimeChains.get(key) || Promise.resolve();
|
|
74
|
+
let release;
|
|
75
|
+
const current = new Promise(resolve => { release = resolve; });
|
|
76
|
+
runtimeChains.set(key, previous.then(() => current, () => current));
|
|
77
|
+
await previous.catch(() => {});
|
|
78
|
+
try {
|
|
79
|
+
return await fn();
|
|
80
|
+
} finally {
|
|
81
|
+
release();
|
|
82
|
+
if (runtimeChains.get(key) === current) {
|
|
83
|
+
runtimeChains.delete(key);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function planSerialOrchestration({ task, mode, force, rawInput }) {
|
|
89
|
+
const suppliedContext = cloneData(rawInput.context || {});
|
|
90
|
+
const suppliedModules = cloneData(rawInput.moduleResults || {});
|
|
91
|
+
let runtimeGate = suppliedModules.runtimeGate || suppliedContext.runtimeGateResult || null;
|
|
92
|
+
|
|
93
|
+
if (!runtimeGate) {
|
|
94
|
+
const runsSoFar = await countRunsForTask(task.id);
|
|
95
|
+
runtimeGate = await evaluateTaskRun({
|
|
96
|
+
task,
|
|
97
|
+
projectRoot: process.cwd(),
|
|
98
|
+
mode,
|
|
99
|
+
force,
|
|
100
|
+
runsSoFar
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const classification = suppliedModules.classification || runtimeGate.taskClassification || null;
|
|
105
|
+
const manager = suppliedModules.manager || runtimeGate.selectedManager || null;
|
|
106
|
+
const engineCandidate = suppliedModules.engineCandidate || runtimeGate.selectedEngine || null;
|
|
107
|
+
const preflight = suppliedModules.preflight || runtimeGate.preflight || null;
|
|
108
|
+
const budget = suppliedModules.budget || (runtimeGate.budget && runtimeGate.budget.evaluation) || runtimeGate.budget || null;
|
|
109
|
+
const policy = suppliedModules.policy || runtimeGate.policy || null;
|
|
110
|
+
|
|
111
|
+
const moduleResults = mergeDefined({
|
|
112
|
+
classification,
|
|
113
|
+
manager,
|
|
114
|
+
engineCandidate,
|
|
115
|
+
preflight,
|
|
116
|
+
budget,
|
|
117
|
+
policy,
|
|
118
|
+
runtimeGate
|
|
119
|
+
}, suppliedModules);
|
|
120
|
+
|
|
121
|
+
const context = {
|
|
122
|
+
...suppliedContext,
|
|
123
|
+
runtimeGateResult: runtimeGate,
|
|
124
|
+
acceptanceCriteria: suppliedContext.acceptanceCriteria || task.acceptance || []
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if ((runtimeGate.decision === 'DELEGATE' || (manager && manager.decision === 'decompose')) && !moduleResults.delegation) {
|
|
128
|
+
moduleResults.delegation = buildDelegationPlan(task, classification || {}, {
|
|
129
|
+
depth: Number(context.orchestrationDepth || 0)
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const plan = runOrchestrationLoop({
|
|
134
|
+
task: cloneData(task),
|
|
135
|
+
context,
|
|
136
|
+
limits: { ...DEFAULT_LIMITS, ...(rawInput.limits || {}) },
|
|
137
|
+
moduleResults,
|
|
138
|
+
adapters: rawInput.adapters || {}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return plan;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function hasBlockingConflict(plan) {
|
|
145
|
+
return Array.isArray(plan && plan.conflicts) && plan.conflicts.some(conflict => {
|
|
146
|
+
const target = String(conflict.file || conflict.path || '').replace(/\\/g, '/').toLowerCase();
|
|
147
|
+
return conflict.severity === 'critical' ||
|
|
148
|
+
conflict.type === 'parallel-write' ||
|
|
149
|
+
conflict.parallel === true ||
|
|
150
|
+
target === '.env' || target.endsWith('/.env') || target.startsWith('../') || target.includes('/../');
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeConcurrency(concurrency) {
|
|
155
|
+
if (!concurrency || concurrency.enabled !== true) {
|
|
156
|
+
return { enabled: false, strategy: 'serial', maxConcurrent: 1 };
|
|
157
|
+
}
|
|
158
|
+
const parsed = Number.parseInt(concurrency.maxConcurrent, 10);
|
|
159
|
+
const requested = Number.isFinite(parsed) ? parsed : 2;
|
|
160
|
+
return {
|
|
161
|
+
enabled: true,
|
|
162
|
+
strategy: 'controlled-parallel',
|
|
163
|
+
maxConcurrent: Math.max(1, Math.min(4, requested))
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function plannedSubtasks(plan) {
|
|
168
|
+
return Array.isArray(plan && plan.plannedSubtasks) ? cloneData(plan.plannedSubtasks) : [];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function subtaskId(subtask) {
|
|
172
|
+
return String(subtask && subtask.id);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function sortSubtasksById(subtasks) {
|
|
176
|
+
return [...subtasks].sort((a, b) => subtaskId(a).localeCompare(subtaskId(b)));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function auditWavePlan(waves, subtasks, concurrency) {
|
|
180
|
+
const byId = new Map(subtasks.map(subtask => [subtaskId(subtask), subtask]));
|
|
181
|
+
return waves.map(wave => {
|
|
182
|
+
const waveSubtasks = wave.subtaskIds.map(id => byId.get(String(id))).filter(Boolean);
|
|
183
|
+
const waveConflicts = detectFileConflicts(waveSubtasks, { projectRoot: process.cwd() });
|
|
184
|
+
const planned = planWaveExecution({ ...wave, subtasks: waveSubtasks }, subtasks, { waveConflicts, maxConcurrent: concurrency.maxConcurrent });
|
|
185
|
+
return {
|
|
186
|
+
waveIndex: wave.waveIndex,
|
|
187
|
+
parallelSubtaskIds: planned.parallel.map(subtaskId),
|
|
188
|
+
serialSubtaskIds: planned.serial.map(subtaskId)
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function dryRunExecutionPlan(plan, concurrencyInput) {
|
|
194
|
+
const concurrency = normalizeConcurrency(concurrencyInput);
|
|
195
|
+
if (!concurrency.enabled) return undefined;
|
|
196
|
+
const subtasks = plannedSubtasks(plan);
|
|
197
|
+
try {
|
|
198
|
+
const waves = computeWaves(subtasks);
|
|
199
|
+
return {
|
|
200
|
+
strategy: concurrency.strategy,
|
|
201
|
+
concurrency: { maxConcurrent: concurrency.maxConcurrent, observedMaxActive: 0 },
|
|
202
|
+
waves: auditWavePlan(waves, subtasks, concurrency)
|
|
203
|
+
};
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return {
|
|
206
|
+
strategy: concurrency.strategy,
|
|
207
|
+
concurrency: { maxConcurrent: concurrency.maxConcurrent, observedMaxActive: 0 },
|
|
208
|
+
waves: [],
|
|
209
|
+
error: (error && error.message) || String(error)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function dependencyList(subtask) {
|
|
215
|
+
return Array.isArray(subtask.blockedBy) ? subtask.blockedBy.map(String) : [];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function serialOrder(subtasks, plan) {
|
|
219
|
+
const byId = new Map(subtasks.map(subtask => [String(subtask.id), subtask]));
|
|
220
|
+
const plannedOrder = plan && plan.simulation && Array.isArray(plan.simulation.order)
|
|
221
|
+
? plan.simulation.order.map(String)
|
|
222
|
+
: subtasks.map(subtask => String(subtask.id));
|
|
223
|
+
const orderRank = new Map(plannedOrder.map((id, index) => [id, index]));
|
|
224
|
+
const remaining = new Set(subtasks.map(subtask => String(subtask.id)));
|
|
225
|
+
const result = [];
|
|
226
|
+
|
|
227
|
+
while (remaining.size > 0) {
|
|
228
|
+
const ready = [...remaining]
|
|
229
|
+
.filter(id => dependencyList(byId.get(id)).every(dep => !remaining.has(dep)))
|
|
230
|
+
.sort((a, b) => (orderRank.get(a) ?? 9999) - (orderRank.get(b) ?? 9999) || a.localeCompare(b));
|
|
231
|
+
if (ready.length === 0) {
|
|
232
|
+
throw new Error('dependency cycle remained after 03C planning');
|
|
233
|
+
}
|
|
234
|
+
const id = ready[0];
|
|
235
|
+
remaining.delete(id);
|
|
236
|
+
result.push(byId.get(id));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function childDescription(subtask) {
|
|
243
|
+
return String(subtask.description || subtask.title || subtask.id);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function assignmentFor(plan, subtaskId) {
|
|
247
|
+
const assignments = plan && plan.plannedDelegation && Array.isArray(plan.plannedDelegation.assignments)
|
|
248
|
+
? plan.plannedDelegation.assignments
|
|
249
|
+
: [];
|
|
250
|
+
return assignments.find(item => String(item.subtaskId) === String(subtaskId)) || null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function materializeChild(parentTask, subtask, plan) {
|
|
254
|
+
const existing = (await getTasks()).find(task =>
|
|
255
|
+
String(task.parentTaskId) === String(parentTask.id) && String(task.subtaskId) === String(subtask.id)
|
|
256
|
+
);
|
|
257
|
+
if (existing) return existing;
|
|
258
|
+
|
|
259
|
+
const assignment = assignmentFor(plan, subtask.id);
|
|
260
|
+
return addTask(childDescription(subtask), {
|
|
261
|
+
title: subtask.title || childDescription(subtask),
|
|
262
|
+
status: 'pending',
|
|
263
|
+
priority: parentTask.priority || 'medium',
|
|
264
|
+
engine: (assignment && assignment.candidateEngine) || subtask.engine || parentTask.engine || 'codex9',
|
|
265
|
+
files: Array.isArray(subtask.expectedFiles) ? subtask.expectedFiles : (Array.isArray(subtask.files) ? subtask.files : []),
|
|
266
|
+
acceptance: Array.isArray(subtask.acceptance) ? subtask.acceptance : (Array.isArray(parentTask.acceptance) ? parentTask.acceptance : []),
|
|
267
|
+
requestKind: parentTask.requestKind || null,
|
|
268
|
+
sourceRequest: parentTask.sourceRequest || null,
|
|
269
|
+
sourceSpec: parentTask.sourceSpec || null,
|
|
270
|
+
parentTaskId: parentTask.id,
|
|
271
|
+
subtaskId: String(subtask.id),
|
|
272
|
+
blockedBy: dependencyList(subtask)
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function materializeChildren(parentTask, subtasks, plan) {
|
|
277
|
+
const children = [];
|
|
278
|
+
for (const subtask of subtasks) {
|
|
279
|
+
children.push(await materializeChild(parentTask, subtask, plan));
|
|
280
|
+
}
|
|
281
|
+
return children;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function childResultEntry({ subtask, task, status, run, skipped = false, reason = null }) {
|
|
285
|
+
return {
|
|
286
|
+
subtaskId: String(subtask.id),
|
|
287
|
+
taskId: task ? task.id : null,
|
|
288
|
+
status: status || (task && task.status) || 'unknown',
|
|
289
|
+
runId: run && run.runId ? run.runId : null,
|
|
290
|
+
run: run ? cloneData(run) : null,
|
|
291
|
+
noChangeExpected: subtask.noChangeExpected === true ||
|
|
292
|
+
subtask.expectedNoChange === true ||
|
|
293
|
+
((subtask.expectedFiles === undefined || (Array.isArray(subtask.expectedFiles) && subtask.expectedFiles.length === 0)) &&
|
|
294
|
+
subtask.requires && subtask.requires.filesystemWrite === false),
|
|
295
|
+
skipped,
|
|
296
|
+
reason
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function evidenceFromRun(run) {
|
|
301
|
+
if (!run || !run.runId) return [];
|
|
302
|
+
const evidence = [{ type: 'run-status', sourceId: run.runId, field: 'status', value: { status: run.status } }];
|
|
303
|
+
if (Object.prototype.hasOwnProperty.call(run, 'workspaceChanged')) {
|
|
304
|
+
evidence.push({
|
|
305
|
+
type: 'workspace-diff',
|
|
306
|
+
sourceId: run.runId,
|
|
307
|
+
field: 'workspace',
|
|
308
|
+
value: {
|
|
309
|
+
workspaceChanged: run.workspaceChanged,
|
|
310
|
+
changedFiles: Array.isArray(run.changedFiles) ? run.changedFiles : [],
|
|
311
|
+
createdFiles: Array.isArray(run.createdFiles) ? run.createdFiles : [],
|
|
312
|
+
deletedFiles: Array.isArray(run.deletedFiles) ? run.deletedFiles : []
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (run.syntaxValidation !== undefined || run.syntaxValidationFailed !== undefined) {
|
|
317
|
+
evidence.push({ type: 'syntax-validation', sourceId: run.runId, field: 'syntaxValidation', value: { syntaxValidation: run.syntaxValidation || [], syntaxValidationFailed: run.syntaxValidationFailed === true } });
|
|
318
|
+
}
|
|
319
|
+
if (run.hostValidationFallback !== undefined) {
|
|
320
|
+
evidence.push({ type: 'host-validation', sourceId: run.runId, field: 'hostValidationFallback', value: run.hostValidationFallback });
|
|
321
|
+
}
|
|
322
|
+
if (run.failureCategory !== undefined || run.infrastructureFailure !== undefined) {
|
|
323
|
+
evidence.push({ type: 'failure-classification', sourceId: run.runId, field: 'failureCategory', value: { failureCategory: run.failureCategory || null, infrastructureFailure: run.infrastructureFailure === true } });
|
|
324
|
+
}
|
|
325
|
+
if (Array.isArray(run.warnings) && run.warnings.length > 0) {
|
|
326
|
+
evidence.push({ type: 'diagnostics', sourceId: run.runId, field: 'warnings', value: { warnings: run.warnings } });
|
|
327
|
+
}
|
|
328
|
+
return evidence;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function subtaskResultFromChild(child) {
|
|
332
|
+
const status = child.status;
|
|
333
|
+
const run = child.run || null;
|
|
334
|
+
if (run && run.runId && ['completed', 'completed_with_warnings', 'failed'].includes(status)) {
|
|
335
|
+
return {
|
|
336
|
+
subtaskId: child.subtaskId,
|
|
337
|
+
runId: run.runId,
|
|
338
|
+
runStatus: status,
|
|
339
|
+
noChangeExpected: child.noChangeExpected === true,
|
|
340
|
+
evidence: evidenceFromRun(run),
|
|
341
|
+
warnings: Array.isArray(run.warnings) ? run.warnings : [],
|
|
342
|
+
infrastructureFailure: run.infrastructureFailure === true
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (status === 'blocked') {
|
|
346
|
+
return { subtaskId: child.subtaskId, runId: null, runStatus: 'blocked', noExecutionReason: 'blocked_before_execution', evidence: [], warnings: child.reason ? [child.reason] : [] };
|
|
347
|
+
}
|
|
348
|
+
if (status === 'needs_human') {
|
|
349
|
+
return { subtaskId: child.subtaskId, runId: null, runStatus: 'needs_human', noExecutionReason: 'needs_human_before_execution', evidence: [], warnings: child.reason ? [child.reason] : [] };
|
|
350
|
+
}
|
|
351
|
+
return { subtaskId: child.subtaskId, runId: null, runStatus: null, noExecutionReason: 'result_missing', evidence: [], warnings: child.reason ? [child.reason] : [] };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function latestRunAndTask(taskId) {
|
|
355
|
+
return withAccountingLock(async () => {
|
|
356
|
+
const task = await getTaskById(taskId);
|
|
357
|
+
const run = await getLatestRunForTask(taskId);
|
|
358
|
+
return { task, run };
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function dependencyBlockReason(subtask, statusBySubtask) {
|
|
363
|
+
for (const dependency of dependencyList(subtask)) {
|
|
364
|
+
const dependencyKey = String(dependency);
|
|
365
|
+
if (!statusBySubtask.has(dependencyKey)) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const dependencyStatus = statusBySubtask.get(dependencyKey);
|
|
369
|
+
if (!ACCEPTABLE_DEPENDENCY_STATUSES.has(dependencyStatus)) {
|
|
370
|
+
return 'dependency ' + dependency + ' finished as ' + (dependencyStatus || 'missing');
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function runChildrenSerially({ parentTask, subtasks, plan, force, suppressOutput, engine }) {
|
|
377
|
+
const ordered = serialOrder(subtasks, plan);
|
|
378
|
+
const materialized = await materializeChildren(parentTask, subtasks, plan);
|
|
379
|
+
const taskBySubtask = new Map(materialized.map(task => [String(task.subtaskId), task]));
|
|
380
|
+
const statusBySubtask = new Map();
|
|
381
|
+
const children = [];
|
|
382
|
+
const errors = [];
|
|
383
|
+
const warnings = [];
|
|
384
|
+
|
|
385
|
+
for (const subtask of ordered) {
|
|
386
|
+
const childTask = taskBySubtask.get(String(subtask.id));
|
|
387
|
+
const blockReason = dependencyBlockReason(subtask, statusBySubtask);
|
|
388
|
+
if (blockReason) {
|
|
389
|
+
const latest = await withAccountingLock(async () => {
|
|
390
|
+
await updateTaskStatus(childTask.id, 'blocked');
|
|
391
|
+
return getTaskById(childTask.id);
|
|
392
|
+
});
|
|
393
|
+
statusBySubtask.set(String(subtask.id), 'blocked');
|
|
394
|
+
children.push(childResultEntry({ subtask, task: latest || childTask, status: 'blocked', run: null, skipped: true, reason: blockReason }));
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let latest = await latestRunAndTask(childTask.id);
|
|
399
|
+
if (latest.task && TERMINAL_STATUSES.has(latest.task.status) && !(latest.task.status === 'needs_human' && force === true)) {
|
|
400
|
+
statusBySubtask.set(String(subtask.id), latest.task.status);
|
|
401
|
+
children.push(childResultEntry({ subtask, task: latest.task, status: latest.task.status, run: latest.run, skipped: true, reason: 'already_terminal' }));
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let runOutcome = null;
|
|
406
|
+
try {
|
|
407
|
+
runOutcome = await runTask(childTask.id, { force, suppressOutput: true, engine });
|
|
408
|
+
} catch (error) {
|
|
409
|
+
const message = 'child ' + subtask.id + ' execution error: ' + ((error && error.message) || String(error));
|
|
410
|
+
errors.push(message);
|
|
411
|
+
warnings.push(message);
|
|
412
|
+
await withAccountingLock(() => updateTaskStatus(childTask.id, 'needs_human'));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
latest = await latestRunAndTask(childTask.id);
|
|
416
|
+
const status = (runOutcome && runOutcome.status) || (latest.task ? latest.task.status : 'needs_human');
|
|
417
|
+
const run = (runOutcome && runOutcome.run) || latest.run;
|
|
418
|
+
statusBySubtask.set(String(subtask.id), status);
|
|
419
|
+
children.push(childResultEntry({ subtask, task: latest.task || childTask, status, run, skipped: false, reason: null }));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return { children, errors, warnings };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function executeChildRun({ subtask, childTask, force, engine }) {
|
|
426
|
+
let runOutcome = null;
|
|
427
|
+
const errors = [];
|
|
428
|
+
const warnings = [];
|
|
429
|
+
try {
|
|
430
|
+
runOutcome = await runTask(childTask.id, { force, suppressOutput: true, engine });
|
|
431
|
+
} catch (error) {
|
|
432
|
+
const message = 'child ' + subtask.id + ' execution error: ' + ((error && error.message) || String(error));
|
|
433
|
+
errors.push(message);
|
|
434
|
+
warnings.push(message);
|
|
435
|
+
await withAccountingLock(() => updateTaskStatus(childTask.id, 'needs_human'));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const latest = await latestRunAndTask(childTask.id);
|
|
439
|
+
const status = (runOutcome && runOutcome.status) || (latest.task ? latest.task.status : 'needs_human');
|
|
440
|
+
const run = (runOutcome && runOutcome.run) || latest.run;
|
|
441
|
+
return {
|
|
442
|
+
entry: childResultEntry({ subtask, task: latest.task || childTask, status, run, skipped: false, reason: null }),
|
|
443
|
+
status,
|
|
444
|
+
errors,
|
|
445
|
+
warnings
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function markDependencyBlocked({ subtask, childTask, reason }) {
|
|
450
|
+
const latest = await withAccountingLock(async () => {
|
|
451
|
+
await updateTaskStatus(childTask.id, 'blocked');
|
|
452
|
+
return getTaskById(childTask.id);
|
|
453
|
+
});
|
|
454
|
+
return childResultEntry({ subtask, task: latest || childTask, status: 'blocked', run: null, skipped: true, reason });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function removeSatisfiedDependencies(subtask, statusBySubtask, pendingIds) {
|
|
458
|
+
const blockedBy = dependencyList(subtask).filter(dependency => pendingIds.has(String(dependency)) && !ACCEPTABLE_DEPENDENCY_STATUSES.has(statusBySubtask.get(String(dependency))));
|
|
459
|
+
return { ...cloneData(subtask), blockedBy };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function settleKnownBlockedSubtasks({ pending, taskBySubtask, statusBySubtask, childrenBySubtask }) {
|
|
463
|
+
let changed = true;
|
|
464
|
+
while (changed) {
|
|
465
|
+
changed = false;
|
|
466
|
+
for (const subtask of [...pending.values()].sort((a, b) => subtaskId(a).localeCompare(subtaskId(b)))) {
|
|
467
|
+
const blockReason = dependencyBlockReason(subtask, statusBySubtask);
|
|
468
|
+
if (!blockReason) continue;
|
|
469
|
+
const childTask = taskBySubtask.get(subtaskId(subtask));
|
|
470
|
+
const entry = await markDependencyBlocked({ subtask, childTask, reason: blockReason });
|
|
471
|
+
statusBySubtask.set(subtaskId(subtask), 'blocked');
|
|
472
|
+
childrenBySubtask.set(subtaskId(subtask), entry);
|
|
473
|
+
pending.delete(subtaskId(subtask));
|
|
474
|
+
changed = true;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function runLimitedSubtasks(subtasks, limit, worker) {
|
|
480
|
+
const ordered = sortSubtasksById(subtasks);
|
|
481
|
+
const results = [];
|
|
482
|
+
const active = new Set();
|
|
483
|
+
let nextIndex = 0;
|
|
484
|
+
let activeCount = 0;
|
|
485
|
+
let observedMaxActive = 0;
|
|
486
|
+
|
|
487
|
+
async function launch(subtask) {
|
|
488
|
+
activeCount += 1;
|
|
489
|
+
observedMaxActive = Math.max(observedMaxActive, activeCount);
|
|
490
|
+
try {
|
|
491
|
+
results.push(await worker(subtask));
|
|
492
|
+
} finally {
|
|
493
|
+
activeCount -= 1;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
while (nextIndex < ordered.length || active.size > 0) {
|
|
498
|
+
while (nextIndex < ordered.length && active.size < limit) {
|
|
499
|
+
const taskPromise = launch(ordered[nextIndex]).finally(() => {
|
|
500
|
+
active.delete(taskPromise);
|
|
501
|
+
});
|
|
502
|
+
active.add(taskPromise);
|
|
503
|
+
nextIndex += 1;
|
|
504
|
+
}
|
|
505
|
+
if (active.size > 0) {
|
|
506
|
+
await Promise.race(active);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return { results: results.sort((a, b) => subtaskId(a.subtask).localeCompare(subtaskId(b.subtask))), observedMaxActive };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function runChildrenControlledParallel({ parentTask, subtasks, plan, force, suppressOutput, engine, concurrency }) {
|
|
514
|
+
const materialized = await materializeChildren(parentTask, subtasks, plan);
|
|
515
|
+
await withAccountingLock(() => ensureSafetyConfig());
|
|
516
|
+
const taskBySubtask = new Map(materialized.map(task => [String(task.subtaskId), task]));
|
|
517
|
+
const statusBySubtask = new Map();
|
|
518
|
+
const childrenBySubtask = new Map();
|
|
519
|
+
const errors = [];
|
|
520
|
+
const warnings = [];
|
|
521
|
+
let observedMaxActive = 0;
|
|
522
|
+
|
|
523
|
+
for (const subtask of sortSubtasksById(subtasks)) {
|
|
524
|
+
const childTask = taskBySubtask.get(subtaskId(subtask));
|
|
525
|
+
const latest = await latestRunAndTask(childTask.id);
|
|
526
|
+
if (latest.task && TERMINAL_STATUSES.has(latest.task.status) && !(latest.task.status === 'needs_human' && force === true)) {
|
|
527
|
+
statusBySubtask.set(subtaskId(subtask), latest.task.status);
|
|
528
|
+
childrenBySubtask.set(subtaskId(subtask), childResultEntry({ subtask, task: latest.task, status: latest.task.status, run: latest.run, skipped: true, reason: 'already_terminal' }));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const pending = new Map(sortSubtasksById(subtasks)
|
|
533
|
+
.filter(subtask => !statusBySubtask.has(subtaskId(subtask)))
|
|
534
|
+
.map(subtask => [subtaskId(subtask), subtask]));
|
|
535
|
+
await settleKnownBlockedSubtasks({ pending, taskBySubtask, statusBySubtask, childrenBySubtask });
|
|
536
|
+
|
|
537
|
+
const pendingIds = new Set(pending.keys());
|
|
538
|
+
const schedulableSubtasks = [...pending.values()].map(subtask => removeSatisfiedDependencies(subtask, statusBySubtask, pendingIds));
|
|
539
|
+
const waves = computeWaves(schedulableSubtasks);
|
|
540
|
+
const auditWaves = [];
|
|
541
|
+
|
|
542
|
+
for (const wave of waves) {
|
|
543
|
+
await settleKnownBlockedSubtasks({ pending, taskBySubtask, statusBySubtask, childrenBySubtask });
|
|
544
|
+
const waveSubtasks = wave.subtaskIds.map(id => pending.get(String(id))).filter(Boolean);
|
|
545
|
+
if (waveSubtasks.length === 0) {
|
|
546
|
+
auditWaves.push({ waveIndex: wave.waveIndex, parallelSubtaskIds: [], serialSubtaskIds: [] });
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const normalizedWaveSubtasks = waveSubtasks.map(subtask => removeSatisfiedDependencies(subtask, statusBySubtask, new Set(waveSubtasks.map(subtaskId))));
|
|
550
|
+
const waveConflicts = detectFileConflicts(normalizedWaveSubtasks, { projectRoot: process.cwd() });
|
|
551
|
+
const planned = planWaveExecution({ ...wave, subtasks: normalizedWaveSubtasks }, normalizedWaveSubtasks, { waveConflicts, maxConcurrent: concurrency.maxConcurrent });
|
|
552
|
+
auditWaves.push({
|
|
553
|
+
waveIndex: wave.waveIndex,
|
|
554
|
+
parallelSubtaskIds: planned.parallel.map(subtaskId),
|
|
555
|
+
serialSubtaskIds: planned.serial.map(subtaskId)
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const worker = async subtask => {
|
|
559
|
+
const originalSubtask = pending.get(subtaskId(subtask)) || subtask;
|
|
560
|
+
const childTask = taskBySubtask.get(subtaskId(subtask));
|
|
561
|
+
const blockReason = dependencyBlockReason(originalSubtask, statusBySubtask);
|
|
562
|
+
if (blockReason) {
|
|
563
|
+
const entry = await markDependencyBlocked({ subtask: originalSubtask, childTask, reason: blockReason });
|
|
564
|
+
return { subtask: originalSubtask, entry, status: 'blocked', errors: [], warnings: [] };
|
|
565
|
+
}
|
|
566
|
+
const result = await executeChildRun({ subtask: originalSubtask, childTask, force, suppressOutput, engine });
|
|
567
|
+
return { subtask: originalSubtask, ...result };
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const parallelRun = await runLimitedSubtasks(planned.parallel, concurrency.maxConcurrent, worker);
|
|
571
|
+
observedMaxActive = Math.max(observedMaxActive, parallelRun.observedMaxActive);
|
|
572
|
+
for (const result of parallelRun.results) {
|
|
573
|
+
statusBySubtask.set(subtaskId(result.subtask), result.status);
|
|
574
|
+
childrenBySubtask.set(subtaskId(result.subtask), result.entry);
|
|
575
|
+
pending.delete(subtaskId(result.subtask));
|
|
576
|
+
warnings.push(...result.warnings);
|
|
577
|
+
errors.push(...result.errors);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
for (const subtask of planned.serial) {
|
|
581
|
+
const result = await worker(subtask);
|
|
582
|
+
observedMaxActive = Math.max(observedMaxActive, 1);
|
|
583
|
+
statusBySubtask.set(subtaskId(result.subtask), result.status);
|
|
584
|
+
childrenBySubtask.set(subtaskId(result.subtask), result.entry);
|
|
585
|
+
pending.delete(subtaskId(result.subtask));
|
|
586
|
+
warnings.push(...result.warnings);
|
|
587
|
+
errors.push(...result.errors);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
await settleKnownBlockedSubtasks({ pending, taskBySubtask, statusBySubtask, childrenBySubtask });
|
|
592
|
+
const children = sortSubtasksById(subtasks).map(subtask => childrenBySubtask.get(subtaskId(subtask))).filter(Boolean);
|
|
593
|
+
return { children, errors, warnings, waves: auditWaves, observedMaxActive };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function fallbackConsolidation(task, reason, children = []) {
|
|
597
|
+
return {
|
|
598
|
+
schemaVersion: 1,
|
|
599
|
+
taskId: String(task.id),
|
|
600
|
+
status: 'inconclusive',
|
|
601
|
+
consolidationCompleteness: 'invalid',
|
|
602
|
+
perSubtask: children.map(child => ({ subtaskId: child.subtaskId, observedState: child.status, runId: child.runId || null })),
|
|
603
|
+
evidence: [],
|
|
604
|
+
missingEvidence: [],
|
|
605
|
+
warnings: [reason],
|
|
606
|
+
failures: [reason],
|
|
607
|
+
codeFailures: [],
|
|
608
|
+
infrastructureFailures: [reason],
|
|
609
|
+
conflicts: [],
|
|
610
|
+
inconsistencies: [],
|
|
611
|
+
invalidReferences: [],
|
|
612
|
+
remainingIssues: [reason],
|
|
613
|
+
inputErrors: [reason],
|
|
614
|
+
recommendedNextAction: 'block',
|
|
615
|
+
verifierInputHints: {},
|
|
616
|
+
summary: reason
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function isValidConsolidation(value) {
|
|
621
|
+
return isPlainObject(value) && value.schemaVersion === 1 && typeof value.taskId === 'string' &&
|
|
622
|
+
['accepted', 'accepted_with_warnings', 'rejected', 'inconclusive', 'needs_human'].includes(value.status) &&
|
|
623
|
+
['complete', 'partial', 'invalid'].includes(value.consolidationCompleteness) &&
|
|
624
|
+
['perSubtask', 'evidence', 'missingEvidence', 'warnings', 'failures', 'codeFailures', 'infrastructureFailures', 'conflicts', 'inconsistencies', 'invalidReferences', 'remainingIssues', 'inputErrors'].every(field => value[field] === undefined || Array.isArray(value[field]));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function isValidVerification(value) {
|
|
628
|
+
const outcome = value && (value.verificationOutcome || value.outcome);
|
|
629
|
+
return isPlainObject(value) && value.schemaVersion === 1 && ['verified', 'verified_with_warnings', 'rejected', 'inconclusive', 'blocked', 'needs_human'].includes(outcome);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function fallbackVerification(reason) {
|
|
633
|
+
return {
|
|
634
|
+
schemaVersion: 1,
|
|
635
|
+
scope: 'consolidated',
|
|
636
|
+
verificationOutcome: 'inconclusive',
|
|
637
|
+
outcome: 'inconclusive',
|
|
638
|
+
verified: false,
|
|
639
|
+
warnings: [reason],
|
|
640
|
+
failures: [reason],
|
|
641
|
+
errors: [reason],
|
|
642
|
+
recommendations: [{ action: 'block', reason, consultative: false }],
|
|
643
|
+
summary: reason
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function deriveParentOutcome(consolidation, verification) {
|
|
648
|
+
const consolidatedStatus = consolidation && consolidation.status;
|
|
649
|
+
const completeness = consolidation && consolidation.consolidationCompleteness;
|
|
650
|
+
const outcome = verification && (verification.verificationOutcome || verification.outcome);
|
|
651
|
+
|
|
652
|
+
if (outcome === 'blocked') {
|
|
653
|
+
return { parentStatus: 'blocked', reason: 'verifier blocked the consolidated result' };
|
|
654
|
+
}
|
|
655
|
+
if (outcome === 'needs_human' || consolidatedStatus === 'needs_human') {
|
|
656
|
+
return { parentStatus: 'needs_human', reason: 'verifier or consolidator requires human review' };
|
|
657
|
+
}
|
|
658
|
+
if (outcome === 'rejected') {
|
|
659
|
+
return { parentStatus: 'failed', reason: 'verifier rejected the consolidated result' };
|
|
660
|
+
}
|
|
661
|
+
if (outcome === 'inconclusive') {
|
|
662
|
+
return { parentStatus: 'blocked', reason: 'verifier was inconclusive' };
|
|
663
|
+
}
|
|
664
|
+
if (SUCCESS_VERIFICATION_OUTCOMES.has(outcome) && SUCCESS_PARENT_STATUSES.has(consolidatedStatus) && ['complete', 'partial'].includes(completeness)) {
|
|
665
|
+
if (outcome === 'verified_with_warnings' || consolidatedStatus === 'accepted_with_warnings' || completeness === 'partial') {
|
|
666
|
+
return { parentStatus: 'completed_with_warnings', reason: 'verified with warnings or partial consolidation' };
|
|
667
|
+
}
|
|
668
|
+
return { parentStatus: 'completed', reason: 'verified consolidated result' };
|
|
669
|
+
}
|
|
670
|
+
return { parentStatus: 'blocked', reason: 'parent completion requires accepted consolidation and verified outcome' };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function consolidateAndVerify({ task, subtasks, children, plan, rawInput }) {
|
|
674
|
+
const subtaskResults = children.map(subtaskResultFromChild);
|
|
675
|
+
let consolidation;
|
|
676
|
+
let verification;
|
|
677
|
+
const warnings = [];
|
|
678
|
+
const errors = [];
|
|
679
|
+
const consolidate = rawInput.consolidateResults || consolidateResults;
|
|
680
|
+
const verify = rawInput.verifyConsolidatedResult || verifyConsolidatedResult;
|
|
681
|
+
|
|
682
|
+
try {
|
|
683
|
+
consolidation = consolidate({
|
|
684
|
+
task: cloneData(task),
|
|
685
|
+
subtasks: cloneData(subtasks),
|
|
686
|
+
subtaskResults: cloneData(subtaskResults),
|
|
687
|
+
warnings: [],
|
|
688
|
+
classification: cloneData(plan.decisions ? plan.decisions.classification : {})
|
|
689
|
+
});
|
|
690
|
+
if (!isValidConsolidation(consolidation)) {
|
|
691
|
+
const reason = 'Consolidator returned malformed result';
|
|
692
|
+
warnings.push(reason);
|
|
693
|
+
errors.push(reason);
|
|
694
|
+
consolidation = fallbackConsolidation(task, reason, children);
|
|
695
|
+
}
|
|
696
|
+
} catch (error) {
|
|
697
|
+
const reason = 'Consolidator failed: ' + ((error && error.message) || String(error));
|
|
698
|
+
warnings.push(reason);
|
|
699
|
+
errors.push(reason);
|
|
700
|
+
consolidation = fallbackConsolidation(task, reason, children);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
try {
|
|
704
|
+
verification = verify({
|
|
705
|
+
consolidatedResult: cloneData(consolidation),
|
|
706
|
+
acceptanceCriteria: Array.isArray(task.acceptance) ? cloneData(task.acceptance) : [],
|
|
707
|
+
options: cloneData(rawInput.verifierOptions || {})
|
|
708
|
+
});
|
|
709
|
+
if (!isValidVerification(verification)) {
|
|
710
|
+
const reason = 'Verifier returned malformed result';
|
|
711
|
+
warnings.push(reason);
|
|
712
|
+
errors.push(reason);
|
|
713
|
+
verification = fallbackVerification(reason);
|
|
714
|
+
}
|
|
715
|
+
} catch (error) {
|
|
716
|
+
const reason = 'Verifier failed: ' + ((error && error.message) || String(error));
|
|
717
|
+
warnings.push(reason);
|
|
718
|
+
errors.push(reason);
|
|
719
|
+
verification = fallbackVerification(reason);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const outcome = deriveParentOutcome(consolidation, verification);
|
|
723
|
+
return { consolidation, verification, ...outcome, warnings, errors, subtaskResults };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function nextOrchestrationAttempt(parentTaskId) {
|
|
727
|
+
const events = await getOrchestrationEvents(parentTaskId);
|
|
728
|
+
return events.filter(event =>
|
|
729
|
+
event && !event.parseError &&
|
|
730
|
+
(event.eventType === 'orchestration_started' || event.eventType === 'orchestration_blocked') &&
|
|
731
|
+
String(event.parentTaskId) === String(parentTaskId)
|
|
732
|
+
).length + 1;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function orchestrationEnvelope(taskId, attempt, startTime, eventType, fields = {}) {
|
|
736
|
+
return {
|
|
737
|
+
orchestrationId: String(taskId) + ':' + attempt,
|
|
738
|
+
parentTaskId: String(taskId),
|
|
739
|
+
attempt,
|
|
740
|
+
mode: 'real',
|
|
741
|
+
eventType,
|
|
742
|
+
startTime,
|
|
743
|
+
timestamp: new Date().toISOString(),
|
|
744
|
+
...fields
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function executeReal(input, task, plan) {
|
|
749
|
+
const warnings = [];
|
|
750
|
+
const errors = [];
|
|
751
|
+
const subtasks = plannedSubtasks(plan);
|
|
752
|
+
const concurrency = normalizeConcurrency(input.concurrency);
|
|
753
|
+
const attempt = await nextOrchestrationAttempt(task.id);
|
|
754
|
+
const startTime = new Date().toISOString();
|
|
755
|
+
let observedMaxActive = 0;
|
|
756
|
+
let auditWaves = [];
|
|
757
|
+
const auditFields = () => ({
|
|
758
|
+
strategy: concurrency.strategy,
|
|
759
|
+
...(concurrency.enabled ? { concurrency: { maxConcurrent: concurrency.maxConcurrent, observedMaxActive }, waves: cloneData(auditWaves) } : {})
|
|
760
|
+
});
|
|
761
|
+
const appendFinished = fields => appendOrchestrationEvent(orchestrationEnvelope(task.id, attempt, startTime, 'orchestration_finished', {
|
|
762
|
+
endTime: new Date().toISOString(),
|
|
763
|
+
trace: plan.trace || [],
|
|
764
|
+
...auditFields(),
|
|
765
|
+
...fields
|
|
766
|
+
}));
|
|
767
|
+
|
|
768
|
+
if (!plan.completedPlanning || plan.finalLoopState !== 'loop.completed' || hasBlockingConflict(plan)) {
|
|
769
|
+
const reason = plan.errors && plan.errors.length > 0
|
|
770
|
+
? plan.errors.map(error => error.message || error.code || String(error)).join('; ')
|
|
771
|
+
: 'real mode requires completed planning without blocking conflicts';
|
|
772
|
+
await appendOrchestrationEvent(orchestrationEnvelope(task.id, attempt, startTime, 'orchestration_blocked', {
|
|
773
|
+
status: plan.finalLoopState === 'loop.needs_human' ? 'needs_human' : 'blocked',
|
|
774
|
+
reason,
|
|
775
|
+
trace: plan.trace || [],
|
|
776
|
+
warnings,
|
|
777
|
+
...auditFields()
|
|
778
|
+
}));
|
|
779
|
+
return buildResult({
|
|
780
|
+
taskId: task.id,
|
|
781
|
+
mode: 'real',
|
|
782
|
+
executed: false,
|
|
783
|
+
plan,
|
|
784
|
+
parentStatus: plan.finalLoopState === 'loop.needs_human' ? 'needs_human' : 'blocked',
|
|
785
|
+
statusOverrideReason: reason,
|
|
786
|
+
trace: plan.trace || [],
|
|
787
|
+
warnings,
|
|
788
|
+
errors: [reason],
|
|
789
|
+
summary: 'Real orchestration refused: ' + reason
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (concurrency.enabled && subtasks.length > 0) {
|
|
794
|
+
const projection = dryRunExecutionPlan(plan, input.concurrency);
|
|
795
|
+
auditWaves = projection && Array.isArray(projection.waves) ? projection.waves : [];
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
await appendOrchestrationEvent(orchestrationEnvelope(task.id, attempt, startTime, 'orchestration_started', auditFields()));
|
|
799
|
+
|
|
800
|
+
if (subtasks.length === 0) {
|
|
801
|
+
let runResult;
|
|
802
|
+
try {
|
|
803
|
+
runResult = await runTask(task.id, { force: input.force === true, suppressOutput: input.suppressOutput === true, engine: input.engine });
|
|
804
|
+
} catch (error) {
|
|
805
|
+
const reason = 'parent direct execution error: ' + ((error && error.message) || String(error));
|
|
806
|
+
await appendFinished({
|
|
807
|
+
status: 'needs_human',
|
|
808
|
+
children: [],
|
|
809
|
+
consolidation: null,
|
|
810
|
+
verification: null,
|
|
811
|
+
warnings: [reason],
|
|
812
|
+
reason
|
|
813
|
+
});
|
|
814
|
+
return buildResult({
|
|
815
|
+
taskId: task.id,
|
|
816
|
+
mode: 'real',
|
|
817
|
+
executed: true,
|
|
818
|
+
plan,
|
|
819
|
+
parentStatus: 'needs_human',
|
|
820
|
+
statusOverrideReason: reason,
|
|
821
|
+
trace: plan.trace || [],
|
|
822
|
+
warnings: [reason],
|
|
823
|
+
errors: [reason],
|
|
824
|
+
summary: reason
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
const latest = await getTaskById(task.id);
|
|
828
|
+
const status = (runResult && runResult.status) || (latest && latest.status) || null;
|
|
829
|
+
await appendFinished({
|
|
830
|
+
status: status || 'blocked',
|
|
831
|
+
children: [],
|
|
832
|
+
consolidation: null,
|
|
833
|
+
verification: null,
|
|
834
|
+
warnings,
|
|
835
|
+
reason: status ? 'direct runTask outcome' : 'runTask returned without terminal status'
|
|
836
|
+
});
|
|
837
|
+
return buildResult({
|
|
838
|
+
taskId: task.id,
|
|
839
|
+
mode: 'real',
|
|
840
|
+
executed: true,
|
|
841
|
+
plan,
|
|
842
|
+
children: [],
|
|
843
|
+
parentStatus: status,
|
|
844
|
+
statusOverrideReason: status ? 'direct runTask outcome' : 'runTask returned without terminal status',
|
|
845
|
+
trace: plan.trace || [],
|
|
846
|
+
warnings,
|
|
847
|
+
errors,
|
|
848
|
+
summary: 'Direct orchestration executed through runTask for task #' + task.id + '.'
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
let childRun;
|
|
853
|
+
try {
|
|
854
|
+
childRun = concurrency.enabled
|
|
855
|
+
? await runChildrenControlledParallel({
|
|
856
|
+
parentTask: task,
|
|
857
|
+
subtasks,
|
|
858
|
+
plan,
|
|
859
|
+
force: input.force === true,
|
|
860
|
+
suppressOutput: input.suppressOutput === true,
|
|
861
|
+
engine: input.engine,
|
|
862
|
+
concurrency
|
|
863
|
+
})
|
|
864
|
+
: await runChildrenSerially({
|
|
865
|
+
parentTask: task,
|
|
866
|
+
subtasks,
|
|
867
|
+
plan,
|
|
868
|
+
force: input.force === true,
|
|
869
|
+
suppressOutput: input.suppressOutput === true,
|
|
870
|
+
engine: input.engine
|
|
871
|
+
});
|
|
872
|
+
observedMaxActive = childRun.observedMaxActive || observedMaxActive;
|
|
873
|
+
if (Array.isArray(childRun.waves)) {
|
|
874
|
+
auditWaves = childRun.waves;
|
|
875
|
+
}
|
|
876
|
+
} catch (error) {
|
|
877
|
+
const reason = 'child persistence or ordering error: ' + ((error && error.message) || String(error));
|
|
878
|
+
await appendFinished({
|
|
879
|
+
status: 'blocked',
|
|
880
|
+
children: [],
|
|
881
|
+
consolidation: null,
|
|
882
|
+
verification: null,
|
|
883
|
+
warnings: [reason],
|
|
884
|
+
reason
|
|
885
|
+
});
|
|
886
|
+
return buildResult({
|
|
887
|
+
taskId: task.id,
|
|
888
|
+
mode: 'real',
|
|
889
|
+
executed: false,
|
|
890
|
+
plan,
|
|
891
|
+
parentStatus: 'blocked',
|
|
892
|
+
statusOverrideReason: reason,
|
|
893
|
+
trace: plan.trace || [],
|
|
894
|
+
warnings: [reason],
|
|
895
|
+
errors: [reason],
|
|
896
|
+
summary: reason
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
warnings.push(...childRun.warnings);
|
|
901
|
+
errors.push(...childRun.errors);
|
|
902
|
+
const verified = await consolidateAndVerify({ task, subtasks, children: childRun.children, plan, rawInput: input });
|
|
903
|
+
warnings.push(...verified.warnings);
|
|
904
|
+
errors.push(...verified.errors);
|
|
905
|
+
|
|
906
|
+
const updated = await updateTaskStatus(task.id, verified.parentStatus);
|
|
907
|
+
if (!updated) {
|
|
908
|
+
errors.push('failed to update parent task status');
|
|
909
|
+
}
|
|
910
|
+
await appendFinished({
|
|
911
|
+
status: updated ? verified.parentStatus : 'blocked',
|
|
912
|
+
children: childRun.children,
|
|
913
|
+
consolidation: verified.consolidation,
|
|
914
|
+
verification: verified.verification,
|
|
915
|
+
warnings,
|
|
916
|
+
reason: updated ? verified.reason : 'failed to update parent task status'
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
return buildResult({
|
|
920
|
+
taskId: task.id,
|
|
921
|
+
mode: 'real',
|
|
922
|
+
executed: true,
|
|
923
|
+
plan,
|
|
924
|
+
children: childRun.children,
|
|
925
|
+
consolidation: verified.consolidation,
|
|
926
|
+
verification: verified.verification,
|
|
927
|
+
parentStatus: updated ? verified.parentStatus : 'blocked',
|
|
928
|
+
statusOverrideReason: updated ? verified.reason : 'failed to update parent task status',
|
|
929
|
+
trace: plan.trace || [],
|
|
930
|
+
warnings,
|
|
931
|
+
errors,
|
|
932
|
+
summary: (concurrency.enabled ? 'Controlled-parallel' : 'Serial') + ' orchestration executed ' + childRun.children.length + ' child task(s); parent status ' + (updated ? verified.parentStatus : 'blocked') + '.'
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async function runSerialOrchestrationInner(rawInput) {
|
|
937
|
+
const sourceInput = rawInput || {};
|
|
938
|
+
const input = {
|
|
939
|
+
...sourceInput,
|
|
940
|
+
context: cloneData(sourceInput.context || {}),
|
|
941
|
+
moduleResults: cloneData(sourceInput.moduleResults || {}),
|
|
942
|
+
limits: cloneData(sourceInput.limits || {}),
|
|
943
|
+
adapters: sourceInput.adapters || {},
|
|
944
|
+
engine: sourceInput.engine,
|
|
945
|
+
consolidateResults: sourceInput.consolidateResults,
|
|
946
|
+
verifyConsolidatedResult: sourceInput.verifyConsolidatedResult,
|
|
947
|
+
concurrency: cloneData(sourceInput.concurrency)
|
|
948
|
+
};
|
|
949
|
+
const taskId = normalizeTaskId(input.taskId);
|
|
950
|
+
const mode = normalizeMode(input.mode);
|
|
951
|
+
let task;
|
|
952
|
+
try {
|
|
953
|
+
task = await getTaskById(taskId);
|
|
954
|
+
} catch (error) {
|
|
955
|
+
const reason = 'failed to read task #' + taskId + ': ' + ((error && error.message) || String(error));
|
|
956
|
+
return buildResult({ taskId, mode, errors: [reason], summary: reason });
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (!task) {
|
|
960
|
+
return buildResult({
|
|
961
|
+
taskId,
|
|
962
|
+
mode,
|
|
963
|
+
errors: ['Task #' + taskId + ' not found.'],
|
|
964
|
+
summary: 'Task #' + taskId + ' not found.'
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
let plan;
|
|
969
|
+
try {
|
|
970
|
+
plan = await planSerialOrchestration({ task, mode, force: input.force === true, rawInput: input });
|
|
971
|
+
} catch (error) {
|
|
972
|
+
const reason = 'planning failed: ' + ((error && error.message) || String(error));
|
|
973
|
+
return buildResult({
|
|
974
|
+
taskId,
|
|
975
|
+
mode,
|
|
976
|
+
errors: [reason],
|
|
977
|
+
summary: reason
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (mode !== 'real') {
|
|
982
|
+
return buildResult({
|
|
983
|
+
taskId,
|
|
984
|
+
mode: 'dry-run',
|
|
985
|
+
executed: false,
|
|
986
|
+
plan,
|
|
987
|
+
executionPlan: dryRunExecutionPlan(plan, input.concurrency),
|
|
988
|
+
trace: plan.trace || [],
|
|
989
|
+
warnings: plan.warnings || [],
|
|
990
|
+
errors: plan.errors || [],
|
|
991
|
+
summary: 'Dry-run orchestration completed without side effects.'
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return executeReal(input, task, plan);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
export async function runSerialOrchestration(input) {
|
|
999
|
+
const frozenInput = input || {};
|
|
1000
|
+
const taskId = normalizeTaskId(frozenInput.taskId);
|
|
1001
|
+
const mode = normalizeMode(frozenInput.mode);
|
|
1002
|
+
if (mode !== 'real') {
|
|
1003
|
+
return runSerialOrchestrationInner(frozenInput);
|
|
1004
|
+
}
|
|
1005
|
+
return runSerialized(taskId, () => runSerialOrchestrationInner(frozenInput));
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
export async function resumeSerialOrchestration(input) {
|
|
1009
|
+
const taskId = normalizeTaskId(input && input.taskId);
|
|
1010
|
+
const task = await getTaskById(taskId);
|
|
1011
|
+
if (!task) {
|
|
1012
|
+
return { resumed: false, reason: 'Task #' + redactCredentials(taskId) + ' not found.' };
|
|
1013
|
+
}
|
|
1014
|
+
const existingChildren = (await getTasks()).filter(item => String(item.parentTaskId) === String(taskId));
|
|
1015
|
+
if (existingChildren.length === 0 && TERMINAL_STATUSES.has(task.status)) {
|
|
1016
|
+
return { resumed: false, reason: 'No serial orchestration to resume for terminal task #' + redactCredentials(taskId) + '.' };
|
|
1017
|
+
}
|
|
1018
|
+
return runSerialOrchestration({ ...input, taskId, mode: 'real', suppressOutput: true });
|
|
1019
|
+
}
|