@hominis/fireforge 0.44.1 → 0.44.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +18 -110
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/commands/build.js +17 -0
  5. package/dist/src/commands/furnace/apply.js +2 -2
  6. package/dist/src/commands/furnace/deploy.js +2 -2
  7. package/dist/src/commands/furnace/index.js +12 -6
  8. package/dist/src/commands/furnace/init.js +5 -2
  9. package/dist/src/commands/furnace/sync.js +2 -2
  10. package/dist/src/commands/re-export-files.js +63 -23
  11. package/dist/src/commands/re-export-scan.js +18 -2
  12. package/dist/src/commands/re-export-single.js +19 -7
  13. package/dist/src/commands/test-register.js +5 -1
  14. package/dist/src/commands/test-run.d.ts +5 -0
  15. package/dist/src/commands/test-run.js +11 -4
  16. package/dist/src/commands/test-verdict.js +10 -1
  17. package/dist/src/commands/test.js +9 -0
  18. package/dist/src/commands/token.js +36 -12
  19. package/dist/src/core/build-audit.js +6 -2
  20. package/dist/src/core/furnace-jsconfig.js +5 -3
  21. package/dist/src/core/furnace-operation.d.ts +25 -2
  22. package/dist/src/core/furnace-operation.js +27 -2
  23. package/dist/src/core/mach-known-noise-filter.d.ts +17 -0
  24. package/dist/src/core/mach-known-noise-filter.js +19 -0
  25. package/dist/src/core/mach.d.ts +8 -3
  26. package/dist/src/core/mach.js +47 -3
  27. package/dist/src/core/run-log.d.ts +60 -0
  28. package/dist/src/core/run-log.js +183 -0
  29. package/dist/src/core/test-harness-output.d.ts +24 -1
  30. package/dist/src/core/test-harness-output.js +46 -15
  31. package/dist/src/core/test-xpcshell-retry.d.ts +2 -2
  32. package/dist/src/core/test-xpcshell-retry.js +6 -1
  33. package/dist/src/core/token-manager.d.ts +7 -2
  34. package/dist/src/core/token-manager.js +74 -20
  35. package/dist/src/core/token-variant.d.ts +46 -2
  36. package/dist/src/core/token-variant.js +190 -33
  37. package/dist/src/types/commands/options.d.ts +30 -1
  38. package/dist/src/utils/child-env.d.ts +30 -0
  39. package/dist/src/utils/child-env.js +30 -0
  40. package/dist/src/utils/options.d.ts +28 -0
  41. package/dist/src/utils/options.js +32 -1
  42. package/dist/src/utils/process.d.ts +22 -0
  43. package/dist/src/utils/process.js +8 -7
  44. package/docs/README.md +35 -0
  45. package/docs/configuration.md +116 -0
  46. package/docs/exit-codes.md +63 -0
  47. package/docs/furnace.md +57 -0
  48. package/docs/lifecycle-invariants.md +309 -0
  49. package/docs/machine-output.md +94 -0
  50. package/docs/patch-workflow.md +119 -0
  51. package/docs/run-logs.md +60 -0
  52. package/docs/testing.md +92 -0
  53. package/docs/verification-trees.md +48 -0
  54. package/package.json +2 -1
@@ -0,0 +1,30 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Environment construction for spawned children.
4
+ *
5
+ * Its own module rather than a helper inside `process.ts` only because that
6
+ * file sits on a 500-line budget; the logic belongs to the exec layer.
7
+ */
8
+ /**
9
+ * The child's environment: `process.env`, with `env` merged over it, then
10
+ * every `envUnset` key removed.
11
+ *
12
+ * One helper rather than five inline spreads — the removal step is easy to
13
+ * add at four of the five spawn sites and forget at the fifth, which would
14
+ * make the option silently inert exactly where it was not tested.
15
+ *
16
+ * Removal is a real removal, not an assignment to `''`: mozbuild's
17
+ * `is_running_under_coding_agent()` reads `CLAUDECODE` for its PRESENCE, so
18
+ * an empty value would not unset it. `Reflect.deleteProperty` rather than
19
+ * `delete obj[key]` because the key is computed.
20
+ *
21
+ * @param options - Options carrying `env` / `envUnset`
22
+ * @returns The environment to hand `spawn`
23
+ */
24
+ export function buildChildEnv(options) {
25
+ const merged = { ...process.env, ...options.env };
26
+ for (const key of options.envUnset ?? []) {
27
+ Reflect.deleteProperty(merged, key);
28
+ }
29
+ return merged;
30
+ }
@@ -66,6 +66,28 @@ export declare const WAIT_LOCK_ENV_VAR = "FIREFORGE_WAIT_LOCK";
66
66
  * ABSENCE, which is where it belongs.
