agileflow 2.99.8 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -3
  3. package/lib/cache-provider.js +155 -0
  4. package/lib/codebase-indexer.js +1 -1
  5. package/lib/content-sanitizer.js +1 -0
  6. package/lib/dashboard-protocol.js +25 -0
  7. package/lib/dashboard-server.js +282 -150
  8. package/lib/errors.js +18 -0
  9. package/lib/file-cache.js +1 -1
  10. package/lib/flag-detection.js +11 -20
  11. package/lib/git-operations.js +15 -33
  12. package/lib/merge-operations.js +40 -34
  13. package/lib/process-executor.js +199 -0
  14. package/lib/registry-cache.js +13 -47
  15. package/lib/skill-loader.js +206 -0
  16. package/lib/smart-json-file.js +2 -4
  17. package/package.json +1 -1
  18. package/scripts/agileflow-configure.js +13 -12
  19. package/scripts/agileflow-statusline.sh +30 -0
  20. package/scripts/agileflow-welcome.js +181 -212
  21. package/scripts/archive-completed-stories.sh +3 -0
  22. package/scripts/auto-self-improve.js +3 -3
  23. package/scripts/ci-summary.js +294 -0
  24. package/scripts/claude-smart.sh +85 -0
  25. package/scripts/claude-tmux.sh +272 -161
  26. package/scripts/damage-control-multi-agent.js +227 -0
  27. package/scripts/lib/bus-utils.js +471 -0
  28. package/scripts/lib/configure-detect.js +87 -10
  29. package/scripts/lib/configure-features.js +110 -4
  30. package/scripts/lib/configure-repair.js +5 -6
  31. package/scripts/lib/configure-utils.js +2 -3
  32. package/scripts/lib/context-formatter.js +87 -8
  33. package/scripts/lib/damage-control-utils.js +37 -3
  34. package/scripts/lib/file-lock.js +392 -0
  35. package/scripts/lib/ideation-index.js +2 -5
  36. package/scripts/lib/lifecycle-detector.js +123 -0
  37. package/scripts/lib/process-cleanup.js +55 -81
  38. package/scripts/lib/scale-detector.js +357 -0
  39. package/scripts/lib/signal-detectors.js +779 -0
  40. package/scripts/lib/story-state-machine.js +1 -1
  41. package/scripts/lib/sync-ideation-status.js +2 -3
  42. package/scripts/lib/task-registry.js +7 -1
  43. package/scripts/lib/team-events.js +357 -0
  44. package/scripts/messaging-bridge.js +79 -36
  45. package/scripts/migrate-ideation-index.js +37 -14
  46. package/scripts/obtain-context.js +37 -19
  47. package/scripts/precompact-context.sh +3 -0
  48. package/scripts/ralph-loop.js +3 -4
  49. package/scripts/smart-detect.js +390 -0
  50. package/scripts/team-manager.js +174 -30
  51. package/src/core/commands/audit.md +13 -11
  52. package/src/core/commands/babysit.md +162 -115
  53. package/src/core/commands/changelog.md +21 -4
  54. package/src/core/commands/configure.md +141 -21
  55. package/src/core/commands/debt.md +12 -2
  56. package/src/core/commands/feedback.md +7 -6
  57. package/src/core/commands/ideate/history.md +1 -1
  58. package/src/core/commands/ideate/new.md +5 -5
  59. package/src/core/commands/logic/audit.md +2 -2
  60. package/src/core/commands/pr.md +7 -6
  61. package/src/core/commands/research/analyze.md +28 -20
  62. package/src/core/commands/research/ask.md +43 -0
  63. package/src/core/commands/research/import.md +29 -21
  64. package/src/core/commands/research/list.md +8 -7
  65. package/src/core/commands/research/synthesize.md +356 -20
  66. package/src/core/commands/research/view.md +8 -5
  67. package/src/core/commands/review.md +24 -6
  68. package/src/core/commands/skill/create.md +34 -0
  69. package/tools/cli/lib/docs-setup.js +4 -0
@@ -21,32 +21,35 @@
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() {
30
+ if (!_http) _http = require('http');
31
+ return _http;
32
+ }
33
+ function getCrypto() {
34
+ if (!_crypto) _crypto = require('crypto');
35
+ return _crypto;
36
+ }
37
+ function getProtocol() {
38
+ if (!_protocol) _protocol = require('./dashboard-protocol');
39
+ return _protocol;
40
+ }
41
+ function getPaths() {
42
+ if (!_paths) _paths = require('./paths');
43
+ return _paths;
44
+ }
45
+ function getValidatePaths() {
46
+ if (!_validatePaths) _validatePaths = require('./validate-paths');
47
+ return _validatePaths;
48
+ }
49
+ function getChildProcess() {
50
+ if (!_childProcess) _childProcess = require('child_process');
51
+ return _childProcess;
52
+ }
50
53
 
51
54
  // Lazy-load automation modules to avoid circular dependencies
52
55
  let AutomationRegistry = null;
