Skip to content

Commit 2016354

Browse files
committed
feat(core): add fast path execution gate
1 parent ce322bf commit 2016354

8 files changed

Lines changed: 488 additions & 6 deletions

File tree

.aiox-core/core/config/schemas/framework-config.schema.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,34 @@
9797
"type": "boolean",
9898
"description": "Require orchestrator review before story state mutation",
9999
"default": true
100+
},
101+
"fast_path": {
102+
"type": "object",
103+
"description": "Fast-path gate defaults for mechanical, batchable, or parallelizable tasks",
104+
"properties": {
105+
"enabled": {
106+
"type": "boolean",
107+
"default": true
108+
},
109+
"min_confidence": {
110+
"type": "number",
111+
"minimum": 0,
112+
"maximum": 1,
113+
"default": 0.58
114+
},
115+
"min_batch_items": {
116+
"type": "integer",
117+
"minimum": 1,
118+
"default": 3
119+
},
120+
"external_executor_threshold": {
121+
"type": "number",
122+
"minimum": 0,
123+
"maximum": 1,
124+
"default": 0.78
125+
}
126+
},
127+
"additionalProperties": false
100128
}
101129
},
102130
"additionalProperties": false
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
'use strict';
2+
3+
const DEFAULT_FAST_PATH_CONFIG = Object.freeze({
4+
enabled: true,
5+
minConfidence: 0.58,
6+
minBatchItems: 3,
7+
externalExecutorThreshold: 0.78,
8+
});
9+
10+
const STRUCTURED_FILE_EXTENSIONS = new Set([
11+
'.csv',
12+
'.json',
13+
'.jsonl',
14+
'.md',
15+
'.toml',
16+
'.tsv',
17+
'.txt',
18+
'.yaml',
19+
'.yml',
20+
]);
21+
22+
const AUTOMATION_PATTERNS = Object.freeze([
23+
{
24+
id: 'bulk-edit',
25+
weight: 3,
26+
pattern: /\b(batch|bulk|many files|multiple files|all files|in one shot|one shot)\b/i,
27+
},
28+
{
29+
id: 'structured-transform',
30+
weight: 3,
31+
pattern: /\b(yaml|json|csv|markdown|frontmatter|schema|variable|variables|field|fields)\b/i,
32+
},
33+
{
34+
id: 'mechanical-edit',
35+
weight: 3,
36+
pattern: /\b(replace|rename|populate|fill|complete|update|convert|transform|normalize|format)\b/i,
37+
},
38+
{
39+
id: 'map-then-apply',
40+
weight: 2,
41+
pattern: /\b(map|extract|derive|template|codemod|script)\b/i,
42+
},
43+
{
44+
id: 'repetition',
45+
weight: 2,
46+
pattern: /\b(repeated|repetitive|same change|similar change|dumb task|tedious)\b/i,
47+
},
48+
{
49+
id: 'parallelizable',
50+
weight: 2,
51+
pattern: /\b(parallel|independent|per file|per item|per record)\b/i,
52+
},
53+
]);
54+
55+
const RISK_PATTERNS = Object.freeze([
56+
{
57+
id: 'architecture',
58+
weight: 3,
59+
pattern: /\b(architecture|architectural|design decision|adr|contract)\b/i,
60+
},
61+
{
62+
id: 'security',
63+
weight: 3,
64+
pattern: /\b(security|secret|token|credential|auth|permission|pii|rls)\b/i,
65+
},
66+
{
67+
id: 'destructive',
68+
weight: 3,
69+
pattern: /\b(delete|remove data|drop|reset|rewrite history|destructive)\b/i,
70+
},
71+
{
72+
id: 'production',
73+
weight: 2,
74+
pattern: /\b(production|prod|release|billing|payment|customer)\b/i,
75+
},
76+
{
77+
id: 'migration',
78+
weight: 2,
79+
pattern: /\b(migration|migrate|schema change|breaking change)\b/i,
80+
},
81+
]);
82+
83+
function normalizeConfig(config = {}) {
84+
return {
85+
enabled: config.enabled !== false,
86+
minConfidence: Number.isFinite(config.minConfidence)
87+
? config.minConfidence
88+
: Number.isFinite(config.min_confidence)
89+
? config.min_confidence
90+
: DEFAULT_FAST_PATH_CONFIG.minConfidence,
91+
minBatchItems: Number.isFinite(config.minBatchItems)
92+
? config.minBatchItems
93+
: Number.isFinite(config.min_batch_items)
94+
? config.min_batch_items
95+
: DEFAULT_FAST_PATH_CONFIG.minBatchItems,
96+
externalExecutorThreshold: Number.isFinite(config.externalExecutorThreshold)
97+
? config.externalExecutorThreshold
98+
: Number.isFinite(config.external_executor_threshold)
99+
? config.external_executor_threshold
100+
: DEFAULT_FAST_PATH_CONFIG.externalExecutorThreshold,
101+
};
102+
}
103+
104+
function normalizeTask(input = {}) {
105+
const task = input.task || input;
106+
return {
107+
description: String(task.description || task.summary || task.title || ''),
108+
files: Array.isArray(task.files) ? task.files : [],
109+
acceptanceCriteria: Array.isArray(task.acceptanceCriteria)
110+
? task.acceptanceCriteria
111+
: Array.isArray(task.acceptance_criteria)
112+
? task.acceptance_criteria
113+
: [],
114+
itemCount: Number.isFinite(task.itemCount)
115+
? task.itemCount
116+
: Number.isFinite(task.item_count)
117+
? task.item_count
118+
: null,
119+
};
120+
}
121+
122+
function normalizePathExtension(filePath) {
123+
const match = String(filePath || '').toLowerCase().match(/(\.[a-z0-9]+)$/);
124+
return match ? match[1] : '';
125+
}
126+
127+
function collectSignals(patterns, text) {
128+
return patterns
129+
.filter(({ pattern }) => pattern.test(text))
130+
.map(({ id, weight }) => ({ id, weight }));
131+
}
132+
133+
function getTaskText(task) {
134+
return [
135+
task.description,
136+
...task.acceptanceCriteria.map((criterion) => String(criterion || '')),
137+
...task.files.map((file) => String(file || '')),
138+
].join('\n');
139+
}
140+
141+
function scoreFastPath({ automationSignals, riskSignals, files, structuredFileCount, batchSize }) {
142+
const automationWeight = automationSignals.reduce((sum, signal) => sum + signal.weight, 0);
143+
const riskWeight = riskSignals.reduce((sum, signal) => sum + signal.weight, 0);
144+
const fileWeight = Math.min(files.length, 8) * 0.45;
145+
const structuredWeight = Math.min(structuredFileCount, 6) * 0.55;
146+
const batchWeight = Math.min(batchSize, 10) * 0.35;
147+
148+
const rawScore = automationWeight + fileWeight + structuredWeight + batchWeight - riskWeight * 1.35;
149+
return Math.max(0, Math.min(1, rawScore / 13));
150+
}
151+
152+
function chooseMode({
153+
confidence,
154+
config,
155+
externalExecutorsEnabled,
156+
parallelizable,
157+
structuredFileCount,
158+
batchSize,
159+
}) {
160+
if (externalExecutorsEnabled && confidence >= config.externalExecutorThreshold) {
161+
return 'external_executor';
162+
}
163+
164+
if (parallelizable && batchSize >= config.minBatchItems) {
165+
return 'parallel_batch';
166+
}
167+
168+
if (structuredFileCount > 0 || batchSize >= config.minBatchItems) {
169+
return 'deterministic_batch';
170+
}
171+
172+
return 'standard';
173+
}
174+
175+
function buildActions(mode) {
176+
if (mode === 'external_executor') {
177+
return [
178+
'Prepare a bounded prompt with target files, schema, acceptance criteria, and validation commands.',
179+
'Run a dry-run plan first, then delegate with the configured external executor sandbox.',
180+
'Review the executor diff before mutating story or issue state.',
181+
];
182+
}
183+
184+
if (mode === 'parallel_batch') {
185+
return [
186+
'Map target files or records once before editing.',
187+
'Group independent changes by file or record and apply them as a batch.',
188+
'Run targeted validation on the changed surface before broader checks.',
189+
];
190+
}
191+
192+
if (mode === 'deterministic_batch') {
193+
return [
194+
'Extract the data shape and replacement rules before editing.',
195+
'Use a deterministic transform or structured parser instead of conversational one-by-one edits.',
196+
'Validate output syntax and diff size before continuing.',
197+
];
198+
}
199+
200+
return [
201+
'Use the standard story/task workflow.',
202+
'Keep changes sequential when risk or ambiguity is higher than the automation signal.',
203+
];
204+
}
205+
206+
function evaluateFastPath(input = {}) {
207+
const config = normalizeConfig(input.config || input.fastPath || {});
208+
const task = normalizeTask(input);
209+
const text = getTaskText(task);
210+
const automationSignals = collectSignals(AUTOMATION_PATTERNS, text);
211+
const riskSignals = collectSignals(RISK_PATTERNS, text);
212+
const structuredFileCount = task.files.filter((file) => (
213+
STRUCTURED_FILE_EXTENSIONS.has(normalizePathExtension(file))
214+
)).length;
215+
const batchSize = task.itemCount || Math.max(task.files.length, structuredFileCount);
216+
const parallelizable = (
217+
task.files.length >= config.minBatchItems ||
218+
automationSignals.some((signal) => signal.id === 'parallelizable')
219+
);
220+
221+
if (!config.enabled) {
222+
return {
223+
gate: 'fast_path',
224+
enabled: false,
225+
passed: false,
226+
mode: 'standard',
227+
confidence: 0,
228+
parallelizable: false,
229+
riskLevel: 'unknown',
230+
reasons: ['fast path gate disabled by configuration'],
231+
evidence: { automationSignals: [], riskSignals: [], fileCount: task.files.length, structuredFileCount, batchSize },
232+
actions: buildActions('standard'),
233+
};
234+
}
235+
236+
const confidence = scoreFastPath({
237+
automationSignals,
238+
riskSignals,
239+
files: task.files,
240+
structuredFileCount,
241+
batchSize,
242+
});
243+
const passed = confidence >= config.minConfidence && riskSignals.length === 0;
244+
const mode = passed
245+
? chooseMode({
246+
confidence,
247+
config,
248+
externalExecutorsEnabled: Boolean(input.externalExecutorsEnabled || input.external_executors_enabled),
249+
parallelizable,
250+
structuredFileCount,
251+
batchSize,
252+
})
253+
: 'standard';
254+
const riskLevel = riskSignals.length >= 2 ? 'high' : riskSignals.length === 1 ? 'medium' : 'low';
255+
const reasons = [
256+
...automationSignals.map((signal) => `automation signal: ${signal.id}`),
257+
...riskSignals.map((signal) => `risk signal: ${signal.id}`),
258+
];
259+
260+
if (structuredFileCount > 0) {
261+
reasons.push(`structured files detected: ${structuredFileCount}`);
262+
}
263+
if (batchSize >= config.minBatchItems) {
264+
reasons.push(`batch size meets threshold: ${batchSize}`);
265+
}
266+
if (!passed && reasons.length === 0) {
267+
reasons.push('insufficient automation signal for fast path');
268+
}
269+
270+
return {
271+
gate: 'fast_path',
272+
enabled: true,
273+
passed,
274+
mode,
275+
confidence,
276+
parallelizable: passed && parallelizable,
277+
riskLevel,
278+
reasons,
279+
evidence: {
280+
automationSignals,
281+
riskSignals,
282+
fileCount: task.files.length,
283+
structuredFileCount,
284+
batchSize,
285+
},
286+
actions: buildActions(mode),
287+
};
288+
}
289+
290+
module.exports = {
291+
DEFAULT_FAST_PATH_CONFIG,
292+
STRUCTURED_FILE_EXTENSIONS,
293+
AUTOMATION_PATTERNS,
294+
RISK_PATTERNS,
295+
evaluateFastPath,
296+
normalizeConfig,
297+
normalizeTask,
298+
};

