@provos/ironcurtain 0.9.1 → 0.11.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 (502) hide show
  1. package/README.md +69 -10
  2. package/dist/auth/oauth-provider.js +1 -4
  3. package/dist/auth/oauth-provider.js.map +1 -1
  4. package/dist/cli.js +40 -2
  5. package/dist/cli.js.map +1 -1
  6. package/dist/config/config-command.js +174 -10
  7. package/dist/config/config-command.js.map +1 -1
  8. package/dist/config/first-start.js +61 -0
  9. package/dist/config/first-start.js.map +1 -1
  10. package/dist/config/generated/compiled-policy.json +528 -161
  11. package/dist/config/generated/test-scenarios.json +4810 -170
  12. package/dist/config/generated/tool-annotations.json +34 -47
  13. package/dist/config/index.d.ts +58 -0
  14. package/dist/config/index.js +97 -15
  15. package/dist/config/index.js.map +1 -1
  16. package/dist/config/mcp-servers.json +2 -2
  17. package/dist/config/model-provider.d.ts +8 -2
  18. package/dist/config/model-provider.js +52 -9
  19. package/dist/config/model-provider.js.map +1 -1
  20. package/dist/config/paths.d.ts +181 -0
  21. package/dist/config/paths.js +326 -23
  22. package/dist/config/paths.js.map +1 -1
  23. package/dist/config/types.d.ts +7 -1
  24. package/dist/config/user-config.d.ts +61 -4
  25. package/dist/config/user-config.js +108 -21
  26. package/dist/config/user-config.js.map +1 -1
  27. package/dist/config/validate-policy-dir.d.ts +10 -0
  28. package/dist/config/validate-policy-dir.js +59 -0
  29. package/dist/config/validate-policy-dir.js.map +1 -0
  30. package/dist/cron/compile-task-policy.js +1 -0
  31. package/dist/cron/compile-task-policy.js.map +1 -1
  32. package/dist/cron/job-commands.js +22 -2
  33. package/dist/cron/job-commands.js.map +1 -1
  34. package/dist/cron/types.d.ts +16 -2
  35. package/dist/cron/types.js +1 -3
  36. package/dist/cron/types.js.map +1 -1
  37. package/dist/daemon/daemon-command.js +16 -2
  38. package/dist/daemon/daemon-command.js.map +1 -1
  39. package/dist/daemon/ironcurtain-daemon.d.ts +44 -1
  40. package/dist/daemon/ironcurtain-daemon.js +210 -8
  41. package/dist/daemon/ironcurtain-daemon.js.map +1 -1
  42. package/dist/docker/adapters/claude-code.d.ts +2 -1
  43. package/dist/docker/adapters/claude-code.js +320 -131
  44. package/dist/docker/adapters/claude-code.js.map +1 -1
  45. package/dist/docker/adapters/goose.d.ts +12 -11
  46. package/dist/docker/adapters/goose.js +80 -27
  47. package/dist/docker/adapters/goose.js.map +1 -1
  48. package/dist/docker/agent-adapter.d.ts +178 -4
  49. package/dist/docker/agent-adapter.js +14 -0
  50. package/dist/docker/agent-adapter.js.map +1 -1
  51. package/dist/docker/agent-registry.js +3 -2
  52. package/dist/docker/agent-registry.js.map +1 -1
  53. package/dist/docker/code-mode-proxy.d.ts +27 -0
  54. package/dist/docker/code-mode-proxy.js +3 -0
  55. package/dist/docker/code-mode-proxy.js.map +1 -1
  56. package/dist/docker/container-lifecycle.d.ts +15 -0
  57. package/dist/docker/container-lifecycle.js +32 -0
  58. package/dist/docker/container-lifecycle.js.map +1 -0
  59. package/dist/docker/docker-agent-session.d.ts +113 -62
  60. package/dist/docker/docker-agent-session.js +241 -300
  61. package/dist/docker/docker-agent-session.js.map +1 -1
  62. package/dist/docker/docker-infrastructure.d.ts +256 -21
  63. package/dist/docker/docker-infrastructure.js +585 -30
  64. package/dist/docker/docker-infrastructure.js.map +1 -1
  65. package/dist/docker/docker-manager.d.ts +36 -1
  66. package/dist/docker/docker-manager.js +168 -26
  67. package/dist/docker/docker-manager.js.map +1 -1
  68. package/dist/docker/docker-progress-sink.d.ts +43 -0
  69. package/dist/docker/docker-progress-sink.js +299 -0
  70. package/dist/docker/docker-progress-sink.js.map +1 -0
  71. package/dist/docker/mitm-proxy.d.ts +90 -1
  72. package/dist/docker/mitm-proxy.js +720 -40
  73. package/dist/docker/mitm-proxy.js.map +1 -1
  74. package/dist/docker/oauth-credentials.d.ts +46 -3
  75. package/dist/docker/oauth-credentials.js +56 -14
  76. package/dist/docker/oauth-credentials.js.map +1 -1
  77. package/dist/docker/oauth-token-manager.js +2 -2
  78. package/dist/docker/oauth-token-manager.js.map +1 -1
  79. package/dist/docker/orientation.d.ts +1 -1
  80. package/dist/docker/provider-config.d.ts +81 -2
  81. package/dist/docker/provider-config.js +222 -28
  82. package/dist/docker/provider-config.js.map +1 -1
  83. package/dist/docker/proxy-tools.d.ts +0 -2
  84. package/dist/docker/proxy-tools.js +2 -7
  85. package/dist/docker/proxy-tools.js.map +1 -1
  86. package/dist/docker/pty-session.d.ts +49 -1
  87. package/dist/docker/pty-session.js +212 -90
  88. package/dist/docker/pty-session.js.map +1 -1
  89. package/dist/docker/pty-types.d.ts +0 -2
  90. package/dist/docker/pty-types.js +0 -2
  91. package/dist/docker/pty-types.js.map +1 -1
  92. package/dist/docker/resource-limits.d.ts +125 -0
  93. package/dist/docker/resource-limits.js +200 -0
  94. package/dist/docker/resource-limits.js.map +1 -0
  95. package/dist/docker/spawn-with-idle-timeout.d.ts +39 -0
  96. package/dist/docker/spawn-with-idle-timeout.js +152 -0
  97. package/dist/docker/spawn-with-idle-timeout.js.map +1 -0
  98. package/dist/docker/sse-extractor.d.ts +51 -0
  99. package/dist/docker/sse-extractor.js +303 -0
  100. package/dist/docker/sse-extractor.js.map +1 -0
  101. package/dist/docker/token-stream-bus.d.ts +83 -0
  102. package/dist/docker/token-stream-bus.js +98 -0
  103. package/dist/docker/token-stream-bus.js.map +1 -0
  104. package/dist/docker/token-stream-types.d.ts +71 -0
  105. package/dist/docker/token-stream-types.js +8 -0
  106. package/dist/docker/token-stream-types.js.map +1 -0
  107. package/dist/docker/types.d.ts +61 -6
  108. package/dist/doctor/checks.d.ts +138 -0
  109. package/dist/doctor/checks.js +511 -0
  110. package/dist/doctor/checks.js.map +1 -0
  111. package/dist/doctor/doctor-command.d.ts +29 -0
  112. package/dist/doctor/doctor-command.js +160 -0
  113. package/dist/doctor/doctor-command.js.map +1 -0
  114. package/dist/doctor/mcp-liveness.d.ts +28 -0
  115. package/dist/doctor/mcp-liveness.js +84 -0
  116. package/dist/doctor/mcp-liveness.js.map +1 -0
  117. package/dist/doctor/oauth-checks.d.ts +23 -0
  118. package/dist/doctor/oauth-checks.js +157 -0
  119. package/dist/doctor/oauth-checks.js.map +1 -0
  120. package/dist/doctor/output.d.ts +26 -0
  121. package/dist/doctor/output.js +82 -0
  122. package/dist/doctor/output.js.map +1 -0
  123. package/dist/escalation/listener-command.js +0 -3
  124. package/dist/escalation/listener-command.js.map +1 -1
  125. package/dist/event-bus/typed-event-bus.d.ts +26 -0
  126. package/dist/event-bus/typed-event-bus.js +35 -0
  127. package/dist/event-bus/typed-event-bus.js.map +1 -0
  128. package/dist/index.js +32 -9
  129. package/dist/index.js.map +1 -1
  130. package/dist/logger.js +31 -2
  131. package/dist/logger.js.map +1 -1
  132. package/dist/memory/auto-save.d.ts +18 -4
  133. package/dist/memory/auto-save.js +46 -6
  134. package/dist/memory/auto-save.js.map +1 -1
  135. package/dist/memory/memory-policy.d.ts +41 -0
  136. package/dist/memory/memory-policy.js +36 -0
  137. package/dist/memory/memory-policy.js.map +1 -0
  138. package/dist/mux/mux-app.d.ts +2 -0
  139. package/dist/mux/mux-app.js +30 -34
  140. package/dist/mux/mux-app.js.map +1 -1
  141. package/dist/mux/mux-command.d.ts +10 -1
  142. package/dist/mux/mux-command.js +62 -35
  143. package/dist/mux/mux-command.js.map +1 -1
  144. package/dist/mux/mux-renderer.js +22 -17
  145. package/dist/mux/mux-renderer.js.map +1 -1
  146. package/dist/mux/pty-bridge.d.ts +2 -0
  147. package/dist/mux/pty-bridge.js +3 -0
  148. package/dist/mux/pty-bridge.js.map +1 -1
  149. package/dist/mux/session-scanner.d.ts +7 -0
  150. package/dist/mux/session-scanner.js +21 -2
  151. package/dist/mux/session-scanner.js.map +1 -1
  152. package/dist/mux/types.d.ts +0 -4
  153. package/dist/mux/types.js.map +1 -1
  154. package/dist/{pipeline → observability}/llm-logger.d.ts +9 -3
  155. package/dist/{pipeline → observability}/llm-logger.js +16 -4
  156. package/dist/observability/llm-logger.js.map +1 -0
  157. package/dist/observe/observe-command.d.ts +12 -0
  158. package/dist/observe/observe-command.js +298 -0
  159. package/dist/observe/observe-command.js.map +1 -0
  160. package/dist/observe/observe-renderer.d.ts +24 -0
  161. package/dist/observe/observe-renderer.js +87 -0
  162. package/dist/observe/observe-renderer.js.map +1 -0
  163. package/dist/observe/observe-tui-rain.d.ts +42 -0
  164. package/dist/observe/observe-tui-rain.js +531 -0
  165. package/dist/observe/observe-tui-rain.js.map +1 -0
  166. package/dist/observe/observe-tui-text-panel.d.ts +46 -0
  167. package/dist/observe/observe-tui-text-panel.js +525 -0
  168. package/dist/observe/observe-tui-text-panel.js.map +1 -0
  169. package/dist/observe/observe-tui-types.d.ts +270 -0
  170. package/dist/observe/observe-tui-types.js +200 -0
  171. package/dist/observe/observe-tui-types.js.map +1 -0
  172. package/dist/observe/observe-tui-word-scorer.d.ts +143 -0
  173. package/dist/observe/observe-tui-word-scorer.js +572 -0
  174. package/dist/observe/observe-tui-word-scorer.js.map +1 -0
  175. package/dist/observe/observe-tui.d.ts +59 -0
  176. package/dist/observe/observe-tui.js +592 -0
  177. package/dist/observe/observe-tui.js.map +1 -0
  178. package/dist/persona/compile-persona-policy.js +1 -0
  179. package/dist/persona/compile-persona-policy.js.map +1 -1
  180. package/dist/persona/memory-gate.d.ts +19 -0
  181. package/dist/persona/memory-gate.js +33 -0
  182. package/dist/persona/memory-gate.js.map +1 -0
  183. package/dist/persona/persona-command.js +41 -6
  184. package/dist/persona/persona-command.js.map +1 -1
  185. package/dist/persona/resolve.d.ts +22 -0
  186. package/dist/persona/resolve.js +45 -2
  187. package/dist/persona/resolve.js.map +1 -1
  188. package/dist/persona/types.d.ts +16 -0
  189. package/dist/pipeline/annotate.d.ts +7 -1
  190. package/dist/pipeline/annotate.js +92 -2
  191. package/dist/pipeline/annotate.js.map +1 -1
  192. package/dist/pipeline/compile.js +2 -0
  193. package/dist/pipeline/compile.js.map +1 -1
  194. package/dist/pipeline/constitution-compiler.d.ts +25 -14
  195. package/dist/pipeline/constitution-compiler.js +107 -38
  196. package/dist/pipeline/constitution-compiler.js.map +1 -1
  197. package/dist/pipeline/dynamic-list-types.d.ts +3 -6
  198. package/dist/pipeline/dynamic-list-types.js +3 -16
  199. package/dist/pipeline/dynamic-list-types.js.map +1 -1
  200. package/dist/pipeline/parallel-progress.d.ts +59 -0
  201. package/dist/pipeline/parallel-progress.js +263 -0
  202. package/dist/pipeline/parallel-progress.js.map +1 -0
  203. package/dist/pipeline/pipeline-runner.d.ts +49 -8
  204. package/dist/pipeline/pipeline-runner.js +419 -231
  205. package/dist/pipeline/pipeline-runner.js.map +1 -1
  206. package/dist/pipeline/pipeline-shared.d.ts +59 -1
  207. package/dist/pipeline/pipeline-shared.js +127 -12
  208. package/dist/pipeline/pipeline-shared.js.map +1 -1
  209. package/dist/pipeline/policy-verifier.js +16 -3
  210. package/dist/pipeline/policy-verifier.js.map +1 -1
  211. package/dist/pipeline/proxy-mcp-connections.js +17 -2
  212. package/dist/pipeline/proxy-mcp-connections.js.map +1 -1
  213. package/dist/pipeline/scenario-generator.d.ts +4 -8
  214. package/dist/pipeline/scenario-generator.js +13 -46
  215. package/dist/pipeline/scenario-generator.js.map +1 -1
  216. package/dist/pipeline/server-prefilter.d.ts +31 -0
  217. package/dist/pipeline/server-prefilter.js +115 -0
  218. package/dist/pipeline/server-prefilter.js.map +1 -0
  219. package/dist/pipeline/types.d.ts +5 -0
  220. package/dist/sandbox/index.d.ts +14 -0
  221. package/dist/sandbox/index.js +335 -116
  222. package/dist/sandbox/index.js.map +1 -1
  223. package/dist/sandbox/ironcurtain-protocol.d.ts +52 -0
  224. package/dist/sandbox/ironcurtain-protocol.js +185 -0
  225. package/dist/sandbox/ironcurtain-protocol.js.map +1 -0
  226. package/dist/session/agent-session.d.ts +6 -0
  227. package/dist/session/agent-session.js +16 -5
  228. package/dist/session/agent-session.js.map +1 -1
  229. package/dist/session/cli-transport.js +0 -6
  230. package/dist/session/cli-transport.js.map +1 -1
  231. package/dist/session/index.d.ts +35 -14
  232. package/dist/session/index.js +324 -130
  233. package/dist/session/index.js.map +1 -1
  234. package/dist/session/preflight.d.ts +50 -13
  235. package/dist/session/preflight.js +252 -80
  236. package/dist/session/preflight.js.map +1 -1
  237. package/dist/session/prompts.d.ts +2 -5
  238. package/dist/session/prompts.js.map +1 -1
  239. package/dist/session/session-manager.d.ts +61 -6
  240. package/dist/session/session-manager.js +72 -10
  241. package/dist/session/session-manager.js.map +1 -1
  242. package/dist/session/session-metadata.d.ts +21 -5
  243. package/dist/session/session-metadata.js +34 -12
  244. package/dist/session/session-metadata.js.map +1 -1
  245. package/dist/session/types.d.ts +311 -5
  246. package/dist/session/types.js +57 -1
  247. package/dist/session/types.js.map +1 -1
  248. package/dist/signal/setup-signal.js +13 -21
  249. package/dist/signal/setup-signal.js.map +1 -1
  250. package/dist/signal/signal-bot-daemon.d.ts +16 -1
  251. package/dist/signal/signal-bot-daemon.js +39 -27
  252. package/dist/signal/signal-bot-daemon.js.map +1 -1
  253. package/dist/signal/signal-container.js +7 -1
  254. package/dist/signal/signal-container.js.map +1 -1
  255. package/dist/skills/discovery.d.ts +77 -0
  256. package/dist/skills/discovery.js +206 -0
  257. package/dist/skills/discovery.js.map +1 -0
  258. package/dist/skills/index.d.ts +4 -0
  259. package/dist/skills/index.js +3 -0
  260. package/dist/skills/index.js.map +1 -0
  261. package/dist/skills/staging.d.ts +28 -0
  262. package/dist/skills/staging.js +68 -0
  263. package/dist/skills/staging.js.map +1 -0
  264. package/dist/skills/types.d.ts +46 -0
  265. package/dist/skills/types.js +2 -0
  266. package/dist/skills/types.js.map +1 -0
  267. package/dist/trusted-process/approval-whitelist.d.ts +0 -2
  268. package/dist/trusted-process/approval-whitelist.js +17 -17
  269. package/dist/trusted-process/approval-whitelist.js.map +1 -1
  270. package/dist/trusted-process/async-mutex.d.ts +32 -0
  271. package/dist/trusted-process/async-mutex.js +50 -0
  272. package/dist/trusted-process/async-mutex.js.map +1 -0
  273. package/dist/trusted-process/audit-log.d.ts +38 -1
  274. package/dist/trusted-process/audit-log.js +82 -13
  275. package/dist/trusted-process/audit-log.js.map +1 -1
  276. package/dist/trusted-process/call-circuit-breaker.js +2 -1
  277. package/dist/trusted-process/call-circuit-breaker.js.map +1 -1
  278. package/dist/trusted-process/control-server.d.ts +101 -0
  279. package/dist/trusted-process/control-server.js +282 -0
  280. package/dist/trusted-process/control-server.js.map +1 -0
  281. package/dist/trusted-process/domain-utils.d.ts +6 -6
  282. package/dist/trusted-process/domain-utils.js +7 -23
  283. package/dist/trusted-process/domain-utils.js.map +1 -1
  284. package/dist/trusted-process/error-prefixes.d.ts +16 -0
  285. package/dist/trusted-process/error-prefixes.js +17 -0
  286. package/dist/trusted-process/error-prefixes.js.map +1 -0
  287. package/dist/trusted-process/index.d.ts +40 -23
  288. package/dist/trusted-process/index.js +131 -225
  289. package/dist/trusted-process/index.js.map +1 -1
  290. package/dist/trusted-process/list-matcher.d.ts +17 -0
  291. package/dist/trusted-process/list-matcher.js +31 -0
  292. package/dist/trusted-process/list-matcher.js.map +1 -0
  293. package/dist/trusted-process/mcp-client-manager.d.ts +35 -6
  294. package/dist/trusted-process/mcp-client-manager.js +40 -45
  295. package/dist/trusted-process/mcp-client-manager.js.map +1 -1
  296. package/dist/trusted-process/mcp-proxy-server.d.ts +31 -120
  297. package/dist/trusted-process/mcp-proxy-server.js +166 -657
  298. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  299. package/dist/trusted-process/policy-engine.d.ts +16 -4
  300. package/dist/trusted-process/policy-engine.js +90 -45
  301. package/dist/trusted-process/policy-engine.js.map +1 -1
  302. package/dist/trusted-process/policy-roots.d.ts +6 -0
  303. package/dist/trusted-process/policy-roots.js +20 -0
  304. package/dist/trusted-process/policy-roots.js.map +1 -1
  305. package/dist/trusted-process/sandbox-integration.d.ts +12 -0
  306. package/dist/trusted-process/sandbox-integration.js +26 -0
  307. package/dist/trusted-process/sandbox-integration.js.map +1 -1
  308. package/dist/trusted-process/tool-call-coordinator.d.ts +269 -0
  309. package/dist/trusted-process/tool-call-coordinator.js +508 -0
  310. package/dist/trusted-process/tool-call-coordinator.js.map +1 -0
  311. package/dist/trusted-process/tool-call-pipeline.d.ts +205 -0
  312. package/dist/trusted-process/tool-call-pipeline.js +798 -0
  313. package/dist/trusted-process/tool-call-pipeline.js.map +1 -0
  314. package/dist/trusted-process/tool-description-hints.d.ts +1 -1
  315. package/dist/types/argument-roles.d.ts +1 -1
  316. package/dist/types/argument-roles.js +14 -1
  317. package/dist/types/argument-roles.js.map +1 -1
  318. package/dist/types/audit.d.ts +9 -0
  319. package/dist/{session → types}/errors.d.ts +8 -2
  320. package/dist/{session → types}/errors.js +8 -2
  321. package/dist/types/errors.js.map +1 -0
  322. package/dist/types/reserved-servers.d.ts +11 -0
  323. package/dist/types/reserved-servers.js +12 -0
  324. package/dist/types/reserved-servers.js.map +1 -0
  325. package/dist/types/server-listing.d.ts +14 -0
  326. package/dist/types/server-listing.js +2 -0
  327. package/dist/types/server-listing.js.map +1 -0
  328. package/dist/types/slug.d.ts +2 -0
  329. package/dist/types/slug.js +2 -0
  330. package/dist/types/slug.js.map +1 -1
  331. package/dist/types/url-normalize.d.ts +16 -0
  332. package/dist/types/url-normalize.js +33 -0
  333. package/dist/types/url-normalize.js.map +1 -0
  334. package/dist/utils/error-message.d.ts +8 -0
  335. package/dist/utils/error-message.js +11 -0
  336. package/dist/utils/error-message.js.map +1 -0
  337. package/dist/utils/exec-error.d.ts +19 -0
  338. package/dist/utils/exec-error.js +16 -0
  339. package/dist/utils/exec-error.js.map +1 -0
  340. package/dist/utils/is-plain-object.d.ts +7 -0
  341. package/dist/utils/is-plain-object.js +12 -0
  342. package/dist/utils/is-plain-object.js.map +1 -0
  343. package/dist/utils/preflight-checks.d.ts +16 -0
  344. package/dist/utils/preflight-checks.js +178 -0
  345. package/dist/utils/preflight-checks.js.map +1 -0
  346. package/dist/web-ui/__tests__/json-rpc-dispatch.test.d.ts +7 -0
  347. package/dist/web-ui/__tests__/json-rpc-dispatch.test.js +725 -0
  348. package/dist/web-ui/__tests__/json-rpc-dispatch.test.js.map +1 -0
  349. package/dist/web-ui/dispatch/escalation-dispatch.d.ts +7 -0
  350. package/dist/web-ui/dispatch/escalation-dispatch.js +52 -0
  351. package/dist/web-ui/dispatch/escalation-dispatch.js.map +1 -0
  352. package/dist/web-ui/dispatch/job-dispatch.d.ts +8 -0
  353. package/dist/web-ui/dispatch/job-dispatch.js +83 -0
  354. package/dist/web-ui/dispatch/job-dispatch.js.map +1 -0
  355. package/dist/web-ui/dispatch/persona-dispatch.d.ts +6 -0
  356. package/dist/web-ui/dispatch/persona-dispatch.js +116 -0
  357. package/dist/web-ui/dispatch/persona-dispatch.js.map +1 -0
  358. package/dist/web-ui/dispatch/session-dispatch.d.ts +9 -0
  359. package/dist/web-ui/dispatch/session-dispatch.js +201 -0
  360. package/dist/web-ui/dispatch/session-dispatch.js.map +1 -0
  361. package/dist/web-ui/dispatch/token-stream-dispatch.d.ts +15 -0
  362. package/dist/web-ui/dispatch/token-stream-dispatch.js +55 -0
  363. package/dist/web-ui/dispatch/token-stream-dispatch.js.map +1 -0
  364. package/dist/web-ui/dispatch/types.d.ts +31 -0
  365. package/dist/web-ui/dispatch/types.js +68 -0
  366. package/dist/web-ui/dispatch/types.js.map +1 -0
  367. package/dist/web-ui/dispatch/workflow-dispatch.d.ts +103 -0
  368. package/dist/web-ui/dispatch/workflow-dispatch.js +925 -0
  369. package/dist/web-ui/dispatch/workflow-dispatch.js.map +1 -0
  370. package/dist/web-ui/json-rpc-dispatch.d.ts +12 -0
  371. package/dist/web-ui/json-rpc-dispatch.js +34 -0
  372. package/dist/web-ui/json-rpc-dispatch.js.map +1 -0
  373. package/dist/web-ui/state-graph.d.ts +13 -0
  374. package/dist/web-ui/state-graph.js +82 -0
  375. package/dist/web-ui/state-graph.js.map +1 -0
  376. package/dist/web-ui/token-stream-bridge.d.ts +76 -0
  377. package/dist/web-ui/token-stream-bridge.js +265 -0
  378. package/dist/web-ui/token-stream-bridge.js.map +1 -0
  379. package/dist/web-ui/web-event-bus.d.ts +152 -0
  380. package/dist/web-ui/web-event-bus.js +19 -0
  381. package/dist/web-ui/web-event-bus.js.map +1 -0
  382. package/dist/web-ui/web-session-transport.d.ts +31 -0
  383. package/dist/web-ui/web-session-transport.js +109 -0
  384. package/dist/web-ui/web-session-transport.js.map +1 -0
  385. package/dist/web-ui/web-ui-server.d.ts +76 -0
  386. package/dist/web-ui/web-ui-server.js +461 -0
  387. package/dist/web-ui/web-ui-server.js.map +1 -0
  388. package/dist/web-ui/web-ui-types.d.ts +290 -0
  389. package/dist/web-ui/web-ui-types.js +32 -0
  390. package/dist/web-ui/web-ui-types.js.map +1 -0
  391. package/dist/web-ui/ws-utils.d.ts +5 -0
  392. package/dist/web-ui/ws-utils.js +12 -0
  393. package/dist/web-ui/ws-utils.js.map +1 -0
  394. package/dist/web-ui-static/assets/index-BFO5z1js.js +76 -0
  395. package/dist/web-ui-static/assets/index-CCUwrkIL.css +1 -0
  396. package/dist/web-ui-static/assets/orbitron-latin-700-normal-4jsRXGGJ.woff2 +0 -0
  397. package/dist/web-ui-static/assets/orbitron-latin-700-normal-DmYOh-W9.woff +0 -0
  398. package/dist/web-ui-static/index.html +14 -0
  399. package/dist/workflow/artifacts.d.ts +59 -0
  400. package/dist/workflow/artifacts.js +188 -0
  401. package/dist/workflow/artifacts.js.map +1 -0
  402. package/dist/workflow/checkpoint-selection.d.ts +34 -0
  403. package/dist/workflow/checkpoint-selection.js +48 -0
  404. package/dist/workflow/checkpoint-selection.js.map +1 -0
  405. package/dist/workflow/checkpoint.d.ts +35 -0
  406. package/dist/workflow/checkpoint.js +84 -0
  407. package/dist/workflow/checkpoint.js.map +1 -0
  408. package/dist/workflow/cli-support.d.ts +92 -0
  409. package/dist/workflow/cli-support.js +423 -0
  410. package/dist/workflow/cli-support.js.map +1 -0
  411. package/dist/workflow/definition-loader.d.ts +32 -0
  412. package/dist/workflow/definition-loader.js +56 -0
  413. package/dist/workflow/definition-loader.js.map +1 -0
  414. package/dist/workflow/discovery.d.ts +78 -0
  415. package/dist/workflow/discovery.js +203 -0
  416. package/dist/workflow/discovery.js.map +1 -0
  417. package/dist/workflow/errors.d.ts +66 -0
  418. package/dist/workflow/errors.js +49 -0
  419. package/dist/workflow/errors.js.map +1 -0
  420. package/dist/workflow/guards.d.ts +8 -0
  421. package/dist/workflow/guards.js +38 -0
  422. package/dist/workflow/guards.js.map +1 -0
  423. package/dist/workflow/lint-integration.d.ts +64 -0
  424. package/dist/workflow/lint-integration.js +56 -0
  425. package/dist/workflow/lint-integration.js.map +1 -0
  426. package/dist/workflow/lint.d.ts +66 -0
  427. package/dist/workflow/lint.js +410 -0
  428. package/dist/workflow/lint.js.map +1 -0
  429. package/dist/workflow/machine-builder.d.ts +90 -0
  430. package/dist/workflow/machine-builder.js +485 -0
  431. package/dist/workflow/machine-builder.js.map +1 -0
  432. package/dist/workflow/message-log.d.ts +85 -0
  433. package/dist/workflow/message-log.js +40 -0
  434. package/dist/workflow/message-log.js.map +1 -0
  435. package/dist/workflow/orchestrator.d.ts +450 -0
  436. package/dist/workflow/orchestrator.js +1941 -0
  437. package/dist/workflow/orchestrator.js.map +1 -0
  438. package/dist/workflow/prompt-builder.d.ts +39 -0
  439. package/dist/workflow/prompt-builder.js +242 -0
  440. package/dist/workflow/prompt-builder.js.map +1 -0
  441. package/dist/workflow/run-state-command.d.ts +9 -0
  442. package/dist/workflow/run-state-command.js +414 -0
  443. package/dist/workflow/run-state-command.js.map +1 -0
  444. package/dist/workflow/run-state-debug-capture.d.ts +40 -0
  445. package/dist/workflow/run-state-debug-capture.js +101 -0
  446. package/dist/workflow/run-state-debug-capture.js.map +1 -0
  447. package/dist/workflow/status-parser.d.ts +68 -0
  448. package/dist/workflow/status-parser.js +259 -0
  449. package/dist/workflow/status-parser.js.map +1 -0
  450. package/dist/workflow/types.d.ts +498 -0
  451. package/dist/workflow/types.js +83 -0
  452. package/dist/workflow/types.js.map +1 -0
  453. package/dist/workflow/validate.d.ts +58 -0
  454. package/dist/workflow/validate.js +499 -0
  455. package/dist/workflow/validate.js.map +1 -0
  456. package/dist/workflow/workflow-command.d.ts +26 -0
  457. package/dist/workflow/workflow-command.js +648 -0
  458. package/dist/workflow/workflow-command.js.map +1 -0
  459. package/dist/workflow/workflow-discovery.d.ts +25 -0
  460. package/dist/workflow/workflow-discovery.js +100 -0
  461. package/dist/workflow/workflow-discovery.js.map +1 -0
  462. package/dist/workflow/workflow-manager.d.ts +122 -0
  463. package/dist/workflow/workflow-manager.js +308 -0
  464. package/dist/workflow/workflow-manager.js.map +1 -0
  465. package/dist/workflow/workflows/design-and-code/workflow.yaml +169 -0
  466. package/dist/workflow/workflows/test-email-summary/skills/email-formatting/SKILL.md +55 -0
  467. package/dist/workflow/workflows/test-email-summary/skills/gmail-query-syntax/SKILL.md +63 -0
  468. package/dist/workflow/workflows/test-email-summary/skills/json-schema-emails/SKILL.md +41 -0
  469. package/dist/workflow/workflows/test-email-summary/skills/json-schema-emails/schema.json +34 -0
  470. package/dist/workflow/workflows/test-email-summary/skills/summary-style-guide/SKILL.md +51 -0
  471. package/dist/workflow/workflows/test-email-summary/workflow.yaml +89 -0
  472. package/dist/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +201 -0
  473. package/dist/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +166 -0
  474. package/dist/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +185 -0
  475. package/dist/workflow/workflows/vuln-discovery/workflow.yaml +920 -0
  476. package/dist/workflow/worktree.d.ts +43 -0
  477. package/dist/workflow/worktree.js +106 -0
  478. package/dist/workflow/worktree.js.map +1 -0
  479. package/docker/Dockerfile.base +12 -1
  480. package/docker/Dockerfile.base.arm64 +17 -1
  481. package/docker/entrypoint-claude-code.sh +46 -0
  482. package/docker/entrypoint-goose.sh +25 -0
  483. package/package.json +22 -8
  484. package/packages/web-ui/README.md +113 -0
  485. package/packages/web-ui/docs/workflow-state-machine.png +0 -0
  486. package/src/config/generated/compiled-policy.json +528 -161
  487. package/src/config/generated/test-scenarios.json +4810 -170
  488. package/src/config/generated/tool-annotations.json +34 -47
  489. package/src/config/mcp-servers.json +2 -2
  490. package/src/workflow/workflows/design-and-code/workflow.yaml +169 -0
  491. package/src/workflow/workflows/test-email-summary/skills/email-formatting/SKILL.md +55 -0
  492. package/src/workflow/workflows/test-email-summary/skills/gmail-query-syntax/SKILL.md +63 -0
  493. package/src/workflow/workflows/test-email-summary/skills/json-schema-emails/SKILL.md +41 -0
  494. package/src/workflow/workflows/test-email-summary/skills/json-schema-emails/schema.json +34 -0
  495. package/src/workflow/workflows/test-email-summary/skills/summary-style-guide/SKILL.md +51 -0
  496. package/src/workflow/workflows/test-email-summary/workflow.yaml +89 -0
  497. package/src/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +201 -0
  498. package/src/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +166 -0
  499. package/src/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +185 -0
  500. package/src/workflow/workflows/vuln-discovery/workflow.yaml +920 -0
  501. package/dist/pipeline/llm-logger.js.map +0 -1
  502. package/dist/session/errors.js.map +0 -1
