@hominis/fireforge 0.21.4 → 0.22.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.
package/CHANGELOG.md CHANGED
@@ -1,792 +1,109 @@
1
1
  # Changelog
2
2
 
3
- ## 0.21.0
4
-
5
- ### Features
6
-
7
- - **Chrome-doc previews and cleanup.** `furnace chrome-doc create` now supports `--dry-run`, validating the same target files and jar registrations without writing. New `furnace chrome-doc remove <name>` removes scaffolded chrome-doc files, jar entries, and optional xpcshell packaging-test directories, with `--dry-run` and `--yes` support.
8
- - **Versioned `status --json` schema.** The JSON output is now an object with `schemaVersion`, `summary`, and `files` instead of a bare array. Error paths also emit versioned JSON objects with `code` and `error`.
9
- - **Configurable patch queue policy.** Projects can now opt into `fireforge.json#patchPolicy` to define category-owned numeric ranges, reserved exception ranges, filename capture patterns, description requirements, gap policy, and mutation enforcement mode. FireForge enforces the policy during export/re-export/reorder/rename projections and reports the same findings from `verify` and `lint --per-patch`.
10
-
11
- ### Hardening
3
+ ## 0.22.0
12
4
 
13
- - **Eval 0.21.0 release-gate fixes.** `export --dry-run` now performs the same supersede and cross-patch ownership checks as real export before calling a plan safe; `furnace deploy --dry-run` validates successful custom-component plans against projected jar.mn registrations; generated Furnace components and browser-chrome test scaffolds are strict-checkJs and lazy-custom-element ready; chrome-doc packaging xpcshell tests no longer trip component-orphan validation; supported optional config keys such as `firefox.sha256` print `(not set)` when absent; and Furnace manifest writes preserve existing top-level/component ordering while appending new entries predictably.
14
- - **Sparse export insertion before reserved ranges.** `fireforge export --order <N>` now creates the new patch at that exact unused order without renumbering later patches, so policy-owned queues can add `241-ui-new-feature.patch` while preserving exact reserved exceptions such as `900-infra-bindgen-basic-string-workaround.patch`. Positional `--before` / `--after` insertion still renumbers, but now refuses with a sparse `--order` suggestion when it would move a reserved patch.
15
- - **V1 readiness audit follow-ups.** `fireforge token coverage` now validates dirty/untracked Furnace token CSS as a token source file instead of ignoring it or counting its expected literal values as raw-color coverage debt. `furnace create --compose <tag>` auto-registers discovered engine widgets into `furnace.json#stock` in the same transaction as the new custom component, so a prior non-interactive `furnace scan` report is enough for compose authoring. `fireforge lint --max-warnings <n>` lets release gates enforce a warning budget (for example `fireforge lint --per-patch --max-warnings 0`) while keeping warnings advisory by default. README guidance now covers first-module `browser/modules/<binaryName>/moz.build` setup, direct `fireforge` binary equivalents for shells without `npx`, Watchman PATH expectations, and the intentionally narrow `patch tier --tier branding` surface.
16
- - **Override removal demotes back to stock.** Removing a Furnace override restores engine files, deletes the override workspace, clears override checksums, and re-adds the component to `stock` tracking instead of dropping it from `furnace.json`. Optional Furnace config fields, including `platformPrefixes`, are preserved across the write.
17
- - **Rename updates browser-chrome test bodies.** `furnace rename` now rewrites generated browser-chrome mochitest contents as well as filenames and `browser.toml`, preventing stale `waitForElement("<old>")` references after a component rename.
18
- - **UI build preflight is stricter.** `fireforge build --ui` now refuses before `mach build faster` when the current objdir lacks a completed launchable bundle, guiding fresh imports and partial builds through a full `fireforge build` first.
19
- - **Interrupt and diagnostics polish.** Signal-driven Furnace preview teardown has regression coverage for stale lock cleanup, `chrome-doc-rollback` markers round-trip through Furnace state validation, watch-mode permission failures name the macOS privacy remediation, and `test --doctor` prints the probed objdir, binary/app path, port, and elapsed time.
5
+ - Added `doctor --clear-resolution` with verify-backed safety checks.
6
+ - Shared patch queue health checks between `verify` and doctor recovery.
7
+ - Improved Furnace repair for empty custom orphan directories.
8
+ - Enforced patch policy during `patch compact`.
9
+ - Shortened README and changelog into maintainer-facing docs.
20
10
 
21
- ### Documentation
11
+ ## 0.21.0
22
12
 
23
- - **README Storybook first-run and audit posture.** The Furnace preview docs now call out the upstream Storybook npm install and audit output as Firefox Storybook workspace dependency state, not FireForge package dependency state.
24
- - **README — chrome-doc lifecycle and JSON schema.** The Furnace and status sections document chrome-doc dry-runs/removal, UI-build preconditions, watch privacy guidance, and the new versioned `status --json` object.
13
+ - Added chrome-doc dry-runs and cleanup.
14
+ - Added versioned `status --json` output.
15
+ - Added configurable patch queue policy.
16
+ - Hardened export, Furnace deploy, and UI build preflights.
17
+ - Improved Furnace rename, override removal, and interrupt diagnostics.
25
18
 
26
19
  ## 0.20.0
27
20
 
28
- ### Features
29
-
30
- - **Pinned Firefox archive verification.** `FirefoxConfig` now accepts optional `firefox.sha256`, and `fireforge config firefox.sha256 <digest>` is a supported config path. The digest must be a 64-character SHA-256 hex string; FireForge verifies both cached and freshly downloaded source archives against it before extraction, so reproducible workflows can fail fast on a mismatched upstream archive instead of discovering the problem after engine mutation has begun.
31
- - **`fireforge patch compact`.** Patch queues with ordinal gaps after deletes, splits, or manual repair can now be compacted back to contiguous numbering under the existing patch manifest lock. The command supports dry-run previews, confirmation/`--yes`, lock-time recomputation, history entries, and the same two-phase rename/rollback safety as other patch renumbering paths.
32
- - **Furnace xpcshell scaffolding and rename helpers.** Furnace test scaffolding now covers xpcshell packaging probes for storage/module-loading code paths, and rename logic is split into focused helpers for component filenames and config rewrites so custom and override renames share the same behaviour.
33
-
34
- ### Hardening
35
-
36
- - **`fireforge download` is locked end-to-end.** The command now takes a project-level `.fireforge/` sidecar lock around the full engine mutation sequence: engine existence checks, forced removal, source download/extraction, git init or resume, patch cleanup, and state updates. Parallel invocations queue with explicit timeout/stale-lock messaging instead of racing over `engine/`.
37
- - **Firefox archive cache mutation is locked per archive.** Cache validation, invalidation, download promotion, and metadata writes now run under a per-archive cache lock. A failed downloader removes only its own unique `.part-*` file unless it already promoted the tarball, so a failing peer can no longer delete another process's valid final cache entry.
38
- - **Download stream timers clean up on all terminal paths.** The stall detector now clears its timer from `destroy(error, callback)` as well as normal `flush`, avoiding late timer errors and unnecessary event-loop retention after pipeline failures.
39
- - **Relative import linting is AST-backed.** `fireforge lint` now uses Acorn/ESTree traversal to detect relative ES imports, side-effect imports, dynamic `import()`, relative re-exports, and `ChromeUtils` / `Cu.import` calls, with a narrow stripped-text fallback only when parsing fails. This closes the false negatives left by the old regex pass while keeping malformed-file diagnostics useful.
40
- - **Cross-platform process and lock probes are stricter.** `findExecutable` now parses Windows `where` output with CRLF-safe per-line trimming and returns the first non-empty candidate. File-lock stale recovery treats only `ESRCH` from `process.kill(pid, 0)` as dead; `EPERM` and unknown errors are treated as live/unknown so FireForge does not reclaim a lock owned by another live process.
41
- - **Filesystem writes get low-risk durability checks.** `writeFileAtomic` preserves the existing mode-preservation behaviour and now best-effort fsyncs the parent directory after rename where the platform supports directory handles. `pathExistsStrict` is available for user-facing probes that should surface `EACCES` / `EPERM` instead of collapsing them into "missing".
42
- - **Furnace mutations use fresh locked state.** `furnace create`, `furnace remove`, and `furnace rename` re-read `furnace.json` inside the mutation lock before validation and writeback, preserving sibling entries created by concurrent commands instead of writing back stale outer snapshots. `furnace remove` also shares cleanup state for custom browser-chrome, xpcshell, and MochiKit scaffolds.
43
- - **Furnace refresh distinguishes conflicts from fatal merge failures.** `git merge-file` result handling now treats normal conflict exits separately from fatal/error output or high exit codes. `furnace refresh --all` continues through later overrides after a per-component failure, reports the failed count, and exits non-zero with the failed override names once the rest of the selection has been attempted.
44
- - **Shared naming and coverage drift guards.** Export placement now reuses `sanitizeName` from patch export instead of maintaining a duplicate slug helper, and the dedicated coverage-threshold script now enrols newly critical modules including Furnace refresh, patch compact, and Furnace xpcshell rename handling.
45
-
46
- ### Documentation
47
-
48
- - **README — download locks and pinned archive checksums.** The quick-start and configuration sections now describe the project download lock, per-archive cache lock, and `firefox.sha256` workflow.
49
- - **README — concurrent Furnace mutations.** The Furnace section now documents locked fresh-state writeback for create/remove/rename and the `refresh --all` failure summary behaviour.
21
+ - Added pinned Firefox archive checksums.
22
+ - Added `fireforge patch compact`.
23
+ - Added Furnace xpcshell scaffolding.
24
+ - Locked download and archive-cache mutation paths.
25
+ - Hardened atomic writes, stale locks, and Furnace refresh.
50
26
 
51
27
  ## 0.19.0
52
28
 
53
- ### Features
54
-
55
- - **`patchLint.checkJsStrict` and `patchLint.checkJsCompilerOptions`.** The patch-lint `checkJs` pass now defaults to the historical loose preset (`strict: false`, `noImplicitAny: false`). Set `"patchLint": { "checkJs": true, "checkJsStrict": true }` to enforce `strict` and `noImplicitAny` on patch-owned `.sys.mjs` so implicit-any parameters surface as `checkjs-type-error`, aligning with strict whole-project checkJs without changing module resolution (`noResolve` and `resource://` suppression unchanged). Optional `checkJsCompilerOptions` (requires `checkJsStrict`) merges allowlisted boolean compiler overrides — for example `{ "strictNullChecks": false }` — after the strict preset for gradual adoption. The Firefox globals shim and `SUPPRESSED_DIAGNOSTIC_CODES` remain shared with `fireforge typecheck` via `typecheck-shim.ts`.
56
- - **`checkJs` ambient modules for Firefox URL imports.** The shared shim in `typecheck-shim.ts` now includes shorthand `declare module 'resource:*'` and `declare module 'chrome:*'`, matching typical `resource://…` / `chrome://…` specifiers without the broken `Record` + `export=` shape under `moduleResolution` Bundler (which typed dynamic imports as `{ default: … }` and broke `.namedExport` access). Bulk `fireforge re-export` with `patchLint.checkJs` no longer routinely needs `--skip-lint` for `.sys.mjs` queues that lazily URL-import storage and infra helpers.
57
-
58
- ### Hardening
59
-
60
- - **`modified-file-missing-header` — standard Mozilla MPL-2.0 block headers with wrapped line breaks.** The upstream fallback scan required a contiguous `Mozilla Public License` substring in the first few lines, so files that follow Mozilla’s usual `/* … Mozilla Public` / ` * License, v. 2.0 … */` wrap (including after Emacs/vim directive blocks) were warned despite a valid notice. `containsUpstreamLicenseText` in `src/core/license-headers.ts` now normalizes common block-comment continuation prefixes before matching, so forks need not add SPDX solely to satisfy patch lint.
61
- - **`fireforge test` — `--marionette-port` auto-forward matches toolkit mochitests and mixed suites.** Auto-forward of `--setpref=marionette.port=<n>` previously keyed off a path heuristic that missed `toolkit/content/tests/**` widget HTML tests (no `/mochitest/` segment), so the preflight could use the operator’s port while mach still defaulted to **2828**. Forwarding now runs whenever `--marionette-port` is set unless `--mach-arg` explicitly includes `--flavor=xpcshell` / `xpcshell-tests` (the pref is unused there). `isMarionetteFlavor` also treats `toolkit/content/tests/` paths as Marionette-relevant unless they sit under `/tests/xpcshell/`, for consistency with other callers of that helper.
62
-
63
- ### Compatibility
64
-
65
- - **`furnace create --with-tests` defaults to `browser-chrome` again** (not `mochikit`, which was the default after the `--test-style` split). Forks whose top-level chrome document has no `tabbrowser` should pass **`--test-style=mochikit`** explicitly. On macOS, toolkit MochiKit / mochitest-chrome-style widget tests can idle until ~370s with no subtests; README documents the tradeoff.
66
-
67
- ### Documentation
68
-
69
- - **README — mochitest timeouts vs Marionette.** The Test harness section documents long idle timeouts (~370s, `TEST_END: TIMEOUT`) on fork custom chrome, `--marionette-port` behaviour with xpcshell flavor, and pointers to fork-side prefs and investigation.
70
- - **README — default test harness and macOS mochitest-chrome.** The README sections “Picking a test harness for `furnace create`”, “Test harness options”, and “Known upstream build issues” describe the browser-chrome default, macOS single-process idle timeout, explicit `--test-style=mochikit`, and `--with-tests` + `--xpcshell` resolution.
29
+ - Added stricter patch `checkJs` options.
30
+ - Added ambient `resource:*` and `chrome:*` module shims.
31
+ - Fixed Mozilla licence-header detection.
32
+ - Fixed Marionette port forwarding for mixed test suites.
33
+ - Restored browser-chrome as the default Furnace test harness.
71
34
 
72
35
  ## 0.18.0
73
36
 
