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,809 @@
1
+ import fs from 'fs/promises';
2
+ import fsSync from 'fs';
3
+ import path from 'path';
4
+ import { CONFIG } from './config.mjs';
5
+ import { execCmdFile, execFileRaw } from './shell.mjs';
6
+ import { getCodexEnv } from './codex-home.mjs';
7
+ import { stampSchemaVersion } from './schema.mjs';
8
+
9
+ function isWindows() {
10
+ return process.platform === 'win32';
11
+ }
12
+
13
+ function isWindowsPlatform(platform = process.platform) {
14
+ return platform === 'win32';
15
+ }
16
+
17
+ function pathApiForPlatform(platform = process.platform) {
18
+ return isWindowsPlatform(platform) ? path.win32 : path.posix;
19
+ }
20
+
21
+ function commandKind(commandPath) {
22
+ const ext = path.extname(commandPath).toLowerCase();
23
+ if (ext === '.cmd' || ext === '.bat') {
24
+ return 'cmd';
25
+ }
26
+ return 'direct';
27
+ }
28
+
29
+ function isJsPath(filePath) {
30
+ return path.extname(filePath).toLowerCase() === '.js';
31
+ }
32
+
33
+ async function pathExists(filePath) {
34
+ try {
35
+ await fs.access(filePath);
36
+ return true;
37
+ } catch (error) {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ async function pathExistsWith(filePath, existsSync = null) {
43
+ if (existsSync) {
44
+ return existsSync(filePath);
45
+ }
46
+ return pathExists(filePath);
47
+ }
48
+
49
+ export function absoluteWhereExe() {
50
+ return 'C:\\Windows\\System32\\where.exe';
51
+ }
52
+
53
+ function normalizeDirForCompare(dir, platform, cwd) {
54
+ const pathApi = pathApiForPlatform(platform);
55
+ const resolved = pathApi.resolve(cwd, dir);
56
+ return isWindowsPlatform(platform) ? resolved.toLowerCase() : resolved;
57
+ }
58
+
59
+ function stripTrailingPathSeparators(value, pathApi) {
60
+ const root = pathApi.parse(value).root;
61
+ let next = value;
62
+ while (next.length > root.length && /[\\/]/.test(next[next.length - 1])) {
63
+ next = next.slice(0, -1);
64
+ }
65
+ return next;
66
+ }
67
+
68
+ function normalizePathForContainment(value, platform, cwd) {
69
+ const pathApi = pathApiForPlatform(platform);
70
+ const resolved = stripTrailingPathSeparators(pathApi.normalize(pathApi.resolve(cwd, value)), pathApi);
71
+ return isWindowsPlatform(platform) ? resolved.toLowerCase() : resolved;
72
+ }
73
+
74
+ function isSameOrDescendantPath(candidate, boundary, platform) {
75
+ const pathApi = pathApiForPlatform(platform);
76
+ if (candidate === boundary) {
77
+ return true;
78
+ }
79
+ const boundaryWithSep = boundary.endsWith(pathApi.sep) ? boundary : boundary + pathApi.sep;
80
+ return candidate.startsWith(boundaryWithSep);
81
+ }
82
+
83
+ function isSafeWhereResult(candidate, {
84
+ platform = process.platform,
85
+ cwd = process.cwd(),
86
+ projectRoot = cwd
87
+ } = {}) {
88
+ const pathApi = pathApiForPlatform(platform);
89
+ if (!pathApi.isAbsolute(candidate)) {
90
+ return false;
91
+ }
92
+
93
+ const candidateKey = normalizePathForContainment(candidate, platform, cwd);
94
+ const cwdKey = normalizePathForContainment(cwd, platform, cwd);
95
+ const projectRootKey = normalizePathForContainment(projectRoot, platform, cwd);
96
+ return !isSameOrDescendantPath(candidateKey, cwdKey, platform)
97
+ && !isSameOrDescendantPath(candidateKey, projectRootKey, platform);
98
+ }
99
+
100
+ export function resolveViaPath(command, {
101
+ env = process.env,
102
+ platform = process.platform,
103
+ existsSync = fsSync.existsSync,
104
+ cwd = process.cwd()
105
+ } = {}) {
106
+ if (!command) {
107
+ return null;
108
+ }
109
+
110
+ const pathApi = pathApiForPlatform(platform);
111
+ if (pathApi.isAbsolute(command)) {
112
+ return existsSync(command) ? pathApi.resolve(command) : null;
113
+ }
114
+
115
+ const delimiter = isWindowsPlatform(platform) ? ';' : ':';
116
+ const cwdKey = normalizeDirForCompare('.', platform, cwd);
117
+ const entries = String(env.PATH || env.Path || '')
118
+ .split(delimiter)
119
+ .map(entry => entry.replace(/^"|"$/g, ''));
120
+
121
+ const extensions = isWindowsPlatform(platform) && !pathApi.extname(command)
122
+ ? String(env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)
123
+ : [''];
124
+
125
+ for (const entry of entries) {
126
+ if (!entry || entry === '.') {
127
+ continue;
128
+ }
129
+ if (normalizeDirForCompare(entry, platform, cwd) === cwdKey) {
130
+ continue;
131
+ }
132
+
133
+ for (const ext of extensions) {
134
+ const candidate = pathApi.resolve(pathApi.join(entry, command + ext));
135
+ if (existsSync(candidate)) {
136
+ return candidate;
137
+ }
138
+ }
139
+ }
140
+
141
+ return null;
142
+ }
143
+
144
+ async function findWithWhere(command, {
145
+ env = process.env,
146
+ platform = process.platform,
147
+ execFileRawFn = execFileRaw,
148
+ cwd = process.cwd(),
149
+ projectRoot = cwd,
150
+ whereExe = null
151
+ } = {}) {
152
+ if (!isWindowsPlatform(platform)) {
153
+ return null;
154
+ }
155
+
156
+ try {
157
+ const result = await execFileRawFn(whereExe || absoluteWhereExe(), [command], { quiet: true, shell: false });
158
+ if (result.code !== 0) {
159
+ return null;
160
+ }
161
+
162
+ const lines = result.stdout
163
+ .split(/\r?\n/)
164
+ .map(line => line.trim())
165
+ .filter(Boolean);
166
+ for (const line of lines) {
167
+ if (isSafeWhereResult(line, { platform, cwd, projectRoot })) {
168
+ return pathApiForPlatform(platform).resolve(cwd, line);
169
+ }
170
+ }
171
+ return null;
172
+ } catch (error) {
173
+ return null;
174
+ }
175
+ }
176
+
177
+ function expandCmdVariables(match, cmdPath) {
178
+ const cmdDir = path.dirname(cmdPath);
179
+ const expanded = match
180
+ .replace(/%~dp0%?[\\/]?/gi, cmdDir + path.sep)
181
+ .replace(/%dp0%?[\\/]?/gi, cmdDir + path.sep)
182
+ .replace(/\\/g, path.sep);
183
+ return path.normalize(expanded);
184
+ }
185
+
186
+ async function findCodexJsFromCmd(cmdPath, { existsSync = null } = {}) {
187
+ try {
188
+ const content = await fs.readFile(cmdPath, 'utf-8');
189
+ const matches = content.match(/["']?[^"'\r\n]*node_modules[^"'\r\n]*codex[^"'\r\n]*\.js["']?/gi) || [];
190
+ for (const rawMatch of matches) {
191
+ const candidate = expandCmdVariables(rawMatch.replace(/^["']|["']$/g, ''), cmdPath);
192
+ if (await pathExistsWith(candidate, existsSync)) {
193
+ return candidate;
194
+ }
195
+ }
196
+ } catch (error) {
197
+ return null;
198
+ }
199
+
200
+ return null;
201
+ }
202
+
203
+ async function findCodexJsCommonPath({ env = process.env, existsSync = null } = {}) {
204
+ if (!env.APPDATA) {
205
+ return null;
206
+ }
207
+
208
+ const npmModules = path.join(env.APPDATA, 'npm', 'node_modules');
209
+ const candidates = [
210
+ path.join(npmModules, '@openai', 'codex', 'bin', 'codex.js'),
211
+ path.join(npmModules, 'codex', 'bin', 'codex.js'),
212
+ path.join(npmModules, '@openai', 'codex', 'dist', 'cli.js'),
213
+ path.join(npmModules, 'codex', 'dist', 'cli.js')
214
+ ];
215
+
216
+ for (const candidate of candidates) {
217
+ if (await pathExistsWith(candidate, existsSync)) {
218
+ return candidate;
219
+ }
220
+ }
221
+
222
+ return null;
223
+ }
224
+
225
+ function codexNotFoundError() {
226
+ const error = new Error('Codex nao encontrado pelo Node. Rode `where.exe codex` e configure `setx MAESTRO_CODEX_PATH "CAMINHO_COMPLETO_DO_CODEX"`.');
227
+ error.code = 'ENOENT';
228
+ return error;
229
+ }
230
+
231
+ function npxNotFoundError() {
232
+ const error = new Error('npx não encontrado pelo Node. Rode `where.exe npx` e configure `setx MAESTRO_NPX_PATH "CAMINHO_COMPLETO_DO_NPX_CMD"`.');
233
+ error.code = 'ENOENT';
234
+ return error;
235
+ }
236
+
237
+ function buildResolutionFailure(error, strategy, args) {
238
+ return {
239
+ code: 127,
240
+ signal: null,
241
+ timedOut: false,
242
+ stdout: '',
243
+ stderr: error.message,
244
+ invocation: null,
245
+ resolved: null,
246
+ strategy,
247
+ invocationMode: null,
248
+ args,
249
+ infrastructureFailure: true,
250
+ failureCategory: 'missing_tool',
251
+ runtimeDecision: 'BLOCK'
252
+ };
253
+ }
254
+
255
+ export function buildCmdOnlyFailure(invocation, strategy, args) {
256
+ return {
257
+ code: 2,
258
+ signal: null,
259
+ timedOut: false,
260
+ stdout: '',
261
+ stderr: 'engine_resolution_cmd_only: resolved to a .cmd/.bat wrapper (' + invocation.command + ') with no real .js/binary behind it; refusing to spawn via cmd.exe.',
262
+ invocation,
263
+ resolved: invocation,
264
+ strategy,
265
+ invocationMode: invocation.mode || null,
266
+ args,
267
+ infrastructureFailure: true,
268
+ failureCategory: 'engine_resolution_cmd_only',
269
+ runtimeDecision: 'BLOCK'
270
+ };
271
+ }
272
+
273
+ async function resolveDiscoveredCodex(command, source, deps) {
274
+ if (commandKind(command) === 'cmd') {
275
+ const scriptPath = await findCodexJsFromCmd(command, deps);
276
+ if (scriptPath) {
277
+ return {
278
+ command: process.execPath,
279
+ argsPrefix: [scriptPath],
280
+ kind: 'direct',
281
+ source: 'codex-js from ' + source
282
+ };
283
+ }
284
+ }
285
+
286
+ return {
287
+ command,
288
+ argsPrefix: [],
289
+ kind: commandKind(command),
290
+ source,
291
+ warning: commandKind(command) === 'cmd'
292
+ ? 'Usando fallback codex.cmd; prompts longos podem quebrar. Configure MAESTRO_CODEX_JS.'
293
+ : ''
294
+ };
295
+ }
296
+
297
+ async function discoverCommand(command, deps) {
298
+ return resolveViaPath(command, deps) || await findWithWhere(command, deps);
299
+ }
300
+
301
+ export async function resolveCodexInvocation(options = {}) {
302
+ const env = options.env || process.env;
303
+ const platform = options.platform || process.platform;
304
+ const deps = {
305
+ env,
306
+ platform,
307
+ existsSync: options.existsSync || fsSync.existsSync,
308
+ cwd: options.cwd || process.cwd(),
309
+ projectRoot: options.projectRoot || options.cwd || process.cwd(),
310
+ whereExe: options.whereExe || null,
311
+ execFileRawFn: options.execFileRawFn || options.execFileRaw || execFileRaw
312
+ };
313
+
314
+ if (env.MAESTRO_CODEX_JS) {
315
+ return {
316
+ command: process.execPath,
317
+ argsPrefix: [env.MAESTRO_CODEX_JS],
318
+ kind: 'direct',
319
+ source: 'MAESTRO_CODEX_JS'
320
+ };
321
+ }
322
+
323
+ const configured = env.MAESTRO_CODEX_PATH;
324
+ if (configured && isJsPath(configured)) {
325
+ return {
326
+ command: process.execPath,
327
+ argsPrefix: [configured],
328
+ kind: 'direct',
329
+ source: 'MAESTRO_CODEX_PATH js'
330
+ };
331
+ }
332
+
333
+ if (configured && commandKind(configured) === 'cmd') {
334
+ const scriptPath = await findCodexJsFromCmd(configured, deps);
335
+ if (scriptPath) {
336
+ return {
337
+ command: process.execPath,
338
+ argsPrefix: [scriptPath],
339
+ kind: 'direct',
340
+ source: 'codex-js from MAESTRO_CODEX_PATH'
341
+ };
342
+ }
343
+ }
344
+
345
+ const commonScriptPath = await findCodexJsCommonPath(deps);
346
+ if (commonScriptPath) {
347
+ return {
348
+ command: process.execPath,
349
+ argsPrefix: [commonScriptPath],
350
+ kind: 'direct',
351
+ source: 'codex-js'
352
+ };
353
+ }
354
+
355
+ if (configured) {
356
+ const pathApi = pathApiForPlatform(platform);
357
+ if (pathApi.isAbsolute(configured)) {
358
+ return resolveDiscoveredCodex(configured, 'MAESTRO_CODEX_PATH fallback', deps);
359
+ }
360
+
361
+ const found = await discoverCommand(configured, deps);
362
+ if (found) {
363
+ return resolveDiscoveredCodex(found, 'MAESTRO_CODEX_PATH PATH fallback', deps);
364
+ }
365
+
366
+ throw codexNotFoundError();
367
+ }
368
+
369
+ if (isWindowsPlatform(platform)) {
370
+ const candidates = ['codex.exe', 'codex.cmd', 'codex'];
371
+ for (const candidate of candidates) {
372
+ const found = await discoverCommand(candidate, deps);
373
+ if (found) {
374
+ return resolveDiscoveredCodex(found, 'PATH/where.exe ' + candidate, deps);
375
+ }
376
+ }
377
+
378
+ throw codexNotFoundError();
379
+ }
380
+
381
+ return {
382
+ command: CONFIG.codexPath,
383
+ argsPrefix: [],
384
+ kind: commandKind(CONFIG.codexPath),
385
+ source: 'fallback'
386
+ };
387
+ }
388
+
389
+ export function getDefaultCodexCmdPath() {
390
+ if (process.env.MAESTRO_CODEX_CMD) {
391
+ return process.env.MAESTRO_CODEX_CMD;
392
+ }
393
+ if (process.env.APPDATA) {
394
+ return path.join(process.env.APPDATA, 'npm', 'codex.cmd');
395
+ }
396
+ return 'C:\\Users\\PC\\AppData\\Roaming\\npm\\codex.cmd';
397
+ }
398
+
399
+ export function getDefaultCodexJsPath() {
400
+ if (process.env.MAESTRO_CODEX_JS) {
401
+ return process.env.MAESTRO_CODEX_JS;
402
+ }
403
+ if (process.env.APPDATA) {
404
+ return path.join(process.env.APPDATA, 'npm', 'node_modules', '@openai', 'codex', 'bin', 'codex.js');
405
+ }
406
+ return 'C:\\Users\\PC\\AppData\\Roaming\\npm\\node_modules\\@openai\\codex\\bin\\codex.js';
407
+ }
408
+
409
+ export function getCodexComparePath() {
410
+ return path.join(CONFIG.maestroPath, 'codex-compare.json');
411
+ }
412
+
413
+ export async function readCodexCompare() {
414
+ try {
415
+ return JSON.parse(await fs.readFile(getCodexComparePath(), 'utf-8'));
416
+ } catch (error) {
417
+ if (error.code === 'ENOENT') {
418
+ return null;
419
+ }
420
+ return null;
421
+ }
422
+ }
423
+
424
+ export async function writeCodexCompare(value) {
425
+ await fs.mkdir(CONFIG.maestroPath, { recursive: true });
426
+ const payload = stampSchemaVersion({
427
+ timestamp: new Date().toISOString(),
428
+ ...value
429
+ });
430
+ await fs.writeFile(getCodexComparePath(), JSON.stringify(payload, null, 2));
431
+ return payload;
432
+ }
433
+
434
+ // Hotfix A.2: auto mode only ever picks an ISOLATED-home invocation. The
435
+ // normal-home modes ('codex-js-normal-home', 'codex-cmd') silently shared
436
+ // the Codex Desktop config (plugins, elevated Windows sandbox, normal
437
+ // CODEX_HOME) with Maestro workers — exactly the mixing this hotfix
438
+ // removes. They remain available only via an explicit
439
+ // MAESTRO_CODEX_INVOCATION override and inside `maestro codex-compare`
440
+ // diagnostics.
441
+ function chooseInvocationFromCompare(compare) {
442
+ const tests = compare && Array.isArray(compare.tests) ? compare.tests : [];
443
+ const byInvocation = new Map();
444
+ for (const test of tests) {
445
+ if (test.invocation) {
446
+ byInvocation.set(test.invocation, test);
447
+ }
448
+ }
449
+ for (const candidate of ['codex-js-isolated', 'codex-cmd-isolated']) {
450
+ const test = byInvocation.get(candidate);
451
+ if (test && test.ok) {
452
+ return candidate;
453
+ }
454
+ }
455
+ return null;
456
+ }
457
+
458
+ export async function resolveCodexInvocationMode() {
459
+ if (CONFIG.codexInvocation && CONFIG.codexInvocation !== 'auto') {
460
+ return CONFIG.codexInvocation;
461
+ }
462
+ const compare = await readCodexCompare();
463
+ return chooseInvocationFromCompare(compare) || 'codex-js-isolated';
464
+ }
465
+
466
+ export async function resolveCodexInvocationForMode(mode = 'auto') {
467
+ const selected = mode === 'auto' ? await resolveCodexInvocationMode() : mode;
468
+ if (selected === 'codex-cmd' || selected === 'codex-cmd-isolated') {
469
+ return {
470
+ command: getDefaultCodexCmdPath(),
471
+ argsPrefix: [],
472
+ kind: 'cmd',
473
+ source: selected,
474
+ mode: selected,
475
+ usesIsolatedHome: selected === 'codex-cmd-isolated'
476
+ };
477
+ }
478
+ if (selected === 'codex-js-normal-home' || selected === 'codex-js-isolated' || selected === 'codex-js') {
479
+ const normalized = selected === 'codex-js' ? 'codex-js-isolated' : selected;
480
+ return {
481
+ command: process.execPath,
482
+ argsPrefix: [getDefaultCodexJsPath()],
483
+ kind: 'direct',
484
+ source: normalized,
485
+ mode: normalized,
486
+ usesIsolatedHome: normalized === 'codex-js-isolated'
487
+ };
488
+ }
489
+ return resolveCodexInvocation();
490
+ }
491
+
492
+ export function buildCodexEnvForInvocation(invocation, baseEnv = process.env) {
493
+ const env = { ...baseEnv };
494
+ if (invocation.usesIsolatedHome) {
495
+ env.CODEX_HOME = CONFIG.codexHome;
496
+ if (!env.NINEROUTER_API_KEY && env.MAESTRO_ALLOW_LOCAL_TEST_KEY === '1') {
497
+ env.NINEROUTER_API_KEY = 'local-test';
498
+ }
499
+ } else {
500
+ delete env.CODEX_HOME;
501
+ }
502
+ return env;
503
+ }
504
+
505
+ export async function resolveCodexCommand() {
506
+ const invocation = await resolveCodexInvocation();
507
+ return {
508
+ command: invocation.command,
509
+ kind: invocation.kind || commandKind(invocation.command),
510
+ source: invocation.source,
511
+ argsPrefix: invocation.argsPrefix,
512
+ warning: invocation.warning
513
+ };
514
+ }
515
+
516
+ export async function runCodex(args, options = {}) {
517
+ const {
518
+ execFileRaw: execFileRawOption,
519
+ execFileRawFn = execFileRawOption || execFileRaw,
520
+ invocation: injectedInvocation,
521
+ strategyName: injectedStrategyName,
522
+ ...execOptions
523
+ } = options;
524
+
525
+ try {
526
+ const invocation = injectedInvocation || await resolveCodexInvocationForMode('auto');
527
+ const strategyName = injectedStrategyName || await resolveRunCodexStrategy();
528
+ const strategyArgs = await applyCodexStrategy(args, strategyName);
529
+ const fullArgs = [...invocation.argsPrefix, ...strategyArgs];
530
+ const kind = invocation.kind || commandKind(invocation.command);
531
+ const env = execOptions.env ? execOptions.env : buildCodexEnvForInvocation(invocation);
532
+ if (kind === 'cmd') {
533
+ return buildCmdOnlyFailure(invocation, strategyName, strategyArgs);
534
+ }
535
+ const result = await execFileRawFn(invocation.command, fullArgs, { ...execOptions, env, shell: false });
536
+ return { ...result, invocation, resolved: invocation, strategy: strategyName, invocationMode: invocation.mode, args: strategyArgs };
537
+ } catch (error) {
538
+ if (error.code === 'ENOENT') {
539
+ return buildResolutionFailure(codexNotFoundError(), injectedStrategyName || null, args);
540
+ }
541
+ throw error;
542
+ }
543
+ }
544
+
545
+ // 'workspace-permissions' was removed in Hotfix A.2: its profile layer was
546
+ // never loaded by any default run (doctor always reported
547
+ // "default_permissions used: false"), and `default_permissions` cannot be
548
+ // combined with the `sandbox_mode` mechanism this project standardizes on.
549
+ // 'trusted-workspace' is deliberately NOT in this list: it
550
+ // bypasses Codex's internal sandbox and may only be enabled explicitly via
551
+ // MAESTRO_CODEX_STRATEGY=trusted-workspace (see evaluateTrustedWorkspaceStrategy),
552
+ // never adopted automatically from a validated-strategy file.
553
+ export const SAFE_CODEX_STRATEGIES = ['basic', 'global-approval', 'config-override', 'ignore-rules'];
554
+
555
+ // Guard for the explicit, audited Windows-sandbox fallback. When Codex's
556
+ // internal Windows sandbox is broken (helper_unknown_error: apply deny-read
557
+ // ACLs), the ONLY sanctioned escape hatch is MAESTRO_CODEX_STRATEGY=
558
+ // trusted-workspace, and it still runs inside every Maestro-side guard
559
+ // (safety guard, runtime gate, deletion guard, workspace diff, engine
560
+ // policy, budget). This function is pure so tests can exercise it without
561
+ // touching the environment.
562
+ export function evaluateTrustedWorkspaceStrategy({
563
+ envStrategy = process.env.MAESTRO_CODEX_STRATEGY,
564
+ cwd = process.cwd(),
565
+ projectRoot = process.cwd()
566
+ } = {}) {
567
+ if (envStrategy !== 'trusted-workspace') {
568
+ return {
569
+ allowed: false,
570
+ reason: 'trusted-workspace requires the explicit environment variable MAESTRO_CODEX_STRATEGY=trusted-workspace; it is never a silent default.'
571
+ };
572
+ }
573
+ const resolvedCwd = path.resolve(String(cwd));
574
+ const resolvedRoot = path.resolve(String(projectRoot));
575
+ const normalizedCwd = resolvedCwd.toLowerCase();
576
+ const normalizedRoot = resolvedRoot.toLowerCase();
577
+ const inside = normalizedCwd === normalizedRoot || normalizedCwd.startsWith(normalizedRoot + path.sep.toLowerCase());
578
+ if (!inside) {
579
+ return {
580
+ allowed: false,
581
+ reason: 'trusted-workspace only operates inside the projectRoot (' + resolvedRoot + '); current cwd is ' + resolvedCwd + '.'
582
+ };
583
+ }
584
+ return {
585
+ allowed: true,
586
+ reason: 'explicit MAESTRO_CODEX_STRATEGY=trusted-workspace inside the projectRoot.'
587
+ };
588
+ }
589
+
590
+ // Runtime wrapper: on top of the pure guard, require an initialized Maestro
591
+ // project root (.maestro present) so the bypass can never run against an
592
+ // arbitrary directory.
593
+ export async function resolveTrustedWorkspaceGuard() {
594
+ const guard = evaluateTrustedWorkspaceStrategy();
595
+ if (!guard.allowed) {
596
+ return guard;
597
+ }
598
+ if (!(await pathExists(path.join(process.cwd(), CONFIG.maestroPath)))) {
599
+ return {
600
+ allowed: false,
601
+ reason: 'trusted-workspace requires an initialized Maestro project root (.maestro not found in ' + process.cwd() + ').'
602
+ };
603
+ }
604
+ return guard;
605
+ }
606
+
607
+ export function getCodexStrategyPath() {
608
+ return path.join(CONFIG.maestroPath, 'codex-strategy.json');
609
+ }
610
+
611
+ export async function readValidatedCodexStrategy() {
612
+ try {
613
+ const value = JSON.parse(await fs.readFile(getCodexStrategyPath(), 'utf-8'));
614
+ return value && value.strategy ? value : null;
615
+ } catch (error) {
616
+ if (error.code === 'ENOENT') {
617
+ return null;
618
+ }
619
+ return null;
620
+ }
621
+ }
622
+
623
+ export async function writeValidatedCodexStrategy(entry) {
624
+ await fs.mkdir(CONFIG.maestroPath, { recursive: true });
625
+ const value = stampSchemaVersion({
626
+ timestamp: new Date().toISOString(),
627
+ ...entry
628
+ });
629
+ await fs.writeFile(getCodexStrategyPath(), JSON.stringify(value, null, 2));
630
+ return value;
631
+ }
632
+
633
+ export async function resolveRunCodexStrategy() {
634
+ const configured = CONFIG.codexStrategy;
635
+ if (configured === 'trusted-workspace') {
636
+ // Explicit, audited fallback only: if the guard refuses (not enabled
637
+ // explicitly, outside the projectRoot, or no .maestro root), fall back
638
+ // to the sandboxed 'basic' strategy instead of bypassing silently.
639
+ const guard = await resolveTrustedWorkspaceGuard();
640
+ return guard.allowed ? 'trusted-workspace' : 'basic';
641
+ }
642
+ if (configured && configured !== 'auto') {
643
+ return configured;
644
+ }
645
+ const validated = await readValidatedCodexStrategy();
646
+ if (validated && SAFE_CODEX_STRATEGIES.includes(validated.strategy)) {
647
+ return validated.strategy;
648
+ }
649
+ return 'basic';
650
+ }
651
+
652
+ function removeOptionPair(args, optionName) {
653
+ const next = [];
654
+ for (let i = 0; i < args.length; i++) {
655
+ if (args[i] === optionName) {
656
+ i++;
657
+ continue;
658
+ }
659
+ next.push(args[i]);
660
+ }
661
+ return next;
662
+ }
663
+
664
+ export async function applyCodexStrategy(args, strategyName) {
665
+ if (!args.length || args[0] !== 'exec') {
666
+ return args;
667
+ }
668
+
669
+ if (strategyName === 'global-approval') {
670
+ return ['--ask-for-approval', 'never', ...args];
671
+ }
672
+ if (strategyName === 'config-override') {
673
+ const prompt = args[args.length - 1];
674
+ const head = args.slice(0, -1);
675
+ return [...head, '-c', 'approval_policy="never"', '-c', 'sandbox_mode="workspace-write"', prompt];
676
+ }
677
+ if (strategyName === 'global-config-override') {
678
+ return ['-c', 'approval_policy="never"', '-c', 'sandbox_mode="workspace-write"', ...args];
679
+ }
680
+ if (strategyName === 'ignore-rules') {
681
+ const prompt = args[args.length - 1];
682
+ const head = args.slice(0, -1);
683
+ return [...head, '--ignore-rules', prompt];
684
+ }
685
+ if (strategyName === 'trusted-workspace') {
686
+ const guard = await resolveTrustedWorkspaceGuard();
687
+ if (!guard.allowed) {
688
+ // Never bypass silently: without the explicit env opt-in inside the
689
+ // projectRoot, keep the normal sandboxed args.
690
+ return args;
691
+ }
692
+ const prompt = args[args.length - 1];
693
+ const head = removeOptionPair(args.slice(0, -1), '--sandbox');
694
+ return [...head, '--dangerously-bypass-approvals-and-sandbox', prompt];
695
+ }
696
+ if (strategyName === 'dangerous-yolo') {
697
+ const prompt = args[args.length - 1];
698
+ const head = args.slice(0, -1);
699
+ return [...head, '--dangerously-bypass-approvals-and-sandbox', prompt];
700
+ }
701
+ return args;
702
+ }
703
+
704
+ export async function getCodexHelpMatrix() {
705
+ const globalHelp = await runCodexRaw(['--help'], { quiet: true, env: getCodexEnv() });
706
+ const execHelp = await runCodexRaw(['exec', '--help'], { quiet: true, env: getCodexEnv() });
707
+ const globalOutput = (globalHelp.stdout || '') + '\n' + (globalHelp.stderr || '');
708
+ const execOutput = (execHelp.stdout || '') + '\n' + (execHelp.stderr || '');
709
+ return {
710
+ globalHelp,
711
+ execHelp,
712
+ hasGlobalAskForApproval: /--ask-for-approval\b/.test(globalOutput),
713
+ hasGlobalShortApproval: /(^|\s)-a[,=\s]/.test(globalOutput) || /-a,\s*--ask-for-approval/.test(globalOutput),
714
+ hasExecSandbox: /--sandbox\b/.test(execOutput),
715
+ hasExecConfig: /--config\b/.test(execOutput),
716
+ hasExecShortConfig: /(^|\s)-c[,=\s]/.test(execOutput) || /-c,\s*--config/.test(execOutput),
717
+ hasExecIgnoreRules: /--ignore-rules\b/.test(execOutput),
718
+ hasDangerousBypass: /--dangerously-bypass-approvals-and-sandbox\b/.test(globalOutput + '\n' + execOutput)
719
+ };
720
+ }
721
+
722
+ export async function runCodexRaw(args, options = {}) {
723
+ const {
724
+ execFileRaw: execFileRawOption,
725
+ execFileRawFn = execFileRawOption || execFileRaw,
726
+ invocation: injectedInvocation,
727
+ invocationMode = 'auto',
728
+ ...execOptions
729
+ } = options;
730
+
731
+ try {
732
+ const invocation = injectedInvocation || await resolveCodexInvocationForMode(invocationMode);
733
+ const fullArgs = [...invocation.argsPrefix, ...args];
734
+ const kind = invocation.kind || commandKind(invocation.command);
735
+ const env = execOptions.env ? execOptions.env : buildCodexEnvForInvocation(invocation);
736
+ if (kind === 'cmd') {
737
+ return buildCmdOnlyFailure(invocation, null, args);
738
+ }
739
+ const result = await execFileRawFn(invocation.command, fullArgs, { ...execOptions, env, shell: false });
740
+ return { ...result, invocation, resolved: invocation, args };
741
+ } catch (error) {
742
+ if (error.code === 'ENOENT') {
743
+ return buildResolutionFailure(codexNotFoundError(), null, args);
744
+ }
745
+ throw error;
746
+ }
747
+ }
748
+
749
+ export async function resolveNpxInvocation() {
750
+ if (process.env.MAESTRO_NPX_PATH) {
751
+ return {
752
+ command: process.env.MAESTRO_NPX_PATH,
753
+ argsPrefix: [],
754
+ kind: commandKind(process.env.MAESTRO_NPX_PATH),
755
+ source: 'MAESTRO_NPX_PATH'
756
+ };
757
+ }
758
+
759
+ if (isWindows()) {
760
+ const candidates = ['npx.cmd', 'npx'];
761
+ for (const candidate of candidates) {
762
+ const found = await findWithWhere(candidate);
763
+ if (found) {
764
+ return {
765
+ command: found,
766
+ argsPrefix: [],
767
+ kind: commandKind(found),
768
+ source: 'where.exe ' + candidate
769
+ };
770
+ }
771
+ }
772
+
773
+ throw npxNotFoundError();
774
+ }
775
+
776
+ return {
777
+ command: CONFIG.npxPath,
778
+ argsPrefix: [],
779
+ kind: commandKind(CONFIG.npxPath),
780
+ source: 'fallback'
781
+ };
782
+ }
783
+
784
+ export async function runNpx(args, options = {}) {
785
+ const invocation = await resolveNpxInvocation();
786
+ const fullArgs = [...invocation.argsPrefix, ...args];
787
+ const kind = invocation.kind || commandKind(invocation.command);
788
+
789
+ try {
790
+ const result = kind === 'cmd'
791
+ ? await execCmdFile(invocation.command, fullArgs, options)
792
+ : await execFileRaw(invocation.command, fullArgs, options);
793
+ return { ...result, invocation, resolved: invocation };
794
+ } catch (error) {
795
+ if (error.code === 'ENOENT') {
796
+ error.message = npxNotFoundError().message;
797
+ }
798
+ throw error;
799
+ }
800
+ }
801
+
802
+ export async function checkCommand(command, args = []) {
803
+ try {
804
+ const result = await execFileRaw(command, args, { quiet: true });
805
+ return { ok: result.code === 0, ...result };
806
+ } catch (error) {
807
+ return { ok: false, code: error.code || 1, stdout: '', stderr: error.message };
808
+ }
809
+ }