chati-dev 2.1.2 → 3.0.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 (50) hide show
  1. package/framework/agents/build/dev.md +1 -0
  2. package/framework/agents/deploy/devops.md +1 -0
  3. package/framework/agents/discover/brief.md +1 -0
  4. package/framework/agents/discover/brownfield-wu.md +1 -0
  5. package/framework/agents/discover/greenfield-wu.md +1 -0
  6. package/framework/agents/plan/architect.md +1 -0
  7. package/framework/agents/plan/detail.md +1 -0
  8. package/framework/agents/plan/phases.md +1 -0
  9. package/framework/agents/plan/tasks.md +1 -0
  10. package/framework/agents/plan/ux.md +1 -0
  11. package/framework/agents/quality/qa-implementation.md +1 -0
  12. package/framework/agents/quality/qa-planning.md +1 -0
  13. package/framework/config.yaml +21 -2
  14. package/framework/constitution.md +66 -1
  15. package/framework/domains/agents/brownfield-wu.yaml +4 -0
  16. package/framework/domains/agents/dev.yaml +4 -0
  17. package/framework/domains/agents/orchestrator.yaml +8 -0
  18. package/framework/domains/constitution.yaml +28 -0
  19. package/framework/domains/global.yaml +20 -0
  20. package/framework/hooks/model-governance.js +17 -15
  21. package/framework/intelligence/context-engine.md +29 -0
  22. package/framework/intelligence/memory-layer.md +17 -0
  23. package/framework/orchestrator/chati.md +94 -14
  24. package/framework/schemas/config.schema.json +44 -0
  25. package/framework/schemas/session.schema.json +27 -0
  26. package/package.json +5 -1
  27. package/src/autonomy/build-loop.js +194 -0
  28. package/src/autonomy/build-state.js +269 -0
  29. package/src/autonomy/execution-profile.js +151 -0
  30. package/src/config/context-file-generator.js +209 -0
  31. package/src/gates/g2-qa-planning.js +4 -2
  32. package/src/gates/g4-qa-implementation.js +5 -2
  33. package/src/gates/gate-base.js +33 -1
  34. package/src/health/engine.js +250 -0
  35. package/src/intelligence/file-tracker.js +117 -0
  36. package/src/intelligence/timeline.js +144 -0
  37. package/src/memory/gotchas-auto-capture.js +253 -0
  38. package/src/terminal/adapters/claude-adapter.js +43 -0
  39. package/src/terminal/adapters/codex-adapter.js +41 -0
  40. package/src/terminal/adapters/copilot-adapter.js +38 -0
  41. package/src/terminal/adapters/gemini-adapter.js +42 -0
  42. package/src/terminal/adapters/index.js +8 -0
  43. package/src/terminal/cli-registry.js +218 -0
  44. package/src/terminal/prompt-builder.js +18 -15
  45. package/src/terminal/spawner.js +19 -9
  46. package/src/terminal/wave-analyzer.js +143 -0
  47. package/framework/manifest.json +0 -5
  48. package/framework/manifest.sig +0 -1
  49. /package/assets/{logo - co/314/201pia.png" → logo - c/303/263pia.png"} +0 -0
  50. /package/assets/{logo - co/314/201pia.svg" → logo - c/303/263pia.svg"} +0 -0