74
- ### Compatibility
75
-
76
- - **No re-export required for existing 0.17.0 patches.** Every fix in this release is additive or defensive: the patch body format is unchanged, the `patches.json` schema only gains two optional fields (both already round-tripped through the validator in 0.17.0), and `fireforge import`/`verify` against a 0.17.0-exported queue continues to behave identically. Operators can upgrade in place.
77
- - **No breaking API changes for downstream `import '@hominis/fireforge'` consumers.** `src/core/mach.ts`'s `build()` / `buildUI()` signatures changed from `Promise<number>` to `Promise<MachCommandResult>` so `fireforge build` can annotate the tail of mach's output (see below), but neither function is exported from `src/index.ts` — the public surface stays the same.
78
- - **Minor cosmetic drift on wire-generated code and token coverage percentages.** `fireforge wire` now stamps `// <BINARY>: wire-<step> <name>` marker comments (see below). Patches exported before 0.18.0 that captured the pre-fix `// <name> init — must be first…` form continue to apply cleanly — the wire mutator's idempotency check is keyed on the call expression, not the comment, so re-running `wire` against a 0.17.0-wired tree is still a no-op and will not double up. Operators who want the marker in existing patches can either re-run `wire` after deleting the old block, or hand-edit the patch once. Token coverage percentages shift upward on any project that copies `--moz-*` CSS baselines (now counted as allowlisted platform vars instead of unknown) — CI thresholds pinned to 0.17.0 numbers may need a one-time adjustment.
79
-
80
- ### Hardening
81
-
82
- - **`fireforge lint` (aggregate) + `fireforge export-all` — EISDIR crashes on patches that introduce new directories.** `resolveLintDiff` in `src/commands/lint.ts` and `resolveFurnaceExclusionPolicy` / the `--exclude-furnace` branch in `src/commands/export-all.ts` both blended `getModifiedFiles()` + `getUntrackedFiles()` (or raw `getWorkingTreeStatus()`) before handing the path list to `getDiffForFilesAgainstHead`. `git status --porcelain=v1 -z` collapses untracked directories to `?? browser/modules/<fork>/` entries, so `generateNewFileDiff` inside the diff pass called `readText()` on a directory path and crashed with `EISDIR: illegal operation on a directory, read` — the eval's imported hominis queue and its fresh-project register-wire workflow both hit this. The fix plumbs `expandUntrackedDirectoryEntries` (already present for `status --ownership`) through both call sites so only file paths reach the diff pass, and `getDiffForFilesAgainstHead` in `src/core/git-diff.ts` now runs a belt-and-suspenders expansion via `getUntrackedFilesInDir` before reading any candidate as a file — a single bad call site cannot re-introduce the crash at that layer. `fireforge lint --per-patch` already worked; aggregate mode and `export-all` now do too.
83
- - **`fireforge doctor` — ownership-aware working-tree classification.** `runEngineGitChecks` in `src/commands/doctor.ts` previously emitted a single generic `Engine working tree has <N> local changes` warning for any dirty count and told the operator to `export, discard, or reset them as appropriate`. That advice was destructive on a freshly-imported patch stack (eval: a correctly imported 126-file queue on `hominis` triggered the warning, and the suggested `discard` / `reset` recovery would have dropped the entire import) and spurious on a fresh project with tool-managed branding (eval: 63/64 dirty files after a successful build were classified as `branding` by `fireforge status`, yet `doctor` still told the operator to export / discard them). The new `inspectEngineWorkingTree` helper in `src/commands/doctor-working-tree.ts` (split out to keep `doctor.ts` under max-lines) reuses the shared `classifyFiles` pipeline — same partitioning as `fireforge status --ownership`: `branding` / `furnace` / `patch-backed` / `conflict` / `unmanaged`. Doctor now warns only on the `unmanaged` bucket (and raises a loud distinct warning on `conflict`, with a pointer at `status --ownership` + `verify`); tool-managed buckets pass with an ownership summary (`N tool-managed changes (X patch-backed, Y branding), 0 unmanaged`). A missing config falls back to the legacy count-only warning so a broken project still gets a useful row. Ordering dependency on `fireforge.json is valid` is declared explicitly via `dependsOn`.
84
- - **`fireforge furnace remove` — drops the locale jar.mn chrome registration for localized custom components.** Before this fix, `furnaceRemoveCommand` in `src/commands/furnace/remove.ts` deleted the `.ftl` file and the component directory but never dropped the `locale/@AB_CD@/<chromeSubPath>/<tag>.ftl` entry that `applyCustomFtlFile` wrote during deploy. The eval's `furnace remove moz-eval-chip --yes` left `engine/browser/locales/jar.mn` pointing at the now-missing FTL; `furnace validate` still passed, and later builds/packaging would have tripped over the stale reference. The fix threads the existing `removeCustomFtlJarMnEntry` helper from `src/core/furnace-apply-ftl.ts` into the localized branch of `furnaceRemoveCommand`, snapshot-protected by the same rollback journal that guards the FTL-file removal so a mid-remove failure restores both together. `fireforge furnace validate` after remove now reports a clean state without a follow-up `apply --dry-run` cleanup plan.
85
- - **`fireforge furnace rename` — rewires the locale jar.mn entry on localized renames.** `updateEngineRegistrations` in `src/commands/furnace/rename.ts` renamed the `.ftl` file on disk but left the locale jar.mn pointing at `locale/.../${oldName}.ftl`. `fireforge furnace validate` passed anyway because the validator only checked the toolkit `jar.mn` (for `.mjs`/`.css` entries), not the locale jar.mn — the eval reported "validate passed" for two full minutes while the engine carried a stale registration for a file that no longer existed. The fix, gated on `customConfig.localized`, calls `removeLocaleFtlJarMnEntry(oldName)` + `addLocaleFtlJarMnEntry(newName)` against `resolveFtlLocaleJarMnPath(config.ftlBasePath)` inside the same rollback journal as the FTL file rename, so both changes land together or roll back together. Non-localized renames are unaffected.
86
- - **`fireforge furnace override` — concurrent invocations no longer silently drop each other's manifest entries.** The eval ran two `furnace override` commands in parallel against the same fresh project (one `moz-button`, one `moz-card`): both exited `0`, both override directories landed on disk, but `furnace.json` kept only one entry. Root cause: `performOverrideMutations` acquired the furnace lock via `runFurnaceMutation` but `saveOverrideConfig` wrote back the outer-snapshot config object — each invocation had loaded an empty-overrides state BEFORE entering the lock, and the second writer clobbered the first writer's addition. `saveOverrideConfig` now re-reads fresh furnace.json state inside the lock via `loadAuthoringFurnaceConfig`, splices the new entry plus the stock-bucket removal onto the fresh state, and writes that back — so a sibling writer's addition survives into the final manifest. A concurrent-writer regression test under `furnace-override.test.ts` pins the contract; other furnace mutations (`remove`, `rename`, `deploy`) already re-read inside the lock and need no equivalent fix.
87
- - **`fireforge doctor --repair-furnace` — detects and recovers orphaned override directories.** New `Furnace manifest sync` check in the new `src/commands/doctor-furnace-manifest-sync.ts` module (spliced into `FURNACE_DOCTOR_CHECKS` so `doctor-furnace.ts` stays under max-lines). Without `--repair-furnace` the check warns when `components/overrides/<name>/` or `components/custom/<name>/` exists on disk but is absent from `furnace.json` — the exact residue left by the concurrent-override race above or by any interrupted mutation. With `--repair-furnace`, override orphans are re-registered by reading the `override.json` sidecar `furnace override` writes during the copy phase (`type`, `description`, `basePath`, `baseVersion`, optional `baseCommit`), folding the recovered entry into a fresh furnace.json read, and persisting. Custom orphans are listed for the operator to either `furnace create` against or delete manually, because custom components have no equivalent sidecar — fabricating `targetPath` / `register` / `localized` values is worse than the orphaned directory itself. Doctor now catches the residue from eval 2's concurrent-override reproduction and the symmetric case where an operator SIGINT's a mutation mid-flight.
88
- - **`fireforge build` — annotates mach's trailing `config.status is out of date` notice instead of leaving it to read as build failure.** mach's post-build guard prints `config.status is out of date with respect to browser/moz.configure` when a tool-managed edit (most commonly `setupBranding`'s `patchMozConfigure` on first build) landed on `moz.configure` before the build. The guard runs AFTER `Your build was successful!` so operators reading the tail of a successful build saw contradictory signals — the eval recorded this on both a fresh project's first build and a pre-existing hominis build. `build()` and `buildUI()` in `src/core/mach.ts` now return the captured `MachCommandResult` (stdout/stderr tail + exit code) instead of just an exit code; `buildCommand` in `src/commands/build.ts` scans `result.stdout`/`stderr` for `/config\.status is out of date/i` and, on match, emits a one-line `info(...)` explaining the notice is a known side effect of tool-managed branding edits and does not require a rebuild. Test-command `buildUI` callers in `src/commands/test.ts` pick up the new return shape; no caller outside the CLI sees the signature change (neither function is exported from `src/index.ts`).
89
- - **`fireforge test --doctor` — triple-path PASS footer survives non-TTY clack rendering.** The eval's `test --doctor` captures on both fresh and hominis projects showed `● Running marionette preflight...` and then exited 0 with nothing else — the 0.17.0 `outro(\`Marionette preflight: PASS (...)\`)`was dropped by clack somewhere in the non-TTY render pipeline (exact root cause could not be isolated from captures alone). Rather than chase the rendering bug, the fix triple-writes the result:`success(\`Marionette preflight: PASS (...)\`)`, then `outro('Test completed')`closes the intro frame, then`process.stdout.write(\`${summary}\n\`)`guarantees the line reaches stdout regardless of clack's buffering. A test in`test.test.ts` asserts all three emissions.
90
- - **`fireforge test` — fork-owned module import failures beat the branding stale-build diagnosis.** The eval's hominis xpcshell test failed loading `resource:///modules/hominis/HominisStore.sys.mjs`; FireForge reported "stale build artifacts, run fireforge build --ui" because the harness teardown also printed a branding warning that matched the narrower stale-build pattern. Re-running the build did nothing — the real failure was that the new module wasn't registered in `browser/modules/hominis/moz.build`. `handleNonZeroTestExit` now checks for `Failed to load resource:///modules/<binaryName>/` BEFORE the branding stale-build branch and surfaces a fork-module-specific diagnostic pointing at `browser/modules/<binary>/moz.build` + `fireforge register`, so the operator sees the correct recovery path on the first try. Existing branding-stale diagnosis is untouched.
91
- - **`fireforge token coverage` — `--moz-*` platform variables allowlisted by default; untracked CSS directories now scanned.** Two independent token-coverage fixes:
92
- - **Platform allowlist** — `src/core/token-coverage.ts` seeds a default `platformPrefixes: ['--moz-']` and treats matches as `allowlisted` rather than `unknown`. A CSS-only Furnace override of `moz-button` with one fork token previously reported 1% coverage because the copied upstream baseline referenced 84 `--moz-*` platform vars; coverage now reports 100% on the same diff, and the one unknown was correctly declared by the fork. Forks can override through a new optional `platformPrefixes?: string[]` field on `furnace.json` — extending the list (e.g. `['--moz-', '--in-content-']`) for forks with additional platform prefixes, or setting an explicit empty array to restore the pre-0.18 strict contract. The field is added to the public `FurnaceConfig` type.
93
- - **Untracked-directory expansion** — `src/commands/token-coverage.ts` now reads `getWorkingTreeStatus` + `expandUntrackedDirectoryEntries` instead of `getStatusWithCodes` so CSS files inside a `?? dir/` untracked directory (the common shape for a freshly-imported patch stack adding a new theme tree) show up in the scan. The eval's hominis run saw `?? browser/branding/hominis/content/aboutDialog.css` and `?? browser/themes/shared/hominis-tokens.css` collapse to directory entries that didn't end in `.css`; coverage reported "No modified CSS files". Both CSS files are now scanned.
94
- - **`fireforge patch reorder` + `fireforge patch delete` — accept manifest `name` handle in addition to filename and ordinal.** `resolvePatchIdentifier` in `src/core/patch-manifest-resolve.ts` previously rejected the `name` field even though CLI help says `<name>` and the manifest stores a `name` on every entry. Operators had to copy the full filename from `patches.json` before every queue mutation. The resolver now matches, in order: ordinal number → exact filename → filename-with-`.patch`-appended → `name` field. Filename lookup still wins when a manifest happens to have a `name` identical to a different filename — legacy scripts passing filenames keep working. Error messages on both commands now list filenames AND their `name` counterparts so operators see both forms as valid identifiers.
95
- - **`fireforge patch reorder` — postcondition assert on each Phase 2 rename.** The eval reported that a two-patch swap updated `patches.json` but left the files at their pre-rename names, and `fireforge verify` then failed ENOENT opening the manifest-renamed file. The code path in `renumberPatchesInManifest` (two-phase staging: stage → final + manifest write) appears correct against both the integration test in `patch-mutations.integration.test.ts` and the shipped 0.17.0 dist — but the eval's operation log shows the manifest was updated while the rename was missing, so a filesystem-specific failure mode exists that the existing integration test does not exercise. Defense-in-depth lands a postcondition `pathExists(newFilename)` assert after every `rename()` in Phase 2, so any silent rename failure aborts the reorder before the manifest write and triggers the Phase 2 rollback (which itself reverses completed final-name moves and unwinds the staging). A second integration test reproduces the exact eval scenario (`001-infra` + `002-ui` swap via `--to 1 --yes`) and asserts both filenames exist on disk after completion.
96
- - **`fireforge register --dry-run` — honours the rule-level idempotency decision.** `registerFile` in `src/core/manifest-rules.ts` returned `{ skipped: true }` when the rule's `isRegistered(...)` check found the file already present in its manifest, but `registerCommand` in `src/commands/register.ts` ignored the flag on the dry-run branch and always printed `[dry-run] Would register <path>`. The next real invocation correctly reported `Already registered: <path>`. Automation that consumed the dry-run as a plan mis-predicted unnecessary work. Dry-run now prints `[dry-run] Already registered: <path> in <manifest>` when `result.skipped` is true — mirroring the real-run copy — and otherwise prints the registration plan as before.
97
- - **`fireforge wire` — `// <BINARY>:` marker comments in wire-generated code.** `lintModificationComments` in `src/core/patch-lint.ts` checks JS/MJS additions for a `// <binaryName.toUpperCase()>:` marker (case-insensitive) and flags `missing-modification-comment` on upstream file edits that lack it. `fireforge wire` itself wrote comments like `// <name> init — must be first, before Firefox subsystem` — generic text that trips the patch-lint rule on the first export after wiring, so the eval saw `[missing-modification-comment] browser/base/content/browser-init.js` on content generated by FireForge's own command. Each wire mutator (`addInitAST`/`legacyAddInit`, `addDestroyAST`/`legacyAddDestroy`, `addSubscriptAST`/`legacyAddSubscript`) now accepts a `marker: string` parameter (default `'FIREFORGE:'` for back-compat with programmatic callers); `browser-wire.ts:wireSubscript` computes the project-scoped marker as `${config.binaryName.toUpperCase()}:` from `fireforge.json` and threads it through. Emitted blocks now carry `// FRESHFORGE: wire-init <name> — must be first, …` (or the caller's equivalent uppercased binaryName), satisfying the lint rule on the first export. Patches exported before 0.18.0 that captured the old comment format still apply cleanly — patch content is unchanged — they just won't have the marker; operators who want the marker in existing patches can re-run `wire` after deleting the old block (idempotency is keyed on the call expression, so re-running alone is a no-op). `.inc.xhtml` fragments inserted by `--dom` are preprocessor directives, not JS — `lintModificationComments` only checks JS, so no marker is needed there.
98
- - **`fireforge build --ui` + `fireforge build` — `.inc.xhtml` fragments excluded from the packaging audit.** `isPackageablePath` in `src/core/build-audit.ts` treated `.inc.xhtml` as a `.xhtml` file, so a wire-introduced `browser/base/content/<name>.inc.xhtml` (consumed via `#include` from a registered chrome document and resolved at packaging time) got flagged as "missing packaged artifact" on the next UI build. `register --dry-run` already refuses to register these fragments with explicit guidance — the build audit now matches that contract with an early `sourcePath.endsWith('.inc.xhtml') → false` return. The two sibling checks now agree about this file type.
99
- - **`fireforge register` — empty single-line `EXTRA_JS_MODULES += []` lists expand before insertion.** `tokenizeMozBuildList` in `src/core/manifest-tokenizers.ts` tracked opening `[` and closing `]` on separate lines. A freshly-scaffolded `browser/modules/<fork>/moz.build` that wrote `EXTRA_JS_MODULES += []` on one line returned `null` from the tokenizer because the scanner never saw a line STARTING with `]`, and `registerFireForgeModule` failed with `Could not find EXTRA_JS_MODULES in moz.build` — blocking the documented `browser/modules/<fork>/*.sys.mjs` workflow at the first module. The tokenizer now detects the single-line empty-list form `...+= []` at open time, rewrites `lines[i]` to `...+= [` and splices in a `]` at `i + 1`, and emits matched `list-open` / `list-close` tokens for the canonical multi-line shape. Downstream `findAlphabeticalMozBuildPosition` and the register helpers then place the new entry inside the brackets as normal.
100
- - **`fireforge wire --dry-run` — notice when the subscript file is absent.** The dry-run branch in `src/commands/wire.ts` deliberately skips the subscript-file existence check to support the "wire first, create the file after" scaffolding workflow. Pre-0.18 that deliberate skip produced a plausible plan even when the subscript was genuinely missing, and the real `wire` invocation then refused with `Subscript file not found`. The skip stays (the workflow is legitimate) but dry-run now emits `Note: <subscriptDir>/<name>.js does not exist yet — the real wire command will require it before writing.` so the operator sees the absence in the preview and can create the file before re-running without `--dry-run`.
101
- - **`fireforge status --unmanaged` + `fireforge register` — browser-chrome test files route to browser.toml, not jar.mn.** The browser-content pattern in `src/core/manifest-rules.ts` matched every `browser/base/content/**/*.{js,mjs,xhtml,css}` path, including test implementation files at `browser/base/content/test/<dir>/browser_*.js`. `status --unmanaged` therefore proposed `fireforge register browser/base/content/test/forgeqa/browser_forgeqa_qa_browser.js` which, if run, would cluttered jar.mn with a test-file chrome URI (the right place for those files is the sibling `browser.toml`). The pattern now excludes the `test/` subtree via a negative lookahead, so these paths fall through to `getUnregistrableAdvice`, which already returns the correct browser.toml-centric guidance for individual test files. `status`, `register`, and `register --dry-run` all now agree that test JS belongs in the test manifest.
102
- - **`fireforge export-all --exclude-furnace` — refuses patches that would register a furnace component without carrying its source files; `fireforge verify` now flags dangling registrations that slipped through earlier versions.** Finding 1 (High). The evaluator's fresh-project smoke sequence (`furnace init` → `token add` → `furnace override moz-button -t css-only` → `furnace create moz-qa-panel --localized --with-tests --test-style mochikit` → `furnace deploy` → `export-all --exclude-furnace`) produced a patch whose `toolkit/content/customElements.js`, `toolkit/content/jar.mn`, and `toolkit/locales/jar.mn` hunks registered `moz-qa-panel` even though the widget sources were filtered out. `fireforge verify` reported "Verify clean" for the broken queue. The fix lands in two layers: a new `collectPatchRegistrationReferences` scanner in `src/core/patch-registration-refs.ts` extracts component-shaped registration references from any patch body, and `verifyCommand` in `src/commands/verify.ts` now cross-checks each extracted reference against both the aggregate `filesAffected` coverage and the engine working tree — any miss fires a `dangling-registration` error naming the specific patch + target path. `export-all --exclude-furnace` in `src/commands/export-all.ts` calls the same scanner before writing and refuses with a clear message when the hunks would register a furnace-managed component not included in the patch. Tests in `patch-registration-refs.test.ts` + `verify.integration.test.ts` pin both halves.
103
- - **`fireforge status --unmanaged` — tolerates a missing parent moz.build instead of exiting non-zero.** Finding 2 (Medium). `printUnregisteredWarnings` in `src/commands/status.ts` awaited `Promise.all` on `isFileRegistered` checks, and the manifest-rules layer threw `GeneralError("Manifest not found: ...")` synchronously when the parent `moz.build` was absent — exactly the state an operator hits after scaffolding a new engine module and before running `register`. Status is a read-only reporter; the non-zero exit broke `status --unmanaged` as a discovery tool. The fix wraps each `isFileRegistered` call in a narrowly scoped try/catch, buckets missing-manifest cases into a distinct "registration manifest does not exist yet" warning list, and lets the command exit cleanly. Other error shapes still propagate. A regression test in `status.test.ts` pins the new contract.
104
- - **`fireforge lint --since HEAD --only-introduced` — aggregate patch-size findings tag as `[introduced]` when the diff set is non-empty.** Finding 4 (Low). The `large-patch-files` and `large-patch-lines` rules emit findings with the synthetic `file: '(patch)'` placeholder, which never matched any entry in the `diffFiles` set. `tagLintIssues` in `src/core/patch-lint-diff-tag.ts` therefore tagged them `[cumulative]` even when the aggregate was entirely the operator's own diff — reading as "pre-existing drift" to an operator asking "what did this task introduce?" The tagger now recognises the synthetic placeholder (exported as `AGGREGATE_PATCH_FILE`) and promotes it to `introduced` whenever the diff set is non-empty. Cumulative semantics are preserved for the empty-diff case. Tests in `patch-lint-diff-tag.test.ts` pin both branches.
105
- - **`fireforge furnace remove` / `rename` / `validate` — xpcshell scaffolds are cleaned up, renamed, and flagged as orphans.** Finding 5 (Medium). `furnace create --with-tests --xpcshell` scaffolds at `browser/base/content/test/<binary>-xpcshell/<name>/`, but the pre-0.18.1 `furnace remove` only touched the sibling browser-mochitest tree and `furnace rename` never reached the scaffold at all. Operators who ran create → rename → remove were left with an orphan scaffold whose filenames still referenced the original pre-rename component. The path template is now centralised in `xpcshellTestParentDir` in `src/core/furnace-constants.ts`; `create-xpcshell.ts` consumes it, a new `cleanupCustomXpcshellTestFiles` helper in `src/commands/furnace/remove.ts` removes the scaffold under the journal contract, a new `renameXpcshellTestFiles` helper in `src/commands/furnace/rename-xpcshell.ts` (extracted to keep `rename.ts` under the per-file LOC budget) updates the directory, test filename, TOML section header, and word-boundary tag references in the test body, and `findOrphanXpcshellScaffolds` in `src/core/furnace-validate.ts` walks the parent directory reporting any entry whose name is not in furnace.json as an `orphan-xpcshell-scaffold` error. The validator degrades silently on projects that never used xpcshell scaffolding. Test coverage lands in `furnace-validate-xpcshell-orphan.test.ts`.
106
- - **`fireforge doctor --repair-patches-manifest` — recovery guidance no longer contradicts the docs by telling operators to hand-edit patches.json.** Finding 6 (Low). The per-filename warning emitted on recovered manifest entries ended with "Edit patches.json to restore the original description if you have it backed up." — which directly contradicts the README and Hominis docs that treat the manifest as FireForge-owned. The reworded warning now points at `fireforge re-export <filename> --description "<your description>"` (or the equivalent `fireforge export` invocation) to overwrite the reconstructed metadata through the tool, and explicitly warns against hand-editing. Test coverage in `doctor.test.ts` pins both the presence of the new guidance and the absence of the old "Edit patches.json" string.
107
- - **`fireforge test --doctor` — PASS/FAIL line written directly to stdout, bypassing any clack flush races.** Finding 7 (Medium). The 0.18.0 "belt-and-suspenders" approach still reproducibly dropped the PASS footer under non-TTY capture — the eval log showed only the intro and `Running marionette preflight...` banner before exit. A new `formatMarionettePreflightLine` helper in `src/core/marionette-preflight.ts` returns the raw banner as a plain string, and `testCommand` in `src/commands/test.ts` writes both the "Running marionette preflight..." intro and the final PASS/FAIL summary via `process.stdout.write` as the authoritative emissions so non-TTY captures always see the summary. The clack `success()` + `outro()` calls are retained for TTY framing. Test coverage in `test.test.ts` now spies on `process.stdout.write` and asserts both the intro and PASS lines are emitted.
108
- - **`fireforge test` — xpcshell appdir probe prefers the macOS `.app/Contents/Resources/<value>` layout on Darwin.** Finding 8 (High). `resolveAbsoluteAppPath` in `src/core/xpcshell-appdir.ts` probed `dist/bin/<value>` first on every platform. On macOS `dist/bin` is a symlink to `<App>.app/Contents/MacOS/`, so `dist/bin/browser` resolved to the _binaries_ directory rather than the Resources tree where `resource:///modules/` is rooted — the auto-injection logged success, but the injected path did not match where modules actually live. The probe now branches on `process.platform`: on Darwin it prefers `<App>.app/Contents/Resources/<value>` first and falls back to `dist/bin/<value>`; other platforms keep the historical order. The operator-facing `buildXpcshellAppdirMessage` in `src/commands/test.ts` also gains a macOS-specific note recommending the `<appname>-appdir = "browser"` xpcshell.toml migration, which is the most reliable fix on rebranded macOS builds. Test coverage in `xpcshell-appdir.test.ts` now exercises both probe orders via `process.platform` branches.
109
- - **`fireforge download` — git-indexing timeouts raise a typed `GitIndexingTimeoutError` with env-var recovery guidance, and the monolithic → chunked transition is visible in non-TTY logs.** Finding 10 (High). On a loaded filesystem the fresh Firefox source indexing legitimately exceeded the 10-minute monolithic `git add -A` budget; the chunked fallback then hit its own `AbortSignal.timeout` and surfaced a generic `AbortError: The operation was aborted` with no recovery direction. `src/core/git-base.ts` now exposes `FIREFORGE_GIT_ADD_TIMEOUT_MS` and `FIREFORGE_GIT_ADD_CHUNK_TIMEOUT_MS` environment variables that override the monolithic (default 10 min) and chunked (default grew to 30 min) timeouts respectively; the chunked pass's timeout is wrapped into a typed `GitIndexingTimeoutError` in `src/errors/git.ts` that names the elapsed budget, points at the environment variable override, and explains the `fireforge download --force` resume path. The fallback-transition progress banner now names the specific timeout so operators watching a non-TTY log see exactly when the monolithic attempt lost. Test coverage in `git-performance.test.ts` pins both the new error type and the refreshed transition banner.
110
- - **`fireforge rebase --dry-run` — refuses when the engine has no baseline commit.** Finding 11 (Medium). A partially-initialised engine (e.g. the aftermath of an aborted `download --force`) has `.git/` in place but no valid HEAD; the real `rebase --yes` immediately failed with `fatal: ambiguous argument 'HEAD'` even though dry-run had previously reported "Dry run complete" suggesting the rebase was ready to run. `handleFreshStart` in `src/commands/rebase/index.ts` now calls `getHead(paths.engine)` and, on `isMissingHeadError`, throws a clear `GeneralError` pointing at `fireforge download --force`. The check runs ahead of the dry-run early-exit so dry-run and real-run preconditions stay in sync. Test coverage in `rebase.test.ts` pins both the dry-run and real-run refusal paths.
111
- - **`fireforge watch` — resolved watchman directory is prepended to the mach subprocess PATH.** Finding 12 (Medium). `fireforge watch` located watchman via the parent shell PATH but the `mach watch` subprocess inherited the Node parent's PATH, which on macOS frequently omits `/opt/homebrew/bin`. `mach watch` then failed at the `watch-project` subscription step with a confusing `FasterBuildException: timed out`. `watch.ts` now resolves watchman's absolute path via `findExecutable`, prepends the containing directory to the subprocess PATH, and threads the composed env through a new optional `options.env` parameter on `watchWithOutput` in `src/core/mach.ts`. The watch-failure diagnostic also gains a line naming the resolved watchman path so operators can distinguish "FireForge didn't find watchman" from "FireForge found watchman but mach still failed to reach it." Test coverage in `watch.test.ts` pins the env plumbing and the de-duplication branch.
112
- - **`fireforge furnace preview` — first-run banner frames the unavoidable npm noise from `mach storybook`.** Finding 13 (Low). `mach storybook` internally drives a ~1000-package `npm install` when the Storybook workspace's `node_modules/` is absent and emits `npm error code ELSPROBLEMS` + `UNMET DEPENDENCY` lines verbatim. Operators on a fresh Firefox checkout read the npm block as a fatal error even though the command recovers and Storybook starts. `furnacePreviewCommand` in `src/commands/furnace/preview.ts` now checks for the absent `node_modules` before spawning mach and emits a clear pre-banner explaining the npm output is an expected one-time first-run cost; an explicit "Storybook stopped cleanly." line fires on clean exits so the npm noise is visually terminated. The `--install` path skips the banner because `mach storybook upgrade` already runs its install before `mach storybook` launches. Test coverage in `furnace-preview.test.ts` pins both the presence of the banner when node_modules is absent and its absence when node_modules is already populated.
113
-
114
- ### Internal
115
-
116
- - **Module extractions preserve the per-file LOC budget.** `doctor-working-tree.ts` hosts `inspectEngineWorkingTree` (the ownership-aware working-tree check), `doctor-furnace-manifest-sync.ts` hosts the orphan override / custom detection + repair, `patch-registration-refs.ts` hosts the dangling-registration scanner shared between `verify` and `export-all`, and `rename-xpcshell.ts` hosts the xpcshell scaffold rename path. All spliced into their respective orchestrators from the hosting file, keeping `doctor.ts`, `doctor-furnace.ts`, `rename.ts`, and `preview.ts` under the 500-line `max-lines` rule. The drift test in `manifest.test.ts` classifies each new file as a helper so the top-level command drift check stays clean.
117
- - **Expanded test coverage.** Every eval3 fix ships with at least one regression test exercising the failing path: `patch-registration-refs.test.ts` + `verify.integration.test.ts` (Finding 1), `status.test.ts` (Finding 2), `patch-lint-diff-tag.test.ts` (Finding 4), `furnace-validate-xpcshell-orphan.test.ts` (Finding 5), `doctor.test.ts` (Finding 6), `test.test.ts` (Finding 7), `xpcshell-appdir.test.ts` (Finding 8), `git-performance.test.ts` (Finding 10), `rebase.test.ts` (Finding 11), `watch.test.ts` (Finding 12), `furnace-preview.test.ts` (Finding 13).
118
-
119
- ### Internal (0.18.0 original)
120
-
121
- - **Two small module extractions preserve the per-file LOC budget.** `doctor-working-tree.ts` hosts `inspectEngineWorkingTree` (the ownership-aware working-tree check), and `doctor-furnace-manifest-sync.ts` hosts the orphan override / custom detection + repair. Both are spliced into their respective registries from the hosting file, keeping `doctor.ts` and `doctor-furnace.ts` under the 500-line `max-lines` rule. The drift test in `manifest.test.ts` is extended to classify both new files as helpers so the top-level command drift check stays clean.
122
- - **Expanded test coverage — 34 new assertions.** Every fix above ships with at least one new test exercising the regression path: `src/core/__tests__/git-diff.test.ts` gains a `?? dir/` expansion case; `src/commands/__tests__/doctor.test.ts` gains patch-backed / conflict / orphan-override / manifest-sync-repair cases; `furnace-remove.test.ts` + `furnace-rename.test.ts` pin the locale jar.mn re-wire / removal; `furnace-override.test.ts` pins the concurrent-writer read-modify-write; `build.test.ts` pins the stale-configure annotation; `test.test.ts` pins the fork-module diagnostic + triple-path PASS footer; `token-coverage.test.ts` pins the `--moz-*` allowlist + untracked-dir expansion; `patch-manifest-helpers.test.ts` pins name-field resolution; `patch-mutations.integration.test.ts` pins the two-patch swap postcondition; `register.test.ts` pins dry-run idempotency; `wire-init.test.ts` pins the marker; `manifest-tokenizers.test.ts` + `register-module.test.ts` pin the empty-list expansion; `wire.test.ts` pins the dry-run missing-subscript notice; `manifest-register.test.ts` pins the test-file pattern exclusion.
37
+ - Kept existing 0.17 patch queues compatible.
38
+ - Fixed aggregate lint and `export-all` directory crashes.
39
+ - Improved doctor ownership classification.
40
+ - Fixed localized Furnace remove and rename registration.
41
+ - Hardened Furnace concurrency, rollback, and validation paths.
123
42
 
124
43
  ## 0.17.0
125
44
 
