@bugmole/cli 0.4.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 (308) hide show
  1. package/.bugmole.env.example +20 -0
  2. package/LICENSE +7 -0
  3. package/README.md +293 -0
  4. package/TESTING.md +117 -0
  5. package/bugmole.config.yaml +39 -0
  6. package/package.json +85 -0
  7. package/scripts/billing/paypal-setup.mjs +121 -0
  8. package/scripts/bugmole-continue.ts +318 -0
  9. package/scripts/bugmole-init.ts +188 -0
  10. package/scripts/bugmole.cjs +17 -0
  11. package/scripts/bugmole.test.ts +344 -0
  12. package/scripts/bugmole.ts +657 -0
  13. package/scripts/ensure-maestro.cjs +79 -0
  14. package/scripts/ios-tunnel-keeper.sh +45 -0
  15. package/scripts/ios-wda-keeper.sh +66 -0
  16. package/scripts/sync-plan-catalog.d.mts +3 -0
  17. package/scripts/sync-plan-catalog.mjs +16 -0
  18. package/scripts/ui-parity-diff.py +65 -0
  19. package/scripts/ui-parity-requirements.txt +1 -0
  20. package/scripts/verify-manage-to-plans.mts +194 -0
  21. package/spec/app-ui-audit.schema.json +176 -0
  22. package/spec/blockers.yaml +79 -0
  23. package/spec/bugs.index.json +42 -0
  24. package/spec/design-dna.schema.json +38 -0
  25. package/spec/domain_rules.yaml +24 -0
  26. package/spec/flows/flow_manage-to-plans-64c3c61b.yaml +8 -0
  27. package/spec/flows/flow_screen-to-evidence-6c3ab309.yaml +7 -0
  28. package/spec/journey_graph.yaml +126 -0
  29. package/spec/journeys.graph.json +2618 -0
  30. package/spec/plans/dashboard-smoke.flow.yaml +20 -0
  31. package/spec/plans/example.flow.yaml +99 -0
  32. package/spec/plans/flow_api-keys-to-logout-7396cd5d.flow.yaml +33 -0
  33. package/spec/plans/flow_api-keys-to-screen-157e10a6.flow.yaml +33 -0
  34. package/spec/plans/flow_manage-to-audits-176c1eb8.flow.yaml +33 -0
  35. package/spec/plans/flow_manage-to-blockers-633282c8.flow.yaml +112 -0
  36. package/spec/plans/flow_manage-to-devices-ad57e202.flow.yaml +33 -0
  37. package/spec/plans/flow_manage-to-logout-86c54656.flow.yaml +33 -0
  38. package/spec/plans/flow_manage-to-manage-resource-action-6325f99a.flow.yaml +185 -0
  39. package/spec/plans/flow_manage-to-parity-issues-01fa2579.flow.yaml +34 -0
  40. package/spec/plans/flow_manage-to-plans-64c3c61b.flow.yaml +123 -0
  41. package/spec/plans/flow_manage-to-screen-6857f915.flow.yaml +106 -0
  42. package/spec/plans/flow_manage-to-tasks-c5791c95.flow.yaml +33 -0
  43. package/spec/plans/flow_profile-to-screen-6045aa3d.flow.yaml +33 -0
  44. package/spec/plans/flow_register-to-api-auth-login-460a9cc8.flow.yaml +34 -0
  45. package/spec/plans/flow_screen-to-audits-07666356.flow.yaml +33 -0
  46. package/spec/plans/flow_screen-to-blockers-091d258b.flow.yaml +33 -0
  47. package/spec/plans/flow_screen-to-devices-cad72f75.flow.yaml +33 -0
  48. package/spec/plans/flow_screen-to-evidence-6c3ab309.flow.yaml +126 -0
  49. package/spec/plans/flow_screen-to-parity-issues-2218e3ad.flow.yaml +34 -0
  50. package/spec/plans/flow_screen-to-plans-8232a94f.flow.yaml +33 -0
  51. package/spec/plans/flow_screen-to-tasks-540671d1.flow.yaml +33 -0
  52. package/spec/plans/login.flow.yaml +22 -0
  53. package/spec/plans/owner-operations.flow.yaml +20 -0
  54. package/spec/project_config.yaml +55 -0
  55. package/spec/roles.yaml +30 -0
  56. package/spec/schema.md +394 -0
  57. package/spec/test-case-results.schema.json +62 -0
  58. package/spec/test-cases.schema.json +85 -0
  59. package/spec/ui-parity-audit.schema.json +194 -0
  60. package/spec/ui-reverse-engineering.schema.json +94 -0
  61. package/src/billing/plan-catalog.test.ts +46 -0
  62. package/src/billing/plan-catalog.ts +199 -0
  63. package/src/integrations/aws-sigv4.test.ts +42 -0
  64. package/src/integrations/aws-sigv4.ts +72 -0
  65. package/src/integrations/device-farm.ts +155 -0
  66. package/src/integrations/github-app.test.ts +57 -0
  67. package/src/integrations/github-app.ts +143 -0
  68. package/src/integrations/gitlab.ts +81 -0
  69. package/src/integrations/temp-email.test.ts +123 -0
  70. package/src/integrations/temp-email.ts +175 -0
  71. package/src/integrations/testflight-feedback.test.ts +51 -0
  72. package/src/integrations/testflight-feedback.ts +173 -0
  73. package/src/integrations/webdriver-client.ts +131 -0
  74. package/src/mcp/server.test.ts +1220 -0
  75. package/src/mcp/server.ts +3064 -0
  76. package/src/mcp/write-test-cases.test.ts +287 -0
  77. package/src/registry/api-key-client.ts +39 -0
  78. package/src/registry/control-plane-client.ts +212 -0
  79. package/src/registry/migrations/0001_registry.sql +47 -0
  80. package/src/registry/migrations/0002_device_authorizations.sql +23 -0
  81. package/src/registry/migrations/0003_project_environments.sql +25 -0
  82. package/src/registry/migrations/0004_device_authorization_email.sql +1 -0
  83. package/src/registry/migrations/0005_testing_control_plane.sql +55 -0
  84. package/src/registry/migrations/0006_workspaces.sql +36 -0
  85. package/src/registry/migrations/0007_project_apps.sql +26 -0
  86. package/src/registry/migrations/0008_agent_tasks.sql +30 -0
  87. package/src/registry/migrations/0009_journey_revisions.sql +17 -0
  88. package/src/registry/migrations/0010_agent_task_journey.sql +2 -0
  89. package/src/registry/migrations/0011_run_journey_revision.sql +1 -0
  90. package/src/registry/migrations/0012_canonical_flow_execution.sql +10 -0
  91. package/src/registry/migrations/0013_device_sessions.sql +22 -0
  92. package/src/registry/migrations/0014_agent_task_step.sql +1 -0
  93. package/src/registry/migrations/0015_agent_task_attempts.sql +6 -0
  94. package/src/registry/migrations/0016_github_issue_tracker.sql +54 -0
  95. package/src/registry/migrations/0017_run_targets.sql +7 -0
  96. package/src/registry/migrations/0018_run_fix_from.sql +4 -0
  97. package/src/registry/migrations/0019_workspace_flags.sql +9 -0
  98. package/src/registry/migrations/0020_orgs.sql +40 -0
  99. package/src/registry/migrations/0021_billing_core.sql +58 -0
  100. package/src/registry/migrations/0022_cloud_runners.sql +19 -0
  101. package/src/registry/migrations/0023_signup.sql +4 -0
  102. package/src/registry/migrations/0024_billing.sql +67 -0
  103. package/src/registry/migrations/0025_notifications.sql +47 -0
  104. package/src/registry/migrations/0026_repo_bindings.sql +28 -0
  105. package/src/registry/migrations/0027_feedback.sql +29 -0
  106. package/src/registry/migrations/0028_devices.sql +48 -0
  107. package/src/registry/migrations/0029_sso.sql +31 -0
  108. package/src/registry/migrations/0030_workspace_domains.sql +18 -0
  109. package/src/registry/migrations/0031_gitlab_and_teams.sql +45 -0
  110. package/src/registry/migrations/0032_personas.sql +15 -0
  111. package/src/registry/migrations/0033_bugmole_rename.sql +11 -0
  112. package/src/registry/task-scheduling.test.ts +100 -0
  113. package/src/registry/task-scheduling.ts +80 -0
  114. package/src/registry-worker/ai/platform-model.ts +77 -0
  115. package/src/registry-worker/ai/routes.test.ts +88 -0
  116. package/src/registry-worker/ai/routes.ts +90 -0
  117. package/src/registry-worker/artifacts.test.ts +98 -0
  118. package/src/registry-worker/artifacts.ts +85 -0
  119. package/src/registry-worker/billing/billing-core.test.ts +175 -0
  120. package/src/registry-worker/billing/checkout-routes.ts +209 -0
  121. package/src/registry-worker/billing/enforcement.ts +69 -0
  122. package/src/registry-worker/billing/entitlements.ts +108 -0
  123. package/src/registry-worker/billing/ledger.ts +186 -0
  124. package/src/registry-worker/billing/paypal/api.ts +259 -0
  125. package/src/registry-worker/billing/paypal/client.ts +91 -0
  126. package/src/registry-worker/billing/paypal/provider.ts +143 -0
  127. package/src/registry-worker/billing/paypal.test.ts +466 -0
  128. package/src/registry-worker/billing/provider.ts +114 -0
  129. package/src/registry-worker/billing/routes.ts +66 -0
  130. package/src/registry-worker/billing/subscriptions.ts +780 -0
  131. package/src/registry-worker/billing/thresholds.ts +107 -0
  132. package/src/registry-worker/core.ts +308 -0
  133. package/src/registry-worker/devices/devices.test.ts +185 -0
  134. package/src/registry-worker/devices/policy.ts +71 -0
  135. package/src/registry-worker/devices/routes.ts +453 -0
  136. package/src/registry-worker/domains/domains.test.ts +210 -0
  137. package/src/registry-worker/domains/routes.ts +139 -0
  138. package/src/registry-worker/domains.ts +88 -0
  139. package/src/registry-worker/email/sender.ts +75 -0
  140. package/src/registry-worker/env.d.ts +14716 -0
  141. package/src/registry-worker/features.ts +20 -0
  142. package/src/registry-worker/feedback/feedback.test.ts +230 -0
  143. package/src/registry-worker/feedback/format.ts +148 -0
  144. package/src/registry-worker/feedback/routes.ts +386 -0
  145. package/src/registry-worker/flags.ts +39 -0
  146. package/src/registry-worker/github/checks.test.ts +177 -0
  147. package/src/registry-worker/github/checks.ts +374 -0
  148. package/src/registry-worker/gitlab/checks.test.ts +141 -0
  149. package/src/registry-worker/gitlab/checks.ts +349 -0
  150. package/src/registry-worker/hooks.ts +54 -0
  151. package/src/registry-worker/index.ts +2077 -0
  152. package/src/registry-worker/jobs/index.ts +29 -0
  153. package/src/registry-worker/jobs/retention.ts +68 -0
  154. package/src/registry-worker/mcp/mcp.test.ts +355 -0
  155. package/src/registry-worker/mcp/routes.ts +215 -0
  156. package/src/registry-worker/mcp/token.ts +126 -0
  157. package/src/registry-worker/mcp/tools.ts +563 -0
  158. package/src/registry-worker/notifications/alerts.ts +212 -0
  159. package/src/registry-worker/notifications/notifications.test.ts +298 -0
  160. package/src/registry-worker/notifications/outbox.ts +83 -0
  161. package/src/registry-worker/notifications/routes.ts +280 -0
  162. package/src/registry-worker/notifications/secrets.ts +49 -0
  163. package/src/registry-worker/notifications/slack.ts +96 -0
  164. package/src/registry-worker/notifications/teams.ts +46 -0
  165. package/src/registry-worker/org/audit.ts +116 -0
  166. package/src/registry-worker/org/routes.test.ts +163 -0
  167. package/src/registry-worker/org/routes.ts +302 -0
  168. package/src/registry-worker/personas/personas.test.ts +78 -0
  169. package/src/registry-worker/personas/routes.ts +100 -0
  170. package/src/registry-worker/repo-triggers.ts +20 -0
  171. package/src/registry-worker/routes/index.ts +74 -0
  172. package/src/registry-worker/run-events.ts +24 -0
  173. package/src/registry-worker/runner/dispatch.ts +219 -0
  174. package/src/registry-worker/runner/jobs.ts +43 -0
  175. package/src/registry-worker/runner/metering.ts +82 -0
  176. package/src/registry-worker/runner/policy.ts +59 -0
  177. package/src/registry-worker/runner/routes.ts +171 -0
  178. package/src/registry-worker/runner/runner.test.ts +358 -0
  179. package/src/registry-worker/runner/tokens.ts +93 -0
  180. package/src/registry-worker/runs.test.ts +60 -0
  181. package/src/registry-worker/signup/policy.ts +57 -0
  182. package/src/registry-worker/signup/routes.ts +106 -0
  183. package/src/registry-worker/signup/signup.test.ts +81 -0
  184. package/src/registry-worker/sso/aegis.ts +141 -0
  185. package/src/registry-worker/sso/membership.ts +157 -0
  186. package/src/registry-worker/sso/routes.ts +458 -0
  187. package/src/registry-worker/sso/sso.test.ts +344 -0
  188. package/src/registry-worker/testing/d1-shim.ts +180 -0
  189. package/src/registry-worker/testing/harness.ts +137 -0
  190. package/src/runner-worker/index.ts +108 -0
  191. package/src/runtime/ai-analysis.ts +97 -0
  192. package/src/runtime/ai-exploration.test.ts +32 -0
  193. package/src/runtime/ai-exploration.ts +69 -0
  194. package/src/runtime/ai-repair.ts +74 -0
  195. package/src/runtime/ai-work.test.ts +99 -0
  196. package/src/runtime/android-screen-record.test.ts +75 -0
  197. package/src/runtime/android-screen-record.ts +192 -0
  198. package/src/runtime/app-understanding.test.ts +123 -0
  199. package/src/runtime/app-understanding.ts +201 -0
  200. package/src/runtime/appium-driver.test.ts +179 -0
  201. package/src/runtime/appium-driver.ts +295 -0
  202. package/src/runtime/blocker-resolution.test.ts +113 -0
  203. package/src/runtime/blocker-resolution.ts +111 -0
  204. package/src/runtime/browser-matrix.integration.test.ts +212 -0
  205. package/src/runtime/browser-matrix.test.ts +143 -0
  206. package/src/runtime/browser-matrix.ts +200 -0
  207. package/src/runtime/canonical-flow.test.ts +52 -0
  208. package/src/runtime/config-validate.ts +185 -0
  209. package/src/runtime/continuous-execution.ts +291 -0
  210. package/src/runtime/cursor-applescript.ts +573 -0
  211. package/src/runtime/cursor-cli-driver.test.ts +78 -0
  212. package/src/runtime/cursor-cli-driver.ts +156 -0
  213. package/src/runtime/cursor-driver-example.ts +117 -0
  214. package/src/runtime/cursor-driver-index.ts +65 -0
  215. package/src/runtime/cursor-driver-init.ts +277 -0
  216. package/src/runtime/cursor-driver-run.test.ts +15 -0
  217. package/src/runtime/cursor-driver-run.ts +323 -0
  218. package/src/runtime/cursor-driver.ts +332 -0
  219. package/src/runtime/cursor-llm-example.ts +90 -0
  220. package/src/runtime/cursor-llm.ts +206 -0
  221. package/src/runtime/cursor-mcp-monitor.ts +386 -0
  222. package/src/runtime/device-clouds/browserstack.ts +73 -0
  223. package/src/runtime/device-clouds/device-farm.ts +52 -0
  224. package/src/runtime/device-clouds/index.ts +92 -0
  225. package/src/runtime/device-clouds/kobiton.ts +70 -0
  226. package/src/runtime/device-clouds/targets.ts +44 -0
  227. package/src/runtime/device-clouds/types.ts +62 -0
  228. package/src/runtime/diff-proposal.ts +84 -0
  229. package/src/runtime/discovery-task.test.ts +29 -0
  230. package/src/runtime/discovery-task.ts +284 -0
  231. package/src/runtime/driver-recovery.ts +69 -0
  232. package/src/runtime/driver.ts +79 -0
  233. package/src/runtime/environment.test.ts +104 -0
  234. package/src/runtime/environment.ts +137 -0
  235. package/src/runtime/executor.test.ts +509 -0
  236. package/src/runtime/executor.ts +921 -0
  237. package/src/runtime/explorer.test.ts +101 -0
  238. package/src/runtime/explorer.ts +1013 -0
  239. package/src/runtime/failure-analysis.test.ts +111 -0
  240. package/src/runtime/failure-analysis.ts +272 -0
  241. package/src/runtime/fixtures/fake-maestro.sh +36 -0
  242. package/src/runtime/flow-language.test.ts +268 -0
  243. package/src/runtime/flow-language.ts +414 -0
  244. package/src/runtime/init-wizard.ts +354 -0
  245. package/src/runtime/ios-screen-record.test.ts +68 -0
  246. package/src/runtime/ios-screen-record.ts +155 -0
  247. package/src/runtime/journey-editor.ts +452 -0
  248. package/src/runtime/journey-evidence.test.ts +161 -0
  249. package/src/runtime/journey-evidence.ts +180 -0
  250. package/src/runtime/journey-graph.test.ts +257 -0
  251. package/src/runtime/journey-graph.ts +170 -0
  252. package/src/runtime/legacy-names.ts +32 -0
  253. package/src/runtime/llm-example.ts +105 -0
  254. package/src/runtime/llm.ts +527 -0
  255. package/src/runtime/local-browser.test.ts +45 -0
  256. package/src/runtime/local-browser.ts +48 -0
  257. package/src/runtime/local-registry-stub.test.ts +325 -0
  258. package/src/runtime/local-registry-stub.ts +803 -0
  259. package/src/runtime/maestro-driver.test.ts +84 -0
  260. package/src/runtime/maestro-driver.ts +209 -0
  261. package/src/runtime/mole-voice.ts +21 -0
  262. package/src/runtime/nav-crawl.test.ts +100 -0
  263. package/src/runtime/nav-crawl.ts +153 -0
  264. package/src/runtime/pipeline.test.ts +405 -0
  265. package/src/runtime/pipeline.ts +833 -0
  266. package/src/runtime/planner.test.ts +37 -0
  267. package/src/runtime/planner.ts +274 -0
  268. package/src/runtime/platform-ai.ts +76 -0
  269. package/src/runtime/playwright-driver.test.ts +93 -0
  270. package/src/runtime/playwright-driver.ts +620 -0
  271. package/src/runtime/project-spec.ts +140 -0
  272. package/src/runtime/record-run-verdicts.ts +68 -0
  273. package/src/runtime/reporter.test.ts +56 -0
  274. package/src/runtime/reporter.ts +158 -0
  275. package/src/runtime/reset.test.ts +44 -0
  276. package/src/runtime/reset.ts +61 -0
  277. package/src/runtime/reviewer.test.ts +73 -0
  278. package/src/runtime/reviewer.ts +158 -0
  279. package/src/runtime/run-job.ts +136 -0
  280. package/src/runtime/run-once.test.ts +207 -0
  281. package/src/runtime/run-once.ts +168 -0
  282. package/src/runtime/run.ts +132 -0
  283. package/src/runtime/screen-recording.ts +34 -0
  284. package/src/runtime/serve-gateway.test.ts +74 -0
  285. package/src/runtime/serve-gateway.ts +164 -0
  286. package/src/runtime/serve-worker.test.ts +23 -0
  287. package/src/runtime/serve-worker.ts +278 -0
  288. package/src/runtime/site-discovery.test.ts +168 -0
  289. package/src/runtime/site-discovery.ts +308 -0
  290. package/src/runtime/target-runner.ts +144 -0
  291. package/src/runtime/test-case-verdicts.test.ts +94 -0
  292. package/src/runtime/test-case-verdicts.ts +120 -0
  293. package/src/runtime/ui-reverse-engineering/coordinator.test.ts +59 -0
  294. package/src/runtime/ui-reverse-engineering/coordinator.ts +391 -0
  295. package/src/runtime/ui-reverse-engineering/types.ts +197 -0
  296. package/src/runtime/web-suite.test.ts +97 -0
  297. package/src/runtime/web-suite.ts +176 -0
  298. package/src/storage/create-object-store.ts +144 -0
  299. package/src/storage/keys.ts +34 -0
  300. package/src/storage/local-artifact-server.test.ts +314 -0
  301. package/src/storage/local-artifact-server.ts +357 -0
  302. package/src/storage/object-store.test.ts +28 -0
  303. package/src/storage/object-store.ts +101 -0
  304. package/src/storage/registry-object-store.ts +88 -0
  305. package/src/storage/remote-object-store.ts +104 -0
  306. package/src/storage/storage-directory.test.ts +43 -0
  307. package/src/storage/storage-directory.ts +24 -0
  308. package/tsconfig.json +24 -0
