chati-dev 3.3.2 → 4.0.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 (48) hide show
  1. package/README.md +4 -4
  2. package/framework/agents/plan/ux.md +41 -1
  3. package/framework/config.yaml +18 -3
  4. package/framework/constitution.md +18 -10
  5. package/framework/context/governance.md +2 -0
  6. package/framework/context/root.md +1 -1
  7. package/framework/data/entity-registry.yaml +43 -3
  8. package/framework/domains/constitution.yaml +1 -1
  9. package/framework/domains/global.yaml +7 -3
  10. package/framework/domains/workflows/standard-flow.yaml +33 -0
  11. package/framework/i18n/en.yaml +11 -2
  12. package/framework/i18n/es.yaml +11 -2
  13. package/framework/i18n/fr.yaml +11 -2
  14. package/framework/i18n/pt.yaml +11 -2
  15. package/framework/intelligence/context-engine.md +22 -17
  16. package/framework/presets/nextjs.yaml +41 -0
  17. package/framework/presets/node-express.yaml +39 -0
  18. package/framework/presets/react-vite.yaml +37 -0
  19. package/framework/presets/supabase-fullstack.yaml +37 -0
  20. package/framework/templates/brandbook-tmpl.yaml +113 -0
  21. package/framework/templates/component-spec-tmpl.yaml +74 -0
  22. package/framework/templates/design-token-tmpl.yaml +55 -0
  23. package/framework/templates/icon-system-tmpl.yaml +95 -0
  24. package/package.json +1 -1
  25. package/scripts/bundle-framework.js +1 -1
  26. package/scripts/doctor/checks/agents.js +77 -0
  27. package/scripts/doctor/checks/constitution.js +41 -0
  28. package/scripts/doctor/checks/domain-alignment.js +58 -0
  29. package/scripts/doctor/checks/prism-layers.js +84 -0
  30. package/scripts/doctor/checks/registry.js +55 -0
  31. package/scripts/doctor/checks/schemas.js +61 -0
  32. package/scripts/doctor/fixes/reference-fix.js +100 -0
  33. package/scripts/doctor/fixes/registry-fix.js +56 -0
  34. package/scripts/doctor/index.js +212 -0
  35. package/scripts/health-check.js +8 -8
  36. package/src/autonomy/surface-criteria.js +226 -0
  37. package/src/context/bracket-tracker.js +44 -13
  38. package/src/context/domain-loader.js +22 -0
  39. package/src/context/engine.js +18 -7
  40. package/src/context/formatter.js +21 -1
  41. package/src/context/layers/l5-keywords.js +53 -0
  42. package/src/intelligence/context-status.js +20 -9
  43. package/src/intelligence/decision-engine.js +253 -0
  44. package/src/terminal/prompt-builder.js +341 -1
  45. package/src/terminal/run-agent.js +15 -0
  46. package/src/terminal/run-parallel.js +77 -5
  47. package/src/terminal/spawner.js +13 -0
  48. package/src/utils/feature-flags.js +106 -0
@@ -16,11 +16,12 @@
16
16
 
17
17
  import { fileURLToPath } from 'url';
18
18
  import { buildAgentPrompt } from './prompt-builder.js';
19
- import { spawnParallelGroup } from './spawner.js';
19
+ import { spawnParallelGroup, spawnTerminal } from './spawner.js';
20
20
  import { TerminalMonitor } from './monitor.js';
21
21
  import { collectResults, mergeHandoffs, buildConsolidatedHandoff } from './collector.js';
22
22
  import { parseAgentOutput } from './handoff-parser.js';
23
23
  import { estimateTokens, COST_PER_1K } from './cost-tracker.js';
24
+ import { getRateLimiter } from './rate-limiter.js';
24
25
 
25
26
  // ---------------------------------------------------------------------------
26
27
  // CLI argument parsing
@@ -119,13 +120,29 @@ async function main() {
119
120
  }
120
121
  }
121
122
 
122
- // Spawn all terminals in parallel
123
+ // Check rate limit capacity before spawning
124
+ const groupProvider = configs[0]?.provider || 'claude';
125
+ const limiter = getRateLimiter(groupProvider);
126
+ const rateStats = limiter.getStats();
127
+ const availableSlots = rateStats.limit - rateStats.used;
128
+ if (availableSlots < configs.length) {
129
+ console.error(`[chati] Rate limiter: ${availableSlots} slots available for ${configs.length} agents. Spawns may be throttled.`);
130
+ }
131
+
132
+ // Spawn all terminals in parallel (with sequential fallback)
123
133
  let group;