@@ -12,26 +12,33 @@
12
12
  */
13
13
  import { resolve } from 'node:path';
14
14
  import chalk from 'chalk';
15
- import { extractServerDomainAllowlists } from '../config/index.js';
15
+ import pLimit from 'p-limit';
16
+ import { extractServerDomainAllowlists, findAnnotationServerDrift, safeForDisplay } from '../config/index.js';
17
+ import { createLanguageModel } from '../config/model-provider.js';
18
+ import { loadUserConfig } from '../config/user-config.js';
16
19
  import { PolicyEngine } from '../trusted-process/policy-engine.js';
17
20
  import { buildCompilerSystemPrompt, ConstitutionCompilerSession, formatAnnotationsSummary, validateCompiledRules, } from './constitution-compiler.js';
18
21
  import { getHandwrittenScenarios } from './handwritten-scenarios.js';
19
22
  import { resolveAllLists } from './list-resolver.js';
20
- import { computeHash, loadExistingArtifact, loadStoredToolAnnotationsFile, resolveRulePaths, writeArtifact, withSpinner, showCached, createPipelineLlm, } from './pipeline-shared.js';
23
+ import { computeHash, loadExistingArtifact, loadStoredToolAnnotationsFile, resolveRulePaths, writeArtifact, createPipelineLlm, createPerServerModel, createThrottledModel, SpinnerProgressReporter, } from './pipeline-shared.js';
21
24
  import { resolveStoredAnnotationsFile } from '../types/argument-roles.js';
