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