agileflow 2.99.7 → 3.0.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 (65) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/cache-provider.js +155 -0
  3. package/lib/codebase-indexer.js +1 -1
  4. package/lib/content-sanitizer.js +1 -0
  5. package/lib/dashboard-protocol.js +25 -0
  6. package/lib/dashboard-server.js +184 -133
  7. package/lib/errors.js +18 -0
  8. package/lib/file-cache.js +1 -1
  9. package/lib/flag-detection.js +11 -20
  10. package/lib/git-operations.js +15 -33
  11. package/lib/merge-operations.js +40 -34
  12. package/lib/process-executor.js +199 -0
  13. package/lib/registry-cache.js +13 -47
  14. package/lib/skill-loader.js +206 -0
  15. package/lib/smart-json-file.js +2 -4
  16. package/package.json +1 -1
  17. package/scripts/agileflow-configure.js +13 -12
  18. package/scripts/agileflow-statusline.sh +30 -0
  19. package/scripts/agileflow-welcome.js +181 -212
  20. package/scripts/auto-self-improve.js +3 -3
  21. package/scripts/claude-smart.sh +67 -0
  22. package/scripts/claude-tmux.sh +248 -170
  23. package/scripts/damage-control-multi-agent.js +227 -0
  24. package/scripts/lib/bus-utils.js +471 -0
  25. package/scripts/lib/configure-detect.js +5 -6
  26. package/scripts/lib/configure-features.js +44 -0
  27. package/scripts/lib/configure-repair.js +5 -6
  28. package/scripts/lib/configure-utils.js +2 -3
  29. package/scripts/lib/context-formatter.js +87 -8
  30. package/scripts/lib/damage-control-utils.js +37 -3
  31. package/scripts/lib/file-lock.js +392 -0
  32. package/scripts/lib/ideation-index.js +2 -5
  33. package/scripts/lib/lifecycle-detector.js +123 -0
  34. package/scripts/lib/process-cleanup.js +55 -81
  35. package/scripts/lib/scale-detector.js +357 -0
  36. package/scripts/lib/signal-detectors.js +779 -0
  37. package/scripts/lib/story-state-machine.js +1 -1
  38. package/scripts/lib/sync-ideation-status.js +2 -3
  39. package/scripts/lib/task-registry.js +7 -1
  40. package/scripts/lib/team-events.js +357 -0
  41. package/scripts/messaging-bridge.js +79 -36
  42. package/scripts/migrate-ideation-index.js +37 -14
  43. package/scripts/obtain-context.js +37 -19
  44. package/scripts/ralph-loop.js +3 -4
  45. package/scripts/smart-detect.js +390 -0
  46. package/scripts/team-manager.js +174 -30
  47. package/src/core/commands/audit.md +13 -11
  48. package/src/core/commands/babysit.md +162 -115
  49. package/src/core/commands/changelog.md +21 -4
  50. package/src/core/commands/configure.md +105 -2
  51. package/src/core/commands/debt.md +12 -2
  52. package/src/core/commands/feedback.md +7 -6
  53. package/src/core/commands/ideate/history.md +1 -1
  54. package/src/core/commands/ideate/new.md +5 -5
  55. package/src/core/commands/logic/audit.md +2 -2
  56. package/src/core/commands/pr.md +7 -6
  57. package/src/core/commands/research/analyze.md +28 -20
  58. package/src/core/commands/research/ask.md +43 -0
  59. package/src/core/commands/research/import.md +29 -21
  60. package/src/core/commands/research/list.md +8 -7
  61. package/src/core/commands/research/synthesize.md +356 -20
  62. package/src/core/commands/research/view.md +8 -5
  63. package/src/core/commands/review.md +24 -6
  64. package/src/core/commands/skill/create.md +34 -0
  65. package/tools/cli/lib/docs-setup.js +4 -0
@@ -21,32 +21,17 @@
21
21
 
22
22
  'use strict';
23
23
 
24
- const http = require('http');
25
- const crypto = require('crypto');
26
24
  const { EventEmitter } = require('events');
27
- const {
28
- OutboundMessageType,
29
- InboundMessageType,
30
- createSessionState,
31
- createError,
32
- createNotification,
33
- createGitDiff,
34
- createTerminalOutput,
35
- createTerminalExit,
36
- createAutomationList,
37
- createAutomationStatus,
38
- createAutomationResult,
39
- createInboxList,
40
- createInboxItem,
41
- createStatusUpdate,
42
- createSessionList,
43
- parseInboundMessage,
44
- serializeMessage,
45
- } = require('./dashboard-protocol');
46
- const { getProjectRoot, isAgileflowProject, getAgentsDir } = require('./paths');
47
- const { validatePath } = require('./validate-paths');
48
- const { execFileSync, spawn } = require('child_process');
49
- const os = require('os');
25
+
26
+ // Lazy-loaded dependencies - deferred until first use
27
+ let _http, _crypto, _protocol, _paths, _validatePaths, _childProcess;
28
+
29
+ function getHttp() { if (!_http) _http = require('http'); return _http; }
30
+ function getCrypto() { if (!_crypto) _crypto = require('crypto'); return _crypto; }
31
+ function getProtocol() { if (!_protocol) _protocol = require('./dashboard-protocol'); return _protocol; }
32
+ function getPaths() { if (!_paths) _paths = require('./paths'); return _paths; }
33
+ function getValidatePaths() { if (!_validatePaths) _validatePaths = require('./validate-paths'); return _validatePaths; }
34
+ function getChildProcess() { if (!_childProcess) _childProcess = require('child_process'); return _childProcess; }
50
35
 
51
36
  // Lazy-load automation modules to avoid circular dependencies
52
37
  let AutomationRegistry = null;
