Skip to content

Commit a770860

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

8 files changed

Lines changed: 521 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: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
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+
const clamp01 = (value, fallback) => {
85+
const numericValue = Number(value);
86+
if (!Number.isFinite(numericValue)) {
87+
return fallback;
88+
}
89+
return Math.min(1, Math.max(0, numericValue));
90+
};
91+
const positiveInteger = (value, fallback) => {
92+
const numericValue = Number(value);
93+
if (!Number.isFinite(numericValue)) {
94+
return fallback;
95+
}
96+
return Math.max(1, Math.floor(numericValue));
97+
};
98+
99+
return {
100+
enabled: config.enabled !== false,
101+
minConfidence: clamp01(
102+
config.minConfidence ?? config.min_confidence,
103+
DEFAULT_FAST_PATH_CONFIG.minConfidence,
104+
),
105+
minBatchItems: positiveInteger(
106+
config.minBatchItems ?? config.min_batch_items,
107+
DEFAULT_FAST_PATH_CONFIG.minBatchItems,
108+
),
109+
externalExecutorThreshold: clamp01(
110+
config.externalExecutorThreshold ?? config.external_executor_threshold,
111+
DEFAULT_FAST_PATH_CONFIG.externalExecutorThreshold,
112+
),
113+
};
114+
}
115+
116+
function normalizeTask(input = {}) {
117+
const task = input.task || input;
118+
return {
119+
description: String(task.description || task.summary || task.title || ''),
120+
files: Array.isArray(task.files) ? task.files : [],
121+
acceptanceCriteria: Array.isArray(task.acceptanceCriteria)
122+
? task.acceptanceCriteria
123+
: Array.isArray(task.acceptance_criteria)
124+
? task.acceptance_criteria
125+
: [],
126+
itemCount: Number.isFinite(task.itemCount)
127+
? task.itemCount
128+
: Number.isFinite(task.item_count)
129+
? task.item_count
130+
: null,
131+
};
132+
}
133+
134+
function normalizePathExtension(filePath) {
135+
const match = String(filePath || '').toLowerCase().match(/(\.[a-z0-9]+)$/);
136+
return match ? match[1] : '';
137+
}
138+
139+
function collectSignals(patterns, text) {
140+
return patterns
141+
.filter(({ pattern }) => pattern.test(text))
142+
.map(({ id, weight }) => ({ id, weight }));
143+
}
144+
145+
function getTaskText(task) {
146+
return [
147+
task.description,
148+
...task.acceptanceCriteria.map((criterion) => String(criterion || '')),
149+
...task.files.map((file) => String(file || '')),
150+
].join('\n');
151+
}
152+
153+
function scoreFastPath({ automationSignals, riskSignals, files, structuredFileCount, batchSize }) {
154+
const automationWeight = automationSignals.reduce((sum, signal) => sum + signal.weight, 0);
155+
const riskWeight = riskSignals.reduce((sum, signal) => sum + signal.weight, 0);
156+
const fileWeight = Math.min(files.length, 8) * 0.45;
157+
const structuredWeight = Math.min(structuredFileCount, 6) * 0.55;
158+
const batchWeight = Math.min(batchSize, 10) * 0.35;
159+
160+
const rawScore = automationWeight + fileWeight + structuredWeight + batchWeight - riskWeight * 1.35;
161+
return Math.max(0, Math.min(1, rawScore / 13));
162+
}
163+
164+
function chooseMode({
165+
confidence,
166+
config,
167+
externalExecutorsEnabled,
168+
parallelizable,
169+
structuredFileCount,
170+
batchSize,
171+
}) {
172+
if (externalExecutorsEnabled && confidence >= config.externalExecutorThreshold) {
173+
return 'external_executor';
174+
}
175+
176+
if (parallelizable && batchSize >= config.minBatchItems) {
177+
return 'parallel_batch';
178+
}
179+
180+
if (structuredFileCount > 0 || batchSize >= config.minBatchItems) {
181+
return 'deterministic_batch';
182+
}
183+
184+
return 'standard';
185+
}
186+
187+
function buildActions(mode) {
188+
if (mode === 'external_executor') {
189+
return [
190+
'Prepare a bounded prompt with target files, schema, acceptance criteria, and validation commands.',
191+
'Run a dry-run plan first, then delegate with the configured external executor sandbox.',
192+
'Review the executor diff before mutating story or issue state.',
193+
];
194+
}
195+
196+
if (mode === 'parallel_batch') {
197+
return [
198+
'Map target files or records once before editing.',
199+
'Group independent changes by file or record and apply them as a batch.',
200+
'Run targeted validation on the changed surface before broader checks.',
201+
];
202+
}
203+
204+
if (mode === 'deterministic_batch') {
205+
return [
206+
'Extract the data shape and replacement rules before editing.',
207+
'Use a deterministic transform or structured parser instead of conversational one-by-one edits.',
208+
'Validate output syntax and diff size before continuing.',
209+
];
210+
}
211+
212+
return [
213+
'Use the standard story/task workflow.',
214+
'Keep changes sequential when risk or ambiguity is higher than the automation signal.',
215+
];
216+
}
217+
218+
function evaluateFastPath(input = {}) {
219+
const config = normalizeConfig(input.config || input.fastPath || {});
220+
const task = normalizeTask(input);
221+
const text = getTaskText(task);
222+
const automationSignals = collectSignals(AUTOMATION_PATTERNS, text);
223+
const riskSignals = collectSignals(RISK_PATTERNS, text);
224+
const structuredFileCount = task.files.filter((file) => (
225+
STRUCTURED_FILE_EXTENSIONS.has(normalizePathExtension(file))
226+
)).length;
227+
const batchSize = task.itemCount ?? Math.max(task.files.length, structuredFileCount);
228+
const parallelizable = (
229+
task.files.length >= config.minBatchItems ||
230+
automationSignals.some((signal) => signal.id === 'parallelizable')
231+
);
232+
233+
if (!config.enabled) {
234+
return {
235+
gate: 'fast_path',
236+
enabled: false,
237+
passed: false,
238+
mode: 'standard',
239+
confidence: 0,
240+
parallelizable: false,
241+
riskLevel: 'unknown',
242+
reasons: ['fast path gate disabled by configuration'],
243+
evidence: { automationSignals: [], riskSignals: [], fileCount: task.files.length, structuredFileCount, batchSize },
244+
actions: buildActions('standard'),
245+
};
246+
}
247+
248+
const confidence = scoreFastPath({
249+
automationSignals,
250+
riskSignals,
251+
files: task.files,
252+
structuredFileCount,
253+
batchSize,
254+
});
255+
const passed = confidence >= config.minConfidence && riskSignals.length === 0;
256+
const mode = passed
257+
? chooseMode({
258+
confidence,
259+
config,
260+
externalExecutorsEnabled: Boolean(input.externalExecutorsEnabled || input.external_executors_enabled),
261+
parallelizable,
262+
structuredFileCount,
263+
batchSize,
264+
})
265+
: 'standard';
266+
const riskLevel = riskSignals.length >= 2 ? 'high' : riskSignals.length === 1 ? 'medium' : 'low';
267+
const reasons = [
268+
...automationSignals.map((signal) => `automation signal: ${signal.id}`),
269+
...riskSignals.map((signal) => `risk signal: ${signal.id}`),
270+
];
271+
272+
if (structuredFileCount > 0) {
273+
reasons.push(`structured files detected: ${structuredFileCount}`);
274+
}
275+
if (batchSize >= config.minBatchItems) {
276+
reasons.push(`batch size meets threshold: ${batchSize}`);
277+
}
278+
if (!passed && reasons.length === 0) {
279+
reasons.push('insufficient automation signal for fast path');
280+
}
281+
282+
return {
283+
gate: 'fast_path',
284+
enabled: true,
285+
passed,
286+
mode,
287+
confidence,
288+
parallelizable: passed && parallelizable,
289+
riskLevel,
290+
reasons,
291+
evidence: {
292+
automationSignals,
293+
riskSignals,
294+
fileCount: task.files.length,
295+
structuredFileCount,
296+
batchSize,
297+
},
298+
actions: buildActions(mode),
299+
};
300+
}
301+
302+
module.exports = {
303+
DEFAULT_FAST_PATH_CONFIG,
304+
STRUCTURED_FILE_EXTENSIONS,
305+
AUTOMATION_PATTERNS,
306+
RISK_PATTERNS,
307+
evaluateFastPath,
308+
normalizeConfig,
309+
normalizeTask,
310+
};

.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.

0 commit comments

Comments
 (0)