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,135 @@
|
|
|
1
|
+
function cloneData(value) {
|
|
2
|
+
if (value === undefined) return undefined;
|
|
3
|
+
return JSON.parse(JSON.stringify(value));
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function subtaskId(subtask) {
|
|
7
|
+
return String(subtask && subtask.id);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function dependencyList(subtask) {
|
|
11
|
+
return Array.isArray(subtask && subtask.blockedBy) ? subtask.blockedBy.map(String) : [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function sortedBySubtaskId(values) {
|
|
15
|
+
return [...values].sort((a, b) => subtaskId(a).localeCompare(subtaskId(b)));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function conflictSubtaskIds(conflict) {
|
|
19
|
+
const ids = [];
|
|
20
|
+
if (conflict && conflict.subtaskId !== undefined && conflict.subtaskId !== null) {
|
|
21
|
+
ids.push(String(conflict.subtaskId));
|
|
22
|
+
}
|
|
23
|
+
if (conflict && Array.isArray(conflict.subtaskIds)) {
|
|
24
|
+
for (const id of conflict.subtaskIds) ids.push(String(id));
|
|
25
|
+
}
|
|
26
|
+
return ids;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hasBlockingWaveConflict(subtask, waveConflicts = []) {
|
|
30
|
+
const id = subtaskId(subtask);
|
|
31
|
+
return waveConflicts.some(conflict => {
|
|
32
|
+
if (!['file-write-conflict', 'sensitive-path', 'missing-expected-files'].includes(conflict && conflict.type)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return conflictSubtaskIds(conflict).includes(id);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function computeWaves(subtasks) {
|
|
40
|
+
if (!Array.isArray(subtasks)) {
|
|
41
|
+
throw new TypeError('subtasks must be an array');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ordered = sortedBySubtaskId(subtasks);
|
|
45
|
+
const byId = new Map();
|
|
46
|
+
for (const subtask of ordered) {
|
|
47
|
+
const id = subtaskId(subtask);
|
|
48
|
+
if (byId.has(id)) {
|
|
49
|
+
throw new Error('duplicate subtask id in orchestration plan: ' + id);
|
|
50
|
+
}
|
|
51
|
+
byId.set(id, subtask);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const subtask of ordered) {
|
|
55
|
+
for (const dependency of dependencyList(subtask)) {
|
|
56
|
+
if (!byId.has(dependency)) {
|
|
57
|
+
throw new Error('unresolved subtask dependency: ' + subtaskId(subtask) + ' blockedBy ' + dependency);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const remaining = new Set(ordered.map(subtaskId));
|
|
63
|
+
const completed = new Set();
|
|
64
|
+
const waves = [];
|
|
65
|
+
|
|
66
|
+
while (remaining.size > 0) {
|
|
67
|
+
const readyIds = [...remaining]
|
|
68
|
+
.filter(id => dependencyList(byId.get(id)).every(dependency => completed.has(dependency)))
|
|
69
|
+
.sort((a, b) => a.localeCompare(b));
|
|
70
|
+
|
|
71
|
+
if (readyIds.length === 0) {
|
|
72
|
+
throw new Error('dependency cycle remained after 03C planning');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const waveSubtasks = readyIds.map(id => cloneData(byId.get(id)));
|
|
76
|
+
waves.push({
|
|
77
|
+
waveIndex: waves.length,
|
|
78
|
+
subtaskIds: readyIds,
|
|
79
|
+
subtasks: waveSubtasks
|
|
80
|
+
});
|
|
81
|
+
for (const id of readyIds) {
|
|
82
|
+
remaining.delete(id);
|
|
83
|
+
completed.add(id);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return waves;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function classifySubtask(subtask, waveConflicts = []) {
|
|
91
|
+
const files = Array.isArray(subtask && subtask.expectedFiles) ? subtask.expectedFiles : [];
|
|
92
|
+
const requires = subtask && subtask.requires;
|
|
93
|
+
if (!requires || requires.filesystemWrite !== false) {
|
|
94
|
+
return 'serial-required';
|
|
95
|
+
}
|
|
96
|
+
if (files.length > 0) {
|
|
97
|
+
return 'serial-required';
|
|
98
|
+
}
|
|
99
|
+
if (hasBlockingWaveConflict(subtask, waveConflicts)) {
|
|
100
|
+
return 'serial-required';
|
|
101
|
+
}
|
|
102
|
+
return 'parallel-eligible';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function planWaveExecution(wave, subtasks, limits = {}) {
|
|
106
|
+
const subtasksById = new Map((Array.isArray(subtasks) ? subtasks : []).map(subtask => [subtaskId(subtask), subtask]));
|
|
107
|
+
const sourceSubtasks = Array.isArray(wave && wave.subtasks)
|
|
108
|
+
? wave.subtasks
|
|
109
|
+
: Array.isArray(wave)
|
|
110
|
+
? wave
|
|
111
|
+
: Array.isArray(wave && wave.subtaskIds)
|
|
112
|
+
? wave.subtaskIds.map(id => subtasksById.get(String(id))).filter(Boolean)
|
|
113
|
+
: [];
|
|
114
|
+
const ordered = sortedBySubtaskId(sourceSubtasks);
|
|
115
|
+
const waveConflicts = Array.isArray(limits.waveConflicts)
|
|
116
|
+
? limits.waveConflicts
|
|
117
|
+
: Array.isArray(limits.conflicts)
|
|
118
|
+
? limits.conflicts
|
|
119
|
+
: [];
|
|
120
|
+
|
|
121
|
+
if (ordered.length <= 1) {
|
|
122
|
+
return { parallel: [], serial: ordered.map(cloneData) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const parallel = [];
|
|
126
|
+
const serial = [];
|
|
127
|
+
for (const subtask of ordered) {
|
|
128
|
+
if (classifySubtask(subtask, waveConflicts) === 'parallel-eligible') {
|
|
129
|
+
parallel.push(cloneData(subtask));
|
|
130
|
+
} else {
|
|
131
|
+
serial.push(cloneData(subtask));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { parallel, serial };
|
|
135
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { CONFIG } from '../config.mjs';
|
|
4
|
+
import { pathExists } from '../files.mjs';
|
|
5
|
+
import { redactObject } from '../format.mjs';
|
|
6
|
+
import { stampSchemaVersion, validateOrchestrationEvent } from '../schema.mjs';
|
|
7
|
+
|
|
8
|
+
const trailFilePath = path.join(CONFIG.maestroPath, 'ORCHESTRATIONS.jsonl');
|
|
9
|
+
|
|
10
|
+
function cloneData(value) {
|
|
11
|
+
if (value === undefined) return undefined;
|
|
12
|
+
return JSON.parse(JSON.stringify(value));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeChild(child) {
|
|
16
|
+
return {
|
|
17
|
+
subtaskId: child && child.subtaskId !== undefined && child.subtaskId !== null ? String(child.subtaskId) : null,
|
|
18
|
+
taskId: child && child.taskId !== undefined ? child.taskId : null,
|
|
19
|
+
runId: child && child.runId ? child.runId : null,
|
|
20
|
+
status: child && child.status ? child.status : 'unknown'
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeConsolidation(consolidation) {
|
|
25
|
+
if (!consolidation || typeof consolidation !== 'object') return null;
|
|
26
|
+
return {
|
|
27
|
+
status: consolidation.status || null,
|
|
28
|
+
consolidationCompleteness: consolidation.consolidationCompleteness || null
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeVerification(verification) {
|
|
33
|
+
if (!verification || typeof verification !== 'object') return null;
|
|
34
|
+
return {
|
|
35
|
+
verificationOutcome: verification.verificationOutcome || verification.outcome || null
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeStrings(values) {
|
|
40
|
+
return Array.isArray(values) ? values.map(value => String(value)) : [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeTrace(values) {
|
|
44
|
+
return Array.isArray(values) ? cloneData(values) : [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeConcurrency(concurrency) {
|
|
48
|
+
if (!concurrency || typeof concurrency !== 'object' || Array.isArray(concurrency)) return undefined;
|
|
49
|
+
const result = {};
|
|
50
|
+
if (concurrency.maxConcurrent !== undefined) result.maxConcurrent = Number(concurrency.maxConcurrent);
|
|
51
|
+
if (concurrency.observedMaxActive !== undefined) result.observedMaxActive = Number(concurrency.observedMaxActive);
|
|
52
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeWaves(waves) {
|
|
56
|
+
if (!Array.isArray(waves)) return undefined;
|
|
57
|
+
return waves.map(wave => ({
|
|
58
|
+
waveIndex: Number(wave && wave.waveIndex),
|
|
59
|
+
parallelSubtaskIds: normalizeStrings(wave && wave.parallelSubtaskIds),
|
|
60
|
+
serialSubtaskIds: normalizeStrings(wave && wave.serialSubtaskIds)
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function executionFields(event) {
|
|
65
|
+
const fields = {};
|
|
66
|
+
if (event && (event.strategy === 'serial' || event.strategy === 'controlled-parallel')) {
|
|
67
|
+
fields.strategy = event.strategy;
|
|
68
|
+
}
|
|
69
|
+
const concurrency = normalizeConcurrency(event && event.concurrency);
|
|
70
|
+
if (concurrency) fields.concurrency = concurrency;
|
|
71
|
+
const waves = normalizeWaves(event && event.waves);
|
|
72
|
+
if (waves) fields.waves = waves;
|
|
73
|
+
return fields;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeEvent(event) {
|
|
77
|
+
const eventType = event && event.eventType;
|
|
78
|
+
const timestamp = event && event.timestamp ? event.timestamp : new Date().toISOString();
|
|
79
|
+
const base = stampSchemaVersion({
|
|
80
|
+
orchestrationId: event && event.orchestrationId !== undefined && event.orchestrationId !== null ? String(event.orchestrationId) : event && event.orchestrationId,
|
|
81
|
+
parentTaskId: event && event.parentTaskId !== undefined && event.parentTaskId !== null ? String(event.parentTaskId) : event && event.parentTaskId,
|
|
82
|
+
attempt: Number(event && event.attempt),
|
|
83
|
+
mode: 'real',
|
|
84
|
+
eventType,
|
|
85
|
+
startTime: event && event.startTime ? event.startTime : timestamp,
|
|
86
|
+
timestamp
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (eventType === 'orchestration_blocked') {
|
|
90
|
+
return {
|
|
91
|
+
...base,
|
|
92
|
+
status: event.status === 'needs_human' ? 'needs_human' : 'blocked',
|
|
93
|
+
reason: String(event.reason || ''),
|
|
94
|
+
trace: normalizeTrace(event.trace),
|
|
95
|
+
warnings: normalizeStrings(event.warnings),
|
|
96
|
+
...executionFields(event)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (eventType === 'orchestration_finished') {
|
|
101
|
+
return {
|
|
102
|
+
...base,
|
|
103
|
+
endTime: event.endTime || timestamp,
|
|
104
|
+
status: event.status || 'blocked',
|
|
105
|
+
children: Array.isArray(event.children) ? event.children.map(normalizeChild) : [],
|
|
106
|
+
consolidation: normalizeConsolidation(event.consolidation),
|
|
107
|
+
verification: normalizeVerification(event.verification),
|
|
108
|
+
warnings: normalizeStrings(event.warnings),
|
|
109
|
+
reason: String(event.reason || ''),
|
|
110
|
+
trace: normalizeTrace(event.trace),
|
|
111
|
+
...executionFields(event)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { ...base, ...executionFields(event) };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function appendOrchestrationEvent(event) {
|
|
119
|
+
const normalized = normalizeEvent(cloneData(event || {}));
|
|
120
|
+
const stamped = cloneData(redactObject(normalized));
|
|
121
|
+
const result = validateOrchestrationEvent(stamped);
|
|
122
|
+
if (!result.ok) {
|
|
123
|
+
console.warn('Orchestration event for task #' + stamped.parentTaskId + ' failed schema validation: ' + result.errors.join(', '));
|
|
124
|
+
}
|
|
125
|
+
await fs.mkdir(CONFIG.maestroPath, { recursive: true });
|
|
126
|
+
if (await pathExists(trailFilePath)) {
|
|
127
|
+
await fs.copyFile(trailFilePath, trailFilePath + '.bak');
|
|
128
|
+
}
|
|
129
|
+
await fs.appendFile(trailFilePath, JSON.stringify(stamped) + '\n', { encoding: 'utf8' });
|
|
130
|
+
return cloneData(stamped);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function getOrchestrationEvents(parentTaskId) {
|
|
134
|
+
try {
|
|
135
|
+
const content = await fs.readFile(trailFilePath, 'utf-8');
|
|
136
|
+
return content
|
|
137
|
+
.split(/\r?\n/)
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.map(line => {
|
|
140
|
+
try {
|
|
141
|
+
const event = JSON.parse(line);
|
|
142
|
+
const validation = validateOrchestrationEvent(event);
|
|
143
|
+
if (!validation.ok) {
|
|
144
|
+
return { ...event, parseError: 'schema validation failed: ' + validation.errors.join(', '), raw: line };
|
|
145
|
+
}
|
|
146
|
+
return event;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return { parseError: error.message, raw: line };
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
.filter(event => parentTaskId === undefined || parentTaskId === null || event.parseError || String(event.parentTaskId) === String(parentTaskId));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error.code === 'ENOENT') {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function getLatestOrchestrationForTask(parentTaskId) {
|
|
161
|
+
const events = await getOrchestrationEvents(parentTaskId);
|
|
162
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
163
|
+
const event = events[index];
|
|
164
|
+
if (event && !event.parseError && (event.eventType === 'orchestration_finished' || event.eventType === 'orchestration_blocked')) {
|
|
165
|
+
return cloneData(event);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function buildOrchestrationSummary(event) {
|
|
172
|
+
if (!event || event.parseError) return null;
|
|
173
|
+
return {
|
|
174
|
+
orchestrationId: event.orchestrationId,
|
|
175
|
+
parentTaskId: event.parentTaskId,
|
|
176
|
+
attempt: event.attempt,
|
|
177
|
+
mode: event.mode,
|
|
178
|
+
eventType: event.eventType,
|
|
179
|
+
status: event.status || null,
|
|
180
|
+
startTime: event.startTime || null,
|
|
181
|
+
endTime: event.endTime || null,
|
|
182
|
+
timestamp: event.timestamp || null,
|
|
183
|
+
children: Array.isArray(event.children) ? event.children.map(normalizeChild) : [],
|
|
184
|
+
consolidation: normalizeConsolidation(event.consolidation),
|
|
185
|
+
verification: normalizeVerification(event.verification),
|
|
186
|
+
warnings: normalizeStrings(event.warnings),
|
|
187
|
+
reason: event.reason || null,
|
|
188
|
+
...(event.strategy ? { strategy: event.strategy } : {}),
|
|
189
|
+
...(event.concurrency ? { concurrency: normalizeConcurrency(event.concurrency) } : {}),
|
|
190
|
+
...(event.waves ? { waves: normalizeWaves(event.waves) } : {})
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { hasTool, isCostAcceptable, isUnknownField, summarizeConfidence } from './capability-registry.mjs';
|
|
2
|
+
|
|
3
|
+
export const PREFLIGHT_DECISIONS = [
|
|
4
|
+
'ACCEPT', 'DECLINE', 'DELEGATE', 'NEEDS_CAPABILITY', 'NEEDS_CONTEXT', 'NEEDS_HUMAN', 'UNAVAILABLE'
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
const REQUIRED_TOOL_KEYS = ['shell', 'filesystemWrite', 'browser', 'vision', 'testExecution'];
|
|
8
|
+
|
|
9
|
+
// vision has no direct 'tools.vision' key in the ficha shape; it lives under
|
|
10
|
+
// capabilities.vision and tools.imageInput. Map classifier requirement keys
|
|
11
|
+
// to the concrete ficha field(s) that must be true.
|
|
12
|
+
function toolCheck(engineRecord, requirementKey) {
|
|
13
|
+
if (requirementKey === 'vision') {
|
|
14
|
+
return engineRecord.capabilities && engineRecord.capabilities.vision === true;
|
|
15
|
+
}
|
|
16
|
+
if (requirementKey === 'browser') {
|
|
17
|
+
return hasTool(engineRecord, 'browser');
|
|
18
|
+
}
|
|
19
|
+
if (requirementKey === 'testExecution') {
|
|
20
|
+
return hasTool(engineRecord, 'testExecution');
|
|
21
|
+
}
|
|
22
|
+
if (requirementKey === 'filesystemWrite') {
|
|
23
|
+
return hasTool(engineRecord, 'filesystemWrite');
|
|
24
|
+
}
|
|
25
|
+
if (requirementKey === 'shell') {
|
|
26
|
+
return hasTool(engineRecord, 'shell');
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Only blocks on a numeric shortfall when BOTH the requirement and the
|
|
32
|
+
// engine's declared capability are known numbers. An engine with an
|
|
33
|
+
// unrated (null/'unknown') capability is a scoring/confidence concern for
|
|
34
|
+
// engine-selector.mjs, not an automatic preflight rejection here — this
|
|
35
|
+
// project never treats "we don't know" the same as "confirmed too weak".
|
|
36
|
+
function findMissingNumericCapabilities(engineRecord, requires) {
|
|
37
|
+
const missing = [];
|
|
38
|
+
const caps = engineRecord.capabilities || {};
|
|
39
|
+
for (const key of ['reasoning', 'coding']) {
|
|
40
|
+
const required = requires[key];
|
|
41
|
+
const have = caps[key];
|
|
42
|
+
if (typeof required === 'number' && typeof have === 'number' && have < required) {
|
|
43
|
+
missing.push(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return missing;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findMissingCapabilities(engineRecord, requires) {
|
|
50
|
+
const missing = [];
|
|
51
|
+
for (const key of REQUIRED_TOOL_KEYS) {
|
|
52
|
+
if (requires[key] === true && !toolCheck(engineRecord, key)) {
|
|
53
|
+
missing.push(key);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return [...missing, ...findMissingNumericCapabilities(engineRecord, requires)];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function confidenceToScore(level) {
|
|
60
|
+
if (level === 'confirmed') return 0.9;
|
|
61
|
+
if (level === 'probable') return 0.6;
|
|
62
|
+
return 0.3;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Deterministic, ficha-only preflight. The Maestro must never trust a
|
|
66
|
+
// model's self-reported capability over this — see runSelfReportedPreflight
|
|
67
|
+
// below for the (optional, short, non-authoritative) model self-check.
|
|
68
|
+
export function runPreflight(task, engineRecord, classification, options = {}) {
|
|
69
|
+
const allowPaid = options.allowPaid === true;
|
|
70
|
+
const allowCritical = options.allowCritical === true;
|
|
71
|
+
|
|
72
|
+
// Accept either a raw engine record or the describeEngine() wrapper shape.
|
|
73
|
+
const record = engineRecord && engineRecord.engineRecord ? engineRecord.engineRecord : engineRecord;
|
|
74
|
+
|
|
75
|
+
if (!record) {
|
|
76
|
+
return {
|
|
77
|
+
decision: 'UNAVAILABLE',
|
|
78
|
+
confidence: 0.9,
|
|
79
|
+
reason: 'Engine not found in capability registry.',
|
|
80
|
+
missingCapabilities: [],
|
|
81
|
+
recommendedRole: null,
|
|
82
|
+
recommendedSuccessor: null,
|
|
83
|
+
suggestedSubtasks: []
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (record.enabled !== true) {
|
|
88
|
+
return {
|
|
89
|
+
decision: 'UNAVAILABLE',
|
|
90
|
+
confidence: 0.9,
|
|
91
|
+
reason: 'Engine "' + record.id + '" is disabled in .maestro/engines.json.',
|
|
92
|
+
missingCapabilities: [],
|
|
93
|
+
recommendedRole: null,
|
|
94
|
+
recommendedSuccessor: null,
|
|
95
|
+
suggestedSubtasks: []
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (record.health && record.health.available === false) {
|
|
100
|
+
return {
|
|
101
|
+
decision: 'UNAVAILABLE',
|
|
102
|
+
confidence: 0.8,
|
|
103
|
+
reason: 'Engine "' + record.id + '" is currently marked unavailable by health tracking.',
|
|
104
|
+
missingCapabilities: [],
|
|
105
|
+
recommendedRole: null,
|
|
106
|
+
recommendedSuccessor: null,
|
|
107
|
+
suggestedSubtasks: []
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const allowUnknownCost = options.allowUnknownCost !== false;
|
|
112
|
+
if (!isCostAcceptable(record, { allowPaid, allowUnknownCost })) {
|
|
113
|
+
return {
|
|
114
|
+
decision: 'DECLINE',
|
|
115
|
+
confidence: 0.85,
|
|
116
|
+
reason: 'Engine "' + record.id + '" cost class is "' + (record.cost ? record.cost.class : 'unknown') +
|
|
117
|
+
'" and is not allowed for this run (allowPaid=' + allowPaid + ', allowUnknownCost=' + allowUnknownCost + ').',
|
|
118
|
+
missingCapabilities: [],
|
|
119
|
+
recommendedRole: null,
|
|
120
|
+
recommendedSuccessor: null,
|
|
121
|
+
suggestedSubtasks: []
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (classification.risk === 'critical' && !allowCritical) {
|
|
126
|
+
return {
|
|
127
|
+
decision: 'NEEDS_HUMAN',
|
|
128
|
+
confidence: 0.7,
|
|
129
|
+
reason: 'Task risk is "critical" (taskKind=' + classification.taskKind + ', complexity=' + classification.complexity + '). Autonomous execution requires explicit allowCritical opt-in or human sign-off.',
|
|
130
|
+
missingCapabilities: [],
|
|
131
|
+
recommendedRole: 'human',
|
|
132
|
+
recommendedSuccessor: null,
|
|
133
|
+
suggestedSubtasks: []
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const missingCapabilities = findMissingCapabilities(record, classification.requires || {});
|
|
138
|
+
if (missingCapabilities.length > 0) {
|
|
139
|
+
return {
|
|
140
|
+
decision: 'NEEDS_CAPABILITY',
|
|
141
|
+
confidence: 0.75,
|
|
142
|
+
reason: 'Engine "' + record.id + '" is missing required capabilities: ' + missingCapabilities.join(', ') + '.',
|
|
143
|
+
missingCapabilities,
|
|
144
|
+
recommendedRole: null,
|
|
145
|
+
recommendedSuccessor: null,
|
|
146
|
+
suggestedSubtasks: []
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const requiredContext = classification.requires ? classification.requires.minimumContextTokens : null;
|
|
151
|
+
const maxContext = record.limits ? record.limits.maxContextTokens : null;
|
|
152
|
+
if (typeof requiredContext === 'number' && typeof maxContext === 'number' && maxContext < requiredContext) {
|
|
153
|
+
return {
|
|
154
|
+
decision: 'NEEDS_CONTEXT',
|
|
155
|
+
confidence: 0.8,
|
|
156
|
+
reason: 'Task needs at least ' + requiredContext + ' context tokens; engine "' + record.id + '" declares a max of ' + maxContext + '.',
|
|
157
|
+
missingCapabilities: [],
|
|
158
|
+
recommendedRole: null,
|
|
159
|
+
recommendedSuccessor: null,
|
|
160
|
+
suggestedSubtasks: []
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (classification.canDelegate && classification.complexity >= 3 && !(record.roles || []).includes('manager') && options.skipDelegationCheck !== true) {
|
|
165
|
+
return {
|
|
166
|
+
decision: 'DELEGATE',
|
|
167
|
+
confidence: 0.55,
|
|
168
|
+
reason: 'Task complexity ' + classification.complexity + ' (taskKind=' + classification.taskKind + ') benefits from decomposition; "' + record.id + '" is a worker, not a manager. Route through a sector manager first (see src/orchestration/sector-managers.mjs).',
|
|
169
|
+
missingCapabilities: [],
|
|
170
|
+
recommendedRole: 'manager',
|
|
171
|
+
recommendedSuccessor: null,
|
|
172
|
+
suggestedSubtasks: []
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const overallConfidence = confidenceToScore(summarizeConfidence(record));
|
|
177
|
+
return {
|
|
178
|
+
decision: 'ACCEPT',
|
|
179
|
+
confidence: overallConfidence,
|
|
180
|
+
reason: 'Engine "' + record.id + '" satisfies required tools, cost policy, context, and risk gates for this task.',
|
|
181
|
+
missingCapabilities: [],
|
|
182
|
+
recommendedRole: 'worker',
|
|
183
|
+
recommendedSuccessor: null,
|
|
184
|
+
suggestedSubtasks: []
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Optional, short, NON-AUTHORITATIVE self-report from the model itself. Per
|
|
189
|
+
// project policy, this can only ever narrow a decision the deterministic
|
|
190
|
+
// preflight already reached (e.g. surface a caveat), never widen it — a
|
|
191
|
+
// model claiming a capability the ficha says it lacks must be ignored.
|
|
192
|
+
export function reconcileSelfReport(deterministicResult, selfReport) {
|
|
193
|
+
if (!selfReport || typeof selfReport !== 'object') {
|
|
194
|
+
return deterministicResult;
|
|
195
|
+
}
|
|
196
|
+
const claims = selfReport.claims || {};
|
|
197
|
+
const contradicted = Object.entries(claims).filter(([key, value]) => {
|
|
198
|
+
return value === true && deterministicResult.missingCapabilities.includes(key);
|
|
199
|
+
});
|
|
200
|
+
if (contradicted.length > 0) {
|
|
201
|
+
return {
|
|
202
|
+
...deterministicResult,
|
|
203
|
+
reason: deterministicResult.reason + ' (model self-report claimed ' +
|
|
204
|
+
contradicted.map(([key]) => key).join(', ') + ' despite ficha saying otherwise; ignored — ficha wins.)'
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return deterministicResult;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function isUnknownRequirement(value) {
|
|
211
|
+
return isUnknownField(value);
|
|
212
|
+
}
|