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
package/src/tasks.mjs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { CONFIG } from './config.mjs';
|
|
4
|
+
import { readJsonSafe, writeJsonWithBackup } from './files.mjs';
|
|
5
|
+
import { stampSchemaVersion, validateTask } from './schema.mjs';
|
|
6
|
+
|
|
7
|
+
const tasksFilePath = path.join(CONFIG.maestroPath, 'TASKS.json');
|
|
8
|
+
const priorityRank = { high: 0, medium: 1, low: 2 };
|
|
9
|
+
const activeStatuses = new Set(['pending', 'in_progress', 'failed', 'blocked']);
|
|
10
|
+
|
|
11
|
+
export async function getTasks() {
|
|
12
|
+
return readJsonSafe(tasksFilePath, []);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function saveTasks(tasks) {
|
|
16
|
+
for (const task of tasks) {
|
|
17
|
+
const result = validateTask(task);
|
|
18
|
+
if (!result.ok) {
|
|
19
|
+
console.warn('Task #' + task.id + ' failed schema validation: ' + result.errors.join(', '));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
await writeJsonWithBackup(tasksFilePath, tasks);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function sortRunnableTasks(tasks) {
|
|
26
|
+
return [...tasks].sort((a, b) => {
|
|
27
|
+
const pa = priorityRank[a.priority] ?? 9;
|
|
28
|
+
const pb = priorityRank[b.priority] ?? 9;
|
|
29
|
+
if (pa !== pb) return pa - pb;
|
|
30
|
+
return String(a.createdAt || '').localeCompare(String(b.createdAt || ''));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getNextTask(tasks) {
|
|
35
|
+
return sortRunnableTasks(tasks.filter(task => task.status === 'pending'))[0] || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function archiveTimestamp() {
|
|
39
|
+
const now = new Date();
|
|
40
|
+
const pad = value => String(value).padStart(2, '0');
|
|
41
|
+
return String(now.getFullYear()) + pad(now.getMonth() + 1) + pad(now.getDate()) + '-' + pad(now.getHours()) + pad(now.getMinutes()) + pad(now.getSeconds());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function uniqueArchivePath(basePath) {
|
|
45
|
+
let candidate = basePath;
|
|
46
|
+
let suffix = 1;
|
|
47
|
+
while (true) {
|
|
48
|
+
try {
|
|
49
|
+
await fs.access(candidate);
|
|
50
|
+
candidate = basePath.replace(/\.json$/, '-' + suffix + '.json');
|
|
51
|
+
suffix += 1;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error.code === 'ENOENT') {
|
|
54
|
+
return candidate;
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function archiveTasks(predicate, reason) {
|
|
62
|
+
const tasks = await getTasks();
|
|
63
|
+
const archived = tasks.filter(predicate);
|
|
64
|
+
if (archived.length === 0) {
|
|
65
|
+
return { archived: [], archivePath: null, remaining: tasks };
|
|
66
|
+
}
|
|
67
|
+
const remaining = tasks.filter(task => !predicate(task));
|
|
68
|
+
const archiveDir = path.join(CONFIG.maestroPath, 'archive');
|
|
69
|
+
await fs.mkdir(archiveDir, { recursive: true });
|
|
70
|
+
const archivePath = await uniqueArchivePath(path.join(archiveDir, 'tasks-' + archiveTimestamp() + '.json'));
|
|
71
|
+
const archivedAt = new Date().toISOString();
|
|
72
|
+
await fs.writeFile(archivePath, JSON.stringify({ archivedAt, reason, tasks: archived.map(task => ({ ...task, status: 'archived', archivedAt, archiveReason: reason })) }, null, 2));
|
|
73
|
+
await saveTasks(remaining);
|
|
74
|
+
return { archived, archivePath, remaining };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function archiveActiveTasksBeforePlan() {
|
|
78
|
+
const tasks = await getTasks();
|
|
79
|
+
const archived = tasks.filter(task => activeStatuses.has(task.status));
|
|
80
|
+
if (archived.length === 0) {
|
|
81
|
+
return { archived: [], archivePath: null, remaining: tasks };
|
|
82
|
+
}
|
|
83
|
+
const remaining = tasks.filter(task => !activeStatuses.has(task.status));
|
|
84
|
+
const archiveDir = path.join(CONFIG.maestroPath, 'archive');
|
|
85
|
+
await fs.mkdir(archiveDir, { recursive: true });
|
|
86
|
+
const archivePath = await uniqueArchivePath(path.join(archiveDir, 'tasks-before-plan-' + archiveTimestamp() + '.json'));
|
|
87
|
+
const archivedAt = new Date().toISOString();
|
|
88
|
+
await fs.writeFile(archivePath, JSON.stringify({ archivedAt, reason: 'before-plan', tasks: archived.map(task => ({ ...task, status: 'archived', archivedAt, archiveReason: 'before-plan' })) }, null, 2));
|
|
89
|
+
await saveTasks(remaining);
|
|
90
|
+
return { archived, archivePath, remaining };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function addTask(description, fields = {}) {
|
|
94
|
+
const tasks = await getTasks();
|
|
95
|
+
const now = new Date().toISOString();
|
|
96
|
+
const newTask = stampSchemaVersion({
|
|
97
|
+
id: tasks.length > 0 ? Math.max(...tasks.map(t => t.id)) + 1 : 1,
|
|
98
|
+
title: fields.title || description,
|
|
99
|
+
description,
|
|
100
|
+
status: fields.status || 'pending',
|
|
101
|
+
priority: fields.priority || 'medium',
|
|
102
|
+
engine: fields.engine || 'codex9',
|
|
103
|
+
files: fields.files || [],
|
|
104
|
+
acceptance: fields.acceptance || [],
|
|
105
|
+
requestKind: fields.requestKind || null,
|
|
106
|
+
sourceRequest: fields.sourceRequest || null,
|
|
107
|
+
sourceSpec: fields.sourceSpec || null,
|
|
108
|
+
...(fields.parentTaskId !== undefined ? { parentTaskId: fields.parentTaskId } : {}),
|
|
109
|
+
...(fields.subtaskId !== undefined ? { subtaskId: fields.subtaskId } : {}),
|
|
110
|
+
...(fields.blockedBy !== undefined ? { blockedBy: fields.blockedBy } : {}),
|
|
111
|
+
createdAt: now,
|
|
112
|
+
updatedAt: now
|
|
113
|
+
});
|
|
114
|
+
tasks.push(newTask);
|
|
115
|
+
await saveTasks(tasks);
|
|
116
|
+
return newTask;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function updateTaskStatus(taskId, status) {
|
|
120
|
+
const tasks = await getTasks();
|
|
121
|
+
const taskIndex = tasks.findIndex(task => String(task.id) === String(taskId));
|
|
122
|
+
if (taskIndex !== -1) {
|
|
123
|
+
tasks[taskIndex].status = status;
|
|
124
|
+
tasks[taskIndex].updatedAt = new Date().toISOString();
|
|
125
|
+
await saveTasks(tasks);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function getTaskById(taskId) {
|
|
132
|
+
const tasks = await getTasks();
|
|
133
|
+
return tasks.find(task => String(task.id) === String(taskId)) || null;
|
|
134
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { CONFIG } from '../config.mjs';
|
|
5
|
+
import { summarizeText } from '../format.mjs';
|
|
6
|
+
|
|
7
|
+
export const UI_COMMANDS = {
|
|
8
|
+
doctor: { args: ['bin/maestro.mjs', 'doctor'] },
|
|
9
|
+
'codex-home': { args: ['bin/maestro.mjs', 'codex-home'] },
|
|
10
|
+
'codex-test': { args: ['bin/maestro.mjs', 'codex-test'] },
|
|
11
|
+
'task-list': { args: ['bin/maestro.mjs', 'task', 'list'] },
|
|
12
|
+
'task-next': { args: ['bin/maestro.mjs', 'task', 'next'] },
|
|
13
|
+
'run-one': { args: ['bin/maestro.mjs', 'run-one'] },
|
|
14
|
+
'run-all-limit-3': { args: ['bin/maestro.mjs', 'run-all', '--limit', '3'] },
|
|
15
|
+
'run-task': { args: taskId => ['bin/maestro.mjs', 'run-task', String(taskId)] },
|
|
16
|
+
'task-pending': { args: taskId => ['bin/maestro.mjs', 'task', 'pending', String(taskId)] },
|
|
17
|
+
checkpoint: { args: ['bin/maestro.mjs', 'checkpoint'] },
|
|
18
|
+
handoff: { args: ['bin/maestro.mjs', 'handoff'] },
|
|
19
|
+
stats: { args: ['bin/maestro.mjs', 'stats'] },
|
|
20
|
+
'release-check': { args: ['bin/maestro.mjs', 'release-check'] },
|
|
21
|
+
'schema-check': { args: ['bin/maestro.mjs', 'schema-check'] }
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function normalizePositiveInteger(value, name = 'value') {
|
|
25
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
|
|
26
|
+
throw new Error(name + ' must be a positive integer');
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getUiCommandDescriptor(commandId, payload = {}) {
|
|
32
|
+
const descriptor = UI_COMMANDS[commandId];
|
|
33
|
+
if (!descriptor) {
|
|
34
|
+
throw new Error('Unknown UI command: ' + commandId);
|
|
35
|
+
}
|
|
36
|
+
if (typeof descriptor.args === 'function') {
|
|
37
|
+
const taskId = normalizePositiveInteger(payload.taskId, 'taskId');
|
|
38
|
+
return { command: 'node', args: descriptor.args(taskId), taskId };
|
|
39
|
+
}
|
|
40
|
+
return { command: 'node', args: descriptor.args, taskId: null };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function appendUiRun(entry, projectRoot) {
|
|
44
|
+
await fs.mkdir(path.join(projectRoot, CONFIG.maestroPath), { recursive: true });
|
|
45
|
+
await fs.appendFile(path.join(projectRoot, CONFIG.maestroPath, 'ui-runs.jsonl'), JSON.stringify(entry) + '\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function runUiCommand(commandId, payload = {}, projectRoot = process.cwd()) {
|
|
49
|
+
const descriptor = getUiCommandDescriptor(commandId, payload);
|
|
50
|
+
const startedAt = new Date().toISOString();
|
|
51
|
+
const startedMs = Date.now();
|
|
52
|
+
const result = await new Promise(resolve => {
|
|
53
|
+
const child = spawn(descriptor.command, descriptor.args, { cwd: projectRoot, shell: false, windowsHide: true });
|
|
54
|
+
let stdout = '';
|
|
55
|
+
let stderr = '';
|
|
56
|
+
child.stdout.on('data', chunk => { stdout += chunk.toString(); });
|
|
57
|
+
child.stderr.on('data', chunk => { stderr += chunk.toString(); });
|
|
58
|
+
child.on('error', error => resolve({ exitCode: 1, stdout, stderr: stderr + error.message }));
|
|
59
|
+
child.on('close', code => resolve({ exitCode: code ?? 1, stdout, stderr }));
|
|
60
|
+
});
|
|
61
|
+
const status = result.exitCode === 0 ? 'completed' : 'failed';
|
|
62
|
+
const entry = {
|
|
63
|
+
timestamp: startedAt,
|
|
64
|
+
commandId,
|
|
65
|
+
taskId: descriptor.taskId,
|
|
66
|
+
args: descriptor.args,
|
|
67
|
+
exitCode: result.exitCode,
|
|
68
|
+
status,
|
|
69
|
+
summary: summarizeText(result.stdout || result.stderr || commandId, 260),
|
|
70
|
+
stdout: summarizeText(result.stdout, 1400),
|
|
71
|
+
stderr: summarizeText(result.stderr, 1400),
|
|
72
|
+
durationMs: Date.now() - startedMs
|
|
73
|
+
};
|
|
74
|
+
await appendUiRun(entry, projectRoot);
|
|
75
|
+
return entry;
|
|
76
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { getUiWatchFiles, readUiState } from './state.mjs';
|
|
3
|
+
|
|
4
|
+
async function fileSignature(files) {
|
|
5
|
+
const stats = await Promise.all(files.map(async file => {
|
|
6
|
+
try {
|
|
7
|
+
const stat = await fs.stat(file);
|
|
8
|
+
return file + ':' + stat.mtimeMs + ':' + stat.size;
|
|
9
|
+
} catch (error) {
|
|
10
|
+
if (error.code === 'ENOENT') return file + ':missing';
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
}));
|
|
14
|
+
return stats.join('|');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeSse(res, event, data) {
|
|
18
|
+
res.write('event: ' + event + '\n');
|
|
19
|
+
res.write('data: ' + JSON.stringify(data) + '\n\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function handleEvents(req, res, projectRoot = process.cwd()) {
|
|
23
|
+
res.writeHead(200, {
|
|
24
|
+
'Content-Type': 'text/event-stream',
|
|
25
|
+
'Cache-Control': 'no-cache',
|
|
26
|
+
Connection: 'keep-alive'
|
|
27
|
+
});
|
|
28
|
+
const files = getUiWatchFiles(projectRoot);
|
|
29
|
+
let signature = await fileSignature(files);
|
|
30
|
+
writeSse(res, 'state', await readUiState(projectRoot));
|
|
31
|
+
const interval = setInterval(async () => {
|
|
32
|
+
try {
|
|
33
|
+
const nextSignature = await fileSignature(files);
|
|
34
|
+
if (nextSignature !== signature) {
|
|
35
|
+
signature = nextSignature;
|
|
36
|
+
writeSse(res, 'state', await readUiState(projectRoot));
|
|
37
|
+
} else {
|
|
38
|
+
writeSse(res, 'heartbeat', { timestamp: new Date().toISOString() });
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
writeSse(res, 'error', { message: error.message });
|
|
42
|
+
}
|
|
43
|
+
}, 1500);
|
|
44
|
+
req.on('close', () => clearInterval(interval));
|
|
45
|
+
}
|