chati-dev 3.2.4 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +96 -0
- package/README.md +2 -2
- package/bin/chati.js +46 -0
- package/framework/agents/build/dev.md +122 -1
- package/framework/agents/deploy/devops.md +128 -3
- package/framework/agents/discover/brief.md +77 -15
- package/framework/agents/discover/brownfield-wu.md +2 -2
- package/framework/agents/discover/greenfield-wu.md +3 -3
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +3 -3
- package/framework/agents/plan/phases.md +127 -2
- package/framework/agents/plan/tasks.md +127 -2
- package/framework/agents/plan/ux.md +269 -22
- package/framework/agents/quality/qa-implementation.md +172 -8
- package/framework/agents/quality/qa-planning.md +147 -2
- package/framework/config.yaml +9 -5
- package/framework/constitution.md +7 -1
- package/framework/context/quality.md +1 -1
- package/framework/context/root.md +1 -1
- package/framework/hooks/constitution-guard.js +18 -2
- package/framework/hooks/mode-governance.js +3 -3
- package/framework/hooks/read-protection.js +10 -2
- package/framework/i18n/en.yaml +6 -0
- package/framework/i18n/es.yaml +6 -0
- package/framework/i18n/fr.yaml +6 -0
- package/framework/i18n/pt.yaml +6 -0
- package/framework/orchestrator/chati.md +102 -6
- package/framework/schemas/task.schema.json +1 -1
- package/framework/tasks/architect-dep-audit.md +128 -0
- package/framework/tasks/architect-stack-selection.md +28 -0
- package/framework/workflows/brownfield-fullstack.yaml +2 -2
- package/framework/workflows/brownfield-service.yaml +2 -2
- package/framework/workflows/brownfield-ui.yaml +2 -2
- package/framework/workflows/greenfield-fullstack.yaml +6 -2
- package/framework/workflows/quick-flow.yaml +7 -5
- package/framework/workflows/standard-flow.yaml +171 -0
- package/package.json +5 -3
- package/src/api/index.js +129 -0
- package/src/autonomy/build-loop.js +93 -6
- package/src/autonomy/build-state.js +20 -2
- package/src/autonomy/cause-analyzer.js +177 -0
- package/src/autonomy/escalation.js +214 -0
- package/src/autonomy/safety-net.js +23 -5
- package/src/autonomy/worktree-manager.js +245 -0
- package/src/config/agent-customizer.js +227 -0
- package/src/config/ide-configs.js +57 -27
- package/src/decision/analyzer.js +148 -0
- package/src/decision/registry-healer.js +38 -21
- package/src/extensions/loader.js +151 -0
- package/src/extensions/registry.js +134 -0
- package/src/gates/circuit-breaker.js +32 -0
- package/src/gates/g3-implementation.js +30 -4
- package/src/gates/g4-qa-implementation.js +34 -5
- package/src/gates/gate-base.js +9 -0
- package/src/health/auto-fix.js +216 -0
- package/src/installer/core.js +24 -11
- package/src/installer/provider-overlay.js +82 -0
- package/src/installer/templates.js +22 -10
- package/src/installer/transaction.js +3 -2
- package/src/installer/validator.js +74 -0
- package/src/intelligence/context-status.js +9 -5
- package/src/intelligence/document-sharder.js +221 -0
- package/src/intelligence/elicitation.js +265 -0
- package/src/intelligence/timeline.js +5 -0
- package/src/memory/gotchas.js +78 -2
- package/src/merger/semantic-merger.js +292 -0
- package/src/orchestrator/agent-selector.js +20 -0
- package/src/orchestrator/handoff-engine.js +77 -0
- package/src/orchestrator/index.js +0 -8
- package/src/orchestrator/intent-classifier.js +182 -0
- package/src/orchestrator/pipeline-manager.js +125 -1
- package/src/orchestrator/session-manager.js +164 -2
- package/src/quality/metrics-collector.js +283 -0
- package/src/quality/test-runner.js +368 -0
- package/src/telemetry/collector.js +83 -0
- package/src/telemetry/config.js +119 -0
- package/src/telemetry/index.js +11 -0
- package/src/telemetry/schema.js +104 -0
- package/src/telemetry/sender.js +60 -0
- package/src/terminal/cli-registry.js +7 -1
- package/src/terminal/cost-tracker.js +197 -0
- package/src/terminal/handoff-parser.js +61 -4
- package/src/terminal/prompt-builder.js +56 -18
- package/src/terminal/rate-limiter.js +172 -0
- package/src/terminal/run-agent.js +39 -0
- package/src/terminal/run-parallel.js +22 -1
- package/src/terminal/spawner.js +181 -3
- package/src/upgrade/migrator.js +2 -2
- package/src/utils/event-bus.js +126 -0
- package/src/utils/file-lock.js +291 -0
- package/src/utils/schema-validator.js +226 -0
- package/src/wizard/i18n.js +11 -0
- package/src/wizard/index.js +42 -20
- package/src/wizard/questions.js +200 -39
- package/src/autonomy/execution-profile.js +0 -151
- package/src/intelligence/file-tracker.js +0 -117
- package/src/memory/gotchas-auto-capture.js +0 -253
- package/src/orchestrator/pipeline-state.js +0 -223
- package/src/terminal/wave-analyzer.js +0 -143
|
@@ -16,6 +16,8 @@ export const INTENT_TYPES = {
|
|
|
16
16
|
STATUS: 'status',
|
|
17
17
|
RESUME: 'resume',
|
|
18
18
|
HELP: 'help',
|
|
19
|
+
QUICK_FLOW: 'quick_flow',
|
|
20
|
+
STANDARD_FLOW: 'standard_flow',
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -68,6 +70,16 @@ const INTENT_KEYWORDS = {
|
|
|
68
70
|
medium: ['how to', 'what is', 'explain', 'confused'],
|
|
69
71
|
low: ['support', 'assist', 'info'],
|
|
70
72
|
},
|
|
73
|
+
[INTENT_TYPES.QUICK_FLOW]: {
|
|
74
|
+
high: ['quick fix', 'hotfix', 'bug fix', 'small change', 'just fix', 'just do it'],
|
|
75
|
+
medium: ['fix', 'tweak', 'config', 'patch', 'simple'],
|
|
76
|
+
low: ['change', 'adjust', 'small', 'update'],
|
|
77
|
+
},
|
|
78
|
+
[INTENT_TYPES.STANDARD_FLOW]: {
|
|
79
|
+
high: ['standard flow', 'standard pipeline', 'medium project', 'moderate scope'],
|
|
80
|
+
medium: ['feature', 'integration', 'enhancement', 'module', 'component'],
|
|
81
|
+
low: ['project', 'application', 'service', 'system'],
|
|
82
|
+
},
|
|
71
83
|
};
|
|
72
84
|
|
|
73
85
|
/**
|
|
@@ -215,6 +227,8 @@ export function getIntentPhase(intent) {
|
|
|
215
227
|
[INTENT_TYPES.STATUS]: null, // no phase change
|
|
216
228
|
[INTENT_TYPES.RESUME]: null, // uses current phase
|
|
217
229
|
[INTENT_TYPES.HELP]: null, // no phase change
|
|
230
|
+
[INTENT_TYPES.QUICK_FLOW]: 'discover', // starts with quick brief
|
|
231
|
+
[INTENT_TYPES.STANDARD_FLOW]: 'discover', // starts with brief
|
|
218
232
|
};
|
|
219
233
|
|
|
220
234
|
return phaseMap[intent];
|
|
@@ -266,3 +280,171 @@ export function checkModeAlignment(intent, currentMode) {
|
|
|
266
280
|
reason: `Intent requires ${targetPhase} mode (backward transition - deviation protocol)`,
|
|
267
281
|
};
|
|
268
282
|
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Quick Flow trigger and disqualifier keywords.
|
|
286
|
+
*/
|
|
287
|
+
const QUICK_FLOW_TRIGGERS = [
|
|
288
|
+
'quick fix', 'hotfix', 'bug fix', 'small change', 'just fix',
|
|
289
|
+
'just do it', 'simple fix', 'patch', 'config change',
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
const QUICK_FLOW_DISQUALIFIERS = [
|
|
293
|
+
'architecture', 'design system', 'database schema', 'auth',
|
|
294
|
+
'authentication', 'multiple services', 'microservices', 'enterprise',
|
|
295
|
+
'compliance', 'full pipeline', 'full process',
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Detect if a message qualifies for Quick Flow (fast-track pipeline).
|
|
300
|
+
* Quick Flow skips Detail, Architect, UX, Phases, Tasks, QA-Planning.
|
|
301
|
+
*
|
|
302
|
+
* @param {string} message - User's raw input
|
|
303
|
+
* @param {object} [sessionContext] - { mode, currentAgent, isGreenfield, hasExistingCodebase }
|
|
304
|
+
* @returns {{ isQuickFlow: boolean, confidence: number, reason: string }}
|
|
305
|
+
*/
|
|
306
|
+
export function detectQuickFlow(message, sessionContext = {}) {
|
|
307
|
+
const normalized = message.toLowerCase().trim();
|
|
308
|
+
|
|
309
|
+
// Check for disqualifiers first (they override triggers)
|
|
310
|
+
for (const disqualifier of QUICK_FLOW_DISQUALIFIERS) {
|
|
311
|
+
if (normalized.includes(disqualifier)) {
|
|
312
|
+
return {
|
|
313
|
+
isQuickFlow: false,
|
|
314
|
+
confidence: 0.9,
|
|
315
|
+
reason: `Disqualified: message contains "${disqualifier}"`,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Complex greenfield projects (multiple requirements, architecture needed) are disqualified
|
|
321
|
+
// But simple greenfield (single-purpose tool, fun project, prototype) are allowed
|
|
322
|
+
const sentenceCount = normalized.split(/[.!?]+/).filter((s) => s.trim().length > 0).length;
|
|
323
|
+
const requirementSignals = (normalized.match(/\band\b|\balso\b|\bplus\b|\badditionally\b/g) || []).length;
|
|
324
|
+
|
|
325
|
+
if (sessionContext.isGreenfield && (sentenceCount > 3 || requirementSignals > 2)) {
|
|
326
|
+
return {
|
|
327
|
+
isQuickFlow: false,
|
|
328
|
+
confidence: 0.7,
|
|
329
|
+
reason: 'Greenfield with multiple requirements detected',
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Check for trigger keywords
|
|
334
|
+
let triggerScore = 0;
|
|
335
|
+
const matchedTriggers = [];
|
|
336
|
+
|
|
337
|
+
for (const trigger of QUICK_FLOW_TRIGGERS) {
|
|
338
|
+
if (normalized.includes(trigger)) {
|
|
339
|
+
triggerScore += 3;
|
|
340
|
+
matchedTriggers.push(trigger);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Single-sentence requests are a signal for quick flow
|
|
345
|
+
if (sentenceCount <= 1 && normalized.length < 200) {
|
|
346
|
+
triggerScore += 2;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Calculate confidence
|
|
350
|
+
const confidence = Math.min(triggerScore / 5, 1.0);
|
|
351
|
+
|
|
352
|
+
if (confidence >= 0.8) {
|
|
353
|
+
return {
|
|
354
|
+
isQuickFlow: true,
|
|
355
|
+
confidence,
|
|
356
|
+
reason: `Quick flow triggers matched: ${matchedTriggers.join(', ') || 'short single-requirement message'}`,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
isQuickFlow: false,
|
|
362
|
+
confidence,
|
|
363
|
+
reason: `Confidence ${confidence.toFixed(2)} below threshold 0.8`,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Standard Flow trigger and disqualifier keywords.
|
|
369
|
+
*/
|
|
370
|
+
const STANDARD_FLOW_TRIGGERS = [
|
|
371
|
+
'standard flow', 'standard pipeline', 'medium project',
|
|
372
|
+
'new feature', 'integration', 'enhancement', 'module',
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
const STANDARD_FLOW_DISQUALIFIERS = [
|
|
376
|
+
'quick fix', 'hotfix', 'just fix', 'simple fix',
|
|
377
|
+
'full pipeline', 'full process', 'enterprise', 'compliance',
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Detect if a message qualifies for Standard Flow (mid-tier pipeline).
|
|
382
|
+
* Standard Flow uses 8 agents (between Quick 4 and Full 12).
|
|
383
|
+
*
|
|
384
|
+
* @param {string} message - User's raw input
|
|
385
|
+
* @param {object} [sessionContext] - { mode, currentAgent, isGreenfield, hasExistingCodebase }
|
|
386
|
+
* @returns {{ isStandardFlow: boolean, confidence: number, reason: string }}
|
|
387
|
+
*/
|
|
388
|
+
export function detectStandardFlow(message, sessionContext = {}) {
|
|
389
|
+
const normalized = message.toLowerCase().trim();
|
|
390
|
+
|
|
391
|
+
// Check for disqualifiers first
|
|
392
|
+
for (const disqualifier of STANDARD_FLOW_DISQUALIFIERS) {
|
|
393
|
+
if (normalized.includes(disqualifier)) {
|
|
394
|
+
return {
|
|
395
|
+
isStandardFlow: false,
|
|
396
|
+
confidence: 0.9,
|
|
397
|
+
reason: `Disqualified: message contains "${disqualifier}"`,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Count requirement signals
|
|
403
|
+
const sentenceCount = normalized.split(/[.!?]+/).filter(s => s.trim().length > 0).length;
|
|
404
|
+
const requirementSignals = (normalized.match(/\band\b|\balso\b|\bplus\b|\badditionally\b/g) || []).length;
|
|
405
|
+
|
|
406
|
+
// Heuristic: 2-5 requirements suggest standard flow (not quick, not full)
|
|
407
|
+
const requirementCount = sentenceCount + requirementSignals;
|
|
408
|
+
|
|
409
|
+
let triggerScore = 0;
|
|
410
|
+
const matchedTriggers = [];
|
|
411
|
+
|
|
412
|
+
// Check for trigger keywords
|
|
413
|
+
for (const trigger of STANDARD_FLOW_TRIGGERS) {
|
|
414
|
+
if (normalized.includes(trigger)) {
|
|
415
|
+
triggerScore += 3;
|
|
416
|
+
matchedTriggers.push(trigger);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Requirement count in the sweet spot (2-5)
|
|
421
|
+
if (requirementCount >= 2 && requirementCount <= 5) {
|
|
422
|
+
triggerScore += 2;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Brownfield moderate context
|
|
426
|
+
if (sessionContext.hasExistingCodebase || sessionContext.isGreenfield === false) {
|
|
427
|
+
triggerScore += 1;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Message length in moderate range (not too short, not too long)
|
|
431
|
+
if (normalized.length > 100 && normalized.length < 1000) {
|
|
432
|
+
triggerScore += 1;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const confidence = Math.min(triggerScore / 5, 1.0);
|
|
436
|
+
|
|
437
|
+
if (confidence >= 0.8) {
|
|
438
|
+
return {
|
|
439
|
+
isStandardFlow: true,
|
|
440
|
+
confidence,
|
|
441
|
+
reason: `Standard flow triggers matched: ${matchedTriggers.join(', ') || 'moderate requirement count'}`,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
isStandardFlow: false,
|
|
447
|
+
confidence,
|
|
448
|
+
reason: `Confidence ${confidence.toFixed(2)} below threshold 0.8`,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { AGENT_PIPELINE, getNextAgent } from './agent-selector.js';
|
|
7
|
+
import { calculateBracket, estimateRemaining } from '../context/bracket-tracker.js';
|
|
8
|
+
import { track as telemetryTrack, flush as telemetryFlush } from '../telemetry/collector.js';
|
|
9
|
+
import { sendEvents } from '../telemetry/sender.js';
|
|
10
|
+
import { getTelemetryConfig } from '../telemetry/config.js';
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Pipeline phases in order.
|
|
@@ -70,13 +74,95 @@ export function initPipeline(options = {}) {
|
|
|
70
74
|
};
|
|
71
75
|
}
|
|
72
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Standard Flow pipeline agents (mid-tier — 8 agents).
|
|
79
|
+
* Skips: WU, UX, Phases (tasks subsumes phase splitting).
|
|
80
|
+
*/
|
|
81
|
+
const STANDARD_FLOW_AGENTS = [
|
|
82
|
+
'brief', 'detail', 'architect', 'tasks',
|
|
83
|
+
'qa-planning', 'dev', 'qa-implementation', 'devops',
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Quick Flow pipeline agents (subset of full pipeline).
|
|
88
|
+
* Skips: WU, Detail, Architect, UX, Phases, Tasks, QA-Planning.
|
|
89
|
+
*/
|
|
90
|
+
const QUICK_FLOW_AGENTS = ['brief', 'dev', 'qa-implementation', 'devops'];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Initialize a Quick Flow pipeline (fast-track for simple tasks).
|
|
94
|
+
*
|
|
95
|
+
* @param {object} options - { isGreenfield, mode }
|
|
96
|
+
* @returns {object} Pipeline state
|
|
97
|
+
*/
|
|
98
|
+
export function initQuickFlowPipeline(options = {}) {
|
|
99
|
+
const { isGreenfield = false, mode = 'discover' } = options;
|
|
100
|
+
|
|
101
|
+
const agents = {};
|
|
102
|
+
for (const agentName of QUICK_FLOW_AGENTS) {
|
|
103
|
+
agents[agentName] = {
|
|
104
|
+
status: AGENT_STATUS.PENDING,
|
|
105
|
+
score: null,
|
|
106
|
+
startedAt: null,
|
|
107
|
+
completedAt: null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
phase: mode,
|
|
113
|
+
isGreenfield,
|
|
114
|
+
isQuickFlow: true,
|
|
115
|
+
startedAt: new Date().toISOString(),
|
|
116
|
+
completedAt: null,
|
|
117
|
+
agents,
|
|
118
|
+
completedAgents: [],
|
|
119
|
+
currentAgent: null,
|
|
120
|
+
modeTransitions: [],
|
|
121
|
+
history: [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Initialize a Standard Flow pipeline (mid-tier for moderate tasks).
|
|
127
|
+
* Uses 8 agents — between Quick Flow (4) and Full Flow (12).
|
|
128
|
+
*
|
|
129
|
+
* @param {object} options - { isGreenfield, mode }
|
|
130
|
+
* @returns {object} Pipeline state
|
|
131
|
+
*/
|
|
132
|
+
export function initStandardFlowPipeline(options = {}) {
|
|
133
|
+
const { isGreenfield = false, mode = 'discover' } = options;
|
|
134
|
+
|
|
135
|
+
const agents = {};
|
|
136
|
+
for (const agentName of STANDARD_FLOW_AGENTS) {
|
|
137
|
+
agents[agentName] = {
|
|
138
|
+
status: AGENT_STATUS.PENDING,
|
|
139
|
+
score: null,
|
|
140
|
+
startedAt: null,
|
|
141
|
+
completedAt: null,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
phase: mode,
|
|
147
|
+
isGreenfield,
|
|
148
|
+
isStandardFlow: true,
|
|
149
|
+
startedAt: new Date().toISOString(),
|
|
150
|
+
completedAt: null,
|
|
151
|
+
agents,
|
|
152
|
+
completedAgents: [],
|
|
153
|
+
currentAgent: null,
|
|
154
|
+
modeTransitions: [],
|
|
155
|
+
history: [],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
73
159
|
/**
|
|
74
160
|
* Advance the pipeline after an agent completes.
|
|
75
161
|
*
|
|
76
162
|
* @param {object} pipelineState - Current state
|
|
77
163
|
* @param {string} completedAgent - Agent that just finished
|
|
78
164
|
* @param {object} [results] - Agent results (score, outputs)
|
|
79
|
-
* @returns {{ state: object, nextAction: string, nextAgent: string|null, needsModeSwitch: boolean }}
|
|
165
|
+
* @returns {{ state: object, nextAction: string, nextAgent: string|null, needsModeSwitch: boolean, contextBracket: object }}
|
|
80
166
|
*/
|
|
81
167
|
export function advancePipeline(pipelineState, completedAgent, results = {}) {
|
|
82
168
|
const newState = { ...pipelineState };
|
|
@@ -98,6 +184,12 @@ export function advancePipeline(pipelineState, completedAgent, results = {}) {
|
|
|
98
184
|
newState.completedAgents.push(completedAgent);
|
|
99
185
|
}
|
|
100
186
|
|
|
187
|
+
// Calculate context bracket from pipeline progress
|
|
188
|
+
const totalAgents = Object.keys(newState.agents).length;
|
|
189
|
+
const completedCount = newState.completedAgents.length;
|
|
190
|
+
const remaining = estimateRemaining(completedCount, totalAgents);
|
|
191
|
+
const contextBracket = calculateBracket(remaining);
|
|
192
|
+
|
|
101
193
|
// Add to history
|
|
102
194
|
newState.history.push({
|
|
103
195
|
agent: completedAgent,
|
|
@@ -120,6 +212,7 @@ export function advancePipeline(pipelineState, completedAgent, results = {}) {
|
|
|
120
212
|
nextAction: 'user_preview',
|
|
121
213
|
nextAgent: null,
|
|
122
214
|
needsModeSwitch: false,
|
|
215
|
+
contextBracket,
|
|
123
216
|
previewContext: {
|
|
124
217
|
qaScore: newState.agents['qa-implementation'].score,
|
|
125
218
|
},
|
|
@@ -146,16 +239,30 @@ export function advancePipeline(pipelineState, completedAgent, results = {}) {
|
|
|
146
239
|
nextAction: 'advance_phase',
|
|
147
240
|
nextAgent: getFirstAgentInPhase(newState, nextPhase),
|
|
148
241
|
needsModeSwitch: true,
|
|
242
|
+
contextBracket,
|
|
149
243
|
};
|
|
150
244
|
}
|
|
151
245
|
|
|
152
246
|
// Pipeline complete
|
|
153
247
|
newState.completedAt = new Date().toISOString();
|
|
248
|
+
telemetryTrack('pipeline_completed', {
|
|
249
|
+
pipelineType: newState.isQuickFlow ? 'quick-flow' : newState.isStandardFlow ? 'standard' : 'full',
|
|
250
|
+
totalDuration: Date.now() - new Date(newState.startedAt).getTime(),
|
|
251
|
+
agentsRun: newState.completedAgents.length,
|
|
252
|
+
finalStatus: 'completed',
|
|
253
|
+
deviationCount: (newState.modeTransitions || []).length,
|
|
254
|
+
});
|
|
255
|
+
const flushedEvents = telemetryFlush();
|
|
256
|
+
if (flushedEvents.length > 0) {
|
|
257
|
+
const tConfig = getTelemetryConfig(newState.targetDir || process.cwd());
|
|
258
|
+
sendEvents(flushedEvents, { ...tConfig, version: newState.chatiVersion || 'unknown' });
|
|
259
|
+
}
|
|
154
260
|
return {
|
|
155
261
|
state: newState,
|
|
156
262
|
nextAction: 'complete',
|
|
157
263
|
nextAgent: null,
|
|
158
264
|
needsModeSwitch: false,
|
|
265
|
+
contextBracket,
|
|
159
266
|
};
|
|
160
267
|
} else {
|
|
161
268
|
// QA failed - wait for issues to be fixed
|
|
@@ -164,6 +271,7 @@ export function advancePipeline(pipelineState, completedAgent, results = {}) {
|
|
|
164
271
|
nextAction: 'wait',
|
|
165
272
|
nextAgent: null,
|
|
166
273
|
needsModeSwitch: false,
|
|
274
|
+
contextBracket,
|
|
167
275
|
};
|
|
168
276
|
}
|
|
169
277
|
}
|
|
@@ -192,16 +300,30 @@ export function advancePipeline(pipelineState, completedAgent, results = {}) {
|
|
|
192
300
|
nextAction: 'advance_phase',
|
|
193
301
|
nextAgent: getFirstAgentInPhase(newState, nextPhase),
|
|
194
302
|
needsModeSwitch: true,
|
|
303
|
+
contextBracket,
|
|
195
304
|
};
|
|
196
305
|
}
|
|
197
306
|
|
|
198
307
|
// Pipeline complete
|
|
199
308
|
newState.completedAt = new Date().toISOString();
|
|
309
|
+
telemetryTrack('pipeline_completed', {
|
|
310
|
+
pipelineType: newState.isQuickFlow ? 'quick-flow' : newState.isStandardFlow ? 'standard' : 'full',
|
|
311
|
+
totalDuration: Date.now() - new Date(newState.startedAt).getTime(),
|
|
312
|
+
agentsRun: newState.completedAgents.length,
|
|
313
|
+
finalStatus: 'completed',
|
|
314
|
+
deviationCount: (newState.modeTransitions || []).length,
|
|
315
|
+
});
|
|
316
|
+
const flushedEvents2 = telemetryFlush();
|
|
317
|
+
if (flushedEvents2.length > 0) {
|
|
318
|
+
const tConfig2 = getTelemetryConfig(newState.targetDir || process.cwd());
|
|
319
|
+
sendEvents(flushedEvents2, { ...tConfig2, version: newState.chatiVersion || 'unknown' });
|
|
320
|
+
}
|
|
200
321
|
return {
|
|
201
322
|
state: newState,
|
|
202
323
|
nextAction: 'complete',
|
|
203
324
|
nextAgent: null,
|
|
204
325
|
needsModeSwitch: false,
|
|
326
|
+
contextBracket,
|
|
205
327
|
};
|
|
206
328
|
}
|
|
207
329
|
|
|
@@ -220,6 +342,7 @@ export function advancePipeline(pipelineState, completedAgent, results = {}) {
|
|
|
220
342
|
nextAction: 'continue',
|
|
221
343
|
nextAgent: nextInfo.next,
|
|
222
344
|
needsModeSwitch: false,
|
|
345
|
+
contextBracket,
|
|
223
346
|
};
|
|
224
347
|
}
|
|
225
348
|
|
|
@@ -229,6 +352,7 @@ export function advancePipeline(pipelineState, completedAgent, results = {}) {
|
|
|
229
352
|
nextAction: 'wait',
|
|
230
353
|
nextAgent: null,
|
|
231
354
|
needsModeSwitch: false,
|
|
355
|
+
contextBracket,
|
|
232
356
|
};
|
|
233
357
|
}
|
|
234
358
|
|
|
@@ -14,7 +14,10 @@ const SESSION_FILE = '.chati/session.yaml';
|
|
|
14
14
|
/**
|
|
15
15
|
* Default session template.
|
|
16
16
|
*/
|
|
17
|
+
const CURRENT_SCHEMA_VERSION = '1.0';
|
|
18
|
+
|
|
17
19
|
const DEFAULT_SESSION = {
|
|
20
|
+
schema_version: CURRENT_SCHEMA_VERSION,
|
|
18
21
|
version: '1.0',
|
|
19
22
|
mode: 'discover',
|
|
20
23
|
project: { name: '', type: 'greenfield', state: 'discover' },
|
|
@@ -102,10 +105,38 @@ export function initSession(projectDir, options = {}) {
|
|
|
102
105
|
}
|
|
103
106
|
}
|
|
104
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Migrate a session object to the current schema version.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} session - Session object (may be missing schema_version)
|
|
112
|
+
* @returns {{ migrated: boolean, fromVersion: string|null, toVersion: string }}
|
|
113
|
+
*/
|
|
114
|
+
export function migrateSession(session) {
|
|
115
|
+
if (!session || typeof session !== 'object') {
|
|
116
|
+
return { migrated: false, fromVersion: null, toVersion: CURRENT_SCHEMA_VERSION };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (session.schema_version === CURRENT_SCHEMA_VERSION) {
|
|
120
|
+
return { migrated: false, fromVersion: session.schema_version, toVersion: CURRENT_SCHEMA_VERSION };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// v0 (no schema_version) → v1.0
|
|
124
|
+
const fromVersion = session.schema_version || null;
|
|
125
|
+
session.schema_version = CURRENT_SCHEMA_VERSION;
|
|
126
|
+
|
|
127
|
+
// Ensure fields added in v1.0 exist
|
|
128
|
+
if (!session.completed_agents) session.completed_agents = [];
|
|
129
|
+
if (!session.agent_results) session.agent_results = {};
|
|
130
|
+
if (!session.deviations) session.deviations = [];
|
|
131
|
+
if (!session.mode_transitions) session.mode_transitions = [];
|
|
132
|
+
|
|
133
|
+
return { migrated: true, fromVersion, toVersion: CURRENT_SCHEMA_VERSION };
|
|
134
|
+
}
|
|
135
|
+
|
|
105
136
|
/**
|
|
106
137
|
* Load current session state.
|
|
107
138
|
* @param {string} projectDir
|
|
108
|
-
* @returns {{ loaded: boolean, session: object|null, error: string|null }}
|
|
139
|
+
* @returns {{ loaded: boolean, session: object|null, error: string|null, migrated?: boolean }}
|
|
109
140
|
*/
|
|
110
141
|
export function loadSession(projectDir) {
|
|
111
142
|
const sessionPath = join(projectDir, SESSION_FILE);
|
|
@@ -122,10 +153,22 @@ export function loadSession(projectDir) {
|
|
|
122
153
|
const content = readFileSync(sessionPath, 'utf-8');
|
|
123
154
|
const session = yaml.load(content);
|
|
124
155
|
|
|
156
|
+
// Run migration if needed
|
|
157
|
+
const migration = migrateSession(session);
|
|
158
|
+
if (migration.migrated) {
|
|
159
|
+
try {
|
|
160
|
+
const yamlContent = yaml.dump(session, { lineWidth: -1, noRefs: true });
|
|
161
|
+
writeFileSync(sessionPath, yamlContent, 'utf-8');
|
|
162
|
+
} catch {
|
|
163
|
+
// Migration write failed — continue with migrated in-memory session
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
125
167
|
return {
|
|
126
168
|
loaded: true,
|
|
127
169
|
session,
|
|
128
170
|
error: null,
|
|
171
|
+
migrated: migration.migrated,
|
|
129
172
|
};
|
|
130
173
|
} catch (err) {
|
|
131
174
|
return {
|
|
@@ -351,6 +394,125 @@ export function getSessionSummary(projectDir) {
|
|
|
351
394
|
};
|
|
352
395
|
}
|
|
353
396
|
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// Team / Concurrent Support
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Claim session ownership for a user.
|
|
403
|
+
* Prevents concurrent users from modifying the same session.
|
|
404
|
+
*
|
|
405
|
+
* @param {string} projectDir
|
|
406
|
+
* @param {string} userId - Unique user identifier
|
|
407
|
+
* @returns {{ claimed: boolean, owner?: string, since?: string, error?: string }}
|
|
408
|
+
*/
|
|
409
|
+
export function claimSession(projectDir, userId) {
|
|
410
|
+
if (!projectDir || !userId) {
|
|
411
|
+
return { claimed: false, error: 'projectDir and userId are required' };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const loadResult = loadSession(projectDir);
|
|
415
|
+
|
|
416
|
+
if (!loadResult.loaded) {
|
|
417
|
+
return { claimed: false, error: loadResult.error };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const session = loadResult.session;
|
|
421
|
+
|
|
422
|
+
// Check existing ownership
|
|
423
|
+
if (session._owner && session._owner.userId && session._owner.userId !== userId) {
|
|
424
|
+
return {
|
|
425
|
+
claimed: false,
|
|
426
|
+
owner: session._owner.userId,
|
|
427
|
+
since: session._owner.since,
|
|
428
|
+
error: `Session already claimed by ${session._owner.userId}`,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Claim it
|
|
433
|
+
const ownership = {
|
|
434
|
+
userId,
|
|
435
|
+
since: new Date().toISOString(),
|
|
436
|
+
hostname: typeof globalThis !== 'undefined' ? (globalThis.process?.env?.HOSTNAME || 'localhost') : 'localhost',
|
|
437
|
+
pid: process.pid,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const updateResult = updateSession(projectDir, { _owner: ownership });
|
|
441
|
+
|
|
442
|
+
if (!updateResult.saved) {
|
|
443
|
+
return { claimed: false, error: updateResult.error };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return { claimed: true, owner: userId, since: ownership.since };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Release session ownership.
|
|
451
|
+
*
|
|
452
|
+
* @param {string} projectDir
|
|
453
|
+
* @param {string} userId - User releasing the session
|
|
454
|
+
* @returns {{ released: boolean, error?: string }}
|
|
455
|
+
*/
|
|
456
|
+
export function releaseSession(projectDir, userId) {
|
|
457
|
+
if (!projectDir || !userId) {
|
|
458
|
+
return { released: false, error: 'projectDir and userId are required' };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const loadResult = loadSession(projectDir);
|
|
462
|
+
|
|
463
|
+
if (!loadResult.loaded) {
|
|
464
|
+
return { released: false, error: loadResult.error };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const session = loadResult.session;
|
|
468
|
+
|
|
469
|
+
// Only the owner can release
|
|
470
|
+
if (session._owner && session._owner.userId && session._owner.userId !== userId) {
|
|
471
|
+
return {
|
|
472
|
+
released: false,
|
|
473
|
+
error: `Cannot release: session owned by ${session._owner.userId}`,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const updateResult = updateSession(projectDir, { _owner: null });
|
|
478
|
+
|
|
479
|
+
if (!updateResult.saved) {
|
|
480
|
+
return { released: false, error: updateResult.error };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return { released: true };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Get current session owner information.
|
|
488
|
+
*
|
|
489
|
+
* @param {string} projectDir
|
|
490
|
+
* @returns {{ owner: string|null, since: string|null, hostname: string|null }}
|
|
491
|
+
*/
|
|
492
|
+
export function getSessionOwner(projectDir) {
|
|
493
|
+
if (!projectDir) {
|
|
494
|
+
return { owner: null, since: null, hostname: null };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const loadResult = loadSession(projectDir);
|
|
498
|
+
|
|
499
|
+
if (!loadResult.loaded || !loadResult.session._owner) {
|
|
500
|
+
return { owner: null, since: null, hostname: null };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const ownership = loadResult.session._owner;
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
owner: ownership.userId || null,
|
|
507
|
+
since: ownership.since || null,
|
|
508
|
+
hostname: ownership.hostname || null,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
// Validation
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
|
|
354
516
|
/**
|
|
355
517
|
* Check if a session exists and is valid.
|
|
356
518
|
* @param {string} projectDir
|
|
@@ -380,7 +542,7 @@ export function validateSession(projectDir) {
|
|
|
380
542
|
const session = loadResult.session;
|
|
381
543
|
|
|
382
544
|
// Validate required fields
|
|
383
|
-
const requiredFields = ['version', 'mode', 'language', 'project_type', 'agents'];
|
|
545
|
+
const requiredFields = ['schema_version', 'version', 'mode', 'language', 'project_type', 'agents'];
|
|
384
546
|
for (const field of requiredFields) {
|
|
385
547
|
if (!session[field]) {
|
|
386
548
|
return {
|