jfl 0.2.5 → 0.4.2

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 (133) hide show
  1. package/README.md +308 -28
  2. package/dist/commands/context-hub.d.ts.map +1 -1
  3. package/dist/commands/context-hub.js +428 -27
  4. package/dist/commands/context-hub.js.map +1 -1
  5. package/dist/commands/eval.d.ts +6 -0
  6. package/dist/commands/eval.d.ts.map +1 -0
  7. package/dist/commands/eval.js +236 -0
  8. package/dist/commands/eval.js.map +1 -0
  9. package/dist/commands/flows.d.ts +4 -1
  10. package/dist/commands/flows.d.ts.map +1 -1
  11. package/dist/commands/flows.js +160 -1
  12. package/dist/commands/flows.js.map +1 -1
  13. package/dist/commands/init.d.ts.map +1 -1
  14. package/dist/commands/init.js +272 -145
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/peter.d.ts.map +1 -1
  17. package/dist/commands/peter.js +220 -1
  18. package/dist/commands/peter.js.map +1 -1
  19. package/dist/commands/pi.d.ts +21 -0
  20. package/dist/commands/pi.d.ts.map +1 -0
  21. package/dist/commands/pi.js +154 -0
  22. package/dist/commands/pi.js.map +1 -0
  23. package/dist/commands/portfolio.d.ts +6 -0
  24. package/dist/commands/portfolio.d.ts.map +1 -0
  25. package/dist/commands/portfolio.js +249 -0
  26. package/dist/commands/portfolio.js.map +1 -0
  27. package/dist/commands/predict.d.ts +6 -0
  28. package/dist/commands/predict.d.ts.map +1 -0
  29. package/dist/commands/predict.js +234 -0
  30. package/dist/commands/predict.js.map +1 -0
  31. package/dist/commands/scope.d.ts +1 -0
  32. package/dist/commands/scope.d.ts.map +1 -1
  33. package/dist/commands/scope.js +189 -2
  34. package/dist/commands/scope.js.map +1 -1
  35. package/dist/commands/synopsis.d.ts +44 -0
  36. package/dist/commands/synopsis.d.ts.map +1 -1
  37. package/dist/commands/synopsis.js +1 -1
  38. package/dist/commands/synopsis.js.map +1 -1
  39. package/dist/commands/update.d.ts.map +1 -1
  40. package/dist/commands/update.js +49 -1
  41. package/dist/commands/update.js.map +1 -1
  42. package/dist/commands/viz.d.ts +7 -0
  43. package/dist/commands/viz.d.ts.map +1 -0
  44. package/dist/commands/viz.js +460 -0
  45. package/dist/commands/viz.js.map +1 -0
  46. package/dist/commands/voice.js.map +1 -1
  47. package/dist/dashboard/index.d.ts +4 -5
  48. package/dist/dashboard/index.d.ts.map +1 -1
  49. package/dist/dashboard/index.js +57 -119
  50. package/dist/dashboard/index.js.map +1 -1
  51. package/dist/dashboard-static/assets/index-B6kRK9Rq.js +116 -0
  52. package/dist/dashboard-static/assets/index-BpdKJPLu.css +1 -0
  53. package/dist/dashboard-static/index.html +16 -0
  54. package/dist/index.js +120 -20
  55. package/dist/index.js.map +1 -1
  56. package/dist/lib/eval-store.d.ts +15 -0
  57. package/dist/lib/eval-store.d.ts.map +1 -0
  58. package/dist/lib/eval-store.js +179 -0
  59. package/dist/lib/eval-store.js.map +1 -0
  60. package/dist/lib/flow-engine.d.ts +13 -0
  61. package/dist/lib/flow-engine.d.ts.map +1 -1
  62. package/dist/lib/flow-engine.js +164 -3
  63. package/dist/lib/flow-engine.js.map +1 -1
  64. package/dist/lib/hub-client.d.ts +80 -0
  65. package/dist/lib/hub-client.d.ts.map +1 -0
  66. package/dist/lib/hub-client.js +46 -0
  67. package/dist/lib/hub-client.js.map +1 -0
  68. package/dist/lib/predictor.d.ts +99 -0
  69. package/dist/lib/predictor.d.ts.map +1 -0
  70. package/dist/lib/predictor.js +394 -0
  71. package/dist/lib/predictor.js.map +1 -0
  72. package/dist/lib/service-gtm.d.ts +88 -44
  73. package/dist/lib/service-gtm.d.ts.map +1 -1
  74. package/dist/lib/service-gtm.js +451 -243
  75. package/dist/lib/service-gtm.js.map +1 -1
  76. package/dist/lib/telemetry-agent.d.ts +57 -0
  77. package/dist/lib/telemetry-agent.d.ts.map +1 -0
  78. package/dist/lib/telemetry-agent.js +268 -0
  79. package/dist/lib/telemetry-agent.js.map +1 -0
  80. package/dist/lib/telemetry-digest.d.ts.map +1 -1
  81. package/dist/lib/telemetry-digest.js +17 -17
  82. package/dist/lib/telemetry-digest.js.map +1 -1
  83. package/dist/lib/telemetry.d.ts +1 -0
  84. package/dist/lib/telemetry.d.ts.map +1 -1
  85. package/dist/lib/telemetry.js +14 -6
  86. package/dist/lib/telemetry.js.map +1 -1
  87. package/dist/lib/trajectory-loader.d.ts +82 -0
  88. package/dist/lib/trajectory-loader.d.ts.map +1 -0
  89. package/dist/lib/trajectory-loader.js +406 -0
  90. package/dist/lib/trajectory-loader.js.map +1 -0
  91. package/dist/mcp/context-hub-mcp.js +60 -0
  92. package/dist/mcp/context-hub-mcp.js.map +1 -1
  93. package/dist/mcp/service-registry-mcp.js +0 -0
  94. package/dist/types/eval.d.ts +18 -0
  95. package/dist/types/eval.d.ts.map +1 -0
  96. package/dist/types/eval.js +5 -0
  97. package/dist/types/eval.js.map +1 -0
  98. package/dist/types/journal.d.ts +133 -0
  99. package/dist/types/journal.d.ts.map +1 -0
  100. package/dist/types/journal.js +59 -0
  101. package/dist/types/journal.js.map +1 -0
  102. package/dist/types/map.d.ts +1 -1
  103. package/dist/types/map.d.ts.map +1 -1
  104. package/dist/types/map.js.map +1 -1
  105. package/dist/ui/service-dashboard.js.map +1 -1
  106. package/dist/utils/jfl-paths.d.ts +1 -0
  107. package/dist/utils/jfl-paths.d.ts.map +1 -1
  108. package/dist/utils/jfl-paths.js +1 -0
  109. package/dist/utils/jfl-paths.js.map +1 -1
  110. package/dist/utils/wallet.js.map +1 -1
  111. package/package.json +7 -2
  112. package/scripts/generate-changesets.sh +113 -0
  113. package/scripts/migrate-to-branch-sessions.sh +201 -0
  114. package/scripts/pp-branch-pr.sh +115 -0
  115. package/scripts/session/session-cleanup.sh +29 -14
  116. package/scripts/session/session-end.sh +0 -10
  117. package/scripts/session/session-init.sh +0 -16
  118. package/scripts/session/session-sync.sh +0 -10
  119. package/template/.jfl/flows-self-driving.yaml +170 -0
  120. package/template/THEORY.md +26 -0
  121. package/template/scripts/session/session-cleanup.sh +28 -10
  122. package/dist/dashboard/components.d.ts +0 -7
  123. package/dist/dashboard/components.d.ts.map +0 -1
  124. package/dist/dashboard/components.js +0 -163
  125. package/dist/dashboard/components.js.map +0 -1
  126. package/dist/dashboard/pages.d.ts +0 -7
  127. package/dist/dashboard/pages.d.ts.map +0 -1
  128. package/dist/dashboard/pages.js +0 -742
  129. package/dist/dashboard/pages.js.map +0 -1
  130. package/dist/dashboard/styles.d.ts +0 -7
  131. package/dist/dashboard/styles.d.ts.map +0 -1
  132. package/dist/dashboard/styles.js +0 -497
  133. package/dist/dashboard/styles.js.map +0 -1
