jfl 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +16 -0
  2. package/dist/commands/context-hub.d.ts.map +1 -1
  3. package/dist/commands/context-hub.js +274 -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/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +230 -145
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/portfolio.d.ts +6 -0
  13. package/dist/commands/portfolio.d.ts.map +1 -0
  14. package/dist/commands/portfolio.js +296 -0
  15. package/dist/commands/portfolio.js.map +1 -0
  16. package/dist/commands/scope.d.ts +1 -0
  17. package/dist/commands/scope.d.ts.map +1 -1
  18. package/dist/commands/scope.js +189 -2
  19. package/dist/commands/scope.js.map +1 -1
  20. package/dist/commands/voice.js.map +1 -1
  21. package/dist/dashboard/components.d.ts +1 -1
  22. package/dist/dashboard/components.d.ts.map +1 -1
  23. package/dist/dashboard/components.js +418 -6
  24. package/dist/dashboard/components.js.map +1 -1
  25. package/dist/dashboard/index.d.ts.map +1 -1
  26. package/dist/dashboard/index.js +32 -5
  27. package/dist/dashboard/index.js.map +1 -1
  28. package/dist/dashboard/pages.d.ts +1 -1
  29. package/dist/dashboard/pages.d.ts.map +1 -1
  30. package/dist/dashboard/pages.js +961 -123
  31. package/dist/dashboard/pages.js.map +1 -1
  32. package/dist/dashboard/styles.d.ts +1 -1
  33. package/dist/dashboard/styles.d.ts.map +1 -1
  34. package/dist/dashboard/styles.js +701 -88
  35. package/dist/dashboard/styles.js.map +1 -1
  36. package/dist/index.js +9 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/lib/eval-store.d.ts +15 -0
  39. package/dist/lib/eval-store.d.ts.map +1 -0
  40. package/dist/lib/eval-store.js +179 -0
  41. package/dist/lib/eval-store.js.map +1 -0
  42. package/dist/lib/flow-engine.d.ts +12 -0
  43. package/dist/lib/flow-engine.d.ts.map +1 -1
  44. package/dist/lib/flow-engine.js +134 -2
  45. package/dist/lib/flow-engine.js.map +1 -1
  46. package/dist/lib/service-gtm.d.ts +10 -1
  47. package/dist/lib/service-gtm.d.ts.map +1 -1
  48. package/dist/lib/service-gtm.js +35 -2
  49. package/dist/lib/service-gtm.js.map +1 -1
  50. package/dist/lib/trajectory-loader.d.ts +82 -0
  51. package/dist/lib/trajectory-loader.d.ts.map +1 -0
  52. package/dist/lib/trajectory-loader.js +406 -0
  53. package/dist/lib/trajectory-loader.js.map +1 -0
  54. package/dist/mcp/context-hub-mcp.js +60 -0
  55. package/dist/mcp/context-hub-mcp.js.map +1 -1
  56. package/dist/types/eval.d.ts +18 -0
  57. package/dist/types/eval.d.ts.map +1 -0
  58. package/dist/types/eval.js +5 -0
  59. package/dist/types/eval.js.map +1 -0
  60. package/dist/types/journal.d.ts +133 -0
  61. package/dist/types/journal.d.ts.map +1 -0
  62. package/dist/types/journal.js +59 -0
  63. package/dist/types/journal.js.map +1 -0
  64. package/dist/types/map.d.ts +1 -1
  65. package/dist/types/map.d.ts.map +1 -1
  66. package/dist/types/map.js.map +1 -1
  67. package/dist/ui/service-dashboard.js.map +1 -1
  68. package/dist/utils/wallet.js.map +1 -1
  69. package/package.json +1 -1
  70. package/scripts/migrate-to-branch-sessions.sh +201 -0
  71. package/scripts/session/session-cleanup.sh +29 -14
  72. package/scripts/session/session-end.sh +0 -10
  73. package/scripts/session/session-init.sh +0 -16
  74. package/scripts/session/session-sync.sh +0 -10
  75. package/template/THEORY.md +26 -0
  76. package/template/scripts/session/session-cleanup.sh +28 -10
