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.
Files changed (81) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +15 -0
  3. package/README.md +727 -0
  4. package/bin/maestro.mjs +246 -0
  5. package/package.json +29 -0
  6. package/src/checkpoint.mjs +102 -0
  7. package/src/codex-diagnostics.mjs +682 -0
  8. package/src/codex-home-inspect.mjs +252 -0
  9. package/src/codex-home.mjs +258 -0
  10. package/src/commands.mjs +809 -0
  11. package/src/config.mjs +11 -0
  12. package/src/debug.mjs +164 -0
  13. package/src/encoding-guard.mjs +125 -0
  14. package/src/engines/ai-router-engine.mjs +37 -0
  15. package/src/engines/codex-engine.mjs +21 -0
  16. package/src/engines/engine.mjs +21 -0
  17. package/src/engines/registry.mjs +29 -0
  18. package/src/files.mjs +65 -0
  19. package/src/format.mjs +132 -0
  20. package/src/lock.mjs +439 -0
  21. package/src/memory-log.mjs +18 -0
  22. package/src/memory.mjs +1 -0
  23. package/src/orchestration/attempt-chain-runtime.mjs +627 -0
  24. package/src/orchestration/budget-manager.mjs +121 -0
  25. package/src/orchestration/capability-registry.mjs +108 -0
  26. package/src/orchestration/consolidator.mjs +772 -0
  27. package/src/orchestration/delegation-executor.mjs +262 -0
  28. package/src/orchestration/delegation-manager.mjs +116 -0
  29. package/src/orchestration/deployment-intent.mjs +36 -0
  30. package/src/orchestration/engine-history.mjs +110 -0
  31. package/src/orchestration/engine-policy.mjs +73 -0
  32. package/src/orchestration/engine-selector.mjs +187 -0
  33. package/src/orchestration/execution-context.mjs +45 -0
  34. package/src/orchestration/failure-classifier.mjs +136 -0
  35. package/src/orchestration/failure-evidence.mjs +237 -0
  36. package/src/orchestration/fallback-chain-lock.mjs +217 -0
  37. package/src/orchestration/fallback-chain-trail.mjs +218 -0
  38. package/src/orchestration/fallback-executor.mjs +146 -0
  39. package/src/orchestration/fallback-graph.mjs +106 -0
  40. package/src/orchestration/fallback-recommendation.mjs +73 -0
  41. package/src/orchestration/file-conflict-detector.mjs +126 -0
  42. package/src/orchestration/host-validation.mjs +241 -0
  43. package/src/orchestration/orchestration-loop.mjs +1971 -0
  44. package/src/orchestration/orchestration-runtime.mjs +1019 -0
  45. package/src/orchestration/orchestration-scheduler.mjs +135 -0
  46. package/src/orchestration/orchestration-trail.mjs +192 -0
  47. package/src/orchestration/preflight.mjs +212 -0
  48. package/src/orchestration/provider-health.mjs +566 -0
  49. package/src/orchestration/provider-router.mjs +352 -0
  50. package/src/orchestration/rc-check-adapters.mjs +817 -0
  51. package/src/orchestration/rc-check.mjs +544 -0
  52. package/src/orchestration/rc-policy.mjs +200 -0
  53. package/src/orchestration/runtime-gate.mjs +146 -0
  54. package/src/orchestration/sector-managers.mjs +215 -0
  55. package/src/orchestration/self-hosting-canary.mjs +231 -0
  56. package/src/orchestration/self-hosting-readiness.mjs +244 -0
  57. package/src/orchestration/spec-planner.mjs +877 -0
  58. package/src/orchestration/subtask-planner.mjs +176 -0
  59. package/src/orchestration/task-classifier.mjs +241 -0
  60. package/src/orchestration/verifier.mjs +1608 -0
  61. package/src/orchestration-commands.mjs +279 -0
  62. package/src/planner.mjs +38 -0
  63. package/src/project.mjs +1 -0
  64. package/src/run-diagnostics.mjs +641 -0
  65. package/src/runner.mjs +243 -0
  66. package/src/safety.mjs +116 -0
  67. package/src/schema.mjs +371 -0
  68. package/src/session-commands.mjs +521 -0
  69. package/src/shell.mjs +93 -0
  70. package/src/smart-planner.mjs +249 -0
  71. package/src/task-commands.mjs +1182 -0
  72. package/src/tasks.mjs +134 -0
  73. package/src/ui/commands.mjs +76 -0
  74. package/src/ui/events.mjs +45 -0
  75. package/src/ui/public/app.js +600 -0
  76. package/src/ui/public/index.html +88 -0
  77. package/src/ui/public/style.css +460 -0
  78. package/src/ui/server.mjs +112 -0
  79. package/src/ui/state.mjs +504 -0
  80. package/src/usage.mjs +178 -0
  81. package/src/workspace-diff.mjs +228 -0
