@hominis/fireforge 0.43.0 → 0.44.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 (470) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +10 -1
  3. package/dist/bin/fireforge.d.ts +4 -9
  4. package/dist/bin/fireforge.js +42 -40
  5. package/dist/build-info.json +4 -4
  6. package/dist/src/cli.js +74 -8
  7. package/dist/src/commands/bootstrap.js +17 -20
  8. package/dist/src/commands/build.js +25 -32
  9. package/dist/src/commands/config.js +34 -41
  10. package/dist/src/commands/discard.js +8 -9
  11. package/dist/src/commands/doctor-check-core.d.ts +4 -18
  12. package/dist/src/commands/doctor-check-core.js +8 -35
  13. package/dist/src/commands/doctor-external-toolchains.js +2 -2
  14. package/dist/src/commands/doctor-furnace-config-sync.d.ts +18 -0
  15. package/dist/src/commands/{doctor-furnace-manifest-sync.js → doctor-furnace-config-sync.js} +27 -26
  16. package/dist/src/commands/doctor-furnace-jar.d.ts +7 -9
  17. package/dist/src/commands/doctor-furnace-jar.js +7 -9
  18. package/dist/src/commands/doctor-furnace.js +21 -24
  19. package/dist/src/commands/doctor-orphaned-harness.d.ts +16 -20
  20. package/dist/src/commands/doctor-orphaned-harness.js +18 -44
  21. package/dist/src/commands/doctor-post-rebase-audit.d.ts +2 -0
  22. package/dist/src/commands/{doctor/post-rebase-audit.js → doctor-post-rebase-audit.js} +3 -3
  23. package/dist/src/commands/doctor-working-tree.d.ts +10 -12
  24. package/dist/src/commands/doctor-working-tree.js +11 -18
  25. package/dist/src/commands/doctor.js +35 -53
  26. package/dist/src/commands/download.js +32 -48
  27. package/dist/src/commands/export-all.js +29 -33
  28. package/dist/src/commands/export-flow.js +6 -6
  29. package/dist/src/commands/export-placement-conflicts.js +10 -9
  30. package/dist/src/commands/export-placement-gate.js +5 -5
  31. package/dist/src/commands/export-shared.d.ts +30 -33
  32. package/dist/src/commands/export-shared.js +45 -43
  33. package/dist/src/commands/export.js +21 -27
  34. package/dist/src/commands/furnace/apply.js +14 -23
  35. package/dist/src/commands/furnace/chrome-doc-remove.js +14 -21
  36. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +23 -23
  37. package/dist/src/commands/furnace/chrome-doc-templates.js +23 -23
  38. package/dist/src/commands/furnace/chrome-doc-tests.js +26 -29
  39. package/dist/src/commands/furnace/chrome-doc.d.ts +16 -17
  40. package/dist/src/commands/furnace/chrome-doc.js +31 -41
  41. package/dist/src/commands/furnace/create-browser-test.d.ts +4 -4
  42. package/dist/src/commands/furnace/create-browser-test.js +8 -8
  43. package/dist/src/commands/furnace/create-mochikit.d.ts +6 -7
  44. package/dist/src/commands/furnace/create-mochikit.js +8 -9
  45. package/dist/src/commands/furnace/create-readback.d.ts +12 -13
  46. package/dist/src/commands/furnace/create-readback.js +12 -13
  47. package/dist/src/commands/furnace/create-templates.d.ts +22 -28
  48. package/dist/src/commands/furnace/create-templates.js +35 -30
  49. package/dist/src/commands/furnace/create-xpcshell.js +2 -2
  50. package/dist/src/commands/furnace/create.js +33 -23
  51. package/dist/src/commands/furnace/deploy.js +16 -24
  52. package/dist/src/commands/furnace/diff.js +9 -9
  53. package/dist/src/commands/furnace/init.js +33 -36
  54. package/dist/src/commands/furnace/override.js +23 -25
  55. package/dist/src/commands/furnace/preview.js +47 -67
  56. package/dist/src/commands/furnace/refresh.js +12 -16
  57. package/dist/src/commands/furnace/remove-mutations.d.ts +3 -6
  58. package/dist/src/commands/furnace/remove-mutations.js +9 -16
  59. package/dist/src/commands/furnace/remove-state.js +1 -1
  60. package/dist/src/commands/furnace/remove.js +33 -38
  61. package/dist/src/commands/furnace/rename-xpcshell.d.ts +8 -11
  62. package/dist/src/commands/furnace/rename-xpcshell.js +8 -11
  63. package/dist/src/commands/furnace/rename.js +59 -69
  64. package/dist/src/commands/furnace/scan.js +19 -33
  65. package/dist/src/commands/furnace/status.js +12 -9
  66. package/dist/src/commands/furnace/sync.js +9 -16
  67. package/dist/src/commands/furnace/validate.js +20 -20
  68. package/dist/src/commands/import.js +42 -35
  69. package/dist/src/commands/lint-per-patch.js +15 -10
  70. package/dist/src/commands/lint-per-run-checkjs.d.ts +13 -13
  71. package/dist/src/commands/lint-per-run-checkjs.js +13 -13
  72. package/dist/src/commands/lint-report.js +6 -7
  73. package/dist/src/commands/lint.d.ts +15 -16
  74. package/dist/src/commands/lint.js +86 -102
  75. package/dist/src/commands/package.js +6 -11
  76. package/dist/src/commands/patch/compact.js +1 -1
  77. package/dist/src/commands/patch/delete.js +24 -33
  78. package/dist/src/commands/patch/index.d.ts +5 -6
  79. package/dist/src/commands/patch/index.js +10 -12
  80. package/dist/src/commands/patch/lint-ignore.js +14 -13
  81. package/dist/src/commands/patch/move-files-create.d.ts +6 -6
  82. package/dist/src/commands/patch/move-files-create.js +8 -8
  83. package/dist/src/commands/patch/move-files-into.d.ts +6 -6
  84. package/dist/src/commands/patch/move-files-into.js +8 -8
  85. package/dist/src/commands/patch/patch-context.d.ts +2 -3
  86. package/dist/src/commands/patch/patch-context.js +2 -3
  87. package/dist/src/commands/patch/rename.d.ts +10 -11
  88. package/dist/src/commands/patch/rename.js +11 -12
  89. package/dist/src/commands/patch/reorder.js +8 -9
  90. package/dist/src/commands/patch/split-plan.js +1 -1
  91. package/dist/src/commands/patch/split.d.ts +8 -10
  92. package/dist/src/commands/patch/split.js +9 -11
  93. package/dist/src/commands/patch/staged-dependency-validate.js +8 -8
  94. package/dist/src/commands/patch/staged-dependency.js +7 -3
  95. package/dist/src/commands/patch/tier.d.ts +9 -9
  96. package/dist/src/commands/patch/tier.js +18 -15
  97. package/dist/src/commands/re-export-adjacent.js +9 -8
  98. package/dist/src/commands/re-export-bulk-scan.d.ts +9 -9
  99. package/dist/src/commands/re-export-bulk-scan.js +22 -21
  100. package/dist/src/commands/re-export-drift.js +10 -10
  101. package/dist/src/commands/re-export-files.d.ts +6 -7
  102. package/dist/src/commands/re-export-files.js +34 -44
  103. package/dist/src/commands/re-export-lint.d.ts +10 -13
  104. package/dist/src/commands/re-export-lint.js +16 -14
  105. package/dist/src/commands/re-export-options.d.ts +6 -6
  106. package/dist/src/commands/re-export-options.js +6 -6
  107. package/dist/src/commands/re-export-register.d.ts +4 -10
  108. package/dist/src/commands/re-export-register.js +5 -11
  109. package/dist/src/commands/re-export-scan.js +1 -1
  110. package/dist/src/commands/re-export-single.d.ts +3 -3
  111. package/dist/src/commands/re-export-single.js +33 -25
  112. package/dist/src/commands/re-export.js +13 -21
  113. package/dist/src/commands/rebase/abort.js +3 -3
  114. package/dist/src/commands/rebase/confirm.js +2 -1
  115. package/dist/src/commands/rebase/continue.d.ts +1 -1
  116. package/dist/src/commands/rebase/continue.js +14 -9
  117. package/dist/src/commands/rebase/index.js +11 -11
  118. package/dist/src/commands/rebase/patch-loop.d.ts +7 -1
  119. package/dist/src/commands/rebase/patch-loop.js +60 -39
  120. package/dist/src/commands/rebase/summary.js +1 -1
  121. package/dist/src/commands/register.js +5 -7
  122. package/dist/src/commands/reset.js +2 -1
  123. package/dist/src/commands/resolve.d.ts +16 -13
  124. package/dist/src/commands/resolve.js +30 -33
  125. package/dist/src/commands/run.js +33 -35
  126. package/dist/src/commands/setup-support.js +15 -19
  127. package/dist/src/commands/setup.js +13 -9
  128. package/dist/src/commands/status-check.d.ts +6 -6
  129. package/dist/src/commands/status-json.d.ts +6 -6
  130. package/dist/src/commands/status-json.js +5 -0
  131. package/dist/src/commands/status-output.d.ts +5 -5
  132. package/dist/src/commands/status-output.js +6 -6
  133. package/dist/src/commands/status-ownership.d.ts +16 -14
  134. package/dist/src/commands/status-ownership.js +16 -14
  135. package/dist/src/commands/status.js +42 -34
  136. package/dist/src/commands/test-build-phase.js +26 -13
  137. package/dist/src/commands/test-diagnose.js +44 -44
  138. package/dist/src/commands/test-register.js +1 -0
  139. package/dist/src/commands/test-run.d.ts +14 -16
  140. package/dist/src/commands/test-run.js +21 -13
  141. package/dist/src/commands/test-stale-gate.d.ts +15 -16
  142. package/dist/src/commands/test-stale-gate.js +15 -16
  143. package/dist/src/commands/test-verdict.d.ts +20 -3
  144. package/dist/src/commands/test-verdict.js +30 -0
  145. package/dist/src/commands/test.js +83 -76
  146. package/dist/src/commands/token-coverage.js +3 -4
  147. package/dist/src/commands/token.js +16 -18
  148. package/dist/src/commands/tree.js +47 -31
  149. package/dist/src/commands/typecheck.d.ts +10 -12
  150. package/dist/src/commands/typecheck.js +24 -27
  151. package/dist/src/commands/verify.js +13 -15
  152. package/dist/src/commands/watch.js +21 -23
  153. package/dist/src/commands/wire.js +52 -62
  154. package/dist/src/core/ast-utils.d.ts +8 -7
  155. package/dist/src/core/ast-utils.js +8 -7
  156. package/dist/src/core/branding.d.ts +9 -14
  157. package/dist/src/core/branding.js +7 -11
  158. package/dist/src/core/browser-wire.d.ts +1 -1
  159. package/dist/src/core/browser-wire.js +14 -19
  160. package/dist/src/core/build-audit-platform.js +25 -33
  161. package/dist/src/core/build-audit-registration.js +17 -25
  162. package/dist/src/core/build-audit-resolve.d.ts +15 -17
  163. package/dist/src/core/build-audit-resolve.js +27 -33
  164. package/dist/src/core/build-audit-transforms.js +18 -19
  165. package/dist/src/core/build-audit.js +28 -42
  166. package/dist/src/core/build-baseline-types.d.ts +29 -35
  167. package/dist/src/core/build-baseline.d.ts +5 -4
  168. package/dist/src/core/build-baseline.js +14 -14
  169. package/dist/src/core/build-overwrite-guard.d.ts +7 -14
  170. package/dist/src/core/build-overwrite-guard.js +62 -19
  171. package/dist/src/core/build-prepare.d.ts +2 -2
  172. package/dist/src/core/build-prepare.js +8 -9
  173. package/dist/src/core/config-state.js +12 -4
  174. package/dist/src/core/config-validate.js +8 -5
  175. package/dist/src/core/config.d.ts +5 -8
  176. package/dist/src/core/config.js +7 -16
  177. package/dist/src/core/coverage-extend.d.ts +15 -13
  178. package/dist/src/core/coverage-extend.js +15 -13
  179. package/dist/src/core/destructive.d.ts +31 -9
  180. package/dist/src/core/destructive.js +29 -26
  181. package/dist/src/core/diff-hunks.js +10 -12
  182. package/dist/src/core/discard-baseline.d.ts +8 -8
  183. package/dist/src/core/discard-baseline.js +16 -15
  184. package/dist/src/core/display-state.d.ts +12 -14
  185. package/dist/src/core/display-state.js +12 -14
  186. package/dist/src/core/engine-changes.d.ts +2 -4
  187. package/dist/src/core/engine-changes.js +2 -4
  188. package/dist/src/core/engine-precondition.d.ts +28 -13
  189. package/dist/src/core/engine-precondition.js +25 -10
  190. package/dist/src/core/engine-session-lock.d.ts +47 -21
  191. package/dist/src/core/engine-session-lock.js +168 -33
  192. package/dist/src/core/export-moved-code-shape.d.ts +10 -10
  193. package/dist/src/core/export-moved-code-shape.js +10 -10
  194. package/dist/src/core/file-lock.d.ts +62 -5
  195. package/dist/src/core/file-lock.js +196 -59
  196. package/dist/src/core/firefox-cache.js +1 -3
  197. package/dist/src/core/firefox-extract.js +10 -14
  198. package/dist/src/core/firefox.d.ts +7 -5
  199. package/dist/src/core/firefox.js +4 -5
  200. package/dist/src/core/furnace-apply-dry-run.d.ts +2 -2
  201. package/dist/src/core/furnace-apply-dry-run.js +3 -2
  202. package/dist/src/core/furnace-apply-ftl.d.ts +7 -9
  203. package/dist/src/core/furnace-apply-ftl.js +12 -12
  204. package/dist/src/core/furnace-apply-helpers.d.ts +7 -7
  205. package/dist/src/core/furnace-apply-helpers.js +19 -16
  206. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +8 -9
  207. package/dist/src/core/furnace-apply-overwrite-warn.js +8 -9
  208. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +3 -4
  209. package/dist/src/core/furnace-apply-undeploy-actions.js +3 -4
  210. package/dist/src/core/furnace-apply.d.ts +5 -8
  211. package/dist/src/core/furnace-apply.js +19 -24
  212. package/dist/src/core/furnace-checksum-utils.d.ts +14 -0
  213. package/dist/src/core/furnace-checksum-utils.js +16 -0
  214. package/dist/src/core/furnace-config.d.ts +21 -30
  215. package/dist/src/core/furnace-config.js +32 -54
  216. package/dist/src/core/furnace-constants.d.ts +8 -0
  217. package/dist/src/core/furnace-constants.js +10 -2
  218. package/dist/src/core/furnace-css-fragments.d.ts +5 -5
  219. package/dist/src/core/furnace-css-fragments.js +5 -5
  220. package/dist/src/core/furnace-jsconfig.d.ts +11 -12
  221. package/dist/src/core/furnace-jsconfig.js +11 -12
  222. package/dist/src/core/furnace-marker.d.ts +7 -8
  223. package/dist/src/core/furnace-marker.js +7 -8
  224. package/dist/src/core/furnace-operation.d.ts +31 -0
  225. package/dist/src/core/furnace-operation.js +54 -17
  226. package/dist/src/core/furnace-precondition.d.ts +61 -0
  227. package/dist/src/core/furnace-precondition.js +67 -0
  228. package/dist/src/core/furnace-registration-remove.d.ts +5 -6
  229. package/dist/src/core/furnace-registration-remove.js +6 -7
  230. package/dist/src/core/furnace-registration-validate.d.ts +13 -0
  231. package/dist/src/core/furnace-registration-validate.js +20 -1
  232. package/dist/src/core/furnace-registration.d.ts +5 -6
  233. package/dist/src/core/furnace-registration.js +10 -12
  234. package/dist/src/core/furnace-rollback.js +7 -7
  235. package/dist/src/core/furnace-scanner.d.ts +1 -0
  236. package/dist/src/core/furnace-scanner.js +1 -2
  237. package/dist/src/core/furnace-stale-export.d.ts +10 -10
  238. package/dist/src/core/furnace-stale-export.js +10 -10
  239. package/dist/src/core/furnace-state-persist.d.ts +6 -8
  240. package/dist/src/core/furnace-state-persist.js +6 -8
  241. package/dist/src/core/furnace-validate-accessibility.d.ts +22 -0
  242. package/dist/src/core/furnace-validate-accessibility.js +310 -1
  243. package/dist/src/core/furnace-validate-compatibility.js +141 -1
  244. package/dist/src/core/furnace-validate-helpers.d.ts +5 -116
  245. package/dist/src/core/furnace-validate-helpers.js +0 -491
  246. package/dist/src/core/furnace-validate-registration.d.ts +7 -0
  247. package/dist/src/core/furnace-validate-registration.js +115 -41
  248. package/dist/src/core/furnace-validate.d.ts +16 -0
  249. package/dist/src/core/furnace-validate.js +188 -145
  250. package/dist/src/core/furnace-version-drift.d.ts +10 -11
  251. package/dist/src/core/furnace-version-drift.js +10 -11
  252. package/dist/src/core/git-base.d.ts +7 -9
  253. package/dist/src/core/git-base.js +7 -9
  254. package/dist/src/core/git-diff.d.ts +20 -9
  255. package/dist/src/core/git-diff.js +148 -63
  256. package/dist/src/core/git-file-ops.d.ts +19 -19
  257. package/dist/src/core/git-file-ops.js +19 -19
  258. package/dist/src/core/git-readonly-index.d.ts +53 -19
  259. package/dist/src/core/git-readonly-index.js +75 -31
  260. package/dist/src/core/git-status.d.ts +5 -6
  261. package/dist/src/core/git-status.js +5 -6
  262. package/dist/src/core/git.d.ts +9 -10
  263. package/dist/src/core/git.js +54 -70
  264. package/dist/src/core/license-headers.d.ts +17 -23
  265. package/dist/src/core/license-headers.js +17 -23
  266. package/dist/src/core/mach-build-artifacts.d.ts +9 -9
  267. package/dist/src/core/mach-build-artifacts.js +57 -19
  268. package/dist/src/core/mach-error-hints.js +42 -29
  269. package/dist/src/core/mach-known-noise-filter.d.ts +15 -16
  270. package/dist/src/core/mach-known-noise-filter.js +18 -20
  271. package/dist/src/core/mach-mozconfig.d.ts +14 -0
  272. package/dist/src/core/mach-mozconfig.js +30 -0
  273. package/dist/src/core/mach-resource-shim.d.ts +43 -58
  274. package/dist/src/core/mach-resource-shim.js +50 -67
  275. package/dist/src/core/mach.d.ts +37 -41
  276. package/dist/src/core/mach.js +50 -51
  277. package/dist/src/core/marionette-port.d.ts +36 -1
  278. package/dist/src/core/marionette-port.js +96 -31
  279. package/dist/src/core/marionette-preflight.d.ts +8 -12
  280. package/dist/src/core/marionette-preflight.js +86 -52
  281. package/dist/src/core/mochitest-server-port.d.ts +42 -0
  282. package/dist/src/core/mochitest-server-port.js +119 -0
  283. package/dist/src/core/module-resolution-preflight.d.ts +9 -11
  284. package/dist/src/core/module-resolution-preflight.js +9 -11
  285. package/dist/src/core/moz-manifest-helpers.d.ts +79 -0
  286. package/dist/src/core/moz-manifest-helpers.js +163 -0
  287. package/dist/src/core/{manifest-register.d.ts → moz-manifest-register.d.ts} +1 -1
  288. package/dist/src/core/{manifest-register.js → moz-manifest-register.js} +13 -28
  289. package/dist/src/core/{manifest-rules.d.ts → moz-manifest-rules.d.ts} +10 -1
  290. package/dist/src/core/{manifest-rules.js → moz-manifest-rules.js} +31 -24
  291. package/dist/src/core/{manifest-tokenizers.d.ts → moz-manifest-tokenizers.d.ts} +3 -5
  292. package/dist/src/core/{manifest-tokenizers.js → moz-manifest-tokenizers.js} +9 -12
  293. package/dist/src/core/ownership-table.d.ts +14 -16
  294. package/dist/src/core/ownership-table.js +14 -16
  295. package/dist/src/core/parser-fallback.d.ts +15 -1
  296. package/dist/src/core/parser-fallback.js +22 -3
  297. package/dist/src/core/patch-apply-fuzz.d.ts +10 -12
  298. package/dist/src/core/patch-apply-fuzz.js +15 -18
  299. package/dist/src/core/patch-apply.d.ts +13 -34
  300. package/dist/src/core/patch-apply.js +16 -76
  301. package/dist/src/core/patch-artifact-normalize.d.ts +13 -15
  302. package/dist/src/core/patch-artifact-normalize.js +13 -15
  303. package/dist/src/core/patch-export-metadata.d.ts +12 -2
  304. package/dist/src/core/patch-export-metadata.js +13 -4
  305. package/dist/src/core/patch-export-update.d.ts +13 -1
  306. package/dist/src/core/patch-export-update.js +25 -8
  307. package/dist/src/core/patch-export.d.ts +15 -8
  308. package/dist/src/core/patch-export.js +10 -11
  309. package/dist/src/core/patch-files.d.ts +5 -6
  310. package/dist/src/core/patch-files.js +5 -6
  311. package/dist/src/core/patch-identifier-suggest.d.ts +7 -12
  312. package/dist/src/core/patch-identifier-suggest.js +14 -19
  313. package/dist/src/core/patch-lint-binary.d.ts +51 -0
  314. package/dist/src/core/patch-lint-binary.js +39 -0
  315. package/dist/src/core/{lint-cache.d.ts → patch-lint-cache.d.ts} +5 -5
  316. package/dist/src/core/{lint-cache.js → patch-lint-cache.js} +21 -23
  317. package/dist/src/core/patch-lint-checkjs.d.ts +16 -16
  318. package/dist/src/core/patch-lint-checkjs.js +16 -16
  319. package/dist/src/core/patch-lint-chrome-jsdoc.d.ts +6 -9
  320. package/dist/src/core/patch-lint-chrome-jsdoc.js +10 -14
  321. package/dist/src/core/patch-lint-creators.d.ts +8 -8
  322. package/dist/src/core/patch-lint-creators.js +8 -8
  323. package/dist/src/core/patch-lint-cross.d.ts +17 -20
  324. package/dist/src/core/patch-lint-cross.js +16 -16
  325. package/dist/src/core/patch-lint-css.d.ts +3 -5
  326. package/dist/src/core/patch-lint-css.js +17 -22
  327. package/dist/src/core/patch-lint-diff-tag.d.ts +14 -16
  328. package/dist/src/core/patch-lint-diff-tag.js +14 -16
  329. package/dist/src/core/patch-lint-jsdoc.d.ts +7 -8
  330. package/dist/src/core/patch-lint-jsdoc.js +26 -19
  331. package/dist/src/core/patch-lint-mozbuild.js +20 -20
  332. package/dist/src/core/patch-lint-observer.d.ts +7 -7
  333. package/dist/src/core/patch-lint-observer.js +7 -7
  334. package/dist/src/core/patch-lint.d.ts +38 -29
  335. package/dist/src/core/patch-lint.js +89 -121
  336. package/dist/src/core/patch-lock.d.ts +1 -1
  337. package/dist/src/core/patch-lock.js +45 -9
  338. package/dist/src/core/patch-manifest-consistency.d.ts +2 -1
  339. package/dist/src/core/patch-manifest-consistency.js +25 -27
  340. package/dist/src/core/patch-manifest-io.js +14 -18
  341. package/dist/src/core/patch-manifest-resolve.d.ts +9 -12
  342. package/dist/src/core/patch-manifest-resolve.js +9 -12
  343. package/dist/src/core/patch-manifest-validate.d.ts +6 -1
  344. package/dist/src/core/patch-manifest-validate.js +25 -13
  345. package/dist/src/core/patch-parse.d.ts +24 -13
  346. package/dist/src/core/patch-parse.js +19 -14
  347. package/dist/src/core/patch-policy.d.ts +2 -2
  348. package/dist/src/core/patch-policy.js +2 -2
  349. package/dist/src/core/patch-registration-refs.d.ts +7 -8
  350. package/dist/src/core/patch-registration-refs.js +9 -9
  351. package/dist/src/core/process-cpu-time.d.ts +12 -0
  352. package/dist/src/core/process-cpu-time.js +52 -0
  353. package/dist/src/core/rebase-session.d.ts +38 -26
  354. package/dist/src/core/rebase-session.js +55 -28
  355. package/dist/src/core/register-browser-content.js +9 -74
  356. package/dist/src/core/{register-test-manifest.js → register-browser-test.js} +7 -42
  357. package/dist/src/core/register-module.js +10 -43
  358. package/dist/src/core/register-scaffold.d.ts +8 -8
  359. package/dist/src/core/register-scaffold.js +10 -10
  360. package/dist/src/core/register-shared-css.d.ts +9 -12
  361. package/dist/src/core/register-shared-css.js +17 -87
  362. package/dist/src/core/register-xpcshell-test.d.ts +6 -7
  363. package/dist/src/core/register-xpcshell-test.js +7 -8
  364. package/dist/src/core/shared-ftl.js +12 -4
  365. package/dist/src/core/state-file.d.ts +45 -0
  366. package/dist/src/core/state-file.js +34 -0
  367. package/dist/src/core/status-binary.d.ts +18 -11
  368. package/dist/src/core/status-binary.js +19 -12
  369. package/dist/src/core/status-classify.d.ts +30 -36
  370. package/dist/src/core/status-classify.js +21 -26
  371. package/dist/src/core/test-changed-prefs.d.ts +30 -0
  372. package/dist/src/core/test-changed-prefs.js +78 -0
  373. package/dist/src/core/test-harness-crash.d.ts +25 -63
  374. package/dist/src/core/test-harness-crash.js +121 -142
  375. package/dist/src/core/test-harness-signature.d.ts +18 -0
  376. package/dist/src/core/test-harness-signature.js +13 -0
  377. package/dist/src/core/test-path-scope.d.ts +12 -15
  378. package/dist/src/core/test-path-scope.js +15 -18
  379. package/dist/src/core/test-stale-check.d.ts +17 -17
  380. package/dist/src/core/test-stale-check.js +50 -43
  381. package/dist/src/core/test-stall-triage.d.ts +51 -0
  382. package/dist/src/core/test-stall-triage.js +126 -0
  383. package/dist/src/core/token-category.js +5 -7
  384. package/dist/src/core/token-coverage.js +9 -9
  385. package/dist/src/core/token-dark-mode.d.ts +9 -11
  386. package/dist/src/core/token-dark-mode.js +9 -11
  387. package/dist/src/core/token-docs.d.ts +2 -0
  388. package/dist/src/core/token-docs.js +2 -0
  389. package/dist/src/core/token-manager.d.ts +10 -2
  390. package/dist/src/core/token-manager.js +17 -8
  391. package/dist/src/core/token-scaffold.d.ts +7 -7
  392. package/dist/src/core/token-scaffold.js +7 -7
  393. package/dist/src/core/token-variant.d.ts +4 -4
  394. package/dist/src/core/token-variant.js +4 -4
  395. package/dist/src/core/toolchain-preflight.d.ts +20 -21
  396. package/dist/src/core/toolchain-preflight.js +28 -31
  397. package/dist/src/core/tree-guard.js +12 -2
  398. package/dist/src/core/tree-store.d.ts +41 -27
  399. package/dist/src/core/tree-store.js +63 -42
  400. package/dist/src/core/typecheck-shim.d.ts +23 -24
  401. package/dist/src/core/typecheck-shim.js +23 -24
  402. package/dist/src/core/typecheck.js +8 -8
  403. package/dist/src/core/wire-destroy.d.ts +3 -3
  404. package/dist/src/core/wire-destroy.js +7 -15
  405. package/dist/src/core/wire-dom-fragment.d.ts +9 -17
  406. package/dist/src/core/wire-dom-fragment.js +11 -44
  407. package/dist/src/core/wire-init.d.ts +2 -3
  408. package/dist/src/core/wire-init.js +8 -19
  409. package/dist/src/core/wire-subscript.d.ts +2 -3
  410. package/dist/src/core/wire-subscript.js +4 -13
  411. package/dist/src/core/wire-utils.d.ts +7 -8
  412. package/dist/src/core/wire-utils.js +7 -8
  413. package/dist/src/core/xpcshell-appdir.d.ts +38 -45
  414. package/dist/src/core/xpcshell-appdir.js +38 -45
  415. package/dist/src/errors/base.d.ts +37 -16
  416. package/dist/src/errors/base.js +37 -16
  417. package/dist/src/errors/build.d.ts +13 -1
  418. package/dist/src/errors/build.js +14 -2
  419. package/dist/src/errors/codes.d.ts +16 -0
  420. package/dist/src/errors/codes.js +16 -0
  421. package/dist/src/errors/furnace.d.ts +4 -4
  422. package/dist/src/errors/git.d.ts +6 -10
  423. package/dist/src/errors/git.js +6 -10
  424. package/dist/src/errors/patch.d.ts +7 -7
  425. package/dist/src/errors/patch.js +7 -7
  426. package/dist/src/errors/rebase.d.ts +4 -4
  427. package/dist/src/errors/rebase.js +4 -4
  428. package/dist/src/index.d.ts +7 -1
  429. package/dist/src/index.js +5 -0
  430. package/dist/src/types/commands/index.d.ts +2 -2
  431. package/dist/src/types/commands/options.d.ts +88 -70
  432. package/dist/src/types/commands/patches.d.ts +24 -34
  433. package/dist/src/types/commands/project.d.ts +5 -34
  434. package/dist/src/types/config.d.ts +25 -23
  435. package/dist/src/types/config.js +6 -1
  436. package/dist/src/types/furnace.d.ts +37 -13
  437. package/dist/src/types/furnace.js +19 -1
  438. package/dist/src/types/index.d.ts +3 -3
  439. package/dist/src/utils/build-info.d.ts +7 -8
  440. package/dist/src/utils/build-info.js +12 -11
  441. package/dist/src/utils/errors.d.ts +6 -8
  442. package/dist/src/utils/errors.js +6 -8
  443. package/dist/src/utils/logger.d.ts +27 -10
  444. package/dist/src/utils/logger.js +28 -16
  445. package/dist/src/utils/machine-output.d.ts +12 -0
  446. package/dist/src/utils/machine-output.js +33 -0
  447. package/dist/src/utils/options.d.ts +31 -14
  448. package/dist/src/utils/options.js +86 -30
  449. package/dist/src/utils/paths.d.ts +10 -8
  450. package/dist/src/utils/paths.js +10 -8
  451. package/dist/src/utils/platform.d.ts +0 -25
  452. package/dist/src/utils/platform.js +1 -42
  453. package/dist/src/utils/process-group.d.ts +9 -4
  454. package/dist/src/utils/process-group.js +19 -5
  455. package/dist/src/utils/process.d.ts +42 -7
  456. package/dist/src/utils/process.js +44 -17
  457. package/dist/src/utils/ps-duration.d.ts +31 -0
  458. package/dist/src/utils/ps-duration.js +70 -0
  459. package/dist/src/utils/regex.d.ts +9 -10
  460. package/dist/src/utils/regex.js +9 -10
  461. package/dist/src/utils/validation.d.ts +58 -28
  462. package/dist/src/utils/validation.js +60 -33
  463. package/package.json +2 -2
  464. package/dist/src/commands/doctor/post-rebase-audit.d.ts +0 -2
  465. package/dist/src/commands/doctor-furnace-manifest-sync.d.ts +0 -18
  466. package/dist/src/core/manifest-helpers.d.ts +0 -38
  467. package/dist/src/core/manifest-helpers.js +0 -120
  468. /package/dist/src/core/{lint-projection.d.ts → patch-lint-projection.d.ts} +0 -0
  469. /package/dist/src/core/{lint-projection.js → patch-lint-projection.js} +0 -0
  470. /package/dist/src/core/{register-test-manifest.d.ts → register-browser-test.d.ts} +0 -0