package/README.md CHANGED
@@ -63,6 +63,8 @@ my-project/ <- GTM workspace (strategy, context, orchestratio
63
63
  │ ├── config.json <- Project config (team, services, ports)
64
64
  │ ├── journal/ <- Session journals (JSONL, one file per session)
65
65
  │ ├── memory.db <- Indexed memory (TF-IDF + embeddings)
66
+ │ ├── agents/ <- Narrowly-scoped agent manifests + policies
67
+ │ ├── flows/ <- Per-agent flow definitions (auto-loaded)
66
68
  │ ├── service-events.jsonl <- Event bus file-drop
67
69
  │ └── services.json <- Registered services
68
70
  ├── knowledge/ <- Strategy docs (VISION, ROADMAP, THESIS, etc.)
@@ -233,6 +235,7 @@ jfl services # Interactive TUI (no args)
233
235
  | `jfl init -n <name>` | Create new GTM workspace |
234
236
  | `jfl status` | Project status and auth |
235
237
  | `jfl hud [-c\|--compact]` | Campaign dashboard (ship date, phases, pipeline) |
238
+ | `jfl doctor [--fix]` | Check project health, auto-repair fixable issues |
236
239
  | `jfl update [--dry]` | Pull latest skills, scripts, templates (preserves CLAUDE.md, .mcp.json) |
237
240
  | `jfl synopsis [hours] [author]` | Work summary (journal + commits + file headers) |
238
241
  | `jfl repair` | Fix corrupted .jfl/config.json |
@@ -290,6 +293,9 @@ jfl services # Interactive TUI (no args)
290
293
 
291
294
  | Command | Description |
292
295
  |---------|-------------|
296
+ | `jfl agent init <name> [-d desc]` | Scaffold agent (manifest + policy + lifecycle flows) |
297
+ | `jfl agent list` | List registered agents |
298
+ | `jfl agent status <name>` | Show agent health and config |
293
299
  | `jfl ralph [args]` | Ralph-tui agent loop orchestrator |
294
300
  | `jfl peter [action]` | Peter Parker model-routed orchestrator (setup, run, status) |
295
301
  | `jfl orchestrate [name] [--list] [--create <n>]` | Multi-service orchestration workflows |
@@ -524,6 +530,16 @@ jfl wallet # Wallet and day pass status
524
530
 
525
531
  ## What's New
526
532
 
533
+ **0.2.5**
534
+ - Feat: Docker-style grouped `jfl --help` — 5 groups (Getting Started, Daily Use, Management, Platform, Advanced), ~30 lines down from 52
535
+ - Feat: `jfl doctor [--fix]` — unified project health checker (9 checks: .jfl dir, config, Context Hub, hooks, memory, journal, agents, flows, git). Auto-repairs hooks, config, and journal with `--fix`
536
+ - Feat: `jfl agent init|list|status` — scaffold narrowly-scoped agents with manifest, policy, and lifecycle flows
537
+ - Feat: Flow engine scans `.jfl/flows/*.yaml` for per-agent flow definitions
538
+ - Feat: Kuva terminal plots + spawn action type in flow engine
539
+ - Fix: Stop committing JFL runtime files (.jfl/logs/, memory.db, *.pid) — gitignore + untrack ([@hathbanger](https://github.com/hathbanger) [#5](https://github.com/402goose/jfl-cli/pull/5))
540
+ - Fix: Enforce `jfl update --auto` on session start with 24h cache ([@hathbanger](https://github.com/hathbanger) [#5](https://github.com/402goose/jfl-cli/pull/5))
541
+ - Test: 31 new tests (agent-manifest, doctor, agent command, flow-engine directory scan)
542
+
527
543
  **0.2.4**
528
544
  - Feat: `jfl telemetry digest` — per-model cost tables, command stats, health analysis, improvement suggestions
529
545
  - Feat: `jfl improve` — self-improvement loop with GitHub issue creation (`--auto`)
@@ -1 +1 @@
1
- {"version":3,"file":"context-hub.d.ts","sourceRoot":"","sources":["../../src/commands/context-hub.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAoiCH,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAiBjF;AA2ND,wBAAsB,qBAAqB,CAAC,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAiHxF;AAMD,wBAAsB,iBAAiB,CACrC,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,iBAwenE"}
1
+ {"version":3,"file":"context-hub.d.ts","sourceRoot":"","sources":["../../src/commands/context-hub.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAmzCH,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAiBjF;AA2ND,wBAAsB,qBAAqB,CAAC,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAiHxF;AAMD,wBAAsB,iBAAiB,CACrC,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,iBA8enE"}
@@ -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,68 @@ 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
+ type: s.type,
636
+ status: s.status,
637
+ context_scope: s.context_scope || null,
638
+ })),
639
+ gtm_parent: cfg.gtm_parent || null,
640
+ portfolio_parent: cfg.portfolio_parent || null,
641
+ };
642
+ }
643
+ catch { }
644
+ }
645
+ const childStatus = await Promise.all(children.map(async (child) => {
646
+ try {
647
+ const resp = await fetch(`http://localhost:${child.port}/health`, {
648
+ signal: AbortSignal.timeout(2000),
649
+ });
650
+ return { name: child.name, port: child.port, status: resp.ok ? "ok" : "error" };
651
+ }
652
+ catch {
653
+ return { name: child.name, port: child.port, status: "down" };
654
+ }
655
+ }));
539
656
  res.writeHead(200, { "Content-Type": "application/json" });
540
657
  res.end(JSON.stringify({
541
658
  status: "running",
542
659
  port,
660
+ type: workspaceType,
661
+ config: workspaceConfig,
543
662
  sources: context.sources,
544
- itemCount: context.items.length
663
+ itemCount: context.items.length,
664
+ ...(children.length > 0 ? { children: childStatus } : {}),
545
665
  }));
546
666
  return;
547
667
  }
