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.
Files changed (98) hide show
  1. package/LICENSE +96 -0
  2. package/bin/chati.js +46 -0
  3. package/framework/agents/build/dev.md +122 -1
  4. package/framework/agents/deploy/devops.md +128 -3
  5. package/framework/agents/discover/brief.md +77 -15
  6. package/framework/agents/discover/brownfield-wu.md +2 -2
  7. package/framework/agents/discover/greenfield-wu.md +3 -3
  8. package/framework/agents/plan/architect.md +2 -2
  9. package/framework/agents/plan/detail.md +3 -3
  10. package/framework/agents/plan/phases.md +127 -2
  11. package/framework/agents/plan/tasks.md +127 -2
  12. package/framework/agents/plan/ux.md +269 -22
  13. package/framework/agents/quality/qa-implementation.md +172 -8
  14. package/framework/agents/quality/qa-planning.md +147 -2
  15. package/framework/config.yaml +9 -5
  16. package/framework/constitution.md +7 -1
  17. package/framework/context/quality.md +1 -1
  18. package/framework/context/root.md +1 -1
  19. package/framework/hooks/constitution-guard.js +18 -2
  20. package/framework/hooks/mode-governance.js +3 -3
  21. package/framework/hooks/read-protection.js +10 -2
  22. package/framework/i18n/en.yaml +6 -0
  23. package/framework/i18n/es.yaml +6 -0
  24. package/framework/i18n/fr.yaml +6 -0
  25. package/framework/i18n/pt.yaml +6 -0
  26. package/framework/orchestrator/chati.md +102 -6
  27. package/framework/schemas/task.schema.json +1 -1
  28. package/framework/tasks/architect-dep-audit.md +128 -0
  29. package/framework/tasks/architect-stack-selection.md +28 -0
  30. package/framework/workflows/brownfield-fullstack.yaml +2 -2
  31. package/framework/workflows/brownfield-service.yaml +2 -2
  32. package/framework/workflows/brownfield-ui.yaml +2 -2
  33. package/framework/workflows/greenfield-fullstack.yaml +6 -2
  34. package/framework/workflows/quick-flow.yaml +7 -5
  35. package/framework/workflows/standard-flow.yaml +171 -0
  36. package/package.json +4 -2
  37. package/src/api/index.js +129 -0
  38. package/src/autonomy/build-loop.js +93 -6
  39. package/src/autonomy/build-state.js +20 -2
  40. package/src/autonomy/cause-analyzer.js +177 -0
  41. package/src/autonomy/escalation.js +214 -0
  42. package/src/autonomy/safety-net.js +23 -5
  43. package/src/autonomy/worktree-manager.js +245 -0
  44. package/src/config/agent-customizer.js +227 -0
  45. package/src/config/ide-configs.js +57 -27
  46. package/src/decision/analyzer.js +148 -0
  47. package/src/decision/registry-healer.js +38 -21
  48. package/src/extensions/loader.js +151 -0
  49. package/src/extensions/registry.js +134 -0
  50. package/src/gates/circuit-breaker.js +32 -0
  51. package/src/gates/g3-implementation.js +30 -4
  52. package/src/gates/g4-qa-implementation.js +34 -5
  53. package/src/gates/gate-base.js +9 -0
  54. package/src/health/auto-fix.js +216 -0
  55. package/src/installer/core.js +24 -11
  56. package/src/installer/provider-overlay.js +82 -0
  57. package/src/installer/templates.js +22 -10
  58. package/src/installer/transaction.js +3 -2
  59. package/src/installer/validator.js +74 -0
  60. package/src/intelligence/context-status.js +9 -5
  61. package/src/intelligence/document-sharder.js +221 -0
  62. package/src/intelligence/elicitation.js +265 -0
  63. package/src/intelligence/timeline.js +5 -0
  64. package/src/memory/gotchas.js +78 -2
  65. package/src/merger/semantic-merger.js +292 -0
  66. package/src/orchestrator/agent-selector.js +20 -0
  67. package/src/orchestrator/handoff-engine.js +77 -0
  68. package/src/orchestrator/index.js +0 -8
  69. package/src/orchestrator/intent-classifier.js +182 -0
  70. package/src/orchestrator/pipeline-manager.js +125 -1
  71. package/src/orchestrator/session-manager.js +164 -2
  72. package/src/quality/metrics-collector.js +283 -0
  73. package/src/quality/test-runner.js +368 -0
  74. package/src/telemetry/collector.js +83 -0
  75. package/src/telemetry/config.js +119 -0
  76. package/src/telemetry/index.js +11 -0
  77. package/src/telemetry/schema.js +104 -0
  78. package/src/telemetry/sender.js +60 -0
  79. package/src/terminal/cli-registry.js +7 -1
  80. package/src/terminal/cost-tracker.js +197 -0
  81. package/src/terminal/handoff-parser.js +61 -4
  82. package/src/terminal/prompt-builder.js +56 -18
  83. package/src/terminal/rate-limiter.js +172 -0
  84. package/src/terminal/run-agent.js +39 -0
  85. package/src/terminal/run-parallel.js +22 -1
  86. package/src/terminal/spawner.js +181 -3
  87. package/src/upgrade/migrator.js +2 -2
  88. package/src/utils/event-bus.js +126 -0
  89. package/src/utils/file-lock.js +291 -0
  90. package/src/utils/schema-validator.js +226 -0
  91. package/src/wizard/i18n.js +11 -0
  92. package/src/wizard/index.js +42 -20
  93. package/src/wizard/questions.js +200 -39
  94. package/src/autonomy/execution-profile.js +0 -151
  95. package/src/intelligence/file-tracker.js +0 -117
  96. package/src/memory/gotchas-auto-capture.js +0 -253
  97. package/src/orchestrator/pipeline-state.js +0 -223
  98. package/src/terminal/wave-analyzer.js +0 -143
@@ -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 8 chars)
87
+ * @returns {string} Hash (first 16 chars)
31
88
  */
32
89
  function hashErrorMessage(message) {
33
90
  const normalized = normalizeErrorMessage(message);
34
- return createHash('md5').update(normalized).digest('hex').substring(0, 8);
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,