22
25
  import { applyScenarioCorrections, buildJudgeSystemPrompt, detectAllDefaultRoleFallbacks, extractScenarioCorrections, filterStructuralConflicts, PolicyVerifierSession, verifyPolicy, } from './policy-verifier.js';
23
26
  import { filterInvalidSchemaScenarios } from './scenario-schema-validator.js';
24
27
  import { buildGeneratorSystemPrompt, generateScenarios, repairScenarios } from './scenario-generator.js';
28
+ import { prefilterServers } from './server-prefilter.js';
25
29
  import { connectViaProxy } from './proxy-mcp-connections.js';
26
30
  /** Creates PipelineModels from user config. Delegates to shared createPipelineLlm. */
27
31
  export async function createPipelineModels(logDir) {
28
32
  const effectiveLogDir = logDir ?? resolve(process.cwd(), 'generated');
29
33
  const llm = await createPipelineLlm(effectiveLogDir, 'unknown');
34
+ const userConfig = loadUserConfig();
35
+ const haikuBaseLlm = await createLanguageModel(userConfig.prefilterModelId, userConfig);
36
+ const { model: prefilterModel } = createPerServerModel(haikuBaseLlm, llm.logPath, 'prefilter');
30
37
  return {
31
- compilationModel: llm.model,
38
+ baseLlm: llm.baseLlm,
32
39
  cacheStrategy: llm.cacheStrategy,
33
- logContext: llm.logContext,
34
40
  logPath: llm.logPath,
41
+ prefilterModel,
35
42
  };
36
43
  }
37
44
  // ---------------------------------------------------------------------------
