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,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Build state manager with checkpoint-based persistence.
|
|
3
|
+
*
|
|
4
|
+
* Manages the state of autonomous build execution, including
|
|
5
|
+
* checkpoints saved after each task completion, abandoned build
|
|
6
|
+
* detection, and attempt logging.
|
|
7
|
+
*
|
|
8
|
+
* Constitution Article XVII — Execution Mode Governance.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Constants
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** Maximum iterations per individual task before giving up */
|
|
19
|
+
export const MAX_ITERATIONS_PER_TASK = 10;
|
|
20
|
+
|
|
21
|
+
/** Global timeout for an autonomous build session (30 minutes) */
|
|
22
|
+
export const GLOBAL_TIMEOUT_MS = 30 * 60 * 1000;
|
|
23
|
+
|
|
24
|
+
/** Time after which a build is considered abandoned (1 hour) */
|
|
25
|
+
export const ABANDONED_THRESHOLD_MS = 60 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build status enum.
|
|
29
|
+
* @enum {string}
|
|
30
|
+
*/
|
|
31
|
+
export const BuildStatus = {
|
|
32
|
+
PENDING: 'pending',
|
|
33
|
+
IN_PROGRESS: 'in_progress',
|
|
34
|
+
PAUSED: 'paused',
|
|
35
|
+
ABANDONED: 'abandoned',
|
|
36
|
+
FAILED: 'failed',
|
|
37
|
+
COMPLETED: 'completed',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Task checkpoint status.
|
|
42
|
+
* @enum {string}
|
|
43
|
+
*/
|
|
44
|
+
export const CheckpointStatus = {
|
|
45
|
+
PENDING: 'pending',
|
|
46
|
+
IN_PROGRESS: 'in_progress',
|
|
47
|
+
COMPLETED: 'completed',
|
|
48
|
+
FAILED: 'failed',
|
|
49
|
+
SKIPPED: 'skipped',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// State Schema
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {object} TaskCheckpoint
|
|
58
|
+
* @property {string} taskId
|
|
59
|
+
* @property {string} status - CheckpointStatus
|
|
60
|
+
* @property {number} attempts - Number of execution attempts
|
|
61
|
+
* @property {string|null} lastAttempt - ISO timestamp of last attempt
|
|
62
|
+
* @property {string|null} output - Last output summary
|
|
63
|
+
* @property {string|null} error - Last error message (if failed)
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {object} BuildState
|
|
68
|
+
* @property {string} sessionId - Build session identifier
|
|
69
|
+
* @property {string} status - BuildStatus
|
|
70
|
+
* @property {TaskCheckpoint[]} checkpoints - Per-task checkpoints
|
|
71
|
+
* @property {string} startedAt - ISO timestamp
|
|
72
|
+
* @property {string|null} lastCheckpoint - ISO timestamp of last checkpoint
|
|
73
|
+
* @property {string|null} completedAt - ISO timestamp of completion
|
|
74
|
+
* @property {number} totalAttempts - Total execution attempts across all tasks
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
const BUILD_STATE_FILE = '.chati/build-state.json';
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// State Management
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Initialize a new build state.
|
|
85
|
+
*
|
|
86
|
+
* @param {string[]} taskIds - Array of task IDs to execute
|
|
87
|
+
* @returns {BuildState}
|
|
88
|
+
*/
|
|
89
|
+
export function createBuildState(taskIds) {
|
|
90
|
+
return {
|
|
91
|
+
sessionId: `build-${Date.now()}`,
|
|
92
|
+
status: BuildStatus.PENDING,
|
|
93
|
+
checkpoints: taskIds.map((taskId) => ({
|
|
94
|
+
taskId,
|
|
95
|
+
status: CheckpointStatus.PENDING,
|
|
96
|
+
attempts: 0,
|
|
97
|
+
lastAttempt: null,
|
|
98
|
+
output: null,
|
|
99
|
+
error: null,
|
|
100
|
+
})),
|
|
101
|
+
startedAt: new Date().toISOString(),
|
|
102
|
+
lastCheckpoint: null,
|
|
103
|
+
completedAt: null,
|
|
104
|
+
totalAttempts: 0,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Load build state from disk.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} projectDir
|
|
112
|
+
* @returns {BuildState|null}
|
|
113
|
+
*/
|
|
114
|
+
export function loadBuildState(projectDir) {
|
|
115
|
+
const statePath = join(projectDir, BUILD_STATE_FILE);
|
|
116
|
+
if (!existsSync(statePath)) return null;
|
|
117
|
+
try {
|
|
118
|
+
const state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
119
|
+
|
|
120
|
+
// Check for abandoned builds
|
|
121
|
+
if (state.status === BuildStatus.IN_PROGRESS && state.lastCheckpoint) {
|
|
122
|
+
const elapsed = Date.now() - new Date(state.lastCheckpoint).getTime();
|
|
123
|
+
if (elapsed > ABANDONED_THRESHOLD_MS) {
|
|
124
|
+
state.status = BuildStatus.ABANDONED;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return state;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Save build state to disk.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} projectDir
|
|
138
|
+
* @param {BuildState} state
|
|
139
|
+
*/
|
|
140
|
+
export function saveBuildState(projectDir, state) {
|
|
141
|
+
const statePath = join(projectDir, BUILD_STATE_FILE);
|
|
142
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
143
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Update a task checkpoint.
|
|
148
|
+
*
|
|
149
|
+
* @param {BuildState} state
|
|
150
|
+
* @param {string} taskId
|
|
151
|
+
* @param {Partial<TaskCheckpoint>} update
|
|
152
|
+
* @returns {BuildState}
|
|
153
|
+
*/
|
|
154
|
+
export function updateCheckpoint(state, taskId, update) {
|
|
155
|
+
const checkpoint = state.checkpoints.find((c) => c.taskId === taskId);
|
|
156
|
+
if (!checkpoint) {
|
|
157
|
+
throw new Error(`Task "${taskId}" not found in build state`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
Object.assign(checkpoint, update);
|
|
161
|
+
state.lastCheckpoint = new Date().toISOString();
|
|
162
|
+
|
|
163
|
+
if (update.attempts !== undefined) {
|
|
164
|
+
state.totalAttempts = state.checkpoints.reduce((sum, c) => sum + c.attempts, 0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return state;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Mark the build as started.
|
|
172
|
+
*
|
|
173
|
+
* @param {BuildState} state
|
|
174
|
+
* @returns {BuildState}
|
|
175
|
+
*/
|
|
176
|
+
export function startBuild(state) {
|
|
177
|
+
state.status = BuildStatus.IN_PROGRESS;
|
|
178
|
+
state.startedAt = new Date().toISOString();
|
|
179
|
+
return state;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Mark the build as completed.
|
|
184
|
+
*
|
|
185
|
+
* @param {BuildState} state
|
|
186
|
+
* @returns {BuildState}
|
|
187
|
+
*/
|
|
188
|
+
export function completeBuild(state) {
|
|
189
|
+
state.status = BuildStatus.COMPLETED;
|
|
190
|
+
state.completedAt = new Date().toISOString();
|
|
191
|
+
return state;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Mark the build as failed.
|
|
196
|
+
*
|
|
197
|
+
* @param {BuildState} state
|
|
198
|
+
* @param {string} [reason]
|
|
199
|
+
* @returns {BuildState}
|
|
200
|
+
*/
|
|
201
|
+
export function failBuild(state, _reason) {
|
|
202
|
+
state.status = BuildStatus.FAILED;
|
|
203
|
+
state.completedAt = new Date().toISOString();
|
|
204
|
+
return state;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get the next pending task checkpoint.
|
|
209
|
+
*
|
|
210
|
+
* @param {BuildState} state
|
|
211
|
+
* @returns {TaskCheckpoint|null}
|
|
212
|
+
*/
|
|
213
|
+
export function getNextPendingTask(state) {
|
|
214
|
+
return state.checkpoints.find(
|
|
215
|
+
(c) => c.status === CheckpointStatus.PENDING || c.status === CheckpointStatus.IN_PROGRESS,
|
|
216
|
+
) || null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Check if a task has exceeded max iterations.
|
|
221
|
+
*
|
|
222
|
+
* @param {TaskCheckpoint} checkpoint
|
|
223
|
+
* @returns {boolean}
|
|
224
|
+
*/
|
|
225
|
+
export function isTaskExhausted(checkpoint) {
|
|
226
|
+
return checkpoint.attempts >= MAX_ITERATIONS_PER_TASK;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if the global timeout has been exceeded.
|
|
231
|
+
*
|
|
232
|
+
* @param {BuildState} state
|
|
233
|
+
* @returns {boolean}
|
|
234
|
+
*/
|
|
235
|
+
export function isTimedOut(state) {
|
|
236
|
+
const elapsed = Date.now() - new Date(state.startedAt).getTime();
|
|
237
|
+
return elapsed > GLOBAL_TIMEOUT_MS;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get build progress summary.
|
|
242
|
+
*
|
|
243
|
+
* @param {BuildState} state
|
|
244
|
+
* @returns {{ total: number, completed: number, failed: number, pending: number, progress: number }}
|
|
245
|
+
*/
|
|
246
|
+
export function getProgress(state) {
|
|
247
|
+
const total = state.checkpoints.length;
|
|
248
|
+
const completed = state.checkpoints.filter((c) => c.status === CheckpointStatus.COMPLETED).length;
|
|
249
|
+
const failed = state.checkpoints.filter((c) => c.status === CheckpointStatus.FAILED).length;
|
|
250
|
+
const pending = state.checkpoints.filter(
|
|
251
|
+
(c) => c.status === CheckpointStatus.PENDING || c.status === CheckpointStatus.IN_PROGRESS,
|
|
252
|
+
).length;
|
|
253
|
+
const progress = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
254
|
+
|
|
255
|
+
return { total, completed, failed, pending, progress };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Clear build state (after completion or manual reset).
|
|
260
|
+
*
|
|
261
|
+
* @param {string} projectDir
|
|
262
|
+
*/
|
|
263
|
+
export function clearBuildState(projectDir) {
|
|
264
|
+
const statePath = join(projectDir, BUILD_STATE_FILE);
|
|
265
|
+
if (existsSync(statePath)) {
|
|
266
|
+
const { unlinkSync } = require('fs');
|
|
267
|
+
unlinkSync(statePath);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Execution profile manager.
|
|
3
|
+
*
|
|
4
|
+
* Manages 3 execution profiles that govern confirmation requirements:
|
|
5
|
+
* - explore: read-only, no writes
|
|
6
|
+
* - guided: confirm before writes (default)
|
|
7
|
+
* - autonomous: full autonomy with quality gates
|
|
8
|
+
*
|
|
9
|
+
* Profiles are orthogonal to mode governance (Article XI).
|
|
10
|
+
* Modes control WHERE; profiles control WHETHER.
|
|
11
|
+
*
|
|
12
|
+
* Constitution Article XVIII — Execution Profile Governance.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync } from 'fs';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Profile Definitions
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Execution profile enum.
|
|
24
|
+
* @enum {string}
|
|
25
|
+
*/
|
|
26
|
+
export const Profile = {
|
|
27
|
+
/** Read-only. Agents analyze and suggest but perform no writes. */
|
|
28
|
+
EXPLORE: 'explore',
|
|
29
|
+
/** Default. Agents propose actions and wait for user confirmation. */
|
|
30
|
+
GUIDED: 'guided',
|
|
31
|
+
/** Full autonomy. Agents execute without confirmation when gates pass. */
|
|
32
|
+
AUTONOMOUS: 'autonomous',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Minimum gate score required to activate autonomous profile */
|
|
36
|
+
export const AUTONOMOUS_GATE_THRESHOLD = 90;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Operations that ALWAYS require confirmation regardless of profile.
|
|
40
|
+
* Constitution Article XVIII, point 4.
|
|
41
|
+
*/
|
|
42
|
+
export const ALWAYS_CONFIRM_OPERATIONS = [
|
|
43
|
+
'file_delete',
|
|
44
|
+
'database_drop',
|
|
45
|
+
'force_push',
|
|
46
|
+
'deploy_production',
|
|
47
|
+
'deviation_activate',
|
|
48
|
+
'backward_transition',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Profile Management
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the current execution profile from session state.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} projectDir
|
|
59
|
+
* @returns {string} Current profile (defaults to 'guided')
|
|
60
|
+
*/
|
|
61
|
+
export function getCurrentProfile(projectDir) {
|
|
62
|
+
const sessionPath = join(projectDir, '.chati', 'session.yaml');
|
|
63
|
+
if (!existsSync(sessionPath)) return Profile.GUIDED;
|
|
64
|
+
|
|
65
|
+
const raw = readFileSync(sessionPath, 'utf-8');
|
|
66
|
+
const match = raw.match(/^\s*execution_profile:\s*(.+)$/m);
|
|
67
|
+
const profile = match ? match[1].trim().replace(/^["']|["']$/g, '') : Profile.GUIDED;
|
|
68
|
+
|
|
69
|
+
return Object.values(Profile).includes(profile) ? profile : Profile.GUIDED;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if a write operation is allowed under the current profile.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} profile - Current execution profile
|
|
76
|
+
* @param {string} operation - Operation type (e.g., 'file_write', 'file_delete')
|
|
77
|
+
* @returns {{ allowed: boolean, reason: string }}
|
|
78
|
+
*/
|
|
79
|
+
export function isWriteAllowed(profile, operation) {
|
|
80
|
+
// Explore profile blocks ALL writes
|
|
81
|
+
if (profile === Profile.EXPLORE) {
|
|
82
|
+
return { allowed: false, reason: '[Article XVIII] Explore profile: read-only mode. No writes permitted.' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Always-confirm operations need user approval regardless of profile
|
|
86
|
+
if (ALWAYS_CONFIRM_OPERATIONS.includes(operation)) {
|
|
87
|
+
return { allowed: false, reason: `[Article XVIII] Operation "${operation}" always requires user confirmation.` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Guided profile: allowed but requires confirmation (caller handles confirmation)
|
|
91
|
+
if (profile === Profile.GUIDED) {
|
|
92
|
+
return { allowed: true, reason: 'Guided profile: confirmation required.' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Autonomous profile: allowed without confirmation
|
|
96
|
+
if (profile === Profile.AUTONOMOUS) {
|
|
97
|
+
return { allowed: true, reason: 'Autonomous profile: proceeding without confirmation.' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { allowed: true, reason: 'Default: allowed.' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate whether autonomous profile can be activated.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} projectDir
|
|
107
|
+
* @returns {{ canActivate: boolean, reason: string, currentScore: number }}
|
|
108
|
+
*/
|
|
109
|
+
export function canActivateAutonomous(projectDir) {
|
|
110
|
+
const sessionPath = join(projectDir, '.chati', 'session.yaml');
|
|
111
|
+
if (!existsSync(sessionPath)) {
|
|
112
|
+
return { canActivate: false, reason: 'No active session', currentScore: 0 };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const raw = readFileSync(sessionPath, 'utf-8');
|
|
116
|
+
|
|
117
|
+
// Look for most recent QA gate score
|
|
118
|
+
const scoreMatches = raw.match(/score:\s*([\d.]+)/g);
|
|
119
|
+
if (!scoreMatches || scoreMatches.length === 0) {
|
|
120
|
+
return { canActivate: false, reason: 'No gate scores found. Run QA gates first.', currentScore: 0 };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const scores = scoreMatches.map((m) => parseFloat(m.replace('score:', '').trim()));
|
|
124
|
+
const latestScore = scores[scores.length - 1];
|
|
125
|
+
|
|
126
|
+
if (latestScore < AUTONOMOUS_GATE_THRESHOLD) {
|
|
127
|
+
return {
|
|
128
|
+
canActivate: false,
|
|
129
|
+
reason: `Gate score ${latestScore}% < required ${AUTONOMOUS_GATE_THRESHOLD}%`,
|
|
130
|
+
currentScore: latestScore,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { canActivate: true, reason: 'Gate scores meet threshold', currentScore: latestScore };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if autonomous profile should be downgraded due to quality drop.
|
|
139
|
+
*
|
|
140
|
+
* @param {number} currentScore - Current quality score
|
|
141
|
+
* @returns {{ shouldDowngrade: boolean, reason: string }}
|
|
142
|
+
*/
|
|
143
|
+
export function shouldDowngradeAutonomous(currentScore) {
|
|
144
|
+
if (currentScore < AUTONOMOUS_GATE_THRESHOLD) {
|
|
145
|
+
return {
|
|
146
|
+
shouldDowngrade: true,
|
|
147
|
+
reason: `Quality dropped to ${currentScore}% (< ${AUTONOMOUS_GATE_THRESHOLD}%). Auto-downgrading to guided profile.`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { shouldDowngrade: false, reason: '' };
|
|
151
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Context file generator for multi-CLI support.
|
|
3
|
+
*
|
|
4
|
+
* When alternative CLI providers are enabled, generates provider-specific
|
|
5
|
+
* context files (GEMINI.md, AGENTS.md) derived from CLAUDE.md content.
|
|
6
|
+
* Constitution Article XIX — context file generation is automatic.
|
|
7
|
+
*
|
|
8
|
+
* Provider conventions (verified Feb 2026):
|
|
9
|
+
* - Gemini CLI: auto-loads GEMINI.md, uses .gemini/commands/*.toml, no rules/
|
|
10
|
+
* - Codex CLI: auto-loads AGENTS.md, no commands/ or rules/ concept
|
|
11
|
+
* - Copilot CLI: auto-loads AGENTS.md + CLAUDE.md + GEMINI.md natively (no COPILOT.md needed)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { getEnabledNonClaudeProviders } from '../utils/config-parser.js';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Replacement Maps
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Text replacements for Gemini CLI context file.
|
|
24
|
+
* Transforms Claude Code-specific references into Gemini CLI equivalents.
|
|
25
|
+
* Note: Gemini has no rules/ directory — references point to chati.dev/context/.
|
|
26
|
+
*/
|
|
27
|
+
const GEMINI_REPLACEMENTS = [
|
|
28
|
+
['Claude Code', 'Gemini CLI'],
|
|
29
|
+
['claude code', 'Gemini CLI'],
|
|
30
|
+
['CLAUDE.md', 'GEMINI.md'],
|
|
31
|
+
['Claude.md', 'GEMINI.md'],
|
|
32
|
+
['.claude/commands/', '.gemini/commands/'],
|
|
33
|
+
['.claude/rules/', 'chati.dev/context/'],
|
|
34
|
+
['.claude/mcp.json', '.gemini/settings.json'],
|
|
35
|
+
['claude --print', 'gemini --prompt'],
|
|
36
|
+
['claude -p', 'gemini --prompt'],
|
|
37
|
+
['CLAUDE.local.md', 'GEMINI.local.md'],
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Sections to strip from Codex CLI context file.
|
|
42
|
+
* Codex does not support hooks, so hook-related content is removed.
|
|
43
|
+
*/
|
|
44
|
+
const CODEX_STRIP_PATTERNS = [
|
|
45
|
+
/## (?:Hooks?|Hook System)[\s\S]*?(?=\n## |\n---|\n$)/gi,
|
|
46
|
+
/- \*\*Hooks?\*\*:.*\n/gi,
|
|
47
|
+
/hook[s]? \(.*?\)/gi,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Text replacements for Codex CLI context file.
|
|
52
|
+
* Note: Codex has no commands/ or rules/ directories — references point to chati.dev/.
|
|
53
|
+
*/
|
|
54
|
+
const CODEX_REPLACEMENTS = [
|
|
55
|
+
['Claude Code', 'Codex CLI'],
|
|
56
|
+
['claude code', 'Codex CLI'],
|
|
57
|
+
['CLAUDE.md', 'AGENTS.md'],
|
|
58
|
+
['Claude.md', 'AGENTS.md'],
|
|
59
|
+
['.claude/commands/', 'chati.dev/orchestrator/'],
|
|
60
|
+
['.claude/rules/', 'chati.dev/context/'],
|
|
61
|
+
['.claude/mcp.json', '.codex/config.toml'],
|
|
62
|
+
['claude --print', 'codex exec'],
|
|
63
|
+
['claude -p', 'codex exec'],
|
|
64
|
+
['CLAUDE.local.md', 'AGENTS.local.md'],
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Generators
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generate GEMINI.md content from CLAUDE.md content.
|
|
73
|
+
*
|
|
74
|
+
* Adapts the content by replacing Claude Code-specific references with
|
|
75
|
+
* Gemini CLI equivalents. Preserves structure and formatting.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} content - Raw CLAUDE.md content
|
|
78
|
+
* @returns {string} Adapted content for GEMINI.md
|
|
79
|
+
*/
|
|
80
|
+
export function generateGeminiMd(content) {
|
|
81
|
+
let result = content;
|
|
82
|
+
|
|
83
|
+
for (const [search, replace] of GEMINI_REPLACEMENTS) {
|
|
84
|
+
result = result.replaceAll(search, replace);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Prepend a header noting this file is auto-generated
|
|
88
|
+
const header = [
|
|
89
|
+
'<!-- Auto-generated by chati.dev from CLAUDE.md — do not edit manually -->',
|
|
90
|
+
'',
|
|
91
|
+
].join('\n');
|
|
92
|
+
|
|
93
|
+
return header + result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generate AGENTS.md content from CLAUDE.md content.
|
|
98
|
+
*
|
|
99
|
+
* Simplifies the content for Codex CLI: strips hook references,
|
|
100
|
+
* replaces CLI-specific paths, and produces a leaner context file
|
|
101
|
+
* focused on code execution.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} content - Raw CLAUDE.md content
|
|
104
|
+
* @returns {string} Adapted content for AGENTS.md
|
|
105
|
+
*/
|
|
106
|
+
export function generateAgentsMd(content) {
|
|
107
|
+
let result = content;
|
|
108
|
+
|
|
109
|
+
// Strip hook-related sections (Codex has no hooks support)
|
|
110
|
+
for (const pattern of CODEX_STRIP_PATTERNS) {
|
|
111
|
+
result = result.replace(pattern, '');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Apply text replacements
|
|
115
|
+
for (const [search, replace] of CODEX_REPLACEMENTS) {
|
|
116
|
+
result = result.replaceAll(search, replace);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Clean up any double blank lines left by stripping
|
|
120
|
+
result = result.replace(/\n{3,}/g, '\n\n');
|
|
121
|
+
|
|
122
|
+
// Prepend a header noting this file is auto-generated
|
|
123
|
+
const header = [
|
|
124
|
+
'<!-- Auto-generated by chati.dev from CLAUDE.md — do not edit manually -->',
|
|
125
|
+
'',
|
|
126
|
+
].join('\n');
|
|
127
|
+
|
|
128
|
+
return header + result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Orchestrator
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Generate context files for all enabled alternative providers.
|
|
137
|
+
*
|
|
138
|
+
* Reads config.yaml to determine which providers are enabled, reads
|
|
139
|
+
* CLAUDE.md from the project root, and writes the appropriate context
|
|
140
|
+
* files (GEMINI.md, AGENTS.md) when their providers are active.
|
|
141
|
+
*
|
|
142
|
+
* Copilot CLI does not need its own context file — it natively reads
|
|
143
|
+
* AGENTS.md, CLAUDE.md, and GEMINI.md.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} projectDir - Project root directory
|
|
146
|
+
* @returns {{ generated: string[], skipped: string[], warning: string|null }}
|
|
147
|
+
*/
|
|
148
|
+
export function generateContextFiles(projectDir) {
|
|
149
|
+
const result = { generated: [], skipped: [], warning: null };
|
|
150
|
+
|
|
151
|
+
// Read CLAUDE.md
|
|
152
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
153
|
+
if (!existsSync(claudeMdPath)) {
|
|
154
|
+
result.warning = 'CLAUDE.md not found — skipping context file generation';
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const claudeContent = readFileSync(claudeMdPath, 'utf-8');
|
|
159
|
+
|
|
160
|
+
// Determine enabled providers from config.yaml
|
|
161
|
+
const enabledProviders = resolveEnabledProviders(projectDir);
|
|
162
|
+
|
|
163
|
+
// Provider-to-generator mapping
|
|
164
|
+
// Note: Copilot is intentionally excluded — it reads AGENTS.md + CLAUDE.md + GEMINI.md natively
|
|
165
|
+
const generators = {
|
|
166
|
+
gemini: {
|
|
167
|
+
filename: 'GEMINI.md',
|
|
168
|
+
generate: generateGeminiMd,
|
|
169
|
+
},
|
|
170
|
+
codex: {
|
|
171
|
+
filename: 'AGENTS.md',
|
|
172
|
+
generate: generateAgentsMd,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
for (const [provider, config] of Object.entries(generators)) {
|
|
177
|
+
if (enabledProviders.includes(provider)) {
|
|
178
|
+
const outputPath = join(projectDir, config.filename);
|
|
179
|
+
const content = config.generate(claudeContent);
|
|
180
|
+
writeFileSync(outputPath, content, 'utf-8');
|
|
181
|
+
result.generated.push(config.filename);
|
|
182
|
+
} else {
|
|
183
|
+
result.skipped.push(config.filename);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Helpers
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Resolve which alternative providers are enabled from config.yaml.
|
|
196
|
+
* Delegates to shared config parser utility.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} projectDir - Project root directory
|
|
199
|
+
* @returns {string[]} List of enabled non-Claude provider names
|
|
200
|
+
*/
|
|
201
|
+
function resolveEnabledProviders(projectDir) {
|
|
202
|
+
return getEnabledNonClaudeProviders(projectDir);
|
|
203
|
+
}
|
|
@@ -41,12 +41,12 @@ export const IDE_CONFIGS = {
|
|
|
41
41
|
},
|
|
42
42
|
'gemini-cli': {
|
|
43
43
|
name: 'Gemini CLI',
|
|
44
|
-
description: 'Google AI
|
|
44
|
+
description: 'Google AI terminal agent',
|
|
45
45
|
recommended: false,
|
|
46
|
-
configPath: '.gemini/
|
|
47
|
-
rulesFile:
|
|
48
|
-
mcpConfigFile:
|
|
49
|
-
formatNotes: '
|
|
46
|
+
configPath: '.gemini/commands/',
|
|
47
|
+
rulesFile: null,
|
|
48
|
+
mcpConfigFile: '.gemini/settings.json',
|
|
49
|
+
formatNotes: 'TOML command format',
|
|
50
50
|
},
|
|
51
51
|
'github-copilot': {
|
|
52
52
|
name: 'GitHub Copilot',
|
|
@@ -55,7 +55,7 @@ export const IDE_CONFIGS = {
|
|
|
55
55
|
configPath: '.github/agents/',
|
|
56
56
|
rulesFile: '.github/copilot-instructions.md',
|
|
57
57
|
mcpConfigFile: null,
|
|
58
|
-
formatNotes: 'GitHub Copilot format',
|
|
58
|
+
formatNotes: 'GitHub Copilot agent format',
|
|
59
59
|
},
|
|
60
60
|
};
|
|
61
61
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync, readdirSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import { GateBase } from './gate-base.js';
|
|
10
|
+
import { GateBase, determineVerdict } from './gate-base.js';
|
|
11
11
|
import { loadSession } from '../orchestrator/session-manager.js';
|
|
12
12
|
import { loadHandoff } from '../tasks/handoff.js';
|
|
13
13
|
|
|
@@ -148,6 +148,8 @@ export class QAPlanningGate extends GateBase {
|
|
|
148
148
|
? Math.round((criteriaResults.length / allCriteria.length) * 100)
|
|
149
149
|
: 0;
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
const verdict = determineVerdict(score, 95);
|
|
152
|
+
|
|
153
|
+
return { score, criteriaResults, allCriteria, warnings, verdict };
|
|
152
154
|
}
|
|
153
155
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { existsSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
|
-
import { GateBase } from './gate-base.js';
|
|
17
|
+
import { GateBase, determineVerdict } from './gate-base.js';
|
|
18
18
|
import { loadSession } from '../orchestrator/session-manager.js';
|
|
19
19
|
import { loadHandoff } from '../tasks/handoff.js';
|
|
20
20
|
|
|
@@ -202,6 +202,9 @@ export class QAImplementationGate extends GateBase {
|
|
|
202
202
|
? Math.round((criteriaResults.length / allCriteria.length) * 100)
|
|
203
203
|
: 0;
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
const hasCriticalBlocker = !evidence.noCriticalBugs;
|
|
206
|
+
const verdict = determineVerdict(score, 95, hasCriticalBlocker);
|
|
207
|
+
|
|
208
|
+
return { score, criteriaResults, allCriteria, warnings, verdict };
|
|
206
209
|
}
|
|
207
210
|
}
|