chati-dev 3.2.4 → 3.3.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.
- package/LICENSE +96 -0
- package/README.md +2 -2
- package/bin/chati.js +46 -0
- package/framework/agents/build/dev.md +122 -1
- package/framework/agents/deploy/devops.md +128 -3
- package/framework/agents/discover/brief.md +77 -15
- package/framework/agents/discover/brownfield-wu.md +2 -2
- package/framework/agents/discover/greenfield-wu.md +3 -3
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +3 -3
- package/framework/agents/plan/phases.md +127 -2
- package/framework/agents/plan/tasks.md +127 -2
- package/framework/agents/plan/ux.md +269 -22
- package/framework/agents/quality/qa-implementation.md +172 -8
- package/framework/agents/quality/qa-planning.md +147 -2
- package/framework/config.yaml +9 -5
- package/framework/constitution.md +7 -1
- package/framework/context/quality.md +1 -1
- package/framework/context/root.md +1 -1
- package/framework/hooks/constitution-guard.js +18 -2
- package/framework/hooks/mode-governance.js +3 -3
- package/framework/hooks/read-protection.js +10 -2
- package/framework/i18n/en.yaml +6 -0
- package/framework/i18n/es.yaml +6 -0
- package/framework/i18n/fr.yaml +6 -0
- package/framework/i18n/pt.yaml +6 -0
- package/framework/orchestrator/chati.md +102 -6
- package/framework/schemas/task.schema.json +1 -1
- package/framework/tasks/architect-dep-audit.md +128 -0
- package/framework/tasks/architect-stack-selection.md +28 -0
- package/framework/workflows/brownfield-fullstack.yaml +2 -2
- package/framework/workflows/brownfield-service.yaml +2 -2
- package/framework/workflows/brownfield-ui.yaml +2 -2
- package/framework/workflows/greenfield-fullstack.yaml +6 -2
- package/framework/workflows/quick-flow.yaml +7 -5
- package/framework/workflows/standard-flow.yaml +171 -0
- package/package.json +5 -3
- package/src/api/index.js +129 -0
- package/src/autonomy/build-loop.js +93 -6
- package/src/autonomy/build-state.js +20 -2
- package/src/autonomy/cause-analyzer.js +177 -0
- package/src/autonomy/escalation.js +214 -0
- package/src/autonomy/safety-net.js +23 -5
- package/src/autonomy/worktree-manager.js +245 -0
- package/src/config/agent-customizer.js +227 -0
- package/src/config/ide-configs.js +57 -27
- package/src/decision/analyzer.js +148 -0
- package/src/decision/registry-healer.js +38 -21
- package/src/extensions/loader.js +151 -0
- package/src/extensions/registry.js +134 -0
- package/src/gates/circuit-breaker.js +32 -0
- package/src/gates/g3-implementation.js +30 -4
- package/src/gates/g4-qa-implementation.js +34 -5
- package/src/gates/gate-base.js +9 -0
- package/src/health/auto-fix.js +216 -0
- package/src/installer/core.js +24 -11
- package/src/installer/provider-overlay.js +82 -0
- package/src/installer/templates.js +22 -10
- package/src/installer/transaction.js +3 -2
- package/src/installer/validator.js +74 -0
- package/src/intelligence/context-status.js +9 -5
- package/src/intelligence/document-sharder.js +221 -0
- package/src/intelligence/elicitation.js +265 -0
- package/src/intelligence/timeline.js +5 -0
- package/src/memory/gotchas.js +78 -2
- package/src/merger/semantic-merger.js +292 -0
- package/src/orchestrator/agent-selector.js +20 -0
- package/src/orchestrator/handoff-engine.js +77 -0
- package/src/orchestrator/index.js +0 -8
- package/src/orchestrator/intent-classifier.js +182 -0
- package/src/orchestrator/pipeline-manager.js +125 -1
- package/src/orchestrator/session-manager.js +164 -2
- package/src/quality/metrics-collector.js +283 -0
- package/src/quality/test-runner.js +368 -0
- package/src/telemetry/collector.js +83 -0
- package/src/telemetry/config.js +119 -0
- package/src/telemetry/index.js +11 -0
- package/src/telemetry/schema.js +104 -0
- package/src/telemetry/sender.js +60 -0
- package/src/terminal/cli-registry.js +7 -1
- package/src/terminal/cost-tracker.js +197 -0
- package/src/terminal/handoff-parser.js +61 -4
- package/src/terminal/prompt-builder.js +56 -18
- package/src/terminal/rate-limiter.js +172 -0
- package/src/terminal/run-agent.js +39 -0
- package/src/terminal/run-parallel.js +22 -1
- package/src/terminal/spawner.js +181 -3
- package/src/upgrade/migrator.js +2 -2
- package/src/utils/event-bus.js +126 -0
- package/src/utils/file-lock.js +291 -0
- package/src/utils/schema-validator.js +226 -0
- package/src/wizard/i18n.js +11 -0
- package/src/wizard/index.js +42 -20
- package/src/wizard/questions.js +200 -39
- package/src/autonomy/execution-profile.js +0 -151
- package/src/intelligence/file-tracker.js +0 -117
- package/src/memory/gotchas-auto-capture.js +0 -253
- package/src/orchestrator/pipeline-state.js +0 -223
- package/src/terminal/wave-analyzer.js +0 -143
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Git worktree isolation for build loop retries.
|
|
3
|
+
*
|
|
4
|
+
* Each retry attempt can optionally run in an isolated git worktree,
|
|
5
|
+
* preventing failed attempts from corrupting the main working directory.
|
|
6
|
+
*
|
|
7
|
+
* Constitution Article XVII — Execution Mode Governance.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @deprecated Not currently imported by any production module.
|
|
12
|
+
* Retained for potential future integration. Remove if still unused by v4.0.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
import { existsSync, mkdirSync, readdirSync, statSync, rmSync } from 'fs';
|
|
17
|
+
import { join, resolve } from 'path';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Constants
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** Directory for worktrees inside .chati/ */
|
|
24
|
+
const WORKTREE_DIR = '.chati/worktrees';
|
|
25
|
+
|
|
26
|
+
/** Maximum age (ms) before a worktree is considered stale (24h). */
|
|
27
|
+
const MAX_WORKTREE_AGE_MS = 24 * 60 * 60 * 1000;
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Execute a git command in a given directory and return trimmed stdout.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} cmd - Git command (without the `git` prefix)
|
|
37
|
+
* @param {string} cwd - Working directory
|
|
38
|
+
* @returns {string} Trimmed stdout
|
|
39
|
+
*/
|
|
40
|
+
function git(cmd, cwd) {
|
|
41
|
+
return execSync(`git ${cmd}`, {
|
|
42
|
+
cwd,
|
|
43
|
+
encoding: 'utf-8',
|
|
44
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
45
|
+
}).trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Public API
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if a directory is inside a git repository.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} dir - Directory path
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
export function isGitRepo(dir) {
|
|
59
|
+
try {
|
|
60
|
+
git('rev-parse --is-inside-work-tree', dir);
|
|
61
|
+
return true;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create an isolated git worktree for a retry attempt.
|
|
69
|
+
*
|
|
70
|
+
* The worktree is placed at `.chati/worktrees/<taskId>-attempt-<N>`.
|
|
71
|
+
* A new branch is created from the current HEAD.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} projectDir - Root project directory
|
|
74
|
+
* @param {string} taskId - Task identifier
|
|
75
|
+
* @param {number} attempt - Attempt number
|
|
76
|
+
* @returns {{ path: string, branch: string, cleanup: () => void }}
|
|
77
|
+
* @throws {Error} If not a git repo or worktree creation fails
|
|
78
|
+
*/
|
|
79
|
+
export function createWorktree(projectDir, taskId, attempt) {
|
|
80
|
+
const absProject = resolve(projectDir);
|
|
81
|
+
|
|
82
|
+
if (!isGitRepo(absProject)) {
|
|
83
|
+
throw new Error(`Not a git repository: ${absProject}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const safeName = `${taskId}-attempt-${attempt}`.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
87
|
+
const worktreeBase = join(absProject, WORKTREE_DIR);
|
|
88
|
+
const worktreePath = join(worktreeBase, safeName);
|
|
89
|
+
const branch = `chati/${safeName}`;
|
|
90
|
+
|
|
91
|
+
// Ensure worktree base directory exists
|
|
92
|
+
mkdirSync(worktreeBase, { recursive: true });
|
|
93
|
+
|
|
94
|
+
// Clean up if a worktree already exists at this path
|
|
95
|
+
if (existsSync(worktreePath)) {
|
|
96
|
+
try {
|
|
97
|
+
git(`worktree remove "${worktreePath}" --force`, absProject);
|
|
98
|
+
} catch {
|
|
99
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Delete branch if it exists from a previous run
|
|
104
|
+
try {
|
|
105
|
+
git(`branch -D "${branch}"`, absProject);
|
|
106
|
+
} catch {
|
|
107
|
+
// Branch doesn't exist — fine
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Create worktree with new branch from HEAD
|
|
111
|
+
git(`worktree add -b "${branch}" "${worktreePath}"`, absProject);
|
|
112
|
+
|
|
113
|
+
const cleanup = () => {
|
|
114
|
+
try {
|
|
115
|
+
git(`worktree remove "${worktreePath}" --force`, absProject);
|
|
116
|
+
} catch {
|
|
117
|
+
// Worktree already removed — ignore
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
git(`branch -D "${branch}"`, absProject);
|
|
121
|
+
} catch {
|
|
122
|
+
// Branch already deleted — ignore
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return { path: worktreePath, branch, cleanup };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Merge changes from a worktree branch back to the main branch.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} projectDir - Root project directory
|
|
133
|
+
* @param {string} worktreeBranch - Worktree branch name
|
|
134
|
+
* @param {string} [mainBranch] - Target branch (auto-detected if omitted)
|
|
135
|
+
* @returns {{ success: boolean, conflicts: string[] }}
|
|
136
|
+
*/
|
|
137
|
+
export function mergeWorktree(projectDir, worktreeBranch, mainBranch) {
|
|
138
|
+
const absProject = resolve(projectDir);
|
|
139
|
+
|
|
140
|
+
// Auto-detect main branch if not specified
|
|
141
|
+
if (!mainBranch) {
|
|
142
|
+
try {
|
|
143
|
+
mainBranch = git('rev-parse --abbrev-ref HEAD', absProject);
|
|
144
|
+
} catch {
|
|
145
|
+
mainBranch = 'main';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Ensure we're on the main branch
|
|
150
|
+
try {
|
|
151
|
+
git(`checkout "${mainBranch}"`, absProject);
|
|
152
|
+
} catch {
|
|
153
|
+
return { success: false, conflicts: [`Failed to checkout ${mainBranch}`] };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Attempt merge
|
|
157
|
+
try {
|
|
158
|
+
git(`merge "${worktreeBranch}" --no-edit`, absProject);
|
|
159
|
+
return { success: true, conflicts: [] };
|
|
160
|
+
} catch (err) {
|
|
161
|
+
// Parse conflicts from error output
|
|
162
|
+
const conflicts = [];
|
|
163
|
+
try {
|
|
164
|
+
const status = git('diff --name-only --diff-filter=U', absProject);
|
|
165
|
+
if (status) {
|
|
166
|
+
conflicts.push(...status.split('\n').filter(Boolean));
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
conflicts.push('Unknown conflict — check git status');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Abort the failed merge
|
|
173
|
+
try {
|
|
174
|
+
git('merge --abort', absProject);
|
|
175
|
+
} catch {
|
|
176
|
+
// Already aborted or not in merging state
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { success: false, conflicts };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Clean up stale worktrees that exceed the maximum age.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} projectDir - Root project directory
|
|
187
|
+
* @param {{ maxAge?: number }} [options={}]
|
|
188
|
+
* @returns {{ cleaned: string[], errors: string[] }}
|
|
189
|
+
*/
|
|
190
|
+
export function cleanupWorktrees(projectDir, options = {}) {
|
|
191
|
+
const absProject = resolve(projectDir);
|
|
192
|
+
const maxAge = options.maxAge || MAX_WORKTREE_AGE_MS;
|
|
193
|
+
const worktreeBase = join(absProject, WORKTREE_DIR);
|
|
194
|
+
const cleaned = [];
|
|
195
|
+
const errors = [];
|
|
196
|
+
|
|
197
|
+
if (!existsSync(worktreeBase)) {
|
|
198
|
+
return { cleaned, errors };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const now = Date.now();
|
|
202
|
+
let entries;
|
|
203
|
+
try {
|
|
204
|
+
entries = readdirSync(worktreeBase);
|
|
205
|
+
} catch {
|
|
206
|
+
return { cleaned, errors: ['Failed to read worktree directory'] };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
const entryPath = join(worktreeBase, entry);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const stat = statSync(entryPath);
|
|
214
|
+
if (!stat.isDirectory()) continue;
|
|
215
|
+
|
|
216
|
+
const age = now - stat.mtimeMs;
|
|
217
|
+
if (age > maxAge) {
|
|
218
|
+
try {
|
|
219
|
+
git(`worktree remove "${entryPath}" --force`, absProject);
|
|
220
|
+
} catch {
|
|
221
|
+
rmSync(entryPath, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Also remove the branch
|
|
225
|
+
const branch = `chati/${entry}`;
|
|
226
|
+
try {
|
|
227
|
+
git(`branch -D "${branch}"`, absProject);
|
|
228
|
+
} catch {
|
|
229
|
+
// Branch already deleted
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
cleaned.push(entry);
|
|
233
|
+
}
|
|
234
|
+
} catch (err) {
|
|
235
|
+
errors.push(`${entry}: ${err.message}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { cleaned, errors };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Exported constants for testing.
|
|
244
|
+
*/
|
|
245
|
+
export { WORKTREE_DIR, MAX_WORKTREE_AGE_MS };
|
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Constants
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** Directory relative to project root where customization files are stored. */
|
|
19
|
+
const CUSTOMIZE_DIR = 'chati.dev/customize';
|
|
20
|
+
|
|
21
|
+
/** Valid override fields that customizations can set. */
|
|
22
|
+
export const VALID_OVERRIDE_FIELDS = [
|
|
23
|
+
'model',
|
|
24
|
+
'extra_context',
|
|
25
|
+
'skip_sections',
|
|
26
|
+
'add_sections',
|
|
27
|
+
'provider',
|
|
28
|
+
'timeout',
|
|
29
|
+
'max_iterations',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Public API
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} Customization
|
|
38
|
+
* @property {boolean} found - Whether a customization file was found
|
|
39
|
+
* @property {string} [model] - Override model name
|
|
40
|
+
* @property {string} [extra_context] - Additional context to inject
|
|
41
|
+
* @property {string[]} [skip_sections] - Sections to omit from prompt
|
|
42
|
+
* @property {{ name: string, content: string }[]} [add_sections] - Sections to append
|
|
43
|
+
* @property {string} [provider] - Override provider name
|
|
44
|
+
* @property {number} [timeout] - Override timeout in ms
|
|
45
|
+
* @property {number} [max_iterations] - Override max iterations
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Load customization for a specific agent.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} projectDir - Project root directory
|
|
52
|
+
* @param {string} agentName - Agent name (e.g., 'brief', 'dev')
|
|
53
|
+
* @returns {{ found: boolean, overrides: Customization }}
|
|
54
|
+
*/
|
|
55
|
+
export function loadCustomization(projectDir, agentName) {
|
|
56
|
+
if (!projectDir || !agentName) {
|
|
57
|
+
return { found: false, overrides: {} };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const customizePath = join(projectDir, CUSTOMIZE_DIR, `${agentName}.yaml`);
|
|
61
|
+
|
|
62
|
+
if (!existsSync(customizePath)) {
|
|
63
|
+
return { found: false, overrides: {} };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const raw = readFileSync(customizePath, 'utf-8');
|
|
68
|
+
const parsed = yaml.load(raw);
|
|
69
|
+
|
|
70
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
71
|
+
return { found: true, overrides: {} };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Filter to valid override fields only
|
|
75
|
+
const overrides = {};
|
|
76
|
+
for (const field of VALID_OVERRIDE_FIELDS) {
|
|
77
|
+
if (parsed[field] !== undefined) {
|
|
78
|
+
overrides[field] = parsed[field];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { found: true, overrides };
|
|
83
|
+
} catch {
|
|
84
|
+
return { found: false, overrides: {} };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Apply customization overrides to an agent prompt.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} agentPrompt - Original agent prompt
|
|
92
|
+
* @param {Customization} customization - Customization overrides
|
|
93
|
+
* @returns {string} Modified prompt
|
|
94
|
+
*/
|
|
95
|
+
export function applyCustomization(agentPrompt, customization) {
|
|
96
|
+
if (!agentPrompt || !customization) {
|
|
97
|
+
return agentPrompt || '';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let prompt = agentPrompt;
|
|
101
|
+
|
|
102
|
+
// Skip sections
|
|
103
|
+
if (customization.skip_sections && Array.isArray(customization.skip_sections)) {
|
|
104
|
+
for (const sectionName of customization.skip_sections) {
|
|
105
|
+
// Match section heading + all content until the next heading of same or higher level, or end of text
|
|
106
|
+
const pattern = new RegExp(
|
|
107
|
+
`^#{1,3}\\s+${escapeRegex(sectionName)}\\s*\\n[\\s\\S]*?(?=\\n#{1,3}\\s|\\Z)`,
|
|
108
|
+
'gm'
|
|
109
|
+
);
|
|
110
|
+
// \\Z is not JS — use two-pass: try lookahead for next heading, then fallback to end
|
|
111
|
+
const headingPattern = new RegExp(
|
|
112
|
+
`^(#{1,3})\\s+${escapeRegex(sectionName)}[^\\n]*\\n([\\s\\S]*?)(?=^#{1,3}\\s)`,
|
|
113
|
+
'gm'
|
|
114
|
+
);
|
|
115
|
+
let replaced = prompt.replace(headingPattern, '');
|
|
116
|
+
if (replaced === prompt) {
|
|
117
|
+
// Section is at the end — no next heading follows
|
|
118
|
+
const endPattern = new RegExp(
|
|
119
|
+
`^(#{1,3})\\s+${escapeRegex(sectionName)}[^\\n]*\\n[\\s\\S]*$`,
|
|
120
|
+
'gm'
|
|
121
|
+
);
|
|
122
|
+
replaced = prompt.replace(endPattern, '');
|
|
123
|
+
}
|
|
124
|
+
prompt = replaced;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Add extra context at the end
|
|
129
|
+
if (customization.extra_context && typeof customization.extra_context === 'string') {
|
|
130
|
+
prompt += `\n\n---\n\n## Additional Context\n\n${customization.extra_context}\n`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Add new sections
|
|
134
|
+
if (customization.add_sections && Array.isArray(customization.add_sections)) {
|
|
135
|
+
for (const section of customization.add_sections) {
|
|
136
|
+
if (section.name && section.content) {
|
|
137
|
+
prompt += `\n\n## ${section.name}\n\n${section.content}\n`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return prompt;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Validate a customization object.
|
|
147
|
+
*
|
|
148
|
+
* @param {object} customization
|
|
149
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
150
|
+
*/
|
|
151
|
+
export function validateCustomization(customization) {
|
|
152
|
+
const errors = [];
|
|
153
|
+
|
|
154
|
+
if (!customization || typeof customization !== 'object') {
|
|
155
|
+
errors.push('Customization must be a non-null object');
|
|
156
|
+
return { valid: false, errors };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check for unknown fields
|
|
160
|
+
for (const key of Object.keys(customization)) {
|
|
161
|
+
if (!VALID_OVERRIDE_FIELDS.includes(key)) {
|
|
162
|
+
errors.push(`Unknown override field: "${key}"`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Validate model
|
|
167
|
+
if (customization.model !== undefined && typeof customization.model !== 'string') {
|
|
168
|
+
errors.push('model must be a string');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Validate extra_context
|
|
172
|
+
if (customization.extra_context !== undefined && typeof customization.extra_context !== 'string') {
|
|
173
|
+
errors.push('extra_context must be a string');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Validate skip_sections
|
|
177
|
+
if (customization.skip_sections !== undefined) {
|
|
178
|
+
if (!Array.isArray(customization.skip_sections)) {
|
|
179
|
+
errors.push('skip_sections must be an array');
|
|
180
|
+
} else if (customization.skip_sections.some(s => typeof s !== 'string')) {
|
|
181
|
+
errors.push('skip_sections must contain only strings');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Validate add_sections
|
|
186
|
+
if (customization.add_sections !== undefined) {
|
|
187
|
+
if (!Array.isArray(customization.add_sections)) {
|
|
188
|
+
errors.push('add_sections must be an array');
|
|
189
|
+
} else {
|
|
190
|
+
for (const section of customization.add_sections) {
|
|
191
|
+
if (!section.name || !section.content) {
|
|
192
|
+
errors.push('Each add_section must have name and content');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Validate timeout
|
|
199
|
+
if (customization.timeout !== undefined) {
|
|
200
|
+
if (typeof customization.timeout !== 'number' || customization.timeout <= 0) {
|
|
201
|
+
errors.push('timeout must be a positive number');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Validate max_iterations
|
|
206
|
+
if (customization.max_iterations !== undefined) {
|
|
207
|
+
if (typeof customization.max_iterations !== 'number' || customization.max_iterations <= 0) {
|
|
208
|
+
errors.push('max_iterations must be a positive number');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { valid: errors.length === 0, errors };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Helpers
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Escape special regex characters in a string.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} str
|
|
223
|
+
* @returns {string}
|
|
224
|
+
*/
|
|
225
|
+
function escapeRegex(str) {
|
|
226
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
227
|
+
}
|
|
@@ -1,68 +1,98 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* IDE Configuration Mapping (
|
|
2
|
+
* IDE Configuration Mapping (7 IDEs)
|
|
3
3
|
* Defines where chati.dev agents are deployed per IDE
|
|
4
4
|
*/
|
|
5
5
|
export const IDE_CONFIGS = {
|
|
6
6
|
'claude-code': {
|
|
7
7
|
name: 'Claude Code',
|
|
8
8
|
description: 'Anthropic official CLI',
|
|
9
|
+
group: 'cli',
|
|
9
10
|
recommended: true,
|
|
10
11
|
configPath: '.claude/commands/',
|
|
11
12
|
rulesFile: '.claude/CLAUDE.md',
|
|
12
13
|
mcpConfigFile: '.claude/mcp.json',
|
|
13
14
|
formatNotes: 'Native markdown format',
|
|
14
15
|
},
|
|
16
|
+
'gemini-cli': {
|
|
17
|
+
name: 'Gemini CLI',
|
|
18
|
+
description: 'Google AI terminal agent',
|
|
19
|
+
group: 'cli',
|
|
20
|
+
recommended: false,
|
|
21
|
+
configPath: '.gemini/commands/',
|
|
22
|
+
rulesFile: 'GEMINI.md',
|
|
23
|
+
mcpConfigFile: '.gemini/settings.json',
|
|
24
|
+
formatNotes: 'TOML command format',
|
|
25
|
+
contextPath: '.gemini/context/',
|
|
26
|
+
hooksPath: '.gemini/hooks/',
|
|
27
|
+
},
|
|
28
|
+
'codex-cli': {
|
|
29
|
+
name: 'Codex CLI',
|
|
30
|
+
description: 'OpenAI Codex terminal agent',
|
|
31
|
+
group: 'cli',
|
|
32
|
+
recommended: false,
|
|
33
|
+
configPath: '.agents/skills/chati/',
|
|
34
|
+
rulesFile: 'AGENTS.md',
|
|
35
|
+
mcpConfigFile: null,
|
|
36
|
+
formatNotes: 'Codex skill format (SKILL.md with YAML frontmatter)',
|
|
37
|
+
rulesPath: '.codex/rules/',
|
|
38
|
+
overrideFile: 'AGENTS.override.md',
|
|
39
|
+
},
|
|
15
40
|
'vscode': {
|
|
16
41
|
name: 'VS Code',
|
|
17
42
|
description: 'Extensions: Continue, Copilot Chat, etc',
|
|
43
|
+
group: 'editor',
|
|
18
44
|
recommended: false,
|
|
19
45
|
configPath: '.vscode/chati/',
|
|
20
46
|
rulesFile: '.vscode/chati/rules.md',
|
|
21
47
|
mcpConfigFile: null,
|
|
22
48
|
formatNotes: 'Markdown with VS Code extension support',
|
|
23
49
|
},
|
|
24
|
-
'antigravity': {
|
|
25
|
-
name: 'AntiGravity',
|
|
26
|
-
description: 'Google agentic development platform',
|
|
27
|
-
recommended: false,
|
|
28
|
-
configPath: '.antigravity/agents/',
|
|
29
|
-
rulesFile: '.antigravity/rules.md',
|
|
30
|
-
mcpConfigFile: null,
|
|
31
|
-
formatNotes: 'Google platform format',
|
|
32
|
-
},
|
|
33
50
|
'cursor': {
|
|
34
51
|
name: 'Cursor',
|
|
35
52
|
description: 'AI-first code editor',
|
|
53
|
+
group: 'editor',
|
|
36
54
|
recommended: false,
|
|
37
55
|
configPath: '.cursor/rules/',
|
|
38
56
|
rulesFile: '.cursorrules',
|
|
39
57
|
mcpConfigFile: null,
|
|
40
58
|
formatNotes: 'Cursor rules format',
|
|
41
59
|
},
|
|
42
|
-
'
|
|
43
|
-
name: '
|
|
44
|
-
description: '
|
|
60
|
+
'windsurf': {
|
|
61
|
+
name: 'Windsurf',
|
|
62
|
+
description: 'AI-powered code editor by Codeium',
|
|
63
|
+
group: 'editor',
|
|
45
64
|
recommended: false,
|
|
46
|
-
configPath: '.
|
|
47
|
-
rulesFile: '
|
|
65
|
+
configPath: '.windsurf/rules/',
|
|
66
|
+
rulesFile: '.windsurfrules',
|
|
48
67
|
mcpConfigFile: null,
|
|
49
|
-
formatNotes: '
|
|
50
|
-
rulesPath: '.codex/rules/',
|
|
51
|
-
overrideFile: 'AGENTS.override.md',
|
|
68
|
+
formatNotes: 'Windsurf rules format (markdown)',
|
|
52
69
|
},
|
|
53
|
-
'
|
|
54
|
-
name: '
|
|
55
|
-
description: 'Google
|
|
70
|
+
'antigravity': {
|
|
71
|
+
name: 'AntiGravity',
|
|
72
|
+
description: 'Google agentic development platform',
|
|
73
|
+
group: 'editor',
|
|
56
74
|
recommended: false,
|
|
57
|
-
configPath: '.
|
|
58
|
-
rulesFile: '
|
|
59
|
-
mcpConfigFile:
|
|
60
|
-
formatNotes: '
|
|
61
|
-
contextPath: '.gemini/context/',
|
|
62
|
-
hooksPath: '.gemini/hooks/',
|
|
75
|
+
configPath: '.antigravity/agents/',
|
|
76
|
+
rulesFile: '.antigravity/rules.md',
|
|
77
|
+
mcpConfigFile: null,
|
|
78
|
+
formatNotes: 'Google platform format',
|
|
63
79
|
},
|
|
64
80
|
};
|
|
65
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Map IDE key to its CLI provider name.
|
|
84
|
+
* Editor-based IDEs (vscode, cursor, windsurf, antigravity) don't have a CLI provider.
|
|
85
|
+
*/
|
|
86
|
+
export const IDE_TO_PROVIDER = {
|
|
87
|
+
'claude-code': 'claude',
|
|
88
|
+
'gemini-cli': 'gemini',
|
|
89
|
+
'codex-cli': 'codex',
|
|
90
|
+
'vscode': null,
|
|
91
|
+
'cursor': null,
|
|
92
|
+
'windsurf': null,
|
|
93
|
+
'antigravity': null,
|
|
94
|
+
};
|
|
95
|
+
|
|
66
96
|
/**
|
|
67
97
|
* Get list of IDEs for selection prompt
|
|
68
98
|
*/
|