134
+ let fallbackUsed = false;
124
135
  try {
125
136
  group = spawnParallelGroup(configs);
126
137
  } catch (err) {
127
- outputError(`Failed to spawn parallel group: ${err.message}`);
128
- process.exit(1);
138
+ console.error(`[chati] Parallel spawn failed: ${err.message}. Falling back to sequential execution.`);
139
+ try {
140
+ group = await sequentialFallback(configs, timeout);
141
+ fallbackUsed = true;
142
+ } catch (fallbackErr) {
143
+ outputError(`Sequential fallback also failed: ${fallbackErr.message}`);
144
+ process.exit(1);
145
+ }
129
146
  }
130
147
 
131
148
  // Monitor until completion
@@ -218,6 +235,7 @@ async function main() {
218
235
  timeSaved: elapsed * (agents.length - 1),
219
236
  },
220
237
  costEstimate: costEstimates,
238
+ fallbackUsed,
221
239
  };
222
240
 
223
241
  process.stdout.write(JSON.stringify(output, null, 2) + '\n');
@@ -228,6 +246,60 @@ async function main() {
228
246
  // Helpers
229
247
  // ---------------------------------------------------------------------------
230
248
 
249
+ /**
250
+ * Sequential fallback: spawn agents one at a time when parallel spawning fails.
251
+ * Produces the same group structure as spawnParallelGroup for transparent handling.
252
+ *
253
+ * @param {object[]} configs - Agent spawn configurations
254
+ * @param {number} timeout - Per-agent timeout in ms
255
+ * @returns {Promise<{ groupId: string, terminals: object[] }>}
256
+ */
257
+ async function sequentialFallback(configs, timeout) {
258
+ const groupId = `seq-fallback-${Date.now()}`;
259
+ const terminals = [];
260
+
261
+ for (let i = 0; i < configs.length; i++) {
262
+ const cfg = configs[i];
263
+ const terminal = spawnTerminal({
264
+ agent: cfg.agent,
265
+ taskId: cfg.taskId,
266
+ model: cfg.model,
267
+ provider: cfg.provider,
268
+ prompt: cfg.prompt,
269
+ workingDir: cfg.workingDir,
270
+ timeout: cfg.timeout || timeout,
271
+ });
272
+
273
+ // Wait for this terminal to finish before spawning the next
274
+ await new Promise((resolve) => {
275
+ const timer = setTimeout(() => {
276
+ terminal.kill?.();
277
+ resolve();
278
+ }, (cfg.timeout || timeout) + 5_000);
279
+
280
+ terminal.onExit?.(() => {
281
+ clearTimeout(timer);
282
+ resolve();
283
+ });
284
+
285
+ // If terminal doesn't have onExit, resolve after a short poll
286
+ if (!terminal.onExit) {
287
+ clearTimeout(timer);
288
+ resolve();
289
+ }
290
+ });
291
+
292
+ terminals.push(terminal);
293
+
294
+ // Small delay between spawns to avoid rate limit pressure
295
+ if (i < configs.length - 1) {
296
+ await new Promise(r => setTimeout(r, 500));
297
+ }
298
+ }
299
+
300
+ return { groupId, terminals };
301
+ }
302
+
231
303
  /**
232
304
  * Determine the next sequential agent after a parallel group.
233
305
  * GROUP 1 (detail+architect+ux) → phases
@@ -256,4 +328,4 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
256
328
  });
257
329
  }
258
330
 
259
- export { parseArgs, determineNextAgent };
331
+ export { parseArgs, determineNextAgent, sequentialFallback };
@@ -10,6 +10,7 @@
10
10
  import { spawn } from 'child_process';
11
11
  import { validateWriteScopes, buildIsolationEnv } from './isolation.js';
12
12
  import { getProvider } from './cli-registry.js';
13
+ import { getRateLimiter } from './rate-limiter.js';
13
14
 
14
15
  // ---------------------------------------------------------------------------
15
16
  // Constants
@@ -234,6 +235,10 @@ export function spawnTerminal(config) {
234
235
  timeout,
235
236
  };
236
237
 
238
+ // Record spawn in rate limiter for throttling
239
+ const providerForRate = config.provider || 'claude';
240
+ getRateLimiter(providerForRate).recordSpawn();
241
+
237
242
  // Capture output (capped at ~10MB to prevent unbounded memory growth)
238
243
  const MAX_BUFFER_CHUNKS = 10_000;
239
244
  if (child.stdout) {
@@ -297,6 +302,14 @@ export function spawnParallelGroup(configs) {
297
302
  throw new Error(`Write scope conflicts detected: ${details}`);
298
303
  }
299
304
 
305
+ // Preemptive rate limit capacity check
306
+ const groupProvider = configs[0]?.provider || 'claude';
307
+ const limiter = getRateLimiter(groupProvider);
308
+ const stats = limiter.getStats();
309
+ if (stats.used + configs.length > stats.limit) {
310
+ console.error(`[chati] Rate limit warning: ${stats.used}/${stats.limit} slots used, requesting ${configs.length} more`);
311
+ }
312
+
300
313
  const groupId = `group-${Date.now()}`;
301
314
  const terminals = configs.map(cfg => spawnTerminal(cfg));
302
315
 
@@ -0,0 +1,106 @@
1
+ /**
2
+ * @fileoverview Feature flag reader for chati.dev framework.
3
+ *
4
+ * Reads the `features:` section from config.yaml and returns boolean
5
+ * values for each feature toggle. All features default to false when
6
+ * not explicitly set.
7
+ *
8
+ * Uses lightweight regex-based parsing consistent with config-parser.js.
9
+ */
10
+
11
+ import { existsSync, readFileSync } from 'fs';
12
+ import { join } from 'path';
13
+
14
+ /**
15
+ * All known feature flags with their default values.
16
+ * New features start as false (opt-in).
17
+ */
18
+ const DEFAULTS = {
19
+ hybrid_budget: false,
20
+ anti_dash: false,
21
+ rate_limiter_integration: false,
22
+ l5_keywords: false,
23
+ prompt_size_guard: false,
24
+ ids_decision_engine: false,
25
+ surface_criteria: false,
26
+ parallel_fallback: false,
27
+ tool_mesh: false,
28
+ tech_presets: false,
29
+ doctor_autofix: false,
30
+ brandbook: false,
31
+ };
32
+
33
+ /**
34
+ * Check if a specific feature is enabled.
35
+ *
36
+ * @param {string} projectDir - Project root directory (contains chati.dev/)
37
+ * @param {string} featureName - Feature flag name (e.g., 'hybrid_budget')
38
+ * @returns {boolean} True if feature is enabled, false otherwise
39
+ */
40
+ export function isFeatureEnabled(projectDir, featureName) {
41
+ const features = getEnabledFeatures(projectDir);
42
+ return features[featureName] ?? DEFAULTS[featureName] ?? false;
43
+ }
44
+
45
+ /**
46
+ * Get all feature flags with their current values.
47
+ *
48
+ * @param {string} projectDir - Project root directory (contains chati.dev/)
49
+ * @returns {Record<string, boolean>} Map of feature name to enabled status
50
+ */
51
+ export function getEnabledFeatures(projectDir) {
52
+ const configPath = join(projectDir, 'chati.dev', 'config.yaml');
53
+ if (!existsSync(configPath)) {
54
+ return { ...DEFAULTS };
55
+ }
56
+
57
+ const raw = readFileSync(configPath, 'utf-8');
58
+ return parseFeaturesSection(raw);
59
+ }
60
+
61
+ /**
62
+ * Parse the features section from raw config.yaml content.
63
+ *
64
+ * @param {string} raw - Raw YAML content
65
+ * @returns {Record<string, boolean>} Parsed feature flags merged with defaults
66
+ */
67
+ export function parseFeaturesSection(raw) {
68
+ const result = { ...DEFAULTS };
69
+
70
+ // Find the features: block
71
+ const featuresMatch = raw.match(/^features:\s*\n((?:\s+\w[\w]*:\s*.+\n?)*)/m);
72
+ if (!featuresMatch) return result;
73
+
74
+ const block = featuresMatch[1];
75
+
76
+ // Extract each key: value pair
77
+ const linePattern = /^\s+(\w[\w]*):\s*(true|false)\s*$/gm;
78
+ let match;
79
+ while ((match = linePattern.exec(block)) !== null) {
80
+ const key = match[1];
81
+ const value = match[2] === 'true';
82
+ if (key in result) {
83
+ result[key] = value;
84
+ }
85
+ }
86
+
87
+ return result;
88
+ }
89
+
90
+ /**
91
+ * Get the list of known feature flag names.
92
+ *
93
+ * @returns {string[]} Array of feature flag names
94
+ */
95
+ export function getFeatureNames() {
96
+ return Object.keys(DEFAULTS);
97
+ }
98
+
99
+ /**
100
+ * Get default values for all feature flags.
101
+ *
102
+ * @returns {Record<string, boolean>} Default feature flag values
103
+ */
104
+ export function getDefaults() {
105
+ return { ...DEFAULTS };
106
+ }