chati-dev 3.2.5 → 3.3.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.
Files changed (98) hide show
  1. package/LICENSE +96 -0
  2. package/bin/chati.js +46 -0
  3. package/framework/agents/build/dev.md +122 -1
  4. package/framework/agents/deploy/devops.md +128 -3
  5. package/framework/agents/discover/brief.md +77 -15
  6. package/framework/agents/discover/brownfield-wu.md +2 -2
  7. package/framework/agents/discover/greenfield-wu.md +3 -3
  8. package/framework/agents/plan/architect.md +2 -2
  9. package/framework/agents/plan/detail.md +3 -3
  10. package/framework/agents/plan/phases.md +127 -2
  11. package/framework/agents/plan/tasks.md +127 -2
  12. package/framework/agents/plan/ux.md +269 -22
  13. package/framework/agents/quality/qa-implementation.md +172 -8
  14. package/framework/agents/quality/qa-planning.md +147 -2
  15. package/framework/config.yaml +9 -5
  16. package/framework/constitution.md +7 -1
  17. package/framework/context/quality.md +1 -1
  18. package/framework/context/root.md +1 -1
  19. package/framework/hooks/constitution-guard.js +18 -2
  20. package/framework/hooks/mode-governance.js +3 -3
  21. package/framework/hooks/read-protection.js +10 -2
  22. package/framework/i18n/en.yaml +6 -0
  23. package/framework/i18n/es.yaml +6 -0
  24. package/framework/i18n/fr.yaml +6 -0
  25. package/framework/i18n/pt.yaml +6 -0
  26. package/framework/orchestrator/chati.md +102 -6
  27. package/framework/schemas/task.schema.json +1 -1
  28. package/framework/tasks/architect-dep-audit.md +128 -0
  29. package/framework/tasks/architect-stack-selection.md +28 -0
  30. package/framework/workflows/brownfield-fullstack.yaml +2 -2
  31. package/framework/workflows/brownfield-service.yaml +2 -2
  32. package/framework/workflows/brownfield-ui.yaml +2 -2
  33. package/framework/workflows/greenfield-fullstack.yaml +6 -2
  34. package/framework/workflows/quick-flow.yaml +7 -5
  35. package/framework/workflows/standard-flow.yaml +171 -0
  36. package/package.json +4 -2
  37. package/src/api/index.js +129 -0
  38. package/src/autonomy/build-loop.js +93 -6
  39. package/src/autonomy/build-state.js +20 -2
  40. package/src/autonomy/cause-analyzer.js +177 -0
  41. package/src/autonomy/escalation.js +214 -0
  42. package/src/autonomy/safety-net.js +23 -5
  43. package/src/autonomy/worktree-manager.js +245 -0
  44. package/src/config/agent-customizer.js +227 -0
  45. package/src/config/ide-configs.js +57 -27
  46. package/src/decision/analyzer.js +148 -0
  47. package/src/decision/registry-healer.js +38 -21
  48. package/src/extensions/loader.js +151 -0
  49. package/src/extensions/registry.js +134 -0
  50. package/src/gates/circuit-breaker.js +32 -0
  51. package/src/gates/g3-implementation.js +30 -4
  52. package/src/gates/g4-qa-implementation.js +34 -5
  53. package/src/gates/gate-base.js +9 -0
  54. package/src/health/auto-fix.js +216 -0
  55. package/src/installer/core.js +24 -11
  56. package/src/installer/provider-overlay.js +82 -0
  57. package/src/installer/templates.js +22 -10
  58. package/src/installer/transaction.js +3 -2
  59. package/src/installer/validator.js +74 -0
  60. package/src/intelligence/context-status.js +9 -5
  61. package/src/intelligence/document-sharder.js +221 -0
  62. package/src/intelligence/elicitation.js +265 -0
  63. package/src/intelligence/timeline.js +5 -0
  64. package/src/memory/gotchas.js +78 -2
  65. package/src/merger/semantic-merger.js +292 -0
  66. package/src/orchestrator/agent-selector.js +20 -0
  67. package/src/orchestrator/handoff-engine.js +77 -0
  68. package/src/orchestrator/index.js +0 -8
  69. package/src/orchestrator/intent-classifier.js +182 -0
  70. package/src/orchestrator/pipeline-manager.js +125 -1
  71. package/src/orchestrator/session-manager.js +164 -2
  72. package/src/quality/metrics-collector.js +283 -0
  73. package/src/quality/test-runner.js +368 -0
  74. package/src/telemetry/collector.js +83 -0
  75. package/src/telemetry/config.js +119 -0
  76. package/src/telemetry/index.js +11 -0
  77. package/src/telemetry/schema.js +104 -0
  78. package/src/telemetry/sender.js +60 -0
  79. package/src/terminal/cli-registry.js +7 -1
  80. package/src/terminal/cost-tracker.js +197 -0
  81. package/src/terminal/handoff-parser.js +61 -4
  82. package/src/terminal/prompt-builder.js +56 -18
  83. package/src/terminal/rate-limiter.js +172 -0
  84. package/src/terminal/run-agent.js +39 -0
  85. package/src/terminal/run-parallel.js +22 -1
  86. package/src/terminal/spawner.js +181 -3
  87. package/src/upgrade/migrator.js +2 -2
  88. package/src/utils/event-bus.js +126 -0
  89. package/src/utils/file-lock.js +291 -0
  90. package/src/utils/schema-validator.js +226 -0
  91. package/src/wizard/i18n.js +11 -0
  92. package/src/wizard/index.js +42 -20
  93. package/src/wizard/questions.js +200 -39
  94. package/src/autonomy/execution-profile.js +0 -151
  95. package/src/intelligence/file-tracker.js +0 -117
  96. package/src/memory/gotchas-auto-capture.js +0 -253
  97. package/src/orchestrator/pipeline-state.js +0 -223
  98. 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: result.output?.slice(0, 500) || 'Task failed',
217
+ error: failureOutput.slice(0, 500),
149
218
  });
150
219
 
151
220
  if (onProgress) {
152
- onProgress({ type: 'task_failed', taskId: checkpoint.taskId, attempt: checkpoint.attempts + 1, error: result.output });
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: err.message?.slice(0, 500) || 'Execution 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
- return checkpoint.attempts >= MAX_ITERATIONS_PER_TASK;
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 hasCritical = triggers.some(t => t.severity === 'critical');
125
+ const criticalTriggers = triggers.filter(t => t.severity === 'critical');
115
126
 
116
- if (hasCritical) {
117
- const criticalTriggers = triggers.filter(t => t.severity === 'critical');
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 conditions: ${criticalTriggers.map(t => t.details).join('; ')}`,
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