chati-dev 4.1.6 → 4.2.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.
@@ -0,0 +1,73 @@
1
+ # Team Planning Tasks Template (Article XXI)
2
+ # Used by orchestrator when forming a Planning Team (post-Brief)
3
+ # Cross-review assignments form a circular review: Detail -> Architect -> UX -> Detail
4
+
5
+ team_type: planning
6
+ phase: plan
7
+ mission: "Co-design PRD, architecture, and UX specification with cross-validated alignment"
8
+
9
+ tasks:
10
+ - id: TT-PLN-001
11
+ title: "Expand Brief into PRD with acceptance criteria"
12
+ assigned_to: detail
13
+ status: pending
14
+ score: null
15
+ blocker: null
16
+ depends_on: []
17
+ cross_review:
18
+ target: architect
19
+ questions:
20
+ - "Do PRD entities align with the data architecture constraints you anticipate?"
21
+ - "Are there requirements that would create architectural contradictions?"
22
+ - "Are scope decisions in Section 4 likely to conflict with your design?"
23
+ artifacts_produced:
24
+ - prd.md
25
+ threshold: 90
26
+
27
+ - id: TT-PLN-002
28
+ title: "Design system architecture and data architecture"
29
+ assigned_to: architect
30
+ status: pending
31
+ score: null
32
+ blocker: null
33
+ depends_on: []
34
+ cross_review:
35
+ target: ux
36
+ questions:
37
+ - "Does the responsive strategy conflict with deployment architecture constraints?"
38
+ - "Do Design System tokens account for API response latency in loading states?"
39
+ - "Are there component complexity choices that contradict the scalability approach?"
40
+ artifacts_produced:
41
+ - architecture.md
42
+ threshold: 90
43
+
44
+ - id: TT-PLN-003
45
+ title: "Design UX specification with brandbook, user flows, and components"
46
+ assigned_to: ux
47
+ status: pending
48
+ score: null
49
+ blocker: null
50
+ depends_on: []
51
+ cross_review:
52
+ target: detail
53
+ questions:
54
+ - "Do any PRD requirements conflict with the selected visual direction?"
55
+ - "Are there user flows implying requirements not captured in the PRD?"
56
+ - "Is the target user profile consistent with UX research findings?"
57
+ artifacts_produced:
58
+ - ux-specification.md
59
+ - brandbook.md
60
+ - brandbook.html
61
+ threshold: 90
62
+
63
+ - id: TT-PLN-004
64
+ title: "Cross-validate alignment: PRD entities = DB tables = UI components"
65
+ assigned_to: null
66
+ status: pending
67
+ score: null
68
+ blocker: null
69
+ depends_on: [TT-PLN-001, TT-PLN-002, TT-PLN-003]
70
+ cross_review: null
71
+ artifacts_produced: []
72
+ threshold: 95
73
+ notes: "Assigned by Team Lead after individual tasks complete. All 3 members participate."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chati-dev",
3
- "version": "4.1.6",
3
+ "version": "4.2.0",
4
4
  "description": "AI-Powered Multi-Agent Orchestration System — Structured vibe coding for Full Stack Development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@ export const SAFETY_TRIGGERS = {
12
12
  CRITICAL_RISK: 'critical_risk', // Security/data risk detected
13
13
  TIMEOUT: 'timeout', // Agent exceeded time limit
14
14
  GOTCHA_SPIKE: 'gotcha_spike', // 5+ new gotchas in current session
15
+ EDIT_LOOP: 'edit_loop', // Same file edited 3+ times in current task (Article XX)
15
16
  };
16
17
 
17
18
  // Critical risk keywords
