@jevitate/cli 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (410) hide show
  1. package/README.md +23 -0
  2. package/dist/ai-cli.d.ts +31 -11
  3. package/dist/ai-cli.js +131 -88
  4. package/dist/ai-cli.js.map +1 -1
  5. package/dist/author-journey-cli.d.ts +4 -0
  6. package/dist/author-journey-cli.js +111 -0
  7. package/dist/author-journey-cli.js.map +1 -0
  8. package/dist/bin.js +60349 -16477
  9. package/dist/bin.js.map +1 -1
  10. package/dist/browser-run-options.d.ts +89 -0
  11. package/dist/browser-run-options.js +175 -0
  12. package/dist/browser-run-options.js.map +1 -0
  13. package/dist/build-info.generated.d.ts +2 -0
  14. package/dist/build-info.generated.js +10 -0
  15. package/dist/build-info.generated.js.map +1 -0
  16. package/dist/campaign-api.d.ts +151 -0
  17. package/dist/campaign-api.js +446 -0
  18. package/dist/campaign-api.js.map +1 -0
  19. package/dist/campaign-cli.d.ts +6 -0
  20. package/dist/campaign-cli.js +66 -0
  21. package/dist/campaign-cli.js.map +1 -0
  22. package/dist/check-api.d.ts +5 -0
  23. package/dist/check-api.js +234 -0
  24. package/dist/check-api.js.map +1 -0
  25. package/dist/check-budget.d.ts +26 -0
  26. package/dist/check-budget.js +90 -0
  27. package/dist/check-budget.js.map +1 -0
  28. package/dist/check-cli.d.ts +31 -0
  29. package/dist/check-cli.js +109 -0
  30. package/dist/check-cli.js.map +1 -0
  31. package/dist/check-execute.d.ts +29 -0
  32. package/dist/check-execute.js +420 -0
  33. package/dist/check-execute.js.map +1 -0
  34. package/dist/check-plan.d.ts +118 -0
  35. package/dist/check-plan.js +516 -0
  36. package/dist/check-plan.js.map +1 -0
  37. package/dist/check-suite.d.ts +184 -0
  38. package/dist/check-suite.js +540 -0
  39. package/dist/check-suite.js.map +1 -0
  40. package/dist/check-types.d.ts +181 -0
  41. package/dist/check-types.js +35 -0
  42. package/dist/check-types.js.map +1 -0
  43. package/dist/cli-args.d.ts +26 -0
  44. package/dist/cli-args.js +50 -0
  45. package/dist/cli-args.js.map +1 -0
  46. package/dist/cli-output.d.ts +106 -0
  47. package/dist/cli-output.js +563 -0
  48. package/dist/cli-output.js.map +1 -0
  49. package/dist/cli-reference.d.ts +3 -0
  50. package/dist/cli-reference.js +81 -0
  51. package/dist/cli-reference.js.map +1 -0
  52. package/dist/cli-refusal.d.ts +12 -0
  53. package/dist/cli-refusal.js +52 -0
  54. package/dist/cli-refusal.js.map +1 -0
  55. package/dist/cli-shared.d.ts +340 -0
  56. package/dist/cli-shared.js +537 -0
  57. package/dist/cli-shared.js.map +1 -0
  58. package/dist/cli-surface.d.ts +33 -0
  59. package/dist/cli-surface.js +48 -0
  60. package/dist/cli-surface.js.map +1 -0
  61. package/dist/conversation-options.d.ts +15 -0
  62. package/dist/conversation-options.js +10 -0
  63. package/dist/conversation-options.js.map +1 -0
  64. package/dist/credentials-file.d.ts +15 -0
  65. package/dist/credentials-file.js +56 -0
  66. package/dist/credentials-file.js.map +1 -0
  67. package/dist/data-dir.d.ts +1 -1
  68. package/dist/data-dir.js +1 -1
  69. package/dist/defect-evidence.d.ts +141 -0
  70. package/dist/defect-evidence.js +505 -0
  71. package/dist/defect-evidence.js.map +1 -0
  72. package/dist/demo-aspect-api.d.ts +209 -0
  73. package/dist/demo-aspect-api.js +398 -0
  74. package/dist/demo-aspect-api.js.map +1 -0
  75. package/dist/demo-aspect-cli.d.ts +3 -0
  76. package/dist/demo-aspect-cli.js +243 -0
  77. package/dist/demo-aspect-cli.js.map +1 -0
  78. package/dist/demo-capture.d.ts +142 -0
  79. package/dist/demo-capture.js +537 -0
  80. package/dist/demo-capture.js.map +1 -0
  81. package/dist/engine.d.ts +35 -0
  82. package/dist/engine.js +25 -0
  83. package/dist/engine.js.map +1 -0
  84. package/dist/environments.d.ts +155 -0
  85. package/dist/environments.js +364 -0
  86. package/dist/environments.js.map +1 -0
  87. package/dist/exit-codes.d.ts +46 -0
  88. package/dist/exit-codes.js +91 -0
  89. package/dist/exit-codes.js.map +1 -0
  90. package/dist/explore-adversarial.d.ts +144 -0
  91. package/dist/explore-adversarial.js +227 -0
  92. package/dist/explore-adversarial.js.map +1 -0
  93. package/dist/explore-api.d.ts +7 -187
  94. package/dist/explore-api.js +7 -301
  95. package/dist/explore-api.js.map +1 -1
  96. package/dist/explore-author.d.ts +79 -0
  97. package/dist/explore-author.js +86 -0
  98. package/dist/explore-author.js.map +1 -0
  99. package/dist/explore-cli.d.ts +7 -0
  100. package/dist/explore-cli.js +1199 -0
  101. package/dist/explore-cli.js.map +1 -0
  102. package/dist/explore-coverage.d.ts +173 -0
  103. package/dist/explore-coverage.js +246 -0
  104. package/dist/explore-coverage.js.map +1 -0
  105. package/dist/explore-feature.d.ts +125 -0
  106. package/dist/explore-feature.js +218 -0
  107. package/dist/explore-feature.js.map +1 -0
  108. package/dist/explore-goal.d.ts +271 -0
  109. package/dist/explore-goal.js +417 -0
  110. package/dist/explore-goal.js.map +1 -0
  111. package/dist/explore-shared.d.ts +177 -0
  112. package/dist/explore-shared.js +149 -0
  113. package/dist/explore-shared.js.map +1 -0
  114. package/dist/explore-specs.d.ts +53 -0
  115. package/dist/explore-specs.js +333 -0
  116. package/dist/explore-specs.js.map +1 -0
  117. package/dist/findings-filing.d.ts +64 -0
  118. package/dist/findings-filing.js +108 -0
  119. package/dist/findings-filing.js.map +1 -0
  120. package/dist/fixture-auth.d.ts +63 -0
  121. package/dist/fixture-auth.js +95 -0
  122. package/dist/fixture-auth.js.map +1 -0
  123. package/dist/fixture-cli.d.ts +99 -0
  124. package/dist/fixture-cli.js +292 -0
  125. package/dist/fixture-cli.js.map +1 -0
  126. package/dist/github-issue-filer.d.ts +62 -0
  127. package/dist/github-issue-filer.js +189 -0
  128. package/dist/github-issue-filer.js.map +1 -0
  129. package/dist/host-health-run.d.ts +25 -0
  130. package/dist/host-health-run.js +33 -0
  131. package/dist/host-health-run.js.map +1 -0
  132. package/dist/http-5xx-outcome.d.ts +6 -0
  133. package/dist/http-5xx-outcome.js +39 -0
  134. package/dist/http-5xx-outcome.js.map +1 -0
  135. package/dist/inbox-cli.d.ts +7 -0
  136. package/dist/inbox-cli.js +197 -0
  137. package/dist/inbox-cli.js.map +1 -0
  138. package/dist/index.d.ts +1 -0
  139. package/dist/index.js +1 -0
  140. package/dist/index.js.map +1 -1
  141. package/dist/init-cli.d.ts +4 -0
  142. package/dist/init-cli.js +156 -0
  143. package/dist/init-cli.js.map +1 -0
  144. package/dist/init-keys.d.ts +30 -2
  145. package/dist/init-keys.js +19 -5
  146. package/dist/init-keys.js.map +1 -1
  147. package/dist/init-next-steps.d.ts +35 -0
  148. package/dist/init-next-steps.js +79 -0
  149. package/dist/init-next-steps.js.map +1 -0
  150. package/dist/invariants-file.d.ts +34 -0
  151. package/dist/invariants-file.js +68 -0
  152. package/dist/invariants-file.js.map +1 -0
  153. package/dist/invariants-validate.d.ts +30 -0
  154. package/dist/invariants-validate.js +105 -0
  155. package/dist/invariants-validate.js.map +1 -0
  156. package/dist/journey-annotate-api.d.ts +76 -0
  157. package/dist/journey-annotate-api.js +269 -0
  158. package/dist/journey-annotate-api.js.map +1 -0
  159. package/dist/journey-annotate-cli.d.ts +9 -0
  160. package/dist/journey-annotate-cli.js +181 -0
  161. package/dist/journey-annotate-cli.js.map +1 -0
  162. package/dist/journey-api.d.ts +104 -2
  163. package/dist/journey-api.js +163 -20
  164. package/dist/journey-api.js.map +1 -1
  165. package/dist/journey-cli.d.ts +4 -0
  166. package/dist/journey-cli.js +411 -0
  167. package/dist/journey-cli.js.map +1 -0
  168. package/dist/journey-demo-api.d.ts +114 -0
  169. package/dist/journey-demo-api.js +405 -0
  170. package/dist/journey-demo-api.js.map +1 -0
  171. package/dist/journey-demo-cli.d.ts +10 -0
  172. package/dist/journey-demo-cli.js +158 -0
  173. package/dist/journey-demo-cli.js.map +1 -0
  174. package/dist/journey-prefix.d.ts +194 -0
  175. package/dist/journey-prefix.js +279 -0
  176. package/dist/journey-prefix.js.map +1 -0
  177. package/dist/key-report.d.ts +49 -0
  178. package/dist/key-report.js +101 -0
  179. package/dist/key-report.js.map +1 -0
  180. package/dist/kill-signal.d.ts +193 -0
  181. package/dist/kill-signal.js +283 -0
  182. package/dist/kill-signal.js.map +1 -0
  183. package/dist/launch-armed.d.ts +37 -0
  184. package/dist/launch-armed.js +31 -0
  185. package/dist/launch-armed.js.map +1 -0
  186. package/dist/ledger-api.d.ts +106 -0
  187. package/dist/ledger-api.js +239 -0
  188. package/dist/ledger-api.js.map +1 -0
  189. package/dist/ledger-cli.d.ts +18 -0
  190. package/dist/ledger-cli.js +107 -0
  191. package/dist/ledger-cli.js.map +1 -0
  192. package/dist/load-api.d.ts +29 -11
  193. package/dist/load-api.js +23 -15
  194. package/dist/load-api.js.map +1 -1
  195. package/dist/load-cli.d.ts +4 -0
  196. package/dist/load-cli.js +97 -0
  197. package/dist/load-cli.js.map +1 -0
  198. package/dist/log-correlation.d.ts +200 -0
  199. package/dist/log-correlation.js +394 -0
  200. package/dist/log-correlation.js.map +1 -0
  201. package/dist/log-lines.d.ts +131 -0
  202. package/dist/log-lines.js +389 -0
  203. package/dist/log-lines.js.map +1 -0
  204. package/dist/log-sources.d.ts +71 -0
  205. package/dist/log-sources.js +236 -0
  206. package/dist/log-sources.js.map +1 -0
  207. package/dist/log-trace.d.ts +80 -0
  208. package/dist/log-trace.js +210 -0
  209. package/dist/log-trace.js.map +1 -0
  210. package/dist/logs-cli.d.ts +3 -0
  211. package/dist/logs-cli.js +51 -0
  212. package/dist/logs-cli.js.map +1 -0
  213. package/dist/logs-retention.d.ts +28 -0
  214. package/dist/logs-retention.js +85 -0
  215. package/dist/logs-retention.js.map +1 -0
  216. package/dist/masked-input.d.ts +22 -0
  217. package/dist/masked-input.js +99 -0
  218. package/dist/masked-input.js.map +1 -0
  219. package/dist/mcp-api.d.ts +91 -1
  220. package/dist/mcp-api.js +514 -18
  221. package/dist/mcp-api.js.map +1 -1
  222. package/dist/mcp-args.d.ts +50 -0
  223. package/dist/mcp-args.js +164 -0
  224. package/dist/mcp-args.js.map +1 -0
  225. package/dist/mcp-cli-bridge.d.ts +34 -0
  226. package/dist/mcp-cli-bridge.js +62 -0
  227. package/dist/mcp-cli-bridge.js.map +1 -0
  228. package/dist/mcp-cli-runner.d.ts +29 -0
  229. package/dist/mcp-cli-runner.js +44 -0
  230. package/dist/mcp-cli-runner.js.map +1 -0
  231. package/dist/mcp-cli-tools.d.ts +129 -0
  232. package/dist/mcp-cli-tools.js +737 -0
  233. package/dist/mcp-cli-tools.js.map +1 -0
  234. package/dist/mcp-paths.d.ts +28 -0
  235. package/dist/mcp-paths.js +76 -0
  236. package/dist/mcp-paths.js.map +1 -0
  237. package/dist/mission-actors.d.ts +40 -0
  238. package/dist/mission-actors.js +72 -0
  239. package/dist/mission-actors.js.map +1 -0
  240. package/dist/mission-api.d.ts +35 -0
  241. package/dist/mission-api.js +60 -3
  242. package/dist/mission-api.js.map +1 -1
  243. package/dist/mission-cli.d.ts +4 -0
  244. package/dist/mission-cli.js +248 -0
  245. package/dist/mission-cli.js.map +1 -0
  246. package/dist/mission-exit.d.ts +22 -0
  247. package/dist/mission-exit.js +26 -0
  248. package/dist/mission-exit.js.map +1 -0
  249. package/dist/mission-fixtures.d.ts +234 -0
  250. package/dist/mission-fixtures.js +862 -0
  251. package/dist/mission-fixtures.js.map +1 -0
  252. package/dist/mission-journal.d.ts +60 -0
  253. package/dist/mission-journal.js +115 -0
  254. package/dist/mission-journal.js.map +1 -0
  255. package/dist/mission-queue-cli.d.ts +10 -0
  256. package/dist/mission-queue-cli.js +198 -0
  257. package/dist/mission-queue-cli.js.map +1 -0
  258. package/dist/mission-queue-runner.d.ts +126 -0
  259. package/dist/mission-queue-runner.js +406 -0
  260. package/dist/mission-queue-runner.js.map +1 -0
  261. package/dist/multi-run-cli.d.ts +37 -0
  262. package/dist/multi-run-cli.js +116 -0
  263. package/dist/multi-run-cli.js.map +1 -0
  264. package/dist/multi-run.d.ts +339 -0
  265. package/dist/multi-run.js +619 -0
  266. package/dist/multi-run.js.map +1 -0
  267. package/dist/openrouter-call.d.ts +22 -0
  268. package/dist/openrouter-call.js +64 -0
  269. package/dist/openrouter-call.js.map +1 -0
  270. package/dist/profile-cli.d.ts +4 -0
  271. package/dist/profile-cli.js +57 -0
  272. package/dist/profile-cli.js.map +1 -0
  273. package/dist/program.d.ts +3 -75
  274. package/dist/program.js +123 -1921
  275. package/dist/program.js.map +1 -1
  276. package/dist/project-dir.d.ts +96 -0
  277. package/dist/project-dir.js +221 -0
  278. package/dist/project-dir.js.map +1 -0
  279. package/dist/record-api.d.ts +13 -7
  280. package/dist/record-api.js +44 -30
  281. package/dist/record-api.js.map +1 -1
  282. package/dist/record-cli.d.ts +4 -0
  283. package/dist/record-cli.js +76 -0
  284. package/dist/record-cli.js.map +1 -0
  285. package/dist/recording-cli.d.ts +4 -0
  286. package/dist/recording-cli.js +248 -0
  287. package/dist/recording-cli.js.map +1 -0
  288. package/dist/regression-api.d.ts +107 -0
  289. package/dist/regression-api.js +387 -6
  290. package/dist/regression-api.js.map +1 -1
  291. package/dist/regression-cli.d.ts +4 -0
  292. package/dist/regression-cli.js +284 -0
  293. package/dist/regression-cli.js.map +1 -0
  294. package/dist/report-api.d.ts +146 -0
  295. package/dist/report-api.js +431 -0
  296. package/dist/report-api.js.map +1 -0
  297. package/dist/report-cli.d.ts +12 -0
  298. package/dist/report-cli.js +147 -0
  299. package/dist/report-cli.js.map +1 -0
  300. package/dist/resource-preflight.d.ts +87 -0
  301. package/dist/resource-preflight.js +160 -0
  302. package/dist/resource-preflight.js.map +1 -0
  303. package/dist/result-schema.d.ts +22 -0
  304. package/dist/result-schema.js +28 -0
  305. package/dist/result-schema.js.map +1 -0
  306. package/dist/run-index.d.ts +29 -0
  307. package/dist/run-index.js +70 -0
  308. package/dist/run-index.js.map +1 -0
  309. package/dist/run-key-preflight.d.ts +42 -0
  310. package/dist/run-key-preflight.js +82 -0
  311. package/dist/run-key-preflight.js.map +1 -0
  312. package/dist/run-screenshots.d.ts +123 -0
  313. package/dist/run-screenshots.js +235 -0
  314. package/dist/run-screenshots.js.map +1 -0
  315. package/dist/runtime-env.d.ts +8 -0
  316. package/dist/runtime-env.js +23 -0
  317. package/dist/runtime-env.js.map +1 -0
  318. package/dist/secret-args.d.ts +23 -0
  319. package/dist/secret-args.js +45 -0
  320. package/dist/secret-args.js.map +1 -0
  321. package/dist/serve-cli.d.ts +4 -0
  322. package/dist/serve-cli.js +95 -0
  323. package/dist/serve-cli.js.map +1 -0
  324. package/dist/server-log-verify.d.ts +36 -0
  325. package/dist/server-log-verify.js +120 -0
  326. package/dist/server-log-verify.js.map +1 -0
  327. package/dist/session-check.d.ts +14 -0
  328. package/dist/session-check.js +44 -0
  329. package/dist/session-check.js.map +1 -0
  330. package/dist/site-cli.d.ts +4 -0
  331. package/dist/site-cli.js +127 -0
  332. package/dist/site-cli.js.map +1 -0
  333. package/dist/site-gate-cli.d.ts +28 -0
  334. package/dist/site-gate-cli.js +59 -0
  335. package/dist/site-gate-cli.js.map +1 -0
  336. package/dist/source-api.js +20 -4
  337. package/dist/source-api.js.map +1 -1
  338. package/dist/source-cli.d.ts +4 -0
  339. package/dist/source-cli.js +284 -0
  340. package/dist/source-cli.js.map +1 -0
  341. package/dist/source-run-api.d.ts +27 -1
  342. package/dist/source-run-api.js +22 -11
  343. package/dist/source-run-api.js.map +1 -1
  344. package/dist/storage-state-snapshot.d.ts +50 -0
  345. package/dist/storage-state-snapshot.js +87 -0
  346. package/dist/storage-state-snapshot.js.map +1 -0
  347. package/dist/suite-explore-options.d.ts +321 -0
  348. package/dist/suite-explore-options.js +162 -0
  349. package/dist/suite-explore-options.js.map +1 -0
  350. package/dist/target-config.d.ts +115 -0
  351. package/dist/target-config.js +225 -0
  352. package/dist/target-config.js.map +1 -0
  353. package/dist/transcript-file.d.ts +12 -0
  354. package/dist/transcript-file.js +23 -0
  355. package/dist/transcript-file.js.map +1 -0
  356. package/dist/type-fixture-file.d.ts +9 -0
  357. package/dist/type-fixture-file.js +48 -0
  358. package/dist/type-fixture-file.js.map +1 -0
  359. package/dist/ui-api.d.ts +3 -0
  360. package/dist/ui-api.js +11 -4
  361. package/dist/ui-api.js.map +1 -1
  362. package/dist/usability-capture.d.ts +43 -0
  363. package/dist/usability-capture.js +327 -0
  364. package/dist/usability-capture.js.map +1 -0
  365. package/dist/usage-config.d.ts +36 -0
  366. package/dist/usage-config.js +114 -0
  367. package/dist/usage-config.js.map +1 -0
  368. package/dist/ux-api.d.ts +3 -75
  369. package/dist/ux-api.js +3 -220
  370. package/dist/ux-api.js.map +1 -1
  371. package/dist/ux-claim-probe.d.ts +45 -0
  372. package/dist/ux-claim-probe.js +386 -0
  373. package/dist/ux-claim-probe.js.map +1 -0
  374. package/dist/ux-cli.d.ts +4 -0
  375. package/dist/ux-cli.js +109 -0
  376. package/dist/ux-cli.js.map +1 -0
  377. package/dist/ux-config.d.ts +29 -0
  378. package/dist/ux-config.js +103 -0
  379. package/dist/ux-config.js.map +1 -0
  380. package/dist/ux-evidence.d.ts +54 -0
  381. package/dist/ux-evidence.js +197 -0
  382. package/dist/ux-evidence.js.map +1 -0
  383. package/dist/ux-product.d.ts +14 -0
  384. package/dist/ux-product.js +39 -0
  385. package/dist/ux-product.js.map +1 -0
  386. package/dist/ux-review.d.ts +116 -0
  387. package/dist/ux-review.js +171 -0
  388. package/dist/ux-review.js.map +1 -0
  389. package/dist/ux-usability.d.ts +263 -0
  390. package/dist/ux-usability.js +580 -0
  391. package/dist/ux-usability.js.map +1 -0
  392. package/dist/verify-fix-api.d.ts +194 -0
  393. package/dist/verify-fix-api.js +506 -0
  394. package/dist/verify-fix-api.js.map +1 -0
  395. package/dist/verify-fix-cli.d.ts +4 -0
  396. package/dist/verify-fix-cli.js +115 -0
  397. package/dist/verify-fix-cli.js.map +1 -0
  398. package/package.json +23 -18
  399. package/skills/jevitate-ci-check/SKILL.md +77 -0
  400. package/skills/jevitate-demo/SKILL.md +61 -0
  401. package/skills/jevitate-explore/SKILL.md +96 -87
  402. package/skills/jevitate-getting-started/SKILL.md +89 -0
  403. package/skills/jevitate-load-test/SKILL.md +11 -2
  404. package/skills/jevitate-mission-scope/SKILL.md +19 -11
  405. package/skills/jevitate-record/SKILL.md +23 -5
  406. package/skills/jevitate-run-journey/SKILL.md +61 -17
  407. package/skills/jevitate-sources/SKILL.md +57 -14
  408. package/skills/jevitate-test-campaign/SKILL.md +203 -0
  409. package/skills/jevitate-ux-review/SKILL.md +106 -16
  410. package/skills/jevitate-verify-fix/SKILL.md +76 -0
