ai-maestro 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +15 -0
  3. package/README.md +727 -0
  4. package/bin/maestro.mjs +246 -0
  5. package/package.json +29 -0
  6. package/src/checkpoint.mjs +102 -0
  7. package/src/codex-diagnostics.mjs +682 -0
  8. package/src/codex-home-inspect.mjs +252 -0
  9. package/src/codex-home.mjs +258 -0
  10. package/src/commands.mjs +809 -0
  11. package/src/config.mjs +11 -0
  12. package/src/debug.mjs +164 -0
  13. package/src/encoding-guard.mjs +125 -0
  14. package/src/engines/ai-router-engine.mjs +37 -0
  15. package/src/engines/codex-engine.mjs +21 -0
  16. package/src/engines/engine.mjs +21 -0
  17. package/src/engines/registry.mjs +29 -0
  18. package/src/files.mjs +65 -0
  19. package/src/format.mjs +132 -0
  20. package/src/lock.mjs +439 -0
  21. package/src/memory-log.mjs +18 -0
  22. package/src/memory.mjs +1 -0
  23. package/src/orchestration/attempt-chain-runtime.mjs +627 -0
  24. package/src/orchestration/budget-manager.mjs +121 -0
  25. package/src/orchestration/capability-registry.mjs +108 -0
  26. package/src/orchestration/consolidator.mjs +772 -0
  27. package/src/orchestration/delegation-executor.mjs +262 -0
  28. package/src/orchestration/delegation-manager.mjs +116 -0
  29. package/src/orchestration/deployment-intent.mjs +36 -0
  30. package/src/orchestration/engine-history.mjs +110 -0
  31. package/src/orchestration/engine-policy.mjs +73 -0
  32. package/src/orchestration/engine-selector.mjs +187 -0
  33. package/src/orchestration/execution-context.mjs +45 -0
  34. package/src/orchestration/failure-classifier.mjs +136 -0
  35. package/src/orchestration/failure-evidence.mjs +237 -0
  36. package/src/orchestration/fallback-chain-lock.mjs +217 -0
  37. package/src/orchestration/fallback-chain-trail.mjs +218 -0
  38. package/src/orchestration/fallback-executor.mjs +146 -0
  39. package/src/orchestration/fallback-graph.mjs +106 -0
  40. package/src/orchestration/fallback-recommendation.mjs +73 -0
  41. package/src/orchestration/file-conflict-detector.mjs +126 -0
  42. package/src/orchestration/host-validation.mjs +241 -0
  43. package/src/orchestration/orchestration-loop.mjs +1971 -0
  44. package/src/orchestration/orchestration-runtime.mjs +1019 -0
  45. package/src/orchestration/orchestration-scheduler.mjs +135 -0
  46. package/src/orchestration/orchestration-trail.mjs +192 -0
  47. package/src/orchestration/preflight.mjs +212 -0
  48. package/src/orchestration/provider-health.mjs +566 -0
  49. package/src/orchestration/provider-router.mjs +352 -0
  50. package/src/orchestration/rc-check-adapters.mjs +817 -0
  51. package/src/orchestration/rc-check.mjs +544 -0
  52. package/src/orchestration/rc-policy.mjs +200 -0
  53. package/src/orchestration/runtime-gate.mjs +146 -0
  54. package/src/orchestration/sector-managers.mjs +215 -0
  55. package/src/orchestration/self-hosting-canary.mjs +231 -0
  56. package/src/orchestration/self-hosting-readiness.mjs +244 -0
  57. package/src/orchestration/spec-planner.mjs +877 -0
  58. package/src/orchestration/subtask-planner.mjs +176 -0
  59. package/src/orchestration/task-classifier.mjs +241 -0
  60. package/src/orchestration/verifier.mjs +1608 -0
  61. package/src/orchestration-commands.mjs +279 -0
  62. package/src/planner.mjs +38 -0
  63. package/src/project.mjs +1 -0
  64. package/src/run-diagnostics.mjs +641 -0
  65. package/src/runner.mjs +243 -0
  66. package/src/safety.mjs +116 -0
  67. package/src/schema.mjs +371 -0
  68. package/src/session-commands.mjs +521 -0
  69. package/src/shell.mjs +93 -0
  70. package/src/smart-planner.mjs +249 -0
  71. package/src/task-commands.mjs +1182 -0
  72. package/src/tasks.mjs +134 -0
  73. package/src/ui/commands.mjs +76 -0
  74. package/src/ui/events.mjs +45 -0
  75. package/src/ui/public/app.js +600 -0
  76. package/src/ui/public/index.html +88 -0
  77. package/src/ui/public/style.css +460 -0
  78. package/src/ui/server.mjs +112 -0
  79. package/src/ui/state.mjs +504 -0
  80. package/src/usage.mjs +178 -0
  81. package/src/workspace-diff.mjs +228 -0
