chati-dev 2.1.2 → 3.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.
- package/README.md +95 -24
- package/bin/chati.js +1 -1
- package/framework/agents/build/dev.md +1 -0
- package/framework/agents/deploy/devops.md +1 -0
- package/framework/agents/discover/brief.md +1 -0
- package/framework/agents/discover/brownfield-wu.md +1 -0
- package/framework/agents/discover/greenfield-wu.md +1 -0
- package/framework/agents/plan/architect.md +1 -0
- package/framework/agents/plan/detail.md +1 -0
- package/framework/agents/plan/phases.md +3 -2
- package/framework/agents/plan/tasks.md +1 -0
- package/framework/agents/plan/ux.md +1 -0
- package/framework/agents/quality/qa-implementation.md +1 -0
- package/framework/agents/quality/qa-planning.md +1 -0
- package/framework/config.yaml +21 -2
- package/framework/constitution.md +67 -2
- package/framework/context/governance.md +16 -3
- package/framework/context/root.md +3 -3
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/domains/agents/brownfield-wu.yaml +4 -0
- package/framework/domains/agents/detail.yaml +1 -1
- package/framework/domains/agents/dev.yaml +4 -0
- package/framework/domains/agents/orchestrator.yaml +8 -0
- package/framework/domains/agents/phases.yaml +2 -2
- package/framework/domains/agents/tasks.yaml +2 -2
- package/framework/domains/constitution.yaml +30 -2
- package/framework/domains/global.yaml +20 -0
- package/framework/hooks/model-governance.js +17 -15
- package/framework/hooks/read-protection.js +1 -0
- package/framework/i18n/en.yaml +11 -4
- package/framework/i18n/es.yaml +11 -4
- package/framework/i18n/fr.yaml +11 -4
- package/framework/i18n/pt.yaml +11 -4
- package/framework/intelligence/context-engine.md +31 -2
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/intelligence/memory-layer.md +17 -0
- package/framework/orchestrator/chati.md +94 -14
- package/framework/patterns/elicitation-library.yaml +2 -2
- package/framework/schemas/config.schema.json +44 -0
- package/framework/schemas/session.schema.json +27 -0
- package/framework/tasks/brownfield-wu-dependency-scan.md +1 -1
- package/framework/tasks/brownfield-wu-migration-plan.md +4 -4
- package/framework/tasks/brownfield-wu-risk-assess.md +2 -2
- package/framework/tasks/detail-expand-prd.md +2 -2
- package/framework/tasks/greenfield-wu-report.md +1 -1
- package/framework/tasks/orchestrator-handoff.md +2 -2
- package/framework/tasks/orchestrator-resume.md +1 -1
- package/framework/tasks/qa-impl-performance-test.md +2 -2
- package/framework/tasks/qa-impl-sast-scan.md +6 -6
- package/framework/tasks/qa-planning-consolidate.md +1 -1
- package/framework/tasks/qa-planning-coverage-plan.md +9 -9
- package/framework/tasks/qa-planning-gate-define.md +6 -6
- package/framework/tasks/qa-planning-risk-matrix.md +1 -1
- package/framework/tasks/tasks-consolidate.md +5 -5
- package/package.json +5 -1
- package/scripts/changelog-generator.js +2 -2
- package/scripts/ide-sync.js +9 -4
- package/scripts/pr-review.js +5 -5
- package/scripts/semantic-lint.js +3 -3
- package/src/autonomy/build-loop.js +194 -0
- package/src/autonomy/build-state.js +269 -0
- package/src/autonomy/execution-profile.js +151 -0
- package/src/config/context-file-generator.js +203 -0
- package/src/config/ide-configs.js +6 -6
- package/src/gates/g2-qa-planning.js +4 -2
- package/src/gates/g4-qa-implementation.js +5 -2
- package/src/gates/gate-base.js +33 -1
- package/src/health/engine.js +245 -0
- package/src/installer/core.js +54 -16
- package/src/installer/templates.js +131 -2
- package/src/installer/validator.js +5 -3
- package/src/intelligence/file-tracker.js +117 -0
- package/src/intelligence/registry-manager.js +2 -2
- package/src/intelligence/timeline.js +144 -0
- package/src/memory/gotchas-auto-capture.js +253 -0
- package/src/preview/detector.js +1 -1
- package/src/terminal/adapters/claude-adapter.js +33 -0
- package/src/terminal/adapters/codex-adapter.js +31 -0
- package/src/terminal/adapters/copilot-adapter.js +28 -0
- package/src/terminal/adapters/gemini-adapter.js +28 -0
- package/src/terminal/adapters/index.js +8 -0
- package/src/terminal/cli-registry.js +190 -0
- package/src/terminal/prompt-builder.js +9 -20
- package/src/terminal/run-agent.js +2 -1
- package/src/terminal/spawner.js +44 -14
- package/src/terminal/wave-analyzer.js +143 -0
- package/src/utils/config-parser.js +97 -0
- package/src/wizard/feedback.js +1 -1
- package/src/wizard/i18n.js +11 -4
- package/src/wizard/index.js +22 -10
- package/src/wizard/questions.js +50 -2
- package/framework/manifest.json +0 -5
- package/framework/manifest.sig +0 -1
- /package/assets/{logo - co/314/201pia.png" → logo - c/303/263pia.png"} +0 -0
- /package/assets/{logo - co/314/201pia.svg" → logo - c/303/263pia.svg"} +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
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 * as adapters from './adapters/index.js';
|
|
10
|
+
import { parseProviderConfig, parseAgentOverride } from '../utils/config-parser.js';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Provider Definitions
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {object} ProviderConfig
|
|
18
|
+
* @property {string} name - Provider identifier
|
|
19
|
+
* @property {string} command - CLI command name
|
|
20
|
+
* @property {string[]} baseArgs - Default CLI arguments for non-interactive mode
|
|
21
|
+
* @property {string} modelFlag - CLI flag for model selection
|
|
22
|
+
* @property {boolean} stdinSupport - Whether prompts can be piped via stdin
|
|
23
|
+
* @property {boolean} hooksSupport - Whether the CLI supports hooks (event middleware)
|
|
24
|
+
* @property {boolean} mcpSupport - Whether the CLI supports MCP servers
|
|
25
|
+
* @property {string|null} contextFile - Project context file name (CLAUDE.md, GEMINI.md, etc.)
|
|
26
|
+
* @property {Record<string, string>} modelMap - Tier-to-model-id mapping
|
|
27
|
+
* @property {object} adapter - CLI-specific adapter module
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** @type {Record<string, ProviderConfig>} */
|
|
31
|
+
const PROVIDERS = {
|
|
32
|
+
claude: {
|
|
33
|
+
name: 'claude',
|
|
34
|
+
command: 'claude',
|
|
35
|
+
baseArgs: ['--print', '--dangerously-skip-permissions'],
|
|
36
|
+
modelFlag: '--model',
|
|
37
|
+
stdinSupport: true,
|
|
38
|
+
hooksSupport: true,
|
|
39
|
+
mcpSupport: true,
|
|
40
|
+
contextFile: 'CLAUDE.md',
|
|
41
|
+
modelMap: {
|
|
42
|
+
opus: 'claude-opus-4-6',
|
|
43
|
+
sonnet: 'claude-sonnet-4-5-20250929',
|
|
44
|
+
haiku: 'claude-haiku-4-5-20251001',
|
|
45
|
+
},
|
|
46
|
+
adapter: adapters.claude,
|
|
47
|
+
},
|
|
48
|
+
gemini: {
|
|
49
|
+
name: 'gemini',
|
|
50
|
+
command: 'gemini',
|
|
51
|
+
baseArgs: [],
|
|
52
|
+
modelFlag: '--model',
|
|
53
|
+
stdinSupport: true,
|
|
54
|
+
hooksSupport: false,
|
|
55
|
+
mcpSupport: true,
|
|
56
|
+
contextFile: 'GEMINI.md',
|
|
57
|
+
modelMap: {
|
|
58
|
+
pro: 'gemini-2.5-pro',
|
|
59
|
+
flash: 'gemini-2.5-flash',
|
|
60
|
+
},
|
|
61
|
+
adapter: adapters.gemini,
|
|
62
|
+
},
|
|
63
|
+
codex: {
|
|
64
|
+
name: 'codex',
|
|
65
|
+
command: 'codex',
|
|
66
|
+
baseArgs: ['exec'],
|
|
67
|
+
modelFlag: '-m',
|
|
68
|
+
stdinSupport: true,
|
|
69
|
+
hooksSupport: false,
|
|
70
|
+
mcpSupport: true,
|
|
71
|
+
contextFile: 'AGENTS.md',
|
|
72
|
+
modelMap: {
|
|
73
|
+
codex: 'gpt-5.3-codex',
|
|
74
|
+
mini: 'codex-mini-latest',
|
|
75
|
+
},
|
|
76
|
+
adapter: adapters.codex,
|
|
77
|
+
},
|
|
78
|
+
copilot: {
|
|
79
|
+
name: 'copilot',
|
|
80
|
+
command: 'copilot',
|
|
81
|
+
baseArgs: ['-p'],
|
|
82
|
+
modelFlag: '--model',
|
|
83
|
+
stdinSupport: true,
|
|
84
|
+
hooksSupport: false,
|
|
85
|
+
mcpSupport: true,
|
|
86
|
+
contextFile: null,
|
|
87
|
+
modelMap: {
|
|
88
|
+
'claude-sonnet': 'claude-sonnet-4.5',
|
|
89
|
+
'gpt-5': 'gpt-5',
|
|
90
|
+
},
|
|
91
|
+
adapter: adapters.copilot,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Provider Resolution
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get a provider configuration by name.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} name - Provider name (claude, gemini, codex, copilot)
|
|
103
|
+
* @returns {ProviderConfig}
|
|
104
|
+
* @throws {Error} When provider is not found
|
|
105
|
+
*/
|
|
106
|
+
export function getProvider(name) {
|
|
107
|
+
const provider = PROVIDERS[name];
|
|
108
|
+
if (!provider) {
|
|
109
|
+
throw new Error(`Unknown CLI provider: "${name}". Available: ${Object.keys(PROVIDERS).join(', ')}`);
|
|
110
|
+
}
|
|
111
|
+
return provider;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get all registered providers.
|
|
116
|
+
*
|
|
117
|
+
* @returns {Record<string, ProviderConfig>}
|
|
118
|
+
*/
|
|
119
|
+
export function getAllProviders() {
|
|
120
|
+
return { ...PROVIDERS };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Load enabled providers from project config.yaml.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} projectDir - Project root directory
|
|
127
|
+
* @returns {{ primary: string, enabled: string[] }}
|
|
128
|
+
*/
|
|
129
|
+
export function loadEnabledProviders(projectDir) {
|
|
130
|
+
const { primary, enabled } = parseProviderConfig(projectDir);
|
|
131
|
+
return { primary, enabled };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Resolve which provider should be used for a given agent.
|
|
136
|
+
* Priority: agent_overrides > agent default > primary provider.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} agent - Agent name
|
|
139
|
+
* @param {string} projectDir - Project root directory
|
|
140
|
+
* @param {Record<string, {provider: string, model: string, tier: string}>} agentModels - Agent model assignments
|
|
141
|
+
* @returns {{ provider: string, model: string }}
|
|
142
|
+
*/
|
|
143
|
+
export function resolveProviderForAgent(agent, projectDir, agentModels) {
|
|
144
|
+
const { primary, enabled } = loadEnabledProviders(projectDir);
|
|
145
|
+
|
|
146
|
+
// Check agent_overrides in config.yaml (block-style YAML)
|
|
147
|
+
const { raw } = parseProviderConfig(projectDir);
|
|
148
|
+
if (raw) {
|
|
149
|
+
const override = parseAgentOverride(raw, agent);
|
|
150
|
+
if (override && enabled.includes(override.provider)) {
|
|
151
|
+
return override;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Use agent's default assignment
|
|
156
|
+
const agentConfig = agentModels[agent];
|
|
157
|
+
if (agentConfig) {
|
|
158
|
+
const agentProvider = agentConfig.provider || primary;
|
|
159
|
+
if (enabled.includes(agentProvider)) {
|
|
160
|
+
return { provider: agentProvider, model: agentConfig.model };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Fallback to primary provider with its default light-tier model
|
|
165
|
+
const provider = PROVIDERS[primary];
|
|
166
|
+
const defaultModel = provider ? Object.keys(provider.modelMap)[0] : 'sonnet';
|
|
167
|
+
return { provider: primary, model: defaultModel };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Check if a provider CLI is available on the system.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} name - Provider name
|
|
174
|
+
* @returns {Promise<boolean>}
|
|
175
|
+
*/
|
|
176
|
+
export async function isProviderAvailable(name) {
|
|
177
|
+
const provider = PROVIDERS[name];
|
|
178
|
+
if (!provider) return false;
|
|
179
|
+
|
|
180
|
+
const { execFileSync } = await import('child_process');
|
|
181
|
+
try {
|
|
182
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
183
|
+
execFileSync(cmd, [provider.command], { stdio: 'ignore' });
|
|
184
|
+
return true;
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export { PROVIDERS };
|
|
@@ -14,24 +14,10 @@ import { runPrism } from '../context/engine.js';
|
|
|
14
14
|
import { loadHandoff, formatHandoff } from '../tasks/handoff.js';
|
|
15
15
|
import { getWriteScope } from './isolation.js';
|
|
16
16
|
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
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',
|
|
34
|
-
};
|
|
17
|
+
// Import DEFAULT_AGENT_MODELS from model-governance (safe — named export,
|
|
18
|
+
// does not trigger main() which is guarded by fileURLToPath check).
|
|
19
|
+
import { DEFAULT_AGENT_MODELS } from '../../framework/hooks/model-governance.js';
|
|
20
|
+
const AGENT_MODELS = DEFAULT_AGENT_MODELS;
|
|
35
21
|
|
|
36
22
|
/**
|
|
37
23
|
* Map of agent names to their definition file paths (relative to project root).
|
|
@@ -116,11 +102,13 @@ export function buildAgentPrompt(config) {
|
|
|
116
102
|
sections.push(buildOutputInstructions());
|
|
117
103
|
|
|
118
104
|
const prompt = sections.join('\n\n---\n\n');
|
|
119
|
-
const
|
|
105
|
+
const assignment = AGENT_MODELS[config.agent] || { provider: 'claude', model: 'sonnet', tier: 'sonnet' };
|
|
106
|
+
const model = assignment.model || assignment;
|
|
120
107
|
|
|
121
108
|
return {
|
|
122
109
|
prompt,
|
|
123
110
|
model,
|
|
111
|
+
provider: assignment.provider || 'claude',
|
|
124
112
|
metadata: {
|
|
125
113
|
agent: config.agent,
|
|
126
114
|
layers: prismResult.layerCount || 0,
|
|
@@ -266,7 +254,8 @@ function buildSessionSection(config) {
|
|
|
266
254
|
`- **User Level**: ${state.user_level || 'auto'}`,
|
|
267
255
|
`- **Execution Mode**: ${state.execution_mode || 'autonomous'}`,
|
|
268
256
|
`- **Your Agent**: ${config.agent}`,
|
|
269
|
-
`- **Your Model**: ${AGENT_MODELS[config.agent] || 'sonnet'}`,
|
|
257
|
+
`- **Your Model**: ${(AGENT_MODELS[config.agent]?.model) || 'sonnet'}`,
|
|
258
|
+
`- **Your Provider**: ${(AGENT_MODELS[config.agent]?.provider) || 'claude'}`,
|
|
270
259
|
];
|
|
271
260
|
|
|
272
261
|
return lines.join('\n');
|
package/src/terminal/spawner.js
CHANGED
|
@@ -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
|
|
@@ -42,7 +43,8 @@ export function _resetCounter() {
|
|
|
42
43
|
* @typedef {object} SpawnConfig
|
|
43
44
|
* @property {string} agent - Agent name (e.g. "architect")
|
|
44
45
|
* @property {string} taskId - Task identifier
|
|
45
|
-
* @property {string} [model] - LLM model (
|
|
46
|
+
* @property {string} [model] - LLM model tier name (e.g. opus, pro, codex, claude-sonnet)
|
|
47
|
+
* @property {string} [provider] - CLI provider name (claude, gemini, codex, copilot)
|
|
46
48
|
* @property {string} [prompt] - Full prompt string (from prompt-builder, piped via stdin)
|
|
47
49
|
* @property {object} [contextPayload] - Context to inject via env var
|
|
48
50
|
* @property {string[]} [writeScope] - Override write scope
|
|
@@ -103,17 +105,29 @@ export function buildSpawnCommand(config) {
|
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
//
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
// Resolve CLI provider — defaults to claude for backwards compatibility
|
|
109
|
+
const providerName = config.provider || 'claude';
|
|
110
|
+
let command, args, prompt;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const provider = getProvider(providerName);
|
|
114
|
+
const adapterResult = provider.adapter.buildCommand(config, provider);
|
|
115
|
+
command = adapterResult.command;
|
|
116
|
+
args = adapterResult.args;
|
|
117
|
+
prompt = adapterResult.stdinPrompt;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
// Fallback to claude if provider resolution fails (backwards compatibility)
|
|
120
|
+
console.error(`[chati] Provider "${providerName}" resolution failed: ${err.message}. Falling back to claude.`);
|
|
121
|
+
command = 'claude';
|
|
122
|
+
args = ['--print', '--dangerously-skip-permissions'];
|
|
123
|
+
if (config.model) {
|
|
124
|
+
const claudeProvider = getProvider('claude');
|
|
125
|
+
const resolvedModel = claudeProvider.modelMap[config.model] || config.model;
|
|
126
|
+
args.push('--model', resolvedModel);
|
|
127
|
+
}
|
|
128
|
+
prompt = config.prompt || null;
|
|
112
129
|
}
|
|
113
130
|
|
|
114
|
-
// Prompt is returned separately for stdin piping (avoids ARG_MAX limits)
|
|
115
|
-
const prompt = config.prompt || null;
|
|
116
|
-
|
|
117
131
|
return { command, args, env, terminalId, prompt };
|
|
118
132
|
}
|
|
119
133
|
|
|
@@ -147,7 +161,7 @@ export function spawnTerminal(config) {
|
|
|
147
161
|
process: child,
|
|
148
162
|
agent: config.agent,
|
|
149
163
|
taskId: config.taskId,
|
|
150
|
-
model: config.model || '
|
|
164
|
+
model: config.model || 'unknown',
|
|
151
165
|
startedAt: new Date().toISOString(),
|
|
152
166
|
status: 'running',
|
|
153
167
|
exitCode: null,
|
|
@@ -156,15 +170,20 @@ export function spawnTerminal(config) {
|
|
|
156
170
|
timeout,
|
|
157
171
|
};
|
|
158
172
|
|
|
159
|
-
// Capture output
|
|
173
|
+
// Capture output (capped at ~10MB to prevent unbounded memory growth)
|
|
174
|
+
const MAX_BUFFER_CHUNKS = 10_000;
|
|
160
175
|
if (child.stdout) {
|
|
161
176
|
child.stdout.on('data', (chunk) => {
|
|
162
|
-
handle.stdout.
|
|
177
|
+
if (handle.stdout.length < MAX_BUFFER_CHUNKS) {
|
|
178
|
+
handle.stdout.push(chunk.toString());
|
|
179
|
+
}
|
|
163
180
|
});
|
|
164
181
|
}
|
|
165
182
|
if (child.stderr) {
|
|
166
183
|
child.stderr.on('data', (chunk) => {
|
|
167
|
-
handle.stderr.
|
|
184
|
+
if (handle.stderr.length < MAX_BUFFER_CHUNKS) {
|
|
185
|
+
handle.stderr.push(chunk.toString());
|
|
186
|
+
}
|
|
168
187
|
});
|
|
169
188
|
}
|
|
170
189
|
|
|
@@ -179,6 +198,17 @@ export function spawnTerminal(config) {
|
|
|
179
198
|
handle.stderr.push(`spawn error: ${err.message}`);
|
|
180
199
|
});
|
|
181
200
|
|
|
201
|
+
// Enforce timeout — kill process if it exceeds max execution time
|
|
202
|
+
const timeoutTimer = setTimeout(() => {
|
|
203
|
+
if (handle.status === 'running') {
|
|
204
|
+
handle.stderr.push(`timeout: process exceeded ${timeout}ms limit`);
|
|
205
|
+
killTerminal(handle);
|
|
206
|
+
}
|
|
207
|
+
}, timeout);
|
|
208
|
+
|
|
209
|
+
// Clear timer when process exits normally
|
|
210
|
+
child.on('exit', () => clearTimeout(timeoutTimer));
|
|
211
|
+
|
|
182
212
|
return handle;
|
|
183
213
|
}
|
|
184
214
|
|
|
@@ -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
|
+
const 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
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared YAML config parser for provider configuration.
|
|
3
|
+
*
|
|
4
|
+
* Extracts provider enabled/primary status and agent overrides from
|
|
5
|
+
* config.yaml using lightweight regex-based parsing. Used by
|
|
6
|
+
* cli-registry.js, context-file-generator.js, and health/engine.js.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse provider configuration from config.yaml.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} projectDir - Project root directory
|
|
16
|
+
* @returns {{ primary: string, enabled: string[], raw: string|null }}
|
|
17
|
+
*/
|
|
18
|
+
export function parseProviderConfig(projectDir) {
|
|
19
|
+
const configPath = join(projectDir, 'chati.dev', 'config.yaml');
|
|
20
|
+
if (!existsSync(configPath)) {
|
|
21
|
+
return { primary: 'claude', enabled: ['claude'], raw: null };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
25
|
+
const providers = ['claude', 'gemini', 'codex', 'copilot'];
|
|
26
|
+
const enabled = [];
|
|
27
|
+
let primary = 'claude';
|
|
28
|
+
|
|
29
|
+
for (const name of providers) {
|
|
30
|
+
const enabledMatch = raw.match(new RegExp(`${name}:[\\s\\S]*?enabled:\\s*(true|false)`, 'm'));
|
|
31
|
+
if (enabledMatch && enabledMatch[1] === 'true') {
|
|
32
|
+
enabled.push(name);
|
|
33
|
+
}
|
|
34
|
+
const primaryMatch = raw.match(new RegExp(`${name}:[\\s\\S]*?primary:\\s*(true|false)`, 'm'));
|
|
35
|
+
if (primaryMatch && primaryMatch[1] === 'true') {
|
|
36
|
+
primary = name;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Claude is always enabled as fallback
|
|
41
|
+
if (!enabled.includes('claude')) {
|
|
42
|
+
enabled.unshift('claude');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { primary, enabled, raw };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse agent override block from config.yaml.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} raw - Raw config.yaml content
|
|
52
|
+
* @param {string} agent - Agent name
|
|
53
|
+
* @returns {{ provider: string, model: string }|null}
|
|
54
|
+
*/
|
|
55
|
+
export function parseAgentOverride(raw, agent) {
|
|
56
|
+
const escaped = agent.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
57
|
+
const overrideBlock = raw.match(new RegExp(`^\\s+${escaped}:\\s*\\n((?:\\s+\\w+:.*\\n?)*)`, 'm'));
|
|
58
|
+
if (!overrideBlock) return null;
|
|
59
|
+
|
|
60
|
+
const block = overrideBlock[1];
|
|
61
|
+
const providerMatch = block.match(/provider:\s*(\S+)/);
|
|
62
|
+
const modelMatch = block.match(/model:\s*(\S+)/);
|
|
63
|
+
|
|
64
|
+
if (providerMatch && modelMatch) {
|
|
65
|
+
return { provider: providerMatch[1], model: modelMatch[1] };
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get list of enabled non-Claude providers.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} projectDir - Project root directory
|
|
74
|
+
* @returns {string[]} List of enabled provider names (excludes claude)
|
|
75
|
+
*/
|
|
76
|
+
export function getEnabledNonClaudeProviders(projectDir) {
|
|
77
|
+
const { enabled } = parseProviderConfig(projectDir);
|
|
78
|
+
return enabled.filter(p => p !== 'claude');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if a CLI command is available on the system.
|
|
83
|
+
* Cross-platform: uses 'which' on Unix, 'where' on Windows.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} command - CLI command name
|
|
86
|
+
* @returns {Promise<boolean>}
|
|
87
|
+
*/
|
|
88
|
+
export async function isCommandAvailable(command) {
|
|
89
|
+
const { execFileSync } = await import('child_process');
|
|
90
|
+
try {
|
|
91
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
92
|
+
execFileSync(cmd, [command], { stdio: 'ignore' });
|
|
93
|
+
return true;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/wizard/feedback.js
CHANGED
|
@@ -60,7 +60,7 @@ export function showQuickStart(title, steps) {
|
|
|
60
60
|
export function showSummary(data) {
|
|
61
61
|
console.log();
|
|
62
62
|
for (const [label, value] of Object.entries(data)) {
|
|
63
|
-
const paddedLabel = label.padEnd(
|
|
63
|
+
const paddedLabel = label.padEnd(16);
|
|
64
64
|
console.log(` ${dim(paddedLabel)} ${value}`);
|
|
65
65
|
}
|
|
66
66
|
}
|
package/src/wizard/i18n.js
CHANGED
|
@@ -20,19 +20,26 @@ const FALLBACK_EN = {
|
|
|
20
20
|
language_label: 'Language',
|
|
21
21
|
ides_label: 'IDEs',
|
|
22
22
|
mcps_label: 'MCPs',
|
|
23
|
+
llm_provider_title: 'Select your default AI provider:',
|
|
24
|
+
llm_provider_claude: 'Claude (Anthropic) — Best for structured reasoning',
|
|
25
|
+
llm_provider_gemini: 'Gemini (Google) — 1M token context window',
|
|
26
|
+
llm_provider_codex: 'Codex (OpenAI) — Optimized for code generation',
|
|
27
|
+
llm_provider_copilot: 'Copilot (GitHub) — Multi-model, GitHub integrated',
|
|
28
|
+
llm_provider_not_installed: '(CLI not detected)',
|
|
29
|
+
llm_provider_label: 'AI Provider',
|
|
23
30
|
will_install: 'Will install:',
|
|
24
31
|
agents_count: '13 agent definitions (DISCOVER, PLAN, BUILD, DEPLOY phases)',
|
|
25
32
|
workflows_count: '6 workflow blueprints',
|
|
26
33
|
templates_count: '6 templates (PRD, Brownfield PRD, Architecture, Task, QA Gate, Quick Brief)',
|
|
27
|
-
constitution: 'Constitution (
|
|
34
|
+
constitution: 'Constitution (19 Articles + Preamble)',
|
|
28
35
|
session_mgmt: 'Session management system',
|
|
29
|
-
quality_gates: 'Quality gates (
|
|
36
|
+
quality_gates: 'Quality gates (3-tier verdicts)',
|
|
30
37
|
proceed: 'Proceed with installation?',
|
|
31
38
|
installing: 'Installing Chati.dev...',
|
|
32
39
|
created_chati: 'Created .chati/ session directory',
|
|
33
40
|
created_framework: 'Created chati.dev/ system directory (agents, templates, workflows)',
|
|
34
41
|
created_commands: 'Created .claude/commands/ (thin router)',
|
|
35
|
-
installed_constitution: 'Installed Constitution (Articles I-
|
|
42
|
+
installed_constitution: 'Installed Constitution (Articles I-XIX)',
|
|
36
43
|
created_session: 'Created session.yaml schema',
|
|
37
44
|
created_claude_md: 'Created CLAUDE.md',
|
|
38
45
|
configured_mcps: 'Configured MCPs:',
|
|
@@ -40,7 +47,7 @@ const FALLBACK_EN = {
|
|
|
40
47
|
agents_valid: 'All 13 agents implement 8 protocols',
|
|
41
48
|
handoff_ok: 'Handoff protocol: OK',
|
|
42
49
|
validation_ok: 'Self-validation criteria: OK',
|
|
43
|
-
constitution_ok: 'Constitution:
|
|
50
|
+
constitution_ok: 'Constitution: 19 articles verified',
|
|
44
51
|
created_memories: 'Created .chati/memories/ (Memory Layer)',
|
|
45
52
|
installed_intelligence: 'Installed Intelligence Layer (Context Engine, Memory, Registry)',
|
|
46
53
|
intelligence_valid: 'Intelligence: 6 spec files verified',
|