@@ -0,0 +1,140 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import YAML from "yaml";
4
+ import { createProjectObjectStore } from "../storage/create-object-store.js";
5
+ import { projectPrefix } from "../storage/keys.js";
6
+
7
+ /**
8
+ * Publish a project's spec (roles, blockers) into its own artifact store.
9
+ *
10
+ * The dashboard reads project-scoped artifacts through the local bridge; it
11
+ * has no access to the repo's spec/ directory. Without this, its blocker
12
+ * list fell back to hardcoded fixtures that described a different
13
+ * application entirely, and the richer fields the spec actually carries —
14
+ * a blocker's type, its selectors, its resolution strategy — had nowhere
15
+ * to come from and were simply not rendered.
16
+ *
17
+ * Publishing per project also stops spec/ being implicitly repo-global:
18
+ * each project points at its own spec_dir, so two projects no longer have
19
+ * to share one roles.yaml.
20
+ */
21
+
22
+ export type PublishedBlocker = {
23
+ id: string;
24
+ title: string;
25
+ description: string;
26
+ hardStop: boolean;
27
+ source: string;
28
+ type?: string;
29
+ selectors: string[];
30
+ resolutionStrategy?: string;
31
+ onEncounter?: string;
32
+ };
33
+
34
+ export type PublishedRole = {
35
+ name: string;
36
+ description?: string;
37
+ permissions: string[];
38
+ };
39
+
40
+ export type PublishedProjectSpec = {
41
+ schemaVersion: 1;
42
+ projectId: string;
43
+ publishedAt: string;
44
+ specDir: string;
45
+ roles: PublishedRole[];
46
+ blockers: PublishedBlocker[];
47
+ };
48
+
49
+ function readYaml(filePath: string): any {
50
+ try {
51
+ return YAML.parse(fs.readFileSync(filePath, "utf8"));
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ function asStrings(value: unknown): string[] {
58
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
59
+ }
60
+
61
+ /** Human title from a snake_case blocker name, without inventing wording. */
62
+ function titleFromName(name: string): string {
63
+ const spaced = name.replace(/[_-]+/g, " ").trim();
64
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
65
+ }
66
+
67
+ export function readProjectSpec(specDir: string, projectId: string): PublishedProjectSpec {
68
+ const rolesDoc = readYaml(path.join(specDir, "roles.yaml"));
69
+ const blockersDoc = readYaml(path.join(specDir, "blockers.yaml"));
70
+
71
+ const roles: PublishedRole[] = Array.isArray(rolesDoc?.roles)
72
+ ? rolesDoc.roles
73
+ .filter((role: any) => typeof role?.name === "string")
74
+ .map((role: any) => ({
75
+ name: role.name,
76
+ description: typeof role.description === "string" ? role.description : undefined,
77
+ permissions: asStrings(role.permissions),
78
+ }))
79
+ : [];
80
+
81
+ const blockers: PublishedBlocker[] = Array.isArray(blockersDoc?.blockers)
82
+ ? blockersDoc.blockers
83
+ .filter((entry: any) => typeof entry?.name === "string")
84
+ .map((entry: any) => ({
85
+ id: entry.name,
86
+ title: titleFromName(entry.name),
87
+ description: typeof entry.description === "string" ? entry.description : "",
88
+ hardStop: entry.hard_stop === true,
89
+ source: "spec/blockers.yaml",
90
+ type: typeof entry.type === "string" ? entry.type : undefined,
91
+ selectors: asStrings(entry.selectors),
92
+ resolutionStrategy: typeof entry.resolution?.strategy === "string" ? entry.resolution.strategy : undefined,
93
+ onEncounter: typeof entry.on_encounter === "string" ? entry.on_encounter : undefined,
94
+ }))
95
+ : [];
96
+
97
+ // Top-level hard_stops are policy conditions rather than encounterable
98
+ // blockers, but they do stop a run, so they belong in the same list
99
+ // instead of being invisible to the dashboard.
100
+ const policyStops: PublishedBlocker[] = Array.isArray(blockersDoc?.hard_stops)
101
+ ? blockersDoc.hard_stops
102
+ .filter((entry: any) => typeof entry?.name === "string")
103
+ .map((entry: any) => ({
104
+ id: entry.name,
105
+ title: titleFromName(entry.name),
106
+ description: typeof entry.description === "string" ? entry.description : "",
107
+ hardStop: true,
108
+ source: "spec/blockers.yaml",
109
+ type: "policy",
110
+ selectors: [],
111
+ resolutionStrategy: typeof entry.action === "string" ? entry.action : undefined,
112
+ onEncounter: typeof entry.condition === "string" ? entry.condition : undefined,
113
+ }))
114
+ : [];
115
+
116
+ const seen = new Set(blockers.map((blocker) => blocker.id));
117
+ return {
118
+ schemaVersion: 1,
119
+ projectId,
120
+ publishedAt: new Date().toISOString(),
121
+ specDir,
122
+ roles,
123
+ blockers: [...blockers, ...policyStops.filter((entry) => !seen.has(entry.id))],
124
+ };
125
+ }
126
+
127
+ export function projectSpecKey(projectId: string): string {
128
+ return `${projectPrefix(projectId)}/spec/project-spec.json`;
129
+ }
130
+
131
+ /** Write the project's spec so the dashboard can read it via the bridge. */
132
+ export async function publishProjectSpec(cfg: any): Promise<PublishedProjectSpec | null> {
133
+ const projectId = cfg.project?.id ?? "default";
134
+ const specDir = cfg.runtime?.spec_dir || "./spec";
135
+ const spec = readProjectSpec(specDir, projectId);
136
+ if (spec.roles.length === 0 && spec.blockers.length === 0) return null;
137
+ const store = createProjectObjectStore(cfg);
138
+ await store.put(projectSpecKey(projectId), JSON.stringify(spec, null, 2) + "\n", "application/json");
139
+ return spec;
140
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Persisting the verdicts a run produced.
3
+ *
4
+ * Kept beside the pure judging logic rather than inside the worker so the
5
+ * artifact shape stays in one place: the dashboard, the pipeline's run stage
6
+ * and the agent-facing bugmole_write_test_case_results tool all read the
7
+ * same contract, and a second writer that drifted from it would silently stop
8
+ * counting.
9
+ */
10
+
11
+ import { createProjectObjectStore } from "../storage/create-object-store.js";
12
+ import { projectPrefix } from "../storage/keys.js";
13
+ import { verdictsFromRun, type NodeOutcome } from "./test-case-verdicts.js";
14
+
15
+ function store(cfg: any) {
16
+ return createProjectObjectStore(cfg, "./artifacts");
17
+ }
18
+
19
+ /**
20
+ * Judge and record every case written for `journeyId` from what the run saw.
21
+ *
22
+ * Returns the number of cases judged, or null when the flow has no case set
23
+ * to judge — that is not a failure, it just means nothing was written for
24
+ * this flow yet and the testcases stage has more to do.
25
+ */
26
+ export async function recordRunVerdicts(
27
+ cfg: any,
28
+ projectId: string,
29
+ journeyId: string,
30
+ nodes: NodeOutcome[],
31
+ runId?: string,
32
+ unreachedReason?: string,
33
+ ): Promise<{ caseSetId: string; judged: number } | null> {
34
+ const objects = await store(cfg).list(`${projectPrefix(projectId)}/test-cases/`);
35
+ for (const object of objects) {
36
+ if (!object.key.endsWith(".json")) continue;
37
+ let caseSet: any;
38
+ try {
39
+ caseSet = JSON.parse(await store(cfg).getText(object.key));
40
+ } catch {
41
+ continue;
42
+ }
43
+ if (caseSet?.journeyId !== journeyId) continue;
44
+ const cases = Array.isArray(caseSet.cases)
45
+ ? caseSet.cases.filter((item: any) => typeof item?.caseId === "string" && typeof item?.nodeId === "string")
46
+ : [];
47
+ if (cases.length === 0) return null;
48
+
49
+ const results = verdictsFromRun(cases, nodes, unreachedReason);
50
+ const record = {
51
+ schemaVersion: 1,
52
+ caseSetId: caseSet.caseSetId,
53
+ projectId,
54
+ journeyId,
55
+ // Stamped from the case set, so a verdict always says which revision of
56
+ // the assumptions it judged — a run against superseded assumptions is
57
+ // discarded rather than shown as proof.
58
+ journeyRevision: caseSet.journeyRevision,
59
+ runId,
60
+ recordedAt: new Date().toISOString(),
61
+ results,
62
+ };
63
+ const key = `${projectPrefix(projectId)}/test-case-results/${caseSet.caseSetId}.json`;
64
+ await store(cfg).put(key, JSON.stringify(record, null, 2) + "\n", "application/json");
65
+ return { caseSetId: caseSet.caseSetId, judged: results.length };
66
+ }
67
+ return null;
68
+ }
@@ -0,0 +1,56 @@
1
+ import { strict as assert } from "node:assert";
2
+ import test from "node:test";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { runKey } from "../storage/keys.js";
7
+
8
+ async function makeTestConfig() {
9
+ const artifactsDir = await fs.mkdtemp(path.join(os.tmpdir(), "bugmole-reporter-test-"));
10
+ return { project: { id: "demo" }, runtime: { artifacts_dir: artifactsDir } };
11
+ }
12
+
13
+ test("report includes real per-step detail and evidence links, and never a fabricated drift metric", async () => {
14
+ const { generateReport } = await import("./reporter.js");
15
+ const cfg = await makeTestConfig();
16
+ const resultsKey = runKey("demo", "run-1", "results.json");
17
+ await fs.mkdir(path.dirname(path.join(cfg.runtime.artifacts_dir, resultsKey)), { recursive: true });
18
+ await fs.writeFile(
19
+ path.join(cfg.runtime.artifacts_dir, resultsKey),
20
+ JSON.stringify({
21
+ runId: "run-1",
22
+ journeyId: "checkout",
23
+ planId: "checkout",
24
+ timestamp: "2026-08-27T00:00:00.000Z",
25
+ results: [
26
+ {
27
+ nodeId: "step-1",
28
+ action: "launch",
29
+ outcome: "PASS",
30
+ reason: { code: "verified", message: "launchAppCommand: COMPLETED" },
31
+ screenshot: { source: "runtime", status: "captured", artifactKey: "projects/demo/journeys/evidence/checkout/step-1.png" },
32
+ },
33
+ {
34
+ nodeId: "step-2",
35
+ action: "assert-home",
36
+ outcome: "FAIL",
37
+ reason: { code: "execution_failed", message: "assertVisibleCommand: FAILED" },
38
+ },
39
+ ],
40
+ }),
41
+ "utf8",
42
+ );
43
+
44
+ const result = await generateReport(cfg, { runId: "run-1", format: "markdown" });
45
+
46
+ assert.equal(result.status, "success");
47
+ assert.equal(result.summary?.passed, 1);
48
+ assert.equal(result.summary?.failed, 1);
49
+ assert.ok(result.summary && !("driftDetected" in result.summary));
50
+ assert.doesNotMatch(result.report ?? "", /Drift Detected/);
51
+ assert.match(result.report ?? "", /launchAppCommand: COMPLETED/);
52
+ assert.match(result.report ?? "", /assertVisibleCommand: FAILED/);
53
+ assert.match(result.report ?? "", /projects\/demo\/journeys\/evidence\/checkout\/step-1\.png/);
54
+
55
+ await fs.rm(cfg.runtime.artifacts_dir, { recursive: true, force: true });
56
+ });
@@ -0,0 +1,158 @@
1
+ import { createProjectObjectStore } from "../storage/create-object-store.js";
2
+ import { json, type ObjectStore } from "../storage/object-store.js";
3
+ import { projectPrefix } from "../storage/keys.js";
4
+ import { bugCount, molesFound } from "./mole-voice.js";
5
+
6
+ interface ReportOptions {
7
+ runId?: string;
8
+ format?: "json" | "markdown";
9
+ }
10
+
11
+ interface ReportResult {
12
+ status: "success" | "error";
13
+ reportPath?: string;
14
+ report?: string;
15
+ summary?: {
16
+ totalRuns: number;
17
+ totalJourneys: number;
18
+ passed: number;
19
+ failed: number;
20
+ unknown: number;
21
+ };
22
+ message?: string;
23
+ error?: string;
24
+ }
25
+
26
+ function evidenceLine(result: any): string | undefined {
27
+ const artifactKey = result?.screenshot?.artifactKey;
28
+ if (!artifactKey) return undefined;
29
+ return `evidence: ${artifactKey}`;
30
+ }
31
+
32
+ /**
33
+ * Reporter: Produces health reports with evidence links and plan diffs
34
+ */
35
+ export async function generateReport(cfg: any, options: ReportOptions): Promise<ReportResult> {
36
+ try {
37
+ const { runId, format = "markdown" } = options;
38
+ const artifactsDir = cfg.runtime.artifacts_dir || "./runs";
39
+ const store: ObjectStore = createProjectObjectStore(cfg, artifactsDir);
40
+
41
+ // Collect all run results
42
+ const runs: any[] = [];
43
+ const projectId = cfg.project?.id ?? "default";
44
+ const objects = await store.list(`${projectPrefix(projectId)}/runs/`);
45
+ for (const object of objects.filter((item) => item.key.endsWith("/results.json"))) {
46
+ const results = JSON.parse(await store.getText(object.key));
47
+ if (!runId || results.runId === runId) runs.push(results);
48
+ }
49
+
50
+ // Calculate summary
51
+ let totalJourneys = 0;
52
+ let passed = 0;
53
+ let failed = 0;
54
+ let unknown = 0;
55
+
56
+ for (const run of runs) {
57
+ if (run.results) {
58
+ for (const result of run.results) {
59
+ totalJourneys++;
60
+ if (result.outcome === "PASS") passed++;
61
+ else if (result.outcome === "FAIL") failed++;
62
+ else unknown++;
63
+ }
64
+ }
65
+ }
66
+
67
+ const summary = {
68
+ totalRuns: runs.length,
69
+ totalJourneys,
70
+ passed,
71
+ failed,
72
+ unknown,
73
+ };
74
+
75
+ // Generate report
76
+ let report: string;
77
+ const timestamp = new Date().toISOString();
78
+
79
+ if (format === "json") {
80
+ report = JSON.stringify(
81
+ {
82
+ timestamp,
83
+ summary,
84
+ runs: runs.map((r) => ({
85
+ runId: r.runId,
86
+ journeyId: r.journeyId,
87
+ planId: r.planId,
88
+ timestamp: r.timestamp,
89
+ results: r.results,
90
+ })),
91
+ },
92
+ null,
93
+ 2,
94
+ );
95
+ } else {
96
+ // Markdown format
97
+ report = `# Bugmole report
98
+
99
+ Generated: ${timestamp}
100
+
101
+ ## Summary
102
+
103
+ **${molesFound(summary.failed)}.**
104
+
105
+ - **Total Runs**: ${summary.totalRuns}
106
+ - **Total Journeys**: ${summary.totalJourneys}
107
+ - **Passed**: ${summary.passed}
108
+ - **Failed**: ${summary.failed}
109
+ - **Unknown**: ${summary.unknown}
110
+
111
+ ## Runs
112
+
113
+ ${runs
114
+ .map(
115
+ (run) => `### Run: ${run.runId}
116
+
117
+ - **Journey**: ${run.journeyId}
118
+ - **Plan**: ${run.planId}
119
+ - **Timestamp**: ${run.timestamp}
120
+ - **Results**: ${run.results?.length || 0} steps
121
+
122
+ ${
123
+ run.results
124
+ ?.map((r: any) => {
125
+ const evidence = evidenceLine(r);
126
+ const detail = r.reason?.message ? ` — ${r.reason.message}` : "";
127
+ return `- ${r.action}: ${r.outcome}${detail}${evidence ? ` (${evidence})` : ""}`;
128
+ })
129
+ .join("\n") || "No results"
130
+ }`,
131
+ )
132
+ .join("\n\n")}
133
+
134
+ ## Next Steps
135
+
136
+ ${failed > 0 ? `- Review the ${bugCount(failed)} our moles found` : ""}
137
+ ${unknown > 0 ? `- Investigate ${unknown} unknown outcome(s)` : ""}
138
+ `;
139
+ }
140
+
141
+ // Save report
142
+ const reportKey = `${projectPrefix(projectId)}/reports/report_${Date.now()}.${format === "json" ? "json" : "md"}`;
143
+ await store.put(reportKey, format === "json" ? json(JSON.parse(report)) : report, format === "json" ? "application/json" : "text/markdown");
144
+
145
+ return {
146
+ status: "success",
147
+ reportPath: reportKey,
148
+ report,
149
+ summary,
150
+ message: `Report generated: ${reportKey}`,
151
+ };
152
+ } catch (error: any) {
153
+ return {
154
+ status: "error",
155
+ error: error.message || "Report generation failed",
156
+ };
157
+ }
158
+ }
@@ -0,0 +1,44 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import { resetProjectData } from "./reset.js";
7
+ import { LocalObjectStore } from "../storage/object-store.js";
8
+
9
+ test("reset removes all current-project generated data but preserves other projects", async () => {
10
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), "bugmole-reset-"));
11
+ const store = new LocalObjectStore(directory);
12
+ await store.put("projects/demo/runs/run-1/results.json", "{}");
13
+ await store.put("projects/demo/reports/report.json", "{}");
14
+ await store.put("projects/demo/progress/progress.json", "{}");
15
+ await store.put("projects/demo/plans/plan.flow.yaml", "version: 1");
16
+ await store.put("projects/demo/journeys/graph.json", "{}");
17
+ await store.put("projects/demo/ui-audits/audit.json", "{}");
18
+ await store.put("projects/demo/ui-parity-audits/audit.json", "{}");
19
+ await store.put("projects/demo/ui-reverse-engineering/job.json", "{}");
20
+ await store.put("projects/other/runs/run-2/results.json", "{}");
21
+ await fs.mkdir(path.join(directory, "spec", "plans"), { recursive: true });
22
+ await fs.writeFile(path.join(directory, "spec", "journeys.graph.json"), "{}");
23
+ await fs.writeFile(path.join(directory, "spec", "plans", "generated.flow.yaml"), "version: 1");
24
+ try {
25
+ await resetProjectData({
26
+ project: { id: "demo" },
27
+ runtime: { artifacts_dir: directory, spec_dir: path.join(directory, "spec") },
28
+ storage: { provider: "local", localDirectory: directory },
29
+ });
30
+ assert.equal(await store.has("projects/demo/runs/run-1/results.json"), false);
31
+ assert.equal(await store.has("projects/demo/reports/report.json"), false);
32
+ assert.equal(await store.has("projects/demo/progress/progress.json"), false);
33
+ assert.equal(await store.has("projects/demo/plans/plan.flow.yaml"), false);
34
+ assert.equal(await store.has("projects/demo/journeys/graph.json"), false);
35
+ assert.equal(await store.has("projects/demo/ui-audits/audit.json"), false);
36
+ assert.equal(await store.has("projects/demo/ui-parity-audits/audit.json"), false);
37
+ assert.equal(await store.has("projects/demo/ui-reverse-engineering/job.json"), false);
38
+ assert.equal(await fs.stat(path.join(directory, "spec", "journeys.graph.json")).catch(() => null), null);
39
+ assert.equal(await fs.stat(path.join(directory, "spec", "plans")).catch(() => null), null);
40
+ assert.equal(await store.has("projects/other/runs/run-2/results.json"), true);
41
+ } finally {
42
+ await fs.rm(directory, { recursive: true, force: true });
43
+ }
44
+ });
@@ -0,0 +1,61 @@
1
+ import readline from "node:readline/promises";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { stdin as input, stdout as output } from "node:process";
5
+ import { createProjectObjectStore } from "../storage/create-object-store.js";
6
+ import { projectPrefix } from "../storage/keys.js";
7
+
8
+ export async function confirmProjectReset(projectId: string): Promise<boolean> {
9
+ const rl = readline.createInterface({ input, output });
10
+ try {
11
+ const answer = await rl.question(
12
+ `This permanently deletes execution data for project "${projectId}". Type "RESET ${projectId}" to continue: `,
13
+ );
14
+ return answer.trim() === `RESET ${projectId}`;
15
+ } finally {
16
+ rl.close();
17
+ }
18
+ }
19
+
20
+ export async function resetProjectData(cfg: any): Promise<void> {
21
+ const projectId = cfg.project?.id ?? "default";
22
+ const artifactsDir = cfg.runtime.artifacts_dir || "./runs";
23
+ const store = createProjectObjectStore(cfg, artifactsDir);
24
+ const prefix = projectPrefix(projectId);
25
+ for (const segment of [
26
+ "runs",
27
+ "reports",
28
+ "progress",
29
+ "cursor-commits",
30
+ "plans",
31
+ "journeys",
32
+ "ui-audits",
33
+ "ui-parity-audits",
34
+ "ui-reverse-engineering",
35
+ ]) {
36
+ await store.deletePrefix(`${prefix}/${segment}/`);
37
+ }
38
+
39
+ // These are generated workspace views, not configuration or contracts.
40
+ // Remove them so the next exploration cannot silently reuse stale state.
41
+ const specDir = path.resolve(cfg.runtime?.spec_dir || "./spec");
42
+ await fs.rm(path.join(specDir, "journeys.graph.json"), { force: true });
43
+ await fs.rm(path.join(specDir, "plans"), { recursive: true, force: true });
44
+
45
+ const registryUrl = process.env.BUGMOLE_REGISTRY_URL?.trim();
46
+ const apiKey = process.env.BUGMOLE_API_KEY?.trim();
47
+ if (registryUrl && apiKey) {
48
+ const response = await fetch(
49
+ `${registryUrl.replace(/\/$/, "")}/api/projects/${encodeURIComponent(projectId)}/reset`,
50
+ {
51
+ method: "POST",
52
+ headers: {
53
+ authorization: `Bearer ${apiKey}`,
54
+ "content-type": "application/json",
55
+ },
56
+ body: JSON.stringify({ confirmation: `RESET ${projectId}` }),
57
+ },
58
+ );
59
+ if (!response.ok) throw new Error(`Registry reset failed (${response.status})`);
60
+ }
61
+ }
@@ -0,0 +1,73 @@
1
+ import { strict as assert } from "node:assert";
2
+ import test from "node:test";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+
7
+ function makeTestConfig(testDir: string) {
8
+ const specDir = path.join(testDir, "spec");
9
+ const artifactsDir = path.join(testDir, "artifacts");
10
+ fs.mkdirSync(specDir, { recursive: true });
11
+ fs.mkdirSync(artifactsDir, { recursive: true });
12
+ fs.writeFileSync(path.join(specDir, "journey_graph.yaml"), "journeys: []\n", "utf8");
13
+ return { runtime: { spec_dir: specDir, artifacts_dir: artifactsDir } };
14
+ }
15
+
16
+ test("does not approve a change on the strength of an evidence object alone", async () => {
17
+ const { review } = await import("./reviewer.js");
18
+ const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "bugmole-reviewer-test-"));
19
+ const cfg = makeTestConfig(testDir);
20
+
21
+ const result = await review(cfg, {
22
+ filePath: "journey_graph.yaml",
23
+ changeSummary: "Widened the selector for the sign-in button.",
24
+ diff: "-tapOn: '#old-button'\n+tapOn: '#new-button'\n",
25
+ // Non-empty, but references nothing that exists on disk.
26
+ evidence: { screenshot: "not-a-real-file.png", note: "trust me" },
27
+ });
28
+
29
+ assert.equal(result.status, "success");
30
+ assert.equal(result.verdict, "REQUIRE_CLARIFICATION");
31
+ assert.match(result.reasoning ?? "", /none found on disk/);
32
+ assert.equal(result.riskAssessment?.couldMaskDefect, true);
33
+ fs.rmSync(testDir, { recursive: true, force: true });
34
+ });
35
+
36
+ test("approves a low-risk change once its referenced evidence is verified on disk", async () => {
37
+ const { review } = await import("./reviewer.js");
38
+ const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "bugmole-reviewer-test-"));
39
+ const cfg = makeTestConfig(testDir);
40
+ const screenshotPath = path.join(cfg.runtime.artifacts_dir, "sign-in-after.png");
41
+ fs.writeFileSync(screenshotPath, Buffer.from([0]));
42
+
43
+ const result = await review(cfg, {
44
+ filePath: "journey_graph.yaml",
45
+ changeSummary: "Widened the selector for the sign-in button.",
46
+ diff: "-tapOn: '#old-button'\n+tapOn: '#new-button'\n",
47
+ evidence: { screenshot: "sign-in-after.png" },
48
+ });
49
+
50
+ assert.equal(result.verdict, "APPROVED");
51
+ assert.equal(result.riskAssessment?.level, "low");
52
+ assert.match(result.reasoning ?? "", /1\/1 referenced artifacts verified/);
53
+ fs.rmSync(testDir, { recursive: true, force: true });
54
+ });
55
+
56
+ test("rejects a diff that removes a journey even with verified evidence", async () => {
57
+ const { review } = await import("./reviewer.js");
58
+ const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "bugmole-reviewer-test-"));
59
+ const cfg = makeTestConfig(testDir);
60
+ const screenshotPath = path.join(cfg.runtime.artifacts_dir, "proof.png");
61
+ fs.writeFileSync(screenshotPath, Buffer.from([0]));
62
+
63
+ const result = await review(cfg, {
64
+ filePath: "journey_graph.yaml",
65
+ changeSummary: "Removed a stale journey no longer used by any plan.",
66
+ diff: "- - id: checkout\n journey: checkout\n",
67
+ evidence: { screenshot: "proof.png" },
68
+ });
69
+
70
+ assert.equal(result.verdict, "REJECTED");
71
+ assert.ok(result.riskAssessment?.failureModes?.includes("Journey removed"));
72
+ fs.rmSync(testDir, { recursive: true, force: true });
73
+ });