@@ -12,6 +12,11 @@ import { getPython } from './mach-python.js';
12
12
  import { installMachResourceGuard } from './mach-resource-shim.js';
13
13
  import { detectHarnessCrashSignature } from './test-harness-crash.js';
14
14
  // Re-export sub-modules so existing `from './mach.js'` imports keep working.
15
+ // This block is a frozen compatibility list, not a barrel to grow: every name
16
+ // here already had a `from './mach.js'` import when the module was split.
17
+ // New helpers belong in their own sub-module and are imported from it
18
+ // directly — adding to this list pulls the whole sub-module into every suite
19
+ // that mocks `mach.js`.
15
20
  export { attemptMozinfoRewrite, buildArtifactMismatchMessage, hasBuildArtifacts, hasRunnableBundle, } from './mach-build-artifacts.js';
16
21
  export { generateMozconfig } from './mach-mozconfig.js';
17
22
  export { ensurePython, resetResolvedPython } from './mach-python.js';
@@ -87,7 +92,7 @@ export async function runMachCapture(args, engineDir, options = {}) {
87
92
  ...(options.env ? { env: options.env } : {}),
88
93
  // Every capture dispatch (test suites, protected builds, package,
89
94
  // storybook) runs as a process-group leader and is group-reaped on
90
- // exit/abort — see ExecOptions.processGroup (0.37.0 item 9a).
95
+ // exit/abort — see ExecOptions.processGroup.
91
96
  processGroup: true,
92
97
  onStdout: (data) => {
93
98
  stdout += data;
@@ -148,18 +153,16 @@ export async function bootstrapWithOutput(engineDir) {
148
153
  return runMachInheritCapture(['bootstrap', '--application-choice', 'browser'], engineDir);
149
154
  }
150
155
  /**
151
- * Prints any matched {@link MachErrorHint} hints for the captured mach output.
152
- * No-op when nothing matches. Always called before a non-zero exit propagates
153
- * so the hint sits immediately below the raw mach error in the operator's
154
- * terminal.
156
+ * Prints any matched {@link MachErrorHint} hints for the captured mach
157
+ * output. No-op when nothing matches. Always called before a non-zero exit
158
+ * propagates so the hint sits immediately below the raw mach error in the
159
+ * operator's terminal.
155
160
  *
156
161
  * The scanner is passed the concatenation of stderr AND stdout because mach
157
162
  * streams its subcommand output through a timestamp-prefixing wrapper that
158
- * writes both streams to whatever FD the subprocess chose — in practice,
159
- * `rustc` errors from `mach build` can land on stdout rather than stderr,
160
- * and the eval run's Darwin 25 `_CharT` hint pattern matched the captured
161
- * text but our pre-0.16 code only fed `result.stderr` into the scanner, so
162
- * the hint never fired.
163
+ * writes both streams to whatever FD the subprocess chose — `rustc` errors
164
+ * from `mach build` can land on stdout, so feeding only `result.stderr`
165
+ * silently loses the match.
163
166
  */
164
167
  function surfaceMachErrorHints(result) {
165
168
  const combined = `${result.stderr}\n${result.stdout}`;
@@ -179,21 +182,20 @@ function surfaceMachErrorHints(result) {
179
182
  const DEFAULT_BUILD_CRASH_RETRIES = 2;
180
183
  /**
181
184
  * The single protected path every FireForge mach build dispatch routes
182
- * through (field report: `build --ui` and the pre-test `--build` step died
183
- * on the resource-monitor crash family while plain `build` survived,
184
- * because only some entries were protected and retry budgets differed):
185
+ * through — `build`, `build --ui`, and the pre-test `--build` step all use
186
+ * it, so no entry point is left unprotected with a different retry budget:
185
187
  *
186
188
  * 1. installs the resource-monitor degrade guard IN the mach virtualenvs
187
- * (plus the PYTHONPATH fallback) — re-installed before every attempt,
188
- * so a venv materialized by a crashed first attempt is guarded on the
189
- * next one instead of every retry dying on the same wedged state;
189
+ * (plus the PYTHONPATH fallback) — re-installed before every attempt, so
190
+ * a venv materialized by a crashed first attempt is guarded on the next
191
+ * one instead of every retry dying on the same wedged state;
190
192
  * 2. spawns a fresh mach process per attempt;
191
193
  * 3. retries ONLY the recognized harness-crash family (resource monitor /
192
194
  * psutil startup tracebacks) up to the uniform budget — an ordinary
193
195
  * compile error is never retried.
194
196
  *
195
- * The protected path never runs `mach configure`, never clobbers, and
196
- * never widens the requested build kind — a `faster` dispatch retries as
197
+ * The protected path never runs `mach configure`, never clobbers, and never
198
+ * widens the requested build kind — a `faster` dispatch retries as
197
199
  * `mach build faster`, so it cannot invalidate more of the objdir than the
198
200
  * command the operator asked for.
199
201
  */
@@ -249,32 +251,27 @@ export async function buildUI(engineDir) {
249
251
  return runProtectedMachBuild('faster', engineDir);
250
252
  }
251
253
  /**
252
- * Runs an operation while holding a sidecar build lock keyed on the
253
- * project root. Concurrent `fireforge build` / `fireforge build --ui`
254
- * invocations against the same tree serialise instead of racing through
255
- * the mach obj-dir.
254
+ * Runs an operation while holding a sidecar build lock keyed on the project
255
+ * root. Concurrent `fireforge build` / `fireforge build --ui` invocations
256
+ * against the same tree serialise instead of racing through the mach
257
+ * obj-dir.
256
258
  *
257
- * Motivating case (2026-04-21 eval): a `fireforge build --ui` run
258
- * kicked off while a full `fireforge build` was still in flight against
259
- * the same engine tree accepted the command and handed off to `mach
260
- * build faster`, which failed almost immediately with `No rule to make
261
- * target 'XUL'`. The real problem is that the first build had not yet
262
- * materialised the full backend; the operator was left staring at a
263
- * low-level make error with no link to the actual cause (a concurrent
264
- * build in flight). The lock intercepts the second invocation before
265
- * it touches mach, and the refusal message names the PID currently
266
- * holding the lock so the operator can decide whether to wait or
267
- * investigate a hung process.
259
+ * Without it, a `build --ui` started while a full `build` is still in flight
260
+ * hands off to `mach build faster` and fails almost immediately with `No
261
+ * rule to make target 'XUL'` — the real problem being that the first build
262
+ * has not yet materialised the full backend, with nothing in the low-level
263
+ * make error pointing at the concurrent run. The lock intercepts the second
264
+ * invocation before it touches mach, and the refusal names the PID holding
265
+ * the lock so the operator can decide whether to wait or investigate.
268
266
  *
269
267
  * Stale-lock recovery: the lock stores the owner PID; a crashed build
270
- * (SIGINT, SIGTERM, or a kernel kill) leaves the lock dir behind but
271
- * not the owning process, and `withFileLock` removes the lock on the
272
- * next attempt when `process.kill(pid, 0)` shows the owner is gone.
268
+ * (SIGINT, SIGTERM, or a kernel kill) leaves the lock dir behind but not the
269
+ * owning process, and `withFileLock` removes the lock on the next attempt
270
+ * when `process.kill(pid, 0)` shows the owner is gone.
273
271
  *
274
- * The project-root variant is the right granularity: a single machine
275
- * may have several FireForge projects side by side, and nothing says
276
- * they cannot build in parallel. The lock serialises *within* one
277
- * project, not across unrelated ones.
272
+ * The project-root variant is the right granularity: a single machine may
273
+ * have several FireForge projects side by side and nothing says they cannot
274
+ * build in parallel. The lock serialises *within* one project.
278
275
  *
279
276
  * Returns whatever the inner operation returns.
280
277
  */
@@ -356,18 +353,17 @@ export async function watch(engineDir) {
356
353
  }
357
354
  /**
358
355
  * Runs mach watch while preserving stdin and capturing emitted output.
356
+ *
357
+ * `env` is threaded through so the caller can prepend the resolved watchman
358
+ * directory to PATH in a way mach inherits: `fireforge watch` can locate
359
+ * `watchman` via `which`, but the mach subprocess spawns with the parent's
360
+ * PATH only — on macOS that typically omits `/opt/homebrew/bin`, so
361
+ * `mach watch` fails at the `watch-project` subscription step.
362
+ *
359
363
  * @param engineDir - Path to the engine directory
360
- * @param options - Optional environment overrides merged into the mach subprocess env
364
+ * @param options - Optional environment overrides merged into the mach
365
+ * subprocess env
361
366
  * @returns Captured output and exit code
362
- *
363
- * 2026-04-24 eval Finding 12: the pre-0.18.1 shape accepted no options
364
- * and so never forwarded the detected watchman path into the mach
365
- * subprocess env. `fireforge watch` could locate `watchman` via PATH
366
- * (the probe's `which` succeeded) but the mach subprocess spawned with
367
- * the parent's PATH only — on macOS that typically omits
368
- * `/opt/homebrew/bin`, so `mach watch` failed at the `watch-project`
369
- * subscription step. Accepting `env` here lets the caller prepend the
370
- * resolved watchman directory to PATH in a way mach inherits.
371
367
  */
372
368
  export async function watchWithOutput(engineDir, options = {}) {
373
369
  return runMachInheritCapture(['watch'], engineDir, options.env ? { env: options.env } : {});
@@ -385,6 +381,9 @@ export async function test(engineDir, testPaths = [], args = []) {
385
381
  /**
386
382
  * Runs mach test while capturing streamed output for better diagnostics.
387
383
  *
384
+ * @param engineDir - Absolute path to the engine checkout
385
+ * @param testPaths - Test paths to pass to `mach test`; empty runs the default set
386
+ * @param args - Extra `mach test` arguments appended after the paths
388
387
  * @param env - Optional extra environment variables for the mach process
389
388
  * (merged over `process.env` by the exec layer). Used by
390
389
  * `fireforge test --perf-samples` to publish the artifact-path contract.
@@ -401,7 +400,7 @@ export async function testWithOutput(engineDir, testPaths = [], args = [], env)
401
400
  * capturing output. Unlike the generic `mach test`, the suite-specific
402
401
  * commands degrade a broken mozlog resource monitor to a warning instead of
403
402
  * crashing at startup, so `fireforge test` dispatches single-suite runs here
404
- * to stay resilient to the host psutil failure (field report E1).
403
+ * to stay resilient to the host psutil failure.
405
404
  *
406
405
  * Signature mirrors {@link testWithOutput} so the two are interchangeable in
407
406
  * the dispatch path.
@@ -31,8 +31,24 @@ export interface MarionettePortProbeResult {
31
31
  export interface RunningBundleProcess {
32
32
  pid: number;
33
33
  commandLine: string;
34
+ /**
35
+ * Elapsed wall-clock seconds since the process started, from `ps
36
+ * etime=`. `NaN` when the field was absent or unparseable — callers must
37
+ * omit the clause rather than print a zero, because "started 0s ago" is
38
+ * exactly the wrong attribution for a peer session's long-lived browser.
39
+ */
40
+ elapsedSeconds: number;
34
41
  }
35
- /** Pure parser kept exported so platform output handling is regression-testable. */
42
+ /**
43
+ * Pure parser kept exported so platform output handling is
44
+ * regression-testable.
45
+ *
46
+ * Reads `pid etime args` where the middle field is a `ps` duration. The
47
+ * elapsed field is parsed OPTIMISTICALLY: a listing produced without
48
+ * `etime=` still parses, with the duration reported as `NaN` and the whole
49
+ * second field folded back into the command line, so a caller that lost the
50
+ * column degrades to the old behaviour instead of dropping the process.
51
+ */
36
52
  export declare function parseProcessList(stdout: string, launchableBinary: string): RunningBundleProcess[];
37
53
  /**
38
54
  * Refuses a browser-harness launch when this objdir's app is already alive,
@@ -43,6 +59,25 @@ export declare function parseProcessList(stdout: string, launchableBinary: strin
43
59
  export declare function ensureLaunchableBrowserNotRunning(launchableBinary: string, options?: {
44
60
  killStaleBrowser?: boolean;
45
61
  }): Promise<void>;
62
+ /**
63
+ * Builds the refusal for a running objdir browser.
64
+ *
65
+ * Reports the EVIDENCE rather than a bare PID: the elapsed time and the
66
+ * command line are what let an operator on a shared checkout decide whose
67
+ * process it is, without reconstructing PID ancestry by hand. A downstream
68
+ * fork hit exactly this — the message named a PID belonging to a peer
69
+ * session's live browser and offered a flag that would have killed a
70
+ * multi-hour evaluation run.
71
+ *
72
+ * So `--kill-stale-marionette` is offered ONLY for a marionette-driven
73
+ * browser. For a bare launch the message says what it found and stops:
74
+ * FireForge cannot tell a dead run's orphan from someone's live window, and
75
+ * the flag is the right MANUAL clear once ownership is verified, never an
76
+ * automatic one.
77
+ *
78
+ * Exported for direct unit testing.
79
+ */
80
+ export declare function describeRunningBundleRefusal(holder: RunningBundleProcess): string;
46
81
  /**
47
82
  * Probes whether the Marionette port is currently bound by a
48
83
  * listener. The probe is best-effort: missing tooling returns
@@ -2,38 +2,33 @@
2
2
  /**
3
3
  * Marionette port probe.
4
4
  *
5
- * Gecko's Marionette control channel binds `127.0.0.1:2828` when a
6
- * Firefox / ForgeFresh / fork instance is launched with
7
- * `-marionette`. The `fireforge test` harness spawns the browser with
8
- * that flag, so any test run needs the port to be free at start.
5
+ * Gecko's Marionette control channel binds `127.0.0.1:2828` when a browser
6
+ * is launched with `-marionette`. The `fireforge test` harness spawns the
7
+ * browser with that flag, so any test run needs the port free at start.
9
8
  *
10
- * Motivating case (2026-04-21 eval, Finding #20): an interrupted
11
- * `fireforge test --headless` left an orphan
12
- * `ForgeFresh.app/Contents/MacOS/forgefresh -marionette` process
13
- * listening on 2828 with parent PID 1. The next `fireforge test` run
14
- * — in a *different* FireForge project — failed immediately with a
15
- * Marionette bind error, and FireForge's generic "re-run build" hint
16
- * did not mention the stale listener. The probe in this module runs
17
- * before every test launch, detects when the port is held, and —
18
- * when the holder is a browser process (by command-line `-marionette`
19
- * flag or by basename matching a known browser binary) — throws a
20
- * targeted `GeneralError` naming the PID and the exact `kill` command
21
- * to run.
9
+ * An interrupted `fireforge test` can leave an orphan browser process
10
+ * listening on 2828 with parent PID 1, and the next test run — possibly in a
11
+ * *different* FireForge project — then fails with a bare Marionette bind
12
+ * error. This probe runs before every test launch, detects when the port is
13
+ * held, and — when the holder is a browser process (by command-line
14
+ * `-marionette` flag or by basename matching a known browser binary) —
15
+ * throws a targeted `GeneralError` naming the PID and the exact `kill`
16
+ * command to run.
22
17
  *
23
18
  * Cross-platform implementation:
24
19
  * - POSIX (macOS / Linux): `lsof -i tcp:<port> -P -n -sTCP:LISTEN`
25
- * - Windows: `Get-NetTCPConnection` via PowerShell, then
26
- * `Get-Process` to resolve the PID into a command line.
20
+ * - Windows: `Get-NetTCPConnection` via PowerShell, then `Get-Process` to
21
+ * resolve the PID into a command line.
27
22
  *
28
- * Both paths tolerate missing tooling: if `lsof` / PowerShell isn't
29
- * installed, the probe returns `{ inUse: false }` rather than failing
30
- * the test run itself — the probe is a best-effort friendliness
31
- * check, not a prerequisite.
23
+ * Both paths tolerate missing tooling: if `lsof` / PowerShell is not
24
+ * installed the probe returns `{ inUse: false }` rather than failing the
25
+ * test run — it is a best-effort friendliness check, not a prerequisite.
32
26
  */
33
27
  import { GeneralError } from '../errors/base.js';
34
28
  import { toError } from '../utils/errors.js';
35
29
  import { getPlatform } from '../utils/platform.js';
36
30
  import { exec } from '../utils/process.js';
31
+ import { formatPsDuration, parsePsDuration } from '../utils/ps-duration.js';
37
32
  /** Default Marionette control port set by `-marionette`. */
38
33
  export const DEFAULT_MARIONETTE_PORT = 2828;
39
34
  /** Basenames of browser binaries that ship a Marionette listener. */
@@ -57,7 +52,8 @@ async function findRunningBundleProcesses(launchableBinary) {
57
52
  if (process.platform === 'win32') {
58
53
  const escapedPath = launchableBinary.replaceAll("'", "''");
59
54
  const script = `Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -eq '${escapedPath}' } | ` +
60
- 'ForEach-Object { Write-Output ("$($_.ProcessId) $($_.CommandLine)") }';
55
+ 'ForEach-Object { Write-Output ("$($_.ProcessId) ' +
56
+ '$([int]((Get-Date) - $_.CreationDate).TotalSeconds)s $($_.CommandLine)") }';
61
57
  const result = await exec('powershell.exe', [
62
58
  '-NoProfile',
63
59
  '-NonInteractive',
@@ -66,24 +62,44 @@ async function findRunningBundleProcesses(launchableBinary) {
66
62
  ]);
67
63
  return parseProcessList(result.stdout, launchableBinary);
68
64
  }
69
- const result = await exec('ps', ['-axo', 'pid=,args=']);
65
+ // `etime=` is what makes the refusal attributable on a shared checkout:
66
+ // a browser that started as YOUR run was finishing is not yours.
67
+ const result = await exec('ps', ['-axo', 'pid=,etime=,args=']);
70
68
  return parseProcessList(result.stdout, launchableBinary);
71
69
  }
72
70
  catch {
73
71
  return [];
74
72
  }
75
73
  }
76
- /** Pure parser kept exported so platform output handling is regression-testable. */
74
+ /**
75
+ * Pure parser kept exported so platform output handling is
76
+ * regression-testable.
77
+ *
78
+ * Reads `pid etime args` where the middle field is a `ps` duration. The
79
+ * elapsed field is parsed OPTIMISTICALLY: a listing produced without
80
+ * `etime=` still parses, with the duration reported as `NaN` and the whole
81
+ * second field folded back into the command line, so a caller that lost the
82
+ * column degrades to the old behaviour instead of dropping the process.
83
+ */
77
84
  export function parseProcessList(stdout, launchableBinary) {
78
85
  const matches = [];
79
86
  for (const line of stdout.split(/\r?\n/)) {
80
- const match = /^\s*(\d+)\s+(.+)$/.exec(line);
81
- if (!match?.[1] || !match[2]?.includes(launchableBinary))
87
+ const match = /^\s*(\d+)\s+(\S+)\s+(.+)$/.exec(line);
88
+ if (!match?.[1])
82
89
  continue;
83
90
  const pid = Number(match[1]);
84
91
  if (!Number.isSafeInteger(pid) || pid <= 0 || pid === process.pid)
85
92
  continue;
86
- matches.push({ pid, commandLine: match[2] });
93
+ const maybeElapsed = match[2] ?? '';
94
+ const elapsedSeconds = parsePsDuration(maybeElapsed);
95
+ // Without an `etime` column the second field is the head of the command
96
+ // line, so put it back rather than swallowing it.
97
+ const commandLine = Number.isNaN(elapsedSeconds)
98
+ ? `${maybeElapsed} ${match[3] ?? ''}`.trim()
99
+ : (match[3] ?? '');
100
+ if (!commandLine.includes(launchableBinary))
101
+ continue;
102
+ matches.push({ pid, commandLine, elapsedSeconds });
87
103
  }
88
104
  return matches;
89
105
  }
@@ -120,12 +136,61 @@ export async function ensureLaunchableBrowserNotRunning(launchableBinary, option
120
136
  throw new GeneralError(`A browser from this objdir is still running (PID ${holder.pid}), but FireForge could not terminate it: ${toError(error).message}`);
121
137
  }
122
138
  }
139
+ throw new GeneralError(describeRunningBundleRefusal(holder));
140
+ }
141
+ /**
142
+ * True when the process was launched BY a test harness — its command line
143
+ * carries `-marionette` (the control channel every `fireforge test` browser
144
+ * is started with) or a harness `-profile`.
145
+ *
146
+ * This is the distinction the refusal turns on. The objdir-bundle probe
147
+ * matches any process running this project's binary, which on a shared
148
+ * checkout includes a peer session's LIVE browser and a developer's own
149
+ * interactive one. Only a marionette-driven browser is safely
150
+ * attributable to an interrupted run.
151
+ */
152
+ function isHarnessDrivenBrowser(commandLine) {
153
+ return /\s-marionette(?:\s|$)/.test(commandLine) || /\s-profile(?:\s|=)/.test(commandLine);
154
+ }
155
+ /**
156
+ * Builds the refusal for a running objdir browser.
157
+ *
158
+ * Reports the EVIDENCE rather than a bare PID: the elapsed time and the
159
+ * command line are what let an operator on a shared checkout decide whose
160
+ * process it is, without reconstructing PID ancestry by hand. A downstream
161
+ * fork hit exactly this — the message named a PID belonging to a peer
162
+ * session's live browser and offered a flag that would have killed a
163
+ * multi-hour evaluation run.
164
+ *
165
+ * So `--kill-stale-marionette` is offered ONLY for a marionette-driven
166
+ * browser. For a bare launch the message says what it found and stops:
167
+ * FireForge cannot tell a dead run's orphan from someone's live window, and
168
+ * the flag is the right MANUAL clear once ownership is verified, never an
169
+ * automatic one.
170
+ *
171
+ * Exported for direct unit testing.
172
+ */
173
+ export function describeRunningBundleRefusal(holder) {
123
174
  const killHint = process.platform === 'win32'
124
175
  ? `Stop-Process -Id ${holder.pid} -Force`
125
176
  : `kill ${holder.pid} # or "kill -9 ${holder.pid}" if it doesn't exit`;
126
- throw new GeneralError(`A browser from this project's objdir is already running (PID ${holder.pid}). ` +
127
- 'It may have survived a previously timed-out or interrupted "fireforge test" run and can wedge the next headed launch. ' +
128
- `Stop it with "${killHint}", or retry with "--kill-stale-marionette".`);
177
+ const elapsed = formatPsDuration(holder.elapsedSeconds);
178
+ const age = elapsed !== undefined ? ` running for ${elapsed}` : '';
179
+ const harnessDriven = isHarnessDrivenBrowser(holder.commandLine);
180
+ const evidence = `A browser from this project's objdir is already running (PID ${holder.pid}${age}).\n` +
181
+ ` command: ${holder.commandLine}\n`;
182
+ if (harnessDriven) {
183
+ return (evidence +
184
+ ' This one carries harness arguments (-marionette/-profile), so it is very likely an orphan ' +
185
+ 'of a timed-out or interrupted "fireforge test" run, and it can wedge the next headed launch.\n' +
186
+ ` Stop it with "${killHint}", or retry with "--kill-stale-marionette".`);
187
+ }
188
+ return (evidence +
189
+ ' This one carries NO harness arguments (-marionette/-profile), so it is not a test browser — ' +
190
+ "it is more likely a developer's own window, or another session sharing this checkout.\n" +
191
+ ' "--kill-stale-marionette" is deliberately NOT suggested here: FireForge cannot tell an orphan ' +
192
+ "from someone else's live browser. Confirm the owner (start time above, and the command line), " +
193
+ `then stop it yourself with "${killHint}" if it is in fact yours.`);
129
194
  }
130
195
  /**
131
196
  * Returns `true` when the holder's command basename or command-line
@@ -46,17 +46,13 @@ export declare function runMarionettePreflight(engineDir: string, options?: Mari
46
46
  export declare function reportMarionettePreflight(result: MarionettePreflightResult): void;
47
47
  /**
48
48
  * Formats the PASS/FAIL banner as a plain string for direct
49
- * `process.stdout.write` use — bypasses the clack logger entirely so
50
- * operators running `fireforge test --doctor` under a non-TTY (pipe,
51
- * CI, `tee`-wrapped capture) always see the final line even when the
52
- * clack renderer swallows trailing log output just before process exit.
53
- *
54
- * 2026-04-24 eval Finding 7 reproducibly captured only the `"Running
55
- * marionette preflight..."` intro and no PASS line at all — the
56
- * `success()` + `outro()` + direct `stdout.write` belt-and-suspenders
57
- * we used to ship still lost the summary under some non-TTY flush
58
- * races. Returning the raw string here lets the caller compose a single
59
- * authoritative write without any clack layer between the probe and
60
- * the captured log.
49
+ * `process.stdout.write` use — bypassing the clack logger entirely so
50
+ * operators running `fireforge test --doctor` under a non-TTY (pipe, CI,
51
+ * `tee`-wrapped capture) always see the final line. Clack's renderer can
52
+ * swallow trailing log output just before process exit, and a
53
+ * `success()` + `outro()` + direct-write combination still loses the
54
+ * summary under some non-TTY flush races. Returning the raw string lets the
55
+ * caller compose a single authoritative write with no clack layer between
56
+ * the probe and the captured log.
61
57
  */
62
58
  export declare function formatMarionettePreflightLine(result: MarionettePreflightResult): string;
@@ -19,7 +19,9 @@ import { tmpdir } from 'node:os';
19
19
  import { join } from 'node:path';
20
20
  import { toError } from '../utils/errors.js';
21
21
  import { pathExists } from '../utils/fs.js';
22
- import { info, warn } from '../utils/logger.js';
22
+ import { info, verbose, warn } from '../utils/logger.js';
23
+ import { installGracefulShutdownForwarder, trackChildClosure } from '../utils/process.js';
24
+ import { killProcessTree, sweepProcessGroup } from '../utils/process-group.js';
23
25
  import { ensureMach } from './mach.js';
24
26
  import { getPython } from './mach-python.js';
25
27
  /** Marionette's default TCP port when the browser is launched with `--marionette`. */
@@ -69,7 +71,7 @@ export async function runMarionettePreflight(engineDir, options = {}) {
69
71
  const elapsed = () => Date.now() - startedAt;
70
72
  // Layer 1: engine directory exists.
71
73
  if (!(await pathExists(engineDir))) {
72
- return fail('engine-present', 'Engine directory not found — run "fireforge download" first.', elapsed());
74
+ return fail('engine-present', 'Engine directory not found. Run "fireforge download" first.', elapsed());
73
75
  }
74
76
  // Layer 2: mach binary resolves in the engine.
75
77
  try {
@@ -96,6 +98,19 @@ export async function runMarionettePreflight(engineDir, options = {}) {
96
98
  }
97
99
  let child;
98
100
  let stderrTail = '';
101
+ // Shutdown contract for a child spawned outside the exec layer.
102
+ //
103
+ // This is the repo's one legitimate raw `spawn`: the preflight needs the
104
+ // live `ChildProcess` mid-run (`hasChildExited` gates the socket poll),
105
+ // returns while the browser is still running, and aborts on a TCP byte
106
+ // rather than on a deadline — none of which any wrapper in utils/process
107
+ // can express. What it does NOT get to skip is the contract every wrapper
108
+ // applies to a detached child: register the closure so the bin signal
109
+ // handler waits for it, and forward the parent's SIGINT/SIGTERM to the
110
+ // group. Without both, Ctrl+C during `fireforge test --doctor` left the
111
+ // whole mach → Firefox tree running.
112
+ let closure;
113
+ let forwarder;
99
114
  try {
100
115
  // Layer 5: browser spawns and does not crash within the settle window.
101
116
  try {
@@ -113,13 +128,12 @@ export async function runMarionettePreflight(engineDir, options = {}) {
113
128
  stdio: ['ignore', 'ignore', 'pipe'],
114
129
  // `detached: true` puts the child in a new process group so we can
115
130
  // signal it and every descendant (Firefox, its helpers) via
116
- // `process.kill(-pid, …)` in the finally block. Without this, the
131
+ // `process.kill(-pid, …)` in the finally block. Without it, the
117
132
  // child is Python running mach; a SIGTERM kills Python but the
118
133
  // Firefox grandchild inherits the stderr pipe FD and keeps Node's
119
- // event loop alive even after the preflight PASS log. The symptom
120
- // is `fireforge test --doctor` printing `Marionette preflight:
121
- // PASS` and then hanging indefinitely in `uv__io_poll` — see the
122
- // eval finding.
134
+ // event loop alive even after the preflight PASS log, so
135
+ // `fireforge test --doctor` prints `Marionette preflight: PASS`
136
+ // and then hangs indefinitely in `uv__io_poll`.
123
137
  detached: true,
124
138
  });
125
139
  }
@@ -127,6 +141,10 @@ export async function runMarionettePreflight(engineDir, options = {}) {
127
141
  return fail('browser-spawns', `Could not spawn mach run: ${toError(error).message}`, elapsed());
128
142
  }
129
143
  const spawnedChild = child;
144
+ closure = trackChildClosure();
145
+ forwarder = installGracefulShutdownForwarder(spawnedChild, 500, (signal) => {
146
+ killProcessGroup(spawnedChild, signal);
147
+ });
130
148
  child.stderr?.on('data', (data) => {
131
149
  const chunk = data.toString();
132
150
  stderrTail = (stderrTail + chunk).slice(-STDERR_TAIL_LIMIT);
@@ -164,29 +182,60 @@ export async function runMarionettePreflight(engineDir, options = {}) {
164
182
  `stderr tail: ${stderrTail.trim().slice(-2_000) || '(empty)'}`, elapsed());
165
183
  }
166
184
  finally {
167
- if (child && !hasChildExited(child)) {
168
- killProcessGroup(child, 'SIGTERM');
169
- // Small escalation: if the child doesn't honour SIGTERM quickly, SIGKILL
170
- // so we don't leave a ghost mach process around after a failed probe.
171
- await delay(500);
172
- if (!hasChildExited(child)) {
173
- killProcessGroup(child, 'SIGKILL');
174
- }
185
+ await teardownPreflightChild(child, forwarder, closure, profileDir);
186
+ }
187
+ }
188
+ /**
189
+ * Deterministic teardown for the preflight's detached child.
190
+ *
191
+ * Extracted from {@link runMarionettePreflight} so the probe body stays
192
+ * inside the per-function complexity budget alongside the shutdown contract
193
+ * (forwarder + closure tracking + group sweep) and the kill escalation.
194
+ *
195
+ * @param child - The spawned child, if the spawn got that far
196
+ * @param forwarder - Parent-signal forwarder to dispose
197
+ * @param closure - Closure registration to settle
198
+ * @param profileDir - Temporary profile directory to remove
199
+ */
200
+ async function teardownPreflightChild(child, forwarder, closure, profileDir) {
201
+ // Dispose before killing: the forwarder's own escalation timer must not
202
+ // race the deterministic teardown below.
203
+ forwarder?.dispose();
204
+ const groupPid = child?.pid;
205
+ if (child && !hasChildExited(child)) {
206
+ killProcessGroup(child, 'SIGTERM');
207
+ // Small escalation: if the child doesn't honour SIGTERM quickly, SIGKILL
208
+ // so we don't leave a ghost mach process around after a failed probe.
209
+ await delay(500);
210
+ if (!hasChildExited(child)) {
211
+ killProcessGroup(child, 'SIGKILL');
175
212
  }
176
- // Destroy the stderr pipe explicitly. Firefox (a grandchild of the Python
177
- // mach wrapper we spawned) can inherit and hold the stderr FD even after
178
- // its direct parent exits — until the pipe closes, Node's readable
179
- // stream stays attached and `uv__io_poll` keeps the event loop alive.
180
- // `destroy()` closes the local end regardless, so `fireforge test
181
- // --doctor` exits cleanly after a passing preflight.
182
- child?.stderr?.destroy();
213
+ }
214
+ // Reap anything that outlived the group signal. The file's own comments
215
+ // worry about exactly this — Firefox grandchildren surviving the Python
216
+ // mach wrapper — and every exec-layer wrapper sweeps for it.
217
+ if (groupPid !== undefined && process.platform !== 'win32') {
183
218
  try {
184
- await rm(profileDir, { recursive: true, force: true });
219
+ await sweepProcessGroup(groupPid);
185
220
  }
186
221
  catch (error) {
187
- warn(`Could not clean up marionette preflight profile: ${toError(error).message}`);
222
+ verbose(`Marionette preflight group sweep failed: ${toError(error).message}`);
188
223
  }
189
224
  }
225
+ closure?.settle();
226
+ // Destroy the stderr pipe explicitly. Firefox (a grandchild of the Python
227
+ // mach wrapper we spawned) can inherit and hold the stderr FD even after
228
+ // its direct parent exits — until the pipe closes, Node's readable
229
+ // stream stays attached and `uv__io_poll` keeps the event loop alive.
230
+ // `destroy()` closes the local end regardless, so `fireforge test
231
+ // --doctor` exits cleanly after a passing preflight.
232
+ child?.stderr?.destroy();
233
+ try {
234
+ await rm(profileDir, { recursive: true, force: true });
235
+ }
236
+ catch (error) {
237
+ warn(`Could not clean up marionette preflight profile: ${toError(error).message}`);
238
+ }
190
239
  }
191
240
  /**
192
241
  * Sends `signal` to the child's whole process group when possible, falling
@@ -195,22 +244,11 @@ export async function runMarionettePreflight(engineDir, options = {}) {
195
244
  * `kill(-pid, …)` is not a supported kernel primitive).
196
245
  */
197
246
  function killProcessGroup(child, signal) {
198
- if (child.pid !== undefined && process.platform !== 'win32') {
199
- try {
200
- process.kill(-child.pid, signal);
201
- return;
202
- }
203
- catch {
204
- // ESRCH / EPERM — fall through to the narrow kill below so we at
205
- // least signal the direct child.
206
- }
207
- }
208
- try {
209
- child.kill(signal);
210
- }
211
- catch {
212
- // Already exited — nothing to do.
213
- }
247
+ // Delegates to the shared implementation, which is a strict superset of
248
+ // the hand-rolled one this replaced: same POSIX `kill(-pid)` with the same
249
+ // fall-through to a direct `child.kill`, plus a Windows `taskkill /T /F`
250
+ // branch and an already-exited early return.
251
+ killProcessTree(child, signal, process.platform !== 'win32');
214
252
  }
215
253
  function fail(layer, message, durationMs) {
216
254
  return {
@@ -291,18 +329,14 @@ export function reportMarionettePreflight(result) {
291
329
  }
292
330
  /**
293
331
  * Formats the PASS/FAIL banner as a plain string for direct
294
- * `process.stdout.write` use — bypasses the clack logger entirely so
295
- * operators running `fireforge test --doctor` under a non-TTY (pipe,
296
- * CI, `tee`-wrapped capture) always see the final line even when the
297
- * clack renderer swallows trailing log output just before process exit.
298
- *
299
- * 2026-04-24 eval Finding 7 reproducibly captured only the `"Running
300
- * marionette preflight..."` intro and no PASS line at all — the
301
- * `success()` + `outro()` + direct `stdout.write` belt-and-suspenders
302
- * we used to ship still lost the summary under some non-TTY flush
303
- * races. Returning the raw string here lets the caller compose a single
304
- * authoritative write without any clack layer between the probe and
305
- * the captured log.
332
+ * `process.stdout.write` use — bypassing the clack logger entirely so
333
+ * operators running `fireforge test --doctor` under a non-TTY (pipe, CI,
334
+ * `tee`-wrapped capture) always see the final line. Clack's renderer can
335
+ * swallow trailing log output just before process exit, and a
336
+ * `success()` + `outro()` + direct-write combination still loses the
337
+ * summary under some non-TTY flush races. Returning the raw string lets the
338
+ * caller compose a single authoritative write with no clack layer between
339
+ * the probe and the captured log.
306
340
  */
307
341
  export function formatMarionettePreflightLine(result) {
308
342
  const status = result.ok ? 'PASS' : 'FAIL';