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,1182 @@
1
+ import { generateCheckpoint } from './checkpoint.mjs';
2
+ import { addTask, archiveTasks, getNextTask, getTaskById, getTasks, sortRunnableTasks, updateTaskStatus } from './tasks.mjs';
3
+ import { executeTaskWithSafety } from './safety.mjs';
4
+ import { recordUsage } from './usage.mjs';
5
+ import { logRun, getLatestRunForTask, getRecentRuns } from './runner.mjs';
6
+ import { logMemory } from './memory-log.mjs';
7
+ import { formatPreview, redactCredentials, summarizeText } from './format.mjs';
8
+ import { getEngine } from './engines/registry.mjs';
9
+ import { detectUnsupportedToolCall, buildRunDiagnostics, detectWindowsShellMismatch, validateChangedFiles, isAnalysisOnlyTask, verifyDeliverableEvidence } from './run-diagnostics.mjs';
10
+ import { snapshotWorkspace, diffWorkspace } from './workspace-diff.mjs';
11
+ import { evaluateTaskRun, countRunsForTask } from './orchestration/runtime-gate.mjs';
12
+ import { classifyFailure } from './orchestration/failure-classifier.mjs';
13
+ import { loadBudget, getHostValidationPolicy } from './orchestration/budget-manager.mjs';
14
+ import { runHostValidation, summarizeHostValidation } from './orchestration/host-validation.mjs';
15
+ import { buildOrchestrationSummary, getLatestOrchestrationForTask } from './orchestration/orchestration-trail.mjs';
16
+ import { execFileRaw } from './shell.mjs';
17
+ import { acquireOrRecover, inspectTaskLock, releaseTaskLock } from './lock.mjs';
18
+
19
+ export function getPlatformGuidance(platform = process.platform) {
20
+ if (platform === 'win32') {
21
+ return [
22
+ 'Você está em Windows PowerShell.',
23
+ 'Não use comandos Unix/Linux quando estiver rodando via PowerShell.',
24
+ '',
25
+ 'Não use:',
26
+ '- ls -la',
27
+ '- tail -50',
28
+ "- cat > arquivo << EOF",
29
+ '- rm -rf',
30
+ '- grep',
31
+ '- sed -i',
32
+ '- heredoc bash',
33
+ '',
34
+ 'Use equivalentes PowerShell:',
35
+ '- Get-ChildItem -Force',
36
+ '- Get-Content arquivo | Select-Object -Last 50',
37
+ '- Remove-Item -Recurse -Force somente se explicitamente permitido',
38
+ '- Select-String',
39
+ '- scripts Node para escrita complexa',
40
+ '',
41
+ 'Para escrever arquivos com conteudo grande, JS/HTML/CSS, ou qualquer texto com acentos:',
42
+ 'Prefira criar um script Node temporario, por exemplo:',
43
+ 'node -e "const fs=require(\'fs\'); fs.writeFileSync(\'static/app.js\', content, { encoding: \'utf8\' })"',
44
+ 'Ou crie um arquivo .tmp-write.mjs temporario, execute, depois remova.',
45
+ 'Evite PowerShell com quoting complexo para JS multilinha.',
46
+ '',
47
+ 'Nunca use Set-Content -Encoding UTF8 ou Out-File -Encoding UTF8: no Windows PowerShell 5.1 isso grava um BOM UTF-8 no inicio do arquivo, o que ja quebrou parsing de JSON/Markdown neste projeto.',
48
+ "Nunca componha conteudo com here-string PowerShell (@' ... '@) quando houver acentos, crases ou ${}: isso ja corrompeu arquivos neste projeto (mojibake, template literals quebrados).",
49
+ 'Se precisar escrever diretamente via PowerShell, use: [System.IO.File]::WriteAllText($path, $content, (New-Object System.Text.UTF8Encoding($false))) — sem BOM.'
50
+ ].join('\n');
51
+ }
52
+ return [
53
+ 'You are running on a Unix-like shell (' + platform + ').',
54
+ 'Prefer POSIX-compatible commands and avoid Windows-only syntax.'
55
+ ].join('\n');
56
+ }
57
+
58
+ // Codex's internal plan/update_plan tool only accepts these step statuses.
59
+ // Maestro's richer internal statuses (blocked, failed, needs_human,
60
+ // archived, completed_with_warnings) must NEVER cross that boundary as-is:
61
+ // Codex rejects them with "unknown variant `blocked`, expected one of
62
+ // `pending`, `in_progress`, `completed`". Map to a supported variant
63
+ // instead — or don't call the tool.
64
+ export const CODEX_PLAN_TOOL_STATUSES = ['pending', 'in_progress', 'completed'];
65
+
66
+ export function mapStatusToCodexPlanVariant(status) {
67
+ switch (status) {
68
+ case 'pending':
69
+ case 'in_progress':
70
+ case 'completed':
71
+ return status;
72
+ case 'completed_with_warnings':
73
+ return 'completed';
74
+ default:
75
+ // blocked / failed / needs_human / archived / anything unknown: the
76
+ // step is not done and not actively running — 'pending' is the only
77
+ // honest supported variant. The real Maestro status stays in
78
+ // TASKS.json / RUNS.jsonl untouched.
79
+ return 'pending';
80
+ }
81
+ }
82
+
83
+ export function buildWorkerPrompt(task) {
84
+ return [
85
+ 'You are a worker agent for AI Maestro.',
86
+ '',
87
+ 'Read these files first:',
88
+ '- .memory/BRIEF.md',
89
+ '- .maestro/PLAN.md',
90
+ '- .maestro/TASKS.json',
91
+ '- .maestro/worker-policy.md',
92
+ '',
93
+ 'Your task is:',
94
+ task.description,
95
+ '',
96
+ 'Use the available tools. Do not ask for user input.',
97
+ 'Follow .maestro/worker-policy.md.',
98
+ 'Work from project root: ' + process.cwd(),
99
+ '',
100
+ 'Mandatory rules for this run:',
101
+ '- Do not use apply_patch. This environment may not support that tool.',
102
+ '- To edit files, use PowerShell, Node scripts, or other real commands actually available here.',
103
+ '- If a command fails, do not claim the change was made.',
104
+ '- Before concluding, verify changed files with: git diff --stat, git diff, Test-Path, Get-Content, or an equivalent command.',
105
+ '- In your final response, separate clearly:',
106
+ ' 1. Files actually changed',
107
+ ' 2. Commands executed',
108
+ ' 3. Commands that failed',
109
+ ' 4. Validations performed',
110
+ ' 5. Pending items',
111
+ '- If this task is analysis-only, do not change any code.',
112
+ '- If you need to change .maestro/TASKS.json, prefer the Maestro CLI commands (maestro task ...) when they exist instead of editing the file directly.',
113
+ '- If you use your internal plan/update_plan tool, its step statuses only accept: pending, in_progress, completed. Never pass Maestro statuses such as blocked, failed or needs_human to that tool; report those situations in prose in your final answer instead.',
114
+ '- Never state that a file was updated without having verified it.',
115
+ '',
116
+ getPlatformGuidance()
117
+ ].join('\n');
118
+ }
119
+
120
+ export function countTasksByStatus(tasks) {
121
+ return tasks.reduce((acc, task) => {
122
+ acc[task.status || 'unknown'] = (acc[task.status || 'unknown'] || 0) + 1;
123
+ return acc;
124
+ }, {});
125
+ }
126
+
127
+ function printSafe(line = '') {
128
+ console.log(redactCredentials(line));
129
+ }
130
+
131
+ function printLines(lines) {
132
+ for (const line of lines) {
133
+ printSafe(line);
134
+ }
135
+ }
136
+
137
+ function shouldSuggestForce(gate) {
138
+ return gate && gate.forceAllowed === true;
139
+ }
140
+
141
+ export function formatForceAction(gate, taskId) {
142
+ if (shouldSuggestForce(gate)) {
143
+ return 'Acao: revise o risco e use "maestro run-task ' + taskId + ' --force" se aprovado.';
144
+ }
145
+ return 'Acao: corrija a causa do bloqueio; --force nao e permitido para este resultado.';
146
+ }
147
+
148
+ function countChangedFiles(changedFiles = [], createdFiles = [], deletedFiles = []) {
149
+ return changedFiles.length + createdFiles.length + deletedFiles.length;
150
+ }
151
+
152
+ function formatChangedSummary(count) {
153
+ if (count === 0) return '✓ Nenhum arquivo alterado';
154
+ return count === 1 ? '✓ 1 arquivo alterado' : '✓ ' + count + ' arquivos alterados';
155
+ }
156
+
157
+ function validationSummary({ syntaxValidationFailed, hostValidationFallback }) {
158
+ if (syntaxValidationFailed) return 'x Falha na validacao de sintaxe';
159
+ if (hostValidationFallback && hostValidationFallback.used) {
160
+ return hostValidationFallback.result === 'passed'
161
+ ? '✓ Validacao do host passou'
162
+ : 'x Falha na validacao do host';
163
+ }
164
+ return '✓ Validacao de sintaxe OK';
165
+ }
166
+
167
+ function printVerboseSections({ result, diffText, runId, debug }) {
168
+ if (!result) return;
169
+ if (debug) {
170
+ printSafe('');
171
+ printSafe('--- stdout completo ---');
172
+ printSafe(result.stdout || '');
173
+ printSafe('--- stderr completo ---');
174
+ printSafe(result.stderr || '');
175
+ if (diffText) {
176
+ printSafe('--- diff completo ---');
177
+ printSafe(diffText);
178
+ }
179
+ return;
180
+ }
181
+ printSafe('');
182
+ printSafe('--- stdout preview ---');
183
+ printSafe(formatPreview(result.stdout || '', { maxLines: 100, maxBytes: 8192, runId }).text || '(vazio)');
184
+ printSafe('--- stderr preview ---');
185
+ printSafe(formatPreview(result.stderr || '', { maxLines: 100, maxBytes: 8192, runId }).text || '(vazio)');
186
+ if (diffText) {
187
+ printSafe('--- diff preview ---');
188
+ printSafe(formatPreview(diffText, { maxLines: 50, maxBytes: 4096, runId }).text);
189
+ }
190
+ }
191
+
192
+ async function collectDiffText() {
193
+ try {
194
+ const result = await execFileRaw('git', ['diff', '--no-ext-diff'], { quiet: true });
195
+ return (result.stdout || '') + (result.stderr ? '\n' + result.stderr : '');
196
+ } catch (error) {
197
+ return '';
198
+ }
199
+ }
200
+
201
+ const TERMINAL_RUN_STATUSES = new Set(['completed', 'completed_with_warnings', 'failed', 'blocked', 'needs_human']);
202
+ const CRASH_RECONCILIATION_WARNING = 'reconciled task stranded in_progress after unexpected process termination';
203
+ let accountingChain = Promise.resolve();
204
+
205
+ function delayMs(ms) {
206
+ return new Promise(resolve => setTimeout(resolve, ms));
207
+ }
208
+
209
+ async function withTransientJsonRetry(fn) {
210
+ let lastError;
211
+ for (let attempt = 0; attempt < 5; attempt += 1) {
212
+ try {
213
+ return await fn();
214
+ } catch (error) {
215
+ if (!(error instanceof SyntaxError)) throw error;
216
+ lastError = error;
217
+ await delayMs(attempt + 1);
218
+ }
219
+ }
220
+ throw lastError;
221
+ }
222
+
223
+ export async function withAccountingLock(fn) {
224
+ const previous = accountingChain;
225
+ let release;
226
+ const current = new Promise(resolve => {
227
+ release = resolve;
228
+ });
229
+ accountingChain = previous.then(() => current, () => current);
230
+ await previous.catch(() => {});
231
+ try {
232
+ return await fn();
233
+ } finally {
234
+ release();
235
+ if (accountingChain === current) {
236
+ accountingChain = Promise.resolve();
237
+ }
238
+ }
239
+ }
240
+
241
+ function isTerminalRunStatus(status) {
242
+ return TERMINAL_RUN_STATUSES.has(status);
243
+ }
244
+
245
+ function parseTimeMs(value) {
246
+ const parsed = Date.parse(value);
247
+ return Number.isFinite(parsed) ? parsed : null;
248
+ }
249
+
250
+ function latestRunCoversInProgressEntry(latestOwnRun, task) {
251
+ if (!latestOwnRun || !isTerminalRunStatus(latestOwnRun.status)) {
252
+ return false;
253
+ }
254
+ const runEnd = parseTimeMs(latestOwnRun.endTime);
255
+ const taskUpdated = parseTimeMs(task.updatedAt);
256
+ return runEnd !== null && taskUpdated !== null && runEnd >= taskUpdated;
257
+ }
258
+
259
+ function needsCrashStatusReconcile(task, latestOwnRun) {
260
+ return task.status === 'in_progress' && !latestRunCoversInProgressEntry(latestOwnRun, task);
261
+ }
262
+
263
+ async function hasReconciliationRun(reconcileKey) {
264
+ const runs = await getRecentRuns(Infinity);
265
+ return runs.some(run => run && run.reconciledCrash === true && run.reconcileKey === reconcileKey);
266
+ }
267
+
268
+ async function getRecoveryWorkspaceState() {
269
+ try {
270
+ const snapshot = await snapshotWorkspace(process.cwd());
271
+ if (snapshot.kind === 'git' && Array.isArray(snapshot.lines) && snapshot.lines.length > 0) {
272
+ return 'dirty-partial';
273
+ }
274
+ } catch (error) {
275
+ return 'clean';
276
+ }
277
+ return 'clean';
278
+ }
279
+
280
+ function recoveryResult(task, fields = {}) {
281
+ return {
282
+ taskId: Number(task.id),
283
+ taskStatus: task.status,
284
+ action: fields.action || 'noop',
285
+ lockOutcome: fields.lockOutcome || null,
286
+ lockCorrupted: fields.lockCorrupted === true,
287
+ recoveredFromPid: fields.recoveredFromPid ?? null,
288
+ activeLock: fields.activeLock || null,
289
+ reconcileKey: fields.reconcileKey || null,
290
+ run: fields.run || null
291
+ };
292
+ }
293
+
294
+ function printRecoverySummary(summary) {
295
+ printSafe('Crash recovery: ' + summary.reconciled + ' reconciled, ' + summary.releasedLocks + ' orphan locks released, ' + summary.skipped + ' skipped.');
296
+ for (const result of summary.results) {
297
+ const suffix = result.lockCorrupted ? ' lockCorrupted=true' : '';
298
+ printSafe('- #' + result.taskId + ' ' + result.action + (result.lockOutcome ? ' (' + result.lockOutcome + ')' : '') + suffix);
299
+ }
300
+ }
301
+
302
+ async function appendCrashReconciliationRun({ task, reconcileKey, recoveredFromPid, lockCorrupted }) {
303
+ const startTime = new Date().toISOString();
304
+ const workspaceState = await getRecoveryWorkspaceState();
305
+ const run = await logRun({
306
+ taskId: Number(task.id),
307
+ status: 'needs_human',
308
+ startTime,
309
+ endTime: new Date().toISOString(),
310
+ strategy: 'none',
311
+ exitCode: 2,
312
+ reconciledCrash: true,
313
+ reconcileKey,
314
+ recoveredFromPid: recoveredFromPid ?? null,
315
+ crashWindow: 'in_progress_no_terminal_run',
316
+ failureCategory: 'process_terminated_unexpectedly',
317
+ infrastructureFailure: true,
318
+ workspaceState,
319
+ warnings: [CRASH_RECONCILIATION_WARNING],
320
+ ...(lockCorrupted ? { lockCorrupted: true } : {})
321
+ });
322
+ await logMemory('Task #' + task.id + ' reconciled from crash (in_progress -> needs_human).');
323
+ return run;
324
+ }
325
+
326
+ export async function reconcileCrashedTasks(options = {}) {
327
+ const taskIds = options.taskIds ? new Set(options.taskIds.map(item => String(item))) : null;
328
+ const dryRun = options.dryRun === true;
329
+ const includeLockOnly = options.includeLockOnly !== false;
330
+ const tasks = await getTasks();
331
+ const summary = {
332
+ dryRun,
333
+ scanned: 0,
334
+ reconciled: 0,
335
+ releasedLocks: 0,
336
+ skipped: 0,
337
+ results: []
338
+ };
339
+
340
+ for (const task of tasks) {
341
+ if (taskIds && !taskIds.has(String(task.id))) {
342
+ continue;
343
+ }
344
+
345
+ const inspection = await inspectTaskLock(task.id);
346
+ if (task.status !== 'in_progress' && (!includeLockOnly || !inspection.exists)) {
347
+ continue;
348
+ }
349
+
350
+ summary.scanned += 1;
351
+ const latestOwnRun = await getLatestRunForTask(task.id);
352
+ const needsStatusReconcile = needsCrashStatusReconcile(task, latestOwnRun);
353
+ const reconcileKey = String(task.id) + ':' + String(task.updatedAt || '');
354
+ const recoveredFromPid = inspection.lock && Number.isInteger(inspection.lock.pid) ? inspection.lock.pid : null;
355
+
356
+ if (dryRun) {
357
+ const action = inspection.heldByActive
358
+ ? 'skip_held_by_active'
359
+ : needsStatusReconcile
360
+ ? 'would_reconcile_to_needs_human'
361
+ : inspection.exists
362
+ ? 'would_release_orphan_lock_only'
363
+ : 'noop';
364
+ if (action.startsWith('skip') || action === 'noop') summary.skipped += 1;
365
+ summary.results.push(recoveryResult(task, {
366
+ action,
367
+ lockOutcome: inspection.heldByActive ? 'held_by_active' : inspection.exists ? 'inspect_only' : 'no_lock',
368
+ lockCorrupted: inspection.lockCorrupted,
369
+ recoveredFromPid,
370
+ activeLock: inspection.activeLock,
371
+ reconcileKey
372
+ }));
373
+ continue;
374
+ }
375
+
376
+ if (inspection.lockCorrupted) {
377
+ if (!needsStatusReconcile) {
378
+ summary.skipped += 1;
379
+ summary.results.push(recoveryResult(task, { action: 'skip_lock_corrupted_no_status_reconcile', lockOutcome: 'lock_corrupted', lockCorrupted: true, reconcileKey }));
380
+ continue;
381
+ }
382
+ if (await hasReconciliationRun(reconcileKey)) {
383
+ summary.skipped += 1;
384
+ summary.results.push(recoveryResult(task, { action: 'noop_already_reconciled', lockOutcome: 'lock_corrupted', lockCorrupted: true, reconcileKey }));
385
+ continue;
386
+ }
387
+ await updateTaskStatus(task.id, 'needs_human');
388
+ const run = await appendCrashReconciliationRun({ task, reconcileKey, recoveredFromPid: null, lockCorrupted: true });
389
+ summary.reconciled += 1;
390
+ summary.results.push(recoveryResult(task, { action: 'reconciled_to_needs_human', lockOutcome: 'lock_corrupted', lockCorrupted: true, reconcileKey, run }));
391
+ continue;
392
+ }
393
+
394
+ const shouldProbeLock = needsStatusReconcile || (includeLockOnly && inspection.exists);
395
+ if (!shouldProbeLock) {
396
+ summary.skipped += 1;
397
+ summary.results.push(recoveryResult(task, { action: 'noop', reconcileKey }));
398
+ continue;
399
+ }
400
+
401
+ const taskLock = await acquireOrRecover(task.id, { command: 'recover task ' + task.id });
402
+ if (taskLock.outcome === 'held_by_active') {
403
+ summary.skipped += 1;
404
+ summary.results.push(recoveryResult(task, {
405
+ action: 'skip_held_by_active',
406
+ lockOutcome: 'held_by_active',
407
+ activeLock: taskLock.activeLock || null,
408
+ reconcileKey
409
+ }));
410
+ continue;
411
+ }
412
+
413
+ const lockHandle = taskLock.lockHandle;
414
+ try {
415
+ if (!needsStatusReconcile) {
416
+ summary.releasedLocks += 1;
417
+ summary.results.push(recoveryResult(task, {
418
+ action: 'released_orphan_lock_only',
419
+ lockOutcome: taskLock.outcome,
420
+ recoveredFromPid,
421
+ reconcileKey
422
+ }));
423
+ continue;
424
+ }
425
+
426
+ if (await hasReconciliationRun(reconcileKey)) {
427
+ summary.skipped += 1;
428
+ summary.results.push(recoveryResult(task, {
429
+ action: 'noop_already_reconciled',
430
+ lockOutcome: taskLock.outcome,
431
+ recoveredFromPid,
432
+ reconcileKey
433
+ }));
434
+ continue;
435
+ }
436
+
437
+ await updateTaskStatus(task.id, 'needs_human');
438
+ const run = await appendCrashReconciliationRun({ task, reconcileKey, recoveredFromPid, lockCorrupted: false });
439
+ summary.reconciled += 1;
440
+ summary.results.push(recoveryResult(task, {
441
+ action: 'reconciled_to_needs_human',
442
+ lockOutcome: taskLock.outcome,
443
+ recoveredFromPid,
444
+ reconcileKey,
445
+ run
446
+ }));
447
+ } finally {
448
+ if (lockHandle) {
449
+ await releaseTaskLock(lockHandle);
450
+ }
451
+ }
452
+ }
453
+
454
+ if (!options.suppressOutput) {
455
+ printRecoverySummary(summary);
456
+ }
457
+ return summary;
458
+ }
459
+
460
+ async function blockTaskBySafety(task, safety, startTime, options = {}) {
461
+ const findings = safety.findings || [];
462
+ const run = await withAccountingLock(async () => {
463
+ await updateTaskStatus(task.id, 'blocked');
464
+ const blockedRun = await logRun({
465
+ taskId: Number(task.id),
466
+ status: 'blocked',
467
+ blockedBySafety: true,
468
+ startTime,
469
+ endTime: new Date().toISOString(),
470
+ strategy: 'none',
471
+ exitCode: 2,
472
+ safetyFindings: findings,
473
+ warnings: [],
474
+ unsupportedToolCall: false,
475
+ suspectedFalsePositive: false,
476
+ windowsShellCommandMismatch: false,
477
+ syntaxValidation: [],
478
+ syntaxValidationFailed: false,
479
+ workspaceChanged: false,
480
+ changedFiles: [],
481
+ createdFiles: [],
482
+ deletedFiles: [],
483
+ analysisTaskChangedCode: false,
484
+ outOfScopeChanges: []
485
+ });
486
+ await recordUsage({
487
+ command: 'run-task',
488
+ strategy: 'none',
489
+ promptChars: ((task.title || '') + '\n' + (task.description || '')).length,
490
+ estimatedOutputTokens: 0,
491
+ exitCode: 2,
492
+ blockedBySafety: true,
493
+ safetyFindings: findings
494
+ });
495
+ return blockedRun;
496
+ });
497
+ await logMemory('Task #' + task.id + ' blocked by safety guard: ' + findings.join('; '));
498
+ if (!options.suppressOutput) {
499
+ printLines([
500
+ 'Task #' + task.id + ' - ' + (task.title || task.description || ''),
501
+ 'Engine: none',
502
+ 'Status final: blocked',
503
+ 'Erro acionavel: safety guard bloqueou a tarefa.',
504
+ 'Padroes detectados: ' + findings.join('; '),
505
+ 'RunId: ' + run.runId,
506
+ 'Log: ' + run.logPath,
507
+ 'Relatorio: ' + run.reportPath
508
+ ]);
509
+ }
510
+ return run;
511
+ }
512
+
513
+ // Flattens a runtime-gate.mjs result into the flat field set both
514
+ // RUNS.jsonl (logRun) and usage.jsonl (recordUsage) expect (Phase 6 of
515
+ // v0.5.1: runtimeGate, runtimeDecision, selectedManager, selectedEngine,
516
+ // preflightDecision, budgetDecision, policyDecision, forced).
517
+ function gateMetadataFields(gate) {
518
+ return {
519
+ runtimeGate: {
520
+ decision: gate.decision,
521
+ blockCategory: gate.blockCategory,
522
+ taskKind: gate.taskClassification.taskKind,
523
+ complexity: gate.taskClassification.complexity,
524
+ risk: gate.taskClassification.risk,
525
+ reason: gate.reason,
526
+ fallback: gate.fallback
527
+ },
528
+ runtimeDecision: gate.decision,
529
+ blockedReasonCategory: gate.blockCategory || null,
530
+ selectedManager: gate.selectedManager ? gate.selectedManager.sector : null,
531
+ selectedEngine: gate.selectedEngine ? gate.selectedEngine.id : null,
532
+ preflightDecision: gate.preflight ? gate.preflight.decision : null,
533
+ budgetDecision: gate.budget && gate.budget.evaluation ? gate.budget.evaluation.allowed : null,
534
+ policyDecision: gate.policy ? gate.policy.ok : null,
535
+ forced: gate.forced === true
536
+ };
537
+ }
538
+
539
+ // The runtime gate stopped the task before any worker was called
540
+ // (BLOCK/NEEDS_HUMAN/DELEGATE/FALLBACK). Mirrors blockTaskBySafety's shape
541
+ // so RUNS.jsonl/usage.jsonl stay consistent, but carries the gate's own
542
+ // reasoning instead of a safety-guard finding list. Per project rule,
543
+ // --force can only ever turn a NEEDS_HUMAN gate result into EXECUTE — by
544
+ // the time this function runs, that has already happened inside
545
+ // evaluateTaskRun(), so a gate that reaches here truly could not run.
546
+ async function handleGateNonExecute(task, gate, startTime, options = {}) {
547
+ const newStatus = gate.decision === 'NEEDS_HUMAN' ? 'needs_human' : 'blocked';
548
+ const gateFields = gateMetadataFields(gate);
549
+ const run = await withAccountingLock(async () => {
550
+ await updateTaskStatus(task.id, newStatus);
551
+ const gateRun = await logRun({
552
+ taskId: Number(task.id),
553
+ status: newStatus,
554
+ blockedBySafety: false,
555
+ blockedByGate: true,
556
+ startTime,
557
+ endTime: new Date().toISOString(),
558
+ strategy: 'none',
559
+ exitCode: 2,
560
+ warnings: gate.reason,
561
+ unsupportedToolCall: false,
562
+ suspectedFalsePositive: false,
563
+ windowsShellCommandMismatch: false,
564
+ syntaxValidation: [],
565
+ syntaxValidationFailed: false,
566
+ workspaceChanged: false,
567
+ changedFiles: [],
568
+ createdFiles: [],
569
+ deletedFiles: [],
570
+ analysisTaskChangedCode: false,
571
+ outOfScopeChanges: [],
572
+ ...gateFields
573
+ });
574
+ await recordUsage({
575
+ command: 'run-task',
576
+ strategy: 'none',
577
+ promptChars: ((task.title || '') + '\n' + (task.description || '')).length,
578
+ estimatedOutputTokens: 0,
579
+ exitCode: 2,
580
+ blockedBySafety: false,
581
+ blockedByGate: true,
582
+ ...gateFields
583
+ });
584
+ return gateRun;
585
+ });
586
+ await logMemory('Task #' + task.id + ' stopped by runtime gate: ' + gate.decision + ' (' + gate.reason.join('; ') + ').');
587
+ if (!options.suppressOutput) {
588
+ const lines = [
589
+ 'Task #' + task.id + ' - ' + (task.title || task.description || ''),
590
+ 'Engine: ' + (gate.selectedEngine ? gate.selectedEngine.id || 'none' : 'none'),
591
+ 'Status final: ' + newStatus,
592
+ 'Erro acionavel: runtime gate ' + gate.decision + '.',
593
+ ...gate.reason.map(reason => '- ' + reason),
594
+ 'RunId: ' + run.runId,
595
+ 'Log: ' + run.logPath,
596
+ 'Relatorio: ' + run.reportPath
597
+ ];
598
+ if (newStatus === 'needs_human') {
599
+ lines.push(formatForceAction(gate, task.id));
600
+ }
601
+ if (options.debug) {
602
+ lines.push('');
603
+ lines.push(JSON.stringify(gate, null, 2));
604
+ }
605
+ printLines(lines);
606
+ }
607
+ return run;
608
+ }
609
+
610
+ // Tasks whose legitimate success doesn't touch anything the workspace diff
611
+ // can see: analysis-only tasks (not supposed to touch code), memory-engine
612
+ // tasks (real output lives inside .memory/, excluded from the diff by
613
+ // design), and review/audit tasks (not supposed to touch anything either).
614
+ export function isNoChangeExpectedTask(task, analysisOnly = isAnalysisOnlyTask(task)) {
615
+ return analysisOnly || task.engine === 'memory' || /revis[aã]o|\breview\b/i.test(task.title || '');
616
+ }
617
+
618
+ export function decideTaskStatus({
619
+ exitCode,
620
+ analysisTaskChangedCode,
621
+ deletedOutOfScope,
622
+ syntaxValidationFailed,
623
+ unsupportedToolCall,
624
+ suspectedFalsePositive,
625
+ windowsShellCommandMismatch,
626
+ explicitWorkerFailure = false,
627
+ workspaceChanged = false,
628
+ hostValidationFallbackPassed = false,
629
+ infrastructureFailure = false,
630
+ deliverableVerification = null
631
+ }) {
632
+ if (exitCode !== 0 && !(infrastructureFailure && hostValidationFallbackPassed)) {
633
+ return 'failed';
634
+ }
635
+ if (analysisTaskChangedCode) {
636
+ return 'failed';
637
+ }
638
+ if (deletedOutOfScope) {
639
+ return 'failed';
640
+ }
641
+ if (syntaxValidationFailed) {
642
+ return 'failed';
643
+ }
644
+ if (deliverableVerification && deliverableVerification.required !== false) {
645
+ if (deliverableVerification.outcome === 'rejected' || deliverableVerification.outcome === 'inconclusive') {
646
+ return 'failed';
647
+ }
648
+ if (deliverableVerification.outcome === 'blocked') {
649
+ return 'blocked';
650
+ }
651
+ if (deliverableVerification.outcome === 'needs_human') {
652
+ return 'needs_human';
653
+ }
654
+ }
655
+ // The worker's own process can exit 0 while its final answer explicitly
656
+ // says it could not finish the task -- that can never read as `completed`.
657
+ // Partial, verified progress (some files really changed) downgrades this
658
+ // to a warning instead of a hard failure; no verified change at all means
659
+ // nothing usable was delivered, so it's a real failure.
660
+ if (explicitWorkerFailure) {
661
+ return workspaceChanged ? 'completed_with_warnings' : 'failed';
662
+ }
663
+ if (hostValidationFallbackPassed) {
664
+ return 'completed_with_warnings';
665
+ }
666
+ if (unsupportedToolCall || suspectedFalsePositive || windowsShellCommandMismatch) {
667
+ return 'completed_with_warnings';
668
+ }
669
+ return 'completed';
670
+ }
671
+
672
+ function inferHostValidationInputs({ output, projectRoot, changedFiles = [], createdFiles = [], policy }) {
673
+ const text = String(output || '');
674
+ const inputs = [];
675
+ const push = commandId => {
676
+ if (!inputs.some(item => item.commandId === commandId) && policy.hostValidationCommands.includes(commandId)) {
677
+ inputs.push({ commandId, projectRoot, policy });
678
+ }
679
+ };
680
+ if (/npm\s+run\s+check/i.test(text)) {
681
+ push('npm-run-check');
682
+ }
683
+ if (/npm(?:\.cmd)?\s+(?:test|[^\r\n]*\btest\b)/i.test(text)) {
684
+ push('npm-test');
685
+ }
686
+ if (/node(?:\.exe)?\s+--check/i.test(text) && policy.hostValidationCommands.includes('node-check')) {
687
+ for (const file of [...changedFiles, ...createdFiles].filter(item => /\.(mjs|js)$/i.test(item))) {
688
+ inputs.push({ commandId: 'node-check', projectRoot, file, policy });
689
+ }
690
+ }
691
+ return inputs;
692
+ }
693
+
694
+ async function runAllowedHostValidationFallback({ output, projectRoot, changedFiles, createdFiles, failure }) {
695
+ const budget = await loadBudget();
696
+ const policy = getHostValidationPolicy(budget);
697
+ if (!policy.allowHostValidationFallback || failure.category !== 'nested_sandbox_process_failure') {
698
+ return { used: false, commands: [], result: 'not_run', results: [] };
699
+ }
700
+ const inputs = inferHostValidationInputs({ output, projectRoot, changedFiles, createdFiles, policy });
701
+ if (!inputs.length) {
702
+ return { used: false, commands: [], result: 'not_applicable', results: [] };
703
+ }
704
+ const results = [];
705
+ for (const input of inputs) {
706
+ results.push(await runHostValidation(input));
707
+ }
708
+ return { ...summarizeHostValidation(results), results };
709
+ }
710
+
711
+ function printRunSummary({ task, engineName, status, warnings, changedFiles, createdFiles, deletedFiles, result, syntaxValidationFailed, hostValidationFallback, run, verbose, debug, diffText }) {
712
+ const workerLine = result.code === 0 ? '✓ Worker finalizado' : 'x Worker falhou (codigo ' + result.code + ')';
713
+ printLines([
714
+ 'Task #' + task.id + ' - ' + (task.title || task.description || ''),
715
+ 'Engine: ' + engineName,
716
+ 'Status: running',
717
+ workerLine,
718
+ formatChangedSummary(countChangedFiles(changedFiles, createdFiles, deletedFiles)),
719
+ validationSummary({ syntaxValidationFailed, hostValidationFallback })
720
+ ]);
721
+ if (warnings.length) {
722
+ printSafe('');
723
+ printSafe('Warnings acionaveis:');
724
+ for (const warning of warnings) {
725
+ printSafe('- ' + warning);
726
+ }
727
+ }
728
+ printLines([
729
+ '',
730
+ 'Status final: ' + status,
731
+ 'RunId: ' + run.runId,
732
+ 'Log: ' + run.logPath,
733
+ 'Relatorio: ' + run.reportPath
734
+ ]);
735
+ if (verbose || debug) {
736
+ printVerboseSections({ result, diffText, runId: run.runId, debug });
737
+ }
738
+ }
739
+
740
+ export async function runTask(id, options = {}) {
741
+ let task = await withTransientJsonRetry(() => getTaskById(id));
742
+ if (!task) {
743
+ console.log('Task #' + id + ' not found.');
744
+ return;
745
+ }
746
+ await withTransientJsonRetry(() => reconcileCrashedTasks({ taskIds: [id], dryRun: options.dryRun === true, suppressOutput: options.suppressOutput === true, includeLockOnly: false }));
747
+ task = await withTransientJsonRetry(() => getTaskById(id));
748
+ if (task.status === 'completed' || task.status === 'completed_with_warnings' || task.status === 'failed') {
749
+ console.log('Task #' + id + ' already ' + task.status + '. Use maestro retry ' + id + ' first.');
750
+ return;
751
+ }
752
+ if (task.status === 'blocked') {
753
+ console.log('Task #' + id + ' is blocked. Use maestro task pending ' + id + ' to reopen it.');
754
+ return;
755
+ }
756
+ if (task.status === 'needs_human' && !options.force) {
757
+ printSafe('Task #' + id + ' needs a human decision before it can run. Use maestro task pending ' + id + ' to reopen it after review.');
758
+ return;
759
+ }
760
+ if (task.status === 'archived') {
761
+ console.log('Task #' + id + ' is archived and cannot be run.');
762
+ return;
763
+ }
764
+
765
+ const runsSoFar = await withTransientJsonRetry(() => countRunsForTask(id));
766
+ const gate = await withTransientJsonRetry(() => evaluateTaskRun({
767
+ task,
768
+ projectRoot: process.cwd(),
769
+ mode: options.dryRun ? 'dry-run' : 'real',
770
+ force: !!options.force,
771
+ runsSoFar
772
+ }));
773
+
774
+ if (options.dryRun) {
775
+ printSafe(JSON.stringify(gate, null, 2));
776
+ printSafe('\n(dry-run: no engine, worker, or model was actually called)');
777
+ return;
778
+ }
779
+
780
+ if (gate.decision !== 'EXECUTE') {
781
+ const run = await handleGateNonExecute(task, gate, new Date().toISOString(), options);
782
+ return { taskId: Number(id), status: gate.decision === 'NEEDS_HUMAN' ? 'needs_human' : 'blocked', run };
783
+ }
784
+
785
+ const taskLock = await acquireOrRecover(id, { command: 'run-task ' + id });
786
+ if (taskLock.outcome === 'held_by_active') {
787
+ if (!options.suppressOutput) {
788
+ printLines([
789
+ 'Task #' + task.id + ' - ' + (task.title || task.description || ''),
790
+ 'Status final: blocked',
791
+ 'Erro acionavel: task lock held by active owner.',
792
+ 'PID ativo: ' + (taskLock.activeLock && taskLock.activeLock.pid != null ? taskLock.activeLock.pid : 'unknown')
793
+ ]);
794
+ }
795
+ return {
796
+ taskId: Number(id),
797
+ status: 'blocked',
798
+ lockOutcome: 'held_by_active',
799
+ activeLock: taskLock.activeLock || null,
800
+ run: null
801
+ };
802
+ }
803
+
804
+ const lockHandle = taskLock.lockHandle;
805
+ const lockOutcome = taskLock.outcome;
806
+ const lockRecovery = lockOutcome === 'recovered_and_acquired'
807
+ ? { recovered: true, recoveredFromPid: taskLock.activeLock && taskLock.activeLock.pid != null ? taskLock.activeLock.pid : null }
808
+ : null;
809
+
810
+ try {
811
+ const prompt = buildWorkerPrompt(task);
812
+ const startTime = new Date().toISOString();
813
+ const engine = options.engine || getEngine((gate.selectedEngine && gate.selectedEngine.id) || task.engine);
814
+ const analysisOnly = isAnalysisOnlyTask(task);
815
+ const workspaceBefore = await snapshotWorkspace(process.cwd());
816
+ let blockedRun = null;
817
+ const safetyResult = await executeTaskWithSafety(
818
+ task,
819
+ async () => {
820
+ await withAccountingLock(() => updateTaskStatus(id, 'in_progress'));
821
+ return engine.run(prompt, { quiet: true });
822
+ },
823
+ async ({ safety }) => {
824
+ blockedRun = await blockTaskBySafety(task, safety, startTime, options);
825
+ }
826
+ );
827
+ if (safetyResult.blocked) {
828
+ return { taskId: Number(id), status: 'blocked', run: blockedRun, lockOutcome };
829
+ }
830
+ const result = safetyResult.result;
831
+ const output = (result.stdout || '') + '\n' + (result.stderr || '');
832
+ const workspaceAfter = await snapshotWorkspace(process.cwd());
833
+ const { workspaceChanged, changedFiles, createdFiles, deletedFiles, outOfScopeChanges } = diffWorkspace(
834
+ workspaceBefore,
835
+ workspaceAfter,
836
+ { scopeFiles: task.files || [] }
837
+ );
838
+ const analysisTaskChangedCode = analysisOnly && (changedFiles.length > 0 || createdFiles.length > 0 || deletedFiles.length > 0);
839
+ const deletedOutOfScope = deletedFiles.length > 0;
840
+ const unsupportedToolCall = detectUnsupportedToolCall(output);
841
+ const windowsShellCommandMismatch = detectWindowsShellMismatch(output);
842
+ const { syntaxValidation, syntaxValidationFailed } = await validateChangedFiles([...changedFiles, ...createdFiles]);
843
+ // A memory/documentation task's real output normally lives inside .memory/,
844
+ // which diffWorkspace excludes by design, and a review/audit task isn't
845
+ // expected to touch any file either — don't treat either as a lie.
846
+ const noChangeExpected = isNoChangeExpectedTask(task, analysisOnly);
847
+ const workerFailure = classifyFailure({ exitCode: result.code, stdout: result.stdout, stderr: result.stderr });
848
+ const diagnostics = buildRunDiagnostics({ output, unsupportedToolCall, workspaceChanged, noChangeExpected });
849
+ if (windowsShellCommandMismatch) {
850
+ diagnostics.warnings.push('comando incompatível com PowerShell foi usado pelo worker');
851
+ }
852
+ if (analysisTaskChangedCode) {
853
+ diagnostics.warnings.push('analysis-only task changed code');
854
+ }
855
+ if (deletedOutOfScope) {
856
+ diagnostics.warnings.push('worker deleted files');
857
+ }
858
+ if (syntaxValidationFailed) {
859
+ for (const entry of syntaxValidation.filter(item => item.ok === false)) {
860
+ diagnostics.warnings.push('syntax validation failed: ' + entry.path);
861
+ }
862
+ }
863
+ if (workerFailure.category === 'nested_sandbox_process_failure') {
864
+ diagnostics.warnings.push('worker validation blocked by nested sandbox');
865
+ }
866
+ if (lockRecovery) {
867
+ diagnostics.warnings.push('recovered orphaned task lock before execution');
868
+ }
869
+
870
+ const hostValidationFallback = await runAllowedHostValidationFallback({
871
+ output,
872
+ projectRoot: process.cwd(),
873
+ changedFiles,
874
+ createdFiles,
875
+ failure: workerFailure
876
+ });
877
+ if (hostValidationFallback.used && hostValidationFallback.result === 'passed') {
878
+ diagnostics.warnings.push('worker validation blocked by nested sandbox; host validation passed');
879
+ } else if (hostValidationFallback.used && hostValidationFallback.result === 'failed') {
880
+ diagnostics.warnings.push('worker validation blocked by nested sandbox; host validation failed');
881
+ }
882
+
883
+ const deliverableVerification = await verifyDeliverableEvidence({
884
+ task,
885
+ projectRoot: process.cwd(),
886
+ changedFiles,
887
+ createdFiles,
888
+ deletedFiles,
889
+ outOfScopeChanges,
890
+ syntaxValidation,
891
+ hostValidationFallback,
892
+ noChangeExpected
893
+ });
894
+ for (const warning of deliverableVerification.warnings || []) {
895
+ diagnostics.warnings.push('deliverable verification warning: ' + warning);
896
+ }
897
+ for (const failure of deliverableVerification.failures || []) {
898
+ diagnostics.warnings.push('deliverable verification failed: ' + failure);
899
+ }
900
+ for (const missing of deliverableVerification.missingEvidence || []) {
901
+ diagnostics.warnings.push('deliverable verification missing evidence: ' + missing);
902
+ }
903
+
904
+ const status = decideTaskStatus({
905
+ exitCode: result.code,
906
+ analysisTaskChangedCode,
907
+ deletedOutOfScope,
908
+ syntaxValidationFailed,
909
+ unsupportedToolCall,
910
+ suspectedFalsePositive: diagnostics.suspectedFalsePositive,
911
+ windowsShellCommandMismatch,
912
+ explicitWorkerFailure: diagnostics.explicitWorkerFailure,
913
+ workspaceChanged,
914
+ hostValidationFallbackPassed: hostValidationFallback.used && hostValidationFallback.result === 'passed',
915
+ infrastructureFailure: workerFailure.infrastructureFailure === true,
916
+ deliverableVerification
917
+ });
918
+
919
+ const diffText = await collectDiffText();
920
+ await withAccountingLock(() => updateTaskStatus(id, status));
921
+ const gateFields = gateMetadataFields(gate);
922
+ const run = await withAccountingLock(() => logRun({
923
+ taskId: Number(id),
924
+ status,
925
+ startTime,
926
+ endTime: new Date().toISOString(),
927
+ strategy: result.strategy,
928
+ exitCode: result.code,
929
+ signal: result.signal || null,
930
+ timedOut: result.timedOut === true,
931
+ stdout: summarizeText(result.stdout),
932
+ stderr: summarizeText(result.stderr),
933
+ fullStdout: result.stdout || '',
934
+ fullStderr: result.stderr || '',
935
+ diffText,
936
+ warnings: diagnostics.warnings,
937
+ workerValidation: {
938
+ attempted: true,
939
+ completed: workerFailure.category !== 'nested_sandbox_process_failure',
940
+ failureCategory: workerFailure.category === 'nested_sandbox_process_failure' ? workerFailure.category : null,
941
+ infrastructureFailure: workerFailure.infrastructureFailure === true
942
+ },
943
+ hostValidationFallback: {
944
+ used: hostValidationFallback.used,
945
+ commands: hostValidationFallback.commands || [],
946
+ result: hostValidationFallback.result,
947
+ metadata: hostValidationFallback.metadata || []
948
+ },
949
+ failureCategory: workerFailure.category,
950
+ infrastructureFailure: workerFailure.infrastructureFailure === true,
951
+ unsupportedToolCall: diagnostics.unsupportedToolCall,
952
+ suspectedFalsePositive: diagnostics.suspectedFalsePositive,
953
+ explicitWorkerFailure: diagnostics.explicitWorkerFailure,
954
+ explicitFailureEvidence: diagnostics.explicitFailureEvidence,
955
+ windowsShellCommandMismatch,
956
+ syntaxValidation,
957
+ syntaxValidationFailed,
958
+ deliverableVerification,
959
+ workspaceChanged,
960
+ changedFiles,
961
+ createdFiles,
962
+ deletedFiles,
963
+ analysisTaskChangedCode,
964
+ outOfScopeChanges,
965
+ lockOutcome,
966
+ ...(lockRecovery ? { lockRecovery } : {}),
967
+ ...gateFields
968
+ }));
969
+ await withAccountingLock(() => recordUsage({
970
+ command: 'run-task',
971
+ strategy: result.strategy,
972
+ prompt,
973
+ output,
974
+ exitCode: result.code,
975
+ warnings: diagnostics.warnings,
976
+ workerValidation: {
977
+ attempted: true,
978
+ completed: workerFailure.category !== 'nested_sandbox_process_failure',
979
+ failureCategory: workerFailure.category === 'nested_sandbox_process_failure' ? workerFailure.category : null,
980
+ infrastructureFailure: workerFailure.infrastructureFailure === true
981
+ },
982
+ hostValidationFallback: {
983
+ used: hostValidationFallback.used,
984
+ commands: hostValidationFallback.commands || [],
985
+ result: hostValidationFallback.result
986
+ },
987
+ failureCategory: workerFailure.category,
988
+ infrastructureFailure: workerFailure.infrastructureFailure === true,
989
+ unsupportedToolCall: diagnostics.unsupportedToolCall,
990
+ suspectedFalsePositive: diagnostics.suspectedFalsePositive,
991
+ explicitWorkerFailure: diagnostics.explicitWorkerFailure,
992
+ windowsShellCommandMismatch,
993
+ syntaxValidationFailed,
994
+ deliverableVerification,
995
+ analysisTaskChangedCode,
996
+ deletedFiles,
997
+ ...gateFields
998
+ }));
999
+ await logMemory('Task #' + id + ' finished as ' + status + ' (exit code ' + result.code + ') using engine ' + engine.name + ' (strategy ' + result.strategy + ')' + (diagnostics.warnings.length ? '; warnings: ' + diagnostics.warnings.join('; ') : '') + '.');
1000
+
1001
+ if (!options.suppressOutput) {
1002
+ printRunSummary({
1003
+ task,
1004
+ engineName: engine.name,
1005
+ status,
1006
+ warnings: diagnostics.warnings,
1007
+ changedFiles,
1008
+ createdFiles,
1009
+ deletedFiles,
1010
+ result,
1011
+ syntaxValidationFailed,
1012
+ hostValidationFallback,
1013
+ run,
1014
+ verbose: !!options.verbose,
1015
+ debug: !!options.debug,
1016
+ diffText
1017
+ });
1018
+ }
1019
+ return {
1020
+ taskId: Number(id),
1021
+ status,
1022
+ warnings: diagnostics.warnings,
1023
+ changedFiles,
1024
+ createdFiles,
1025
+ deletedFiles,
1026
+ run,
1027
+ result,
1028
+ diagnostics,
1029
+ output,
1030
+ lockOutcome,
1031
+ ...(lockRecovery ? { lockRecovery } : {})
1032
+ };
1033
+ } finally {
1034
+ await releaseTaskLock(lockHandle);
1035
+ }
1036
+ }
1037
+
1038
+ export async function runOne(options = {}) {
1039
+ const tasks = await getTasks();
1040
+ const task = getNextTask(tasks);
1041
+ if (!task) {
1042
+ console.log('No pending tasks found.');
1043
+ return;
1044
+ }
1045
+ await runTask(task.id, options);
1046
+ }
1047
+
1048
+ export async function runAll(options = {}) {
1049
+ const limitArgIndex = process.argv.indexOf('--limit');
1050
+ const limit = limitArgIndex >= 0 ? parseInt(process.argv[limitArgIndex + 1], 10) : Infinity;
1051
+ await reconcileCrashedTasks({ dryRun: options.dryRun === true, suppressOutput: options.suppressOutput === true });
1052
+ const tasks = await getTasks();
1053
+ let count = 0;
1054
+ const summary = { completed: 0, completed_with_warnings: 0, failed: 0, not_started: 0 };
1055
+ let lastReportPath = null;
1056
+ const runnableTasks = sortRunnableTasks(tasks.filter(item => item.status === 'pending')).slice(0, limit);
1057
+ if (options.dryRun) {
1058
+ printSafe('Dry-run: tasks pending que seriam executadas:');
1059
+ if (runnableTasks.length === 0) {
1060
+ printSafe('- nenhuma');
1061
+ }
1062
+ for (const task of runnableTasks) {
1063
+ printSafe('- #' + task.id + ' [' + (task.priority || 'medium') + '] ' + (task.title || task.description || ''));
1064
+ }
1065
+ }
1066
+ for (const task of runnableTasks) {
1067
+ const result = await runTask(task.id, { ...options, suppressOutput: true });
1068
+ count++;
1069
+ const refreshed = await getTaskById(task.id);
1070
+ const status = refreshed ? refreshed.status : (result && result.status) || 'not_started';
1071
+ if (status === 'completed') summary.completed++;
1072
+ else if (status === 'completed_with_warnings') summary.completed_with_warnings++;
1073
+ else if (status === 'failed') summary.failed++;
1074
+ else summary.not_started++;
1075
+ if (result && result.run && result.run.reportPath) {
1076
+ lastReportPath = result.run.reportPath;
1077
+ }
1078
+ const icon = status === 'completed' ? '✓' : status === 'completed_with_warnings' ? '⚠' : status === 'failed' ? 'x' : '-';
1079
+ printSafe(icon + ' #' + task.id + ' ' + status);
1080
+ if (refreshed && (refreshed.status === 'failed' || refreshed.status === 'blocked' || refreshed.status === 'needs_human')) {
1081
+ break;
1082
+ }
1083
+ }
1084
+ if (!options.dryRun) {
1085
+ await generateCheckpoint();
1086
+ await recordUsage({ command: 'run-all', promptChars: 0, estimatedOutputTokens: 0, exitCode: 0, tasksRun: count });
1087
+ }
1088
+ printSafe('Resumo: ' + summary.completed + ' concluidas, ' + summary.completed_with_warnings + ' com avisos, ' + summary.failed + ' falhou, ' + summary.not_started + ' nao iniciada');
1089
+ if (lastReportPath) {
1090
+ printSafe('Relatorio: ' + lastReportPath);
1091
+ }
1092
+ }
1093
+
1094
+ export async function retry(id, options = {}) {
1095
+ await updateTaskStatus(id, 'pending');
1096
+ await runTask(id, options);
1097
+ }
1098
+
1099
+ export async function taskCommand(args) {
1100
+ if (args[0] === 'list') {
1101
+ const tasks = await getTasks();
1102
+ const counts = countTasksByStatus(tasks);
1103
+ console.log('Tasks: ' + JSON.stringify(counts));
1104
+ for (const task of sortRunnableTasks(tasks)) {
1105
+ const date = String(task.updatedAt || task.createdAt || '').slice(0, 10);
1106
+ console.log('- #' + task.id + ' [' + task.status + '] [' + (task.priority || 'medium') + '] [' + (task.engine || 'codex9') + '] ' + date + ' ' + (task.title || task.description));
1107
+ }
1108
+ const next = getNextTask(tasks);
1109
+ console.log('Next: ' + (next ? '#' + next.id + ' ' + (next.title || next.description) : 'none'));
1110
+ return;
1111
+ }
1112
+ if (args[0] === 'next') {
1113
+ const next = getNextTask(await getTasks());
1114
+ console.log(next ? JSON.stringify(next, null, 2) : 'No pending task.');
1115
+ return;
1116
+ }
1117
+ if (args[0] === 'archive-failed') {
1118
+ const result = await archiveTasks(task => task.status === 'failed', 'archive-failed');
1119
+ await logMemory('Archived failed tasks: ' + result.archived.length + (result.archivePath ? ' to ' + result.archivePath : ''));
1120
+ console.log('Archived failed tasks: ' + result.archived.length + (result.archivePath ? ' -> ' + result.archivePath : ''));
1121
+ return;
1122
+ }
1123
+ if (args[0] === 'archive-completed') {
1124
+ const result = await archiveTasks(task => task.status === 'completed', 'archive-completed');
1125
+ await logMemory('Archived completed tasks: ' + result.archived.length + (result.archivePath ? ' to ' + result.archivePath : ''));
1126
+ console.log('Archived completed tasks: ' + result.archived.length + (result.archivePath ? ' -> ' + result.archivePath : ''));
1127
+ return;
1128
+ }
1129
+ if (args[0] === 'clean-test') {
1130
+ const testPattern = /(MAESTRO_TESTE\.md|MAESTRO_WORKER_TEST\.md|MAESTRO_CODEX_TEST\.md|WORKING_|PROBE_)/i;
1131
+ const result = await archiveTasks(task => testPattern.test((task.title || '') + ' ' + (task.description || '')), 'clean-test');
1132
+ await logMemory('Archived test tasks: ' + result.archived.length + (result.archivePath ? ' to ' + result.archivePath : ''));
1133
+ console.log('Archived test tasks: ' + result.archived.length + (result.archivePath ? ' -> ' + result.archivePath : ''));
1134
+ return;
1135
+ }
1136
+ if (args[0] === 'show' && args[1]) {
1137
+ const task = await getTaskById(args[1]);
1138
+ if (!task) {
1139
+ console.log('Task not found.');
1140
+ return;
1141
+ }
1142
+ const lastRun = await getLatestRunForTask(args[1]);
1143
+ const lastOrchestration = buildOrchestrationSummary(await getLatestOrchestrationForTask(args[1]));
1144
+ console.log(JSON.stringify({
1145
+ ...task,
1146
+ lastRun: lastRun ? {
1147
+ status: lastRun.status,
1148
+ exitCode: lastRun.exitCode,
1149
+ warnings: lastRun.warnings || [],
1150
+ changedFiles: lastRun.changedFiles || [],
1151
+ createdFiles: lastRun.createdFiles || [],
1152
+ deletedFiles: lastRun.deletedFiles || [],
1153
+ analysisTaskChangedCode: !!lastRun.analysisTaskChangedCode,
1154
+ syntaxValidationFailed: !!lastRun.syntaxValidationFailed,
1155
+ endTime: lastRun.endTime
1156
+ } : null,
1157
+ lastOrchestration
1158
+ }, null, 2));
1159
+ return;
1160
+ }
1161
+ if (args[0] === 'add' && args[1]) {
1162
+ const task = await addTask(args.slice(1).join(' '));
1163
+ console.log('Task #' + task.id + ' added.');
1164
+ return;
1165
+ }
1166
+ if (args[0] === 'complete' && args[1]) {
1167
+ await updateTaskStatus(args[1], 'completed');
1168
+ console.log('Task #' + args[1] + ' marked completed.');
1169
+ return;
1170
+ }
1171
+ if (args[0] === 'fail' && args[1]) {
1172
+ await updateTaskStatus(args[1], 'failed');
1173
+ console.log('Task #' + args[1] + ' marked failed.');
1174
+ return;
1175
+ }
1176
+ if (args[0] === 'pending' && args[1]) {
1177
+ await updateTaskStatus(args[1], 'pending');
1178
+ console.log('Task #' + args[1] + ' reopened as pending. Aviso: a próxima execução pode bloquear de novo pelo safety guard.');
1179
+ return;
1180
+ }
1181
+ console.log('Usage: maestro task list | next | show <id> | add "<description>" | complete <id> | fail <id> | pending <id> | archive-failed | archive-completed | clean-test');
1182
+ }