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,521 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { CONFIG } from './config.mjs';
4
+ import { pathExists, writeUtf8TextFile } from './files.mjs';
5
+ import { execFile, execCmdFile, execFileRaw } from './shell.mjs';
6
+ import { runNpx, resolveCodexInvocation, getCodexHelpMatrix, readValidatedCodexStrategy, resolveRunCodexStrategy, readCodexCompare, resolveCodexInvocationMode, resolveCodexInvocationForMode } from './commands.mjs';
7
+ import { buildSmartTasks } from './smart-planner.mjs';
8
+ import { ensureCodexHome, getLegacyWorkspacePermissionsConfigPath } from './codex-home.mjs';
9
+ import { getEffectiveCodexRuntime, formatEffectiveRuntimeLines } from './debug.mjs';
10
+ import { generateCheckpoint } from './checkpoint.mjs';
11
+ import { getRecentRuns } from './runner.mjs';
12
+ import { generatePlan } from './planner.mjs';
13
+ import { getTasks, sortRunnableTasks } from './tasks.mjs';
14
+ import { ensureSafetyConfig } from './safety.mjs';
15
+ import { getUsageSummary } from './usage.mjs';
16
+ import { logMemory, memoryDecision } from './memory-log.mjs';
17
+ import { doctorLine, summarizeText } from './format.mjs';
18
+ import { validateTask, validateRunEntry, validateUsageSummary } from './schema.mjs';
19
+ import { readJsonSafe } from './files.mjs';
20
+ import { codexHomeCommand, codexTestInvocation, hasRecentCodexTest } from './codex-diagnostics.mjs';
21
+ import { countTasksByStatus, getPlatformGuidance, decideTaskStatus } from './task-commands.mjs';
22
+ import { detectWindowsShellMismatch, validateChangedFile, isAnalysisOnlyTask } from './run-diagnostics.mjs';
23
+ import { snapshotWorkspace, diffWorkspace } from './workspace-diff.mjs';
24
+ import { getFullHealthSnapshot } from './orchestration/provider-health.mjs';
25
+ import os from 'os';
26
+
27
+ export function getHelpText() {
28
+ return [
29
+ 'AI-MAESTRO',
30
+ 'The project remembers. Workers execute.',
31
+ '',
32
+ 'Fluxo recomendado:',
33
+ '',
34
+ '1. maestro start "pedido"',
35
+ '2. maestro ui',
36
+ '3. maestro task list',
37
+ '4. maestro run-task <id>',
38
+ '5. maestro checkpoint',
39
+ '6. maestro handoff',
40
+ '',
41
+ 'Comandos principais:',
42
+ '',
43
+ '- maestro start "pedido"',
44
+ '- maestro ui',
45
+ '- maestro status',
46
+ '- maestro doctor',
47
+ '- maestro task list',
48
+ '- maestro task show <id>',
49
+ '- maestro run-one',
50
+ '- maestro run-task <id>',
51
+ '- maestro run-task <id> --failover [--dry-run] [--resume]',
52
+ '- maestro run-all --limit 3',
53
+ '- maestro run-log <run-id>',
54
+ '- maestro report <run-id>',
55
+ '- maestro checkpoint',
56
+ '- maestro handoff',
57
+ '- maestro release-check',
58
+ '- maestro readiness',
59
+ '- maestro provider-health [--json]',
60
+ '- maestro canary <path>',
61
+ '',
62
+ 'Exemplos:',
63
+ '',
64
+ ' maestro start "os botoes nao funcionam, termine de fazer o app"',
65
+ ' maestro run-one',
66
+ ' maestro run-one --quiet',
67
+ ' maestro ui',
68
+ '',
69
+ 'Use "maestro help", "maestro --help" ou "maestro -h" para ver esta mensagem novamente.'
70
+ ].join('\n');
71
+ }
72
+
73
+ export function printHelp() {
74
+ console.log(getHelpText());
75
+ }
76
+
77
+ export async function init() {
78
+ if (!(await pathExists(CONFIG.memoryPath))) {
79
+ const npxResult = await runNpx(['ai-universal-memory', 'init']);
80
+ if (npxResult.code !== 0) {
81
+ throw new Error('npx ai-universal-memory init failed: ' + npxResult.stderr);
82
+ }
83
+ }
84
+ await fs.mkdir(CONFIG.maestroPath, { recursive: true });
85
+ for (const file of ['TASKS.json', 'PLAN.md', 'RUNS.jsonl']) {
86
+ const filePath = path.join(CONFIG.maestroPath, file);
87
+ if (!(await pathExists(filePath))) {
88
+ await fs.writeFile(filePath, file === 'TASKS.json' ? '[]' : '');
89
+ }
90
+ }
91
+ await ensureSafetyConfig();
92
+ await ensureWorkerPolicy();
93
+ await logMemory('AI Maestro project initialized.');
94
+ console.log('AI Maestro initialization complete.');
95
+ }
96
+
97
+ export async function status() {
98
+ console.log('AI Maestro Status:');
99
+ doctorLine(await pathExists(CONFIG.memoryPath) ? 'OK' : 'AVISO', 'Memory path', CONFIG.memoryPath);
100
+ doctorLine(await pathExists(CONFIG.maestroPath) ? 'OK' : 'AVISO', 'Maestro path', CONFIG.maestroPath);
101
+ const tasks = await getTasks();
102
+ console.log('- Tasks: ' + tasks.length);
103
+ console.log('- Task status counts: ' + JSON.stringify(countTasksByStatus(tasks)));
104
+ const recentRuns = await getRecentRuns(10);
105
+ const withWarnings = recentRuns.filter(run => (run.warnings || []).length > 0);
106
+ console.log('- Recent runs with warnings: ' + withWarnings.length + '/' + recentRuns.length);
107
+ for (const run of withWarnings.slice(-5)) {
108
+ console.log(' - Task #' + run.taskId + ' [' + run.status + ']: ' + (run.warnings || []).join('; '));
109
+ }
110
+ }
111
+
112
+ export async function plan(request) {
113
+ let briefContent = '';
114
+ try {
115
+ briefContent = await fs.readFile(path.join(CONFIG.memoryPath, 'BRIEF.md'), 'utf-8');
116
+ } catch (error) {
117
+ briefContent = '';
118
+ }
119
+ const result = await generatePlan(request, briefContent);
120
+ if (result.archive && result.archive.archived.length > 0) {
121
+ await logMemory('Archived active tasks before plan: ' + result.archive.archived.length + ' to ' + result.archive.archivePath);
122
+ }
123
+ await logMemory('Planned for request: "' + request + '"');
124
+ await memoryDecision('Generated structured task plan for "' + request + '" without premium AI.');
125
+ console.log('Plan generated in .maestro/PLAN.md and .maestro/TASKS.json');
126
+ }
127
+
128
+ export const WORKER_POLICY_CONTENT = [
129
+ '# AI Maestro Worker Policy',
130
+ '',
131
+ '- Do not modify `.env` files.',
132
+ '- Do not deploy.',
133
+ '- Do not delete files.',
134
+ '- Do not modify the normal Codex home.',
135
+ '- Do not modify `C:\\AI-Router`.',
136
+ '- Record files changed in the final response.',
137
+ '- Run relevant validations.',
138
+ '- Log meaningful work to `.memory/` when possible.',
139
+ '',
140
+ '## Windows/PowerShell',
141
+ '',
142
+ '- Use `Get-ChildItem -Force` instead of `ls -la`.',
143
+ '- Use `Get-Content file | Select-Object -Last N` instead of `tail`.',
144
+ '- Do not use Bash heredoc (`cat > file << EOF`).',
145
+ '- To write large files, prefer a temporary Node script instead of complex PowerShell quoting.',
146
+ '- Always validate a changed file before concluding the task.',
147
+ '',
148
+ '## Encoding (UTF-8)',
149
+ '',
150
+ '- Do not use `Set-Content -Encoding UTF8` or `Out-File -Encoding UTF8`: Windows PowerShell 5.1 writes a UTF-8 BOM with that flag, which has broken JSON/Markdown parsing in this project before.',
151
+ "- Do not compose file content with a PowerShell here-string (`@'...'@` or `@\"...\"@`) when it contains accented characters, backticks, or `${}` template syntax: PowerShell's quoting has repeatedly mangled these into mojibake or broken syntax.",
152
+ '- Prefer writing content with a temporary Node script using UTF-8 explicitly, e.g. `node -e "require(\'fs\').writeFileSync(path, content, { encoding: \'utf8\' })"`.',
153
+ '- If PowerShell must write the file directly, use `[System.IO.File]::WriteAllText($path, $content, (New-Object System.Text.UTF8Encoding($false)))` (no BOM), never `-Encoding UTF8`.',
154
+ '- After writing, verify with `Get-Content -Raw` or a byte-level check that no BOM (`EF BB BF`) was introduced and that accented words were not replaced with `?` or a Unicode replacement character.',
155
+ ''
156
+ ].join('\n');
157
+
158
+ export async function ensureWorkerPolicy(maestroPath = CONFIG.maestroPath) {
159
+ await fs.mkdir(maestroPath, { recursive: true });
160
+ const policyPath = path.join(maestroPath, 'worker-policy.md');
161
+ if (!(await pathExists(policyPath))) {
162
+ await writeUtf8TextFile(policyPath, WORKER_POLICY_CONTENT);
163
+ }
164
+ return policyPath;
165
+ }
166
+
167
+ export async function checkpoint() {
168
+ await generateCheckpoint();
169
+ await logMemory('Project checkpoint generated.');
170
+ console.log('Checkpoint generated in .maestro/CHECKPOINT.md');
171
+ }
172
+
173
+ export async function handoff() {
174
+ await execFile('node', [path.join(CONFIG.memoryPath, 'tools', 'cli.mjs'), 'handoff']);
175
+ try {
176
+ const memoryHandoff = await fs.readFile(path.join(CONFIG.memoryPath, 'handoff.md'), 'utf-8');
177
+ await writeUtf8TextFile(path.join(CONFIG.maestroPath, 'HANDOFF.md'), memoryHandoff);
178
+ } catch (error) {
179
+ await writeUtf8TextFile(path.join(CONFIG.maestroPath, 'HANDOFF.md'), '# AI Maestro Handoff\n\nNo memory handoff available.\n');
180
+ }
181
+ console.log('Handoff report generated in .memory/handoff.md');
182
+ }
183
+
184
+ export function shouldRunWorkerValidation({ validateWorker, hasPriorValidation }) {
185
+ return !!validateWorker || !hasPriorValidation;
186
+ }
187
+
188
+ async function hasAnyPriorValidation() {
189
+ if (await hasRecentCodexTest()) {
190
+ return true;
191
+ }
192
+ const validated = await readValidatedCodexStrategy();
193
+ if (validated) {
194
+ return true;
195
+ }
196
+ const compare = await readCodexCompare();
197
+ return !!(compare && compare.winner);
198
+ }
199
+
200
+ export async function startCommand(request, options = {}) {
201
+ if (!request) {
202
+ console.log('Usage: maestro start "<pedido>" [--validate-worker]');
203
+ return;
204
+ }
205
+ await init();
206
+ await doctor();
207
+ await codexHomeCommand();
208
+ const hasPriorValidation = await hasAnyPriorValidation();
209
+ if (shouldRunWorkerValidation({ validateWorker: options.validateWorker, hasPriorValidation })) {
210
+ await codexTestInvocation();
211
+ } else {
212
+ console.log('Codex validation already on record; skipping smoke test. Use --validate-worker to force it.');
213
+ }
214
+ await plan(request);
215
+ await generateCheckpoint();
216
+ console.log('Next commands:');
217
+ console.log('- maestro task list');
218
+ console.log('- maestro run-one');
219
+ console.log('- maestro run-all --limit 3');
220
+ console.log('- maestro handoff');
221
+ }
222
+
223
+ export async function continueCommand() {
224
+ let brief = '';
225
+ let handoffText = '';
226
+ try { brief = await fs.readFile(path.join(CONFIG.memoryPath, 'BRIEF.md'), 'utf-8'); } catch (error) { brief = 'No .memory/BRIEF.md found.'; }
227
+ try { handoffText = await fs.readFile(path.join(CONFIG.maestroPath, 'HANDOFF.md'), 'utf-8'); } catch (error) {
228
+ try { handoffText = await fs.readFile(path.join(CONFIG.memoryPath, 'handoff.md'), 'utf-8'); } catch (inner) { handoffText = 'No handoff found.'; }
229
+ }
230
+ const tasks = await getTasks();
231
+ const pending = sortRunnableTasks(tasks.filter(task => task.status === 'pending'));
232
+ console.log('Brief:\n' + brief.split(/\r?\n/).slice(0, 8).join('\n'));
233
+ console.log('\nHandoff:\n' + handoffText.split(/\r?\n/).slice(0, 12).join('\n'));
234
+ console.log('\nPending tasks: ' + pending.length);
235
+ pending.slice(0, 10).forEach(task => console.log('- #' + task.id + ' [' + task.priority + '] ' + (task.title || task.description)));
236
+ console.log('\nSuggested next command: ' + (pending.length ? 'maestro run-one' : 'maestro plan "..."'));
237
+ }
238
+
239
+ async function readJsonlSafe(filePath) {
240
+ try {
241
+ const content = await fs.readFile(filePath, 'utf-8');
242
+ return content.split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line));
243
+ } catch (error) {
244
+ if (error.code === 'ENOENT') {
245
+ return [];
246
+ }
247
+ return null;
248
+ }
249
+ }
250
+
251
+ export async function schemaCheck() {
252
+ const checks = [];
253
+
254
+ const tasks = await readJsonSafe(path.join(CONFIG.maestroPath, 'TASKS.json'), []);
255
+ const taskErrors = tasks.flatMap(task => validateTask(task).errors.map(error => '#' + task.id + ': ' + error));
256
+ checks.push({ name: '.maestro/TASKS.json', ok: taskErrors.length === 0, errors: taskErrors });
257
+
258
+ const runs = await readJsonlSafe(path.join(CONFIG.maestroPath, 'RUNS.jsonl'));
259
+ const runErrors = runs === null ? ['file could not be parsed'] : runs.flatMap(entry => validateRunEntry(entry).errors);
260
+ checks.push({ name: '.maestro/RUNS.jsonl', ok: runErrors.length === 0, errors: runErrors });
261
+
262
+ const summary = await readJsonSafe(path.join(CONFIG.maestroPath, 'usage-summary.json'), null);
263
+ const summaryErrors = summary ? validateUsageSummary(summary).errors : [];
264
+ checks.push({ name: '.maestro/usage-summary.json', ok: summaryErrors.length === 0, errors: summaryErrors });
265
+
266
+ const ok = checks.every(check => check.ok);
267
+ for (const check of checks) {
268
+ console.log((check.ok ? 'OK' : 'FAIL') + ': ' + check.name + (check.errors && check.errors.length ? ' -> ' + check.errors.join('; ') : ''));
269
+ }
270
+ if (!ok) {
271
+ process.exitCode = 1;
272
+ }
273
+ return { ok, checks };
274
+ }
275
+
276
+ export async function releaseCheck() {
277
+ const checks = [];
278
+ async function runCheck(name, command, args) {
279
+ const result = command.endsWith('.cmd')
280
+ ? await execCmdFile(command, args, { quiet: true })
281
+ : await execFileRaw(command, args, { quiet: true });
282
+ checks.push({ name, command: command + ' ' + args.join(' '), exitCode: result.code, ok: result.code === 0, stdout: summarizeText(result.stdout, 500), stderr: summarizeText(result.stderr, 500) });
283
+ return result.code === 0;
284
+ }
285
+ await runCheck('syntax bin', 'node', ['--check', 'bin/maestro.mjs']);
286
+ await runCheck('syntax config', 'node', ['--check', 'src/config.mjs']);
287
+ await runCheck('syntax commands', 'node', ['--check', 'src/commands.mjs']);
288
+ await runCheck('syntax codex-home', 'node', ['--check', 'src/codex-home.mjs']);
289
+ await runCheck('syntax debug', 'node', ['--check', 'src/debug.mjs']);
290
+ await runCheck('syntax safety', 'node', ['--check', 'src/safety.mjs']);
291
+ await runCheck('syntax planner', 'node', ['--check', 'src/planner.mjs']);
292
+ await runCheck('syntax usage', 'node', ['--check', 'src/usage.mjs']);
293
+ await runCheck('syntax schema', 'node', ['--check', 'src/schema.mjs']);
294
+ await runCheck('syntax lock', 'node', ['--check', 'src/lock.mjs']);
295
+ await runCheck('syntax engines', 'node', ['--check', 'src/engines/registry.mjs']);
296
+ await runCheck('syntax codex-diagnostics', 'node', ['--check', 'src/codex-diagnostics.mjs']);
297
+ await runCheck('syntax codex-home-inspect', 'node', ['--check', 'src/codex-home-inspect.mjs']);
298
+ await runCheck('syntax task-commands', 'node', ['--check', 'src/task-commands.mjs']);
299
+ await runCheck('syntax session-commands', 'node', ['--check', 'src/session-commands.mjs']);
300
+ await runCheck('syntax smart-planner', 'node', ['--check', 'src/smart-planner.mjs']);
301
+ await runCheck('syntax run-diagnostics', 'node', ['--check', 'src/run-diagnostics.mjs']);
302
+ await runCheck('syntax workspace-diff', 'node', ['--check', 'src/workspace-diff.mjs']);
303
+ await runCheck('syntax ui server', 'node', ['--check', 'src/ui/server.mjs']);
304
+ await runCheck('syntax ui state', 'node', ['--check', 'src/ui/state.mjs']);
305
+ await runCheck('syntax ui commands', 'node', ['--check', 'src/ui/commands.mjs']);
306
+ await runCheck('syntax ui events', 'node', ['--check', 'src/ui/events.mjs']);
307
+ if (process.platform === 'win32') {
308
+ await runCheck('npm test', 'cmd.exe', ['/d', '/s', '/c', 'npm test']);
309
+ } else {
310
+ await runCheck('npm test', 'npm', ['test']);
311
+ }
312
+ await runCheck('doctor', 'node', ['bin/maestro.mjs', 'doctor']);
313
+ await runCheck('codex-test', 'node', ['bin/maestro.mjs', 'codex-test']);
314
+ await runCheck('stats', 'node', ['bin/maestro.mjs', 'stats']);
315
+ await runCheck('checkpoint', 'node', ['bin/maestro.mjs', 'checkpoint']);
316
+ await runCheck('handoff', 'node', ['bin/maestro.mjs', 'handoff']);
317
+ await runCheck('ui endpoints', 'node', ['--input-type=module', '-e', "const { startUiServer } = await import('./src/ui/server.mjs'); const ui = await startUiServer({ port: 0 }); try { const stateRes = await fetch(ui.url + '/api/state'); if (!stateRes.ok) throw new Error('/api/state status ' + stateRes.status); const handoffRes = await fetch(ui.url + '/api/handoff'); if (!handoffRes.ok) throw new Error('/api/handoff status ' + handoffRes.status); const state = await stateRes.json(); const handoff = await handoffRes.json(); if (!Array.isArray(state.timeline)) throw new Error('timeline missing'); if (typeof handoff.content !== 'string') throw new Error('handoff content missing'); if (!state.statusMap || !state.statusMap.completed_with_warnings) throw new Error('statusMap missing completed_with_warnings'); console.log('ui endpoints ok'); } finally { await new Promise(resolve => ui.server.close(resolve)); }"]);
318
+ await runCheck('help shown for no args', 'node', ['bin/maestro.mjs']);
319
+ const helpProbe = checks[checks.length - 1];
320
+ checks[checks.length - 1] = { ...helpProbe, ok: helpProbe.ok && /AI-MAESTRO/.test(helpProbe.stdout) };
321
+ const plannerProbe = buildSmartTasks('os botões não funcionam, termine de fazer o app, botões funcionando, e acesso aos arquivos pelo painel');
322
+ checks.push({
323
+ name: 'planner kind smart-heuristic-v1',
324
+ ok: plannerProbe.plannerKind === 'smart-heuristic-v1' && plannerProbe.kind === 'ui-app'
325
+ });
326
+ checks.push({
327
+ name: 'schema accepts completed_with_warnings',
328
+ ok: validateTask({ id: 1, description: 'x', status: 'completed_with_warnings' }).ok === true
329
+ });
330
+ const platformGuidance = getPlatformGuidance('win32');
331
+ checks.push({
332
+ name: 'platform guidance forbids Unix commands on Windows',
333
+ ok: platformGuidance.includes('ls -la') && platformGuidance.includes('Get-ChildItem')
334
+ });
335
+ checks.push({
336
+ name: 'windowsShellCommandMismatch detector',
337
+ ok: detectWindowsShellMismatch("tail : O termo 'tail' não é reconhecido") === true
338
+ && detectWindowsShellMismatch('tudo certo, nenhum erro aqui') === false
339
+ });
340
+ const syntaxProbeDir = await fs.mkdtemp(path.join(os.tmpdir(), 'maestro-release-check-'));
341
+ try {
342
+ const badJsPath = path.join(syntaxProbeDir, 'bad.js');
343
+ await fs.writeFile(badJsPath, 'function( { const x = ;');
344
+ const badJsonPath = path.join(syntaxProbeDir, 'bad.json');
345
+ await fs.writeFile(badJsonPath, '{ invalid json');
346
+ const badJs = await validateChangedFile(badJsPath);
347
+ const badJson = await validateChangedFile(badJsonPath);
348
+ checks.push({ name: 'validateChangedFile rejects broken JS/JSON', ok: badJs.ok === false && badJson.ok === false });
349
+ } finally {
350
+ await fs.rm(syntaxProbeDir, { recursive: true, force: true });
351
+ }
352
+ checks.push({
353
+ name: 'isAnalysisOnlyTask classifies mapear/analisar tasks',
354
+ ok: isAnalysisOnlyTask({ title: 'Mapear stack e estrutura real do app', description: '', acceptance: [] }) === true
355
+ && isAnalysisOnlyTask({ title: 'Corrigir interacoes dos botoes', description: '', acceptance: [] }) === false
356
+ });
357
+ const workspaceProbeDir = await fs.mkdtemp(path.join(os.tmpdir(), 'maestro-workspace-check-'));
358
+ try {
359
+ await fs.writeFile(path.join(workspaceProbeDir, 'app.js'), 'const x = 1;');
360
+ const before = await snapshotWorkspace(workspaceProbeDir);
361
+ await fs.writeFile(path.join(workspaceProbeDir, 'app.js'), 'const x = 2;');
362
+ const after = await snapshotWorkspace(workspaceProbeDir);
363
+ const diff = diffWorkspace(before, after);
364
+ checks.push({ name: 'workspace diff detects a changed file', ok: diff.changedFiles.includes('app.js') });
365
+ } finally {
366
+ await fs.rm(workspaceProbeDir, { recursive: true, force: true });
367
+ }
368
+ checks.push({
369
+ name: 'decideTaskStatus fails analysis-only code changes and deletions',
370
+ ok: decideTaskStatus({ exitCode: 0, analysisTaskChangedCode: true, deletedOutOfScope: false, syntaxValidationFailed: false, unsupportedToolCall: false, suspectedFalsePositive: false, windowsShellCommandMismatch: false }) === 'failed'
371
+ && decideTaskStatus({ exitCode: 0, analysisTaskChangedCode: false, deletedOutOfScope: true, syntaxValidationFailed: false, unsupportedToolCall: false, suspectedFalsePositive: false, windowsShellCommandMismatch: false }) === 'failed'
372
+ });
373
+ const schemaResult = await schemaCheck();
374
+ checks.push({ name: 'schema-check', ok: schemaResult.ok });
375
+ const readmeOk = await pathExists('README.md');
376
+ const uiAssetsOk = await Promise.all([
377
+ pathExists(path.join('src', 'ui', 'public', 'index.html')),
378
+ pathExists(path.join('src', 'ui', 'public', 'app.js')),
379
+ pathExists(path.join('src', 'ui', 'public', 'style.css'))
380
+ ]);
381
+ const packageJson = JSON.parse(await fs.readFile('package.json', 'utf-8'));
382
+ const packageOk = !!(packageJson.bin && packageJson.bin.maestro);
383
+ const helpText = 'help start continue codex-home codex-test codex-compare task run-one run-task run-all checkpoint handoff stats release-check schema-check task-pending ui --quiet --validate-worker';
384
+ checks.push({ name: 'README exists', ok: readmeOk });
385
+ checks.push({ name: 'ui assets exist', ok: uiAssetsOk.every(Boolean) });
386
+ checks.push({ name: 'package bin.maestro', ok: packageOk });
387
+ checks.push({ name: 'command list expected', ok: ['run-task', 'task-pending', 'ui'].every(item => helpText.includes(item)), commands: helpText });
388
+ const ok = checks.every(check => check.ok);
389
+ const report = '# AI Maestro Release Check\n\n- Date: ' + new Date().toISOString() + '\n- Status: ' + (ok ? 'PASS' : 'FAIL') + '\n\n## Checks\n\n' + checks.map(check => '- ' + (check.ok ? 'OK' : 'FAIL') + ' ' + check.name + (check.exitCode !== undefined ? ' exit=' + check.exitCode : '')).join('\n') + '\n';
390
+ await writeUtf8TextFile(path.join(CONFIG.maestroPath, 'RELEASE_CHECK.md'), report);
391
+ console.log(report);
392
+ if (!ok) process.exitCode = 1;
393
+ }
394
+
395
+ export async function doctor() {
396
+ console.log('Running AI Maestro doctor...');
397
+ doctorLine('OK', 'Project root', process.cwd());
398
+ doctorLine('OK', 'CODEX_HOME', CONFIG.codexHome);
399
+ doctorLine('OK', 'Profile', CONFIG.codexProfile);
400
+ doctorLine('OK', 'Configured strategy', CONFIG.codexStrategy);
401
+ doctorLine('OK', 'Current strategy', await resolveRunCodexStrategy());
402
+ doctorLine('OK', 'Configured invocation', CONFIG.codexInvocation);
403
+ doctorLine('OK', 'Current invocation', await resolveCodexInvocationMode());
404
+ if (process.env.NINEROUTER_API_KEY) {
405
+ doctorLine('OK', 'NINEROUTER_API_KEY', 'set');
406
+ } else if (process.env.MAESTRO_ALLOW_LOCAL_TEST_KEY === '1') {
407
+ doctorLine('AVISO', 'NINEROUTER_API_KEY', 'missing; using explicit MAESTRO_ALLOW_LOCAL_TEST_KEY=1 fallback local-test');
408
+ } else {
409
+ doctorLine('ERRO', 'NINEROUTER_API_KEY', 'missing; set it or explicitly set MAESTRO_ALLOW_LOCAL_TEST_KEY=1 for local diagnostics');
410
+ }
411
+ const compare = await readCodexCompare();
412
+ let repair = null;
413
+ let homeDiff = null;
414
+ let probe = null;
415
+ try {
416
+ repair = JSON.parse(await fs.readFile(path.join(CONFIG.maestroPath, 'codex-home-repair.json'), 'utf-8'));
417
+ } catch (error) {
418
+ repair = null;
419
+ }
420
+ try {
421
+ homeDiff = JSON.parse(await fs.readFile(path.join(CONFIG.maestroPath, 'codex-home-diff.json'), 'utf-8'));
422
+ } catch (error) {
423
+ homeDiff = null;
424
+ }
425
+ try {
426
+ probe = JSON.parse(await fs.readFile(path.join(CONFIG.maestroPath, 'codex-home-probe.json'), 'utf-8'));
427
+ } catch (error) {
428
+ probe = null;
429
+ }
430
+ doctorLine(compare ? 'OK' : 'AVISO', 'Last codex-compare', compare ? 'winner=' + (compare.winner || 'none') : 'none');
431
+ if (compare && compare.tests) {
432
+ doctorLine('OK', 'codex-compare tests', compare.tests.map(test => test.id + '=' + (test.ok ? 'ok' : 'fail')).join(', '));
433
+ doctorLine(compare.winner ? 'OK' : 'ERRO', 'codex-compare recommendation', compare.recommendation || 'none');
434
+ }
435
+ doctorLine(homeDiff ? 'OK' : 'AVISO', 'Last codex-home-diff', homeDiff ? 'normalFiles=' + homeDiff.normal.files.length + ', maestroFiles=' + homeDiff.maestro.files.length : 'none');
436
+ doctorLine(repair ? 'OK' : 'AVISO', 'Last codex-home-repair', repair ? 'backup=' + repair.backupPath : 'none');
437
+ doctorLine(probe ? (probe.winner ? 'OK' : 'AVISO') : 'AVISO', 'Last codex-home-probe', probe ? ('winner=' + (probe.winner ? probe.winner.candidate : 'none') + ', tested=' + probe.candidatesTested) : 'none');
438
+ const currentInvocation = await resolveCodexInvocationMode();
439
+ if (currentInvocation.includes('normal-home') || currentInvocation === 'codex-cmd') {
440
+ doctorLine('ERRO', 'Normal-home in use', 'O worker está usando o CODEX_HOME normal (configuração Desktop compartilhada). Isso só acontece com MAESTRO_CODEX_INVOCATION explícito; o modo auto usa somente o home isolado.');
441
+ }
442
+ const validated = await readValidatedCodexStrategy();
443
+ doctorLine(validated ? 'OK' : 'AVISO', 'Last validated strategy', validated ? validated.strategy : 'none');
444
+ const home = await ensureCodexHome();
445
+ doctorLine('OK', 'Config file', home.configPath);
446
+ if (home.sanitized) {
447
+ doctorLine('AVISO', 'Isolated config sanitized', 'configuração Desktop contaminada foi salva em backup e reescrita minimal (backup: ' + home.backupPath + ')');
448
+ }
449
+ doctorLine(home.analysis && home.analysis.contaminated ? 'ERRO' : 'OK', 'Isolated config clean of Desktop/normal-home refs', !(home.analysis && home.analysis.contaminated));
450
+ doctorLine('OK', 'Legacy workspace-permissions config (unused)', getLegacyWorkspacePermissionsConfigPath());
451
+ doctorLine('OK', 'default_permissions used', home.checks.usesDefaultPermissions + ' (não usado pelo Maestro: conflita com sandbox_mode, que é o mecanismo adotado; o antigo workspace-permissions.config.toml nunca era carregado)');
452
+ doctorLine(home.checks.mixesDefaultPermissionsAndSandboxMode ? 'ERRO' : 'OK', 'default_permissions + sandbox_mode mix', home.checks.mixesDefaultPermissionsAndSandboxMode);
453
+ try {
454
+ const runtime = await getEffectiveCodexRuntime();
455
+ console.log('Effective Codex runtime (Hotfix A.2):');
456
+ for (const line of formatEffectiveRuntimeLines(runtime)) {
457
+ console.log(line);
458
+ }
459
+ doctorLine(runtime.isolatedHome ? 'OK' : 'ERRO', 'Effective CODEX_HOME isolated', runtime.isolatedHome);
460
+ doctorLine(runtime.referencesNormalCodexHome ? 'ERRO' : 'OK', 'Effective config references normal .codex', runtime.referencesNormalCodexHome);
461
+ doctorLine(runtime.desktopPluginsEnabled || runtime.mcpServersConfigured ? 'ERRO' : 'OK', 'Desktop plugins/MCP in effective config', runtime.desktopPluginsEnabled || runtime.mcpServersConfigured);
462
+ doctorLine(runtime.provider === '9router' ? 'OK' : 'ERRO', 'Effective provider', runtime.provider);
463
+ doctorLine(runtime.windowsSandboxElevated ? 'ERRO' : 'OK', 'Windows sandbox "elevated" in effective config', runtime.windowsSandboxElevated);
464
+ } catch (error) {
465
+ doctorLine('ERRO', 'Effective Codex runtime', error.message);
466
+ }
467
+ try {
468
+ const invocation = await resolveCodexInvocation();
469
+ doctorLine('OK', 'Codex command', invocation.command + ' (' + invocation.source + ')');
470
+ doctorLine('OK', 'Codex args prefix', invocation.argsPrefix.join(' '));
471
+ const selectedInvocation = await resolveCodexInvocationForMode(await resolveCodexInvocationMode());
472
+ doctorLine('OK', 'Selected Codex command', selectedInvocation.command + ' (' + selectedInvocation.source + ')');
473
+ doctorLine('OK', 'Selected Codex args prefix', selectedInvocation.argsPrefix.join(' '));
474
+ } catch (error) {
475
+ doctorLine('ERRO', 'Codex command', error.message);
476
+ }
477
+ try {
478
+ const help = await getCodexHelpMatrix();
479
+ doctorLine(help.hasGlobalAskForApproval ? 'OK' : 'AVISO', '--ask-for-approval global', help.hasGlobalAskForApproval);
480
+ doctorLine(help.hasExecSandbox ? 'OK' : 'ERRO', '--sandbox exec', help.hasExecSandbox);
481
+ doctorLine(help.hasExecShortConfig ? 'OK' : 'AVISO', '-c exec', help.hasExecShortConfig);
482
+ doctorLine(help.hasExecIgnoreRules ? 'OK' : 'AVISO', '--ignore-rules exec', help.hasExecIgnoreRules);
483
+ doctorLine(help.hasDangerousBypass ? 'OK' : 'AVISO', 'danger bypass flag present', help.hasDangerousBypass);
484
+ const output = help.globalHelp.stdout + help.globalHelp.stderr + help.execHelp.stdout + help.execHelp.stderr;
485
+ doctorLine(/api\.openai\.com/i.test(output) ? 'ERRO' : 'OK', 'api.openai.com check', /api\.openai\.com/i.test(output) ? 'detected' : 'not detected');
486
+ } catch (error) {
487
+ doctorLine('ERRO', 'Codex help', error.message);
488
+ }
489
+ }
490
+
491
+ export async function stats() {
492
+ console.log(JSON.stringify(await getUsageSummary(), null, 2));
493
+ }
494
+
495
+ export async function providerHealthCommand(options = {}) {
496
+ const snapshot = await getFullHealthSnapshot();
497
+ if (options.json) {
498
+ console.log(JSON.stringify(snapshot, null, 2));
499
+ } else {
500
+ console.log('Provider health:');
501
+ const targetIds = Object.keys(snapshot.targets || {}).sort();
502
+ if (snapshot.diagnostics && snapshot.diagnostics.degraded) {
503
+ doctorLine('AVISO', 'Provider health snapshot', 'could not be read; assuming safe empty state (' + snapshot.diagnostics.degradedReason + ')');
504
+ }
505
+ if (targetIds.length === 0) {
506
+ console.log('- No provider health data yet; all targets are assumed CLOSED.');
507
+ } else {
508
+ for (const targetId of targetIds) {
509
+ const entry = snapshot.targets[targetId];
510
+ const cooldown = entry.cooldownUntil ? ' cooldownUntil=' + entry.cooldownUntil : '';
511
+ const retryAfter = entry.observedRetryAfterMs != null ? ' observedRetryAfterMs=' + entry.observedRetryAfterMs : '';
512
+ console.log('- ' + targetId + ': ' + entry.state + ' failures=' + entry.consecutiveFailures + ' successes=' + entry.consecutiveSuccesses + cooldown + retryAfter);
513
+ }
514
+ }
515
+ console.log('Consistency: eventually consistent; same-process Node lock only; no cross-process lock.');
516
+ }
517
+ if (snapshot.diagnostics && snapshot.diagnostics.degradedReason === 'io-error') {
518
+ process.exitCode = 1;
519
+ }
520
+ return snapshot;
521
+ }
package/src/shell.mjs ADDED
@@ -0,0 +1,93 @@
1
+ import { spawn } from 'child_process';
2
+ import { redactCredentials } from './format.mjs';
3
+
4
+ export async function exec(command, args = [], options = {}) {
5
+ return execFile(command, args, { ...options, shell: false });
6
+ }
7
+
8
+ export async function execFile(command, args = [], options = {}) {
9
+ const result = await spawnCollect(command, args, options);
10
+ if (result.code !== 0) {
11
+ const error = new Error('Command failed with exit code ' + result.code + '. Stderr: ' + result.stderr);
12
+ error.code = result.code;
13
+ error.stdout = result.stdout;
14
+ error.stderr = result.stderr;
15
+ throw error;
16
+ }
17
+ return result;
18
+ }
19
+
20
+ export async function execFileRaw(command, args = [], options = {}) {
21
+ return spawnCollect(command, args, options);
22
+ }
23
+
24
+ function quoteCmdArg(value) {
25
+ const text = String(value);
26
+ const singleLine = text.replace(/\r?\n/g, '\\n');
27
+ return '"' + singleLine.replace(/(["^&|<>()%])/g, '^$1') + '"';
28
+ }
29
+
30
+ export async function execCmdFile(commandPath, args = [], options = {}) {
31
+ const innerCommand = [quoteCmdArg(commandPath), ...args.map(quoteCmdArg)].join(' ');
32
+ const commandLine = '"' + innerCommand + '"';
33
+ return execFileRaw('cmd.exe', ['/d', '/s', '/c', commandLine], {
34
+ ...options,
35
+ windowsVerbatimArguments: true
36
+ });
37
+ }
38
+
39
+ async function spawnCollect(command, args = [], options = {}) {
40
+ return new Promise((resolve, reject) => {
41
+ const timeoutMs = options.timeoutMs || options.timeout_ms || 0;
42
+ const child = spawn(command, args, {
43
+ shell: false,
44
+ stdio: ['ignore', 'pipe', 'pipe'],
45
+ ...options
46
+ });
47
+
48
+ let stdout = '';
49
+ let stderr = '';
50
+
51
+ child.stdout.on('data', (data) => {
52
+ const text = data.toString();
53
+ stdout += text;
54
+ if (!options.quiet) {
55
+ process.stdout.write(redactCredentials(text));
56
+ }
57
+ });
58
+
59
+ child.stderr.on('data', (data) => {
60
+ const text = data.toString();
61
+ stderr += text;
62
+ if (!options.quiet) {
63
+ process.stderr.write(redactCredentials(text));
64
+ }
65
+ });
66
+
67
+ let timedOut = false;
68
+ let timer = null;
69
+ if (timeoutMs > 0) {
70
+ timer = setTimeout(() => {
71
+ timedOut = true;
72
+ child.kill('SIGTERM');
73
+ }, timeoutMs);
74
+ }
75
+
76
+ child.on('close', (code, signal) => {
77
+ if (timer) {
78
+ clearTimeout(timer);
79
+ }
80
+ resolve({
81
+ code: timedOut ? 124 : code,
82
+ signal,
83
+ timedOut,
84
+ stdout,
85
+ stderr: timedOut ? stderr + '\nTimed out after ' + timeoutMs + 'ms.' : stderr
86
+ });
87
+ });
88
+
89
+ child.on('error', (err) => {
90
+ reject(err);
91
+ });
92
+ });
93
+ }