package/dist/program.js CHANGED
@@ -1,1938 +1,140 @@
1
- import { readFile, writeFile, mkdir, mkdtemp, rm } from "node:fs/promises";
2
- import { dirname } from "node:path";
3
- import { tmpdir, userInfo } from "node:os";
4
- import { join } from "node:path";
5
1
  import { Command } from "commander";
6
- import * as clack from "@clack/prompts";
7
- import { SitePolicySchema, simulateTiming } from "@jevitate/domain";
8
- import { openDatabase, migrateToLatest, SqliteSitePolicyRepository } from "@jevitate/storage-sqlite";
9
- import { RecordingSchema, AuthoringTakeSchema, PostdocDecisionsSchema, promoteToVariable, diffTakes, applyPostdoc, flattenBaseFillSteps, fitInteractionPolicy, } from "@jevitate/recording";
10
- import { FsJourneyStore, JourneyRegistry, ParamValidationError } from "@jevitate/journey";
11
- import { envCredentialStore, requireKeys, MissingCredentialError, FakeGenerationGateway, OpenRouterGenerationGateway, JevJudgmentGateway, } from "@jevitate/ai-core";
12
- import { UnauthorizedExploreTargetError } from "@jevitate/explore";
13
- import { PlaywrightBrowserPort } from "@jevitate/playwright";
14
- import { CastActor, BrowseTheWeb } from "@jevitate/screenplay";
15
- import { safeRunPolicy } from "@jevitate/domain";
16
- import { makeExploreSelfHealer } from "./self-heal-adapter.js";
17
- import { ok, fail } from "./envelope.js";
18
- import { runJourneyProgrammatically, UnknownJourneyError } from "./journey-api.js";
19
- import { runJourneyLoadTest, UnknownLoadJourneyError } from "./load-api.js";
20
- import { runRegressionCapture } from "./regression-api.js";
21
- import { addMissionTarget, listMissionTargets, promoteMissionTarget, missionTargetContext, UnknownMissionTargetError, } from "./mission-api.js";
22
- import { startMcpServer } from "./mcp-api.js";
23
- import { startUiServer } from "./ui-api.js";
24
- import { registerAiCommands, realSecureIO } from "./ai-cli.js";
25
- import { collectAllMissingKeys } from "./init-keys.js";
26
- import { readCliVersion } from "./version.js";
27
- import { detectRuntimes, resolveInstallTargetPaths, installSkills, } from "./init-skills.js";
28
- import { registerMcp, resolveMcpTargetPaths, renderPrintConfig, } from "./init-mcp.js";
29
- import { loadManifest } from "@jevitate/skills";
30
- import { runExploration, runAuthorJourney, runCoverageMission, runAdversarialCliMission, runFeatureCliMission, parseAssertionSpec, resolveExploreAllowlist, } from "./explore-api.js";
31
- import { runUsabilityMission, runUxReview, UxAnalysisFailedError } from "./ux-api.js";
32
- import { resolveDataDir } from "./data-dir.js";
33
- import { runRecording, resolveRecordAllowlist, } from "./record-api.js";
34
- import { addSource, listSources, pullSource, updateSource, removeSource, trustJourney, publishJourneyToSource, realGhPort, NotPromotedError, NoDeclaredOriginsError, } from "./source-api.js";
35
- import { runSourceJourney, realResolvedJourneyRunner, } from "./source-run-api.js";
36
- import { FsTrustStore, FsAckStore, UnknownSourceError, EmbeddedSecretError, UndeclaredOriginError, UndeclaredTouError, HashMismatchError, UntrustedRiskyJourneyError, SourceValidationError, DEFAULT_LOCK_PATH, } from "@jevitate/sources";
37
- // `~/.jevitate/*` is the product's runtime-data convention (product = Jevitate).
38
- // See data-dir.ts.
39
- const DEFAULT_DB_PATH = resolveDataDir(["db.sqlite"]);
40
- const DEFAULT_JOURNEYS_DIR = resolveDataDir(["journeys"]);
41
- const DEFAULT_REGRESSIONS_DIR = resolveDataDir(["regressions"]);
42
- const DEFAULT_MISSION_TARGETS_DIR = resolveDataDir(["missions", "targets"]);
43
- const DEFAULT_INBOX_DIR = resolveDataDir(["inbox"]);
44
- function resolveDbPath(deps, flag) {
45
- return flag ?? deps.dbPath ?? DEFAULT_DB_PATH;
46
- }
47
- /**
48
- * Mirrors `resolveDbPath`'s flag > deps > home-dir-default convention: a
49
- * per-invocation `--dir` flag wins, then a `CliDeps.journeysDir` wired in by
50
- * the host, then `~/.jevitate/journeys`.
51
- */
52
- function resolveJourneysDir(deps, flag) {
53
- return flag ?? deps.journeysDir ?? DEFAULT_JOURNEYS_DIR;
54
- }
55
- /** Same flag > home-dir-default convention as `resolveJourneysDir`, for the
56
- * committed-regressions directory `regression capture` writes into. */
57
- function resolveRegressionsDir(flag) {
58
- return flag ?? DEFAULT_REGRESSIONS_DIR;
59
- }
60
- /** Same flag > deps > home-dir-default convention as `resolveJourneysDir`, for
61
- * the mission-targets store `mission target ...` reads/writes. */
62
- function resolveMissionTargetsDir(deps, flag) {
63
- return flag ?? deps.missionTargetsDir ?? DEFAULT_MISSION_TARGETS_DIR;
64
- }
65
- /** Same flag > deps > home-dir-default convention as `resolveJourneysDir`, for
66
- * the inbox store `jevitate mcp`'s inbox tools AND `jevitate ui` both read
67
- * from — the SAME resolved directory, so the two commands agree on where
68
- * approvals/handbacks/reviews live. */
69
- function resolveInboxDir(deps, flag) {
70
- return flag ?? deps.inboxDir ?? DEFAULT_INBOX_DIR;
71
- }
72
- /**
73
- * Assembles the injected `SourceApiDeps` for the distributed-sources commands
74
- * from `CliDeps.sources` (test-injected ports) or the real production
75
- * defaults: `~/.jevitate/sources` clones, `<cwd>/jevitate.lock`, and the
76
- * local, per-user `FsTrustStore`/`FsAckStore` under `~/.jevitate/trust`.
77
- * Trust/ack stores are LOCAL by design (§14.1) — never the team-shared lock.
78
- */
79
- function resolveSourceApiDeps(deps) {
80
- const s = deps.sources ?? {};
81
- return {
82
- sourcesDir: s.sourcesDir ?? resolveDataDir(["sources"]),
83
- lockPath: s.lockPath ?? DEFAULT_LOCK_PATH(),
84
- trust: new FsTrustStore(s.trustDir ?? resolveDataDir(["trust"])),
85
- ack: new FsAckStore(s.ackDir ?? resolveDataDir(["trust", "acks"])),
86
- git: s.git,
87
- now: s.now,
88
- };
89
- }
90
- /** The identity recorded in a `TrustRecord`/`TouAck` — an injected value, else
91
- * the OS username, else `"local"`. Never a secret/credential. */
92
- function resolveApprovedBy(deps) {
93
- if (deps.sources?.approvedBy)
94
- return deps.sources.approvedBy;
95
- try {
96
- return userInfo().username || "local";
97
- }
98
- catch {
99
- return "local";
100
- }
101
- }
102
- /**
103
- * Builds a fresh, real Playwright-backed `Actor` (its own temp profile dir +
104
- * browser context, per `journey-api.ts`'s `runJourneyProgrammatically`
105
- * pattern) and returns it alongside a `close()` to tear the session down.
106
- * `@jevitate/regression`'s `makeActor: () => Promise<Actor>` contract calls
107
- * this once per reproduce/minimize attempt — a Playwright session cannot be
108
- * reused after a run — so callers must close each one it hands back.
109
- */
110
- async function makeRealBrowserActor(site) {
111
- const profileDir = await mkdtemp(join(tmpdir(), "jevitate-regression-"));
112
- const port = new PlaywrightBrowserPort();
113
- const session = await port.open({ profileDir, headless: true, allowedOrigins: [site], baseUrl: site });
114
- const actor = CastActor.named("regression-capture").whoCan(new BrowseTheWeb(session, [site]));
115
- return {
116
- actor,
117
- close: async () => {
118
- await session.close();
119
- await rm(profileDir, { recursive: true, force: true });
120
- },
121
- };
122
- }
123
- /**
124
- * Parses repeated `--param key=value` flags into a `Record<string,string>`.
125
- * `previous` starts as the option's default (`{}`) and this is called once
126
- * per occurrence, commander's standard "collect" pattern.
127
- */
128
- function collectParam(value, previous) {
129
- const idx = value.indexOf("=");
130
- const key = idx === -1 ? value : value.slice(0, idx);
131
- const val = idx === -1 ? "" : value.slice(idx + 1);
132
- return { ...previous, [key]: val };
133
- }
134
- function makeClock() {
135
- return {
136
- nowIso: () => new Date().toISOString(),
137
- monotonicMs: () => Date.now(),
138
- };
139
- }
140
- async function withSitePolicyRepository(dbPath, fn) {
141
- await mkdir(dirname(dbPath), { recursive: true });
142
- const db = openDatabase(dbPath);
143
- try {
144
- await migrateToLatest(db);
145
- const repository = new SqliteSitePolicyRepository(db, makeClock());
146
- return await fn(repository);
147
- }
148
- finally {
149
- await db.destroy();
150
- }
151
- }
152
- function isPlannedStep(value) {
153
- if (typeof value !== "object" || value === null)
154
- return false;
155
- const record = value;
156
- if (typeof record.label !== "string")
157
- return false;
158
- switch (record.kind) {
159
- case "type":
160
- return typeof record.text === "string";
161
- case "click":
162
- case "navigate":
163
- return true;
164
- case "read":
165
- return typeof record.chars === "number";
166
- default:
167
- return false;
168
- }
169
- }
170
- function parsePlannedScript(raw) {
171
- const parsed = JSON.parse(raw);
172
- if (!Array.isArray(parsed) || !parsed.every(isPlannedStep)) {
173
- throw new Error("script must be a JSON array of planned steps ({kind, label, ...})");
174
- }
175
- return parsed;
176
- }
2
+ import { registerDoctorCommand } from "./resource-preflight.js";
3
+ import { fail } from "./envelope.js";
4
+ import { registerLedgerCommands } from "./ledger-cli.js";
5
+ import { registerAiCommands } from "./ai-cli.js";
6
+ import { registerCheckCommand } from "./check-cli.js";
7
+ import { registerReportCommands } from "./report-cli.js";
8
+ import { registerInvariantsCommands } from "./invariants-validate.js";
9
+ import { currentEngineInfo } from "./engine.js";
10
+ import { EXIT_CODES } from "./exit-codes.js";
11
+ import { commandPath, trackActionCommand } from "./cli-refusal.js";
12
+ import { resolveDbPath, resolveJourneysDir, resolveMissionTargetsDir, withBrowserLaunchFlags, browserLaunchFromFlags, buildExploreGateways, } from "./cli-shared.js";
13
+ import { registerInitCommands } from "./init-cli.js";
14
+ import { registerProfileCommands } from "./profile-cli.js";
15
+ import { registerSiteCommands } from "./site-cli.js";
16
+ import { registerRecordingCommands } from "./recording-cli.js";
17
+ import { registerJourneyCommands } from "./journey-cli.js";
18
+ import { registerSourceCommands } from "./source-cli.js";
19
+ import { registerLoadCommands } from "./load-cli.js";
20
+ import { registerExploreCommands } from "./explore-cli.js";
21
+ import { registerCampaignCommands } from "./campaign-cli.js";
22
+ import { registerVerifyFixCommands } from "./verify-fix-cli.js";
23
+ import { registerAuthorJourneyCommands } from "./author-journey-cli.js";
24
+ import { registerDemoCommands } from "./demo-aspect-cli.js";
25
+ import { registerRecordCommands } from "./record-cli.js";
26
+ import { registerRegressionCommands } from "./regression-cli.js";
27
+ import { registerMissionCommands } from "./mission-cli.js";
28
+ import { registerServeCommands } from "./serve-cli.js";
29
+ import { registerUxCommands } from "./ux-cli.js";
30
+ import { registerLogsCommands } from "./logs-cli.js";
31
+ import { registerInboxCommands } from "./inbox-cli.js";
32
+ export { fakeDoneJudge } from "./cli-shared.js";
177
33
  /**
178
- * Writes a JSON envelope using the program's CURRENTLY-CONFIGURED output
179
- * writer (read at call time via `configureOutput()`), so tests that call
180
- * `program.configureOutput({ writeOut })` after `buildProgram()` still see
181
- * output routed to their writer. Also sets `process.exitCode` (0 for `ok`,
182
- * 1 for `fail`) instead of hard-exiting, so `exitOverride()` in tests works.
34
+ * `--version`'s display string: the published semver alone once that alone identifies the
35
+ * build (a real npm install), plus commit/builtAt whenever the build could determine them (a
36
+ * dev checkout / `npm link`ed working tree) — issue #83, "`jevitate --version` stays `0.1.0`
37
+ * across 3 rebuilds in one hour ... nothing says which commit produced a result." `"unknown"`
38
+ * (never shown here at all — omitted instead) rather than a fabricated commit/time.
183
39
  */
