chati-dev 3.2.5 → 3.3.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/LICENSE +96 -0
- 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 +4 -2
- 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
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Pipeline state persistence layer.
|
|
3
|
-
* Handles reading/writing pipeline state to session.yaml.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
7
|
-
import { join, dirname } from 'node:path';
|
|
8
|
-
import yaml from 'js-yaml';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Default session file path relative to project root.
|
|
12
|
-
*/
|
|
13
|
-
const SESSION_PATH = '.chati/session.yaml';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Get full path to session file.
|
|
17
|
-
*
|
|
18
|
-
* @param {string} projectDir - Project root directory
|
|
19
|
-
* @returns {string}
|
|
20
|
-
*/
|
|
21
|
-
function getSessionPath(projectDir) {
|
|
22
|
-
return join(projectDir, SESSION_PATH);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Load pipeline state from session.yaml.
|
|
27
|
-
*
|
|
28
|
-
* @param {string} projectDir
|
|
29
|
-
* @returns {{ loaded: boolean, state: object|null }}
|
|
30
|
-
*/
|
|
31
|
-
export function loadPipelineState(projectDir) {
|
|
32
|
-
const sessionPath = getSessionPath(projectDir);
|
|
33
|
-
|
|
34
|
-
if (!existsSync(sessionPath)) {
|
|
35
|
-
return {
|
|
36
|
-
loaded: false,
|
|
37
|
-
state: null,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
const content = readFileSync(sessionPath, 'utf8');
|
|
43
|
-
const session = yaml.load(content);
|
|
44
|
-
|
|
45
|
-
// Extract pipeline-relevant fields
|
|
46
|
-
const state = {
|
|
47
|
-
phase: session.mode || 'discover',
|
|
48
|
-
isGreenfield: session.project_type === 'greenfield',
|
|
49
|
-
startedAt: session.started_at || null,
|
|
50
|
-
completedAt: session.completed_at || null,
|
|
51
|
-
agents: session.agents || {},
|
|
52
|
-
completedAgents: session.completed_agents || [],
|
|
53
|
-
currentAgent: session.current_agent || null,
|
|
54
|
-
modeTransitions: session.mode_transitions || [],
|
|
55
|
-
history: session.history || [],
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
loaded: true,
|
|
60
|
-
state,
|
|
61
|
-
};
|
|
62
|
-
} catch (error) {
|
|
63
|
-
console.error(`Failed to load pipeline state: ${error.message}`);
|
|
64
|
-
return {
|
|
65
|
-
loaded: false,
|
|
66
|
-
state: null,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Save pipeline state to session.yaml.
|
|
73
|
-
*
|
|
74
|
-
* @param {string} projectDir
|
|
75
|
-
* @param {object} state
|
|
76
|
-
* @returns {{ saved: boolean }}
|
|
77
|
-
*/
|
|
78
|
-
export function savePipelineState(projectDir, state) {
|
|
79
|
-
const sessionPath = getSessionPath(projectDir);
|
|
80
|
-
|
|
81
|
-
// Ensure .chati directory exists
|
|
82
|
-
const chatDir = dirname(sessionPath);
|
|
83
|
-
if (!existsSync(chatDir)) {
|
|
84
|
-
mkdirSync(chatDir, { recursive: true });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
// Convert pipeline state to session format
|
|
89
|
-
const session = {
|
|
90
|
-
mode: state.phase,
|
|
91
|
-
project_type: state.isGreenfield ? 'greenfield' : 'brownfield',
|
|
92
|
-
started_at: state.startedAt,
|
|
93
|
-
completed_at: state.completedAt,
|
|
94
|
-
agents: state.agents,
|
|
95
|
-
completed_agents: state.completedAgents,
|
|
96
|
-
current_agent: state.currentAgent,
|
|
97
|
-
mode_transitions: state.modeTransitions,
|
|
98
|
-
history: state.history,
|
|
99
|
-
updated_at: new Date().toISOString(),
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const content = yaml.dump(session, {
|
|
103
|
-
indent: 2,
|
|
104
|
-
lineWidth: 120,
|
|
105
|
-
noRefs: true,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
writeFileSync(sessionPath, content, 'utf8');
|
|
109
|
-
|
|
110
|
-
return { saved: true };
|
|
111
|
-
} catch (error) {
|
|
112
|
-
console.error(`Failed to save pipeline state: ${error.message}`);
|
|
113
|
-
return { saved: false };
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Merge pipeline state into existing session.yaml (preserving other fields).
|
|
119
|
-
*
|
|
120
|
-
* @param {string} projectDir
|
|
121
|
-
* @param {object} updates - Partial pipeline state updates
|
|
122
|
-
* @returns {{ saved: boolean }}
|
|
123
|
-
*/
|
|
124
|
-
export function updatePipelineState(projectDir, updates) {
|
|
125
|
-
const sessionPath = getSessionPath(projectDir);
|
|
126
|
-
|
|
127
|
-
// Load existing session or start with empty object
|
|
128
|
-
let session = {};
|
|
129
|
-
if (existsSync(sessionPath)) {
|
|
130
|
-
try {
|
|
131
|
-
const content = readFileSync(sessionPath, 'utf8');
|
|
132
|
-
session = yaml.load(content) || {};
|
|
133
|
-
} catch (error) {
|
|
134
|
-
console.error(`Failed to load existing session: ${error.message}`);
|
|
135
|
-
// Continue with empty session
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Ensure .chati directory exists
|
|
140
|
-
const chatDir = dirname(sessionPath);
|
|
141
|
-
if (!existsSync(chatDir)) {
|
|
142
|
-
mkdirSync(chatDir, { recursive: true });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
// Apply updates to session
|
|
147
|
-
if (updates.phase !== undefined) {
|
|
148
|
-
session.mode = updates.phase;
|
|
149
|
-
}
|
|
150
|
-
if (updates.isGreenfield !== undefined) {
|
|
151
|
-
session.project_type = updates.isGreenfield ? 'greenfield' : 'brownfield';
|
|
152
|
-
}
|
|
153
|
-
if (updates.startedAt !== undefined) {
|
|
154
|
-
session.started_at = updates.startedAt;
|
|
155
|
-
}
|
|
156
|
-
if (updates.completedAt !== undefined) {
|
|
157
|
-
session.completed_at = updates.completedAt;
|
|
158
|
-
}
|
|
159
|
-
if (updates.agents !== undefined) {
|
|
160
|
-
session.agents = updates.agents;
|
|
161
|
-
}
|
|
162
|
-
if (updates.completedAgents !== undefined) {
|
|
163
|
-
session.completed_agents = updates.completedAgents;
|
|
164
|
-
}
|
|
165
|
-
if (updates.currentAgent !== undefined) {
|
|
166
|
-
session.current_agent = updates.currentAgent;
|
|
167
|
-
}
|
|
168
|
-
if (updates.modeTransitions !== undefined) {
|
|
169
|
-
session.mode_transitions = updates.modeTransitions;
|
|
170
|
-
}
|
|
171
|
-
if (updates.history !== undefined) {
|
|
172
|
-
session.history = updates.history;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
session.updated_at = new Date().toISOString();
|
|
176
|
-
|
|
177
|
-
const content = yaml.dump(session, {
|
|
178
|
-
indent: 2,
|
|
179
|
-
lineWidth: 120,
|
|
180
|
-
noRefs: true,
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
writeFileSync(sessionPath, content, 'utf8');
|
|
184
|
-
|
|
185
|
-
return { saved: true };
|
|
186
|
-
} catch (error) {
|
|
187
|
-
console.error(`Failed to update pipeline state: ${error.message}`);
|
|
188
|
-
return { saved: false };
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Check if session file exists.
|
|
194
|
-
*
|
|
195
|
-
* @param {string} projectDir
|
|
196
|
-
* @returns {boolean}
|
|
197
|
-
*/
|
|
198
|
-
export function sessionExists(projectDir) {
|
|
199
|
-
const sessionPath = getSessionPath(projectDir);
|
|
200
|
-
return existsSync(sessionPath);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Delete session file.
|
|
205
|
-
*
|
|
206
|
-
* @param {string} projectDir
|
|
207
|
-
* @returns {{ deleted: boolean }}
|
|
208
|
-
*/
|
|
209
|
-
export function deleteSession(projectDir) {
|
|
210
|
-
const sessionPath = getSessionPath(projectDir);
|
|
211
|
-
|
|
212
|
-
if (!existsSync(sessionPath)) {
|
|
213
|
-
return { deleted: true };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
try {
|
|
217
|
-
unlinkSync(sessionPath);
|
|
218
|
-
return { deleted: true };
|
|
219
|
-
} catch (error) {
|
|
220
|
-
console.error(`Failed to delete session: ${error.message}`);
|
|
221
|
-
return { deleted: false };
|
|
222
|
-
}
|
|
223
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
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
|
-
}
|