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