chati-dev 4.3.1 → 4.4.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.
Files changed (158) hide show
  1. package/README.md +18 -13
  2. package/bin/chati.js +28 -4
  3. package/framework/agents/build/dev.md +5 -1
  4. package/framework/agents/discover/greenfield-wu.md +16 -1
  5. package/framework/agents/plan/tasks.md +31 -0
  6. package/framework/agents/plan/ux-brand-architect.md +21 -2
  7. package/framework/agents/plan/ux-component-engineer.md +10 -0
  8. package/framework/agents/quality/qa-implementation.md +1 -1
  9. package/framework/agents/quality/qa-planning.md +1 -1
  10. package/framework/agents/quality/qa-visual.md +24 -21
  11. package/framework/config.yaml +5 -4
  12. package/framework/constitution.md +60 -11
  13. package/framework/context/governance.md +12 -10
  14. package/framework/context/root.md +2 -2
  15. package/framework/data/entity-registry.yaml +12 -4
  16. package/framework/data/qa-rubrics.yaml +335 -0
  17. package/framework/domains/constitution.yaml +3 -3
  18. package/framework/executors/json-validate.js +80 -0
  19. package/framework/executors/npm-script.js +72 -0
  20. package/framework/executors/path-check.js +67 -0
  21. package/framework/executors/registry.yaml +36 -0
  22. package/framework/hooks/advance-trigger.js +47 -3
  23. package/framework/hooks/constitution-guard.js +9 -4
  24. package/framework/hooks/git-push-authority.js +113 -0
  25. package/framework/hooks/license-guard.js +59 -9
  26. package/framework/hooks/model-governance.js +40 -17
  27. package/framework/hooks/prism-engine.js +78 -11
  28. package/framework/hooks/session-digest.js +12 -3
  29. package/framework/hooks/settings.json +7 -3
  30. package/framework/hooks/style-guard.js +52 -3
  31. package/framework/hooks/team-quality-gate.js +43 -5
  32. package/framework/i18n/en.yaml +3 -3
  33. package/framework/i18n/es.yaml +3 -3
  34. package/framework/i18n/fr.yaml +3 -3
  35. package/framework/i18n/pt.yaml +3 -3
  36. package/framework/intelligence/confidence.yaml +85 -6
  37. package/framework/intelligence/context-engine.md +9 -5
  38. package/framework/intelligence/decision-engine.md +1 -1
  39. package/framework/manifest.json +1351 -0
  40. package/framework/manifest.sig +1 -0
  41. package/framework/orchestrator/chati-router.js +1 -1
  42. package/framework/orchestrator/chati-update.md +19 -3
  43. package/framework/orchestrator/chati.md +24 -3
  44. package/framework/quality-gates/planning-gate.md +2 -0
  45. package/framework/scaffold/motion-premium/scaffold.yaml +2 -1
  46. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +1 -1
  47. package/framework/scaffold/saas-dashboard/README.md +58 -0
  48. package/framework/scaffold/saas-dashboard/app/dashboard/page.tsx.template +546 -0
  49. package/framework/scaffold/saas-dashboard/app/globals.css.template +746 -0
  50. package/framework/scaffold/saas-dashboard/app/layout.tsx.template +192 -0
  51. package/framework/scaffold/saas-dashboard/app/shell.css.template +1070 -0
  52. package/framework/scaffold/saas-dashboard/components/data/Chart.tsx.template +725 -0
  53. package/framework/scaffold/saas-dashboard/components/data/DataTable.tsx.template +623 -0
  54. package/framework/scaffold/saas-dashboard/components/data/DataTable.types.ts.template +104 -0
  55. package/framework/scaffold/saas-dashboard/components/data/DataTablePagination.tsx.template +234 -0
  56. package/framework/scaffold/saas-dashboard/components/data/SkeletonBlock.tsx.template +111 -0
  57. package/framework/scaffold/saas-dashboard/components/data/StatCard.tsx.template +393 -0
  58. package/framework/scaffold/saas-dashboard/components/data/StatCardGrid.tsx.template +121 -0
  59. package/framework/scaffold/saas-dashboard/components/feedback/EmptyState.tsx.template +281 -0
  60. package/framework/scaffold/saas-dashboard/components/interaction/CommandPalette.tsx.template +767 -0
  61. package/framework/scaffold/saas-dashboard/components/interaction/Field.tsx.template +351 -0
  62. package/framework/scaffold/saas-dashboard/components/interaction/Form.tsx.template +424 -0
  63. package/framework/scaffold/saas-dashboard/components/primitives/Badge.tsx.template +193 -0
  64. package/framework/scaffold/saas-dashboard/components/primitives/Button.tsx.template +261 -0
  65. package/framework/scaffold/saas-dashboard/components/primitives/Card.tsx.template +287 -0
  66. package/framework/scaffold/saas-dashboard/components/primitives/Input.tsx.template +310 -0
  67. package/framework/scaffold/saas-dashboard/components/primitives/Spinner.tsx.template +135 -0
  68. package/framework/scaffold/saas-dashboard/components/primitives/index.ts.template +60 -0
  69. package/framework/scaffold/saas-dashboard/components/shell/AppShell.tsx.template +174 -0
  70. package/framework/scaffold/saas-dashboard/components/shell/Breadcrumb.tsx.template +186 -0
  71. package/framework/scaffold/saas-dashboard/components/shell/Container.tsx.template +86 -0
  72. package/framework/scaffold/saas-dashboard/components/shell/NavItem.tsx.template +147 -0
  73. package/framework/scaffold/saas-dashboard/components/shell/Sidebar.tsx.template +378 -0
  74. package/framework/scaffold/saas-dashboard/components/shell/SidebarContext.tsx.template +169 -0
  75. package/framework/scaffold/saas-dashboard/components/shell/ThemeToggle.tsx.template +128 -0
  76. package/framework/scaffold/saas-dashboard/components/shell/Topbar.tsx.template +301 -0
  77. package/framework/scaffold/saas-dashboard/lib/brand.ts.template +324 -0
  78. package/framework/scaffold/saas-dashboard/scaffold.yaml +225 -0
  79. package/framework/schemas/session.schema.json +5 -0
  80. package/framework/schemas/task.schema.json +9 -0
  81. package/framework/scripts/visual-qa.js +87 -1
  82. package/framework/tasks/orchestrator-health.md +4 -4
  83. package/framework/tasks/qa-impl-verdict.md +11 -5
  84. package/framework/templates/qa-gate-tmpl.yaml +24 -11
  85. package/package.json +4 -4
  86. package/src/config/claude-settings-generator.js +9 -6
  87. package/src/config/context-file-generator.js +0 -6
  88. package/src/dashboard/renderer.js +0 -36
  89. package/src/executors/runner.js +204 -0
  90. package/src/installer/core.js +35 -4
  91. package/src/installer/manifest.js +30 -4
  92. package/src/installer/signing-public-key.pem +1 -1
  93. package/src/installer/templates.js +5 -6
  94. package/src/installer/validator.js +2 -1
  95. package/src/intelligence/registry-manager.js +3 -2
  96. package/src/license/client.js +9 -3
  97. package/src/license/commands.js +12 -2
  98. package/src/license/machine-id.js +42 -1
  99. package/src/memory/gotchas.js +58 -40
  100. package/src/memory/magic-docs.js +7 -2
  101. package/src/memory/session-digest.js +9 -4
  102. package/src/orchestrator/cli.js +195 -23
  103. package/src/orchestrator/deviation-handler.js +5 -3
  104. package/src/orchestrator/doctor.js +98 -16
  105. package/src/orchestrator/pipeline-manager.js +44 -22
  106. package/src/orchestrator/session-manager.js +56 -8
  107. package/src/telemetry/sender.js +7 -19
  108. package/src/terminal/run-parallel.js +14 -14
  109. package/src/terminal/run-team.js +3 -3
  110. package/src/upgrade/backup.js +22 -44
  111. package/src/upgrade/migrator.js +51 -12
  112. package/src/upgrade/tracked-files-detector.js +34 -16
  113. package/src/utils/constitution-meta.js +12 -0
  114. package/src/utils/feature-flags.js +26 -2
  115. package/src/utils/flatten-entities.js +4 -40
  116. package/src/utils/schema-validator.js +0 -14
  117. package/src/wizard/i18n.js +3 -3
  118. package/src/api/index.js +0 -120
  119. package/src/autonomy/autonomous-gate.js +0 -294
  120. package/src/autonomy/build-loop.js +0 -281
  121. package/src/autonomy/build-state.js +0 -286
  122. package/src/autonomy/cause-analyzer.js +0 -177
  123. package/src/autonomy/escalation.js +0 -214
  124. package/src/autonomy/index.js +0 -51
  125. package/src/autonomy/mode-manager.js +0 -225
  126. package/src/autonomy/mode-suggester.js +0 -283
  127. package/src/autonomy/progress-reporter.js +0 -275
  128. package/src/autonomy/safety-net.js +0 -370
  129. package/src/config/agent-customizer.js +0 -231
  130. package/src/decision/analyzer.js +0 -291
  131. package/src/decision/engine.js +0 -250
  132. package/src/decision/index.js +0 -38
  133. package/src/decision/registry-healer.js +0 -468
  134. package/src/decision/registry-updater.js +0 -339
  135. package/src/extensions/loader.js +0 -145
  136. package/src/extensions/registry.js +0 -134
  137. package/src/gates/circuit-breaker.js +0 -151
  138. package/src/gates/g1-planning-complete.js +0 -154
  139. package/src/gates/g2-qa-planning.js +0 -156
  140. package/src/gates/g3-implementation.js +0 -215
  141. package/src/gates/g4-qa-implementation.js +0 -240
  142. package/src/gates/g5-deploy-ready.js +0 -181
  143. package/src/gates/gate-base.js +0 -185
  144. package/src/gates/index.js +0 -46
  145. package/src/health/auto-fix.js +0 -216
  146. package/src/health/engine.js +0 -246
  147. package/src/merger/semantic-merger.js +0 -292
  148. package/src/preview/detector.js +0 -238
  149. package/src/preview/index.js +0 -20
  150. package/src/preview/launcher.js +0 -235
  151. package/src/preview/log-buffer.js +0 -103
  152. package/src/quality/metrics-collector.js +0 -281
  153. package/src/quality/test-runner.js +0 -366
  154. package/src/tasks/executor.js +0 -195
  155. package/src/tasks/index.js +0 -4
  156. package/src/tasks/loader.js +0 -210
  157. package/src/tasks/router.js +0 -182
  158. package/src/utils/event-bus.js +0 -126