126
- ### Eval-driven hardening
127
-
128
- - **`fireforge lint --per-patch` — branding-tier detection tolerates the one-line registration sibling + explicit `tier: "branding"` opt-in, and `lintIgnore` now round-trips through the manifest loader.** The 0.16.0 branding tier (notice 3000 / warning 8000 / error 20000) was gated on `isBrandingOnlyPatch` in `src/core/patch-lint.ts` returning true only when _every_ entry in `filesAffected` began with `browser/branding/`. Real-world branding patches follow the Firefox convention of also touching `browser/moz.configure` (to register the new branding flavor with the top-level configure), so the 2026-04-21 external audit reported that `lint --per-patch` still fired `ERROR [large-patch-lines] 001 (patch): Patch is 15665 lines (hard limit: 3000)` on a minimum branding diff — the 0.16.0 fix only helped forks with trivially scoped branding patches. Two mechanisms land together: (a) the auto-detector now accepts a narrow allowlist of branding-registration siblings (`browser/moz.configure`, `browser/confvars.sh`) in addition to the `browser/branding/` prefix, guarded by a "≥1 file under `browser/branding/`" requirement so a config-only patch cannot accidentally qualify; (b) a new optional `tier?: 'branding'` field on `PatchMetadata` forces the branding thresholds regardless of `filesAffected`, threaded through `runPatchLint`, `export`, `export-all`, `re-export`, `re-export --files`, and `lint --per-patch` — mirroring how `lintIgnore` expresses per-patch intent. Precedence stays `test > branding > general` (tests still win even with explicit `tier: "branding"`). A one-line `info()` surfaces the tier choice when branding applies, distinguishing "all files under browser/branding/ + registration siblings" from "operator declared tier: branding". Coupled with a pre-existing latent bug uncovered during the audit: `validatePatchMetadata` in `src/core/patch-manifest-validate.ts` returned a fresh object enumerating only the required fields, silently stripping `lintIgnore` on every manifest load — so the 0.16.0 escape hatch only round-tripped through tests that mocked `loadPatchesManifest` directly. Real operator edits to `patches.json` evaporated. The validator now preserves `lintIgnore` (via a new `optionalStringArray` helper in `src/utils/parse.ts`) and `tier`, and `rebuildPatchesManifest` in `src/core/patch-manifest-consistency.ts` preserves both fields on `doctor --repair-patches-manifest` so the repair path no longer strips them. Unknown `tier` values are rejected at load time with a clear message rather than silently stripped — a typo like `"Branding"` surfaces as a loader error, matching the strict validation already applied to `category` and `sourceEsrVersion`.
129
- - **`fireforge config` — serialised writes behind a sidecar lock.** The 2026-04-21 eval reproduced silent data loss by running two `fireforge config` invocations in parallel against the same `fireforge.json`: both commands exited `0`, but only one key survived. Atomic-rename writes (`writeJson`'s temp file + rename) prevented torn files but not lost updates: each writer read the pre-state, mutated its own copy, and the second rename clobbered the first writer's change. A new `withConfigFileLock(projectRoot, operation)` helper in `src/core/config.ts` wraps the read-modify-write cycle in `src/commands/config.ts:configCommand` — both the strict-validated and `--force` write branches now take the lock. Reads (`loadConfig`, `loadRawConfigDocument`) stay lock-free: atomic rename means readers always see either the pre- or post-state, so only writers need to be serialised. Stale-lock recovery reuses the existing PID-alive probe from `withFileLock` so a crashed `fireforge config` does not wedge the next command.
130
- - **`fireforge token add --mode override` — dark values land inside the nested `:root { }`.** The previous `insertDarkModeOverride` in `src/core/token-manager.ts` found the outer `@media (prefers-color-scheme: dark) { }` block's closing `}` and spliced the dark-value declaration before that line — which is _after_ the nested `:root { }` had already closed, producing a declaration outside any rule block. The generated tokens CSS was syntactically malformed after every legitimate `token add --mode override` call, and `token coverage` no longer recognised the added tokens. The fix walks the comment-stripped line array to find the `:root {` opener _inside_ the `@media` block, then depth-counts to its own closing `}`, and inserts there. A fallback path (warn + synthesise a fresh nested `:root` before the outer close) handles malformed scaffolds where the nested `:root` was removed, so we never silently drop a dark value or emit a top-level declaration. The test coverage in `src/core/__tests__/token-manager.test.ts` now pins the invariant that the dark entry's line index must be _less_ than the inner `:root`'s closing `}`, so a future refactor cannot regress to the outer-block landing.
131
- - **`fireforge furnace rename` — cleans up deployed widgets, renames mochikit test + chrome.toml.** The previous `renameTestFiles` helper in `src/commands/furnace/rename.ts` handled the `browser/base/content/test/<binaryName>/` browser-chrome layout but not the `toolkit/content/tests/widgets/` mochikit layout, and `performRenameMutations` made no attempt to clear `engine/<oldTargetPath>/` after deployment. The 2026-04-21 eval renamed `ff-chip-row` → `ff-chip-stack` and ended up with `engine/toolkit/content/widgets/ff-chip-row/` still deployed, `test_ff-chip-row.html` still importing `chrome://global/content/elements/ff-chip-row.mjs`, and the `chrome.toml` entry still naming the old file. Two new helpers pick up the slack: `removeStaleDeployedComponentDir` snapshots + removes `engine/<oldTargetPath>/` so the next `furnace apply` is the sole writer of the new deployment; `renameMochikitTestFiles` snapshots the old scaffold, rewrites the chrome URI + class-test identifiers to the new name, and updates the widgets `chrome.toml` entry. Both run under the same rollback journal as the rest of the rename, so a later failure restores every touched path.
132
- - **`fireforge furnace create --with-tests` — scaffolded mochikit test runs to completion.** `generateMochikitTestContent` in `src/commands/furnace/create-templates.ts` previously emitted `SimpleTest.waitForExplicitFinish()` alongside an `add_task(...)` and no explicit `SimpleTest.finish()`. The test harness waits forever: `waitForExplicitFinish()` tells the harness not to finish on script end, and the `add_task`-managed finish never fires because the scaffold has no explicit `finish()` body. The 2026-04-21 eval's `fireforge test --headless toolkit/content/tests/widgets/test_ff-chip-row.html` hung until the operator SIGINT'd it. The fix removes the `waitForExplicitFinish()` call — `add_task` already calls `SimpleTest.finish()` when every queued task resolves, matching the convention upstream widget tests (`toolkit/content/tests/widgets/test_moz-button.html` and siblings) use. A regression test in `src/commands/furnace/__tests__/create-mochikit.test.ts` pins the contract that the generated content must not contain `waitForExplicitFinish`.
133
- - **`fireforge furnace chrome-doc create --with-tests` — packaging test probes the correct packaged CSS path.** `generateChromeDocPackagingTest` in `src/commands/furnace/chrome-doc-tests.ts` probed `<AppDir>/chrome/browser/skin/classic/browser/<name>-chrome.css`, but the `jar.inc.mn` entry in `chrome-doc-templates.ts:chromeDocJarIncMnCssEntry` registers the file at `content/browser/<name>-chrome.css` — so the packaged location is `.../chrome/browser/content/browser/<name>-chrome.css`, not the skin layout. The 2026-04-21 eval's `fireforge test --build` against a scaffolded chrome-doc failed with a spurious "missing" assertion even though the file was correctly packaged. Both the primary probe and the macOS `.app`-bundle fallback now name the `content/browser/` layout, and a negative-match test guards against anyone pinning it back to `skin/classic/browser/` in a future refactor.
134
- - **`fireforge status --json` — cross-patch ownership conflicts surface as `conflict` with `claimedBy`.** `classifyFiles` in `src/commands/status.ts` tracked patch ownership as a `Set<string>` and collapsed multi-owner paths into the single-owner branch, where the content-compare then routed them into `unmanaged` when the engine content didn't match any single patch's expected result. The 2026-04-21 eval's `status --json` run reported `"classification": "unmanaged"` on two files that `status --ownership` correctly labelled `CONFLICT` — scripts built on the JSON view mis-diagnosed the drift and could have taken the wrong corrective action. The classifier now builds a `Map<string, string[]>` (filename → claiming patch filenames), emits `classification: "conflict"` for entries claimed by two or more patches, and attaches `claimedBy: string[]` to those entries so machine consumers can read the ownership set directly. The human default-mode output now surfaces a `Cross-patch ownership conflicts` section at the top pointing at `status --ownership` and `re-export --files` for recovery. Single-claim entries stay byte-identical to the pre-0.16.0 JSON shape (no unconditional `claimedBy` field) so parsers unaware of the new classification continue to work.
135
- - **`fireforge furnace init --ftl-base-path` — rejects file-shaped values up-front.** `validateFtlBasePath` in `src/commands/furnace/init.ts` only enforced syntactic safety (no absolute paths, no `..`, no null bytes). A plausible-but-invalid value like `browser/forgefresh.ftl` passed the gate, and the next localized `furnace create` scaffolded a component whose generated `.mjs` referenced `insertFTLIfNeeded("<name>.ftl")` while `furnace.json` never got the component entry — the scaffold was orphaned, every follow-up command failed with "not found in furnace.json", and the 2026-04-21 eval recorded this as Finding #5 (apparent non-registration) whose real root cause was Finding #6 (bad `ftlBasePath`). The validator now refuses any value whose basename carries `.ftl`, `.properties`, or `.dtd` with a message that names the file and points at a locale directory (`toolkit/locales/en-US/toolkit/global` or `browser/locales/en-US/browser`). When the engine directory is present on disk, it additionally probes whether the resolved path is a directory and warns (non-blocking) if the path does not yet exist — a fresh project that has not `fireforge download`-ed yet is legitimate.
136
- - **`fireforge furnace init` — defaults `tokenPrefix` from `fireforge.json`'s `binaryName`.** `createDefaultFurnaceConfig` previously accepted no arguments and omitted `tokenPrefix` entirely, so every fresh project that ran `furnace init` → `token add` → `token coverage` got `0 tokens` and "all unknown" reports until the operator discovered the missing key and hand-edited `furnace.json`. The helper now accepts `{ binaryName }` and, when passed, seeds `tokenPrefix: \`--${binaryName}-\``so the coverage scan has a prefix to key off immediately.`furnaceInitCommand`best-effort-loads`fireforge.json`and threads the binaryName through — a project that initialises Furnace before`fireforge setup`completes still gets a valid prefix-less default (and`token coverage` continues to warn when invoked without a prefix set).
137
- - **`fireforge build` + `fireforge build --ui` — per-project build lock prevents overlap.** A second `fireforge build --ui` launched while a full `fireforge build` was still running against the same engine tree raced the `obj-*` directory and failed immediately with `No rule to make target 'XUL'` — mach's downstream consequence of an incomplete backend, not a clue that the overlap was the root cause. A new `withBuildLock(projectRoot, operation)` in `src/core/mach.ts` backs onto `withFileLock` at `.fireforge-build.lock` beside the project root; `buildCommand` in `src/commands/build.ts` wraps both `build()` and `buildUI()` call paths in the lock. The refusal message names the holder PID and points at the stale-lock recovery path. Timeouts are bumped to 24h (a slow full build legitimately exceeds the default 30s) but the lock releases on process exit or via PID-alive stale recovery so a crashed build cannot permanently wedge the next invocation. A dedicated integration test in `src/core/__tests__/build-lock.integration.test.ts` pins the serialisation, throw-propagation, and stale-recovery contracts.
138
- - **`fireforge wire --dry-run` — insertion-point probe runs in both modes.** The dry-run branch previously skipped the `pathExists(join(paths.engine, domTargetPath))` check that the real-run branch ran, and even with existence confirmed the real run could still throw `Could not find insertion point in chrome document` from deep inside `addDomFragment` when the resolved chrome doc offered neither `#include browser-sets.inc` nor `<html:body>`. The 2026-04-21 eval's `wire ... --dom ... --dry-run` previewed a plausible plan targeting `tokenHostDocuments[0]`, then the same command without `--dry-run` failed against a `furnace chrome-doc create`-scaffolded document that lacked both anchors. A new `probeDomFragmentInsertionPoint` helper in `src/core/wire-dom-fragment.ts` reads the chrome doc and runs the same tokenised + legacy insertion-point scan the real run uses; `wireCommand` now calls it in both modes and surfaces the `Could not find insertion point` error before printing the plan. The dry-run preview now refuses the same cases the real run would refuse, before any operator commits to executing.
139
- - **`fireforge resolve` — `--yes` escape hatch + clearer two-step messaging.** The command refused any non-interactive invocation even after a CI-assisted manual merge was complete, because the TTY guard ran unconditionally — scripted recovery flows could complete the merge but could not then record the refreshed patch body. A new `--yes` / `-y` flag in `src/commands/resolve.ts` skips the interactive `confirm(...)` prompt and passes through in non-interactive mode; the unconditional TTY refusal fires only when the flag is absent. The command description changes from `Update a broken patch with manual fixes and continue` to `Update a broken patch with manual fixes (then run "fireforge import" to resume the queue)`, and the post-success info line now names the second-step command explicitly — the old copy implied a one-step flow where operators sometimes believed resolve continued the queue itself. Help snapshot under `src/__tests__/__snapshots__/help.test.ts.snap` is refreshed to match; resolve tests cover both the `--yes`-in-non-TTY path and the continuation messaging.
140
- - **`fireforge test` — marionette port probe catches stale browsers before mach launches.** An interrupted `fireforge test --headless` run can leave a `<binaryName> -marionette` child listening on port `2828` with parent PID `1`. The next `fireforge test` run — potentially in a sibling FireForge project — fails immediately with a mach Marionette bind error that points nowhere near the real cause, and the generic "delete obj-\* and rebuild" guidance wastes operator time. A new `probeMarionettePort` / `assertMarionettePortAvailable` pair in `src/core/marionette-port.ts` runs `lsof -i tcp:2828 -sTCP:LISTEN` (POSIX) or `Get-NetTCPConnection` (Windows) before every test launch; when the holder's basename or command line identifies a Firefox-family browser (including `binaryName` from `fireforge.json` for branded forks), the probe raises a targeted `GeneralError` naming the PID and the exact `kill` command. Unrelated listeners produce a softer "this is not a FireForge-launched browser" error so the operator can tell the two cases apart. The probe is best-effort: missing `lsof`/PowerShell falls back to `{ inUse: false }` rather than failing the test run itself.
141
- - **`fireforge lint` (default) — aggregate-mode skips tool-managed branding.** `resolveLintDiff` in `src/commands/lint.ts` passed the full `getAllDiff(engineDir)` to `lintExportedPatch` when no file list was supplied, so a fresh-setup workspace with 63 modified branding files fired `large-patch-lines`, `large-patch-files`, and `missing-license-header` on tool-managed content the operator never authored. The 2026-04-21 eval's first `fireforge lint` on a newly-built `fresh/` tree failed with blocking patch-lint errors despite the project being minimal-customisation. The aggregate-mode branch now loads `fireforge.json`'s `binaryName`, partitions the dirty tree with `isBrandingManagedPath`, and passes only the non-branding paths into `getDiffForFilesAgainstHead`. The operator sees a one-line `info()` naming the excluded count so the exclusion is visible, not silent. Explicit-path mode (`fireforge lint <path>`) preserves the previous behaviour — passing a branding path explicitly still lints it, so operators who need to audit generated branding content can do so.
142
- - **`fireforge doctor --repair-patches-manifest` — names every reconstructed entry.** `rebuildPatchesManifest` previously returned only the rebuilt manifest, silently overwriting `description` / `createdAt` with generic fallback values when the existing entry was missing. FireForge patch files do not carry header metadata that could carry human-written descriptions forward, so full fidelity is impossible — but at least visibility is. The helper now returns `{ manifest, recoveredFilenames }` in `src/core/patch-manifest-consistency.ts`; the doctor repair path prints a per-filename `warn(...)` telling the operator exactly which manifest entry was reconstructed from generic defaults and suggesting they edit `patches.json` if they have the original description backed up. The summary row count now reports both the total patches rebuilt and the subset that needed reconstruction.
45
+ - Improved fresh-project setup and branding output.
46
+ - Added `patch tier` and per-patch lint-ignore editing.
47
+ - Fixed `export-all` ownership and Furnace exclusion cases.
48
+ - Improved build, test, and status diagnostics.
49
+ - Cleaned up fork-specific examples.
143
50
 
144
51
  ## 0.16.0
145
52
 
146
- ### UX, correctness, and consistency
147
-
148
- - **`fireforge patch` / `fireforge token` — exit 0 with help (parent-command contract).** `fireforge furnace` exited 0 and printed its status message when run with no subcommand; `fireforge patch` and `fireforge token` silently inherited commander's default help-then-exit-1 path. Scripts probing the CLI surface therefore saw an inconsistent exit contract for three parent commands that do the same job (group related subcommands). Both `patch` and `token` now install a default `.action()` that prints their own help via `outputHelp()` and returns successfully — exit 0, same output shape, no destructive or stateful side effect. A new drift test in `src/commands/__tests__/manifest.test.ts` asserts every group-style parent has a default action installed so a future parent cannot regress back to the exit-1 contract silently.
149
- - **`fireforge furnace status` tips now prefix with `fireforge`.** The trailing `info()` line at the end of `furnace status` said `run \`furnace status <name>\``/`furnace --help`, which only worked if the operator happened to have a separate `furnace` binary on PATH. Copy-pasting the suggestion out of FireForge's own output produced a shell error on every fresh project. The message now names the real invocation (`fireforge furnace status <name>`, `fireforge furnace --help`), so the suggested commands are directly runnable.
150
- - **`fireforge download` — de-duplicated git-init progress in non-TTY logs.** The resume and init `onProgress` callbacks called both `spinner.message(msg)` and `step(msg)` in non-TTY mode, producing two copies of every git-init progress line in CI logs because `src/utils/logger.ts`'s non-TTY spinner fallback already calls `p.log.step(msg)` from `message()`. The explicit `step()` sibling call is removed on both paths, so each progress message appears exactly once regardless of TTY mode. The tests in `download.test.ts` / `download.integration.test.ts` now assert the spinner-handle contract directly instead of the removed `step()` signal.
151
- - **`fireforge download` — honest closing message on an empty patch queue.** `download` always stopped the restore spinner with `Patch-touched files restored`, even when the project had never exported a patch — a misleading claim of work on a fresh workspace. `cleanPatchTouchedFiles` now returns a structured `{ hadQueue, restored, preserved }` result, and a new `closeRestoreSpinner` helper picks one of three stop messages: `No patches in queue — nothing to restore` (empty queue), `Patch-touched files already match baseline` (queue present, nothing dirty), or the original `Patch-touched files restored` (work actually happened).
152
- - **`fireforge build` — gecko-profiler bindgen hint now surfaces on Darwin 25.** The `_CharT` / `basic_string___self_view` hint had been registered in `mach-error-hints.ts` since 0.16.0 but never fired against the eval's Darwin 25 build log. Root cause: `build()` and `buildUI()` in `src/core/mach.ts` fed only `result.stderr` to `surfaceMachErrorHints`, but mach's timestamp-prefixing wrapper streamed the `rustc error[E0425]` lines through stdout. The hint surfacer now scans `${result.stderr}\n${result.stdout}` so the existing `_CharT` pattern matches regardless of which stream mach chose. A new regression test in `mach.test.ts` pins the stdout-only failure mode so a future refactor cannot accidentally narrow the capture again.
153
- - **`fireforge build` — new hint clarifying the post-failure `Configure complete!` epilogue.** When `mach build` fails, mach's own shutdown pipeline runs a `Config object not found by mach. / Configure complete! / Be sure to run |mach build|...` block on the way out. That block is plain upstream mach output, printed after the non-zero exit code has already been established, but it looks deceptively like a success banner. A new `MACH_ERROR_HINTS` entry matches the exact `Config object not found by mach.\s*Configure complete!` signature and surfaces `Ignore the trailing "Config object not found by mach. / Configure complete!" block — that is mach's post-failure configure summary printed after the build already failed, not a sign the build succeeded.` The pattern is narrow enough that a real post-`mach configure` success (which legitimately prints "Configure complete!" alone) does not trigger it.
154
- - **`fireforge wire --dry-run` — init/destroy validation now matches the real run.** Pre-0.16 the `validateWireName(expression, …)` check only ran inside `addInitToBrowserInit` / `addDestroyToBrowserInit` (the real-execution path), so `fireforge wire eval-startup --init 'void 0' --dry-run` succeeded and rendered a plausible preview — then the same arguments without `--dry-run` failed with `Invalid init expression "void 0": must contain only letters, digits, hyphens, underscores, dots, and $ signs`. Validation is now hoisted into `wireCommand` before the dry-run/real branch, so both paths enforce the identical regex. The library-level call inside addInit/addDestroy remains as defence-in-depth for programmatic callers that bypass the CLI entry point.
155
- - **`fireforge wire` — coerces bare property chains into function calls.** The init/destroy validator accepted both `Foo.bar` and `Foo.bar()` shapes, but the emitted code template interpolated the expression verbatim — so `fireforge wire eval-startup --init EvalStartup.init` wrote `EvalStartup.init;` (a plain property reference) into `browser-init.js`, silently producing a lifecycle hookup that never invoked the hook. A new `coerceToCall(expression)` helper in `src/core/wire-utils.ts` appends `()` when the expression lacks trailing parens, idempotent when they are already present. Both the AST (`addInitAST`/`addDestroyAST`) and legacy fallback (`legacyAddInit`/`legacyAddDestroy`) code paths route the expression through the coercer so they agree on the emitted block shape. The idempotency regex in `addInitToBrowserInit`/`addDestroyToBrowserInit` also matches against the coerced form, so re-running `wire` with the bare form is correctly a no-op against a file that already contains the coerced call. The dry-run preview (`printWireDryRun`) applies the same coercion so the preview and the real run match.
156
- - **`fireforge furnace create` — defensive read-back after writing `furnace.json`.** The eval observed a run where `furnace create eval-card --with-tests --localized --allow-prefix-mismatch` reported success and wrote component files under `components/custom/eval-card/`, but `furnace.json` ended up with `"custom": {}` and every subsequent command (`status`, `apply`, `rename`, `remove`) on `eval-card` failed with "not found in furnace.json". Local reproduction with the same source and same arguments writes the entry correctly and the unit-test coverage is green, so the bug could not be pinpointed from source alone. The defensive fix is a read-back verification in `performCreateMutations`: after `writeFurnaceConfig(…, config)` we call `loadFurnaceConfig(projectRoot)` and assert `componentName in persisted.custom`. If the entry is missing, the command throws a `FurnaceError` pointing to the prefix rule and `--allow-prefix-mismatch`, the rollback journal restores the pre-command state, and the operator sees the failure instead of a phantom success. The mock in `furnace-create.test.ts` was updated to reflect `writeFurnaceConfig` into subsequent `loadFurnaceConfig` reads so the unit-test path exercises the same round-trip.
157
- - **`fireforge token coverage` — scans deployed Furnace custom-component CSS.** Coverage discovery was git-status-based: it read `getStatusWithCodes` and filtered to `.css` files. A `moz-eval-card.css` deployed into `engine/toolkit/content/widgets/moz-eval-card/` by `furnace deploy` never appeared in the scan if it was already tracked in the engine's own git repository (i.e. not dirty in status). The command now loads `furnace.json` (when present) and walks every entry in `config.custom`, probing `${targetPath}/${componentName}.css` under the engine directory. Files that exist on disk are merged with the git-status set and de-duplicated; the tokens CSS itself is still excluded. Projects without `furnace.json` keep the old git-status-only path unchanged. A new test case in `token-coverage.test.ts` pins the augmented discovery against the eval's exact scenario.
158
- - **`fireforge furnace preview` — backend-artifact failure gets its own diagnosis.** When `mach storybook` failed deep inside `config.status` / `chrome-map.json` because the Firefox build backend was incomplete, the pre-0.16 heuristic's second clause matched the literal string `backend` — which does not appear in the error output — so the generic message sent the operator back to `fireforge furnace preview --install` (the wrong recovery path) after the install had already succeeded. `buildStorybookFailureMessage` is now exported and covered by a dedicated test file (`preview-failure-message.test.ts`). It first checks a narrow set of backend-artifact patterns (`chrome-map.json`, `config.status`, `obj-*/dist/bin/.lldbinit`) against a file-not-found signal; on match it produces a distinct message telling the operator to rerun `fireforge build` and wait for it to finish. The generic "missing Storybook dependencies" and fallthrough branches are preserved for the cases they actually describe.
159
- - **`fireforge export` / `fireforge export-all` — new `--allow-overlap` gate against silent cross-patch ownership.** Pre-0.16 `export` only caught FULL-coverage supersedes via `findAllPatchesForFiles`. A second export targeting a shared file like `browser/themes/shared/jar.inc.mn` happily created a queue where two patches both listed the same file in `filesAffected`, and `fireforge verify` then immediately failed with "cross-patch filesAffected conflicts". A new `findPartialOwnershipOverlap` helper in `export-shared.ts` walks the manifest and maps each overlapping file to its claiming patches, excluding any patches that the caller already intends to fully supersede. The new `guardOwnershipOverlap` routes the result through a non-interactive refusal or an interactive prompt: without `--allow-overlap`, the command refuses in non-interactive mode and asks for acknowledgement in interactive mode. The refusal message points at `fireforge re-export --files <paths> <patch>` as the right primitive for repartitioning ownership, which replaces the (much more dangerous) manual `patches.json` editing the eval's operator resorted to. Both `export` and `export-all` now expose a matching `--allow-overlap` flag.
160
- - **`fireforge re-export --scan` — broad expansions require explicit acknowledgement.** Pre-0.16 `--scan` blindly merged every modified or untracked file in a patch's parent directories into its `filesAffected`. The eval scenario: two small patches in adjacent-but-unrelated features shared a directory tree, and a single `re-export --all --scan` silently absorbed the entire neighbour feature (xhtml + xpcshell tests + theme CSS) into the wrong patch. The 0.16.0 gate: when `--scan` would add more than three files, or files spanning more than one directory, the command calls `confirmBroadScanAdditions` — dry-run proceeds silently (previewing is the whole point), `--yes` proceeds silently (the explicit opt-in), interactive mode prompts for confirmation, and non-interactive mode without `--yes` refuses with the expansion summary. Small same-directory additions (the common refresh case) stay frictionless.
161
- - **`fireforge test --doctor` — closes the intro frame with an explicit outro.** Running `test --doctor` on its own showed `● Running marionette preflight...` and then exited silently with code 0 in the eval's non-TTY capture — the `Marionette preflight: PASS (…)` line that `reportMarionettePreflight` emitted via `info()` failed to render inside the unclosed clack intro frame. The doctor-only success branch now calls `outro(\`Marionette preflight: PASS (${durationMs}ms)\`)`before returning, which closes the tree and gives scripts a deterministic "done" marker to parse. The failing branch already throws through`GeneralError`, which is routed via the standard error pipeline and does not need the same treatment.
162
- - **`fireforge run --smoke-exit` — allowlist summary shows both error-class and total counts.** The previous summary only reported `Allowlisted hits: N`, where `N` was incremented only when a line matched both `SMOKE_ERROR_PATTERNS` AND the caller-supplied allowlist. An operator whose `--console-allow RSLoader:` pattern visibly matched `console.warn: RSLoader: …` lines still saw `Allowlisted hits: 0`, because `console.warn:` is not a smoke-error class — the allowlist was never consulted for those lines. The summary now distinguishes two counters: `Allowlisted error hits (suppressed): N` (the exit-contract number: errors the allowlist kept out of the findings list) and `Allowlisted lines total: M` (the mental-model number: every console line that matched the allowlist, regardless of error class). The exit contract itself is unchanged — unallowed errors still fail the smoke window.
163
- - **`fireforge resolve` — `filesAffected` is always recomputed from the new diff body.** Pre-0.16 the resolve command updated `patches.json.filesAffected` only when `activeFiles.length < existingFiles.length` (files deleted from disk). If the user's manual fix eliminated every hunk for a specific file while the file itself still existed on disk, the metadata kept claiming it — and the next `fireforge import` immediately failed the patch-manifest consistency check with "patches.json declares [...] but the patch file targets [...]". The command now threads the generated `diffContent` through `extractAffectedFiles` (the same helper `export` and the consistency checker use) and unconditionally passes the result as `filesAffected`. Resolve and consistency-check therefore always agree on the set of targeted files. A new round-trip test in `resolve.test.ts` pins the scenario where the diff shrinks but every file still exists on disk.
164
-
165
- ### Security
166
-
167
- - **Release workflow — shell injection via `${{ inputs.version }}` interpolation.** `.github/workflows/release.yml` previously interpolated `${{ inputs.version }}` directly into `npm version "${{ inputs.version }}" --no-git-tag-version`, so anyone with `actions:write` could trigger `workflow_dispatch` with a crafted version string (e.g. `1.0.0"; <command> #`) and execute arbitrary shell inside the job. The release job carries `contents: write`, `id-token: write`, and the `npm` trusted-publishing environment, so that shell would have had an open door to the publish credentials. The fix routes `${{ inputs.version }}` through an `env: INPUT_VERSION:` block on the `Bump version` step and references `"$INPUT_VERSION"` inside the `run:` script, so GitHub Actions substitutes the value into the process environment rather than into the shell source. The `Tag and push` step gets the same treatment for `${{ steps.version.outputs.version }}` — defense-in-depth, since `npm version`'s semver check already filters that interpolation, but the pattern is consistent and cheap.
168
- - **`fireforge config` — prototype pollution via sentinel key segments.** `mutateConfig` in `src/core/config-mutate.ts` walks a dot-separated key through `getOrCreateChildRecord(parent, segment)` with no filter on `__proto__`, `constructor`, or `prototype`. `fireforge config __proto__.polluted 1 --force` therefore reached `parent["__proto__"]` and wrote a plain property onto `Object.prototype`, polluting every object in the Node process for the rest of the run. `--force` was the motivating pathway (the strict path guard rejects unknown top-level keys for non-sentinels), but the raw sink was also publicly re-exported from `src/core/config.ts`, widening the blast radius to any future caller. The fix rejects sentinel segments up-front in `mutateConfig` with a `ConfigError` before any clone or mutation — a single guard at the entry point covers both the descent loop and the final leaf assignment, and surfaces to the CLI as a normal "invalid key" failure rather than a crash. `readJson`'s existing reviver already strips the sentinels from loads, so input configs can't arrive pre-polluted.
169
- - **`fireforge wire --dom` — asymmetric newline marker parser projection drift.** `parseHunksForFile` in `src/core/patch-parse.ts` tracked `` as a single `noNewlineAtEnd: boolean` — collapsing the old-side and new-side markers into one flag. Asymmetric trailing-newline changes (e.g. removing the newline from the old side while the new side keeps one, or vice versa) were indistinguishable from the symmetric case, so `applyPatchToContent` produced content that disagreed with `git apply` on whether to emit a trailing newline. The projection drift surfaced as phantom entries in `fireforge status` and wipe-safe reprojection work in `fireforge import` for patches that were otherwise clean. The fix splits the field into `noNewlineAtEndOld` / `noNewlineAtEndNew` and peeks the body line that the marker trails (`-` → old-only, `+` → new-only, ` ` context → both), and `applyPatchToContent` now reads `noNewlineAtEndNew` for the output-side newline decision because the content we emit corresponds to the new side. `extractNewFileContentFromDiff` is unchanged — new-file patches only contain `+` lines, so its separate `hasNoNewlineMarker` local is already correct by construction.
170
- - **`fireforge wire --dom` — engine-relative inputs probed against CWD.** `src/commands/wire.ts` passed the raw `stripEnginePrefix(options.dom)` result to `pathExists` without joining `paths.engine` first, so a relative `--dom browser/base/content/foo.inc.xhtml` was probed inside the operator's shell directory and failed "DOM fragment file not found" even when the file existed in the correct engine location. The follow-on `isPathInsideRoot` and `toRootRelativePath` calls worked by coincidence — they internally `resolve(paths.engine, candidate)`, which masked the bug past the existence probe but only because those calls never hit the filesystem. The fix mirrors the pattern already in use in `src/commands/register.ts`: when the candidate is absolute probe it as-is, otherwise `join(paths.engine, candidate)` first. The error message still echoes the original operator input (not the internal joined path) so it remains copy-pasteable back into the CLI.
171
-
172
- ### Config — `--force`-written keys are now readable
173
-
174
- - `fireforge config <key>` (read mode) now consults the raw `fireforge.json` document instead of the validated, typed config that `loadConfig` produces. Before this change, `fireforge config totallyUnknown value --force` succeeded (the key was persisted to disk via `writeConfigDocument`), but the corresponding `fireforge config totallyUnknown` read threw `Unknown config key: totallyUnknown` because `validateConfig` rebuilds a clean object containing only the schema-known fields — the forced key survived on disk but was invisible to the typed read path. A new `loadRawConfigDocument` helper in `src/core/config.ts` returns the raw JSON record, and the command's read branch now traverses that document. Writes are unaffected: schema validation still enforces shape for known keys, and `--force` continues to be the escape hatch for unknown keys.
175
- - The set-mode `--force` branch also now seeds the mutation from `loadRawConfigDocument`, so writing a second forced key no longer drops previously-written forced keys. Before this, the sequence `config foo 1 --force && config bar 2 --force` silently lost `foo` because the intermediate `loadConfig` stripped it out of the in-memory config.
176
-
177
- ### Download — Extracting phase spinner
178
-
179
- - `fireforge download` now switches its spinner message from `Downloading Firefox <ver>... 100%` to `Extracting Firefox <ver>... (decompressing ~600 MB of source; typically 30–90s)` when the byte transfer completes and `tar -xf` starts. Before this change, the download spinner stayed pinned at "Downloading… 100%" for the entire extraction window — on a 601 MB ESR archive that is ~30–90 seconds of silent tar decompression where the first-run setup looked network-stalled precisely when the payload was already on disk. The new `FirefoxSourcePhaseCallback` in `src/core/firefox.ts` fires `'extract'` right before `extractTarXz`, and the download command swaps spinners on that signal; downgrading the initial `const s = spinner(...)` to `let s` is the only mutation site in the command.
180
-
181
- ### Status — `--json` returns `[]` on a clean tree
182
-
183
- - `fireforge status --json` now emits a valid JSON document (`[]\n`) when there are no modified files, instead of falling through to the human-readable `No modified files` / `Working tree clean` banner. Before this change, the clean-tree early-return ran before the `--json` branch and silently printed human text, so automation that piped the command through a JSON parser broke precisely on the most common clean-workspace invocation. The `--raw` mode gets the same guard: a clean tree writes nothing to stdout in raw mode, matching what native `git status --porcelain` does on a clean repo.
184
-
185
- ### Furnace init — tokens CSS scaffold + raw-color allowlist
186
-
187
- - `fireforge furnace init` now scaffolds the Furnace-managed tokens CSS at `engine/browser/themes/shared/<binaryName>-tokens.css` whenever the engine directory exists, and registers that path in `fireforge.json`'s `patchLint.rawColorAllowlist`. Before this change, `furnace init` only wrote `furnace.json`, so every fresh project's first `fireforge token add` hit `Token CSS file not found: browser/themes/shared/<binaryName>-tokens.css`. The scaffold writes a `:root { … }` shell seeded with four default category headers (`Colors — General`, `Colors — Canvas`, `Colors — Experiment`, `Spacing`) plus a `@media (prefers-color-scheme: dark)` overrides block; all four are recognised by `assertTokenCategoryExists` so `token add --category 'Colors — General' …` works end-to-end on a fresh project. The allowlist registration is idempotent (no-op when the entry is already present) so a `furnace init --force` on an existing project doesn't duplicate it.
188
- - `fireforge token add`'s "Category not found" error now lists the categories actually present in the file, along with a copy-pasteable header template (`/* = My Category = */`) and pointer to `fireforge furnace init --force`. Before this, the error told operators that "categories are defined by comment headers" without showing what was available — `token add` failed silently on any typo in the category name.
189
-
190
- ### README — `token add` syntax update
191
-
192
- - The `Additional workflow commands` block in `README.md` now reflects the `token add <token> <value>` subcommand shape shipped in 0.14+. The previous example used a `--name/--value` form that no longer exists on the CLI surface and led operators through a broken copy-paste on their first try. The updated example also points at the Furnace tokens-CSS prerequisite so first-time operators know why `fireforge furnace init` runs before `token add`.
193
-
194
- ### Furnace create — prefix mismatch is a hard refusal
195
-
196
- - `fireforge furnace create <name>` now refuses before any filesystem writes when `furnace.json`'s `componentPrefix` is set and `<name>` does not start with it. Before this change, the prefix check was a `warn()` that let the flow continue, producing runs where the command reported success, scaffolded files under `components/custom/<name>/`, registered tests in `browser/base/moz.build`, but the result was a second-class citizen of the fork's convention — subsequent follow-ups (list, status, rename) behaved inconsistently because the name didn't match what `fireforge furnace scan` / override workflows expected to see. A new `--allow-prefix-mismatch` flag is the intentional escape hatch when the mismatch is deliberate (e.g. a throwaway experiment); without it, the command throws `InvalidArgumentError` up-front with specific guidance about prefixing the name or editing `componentPrefix` in `furnace.json`.
197
-
198
- ### Wire — `--dom` engine-relative normalisation
199
-
200
- - `fireforge wire --dom <path>` now accepts repo-root-relative forms (`engine/browser/base/content/foo.inc.xhtml`) and engine-relative forms (`browser/base/content/foo.inc.xhtml`) interchangeably, matching `lint`/`export`/`register`/`test`. Before this, passing the `engine/`-prefixed form from the repo root sailed through `pathExists` but then double-rooted through `toRootRelativePath(engineDir, 'engine/…')` — `resolve(engineDir, 'engine/…')` landed at `engineDir/engine/…`, which passed `isPathInsideRoot` but produced a `safeDomFilePath` of `engine/browser/base/content/foo.inc.xhtml`. The computed `#include` then read `#include ../../../engine/browser/base/content/foo.inc.xhtml`, nonsense that would never preprocess correctly. `stripEnginePrefix` normalises the input before the path probe so both forms produce `#include foo.inc.xhtml` in `browser.xhtml`. The `--target` option gets the same normalisation for symmetry.
201
-
202
- ### Lint — `token-prefix-violation` scoped to added lines
203
-
204
- - `lintPatchedCss` now scopes its `token-prefix-violation` scan to the added/modified lines when diff context is available, mirroring what the `raw-color-value` rule already does. Before this change, a small CSS-only override of a stock component (e.g. `moz-card`) was flagged for every stock `var(--moz-card-*)` reference in the unchanged portion of the applied file, because the scanner saw the full applied CSS and treated every inherited reference as if the fork had introduced it. The fix ingests `addedLinesByFile.get(file)` as the scan source when present (with CSS comments stripped), de-duplicates per-prop so the same introduced var consumed five times produces one issue instead of five, and falls back to whole-file scanning only when no diff is available (matching the pre-existing contract for callers that pass raw CSS with no diff). `localDeclarations` continues to be collected from the full file so a var declared in an unchanged line is still recognised as a same-file runtime channel.
205
-
206
- ### Lint — aggregate multi-patch size rules are warnings, not errors
207
-
208
- - `fireforge lint` running against an aggregate diff on a multi-patch queue (the default mode, `ctx.entries.length > 1`, no file paths supplied) now emits the two size rules (`large-patch-lines`, `large-patch-files`) as warnings rather than errors. Before this, a freshly-imported patch stack of 20+ patches failed the default lint on aggregate counts that are mathematically impossible to satisfy without splitting patches that were already split — the actionable unit is the individual patch, and `--per-patch` is the right mode. Per-patch mode (`lintPerPatch`) keeps the rules as errors because the size is genuinely per-patch there. The "aggregate mode" hint line is updated to note the severity downgrade so operators see the full picture.
209
-
210
- ### Furnace chrome-doc — locale jar.mn source path fix
211
-
212
- - `localeJarMnEntryForChromeDoc` now emits `(%browser/<name>.ftl)` as the source-path column instead of `(%<name>.ftl)`. Before this fix, the first `fireforge build` after `fireforge furnace chrome-doc create <name>` failed with `jar.mn: Cannot find <name>.ftl` during backend generation because the FTL file is scaffolded under `engine/browser/locales/en-US/browser/<name>.ftl`, and the `%`-rooted jar path resolves relative to the per-locale root (e.g. `en-US/`), not the locale bundle root. The missing `browser/` subdirectory in the source path made the entry point at `en-US/<name>.ftl`, which doesn't exist. The extended docstring on the helper calls out the path resolution rule so a future refactor can't re-introduce the drift.
213
-
214
- ### Build — gecko-profiler bindgen hint
215
-
216
- - The `MACH_ERROR_HINTS` table gains a pattern that matches the distinctive `cannot find type \`\_CharT\` in this scope`+`gecko-profiler-` co-occurrence emitted by upstream bindgen on some macOS libc++ SDK versions. The generated alias (`pub type basic_string**\_self_view = root::std::**1::basic_string_view<\_CharT>;`) references a type name that is not in scope at the landing site, so the Rust compile fails partway through the build. The new hint points operators at Hominis' `990-infra-bindgen-basic-string-workaround.patch` (which strips the offending line post-generation) and also prints the exact file to edit (`<objdir>/release/build/gecko-profiler-\*/out/gecko/bindings.rs`) for operators not on Hominis' patch queue. The hint lands via the existing `surfaceMachErrorHints`plumbing in`src/core/mach.ts`—`mach build`already captures stderr and feeds it through`explainMachError`, so no new wiring is needed.
217
-
218
- ### Export-all — `--exclude-furnace`
219
-
220
- - `fireforge export-all --exclude-furnace` now filters Furnace-managed paths out of the aggregate diff instead of refusing the command when any are present. Before this, a mixed workspace (Furnace overrides + non-Furnace edits) could not use `export-all` at all — the operator had to fall back to `fireforge export <paths…>` with a hand-curated file list. The filter reuses the existing `collectFurnaceManagedPrefixes` helper to identify the Furnace-owned subset, rescopes the diff via `getDiffForFilesAgainstHead` over the remaining paths, and prints a one-line info (`Excluded N furnace-managed file(s) from export; exporting M remaining path(s).`). Without the flag, the existing refusal-with-guidance stays — the default still protects against accidentally capturing Furnace files as a regular patch.
221
-
222
- ### Furnace preview — accepts `.cargo/config.toml.in`
223
-
224
- - `assertPreviewPrerequisites` now accepts either `engine/.cargo/config.toml` OR `engine/.cargo/config.toml.in` as proof that the Rust toolchain is registered. Before this, the preflight insisted on the plain file, but `fireforge bootstrap` alone produces only the `.in` template (the plain file is generated at `mach configure` time). Operators who followed the remediation instruction ("run bootstrap then rerun preview") hit the same refusal on the retry and had no in-surface recovery. The relaxed check still catches a completely un-bootstrapped engine (neither file exists), and the separate `hasBuildArtifacts` check above remains the authoritative "no dist" guard so this relaxation doesn't weaken the signal we care about.
225
-
226
- ### Rebase — Furnace override baseVersion stamped alongside patches
227
-
228
- - After a successful rebase, `runPatchLoop` now calls `stampFurnaceOverrideBaseVersions` (new helper in `src/core/furnace-config.ts`) to update every override's `baseVersion` in `furnace.json` to `session.toVersion`, right after the existing `stampPatchVersions` call. Before this, a successful ESR bump from, say, `140.9.0esr` to `140.9.1esr` stamped every patch's `sourceEsrVersion` but left every override in `furnace.json` pointing at the old baseline, and the very next `fireforge doctor` failed `Furnace component validation` on every override until the operator hand-updated the file. The stamp emits a one-line info (`Stamped N Furnace override baseVersion(s) to <version>.`) and no-ops cleanly when there are zero overrides. Per-override content health is still the job of `fireforge furnace validate` / `doctor --repair-furnace`; the stamp only closes the version-drift reporting gap so a successful rebase doesn't leave the workspace in a doctor-failing state.
229
-
230
- ### Build baseline — packageable fingerprints for `test --doctor`
231
-
232
- - `BuildBaseline` gains a `packageableFingerprints: Record<path, sha256>` field recorded at build completion for every packageable-dirty engine path. `checkStaleBuildForTest` re-hashes each current packageable-dirty file and flags only those whose live hash differs from the baseline entry (or paths new since the baseline). Before this change, the stale probe reported every workdir-dirty file as "changed since the last build" every time, because a project with imported patches + Furnace-applied components always has a persistent workdir diff against HEAD — the engine HEAD SHA doesn't move between builds even though the workdir does, so `git diff --name-only HEAD` always returned the full post-import / post-apply set. The result was a warning that fired immediately after a successful full + UI build with no edits in between. The fingerprint layer captures "these files had this content when the build ran", so the only paths that register as stale now are ones whose content actually changed.
233
- - Baselines written by older FireForge versions do not carry `packageableFingerprints`; the stale check falls through to the pre-0.16.0 path-only comparison in that case, so upgrading does not silently flip the semantics for already-built workspaces until the next `fireforge build` re-records the baseline.
234
-
235
- ### Known limitations (unchanged in 0.16.0)
236
-
237
- - **`fireforge test` against a browser-test harness fails with `ERROR_SIGNEDSTATE_REQUIRED`.** Gecko's add-on manager rejects the Marionette harness add-on as unsigned when the fork build's release settings enforce signing. Pref injection at the `fireforge test` boundary needs to plumb `--setpref xpinstall.signatures.required=false` (and a matching dev-root pref) through every harness invocation type; that work is tracked for 0.17.0. Workaround: toggle `MOZ_AUTOMATION=1` / relax signing in the launched build profile out-of-band.
238
- - **`fireforge test --build` can loop on xpcshell tests that read packaged chrome resources.** The `--build` path drives `mach build faster`, which regenerates the fast-rebuild subset but not the chrome package xpcshell resolves through `chrome://branding/…` / `resource:///modules/…`. The stale-check then flags the same files on the retry even though the inline build ran. Workaround: run a full `fireforge build` (without `--ui`) before `fireforge test`; a proper test-type classifier + `--full-rebuild` flag is planned for 0.17.0.
239
- - **`fireforge watch` can fail with `FasterBuildException: timed out waiting for response` while a direct `watchman watch-project <engine>` succeeds.** The timeout originates entirely inside `mozbuild.faster_daemon`; FireForge has no hook to extend or recover from it. Workaround: `watchman watch-del <engineDir>` followed by `watchman watch-project <engineDir>` to reset the watcher's internal state before rerunning `fireforge watch`.
240
-
241
- ### Wire — transactional rollback
242
-
243
- - `fireforge wire` now snapshots every file the mutation sequence may touch (`browser/base/content/browser-main.js`, conditionally `browser/base/content/browser-init.js`, the chrome document the `#include` lands in, and `browser/base/jar.mn`) before any write, and restores them when any step fails. The evaluator hit the motivating case on `hominis/`: a `wire mock-wire --init … --destroy … --dom …` run threw `Could not find insertion point in chrome document` AFTER `browser-main.js`, `browser-init.js`, and `browser/base/jar.mn` had already been mutated — the operator had to hand-revert the partial mutation. The journal plumbing reuses `createRollbackJournal` / `snapshotFile` / `restoreRollbackJournal` from Furnace's rollback module; a rollback that itself fails surfaces both the original wire failure and the rollback diagnosis in a single `GeneralError` with `review "git status" under engine/` guidance so the operator knows the engine may need manual attention.
244
- - The snapshot set is conditional on which option set the run actually uses — no unused snapshot cost on a `wire` invocation that doesn't pass `--init`/`--destroy` or `--dom`. Real-fs integration tests in `browser-wire-rollback.integration.test.ts` pin both the failure-rollback contract and the successful-run pass-through so a future refactor can't regress either branch silently.
245
-
246
- ### Branding/mozconfig preflight
247
-
248
- - New `assertBrandingMozconfigAgreement` fires at the end of `generateMozconfig` and refuses to hand off to `mach` when the just-written `engine/mozconfig` sets `--with-branding=browser/branding/<X>` while FireForge's branding tree lives at `browser/branding/<Y>`. The new `BrandingMozconfigMismatchError` enumerates three reasons — `mozconfig-missing-branding`, `name-mismatch`, `branding-dir-missing` — each with an actionable recovery line: edit `configs/common.mozconfig` to use `${binaryName}`, or align `fireforge.json`'s `binaryName` with the baked-in value. Motivating case: the evaluator's real `fresh/` tree produced a branding scaffold at `browser/branding/freshforge/` while the rendered mozconfig still pointed `mach` at `browser/branding/freshtest/moz.build`, and every first build failed deep inside moz.build resolution with a confusing "path does not exist" message. The preflight turns that into a single-line refusal before `mach` runs.
249
- - The extractor (`extractWithBrandingPath`) matches `/^\s*(?:ac_add_options\s+)?--with-branding\s*=\s*(\S+)/m` so both the bare form and the `ac_add_options`-prefixed on-disk convention are recognised, picks the last match (matches `mach`'s last-write-wins semantics for overlapping `ac_add_options` calls), and normalises backslash separators before comparing.
250
- - The preflight only fires on values that would actually fail under `mach`; it does not prescribe a single shape for `configs/*.mozconfig`. A follow-up (tracked for 0.17.0) will flip `setup-support.ts` to keep templates unsubstituted and substitute at `generateMozconfig` time, removing the drift vector entirely. The 0.16.0 fix is preflight-only because existing projects already have post-substitution configs; reshaping those requires a migration we intend to ship with 0.17.
251
-
252
- ### Export-all — duplicate new-file-creation guard
253
-
254
- - `fireforge export-all` now refuses before writing when the aggregate diff would newly-create (`new file mode`) a path some other patch in the queue already creates. Motivating case on `fresh/lab/`: exporting patch 1 with `browser/modules/labforge/Hello.sys.mjs` as a new file, then running `export-all --name bye-module --category infra` without scoping the change set, produced a second patch that also claimed the same path — `verify` then failed on `files-affected-mismatch`, a cross-patch `filesAffected` conflict, AND a `duplicate-new-file-creation` error all at once, and the fix required either `patch delete` or hand-edited `re-export --files`. The new guard slots in right after the existing branding and furnace refusals at `src/commands/export-all.ts:checkDuplicateNewFileCreations`; it lists every conflicting path and every existing owner, and points the operator at `fireforge export <path>` with explicit file scoping as the clean recovery.
255
- - The guard reuses `detectNewFilesInDiff` + `collectNewFileCreatorsByPath` (already used by `verify` and `status --ownership`), so the pre-export check and the post-hoc detection report exactly the same conflict set. No change to `export` itself — single-path exports stay the surgical primitive for this case.
256
-
257
- ### Path normalization parity (`lint` / `export`)
258
-
259
- - `fireforge lint <paths...>` and `fireforge export <paths...>` now accept both repo-root-relative forms (`engine/browser/base/content/foo.js`) and engine-relative forms (`browser/base/content/foo.js`), matching the normalization already implemented by `register` and `test`. Motivating cases on both `fresh/` and `fresh/lab/`: pasting `engine/browser/base/content/fresh-extra-a.js` into `fireforge export` produced `File "engine/..." has no changes to export.` and the same input to `fireforge lint` produced `No modified files found in the specified paths.` — both because the status lookup sees paths relative to `engine/` and the explicit prefix double-rooted the candidate. Re-running with the engine-relative form succeeded. The normalization is now shared: `stripEnginePrefix` in `src/utils/paths.ts` is the single source of truth, and `register`/`test` delegate to it so every command that takes an engine-relative path treats `Engine/`, `engine\\`, and leading whitespace identically.
260
-
261
- ### Furnace override — stock auto-promotion
262
-
263
- - `fireforge furnace override <name>` no longer rejects the component when `name` is already present in `config.stock`. Motivating case on `hominis/`: every stock-discovered widget (populated by `furnace scan`) forced the operator to hand-edit `furnace.json` before the override could be created, with the error `"<name>" is already registered as a stock component. Remove it from config.stock before creating an override.` That's busywork — the whole point of `override` is to fork a stock component. The new contract splices the name out of `config.stock` in-memory and lets the existing mutation-phase `writeFurnaceConfig` persist the promotion atomically alongside the new override entry, under the same rollback journal. The collision check for `config.overrides[name]` and `config.custom[name]` stays — those are real conflicts. Promotion emits a one-line `Promoting "<name>" from stock to override.` so the operator sees that the stock entry is gone.
264
- - The batch variant `furnaceBatchOverrideCommand` applies the same promotion per-name; a batch that overrides both `moz-button` and `moz-card` demotes both from `stock` in the same `writeFurnaceConfig` write.
265
-
266
- ### Furnace diff — FTL deployment path parity
267
-
268
- - `fireforge furnace diff <name>` now resolves `.ftl` entries through the configured `ftlDir` instead of the component's `targetPath`, matching the path `furnace apply`'s `applyCustomFtlFile` writes to. Before the fix, a custom component with a `.ftl` always reported "`<name>.ftl`: not yet deployed to engine (new file)" after a clean apply, because `diff` probed `engine/<customConfig.targetPath>/<name>.ftl` while the deployed file was at `engine/<ftlDir>/<name>.ftl`. The diff header also now names the locale path (`--- engine/<ftlDir>/<name>.ftl`) so the rendered diff anchors at the same target on both sides. Motivating case on `hominis/`: `furnace diff moz-lab-pill` after a successful apply/deploy reported the `.ftl` as new even though `engine/toolkit/locales/en-US/toolkit/global/moz-lab-pill.ftl` was present on disk and `furnace status moz-lab-pill` reported clean — `diff`, `status`, and `validate` now agree on deployed state.
269
-
270
- ### Furnace rename — path-label fix
271
-
272
- - `fireforge furnace rename <old> <new>` for a custom component now reports `components/custom/<new>/` instead of `components/customs/<new>/` in the guidance that follows a successful rename (and in the "directory not found" / "target directory already exists" error messages). The actual filesystem operations always used `furnacePaths.customDir` / `furnacePaths.overridesDir` correctly — this was a cosmetic mis-pluralisation produced by appending `s` to the `custom` / `override` furnace-state key — but operators who copied the path from the message to a `cd` or `ls` invocation hit "no such file or directory". Override renames continue to name `components/overrides/<new>/` (which was already correct by coincidence with the plural on-disk dir). A `componentDirLabel` helper centralises the singular/plural pick so a future refactor cannot re-introduce the drift.
273
-
274
- ### Furnace preview — build + toolchain preflight
275
-
276
- - `fireforge furnace preview` now refuses before staging components or running the multi-minute `mach storybook upgrade` npm install when either (a) no `obj-*/dist/` tree exists, or (b) `.cargo/config.toml` is absent under the engine directory. Motivating case on `fresh/`: on an otherwise-unbuilt engine, preview staged workspace components into `engine/`, npm-installed 1023 packages into the upstream Storybook workspace, and only then failed deep in `mach storybook` with errors about `.cargo/config.toml` and `chrome-map.json` — both artefacts `fireforge build` / `fireforge bootstrap` would have produced. The new preflight catches the dominant failure mode (no `dist/`) fast via the existing `hasBuildArtifacts` helper and points the operator at `fireforge build`; the secondary check fires on a bootstrap-incomplete tree and points at `fireforge bootstrap`. Extracted into `assertPreviewPrerequisites` so `furnacePreviewCommand` stays under the per-function LOC budget as the preflight list grows.
277
-
278
- ### Register — `.inc.xhtml` fragments routed through `getUnregistrableAdvice`
279
-
280
- - The browser-content registration pattern in `src/core/manifest-rules.ts` now excludes `.inc.xhtml` fragments under `browser/base/content/`. Motivating case on `fresh/`: wiring a `browser/base/content/fresh-fragment.inc.xhtml` into `browser.xhtml` with `fireforge wire ... --dom …` then running `fireforge status` flagged the fragment as `Potentially unregistered`, and `fireforge register <fragment>.inc.xhtml --dry-run` proposed a bogus `browser/base/jar.mn` entry. `.inc.xhtml` files are deliberately consumed via `#include` from a registered chrome document; they don't need a separate chrome URI entry. The narrowed regex now routes these paths through `getUnregistrableAdvice`, which emits specific guidance: "`.inc.xhtml` fragments are consumed via `#include` from a registered chrome document — run `fireforge wire <name> --dom <path>`, or add the `#include` directive manually in the top-level chrome document." Plain `.xhtml` files under `browser/base/content/` still match the registrable pattern.
281
-
282
- ### Stackless precondition errors
283
-
284
- - `getProjectRoot` now throws a typed `ConfigNotFoundError` (exit code `CONFIG_ERROR` / 2) instead of a plain `Error` when no `fireforge.json` exists in any ancestor of the current working directory. Motivating case on `fresh/` before setup: every precondition-checking command (`doctor`, `status`, `download`, `import`, `build`, `run`, `test`, `lint`, `verify`) printed `Unexpected error: Could not find fireforge.json...` followed by a full JS stack trace — a routine user mistake surfaced as what looked like an internal crash. `withErrorHandling` now routes the `FireForgeError` subclass through `logError(error.userMessage)` and exits with the CONFIG_ERROR code; the stack-dump fallback stays in place for genuinely unexpected errors. The user-facing message is the already-defined `ConfigNotFoundError` copy — `This directory does not appear to be a FireForge project. Navigate to your project root directory, or run "fireforge setup" to initialize a new project.`
285
-
286
- ### Packager NoneType hint
287
-
288
- - `fireforge package` now captures `mach package`'s streamed output (via the new `machPackageCapture` helper that layers over `runMachCapture`) and feeds the stderr tail through `explainMachError` on non-zero exit. A new hint pattern matches the `AttributeError: 'NoneType' object has no attribute 'open'` / `packager.py` co-occurrence (either ordering) and surfaces an actionable guidance line: "This usually means the packager was handed an incomplete `obj-*/dist/` tree — e.g. running `fireforge package` before a full `fireforge build` (not --ui) completed. Re-run `fireforge build` to completion … before rerunning `fireforge package`." Motivating case on a real `hominis/` tree that reached `mach package` but had not completed a full build: the raw mach traceback was surfaced but wrapped only in "Packaging failed with exit code 1"; operators had to read the Python traceback to learn that the `obj-*/dist/` tree was incomplete. The hint now lands in the thrown `BuildError` directly after the generic exit-code line, so the operator reads the recovery instruction before scrolling up through the traceback.
289
- - The legacy `machPackage` helper stays exported for callers that only need the exit code; the new variant is additive. A negative-match test pins the pattern to the specific `packager.py` + `.open` pairing so unrelated `NoneType` errors elsewhere in mach output don't falsely trigger the hint.
290
-
291
- ### Run + watch — bundle-readiness agreement
292
-
293
- - `fireforge run` now probes for the launchable binary (`obj-*/dist/<App>.app/Contents/MacOS/<binary>` on macOS, `obj-*/dist/bin/<binary>` on Linux, `obj-*/dist/bin/<binary>.exe` on Windows) via the new `hasRunnableBundle` helper in `src/core/mach-build-artifacts.ts`. Motivating case on a mid-build `hominis/` tree: `fireforge run` failed inside `mach run` because `dist/Hominis.app/Contents/MacOS/hominis` didn't exist yet, while `fireforge watch` happily announced "Using build artifacts from obj-…/" and entered watch mode — the two commands disagreed about whether the obj dir was usable. `run` now refuses up-front with a targeted error naming the missing path ("the expected binary at `obj-debug/dist/MyBrowser.app/Contents/MacOS/mybrowser` is missing — the build may have aborted or is still in progress"). `watch` stays permissive (it exists to drive rebuilds of partially-built trees) but now reports the bundle state in its startup banner: `Using build artifacts from obj-…/ (bundle: runnable)` or `Using build artifacts from obj-…/ (bundle: pending — watch will rebuild)`, so the operator can see at a glance why `run` would refuse right now.
294
- - `hasRunnableBundle` is exported from `mach.ts` alongside `hasBuildArtifacts`, degrades to `runnable: false` on readdir failure (no throw), and reports `expectedPath` even on the not-runnable branch so error copy can always name what to look for on disk. The three-platform probe is covered by explicit unit tests that mock `getPlatform`.
295
-
296
- ### Token add — Furnace-initialized precondition
297
-
298
- - `fireforge token add` now refuses before any normalization when `furnace.json` is missing under the project root. Motivating case on `fresh/`: running `token add surface-test '#abcdef' --category 'Colors — Canvas' --mode static --dry-run` on a brand-new project surfaced a warning about the missing `furnace.json` and then hard-failed with `Token CSS file not found: browser/themes/shared/freshforge-tokens.css` — technically correct, but the missing tokens CSS is a downstream artefact of Furnace not being initialized. The new guard throws a targeted `FurnaceError`: `Token management requires Furnace. Run "fireforge furnace init" first, then rerun "fireforge token add …"`. Projects with an initialized Furnace hit the existing "tokens CSS file not found" path unchanged when they're missing the rendered tokens file specifically (e.g. after a manual deletion).
299
-
300
- ### Re-export — stale-manifest advisory without `--scan`
301
-
302
- - `fireforge re-export <patch>` now warns at the start of a single-patch run when `--scan`/`--files` is not set AND one or more files in `patches.json`'s `filesAffected` no longer exist on disk. Motivating case on `fresh/lab/`: after discarding a file and re-exporting the patch without `--scan`, the refreshed patch body targeted fewer files than the manifest claimed, and `fireforge verify` then failed on manifest-consistency. The warning names the missing paths and spells out both recovery modes: re-run with `--scan` to reconcile `filesAffected` with the worktree, or `--files <paths>` to set the list explicitly. Behaviour is unchanged when `--scan` / `--files` is set (those modes already reconcile the manifest); this is advisory-only.
303
-
304
- ### Download — indexing banner
305
-
306
- - `fireforge download` now emits a one-line `Indexing downloaded source into git (one-time; typically 1–3 minutes on a ~600 MB Firefox tree)...` before starting the git-init spinner. Motivating case on `fresh/`: after the 609 MB archive download completed, the `git add -A` pass ran silently for minutes, long enough that a CI log tail or non-TTY shell looked like the command had hung. The banner lands via `info` (not the interactive spinner) so non-TTY wrappers still see the heads-up in log scrollback.
307
-
308
- ### Status — atomic-temp-file filter
309
-
310
- - `fireforge status` no longer surfaces FireForge's own in-flight atomic-write temp files in any output mode. Motivating case on `hominis/`: a `status --json` that coincided with a `brand.ftl` or `mozconfig` write briefly listed paths like `.brand.ftl.fireforge-tmp-12345-<uuid>` and `.mozconfig.fireforge-tmp-12345-<uuid>` alongside real changes. `src/utils/fs.ts` now exports `FIREFORGE_TMP_PATH_PATTERN`, a regex anchored on the exact shape `createAtomicTempPath` produces (`<dir>/.<filename>.fireforge-tmp-<pid>-<uuid>`), and `status.ts` filters every status entry through it after `expandDirectoryEntries` and before classification. All status modes — default, raw, unmanaged, ownership, json — apply the same filter, so a late `status` call during a large write produces the same output regardless of which view the operator chose. The pattern is tight enough to let an operator-named `.notes.fireforge-tmp-backup` (no PID+UUID continuation) pass through unfiltered.
311
-
312
- ### Lifecycle — `test --doctor` exits cleanly on passing preflight
313
-
314
- - `src/core/marionette-preflight.ts` now spawns the browser in its own process group (`detached: true`) and sends SIGTERM / SIGKILL via `process.kill(-pid, …)` in the finally block, with an explicit `child.stderr?.destroy()` to close the local end of the stderr pipe. Before this, `fireforge test --doctor` routinely printed `Marionette preflight: PASS` and then hung indefinitely in `uv__io_poll` — the Python mach wrapper exited under SIGTERM but Firefox (a grandchild) inherited the stderr FD and kept Node's event loop alive. A process-group kill takes down the whole tree; destroying the stderr stream closes the local handle regardless of what the grandchild does with its copy. The non-win32 guard falls back to `child.kill(signal)` on Windows, where negative-PID signalling is not a supported kernel primitive.
315
-
316
- ### Lifecycle — `download` progress visibility during git indexing and patch-touched restore
317
-
318
- - `src/core/git.ts` adds a 15-second heartbeat during the monolithic `git add -A`: every tick reports elapsed seconds through `onProgress` so a spinner or non-TTY log-scraping CI job both see that indexing is still making progress. A ~600 MB Firefox tree takes 60–120 seconds for the monolithic add, during which git emits nothing on stdout/stderr; without the heartbeat the CLI looked hung precisely during the expected work window and an eval run consistently SIGINT'd mid-way assuming the process had stalled. `src/commands/download.ts` also wraps the post-commit `cleanPatchTouchedFiles` pass in its own spinner so the phase is visibly distinct from the preceding git-add window. Neither change alters the underlying timing budget — they surface progress that was always there.
319
-
320
- ### Doctor — post-interrupt engine state check and watchman preflight
321
-
322
- - `fireforge status` now surfaces a single recovery banner when the engine git repository has no HEAD, pointing at `fireforge download --force`. Before this, interrupting a `fireforge download` during the initial git-add left engine/ extracted but with an unborn HEAD; `status` then flooded the output with hundreds of thousands of untracked entries plus a truncation warning — correct, but not actionable. `src/commands/status.ts` now probes HEAD up-front (via `getHead` + `isMissingHeadError`) and throws a `GeneralError` with the recovery guidance, which matches doctor's existing row for the same state. `--raw` / `--json` modes get the same error message but no banner, so their consumers still see the structural failure.
323
- - `src/commands/doctor.ts` adds a `Watchman available` check (warning severity when watchman is not on `PATH`). Before this, operators got through setup → download → bootstrap → build without ever seeing the requirement, then hit it only when `fireforge watch` refused to start. A warning row is the right shape: most projects never run watch, so a missing watchman should not fail `doctor` outright — but the gap is now visible during the normal onboarding sweep. The README setup requirements section lists watchman explicitly, and the check runs alongside `mach available` so the location is ergonomic.
324
-
325
- ### Setup — package.json license kept in sync with fireforge.json
326
-
327
- - `src/commands/setup-support.ts` now rewrites an existing root `package.json`'s `license` field to match the project license picked during `fireforge setup` (instead of only writing a new minimal `package.json` when none existed). Every other field is preserved — `name`, `description`, `dependencies`, `scripts`, `private`, author metadata. Before this, `fireforge setup --force` that selected a new license (e.g. EUPL-1.2 → 0BSD) updated `fireforge.json` but left the package.json license stale, so the two files described different projects. A malformed existing `package.json` is left alone rather than rewritten; the file's trailing-newline state is preserved so a hand-edited convention survives the sync.
328
-
329
- ### Branding — generated files carry the project license header
330
-
331
- - `src/core/branding.ts` now stamps `configure.sh`, `brand.properties`, and `brand.ftl` with the license header that matches the project's `fireforge.json` `license` field (via `getLicenseHeader(config.license, 'hash')` from `license-headers.ts`). Before this, the three generated files hard-coded the Mozilla MPL-2.0 header regardless of the project license, so a 0BSD / EUPL-1.2 / GPL-2.0-or-later fork's first export failed `patch-lint`'s `missing-license-header` on its own generated branding with no actionable fix. The fix threads the license through `BrandingConfig` (optional, defaults to `DEFAULT_LICENSE` for pre-0.16 callers) and `build-prepare.ts` sets it from the active project config. Copied upstream branding assets under `browser/branding/<binary>/` still carry Mozilla's MPL-2.0 headers (those files are Mozilla-copyrighted template material) and are auto-exempted by the lint rules below.
332
-
333
- ### Patch lint — branding tier for `large-patch-lines`, auto-exemption for branding headers and colors
334
-
335
- - `src/core/patch-lint.ts` adds a `branding` threshold tier to `lintPatchSize` (notice 3000 / warning 8000 / error 20000) and selects it when every file in a patch lives under `browser/branding/`. Before this, a first-export of setup-generated branding landed at 15,904 lines (localized `brand.ftl` across many locales + SVG path data + copied upstream CSS) and fired the general hard limit of 3000 as an error — even though the patch was already the minimum branding diff. The branding tier keeps the soft warning (8000) visible but moves the hard limit to a threshold that genuinely suggests "something other than branding is bundled in here too". Mixed patches (branding + other trees) still fall through to the general tier so an operator bundling unrelated edits into a branding change still sees the warning.
336
- - `lintPatchedCss` now auto-exempts files under `browser/branding/` from the `raw-color-value` check. Copied-from-`unofficial` branding CSS contains hex literals (about dialogs, installer pages, branded chrome) that are legitimate Mozilla design decisions, not fork-editorial choices — listing every copied path in `patchLint.rawColorAllowlist` would add dozens of entries that the operator did not author. The narrower exemption applies by path prefix, so a fork that authors an entirely new branding tree under a different top-level directory still sees the lint fire. `lintNewFileHeaders` adds a parallel carve-out: a new file under `browser/branding/` that starts with any recognised license header (MPL-2.0, EUPL-1.2, 0BSD, GPL-2.0-or-later) passes the check, even when the header does not match the project license — an operator forking Mozilla's branding template inherits Mozilla's MPL-2.0 header and cannot legitimately rewrite it to another license without misrepresenting authorship. A file with no header at all still fails.
337
-
338
- ### Patch manifest — binary file paths survive verify, repair, and rebuild
339
-
340
- - `src/core/patch-files.ts` now delegates `getAllTargetFilesFromPatch` to `extractAffectedFiles` from `patch-parse.ts`, which matches both `diff --git a/… b/…` and `+++ b/…` lines. Before this, the custom `+++ b/…`-only regex missed every file in a `GIT binary patch` section (binary diffs have no `+++` line, only a diff header), so `fireforge verify` reported `files-affected-mismatch` against branding patches and `fireforge doctor --repair-patches-manifest` "repaired" the mismatch by rewriting the manifest down to the text-only subset — hiding the true scope of the patch. Three downstream callers (`patch-manifest-query.ts`, `patch-manifest-consistency.ts`, `patch-apply.ts`) inherit the fix without local changes. The returned list is alphabetically sorted (matching `extractAffectedFiles`); callers already compare `filesAffected` as a set, so the order change is API-safe.
341
-
342
- ### Import — `--until` scopes patch-integrity checks to the targeted range
343
-
344
- - `fireforge import --until <filename>` now filters the patch-integrity and manifest-consistency issues to patches at or before the target, so a malformed later patch does not block replaying an earlier good subset. Before this, `validatePatchIntegrity` returned issues for every patch on disk and the block / force-prompt pathway fired on all of them — an operator with `--until 001-foo.patch` who wanted to step around a broken `002-bar.patch` got "Refusing to import while 1 patch integrity issue" even though patch 2 was out of scope. The new `buildUntilFilenameSet` helper (in `src/commands/import.ts`) resolves the `--until` target via the same `.patch`-suffix-tolerant lookup `applyPatchesWithContinue` already uses, and the filter applies to version-compatibility warnings, the "Found N patches to apply" banner, and the dry-run listing too. Structural manifest issues (missing / unparseable `patches.json`) remain global — those block any import regardless of scope, because the manifest has to be valid to resolve filenames against in the first place.
345
-
346
- ### Patch delete — dependency warning clarifies runtime-only impact
347
-
348
- - `src/commands/patch/delete.ts`'s refusal message now reads `"N later patch(es) contain import statements that reference files created by <target>. Patch application itself will still succeed, but runtime imports will fail at browser startup until those files are re-introduced."` instead of the previous "later patches depend on files created by X". The old phrasing implied patch application itself would fail, which is incorrect — `git apply` does not resolve `ChromeUtils.importESModule` specifiers and will happily apply a queue whose imports point at deleted files. An eval run confirmed this directly: forcing `patch delete` with `--force-unsafe` and then re-importing the mutated 20-patch queue produced zero rejects. The reworded message lets operators planning a rename / refactor (the legitimate "I'll re-introduce the imported files under a new name") make the call without being scared off by a phantom apply-time risk; the runtime risk is still clearly named.
349
-
350
- ### Token add — `--mode` option marked `(required)` in help output
351
-
352
- - `src/commands/token.ts` appends `(required)` to the `--mode` description. Commander's `.makeOptionMandatory(true)` enforces the option at runtime, but it does not render a `(required)` marker in `--help` output the way `.requiredOption` does — a real invocation derived from the built-in help failed with `error: required option '--mode <mode>' not specified` despite help listing the option alongside normal optional flags. Switching to `.requiredOption` would have lost the `.choices(['auto', 'static', 'override'])` enforcement, so keeping the Option-object form with an explicit description suffix is the minimal fix that makes help honest. Runtime validation is unchanged.
353
-
354
- ### Furnace validate — customized built-ins accepted as valid components
355
-
356
- - `classExtendsMozLitElement` in `src/core/furnace-validate-helpers.ts` now accepts a class that extends `HTMLAnchorElement`, `HTMLButtonElement`, or any other `HTML<Something>Element` **when** the same module calls `customElements.define(..., ..., { extends: "<tagname>" })` with a literal `extends:` option. Before this, `fireforge furnace override moz-support-link --type full` wrote the upstream source verbatim (the toolkit's `moz-support-link` extends `HTMLAnchorElement` with `customElements.define("moz-support-link", ..., { extends: "a" })`) and `furnace validate` then rejected it with `[not-moz-lit-element] Component class must extend MozLitElement` — blocking deploy on a legitimate upstream pattern. Both halves of the shape are required: a class that extends `HTMLButtonElement` without the matching `extends:` option is almost certainly an author mistake and still fires `not-moz-lit-element`. The autonomous-element path (`extends MozLitElement`) is unchanged.
357
-
358
- ### Furnace chrome-doc — jar.inc.mn shared path prefix and XHTML preprocessor flag fixes
359
-
360
- - `src/commands/furnace/chrome-doc-templates.ts` emits `../shared/<name>-chrome.css` (was `shared/<name>-chrome.css`) in the `jar.inc.mn` entry. `jar.inc.mn` is included from each theme-specific manifest (`browser/themes/osx/jar.mn`, `linux/jar.mn`, `windows/jar.mn`) where every existing entry resolves source paths relative to the including manifest's directory — a bare `shared/` produced `obj-.../browser/themes/osx/shared/<name>-chrome.css`, which does not exist. The new `../shared/` prefix climbs out of the theme-specific directory and lands on the real `browser/themes/shared/` tree.
361
- - `jarMnEntriesForChromeDoc` no longer marks the scaffolded XHTML or JS entries with the `*` preprocessor flag. The generated XHTML / JS contain no `#filter` / `#expand` / `#include` directives, and mach's `process_install_manifest.py` fails the whole package step with "no preprocessor directives found" when a `*`-flagged entry has nothing for the preprocessor to do. A fork that later needs brand substitution can reintroduce `*` alongside a top-of-file `#filter substitution` directive.
362
-
363
- ### Furnace packaging test — probes both dist/bin/browser and app-bundle layouts
364
-
365
- - `src/commands/furnace/chrome-doc-tests.ts`'s scaffolded packaging test now probes both candidate packaged-tree layouts per asset via a `probeEither(primary, fallback, description)` helper: `<AppDir>/chrome/browser/…` (the unpacked layout when `XCurProcD` honours `firefox-appdir = "browser"` and resolves into `dist/bin/browser/`) AND `<AppDir>/browser/chrome/browser/…` (the macOS .app-bundle and some ESR layouts where `XCurProcD` sits one level above `browser/` even when the appdir directive is set). The assertion only fails when both candidates miss, which is the actual stale-build / missing jar.mn case. Before this, the eval on macOS consistently showed the test reporting "missing" against a file that was packaged correctly, just at the bundle-layout path the probe did not walk.
366
-
367
- ### Furnace register — xpcshell.toml manifests get correct wiring guidance
368
-
369
- - `src/core/manifest-rules.ts`'s `getUnregistrableAdvice` now emits xpcshell-specific guidance when the path ends in `xpcshell.toml`, pointing at `XPCSHELL_TESTS_MANIFESTS` in the nearest moz.build. Before this, the generic `testMatch` branch caught the path and suggested registering a non-existent `browser.toml` — wrong manifest type, and a path that did not exist in the generated tree. The new branch mirrors the intentional design in `create-xpcshell.ts`, which scaffolds the manifest and prints the same warning so the operator knows wiring is deliberately manual. Browser-chrome test registration (`browser.toml`) is unchanged and still auto-registered.
370
-
371
- ### Furnace xpcshell scaffold — filesystem probe replaces browser-global module-load test
372
-
373
- - `fireforge furnace create --xpcshell` now scaffolds `test_<name>_packaged.js` (was `test_<name>_module_loads.js`) with a filesystem-probe test instead of a `ChromeUtils.importESModule` call. Lit-based components import `chrome://global/content/vendor/lit.all.mjs`, which references `window` at module-load time — xpcshell has no `window` global, so the old module-load path reliably failed with `ReferenceError: window is not defined` for every Lit-based fork component (the eval scenario). The replacement probes `XCurProcD` at both candidate layouts for `<name>.mjs` and `<name>.css`, matching the chrome-doc packaging scaffold's pattern. This tests what xpcshell CAN test (packaging) without tripping on browser-only globals; functional UI assertions still belong in a browser-chrome mochitest (`furnace create --test-style browser-chrome`) and the scaffolded test carries an inline comment pointing there.
374
-
375
- ### Furnace lock — stale lock cleanup via PID-first check, signal handler, and doctor repair
376
-
377
- - `src/core/file-lock.ts`'s `removeIfStaleLock` now checks the PID file BEFORE the age gate. If the lock's PID file says its owner is no longer alive, the lock is removed immediately regardless of age. Before this ordering change the age gate (5 min default) fired first, so a lock written by a process the user had just SIGINT'd sat undisturbed for the full window even though its owner was explicitly gone — the next `fireforge furnace …` / `fireforge test --build` then timed out waiting, and the only operator recovery was to `rm -rf .fireforge/furnace.lock` manually. The age-only fallback still applies when the PID file is missing (older release locks, externally-created lock directories).
378
- - `bin/fireforge.ts`'s signal handler calls a new `forceReleaseFurnaceLocksForActiveOperations()` sweep after `rollbackActiveOperationsForSignal` and before `process.exit`. `withFileLock`'s `finally { rm }` never runs when the handler calls `process.exit`, so without the sweep a SIGINT during `furnace preview` left the lock behind and subsequent commands stalled. Errors are logged but swallowed — a slow I/O failure at shutdown cannot prevent the process from exiting.
379
- - `src/commands/doctor-furnace.ts` adds a `Furnace lock` check that detects and (under `--repair-furnace`) removes a stale lock directory. Two signals flag a lock as stale: (1) PID file present but owner dead, (2) PID file absent AND directory older than 60s. Without `--repair-furnace` the check is warning-only; with it, the lock is `rm -rf`'d. This is the recovery path when the signal-handler sweep misses (SIGKILL, older FireForge release lock, externally-created directory).
380
-
381
- ### Diagnostics — xpcshell-appdir wins over stale-build-artifact on generic resource failures
382
-
383
- - `src/commands/test.ts`'s `hasStaleBuildArtifactsSignal` no longer matches `resource:///modules/distribution.sys.mjs` — the signal now requires a branding-specific path (`chrome://branding/locale/brand.properties`, `browser/branding/<name>/moz.build`). Before this narrowing, any `Failed to load resource:///modules/…` failure routed to the "rebuild" advice, which was wrong for the eval's Hominis case (`HominisStore.sys.mjs` missed because of an appdir / packaging issue, not stale artifacts — rebuilding did nothing). Branding-specific failures still win ahead of the xpcshell-appdir hint; cases that used to match the distribution literal now fall through to xpcshell-appdir, which is the right first guess for generic `resource:///modules/…` module-load failures.
53
+ - Hardened release and config security paths.
54
+ - Fixed `config --force` read/write behaviour.
55
+ - Improved download, status, and setup feedback.
56
+ - Added safer Furnace init, create, preview, and chrome-doc behaviour.
57
+ - Improved lint, build audit, and rebase reliability.
384
58
 
385
59
  ## 0.15.0
386
60
 
387
- ### Re-export opt-in `--stamp` and per-patch `lintIgnore`
388
-
389
- - New `fireforge re-export --stamp` stamps `sourceEsrVersion` on every successfully re-exported patch to the current `firefox.version` from `fireforge.json`. Previously `re-export` only ever refreshed patch bodies and `filesAffected`; version stamping was exclusive to `rebase`'s `stampPatchVersions` call (plus `doctor --repair-patches-manifest`). An operator asked to "re-export targeting a new ESR" had no in-surface signal that the command could not deliver the version half of that request, and had to route through the full rebase flow (which requires a Firefox source re-download) purely to update a version string. `--stamp` closes that gap for the case where the re-export cleanly refreshes every selected patch — a partial run (any skipped or failed patch) refuses to stamp and the success line notes the refusal, so a torn "some bodies refreshed at old version, some at new" state is not representable. The command description and `--help` text now explicitly call out that `re-export` does NOT change `sourceEsrVersion` by default.
390
- - New optional `lintIgnore: string[]` field on each patch entry in `patches.json` lists lint check IDs to suppress for that patch specifically. Surgical alternative to `--skip-lint` (which downgrades _every_ error to a warning) for the class of patch that is advisory-noisy by nature — cohesive branding bundles, localised-resource packs, auto-generated manifests — where a rule like `large-patch-lines` is not actionable. Threaded through `lintExportedPatch` as an optional `ignoreChecks` filter, honoured by `re-export`, `re-export --files`, and `lint --per-patch`. The file-level `fireforge-ignore:` comment markers for `raw-color-value` and `forward-import` are unchanged; `lintIgnore` fills the gap at the patch level where no per-line marker can exist (the `.patch` body is regenerated on every export). Unknown check IDs are a no-op so the metadata documents the _intent_ to suppress even if the rule is renamed later.
391
- - Motivating case: re-exporting a 22-patch queue onto 140.9.0esr after `download --force` failed on `001-branding-branding-assets` with `ERROR [large-patch-lines] (patch): Patch is 15665 lines (hard limit: 3000)` — a 57-file branding bundle that genuinely cannot be split. The only escape was `--skip-lint` (downgrades 22 patches' worth of errors) or the full `rebase` flow (already-wasted Firefox download). With `lintIgnore: ["large-patch-lines", "large-patch-files"]` on that one patch, `re-export --all --scan --stamp` now completes in one call.
392
-
393
- ### Lint — `--per-patch` scope and aggregate-mode hint
394
-
395
- - New `fireforge lint --per-patch` scopes the lint diff to each patch's own `filesAffected` in turn rather than the aggregate `git diff HEAD` across every applied patch. Motivating case: running `fireforge lint` (no args) after `fireforge import` / `fireforge rebase` has just applied a 22-patch queue produces an aggregate diff of every patch summed, which means the patch-size advisory rules (`large-patch-lines`, `large-patch-files`) fire against the sum — e.g. `Patch is 37529 lines`, `Patch affects 126 files` — with `Lint failed` and a non-zero exit code on a repo that is actually in a good state. The aggregate framing reads as a task-specific regression when it is really an artefact of aggregation. `--per-patch` restates the scope so each patch lints as its own isolated diff, honours the patch's own `lintIgnore` entries, and runs the cross-patch rules (`duplicate-new-file-creation`, `forward-import`) once over the whole queue so queue-level findings are not lost by the rescoping. Mutually exclusive with explicit file paths (the two scope contracts are different).
396
- - Aggregate-mode runs that would otherwise surface a `large-patch-lines` / `large-patch-files` error against a multi-patch queue now print a one-line `NOTE: aggregate diff across all applied patches. Use 'fireforge lint --per-patch' to lint each patch individually; patch-size rules fire against the sum in aggregate mode.` ahead of the failure message, so the operator reaches the per-patch escape hatch without having to read the help text first. The note fires only when the patches directory has at least two entries AND the rule that fired is a patch-size rule — a single-patch queue or a non-size rule behaves identically to before.
397
- - Per-patch output namespaces every issue with its owning patch filename (`ERROR [relative-import] 001-ui-test.patch :: browser/base/content/a.ts: …`) so triage can attribute findings without cross-referencing patches.json. The passing summary reports how many patches were actually linted (patches with no files on disk or an empty projected diff are silently skipped — they are not a finding).
398
-
399
- ### Test — xpcshell appdir auto-injection
400
-
401
- - `fireforge test` now auto-injects `--app-path=<absolute>` into mach test invocations whose nearest `xpcshell.toml` sets `firefox-appdir = "browser"` on a rebranded fork (appname != `firefox`). Without this, every `resource:///modules/<name>.sys.mjs` import inside the harness throws because the upstream xpcshell harness reads the appdir override under the appname-keyed manifest field (`<appname>-appdir`) — the literal `firefox-appdir = "browser"` directive is silently ignored when `appname` is anything other than `firefox`, so `appPath` falls back to `xrePath` (one level above the real app root). The resolver lives in the new `src/core/xpcshell-appdir.ts`: it walks each test path to the nearest `xpcshell.toml`, reads `mozinfo.json` for the active appname, prefers any `<appname>-appdir` already in the manifest (so an operator who already migrated is not overridden), and otherwise probes `<objDir>/dist/bin/<value>` and `<objDir>/dist/<bundle>.app/Contents/Resources/<value>` for the absolute target. Operator overrides via `--mach-arg=--app-path=…` always win and the resolver is skipped silently when one is detected. Mismatches across multiple test paths (different manifests resolving to different app dirs) and unresolvable manifest values (no candidate under `dist/`) are surfaced as warnings rather than guessed at, so triage can reach the underlying cause instead of debugging a wrong path.
402
- - The `xpcshell appdir` diagnostic that fires when the injection did not prevent the symptom now branches its "Likely triggers" copy on whether the auto-injection ran. When it ran and the failure persists, the message says so and points at packing or layout mismatches; when it did not run (no manifest found, or appname=`firefox`), the original appname-key explanation is preserved. Adds a fourth troubleshooting option — adding `<appname>-appdir = "browser"` alongside `firefox-appdir = "browser"` in the manifest — so operators can adopt the durable fix instead of relying on injection on every run.
403
- - `fireforge test` was previously diagnostic-only for this class of failure: it detected the symptom and surfaced a hint suggesting `--mach-arg` overrides. The hint remains as a fallback for cases the resolver cannot fix, but no manual flag is needed for the common case.
404
-
405
- ### Run — `--smoke-exit` for unattended chrome smoke checks
406
-
407
- - New `fireforge run --smoke-exit <seconds>` launches the real built browser with the dev profile, streams the merged console line-by-line, sends `SIGTERM` to the entire child process group at the deadline, and exits non-zero when any `JavaScript error:` / `console.error:` / `[JavaScript Error]` / `###!!! [Parent]` line surfaces inside the smoke window without matching the operator-supplied allowlist. Closes the headless-vs-real-chrome gap that previously forced agents to choose between `fireforge run` (no exit hook, hangs on a human) and `fireforge run --headless` (does not load `browser.xhtml`/`<custom>.xhtml`, so chrome-window constructor errors stay invisible). The motivating case was `TypeError: lazy.HominisEvents.HOMINIS_TOPICS is undefined` thrown from a chrome-document constructor on every window-open, which `--headless` never observed because the main document was never instantiated.
408
- - Allowlist surface lives in the new `src/core/smoke-patterns.ts`: regex error patterns (`SMOKE_ERROR_PATTERNS`) anchored on the start of each line so embedded mentions of the same prefix inside descriptive prose do not trip the scanner, plus `compileAllowlistFromFile` and `compileAllowlistFromStrings` for `--console-allow-file <path>` and repeatable `--console-allow <regex>` inputs respectively. Allowlisted hits still count toward the summary so operators can see how many lines were suppressed; only unallowed errors drive the exit code. A blank or `#`-prefixed line in an allowlist file is skipped, and a malformed regex throws fast at CLI parse time rather than silently matching nothing.
409
- - New `--capture-console <file>` mirrors the captured stream to a file path so post-exit inspection has the raw log without re-running. Mirroring is inline with line dispatch and the file is fsync'd on close.
410
- - Smoke-run exit codes are wired distinctly from `BUILD_ERROR`: `SMOKE_EXIT_FAILURE = 12` (unallowed console errors observed inside the window), `SMOKE_LAUNCH_FAILURE = 13` (browser exited non-clean before the window elapsed — crashes before console wiring, missing profile, etc.). CI pipelines can route these distinctly from compile/config failures and from the existing FireForge exit codes. Carried by the new `SmokeRunError` in `src/errors/run.ts` so the throw site signals both the message and the exit code in one type. The `runMachSmoke()` wrapper in `src/core/mach.ts` handles the deadline-driven SIGTERM/SIGKILL escalation (default 10 s grace between SIGTERM and SIGKILL because Firefox's `AsyncShutdown` and `profileBeforeChange` blockers can take ~5–10 s to flush in-memory state; a shorter grace risks corrupting the dev profile mid-quit). POSIX-only — process-group semantics do not map cleanly onto Windows, so the flag is rejected up-front there. A smoke window shorter than 30 s also surfaces a one-line warning recommending a longer interval, since cold-start time alone can consume that budget on a debug build.
411
-
412
- ### Furnace create — `--shared-ftl` for feature-scoped Fluent bundles
413
-
414
- - `furnace create <tag> --localized --shared-ftl <chrome-uri>` now scaffolds a component that participates in a pre-existing feature-scoped `.ftl` bundle (e.g. `browser/hominis-dock.ftl`) instead of getting its own per-component stub. Without the flag, every `--localized` create produced a `<tag>.ftl` under `components/custom/<tag>/` plus an `insertFTLIfNeeded("toolkit/global/<tag>.ftl")` call in the `.mjs`; on a feature like a dock with eight components sharing one bundle, this stranded seven empty per-component stubs in the workspace and required hand-editing every `.mjs` to point `insertFTLIfNeeded` at the shared file (and accepting that the seven empty stubs would still get packaged into `Resources/localization/en-US/toolkit/global/`). The flag short-circuits all of that: the `.mjs` is generated with the shared path baked in via `generateMjsContent`, the per-component `.ftl` is not written, and the `furnace.json` `custom` entry carries a new `sharedFtl` field so subsequent runs (and validators) know the component participates in a shared bundle.
415
- - `--shared-ftl` implies `--localized`; combining `--no-localized` with `--shared-ftl` is rejected fast-fail with an actionable message rather than letting the cross-field check inside the config parser surface the same error later. The interactive features prompt is suppressed when `--shared-ftl` is set so the operator is not asked to flip a flag we are about to enforce.
416
- - `furnace apply` and `furnace remove` honour `sharedFtl` symmetrically: apply does NOT copy a per-component `.ftl` into the FTL tree nor add a locale `jar.mn` entry (the shared file is registered by whoever owns the feature bundle), and remove early-returns from `removeCustomFtlJarMnEntry` so dropping our component's reference does not orphan every other participant in the shared bundle. The dry-run path mirrors apply, so a `--dry-run` plan accurately previews the actual file set.
417
- - `furnace validate`'s `missing-ftl` structural check is skipped for `sharedFtl` components — there is no per-component `<tag>.ftl` to require. The remaining error message also points at `sharedFtl` as the third option ("Create the file, set localized: false in furnace.json, or switch to sharedFtl") so an operator who hits the warning sees the structured way out alongside the existing two.
418
- - The `sharedFtl` value is validated in one place (`src/core/shared-ftl.ts`) by both the CLI flag path and the `furnace.json` parser, so the two entry points cannot drift. Backticks, backslashes, and `${` are rejected because the value is interpolated verbatim into the generated `.mjs` template literal — without that check, an unsafe input would either close the template or open an executable expression. `--no-localized + --shared-ftl` and `--shared-ftl=""` (empty after trim) are rejected with structured reasons. The custom-component schema parser was extracted from `furnace-config.ts` into `src/core/furnace-config-custom.ts` to keep the main config module under the per-file LOC budget as the schema continues to gain opt-in fields (`composes`, `keyboardCovered`, `sharedFtl`).
419
-
420
- ### Furnace validate — `no-keyboard-handler` composition awareness
421
-
422
- - `no-keyboard-handler` no longer warns when `@click` sits on a custom-element host whose `composes` entry names a native-interactive child (e.g. `moz-button`, `moz-toggle`, `moz-checkbox`, `moz-radio`, `moz-menulist`). The wrapper's `@click` handler catches keyboard activation transitively — `<moz-button>` itself dispatches `click` on Enter/Space via the platform — so a duplicate `@keydown`/`@keypress` handler on the wrapper would either no-op or double-fire alongside the child's built-in keyboard path. Previously a `hominis-pin-section` rendering a row of `<hominis-dock-button @click=…>` instances flagged `[no-keyboard-handler] Interactive element has @click but no keyboard event handler` on every validate, even though the entire row was fully keyboard-accessible.
423
- - New optional `keyboardCovered: true` field on a custom component's `furnace.json` entry forces the same skip when the wrapped inner element is hand-authored (e.g. a vendored `<button>`) or is a non-stock `moz-*` widget that does not appear in `composes`. Operator-asserted: setting this to `true` does not re-check the component, so it can be used to silence a genuine finding. The check's docstring calls out the preferred path of adding the wrapped tag to `composes` when that field applies, since `composes` carries semantic value beyond a11y.
424
- - Unchanged: synthetic interactive markup (`<div @click>`) and bare `<a>` without an `href` attribute still fire the warning. Those are the real keyboard-a11y hazards the rule was written for; the new branches are narrowly scoped to the composition cases that previously trained authors to ignore validator output.
425
-
426
- ### Test — diagnostics for harness failure modes
427
-
428
- - `fireforge test` now detects the `MochitestDesktop.http3Server` AttributeError that crashes the mochitest cleanup path and rewrites it as an actionable hint: branding registration is the lazy-init chain that initialises `http3Server`, so the AttributeError at teardown is almost always a downstream symptom of `chrome://branding` not registering correctly in the fork. The hint enumerates the three concrete checks (branding listed in `browser/branding/moz.build`, `chrome://branding/locale/brand.properties` resolves at runtime, `BROWSER_CHROME_MANIFESTS` registers the chrome.manifest) so operators do not chase the AttributeError as if it were the root cause. Surfaces as a `GeneralError` so the operator sees the diagnostic instead of the generic `BuildError("Tests failed")`. The crash itself remains an upstream Firefox harness bug — FireForge can only diagnose it.
429
- - New `fireforge test --mach-arg <arg>` (repeatable) forwards a single argument verbatim to `mach test`, after FireForge-managed flags. Escape valve for upstream xpcshell/mochitest flags FireForge does not model directly. Used by the xpcshell appdir hint as the operator's escape valve when auto-injection is the wrong fix; also useful for `--keep-going`, `--verbose`, or `--debugger` while iterating on a flaky test.
430
-
431
- ### Furnace create
432
-
433
- - `furnace create` now accepts `--dry-run` so operators can preview the planned file set, test scaffold, and `furnace.json` mutation without touching the workspace. Previously the only way to preview a create was `fireforge furnace create --help` followed by running the real command and inspecting the result after the fact — a workflow that stranded a component directory, a furnace.json entry, and (with `--with-tests`) test sources under `engine/` behind every aborted preview. The dry-run path runs every validation the real command would run (name shape, name conflicts, engine pre-existence, `--compose` target existence + cycle detection, prefix warning) BEFORE emitting the plan, so a preview that fails matches a real run that would fail. A new `src/commands/furnace/create-dry-run.ts` owns the plan formatter and the success-note formatter so the two renderings stay in lock-step when the scaffolded layout changes.
434
-
435
- ### Furnace validate — `missing-token-link` auto-detection
436
-
437
- - `missing-token-link` now auto-detects chrome host documents that mount a component without requiring the operator to configure `tokenHostDocuments`. The validator scans `browser/base/content/*.xhtml` for any document that references the component's tag name and adds those to the scan set alongside the configured (or default) host documents. Previously a fork that mounted `moz-mybrowser-canvas` from its own `mybrowser.xhtml` chrome document had to either configure `tokenHostDocuments` explicitly or ignore every `missing-token-link` warning for Hominis-hosted components; the warning false-fired on `browser.xhtml` (the upstream default) while the component's actual host document linked the tokens CSS correctly. The warning message now reads "none of the scanned chrome host documents" to reflect that the set can include both configured and auto-detected entries. Auto-detected documents that are already in `tokenHostDocuments` are deduplicated so they never render twice in the warning list.
438
-
439
- ### Build audit — known packaging transforms
440
-
441
- - The post-build audit now applies known source→chrome packaging transforms after jar.mn registration lookup and before the similarity scorer. Motivating case: `engine/browser/base/content/hominis.js` packages to `chrome/browser/content/browser/hominis.js` under dist/, but an unrelated patch placed a `browser/defaults/preferences/hominis.js` pref file in the same build. The source's jar.mn entry has no `(source)` annotation — just a bare target line — so the registration-aware resolver could not anchor the match. The scorer then tied both candidates at score=10 (every intermediate segment of the source is in the "generic" list so no bonus applies), `resolveBestArtifact` picked whichever the directory walk hit first, and the structural-relation check rejected every candidate as unrelated. The audit reported "missing" on the correctly-packaged chrome resource every build. The new `src/core/build-audit-transforms.ts` maps well-known source prefixes (`browser/base/content/`, `toolkit/content/widgets/`, `toolkit/content/`) to their upstream chrome suffixes and picks the first dist candidate whose absolute path ends with the expected suffix — treating the match as high-confidence (bypasses the structural-relation check). Rules are intentionally narrow to the subtrees whose packaging target is stable across every fork we know about; a fork that reroutes a known subtree can still win via `(source)` annotations in its own jar.mn.
442
-
443
- ### Register
444
-
445
- - `fireforge register <path>` now accepts both repo-root-relative paths (e.g. `engine/browser/base/content/foo.xhtml`) and engine-relative paths (e.g. `browser/base/content/foo.xhtml`). Previously passing the former — a form operators commonly produce by copying paths from `git status` or shell tab completion — failed with `Invalid Argument: File not found in engine: engine/browser/base/content/foo.xhtml`, which named a doubled path that gave no hint the fix was to drop the `engine/` prefix. A leading `engine/` segment is now stripped before the existence check and before the manifest writer runs; the `[dry-run] Would register`, `Already registered`, and `Registered` log lines all render the normalised engine-relative path so the operator sees exactly what the manifest writer received.
446
-
447
- ### Test — stale-build preflight
448
-
449
- - `fireforge test` now runs a stale-build preflight when `--build` was NOT passed. The probe diffs engine HEAD (and the workdir) against the last-build baseline (`.fireforge/last-build.json`), filters to paths that imply packaging, and — when any match — emits a single up-front warning naming the changed files and pointing at `fireforge test --build`. Previously the mismatch only surfaced as a cryptic `NS_ERROR_FILE_NOT_FOUND` against a `chrome://browser/content/…` URI AFTER xpcshell or mach test launched; the motivating case was scaffolding a new top-level chrome document plus a BrowserGlue-style xpcshell test, where the test file existed, the manifests were registered, but `obj-*/dist/` still held the pre-edit bundle and chrome URIs resolved to nothing. The preflight is warn-only (never blocks) because a fork that rebuilt out-of-band — a direct `./mach build` invocation, an IDE plugin, a separate CI stage — can legitimately have a fresh `dist/` with no FireForge-recorded baseline update. Lives in the new `src/core/test-stale-check.ts`.
450
-
451
- ### Lint — `--only-introduced` exit-code scope
452
-
453
- - New `fireforge lint --only-introduced` flag scopes the exit code to issues tagged `[introduced]` by `--since`. Cumulative pre-existing queue errors still print (the operator retains full visibility into queue state), but do not fail lint — so a branch whose own diff is clean passes CI even when the repo carries unrelated `raw-color` / license-header errors from older patches. Requires `--since`; without a revision there is no introduced-vs-cumulative distinction and the combination is rejected up-front with an actionable message rather than silently treating every error as cumulative. The failure message reports the count of cumulative errors suppressed by the flag (`N cumulative error(s) suppressed by --only-introduced`) so a branch that turned clean only by virtue of the flag still tells the operator what was hidden. Pre-flag behaviour — any error fails lint — is unchanged when the flag is absent.
454
-
455
- ### Furnace create — xpcshell chrome-URI documentation
456
-
457
- - `furnace create --xpcshell` scaffolds a generated test that now carries an inline explanation of the xpcshell chrome-URI boundary: toolkit chrome (`chrome://global/*`) IS registered and resolvable from the harness, but browser chrome (`chrome://browser/*`) is NOT registered even when `firefox-appdir = "browser"` is set, and the manifest set xpcshell loads lags what the real browser loads. The comment points at `furnace create --test-style=browser-chrome` as the correct harness for tests that need browser chrome. The `--xpcshell` help text picks up the same note so operators reading `fireforge furnace create --help` see the constraint before they scaffold. Previously the scaffolded test and the help text only described xpcshell as "headless, no tabbrowser", which read as "it just won't render UI" — not "it can't fetch browser chrome URIs". Motivating case: `test_browserGlue_hominis_startup.js` trying to fetch `chrome://browser/content/hominis.xhtml` via `NetUtil.asyncFetch` and hitting `NS_ERROR_FILE_NOT_FOUND` despite the file being present under `obj-*/dist/`.
458
-
459
- ### Furnace chrome-doc — platform-module compatibility + packaging verification
460
-
461
- - Every `furnace chrome-doc create`-scaffolded root element now carries a `data-furnace-chrome-doc="<name>"` sentinel attribute. Fork-side patches to upstream platform modules that observe `browser-delayed-startup-finished` and walk INTO the window assuming `browser.xhtml`'s DOM — `DevToolsStartup`, `PageActions`, `SessionStore`, `DownloadsButton`, and the growing set of modules that treat every `navigator:browser` window as a main browser window — can guard on this attribute cheaply via `document.documentElement.hasAttribute("data-furnace-chrome-doc")` to skip the walk on a custom chrome doc. The attribute name is fork-neutral so a fork upgrading across FireForge versions does not have to rewrite every guard; the name carried in the value distinguishes multiple chrome docs when a patch needs finer-grained routing. Motivating case: Hominis' `hominis.xhtml` launch fired `TypeError: can't access property "addEventListener", menu is null` from `DevToolsStartup.sys.mjs` and `TypeError: can't access property "placeAllActionsInUrlbar", bpa is undefined` from `PageActions.sys.mjs` on every window-open, because both modules assumed structure that only `browser.xhtml` provides. Both fire as non-fatal Browser Console noise today, but the set of walking modules grows each upstream release, and chasing them with per-element stubs (`<xul:keyset id="mainKeyset"/>`, `<menubar>` placeholders, …) is whack-a-mole compared to a single guard attribute. Exported as `FURNACE_CHROME_DOC_SENTINEL` so test code and external checks can reference the exact name without hardcoding the string.
462
- - New `furnace chrome-doc create --with-tests` flag scaffolds an xpcshell packaging-verification test (`test_<name>_packaging.js` + `xpcshell.toml`) under `engine/browser/base/content/test/<binary>-xpcshell/<name>/`. The generated test probes the packaged app directory via `Services.dirsvc.get("XCurProcD", Ci.nsIFile)` and navigates to `<AppDir>/chrome/browser/content/browser/<name>.xhtml` (and the corresponding `skin/classic/browser/<name>-chrome.css`), asserting each file exists and is non-empty. Crucially the test does NOT go through `chrome://` URI resolution — the xpcshell harness's browser-chrome manifest set lags the real browser's even with `firefox-appdir = "browser"` set, so `NetUtil.asyncFetch` on a packaged chrome URI returns `NS_ERROR_FILE_NOT_FOUND` against a file that IS correctly packaged (the Hominis startup-verification failure mode). Direct filesystem probing sidesteps the chrome-registration gap entirely. The generated test also carries an inline comment flagging the omni.ja-packed-build limitation so an operator running a packed-tree configuration sees that the scaffold assumes an unpacked `mach build` layout before it fails. `XPCSHELL_TESTS_MANIFESTS` registration is left to the operator because the owning `moz.build` depends on the fork's layout. Lives in the new `src/commands/furnace/chrome-doc-tests.ts`. Writes go through the same rollback journal as the chrome-doc scaffolder itself, so a SIGINT mid-scaffold restores both the source files and the test scaffold.
463
- - `furnace chrome-doc create`'s "Next steps" note now calls out the platform-module-compatibility sentinel explicitly and points at the README's "Platform module compatibility" section, plus the manual registration step when `--with-tests` is set, so operators see the relevant context before they run `fireforge build`.
464
-
465
- ### Build audit — registration-aware resolution
466
-
467
- - `fireforge build`'s post-build audit now anchors artifact resolution to the `(source)` references in `jar.mn`. When a source file is claimed by a `jar.mn` entry, the audit walks the registration to compute the expected target path (e.g. `content/browser/mybrowser.js`) and probes the dist tree for a candidate whose absolute path ends with that suffix — rather than basename-similarity scoring, which could not distinguish a correctly-registered chrome resource from an unrelated same-basename file elsewhere in the tree. Motivating case: a fork that added `engine/browser/base/content/mybrowser.js` (registered in `browser/base/jar.mn`, packaged to `chrome/browser/content/browser/mybrowser.js`) alongside an unrelated `browser/defaults/preferences/mybrowser.js` pref from an earlier patch. The heuristic locked onto whichever the directory walk hit first, awarded both candidates an equal trailing-overlap score (basename only), and reported the chrome resource as "missing" in the `Packaged: ...` summary — even though packaging had landed it correctly. Registration-aware resolution in the new `src/core/build-audit-registration.ts` picks the correct artifact without consulting the scorer; the similarity heuristic only runs when no `jar.mn` registration is found (moz.build-registered sources such as `FINAL_TARGET_FILES` and `JS_PREFERENCE_FILES`).
468
- - When a source IS registered in `jar.mn` but the registered target is absent from dist, the audit now reports "missing" with a warning that names the `jar.mn` entry (`is registered in engine/browser/base/jar.mn as "content/browser/foo.js (content/foo.js)" but no packaged artifact ending in "/content/browser/foo.js" was found under dist/`). This is distinguishable from an unregistered miss and tells the operator "registration is intact, packaging dropped the file" — not "check the jar.mn again".
469
- - When the heuristic fallback downgrades to `missing` (unrelated same-basename hit, no structural relation to the source), the warning now enumerates EVERY same-basename candidate in dist/ up to five entries, then truncates with a `(+N more)` tail. Previously only the scorer's single pick was surfaced, which often misled operators by naming a file that had nothing to do with the source; the full set lets triage see the real artifact alongside the confounders at a glance.
470
-
471
- ### Build preflight — `--rewrite-mozinfo` for safe relocations
472
-
473
- - New `fireforge build --rewrite-mozinfo` option handles the class of problem where a workspace was simply moved to a new path and `obj-*/mozinfo.json` still records the old `topsrcdir` / `topobjdir`. Without the flag, the stale-objdir preflight aborts with a "delete and rebuild" instruction; the full rebuild takes ~20 minutes and discards ~14 GB of otherwise-intact obj artefacts. With the flag, FireForge patches `mozinfo.json`'s recorded paths in place and runs `mach configure` so the recursive-make backend regenerates against the corrected paths — no obj-\* scrub, no fresh compile.
474
- - The rewriter refuses any change that is not a pure prefix-move: mozinfo must record both `topsrcdir` and `topobjdir`, `topobjdir` must resolve to `<topsrcdir>/<objDir>` (out-of-tree builds are rejected), and the detected obj-\* directory name must match the one mozinfo recorded. On any refusal the command falls back to the original clean-rebuild guidance with the refusal reason appended (`mozinfo rewrite refused: …`), so an unsafe relocation is never silently misrepaired. An external mozconfig (one that lives outside the old topsrcdir — e.g. a shared `$HOME/configs/shared-mozconfig`) is left untouched by the rewriter; a relocated workspace that also moved its mozconfig still falls back to clean-rebuild.
475
- - The `buildArtifactMismatchMessage` copy now points operators at the new flag: "If the workspace was simply moved (same tree, different prefix), "fireforge build --rewrite-mozinfo" will patch mozinfo.json paths in place and run mach configure instead of scrubbing the whole tree." A failed `mach configure` after a successful rewrite surfaces as a `BuildError` so the operator sees "rewrote mozinfo but configure failed" distinctly from "rewrite refused".
476
-
477
- ### Furnace registration
478
-
479
- - `furnace apply` idempotency check is marker-comment-tolerant. Previously the single-line substring match (`content.includes('["tag",')`) missed multi-line entries, and the standalone-line regex anchored on `\s*$`, which did not allow trailing `// <marker>:` comments an operator may have appended to a previously-written entry. A duplicate tag was then inserted on every re-apply, and the second `setElementCreationCallback` invocation threw `NotSupportedError: Operation is not supported` at every window-load. The idempotency check now matches on tag-name column 0 (both single- and multi-line array shapes) and tolerates trailing `//` comments on the line.
480
- - New optional fireforge.json field `markerComment` (e.g. `"MYBROWSER"`) is appended as a ` // MYBROWSER:` suffix to every line FireForge writes into `customElements.js`. Keeps fork modifications discoverable and re-applies idempotent without hand-tagging after each apply. The field is threaded through `applyCustomComponent` and `furnace deploy`, not just `furnace create`.
481
- - `addCustomElementRegistration` and its regex fallback both accept the new marker as an optional parameter; the AST idempotency check and the regex-fallback idempotency check share a single helper (`isTagAlreadyRegistered`).
482
-
483
- ### Furnace `--localized`
484
-
485
- - `furnace create --localized` now emits the Mozilla-idiomatic `MozLitElement` l10n pattern: a module-level `window.MozXULElement?.insertFTLIfNeeded("<chrome-uri>")` call and `this.ownerDocument.l10n?.connectRoot(this.shadowRoot)` / `disconnectRoot` in `connectedCallback` / `disconnectedCallback`. Previously the template called `this.insertFTLIfNeeded(...)` directly on a `MozLitElement` instance, which throws `TypeError: this.insertFTLIfNeeded is not a function` at every connect because that method lives on `MozXULElement`, not `MozLitElement`. The `--localized` path was silently non-functional.
486
- - `furnace apply` now registers the scaffolded `.ftl` in the locale jar.mn (default `toolkit/locales/jar.mn`) so the chrome URI `insertFTLIfNeeded` expects actually resolves at runtime. Previously only the `.ftl` file itself was copied into the FTL tree, with no chrome registration. `furnace remove` and the workspace-delete codepath (`undeployCustomFiles`) drop the jar.mn entry symmetrically.
487
- - The locale jar.mn write degrades gracefully — a missing target (non-standard fork tree) surfaces a structured step error rather than aborting apply, so a well-formed `.mjs`/`.css` is never blocked by a broken locale path.
488
- - FTL chrome URIs are now derived from `furnace.json.ftlBasePath` via a pair of helpers (`resolveFtlChromeSubPath`, `resolveFtlLocaleJarMnPath`) so forks that customise the FTL tree get matching `insertFTLIfNeeded` and jar.mn output.
489
-
490
- ### Furnace validate
491
-
492
- - `missing-token-link` now reads `tokenHostDocuments` from furnace.json and scans every configured chrome document for the tokens CSS link. Warning fires only when NONE of them link the tokens CSS; the warning enumerates the documents it actually checked. Previously the check was hardcoded to `browser/base/content/browser.xhtml`, which false-positived on forks that mount components in a different chrome document (e.g. `mybrowser.xhtml`). Defaults to `["browser/base/content/browser.xhtml"]` when omitted — behaviour is unchanged for projects that never set the field.
493
- - `no-keyboard-handler` no longer warns when `@click` sits on a native interactive element (`<button>`, `<a href>`, `<input>`, `<select>`, `<textarea>`, `<summary>`, `<details>`, or the Firefox `moz-button`/`moz-toggle`/`moz-checkbox`/`moz-radio`/`moz-menulist` widgets). Those elements dispatch `click` on Enter and Space via the platform, so a duplicate `@keydown`/`@keypress` handler would double-fire. The rule still fires for synthetic interactive markup (e.g. `<div @click>`) and for bare `<a>` without an `href` attribute, which are the real keyboard-a11y hazards.
494
- - `token-prefix-violation` stops flagging component-owned runtime CSS custom properties. Previously every `var(--foo)` that did not match `tokenPrefix` was rejected as a design-token escape, even when the property was a per-frame state channel (`--cam-x`, `--tile-z`) both written and read by the component. Two relaxations ship together: (a) a new optional `runtimeVariables: string[]` field in `furnace.json` explicitly allowlists cross-component runtime channels (e.g. set in JS, read in the child's CSS); (b) variables that are both declared (`--foo: value;`) and consumed (`var(--foo)`) inside the same CSS file are auto-exempted — no config entry required. The same relaxations apply to the patch-stack lint. The violation message now calls out `runtimeVariables` as the third escape hatch alongside `tokenPrefix` and `tokenAllowlist`.
495
- - `hardcoded-text` narrowed from a bare `>…<` scan to three context-aware probes: text inside Lit `` html`…` `` template literals, string literals assigned via `.textContent = "…"` / `.innerHTML = "…"`, and XUL-attribute values set via `setAttribute("label"|"title"|"tooltiptext", "…")`. Previously the rule also matched JS comparisons (`if (x > 0 && y < 100)`), long diagnostic strings (`console.error("…")`), and identifier literals passed to `querySelector`, producing noise that trained authors to ignore validator warnings. The file-wide `// furnace-ignore: hardcoded-text` escape hatch is preserved.
496
-
497
- ### Run / test
498
-
499
- - `fireforge run`, `fireforge watch`, `fireforge build`, and every other `mach` invocation launched with inherited stdio now forward parent `SIGINT`/`SIGTERM` to the child as `SIGTERM` and wait ~1.5 s before escalating to `SIGKILL`. A second Ctrl-C during the grace window escalates immediately (matches the usual "hit Ctrl-C twice to force-quit" UX). Previously the parent could exit before Gecko's `AsyncShutdown` / `profileBeforeChange` blockers finished flushing in-memory state, losing the last few seconds of edits. The grace window is configurable via a new `shutdownGraceMs` option on `execInherit` / `execInheritCapture`.
500
- - New `fireforge test --doctor` runs a short marionette handshake preflight before (optionally) invoking `mach test`. Spawns the built browser headless, opens a TCP socket to `127.0.0.1:2828`, waits for the handshake bytes, and reports PASS/FAIL with the tail of stderr on FAIL. When `--doctor` is supplied with no test paths, it exits after the preflight — a sub-minute way to tell "marionette wedged" apart from "test failed to discover" when `mach test` hangs for the full 360 s marionette timeout. When supplied with test paths, a FAIL preflight short-circuits before `mach test` runs.
501
- - `fireforge test --doctor` is now a cascade of six layered probes (engine-present → mach-available → python-available → profile-creatable → browser-spawns → marionette-handshake) with tight per-layer budgets. Previously the single 30 s socket poll gave the same generic "socket did not respond" diagnostic whether mach was missing, Python was unavailable, `/tmp` was not writable, or the browser binary crashed at startup — so operators had no lead on where to start debugging. Each layer now short-circuits with a `[layer N/6: <name>]`-prefixed detail message so the first broken layer is surfaced immediately, and a crashing browser is caught by a short settle window at layer 5 instead of wasting the full budget waiting for bytes that never come.
502
-
503
- ### Furnace build
504
-
505
- - `fireforge build` already auto-applies Furnace components (via `prepareBuildEnvironment`) before the mach build step, but the behaviour was undocumented and silent — operators who edited `components/custom/` and then ran `fireforge build` could not distinguish "auto-apply wrote files" from "nothing changed". A loud `Furnace: source → engine sync wrote N component(s) before build (...)` banner now fires whenever apply wrote files, naming every component that was synced. The `fireforge build --help` description and help footer now call out that apply runs before the build step.
506
-
507
- ### Furnace create
508
-
509
- - New `furnace create --xpcshell` flag scaffolds an xpcshell test harness alongside (or instead of) the browser-chrome mochitest that `--with-tests` already produces. xpcshell runs headless without a `tabbrowser`, so storage-layer / observer-driven / module-loading code on forks that do not mount the upstream browser chrome (no `openLinkIn` → `URILoadingHelper`) can be covered without the harness complaining about a missing tab strip. Writes `test_<name>_module_loads.js` and an `xpcshell.toml` manifest into `engine/browser/base/content/test/<binary-name>-xpcshell/<component-name>/`. Registration in `XPCSHELL_TESTS_MANIFESTS` is left to the operator — the moz.build that should own the entry depends on where the component lives.
510
-
511
- ### Build audit
512
-
513
- - `fireforge build` now runs a warn-only post-build dist-tree audit after a successful mach build. The audit diffs engine-relative paths touched since the last successful build (stored as `.fireforge/last-build.json`) against the dist bundle, and warns per file that is packageable-by-convention (`.js`/`.mjs`/`.css`/`.ftl`/`.xhtml`/`app/profile/…`) but has no matching artifact, or whose dist mtime is older than the source. Surfaces the class of bug where a new pref file or widget is edited but never registered in `moz.build` / `jar.mn` / `package-manifest.in`.
514
- - Build-system inputs (`jar.mn`, `moz.build`, `moz.configure`, `Makefile.in`, `mozbuild.in`) are now excluded from the audit's "must appear in dist/" check. They are consumed by the build to produce chrome registrations / make targets but never themselves ship, so every edit produced a guaranteed false positive — and worse, when an unrelated upstream `moz.build` coincidentally existed elsewhere in the bundle (e.g. `<App>.app/Contents/moz.build`) the audit reported "source is newer than packaged artifact" against two completely unrelated files. The exclusion list lives next to `PACKAGEABLE_EXTENSIONS` in `src/core/build-audit.ts`.
515
- - Same-basename collisions in `dist/` are now disambiguated by trailing-segment overlap: a branding override at `engine/browser/branding/<name>/content/aboutDialog.css` (which ships at `chrome/<area>/content/branding/aboutDialog.css`) no longer gets matched against the unrelated upstream `chrome/<area>/content/browser/aboutDialog.css`. The scorer rewards candidates whose path contains meaningful intermediate segments from the source (e.g. the `branding` segment) so re-rooted artifacts win over coincidentally-named ones. Generic segments like `content` / `chrome` / `bin` do not count toward the bonus to avoid breaking ties on noise.
516
- - Test sources (anything under `/test(s)/`, plus `browser_*.js` / `test_*.js` / `xpcshell.toml` / `browser.ini`) are now resolved against the `_tests/` tree under the active `obj-*` directory instead of `dist/`. Mochitest and xpcshell harnesses copy registered tests under `_tests/testing/...`, never into the packaged bundle, so the previous dist-only walk false-flagged every registered test as "missing packaged artifact". Misses still warn — but they now point at `_tests/`, directing the operator to `BROWSER_CHROME_MANIFESTS` / `XPCSHELL_TESTS_MANIFESTS` instead of `package-manifest.in`.
517
- - Files inside an `if CONFIG[…]:` block in their owning `moz.build` are now skipped on hosts where the gate evaluates off (Windows-only stubinstaller CSS on a macOS build, Darwin-only artwork on Linux, etc.). The detection walks up from the source file to the closest `moz.build`, scans for the basename inside a Python-style indented `if CONFIG[…]:` block, and matches the gate against the host platform via `getPlatform()`. Negation expressions (`!= "WINNT"`, `not CONFIG[…]`) are conservatively NOT treated as single-OS gates, so we never wrongly suppress a warning for a file that should ship on the current host. Lives in the new `src/core/build-audit-platform.ts`.
518
- - Platform-gate detection now also covers subtrees packaged through platform-specific `Makefile.in` recipes that live outside the `moz.build` graph. Paths under `/stubinstaller/` (Windows NSIS stub installer), `browser/installer/windows/`, `browser/installer/macosx/`, and `browser/installer/linux/` count as host-gated by path convention on hosts where the target platform does not match. Previously the audit warned on every touched branding stubinstaller CSS on every non-Windows build because no ancestor `moz.build` wrapped those files in an `if CONFIG[…]:` block — the packaging trigger is `browser/installer/windows/Makefile.in` / `nsis/stub.nsh`, which the scanner does not parse. An explicit moz.build gate still wins if one is present, so fork-specific overrides behave as before. Surfaced as two warnings per macOS build before the fix.
519
- - Test-path audits are now gated on the `_tests/all-tests.json` marker file that `mach package-tests` writes. Plain `mach build` populates a partial `_tests/` subtree and stops, so every correctly-registered mochitest / xpcshell source was false-flagged as "missing packaged artifact" on the common build-only path. The audit now checks for the marker and silently skips test-path sources when the marker is absent — operators who want green-checked test registrations run `cd engine && ./mach package-tests` or a scoped `fireforge test <name>` after the build. Surfaced as 24 warnings per build (one per registered mochitest / xpcshell source) before the fix.
520
- - Stale-artifact warnings now require the matched candidate to be structurally related to the source: either the immediate parent directory trail-matches, or a non-generic source segment (`branding`, the fork's own directory name) appears mid-candidate. Same-basename hits in unrelated subtrees — the motivating case: `engine/browser/modules/<name>/test/head.js` matching the upstream `_tests/xpcshell/dom/quota/test/xpcshell/common/head.js` — are now classified as `missing` with a warning that names the unrelated candidate, rather than a `stale` warning that reads as "your build dropped this file" when in fact the match is spurious. The confidence check only kicks in when staleness would otherwise fire, so `updated` classifications keep their current loose matching. Generic directory segments (`test`, `tests`, `unit`, `common`, `xpcshell`, `mochitest` plus the existing list) no longer contribute to the structural-match bonus so trailing-segment spoofs (a shared `test` / `xpcshell` segment on an unrelated file) are rejected.
521
- - Ends every build with a `Packaged: N updated, M stale, K missing, S skipped` summary so operators can distinguish a fast-because-incremental build from a fast-because-silently-skipped one without a post-build `find`.
522
- - `fireforge build` auto-runs `mach configure` before the mach build step when any `moz.build`, `moz.configure`, or `Makefile.in` changed since the last successful build. Prevents the stale-backend trap where an incremental build skips work against a recursive-make backend that no longer matches the source tree. Emits a `Backend config changed; running mach configure first...` banner so the extra step is visible, and continues the build even if configure exits non-zero.
523
- - Mach build failures with known-cryptic mozbuild errors now print actionable hints. First entry in the table: `mozbuild.preprocessor.Preprocessor.Error: no preprocessor directives found` prints `Use JS_PREFERENCE_FILES instead, or add at least one #filter / #expand directive to the file.` The hint table lives in `src/core/mach-error-hints.ts` and is extensible — new cryptic errors we diagnose get one-line hints added without touching the build wrapper.
524
-
525
- ### Lint
526
-
527
- - `fireforge lint --since <git-rev>` tags each lint issue as `[introduced]` (file touched in the diff since `<git-rev>`) or `[cumulative]` (pre-existing patch-state drift). Output gains the tag prefix and the summary line splits counts (e.g. `Lint: 2 introduced error(s), 0 introduced warning(s); 5 cumulative error(s), 1 cumulative warning(s)`). Exit code semantics are unchanged — an introduced OR cumulative error still fails lint — but triage of "is the diff I just produced clean?" no longer requires mentally subtracting pre-existing noise from every report. Without `--since` the output is unchanged.
528
-
529
- ### Fork chrome-doc assumptions
530
-
531
- - `fireforge doctor`'s `Furnace engine paths` check now reads `furnace.json.tokenHostDocuments` instead of hardcoding `browser/base/content/browser.xhtml`. Forks that replaced the stock chrome document were emitting a permanent "browser.xhtml missing" warning every doctor run; reusing the same field the `missing-token-link` validator already consumes means a fork configures chrome-doc paths once and both checks agree. Defaults to `['browser/base/content/browser.xhtml']` when unset — behaviour is unchanged for forks that ship the stock chrome document.
532
- - `fireforge wire --dom` no longer hardcodes `browser/base/content/browser.xhtml` as the target chrome document. The target now resolves in this order: explicit `--target <path>` flag → first entry of `furnace.json.tokenHostDocuments` → upstream `browser/base/content/browser.xhtml`. Forks that replaced browser.xhtml were getting a cryptic "could not find insertion point in browser.xhtml" error that read like a FireForge bug; the resolved path now propagates into the dry-run plan, the success message, and the insertion-failure error so the actual target is surfaced. When the resolved target does not exist on disk, wire fails up-front with a pointer to `tokenHostDocuments` / `--target` rather than blowing up in the AST pass. The `addDomFragment` core now computes the `#include` directive relative to the target's own directory instead of a hardcoded `browser/base/content/`, so a target that lives elsewhere in the engine tree gets a correctly resolved include path.
533
-
534
- ### Furnace chrome-doc
535
-
536
- - New `furnace chrome-doc create <name>` subcommand scaffolds a top-level chrome document (xhtml + js + css + ftl) plus the three jar.mn registrations (`browser/base/jar.mn`, `browser/themes/shared/jar.inc.mn`, `browser/locales/jar.mn`). Default emits titlebar-buttonbox markup and a `windowtype="navigator:browser"` shell; `--no-titlebar` produces a frameless overlay with the macOS `.titlebar-button { display: none }` carve-out. Mirrors the workflow for custom elements (`furnace create`) so hand-authoring mistakes — the `*` preprocessor flag, the startup-topic observer, the platform titlebar inheritance — are eliminated. All writes go through a rollback journal under the signal-handler pathway: a Ctrl+C mid-scaffold restores every touched file.
537
-
538
- ### Furnace create
539
-
540
- - New `--test-style <mochikit|browser-chrome|xpcshell>` option for `furnace create --with-tests`. `mochikit` (the new default when `--with-tests` is set alone) scaffolds a `chrome://mochikit/...` test under `engine/toolkit/content/tests/widgets/test_<tag>.html` that loads the component module directly and smoke-asserts `customElements.whenDefined(tag)`. Runs today against forks whose top-level chrome document lacks a `tabbrowser` (the class of bug that forced `--xpcshell` for storage-layer code) because the harness doesn't traverse `URILoadingHelper.openLinkIn`. `browser-chrome` is the former default and requires a working tabbrowser; `xpcshell` is equivalent to `--xpcshell`. Backwards-compat: `--xpcshell` alone still works; conflicting flag combinations are rejected up-front.
541
-
542
- ### Run / signals
543
-
544
- - `fireforge run` (and every other command that never registers a furnace mutation) no longer prints the alarming `Received SIGTERM; rolling back in-flight furnace mutations…` line when the CLI receives SIGINT or SIGTERM. The global signal handler now gates the rollback banner on the presence of at least one live mutation in the registry — plain launches exit silently with code 130/143 as they always did. A new regression test asserts `warn` is not called when no operations are registered.
545
-
546
- ### Internal
547
-
548
- - Extracted `furnace-apply-ftl.ts`, `furnace-config-tokens.ts`, and `create-templates.ts` to keep apply / config / scaffolding files under the per-file LOC budget after the new features landed. `parseStringArray` is now exported from `furnace-config.ts` for cross-module reuse.
549
- - New `src/core/marionette-preflight.ts` owns the `--doctor` probe and its teardown semantics.
550
- - Test mocks for `furnace-registration.js` now cover the new `addLocaleFtlJarMnEntry` / `removeLocaleFtlJarMnEntry` exports; `config.js` mocks in apply-batch tests now cover `loadConfig` because the apply path reads `markerComment` from fireforge.json.
551
- - Repo-wide scrub of fork-example mentions (`hominis.xhtml`, `HOMINIS` marker-comment examples, fixture tag names) in favour of a generic `mybrowser` / `MYBROWSER` placeholder. FireForge reads as fork-agnostic in docs and fixtures; the npm identity (`@hominis/fireforge`) is unchanged. Closes a v0.15.0 slip-through (one `@hominis/fireforge` reference remained in `src/core/furnace-operation.ts` as a generic example alongside the npm-identity occurrences; the code example is now fork-neutral).
552
- - Second pass on the same scrub: residual `hominis.xhtml` test fixtures in `wire.test.ts`, `browser-wire.test.ts`, and `doctor.test.ts` are now `mybrowser-shell.xhtml`; the `hominis.js` reference in the build-audit changelog motivating-case description is now generic. Tests retain a distinct override-target (`mybrowser.xhtml`) to preserve the configured-vs-overridden semantic of the wire `--target` precedence test.
553
- - New modules landed under coverage-gate protection: `src/core/mach-error-hints.ts`, `src/core/build-audit.ts`, `src/core/build-audit-resolve.ts`, `src/core/build-audit-platform.ts`, `src/core/build-baseline.ts`, `src/core/patch-lint-diff-tag.ts`, `src/commands/furnace/chrome-doc.ts`, `src/commands/furnace/chrome-doc-templates.ts`, `src/commands/furnace/create-mochikit.ts`. Per-module thresholds added to `scripts/check-coverage-thresholds.mjs`.
554
- - Path resolution and Python-style moz.build gate detection extracted from `build-audit.ts` into the new `build-audit-resolve.ts` (basename collisions, `_tests/` routing, trailing-segment scoring) and `build-audit-platform.ts` (`if CONFIG[…]:` gate parser keyed on host platform). Keeps the orchestrator under the per-file LOC budget after the false-positive fixes landed.
61
+ - Added `re-export --stamp` and per-patch lint ignores.
62
+ - Added `lint --per-patch`, `--since`, and introduced-only checks.
63
+ - Added xpcshell appdir handling and test diagnostics.
64
+ - Added `run --smoke-exit` for unattended chrome smoke checks.
65
+ - Expanded Furnace localisation, chrome-doc, build, and validation support.
555
66
 
556
67
  ## 0.14.0
557
68
 
558
- ### Concurrency and atomicity
559
-
560
- - Patch body and manifest writes in `re-export`, `rebase --continue`, and the post-apply re-export loop in `rebase` are now atomic via `updatePatchAndMetadata`, so a concurrent `resolve` / `re-export` / `patch compact` / `patch reorder` cannot leave body and metadata disagreeing.
561
- - State writes in `import`, `resolve`, and `rebase` (abort, continue, patch loop) use transactional `updateState` so a concurrent command's unrelated keys are no longer clobbered.
562
- - `rebase` apply + session persist is guarded by a new `runInSignalCriticalSection` primitive in `src/core/signal-critical.ts`; SIGINT / SIGTERM between apply and persist (5 s ceiling) no longer leaves an applied patch marked pending, so `--continue` does not re-apply it.
563
-
564
- ### Rebase
565
-
566
- - Per-patch re-export failures after apply are collected instead of silently dropped. The session stays on disk and `sourceEsrVersion` is not stamped until every re-export succeeds, so `--continue` can retry after the root cause is fixed.
567
- - `rebase --continue` retries the post-apply pipeline when the apply loop has already finished; the prior "session may be corrupt" rejection no longer blocks resumption.
568
- - `rebase --abort` splits into four sequenced steps (git reset, furnace state clear, `pendingResolution` clear, session clear) so failures get correctly-labelled errors and the session stays on disk for retry.
569
-
570
- ### Download
571
-
572
- - `download` restores patch-touched files to baseline after the initial commit (or a resumed partial init), so extraction artefacts and line-ending normalisation no longer force `fireforge import --force` on a clean install. Pre-existing uncommitted edits are preserved and warned about.
573
- - `cleanPatchTouchedFiles` runs before stamping `state.downloadedVersion`, preserving the invariant that the stamped version matches a clean engine.
574
- - Resume preserves the original error cause (timeout, permission denied, corrupted git object, disk full) instead of discarding it behind a generic `PartialEngineExistsError`. Unexpected errors during the partial-engine probe are also wrapped rather than re-thrown bare.
575
-
576
- ### Import
577
-
578
- - Classification no longer swallows structural errors as "unmanaged dirty file". Only pure-IO errors (`ENOENT`, `EACCES`, `EPERM`, `EISDIR`, `EBUSY`) fall through; `PatchError`, manifest corruption, and patch-parse failures re-throw with the original diagnostic.
579
- - Patch integrity issues prompt in interactive mode and error in non-interactive mode instead of warn-and-continue; `--force` still bypasses with an explicit warning.
580
-
581
- ### Furnace
582
-
583
- - `furnace apply --watch` picks up newly-created component directories dynamically, remembers edits that arrive during an in-flight apply (a second cycle runs automatically), and classifies errors errno-aware (`EACCES`, `ENOSPC`, `EBUSY`, `ENOENT`, `ETIMEDOUT`) instead of collapsing into a generic "Apply failed".
584
- - `furnace override` rejects collisions with `config.stock` and `config.custom` in both single and batch paths, and wraps snapshot + copy pairs in per-file error context so a mid-copy failure names the failing file.
585
- - `furnace remove` requires a git engine for custom components (not just overrides) and hoists the precondition outside the lock and journal registration. A summary line surfaces when test-file cleanup fails partway.
586
- - `furnace rename` does prefix-only filename replacement; the prior substring replacement mangled names when `oldName` appeared more than once. Content regexes now escape every metacharacter.
587
- - `furnace deploy` asserts `applied[0].name` matches the requested component before persisting state; state-mismatch errors recommend `fireforge doctor --repair-furnace`.
588
- - `furnace validate --fix` reports the actual delta from re-validation instead of inflating the count on no-op fixes.
589
- - `furnace list -v` tolerates missing or unreadable component directories, rendering `unavailable` instead of terminating the listing.
590
- - `furnace diff` surfaces `--reset-base` recovery in the primary error rather than a secondary catch block.
591
- - `furnace init --ftl-base-path` traversal check uses path normalisation instead of substring match, rejecting absolute paths, null bytes, and `..` segments. Interactive detection checks both `stdin.isTTY` and `stdout.isTTY` to match every other interactive command.
592
-
593
- ### Other commands
594
-
595
- - `setup` rejects project names whose sanitised slug is empty (emoji-only, pure punctuation, `---`). `validateConfig` similarly rejects empty `name`, `vendor`, `appId`, `binaryName`.
596
- - `config --force` no longer bypasses structural validation for known keys in `SUPPORTED_CONFIG_PATHS`; the flag is only an escape hatch for unknown keys.
597
- - `watch` probes `watchman --version` with a 5 s timeout before starting, and runs the furnace staleness check previously only in `run`. Both commands share a new `warnIfFurnaceStale` helper in `src/core/furnace-staleness.ts`.
598
- - `test` path normalisation is case-insensitive, accepts `\` as well as `/`, and trims whitespace, so `Engine/foo/bar` on macOS / Windows no longer reaches `mach` with the prefix intact.
599
- - `status` caps untracked-directory expansion at 5 000 files per directory (configurable via `FIREFORGE_MAX_UNTRACKED_FILES`) and renders a top-of-output banner when directories were truncated, so large outputs don't hide the warning in scrollback.
600
- - `export` empty-diff error distinguishes the `--skip-lint` case; `export-shared` always announces when `--skip-lint` is active.
601
- - `wire <name>` and `register --after <entry>` validate their inputs against strict regexes, rejecting path separators, parent-dir segments, control characters, and line terminators before any filesystem operation.
602
- - `token add --mode` uses Commander's `.choices()` so invalid modes fail with the built-in message and `--help` lists the valid options.
603
- - `run` whitelisted exit codes (0, 130 SIGINT, 143 SIGTERM) are documented inline; SIGKILL (137) and other abnormal signal codes surface as build errors.
604
- - `discard` wraps error causes via `toError` so thrown strings or numbers propagate as real Errors with stack traces.
605
-
606
- ### Internal
607
-
608
- - New unit tests for `validateCheckDependencies` in `src/commands/doctor.ts` assert the forward-only dependency invariant so a regression cannot slip in when reordering checks.
69
+ - Made patch and state writes transactional.
70
+ - Hardened rebase, import, and download recovery.
71
+ - Improved Furnace apply, rename, deploy, diff, and validation.
72
+ - Added broader input validation across setup, config, wire, register, and test.
73
+ - Improved status output and watch/run preflights.
609
74
 
610
75
  ## 0.13.0
611
76
 
612
- ### Setup
613
-
614
- - **`fireforge bootstrap` now runs targeted post-bootstrap checks** instead of pattern-matching output text. When `mach bootstrap` exits successfully but sub-downloads fail (e.g. HTTP 403 from Apple's CDN), FireForge validates actual system state — checking whether a macOS SDK is available via Xcode — and reports actionable results using the same `✓`/`!`/`✗` severity rendering as `fireforge doctor`. Non-critical issues (SDK download failed but Xcode provides one) are reported as warnings rather than alarming "did not complete successfully" errors.
615
-
616
- ### Lint fixes
617
-
618
- - **`file-too-large` now uses tiered severity thresholds.** The old single 650-line warning is replaced with a three-tier system (notice / warning / error) that distinguishes general files from test files. General files: 500–749 lines notice, 750–899 warning, 900+ error. Test files (paths containing `/test/`, or filenames matching `browser_*.js`, `test_*.js`, `xpcshell_*.js`): 1200–1399 notice, 1400–1599 warning, 1600+ error. Messages include the applicable thresholds so users know where they stand. The new `notice` severity is displayed but does not count toward warning or error totals and does not block export.
619
- - **`observer-topic-naming` no longer matches across newlines.** The regex that extracts topic strings from `notifyObservers`/`addObserver`/`removeObserver` calls now anchors to a single line, preventing false positives when the call spans multiple lines and an unrelated string literal appears later.
620
- - **`raw-color-value` now supports a file allowlist and inline suppression.** New `patchLint.rawColorAllowlist` config array in `fireforge.json` exempts file paths (exact or basename match) from the raw-color check — intended for design token files that must contain raw color values. Individual declarations can also be suppressed with an inline `/* fireforge-ignore: raw-color-value */` comment.
621
- - **`large-patch-lines` now uses tiered severity thresholds.** The old single >300-line warning is replaced with a three-tier system matching the `file-too-large` pattern. General patches: 800+ lines notice, 1500+ warning, 3000+ error. Test-only patches (all files match test patterns): 1500+ notice, 3000+ warning, 6000+ error. The previous threshold was too restrictive relative to file LOC limits — creating a single new file at the `file-too-large` notice tier (500 LOC) already exceeded it. Messages now include the applicable soft and hard limits.
622
- - **`large-patch-lines` now ignores binary content.** Patches whose diff contains GIT binary patch hunks (PNG, ICO, ICNS, BMP, etc.) no longer count base85-encoded data toward the line limit. This removes the need for `--skip-lint` on branding asset patches that are predominantly binary.
623
- - **`modified-file-missing-header` no longer false-positives on upstream files.** Modified upstream files (e.g. `BrowserGlue.sys.mjs`) that carry an MPL-2.0 header in `/* */` block-comment style were incorrectly flagged because the check only tried the comment style inferred from the file extension. The check now cascades through all comment styles and falls back to scanning leading lines for raw license identifier strings (MPL, Apache, MIT, GPL, SPDX).
624
-
625
- ### New commands
626
-
627
- - **`fireforge patch compact`** — closes ordinal gaps in the patch queue in a single atomic operation. After deletes or splits, patch ordinals may have gaps (e.g. 1, 3, 7); `compact` renumbers them sequentially (1, 2, 3). Previously this required N sequential `patch reorder` calls. Supports `--dry-run` and `--yes`.
628
-
629
- ### Register improvements
630
-
631
- - **`register` now supports `.xhtml` and `.css` files in `browser/base/content/`.** Previously only `.js` and `.mjs` files were accepted; XHTML and CSS files required manual `jar.mn` edits.
632
- - **`register` now gives actionable advice for unregistrable file types.** Attempting to register a `.ftl` locale file explains that FTL files are auto-discovered via `jar.mn` glob patterns. Attempting to register an individual test file explains that it should be added to the corresponding `browser.toml` and suggests the correct `register` invocation for the test directory manifest.
633
-
634
- ### General Improvements
635
-
636
- - **Minor Refactor**
77
+ - Improved bootstrap checks after `mach bootstrap`.
78
+ - Added tiered lint severity for large files and patches.
79
+ - Added raw-colour allowlists and inline suppression.
80
+ - Added `fireforge patch compact`.
81
+ - Improved register support for XHTML, CSS, and clearer advice.
637
82
 
638
83
  ## 0.12.0
639
84
 
640
- ### JSDoc validation (breaking)
641
-
642
- - **JSDoc enforcement is now AST-based and severity `error`.** The previous heuristic (walk backwards from `export` to find `*/`) has been replaced with Acorn-based AST analysis. Exported functions must have a JSDoc block with `@param` for each parameter (names must match) and `@returns` when returning a value. Exported classes require a JSDoc block. Exported constants require `@type`. This is a breaking change: projects that previously passed with incomplete JSDoc will now see lint errors.
643
- - **Patch-owned scope.** JSDoc enforcement now applies to all patch-owned `.sys.mjs` files, not just files new in the current diff. A file is patch-owned if it was created by the current diff or by any existing patch in the queue.
644
- - New check: **`jsdoc-param-mismatch`** (error) flags `@param` tags that are missing or have the wrong name.
645
- - New check: **`jsdoc-missing-returns`** (error) — flags functions that return a value but lack `@returns`.
646
- - Exported constants and classes require a JSDoc block but do not require specific tags like `@type`.
647
-
648
- ### Optional checkJs pass
649
-
650
- - **`patchLint.checkJs`** — new opt-in config field in `fireforge.json`. When enabled, runs TypeScript's `checkJs` pass (`allowJs + checkJs + noEmit`) on patch-owned `.sys.mjs` files only. Firefox globals are shimmed automatically. Diagnostics are filtered to patch-owned files so upstream noise is suppressed.
651
- - New check: **`checkjs-type-error`** (error/warning) — surfaces type errors from the TypeScript compiler.
652
-
653
- ### Hardening
654
-
655
- - **Path validation.** `binaryName` in `fireforge.json` now rejects null bytes and absolute paths (including Windows drive letters). `isContainedRelativePath` and `isPathInsideRoot` reject null bytes. Furnace custom component `targetPath` rejects null bytes and absolute paths.
656
- - **Symlink traversal protection.** Patch target validation now checks whether existing paths are symlinks resolving outside the engine tree before applying.
657
- - **PID-aware stale lock recovery.** The file lock writes the owning PID into the lock directory. Stale lock recovery checks whether the PID is still alive before removing, preventing premature removal when a slow operation legitimately holds the lock.
658
- - **Forward-import detection** now catches `ChromeUtils.importESModule()` calls in addition to static/dynamic ES imports and `defineESModuleGetters`.
659
- - **Furnace rollback failure markers** now include the component name and operation context, improving diagnostics in `fireforge doctor`.
660
- - New lint check in README: **`modified-file-missing-header`** (warning) was implemented but not documented; now listed in the lint checks table.
85
+ - Made JSDoc linting AST-based and stricter.
86
+ - Added optional patch-owned `checkJs`.
87
+ - Hardened path validation and symlink handling.
88
+ - Improved stale-lock recovery.
89
+ - Expanded forward-import detection and Furnace repair diagnostics.
661
90
 
662
91
  ## 0.11.0
663
92
 
664
- ### New commands
665
-
666
- - **`fireforge verify`** read-only integrity check for the patch queue. Reports duplicate file creations across patches, forward imports, orphaned patch files, and manifest inconsistencies. Exits non-zero on any error, making it usable as a CI pre-flight gate.
667
- - **`fireforge patch delete <name>`** — removes a patch file and its manifest entry atomically. Refuses when a later patch imports from a file the deleted patch owns (bypassable with `--force-unsafe`).
668
- - **`fireforge patch reorder <name> --to <N> | --before <anchor> | --after <anchor>`** — moves a patch to a new position, renumbers surrounding patches, and runs cross-patch lint against the projected order before writing.
669
-
670
- ### New flags and options
671
-
672
- - `fireforge export --dry-run` previews the full export plan (filename, metadata, affected files) without writing. With `--supersede`, shows which existing patches would be absorbed and why.
673
- - `fireforge export --order <N> | --before <anchor> | --after <anchor>` places a new patch at a specific position and shifts subsequent patches up.
674
- - `fireforge re-export --files <paths> <patch>` restricts a re-export to an explicit file subset, useful for splitting or shrinking a patch's scope.
675
- - `fireforge import --until <patch>` (alias `--stop-at`) applies patches only up to the named patch, useful for bisection.
676
- - `fireforge status --ownership` prints a flat table mapping every managed path to its owning patch and flags ownership conflicts.
677
- - `fireforge furnace apply --force` and `furnace deploy --force` proceed despite `baseVersion` drift between `furnace.json` and the Firefox version.
678
- - `fireforge furnace deploy --skip-validate` skips the validation suite during deploy.
679
- - `fireforge furnace override` now accepts multiple tag names in a single invocation for batch creation.
680
- - `fireforge import --dry-run` previews which patches would be applied, in order, without modifying the engine.
681
- - `fireforge status --json` outputs classified file status as machine-readable JSON for CI scripting.
682
-
683
- ### Furnace improvements
684
-
685
- - **`furnace refresh <name>`** merges upstream Firefox changes into an override workspace via three-way merge. Clean merges update `baseVersion` automatically; conflicts leave standard markers for manual resolution. Supports `--dry-run` and `--reset-base` (skip merge, just update the baseline).
686
- - **Full overrides now include shared Fluent files.** Localized widgets (those with a `.ftl` file) are now copied, applied, removed, and diffed end-to-end instead of silently dropping the locale payload.
687
- - **`furnace diff` rewritten with proper multi-hunk output.** Scattered edits across a file now render as separate hunks with context lines instead of one giant block.
688
- - **`furnace apply` detects and undeploys deleted workspace files.** If you remove a file from a component's workspace directory and re-run apply, the corresponding engine copy is cleaned up and registrations are adjusted.
689
- - **`furnace status` now distinguishes workspace edits from engine drift.** These have different remediation paths and are reported separately instead of collapsed into one message.
690
- - **`furnace scan` offers to override just-added stock components** in the same interactive session.
691
- - **Preview stages workspace files into the engine** before launching Storybook so fresh edits actually appear, then rolls them back on teardown.
692
- - **FTL base path is now configurable** via `ftlBasePath` in `furnace.json` for projects with non-standard locale paths.
693
- - **`scanPaths` in `furnace.json`** lets `furnace scan` discover components outside the default `toolkit/content/widgets`.
694
- - File copies during apply are now parallelized within each component.
695
-
696
- ### New lint rules
697
-
698
- - **`duplicate-new-file-creation`** (error) — flags any path that appears as a new-file creation in more than one patch.
699
- - **`forward-import`** (error) — flags imports that reference a file owned by a later-ordered patch. Supports an inline suppression marker (`// fireforge-ignore: forward-import`) for false positives from basename collisions.
700
-
701
- ### Doctor and diagnostics
702
-
703
- - `fireforge doctor` now runs the full Furnace component validation suite (structure, accessibility, compatibility, registration) and reports issues without needing a separate `furnace validate` run.
704
- - New `--repair-furnace` flag reconciles the engine when a furnace operation was interrupted or left inconsistent state.
705
- - `fireforge doctor` checks that Firefox-internal paths Furnace depends on still exist and reports targeted warnings when they are missing.
706
- - `furnace validate` now enforces `.ftl` presence for `localized: true` custom components and no longer false-warns about missing CSS jar entries when the component has no CSS file.
707
-
708
- ### Reliability
709
-
710
- - All furnace mutations now serialize on a project-wide lock, preventing concurrent operations from racing on engine state.
711
- - Ctrl+C and SIGTERM trigger clean rollback across all furnace commands. A `pendingRepair` marker is only written when rollback was actually incomplete, so normal interrupts do not leave false-positive repair flags.
712
- - Override `baseVersion` drift now blocks `apply` and `deploy` by default instead of warning and continuing. Pass `--force` to override, or use `furnace refresh` to update the baseline.
713
- - Post-apply consistency check verifies that `customElements.js` and `jar.mn` entries match what was deployed.
714
- - Engine-side content hashes are cached in the furnace state file, making drift detection faster for the common no-change case.
715
- - Branding file writes are now content-aware: re-running setup with the same configuration no longer bumps file timestamps, avoiding unnecessary `config.status` reconfiguration during incremental builds.
716
- - `build` and `test --build` now share the same preparation pipeline including Furnace apply, so incremental test builds no longer run against stale component state.
717
- - `furnace remove` on an override now restores every overridden engine file to its Firefox baseline instead of leaving deployed files behind.
718
- - Scanner results are cached by content hash within a process, avoiding redundant parsing during scan-status-apply sequences.
719
- - `download` and `build` now check available disk space before starting and warn when free space is low (Firefox source ~5 GB, full build ~20 GB).
720
- - `getProjectRoot()` now throws instead of silent fallback.
721
- - `getPackageRoot()` caches its result after the first call, avoiding repeated filesystem walks.
722
- - Process spawn timeout is now enforced via `AbortSignal.timeout()` instead of the unreliable `timeout` option on `child_process.spawn()`.
723
-
724
- ### Bug fixes
725
-
726
- - `fireforge setup` now writes an initial `patches/patches.json` (with `version: 1`) when creating a new project. Previously, setup created the `patches/` directory but not the manifest, causing `fireforge doctor` to fail the "Patch manifest consistency" check on a fresh project. Re-running `setup --force` on an existing project preserves the current manifest.
727
- - The full Firefox integration test script (`scripts/run-full-firefox-integration.mjs`) now uses `--yes` instead of `--force` when invoking `fireforge discard`, matching the actual CLI flag. This was the sole cause of integration test failures in the discard and recovery workflow steps.
728
- - The integration test's cleanup loop now uses direct git operations (`git checkout` for tracked files, `git clean` for untracked) instead of routing through `fireforge discard`, which could not handle untracked branding files introduced by the build.
729
- - `furnace refresh` now correctly advances the per-override `baseCommit` to the engine HEAD after a successful merge, preventing phantom conflicts on subsequent refreshes.
730
- - `furnace rename` uses the correct file-removal function for FTL files.
731
- - `furnace remove` now parses browser.toml sections properly, cleaning up metadata keys below the section header instead of leaving stale fragments.
732
- - Registration duplicate detection now uses exact path matching so `moz-card` no longer collides with `moz-card-group`.
733
- - The `customElements.js` parser now accepts `const` and `var` loop declarations alongside `let`.
734
- - `re-export --files` refuses to write when a requested path would produce no hunks, preventing manifest/patch-body desynchronisation.
735
- - `patch delete` now respects the `fireforge-ignore: forward-import` suppression marker, matching the behavior of `verify` and `lint`.
736
- - Furnace apply no longer reports "up to date" after `reset --yes` or `download --force` wiped the engine. Both commands now clear the furnace state, and the skip logic checks engine-side drift before trusting cached checksums.
737
- - `status` now classifies Furnace-managed engine paths as `furnace` instead of `unmanaged`, and `export-all` refuses to capture them.
738
- - AST parser fallback in the scanner now emits a warning instead of failing silently.
739
- - `stock` entries in `furnace.json` are validated against a safe character set, rejecting path-traversal strings.
740
-
741
- ### Internal
742
-
743
- - The full Firefox integration script now accepts `FIREFORGE_FULL_FIREFOX_VERSION` to override the Firefox version used during the test run, decoupling the test from the version baked into `fireforge.json`.
744
- - The integration test now verifies that `obj-*/dist/bin/` exists after a build reports success, detecting cases where mach masks a build failure with exit code 0.
745
- - The integration test cleanup loop now uses direct git operations (`checkout` / `clean`) instead of routing through `fireforge discard`, correctly handling untracked branding files introduced by the build.
746
- - New unit tests from a full local Firefox integration run: Python version resolution skips candidates above mach's `MAX_PYTHON_VERSION_TO_CONSIDER` and falls through to a compatible version; fresh-project manifest consistency returns zero issues for the empty manifest that `setup` now writes; bootstrap soft-failure detection catches the `urllib.error.HTTPError: HTTP Error 403` pattern observed in real `mach bootstrap` output.
747
- - CLI command registration is now driven by a declarative manifest instead of hand-listed calls.
748
- - Doctor checks are a declarative registry with per-check `run`, `skipIf`, and `fix` fields.
749
- - New shared destructive-op framework handles confirmation, `--dry-run`, `--yes`/`--force-unsafe`, and audit logging for the patch mutation commands.
750
- - Export internals factored into `planExport` / `executeExportPlan` so dry-run and real writes share one code path.
751
- - Ownership table builder extracted from `status.ts` into `src/core/ownership-table.ts`.
752
- - Cross-patch lint regression calculator extracted from `re-export.ts` into `src/core/lint-projection.ts`.
753
- - The `re-export --files` path extracted into `src/commands/re-export-files.ts` to keep `re-export.ts` under the line limit.
754
- - `max-lines` and `max-lines-per-function` ESLint rules promoted from `warn` to `error`.
755
- - Doctor check ordering dependencies documented in the registry comment.
756
- - Default Firefox version bumped to ESR 140.9.0.
757
-
758
- ### Packaging
759
-
760
- - Package metadata and lockfile updated to 0.11.0.
93
+ - Added `verify`, `patch delete`, and `patch reorder`.
94
+ - Added export, import, status, and Furnace workflow flags.
95
+ - Expanded Furnace refresh, apply, remove, scan, diff, and status.
96
+ - Added cross-patch lint rules.
97
+ - Improved doctor, rollback, build preparation, and packaging reliability.
761
98
 
762
99
  ## 0.10.0
763
100
 
764
- ### Patch workflow validation
765
-
766
- - Re-export now runs the same patch lint gate as export and export-all before writing patch files or manifest metadata.
767
- - `re-export --skip-lint` now downgrades lint errors to warnings consistently, while default re-export blocks on lint errors and keeps artifacts unchanged.
768
- - Raw CSS colors introduced by a patch are now patch lint errors, matching Furnace validation, without blocking on unrelated pre-existing upstream raw colors.
769
- - Furnace accessibility validation now warns about missing ARIA roles only for generic interactive markup, so native semantic elements are not pushed toward redundant ARIA.
770
-
771
- ### General improvements
772
-
773
- - getPackageRoot up to this point expected hardcoded `@hominis/fireforge`, was changed to just the package name for potential forks and more flexibility when changing project name.
774
- - Some test generators were derived from an early downstream fork; the fork-specific names have been replaced with generic naming so the templates apply to any Firefox fork.
775
-
776
- ### Build and Git reliability
777
-
778
- - Build preflight now fails clearly when multiple build artifact directories make the target ambiguous.
779
- - Git diff and status helpers now surface command failures instead of silently treating failed commands as empty output.
780
- - Stale lock cleanup now distinguishes disappearance races from real cleanup failures.
781
-
782
- ### Packaging
783
-
784
- - Package metadata and smoke tests now use version 0.10.0.
785
- - npm install instructions use the scoped `@hominis/fireforge` package name.
786
- - Packaging and full Firefox integration helpers now handle platform-specific npm and mozconfig names more consistently.
101
+ - Tightened patch export and re-export validation.
102
+ - Added raw-colour linting for patch diffs.
103
+ - Improved Furnace accessibility checks.
104
+ - Improved build-artifact and git failure handling.
105
+ - Updated package metadata and install guidance.
787
106
 
788
107
  ## 0.9.0
789
108
 
790
- ### npm release
791
-
792
- - Package is now installable via `npm install @hominis/fireforge` or `npm install -g @hominis/fireforge`.
109
+ - Published the npm package.