@@ -142,7 +145,7 @@ class DashboardSession {
142
145
  send(message) {
143
146
  if (this.ws && this.ws.writable) {
144
147
  try {
145
- const frame = encodeWebSocketFrame(serializeMessage(message));
148
+ const frame = encodeWebSocketFrame(getProtocol().serializeMessage(message));
146
149
  this.ws.write(frame);
147
150
  this.lastActivity = new Date();
148
151
  } catch (error) {
@@ -180,7 +183,7 @@ class DashboardSession {
180
183
  setState(state) {
181
184
  this.state = state;
182
185
  this.send(
183
- createSessionState(this.id, state, {
186
+ getProtocol().createSessionState(this.id, state, {
184
187
  messageCount: this.messages.length,
185
188
  lastActivity: this.lastActivity.toISOString(),
186
189
  })
@@ -249,13 +252,13 @@ class TerminalInstance {
249
252
 
250
253
  this.pty.onData(data => {
251
254
  if (!this.closed) {
252
- this.session.send(createTerminalOutput(this.id, data));
255
+ this.session.send(getProtocol().createTerminalOutput(this.id, data));
253
256
  }
254
257
  });
255
258
 
256
259
  this.pty.onExit(({ exitCode }) => {
257
260
  this.closed = true;
258
- this.session.send(createTerminalExit(this.id, exitCode));
261
+ this.session.send(getProtocol().createTerminalExit(this.id, exitCode));
259
262
  });
260
263
 
261
264
  return true;
@@ -275,7 +278,7 @@ class TerminalInstance {
275
278
  const filteredEnv = this._getFilteredEnv();
276
279
 
277
280
  // Use bash with interactive flag for better compatibility
278
- this.pty = spawn(this.shell, ['-i'], {
281
+ this.pty = getChildProcess().spawn(this.shell, ['-i'], {
279
282
  cwd: this.cwd,
280
283
  env: {
281
284
  ...filteredEnv,
@@ -291,25 +294,27 @@ class TerminalInstance {
291
294
 
292
295
  this.pty.stdout.on('data', data => {
293
296
  if (!this.closed) {
294
- this.session.send(createTerminalOutput(this.id, data.toString()));
297
+ this.session.send(getProtocol().createTerminalOutput(this.id, data.toString()));
295
298
  }
296
299
  });
297
300
 
298
301
  this.pty.stderr.on('data', data => {
299
302
  if (!this.closed) {
300
- this.session.send(createTerminalOutput(this.id, data.toString()));
303
+ this.session.send(getProtocol().createTerminalOutput(this.id, data.toString()));
301
304
  }
302
305
  });
303
306
 
304
307
  this.pty.on('close', exitCode => {
305
308
  this.closed = true;
306
- this.session.send(createTerminalExit(this.id, exitCode));
309
+ this.session.send(getProtocol().createTerminalExit(this.id, exitCode));
307
310
  });
308
311
 
309
312
  this.pty.on('error', error => {
310
313
  console.error('[Terminal] Shell error:', error.message);
311
314
  if (!this.closed) {
312
- this.session.send(createTerminalOutput(this.id, `\r\nError: ${error.message}\r\n`));
315
+ this.session.send(
316
+ getProtocol().createTerminalOutput(this.id, `\r\nError: ${error.message}\r\n`)
317
+ );
313
318
  }
314
319
  });
315
320
 
@@ -318,7 +323,7 @@ class TerminalInstance {
318
323
  if (!this.closed) {
319
324
  const welcomeMsg = `\x1b[32mAgileFlow Terminal\x1b[0m (basic mode - node-pty not available)\r\n`;
320
325
  const cwdMsg = `Working directory: ${this.cwd}\r\n\r\n`;
321
- this.session.send(createTerminalOutput(this.id, welcomeMsg + cwdMsg));
326
+ this.session.send(getProtocol().createTerminalOutput(this.id, welcomeMsg + cwdMsg));
322
327
  }
323
328
  }, 100);
324
329
 
@@ -355,7 +360,7 @@ class TerminalInstance {
355
360
  }
356
361
 
357
362
  // Echo to terminal
358
- this.session.send(createTerminalOutput(this.id, echoData));
363
+ this.session.send(getProtocol().createTerminalOutput(this.id, echoData));
359
364
 
360
365
  // Send to shell stdin
361
366
  this.pty.stdin.write(data);
@@ -406,7 +411,7 @@ class TerminalManager {
406
411
  * @returns {string} - Terminal ID
407
412
  */
408
413
  createTerminal(session, options = {}) {
409
- const terminalId = options.id || crypto.randomBytes(8).toString('hex');
414
+ const terminalId = options.id || getCrypto().randomBytes(8).toString('hex');
410
415
  const terminal = new TerminalInstance(terminalId, session, {
411
416
  cwd: options.cwd || session.projectRoot,
412
417
  cols: options.cols,
@@ -507,13 +512,13 @@ class DashboardServer extends EventEmitter {
507
512
 
508
513
  this.port = options.port || DEFAULT_PORT;
509
514
  this.host = options.host || DEFAULT_HOST;
510
- this.projectRoot = options.projectRoot || getProjectRoot();
515
+ this.projectRoot = options.projectRoot || getPaths().getProjectRoot();
511
516
 
512
517
  // Auth is on by default - auto-generate key if not provided
513
518
  // Set requireAuth: false explicitly to disable
514
519
  this.requireAuth = options.requireAuth !== false;
515
520
  this.apiKey =
516
- options.apiKey || (this.requireAuth ? crypto.randomBytes(32).toString('hex') : null);
521
+ options.apiKey || (this.requireAuth ? getCrypto().randomBytes(32).toString('hex') : null);
517
522
 
518
523
  // Session management
519
524
  this.sessions = new Map();
@@ -536,12 +541,15 @@ class DashboardServer extends EventEmitter {
536
541
  this.httpServer = null;
537
542
 
538
543
  // Validate project
539
- if (!isAgileflowProject(this.projectRoot)) {
544
+ if (!getPaths().isAgileflowProject(this.projectRoot)) {
540
545
  throw new Error(`Not an AgileFlow project: ${this.projectRoot}`);
541
546
  }
542
547
 
543
548
  // Initialize automation registry lazily
544
549
  this._initAutomations();
550
+
551
+ // Listen for team metrics saves to broadcast to clients
552
+ this._initTeamMetricsListener();
545
553
  }
546
554
 
547
555
  /**
@@ -555,12 +563,12 @@ class DashboardServer extends EventEmitter {
555
563
  // Listen to runner events
556
564
  this._automationRunner.on('started', ({ automationId }) => {
557
565
  this._runningAutomations.set(automationId, { startTime: Date.now() });
558
- this.broadcast(createAutomationStatus(automationId, 'running'));
566
+ this.broadcast(getProtocol().createAutomationStatus(automationId, 'running'));
559
567
  });
560
568
 
561
569
  this._automationRunner.on('completed', ({ automationId, result }) => {
562
570
  this._runningAutomations.delete(automationId);
563
- this.broadcast(createAutomationStatus(automationId, 'completed', result));
571
+ this.broadcast(getProtocol().createAutomationStatus(automationId, 'completed', result));
564
572
 
565
573
  // Add result to inbox if it has output or changes
566
574
  if (result.output || result.changes) {
@@ -570,7 +578,9 @@ class DashboardServer extends EventEmitter {
570
578
 
571
579
  this._automationRunner.on('failed', ({ automationId, result }) => {
572
580
  this._runningAutomations.delete(automationId);
573
- this.broadcast(createAutomationStatus(automationId, 'error', { error: result.error }));
581
+ this.broadcast(
582
+ getProtocol().createAutomationStatus(automationId, 'error', { error: result.error })
583
+ );
574
584
 
575
585
  // Add failure to inbox
576
586
  this._addToInbox(automationId, result);
@@ -607,7 +617,7 @@ class DashboardServer extends EventEmitter {
607
617
  };
608
618
 
609
619
  this._inbox.set(itemId, item);
610
- this.broadcast(createInboxItem(item));
620
+ this.broadcast(getProtocol().createInboxItem(item));
611
621
  }
612
622
 
613
623
  /**
@@ -623,7 +633,7 @@ class DashboardServer extends EventEmitter {
623
633
  'Cache-Control': 'no-store',
624
634
  };
625
635
 
626
- this.httpServer = http.createServer((req, res) => {
636
+ this.httpServer = getHttp().createServer((req, res) => {
627
637
  // Simple health check endpoint
628
638
  if (req.url === '/health') {
629
639
  res.writeHead(200, securityHeaders);
@@ -713,7 +723,7 @@ class DashboardServer extends EventEmitter {
713
723
  const providedBuffer = Buffer.from(providedKey, 'utf8');
714
724
  if (
715
725
  keyBuffer.length !== providedBuffer.length ||
716
- !crypto.timingSafeEqual(keyBuffer, providedBuffer)
726
+ !getCrypto().timingSafeEqual(keyBuffer, providedBuffer)
717
727
  ) {
718
728
  socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
719
729
  socket.destroy();
@@ -744,7 +754,7 @@ class DashboardServer extends EventEmitter {
744
754
 
745
755
  // Complete WebSocket handshake
746
756
  const key = req.headers['sec-websocket-key'];
747
- const acceptKey = crypto
757
+ const acceptKey = getCrypto()
748
758
  .createHash('sha1')
749
759
  .update(key + WS_GUID)
750
760
  .digest('base64');
@@ -778,7 +788,7 @@ class DashboardServer extends EventEmitter {
778
788
  }
779
789
 
780
790
  // Generate new session ID
781
- return crypto.randomBytes(16).toString('hex');
791
+ return getCrypto().randomBytes(16).toString('hex');
782
792
  }
783
793
 
784
794
  /**
@@ -801,7 +811,7 @@ class DashboardServer extends EventEmitter {
801
811
 
802
812
  // Send initial state
803
813
  session.send(
804
- createSessionState(sessionId, 'connected', {
814
+ getProtocol().createSessionState(sessionId, 'connected', {
805
815
  resumed: isResume,
806
816
  messageCount: session.messages.length,
807
817
  project: require('path').basename(this.projectRoot),
@@ -814,6 +824,9 @@ class DashboardServer extends EventEmitter {
814
824
  // Send project status (stories/epics)
815
825
  this.sendStatusUpdate(session);
816
826
 
827
+ // Send team metrics
828
+ this.sendTeamMetrics(session);
829
+
817
830
  // Send session list with sync info
818
831
  this.sendSessionList(session);
819
832
 
@@ -874,83 +887,85 @@ class DashboardServer extends EventEmitter {
874
887
  handleMessage(session, data) {
875
888
  // Rate limit incoming messages
876
889
  if (!session.checkRateLimit()) {
877
- session.send(createError('RATE_LIMITED', 'Too many messages, please slow down'));
890
+ session.send(
891
+ getProtocol().createError('RATE_LIMITED', 'Too many messages, please slow down')
892
+ );
878
893
  return;
879
894
  }
880
895
 
881
- const message = parseInboundMessage(data);
896
+ const message = getProtocol().parseInboundMessage(data);
882
897
  if (!message) {
883
- session.send(createError('INVALID_MESSAGE', 'Failed to parse message'));
898
+ session.send(getProtocol().createError('INVALID_MESSAGE', 'Failed to parse message'));
884
899
  return;
885
900
  }
886
901
 
887
902
  console.log(`[Session ${session.id}] Received: ${message.type}`);
888
903
 
889
904
  switch (message.type) {
890
- case InboundMessageType.MESSAGE:
905
+ case getProtocol().InboundMessageType.MESSAGE:
891
906
  this.handleUserMessage(session, message);
892
907
  break;
893
908
 
894
- case InboundMessageType.CANCEL:
909
+ case getProtocol().InboundMessageType.CANCEL:
895
910
  this.handleCancel(session);
896
911
  break;
897
912
 
898
- case InboundMessageType.REFRESH:
913
+ case getProtocol().InboundMessageType.REFRESH:
899
914
  this.handleRefresh(session, message);
900
915
  break;
901
916
 
902
- case InboundMessageType.GIT_STAGE:
903
- case InboundMessageType.GIT_UNSTAGE:
904
- case InboundMessageType.GIT_REVERT:
905
- case InboundMessageType.GIT_COMMIT:
917
+ case getProtocol().InboundMessageType.GIT_STAGE:
918
+ case getProtocol().InboundMessageType.GIT_UNSTAGE:
919
+ case getProtocol().InboundMessageType.GIT_REVERT:
920
+ case getProtocol().InboundMessageType.GIT_COMMIT:
906
921
  this.handleGitAction(session, message);
907
922
  break;
908
923
 
909
- case InboundMessageType.GIT_DIFF_REQUEST:
924
+ case getProtocol().InboundMessageType.GIT_DIFF_REQUEST:
910
925
  this.handleDiffRequest(session, message);
911
926
  break;
912
927
 
913
- case InboundMessageType.SESSION_CLOSE:
928
+ case getProtocol().InboundMessageType.SESSION_CLOSE:
914
929
  this.closeSession(session.id);
915
930
  break;
916
931
 
917
- case InboundMessageType.TERMINAL_SPAWN:
932
+ case getProtocol().InboundMessageType.TERMINAL_SPAWN:
918
933
  this.handleTerminalSpawn(session, message);
919
934
  break;
920
935
 
921
- case InboundMessageType.TERMINAL_INPUT:
936
+ case getProtocol().InboundMessageType.TERMINAL_INPUT:
922
937
  this.handleTerminalInput(session, message);
923
938
  break;
924
939
 
925
- case InboundMessageType.TERMINAL_RESIZE:
940
+ case getProtocol().InboundMessageType.TERMINAL_RESIZE:
926
941
  this.handleTerminalResize(session, message);
927
942
  break;
928
943
 
929
- case InboundMessageType.TERMINAL_CLOSE:
944
+ case getProtocol().InboundMessageType.TERMINAL_CLOSE:
930
945
  this.handleTerminalClose(session, message);
931
946
  break;
932
947
 
933
- case InboundMessageType.AUTOMATION_LIST_REQUEST:
948
+ case getProtocol().InboundMessageType.AUTOMATION_LIST_REQUEST:
934
949
  this.sendAutomationList(session);
935
950
  break;
936
951
 
937
- case InboundMessageType.AUTOMATION_RUN:
952
+ case getProtocol().InboundMessageType.AUTOMATION_RUN:
938
953
  this.handleAutomationRun(session, message);
939
954
  break;
940
955
 
941
- case InboundMessageType.AUTOMATION_STOP:
956
+ case getProtocol().InboundMessageType.AUTOMATION_STOP:
942
957
  this.handleAutomationStop(session, message);
943
958
  break;
944
959
 
945
- case InboundMessageType.INBOX_LIST_REQUEST:
960
+ case getProtocol().InboundMessageType.INBOX_LIST_REQUEST:
946
961
  this.sendInboxList(session);
947
962
  break;
948
963
 
949
- case InboundMessageType.INBOX_ACTION:
964
+ case getProtocol().InboundMessageType.INBOX_ACTION:
950
965
  this.handleInboxAction(session, message);
951
966
  break;
952
967
 
953
- case InboundMessageType.OPEN_FILE:
968
+ case getProtocol().InboundMessageType.OPEN_FILE:
954
969
  this.handleOpenFile(session, message);
955
970
  break;
956
971
 
@@ -966,7 +981,7 @@ class DashboardServer extends EventEmitter {
966
981
  handleUserMessage(session, message) {
967
982
  const content = message.content?.trim();
968
983
  if (!content) {
969
- session.send(createError('EMPTY_MESSAGE', 'Message content is empty'));
984
+ session.send(getProtocol().createError('EMPTY_MESSAGE', 'Message content is empty'));
970
985
  return;
971
986
  }
972
987
 
@@ -985,7 +1000,7 @@ class DashboardServer extends EventEmitter {
985
1000
  */
986
1001
  handleCancel(session) {
987
1002
  session.setState('idle');
988
- session.send(createNotification('info', 'Cancelled', 'Operation cancelled'));
1003
+ session.send(getProtocol().createNotification('info', 'Cancelled', 'Operation cancelled'));
989
1004
  this.emit('user:cancel', session);
990
1005
  }
991
1006
 
@@ -1015,9 +1030,13 @@ class DashboardServer extends EventEmitter {
1015
1030
  case 'inbox':
1016
1031
  this.sendInboxList(session);
1017
1032
  break;
1033
+ case 'team_metrics':
1034
+ this.sendTeamMetrics(session);
1035
+ break;
1018
1036
  default:
1019
1037
  this.sendGitStatus(session);
1020
1038
  this.sendStatusUpdate(session);
1039
+ this.sendTeamMetrics(session);
1021
1040
  this.sendSessionList(session);
1022
1041
  this.sendAutomationList(session);
1023
1042
  this.sendInboxList(session);
@@ -1035,12 +1054,12 @@ class DashboardServer extends EventEmitter {
1035
1054
  if (files && files.length > 0) {
1036
1055
  for (const f of files) {
1037
1056
  if (typeof f !== 'string' || f.includes('\0')) {
1038
- session.send(createError('GIT_ERROR', 'Invalid file path'));
1057
+ session.send(getProtocol().createError('GIT_ERROR', 'Invalid file path'));
1039
1058
  return;
1040
1059
  }
1041
1060
  const resolved = require('path').resolve(this.projectRoot, f);
1042
1061
  if (!resolved.startsWith(this.projectRoot)) {
1043
- session.send(createError('GIT_ERROR', 'File path outside project'));
1062
+ session.send(getProtocol().createError('GIT_ERROR', 'File path outside project'));
1044
1063
  return;
1045
1064
  }
1046
1065
  }
@@ -1053,7 +1072,7 @@ class DashboardServer extends EventEmitter {
1053
1072
  commitMessage.length > 10000 ||
1054
1073
  commitMessage.includes('\0')
1055
1074
  ) {
1056
- session.send(createError('GIT_ERROR', 'Invalid commit message'));
1075
+ session.send(getProtocol().createError('GIT_ERROR', 'Invalid commit message'));
1057
1076
  return;
1058
1077
  }
1059
1078
  }
@@ -1062,40 +1081,50 @@ class DashboardServer extends EventEmitter {
1062
1081
 
1063
1082
  try {
1064
1083
  switch (type) {
1065
- case InboundMessageType.GIT_STAGE:
1084
+ case getProtocol().InboundMessageType.GIT_STAGE:
1066
1085
  if (fileArgs) {
1067
- execFileSync('git', ['add', '--', ...fileArgs], { cwd: this.projectRoot });
1086
+ getChildProcess().execFileSync('git', ['add', '--', ...fileArgs], {
1087
+ cwd: this.projectRoot,
1088
+ });
1068
1089
  } else {
1069
- execFileSync('git', ['add', '-A'], { cwd: this.projectRoot });
1090
+ getChildProcess().execFileSync('git', ['add', '-A'], { cwd: this.projectRoot });
1070
1091
  }
1071
1092
  break;
1072
- case InboundMessageType.GIT_UNSTAGE:
1093
+ case getProtocol().InboundMessageType.GIT_UNSTAGE:
1073
1094
  if (fileArgs) {
1074
- execFileSync('git', ['restore', '--staged', '--', ...fileArgs], {
1095
+ getChildProcess().execFileSync('git', ['restore', '--staged', '--', ...fileArgs], {
1075
1096
  cwd: this.projectRoot,
1076
1097
  });
1077
1098
  } else {
1078
- execFileSync('git', ['restore', '--staged', '.'], { cwd: this.projectRoot });
1099
+ getChildProcess().execFileSync('git', ['restore', '--staged', '.'], {
1100
+ cwd: this.projectRoot,
1101
+ });
1079
1102
  }
1080
1103
  break;
1081
- case InboundMessageType.GIT_REVERT:
1104
+ case getProtocol().InboundMessageType.GIT_REVERT:
1082
1105
  if (fileArgs) {
1083
- execFileSync('git', ['checkout', '--', ...fileArgs], { cwd: this.projectRoot });
1106
+ getChildProcess().execFileSync('git', ['checkout', '--', ...fileArgs], {
1107
+ cwd: this.projectRoot,
1108
+ });
1084
1109
  }
1085
1110
  break;
1086
- case InboundMessageType.GIT_COMMIT:
1111
+ case getProtocol().InboundMessageType.GIT_COMMIT:
1087
1112
  if (commitMessage) {
1088
- execFileSync('git', ['commit', '-m', commitMessage], { cwd: this.projectRoot });
1113
+ getChildProcess().execFileSync('git', ['commit', '-m', commitMessage], {
1114
+ cwd: this.projectRoot,
1115
+ });
1089
1116
  }
1090
1117
  break;
1091
1118
  }
1092
1119
 
1093
1120
  // Send updated git status
1094
1121
  this.sendGitStatus(session);
1095
- session.send(createNotification('success', 'Git', `${type.replace('git_', '')} completed`));
1122
+ session.send(
1123
+ getProtocol().createNotification('success', 'Git', `${type.replace('git_', '')} completed`)
1124
+ );
1096
1125
  } catch (error) {
1097
1126
  console.error('[Git Error]', error.message);
1098
- session.send(createError('GIT_ERROR', 'Git operation failed'));
1127
+ session.send(getProtocol().createError('GIT_ERROR', 'Git operation failed'));
1099
1128
  }
1100
1129
  }
1101
1130
 
@@ -1106,7 +1135,7 @@ class DashboardServer extends EventEmitter {
1106
1135
  try {
1107
1136
  const status = this.getGitStatus();
1108
1137
  session.send({
1109
- type: OutboundMessageType.GIT_STATUS,
1138
+ type: getProtocol().OutboundMessageType.GIT_STATUS,
1110
1139
  ...status,
1111
1140
  timestamp: new Date().toISOString(),
1112
1141
  });
@@ -1120,13 +1149,15 @@ class DashboardServer extends EventEmitter {
1120
1149
  */
1121
1150
  getGitStatus() {
1122
1151
  try {
1123
- const branch = execFileSync('git', ['branch', '--show-current'], {
1124
- cwd: this.projectRoot,
1125
- encoding: 'utf8',
1126
- stdio: ['pipe', 'pipe', 'pipe'],
1127
- }).trim();
1152
+ const branch = getChildProcess()
1153
+ .execFileSync('git', ['branch', '--show-current'], {
1154
+ cwd: this.projectRoot,
1155
+ encoding: 'utf8',
1156
+ stdio: ['pipe', 'pipe', 'pipe'],
1157
+ })
1158
+ .trim();
1128
1159
 
1129
- const statusOutput = execFileSync('git', ['status', '--porcelain'], {
1160
+ const statusOutput = getChildProcess().execFileSync('git', ['status', '--porcelain'], {
1130
1161
  cwd: this.projectRoot,
1131
1162
  encoding: 'utf8',
1132
1163
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -1185,7 +1216,7 @@ class DashboardServer extends EventEmitter {
1185
1216
  const { path: filePath, staged } = message;
1186
1217
 
1187
1218
  if (!filePath) {
1188
- session.send(createError('INVALID_REQUEST', 'File path is required'));
1219
+ session.send(getProtocol().createError('INVALID_REQUEST', 'File path is required'));
1189
1220
  return;
1190
1221
  }
1191
1222
 
@@ -1194,7 +1225,7 @@ class DashboardServer extends EventEmitter {
1194
1225
  const stats = this.parseDiffStats(diff);
1195
1226
 
1196
1227
  session.send(
1197
- createGitDiff(filePath, diff, {
1228
+ getProtocol().createGitDiff(filePath, diff, {
1198
1229
  additions: stats.additions,
1199
1230
  deletions: stats.deletions,
1200
1231
  staged: !!staged,
@@ -1202,7 +1233,7 @@ class DashboardServer extends EventEmitter {
1202
1233
  );
1203
1234
  } catch (error) {
1204
1235
  console.error('[Diff Error]', error.message);
1205
- session.send(createError('DIFF_ERROR', 'Failed to get diff'));
1236
+ session.send(getProtocol().createError('DIFF_ERROR', 'Failed to get diff'));
1206
1237
  }
1207
1238
  }
1208
1239
 
@@ -1214,7 +1245,9 @@ class DashboardServer extends EventEmitter {
1214
1245
  */
1215
1246
  getFileDiff(filePath, staged = false) {
1216
1247
  // Validate filePath stays within project root
1217
- const pathResult = validatePath(filePath, this.projectRoot, { allowSymlinks: true });
1248
+ const pathResult = getValidatePaths().validatePath(filePath, this.projectRoot, {
1249
+ allowSymlinks: true,
1250
+ });
1218
1251
  if (!pathResult.ok) {
1219
1252
  return '';
1220
1253
  }
@@ -1222,17 +1255,19 @@ class DashboardServer extends EventEmitter {
1222
1255
  try {
1223
1256
  const diffArgs = staged ? ['diff', '--cached', '--', filePath] : ['diff', '--', filePath];
1224
1257
 
1225
- const diff = execFileSync('git', diffArgs, {
1258
+ const diff = getChildProcess().execFileSync('git', diffArgs, {
1226
1259
  cwd: this.projectRoot,
1227
1260
  encoding: 'utf8',
1228
1261
  });
1229
1262
 
1230
1263
  // If no diff, file might be untracked - show entire file content as addition
1231
1264
  if (!diff && !staged) {
1232
- const statusOutput = execFileSync('git', ['status', '--porcelain', '--', filePath], {
1233
- cwd: this.projectRoot,
1234
- encoding: 'utf8',
1235
- }).trim();
1265
+ const statusOutput = getChildProcess()
1266
+ .execFileSync('git', ['status', '--porcelain', '--', filePath], {
1267
+ cwd: this.projectRoot,
1268
+ encoding: 'utf8',
1269
+ })
1270
+ .trim();
1236
1271
 
1237
1272
  // Check if file is untracked
1238
1273
  if (statusOutput.startsWith('??')) {
@@ -1318,12 +1353,68 @@ class DashboardServer extends EventEmitter {
1318
1353
  })),
1319
1354
  };
1320
1355
 
1321
- session.send(createStatusUpdate(summary));
1356
+ session.send(getProtocol().createStatusUpdate(summary));
1322
1357
  } catch (error) {
1323
1358
  console.error('[Status Update Error]', error.message);
1324
1359
  }
1325
1360
  }
1326
1361
 
1362
+ /**
1363
+ * Initialize listener for team metrics events
1364
+ */
1365
+ _initTeamMetricsListener() {
1366
+ try {
1367
+ const { teamMetricsEmitter } = require('../scripts/lib/team-events');
1368
+ teamMetricsEmitter.on('metrics_saved', () => {
1369
+ this.broadcastTeamMetrics();
1370
+ });
1371
+ } catch (e) {
1372
+ // team-events not available - non-critical
1373
+ }
1374
+ }
1375
+
1376
+ /**
1377
+ * Send team metrics to a single session
1378
+ */
1379
+ sendTeamMetrics(session) {
1380
+ const path = require('path');
1381
+ const fs = require('fs');
1382
+ const sessionStatePath = path.join(this.projectRoot, 'docs', '09-agents', 'session-state.json');
1383
+ if (!fs.existsSync(sessionStatePath)) return;
1384
+
1385
+ try {
1386
+ const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
1387
+ const traces = (state.team_metrics && state.team_metrics.traces) || {};
1388
+
1389
+ for (const [traceId, metrics] of Object.entries(traces)) {
1390
+ session.send(getProtocol().createTeamMetrics(traceId, metrics));
1391
+ }
1392
+ } catch (error) {
1393
+ // Non-critical
1394
+ }
1395
+ }
1396
+
1397
+ /**
1398
+ * Broadcast team metrics to all connected clients
1399
+ */
1400
+ broadcastTeamMetrics() {
1401
+ const path = require('path');
1402
+ const fs = require('fs');
1403
+ const sessionStatePath = path.join(this.projectRoot, 'docs', '09-agents', 'session-state.json');
1404
+ if (!fs.existsSync(sessionStatePath)) return;
1405
+
1406
+ try {
1407
+ const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
1408
+ const traces = (state.team_metrics && state.team_metrics.traces) || {};
1409
+
1410
+ for (const [traceId, metrics] of Object.entries(traces)) {
1411
+ this.broadcast(getProtocol().createTeamMetrics(traceId, metrics));
1412
+ }
1413
+ } catch (error) {
1414
+ // Non-critical
1415
+ }
1416
+ }
1417
+
1327
1418
  /**
1328
1419
  * Send session list with sync status to dashboard
1329
1420
  */
@@ -1347,23 +1438,23 @@ class DashboardServer extends EventEmitter {
1347
1438
  // Get branch and sync status via git
1348
1439
  try {
1349
1440
  const cwd = s.metadata.worktreePath || this.projectRoot;
1350
- entry.branch = execFileSync('git', ['branch', '--show-current'], {
1351
- cwd,
1352
- encoding: 'utf8',
1353
- stdio: ['pipe', 'pipe', 'pipe'],
1354
- }).trim();
1441
+ entry.branch = getChildProcess()
1442
+ .execFileSync('git', ['branch', '--show-current'], {
1443
+ cwd,
1444
+ encoding: 'utf8',
1445
+ stdio: ['pipe', 'pipe', 'pipe'],
1446
+ })
1447
+ .trim();
1355
1448
 
1356
1449
  // Get ahead/behind counts relative to upstream
1357
1450
  try {
1358
- const counts = execFileSync(
1359
- 'git',
1360
- ['rev-list', '--left-right', '--count', 'HEAD...@{u}'],
1361
- {
1451
+ const counts = getChildProcess()
1452
+ .execFileSync('git', ['rev-list', '--left-right', '--count', 'HEAD...@{u}'], {
1362
1453
  cwd,
1363
1454
  encoding: 'utf8',
1364
1455
  stdio: ['pipe', 'pipe', 'pipe'],
1365
- }
1366
- ).trim();
1456
+ })
1457
+ .trim();
1367
1458
  const [ahead, behind] = counts.split(/\s+/).map(Number);
1368
1459
  entry.ahead = ahead || 0;
1369
1460
  entry.behind = behind || 0;
@@ -1388,7 +1479,7 @@ class DashboardServer extends EventEmitter {
1388
1479
  sessions.push(entry);
1389
1480
  }
1390
1481
 
1391
- session.send(createSessionList(sessions));
1482
+ session.send(getProtocol().createSessionList(sessions));
1392
1483
  }
1393
1484
 
1394
1485
  /**
@@ -1398,14 +1489,16 @@ class DashboardServer extends EventEmitter {
1398
1489
  const { path: filePath, line } = message;
1399
1490
 
1400
1491
  if (!filePath || typeof filePath !== 'string') {
1401
- session.send(createError('INVALID_REQUEST', 'File path is required'));
1492
+ session.send(getProtocol().createError('INVALID_REQUEST', 'File path is required'));
1402
1493
  return;
1403
1494
  }
1404
1495
 
1405
1496
  // Validate the path stays within project root
1406
- const pathResult = validatePath(filePath, this.projectRoot, { allowSymlinks: true });
1497
+ const pathResult = getValidatePaths().validatePath(filePath, this.projectRoot, {
1498
+ allowSymlinks: true,
1499
+ });
1407
1500
  if (!pathResult.ok) {
1408
- session.send(createError('OPEN_FILE_ERROR', 'File path outside project'));
1501
+ session.send(getProtocol().createError('OPEN_FILE_ERROR', 'File path outside project'));
1409
1502
  return;
1410
1503
  }
1411
1504
 
@@ -1423,28 +1516,36 @@ class DashboardServer extends EventEmitter {
1423
1516
  case 'cursor':
1424
1517
  case 'windsurf': {
1425
1518
  const gotoArg = lineNum ? `${fullPath}:${lineNum}` : fullPath;
1426
- spawn(editor, ['--goto', gotoArg], { detached: true, stdio: 'ignore' }).unref();
1519
+ getChildProcess()
1520
+ .spawn(editor, ['--goto', gotoArg], { detached: true, stdio: 'ignore' })
1521
+ .unref();
1427
1522
  break;
1428
1523
  }
1429
1524
  case 'subl':
1430
1525
  case 'sublime_text': {
1431
1526
  const sublArg = lineNum ? `${fullPath}:${lineNum}` : fullPath;
1432
- spawn(editor, [sublArg], { detached: true, stdio: 'ignore' }).unref();
1527
+ getChildProcess().spawn(editor, [sublArg], { detached: true, stdio: 'ignore' }).unref();
1433
1528
  break;
1434
1529
  }
1435
1530
  default: {
1436
1531
  // Generic: just open the file
1437
- spawn(editor, [fullPath], { detached: true, stdio: 'ignore' }).unref();
1532
+ getChildProcess().spawn(editor, [fullPath], { detached: true, stdio: 'ignore' }).unref();
1438
1533
  break;
1439
1534
  }
1440
1535
  }
1441
1536
 
1442
1537
  session.send(
1443
- createNotification('info', 'Editor', `Opened ${require('path').basename(fullPath)}`)
1538
+ getProtocol().createNotification(
1539
+ 'info',
1540
+ 'Editor',
1541
+ `Opened ${require('path').basename(fullPath)}`
1542
+ )
1444
1543
  );
1445
1544
  } catch (error) {
1446
1545
  console.error('[Open File Error]', error.message);
1447
- session.send(createError('OPEN_FILE_ERROR', `Failed to open file: ${error.message}`));
1546
+ session.send(
1547
+ getProtocol().createError('OPEN_FILE_ERROR', `Failed to open file: ${error.message}`)
1548
+ );
1448
1549
  }
1449
1550
  }
1450
1551
 
@@ -1457,10 +1558,15 @@ class DashboardServer extends EventEmitter {
1457
1558
  // Validate cwd stays within project root
1458
1559
  let safeCwd = this.projectRoot;
1459
1560
  if (cwd) {
1460
- const cwdResult = validatePath(cwd, this.projectRoot, { allowSymlinks: true });
1561
+ const cwdResult = getValidatePaths().validatePath(cwd, this.projectRoot, {
1562
+ allowSymlinks: true,
1563
+ });
1461
1564
  if (!cwdResult.ok) {
1462
1565
  session.send(
1463
- createError('TERMINAL_ERROR', 'Working directory must be within project root')
1566
+ getProtocol().createError(
1567
+ 'TERMINAL_ERROR',
1568
+ 'Working directory must be within project root'
1569
+ )
1464
1570
  );
1465
1571
  return;
1466
1572
  }
@@ -1480,7 +1586,7 @@ class DashboardServer extends EventEmitter {
1480
1586
  timestamp: new Date().toISOString(),
1481
1587
  });
1482
1588
  } else {
1483
- session.send(createError('TERMINAL_ERROR', 'Failed to spawn terminal'));
1589
+ session.send(getProtocol().createError('TERMINAL_ERROR', 'Failed to spawn terminal'));
1484
1590
  }
1485
1591
  }
1486
1592
 
@@ -1521,7 +1627,7 @@ class DashboardServer extends EventEmitter {
1521
1627
  }
1522
1628
 
1523
1629
  this.terminalManager.closeTerminal(terminalId);
1524
- session.send(createNotification('info', 'Terminal', 'Terminal closed'));
1630
+ session.send(getProtocol().createNotification('info', 'Terminal', 'Terminal closed'));
1525
1631
  }
1526
1632
 
1527
1633
  // ==========================================================================
@@ -1533,7 +1639,7 @@ class DashboardServer extends EventEmitter {
1533
1639
  */
1534
1640
  sendAutomationList(session) {
1535
1641
  if (!this._automationRegistry) {
1536
- session.send(createAutomationList([]));
1642
+ session.send(getProtocol().createAutomationList([]));
1537
1643
  return;
1538
1644
  }
1539
1645
 
@@ -1555,10 +1661,10 @@ class DashboardServer extends EventEmitter {
1555
1661
  };
1556
1662
  });
1557
1663
 
1558
- session.send(createAutomationList(enriched));
1664
+ session.send(getProtocol().createAutomationList(enriched));
1559
1665
  } catch (error) {
1560
1666
  console.error('[Automations] List error:', error.message);
1561
- session.send(createAutomationList([]));
1667
+ session.send(getProtocol().createAutomationList([]));
1562
1668
  }
1563
1669
  }
1564
1670
 
@@ -1628,12 +1734,14 @@ class DashboardServer extends EventEmitter {
1628
1734
  const { id: automationId } = message;
1629
1735
 
1630
1736
  if (!automationId) {
1631
- session.send(createError('INVALID_REQUEST', 'Automation ID is required'));
1737
+ session.send(getProtocol().createError('INVALID_REQUEST', 'Automation ID is required'));
1632
1738
  return;
1633
1739
  }
1634
1740
 
1635
1741
  if (!this._automationRunner) {
1636
- session.send(createError('AUTOMATION_ERROR', 'Automation runner not initialized'));
1742
+ session.send(
1743
+ getProtocol().createError('AUTOMATION_ERROR', 'Automation runner not initialized')
1744
+ );
1637
1745
  return;
1638
1746
  }
1639
1747
 
@@ -1641,12 +1749,18 @@ class DashboardServer extends EventEmitter {
1641
1749
  // Check if already running
1642
1750
  if (this._runningAutomations.has(automationId)) {
1643
1751
  session.send(
1644
- createNotification('warning', 'Automation', `${automationId} is already running`)
1752
+ getProtocol().createNotification(
1753
+ 'warning',
1754
+ 'Automation',
1755
+ `${automationId} is already running`
1756
+ )
1645
1757
  );
1646
1758
  return;
1647
1759
  }
1648
1760
 
1649
- session.send(createNotification('info', 'Automation', `Starting ${automationId}...`));
1761
+ session.send(
1762
+ getProtocol().createNotification('info', 'Automation', `Starting ${automationId}...`)
1763
+ );
1650
1764
 
1651
1765
  // Run the automation (async)
1652
1766
  const result = await this._automationRunner.run(automationId);
@@ -1654,23 +1768,39 @@ class DashboardServer extends EventEmitter {
1654
1768
  // Send result notification
1655
1769
  if (result.success) {
1656
1770
  session.send(
1657
- createNotification('success', 'Automation', `${automationId} completed successfully`)
1771
+ getProtocol().createNotification(
1772
+ 'success',
1773
+ 'Automation',
1774
+ `${automationId} completed successfully`
1775
+ )
1658
1776
  );
1659
1777
  } else {
1660
1778
  session.send(
1661
- createNotification('error', 'Automation', `${automationId} failed: ${result.error}`)
1779
+ getProtocol().createNotification(
1780
+ 'error',
1781
+ 'Automation',
1782
+ `${automationId} failed: ${result.error}`
1783
+ )
1662
1784
  );
1663
1785
  }
1664
1786
 
1665
1787
  // Send final status
1666
- session.send(createAutomationStatus(automationId, result.success ? 'idle' : 'error', result));
1788
+ session.send(
1789
+ getProtocol().createAutomationStatus(
1790
+ automationId,
1791
+ result.success ? 'idle' : 'error',
1792
+ result
1793
+ )
1794
+ );
1667
1795
 
1668
1796
  // Refresh the list
1669
1797
  this.sendAutomationList(session);
1670
1798
  } catch (error) {
1671
1799
  console.error('[Automation Error]', error.message);
1672
- session.send(createError('AUTOMATION_ERROR', 'Automation execution failed'));
1673
- session.send(createAutomationStatus(automationId, 'error', { error: 'Execution failed' }));
1800
+ session.send(getProtocol().createError('AUTOMATION_ERROR', 'Automation execution failed'));
1801
+ session.send(
1802
+ getProtocol().createAutomationStatus(automationId, 'error', { error: 'Execution failed' })
1803
+ );
1674
1804
  }
1675
1805
  }
1676
1806
 
@@ -1681,7 +1811,7 @@ class DashboardServer extends EventEmitter {
1681
1811
  const { id: automationId } = message;
1682
1812
 
1683
1813
  if (!automationId) {
1684
- session.send(createError('INVALID_REQUEST', 'Automation ID is required'));
1814
+ session.send(getProtocol().createError('INVALID_REQUEST', 'Automation ID is required'));
1685
1815
  return;
1686
1816
  }
1687
1817
 
@@ -1691,8 +1821,8 @@ class DashboardServer extends EventEmitter {
1691
1821
  }
1692
1822
 
1693
1823
  this._runningAutomations.delete(automationId);
1694
- session.send(createAutomationStatus(automationId, 'idle'));
1695
- session.send(createNotification('info', 'Automation', `${automationId} stopped`));
1824
+ session.send(getProtocol().createAutomationStatus(automationId, 'idle'));
1825
+ session.send(getProtocol().createNotification('info', 'Automation', `${automationId} stopped`));
1696
1826
  }
1697
1827
 
1698
1828
  // ==========================================================================
@@ -1707,7 +1837,7 @@ class DashboardServer extends EventEmitter {
1707
1837
  (a, b) => new Date(b.timestamp) - new Date(a.timestamp)
1708
1838
  );
1709
1839
 
1710
- session.send(createInboxList(items));
1840
+ session.send(getProtocol().createInboxList(items));
1711
1841
  }
1712
1842
 
1713
1843
  /**
@@ -1717,13 +1847,13 @@ class DashboardServer extends EventEmitter {
1717
1847
  const { id: itemId, action } = message;
1718
1848
 
1719
1849
  if (!itemId) {
1720
- session.send(createError('INVALID_REQUEST', 'Item ID is required'));
1850
+ session.send(getProtocol().createError('INVALID_REQUEST', 'Item ID is required'));
1721
1851
  return;
1722
1852
  }
1723
1853
 
1724
1854
  const item = this._inbox.get(itemId);
1725
1855
  if (!item) {
1726
- session.send(createError('NOT_FOUND', `Inbox item ${itemId} not found`));
1856
+ session.send(getProtocol().createError('NOT_FOUND', `Inbox item ${itemId} not found`));
1727
1857
  return;
1728
1858
  }
1729
1859
 
@@ -1731,14 +1861,16 @@ class DashboardServer extends EventEmitter {
1731
1861
  case 'accept':
1732
1862
  // Mark as accepted and remove
1733
1863
  item.status = 'accepted';
1734
- session.send(createNotification('success', 'Inbox', `Accepted: ${item.title}`));
1864
+ session.send(
1865
+ getProtocol().createNotification('success', 'Inbox', `Accepted: ${item.title}`)
1866
+ );
1735
1867
  this._inbox.delete(itemId);
1736
1868
  break;
1737
1869
 
1738
1870
  case 'dismiss':
1739
1871
  // Mark as dismissed and remove
1740
1872
  item.status = 'dismissed';
1741
- session.send(createNotification('info', 'Inbox', `Dismissed: ${item.title}`));
1873
+ session.send(getProtocol().createNotification('info', 'Inbox', `Dismissed: ${item.title}`));
1742
1874
  this._inbox.delete(itemId);
1743
1875
  break;
1744
1876
 
@@ -1748,7 +1880,7 @@ class DashboardServer extends EventEmitter {
1748
1880
  break;
1749
1881
 
1750
1882
  default:
1751
- session.send(createError('INVALID_ACTION', `Unknown action: ${action}`));
1883
+ session.send(getProtocol().createError('INVALID_ACTION', `Unknown action: ${action}`));
1752
1884
  return;
1753
1885
  }
1754
1886