@@ -98,6 +99,9 @@ export function evaluateTrigger(trigger, state) {
98
99
  case SAFETY_TRIGGERS.GOTCHA_SPIKE:
99
100
  return evaluateGotchaSpike(state);
100
101
 
102
+ case SAFETY_TRIGGERS.EDIT_LOOP:
103
+ return evaluateEditLoop(state);
104
+
101
105
  default:
102
106
  return {
103
107
  triggered: false,
@@ -291,6 +295,34 @@ function evaluateGotchaSpike(state) {
291
295
  };
292
296
  }
293
297
 
298
+ /**
299
+ * Article XX — Detect edit loops (same file edited 3+ times in current task).
300
+ * state.fileEdits should be an object: { 'path/to/file.tsx': 3, ... }
301
+ */
302
+ function evaluateEditLoop(state) {
303
+ const threshold = 3;
304
+ const fileEdits = state.fileEdits || {};
305
+ const loopedFiles = Object.entries(fileEdits)
306
+ .filter(([, count]) => count >= threshold);
307
+
308
+ if (loopedFiles.length > 0) {
309
+ const details = loopedFiles
310
+ .map(([path, count]) => `${path} (${count} edits)`)
311
+ .join(', ');
312
+ return {
313
+ triggered: true,
314
+ severity: 'critical',
315
+ details: `Edit loop detected (Article XX): ${details}. Stop and diagnose root cause before continuing.`,
316
+ };
317
+ }
318
+
319
+ return {
320
+ triggered: false,
321
+ severity: 'warning',
322
+ details: 'No edit loops detected',
323
+ };
324
+ }
325
+
294
326
  /**
295
327
  * Get critical risk keywords list.
296
328
  * @returns {string[]}
@@ -30,8 +30,11 @@ import {
30
30
  import { analyzeDeviationImpact, applyDeviation } from './index.js';
31
31
  import { detectQuickFlow, detectStandardFlow } from './index.js';
32
32
  import { AGENT_FILE_MAP } from '../terminal/prompt-builder.js';
33
- import { readFileSync, writeFileSync, existsSync } from 'fs';
33
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
34
34
  import { join, resolve } from 'path';
35
+ import {
36
+ initTaskList, readTaskList, getTeamProgress,
37
+ } from '../terminal/team-task-list.js';
35
38
 
36
39
  // ---------------------------------------------------------------------------
37
40
  // Constants
@@ -375,12 +378,27 @@ async function handleNext(projectDir) {
375
378
  // Resolve model for next agent
376
379
  const modelInfo = resolveAgentModel(nextAgent, projectDir);
377
380
 
378
- // Check for parallel group
381
+ // Check for parallel group — with Agent Teams override (Article XXI)
379
382
  let action, spawnCommand = null, parallelSpawnCommand = null, parallelAgents = [];
383
+ let teamData = null;
380
384
  if (nextInfo.isParallel && nextInfo.group.length > 1) {
381
- action = 'spawn_parallel';
382
- parallelAgents = nextInfo.group;
383
- parallelSpawnCommand = buildParallelSpawnCommand(nextInfo.group, projectDir, lastAgent, modelInfo.provider, 900000);
385
+ // When agent_teams is enabled, substitute spawn_parallel with spawn_team
386
+ if (isAgentTeamsEnabled(projectDir)) {
387
+ const teamType = nextInfo.group.includes('dev') ? 'build' : 'planning';
388
+ const teamConfig = TEAM_CONFIGS[teamType];
389
+ const teamId = generateTeamId(teamConfig.slug);
390
+ action = 'spawn_team';
391
+ parallelAgents = teamConfig.members;
392
+ teamData = {
393
+ team_id: teamId,
394
+ team_type: teamType,
395
+ members: teamConfig.members,
396
+ };
397
+ } else {
398
+ action = 'spawn_parallel';
399
+ parallelAgents = nextInfo.group;
400
+ parallelSpawnCommand = buildParallelSpawnCommand(nextInfo.group, projectDir, lastAgent, modelInfo.provider, 900000);
401
+ }
384
402
  } else if (isInteractive) {
385
403
  action = 'activate_interactive';
386
404
  } else {
@@ -391,7 +409,7 @@ async function handleNext(projectDir) {
391
409
  const progress = getPipelineProgress(pipelineState);
392
410
  const bracket = estimateContextBracket(completedAgents.length, AGENT_PIPELINE.length);
393
411
 
394
- return {
412
+ const result = {
395
413
  action,
396
414
  agent: nextAgent,
397
415
  agent_file: agentFile,
@@ -407,6 +425,15 @@ async function handleNext(projectDir) {
407
425
  session: { language: session.language, project_type: session.project_type || session.project?.type, execution_mode: session.execution_mode, user_level: session.user_level || 'auto' },
408
426
  status_summary: `${lastAgent} completed. Next: ${nextAgent} (${action.replace('_', ' ')}, ${agentDef?.phase || 'unknown'} phase).`,
409
427
  };
428
+
429
+ // Include team data when spawn_team action is selected (Article XXI)
430
+ if (teamData) {
431
+ result.team_id = teamData.team_id;
432
+ result.team_type = teamData.team_type;
433
+ result.members = teamData.members;
434
+ }
435
+
436
+ return result;
410
437
  }
411
438
 
412
439
  async function handleAdvance(projectDir, args) {
@@ -929,6 +956,367 @@ async function handleScan(projectDir, args) {
929
956
  }
930
957
  }
931
958
 
959
+ // ---------------------------------------------------------------------------
960
+ // Agent Teams Handlers (Article XXI)
961
+ // ---------------------------------------------------------------------------
962
+
963
+ const TEAM_CONFIGS = {
964
+ planning: {
965
+ members: ['detail', 'architect', 'ux'],
966
+ slug: 'pln',
967
+ templateFile: 'team-planning-tasks.yaml',
968
+ },
969
+ build: {
970
+ members: ['dev', 'qa-implementation'],
971
+ slug: 'bld',
972
+ templateFile: 'team-build-tasks.yaml',
973
+ },
974
+ };
975
+
976
+ function generateTeamId(slug) {
977
+ const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
978
+ return `TM-${date}-${slug}`;
979
+ }
980
+
981
+ function isAgentTeamsEnabled(projectDir) {
982
+ const configPath = join(projectDir, 'chati.dev', 'config.yaml');
983
+ if (!existsSync(configPath)) return false;
984
+ const raw = readFileSync(configPath, 'utf-8');
985
+ const match = raw.match(/agent_teams:\s*(true|false)/);
986
+ return match ? match[1] === 'true' : false;
987
+ }
988
+
989
+ async function handleSpawnTeam(projectDir, args) {
990
+ const teamType = args['team-type'];
991
+ if (!teamType || !TEAM_CONFIGS[teamType]) {
992
+ return errorResult('--team-type required (planning|build)', 'MISSING_TEAM_TYPE');
993
+ }
994
+
995
+ // Check feature flag
996
+ if (!isAgentTeamsEnabled(projectDir)) {
997
+ return {
998
+ action: 'spawn_parallel',
999
+ fallback_required: true,
1000
+ fallback_reason: 'agent_teams_disabled',
1001
+ };
1002
+ }
1003
+
1004
+ const config = TEAM_CONFIGS[teamType];
1005
+ const teamId = generateTeamId(config.slug);
1006
+ const teamDir = join(projectDir, '.chati', 'teams', teamId);
1007
+ const mailboxDir = join(teamDir, 'mailbox');
1008
+ const taskListPath = join(teamDir, 'tasks.yaml');
1009
+ const templatePath = join(projectDir, 'chati.dev', 'templates', config.templateFile);
1010
+
1011
+ // Create team directories
1012
+ mkdirSync(mailboxDir, { recursive: true });
1013
+
1014
+ // Initialize task list from template
1015
+ try {
1016
+ initTaskList(taskListPath, templatePath, { team_id: teamId });
1017
+
1018
+ // For build teams: dynamically populate tasks from tasks.md (Article XXI §9)
1019
+ if (teamType === 'build') {
1020
+ const tasksArtifact = join(projectDir, 'chati.dev', 'artifacts', '6-Tasks', 'tasks.md');
1021
+ if (existsSync(tasksArtifact)) {
1022
+ const tasksContent = readFileSync(tasksArtifact, 'utf-8');
1023
+ // Extract task IDs (pattern: T{phase}.{seq})
1024
+ const taskMatches = tasksContent.match(/^#+\s*(T\d+\.\d+)[:\s—-]+(.+)$/gm) || [];
1025
+ const dynamicTasks = [];
1026
+ let seq = 1;
1027
+
1028
+ for (const match of taskMatches) {
1029
+ const m = match.match(/^#+\s*(T\d+\.\d+)[:\s—-]+(.+)$/);
1030
+ if (!m) continue;
1031
+ const [, taskRef, title] = m;
1032
+ const seqStr = String(seq).padStart(3, '0');
1033
+
1034
+ // Dev task
1035
+ dynamicTasks.push({
1036
+ id: `TT-BLD-${seqStr}-DEV`,
1037
+ title: `${taskRef}: ${title.trim()}`,
1038
+ assigned_to: 'dev',
1039
+ status: 'pending',
1040
+ score: null,
1041
+ blocker: null,
1042
+ source_task: taskRef,
1043
+ depends_on: seq > 1 ? [`TT-BLD-${String(seq - 1).padStart(3, '0')}-QA`] : [],
1044
+ threshold: 95,
1045
+ });
1046
+
1047
+ // QA review task
1048
+ dynamicTasks.push({
1049
+ id: `TT-BLD-${seqStr}-QA`,
1050
+ title: `${taskRef}: Review + approve`,
1051
+ assigned_to: 'qa-implementation',
1052
+ status: 'pending',
1053
+ score: null,
1054
+ blocker: null,
1055
+ source_task: taskRef,
1056
+ depends_on: [`TT-BLD-${seqStr}-DEV`],
1057
+ threshold: 95,
1058
+ });
1059
+
1060
+ seq++;
1061
+ }
1062
+
1063
+ if (dynamicTasks.length > 0) {
1064
+ const taskList = readTaskList(taskListPath);
1065
+ if (taskList) {
1066
+ taskList.tasks = dynamicTasks;
1067
+ taskList.last_updated = new Date().toISOString();
1068
+ const yaml = await import('js-yaml');
1069
+ writeFileSync(taskListPath, yaml.default.dump(taskList, { lineWidth: -1, noRefs: true }), 'utf-8');
1070
+ }
1071
+ }
1072
+ }
1073
+ }
1074
+ } catch (err) {
1075
+ return errorResult(`Failed to init team task list: ${err.message}`, 'TEAM_INIT_ERROR');
1076
+ }
1077
+
1078
+ // Update session with team entry
1079
+ const previousAgent = args['previous-agent'] || 'none';
1080
+ const provider = args.provider || 'claude';
1081
+
1082
+ try {
1083
+ const { loaded, session } = loadSession(projectDir);
1084
+ if (loaded && session) {
1085
+ if (!session.teams) session.teams = [];
1086
+ session.teams.push({
1087
+ team_id: teamId,
1088
+ phase: teamType === 'planning' ? 'planning' : 'build',
1089
+ mission: teamType === 'planning'
1090
+ ? 'Co-design PRD, architecture, and UX specification with cross-validated alignment'
1091
+ : 'Implement all tasks with per-task QA review and evidence-based quality gates',
1092
+ lead: 'orchestrator',
1093
+ roster: config.members,
1094
+ status: 'forming',
1095
+ task_list_path: `.chati/teams/${teamId}/tasks.yaml`,
1096
+ mailbox_path: `.chati/teams/${teamId}/mailbox/`,
1097
+ formed_at: new Date().toISOString(),
1098
+ dissolved_at: null,
1099
+ team_score: null,
1100
+ member_scores: {},
1101
+ correction_cycles: 0,
1102
+ echo_events: [],
1103
+ fallback_used: false,
1104
+ });
1105
+
1106
+ if (!session.team_events) session.team_events = [];
1107
+ session.team_events.push({
1108
+ timestamp: new Date().toISOString(),
1109
+ event: 'formed',
1110
+ team_id: teamId,
1111
+ trigger: `${teamType} team formation after ${previousAgent}`,
1112
+ });
1113
+
1114
+ await updateSession(projectDir, session);
1115
+ }
1116
+ } catch { /* non-critical: session update may fail, team can still spawn */ }
1117
+
1118
+ // Build spawn command
1119
+ const spawnCmd = [
1120
+ 'node', join('packages', 'chati-dev', 'src', 'terminal', 'run-team.js'),
1121
+ '--team-id', teamId,
1122
+ '--team-type', teamType,
1123
+ '--project-dir', resolve(projectDir),
1124
+ '--previous-agent', previousAgent,
1125
+ '--provider', provider,
1126
+ '--timeout', args.timeout || '1800000',
1127
+ ].join(' ');
1128
+
1129
+ return {
1130
+ action: 'spawn_team',
1131
+ team_id: teamId,
1132
+ team_type: teamType,
1133
+ members: config.members,
1134
+ task_list_path: `.chati/teams/${teamId}/tasks.yaml`,
1135
+ mailbox_path: `.chati/teams/${teamId}/mailbox/`,
1136
+ spawn_team_command: spawnCmd,
1137
+ poll_interval_ms: 5000,
1138
+ fallback_required: false,
1139
+ };
1140
+ }
1141
+
1142
+ async function handleTeamStatus(projectDir, args) {
1143
+ const teamId = args['team-id'];
1144
+ if (!teamId) return errorResult('--team-id required', 'MISSING_TEAM_ID');
1145
+
1146
+ const taskListPath = join(projectDir, '.chati', 'teams', teamId, 'tasks.yaml');
1147
+ const progress = getTeamProgress(taskListPath);
1148
+
1149
+ // Read echo events from session
1150
+ let echoEvents = [];
1151
+ try {
1152
+ const { loaded, session } = loadSession(projectDir);
1153
+ if (loaded && session && session.teams) {
1154
+ const team = session.teams.find(t => t.team_id === teamId);
1155
+ if (team) echoEvents = team.echo_events || [];
1156
+ }
1157
+ } catch { /* non-critical */ }
1158
+
1159
+ return {
1160
+ team_id: teamId,
1161
+ status: progress.done === progress.total ? 'completing' : 'active',
1162
+ member_progress: progress.memberProgress,
1163
+ total_tasks: progress.total,
1164
+ completed_tasks: progress.done,
1165
+ pending_tasks: progress.pending,
1166
+ blocked_tasks: progress.blocked,
1167
+ echo_events: echoEvents,
1168
+ };
1169
+ }
1170
+
1171
+ async function handleTeamDissolve(projectDir, args) {
1172
+ const teamId = args['team-id'];
1173
+ if (!teamId) return errorResult('--team-id required', 'MISSING_TEAM_ID');
1174
+
1175
+ const taskListPath = join(projectDir, '.chati', 'teams', teamId, 'tasks.yaml');
1176
+ const progress = getTeamProgress(taskListPath);
1177
+
1178
+ // Derive team type from team ID slug (for mailbox checks and tier threshold)
1179
+ const teamType = teamId.includes('-bld') ? 'build' : 'planning';
1180
+
1181
+ // Read echo events from session
1182
+ let echoEvents = [];
1183
+ try {
1184
+ const { loaded: echoLoaded, session: echoSession } = loadSession(projectDir);
1185
+ if (echoLoaded && echoSession && echoSession.teams) {
1186
+ const team = echoSession.teams.find(t => t.team_id === teamId);
1187
+ if (team) echoEvents = team.echo_events || [];
1188
+ }
1189
+ } catch { /* best-effort */ }
1190
+
1191
+ // Calculate team score
1192
+ const memberScores = {};
1193
+ let totalScore = 0;
1194
+ let scoreCount = 0;
1195
+ for (const [member, data] of Object.entries(progress.memberProgress)) {
1196
+ if (data.score !== null) {
1197
+ memberScores[member] = data.score;
1198
+ totalScore += data.score;
1199
+ scoreCount++;
1200
+ }
1201
+ }
1202
+ const teamScore = scoreCount > 0 ? Math.round((totalScore / scoreCount) * 10) / 10 : 0;
1203
+
1204
+ // Team Quality Gate — verify all 6 conditions (Article XXI section 6)
1205
+ const allDone = progress.done === progress.total;
1206
+ // Dynamic tier threshold per Article XXI §6: must meet highest tier in roster
1207
+ // QA agents (qa-planning, qa-implementation) require 95%, standard agents 90%
1208
+ const QA_AGENTS = ['qa-planning', 'qa-implementation'];
1209
+ const teamConfig = TEAM_CONFIGS[teamType];
1210
+ const hasQaAgent = teamConfig ? teamConfig.members.some(m => QA_AGENTS.includes(m)) : false;
1211
+ const tierThreshold = hasQaAgent ? 95 : 90;
1212
+ const allScoresPass = Object.values(memberScores).every(s => s >= tierThreshold);
1213
+ const noBlockers = progress.blocked === 0;
1214
+
1215
+ // Check mailbox cleanliness
1216
+ let mailboxClean = true;
1217
+ try {
1218
+ const { readInbox } = await import('../terminal/team-task-list.js');
1219
+ const mailboxDir = join(projectDir, '.chati', 'teams', teamId, 'mailbox');
1220
+ // Check all members for unresolved messages
1221
+ const dissolveTeamConfig = TEAM_CONFIGS[teamType];
1222
+ if (dissolveTeamConfig) {
1223
+ for (const member of dissolveTeamConfig.members) {
1224
+ const inbox = readInbox(mailboxDir, member);
1225
+ const unresolved = inbox.filter(m => m.type === 'task_review_findings' && m.payload?.verdict === 'block');
1226
+ if (unresolved.length > 0) { mailboxClean = false; break; }
1227
+ }
1228
+ }
1229
+ } catch { /* mailbox check is best-effort */ }
1230
+
1231
+ // Check correction cycles against config limit (Article XXI §6)
1232
+ let correctionCyclesOk = true;
1233
+ let maxCorrectionCycles = 2; // default per constitution
1234
+ try {
1235
+ const configPath = join(projectDir, 'chati.dev', 'config.yaml');
1236
+ if (existsSync(configPath)) {
1237
+ const configRaw = readFileSync(configPath, 'utf-8');
1238
+ const maxMatch = configRaw.match(/team_correction_cycles_max:\s*(\d+)/);
1239
+ if (maxMatch) maxCorrectionCycles = parseInt(maxMatch[1], 10);
1240
+ }
1241
+ } catch { /* use default */ }
1242
+ try {
1243
+ const { loaded, session: sess } = loadSession(projectDir);
1244
+ if (loaded && sess && sess.teams) {
1245
+ const team = sess.teams.find(t => t.team_id === teamId);
1246
+ if (team && team.correction_cycles > maxCorrectionCycles) correctionCyclesOk = false;
1247
+ }
1248
+ } catch { /* best-effort */ }
1249
+
1250
+ // Determine dissolution state based on all conditions
1251
+ const qualityGatePassed = allDone && allScoresPass && mailboxClean && noBlockers && correctionCyclesOk && teamScore >= tierThreshold;
1252
+ const state = qualityGatePassed ? 'clean' : 'degraded';
1253
+ const gateFailures = [];
1254
+ if (!allDone) gateFailures.push('incomplete_tasks');
1255
+ if (!allScoresPass) gateFailures.push('scores_below_threshold');
1256
+ if (!mailboxClean) gateFailures.push('unresolved_mailbox_messages');
1257
+ if (!noBlockers) gateFailures.push('open_blockers');
1258
+ if (!correctionCyclesOk) gateFailures.push('correction_cycles_exceeded');
1259
+ if (teamScore < 90) gateFailures.push('team_score_below_threshold');
1260
+
1261
+ // Update session
1262
+ try {
1263
+ const { loaded, session } = loadSession(projectDir);
1264
+ if (loaded && session && session.teams) {
1265
+ const team = session.teams.find(t => t.team_id === teamId);
1266
+ if (team) {
1267
+ team.status = state === 'clean' ? 'dissolved' : 'degraded';
1268
+ team.dissolved_at = new Date().toISOString();
1269
+ team.team_score = teamScore;
1270
+ team.member_scores = memberScores;
1271
+ }
1272
+
1273
+ if (!session.team_events) session.team_events = [];
1274
+ session.team_events.push({
1275
+ timestamp: new Date().toISOString(),
1276
+ event: state === 'clean' ? 'dissolved' : 'degraded',
1277
+ team_id: teamId,
1278
+ trigger: state === 'clean' ? 'Team Quality Gate passed' : `Degraded dissolution — failures: ${gateFailures.join(', ')}`,
1279
+ state,
1280
+ });
1281
+
1282
+ // Sync member scores to agents block
1283
+ if (session.agents) {
1284
+ for (const [member, score] of Object.entries(memberScores)) {
1285
+ if (session.agents[member]) {
1286
+ session.agents[member].score = score;
1287
+ session.agents[member].status = 'completed';
1288
+ session.agents[member].completed_at = new Date().toISOString();
1289
+ }
1290
+ }
1291
+ }
1292
+
1293
+ await updateSession(projectDir, session);
1294
+ }
1295
+ } catch { /* non-critical */ }
1296
+
1297
+ return {
1298
+ dissolved: true,
1299
+ team_id: teamId,
1300
+ team_score: teamScore,
1301
+ member_scores: memberScores,
1302
+ state,
1303
+ all_tasks_complete: allDone,
1304
+ echo_events: echoEvents,
1305
+ quality_gate: {
1306
+ passed: qualityGatePassed,
1307
+ failures: gateFailures,
1308
+ checks: {
1309
+ all_tasks_complete: allDone,
1310
+ all_scores_pass_threshold: allScoresPass,
1311
+ mailbox_clean: mailboxClean,
1312
+ no_open_blockers: noBlockers,
1313
+ correction_cycles_ok: correctionCyclesOk,
1314
+ team_score_above_threshold: teamScore >= 90,
1315
+ },
1316
+ },
1317
+ };
1318
+ }
1319
+
932
1320
  // ---------------------------------------------------------------------------