.aiox-core/core/orchestration/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const TechStackDetector = require('./tech-stack-detector');
2323
const ConditionEvaluator = require('./condition-evaluator');
2424
const SkillDispatcher = require('./skill-dispatcher');
2525
const executionProfileResolver = require('./execution-profile-resolver');
26+
const fastPathGate = require('./fast-path-gate');
2627

2728
// Epic 0: Master Orchestrator (ADE)
2829
const MasterOrchestrator = require('./master-orchestrator');
@@ -162,6 +163,8 @@ module.exports = {
162163
SkillDispatcher,
163164
ExecutionProfileResolver: executionProfileResolver,
164165
resolveExecutionProfile: executionProfileResolver.resolveExecutionProfile,
166+
FastPathGate: fastPathGate,
167+
evaluateFastPath: fastPathGate.evaluateFastPath,
165168

166169
// Epic 0: Orchestrator constants
167170
OrchestratorState: MasterOrchestrator.OrchestratorState,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Fast Path Gate
2+
3+
## Purpose
4+
5+
Choose the fastest safe execution path before starting a task. This gate prevents slow one-by-one conversational edits for mechanical work such as YAML population, bulk replacements, structured data normalization, and repeated per-file updates.
6+
7+
## Inputs
8+
9+
- `description` — task summary or user request
10+
- `files` — known target files
11+
- `acceptanceCriteria` — expected outcomes
12+
- `itemCount` — optional number of records, fields, or files to process
13+
- `externalExecutorsEnabled` — whether delegation through `aiox-delegate` is allowed
14+
15+
## Execution
16+
17+
1. Evaluate the task with `evaluateFastPath()` from `.aiox-core/core/orchestration/fast-path-gate.js`.
18+
2. If the gate returns `mode: standard`, continue with the normal story/task workflow.
19+
3. If the gate returns `mode: deterministic_batch`, extract the schema and write one deterministic transform or structured edit plan before changing files.
20+
4. If the gate returns `mode: parallel_batch`, map all independent targets first, then apply grouped edits in parallel batches.
21+
5. If the gate returns `mode: external_executor`, create a bounded executor prompt and run `aiox-delegate` with the configured sandbox.
22+
6. Always review the resulting diff and run targeted validation before story or issue closure.
23+
24+
## Acceptance Criteria
25+
26+
- Mechanical repeated tasks are not executed as long sequential conversational edits.
27+
- Security, production, destructive, migration, and architectural tasks fall back to the standard workflow unless explicitly re-scoped.
28+
- Fast-path decisions include confidence, reasons, evidence, and next actions.
29+
- External executor usage remains opt-in and sandboxed by configuration.
30+
31+
## Anti-Patterns
32+
33+
- Do not use fast path for ambiguous architecture or security decisions.
34+
- Do not skip validation because a task is mechanical.
35+
- Do not delegate externally when the task contains secrets or production risk.
36+
- Do not mutate story or issue state until the diff has been reviewed.

.aiox-core/framework-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ dev:
5151
execution_mode: native # native | delegate
5252
delegate_to: codex
5353
auto_review: true
54+
fast_path:
55+
enabled: true
56+
min_confidence: 0.58
57+
min_batch_items: 3
58+
external_executor_threshold: 0.78
5459

5560
external_executors:
5661
enabled: false

0 commit comments

Comments
 (0)