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,262 @@
|
|
|
1
|
+
// src/orchestration/delegation-executor.mjs
|
|
2
|
+
// Responsabilidade: Criar plano de delegação, não executar ainda.
|
|
3
|
+
|
|
4
|
+
import { selectSectorManager, shouldUseManager } from './sector-managers.mjs';
|
|
5
|
+
import { planSubtasks, validateSubtaskPlan } from './subtask-planner.mjs';
|
|
6
|
+
import { detectFileConflicts } from './file-conflict-detector.mjs';
|
|
7
|
+
|
|
8
|
+
// detectFileConflicts moved to file-conflict-detector.mjs (single source of
|
|
9
|
+
// truth, per spec section 4); re-exported here so existing importers of
|
|
10
|
+
// this module keep working.
|
|
11
|
+
export { detectFileConflicts };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Plano de delegação para uma tarefa.
|
|
15
|
+
* @param {object} input - Objeto contendo task, classification, projectContext, etc.
|
|
16
|
+
* @returns {object} Plano de delegação estruturado.
|
|
17
|
+
*/
|
|
18
|
+
export async function planDelegation(input) {
|
|
19
|
+
const {
|
|
20
|
+
task,
|
|
21
|
+
classification,
|
|
22
|
+
projectContext = {},
|
|
23
|
+
engines = [],
|
|
24
|
+
history = []
|
|
25
|
+
} = input;
|
|
26
|
+
|
|
27
|
+
// Decidir se precisa delegação
|
|
28
|
+
if (!classification || !shouldUseManager(classification)) {
|
|
29
|
+
return {
|
|
30
|
+
decision: 'NO_DELEGATION',
|
|
31
|
+
manager: null,
|
|
32
|
+
subtasks: [],
|
|
33
|
+
assignments: [],
|
|
34
|
+
executionMode: 'serial',
|
|
35
|
+
requiresHuman: false,
|
|
36
|
+
warnings: [`Tarefa não atende critérios para delegação. Complexidade: ${classification?.complexity || 'unknown'}, Risk: ${classification?.risk || 'unknown'}`]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Selecionar gerente
|
|
41
|
+
const manager = selectSectorManager(classification);
|
|
42
|
+
if (!manager) {
|
|
43
|
+
return {
|
|
44
|
+
decision: 'NO_DELEGATION',
|
|
45
|
+
manager: null,
|
|
46
|
+
subtasks: [],
|
|
47
|
+
assignments: [],
|
|
48
|
+
executionMode: 'serial',
|
|
49
|
+
requiresHuman: false,
|
|
50
|
+
warnings: [`Nenhum gerente de setor aplicável para taskKind: ${classification.taskKind}`]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Planejar subtarefas
|
|
55
|
+
const subtaskPlanInput = {
|
|
56
|
+
task,
|
|
57
|
+
classification,
|
|
58
|
+
manager,
|
|
59
|
+
projectContext,
|
|
60
|
+
constraints: {
|
|
61
|
+
maxSubtasks: manager.maxSubtasks || 4,
|
|
62
|
+
maxDepth: 2
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const subtaskPlan = planSubtasks(subtaskPlanInput);
|
|
67
|
+
|
|
68
|
+
if (!subtaskPlan.canSplit) {
|
|
69
|
+
// Task não será dividida, mas ainda pode ser delegada ao manager
|
|
70
|
+
return {
|
|
71
|
+
decision: 'PLAN_ONLY',
|
|
72
|
+
manager,
|
|
73
|
+
subtasks: [],
|
|
74
|
+
assignments: [
|
|
75
|
+
{
|
|
76
|
+
subtaskId: task.id,
|
|
77
|
+
role: 'worker',
|
|
78
|
+
candidateEngine: selectBestEngine(classification, engines, history),
|
|
79
|
+
reason: [`Delegada ao gerente ${manager.id} para execução direta.`]
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
executionMode: 'serial',
|
|
83
|
+
requiresHuman: classification.risk === 'critical',
|
|
84
|
+
warnings: subtaskPlan.warnings || []
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Validar plano de subtarefas
|
|
89
|
+
const validation = validateSubtaskPlan(subtaskPlan);
|
|
90
|
+
if (!validation.isValid) {
|
|
91
|
+
return {
|
|
92
|
+
decision: 'NO_DELEGATION',
|
|
93
|
+
manager,
|
|
94
|
+
subtasks: [],
|
|
95
|
+
assignments: [],
|
|
96
|
+
executionMode: 'serial',
|
|
97
|
+
requiresHuman: true,
|
|
98
|
+
warnings: validation.errors
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Detectar conflitos de arquivo
|
|
103
|
+
const fileConflicts = detectFileConflicts(subtaskPlan.subtasks);
|
|
104
|
+
let executionMode = 'serial'; // v0.5.2: sempre serial
|
|
105
|
+
if (fileConflicts.length > 0) {
|
|
106
|
+
// Conflito força serial
|
|
107
|
+
executionMode = 'serial';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Criar assignments
|
|
111
|
+
const assignments = subtaskPlan.subtasks.map(subtask => {
|
|
112
|
+
let role = 'worker';
|
|
113
|
+
if (subtask.verificationRequired && subtask === subtaskPlan.subtasks[subtaskPlan.subtasks.length - 1]) {
|
|
114
|
+
role = 'verifier';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const engine = selectBestEngine(subtask, engines, history);
|
|
118
|
+
return {
|
|
119
|
+
subtaskId: subtask.id,
|
|
120
|
+
role,
|
|
121
|
+
candidateEngine: engine,
|
|
122
|
+
reason: [
|
|
123
|
+
`Subtarefa de tipo ${subtask.taskKind}`,
|
|
124
|
+
`Risco: ${subtask.risk}`,
|
|
125
|
+
`Motor candidato: ${engine}`
|
|
126
|
+
]
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const requiresHuman =
|
|
131
|
+
classification.risk === 'critical' ||
|
|
132
|
+
(manager.requiresHumanFor || []).some(cond => classification.taskKind === cond);
|
|
133
|
+
|
|
134
|
+
const warnings = subtaskPlan.warnings || [];
|
|
135
|
+
if (fileConflicts.length > 0) {
|
|
136
|
+
warnings.push(`${fileConflicts.length} conflito(s) de arquivo detectado(s). Executar em modo serial.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
decision: 'READY',
|
|
141
|
+
manager,
|
|
142
|
+
subtasks: subtaskPlan.subtasks,
|
|
143
|
+
assignments,
|
|
144
|
+
executionMode,
|
|
145
|
+
requiresHuman,
|
|
146
|
+
warnings
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Seleciona o melhor motor para uma tarefa/subtarefa.
|
|
152
|
+
* Versão simplificada: retorna o primeiro motor válido.
|
|
153
|
+
*/
|
|
154
|
+
function selectBestEngine(classification, engines = [], history = []) {
|
|
155
|
+
if (!engines || engines.length === 0) {
|
|
156
|
+
return 'codex-default';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const available = engines.filter(e => e.enabled === true);
|
|
160
|
+
if (available.length === 0) {
|
|
161
|
+
return 'codex-default';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const preferred = available.find(e => e.id === 'codex9' || e.id === 'codex8');
|
|
165
|
+
return preferred ? preferred.id : available[0].id;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Valida um plano de delegação.
|
|
170
|
+
*/
|
|
171
|
+
export function validateDelegationPlan(plan) {
|
|
172
|
+
const errors = [];
|
|
173
|
+
const warnings = [];
|
|
174
|
+
|
|
175
|
+
if (!plan || typeof plan !== 'object') {
|
|
176
|
+
errors.push('Plano inválido: nulo ou não é um objeto.');
|
|
177
|
+
return { isValid: false, errors, warnings };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const validDecisions = ['NO_DELEGATION', 'PLAN_ONLY', 'READY'];
|
|
181
|
+
if (!validDecisions.includes(plan.decision)) {
|
|
182
|
+
errors.push(`Campo decision inválido. Deve ser um de: ${validDecisions.join(', ')}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (plan.decision === 'READY' && !plan.manager) {
|
|
186
|
+
errors.push('Decisão READY requer um gerente válido.');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!Array.isArray(plan.subtasks)) {
|
|
190
|
+
errors.push('Campo subtasks deve ser um array.');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!Array.isArray(plan.assignments)) {
|
|
194
|
+
errors.push('Campo assignments deve ser um array.');
|
|
195
|
+
} else {
|
|
196
|
+
plan.assignments.forEach((assignment, idx) => {
|
|
197
|
+
if (!assignment.subtaskId || typeof assignment.subtaskId !== 'string') {
|
|
198
|
+
errors.push(`Assignment ${idx}: subtaskId inválido.`);
|
|
199
|
+
}
|
|
200
|
+
if (!['worker', 'reviewer', 'fixer', 'verifier'].includes(assignment.role)) {
|
|
201
|
+
errors.push(`Assignment ${idx}: role inválida.`);
|
|
202
|
+
}
|
|
203
|
+
if (!assignment.candidateEngine || typeof assignment.candidateEngine !== 'string') {
|
|
204
|
+
errors.push(`Assignment ${idx}: candidateEngine inválido.`);
|
|
205
|
+
}
|
|
206
|
+
if (!Array.isArray(assignment.reason)) {
|
|
207
|
+
errors.push(`Assignment ${idx}: reason deve ser um array.`);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (plan.executionMode !== 'serial') {
|
|
213
|
+
warnings.push(`v0.5.2: executionMode deve ser 'serial', encontrado '${plan.executionMode}'.`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (typeof plan.requiresHuman !== 'boolean') {
|
|
217
|
+
errors.push('Campo requiresHuman deve ser um booleano.');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!Array.isArray(plan.warnings)) {
|
|
221
|
+
errors.push('Campo warnings deve ser um array.');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { isValid: errors.length === 0, errors, warnings };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Gera um resumo legível de um plano de delegação.
|
|
229
|
+
*/
|
|
230
|
+
export function summarizeDelegationPlan(plan) {
|
|
231
|
+
if (!plan) {
|
|
232
|
+
return 'Plano de delegação inválido.';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let summary = `Plano de Delegação:\n`;
|
|
236
|
+
summary += `Decisão: ${plan.decision}\n`;
|
|
237
|
+
|
|
238
|
+
if (plan.manager) {
|
|
239
|
+
summary += `Gerente: ${plan.manager.displayName || plan.manager.id || 'desconhecido'}\n`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
summary += `Subtarefas: ${plan.subtasks?.length || 0}\n`;
|
|
243
|
+
summary += `Assignments: ${plan.assignments?.length || 0}\n`;
|
|
244
|
+
summary += `Modo de Execução: ${plan.executionMode || 'serial'}\n`;
|
|
245
|
+
summary += `Requer Intervenção Humana: ${plan.requiresHuman ? 'Sim' : 'Não'}\n`;
|
|
246
|
+
|
|
247
|
+
if (plan.assignments && plan.assignments.length > 0) {
|
|
248
|
+
summary += `\nAssignments:\n`;
|
|
249
|
+
plan.assignments.forEach((assignment, idx) => {
|
|
250
|
+
summary += ` ${idx + 1}. ${assignment.subtaskId} → ${assignment.candidateEngine} (role: ${assignment.role})\n`;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (plan.warnings && plan.warnings.length > 0) {
|
|
255
|
+
summary += `\nAvisos:\n`;
|
|
256
|
+
plan.warnings.forEach(w => {
|
|
257
|
+
summary += `- ${w}\n`;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return summary;
|
|
262
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { CONFIG } from '../config.mjs';
|
|
4
|
+
import { stampSchemaVersion } from '../schema.mjs';
|
|
5
|
+
import { planSectorWork } from './sector-managers.mjs';
|
|
6
|
+
|
|
7
|
+
const delegationLogPath = path.join(CONFIG.maestroPath, 'delegation-log.jsonl');
|
|
8
|
+
|
|
9
|
+
// Hard limits from the spec (Phase 9). These are enforced here, not just
|
|
10
|
+
// documented — buildDelegationPlan() below refuses to exceed them rather
|
|
11
|
+
// than trusting callers to check first.
|
|
12
|
+
export const DELEGATION_LIMITS = {
|
|
13
|
+
maxDepth: 2,
|
|
14
|
+
maxSubagentsPerManager: 4,
|
|
15
|
+
maxAutoSubtasksPerTask: 8,
|
|
16
|
+
maxEnginesTriedPerTask: 3,
|
|
17
|
+
maxRetryPerEngine: 1
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function getDelegationLogPath() {
|
|
21
|
+
return delegationLogPath;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function logDelegationEvent(entry) {
|
|
25
|
+
await fs.mkdir(CONFIG.maestroPath, { recursive: true });
|
|
26
|
+
const stamped = stampSchemaVersion({
|
|
27
|
+
timestamp: new Date().toISOString(),
|
|
28
|
+
...entry
|
|
29
|
+
});
|
|
30
|
+
await fs.appendFile(delegationLogPath, JSON.stringify(stamped) + '\n');
|
|
31
|
+
return stamped;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Two subtasks conflict if they declare overlapping `files`. Conflicting
|
|
35
|
+
// subtasks MUST run sequentially (never concurrently) in this release —
|
|
36
|
+
// v0.5.0 does not implement parallel writes at all (see
|
|
37
|
+
// .maestro/SUBAGENT_POLICY.md), so this function currently only exists to
|
|
38
|
+
// make that guarantee inspectable/testable, not to unlock parallelism.
|
|
39
|
+
export function detectFileConflicts(subtasks) {
|
|
40
|
+
const conflicts = [];
|
|
41
|
+
for (let i = 0; i < subtasks.length; i++) {
|
|
42
|
+
for (let j = i + 1; j < subtasks.length; j++) {
|
|
43
|
+
const filesA = new Set(subtasks[i].files || []);
|
|
44
|
+
const filesB = subtasks[j].files || [];
|
|
45
|
+
const overlap = filesB.filter(file => filesA.has(file));
|
|
46
|
+
if (overlap.length > 0) {
|
|
47
|
+
conflicts.push({ a: i, b: j, files: overlap });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return conflicts;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// depth: how many delegation levels already happened above this call
|
|
55
|
+
// (0 = top-level task, 1 = subtask of a sector manager, 2 = would be a
|
|
56
|
+
// sub-subtask — refused, since maxDepth is 2 meaning "at most 2 manager
|
|
57
|
+
// hops from the original task").
|
|
58
|
+
export function buildDelegationPlan(task, classification, { depth = 0 } = {}) {
|
|
59
|
+
if (depth >= DELEGATION_LIMITS.maxDepth) {
|
|
60
|
+
return {
|
|
61
|
+
delegate: false,
|
|
62
|
+
sector: null,
|
|
63
|
+
reason: 'max delegation depth (' + DELEGATION_LIMITS.maxDepth + ') reached at depth=' + depth + '; task must execute directly or be declined.',
|
|
64
|
+
subtasks: []
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const plan = planSectorWork(task, classification);
|
|
69
|
+
if (plan.decision !== 'decompose') {
|
|
70
|
+
return { delegate: false, sector: plan.sector, reason: plan.reason, subtasks: [] };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let subtasks = plan.subtasks;
|
|
74
|
+
if (subtasks.length > DELEGATION_LIMITS.maxSubagentsPerManager) {
|
|
75
|
+
subtasks = subtasks.slice(0, DELEGATION_LIMITS.maxSubagentsPerManager);
|
|
76
|
+
}
|
|
77
|
+
if (subtasks.length > DELEGATION_LIMITS.maxAutoSubtasksPerTask) {
|
|
78
|
+
subtasks = subtasks.slice(0, DELEGATION_LIMITS.maxAutoSubtasksPerTask);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const conflicts = detectFileConflicts(subtasks);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
delegate: true,
|
|
85
|
+
sector: plan.sector,
|
|
86
|
+
reason: plan.reason,
|
|
87
|
+
subtasks,
|
|
88
|
+
fileConflicts: conflicts,
|
|
89
|
+
executionOrder: 'serial',
|
|
90
|
+
depth: depth + 1
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function recordDelegation({ task, plan, delegatedBy = 'maestro', outcome = null }) {
|
|
95
|
+
return logDelegationEvent({
|
|
96
|
+
taskId: task.id ?? null,
|
|
97
|
+
delegatedBy,
|
|
98
|
+
sector: plan.sector,
|
|
99
|
+
delegate: plan.delegate,
|
|
100
|
+
reason: plan.reason,
|
|
101
|
+
subtaskCount: plan.subtasks ? plan.subtasks.length : 0,
|
|
102
|
+
subtaskTitles: (plan.subtasks || []).map(subtask => subtask.title),
|
|
103
|
+
fileConflicts: plan.fileConflicts || [],
|
|
104
|
+
executionOrder: plan.executionOrder || null,
|
|
105
|
+
depth: plan.depth ?? 0,
|
|
106
|
+
outcome
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function withinEngineAttemptLimit(attemptCount) {
|
|
111
|
+
return attemptCount < DELEGATION_LIMITS.maxEnginesTriedPerTask;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function withinRetryLimit(retryCount) {
|
|
115
|
+
return retryCount < DELEGATION_LIMITS.maxRetryPerEngine;
|
|
116
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { stripAccents } from '../smart-planner.mjs';
|
|
2
|
+
|
|
3
|
+
// A bare "deploy"-shaped word is not enough on its own: acceptance criteria
|
|
4
|
+
// and task descriptions routinely mention deploy only to forbid it ("sem
|
|
5
|
+
// deploy", "nao fazer deploy", "deployment is out of scope"). This helper
|
|
6
|
+
// only reports an affirmative deploy intent when a deploy-action word
|
|
7
|
+
// appears in a clause that has no negation/exclusion cue anywhere in that
|
|
8
|
+
// same clause. Negation can precede ("nao fazer deploy") or follow ("deploy
|
|
9
|
+
// nao deve ser realizado") the action word, so a fixed look-behind window
|
|
10
|
+
// isn't enough -- splitting the text into clauses and checking each one as
|
|
11
|
+
// a whole handles both orders without depending solely on \bdeploy\b.
|
|
12
|
+
const DEPLOY_ACTION_PATTERN = /\bdeploy(ment)?\b|\bpublicar\b|\bimplantar\b|\bimplanta(c|ç)[aã]o\b|\bpush to prod|\brelease to prod|\bpublish to prod/;
|
|
13
|
+
const NEGATION_PATTERN = /\bnao\b|\bsem\b|\bnunca\b|\bjamais\b|\bnot\b|\bdon'?t\b|\bdo not\b/;
|
|
14
|
+
const EXCLUSION_PATTERN = /fora do escopo|out of scope|nao faz parte|not part of/;
|
|
15
|
+
|
|
16
|
+
function splitClauses(text) {
|
|
17
|
+
const parts = text.split(/[.,;:!?\n]+/).map(part => part.trim()).filter(Boolean);
|
|
18
|
+
return parts.length ? parts : [text];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function detectAffirmativeDeploymentIntent(rawText) {
|
|
22
|
+
const text = stripAccents(String(rawText || '').toLowerCase());
|
|
23
|
+
if (!DEPLOY_ACTION_PATTERN.test(text)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
for (const clause of splitClauses(text)) {
|
|
27
|
+
if (!DEPLOY_ACTION_PATTERN.test(clause)) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (NEGATION_PATTERN.test(clause) || EXCLUSION_PATTERN.test(clause)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
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 { stampSchemaVersion } from '../schema.mjs';
|
|
6
|
+
|
|
7
|
+
const historyPath = path.join(CONFIG.maestroPath, 'engine-history.jsonl');
|
|
8
|
+
|
|
9
|
+
export function getEngineHistoryPath() {
|
|
10
|
+
return historyPath;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// entry shape (all fields optional except engine/success):
|
|
14
|
+
// { engine, provider, model, taskKind, complexity, success, failureCategory,
|
|
15
|
+
// durationMs, estimatedTokens, estimatedCost, toolsAvailable, retries,
|
|
16
|
+
// fallbackUsed, verifierResult, runKind, taskId }
|
|
17
|
+
export async function recordEngineOutcome(entry) {
|
|
18
|
+
await fs.mkdir(CONFIG.maestroPath, { recursive: true });
|
|
19
|
+
const stamped = stampSchemaVersion({
|
|
20
|
+
timestamp: new Date().toISOString(),
|
|
21
|
+
...entry
|
|
22
|
+
});
|
|
23
|
+
if (await pathExists(historyPath)) {
|
|
24
|
+
await fs.copyFile(historyPath, historyPath + '.bak');
|
|
25
|
+
}
|
|
26
|
+
await fs.appendFile(historyPath, JSON.stringify(stamped) + '\n');
|
|
27
|
+
return stamped;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function readEngineHistory(limit = Infinity) {
|
|
31
|
+
try {
|
|
32
|
+
const content = await fs.readFile(historyPath, 'utf-8');
|
|
33
|
+
const rows = content
|
|
34
|
+
.split(/\r?\n/)
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map(line => {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(line);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return { parseError: error.message, raw: line };
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return Number.isFinite(limit) ? rows.slice(-limit) : rows;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error.code === 'ENOENT') {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function getHistoryForEngine(engineId, { limit = 50 } = {}) {
|
|
53
|
+
const rows = await readEngineHistory(Infinity);
|
|
54
|
+
return rows.filter(row => row.engine === engineId).slice(-limit);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function computeSuccessRate(entries) {
|
|
58
|
+
const valid = entries.filter(entry => typeof entry.success === 'boolean');
|
|
59
|
+
if (valid.length === 0) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return valid.filter(entry => entry.success === true).length / valid.length;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function toEngineSelectorHistory(entries) {
|
|
66
|
+
return entries
|
|
67
|
+
.filter(entry => typeof entry.success === 'boolean' && entry.engine)
|
|
68
|
+
.map(entry => ({ engine: entry.engine, success: entry.success }));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function summarizeHistoryByEngine() {
|
|
72
|
+
const rows = await readEngineHistory(Infinity);
|
|
73
|
+
const byEngine = {};
|
|
74
|
+
for (const row of rows) {
|
|
75
|
+
if (!row.engine) continue;
|
|
76
|
+
const bucket = byEngine[row.engine] || (byEngine[row.engine] = {
|
|
77
|
+
engine: row.engine,
|
|
78
|
+
totalRuns: 0,
|
|
79
|
+
successes: 0,
|
|
80
|
+
failures: 0,
|
|
81
|
+
failureCategories: {},
|
|
82
|
+
totalDurationMs: 0,
|
|
83
|
+
durationSamples: 0,
|
|
84
|
+
lastRunAt: null
|
|
85
|
+
});
|
|
86
|
+
bucket.totalRuns += 1;
|
|
87
|
+
if (row.success === true) bucket.successes += 1;
|
|
88
|
+
if (row.success === false) {
|
|
89
|
+
bucket.failures += 1;
|
|
90
|
+
const category = row.failureCategory || 'unknown_failure';
|
|
91
|
+
bucket.failureCategories[category] = (bucket.failureCategories[category] || 0) + 1;
|
|
92
|
+
}
|
|
93
|
+
if (typeof row.durationMs === 'number') {
|
|
94
|
+
bucket.totalDurationMs += row.durationMs;
|
|
95
|
+
bucket.durationSamples += 1;
|
|
96
|
+
}
|
|
97
|
+
if (!bucket.lastRunAt || row.timestamp > bucket.lastRunAt) {
|
|
98
|
+
bucket.lastRunAt = row.timestamp;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return Object.values(byEngine).map(bucket => ({
|
|
102
|
+
engine: bucket.engine,
|
|
103
|
+
totalRuns: bucket.totalRuns,
|
|
104
|
+
successRate: bucket.totalRuns > 0 ? bucket.successes / bucket.totalRuns : null,
|
|
105
|
+
failures: bucket.failures,
|
|
106
|
+
failureCategories: bucket.failureCategories,
|
|
107
|
+
averageDurationMs: bucket.durationSamples > 0 ? Math.round(bucket.totalDurationMs / bucket.durationSamples) : null,
|
|
108
|
+
lastRunAt: bucket.lastRunAt
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Categorical engine-usage policy (Phase 14), separate from budget-manager.mjs
|
|
2
|
+
// (which enforces numeric limits: runs/subagents/depth/tokens/currency).
|
|
3
|
+
// This module answers "is this engine even allowed to be used this way",
|
|
4
|
+
// independent of how much budget remains.
|
|
5
|
+
|
|
6
|
+
function envAllows(name) {
|
|
7
|
+
return process.env[name] === '1';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Only checks `id`/`provider` — short, structured slugs — not the free-text
|
|
11
|
+
// `model` field. Some fiches (e.g. ai-router) put a whole descriptive
|
|
12
|
+
// sentence in `model` ("delegates to whichever CLI ... fcc-claude"), and a
|
|
13
|
+
// naive substring check there false-positives on any engine that merely
|
|
14
|
+
// mentions Claude as one option among several, not one that IS Claude.
|
|
15
|
+
function looksLikeClaude(engineRecord) {
|
|
16
|
+
const id = String((engineRecord && engineRecord.id) || '').toLowerCase();
|
|
17
|
+
const provider = String((engineRecord && engineRecord.provider) || '').toLowerCase();
|
|
18
|
+
return id.includes('claude') || provider.includes('claude') || provider.includes('anthropic');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function looksLikeDirectOpenAI(engineRecord) {
|
|
22
|
+
const provider = String((engineRecord && engineRecord.provider) || '').toLowerCase();
|
|
23
|
+
const id = String((engineRecord && engineRecord.id) || '').toLowerCase();
|
|
24
|
+
// Explicitly excludes 9router-routed models even if the underlying model
|
|
25
|
+
// happens to be a GPT/OpenAI model — 9Router is the allowed path; only a
|
|
26
|
+
// provider that talks to OpenAI's API directly is gated by this rule.
|
|
27
|
+
if (provider === '9router' || id.startsWith('9router')) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return provider === 'openai' || provider === 'openai-direct' || id.includes('openai-direct');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// context: { role: 'worker'|'review'|'manager'|..., isClaudeReview: boolean,
|
|
34
|
+
// allowClaudeReviewFlag: boolean, allowDirectOpenAIFlag: boolean }
|
|
35
|
+
// The `*Flag` fields let a caller pass budget.json's allowClaudeReview /
|
|
36
|
+
// allowDirectOpenAI values in directly; the env vars are checked either way,
|
|
37
|
+
// matching the spec's "bloqueado sem <ENV>=1" language.
|
|
38
|
+
export function checkEnginePolicy(engineRecord, context = {}) {
|
|
39
|
+
const violations = [];
|
|
40
|
+
const warnings = [];
|
|
41
|
+
const role = context.role || 'worker';
|
|
42
|
+
|
|
43
|
+
if (looksLikeClaude(engineRecord)) {
|
|
44
|
+
if (role === 'worker' && context.isClaudeReview !== true) {
|
|
45
|
+
violations.push(
|
|
46
|
+
'Claude-family engine "' + (engineRecord && engineRecord.id) + '" must not be used as a default worker. ' +
|
|
47
|
+
'See .maestro/ENGINE_SELECTION_POLICY.md — Claude/premium review is opt-in only, never routine task execution.'
|
|
48
|
+
);
|
|
49
|
+
} else if (!envAllows('MAESTRO_ALLOW_CLAUDE_REVIEW') && context.allowClaudeReviewFlag !== true) {
|
|
50
|
+
violations.push(
|
|
51
|
+
'Claude review requires MAESTRO_ALLOW_CLAUDE_REVIEW=1 or .maestro/budget.json allowClaudeReview=true.'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (looksLikeDirectOpenAI(engineRecord)) {
|
|
57
|
+
if (!envAllows('MAESTRO_ALLOW_DIRECT_OPENAI') && context.allowDirectOpenAIFlag !== true) {
|
|
58
|
+
violations.push(
|
|
59
|
+
'Direct OpenAI access ("' + (engineRecord && engineRecord.id) + '", provider="' + (engineRecord && engineRecord.provider) + '") requires ' +
|
|
60
|
+
'MAESTRO_ALLOW_DIRECT_OPENAI=1 or .maestro/budget.json allowDirectOpenAI=true.'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (engineRecord && engineRecord.validated !== true) {
|
|
66
|
+
warnings.push(
|
|
67
|
+
'Engine "' + engineRecord.id + '" is not validated (validated=false in .maestro/engines.json); ' +
|
|
68
|
+
'treat any result with reduced confidence until it is explicitly validated (see "maestro engines validate").'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { ok: violations.length === 0, violations, warnings };
|
|
73
|
+
}
|