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/config.mjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const CONFIG = {
|
|
2
|
+
memoryPath: '.memory',
|
|
3
|
+
maestroPath: '.maestro',
|
|
4
|
+
codexPath: process.env.MAESTRO_CODEX_PATH || 'codex',
|
|
5
|
+
npxPath: process.env.MAESTRO_NPX_PATH || 'npx',
|
|
6
|
+
normalCodexHome: process.env.MAESTRO_NORMAL_CODEX_HOME || 'C:\\Users\\PC\\.codex',
|
|
7
|
+
codexHome: process.env.MAESTRO_CODEX_HOME || 'C:\\Users\\PC\\.codex-maestro',
|
|
8
|
+
codexProfile: process.env.MAESTRO_CODEX_PROFILE || '9router',
|
|
9
|
+
codexStrategy: process.env.MAESTRO_CODEX_STRATEGY || 'auto',
|
|
10
|
+
codexInvocation: process.env.MAESTRO_CODEX_INVOCATION || 'auto'
|
|
11
|
+
};
|
package/src/debug.mjs
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { CONFIG } from './config.mjs';
|
|
4
|
+
import { getCodexEnv, getCodexHome, ensureCodexHome, analyzeIsolatedCodexConfig, getLegacyWorkspacePermissionsConfigPath } from './codex-home.mjs';
|
|
5
|
+
import {
|
|
6
|
+
resolveCodexInvocation, getCodexHelpMatrix, readValidatedCodexStrategy, resolveRunCodexStrategy,
|
|
7
|
+
readCodexCompare, resolveCodexInvocationMode, resolveCodexInvocationForMode,
|
|
8
|
+
buildCodexEnvForInvocation, applyCodexStrategy
|
|
9
|
+
} from './commands.mjs';
|
|
10
|
+
import { pathExists } from './files.mjs';
|
|
11
|
+
|
|
12
|
+
// Env vars worth showing in doctor/debug output. Anything whose NAME looks
|
|
13
|
+
// like a secret is replaced by '<redacted>' — the VALUE of a key must never
|
|
14
|
+
// reach stdout, logs, or .maestro reports.
|
|
15
|
+
const REPORTED_ENV_KEYS = [
|
|
16
|
+
'CODEX_HOME', 'NINEROUTER_API_KEY', 'MAESTRO_CODEX_HOME', 'MAESTRO_CODEX_STRATEGY',
|
|
17
|
+
'MAESTRO_CODEX_INVOCATION', 'MAESTRO_ALLOW_LOCAL_TEST_KEY'
|
|
18
|
+
];
|
|
19
|
+
const SECRET_NAME_PATTERN = /(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH)/i;
|
|
20
|
+
|
|
21
|
+
export function redactSecretsFromEnv(env = {}) {
|
|
22
|
+
const report = {};
|
|
23
|
+
for (const key of REPORTED_ENV_KEYS) {
|
|
24
|
+
if (env[key] === undefined) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
report[key] = SECRET_NAME_PATTERN.test(key) ? '<redacted>' : String(env[key]);
|
|
28
|
+
}
|
|
29
|
+
return report;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function matchToml(content, key) {
|
|
33
|
+
const match = String(content || '').match(new RegExp('^\\s*' + key + '\\s*=\\s*"([^"]+)"', 'm'));
|
|
34
|
+
return match ? match[1] : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function readFileSafe(filePath) {
|
|
38
|
+
try {
|
|
39
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Resolves what a real worker run would ACTUALLY use right now: which
|
|
46
|
+
// config files, which CODEX_HOME, which strategy/sandbox/provider/model,
|
|
47
|
+
// whether any Desktop plugin/MCP or normal-home reference leaks in, and
|
|
48
|
+
// the final argv (with a placeholder prompt and secrets redacted).
|
|
49
|
+
export async function getEffectiveCodexRuntime() {
|
|
50
|
+
await ensureCodexHome();
|
|
51
|
+
const invocationMode = await resolveCodexInvocationMode();
|
|
52
|
+
const invocation = await resolveCodexInvocationForMode(invocationMode);
|
|
53
|
+
const env = buildCodexEnvForInvocation(invocation);
|
|
54
|
+
const strategy = await resolveRunCodexStrategy();
|
|
55
|
+
const effectiveCodexHome = env.CODEX_HOME || CONFIG.normalCodexHome;
|
|
56
|
+
const isolatedHome = effectiveCodexHome === CONFIG.codexHome;
|
|
57
|
+
const configFile = path.join(effectiveCodexHome, 'config.toml');
|
|
58
|
+
const profileFile = path.join(effectiveCodexHome, CONFIG.codexProfile + '.config.toml');
|
|
59
|
+
const configContent = await readFileSafe(configFile);
|
|
60
|
+
const profileContent = await readFileSafe(profileFile);
|
|
61
|
+
const analysis = analyzeIsolatedCodexConfig(configContent + '\n' + profileContent);
|
|
62
|
+
|
|
63
|
+
const templateArgs = ['exec', '--profile', CONFIG.codexProfile, '--sandbox', 'workspace-write', '--skip-git-repo-check', '<prompt>'];
|
|
64
|
+
const finalArgs = await applyCodexStrategy(templateArgs, strategy);
|
|
65
|
+
const fullArgs = [...invocation.argsPrefix, ...finalArgs];
|
|
66
|
+
const sandboxIndex = finalArgs.indexOf('--sandbox');
|
|
67
|
+
const sandbox = finalArgs.includes('--dangerously-bypass-approvals-and-sandbox')
|
|
68
|
+
? 'bypassed (trusted-workspace, explicit opt-in)'
|
|
69
|
+
: (sandboxIndex >= 0 ? finalArgs[sandboxIndex + 1] : (matchToml(profileContent, 'sandbox_mode') || matchToml(configContent, 'sandbox_mode') || 'unknown'));
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
invocationMode,
|
|
73
|
+
command: invocation.command,
|
|
74
|
+
finalArgs: fullArgs,
|
|
75
|
+
effectiveCodexHome,
|
|
76
|
+
isolatedHome,
|
|
77
|
+
configFile,
|
|
78
|
+
profileFile,
|
|
79
|
+
profile: CONFIG.codexProfile,
|
|
80
|
+
strategy,
|
|
81
|
+
sandbox,
|
|
82
|
+
// The --profile layer wins over the base config, so read it first.
|
|
83
|
+
provider: matchToml(profileContent, 'model_provider') || matchToml(configContent, 'model_provider') || 'unknown',
|
|
84
|
+
model: matchToml(profileContent, 'model') || matchToml(configContent, 'model') || 'unknown',
|
|
85
|
+
desktopPluginsEnabled: analysis.desktopPluginsEnabled || analysis.marketplacesConfigured,
|
|
86
|
+
mcpServersConfigured: analysis.mcpServersConfigured,
|
|
87
|
+
referencesNormalCodexHome: analysis.referencesNormalCodexHome,
|
|
88
|
+
windowsSandboxElevated: analysis.windowsSandboxElevated,
|
|
89
|
+
defaultPermissionsApplied: analysis.usesDefaultPermissions,
|
|
90
|
+
envReport: redactSecretsFromEnv(env)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function formatEffectiveRuntimeLines(runtime) {
|
|
95
|
+
return [
|
|
96
|
+
'- Effective config file: ' + runtime.configFile,
|
|
97
|
+
'- Effective profile layer: ' + runtime.profileFile + ' (via --profile ' + runtime.profile + ')',
|
|
98
|
+
'- Effective CODEX_HOME: ' + runtime.effectiveCodexHome + (runtime.isolatedHome ? ' (isolated)' : ' (NOT isolated - normal home)'),
|
|
99
|
+
'- Effective invocation mode: ' + runtime.invocationMode,
|
|
100
|
+
'- Effective strategy: ' + runtime.strategy,
|
|
101
|
+
'- Effective sandbox: ' + runtime.sandbox,
|
|
102
|
+
'- Effective provider: ' + runtime.provider,
|
|
103
|
+
'- Effective model: ' + runtime.model,
|
|
104
|
+
'- Desktop plugins/marketplaces enabled: ' + runtime.desktopPluginsEnabled,
|
|
105
|
+
'- Desktop MCP servers configured: ' + runtime.mcpServersConfigured,
|
|
106
|
+
'- References normal .codex home: ' + runtime.referencesNormalCodexHome,
|
|
107
|
+
'- Windows sandbox "elevated" present: ' + runtime.windowsSandboxElevated,
|
|
108
|
+
'- default_permissions applied: ' + runtime.defaultPermissionsApplied,
|
|
109
|
+
'- Final invocation (redacted): ' + runtime.command + ' ' + runtime.finalArgs.join(' '),
|
|
110
|
+
'- Env (redacted): ' + JSON.stringify(runtime.envReport)
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function getCodexDebugInfo() {
|
|
115
|
+
const ensureResult = await ensureCodexHome();
|
|
116
|
+
const codexHome = getCodexHome();
|
|
117
|
+
const codexConfigPath = path.join(codexHome, 'config.toml');
|
|
118
|
+
const codexProfileConfigPath = path.join(codexHome, '9router.config.toml');
|
|
119
|
+
const legacyWorkspacePermissionsConfigPath = getLegacyWorkspacePermissionsConfigPath();
|
|
120
|
+
const invocation = await resolveCodexInvocation();
|
|
121
|
+
const selectedInvocationMode = await resolveCodexInvocationMode();
|
|
122
|
+
const selectedInvocation = await resolveCodexInvocationForMode(selectedInvocationMode);
|
|
123
|
+
const validated = await readValidatedCodexStrategy();
|
|
124
|
+
const currentStrategy = await resolveRunCodexStrategy();
|
|
125
|
+
const compare = await readCodexCompare();
|
|
126
|
+
const effectiveRuntime = await getEffectiveCodexRuntime();
|
|
127
|
+
|
|
128
|
+
let helpMatrix;
|
|
129
|
+
try {
|
|
130
|
+
helpMatrix = await getCodexHelpMatrix();
|
|
131
|
+
} catch (error) {
|
|
132
|
+
helpMatrix = { error: error.message };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
command: invocation.command,
|
|
137
|
+
argsPrefix: invocation.argsPrefix,
|
|
138
|
+
source: invocation.source,
|
|
139
|
+
selectedInvocationMode,
|
|
140
|
+
selectedCommand: selectedInvocation.command,
|
|
141
|
+
selectedArgsPrefix: selectedInvocation.argsPrefix,
|
|
142
|
+
selectedSource: selectedInvocation.source,
|
|
143
|
+
selectedUsesIsolatedHome: selectedInvocation.usesIsolatedHome,
|
|
144
|
+
codexHome,
|
|
145
|
+
profile: CONFIG.codexProfile,
|
|
146
|
+
currentStrategy,
|
|
147
|
+
validatedStrategy: validated,
|
|
148
|
+
lastCompare: compare,
|
|
149
|
+
codexConfigPath,
|
|
150
|
+
codexProfileConfigPath,
|
|
151
|
+
legacyWorkspacePermissionsConfigPath,
|
|
152
|
+
legacyWorkspacePermissionsConfigExists: await pathExists(legacyWorkspacePermissionsConfigPath),
|
|
153
|
+
workdir: process.cwd(),
|
|
154
|
+
is9RouterActive: !!getCodexEnv().NINEROUTER_API_KEY,
|
|
155
|
+
isolatedConfigExists: await pathExists(codexConfigPath),
|
|
156
|
+
isolatedProfileConfigExists: await pathExists(codexProfileConfigPath),
|
|
157
|
+
checks: ensureResult.checks,
|
|
158
|
+
analysis: ensureResult.analysis,
|
|
159
|
+
effectiveRuntime,
|
|
160
|
+
helpMatrix,
|
|
161
|
+
codexConfigContent: ensureResult.content,
|
|
162
|
+
codexProfileConfigContent: ensureResult.profileContent
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
|
|
3
|
+
const BOM_BYTES = [0xef, 0xbb, 0xbf];
|
|
4
|
+
const REPLACEMENT_CHAR = '\uFFFD';
|
|
5
|
+
|
|
6
|
+
// Classic symptom of UTF-8 bytes misread as Latin-1/Windows-1252 and
|
|
7
|
+
// re-encoded: an accented character like "ã" (UTF-8: 0xC3 0xA3) gets
|
|
8
|
+
// interpreted byte-by-byte as two Latin-1 characters ("Ã" + "£"), which are
|
|
9
|
+
// then valid UTF-8 on their own. The result decodes fine as UTF-8 but reads
|
|
10
|
+
// as garbage. à (U+00C3) and  (U+00C2) are the two first-byte artifacts
|
|
11
|
+
// this project has actually seen (áéíóúçãõ... all start with one of these).
|
|
12
|
+
const MOJIBAKE_BYTE_PATTERN = /[\u00C3\u00C2][\u0080-\u00BF]/;
|
|
13
|
+
|
|
14
|
+
// Words this project's own docs/prompts use often enough that a corrupted
|
|
15
|
+
// occurrence (accents replaced with "?") is a strong, low-false-positive
|
|
16
|
+
// signal something went wrong. Not exhaustive — a targeted check, not a
|
|
17
|
+
// spellchecker.
|
|
18
|
+
const KNOWN_WORDS = [
|
|
19
|
+
'ação', 'não', 'execução', 'implementação', 'configuração',
|
|
20
|
+
'validação', 'documentação', 'está', 'até'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const HAS_ACCENT_CHAR = /[áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ]/;
|
|
24
|
+
const ACCENT_CHARS_GLOBAL = /[áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ]/g;
|
|
25
|
+
|
|
26
|
+
function escapeRegExp(text) {
|
|
27
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildCorruptedWordPattern(word) {
|
|
31
|
+
const corrupted = escapeRegExp(word).replace(ACCENT_CHARS_GLOBAL, '\\?');
|
|
32
|
+
return new RegExp('\\b' + corrupted + '\\b', 'i');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const KNOWN_WORD_PATTERNS = KNOWN_WORDS
|
|
36
|
+
.filter(word => HAS_ACCENT_CHAR.test(word))
|
|
37
|
+
.map(word => ({ word, pattern: buildCorruptedWordPattern(word) }));
|
|
38
|
+
|
|
39
|
+
export function hasBom(buffer) {
|
|
40
|
+
return buffer.length >= 3 &&
|
|
41
|
+
buffer[0] === BOM_BYTES[0] &&
|
|
42
|
+
buffer[1] === BOM_BYTES[1] &&
|
|
43
|
+
buffer[2] === BOM_BYTES[2];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isValidUtf8Buffer(buffer) {
|
|
47
|
+
try {
|
|
48
|
+
new TextDecoder('utf-8', { fatal: true }).decode(buffer);
|
|
49
|
+
return true;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function findMojibakeWords(text) {
|
|
56
|
+
const found = [];
|
|
57
|
+
for (const { word, pattern } of KNOWN_WORD_PATTERNS) {
|
|
58
|
+
if (pattern.test(text)) {
|
|
59
|
+
found.push(word);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function hasMojibakeBytePattern(text) {
|
|
66
|
+
return MOJIBAKE_BYTE_PATTERN.test(text);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function hasReplacementChar(text) {
|
|
70
|
+
return text.includes(REPLACEMENT_CHAR);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Pure text-level check — used when the content is already in memory
|
|
74
|
+
// (e.g. right after writing it) and there's no buffer to re-read.
|
|
75
|
+
export function validateUtf8Text(text, options = {}) {
|
|
76
|
+
const { allowBom = false } = options;
|
|
77
|
+
const issues = [];
|
|
78
|
+
|
|
79
|
+
if (!allowBom && text.startsWith('')) {
|
|
80
|
+
issues.push('unexpected-bom');
|
|
81
|
+
}
|
|
82
|
+
if (hasReplacementChar(text)) {
|
|
83
|
+
issues.push('replacement-character');
|
|
84
|
+
}
|
|
85
|
+
if (hasMojibakeBytePattern(text)) {
|
|
86
|
+
issues.push('mojibake-byte-pattern');
|
|
87
|
+
}
|
|
88
|
+
const corruptedWords = findMojibakeWords(text);
|
|
89
|
+
if (corruptedWords.length > 0) {
|
|
90
|
+
issues.push('mojibake-known-words:' + corruptedWords.join(','));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { ok: issues.length === 0, issues };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// File-level check — reads raw bytes so BOM and invalid UTF-8 sequences
|
|
97
|
+
// (which don't survive a lossy 'utf8' decode) can actually be detected.
|
|
98
|
+
export async function validateUtf8File(filePath, options = {}) {
|
|
99
|
+
const { allowBom = false } = options;
|
|
100
|
+
const buffer = await fs.readFile(filePath);
|
|
101
|
+
const issues = [];
|
|
102
|
+
|
|
103
|
+
if (!isValidUtf8Buffer(buffer)) {
|
|
104
|
+
issues.push('invalid-utf8-bytes');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const bom = hasBom(buffer);
|
|
108
|
+
if (bom && !allowBom) {
|
|
109
|
+
issues.push('unexpected-bom');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const text = buffer.toString('utf8');
|
|
113
|
+
if (hasReplacementChar(text)) {
|
|
114
|
+
issues.push('replacement-character');
|
|
115
|
+
}
|
|
116
|
+
if (hasMojibakeBytePattern(text)) {
|
|
117
|
+
issues.push('mojibake-byte-pattern');
|
|
118
|
+
}
|
|
119
|
+
const corruptedWords = findMojibakeWords(text);
|
|
120
|
+
if (corruptedWords.length > 0) {
|
|
121
|
+
issues.push('mojibake-known-words:' + corruptedWords.join(','));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { ok: issues.length === 0, issues, hasBom: bom };
|
|
125
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { execFileRaw } from '../shell.mjs';
|
|
2
|
+
|
|
3
|
+
export const name = 'ai-router';
|
|
4
|
+
|
|
5
|
+
function isEnabled() {
|
|
6
|
+
return process.env.MAESTRO_ENABLE_AI_ROUTER === '1';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Unvalidated in AI-MAESTRO: reference-only integration, opt-in, never touches C:\AI-Router.
|
|
10
|
+
// See .maestro/TECHNICAL_DEBT.md "AI-Router Future Dependency Limits".
|
|
11
|
+
export async function run(prompt, options = {}) {
|
|
12
|
+
if (!isEnabled()) {
|
|
13
|
+
throw new Error('AI-Router engine is opt-in only. Set MAESTRO_ENABLE_AI_ROUTER=1 to use it.');
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const result = await execFileRaw('ai', ['simples', prompt], { quiet: true, ...options });
|
|
17
|
+
return { code: result.code, stdout: result.stdout, stderr: result.stderr, strategy: 'ai-router-simples' };
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return { code: 1, stdout: '', stderr: error.message, strategy: 'error' };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function describe() {
|
|
24
|
+
try {
|
|
25
|
+
const result = await execFileRaw('where.exe', ['ai'], { quiet: true });
|
|
26
|
+
const available = result.code === 0;
|
|
27
|
+
return {
|
|
28
|
+
name,
|
|
29
|
+
ready: available && isEnabled(),
|
|
30
|
+
details: (available ? 'ai CLI found on PATH' : 'ai CLI not found on PATH') + (isEnabled() ? '' : '; MAESTRO_ENABLE_AI_ROUTER not set')
|
|
31
|
+
};
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return { name, ready: false, details: 'ai CLI not found: ' + error.message };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default { name, run, describe };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CONFIG } from '../config.mjs';
|
|
2
|
+
import { runCodex, resolveRunCodexStrategy } from '../commands.mjs';
|
|
3
|
+
|
|
4
|
+
export const name = 'codex9';
|
|
5
|
+
|
|
6
|
+
export async function run(prompt, options = {}) {
|
|
7
|
+
try {
|
|
8
|
+
return await runCodex(
|
|
9
|
+
['exec', '--profile', CONFIG.codexProfile, '--sandbox', 'workspace-write', '--skip-git-repo-check', prompt],
|
|
10
|
+
options
|
|
11
|
+
);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
return { code: 1, stdout: '', stderr: error.message, strategy: await resolveRunCodexStrategy() };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function describe() {
|
|
18
|
+
return { name, ready: true, details: 'Codex CLI via 9Router (profile ' + CONFIG.codexProfile + '), invocation resolved at run time.' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default { name, run, describe };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} EngineResult
|
|
3
|
+
* @property {number} code
|
|
4
|
+
* @property {string} stdout
|
|
5
|
+
* @property {string} stderr
|
|
6
|
+
* @property {string} strategy
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} EngineDescription
|
|
11
|
+
* @property {string} name
|
|
12
|
+
* @property {boolean} ready
|
|
13
|
+
* @property {string} details
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Engine contract every worker backend must satisfy:
|
|
18
|
+
* - name: string
|
|
19
|
+
* - run(prompt, options) => Promise<EngineResult>
|
|
20
|
+
* - describe() => Promise<EngineDescription> (non-mutating; used by `maestro doctor`)
|
|
21
|
+
*/
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import codexEngine from './codex-engine.mjs';
|
|
2
|
+
import aiRouterEngine from './ai-router-engine.mjs';
|
|
3
|
+
|
|
4
|
+
const engines = {
|
|
5
|
+
[codexEngine.name]: codexEngine,
|
|
6
|
+
[aiRouterEngine.name]: aiRouterEngine
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Planning-time labels from the smart planner (src/smart-planner.mjs's
|
|
10
|
+
// inferEngine) that describe what a task is about but are not distinct
|
|
11
|
+
// runnable backends. Route them to the real default worker instead of
|
|
12
|
+
// crashing at run-task time.
|
|
13
|
+
const ENGINE_ALIASES = {
|
|
14
|
+
memory: codexEngine.name,
|
|
15
|
+
'premium-review': codexEngine.name
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function getEngine(name = 'codex9') {
|
|
19
|
+
const key = name || 'codex9';
|
|
20
|
+
const engine = engines[key] || engines[ENGINE_ALIASES[key]];
|
|
21
|
+
if (!engine) {
|
|
22
|
+
throw new Error('Unknown engine "' + key + '". Available engines: ' + Object.keys(engines).join(', '));
|
|
23
|
+
}
|
|
24
|
+
return engine;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function listEngines() {
|
|
28
|
+
return Object.keys(engines);
|
|
29
|
+
}
|
package/src/files.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export async function pathExists(filePath) {
|
|
5
|
+
try {
|
|
6
|
+
await fs.access(filePath);
|
|
7
|
+
return true;
|
|
8
|
+
} catch (error) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Explicit `{ encoding: 'utf8' }` writes, used by every text file this
|
|
14
|
+
// project generates (worker-policy.md, CHECKPOINT.md, PLAN.md, HANDOFF.md,
|
|
15
|
+
// ...). Node's fs.writeFile already defaults a string write to utf8 with no
|
|
16
|
+
// BOM, but the corruption this guards against comes from PowerShell
|
|
17
|
+
// (`Set-Content -Encoding UTF8`, which does write a BOM) — being explicit
|
|
18
|
+
// here documents the intent and gives every caller one place to fix if that
|
|
19
|
+
// ever changes.
|
|
20
|
+
export async function writeUtf8TextFile(filePath, content) {
|
|
21
|
+
await fs.writeFile(filePath, content, { encoding: 'utf8' });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function appendUtf8TextFile(filePath, content) {
|
|
25
|
+
await fs.appendFile(filePath, content, { encoding: 'utf8' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function writeTextWithBackup(filePath, content) {
|
|
29
|
+
if (await pathExists(filePath)) {
|
|
30
|
+
const backupPath = filePath + '.bak';
|
|
31
|
+
await fs.copyFile(filePath, backupPath);
|
|
32
|
+
} else {
|
|
33
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
await writeUtf8TextFile(filePath, content);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function writeJsonWithBackup(filePath, value) {
|
|
39
|
+
await writeTextWithBackup(filePath, JSON.stringify(value, null, 2));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const BOM = String.fromCharCode(0xfeff);
|
|
43
|
+
|
|
44
|
+
// PowerShell's `Set-Content -Encoding UTF8` writes a BOM by default, and
|
|
45
|
+
// workers routinely use that to write files. Strip it so downstream
|
|
46
|
+
// JSON.parse calls don't choke on a byte that's invisible in most editors.
|
|
47
|
+
export function stripBom(text) {
|
|
48
|
+
return text.startsWith(BOM) ? text.slice(BOM.length) : text;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function readJsonSafe(filePath, fallback) {
|
|
52
|
+
try {
|
|
53
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
54
|
+
return JSON.parse(stripBom(content));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error.code === 'ENOENT') {
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
if (error instanceof SyntaxError) {
|
|
60
|
+
const diagnosticPath = filePath + '.diagnostic.txt';
|
|
61
|
+
await fs.writeFile(diagnosticPath, 'Invalid JSON in ' + filePath + ': ' + error.message + '\n');
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/format.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const BOM_CHAR = String.fromCharCode(0xfeff);
|
|
2
|
+
const NULL_CHAR = String.fromCharCode(0);
|
|
3
|
+
|
|
4
|
+
export function summarizeText(text, limit = 900) {
|
|
5
|
+
const value = (text || '').replace(/\s+/g, ' ').trim();
|
|
6
|
+
return value.length > limit ? value.slice(0, limit) + '...' : value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function escapeRegExp(value) {
|
|
10
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function sensitiveEnvValues(env = process.env) {
|
|
14
|
+
const sensitiveKey = /(NINEROUTER_API_KEY|API[_-]?KEY|TOKEN|PASSWORD|PASSWD|SECRET|AUTH|COOKIE|CREDENTIAL)/i;
|
|
15
|
+
return Object.entries(env)
|
|
16
|
+
.filter(([key, value]) => sensitiveKey.test(key) && typeof value === 'string' && value.length >= 8)
|
|
17
|
+
.map(([, value]) => value)
|
|
18
|
+
.filter(value => !/^(true|false|null|undefined)$/i.test(value));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function redactCredentials(input, options = {}) {
|
|
22
|
+
if (input === null || input === undefined) {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
let text = typeof input === 'string' ? input : JSON.stringify(input, null, 2);
|
|
26
|
+
const marker = '[REDACTED]';
|
|
27
|
+
|
|
28
|
+
for (const value of sensitiveEnvValues(options.env || process.env)) {
|
|
29
|
+
text = text.replace(new RegExp(escapeRegExp(value), 'g'), marker);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
text = text.replace(/(https?:\/\/)([^\/\s:@]+):([^@\s\/]+)@/gi, '$1' + marker + '@');
|
|
33
|
+
text = text.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, marker);
|
|
34
|
+
text = text.replace(/\bnr-[A-Za-z0-9_-]{8,}\b/g, marker);
|
|
35
|
+
text = text.replace(/\b(Authorization\s*:\s*(?:Bearer|Basic)\s+)[^\r\n,}]+/gi, '$1' + marker);
|
|
36
|
+
text = text.replace(/\b(Set-Cookie|Cookie)\s*:\s*[^\r\n]+/gi, '$1: ' + marker);
|
|
37
|
+
text = text.replace(/("(?:[^"]*(?:authorization|cookie|password|passwd|api[_-]?key|token|secret|credential|NINEROUTER_API_KEY)[^"]*)"\s*:\s*)"[^"]*"/gi, '$1"' + marker + '"');
|
|
38
|
+
text = text.replace(/('(?:[^']*(?:authorization|cookie|password|passwd|api[_-]?key|token|secret|credential|NINEROUTER_API_KEY)[^']*)'\s*:\s*)'[^']*'/gi, '$1\'' + marker + '\'');
|
|
39
|
+
text = text.replace(/\b(NINEROUTER_API_KEY|password|passwd|api[_-]?key|token|secret|credential)\s*=\s*("[^"]*"|'[^']*'|[^\s&;,]+)/gi, '$1=' + marker);
|
|
40
|
+
text = text.replace(/\b(NINEROUTER_API_KEY|password|passwd|api[_-]?key|token|secret|credential)\s*:\s*("[^"]*"|'[^']*'|[^\s,;}]+)/gi, '$1: ' + marker);
|
|
41
|
+
|
|
42
|
+
return text;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function redactObject(value, options = {}) {
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
return value.map(item => redactObject(item, options));
|
|
48
|
+
}
|
|
49
|
+
if (value && typeof value === 'object') {
|
|
50
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactObject(item, options)]));
|
|
51
|
+
}
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
return redactCredentials(value, options);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function truncatePreview(text, { maxLines = 100, maxBytes = 8192 } = {}) {
|
|
59
|
+
const value = String(text || '');
|
|
60
|
+
const lines = value.split(/\r?\n/);
|
|
61
|
+
let truncatedByLines = false;
|
|
62
|
+
let preview = value;
|
|
63
|
+
if (lines.length > maxLines) {
|
|
64
|
+
truncatedByLines = true;
|
|
65
|
+
preview = lines.slice(0, maxLines).join('\n');
|
|
66
|
+
}
|
|
67
|
+
let truncatedByBytes = false;
|
|
68
|
+
const bytes = Buffer.from(preview, 'utf8');
|
|
69
|
+
if (bytes.length > maxBytes) {
|
|
70
|
+
truncatedByBytes = true;
|
|
71
|
+
preview = bytes.subarray(0, maxBytes).toString('utf8');
|
|
72
|
+
}
|
|
73
|
+
const originalBytes = Buffer.byteLength(value, 'utf8');
|
|
74
|
+
const previewBytes = Buffer.byteLength(preview, 'utf8');
|
|
75
|
+
return {
|
|
76
|
+
text: preview,
|
|
77
|
+
truncated: truncatedByLines || truncatedByBytes || previewBytes < originalBytes,
|
|
78
|
+
originalLines: lines.length,
|
|
79
|
+
shownLines: preview ? preview.split(/\r?\n/).length : 0,
|
|
80
|
+
originalBytes,
|
|
81
|
+
shownBytes: previewBytes,
|
|
82
|
+
truncatedLines: Math.max(0, lines.length - (preview ? preview.split(/\r?\n/).length : 0)),
|
|
83
|
+
truncatedBytes: Math.max(0, originalBytes - previewBytes)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatPreview(text, {
|
|
88
|
+
maxLines = 100,
|
|
89
|
+
maxBytes = 8192,
|
|
90
|
+
runId,
|
|
91
|
+
kind = 'saida'
|
|
92
|
+
} = {}) {
|
|
93
|
+
const redacted = redactCredentials(text);
|
|
94
|
+
const preview = truncatePreview(redacted, { maxLines, maxBytes });
|
|
95
|
+
const lines = [preview.text];
|
|
96
|
+
if (preview.truncated) {
|
|
97
|
+
lines.push('[saída truncada — consulte maestro run-log ' + (runId || '<run-id>') + ']');
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
...preview,
|
|
101
|
+
text: lines.filter(Boolean).join('\n'),
|
|
102
|
+
kind
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function stripBom(text) {
|
|
107
|
+
return text.startsWith(BOM_CHAR) ? text.slice(BOM_CHAR.length) : text;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function normalizeReadContent(buffer) {
|
|
111
|
+
if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
|
|
112
|
+
return stripBom(buffer.toString('utf16le'));
|
|
113
|
+
}
|
|
114
|
+
if (buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) {
|
|
115
|
+
return stripBom(buffer.swap16().toString('utf16le'));
|
|
116
|
+
}
|
|
117
|
+
const text = buffer.toString('utf-8');
|
|
118
|
+
if (text.indexOf(NULL_CHAR) !== -1) {
|
|
119
|
+
return stripBom(buffer.toString('utf16le'));
|
|
120
|
+
}
|
|
121
|
+
return stripBom(text);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function detectField(output, name) {
|
|
125
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
126
|
+
const match = output.match(new RegExp(escaped + ':\\s*([^\\r\\n]+)', 'i'));
|
|
127
|
+
return match ? match[1].trim() : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function doctorLine(status, name, value, fix) {
|
|
131
|
+
console.log(redactCredentials(status + ': ' + name + ': ' + value + (fix ? ' (Fix: ' + fix + ')' : '')));
|
|
132
|
+
}
|