chati-dev 3.2.5 → 3.3.1
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/LICENSE +96 -0
- package/bin/chati.js +46 -0
- package/framework/agents/build/dev.md +122 -1
- package/framework/agents/deploy/devops.md +128 -3
- package/framework/agents/discover/brief.md +77 -15
- package/framework/agents/discover/brownfield-wu.md +2 -2
- package/framework/agents/discover/greenfield-wu.md +3 -3
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +3 -3
- package/framework/agents/plan/phases.md +127 -2
- package/framework/agents/plan/tasks.md +127 -2
- package/framework/agents/plan/ux.md +269 -22
- package/framework/agents/quality/qa-implementation.md +172 -8
- package/framework/agents/quality/qa-planning.md +147 -2
- package/framework/config.yaml +9 -5
- package/framework/constitution.md +7 -1
- package/framework/context/quality.md +1 -1
- package/framework/context/root.md +1 -1
- package/framework/hooks/constitution-guard.js +18 -2
- package/framework/hooks/mode-governance.js +3 -3
- package/framework/hooks/read-protection.js +10 -2
- package/framework/i18n/en.yaml +6 -0
- package/framework/i18n/es.yaml +6 -0
- package/framework/i18n/fr.yaml +6 -0
- package/framework/i18n/pt.yaml +6 -0
- package/framework/orchestrator/chati.md +102 -6
- package/framework/schemas/task.schema.json +1 -1
- package/framework/tasks/architect-dep-audit.md +128 -0
- package/framework/tasks/architect-stack-selection.md +28 -0
- package/framework/workflows/brownfield-fullstack.yaml +2 -2
- package/framework/workflows/brownfield-service.yaml +2 -2
- package/framework/workflows/brownfield-ui.yaml +2 -2
- package/framework/workflows/greenfield-fullstack.yaml +6 -2
- package/framework/workflows/quick-flow.yaml +7 -5
- package/framework/workflows/standard-flow.yaml +171 -0
- package/package.json +4 -2
- package/src/api/index.js +129 -0
- package/src/autonomy/build-loop.js +93 -6
- package/src/autonomy/build-state.js +20 -2
- package/src/autonomy/cause-analyzer.js +177 -0
- package/src/autonomy/escalation.js +214 -0
- package/src/autonomy/safety-net.js +23 -5
- package/src/autonomy/worktree-manager.js +245 -0
- package/src/config/agent-customizer.js +227 -0
- package/src/config/ide-configs.js +57 -27
- package/src/decision/analyzer.js +148 -0
- package/src/decision/registry-healer.js +38 -21
- package/src/extensions/loader.js +151 -0
- package/src/extensions/registry.js +134 -0
- package/src/gates/circuit-breaker.js +32 -0
- package/src/gates/g3-implementation.js +30 -4
- package/src/gates/g4-qa-implementation.js +34 -5
- package/src/gates/gate-base.js +9 -0
- package/src/health/auto-fix.js +216 -0
- package/src/installer/core.js +24 -11
- package/src/installer/provider-overlay.js +82 -0
- package/src/installer/templates.js +22 -10
- package/src/installer/transaction.js +3 -2
- package/src/installer/validator.js +74 -0
- package/src/intelligence/context-status.js +9 -5
- package/src/intelligence/document-sharder.js +221 -0
- package/src/intelligence/elicitation.js +265 -0
- package/src/intelligence/timeline.js +5 -0
- package/src/memory/gotchas.js +78 -2
- package/src/merger/semantic-merger.js +292 -0
- package/src/orchestrator/agent-selector.js +20 -0
- package/src/orchestrator/handoff-engine.js +77 -0
- package/src/orchestrator/index.js +0 -8
- package/src/orchestrator/intent-classifier.js +182 -0
- package/src/orchestrator/pipeline-manager.js +125 -1
- package/src/orchestrator/session-manager.js +164 -2
- package/src/quality/metrics-collector.js +283 -0
- package/src/quality/test-runner.js +368 -0
- package/src/telemetry/collector.js +83 -0
- package/src/telemetry/config.js +119 -0
- package/src/telemetry/index.js +11 -0
- package/src/telemetry/schema.js +104 -0
- package/src/telemetry/sender.js +60 -0
- package/src/terminal/cli-registry.js +7 -1
- package/src/terminal/cost-tracker.js +197 -0
- package/src/terminal/handoff-parser.js +61 -4
- package/src/terminal/prompt-builder.js +56 -18
- package/src/terminal/rate-limiter.js +172 -0
- package/src/terminal/run-agent.js +39 -0
- package/src/terminal/run-parallel.js +22 -1
- package/src/terminal/spawner.js +181 -3
- package/src/upgrade/migrator.js +2 -2
- package/src/utils/event-bus.js +126 -0
- package/src/utils/file-lock.js +291 -0
- package/src/utils/schema-validator.js +226 -0
- package/src/wizard/i18n.js +11 -0
- package/src/wizard/index.js +42 -20
- package/src/wizard/questions.js +200 -39
- package/src/autonomy/execution-profile.js +0 -151
- package/src/intelligence/file-tracker.js +0 -117
- package/src/memory/gotchas-auto-capture.js +0 -253
- package/src/orchestrator/pipeline-state.js +0 -223
- package/src/terminal/wave-analyzer.js +0 -143
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
CheckpointStatus,
|
|
26
26
|
BuildStatus,
|
|
27
27
|
} from './build-state.js';
|
|
28
|
+
import { analyzeCause, buildRetryGuidance } from './cause-analyzer.js';
|
|
29
|
+
import { shouldEscalate, getEscalationConfig, buildEscalationSummary } from './escalation.js';
|
|
28
30
|
|
|
29
31
|
// ---------------------------------------------------------------------------
|
|
30
32
|
// Build Loop
|
|
@@ -34,9 +36,10 @@ import {
|
|
|
34
36
|
* @typedef {object} BuildLoopConfig
|
|
35
37
|
* @property {string} projectDir - Project root directory
|
|
36
38
|
* @property {string[]} taskIds - Task IDs to execute
|
|
37
|
-
* @property {function(string): Promise<{success: boolean, output: string}>} executor - Task execution function
|
|
39
|
+
* @property {function(string, object?): Promise<{success: boolean, output: string}>} executor - Task execution function (taskId, options?)
|
|
38
40
|
* @property {function(object): void} [onProgress] - Progress callback
|
|
39
41
|
* @property {boolean} [resume=false] - Whether to resume from existing state
|
|
42
|
+
* @property {string} [model='sonnet'] - Current model tier for escalation
|
|
40
43
|
*/
|
|
41
44
|
|
|
42
45
|
/**
|
|
@@ -64,7 +67,7 @@ import {
|
|
|
64
67
|
* @returns {Promise<BuildLoopResult>}
|
|
65
68
|
*/
|
|
66
69
|
export async function runBuildLoop(config) {
|
|
67
|
-
const { projectDir, taskIds, executor, onProgress, resume = false } = config;
|
|
70
|
+
const { projectDir, taskIds, executor, onProgress, resume = false, model = 'sonnet' } = config;
|
|
68
71
|
|
|
69
72
|
// Load or create state
|
|
70
73
|
let state = resume ? loadBuildState(projectDir) : null;
|
|
@@ -78,6 +81,10 @@ export async function runBuildLoop(config) {
|
|
|
78
81
|
|
|
79
82
|
const startTime = Date.now();
|
|
80
83
|
|
|
84
|
+
// Per-task attempt history for cause analysis
|
|
85
|
+
/** @type {Map<string, Array<{category: string, output: string}>>} */
|
|
86
|
+
const attemptHistory = new Map();
|
|
87
|
+
|
|
81
88
|
// Main loop
|
|
82
89
|
while (true) {
|
|
83
90
|
// Check global timeout
|
|
@@ -115,6 +122,54 @@ export async function runBuildLoop(config) {
|
|
|
115
122
|
continue;
|
|
116
123
|
}
|
|
117
124
|
|
|
125
|
+
// --- Escalation check (before execution) ---
|
|
126
|
+
const previousAttempts = attemptHistory.get(checkpoint.taskId) || [];
|
|
127
|
+
let executorOptions = {};
|
|
128
|
+
|
|
129
|
+
if (checkpoint.attempts > 0 && checkpoint.error) {
|
|
130
|
+
// Analyze the cause of the previous failure
|
|
131
|
+
const analysis = analyzeCause(checkpoint.error, previousAttempts);
|
|
132
|
+
const escalation = shouldEscalate(checkpoint, analysis);
|
|
133
|
+
|
|
134
|
+
if (escalation.escalate) {
|
|
135
|
+
const escalationConfig = getEscalationConfig(escalation.newLevel, model);
|
|
136
|
+
|
|
137
|
+
// Update checkpoint with escalation level
|
|
138
|
+
state = updateCheckpoint(state, checkpoint.taskId, {
|
|
139
|
+
escalationLevel: escalation.newLevel,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (onProgress) {
|
|
143
|
+
onProgress({
|
|
144
|
+
type: 'escalation',
|
|
145
|
+
taskId: checkpoint.taskId,
|
|
146
|
+
level: escalation.newLevel,
|
|
147
|
+
summary: buildEscalationSummary(escalation.newLevel, escalation.reason),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Pause for human intervention at MAX level
|
|
152
|
+
if (escalationConfig.shouldPause) {
|
|
153
|
+
state = updateCheckpoint(state, checkpoint.taskId, {
|
|
154
|
+
status: CheckpointStatus.FAILED,
|
|
155
|
+
error: `Escalation MAX — paused for human intervention: ${escalation.reason}`,
|
|
156
|
+
});
|
|
157
|
+
saveBuildState(projectDir, state);
|
|
158
|
+
|
|
159
|
+
if (onProgress) {
|
|
160
|
+
onProgress({ type: 'escalation_pause', taskId: checkpoint.taskId, reason: escalation.reason });
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Build executor options with escalation context
|
|
166
|
+
executorOptions.modelOverride = escalationConfig.model;
|
|
167
|
+
if (escalationConfig.contextBoost) {
|
|
168
|
+
executorOptions.retryGuidance = buildRetryGuidance(analysis, checkpoint.attempts + 1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
118
173
|
// Mark task as in progress
|
|
119
174
|
state = updateCheckpoint(state, checkpoint.taskId, {
|
|
120
175
|
status: CheckpointStatus.IN_PROGRESS,
|
|
@@ -130,32 +185,64 @@ export async function runBuildLoop(config) {
|
|
|
130
185
|
|
|
131
186
|
// Execute task
|
|
132
187
|
try {
|
|
133
|
-
const result = await executor(checkpoint.taskId);
|
|
188
|
+
const result = await executor(checkpoint.taskId, executorOptions);
|
|
134
189
|
|
|
135
190
|
if (result.success) {
|
|
136
191
|
state = updateCheckpoint(state, checkpoint.taskId, {
|
|
137
192
|
status: CheckpointStatus.COMPLETED,
|
|
138
193
|
output: result.output?.slice(0, 1000) || 'Completed',
|
|
139
194
|
error: null,
|
|
195
|
+
escalationLevel: undefined, // Reset on success
|
|
140
196
|
});
|
|
141
197
|
|
|
142
198
|
if (onProgress) {
|
|
143
199
|
onProgress({ type: 'task_completed', taskId: checkpoint.taskId });
|
|
144
200
|
}
|
|
145
201
|
} else {
|
|
202
|
+
// --- Self-critique: analyze failure cause ---
|
|
203
|
+
const failureOutput = result.output || 'Task failed';
|
|
204
|
+
const analysis = analyzeCause(failureOutput, previousAttempts);
|
|
205
|
+
|
|
206
|
+
// Record attempt in history
|
|
207
|
+
if (!attemptHistory.has(checkpoint.taskId)) {
|
|
208
|
+
attemptHistory.set(checkpoint.taskId, []);
|
|
209
|
+
}
|
|
210
|
+
attemptHistory.get(checkpoint.taskId).push({
|
|
211
|
+
category: analysis.category,
|
|
212
|
+
output: failureOutput.slice(0, 500),
|
|
213
|
+
});
|
|
214
|
+
|
|
146
215
|
state = updateCheckpoint(state, checkpoint.taskId, {
|
|
147
216
|
status: CheckpointStatus.IN_PROGRESS, // Will retry
|
|
148
|
-
error:
|
|
217
|
+
error: failureOutput.slice(0, 500),
|
|
149
218
|
});
|
|
150
219
|
|
|
151
220
|
if (onProgress) {
|
|
152
|
-
onProgress({
|
|
221
|
+
onProgress({
|
|
222
|
+
type: 'task_failed',
|
|
223
|
+
taskId: checkpoint.taskId,
|
|
224
|
+
attempt: checkpoint.attempts + 1,
|
|
225
|
+
error: failureOutput,
|
|
226
|
+
causeAnalysis: analysis,
|
|
227
|
+
});
|
|
153
228
|
}
|
|
154
229
|
}
|
|
155
230
|
} catch (err) {
|
|
231
|
+
const errorMsg = err.message?.slice(0, 500) || 'Execution error';
|
|
232
|
+
|
|
233
|
+
// Record exception in history for cause analysis
|
|
234
|
+
const analysis = analyzeCause(errorMsg, previousAttempts);
|
|
235
|
+
if (!attemptHistory.has(checkpoint.taskId)) {
|
|
236
|
+
attemptHistory.set(checkpoint.taskId, []);
|
|
237
|
+
}
|
|
238
|
+
attemptHistory.get(checkpoint.taskId).push({
|
|
239
|
+
category: analysis.category,
|
|
240
|
+
output: errorMsg,
|
|
241
|
+
});
|
|
242
|
+
|
|
156
243
|
state = updateCheckpoint(state, checkpoint.taskId, {
|
|
157
244
|
status: CheckpointStatus.IN_PROGRESS, // Will retry
|
|
158
|
-
error:
|
|
245
|
+
error: errorMsg,
|
|
159
246
|
});
|
|
160
247
|
}
|
|
161
248
|
|
|
@@ -18,6 +18,9 @@ import { join, dirname } from 'path';
|
|
|
18
18
|
/** Maximum iterations per individual task before giving up */
|
|
19
19
|
export const MAX_ITERATIONS_PER_TASK = 10;
|
|
20
20
|
|
|
21
|
+
/** Maximum time per individual task before giving up (10 minutes) */
|
|
22
|
+
export const MAX_TIME_PER_TASK_MS = 10 * 60 * 1000;
|
|
23
|
+
|
|
21
24
|
/** Global timeout for an autonomous build session (30 minutes) */
|
|
22
25
|
export const GLOBAL_TIMEOUT_MS = 30 * 60 * 1000;
|
|
23
26
|
|
|
@@ -58,6 +61,7 @@ export const CheckpointStatus = {
|
|
|
58
61
|
* @property {string} taskId
|
|
59
62
|
* @property {string} status - CheckpointStatus
|
|
60
63
|
* @property {number} attempts - Number of execution attempts
|
|
64
|
+
* @property {string|null} firstAttempt - ISO timestamp of first attempt
|
|
61
65
|
* @property {string|null} lastAttempt - ISO timestamp of last attempt
|
|
62
66
|
* @property {string|null} output - Last output summary
|
|
63
67
|
* @property {string|null} error - Last error message (if failed)
|
|
@@ -94,6 +98,7 @@ export function createBuildState(taskIds) {
|
|
|
94
98
|
taskId,
|
|
95
99
|
status: CheckpointStatus.PENDING,
|
|
96
100
|
attempts: 0,
|
|
101
|
+
firstAttempt: null,
|
|
97
102
|
lastAttempt: null,
|
|
98
103
|
output: null,
|
|
99
104
|
error: null,
|
|
@@ -157,6 +162,11 @@ export function updateCheckpoint(state, taskId, update) {
|
|
|
157
162
|
throw new Error(`Task "${taskId}" not found in build state`);
|
|
158
163
|
}
|
|
159
164
|
|
|
165
|
+
// Set firstAttempt on the first execution attempt
|
|
166
|
+
if (update.attempts !== undefined && update.attempts > 0 && !checkpoint.firstAttempt) {
|
|
167
|
+
checkpoint.firstAttempt = new Date().toISOString();
|
|
168
|
+
}
|
|
169
|
+
|
|
160
170
|
Object.assign(checkpoint, update);
|
|
161
171
|
state.lastCheckpoint = new Date().toISOString();
|
|
162
172
|
|
|
@@ -217,13 +227,21 @@ export function getNextPendingTask(state) {
|
|
|
217
227
|
}
|
|
218
228
|
|
|
219
229
|
/**
|
|
220
|
-
* Check if a task has exceeded max iterations.
|
|
230
|
+
* Check if a task has exceeded max iterations or max time.
|
|
231
|
+
*
|
|
232
|
+
* Dual check: exhausted if attempts >= MAX_ITERATIONS_PER_TASK
|
|
233
|
+
* OR if elapsed time since first attempt >= MAX_TIME_PER_TASK_MS.
|
|
221
234
|
*
|
|
222
235
|
* @param {TaskCheckpoint} checkpoint
|
|
223
236
|
* @returns {boolean}
|
|
224
237
|
*/
|
|
225
238
|
export function isTaskExhausted(checkpoint) {
|
|
226
|
-
|
|
239
|
+
if (checkpoint.attempts >= MAX_ITERATIONS_PER_TASK) return true;
|
|
240
|
+
if (checkpoint.firstAttempt) {
|
|
241
|
+
const elapsed = Date.now() - new Date(checkpoint.firstAttempt).getTime();
|
|
242
|
+
if (elapsed >= MAX_TIME_PER_TASK_MS) return true;
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
227
245
|
}
|
|
228
246
|
|
|
229
247
|
/**
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Cause analysis for failed build loop tasks.
|
|
3
|
+
*
|
|
4
|
+
* Analyzes agent output to classify failure categories, detect
|
|
5
|
+
* repetitive patterns, and generate retry guidance.
|
|
6
|
+
*
|
|
7
|
+
* Constitution Article XVII — Execution Mode Governance.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Failure Categories
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Categories of task failures.
|
|
16
|
+
* @enum {string}
|
|
17
|
+
*/
|
|
18
|
+
export const FAILURE_CATEGORIES = {
|
|
19
|
+
SYNTAX_ERROR: 'syntax_error',
|
|
20
|
+
TEST_FAILURE: 'test_failure',
|
|
21
|
+
LINT_VIOLATION: 'lint_violation',
|
|
22
|
+
TYPE_ERROR: 'type_error',
|
|
23
|
+
TIMEOUT: 'timeout',
|
|
24
|
+
SCOPE_VIOLATION: 'scope_violation',
|
|
25
|
+
DEPENDENCY: 'dependency',
|
|
26
|
+
RUNTIME_ERROR: 'runtime_error',
|
|
27
|
+
UNKNOWN: 'unknown',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Patterns for classifying failure output.
|
|
32
|
+
*/
|
|
33
|
+
const CATEGORY_PATTERNS = [
|
|
34
|
+
{ regex: /SyntaxError|Unexpected token|parsing error|unterminated/i, category: FAILURE_CATEGORIES.SYNTAX_ERROR },
|
|
35
|
+
{ regex: /test.*fail|assertion.*fail|expect.*receive|expected.*but got/i, category: FAILURE_CATEGORIES.TEST_FAILURE },
|
|
36
|
+
{ regex: /lint.*error|eslint|prettier.*error|formatting/i, category: FAILURE_CATEGORIES.LINT_VIOLATION },
|
|
37
|
+
{ regex: /TypeError|type.*mismatch|cannot read propert|is not a function/i, category: FAILURE_CATEGORIES.TYPE_ERROR },
|
|
38
|
+
{ regex: /timeout|ETIMEDOUT|exceeded.*time|timed out/i, category: FAILURE_CATEGORIES.TIMEOUT },
|
|
39
|
+
{ regex: /scope.*violation|cannot write|not allowed.*mode|Article XI/i, category: FAILURE_CATEGORIES.SCOPE_VIOLATION },
|
|
40
|
+
{ regex: /Cannot find module|ENOENT|module not found|import.*failed|missing dependency/i, category: FAILURE_CATEGORIES.DEPENDENCY },
|
|
41
|
+
{ regex: /ReferenceError|RangeError|Error:|runtime error|ENOMEM/i, category: FAILURE_CATEGORIES.RUNTIME_ERROR },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Public API
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Analyze the cause of a task failure.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} output - Agent output/error text
|
|
52
|
+
* @param {Array<{category: string, output: string}>} [previousAttempts=[]] - History of prior attempts
|
|
53
|
+
* @returns {{ category: string, rootCause: string, suggestion: string, isRepetitive: boolean }}
|
|
54
|
+
*/
|
|
55
|
+
export function analyzeCause(output, previousAttempts = []) {
|
|
56
|
+
const text = output || '';
|
|
57
|
+
const category = classifyOutput(text);
|
|
58
|
+
const rootCause = extractRootCause(text, category);
|
|
59
|
+
const suggestion = generateSuggestion(category, rootCause);
|
|
60
|
+
const isRepetitive = checkRepetitive(category, previousAttempts);
|
|
61
|
+
|
|
62
|
+
return { category, rootCause, suggestion, isRepetitive };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build retry guidance for the next attempt based on cause analysis.
|
|
67
|
+
*
|
|
68
|
+
* @param {{ category: string, rootCause: string, suggestion: string, isRepetitive: boolean }} analysis
|
|
69
|
+
* @param {number} attempt - Current attempt number
|
|
70
|
+
* @returns {string} Guidance text for the agent
|
|
71
|
+
*/
|
|
72
|
+
export function buildRetryGuidance(analysis, attempt) {
|
|
73
|
+
const parts = [];
|
|
74
|
+
|
|
75
|
+
parts.push(`## Retry Guidance (Attempt ${attempt})`);
|
|
76
|
+
parts.push('');
|
|
77
|
+
parts.push(`**Previous failure**: ${analysis.category}`);
|
|
78
|
+
|
|
79
|
+
if (analysis.rootCause) {
|
|
80
|
+
parts.push(`**Root cause**: ${analysis.rootCause}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
parts.push(`**Suggestion**: ${analysis.suggestion}`);
|
|
84
|
+
|
|
85
|
+
if (analysis.isRepetitive) {
|
|
86
|
+
parts.push('');
|
|
87
|
+
parts.push('**WARNING**: This is a REPETITIVE failure. The same category of error occurred in previous attempts.');
|
|
88
|
+
parts.push('You MUST try a fundamentally different approach — do not repeat the same fix.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return parts.join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Internal Helpers
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Classify output text into a failure category.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} text
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
function classifyOutput(text) {
|
|
105
|
+
for (const { regex, category } of CATEGORY_PATTERNS) {
|
|
106
|
+
if (regex.test(text)) {
|
|
107
|
+
return category;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return FAILURE_CATEGORIES.UNKNOWN;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extract a concise root cause from the output.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} text
|
|
117
|
+
* @param {string} category
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
function extractRootCause(text, category) {
|
|
121
|
+
if (!text) return 'No output available';
|
|
122
|
+
|
|
123
|
+
// Try to find the most informative error line
|
|
124
|
+
const lines = text.split('\n').filter(l => l.trim());
|
|
125
|
+
|
|
126
|
+
// Look for lines containing "Error:" or similar markers
|
|
127
|
+
const errorLine = lines.find(l =>
|
|
128
|
+
/error:|Error:|ERR!|FAIL|failed/i.test(l)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (errorLine) {
|
|
132
|
+
return errorLine.trim().slice(0, 200);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Fall back to last non-empty line (often the summary)
|
|
136
|
+
if (lines.length > 0) {
|
|
137
|
+
return lines[lines.length - 1].trim().slice(0, 200);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return `${category} detected (no specific error line found)`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Generate a suggestion based on the failure category.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} category
|
|
147
|
+
* @param {string} rootCause
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
function generateSuggestion(category, rootCause) {
|
|
151
|
+
const suggestions = {
|
|
152
|
+
[FAILURE_CATEGORIES.SYNTAX_ERROR]: 'Check for missing brackets, semicolons, or malformed expressions. Review the exact line mentioned in the error.',
|
|
153
|
+
[FAILURE_CATEGORIES.TEST_FAILURE]: 'Review the failing test assertions. Ensure the implementation matches the expected behavior described in the test.',
|
|
154
|
+
[FAILURE_CATEGORIES.LINT_VIOLATION]: 'Fix formatting and style issues. Check import ordering, unused variables, and indentation.',
|
|
155
|
+
[FAILURE_CATEGORIES.TYPE_ERROR]: 'Verify that variable types match expected types. Check for null/undefined access and incorrect function signatures.',
|
|
156
|
+
[FAILURE_CATEGORIES.TIMEOUT]: 'The operation took too long. Consider optimizing the approach or breaking the task into smaller pieces.',
|
|
157
|
+
[FAILURE_CATEGORIES.SCOPE_VIOLATION]: 'The write operation is outside the allowed scope for the current mode. Only modify files within the permitted directories.',
|
|
158
|
+
[FAILURE_CATEGORIES.DEPENDENCY]: 'A required module or dependency is missing. Ensure all imports reference existing files and packages are installed.',
|
|
159
|
+
[FAILURE_CATEGORIES.RUNTIME_ERROR]: 'A runtime error occurred during execution. Check for logic errors, invalid operations, and edge cases.',
|
|
160
|
+
[FAILURE_CATEGORIES.UNKNOWN]: 'Review the full output to identify the issue. The error does not match known patterns.',
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return suggestions[category] || suggestions[FAILURE_CATEGORIES.UNKNOWN];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if the same failure category appeared in previous attempts.
|
|
168
|
+
*
|
|
169
|
+
* @param {string} category
|
|
170
|
+
* @param {Array<{category: string}>} previousAttempts
|
|
171
|
+
* @returns {boolean}
|
|
172
|
+
*/
|
|
173
|
+
function checkRepetitive(category, previousAttempts) {
|
|
174
|
+
if (category === FAILURE_CATEGORIES.UNKNOWN) return false;
|
|
175
|
+
const sameCategory = previousAttempts.filter(a => a.category === category);
|
|
176
|
+
return sameCategory.length >= 2;
|
|
177
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Progressive escalation for build loop retries.
|
|
3
|
+
*
|
|
4
|
+
* When a task fails repeatedly, escalation progressively increases
|
|
5
|
+
* the resources (model tier, context enrichment) and ultimately
|
|
6
|
+
* pauses for human intervention.
|
|
7
|
+
*
|
|
8
|
+
* Constitution Article XVII — Execution Mode Governance.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Escalation Levels
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Escalation level definitions.
|
|
17
|
+
* @enum {number}
|
|
18
|
+
*/
|
|
19
|
+
export const ESCALATION_LEVELS = {
|
|
20
|
+
/** Standard — use configured model, no extra context. */
|
|
21
|
+
STANDARD: 0,
|
|
22
|
+
/** Enriched — same model, inject cause analysis + retry guidance. */
|
|
23
|
+
ENRICHED: 1,
|
|
24
|
+
/** Upgraded — escalate to higher-tier model (e.g. sonnet → opus). */
|
|
25
|
+
UPGRADED: 2,
|
|
26
|
+
/** Max — pause execution and request human intervention. */
|
|
27
|
+
MAX: 3,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Model upgrade map — maps current model tier to the next tier.
|
|
32
|
+
* Keys are logical tier names used in chati config.
|
|
33
|
+
*/
|
|
34
|
+
const MODEL_UPGRADE_MAP = {
|
|
35
|
+
haiku: 'sonnet',
|
|
36
|
+
sonnet: 'opus',
|
|
37
|
+
opus: 'opus', // opus is ceiling
|
|
38
|
+
// Gemini equivalents
|
|
39
|
+
flash: 'pro',
|
|
40
|
+
pro: 'pro',
|
|
41
|
+
// Codex equivalents
|
|
42
|
+
codex: 'codex',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Thresholds for automatic escalation.
|
|
47
|
+
*/
|
|
48
|
+
const ESCALATION_THRESHOLDS = {
|
|
49
|
+
/** Attempts before escalating from STANDARD to ENRICHED. */
|
|
50
|
+
ENRICHED_AFTER: 2,
|
|
51
|
+
/** Attempts before escalating from ENRICHED to UPGRADED. */
|
|
52
|
+
UPGRADED_AFTER: 4,
|
|
53
|
+
/** Attempts before escalating from UPGRADED to MAX. */
|
|
54
|
+
MAX_AFTER: 7,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Public API
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the current escalation level for a task checkpoint.
|
|
63
|
+
*
|
|
64
|
+
* @param {{ attempts: number, escalationLevel?: number }} checkpoint
|
|
65
|
+
* @returns {number} Current escalation level (0-3)
|
|
66
|
+
*/
|
|
67
|
+
export function getEscalationLevel(checkpoint) {
|
|
68
|
+
if (!checkpoint) return ESCALATION_LEVELS.STANDARD;
|
|
69
|
+
|
|
70
|
+
// Explicit level takes precedence (set by previous escalation)
|
|
71
|
+
if (typeof checkpoint.escalationLevel === 'number') {
|
|
72
|
+
return checkpoint.escalationLevel;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Derive from attempt count
|
|
76
|
+
const attempts = checkpoint.attempts || 0;
|
|
77
|
+
|
|
78
|
+
if (attempts >= ESCALATION_THRESHOLDS.MAX_AFTER) {
|
|
79
|
+
return ESCALATION_LEVELS.MAX;
|
|
80
|
+
}
|
|
81
|
+
if (attempts >= ESCALATION_THRESHOLDS.UPGRADED_AFTER) {
|
|
82
|
+
return ESCALATION_LEVELS.UPGRADED;
|
|
83
|
+
}
|
|
84
|
+
if (attempts >= ESCALATION_THRESHOLDS.ENRICHED_AFTER) {
|
|
85
|
+
return ESCALATION_LEVELS.ENRICHED;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return ESCALATION_LEVELS.STANDARD;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Determine whether escalation should occur based on checkpoint
|
|
93
|
+
* state and cause analysis.
|
|
94
|
+
*
|
|
95
|
+
* @param {{ attempts: number, escalationLevel?: number }} checkpoint
|
|
96
|
+
* @param {{ isRepetitive: boolean, category: string }} analysis - Cause analysis result
|
|
97
|
+
* @returns {{ escalate: boolean, newLevel: number, reason: string }}
|
|
98
|
+
*/
|
|
99
|
+
export function shouldEscalate(checkpoint, analysis) {
|
|
100
|
+
const currentLevel = getEscalationLevel(checkpoint);
|
|
101
|
+
const attempts = checkpoint?.attempts || 0;
|
|
102
|
+
|
|
103
|
+
// Already at max — no further escalation
|
|
104
|
+
if (currentLevel >= ESCALATION_LEVELS.MAX) {
|
|
105
|
+
return {
|
|
106
|
+
escalate: false,
|
|
107
|
+
newLevel: ESCALATION_LEVELS.MAX,
|
|
108
|
+
reason: 'Already at maximum escalation level',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Repetitive failures trigger immediate escalation (+1 level)
|
|
113
|
+
if (analysis?.isRepetitive) {
|
|
114
|
+
const newLevel = Math.min(currentLevel + 1, ESCALATION_LEVELS.MAX);
|
|
115
|
+
return {
|
|
116
|
+
escalate: true,
|
|
117
|
+
newLevel,
|
|
118
|
+
reason: `Repetitive ${analysis.category} failure — escalating to level ${newLevel}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Threshold-based escalation
|
|
123
|
+
let targetLevel = ESCALATION_LEVELS.STANDARD;
|
|
124
|
+
if (attempts >= ESCALATION_THRESHOLDS.MAX_AFTER) {
|
|
125
|
+
targetLevel = ESCALATION_LEVELS.MAX;
|
|
126
|
+
} else if (attempts >= ESCALATION_THRESHOLDS.UPGRADED_AFTER) {
|
|
127
|
+
targetLevel = ESCALATION_LEVELS.UPGRADED;
|
|
128
|
+
} else if (attempts >= ESCALATION_THRESHOLDS.ENRICHED_AFTER) {
|
|
129
|
+
targetLevel = ESCALATION_LEVELS.ENRICHED;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (targetLevel > currentLevel) {
|
|
133
|
+
return {
|
|
134
|
+
escalate: true,
|
|
135
|
+
newLevel: targetLevel,
|
|
136
|
+
reason: `Attempt ${attempts} reached threshold for level ${targetLevel}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
escalate: false,
|
|
142
|
+
newLevel: currentLevel,
|
|
143
|
+
reason: 'No escalation needed',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get configuration for a given escalation level.
|
|
149
|
+
*
|
|
150
|
+
* @param {number} level - Escalation level (0-3)
|
|
151
|
+
* @param {string} [currentModel='sonnet'] - Current model tier name
|
|
152
|
+
* @returns {{ model: string|null, contextBoost: boolean, shouldPause: boolean, description: string }}
|
|
153
|
+
*/
|
|
154
|
+
export function getEscalationConfig(level, currentModel = 'sonnet') {
|
|
155
|
+
switch (level) {
|
|
156
|
+
case ESCALATION_LEVELS.STANDARD:
|
|
157
|
+
return {
|
|
158
|
+
model: null, // no override
|
|
159
|
+
contextBoost: false,
|
|
160
|
+
shouldPause: false,
|
|
161
|
+
description: 'Standard execution — no escalation',
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
case ESCALATION_LEVELS.ENRICHED:
|
|
165
|
+
return {
|
|
166
|
+
model: null, // same model
|
|
167
|
+
contextBoost: true,
|
|
168
|
+
shouldPause: false,
|
|
169
|
+
description: 'Enriched context — cause analysis + retry guidance injected',
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
case ESCALATION_LEVELS.UPGRADED:
|
|
173
|
+
return {
|
|
174
|
+
model: MODEL_UPGRADE_MAP[currentModel] || currentModel,
|
|
175
|
+
contextBoost: true,
|
|
176
|
+
shouldPause: false,
|
|
177
|
+
description: `Model upgraded from ${currentModel} to ${MODEL_UPGRADE_MAP[currentModel] || currentModel}`,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
case ESCALATION_LEVELS.MAX:
|
|
181
|
+
return {
|
|
182
|
+
model: MODEL_UPGRADE_MAP[currentModel] || currentModel,
|
|
183
|
+
contextBoost: true,
|
|
184
|
+
shouldPause: true,
|
|
185
|
+
description: 'Maximum escalation — pausing for human intervention',
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
default:
|
|
189
|
+
return {
|
|
190
|
+
model: null,
|
|
191
|
+
contextBoost: false,
|
|
192
|
+
shouldPause: false,
|
|
193
|
+
description: `Unknown escalation level: ${level}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Build a human-readable escalation summary.
|
|
200
|
+
*
|
|
201
|
+
* @param {number} level - Escalation level
|
|
202
|
+
* @param {string} reason - Escalation reason
|
|
203
|
+
* @returns {string}
|
|
204
|
+
*/
|
|
205
|
+
export function buildEscalationSummary(level, reason) {
|
|
206
|
+
const levelNames = ['STANDARD', 'ENRICHED', 'UPGRADED', 'MAX'];
|
|
207
|
+
const name = levelNames[level] || 'UNKNOWN';
|
|
208
|
+
return `[Escalation ${name}] ${reason}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Exported for testing — escalation thresholds.
|
|
213
|
+
*/
|
|
214
|
+
export { ESCALATION_THRESHOLDS };
|
|
@@ -28,6 +28,17 @@ const CRITICAL_RISK_KEYWORDS = [
|
|
|
28
28
|
'database drop',
|
|
29
29
|
'rm -rf',
|
|
30
30
|
'delete all',
|
|
31
|
+
'credential',
|
|
32
|
+
'password',
|
|
33
|
+
'secret',
|
|
34
|
+
'token',
|
|
35
|
+
'privilege escalation',
|
|
36
|
+
'RCE',
|
|
37
|
+
'XSS',
|
|
38
|
+
'CSRF',
|
|
39
|
+
'SQL injection',
|
|
40
|
+
'command injection',
|
|
41
|
+
'backdoor',
|
|
31
42
|
];
|
|
32
43
|
|
|
33
44
|
/**
|
|
@@ -111,13 +122,20 @@ export function getRecommendedAction(triggers) {
|
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
// Check for critical severity
|
|
114
|
-
const
|
|
125
|
+
const criticalTriggers = triggers.filter(t => t.severity === 'critical');
|
|
115
126
|
|
|
116
|
-
if (
|
|
117
|
-
|
|
127
|
+
if (criticalTriggers.length >= 2) {
|
|
128
|
+
return {
|
|
129
|
+
action: 'abort',
|
|
130
|
+
reason: `Multiple critical safety conditions: ${criticalTriggers.map(t => t.details).join('; ')}`,
|
|
131
|
+
resumable: false,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (criticalTriggers.length === 1) {
|
|
118
136
|
return {
|
|
119
137
|
action: 'pause',
|
|
120
|
-
reason: `Critical safety
|
|
138
|
+
reason: `Critical safety condition: ${criticalTriggers[0].details}`,
|
|
121
139
|
resumable: true,
|
|
122
140
|
};
|
|
123
141
|
}
|
|
@@ -308,7 +326,7 @@ export function buildSafetyReport(state) {
|
|
|
308
326
|
message = 'All safety checks passed. Pipeline can continue.';
|
|
309
327
|
} else {
|
|
310
328
|
const action = getRecommendedAction(result.triggers);
|
|
311
|
-
status = action.action === 'pause' ? 'unsafe' : 'warning';
|
|
329
|
+
status = (action.action === 'pause' || action.action === 'abort') ? 'unsafe' : 'warning';
|
|
312
330
|
message = action.reason;
|
|
313
331
|
}
|
|
314
332
|
|