@@ -40,6 +47,29 @@ export async function createPipelineModels(logDir) {
40
47
  function computeScenariosHash(systemPrompt, handwrittenScenarios) {
41
48
  return computeHash(systemPrompt, JSON.stringify(handwrittenScenarios));
42
49
  }
50
+ /**
51
+ * Preventive warning: if `mcp-servers.json` declares servers that are not
52
+ * present in `tool-annotations.json`, compiling will produce a policy with
53
+ * no rules for those servers and every call to them will default-deny at
54
+ * runtime. Surface this before starting the (slow) compile so the user
55
+ * can re-run the per-server annotator first.
56
+ *
57
+ * `mcpServers` may be undefined when compile-policy is run with `--no-mcp`,
58
+ * in which case the check is skipped.
59
+ */
60
+ function warnOnCompileAnnotationGaps(mcpServers, storedAnnotationsFile) {
61
+ if (!mcpServers)
62
+ return;
63
+ const { missing } = findAnnotationServerDrift(storedAnnotationsFile, mcpServers);
64
+ if (missing.length === 0)
65
+ return;
66
+ console.error(chalk.yellow(`Warning: ${missing.length} configured MCP server(s) have no tool annotations: ${missing.map(safeForDisplay).join(', ')}.`));
67
+ console.error(chalk.yellow(' The compiled policy will have no rules for these servers and their tools will be denied.'));
68
+ for (const name of missing) {
69
+ console.error(chalk.yellow(` Run \`ironcurtain annotate-tools --server ${safeForDisplay(name)}\` to annotate this server.`));
70
+ }
71
+ console.error('');
72
+ }
43
73
  function extractPermittedDirectories(rules) {
44
74
  const dirs = new Set();
45
75
  for (const rule of rules) {
@@ -52,7 +82,8 @@ function extractPermittedDirectories(rules) {
52
82
  function collectProbeScenarios(result) {
53
83
  return result.rounds.flatMap((round) => round.newScenarios);
54
84
  }
55
- function filterAndLogStructuralConflicts(engine, scenarios, label = 'Discarded scenario (structural conflict)', storedAnnotations) {
85
+ function filterAndLogStructuralConflicts(engine, scenarios, label = 'Discarded scenario (structural conflict)', storedAnnotations, reporter) {
86
+ const warn = (msg) => (reporter ? reporter.warn(msg) : console.error(msg));
56
87
  const filterResult = filterStructuralConflicts(engine, scenarios);
57
88
  let valid = filterResult.valid;
58
89
  const discarded = filterResult.discarded;
@@ -60,7 +91,7 @@ function filterAndLogStructuralConflicts(engine, scenarios, label = 'Discarded s
60
91
  const prefix = d.scenario.source === 'handwritten'
61
92
  ? chalk.yellow('Warning: handwritten scenario conflicts with structural invariant:')
62
93
  : chalk.dim(`${label}:`);
63
- console.error(` ${prefix} "${d.scenario.description}" — ${d.rule} always returns ${d.actual}`);
94
+ warn(` ${prefix} "${d.scenario.description}" — ${d.rule} always returns ${d.actual}`);
64
95
  }
65
96
  // Discard scenarios whose arguments don't match any conditional role spec —
66
97
  // they fall back to all default roles and test the wrong thing.
@@ -74,7 +105,7 @@ function filterAndLogStructuralConflicts(engine, scenarios, label = 'Discarded s
74
105
  const w = fallbackWarnings.find((fw) => fw.scenario.description === s.description);
75
106
  const details = w?.details.join('; ') ?? 'unknown conditional mismatch';
76
107
  const discardLabel = 'Discarded (default role fallback)';
77
- console.error(` ${chalk.dim(`${discardLabel}:`)} "${s.description}" — ${details}`);
108
+ warn(` ${chalk.dim(`${discardLabel}:`)} "${s.description}" — ${details}`);
78
109
  discarded.push({ scenario: s, rule: 'default-role-fallback', actual: 'deny' });
79
110
  }
80
111
  else {
@@ -88,7 +119,7 @@ function filterAndLogStructuralConflicts(engine, scenarios, label = 'Discarded s
88
119
  const schemaResult = filterInvalidSchemaScenarios(valid, storedAnnotations);
89
120
  if (schemaResult.discarded.length > 0) {
90
121
  for (const d of schemaResult.discarded) {
91
- console.error(` ${chalk.dim('Discarded (schema mismatch):')} "${d.scenario.description}" — ${d.rule}`);
122
+ warn(` ${chalk.dim('Discarded (schema mismatch):')} "${d.scenario.description}" — ${d.rule}`);
92
123
  }
93
124
  discarded.push(...schemaResult.discarded);
94
125
  valid = schemaResult.valid;
@@ -96,6 +127,10 @@ function filterAndLogStructuralConflicts(engine, scenarios, label = 'Discarded s
96
127
  }
97
128
  return { valid, discarded };
98
129
  }
130
+ /** Coerce an unknown thrown value into an Error. */
131
+ function toError(err) {
132
+ return err instanceof Error ? err : new Error(String(err));
133
+ }
99
134
  class RuleValidationError extends Error {
100
135
  errors;
101
136
  constructor(errors) {
@@ -104,11 +139,12 @@ class RuleValidationError extends Error {
104
139
  this.name = 'RuleValidationError';
105
140
  }
106
141
  }
107
- function validateRulesOrThrow(rules, listDefinitions = []) {
142
+ function validateRulesOrThrow(rules, listDefinitions = [], reporter) {
143
+ const warn = (msg) => (reporter ? reporter.warn(msg) : console.error(msg));
108
144
  const ruleValidation = validateCompiledRules(rules, listDefinitions);
109
145
  if (ruleValidation.warnings.length > 0) {
110
146
  for (const w of ruleValidation.warnings) {
111
- console.error(` ${chalk.yellow('Warning:')} ${w}`);
147
+ warn(` ${chalk.yellow('Warning:')} ${w}`);
112
148
  }
113
149
  }
114
150
  if (!ruleValidation.valid) {
@@ -262,7 +298,7 @@ export function deduplicateListDefinitions(defs) {
262
298
  * Rules are concatenated in alphabetical server order for determinism.
263
299
  * List definitions are deduplicated by name.
264
300
  */
265
- export function mergeServerResults(results, constitutionHash) {
301
+ export function mergeServerResults(results, constitutionHash, skippedServers) {
266
302
  const sortedResults = [...results].sort((a, b) => a.serverName.localeCompare(b.serverName));
267
303
  const allRules = sortedResults.flatMap((r) => r.rules);
268
304
  const allListDefs = sortedResults.flatMap((r) => r.listDefinitions);
@@ -277,6 +313,12 @@ export function mergeServerResults(results, constitutionHash) {
277
313
  if (uniqueListDefs.length > 0) {
278
314
  artifact.listDefinitions = uniqueListDefs;
279
315
  }
316
+ // skippedServers is informational metadata — not read by PolicyEngine.
317
+ // It's intentionally excluded from inputHash; the constitutionHash already
318
+ // changes when the constitution changes, which drives different skip decisions.
319
+ if (skippedServers && skippedServers.length > 0) {
320
+ artifact.skippedServers = skippedServers;
321
+ }
280
322
  return artifact;
281
323
  }
282
324
  // ---------------------------------------------------------------------------
@@ -291,13 +333,15 @@ export function mergeServerResults(results, constitutionHash) {
291
333
  * (broad principles) and 'task-policy' (strict whitelist) modes.
292
334
  */
293
335
  export class PipelineRunner {
294
- model;
336
+ baseLlm;
295
337
  cacheStrategy;
296
- logContext;
338
+ logPath;
339
+ prefilterModel;
297
340
  constructor(models) {
298
- this.model = models.compilationModel;
341
+ this.baseLlm = models.baseLlm;
299
342
  this.cacheStrategy = models.cacheStrategy;
300
- this.logContext = models.logContext;
343
+ this.prefilterModel = models.prefilterModel;
344
+ this.logPath = models.logPath;
301
345
  }
302
346
  /**
303
347
  * Runs the full pipeline. Returns the compiled policy on success.
@@ -327,13 +371,14 @@ export class PipelineRunner {
327
371
  if (!storedAnnotationsFile) {
328
372
  throw new Error("tool-annotations.json not found. Run 'ironcurtain annotate-tools' first.");
329
373
  }
374
+ warnOnCompileAnnotationGaps(config.mcpServers, storedAnnotationsFile);
330
375
  // Resolve conditional role specs to flat annotations for compiler prompts
331
376
  const toolAnnotationsFile = resolveStoredAnnotationsFile(storedAnnotationsFile);
332
377
  const constitutionHash = computeHash(config.constitutionInput);
333
- // Phase 1: Per-server compilation
334
- const serverResults = await this.compileAllServers(config, toolAnnotationsFile, constitutionHash, storedAnnotationsFile);
378
+ // Phase 1: Per-server compilation (with optional Haiku pre-filter)
379
+ const { results: serverResults, skippedServers } = await this.compileAllServers(config, toolAnnotationsFile, constitutionHash, storedAnnotationsFile);
335
380
  // Phase 2: Merge
336
- const mergedPolicy = mergeServerResults(serverResults, constitutionHash);
381
+ const mergedPolicy = mergeServerResults(serverResults, constitutionHash, skippedServers);
337
382
  writeArtifact(config.outputDir, 'compiled-policy.json', mergedPolicy);
338
383
  // Merge scenarios for the global artifact
339
384
  const allScenarios = serverResults.flatMap((r) => r.scenarios);
@@ -391,11 +436,11 @@ export class PipelineRunner {
391
436
  return mergedPolicy;
392
437
  }
393
438
  /**
394
- * Compiles rules for all servers sequentially.
395
- * Respects serverFilter if provided (for debugging single servers).
439
+ * Compiles rules for all servers, in parallel when multiple servers are present.
440
+ * Single-server compilation uses sequential spinner-based output.
441
+ * Multi-server compilation uses parallel execution with a multi-line progress display.
396
442
  */
397
443
  async compileAllServers(config, toolAnnotationsFile, constitutionHash, storedAnnotationsFile) {
398
- const results = [];
399
444
  const serverEntries = Object.entries(toolAnnotationsFile.servers);
400
445
  // Apply server filter if provided
401
446
  const { serverFilter } = config;
@@ -406,35 +451,126 @@ export class PipelineRunner {
406
451
  throw new Error(`No matching servers found for filter: ${config.serverFilter.join(', ')}. ` +
407
452
  `Available: ${serverEntries.map(([n]) => n).join(', ')}`);
408
453
  }
454
+ // Pre-filter with Haiku: skip servers irrelevant to the input text
455
+ let entriesToCompile = filteredEntries;
456
+ const skippedServers = [];
457
+ if (config.prefilterText !== undefined && !config.serverFilter) {
458
+ const prefilterText = config.prefilterText;
459
+ if (prefilterText.trim() === '') {
460
+ console.error(chalk.yellow('Pre-filter input is empty. All servers skipped (default-deny applies).'));
461
+ const allSkipped = filteredEntries.map(([name]) => ({
462
+ serverName: name,
463
+ reason: 'No input text provided — all servers skipped by default',
464
+ }));
465
+ return { results: [], skippedServers: allSkipped };
466
+ }
467
+ const serverToolPairs = filteredEntries.map(([name]) => [name, toolAnnotationsFile.servers[name].tools]);
468
+ const decisions = await prefilterServers(prefilterText, serverToolPairs, this.prefilterModel, config.constitutionKind);
469
+ const decisionMap = new Map(decisions.map((d) => [d.serverName, d]));
470
+ entriesToCompile = filteredEntries.filter(([name]) => {
471
+ const decision = decisionMap.get(name);
472
+ if (decision?.skip) {
473
+ console.error(` ${chalk.dim(name)}: ${chalk.yellow('skipped')} — ${decision.reason}`);
474
+ skippedServers.push({ serverName: name, reason: decision.reason });
475
+ return false;
476
+ }
477
+ return true;
478
+ });
479
+ const skippedCount = filteredEntries.length - entriesToCompile.length;
480
+ if (skippedCount > 0) {
481
+ console.error(` Pre-filter: ${skippedCount} server(s) skipped, ${entriesToCompile.length} proceeding`);
482
+ }
483
+ }
484
+ if (entriesToCompile.length === 0) {
485
+ console.error(chalk.yellow('All servers skipped by pre-filter. Policy will use default-deny only.'));
486
+ return { results: [], skippedServers };
487
+ }
409
488
  // Load handwritten scenarios once, then filter per server (avoids N redundant loads)
410
489
  const includeHandwritten = config.includeHandwrittenScenarios ?? config.constitutionKind === 'constitution';
411
490
  const allHandwrittenScenarios = includeHandwritten ? getHandwrittenScenarios(config.allowedDirectory) : [];
491
+ // Create concurrency controls
492
+ const llmSemaphore = pLimit(8);
493
+ const serverLimit = pLimit(10);
494
+ const buildUnit = (serverName) => ({
495
+ serverName,
496
+ annotations: toolAnnotationsFile.servers[serverName].tools,
497
+ storedAnnotations: storedAnnotationsFile.servers[serverName].tools,
498
+ constitutionText: config.constitutionInput,
499
+ constitutionKind: config.constitutionKind,
500
+ allowedDirectory: config.allowedDirectory,
501
+ protectedPaths: config.protectedPaths,
502
+ mcpServerConfig: config.mcpServers?.[serverName],
503
+ handwrittenScenarios: allHandwrittenScenarios.filter((s) => s.request.serverName === serverName),
504
+ });
505
+ const useParallel = entriesToCompile.length > 1;
506
+ let results;
507
+ if (useParallel) {
508
+ results = await this.compileServersParallel(entriesToCompile, config, constitutionHash, buildUnit, llmSemaphore, serverLimit);
509
+ }
510
+ else {
511
+ results = await this.compileServersSequential(entriesToCompile, config, constitutionHash, buildUnit, llmSemaphore);
512
+ }
513
+ return { results, skippedServers };
514
+ }
515
+ /** Creates a per-server model wrapped with LLM call throttling. */
516
+ createServerModel(serverName, llmSemaphore) {
517
+ const { model, logContext } = createPerServerModel(this.baseLlm, this.logPath, serverName);
518
+ return { model: createThrottledModel(model, llmSemaphore), logContext };
519
+ }
520
+ async compileServersSequential(filteredEntries, config, constitutionHash, buildUnit, llmSemaphore) {
521
+ const results = [];
412
522
  const totalServers = filteredEntries.length;
413
523
  const failedServers = [];
414
524
  for (let i = 0; i < filteredEntries.length; i++) {
415
- const [serverName, serverData] = filteredEntries[i];
525
+ const [serverName] = filteredEntries[i];
416
526
  console.error('');
417
527
  console.error(chalk.bold(`[${i + 1}/${totalServers}] Compiling server: ${serverName}`));
528
+ const { model, logContext } = this.createServerModel(serverName, llmSemaphore);
529
+ const reporter = new SpinnerProgressReporter(serverName);
418
530
  try {
419
- const result = await this.compileServer({
420
- serverName,
421
- annotations: serverData.tools,
422
- storedAnnotations: storedAnnotationsFile.servers[serverName].tools,
423
- constitutionText: config.constitutionInput,
424
- constitutionKind: config.constitutionKind,
425
- allowedDirectory: config.allowedDirectory,
426
- protectedPaths: config.protectedPaths,
427
- mcpServerConfig: config.mcpServers?.[serverName],
428
- handwrittenScenarios: allHandwrittenScenarios.filter((s) => s.request.serverName === serverName),
429
- }, config, constitutionHash);
531
+ const result = await this.compileServer(buildUnit(serverName), config, constitutionHash, model, logContext, reporter);
430
532
  results.push(result);
431
533
  }
432
534
  catch (err) {
433
- const msg = err instanceof Error ? err.message : String(err);
434
- console.error(` ${chalk.red(`Server "${serverName}" failed:`)} ${msg}`);
535
+ const coerced = toError(err);
536
+ reporter.fail('compiling', coerced);
537
+ console.error(` ${chalk.red(`Server "${serverName}" failed:`)} ${coerced.message}`);
538
+ failedServers.push(serverName);
539
+ }
540
+ }
541
+ return this.handleCompilationResults(results, failedServers);
542
+ }
543
+ async compileServersParallel(filteredEntries, config, constitutionHash, buildUnit, llmSemaphore, serverLimit) {
544
+ // Lazy import to avoid loading parallel-progress.ts when not needed
545
+ const { ParallelProgressDisplay, ParallelProgressReporter } = await import('./parallel-progress.js');
546
+ const serverNames = filteredEntries.map(([name]) => name);
547
+ const display = new ParallelProgressDisplay(serverNames);
548
+ const settled = await Promise.allSettled(filteredEntries.map(([serverName]) => serverLimit(async () => {
549
+ const { model, logContext } = this.createServerModel(serverName, llmSemaphore);
550
+ const reporter = new ParallelProgressReporter(display, serverName);
551
+ try {
552
+ return await this.compileServer(buildUnit(serverName), config, constitutionHash, model, logContext, reporter);
553
+ }
554
+ catch (err) {
555
+ reporter.fail('compiling', toError(err));
556
+ throw err;
557
+ }
558
+ })));
559
+ display.finish();
560
+ const results = [];
561
+ const failedServers = [];
562
+ for (const [i, outcome] of settled.entries()) {
563
+ if (outcome.status === 'fulfilled') {
564
+ results.push(outcome.value);
565
+ }
566
+ else {
567
+ const serverName = filteredEntries[i][0];
435
568
  failedServers.push(serverName);
436
569
  }
437
570
  }
571
+ return this.handleCompilationResults(results, failedServers);
572
+ }
573
+ handleCompilationResults(results, failedServers) {
438
574
  if (failedServers.length > 0) {
439
575
  console.error(`\n${chalk.yellow(`Warning: ${failedServers.length} server(s) failed verification: ${failedServers.join(', ')}`)}`);
440
576
  console.error(chalk.yellow('Continuing with successfully compiled servers.'));
@@ -448,25 +584,28 @@ export class PipelineRunner {
448
584
  /**
449
585
  * Compiles a single server: compile -> generate scenarios -> verify -> repair.
450
586
  * Self-contained compile-verify-repair loop per server.
587
+ *
588
+ * Each server receives its own model (with per-server logging middleware),
589
+ * logContext (for step labeling), and reporter (for progress/warnings).
590
+ * This makes the method safe for concurrent execution.
451
591
  */
452
- async compileServer(unit, config, constitutionHash) {
592
+ async compileServer(unit, config, constitutionHash, model, logContext, reporter) {
453
593
  const serverOutputDir = resolve(config.outputDir, 'servers', unit.serverName);
454
594
  // Build per-server system prompt (only this server's annotations)
455
595
  const compilerPrompt = unit.constitutionKind === 'task-policy'
456
- ? buildTaskCompilerSystemPrompt(unit.constitutionText, unit.annotations, unit.protectedPaths, unit.allowedDirectory, unit.serverName)
457
- : buildCompilerSystemPrompt(unit.constitutionText, unit.annotations, { protectedPaths: unit.protectedPaths, allowedDirectory: unit.allowedDirectory }, unit.handwrittenScenarios.length > 0 ? unit.handwrittenScenarios : undefined, { serverScope: unit.serverName });
596
+ ? buildTaskCompilerSystemPrompt(unit.constitutionText, unit.storedAnnotations, unit.protectedPaths, unit.allowedDirectory, unit.serverName)
597
+ : buildCompilerSystemPrompt(unit.constitutionText, unit.storedAnnotations, { protectedPaths: unit.protectedPaths, allowedDirectory: unit.allowedDirectory }, unit.handwrittenScenarios.length > 0 ? unit.handwrittenScenarios : undefined, { serverScope: unit.serverName });
458
598
  // Check per-server cache
459
599
  const inputHash = computeServerPolicyHash(unit.serverName, unit.constitutionText, unit.annotations, compilerPrompt);
460
600
  const existingServerPolicy = loadExistingArtifact(serverOutputDir, 'compiled-policy.json');
461
601
  const existingServerScenarios = loadExistingArtifact(serverOutputDir, 'test-scenarios.json');
462
602
  if (existingServerPolicy && existingServerPolicy.inputHash === inputHash && existingServerScenarios) {
463
- // Verify scenario hash to detect stale scenarios (e.g., changed templates or handwritten scenarios)
464
603
  const cachedPermittedDirs = extractPermittedDirectories(resolveRulePaths(existingServerPolicy.rules));
465
604
  const cachedDynamicLists = loadExistingArtifact(serverOutputDir, 'dynamic-lists.json');
466
- const cachedScenarioPrompt = buildGeneratorSystemPrompt(unit.constitutionText, unit.annotations, unit.allowedDirectory, cachedPermittedDirs, cachedDynamicLists, unit.storedAnnotations);
605
+ const cachedScenarioPrompt = buildGeneratorSystemPrompt(unit.constitutionText, unit.storedAnnotations, unit.allowedDirectory, cachedPermittedDirs, cachedDynamicLists);
467
606
  const cachedScenarioHash = computeScenariosHash(cachedScenarioPrompt, unit.handwrittenScenarios);
468
607
  if (existingServerScenarios.inputHash === cachedScenarioHash) {
469
- showCached(` ${unit.serverName}: compilation`);
608
+ reporter.complete('cached', ` ${unit.serverName}: compilation ${chalk.dim('(cached)')}`, 0);
470
609
  return {
471
610
  serverName: unit.serverName,
472
611
  rules: resolveRulePaths(existingServerPolicy.rules),
@@ -480,20 +619,19 @@ export class PipelineRunner {
480
619
  }
481
620
  const compilerSystem = this.cacheStrategy.wrapSystemPrompt(compilerPrompt);
482
621
  // Step 1: Compile rules for this server
483
- this.logContext.stepName = `compile-${unit.serverName}`;
484
- const compileResult = await this.compileServerPolicyRules(unit, compilerSystem, inputHash);
622
+ logContext.stepName = `compile-${unit.serverName}`;
623
+ const compileResult = await this.compileServerPolicyRules(unit, compilerSystem, inputHash, model, reporter);
485
624
  let { rules } = compileResult;
486
625
  let { listDefinitions } = compileResult;
487
- let compilerSession = compileResult.session;
488
626
  // Validate server scoping (debug assertion)
489
627
  validateServerScoping(unit.serverName, rules);
490
628
  // Resolve dynamic lists for this server (before scenario generation so lists are available)
491
629
  let dynamicLists;
492
630
  if (listDefinitions.length > 0) {
493
- dynamicLists = await this.resolveServerLists(listDefinitions, serverOutputDir, config, ` ${unit.serverName}`);
631
+ dynamicLists = await this.resolveServerLists(listDefinitions, serverOutputDir, config, ` ${unit.serverName}`, model, logContext, reporter);
494
632
  }
495
633
  // Build per-server policy artifact for engine construction
496
- let serverPolicyFile = buildPolicyArtifact(constitutionHash, rules, listDefinitions, inputHash);
634
+ const serverPolicyFile = buildPolicyArtifact(constitutionHash, rules, listDefinitions, inputHash);
497
635
  // Persist early so intermediate results survive scenario/verification failures
498
636
  const writeServerPolicy = () => writeArtifact(serverOutputDir, 'compiled-policy.json', {
499
637
  generatedAt: new Date().toISOString(),
@@ -514,188 +652,239 @@ export class PipelineRunner {
514
652
  ? extractServerDomainAllowlists({ [unit.serverName]: unit.mcpServerConfig })
515
653
  : undefined;
516
654
  // Step 2: Generate test scenarios for this server
517
- this.logContext.stepName = `scenarios-${unit.serverName}`;
655
+ logContext.stepName = `scenarios-${unit.serverName}`;
518
656
  const permittedDirectories = extractPermittedDirectories(rules);
519
- const scenarioPrompt = buildGeneratorSystemPrompt(unit.constitutionText, unit.annotations, unit.allowedDirectory, permittedDirectories, dynamicLists, unit.storedAnnotations);
657
+ const scenarioPrompt = buildGeneratorSystemPrompt(unit.constitutionText, unit.storedAnnotations, unit.allowedDirectory, permittedDirectories, dynamicLists);
520
658
  const scenarioHash = computeScenariosHash(scenarioPrompt, unit.handwrittenScenarios);
521
- const scenarioResult = await this.generateTestScenarios(unit.constitutionText, unit.annotations, unit.allowedDirectory, unit.handwrittenScenarios, scenarioHash, existingServerScenarios, ` ${unit.serverName}`, permittedDirectories, dynamicLists, unit.storedAnnotations);
522
- // Step 3: Verify compiled rules against scenarios
523
- this.logContext.stepName = `verify-${unit.serverName}`;
524
- const filterEngine = new PolicyEngine(serverPolicyFile, serverAnnotationsFile, unit.protectedPaths, unit.allowedDirectory, undefined, dynamicLists);
525
- const { valid: filteredScenarios, discarded: discardedScenarios } = filterAndLogStructuralConflicts(filterEngine, scenarioResult.scenarios, undefined, unit.storedAnnotations);
526
- // Generate replacement scenarios for structurally discarded ones (pre-loop, one-time)
527
- if (discardedScenarios.length > 0) {
528
- const discardedForRepair = discardedScenarios.map((d) => ({
529
- scenario: d.scenario,
530
- feedback: `${d.rule} always returns ${d.actual}`,
531
- }));
532
- this.logContext.stepName = `repair-scenarios-${unit.serverName}`;
533
- const replacementScenarios = await repairScenarios(discardedForRepair, unit.constitutionText, unit.annotations, unit.allowedDirectory, this.model, permittedDirectories, dynamicLists, (msg) => console.error(` ${chalk.dim(msg)}`), unit.storedAnnotations);
534
- if (replacementScenarios.length > 0) {
535
- // Filter replacements through structural invariants too
536
- const { valid: validReplacements } = filterAndLogStructuralConflicts(filterEngine, replacementScenarios, 'Discarded replacement (structural conflict)', unit.storedAnnotations);
537
- scenarioResult.scenarios.push(...validReplacements);
538
- filteredScenarios.push(...validReplacements);
539
- console.error(` ${chalk.dim(`Repaired ${discardedScenarios.length} discarded scenario(s) → ${validReplacements.length} replacement(s)`)}`);
540
- }
659
+ const scenarioResult = await this.generateTestScenarios(unit.constitutionText, unit.storedAnnotations, unit.allowedDirectory, unit.handwrittenScenarios, scenarioHash, existingServerScenarios, ` ${unit.serverName}`, permittedDirectories, dynamicLists, model, reporter);
660
+ // Step 3: Verify and repair
661
+ const state = await this.verifyAndRepairServer(unit, config, constitutionHash, inputHash, serverPolicyFile, serverAnnotationsFile, serverDomainAllowlists, dynamicLists, rules, listDefinitions, compileResult.session, scenarioResult, permittedDirectories, compilerSystem, model, logContext, reporter, serverOutputDir, writeServerPolicy);
662
+ // Sync repaired state back so the writeServerPolicy closure sees updated values
663
+ rules = state.rules;
664
+ listDefinitions = state.listDefinitions;
665
+ // Write per-server artifacts (before verification check so they can be inspected on failure)
666
+ const finalScenarios = [...scenarioResult.scenarios, ...state.accumulatedProbes];
667
+ writeServerPolicy();
668
+ writeArtifact(serverOutputDir, 'test-scenarios.json', {
669
+ generatedAt: new Date().toISOString(),
670
+ constitutionHash,
671
+ inputHash: scenarioResult.inputHash,
672
+ scenarios: finalScenarios,
673
+ });
674
+ if (!state.verificationResult.pass) {
675
+ throw new Error(`Verification FAILED for server "${unit.serverName}" — artifacts written for inspection but policy may need review.`);
541
676
  }
677
+ const summary = `${state.rules.length} rules, ${finalScenarios.length} scenarios` +
678
+ (state.repairAttempts > 0 ? `, ${state.repairAttempts} repair(s)` : '');
679
+ reporter.done(summary);
680
+ return {
681
+ serverName: unit.serverName,
682
+ rules: state.rules,
683
+ listDefinitions: state.listDefinitions,
684
+ scenarios: finalScenarios,
685
+ inputHash,
686
+ constitutionHash,
687
+ resolvedLists: state.dynamicLists,
688
+ };
689
+ }
690
+ /**
691
+ * Runs initial verification and the compile-verify-repair loop for a server.
692
+ * Returns the mutable compilation state after all repair attempts.
693
+ */
694
+ async verifyAndRepairServer(unit, config, constitutionHash, inputHash, serverPolicyFile, serverAnnotationsFile, serverDomainAllowlists, dynamicLists, rules, listDefinitions, compilerSession, scenarioResult, permittedDirectories, compilerSystem, model, logContext, reporter, serverOutputDir, writeServerPolicy) {
695
+ logContext.stepName = `verify-${unit.serverName}`;
696
+ const filterEngine = new PolicyEngine(serverPolicyFile, serverAnnotationsFile, unit.protectedPaths, unit.allowedDirectory, undefined, dynamicLists);
697
+ const { filteredScenarios } = await this.filterAndRepairDiscardedScenarios(filterEngine, scenarioResult, unit, permittedDirectories, dynamicLists, model, logContext, reporter);
542
698
  const serverToolNames = [...new Set(unit.annotations.map((a) => a.toolName))];
543
699
  const serverNames = [unit.serverName];
544
700
  const serverTools = unit.annotations.map((a) => ({ serverName: a.serverName, toolName: a.toolName }));
545
- let verifierSystem = this.cacheStrategy.wrapSystemPrompt(buildJudgeSystemPrompt(unit.constitutionText, serverPolicyFile, unit.protectedPaths, serverTools, dynamicLists, unit.allowedDirectory, unit.storedAnnotations));
546
- let verifierSession = new PolicyVerifierSession({
701
+ const verifierSystem = this.cacheStrategy.wrapSystemPrompt(buildJudgeSystemPrompt(unit.constitutionText, serverPolicyFile, unit.protectedPaths, serverTools, dynamicLists, unit.allowedDirectory, unit.storedAnnotations));
702
+ const verifierSession = new PolicyVerifierSession({
547
703
  system: verifierSystem,
548
- model: this.model,
704
+ model,
549
705
  serverNames,
550
706
  toolNames: serverToolNames,
551
707
  storedAnnotations: unit.storedAnnotations,
552
708
  });
709
+ reporter.update('verifying');
710
+ const verifyStart = Date.now();
711
+ const verificationResultInitial = await verifyPolicy(unit.constitutionText, serverPolicyFile, serverAnnotationsFile, unit.protectedPaths, filteredScenarios, model, 3, unit.allowedDirectory, (msg) => reporter.update('verifying', msg), serverDomainAllowlists, dynamicLists, verifierSystem, verifierSession, unit.storedAnnotations);
712
+ const verifyElapsed = (Date.now() - verifyStart) / 1000;
553
713
  const verifyLabel = ` ${unit.serverName}: Verifying`;
554
- const { result: verificationResultInitial } = await withSpinner(verifyLabel, async (spinner) => verifyPolicy(unit.constitutionText, serverPolicyFile, serverAnnotationsFile, unit.protectedPaths, filteredScenarios, this.model, 3, unit.allowedDirectory, (msg) => {
555
- spinner.text = `${verifyLabel} ${msg}`;
556
- }, serverDomainAllowlists, dynamicLists, verifierSystem, verifierSession, unit.storedAnnotations), (r, elapsed) => r.pass
557
- ? `${verifyLabel}: ${r.rounds.length} round(s) (${elapsed.toFixed(1)}s)`
558
- : `${verifyLabel}: completed with failures (${elapsed.toFixed(1)}s)`);
559
- let verificationResult = verificationResultInitial;
560
- if (!verificationResult.pass) {
561
- this.logVerboseFailures(verificationResult);
714
+ reporter.complete('verifying', verificationResultInitial.pass
715
+ ? `${verifyLabel}: ${verificationResultInitial.rounds.length} round(s) (${verifyElapsed.toFixed(1)}s)`
716
+ : `${verifyLabel}: completed with failures (${verifyElapsed.toFixed(1)}s)`, verifyElapsed);
717
+ if (!verificationResultInitial.pass) {
718
+ this.logVerboseFailures(verificationResultInitial, reporter);
562
719
  }
563
720
  // Collect probe scenarios
564
- const { valid: filteredInitialProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(verificationResult), 'Discarded probe (structural conflict)', unit.storedAnnotations);
565
- const accumulatedProbes = filteredInitialProbes;
566
- // Compile-verify-repair loop (up to 2 repair attempts)
567
- const MAX_REPAIRS = 2;
568
- let repairAttempts = 0;
569
- let currentFilteredScenarios;
570
- if (!verificationResult.pass) {
571
- const baseInputHash = inputHash;
572
- for (let attempt = 1; attempt <= MAX_REPAIRS; attempt++) {
573
- console.error('');
574
- const lastRound = verificationResult.rounds[verificationResult.rounds.length - 1];
575
- const judgeAnalysis = lastRound?.llmAnalysis ?? verificationResult.summary;
576
- const attributedFailures = lastRound?.attributedFailures ?? [];
577
- const allScenarios = [...scenarioResult.scenarios, ...accumulatedProbes];
578
- const { corrections, handwrittenWarnings } = extractScenarioCorrections(attributedFailures, allScenarios);
579
- for (const warning of handwrittenWarnings) {
580
- console.error(` ${chalk.yellow('Warning:')} ${warning}`);
581
- }
721
+ const { valid: filteredInitialProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(verificationResultInitial), 'Discarded probe (structural conflict)', unit.storedAnnotations, reporter);
722
+ const state = {
723
+ rules,
724
+ listDefinitions,
725
+ compilerSession,
726
+ dynamicLists,
727
+ serverPolicyFile,
728
+ verificationResult: verificationResultInitial,
729
+ accumulatedProbes: filteredInitialProbes,
730
+ repairAttempts: 0,
731
+ };
732
+ if (!state.verificationResult.pass) {
733
+ await this.runRepairLoop(state, unit, config, constitutionHash, inputHash, serverAnnotationsFile, serverDomainAllowlists, scenarioResult, filterEngine, serverToolNames, serverNames, serverTools, compilerSystem, model, logContext, reporter, serverOutputDir, writeServerPolicy);
734
+ }
735
+ return state;
736
+ }
737
+ /**
738
+ * Filters structurally conflicting scenarios and generates replacements
739
+ * for discarded ones via LLM repair (pre-loop, one-time).
740
+ */
741
+ async filterAndRepairDiscardedScenarios(filterEngine, scenarioResult, unit, permittedDirectories, dynamicLists, model, logContext, reporter) {
742
+ const { valid: filteredScenarios, discarded: discardedScenarios } = filterAndLogStructuralConflicts(filterEngine, scenarioResult.scenarios, undefined, unit.storedAnnotations, reporter);
743
+ if (discardedScenarios.length > 0) {
744
+ const discardedForRepair = discardedScenarios.map((d) => ({
745
+ scenario: d.scenario,
746
+ feedback: `${d.rule} always returns ${d.actual}`,
747
+ }));
748
+ logContext.stepName = `repair-scenarios-${unit.serverName}`;
749
+ reporter.update('repair-scenarios');
750
+ const repairStart = Date.now();
751
+ const replacementScenarios = await repairScenarios(discardedForRepair, unit.constitutionText, unit.storedAnnotations, unit.allowedDirectory, model, permittedDirectories, dynamicLists, (msg) => reporter.warn(` ${chalk.dim(msg)}`));
752
+ if (replacementScenarios.length > 0) {
753
+ const { valid: validReplacements } = filterAndLogStructuralConflicts(filterEngine, replacementScenarios, 'Discarded replacement (structural conflict)', unit.storedAnnotations, reporter);
754
+ scenarioResult.scenarios.push(...validReplacements);
755
+ filteredScenarios.push(...validReplacements);
756
+ reporter.warn(` ${chalk.dim(`Repaired ${discardedScenarios.length} discarded scenario(s) → ${validReplacements.length} replacement(s)`)}`);
757
+ }
758
+ const repairElapsed = (Date.now() - repairStart) / 1000;
759
+ reporter.complete('repair-scenarios', ` ${unit.serverName}: Repaired scenarios (${repairElapsed.toFixed(1)}s)`, repairElapsed);
760
+ }
761
+ return { filteredScenarios };
762
+ }
763
+ /**
764
+ * Compile-verify-repair loop (up to 2 repair attempts).
765
+ * Mutates `state` in place with updated rules, lists, and verification results.
766
+ */
767
+ async runRepairLoop(state, unit, config, constitutionHash, inputHash, serverAnnotationsFile, serverDomainAllowlists, scenarioResult, filterEngine, serverToolNames, serverNames, serverTools, compilerSystem, model, logContext, reporter, serverOutputDir, writeServerPolicy) {
768
+ const MAX_REPAIRS = 3;
769
+ // Each iteration consumes the just-completed verification (initial verify
770
+ // on attempt 0, prior repair-verify on attempts 1..MAX_REPAIRS), exits early
771
+ // if the result is clean, otherwise repairs and re-verifies. Consuming at
772
+ // the top of the loop ensures the final repair-verify's corrections are
773
+ // applied — no post-loop catch-up needed.
774
+ for (let attempt = 0; attempt <= MAX_REPAIRS; attempt++) {
775
+ const lastRound = state.verificationResult.rounds[state.verificationResult.rounds.length - 1];
776
+ const judgeAnalysis = lastRound?.llmAnalysis ?? state.verificationResult.summary;
777
+ const attributedFailures = lastRound?.attributedFailures ?? [];
778
+ const allScenarios = [...scenarioResult.scenarios, ...state.accumulatedProbes];
779
+ const { corrections, handwrittenWarnings } = extractScenarioCorrections(attributedFailures, allScenarios);
780
+ for (const warning of handwrittenWarnings) {
781
+ reporter.warn(` ${chalk.yellow('Warning:')} ${warning}`);
782
+ }
783
+ if (corrections.length > 0) {
784
+ scenarioResult.scenarios = applyScenarioCorrections(scenarioResult.scenarios, corrections);
785
+ const correctedProbes = applyScenarioCorrections(state.accumulatedProbes, corrections);
786
+ state.accumulatedProbes.splice(0, state.accumulatedProbes.length, ...correctedProbes);
787
+ reporter.warn(` ${chalk.dim(`Corrected ${corrections.length} scenario expectation(s)`)}`);
788
+ }
789
+ const attributionByDescription = new Map(attributedFailures.map((a) => [a.scenarioDescription, a]));
790
+ const ruleBlamedFailures = state.verificationResult.failedScenarios.filter((f) => {
791
+ const attr = attributionByDescription.get(f.scenario.description);
792
+ if (!attr || attr.blame.kind === 'rule' || attr.blame.kind === 'both')
793
+ return true;
794
+ return handwrittenWarnings.some((w) => w.includes(f.scenario.description));
795
+ });
796
+ // Clean exit: rules are correct (already passing, or only scenario-blame
797
+ // failures that we just corrected). Synthesize a passing result when
798
+ // corrections resolved the remaining failures.
799
+ if (state.verificationResult.pass)
800
+ break;
801
+ if (ruleBlamedFailures.length === 0) {
582
802
  if (corrections.length > 0) {
583
- scenarioResult.scenarios = applyScenarioCorrections(scenarioResult.scenarios, corrections);
584
- const correctedProbes = applyScenarioCorrections(accumulatedProbes, corrections);
585
- accumulatedProbes.splice(0, accumulatedProbes.length, ...correctedProbes);
586
- console.error(` ${chalk.dim(`Corrected ${corrections.length} scenario expectation(s)`)}`);
587
- }
588
- ({ valid: currentFilteredScenarios } = filterAndLogStructuralConflicts(filterEngine, scenarioResult.scenarios, undefined, unit.storedAnnotations));
589
- const allRuleBlamedFailures = verificationResult.failedScenarios.filter((f) => {
590
- const attr = attributedFailures.find((a) => a.scenarioDescription === f.scenario.description);
591
- if (!attr || attr.blame.kind === 'rule' || attr.blame.kind === 'both')
592
- return true;
593
- return handwrittenWarnings.some((w) => w.includes(f.scenario.description));
594
- });
595
- if (allRuleBlamedFailures.length > 0) {
596
- const repairContext = {
597
- failedScenarios: allRuleBlamedFailures,
598
- judgeAnalysis,
599
- attemptNumber: attempt,
600
- existingListDefinitions: listDefinitions.length > 0 ? listDefinitions : undefined,
601
- handwrittenScenarios: unit.handwrittenScenarios.length > 0 ? unit.handwrittenScenarios : undefined,
803
+ const correctionNote = `\n\n[pipeline-runner: cleared ${corrections.length} scenario expectation(s) via judge attribution; no rule-blamed failures remained.]`;
804
+ state.verificationResult = {
805
+ ...state.verificationResult,
806
+ pass: true,
807
+ failedScenarios: [],
808
+ summary: `${state.verificationResult.summary}${correctionNote}`,
602
809
  };
603
- this.logContext.stepName = `repair-compile-${unit.serverName}-${attempt}`;
604
- const repairText = ` ${unit.serverName} repair ${attempt}/${MAX_REPAIRS}: Recompiling`;
605
- const { result: repairResult } = await withSpinner(repairText, async (spinner) => this.compilePolicyRulesWithPointFix(rules, unit.annotations, unit.protectedPaths, baseInputHash, repairContext, compilerSystem, compilerSession, listDefinitions, (msg) => {
606
- spinner.text = `${repairText} — ${msg}`;
607
- }), (r, elapsed) => `${repairText}: ${r.rules.length} rules (${elapsed.toFixed(1)}s)`);
608
- rules = repairResult.rules;
609
- listDefinitions = repairResult.listDefinitions;
610
- compilerSession = repairResult.session;
611
- // Re-validate server scoping after repair
612
- validateServerScoping(unit.serverName, rules);
613
- // Re-resolve dynamic lists if repair changed list definitions
614
- if (listDefinitions.length > 0) {
615
- dynamicLists = await this.resolveServerLists(listDefinitions, serverOutputDir, config, ` ${unit.serverName}`);
616
- }
617
- serverPolicyFile = buildPolicyArtifact(constitutionHash, rules, listDefinitions, inputHash);
618
- // Persist repaired rules immediately
619
- writeServerPolicy();
620
- verifierSystem = this.cacheStrategy.wrapSystemPrompt(buildJudgeSystemPrompt(unit.constitutionText, serverPolicyFile, unit.protectedPaths, serverTools, dynamicLists, unit.allowedDirectory, unit.storedAnnotations));
621
- verifierSession = new PolicyVerifierSession({
622
- system: verifierSystem,
623
- model: this.model,
624
- serverNames,
625
- toolNames: serverToolNames,
626
- storedAnnotations: unit.storedAnnotations,
627
- });
628
- }
629
- else {
630
- console.error(` ${chalk.dim('No rule-blamed failures — skipping recompilation')}`);
631
- }
632
- this.logContext.stepName = `repair-verify-${unit.serverName}-${attempt}`;
633
- const scenariosForRepairVerify = [...currentFilteredScenarios, ...accumulatedProbes];
634
- const repairVerifyText = ` ${unit.serverName} repair ${attempt}/${MAX_REPAIRS}: Verifying`;
635
- const { result: repairVerifyResult } = await withSpinner(repairVerifyText, async (spinner) => verifyPolicy(unit.constitutionText, serverPolicyFile, serverAnnotationsFile, unit.protectedPaths, scenariosForRepairVerify, this.model, 1, unit.allowedDirectory, (msg) => {
636
- spinner.text = `${repairVerifyText} — ${msg}`;
637
- }, serverDomainAllowlists, dynamicLists, verifierSystem, verifierSession, unit.storedAnnotations), (r, elapsed) => r.pass
638
- ? `${repairVerifyText}: passed (${elapsed.toFixed(1)}s)`
639
- : `${repairVerifyText}: ${r.failedScenarios.length} failure(s) (${elapsed.toFixed(1)}s)`);
640
- verificationResult = repairVerifyResult;
641
- if (!verificationResult.pass) {
642
- this.logVerboseFailures(verificationResult);
643
- }
644
- const { valid: validRepairProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(verificationResult), 'Discarded probe (structural conflict)', unit.storedAnnotations);
645
- accumulatedProbes.push(...validRepairProbes);
646
- repairAttempts = attempt;
647
- if (verificationResult.pass) {
648
- break;
649
810
  }
811
+ break;
650
812
  }
813
+ // Out of repair budget — exit with the current (failing) result.
814
+ if (attempt === MAX_REPAIRS)
815
+ break;
816
+ const repairAttempt = attempt + 1;
817
+ const { valid: currentFilteredScenarios } = filterAndLogStructuralConflicts(filterEngine, scenarioResult.scenarios, undefined, unit.storedAnnotations, reporter);
818
+ const repairContext = {
819
+ failedScenarios: ruleBlamedFailures,
820
+ judgeAnalysis,
821
+ attemptNumber: repairAttempt,
822
+ existingListDefinitions: state.listDefinitions.length > 0 ? state.listDefinitions : undefined,
823
+ handwrittenScenarios: unit.handwrittenScenarios.length > 0 ? unit.handwrittenScenarios : undefined,
824
+ };
825
+ logContext.stepName = `repair-compile-${unit.serverName}-${repairAttempt}`;
826
+ reporter.update('repair-compile', `attempt ${repairAttempt}/${MAX_REPAIRS}`);
827
+ const repairCompileStart = Date.now();
828
+ const repairResult = await this.compilePolicyRulesWithPointFix(state.rules, unit.storedAnnotations, unit.protectedPaths, inputHash, repairContext, compilerSystem, state.compilerSession, state.listDefinitions, model, (msg) => reporter.update('repair-compile', msg), reporter);
829
+ const repairCompileElapsed = (Date.now() - repairCompileStart) / 1000;
830
+ reporter.complete('repair-compile', ` ${unit.serverName} repair ${repairAttempt}/${MAX_REPAIRS}: ${repairResult.rules.length} rules (${repairCompileElapsed.toFixed(1)}s)`, repairCompileElapsed);
831
+ state.rules = repairResult.rules;
832
+ state.listDefinitions = repairResult.listDefinitions;
833
+ state.compilerSession = repairResult.session;
834
+ validateServerScoping(unit.serverName, state.rules);
835
+ if (state.listDefinitions.length > 0) {
836
+ state.dynamicLists = await this.resolveServerLists(state.listDefinitions, serverOutputDir, config, ` ${unit.serverName}`, model, logContext, reporter);
837
+ }
838
+ state.serverPolicyFile = buildPolicyArtifact(constitutionHash, state.rules, state.listDefinitions, inputHash);
839
+ writeServerPolicy();
840
+ const verifierSystem = this.cacheStrategy.wrapSystemPrompt(buildJudgeSystemPrompt(unit.constitutionText, state.serverPolicyFile, unit.protectedPaths, serverTools, state.dynamicLists, unit.allowedDirectory, unit.storedAnnotations));
841
+ const verifierSession = new PolicyVerifierSession({
842
+ system: verifierSystem,
843
+ model,
844
+ serverNames,
845
+ toolNames: serverToolNames,
846
+ storedAnnotations: unit.storedAnnotations,
847
+ });
848
+ logContext.stepName = `repair-verify-${unit.serverName}-${repairAttempt}`;
849
+ const scenariosForRepairVerify = [...currentFilteredScenarios, ...state.accumulatedProbes];
850
+ reporter.update('repair-verify', `attempt ${repairAttempt}/${MAX_REPAIRS}`);
851
+ const repairVerifyStart = Date.now();
852
+ const repairVerifyResult = await verifyPolicy(unit.constitutionText, state.serverPolicyFile, serverAnnotationsFile, unit.protectedPaths, scenariosForRepairVerify, model, 1, unit.allowedDirectory, (msg) => reporter.update('repair-verify', msg), serverDomainAllowlists, state.dynamicLists, verifierSystem, verifierSession, unit.storedAnnotations);
853
+ const repairVerifyElapsed = (Date.now() - repairVerifyStart) / 1000;
854
+ const repairVerifyText = ` ${unit.serverName} repair ${repairAttempt}/${MAX_REPAIRS}: Verifying`;
855
+ reporter.complete('repair-verify', repairVerifyResult.pass
856
+ ? `${repairVerifyText}: passed (${repairVerifyElapsed.toFixed(1)}s)`
857
+ : `${repairVerifyText}: ${repairVerifyResult.failedScenarios.length} failure(s) (${repairVerifyElapsed.toFixed(1)}s)`, repairVerifyElapsed);
858
+ state.verificationResult = repairVerifyResult;
859
+ if (!state.verificationResult.pass) {
860
+ this.logVerboseFailures(state.verificationResult, reporter);
861
+ }
862
+ const { valid: validRepairProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(state.verificationResult), 'Discarded probe (structural conflict)', unit.storedAnnotations, reporter);
863
+ state.accumulatedProbes.push(...validRepairProbes);
864
+ state.repairAttempts = repairAttempt;
651
865
  }
652
- // Write per-server artifacts (before verification check so they can be inspected on failure)
653
- const finalScenarios = [...scenarioResult.scenarios, ...accumulatedProbes];
654
- writeServerPolicy();
655
- writeArtifact(serverOutputDir, 'test-scenarios.json', {
656
- generatedAt: new Date().toISOString(),
657
- constitutionHash,
658
- inputHash: scenarioResult.inputHash,
659
- scenarios: finalScenarios,
660
- });
661
- if (!verificationResult.pass) {
662
- throw new Error(`Verification FAILED for server "${unit.serverName}" — artifacts written for inspection but policy may need review.`);
663
- }
664
- console.error(` ${chalk.green(unit.serverName)}: ${rules.length} rules, ${finalScenarios.length} scenarios` +
665
- (repairAttempts > 0 ? `, ${repairAttempts} repair(s)` : ''));
666
- return {
667
- serverName: unit.serverName,
668
- rules,
669
- listDefinitions,
670
- scenarios: finalScenarios,
671
- inputHash,
672
- constitutionHash,
673
- resolvedLists: dynamicLists,
674
- };
675
866
  }
676
867
  /**
677
868
  * Compiles policy rules for a single server (initial compilation).
678
869
  * Uses per-server schema with requireServer: true.
679
870
  */
680
- async compileServerPolicyRules(unit, system, inputHash) {
681
- const stepText = ` ${unit.serverName}: Compiling rules`;
871
+ async compileServerPolicyRules(unit, system, inputHash, model, reporter) {
872
+ reporter.update('compiling');
873
+ const start = Date.now();
682
874
  const session = new ConstitutionCompilerSession({
683
875
  system,
684
- model: this.model,
685
- annotations: unit.annotations,
876
+ model,
877
+ annotations: unit.storedAnnotations,
686
878
  schemaOptions: { requireServer: true },
687
879
  });
688
- const { result: compilationOutput } = await withSpinner(stepText, async (spinner) => {
689
- const output = await session.compile((msg) => {
690
- spinner.text = `${stepText} — ${msg}`;
691
- });
692
- const compiledRules = resolveRulePaths(output.rules);
693
- validateRulesOrThrow(compiledRules, output.listDefinitions);
694
- return { rules: compiledRules, listDefinitions: output.listDefinitions };
695
- }, (output, elapsed) => `${stepText}: ${output.rules.length} rules (${elapsed.toFixed(1)}s)`);
880
+ const output = await session.compile((msg) => reporter.update('compiling', msg));
881
+ const compiledRules = resolveRulePaths(output.rules);
882
+ validateRulesOrThrow(compiledRules, output.listDefinitions, reporter);
883
+ const elapsed = (Date.now() - start) / 1000;
884
+ reporter.complete('compiling', ` ${unit.serverName}: Compiling rules: ${compiledRules.length} rules (${elapsed.toFixed(1)}s)`, elapsed);
696
885
  return {
697
- rules: compilationOutput.rules,
698
- listDefinitions: compilationOutput.listDefinitions,
886
+ rules: compiledRules,
887
+ listDefinitions: output.listDefinitions,
699
888
  inputHash,
700
889
  session,
701
890
  };
@@ -703,7 +892,7 @@ export class PipelineRunner {
703
892
  // -----------------------------------------------------------------------
704
893
  // Private compilation methods
705
894
  // -----------------------------------------------------------------------
706
- async compilePolicyRulesWithRepair(annotations, protectedPaths, baseInputHash, repairContext, system, session, onProgress) {
895
+ async compilePolicyRulesWithRepair(annotations, protectedPaths, baseInputHash, repairContext, system, session, model, onProgress, reporter) {
707
896
  let output;
708
897
  if (session) {
709
898
  output = await session.recompile(repairContext, onProgress);
@@ -711,10 +900,10 @@ export class PipelineRunner {
711
900
  else {
712
901
  const { compileConstitution } = await import('./constitution-compiler.js');
713
902
  output = await compileConstitution('', // Not needed when system prompt is provided
714
- annotations, { protectedPaths }, this.model, repairContext, onProgress, system);
903
+ annotations, { protectedPaths }, model, repairContext, onProgress, system);
715
904
  }
716
905
  const compiledRules = resolveRulePaths(output.rules);
717
- validateRulesOrThrow(compiledRules, output.listDefinitions);
906
+ validateRulesOrThrow(compiledRules, output.listDefinitions, reporter);
718
907
  return {
719
908
  rules: compiledRules,
720
909
  listDefinitions: output.listDefinitions,
@@ -728,17 +917,17 @@ export class PipelineRunner {
728
917
  * patch instead of regenerating the entire rule set. This avoids oscillation
729
918
  * where fixing one failure breaks previously-passing rules.
730
919
  */
731
- async compilePolicyRulesWithPointFix(existingRules, annotations, protectedPaths, baseInputHash, repairContext, system, session, existingListDefinitions, onProgress) {
920
+ async compilePolicyRulesWithPointFix(existingRules, annotations, protectedPaths, baseInputHash, repairContext, system, session, existingListDefinitions, model, onProgress, reporter) {
732
921
  let output;
733
922
  if (session) {
734
923
  output = await session.repairPointFix(existingRules, repairContext, existingListDefinitions, onProgress);
735
924
  }
736
925
  else {
737
926
  // No session available -- fall back to full recompile
738
- return this.compilePolicyRulesWithRepair(annotations, protectedPaths, baseInputHash, repairContext, system, session, onProgress);
927
+ return this.compilePolicyRulesWithRepair(annotations, protectedPaths, baseInputHash, repairContext, system, session, model, onProgress, reporter);
739
928
  }
740
929
  const compiledRules = resolveRulePaths(output.rules);
741
- validateRulesOrThrow(compiledRules, output.listDefinitions);
930
+ validateRulesOrThrow(compiledRules, output.listDefinitions, reporter);
742
931
  return {
743
932
  rules: compiledRules,
744
933
  listDefinitions: output.listDefinitions,
@@ -751,25 +940,24 @@ export class PipelineRunner {
751
940
  * Loads existing per-server dynamic-lists.json for cache comparison,
752
941
  * connects MCP servers if needed, and writes results to serverOutputDir.
753
942
  */
754
- async resolveServerLists(listDefinitions, serverOutputDir, config, labelPrefix) {
755
- this.logContext.stepName = `resolve-lists-${labelPrefix.trim()}`;
943
+ async resolveServerLists(listDefinitions, serverOutputDir, config, labelPrefix, model, logContext, reporter) {
944
+ logContext.stepName = `resolve-lists-${labelPrefix.trim()}`;
756
945
  const existingLists = loadExistingArtifact(serverOutputDir, 'dynamic-lists.json');
757
946
  const needsMcp = listDefinitions.some((d) => d.requiresMcp);
758
947
  let proxy;
759
948
  if (needsMcp && config.mcpServers) {
760
949
  proxy = await connectViaProxy(listDefinitions, config.mcpServers, config.toolAnnotationsDir);
761
950
  }
762
- const listStepText = `${labelPrefix}: Resolving dynamic lists`;
951
+ reporter.update('lists');
952
+ const start = Date.now();
763
953
  try {
764
- const { result: resolvedLists } = await withSpinner(listStepText, async (spinner) => resolveAllLists(listDefinitions, {
765
- model: this.model,
954
+ const resolvedLists = await resolveAllLists(listDefinitions, {
955
+ model,
766
956
  proxyConnection: proxy ? { client: proxy.client, tools: proxy.tools } : undefined,
767
- }, existingLists, (msg) => {
768
- spinner.text = `${listStepText} ${msg}`;
769
- }), (result, elapsed) => {
770
- const count = Object.keys(result.lists).length;
771
- return `${listStepText}: ${count} list(s) resolved (${elapsed.toFixed(1)}s)`;
772
- });
957
+ }, existingLists, (msg) => reporter.update('lists', msg));
958
+ const elapsed = (Date.now() - start) / 1000;
959
+ const count = Object.keys(resolvedLists.lists).length;
960
+ reporter.complete('lists', `${labelPrefix}: Resolving dynamic lists: ${count} list(s) resolved (${elapsed.toFixed(1)}s)`, elapsed);
773
961
  writeArtifact(serverOutputDir, 'dynamic-lists.json', resolvedLists);
774
962
  return resolvedLists;
775
963
  }
@@ -779,28 +967,28 @@ export class PipelineRunner {
779
967
  }
780
968
  }
781
969
  }
782
- async generateTestScenarios(constitutionText, annotations, allowedDirectory, handwrittenScenarios, inputHash, existingScenarios, stepLabel, permittedDirectories, dynamicLists, storedAnnotations) {
970
+ async generateTestScenarios(constitutionText, annotations, allowedDirectory, handwrittenScenarios, inputHash, existingScenarios, stepLabel, permittedDirectories, dynamicLists, model, reporter) {
783
971
  const stepText = `${stepLabel} Generating test scenarios`;
784
972
  if (existingScenarios && existingScenarios.inputHash === inputHash) {
785
- showCached(stepText);
973
+ reporter.complete('scenarios', `${stepText} ${chalk.dim('(cached)')}`, 0);
786
974
  return { scenarios: existingScenarios.scenarios, inputHash };
787
975
  }
788
- const { result: scenarios } = await withSpinner(stepText, async (spinner) => generateScenarios(constitutionText, annotations, handwrittenScenarios, allowedDirectory, this.model, permittedDirectories, (msg) => {
789
- spinner.text = `${stepText} — ${msg}`;
790
- }, dynamicLists, (prompt) => this.cacheStrategy.wrapSystemPrompt(prompt), storedAnnotations), (scenarios, elapsed) => {
791
- const generatedCount = scenarios.length - handwrittenScenarios.length;
792
- return `${stepText}: ${scenarios.length} scenarios (${handwrittenScenarios.length} handwritten + ${generatedCount} generated) (${elapsed.toFixed(1)}s)`;
793
- });
976
+ reporter.update('scenarios');
977
+ const start = Date.now();
978
+ const scenarios = await generateScenarios(constitutionText, annotations, handwrittenScenarios, allowedDirectory, model, permittedDirectories, (msg) => reporter.update('scenarios', msg), dynamicLists, (prompt) => this.cacheStrategy.wrapSystemPrompt(prompt));
979
+ const elapsed = (Date.now() - start) / 1000;
980
+ const generatedCount = scenarios.length - handwrittenScenarios.length;
981
+ reporter.complete('scenarios', `${stepText}: ${scenarios.length} scenarios (${handwrittenScenarios.length} handwritten + ${generatedCount} generated) (${elapsed.toFixed(1)}s)`, elapsed);
794
982
  return { scenarios, inputHash };
795
983
  }
796
- logVerboseFailures(result) {
797
- console.error('');
798
- console.error(chalk.red('Verification FAILED:'));
799
- console.error(result.summary);
800
- console.error('');
984
+ logVerboseFailures(result, reporter) {
985
+ reporter.warn('');
986
+ reporter.warn(chalk.red('Verification FAILED:'));
987
+ reporter.warn(result.summary);
988
+ reporter.warn('');
801
989
  for (const f of result.failedScenarios) {
802
- console.error(` ${chalk.red('FAIL:')} ${f.scenario.description}`);
803
- console.error(` Expected: ${f.scenario.expectedDecision}, Got: ${f.actualDecision} (rule: ${f.matchingRule})`);
990
+ reporter.warn(` ${chalk.red('FAIL:')} ${f.scenario.description}`);
991
+ reporter.warn(` Expected: ${f.scenario.expectedDecision}, Got: ${f.actualDecision} (rule: ${f.matchingRule})`);
804
992
  }
805
993
  }
806
994
  }