@@ -0,0 +1,73 @@
1
+ import { planFallback } from './fallback-graph.mjs';
2
+ import { computeNextAttemptTime, getHealthState } from './provider-health.mjs';
3
+
4
+ const TAXONOMY_TO_GRAPH_CATEGORY = {
5
+ infrastructure: 'provider_unavailable',
6
+ authentication: 'authentication_error',
7
+ quota: 'quota_exceeded',
8
+ rate_limit: 'rate_limit',
9
+ timeout: 'network_error',
10
+ network: 'network_error',
11
+ stream_interruption: 'network_error',
12
+ provider_unavailable: 'provider_unavailable',
13
+ model_unavailable: 'provider_unavailable',
14
+ engine_resolution: 'unknown_failure',
15
+ worker_failure: 'unknown_failure',
16
+ deliverable_rejection: 'test_failure',
17
+ safety_block: 'policy_violation',
18
+ human_decision: 'unknown_failure',
19
+ unknown: 'unknown_failure'
20
+ };
21
+
22
+ function graphCategoryFor(failureClass) {
23
+ return TAXONOMY_TO_GRAPH_CATEGORY[failureClass] || 'unknown_failure';
24
+ }
25
+
26
+ function alternativeAllowed(rule) {
27
+ return ['try-equivalent-provider', 'retry-then-successor', 'switch-provider', 'escalate-capability', 'escalate-context', 'require-capability'].includes(rule && rule.action);
28
+ }
29
+
30
+ export async function recommendFallback({ healthTargetId, failureClass = 'unknown', taskClassification = null, timestamp = null } = {}) {
31
+ if (!healthTargetId || typeof healthTargetId !== 'string') {
32
+ throw new TypeError('healthTargetId is required');
33
+ }
34
+ const health = await getHealthState(healthTargetId, { timestamp });
35
+ const underlyingRule = await planFallback({ category: graphCategoryFor(failureClass), taskClassification });
36
+ const reason = [];
37
+ let recommendation = 'use_current';
38
+
39
+ if (health.state === 'OPEN') {
40
+ const nextAttempt = computeNextAttemptTime(health);
41
+ if (nextAttempt) {
42
+ recommendation = alternativeAllowed(underlyingRule) ? 'try_alternative' : 'wait_cooldown';
43
+ reason.push('current health target is OPEN until ' + nextAttempt);
44
+ } else {
45
+ recommendation = 'wait_cooldown';
46
+ reason.push('current health target is OPEN and waiting for cooldown evaluation');
47
+ }
48
+ } else if (health.state === 'HALF_OPEN') {
49
+ if (health.halfOpenProbeAvailable === false) {
50
+ recommendation = 'wait_cooldown';
51
+ reason.push('HALF_OPEN probe is already reserved in this process');
52
+ } else {
53
+ recommendation = 'use_current';
54
+ reason.push('HALF_OPEN allows at most one probe; no successor engine is selected in 03H1');
55
+ }
56
+ } else {
57
+ recommendation = 'use_current';
58
+ reason.push('current health target is CLOSED');
59
+ }
60
+
61
+ if (underlyingRule && underlyingRule.action) {
62
+ reason.push('fallback graph action would be ' + underlyingRule.action + ', but 03H1 only recommends');
63
+ }
64
+
65
+ return {
66
+ recommendation,
67
+ currentState: health.state,
68
+ cooldownUntil: health.cooldownUntil || null,
69
+ halfOpenProbeAvailable: health.state === 'HALF_OPEN' ? health.halfOpenProbeAvailable !== false : false,
70
+ underlyingRule,
71
+ reason
72
+ };
73
+ }
@@ -0,0 +1,126 @@
1
+ import fsSync from 'fs';
2
+ import path from 'path';
3
+
4
+ // Canonical detectFileConflicts implementation (spec section 4 of
5
+ // .maestro/v0.5.2/02_PART_B_DELEGATION_AND_SUBTASKS.md). Previously
6
+ // duplicated between here and delegation-executor.mjs; delegation-executor.mjs
7
+ // now imports from this module instead of keeping its own copy.
8
+
9
+ // "Does this subtask write/modify a file?" must use the same signal
10
+ // task-classifier.mjs already computes (requires.filesystemWrite), not an
11
+ // invented taskKind vocabulary like 'write'/'modify' — no taskKind in
12
+ // TASK_KINDS (task-classifier.mjs) is ever literally 'write' or 'modify'.
13
+ // A subtask missing `requires` entirely (e.g. a hand-built fixture) is
14
+ // treated as a write task, matching this function's original conservative
15
+ // behavior of flagging any subtask with no expectedFiles.
16
+ function isWriteSubtask(subtask) {
17
+ return subtask.requires ? subtask.requires.filesystemWrite !== false : true;
18
+ }
19
+
20
+ function normalizeForPlatform(filePath, platform) {
21
+ const normalized = path.normalize(filePath);
22
+ return platform === 'win32' ? normalized.toLowerCase() : normalized;
23
+ }
24
+
25
+ function isOutsideRoot(rootPath, candidatePath) {
26
+ const relative = path.relative(rootPath, candidatePath);
27
+ return relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative);
28
+ }
29
+
30
+ function existingPathForRealpath(candidatePath, rootPath) {
31
+ let current = candidatePath;
32
+ while (current && current !== path.dirname(current)) {
33
+ if (fsSync.existsSync(current)) return current;
34
+ if (path.resolve(current) === path.resolve(rootPath)) return rootPath;
35
+ current = path.dirname(current);
36
+ }
37
+ return rootPath;
38
+ }
39
+
40
+ function containmentConflict(file, projectRoot) {
41
+ const root = path.resolve(projectRoot || process.cwd());
42
+ const candidate = path.resolve(root, String(file));
43
+ if (isOutsideRoot(root, candidate)) {
44
+ return {
45
+ type: 'sensitive-path',
46
+ severity: 'high',
47
+ path: String(file),
48
+ reason: 'expectedFiles entry resolves outside the project root'
49
+ };
50
+ }
51
+
52
+ try {
53
+ const rootReal = fsSync.realpathSync.native(root);
54
+ const existing = existingPathForRealpath(candidate, root);
55
+ const existingReal = fsSync.realpathSync.native(existing);
56
+ if (isOutsideRoot(rootReal, existingReal)) {
57
+ return {
58
+ type: 'sensitive-path',
59
+ severity: 'high',
60
+ path: String(file),
61
+ reason: 'expectedFiles entry traverses a symlink outside the project root'
62
+ };
63
+ }
64
+ } catch (error) {
65
+ return null;
66
+ }
67
+
68
+ return null;
69
+ }
70
+
71
+ export function detectFileConflicts(subtasks, options = {}) {
72
+ if (!Array.isArray(subtasks) || subtasks.length === 0) {
73
+ return [];
74
+ }
75
+
76
+ const conflicts = [];
77
+ const fileMap = new Map(); // arquivo => array de índices de subtarefas
78
+ const fileLabels = new Map();
79
+ const projectRoot = options.projectRoot || process.cwd();
80
+ const platform = options.platform || process.platform;
81
+
82
+ subtasks.forEach((subtask, idx) => {
83
+ const files = subtask.expectedFiles || [];
84
+ if (files.length === 0 && isWriteSubtask(subtask)) {
85
+ conflicts.push({
86
+ subtaskId: subtask.id,
87
+ type: 'missing-expected-files',
88
+ severity: 'medium',
89
+ reason: 'Subtarefa não declara expectedFiles, pode causar confusão sobre qual arquivo altera.'
90
+ });
91
+ }
92
+ files.forEach(file => {
93
+ const containment = containmentConflict(file, projectRoot);
94
+ if (containment) {
95
+ conflicts.push({
96
+ ...containment,
97
+ subtaskId: subtask.id
98
+ });
99
+ }
100
+ const resolved = path.resolve(projectRoot, String(file));
101
+ const key = normalizeForPlatform(resolved, platform);
102
+ if (!fileMap.has(key)) {
103
+ fileMap.set(key, []);
104
+ fileLabels.set(key, file);
105
+ }
106
+ fileMap.get(key).push(idx);
107
+ });
108
+ });
109
+
110
+ // Detecta overlaps de arquivo: duas ou mais subtarefas alterando o mesmo
111
+ // arquivo, o que também representa risco de sobrescrita e conflito para
112
+ // execução paralela.
113
+ fileMap.forEach((indices, key) => {
114
+ if (indices.length > 1) {
115
+ conflicts.push({
116
+ file: fileLabels.get(key),
117
+ subtaskIds: indices.map(i => subtasks[i].id),
118
+ type: 'file-write-conflict',
119
+ severity: 'high',
120
+ reason: `Múltiplas subtarefas alteram o mesmo arquivo: ${indices.map(i => subtasks[i].title).join(', ')}`
121
+ });
122
+ }
123
+ });
124
+
125
+ return conflicts;
126
+ }
@@ -0,0 +1,241 @@
1
+ import fs from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import path from 'path';
4
+ import { spawn } from 'child_process';
5
+
6
+ export const DEFAULT_HOST_VALIDATION_POLICY = {
7
+ allowHostValidationFallback: true,
8
+ hostValidationCommands: ['npm-test', 'npm-run-check', 'node-check']
9
+ };
10
+
11
+ const COMMANDS = {
12
+ 'npm-test': { kind: 'npm', args: ['test'] },
13
+ 'npm-run-check': { kind: 'npm', args: ['run', 'check'] },
14
+ 'node-check': { executable: process.execPath, args: null }
15
+ };
16
+
17
+ export function resolveNpmInvocation(args = []) {
18
+ if (process.platform !== 'win32') {
19
+ return { executable: 'npm', args };
20
+ }
21
+ const candidates = [];
22
+ if (process.env.MAESTRO_NPX_PATH) {
23
+ candidates.push(path.join(path.dirname(process.env.MAESTRO_NPX_PATH), 'node_modules', 'npm', 'bin', 'npm-cli.js'));
24
+ }
25
+ if (process.env.APPDATA) {
26
+ candidates.push(path.join(process.env.APPDATA, 'npm', 'node_modules', 'npm', 'bin', 'npm-cli.js'));
27
+ }
28
+ if (process.env.ProgramFiles) {
29
+ candidates.push(path.join(process.env.ProgramFiles, 'nodejs', 'node_modules', 'npm', 'bin', 'npm-cli.js'));
30
+ }
31
+ candidates.push(path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'));
32
+ const cli = candidates.find(candidate => existsSync(candidate));
33
+ if (cli) {
34
+ return { executable: process.execPath, args: [cli, ...args] };
35
+ }
36
+ return { executable: 'npm', args };
37
+ }
38
+
39
+ const SECRET_PATTERNS = [
40
+ /sk-[A-Za-z0-9_-]{10,}/g,
41
+ /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
42
+ /(NINEROUTER_API_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY|TOKEN|SECRET|PASSWORD)=([^\s]+)/gi
43
+ ];
44
+
45
+ function redact(text) {
46
+ let value = String(text || '');
47
+ for (const pattern of SECRET_PATTERNS) {
48
+ value = value.replace(pattern, (match, key) => key ? key + '=[REDACTED]' : '[REDACTED]');
49
+ }
50
+ return value;
51
+ }
52
+
53
+ function ensureProjectRoot(projectRoot) {
54
+ if (typeof projectRoot !== 'string' || projectRoot.trim() === '') {
55
+ throw new Error('projectRoot is required');
56
+ }
57
+ return path.resolve(projectRoot);
58
+ }
59
+
60
+ function isInside(root, candidate) {
61
+ const rel = path.relative(root, candidate);
62
+ return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
63
+ }
64
+
65
+ function assertNoFreeFormCommand(input) {
66
+ if (input.command || input.commandLine || input.cmd || input.args || input.argv || input.shell === true) {
67
+ throw new Error('free-form host commands are not allowed');
68
+ }
69
+ if (input.fromWorkerOutput === true || input.source === 'worker-output') {
70
+ throw new Error('commands copied from worker output are not allowed');
71
+ }
72
+ }
73
+
74
+ function validateNodeCheckFile(projectRoot, file) {
75
+ if (typeof file !== 'string' || file.trim() === '') {
76
+ throw new Error('node-check requires a file');
77
+ }
78
+ const normalizedInput = file.replace(/\\/g, path.sep);
79
+ const fullPath = path.resolve(projectRoot, normalizedInput);
80
+ if (!isInside(projectRoot, fullPath)) {
81
+ throw new Error('node-check file must stay inside projectRoot');
82
+ }
83
+ const relative = path.relative(projectRoot, fullPath);
84
+ const segments = relative.split(path.sep).map(segment => segment.toLowerCase());
85
+ if (segments.includes('.env') || path.basename(fullPath).toLowerCase() === '.env') {
86
+ throw new Error('node-check refuses .env');
87
+ }
88
+ const ext = path.extname(fullPath).toLowerCase();
89
+ if (ext !== '.js' && ext !== '.mjs') {
90
+ throw new Error('node-check only accepts .js or .mjs files');
91
+ }
92
+ return { fullPath, relative };
93
+ }
94
+
95
+ export function normalizeHostValidationPolicy(policy = {}) {
96
+ const allowHostValidationFallback = policy.allowHostValidationFallback !== false;
97
+ const configured = Array.isArray(policy.hostValidationCommands)
98
+ ? policy.hostValidationCommands.filter(commandId => Object.prototype.hasOwnProperty.call(COMMANDS, commandId))
99
+ : DEFAULT_HOST_VALIDATION_POLICY.hostValidationCommands;
100
+ return { allowHostValidationFallback, hostValidationCommands: configured };
101
+ }
102
+
103
+ export function validateHostCommand(input = {}) {
104
+ assertNoFreeFormCommand(input);
105
+ const commandId = input.commandId;
106
+ if (!Object.prototype.hasOwnProperty.call(COMMANDS, commandId)) {
107
+ throw new Error('unknown host validation command: ' + String(commandId || ''));
108
+ }
109
+ const projectRoot = ensureProjectRoot(input.projectRoot);
110
+ const policy = normalizeHostValidationPolicy(input.policy || DEFAULT_HOST_VALIDATION_POLICY);
111
+ if (!policy.allowHostValidationFallback) {
112
+ throw new Error('host validation fallback is disabled by policy');
113
+ }
114
+ if (!policy.hostValidationCommands.includes(commandId)) {
115
+ throw new Error('host validation command is not allowlisted: ' + commandId);
116
+ }
117
+ if (commandId === 'npm-install' || /install/i.test(commandId)) {
118
+ throw new Error('npm install is not allowed');
119
+ }
120
+ const command = COMMANDS[commandId];
121
+ const invocation = command.kind === 'npm'
122
+ ? resolveNpmInvocation(command.args || [])
123
+ : { executable: command.executable, args: [...(command.args || [])] };
124
+ let args = [...invocation.args];
125
+ let file = null;
126
+ if (commandId === 'node-check') {
127
+ file = validateNodeCheckFile(projectRoot, input.file);
128
+ args = ['--check', file.fullPath];
129
+ }
130
+ return {
131
+ commandId,
132
+ projectRoot,
133
+ executable: invocation.executable,
134
+ args,
135
+ shell: false,
136
+ timeoutMs: Number.isFinite(input.timeoutMs) ? input.timeoutMs : 120000,
137
+ file: file ? file.relative : null
138
+ };
139
+ }
140
+
141
+ function spawnCollect(command, args, options, spawnFn = spawn) {
142
+ return new Promise(resolve => {
143
+ const startedAt = Date.now();
144
+ const child = spawnFn(command, args, {
145
+ cwd: options.cwd,
146
+ shell: false,
147
+ stdio: ['ignore', 'pipe', 'pipe']
148
+ });
149
+ let stdout = '';
150
+ let stderr = '';
151
+ let settled = false;
152
+ let timer = null;
153
+ const finish = result => {
154
+ if (settled) return;
155
+ settled = true;
156
+ if (timer) clearTimeout(timer);
157
+ resolve({ ...result, durationMs: Date.now() - startedAt });
158
+ };
159
+ timer = setTimeout(() => {
160
+ if (child && typeof child.kill === 'function') child.kill('SIGTERM');
161
+ finish({ code: 124, stdout, stderr: stderr + '\nTimed out after ' + options.timeoutMs + 'ms.' });
162
+ }, options.timeoutMs);
163
+ if (child.stdout && typeof child.stdout.on === 'function') {
164
+ child.stdout.on('data', data => { stdout += data.toString(); });
165
+ }
166
+ if (child.stderr && typeof child.stderr.on === 'function') {
167
+ child.stderr.on('data', data => { stderr += data.toString(); });
168
+ }
169
+ child.on('error', error => finish({ code: error.code || 1, stdout, stderr: error.message }));
170
+ child.on('close', code => finish({ code, stdout, stderr }));
171
+ });
172
+ }
173
+
174
+ export async function runHostValidation(input = {}, options = {}) {
175
+ let validated;
176
+ try {
177
+ validated = validateHostCommand(input);
178
+ } catch (error) {
179
+ return {
180
+ ok: false,
181
+ result: 'rejected',
182
+ error: error.message,
183
+ commandId: input.commandId || null,
184
+ metadata: { rejected: true }
185
+ };
186
+ }
187
+ try {
188
+ await fs.access(validated.projectRoot);
189
+ } catch (error) {
190
+ return {
191
+ ok: false,
192
+ result: 'rejected',
193
+ error: 'projectRoot is not accessible: ' + error.message,
194
+ commandId: validated.commandId,
195
+ metadata: { rejected: true }
196
+ };
197
+ }
198
+ const raw = await spawnCollect(validated.executable, validated.args, {
199
+ cwd: validated.projectRoot,
200
+ timeoutMs: validated.timeoutMs
201
+ }, options.spawnFn);
202
+ const result = raw.code === 0 ? 'passed' : 'failed';
203
+ return {
204
+ ok: raw.code === 0,
205
+ result,
206
+ commandId: validated.commandId,
207
+ exitCode: raw.code,
208
+ stdout: redact(raw.stdout),
209
+ stderr: redact(raw.stderr),
210
+ metadata: {
211
+ commandId: validated.commandId,
212
+ executable: validated.executable,
213
+ args: validated.args,
214
+ cwd: validated.projectRoot,
215
+ shell: false,
216
+ timeoutMs: validated.timeoutMs,
217
+ durationMs: raw.durationMs,
218
+ file: validated.file
219
+ }
220
+ };
221
+ }
222
+
223
+ export function summarizeHostValidation(result) {
224
+ if (!result) {
225
+ return { used: false, commands: [], result: 'not_run' };
226
+ }
227
+ if (Array.isArray(result)) {
228
+ return {
229
+ used: result.length > 0,
230
+ commands: result.map(item => item.commandId).filter(Boolean),
231
+ result: result.length > 0 && result.every(item => item.ok) ? 'passed' : 'failed',
232
+ metadata: result.map(item => item.metadata).filter(Boolean)
233
+ };
234
+ }
235
+ return {
236
+ used: true,
237
+ commands: [result.commandId].filter(Boolean),
238
+ result: result.result || (result.ok ? 'passed' : 'failed'),
239
+ metadata: result.metadata ? [result.metadata] : []
240
+ };
241
+ }