chati-dev 4.0.11 → 4.1.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 (107) hide show
  1. package/README.md +5 -0
  2. package/bin/chati.js +16 -0
  3. package/framework/agents/build/dev.md +43 -10
  4. package/framework/agents/discover/brief.md +38 -8
  5. package/framework/agents/discover/brownfield-wu.md +44 -3
  6. package/framework/agents/discover/greenfield-wu.md +14 -0
  7. package/framework/agents/plan/architect.md +6 -0
  8. package/framework/agents/plan/detail.md +25 -0
  9. package/framework/agents/plan/tasks.md +29 -1
  10. package/framework/agents/plan/ux-brand-architect.md +215 -0
  11. package/framework/agents/plan/ux-component-engineer.md +289 -0
  12. package/framework/agents/plan/ux-researcher.md +166 -0
  13. package/framework/agents/plan/ux.md +126 -1008
  14. package/framework/agents/quality/qa-implementation.md +121 -22
  15. package/framework/agents/quality/qa-planning.md +18 -0
  16. package/framework/config.yaml +15 -4
  17. package/framework/constitution.md +8 -2
  18. package/framework/context/root.md +1 -1
  19. package/framework/data/entity-registry.yaml +59 -3
  20. package/framework/hooks/constitution-guard.js +67 -1
  21. package/framework/hooks/license-guard.js +4 -4
  22. package/framework/hooks/model-governance.js +2 -1
  23. package/framework/hooks/prism-engine.js +74 -6
  24. package/framework/hooks/read-protection.js +1 -1
  25. package/framework/hooks/session-digest.js +159 -7
  26. package/framework/hooks/settings.json +16 -4
  27. package/framework/hooks/style-guard.js +134 -0
  28. package/framework/hooks/undercover-guard.js +220 -0
  29. package/framework/intelligence/context-engine.md +21 -0
  30. package/framework/intelligence/hooks-performance.md +54 -0
  31. package/framework/intelligence/memory-layer.md +47 -0
  32. package/framework/migrations/v4.0-to-v4.1.yaml +165 -0
  33. package/framework/orchestrator/chati.md +327 -1067
  34. package/framework/templates/brandbook-html-tmpl.md +107 -0
  35. package/framework/templates/session-memory-tmpl.yaml +68 -0
  36. package/package.json +1 -1
  37. package/src/autonomy/build-state.js +1 -1
  38. package/src/autonomy/worktree-manager.js +13 -13
  39. package/src/config/agent-customizer.js +1 -1
  40. package/src/config/gemini-hooks-generator.js +6 -6
  41. package/src/config/ide-configs.js +1 -1
  42. package/src/context/bracket-tracker.js +25 -4
  43. package/src/context/engine.js +37 -7
  44. package/src/context/formatter.js +45 -1
  45. package/src/context/index.js +3 -3
  46. package/src/dashboard/data-reader.js +7 -7
  47. package/src/decision/engine.js +2 -2
  48. package/src/decision/registry-healer.js +1 -1
  49. package/src/decision/registry-updater.js +2 -2
  50. package/src/extensions/loader.js +1 -1
  51. package/src/gates/g1-planning-complete.js +1 -1
  52. package/src/gates/g2-qa-planning.js +1 -1
  53. package/src/gates/g3-implementation.js +4 -4
  54. package/src/gates/g4-qa-implementation.js +2 -2
  55. package/src/health/auto-fix.js +3 -3
  56. package/src/health/engine.js +2 -2
  57. package/src/installer/brownfield-upgrader.js +3 -3
  58. package/src/installer/manifest.js +1 -1
  59. package/src/installer/transaction.js +1 -1
  60. package/src/installer/validator.js +2 -2
  61. package/src/intelligence/decision-engine.js +1 -1
  62. package/src/intelligence/memory-manager.js +1 -1
  63. package/src/intelligence/registry-manager.js +2 -2
  64. package/src/intelligence/timeline.js +1 -1
  65. package/src/license/client.js +1 -1
  66. package/src/license/commands.js +2 -2
  67. package/src/memory/agent-memory.js +3 -3
  68. package/src/memory/daily-digest.js +170 -0
  69. package/src/memory/dream.js +254 -0
  70. package/src/memory/gotchas.js +2 -2
  71. package/src/memory/index.js +18 -0
  72. package/src/memory/magic-docs.js +98 -0
  73. package/src/memory/memory-extractor.js +163 -0
  74. package/src/memory/session-digest.js +144 -6
  75. package/src/merger/yaml-merger.js +1 -1
  76. package/src/orchestrator/cli.js +980 -0
  77. package/src/orchestrator/handoff-engine.js +25 -4
  78. package/src/orchestrator/index.js +9 -0
  79. package/src/orchestrator/session-manager.js +1 -1
  80. package/src/preview/detector.js +3 -3
  81. package/src/preview/launcher.js +2 -2
  82. package/src/quality/metrics-collector.js +1 -1
  83. package/src/quality/test-runner.js +2 -4
  84. package/src/scanning/density-scanner.js +51 -0
  85. package/src/scanning/env-scanner.js +97 -0
  86. package/src/scanning/index.js +7 -0
  87. package/src/scanning/leakage-scanner.js +54 -0
  88. package/src/scanning/placeholder-scanner.js +40 -0
  89. package/src/scanning/security-scanner.js +94 -0
  90. package/src/security/bash-security.js +335 -0
  91. package/src/security/index.js +9 -0
  92. package/src/telemetry/config.js +3 -3
  93. package/src/telemetry/sender.js +1 -1
  94. package/src/terminal/cli-registry.js +1 -1
  95. package/src/terminal/cost-tracker.js +1 -2
  96. package/src/terminal/prompt-builder.js +11 -27
  97. package/src/terminal/rate-limiter.js +1 -2
  98. package/src/terminal/run-agent.js +3 -3
  99. package/src/terminal/spawner.js +41 -4
  100. package/src/upgrade/checker.js +2 -2
  101. package/src/upgrade/migrator.js +1 -1
  102. package/src/utils/config-parser.js +1 -1
  103. package/src/utils/file-lock.js +3 -3
  104. package/src/utils/provider-limits.js +35 -4
  105. package/src/wizard/i18n.js +2 -2
  106. package/src/wizard/index.js +2 -1
  107. package/src/wizard/questions.js +4 -2
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Bash Security — 23-check shell injection defense system.
3
+ *
4
+ * Inspired by Claude Code's bashSecurity.ts which runs every bash command
5
+ * through 23 numbered security checks protecting against shell injection,
6
+ * unicode attacks, IFS manipulation, and prompt injection via bash.
7
+ *
8
+ * This is critical for chati.dev because autonomous agents (run-agent.js)
9
+ * spawn bash processes. A malicious codebase being analyzed could exploit
10
+ * shell injection vectors to escape the sandbox.
11
+ *
12
+ * Each check has:
13
+ * - id: Unique check identifier (matches Claude Code's naming)
14
+ * - name: Human-readable description
15
+ * - pattern: RegExp to detect the threat
16
+ * - severity: critical | high | medium
17
+ * - description: What the attack does
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} SecurityCheck
22
+ * @property {string} id - Check identifier
23
+ * @property {string} name - Human-readable name
24
+ * @property {RegExp} pattern - Detection pattern
25
+ * @property {string} severity - critical | high | medium
26
+ * @property {string} description - What this check catches
27
+ */
28
+
29
+ /** @type {SecurityCheck[]} */
30
+ export const SECURITY_CHECKS = [
31
+ // --- Category: Command Structure Attacks ---
32
+ {
33
+ id: 'INCOMPLETE_COMMANDS',
34
+ name: 'Incomplete command with dangling operators',
35
+ pattern: /[|&;]\s*$/,
36
+ severity: 'high',
37
+ description: 'Command ends with pipe/semicolon/ampersand, expecting injected continuation',
38
+ },
39
+ {
40
+ id: 'JQ_SYSTEM_FUNCTION',
41
+ name: 'jq system() function call',
42
+ pattern: /jq\b.*\bsystem\s*\(/i,
43
+ severity: 'critical',
44
+ description: 'jq system() executes arbitrary shell commands',
45
+ },
46
+ {
47
+ id: 'JQ_FILE_ARGUMENTS',
48
+ name: 'jq with file argument injection',
49
+ pattern: /jq\b.*--from-file|jq\b.*-f\s+[^|&;]+/i,
50
+ severity: 'high',
51
+ description: 'jq --from-file can read arbitrary files',
52
+ },
53
+
54
+ // --- Category: Flag/Argument Obfuscation ---
55
+ {
56
+ id: 'OBFUSCATED_FLAGS',
57
+ name: 'Obfuscated command flags',
58
+ pattern: /\$[({].*[)}].*-/,
59
+ severity: 'high',
60
+ description: 'Variable expansion used to construct flags, hiding dangerous options',
61
+ },
62
+ {
63
+ id: 'SHELL_METACHARACTERS',
64
+ name: 'Shell metacharacter injection',
65
+ pattern: /[`]|(?:\$\((?!.*\becho\b))/,
66
+ severity: 'critical',
67
+ description: 'Backticks or $() command substitution (excluding safe echo patterns)',
68
+ },
69
+ {
70
+ id: 'DANGEROUS_VARIABLES',
71
+ name: 'Dangerous environment variable manipulation',
72
+ pattern: /(?:^|\s)(?:PATH|LD_PRELOAD|LD_LIBRARY_PATH|DYLD_INSERT_LIBRARIES|PYTHONPATH|NODE_PATH|RUBYLIB|PERL5LIB)\s*=/,
73
+ severity: 'critical',
74
+ description: 'Overwriting PATH/LD_PRELOAD can hijack command resolution or inject libraries',
75
+ },
76
+
77
+ // --- Category: Whitespace & Encoding Attacks ---
78
+ {
79
+ id: 'NEWLINES',
80
+ name: 'Embedded newlines in command',
81
+ pattern: /(?<!\\)\n.*(?:rm|curl|wget|chmod|chown|sudo|eval|exec)/,
82
+ severity: 'high',
83
+ description: 'Newlines can hide dangerous commands after an innocent-looking first line',
84
+ },
85
+ {
86
+ id: 'BACKSLASH_ESCAPED_WHITESPACE',
87
+ name: 'Backslash-escaped whitespace hiding arguments',
88
+ pattern: /\\\s+(?:-|\/)/,
89
+ severity: 'medium',
90
+ description: 'Escaped whitespace can hide flags or paths from visual inspection',
91
+ },
92
+ {
93
+ id: 'CONTROL_CHARACTERS',
94
+ name: 'ASCII control characters',
95
+ // eslint-disable-next-line no-control-regex
96
+ pattern: /[\x00-\x08\x0e-\x1f\x7f]/,
97
+ severity: 'critical',
98
+ description: 'Control characters can manipulate terminal behavior or hide content',
99
+ },
100
+ {
101
+ id: 'UNICODE_WHITESPACE',
102
+ name: 'Unicode zero-width or invisible characters',
103
+ pattern: /[\u200B-\u200F\u2028-\u202F\uFEFF\u00A0\u2060\u180E]/,
104
+ severity: 'critical',
105
+ description: 'Zero-width spaces and invisible unicode can hide injected commands',
106
+ },
107
+
108
+ // --- Category: Shell Expansion Attacks ---
109
+ {
110
+ id: 'DANGEROUS_PATTERNS_COMMAND_SUBSTITUTION',
111
+ name: 'Command substitution in dangerous context',
112
+ pattern: /\$\(.*(?:curl|wget|nc|bash|sh|python|perl|ruby|node)\b/i,
113
+ severity: 'critical',
114
+ description: 'Command substitution executing network or interpreter commands',
115
+ },
116
+ {
117
+ id: 'DANGEROUS_PATTERNS_INPUT_REDIRECTION',
118
+ name: 'Input redirection from sensitive sources',
119
+ pattern: /<\s*(?:\/etc\/(?:passwd|shadow|sudoers)|\/proc\/|~\/\.ssh\/|~\/\.aws\/)/,
120
+ severity: 'critical',
121
+ description: 'Reading sensitive system files via input redirection',
122
+ },
123
+ {
124
+ id: 'DANGEROUS_PATTERNS_OUTPUT_REDIRECTION',
125
+ name: 'Output redirection to sensitive targets',
126
+ pattern: />\s*(?:\/etc\/|~\/\.ssh\/|~\/\.bashrc|~\/\.zshrc|~\/\.profile|~\/\.gitconfig)/,
127
+ severity: 'critical',
128
+ description: 'Overwriting sensitive system or user configuration files',
129
+ },
130
+ {
131
+ id: 'IFS_INJECTION',
132
+ name: 'IFS variable manipulation',
133
+ pattern: /\bIFS\s*=/,
134
+ severity: 'critical',
135
+ description: 'Changing Internal Field Separator can alter how shell parses commands',
136
+ },
137
+ {
138
+ id: 'BRACE_EXPANSION',
139
+ name: 'Dangerous brace expansion',
140
+ pattern: /\{.*(?:rm|curl|wget|chmod|eval|exec|sudo).*[,}]/,
141
+ severity: 'high',
142
+ description: 'Brace expansion can generate multiple dangerous commands from one expression',
143
+ },
144
+
145
+ // --- Category: Git & Process Attacks ---
146
+ {
147
+ id: 'GIT_COMMIT_SUBSTITUTION',
148
+ name: 'Git command with embedded substitution',
149
+ pattern: /git\s+(?:commit|push|tag).*\$[({]/,
150
+ severity: 'high',
151
+ description: 'Variable expansion in git commands can inject arbitrary content',
152
+ },
153
+ {
154
+ id: 'PROC_ENVIRON_ACCESS',
155
+ name: 'Process environment access',
156
+ pattern: /\/proc\/(?:self|\d+)\/(?:environ|cmdline|maps|mem)/,
157
+ severity: 'critical',
158
+ description: 'Accessing /proc can leak secrets, memory contents, or environment variables',
159
+ },
160
+
161
+ // --- Category: Token & Parsing Attacks ---
162
+ {
163
+ id: 'MALFORMED_TOKEN_INJECTION',
164
+ name: 'Malformed token or escape sequence',
165
+ pattern: /\\x[0-9a-f]{2}|\\u[0-9a-f]{4}|\\[0-7]{3}/i,
166
+ severity: 'high',
167
+ description: 'Hex/unicode/octal escape sequences can encode hidden characters',
168
+ },
169
+ {
170
+ id: 'MID_WORD_HASH',
171
+ name: 'Hash character in non-comment position',
172
+ pattern: /\w#\w/,
173
+ severity: 'medium',
174
+ description: 'Hash in mid-word position may indicate parameter expansion trick',
175
+ },
176
+ {
177
+ id: 'COMMENT_QUOTE_DESYNC',
178
+ name: 'Comment/quote desynchronization',
179
+ pattern: /#.*['"][^'"]*$/,
180
+ severity: 'medium',
181
+ description: 'Unclosed quotes after comment can desync parsing across lines',
182
+ },
183
+ {
184
+ id: 'QUOTED_NEWLINE',
185
+ name: 'Newline inside quoted string hiding commands',
186
+ pattern: /["'][^"']*\n[^"']*["']/,
187
+ severity: 'high',
188
+ description: 'Newlines inside quotes can hide command sequences',
189
+ },
190
+
191
+ // --- Category: Zsh-Specific Attacks ---
192
+ {
193
+ id: 'ZSH_DANGEROUS_COMMANDS',
194
+ name: 'Zsh-specific dangerous builtins',
195
+ pattern: /\b(?:zmodload|sysopen|sysread|syswrite|zsystem|zselect|ztcp)\b/,
196
+ severity: 'critical',
197
+ description: 'Zsh builtins that can load modules, open sockets, or access raw file descriptors',
198
+ },
199
+ {
200
+ id: 'BACKSLASH_ESCAPED_OPERATORS',
201
+ name: 'Backslash-escaped shell operators',
202
+ pattern: /\\[|;&]/,
203
+ severity: 'medium',
204
+ description: 'Escaped operators may bypass naive pipe/semicolon filtering',
205
+ },
206
+ ];
207
+
208
+ /**
209
+ * Run all 23 security checks against a bash command.
210
+ *
211
+ * @param {string} command - Bash command to validate
212
+ * @returns {{ safe: boolean, findings: Array<{id: string, name: string, severity: string, description: string}> }}
213
+ */
214
+ export function runSecurityChecks(command) {
215
+ if (!command || typeof command !== 'string') {
216
+ return { safe: true, findings: [], checksRun: SECURITY_CHECKS.length };
217
+ }
218
+
219
+ const findings = [];
220
+
221
+ for (const check of SECURITY_CHECKS) {
222
+ check.pattern.lastIndex = 0; // Reset for global patterns
223
+ if (check.pattern.test(command)) {
224
+ findings.push({
225
+ id: check.id,
226
+ name: check.name,
227
+ severity: check.severity,
228
+ description: check.description,
229
+ });
230
+ }
231
+ }
232
+
233
+ return {
234
+ safe: findings.length === 0,
235
+ findings,
236
+ checksRun: SECURITY_CHECKS.length,
237
+ criticalCount: findings.filter(f => f.severity === 'critical').length,
238
+ highCount: findings.filter(f => f.severity === 'high').length,
239
+ mediumCount: findings.filter(f => f.severity === 'medium').length,
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Quick check — returns true if command has any critical findings.
245
+ * @param {string} command
246
+ * @returns {boolean}
247
+ */
248
+ export function hasCriticalRisk(command) {
249
+ if (!command) return false;
250
+ return SECURITY_CHECKS
251
+ .filter(c => c.severity === 'critical')
252
+ .some(c => {
253
+ c.pattern.lastIndex = 0;
254
+ return c.pattern.test(command);
255
+ });
256
+ }
257
+
258
+ /**
259
+ * Get security check summary for dashboard/reporting.
260
+ */
261
+ export function getSecurityCheckSummary() {
262
+ const bySeverity = { critical: 0, high: 0, medium: 0 };
263
+ for (const check of SECURITY_CHECKS) {
264
+ bySeverity[check.severity]++;
265
+ }
266
+ return {
267
+ totalChecks: SECURITY_CHECKS.length,
268
+ bySeverity,
269
+ categories: [
270
+ 'Command Structure Attacks',
271
+ 'Flag/Argument Obfuscation',
272
+ 'Whitespace & Encoding Attacks',
273
+ 'Shell Expansion Attacks',
274
+ 'Git & Process Attacks',
275
+ 'Token & Parsing Attacks',
276
+ 'Zsh-Specific Attacks',
277
+ ],
278
+ };
279
+ }
280
+
281
+ /**
282
+ * Language-specific security checks for SAST scanning.
283
+ * Complements the 23 bash security checks with code-level patterns
284
+ * for JavaScript, TypeScript, and Python.
285
+ */
286
+ export const LANGUAGE_SECURITY_CHECKS = {
287
+ javascript: [
288
+ { id: 'JS_EVAL', pattern: /\beval\s*\(/, severity: 'critical', description: 'eval() enables remote code execution' },
289
+ { id: 'JS_NEW_FUNCTION', pattern: /new\s+Function\s*\(/, severity: 'critical', description: 'new Function() enables RCE' },
290
+ { id: 'JS_INNERHTML', pattern: /\.innerHTML\s*=/, severity: 'critical', description: 'innerHTML enables XSS attacks' },
291
+ { id: 'JS_OUTERHTML', pattern: /\.outerHTML\s*=/, severity: 'critical', description: 'outerHTML enables XSS attacks' },
292
+ { id: 'JS_DOCUMENT_WRITE', pattern: /document\.write\s*\(/, severity: 'critical', description: 'document.write enables XSS' },
293
+ { id: 'JS_DANGEROUSLY_SET', pattern: /dangerouslySetInnerHTML/, severity: 'critical', description: 'React XSS vector' },
294
+ { id: 'JS_NULL_DOM', pattern: /getElementById\s*\([^)]+\)\s*\./, severity: 'high', description: 'DOM query without null check may crash' },
295
+ { id: 'JS_QUERYSELECTOR_NULL', pattern: /querySelector\s*\([^)]+\)\s*\./, severity: 'high', description: 'querySelector without null check may crash' },
296
+ ],
297
+ typescript: [
298
+ { id: 'TS_EVAL', pattern: /\beval\s*\(/, severity: 'critical', description: 'eval() enables RCE' },
299
+ { id: 'TS_INNERHTML', pattern: /\.innerHTML\s*=/, severity: 'critical', description: 'innerHTML enables XSS' },
300
+ { id: 'TS_DANGEROUSLY_SET', pattern: /dangerouslySetInnerHTML/, severity: 'critical', description: 'React XSS vector' },
301
+ { id: 'TS_ANY_CAST', pattern: /as\s+any\b/, severity: 'medium', description: 'Type assertion to any bypasses type safety' },
302
+ { id: 'TS_NULL_DOM', pattern: /getElementById\s*\([^)]+\)\s*\./, severity: 'high', description: 'DOM query without null check' },
303
+ ],
304
+ python: [
305
+ { id: 'PY_EVAL', pattern: /\beval\s*\(/, severity: 'critical', description: 'eval() enables RCE' },
306
+ { id: 'PY_EXEC', pattern: /\bexec\s*\(/, severity: 'critical', description: 'exec() enables RCE' },
307
+ { id: 'PY_COMPILE', pattern: /\bcompile\s*\(/, severity: 'high', description: 'compile() can enable code injection' },
308
+ { id: 'PY_SHELL_TRUE', pattern: /shell\s*=\s*True/, severity: 'critical', description: 'shell=True enables command injection' },
309
+ { id: 'PY_PICKLE', pattern: /pickle\.loads?\s*\(/, severity: 'critical', description: 'pickle from untrusted source is unsafe' },
310
+ { id: 'PY_YAML_LOAD', pattern: /yaml\.load\s*\([^)]*\)\s*(?!.*Loader)/, severity: 'high', description: 'yaml.load without SafeLoader is unsafe' },
311
+ ],
312
+ };
313
+
314
+ /**
315
+ * Run language-specific security checks against source code content.
316
+ * @param {string} content - Source code to scan
317
+ * @param {string} language - Language identifier (javascript, typescript, python)
318
+ * @returns {{ safe: boolean, findings: Array, checksRun: number }}
319
+ */
320
+ export function runLanguageSecurityChecks(content, language) {
321
+ const checks = LANGUAGE_SECURITY_CHECKS[language] || [];
322
+ if (!content || checks.length === 0) return { safe: true, findings: [], checksRun: 0 };
323
+
324
+ const findings = [];
325
+ for (const check of checks) {
326
+ if (check.pattern.test(content)) {
327
+ findings.push({ id: check.id, severity: check.severity, description: check.description });
328
+ }
329
+ }
330
+ return {
331
+ safe: findings.length === 0,
332
+ findings,
333
+ checksRun: checks.length,
334
+ };
335
+ }
@@ -0,0 +1,9 @@
1
+ // Barrel exports for security system
2
+ export {
3
+ runSecurityChecks,
4
+ hasCriticalRisk,
5
+ getSecurityCheckSummary,
6
+ SECURITY_CHECKS,
7
+ LANGUAGE_SECURITY_CHECKS,
8
+ runLanguageSecurityChecks,
9
+ } from './bash-security.js';
@@ -41,7 +41,7 @@ export function getTelemetryConfig(targetDir) {
41
41
  anonymousId: telemetry.anonymous_id || null,
42
42
  endpoint: telemetry.endpoint || defaults.endpoint,
43
43
  };
44
- } catch {
44
+ } catch { /* expected: file may not exist */
45
45
  return defaults;
46
46
  }
47
47
  }
@@ -81,7 +81,7 @@ export function setEnabled(targetDir, enabled) {
81
81
  }
82
82
 
83
83
  writeFileSync(configPath, yaml.dump(config, { lineWidth: -1 }), 'utf-8');
84
- } catch {
84
+ } catch { /* expected: operation may fail gracefully */
85
85
  // Silently fail — telemetry config is non-critical
86
86
  }
87
87
  }
@@ -108,7 +108,7 @@ export function getAnonymousId(targetDir) {
108
108
  if (!parsed.telemetry) parsed.telemetry = {};
109
109
  parsed.telemetry.anonymous_id = id;
110
110
  writeFileSync(configPath, yaml.dump(parsed, { lineWidth: -1 }), 'utf-8');
111
- } catch {
111
+ } catch { /* expected: file may not exist */
112
112
  // Return generated ID even if persistence fails
113
113
  }
114
114
  }
@@ -72,7 +72,7 @@ export async function sendEvents(events, config) {
72
72
 
73
73
  clearTimeout(timeout);
74
74
  if (res.ok || res.status < 500) return true; // 4xx = don't retry
75
- } catch {
75
+ } catch { /* expected: network may be unavailable */
76
76
  // Network error or timeout — retry if attempts remain
77
77
  if (attempt < 3) {
78
78
  await new Promise(r => setTimeout(r, attempt * 500)); // 500ms, 1000ms
@@ -173,7 +173,7 @@ export async function isProviderAvailable(name) {
173
173
  const cmd = process.platform === 'win32' ? 'where' : 'which';
174
174
  execFileSync(cmd, [provider.command], { stdio: 'ignore' });
175
175
  return true;
176
- } catch {
176
+ } catch { /* expected: git command may fail */
177
177
  return false;
178
178
  }
179
179
  }
@@ -23,9 +23,8 @@ export const COST_PER_1K = {
23
23
  // Gemini models
24
24
  pro: 0.007,
25
25
  flash: 0.001,
26
- // Codex/Copilot
26
+ // Codex
27
27
  codex: 0.010,
28
- copilot: 0.010,
29
28
  mini: 0.003,
30
29
  // Fallback
31
30
  unknown: 0.015,
@@ -160,8 +160,8 @@ export function buildAgentPrompt(config) {
160
160
  if (gotchasSection) {
161
161
  sections.push('<!-- GOTCHAS -->\n' + gotchasSection);
162
162
  }
163
- } catch {
164
- // Gotchas are advisory — never block prompt building
163
+ } catch (err) {
164
+ process.stderr.write(`[chati] prompt-builder gotchas: ${err.message}\n`);
165
165
  }
166
166
 
167
167
  // 5. Additional context (user input relay for needs_input)
@@ -516,7 +516,7 @@ export function loadPreset(stack, projectDir) {
516
516
  const raw = readFileSync(presetPath, 'utf-8');
517
517
  const preset = parsePresetYaml(raw);
518
518
  return { loaded: true, preset, stack };
519
- } catch {
519
+ } catch { /* expected: file may not exist */
520
520
  continue;
521
521
  }
522
522
  }
@@ -549,7 +549,7 @@ export function detectPreset(projectDir) {
549
549
  let presetFiles;
550
550
  try {
551
551
  presetFiles = readdirSync(presetsDir).filter(f => f.endsWith('.yaml'));
552
- } catch {
552
+ } catch { /* expected: directory may not exist */
553
553
  return null;
554
554
  }
555
555
 
@@ -708,10 +708,7 @@ function buildOutputInstructions(config) {
708
708
  const modelValue = config?.model || 'unknown';
709
709
 
710
710
  return `<!-- OUTPUT INSTRUCTIONS -->
711
- ## Output Instructions (MANDATORY)
712
-
713
- When you complete your work, you MUST include a structured handoff block at the END of your response.
714
- This block is how the orchestrator reads your results. Without it, your work cannot be collected.
711
+ At the END of your response, include this handoff block:
715
712
 
716
713
  \`\`\`
717
714
  <chati-handoff>
@@ -719,29 +716,16 @@ status: complete
719
716
  score: 95
720
717
  provider: ${providerValue}
721
718
  model: ${modelValue}
722
- summary: One to three sentence summary of what was accomplished.
719
+ summary: One to three sentence summary.
723
720
  outputs:
724
- - path/to/artifact1.md
725
- - path/to/artifact2.md
721
+ - path/to/artifact.md
726
722
  decisions:
727
- key1: value1
728
- key2: value2
729
- blockers:
730
- - Description of any unresolved blocker
723
+ key: value
724
+ blockers: []
731
725
  needs_input_question: null
732
726
  </chati-handoff>
733
727
  \`\`\`
734
728
 
735
- ### Status values:
736
- - **complete**: All work finished successfully (score >= 95 required)
737
- - **partial**: Some work done but not all criteria met
738
- - **needs_input**: You need information from the user to continue. Set \`needs_input_question\` to your question.
739
- - **error**: Something went wrong that you cannot recover from
740
-
741
- ### Important:
742
- - The \`<chati-handoff>\` block MUST appear in your response
743
- - Score must be 0-100 (95+ to pass quality gate)
744
- - List ALL artifacts you created/modified in outputs
745
- - The \`provider\` and \`model\` fields MUST match: provider: ${providerValue}, model: ${modelValue}
746
- - If you need user input, set status to "needs_input" and write your question in needs_input_question`;
729
+ Status: complete (score >= 95) | partial | needs_input (set question) | error
730
+ List ALL created/modified artifacts in outputs. Provider: ${providerValue}, model: ${modelValue}.`;
747
731
  }
@@ -19,7 +19,6 @@ export const DEFAULT_LIMITS = {
19
19
  claude: 20,
20
20
  gemini: 15,
21
21
  codex: 10,
22
- copilot: 10,
23
22
  };
24
23
 
25
24
  /** Sliding window size in milliseconds (1 minute). */
@@ -41,7 +40,7 @@ const WINDOW_MS = 60_000;
41
40
  /**
42
41
  * Create a rate limiter instance for a specific provider.
43
42
  *
44
- * @param {string} provider - Provider name (claude, gemini, codex, copilot)
43
+ * @param {string} provider - Provider name (claude, gemini, codex)
45
44
  * @param {{ limit?: number }} [customLimits={}]
46
45
  * @returns {{ canSpawn: () => boolean, recordSpawn: () => void, waitForSlot: () => Promise<void>, getStats: () => RateLimiterStats, reset: () => void }}
47
46
  */
@@ -78,8 +78,8 @@ async function main() {
78
78
  const raw = readFileSync(sessionPath, 'utf-8');
79
79
  sessionState = parseSimpleYaml(raw);
80
80
  }
81
- } catch {
82
- // Session state is optional — continue without it
81
+ } catch (err) {
82
+ process.stderr.write(`[chati] run-agent session-parse: ${err.message}\n`);
83
83
  }
84
84
 
85
85
  // Build the agent prompt
@@ -248,7 +248,7 @@ async function flushAndSend(projectDir) {
248
248
  try {
249
249
  const tConfig = getTelemetryConfig(projectDir);
250
250
  await sendEvents(events, { ...tConfig, version: tConfig.chatiVersion || 'unknown' });
251
- } catch {
251
+ } catch { /* expected: operation may fail gracefully */
252
252
  // Silently fail — telemetry must never block agent execution
253
253
  }
254
254
  }
@@ -23,8 +23,15 @@ export const DEFAULT_CONCURRENCY = 3;
23
23
  export const TRANSIENT_PATTERNS = [
24
24
  /rate limit/i, /too many requests/i, /429/, /503/,
25
25
  /timeout/i, /ECONNRESET/, /ECONNREFUSED/,
26
+ /529/, /overloaded/i, /capacity/i,
26
27
  ];
27
28
 
29
+ /** Model downgrade map for fallback on overload errors. */
30
+ const MODEL_DOWNGRADE_MAP = { opus: 'sonnet', pro: 'flash' };
31
+
32
+ /** Patterns that indicate overload (triggers model fallback). */
33
+ const OVERLOAD_PATTERNS = [/529/, /overloaded/i, /capacity/i, /too many requests/i];
34
+
28
35
  // ---------------------------------------------------------------------------
29
36
  // Helpers
30
37
  // ---------------------------------------------------------------------------
@@ -156,7 +163,8 @@ export function buildSpawnCommand(config) {
156
163
  if (config.contextPayload) {
157
164
  try {
158
165
  env.CHATI_CONTEXT = JSON.stringify(config.contextPayload);
159
- } catch {
166
+ } catch (err) {
167
+ process.stderr.write(`[chati] spawner context-serialize: ${err.message}\n`);
160
168
  env.CHATI_CONTEXT = '{}';
161
169
  }
162
170
  }
@@ -431,7 +439,7 @@ export function killTerminal(handle) {
431
439
  const forceKillTimer = setTimeout(() => {
432
440
  try {
433
441
  handle.process.kill('SIGKILL');
434
- } catch {
442
+ } catch { /* expected: process may already be dead */
435
443
  // already dead -- ignore
436
444
  }
437
445
  }, 5000);
@@ -445,7 +453,7 @@ export function killTerminal(handle) {
445
453
 
446
454
  try {
447
455
  handle.process.kill('SIGTERM');
448
- } catch {
456
+ } catch { /* expected: process may already be dead */
449
457
  clearTimeout(forceKillTimer);
450
458
  handle.status = 'killed';
451
459
  resolve({ killed: false, exitCode: handle.exitCode });
@@ -509,6 +517,7 @@ export async function spawnTerminalWithRetry(config, retryOptions = {}) {
509
517
  const maxRetries = retryOptions.maxRetries ?? 2;
510
518
  const baseDelay = retryOptions.baseDelay ?? 2000;
511
519
  const shouldRetry = retryOptions.shouldRetry || isTransientFailure;
520
+ const enableModelFallback = retryOptions.enableModelFallback ?? true;
512
521
 
513
522
  let lastHandle = null;
514
523
 
@@ -536,7 +545,35 @@ export async function spawnTerminalWithRetry(config, retryOptions = {}) {
536
545
  }
537
546
 
538
547
  // Non-transient or out of retries — return as-is
539
- return handle;
548
+ break;
549
+ }
550
+
551
+ // Model fallback: if all retries exhausted with overload errors, try with downgraded model
552
+ if (enableModelFallback && lastHandle && lastHandle.exitCode !== 0) {
553
+ const stderrStr = Array.isArray(lastHandle.stderr) ? lastHandle.stderr.join('') : (lastHandle.stderr || '');
554
+ const isOverload = OVERLOAD_PATTERNS.some(p => p.test(stderrStr));
555
+ const fallbackModel = MODEL_DOWNGRADE_MAP[config.model];
556
+
557
+ if (isOverload && fallbackModel) {
558
+ const fallbackConfig = { ...config, model: fallbackModel };
559
+ const fallbackHandle = spawnTerminal(fallbackConfig);
560
+
561
+ await new Promise((resolve) => {
562
+ if (!fallbackHandle.process) { resolve(); return; }
563
+ if (fallbackHandle.status !== 'running') { resolve(); return; }
564
+ fallbackHandle.process.once('exit', () => resolve());
565
+ });
566
+
567
+ // Record fallback metadata
568
+ fallbackHandle.modelFallback = {
569
+ original: config.model,
570
+ actual: fallbackModel,
571
+ reason: 'overload_exhausted_retries',
572
+ timestamp: new Date().toISOString(),
573
+ };
574
+
575
+ return fallbackHandle;
576
+ }
540
577
  }
541
578
 
542
579
  return lastHandle;
@@ -13,7 +13,7 @@ export function getCurrentVersion(targetDir) {
13
13
  try {
14
14
  const config = yaml.load(readFileSync(configPath, 'utf-8'));
15
15
  return config?.version || null;
16
- } catch {
16
+ } catch { /* expected: file may not exist */
17
17
  return null;
18
18
  }
19
19
  }
@@ -82,7 +82,7 @@ export function updateConfigVersion(targetDir, newVersion) {
82
82
  config.version = newVersion;
83
83
  writeFileSync(configPath, yaml.dump(config, { lineWidth: -1 }), 'utf-8');
84
84
  return true;
85
- } catch {
85
+ } catch { /* expected: file may not exist */
86
86
  return false;
87
87
  }
88
88
  }
@@ -38,7 +38,7 @@ export function findMigrations(targetDir, fromVersion, toVersion) {
38
38
  rollback: content.rollback || [],
39
39
  });
40
40
  }
41
- } catch {
41
+ } catch { /* expected: operation may fail gracefully */
42
42
  // Skip invalid migration files
43
43
  }
44
44
  }
@@ -91,7 +91,7 @@ export async function isCommandAvailable(command) {
91
91
  const cmd = process.platform === 'win32' ? 'where' : 'which';
92
92
  execFileSync(cmd, [command], { stdio: 'ignore' });
93
93
  return true;
94
- } catch {
94
+ } catch { /* expected: git command may fail */
95
95
  return false;
96
96
  }
97
97
  }
@@ -45,7 +45,7 @@ function isProcessAlive(pid) {
45
45
  try {
46
46
  process.kill(pid, 0);
47
47
  return true;
48
- } catch {
48
+ } catch { /* expected: process may already be dead */
49
49
  return false;
50
50
  }
51
51
  }
@@ -60,7 +60,7 @@ function readLockFile(lockPath) {
60
60
  try {
61
61
  const content = readFileSync(lockPath, 'utf-8');
62
62
  return JSON.parse(content);
63
- } catch {
63
+ } catch { /* expected: file may not exist */
64
64
  return null;
65
65
  }
66
66
  }
@@ -182,7 +182,7 @@ export function releaseLock(lockPath) {
182
182
  unlinkSync(lockPath);
183
183
  }
184
184
  }
185
- } catch {
185
+ } catch { /* expected: file may already be deleted */
186
186
  // Lock may already be released — ignore
187
187
  }
188
188
  }