933
1321
  // Main entry point
934
1322
  // ---------------------------------------------------------------------------
@@ -977,8 +1365,17 @@ export async function runOrchestrate(subCommand, argv, projectDir) {
977
1365
  case 'scan':
978
1366
  output(await handleScan(projectDir, args));
979
1367
  break;
1368
+ case 'spawn-team':
1369
+ output(await handleSpawnTeam(projectDir, args));
1370
+ break;
1371
+ case 'team-status':
1372
+ output(await handleTeamStatus(projectDir, args));
1373
+ break;
1374
+ case 'team-dissolve':
1375
+ output(await handleTeamDissolve(projectDir, args));
1376
+ break;
980
1377
  default:
981
- output(errorResult(`Unknown sub-command: ${subCommand}. Valid: next, advance, init, validate-handoff, status, deviation, exit, providers, detect-flow, backlog, qa-plan-score, qa-impl-score, scan`, 'UNKNOWN_COMMAND'));
1378
+ output(errorResult(`Unknown sub-command: ${subCommand}. Valid: next, advance, init, validate-handoff, status, deviation, exit, providers, detect-flow, backlog, qa-plan-score, qa-impl-score, scan, spawn-team, team-status, team-dissolve`, 'UNKNOWN_COMMAND'));
982
1379
  break;
983
1380
  }
984
1381
  } catch (err) {