67
67
  */
68
68
  export declare function resolveWaitLockSeconds(value: string | number | boolean | undefined): number | undefined;
69
+ /**
70
+ * The session-wide wait budget in MILLISECONDS, or undefined when none is
71
+ * set. Sourced from {@link WAIT_LOCK_ENV_VAR} only.
72
+ *
73
+ * Exists for the SECONDARY locks a command takes after the one its
74
+ * `--wait-lock` flag was threaded to. A furnace mutation takes the engine
75
+ * session lock and then `.fireforge/furnace.lock`; only the first had a
76
+ * budget, so a contended deploy died on the file lock's fixed 30 s while
77
+ * its operator believed a much larger budget was in force. Threading the
78
+ * flag reaches the three furnace commands that declare it — this reaches
79
+ * the twelve that do not (`create`, `remove`, `rename`, `refresh`, `scan`,
80
+ * `override`, `chrome-doc`, …), which have no flag to thread but are just
81
+ * as contended.
82
+ *
83
+ * Deliberately env-only: a flag belongs to one invocation and is passed
84
+ * explicitly where it applies, while the environment is exactly the place a
85
+ * session-wide default is expressed. An explicit budget at a call site
86
+ * always wins over this.
87
+ *
88
+ * @returns The session budget in milliseconds, or undefined
89
+ */
90
+ export declare function sessionWaitLockMs(): number | undefined;
69
91
  /**
70
92
  * True when `command` already declares `--wait-lock` itself. Used by the
71
93
  * CLI wiring to add the accept-and-ignore variant only where the honoring
@@ -85,5 +107,11 @@ export declare function ensureWaitLockOptionEverywhere(command: Command): void;
85
107
  * for the bare flag or a validated integer; feed it through
86
108
  * {@link resolveWaitLockSeconds} at the call site to obtain the
87
109
  * `waitLockSeconds` for `withEngineSessionLock`/`withPatchDirectoryLock`.
110
+ *
111
+ * A command can take MORE than one lock — a furnace mutation takes the
112
+ * engine session lock and then `.fireforge/furnace.lock` — and the budget
113
+ * has to reach all of them or the flag is a half-truth. The secondary locks
114
+ * read the session budget through {@link sessionWaitLockMs} rather than
115
+ * having it threaded to every call site.
88
116
  */
89
117
  export declare function addWaitLockOption(command: Command): Command;
@@ -144,6 +144,31 @@ export function resolveWaitLockSeconds(value) {
144
144
  }
145
145
  return parsed;
146
146
  }
