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.
- package/README.md +16 -0
- package/dist/commands/context-hub.d.ts.map +1 -1
- package/dist/commands/context-hub.js +274 -27
- package/dist/commands/context-hub.js.map +1 -1
- package/dist/commands/eval.d.ts +6 -0
- package/dist/commands/eval.d.ts.map +1 -0
- package/dist/commands/eval.js +236 -0
- package/dist/commands/eval.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +230 -145
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/portfolio.d.ts +6 -0
- package/dist/commands/portfolio.d.ts.map +1 -0
- package/dist/commands/portfolio.js +296 -0
- package/dist/commands/portfolio.js.map +1 -0
- package/dist/commands/scope.d.ts +1 -0
- package/dist/commands/scope.d.ts.map +1 -1
- package/dist/commands/scope.js +189 -2
- package/dist/commands/scope.js.map +1 -1
- package/dist/commands/voice.js.map +1 -1
- package/dist/dashboard/components.d.ts +1 -1
- package/dist/dashboard/components.d.ts.map +1 -1
- package/dist/dashboard/components.js +418 -6
- package/dist/dashboard/components.js.map +1 -1
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +32 -5
- package/dist/dashboard/index.js.map +1 -1
- package/dist/dashboard/pages.d.ts +1 -1
- package/dist/dashboard/pages.d.ts.map +1 -1
- package/dist/dashboard/pages.js +961 -123
- package/dist/dashboard/pages.js.map +1 -1
- package/dist/dashboard/styles.d.ts +1 -1
- package/dist/dashboard/styles.d.ts.map +1 -1
- package/dist/dashboard/styles.js +701 -88
- package/dist/dashboard/styles.js.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/eval-store.d.ts +15 -0
- package/dist/lib/eval-store.d.ts.map +1 -0
- package/dist/lib/eval-store.js +179 -0
- package/dist/lib/eval-store.js.map +1 -0
- package/dist/lib/flow-engine.d.ts +12 -0
- package/dist/lib/flow-engine.d.ts.map +1 -1
- package/dist/lib/flow-engine.js +134 -2
- package/dist/lib/flow-engine.js.map +1 -1
- package/dist/lib/service-gtm.d.ts +10 -1
- package/dist/lib/service-gtm.d.ts.map +1 -1
- package/dist/lib/service-gtm.js +35 -2
- package/dist/lib/service-gtm.js.map +1 -1
- package/dist/lib/trajectory-loader.d.ts +82 -0
- package/dist/lib/trajectory-loader.d.ts.map +1 -0
- package/dist/lib/trajectory-loader.js +406 -0
- package/dist/lib/trajectory-loader.js.map +1 -0
- package/dist/mcp/context-hub-mcp.js +60 -0
- package/dist/mcp/context-hub-mcp.js.map +1 -1
- package/dist/types/eval.d.ts +18 -0
- package/dist/types/eval.d.ts.map +1 -0
- package/dist/types/eval.js +5 -0
- package/dist/types/eval.js.map +1 -0
- package/dist/types/journal.d.ts +133 -0
- package/dist/types/journal.d.ts.map +1 -0
- package/dist/types/journal.js +59 -0
- package/dist/types/journal.js.map +1 -0
- package/dist/types/map.d.ts +1 -1
- package/dist/types/map.d.ts.map +1 -1
- package/dist/types/map.js.map +1 -1
- package/dist/ui/service-dashboard.js.map +1 -1
- package/dist/utils/wallet.js.map +1 -1
- package/package.json +1 -1
- package/scripts/migrate-to-branch-sessions.sh +201 -0
- package/scripts/session/session-cleanup.sh +29 -14
- package/scripts/session/session-end.sh +0 -10
- package/scripts/session/session-init.sh +0 -16
- package/scripts/session/session-sync.sh +0 -10
- package/template/THEORY.md +26 -0
- 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;
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
|
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
|
|
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)`);
|