@@ -0,0 +1,218 @@
1
+ /**
2
+ * @fileoverview CLI Provider Registry for multi-CLI agent execution.
3
+ *
4
+ * Central registry of all supported CLI providers with their capabilities,
5
+ * command syntax, model maps, and feature support. This is the source of
6
+ * truth for multi-CLI governance (Constitution Article XIX).
7
+ */
8
+
9
+ import { existsSync, readFileSync } from 'fs';
10
+ import { join } from 'path';
11
+ import * as adapters from './adapters/index.js';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Provider Definitions
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /**
18
+ * @typedef {object} ProviderConfig
19
+ * @property {string} name - Provider identifier
20
+ * @property {string} command - CLI command name
21
+ * @property {string[]} baseArgs - Default CLI arguments for non-interactive mode
22
+ * @property {string} modelFlag - CLI flag for model selection
23
+ * @property {boolean} stdinSupport - Whether prompts can be piped via stdin
24
+ * @property {boolean} hooksSupport - Whether the CLI supports hooks (event middleware)
25
+ * @property {boolean} mcpSupport - Whether the CLI supports MCP servers
26
+ * @property {string|null} contextFile - Project context file name (CLAUDE.md, GEMINI.md, etc.)
27
+ * @property {Record<string, string>} modelMap - Tier-to-model-id mapping
28
+ * @property {object} adapter - CLI-specific adapter module
29
+ */
30
+
31
+ /** @type {Record<string, ProviderConfig>} */
32
+ const PROVIDERS = {
33
+ claude: {
34
+ name: 'claude',
35
+ command: 'claude',
36
+ baseArgs: ['--print', '--dangerously-skip-permissions'],
37
+ modelFlag: '--model',
38
+ stdinSupport: true,
39
+ hooksSupport: true,
40
+ mcpSupport: true,
41
+ contextFile: 'CLAUDE.md',
42
+ modelMap: {
43
+ opus: 'claude-opus-4-6',
44
+ sonnet: 'claude-sonnet-4-5-20250929',
45
+ haiku: 'claude-haiku-4-5-20251001',
46
+ },
47
+ adapter: adapters.claude,
48
+ },
49
+ gemini: {
50
+ name: 'gemini',
51
+ command: 'gemini',
52
+ baseArgs: ['--prompt'],
53
+ modelFlag: '--model',
54
+ stdinSupport: true,
55
+ hooksSupport: true,
56
+ mcpSupport: true,
57
+ contextFile: 'GEMINI.md',
58
+ modelMap: {
59
+ pro: 'gemini-2.5-pro',
60
+ flash: 'gemini-2.5-flash',
61
+ },
62
+ adapter: adapters.gemini,
63
+ },
64
+ codex: {
65
+ name: 'codex',
66
+ command: 'codex',
67
+ baseArgs: ['exec'],
68
+ modelFlag: '-m',
69
+ stdinSupport: true,
70
+ hooksSupport: false,
71
+ mcpSupport: true,
72
+ contextFile: 'AGENTS.md',
73
+ modelMap: {
74
+ codex: 'gpt-5.3-codex',
75
+ mini: 'gpt-5.1-codex-mini',
76
+ },
77
+ adapter: adapters.codex,
78
+ },
79
+ copilot: {
80
+ name: 'copilot',
81
+ command: 'copilot',
82
+ baseArgs: ['-p'],
83
+ modelFlag: '--model',
84
+ stdinSupport: true,
85
+ hooksSupport: true,
86
+ mcpSupport: true,
87
+ contextFile: null,
88
+ modelMap: {
89
+ 'claude-sonnet': 'claude-sonnet-4.5',
90
+ 'gpt-5': 'gpt-5.1',
91
+ },
92
+ adapter: adapters.copilot,
93
+ },
94
+ };
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Provider Resolution
98
+ // ---------------------------------------------------------------------------
99
+
100
+ /**
101
+ * Get a provider configuration by name.
102
+ *
103
+ * @param {string} name - Provider name (claude, gemini, codex, copilot)
104
+ * @returns {ProviderConfig}
105
+ * @throws {Error} When provider is not found
106
+ */
107
+ export function getProvider(name) {
108
+ const provider = PROVIDERS[name];
109
+ if (!provider) {
110
+ throw new Error(`Unknown CLI provider: "${name}". Available: ${Object.keys(PROVIDERS).join(', ')}`);
111
+ }
112
+ return provider;
113
+ }
114
+
115
+ /**
116
+ * Get all registered providers.
117
+ *
118
+ * @returns {Record<string, ProviderConfig>}
119
+ */
120
+ export function getAllProviders() {
121
+ return { ...PROVIDERS };
122
+ }
123
+
124
+ /**
125
+ * Load enabled providers from project config.yaml.
126
+ *
127
+ * @param {string} projectDir - Project root directory
128
+ * @returns {{ primary: string, enabled: string[] }}
129
+ */
130
+ export function loadEnabledProviders(projectDir) {
131
+ const configPath = join(projectDir, 'chati.dev', 'config.yaml');
132
+ if (!existsSync(configPath)) {
133
+ return { primary: 'claude', enabled: ['claude'] };
134
+ }
135
+
136
+ const raw = readFileSync(configPath, 'utf-8');
137
+
138
+ // Lightweight YAML extraction (avoid dependency in spawning path)
139
+ const enabled = [];
140
+ let primary = 'claude';
141
+
142
+ for (const name of Object.keys(PROVIDERS)) {
143
+ const enabledMatch = raw.match(new RegExp(`${name}:[\\s\\S]*?enabled:\\s*(true|false)`, 'm'));
144
+ if (enabledMatch && enabledMatch[1] === 'true') {
145
+ enabled.push(name);
146
+ }
147
+ const primaryMatch = raw.match(new RegExp(`${name}:[\\s\\S]*?primary:\\s*(true|false)`, 'm'));
148
+ if (primaryMatch && primaryMatch[1] === 'true') {
149
+ primary = name;
150
+ }
151
+ }
152
+
153
+ // Claude is always enabled as fallback
154
+ if (!enabled.includes('claude')) {
155
+ enabled.unshift('claude');
156
+ }
157
+
158
+ return { primary, enabled };
159
+ }
160
+
161
+ /**
162
+ * Resolve which provider should be used for a given agent.
163
+ * Priority: agent_overrides > agent default > primary provider.
164
+ *
165
+ * @param {string} agent - Agent name
166
+ * @param {string} projectDir - Project root directory
167
+ * @param {Record<string, {provider: string, model: string, tier: string}>} agentModels - Agent model assignments
168
+ * @returns {{ provider: string, model: string }}
169
+ */
170
+ export function resolveProviderForAgent(agent, projectDir, agentModels) {
171
+ const { primary, enabled } = loadEnabledProviders(projectDir);
172
+
173
+ // Check agent_overrides in config.yaml
174
+ const configPath = join(projectDir, 'chati.dev', 'config.yaml');
175
+ if (existsSync(configPath)) {
176
+ const raw = readFileSync(configPath, 'utf-8');
177
+ const overrideMatch = raw.match(new RegExp(`${agent}:\\s*\\{[^}]*provider:\\s*(\\w+)[^}]*model:\\s*(\\w+)`, 'm'));
178
+ if (overrideMatch) {
179
+ const overrideProvider = overrideMatch[1];
180
+ if (enabled.includes(overrideProvider)) {
181
+ return { provider: overrideProvider, model: overrideMatch[2] };
182
+ }
183
+ }
184
+ }
185
+
186
+ // Use agent's default assignment
187
+ const agentConfig = agentModels[agent];
188
+ if (agentConfig) {
189
+ const agentProvider = agentConfig.provider || primary;
190
+ if (enabled.includes(agentProvider)) {
191
+ return { provider: agentProvider, model: agentConfig.model };
192
+ }
193
+ }
194
+
195
+ // Fallback to primary
196
+ return { provider: primary, model: 'sonnet' };
197
+ }
198
+
199
+ /**
200
+ * Check if a provider CLI is available on the system.
201
+ *
202
+ * @param {string} name - Provider name
203
+ * @returns {Promise<boolean>}
204
+ */
205
+ export async function isProviderAvailable(name) {
206
+ const provider = PROVIDERS[name];
207
+ if (!provider) return false;
208
+
209
+ const { execSync } = await import('child_process');
210
+ try {
211
+ execSync(`which ${provider.command}`, { stdio: 'ignore' });
212
+ return true;
213
+ } catch {
214
+ return false;
215
+ }
216
+ }
217
+
218
+ export { PROVIDERS };
@@ -18,19 +18,19 @@ import { getWriteScope } from './isolation.js';
18
18
  // without circular dependency issues (the hook file runs main() on import
