jfl 0.4.4 → 0.6.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/dist/commands/context-hub.d.ts +1 -0
- package/dist/commands/context-hub.d.ts.map +1 -1
- package/dist/commands/context-hub.js +1064 -41
- package/dist/commands/context-hub.js.map +1 -1
- package/dist/commands/eval.d.ts +1 -1
- package/dist/commands/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +192 -1
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/findings.d.ts +6 -0
- package/dist/commands/findings.d.ts.map +1 -0
- package/dist/commands/findings.js +203 -0
- package/dist/commands/findings.js.map +1 -0
- package/dist/commands/hud.d.ts.map +1 -1
- package/dist/commands/hud.js +47 -9
- package/dist/commands/hud.js.map +1 -1
- package/dist/commands/ide.d.ts +27 -0
- package/dist/commands/ide.d.ts.map +1 -0
- package/dist/commands/ide.js +546 -0
- package/dist/commands/ide.js.map +1 -0
- package/dist/commands/onboard.d.ts.map +1 -1
- package/dist/commands/onboard.js +212 -2
- package/dist/commands/onboard.js.map +1 -1
- package/dist/commands/openclaw.d.ts +3 -0
- package/dist/commands/openclaw.d.ts.map +1 -1
- package/dist/commands/openclaw.js +76 -2
- package/dist/commands/openclaw.js.map +1 -1
- package/dist/commands/peter.d.ts +3 -0
- package/dist/commands/peter.d.ts.map +1 -1
- package/dist/commands/peter.js +1168 -58
- package/dist/commands/peter.js.map +1 -1
- package/dist/commands/pi-fleet.d.ts +18 -0
- package/dist/commands/pi-fleet.d.ts.map +1 -0
- package/dist/commands/pi-fleet.js +382 -0
- package/dist/commands/pi-fleet.js.map +1 -0
- package/dist/commands/pi.d.ts.map +1 -1
- package/dist/commands/pi.js +18 -3
- package/dist/commands/pi.js.map +1 -1
- package/dist/commands/scope.d.ts.map +1 -1
- package/dist/commands/scope.js +90 -1
- package/dist/commands/scope.js.map +1 -1
- package/dist/commands/services.d.ts.map +1 -1
- package/dist/commands/services.js +18 -0
- package/dist/commands/services.js.map +1 -1
- package/dist/commands/setup.d.ts +12 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +322 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +22 -4
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/train.d.ts +33 -0
- package/dist/commands/train.d.ts.map +1 -0
- package/dist/commands/train.js +510 -0
- package/dist/commands/train.js.map +1 -0
- package/dist/commands/verify.d.ts +14 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +276 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/viz.d.ts.map +1 -1
- package/dist/commands/viz.js +417 -0
- package/dist/commands/viz.js.map +1 -1
- package/dist/dashboard-static/assets/index-CW9ZxqX8.css +1 -0
- package/dist/dashboard-static/assets/index-DNN__p4K.js +121 -0
- package/dist/dashboard-static/index.html +2 -2
- package/dist/index.js +324 -64
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-config.d.ts +52 -0
- package/dist/lib/agent-config.d.ts.map +1 -0
- package/dist/lib/agent-config.js +231 -0
- package/dist/lib/agent-config.js.map +1 -0
- package/dist/lib/agent-generator.d.ts +10 -0
- package/dist/lib/agent-generator.d.ts.map +1 -1
- package/dist/lib/agent-generator.js +64 -10
- package/dist/lib/agent-generator.js.map +1 -1
- package/dist/lib/agent-session.d.ts +104 -0
- package/dist/lib/agent-session.d.ts.map +1 -0
- package/dist/lib/agent-session.js +635 -0
- package/dist/lib/agent-session.js.map +1 -0
- package/dist/lib/eval-snapshot.d.ts +47 -0
- package/dist/lib/eval-snapshot.d.ts.map +1 -0
- package/dist/lib/eval-snapshot.js +315 -0
- package/dist/lib/eval-snapshot.js.map +1 -0
- package/dist/lib/eval-store.d.ts +5 -0
- package/dist/lib/eval-store.d.ts.map +1 -1
- package/dist/lib/eval-store.js +33 -3
- package/dist/lib/eval-store.js.map +1 -1
- package/dist/lib/findings-engine.d.ts +51 -0
- package/dist/lib/findings-engine.d.ts.map +1 -0
- package/dist/lib/findings-engine.js +338 -0
- package/dist/lib/findings-engine.js.map +1 -0
- package/dist/lib/flow-engine.d.ts +8 -0
- package/dist/lib/flow-engine.d.ts.map +1 -1
- package/dist/lib/flow-engine.js +84 -2
- package/dist/lib/flow-engine.js.map +1 -1
- package/dist/lib/hub-client.d.ts +1 -0
- package/dist/lib/hub-client.d.ts.map +1 -1
- package/dist/lib/hub-client.js +33 -6
- package/dist/lib/hub-client.js.map +1 -1
- package/dist/lib/ide-panes.d.ts +58 -0
- package/dist/lib/ide-panes.d.ts.map +1 -0
- package/dist/lib/ide-panes.js +508 -0
- package/dist/lib/ide-panes.js.map +1 -0
- package/dist/lib/memory-db.js +4 -4
- package/dist/lib/memory-db.js.map +1 -1
- package/dist/lib/memory-indexer.d.ts.map +1 -1
- package/dist/lib/memory-indexer.js +3 -0
- package/dist/lib/memory-indexer.js.map +1 -1
- package/dist/lib/memory-search.d.ts +148 -4
- package/dist/lib/memory-search.d.ts.map +1 -1
- package/dist/lib/memory-search.js +496 -58
- package/dist/lib/memory-search.js.map +1 -1
- package/dist/lib/meta-orchestrator.d.ts +104 -0
- package/dist/lib/meta-orchestrator.d.ts.map +1 -0
- package/dist/lib/meta-orchestrator.js +373 -0
- package/dist/lib/meta-orchestrator.js.map +1 -0
- package/dist/lib/peer-agent-generator.d.ts.map +1 -1
- package/dist/lib/peer-agent-generator.js +43 -19
- package/dist/lib/peer-agent-generator.js.map +1 -1
- package/dist/lib/pi-sky/bridge.d.ts +55 -0
- package/dist/lib/pi-sky/bridge.d.ts.map +1 -0
- package/dist/lib/pi-sky/bridge.js +264 -0
- package/dist/lib/pi-sky/bridge.js.map +1 -0
- package/dist/lib/pi-sky/cost-monitor.d.ts +21 -0
- package/dist/lib/pi-sky/cost-monitor.d.ts.map +1 -0
- package/dist/lib/pi-sky/cost-monitor.js +126 -0
- package/dist/lib/pi-sky/cost-monitor.js.map +1 -0
- package/dist/lib/pi-sky/eval-sweep.d.ts +27 -0
- package/dist/lib/pi-sky/eval-sweep.d.ts.map +1 -0
- package/dist/lib/pi-sky/eval-sweep.js +141 -0
- package/dist/lib/pi-sky/eval-sweep.js.map +1 -0
- package/dist/lib/pi-sky/event-router.d.ts +32 -0
- package/dist/lib/pi-sky/event-router.d.ts.map +1 -0
- package/dist/lib/pi-sky/event-router.js +176 -0
- package/dist/lib/pi-sky/event-router.js.map +1 -0
- package/dist/lib/pi-sky/experiment.d.ts +9 -0
- package/dist/lib/pi-sky/experiment.d.ts.map +1 -0
- package/dist/lib/pi-sky/experiment.js +83 -0
- package/dist/lib/pi-sky/experiment.js.map +1 -0
- package/dist/lib/pi-sky/index.d.ts +16 -0
- package/dist/lib/pi-sky/index.d.ts.map +1 -0
- package/dist/lib/pi-sky/index.js +16 -0
- package/dist/lib/pi-sky/index.js.map +1 -0
- package/dist/lib/pi-sky/stratus-gate.d.ts +28 -0
- package/dist/lib/pi-sky/stratus-gate.d.ts.map +1 -0
- package/dist/lib/pi-sky/stratus-gate.js +61 -0
- package/dist/lib/pi-sky/stratus-gate.js.map +1 -0
- package/dist/lib/pi-sky/swarm.d.ts +28 -0
- package/dist/lib/pi-sky/swarm.d.ts.map +1 -0
- package/dist/lib/pi-sky/swarm.js +208 -0
- package/dist/lib/pi-sky/swarm.js.map +1 -0
- package/dist/lib/pi-sky/types.d.ts +139 -0
- package/dist/lib/pi-sky/types.d.ts.map +1 -0
- package/dist/lib/pi-sky/types.js +2 -0
- package/dist/lib/pi-sky/types.js.map +1 -0
- package/dist/lib/pi-sky/voice-bridge.d.ts +20 -0
- package/dist/lib/pi-sky/voice-bridge.d.ts.map +1 -0
- package/dist/lib/pi-sky/voice-bridge.js +91 -0
- package/dist/lib/pi-sky/voice-bridge.js.map +1 -0
- package/dist/lib/policy-head.d.ts +40 -0
- package/dist/lib/policy-head.d.ts.map +1 -0
- package/dist/lib/policy-head.js +234 -0
- package/dist/lib/policy-head.js.map +1 -0
- package/dist/lib/predictor.d.ts +10 -0
- package/dist/lib/predictor.d.ts.map +1 -1
- package/dist/lib/predictor.js +46 -7
- package/dist/lib/predictor.js.map +1 -1
- package/dist/lib/replay-buffer.d.ts +93 -0
- package/dist/lib/replay-buffer.d.ts.map +1 -0
- package/dist/lib/replay-buffer.js +302 -0
- package/dist/lib/replay-buffer.js.map +1 -0
- package/dist/lib/sentinel-rl.d.ts +97 -0
- package/dist/lib/sentinel-rl.d.ts.map +1 -0
- package/dist/lib/sentinel-rl.js +430 -0
- package/dist/lib/sentinel-rl.js.map +1 -0
- package/dist/lib/session-lock.d.ts +61 -0
- package/dist/lib/session-lock.d.ts.map +1 -0
- package/dist/lib/session-lock.js +438 -0
- package/dist/lib/session-lock.js.map +1 -0
- package/dist/lib/setup/agent-generator.d.ts +18 -0
- package/dist/lib/setup/agent-generator.d.ts.map +1 -0
- package/dist/lib/setup/agent-generator.js +114 -0
- package/dist/lib/setup/agent-generator.js.map +1 -0
- package/dist/lib/setup/context-analyzer.d.ts +16 -0
- package/dist/lib/setup/context-analyzer.d.ts.map +1 -0
- package/dist/lib/setup/context-analyzer.js +112 -0
- package/dist/lib/setup/context-analyzer.js.map +1 -0
- package/dist/lib/setup/doc-auditor.d.ts +54 -0
- package/dist/lib/setup/doc-auditor.d.ts.map +1 -0
- package/dist/lib/setup/doc-auditor.js +629 -0
- package/dist/lib/setup/doc-auditor.js.map +1 -0
- package/dist/lib/setup/domain-generator.d.ts +7 -0
- package/dist/lib/setup/domain-generator.d.ts.map +1 -0
- package/dist/lib/setup/domain-generator.js +58 -0
- package/dist/lib/setup/domain-generator.js.map +1 -0
- package/dist/lib/setup/smart-eval-generator.d.ts +38 -0
- package/dist/lib/setup/smart-eval-generator.d.ts.map +1 -0
- package/dist/lib/setup/smart-eval-generator.js +378 -0
- package/dist/lib/setup/smart-eval-generator.js.map +1 -0
- package/dist/lib/setup/smart-recommender.d.ts +63 -0
- package/dist/lib/setup/smart-recommender.d.ts.map +1 -0
- package/dist/lib/setup/smart-recommender.js +329 -0
- package/dist/lib/setup/smart-recommender.js.map +1 -0
- package/dist/lib/setup/spec-generator.d.ts +63 -0
- package/dist/lib/setup/spec-generator.d.ts.map +1 -0
- package/dist/lib/setup/spec-generator.js +310 -0
- package/dist/lib/setup/spec-generator.js.map +1 -0
- package/dist/lib/setup/violation-agent-generator.d.ts +32 -0
- package/dist/lib/setup/violation-agent-generator.d.ts.map +1 -0
- package/dist/lib/setup/violation-agent-generator.js +255 -0
- package/dist/lib/setup/violation-agent-generator.js.map +1 -0
- package/dist/lib/stratus-client.d.ts +1 -0
- package/dist/lib/stratus-client.d.ts.map +1 -1
- package/dist/lib/stratus-client.js +24 -2
- package/dist/lib/stratus-client.js.map +1 -1
- package/dist/lib/telemetry-agent-v2.d.ts +128 -0
- package/dist/lib/telemetry-agent-v2.d.ts.map +1 -0
- package/dist/lib/telemetry-agent-v2.js +1042 -0
- package/dist/lib/telemetry-agent-v2.js.map +1 -0
- package/dist/lib/telemetry-agent.d.ts.map +1 -1
- package/dist/lib/telemetry-agent.js +27 -6
- package/dist/lib/telemetry-agent.js.map +1 -1
- package/dist/lib/telemetry-digest.d.ts.map +1 -1
- package/dist/lib/telemetry-digest.js +27 -5
- package/dist/lib/telemetry-digest.js.map +1 -1
- package/dist/lib/telemetry.d.ts.map +1 -1
- package/dist/lib/telemetry.js +29 -4
- package/dist/lib/telemetry.js.map +1 -1
- package/dist/lib/text-preprocessing.d.ts +83 -0
- package/dist/lib/text-preprocessing.d.ts.map +1 -0
- package/dist/lib/text-preprocessing.js +261 -0
- package/dist/lib/text-preprocessing.js.map +1 -0
- package/dist/lib/training-buffer.d.ts +86 -0
- package/dist/lib/training-buffer.d.ts.map +1 -0
- package/dist/lib/training-buffer.js +139 -0
- package/dist/lib/training-buffer.js.map +1 -0
- package/dist/lib/tuple-miner.d.ts +30 -0
- package/dist/lib/tuple-miner.d.ts.map +1 -0
- package/dist/lib/tuple-miner.js +427 -0
- package/dist/lib/tuple-miner.js.map +1 -0
- package/dist/lib/vm-backend.d.ts +72 -0
- package/dist/lib/vm-backend.d.ts.map +1 -0
- package/dist/lib/vm-backend.js +175 -0
- package/dist/lib/vm-backend.js.map +1 -0
- package/dist/lib/workspace/backend.d.ts +53 -0
- package/dist/lib/workspace/backend.d.ts.map +1 -0
- package/dist/lib/workspace/backend.js +37 -0
- package/dist/lib/workspace/backend.js.map +1 -0
- package/dist/lib/workspace/cmux-adapter.d.ts +46 -0
- package/dist/lib/workspace/cmux-adapter.d.ts.map +1 -0
- package/dist/lib/workspace/cmux-adapter.js +261 -0
- package/dist/lib/workspace/cmux-adapter.js.map +1 -0
- package/dist/lib/workspace/data-pipeline.d.ts +35 -0
- package/dist/lib/workspace/data-pipeline.d.ts.map +1 -0
- package/dist/lib/workspace/data-pipeline.js +463 -0
- package/dist/lib/workspace/data-pipeline.js.map +1 -0
- package/dist/lib/workspace/engine.d.ts +64 -0
- package/dist/lib/workspace/engine.d.ts.map +1 -0
- package/dist/lib/workspace/engine.js +397 -0
- package/dist/lib/workspace/engine.js.map +1 -0
- package/dist/lib/workspace/notifications.d.ts +14 -0
- package/dist/lib/workspace/notifications.d.ts.map +1 -0
- package/dist/lib/workspace/notifications.js +41 -0
- package/dist/lib/workspace/notifications.js.map +1 -0
- package/dist/lib/workspace/surface-registry.d.ts +49 -0
- package/dist/lib/workspace/surface-registry.d.ts.map +1 -0
- package/dist/lib/workspace/surface-registry.js +217 -0
- package/dist/lib/workspace/surface-registry.js.map +1 -0
- package/dist/lib/workspace/surface-type.d.ts +153 -0
- package/dist/lib/workspace/surface-type.d.ts.map +1 -0
- package/dist/lib/workspace/surface-type.js +9 -0
- package/dist/lib/workspace/surface-type.js.map +1 -0
- package/dist/lib/workspace/surfaces/agent-overview.d.ts +16 -0
- package/dist/lib/workspace/surfaces/agent-overview.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/agent-overview.js +116 -0
- package/dist/lib/workspace/surfaces/agent-overview.js.map +1 -0
- package/dist/lib/workspace/surfaces/agent.d.ts +16 -0
- package/dist/lib/workspace/surfaces/agent.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/agent.js +112 -0
- package/dist/lib/workspace/surfaces/agent.js.map +1 -0
- package/dist/lib/workspace/surfaces/claude.d.ts +15 -0
- package/dist/lib/workspace/surfaces/claude.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/claude.js +23 -0
- package/dist/lib/workspace/surfaces/claude.js.map +1 -0
- package/dist/lib/workspace/surfaces/dashboard.d.ts +21 -0
- package/dist/lib/workspace/surfaces/dashboard.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/dashboard.js +32 -0
- package/dist/lib/workspace/surfaces/dashboard.js.map +1 -0
- package/dist/lib/workspace/surfaces/eval.d.ts +15 -0
- package/dist/lib/workspace/surfaces/eval.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/eval.js +42 -0
- package/dist/lib/workspace/surfaces/eval.js.map +1 -0
- package/dist/lib/workspace/surfaces/event-stream.d.ts +16 -0
- package/dist/lib/workspace/surfaces/event-stream.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/event-stream.js +40 -0
- package/dist/lib/workspace/surfaces/event-stream.js.map +1 -0
- package/dist/lib/workspace/surfaces/flow.d.ts +16 -0
- package/dist/lib/workspace/surfaces/flow.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/flow.js +49 -0
- package/dist/lib/workspace/surfaces/flow.js.map +1 -0
- package/dist/lib/workspace/surfaces/index.d.ts +16 -0
- package/dist/lib/workspace/surfaces/index.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/index.js +16 -0
- package/dist/lib/workspace/surfaces/index.js.map +1 -0
- package/dist/lib/workspace/surfaces/portfolio.d.ts +16 -0
- package/dist/lib/workspace/surfaces/portfolio.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/portfolio.js +102 -0
- package/dist/lib/workspace/surfaces/portfolio.js.map +1 -0
- package/dist/lib/workspace/surfaces/service.d.ts +16 -0
- package/dist/lib/workspace/surfaces/service.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/service.js +45 -0
- package/dist/lib/workspace/surfaces/service.js.map +1 -0
- package/dist/lib/workspace/surfaces/shell.d.ts +15 -0
- package/dist/lib/workspace/surfaces/shell.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/shell.js +19 -0
- package/dist/lib/workspace/surfaces/shell.js.map +1 -0
- package/dist/lib/workspace/surfaces/telemetry.d.ts +16 -0
- package/dist/lib/workspace/surfaces/telemetry.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/telemetry.js +48 -0
- package/dist/lib/workspace/surfaces/telemetry.js.map +1 -0
- package/dist/lib/workspace/surfaces/topology.d.ts +15 -0
- package/dist/lib/workspace/surfaces/topology.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/topology.js +19 -0
- package/dist/lib/workspace/surfaces/topology.js.map +1 -0
- package/dist/lib/workspace/surfaces/training.d.ts +16 -0
- package/dist/lib/workspace/surfaces/training.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/training.js +22 -0
- package/dist/lib/workspace/surfaces/training.js.map +1 -0
- package/dist/lib/workspace/tmux-adapter.d.ts +27 -0
- package/dist/lib/workspace/tmux-adapter.d.ts.map +1 -0
- package/dist/lib/workspace/tmux-adapter.js +106 -0
- package/dist/lib/workspace/tmux-adapter.js.map +1 -0
- package/dist/mcp/context-hub-mcp.js +7 -24
- package/dist/mcp/context-hub-mcp.js.map +1 -1
- package/dist/types/flows.d.ts +2 -0
- package/dist/types/flows.d.ts.map +1 -1
- package/dist/types/ide.d.ts +49 -0
- package/dist/types/ide.d.ts.map +1 -0
- package/dist/types/ide.js +5 -0
- package/dist/types/ide.js.map +1 -0
- package/dist/types/platform-digest.d.ts +228 -0
- package/dist/types/platform-digest.d.ts.map +1 -0
- package/dist/types/platform-digest.js +5 -0
- package/dist/types/platform-digest.js.map +1 -0
- package/dist/types/telemetry-digest.d.ts +2 -0
- package/dist/types/telemetry-digest.d.ts.map +1 -1
- package/dist/utils/ensure-project.d.ts +1 -0
- package/dist/utils/ensure-project.d.ts.map +1 -1
- package/dist/utils/ensure-project.js +19 -7
- package/dist/utils/ensure-project.js.map +1 -1
- package/dist/utils/jfl-config.d.ts +1 -0
- package/dist/utils/jfl-config.d.ts.map +1 -1
- package/dist/utils/jfl-config.js +19 -1
- package/dist/utils/jfl-config.js.map +1 -1
- package/dist/utils/jfl-paths.d.ts +5 -0
- package/dist/utils/jfl-paths.d.ts.map +1 -1
- package/dist/utils/jfl-paths.js +25 -3
- package/dist/utils/jfl-paths.js.map +1 -1
- package/package.json +3 -2
- package/packages/pi/AGENTS.md +112 -0
- package/packages/pi/extensions/agent-grid.ts +191 -0
- package/packages/pi/extensions/agent-names.ts +178 -0
- package/packages/pi/extensions/autoresearch.ts +427 -0
- package/packages/pi/extensions/bookmarks.ts +85 -0
- package/packages/pi/extensions/context.ts +184 -0
- package/packages/pi/extensions/crm-tool.ts +61 -0
- package/packages/pi/extensions/eval-tool.ts +224 -0
- package/packages/pi/extensions/eval.ts +60 -0
- package/packages/pi/extensions/footer.ts +239 -0
- package/packages/pi/extensions/hub-resolver.ts +63 -0
- package/packages/pi/extensions/hud-tool.ts +145 -0
- package/packages/pi/extensions/index.ts +405 -0
- package/packages/pi/extensions/journal.ts +224 -0
- package/packages/pi/extensions/map-bridge.ts +178 -0
- package/packages/pi/extensions/memory-tool.ts +73 -0
- package/packages/pi/extensions/notifications.ts +73 -0
- package/packages/pi/extensions/peter-parker.ts +202 -0
- package/packages/pi/extensions/policy-head-tool.ts +276 -0
- package/packages/pi/extensions/portfolio-bridge.ts +90 -0
- package/packages/pi/extensions/session.ts +142 -0
- package/packages/pi/extensions/shortcuts.ts +259 -0
- package/packages/pi/extensions/stratus-bridge.ts +115 -0
- package/packages/pi/extensions/synopsis-tool.ts +83 -0
- package/packages/pi/extensions/tool-renderers.ts +353 -0
- package/packages/pi/extensions/training-buffer-tool.ts +368 -0
- package/packages/pi/extensions/types.ts +163 -0
- package/packages/pi/package-lock.json +346 -0
- package/packages/pi/package.json +44 -0
- package/packages/pi/skills/agent-browser/SKILL.md +116 -0
- package/packages/pi/skills/brand-architect/SKILL.md +240 -0
- package/packages/pi/skills/brand-architect/config.yaml +137 -0
- package/packages/pi/skills/campaign-hud/config.yaml +112 -0
- package/packages/pi/skills/content-creator/SKILL.md +294 -0
- package/packages/pi/skills/context/SKILL.md +65 -0
- package/packages/pi/skills/debug/MULTI_AGENT.md +360 -0
- package/packages/pi/skills/debug/SKILL.md +554 -0
- package/packages/pi/skills/end/SKILL.md +1782 -0
- package/packages/pi/skills/eval/SKILL.md +75 -0
- package/packages/pi/skills/fly-deploy/SKILL.md +676 -0
- package/packages/pi/skills/founder-video/SKILL.md +467 -0
- package/packages/pi/skills/hud/SKILL.md +160 -0
- package/packages/pi/skills/orchestrate/SKILL.md +74 -0
- package/packages/pi/skills/pi-agents/SKILL.md +78 -0
- package/packages/pi/skills/react-best-practices/AGENTS.md +2249 -0
- package/packages/pi/skills/react-best-practices/README.md +123 -0
- package/packages/pi/skills/react-best-practices/SKILL.md +125 -0
- package/packages/pi/skills/react-best-practices/metadata.json +15 -0
- package/packages/pi/skills/react-best-practices/rules/_sections.md +46 -0
- package/packages/pi/skills/react-best-practices/rules/_template.md +28 -0
- package/packages/pi/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/packages/pi/skills/react-best-practices/rules/advanced-use-latest.md +49 -0
- package/packages/pi/skills/react-best-practices/rules/async-api-routes.md +38 -0
- package/packages/pi/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/packages/pi/skills/react-best-practices/rules/async-dependencies.md +36 -0
- package/packages/pi/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/packages/pi/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/packages/pi/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/packages/pi/skills/react-best-practices/rules/bundle-conditional.md +31 -0
- package/packages/pi/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/packages/pi/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/packages/pi/skills/react-best-practices/rules/bundle-preload.md +50 -0
- package/packages/pi/skills/react-best-practices/rules/client-event-listeners.md +74 -0
- package/packages/pi/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/packages/pi/skills/react-best-practices/rules/js-batch-dom-css.md +82 -0
- package/packages/pi/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/packages/pi/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/packages/pi/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/packages/pi/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/packages/pi/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/packages/pi/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/packages/pi/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/packages/pi/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/packages/pi/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/packages/pi/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/packages/pi/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/packages/pi/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/packages/pi/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/packages/pi/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/packages/pi/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/packages/pi/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/packages/pi/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/packages/pi/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/packages/pi/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/packages/pi/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/packages/pi/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/packages/pi/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/packages/pi/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/packages/pi/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/packages/pi/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/packages/pi/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/packages/pi/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/packages/pi/skills/react-best-practices/rules/server-cache-react.md +26 -0
- package/packages/pi/skills/react-best-practices/rules/server-parallel-fetching.md +79 -0
- package/packages/pi/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/packages/pi/skills/remotion-best-practices/SKILL.md +43 -0
- package/packages/pi/skills/remotion-best-practices/rules/3d.md +86 -0
- package/packages/pi/skills/remotion-best-practices/rules/animations.md +29 -0
- package/packages/pi/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
- package/packages/pi/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
- package/packages/pi/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
- package/packages/pi/skills/remotion-best-practices/rules/assets.md +78 -0
- package/packages/pi/skills/remotion-best-practices/rules/audio.md +172 -0
- package/packages/pi/skills/remotion-best-practices/rules/calculate-metadata.md +104 -0
- package/packages/pi/skills/remotion-best-practices/rules/can-decode.md +75 -0
- package/packages/pi/skills/remotion-best-practices/rules/charts.md +58 -0
- package/packages/pi/skills/remotion-best-practices/rules/compositions.md +146 -0
- package/packages/pi/skills/remotion-best-practices/rules/display-captions.md +126 -0
- package/packages/pi/skills/remotion-best-practices/rules/extract-frames.md +229 -0
- package/packages/pi/skills/remotion-best-practices/rules/fonts.md +152 -0
- package/packages/pi/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
- package/packages/pi/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
- package/packages/pi/skills/remotion-best-practices/rules/get-video-duration.md +58 -0
- package/packages/pi/skills/remotion-best-practices/rules/gifs.md +138 -0
- package/packages/pi/skills/remotion-best-practices/rules/images.md +130 -0
- package/packages/pi/skills/remotion-best-practices/rules/import-srt-captions.md +67 -0
- package/packages/pi/skills/remotion-best-practices/rules/lottie.md +68 -0
- package/packages/pi/skills/remotion-best-practices/rules/measuring-dom-nodes.md +35 -0
- package/packages/pi/skills/remotion-best-practices/rules/measuring-text.md +143 -0
- package/packages/pi/skills/remotion-best-practices/rules/sequencing.md +106 -0
- package/packages/pi/skills/remotion-best-practices/rules/tailwind.md +11 -0
- package/packages/pi/skills/remotion-best-practices/rules/text-animations.md +20 -0
- package/packages/pi/skills/remotion-best-practices/rules/timing.md +179 -0
- package/packages/pi/skills/remotion-best-practices/rules/transcribe-captions.md +19 -0
- package/packages/pi/skills/remotion-best-practices/rules/transitions.md +122 -0
- package/packages/pi/skills/remotion-best-practices/rules/trimming.md +53 -0
- package/packages/pi/skills/remotion-best-practices/rules/videos.md +171 -0
- package/packages/pi/skills/search/SKILL.md +220 -0
- package/packages/pi/skills/spec/SKILL.md +377 -0
- package/packages/pi/skills/startup/SKILL.md +315 -0
- package/packages/pi/skills/web-architect/SKILL.md +309 -0
- package/packages/pi/skills/x-algorithm/SKILL.md +305 -0
- package/packages/pi/teams/dev-team.yaml +63 -0
- package/packages/pi/teams/gtm-team.yaml +79 -0
- package/packages/pi/themes/jfl.theme.json +76 -0
- package/packages/pi/tsconfig.json +21 -0
- package/scripts/collect-tuples.sh +124 -0
- package/scripts/destroy-fleet.sh +37 -0
- package/scripts/jfl-ide.sh +48 -0
- package/scripts/session/session-cleanup.sh +4 -11
- package/scripts/session/session-init.sh +6 -0
- package/scripts/session/session-sync.sh +25 -0
- package/scripts/setup-branch-protection.sh +106 -0
- package/scripts/spawn-fleet.sh +144 -0
- package/scripts/train/requirements.txt +5 -0
- package/scripts/train/train-policy-head.py +477 -0
- package/scripts/train/v2/dataset.py +81 -0
- package/scripts/train/v2/domain.json +18 -0
- package/scripts/train/v2/eval.py +196 -0
- package/scripts/train/v2/generate_data.py +219 -0
- package/scripts/train/v2/infer.py +188 -0
- package/scripts/train/v2/model.py +112 -0
- package/scripts/train/v2/precompute.py +132 -0
- package/scripts/train/v2/train.py +302 -0
- package/scripts/train/v2/transform_buffer.py +227 -0
- package/scripts/train/v2/validate_data.py +115 -0
- package/scripts/train-policy-head.py +434 -0
- package/scripts/vm-swarm/README.md +301 -0
- package/scripts/vm-swarm/collect-tuples.sh +331 -0
- package/scripts/vm-swarm/create-base-template.sh +339 -0
- package/scripts/vm-swarm/kill-fleet.sh +204 -0
- package/scripts/vm-swarm/monitor-fleet.sh +346 -0
- package/scripts/vm-swarm/spawn-fleet.sh +304 -0
- package/template/.claude/settings.json +2 -15
- package/template/.github/workflows/jfl-eval.yml +6 -1
- package/template/.github/workflows/jfl-review.yml +4 -0
- package/template/scripts/session/session-cleanup.sh +2 -11
- package/template/scripts/session/session-end-hub.sh +72 -0
- package/template/scripts/session/session-end.sh +69 -6
- package/template/scripts/session/session-init.sh +55 -30
- package/template/scripts/session/session-lock.sh +464 -0
- package/template/scripts/session/session-start-hub.sh +105 -0
- package/template/templates/service-agent/workflows/jfl-eval.yml +19 -0
- package/dist/dashboard-static/assets/index-B6kRK9Rq.js +0 -116
- package/dist/dashboard-static/assets/index-BpdKJPLu.css +0 -1
|
@@ -25,6 +25,9 @@ import { FlowEngine } from "../lib/flow-engine.js";
|
|
|
25
25
|
import { WebSocketServer } from "ws";
|
|
26
26
|
import { telemetry } from "../lib/telemetry.js";
|
|
27
27
|
import { transformHookPayload } from "../lib/hook-transformer.js";
|
|
28
|
+
import { loadAllAgentConfigs } from "../lib/agent-config.js";
|
|
29
|
+
import { TrainingBuffer } from "../lib/training-buffer.js";
|
|
30
|
+
import { FindingsEngine } from "../lib/findings-engine.js";
|
|
28
31
|
const PID_FILE = ".jfl/context-hub.pid";
|
|
29
32
|
const LOG_FILE = ".jfl/logs/context-hub.log";
|
|
30
33
|
const TOKEN_FILE = ".jfl/context-hub.token";
|
|
@@ -152,7 +155,11 @@ function readKnowledgeDocs(projectRoot) {
|
|
|
152
155
|
"NARRATIVE.md",
|
|
153
156
|
"THESIS.md",
|
|
154
157
|
"BRAND_DECISIONS.md",
|
|
155
|
-
"TASKS.md"
|
|
158
|
+
"TASKS.md",
|
|
159
|
+
"ARCHITECTURE.md",
|
|
160
|
+
"SERVICE_SPEC.md",
|
|
161
|
+
"DEPLOYMENT.md",
|
|
162
|
+
"RUNBOOK.md",
|
|
156
163
|
];
|
|
157
164
|
for (const filename of priorityFiles) {
|
|
158
165
|
const filePath = path.join(knowledgeDir, filename);
|
|
@@ -163,7 +170,7 @@ function readKnowledgeDocs(projectRoot) {
|
|
|
163
170
|
source: "knowledge",
|
|
164
171
|
type: "doc",
|
|
165
172
|
title,
|
|
166
|
-
content: content.slice(0, 2000),
|
|
173
|
+
content: content.slice(0, 2000),
|
|
167
174
|
path: filePath
|
|
168
175
|
});
|
|
169
176
|
}
|
|
@@ -409,7 +416,7 @@ function getUnifiedContext(projectRoot, query, taskType) {
|
|
|
409
416
|
journal: journalItems.length > 0,
|
|
410
417
|
knowledge: knowledgeItems.length > 0,
|
|
411
418
|
code: codeItems.length > 0,
|
|
412
|
-
memory:
|
|
419
|
+
memory: fs.existsSync(path.join(projectRoot, ".jfl", "memory.db")),
|
|
413
420
|
},
|
|
414
421
|
query,
|
|
415
422
|
taskType
|
|
@@ -540,6 +547,20 @@ function createServer(projectRoot, port, eventBus, flowEngine) {
|
|
|
540
547
|
res.end(JSON.stringify({ status: "ok", port }));
|
|
541
548
|
return;
|
|
542
549
|
}
|
|
550
|
+
// Setup report — serves .jfl/setup-report.json for dashboard consumption
|
|
551
|
+
if (url.pathname === "/api/setup-report" && req.method === "GET") {
|
|
552
|
+
const reportPath = path.join(projectRoot, ".jfl", "setup-report.json");
|
|
553
|
+
if (fs.existsSync(reportPath)) {
|
|
554
|
+
const report = fs.readFileSync(reportPath, "utf-8");
|
|
555
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
556
|
+
res.end(report);
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
560
|
+
res.end(JSON.stringify({ error: "No setup report. Run: jfl setup" }));
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
543
564
|
// Dashboard - served without API auth (has its own token flow in JS)
|
|
544
565
|
if (url.pathname.startsWith("/dashboard")) {
|
|
545
566
|
import("../dashboard/index.js").then(({ handleDashboardRoutes }) => {
|
|
@@ -766,6 +787,107 @@ function createServer(projectRoot, port, eventBus, flowEngine) {
|
|
|
766
787
|
}
|
|
767
788
|
return;
|
|
768
789
|
}
|
|
790
|
+
// RAG Chat — search context + memory, then stream LLM response
|
|
791
|
+
if (url.pathname === "/api/chat" && req.method === "POST") {
|
|
792
|
+
let body = "";
|
|
793
|
+
req.on("data", chunk => body += chunk);
|
|
794
|
+
req.on("end", async () => {
|
|
795
|
+
try {
|
|
796
|
+
const { message, history = [] } = JSON.parse(body || "{}");
|
|
797
|
+
if (!message) {
|
|
798
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
799
|
+
res.end(JSON.stringify({ error: "message required" }));
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const [memRaw, ctxResult] = await Promise.allSettled([
|
|
803
|
+
searchMemories(message, { maxItems: 5 }),
|
|
804
|
+
getPortfolioContext(projectRoot, message, undefined, 5),
|
|
805
|
+
]);
|
|
806
|
+
const memResults = memRaw.status === "fulfilled"
|
|
807
|
+
? memRaw.value.map(r => ({ title: r.memory.title, content: r.memory.content, type: r.memory.type, relevance: r.relevance }))
|
|
808
|
+
: [];
|
|
809
|
+
const ctxResults = ctxResult.status === "fulfilled"
|
|
810
|
+
? (ctxResult.value.items || []).slice(0, 5).map((i) => ({ title: i.title || i.path, content: i.content?.slice(0, 500), type: i.type }))
|
|
811
|
+
: [];
|
|
812
|
+
const sources = [...memResults, ...ctxResults];
|
|
813
|
+
let contextBlock = "";
|
|
814
|
+
if (sources.length > 0) {
|
|
815
|
+
contextBlock = "Here is relevant context from the project:\n\n" +
|
|
816
|
+
sources.map((s, i) => `[${i + 1}] ${s.title} (${s.type})\n${s.content}`).join("\n\n") +
|
|
817
|
+
"\n\n---\n\n";
|
|
818
|
+
}
|
|
819
|
+
// Load .env from project root for API keys
|
|
820
|
+
const envPath = path.join(projectRoot, ".env");
|
|
821
|
+
if (fs.existsSync(envPath)) {
|
|
822
|
+
for (const line of fs.readFileSync(envPath, "utf-8").split("\n")) {
|
|
823
|
+
const match = line.match(/^([A-Z_]+)=(.+)$/);
|
|
824
|
+
if (match && !process.env[match[1]])
|
|
825
|
+
process.env[match[1]] = match[2].trim();
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const apiKey = process.env.STRATUS_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY;
|
|
829
|
+
if (!apiKey) {
|
|
830
|
+
res.writeHead(200, {
|
|
831
|
+
"Content-Type": "text/event-stream",
|
|
832
|
+
"Cache-Control": "no-cache",
|
|
833
|
+
"Connection": "keep-alive",
|
|
834
|
+
});
|
|
835
|
+
const fallback = sources.length > 0
|
|
836
|
+
? "I found relevant context but no LLM API key is configured. Set STRATUS_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY in your .env.\n\n**Sources found:**\n" +
|
|
837
|
+
sources.map((s, i) => `${i + 1}. **${s.title}** \`${s.type}\`\n ${s.content?.slice(0, 150)}`).join("\n")
|
|
838
|
+
: "No API key configured and no relevant context found.";
|
|
839
|
+
res.write(`data: ${JSON.stringify({ sources })}\n\n`);
|
|
840
|
+
res.write(`data: ${JSON.stringify({ delta: fallback })}\n\n`);
|
|
841
|
+
res.write("data: [DONE]\n\n");
|
|
842
|
+
res.end();
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
const useStratus = apiKey === process.env.STRATUS_API_KEY;
|
|
846
|
+
const baseURL = useStratus ? "https://api.stratus.run/v1" : undefined;
|
|
847
|
+
const model = useStratus ? "stratus-x1ac-huge-claude-sonnet-4-6" : "gpt-4o-mini";
|
|
848
|
+
const OpenAI = (await import("openai")).default;
|
|
849
|
+
const client = new OpenAI({ apiKey, baseURL });
|
|
850
|
+
const messages = [
|
|
851
|
+
{
|
|
852
|
+
role: "system",
|
|
853
|
+
content: `You are JFL Assistant, helping the user understand their project context. Answer questions based on the provided context from journal entries, knowledge docs, and code files. Be concise and direct. If the context doesn't contain enough information, say so. Reference specific sources when possible.\n\n${contextBlock}`
|
|
854
|
+
},
|
|
855
|
+
...history.slice(-6).map((m) => ({ role: m.role, content: m.content })),
|
|
856
|
+
{ role: "user", content: message },
|
|
857
|
+
];
|
|
858
|
+
res.writeHead(200, {
|
|
859
|
+
"Content-Type": "text/event-stream",
|
|
860
|
+
"Cache-Control": "no-cache",
|
|
861
|
+
"Connection": "keep-alive",
|
|
862
|
+
});
|
|
863
|
+
res.write(`data: ${JSON.stringify({ sources })}\n\n`);
|
|
864
|
+
try {
|
|
865
|
+
const stream = await client.chat.completions.create({
|
|
866
|
+
model,
|
|
867
|
+
messages,
|
|
868
|
+
stream: true,
|
|
869
|
+
max_tokens: 1024,
|
|
870
|
+
});
|
|
871
|
+
for await (const chunk of stream) {
|
|
872
|
+
const delta = chunk.choices?.[0]?.delta?.content;
|
|
873
|
+
if (delta) {
|
|
874
|
+
res.write(`data: ${JSON.stringify({ delta })}\n\n`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
catch (llmErr) {
|
|
879
|
+
res.write(`data: ${JSON.stringify({ delta: `\n\nLLM error: ${llmErr.message}` })}\n\n`);
|
|
880
|
+
}
|
|
881
|
+
res.write("data: [DONE]\n\n");
|
|
882
|
+
res.end();
|
|
883
|
+
}
|
|
884
|
+
catch (err) {
|
|
885
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
886
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
769
891
|
// Memory search
|
|
770
892
|
if (url.pathname === "/api/memory/search" && req.method === "POST") {
|
|
771
893
|
let body = "";
|
|
@@ -778,7 +900,17 @@ function createServer(projectRoot, port, eventBus, flowEngine) {
|
|
|
778
900
|
res.end(JSON.stringify({ error: "query required" }));
|
|
779
901
|
return;
|
|
780
902
|
}
|
|
781
|
-
const
|
|
903
|
+
const raw = await searchMemories(query, { type, maxItems, since });
|
|
904
|
+
const results = raw.map(r => ({
|
|
905
|
+
title: r.memory.title,
|
|
906
|
+
content: r.memory.content,
|
|
907
|
+
summary: r.memory.summary,
|
|
908
|
+
type: r.memory.type,
|
|
909
|
+
source: r.memory.source,
|
|
910
|
+
ts: r.memory.created_at,
|
|
911
|
+
score: r.score,
|
|
912
|
+
relevance: r.relevance,
|
|
913
|
+
}));
|
|
782
914
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
783
915
|
res.end(JSON.stringify({ results }));
|
|
784
916
|
}
|
|
@@ -851,6 +983,23 @@ function createServer(projectRoot, port, eventBus, flowEngine) {
|
|
|
851
983
|
}
|
|
852
984
|
return;
|
|
853
985
|
}
|
|
986
|
+
// Eval entries (all raw entries for cycle display)
|
|
987
|
+
if (url.pathname === "/api/eval/entries" && req.method === "GET") {
|
|
988
|
+
try {
|
|
989
|
+
const { readEvals } = await import("../lib/eval-store.js");
|
|
990
|
+
const limit = parseInt(url.searchParams.get("limit") || "100", 10);
|
|
991
|
+
const entries = readEvals(projectRoot)
|
|
992
|
+
.sort((a, b) => b.ts.localeCompare(a.ts))
|
|
993
|
+
.slice(0, limit);
|
|
994
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
995
|
+
res.end(JSON.stringify({ entries }));
|
|
996
|
+
}
|
|
997
|
+
catch (err) {
|
|
998
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
999
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1000
|
+
}
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
854
1003
|
// Eval leaderboard
|
|
855
1004
|
if (url.pathname === "/api/eval/leaderboard" && req.method === "GET") {
|
|
856
1005
|
try {
|
|
@@ -1076,6 +1225,116 @@ function createServer(projectRoot, port, eventBus, flowEngine) {
|
|
|
1076
1225
|
}
|
|
1077
1226
|
return;
|
|
1078
1227
|
}
|
|
1228
|
+
// ── Autoresearch / Experiments API ──────────────────────────────
|
|
1229
|
+
// Agent configs
|
|
1230
|
+
if (url.pathname === "/api/v1/agents" && req.method === "GET") {
|
|
1231
|
+
try {
|
|
1232
|
+
const { listAgentConfigs, loadAgentConfig } = await import("../lib/agent-config.js");
|
|
1233
|
+
const names = listAgentConfigs(projectRoot);
|
|
1234
|
+
const agents = names.map(name => {
|
|
1235
|
+
try {
|
|
1236
|
+
return loadAgentConfig(projectRoot, name);
|
|
1237
|
+
}
|
|
1238
|
+
catch {
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
}).filter(Boolean);
|
|
1242
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1243
|
+
res.end(JSON.stringify({ agents }));
|
|
1244
|
+
}
|
|
1245
|
+
catch (err) {
|
|
1246
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1247
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1248
|
+
}
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
// Replay buffer (experiment history)
|
|
1252
|
+
if (url.pathname === "/api/v1/experiments" && req.method === "GET") {
|
|
1253
|
+
try {
|
|
1254
|
+
const bufferPath = path.join(projectRoot, ".jfl", "replay-buffer.jsonl");
|
|
1255
|
+
const trainingPath = path.join(projectRoot, ".jfl", "training-buffer.jsonl");
|
|
1256
|
+
const experiments = [];
|
|
1257
|
+
for (const p of [bufferPath, trainingPath]) {
|
|
1258
|
+
if (fs.existsSync(p)) {
|
|
1259
|
+
const lines = fs.readFileSync(p, "utf-8").trim().split("\n").filter(Boolean);
|
|
1260
|
+
for (const line of lines.slice(-100)) {
|
|
1261
|
+
try {
|
|
1262
|
+
experiments.push(JSON.parse(line));
|
|
1263
|
+
}
|
|
1264
|
+
catch { }
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const agent = url.searchParams.get("agent");
|
|
1269
|
+
const filtered = agent ? experiments.filter(e => e.agent === agent) : experiments;
|
|
1270
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1271
|
+
res.end(JSON.stringify({ experiments: filtered, total: filtered.length }));
|
|
1272
|
+
}
|
|
1273
|
+
catch (err) {
|
|
1274
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1275
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1276
|
+
}
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
// Session results
|
|
1280
|
+
if (url.pathname === "/api/v1/sessions" && req.method === "GET") {
|
|
1281
|
+
try {
|
|
1282
|
+
const sessionsDir = path.join(projectRoot, ".jfl", "sessions");
|
|
1283
|
+
const sessions = [];
|
|
1284
|
+
if (fs.existsSync(sessionsDir)) {
|
|
1285
|
+
for (const dir of fs.readdirSync(sessionsDir)) {
|
|
1286
|
+
const resultsPath = path.join(sessionsDir, dir, "results.tsv");
|
|
1287
|
+
if (fs.existsSync(resultsPath)) {
|
|
1288
|
+
const content = fs.readFileSync(resultsPath, "utf-8").trim();
|
|
1289
|
+
const lines = content.split("\n").slice(1); // skip header
|
|
1290
|
+
const rounds = lines.filter(l => l.trim()).map(line => {
|
|
1291
|
+
const [round, task, baseline, metric, delta, kept, duration, error, timestamp] = line.split("\t");
|
|
1292
|
+
return { round: +round, task, baseline: +baseline, metric: +metric, delta: +delta, kept: kept === "1", duration_ms: +duration, error, timestamp };
|
|
1293
|
+
});
|
|
1294
|
+
if (rounds.length > 0) {
|
|
1295
|
+
sessions.push({ id: dir, rounds, agent: dir.replace(/-[a-f0-9]{8}-\d+$/, "") });
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1301
|
+
res.end(JSON.stringify({ sessions }));
|
|
1302
|
+
}
|
|
1303
|
+
catch (err) {
|
|
1304
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1305
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1306
|
+
}
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
// Product context
|
|
1310
|
+
if (url.pathname === "/api/v1/product-context" && req.method === "GET") {
|
|
1311
|
+
const contextPath = path.join(projectRoot, ".jfl", "product-context.md");
|
|
1312
|
+
if (fs.existsSync(contextPath)) {
|
|
1313
|
+
const content = fs.readFileSync(contextPath, "utf-8");
|
|
1314
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1315
|
+
res.end(JSON.stringify({ context: content, updatedAt: fs.statSync(contextPath).mtime.toISOString() }));
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1319
|
+
res.end(JSON.stringify({ context: null, updatedAt: null }));
|
|
1320
|
+
}
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
// Product analysis (telemetry agent v2)
|
|
1324
|
+
if (url.pathname === "/api/v1/product-analysis" && req.method === "GET") {
|
|
1325
|
+
try {
|
|
1326
|
+
const { TelemetryAgentV2 } = await import("../lib/telemetry-agent-v2.js");
|
|
1327
|
+
const agent = new TelemetryAgentV2({ projectRoot });
|
|
1328
|
+
const analysis = await agent.analyzeProduct();
|
|
1329
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1330
|
+
res.end(JSON.stringify(analysis));
|
|
1331
|
+
}
|
|
1332
|
+
catch (err) {
|
|
1333
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1334
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1335
|
+
}
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1079
1338
|
// Flow definitions
|
|
1080
1339
|
if (url.pathname === "/api/flows" && req.method === "GET") {
|
|
1081
1340
|
if (!flowEngine) {
|
|
@@ -1160,6 +1419,348 @@ function createServer(projectRoot, port, eventBus, flowEngine) {
|
|
|
1160
1419
|
});
|
|
1161
1420
|
return;
|
|
1162
1421
|
}
|
|
1422
|
+
// Topology — returns nodes/edges for agent topology visualization
|
|
1423
|
+
if (url.pathname === "/api/v1/topology" && req.method === "GET") {
|
|
1424
|
+
try {
|
|
1425
|
+
const configPath = path.join(projectRoot, ".jfl", "config.json");
|
|
1426
|
+
let registeredServices = [];
|
|
1427
|
+
let workspaceType = "standalone";
|
|
1428
|
+
let workspaceName = "workspace";
|
|
1429
|
+
if (fs.existsSync(configPath)) {
|
|
1430
|
+
try {
|
|
1431
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1432
|
+
registeredServices = config.registered_services || [];
|
|
1433
|
+
workspaceType = config.type || "standalone";
|
|
1434
|
+
workspaceName = config.name || "workspace";
|
|
1435
|
+
}
|
|
1436
|
+
catch { }
|
|
1437
|
+
}
|
|
1438
|
+
// Read event counts from map-events.jsonl
|
|
1439
|
+
const mapEventsPath = path.join(projectRoot, ".jfl", "map-events.jsonl");
|
|
1440
|
+
const eventCounts = {};
|
|
1441
|
+
const edgeEventCounts = {}; // "source:target:eventType" -> count
|
|
1442
|
+
const recentWindow = 24 * 60 * 60 * 1000; // 24 hours
|
|
1443
|
+
if (fs.existsSync(mapEventsPath)) {
|
|
1444
|
+
try {
|
|
1445
|
+
const lines = fs.readFileSync(mapEventsPath, "utf-8").trim().split("\n");
|
|
1446
|
+
const now = Date.now();
|
|
1447
|
+
for (const line of lines.slice(-1000)) { // Last 1000 events
|
|
1448
|
+
if (!line)
|
|
1449
|
+
continue;
|
|
1450
|
+
try {
|
|
1451
|
+
const evt = JSON.parse(line);
|
|
1452
|
+
const ts = new Date(evt.ts).getTime();
|
|
1453
|
+
if (now - ts > recentWindow)
|
|
1454
|
+
continue;
|
|
1455
|
+
// Count events by source
|
|
1456
|
+
if (evt.source) {
|
|
1457
|
+
const srcId = evt.source.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1458
|
+
eventCounts[srcId] = (eventCounts[srcId] || 0) + 1;
|
|
1459
|
+
}
|
|
1460
|
+
// Count events by type prefix (for edge matching)
|
|
1461
|
+
if (evt.type) {
|
|
1462
|
+
const prefix = evt.type.split(":")[0];
|
|
1463
|
+
eventCounts[prefix] = (eventCounts[prefix] || 0) + 1;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
catch { }
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
catch { }
|
|
1470
|
+
}
|
|
1471
|
+
// System agents (always present in a JFL installation)
|
|
1472
|
+
// Only Peter Parker is a real system agent — others come from .jfl/agents/*.toml configs
|
|
1473
|
+
// Stratus, eval-engine, telemetry-agent are either external infra or RL agents now
|
|
1474
|
+
const systemAgents = [
|
|
1475
|
+
{ id: "peter-parker", label: "Peter Parker", type: "orchestrator", status: "running", produces: ["peter:task-completed", "peter:rollout-request", "peter:experiment-start"], consumes: ["telemetry:insight", "telemetry:metric-alert", "eval:scored", "sentinel:recommendation"] },
|
|
1476
|
+
];
|
|
1477
|
+
const nodes = [
|
|
1478
|
+
...systemAgents.map(a => ({
|
|
1479
|
+
id: a.id,
|
|
1480
|
+
label: a.label,
|
|
1481
|
+
type: a.type,
|
|
1482
|
+
status: a.status,
|
|
1483
|
+
eventCount: eventCounts[a.id] || eventCounts[a.id.split("-")[0]] || 0,
|
|
1484
|
+
produces: a.produces,
|
|
1485
|
+
consumes: a.consumes,
|
|
1486
|
+
})),
|
|
1487
|
+
];
|
|
1488
|
+
// Add registered services as nodes
|
|
1489
|
+
for (const service of registeredServices) {
|
|
1490
|
+
if (!nodes.find(n => n.id === service.name)) {
|
|
1491
|
+
const nodeType = service.type === "agent" ? "agent"
|
|
1492
|
+
: service.type === "eval" ? "eval"
|
|
1493
|
+
: service.type === "gtm" ? "gtm"
|
|
1494
|
+
: "service";
|
|
1495
|
+
nodes.push({
|
|
1496
|
+
id: service.name,
|
|
1497
|
+
label: service.name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
|
|
1498
|
+
type: nodeType,
|
|
1499
|
+
status: service.status === "running" ? "running" : service.status === "idle" ? "idle" : "stopped",
|
|
1500
|
+
eventCount: eventCounts[service.name] || 0,
|
|
1501
|
+
produces: service.context_scope?.produces,
|
|
1502
|
+
consumes: service.context_scope?.consumes,
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
// Add RL agent nodes from .jfl/agents/*.toml configs
|
|
1507
|
+
try {
|
|
1508
|
+
const rlAgentConfigs = loadAllAgentConfigs(projectRoot);
|
|
1509
|
+
const trainingBuffer = new TrainingBuffer(projectRoot);
|
|
1510
|
+
const trainingEntries = trainingBuffer.read();
|
|
1511
|
+
for (const config of rlAgentConfigs) {
|
|
1512
|
+
const nodeId = `rl-agent-${config.name}`;
|
|
1513
|
+
// Skip if node already exists (e.g., matches a registered service name)
|
|
1514
|
+
if (nodes.find(n => n.id === nodeId || n.id === config.name)) {
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
// Check for recent training data (within last 24h)
|
|
1518
|
+
const now = Date.now();
|
|
1519
|
+
const recentWindow = 24 * 60 * 60 * 1000;
|
|
1520
|
+
const recentEntries = trainingEntries.filter(e => {
|
|
1521
|
+
if (e.agent !== config.name)
|
|
1522
|
+
return false;
|
|
1523
|
+
const ts = new Date(e.ts).getTime();
|
|
1524
|
+
return now - ts < recentWindow;
|
|
1525
|
+
});
|
|
1526
|
+
const status = recentEntries.length > 0 ? "running" : "idle";
|
|
1527
|
+
// Convert name to proper label (e.g., "cli-speed" -> "Cli Speed")
|
|
1528
|
+
const label = config.name
|
|
1529
|
+
.split("-")
|
|
1530
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1531
|
+
.join(" ");
|
|
1532
|
+
nodes.push({
|
|
1533
|
+
id: nodeId,
|
|
1534
|
+
label,
|
|
1535
|
+
type: "agent",
|
|
1536
|
+
status,
|
|
1537
|
+
eventCount: recentEntries.length,
|
|
1538
|
+
produces: config.context_scope?.produces,
|
|
1539
|
+
consumes: config.context_scope?.consumes,
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
catch (err) {
|
|
1544
|
+
// Non-fatal: RL agents are optional
|
|
1545
|
+
}
|
|
1546
|
+
// For portfolio mode, fetch child GTM services and their registered services
|
|
1547
|
+
const childHubs = getChildHubs(projectRoot);
|
|
1548
|
+
const hierarchy = {
|
|
1549
|
+
gtms: [],
|
|
1550
|
+
};
|
|
1551
|
+
if (workspaceType === "portfolio" && childHubs.length > 0) {
|
|
1552
|
+
hierarchy.portfolio = workspaceName;
|
|
1553
|
+
for (const child of childHubs) {
|
|
1554
|
+
const gtmServices = [];
|
|
1555
|
+
// Try to read child's config for its registered services
|
|
1556
|
+
const childConfigPath = path.join(child.path, ".jfl", "config.json");
|
|
1557
|
+
if (fs.existsSync(childConfigPath)) {
|
|
1558
|
+
try {
|
|
1559
|
+
const childConfig = JSON.parse(fs.readFileSync(childConfigPath, "utf-8"));
|
|
1560
|
+
const childServices = childConfig.registered_services || [];
|
|
1561
|
+
for (const svc of childServices) {
|
|
1562
|
+
const svcId = `${child.name}/${svc.name}`;
|
|
1563
|
+
gtmServices.push(svc.name);
|
|
1564
|
+
// Add child services as nodes (if not already present)
|
|
1565
|
+
if (!nodes.find(n => n.id === svcId)) {
|
|
1566
|
+
nodes.push({
|
|
1567
|
+
id: svcId,
|
|
1568
|
+
label: `${svc.name}`,
|
|
1569
|
+
type: svc.type === "agent" ? "agent" : "service",
|
|
1570
|
+
status: "idle", // We don't know remote status
|
|
1571
|
+
eventCount: 0,
|
|
1572
|
+
produces: svc.context_scope?.produces,
|
|
1573
|
+
consumes: svc.context_scope?.consumes,
|
|
1574
|
+
parent: child.name,
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
catch { }
|
|
1580
|
+
}
|
|
1581
|
+
hierarchy.gtms.push({
|
|
1582
|
+
name: child.name,
|
|
1583
|
+
port: child.port,
|
|
1584
|
+
services: gtmServices,
|
|
1585
|
+
});
|
|
1586
|
+
// Add GTM node if not present
|
|
1587
|
+
if (!nodes.find(n => n.id === child.name)) {
|
|
1588
|
+
nodes.push({
|
|
1589
|
+
id: child.name,
|
|
1590
|
+
label: child.name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
|
|
1591
|
+
type: "gtm",
|
|
1592
|
+
status: "running",
|
|
1593
|
+
eventCount: 0,
|
|
1594
|
+
children: gtmServices,
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
// Build edges from produces/consumes relationships
|
|
1600
|
+
const edges = [];
|
|
1601
|
+
let edgeId = 0;
|
|
1602
|
+
// Create edges between services based on scope patterns
|
|
1603
|
+
for (const producer of nodes) {
|
|
1604
|
+
if (!producer.produces)
|
|
1605
|
+
continue;
|
|
1606
|
+
for (const eventPattern of producer.produces) {
|
|
1607
|
+
for (const consumer of nodes) {
|
|
1608
|
+
if (producer.id === consumer.id || !consumer.consumes)
|
|
1609
|
+
continue;
|
|
1610
|
+
for (const consumePattern of consumer.consumes) {
|
|
1611
|
+
// Check if patterns match (simple glob matching)
|
|
1612
|
+
const patternMatches = (prod, cons) => {
|
|
1613
|
+
if (cons.endsWith(":*")) {
|
|
1614
|
+
return prod.startsWith(cons.slice(0, -1));
|
|
1615
|
+
}
|
|
1616
|
+
if (cons === "*")
|
|
1617
|
+
return true;
|
|
1618
|
+
return prod === cons || prod.startsWith(cons.split(":")[0] + ":");
|
|
1619
|
+
};
|
|
1620
|
+
if (patternMatches(eventPattern, consumePattern)) {
|
|
1621
|
+
const existing = edges.find(e => e.source === producer.id && e.target === consumer.id && e.eventType === eventPattern);
|
|
1622
|
+
if (!existing) {
|
|
1623
|
+
const edgeKey = `${producer.id}:${consumer.id}:${eventPattern}`;
|
|
1624
|
+
const prefix = eventPattern.split(":")[0];
|
|
1625
|
+
edges.push({
|
|
1626
|
+
id: `e${++edgeId}`,
|
|
1627
|
+
source: producer.id,
|
|
1628
|
+
target: consumer.id,
|
|
1629
|
+
eventType: eventPattern,
|
|
1630
|
+
category: eventPattern.includes("eval") || eventPattern.includes("scored") ? "success"
|
|
1631
|
+
: eventPattern.includes("peter") || eventPattern.includes("rollout") ? "rl"
|
|
1632
|
+
: "data",
|
|
1633
|
+
recentEvents: edgeEventCounts[edgeKey] || eventCounts[prefix] || 0,
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
// Add known system flow edges that might not be captured by scope
|
|
1642
|
+
const systemEdges = [
|
|
1643
|
+
{ source: "telemetry-agent", target: "peter-parker", eventType: "telemetry:insight", category: "data" },
|
|
1644
|
+
{ source: "peter-parker", target: "eval-engine", eventType: "peter:task-completed", category: "rl" },
|
|
1645
|
+
{ source: "eval-engine", target: "telemetry-agent", eventType: "eval:scored", category: "success" },
|
|
1646
|
+
{ source: "peter-parker", target: "stratus", eventType: "peter:rollout-request", category: "rl" },
|
|
1647
|
+
{ source: "stratus", target: "eval-engine", eventType: "stratus:prediction", category: "success" },
|
|
1648
|
+
];
|
|
1649
|
+
for (const sysEdge of systemEdges) {
|
|
1650
|
+
const exists = edges.find(e => e.source === sysEdge.source && e.target === sysEdge.target);
|
|
1651
|
+
if (!exists && nodes.find(n => n.id === sysEdge.source) && nodes.find(n => n.id === sysEdge.target)) {
|
|
1652
|
+
const prefix = sysEdge.eventType.split(":")[0];
|
|
1653
|
+
edges.push({
|
|
1654
|
+
id: `e${++edgeId}`,
|
|
1655
|
+
...sysEdge,
|
|
1656
|
+
recentEvents: eventCounts[prefix] || 0,
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1661
|
+
res.end(JSON.stringify({
|
|
1662
|
+
nodes,
|
|
1663
|
+
edges,
|
|
1664
|
+
hierarchy: workspaceType === "portfolio" ? hierarchy : undefined,
|
|
1665
|
+
workspaceType,
|
|
1666
|
+
workspaceName,
|
|
1667
|
+
}));
|
|
1668
|
+
}
|
|
1669
|
+
catch (err) {
|
|
1670
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1671
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1672
|
+
}
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
// Autoresearch status — returns current autoresearch run state
|
|
1676
|
+
if (url.pathname === "/api/v1/autoresearch/status" && req.method === "GET") {
|
|
1677
|
+
try {
|
|
1678
|
+
const status = {
|
|
1679
|
+
running: false,
|
|
1680
|
+
currentRound: 0,
|
|
1681
|
+
totalRounds: 0,
|
|
1682
|
+
baselineComposite: null,
|
|
1683
|
+
proposals: [],
|
|
1684
|
+
dimensions: {},
|
|
1685
|
+
history: [],
|
|
1686
|
+
lastUpdate: null,
|
|
1687
|
+
};
|
|
1688
|
+
// Try to read from log files
|
|
1689
|
+
const logPaths = [
|
|
1690
|
+
path.join(projectRoot, ".jfl", "autoresearch-continuous.log"),
|
|
1691
|
+
path.join(projectRoot, ".jfl", "autoresearch-overnight.log"),
|
|
1692
|
+
];
|
|
1693
|
+
let logContent = "";
|
|
1694
|
+
let logPath = "";
|
|
1695
|
+
for (const p of logPaths) {
|
|
1696
|
+
if (fs.existsSync(p)) {
|
|
1697
|
+
const stat = fs.statSync(p);
|
|
1698
|
+
// Use the most recently modified log
|
|
1699
|
+
if (!logPath || stat.mtimeMs > fs.statSync(logPath).mtimeMs) {
|
|
1700
|
+
logPath = p;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
if (logPath) {
|
|
1705
|
+
logContent = fs.readFileSync(logPath, "utf-8");
|
|
1706
|
+
status.lastUpdate = fs.statSync(logPath).mtime.toISOString();
|
|
1707
|
+
// Parse total rounds from header
|
|
1708
|
+
const roundsMatch = logContent.match(/Autoresearch Mode \((\d+) rounds\)/);
|
|
1709
|
+
if (roundsMatch) {
|
|
1710
|
+
status.totalRounds = parseInt(roundsMatch[1], 10);
|
|
1711
|
+
}
|
|
1712
|
+
// Parse baseline
|
|
1713
|
+
const baselineMatch = logContent.match(/Baseline composite: ([\d.]+)/);
|
|
1714
|
+
if (baselineMatch) {
|
|
1715
|
+
status.baselineComposite = parseFloat(baselineMatch[1]);
|
|
1716
|
+
}
|
|
1717
|
+
// Parse latest round number
|
|
1718
|
+
const roundMatches = [...logContent.matchAll(/── Round (\d+)\/\d+ ──/g)];
|
|
1719
|
+
if (roundMatches.length > 0) {
|
|
1720
|
+
status.currentRound = parseInt(roundMatches[roundMatches.length - 1][1], 10);
|
|
1721
|
+
}
|
|
1722
|
+
// Parse policy head proposals (get the latest set)
|
|
1723
|
+
const proposalBlocks = [...logContent.matchAll(/Policy head re-ranked 3 proposals.*?\n([\s\S]*?)(?=\n\s*Task:|$)/g)];
|
|
1724
|
+
if (proposalBlocks.length > 0) {
|
|
1725
|
+
const latestBlock = proposalBlocks[proposalBlocks.length - 1][1];
|
|
1726
|
+
const proposalMatches = [...latestBlock.matchAll(/#(\d+) \[pred=([-\d.]+)\] ([^\n]+)/g)];
|
|
1727
|
+
status.proposals = proposalMatches.map(m => ({
|
|
1728
|
+
rank: parseInt(m[1], 10),
|
|
1729
|
+
predicted: parseFloat(m[2]),
|
|
1730
|
+
description: m[3].trim(),
|
|
1731
|
+
}));
|
|
1732
|
+
}
|
|
1733
|
+
// Parse dimensions from latest eval
|
|
1734
|
+
const dimMatches = [...logContent.matchAll(/Dimensions: (tests=[\d.]+ tsc=[\d.]+ lint=[\d.]+ telemetry=[\d.]+ newTests=[\d.]+)/g)];
|
|
1735
|
+
if (dimMatches.length > 0) {
|
|
1736
|
+
const latest = dimMatches[dimMatches.length - 1][1];
|
|
1737
|
+
for (const [, key, val] of latest.matchAll(/(tests|tsc|lint|telemetry|newTests)=([\d.]+)/g)) {
|
|
1738
|
+
status.dimensions[key] = parseFloat(val);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
// Parse round results for history
|
|
1742
|
+
const resultMatches = [...logContent.matchAll(/Round (\d+) result: ([\d.]+) \(([-=+][\d.]+)\)\s*\n\s*Tests: (\d+\/\d+)/g)];
|
|
1743
|
+
status.history = resultMatches.map(m => ({
|
|
1744
|
+
round: parseInt(m[1], 10),
|
|
1745
|
+
composite: parseFloat(m[2]),
|
|
1746
|
+
delta: m[3].startsWith("=") ? 0 : parseFloat(m[3]),
|
|
1747
|
+
tests: m[4],
|
|
1748
|
+
}));
|
|
1749
|
+
// Check if running (log modified in last 5 minutes and no completion message)
|
|
1750
|
+
const fiveMinAgo = Date.now() - 5 * 60 * 1000;
|
|
1751
|
+
const logMtime = fs.statSync(logPath).mtimeMs;
|
|
1752
|
+
const hasCompletionMsg = logContent.includes("All rounds complete") || logContent.includes("Autoresearch finished");
|
|
1753
|
+
status.running = logMtime > fiveMinAgo && !hasCompletionMsg && status.currentRound > 0;
|
|
1754
|
+
}
|
|
1755
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1756
|
+
res.end(JSON.stringify(status));
|
|
1757
|
+
}
|
|
1758
|
+
catch (err) {
|
|
1759
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1760
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1761
|
+
}
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1163
1764
|
if (url.pathname === "/api/actions/spawn" && req.method === "POST") {
|
|
1164
1765
|
let body = "";
|
|
1165
1766
|
req.on("data", chunk => body += chunk);
|
|
@@ -1198,6 +1799,359 @@ function createServer(projectRoot, port, eventBus, flowEngine) {
|
|
|
1198
1799
|
});
|
|
1199
1800
|
return;
|
|
1200
1801
|
}
|
|
1802
|
+
// ── Session Parity API ────────────────────────────────────────────
|
|
1803
|
+
// These endpoints provide runtime-agnostic session lifecycle.
|
|
1804
|
+
// Both Claude Code hooks and Pi extensions call these instead of
|
|
1805
|
+
// duplicating logic. Single source of truth for session init/end.
|
|
1806
|
+
// POST /api/session/init — sync repos, create branch, run doctor
|
|
1807
|
+
if (url.pathname === "/api/session/init" && req.method === "POST") {
|
|
1808
|
+
let body = "";
|
|
1809
|
+
req.on("data", (chunk) => body += chunk);
|
|
1810
|
+
req.on("end", async () => {
|
|
1811
|
+
try {
|
|
1812
|
+
const { runtime } = JSON.parse(body || "{}");
|
|
1813
|
+
const warnings = [];
|
|
1814
|
+
const scriptDir = path.join(projectRoot, "scripts", "session");
|
|
1815
|
+
// Step 1: Sync repos
|
|
1816
|
+
const syncScript = path.join(scriptDir, "session-sync.sh");
|
|
1817
|
+
let syncOk = true;
|
|
1818
|
+
if (fs.existsSync(syncScript)) {
|
|
1819
|
+
try {
|
|
1820
|
+
execSync(`bash "${syncScript}"`, { cwd: projectRoot, timeout: 60000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1821
|
+
}
|
|
1822
|
+
catch (err) {
|
|
1823
|
+
syncOk = false;
|
|
1824
|
+
warnings.push(`Sync warning: ${(err.message || "").split("\n")[0]}`);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
// Step 2: Doctor check
|
|
1828
|
+
const doctorScript = path.join(scriptDir, "jfl-doctor.sh");
|
|
1829
|
+
let doctorErrors = 0;
|
|
1830
|
+
let doctorWarnings = 0;
|
|
1831
|
+
if (fs.existsSync(doctorScript)) {
|
|
1832
|
+
try {
|
|
1833
|
+
const doctorOut = execSync(`bash "${doctorScript}"`, { cwd: projectRoot, timeout: 30000, stdio: ["pipe", "pipe", "pipe"] }).toString();
|
|
1834
|
+
const em = doctorOut.match(/(\d+) error\(s\)/);
|
|
1835
|
+
const wm = doctorOut.match(/(\d+) warning\(s\)/);
|
|
1836
|
+
doctorErrors = em ? parseInt(em[1]) : 0;
|
|
1837
|
+
doctorWarnings = wm ? parseInt(wm[1]) : 0;
|
|
1838
|
+
if (doctorErrors > 0)
|
|
1839
|
+
warnings.push(`Doctor: ${doctorErrors} errors`);
|
|
1840
|
+
}
|
|
1841
|
+
catch { }
|
|
1842
|
+
}
|
|
1843
|
+
// Step 3: Create session branch
|
|
1844
|
+
let currentBranch = "";
|
|
1845
|
+
try {
|
|
1846
|
+
currentBranch = execSync("git branch --show-current", { cwd: projectRoot, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
1847
|
+
}
|
|
1848
|
+
catch { }
|
|
1849
|
+
let sessionBranch = currentBranch;
|
|
1850
|
+
if (!currentBranch.startsWith("session-")) {
|
|
1851
|
+
const user = (() => {
|
|
1852
|
+
try {
|
|
1853
|
+
return execSync("git config user.name", { cwd: projectRoot, stdio: ["pipe", "pipe", "pipe"] })
|
|
1854
|
+
.toString().trim().replace(/\s+/g, "-").toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 30) || "user";
|
|
1855
|
+
}
|
|
1856
|
+
catch {
|
|
1857
|
+
return "user";
|
|
1858
|
+
}
|
|
1859
|
+
})();
|
|
1860
|
+
const now = new Date();
|
|
1861
|
+
const dateStr = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
1862
|
+
const timeStr = now.toISOString().slice(11, 16).replace(":", "");
|
|
1863
|
+
const randomId = Math.random().toString(16).slice(2, 8);
|
|
1864
|
+
sessionBranch = `session-${user}-${dateStr}-${timeStr}-${randomId}`;
|
|
1865
|
+
try {
|
|
1866
|
+
execSync(`git checkout -b "${sessionBranch}"`, { cwd: projectRoot, stdio: ["pipe", "pipe", "pipe"] });
|
|
1867
|
+
}
|
|
1868
|
+
catch {
|
|
1869
|
+
sessionBranch = currentBranch || "main";
|
|
1870
|
+
warnings.push("Could not create session branch");
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
// Step 4: Save session info
|
|
1874
|
+
const jflDir = path.join(projectRoot, ".jfl");
|
|
1875
|
+
fs.mkdirSync(path.join(jflDir, "logs"), { recursive: true });
|
|
1876
|
+
fs.mkdirSync(path.join(jflDir, "journal"), { recursive: true });
|
|
1877
|
+
fs.writeFileSync(path.join(jflDir, "current-session-branch.txt"), sessionBranch);
|
|
1878
|
+
fs.writeFileSync(path.join(jflDir, "current-worktree.txt"), "direct");
|
|
1879
|
+
// Emit session start event
|
|
1880
|
+
if (eventBus) {
|
|
1881
|
+
eventBus.emit({
|
|
1882
|
+
type: "session:started",
|
|
1883
|
+
source: `hub:${runtime || "unknown"}`,
|
|
1884
|
+
data: { branch: sessionBranch, runtime, warnings },
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1888
|
+
res.end(JSON.stringify({
|
|
1889
|
+
ok: true,
|
|
1890
|
+
branch: sessionBranch,
|
|
1891
|
+
syncOk,
|
|
1892
|
+
doctor: { errors: doctorErrors, warnings: doctorWarnings },
|
|
1893
|
+
warnings,
|
|
1894
|
+
}));
|
|
1895
|
+
}
|
|
1896
|
+
catch (err) {
|
|
1897
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1898
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
// POST /api/prompt — get system prompt injection (CLAUDE.md + context)
|
|
1904
|
+
if (url.pathname === "/api/prompt" && req.method === "POST") {
|
|
1905
|
+
let body = "";
|
|
1906
|
+
req.on("data", (chunk) => body += chunk);
|
|
1907
|
+
req.on("end", async () => {
|
|
1908
|
+
try {
|
|
1909
|
+
const { taskType, maxItems } = JSON.parse(body || "{}");
|
|
1910
|
+
const parts = [];
|
|
1911
|
+
// 1. Load CLAUDE.md
|
|
1912
|
+
const claudeMdPath = path.join(projectRoot, "CLAUDE.md");
|
|
1913
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
1914
|
+
const content = fs.readFileSync(claudeMdPath, "utf-8");
|
|
1915
|
+
if (content.length > 50000) {
|
|
1916
|
+
const sections = content.split(/^## /m);
|
|
1917
|
+
const critical = sections.filter((s) => /CRITICAL|Session Sync|Journal Protocol|Immediate Decision|Working Mode|Core Architecture/i.test(s.slice(0, 100)));
|
|
1918
|
+
if (critical.length > 0) {
|
|
1919
|
+
parts.push("# Project Instructions (CLAUDE.md — critical sections)\n");
|
|
1920
|
+
parts.push("## " + critical.join("\n\n## "));
|
|
1921
|
+
}
|
|
1922
|
+
else {
|
|
1923
|
+
parts.push("# Project Instructions (CLAUDE.md — truncated)\n");
|
|
1924
|
+
parts.push(content.slice(0, 30000));
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
else {
|
|
1928
|
+
parts.push("# Project Instructions (CLAUDE.md)\n");
|
|
1929
|
+
parts.push(content);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
// 2. Load recent context
|
|
1933
|
+
try {
|
|
1934
|
+
const contextItems = readJournalEntries(projectRoot, maxItems ?? 20);
|
|
1935
|
+
if (contextItems.length > 0) {
|
|
1936
|
+
parts.push("\n## Recent Project Context\n");
|
|
1937
|
+
parts.push(contextItems.map(item => {
|
|
1938
|
+
const prefix = item.source ? `[${item.source}] ` : "";
|
|
1939
|
+
return `${prefix}${item.content}`;
|
|
1940
|
+
}).join("\n\n"));
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
catch { }
|
|
1944
|
+
// 3. Load knowledge docs summaries
|
|
1945
|
+
const knowledgeDir = path.join(projectRoot, "knowledge");
|
|
1946
|
+
if (fs.existsSync(knowledgeDir)) {
|
|
1947
|
+
const knowledgeDocs = ["VISION.md", "ROADMAP.md", "NARRATIVE.md", "THESIS.md"];
|
|
1948
|
+
const summaries = [];
|
|
1949
|
+
for (const doc of knowledgeDocs) {
|
|
1950
|
+
const docPath = path.join(knowledgeDir, doc);
|
|
1951
|
+
if (fs.existsSync(docPath)) {
|
|
1952
|
+
const content = fs.readFileSync(docPath, "utf-8");
|
|
1953
|
+
if (content.length > 100) {
|
|
1954
|
+
summaries.push(`### ${doc}\n${content.slice(0, 500)}${content.length > 500 ? "\n..." : ""}`);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
if (summaries.length > 0) {
|
|
1959
|
+
parts.push("\n## Knowledge Documents\n");
|
|
1960
|
+
parts.push(summaries.join("\n\n"));
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1964
|
+
res.end(JSON.stringify({
|
|
1965
|
+
prompt: parts.join("\n"),
|
|
1966
|
+
claudeMdSize: fs.existsSync(path.join(projectRoot, "CLAUDE.md"))
|
|
1967
|
+
? fs.statSync(path.join(projectRoot, "CLAUDE.md")).size : 0,
|
|
1968
|
+
taskType: taskType ?? "general",
|
|
1969
|
+
}));
|
|
1970
|
+
}
|
|
1971
|
+
catch (err) {
|
|
1972
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1973
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1974
|
+
}
|
|
1975
|
+
});
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
// POST /api/session/end — cleanup session (merge, commit, etc)
|
|
1979
|
+
if (url.pathname === "/api/session/end" && req.method === "POST") {
|
|
1980
|
+
let body = "";
|
|
1981
|
+
req.on("data", (chunk) => body += chunk);
|
|
1982
|
+
req.on("end", async () => {
|
|
1983
|
+
try {
|
|
1984
|
+
const { runtime, skipCleanup } = JSON.parse(body || "{}");
|
|
1985
|
+
// Check journal exists
|
|
1986
|
+
let hasJournal = false;
|
|
1987
|
+
try {
|
|
1988
|
+
const branch = execSync("git branch --show-current", { cwd: projectRoot, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
1989
|
+
const journalPath = path.join(projectRoot, ".jfl", "journal", `${branch}.jsonl`);
|
|
1990
|
+
hasJournal = fs.existsSync(journalPath) && fs.statSync(journalPath).size > 0;
|
|
1991
|
+
}
|
|
1992
|
+
catch { }
|
|
1993
|
+
let cleanupResult = "skipped";
|
|
1994
|
+
if (!skipCleanup) {
|
|
1995
|
+
const cleanupScript = path.join(projectRoot, "scripts", "session", "session-cleanup.sh");
|
|
1996
|
+
if (fs.existsSync(cleanupScript)) {
|
|
1997
|
+
try {
|
|
1998
|
+
execSync(`bash "${cleanupScript}"`, { cwd: projectRoot, timeout: 60000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1999
|
+
cleanupResult = "ok";
|
|
2000
|
+
}
|
|
2001
|
+
catch (err) {
|
|
2002
|
+
cleanupResult = `error: ${(err.message || "").split("\n")[0]}`;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
else {
|
|
2006
|
+
cleanupResult = "no cleanup script";
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
// Emit session end event
|
|
2010
|
+
if (eventBus) {
|
|
2011
|
+
eventBus.emit({
|
|
2012
|
+
type: "session:ended",
|
|
2013
|
+
source: `hub:${runtime || "unknown"}`,
|
|
2014
|
+
data: { hasJournal, cleanupResult, runtime },
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2018
|
+
res.end(JSON.stringify({
|
|
2019
|
+
ok: true,
|
|
2020
|
+
hasJournal,
|
|
2021
|
+
cleanupResult,
|
|
2022
|
+
warnings: hasJournal ? [] : ["No journal entry for this session"],
|
|
2023
|
+
}));
|
|
2024
|
+
}
|
|
2025
|
+
catch (err) {
|
|
2026
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2027
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
// ── Findings API ──────────────────────────────────────────────────
|
|
2033
|
+
// GET /api/v1/findings — list current findings
|
|
2034
|
+
if (url.pathname === "/api/v1/findings" && req.method === "GET") {
|
|
2035
|
+
try {
|
|
2036
|
+
const engine = new FindingsEngine(projectRoot);
|
|
2037
|
+
const refresh = url.searchParams.get("refresh") === "true";
|
|
2038
|
+
let findings;
|
|
2039
|
+
if (refresh) {
|
|
2040
|
+
findings = await engine.analyze();
|
|
2041
|
+
}
|
|
2042
|
+
else {
|
|
2043
|
+
findings = engine.getFindings();
|
|
2044
|
+
// Auto-analyze if no findings exist
|
|
2045
|
+
if (findings.length === 0) {
|
|
2046
|
+
findings = await engine.analyze();
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
// Filter out dismissed unless ?include_dismissed=true
|
|
2050
|
+
const includeDismissed = url.searchParams.get("include_dismissed") === "true";
|
|
2051
|
+
if (!includeDismissed) {
|
|
2052
|
+
findings = findings.filter(f => !f.dismissed);
|
|
2053
|
+
}
|
|
2054
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2055
|
+
res.end(JSON.stringify({ findings, total: findings.length }));
|
|
2056
|
+
}
|
|
2057
|
+
catch (err) {
|
|
2058
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2059
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2060
|
+
}
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
// POST /api/v1/findings/:id/dismiss — dismiss a finding
|
|
2064
|
+
if (url.pathname.match(/^\/api\/v1\/findings\/[^/]+\/dismiss$/) && req.method === "POST") {
|
|
2065
|
+
try {
|
|
2066
|
+
const findingId = decodeURIComponent(url.pathname.split("/")[4]);
|
|
2067
|
+
const engine = new FindingsEngine(projectRoot);
|
|
2068
|
+
const success = engine.dismissFinding(findingId);
|
|
2069
|
+
if (success) {
|
|
2070
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2071
|
+
res.end(JSON.stringify({ ok: true, dismissed: findingId }));
|
|
2072
|
+
}
|
|
2073
|
+
else {
|
|
2074
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2075
|
+
res.end(JSON.stringify({ error: "Finding not found" }));
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
catch (err) {
|
|
2079
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2080
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2081
|
+
}
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
// POST /api/v1/findings/:id/spawn — spawn an agent from a finding
|
|
2085
|
+
if (url.pathname.match(/^\/api\/v1\/findings\/[^/]+\/spawn$/) && req.method === "POST") {
|
|
2086
|
+
try {
|
|
2087
|
+
const findingId = decodeURIComponent(url.pathname.split("/")[4]);
|
|
2088
|
+
const engine = new FindingsEngine(projectRoot);
|
|
2089
|
+
const findings = engine.getFindings();
|
|
2090
|
+
const finding = findings.find(f => f.id === findingId);
|
|
2091
|
+
if (!finding) {
|
|
2092
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2093
|
+
res.end(JSON.stringify({ error: "Finding not found" }));
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
if (!finding.agent_config) {
|
|
2097
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2098
|
+
res.end(JSON.stringify({ error: "Finding has no agent config" }));
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
// Spawn peter-parker to fix the issue
|
|
2102
|
+
const env = { ...process.env };
|
|
2103
|
+
delete env.ANTHROPIC_API_KEY;
|
|
2104
|
+
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
2105
|
+
const agentConfig = finding.agent_config;
|
|
2106
|
+
const prompt = `Fix this issue: ${finding.title}\n\n${finding.description}\n\nTarget metric: ${agentConfig.metric} >= ${agentConfig.target}\nScope files: ${agentConfig.scope_files.join(", ")}`;
|
|
2107
|
+
const child = spawn("jfl", ["peter", "run", "--prompt", prompt], {
|
|
2108
|
+
cwd: projectRoot,
|
|
2109
|
+
detached: true,
|
|
2110
|
+
stdio: "ignore",
|
|
2111
|
+
env,
|
|
2112
|
+
});
|
|
2113
|
+
child.unref();
|
|
2114
|
+
// Emit event for tracking
|
|
2115
|
+
if (eventBus) {
|
|
2116
|
+
eventBus.emit({
|
|
2117
|
+
type: "findings:agent-spawned",
|
|
2118
|
+
source: "findings-engine",
|
|
2119
|
+
data: {
|
|
2120
|
+
finding_id: findingId,
|
|
2121
|
+
finding_type: finding.type,
|
|
2122
|
+
finding_title: finding.title,
|
|
2123
|
+
pid: child.pid,
|
|
2124
|
+
},
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2128
|
+
res.end(JSON.stringify({
|
|
2129
|
+
ok: true,
|
|
2130
|
+
pid: child.pid,
|
|
2131
|
+
finding_id: findingId,
|
|
2132
|
+
agent_config: agentConfig,
|
|
2133
|
+
}));
|
|
2134
|
+
}
|
|
2135
|
+
catch (err) {
|
|
2136
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2137
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2138
|
+
}
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
// POST /api/v1/findings/analyze — force re-analyze
|
|
2142
|
+
if (url.pathname === "/api/v1/findings/analyze" && req.method === "POST") {
|
|
2143
|
+
try {
|
|
2144
|
+
const engine = new FindingsEngine(projectRoot);
|
|
2145
|
+
const findings = await engine.analyze();
|
|
2146
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2147
|
+
res.end(JSON.stringify({ findings, total: findings.length }));
|
|
2148
|
+
}
|
|
2149
|
+
catch (err) {
|
|
2150
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2151
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2152
|
+
}
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
1201
2155
|
// 404
|
|
1202
2156
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1203
2157
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
@@ -1284,47 +2238,19 @@ function getTrackedProjects() {
|
|
|
1284
2238
|
.map(p => ({ path: p, port: getProjectPort(p) }));
|
|
1285
2239
|
}
|
|
1286
2240
|
async function ensureForProject(projectRoot, port, quiet = false) {
|
|
2241
|
+
// Rule: ensure ONLY starts hubs, NEVER kills them.
|
|
2242
|
+
// If something is on the port, leave it alone.
|
|
1287
2243
|
const status = isRunning(projectRoot);
|
|
1288
2244
|
if (status.running) {
|
|
1289
|
-
|
|
1290
|
-
const response = await fetch(`http://localhost:${port}/health`, {
|
|
1291
|
-
signal: AbortSignal.timeout(2000)
|
|
1292
|
-
});
|
|
1293
|
-
if (response.ok) {
|
|
1294
|
-
return { status: "running", message: `Already running (PID: ${status.pid})` };
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
catch {
|
|
1298
|
-
// Process exists but not responding, fall through
|
|
1299
|
-
}
|
|
2245
|
+
return { status: "running", message: `Already running (PID: ${status.pid})` };
|
|
1300
2246
|
}
|
|
1301
2247
|
const portInUse = await isPortInUse(port);
|
|
1302
2248
|
if (portInUse) {
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
});
|
|
1307
|
-
if (response.ok) {
|
|
1308
|
-
return { status: "running", message: "Running (PID file missing but healthy)" };
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
catch {
|
|
1312
|
-
// Not responding
|
|
1313
|
-
}
|
|
1314
|
-
try {
|
|
1315
|
-
const lsofOutput = execSync(`lsof -ti :${port}`, { encoding: 'utf-8' }).trim();
|
|
1316
|
-
if (lsofOutput) {
|
|
1317
|
-
const orphanedPid = parseInt(lsofOutput.split('\n')[0], 10);
|
|
1318
|
-
if (!status.pid || orphanedPid !== status.pid) {
|
|
1319
|
-
process.kill(orphanedPid, 'SIGTERM');
|
|
1320
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
catch {
|
|
1325
|
-
// lsof failed or process already gone
|
|
1326
|
-
}
|
|
2249
|
+
// Something is on this port. Don't kill it — could be a healthy hub
|
|
2250
|
+
// whose PID file was lost, or a hub started by another process.
|
|
2251
|
+
return { status: "running", message: `Port ${port} in use (assuming healthy)` };
|
|
1327
2252
|
}
|
|
2253
|
+
// Nothing running, nothing on port — safe to start
|
|
1328
2254
|
const result = await startDaemon(projectRoot, port);
|
|
1329
2255
|
if (result.success) {
|
|
1330
2256
|
return { status: "started", message: result.message };
|
|
@@ -1378,7 +2304,7 @@ async function startDaemon(projectRoot, port) {
|
|
|
1378
2304
|
}
|
|
1379
2305
|
// Start as detached process with CONTEXT_HUB_DAEMON=1 so the serve
|
|
1380
2306
|
// action knows to ignore SIGTERM during its startup grace period
|
|
1381
|
-
const child = spawn(jflCmd, ["context-hub", "serve", "--port", String(port)], {
|
|
2307
|
+
const child = spawn(jflCmd, ["context-hub", "serve", "--port", String(port), "--project-root", projectRoot], {
|
|
1382
2308
|
cwd: projectRoot,
|
|
1383
2309
|
detached: true,
|
|
1384
2310
|
stdio: ["ignore", fs.openSync(logFile, "a"), fs.openSync(logFile, "a")],
|
|
@@ -1563,7 +2489,7 @@ export async function ensureDaemonInstalled(opts) {
|
|
|
1563
2489
|
// ============================================================================
|
|
1564
2490
|
export async function contextHubCommand(action, options = {}) {
|
|
1565
2491
|
const isGlobal = options.global || false;
|
|
1566
|
-
const projectRoot = isGlobal ? homedir() : process.cwd();
|
|
2492
|
+
const projectRoot = options.projectRoot || (isGlobal ? homedir() : process.cwd());
|
|
1567
2493
|
const port = options.port || getProjectPort(projectRoot);
|
|
1568
2494
|
// Ensure directories exist (skip for actions that don't need local project root)
|
|
1569
2495
|
const globalActions = ["ensure-all", "doctor", "install-daemon", "uninstall-daemon"];
|
|
@@ -1766,6 +2692,32 @@ export async function contextHubCommand(action, options = {}) {
|
|
|
1766
2692
|
break;
|
|
1767
2693
|
}
|
|
1768
2694
|
case "serve": {
|
|
2695
|
+
// Load .env files for API keys (hub runs as detached process)
|
|
2696
|
+
for (const envFile of [
|
|
2697
|
+
path.join(projectRoot, ".env"),
|
|
2698
|
+
path.join(projectRoot, ".env.local"),
|
|
2699
|
+
path.join(process.env.HOME || "/tmp", ".env"),
|
|
2700
|
+
]) {
|
|
2701
|
+
if (fs.existsSync(envFile)) {
|
|
2702
|
+
const envContent = fs.readFileSync(envFile, "utf-8");
|
|
2703
|
+
for (const line of envContent.split("\n")) {
|
|
2704
|
+
const trimmed = line.trim();
|
|
2705
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
2706
|
+
continue;
|
|
2707
|
+
const eqIdx = trimmed.indexOf("=");
|
|
2708
|
+
if (eqIdx === -1)
|
|
2709
|
+
continue;
|
|
2710
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
2711
|
+
let val = trimmed.slice(eqIdx + 1).trim();
|
|
2712
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
2713
|
+
val = val.slice(1, -1);
|
|
2714
|
+
}
|
|
2715
|
+
if (!process.env[key]) {
|
|
2716
|
+
process.env[key] = val;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
1769
2721
|
// Run server in foreground (used by daemon)
|
|
1770
2722
|
const serviceEventsPath = path.join(projectRoot, ".jfl", "service-events.jsonl");
|
|
1771
2723
|
const mapPersistPath = path.join(projectRoot, ".jfl", "map-events.jsonl");
|
|
@@ -1779,6 +2731,77 @@ export async function contextHubCommand(action, options = {}) {
|
|
|
1779
2731
|
const flowEngine = new FlowEngine(eventBus, projectRoot);
|
|
1780
2732
|
const server = createServer(projectRoot, port, eventBus, flowEngine);
|
|
1781
2733
|
let isListening = false;
|
|
2734
|
+
// Cross-service scope impact detection (GTM/portfolio level)
|
|
2735
|
+
// When eval:scored fires with improved=true, detect which other services
|
|
2736
|
+
// are affected and emit scope:impact events for each
|
|
2737
|
+
const configPath = path.join(projectRoot, ".jfl", "config.json");
|
|
2738
|
+
const hubConfig = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf-8")) : {};
|
|
2739
|
+
if (hubConfig.type === "gtm" || hubConfig.type === "portfolio") {
|
|
2740
|
+
eventBus.subscribe({
|
|
2741
|
+
clientId: "scope-detector",
|
|
2742
|
+
patterns: ["eval:scored"],
|
|
2743
|
+
transport: "poll",
|
|
2744
|
+
callback: (event) => {
|
|
2745
|
+
if (event.data?.improved !== "true" && event.data?.improved !== true)
|
|
2746
|
+
return;
|
|
2747
|
+
const serviceName = event.data?.service || event.source || "unknown";
|
|
2748
|
+
const registeredServices = hubConfig.registered_services || [];
|
|
2749
|
+
// Find source service's produces
|
|
2750
|
+
const sourceReg = registeredServices.find((s) => s.name === serviceName);
|
|
2751
|
+
if (!sourceReg?.path)
|
|
2752
|
+
return;
|
|
2753
|
+
try {
|
|
2754
|
+
const svcConfigPath = path.join(sourceReg.path, ".jfl", "config.json");
|
|
2755
|
+
if (!fs.existsSync(svcConfigPath))
|
|
2756
|
+
return;
|
|
2757
|
+
const svcConfig = JSON.parse(fs.readFileSync(svcConfigPath, "utf-8"));
|
|
2758
|
+
const produces = svcConfig.context_scope?.produces || [];
|
|
2759
|
+
if (produces.length === 0)
|
|
2760
|
+
return;
|
|
2761
|
+
// Check each other service for consuming matches
|
|
2762
|
+
for (const otherSvc of registeredServices) {
|
|
2763
|
+
if (otherSvc.name === serviceName || !otherSvc.path)
|
|
2764
|
+
continue;
|
|
2765
|
+
const otherConfigPath = path.join(otherSvc.path, ".jfl", "config.json");
|
|
2766
|
+
if (!fs.existsSync(otherConfigPath))
|
|
2767
|
+
continue;
|
|
2768
|
+
const otherConfig = JSON.parse(fs.readFileSync(otherConfigPath, "utf-8"));
|
|
2769
|
+
const consumes = otherConfig.context_scope?.consumes || [];
|
|
2770
|
+
// Match produces against consumes
|
|
2771
|
+
const matched = [];
|
|
2772
|
+
for (const p of produces) {
|
|
2773
|
+
for (const c of consumes) {
|
|
2774
|
+
if (c === "*" || p === c || (c.endsWith(":*") && p.startsWith(c.slice(0, -1))) || (c.endsWith("*") && p.startsWith(c.slice(0, -1)))) {
|
|
2775
|
+
matched.push(`${p} → ${c}`);
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
if (matched.length > 0) {
|
|
2780
|
+
const ts = new Date().toISOString();
|
|
2781
|
+
console.log(`[${ts}] scope:impact — ${serviceName} → ${otherSvc.name} (${matched.length} patterns)`);
|
|
2782
|
+
eventBus.emit({
|
|
2783
|
+
type: "scope:impact",
|
|
2784
|
+
source: "scope-detector",
|
|
2785
|
+
data: {
|
|
2786
|
+
source_service: serviceName,
|
|
2787
|
+
affected_service: otherSvc.name,
|
|
2788
|
+
affected_service_path: otherSvc.path,
|
|
2789
|
+
scope_patterns: matched,
|
|
2790
|
+
source_pr: event.data?.pr_number || "",
|
|
2791
|
+
change_description: event.data?.branch || "eval improvement",
|
|
2792
|
+
source_delta: event.data?.delta || "0",
|
|
2793
|
+
},
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
catch (err) {
|
|
2799
|
+
console.error(`[scope-detector] Error checking impact for ${serviceName}:`, err.message);
|
|
2800
|
+
}
|
|
2801
|
+
},
|
|
2802
|
+
});
|
|
2803
|
+
console.log(`[scope-detector] Cross-service impact detection enabled for ${hubConfig.type} hub`);
|
|
2804
|
+
}
|
|
1782
2805
|
// When spawned as daemon, ignore SIGTERM during startup grace period.
|
|
1783
2806
|
// The parent process (hook runner) may exit and send SIGTERM to the
|
|
1784
2807
|
// process group before we're fully detached. After grace period,
|