@@ -415,11 +415,89 @@ function getUnifiedContext(projectRoot, query, taskType) {
415
415
  taskType
416
416
  };
417
417
  }
418
+ function getChildHubs(projectRoot) {
419
+ const configPath = path.join(projectRoot, ".jfl", "config.json");
420
+ if (!fs.existsSync(configPath))
421
+ return [];
422
+ try {
423
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
424
+ if (config.type !== "portfolio")
425
+ return [];
426
+ const children = [];
427
+ for (const service of config.registered_services || []) {
428
+ if (service.type === "gtm" && service.path) {
429
+ const childPort = getProjectPort(service.path);
430
+ const tokenPath = path.join(service.path, ".jfl", "context-hub.token");
431
+ const token = fs.existsSync(tokenPath)
432
+ ? fs.readFileSync(tokenPath, "utf-8").trim()
433
+ : undefined;
434
+ children.push({ name: service.name, path: service.path, port: childPort, token });
435
+ }
436
+ }
437
+ return children;
438
+ }
439
+ catch {
440
+ return [];
441
+ }
442
+ }
443
+ async function fetchChildContext(child, endpoint, body) {
444
+ try {
445
+ const headers = { "Content-Type": "application/json" };
446
+ if (child.token)
447
+ headers["Authorization"] = `Bearer ${child.token}`;
448
+ const response = await fetch(`http://localhost:${child.port}${endpoint}`, {
449
+ method: "POST",
450
+ headers,
451
+ body: JSON.stringify(body),
452
+ signal: AbortSignal.timeout(5000),
453
+ });
454
+ if (!response.ok)
455
+ return [];
456
+ const data = (await response.json());
457
+ return (data.items || []).map((item) => ({
458
+ ...item,
459
+ title: `[${child.name}] ${item.title}`,
460
+ }));
461
+ }
462
+ catch {
463
+ return readJournalEntries(child.path, 10).map((item) => ({
464
+ ...item,
465
+ title: `[${child.name}] ${item.title}`,
466
+ }));
467
+ }
468
+ }
469
+ async function getPortfolioContext(projectRoot, query, taskType, maxItems) {
470
+ const local = getUnifiedContext(projectRoot, query, taskType);
471
+ const children = getChildHubs(projectRoot);
472
+ if (children.length === 0)
473
+ return local;
474
+ const endpoint = query ? "/api/context/search" : "/api/context";
475
+ const body = { maxItems: maxItems || 20 };
476
+ if (query)
477
+ body.query = query;
478
+ if (taskType)
479
+ body.taskType = taskType;
480
+ const childResults = await Promise.all(children.map((child) => fetchChildContext(child, endpoint, body)));
481
+ let merged = [...local.items, ...childResults.flat()];
482
+ if (query) {
483
+ merged = semanticSearch(merged, query);
484
+ }
485
+ return {
486
+ items: maxItems ? merged.slice(0, maxItems) : merged,
487
+ sources: {
488
+ ...local.sources,
489
+ journal: true,
490
+ knowledge: true,
491
+ },
492
+ query,
493
+ taskType,
494
+ };
495
+ }
418
496
  // ============================================================================
