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.
Files changed (428) hide show
  1. package/README.md +15 -5
  2. package/dist/commands/context-hub.d.ts.map +1 -1
  3. package/dist/commands/context-hub.js +818 -39
  4. package/dist/commands/context-hub.js.map +1 -1
  5. package/dist/commands/eval.d.ts +1 -1
  6. package/dist/commands/eval.d.ts.map +1 -1
  7. package/dist/commands/eval.js +192 -1
  8. package/dist/commands/eval.js.map +1 -1
  9. package/dist/commands/findings.d.ts +6 -0
  10. package/dist/commands/findings.d.ts.map +1 -0
  11. package/dist/commands/findings.js +203 -0
  12. package/dist/commands/findings.js.map +1 -0
  13. package/dist/commands/hud.d.ts.map +1 -1
  14. package/dist/commands/hud.js +47 -9
  15. package/dist/commands/hud.js.map +1 -1
  16. package/dist/commands/ide.d.ts +27 -0
  17. package/dist/commands/ide.d.ts.map +1 -0
  18. package/dist/commands/ide.js +546 -0
  19. package/dist/commands/ide.js.map +1 -0
  20. package/dist/commands/onboard.d.ts.map +1 -1
  21. package/dist/commands/onboard.js +212 -2
  22. package/dist/commands/onboard.js.map +1 -1
  23. package/dist/commands/openclaw.d.ts +3 -0
  24. package/dist/commands/openclaw.d.ts.map +1 -1
  25. package/dist/commands/openclaw.js +76 -2
  26. package/dist/commands/openclaw.js.map +1 -1
  27. package/dist/commands/peter.d.ts +3 -0
  28. package/dist/commands/peter.d.ts.map +1 -1
  29. package/dist/commands/peter.js +1218 -2
  30. package/dist/commands/peter.js.map +1 -1
  31. package/dist/commands/pi-fleet.d.ts +18 -0
  32. package/dist/commands/pi-fleet.d.ts.map +1 -0
  33. package/dist/commands/pi-fleet.js +382 -0
  34. package/dist/commands/pi-fleet.js.map +1 -0
  35. package/dist/commands/pi.d.ts.map +1 -1
  36. package/dist/commands/pi.js +18 -3
  37. package/dist/commands/pi.js.map +1 -1
  38. package/dist/commands/scope.d.ts.map +1 -1
  39. package/dist/commands/scope.js +90 -1
  40. package/dist/commands/scope.js.map +1 -1
  41. package/dist/commands/services.d.ts.map +1 -1
  42. package/dist/commands/services.js +18 -0
  43. package/dist/commands/services.js.map +1 -1
  44. package/dist/commands/status.d.ts.map +1 -1
  45. package/dist/commands/status.js +22 -4
  46. package/dist/commands/status.js.map +1 -1
  47. package/dist/commands/viz.d.ts.map +1 -1
  48. package/dist/commands/viz.js +417 -0
  49. package/dist/commands/viz.js.map +1 -1
  50. package/dist/dashboard-static/assets/index-B6b867Pv.js +121 -0
  51. package/dist/dashboard-static/assets/index-Y4BrqxV-.css +1 -0
  52. package/dist/dashboard-static/index.html +2 -2
  53. package/dist/index.js +228 -62
  54. package/dist/index.js.map +1 -1
  55. package/dist/lib/agent-config.d.ts +52 -0
  56. package/dist/lib/agent-config.d.ts.map +1 -0
  57. package/dist/lib/agent-config.js +231 -0
  58. package/dist/lib/agent-config.js.map +1 -0
  59. package/dist/lib/agent-generator.d.ts +10 -0
  60. package/dist/lib/agent-generator.d.ts.map +1 -1
  61. package/dist/lib/agent-generator.js +64 -10
  62. package/dist/lib/agent-generator.js.map +1 -1
  63. package/dist/lib/agent-session.d.ts +104 -0
  64. package/dist/lib/agent-session.d.ts.map +1 -0
  65. package/dist/lib/agent-session.js +627 -0
  66. package/dist/lib/agent-session.js.map +1 -0
  67. package/dist/lib/eval-snapshot.d.ts +47 -0
  68. package/dist/lib/eval-snapshot.d.ts.map +1 -0
  69. package/dist/lib/eval-snapshot.js +315 -0
  70. package/dist/lib/eval-snapshot.js.map +1 -0
  71. package/dist/lib/eval-store.d.ts +5 -0
  72. package/dist/lib/eval-store.d.ts.map +1 -1
  73. package/dist/lib/eval-store.js +33 -3
  74. package/dist/lib/eval-store.js.map +1 -1
  75. package/dist/lib/findings-engine.d.ts +51 -0
  76. package/dist/lib/findings-engine.d.ts.map +1 -0
  77. package/dist/lib/findings-engine.js +338 -0
  78. package/dist/lib/findings-engine.js.map +1 -0
  79. package/dist/lib/flow-engine.d.ts +8 -0
  80. package/dist/lib/flow-engine.d.ts.map +1 -1
  81. package/dist/lib/flow-engine.js +84 -2
  82. package/dist/lib/flow-engine.js.map +1 -1
  83. package/dist/lib/hub-client.d.ts +1 -0
  84. package/dist/lib/hub-client.d.ts.map +1 -1
  85. package/dist/lib/hub-client.js +33 -6
  86. package/dist/lib/hub-client.js.map +1 -1
  87. package/dist/lib/ide-panes.d.ts +58 -0
  88. package/dist/lib/ide-panes.d.ts.map +1 -0
  89. package/dist/lib/ide-panes.js +508 -0
  90. package/dist/lib/ide-panes.js.map +1 -0
  91. package/dist/lib/memory-db.js +4 -4
  92. package/dist/lib/memory-db.js.map +1 -1
  93. package/dist/lib/memory-indexer.d.ts.map +1 -1
  94. package/dist/lib/memory-indexer.js +3 -0
  95. package/dist/lib/memory-indexer.js.map +1 -1
  96. package/dist/lib/memory-search.d.ts +148 -4
  97. package/dist/lib/memory-search.d.ts.map +1 -1
  98. package/dist/lib/memory-search.js +496 -58
  99. package/dist/lib/memory-search.js.map +1 -1
  100. package/dist/lib/meta-orchestrator.d.ts +104 -0
  101. package/dist/lib/meta-orchestrator.d.ts.map +1 -0
  102. package/dist/lib/meta-orchestrator.js +373 -0
  103. package/dist/lib/meta-orchestrator.js.map +1 -0
  104. package/dist/lib/peer-agent-generator.d.ts.map +1 -1
  105. package/dist/lib/peer-agent-generator.js +43 -19
  106. package/dist/lib/peer-agent-generator.js.map +1 -1
  107. package/dist/lib/policy-head.d.ts +25 -0
  108. package/dist/lib/policy-head.d.ts.map +1 -0
  109. package/dist/lib/policy-head.js +136 -0
  110. package/dist/lib/policy-head.js.map +1 -0
  111. package/dist/lib/replay-buffer.d.ts +93 -0
  112. package/dist/lib/replay-buffer.d.ts.map +1 -0
  113. package/dist/lib/replay-buffer.js +302 -0
  114. package/dist/lib/replay-buffer.js.map +1 -0
  115. package/dist/lib/sentinel-rl.d.ts +97 -0
  116. package/dist/lib/sentinel-rl.d.ts.map +1 -0
  117. package/dist/lib/sentinel-rl.js +430 -0
  118. package/dist/lib/sentinel-rl.js.map +1 -0
  119. package/dist/lib/session-lock.d.ts +61 -0
  120. package/dist/lib/session-lock.d.ts.map +1 -0
  121. package/dist/lib/session-lock.js +438 -0
  122. package/dist/lib/session-lock.js.map +1 -0
  123. package/dist/lib/stratus-client.d.ts +1 -0
  124. package/dist/lib/stratus-client.d.ts.map +1 -1
  125. package/dist/lib/stratus-client.js +24 -2
  126. package/dist/lib/stratus-client.js.map +1 -1
  127. package/dist/lib/telemetry-agent-v2.d.ts +128 -0
  128. package/dist/lib/telemetry-agent-v2.d.ts.map +1 -0
  129. package/dist/lib/telemetry-agent-v2.js +1042 -0
  130. package/dist/lib/telemetry-agent-v2.js.map +1 -0
  131. package/dist/lib/telemetry-agent.d.ts.map +1 -1
  132. package/dist/lib/telemetry-agent.js +27 -6
  133. package/dist/lib/telemetry-agent.js.map +1 -1
  134. package/dist/lib/telemetry-digest.d.ts.map +1 -1
  135. package/dist/lib/telemetry-digest.js +27 -5
  136. package/dist/lib/telemetry-digest.js.map +1 -1
  137. package/dist/lib/telemetry.d.ts.map +1 -1
  138. package/dist/lib/telemetry.js +29 -4
  139. package/dist/lib/telemetry.js.map +1 -1
  140. package/dist/lib/text-preprocessing.d.ts +83 -0
  141. package/dist/lib/text-preprocessing.d.ts.map +1 -0
  142. package/dist/lib/text-preprocessing.js +261 -0
  143. package/dist/lib/text-preprocessing.js.map +1 -0
  144. package/dist/lib/training-buffer.d.ts +86 -0
  145. package/dist/lib/training-buffer.d.ts.map +1 -0
  146. package/dist/lib/training-buffer.js +139 -0
  147. package/dist/lib/training-buffer.js.map +1 -0
  148. package/dist/lib/tuple-miner.d.ts +30 -0
  149. package/dist/lib/tuple-miner.d.ts.map +1 -0
  150. package/dist/lib/tuple-miner.js +427 -0
  151. package/dist/lib/tuple-miner.js.map +1 -0
  152. package/dist/lib/vm-backend.d.ts +72 -0
  153. package/dist/lib/vm-backend.d.ts.map +1 -0
  154. package/dist/lib/vm-backend.js +175 -0
  155. package/dist/lib/vm-backend.js.map +1 -0
  156. package/dist/lib/workspace/backend.d.ts +53 -0
  157. package/dist/lib/workspace/backend.d.ts.map +1 -0
  158. package/dist/lib/workspace/backend.js +37 -0
  159. package/dist/lib/workspace/backend.js.map +1 -0
  160. package/dist/lib/workspace/cmux-adapter.d.ts +46 -0
  161. package/dist/lib/workspace/cmux-adapter.d.ts.map +1 -0
  162. package/dist/lib/workspace/cmux-adapter.js +261 -0
  163. package/dist/lib/workspace/cmux-adapter.js.map +1 -0
  164. package/dist/lib/workspace/data-pipeline.d.ts +35 -0
  165. package/dist/lib/workspace/data-pipeline.d.ts.map +1 -0
  166. package/dist/lib/workspace/data-pipeline.js +463 -0
  167. package/dist/lib/workspace/data-pipeline.js.map +1 -0
  168. package/dist/lib/workspace/engine.d.ts +64 -0
  169. package/dist/lib/workspace/engine.d.ts.map +1 -0
  170. package/dist/lib/workspace/engine.js +397 -0
  171. package/dist/lib/workspace/engine.js.map +1 -0
  172. package/dist/lib/workspace/notifications.d.ts +14 -0
  173. package/dist/lib/workspace/notifications.d.ts.map +1 -0
  174. package/dist/lib/workspace/notifications.js +41 -0
  175. package/dist/lib/workspace/notifications.js.map +1 -0
  176. package/dist/lib/workspace/surface-registry.d.ts +49 -0
  177. package/dist/lib/workspace/surface-registry.d.ts.map +1 -0
  178. package/dist/lib/workspace/surface-registry.js +217 -0
  179. package/dist/lib/workspace/surface-registry.js.map +1 -0
  180. package/dist/lib/workspace/surface-type.d.ts +153 -0
  181. package/dist/lib/workspace/surface-type.d.ts.map +1 -0
  182. package/dist/lib/workspace/surface-type.js +9 -0
  183. package/dist/lib/workspace/surface-type.js.map +1 -0
  184. package/dist/lib/workspace/surfaces/agent-overview.d.ts +16 -0
  185. package/dist/lib/workspace/surfaces/agent-overview.d.ts.map +1 -0
  186. package/dist/lib/workspace/surfaces/agent-overview.js +116 -0
  187. package/dist/lib/workspace/surfaces/agent-overview.js.map +1 -0
  188. package/dist/lib/workspace/surfaces/agent.d.ts +16 -0
  189. package/dist/lib/workspace/surfaces/agent.d.ts.map +1 -0
  190. package/dist/lib/workspace/surfaces/agent.js +112 -0
  191. package/dist/lib/workspace/surfaces/agent.js.map +1 -0
  192. package/dist/lib/workspace/surfaces/claude.d.ts +15 -0
  193. package/dist/lib/workspace/surfaces/claude.d.ts.map +1 -0
  194. package/dist/lib/workspace/surfaces/claude.js +23 -0
  195. package/dist/lib/workspace/surfaces/claude.js.map +1 -0
  196. package/dist/lib/workspace/surfaces/dashboard.d.ts +21 -0
  197. package/dist/lib/workspace/surfaces/dashboard.d.ts.map +1 -0
  198. package/dist/lib/workspace/surfaces/dashboard.js +32 -0
  199. package/dist/lib/workspace/surfaces/dashboard.js.map +1 -0
  200. package/dist/lib/workspace/surfaces/eval.d.ts +15 -0
  201. package/dist/lib/workspace/surfaces/eval.d.ts.map +1 -0
  202. package/dist/lib/workspace/surfaces/eval.js +42 -0
  203. package/dist/lib/workspace/surfaces/eval.js.map +1 -0
  204. package/dist/lib/workspace/surfaces/event-stream.d.ts +16 -0
  205. package/dist/lib/workspace/surfaces/event-stream.d.ts.map +1 -0
  206. package/dist/lib/workspace/surfaces/event-stream.js +40 -0
  207. package/dist/lib/workspace/surfaces/event-stream.js.map +1 -0
  208. package/dist/lib/workspace/surfaces/flow.d.ts +16 -0
  209. package/dist/lib/workspace/surfaces/flow.d.ts.map +1 -0
  210. package/dist/lib/workspace/surfaces/flow.js +49 -0
  211. package/dist/lib/workspace/surfaces/flow.js.map +1 -0
  212. package/dist/lib/workspace/surfaces/index.d.ts +16 -0
  213. package/dist/lib/workspace/surfaces/index.d.ts.map +1 -0
  214. package/dist/lib/workspace/surfaces/index.js +16 -0
  215. package/dist/lib/workspace/surfaces/index.js.map +1 -0
  216. package/dist/lib/workspace/surfaces/portfolio.d.ts +16 -0
  217. package/dist/lib/workspace/surfaces/portfolio.d.ts.map +1 -0
  218. package/dist/lib/workspace/surfaces/portfolio.js +102 -0
  219. package/dist/lib/workspace/surfaces/portfolio.js.map +1 -0
  220. package/dist/lib/workspace/surfaces/service.d.ts +16 -0
  221. package/dist/lib/workspace/surfaces/service.d.ts.map +1 -0
  222. package/dist/lib/workspace/surfaces/service.js +45 -0
  223. package/dist/lib/workspace/surfaces/service.js.map +1 -0
  224. package/dist/lib/workspace/surfaces/shell.d.ts +15 -0
  225. package/dist/lib/workspace/surfaces/shell.d.ts.map +1 -0
  226. package/dist/lib/workspace/surfaces/shell.js +19 -0
  227. package/dist/lib/workspace/surfaces/shell.js.map +1 -0
  228. package/dist/lib/workspace/surfaces/telemetry.d.ts +16 -0
  229. package/dist/lib/workspace/surfaces/telemetry.d.ts.map +1 -0
  230. package/dist/lib/workspace/surfaces/telemetry.js +48 -0
  231. package/dist/lib/workspace/surfaces/telemetry.js.map +1 -0
  232. package/dist/lib/workspace/surfaces/topology.d.ts +15 -0
  233. package/dist/lib/workspace/surfaces/topology.d.ts.map +1 -0
  234. package/dist/lib/workspace/surfaces/topology.js +19 -0
  235. package/dist/lib/workspace/surfaces/topology.js.map +1 -0
  236. package/dist/lib/workspace/surfaces/training.d.ts +16 -0
  237. package/dist/lib/workspace/surfaces/training.d.ts.map +1 -0
  238. package/dist/lib/workspace/surfaces/training.js +22 -0
  239. package/dist/lib/workspace/surfaces/training.js.map +1 -0
  240. package/dist/lib/workspace/tmux-adapter.d.ts +27 -0
  241. package/dist/lib/workspace/tmux-adapter.d.ts.map +1 -0
  242. package/dist/lib/workspace/tmux-adapter.js +106 -0
  243. package/dist/lib/workspace/tmux-adapter.js.map +1 -0
  244. package/dist/mcp/context-hub-mcp.js +7 -24
  245. package/dist/mcp/context-hub-mcp.js.map +1 -1
  246. package/dist/types/flows.d.ts +2 -0
  247. package/dist/types/flows.d.ts.map +1 -1
  248. package/dist/types/ide.d.ts +49 -0
  249. package/dist/types/ide.d.ts.map +1 -0
  250. package/dist/types/ide.js +5 -0
  251. package/dist/types/ide.js.map +1 -0
  252. package/dist/types/platform-digest.d.ts +228 -0
  253. package/dist/types/platform-digest.d.ts.map +1 -0
  254. package/dist/types/platform-digest.js +5 -0
  255. package/dist/types/platform-digest.js.map +1 -0
  256. package/dist/types/telemetry-digest.d.ts +2 -0
  257. package/dist/types/telemetry-digest.d.ts.map +1 -1
  258. package/dist/utils/ensure-project.d.ts +1 -0
  259. package/dist/utils/ensure-project.d.ts.map +1 -1
  260. package/dist/utils/ensure-project.js +19 -7
  261. package/dist/utils/ensure-project.js.map +1 -1
  262. package/dist/utils/jfl-config.d.ts +1 -0
  263. package/dist/utils/jfl-config.d.ts.map +1 -1
  264. package/dist/utils/jfl-config.js +19 -1
  265. package/dist/utils/jfl-config.js.map +1 -1
  266. package/dist/utils/jfl-paths.d.ts +5 -0
  267. package/dist/utils/jfl-paths.d.ts.map +1 -1
  268. package/dist/utils/jfl-paths.js +25 -3
  269. package/dist/utils/jfl-paths.js.map +1 -1
  270. package/package.json +3 -2
  271. package/packages/pi/AGENTS.md +112 -0
  272. package/packages/pi/extensions/agent-grid.ts +191 -0
  273. package/packages/pi/extensions/agent-names.ts +178 -0
  274. package/packages/pi/extensions/autoresearch.ts +427 -0
  275. package/packages/pi/extensions/bookmarks.ts +85 -0
  276. package/packages/pi/extensions/context.ts +151 -0
  277. package/packages/pi/extensions/crm-tool.ts +61 -0
  278. package/packages/pi/extensions/eval-tool.ts +224 -0
  279. package/packages/pi/extensions/eval.ts +60 -0
  280. package/packages/pi/extensions/footer.ts +239 -0
  281. package/packages/pi/extensions/hud-tool.ts +145 -0
  282. package/packages/pi/extensions/index.ts +392 -0
  283. package/packages/pi/extensions/journal.ts +224 -0
  284. package/packages/pi/extensions/map-bridge.ts +178 -0
  285. package/packages/pi/extensions/memory-tool.ts +68 -0
  286. package/packages/pi/extensions/notifications.ts +73 -0
  287. package/packages/pi/extensions/peter-parker.ts +202 -0
  288. package/packages/pi/extensions/policy-head-tool.ts +276 -0
  289. package/packages/pi/extensions/portfolio-bridge.ts +90 -0
  290. package/packages/pi/extensions/session.ts +90 -0
  291. package/packages/pi/extensions/shortcuts.ts +259 -0
  292. package/packages/pi/extensions/stratus-bridge.ts +115 -0
  293. package/packages/pi/extensions/synopsis-tool.ts +83 -0
  294. package/packages/pi/extensions/tool-renderers.ts +352 -0
  295. package/packages/pi/extensions/training-buffer-tool.ts +368 -0
  296. package/packages/pi/extensions/types.ts +163 -0
  297. package/packages/pi/package-lock.json +346 -0
  298. package/packages/pi/package.json +44 -0
  299. package/packages/pi/skills/agent-browser/SKILL.md +116 -0
  300. package/packages/pi/skills/brand-architect/SKILL.md +240 -0
  301. package/packages/pi/skills/brand-architect/config.yaml +137 -0
  302. package/packages/pi/skills/campaign-hud/config.yaml +112 -0
  303. package/packages/pi/skills/content-creator/SKILL.md +294 -0
  304. package/packages/pi/skills/context/SKILL.md +65 -0
  305. package/packages/pi/skills/debug/MULTI_AGENT.md +360 -0
  306. package/packages/pi/skills/debug/SKILL.md +554 -0
  307. package/packages/pi/skills/end/SKILL.md +1782 -0
  308. package/packages/pi/skills/eval/SKILL.md +75 -0
  309. package/packages/pi/skills/fly-deploy/SKILL.md +676 -0
  310. package/packages/pi/skills/founder-video/SKILL.md +467 -0
  311. package/packages/pi/skills/hud/SKILL.md +160 -0
  312. package/packages/pi/skills/orchestrate/SKILL.md +74 -0
  313. package/packages/pi/skills/pi-agents/SKILL.md +78 -0
  314. package/packages/pi/skills/react-best-practices/AGENTS.md +2249 -0
  315. package/packages/pi/skills/react-best-practices/README.md +123 -0
  316. package/packages/pi/skills/react-best-practices/SKILL.md +125 -0
  317. package/packages/pi/skills/react-best-practices/metadata.json +15 -0
  318. package/packages/pi/skills/react-best-practices/rules/_sections.md +46 -0
  319. package/packages/pi/skills/react-best-practices/rules/_template.md +28 -0
  320. package/packages/pi/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  321. package/packages/pi/skills/react-best-practices/rules/advanced-use-latest.md +49 -0
  322. package/packages/pi/skills/react-best-practices/rules/async-api-routes.md +38 -0
  323. package/packages/pi/skills/react-best-practices/rules/async-defer-await.md +80 -0
  324. package/packages/pi/skills/react-best-practices/rules/async-dependencies.md +36 -0
  325. package/packages/pi/skills/react-best-practices/rules/async-parallel.md +28 -0
  326. package/packages/pi/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
  327. package/packages/pi/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
  328. package/packages/pi/skills/react-best-practices/rules/bundle-conditional.md +31 -0
  329. package/packages/pi/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
  330. package/packages/pi/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  331. package/packages/pi/skills/react-best-practices/rules/bundle-preload.md +50 -0
  332. package/packages/pi/skills/react-best-practices/rules/client-event-listeners.md +74 -0
  333. package/packages/pi/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
  334. package/packages/pi/skills/react-best-practices/rules/js-batch-dom-css.md +82 -0
  335. package/packages/pi/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
  336. package/packages/pi/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
  337. package/packages/pi/skills/react-best-practices/rules/js-cache-storage.md +70 -0
  338. package/packages/pi/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
  339. package/packages/pi/skills/react-best-practices/rules/js-early-exit.md +50 -0
  340. package/packages/pi/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
  341. package/packages/pi/skills/react-best-practices/rules/js-index-maps.md +37 -0
  342. package/packages/pi/skills/react-best-practices/rules/js-length-check-first.md +49 -0
  343. package/packages/pi/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
  344. package/packages/pi/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
  345. package/packages/pi/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
  346. package/packages/pi/skills/react-best-practices/rules/rendering-activity.md +26 -0
  347. package/packages/pi/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  348. package/packages/pi/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
  349. package/packages/pi/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
  350. package/packages/pi/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  351. package/packages/pi/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  352. package/packages/pi/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
  353. package/packages/pi/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
  354. package/packages/pi/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
  355. package/packages/pi/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
  356. package/packages/pi/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
  357. package/packages/pi/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  358. package/packages/pi/skills/react-best-practices/rules/rerender-memo.md +44 -0
  359. package/packages/pi/skills/react-best-practices/rules/rerender-transitions.md +40 -0
  360. package/packages/pi/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
  361. package/packages/pi/skills/react-best-practices/rules/server-cache-lru.md +41 -0
  362. package/packages/pi/skills/react-best-practices/rules/server-cache-react.md +26 -0
  363. package/packages/pi/skills/react-best-practices/rules/server-parallel-fetching.md +79 -0
  364. package/packages/pi/skills/react-best-practices/rules/server-serialization.md +38 -0
  365. package/packages/pi/skills/remotion-best-practices/SKILL.md +43 -0
  366. package/packages/pi/skills/remotion-best-practices/rules/3d.md +86 -0
  367. package/packages/pi/skills/remotion-best-practices/rules/animations.md +29 -0
  368. package/packages/pi/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
  369. package/packages/pi/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
  370. package/packages/pi/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
  371. package/packages/pi/skills/remotion-best-practices/rules/assets.md +78 -0
  372. package/packages/pi/skills/remotion-best-practices/rules/audio.md +172 -0
  373. package/packages/pi/skills/remotion-best-practices/rules/calculate-metadata.md +104 -0
  374. package/packages/pi/skills/remotion-best-practices/rules/can-decode.md +75 -0
  375. package/packages/pi/skills/remotion-best-practices/rules/charts.md +58 -0
  376. package/packages/pi/skills/remotion-best-practices/rules/compositions.md +146 -0
  377. package/packages/pi/skills/remotion-best-practices/rules/display-captions.md +126 -0
  378. package/packages/pi/skills/remotion-best-practices/rules/extract-frames.md +229 -0
  379. package/packages/pi/skills/remotion-best-practices/rules/fonts.md +152 -0
  380. package/packages/pi/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
  381. package/packages/pi/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
  382. package/packages/pi/skills/remotion-best-practices/rules/get-video-duration.md +58 -0
  383. package/packages/pi/skills/remotion-best-practices/rules/gifs.md +138 -0
  384. package/packages/pi/skills/remotion-best-practices/rules/images.md +130 -0
  385. package/packages/pi/skills/remotion-best-practices/rules/import-srt-captions.md +67 -0
  386. package/packages/pi/skills/remotion-best-practices/rules/lottie.md +68 -0
  387. package/packages/pi/skills/remotion-best-practices/rules/measuring-dom-nodes.md +35 -0
  388. package/packages/pi/skills/remotion-best-practices/rules/measuring-text.md +143 -0
  389. package/packages/pi/skills/remotion-best-practices/rules/sequencing.md +106 -0
  390. package/packages/pi/skills/remotion-best-practices/rules/tailwind.md +11 -0
  391. package/packages/pi/skills/remotion-best-practices/rules/text-animations.md +20 -0
  392. package/packages/pi/skills/remotion-best-practices/rules/timing.md +179 -0
  393. package/packages/pi/skills/remotion-best-practices/rules/transcribe-captions.md +19 -0
  394. package/packages/pi/skills/remotion-best-practices/rules/transitions.md +122 -0
  395. package/packages/pi/skills/remotion-best-practices/rules/trimming.md +53 -0
  396. package/packages/pi/skills/remotion-best-practices/rules/videos.md +171 -0
  397. package/packages/pi/skills/search/SKILL.md +220 -0
  398. package/packages/pi/skills/spec/SKILL.md +377 -0
  399. package/packages/pi/skills/startup/SKILL.md +315 -0
  400. package/packages/pi/skills/web-architect/SKILL.md +309 -0
  401. package/packages/pi/skills/x-algorithm/SKILL.md +305 -0
  402. package/packages/pi/teams/dev-team.yaml +63 -0
  403. package/packages/pi/teams/gtm-team.yaml +79 -0
  404. package/packages/pi/themes/jfl.theme.json +76 -0
  405. package/packages/pi/tsconfig.json +21 -0
  406. package/scripts/collect-tuples.sh +124 -0
  407. package/scripts/destroy-fleet.sh +37 -0
  408. package/scripts/jfl-ide.sh +48 -0
  409. package/scripts/session/session-cleanup.sh +4 -11
  410. package/scripts/session/session-init.sh +6 -0
  411. package/scripts/session/session-sync.sh +25 -0
  412. package/scripts/setup-branch-protection.sh +106 -0
  413. package/scripts/spawn-fleet.sh +144 -0
  414. package/scripts/train-policy-head.py +434 -0
  415. package/scripts/vm-swarm/README.md +301 -0
  416. package/scripts/vm-swarm/collect-tuples.sh +331 -0
  417. package/scripts/vm-swarm/create-base-template.sh +339 -0
  418. package/scripts/vm-swarm/kill-fleet.sh +204 -0
  419. package/scripts/vm-swarm/monitor-fleet.sh +346 -0
  420. package/scripts/vm-swarm/spawn-fleet.sh +304 -0
  421. package/template/.github/workflows/jfl-eval.yml +105 -8
  422. package/template/.github/workflows/jfl-review.yml +4 -0
  423. package/template/scripts/session/session-end.sh +69 -6
  424. package/template/scripts/session/session-init.sh +55 -30
  425. package/template/scripts/session/session-lock.sh +464 -0
  426. package/template/templates/service-agent/workflows/jfl-eval.yml +19 -0
  427. package/dist/dashboard-static/assets/index-B6kRK9Rq.js +0 -116
  428. 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), // Truncate for context
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: false
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 results = await searchMemories(query, { type, maxItems, since });
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
- try {
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
- try {
1304
- const response = await fetch(`http://localhost:${port}/health`, {
1305
- signal: AbortSignal.timeout(2000)
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,