147
+ /**
148
+ * The session-wide wait budget in MILLISECONDS, or undefined when none is
149
+ * set. Sourced from {@link WAIT_LOCK_ENV_VAR} only.
150
+ *
151
+ * Exists for the SECONDARY locks a command takes after the one its
152
+ * `--wait-lock` flag was threaded to. A furnace mutation takes the engine
153
+ * session lock and then `.fireforge/furnace.lock`; only the first had a
154
+ * budget, so a contended deploy died on the file lock's fixed 30 s while
155
+ * its operator believed a much larger budget was in force. Threading the
156
+ * flag reaches the three furnace commands that declare it — this reaches
157
+ * the twelve that do not (`create`, `remove`, `rename`, `refresh`, `scan`,
158
+ * `override`, `chrome-doc`, …), which have no flag to thread but are just
159
+ * as contended.
160
+ *
161
+ * Deliberately env-only: a flag belongs to one invocation and is passed
162
+ * explicitly where it applies, while the environment is exactly the place a
163
+ * session-wide default is expressed. An explicit budget at a call site
164
+ * always wins over this.
165
+ *
166
+ * @returns The session budget in milliseconds, or undefined
167
+ */
168
+ export function sessionWaitLockMs() {
169
+ const seconds = waitLockSecondsFromEnv();
170
+ return seconds === undefined ? undefined : seconds * 1000;
171
+ }
147
172
  /**
148
173
  * Registers `--wait-lock [seconds]` on a command that takes NO lock, so a
149
174
  * scripted sequence can blanket-append the flag without hitting a usage
@@ -188,7 +213,13 @@ export function ensureWaitLockOptionEverywhere(command) {
188
213
  * for the bare flag or a validated integer; feed it through
189
214
  * {@link resolveWaitLockSeconds} at the call site to obtain the
190
215
  * `waitLockSeconds` for `withEngineSessionLock`/`withPatchDirectoryLock`.
216
+ *
217
+ * A command can take MORE than one lock — a furnace mutation takes the
218
+ * engine session lock and then `.fireforge/furnace.lock` — and the budget
219
+ * has to reach all of them or the flag is a half-truth. The secondary locks
220
+ * read the session budget through {@link sessionWaitLockMs} rather than
221
+ * having it threaded to every call site.
191
222
  */
192
223
  export function addWaitLockOption(command) {
193
- return command.option('--wait-lock [seconds]', `Wait up to this many seconds (default 60) for another FireForge command to release the contended lock, instead of failing on the default budget. Set ${WAIT_LOCK_ENV_VAR} to apply a budget to every command in a session`, commanderArgParser((raw) => resolveWaitLockSeconds(raw)));
224
+ return command.option('--wait-lock [seconds]', `Wait up to this many seconds (default 60) for another FireForge command to release EVERY lock this command takes, instead of failing on the default budget. Set ${WAIT_LOCK_ENV_VAR} to apply a budget to every command in a session`, commanderArgParser((raw) => resolveWaitLockSeconds(raw)));
194
225
  }