@@ -1,370 +0,0 @@
1
- /**
2
- * @fileoverview Safety net for autonomous execution
3
- * Auto-pauses pipeline on dangerous conditions
4
- */
5
-
6
- /**
7
- * Safety conditions that trigger automatic pause.
8
- */
9
- export const SAFETY_TRIGGERS = {
10
- CONSECUTIVE_FAILURES: 'consecutive_failures', // 3+ failures in a row
11
- LOW_SCORE: 'low_score', // Score < 70%
12
- CRITICAL_RISK: 'critical_risk', // Security/data risk detected
13
- TIMEOUT: 'timeout', // Agent exceeded time limit
14
- GOTCHA_SPIKE: 'gotcha_spike', // 5+ new gotchas in current session
15
- EDIT_LOOP: 'edit_loop', // Same file edited 3+ times in current task (Article XX)
16
- };
17
-
18
- // Critical risk keywords
19
- const CRITICAL_RISK_KEYWORDS = [
20
- 'security',
21
- 'vulnerability',
22
- 'exploit',
23
- 'injection',
24
- 'authentication',
25
- 'authorization',
26
- 'data loss',
27
- 'data corruption',
28
- 'production',
29
- 'database drop',
30
- 'rm -rf',
31
- 'delete all',
32
- 'credential',
33
- 'password',
34
- 'secret',
35
- 'token',
36
- 'privilege escalation',
37
- 'RCE',
38
- 'XSS',
39
- 'CSRF',
40
- 'SQL injection',
41
- 'command injection',
42
- 'backdoor',
43
- ];
44
-
45
- /**
46
- * Check all safety conditions against current state.
47
- * @param {object} state
48
- * @param {number} state.consecutiveFailures - Number of consecutive gate failures
49
- * @param {number} state.lastScore - Most recent gate score
50
- * @param {string[]} state.riskFlags - Active risk flags
51
- * @param {number} state.sessionGotchas - New gotchas this session
52
- * @param {number} [state.agentDurationMs] - Time current agent has been running
53
- * @param {number} [state.timeoutMs] - Maximum allowed duration
54
- * @returns {{ safe: boolean, triggers: object[], recommendation: string }}
55
- */
56
- export function checkSafety(state) {
57
- const triggers = [];
58
-
59
- // Check each trigger
60
- Object.values(SAFETY_TRIGGERS).forEach(trigger => {
61
- const result = evaluateTrigger(trigger, state);
62
- if (result.triggered) {
63
- triggers.push(result);
64
- }
65
- });
66
-
67
- const safe = triggers.length === 0;
68
- const recommendation = safe
69
- ? 'All safety checks passed'
70
- : getRecommendedAction(triggers).reason;
71
-
72
- return {
73
- safe,
74
- triggers,
75
- recommendation,
76
- };
77
- }
78
-
79
- /**
80
- * Evaluate a single safety condition.
81
- * @param {string} trigger - SAFETY_TRIGGERS value
82
- * @param {object} state
83
- * @returns {{ triggered: boolean, severity: 'warning'|'critical', details: string }}
84
- */
85
- export function evaluateTrigger(trigger, state) {
86
- switch (trigger) {
87
- case SAFETY_TRIGGERS.CONSECUTIVE_FAILURES:
88
- return evaluateConsecutiveFailures(state);
89
-
90
- case SAFETY_TRIGGERS.LOW_SCORE:
91
- return evaluateLowScore(state);
92
-
93
- case SAFETY_TRIGGERS.CRITICAL_RISK:
94
- return evaluateCriticalRisk(state);
95
-
96
- case SAFETY_TRIGGERS.TIMEOUT:
97
- return evaluateTimeout(state);
98
-
99
- case SAFETY_TRIGGERS.GOTCHA_SPIKE:
100
- return evaluateGotchaSpike(state);
101
-
102
- case SAFETY_TRIGGERS.EDIT_LOOP:
103
- return evaluateEditLoop(state);
104
-
105
- default:
106
- return {
107
- triggered: false,
108
- severity: 'warning',
109
- details: 'Unknown trigger',
110
- };
111
- }
112
- }
113
-
114
- /**
115
- * Get recommended action when safety net triggers.
116
- * @param {object[]} triggers - Triggered safety conditions
117
- * @returns {{ action: 'pause'|'warn'|'abort', reason: string, resumable: boolean }}
118
- */
119
- export function getRecommendedAction(triggers) {
120
- if (triggers.length === 0) {
121
- return {
122
- action: 'warn',
123
- reason: 'No safety triggers detected',
124
- resumable: true,
125
- };
126
- }
127
-
128
- // Check for critical severity
129
- const criticalTriggers = triggers.filter(t => t.severity === 'critical');
130
-
131
- if (criticalTriggers.length >= 2) {
132
- return {
133
- action: 'abort',
134
- reason: `Multiple critical safety conditions: ${criticalTriggers.map(t => t.details).join('; ')}`,
135
- resumable: false,
136
- };
137
- }
138
-
139
- if (criticalTriggers.length === 1) {
140
- return {
141
- action: 'pause',
142
- reason: `Critical safety condition: ${criticalTriggers[0].details}`,
143
- resumable: true,
144
- };
145
- }
146
-
147
- // Only warnings
148
- return {
149
- action: 'warn',
150
- reason: `Safety warnings: ${triggers.map(t => t.details).join('; ')}`,
151
- resumable: true,
152
- };
153
- }
154
-
155
- /**
156
- * Evaluate consecutive failures trigger.
157
- * @param {object} state
158
- * @returns {{ triggered: boolean, severity: string, details: string }}
159
- */
160
- function evaluateConsecutiveFailures(state) {
161
- const threshold = 3;
162
- const failures = state.consecutiveFailures || 0;
163
-
164
- if (failures >= threshold) {
165
- return {
166
- triggered: true,
167
- severity: 'critical',
168
- details: `${failures} consecutive gate failures (threshold: ${threshold})`,
169
- };
170
- }
171
-
172
- return {
173
- triggered: false,
174
- severity: 'warning',
175
- details: 'No consecutive failures',
176
- };
177
- }
178
-
179
- /**
180
- * Evaluate low score trigger.
181
- * @param {object} state
182
- * @returns {{ triggered: boolean, severity: string, details: string }}
183
- */
184
- function evaluateLowScore(state) {
185
- const threshold = 70;
186
- const score = state.lastScore;
187
-
188
- if (typeof score !== 'number') {
189
- return {
190
- triggered: false,
191
- severity: 'warning',
192
- details: 'No score available',
193
- };
194
- }
195
-
196
- if (score < threshold) {
197
- return {
198
- triggered: true,
199
- severity: 'warning',
200
- details: `Low quality score: ${score}% (threshold: ${threshold}%)`,
201
- };
202
- }
203
-
204
- return {
205
- triggered: false,
206
- severity: 'warning',
207
- details: 'Score within acceptable range',
208
- };
209
- }
210
-
211
- /**
212
- * Evaluate critical risk trigger.
213
- * @param {object} state
214
- * @returns {{ triggered: boolean, severity: string, details: string }}
215
- */
216
- function evaluateCriticalRisk(state) {
217
- const riskFlags = state.riskFlags || [];
218
-
219
- // Check for critical risk keywords
220
- const criticalFlags = riskFlags.filter(flag =>
221
- CRITICAL_RISK_KEYWORDS.some(keyword =>
222
- flag.toLowerCase().includes(keyword.toLowerCase())
223
- )
224
- );
225
-
226
- if (criticalFlags.length > 0) {
227
- return {
228
- triggered: true,
229
- severity: 'critical',
230
- details: `Critical risks detected: ${criticalFlags.join(', ')}`,
231
- };
232
- }
233
-
234
- return {
235
- triggered: false,
236
- severity: 'warning',
237
- details: 'No critical risks detected',
238
- };
239
- }
240
-
241
- /**
242
- * Evaluate timeout trigger.
243
- * @param {object} state
244
- * @returns {{ triggered: boolean, severity: string, details: string }}
245
- */
246
- function evaluateTimeout(state) {
247
- const { agentDurationMs, timeoutMs } = state;
248
-
249
- if (typeof agentDurationMs !== 'number' || typeof timeoutMs !== 'number') {
250
- return {
251
- triggered: false,
252
- severity: 'warning',
253
- details: 'No timeout configured',
254
- };
255
- }
256
-
257
- if (agentDurationMs >= timeoutMs) {
258
- const minutes = Math.round(agentDurationMs / 60000);
259
- const maxMinutes = Math.round(timeoutMs / 60000);
260
- return {
261
- triggered: true,
262
- severity: 'critical',
263
- details: `Agent timeout: ${minutes}min (max: ${maxMinutes}min)`,
264
- };
265
- }
266
-
267
- return {
268
- triggered: false,
269
- severity: 'warning',
270
- details: 'Agent within time limits',
271
- };
272
- }
273
-
274
- /**
275
- * Evaluate gotcha spike trigger.
276
- * @param {object} state
277
- * @returns {{ triggered: boolean, severity: string, details: string }}
278
- */
279
- function evaluateGotchaSpike(state) {
280
- const threshold = 5;
281
- const gotchas = state.sessionGotchas || 0;
282
-
283
- if (gotchas >= threshold) {
284
- return {
285
- triggered: true,
286
- severity: 'warning',
287
- details: `High gotcha count: ${gotchas} (threshold: ${threshold})`,
288
- };
289
- }
290
-
291
- return {
292
- triggered: false,
293
- severity: 'warning',
294
- details: 'Gotcha count within limits',
295
- };
296
- }
297
-
298
- /**
299
- * Article XX — Detect edit loops (same file edited 3+ times in current task).
300
- * state.fileEdits should be an object: { 'path/to/file.tsx': 3, ... }
301
- */
302
- function evaluateEditLoop(state) {
303
- const threshold = 3;
304
- const fileEdits = state.fileEdits || {};
305
- const loopedFiles = Object.entries(fileEdits)
306
- .filter(([, count]) => count >= threshold);
307
-
308
- if (loopedFiles.length > 0) {
309
- const details = loopedFiles
310
- .map(([path, count]) => `${path} (${count} edits)`)
311
- .join(', ');
312
- return {
313
- triggered: true,
314
- severity: 'critical',
315
- details: `Edit loop detected (Article XX): ${details}. Stop and diagnose root cause before continuing.`,
316
- };
317
- }
318
-
319
- return {
320
- triggered: false,
321
- severity: 'warning',
322
- details: 'No edit loops detected',
323
- };
324
- }
325
-
326
- /**
327
- * Get critical risk keywords list.
328
- * @returns {string[]}
329
- */
330
- export function getCriticalRiskKeywords() {
331
- return [...CRITICAL_RISK_KEYWORDS];
332
- }
333
-
334
- /**
335
- * Check if a risk flag contains critical keywords.
336
- * @param {string} flag
337
- * @returns {boolean}
338
- */
339
- export function isCriticalRisk(flag) {
340
- return CRITICAL_RISK_KEYWORDS.some(keyword =>
341
- flag.toLowerCase().includes(keyword.toLowerCase())
342
- );
343
- }
344
-
345
- /**
346
- * Build safety report for user.
347
- * @param {object} state
348
- * @returns {{ status: string, message: string, triggers: object[] }}
349
- */
350
- export function buildSafetyReport(state) {
351
- const result = checkSafety(state);
352
-
353
- let status;
354
- let message;
355
-
356
- if (result.safe) {
357
- status = 'safe';
358
- message = 'All safety checks passed. Pipeline can continue.';
359
- } else {
360
- const action = getRecommendedAction(result.triggers);
361
- status = (action.action === 'pause' || action.action === 'abort') ? 'unsafe' : 'warning';
362
- message = action.reason;
363
- }
364
-
365
- return {
366
- status,
367
- message,
368
- triggers: result.triggers,
369
- };
370
- }
@@ -1,231 +0,0 @@
1
- /**
2
- * @fileoverview Agent customization via per-project override files.
3
- *
4
- * Loads `.customize.yaml` files from `chati.dev/customize/<agent>.yaml`
5
- * and applies overrides to agent prompts.
6
- *
7
- * Constitution Article XVI — Model Governance.
8
- */
9
-
10
- import { existsSync, readFileSync } from 'fs';
11
- import { join } from 'path';
12
- import yaml from 'js-yaml';
13
- import { resolveFrameworkDir } from '../utils/framework-dir.js';
14
-
15
- // ---------------------------------------------------------------------------
16
- // Constants
17
- // ---------------------------------------------------------------------------
18
-
19
- /**
20
- * Get the customize directory for a given project.
21
- * Supports both .chati.dev/ and chati.dev/ structures.
22
- * @param {string} projectDir
23
- * @returns {string} Relative path from project root
24
- */
25
- function getCustomizeDir(projectDir) {
26
- return `${resolveFrameworkDir(projectDir)}/customize`;
27
- }
28
-
29
- /** Valid override fields that customizations can set. */
30
- export const VALID_OVERRIDE_FIELDS = [
31
- 'model',
32
- 'extra_context',
33
- 'skip_sections',
34
- 'add_sections',
35
- 'provider',
36
- 'timeout',
37
- 'max_iterations',
38
- ];
39
-
40
- // ---------------------------------------------------------------------------
41
- // Public API
42
- // ---------------------------------------------------------------------------
43
-
44
- /**
45
- * @typedef {object} Customization
46
- * @property {boolean} found - Whether a customization file was found
47
- * @property {string} [model] - Override model name
48
- * @property {string} [extra_context] - Additional context to inject
49
- * @property {string[]} [skip_sections] - Sections to omit from prompt
50
- * @property {{ name: string, content: string }[]} [add_sections] - Sections to append
51
- * @property {string} [provider] - Override provider name
52
- * @property {number} [timeout] - Override timeout in ms
53
- * @property {number} [max_iterations] - Override max iterations
54
- */
55
-
56
- /**
57
- * Load customization for a specific agent.
58
- *
59
- * @param {string} projectDir - Project root directory
60
- * @param {string} agentName - Agent name (e.g., 'brief', 'dev')
61
- * @returns {{ found: boolean, overrides: Customization }}
62
- */
63
- export function loadCustomization(projectDir, agentName) {
64
- if (!projectDir || !agentName) {
65
- return { found: false, overrides: {} };
66
- }
67
-
68
- const customizePath = join(projectDir, getCustomizeDir(projectDir), `${agentName}.yaml`);
69
-
70
- if (!existsSync(customizePath)) {
71
- return { found: false, overrides: {} };
72
- }
73
-
74
- try {
75
- const raw = readFileSync(customizePath, 'utf-8');
76
- const parsed = yaml.load(raw);
77
-
78
- if (!parsed || typeof parsed !== 'object') {
79
- return { found: true, overrides: {} };
80
- }
81
-
82
- // Filter to valid override fields only
83
- const overrides = {};
84
- for (const field of VALID_OVERRIDE_FIELDS) {
85
- if (parsed[field] !== undefined) {
86
- overrides[field] = parsed[field];
87
- }
88
- }
89
-
90
- return { found: true, overrides };
91
- } catch { /* expected: content may be malformed */
92
- return { found: false, overrides: {} };
93
- }
94
- }
95
-
96
- /**
97
- * Apply customization overrides to an agent prompt.
98
- *
99
- * @param {string} agentPrompt - Original agent prompt
100
- * @param {Customization} customization - Customization overrides
101
- * @returns {string} Modified prompt
102
- */
103
- export function applyCustomization(agentPrompt, customization) {
104
- if (!agentPrompt || !customization) {
105
- return agentPrompt || '';
106
- }
107
-
108
- let prompt = agentPrompt;
109
-
110
- // Skip sections
111
- if (customization.skip_sections && Array.isArray(customization.skip_sections)) {
112
- for (const sectionName of customization.skip_sections) {
113
- // Match section heading + all content until the next heading of same or higher level, or end of text
114
- // Two-pass approach: try lookahead for next heading, then fallback to end
115
- const headingPattern = new RegExp(
116
- `^(#{1,3})\\s+${escapeRegex(sectionName)}[^\\n]*\\n([\\s\\S]*?)(?=^#{1,3}\\s)`,
117
- 'gm'
118
- );
119
- let replaced = prompt.replace(headingPattern, '');
120
- if (replaced === prompt) {
121
- // Section is at the end — no next heading follows
122
- const endPattern = new RegExp(
123
- `^(#{1,3})\\s+${escapeRegex(sectionName)}[^\\n]*\\n[\\s\\S]*$`,
124
- 'gm'
125
- );
126
- replaced = prompt.replace(endPattern, '');
127
- }
128
- prompt = replaced;
129
- }
130
- }
131
-
132
- // Add extra context at the end
133
- if (customization.extra_context && typeof customization.extra_context === 'string') {
134
- prompt += `\n\n---\n\n## Additional Context\n\n${customization.extra_context}\n`;
135
- }
136
-
137
- // Add new sections
138
- if (customization.add_sections && Array.isArray(customization.add_sections)) {
139
- for (const section of customization.add_sections) {
140
- if (section.name && section.content) {
141
- prompt += `\n\n## ${section.name}\n\n${section.content}\n`;
142
- }
143
- }
144
- }
145
-
146
- return prompt;
147
- }
148
-
149
- /**
150
- * Validate a customization object.
151
- *
152
- * @param {object} customization
153
- * @returns {{ valid: boolean, errors: string[] }}
154
- */
155
- export function validateCustomization(customization) {
156
- const errors = [];
157
-
158
- if (!customization || typeof customization !== 'object') {
159
- errors.push('Customization must be a non-null object');
160
- return { valid: false, errors };
161
- }
162
-
163
- // Check for unknown fields
164
- for (const key of Object.keys(customization)) {
165
- if (!VALID_OVERRIDE_FIELDS.includes(key)) {
166
- errors.push(`Unknown override field: "${key}"`);
167
- }
168
- }
169
-
170
- // Validate model
171
- if (customization.model !== undefined && typeof customization.model !== 'string') {
172
- errors.push('model must be a string');
173
- }
174
-
175
- // Validate extra_context
176
- if (customization.extra_context !== undefined && typeof customization.extra_context !== 'string') {
177
- errors.push('extra_context must be a string');
178
- }
179
-
180
- // Validate skip_sections
181
- if (customization.skip_sections !== undefined) {
182
- if (!Array.isArray(customization.skip_sections)) {
183
- errors.push('skip_sections must be an array');
184
- } else if (customization.skip_sections.some(s => typeof s !== 'string')) {
185
- errors.push('skip_sections must contain only strings');
186
- }
187
- }
188
-
189
- // Validate add_sections
190
- if (customization.add_sections !== undefined) {
191
- if (!Array.isArray(customization.add_sections)) {
192
- errors.push('add_sections must be an array');
193
- } else {
194
- for (const section of customization.add_sections) {
195
- if (!section.name || !section.content) {
196
- errors.push('Each add_section must have name and content');
197
- }
198
- }
199
- }
200
- }
201
-
202
- // Validate timeout
203
- if (customization.timeout !== undefined) {
204
- if (typeof customization.timeout !== 'number' || customization.timeout <= 0) {
205
- errors.push('timeout must be a positive number');
206
- }
207
- }
208
-
209
- // Validate max_iterations
210
- if (customization.max_iterations !== undefined) {
211
- if (typeof customization.max_iterations !== 'number' || customization.max_iterations <= 0) {
212
- errors.push('max_iterations must be a positive number');
213
- }
214
- }
215
-
216
- return { valid: errors.length === 0, errors };
217
- }
218
-
219
- // ---------------------------------------------------------------------------
220
- // Helpers
221
- // ---------------------------------------------------------------------------
222
-
223
- /**
224
- * Escape special regex characters in a string.
225
- *
226
- * @param {string} str
227
- * @returns {string}
228
- */
229
- function escapeRegex(str) {
230
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
231
- }