184
- function emitJson(program, envelope) {
185
- const writeOut = program.configureOutput().writeOut;
186
- writeOut?.(`${JSON.stringify(envelope)}\n`);
187
- process.exitCode = envelope.ok ? 0 : 1;
40
+ function versionString() {
41
+ const engine = currentEngineInfo();
42
+ if (engine.commit === "unknown" && engine.builtAt === "unknown")
43
+ return engine.version;
44
+ return `${engine.version} (commit ${engine.commit}, built ${engine.builtAt})`;
188
45
  }
189
46
  export function buildProgram(deps) {
190
47
  const program = new Command();
191
- program.name("jevitate").description("Local browser automation platform").version(readCliVersion());
192
- program
193
- .command("init")
194
- .option("--json", "emit a JSON envelope")
195
- .option("--skip-keys", "skip credential collection")
196
- .option("--skip-skills", "skip skill installation")
197
- .option("--skip-mcp", "skip registering the jevitate MCP server in detected harnesses")
198
- .option("--targets <ids>", "comma-separated runtime ids to force-install to, overriding detection")
199
- .option("--force", "overwrite a user-modified installed skill file/block or MCP config entry")
200
- .option("--dry-run", "report planned skill-install/mcp-register actions without writing")
201
- .action(async function () {
202
- const { json, skipKeys, skipSkills, skipMcp, targets, force, dryRun } = this.opts();
203
- try {
204
- const data = { initialized: true };
205
- if (!skipKeys) {
206
- // SECURITY: reuses the existing, already-guardrailed credential
207
- // collection. The report holds only key NAMES (required/collected),
208
- // never a value — nothing here reads, echoes, logs, or returns a key.
209
- const store = envCredentialStore(deps.ai?.env ?? process.env, deps.ai?.localConfig ?? {});
210
- const io = deps.ai?.secureIO ?? realSecureIO();
211
- data.keys = await collectAllMissingKeys(store, io);
212
- }
213
- // Explicit `--targets` overrides detection entirely (the user takes
214
- // full control); otherwise `detectRuntimes` decides, always including
215
- // the always-on generic fallback. Shared by the skill install and the
216
- // MCP registration so a single selection drives both.
217
- const runtimes = targets
218
- ? targets.split(",").map((t) => t.trim()).filter((t) => t.length > 0)
219
- : detectRuntimes(deps.init?.detection);
220
- if (!skipSkills) {
221
- const paths = resolveInstallTargetPaths(deps.init?.detection);
222
- const statePath = deps.init?.statePath ?? resolveDataDir(["skills-install-state.json"]);
223
- const skills = loadManifest();
224
- data.skills = await installSkills(runtimes, skills, paths, statePath, { force, dryRun });
225
- }
226
- if (!skipMcp) {
227
- // Register the `jevitate mcp` server for each detected/selected
228
- // harness, with the SAME never-clobber safety as skills: a user's
229
- // conflicting or unparseable config is never overwritten without
230
- // --force; each declined target reports a printable instruction
231
- // instead (honest, never corrupts a config). `generic` has no MCP
232
- // convention and is skipped inside `registerMcp`.
233
- const mcpPaths = resolveMcpTargetPaths(deps.init?.detection);
234
- data.mcp = await registerMcp(runtimes, mcpPaths, { force, dryRun });
235
- }
236
- const envelope = ok(data);
237
- if (json) {
238
- emitJson(program, envelope);
239
- }
240
- else {
241
- const out = program.configureOutput().writeOut;
242
- out?.("jevitate initialized\n");
243
- if (data.keys)
244
- out?.(`keys: ${JSON.stringify(data.keys)}\n`);
245
- if (data.skills)
246
- out?.(`skills: ${data.skills.length} target/skill pairs processed\n`);
247
- if (data.mcp)
248
- out?.(`mcp: ${data.mcp.length} harness config(s) processed\n`);
249
- process.exitCode = 0;
250
- }
251
- }
252
- catch (err) {
253
- emitJson(program, fail("E_INIT", String(err instanceof Error ? err.message : err)));
254
- }
255
- });
256
- const profile = program.command("profile");
257
- profile
258
- .command("create <name>")
259
- .option("--json", "emit a JSON envelope")
260
- .action(async function (name) {
261
- const { json } = this.opts();
262
- try {
263
- const status = await deps.profiles.create(name);
264
- const envelope = ok(status);
265
- if (json) {
266
- emitJson(program, envelope);
267
- }
268
- else {
269
- program.configureOutput().writeOut?.(`profile '${status.name}' created at ${status.dir}\n`);
270
- process.exitCode = 0;
271
- }
272
- }
273
- catch (err) {
274
- emitJson(program, fail("E_PROFILE_CREATE", String(err)));
275
- }
276
- });
277
- profile
278
- .command("status <name>")
279
- .option("--json", "emit a JSON envelope")
280
- .action(async function (name) {
281
- const { json } = this.opts();
282
- try {
283
- const status = await deps.profiles.status(name);
284
- const envelope = ok(status);
285
- if (json) {
286
- emitJson(program, envelope);
287
- }
288
- else {
289
- program.configureOutput().writeOut?.(`profile '${status.name}': ${status.exists ? "exists" : "missing"} (${status.dir})\n`);
290
- process.exitCode = 0;
291
- }
292
- }
293
- catch (err) {
294
- emitJson(program, fail("E_PROFILE_STATUS", String(err)));
295
- }
296
- });
297
- const site = program.command("site");
298
- const sitePolicy = site.command("policy");
299
- sitePolicy
300
- .command("get <site>")
301
- .option("--account <account>", "account id", "primary")
302
- .option("--db <path>", "sqlite db path")
303
- .option("--json", "emit a JSON envelope")
304
- .action(async function (siteId) {
305
- const { account, db, json } = this.opts();
306
- try {
307
- const dbPath = resolveDbPath(deps, db);
308
- const policy = await withSitePolicyRepository(dbPath, (repository) => repository.get(siteId, account));
309
- const envelope = ok(policy);
310
- if (json) {
311
- emitJson(program, envelope);
312
- }
313
- else {
314
- if (policy) {
315
- program.configureOutput().writeOut?.(`policy for '${siteId}' (version ${policy.version}): ${JSON.stringify(policy)}\n`);
316
- }
317
- else {
318
- program.configureOutput().writeOut?.(`no policy configured for '${siteId}' (account '${account}')\n`);
319
- }
320
- process.exitCode = 0;
321
- }
322
- }
323
- catch (err) {
324
- emitJson(program, fail("E_SITE_POLICY_GET", String(err)));
325
- }
326
- });
327
- sitePolicy
328
- .command("set <site>")
329
- .requiredOption("--file <path>", "path to a policy JSON file")
330
- .option("--account <account>", "account id", "primary")
331
- .option("--db <path>", "sqlite db path")
332
- .option("--json", "emit a JSON envelope")
333
- .action(async function (siteId) {
334
- const { account, db, file, json } = this.opts();
335
- let policy;
336
- try {
337
- const raw = await readFile(file, "utf8");
338
- policy = SitePolicySchema.parse(JSON.parse(raw));
339
- }
340
- catch (err) {
341
- emitJson(program, fail("E_INVALID_POLICY", String(err)));
342
- return;
343
- }
344
- try {
345
- const dbPath = resolveDbPath(deps, db);
346
- await withSitePolicyRepository(dbPath, (repository) => repository.set(siteId, account, policy));
347
- const envelope = ok({ site: siteId, account, version: policy.version });
348
- if (json) {
349
- emitJson(program, envelope);
350
- }
351
- else {
352
- program.configureOutput().writeOut?.(`policy for '${siteId}' (account '${account}') set to version ${policy.version}\n`);
353
- process.exitCode = 0;
354
- }
355
- }
356
- catch (err) {
357
- emitJson(program, fail("E_SITE_POLICY_SET", String(err)));
358
- }
359
- });
360
- site
361
- .command("simulate <site>")
362
- .requiredOption("--script <path>", "path to a planned-step script JSON file")
363
- .option("--seed <n>", "deterministic RNG seed", "0")
364
- .option("--account <account>", "account id", "primary")
365
- .option("--db <path>", "sqlite db path")
366
- .option("--json", "emit a JSON envelope")
367
- .action(async function (siteId) {
368
- const { account, db, script, seed, json } = this.opts();
369
- let plannedScript;
370
- try {
371
- const raw = await readFile(script, "utf8");
372
- plannedScript = parsePlannedScript(raw);
373
- }
374
- catch (err) {
375
- emitJson(program, fail("E_INVALID_SCRIPT", String(err)));
376
- return;
377
- }
378
- const seedNum = Number(seed);
379
- if (!Number.isFinite(seedNum)) {
380
- emitJson(program, fail("E_INVALID_SEED", `--seed must be a finite number, got ${JSON.stringify(seed)}`));
381
- return;
382
- }
383
- try {
384
- const dbPath = resolveDbPath(deps, db);
385
- const policy = await withSitePolicyRepository(dbPath, (repository) => repository.get(siteId, account));
386
- const interaction = policy?.interaction ?? {};
387
- const profile = simulateTiming(interaction, seedNum, plannedScript);
388
- const envelope = ok(profile);
389
- if (json) {
390
- emitJson(program, envelope);
391
- }
392
- else {
393
- const out = program.configureOutput().writeOut;
394
- for (const step of profile.steps) {
395
- out?.(`${step.kind} '${step.label}': ${step.delayMs}ms\n`);
396
- }
397
- out?.(`totalMs: ${profile.totalMs}\n`);
398
- process.exitCode = 0;
399
- }
400
- }
401
- catch (err) {
402
- emitJson(program, fail("E_SITE_SIMULATE", String(err)));
403
- }
404
- });
405
- const recording = program.command("recording");
406
- recording
407
- .command("promote <file>")
408
- .requiredOption("--page <n>", "page index")
409
- .requiredOption("--step <n>", "step index within the page")
410
- .requiredOption("--var <name>", "variable name to bind")
411
- .action(async function (file) {
412
- const { page, step, var: varName } = this.opts();
413
- try {
414
- const raw = await readFile(file, "utf8");
415
- const rec = RecordingSchema.parse(JSON.parse(raw));
416
- const result = promoteToVariable(rec, { page: Number(page), step: Number(step) }, varName);
417
- program.configureOutput().writeOut?.(`${JSON.stringify(result, null, 2)}\n`);
418
- process.exitCode = 0;
419
- }
420
- catch (err) {
421
- emitJson(program, fail("E_INVALID_RECORDING", String(err)));
422
- }
423
- });
424
- recording
425
- .command("diff <takeA> <takeB> [more...]")
426
- .option("--json", "emit a JSON envelope")
427
- .action(async function (takeA, takeB, more) {
428
- const { json } = this.opts();
429
- try {
430
- const files = [takeA, takeB, ...more];
431
- const takes = await Promise.all(files.map(async (f) => {
432
- const raw = await readFile(f, "utf8");
433
- const parsed = AuthoringTakeSchema.parse(JSON.parse(raw));
434
- return { recording: parsed.recording, values: new Map(Object.entries(parsed.values)) };
435
- }));
436
- const diffResult = diffTakes(takes);
437
- const envelope = ok(diffResult);
438
- if (json) {
439
- emitJson(program, envelope);
440
- }
441
- else {
442
- const out = program.configureOutput().writeOut;
443
- diffResult.columns.forEach((col, i) => {
444
- const type = col.inferredType ? `, type=${col.inferredType}` : "";
445
- out?.(`column ${i}: ${col.kind} (confidence ${col.confidence.toFixed(2)}${type}) values=${JSON.stringify(col.values)}\n`);
446
- });
447
- process.exitCode = 0;
448
- }
449
- }
450
- catch (err) {
451
- emitJson(program, fail("E_INVALID_TAKE", String(err)));
452
- }
453
- });
454
- recording
455
- .command("fit <file>")
456
- .option("--json", "emit a JSON envelope")
457
- .action(async function (file) {
458
- const { json } = this.opts();
459
- try {
460
- const raw = await readFile(file, "utf8");
461
- const rec = RecordingSchema.parse(JSON.parse(raw));
462
- const interaction = fitInteractionPolicy(rec);
463
- const policy = { version: "1.0.0", interaction };
464
- if (json) {
465
- emitJson(program, ok(policy));
466
- }
467
- else {
468
- program.configureOutput().writeOut?.(`${JSON.stringify(policy, null, 2)}\n`);
469
- process.exitCode = 0;
470
- }
471
- }
472
- catch (err) {
473
- emitJson(program, fail("E_INVALID_RECORDING", String(err)));
474
- }
475
- });
476
- recording
477
- .command("postdoc <take> [more...]")
478
- .option("--decisions <file>", "path to a PostdocDecision[] JSON file (non-interactive mode)")
479
- .option("--out <file>", "write the resulting Recording to this file instead of stdout")
480
- .option("--json", "emit a JSON envelope")
481
- .action(async function (take, more) {
482
- const { decisions: decisionsFile, out, json } = this.opts();
483
- try {
484
- const files = [take, ...more];
485
- const takes = await Promise.all(files.map(async (f) => {
486
- const raw = await readFile(f, "utf8");
487
- const parsed = AuthoringTakeSchema.parse(JSON.parse(raw));
488
- return { recording: parsed.recording, values: new Map(Object.entries(parsed.values)) };
489
- }));
490
- const diff = diffTakes(takes);
491
- let decisions;
492
- if (decisionsFile !== undefined) {
493
- decisions = await loadDecisions(decisionsFile);
494
- }
495
- else {
496
- decisions = await promptForDecisions(takes[0]);
497
- }
498
- const result = applyPostdoc(takes[0], diff, decisions);
499
- if (out !== undefined) {
500
- await writeFile(out, `${JSON.stringify(result, null, 2)}\n`, "utf8");
501
- }
502
- if (json) {
503
- emitJson(program, ok(result));
504
- }
505
- else if (out === undefined) {
506
- program.configureOutput().writeOut?.(`${JSON.stringify(result, null, 2)}\n`);
507
- process.exitCode = 0;
508
- }
509
- else {
510
- process.exitCode = 0;
511
- }
512
- }
513
- catch (err) {
514
- if (err instanceof DecisionsParseError) {
515
- emitJson(program, fail("E_INVALID_DECISIONS", String(err.cause)));
516
- }
517
- else {
518
- emitJson(program, fail("E_INVALID_TAKE", String(err)));
519
- }
520
- }
521
- });
522
- const journey = program.command("journey");
523
- /**
524
- * `journey list` = ALL journeys' metadata via the store directly
525
- * (promoted AND unpromoted) — a local/dev-facing listing of everything on
526
- * disk. `journey find` (below) = promoted-only, via `JourneyRegistry.find`
527
- * — the same promoted-only projection external callers (e.g. the
528
- * mcp-facade) see. Keeping these distinct means `list` is useful for
529
- * authoring/debugging while `find` genuinely reflects what's discoverable.
530
- */
531
- journey
532
- .command("list")
533
- .option("--dir <path>", "journeys directory (default: ~/.jevitate/journeys)")
534
- .option("--json", "emit a JSON envelope")
535
- .action(async function () {
536
- const { dir, json } = this.opts();
537
- try {
538
- const store = new FsJourneyStore(resolveJourneysDir(deps, dir));
539
- const metas = await store.list();
540
- const envelope = ok(metas);
541
- if (json) {
542
- emitJson(program, envelope);
543
- }
544
- else {
545
- const out = program.configureOutput().writeOut;
546
- for (const m of metas) {
547
- out?.(`${m.id}\t${m.name}${m.promoted ? "" : " (unpromoted)"}\n`);
548
- }
549
- process.exitCode = 0;
550
- }
551
- }
552
- catch (err) {
553
- emitJson(program, fail("E_JOURNEY_LIST", String(err)));
554
- }
555
- });
556
- // RULING 5: uses `JourneyRegistry.find` (from `@jevitate/journey`) directly —
557
- // NEVER `@jevitate/mcp-facade`'s `findCapabilities` — Slice 1 forbids the CLI
558
- // depending on `@jevitate/mcp-facade`. `JourneyRegistry.find` is already
559
- // promoted-only, so this is the same promoted-only view without the
560
- // forbidden dependency.
561
- journey
562
- .command("find <query>")
563
- .option("--dir <path>", "journeys directory (default: ~/.jevitate/journeys)")
564
- .option("--json", "emit a JSON envelope")
565
- .action(async function (query) {
566
- const { dir, json } = this.opts();
567
- try {
568
- const store = new FsJourneyStore(resolveJourneysDir(deps, dir));
569
- const registry = new JourneyRegistry(store);
570
- const metas = await registry.find(query);
571
- const capabilities = metas.map((m) => ({
572
- id: m.id,
573
- name: m.name,
574
- description: m.description,
575
- params: m.params,
576
- }));
577
- const envelope = ok(capabilities);
578
- if (json) {
579
- emitJson(program, envelope);
580
- }
581
- else {
582
- const out = program.configureOutput().writeOut;
583
- for (const c of capabilities) {
584
- out?.(`${c.id}\t${c.name}\tparams=[${c.params.join(", ")}]\n`);
585
- }
586
- process.exitCode = 0;
587
- }
588
- }
589
- catch (err) {
590
- emitJson(program, fail("E_JOURNEY_FIND", String(err)));
591
- }
592
- });
593
- journey
594
- .command("run <id>")
595
- .option("--dir <path>", "journeys directory (default: ~/.jevitate/journeys)")
596
- .option("--param <kv>", "param as key=value (repeatable)", collectParam, {})
597
- // Ticket #7 (additive): opt a run into scoped self-healing. Default
598
- // `fail-closed` preserves Slice 1 behavior exactly (no healer wired). A
599
- // write/irreversible step NEVER auto-heals in any mode (enforced by the
600
- // runtime's write floor). `hybrid`/`full` need an AI gateway, selected
601
- // with --real/--fake-ai (mirrors `explore`); requesting a heal mode
602
- // without one fails CLOSED, never a silent unhealed run.
603
- .option("--self-heal <mode>", "self-heal policy mode: fail-closed | hybrid | full", "fail-closed")
604
- .option("--real", "use live Jev + OpenRouter gateways for self-heal (requires keys)", false)
605
- .option("--fake-ai", "use deterministic fake gateways for self-heal (pipeline smoke only)", false)
606
- .option("--json", "emit a JSON envelope")
607
- .action(async function (id) {
608
- const { dir, param, selfHeal, real, fakeAi, json } = this.opts();
609
- if (selfHeal !== "fail-closed" && selfHeal !== "hybrid" && selfHeal !== "full") {
610
- emitJson(program, fail("E_SELF_HEAL_MODE", `--self-heal must be one of fail-closed | hybrid | full (got '${selfHeal}')`));
611
- return;
612
- }
613
- const selfHealMode = selfHeal;
614
- // When a heal mode is requested, build the SelfHealer HERE (this action
615
- // owns `deps` + the credential preflight); a missing/unselected gateway
616
- // fails CLOSED before any browser launch, rather than silently running
617
- // with no healer. fail-closed needs no gateway (identical to today).
618
- let selfHealer;
619
- let policy = safeRunPolicy();
620
- if (selfHealMode !== "fail-closed") {
621
- let judge;
622
- let gen;
623
- try {
624
- ({ judge, gen } = await buildExploreGateways(deps, { real: real ?? false, fakeAi: fakeAi ?? false }));
625
- }
626
- catch (err) {
627
- if (err instanceof MissingCredentialError || err instanceof GatewaySelectionError) {
628
- emitJson(program, fail("E_AI_SETUP_REQUIRED", err.message));
629
- }
630
- else {
631
- emitJson(program, fail("E_JOURNEY_RUN", String(err instanceof Error ? err.message : err)));
632
- }
633
- return;
634
- }
635
- selfHealer = makeExploreSelfHealer(judge, gen);
636
- policy = { ...policy, selfHeal: { mode: selfHealMode } };
637
- }
638
- try {
639
- // `runJourneyProgrammatically` validates params UP FRONT (before any
640
- // browser launch). The default policy stays `safeRunPolicy()`
641
- // (fail-closed secret mode) — only `selfHeal.mode` is threaded from
642
- // the flag; a `--secret-mode` override is a later slice's concern.
643
- const result = await runJourneyProgrammatically({
644
- dir: resolveJourneysDir(deps, dir),
645
- id,
646
- params: param,
647
- policy,
648
- selfHealer,
649
- });
650
- const envelope = ok(result);
651
- if (json) {
652
- emitJson(program, envelope);
653
- // "ok" and "healed" (a recovered run) are both successes; only
654
- // "quarantined" is a non-zero exit.
655
- if (result.outcome === "quarantined")
656
- process.exitCode = 1;
657
- }
658
- else {
659
- program.configureOutput().writeOut?.(`${JSON.stringify(result)}\n`);
660
- process.exitCode = result.outcome === "quarantined" ? 1 : 0;
661
- }
662
- }
663
- catch (err) {
664
- if (err instanceof UnknownJourneyError) {
665
- emitJson(program, fail("E_UNKNOWN_JOURNEY", String(err.message)));
666
- }
667
- else if (err instanceof ParamValidationError) {
668
- emitJson(program, fail("E_INVALID_PARAMS", String(err.message)));
669
- }
670
- else {
671
- emitJson(program, fail("E_JOURNEY_RUN", String(err)));
672
- }
673
- }
674
- });
675
- // #19 — publish a promoted local Journey to a registered distributed source.
676
- // Preserves every publish-side guard in `@jevitate/sources` (promoted-only,
677
- // secret-references-only, declared-origin coverage); writes onto a NEW
678
- // `publish/<id>` branch and degrades gracefully when `gh` is absent.
679
- journey
680
- .command("publish <id>")
681
- .requiredOption("--to <source>", "registered source name to publish into")
682
- .option("--dir <path>", "journeys directory (default: ~/.jevitate/journeys)")
683
- .option("--declare-origin <origin>", "origin this Journey is authorized for (repeatable; default: derived from navigate steps)", (v, prev) => [...prev, v], [])
684
- .option("--as <id>", "publish under a different id than the local one")
685
- .option("--json", "emit a JSON envelope")
686
- .action(async function (id) {
687
- const { to, dir, declareOrigin, as: asId, json } = this.opts();
688
- try {
689
- const apiDeps = resolveSourceApiDeps(deps);
690
- const gh = deps.sources?.gh ?? realGhPort;
691
- const result = await publishJourneyToSource({ ...apiDeps, gh }, {
692
- journeysDir: resolveJourneysDir(deps, dir),
693
- id,
694
- toSource: to,
695
- declareOrigins: declareOrigin,
696
- asId,
697
- });
698
- const envelope = ok(result);
699
- if (json) {
700
- emitJson(program, envelope);
701
- }
702
- else {
703
- const out = program.configureOutput().writeOut;
704
- out?.(`published '${id}' to '${to}' on branch ${result.branch}\n`);
705
- if (result.prUrl)
706
- out?.(`PR: ${result.prUrl}\n`);
707
- else if (result.instructions)
708
- out?.(`${result.instructions}\n`);
709
- process.exitCode = 0;
710
- }
711
- }
712
- catch (err) {
713
- if (err instanceof UnknownSourceError) {
714
- emitJson(program, fail("E_JOURNEY_PUBLISH_UNKNOWN_SOURCE", err.message));
715
- }
716
- else if (err instanceof UnknownJourneyError) {
717
- emitJson(program, fail("E_JOURNEY_PUBLISH_UNKNOWN_JOURNEY", err.message));
718
- }
719
- else if (err instanceof NotPromotedError) {
720
- emitJson(program, fail("E_JOURNEY_PUBLISH_NOT_PROMOTED", err.message));
721
- }
722
- else if (err instanceof NoDeclaredOriginsError) {
723
- emitJson(program, fail("E_JOURNEY_PUBLISH_NO_ORIGINS", err.message));
724
- }
725
- else if (err instanceof EmbeddedSecretError) {
726
- emitJson(program, fail("E_JOURNEY_PUBLISH_SECRET", err.message));
727
- }
728
- else if (err instanceof UndeclaredOriginError) {
729
- emitJson(program, fail("E_JOURNEY_PUBLISH_ORIGIN", err.message));
730
- }
731
- else {
732
- emitJson(program, fail("E_JOURNEY_PUBLISH", String(err instanceof Error ? err.message : err)));
733
- }
734
- }
735
- });
736
- // #18 — manage distributed Journey sources (add/list/pull/update/remove/
737
- // trust). Trust is an explicit user act, content-hash-bound; add/pull/update
738
- // never trust anything implicitly.
739
- const source = program.command("source");
740
- source
741
- .command("add <name> <gitUrl>")
742
- .option("--accept-tou", "acknowledge the source's declared Terms of Use (required before its Journeys can run)")
743
- .option("--json", "emit a JSON envelope")
744
- .action(async function (name, gitUrl) {
745
- const { acceptTou, json } = this.opts();
746
- try {
747
- const apiDeps = resolveSourceApiDeps(deps);
748
- const result = await addSource(apiDeps, {
749
- name,
750
- gitUrl,
751
- acceptTou: acceptTou ?? false,
752
- ackedBy: resolveApprovedBy(deps),
753
- });
754
- const envelope = ok(result);
755
- if (json) {
756
- emitJson(program, envelope);
757
- }
758
- else {
759
- const out = program.configureOutput().writeOut;
760
- out?.(`added '${name}' pinned at ${result.pinnedCommit}\n`);
761
- out?.(`Terms of Use for ${result.touSurface.gitUrl}:\n`);
762
- for (const site of result.touSurface.sites)
763
- out?.(` ${site.origin}\t${site.touBasis}\n`);
764
- out?.(result.touAccepted ? "ToU acknowledged.\n" : "ToU NOT acknowledged — re-run with --accept-tou before running this source's Journeys.\n");
765
- process.exitCode = 0;
766
- }
767
- }
768
- catch (err) {
769
- if (err instanceof SourceValidationError) {
770
- emitJson(program, fail("E_SOURCE_INVALID_MANIFEST", err.message));
771
- }
772
- else {
773
- emitJson(program, fail("E_SOURCE_ADD", String(err instanceof Error ? err.message : err)));
774
- }
775
- }
776
- });
777
- source
778
- .command("list")
779
- .option("--json", "emit a JSON envelope")
780
- .action(async function () {
781
- const { json } = this.opts();
782
- try {
783
- const listing = await listSources(resolveSourceApiDeps(deps));
784
- const envelope = ok(listing);
785
- if (json) {
786
- emitJson(program, envelope);
787
- }
788
- else {
789
- const out = program.configureOutput().writeOut;
790
- for (const s of listing) {
791
- out?.(`${s.name}\t${s.gitUrl}\t${s.pinnedCommit}\ttrusted=[${s.trustedJourneys.join(", ")}]\n`);
792
- }
793
- process.exitCode = 0;
794
- }
795
- }
796
- catch (err) {
797
- emitJson(program, fail("E_SOURCE_LIST", String(err instanceof Error ? err.message : err)));
798
- }
799
- });
800
- source
801
- .command("pull <name>")
802
- .option("--json", "emit a JSON envelope")
803
- .action(async function (name) {
804
- const { json } = this.opts();
805
- try {
806
- const result = await pullSource(resolveSourceApiDeps(deps), name);
807
- const envelope = ok(result);
808
- if (json) {
809
- emitJson(program, envelope);
810
- }
811
- else {
812
- program.configureOutput().writeOut?.(`pulled '${name}' (pin unchanged at ${result.pinnedCommit}; run 'source update' to advance)\n`);
813
- process.exitCode = 0;
814
- }
815
- }
816
- catch (err) {
817
- if (err instanceof UnknownSourceError) {
818
- emitJson(program, fail("E_SOURCE_UNKNOWN", err.message));
819
- }
820
- else {
821
- emitJson(program, fail("E_SOURCE_PULL", String(err instanceof Error ? err.message : err)));
822
- }
823
- }
824
- });
825
- source
826
- .command("update <name>")
827
- .option("--json", "emit a JSON envelope")
828
- .action(async function (name) {
829
- const { json } = this.opts();
830
- try {
831
- const result = await updateSource(resolveSourceApiDeps(deps), name);
832
- const envelope = ok(result);
833
- if (json) {
834
- emitJson(program, envelope);
835
- }
836
- else {
837
- program.configureOutput().writeOut?.(`updated '${name}' -> pinned at ${result.pinnedCommit}\n`);
838
- process.exitCode = 0;
839
- }
840
- }
841
- catch (err) {
842
- if (err instanceof UnknownSourceError) {
843
- emitJson(program, fail("E_SOURCE_UNKNOWN", err.message));
844
- }
845
- else {
846
- emitJson(program, fail("E_SOURCE_UPDATE", String(err instanceof Error ? err.message : err)));
847
- }
848
- }
849
- });
850
- source
851
- .command("remove <name>")
852
- .option("--json", "emit a JSON envelope")
853
- .action(async function (name) {
854
- const { json } = this.opts();
855
- try {
856
- const result = await removeSource(resolveSourceApiDeps(deps), name);
857
- const envelope = ok(result);
858
- if (json) {
859
- emitJson(program, envelope);
860
- }
861
- else {
862
- program.configureOutput().writeOut?.(`removed '${name}'\n`);
863
- process.exitCode = 0;
864
- }
865
- }
866
- catch (err) {
867
- if (err instanceof UnknownSourceError) {
868
- emitJson(program, fail("E_SOURCE_UNKNOWN", err.message));
869
- }
870
- else {
871
- emitJson(program, fail("E_SOURCE_REMOVE", String(err instanceof Error ? err.message : err)));
872
- }
873
- }
874
- });
875
- source
876
- .command("trust <name> <journeyId>")
877
- .description("explicitly trust one Journey in a source, bound to its current content hash")
878
- .option("--json", "emit a JSON envelope")
879
- .action(async function (name, journeyId) {
880
- const { json } = this.opts();
881
- try {
882
- const result = await trustJourney(resolveSourceApiDeps(deps), {
883
- sourceName: name,
884
- journeyId,
885
- approvedBy: resolveApprovedBy(deps),
886
- });
887
- // Never emit the Journey's content — only the address + bound hash.
888
- const view = { sourceId: result.sourceId, journeyId: result.journeyId, contentHash: result.contentHash, approvedBy: result.approvedBy, approvedAtIso: result.approvedAtIso };
889
- const envelope = ok(view);
890
- if (json) {
891
- emitJson(program, envelope);
892
- }
893
- else {
894
- program.configureOutput().writeOut?.(`trusted '${name}/${journeyId}' at ${result.contentHash}\n`);
895
- process.exitCode = 0;
896
- }
897
- }
898
- catch (err) {
899
- if (err instanceof UnknownSourceError) {
900
- emitJson(program, fail("E_SOURCE_UNKNOWN", err.message));
901
- }
902
- else if (err instanceof UnknownJourneyError) {
903
- emitJson(program, fail("E_SOURCE_UNKNOWN_JOURNEY", err.message));
904
- }
905
- else {
906
- emitJson(program, fail("E_SOURCE_TRUST", String(err instanceof Error ? err.message : err)));
907
- }
908
- }
909
- });
910
- // #26 — run a Journey that lives in a trusted remote source, THROUGH the
911
- // existing run-gate (`@jevitate/sources`' `resolveForRun`). RULING: this is a
912
- // `source run` subcommand (not `journey run --from-source`) because the whole
913
- // trust boundary is source-scoped — the `<source>/<id>` address, the
914
- // per-source manifest/ToU-ack/trust records all live under `source`. `journey
915
- // run` stays the LOCAL FsJourneyStore path; keeping remote runs here keeps the
916
- // two trust boundaries visibly separate. The run NEVER bypasses a gate: every
917
- // refusal below is a typed error thrown by `resolveForRun` BEFORE any browser.
918
- source
919
- .command("run <name> <journeyId>")
920
- .description("run a Journey from a trusted remote source through the run-gate")
921
- .option("--param <kv>", "param as key=value (repeatable)", collectParam, {})
922
- .option("--json", "emit a JSON envelope")
923
- .action(async function (name, journeyId) {
924
- const { param, json } = this.opts();
925
- try {
926
- const apiDeps = {
927
- ...resolveSourceApiDeps(deps),
928
- runJourney: deps.sources?.runJourney ?? realResolvedJourneyRunner,
929
- };
930
- const result = await runSourceJourney(apiDeps, { sourceName: name, journeyId, params: param });
931
- const envelope = ok(result);
932
- if (json) {
933
- emitJson(program, envelope);
934
- if (result.outcome === "quarantined")
935
- process.exitCode = 1;
936
- }
937
- else {
938
- program.configureOutput().writeOut?.(`${JSON.stringify(result)}\n`);
939
- process.exitCode = result.outcome === "quarantined" ? 1 : 0;
940
- }
941
- }
942
- catch (err) {
943
- // Each run-gate refusal maps to a distinct E_SOURCE_RUN* code so a
944
- // caller can tell WHY the run was refused without string-matching.
945
- if (err instanceof UnknownSourceError) {
946
- emitJson(program, fail("E_SOURCE_RUN_UNKNOWN", err.message));
947
- }
948
- else if (err instanceof HashMismatchError) {
949
- emitJson(program, fail("E_SOURCE_RUN_HASH_MISMATCH", err.message));
950
- }
951
- else if (err instanceof UntrustedRiskyJourneyError) {
952
- emitJson(program, fail("E_SOURCE_RUN_UNTRUSTED", err.message));
953
- }
954
- else if (err instanceof UndeclaredOriginError) {
955
- emitJson(program, fail("E_SOURCE_RUN_ORIGIN", err.message));
956
- }
957
- else if (err instanceof UndeclaredTouError) {
958
- emitJson(program, fail("E_SOURCE_RUN_TOU", err.message));
959
- }
960
- else if (err instanceof EmbeddedSecretError) {
961
- emitJson(program, fail("E_SOURCE_RUN_SECRET", err.message));
962
- }
963
- else if (err instanceof SourceValidationError) {
964
- emitJson(program, fail("E_SOURCE_RUN_INVALID_MANIFEST", err.message));
965
- }
966
- else if (err instanceof ParamValidationError) {
967
- emitJson(program, fail("E_INVALID_PARAMS", err.message));
968
- }
969
- else {
970
- emitJson(program, fail("E_SOURCE_RUN", String(err instanceof Error ? err.message : err)));
971
- }
972
- }
973
- });
974
- const load = program.command("load");
975
- load
976
- .command("run <journeyId>")
977
- .option("--dir <path>", "journeys directory (default: ~/.jevitate/journeys)")
978
- .option("--param <kv>", "param as key=value (repeatable)", collectParam, {})
979
- // `--authorized-origin` is mandatory, but enforced IN THE ACTION (below)
980
- // via a `fail` envelope rather than commander's `.requiredOption` — which
981
- // hard-exits via `process.exit`, inconsistent with this CLI's convention
982
- // of emitting a JSON envelope + setting `process.exitCode` (see emitJson).
983
- .option("--authorized-origin <origin>", "allowed load-test target origin (repeatable) — required, fails closed if omitted", (v, prev) => [...prev, v], [])
984
- .option("--concurrency <n>", "pool size", "1")
985
- .option("--iterations <n>", "iterations per actor", "1")
986
- .option("--seed <n>", "master RNG seed", "1")
987
- .option("--json", "emit a JSON envelope")
988
- .action(async function (journeyId) {
989
- const { dir, param, authorizedOrigin, concurrency, iterations, seed, json } = this.opts();
990
- if (authorizedOrigin.length === 0) {
991
- emitJson(program, fail("E_LOAD_RUN", "at least one --authorized-origin is required (refusing to load-test with an empty allowlist)"));
992
- return;
993
- }
994
- try {
995
- const report = await runJourneyLoadTest({
996
- dir: resolveJourneysDir(deps, dir),
997
- id: journeyId,
998
- params: param,
999
- concurrency: Number(concurrency),
1000
- iterationsPerActor: Number(iterations),
1001
- seed: Number(seed),
1002
- authorizedOrigins: authorizedOrigin,
1003
- });
1004
- const envelope = ok(report);
1005
- if (json) {
1006
- emitJson(program, envelope);
1007
- }
1008
- else {
1009
- program.configureOutput().writeOut?.(`${JSON.stringify(report, null, 2)}\n`);
1010
- process.exitCode = 0;
1011
- }
1012
- }
1013
- catch (err) {
1014
- if (err instanceof UnknownLoadJourneyError) {
1015
- emitJson(program, fail("E_UNKNOWN_JOURNEY", String(err.message)));
1016
- }
1017
- else {
1018
- emitJson(program, fail("E_LOAD_RUN", String(err instanceof Error ? err.message : err)));
1019
- }
1020
- }
1021
- });
1022
- program
1023
- .command("explore")
1024
- .description("goal-directed exploration -> a deterministic Recording (authoring/test plane)")
1025
- .option("--url <url>", "target URL (must be an authorized origin)")
1026
- .option("--strategy <name>", "exploration strategy: goal (default) | coverage | exploratory | adversarial | usability (UX review: ranked, cited findings)", "goal")
1027
- .option("--goal <text>", "natural-language goal / job (required for --strategy goal and usability)")
1028
- .option("--app-class <class>", "app class for UX calibration (required for --strategy usability), e.g. consumer|admin|internal")
1029
- .option("--success <spec>", "independent success assertion, e.g. urlIncludes:/inbox")
1030
- .option("--feature <name>", "run the capability-scoped feature-testing mission (instead of --goal/--success)")
1031
- .option("--route <glob>", "in-scope route glob for --feature (repeatable), e.g. /thread/**", (v, prev) => [...prev, v], [])
1032
- .option("--allow <origin>", "authorized origin (repeatable); defaults to the URL's own origin", (v, prev) => [...prev, v], [])
1033
- .option("--secret <value>", "a secret/PII value to keep out of every model call (repeatable)", (v, prev) => [...prev, v], [])
1034
- .option("--max-actions <n>", "hard cap on executed actions")
1035
- .option("--max-decisions <n>", "hard cap on model decisions")
1036
- .option("--real", "use live Jev + OpenRouter gateways (requires keys)", false)
1037
- .option("--fake-ai", "use deterministic fake gateways (pipeline smoke only)", false)
1038
- .option("--out <dir>", "directory to write the emitted Recording")
1039
- .option("--json", "emit a JSON envelope")
1040
- .action(async function () {
1041
- const o = this.opts();
1042
- const strategy = o.strategy ?? "goal";
1043
- // Additive coverage/exploratory strategy: proof-by-induction state coverage.
1044
- // It takes no goal/success (the frontier itself is the objective), so it is
1045
- // a distinct, goal-free path that leaves the goal strategy below unchanged.
1046
- if (strategy === "coverage" || strategy === "exploratory") {
1047
- if (!o.url) {
1048
- emitJson(program, fail("E_EXPLORE_ARGS", "--url is required"));
1049
- return;
1050
- }
1051
- const covAllowlist = resolveExploreAllowlist(o.url, o.allow);
1052
- const covBounds = {};
1053
- if (o.maxActions !== undefined)
1054
- covBounds.maxActions = Number(o.maxActions);
1055
- if (o.maxDecisions !== undefined)
1056
- covBounds.maxDecisions = Number(o.maxDecisions);
1057
- let covJudge;
1058
- let covGen;
1059
- try {
1060
- ({ judge: covJudge, gen: covGen } = await buildExploreGateways(deps, {
1061
- real: o.real ?? false,
1062
- fakeAi: o.fakeAi ?? false,
1063
- }));
1064
- }
1065
- catch (err) {
1066
- if (err instanceof MissingCredentialError || err instanceof GatewaySelectionError) {
1067
- emitJson(program, fail("E_AI_SETUP_REQUIRED", err.message));
1068
- }
1069
- else {
1070
- emitJson(program, fail("E_EXPLORE_SETUP", String(err instanceof Error ? err.message : err)));
1071
- }
1072
- return;
1073
- }
1074
- try {
1075
- const result = await runCoverageMission({
1076
- url: o.url,
1077
- allowlist: covAllowlist,
1078
- judge: covJudge,
1079
- gen: covGen,
1080
- bounds: Object.keys(covBounds).length > 0 ? covBounds : undefined,
1081
- outDir: o.out,
1082
- browserPortFactory: deps.explore?.browserPortFactory,
1083
- });
1084
- const envelope = ok(result);
1085
- if (o.json) {
1086
- emitJson(program, envelope);
1087
- }
1088
- else {
1089
- program.configureOutput().writeOut?.(`${JSON.stringify(result)}\n`);
1090
- }
1091
- if (result.coverage.defects.length > 0)
1092
- process.exitCode = 1;
1093
- }
1094
- catch (err) {
1095
- if (err instanceof UnauthorizedExploreTargetError) {
1096
- emitJson(program, fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
1097
- }
1098
- else {
1099
- emitJson(program, fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
1100
- }
1101
- }
1102
- return;
1103
- }
1104
- // Additive adversarial strategy: a bounded "try to break it" run whose
1105
- // stop decision comes from a trusted hard-signal oracle (never Jev's
1106
- // Noul). Requires only --url; --goal/--success are goal-strategy inputs.
1107
- if (strategy === "adversarial") {
1108
- if (!o.url) {
1109
- emitJson(program, fail("E_EXPLORE_ARGS", "--url is required for --strategy adversarial"));
1110
- return;
1111
- }
1112
- const advAllowlist = resolveExploreAllowlist(o.url, o.allow);
1113
- let advJudge;
1114
- let advGen;
1115
- try {
1116
- ({ judge: advJudge, gen: advGen } = await buildExploreGateways(deps, {
1117
- real: o.real ?? false,
1118
- fakeAi: o.fakeAi ?? false,
1119
- }));
1120
- }
1121
- catch (err) {
1122
- if (err instanceof MissingCredentialError || err instanceof GatewaySelectionError) {
1123
- emitJson(program, fail("E_AI_SETUP_REQUIRED", err.message));
1124
- }
1125
- else {
1126
- emitJson(program, fail("E_EXPLORE_SETUP", String(err instanceof Error ? err.message : err)));
1127
- }
1128
- return;
1129
- }
1130
- const profileDir = await mkdtemp(join(tmpdir(), "jevitate-adversarial-"));
1131
- try {
1132
- const result = await runAdversarialCliMission({
1133
- seedUrl: o.url,
1134
- allowlist: advAllowlist,
1135
- strategies: [
1136
- "ordering-violation",
1137
- "repeat-rapid",
1138
- "boundary-input",
1139
- "contradictory-actions",
1140
- "nav-during-pending",
1141
- ],
1142
- judgment: advJudge,
1143
- generation: advGen,
1144
- profileDir,
1145
- browserPortFactory: deps.explore?.browserPortFactory,
1146
- });
1147
- emitJson(program, ok(result));
1148
- // A discovered defect gates CI, mirroring how a failing test would.
1149
- if (result.outcome === "defect")
1150
- process.exitCode = 1;
1151
- }
1152
- catch (err) {
1153
- if (err instanceof UnauthorizedExploreTargetError) {
1154
- emitJson(program, fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
1155
- }
1156
- else {
1157
- emitJson(program, fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
1158
- }
1159
- }
1160
- finally {
1161
- await rm(profileDir, { recursive: true, force: true });
1162
- }
1163
- return;
1164
- }
1165
- // Additive: `--strategy usability` (issue #30) — a UX review. Reuses the
1166
- // explore loop (goal = the job) and analyzes each observed screen against
1167
- // the cited @jevitate/ux rubric. Findings are ADVISORY: a UX finding never
1168
- // gates the run (no non-zero exit).
1169
- if (strategy === "usability") {
1170
- if (!o.url || !o.goal) {
1171
- emitJson(program, fail("E_EXPLORE_ARGS", "--url and --goal (the job) are required for --strategy usability"));
1172
- return;
1173
- }
1174
- if (!o.appClass) {
1175
- emitJson(program, fail("E_UX_ARGS", "--app-class is required for --strategy usability"));
1176
- return;
1177
- }
1178
- const uxAllowlist = resolveExploreAllowlist(o.url, o.allow);
1179
- const uxBounds = {};
1180
- if (o.maxActions !== undefined)
1181
- uxBounds.maxActions = Number(o.maxActions);
1182
- if (o.maxDecisions !== undefined)
1183
- uxBounds.maxDecisions = Number(o.maxDecisions);
1184
- let uxJudge;
1185
- let uxGen;
1186
- try {
1187
- ({ judge: uxJudge, gen: uxGen } = await buildExploreGateways(deps, {
1188
- real: o.real ?? false,
1189
- fakeAi: o.fakeAi ?? false,
1190
- }));
1191
- }
1192
- catch (err) {
1193
- if (err instanceof MissingCredentialError || err instanceof GatewaySelectionError) {
1194
- emitJson(program, fail("E_AI_SETUP_REQUIRED", err.message));
1195
- }
1196
- else {
1197
- emitJson(program, fail("E_EXPLORE_SETUP", String(err instanceof Error ? err.message : err)));
1198
- }
1199
- return;
1200
- }
1201
- try {
1202
- const result = await runUsabilityMission({
1203
- url: o.url,
1204
- job: o.goal,
1205
- allowlist: uxAllowlist,
1206
- appContext: { appClass: o.appClass, job: o.goal },
1207
- judge: uxJudge,
1208
- gen: uxGen,
1209
- bounds: Object.keys(uxBounds).length > 0 ? uxBounds : undefined,
1210
- secrets: o.secret.length > 0 ? o.secret : undefined,
1211
- outDir: o.out,
1212
- browserPortFactory: deps.explore?.browserPortFactory,
1213
- });
1214
- emitJson(program, ok(result));
1215
- }
1216
- catch (err) {
1217
- if (err instanceof UnauthorizedExploreTargetError) {
1218
- emitJson(program, fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
1219
- }
1220
- else if (err instanceof UxAnalysisFailedError) {
1221
- emitJson(program, fail("E_UX_ANALYSIS", err.message));
1222
- }
1223
- else {
1224
- emitJson(program, fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
1225
- }
1226
- }
1227
- return;
1228
- }
1229
- // Additive: `--feature <name>` runs the capability-scoped feature-testing
1230
- // mission (ticket #2 / site #11). It is model-free, so it needs neither
1231
- // --goal/--success nor a gateway selection; the goal-based path below is
1232
- // untouched when --feature is absent.
1233
- if (o.feature) {
1234
- if (!o.url) {
1235
- emitJson(program, fail("E_EXPLORE_ARGS", "--url is required with --feature"));
1236
- return;
1237
- }
1238
- const featAllowlist = resolveExploreAllowlist(o.url, o.allow);
1239
- const profileDir = await mkdtemp(join(tmpdir(), "jevitate-feature-"));
1240
- try {
1241
- const result = await runFeatureCliMission({
1242
- seedUrl: o.url,
1243
- allowlist: featAllowlist,
1244
- capability: o.feature,
1245
- routeGlobs: o.route ?? [],
1246
- profileDir,
1247
- });
1248
- emitJson(program, ok(result));
1249
- }
1250
- catch (err) {
1251
- if (err instanceof UnauthorizedExploreTargetError) {
1252
- emitJson(program, fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
1253
- }
1254
- else {
1255
- emitJson(program, fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
1256
- }
1257
- }
1258
- finally {
1259
- await rm(profileDir, { recursive: true, force: true });
1260
- }
1261
- return;
1262
- }
1263
- if (!o.url || !o.goal || !o.success) {
1264
- emitJson(program, fail("E_EXPLORE_ARGS", "--url, --goal and --success are all required"));
1265
- return;
1266
- }
1267
- let successAssertion;
1268
- try {
1269
- successAssertion = parseAssertionSpec(o.success);
1270
- }
1271
- catch (err) {
1272
- emitJson(program, fail("E_EXPLORE_ASSERTION", String(err instanceof Error ? err.message : err)));
1273
- return;
1274
- }
1275
- const allowlist = resolveExploreAllowlist(o.url, o.allow);
1276
- const bounds = {};
1277
- if (o.maxActions !== undefined)
1278
- bounds.maxActions = Number(o.maxActions);
1279
- if (o.maxDecisions !== undefined)
1280
- bounds.maxDecisions = Number(o.maxDecisions);
1281
- let judge;
1282
- let gen;
1283
- try {
1284
- ({ judge, gen } = await buildExploreGateways(deps, { real: o.real ?? false, fakeAi: o.fakeAi ?? false }));
1285
- }
1286
- catch (err) {
1287
- if (err instanceof MissingCredentialError || err instanceof GatewaySelectionError) {
1288
- emitJson(program, fail("E_AI_SETUP_REQUIRED", err.message));
1289
- }
1290
- else {
1291
- emitJson(program, fail("E_EXPLORE_SETUP", String(err instanceof Error ? err.message : err)));
1292
- }
1293
- return;
1294
- }
1295
- try {
1296
- const result = await runExploration({
1297
- url: o.url,
1298
- goal: o.goal,
1299
- successAssertion,
1300
- allowlist,
1301
- judge,
1302
- gen,
1303
- bounds: Object.keys(bounds).length > 0 ? bounds : undefined,
1304
- secrets: o.secret.length > 0 ? o.secret : undefined,
1305
- outDir: o.out,
1306
- browserPortFactory: deps.explore?.browserPortFactory,
1307
- });
1308
- const envelope = ok(result);
1309
- if (o.json) {
1310
- emitJson(program, envelope);
1311
- if (result.outcome !== "succeeded")
1312
- process.exitCode = 1;
1313
- }
1314
- else {
1315
- program.configureOutput().writeOut?.(`${JSON.stringify(result)}\n`);
1316
- process.exitCode = result.outcome === "succeeded" ? 0 : 1;
1317
- }
1318
- }
1319
- catch (err) {
1320
- if (err instanceof UnauthorizedExploreTargetError) {
1321
- emitJson(program, fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
1322
- }
1323
- else {
1324
- emitJson(program, fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
1325
- }
1326
- }
1327
- });
1328
- // Additive: `explore author-journey` — Jev-driving authors a promotable
1329
- // Journey (Ticket #6). Drives the goal-based mission, feeds its take(s)
1330
- // through RxD's diff/postdoc pipeline, and writes an UNPROMOTED,
1331
- // parameterized Journey to the journeys store. The record-by-demonstration
1332
- // authoring path is untouched.
1333
- program
1334
- .command("explore-author-journey")
1335
- .description("Jev-driving authors a promotable Journey (authoring plane); never auto-promoted")
1336
- .option("--url <url>", "target URL (must be an authorized origin)")
1337
- .option("--goal <text>", "natural-language goal")
1338
- .option("--success <spec>", "independent success assertion, e.g. urlIncludes:/confirmed")
1339
- .option("--id <id>", "journey id (used for the <id>.json filename in the store)")
1340
- .option("--name <name>", "human-readable journey name")
1341
- .option("--takes <n>", "corroborating takes incl. discovery (default 1)", "1")
1342
- .option("--journeys-dir <dir>", "journeys store directory (default: ~/.jevitate/journeys)")
1343
- .option("--allow <origin>", "authorized origin (repeatable); defaults to the URL's own origin", (v, prev) => [...prev, v], [])
1344
- .option("--max-actions <n>", "hard cap on executed actions")
1345
- .option("--max-decisions <n>", "hard cap on model decisions")
1346
- .option("--real", "use live Jev + OpenRouter gateways (requires keys)", false)
1347
- .option("--fake-ai", "use deterministic fake gateways (pipeline smoke only)", false)
1348
- .option("--json", "emit a JSON envelope")
1349
- .action(async function () {
1350
- const o = this.opts();
1351
- if (!o.url || !o.goal || !o.success || !o.id || !o.name) {
1352
- emitJson(program, fail("E_AUTHOR_ARGS", "--url, --goal, --success, --id and --name are all required"));
1353
- return;
1354
- }
1355
- let successAssertion;
1356
- try {
1357
- successAssertion = parseAssertionSpec(o.success);
1358
- }
1359
- catch (err) {
1360
- emitJson(program, fail("E_EXPLORE_ASSERTION", String(err instanceof Error ? err.message : err)));
1361
- return;
1362
- }
1363
- const allowlist = resolveExploreAllowlist(o.url, o.allow);
1364
- const bounds = {};
1365
- if (o.maxActions !== undefined)
1366
- bounds.maxActions = Number(o.maxActions);
1367
- if (o.maxDecisions !== undefined)
1368
- bounds.maxDecisions = Number(o.maxDecisions);
1369
- let judge;
1370
- let gen;
1371
- try {
1372
- ({ judge, gen } = await buildExploreGateways(deps, { real: o.real ?? false, fakeAi: o.fakeAi ?? false }));
1373
- }
1374
- catch (err) {
1375
- if (err instanceof MissingCredentialError || err instanceof GatewaySelectionError) {
1376
- emitJson(program, fail("E_AI_SETUP_REQUIRED", err.message));
1377
- }
1378
- else {
1379
- emitJson(program, fail("E_EXPLORE_SETUP", String(err instanceof Error ? err.message : err)));
1380
- }
1381
- return;
1382
- }
1383
- try {
1384
- const result = await runAuthorJourney({
1385
- url: o.url,
1386
- goal: o.goal,
1387
- successAssertion,
1388
- allowlist,
1389
- journeysDir: resolveJourneysDir(deps, o.journeysDir),
1390
- journeyId: o.id,
1391
- journeyName: o.name,
1392
- takes: Number(o.takes),
1393
- judge,
1394
- gen,
1395
- bounds: Object.keys(bounds).length > 0 ? bounds : undefined,
1396
- browserPortFactory: deps.explore?.browserPortFactory,
1397
- });
1398
- const envelope = ok(result);
1399
- if (o.json) {
1400
- emitJson(program, envelope);
1401
- if (result.outcome !== "authored")
1402
- process.exitCode = 1;
1403
- }
1404
- else {
1405
- program.configureOutput().writeOut?.(`${JSON.stringify(result)}\n`);
1406
- process.exitCode = result.outcome === "authored" ? 0 : 1;
1407
- }
1408
- }
1409
- catch (err) {
1410
- if (err instanceof UnauthorizedExploreTargetError) {
1411
- emitJson(program, fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
1412
- }
1413
- else {
1414
- emitJson(program, fail("E_AUTHOR_JOURNEY", String(err instanceof Error ? err.message : err)));
1415
- }
1416
- }
1417
- });
1418
- // Additive: `jevitate record` — record-by-demonstration (Ticket #22). Opens a
1419
- // real browser on an authorized origin, lets the user demonstrate a flow, and
1420
- // captures it into a schema-valid, replayable Recording written to disk. The
1421
- // authorized-origin guard is enforced FIRST (fail-closed) inside runRecording,
1422
- // before any browser is opened; the temp profile dir is always cleaned up.
1423
- program
1424
- .command("record")
1425
- .description("record a demonstrated flow into a Recording (authoring plane)")
1426
- .option("--url <url>", "start URL to demonstrate from (must be an authorized origin)")
1427
- .option("--intent <text>", "your framing of the journey (carried to Recording.intent)")
1428
- .option("--retro <text>", "optional retrospective note (carried to Recording.retro)")
1429
- .option("--allow <origin>", "authorized origin (repeatable); defaults to the URL's own origin", (v, prev) => [...prev, v], [])
1430
- .option("--headless", "run headless (default: headed — a record session is a live demonstration)", false)
1431
- .option("--out <dir>", "directory to write the emitted Recording (default: ~/.jevitate/recordings)")
1432
- .option("--json", "emit a JSON envelope")
1433
- .action(async function () {
1434
- const o = this.opts();
1435
- if (!o.url) {
1436
- emitJson(program, fail("E_RECORD_ARGS", "--url is required"));
1437
- return;
1438
- }
1439
- const allowlist = resolveRecordAllowlist(o.url, o.allow);
1440
- try {
1441
- const result = await runRecording({
1442
- url: o.url,
1443
- allowlist,
1444
- intent: o.intent,
1445
- retro: o.retro,
1446
- outDir: o.out,
1447
- headless: o.headless ?? false,
1448
- browserPortFactory: deps.record?.browserPortFactory,
1449
- recorderFactory: deps.record?.recorderFactory,
1450
- waitForStop: deps.record?.waitForStop,
1451
- });
1452
- const summary = {
1453
- recordingPath: result.recordingPath,
1454
- steps: result.steps,
1455
- pages: result.pages,
1456
- finalUrl: result.finalUrl,
1457
- };
1458
- if (o.json) {
1459
- emitJson(program, ok(summary));
1460
- }
1461
- else {
1462
- program.configureOutput().writeOut?.(`${JSON.stringify(summary)}\n`);
1463
- }
1464
- }
1465
- catch (err) {
1466
- if (err instanceof UnauthorizedExploreTargetError) {
1467
- emitJson(program, fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
1468
- }
1469
- else {
1470
- emitJson(program, fail("E_RECORD_RUN", String(err instanceof Error ? err.message : err)));
1471
- }
1472
- }
1473
- });
1474
- // Additive: `@jevitate/regression` — reproduce -> minimize -> commit a
1475
- // failing Recording into a committed regression artifact (Ticket #5).
1476
- // Independent of the `journey`/`load` commands above; wires a real
1477
- // Playwright-backed `makeActor` (one fresh browser session per
1478
- // reproduce/minimize attempt, closed after each use) into
1479
- // `runRegressionCapture`.
1480
- const regression = program.command("regression");
1481
- regression
1482
- .command("capture")
1483
- .requiredOption("--from <file>", "path to the schema-valid failing Recording JSON to capture")
1484
- .requiredOption("--id <id>", "regression id (used for the committed <id>.recording.json/<id>.meta.json filenames)")
1485
- .option("--dir <path>", "regressions directory (default: ~/.jevitate/regressions)")
1486
- .option("--attempts <n>", "reproduction attempts before labeling flaky", "3")
1487
- .option("--summary <text>", "optional human-readable bug summary recorded in the meta sidecar")
1488
- .option("--json", "emit a JSON envelope")
1489
- .action(async function () {
1490
- const { from, id, dir, attempts, summary, json } = this.opts();
1491
- const opened = [];
1492
- try {
1493
- const raw = JSON.parse(await readFile(from, "utf8"));
1494
- const recording = RecordingSchema.parse(raw);
1495
- const result = await runRegressionCapture({
1496
- failingRecordingPath: from,
1497
- id,
1498
- regressionsDir: resolveRegressionsDir(dir),
1499
- attempts: Number(attempts),
1500
- bugSummary: summary,
1501
- makeActor: async () => {
1502
- const { actor, close } = await makeRealBrowserActor(recording.site);
1503
- opened.push(close);
1504
- return actor;
1505
- },
1506
- });
1507
- const envelope = ok(result);
1508
- if (json) {
1509
- emitJson(program, envelope);
1510
- }
1511
- else {
1512
- program.configureOutput().writeOut?.(`${JSON.stringify(result)}\n`);
1513
- process.exitCode = 0;
1514
- }
1515
- }
1516
- catch (err) {
1517
- emitJson(program, fail("E_REGRESSION_CAPTURE", String(err instanceof Error ? err.message : err)));
1518
- }
1519
- finally {
1520
- for (const close of opened)
1521
- await close();
1522
- }
1523
- });
1524
- // Additive: `mission target` — register/list/promote exploration mission
1525
- // targets (Ticket #21). Wires the real fs-backed `@jevitate/missions`
1526
- // store/registry (the SAME store `queue_exploration` resolves promoted
1527
- // targets from). SECURITY: `add` registers UNPROMOTED — the promoted-only
1528
- // gate stays intact, so a registered target is not resolvable by
1529
- // `queue_exploration` until a separate `promote` flips it.
1530
- const mission = program.command("mission");
1531
- const missionTarget = mission.command("target");
1532
- missionTarget
1533
- .command("add <id>")
1534
- .description("register an exploration mission target (UNPROMOTED — not usable by queue_exploration until promoted)")
1535
- .option("--name <name>", "human-readable target name")
1536
- .option("--authorized-origin <origin>", "the single authorized exploration origin for this target")
1537
- .option("--base-url <url>", "the base URL a mission starts navigation from")
1538
- .option("--description <text>", "optional human-readable description")
1539
- .option("--dir <path>", "mission targets directory (default: ~/.jevitate/missions/targets)")
1540
- .option("--json", "emit a JSON envelope")
1541
- .action(async function (id) {
1542
- const { name, authorizedOrigin, baseUrl, description, dir, json } = this.opts();
1543
- // Validate in-action + fail envelope (not commander's hard-exiting
1544
- // `.requiredOption`), matching this CLI's convention.
1545
- if (!name || !authorizedOrigin || !baseUrl) {
1546
- emitJson(program, fail("E_MISSION_TARGET_ARGS", "--name, --authorized-origin and --base-url are all required"));
1547
- return;
1548
- }
1549
- try {
1550
- const ctx = missionTargetContext(resolveMissionTargetsDir(deps, dir));
1551
- const target = await addMissionTarget(ctx, { id, name, authorizedOrigin, baseUrl, description });
1552
- const envelope = ok(target);
1553
- if (json) {
1554
- emitJson(program, envelope);
1555
- }
1556
- else {
1557
- program.configureOutput().writeOut?.(`registered mission target '${target.id}' (unpromoted — run 'jevitate mission target promote ${target.id}' to make it resolvable)\n`);
1558
- process.exitCode = 0;
1559
- }
1560
- }
1561
- catch (err) {
1562
- emitJson(program, fail("E_MISSION_TARGET_ADD", String(err instanceof Error ? err.message : err)));
1563
- }
1564
- });
1565
- missionTarget
1566
- .command("list")
1567
- .description("list ALL mission targets (promoted and unpromoted) — a local/dev-facing listing")
1568
- .option("--dir <path>", "mission targets directory (default: ~/.jevitate/missions/targets)")
1569
- .option("--json", "emit a JSON envelope")
1570
- .action(async function () {
1571
- const { dir, json } = this.opts();
1572
- try {
1573
- const ctx = missionTargetContext(resolveMissionTargetsDir(deps, dir));
1574
- const targets = await listMissionTargets(ctx);
1575
- const envelope = ok(targets);
1576
- if (json) {
1577
- emitJson(program, envelope);
1578
- }
1579
- else {
1580
- const out = program.configureOutput().writeOut;
1581
- for (const t of targets) {
1582
- out?.(`${t.id}\t${t.name}\t${t.authorizedOrigin}${t.promoted ? "" : " (unpromoted)"}\n`);
1583
- }
1584
- process.exitCode = 0;
1585
- }
1586
- }
1587
- catch (err) {
1588
- emitJson(program, fail("E_MISSION_TARGET_LIST", String(err instanceof Error ? err.message : err)));
1589
- }
1590
- });
1591
- missionTarget
1592
- .command("promote <id>")
1593
- .description("promote a registered target so queue_exploration can resolve it")
1594
- .option("--dir <path>", "mission targets directory (default: ~/.jevitate/missions/targets)")
1595
- .option("--json", "emit a JSON envelope")
1596
- .action(async function (id) {
1597
- const { dir, json } = this.opts();
1598
- try {
1599
- const ctx = missionTargetContext(resolveMissionTargetsDir(deps, dir));
1600
- const target = await promoteMissionTarget(ctx, id);
1601
- const envelope = ok(target);
1602
- if (json) {
1603
- emitJson(program, envelope);
1604
- }
1605
- else {
1606
- program.configureOutput().writeOut?.(`promoted mission target '${target.id}'\n`);
1607
- process.exitCode = 0;
1608
- }
1609
- }
1610
- catch (err) {
1611
- if (err instanceof UnknownMissionTargetError) {
1612
- emitJson(program, fail("E_UNKNOWN_MISSION_TARGET", err.message));
1613
- }
1614
- else {
1615
- emitJson(program, fail("E_MISSION_TARGET_PROMOTE", String(err instanceof Error ? err.message : err)));
1616
- }
1617
- }
1618
- });
1619
- // Additive: `jevitate mcp` (Ticket #20) — start an MCP stdio server that
1620
- // exposes ONLY `@jevitate/mcp-facade`'s allowlisted tools (never the raw
1621
- // browser primitives in FORBIDDEN_TOOLS). This is the subcommand form of the
1622
- // MCP server (single-bundle deployment — no separate published package).
1623
- // The server owns stdin/stdout as the MCP protocol channel, so on success it
1624
- // blocks and writes NOTHING to stdout; only a setup failure (before the
1625
- // transport connects) emits a JSON envelope.
1626
- program
1627
- .command("mcp")
1628
- .description("start an MCP stdio server exposing only the allowlisted Jevitate tools")
1629
- .option("--dir <path>", "journeys directory (default: ~/.jevitate/journeys)")
1630
- .option("--print-config <harness>", "print the config snippet to register `jevitate mcp` in a harness (claude | cursor | codex | json) and exit — prints only, writes nothing")
1631
- .action(async function () {
1632
- const { dir, printConfig } = this.opts();
1633
- // `--print-config <harness>` is the universal escape hatch: render the
1634
- // exact registration snippet and exit WITHOUT starting the server (safe:
1635
- // no writes, no stdio takeover). An unknown harness is a fail envelope.
1636
- if (printConfig !== undefined) {
1637
- const harness = printConfig;
1638
- if (harness !== "claude" && harness !== "cursor" && harness !== "codex" && harness !== "json") {
1639
- emitJson(program, fail("E_MCP_PRINT_CONFIG", `--print-config must be one of claude | cursor | codex | json (got '${printConfig}')`));
1640
- return;
1641
- }
1642
- program.configureOutput().writeOut?.(`${renderPrintConfig(harness)}\n`);
1643
- process.exitCode = 0;
1644
- return;
1645
- }
1646
- try {
1647
- // Credential store + generation gateway for the allowlisted
1648
- // `ai_generate_text` tool. The gateway is the REAL OpenRouter adapter:
1649
- // the key is read only inside it (Authorization header only), every
1650
- // outbound payload passes the never-to-model guard, and the facade's
1651
- // preflight returns a typed `setup_required` when the key is absent —
1652
- // so no `--real/--fake` flag is needed for the non-interactive server.
1653
- const aiStore = envCredentialStore(deps.ai?.env ?? process.env, deps.ai?.localConfig ?? {});
1654
- const generationGateway = deps.ai?.gateway ??
1655
- new OpenRouterGenerationGateway({
1656
- store: aiStore,
1657
- catalog: deps.ai?.catalog ?? DEFAULT_EXPLORE_CATALOG,
1658
- constraints: deps.ai?.constraints ?? DEFAULT_EXPLORE_CONSTRAINTS,
1659
- call: await realOpenRouterCall(),
1660
- });
1661
- await startMcpServer({
1662
- journeysDir: resolveJourneysDir(deps, dir),
1663
- missionTargetsDir: resolveMissionTargetsDir(deps),
1664
- missionQueueDir: resolveDataDir(["missions", "queue"]),
1665
- inboxDir: resolveInboxDir(deps),
1666
- credentialStore: aiStore,
1667
- generationGateway,
1668
- });
1669
- }
1670
- catch (err) {
1671
- emitJson(program, fail("E_MCP_SERVE", String(err instanceof Error ? err.message : err)));
1672
- }
1673
- });
1674
- // Additive: `jevitate ui` (Task 8) — starts the local, loopback-only HTTP
1675
- // HITL approval dashboard (ui-api.ts's `startUiServer`). Resolves the SAME
1676
- // inbox dir `jevitate mcp`'s inbox tools serve (resolveInboxDir), so the
1677
- // two commands agree on where approvals/handbacks/reviews live. On success
1678
- // it prints the bound URL (carrying the capability token) and stays alive —
1679
- // the open HTTP server keeps the process running, the same way `mcp`'s open
1680
- // stdio transport does.
1681
- program
1682
- .command("ui")
1683
- .description("start the local HITL approval dashboard (loopback-only HTTP server)")
1684
- .option("--port <n>", "explicit port (fails on conflict; default 4180, retries on conflict)")
1685
- .option("--no-open", "do not open the dashboard URL in the default browser")
1686
- .option("--inbox-dir <path>", "inbox store directory (default: ~/.jevitate/inbox — same dir `jevitate mcp` serves)")
1687
- .action(async function () {
1688
- const o = this.opts();
1689
- try {
1690
- const start = deps.ui?.startUiServer ?? startUiServer;
1691
- const handle = await start({
1692
- inboxDir: resolveInboxDir(deps, o.inboxDir),
1693
- open: o.open ?? true,
1694
- ...(o.port !== undefined ? { port: Number(o.port) } : {}),
1695
- });
1696
- program.configureOutput().writeOut?.(`${handle.url}\n`);
1697
- }
1698
- catch (err) {
1699
- emitJson(program, fail("E_UI_SERVE", String(err instanceof Error ? err.message : err)));
1700
- }
1701
- });
1702
- // Additive: `jevitate ux <recording>` (issue #30) — offline UX review of a
1703
- // saved Recording. Findings are advisory; a `failed` analysis is a non-zero
1704
- // fail envelope (never a fabricated clean report).
1705
- program
1706
- .command("ux <recording>")
1707
- .description("offline UX review of a saved Recording — ranked, cited usability findings")
1708
- .option("--app-class <class>", "app class for calibration (required), e.g. consumer|admin|internal")
1709
- .option("--persona <p>", "optional persona for calibration")
1710
- .option("--job <text>", "the job the flow pursues (improves relevance)")
1711
- .option("--out <dir>", "directory to write the UX report")
1712
- .option("--real", "use live Jev gateways (requires keys)", false)
1713
- .option("--fake-ai", "use deterministic fake gateways", false)
1714
- .option("--json", "emit a JSON envelope")
1715
- .action(async function (recordingPath) {
1716
- const o = this.opts();
1717
- if (!o.appClass) {
1718
- emitJson(program, fail("E_UX_ARGS", "--app-class is required"));
1719
- return;
1720
- }
1721
- let recording;
1722
- try {
1723
- recording = RecordingSchema.parse(JSON.parse(await readFile(recordingPath, "utf8")));
1724
- }
1725
- catch (err) {
1726
- emitJson(program, fail("E_UX_RECORDING", String(err instanceof Error ? err.message : err)));
1727
- return;
1728
- }
1729
- let uxJudge;
1730
- try {
1731
- ({ judge: uxJudge } = await buildExploreGateways(deps, { real: o.real ?? false, fakeAi: o.fakeAi ?? false }));
1732
- }
1733
- catch (err) {
1734
- if (err instanceof MissingCredentialError || err instanceof GatewaySelectionError) {
1735
- emitJson(program, fail("E_AI_SETUP_REQUIRED", err.message));
1736
- }
1737
- else {
1738
- emitJson(program, fail("E_UX_SETUP", String(err instanceof Error ? err.message : err)));
1739
- }
1740
- return;
1741
- }
1742
- try {
1743
- const result = await runUxReview({
1744
- recording,
1745
- appContext: {
1746
- appClass: o.appClass,
1747
- ...(o.persona ? { persona: o.persona } : {}),
1748
- ...(o.job ? { job: o.job } : {}),
1749
- },
1750
- judge: uxJudge,
1751
- outDir: o.out,
1752
- });
1753
- emitJson(program, ok(result));
1754
- }
1755
- catch (err) {
1756
- if (err instanceof UxAnalysisFailedError) {
1757
- emitJson(program, fail("E_UX_ANALYSIS", err.message));
1758
- }
1759
- else {
1760
- emitJson(program, fail("E_UX_RUN", String(err instanceof Error ? err.message : err)));
1761
- }
1762
- }
1763
- });
48
+ program.name("jevitate").description("Autonomous browser testing that turns discovered bugs into deterministic regression tests").version(versionString());
49
+ // #218: the shared refusal path (cli-refusal.ts) needs to know which command is running.
50
+ trackActionCommand(program);
51
+ registerInitCommands(program, deps);
52
+ registerProfileCommands(program, deps);
53
+ registerSiteCommands(program, deps);
54
+ registerRecordingCommands(program, deps);
55
+ registerJourneyCommands(program, deps);
56
+ registerSourceCommands(program, deps);
57
+ registerLoadCommands(program, deps);
58
+ registerExploreCommands(program, deps, buildProgram);
59
+ registerCampaignCommands(program, deps, buildProgram); // #293: journey-anchored campaigns
60
+ registerVerifyFixCommands(program, deps);
61
+ // `ledger add|verify|list` (#195 part 6): the repro material verify-fix needs, kept by fingerprint.
62
+ registerLedgerCommands(program, {
63
+ ...(deps.explore?.targetsConfigPath === undefined ? {} : { targetsConfigPath: deps.explore.targetsConfigPath }),
64
+ ...(deps.explore?.browserPortFactory === undefined ? {} : { browserPortFactory: deps.explore.browserPortFactory }),
65
+ browserLaunch: (flags) => browserLaunchFromFlags(flags),
66
+ }, withBrowserLaunchFlags);
67
+ registerAuthorJourneyCommands(program, deps);
68
+ registerDemoCommands(program, deps);
69
+ registerRecordCommands(program, deps);
70
+ registerRegressionCommands(program, deps);
71
+ registerMissionCommands(program, deps);
72
+ registerServeCommands(program, deps, buildProgram);
73
+ registerInboxCommands(program, deps); // #254: MCP inbox tools from the CLI
74
+ registerUxCommands(program, deps);
1764
75
  registerAiCommands(program, deps);
76
+ // #137 / #138 / #139 — CI gate, baseline diff and the consolidated defect report (own files).
77
+ registerCheckCommand(program, {
78
+ buildGateways: (sel) => buildExploreGateways(deps, sel),
79
+ journeysDir: resolveJourneysDir(deps),
80
+ sitePolicyDbPath: resolveDbPath(deps),
81
+ ...(deps.explore?.targetsConfigPath === undefined ? {} : { targetsConfigPath: deps.explore.targetsConfigPath }),
82
+ ...(deps.environmentsFile === undefined ? {} : { environmentsFile: deps.environmentsFile }),
83
+ ...(deps.explore?.browserPortFactory === undefined ? {} : { browserPortFactory: deps.explore.browserPortFactory }),
84
+ browserLaunch: (flags) => browserLaunchFromFlags(flags),
85
+ }, withBrowserLaunchFlags);
86
+ registerReportCommands(program, { missionTargetsDir: resolveMissionTargetsDir(deps) });
87
+ registerLogsCommands(program, deps);
88
+ registerInvariantsCommands(program);
89
+ registerDoctorCommand(program); // #205: resource governance — host load, machine browser slots, orphans
90
+ useUsageExitCode(program);
1765
91
  return program;
1766
92
  }
1767
- /** Distinct from MissingCredentialError: "no --real/--fake-ai selected" vs "keys missing." */
1768
- class GatewaySelectionError extends Error {
1769
- }
1770
- const DEFAULT_EXPLORE_CATALOG = [
1771
- { id: "openai/gpt-4o-mini", promptUsdPer1k: 0.15, completionUsdPer1k: 0.6, regions: [], latencyClass: "fast", capabilities: [] },
1772
- ];
1773
- const DEFAULT_EXPLORE_CONSTRAINTS = { requiredCapabilities: [] };
1774
93
  /**
1775
- * Selects the exploration gateways. Injected gateways (tests) win; otherwise
1776
- * `--real` builds the live Jev + OpenRouter adapters behind a fail-closed
1777
- * credential preflight, and `--fake-ai` uses deterministic fakes (a pipeline
1778
- * smoke — the fake judge always proposes `done`, so it will not drive to a
1779
- * goal). No selection is a fail-closed refusal, never a silent fake.
94
+ * #210: a commander parse error (unknown option, missing argument or required option, bad choice)
95
+ * is a usage error — exit 64 like every command's own argument errors, never 1 (defects found).
96
+ * Wraps each command's `error()`, which every commander parse error goes through.
1780
97
  */
1781
- async function buildExploreGateways(deps, opts) {
1782
- if (deps.explore?.judge && deps.explore?.gen) {
1783
- return { judge: deps.explore.judge, gen: deps.explore.gen };
1784
- }
1785
- const store = envCredentialStore(deps.explore?.env ?? process.env, deps.explore?.localConfig ?? {});
1786
- if (opts.real) {
1787
- requireKeys("generation", store); // fail-closed
1788
- requireKeys("judgment", store); // fail-closed
1789
- const gen = new OpenRouterGenerationGateway({
1790
- store,
1791
- catalog: DEFAULT_EXPLORE_CATALOG,
1792
- constraints: DEFAULT_EXPLORE_CONSTRAINTS,
1793
- call: await realOpenRouterCall(),
1794
- });
1795
- const judge = new JevJudgmentGateway(store, await realJevClientCall());
1796
- return { judge, gen };
1797
- }
1798
- if (opts.fakeAi) {
1799
- return { judge: fakeDoneJudge(), gen: new FakeGenerationGateway() };
1800
- }
1801
- throw new GatewaySelectionError("no gateway selected — pass --real for live Jev+OpenRouter (after `jevitate ai setup`), or --fake-ai for a deterministic pipeline smoke");
1802
- }
1803
- /** A judge that always proposes `done` — used only by `--fake-ai` (smoke). */
1804
- function fakeDoneJudge() {
1805
- return {
1806
- async systemOne(_args) {
1807
- return { op: { kind: "choice", value: "done", confidence: 1 } };
1808
- },
1809
- };
1810
- }
1811
- /** Real OpenRouter seam (lazy import; not unit-tested) — mirrors ai-cli.ts. */
1812
- async function realOpenRouterCall() {
1813
- const { generateObject } = await import("ai");
1814
- const { createOpenRouter } = await import("@openrouter/ai-sdk-provider");
1815
- return async ({ model, schema, body, authHeader }) => {
1816
- const openrouter = createOpenRouter({ headers: { Authorization: authHeader } });
1817
- const start = Date.now();
1818
- const { object } = await generateObject({ model: openrouter(model), schema, prompt: JSON.stringify(body) });
1819
- return { object, latencyMs: Date.now() - start };
98
+ function useUsageExitCode(cmd) {
99
+ const original = cmd.error.bind(cmd);
100
+ cmd.error = (message, errorOptions) => {
101
+ // An argParser's InvalidArgumentError (cli-args.ts) carries commander's generic exit code 1.
102
+ const exitCode = errorOptions?.code === "commander.invalidArgument" ? EXIT_CODES.usage : (errorOptions?.exitCode ?? EXIT_CODES.usage);
103
+ const bare = message.replace(/^error: /, "");
104
+ // #218: a bad numeric value (cli-args.ts), unknown flag or missing argument under `--json` still
105
+ // yields the one envelope line a machine caller parses; commander's human line goes to stderr.
106
+ if (exitCode === EXIT_CODES.usage && rawArgsOf(rootOf(cmd)).includes("--json")) {
107
+ const envelope = fail(usageParseErrorCode(cmd), bare);
108
+ rootOf(cmd).configureOutput().writeOut?.(`${JSON.stringify(envelope)}\n`);
109
+ }
110
+ // #227: the human stderr line matches every other refusal's `error <CODE>: …` (+ a --help hint) —
111
+ // never commander's own unlabeled `error: …` text, which used to be the only usage refusal
112
+ // without a code (the code appeared only under --json).
113
+ const path = commandPath(cmd);
114
+ const human = exitCode === EXIT_CODES.usage ? `error ${usageParseErrorCode(cmd)}: ${bare}\nnext: jevitate ${path === "" ? "" : `${path} `}--help` : message;
115
+ return original(human, { ...errorOptions, exitCode });
1820
116
  };
117
+ for (const sub of cmd.commands)
118
+ useUsageExitCode(sub);
1821
119
  }
1822
120
  /**
1823
- * Real Jev seam (documented, lazy, not unit-tested). Uses a non-literal
1824
- * specifier so this package builds and tests without `@typesafe-ai/sdk`
1825
- * installed; the host installs + wires it for live judgment.
121
+ * The envelope code of a command line commander refused while parsing it (a usage error, 64): the
122
+ * command's own `E_<COMMAND>_ARGS` (`explore` → E_EXPLORE_ARGS, `verify-fix` → E_VERIFY_FIX_ARGS),
123
+ * the code its in-action argument checks already use; `E_CLI_ARGS` at the root (an unknown command).
1826
124
  */
1827
- async function realJevClientCall() {
1828
- const specifier = "@typesafe-ai/sdk";
1829
- const sdk = (await import(specifier).catch(() => {
1830
- throw new Error("live judgment requires @typesafe-ai/sdk to be installed and wired (see jev.ts seam)");
1831
- }));
1832
- return async ({ state, questions, authHeader }) => {
1833
- const client = sdk.createClient({ authHeader });
1834
- return client.systemOne({ state, questions });
1835
- };
1836
- }
1837
- /**
1838
- * Distinguishes a malformed `--decisions <file>` (E_INVALID_DECISIONS) from
1839
- * every other failure mode of the `postdoc` action (E_INVALID_TAKE) without
1840
- * making `loadDecisions` itself responsible for emitting the CLI envelope —
1841
- * matching this file's existing pattern of one try/catch per subcommand
1842
- * mapping to one error code.
1843
- */
1844
- class DecisionsParseError extends Error {
1845
- cause;
1846
- constructor(cause) {
1847
- super(String(cause));
1848
- this.cause = cause;
1849
- }
1850
- }
1851
- async function loadDecisions(file) {
1852
- let raw;
1853
- let parsed;
1854
- try {
1855
- raw = await readFile(file, "utf8");
1856
- parsed = JSON.parse(raw);
1857
- }
1858
- catch (err) {
1859
- throw new DecisionsParseError(err);
1860
- }
1861
- const result = PostdocDecisionsSchema.safeParse(parsed);
1862
- if (!result.success) {
1863
- throw new DecisionsParseError(result.error);
1864
- }
1865
- return result.data;
1866
- }
1867
- /**
1868
- * Thin `@clack/prompts` adapter: walks `authoring`'s fill/select steps in
1869
- * order and asks the human how to classify each one. ALL logic (variance
1870
- * guards, secret-materialization checks, the actual step rewrite) lives in
1871
- * `applyPostdoc`/`diffTakes` — this function only collects a
1872
- * `PostdocDecision[]` to hand them.
1873
- */
1874
- async function promptForDecisions(authoring) {
1875
- clack.intro("recording postdoc — review captured fill/select steps");
1876
- const decisions = [];
1877
- const fillSteps = flattenBaseFillSteps(authoring.recording);
1878
- for (const { ref } of fillSteps) {
1879
- const classify = await clack.select({
1880
- message: `Step ${ref.page}:${ref.step} — how should this value be classified?`,
1881
- options: [
1882
- { value: "constant", label: "constant", hint: "fix this value in the artifact" },
1883
- { value: "variable", label: "variable", hint: "prompt for a value at replay time" },
1884
- { value: "handback", label: "handback", hint: "hand control to a human at replay time" },
1885
- ],
1886
- });
1887
- if (clack.isCancel(classify)) {
1888
- clack.cancel("postdoc review cancelled");
1889
- process.exit(1);
1890
- }
1891
- const label = await promptOptionalText("Label for this step? (blank to skip)");
1892
- const chunk = await promptOptionalText("Chunk name for this step? (blank to skip)");
1893
- let decision;
1894
- if (classify === "constant") {
1895
- const acknowledgeVaried = await clack.confirm({
1896
- message: "Acknowledge this value varied across takes anyway?",
1897
- initialValue: false,
1898
- });
1899
- if (clack.isCancel(acknowledgeVaried)) {
1900
- clack.cancel("postdoc review cancelled");
1901
- process.exit(1);
1902
- }
1903
- decision = { step: ref, classify: "constant", ...(acknowledgeVaried ? { acknowledgeVaried: true } : {}) };
1904
- }
1905
- else if (classify === "variable") {
1906
- const name = await clack.text({ message: "Variable name?" });
1907
- if (clack.isCancel(name)) {
1908
- clack.cancel("postdoc review cancelled");
1909
- process.exit(1);
1910
- }
1911
- decision = { step: ref, classify: "variable", name };
1912
- }
1913
- else {
1914
- const prompt = await clack.text({ message: "Handback prompt for the human operator?" });
1915
- if (clack.isCancel(prompt)) {
1916
- clack.cancel("postdoc review cancelled");
1917
- process.exit(1);
1918
- }
1919
- decision = { step: ref, classify: "handback", prompt };
1920
- }
1921
- if (label !== undefined)
1922
- decision = { ...decision, label };
1923
- if (chunk !== undefined)
1924
- decision = { ...decision, chunk };
1925
- decisions.push(decision);
1926
- }
1927
- clack.outro("review complete");
1928
- return decisions;
1929
- }
1930
- async function promptOptionalText(message) {
1931
- const value = await clack.text({ message, defaultValue: "" });
1932
- if (clack.isCancel(value)) {
1933
- clack.cancel("postdoc review cancelled");
1934
- process.exit(1);
1935
- }
1936
- return value === "" ? undefined : value;
125
+ function usageParseErrorCode(cmd) {
126
+ const path = commandPath(cmd);
127
+ return `E_${path === "" ? "CLI" : path.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_ARGS`;
128
+ }
129
+ /** The command line as given (commander keeps it on the root; untyped). */
130
+ function rawArgsOf(root) {
131
+ const raw = root.rawArgs;
132
+ return Array.isArray(raw) ? raw : [];
133
+ }
134
+ function rootOf(cmd) {
135
+ let c = cmd;
136
+ while (c.parent !== null)
137
+ c = c.parent;
138
+ return c;
1937
139
  }
1938
140
  //# sourceMappingURL=program.js.map