@@ -28,6 +28,28 @@ export interface ExecOptions {
28
28
  cwd?: string;
29
29
  /** Environment variables */
30
30
  env?: Record<string, string>;
31
+ /**
32
+ * Environment variables to REMOVE from the child's inherited environment,
33
+ * applied after {@link ExecOptions.env} is merged over `process.env`.
34
+ *
35
+ * Needed because `env` can only add or overwrite, and some variables are
36
+ * read for their PRESENCE — mozbuild's `is_running_under_coding_agent()`
37
+ * keys on `CLAUDECODE`, so setting it empty would not unset it. An
38
+ * explicit delete says what it means; relying on Node dropping
39
+ * `undefined`-valued env entries would be a semantics gamble at a layer
40
+ * that must not have any.
41
+ */
42
+ envUnset?: readonly string[];
43
+ /**
44
+ * Streams to mirror the child's output into, replacing the default
45
+ * `process.stdout` / `process.stderr` on the stdio-inheriting capture
46
+ * path. Lets a caller tee the live output somewhere else (a run log)
47
+ * without a second per-chunk hook.
48
+ */
49
+ mirror?: {
50
+ stdout?: NodeJS.WritableStream;
51
+ stderr?: NodeJS.WritableStream;
52
+ };
31
53
  /** Timeout in milliseconds */
32
54
  timeout?: number;
33
55
  /**
@@ -3,6 +3,7 @@ import { spawn } from 'node:child_process';
3
3
  import { constants as osConstants } from 'node:os';
4
4
  import { StringDecoder } from 'node:string_decoder';
5
5
  import { ExecTimeoutError } from '../errors/base.js';
6
+ import { buildChildEnv } from './child-env.js';
6
7
  import { killProcessTree, sweepProcessGroup } from './process-group.js';
7
8
  // 50 MB cap per stream to prevent OOM — large toolchain builds (e.g. Firefox, Chromium)
8
9
  // can easily blow past this, so we truncate rather than let the buffer grow unbounded.
@@ -85,7 +86,7 @@ export async function exec(command, args, options = {}) {
85
86
  return new Promise((resolve, reject) => {
86
87
  const child = spawn(command, args, {
87
88
  cwd: options.cwd,
88
- env: { ...process.env, ...options.env },
89
+ env: buildChildEnv(options),
89
90
  stdio: ['ignore', 'pipe', 'pipe'],
90
91
  signal: buildSignalFromTimeout(options.timeout),
91
92
  });
@@ -119,7 +120,7 @@ export async function execStream(command, args, options = {}) {
119
120
  const usesProcessGroup = options.processGroup === true && process.platform !== 'win32';
120
121
  const child = spawn(command, args, {
121
122
  cwd: options.cwd,
122
- env: { ...process.env, ...options.env },
123
+ env: buildChildEnv(options),
123
124
  stdio: ['ignore', 'pipe', 'pipe'],
124
125
  signal: buildSignalFromTimeout(options.timeout),
125
126
  detached: usesProcessGroup,
@@ -256,7 +257,7 @@ export async function execInherit(command, args, options = {}) {
256
257
  const usesProcessGroup = options.processGroup === true && process.platform !== 'win32';
257
258
  const child = spawn(command, args, {
258
259
  cwd: options.cwd,
259
- env: { ...process.env, ...options.env },
260
+ env: buildChildEnv(options),
260
261
  stdio: 'inherit',
261
262
  signal: buildSignalFromTimeout(options.timeout),
262
263
  detached: usesProcessGroup,
@@ -368,14 +369,14 @@ export async function execInheritCapture(command, args, options = {}) {
368
369
  const usesProcessGroup = options.processGroup === true && process.platform !== 'win32';
369
370
  const child = spawn(command, args, {
370
371
  cwd: options.cwd,
371
- env: { ...process.env, ...options.env },
372
+ env: buildChildEnv(options),
372
373
  stdio: ['inherit', 'pipe', 'pipe'],
373
374
  signal: buildSignalFromTimeout(options.timeout),
374
375
  detached: usesProcessGroup,
375
376
  });
376
377
  const groupPid = usesProcessGroup ? child.pid : undefined;
377
- const out = createStreamCollector(process.stdout);
378
- const err = createStreamCollector(process.stderr);
378
+ const out = createStreamCollector(options.mirror?.stdout ?? process.stdout);
379
+ const err = createStreamCollector(options.mirror?.stderr ?? process.stderr);
379
380
  child.stdout.on('data', out.onData);
380
381
  child.stderr.on('data', err.onData);
381
382
  const graceMs = options.shutdownGraceMs ?? 1500;
@@ -440,7 +441,7 @@ export async function execSmokeRun(command, args, options) {
440
441
  const usesProcessGroup = process.platform !== 'win32';
441
442
  const child = spawn(command, args, {
442
443
  cwd: options.cwd,
443
- env: { ...process.env, ...options.env },
444
+ env: buildChildEnv(options),
444
445
  stdio: ['ignore', 'pipe', 'pipe'],
445
446
  // A new process group on POSIX lets us signal the whole descendant
446
447
  // tree at once, which is essential for mach → python → firefox →
package/docs/README.md ADDED
@@ -0,0 +1,35 @@
1
+ <!-- SPDX-License-Identifier: EUPL-1.2 -->
2
+
3
+ # FireForge documentation
4
+
5
+ [`../README.md`](../README.md) is the getting-started page. These files are
6
+ the reference material behind it. `fireforge --help` (and
7
+ `fireforge <command> --help`) stays the authority on flags; the docs cover
8
+ what a flag list cannot state.
9
+
10
+ ## Using FireForge
11
+
12
+ | File | Covers |
13
+ | ---------------------------------------------- | ------------------------------------------------------------------------------------ |
14
+ | [patch-workflow.md](patch-workflow.md) | Export, re-export, deletions, queue maintenance, per-patch lint, CI gating, rebasing |
15
+ | [testing.md](testing.md) | `fireforge test`: scope, modes, build freshness, the verdict line, verbosity |
16
+ | [furnace.md](furnace.md) | Components: overrides, shared CSS fragments, typed cross-module imports |
17
+ | [verification-trees.md](verification-trees.md) | CoW snapshots for concurrent read-mostly verification |
18
+ | [configuration.md](configuration.md) | `fireforge.json` settings, design tokens, lock waits, environment variables |
19
+
20
+ ## Contracts for scripts and CI
21
+
22
+ | File | Covers |
23
+ | -------------------------------------- | --------------------------------------------------------------------------- |
24
+ | [exit-codes.md](exit-codes.md) | Every exit code, which error class produces it, and how CI should branch |
25
+ | [machine-output.md](machine-output.md) | The `--json` / `--raw` / verdict-line output contract and its versioning |
26
+ | [run-logs.md](run-logs.md) | `.fireforge/logs/`: what `test` and `build` write, and what survives a pipe |
27
+
28
+ ## Internals
29
+
30
+ | File | Covers |
31
+ | -------------------------------------------------- | ---------------------------------------------------------------------------- |
32
+ | [lifecycle-invariants.md](lifecycle-invariants.md) | Locking, rollback, signal handling, and the decision table for new mutations |
33
+
34
+ Release history and the reasoning behind individual changes live in
35
+ [`../CHANGELOG.md`](../CHANGELOG.md).
@@ -0,0 +1,116 @@
1
+ <!-- SPDX-License-Identifier: EUPL-1.2 -->
2
+
3
+ # Configuration
4
+
5
+ `fireforge.json` at the project root is the project's configuration;
6
+ `furnace.json` holds component configuration. This file covers the settings
7
+ whose behaviour needs more than a schema line, plus the environment variables
8
+ FireForge reads.
9
+
10
+ ## External toolchains
11
+
12
+ Projects with generated asset prerequisites can declare opt-in
13
+ `externalToolchains`. `fireforge doctor` reports missing **required** tools as
14
+ errors and missing **optional** tools as warnings.
15
+
16
+ Seasonal branding on macOS, for example, can declare Apple's Icon Composer
17
+ tool at
18
+ `/Applications/Xcode.app/Contents/Applications/Icon Composer.app/Contents/Executables/ictool`,
19
+ plus `actool` via `xcrun`, `sips`, and `iconutil`.
20
+
21
+ ## `patchLint`
22
+
23
+ Per-patch lint type-checks plain Firefox JS against a built-in Firefox-globals
24
+ shim that tracks upstream WebIDL additions per release.
25
+
26
+ - `patchLint.checkJsExtraShim` adds members to the structured shim globals via
27
+ TypeScript interface merging, e.g.
28
+ `interface ChromeUtilsShim { newApi(): any }`.
29
+ - `patchLint.checkJsTestFiles: true` extends the pass to patch-owned test
30
+ `.js` files (`browser_*` / `test_*` / `xpcshell_*` and `/test/` paths). Each
31
+ is checked as its own small script-scope program against a loose built-in
32
+ harness shim.
33
+ - `patchLint.checkJsTestShim` points at a project `.d.ts` whose typed
34
+ declarations (e.g. a real `TestUtils` interface) override the loose
35
+ baseline, so calls to nonexistent harness members fail at export time.
36
+
37
+ ## `patchPolicy`
38
+
39
+ `patchPolicy.ranges` makes `patch compact` range-aware, so closing ordinal
40
+ gaps does not move a patch out of the band its category owns.
41
+
42
+ ## `test`
43
+
44
+ `test.canaryPath` and `test.canaryTimeoutSeconds` supply the defaults for
45
+ `fireforge test --canary`. See [`testing.md`](testing.md).
46
+
47
+ ## Design tokens
48
+
49
+ Tokens are managed with `fireforge token add`.
50
+
51
+ - `--create-category` declares a new category banner and inserts the token in
52
+ one step. It is incompatible with `--variant`; author the category with the
53
+ base token first, then add each variant.
54
+ - `--category` is required for a base declaration and optional under
55
+ `--variant`, where the declaration is routed into a `:root<selector>` block
56
+ and never into a category section. It is still validated when supplied.
57
+ - `--variant` takes the same selector grammar the block matcher understands:
58
+ one attribute fragment (`[data-private]`, `[data-skin=precision]`), a run of
59
+ them (`[data-skin=precision][data-theme=dark]`), and an optional pseudo-class
60
+ tail (`[data-skin=precision]:not([data-private])`). Names and values must be
61
+ identifier-safe, and `=value` is normalized to `="value"`. A variant naming a
62
+ qualifier matches only a block carrying that qualifier; one without a
63
+ qualifier still matches a qualified block, and the write says so.
64
+ - `--mode override` mirrors into both `:root[data-theme="dark"]` and
65
+ `:root[data-theme="light"]`. A qualified block such as
66
+ `:root[data-theme="light"]:not([data-private])` is matched and written
67
+ through, with a warning naming the selector — the qualifier is
68
+ semantically load-bearing, but silently skipping half the mirror is the
69
+ worse failure.
70
+ - A token already present in the target block is reported with its location
71
+ (`already exists in :root[…] (line N), unchanged`) rather than a bare
72
+ no-op.
73
+
74
+ ## Smoke runs
75
+
76
+ For unattended smoke checks, `fireforge run --smoke-exit <s> --headless`
77
+ launches the browser headless. A headed smoke window on a shared desktop
78
+ absorbs live input, which contaminates the console capture — headed non-CI
79
+ launches print a warning saying so. Exit codes 12 and 13 separate a console
80
+ regression from a launch failure; see [`exit-codes.md`](exit-codes.md).
81
+
82
+ ## Lock waits
83
+
84
+ Every FireForge command that mutates the engine takes the engine-session lock,
85
+ and some take a second subsystem lock as well. Contention fails fast after
86
+ about a second by default.
87
+
88
+ `--wait-lock [seconds]` (bare flag = 60, valid 1–3600) replaces that with a
89
+ bounded wait, and the budget covers **every** lock the command takes — for
90
+ `furnace apply`/`deploy`/`sync` that means the furnace lock as well as the
91
+ engine-session lock. While waiting, a progress line roughly every 5 seconds
92
+ identifies the holder by PID, command and start time.
93
+
94
+ Commands that take no lock accept `--wait-lock` and ignore it, so a scripted
95
+ sequence can blanket-append the flag without a usage error killing the run.
96
+
97
+ `FIREFORGE_WAIT_LOCK=<seconds>` applies the same budget to every command in a
98
+ session, using the same bounds as the flag. It is the only way to give a
99
+ budget to the furnace mutators that declare no flag (`create`, `remove`,
100
+ `rename`, `refresh`, `scan`, `override`, `chrome-doc`, …). An explicit
101
+ `--wait-lock` always wins.
102
+
103
+ `fireforge status --lock` reports the current holder and queue depth.
104
+
105
+ ## Environment variables
106
+
107
+ | Variable | Effect |
108
+ | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
109
+ | `FIREFORGE_WAIT_LOCK` | Session-wide lock wait budget in seconds (1–3600). See above. |
110
+ | `FIREFORGE_MAX_UNTRACKED_FILES` | Per-directory cap when expanding collapsed untracked directories in `status` (default 5000). A non-positive value warns and is ignored. |
111
+ | `FIREFORGE_GIT_ADD_TIMEOUT_MS` | Timeout for the monolithic `git add -A` baseline pass (default 10 min). For slow or loaded filesystems. |
112
+ | `FIREFORGE_GIT_ADD_CHUNK_TIMEOUT_MS` | Timeout for the chunked `git add -- <dir>` fallback (default 30 min). Paired with the above. |
113
+
114
+ `CLAUDECODE` is read by mozbuild, not by FireForge, and `fireforge test
115
+ --full-output` unsets it for test dispatches only — see
116
+ [`testing.md`](testing.md#output-verbosity).
@@ -0,0 +1,63 @@
1
+ <!-- SPDX-License-Identifier: EUPL-1.2 -->
2
+
3
+ # Exit codes (internal)
4
+
5
+ The exit code is the machine-readable half of the CLI's contract: a script
6
+ branches on it, and CI decides whether to retry or escalate. The values live
7
+ in `src/errors/codes.ts`; this file is the operator-facing statement of what
8
+ they mean and which class produces each.
9
+
10
+ | Code | Name | Meaning | Produced by |
11
+ | ---: | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
12
+ | 0 | `SUCCESS` | Completed. Also a run that prompted and the operator declined — declining is a successful run that chose not to proceed. | — |
13
+ | 1 | `GENERAL_ERROR` | Unspecified failure the operator must act on. | `GeneralError`, `ParserFallbackError` |
14
+ | 2 | `CONFIG_ERROR` | `fireforge.json` missing or invalid. | `ConfigError` and subclasses |
15
+ | 3 | `DOWNLOAD_ERROR` | Could not download or extract Firefox source. | `DownloadError` and subclasses |
16
+ | 4 | `GIT_ERROR` | A git operation failed. | `GitError` and subclasses |
17
+ | 5 | `BUILD_ERROR` | `mach build` failed. | `BuildError` and subclasses |
18
+ | 6 | `PATCH_ERROR` | A patch failed to apply, or the patch queue could not be mutated. | `PatchError` |
19
+ | 7 | `MISSING_DEPENDENCY` | A required tool (python3, git, tar) was not found. | `PythonNotFoundError`, `GitNotFoundError`, `MachNotFoundError` |
20
+ | 8 | `INVALID_ARGUMENT` | The flags were wrong. Usage problem, not an environment one. | `InvalidArgumentError` |
21
+ | 9 | `FURNACE_ERROR` | A Furnace component operation failed. | `FurnaceError` |
22
+ | 10 | `RESOLUTION_ERROR` | Conflict resolution failed. | `ResolutionError` |
23
+ | 11 | `INTERNAL_ERROR` | A FireForge invariant did not hold. **A bug in FireForge**, not something the operator can fix. | `InternalInvariantError` |
24
+ | 12 | `SMOKE_EXIT_FAILURE` | `run --smoke-exit` saw unallowed console errors in the smoke window. | `SmokeRunError` |
25
+ | 13 | `SMOKE_LAUNCH_FAILURE` | `run --smoke-exit` saw a non-clean browser exit before the window elapsed. | `SmokeRunError` |
26
+ | 14 | `INCONCLUSIVE` | A test run's verdict was thrown away: `engine/` moved while the harness ran. **Not red** — re-run it. | `InconclusiveVerdictError` |
27
+ | 15 | `LOCK_TIMEOUT` | A lock wait expired, so the run never started. Re-queue it, with a larger `--wait-lock`. | `LockContentionError` |
28
+ | 130 | `USER_CANCELLED` | The operator INTERRUPTED a prompt (Esc / Ctrl+C). 128 + SIGINT. | `CancellationError` |
29
+
30
+ ## The three distinctions that matter for CI
31
+
32
+ **Retry vs escalate.** `GENERAL_ERROR` usually means the operator must change
33
+ something and a retry may help. `INTERNAL_ERROR` means nothing they could
34
+ change would have helped — escalate it, do not retry.
35
+
36
+ **Red vs never-ran vs thrown-away.** These are three different facts and,
37
+ before 0.44.0, two of them shared exit 1. `BUILD_ERROR` (5) is a suite that
38
+ FAILED. `LOCK_TIMEOUT` (15) is a run that never started, because another
39
+ FireForge process held the lock for the whole budget — nothing about the
40
+ request was wrong, so re-queue it. `INCONCLUSIVE` (14) is a suite whose
41
+ result was DISCARDED, because `engine/` moved underneath it — the code may
42
+ be perfectly green, and reporting it as red is wrong. A summary line reading
43
+ `FAIL — exit 1` beside `FAIL — exit 5` invited treating opposites as the same
44
+ kind of fact; the codes now separate them mechanically. The
45
+ `FIREFORGE-VERDICT:` line's `reason=` key carries the same distinction for
46
+ humans.
47
+
48
+ Note that `LockContentionError` backs every lock in FireForge — engine
49
+ session, patch directory, build, furnace — so all four now exit 15. That is
50
+ deliberate: the fact is the same whichever lock was contended.
51
+
52
+ **Interrupted vs declined.** Before 0.44.0 both a Ctrl+C and a deliberate
53
+ "no" at a confirmation returned the same value and exited 0, so a script
54
+ could not tell them apart. They are now distinct: an interrupt raises
55
+ `CancellationError` and exits **130**; a declined confirmation returns
56
+ normally and exits **0**, because the run did what it was asked and the
57
+ answer was no.
58
+
59
+ ## Adding a code
60
+
61
+ New codes go in `src/errors/codes.ts` with a comment saying what a consumer
62
+ should DO differently on seeing it — if the answer is "nothing different from
63
+ an existing code", it does not need one. Add a row here in the same change.
@@ -0,0 +1,57 @@
1
+ <!-- SPDX-License-Identifier: EUPL-1.2 -->
2
+
3
+ # Furnace components
4
+
5
+ Furnace is FireForge's answer to UI components. It tracks stock widgets,
6
+ creates new fork-owned widgets, and overrides existing ones. The files still
7
+ land inside the Firefox source directory and end up as part of the regular
8
+ patches to export.
9
+
10
+ ```bash
11
+ npx fireforge furnace scan
12
+ npx fireforge furnace override moz-button -t css-only
13
+ npx fireforge furnace create moz-my-widget
14
+ npx fireforge furnace deploy
15
+ npx fireforge furnace status
16
+ npx fireforge furnace preview
17
+ ```
18
+
19
+ `fireforge furnace --help` lists every subcommand.
20
+
21
+ ## Shared CSS fragments
22
+
23
+ Cross-widget CSS can be single-sourced. Place the fragment in
24
+ `components/shared/` and reference it from a widget stylesheet with a
25
+ comment:
26
+
27
+ ```css
28
+ /* @fireforge-include base-button.css */
29
+ ```
30
+
31
+ `deploy` expands it into the deployed copy only, so editing the fragment
32
+ surfaces as component drift until the next deploy.
33
+
34
+ ## Typed cross-module imports
35
+
36
+ For multi-file components, set `furnace.json#typecheckJsconfig` to a
37
+ consumer-owned jsconfig. `deploy` then maintains `compilerOptions.paths`
38
+ entries mapping each deployed
39
+ `chrome://global/content/elements/<file>.mjs` URL to its workspace source.
40
+
41
+ Entries are recognized as Furnace-managed by containment in the components
42
+ workspace, so a hand-written entry is preserved and a managed entry whose
43
+ helper was deleted is pruned.
44
+
45
+ ## Safety
46
+
47
+ Every furnace mutation runs through `runFurnaceMutation`: a per-root lock, a
48
+ rollback journal restored on both a signal and a thrown error, and a
49
+ `pendingRepair` marker that blocks further mutations when a rollback was
50
+ incomplete — cleared with `fireforge doctor --repair-furnace`. The full
51
+ contract, including which commands take which locks, is in
52
+ [`lifecycle-invariants.md`](lifecycle-invariants.md).
53
+
54
+ `apply`, `deploy` and `sync` accept `--wait-lock [seconds]`, whose budget
55
+ covers **both** locks they take. The twelve mutators without the flag pick up
56
+ a session budget from `FIREFORGE_WAIT_LOCK`; see
57
+ [`configuration.md`](configuration.md#lock-waits).