19
19
  // which writes to stdout — we only need the data, so we inline the map).
20
20
  const AGENT_MODELS = {
21
- orchestrator: 'sonnet',
22
- 'greenfield-wu': 'haiku',
23
- 'brownfield-wu': 'opus',
24
- brief: 'sonnet',
25
- detail: 'opus',
26
- architect: 'opus',
27
- ux: 'sonnet',
28
- phases: 'sonnet',
29
- tasks: 'sonnet',
30
- 'qa-planning': 'opus',
31
- 'qa-implementation': 'opus',
32
- dev: 'opus',
33
- devops: 'sonnet',
21
+ orchestrator: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
22
+ 'greenfield-wu': { provider: 'claude', model: 'haiku', tier: 'haiku' },
23
+ 'brownfield-wu': { provider: 'claude', model: 'opus', tier: 'opus' },
24
+ brief: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
25
+ detail: { provider: 'claude', model: 'opus', tier: 'opus' },
26
+ architect: { provider: 'claude', model: 'opus', tier: 'opus' },
27
+ ux: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
28
+ phases: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
29
+ tasks: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
30
+ 'qa-planning': { provider: 'claude', model: 'opus', tier: 'opus' },
31
+ 'qa-implementation': { provider: 'claude', model: 'opus', tier: 'opus' },
32
+ dev: { provider: 'claude', model: 'opus', tier: 'opus' },
33
+ devops: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
34
34
  };
