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
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { listEngineRecords, hasTool, isCostAcceptable } from './capability-registry.mjs';
|
|
2
|
+
import { runPreflight } from './preflight.mjs';
|
|
3
|
+
|
|
4
|
+
const REQUIRED_TOOL_KEYS = ['shell', 'filesystemWrite', 'browser', 'vision', 'testExecution'];
|
|
5
|
+
|
|
6
|
+
function checkRequiredTool(engineRecord, key) {
|
|
7
|
+
if (key === 'vision') {
|
|
8
|
+
return engineRecord.capabilities && engineRecord.capabilities.vision === true;
|
|
9
|
+
}
|
|
10
|
+
return hasTool(engineRecord, key);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hardFilterReason(engineRecord, classification, { allowPaid, allowUnknownCost }) {
|
|
14
|
+
if (engineRecord.enabled !== true) {
|
|
15
|
+
return 'disabled';
|
|
16
|
+
}
|
|
17
|
+
if (engineRecord.health && engineRecord.health.available === false) {
|
|
18
|
+
return 'marked unavailable';
|
|
19
|
+
}
|
|
20
|
+
if (!isCostAcceptable(engineRecord, { allowPaid, allowUnknownCost })) {
|
|
21
|
+
return 'cost class "' + (engineRecord.cost ? engineRecord.cost.class : 'unknown') + '" not allowed (allowPaid=' + allowPaid + ')';
|
|
22
|
+
}
|
|
23
|
+
const requires = classification.requires || {};
|
|
24
|
+
for (const key of REQUIRED_TOOL_KEYS) {
|
|
25
|
+
if (requires[key] === true && !checkRequiredTool(engineRecord, key)) {
|
|
26
|
+
return 'missing required tool: ' + key;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const requiredContext = requires.minimumContextTokens;
|
|
30
|
+
const maxContext = engineRecord.limits ? engineRecord.limits.maxContextTokens : null;
|
|
31
|
+
if (typeof requiredContext === 'number' && typeof maxContext === 'number' && maxContext < requiredContext) {
|
|
32
|
+
return 'insufficient context window (' + maxContext + ' < ' + requiredContext + ' required)';
|
|
33
|
+
}
|
|
34
|
+
const requestsPerDay = engineRecord.limits ? engineRecord.limits.requestsPerDay : null;
|
|
35
|
+
if (requestsPerDay === 0) {
|
|
36
|
+
return 'daily quota exhausted (requestsPerDay=0)';
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findHistoryFor(history, engineId) {
|
|
42
|
+
return history.filter(entry => entry.engine === engineId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function successRateFromHistory(entries) {
|
|
46
|
+
if (!entries.length) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const successes = entries.filter(entry => entry.success === true).length;
|
|
50
|
+
return successes / entries.length;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function scoreEngine(engineRecord, classification, { history = [], preferFree = true } = {}) {
|
|
54
|
+
const reasons = [];
|
|
55
|
+
let score = 0;
|
|
56
|
+
|
|
57
|
+
const costClass = engineRecord.cost ? engineRecord.cost.class : 'unknown';
|
|
58
|
+
if (costClass === 'free' || costClass === 'local') {
|
|
59
|
+
score += preferFree ? 15 : 8;
|
|
60
|
+
reasons.push('cost class "' + costClass + '"' + (preferFree ? ' (preferred)' : ''));
|
|
61
|
+
} else if (costClass === 'free-tier') {
|
|
62
|
+
score += preferFree ? 10 : 6;
|
|
63
|
+
reasons.push('cost class "free-tier"');
|
|
64
|
+
} else if (costClass === 'paid') {
|
|
65
|
+
score += 0;
|
|
66
|
+
reasons.push('cost class "paid"');
|
|
67
|
+
} else {
|
|
68
|
+
score += 3;
|
|
69
|
+
reasons.push('cost class "unknown" (small neutral credit, not preferred over confirmed-free)');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const caps = engineRecord.capabilities || {};
|
|
73
|
+
const requires = classification.requires || {};
|
|
74
|
+
for (const key of ['reasoning', 'coding']) {
|
|
75
|
+
const have = caps[key];
|
|
76
|
+
const need = requires[key];
|
|
77
|
+
if (typeof have === 'number' && typeof need === 'number') {
|
|
78
|
+
if (have >= need) {
|
|
79
|
+
score += 8 + (have - need) * 2;
|
|
80
|
+
reasons.push(key + ' capability ' + have + ' meets requirement ' + need);
|
|
81
|
+
} else {
|
|
82
|
+
score -= 10;
|
|
83
|
+
reasons.push(key + ' capability ' + have + ' below requirement ' + need);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Prefer a smaller/cheaper engine for trivial tasks, and a stronger one
|
|
89
|
+
// for architecture/critical work, but only when capability is confirmed
|
|
90
|
+
// or probable enough to have a numeric score at all — never invent one.
|
|
91
|
+
if (classification.complexity <= 2 && typeof caps.coding === 'number' && caps.coding >= 4) {
|
|
92
|
+
score -= 3;
|
|
93
|
+
reasons.push('deprioritized: strong engine on a low-complexity task');
|
|
94
|
+
}
|
|
95
|
+
if ((classification.taskKind === 'architecture' || classification.risk === 'critical') && typeof caps.reasoning === 'number') {
|
|
96
|
+
if (caps.reasoning >= 4) {
|
|
97
|
+
score += 6;
|
|
98
|
+
reasons.push('reasoning capability suits architecture/critical task');
|
|
99
|
+
} else {
|
|
100
|
+
score -= 6;
|
|
101
|
+
reasons.push('reasoning capability likely too low for architecture/critical task');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const entries = findHistoryFor(history, engineRecord.id);
|
|
106
|
+
const successRate = successRateFromHistory(entries);
|
|
107
|
+
if (successRate !== null) {
|
|
108
|
+
score += Math.round(successRate * 20);
|
|
109
|
+
reasons.push('recent success rate ' + Math.round(successRate * 100) + '% over ' + entries.length + ' runs');
|
|
110
|
+
} else if (engineRecord.health && typeof engineRecord.health.recentSuccessRate === 'number') {
|
|
111
|
+
score += Math.round(engineRecord.health.recentSuccessRate * 20);
|
|
112
|
+
reasons.push('recorded success rate ' + Math.round(engineRecord.health.recentSuccessRate * 100) + '%');
|
|
113
|
+
} else {
|
|
114
|
+
score += 5;
|
|
115
|
+
reasons.push('no history yet (neutral score)');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const consecutiveFailures = engineRecord.health ? (engineRecord.health.consecutiveFailures || 0) : 0;
|
|
119
|
+
if (consecutiveFailures > 0) {
|
|
120
|
+
score -= consecutiveFailures * 3;
|
|
121
|
+
reasons.push(consecutiveFailures + ' consecutive recent failures (penalty)');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (engineRecord.validated === true) {
|
|
125
|
+
score += 5;
|
|
126
|
+
reasons.push('engine is validated');
|
|
127
|
+
} else {
|
|
128
|
+
reasons.push('engine is not yet validated');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { score, reasons };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function selectEngine(classification, options = {}) {
|
|
135
|
+
const allowPaid = options.allowPaid === true;
|
|
136
|
+
const allowUnknownCost = options.allowUnknownCost !== false;
|
|
137
|
+
const preferFree = options.preferFree !== false;
|
|
138
|
+
const engines = options.engines || await listEngineRecords();
|
|
139
|
+
const history = options.history || [];
|
|
140
|
+
|
|
141
|
+
const candidates = [];
|
|
142
|
+
const alternatives = [];
|
|
143
|
+
|
|
144
|
+
for (const engineRecord of engines) {
|
|
145
|
+
const rejection = hardFilterReason(engineRecord, classification, { allowPaid, allowUnknownCost });
|
|
146
|
+
if (rejection) {
|
|
147
|
+
alternatives.push({ engine: engineRecord.id, rejectedBecause: rejection });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const { score, reasons } = scoreEngine(engineRecord, classification, { history, preferFree });
|
|
151
|
+
candidates.push({ engine: engineRecord, score, reasons });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (candidates.length === 0) {
|
|
155
|
+
return {
|
|
156
|
+
selectedEngine: null,
|
|
157
|
+
score: null,
|
|
158
|
+
reason: ['No enabled engine in .maestro/engines.json satisfies this task\'s hard requirements.'],
|
|
159
|
+
alternatives
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
164
|
+
const winner = candidates[0];
|
|
165
|
+
const runnerUps = candidates.slice(1).map(candidate => ({
|
|
166
|
+
engine: candidate.engine.id,
|
|
167
|
+
rejectedBecause: 'lower score (' + candidate.score + ' < ' + winner.score + ')'
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
selectedEngine: winner.engine.id,
|
|
172
|
+
score: winner.score,
|
|
173
|
+
reason: winner.reasons,
|
|
174
|
+
alternatives: [...alternatives, ...runnerUps]
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function selectEngineWithPreflight(task, classification, options = {}) {
|
|
179
|
+
const selection = await selectEngine(classification, options);
|
|
180
|
+
if (!selection.selectedEngine) {
|
|
181
|
+
return { selection, preflight: null };
|
|
182
|
+
}
|
|
183
|
+
const engines = options.engines || await listEngineRecords();
|
|
184
|
+
const engineRecord = engines.find(engine => engine.id === selection.selectedEngine);
|
|
185
|
+
const preflight = runPreflight(task, engineRecord, classification, options);
|
|
186
|
+
return { selection, preflight };
|
|
187
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/orchestration/execution-context.mjs
|
|
2
|
+
|
|
3
|
+
export function buildExecutionContext(input) {
|
|
4
|
+
const context = {
|
|
5
|
+
projectRoot: input.projectRoot || '',
|
|
6
|
+
task: input.task || {},
|
|
7
|
+
originalRequest: input.originalRequest || '',
|
|
8
|
+
taskClassification: input.taskClassification || {},
|
|
9
|
+
selectedManager: input.selectedManager || {},
|
|
10
|
+
selectedEngine: input.selectedEngine || {},
|
|
11
|
+
runtimeGate: input.runtimeGate || {},
|
|
12
|
+
budget: input.budget || {},
|
|
13
|
+
policy: input.policy || {},
|
|
14
|
+
attemptNumber: input.attemptNumber || 1,
|
|
15
|
+
maxAttempts: input.maxAttempts || 3,
|
|
16
|
+
parentRunId: input.parentRunId || null,
|
|
17
|
+
runKind: input.runKind || 'task',
|
|
18
|
+
orchestrationDepth: input.orchestrationDepth || 0,
|
|
19
|
+
previousFailures: input.previousFailures || [],
|
|
20
|
+
constraints: input.constraints || {},
|
|
21
|
+
allowedActions: input.allowedActions || [],
|
|
22
|
+
forbiddenActions: input.forbiddenActions || []
|
|
23
|
+
};
|
|
24
|
+
return context;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function summarizeExecutionContext(context) {
|
|
28
|
+
const summary = {
|
|
29
|
+
taskTitle: context.task ? context.task.title : 'No Task',
|
|
30
|
+
runKind: context.runKind,
|
|
31
|
+
attemptNumber: context.attemptNumber,
|
|
32
|
+
orchestrationDepth: context.orchestrationDepth,
|
|
33
|
+
// Add other non-sensitive summary fields
|
|
34
|
+
};
|
|
35
|
+
return summary;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateExecutionContext(context) {
|
|
39
|
+
// console.log('Validating context:', JSON.stringify(context, null, 2)); // Debug log
|
|
40
|
+
if (!context || !context.task || !context.task.id) {
|
|
41
|
+
// console.log('Throwing error due to missing task.id'); // Debug log
|
|
42
|
+
throw new Error('Validation Error: Execution context must contain a valid task.');
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// src/orchestration/failure-classifier.mjs
|
|
2
|
+
|
|
3
|
+
const NESTED_SANDBOX_PROCESS_FAILURE_PATTERNS = [
|
|
4
|
+
/spawn\s+EPERM/i,
|
|
5
|
+
/operation not permitted/i,
|
|
6
|
+
/CreateProcess failed/i,
|
|
7
|
+
/nested sandbox/i,
|
|
8
|
+
/child process blocked/i,
|
|
9
|
+
/npm(?:\.cmd)?\s+[^\r\n]*\b(blocked|EPERM|operation not permitted|CreateProcess failed)/i,
|
|
10
|
+
/subprocess(?:o)?\s+(?:blocked|impedido|bloqueado)/i,
|
|
11
|
+
/process creation\s+(?:blocked|denied|not permitted)/i,
|
|
12
|
+
/sandbox[^\r\n]*(?:blocked|denied|not permitted)[^\r\n]*(?:child process|subprocess|process creation|npm(?:\.cmd)?)/i
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export function isNestedSandboxProcessFailure(input = '') {
|
|
16
|
+
const text = typeof input === 'string'
|
|
17
|
+
? input
|
|
18
|
+
: [input.stdout, input.stderr, input.message, input.error && input.error.message]
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.join('\n');
|
|
21
|
+
return NESTED_SANDBOX_PROCESS_FAILURE_PATTERNS.some(pattern => pattern.test(String(text || '')));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function nestedSandboxFailure(reasonText) {
|
|
25
|
+
return {
|
|
26
|
+
category: 'nested_sandbox_process_failure',
|
|
27
|
+
severity: 'high',
|
|
28
|
+
retryable: false,
|
|
29
|
+
shouldFallback: true,
|
|
30
|
+
shouldAskHuman: false,
|
|
31
|
+
infrastructureFailure: true,
|
|
32
|
+
reason: ['Sandbox blocked subprocess creation for worker validation.'],
|
|
33
|
+
evidence: [reasonText].filter(Boolean),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function classifyFailure(input = {}) {
|
|
38
|
+
const { exitCode, stdout, stderr, runtimeGate, syntaxValidation, enginePolicyViolation, taskStatus, result } = input;
|
|
39
|
+
let category = 'unknown_failure';
|
|
40
|
+
let severity = 'medium';
|
|
41
|
+
let retryable = true;
|
|
42
|
+
let shouldFallback = true;
|
|
43
|
+
let shouldAskHuman = false;
|
|
44
|
+
let infrastructureFailure = false;
|
|
45
|
+
const reason = [];
|
|
46
|
+
const evidence = [];
|
|
47
|
+
|
|
48
|
+
const fullOutput = [stdout, stderr, result && result.stdout, result && result.stderr, result && result.message]
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join(' ');
|
|
51
|
+
|
|
52
|
+
if (isNestedSandboxProcessFailure(fullOutput)) {
|
|
53
|
+
return nestedSandboxFailure(fullOutput);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const checkMessage = (message, keywords, assignedCategory, assignedSeverity = 'medium', assignedRetryable = true, assignedShouldFallback = true, assignedShouldAskHuman = false, assignedInfrastructureFailure = false) => {
|
|
57
|
+
for (const keyword of keywords) {
|
|
58
|
+
if (message.includes(keyword)) {
|
|
59
|
+
category = assignedCategory;
|
|
60
|
+
severity = assignedSeverity;
|
|
61
|
+
retryable = assignedRetryable;
|
|
62
|
+
shouldFallback = assignedShouldFallback;
|
|
63
|
+
shouldAskHuman = assignedShouldAskHuman;
|
|
64
|
+
infrastructureFailure = assignedInfrastructureFailure;
|
|
65
|
+
reason.push(`Keyword "${keyword}" found in message.`);
|
|
66
|
+
evidence.push(message);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (checkMessage(fullOutput, ['apply deny-read ACLs', 'apply deny-read acls', 'helper_unknown_error'], 'windows_sandbox_acl_failure', 'critical', false, true, true, true)) {}
|
|
74
|
+
else if (checkMessage(fullOutput, ['rate limit', '429'], 'rate_limit', 'high', true, true, false)) {}
|
|
75
|
+
else if (checkMessage(fullOutput, ['quota', 'insufficient quota'], 'quota_exceeded', 'high', true, true, false)) {}
|
|
76
|
+
else if (checkMessage(fullOutput, ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'], 'network_error', 'medium', true, true, false)) {}
|
|
77
|
+
else if (checkMessage(fullOutput, ['401', '403', 'auth', 'unauthorized'], 'authentication_error', 'critical', false, false, true)) {}
|
|
78
|
+
else if (checkMessage(fullOutput, ['missing tool', 'tool unavailable', 'no shell', 'filesystemWrite false'], 'missing_tool', 'high', false, true, true)) {}
|
|
79
|
+
else if (checkMessage(fullOutput, ['Unexpected token', 'SyntaxError', 'node --check failed'], 'syntax_failure', 'medium', false, true, false)) {}
|
|
80
|
+
else if (checkMessage(fullOutput, ['test failed', 'npm test failed', 'failing tests'], 'test_failure', 'medium', true, true, false)) {}
|
|
81
|
+
else if (checkMessage(fullOutput, ['policy violation', 'enginePolicyViolation', 'cannot access external network'], 'policy_violation', 'critical', false, false, true)) {}
|
|
82
|
+
else if (checkMessage(fullOutput, ['budget exceeded', 'allowPaid false'], 'budget_exceeded', 'high', false, false, true)) {}
|
|
83
|
+
else if (checkMessage(fullOutput, ['needs_human', 'NEEDS_HUMAN'], 'needs_human', 'critical', false, false, true)) {}
|
|
84
|
+
else if (checkMessage(fullOutput, ['ls -la', 'tail -50', 'cat << EOF', 'grep', 'sed -i'], 'windows_shell_mismatch', 'high', false, true, true) && process.platform === 'win32') {}
|
|
85
|
+
else if (checkMessage(fullOutput, ['unsupported tool', 'apply_patch unavailable'], 'unsupported_tool_call', 'medium', false, true, false)) {}
|
|
86
|
+
else if (checkMessage(fullOutput, ['suspected_false_positive'], 'suspected_false_positive', 'low', true, true, false)) {}
|
|
87
|
+
else if (runtimeGate && runtimeGate.status === 'NEEDS_HUMAN') {
|
|
88
|
+
category = 'needs_human';
|
|
89
|
+
severity = 'critical';
|
|
90
|
+
shouldAskHuman = true;
|
|
91
|
+
retryable = false;
|
|
92
|
+
shouldFallback = false;
|
|
93
|
+
reason.push('Runtime gate explicitly set to NEEDS_HUMAN.');
|
|
94
|
+
evidence.push(runtimeGate);
|
|
95
|
+
}
|
|
96
|
+
else if (enginePolicyViolation) {
|
|
97
|
+
category = 'policy_violation';
|
|
98
|
+
severity = 'critical';
|
|
99
|
+
shouldAskHuman = true;
|
|
100
|
+
retryable = false;
|
|
101
|
+
shouldFallback = false;
|
|
102
|
+
reason.push('Engine policy violation detected.');
|
|
103
|
+
evidence.push(enginePolicyViolation);
|
|
104
|
+
}
|
|
105
|
+
else if (syntaxValidation && !syntaxValidation.isValid) {
|
|
106
|
+
category = 'syntax_failure';
|
|
107
|
+
severity = 'medium';
|
|
108
|
+
retryable = false;
|
|
109
|
+
shouldFallback = true;
|
|
110
|
+
reason.push('Syntax validation failed.');
|
|
111
|
+
evidence.push(syntaxValidation);
|
|
112
|
+
}
|
|
113
|
+
else if (taskStatus && taskStatus === 'failed') {
|
|
114
|
+
category = 'unknown_failure';
|
|
115
|
+
severity = 'medium';
|
|
116
|
+
retryable = true;
|
|
117
|
+
shouldFallback = true;
|
|
118
|
+
reason.push('Task reported as failed without specific classification.');
|
|
119
|
+
evidence.push(taskStatus);
|
|
120
|
+
if (exitCode === 0) {
|
|
121
|
+
retryable = false;
|
|
122
|
+
severity = 'high';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
category,
|
|
128
|
+
severity,
|
|
129
|
+
retryable,
|
|
130
|
+
shouldFallback,
|
|
131
|
+
shouldAskHuman,
|
|
132
|
+
infrastructureFailure,
|
|
133
|
+
reason,
|
|
134
|
+
evidence,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { redactCredentials, redactObject } from '../format.mjs';
|
|
2
|
+
|
|
3
|
+
export const FAILURE_CLASSES = new Set([
|
|
4
|
+
'infrastructure',
|
|
5
|
+
'authentication',
|
|
6
|
+
'quota',
|
|
7
|
+
'rate_limit',
|
|
8
|
+
'timeout',
|
|
9
|
+
'network',
|
|
10
|
+
'stream_interruption',
|
|
11
|
+
'provider_unavailable',
|
|
12
|
+
'model_unavailable',
|
|
13
|
+
'engine_resolution',
|
|
14
|
+
'worker_failure',
|
|
15
|
+
'deliverable_rejection',
|
|
16
|
+
'safety_block',
|
|
17
|
+
'human_decision',
|
|
18
|
+
'unknown'
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export const HEALTH_AFFECTING_FAILURE_CLASSES = new Set([
|
|
22
|
+
'infrastructure',
|
|
23
|
+
'authentication',
|
|
24
|
+
'quota',
|
|
25
|
+
'rate_limit',
|
|
26
|
+
'timeout',
|
|
27
|
+
'network',
|
|
28
|
+
'stream_interruption',
|
|
29
|
+
'provider_unavailable',
|
|
30
|
+
'model_unavailable'
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export const RETRYABLE_FAILURE_CLASSES = new Set([
|
|
34
|
+
'infrastructure',
|
|
35
|
+
'rate_limit',
|
|
36
|
+
'timeout',
|
|
37
|
+
'network',
|
|
38
|
+
'stream_interruption',
|
|
39
|
+
'provider_unavailable'
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
export const LONG_COOLDOWN_FAILURE_CLASSES = new Set(['authentication', 'quota']);
|
|
43
|
+
|
|
44
|
+
export const EVIDENCE_SUMMARY_LIMIT = 2000;
|
|
45
|
+
export const RETRY_AFTER_SAFETY_CEILING_MS = 86400000;
|
|
46
|
+
|
|
47
|
+
const CONTENT_PATTERNS = [
|
|
48
|
+
{ failureClass: 'authentication', retryable: false, patterns: [/\b401\b/i, /\b403\b/i, /unauthori[sz]ed/i, /invalid api key/i, /authentication failed/i, /\bapi[_\s-]?key\b.*(?:invalid|missing|expired)/i] },
|
|
49
|
+
{ failureClass: 'quota', retryable: false, patterns: [/quota/i, /insufficient[_\s-]?(?:credit|balance)/i, /out of credits/i, /billing limit/i] },
|
|
50
|
+
{ failureClass: 'rate_limit', retryable: true, patterns: [/rate[\s-]?limit/i, /too many requests/i, /\b429\b/i] },
|
|
51
|
+
{ failureClass: 'stream_interruption', retryable: true, patterns: [/Reconnecting\.\.\.\s*\d+\s*\/\s*\d+/i, /stream disconnected before completion/i] },
|
|
52
|
+
{ failureClass: 'network', retryable: true, patterns: [/\bECONNRESET\b/i, /\bECONNREFUSED\b/i, /\bENOTFOUND\b/i, /\bETIMEDOUT\b/i, /\bEAI_AGAIN\b/i, /network error/i, /fetch failed/i, /connection (?:reset|refused|timed out)/i] },
|
|
53
|
+
{ failureClass: 'provider_unavailable', retryable: true, patterns: [/\b5\d\d\b/i, /service unavailable/i, /bad gateway/i, /gateway timeout/i, /provider (?:is )?(?:down|unavailable)/i] },
|
|
54
|
+
{ failureClass: 'model_unavailable', retryable: false, patterns: [/model (?:not found|unavailable|does not exist)/i, /unsupported model/i, /model .* is not available/i] },
|
|
55
|
+
{ failureClass: 'safety_block', retryable: false, patterns: [/safety (?:guard|policy|block)/i, /policy violation/i, /blocked by safety/i] },
|
|
56
|
+
{ failureClass: 'deliverable_rejection', retryable: false, patterns: [/deliverable (?:rejected|verification failed)/i, /verification outcome: rejected/i, /acceptance criteria failed/i] },
|
|
57
|
+
{ failureClass: 'human_decision', retryable: false, patterns: [/needs[_\s-]?human/i, /human decision required/i, /requires human/i] },
|
|
58
|
+
{ failureClass: 'worker_failure', retryable: false, patterns: [/worker failed/i, /invalid output/i, /malformed (?:json|output)/i, /unsupported tool call/i] }
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
function asString(value) {
|
|
62
|
+
return typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isPlainObject(value) {
|
|
66
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function escapeRegExp(value) {
|
|
70
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function uniqueNonEmptyStrings(values) {
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const result = [];
|
|
76
|
+
for (const value of values.flat(Infinity)) {
|
|
77
|
+
const text = asString(value);
|
|
78
|
+
if (!text || seen.has(text)) continue;
|
|
79
|
+
seen.add(text);
|
|
80
|
+
result.push(text);
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function normalizeFailureEvidence(input = {}) {
|
|
86
|
+
const acceptanceCriteria = Array.isArray(input.acceptanceCriteria)
|
|
87
|
+
? input.acceptanceCriteria
|
|
88
|
+
: Array.isArray(input.acceptance)
|
|
89
|
+
? input.acceptance
|
|
90
|
+
: [];
|
|
91
|
+
const commandEchoed = input.commandEchoed ?? input.echoedCommand ?? '';
|
|
92
|
+
return {
|
|
93
|
+
source: asString(input.source || 'unknown'),
|
|
94
|
+
command: asString(input.command || ''),
|
|
95
|
+
prompt: asString(input.prompt || ''),
|
|
96
|
+
taskDescription: asString(input.taskDescription || input.description || ''),
|
|
97
|
+
acceptanceCriteria: acceptanceCriteria.map(asString),
|
|
98
|
+
commandEchoed: asString(commandEchoed),
|
|
99
|
+
knownInputs: uniqueNonEmptyStrings([
|
|
100
|
+
input.knownInputs || [],
|
|
101
|
+
input.prompt || '',
|
|
102
|
+
input.taskDescription || input.description || '',
|
|
103
|
+
acceptanceCriteria,
|
|
104
|
+
commandEchoed
|
|
105
|
+
]),
|
|
106
|
+
exitCode: typeof input.exitCode === 'number' ? input.exitCode : input.exitCode === null ? null : typeof input.code === 'number' ? input.code : null,
|
|
107
|
+
signal: input.signal == null ? null : asString(input.signal),
|
|
108
|
+
timedOut: input.timedOut === true || input.timeout === true,
|
|
109
|
+
timeout: input.timeout === true || input.timedOut === true,
|
|
110
|
+
processError: input.processError == null ? null : asString(input.processError && input.processError.message ? input.processError.message : input.processError),
|
|
111
|
+
stdout: asString(input.stdout),
|
|
112
|
+
stderr: asString(input.stderr),
|
|
113
|
+
fullStdout: input.fullStdout == null ? null : asString(input.fullStdout),
|
|
114
|
+
fullStderr: input.fullStderr == null ? null : asString(input.fullStderr),
|
|
115
|
+
structuredResult: input.structuredResult == null ? null : input.structuredResult,
|
|
116
|
+
retryAfterMs: input.retryAfterMs
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function stripKnownInputEcho(text, knownInputs = []) {
|
|
121
|
+
let cleaned = asString(text);
|
|
122
|
+
for (const input of uniqueNonEmptyStrings(knownInputs)) {
|
|
123
|
+
cleaned = cleaned.replace(new RegExp(escapeRegExp(input), 'g'), '');
|
|
124
|
+
}
|
|
125
|
+
return cleaned;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function validateRetryAfterMs(value) {
|
|
129
|
+
if (value === undefined || value === null) {
|
|
130
|
+
return { valid: false, value: null };
|
|
131
|
+
}
|
|
132
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
133
|
+
return { valid: false, value: null };
|
|
134
|
+
}
|
|
135
|
+
return { valid: true, value: Math.min(value, RETRY_AFTER_SAFETY_CEILING_MS) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function validateStructuredResult(value) {
|
|
139
|
+
if (!isPlainObject(value)) return { valid: false };
|
|
140
|
+
if (value.outcome !== 'success' && value.outcome !== 'failure') return { valid: false };
|
|
141
|
+
if (value.outcome === 'success') return { valid: true, outcome: 'success' };
|
|
142
|
+
const failureClass = value.failureClass || value.class || value.category;
|
|
143
|
+
if (!FAILURE_CLASSES.has(failureClass)) return { valid: false };
|
|
144
|
+
return { valid: true, outcome: 'failure', failureClass };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function retryableFor(failureClass) {
|
|
148
|
+
return RETRYABLE_FAILURE_CLASSES.has(failureClass);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function affectsHealth(failureClass) {
|
|
152
|
+
return HEALTH_AFFECTING_FAILURE_CLASSES.has(failureClass);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function summary(text) {
|
|
156
|
+
const redacted = redactCredentials(asString(text));
|
|
157
|
+
return redacted.length > EVIDENCE_SUMMARY_LIMIT ? redacted.slice(0, EVIDENCE_SUMMARY_LIMIT) : redacted;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function classification({ outcome = 'failure', failureClass = null, evidenceSource, evidenceSummary = '', reason }) {
|
|
161
|
+
const retryAfter = validateRetryAfterMs(classification.currentRetryAfterMs);
|
|
162
|
+
const result = {
|
|
163
|
+
outcome,
|
|
164
|
+
failureClass,
|
|
165
|
+
retryable: failureClass ? retryableFor(failureClass) : false,
|
|
166
|
+
affectsHealth: failureClass ? affectsHealth(failureClass) : false,
|
|
167
|
+
evidenceSource,
|
|
168
|
+
evidenceSummary: summary(evidenceSummary),
|
|
169
|
+
reason
|
|
170
|
+
};
|
|
171
|
+
if (retryAfter.valid) {
|
|
172
|
+
result.retryAfterMs = retryAfter.value;
|
|
173
|
+
}
|
|
174
|
+
return redactObject(result);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
classification.currentRetryAfterMs = null;
|
|
178
|
+
|
|
179
|
+
function matchContent(text) {
|
|
180
|
+
for (const entry of CONTENT_PATTERNS) {
|
|
181
|
+
for (const pattern of entry.patterns) {
|
|
182
|
+
const matched = asString(text).match(pattern);
|
|
183
|
+
if (matched) {
|
|
184
|
+
return { failureClass: entry.failureClass, matchedText: matched[0] };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function classifyFailureEvidence(input = {}) {
|
|
192
|
+
const evidence = normalizeFailureEvidence(input);
|
|
193
|
+
classification.currentRetryAfterMs = evidence.retryAfterMs;
|
|
194
|
+
try {
|
|
195
|
+
if (evidence.timedOut) {
|
|
196
|
+
return classification({ failureClass: 'timeout', evidenceSource: 'timedOut', evidenceSummary: 'timedOut=true', reason: 'process timed out' });
|
|
197
|
+
}
|
|
198
|
+
if (evidence.processError) {
|
|
199
|
+
return classification({ failureClass: 'engine_resolution', evidenceSource: 'processError', evidenceSummary: evidence.processError, reason: 'runner reported a process error' });
|
|
200
|
+
}
|
|
201
|
+
if (evidence.signal) {
|
|
202
|
+
return classification({ failureClass: 'infrastructure', evidenceSource: 'signal', evidenceSummary: evidence.signal, reason: 'process ended by signal' });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const structured = validateStructuredResult(evidence.structuredResult);
|
|
206
|
+
if (structured.valid) {
|
|
207
|
+
if (structured.outcome === 'success') {
|
|
208
|
+
return classification({ outcome: 'success', failureClass: null, evidenceSource: 'structuredResult', evidenceSummary: 'outcome=success', reason: 'worker returned structured success' });
|
|
209
|
+
}
|
|
210
|
+
return classification({ failureClass: structured.failureClass, evidenceSource: 'structuredResult', evidenceSummary: JSON.stringify(evidence.structuredResult), reason: 'worker returned structured failure' });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const sources = [
|
|
214
|
+
['stderr', evidence.stderr],
|
|
215
|
+
['stdout', evidence.stdout],
|
|
216
|
+
['fullStderr', evidence.fullStderr],
|
|
217
|
+
['fullStdout', evidence.fullStdout]
|
|
218
|
+
];
|
|
219
|
+
for (const [source, text] of sources) {
|
|
220
|
+
const cleaned = stripKnownInputEcho(text, evidence.knownInputs);
|
|
221
|
+
const matched = matchContent(cleaned);
|
|
222
|
+
if (matched) {
|
|
223
|
+
return classification({ failureClass: matched.failureClass, evidenceSource: source, evidenceSummary: cleaned, reason: 'matched reliable worker output pattern' });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (evidence.exitCode === 0) {
|
|
228
|
+
return classification({ outcome: 'success', failureClass: null, evidenceSource: 'exitCode', evidenceSummary: 'exitCode=0', reason: 'process exited successfully and no failure evidence matched' });
|
|
229
|
+
}
|
|
230
|
+
if (typeof evidence.exitCode === 'number' && evidence.exitCode !== 0) {
|
|
231
|
+
return classification({ failureClass: 'worker_failure', evidenceSource: 'exitCode', evidenceSummary: 'exitCode=' + evidence.exitCode, reason: 'non-zero exit without provider-health evidence' });
|
|
232
|
+
}
|
|
233
|
+
return classification({ failureClass: 'unknown', evidenceSource: 'none', evidenceSummary: '', reason: 'no reliable failure evidence matched' });
|
|
234
|
+
} finally {
|
|
235
|
+
classification.currentRetryAfterMs = null;
|
|
236
|
+
}
|
|
237
|
+
}
|