419
497
  // HTTP Server
420
498
  // ============================================================================
421
- function createServer(projectRoot, port, eventBus) {
422
- const server = http.createServer((req, res) => {
499
+ function createServer(projectRoot, port, eventBus, flowEngine) {
500
+ const server = http.createServer(async (req, res) => {
423
501
  const requestStart = Date.now();
424
502
  const pathname = new URL(req.url || "/", `http://localhost:${port}`).pathname;
425
503
  // Intercept writeHead to capture status code for telemetry
@@ -533,29 +611,74 @@ function createServer(projectRoot, port, eventBus) {
533
611
  }));
534
612
  return;
535
613
  }
536
- // Status
614
+ // Status (includes child hub health for portfolio)
537
615
  if (url.pathname === "/api/context/status" && req.method === "GET") {
538
616
  const context = getUnifiedContext(projectRoot);
617
+ const children = getChildHubs(projectRoot);
618
+ // Read actual workspace type from config
619
+ let workspaceType = "standalone";
620
+ let workspaceConfig = {};
621
+ const configPath = path.join(projectRoot, ".jfl", "config.json");
622
+ if (fs.existsSync(configPath)) {
623
+ try {
624
+ const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
625
+ if (cfg.type === "portfolio" || cfg.type === "gtm" || cfg.type === "service") {
626
+ workspaceType = cfg.type;
627
+ }
628
+ workspaceConfig = {
629
+ name: cfg.name,
630
+ type: cfg.type,
631
+ description: cfg.description,
632
+ scope: cfg.context_scope || null,
633
+ registered_services: (cfg.registered_services || []).map((s) => ({
634
+ name: s.name,
635
+ path: s.path,
636
+ type: s.type,
637
+ status: s.status,
638
+ context_scope: s.context_scope || null,
639
+ })),
640
+ openclaw_agents: (cfg.openclaw_agents || []).map((a) => ({
641
+ id: a.id,
642
+ runtime: a.runtime,
643
+ registered_at: a.registered_at,
644
+ })),
645
+ gtm_parent: cfg.gtm_parent || null,
646
+ portfolio_parent: cfg.portfolio_parent || null,
647
+ };
648
+ }
649
+ catch { }
650
+ }
651
+ const childStatus = await Promise.all(children.map(async (child) => {
652
+ try {
653
+ const resp = await fetch(`http://localhost:${child.port}/health`, {
654
+ signal: AbortSignal.timeout(2000),
655
+ });
656
+ return { name: child.name, port: child.port, status: resp.ok ? "ok" : "error" };
657
+ }
658
+ catch {
659
+ return { name: child.name, port: child.port, status: "down" };
660
+ }
661
+ }));
539
662
  res.writeHead(200, { "Content-Type": "application/json" });
540
663
  res.end(JSON.stringify({
541
664
  status: "running",
542
665
  port,
666
+ type: workspaceType,
667
+ config: workspaceConfig,
543
668
  sources: context.sources,
544
- itemCount: context.items.length
669
+ itemCount: context.items.length,
670
+ ...(children.length > 0 ? { children: childStatus } : {}),
545
671
  }));
546
672
  return;
547
673
  }
548
- // Get context
674
+ // Get context (portfolio-aware: fans out to child hubs)
549
675
  if (url.pathname === "/api/context" && req.method === "POST") {
550
676
  let body = "";
551
677
  req.on("data", chunk => body += chunk);
552
- req.on("end", () => {
678
+ req.on("end", async () => {
553
679
  try {
554
680
  const { query, taskType, maxItems } = JSON.parse(body || "{}");
555
- const context = getUnifiedContext(projectRoot, query, taskType);
556
- if (maxItems && context.items.length > maxItems) {
557
- context.items = context.items.slice(0, maxItems);
558
- }
681
+ const context = await getPortfolioContext(projectRoot, query, taskType, maxItems);
559
682
  telemetry.track({
560
683
  category: 'context_hub',
561
684
  event: 'context_hub:context_loaded',
@@ -575,11 +698,11 @@ function createServer(projectRoot, port, eventBus) {
575
698
  });
576
699
  return;
577
700
  }
578
- // Search
701
+ // Search (portfolio-aware: fans out to child hubs)
579
702
  if (url.pathname === "/api/context/search" && req.method === "POST") {
580
703
  let body = "";
581
704
  req.on("data", chunk => body += chunk);
582
- req.on("end", () => {
705
+ req.on("end", async () => {
583
706
  try {
584
707
  const { query, maxItems = 20 } = JSON.parse(body || "{}");
585
708
  if (!query) {
@@ -588,7 +711,7 @@ function createServer(projectRoot, port, eventBus) {
588
711
  return;
589
712
  }
590
713
  const searchStart = Date.now();
591
- const context = getUnifiedContext(projectRoot, query);
714
+ const context = await getPortfolioContext(projectRoot, query, undefined, maxItems);
592
715
  context.items = context.items
593
716
  .filter(item => item.relevance && item.relevance > 0)
594
717
  .slice(0, maxItems);
@@ -707,6 +830,90 @@ function createServer(projectRoot, port, eventBus) {
707
830
  });
708
831
  return;
709
832
  }
833
+ // Eval trajectory
834
+ if (url.pathname === "/api/eval/trajectory" && req.method === "GET") {
835
+ try {
836
+ const { getTrajectory } = await import("../lib/eval-store.js");
837
+ const agent = url.searchParams.get("agent") || "";
838
+ const metric = url.searchParams.get("metric") || "composite";
839
+ if (!agent) {
840
+ res.writeHead(400, { "Content-Type": "application/json" });
841
+ res.end(JSON.stringify({ error: "agent query param required" }));
842
+ return;
843
+ }
844
+ const points = getTrajectory(agent, metric, projectRoot);
845
+ res.writeHead(200, { "Content-Type": "application/json" });
846
+ res.end(JSON.stringify({ agent, metric, points }));
847
+ }
848
+ catch (err) {
849
+ res.writeHead(500, { "Content-Type": "application/json" });
850
+ res.end(JSON.stringify({ error: err.message }));
851
+ }
852
+ return;
853
+ }
854
+ // Eval leaderboard
855
+ if (url.pathname === "/api/eval/leaderboard" && req.method === "GET") {
856
+ try {
857
+ const { readEvals, listAgents, getLatestEval, getTrajectory } = await import("../lib/eval-store.js");
858
+ const agents = listAgents(projectRoot);
859
+ const leaderboard = agents.map(agent => {
860
+ const latest = getLatestEval(agent, projectRoot);
861
+ const trajectory = getTrajectory(agent, "composite", projectRoot);
862
+ const prevPoint = trajectory.length >= 2 ? trajectory[trajectory.length - 2] : null;
863
+ const delta = latest?.composite != null && prevPoint
864
+ ? latest.composite - prevPoint.value
865
+ : null;
866
+ return {
867
+ agent,
868
+ composite: latest?.composite ?? null,
869
+ metrics: latest?.metrics ?? {},
870
+ delta,
871
+ model_version: latest?.model_version ?? null,
872
+ lastTs: latest?.ts ?? null,
873
+ trajectory: trajectory.slice(-20).map(p => p.value),
874
+ };
875
+ }).sort((a, b) => (b.composite ?? 0) - (a.composite ?? 0));
876
+ res.writeHead(200, { "Content-Type": "application/json" });
877
+ res.end(JSON.stringify(leaderboard));
878
+ }
879
+ catch (err) {
880
+ res.writeHead(500, { "Content-Type": "application/json" });
881
+ res.end(JSON.stringify({ error: err.message }));
882
+ }
883
+ return;
884
+ }
885
+ // Synopsis (work summary)
886
+ if (url.pathname === "/api/synopsis" && req.method === "GET") {
887
+ try {
888
+ const hours = parseInt(url.searchParams.get("hours") || "24", 10);
889
+ const author = url.searchParams.get("author") || undefined;
890
+ const { generateSynopsis } = await import("./synopsis.js");
891
+ const synopsis = generateSynopsis(projectRoot, hours, author);
892
+ res.writeHead(200, { "Content-Type": "application/json" });
893
+ res.end(JSON.stringify(synopsis));
894
+ }
895
+ catch (err) {
896
+ res.writeHead(500, { "Content-Type": "application/json" });
897
+ res.end(JSON.stringify({ error: err.message }));
898
+ }
899
+ return;
900
+ }
901
+ // Prediction accuracy (Stratus)
902
+ if (url.pathname === "/api/eval/predictions" && req.method === "GET") {
903
+ try {
904
+ const { Predictor } = await import("../lib/predictor.js");
905
+ const predictor = new Predictor({ projectRoot });
906
+ const accuracy = predictor.getAccuracy();
907
+ const recent = predictor.getHistory(20).reverse();
908
+ res.writeHead(200, { "Content-Type": "application/json" });
909
+ res.end(JSON.stringify({ accuracy, recent }));
910
+ }
911
+ catch (err) {
912
+ res.writeHead(200, { "Content-Type": "application/json" });
913
+ res.end(JSON.stringify({ accuracy: { total: 0, resolved: 0, direction_accuracy: 0, mean_delta_error: 0, calibration: 0 }, recent: [] }));
914
+ }
915
+ return;
916
+ }
710
917
  // Cross-project health
711
918
  if (url.pathname === "/api/projects" && req.method === "GET") {
712
919
  const tracked = getTrackedProjects();
@@ -820,6 +1027,177 @@ function createServer(projectRoot, port, eventBus) {
820
1027
  });
821
1028
  return;
822
1029
  }
1030
+ // Telemetry digest
1031
+ if (url.pathname === "/api/telemetry/digest" && req.method === "GET") {
1032
+ try {
1033
+ const { loadLocalEvents, analyzeEvents } = await import("../lib/telemetry-digest.js");
1034
+ const hours = parseInt(url.searchParams.get("hours") || "168", 10);
1035
+ const events = loadLocalEvents();
1036
+ const digest = analyzeEvents(events, hours);
1037
+ res.writeHead(200, { "Content-Type": "application/json" });
1038
+ res.end(JSON.stringify(digest));
1039
+ }
1040
+ catch (err) {
1041
+ res.writeHead(500, { "Content-Type": "application/json" });
1042
+ res.end(JSON.stringify({ error: err.message }));
1043
+ }
1044
+ return;
1045
+ }
1046
+ // Telemetry agent status
1047
+ if (url.pathname === "/api/telemetry/agent" && req.method === "GET") {
1048
+ const agent = server.__telemetryAgent;
1049
+ if (agent) {
1050
+ res.writeHead(200, { "Content-Type": "application/json" });
1051
+ res.end(JSON.stringify(agent.getStatus()));
1052
+ }
1053
+ else {
1054
+ res.writeHead(200, { "Content-Type": "application/json" });
1055
+ res.end(JSON.stringify({ running: false, lastRun: '', runCount: 0, lastInsights: [] }));
1056
+ }
1057
+ return;
1058
+ }
1059
+ // Telemetry agent: trigger manual run
1060
+ if (url.pathname === "/api/telemetry/agent/run" && req.method === "POST") {
1061
+ const agent = server.__telemetryAgent;
1062
+ if (agent) {
1063
+ try {
1064
+ const insights = await agent.run();
1065
+ res.writeHead(200, { "Content-Type": "application/json" });
1066
+ res.end(JSON.stringify({ ok: true, insights }));
1067
+ }
1068
+ catch (err) {
1069
+ res.writeHead(500, { "Content-Type": "application/json" });
1070
+ res.end(JSON.stringify({ error: err.message }));
1071
+ }
1072
+ }
1073
+ else {
1074
+ res.writeHead(503, { "Content-Type": "application/json" });
1075
+ res.end(JSON.stringify({ error: "Telemetry agent not running" }));
1076
+ }
1077
+ return;
1078
+ }
1079
+ // Flow definitions
1080
+ if (url.pathname === "/api/flows" && req.method === "GET") {
1081
+ if (!flowEngine) {
1082
+ res.writeHead(200, { "Content-Type": "application/json" });
1083
+ res.end(JSON.stringify([]));
1084
+ return;
1085
+ }
1086
+ res.writeHead(200, { "Content-Type": "application/json" });
1087
+ res.end(JSON.stringify(flowEngine.getFlows()));
1088
+ return;
1089
+ }
1090
+ // Flow executions
1091
+ if (url.pathname === "/api/flows/executions" && req.method === "GET") {
1092
+ if (!flowEngine) {
1093
+ res.writeHead(200, { "Content-Type": "application/json" });
1094
+ res.end(JSON.stringify({ executions: [] }));
1095
+ return;
1096
+ }
1097
+ res.writeHead(200, { "Content-Type": "application/json" });
1098
+ res.end(JSON.stringify({ executions: flowEngine.getExecutions() }));
1099
+ return;
1100
+ }
1101
+ // Flow approval
1102
+ if (url.pathname.match(/^\/api\/flows\/[^/]+\/approve$/) && req.method === "POST") {
1103
+ if (!flowEngine) {
1104
+ res.writeHead(503, { "Content-Type": "application/json" });
1105
+ res.end(JSON.stringify({ error: "Flow engine not initialized" }));
1106
+ return;
1107
+ }
1108
+ const flowName = decodeURIComponent(url.pathname.split("/")[3]);
1109
+ let body = "";
1110
+ req.on("data", chunk => body += chunk);
1111
+ req.on("end", async () => {
1112
+ try {
1113
+ const { trigger_event_id } = JSON.parse(body || "{}");
1114
+ if (!trigger_event_id) {
1115
+ res.writeHead(400, { "Content-Type": "application/json" });
1116
+ res.end(JSON.stringify({ error: "trigger_event_id required" }));
1117
+ return;
1118
+ }
1119
+ const result = await flowEngine.approveGated(flowName, trigger_event_id);
1120
+ if (!result) {
1121
+ res.writeHead(404, { "Content-Type": "application/json" });
1122
+ res.end(JSON.stringify({ error: "Gated execution not found" }));
1123
+ return;
1124
+ }
1125
+ res.writeHead(200, { "Content-Type": "application/json" });
1126
+ res.end(JSON.stringify(result));
1127
+ }
1128
+ catch (err) {
1129
+ res.writeHead(500, { "Content-Type": "application/json" });
1130
+ res.end(JSON.stringify({ error: err.message }));
1131
+ }
1132
+ });
1133
+ return;
1134
+ }
1135
+ if (url.pathname.match(/^\/api\/flows\/[^/]+\/toggle$/) && req.method === "POST") {
1136
+ if (!flowEngine) {
1137
+ res.writeHead(503, { "Content-Type": "application/json" });
1138
+ res.end(JSON.stringify({ error: "Flow engine not initialized" }));
1139
+ return;
1140
+ }
1141
+ const flowName = decodeURIComponent(url.pathname.split("/")[3]);
1142
+ let body = "";
1143
+ req.on("data", chunk => body += chunk);
1144
+ req.on("end", () => {
1145
+ try {
1146
+ const { enabled } = JSON.parse(body || "{}");
1147
+ const result = flowEngine.toggleFlow(flowName, enabled);
1148
+ if (!result) {
1149
+ res.writeHead(404, { "Content-Type": "application/json" });
1150
+ res.end(JSON.stringify({ error: "Flow not found" }));
1151
+ return;
1152
+ }
1153
+ res.writeHead(200, { "Content-Type": "application/json" });
1154
+ res.end(JSON.stringify({ ok: true, flow: flowName, enabled: result.enabled }));
1155
+ }
1156
+ catch (err) {
1157
+ res.writeHead(500, { "Content-Type": "application/json" });
1158
+ res.end(JSON.stringify({ error: err.message }));
1159
+ }
1160
+ });
1161
+ return;
1162
+ }
1163
+ if (url.pathname === "/api/actions/spawn" && req.method === "POST") {
1164
+ let body = "";
1165
+ req.on("data", chunk => body += chunk);
1166
+ req.on("end", () => {
1167
+ try {
1168
+ const { command, args, cwd, event_type, event_data } = JSON.parse(body || "{}");
1169
+ if (!command) {
1170
+ res.writeHead(400, { "Content-Type": "application/json" });
1171
+ res.end(JSON.stringify({ error: "command required" }));
1172
+ return;
1173
+ }
1174
+ const env = { ...process.env };
1175
+ delete env.ANTHROPIC_API_KEY;
1176
+ delete env.CLAUDE_CODE_ENTRYPOINT;
1177
+ const child = spawn(command, args || [], {
1178
+ cwd: cwd || projectRoot,
1179
+ detached: true,
1180
+ stdio: "ignore",
1181
+ env,
1182
+ });
1183
+ child.unref();
1184
+ if (event_type && eventBus) {
1185
+ eventBus.emit({
1186
+ type: event_type,
1187
+ source: "dashboard:action",
1188
+ data: event_data || { command, args, pid: child.pid },
1189
+ });
1190
+ }
1191
+ res.writeHead(200, { "Content-Type": "application/json" });
1192
+ res.end(JSON.stringify({ ok: true, pid: child.pid }));
1193
+ }
1194
+ catch (err) {
1195
+ res.writeHead(500, { "Content-Type": "application/json" });
1196
+ res.end(JSON.stringify({ error: err.message }));
1197
+ }
1198
+ });
1199
+ return;
1200
+ }
823
1201
  // 404
824
1202
  res.writeHead(404, { "Content-Type": "application/json" });
825
1203
  res.end(JSON.stringify({ error: "Not found" }));
@@ -1187,17 +1565,18 @@ export async function contextHubCommand(action, options = {}) {
1187
1565
  const isGlobal = options.global || false;
1188
1566
  const projectRoot = isGlobal ? homedir() : process.cwd();
1189
1567
  const port = options.port || getProjectPort(projectRoot);
1190
- // Ensure directories exist
1191
- if (isGlobal) {
1192
- // Global mode: use XDG directories
1193
- const { JFL_PATHS, ensureJflDirs } = await import("../utils/jfl-paths.js");
1194
- ensureJflDirs();
1195
- }
1196
- else {
1197
- // Project mode: use .jfl/
1198
- const jflDir = path.join(projectRoot, ".jfl");
1199
- if (!fs.existsSync(jflDir)) {
1200
- fs.mkdirSync(jflDir, { recursive: true });
1568
+ // Ensure directories exist (skip for actions that don't need local project root)
1569
+ const globalActions = ["ensure-all", "doctor", "install-daemon", "uninstall-daemon"];
1570
+ if (!globalActions.includes(action || "")) {
1571
+ if (isGlobal) {
1572
+ const { JFL_PATHS, ensureJflDirs } = await import("../utils/jfl-paths.js");
1573
+ ensureJflDirs();
1574
+ }
1575
+ else {
1576
+ const jflDir = path.join(projectRoot, ".jfl");
1577
+ if (!fs.existsSync(jflDir)) {
1578
+ fs.mkdirSync(jflDir, { recursive: true });
1579
+ }
1201
1580
  }
1202
1581
  }
1203
1582
  switch (action) {
@@ -1397,7 +1776,8 @@ export async function contextHubCommand(action, options = {}) {
1397
1776
  serviceEventsPath,
1398
1777
  journalDir: fs.existsSync(journalDir) ? journalDir : null,
1399
1778
  });
1400
- const server = createServer(projectRoot, port, eventBus);
1779
+ const flowEngine = new FlowEngine(eventBus, projectRoot);
1780
+ const server = createServer(projectRoot, port, eventBus, flowEngine);
1401
1781
  let isListening = false;
1402
1782
  // When spawned as daemon, ignore SIGTERM during startup grace period.
1403
1783
  // The parent process (hook runner) may exit and send SIGTERM to the
@@ -1470,9 +1850,13 @@ export async function contextHubCommand(action, options = {}) {
1470
1850
  console.error(`[${timestamp}] Failed to initialize memory system:`, err.message);
1471
1851
  // Don't exit - memory is optional
1472
1852
  }
1473
- // Start flow engine
1853
+ // Start flow engine (with child hub connections for portfolio mode)
1474
1854
  try {
1475
- const flowEngine = new FlowEngine(eventBus, projectRoot);
1855
+ const children = getChildHubs(projectRoot);
1856
+ if (children.length > 0) {
1857
+ flowEngine.setChildren(children);
1858
+ console.log(`[${timestamp}] Portfolio mode: connecting to ${children.length} child hub(s)`);
1859
+ }
1476
1860
  const flowCount = await flowEngine.start();
1477
1861
  if (flowCount > 0) {
1478
1862
  console.log(`[${timestamp}] Flow engine started with ${flowCount} active flow(s)`);
@@ -1481,6 +1865,23 @@ export async function contextHubCommand(action, options = {}) {
1481
1865
  catch (err) {
1482
1866
  console.error(`[${timestamp}] Failed to start flow engine:`, err.message);
1483
1867
  }
1868
+ // Start telemetry agent (periodic pattern detection)
1869
+ try {
1870
+ const { TelemetryAgent } = await import("../lib/telemetry-agent.js");
1871
+ const telemetryAgent = new TelemetryAgent({
1872
+ projectRoot,
1873
+ intervalMs: 30 * 60 * 1000,
1874
+ emitEvent: (type, data, source) => {
1875
+ eventBus.emit({ type: type, data, source: source || 'telemetry-agent' });
1876
+ },
1877
+ });
1878
+ telemetryAgent.start();
1879
+ server.__telemetryAgent = telemetryAgent;
1880
+ console.log(`[${timestamp}] Telemetry agent started (interval: 30m)`);
1881
+ }
1882
+ catch (err) {
1883
+ console.error(`[${timestamp}] Failed to start telemetry agent:`, err.message);
1884
+ }
1484
1885
  console.log(`[${timestamp}] MAP event bus initialized (buffer: 1000, subscribers: ${eventBus.getSubscriberCount()})`);
1485
1886
  console.log(`[${timestamp}] Ready to serve requests`);
1486
1887
  });