@jevitate/cli 0.1.0 → 0.2.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 (375) hide show
  1. package/README.md +23 -0
  2. package/dist/ai-cli.js +40 -30
  3. package/dist/ai-cli.js.map +1 -1
  4. package/dist/author-journey-cli.d.ts +4 -0
  5. package/dist/author-journey-cli.js +109 -0
  6. package/dist/author-journey-cli.js.map +1 -0
  7. package/dist/bin.js +58077 -22454
  8. package/dist/bin.js.map +1 -1
  9. package/dist/browser-run-options.d.ts +58 -0
  10. package/dist/browser-run-options.js +106 -0
  11. package/dist/browser-run-options.js.map +1 -0
  12. package/dist/build-info.generated.d.ts +2 -0
  13. package/dist/build-info.generated.js +10 -0
  14. package/dist/build-info.generated.js.map +1 -0
  15. package/dist/check-api.d.ts +5 -0
  16. package/dist/check-api.js +233 -0
  17. package/dist/check-api.js.map +1 -0
  18. package/dist/check-budget.d.ts +26 -0
  19. package/dist/check-budget.js +89 -0
  20. package/dist/check-budget.js.map +1 -0
  21. package/dist/check-cli.d.ts +31 -0
  22. package/dist/check-cli.js +109 -0
  23. package/dist/check-cli.js.map +1 -0
  24. package/dist/check-execute.d.ts +25 -0
  25. package/dist/check-execute.js +385 -0
  26. package/dist/check-execute.js.map +1 -0
  27. package/dist/check-plan.d.ts +113 -0
  28. package/dist/check-plan.js +448 -0
  29. package/dist/check-plan.js.map +1 -0
  30. package/dist/check-suite.d.ts +171 -0
  31. package/dist/check-suite.js +515 -0
  32. package/dist/check-suite.js.map +1 -0
  33. package/dist/check-types.d.ts +181 -0
  34. package/dist/check-types.js +35 -0
  35. package/dist/check-types.js.map +1 -0
  36. package/dist/cli-args.d.ts +26 -0
  37. package/dist/cli-args.js +50 -0
  38. package/dist/cli-args.js.map +1 -0
  39. package/dist/cli-output.d.ts +102 -0
  40. package/dist/cli-output.js +516 -0
  41. package/dist/cli-output.js.map +1 -0
  42. package/dist/cli-reference.d.ts +3 -0
  43. package/dist/cli-reference.js +81 -0
  44. package/dist/cli-reference.js.map +1 -0
  45. package/dist/cli-refusal.d.ts +12 -0
  46. package/dist/cli-refusal.js +52 -0
  47. package/dist/cli-refusal.js.map +1 -0
  48. package/dist/cli-shared.d.ts +330 -0
  49. package/dist/cli-shared.js +492 -0
  50. package/dist/cli-shared.js.map +1 -0
  51. package/dist/cli-surface.d.ts +33 -0
  52. package/dist/cli-surface.js +48 -0
  53. package/dist/cli-surface.js.map +1 -0
  54. package/dist/conversation-options.d.ts +15 -0
  55. package/dist/conversation-options.js +10 -0
  56. package/dist/conversation-options.js.map +1 -0
  57. package/dist/credentials-file.d.ts +15 -0
  58. package/dist/credentials-file.js +56 -0
  59. package/dist/credentials-file.js.map +1 -0
  60. package/dist/data-dir.d.ts +1 -1
  61. package/dist/data-dir.js +1 -1
  62. package/dist/defect-evidence.d.ts +141 -0
  63. package/dist/defect-evidence.js +502 -0
  64. package/dist/defect-evidence.js.map +1 -0
  65. package/dist/demo-aspect-api.d.ts +209 -0
  66. package/dist/demo-aspect-api.js +397 -0
  67. package/dist/demo-aspect-api.js.map +1 -0
  68. package/dist/demo-aspect-cli.d.ts +3 -0
  69. package/dist/demo-aspect-cli.js +241 -0
  70. package/dist/demo-aspect-cli.js.map +1 -0
  71. package/dist/demo-capture.d.ts +108 -0
  72. package/dist/demo-capture.js +438 -0
  73. package/dist/demo-capture.js.map +1 -0
  74. package/dist/engine.d.ts +35 -0
  75. package/dist/engine.js +25 -0
  76. package/dist/engine.js.map +1 -0
  77. package/dist/environments.d.ts +155 -0
  78. package/dist/environments.js +364 -0
  79. package/dist/environments.js.map +1 -0
  80. package/dist/exit-codes.d.ts +46 -0
  81. package/dist/exit-codes.js +91 -0
  82. package/dist/exit-codes.js.map +1 -0
  83. package/dist/explore-adversarial.d.ts +137 -0
  84. package/dist/explore-adversarial.js +218 -0
  85. package/dist/explore-adversarial.js.map +1 -0
  86. package/dist/explore-api.d.ts +7 -187
  87. package/dist/explore-api.js +7 -301
  88. package/dist/explore-api.js.map +1 -1
  89. package/dist/explore-author.d.ts +79 -0
  90. package/dist/explore-author.js +86 -0
  91. package/dist/explore-author.js.map +1 -0
  92. package/dist/explore-cli.d.ts +7 -0
  93. package/dist/explore-cli.js +963 -0
  94. package/dist/explore-cli.js.map +1 -0
  95. package/dist/explore-coverage.d.ts +158 -0
  96. package/dist/explore-coverage.js +240 -0
  97. package/dist/explore-coverage.js.map +1 -0
  98. package/dist/explore-feature.d.ts +127 -0
  99. package/dist/explore-feature.js +220 -0
  100. package/dist/explore-feature.js.map +1 -0
  101. package/dist/explore-goal.d.ts +253 -0
  102. package/dist/explore-goal.js +374 -0
  103. package/dist/explore-goal.js.map +1 -0
  104. package/dist/explore-shared.d.ts +152 -0
  105. package/dist/explore-shared.js +129 -0
  106. package/dist/explore-shared.js.map +1 -0
  107. package/dist/explore-specs.d.ts +53 -0
  108. package/dist/explore-specs.js +333 -0
  109. package/dist/explore-specs.js.map +1 -0
  110. package/dist/findings-filing.d.ts +64 -0
  111. package/dist/findings-filing.js +108 -0
  112. package/dist/findings-filing.js.map +1 -0
  113. package/dist/fixture-auth.d.ts +52 -0
  114. package/dist/fixture-auth.js +82 -0
  115. package/dist/fixture-auth.js.map +1 -0
  116. package/dist/fixture-cli.d.ts +70 -0
  117. package/dist/fixture-cli.js +178 -0
  118. package/dist/fixture-cli.js.map +1 -0
  119. package/dist/github-issue-filer.d.ts +62 -0
  120. package/dist/github-issue-filer.js +188 -0
  121. package/dist/github-issue-filer.js.map +1 -0
  122. package/dist/host-health-run.d.ts +24 -0
  123. package/dist/host-health-run.js +27 -0
  124. package/dist/host-health-run.js.map +1 -0
  125. package/dist/http-5xx-outcome.d.ts +6 -0
  126. package/dist/http-5xx-outcome.js +39 -0
  127. package/dist/http-5xx-outcome.js.map +1 -0
  128. package/dist/inbox-cli.d.ts +7 -0
  129. package/dist/inbox-cli.js +197 -0
  130. package/dist/inbox-cli.js.map +1 -0
  131. package/dist/index.d.ts +1 -0
  132. package/dist/index.js +1 -0
  133. package/dist/index.js.map +1 -1
  134. package/dist/init-cli.d.ts +4 -0
  135. package/dist/init-cli.js +127 -0
  136. package/dist/init-cli.js.map +1 -0
  137. package/dist/init-keys.d.ts +19 -2
  138. package/dist/init-keys.js +15 -4
  139. package/dist/init-keys.js.map +1 -1
  140. package/dist/init-next-steps.d.ts +35 -0
  141. package/dist/init-next-steps.js +79 -0
  142. package/dist/init-next-steps.js.map +1 -0
  143. package/dist/invariants-file.d.ts +34 -0
  144. package/dist/invariants-file.js +68 -0
  145. package/dist/invariants-file.js.map +1 -0
  146. package/dist/invariants-validate.d.ts +30 -0
  147. package/dist/invariants-validate.js +105 -0
  148. package/dist/invariants-validate.js.map +1 -0
  149. package/dist/journey-annotate-api.d.ts +76 -0
  150. package/dist/journey-annotate-api.js +256 -0
  151. package/dist/journey-annotate-api.js.map +1 -0
  152. package/dist/journey-annotate-cli.d.ts +9 -0
  153. package/dist/journey-annotate-cli.js +179 -0
  154. package/dist/journey-annotate-cli.js.map +1 -0
  155. package/dist/journey-api.d.ts +79 -2
  156. package/dist/journey-api.js +136 -19
  157. package/dist/journey-api.js.map +1 -1
  158. package/dist/journey-cli.d.ts +4 -0
  159. package/dist/journey-cli.js +370 -0
  160. package/dist/journey-cli.js.map +1 -0
  161. package/dist/journey-demo-api.d.ts +112 -0
  162. package/dist/journey-demo-api.js +392 -0
  163. package/dist/journey-demo-api.js.map +1 -0
  164. package/dist/journey-demo-cli.d.ts +10 -0
  165. package/dist/journey-demo-cli.js +155 -0
  166. package/dist/journey-demo-cli.js.map +1 -0
  167. package/dist/kill-signal.d.ts +193 -0
  168. package/dist/kill-signal.js +284 -0
  169. package/dist/kill-signal.js.map +1 -0
  170. package/dist/launch-armed.d.ts +37 -0
  171. package/dist/launch-armed.js +31 -0
  172. package/dist/launch-armed.js.map +1 -0
  173. package/dist/ledger-api.d.ts +106 -0
  174. package/dist/ledger-api.js +237 -0
  175. package/dist/ledger-api.js.map +1 -0
  176. package/dist/ledger-cli.d.ts +18 -0
  177. package/dist/ledger-cli.js +107 -0
  178. package/dist/ledger-cli.js.map +1 -0
  179. package/dist/load-api.d.ts +29 -11
  180. package/dist/load-api.js +23 -15
  181. package/dist/load-api.js.map +1 -1
  182. package/dist/load-cli.d.ts +4 -0
  183. package/dist/load-cli.js +97 -0
  184. package/dist/load-cli.js.map +1 -0
  185. package/dist/log-correlation.d.ts +147 -0
  186. package/dist/log-correlation.js +325 -0
  187. package/dist/log-correlation.js.map +1 -0
  188. package/dist/log-lines.d.ts +131 -0
  189. package/dist/log-lines.js +389 -0
  190. package/dist/log-lines.js.map +1 -0
  191. package/dist/log-sources.d.ts +71 -0
  192. package/dist/log-sources.js +235 -0
  193. package/dist/log-sources.js.map +1 -0
  194. package/dist/logs-cli.d.ts +3 -0
  195. package/dist/logs-cli.js +51 -0
  196. package/dist/logs-cli.js.map +1 -0
  197. package/dist/logs-retention.d.ts +28 -0
  198. package/dist/logs-retention.js +84 -0
  199. package/dist/logs-retention.js.map +1 -0
  200. package/dist/mcp-api.d.ts +89 -1
  201. package/dist/mcp-api.js +472 -18
  202. package/dist/mcp-api.js.map +1 -1
  203. package/dist/mcp-args.d.ts +38 -0
  204. package/dist/mcp-args.js +131 -0
  205. package/dist/mcp-args.js.map +1 -0
  206. package/dist/mcp-cli-bridge.d.ts +34 -0
  207. package/dist/mcp-cli-bridge.js +62 -0
  208. package/dist/mcp-cli-bridge.js.map +1 -0
  209. package/dist/mcp-cli-runner.d.ts +29 -0
  210. package/dist/mcp-cli-runner.js +44 -0
  211. package/dist/mcp-cli-runner.js.map +1 -0
  212. package/dist/mcp-cli-tools.d.ts +125 -0
  213. package/dist/mcp-cli-tools.js +680 -0
  214. package/dist/mcp-cli-tools.js.map +1 -0
  215. package/dist/mcp-paths.d.ts +28 -0
  216. package/dist/mcp-paths.js +76 -0
  217. package/dist/mcp-paths.js.map +1 -0
  218. package/dist/mission-actors.d.ts +40 -0
  219. package/dist/mission-actors.js +72 -0
  220. package/dist/mission-actors.js.map +1 -0
  221. package/dist/mission-api.d.ts +35 -0
  222. package/dist/mission-api.js +58 -2
  223. package/dist/mission-api.js.map +1 -1
  224. package/dist/mission-cli.d.ts +4 -0
  225. package/dist/mission-cli.js +247 -0
  226. package/dist/mission-cli.js.map +1 -0
  227. package/dist/mission-exit.d.ts +22 -0
  228. package/dist/mission-exit.js +26 -0
  229. package/dist/mission-exit.js.map +1 -0
  230. package/dist/mission-fixtures.d.ts +225 -0
  231. package/dist/mission-fixtures.js +828 -0
  232. package/dist/mission-fixtures.js.map +1 -0
  233. package/dist/mission-journal.d.ts +60 -0
  234. package/dist/mission-journal.js +115 -0
  235. package/dist/mission-journal.js.map +1 -0
  236. package/dist/mission-queue-cli.d.ts +10 -0
  237. package/dist/mission-queue-cli.js +198 -0
  238. package/dist/mission-queue-cli.js.map +1 -0
  239. package/dist/mission-queue-runner.d.ts +126 -0
  240. package/dist/mission-queue-runner.js +397 -0
  241. package/dist/mission-queue-runner.js.map +1 -0
  242. package/dist/multi-run-cli.d.ts +37 -0
  243. package/dist/multi-run-cli.js +110 -0
  244. package/dist/multi-run-cli.js.map +1 -0
  245. package/dist/multi-run.d.ts +339 -0
  246. package/dist/multi-run.js +619 -0
  247. package/dist/multi-run.js.map +1 -0
  248. package/dist/openrouter-call.d.ts +22 -0
  249. package/dist/openrouter-call.js +63 -0
  250. package/dist/openrouter-call.js.map +1 -0
  251. package/dist/profile-cli.d.ts +4 -0
  252. package/dist/profile-cli.js +57 -0
  253. package/dist/profile-cli.js.map +1 -0
  254. package/dist/program.d.ts +3 -75
  255. package/dist/program.js +119 -1921
  256. package/dist/program.js.map +1 -1
  257. package/dist/project-dir.d.ts +96 -0
  258. package/dist/project-dir.js +220 -0
  259. package/dist/project-dir.js.map +1 -0
  260. package/dist/record-api.d.ts +10 -6
  261. package/dist/record-api.js +39 -28
  262. package/dist/record-api.js.map +1 -1
  263. package/dist/record-cli.d.ts +4 -0
  264. package/dist/record-cli.js +64 -0
  265. package/dist/record-cli.js.map +1 -0
  266. package/dist/recording-cli.d.ts +4 -0
  267. package/dist/recording-cli.js +248 -0
  268. package/dist/recording-cli.js.map +1 -0
  269. package/dist/regression-api.d.ts +101 -0
  270. package/dist/regression-api.js +384 -5
  271. package/dist/regression-api.js.map +1 -1
  272. package/dist/regression-cli.d.ts +4 -0
  273. package/dist/regression-cli.js +232 -0
  274. package/dist/regression-cli.js.map +1 -0
  275. package/dist/report-api.d.ts +146 -0
  276. package/dist/report-api.js +430 -0
  277. package/dist/report-api.js.map +1 -0
  278. package/dist/report-cli.d.ts +12 -0
  279. package/dist/report-cli.js +147 -0
  280. package/dist/report-cli.js.map +1 -0
  281. package/dist/result-schema.d.ts +22 -0
  282. package/dist/result-schema.js +28 -0
  283. package/dist/result-schema.js.map +1 -0
  284. package/dist/run-index.d.ts +29 -0
  285. package/dist/run-index.js +70 -0
  286. package/dist/run-index.js.map +1 -0
  287. package/dist/run-screenshots.d.ts +123 -0
  288. package/dist/run-screenshots.js +229 -0
  289. package/dist/run-screenshots.js.map +1 -0
  290. package/dist/runtime-env.d.ts +8 -0
  291. package/dist/runtime-env.js +22 -0
  292. package/dist/runtime-env.js.map +1 -0
  293. package/dist/secret-args.d.ts +23 -0
  294. package/dist/secret-args.js +45 -0
  295. package/dist/secret-args.js.map +1 -0
  296. package/dist/serve-cli.d.ts +4 -0
  297. package/dist/serve-cli.js +95 -0
  298. package/dist/serve-cli.js.map +1 -0
  299. package/dist/server-log-verify.d.ts +34 -0
  300. package/dist/server-log-verify.js +115 -0
  301. package/dist/server-log-verify.js.map +1 -0
  302. package/dist/session-check.d.ts +14 -0
  303. package/dist/session-check.js +44 -0
  304. package/dist/session-check.js.map +1 -0
  305. package/dist/site-cli.d.ts +4 -0
  306. package/dist/site-cli.js +127 -0
  307. package/dist/site-cli.js.map +1 -0
  308. package/dist/site-gate-cli.d.ts +28 -0
  309. package/dist/site-gate-cli.js +58 -0
  310. package/dist/site-gate-cli.js.map +1 -0
  311. package/dist/source-api.js +18 -3
  312. package/dist/source-api.js.map +1 -1
  313. package/dist/source-cli.d.ts +4 -0
  314. package/dist/source-cli.js +284 -0
  315. package/dist/source-cli.js.map +1 -0
  316. package/dist/source-run-api.d.ts +27 -1
  317. package/dist/source-run-api.js +22 -11
  318. package/dist/source-run-api.js.map +1 -1
  319. package/dist/storage-state-snapshot.d.ts +50 -0
  320. package/dist/storage-state-snapshot.js +87 -0
  321. package/dist/storage-state-snapshot.js.map +1 -0
  322. package/dist/suite-explore-options.d.ts +289 -0
  323. package/dist/suite-explore-options.js +140 -0
  324. package/dist/suite-explore-options.js.map +1 -0
  325. package/dist/target-config.d.ts +109 -0
  326. package/dist/target-config.js +216 -0
  327. package/dist/target-config.js.map +1 -0
  328. package/dist/transcript-file.d.ts +12 -0
  329. package/dist/transcript-file.js +23 -0
  330. package/dist/transcript-file.js.map +1 -0
  331. package/dist/ui-api.d.ts +3 -0
  332. package/dist/ui-api.js +7 -1
  333. package/dist/ui-api.js.map +1 -1
  334. package/dist/usability-capture.d.ts +43 -0
  335. package/dist/usability-capture.js +326 -0
  336. package/dist/usability-capture.js.map +1 -0
  337. package/dist/usage-config.d.ts +36 -0
  338. package/dist/usage-config.js +114 -0
  339. package/dist/usage-config.js.map +1 -0
  340. package/dist/ux-api.d.ts +3 -75
  341. package/dist/ux-api.js +3 -220
  342. package/dist/ux-api.js.map +1 -1
  343. package/dist/ux-cli.d.ts +4 -0
  344. package/dist/ux-cli.js +102 -0
  345. package/dist/ux-cli.js.map +1 -0
  346. package/dist/ux-config.d.ts +29 -0
  347. package/dist/ux-config.js +103 -0
  348. package/dist/ux-config.js.map +1 -0
  349. package/dist/ux-evidence.d.ts +54 -0
  350. package/dist/ux-evidence.js +197 -0
  351. package/dist/ux-evidence.js.map +1 -0
  352. package/dist/ux-review.d.ts +112 -0
  353. package/dist/ux-review.js +151 -0
  354. package/dist/ux-review.js.map +1 -0
  355. package/dist/ux-usability.d.ts +238 -0
  356. package/dist/ux-usability.js +494 -0
  357. package/dist/ux-usability.js.map +1 -0
  358. package/dist/verify-fix-api.d.ts +173 -0
  359. package/dist/verify-fix-api.js +454 -0
  360. package/dist/verify-fix-api.js.map +1 -0
  361. package/dist/verify-fix-cli.d.ts +4 -0
  362. package/dist/verify-fix-cli.js +110 -0
  363. package/dist/verify-fix-cli.js.map +1 -0
  364. package/package.json +19 -14
  365. package/skills/jevitate-ci-check/SKILL.md +77 -0
  366. package/skills/jevitate-demo/SKILL.md +61 -0
  367. package/skills/jevitate-explore/SKILL.md +88 -87
  368. package/skills/jevitate-getting-started/SKILL.md +88 -0
  369. package/skills/jevitate-load-test/SKILL.md +11 -2
  370. package/skills/jevitate-mission-scope/SKILL.md +19 -11
  371. package/skills/jevitate-record/SKILL.md +23 -5
  372. package/skills/jevitate-run-journey/SKILL.md +61 -17
  373. package/skills/jevitate-sources/SKILL.md +57 -14
  374. package/skills/jevitate-ux-review/SKILL.md +106 -16
  375. package/skills/jevitate-verify-fix/SKILL.md +76 -0
