jfl 0.4.3 → 0.5.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 +15 -5
- package/dist/commands/context-hub.d.ts.map +1 -1
- package/dist/commands/context-hub.js +818 -39
- 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 +1218 -2
- 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/status.d.ts.map +1 -1
- package/dist/commands/status.js +22 -4
- package/dist/commands/status.js.map +1 -1
- 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-B6b867Pv.js +121 -0
- package/dist/dashboard-static/assets/index-Y4BrqxV-.css +1 -0
- package/dist/dashboard-static/index.html +2 -2
- package/dist/index.js +228 -62
- 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 +627 -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/policy-head.d.ts +25 -0
- package/dist/lib/policy-head.d.ts.map +1 -0
- package/dist/lib/policy-head.js +136 -0
- package/dist/lib/policy-head.js.map +1 -0
- 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/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 +151 -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/hud-tool.ts +145 -0
- package/packages/pi/extensions/index.ts +392 -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 +68 -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 +90 -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 +352 -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-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/.github/workflows/jfl-eval.yml +105 -8
- package/template/.github/workflows/jfl-review.yml +4 -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/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
|
|
@@ -766,6 +773,107 @@ function createServer(projectRoot, port, eventBus, flowEngine) {
|
|
|
766
773
|
}
|
|
767
774
|
return;
|
|
768
775
|
}
|
|
776
|
+
// RAG Chat — search context + memory, then stream LLM response
|
|
777
|
+
if (url.pathname === "/api/chat" && req.method === "POST") {
|
|
778
|
+
let body = "";
|
|
779
|
+
req.on("data", chunk => body += chunk);
|
|
780
|
+
req.on("end", async () => {
|
|
781
|
+
try {
|
|
782
|
+
const { message, history = [] } = JSON.parse(body || "{}");
|
|
783
|
+
if (!message) {
|
|
784
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
785
|
+
res.end(JSON.stringify({ error: "message required" }));
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
const [memRaw, ctxResult] = await Promise.allSettled([
|
|
789
|
+
searchMemories(message, { maxItems: 5 }),
|
|
790
|
+
getPortfolioContext(projectRoot, message, undefined, 5),
|
|
791
|
+
]);
|
|
792
|
+
const memResults = memRaw.status === "fulfilled"
|
|
793
|
+
? memRaw.value.map(r => ({ title: r.memory.title, content: r.memory.content, type: r.memory.type, relevance: r.relevance }))
|
|
794
|
+
: [];
|
|
795
|
+
const ctxResults = ctxResult.status === "fulfilled"
|
|
796
|
+
? (ctxResult.value.items || []).slice(0, 5).map((i) => ({ title: i.title || i.path, content: i.content?.slice(0, 500), type: i.type }))
|
|
797
|
+
: [];
|
|
798
|
+
const sources = [...memResults, ...ctxResults];
|
|
799
|
+
let contextBlock = "";
|
|
800
|
+
if (sources.length > 0) {
|
|
801
|
+
contextBlock = "Here is relevant context from the project:\n\n" +
|
|
802
|
+
sources.map((s, i) => `[${i + 1}] ${s.title} (${s.type})\n${s.content}`).join("\n\n") +
|
|
803
|
+
"\n\n---\n\n";
|
|
804
|
+
}
|
|
805
|
+
// Load .env from project root for API keys
|
|
806
|
+
const envPath = path.join(projectRoot, ".env");
|
|
807
|
+
if (fs.existsSync(envPath)) {
|
|
808
|
+
for (const line of fs.readFileSync(envPath, "utf-8").split("\n")) {
|
|
809
|
+
const match = line.match(/^([A-Z_]+)=(.+)$/);
|
|
810
|
+
if (match && !process.env[match[1]])
|
|
811
|
+
process.env[match[1]] = match[2].trim();
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const apiKey = process.env.STRATUS_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY;
|
|
815
|
+
if (!apiKey) {
|
|
816
|
+
res.writeHead(200, {
|
|
817
|
+
"Content-Type": "text/event-stream",
|
|
818
|
+
"Cache-Control": "no-cache",
|
|
819
|
+
"Connection": "keep-alive",
|
|
820
|
+
});
|
|
821
|
+
const fallback = sources.length > 0
|
|
822
|
+
? "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" +
|
|
823
|
+
sources.map((s, i) => `${i + 1}. **${s.title}** \`${s.type}\`\n ${s.content?.slice(0, 150)}`).join("\n")
|
|
824
|
+
: "No API key configured and no relevant context found.";
|
|
825
|
+
res.write(`data: ${JSON.stringify({ sources })}\n\n`);
|
|
826
|
+
res.write(`data: ${JSON.stringify({ delta: fallback })}\n\n`);
|
|
827
|
+
res.write("data: [DONE]\n\n");
|
|
828
|
+
res.end();
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
const useStratus = apiKey === process.env.STRATUS_API_KEY;
|
|
832
|
+
const baseURL = useStratus ? "https://api.stratus.run/v1" : undefined;
|
|
833
|
+
const model = useStratus ? "stratus-x1ac-huge-claude-sonnet-4-6" : "gpt-4o-mini";
|
|
834
|
+
const OpenAI = (await import("openai")).default;
|
|
835
|
+
const client = new OpenAI({ apiKey, baseURL });
|
|
836
|
+
const messages = [
|
|
837
|
+
{
|
|
838
|
+
role: "system",
|
|
839
|
+
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}`
|
|
840
|
+
},
|
|
841
|
+
...history.slice(-6).map((m) => ({ role: m.role, content: m.content })),
|
|
842
|
+
{ role: "user", content: message },
|
|
843
|
+
];
|
|
844
|
+
res.writeHead(200, {
|
|
845
|
+
"Content-Type": "text/event-stream",
|
|
846
|
+
"Cache-Control": "no-cache",
|
|
847
|
+
"Connection": "keep-alive",
|
|
848
|
+
});
|
|
849
|
+
res.write(`data: ${JSON.stringify({ sources })}\n\n`);
|
|
850
|
+
try {
|
|
851
|
+
const stream = await client.chat.completions.create({
|
|
852
|
+
model,
|
|
853
|
+
messages,
|
|
854
|
+
stream: true,
|
|
855
|
+
max_tokens: 1024,
|
|
856
|
+
});
|
|
857
|
+
for await (const chunk of stream) {
|
|
858
|
+
const delta = chunk.choices?.[0]?.delta?.content;
|
|
859
|
+
if (delta) {
|
|
860
|
+
res.write(`data: ${JSON.stringify({ delta })}\n\n`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
catch (llmErr) {
|
|
865
|
+
res.write(`data: ${JSON.stringify({ delta: `\n\nLLM error: ${llmErr.message}` })}\n\n`);
|
|
866
|
+
}
|
|
867
|
+
res.write("data: [DONE]\n\n");
|
|
868
|
+
res.end();
|
|
869
|
+
}
|
|
870
|
+
catch (err) {
|
|
871
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
872
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
769
877
|
// Memory search
|
|
770
878
|
if (url.pathname === "/api/memory/search" && req.method === "POST") {
|
|
771
879
|
let body = "";
|
|
@@ -778,7 +886,17 @@ function createServer(projectRoot, port, eventBus, flowEngine) {
|
|
|
778
886
|
res.end(JSON.stringify({ error: "query required" }));
|
|
779
887
|
return;
|
|
780
888
|
}
|
|
781
|
-
const
|
|
889
|
+
const raw = await searchMemories(query, { type, maxItems, since });
|
|
890
|
+
const results = raw.map(r => ({
|
|
891
|
+
title: r.memory.title,
|
|
892
|
+
content: r.memory.content,
|
|
893
|
+
summary: r.memory.summary,
|
|
894
|
+
type: r.memory.type,
|
|
895
|
+
source: r.memory.source,
|
|
896
|
+
ts: r.memory.created_at,
|
|
897
|
+
score: r.score,
|
|
898
|
+
relevance: r.relevance,
|
|
899
|
+
}));
|
|
782
900
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
783
901
|
res.end(JSON.stringify({ results }));
|
|
784
902
|
}
|
|
@@ -851,6 +969,23 @@ function createServer(projectRoot, port, eventBus, flowEngine) {
|
|
|
851
969
|
}
|
|
852
970
|
return;
|
|
853
971
|
}
|
|
972
|
+
// Eval entries (all raw entries for cycle display)
|
|
973
|
+
if (url.pathname === "/api/eval/entries" && req.method === "GET") {
|
|
974
|
+
try {
|
|
975
|
+
const { readEvals } = await import("../lib/eval-store.js");
|
|
976
|
+
const limit = parseInt(url.searchParams.get("limit") || "100", 10);
|
|
977
|
+
const entries = readEvals(projectRoot)
|
|
978
|
+
.sort((a, b) => b.ts.localeCompare(a.ts))
|
|
979
|
+
.slice(0, limit);
|
|
980
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
981
|
+
res.end(JSON.stringify({ entries }));
|
|
982
|
+
}
|
|
983
|
+
catch (err) {
|
|
984
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
985
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
986
|
+
}
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
854
989
|
// Eval leaderboard
|
|
855
990
|
if (url.pathname === "/api/eval/leaderboard" && req.method === "GET") {
|
|
856
991
|
try {
|
|
@@ -1076,6 +1211,116 @@ function createServer(projectRoot, port, eventBus, flowEngine) {
|
|
|
1076
1211
|
}
|
|
1077
1212
|
return;
|
|
1078
1213
|
}
|
|
1214
|
+
// ── Autoresearch / Experiments API ──────────────────────────────
|
|
1215
|
+
// Agent configs
|
|
1216
|
+
if (url.pathname === "/api/v1/agents" && req.method === "GET") {
|
|
1217
|
+
try {
|
|
1218
|
+
const { listAgentConfigs, loadAgentConfig } = await import("../lib/agent-config.js");
|
|
1219
|
+
const names = listAgentConfigs(projectRoot);
|
|
1220
|
+
const agents = names.map(name => {
|
|
1221
|
+
try {
|
|
1222
|
+
return loadAgentConfig(projectRoot, name);
|
|
1223
|
+
}
|
|
1224
|
+
catch {
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
}).filter(Boolean);
|
|
1228
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1229
|
+
res.end(JSON.stringify({ agents }));
|
|
1230
|
+
}
|
|
1231
|
+
catch (err) {
|
|
1232
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1233
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1234
|
+
}
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
// Replay buffer (experiment history)
|
|
1238
|
+
if (url.pathname === "/api/v1/experiments" && req.method === "GET") {
|
|
1239
|
+
try {
|
|
1240
|
+
const bufferPath = path.join(projectRoot, ".jfl", "replay-buffer.jsonl");
|
|
1241
|
+
const trainingPath = path.join(projectRoot, ".jfl", "training-buffer.jsonl");
|
|
1242
|
+
const experiments = [];
|
|
1243
|
+
for (const p of [bufferPath, trainingPath]) {
|
|
1244
|
+
if (fs.existsSync(p)) {
|
|
1245
|
+
const lines = fs.readFileSync(p, "utf-8").trim().split("\n").filter(Boolean);
|
|
1246
|
+
for (const line of lines.slice(-100)) {
|
|
1247
|
+
try {
|
|
1248
|
+
experiments.push(JSON.parse(line));
|
|
1249
|
+
}
|
|
1250
|
+
catch { }
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
const agent = url.searchParams.get("agent");
|
|
1255
|
+
const filtered = agent ? experiments.filter(e => e.agent === agent) : experiments;
|
|
1256
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1257
|
+
res.end(JSON.stringify({ experiments: filtered, total: filtered.length }));
|
|
1258
|
+
}
|
|
1259
|
+
catch (err) {
|
|
1260
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1261
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1262
|
+
}
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
// Session results
|
|
1266
|
+
if (url.pathname === "/api/v1/sessions" && req.method === "GET") {
|
|
1267
|
+
try {
|
|
1268
|
+
const sessionsDir = path.join(projectRoot, ".jfl", "sessions");
|
|
1269
|
+
const sessions = [];
|
|
1270
|
+
if (fs.existsSync(sessionsDir)) {
|
|
1271
|
+
for (const dir of fs.readdirSync(sessionsDir)) {
|
|
1272
|
+
const resultsPath = path.join(sessionsDir, dir, "results.tsv");
|
|
1273
|
+
if (fs.existsSync(resultsPath)) {
|
|
1274
|
+
const content = fs.readFileSync(resultsPath, "utf-8").trim();
|
|
1275
|
+
const lines = content.split("\n").slice(1); // skip header
|
|
1276
|
+
const rounds = lines.filter(l => l.trim()).map(line => {
|
|
1277
|
+
const [round, task, baseline, metric, delta, kept, duration, error, timestamp] = line.split("\t");
|
|
1278
|
+
return { round: +round, task, baseline: +baseline, metric: +metric, delta: +delta, kept: kept === "1", duration_ms: +duration, error, timestamp };
|
|
1279
|
+
});
|
|
1280
|
+
if (rounds.length > 0) {
|
|
1281
|
+
sessions.push({ id: dir, rounds, agent: dir.replace(/-[a-f0-9]{8}-\d+$/, "") });
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1287
|
+
res.end(JSON.stringify({ sessions }));
|
|
1288
|
+
}
|
|
1289
|
+
catch (err) {
|
|
1290
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1291
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1292
|
+
}
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
// Product context
|
|
1296
|
+
if (url.pathname === "/api/v1/product-context" && req.method === "GET") {
|
|
1297
|
+
const contextPath = path.join(projectRoot, ".jfl", "product-context.md");
|
|
1298
|
+
if (fs.existsSync(contextPath)) {
|
|
1299
|
+
const content = fs.readFileSync(contextPath, "utf-8");
|
|
1300
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1301
|
+
res.end(JSON.stringify({ context: content, updatedAt: fs.statSync(contextPath).mtime.toISOString() }));
|
|
1302
|
+
}
|
|
1303
|
+
else {
|
|
1304
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1305
|
+
res.end(JSON.stringify({ context: null, updatedAt: null }));
|
|
1306
|
+
}
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
// Product analysis (telemetry agent v2)
|
|
1310
|
+
if (url.pathname === "/api/v1/product-analysis" && req.method === "GET") {
|
|
1311
|
+
try {
|
|
1312
|
+
const { TelemetryAgentV2 } = await import("../lib/telemetry-agent-v2.js");
|
|
1313
|
+
const agent = new TelemetryAgentV2({ projectRoot });
|
|
1314
|
+
const analysis = await agent.analyzeProduct();
|
|
1315
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1316
|
+
res.end(JSON.stringify(analysis));
|
|
1317
|
+
}
|
|
1318
|
+
catch (err) {
|
|
1319
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1320
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1321
|
+
}
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1079
1324
|
// Flow definitions
|
|
1080
1325
|
if (url.pathname === "/api/flows" && req.method === "GET") {
|
|
1081
1326
|
if (!flowEngine) {
|
|
@@ -1160,6 +1405,348 @@ function createServer(projectRoot, port, eventBus, flowEngine) {
|
|
|
1160
1405
|
});
|
|
1161
1406
|
return;
|
|
1162
1407
|
}
|
|
1408
|
+
// Topology — returns nodes/edges for agent topology visualization
|
|
1409
|
+
if (url.pathname === "/api/v1/topology" && req.method === "GET") {
|
|
1410
|
+
try {
|
|
1411
|
+
const configPath = path.join(projectRoot, ".jfl", "config.json");
|
|
1412
|
+
let registeredServices = [];
|
|
1413
|
+
let workspaceType = "standalone";
|
|
1414
|
+
let workspaceName = "workspace";
|
|
1415
|
+
if (fs.existsSync(configPath)) {
|
|
1416
|
+
try {
|
|
1417
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1418
|
+
registeredServices = config.registered_services || [];
|
|
1419
|
+
workspaceType = config.type || "standalone";
|
|
1420
|
+
workspaceName = config.name || "workspace";
|
|
1421
|
+
}
|
|
1422
|
+
catch { }
|
|
1423
|
+
}
|
|
1424
|
+
// Read event counts from map-events.jsonl
|
|
1425
|
+
const mapEventsPath = path.join(projectRoot, ".jfl", "map-events.jsonl");
|
|
1426
|
+
const eventCounts = {};
|
|
1427
|
+
const edgeEventCounts = {}; // "source:target:eventType" -> count
|
|
1428
|
+
const recentWindow = 24 * 60 * 60 * 1000; // 24 hours
|
|
1429
|
+
if (fs.existsSync(mapEventsPath)) {
|
|
1430
|
+
try {
|
|
1431
|
+
const lines = fs.readFileSync(mapEventsPath, "utf-8").trim().split("\n");
|
|
1432
|
+
const now = Date.now();
|
|
1433
|
+
for (const line of lines.slice(-1000)) { // Last 1000 events
|
|
1434
|
+
if (!line)
|
|
1435
|
+
continue;
|
|
1436
|
+
try {
|
|
1437
|
+
const evt = JSON.parse(line);
|
|
1438
|
+
const ts = new Date(evt.ts).getTime();
|
|
1439
|
+
if (now - ts > recentWindow)
|
|
1440
|
+
continue;
|
|
1441
|
+
// Count events by source
|
|
1442
|
+
if (evt.source) {
|
|
1443
|
+
const srcId = evt.source.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1444
|
+
eventCounts[srcId] = (eventCounts[srcId] || 0) + 1;
|
|
1445
|
+
}
|
|
1446
|
+
// Count events by type prefix (for edge matching)
|
|
1447
|
+
if (evt.type) {
|
|
1448
|
+
const prefix = evt.type.split(":")[0];
|
|
1449
|
+
eventCounts[prefix] = (eventCounts[prefix] || 0) + 1;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
catch { }
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
catch { }
|
|
1456
|
+
}
|
|
1457
|
+
// System agents (always present in a JFL installation)
|
|
1458
|
+
// Only Peter Parker is a real system agent — others come from .jfl/agents/*.toml configs
|
|
1459
|
+
// Stratus, eval-engine, telemetry-agent are either external infra or RL agents now
|
|
1460
|
+
const systemAgents = [
|
|
1461
|
+
{ 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"] },
|
|
1462
|
+
];
|
|
1463
|
+
const nodes = [
|
|
1464
|
+
...systemAgents.map(a => ({
|
|
1465
|
+
id: a.id,
|
|
1466
|
+
label: a.label,
|
|
1467
|
+
type: a.type,
|
|
1468
|
+
status: a.status,
|
|
1469
|
+
eventCount: eventCounts[a.id] || eventCounts[a.id.split("-")[0]] || 0,
|
|
1470
|
+
produces: a.produces,
|
|
1471
|
+
consumes: a.consumes,
|
|
1472
|
+
})),
|
|
1473
|
+
];
|
|
1474
|
+
// Add registered services as nodes
|
|
1475
|
+
for (const service of registeredServices) {
|
|
1476
|
+
if (!nodes.find(n => n.id === service.name)) {
|
|
1477
|
+
const nodeType = service.type === "agent" ? "agent"
|
|
1478
|
+
: service.type === "eval" ? "eval"
|
|
1479
|
+
: service.type === "gtm" ? "gtm"
|
|
1480
|
+
: "service";
|
|
1481
|
+
nodes.push({
|
|
1482
|
+
id: service.name,
|
|
1483
|
+
label: service.name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
|
|
1484
|
+
type: nodeType,
|
|
1485
|
+
status: service.status === "running" ? "running" : service.status === "idle" ? "idle" : "stopped",
|
|
1486
|
+
eventCount: eventCounts[service.name] || 0,
|
|
1487
|
+
produces: service.context_scope?.produces,
|
|
1488
|
+
consumes: service.context_scope?.consumes,
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
// Add RL agent nodes from .jfl/agents/*.toml configs
|
|
1493
|
+
try {
|
|
1494
|
+
const rlAgentConfigs = loadAllAgentConfigs(projectRoot);
|
|
1495
|
+
const trainingBuffer = new TrainingBuffer(projectRoot);
|
|
1496
|
+
const trainingEntries = trainingBuffer.read();
|
|
1497
|
+
for (const config of rlAgentConfigs) {
|
|
1498
|
+
const nodeId = `rl-agent-${config.name}`;
|
|
1499
|
+
// Skip if node already exists (e.g., matches a registered service name)
|
|
1500
|
+
if (nodes.find(n => n.id === nodeId || n.id === config.name)) {
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
// Check for recent training data (within last 24h)
|
|
1504
|
+
const now = Date.now();
|
|
1505
|
+
const recentWindow = 24 * 60 * 60 * 1000;
|
|
1506
|
+
const recentEntries = trainingEntries.filter(e => {
|
|
1507
|
+
if (e.agent !== config.name)
|
|
1508
|
+
return false;
|
|
1509
|
+
const ts = new Date(e.ts).getTime();
|
|
1510
|
+
return now - ts < recentWindow;
|
|
1511
|
+
});
|
|
1512
|
+
const status = recentEntries.length > 0 ? "running" : "idle";
|
|
1513
|
+
// Convert name to proper label (e.g., "cli-speed" -> "Cli Speed")
|
|
1514
|
+
const label = config.name
|
|
1515
|
+
.split("-")
|
|
1516
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1517
|
+
.join(" ");
|
|
1518
|
+
nodes.push({
|
|
1519
|
+
id: nodeId,
|
|
1520
|
+
label,
|
|
1521
|
+
type: "agent",
|
|
1522
|
+
status,
|
|
1523
|
+
eventCount: recentEntries.length,
|
|
1524
|
+
produces: config.context_scope?.produces,
|
|
1525
|
+
consumes: config.context_scope?.consumes,
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
catch (err) {
|
|
1530
|
+
// Non-fatal: RL agents are optional
|
|
1531
|
+
}
|
|
1532
|
+
// For portfolio mode, fetch child GTM services and their registered services
|
|
1533
|
+
const childHubs = getChildHubs(projectRoot);
|
|
1534
|
+
const hierarchy = {
|
|
1535
|
+
gtms: [],
|
|
1536
|
+
};
|
|
1537
|
+
if (workspaceType === "portfolio" && childHubs.length > 0) {
|
|
1538
|
+
hierarchy.portfolio = workspaceName;
|
|
1539
|
+
for (const child of childHubs) {
|
|
1540
|
+
const gtmServices = [];
|
|
1541
|
+
// Try to read child's config for its registered services
|
|
1542
|
+
const childConfigPath = path.join(child.path, ".jfl", "config.json");
|
|
1543
|
+
if (fs.existsSync(childConfigPath)) {
|
|
1544
|
+
try {
|
|
1545
|
+
const childConfig = JSON.parse(fs.readFileSync(childConfigPath, "utf-8"));
|
|
1546
|
+
const childServices = childConfig.registered_services || [];
|
|
1547
|
+
for (const svc of childServices) {
|
|
1548
|
+
const svcId = `${child.name}/${svc.name}`;
|
|
1549
|
+
gtmServices.push(svc.name);
|
|
1550
|
+
// Add child services as nodes (if not already present)
|
|
1551
|
+
if (!nodes.find(n => n.id === svcId)) {
|
|
1552
|
+
nodes.push({
|
|
1553
|
+
id: svcId,
|
|
1554
|
+
label: `${svc.name}`,
|
|
1555
|
+
type: svc.type === "agent" ? "agent" : "service",
|
|
1556
|
+
status: "idle", // We don't know remote status
|
|
1557
|
+
eventCount: 0,
|
|
1558
|
+
produces: svc.context_scope?.produces,
|
|
1559
|
+
consumes: svc.context_scope?.consumes,
|
|
1560
|
+
parent: child.name,
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
catch { }
|
|
1566
|
+
}
|
|
1567
|
+
hierarchy.gtms.push({
|
|
1568
|
+
name: child.name,
|
|
1569
|
+
port: child.port,
|
|
1570
|
+
services: gtmServices,
|
|
1571
|
+
});
|
|
1572
|
+
// Add GTM node if not present
|
|
1573
|
+
if (!nodes.find(n => n.id === child.name)) {
|
|
1574
|
+
nodes.push({
|
|
1575
|
+
id: child.name,
|
|
1576
|
+
label: child.name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
|
|
1577
|
+
type: "gtm",
|
|
1578
|
+
status: "running",
|
|
1579
|
+
eventCount: 0,
|
|
1580
|
+
children: gtmServices,
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
// Build edges from produces/consumes relationships
|
|
1586
|
+
const edges = [];
|
|
1587
|
+
let edgeId = 0;
|
|
1588
|
+
// Create edges between services based on scope patterns
|
|
1589
|
+
for (const producer of nodes) {
|
|
1590
|
+
if (!producer.produces)
|
|
1591
|
+
continue;
|
|
1592
|
+
for (const eventPattern of producer.produces) {
|
|
1593
|
+
for (const consumer of nodes) {
|
|
1594
|
+
if (producer.id === consumer.id || !consumer.consumes)
|
|
1595
|
+
continue;
|
|
1596
|
+
for (const consumePattern of consumer.consumes) {
|
|
1597
|
+
// Check if patterns match (simple glob matching)
|
|
1598
|
+
const patternMatches = (prod, cons) => {
|
|
1599
|
+
if (cons.endsWith(":*")) {
|
|
1600
|
+
return prod.startsWith(cons.slice(0, -1));
|
|
1601
|
+
}
|
|
1602
|
+
if (cons === "*")
|
|
1603
|
+
return true;
|
|
1604
|
+
return prod === cons || prod.startsWith(cons.split(":")[0] + ":");
|
|
1605
|
+
};
|
|
1606
|
+
if (patternMatches(eventPattern, consumePattern)) {
|
|
1607
|
+
const existing = edges.find(e => e.source === producer.id && e.target === consumer.id && e.eventType === eventPattern);
|
|
1608
|
+
if (!existing) {
|
|
1609
|
+
const edgeKey = `${producer.id}:${consumer.id}:${eventPattern}`;
|
|
1610
|
+
const prefix = eventPattern.split(":")[0];
|
|
1611
|
+
edges.push({
|
|
1612
|
+
id: `e${++edgeId}`,
|
|
1613
|
+
source: producer.id,
|
|
1614
|
+
target: consumer.id,
|
|
1615
|
+
eventType: eventPattern,
|
|
1616
|
+
category: eventPattern.includes("eval") || eventPattern.includes("scored") ? "success"
|
|
1617
|
+
: eventPattern.includes("peter") || eventPattern.includes("rollout") ? "rl"
|
|
1618
|
+
: "data",
|
|
1619
|
+
recentEvents: edgeEventCounts[edgeKey] || eventCounts[prefix] || 0,
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
// Add known system flow edges that might not be captured by scope
|
|
1628
|
+
const systemEdges = [
|
|
1629
|
+
{ source: "telemetry-agent", target: "peter-parker", eventType: "telemetry:insight", category: "data" },
|
|
1630
|
+
{ source: "peter-parker", target: "eval-engine", eventType: "peter:task-completed", category: "rl" },
|
|
1631
|
+
{ source: "eval-engine", target: "telemetry-agent", eventType: "eval:scored", category: "success" },
|
|
1632
|
+
{ source: "peter-parker", target: "stratus", eventType: "peter:rollout-request", category: "rl" },
|
|
1633
|
+
{ source: "stratus", target: "eval-engine", eventType: "stratus:prediction", category: "success" },
|
|
1634
|
+
];
|
|
1635
|
+
for (const sysEdge of systemEdges) {
|
|
1636
|
+
const exists = edges.find(e => e.source === sysEdge.source && e.target === sysEdge.target);
|
|
1637
|
+
if (!exists && nodes.find(n => n.id === sysEdge.source) && nodes.find(n => n.id === sysEdge.target)) {
|
|
1638
|
+
const prefix = sysEdge.eventType.split(":")[0];
|
|
1639
|
+
edges.push({
|
|
1640
|
+
id: `e${++edgeId}`,
|
|
1641
|
+
...sysEdge,
|
|
1642
|
+
recentEvents: eventCounts[prefix] || 0,
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1647
|
+
res.end(JSON.stringify({
|
|
1648
|
+
nodes,
|
|
1649
|
+
edges,
|
|
1650
|
+
hierarchy: workspaceType === "portfolio" ? hierarchy : undefined,
|
|
1651
|
+
workspaceType,
|
|
1652
|
+
workspaceName,
|
|
1653
|
+
}));
|
|
1654
|
+
}
|
|
1655
|
+
catch (err) {
|
|
1656
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1657
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1658
|
+
}
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
// Autoresearch status — returns current autoresearch run state
|
|
1662
|
+
if (url.pathname === "/api/v1/autoresearch/status" && req.method === "GET") {
|
|
1663
|
+
try {
|
|
1664
|
+
const status = {
|
|
1665
|
+
running: false,
|
|
1666
|
+
currentRound: 0,
|
|
1667
|
+
totalRounds: 0,
|
|
1668
|
+
baselineComposite: null,
|
|
1669
|
+
proposals: [],
|
|
1670
|
+
dimensions: {},
|
|
1671
|
+
history: [],
|
|
1672
|
+
lastUpdate: null,
|
|
1673
|
+
};
|
|
1674
|
+
// Try to read from log files
|
|
1675
|
+
const logPaths = [
|
|
1676
|
+
path.join(projectRoot, ".jfl", "autoresearch-continuous.log"),
|
|
1677
|
+
path.join(projectRoot, ".jfl", "autoresearch-overnight.log"),
|
|
1678
|
+
];
|
|
1679
|
+
let logContent = "";
|
|
1680
|
+
let logPath = "";
|
|
1681
|
+
for (const p of logPaths) {
|
|
1682
|
+
if (fs.existsSync(p)) {
|
|
1683
|
+
const stat = fs.statSync(p);
|
|
1684
|
+
// Use the most recently modified log
|
|
1685
|
+
if (!logPath || stat.mtimeMs > fs.statSync(logPath).mtimeMs) {
|
|
1686
|
+
logPath = p;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
if (logPath) {
|
|
1691
|
+
logContent = fs.readFileSync(logPath, "utf-8");
|
|
1692
|
+
status.lastUpdate = fs.statSync(logPath).mtime.toISOString();
|
|
1693
|
+
// Parse total rounds from header
|
|
1694
|
+
const roundsMatch = logContent.match(/Autoresearch Mode \((\d+) rounds\)/);
|
|
1695
|
+
if (roundsMatch) {
|
|
1696
|
+
status.totalRounds = parseInt(roundsMatch[1], 10);
|
|
1697
|
+
}
|
|
1698
|
+
// Parse baseline
|
|
1699
|
+
const baselineMatch = logContent.match(/Baseline composite: ([\d.]+)/);
|
|
1700
|
+
if (baselineMatch) {
|
|
1701
|
+
status.baselineComposite = parseFloat(baselineMatch[1]);
|
|
1702
|
+
}
|
|
1703
|
+
// Parse latest round number
|
|
1704
|
+
const roundMatches = [...logContent.matchAll(/── Round (\d+)\/\d+ ──/g)];
|
|
1705
|
+
if (roundMatches.length > 0) {
|
|
1706
|
+
status.currentRound = parseInt(roundMatches[roundMatches.length - 1][1], 10);
|
|
1707
|
+
}
|
|
1708
|
+
// Parse policy head proposals (get the latest set)
|
|
1709
|
+
const proposalBlocks = [...logContent.matchAll(/Policy head re-ranked 3 proposals.*?\n([\s\S]*?)(?=\n\s*Task:|$)/g)];
|
|
1710
|
+
if (proposalBlocks.length > 0) {
|
|
1711
|
+
const latestBlock = proposalBlocks[proposalBlocks.length - 1][1];
|
|
1712
|
+
const proposalMatches = [...latestBlock.matchAll(/#(\d+) \[pred=([-\d.]+)\] ([^\n]+)/g)];
|
|
1713
|
+
status.proposals = proposalMatches.map(m => ({
|
|
1714
|
+
rank: parseInt(m[1], 10),
|
|
1715
|
+
predicted: parseFloat(m[2]),
|
|
1716
|
+
description: m[3].trim(),
|
|
1717
|
+
}));
|
|
1718
|
+
}
|
|
1719
|
+
// Parse dimensions from latest eval
|
|
1720
|
+
const dimMatches = [...logContent.matchAll(/Dimensions: (tests=[\d.]+ tsc=[\d.]+ lint=[\d.]+ telemetry=[\d.]+ newTests=[\d.]+)/g)];
|
|
1721
|
+
if (dimMatches.length > 0) {
|
|
1722
|
+
const latest = dimMatches[dimMatches.length - 1][1];
|
|
1723
|
+
for (const [, key, val] of latest.matchAll(/(tests|tsc|lint|telemetry|newTests)=([\d.]+)/g)) {
|
|
1724
|
+
status.dimensions[key] = parseFloat(val);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
// Parse round results for history
|
|
1728
|
+
const resultMatches = [...logContent.matchAll(/Round (\d+) result: ([\d.]+) \(([-=+][\d.]+)\)\s*\n\s*Tests: (\d+\/\d+)/g)];
|
|
1729
|
+
status.history = resultMatches.map(m => ({
|
|
1730
|
+
round: parseInt(m[1], 10),
|
|
1731
|
+
composite: parseFloat(m[2]),
|
|
1732
|
+
delta: m[3].startsWith("=") ? 0 : parseFloat(m[3]),
|
|
1733
|
+
tests: m[4],
|
|
1734
|
+
}));
|
|
1735
|
+
// Check if running (log modified in last 5 minutes and no completion message)
|
|
1736
|
+
const fiveMinAgo = Date.now() - 5 * 60 * 1000;
|
|
1737
|
+
const logMtime = fs.statSync(logPath).mtimeMs;
|
|
1738
|
+
const hasCompletionMsg = logContent.includes("All rounds complete") || logContent.includes("Autoresearch finished");
|
|
1739
|
+
status.running = logMtime > fiveMinAgo && !hasCompletionMsg && status.currentRound > 0;
|
|
1740
|
+
}
|
|
1741
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1742
|
+
res.end(JSON.stringify(status));
|
|
1743
|
+
}
|
|
1744
|
+
catch (err) {
|
|
1745
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1746
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1747
|
+
}
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1163
1750
|
if (url.pathname === "/api/actions/spawn" && req.method === "POST") {
|
|
1164
1751
|
let body = "";
|
|
1165
1752
|
req.on("data", chunk => body += chunk);
|
|
@@ -1198,6 +1785,129 @@ function createServer(projectRoot, port, eventBus, flowEngine) {
|
|
|
1198
1785
|
});
|
|
1199
1786
|
return;
|
|
1200
1787
|
}
|
|
1788
|
+
// ── Findings API ──────────────────────────────────────────────────
|
|
1789
|
+
// GET /api/v1/findings — list current findings
|
|
1790
|
+
if (url.pathname === "/api/v1/findings" && req.method === "GET") {
|
|
1791
|
+
try {
|
|
1792
|
+
const engine = new FindingsEngine(projectRoot);
|
|
1793
|
+
const refresh = url.searchParams.get("refresh") === "true";
|
|
1794
|
+
let findings;
|
|
1795
|
+
if (refresh) {
|
|
1796
|
+
findings = await engine.analyze();
|
|
1797
|
+
}
|
|
1798
|
+
else {
|
|
1799
|
+
findings = engine.getFindings();
|
|
1800
|
+
// Auto-analyze if no findings exist
|
|
1801
|
+
if (findings.length === 0) {
|
|
1802
|
+
findings = await engine.analyze();
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
// Filter out dismissed unless ?include_dismissed=true
|
|
1806
|
+
const includeDismissed = url.searchParams.get("include_dismissed") === "true";
|
|
1807
|
+
if (!includeDismissed) {
|
|
1808
|
+
findings = findings.filter(f => !f.dismissed);
|
|
1809
|
+
}
|
|
1810
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1811
|
+
res.end(JSON.stringify({ findings, total: findings.length }));
|
|
1812
|
+
}
|
|
1813
|
+
catch (err) {
|
|
1814
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1815
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1816
|
+
}
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
// POST /api/v1/findings/:id/dismiss — dismiss a finding
|
|
1820
|
+
if (url.pathname.match(/^\/api\/v1\/findings\/[^/]+\/dismiss$/) && req.method === "POST") {
|
|
1821
|
+
try {
|
|
1822
|
+
const findingId = decodeURIComponent(url.pathname.split("/")[4]);
|
|
1823
|
+
const engine = new FindingsEngine(projectRoot);
|
|
1824
|
+
const success = engine.dismissFinding(findingId);
|
|
1825
|
+
if (success) {
|
|
1826
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1827
|
+
res.end(JSON.stringify({ ok: true, dismissed: findingId }));
|
|
1828
|
+
}
|
|
1829
|
+
else {
|
|
1830
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1831
|
+
res.end(JSON.stringify({ error: "Finding not found" }));
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
catch (err) {
|
|
1835
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1836
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1837
|
+
}
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
// POST /api/v1/findings/:id/spawn — spawn an agent from a finding
|
|
1841
|
+
if (url.pathname.match(/^\/api\/v1\/findings\/[^/]+\/spawn$/) && req.method === "POST") {
|
|
1842
|
+
try {
|
|
1843
|
+
const findingId = decodeURIComponent(url.pathname.split("/")[4]);
|
|
1844
|
+
const engine = new FindingsEngine(projectRoot);
|
|
1845
|
+
const findings = engine.getFindings();
|
|
1846
|
+
const finding = findings.find(f => f.id === findingId);
|
|
1847
|
+
if (!finding) {
|
|
1848
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1849
|
+
res.end(JSON.stringify({ error: "Finding not found" }));
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
if (!finding.agent_config) {
|
|
1853
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1854
|
+
res.end(JSON.stringify({ error: "Finding has no agent config" }));
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
// Spawn peter-parker to fix the issue
|
|
1858
|
+
const env = { ...process.env };
|
|
1859
|
+
delete env.ANTHROPIC_API_KEY;
|
|
1860
|
+
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
1861
|
+
const agentConfig = finding.agent_config;
|
|
1862
|
+
const prompt = `Fix this issue: ${finding.title}\n\n${finding.description}\n\nTarget metric: ${agentConfig.metric} >= ${agentConfig.target}\nScope files: ${agentConfig.scope_files.join(", ")}`;
|
|
1863
|
+
const child = spawn("jfl", ["peter", "run", "--prompt", prompt], {
|
|
1864
|
+
cwd: projectRoot,
|
|
1865
|
+
detached: true,
|
|
1866
|
+
stdio: "ignore",
|
|
1867
|
+
env,
|
|
1868
|
+
});
|
|
1869
|
+
child.unref();
|
|
1870
|
+
// Emit event for tracking
|
|
1871
|
+
if (eventBus) {
|
|
1872
|
+
eventBus.emit({
|
|
1873
|
+
type: "findings:agent-spawned",
|
|
1874
|
+
source: "findings-engine",
|
|
1875
|
+
data: {
|
|
1876
|
+
finding_id: findingId,
|
|
1877
|
+
finding_type: finding.type,
|
|
1878
|
+
finding_title: finding.title,
|
|
1879
|
+
pid: child.pid,
|
|
1880
|
+
},
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1884
|
+
res.end(JSON.stringify({
|
|
1885
|
+
ok: true,
|
|
1886
|
+
pid: child.pid,
|
|
1887
|
+
finding_id: findingId,
|
|
1888
|
+
agent_config: agentConfig,
|
|
1889
|
+
}));
|
|
1890
|
+
}
|
|
1891
|
+
catch (err) {
|
|
1892
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1893
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1894
|
+
}
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
// POST /api/v1/findings/analyze — force re-analyze
|
|
1898
|
+
if (url.pathname === "/api/v1/findings/analyze" && req.method === "POST") {
|
|
1899
|
+
try {
|
|
1900
|
+
const engine = new FindingsEngine(projectRoot);
|
|
1901
|
+
const findings = await engine.analyze();
|
|
1902
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1903
|
+
res.end(JSON.stringify({ findings, total: findings.length }));
|
|
1904
|
+
}
|
|
1905
|
+
catch (err) {
|
|
1906
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1907
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1908
|
+
}
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1201
1911
|
// 404
|
|
1202
1912
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1203
1913
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
@@ -1284,47 +1994,19 @@ function getTrackedProjects() {
|
|
|
1284
1994
|
.map(p => ({ path: p, port: getProjectPort(p) }));
|
|
1285
1995
|
}
|
|
1286
1996
|
async function ensureForProject(projectRoot, port, quiet = false) {
|
|
1997
|
+
// Rule: ensure ONLY starts hubs, NEVER kills them.
|
|
1998
|
+
// If something is on the port, leave it alone.
|
|
1287
1999
|
const status = isRunning(projectRoot);
|
|
1288
2000
|
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
|
-
}
|
|
2001
|
+
return { status: "running", message: `Already running (PID: ${status.pid})` };
|
|
1300
2002
|
}
|
|
1301
2003
|
const portInUse = await isPortInUse(port);
|
|
1302
2004
|
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
|
-
}
|
|
2005
|
+
// Something is on this port. Don't kill it — could be a healthy hub
|
|
2006
|
+
// whose PID file was lost, or a hub started by another process.
|
|
2007
|
+
return { status: "running", message: `Port ${port} in use (assuming healthy)` };
|
|
1327
2008
|
}
|
|
2009
|
+
// Nothing running, nothing on port — safe to start
|
|
1328
2010
|
const result = await startDaemon(projectRoot, port);
|
|
1329
2011
|
if (result.success) {
|
|
1330
2012
|
return { status: "started", message: result.message };
|
|
@@ -1766,6 +2448,32 @@ export async function contextHubCommand(action, options = {}) {
|
|
|
1766
2448
|
break;
|
|
1767
2449
|
}
|
|
1768
2450
|
case "serve": {
|
|
2451
|
+
// Load .env files for API keys (hub runs as detached process)
|
|
2452
|
+
for (const envFile of [
|
|
2453
|
+
path.join(projectRoot, ".env"),
|
|
2454
|
+
path.join(projectRoot, ".env.local"),
|
|
2455
|
+
path.join(process.env.HOME || "/tmp", ".env"),
|
|
2456
|
+
]) {
|
|
2457
|
+
if (fs.existsSync(envFile)) {
|
|
2458
|
+
const envContent = fs.readFileSync(envFile, "utf-8");
|
|
2459
|
+
for (const line of envContent.split("\n")) {
|
|
2460
|
+
const trimmed = line.trim();
|
|
2461
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
2462
|
+
continue;
|
|
2463
|
+
const eqIdx = trimmed.indexOf("=");
|
|
2464
|
+
if (eqIdx === -1)
|
|
2465
|
+
continue;
|
|
2466
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
2467
|
+
let val = trimmed.slice(eqIdx + 1).trim();
|
|
2468
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
2469
|
+
val = val.slice(1, -1);
|
|
2470
|
+
}
|
|
2471
|
+
if (!process.env[key]) {
|
|
2472
|
+
process.env[key] = val;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
1769
2477
|
// Run server in foreground (used by daemon)
|
|
1770
2478
|
const serviceEventsPath = path.join(projectRoot, ".jfl", "service-events.jsonl");
|
|
1771
2479
|
const mapPersistPath = path.join(projectRoot, ".jfl", "map-events.jsonl");
|
|
@@ -1779,6 +2487,77 @@ export async function contextHubCommand(action, options = {}) {
|
|
|
1779
2487
|
const flowEngine = new FlowEngine(eventBus, projectRoot);
|
|
1780
2488
|
const server = createServer(projectRoot, port, eventBus, flowEngine);
|
|
1781
2489
|
let isListening = false;
|
|
2490
|
+
// Cross-service scope impact detection (GTM/portfolio level)
|
|
2491
|
+
// When eval:scored fires with improved=true, detect which other services
|
|
2492
|
+
// are affected and emit scope:impact events for each
|
|
2493
|
+
const configPath = path.join(projectRoot, ".jfl", "config.json");
|
|
2494
|
+
const hubConfig = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf-8")) : {};
|
|
2495
|
+
if (hubConfig.type === "gtm" || hubConfig.type === "portfolio") {
|
|
2496
|
+
eventBus.subscribe({
|
|
2497
|
+
clientId: "scope-detector",
|
|
2498
|
+
patterns: ["eval:scored"],
|
|
2499
|
+
transport: "poll",
|
|
2500
|
+
callback: (event) => {
|
|
2501
|
+
if (event.data?.improved !== "true" && event.data?.improved !== true)
|
|
2502
|
+
return;
|
|
2503
|
+
const serviceName = event.data?.service || event.source || "unknown";
|
|
2504
|
+
const registeredServices = hubConfig.registered_services || [];
|
|
2505
|
+
// Find source service's produces
|
|
2506
|
+
const sourceReg = registeredServices.find((s) => s.name === serviceName);
|
|
2507
|
+
if (!sourceReg?.path)
|
|
2508
|
+
return;
|
|
2509
|
+
try {
|
|
2510
|
+
const svcConfigPath = path.join(sourceReg.path, ".jfl", "config.json");
|
|
2511
|
+
if (!fs.existsSync(svcConfigPath))
|
|
2512
|
+
return;
|
|
2513
|
+
const svcConfig = JSON.parse(fs.readFileSync(svcConfigPath, "utf-8"));
|
|
2514
|
+
const produces = svcConfig.context_scope?.produces || [];
|
|
2515
|
+
if (produces.length === 0)
|
|
2516
|
+
return;
|
|
2517
|
+
// Check each other service for consuming matches
|
|
2518
|
+
for (const otherSvc of registeredServices) {
|
|
2519
|
+
if (otherSvc.name === serviceName || !otherSvc.path)
|
|
2520
|
+
continue;
|
|
2521
|
+
const otherConfigPath = path.join(otherSvc.path, ".jfl", "config.json");
|
|
2522
|
+
if (!fs.existsSync(otherConfigPath))
|
|
2523
|
+
continue;
|
|
2524
|
+
const otherConfig = JSON.parse(fs.readFileSync(otherConfigPath, "utf-8"));
|
|
2525
|
+
const consumes = otherConfig.context_scope?.consumes || [];
|
|
2526
|
+
// Match produces against consumes
|
|
2527
|
+
const matched = [];
|
|
2528
|
+
for (const p of produces) {
|
|
2529
|
+
for (const c of consumes) {
|
|
2530
|
+
if (c === "*" || p === c || (c.endsWith(":*") && p.startsWith(c.slice(0, -1))) || (c.endsWith("*") && p.startsWith(c.slice(0, -1)))) {
|
|
2531
|
+
matched.push(`${p} → ${c}`);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
if (matched.length > 0) {
|
|
2536
|
+
const ts = new Date().toISOString();
|
|
2537
|
+
console.log(`[${ts}] scope:impact — ${serviceName} → ${otherSvc.name} (${matched.length} patterns)`);
|
|
2538
|
+
eventBus.emit({
|
|
2539
|
+
type: "scope:impact",
|
|
2540
|
+
source: "scope-detector",
|
|
2541
|
+
data: {
|
|
2542
|
+
source_service: serviceName,
|
|
2543
|
+
affected_service: otherSvc.name,
|
|
2544
|
+
affected_service_path: otherSvc.path,
|
|
2545
|
+
scope_patterns: matched,
|
|
2546
|
+
source_pr: event.data?.pr_number || "",
|
|
2547
|
+
change_description: event.data?.branch || "eval improvement",
|
|
2548
|
+
source_delta: event.data?.delta || "0",
|
|
2549
|
+
},
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
catch (err) {
|
|
2555
|
+
console.error(`[scope-detector] Error checking impact for ${serviceName}:`, err.message);
|
|
2556
|
+
}
|
|
2557
|
+
},
|
|
2558
|
+
});
|
|
2559
|
+
console.log(`[scope-detector] Cross-service impact detection enabled for ${hubConfig.type} hub`);
|
|
2560
|
+
}
|
|
1782
2561
|
// When spawned as daemon, ignore SIGTERM during startup grace period.
|
|
1783
2562
|
// The parent process (hook runner) may exit and send SIGTERM to the
|
|
1784
2563
|
// process group before we're fully detached. After grace period,
|