@@ -0,0 +1,772 @@
1
+ const SCHEMA_VERSION = 1;
2
+
3
+ const RUN_STATUSES = Object.freeze([
4
+ 'completed',
5
+ 'completed_with_warnings',
6
+ 'failed',
7
+ 'blocked',
8
+ 'needs_human'
9
+ ]);
10
+
11
+ const CONSOLIDATION_STATUSES = Object.freeze([
12
+ 'accepted',
13
+ 'accepted_with_warnings',
14
+ 'rejected',
15
+ 'inconclusive',
16
+ 'needs_human'
17
+ ]);
18
+
19
+ const COMPLETENESS_VALUES = Object.freeze(['complete', 'partial', 'invalid']);
20
+ const NEXT_ACTIONS = Object.freeze(['complete', 'verify', 'fallback', 'fix', 'ask_human', 'block']);
21
+ const EVIDENCE_TYPES = Object.freeze([
22
+ 'run-status',
23
+ 'syntax-validation',
24
+ 'workspace-diff',
25
+ 'test-result',
26
+ 'host-validation',
27
+ 'failure-classification',
28
+ 'verifier-result',
29
+ 'diagnostics',
30
+ 'other'
31
+ ]);
32
+
33
+ const TASK_STATUSES = Object.freeze([
34
+ 'pending',
35
+ 'in_progress',
36
+ 'completed',
37
+ 'completed_with_warnings',
38
+ 'failed',
39
+ 'blocked',
40
+ 'needs_human',
41
+ 'archived'
42
+ ]);
43
+
44
+ export function consolidateResults(input) {
45
+ assertConsolidationInput(input);
46
+
47
+ const taskId = String(input.task.id);
48
+ const subtasks = normalizeSubtasks(input.subtasks || []);
49
+ const subtaskResults = normalizeSubtaskResults(input.subtaskResults || []);
50
+ const rawTaskWarnings = input.warnings || [];
51
+ const taskWarnings = normalizeStringArray(rawTaskWarnings);
52
+ const provenance = normalizeEvidenceArray(input.provenance || [], {
53
+ inputErrors: [],
54
+ owner: 'input.provenance'
55
+ });
56
+ const fallbackResults = [...(input.fallbackResults || [])];
57
+ const classification = isPlainObject(input.classification) ? input.classification : {};
58
+ const verifierResult = input.verifierResult ?? null;
59
+
60
+ const inputErrors = collectInputErrors(input, subtasks, rawTaskWarnings, provenance.inputErrors);
61
+ const invalidReferences = collectReferenceErrors(taskId, subtasks, subtaskResults);
62
+ const resultBySubtask = firstResultBySubtask(subtaskResults);
63
+ const declaredSubtaskIds = subtasks.length > 0 ? subtasks.map(subtask => subtask.id) : parentSubtaskIds(taskId, subtaskResults);
64
+ const subtaskById = new Map(subtasks.map(subtask => [subtask.id, subtask]));
65
+ const allEvidence = [];
66
+
67
+ for (const entry of provenance.entries) {
68
+ allEvidence.push({
69
+ rawSubtaskId: taskId,
70
+ rawRunId: null,
71
+ originSequence: entry.sequence,
72
+ type: entry.type,
73
+ sourceId: entry.sourceId,
74
+ field: entry.field,
75
+ value: cloneData(entry.value)
76
+ });
77
+ }
78
+
79
+ fallbackResults.forEach((value, index) => {
80
+ allEvidence.push({
81
+ rawSubtaskId: taskId,
82
+ rawRunId: null,
83
+ originSequence: provenance.entries.length + index,
84
+ type: 'diagnostics',
85
+ sourceId: `fallback:${index}`,
86
+ field: 'fallbackResults',
87
+ value: cloneData(value)
88
+ });
89
+ });
90
+
91
+ const preliminarySubtasks = declaredSubtaskIds.map(subtaskId => {
92
+ const subtask = subtaskById.get(subtaskId) || null;
93
+ const result = resultBySubtask.get(subtaskId) || null;
94
+ return buildSubtaskConsolidation({
95
+ taskId,
96
+ subtaskId,
97
+ subtask,
98
+ result,
99
+ allEvidence,
100
+ invalidReferences,
101
+ inputErrors
102
+ });
103
+ });
104
+
105
+ const evidence = buildEvidenceEntries(allEvidence);
106
+ const evidenceRefsBySubtask = groupEvidenceRefsBySubtask(evidence);
107
+ const internalPerSubtask = preliminarySubtasks
108
+ .map(item => ({
109
+ ...item,
110
+ evidenceRefs: evidenceRefsBySubtask.get(item.subtaskId) || []
111
+ }))
112
+ .sort((a, b) => compareStrings(a.subtaskId, b.subtaskId));
113
+ const perSubtask = internalPerSubtask.map(({ verificationRequired, ...item }) => item);
114
+
115
+ const missingEvidence = collectMissingEvidence({
116
+ taskId,
117
+ classification,
118
+ verifierResult,
119
+ perSubtask: internalPerSubtask,
120
+ evidence
121
+ });
122
+ const warnings = uniqueSorted([
123
+ ...taskWarnings,
124
+ ...perSubtask.flatMap(item => item.warnings)
125
+ ]);
126
+ const codeFailures = uniqueSorted(perSubtask.flatMap(item => item.isCodeFailure ? item.failures : []));
127
+ const infrastructureFailures = uniqueSorted(perSubtask.flatMap(item => item.infrastructureFailure ? item.failures : []));
128
+ const failures = uniqueSorted([...codeFailures, ...infrastructureFailures]);
129
+ const conflicts = uniqueSorted(perSubtask
130
+ .filter(item => item.observedState === 'contradictory')
131
+ .map(item => `Subtask ${item.subtaskId} has conflicting execution signals`));
132
+ const inconsistencies = uniqueSorted([
133
+ ...perSubtask
134
+ .filter(item => item.observedState === 'contradictory')
135
+ .map(item => `Subtask ${item.subtaskId} contradicts runStatus ${String(item.runStatus)}`),
136
+ ...invalidReferences
137
+ ]);
138
+ const remainingIssues = uniqueSorted([
139
+ ...failures,
140
+ ...missingEvidence.map(item => `${item.subtaskId}: ${item.reason}`),
141
+ ...invalidReferences,
142
+ ...inputErrors
143
+ ]);
144
+ const consolidationCompleteness = decideCompleteness(inputErrors, invalidReferences, perSubtask);
145
+ const { status, recommendedNextAction } = decideStatus({
146
+ inputErrors,
147
+ perSubtask: internalPerSubtask,
148
+ warnings,
149
+ classification,
150
+ verifierResult
151
+ });
152
+
153
+ const verifierInputHints = {
154
+ requiresIndependentCheck: status !== 'accepted' || warnings.length > 0 || missingEvidence.length > 0,
155
+ criticalWithoutVerifier: classification.risk === 'critical' && verifierResult === null
156
+ };
157
+
158
+ const result = {
159
+ schemaVersion: SCHEMA_VERSION,
160
+ taskId,
161
+ status,
162
+ consolidationCompleteness,
163
+ perSubtask,
164
+ evidence,
165
+ missingEvidence,
166
+ warnings,
167
+ failures,
168
+ codeFailures,
169
+ infrastructureFailures,
170
+ conflicts,
171
+ inconsistencies,
172
+ invalidReferences,
173
+ remainingIssues,
174
+ recommendedNextAction,
175
+ verifierInputHints,
176
+ summary: '',
177
+ inputErrors
178
+ };
179
+ return { ...result, summary: summarizeConsolidation(result) };
180
+ }
181
+
182
+ export function validateConsolidation(result) {
183
+ const errors = [];
184
+ const warnings = [];
185
+ try {
186
+ if (!isPlainObject(result)) {
187
+ return { isValid: false, errors: ['result must be an object'], warnings };
188
+ }
189
+ if (result.schemaVersion !== SCHEMA_VERSION) {
190
+ errors.push('schemaVersion must be 1');
191
+ }
192
+ if (typeof result.taskId !== 'string' || result.taskId.length === 0) {
193
+ errors.push('taskId must be a non-empty string');
194
+ }
195
+ if (!CONSOLIDATION_STATUSES.includes(result.status)) {
196
+ errors.push('status is not a recognized consolidation status');
197
+ }
198
+ if (!COMPLETENESS_VALUES.includes(result.consolidationCompleteness)) {
199
+ errors.push('consolidationCompleteness is not recognized');
200
+ }
201
+ for (const field of [
202
+ 'perSubtask',
203
+ 'evidence',
204
+ 'missingEvidence',
205
+ 'warnings',
206
+ 'failures',
207
+ 'codeFailures',
208
+ 'infrastructureFailures',
209
+ 'conflicts',
210
+ 'inconsistencies',
211
+ 'invalidReferences',
212
+ 'remainingIssues',
213
+ 'inputErrors'
214
+ ]) {
215
+ if (!Array.isArray(result[field])) {
216
+ errors.push(`${field} must be an array`);
217
+ }
218
+ }
219
+ if (!NEXT_ACTIONS.includes(result.recommendedNextAction)) {
220
+ errors.push('recommendedNextAction is not recognized');
221
+ }
222
+ if (!isPlainObject(result.verifierInputHints)) {
223
+ errors.push('verifierInputHints must be an object');
224
+ }
225
+ if (typeof result.summary !== 'string' || result.summary.length === 0) {
226
+ warnings.push('summary is missing or empty');
227
+ }
228
+ if (Array.isArray(result.failures) && Array.isArray(result.codeFailures) && Array.isArray(result.infrastructureFailures)) {
229
+ const expectedFailures = uniqueSorted([...result.codeFailures, ...result.infrastructureFailures]);
230
+ if (!arrayEqual(result.failures, expectedFailures)) {
231
+ errors.push('failures must be the union of codeFailures and infrastructureFailures');
232
+ }
233
+ }
234
+ } catch (error) {
235
+ errors.push(`validation failed: ${error && error.message ? error.message : String(error)}`);
236
+ }
237
+ return { isValid: errors.length === 0, errors, warnings };
238
+ }
239
+
240
+ export function summarizeConsolidation(result) {
241
+ try {
242
+ if (!isPlainObject(result)) {
243
+ return 'Consolidation result summary unavailable: result is not an object';
244
+ }
245
+ if (!CONSOLIDATION_STATUSES.includes(result.status)) {
246
+ return 'Consolidation result summary unavailable: unknown status';
247
+ }
248
+ if (!COMPLETENESS_VALUES.includes(result.consolidationCompleteness)) {
249
+ return 'Consolidation result summary unavailable: unknown completeness';
250
+ }
251
+ const perSubtask = Array.isArray(result.perSubtask) ? result.perSubtask : [];
252
+ const issueCount = countArray(result.failures) + countArray(result.missingEvidence) +
253
+ countArray(result.invalidReferences) + countArray(result.inputErrors) + countArray(result.conflicts);
254
+ return `Consolidation ${result.status}: ${perSubtask.length} subtask(s), completeness ${result.consolidationCompleteness}, ${issueCount} issue(s).`;
255
+ } catch (error) {
256
+ return `Consolidation result summary unavailable: ${error && error.message ? error.message : 'unknown error'}`;
257
+ }
258
+ }
259
+
260
+ function assertConsolidationInput(input) {
261
+ if (!isPlainObject(input)) {
262
+ throw new TypeError('input must be an object');
263
+ }
264
+ if (!isPlainObject(input.task) || input.task.id === undefined || input.task.id === null) {
265
+ throw new TypeError('input.task must be an object with id');
266
+ }
267
+ for (const field of ['subtasks', 'subtaskResults', 'acceptanceCriteria', 'warnings', 'provenance', 'fallbackResults']) {
268
+ if (input[field] !== undefined && !Array.isArray(input[field])) {
269
+ throw new TypeError(`input.${field} must be an array`);
270
+ }
271
+ }
272
+ const subtaskResults = input.subtaskResults || [];
273
+ subtaskResults.forEach((item, index) => {
274
+ if (!isPlainObject(item)) {
275
+ throw new TypeError(`input.subtaskResults[${index}] must be an object`);
276
+ }
277
+ if (!isNonBlankString(item.subtaskId)) {
278
+ throw new TypeError(`input.subtaskResults[${index}].subtaskId must be a non-empty string`);
279
+ }
280
+ });
281
+ }
282
+
283
+ function collectInputErrors(input, subtasks, taskWarnings, provenanceErrors) {
284
+ const errors = [];
285
+ if (typeof input.task.description !== 'string' || input.task.description.length === 0) {
286
+ errors.push('task.description must be a non-empty string');
287
+ }
288
+ if (typeof input.task.status !== 'string' || !TASK_STATUSES.includes(input.task.status)) {
289
+ errors.push(`task.status is invalid: ${String(input.task.status)}`);
290
+ }
291
+ if (hasDuplicate(subtasks.map(subtask => subtask.id))) {
292
+ errors.push('subtasks contains duplicate id');
293
+ }
294
+ addArrayItemTypeErrors(errors, input.acceptanceCriteria || [], 'acceptanceCriteria', 'string');
295
+ addArrayItemTypeErrors(errors, taskWarnings, 'warnings', 'string');
296
+ errors.push(...provenanceErrors);
297
+ (input.subtaskResults || []).forEach((item, index) => {
298
+ if (item.evidence !== undefined && !Array.isArray(item.evidence)) {
299
+ errors.push(`subtaskResults[${index}].evidence must be an array`);
300
+ }
301
+ if (item.warnings !== undefined && !Array.isArray(item.warnings)) {
302
+ errors.push(`subtaskResults[${index}].warnings must be an array`);
303
+ }
304
+ });
305
+ return uniqueSorted(errors);
306
+ }
307
+
308
+ function normalizeSubtasks(subtasks) {
309
+ return [...subtasks]
310
+ .filter(isPlainObject)
311
+ .map((subtask, index) => ({
312
+ raw: subtask,
313
+ id: typeof subtask.id === 'string' ? subtask.id : String(subtask.id ?? `missing-id-${index}`),
314
+ verificationRequired: subtask.verificationRequired === true,
315
+ noChangeExpected: subtask.noChangeExpected === true || (isPlainObject(subtask.requires) && subtask.requires.noChangeExpected === true)
316
+ }))
317
+ .sort((a, b) => compareStrings(a.id, b.id) || compareStrings(canonicalStringify(a.raw), canonicalStringify(b.raw)));
318
+ }
319
+
320
+ function normalizeSubtaskResults(results) {
321
+ return [...results]
322
+ .map((result, index) => ({ ...result, originalIndex: index }))
323
+ .sort((a, b) => compareStrings(a.subtaskId, b.subtaskId) ||
324
+ compareNullableStrings(a.runId, b.runId) ||
325
+ compareNullableStrings(a.runStatus, b.runStatus) ||
326
+ compareNullableStrings(a.noExecutionReason, b.noExecutionReason) ||
327
+ compareStrings(canonicalStringify(a), canonicalStringify(b)));
328
+ }
329
+
330
+ function normalizeStringArray(values) {
331
+ return values.filter(value => typeof value === 'string');
332
+ }
333
+
334
+ function normalizeEvidenceArray(values, { inputErrors, owner }) {
335
+ const entries = [];
336
+ values.forEach((item, index) => {
337
+ if (!isPlainObject(item)) {
338
+ inputErrors.push(`${owner}[${index}] must be an object`);
339
+ return;
340
+ }
341
+ if (!EVIDENCE_TYPES.includes(item.type)) {
342
+ inputErrors.push(`${owner}[${index}].type is invalid`);
343
+ return;
344
+ }
345
+ if (!isNonBlankString(item.sourceId)) {
346
+ inputErrors.push(`${owner}[${index}].sourceId must be a non-empty string`);
347
+ return;
348
+ }
349
+ entries.push({
350
+ sequence: index,
351
+ type: item.type,
352
+ sourceId: item.sourceId,
353
+ field: typeof item.field === 'string' ? item.field : null,
354
+ value: cloneData(item.value)
355
+ });
356
+ });
357
+ return { entries, inputErrors };
358
+ }
359
+
360
+ function collectReferenceErrors(taskId, subtasks, results) {
361
+ const errors = [];
362
+ const subtaskIds = new Set(subtasks.map(subtask => subtask.id));
363
+ const resultSubtaskIds = results.map(result => result.subtaskId);
364
+ for (const id of duplicateValues(resultSubtaskIds)) {
365
+ errors.push(`duplicate subtaskResult subtaskId: ${id}`);
366
+ }
367
+ const runOwners = new Map();
368
+ for (const result of results) {
369
+ if (isNonBlankString(result.runId)) {
370
+ const owner = runOwners.get(result.runId);
371
+ if (owner && owner !== result.subtaskId) {
372
+ errors.push(`duplicate runId ${result.runId} for subtasks ${owner} and ${result.subtaskId}`);
373
+ }
374
+ runOwners.set(result.runId, result.subtaskId);
375
+ }
376
+ if (subtasks.length > 0) {
377
+ if (!subtaskIds.has(result.subtaskId)) {
378
+ errors.push(`orphan subtaskResult reference: ${result.subtaskId}`);
379
+ }
380
+ } else if (result.subtaskId !== taskId) {
381
+ errors.push(`orphan subtaskResult reference: ${result.subtaskId}`);
382
+ }
383
+ const validity = validateRunAssociation(result);
384
+ if (!validity.valid) {
385
+ errors.push(`invalid run association for ${result.subtaskId}: ${validity.reason}`);
386
+ }
387
+ }
388
+ return uniqueSorted(errors);
389
+ }
390
+
391
+ function firstResultBySubtask(results) {
392
+ const map = new Map();
393
+ for (const result of results) {
394
+ if (!map.has(result.subtaskId)) {
395
+ map.set(result.subtaskId, result);
396
+ }
397
+ }
398
+ return map;
399
+ }
400
+
401
+ function parentSubtaskIds(taskId, results) {
402
+ return results.some(result => result.subtaskId === taskId) ? [taskId] : [];
403
+ }
404
+
405
+ function buildSubtaskConsolidation({ taskId, subtaskId, subtask, result, allEvidence, invalidReferences, inputErrors }) {
406
+ if (!result) {
407
+ return {
408
+ subtaskId,
409
+ runId: null,
410
+ runStatus: null,
411
+ noExecutionReason: 'result_missing',
412
+ observedState: 'missing',
413
+ isCodeFailure: false,
414
+ infrastructureFailure: false,
415
+ warnings: [],
416
+ failures: [],
417
+ evidenceRefs: [],
418
+ missingEvidence: ['Subtask result is missing'],
419
+ verificationRequired: subtask ? subtask.verificationRequired : true
420
+ };
421
+ }
422
+
423
+ const evidenceErrors = [];
424
+ const evidence = normalizeEvidenceArray(result.evidence || [], {
425
+ inputErrors: evidenceErrors,
426
+ owner: `subtaskResults[${result.originalIndex}].evidence`
427
+ });
428
+ inputErrors.push(...evidenceErrors);
429
+ for (const entry of evidence.entries) {
430
+ allEvidence.push({
431
+ rawSubtaskId: result.subtaskId || taskId,
432
+ rawRunId: normalizeRunId(result.runId),
433
+ originSequence: entry.sequence,
434
+ type: entry.type,
435
+ sourceId: entry.sourceId,
436
+ field: entry.field,
437
+ value: entry.value
438
+ });
439
+ }
440
+
441
+ const validity = validateRunAssociation(result);
442
+ const observedState = validity.valid
443
+ ? observeState(result, evidence.entries, subtask)
444
+ : 'invalid';
445
+ const infrastructureFailure = observedState === 'failed' && result.infrastructureFailure === true;
446
+ const isCodeFailure = observedState === 'failed' && result.infrastructureFailure !== true;
447
+ const warnings = normalizeStringArray(result.warnings || []);
448
+ if (result.runStatus === 'completed_with_warnings') {
449
+ warnings.push(`Subtask ${subtaskId} completed with warnings`);
450
+ }
451
+ const failures = [];
452
+ if (isCodeFailure) {
453
+ failures.push(`Subtask ${subtaskId} failed`);
454
+ }
455
+ if (infrastructureFailure) {
456
+ failures.push(`Subtask ${subtaskId} failed due to infrastructure`);
457
+ }
458
+ const missingEvidence = [];
459
+ for (const entry of evidence.entries) {
460
+ if (entry.type === 'test-result' && isPlainObject(entry.value) && entry.value.ran === false) {
461
+ missingEvidence.push('Tests were not executed');
462
+ }
463
+ }
464
+ if (observedState === 'invalid' && !invalidReferences.some(ref => ref.includes(subtaskId))) {
465
+ invalidReferences.push(`invalid run association for ${subtaskId}: ${validity.reason}`);
466
+ }
467
+ return {
468
+ subtaskId,
469
+ runId: normalizeRunId(result.runId),
470
+ runStatus: result.runStatus ?? null,
471
+ noExecutionReason: result.noExecutionReason ?? null,
472
+ observedState,
473
+ isCodeFailure,
474
+ infrastructureFailure,
475
+ warnings: uniqueSorted(warnings),
476
+ failures: uniqueSorted(failures),
477
+ evidenceRefs: [],
478
+ missingEvidence: uniqueSorted(missingEvidence),
479
+ verificationRequired: subtask ? subtask.verificationRequired : true
480
+ };
481
+ }
482
+
483
+ function validateRunAssociation(result) {
484
+ const runId = result.runId;
485
+ const runStatus = result.runStatus;
486
+ const reason = result.noExecutionReason;
487
+ const evidence = Array.isArray(result.evidence) ? result.evidence : [];
488
+ if (typeof runId === 'string' && runId.trim().length === 0) {
489
+ return { valid: false, reason: 'runId must be a non-empty string or null' };
490
+ }
491
+ if (isNonBlankString(runId)) {
492
+ if (runStatus === 'completed' || runStatus === 'completed_with_warnings' || runStatus === 'failed') {
493
+ return reason === undefined
494
+ ? { valid: true, reason: null }
495
+ : { valid: false, reason: 'executed result must not include noExecutionReason' };
496
+ }
497
+ if (runStatus === null || runStatus === undefined) {
498
+ return { valid: false, reason: 'runId present without runStatus' };
499
+ }
500
+ if (runStatus === 'blocked' || runStatus === 'needs_human') {
501
+ return { valid: false, reason: 'blocked or needs_human with runId is not allowed' };
502
+ }
503
+ return { valid: false, reason: `unknown runStatus ${String(runStatus)}` };
504
+ }
505
+ if (runId === null) {
506
+ if (evidence.some(evidenceDeclaresExecution)) {
507
+ return { valid: false, reason: 'runId null is incompatible with evidence that declares execution' };
508
+ }
509
+ if (runStatus === 'blocked' && reason === 'blocked_before_execution') {
510
+ return { valid: true, reason: null };
511
+ }
512
+ if (runStatus === 'needs_human' && reason === 'needs_human_before_execution') {
513
+ return { valid: true, reason: null };
514
+ }
515
+ if (runStatus === null && (reason === 'not_executed' || reason === 'result_missing')) {
516
+ return { valid: true, reason: null };
517
+ }
518
+ if (runStatus === 'completed' || runStatus === 'completed_with_warnings' || runStatus === 'failed') {
519
+ return { valid: false, reason: 'execution status requires non-empty runId' };
520
+ }
521
+ if ((runStatus === 'blocked' || runStatus === 'needs_human') && reason === undefined) {
522
+ return { valid: false, reason: 'noExecutionReason is required before execution' };
523
+ }
524
+ return { valid: false, reason: `invalid no-execution combination ${String(runStatus)}/${String(reason)}` };
525
+ }
526
+ return { valid: false, reason: 'runId key must be a non-empty string or null' };
527
+ }
528
+
529
+ function evidenceDeclaresExecution(item) {
530
+ if (!isPlainObject(item)) {
531
+ return false;
532
+ }
533
+ if (item.type === 'workspace-diff' || item.type === 'host-validation' || item.type === 'syntax-validation') {
534
+ return true;
535
+ }
536
+ if (item.type === 'test-result' && isPlainObject(item.value) && item.value.ran === true) {
537
+ return true;
538
+ }
539
+ if (item.type === 'failure-classification' && isPlainObject(item.value)) {
540
+ return item.value.category === 'syntax_failure' ||
541
+ item.value.category === 'test_failure' ||
542
+ item.value.category === 'unknown_failure' ||
543
+ item.value.infrastructureFailure === true;
544
+ }
545
+ return false;
546
+ }
547
+
548
+ function observeState(result, evidence, subtask) {
549
+ if (result.runId === null) {
550
+ if (result.runStatus === 'blocked' || result.runStatus === 'needs_human') {
551
+ return result.runStatus;
552
+ }
553
+ return 'missing';
554
+ }
555
+ if (hasContradiction(result, evidence, subtask)) {
556
+ return 'contradictory';
557
+ }
558
+ return result.runStatus;
559
+ }
560
+
561
+ function hasContradiction(result, evidence, subtask) {
562
+ if (result.runStatus !== 'completed' && result.runStatus !== 'completed_with_warnings') {
563
+ return false;
564
+ }
565
+ const noChangeExpected = result.noChangeExpected === true ||
566
+ result.expectedNoChange === true ||
567
+ (subtask && subtask.noChangeExpected === true);
568
+ return evidence.some(entry => {
569
+ if (!isPlainObject(entry.value)) {
570
+ return false;
571
+ }
572
+ if (entry.type === 'syntax-validation' && entry.value.syntaxValidationFailed === true) {
573
+ return true;
574
+ }
575
+ if (entry.type === 'workspace-diff' && entry.value.workspaceChanged === false && !noChangeExpected) {
576
+ return true;
577
+ }
578
+ if (entry.type === 'test-result' && entry.value.ran === true && entry.value.passed === false) {
579
+ return true;
580
+ }
581
+ if (entry.type === 'host-validation' && entry.value.result === 'failed') {
582
+ return true;
583
+ }
584
+ return false;
585
+ });
586
+ }
587
+
588
+ function buildEvidenceEntries(rawEvidence) {
589
+ return [...rawEvidence]
590
+ .sort((a, b) => compareStrings(a.rawSubtaskId, b.rawSubtaskId) ||
591
+ compareNullableStrings(a.rawRunId, b.rawRunId) ||
592
+ a.originSequence - b.originSequence ||
593
+ compareStrings(a.type, b.type) ||
594
+ compareStrings(a.sourceId, b.sourceId))
595
+ .map((entry, index) => ({
596
+ id: `${entry.rawSubtaskId}:${entry.rawRunId ?? 'none'}:${index}`,
597
+ subtaskId: entry.rawSubtaskId,
598
+ runId: entry.rawRunId,
599
+ sequence: entry.originSequence,
600
+ type: entry.type,
601
+ sourceId: entry.sourceId,
602
+ field: entry.field ?? null,
603
+ value: entry.value
604
+ }));
605
+ }
606
+
607
+ function groupEvidenceRefsBySubtask(evidence) {
608
+ const grouped = new Map();
609
+ for (const entry of evidence) {
610
+ const current = grouped.get(entry.subtaskId) || [];
611
+ current.push(entry.id);
612
+ grouped.set(entry.subtaskId, current);
613
+ }
614
+ return grouped;
615
+ }
616
+
617
+ function collectMissingEvidence({ taskId, classification, verifierResult, perSubtask }) {
618
+ const entries = [];
619
+ if (classification.risk === 'critical' && verifierResult === null) {
620
+ entries.push({
621
+ subtaskId: taskId,
622
+ kind: 'verifier-result',
623
+ reason: 'Critical task requires verifier result'
624
+ });
625
+ }
626
+ for (const item of perSubtask) {
627
+ if (item.observedState === 'missing') {
628
+ entries.push({
629
+ subtaskId: item.subtaskId,
630
+ kind: 'subtask-result',
631
+ reason: 'Subtask result is missing'
632
+ });
633
+ }
634
+ for (const reason of item.missingEvidence) {
635
+ entries.push({
636
+ subtaskId: item.subtaskId,
637
+ kind: 'evidence',
638
+ reason
639
+ });
640
+ }
641
+ }
642
+ return entries.sort((a, b) => compareStrings(a.subtaskId, b.subtaskId) ||
643
+ compareStrings(a.kind, b.kind) ||
644
+ compareStrings(a.reason, b.reason));
645
+ }
646
+
647
+ function decideCompleteness(inputErrors, invalidReferences, perSubtask) {
648
+ if (inputErrors.length > 0) {
649
+ return 'invalid';
650
+ }
651
+ if (invalidReferences.length > 0 || perSubtask.some(item => ['missing', 'contradictory', 'invalid'].includes(item.observedState))) {
652
+ return 'partial';
653
+ }
654
+ return 'complete';
655
+ }
656
+
657
+ function decideStatus({ inputErrors, perSubtask, warnings, classification, verifierResult }) {
658
+ if (inputErrors.length > 0) {
659
+ return { status: 'inconclusive', recommendedNextAction: 'verify' };
660
+ }
661
+ if (perSubtask.some(item => item.observedState === 'invalid')) {
662
+ return { status: 'inconclusive', recommendedNextAction: 'verify' };
663
+ }
664
+ if (perSubtask.some(item => item.observedState === 'needs_human') ||
665
+ (classification.risk === 'critical' && verifierResult === null)) {
666
+ return { status: 'needs_human', recommendedNextAction: 'ask_human' };
667
+ }
668
+ if (perSubtask.some(item => item.observedState === 'blocked')) {
669
+ return { status: 'needs_human', recommendedNextAction: 'ask_human' };
670
+ }
671
+ if (perSubtask.some(item => item.observedState === 'missing' && item.verificationRequired !== false)) {
672
+ return { status: 'inconclusive', recommendedNextAction: 'verify' };
673
+ }
674
+ if (perSubtask.some(item => item.observedState === 'contradictory')) {
675
+ return { status: 'inconclusive', recommendedNextAction: 'verify' };
676
+ }
677
+ if (perSubtask.some(item => item.isCodeFailure)) {
678
+ return { status: 'rejected', recommendedNextAction: 'fix' };
679
+ }
680
+ if (perSubtask.some(item => item.infrastructureFailure)) {
681
+ return { status: 'inconclusive', recommendedNextAction: 'verify' };
682
+ }
683
+ if (perSubtask.some(item => item.observedState === 'missing')) {
684
+ return { status: 'accepted_with_warnings', recommendedNextAction: 'verify' };
685
+ }
686
+ if (perSubtask.length === 0) {
687
+ return { status: 'inconclusive', recommendedNextAction: 'verify' };
688
+ }
689
+ if (perSubtask.every(item => item.observedState === 'completed' || item.observedState === 'completed_with_warnings')) {
690
+ if (warnings.length > 0 || perSubtask.some(item => item.observedState === 'completed_with_warnings')) {
691
+ return { status: 'accepted_with_warnings', recommendedNextAction: 'verify' };
692
+ }
693
+ return { status: 'accepted', recommendedNextAction: 'complete' };
694
+ }
695
+ return { status: 'inconclusive', recommendedNextAction: 'verify' };
696
+ }
697
+
698
+ function addArrayItemTypeErrors(errors, values, field, type) {
699
+ values.forEach((value, index) => {
700
+ if (typeof value !== type) {
701
+ errors.push(`${field}[${index}] must be a ${type}`);
702
+ }
703
+ });
704
+ }
705
+
706
+ function normalizeRunId(runId) {
707
+ return isNonBlankString(runId) ? runId : null;
708
+ }
709
+
710
+ function hasDuplicate(values) {
711
+ return duplicateValues(values).length > 0;
712
+ }
713
+
714
+ function duplicateValues(values) {
715
+ const seen = new Set();
716
+ const duplicates = new Set();
717
+ for (const value of values) {
718
+ if (seen.has(value)) {
719
+ duplicates.add(value);
720
+ }
721
+ seen.add(value);
722
+ }
723
+ return [...duplicates].sort(compareStrings);
724
+ }
725
+
726
+ function uniqueSorted(values) {
727
+ return [...new Set(values.filter(value => typeof value === 'string'))].sort(compareStrings);
728
+ }
729
+
730
+ function arrayEqual(left, right) {
731
+ return left.length === right.length && left.every((value, index) => value === right[index]);
732
+ }
733
+
734
+ function countArray(value) {
735
+ return Array.isArray(value) ? value.length : 0;
736
+ }
737
+
738
+ function isPlainObject(value) {
739
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
740
+ }
741
+
742
+ function isNonBlankString(value) {
743
+ return typeof value === 'string' && value.trim().length > 0;
744
+ }
745
+
746
+ function cloneData(value) {
747
+ if (Array.isArray(value)) {
748
+ return value.map(cloneData);
749
+ }
750
+ if (isPlainObject(value)) {
751
+ return Object.fromEntries(Object.keys(value).map(key => [key, cloneData(value[key])]));
752
+ }
753
+ return value;
754
+ }
755
+
756
+ function compareStrings(a, b) {
757
+ return String(a).localeCompare(String(b));
758
+ }
759
+
760
+ function compareNullableStrings(a, b) {
761
+ return String(a ?? '').localeCompare(String(b ?? ''));
762
+ }
763
+
764
+ function canonicalStringify(value) {
765
+ if (Array.isArray(value)) {
766
+ return `[${value.map(canonicalStringify).join(',')}]`;
767
+ }
768
+ if (isPlainObject(value)) {
769
+ return `{${Object.keys(value).sort(compareStrings).map(key => `${key}:${canonicalStringify(value[key])}`).join(',')}}`;
770
+ }
771
+ return JSON.stringify(value);
772
+ }