chati-dev 3.2.5 → 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/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
package/src/memory/gotchas.js
CHANGED
|
@@ -24,14 +24,71 @@ function normalizeErrorMessage(message) {
|
|
|
24
24
|
.trim();
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Error categories for gotcha classification.
|
|
29
|
+
* @enum {string}
|
|
30
|
+
*/
|
|
31
|
+
export const GotchaCategory = {
|
|
32
|
+
BUILD: 'BUILD',
|
|
33
|
+
TEST: 'TEST',
|
|
34
|
+
LINT: 'LINT',
|
|
35
|
+
RUNTIME: 'RUNTIME',
|
|
36
|
+
INTEGRATION: 'INTEGRATION',
|
|
37
|
+
SECURITY: 'SECURITY',
|
|
38
|
+
CONFIG: 'CONFIG',
|
|
39
|
+
UNKNOWN: 'UNKNOWN',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Severity levels for gotchas.
|
|
44
|
+
* @enum {string}
|
|
45
|
+
*/
|
|
46
|
+
export const GotchaSeverity = {
|
|
47
|
+
INFO: 'INFO',
|
|
48
|
+
WARNING: 'WARNING',
|
|
49
|
+
CRITICAL: 'CRITICAL',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Patterns for classifying errors into categories and severities.
|
|
54
|
+
*/
|
|
55
|
+
const CLASSIFICATION_PATTERNS = [
|
|
56
|
+
{ regex: /Cannot find module|ENOENT|import.*not found/i, category: GotchaCategory.BUILD, severity: GotchaSeverity.WARNING },
|
|
57
|
+
{ regex: /SyntaxError|Unexpected token/i, category: GotchaCategory.BUILD, severity: GotchaSeverity.CRITICAL },
|
|
58
|
+
{ regex: /TypeError|ReferenceError/i, category: GotchaCategory.RUNTIME, severity: GotchaSeverity.WARNING },
|
|
59
|
+
{ regex: /test.*fail|assertion|expect/i, category: GotchaCategory.TEST, severity: GotchaSeverity.WARNING },
|
|
60
|
+
{ regex: /lint.*error|eslint|prettier/i, category: GotchaCategory.LINT, severity: GotchaSeverity.INFO },
|
|
61
|
+
{ regex: /CORS|ETIMEDOUT|timeout|ECONNREFUSED/i, category: GotchaCategory.INTEGRATION, severity: GotchaSeverity.WARNING },
|
|
62
|
+
{ regex: /security|vulnerability|injection|EACCES|401|403|unauthorized/i, category: GotchaCategory.SECURITY, severity: GotchaSeverity.CRITICAL },
|
|
63
|
+
{ regex: /out of memory|heap|stack overflow/i, category: GotchaCategory.RUNTIME, severity: GotchaSeverity.CRITICAL },
|
|
64
|
+
{ regex: /config|yaml|json.*parse|invalid.*option/i, category: GotchaCategory.CONFIG, severity: GotchaSeverity.WARNING },
|
|
65
|
+
{ regex: /deprecated/i, category: GotchaCategory.BUILD, severity: GotchaSeverity.INFO },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Classify an error message into category and severity.
|
|
70
|
+
* @param {string} message - Error message
|
|
71
|
+
* @returns {{ category: string, severity: string }}
|
|
72
|
+
*/
|
|
73
|
+
export function classifyError(message) {
|
|
74
|
+
const lowerMessage = (message || '').toLowerCase();
|
|
75
|
+
for (const pattern of CLASSIFICATION_PATTERNS) {
|
|
76
|
+
if (pattern.regex.test(lowerMessage)) {
|
|
77
|
+
return { category: pattern.category, severity: pattern.severity };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { category: GotchaCategory.UNKNOWN, severity: GotchaSeverity.INFO };
|
|
81
|
+
}
|
|
82
|
+
|
|
27
83
|
/**
|
|
28
84
|
* Hash an error message to create a pattern identifier.
|
|
85
|
+
* Uses SHA-256 truncated to 16 chars for collision resistance.
|
|
29
86
|
* @param {string} message - Error message
|
|
30
|
-
* @returns {string} Hash (first
|
|
87
|
+
* @returns {string} Hash (first 16 chars)
|
|
31
88
|
*/
|
|
32
89
|
function hashErrorMessage(message) {
|
|
33
90
|
const normalized = normalizeErrorMessage(message);
|
|
34
|
-
return createHash('
|
|
91
|
+
return createHash('sha256').update(normalized).digest('hex').substring(0, 16);
|
|
35
92
|
}
|
|
36
93
|
|
|
37
94
|
/**
|
|
@@ -167,6 +224,7 @@ export function recordError(projectDir, error) {
|
|
|
167
224
|
|
|
168
225
|
// Create new gotcha
|
|
169
226
|
const gotchaId = generateGotchaId(gotchas);
|
|
227
|
+
const classification = classifyError(message);
|
|
170
228
|
const newGotcha = {
|
|
171
229
|
id: gotchaId,
|
|
172
230
|
pattern: hash,
|
|
@@ -179,6 +237,8 @@ export function recordError(projectDir, error) {
|
|
|
179
237
|
last_seen: timestamp,
|
|
180
238
|
promoted_at: timestamp,
|
|
181
239
|
resolution: null,
|
|
240
|
+
category: classification.category,
|
|
241
|
+
severity: classification.severity,
|
|
182
242
|
context,
|
|
183
243
|
};
|
|
184
244
|
|
|
@@ -307,11 +367,27 @@ export function getGotchaStats(projectDir) {
|
|
|
307
367
|
.sort((a, b) => b.count - a.count)
|
|
308
368
|
.slice(0, 10);
|
|
309
369
|
|
|
370
|
+
// Count by category
|
|
371
|
+
const categoryCounts = {};
|
|
372
|
+
for (const g of gotchas) {
|
|
373
|
+
const cat = g.category || GotchaCategory.UNKNOWN;
|
|
374
|
+
categoryCounts[cat] = (categoryCounts[cat] || 0) + 1;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Count by severity
|
|
378
|
+
const severityCounts = {};
|
|
379
|
+
for (const g of gotchas) {
|
|
380
|
+
const sev = g.severity || GotchaSeverity.INFO;
|
|
381
|
+
severityCounts[sev] = (severityCounts[sev] || 0) + 1;
|
|
382
|
+
}
|
|
383
|
+
|
|
310
384
|
return {
|
|
311
385
|
totalErrors: errorLog.length,
|
|
312
386
|
totalGotchas: gotchas.length,
|
|
313
387
|
recentErrors: recentErrors.length,
|
|
314
388
|
topPatterns,
|
|
389
|
+
categoryCounts,
|
|
390
|
+
severityCounts,
|
|
315
391
|
};
|
|
316
392
|
}
|
|
317
393
|
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Semantic merge engine for parallel agent outputs.
|
|
3
|
+
*
|
|
4
|
+
* Detects file-level, import, and naming conflicts between
|
|
5
|
+
* changes produced by concurrent agents, and applies merge
|
|
6
|
+
* strategies (conservative or latest-wins).
|
|
7
|
+
*
|
|
8
|
+
* Constitution Protocol 5 — Two-Layer Handoff.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Constants
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/** Conflict type identifiers. */
|
|
16
|
+
export const CONFLICT_TYPES = {
|
|
17
|
+
FILE_OVERLAP: 'file_overlap',
|
|
18
|
+
IMPORT_CONFLICT: 'import_conflict',
|
|
19
|
+
NAMING_CONFLICT: 'naming_conflict',
|
|
20
|
+
STRUCTURAL_CONFLICT: 'structural_conflict',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Merge strategy options. */
|
|
24
|
+
export const MERGE_STRATEGIES = {
|
|
25
|
+
CONSERVATIVE: 'conservative',
|
|
26
|
+
LATEST: 'latest',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Public API
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {object} Change
|
|
35
|
+
* @property {string} agent - Agent name that produced the change
|
|
36
|
+
* @property {string} file - File path affected
|
|
37
|
+
* @property {string} [content] - New content (for full-file changes)
|
|
38
|
+
* @property {string[]} [imports] - Import statements added
|
|
39
|
+
* @property {string[]} [exports] - Export names added
|
|
40
|
+
* @property {string} [timestamp] - ISO timestamp of the change
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {object} Conflict
|
|
45
|
+
* @property {string} type - Conflict type (from CONFLICT_TYPES)
|
|
46
|
+
* @property {string} file - File involved
|
|
47
|
+
* @property {string[]} agents - Agents that conflict
|
|
48
|
+
* @property {string} description - Human-readable description
|
|
49
|
+
* @property {string} severity - 'error' | 'warning'
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Detect conflicts between a set of changes from different agents.
|
|
54
|
+
*
|
|
55
|
+
* @param {Change[]} changes
|
|
56
|
+
* @returns {{ hasConflicts: boolean, conflicts: Conflict[] }}
|
|
57
|
+
*/
|
|
58
|
+
export function detectConflicts(changes) {
|
|
59
|
+
if (!Array.isArray(changes) || changes.length < 2) {
|
|
60
|
+
return { hasConflicts: false, conflicts: [] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const conflicts = [];
|
|
64
|
+
|
|
65
|
+
// 1. File overlap detection — multiple agents changing same file
|
|
66
|
+
const fileAgentMap = groupByFile(changes);
|
|
67
|
+
for (const [file, agentChanges] of fileAgentMap.entries()) {
|
|
68
|
+
const agents = [...new Set(agentChanges.map(c => c.agent))];
|
|
69
|
+
if (agents.length > 1) {
|
|
70
|
+
conflicts.push({
|
|
71
|
+
type: CONFLICT_TYPES.FILE_OVERLAP,
|
|
72
|
+
file,
|
|
73
|
+
agents,
|
|
74
|
+
description: `Multiple agents (${agents.join(', ')}) modified ${file}`,
|
|
75
|
+
severity: 'error',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 2. Import conflict detection — duplicate or contradictory imports
|
|
81
|
+
const importsByFile = new Map();
|
|
82
|
+
for (const change of changes) {
|
|
83
|
+
if (!change.imports || change.imports.length === 0) continue;
|
|
84
|
+
if (!importsByFile.has(change.file)) {
|
|
85
|
+
importsByFile.set(change.file, []);
|
|
86
|
+
}
|
|
87
|
+
importsByFile.get(change.file).push({ agent: change.agent, imports: change.imports });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const [file, importSets] of importsByFile.entries()) {
|
|
91
|
+
if (importSets.length < 2) continue;
|
|
92
|
+
|
|
93
|
+
// Check for duplicate imports from different agents
|
|
94
|
+
const allImports = new Map();
|
|
95
|
+
for (const { agent, imports } of importSets) {
|
|
96
|
+
for (const imp of imports) {
|
|
97
|
+
const normalized = normalizeImport(imp);
|
|
98
|
+
if (allImports.has(normalized)) {
|
|
99
|
+
const existing = allImports.get(normalized);
|
|
100
|
+
if (existing.agent !== agent) {
|
|
101
|
+
conflicts.push({
|
|
102
|
+
type: CONFLICT_TYPES.IMPORT_CONFLICT,
|
|
103
|
+
file,
|
|
104
|
+
agents: [existing.agent, agent],
|
|
105
|
+
description: `Duplicate import "${normalized}" from agents ${existing.agent} and ${agent}`,
|
|
106
|
+
severity: 'warning',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
allImports.set(normalized, { agent, raw: imp });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 3. Naming conflict detection — same export name from different agents
|
|
117
|
+
const exportsByName = new Map();
|
|
118
|
+
for (const change of changes) {
|
|
119
|
+
if (!change.exports || change.exports.length === 0) continue;
|
|
120
|
+
for (const exp of change.exports) {
|
|
121
|
+
if (!exportsByName.has(exp)) {
|
|
122
|
+
exportsByName.set(exp, []);
|
|
123
|
+
}
|
|
124
|
+
exportsByName.get(exp).push({ agent: change.agent, file: change.file });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const [name, sources] of exportsByName.entries()) {
|
|
129
|
+
const uniqueAgents = [...new Set(sources.map(s => s.agent))];
|
|
130
|
+
if (uniqueAgents.length > 1) {
|
|
131
|
+
const files = [...new Set(sources.map(s => s.file))];
|
|
132
|
+
conflicts.push({
|
|
133
|
+
type: CONFLICT_TYPES.NAMING_CONFLICT,
|
|
134
|
+
file: files[0],
|
|
135
|
+
agents: uniqueAgents,
|
|
136
|
+
description: `Export name "${name}" defined by multiple agents: ${uniqueAgents.join(', ')}`,
|
|
137
|
+
severity: 'error',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
hasConflicts: conflicts.length > 0,
|
|
144
|
+
conflicts,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Merge changes from multiple agents using a strategy.
|
|
150
|
+
*
|
|
151
|
+
* @param {Change[]} changes
|
|
152
|
+
* @param {string} [strategy='conservative'] - Merge strategy
|
|
153
|
+
* @returns {{ merged: Change[], skipped: Change[], strategy: string }}
|
|
154
|
+
*/
|
|
155
|
+
export function mergeChanges(changes, strategy = MERGE_STRATEGIES.CONSERVATIVE) {
|
|
156
|
+
if (!Array.isArray(changes) || changes.length === 0) {
|
|
157
|
+
return { merged: [], skipped: [], strategy };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (changes.length === 1) {
|
|
161
|
+
return { merged: [...changes], skipped: [], strategy };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const fileAgentMap = groupByFile(changes);
|
|
165
|
+
const merged = [];
|
|
166
|
+
const skipped = [];
|
|
167
|
+
|
|
168
|
+
for (const [file, agentChanges] of fileAgentMap.entries()) {
|
|
169
|
+
const agents = [...new Set(agentChanges.map(c => c.agent))];
|
|
170
|
+
|
|
171
|
+
if (agents.length === 1) {
|
|
172
|
+
// No conflict — include all changes for this file
|
|
173
|
+
merged.push(...agentChanges);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Conflict resolution based on strategy
|
|
178
|
+
if (strategy === MERGE_STRATEGIES.LATEST) {
|
|
179
|
+
// Latest wins — sort by timestamp, take the most recent
|
|
180
|
+
const sorted = [...agentChanges].sort((a, b) => {
|
|
181
|
+
const tA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
|
182
|
+
const tB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
|
183
|
+
return tB - tA;
|
|
184
|
+
});
|
|
185
|
+
merged.push(sorted[0]);
|
|
186
|
+
skipped.push(...sorted.slice(1));
|
|
187
|
+
} else {
|
|
188
|
+
// Conservative — keep the first agent's changes, skip later ones
|
|
189
|
+
const sorted = [...agentChanges].sort((a, b) => {
|
|
190
|
+
const tA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
|
191
|
+
const tB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
|
192
|
+
return tA - tB;
|
|
193
|
+
});
|
|
194
|
+
merged.push(sorted[0]);
|
|
195
|
+
skipped.push(...sorted.slice(1));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { merged, skipped, strategy };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Validate that a merged result is structurally sound.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} original - Original file content
|
|
206
|
+
* @param {string} merged - Merged file content
|
|
207
|
+
* @returns {{ valid: boolean, issues: string[] }}
|
|
208
|
+
*/
|
|
209
|
+
export function validateMergedResult(original, merged) {
|
|
210
|
+
const issues = [];
|
|
211
|
+
|
|
212
|
+
if (!merged || typeof merged !== 'string') {
|
|
213
|
+
issues.push('Merged content is empty or not a string');
|
|
214
|
+
return { valid: false, issues };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check for obvious syntax issues
|
|
218
|
+
|
|
219
|
+
// 1. Balanced braces
|
|
220
|
+
const openBraces = (merged.match(/\{/g) || []).length;
|
|
221
|
+
const closeBraces = (merged.match(/\}/g) || []).length;
|
|
222
|
+
if (openBraces !== closeBraces) {
|
|
223
|
+
issues.push(`Unbalanced braces: ${openBraces} open, ${closeBraces} close`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 2. Balanced parentheses
|
|
227
|
+
const openParens = (merged.match(/\(/g) || []).length;
|
|
228
|
+
const closeParens = (merged.match(/\)/g) || []).length;
|
|
229
|
+
if (openParens !== closeParens) {
|
|
230
|
+
issues.push(`Unbalanced parentheses: ${openParens} open, ${closeParens} close`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 3. Balanced brackets
|
|
234
|
+
const openBrackets = (merged.match(/\[/g) || []).length;
|
|
235
|
+
const closeBrackets = (merged.match(/\]/g) || []).length;
|
|
236
|
+
if (openBrackets !== closeBrackets) {
|
|
237
|
+
issues.push(`Unbalanced brackets: ${openBrackets} open, ${closeBrackets} close`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 4. Check that imports haven't been duplicated
|
|
241
|
+
const importLines = merged.match(/^import\s+.+$/gm) || [];
|
|
242
|
+
const normalizedImports = importLines.map(normalizeImport);
|
|
243
|
+
const uniqueImports = new Set(normalizedImports);
|
|
244
|
+
if (uniqueImports.size < normalizedImports.length) {
|
|
245
|
+
issues.push(`Duplicate imports detected (${normalizedImports.length - uniqueImports.size} duplicates)`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 5. Check content wasn't completely destroyed
|
|
249
|
+
if (original && merged.length < original.length * 0.5) {
|
|
250
|
+
issues.push('Merged content is less than 50% of original size — possible data loss');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
valid: issues.length === 0,
|
|
255
|
+
issues,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Helpers
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Group changes by file path.
|
|
265
|
+
*
|
|
266
|
+
* @param {Change[]} changes
|
|
267
|
+
* @returns {Map<string, Change[]>}
|
|
268
|
+
*/
|
|
269
|
+
function groupByFile(changes) {
|
|
270
|
+
const map = new Map();
|
|
271
|
+
for (const change of changes) {
|
|
272
|
+
if (!change.file) continue;
|
|
273
|
+
if (!map.has(change.file)) {
|
|
274
|
+
map.set(change.file, []);
|
|
275
|
+
}
|
|
276
|
+
map.get(change.file).push(change);
|
|
277
|
+
}
|
|
278
|
+
return map;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Normalize an import statement for comparison.
|
|
283
|
+
*
|
|
284
|
+
* @param {string} importStr
|
|
285
|
+
* @returns {string}
|
|
286
|
+
*/
|
|
287
|
+
function normalizeImport(importStr) {
|
|
288
|
+
return importStr
|
|
289
|
+
.replace(/['"]/g, "'")
|
|
290
|
+
.replace(/\s+/g, ' ')
|
|
291
|
+
.trim();
|
|
292
|
+
}
|
|
@@ -87,6 +87,26 @@ export function selectAgent(context) {
|
|
|
87
87
|
|
|
88
88
|
// If starting fresh with no completed agents
|
|
89
89
|
if (completedAgents.length === 0) {
|
|
90
|
+
// Quick Flow: fast-track pipeline (Brief quick → Dev → QA → Deploy)
|
|
91
|
+
if (intent === INTENT_TYPES.QUICK_FLOW) {
|
|
92
|
+
return {
|
|
93
|
+
agent: 'brief',
|
|
94
|
+
reason: 'Quick flow detected — fast-track pipeline',
|
|
95
|
+
parallelGroup: null,
|
|
96
|
+
workflowType: 'quick-flow',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Standard Flow: mid-tier pipeline (8 agents)
|
|
101
|
+
if (intent === INTENT_TYPES.STANDARD_FLOW) {
|
|
102
|
+
return {
|
|
103
|
+
agent: 'brief',
|
|
104
|
+
reason: 'Standard flow detected — mid-tier pipeline',
|
|
105
|
+
parallelGroup: null,
|
|
106
|
+
workflowType: 'standard-flow',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
90
110
|
// Planning intent starts with WU
|
|
91
111
|
if (intent === INTENT_TYPES.DISCOVER) {
|
|
92
112
|
const wuAgent = isGreenfield ? 'greenfield-wu' : 'brownfield-wu';
|
|
@@ -222,6 +222,83 @@ export function getHandoffHistory(projectDir) {
|
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Minimum required handoff fields per receiving agent.
|
|
227
|
+
* Used by pre-flight context integrity check.
|
|
228
|
+
*/
|
|
229
|
+
const AGENT_HANDOFF_REQUIREMENTS = {
|
|
230
|
+
'brief': { needs: ['summary'] },
|
|
231
|
+
'detail': { needs: ['summary', 'outputs'] },
|
|
232
|
+
'architect': { needs: ['summary', 'outputs'] },
|
|
233
|
+
'ux': { needs: ['summary', 'outputs'] },
|
|
234
|
+
'phases': { needs: ['summary', 'outputs'] },
|
|
235
|
+
'tasks': { needs: ['summary', 'outputs'] },
|
|
236
|
+
'qa-planning': { needs: ['summary', 'outputs'] },
|
|
237
|
+
'dev': { needs: ['summary', 'outputs'] },
|
|
238
|
+
'qa-implementation': { needs: ['summary', 'outputs'] },
|
|
239
|
+
'devops': { needs: ['summary', 'outputs'] },
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Pre-flight context integrity check.
|
|
244
|
+
* Validates that a handoff contains sufficient context for the receiving agent.
|
|
245
|
+
* Run this BEFORE activating an agent to catch missing context early.
|
|
246
|
+
*
|
|
247
|
+
* @param {object|null} handoff - Loaded handoff from previous agent
|
|
248
|
+
* @param {string} receivingAgent - Agent about to be activated
|
|
249
|
+
* @returns {{ valid: boolean, missing: string[], warnings: string[] }}
|
|
250
|
+
*/
|
|
251
|
+
export function validateHandoffIntegrity(handoff, receivingAgent) {
|
|
252
|
+
const missing = [];
|
|
253
|
+
const warnings = [];
|
|
254
|
+
|
|
255
|
+
// No handoff at all
|
|
256
|
+
if (!handoff) {
|
|
257
|
+
missing.push('No handoff document found from previous agent');
|
|
258
|
+
return { valid: false, missing, warnings };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Universal checks
|
|
262
|
+
if (!handoff.summary || handoff.summary.trim().length < 10) {
|
|
263
|
+
missing.push('Handoff summary is empty or too short (< 10 chars)');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (handoff.status === 'partial') {
|
|
267
|
+
warnings.push('Previous agent handoff is marked as partial');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (handoff.score !== null && handoff.score !== undefined && handoff.score < 90) {
|
|
271
|
+
warnings.push(`Previous agent score (${handoff.score}) is below 90% threshold`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Agent-specific checks
|
|
275
|
+
const reqs = AGENT_HANDOFF_REQUIREMENTS[receivingAgent];
|
|
276
|
+
if (reqs) {
|
|
277
|
+
for (const field of reqs.needs) {
|
|
278
|
+
const value = handoff[field];
|
|
279
|
+
if (!value || (Array.isArray(value) && value.length === 0)) {
|
|
280
|
+
missing.push(`Required field '${field}' is missing or empty`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Check for unresolved blockers
|
|
286
|
+
if (handoff.blockers && handoff.blockers.length > 0) {
|
|
287
|
+
const criticalBlockers = handoff.blockers.filter(b =>
|
|
288
|
+
b.toLowerCase().includes('critical') || b.toLowerCase().includes('blocking')
|
|
289
|
+
);
|
|
290
|
+
if (criticalBlockers.length > 0) {
|
|
291
|
+
warnings.push(`${criticalBlockers.length} critical blocker(s) from previous agent`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
valid: missing.length === 0,
|
|
297
|
+
missing,
|
|
298
|
+
warnings,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
225
302
|
/**
|
|
226
303
|
* Check if rollback to a previous agent is possible.
|
|
227
304
|
* @param {string} projectDir
|
|
@@ -32,14 +32,6 @@ export {
|
|
|
32
32
|
markAgentInProgress,
|
|
33
33
|
} from './pipeline-manager.js';
|
|
34
34
|
|
|
35
|
-
export {
|
|
36
|
-
loadPipelineState,
|
|
37
|
-
savePipelineState,
|
|
38
|
-
updatePipelineState,
|
|
39
|
-
sessionExists,
|
|
40
|
-
deleteSession,
|
|
41
|
-
} from './pipeline-state.js';
|
|
42
|
-
|
|
43
35
|
export {
|
|
44
36
|
executeHandoff,
|
|
45
37
|
validateHandoffPreconditions,
|