35
35
 
36
36
  /**
@@ -116,11 +116,13 @@ export function buildAgentPrompt(config) {
116
116
  sections.push(buildOutputInstructions());
117
117
 
118
118
  const prompt = sections.join('\n\n---\n\n');
119
- const model = AGENT_MODELS[config.agent] || 'sonnet';
119
+ const assignment = AGENT_MODELS[config.agent] || { provider: 'claude', model: 'sonnet', tier: 'sonnet' };
120
+ const model = assignment.model || assignment;
120
121
 
121
122
  return {
122
123
  prompt,
123
124
  model,
125
+ provider: assignment.provider || 'claude',
124
126
  metadata: {
125
127
  agent: config.agent,
126
128
  layers: prismResult.layerCount || 0,
@@ -266,7 +268,8 @@ function buildSessionSection(config) {
266
268
  `- **User Level**: ${state.user_level || 'auto'}`,
267
269
  `- **Execution Mode**: ${state.execution_mode || 'autonomous'}`,
268
270
  `- **Your Agent**: ${config.agent}`,
269
- `- **Your Model**: ${AGENT_MODELS[config.agent] || 'sonnet'}`,
271
+ `- **Your Model**: ${(AGENT_MODELS[config.agent]?.model) || 'sonnet'}`,
272
+ `- **Your Provider**: ${(AGENT_MODELS[config.agent]?.provider) || 'claude'}`,
270
273
  ];
271
274
 
272
275
  return lines.join('\n');
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { spawn } from 'child_process';
11
11
  import { validateWriteScopes, buildIsolationEnv } from './isolation.js';
12
+ import { getProvider } from './cli-registry.js';
12
13
 
13
14
  // ---------------------------------------------------------------------------
14
15
  // Helpers
@@ -103,17 +104,26 @@ export function buildSpawnCommand(config) {
103
104
  }
104
105
  }
105
106
 
106
- // Build CLI argsprompt is piped via stdin, NOT as a CLI argument
107
- const command = 'claude';
108
- const args = ['--print', '--dangerously-skip-permissions'];
109
-
110
- if (config.model) {
111
- args.push('--model', config.model);
107
+ // Resolve CLI providerdefaults to claude for backwards compatibility
108
+ const providerName = config.provider || 'claude';
109
+ let command, args, prompt;
110
+
111
+ try {
112
+ const provider = getProvider(providerName);
113
+ const adapterResult = provider.adapter.buildCommand(config, provider);
114
+ command = adapterResult.command;
115
+ args = adapterResult.args;
116
+ prompt = adapterResult.stdinPrompt;
117
+ } catch {
118
+ // Fallback to claude if provider resolution fails (backwards compatibility)
119
+ command = 'claude';
120
+ args = ['--print', '--dangerously-skip-permissions'];
121
+ if (config.model) {
122
+ args.push('--model', config.model);
123
+ }
124
+ prompt = config.prompt || null;
112
125
  }
113
126
 
114
- // Prompt is returned separately for stdin piping (avoids ARG_MAX limits)
115
- const prompt = config.prompt || null;
116
-
117
127
  return { command, args, env, terminalId, prompt };
118
128
  }
119
129
 
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @fileoverview Wave analyzer for task parallelization.
3
+ *
4
+ * Analyzes task dependencies to group them into waves —
5
+ * sets of tasks that can execute in parallel because they
6
+ * have no inter-dependencies.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Types
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * @typedef {object} TaskNode
15
+ * @property {string} id - Task identifier
16
+ * @property {string[]} dependencies - IDs of tasks this depends on
17
+ * @property {string[]} [writeScope] - Files/dirs this task writes to
18
+ */
19
+
20
+ /**
21
+ * @typedef {object} Wave
22
+ * @property {number} index - Wave number (0-based)
23
+ * @property {string[]} taskIds - Task IDs in this wave
24
+ */
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Wave Analysis
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /**
31
+ * Build a dependency graph and compute waves using topological sorting.
32
+ *
33
+ * @param {TaskNode[]} tasks - Array of tasks with dependencies
34
+ * @returns {Wave[]}
35
+ * @throws {Error} When circular dependencies are detected
36
+ */
37
+ export function analyzeWaves(tasks) {
38
+ if (!tasks || tasks.length === 0) return [];
39
+
40
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
41
+ const inDegree = new Map();
42
+ const adjList = new Map();
43
+
44
+ // Initialize
45
+ for (const task of tasks) {
46
+ inDegree.set(task.id, 0);
47
+ adjList.set(task.id, []);
48
+ }
49
+
50
+ // Build adjacency and in-degree
51
+ for (const task of tasks) {
52
+ for (const dep of task.dependencies || []) {
53
+ if (taskMap.has(dep)) {
54
+ adjList.get(dep).push(task.id);
55
+ inDegree.set(task.id, (inDegree.get(task.id) || 0) + 1);
56
+ }
57
+ }
58
+ }
59
+
60
+ // Kahn's algorithm with wave grouping
61
+ const waves = [];
62
+ let remaining = new Set(tasks.map((t) => t.id));
63
+
64
+ while (remaining.size > 0) {
65
+ // Find all tasks with no pending dependencies (in-degree 0)
66
+ const wave = [];
67
+ for (const id of remaining) {
68
+ if ((inDegree.get(id) || 0) === 0) {
69
+ wave.push(id);
70
+ }
71
+ }
72
+
73
+ if (wave.length === 0) {
74
+ const circular = [...remaining].join(', ');
75
+ throw new Error(`Circular dependency detected among tasks: ${circular}`);
76
+ }
77
+
78
+ waves.push({ index: waves.length, taskIds: wave });
79
+
80
+ // Remove wave tasks and update in-degrees
81
+ for (const id of wave) {
82
+ remaining.delete(id);
83
+ for (const dependent of adjList.get(id) || []) {
84
+ inDegree.set(dependent, (inDegree.get(dependent) || 0) - 1);
85
+ }
86
+ }
87
+ }
88
+
89
+ return waves;
90
+ }
91
+
92
+ /**
93
+ * Validate that tasks within a wave don't have write scope conflicts.
94
+ *
95
+ * @param {TaskNode[]} tasks - All tasks
96
+ * @param {Wave} wave - Wave to validate
97
+ * @returns {{ valid: boolean, conflicts: { taskA: string, taskB: string, path: string }[] }}
98
+ */
99
+ export function validateWaveScopes(tasks, wave) {
100
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
101
+ const conflicts = [];
102
+
103
+ const waveTasks = wave.taskIds.map((id) => taskMap.get(id)).filter(Boolean);
104
+
105
+ for (let i = 0; i < waveTasks.length; i++) {
106
+ for (let j = i + 1; j < waveTasks.length; j++) {
107
+ const scopeA = waveTasks[i].writeScope || [];
108
+ const scopeB = waveTasks[j].writeScope || [];
109
+
110
+ for (const pathA of scopeA) {
111
+ for (const pathB of scopeB) {
112
+ if (pathA === pathB || pathA.startsWith(pathB + '/') || pathB.startsWith(pathA + '/')) {
113
+ conflicts.push({
114
+ taskA: waveTasks[i].id,
115
+ taskB: waveTasks[j].id,
116
+ path: pathA,
117
+ });
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ return { valid: conflicts.length === 0, conflicts };
125
+ }
126
+
127
+ /**
128
+ * Get a summary of the wave analysis.
129
+ *
130
+ * @param {Wave[]} waves
131
+ * @returns {{ totalWaves: number, totalTasks: number, maxParallel: number, sequential: boolean }}
132
+ */
133
+ export function getWaveSummary(waves) {
134
+ const totalTasks = waves.reduce((sum, w) => sum + w.taskIds.length, 0);
135
+ const maxParallel = Math.max(...waves.map((w) => w.taskIds.length), 0);
136
+
137
+ return {
138
+ totalWaves: waves.length,
139
+ totalTasks,
140
+ maxParallel,
141
+ sequential: waves.every((w) => w.taskIds.length === 1),
142
+ };
143
+ }
@@ -1,5 +0,0 @@
1
- {
2
- "createdAt": "2026-02-17T14:15:05.931Z",
3
- "files": {},
4
- "version": "2.1.2"
5
- }
@@ -1 +0,0 @@
1
- Wjy+M0Wcyt+KTzRu0E4fc8sB97cTe7ye+qEtNGq/av7AzcB2GsSNlwrOhfMV64ukMBHSrBMQ995sDbTtzNKADA==