@@ -0,0 +1,963 @@
1
+ import { existsSync } from "node:fs";
2
+ import { envCredentialStore, MissingCredentialError } from "@jevitate/ai-core";
3
+ import { loadLocalCredentials } from "./credentials-file.js";
4
+ import { FixtureNotFoundError, ScopeUnderivableError, UnauthorizedExploreTargetError, resolveCoverageThresholds, parseSecretField, SecretFieldSpecError, validateDenyPatterns, } from "@jevitate/explore";
5
+ import { ok, fail } from "./envelope.js";
6
+ import { SessionFileInProjectError, assertSessionFileOutsideProject } from "./project-dir.js";
7
+ import { InvariantsFileError, loadInvariantFiles, resolveInvariantAuthTokens } from "./invariants-file.js";
8
+ import { FilingConfigError, loadFilingFileConfig, resolveFilingConfig } from "./findings-filing.js";
9
+ import { fileDraftsWithEvidence } from "./defect-evidence.js";
10
+ import { parseScreenshotsArg } from "./run-screenshots.js";
11
+ import { GitHubIssueFiler } from "./github-issue-filer.js";
12
+ import { TargetConfigError, loadTargetsFile, resolveTargetConfig } from "./target-config.js";
13
+ import { buildMissionFixtures, checkSetupRefs, checkUrlRefOrigin, invariantSetupTexts, substituteSpecSetupRefs, fixtureSetupFailedResult, withFixtureFlags, } from "./fixture-cli.js";
14
+ import { FixtureSetupError, FixtureSpecError, SETUP_REF, UnboundSetupRefError, substituteSetupRefs, } from "./mission-fixtures.js";
15
+ import { LITERAL_SECRET_WARNING, SecretArgError, resolveSecretArgs } from "./secret-args.js";
16
+ import { withEngine } from "./engine.js";
17
+ import { setKillSwitchOutput } from "./kill-signal.js";
18
+ import { EXIT_CODES } from "./exit-codes.js";
19
+ import { intArg, positiveNumberArg, nonNegativeIntArg, positiveIntArg, ratioArg } from "./cli-args.js";
20
+ import { formatMissionHuman, formatMultiRunHuman } from "./cli-output.js";
21
+ import { runExploration, runCoverageMission, runAdversarialCliMission, CLI_ADVERSARIAL_STRATEGIES, runFeatureCliMission, parseSuccessSpec, resolveExploreAllowlist, } from "./explore-api.js";
22
+ import { parseLogSourceSpecs, LogSourceSpecError } from "./log-sources.js";
23
+ import { parseLogDefectSpecs, parseLogIgnoreSpecs } from "./log-correlation.js";
24
+ import { LogSpecError } from "./log-lines.js";
25
+ import { MultiRunArgsError, resolveMultiRunPlan, wantsMultiRun } from "./multi-run.js";
26
+ import { MultiRunAbortedError, runExploreMultiRun } from "./multi-run-cli.js";
27
+ import { checkActorsAgainstSpec, resolveMissionActors } from "./mission-actors.js";
28
+ import { runUsabilityMission, UsabilityInvariantsUnsupportedError } from "./ux-api.js";
29
+ import { UxConfigError } from "./ux-config.js";
30
+ import { MinConfidenceError, QualityPolicyError, MaxFindingsPerRouteError } from "@jevitate/ux";
31
+ import { withBrowserLaunchFlags, withDemoFlags, withScreenshotsFlag, browserRunFromFlags, withEmulationFlags, emulationFromFlags, emitCommandResult, stallTimeoutMs, EXPLORE_STRATEGIES, EXPLORE_OUTCOME_HELP, GatewaySelectionError, buildExploreGateways, } from "./cli-shared.js";
32
+ import { multiWindowWarning } from "./browser-run-options.js";
33
+ /**
34
+ * Registers `jevitate explore` (every strategy: goal, coverage, exploratory, adversarial, usability, feature, multi-run).
35
+ * `buildProgram` builds a fresh program for each run of a multi-run (#141/#143).
36
+ */
37
+ export function registerExploreCommands(program, deps, buildProgram) {
38
+ withScreenshotsFlag(withEmulationFlags(withFixtureFlags(withDemoFlags(withBrowserLaunchFlags(program
39
+ .command("explore")
40
+ .description("goal-directed exploration -> a deterministic Recording (authoring/test plane)")), { recordVideo: true, overlay: true }))))
41
+ .option("--url <url>", "target URL (must be an authorized origin)")
42
+ .option("--strategy <name>", "exploration strategy: goal (default) | coverage | exploratory | adversarial | usability (UX review: ranked, cited findings)", "goal")
43
+ .option("--goal <text>", "natural-language goal / job (required for --strategy goal and usability)")
44
+ .option("--app-class <class>", "app class for UX calibration (required for --strategy usability), e.g. consumer|admin|internal")
45
+ .option("--show <labels>", "opt-in filter on the quality grade (comma list of actionable,relevant-minor,generic,wrong); others are suppressed and counted; default JEVITATE_UX_SHOW, then ~/.jevitate/config.json ux.show, then ALL grades — the grader is uncalibrated (#133), so by default every finding is shown with its grade")
46
+ .option("--min-confidence <n>", "(--strategy usability) findings below this FINDING confidence (0..1, a finding's own violation/applicability/grounding score — NOT its quality-grade confidence, a separate independent-grader number shown as finding.quality.confidence) are suppressed and counted in report.suppressed; default JEVITATE_UX_MIN_CONFIDENCE, then ~/.jevitate/config.json ux.minConfidence, then 0.3")
47
+ .option("--max-findings-per-page <n>", "(--strategy usability) cap on UX findings per route/page, highest-confidence first; the rest are counted in report.suppressed as per-page-cap, never dropped silently; default JEVITATE_UX_MAX_FINDINGS_PER_PAGE, then ~/.jevitate/config.json ux.maxFindingsPerPage, then 5")
48
+ .option("--success <spec>", [
49
+ "independent success check (repeatable; every one must hold; --strategy goal and usability). Kinds:",
50
+ "urlIncludes:<text> | visible:<d> | textIncludes:<d>|<text> (case-insensitive) | count:<d>|min=<n>,max=<n>",
51
+ "| valueEquals:<d>|<value> (a form control's value) | reloadThen:<check> (reload first: proves it persisted)",
52
+ "| visual state (#148, read and decided by code): style:<d>|<prop><op><value> (computed style of every match;",
53
+ "<prop> an allowlisted CSS property or a channel of one, e.g. alpha(background-color)>0, color=rgb(255, 0, 0); op = != > >= < <=)",
54
+ "| inViewport:<d>[|min=<ratio>] (visible fraction, default 0.5) | box:<d>|minWidth=<n>,maxWidth=<n>,minHeight=<n>,maxHeight=<n>",
55
+ "| overlaps:<d>|<d2> | noOverlap:<d>|<d2> | attr:<d>|<name>=<value> (or <name> present, !<name> absent)",
56
+ "| flashed:<d>|class=<cls> (or attr=<name>, animation)[|withinMs=<n>] (a transient state gained after the last user input)",
57
+ "| requestMade:<METHOD> <path-glob> | responseStatus:<METHOD> <path-glob>=<2xx|4xx|code>.",
58
+ "<d> is testId=..;role=..;name=..;label=..;text=..;css=.. or a CSS selector such as [data-testid=x].",
59
+ "<path-glob> must start with \"/\" (it matches the request's path, e.g. /api/profile/* or /api/**); * as METHOD matches any method.",
60
+ "e.g. --success 'requestMade:PUT /api/profile' --success 'reloadThen:valueEquals:[data-testid=last-name]|Litmus'.",
61
+ "Omit it for a find-out goal (e.g. \"find out how many contacts... report the answer\"): the run must then end",
62
+ "with the model's own `report` op, and the grounded answer (#101) is the verdict — no page/network check needed.",
63
+ ].join(" "), (v, prev) => [...prev, v], [])
64
+ .option("--success-when <when>", "when the --success page checks must hold: final (default; on the final page) | held (on the final page, or all together at any settled step — a one-time secret, a toast). reloadThen is always final")
65
+ .option("--allow-vacuous-checks", "downgrade a vacuous --success check to a warning. By default a check satisfied before the run's first action — a page check that held on the seed page and never changed " +
66
+ "(an empty result container), a requestMade/responseStatus matched only by a page-load or polling request — FAILS: it cannot verify the goal")
67
+ .option("--feature <name>", "run the capability-scoped feature-testing mission (instead of --goal/--success)")
68
+ .option("--route <glob>", "in-scope route glob (repeatable), e.g. /thread/** — for --feature it replaces the default scope (the start URL's route and everything under it); it widens --strategy adversarial/coverage/exploratory beyond the start URL's route", (v, prev) => [...prev, v], [])
69
+ .option("--scope <mode>", "--strategy coverage/exploratory: 'app' widens containment to the whole app (same as --route '/**'); default: the start URL's route plus --route globs")
70
+ .option("--allow <origin>", "authorized origin (repeatable); REPLACES the default allowlist when given (the URL's own origin is used only when --allow is omitted entirely) -- include the URL's own origin explicitly if you still need it", (v, prev) => [...prev, v], [])
71
+ .option("--secret <value|env:VAR>", "REDACTION ONLY: a secret/PII value kept out of every model call and artifact (repeatable); env:VAR reads it from the environment (preferred: a literal is visible in the process list and shell history). It is never typed into a field — to log in, bind it with --secret-field (or start from --storage-state)", (v, prev) => [...prev, v], [])
72
+ .option("--secret-field <binding>", "goal/usability strategy: '<label|testId|type|id|name>=<value>=env:<VAR>' (repeatable), e.g. 'label=Password=env:APP_PASSWORD'. When the run types into a matching field, code types $VAR itself; the model sees only «secret:VAR» and the Recording {redacted:true}", (v, prev) => [...prev, v], [])
73
+ .option("--totp <binding>", "goal/usability strategy: '<descriptor>=env:<VAR>' with $VAR a base32 TOTP seed (repeatable), e.g. 'label=Authentication code=env:APP_TOTP_SEED'. The 6-digit code is computed locally (RFC 6238) when the field is typed; the seed never reaches a model or disk", (v, prev) => [...prev, v], [])
74
+ .option("--fixture <path>", "local file the upload op attaches to a file input (goal and usability strategies); must exist")
75
+ .option("--storage-state <file>", "Playwright storageState JSON to start the session authenticated (deterministic login pre-step); must exist")
76
+ .option("--actor <name=storageState>", "multi-actor mission (#147, goal only; repeatable): the FIRST actor is the primary (the only one the model drives, " +
77
+ "from its own storageState); every other actor is an observer in its OWN fresh context that only runs the " +
78
+ "--invariants' cross-actor checks (capture + probe as:/deniedAs) — never clicks or types. Replaces --storage-state", (v, prev) => [...prev, v], [])
79
+ .option("--save-storage-state <file>", "write the context's storageState (cookies + origin storage) here when the run ends; mode 0600, contents never logged. " +
80
+ "Useful with a rotating refresh token: --storage-state's file goes stale after one authenticated run refreshes it, " +
81
+ "so point --save-storage-state at the SAME file (or a new one) to keep it usable for the next run. " +
82
+ "Written on every exit path -- a crash or a SIGTERM/SIGINT kill included (#159), not only a clean end -- but " +
83
+ "never over a good file with a session that already looks lost/logged-out; the last known-good state is used " +
84
+ "instead, or nothing is written if none was ever captured.")
85
+ .option("--max-actions <n>", "hard cap on executed actions", positiveIntArg)
86
+ .option("--max-decisions <n>", "hard cap on model decisions", positiveIntArg)
87
+ .option("--stall-timeout <seconds>", "--strategy coverage/exploratory and --feature: end the run inconclusive (stalled) when no step completes within this many seconds (default 120)", positiveNumberArg)
88
+ .option("--reply-wait-ms <ms>", "conversational pages: how long to keep waiting for a reply while the page shows no sign of working on one " +
89
+ "(goal and usability; default 60000). While a request the message started is in flight, a busy indicator shows, " +
90
+ "or the reply is still growing, the wait continues up to --reply-ceiling-ms", positiveIntArg)
91
+ .option("--reply-ceiling-ms <ms>", "conversational pages: hard ceiling on one reply wait, however busy the page stays (default 180000; never below --reply-wait-ms)", positiveIntArg)
92
+ .option("--reply-max-chars <n>", "conversational pages: cap on each generated chat message (goal and usability; default 300)", intArg({ min: 20, max: 2000 }))
93
+ .option("--job-wait-ms <ms>", "goal and usability: while the page shows an in-progress status (\"Simulating…\", aria-busy, a job \"is running\"), " +
94
+ "waits keep waiting with backoff — and a model 'blocked' is deferred — up to this budget (default: --reply-ceiling-ms, 180000)", positiveIntArg)
95
+ .option("--deny <pattern>", "a control no mission may click (repeatable): an accessible-name regex (/Archive/i or Archive) or a descriptor role=button;name=Archive. " +
96
+ "Session-ending (Sign out), destructive (Delete, Revoke, Rotate) and paid (Buy, Run simulation, Generate, Send invite) controls are refused by default", (v, prev) => [...prev, v], [])
97
+ .option("--paid <pattern>", "an app control that costs money or credits (repeatable; same syntax as --deny), e.g. /^(Analyze|Draft|Improve)\\b/i: treated like the built-in paid " +
98
+ "vocabulary — the budget guard sees it, hang replays never repeat it, and a goal that asks for it may still click it", (v, prev) => [...prev, v], [])
99
+ .option("--allow-destructive", "let missions click session-ending, destructive and paid controls (a --deny pattern still holds). A goal run already may click one its goal asks for")
100
+ .option("--allow-writes", "let a find-out goal (no --success check, ended by report) change the app. By default it is read-only: controls that start a write flow " +
101
+ "(checkout, upgrade, create, save, submit…) are refused and the write requests an action fires are blocked, unless the goal itself asks for a change")
102
+ .option("--allow-write <glob>", "a write-request path a read-only find-out goal never blocks (repeatable; ** spans segments; a glob starting with https:// matches " +
103
+ "origin + path, e.g. https://abc.supabase.co/rest/v1/**), beyond the built-in auth-refresh ones " +
104
+ "(**/refresh*, **/token*, **/oauth/**, **/auth/**/refresh*). The app's background writes outside an action always pass", (v, prev) => [...prev, v], [])
105
+ .option("--read-rpc <glob>", "a POST request that only READS (repeatable): an RPC-method glob (Estimate*, pkg.Service/Preview*) or a path glob (/api/search*). " +
106
+ "gRPC-web/Connect Get*/List*/Search*/Find*/Watch*/Stream*/Count*/Describe*/Read* methods are reads already. Reads are never guarded or reported as duplicate writes", (v, prev) => [...prev, v], [])
107
+ .option("--real", "use live Jev + OpenRouter gateways (requires keys)", false)
108
+ .option("--fake-ai", "use deterministic fake gateways (pipeline smoke only)", false)
109
+ .option("--out <dir>", "directory to write the emitted Recording")
110
+ .option("--file-issues", "file findings as issues (needs a repo: --issue-repo or ~/.jevitate/filing.json); default: drafts only")
111
+ .option("--issue-repo <owner/name>", "the system-under-test repo findings for THIS target are filed to")
112
+ .option("--hang-replays <n>", "fresh-context replays that confirm a hang (default 2; 0 = don't replay, the hang is reported unconfirmed)", nonNegativeIntArg)
113
+ .option("--hang-replay-writes", "let hang replays re-send a paid/destructive write the run sent (default: such a hang is reported inconclusive, never replayed)")
114
+ .option("--settle-ignore <pattern>", "a request URL pattern the target marks as background (never pending work; repeatable, * wildcard)", (v, prev) => [...prev, v], [])
115
+ .option("--long-poll-ms <n>", "a request pending this long on an interactive page is a long-poll (default 5000)", nonNegativeIntArg)
116
+ .option("--api-prefix <path>", "a path prefix whose requests are the app's API in the timing summary (repeatable), e.g. /api/", (v, prev) => [...prev, v], [])
117
+ .option("--ignore-no-progress <pattern>", "a route / action label / busy indicator where ui-no-progress is expected (repeatable, * wildcard)", (v, prev) => [...prev, v], [])
118
+ .option("--jevitate-repo <owner/name>", "where jevitate engine findings are filed (default matt-cochran/jevitate)")
119
+ .option("--min-control-coverage <ratio>", "adversarial: share of the target's controls (0..1) a run must exercise before 'found nothing' is clean (default 0.25); below it the run is inconclusive", ratioArg)
120
+ .option("--no-require-form-submit", "adversarial: do not require a submitted form for a clean result (default: required when the target has a form)")
121
+ .option("--invariants <file>", "app-declared invariants JSON (repeatable; goal, coverage, exploratory, adversarial, --feature): checked around every action, a violation is a defect (exit 1). Validated before any browser opens; probes are GET/HEAD on an --allow origin only", (v, prev) => [...prev, v], [])
122
+ .option("--log-source <spec>", "backend log source (repeatable; every strategy, incl. usability): file:<path> (tailed from its current end) | docker:<container> (docker logs -f --since 0s) | cmd:<command> (needs --allow-log-cmd). Read-only, operator-declared, never the model's choice. Error/warning lines are correlated to the step they landed during and attached to its transcript evidence, redacted", (v, prev) => [...prev, v], [])
123
+ .option("--allow-log-cmd", "opt-in: a --log-source cmd:<command> may run as a subprocess (operator-declared only; refused otherwise)", false)
124
+ .option("--log-defect <level|/regex/>", "backend log lines matching this (repeatable) become a server-log defect: a level (error|warn|info|debug, matched as level>=this) or a /regex/flags/ over the raw line. Its fingerprint is the normalized message (ids/numbers/uuids/timestamps stripped) plus the correlated route; verify-fix re-checks it by re-tailing the same --log-source(s)", (v, prev) => [...prev, v], [])
125
+ .option("--log-quiet-ok <spec>", "declares a --log-source spec (exact match, repeatable) as legitimately quiet: zero lines from it does not make the --log-defect oracle unhealthy (#169). Without it, a declared source that opened but delivered not one line makes an otherwise-clean run inconclusive, same as one that failed to open", (v, prev) => [...prev, v], [])
126
+ .option("--log-ignore <regex|substring>", "excludes known-noise backend log lines (repeatable, /regex/flags/ over the raw line or a plain substring) from BOTH correlation and the --log-defect oracle (#169 item 3) — e.g. a periodic background job's own expected error. Counted separately as serverLogs.ignoredLines; never makes --log-quiet-ok unnecessary, since an ignored line still proves the source is being tailed", (v, prev) => [...prev, v], [])
127
+ .option("--server-log-drain-ms <ms>", "how long to keep tailing --log-source after the run's last action, to catch async backend work that settles after the browser gave up (default 3000)", nonNegativeIntArg)
128
+ .option("--repeat <n>", "run the mission N times, one after another, each in a fresh browser context, and vote (#141): findings seen in fewer than --min-agreement runs are reported as flaky, not counted")
129
+ .option("--min-agreement <k>", "with --repeat: runs a finding (and the outcome) must recur in to count (default: a majority of N)")
130
+ .option("--persona <name=storageState>", "run the same mission once per persona (repeatable), serially, each from its own storageState, and diff them (#143): requests, statuses (a 403 vs 200 is a candidate RBAC finding), controls, outcome", (v, prev) => [...prev, v], [])
131
+ .option("--personas <file>", "personas JSON: {\"<name>\": \"<storageState>\"} or {\"personas\": [{\"name\", \"storageState\"}]}")
132
+ .option("--check-overflow", "check the horizontal-overflow hard signal (#149) even at a desktop (>=1024px) viewport — --strategy coverage/exploratory " +
133
+ "(a defect), adversarial (a defect) or usability (a signal finding). " +
134
+ "On by default whenever --viewport/--device emulates a viewport narrower than 1024px")
135
+ .option("--ignore-overflow <selector>", "a CSS selector (repeatable) whose overflow is intentional — excluded from the horizontal-overflow signal, like --ignore-no-progress", (v, prev) => [...prev, v], [])
136
+ .option("--evidence-video", "per defect: replay its minimal repro with captions + the failing step marked, record a masked clip and before/at screenshots (defects[].evidence; linked from drafts)")
137
+ .option("--json", "emit the JSON envelope (default: a human summary)")
138
+ .addHelpText("after", [
139
+ "",
140
+ "Authenticated missions:",
141
+ " --secret only REDACTS a value; it is never typed. Prefer starting logged in: save a Playwright",
142
+ " storageState once (e.g. `npx playwright codegen --save-storage=auth.json <url>`) and pass",
143
+ " --storage-state auth.json. To drive a login/signup form, bind fields to environment variables:",
144
+ " --secret-field 'label=Password=env:APP_PASSWORD' and, for MFA, --totp 'label=Code=env:APP_TOTP_SEED'.",
145
+ " See 'Authenticated missions' in the README.",
146
+ ].join("\n"))
147
+ .addHelpText("after", [
148
+ "",
149
+ "Viewport/device emulation (#149):",
150
+ " Default: Playwright's own default viewport (1280x720, desktop, no touch) — nothing narrower",
151
+ " unless --viewport or --device is given (mutually exclusive). --device validates against",
152
+ " Playwright's built-in devices registry (viewport + scale + mobile/touch + UA); an unknown",
153
+ " name is refused before any browser opens. The emulation is recorded on the Recording, so",
154
+ " verify-fix/regression replay reproduce under the SAME device by default.",
155
+ ].join("\n"))
156
+ .addHelpText("after", EXPLORE_OUTCOME_HELP)
157
+ .action(async function () {
158
+ const o = this.opts();
159
+ // #210: one output rule for every strategy — the envelope with --json, a human summary without.
160
+ const emitExplore = (envelope, exitCode, human = formatMissionHuman) => emitCommandResult(program, envelope, { json: o.json === true, command: "explore", human, ...(exitCode === undefined ? {} : { exitCode }) });
161
+ // #230: an unknown --strategy must be refused before any other required-option message — it
162
+ // would otherwise fall through to the default goal-strategy path and silently run a goal
163
+ // mission. Checked first, ahead of every other validation below.
164
+ if (o.strategy !== undefined && !EXPLORE_STRATEGIES.includes(o.strategy)) {
165
+ emitExplore(fail("E_EXPLORE_ARGS", `unknown strategy ${JSON.stringify(o.strategy)} (one of ${EXPLORE_STRATEGIES.join(", ")})`));
166
+ return;
167
+ }
168
+ // #195: a session file never lands in the repo's .jevitate/ (refused before any run, multi-runs included).
169
+ if (o.saveStorageState !== undefined) {
170
+ try {
171
+ assertSessionFileOutsideProject(o.saveStorageState, "--save-storage-state");
172
+ }
173
+ catch (err) {
174
+ if (!(err instanceof SessionFileInProjectError))
175
+ throw err;
176
+ emitExplore(fail(err.code, err.message));
177
+ return;
178
+ }
179
+ }
180
+ const strategy = o.strategy ?? "goal";
181
+ // #195: `--secret env:VAR` is resolved from the environment before anything runs (fail closed).
182
+ try {
183
+ const resolved = resolveSecretArgs(o.secret, process.env, "--secret");
184
+ if (resolved.literals > 0)
185
+ program.configureOutput().writeErr?.(LITERAL_SECRET_WARNING);
186
+ o.secret = resolved.secrets;
187
+ }
188
+ catch (err) {
189
+ if (!(err instanceof SecretArgError))
190
+ throw err;
191
+ emitExplore(fail("E_EXPLORE_ARGS", err.message));
192
+ return;
193
+ }
194
+ // #245 demo mode: resolved (and a headed run without a display refused, exit 64) before any
195
+ // browser opens — a multi-run's every run re-resolves the same flags.
196
+ let browser;
197
+ try {
198
+ browser = browserRunFromFlags(o, deps.explore?.env ?? process.env);
199
+ }
200
+ catch (err) {
201
+ emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
202
+ return;
203
+ }
204
+ // #251: an unusable --screenshots value is a usage error (64), refused before any browser opens.
205
+ let screenshots;
206
+ try {
207
+ screenshots = parseScreenshotsArg(o.screenshots);
208
+ }
209
+ catch (err) {
210
+ emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
211
+ return;
212
+ }
213
+ // #251/#250: the capture options every strategy's runner takes.
214
+ const screenshotsOpt = { ...(screenshots === undefined ? {} : { screenshots }), ...(o.evidenceVideo === true ? { evidenceVideo: true } : {}) };
215
+ if (browser?.headed === true && (wantsMultiRun(o) || o.actor.length > 1)) {
216
+ program.configureOutput().writeErr?.(multiWindowWarning(o.actor.length > 1 ? "several --actor sessions" : "--repeat/--persona"));
217
+ }
218
+ // Repeat-and-vote (#141) / persona matrix (#143): the same command, run sequentially and aggregated.
219
+ if (wantsMultiRun(o)) {
220
+ try {
221
+ const plan = resolveMultiRunPlan(o);
222
+ const result = await runExploreMultiRun({
223
+ cmd: this,
224
+ newProgram: () => buildProgram(deps),
225
+ plan,
226
+ strategy,
227
+ ...(o.out === undefined ? {} : { out: o.out }),
228
+ // #220: a killed multi-run prints ITS partial summary, by this command's own output rule.
229
+ killOutput: (partial) => (o.json === true ? `${JSON.stringify(ok(withEngine(partial)))}\n` : formatMultiRunHuman(partial)),
230
+ });
231
+ emitExplore(ok(withEngine(result)), result.exitCode, formatMultiRunHuman);
232
+ }
233
+ catch (err) {
234
+ if (err instanceof MultiRunArgsError)
235
+ emitExplore(fail(err.code, err.message));
236
+ else if (err instanceof MultiRunAbortedError)
237
+ emitExplore(fail(err.envelope.error.code, err.envelope.error.message));
238
+ else
239
+ emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
240
+ }
241
+ return;
242
+ }
243
+ // #120: a killed run prints what this command would have printed — the envelope with --json,
244
+ // else the human summary (#210) — before it exits.
245
+ setKillSwitchOutput(o.json === true ? "envelope" : "human");
246
+ const conversation = {
247
+ ...(o.replyWaitMs === undefined ? {} : { replyWaitMs: Number(o.replyWaitMs) }),
248
+ ...(o.replyCeilingMs === undefined ? {} : { replyCeilingMs: Number(o.replyCeilingMs) }),
249
+ ...(o.replyMaxChars === undefined ? {} : { replyMaxChars: Number(o.replyMaxChars) }),
250
+ ...(o.jobWaitMs === undefined ? {} : { jobWaitMs: Number(o.jobWaitMs) }),
251
+ };
252
+ if ((conversation.replyWaitMs !== undefined && !(Number.isInteger(conversation.replyWaitMs) && conversation.replyWaitMs > 0)) ||
253
+ (conversation.replyCeilingMs !== undefined &&
254
+ !(Number.isInteger(conversation.replyCeilingMs) && conversation.replyCeilingMs > 0)) ||
255
+ (conversation.replyMaxChars !== undefined &&
256
+ !(Number.isInteger(conversation.replyMaxChars) && conversation.replyMaxChars >= 20 && conversation.replyMaxChars <= 2000))) {
257
+ emitExplore(fail("E_EXPLORE_ARGS", "--reply-wait-ms and --reply-ceiling-ms must be positive integers; --reply-max-chars an integer in 20..2000"));
258
+ return;
259
+ }
260
+ if (conversation.jobWaitMs !== undefined && !(Number.isInteger(conversation.jobWaitMs) && conversation.jobWaitMs > 0)) {
261
+ emitExplore(fail("E_EXPLORE_ARGS", "--job-wait-ms must be a positive integer"));
262
+ return;
263
+ }
264
+ // #154: refused BEFORE any browser opens. 0 is valid: "don't replay" — a hang is then
265
+ // reported unconfirmed (inconclusive), never replayed and never a crash.
266
+ if (o.hangReplays !== undefined && !/^\d+$/.test(o.hangReplays.trim())) {
267
+ emitExplore(fail("E_EXPLORE_ARGS", `--hang-replays must be a non-negative integer (0 = don't replay; the hang is reported unconfirmed), got "${o.hangReplays}"`));
268
+ return;
269
+ }
270
+ try {
271
+ validateDenyPatterns(o.deny);
272
+ validateDenyPatterns(o.paid, "--paid");
273
+ }
274
+ catch (err) {
275
+ emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
276
+ return;
277
+ }
278
+ // #149: refused BEFORE any browser opens (an unknown --device, or --viewport + --device together).
279
+ let emulation;
280
+ try {
281
+ emulation = emulationFromFlags(o);
282
+ }
283
+ catch (err) {
284
+ emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
285
+ return;
286
+ }
287
+ const overflow = { checkOverflow: o.checkOverflow ?? false, ignoreSelectors: o.ignoreOverflow };
288
+ // Issue filing: drafts are always written; filing needs --file-issues (or config) AND a repo.
289
+ let filing;
290
+ if (o.url !== undefined) {
291
+ try {
292
+ filing = resolveFilingConfig(loadFilingFileConfig(deps.explore?.filingConfigPath), {
293
+ ...(o.fileIssues === undefined ? {} : { fileIssues: o.fileIssues }),
294
+ ...(o.issueRepo === undefined ? {} : { issueRepo: o.issueRepo }),
295
+ ...(o.jevitateRepo === undefined ? {} : { jevitateRepo: o.jevitateRepo }),
296
+ }, new URL(o.url).origin);
297
+ }
298
+ catch (err) {
299
+ if (err instanceof FilingConfigError) {
300
+ emitExplore(fail(err.code, err.message));
301
+ return;
302
+ }
303
+ if (!(err instanceof TypeError))
304
+ throw err;
305
+ // An unparseable --url is refused by the authorized-target guard below.
306
+ }
307
+ }
308
+ // Per-target settle/hang configuration: ~/.jevitate/targets.json by origin, plus flags.
309
+ let target;
310
+ if (o.url !== undefined) {
311
+ try {
312
+ target = resolveTargetConfig(loadTargetsFile(deps.explore?.targetsConfigPath), new URL(o.url).origin, {
313
+ settleIgnore: o.settleIgnore,
314
+ ignoreNoProgress: o.ignoreNoProgress,
315
+ apiPrefixes: o.apiPrefix,
316
+ deny: o.deny,
317
+ paid: o.paid,
318
+ readRpc: o.readRpc,
319
+ ...(o.allowDestructive === true ? { allowDestructive: true } : {}),
320
+ ...(o.allowWrites === true ? { allowWrites: true } : {}),
321
+ allowWrite: o.allowWrite,
322
+ ...(o.hangReplayWrites === true ? { hangReplayWrites: true } : {}),
323
+ ...(o.longPollMs === undefined ? {} : { longPollMs: Number(o.longPollMs) }),
324
+ });
325
+ }
326
+ catch (err) {
327
+ if (err instanceof TargetConfigError) {
328
+ emitExplore(fail(err.code, err.message));
329
+ return;
330
+ }
331
+ if (!(err instanceof TypeError))
332
+ throw err;
333
+ }
334
+ }
335
+ const issueFiler = deps.explore?.issueFiler ??
336
+ (() => new GitHubIssueFiler({ store: envCredentialStore(process.env, loadLocalCredentials()) }));
337
+ // #250 --evidence-video: the runner attaches each defect's captioned repro clip + key
338
+ // screenshots; drafts are filed only AFTER their media is linked (the run writes drafts only).
339
+ const evidenceOn = o.evidenceVideo === true;
340
+ const runFiling = filing === undefined ? undefined : evidenceOn ? { ...filing, enabled: false } : filing;
341
+ const withEvidence = async (result) => {
342
+ if (!evidenceOn)
343
+ return result;
344
+ let out = result;
345
+ if (filing?.enabled === true)
346
+ out = await fileDraftsWithEvidence(out, filing, issueFiler, new Date().toISOString());
347
+ return out;
348
+ };
349
+ // `--fixture` feeds the upload op, which only the explore loop (goal and
350
+ // usability strategies) can issue. Refuse it elsewhere rather than
351
+ // silently ignoring a file the user expected to be uploaded.
352
+ if (o.fixture !== undefined && (o.feature !== undefined || (strategy !== "goal" && strategy !== "usability"))) {
353
+ emitExplore(fail("E_EXPLORE_ARGS", "--fixture is supported only with --strategy goal or usability"));
354
+ return;
355
+ }
356
+ // #225: success checks judge a goal / a usability job — every other strategy (and --feature) would
357
+ // silently ignore them, so they are refused up front, never dropped.
358
+ if ((o.success.length > 0 || o.successWhen !== undefined || o.allowVacuousChecks === true) &&
359
+ (o.feature !== undefined || (strategy !== "goal" && strategy !== "usability"))) {
360
+ emitExplore(fail("E_EXPLORE_ARGS", `--success, --success-when and --allow-vacuous-checks are supported only with --strategy goal or usability (not ${o.feature !== undefined ? "--feature" : `--strategy ${strategy}`})`));
361
+ return;
362
+ }
363
+ if (o.storageState !== undefined && !existsSync(o.storageState)) {
364
+ emitExplore(fail("E_EXPLORE_ARGS", `storage state not found: ${o.storageState}`));
365
+ return;
366
+ }
367
+ // Multi-actor missions (#147): the first --actor is the primary, the rest are observers.
368
+ let actors;
369
+ try {
370
+ actors = resolveMissionActors(o.actor);
371
+ }
372
+ catch (err) {
373
+ if (!(err instanceof MultiRunArgsError))
374
+ throw err;
375
+ emitExplore(fail(err.code, err.message));
376
+ return;
377
+ }
378
+ if (actors !== null) {
379
+ if (o.feature !== undefined || strategy !== "goal") {
380
+ emitExplore(fail("E_EXPLORE_ARGS", "--actor is supported only with --strategy goal"));
381
+ return;
382
+ }
383
+ if (o.storageState !== undefined) {
384
+ emitExplore(fail("E_EXPLORE_ARGS", "--storage-state cannot be combined with --actor (the first --actor is the primary's session)"));
385
+ return;
386
+ }
387
+ }
388
+ // The primary's session: its --actor state, else --storage-state.
389
+ const primaryStorageState = actors?.primary.storageState ?? o.storageState;
390
+ // App-declared invariants (#86): validated (schema, observables, probe origins) BEFORE any browser.
391
+ let invariants;
392
+ // #135: authFrom.secret refs (env:VAR), resolved from the environment HERE — the one place this
393
+ // package reads process.env for invariants — never inside @jevitate/explore or @jevitate/recording.
394
+ let invariantAuthTokens;
395
+ if (o.invariants.length > 0) {
396
+ if (o.url !== undefined) {
397
+ try {
398
+ const loaded = loadInvariantFiles(o.invariants, {
399
+ allowlist: resolveExploreAllowlist(o.url, o.allow),
400
+ baseUrl: o.url,
401
+ observers: actors?.observers.map((a) => a.name) ?? [],
402
+ });
403
+ invariants = loaded;
404
+ invariantAuthTokens = loaded === undefined ? undefined : resolveInvariantAuthTokens(loaded, process.env);
405
+ checkActorsAgainstSpec(actors, loaded);
406
+ }
407
+ catch (err) {
408
+ if (err instanceof MultiRunArgsError) {
409
+ emitExplore(fail(err.code, err.message));
410
+ return;
411
+ }
412
+ if (!(err instanceof InvariantsFileError))
413
+ throw err;
414
+ emitExplore(fail(err.code, err.message));
415
+ return;
416
+ }
417
+ }
418
+ // #147: captures and cross-actor checks run in the goal loop only — never silently skipped elsewhere.
419
+ if (invariants?.capture !== undefined && (o.feature !== undefined || strategy !== "goal")) {
420
+ emitExplore(fail("E_EXPLORE_ARGS", "invariants with capture (cross-actor checks) are supported only with --strategy goal"));
421
+ return;
422
+ }
423
+ }
424
+ const withInvariants = {
425
+ ...(invariants === undefined ? {} : { invariants }),
426
+ ...(invariantAuthTokens === undefined || invariantAuthTokens.size === 0 ? {} : { invariantAuthTokens }),
427
+ };
428
+ // Backend log sources (#142): validated (spec shape, --allow-log-cmd gate, matcher regexes)
429
+ // BEFORE any browser opens — the same fail-closed discipline as --invariants above. Supported
430
+ // on every strategy, INCLUDING usability (#142 follow-up): lines attach to usability steps the
431
+ // same way, though a UX run's own outcome stays advisory (a server-log defect is still reported,
432
+ // never gates the exit code — the same rule as every other UX finding).
433
+ let serverLog;
434
+ if (o.logSource.length > 0 || o.logDefect.length > 0) {
435
+ try {
436
+ const sources = parseLogSourceSpecs(o.logSource, o.allowLogCmd ?? false);
437
+ const logDefect = parseLogDefectSpecs(o.logDefect);
438
+ const logIgnore = parseLogIgnoreSpecs(o.logIgnore);
439
+ serverLog = {
440
+ sources,
441
+ logDefect,
442
+ allowLogCmd: o.allowLogCmd ?? false,
443
+ quietOk: o.logQuietOk,
444
+ logIgnore,
445
+ ...(o.serverLogDrainMs === undefined ? {} : { drainMs: Number(o.serverLogDrainMs) }),
446
+ };
447
+ }
448
+ catch (err) {
449
+ if (err instanceof LogSourceSpecError || err instanceof LogSpecError) {
450
+ emitExplore(fail(err.code, err.message));
451
+ return;
452
+ }
453
+ throw err;
454
+ }
455
+ }
456
+ const withServerLog = serverLog === undefined ? {} : { serverLog };
457
+ // Secret field bindings (#72): resolved from the environment here, typed by code in the goal loop.
458
+ let secretFields = [];
459
+ if (o.secretField.length > 0 || o.totp.length > 0) {
460
+ if (o.feature !== undefined || (strategy !== "goal" && strategy !== "usability")) {
461
+ emitExplore(fail("E_EXPLORE_ARGS", "--secret-field and --totp are supported only with --strategy goal or usability"));
462
+ return;
463
+ }
464
+ try {
465
+ secretFields = [
466
+ ...o.secretField.map((s) => parseSecretField(s, "value", process.env)),
467
+ ...o.totp.map((s) => parseSecretField(s, "totp", process.env)),
468
+ ];
469
+ }
470
+ catch (err) {
471
+ if (!(err instanceof SecretFieldSpecError))
472
+ throw err;
473
+ emitExplore(fail(err.code, err.message));
474
+ return;
475
+ }
476
+ }
477
+ // Mission fixtures (#140/#144) run around the goal loop and its replays only.
478
+ const fixtureFlagsGiven = o.fixtures !== undefined || o.before !== undefined || o.after !== undefined;
479
+ if (fixtureFlagsGiven && (o.feature !== undefined || strategy !== "goal")) {
480
+ emitExplore(fail("E_EXPLORE_ARGS", "--fixtures, --before and --after are supported only with --strategy goal"));
481
+ return;
482
+ }
483
+ // Additive coverage/exploratory strategy: proof-by-induction state coverage.
484
+ // It takes no goal/success (the frontier itself is the objective), so it is
485
+ // a distinct, goal-free path that leaves the goal strategy below unchanged.
486
+ if (strategy === "coverage" || strategy === "exploratory") {
487
+ if (!o.url) {
488
+ emitExplore(fail("E_EXPLORE_ARGS", "--url is required"));
489
+ return;
490
+ }
491
+ if (o.scope !== undefined && o.scope !== "app") {
492
+ emitExplore(fail("E_EXPLORE_ARGS", `--scope must be "app" (got ${JSON.stringify(o.scope)})`));
493
+ return;
494
+ }
495
+ const covAllowlist = resolveExploreAllowlist(o.url, o.allow);
496
+ const covBounds = {};
497
+ if (o.maxActions !== undefined)
498
+ covBounds.maxActions = Number(o.maxActions);
499
+ if (o.maxDecisions !== undefined)
500
+ covBounds.maxDecisions = Number(o.maxDecisions);
501
+ // Scope containment (#89, reusing #64's model): the start URL's route plus --route globs;
502
+ // --scope app (or --route '/**') widens it to the whole app.
503
+ const covRouteGlobs = [...o.route, ...(o.scope === "app" ? ["/**"] : [])];
504
+ const covStall = stallTimeoutMs(o.stallTimeout);
505
+ if (covStall === null) {
506
+ emitExplore(fail("E_EXPLORE_ARGS", `--stall-timeout must be a positive number of seconds (got ${JSON.stringify(o.stallTimeout)})`));
507
+ return;
508
+ }
509
+ let covJudge;
510
+ let covGen;
511
+ let covUsage;
512
+ try {
513
+ ({ judge: covJudge, gen: covGen, usage: covUsage } = await buildExploreGateways(deps, {
514
+ real: o.real ?? false,
515
+ fakeAi: o.fakeAi ?? false,
516
+ }));
517
+ }
518
+ catch (err) {
519
+ if (err instanceof MissingCredentialError || err instanceof GatewaySelectionError) {
520
+ emitExplore(fail("E_AI_SETUP_REQUIRED", err.message));
521
+ }
522
+ else {
523
+ emitExplore(fail("E_EXPLORE_SETUP", String(err instanceof Error ? err.message : err)));
524
+ }
525
+ return;
526
+ }
527
+ try {
528
+ const result = await runCoverageMission({
529
+ ...(target === undefined ? {} : { target }),
530
+ url: o.url,
531
+ allowlist: covAllowlist,
532
+ judge: covJudge,
533
+ gen: covGen,
534
+ usage: covUsage,
535
+ bounds: Object.keys(covBounds).length > 0 ? covBounds : undefined,
536
+ ...(covRouteGlobs.length > 0 ? { routeGlobs: covRouteGlobs } : {}),
537
+ strategy,
538
+ ...(covStall === undefined ? {} : { stallTimeoutMs: covStall }),
539
+ outDir: o.out,
540
+ browserPortFactory: deps.explore?.browserPortFactory,
541
+ browser,
542
+ ...screenshotsOpt,
543
+ ...(emulation === undefined ? {} : { emulation }),
544
+ overflow,
545
+ ...(o.storageState !== undefined ? { storageState: o.storageState } : {}),
546
+ ...(o.saveStorageState !== undefined ? { saveStorageState: o.saveStorageState } : {}),
547
+ ...withInvariants,
548
+ ...withServerLog,
549
+ });
550
+ // Typed verdict → exit code (0 clean · 1 defects · 2 crashed; see exit-codes.ts).
551
+ emitExplore(ok(await withEvidence(result)), result.exitCode);
552
+ }
553
+ catch (err) {
554
+ if (err instanceof UnauthorizedExploreTargetError) {
555
+ emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
556
+ }
557
+ else if (err instanceof ScopeUnderivableError) {
558
+ // #224: no default route scope from --url — a usage error (64), refused before any browser.
559
+ emitExplore(fail("E_EXPLORE_ARGS", err.message));
560
+ }
561
+ else {
562
+ emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
563
+ }
564
+ }
565
+ return;
566
+ }
567
+ // Additive adversarial strategy: a bounded "try to break it" run whose
568
+ // stop decision comes from a trusted hard-signal oracle (never Jev's
569
+ // Noul). Requires only --url; --goal/--success are goal-strategy inputs.
570
+ if (strategy === "adversarial") {
571
+ if (!o.url) {
572
+ emitExplore(fail("E_EXPLORE_ARGS", "--url is required for --strategy adversarial"));
573
+ return;
574
+ }
575
+ const advAllowlist = resolveExploreAllowlist(o.url, o.allow);
576
+ let advJudge;
577
+ let advGen;
578
+ let advUsage;
579
+ try {
580
+ ({ judge: advJudge, gen: advGen, usage: advUsage } = await buildExploreGateways(deps, {
581
+ real: o.real ?? false,
582
+ fakeAi: o.fakeAi ?? false,
583
+ }));
584
+ }
585
+ catch (err) {
586
+ if (err instanceof MissingCredentialError || err instanceof GatewaySelectionError) {
587
+ emitExplore(fail("E_AI_SETUP_REQUIRED", err.message));
588
+ }
589
+ else {
590
+ emitExplore(fail("E_EXPLORE_SETUP", String(err instanceof Error ? err.message : err)));
591
+ }
592
+ return;
593
+ }
594
+ let coverageThresholds;
595
+ try {
596
+ coverageThresholds = resolveCoverageThresholds({
597
+ ...(o.minControlCoverage === undefined ? {} : { minControlRatio: Number(o.minControlCoverage) }),
598
+ requireFormSubmit: o.requireFormSubmit,
599
+ });
600
+ }
601
+ catch (err) {
602
+ emitExplore(fail("E_EXPLORE_ARGS", String(err instanceof Error ? err.message : err)));
603
+ return;
604
+ }
605
+ const advBounds = {};
606
+ if (o.maxActions !== undefined)
607
+ advBounds.maxActions = Number(o.maxActions);
608
+ if (o.maxDecisions !== undefined)
609
+ advBounds.maxDecisions = Number(o.maxDecisions);
610
+ try {
611
+ const result = await runAdversarialCliMission({
612
+ ...(target === undefined ? {} : { target }),
613
+ seedUrl: o.url,
614
+ allowlist: advAllowlist,
615
+ usage: advUsage,
616
+ ...(o.route.length > 0 ? { routeGlobs: o.route } : {}),
617
+ coverageThresholds,
618
+ bounds: Object.keys(advBounds).length > 0 ? advBounds : undefined,
619
+ secrets: o.secret.length > 0 ? o.secret : undefined,
620
+ ...(runFiling === undefined ? {} : { filing: runFiling }),
621
+ issueFiler,
622
+ ...(o.hangReplays === undefined ? {} : { hangReplays: Number(o.hangReplays) }),
623
+ strategies: CLI_ADVERSARIAL_STRATEGIES,
624
+ judgment: advJudge,
625
+ generation: advGen,
626
+ browserPortFactory: deps.explore?.browserPortFactory,
627
+ browser,
628
+ ...screenshotsOpt,
629
+ ...(emulation === undefined ? {} : { emulation }),
630
+ overflow,
631
+ outDir: o.out,
632
+ ...(o.storageState !== undefined ? { storageState: o.storageState } : {}),
633
+ ...(o.saveStorageState !== undefined ? { saveStorageState: o.saveStorageState } : {}),
634
+ ...withInvariants,
635
+ ...withServerLog,
636
+ });
637
+ // The typed verdict gates CI: 0 clean · 1 defects found (a failing check) · 2 the run
638
+ // itself broke (inconclusive/crashed) — see exit-codes.ts.
639
+ emitExplore(ok(await withEvidence(result)), result.exitCode);
640
+ }
641
+ catch (err) {
642
+ if (err instanceof UnauthorizedExploreTargetError) {
643
+ emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
644
+ }
645
+ else if (err instanceof ScopeUnderivableError) {
646
+ // #224: no default route scope from --url — a usage error (64), refused before any browser.
647
+ emitExplore(fail("E_EXPLORE_ARGS", err.message));
648
+ }
649
+ else {
650
+ emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
651
+ }
652
+ }
653
+ return;
654
+ }
655
+ // Additive: `--strategy usability` (issue #30) — a UX review. Reuses the
656
+ // explore loop (goal = the job) and analyzes each observed screen against
657
+ // the cited @jevitate/ux rubric. Findings are ADVISORY: a UX finding never
658
+ // gates the run (no non-zero exit).
659
+ if (strategy === "usability") {
660
+ if (!o.url || !o.goal) {
661
+ emitExplore(fail("E_EXPLORE_ARGS", "--url and --goal (the job) are required for --strategy usability"));
662
+ return;
663
+ }
664
+ if (!o.appClass) {
665
+ emitExplore(fail("E_UX_ARGS", "--app-class is required for --strategy usability"));
666
+ return;
667
+ }
668
+ // #225: --success is never ignored — an independent completion check on the job, parsed and
669
+ // validated exactly as for --strategy goal (same kinds, same --success-when / vacuous rules).
670
+ let uxSuccessChecks;
671
+ try {
672
+ uxSuccessChecks = o.success.map(parseSuccessSpec);
673
+ }
674
+ catch (err) {
675
+ emitExplore(fail("E_EXPLORE_ASSERTION", String(err instanceof Error ? err.message : err)));
676
+ return;
677
+ }
678
+ if (o.successWhen !== undefined && o.successWhen !== "held" && o.successWhen !== "final") {
679
+ emitExplore(fail("E_EXPLORE_ARGS", `--success-when must be "held" or "final", got ${JSON.stringify(o.successWhen)}`));
680
+ return;
681
+ }
682
+ if (uxSuccessChecks.length === 0 && (o.successWhen !== undefined || o.allowVacuousChecks === true)) {
683
+ emitExplore(fail("E_EXPLORE_ARGS", "--success-when and --allow-vacuous-checks need at least one --success check"));
684
+ return;
685
+ }
686
+ const uxAllowlist = resolveExploreAllowlist(o.url, o.allow);
687
+ const uxBounds = {};
688
+ if (o.maxActions !== undefined)
689
+ uxBounds.maxActions = Number(o.maxActions);
690
+ if (o.maxDecisions !== undefined)
691
+ uxBounds.maxDecisions = Number(o.maxDecisions);
692
+ let uxJudge;
693
+ let uxGen;
694
+ let uxUsage;
695
+ try {
696
+ ({ judge: uxJudge, gen: uxGen, usage: uxUsage } = await buildExploreGateways(deps, {
697
+ real: o.real ?? false,
698
+ fakeAi: o.fakeAi ?? false,
699
+ }));
700
+ }
701
+ catch (err) {
702
+ if (err instanceof MissingCredentialError || err instanceof GatewaySelectionError) {
703
+ emitExplore(fail("E_AI_SETUP_REQUIRED", err.message));
704
+ }
705
+ else {
706
+ emitExplore(fail("E_EXPLORE_SETUP", String(err instanceof Error ? err.message : err)));
707
+ }
708
+ return;
709
+ }
710
+ try {
711
+ const result = await runUsabilityMission({
712
+ ...(target === undefined ? {} : { target }),
713
+ url: o.url,
714
+ job: o.goal,
715
+ allowlist: uxAllowlist,
716
+ appContext: { appClass: o.appClass, job: o.goal },
717
+ judge: uxJudge,
718
+ gen: uxGen,
719
+ usage: uxUsage,
720
+ ...(o.minConfidence !== undefined ? { minConfidence: o.minConfidence } : {}),
721
+ ...(o.show !== undefined ? { show: o.show } : {}),
722
+ ...(o.maxFindingsPerPage !== undefined ? { maxFindingsPerRoute: o.maxFindingsPerPage } : {}),
723
+ bounds: Object.keys(uxBounds).length > 0 ? uxBounds : undefined,
724
+ conversation,
725
+ secrets: o.secret.length > 0 ? o.secret : undefined,
726
+ ...(secretFields.length > 0 ? { secretFields } : {}),
727
+ fixture: o.fixture,
728
+ outDir: o.out,
729
+ browserPortFactory: deps.explore?.browserPortFactory,
730
+ browser,
731
+ ...screenshotsOpt,
732
+ ...(emulation === undefined ? {} : { emulation }),
733
+ overflow,
734
+ ...(o.storageState !== undefined ? { storageState: o.storageState } : {}),
735
+ ...(o.saveStorageState !== undefined ? { saveStorageState: o.saveStorageState } : {}),
736
+ ...withServerLog,
737
+ ...withInvariants,
738
+ ...(uxSuccessChecks.length === 0 ? {} : { successChecks: uxSuccessChecks }),
739
+ ...(o.successWhen === "held" || o.successWhen === "final" ? { successWhen: o.successWhen } : {}),
740
+ ...(o.allowVacuousChecks === true ? { allowVacuousChecks: true } : {}),
741
+ });
742
+ // UX findings are advisory (0); a failed --success check (#225) is 1, as on a goal run; a
743
+ // broken run or an unavailable analysis is 2.
744
+ emitExplore(ok(await withEvidence(result)), result.exitCode);
745
+ }
746
+ catch (err) {
747
+ if (err instanceof UnauthorizedExploreTargetError) {
748
+ emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
749
+ }
750
+ else if (err instanceof ScopeUnderivableError) {
751
+ // #224: no default route scope from --url — a usage error (64), refused before any browser.
752
+ emitExplore(fail("E_EXPLORE_ARGS", err.message));
753
+ }
754
+ else if (err instanceof FixtureNotFoundError) {
755
+ emitExplore(fail("E_EXPLORE_FIXTURE", err.message));
756
+ }
757
+ else if (err instanceof MinConfidenceError || err instanceof QualityPolicyError || err instanceof MaxFindingsPerRouteError || err instanceof UxConfigError) {
758
+ emitExplore(fail("E_UX_ARGS", err.message));
759
+ }
760
+ else if (err instanceof UsabilityInvariantsUnsupportedError) {
761
+ emitExplore(fail("E_EXPLORE_ARGS", err.message));
762
+ }
763
+ else {
764
+ emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
765
+ }
766
+ }
767
+ return;
768
+ }
769
+ // Additive: `--feature <name>` runs the capability-scoped feature-testing
770
+ // mission (ticket #2 / site #11). It is model-free, so it needs neither
771
+ // --goal/--success nor a gateway selection; the goal-based path below is
772
+ // untouched when --feature is absent.
773
+ if (o.feature) {
774
+ if (!o.url) {
775
+ emitExplore(fail("E_EXPLORE_ARGS", "--url is required with --feature"));
776
+ return;
777
+ }
778
+ const featAllowlist = resolveExploreAllowlist(o.url, o.allow);
779
+ const featBounds = {};
780
+ if (o.maxActions !== undefined)
781
+ featBounds.maxActions = Number(o.maxActions);
782
+ if (o.maxDecisions !== undefined)
783
+ featBounds.maxDecisions = Number(o.maxDecisions);
784
+ const featStall = stallTimeoutMs(o.stallTimeout);
785
+ if (featStall === null) {
786
+ emitExplore(fail("E_EXPLORE_ARGS", `--stall-timeout must be a positive number of seconds (got ${JSON.stringify(o.stallTimeout)})`));
787
+ return;
788
+ }
789
+ try {
790
+ const result = await runFeatureCliMission({
791
+ ...(featStall === undefined ? {} : { stallTimeoutMs: featStall }),
792
+ seedUrl: o.url,
793
+ allowlist: featAllowlist,
794
+ capability: o.feature,
795
+ routeGlobs: o.route ?? [],
796
+ bounds: Object.keys(featBounds).length > 0 ? featBounds : undefined,
797
+ outDir: o.out,
798
+ browserPortFactory: deps.explore?.browserPortFactory,
799
+ browser,
800
+ ...screenshotsOpt,
801
+ ...(emulation === undefined ? {} : { emulation }),
802
+ ...(o.storageState !== undefined ? { storageState: o.storageState } : {}),
803
+ ...(o.saveStorageState !== undefined ? { saveStorageState: o.saveStorageState } : {}),
804
+ ...withInvariants,
805
+ ...withServerLog,
806
+ ...(target?.safety === undefined ? {} : { safety: target.safety }),
807
+ });
808
+ emitExplore(ok(await withEvidence(result)), result.exitCode);
809
+ }
810
+ catch (err) {
811
+ if (err instanceof UnauthorizedExploreTargetError) {
812
+ emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
813
+ }
814
+ else if (err instanceof ScopeUnderivableError) {
815
+ // #224: no default route scope from --url — a usage error (64), refused before any browser.
816
+ emitExplore(fail("E_EXPLORE_ARGS", err.message));
817
+ }
818
+ else {
819
+ emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
820
+ }
821
+ }
822
+ return;
823
+ }
824
+ // --success may be omitted for a find-out goal (#130d): the run is then verified by a grounded
825
+ // `report` answer (#101) instead of an independent page/network check.
826
+ if (!o.url || !o.goal) {
827
+ emitExplore(fail("E_EXPLORE_ARGS", "--url and --goal are required"));
828
+ return;
829
+ }
830
+ let successChecks;
831
+ try {
832
+ successChecks = o.success.map(parseSuccessSpec);
833
+ }
834
+ catch (err) {
835
+ emitExplore(fail("E_EXPLORE_ASSERTION", String(err instanceof Error ? err.message : err)));
836
+ return;
837
+ }
838
+ if (o.successWhen !== undefined && o.successWhen !== "held" && o.successWhen !== "final") {
839
+ emitExplore(fail("E_EXPLORE_ARGS", `--success-when must be "held" or "final", got ${JSON.stringify(o.successWhen)}`));
840
+ return;
841
+ }
842
+ const successWhen = o.successWhen === "held" || o.successWhen === "final" ? o.successWhen : undefined;
843
+ const allowlist = resolveExploreAllowlist(o.url, o.allow);
844
+ // Fixtures (#140/#144): the spec and every ${setup.x} reference are validated here, before any
845
+ // browser or request; the setup itself runs just before the mission (below).
846
+ let fx;
847
+ try {
848
+ checkUrlRefOrigin(o.url);
849
+ fx = buildMissionFixtures(o, {
850
+ allowlist,
851
+ baseUrl: o.url.replace(SETUP_REF, "0"),
852
+ ...(primaryStorageState === undefined ? {} : { storageState: primaryStorageState }),
853
+ secretFields,
854
+ secrets: o.secret,
855
+ ...(target?.fixtures === undefined ? {} : { targetFixtures: target.fixtures }),
856
+ });
857
+ checkSetupRefs({ "--url": o.url, "--goal": o.goal, "--success": o.success, ...invariantSetupTexts(invariants) }, fx);
858
+ }
859
+ catch (err) {
860
+ if (!(err instanceof FixtureSpecError || err instanceof UnboundSetupRefError))
861
+ throw err;
862
+ emitExplore(fail(err.code, err.message));
863
+ return;
864
+ }
865
+ const bounds = {};
866
+ if (o.maxActions !== undefined)
867
+ bounds.maxActions = Number(o.maxActions);
868
+ if (o.maxDecisions !== undefined)
869
+ bounds.maxDecisions = Number(o.maxDecisions);
870
+ let judge;
871
+ let gen;
872
+ let usage;
873
+ try {
874
+ ({ judge, gen, usage } = await buildExploreGateways(deps, { real: o.real ?? false, fakeAi: o.fakeAi ?? false }));
875
+ }
876
+ catch (err) {
877
+ if (err instanceof MissingCredentialError || err instanceof GatewaySelectionError) {
878
+ emitExplore(fail("E_AI_SETUP_REQUIRED", err.message));
879
+ }
880
+ else {
881
+ emitExplore(fail("E_EXPLORE_SETUP", String(err instanceof Error ? err.message : err)));
882
+ }
883
+ return;
884
+ }
885
+ let url = o.url;
886
+ let goal = o.goal;
887
+ let runInvariants = withInvariants;
888
+ if (fx !== undefined) {
889
+ // Never run the mission on unknown state: a failed setup ends the run inconclusive (a
890
+ // configuration error), after restoring whatever the partial setup created.
891
+ try {
892
+ await fx.setup();
893
+ const b = fx.bindings();
894
+ url = substituteSetupRefs(o.url, b, { where: "--url" });
895
+ goal = substituteSetupRefs(o.goal, b, { where: "--goal" });
896
+ successChecks = o.success.map((spec) => parseSuccessSpec(substituteSetupRefs(spec, b, { where: "--success" })));
897
+ // #187: ${setup.x} in the invariants (probe paths, deniedAs.open, capture routes), origin-fixed.
898
+ if (invariants !== undefined)
899
+ runInvariants = { ...withInvariants, invariants: substituteSpecSetupRefs(invariants, b, url) };
900
+ }
901
+ catch (err) {
902
+ if (!(err instanceof FixtureSetupError || err instanceof UnboundSetupRefError)) {
903
+ await fx.restore();
904
+ throw err;
905
+ }
906
+ await fx.restore();
907
+ emitExplore(ok(withEngine(fixtureSetupFailedResult(err, fx))), EXIT_CODES.inconclusive);
908
+ return;
909
+ }
910
+ }
911
+ try {
912
+ const result = await runExploration({
913
+ ...(target === undefined ? {} : { target }),
914
+ url,
915
+ goal,
916
+ successChecks,
917
+ ...(successWhen === undefined ? {} : { successWhen }),
918
+ ...(o.allowVacuousChecks === true ? { allowVacuousChecks: true } : {}),
919
+ allowlist,
920
+ judge,
921
+ gen,
922
+ usage,
923
+ bounds: Object.keys(bounds).length > 0 ? bounds : undefined,
924
+ secrets: o.secret.length > 0 ? o.secret : undefined,
925
+ ...(secretFields.length > 0 ? { secretFields } : {}),
926
+ fixture: o.fixture,
927
+ outDir: o.out,
928
+ browserPortFactory: deps.explore?.browserPortFactory,
929
+ browser,
930
+ ...screenshotsOpt,
931
+ ...(emulation === undefined ? {} : { emulation }),
932
+ ...(primaryStorageState !== undefined ? { storageState: primaryStorageState } : {}),
933
+ ...(o.saveStorageState !== undefined ? { saveStorageState: o.saveStorageState } : {}),
934
+ ...(actors === null ? {} : { actors }),
935
+ ...(runFiling === undefined ? {} : { filing: runFiling }),
936
+ issueFiler,
937
+ ...(o.hangReplays === undefined ? {} : { hangReplays: Number(o.hangReplays) }),
938
+ conversation,
939
+ ...runInvariants,
940
+ ...withServerLog,
941
+ ...(fx === undefined ? {} : { fixtures: fx }),
942
+ });
943
+ // 0 succeeded · 1 assertion not met · 2 the run broke (inconclusive/crashed).
944
+ emitExplore(ok(await withEvidence(result)), result.exitCode);
945
+ }
946
+ catch (err) {
947
+ if (err instanceof UnauthorizedExploreTargetError) {
948
+ emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
949
+ }
950
+ else if (err instanceof FixtureNotFoundError) {
951
+ emitExplore(fail("E_EXPLORE_FIXTURE", err.message));
952
+ }
953
+ else {
954
+ emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
955
+ }
956
+ }
957
+ finally {
958
+ // Every exit path restores the fixture state (a no-op when the mission already did).
959
+ await fx?.restore();
960
+ }
961
+ });
962
+ }
963
+ //# sourceMappingURL=explore-cli.js.map