Skip to content

Commit 0ad5a9f

Browse files
authored
fix(core): harden core super update governance and lifecycle (#802)
* feat: add optimized AIOX agents and skills for Grok Build Introduce .grok/ integration with token-efficient agent profiles, slash skills, roles, personas, and compact rules. Add regenerable sync script (npm run sync:skills:grok) so Grok stays aligned with .source agents under .aiox-core/development/agents/. * fix: improve grok-skills-sync purpose metadata for IDS registry Clarify the generator module purpose so entity-registry extraction no longer picks an unrelated CodeRabbit comment string. * test: harden flaky pre-push suites for local agent pollution Make Claude subagent governance validate the core aiox-* set instead of exact directory equality, and guard dev-context-loader cacheHits when status/shape is incomplete. * fix: regenerate install manifest for entity-registry hash CI Install Manifest Validation failed because data/entity-registry.yaml hash no longer matched install-manifest.yaml after IDS updates. * fix: address CodeRabbit review on Grok skills sync Escape YAML/TOML scalars, constrain generated write paths under .grok/, block git push for squad-creator, label markdown fences, and keep the cacheHits > 0 assertion in dev-context-loader. * chore: sync entity-registry and install manifest after IDS hook Keep install-manifest hash aligned with entity-registry so CI Install Manifest Validation stays green. * docs: add CORE-SUPER-UPDATE epic and roadmap from hub/enterprise harvest Branch off PR #800 (Grok). Captures P0–P2 waves for SDC skills, runtime hygiene, orchestration merge, IDE parity, and selective constitution updates without importing Sinkra product or enterprise IP. Note: docs/stories is gitignored; force-add for this framework epic only. * docs: apply roundtable APPROVE_WITH_FIXES to CORE-SUPER-UPDATE MVP cut (A+B), hard A3 merge gates, B0 lean spike, denylist CI story, deferred D5/E4/F3, semver ship train, tightened A1 ACs. Full finding resolution table embedded in epic (100% resolved). * docs: rehome CORE-SUPER-UPDATE under docs/framework/epics (architect-first) Address architect-first findings: public versionable path (no force-add gitignored docs/stories), drop stale stories index, ARCH-A slice, A2 as #797 residual investigation (unref already present), permissions metric clarified (extend not replace), Wave B–E blocked until per-wave ARCH, hard ban on workspace/ in OSS, hub XI/XII numbering locked for constitution. * feat(synapse): complete CORE-SU.A1 configurable pipeline timeout tests/docs Engine already resolves AIOX_SYNAPSE_PIPELINE_TIMEOUT_MS > core-config synapse.pipelineTimeoutMs > 100ms default with clamp and timeout warn. Add unit coverage for precedence/invalid values/soft-fail path and document knobs in config-override-guide. Closes #798 when merged. * feat(synapse): wire configurable pipeline timeout in engine and core-config Export resolvePipelineTimeoutMs (env > config > 100ms default, clamp 1-30000), soft-fail remaining layers with console.warn. Config key synapse.pipelineTimeoutMs. Completes CORE-SU.A1 implementation. * chore: sync manifest after A1 + mark A1 done on roadmap * fix(config): skip ConfigCache sweep timer under Jest (#797) unref() alone still allows post-teardown callbacks. Skip setInterval when JEST_WORKER_ID is set; keep production timer + unref; gate logs on AIOX_DEBUG; export disposeConfigCacheTimers. Applies to core and infrastructure copies. CORE-SU.A2 YOLO. * chore: regenerate install manifest after ConfigCache fix * feat(permissions): add path, prompt, and SSRF guards (CORE-SU.A3) Extend core/permissions without replacing PermissionMode/OperationGuard. OSS path deny list excludes product workspace/ trees. Unit tests for traversal, injection patterns, private/metadata SSRF. Wave A P0. * chore: regenerate install manifest after permissions guards * feat(security): OSS port denylist CI + doctor check (CORE-SU.A4) Add scan for hub/enterprise leakage (workspace product paths, sinkra_*, mux-adapter, coolify, machine paths). npm run validate:port-denylist; doctor check port-denylist. Completes Wave A. * chore: regenerate install manifest after port-denylist * feat(sdc): Wave B lean SDC skills + Grok sync (CORE-SUPER-UPDATE) Ship OSS lean SDC skill wrappers (validate → develop → review → apply-qa-fixes → close + full-sdc orchestrator) with tasks as SOT. Wire grok-skills-sync to copy development/skills → .grok/skills/aiox-*. Sequence Lock: only close-story sets Done. No product harvest trees. * feat(sdc): executable full-sdc + wave-execute runtime (CLI First) Add lean SDC/wave engines under .aiox-core/core/sdc with aiox sdc and aiox wave CLI for plan/next/verify/progress. Upgrade full-sdc skill to EXECUTE loop; add wave-execute that dispatches full-sdc children via DAG + file-ownership batches. Grok skills synced; unit tests included. * feat(sdc): C1 wave-run controller + close Wave A residuals Close CORE-SU.A1 with hook-runtime synapse config wiring and QA PASS. Add ARCH-C and wave-run advance/mark/cascade/report CLI. Normalize A2–A4 to Done; package scripts sdc/wave; unit tests for wave-run. * fix(security): harden port-denylist scan + format sdc modules Scan .claude surfaces; fail closed on unreadable paths; doctor wraps scan errors. Apply lint formatting to sdc/wave-run modules. * feat(sdc): complete Wave C via wave-execute + full-sdc (C2–C4) Execute CORE-SU-C wave: dispatch-adapter (parallel/sequential pool), epic-glue + aiox wave from-epic, tests (23). Stories C2–C4 Done; wave report completed. Skills updated for from-epic flow. * feat(install): Windows npx ECOMPROMISED guidance (CORE-SU.F1 #773) Add install-time hint, doctor windows-npx-install check (17 total), and docs for cold-cache lock timeout. ARCH-D/E drafts unlock stretch planning. Wave CORE-SU-F completed via full-sdc. * feat(core): real pm.sh agent CLI + constitution XI/XII + IDE contract Remove pm.sh echo-stub: invoke CLAUDE_CMD/claude or fail closed. Port OSS-safe Articles XI (Squad-First) and XII (Model Governance). Add ide-sync-contract (D1) and closeout stories for wave-execute. * docs(governance): dual-path stories + ignore unhardened .codex Official policy: docs/framework/epics/ for versioned framework OSS work; docs/stories/ remains project L4 (gitignored template). Align AGENTS.md, CLAUDE.md, story-lifecycle paths, and Grok sync prompts. Gitignore .codex/ until absolute paths / force-push wording are hardened for OSS. * feat(harvest): Wave 0 three-way framework diff + analysis corrections Add npm run diff:framework-3way (peer discovery via env/flags), doctor advisory framework-3way-diff (18 checks), and ANALYSIS-3WAY-CORRECTIONS from verified OSS×hub×enterprise facts. Prefer enterprise lean full-sdc as skill enrichment base; never blind-merge OSS-wins modules. * docs(harvest): mark Wave 0 arch/story Done; peer env vars * feat(synapse): port memory-bridge heuristics from hub (CORE-SU.MB) Cold/warm timeouts, processSessionDigest + debounced reinforce worker, OSS global-heuristic-hints.yaml, and heuristics unit tests. Does not replace hook-runtime wholesale. Graceful when MIS absent. * docs(epic): mark core super update pr-ready * chore(ids): refresh generated registry metadata * chore(epic): freeze CORE-SUPER-UPDATE required scope complete Mark epic complete for ship; document optional/deferred backlog as out-of-release. Gitignore legacy .claude/skills/aios-*/ local noise before pre-push and CodeRabbit. * fix(doctor): keep framework-3way check lightweight Avoid full tree hash in aiox doctor (was timing out CLI tests when peers exist). Presence-only advisory; full report stays on npm run diff:framework-3way. * chore(ids): sync entity-registry after doctor check fix * docs: strip trailing whitespace in ROADMAP for pre-push * chore(ids): sync registry metadata after pre-push * fix(security): address CodeRabbit majors from pre-push review SSRF fe80::/10 + IPv4-mapped delegation; memory-bridge warm only on success; wave cascade skip origin; SDC safeId/loadJson/plan validation; npx hint null-env; port-denylist Windows path; gate match anchoring. * chore(ids): refresh entity-registry after CR fixes * fix(ci): allow Claude SDC skills in parity gate * chore(ids): sync registry after parity fix * fix(ids): skip registry rewrite when entity is unchanged Avoid lastVerified thrash on no-op processChanges; covers pre-push noise from IDS hooks. * chore(ids): sync registry after idempotent updater fix * fix(core): harden core super update governance * chore(ids): sync registry after core hardening * docs(core): bind final QA provenance to commit * fix(ci): restore Vercel preview build * docs(core): bind Vercel QA provenance * fix(core): address PR review feedback * chore(ids): regen entity-registry * docs(core): bind PR review QA provenance * chore(ids): restore deterministic registry * docs(core): finalize PR review provenance * fix(core): respect interactive QA remediation mode * chore(ids): refresh review remediation registry * docs(core): record interactive remediation QA * fix(core): enforce preflight before phase execution * chore(ids): refresh preflight remediation registry * docs(core): record preflight ordering QA * test(core): decouple lifecycle audit wording * chore(ids): refresh CI remediation registry * docs(core): record CI remediation QA * fix(core): halt failed QA remediation verification * chore(ids): refresh QA loop remediation registry * docs(core): record QA remediation verification
1 parent a139500 commit 0ad5a9f

83 files changed

Lines changed: 5802 additions & 1886 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.aiox-core/cli/commands/config/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ function splitL4(config) {
415415
// mcp_configuration (Section 5)
416416
if (config.mcp) l4.mcp = config.mcp;
417417

418-
// coderabbit secrets/machine (Section 9 - machine portion)
418+
// coderabbit secret and machine config (Section 9 - machine portion)
419419
if (config.coderabbit_integration) {
420420
const cr = config.coderabbit_integration;
421421
const l4cr = {};

.aiox-core/cli/commands/sdc/index.js

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,17 @@
1111

1212
'use strict';
1313

14+
const fs = require('fs');
1415
const path = require('path');
1516
const { Command } = require('commander');
17+
const { assertDispatchGovernance } = require('../../../core/permissions');
1618
const sdc = require('../../../core/sdc');
1719

20+
/**
21+
* Build the `aiox sdc` Commander command tree.
22+
*
23+
* @returns {Command} Configured SDC command.
24+
*/
1825
function createSdcCommand() {
1926
const cmd = new Command('sdc');
2027
cmd.description('Lean Full SDC runtime (plan, verify, progress)');
@@ -26,6 +33,38 @@ function createSdcCommand() {
2633
console.log(sdc.PHASES.join('\n'));
2734
});
2835

36+
cmd
37+
.command('preflight')
38+
.description('Enforce model budget, story binding, and intent scan before dispatch')
39+
.argument('<story-path>', 'Path to the story bound to the model call')
40+
.requiredOption('--budget-usd <amount>', 'Positive model budget ceiling in USD')
41+
.requiredOption('--intent-file <path>', 'File containing the exact child model intent')
42+
.option('--context-file <path>', 'File containing the exact child context')
43+
.option('--task <task>', 'Dispatch task name', 'develop')
44+
.option('--json', 'JSON evidence output', false)
45+
.action((storyPath, opts) => {
46+
try {
47+
const absoluteStoryPath = path.resolve(storyPath);
48+
const intent = fs.readFileSync(path.resolve(opts.intentFile), 'utf8');
49+
const context = opts.contextFile
50+
? fs.readFileSync(path.resolve(opts.contextFile), 'utf8')
51+
: '';
52+
const evidence = assertDispatchGovernance({
53+
budgetCeilingUsd: opts.budgetUsd,
54+
task: opts.task,
55+
intent: `${intent}\n${context}`,
56+
story: absoluteStoryPath,
57+
projectRoot: path.dirname(absoluteStoryPath),
58+
requiresStory: true,
59+
});
60+
if (opts.json) console.log(JSON.stringify(evidence, null, 2));
61+
else console.log(`SDC dispatch preflight: PASS (${evidence.story.storyId})`);
62+
} catch (error) {
63+
console.error(`[${error.code || 'SDC_PREFLIGHT'}] ${error.message}`);
64+
process.exitCode = 5;
65+
}
66+
});
67+
2968
cmd
3069
.command('plan')
3170
.description('Initialize full-sdc run state for a story')
@@ -124,6 +163,7 @@ function createSdcCommand() {
124163
'passed | failed | skipped | halted'
125164
)
126165
.option('--notes <notes>', 'Optional notes')
166+
.option('--outcome <outcome>', 'approved | changes_requested (review phase)')
127167
.option('--json', 'JSON output', false)
128168
.action((storyId, phase, opts) => {
129169
let state = sdc.loadSdcState(storyId);
@@ -132,10 +172,16 @@ function createSdcCommand() {
132172
process.exitCode = 1;
133173
return;
134174
}
135-
if (phase === 'apply_qa_fixes' && opts.status === 'passed') {
136-
state.qgIterations = (state.qgIterations || 0) + 1;
175+
if (
176+
phase === 'review' &&
177+
opts.status === 'passed' &&
178+
!['approved', 'changes_requested'].includes(opts.outcome)
179+
) {
180+
console.error('Review passed requires --outcome approved|changes_requested');
181+
process.exitCode = 1;
182+
return;
137183
}
138-
sdc.markPhase(state, phase, opts.status, opts.notes);
184+
sdc.markPhase(state, phase, opts.status, opts.notes, { outcome: opts.outcome });
139185
sdc.saveSdcState(state);
140186
if (opts.json) {
141187
console.log(JSON.stringify(state, null, 2));

.aiox-core/cli/commands/wave/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ function resolveStoryPaths(inputs) {
4141
return [...new Set(out.map((p) => path.resolve(p)))];
4242
}
4343

44+
/**
45+
* Build the `aiox wave` Commander command tree.
46+
*
47+
* @returns {Command} Configured wave command.
48+
*/
4449
function createWaveCommand() {
4550
const cmd = new Command('wave');
4651
cmd.description('Lean wave-execute planner (DAG + file partition)');

.aiox-core/core/execution/parallel-executor.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,16 +217,19 @@ class ParallelExecutor extends EventEmitter {
217217
* Wrap execution with timeout and error handling
218218
*/
219219
async _wrapExecution(provider, executor) {
220+
let timeoutId;
220221
try {
221222
const result = await Promise.race([
222223
executor(),
223-
new Promise((_, reject) =>
224-
setTimeout(() => reject(new Error('Timeout')), this.timeout),
225-
),
224+
new Promise((_, reject) => {
225+
timeoutId = setTimeout(() => reject(new Error('Timeout')), this.timeout);
226+
}),
226227
]);
227228
return { ...result, provider };
228229
} catch (error) {
229230
return { success: false, error: error.message, provider };
231+
} finally {
232+
if (timeoutId) clearTimeout(timeoutId);
230233
}
231234
}
232235

.aiox-core/core/execution/parallel-monitor.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,10 @@ class ParallelMonitor extends EventEmitter {
166166
this.broadcast('task_completed', { taskId, task });
167167

168168
// Remove from active after delay
169-
setTimeout(() => {
169+
const cleanupTimer = setTimeout(() => {
170170
this.activeTasks.delete(taskId);
171171
}, 10000);
172+
cleanupTimer.unref?.();
172173
}
173174
}
174175

@@ -193,9 +194,10 @@ class ParallelMonitor extends EventEmitter {
193194
this.broadcast('wave_completed', { waveId, wave });
194195

195196
// Move to history after delay
196-
setTimeout(() => {
197+
const cleanupTimer = setTimeout(() => {
197198
this.activeWaves.delete(waveId);
198199
}, 30000);
200+
cleanupTimer.unref?.();
199201
}
200202
}
201203

.aiox-core/core/execution/wave-executor.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ class WaveExecutor extends EventEmitter {
206206
*/
207207
async executeTaskWithTimeout(task, context) {
208208
const startTime = Date.now();
209+
let timeoutId;
209210

210211
// Track active execution
211212
this.activeExecutions.set(task.id, {
@@ -219,7 +220,7 @@ class WaveExecutor extends EventEmitter {
219220
try {
220221
// Create timeout promise
221222
const timeoutPromise = new Promise((_, reject) => {
222-
setTimeout(() => {
223+
timeoutId = setTimeout(() => {
223224
reject(new Error(`Task ${task.id} timed out after ${this.taskTimeout}ms`));
224225
}, this.taskTimeout);
225226
});
@@ -272,10 +273,12 @@ class WaveExecutor extends EventEmitter {
272273
duration: Date.now() - startTime,
273274
};
274275
} finally {
276+
if (timeoutId) clearTimeout(timeoutId);
275277
// Remove from active after a delay (for monitoring)
276-
setTimeout(() => {
278+
const cleanupTimer = setTimeout(() => {
277279
this.activeExecutions.delete(task.id);
278280
}, 5000);
281+
cleanupTimer.unref?.();
279282
}
280283
}
281284

.aiox-core/core/orchestration/agent-invoker.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -419,12 +419,20 @@ class AgentInvoker extends EventEmitter {
419419
* @private
420420
*/
421421
async _executeWithTimeout(fn, timeout) {
422-
return Promise.race([
423-
fn(),
424-
new Promise((_, reject) =>
425-
setTimeout(() => reject(new Error('Task execution timed out')), timeout),
426-
),
427-
]);
422+
let timeoutId;
423+
try {
424+
return await Promise.race([
425+
fn(),
426+
new Promise((_, reject) => {
427+
timeoutId = setTimeout(
428+
() => reject(new Error('Task execution timed out')),
429+
timeout,
430+
);
431+
}),
432+
]);
433+
} finally {
434+
if (timeoutId) clearTimeout(timeoutId);
435+
}
428436
}
429437

430438
/**

.aiox-core/core/orchestration/terminal-spawner.js

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
'use strict';
1414

15-
const { spawn, execSync } = require('child_process');
15+
const { spawn, execFileSync } = require('child_process');
1616
const fs = require('fs').promises;
1717
const fsSync = require('fs');
1818
const path = require('path');
@@ -30,6 +30,8 @@ const RETRY_DELAY_MS = 1000;
3030
* @enum {string}
3131
*/
3232
const ENVIRONMENT_TYPE = {
33+
INLINE: 'INLINE',
34+
TEST: 'TEST',
3335
NATIVE_TERMINAL: 'NATIVE_TERMINAL',
3436
VSCODE: 'VSCODE',
3537
SSH: 'SSH',
@@ -49,16 +51,29 @@ const ENVIRONMENT_TYPE = {
4951
* Detects the current execution environment (Story 12.10 - Task 1)
5052
*
5153
* Detection priority:
52-
* 1. CI/CD (GitHub Actions, GitLab CI, etc.)
53-
* 2. Docker container
54-
* 3. SSH session
55-
* 4. VS Code integrated terminal
56-
* 5. Native terminal (default)
54+
* 1. Explicit inline/no-visual override
55+
* 2. CI/CD (GitHub Actions, GitLab CI, etc.)
56+
* 3. Docker container
57+
* 4. SSH session
58+
* 5. VS Code integrated terminal
59+
* 6. Test runner
60+
* 7. Native terminal (default)
5761
*
5862
* @returns {EnvironmentInfo} Environment detection result
5963
*/
6064
function detectEnvironment() {
61-
// 1. CI/CD detection (Task 1.5)
65+
if (
66+
process.env.AIOX_INLINE_MODE === 'true' ||
67+
process.env.AIOX_NO_VISUAL_TERMINAL === 'true'
68+
) {
69+
return {
70+
type: ENVIRONMENT_TYPE.INLINE,
71+
supportsVisualTerminal: false,
72+
reason: 'Explicit inline/no-visual terminal mode',
73+
};
74+
}
75+
76+
// 2. CI/CD detection (Task 1.5)
6277
// Check common CI environment variables
6378
if (
6479
process.env.CI === 'true' ||
@@ -78,7 +93,7 @@ function detectEnvironment() {
7893
};
7994
}
8095

81-
// 2. Docker container detection (Task 1.4)
96+
// 3. Docker container detection (Task 1.4)
8297
// Check for /.dockerenv file or cgroup indicators
8398
try {
8499
if (fsSync.existsSync('/.dockerenv')) {
@@ -104,7 +119,7 @@ function detectEnvironment() {
104119
// Ignore file read errors (e.g., on Windows)
105120
}
106121

107-
// 3. SSH session detection (Task 1.3)
122+
// 4. SSH session detection (Task 1.3)
108123
if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) {
109124
return {
110125
type: ENVIRONMENT_TYPE.SSH,
@@ -113,7 +128,7 @@ function detectEnvironment() {
113128
};
114129
}
115130

116-
// 4. VS Code integrated terminal detection (Task 1.2)
131+
// 5. VS Code integrated terminal detection (Task 1.2)
117132
if (
118133
process.env.TERM_PROGRAM === 'vscode' ||
119134
process.env.VSCODE_PID ||
@@ -127,7 +142,15 @@ function detectEnvironment() {
127142
};
128143
}
129144

130-
// 5. Native terminal (default) - supports visual spawn
145+
if (process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID) {
146+
return {
147+
type: ENVIRONMENT_TYPE.TEST,
148+
supportsVisualTerminal: false,
149+
reason: 'Test runner detected (visual side effects disabled)',
150+
};
151+
}
152+
153+
// 7. Native terminal (default) - supports visual spawn
131154
return {
132155
type: ENVIRONMENT_TYPE.NATIVE_TERMINAL,
133156
supportsVisualTerminal: true,
@@ -146,6 +169,7 @@ const DEFAULT_OPTIONS = {
146169
outputDir: os.tmpdir(),
147170
retries: MAX_RETRIES,
148171
debug: false,
172+
budgetCeilingUsd: process.env.AIOX_MODEL_BUDGET_CEILING_USD,
149173
};
150174

151175
/**
@@ -176,10 +200,12 @@ function getScriptPath() {
176200
}
177201

178202
/**
179-
* Validates spawn arguments
180-
* @param {string} agent - Agent ID
181-
* @param {string} task - Task to execute
182-
* @throws {Error} If arguments are invalid
203+
* Validate the public agent and task identifiers before spawning a process.
204+
*
205+
* @param {string} agent - Agent ID beginning with a letter and containing only letters, digits, or hyphens.
206+
* @param {string} task - Task ID beginning with a letter and containing only letters, digits, or hyphens.
207+
* @returns {void}
208+
* @throws {Error} When either identifier is missing, non-string, or has an invalid format.
183209
*/
184210
function validateArgs(agent, task) {
185211
if (!agent || typeof agent !== 'string') {
@@ -308,6 +334,7 @@ function sleep(ms) {
308334
* @param {number} [options.timeout=300000] - Timeout in ms
309335
* @param {string} [options.outputDir] - Directory for output files
310336
* @param {boolean} [options.debug=false] - Enable debug logging
337+
* @param {number|string} [options.budgetCeilingUsd] - Required automated model budget ceiling
311338
* @returns {Promise<SpawnResult>} Result with output and status
312339
*/
313340
async function spawnInline(agent, task, options = {}) {
@@ -354,6 +381,7 @@ async function spawnInline(agent, task, options = {}) {
354381
AIOX_DEBUG: opts.debug ? 'true' : 'false',
355382
AIOX_OUTPUT_DIR: opts.outputDir,
356383
AIOX_INLINE_MODE: 'true', // Signal to pm.sh that we're running inline
384+
AIOX_MODEL_BUDGET_CEILING_USD: String(opts.budgetCeilingUsd || ''),
357385
},
358386
stdio: ['ignore', 'pipe', 'pipe'],
359387
});
@@ -479,6 +507,7 @@ async function spawnInline(agent, task, options = {}) {
479507
* @param {string} [options.outputDir] - Directory for output files
480508
* @param {number} [options.retries=3] - Number of retry attempts
481509
* @param {boolean} [options.debug=false] - Enable debug logging
510+
* @param {number|string} [options.budgetCeilingUsd] - Required automated model budget ceiling
482511
* @returns {Promise<SpawnResult>} Result with output and status
483512
*
484513
* @example
@@ -552,9 +581,10 @@ async function spawnAgent(agent, task, options = {}) {
552581
...process.env,
553582
AIOX_DEBUG: opts.debug ? 'true' : 'false',
554583
AIOX_OUTPUT_DIR: opts.outputDir,
584+
AIOX_MODEL_BUDGET_CEILING_USD: String(opts.budgetCeilingUsd || ''),
555585
};
556586

557-
const result = execSync(`bash "${scriptPath}" ${args.join(' ')}`, {
587+
const result = execFileSync('bash', [scriptPath, ...args], {
558588
encoding: 'utf8',
559589
timeout: opts.timeout,
560590
env,
@@ -1026,6 +1056,7 @@ module.exports = {
10261056
formatCompatibilityReport,
10271057

10281058
// Utilities
1059+
validateArgs,
10291060
isSpawnerAvailable,
10301061
getPlatform,
10311062
cleanupOldFiles,

0 commit comments

Comments
 (0)