548
- // Get context
668
+ // Get context (portfolio-aware: fans out to child hubs)
549
669
  if (url.pathname === "/api/context" && req.method === "POST") {
550
670
  let body = "";
551
671
  req.on("data", chunk => body += chunk);
552
- req.on("end", () => {
672
+ req.on("end", async () => {
553
673
  try {
554
674
  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
- }
675
+ const context = await getPortfolioContext(projectRoot, query, taskType, maxItems);
559
676
  telemetry.track({
560
677
  category: 'context_hub',
561
678
  event: 'context_hub:context_loaded',
@@ -575,11 +692,11 @@ function createServer(projectRoot, port, eventBus) {
575
692
  });
576
693
  return;
577
694
  }
578
- // Search
695
+ // Search (portfolio-aware: fans out to child hubs)
579
696
  if (url.pathname === "/api/context/search" && req.method === "POST") {
580
697
  let body = "";
581
698
  req.on("data", chunk => body += chunk);
582
- req.on("end", () => {
699
+ req.on("end", async () => {
583
700
  try {
584
701
  const { query, maxItems = 20 } = JSON.parse(body || "{}");
585
702
  if (!query) {
@@ -588,7 +705,7 @@ function createServer(projectRoot, port, eventBus) {
588
705
  return;
589
706
  }
590
707
  const searchStart = Date.now();
591
- const context = getUnifiedContext(projectRoot, query);
708
+ const context = await getPortfolioContext(projectRoot, query, undefined, maxItems);
592
709
  context.items = context.items
593
710
  .filter(item => item.relevance && item.relevance > 0)
594
711
  .slice(0, maxItems);
@@ -707,6 +824,58 @@ function createServer(projectRoot, port, eventBus) {
707
824
  });
708
825
  return;
709
826
  }
827
+ // Eval trajectory
828
+ if (url.pathname === "/api/eval/trajectory" && req.method === "GET") {
829
+ try {
830
+ const { getTrajectory } = await import("../lib/eval-store.js");
831
+ const agent = url.searchParams.get("agent") || "";
832
+ const metric = url.searchParams.get("metric") || "composite";
833
+ if (!agent) {
834
+ res.writeHead(400, { "Content-Type": "application/json" });
835
+ res.end(JSON.stringify({ error: "agent query param required" }));
836
+ return;
837
+ }
838
+ const points = getTrajectory(agent, metric, projectRoot);
839
+ res.writeHead(200, { "Content-Type": "application/json" });
840
+ res.end(JSON.stringify({ agent, metric, points }));
841
+ }
842
+ catch (err) {
843
+ res.writeHead(500, { "Content-Type": "application/json" });
844
+ res.end(JSON.stringify({ error: err.message }));
845
+ }
846
+ return;
847
+ }
848
+ // Eval leaderboard
849
+ if (url.pathname === "/api/eval/leaderboard" && req.method === "GET") {
850
+ try {
851
+ const { readEvals, listAgents, getLatestEval, getTrajectory } = await import("../lib/eval-store.js");
852
+ const agents = listAgents(projectRoot);
853
+ const leaderboard = agents.map(agent => {
854
+ const latest = getLatestEval(agent, projectRoot);
855
+ const trajectory = getTrajectory(agent, "composite", projectRoot);
856
+ const prevPoint = trajectory.length >= 2 ? trajectory[trajectory.length - 2] : null;
857
+ const delta = latest?.composite != null && prevPoint
858
+ ? latest.composite - prevPoint.value
859
+ : null;
860
+ return {
861
+ agent,
862
+ composite: latest?.composite ?? null,
863
+ metrics: latest?.metrics ?? {},
864
+ delta,
865
+ model_version: latest?.model_version ?? null,
866
+ lastTs: latest?.ts ?? null,
867
+ trajectory: trajectory.slice(-20).map(p => p.value),
868
+ };
869
+ }).sort((a, b) => (b.composite ?? 0) - (a.composite ?? 0));
870
+ res.writeHead(200, { "Content-Type": "application/json" });
871
+ res.end(JSON.stringify(leaderboard));
872
+ }
873
+ catch (err) {
874
+ res.writeHead(500, { "Content-Type": "application/json" });
875
+ res.end(JSON.stringify({ error: err.message }));
876
+ }
877
+ return;
878
+ }
710
879
  // Cross-project health
711
880
  if (url.pathname === "/api/projects" && req.method === "GET") {
712
881
  const tracked = getTrackedProjects();
@@ -820,6 +989,78 @@ function createServer(projectRoot, port, eventBus) {
820
989
  });
821
990
  return;
822
991
  }
992
+ // Telemetry digest
993
+ if (url.pathname === "/api/telemetry/digest" && req.method === "GET") {
994
+ try {
995
+ const { loadLocalEvents, analyzeEvents } = await import("../lib/telemetry-digest.js");
996
+ const hours = parseInt(url.searchParams.get("hours") || "168", 10);
997
+ const events = loadLocalEvents();
998
+ const digest = analyzeEvents(events, hours);
999
+ res.writeHead(200, { "Content-Type": "application/json" });
1000
+ res.end(JSON.stringify(digest));
1001
+ }
1002
+ catch (err) {
1003
+ res.writeHead(500, { "Content-Type": "application/json" });
1004
+ res.end(JSON.stringify({ error: err.message }));
1005
+ }
1006
+ return;
1007
+ }
1008
+ // Flow definitions
1009
+ if (url.pathname === "/api/flows" && req.method === "GET") {
1010
+ if (!flowEngine) {
1011
+ res.writeHead(200, { "Content-Type": "application/json" });
1012
+ res.end(JSON.stringify([]));
1013
+ return;
1014
+ }
1015
+ res.writeHead(200, { "Content-Type": "application/json" });
1016
+ res.end(JSON.stringify(flowEngine.getFlows()));
1017
+ return;
1018
+ }
1019
+ // Flow executions
1020
+ if (url.pathname === "/api/flows/executions" && req.method === "GET") {
1021
+ if (!flowEngine) {
1022
+ res.writeHead(200, { "Content-Type": "application/json" });
1023
+ res.end(JSON.stringify({ executions: [] }));
1024
+ return;
1025
+ }
1026
+ res.writeHead(200, { "Content-Type": "application/json" });
1027
+ res.end(JSON.stringify({ executions: flowEngine.getExecutions() }));
1028
+ return;
1029
+ }
1030
+ // Flow approval
1031
+ if (url.pathname.match(/^\/api\/flows\/[^/]+\/approve$/) && req.method === "POST") {
1032
+ if (!flowEngine) {
1033
+ res.writeHead(503, { "Content-Type": "application/json" });
1034
+ res.end(JSON.stringify({ error: "Flow engine not initialized" }));
1035
+ return;
1036
+ }
1037
+ const flowName = decodeURIComponent(url.pathname.split("/")[3]);
1038
+ let body = "";
1039
+ req.on("data", chunk => body += chunk);
1040
+ req.on("end", async () => {
1041
+ try {
1042
+ const { trigger_event_id } = JSON.parse(body || "{}");
1043
+ if (!trigger_event_id) {
1044
+ res.writeHead(400, { "Content-Type": "application/json" });
1045
+ res.end(JSON.stringify({ error: "trigger_event_id required" }));
1046
+ return;
1047
+ }
1048
+ const result = await flowEngine.approveGated(flowName, trigger_event_id);
1049
+ if (!result) {
1050
+ res.writeHead(404, { "Content-Type": "application/json" });
1051
+ res.end(JSON.stringify({ error: "Gated execution not found" }));
1052
+ return;
1053
+ }
1054
+ res.writeHead(200, { "Content-Type": "application/json" });
1055
+ res.end(JSON.stringify(result));
1056
+ }
1057
+ catch (err) {
1058
+ res.writeHead(500, { "Content-Type": "application/json" });
1059
+ res.end(JSON.stringify({ error: err.message }));
1060
+ }
1061
+ });
1062
+ return;
1063
+ }
823
1064
  // 404
824
1065
  res.writeHead(404, { "Content-Type": "application/json" });
825
1066
  res.end(JSON.stringify({ error: "Not found" }));
@@ -1187,17 +1428,18 @@ export async function contextHubCommand(action, options = {}) {
1187
1428
  const isGlobal = options.global || false;
1188
1429
  const projectRoot = isGlobal ? homedir() : process.cwd();
1189
1430
  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 });
1431
+ // Ensure directories exist (skip for actions that don't need local project root)
1432
+ const globalActions = ["ensure-all", "doctor", "install-daemon", "uninstall-daemon"];
1433
+ if (!globalActions.includes(action || "")) {
1434
+ if (isGlobal) {
1435
+ const { JFL_PATHS, ensureJflDirs } = await import("../utils/jfl-paths.js");
1436
+ ensureJflDirs();
1437
+ }
1438
+ else {
1439
+ const jflDir = path.join(projectRoot, ".jfl");
1440
+ if (!fs.existsSync(jflDir)) {
1441
+ fs.mkdirSync(jflDir, { recursive: true });
1442
+ }
1201
1443
  }
1202
1444
  }
1203
1445
  switch (action) {
@@ -1397,7 +1639,8 @@ export async function contextHubCommand(action, options = {}) {
1397
1639
  serviceEventsPath,
1398
1640
  journalDir: fs.existsSync(journalDir) ? journalDir : null,
1399
1641
  });
1400
- const server = createServer(projectRoot, port, eventBus);
1642
+ const flowEngine = new FlowEngine(eventBus, projectRoot);
1643
+ const server = createServer(projectRoot, port, eventBus, flowEngine);
1401
1644
  let isListening = false;
1402
1645
  // When spawned as daemon, ignore SIGTERM during startup grace period.
1403
1646
  // The parent process (hook runner) may exit and send SIGTERM to the
@@ -1470,9 +1713,13 @@ export async function contextHubCommand(action, options = {}) {
1470
1713
  console.error(`[${timestamp}] Failed to initialize memory system:`, err.message);
1471
1714
  // Don't exit - memory is optional
1472
1715
  }
1473
- // Start flow engine
1716
+ // Start flow engine (with child hub connections for portfolio mode)
1474
1717
  try {
1475
- const flowEngine = new FlowEngine(eventBus, projectRoot);
1718
+ const children = getChildHubs(projectRoot);
1719
+ if (children.length > 0) {
1720
+ flowEngine.setChildren(children);
1721
+ console.log(`[${timestamp}] Portfolio mode: connecting to ${children.length} child hub(s)`);
1722
+ }
1476
1723
  const flowCount = await flowEngine.start();
1477
1724
  if (flowCount > 0) {
1478
1725
  console.log(`[${timestamp}] Flow engine started with ${flowCount} active flow(s)`);