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,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
+ }