@@ -142,7 +127,7 @@ class DashboardSession {
142
127
  send(message) {
143
128
  if (this.ws && this.ws.writable) {
144
129
  try {
145
- const frame = encodeWebSocketFrame(serializeMessage(message));
130
+ const frame = encodeWebSocketFrame(getProtocol().serializeMessage(message));
146
131
  this.ws.write(frame);
147
132
  this.lastActivity = new Date();
148
133
  } catch (error) {
@@ -180,7 +165,7 @@ class DashboardSession {
180
165
  setState(state) {
181
166
  this.state = state;
182
167
  this.send(
183
- createSessionState(this.id, state, {
168
+ getProtocol().createSessionState(this.id, state, {
184
169
  messageCount: this.messages.length,
185
170
  lastActivity: this.lastActivity.toISOString(),
186
171
  })
@@ -249,13 +234,13 @@ class TerminalInstance {
249
234
 
250
235
  this.pty.onData(data => {
251
236
  if (!this.closed) {
252
- this.session.send(createTerminalOutput(this.id, data));
237
+ this.session.send(getProtocol().createTerminalOutput(this.id, data));
253
238
  }
254
239
  });
255
240
 
256
241
  this.pty.onExit(({ exitCode }) => {
257
242
  this.closed = true;
258
- this.session.send(createTerminalExit(this.id, exitCode));
243
+ this.session.send(getProtocol().createTerminalExit(this.id, exitCode));
259
244
  });
260
245
 
261
246
  return true;
@@ -275,7 +260,7 @@ class TerminalInstance {
275
260
  const filteredEnv = this._getFilteredEnv();
276
261
 
277
262
  // Use bash with interactive flag for better compatibility
278
- this.pty = spawn(this.shell, ['-i'], {
263
+ this.pty = getChildProcess().spawn(this.shell, ['-i'], {
279
264
  cwd: this.cwd,
280
265
  env: {
281
266
  ...filteredEnv,
@@ -291,25 +276,25 @@ class TerminalInstance {
291
276
 
292
277
  this.pty.stdout.on('data', data => {
293
278
  if (!this.closed) {
294
- this.session.send(createTerminalOutput(this.id, data.toString()));
279
+ this.session.send(getProtocol().createTerminalOutput(this.id, data.toString()));
295
280
  }
296
281
  });
297
282
 
298
283
  this.pty.stderr.on('data', data => {
299
284
  if (!this.closed) {
300
- this.session.send(createTerminalOutput(this.id, data.toString()));
285
+ this.session.send(getProtocol().createTerminalOutput(this.id, data.toString()));
301
286
  }
302
287
  });
303
288
 
304
289
  this.pty.on('close', exitCode => {
305
290
  this.closed = true;
306
- this.session.send(createTerminalExit(this.id, exitCode));
291
+ this.session.send(getProtocol().createTerminalExit(this.id, exitCode));
307
292
  });
308
293
 
309
294
  this.pty.on('error', error => {
310
295
  console.error('[Terminal] Shell error:', error.message);
311
296
  if (!this.closed) {
312
- this.session.send(createTerminalOutput(this.id, `\r\nError: ${error.message}\r\n`));
297
+ this.session.send(getProtocol().createTerminalOutput(this.id, `\r\nError: ${error.message}\r\n`));
313
298
  }
314
299
  });
315
300
 
@@ -318,7 +303,7 @@ class TerminalInstance {
318
303
  if (!this.closed) {
319
304
  const welcomeMsg = `\x1b[32mAgileFlow Terminal\x1b[0m (basic mode - node-pty not available)\r\n`;
320
305
  const cwdMsg = `Working directory: ${this.cwd}\r\n\r\n`;
321
- this.session.send(createTerminalOutput(this.id, welcomeMsg + cwdMsg));
306
+ this.session.send(getProtocol().createTerminalOutput(this.id, welcomeMsg + cwdMsg));
322
307
  }
323
308
  }, 100);
324
309
 
@@ -355,7 +340,7 @@ class TerminalInstance {
355
340
  }
356
341
 
357
342
  // Echo to terminal
358
- this.session.send(createTerminalOutput(this.id, echoData));
343
+ this.session.send(getProtocol().createTerminalOutput(this.id, echoData));
359
344
 
360
345
  // Send to shell stdin
361
346
  this.pty.stdin.write(data);
@@ -406,7 +391,7 @@ class TerminalManager {
406
391
  * @returns {string} - Terminal ID
407
392
  */
408
393
  createTerminal(session, options = {}) {
409
- const terminalId = options.id || crypto.randomBytes(8).toString('hex');
394
+ const terminalId = options.id || getCrypto().randomBytes(8).toString('hex');
410
395
  const terminal = new TerminalInstance(terminalId, session, {
411
396
  cwd: options.cwd || session.projectRoot,
412
397
  cols: options.cols,
@@ -507,13 +492,13 @@ class DashboardServer extends EventEmitter {
507
492
 
508
493
  this.port = options.port || DEFAULT_PORT;
509
494
  this.host = options.host || DEFAULT_HOST;
510
- this.projectRoot = options.projectRoot || getProjectRoot();
495
+ this.projectRoot = options.projectRoot || getPaths().getProjectRoot();
511
496
 
512
497
  // Auth is on by default - auto-generate key if not provided
513
498
  // Set requireAuth: false explicitly to disable
514
499
  this.requireAuth = options.requireAuth !== false;
515
500
  this.apiKey =
516
- options.apiKey || (this.requireAuth ? crypto.randomBytes(32).toString('hex') : null);
501
+ options.apiKey || (this.requireAuth ? getCrypto().randomBytes(32).toString('hex') : null);
517
502
 
518
503
  // Session management
519
504
  this.sessions = new Map();
@@ -536,12 +521,15 @@ class DashboardServer extends EventEmitter {
536
521
  this.httpServer = null;
537
522
 
538
523
  // Validate project
539
- if (!isAgileflowProject(this.projectRoot)) {
524
+ if (!getPaths().isAgileflowProject(this.projectRoot)) {
540
525
  throw new Error(`Not an AgileFlow project: ${this.projectRoot}`);
541
526
  }
542
527
 
543
528
  // Initialize automation registry lazily
544
529
  this._initAutomations();
530
+
531
+ // Listen for team metrics saves to broadcast to clients
532
+ this._initTeamMetricsListener();
545
533
  }
546
534
 
547
535
  /**
@@ -555,12 +543,12 @@ class DashboardServer extends EventEmitter {
555
543
  // Listen to runner events
556
544
  this._automationRunner.on('started', ({ automationId }) => {
557
545
  this._runningAutomations.set(automationId, { startTime: Date.now() });
558
- this.broadcast(createAutomationStatus(automationId, 'running'));
546
+ this.broadcast(getProtocol().createAutomationStatus(automationId, 'running'));
559
547
  });
560
548
 
561
549
  this._automationRunner.on('completed', ({ automationId, result }) => {
562
550
  this._runningAutomations.delete(automationId);
563
- this.broadcast(createAutomationStatus(automationId, 'completed', result));
551
+ this.broadcast(getProtocol().createAutomationStatus(automationId, 'completed', result));
564
552
 
565
553
  // Add result to inbox if it has output or changes
566
554
  if (result.output || result.changes) {
@@ -570,7 +558,7 @@ class DashboardServer extends EventEmitter {
570
558
 
571
559
  this._automationRunner.on('failed', ({ automationId, result }) => {
572
560
  this._runningAutomations.delete(automationId);
573
- this.broadcast(createAutomationStatus(automationId, 'error', { error: result.error }));
561
+ this.broadcast(getProtocol().createAutomationStatus(automationId, 'error', { error: result.error }));
574
562
 
575
563
  // Add failure to inbox
576
564
  this._addToInbox(automationId, result);
@@ -607,7 +595,7 @@ class DashboardServer extends EventEmitter {
607
595
  };
608
596
 
609
597
  this._inbox.set(itemId, item);
610
- this.broadcast(createInboxItem(item));
598
+ this.broadcast(getProtocol().createInboxItem(item));
611
599
  }
612
600
 
613
601
  /**
@@ -623,7 +611,7 @@ class DashboardServer extends EventEmitter {
623
611
  'Cache-Control': 'no-store',
624
612
  };
625
613
 
626
- this.httpServer = http.createServer((req, res) => {
614
+ this.httpServer = getHttp().createServer((req, res) => {
627
615
  // Simple health check endpoint
628
616
  if (req.url === '/health') {
629
617
  res.writeHead(200, securityHeaders);
@@ -713,7 +701,7 @@ class DashboardServer extends EventEmitter {
713
701
  const providedBuffer = Buffer.from(providedKey, 'utf8');
714
702
  if (
715
703
  keyBuffer.length !== providedBuffer.length ||
716
- !crypto.timingSafeEqual(keyBuffer, providedBuffer)
704
+ !getCrypto().timingSafeEqual(keyBuffer, providedBuffer)
717
705
  ) {
718
706
  socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
719
707
  socket.destroy();
@@ -778,7 +766,7 @@ class DashboardServer extends EventEmitter {
778
766
  }
779
767
 
780
768
  // Generate new session ID
781
- return crypto.randomBytes(16).toString('hex');
769
+ return getCrypto().randomBytes(16).toString('hex');
782
770
  }
783
771
 
784
772
  /**
@@ -801,7 +789,7 @@ class DashboardServer extends EventEmitter {
801
789
 
802
790
  // Send initial state
803
791
  session.send(
804
- createSessionState(sessionId, 'connected', {
792
+ getProtocol().createSessionState(sessionId, 'connected', {
805
793
  resumed: isResume,
806
794
  messageCount: session.messages.length,
807
795
  project: require('path').basename(this.projectRoot),
@@ -814,6 +802,9 @@ class DashboardServer extends EventEmitter {
814
802
  // Send project status (stories/epics)
815
803
  this.sendStatusUpdate(session);
816
804
 
805
+ // Send team metrics
806
+ this.sendTeamMetrics(session);
807
+
817
808
  // Send session list with sync info
818
809
  this.sendSessionList(session);
819
810
 
@@ -874,83 +865,83 @@ class DashboardServer extends EventEmitter {
874
865
  handleMessage(session, data) {
875
866
  // Rate limit incoming messages
876
867
  if (!session.checkRateLimit()) {
877
- session.send(createError('RATE_LIMITED', 'Too many messages, please slow down'));
868
+ session.send(getProtocol().createError('RATE_LIMITED', 'Too many messages, please slow down'));
878
869
  return;
879
870
  }
880
871
 
881
- const message = parseInboundMessage(data);
872
+ const message = getProtocol().parseInboundMessage(data);
882
873
  if (!message) {
883
- session.send(createError('INVALID_MESSAGE', 'Failed to parse message'));
874
+ session.send(getProtocol().createError('INVALID_MESSAGE', 'Failed to parse message'));
884
875
  return;
885
876
  }
886
877
 
887
878
  console.log(`[Session ${session.id}] Received: ${message.type}`);
888
879
 
889
880
  switch (message.type) {
890
- case InboundMessageType.MESSAGE:
881
+ case getProtocol().InboundMessageType.MESSAGE:
891
882
  this.handleUserMessage(session, message);
892
883
  break;
893
884
 
894
- case InboundMessageType.CANCEL:
885
+ case getProtocol().InboundMessageType.CANCEL:
895
886
  this.handleCancel(session);
896
887
  break;
897
888
 
898
- case InboundMessageType.REFRESH:
889
+ case getProtocol().InboundMessageType.REFRESH:
899
890
  this.handleRefresh(session, message);
900
891
  break;
901
892
 
902
- case InboundMessageType.GIT_STAGE:
903
- case InboundMessageType.GIT_UNSTAGE:
904
- case InboundMessageType.GIT_REVERT:
905
- case InboundMessageType.GIT_COMMIT:
893
+ case getProtocol().InboundMessageType.GIT_STAGE:
894
+ case getProtocol().InboundMessageType.GIT_UNSTAGE:
895
+ case getProtocol().InboundMessageType.GIT_REVERT:
896
+ case getProtocol().InboundMessageType.GIT_COMMIT:
906
897
  this.handleGitAction(session, message);
907
898
  break;
908
899
 
909
- case InboundMessageType.GIT_DIFF_REQUEST:
900
+ case getProtocol().InboundMessageType.GIT_DIFF_REQUEST:
910
901
  this.handleDiffRequest(session, message);
911
902
  break;
912
903
 
913
- case InboundMessageType.SESSION_CLOSE:
904
+ case getProtocol().InboundMessageType.SESSION_CLOSE:
914
905
  this.closeSession(session.id);
915
906
  break;
916
907
 
917
- case InboundMessageType.TERMINAL_SPAWN:
908
+ case getProtocol().InboundMessageType.TERMINAL_SPAWN:
918
909
  this.handleTerminalSpawn(session, message);
919
910
  break;
920
911
 
921
- case InboundMessageType.TERMINAL_INPUT:
912
+ case getProtocol().InboundMessageType.TERMINAL_INPUT:
922
913
  this.handleTerminalInput(session, message);
923
914
  break;
924
915
 
925
- case InboundMessageType.TERMINAL_RESIZE:
916
+ case getProtocol().InboundMessageType.TERMINAL_RESIZE:
926
917
  this.handleTerminalResize(session, message);
927
918
  break;
928
919
 
929
- case InboundMessageType.TERMINAL_CLOSE:
920
+ case getProtocol().InboundMessageType.TERMINAL_CLOSE:
930
921
  this.handleTerminalClose(session, message);
931
922
  break;
932
923
 
933
- case InboundMessageType.AUTOMATION_LIST_REQUEST:
924
+ case getProtocol().InboundMessageType.AUTOMATION_LIST_REQUEST:
934
925
  this.sendAutomationList(session);
935
926
  break;
936
927
 
937
- case InboundMessageType.AUTOMATION_RUN:
928
+ case getProtocol().InboundMessageType.AUTOMATION_RUN:
938
929
  this.handleAutomationRun(session, message);
939
930
  break;
940
931
 
941
- case InboundMessageType.AUTOMATION_STOP:
932
+ case getProtocol().InboundMessageType.AUTOMATION_STOP:
942
933
  this.handleAutomationStop(session, message);
943
934
  break;
944
935
 
945
- case InboundMessageType.INBOX_LIST_REQUEST:
936
+ case getProtocol().InboundMessageType.INBOX_LIST_REQUEST:
946
937
  this.sendInboxList(session);
947
938
  break;
948
939
 
949
- case InboundMessageType.INBOX_ACTION:
940
+ case getProtocol().InboundMessageType.INBOX_ACTION:
950
941
  this.handleInboxAction(session, message);
951
942
  break;
952
943
 
953
- case InboundMessageType.OPEN_FILE:
944
+ case getProtocol().InboundMessageType.OPEN_FILE:
954
945
  this.handleOpenFile(session, message);
955
946
  break;
956
947
 
@@ -966,7 +957,7 @@ class DashboardServer extends EventEmitter {
966
957
  handleUserMessage(session, message) {
967
958
  const content = message.content?.trim();
968
959
  if (!content) {
969
- session.send(createError('EMPTY_MESSAGE', 'Message content is empty'));
960
+ session.send(getProtocol().createError('EMPTY_MESSAGE', 'Message content is empty'));
970
961
  return;
971
962
  }
972
963
 
@@ -985,7 +976,7 @@ class DashboardServer extends EventEmitter {
985
976
  */
986
977
  handleCancel(session) {
987
978
  session.setState('idle');
988
- session.send(createNotification('info', 'Cancelled', 'Operation cancelled'));
979
+ session.send(getProtocol().createNotification('info', 'Cancelled', 'Operation cancelled'));
989
980
  this.emit('user:cancel', session);
990
981
  }
991
982
 
@@ -1015,9 +1006,13 @@ class DashboardServer extends EventEmitter {
1015
1006
  case 'inbox':
1016
1007
  this.sendInboxList(session);
1017
1008
  break;
1009
+ case 'team_metrics':
1010
+ this.sendTeamMetrics(session);
1011
+ break;
1018
1012
  default:
1019
1013
  this.sendGitStatus(session);
1020
1014
  this.sendStatusUpdate(session);
1015
+ this.sendTeamMetrics(session);
1021
1016
  this.sendSessionList(session);
1022
1017
  this.sendAutomationList(session);
1023
1018
  this.sendInboxList(session);
@@ -1035,12 +1030,12 @@ class DashboardServer extends EventEmitter {
1035
1030
  if (files && files.length > 0) {
1036
1031
  for (const f of files) {
1037
1032
  if (typeof f !== 'string' || f.includes('\0')) {
1038
- session.send(createError('GIT_ERROR', 'Invalid file path'));
1033
+ session.send(getProtocol().createError('GIT_ERROR', 'Invalid file path'));
1039
1034
  return;
1040
1035
  }
1041
1036
  const resolved = require('path').resolve(this.projectRoot, f);
1042
1037
  if (!resolved.startsWith(this.projectRoot)) {
1043
- session.send(createError('GIT_ERROR', 'File path outside project'));
1038
+ session.send(getProtocol().createError('GIT_ERROR', 'File path outside project'));
1044
1039
  return;
1045
1040
  }
1046
1041
  }
@@ -1053,7 +1048,7 @@ class DashboardServer extends EventEmitter {
1053
1048
  commitMessage.length > 10000 ||
1054
1049
  commitMessage.includes('\0')
1055
1050
  ) {
1056
- session.send(createError('GIT_ERROR', 'Invalid commit message'));
1051
+ session.send(getProtocol().createError('GIT_ERROR', 'Invalid commit message'));
1057
1052
  return;
1058
1053
  }
1059
1054
  }
@@ -1062,40 +1057,40 @@ class DashboardServer extends EventEmitter {
1062
1057
 
1063
1058
  try {
1064
1059
  switch (type) {
1065
- case InboundMessageType.GIT_STAGE:
1060
+ case getProtocol().InboundMessageType.GIT_STAGE:
1066
1061
  if (fileArgs) {
1067
- execFileSync('git', ['add', '--', ...fileArgs], { cwd: this.projectRoot });
1062
+ getChildProcess().execFileSync('git', ['add', '--', ...fileArgs], { cwd: this.projectRoot });
1068
1063
  } else {
1069
- execFileSync('git', ['add', '-A'], { cwd: this.projectRoot });
1064
+ getChildProcess().execFileSync('git', ['add', '-A'], { cwd: this.projectRoot });
1070
1065
  }
1071
1066
  break;
1072
- case InboundMessageType.GIT_UNSTAGE:
1067
+ case getProtocol().InboundMessageType.GIT_UNSTAGE:
1073
1068
  if (fileArgs) {
1074
- execFileSync('git', ['restore', '--staged', '--', ...fileArgs], {
1069
+ getChildProcess().execFileSync('git', ['restore', '--staged', '--', ...fileArgs], {
1075
1070
  cwd: this.projectRoot,
1076
1071
  });
1077
1072
  } else {
1078
- execFileSync('git', ['restore', '--staged', '.'], { cwd: this.projectRoot });
1073
+ getChildProcess().execFileSync('git', ['restore', '--staged', '.'], { cwd: this.projectRoot });
1079
1074
  }
1080
1075
  break;
1081
- case InboundMessageType.GIT_REVERT:
1076
+ case getProtocol().InboundMessageType.GIT_REVERT:
1082
1077
  if (fileArgs) {
1083
- execFileSync('git', ['checkout', '--', ...fileArgs], { cwd: this.projectRoot });
1078
+ getChildProcess().execFileSync('git', ['checkout', '--', ...fileArgs], { cwd: this.projectRoot });
1084
1079
  }
1085
1080
  break;
1086
- case InboundMessageType.GIT_COMMIT:
1081
+ case getProtocol().InboundMessageType.GIT_COMMIT:
1087
1082
  if (commitMessage) {
1088
- execFileSync('git', ['commit', '-m', commitMessage], { cwd: this.projectRoot });
1083
+ getChildProcess().execFileSync('git', ['commit', '-m', commitMessage], { cwd: this.projectRoot });
1089
1084
  }
1090
1085
  break;
1091
1086
  }
1092
1087
 
1093
1088
  // Send updated git status
1094
1089
  this.sendGitStatus(session);
1095
- session.send(createNotification('success', 'Git', `${type.replace('git_', '')} completed`));
1090
+ session.send(getProtocol().createNotification('success', 'Git', `${type.replace('git_', '')} completed`));
1096
1091
  } catch (error) {
1097
1092
  console.error('[Git Error]', error.message);
1098
- session.send(createError('GIT_ERROR', 'Git operation failed'));
1093
+ session.send(getProtocol().createError('GIT_ERROR', 'Git operation failed'));
1099
1094
  }
1100
1095
  }
1101
1096
 
@@ -1106,7 +1101,7 @@ class DashboardServer extends EventEmitter {
1106
1101
  try {
1107
1102
  const status = this.getGitStatus();
1108
1103
  session.send({
1109
- type: OutboundMessageType.GIT_STATUS,
1104
+ type: getProtocol().OutboundMessageType.GIT_STATUS,
1110
1105
  ...status,
1111
1106
  timestamp: new Date().toISOString(),
1112
1107
  });
@@ -1120,13 +1115,13 @@ class DashboardServer extends EventEmitter {
1120
1115
  */
1121
1116
  getGitStatus() {
1122
1117
  try {
1123
- const branch = execFileSync('git', ['branch', '--show-current'], {
1118
+ const branch = getChildProcess().execFileSync('git', ['branch', '--show-current'], {
1124
1119
  cwd: this.projectRoot,
1125
1120
  encoding: 'utf8',
1126
1121
  stdio: ['pipe', 'pipe', 'pipe'],
1127
1122
  }).trim();
1128
1123
 
1129
- const statusOutput = execFileSync('git', ['status', '--porcelain'], {
1124
+ const statusOutput = getChildProcess().execFileSync('git', ['status', '--porcelain'], {
1130
1125
  cwd: this.projectRoot,
1131
1126
  encoding: 'utf8',
1132
1127
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -1185,7 +1180,7 @@ class DashboardServer extends EventEmitter {
1185
1180
  const { path: filePath, staged } = message;
1186
1181
 
1187
1182
  if (!filePath) {
1188
- session.send(createError('INVALID_REQUEST', 'File path is required'));
1183
+ session.send(getProtocol().createError('INVALID_REQUEST', 'File path is required'));
1189
1184
  return;
1190
1185
  }
1191
1186
 
@@ -1194,7 +1189,7 @@ class DashboardServer extends EventEmitter {
1194
1189
  const stats = this.parseDiffStats(diff);
1195
1190
 
1196
1191
  session.send(
1197
- createGitDiff(filePath, diff, {
1192
+ getProtocol().createGitDiff(filePath, diff, {
1198
1193
  additions: stats.additions,
1199
1194
  deletions: stats.deletions,
1200
1195
  staged: !!staged,
@@ -1202,7 +1197,7 @@ class DashboardServer extends EventEmitter {
1202
1197
  );
1203
1198
  } catch (error) {
1204
1199
  console.error('[Diff Error]', error.message);
1205
- session.send(createError('DIFF_ERROR', 'Failed to get diff'));
1200
+ session.send(getProtocol().createError('DIFF_ERROR', 'Failed to get diff'));
1206
1201
  }
1207
1202
  }
1208
1203
 
@@ -1214,7 +1209,7 @@ class DashboardServer extends EventEmitter {
1214
1209
  */
1215
1210
  getFileDiff(filePath, staged = false) {
1216
1211
  // Validate filePath stays within project root
1217
- const pathResult = validatePath(filePath, this.projectRoot, { allowSymlinks: true });
1212
+ const pathResult = getValidatePaths().validatePath(filePath, this.projectRoot, { allowSymlinks: true });
1218
1213
  if (!pathResult.ok) {
1219
1214
  return '';
1220
1215
  }
@@ -1222,14 +1217,14 @@ class DashboardServer extends EventEmitter {
1222
1217
  try {
1223
1218
  const diffArgs = staged ? ['diff', '--cached', '--', filePath] : ['diff', '--', filePath];
1224
1219
 
1225
- const diff = execFileSync('git', diffArgs, {
1220
+ const diff = getChildProcess().execFileSync('git', diffArgs, {
1226
1221
  cwd: this.projectRoot,
1227
1222
  encoding: 'utf8',
1228
1223
  });
1229
1224
 
1230
1225
  // If no diff, file might be untracked - show entire file content as addition
1231
1226
  if (!diff && !staged) {
1232
- const statusOutput = execFileSync('git', ['status', '--porcelain', '--', filePath], {
1227
+ const statusOutput = getChildProcess().execFileSync('git', ['status', '--porcelain', '--', filePath], {
1233
1228
  cwd: this.projectRoot,
1234
1229
  encoding: 'utf8',
1235
1230
  }).trim();
@@ -1318,12 +1313,68 @@ class DashboardServer extends EventEmitter {
1318
1313
  })),
1319
1314
  };
1320
1315
 
1321
- session.send(createStatusUpdate(summary));
1316
+ session.send(getProtocol().createStatusUpdate(summary));
1322
1317
  } catch (error) {
1323
1318
  console.error('[Status Update Error]', error.message);
1324
1319
  }
1325
1320
  }
1326
1321
 
1322
+ /**
1323
+ * Initialize listener for team metrics events
1324
+ */
1325
+ _initTeamMetricsListener() {
1326
+ try {
1327
+ const { teamMetricsEmitter } = require('../scripts/lib/team-events');
1328
+ teamMetricsEmitter.on('metrics_saved', () => {
1329
+ this.broadcastTeamMetrics();
1330
+ });
1331
+ } catch (e) {
1332
+ // team-events not available - non-critical
1333
+ }
1334
+ }
1335
+
1336
+ /**
1337
+ * Send team metrics to a single session
1338
+ */
1339
+ sendTeamMetrics(session) {
1340
+ const path = require('path');
1341
+ const fs = require('fs');
1342
+ const sessionStatePath = path.join(this.projectRoot, 'docs', '09-agents', 'session-state.json');
1343
+ if (!fs.existsSync(sessionStatePath)) return;
1344
+
1345
+ try {
1346
+ const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
1347
+ const traces = (state.team_metrics && state.team_metrics.traces) || {};
1348
+
1349
+ for (const [traceId, metrics] of Object.entries(traces)) {
1350
+ session.send(getProtocol().createTeamMetrics(traceId, metrics));
1351
+ }
1352
+ } catch (error) {
1353
+ // Non-critical
1354
+ }
1355
+ }
1356
+
1357
+ /**
1358
+ * Broadcast team metrics to all connected clients
1359
+ */
1360
+ broadcastTeamMetrics() {
1361
+ const path = require('path');
1362
+ const fs = require('fs');
1363
+ const sessionStatePath = path.join(this.projectRoot, 'docs', '09-agents', 'session-state.json');
1364
+ if (!fs.existsSync(sessionStatePath)) return;
1365
+
1366
+ try {
1367
+ const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
1368
+ const traces = (state.team_metrics && state.team_metrics.traces) || {};
1369
+
1370
+ for (const [traceId, metrics] of Object.entries(traces)) {
1371
+ this.broadcast(getProtocol().createTeamMetrics(traceId, metrics));
1372
+ }
1373
+ } catch (error) {
1374
+ // Non-critical
1375
+ }
1376
+ }
1377
+
1327
1378
  /**
1328
1379
  * Send session list with sync status to dashboard
1329
1380
  */
@@ -1347,7 +1398,7 @@ class DashboardServer extends EventEmitter {
1347
1398
  // Get branch and sync status via git
1348
1399
  try {
1349
1400
  const cwd = s.metadata.worktreePath || this.projectRoot;
1350
- entry.branch = execFileSync('git', ['branch', '--show-current'], {
1401
+ entry.branch = getChildProcess().execFileSync('git', ['branch', '--show-current'], {
1351
1402
  cwd,
1352
1403
  encoding: 'utf8',
1353
1404
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -1355,7 +1406,7 @@ class DashboardServer extends EventEmitter {
1355
1406
 
1356
1407
  // Get ahead/behind counts relative to upstream
1357
1408
  try {
1358
- const counts = execFileSync(
1409
+ const counts = getChildProcess().execFileSync(
1359
1410
  'git',
1360
1411
  ['rev-list', '--left-right', '--count', 'HEAD...@{u}'],
1361
1412
  {
@@ -1388,7 +1439,7 @@ class DashboardServer extends EventEmitter {
1388
1439
  sessions.push(entry);
1389
1440
  }
1390
1441
 
1391
- session.send(createSessionList(sessions));
1442
+ session.send(getProtocol().createSessionList(sessions));
1392
1443
  }
1393
1444
 
1394
1445
  /**
@@ -1398,14 +1449,14 @@ class DashboardServer extends EventEmitter {
1398
1449
  const { path: filePath, line } = message;
1399
1450
 
1400
1451
  if (!filePath || typeof filePath !== 'string') {
1401
- session.send(createError('INVALID_REQUEST', 'File path is required'));
1452
+ session.send(getProtocol().createError('INVALID_REQUEST', 'File path is required'));
1402
1453
  return;
1403
1454
  }
1404
1455
 
1405
1456
  // Validate the path stays within project root
1406
- const pathResult = validatePath(filePath, this.projectRoot, { allowSymlinks: true });
1457
+ const pathResult = getValidatePaths().validatePath(filePath, this.projectRoot, { allowSymlinks: true });
1407
1458
  if (!pathResult.ok) {
1408
- session.send(createError('OPEN_FILE_ERROR', 'File path outside project'));
1459
+ session.send(getProtocol().createError('OPEN_FILE_ERROR', 'File path outside project'));
1409
1460
  return;
1410
1461
  }
1411
1462
 
@@ -1423,28 +1474,28 @@ class DashboardServer extends EventEmitter {
1423
1474
  case 'cursor':
1424
1475
  case 'windsurf': {
1425
1476
  const gotoArg = lineNum ? `${fullPath}:${lineNum}` : fullPath;
1426
- spawn(editor, ['--goto', gotoArg], { detached: true, stdio: 'ignore' }).unref();
1477
+ getChildProcess().spawn(editor, ['--goto', gotoArg], { detached: true, stdio: 'ignore' }).unref();
1427
1478
  break;
1428
1479
  }
1429
1480
  case 'subl':
1430
1481
  case 'sublime_text': {
1431
1482
  const sublArg = lineNum ? `${fullPath}:${lineNum}` : fullPath;
1432
- spawn(editor, [sublArg], { detached: true, stdio: 'ignore' }).unref();
1483
+ getChildProcess().spawn(editor, [sublArg], { detached: true, stdio: 'ignore' }).unref();
1433
1484
  break;
1434
1485
  }
1435
1486
  default: {
1436
1487
  // Generic: just open the file
1437
- spawn(editor, [fullPath], { detached: true, stdio: 'ignore' }).unref();
1488
+ getChildProcess().spawn(editor, [fullPath], { detached: true, stdio: 'ignore' }).unref();
1438
1489
  break;
1439
1490
  }
1440
1491
  }
1441
1492
 
1442
1493
  session.send(
1443
- createNotification('info', 'Editor', `Opened ${require('path').basename(fullPath)}`)
1494
+ getProtocol().createNotification('info', 'Editor', `Opened ${require('path').basename(fullPath)}`)
1444
1495
  );
1445
1496
  } catch (error) {
1446
1497
  console.error('[Open File Error]', error.message);
1447
- session.send(createError('OPEN_FILE_ERROR', `Failed to open file: ${error.message}`));
1498
+ session.send(getProtocol().createError('OPEN_FILE_ERROR', `Failed to open file: ${error.message}`));
1448
1499
  }
1449
1500
  }
1450
1501
 
@@ -1457,10 +1508,10 @@ class DashboardServer extends EventEmitter {
1457
1508
  // Validate cwd stays within project root
1458
1509
  let safeCwd = this.projectRoot;
1459
1510
  if (cwd) {
1460
- const cwdResult = validatePath(cwd, this.projectRoot, { allowSymlinks: true });
1511
+ const cwdResult = getValidatePaths().validatePath(cwd, this.projectRoot, { allowSymlinks: true });
1461
1512
  if (!cwdResult.ok) {
1462
1513
  session.send(
1463
- createError('TERMINAL_ERROR', 'Working directory must be within project root')
1514
+ getProtocol().createError('TERMINAL_ERROR', 'Working directory must be within project root')
1464
1515
  );
1465
1516
  return;
1466
1517
  }
@@ -1480,7 +1531,7 @@ class DashboardServer extends EventEmitter {
1480
1531
  timestamp: new Date().toISOString(),
1481
1532
  });
1482
1533
  } else {
1483
- session.send(createError('TERMINAL_ERROR', 'Failed to spawn terminal'));
1534
+ session.send(getProtocol().createError('TERMINAL_ERROR', 'Failed to spawn terminal'));
1484
1535
  }
1485
1536
  }
1486
1537
 
@@ -1521,7 +1572,7 @@ class DashboardServer extends EventEmitter {
1521
1572
  }
1522
1573
 
1523
1574
  this.terminalManager.closeTerminal(terminalId);
1524
- session.send(createNotification('info', 'Terminal', 'Terminal closed'));
1575
+ session.send(getProtocol().createNotification('info', 'Terminal', 'Terminal closed'));
1525
1576
  }
1526
1577
 
1527
1578
  // ==========================================================================
@@ -1533,7 +1584,7 @@ class DashboardServer extends EventEmitter {
1533
1584
  */
1534
1585
  sendAutomationList(session) {
1535
1586
  if (!this._automationRegistry) {
1536
- session.send(createAutomationList([]));
1587
+ session.send(getProtocol().createAutomationList([]));
1537
1588
  return;
1538
1589
  }
1539
1590
 
@@ -1555,10 +1606,10 @@ class DashboardServer extends EventEmitter {
1555
1606
  };
1556
1607
  });
1557
1608
 
1558
- session.send(createAutomationList(enriched));
1609
+ session.send(getProtocol().createAutomationList(enriched));
1559
1610
  } catch (error) {
1560
1611
  console.error('[Automations] List error:', error.message);
1561
- session.send(createAutomationList([]));
1612
+ session.send(getProtocol().createAutomationList([]));
1562
1613
  }
1563
1614
  }
1564
1615
 
@@ -1628,12 +1679,12 @@ class DashboardServer extends EventEmitter {
1628
1679
  const { id: automationId } = message;
1629
1680
 
1630
1681
  if (!automationId) {
1631
- session.send(createError('INVALID_REQUEST', 'Automation ID is required'));
1682
+ session.send(getProtocol().createError('INVALID_REQUEST', 'Automation ID is required'));
1632
1683
  return;
1633
1684
  }
1634
1685
 
1635
1686
  if (!this._automationRunner) {
1636
- session.send(createError('AUTOMATION_ERROR', 'Automation runner not initialized'));
1687
+ session.send(getProtocol().createError('AUTOMATION_ERROR', 'Automation runner not initialized'));
1637
1688
  return;
1638
1689
  }
1639
1690
 
@@ -1641,12 +1692,12 @@ class DashboardServer extends EventEmitter {
1641
1692
  // Check if already running
1642
1693
  if (this._runningAutomations.has(automationId)) {
1643
1694
  session.send(
1644
- createNotification('warning', 'Automation', `${automationId} is already running`)
1695
+ getProtocol().createNotification('warning', 'Automation', `${automationId} is already running`)
1645
1696
  );
1646
1697
  return;
1647
1698
  }
1648
1699
 
1649
- session.send(createNotification('info', 'Automation', `Starting ${automationId}...`));
1700
+ session.send(getProtocol().createNotification('info', 'Automation', `Starting ${automationId}...`));
1650
1701
 
1651
1702
  // Run the automation (async)
1652
1703
  const result = await this._automationRunner.run(automationId);
@@ -1654,23 +1705,23 @@ class DashboardServer extends EventEmitter {
1654
1705
  // Send result notification
1655
1706
  if (result.success) {
1656
1707
  session.send(
1657
- createNotification('success', 'Automation', `${automationId} completed successfully`)
1708
+ getProtocol().createNotification('success', 'Automation', `${automationId} completed successfully`)
1658
1709
  );
1659
1710
  } else {
1660
1711
  session.send(
1661
- createNotification('error', 'Automation', `${automationId} failed: ${result.error}`)
1712
+ getProtocol().createNotification('error', 'Automation', `${automationId} failed: ${result.error}`)
1662
1713
  );
1663
1714
  }
1664
1715
 
1665
1716
  // Send final status
1666
- session.send(createAutomationStatus(automationId, result.success ? 'idle' : 'error', result));
1717
+ session.send(getProtocol().createAutomationStatus(automationId, result.success ? 'idle' : 'error', result));
1667
1718
 
1668
1719
  // Refresh the list
1669
1720
  this.sendAutomationList(session);
1670
1721
  } catch (error) {
1671
1722
  console.error('[Automation Error]', error.message);
1672
- session.send(createError('AUTOMATION_ERROR', 'Automation execution failed'));
1673
- session.send(createAutomationStatus(automationId, 'error', { error: 'Execution failed' }));
1723
+ session.send(getProtocol().createError('AUTOMATION_ERROR', 'Automation execution failed'));
1724
+ session.send(getProtocol().createAutomationStatus(automationId, 'error', { error: 'Execution failed' }));
1674
1725
  }
1675
1726
  }
1676
1727
 
@@ -1681,7 +1732,7 @@ class DashboardServer extends EventEmitter {
1681
1732
  const { id: automationId } = message;
1682
1733
 
1683
1734
  if (!automationId) {
1684
- session.send(createError('INVALID_REQUEST', 'Automation ID is required'));
1735
+ session.send(getProtocol().createError('INVALID_REQUEST', 'Automation ID is required'));
1685
1736
  return;
1686
1737
  }
1687
1738
 
@@ -1691,8 +1742,8 @@ class DashboardServer extends EventEmitter {
1691
1742
  }
1692
1743
 
1693
1744
  this._runningAutomations.delete(automationId);
1694
- session.send(createAutomationStatus(automationId, 'idle'));
1695
- session.send(createNotification('info', 'Automation', `${automationId} stopped`));
1745
+ session.send(getProtocol().createAutomationStatus(automationId, 'idle'));
1746
+ session.send(getProtocol().createNotification('info', 'Automation', `${automationId} stopped`));
1696
1747
  }
1697
1748
 
1698
1749
  // ==========================================================================
@@ -1707,7 +1758,7 @@ class DashboardServer extends EventEmitter {
1707
1758
  (a, b) => new Date(b.timestamp) - new Date(a.timestamp)
1708
1759
  );
1709
1760
 
1710
- session.send(createInboxList(items));
1761
+ session.send(getProtocol().createInboxList(items));
1711
1762
  }
1712
1763
 
1713
1764
  /**
@@ -1717,13 +1768,13 @@ class DashboardServer extends EventEmitter {
1717
1768
  const { id: itemId, action } = message;
1718
1769
 
1719
1770
  if (!itemId) {
1720
- session.send(createError('INVALID_REQUEST', 'Item ID is required'));
1771
+ session.send(getProtocol().createError('INVALID_REQUEST', 'Item ID is required'));
1721
1772
  return;
1722
1773
  }
1723
1774
 
1724
1775
  const item = this._inbox.get(itemId);
1725
1776
  if (!item) {
1726
- session.send(createError('NOT_FOUND', `Inbox item ${itemId} not found`));
1777
+ session.send(getProtocol().createError('NOT_FOUND', `Inbox item ${itemId} not found`));
1727
1778
  return;
1728
1779
  }
1729
1780
 
@@ -1731,14 +1782,14 @@ class DashboardServer extends EventEmitter {
1731
1782
  case 'accept':
1732
1783
  // Mark as accepted and remove
1733
1784
  item.status = 'accepted';
1734
- session.send(createNotification('success', 'Inbox', `Accepted: ${item.title}`));
1785
+ session.send(getProtocol().createNotification('success', 'Inbox', `Accepted: ${item.title}`));
1735
1786
  this._inbox.delete(itemId);
1736
1787
  break;
1737
1788
 
1738
1789
  case 'dismiss':
1739
1790
  // Mark as dismissed and remove
1740
1791
  item.status = 'dismissed';
1741
- session.send(createNotification('info', 'Inbox', `Dismissed: ${item.title}`));
1792
+ session.send(getProtocol().createNotification('info', 'Inbox', `Dismissed: ${item.title}`));
1742
1793
  this._inbox.delete(itemId);
1743
1794
  break;
1744
1795
 
@@ -1748,7 +1799,7 @@ class DashboardServer extends EventEmitter {
1748
1799
  break;
1749
1800
 
1750
1801
  default:
1751
- session.send(createError('INVALID_ACTION', `Unknown action: ${action}`));
1802
+ session.send(getProtocol().createError('INVALID_ACTION', `Unknown action: ${action}`));
1752
1803
  return;
1753
1804
  }
1754
1805