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
package/src/runner.mjs ADDED
@@ -0,0 +1,243 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { CONFIG } from './config.mjs';
4
+ import { pathExists } from './files.mjs';
5
+ import { stampSchemaVersion, validateRunEntry } from './schema.mjs';
6
+ import { redactCredentials, redactObject } from './format.mjs';
7
+
8
+ const runsFilePath = path.join(CONFIG.maestroPath, 'RUNS.jsonl');
9
+ const latestRunPath = path.join(CONFIG.maestroPath, 'latest-run.json');
10
+
11
+ function makeRunId() {
12
+ return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
13
+ }
14
+
15
+ function artifactPath(kind, runId) {
16
+ const dir = kind === 'report' ? 'reports' : 'logs';
17
+ const ext = kind === 'report' ? '.md' : '.log';
18
+ return path.join(CONFIG.maestroPath, dir, 'run-' + runId + ext);
19
+ }
20
+
21
+ function publicPath(filePath) {
22
+ return filePath.split(path.sep).join('/');
23
+ }
24
+
25
+ async function writeAtomicUtf8(filePath, content) {
26
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
27
+ const tempPath = filePath + '.tmp-' + process.pid + '-' + Date.now();
28
+ await fs.writeFile(tempPath, content, { encoding: 'utf8' });
29
+ await fs.rename(tempPath, filePath);
30
+ }
31
+
32
+ async function readLatestRunIndex() {
33
+ try {
34
+ return JSON.parse(await fs.readFile(latestRunPath, 'utf-8'));
35
+ } catch (error) {
36
+ if (error.code === 'ENOENT' || error instanceof SyntaxError) {
37
+ return { latest: null, byTask: {}, byRunId: {} };
38
+ }
39
+ throw error;
40
+ }
41
+ }
42
+
43
+ async function writeLatestRunIndex(entry) {
44
+ const index = await readLatestRunIndex();
45
+ const safeEntry = redactObject(entry);
46
+ const next = {
47
+ latest: safeEntry,
48
+ byTask: {
49
+ ...(index.byTask || {}),
50
+ [String(entry.taskId)]: safeEntry
51
+ },
52
+ byRunId: {
53
+ ...(index.byRunId || {}),
54
+ [entry.runId]: safeEntry
55
+ }
56
+ };
57
+ await writeAtomicUtf8(latestRunPath, JSON.stringify(next, null, 2) + '\n');
58
+ return next;
59
+ }
60
+
61
+ function buildRunLog({ run, fullStdout = '', fullStderr = '' }) {
62
+ return redactCredentials([
63
+ 'AI Maestro run ' + run.runId,
64
+ 'Task: #' + run.taskId,
65
+ 'Status: ' + run.status,
66
+ 'Strategy: ' + (run.strategy || 'none'),
67
+ 'Exit code: ' + (run.exitCode === undefined ? 'n/a' : run.exitCode),
68
+ 'Started: ' + (run.startTime || 'n/a'),
69
+ 'Ended: ' + (run.endTime || 'n/a'),
70
+ '',
71
+ '--- stdout ---',
72
+ fullStdout || '',
73
+ '',
74
+ '--- stderr ---',
75
+ fullStderr || ''
76
+ ].join('\n'));
77
+ }
78
+
79
+ function buildRunReport({ run, fullStdout = '', fullStderr = '', diffText = '' }) {
80
+ const changed = [
81
+ ...(run.changedFiles || []),
82
+ ...(run.createdFiles || []),
83
+ ...(run.deletedFiles || [])
84
+ ];
85
+ return redactCredentials([
86
+ '# AI Maestro Run Report',
87
+ '',
88
+ '- RunId: ' + run.runId,
89
+ '- Task: #' + run.taskId,
90
+ '- Status: ' + run.status,
91
+ '- Strategy: ' + (run.strategy || 'none'),
92
+ '- Exit code: ' + (run.exitCode === undefined ? 'n/a' : run.exitCode),
93
+ '- Started: ' + (run.startTime || 'n/a'),
94
+ '- Ended: ' + (run.endTime || 'n/a'),
95
+ '',
96
+ '## Warnings',
97
+ '',
98
+ (run.warnings && run.warnings.length ? run.warnings.map(item => '- ' + item).join('\n') : '- none'),
99
+ '',
100
+ '## Changed Files',
101
+ '',
102
+ (changed.length ? changed.map(item => '- ' + item).join('\n') : '- none'),
103
+ '',
104
+ '## Diff',
105
+ '',
106
+ diffText || '(no diff captured)',
107
+ '',
108
+ '## stdout',
109
+ '',
110
+ '```text',
111
+ fullStdout || '',
112
+ '```',
113
+ '',
114
+ '## stderr',
115
+ '',
116
+ '```text',
117
+ fullStderr || '',
118
+ '```',
119
+ ''
120
+ ].join('\n'));
121
+ }
122
+
123
+ export async function logRun(runDetails) {
124
+ const {
125
+ fullStdout = runDetails.stdout || '',
126
+ fullStderr = runDetails.stderr || '',
127
+ diffText = '',
128
+ ...publicRunDetails
129
+ } = runDetails;
130
+ const runId = publicRunDetails.runId || makeRunId();
131
+ const logPath = artifactPath('log', runId);
132
+ const reportPath = artifactPath('report', runId);
133
+ const stamped = redactObject(stampSchemaVersion({
134
+ ...publicRunDetails,
135
+ runId,
136
+ logPath: publicPath(logPath),
137
+ reportPath: publicPath(reportPath)
138
+ }));
139
+ const result = validateRunEntry(stamped);
140
+ if (!result.ok) {
141
+ console.warn('Run entry for task #' + stamped.taskId + ' failed schema validation: ' + result.errors.join(', '));
142
+ }
143
+ await writeAtomicUtf8(logPath, buildRunLog({ run: stamped, fullStdout, fullStderr }));
144
+ await writeAtomicUtf8(reportPath, buildRunReport({ run: stamped, fullStdout, fullStderr, diffText }));
145
+ await writeLatestRunIndex({
146
+ runId,
147
+ taskId: stamped.taskId,
148
+ logPath: publicPath(logPath),
149
+ reportPath: publicPath(reportPath),
150
+ status: stamped.status,
151
+ updatedAt: stamped.endTime || new Date().toISOString()
152
+ });
153
+ if (await pathExists(runsFilePath)) {
154
+ await fs.copyFile(runsFilePath, runsFilePath + '.bak');
155
+ }
156
+ await fs.appendFile(runsFilePath, JSON.stringify(stamped) + '\n', { encoding: 'utf8' });
157
+ return stamped;
158
+ }
159
+
160
+ export async function getRecentRuns(limit = 5) {
161
+ try {
162
+ const content = await fs.readFile(runsFilePath, 'utf-8');
163
+ return content
164
+ .split(/\r?\n/)
165
+ .filter(Boolean)
166
+ .slice(-limit)
167
+ .map(line => {
168
+ try {
169
+ return JSON.parse(line);
170
+ } catch (error) {
171
+ return { parseError: error.message, raw: line };
172
+ }
173
+ });
174
+ } catch (error) {
175
+ if (error.code === 'ENOENT') {
176
+ return [];
177
+ }
178
+ throw error;
179
+ }
180
+ }
181
+
182
+ export async function getLatestRunForTask(taskId) {
183
+ const runs = await getRecentRuns(Infinity);
184
+ for (let i = runs.length - 1; i >= 0; i--) {
185
+ if (String(runs[i].taskId) === String(taskId)) {
186
+ return runs[i];
187
+ }
188
+ }
189
+ return null;
190
+ }
191
+
192
+ export async function resolveRunArtifact({ kind, runId, taskId, latest = false }) {
193
+ const index = await readLatestRunIndex();
194
+ let entry = null;
195
+ if (latest && taskId !== undefined && taskId !== null) {
196
+ entry = index.byTask ? index.byTask[String(taskId)] : null;
197
+ if (!entry) {
198
+ throw new Error('No run found for task #' + taskId + '.');
199
+ }
200
+ } else if (runId) {
201
+ entry = index.byRunId ? index.byRunId[runId] : null;
202
+ if (!entry) {
203
+ const candidate = artifactPath(kind, runId);
204
+ if (await pathExists(candidate)) {
205
+ entry = { runId, taskId: null, logPath: publicPath(artifactPath('log', runId)), reportPath: publicPath(artifactPath('report', runId)) };
206
+ } else {
207
+ throw new Error('Run "' + runId + '" not found. Use "maestro task show <id>" or "maestro run-log --task <id> --latest".');
208
+ }
209
+ }
210
+ } else {
211
+ const command = kind === 'log' ? 'run-log' : 'report';
212
+ throw new Error('Missing run id. Usage: maestro ' + command + ' <run-id> or maestro ' + command + ' --task <id> --latest.');
213
+ }
214
+ const selectedPath = kind === 'report' ? entry.reportPath : entry.logPath;
215
+ if (!selectedPath) {
216
+ throw new Error('Run "' + entry.runId + '" has no ' + kind + ' path in .maestro/latest-run.json.');
217
+ }
218
+ if (!(await pathExists(selectedPath))) {
219
+ throw new Error('Run "' + entry.runId + '" ' + kind + ' file is missing: ' + selectedPath);
220
+ }
221
+ return { ...entry, path: selectedPath };
222
+ }
223
+
224
+ export function selectArtifactLines(content, { lines, tail = false } = {}) {
225
+ if (!lines) {
226
+ return content;
227
+ }
228
+ const allLines = String(content).split(/\r?\n/);
229
+ while (tail && allLines.length && allLines[allLines.length - 1] === '') {
230
+ allLines.pop();
231
+ }
232
+ const selected = tail ? allLines.slice(-lines) : allLines.slice(0, lines);
233
+ return selected.join('\n');
234
+ }
235
+
236
+ export async function readRunArtifact({ kind, runId, taskId, latest = false, lines, tail = false }) {
237
+ const resolved = await resolveRunArtifact({ kind, runId, taskId, latest });
238
+ const content = redactCredentials(await fs.readFile(resolved.path, 'utf-8'));
239
+ return {
240
+ ...resolved,
241
+ content: selectArtifactLines(content, { lines, tail })
242
+ };
243
+ }
package/src/safety.mjs ADDED
@@ -0,0 +1,116 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { CONFIG } from './config.mjs';
4
+ import { detectAffirmativeDeploymentIntent } from './orchestration/deployment-intent.mjs';
5
+
6
+ const STALE_DEPLOY_PATTERNS = new Set(['\\bdeploy\\b', '\\bpublish\\b.*\\bprod']);
7
+
8
+ const defaultSafety = {
9
+ protectedFiles: [
10
+ '\\.env\\b',
11
+ '\\.env$',
12
+ '\\.env\\.',
13
+ 'id_rsa$',
14
+ 'id_ed25519$',
15
+ 'known_hosts$',
16
+ '\\.pem$',
17
+ '\\.ssh\\\\',
18
+ 'sshd_config$'
19
+ ],
20
+ dangerousPatterns: [
21
+ 'rm\\s+-rf',
22
+ 'Remove-Item.*-Recurse.*-Force',
23
+ 'rd\\s+/s\\s*/q',
24
+ 'del\\s+/s\\s*/q',
25
+ 'DROP\\s+TABLE',
26
+ 'DROP\\s+DATABASE',
27
+ 'DELETE\\s+FROM',
28
+ 'TRUNCATE\\s+TABLE',
29
+ 'git\\s+push.*--force',
30
+ 'git\\s+reset\\s+--hard',
31
+ 'git\\s+clean\\s+-f',
32
+ 'terraform\\s+destroy',
33
+ 'kubectl\\s+delete',
34
+ 'docker\\s+system\\s+prune',
35
+ 'docker\\s+volume\\s+rm',
36
+ 'format\\s+[a-zA-Z]:',
37
+ '\\.ssh\\\\config',
38
+ 'sshd_config',
39
+ 'shutdown\\s+/',
40
+ 'Stop-Computer',
41
+ 'Restart-Computer'
42
+ ]
43
+ };
44
+
45
+ export function checkDeploymentSafety(text) {
46
+ const normalizedText = String(text || '');
47
+ const hasProductionTarget = /\bprod/i.test(normalizedText);
48
+ if (hasProductionTarget && detectAffirmativeDeploymentIntent(normalizedText)) {
49
+ return { blocked: true, reason: 'affirmative deploy intent targeting production' };
50
+ }
51
+ return { blocked: false, reason: null };
52
+ }
53
+
54
+ export function migrateSafetyConfig(safetyConfig) {
55
+ const source = safetyConfig && typeof safetyConfig === 'object' ? safetyConfig : {};
56
+ const dangerousPatterns = Array.isArray(source.dangerousPatterns) ? source.dangerousPatterns : [];
57
+ const migratedPatterns = dangerousPatterns.filter(pattern => !STALE_DEPLOY_PATTERNS.has(pattern));
58
+ const changed = migratedPatterns.length !== dangerousPatterns.length;
59
+ return {
60
+ changed,
61
+ config: {
62
+ ...source,
63
+ protectedFiles: Array.isArray(source.protectedFiles) ? source.protectedFiles : defaultSafety.protectedFiles,
64
+ dangerousPatterns: migratedPatterns
65
+ }
66
+ };
67
+ }
68
+
69
+ export async function ensureSafetyConfig() {
70
+ await fs.mkdir(CONFIG.maestroPath, { recursive: true });
71
+ const safetyPath = path.join(CONFIG.maestroPath, 'safety.json');
72
+ try {
73
+ const existing = JSON.parse(await fs.readFile(safetyPath, 'utf-8'));
74
+ const migration = migrateSafetyConfig(existing);
75
+ if (migration.changed) {
76
+ await fs.writeFile(safetyPath, JSON.stringify(migration.config, null, 2));
77
+ }
78
+ } catch (error) {
79
+ if (error.code !== 'ENOENT') {
80
+ throw error;
81
+ }
82
+ await fs.writeFile(safetyPath, JSON.stringify(defaultSafety, null, 2));
83
+ }
84
+ return safetyPath;
85
+ }
86
+
87
+ export async function checkTaskSafety(text) {
88
+ const safetyPath = await ensureSafetyConfig();
89
+ const safety = JSON.parse(await fs.readFile(safetyPath, 'utf-8'));
90
+ const findings = [];
91
+ for (const pattern of safety.dangerousPatterns || []) {
92
+ if (new RegExp(pattern, 'i').test(text)) {
93
+ findings.push('dangerous pattern: ' + pattern);
94
+ }
95
+ }
96
+ const deployment = checkDeploymentSafety(text);
97
+ if (deployment.blocked) {
98
+ findings.push('dangerous pattern: affirmative deploy intent: ' + deployment.reason);
99
+ }
100
+ for (const pattern of safety.protectedFiles || []) {
101
+ if (new RegExp(pattern, 'i').test(text)) {
102
+ findings.push('protected file: ' + pattern);
103
+ }
104
+ }
105
+ return { ok: findings.length === 0, findings, safetyPath };
106
+ }
107
+
108
+ export async function executeTaskWithSafety(task, workerFn, onBlocked = async () => {}) {
109
+ const text = [task.title || '', task.description || ''].join('\n');
110
+ const safety = await checkTaskSafety(text);
111
+ if (!safety.ok) {
112
+ await onBlocked({ task, safety });
113
+ return { blocked: true, safety };
114
+ }
115
+ return { blocked: false, result: await workerFn() };
116
+ }