firstlight 0.0.0-stage → 0.3.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 +58 -0
- package/LICENSE +21 -0
- package/README.md +455 -2
- package/dist/baseline.js +86 -0
- package/dist/cache.js +54 -0
- package/dist/checks.js +125 -0
- package/dist/cli.js +519 -0
- package/dist/config.js +405 -0
- package/dist/diagnostics.js +482 -0
- package/dist/errors.js +35 -0
- package/dist/eta.js +76 -0
- package/dist/names.js +67 -0
- package/dist/processes.js +56 -0
- package/dist/report.js +299 -0
- package/dist/store.js +369 -0
- package/dist/supervisor.js +188 -0
- package/dist/tree.js +60 -0
- package/package.json +60 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0 (2026-10-16)
|
|
4
|
+
|
|
5
|
+
- A version bump that lands on `main` now publishes the package: `publish.yml` runs on a push to `main` that changes `package.json`, releases nothing when the `v<version>` tag already exists, and after `npm publish` creates that tag and the GitHub release on the published commit. Nobody pushes a release tag by hand.
|
|
6
|
+
|
|
7
|
+
- A check whose command, or whose `npm run` script, joins simple commands with `&&` gets the parser of each command that has one, in order, so `tsc --noEmit && vitest run` shows tsc's or vitest's diagnostics instead of a 40-line log tail. `parser` in `firstlight.json` and the parser recorded in `run.json` can be a list, like `["tsc", "vitest"]`, and `firstlight init` writes one for such a script. A list joined by `||` or `;`, or a pipe, still gets none.
|
|
8
|
+
|
|
9
|
+
- The README has a CPU contention section: measurements of checks run alone and together in three repositories, why firstlight has no concurrency limit, and how `after` trades a longer run for an earlier result of one heavy check.
|
|
10
|
+
|
|
11
|
+
- `firstlight init [script|pattern...]` proposes a `firstlight.json` from the `package.json` scripts and prints it; `--write` writes it, and refuses to replace an existing one without `--force`. With no names, it holds the scripts `lint`, `typecheck`, `test`, and `archstrict` that exist; names and patterns pick other scripts. Each check runs `npm run <script>`, gets the parser inferred from the script, and gets a `timeoutSec` of three times its newest pass, in whole minutes, when that is more than the 600-second default. It names the check-like scripts it left out, with the command that adds them. `--json` prints one `{"type":"init",...}` object. A shell command, an unknown script, a missing `package.json`, or a package with none of the four scripts and no names gives the error envelope.
|
|
12
|
+
- A check name in `firstlight.json` may contain `:`, so a check can keep its script's name, like `check:architecture`.
|
|
13
|
+
|
|
14
|
+
- Without `firstlight.json`, `firstlight run` names the `package.json` scripts it did not run whose name starts with `lint`, `typecheck`, `test`, `check`, or `archstrict` (for example `check:architecture`, `test:mutation`, or `check`), on a `not run:` line after its result, with a hint to name one or write `firstlight.json` with `firstlight init`. With `--json`, the summary gains `not_run`, only when that list is not empty. It still runs only `lint`, `typecheck`, `test`, and `archstrict`, because such a script can chain the others or take hours. When none of those four exists, the error envelope names the check-like scripts and its `do` runs them.
|
|
15
|
+
|
|
16
|
+
- Once every check of a run is done and one did not pass, the next command that `run`, `results`, `results --new`, and `follow` print is `firstlight run --failed` instead of `firstlight run`, in the text `next:` line and the `next` field of `--json`. While a check is still running, it stays `firstlight results --wait`. The `do` of an error envelope does not change, and neither does the next command after `stop`.
|
|
17
|
+
|
|
18
|
+
- On a Node older than 24, every command stops before it reads its arguments or starts a run, and prints the error envelope `{"error":"firstlight needs Node 24 or newer (found v20.19.0)","do":"install Node 24 or newer, then rerun the same command"}` with exit code 1. Before, an older Node could run firstlight on a version it was never tested on.
|
|
19
|
+
|
|
20
|
+
## 0.2.0 (2026-10-13)
|
|
21
|
+
|
|
22
|
+
`0.1.0` was removed from npm on 2026-10-12, and npm does not let a removed version be published again. `0.2.0` holds everything in `0.1.0` and the changes below.
|
|
23
|
+
|
|
24
|
+
- `firstlight run --failed` runs only the checks of the latest run that did not pass (`failed`, `timed_out`, `skipped`, `cancelled`, or `unknown`). Each one resolves against the current `firstlight.json` like a name given to `run`: a configured check comes with its `after` checks, and reused passes still apply. A script or shell command from the command line runs the same command again, and a configured check removed from `firstlight.json` is left out. With no run yet, or no check that did not pass, it prints the error envelope with `firstlight run` and exits 1. It takes no check names.
|
|
25
|
+
- `firstlight follow` attaches to the latest run and prints one line for each status change, from the first one, as it happens: `test passed (exit 0, 10.4s)`, or `typecheck failed (exit 2, 3.5s)` with the same log tail that `run` prints. It ends with the summary lines of `results --wait` and exits with the same code. `--json` prints one check object per change, in the shape of `results --json`, then the summary. Each line is written on its own, so a line watcher such as Claude Code's Monitor sees it at once. `follow` never writes `events.jsonl`.
|
|
26
|
+
- A failed or timed-out check prints its diagnostics before the log tail, as `file:line:col rule message` lines, when firstlight can read the tool's output. The parsers read the text of `tsc` (plain and `--pretty`), the vitest default reporter, `node --test` (spec and TAP reporters), and `archstrict check`. A test's diagnostic names the test's full name and the first stack frame inside the repository. With diagnostics, the log tail is 10 lines instead of 40. A check shows at most 20 diagnostics, then a count of the rest. In `--json`, each diagnostic is a `{"type":"diagnostic","check","file","line","col","rule","message"}` line after its check, and the check has `more_diagnostics` when some were left out. Without a parser, or with no diagnostics, the output does not change. This applies to `run`, `results`, and the failure lines of `follow`.
|
|
27
|
+
- `firstlight results --new` reports only the failures that the previous run did not have, and counts the rest: `not new: 2 failures already in the previous run, 3 passed`. The previous run of a check is the newest earlier kept run in which the same check, with the same `run` command, passed, failed, or timed out. A diagnostic is new when that run has none of the same check with the same file, rule, and message; the line and column do not count, so moved code is not new. A failure without diagnostics is new when the check did not fail in that run. Without a previous run, every failure is new, and one line says so. It exits 1 only for a new failure, or for a check that is still running, was cancelled, or is unknown. It works with `--wait`. With `--json`, each check gains `new` and `baseline` (and `old_diagnostics` when it failed), only new diagnostics are printed, and the summary gains `new`, `new_failures`, `old_failures`, and `no_baseline`.
|
|
28
|
+
- A vitest diagnostic shows the evidence file and the rerun command that depug's reporter printed for its test, as `evidence: <path>` and `rerun: <command>` lines under the diagnostic. In `--json`, the diagnostic line gets `evidence` and `rerun` fields. firstlight matches by the test file and the full test name in depug's rerun command; when two failed tests or two depug failures have the same file and name, or depug printed nothing for a test, it attaches nothing. A log without depug lines gives the same diagnostics as before. This applies to `run`, `results`, `results --new`, and `follow`.
|
|
29
|
+
- A check in `firstlight.json` takes `parser`: `tsc`, `vitest`, `node-test`, `archstrict`, or `none`. Any other value is a config error. Without it, firstlight infers the parser from a simple `run` command (`tsc`, `vitest`, `node --test`, `archstrict`), and from the script that `npm run <script>` or `npm test` runs, one level deep.
|
|
30
|
+
|
|
31
|
+
## 0.1.0 (2026-10-12)
|
|
32
|
+
|
|
33
|
+
The first version of firstlight.
|
|
34
|
+
|
|
35
|
+
- `firstlight run` starts every check from `firstlight.json` in parallel and returns at the first check that fails or times out. It prints that check's exit code, the last 40 lines of its log, the status of the other checks, and the next command. The other checks keep running. When every check passes, it prints one line and exits 0.
|
|
36
|
+
- `firstlight run <check>...` runs only the named checks and the checks they are `after`.
|
|
37
|
+
- `firstlight run` reads like npm-run-all's `run-p`: `firstlight run lint typecheck test` runs those `package.json` scripts in parallel as `npm run <script>`, and `firstlight run "lint:*"` runs every script that matches the pattern, in `package.json` order. `*` matches inside one `:`-separated segment, and `**` matches across segments. A check of the same name in `firstlight.json` comes first, with its `after` checks. An unknown name, or a pattern that matches no script, prints the error envelope with the configured checks and the scripts.
|
|
38
|
+
- A check can be any shell command, given on the command line: an argument with whitespace, like `firstlight run "bundle exec rubocop" "bundle exec rspec"`, or any argument after `--`, like `firstlight run lint -- make`. Its name is the command itself. The checked project needs no `package.json`.
|
|
39
|
+
- A log file name replaces each character outside `[A-Za-z0-9._-]` in the check name with `_`, and adds `-2`, `-3`, and so on when two checks of a run would share one file. The check name in the output stays as written.
|
|
40
|
+
- A second `firstlight run` stops the checks still running from the previous run before it starts.
|
|
41
|
+
- `firstlight results` reports the latest run, and `--wait` blocks until every check is done. It exits 1 when any check failed, timed out, was skipped or cancelled, or is unknown. `firstlight stop` cancels the running checks, and does nothing when none are running.
|
|
42
|
+
- A check can wait for other checks with `after`. If one of them does not pass, the check is `skipped`. A check that runs longer than `timeoutSec` (600 by default) is stopped and recorded as `timed_out`.
|
|
43
|
+
- Without `firstlight.json`, the `package.json` scripts `lint`, `typecheck`, `test`, and `archstrict` become checks.
|
|
44
|
+
- A detached supervisor per run starts the checks, each in its own process group, and records every status change in `events.jsonl` under `$XDG_STATE_HOME/firstlight/`. Nothing is written into the checked repository. The newest 20 runs are kept.
|
|
45
|
+
- If the supervisor dies before a check finishes, that check is reported as `unknown` with `orphan: true`, and no exit code is invented.
|
|
46
|
+
- A running or pending check shows about how long it has left, and the `next: firstlight results --wait` line shows the time left for the whole run. The estimate comes from the newest pass of the same check, with the same `run` command, in the kept runs. A running check past that duration says `past its usual` instead. Without that history, no estimate is shown. In `--json`, each check has `eta_ms` and `overdue`, and the summary has `eta_ms`.
|
|
47
|
+
- `firstlight run` reuses a pass instead of running the check again when a kept run passed it on the same working tree with the same command, and its `after` checks did the same. The working tree is a git tree hash of the tracked and untracked files, without ignored files, computed through a temporary index, so the real index and refs do not change. The text says `test passed (cached from run 01K..., 10.4s originally)`, the all-passed line counts the reused checks, and in `--json` each check has `cached_from`. Only a pass is reused. Environment variables and files outside the repository are not part of the comparison; `firstlight run --no-cache` runs every check anyway and still records its keys. Outside a git repository nothing is reused.
|
|
48
|
+
- `--json` prints JSON Lines: one object per check and one summary. A failure of firstlight itself prints `{"error","do"}` on stderr and exits 1.
|
|
49
|
+
|
|
50
|
+
Development:
|
|
51
|
+
|
|
52
|
+
- Runtime code still uses Node built-ins only. Development now uses TypeScript for `npm run typecheck`, vitest for `npm test`, Hegel property tests for the state machine, `after` validation, and run ids, archstrict for module boundaries and the layer order (`npm run archstrict`), Stryker with stryker-agent-reporter for mutation testing (`npm run mutation`), and depug, which prints an evidence file and a rerun command for each failed test. Every version is pinned exactly.
|
|
53
|
+
- The tests moved from `node:test` to vitest and assert the same behavior.
|
|
54
|
+
- `firstlight.json` makes firstlight check itself: `typecheck`, `test`, `archstrict`, and a scoped `mutation` run, in parallel.
|
|
55
|
+
- CI runs `npm ci`, `npm test`, `npm run typecheck`, and `npm run archstrict` on Node 24, with the actions pinned to commits.
|
|
56
|
+
- The package is ready to publish to npm as `firstlight`: `npm i -D firstlight` installs a `firstlight` bin. `npm run build` compiles `src/` to `dist/` with tsc, rewriting the `.ts` relative imports to `.js`, because Node does not strip types from files under `node_modules`. `prepack` runs the build, and the tarball holds `dist/`, `README.md`, `CHANGELOG.md`, `LICENSE`, and `package.json`. The supervisor starts from the same compiled file as the CLI.
|
|
57
|
+
- `test/pack.test.ts` packs the package, installs the tarball into a temporary project, and runs the installed bin: `--help`, and a `firstlight run` of a passing shell command.
|
|
58
|
+
- `.github/workflows/publish.yml` publishes a `v*` tag to npm through a trusted publisher after the GitHub Environment `publish` approves it, with provenance, then creates the GitHub release from that version's changelog section. `docs/releasing.md` lists the one-time bootstrap and the steps for each version. `SECURITY.md` points to private vulnerability reporting.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 meganemura
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,456 @@
|
|
|
1
|
-
#
|
|
1
|
+
# firstlight
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
firstlight is a parallel check runner for coding agents. An agent runs lint, typecheck, and tests through it.
|
|
4
|
+
|
|
5
|
+
firstlight starts every check at the same time and returns as soon as the first one fails, so the agent can start to fix it. The other checks keep running in the background, and the agent reads their results later. When the agent runs `firstlight run` again, firstlight first stops the checks that are still running from the previous run, then starts over.
|
|
6
|
+
|
|
7
|
+
`firstlight run` reads like npm-run-all's `run-p`, the parallel runner:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
firstlight run lint typecheck test
|
|
11
|
+
firstlight run "lint:*" test
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
A check is any shell command, so a project does not need Node or a `package.json`. Node is only needed to run firstlight itself. A Ruby project can pass its commands directly:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
firstlight run "bundle exec rubocop" "bundle exec rspec"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
or keep them in `firstlight.json`:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"checks": {
|
|
25
|
+
"rubocop": { "run": "bundle exec rubocop" },
|
|
26
|
+
"rspec": { "run": "bundle exec rspec", "timeoutSec": 900 }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
and run `firstlight run`, or `firstlight run rspec` for one of them.
|
|
32
|
+
|
|
33
|
+
## Agent flow
|
|
34
|
+
|
|
35
|
+
1. Start the checks:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
firstlight run lint typecheck test
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
It returns at the first failed or timed-out check, with the last 40 lines of that check's log, the status of every other check, and the next command. When firstlight can read that tool's output, the check's [diagnostics](#diagnostics) come first and the log tail is 10 lines. If every check passes, it prints one line and exits 0.
|
|
42
|
+
|
|
43
|
+
While other checks are still running, the next command is `firstlight results --wait`.
|
|
44
|
+
|
|
45
|
+
2. Fix the failure. Meanwhile, the other checks continue.
|
|
46
|
+
|
|
47
|
+
3. Read the rest:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
firstlight results --wait
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or watch each check finish as it happens, with `firstlight follow`.
|
|
54
|
+
|
|
55
|
+
4. Run only what did not pass. Once every check is done and one failed, this is the next command that `results` and `follow` print. It cancels anything left from step 1:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
firstlight run --failed
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or run every check again:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
firstlight run lint typecheck test
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Arguments of `run`
|
|
68
|
+
|
|
69
|
+
`run` is the parallel `run-p` equivalent. Each argument is one of:
|
|
70
|
+
|
|
71
|
+
| Argument | Meaning |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| a word, like `test` | The check of that name in `firstlight.json`, together with the checks it is `after`. Without such a check, the `package.json` script of that name, run as `npm run test`. |
|
|
74
|
+
| a pattern, like `"lint:*"` | Every `package.json` script it matches, in `package.json` order. As in npm-run-all, `*` matches inside one `:`-separated segment and `**` matches across segments: `lint:*` matches `lint:js` and `lint:css` but not `lint:js:fix`, and `test:**` matches all three levels. Only `*` is special. Quote a pattern so the shell does not expand it. |
|
|
75
|
+
| an argument with whitespace, like `"bundle exec rspec"` | A shell command. Its check name is the command itself. |
|
|
76
|
+
| any argument after `--` | A shell command, one per argument, even a single word: `firstlight run lint -- make "bundle exec rspec"` runs the `lint` check or script, `make`, and `bundle exec rspec`. Put `--cwd` and `--json` before `--`. |
|
|
77
|
+
|
|
78
|
+
The checks run in argument order, and a configured check's `after` checks come just before it. An argument repeated, or a pattern that matches a name already given, runs once. Two different checks under one name are refused. A word that is neither a configured check nor a script, and a pattern that matches no script, stop firstlight with the error envelope, which lists the configured checks and the scripts.
|
|
79
|
+
|
|
80
|
+
With no arguments, `firstlight run` runs every check in `firstlight.json`, or, without that file, the `package.json` scripts `lint`, `typecheck`, `test`, and `archstrict` that exist.
|
|
81
|
+
|
|
82
|
+
firstlight has no sequential `run-s` mode and none of npm-run-all's other flags. To run a check only after another passes, give it `after` in `firstlight.json`.
|
|
83
|
+
|
|
84
|
+
## Install
|
|
85
|
+
|
|
86
|
+
Requires Node.js 24 or newer. On an older Node, every command stops before it does anything and prints `{"error":"firstlight needs Node 24 or newer (found v20.19.0)","do":"install Node 24 or newer, then rerun the same command"}` with exit code 1. Add it to a project as a development dependency:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
npm i -D firstlight
|
|
90
|
+
npx firstlight --help
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Or run it once without adding it:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
npx firstlight run
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Without `firstlight.json`, `firstlight run` runs the `package.json` scripts `lint`, `typecheck`, `test`, and `archstrict`. To choose the checks, let `firstlight init` propose a `firstlight.json`, read it, then write it:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
npx firstlight init typecheck test check:architecture
|
|
103
|
+
npx firstlight init typecheck test check:architecture --write
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
See [Starting a config](#starting-a-config).
|
|
107
|
+
|
|
108
|
+
The package holds JavaScript compiled from `src/` and has no runtime dependencies. From a checkout, Node strips the TypeScript types and runs the source directly, with no build:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
node src/cli.ts --help
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Config
|
|
115
|
+
|
|
116
|
+
`firstlight.json` at the repository root:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"checks": {
|
|
121
|
+
"build": { "run": "npm run build" },
|
|
122
|
+
"types": { "run": "tsc --noEmit", "after": ["build"] },
|
|
123
|
+
"test": { "run": "node --test", "after": ["build"], "timeoutSec": 600 }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
| Key | Meaning |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| `run` | A command string. firstlight runs it with `/bin/sh -c` in the repository root. stdout and stderr go to one log file. |
|
|
131
|
+
| `after` | Checks that must pass before this one starts. If one of them does not pass, this check is `skipped`. |
|
|
132
|
+
| `timeoutSec` | Seconds before firstlight stops the check and records `timed_out`. The default is 600. |
|
|
133
|
+
| `parser` | How to read the check's log when it fails: `tsc`, `vitest`, `node-test`, or `archstrict`, or a list of them, like `["tsc", "vitest"]`, read in order. Without it, firstlight infers the parser from `run`. `none` turns diagnostics off for this check. See [Diagnostics](#diagnostics). |
|
|
134
|
+
|
|
135
|
+
A check name in `firstlight.json` must match `[A-Za-z0-9][A-Za-z0-9._:-]*`, so that an argument that names it cannot also read as a pattern or a command. firstlight rejects unknown keys, unknown `after` names, cycles, and an unknown `parser` before it starts anything.
|
|
136
|
+
|
|
137
|
+
Without `firstlight.json`, each of the `package.json` scripts `lint`, `typecheck`, `test`, and `archstrict` that exists becomes a check that runs `npm run <name>`, with no `after`.
|
|
138
|
+
|
|
139
|
+
Other scripts whose name starts with one of those words or with `check` (`check:architecture`, `test:mutation`, `check`) are not run, because such a script can chain the others or take hours. `firstlight run` names them instead, after its result:
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
passed 2 checks in 79.5s (run 01K...)
|
|
143
|
+
not run: check:architecture, test:mutation (without firstlight.json, only lint, typecheck, test, archstrict run; name a script, like firstlight run check:architecture, or write firstlight.json with firstlight init)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
With `--json`, the summary gains `"not_run": ["check:architecture", "test:mutation"]`. When none of the four scripts exists, the error envelope names those scripts and suggests running them.
|
|
147
|
+
|
|
148
|
+
### Starting a config
|
|
149
|
+
|
|
150
|
+
`firstlight init` proposes a `firstlight.json` from the `package.json` scripts and prints it. It writes nothing until `--write` is given, and `--write` does not replace an existing `firstlight.json` without `--force`.
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
$ firstlight init typecheck test check:architecture
|
|
154
|
+
would write /repo/firstlight.json:
|
|
155
|
+
{
|
|
156
|
+
"checks": {
|
|
157
|
+
"typecheck": {
|
|
158
|
+
"run": "npm run typecheck",
|
|
159
|
+
"parser": "tsc"
|
|
160
|
+
},
|
|
161
|
+
"test": {
|
|
162
|
+
"run": "npm run test",
|
|
163
|
+
"parser": "vitest"
|
|
164
|
+
},
|
|
165
|
+
"check:architecture": {
|
|
166
|
+
"run": "npm run check:architecture"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
left out: test:mutation (to add them: firstlight init typecheck test check:architecture test:mutation)
|
|
171
|
+
next: firstlight init typecheck test check:architecture --write
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- With no names, the proposal holds the scripts `lint`, `typecheck`, `test`, and `archstrict` that exist. Names and patterns (`"check:*"`, as in `run`) pick any scripts instead. A shell command is not a script, so add it to `firstlight.json` by hand.
|
|
175
|
+
- Each check is named after its script and runs `npm run <script>`, the same command `firstlight run <script>` runs, so the runs before `init` still count for reused passes and time estimates.
|
|
176
|
+
- `parser` is written when firstlight can infer it from the script (see [Diagnostics](#diagnostics)). A script that chains commands with `&&`, like `tsc --noEmit && vitest run`, gets the list of its commands' parsers.
|
|
177
|
+
- `timeoutSec` is written when a kept run passed the same check in more than a third of the default 600 seconds: three times that duration, rounded up to whole minutes.
|
|
178
|
+
- `left out:` names the scripts that look like checks (they start with `lint`, `typecheck`, `test`, `check`, or `archstrict`) and that the proposal does not hold, with the command that adds them.
|
|
179
|
+
- `--json` prints one object: `{"type":"init","file","written","existed","config","left_out","next"}`.
|
|
180
|
+
|
|
181
|
+
The repository root is `git rev-parse --show-toplevel` from the current directory, or the current directory when that fails. `--cwd DIR` starts from `DIR` instead.
|
|
182
|
+
|
|
183
|
+
## Commands
|
|
184
|
+
|
|
185
|
+
```text
|
|
186
|
+
firstlight run [name|pattern|"command"...] [-- command...] [--no-cache] [--cwd DIR] [--json]
|
|
187
|
+
firstlight run --failed [--no-cache] [--cwd DIR] [--json]
|
|
188
|
+
firstlight results [--wait] [--new] [--cwd DIR] [--json]
|
|
189
|
+
firstlight follow [--cwd DIR] [--json]
|
|
190
|
+
firstlight stop [--cwd DIR] [--json]
|
|
191
|
+
firstlight init [script|pattern...] [--write [--force]] [--cwd DIR] [--json]
|
|
192
|
+
firstlight --help
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- `run` stops the previous run if it is still going, starts the checks, and returns at the first `failed` or `timed_out` check, or when every check is done. A check that already passed on the same working tree is reused instead of run; see [Reused passes](#reused-passes). `--no-cache` runs every selected check.
|
|
196
|
+
- `run --failed` runs only the checks that did not pass in the latest run; see [Running what failed](#running-what-failed).
|
|
197
|
+
- `results` reports the latest run: status, exit code, elapsed time, and for `failed` and `timed_out` checks the diagnostics, the log path, and the last 40 log lines (10 when there are diagnostics). `--wait` blocks until every check is done. `--new` reports only the failures the previous run did not have; see [Only new failures](#only-new-failures).
|
|
198
|
+
- `follow` prints the status changes of the latest run as they happen; see [Following a run](#following-a-run).
|
|
199
|
+
- `stop` cancels the checks that are still running. With nothing running, it does nothing and exits 0.
|
|
200
|
+
- `init` proposes a `firstlight.json` from the `package.json` scripts, and writes it with `--write`; see [Starting a config](#starting-a-config).
|
|
201
|
+
|
|
202
|
+
A `running` or `pending` check shows about how long it has left, and the `next: firstlight results --wait` line shows the time left for the whole run, so the agent knows when to pull the rest:
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
lint failed (exit 1, 0.4s)
|
|
206
|
+
log: /.../lint.log
|
|
207
|
+
src/a.ts:3:1 unused variable x
|
|
208
|
+
test running (3.5s, about 8.0s left)
|
|
209
|
+
types pending (about 12s left)
|
|
210
|
+
run 01K...: 1 failed, 2 running
|
|
211
|
+
next: firstlight results --wait (about 12s left)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The estimate comes from the `elapsed_ms` of the newest `passed` result of a check with the same name and the same `run` command in an earlier kept run. This holds for script and command checks too: `lint:js` is the command `npm run lint:js` every time. A failed, timed-out, or cancelled result does not count, because it stopped early. A running check has that duration minus the time it has run. When it has already run longer, the line says `past its usual 9.0s` instead. A pending check has its own duration plus the longest time left of its `after` checks. The run has the longest time left of its unfinished checks. When a check, or one of its `after` checks, has no such history, firstlight shows no estimate for it, and none for the run. Times below 10 seconds keep one decimal; longer times are whole seconds.
|
|
215
|
+
|
|
216
|
+
## Running what failed
|
|
217
|
+
|
|
218
|
+
`firstlight run --failed` runs the checks of the latest run that did not pass: `failed`, `timed_out`, `skipped`, `cancelled`, and `unknown`. If the latest run is still going, it is stopped first, so a check that stop cancels runs again too. Each check resolves the way its name does in `firstlight run <name>`, against the current `firstlight.json`: a configured check comes with the checks it is `after`, and a reused pass still applies, so an `after` check that passed on the same working tree is not run again. A `package.json` script or a shell command that was given on the command line runs the same command again. A configured check that has since been removed from `firstlight.json` is left out.
|
|
219
|
+
|
|
220
|
+
When there is no run yet, or every check of the latest run passed, `run --failed` starts nothing, prints the error envelope with `"do":"firstlight run"`, and exits 1, because there is nothing it could prove. The same happens when every check that did not pass has been removed from `firstlight.json`. `--failed` takes no check names.
|
|
221
|
+
|
|
222
|
+
The latest run is the one `--failed` reads, so after `firstlight run --failed`, the next `--failed` narrows to what still fails among those checks. Run `firstlight run` to run every check again.
|
|
223
|
+
|
|
224
|
+
## Following a run
|
|
225
|
+
|
|
226
|
+
`firstlight follow` attaches to the latest run and prints one line for each status change of each check, starting from the first one, so a late caller sees the whole history. It then prints the summary lines that `results --wait` prints and exits with the code `results --wait` would give:
|
|
227
|
+
|
|
228
|
+
```text
|
|
229
|
+
lint running
|
|
230
|
+
test running
|
|
231
|
+
lint failed (exit 1, 0.4s)
|
|
232
|
+
log: /.../lint.log
|
|
233
|
+
src/a.ts:3:1 unused variable x
|
|
234
|
+
test passed (exit 0, 10.4s)
|
|
235
|
+
run 01K...: 1 failed, 1 passed
|
|
236
|
+
next: firstlight run --failed
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
A `running` line shows the time left that firstlight estimated when the check started, when there is one. A failed or timed-out check has the same [diagnostics](#diagnostics) and log tail that `run` prints. A reused pass says `cached from run 01K...`, and a check whose supervisor died appears as `unknown` at the end. With `--json`, each change is one check object, in the same shape as `results --json`, followed by the diagnostic lines of a failed check, and the last line is the summary object.
|
|
240
|
+
|
|
241
|
+
Each line is written as it happens, with no buffering, so a tool that watches a command's output line by line, such as Claude Code's Monitor, sees each check finish. `follow` only reads the state; the supervisor stays the only writer of `events.jsonl`. With no run yet, it prints the error envelope with `"do":"firstlight run"`.
|
|
242
|
+
|
|
243
|
+
## Only new failures
|
|
244
|
+
|
|
245
|
+
`firstlight results --new` reports only the failures that were not there before, so a project that already has failures can still gate a change on not adding one:
|
|
246
|
+
|
|
247
|
+
```text
|
|
248
|
+
typecheck failed (exit 2, 1.4s)
|
|
249
|
+
src/a.ts:4:26 TS2304 Cannot find name 'missing'.
|
|
250
|
+
(1 diagnostic already in run 01K...)
|
|
251
|
+
log: /.../typecheck.log
|
|
252
|
+
...
|
|
253
|
+
not new: 1 failure already in the previous run, 2 passed
|
|
254
|
+
run 01K...: 1 new failure
|
|
255
|
+
next: firstlight run --failed
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The previous run is chosen per check: it is the newest kept run before the latest one in which a check with the same name and the same `run` command `passed`, `failed`, or `timed_out`. A run in which that check was `cancelled`, `skipped`, or never finished is passed over, because it proved nothing either way. This is per check, not one run for all, because `run --failed` and `run <name>` leave runs that hold only some of the checks.
|
|
259
|
+
|
|
260
|
+
A failure is new when:
|
|
261
|
+
|
|
262
|
+
- for a check with [diagnostics](#diagnostics): no diagnostic of the same check in the previous run has the same file, rule, and message. The line and column do not count, so code that only moved is not new. The cost is that a second copy of a known error, in the same file with the same rule and message, is not new either.
|
|
263
|
+
- for a failed or timed-out check without diagnostics: the check did not fail or time out in the previous run.
|
|
264
|
+
- for a check with no previous run at all: always. The output says so in one line: `no earlier run to compare with, so every failure counts as new`, or `no earlier result for lint, so its failures count as new` when only some checks have none.
|
|
265
|
+
|
|
266
|
+
The output lists each check with a new failure, with only its new diagnostics, and each check that proved nothing yet (`pending`, `running`, `cancelled`, or `unknown`). Passed checks, skipped checks, and failures that were already there are counts on the `not new:` line. A skipped check is judged through the `after` check whose failure skipped it.
|
|
267
|
+
|
|
268
|
+
`--new` works with `--wait`, and the gate use waits for every check: `firstlight results --new --wait`. It exits 1 when there is a new failure, or when a check is still `pending` or `running`, was `cancelled`, or is `unknown`, because those checks prove nothing. Otherwise it exits 0, also when the run has failures that were already there.
|
|
269
|
+
|
|
270
|
+
With `--json`, every check object of `results --json` comes, with two more fields: `new` (`true` when the check has a new failure) and `baseline` (the id of the run it was compared with, or `null`). A failed check also has `old_diagnostics`, the number of its diagnostics that were already there. Only the new diagnostics follow a check, and `more_diagnostics` counts the new ones past 20. The summary has four more fields: `new` (the checks with a new failure), `new_failures` and `old_failures` (counts, where a failure is one diagnostic, or one failed check without diagnostics), and `no_baseline` (the checks with no previous run).
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{"type":"check","name":"typecheck","status":"failed","exit_code":2,"elapsed_ms":1412,"log":"/.../typecheck.log","eta_ms":0,"overdue":false,"cached_from":null,"new":true,"baseline":"01K...","old_diagnostics":1,"tail":["..."]}
|
|
274
|
+
{"type":"diagnostic","check":"typecheck","file":"src/a.ts","line":4,"col":26,"rule":"TS2304","message":"Cannot find name 'missing'."}
|
|
275
|
+
{"type":"summary","run":"01K...","failed":["typecheck"],"running":[],"passed":[],"cancelled":[],"next":"firstlight run --failed","eta_ms":0,"new":["typecheck"],"new_failures":1,"old_failures":1,"no_baseline":[]}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Diagnostics
|
|
279
|
+
|
|
280
|
+
When a check fails or times out, firstlight reads its log with the check's parser, if it has one, and prints the problems the tool reported in one shape, before the log tail:
|
|
281
|
+
|
|
282
|
+
```text
|
|
283
|
+
typecheck failed (exit 2, 1.4s)
|
|
284
|
+
src/a.ts:1:14 TS2322 Type 'number' is not assignable to type 'string'.
|
|
285
|
+
src/a.ts:2:26 TS2304 Cannot find name 'missing'.
|
|
286
|
+
log: /.../typecheck.log
|
|
287
|
+
> app@1.0.0 typecheck
|
|
288
|
+
> tsc --noEmit
|
|
289
|
+
...
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Each line is `file:line:col rule message`. A part the tool does not report is left out. With diagnostics, the log tail is 10 lines instead of 40, because the diagnostics carry the substance. A check shows at most 20 diagnostics, then `(N more diagnostics in the log)`. Without a parser, or when the parser finds nothing, the output is the log tail as before.
|
|
293
|
+
|
|
294
|
+
| Parser | Reads | `rule` |
|
|
295
|
+
| --- | --- | --- |
|
|
296
|
+
| `tsc` | `path(line,col): error TSxxxx: message`, and the `--pretty` form `path:line:col - error TSxxxx: message` | the `TSxxxx` code |
|
|
297
|
+
| `vitest` | the default reporter: each `FAIL` block, its error, and the first stack frame inside the repository | the test's full name, `suite > test`; none for a file that failed to load |
|
|
298
|
+
| `node-test` | `node --test` with the spec reporter (its `failing tests` section) or the TAP reporter; the first stack frame inside the repository | the test's full name, `suite > test` |
|
|
299
|
+
| `archstrict` | the text of `archstrict check`: `[rule] path:line:col` and the evidence line | the rule id |
|
|
300
|
+
|
|
301
|
+
The message is the first line of the error. A line that ends in `:` takes the next line too, so `Expected values to be strictly equal: 7 !== 6` stays whole. Paths inside the repository are relative to its root. Colors and CRLF line endings make no difference.
|
|
302
|
+
|
|
303
|
+
A check's `parser` in `firstlight.json` chooses the parser. Without one, firstlight infers it from the `run` command when the command is simple commands joined by `&&`: `tsc`, `vitest`, `node --test`, or `archstrict`, also through `npx`, `pnpm exec`, a `node_modules/.bin/` path, or a leading `NAME=value`. For `npm run <script>` and `npm test`, it reads the script's own command in `package.json`, one level deep. An `&&` list, like `tsc --noEmit && vitest run`, gets the parser of each command that has one, in order, and its diagnostics are what each parser finds; a parser skips the lines of other tools. A command with `||`, `;`, or a pipe gets no parser, because which tool's output the log ends with is not fixed. firstlight decides the parser when the run starts, so `results` reads the log the same way later. A parser reads text, so a command that prints JSON, such as `archstrict check --json`, gives no diagnostics.
|
|
304
|
+
|
|
305
|
+
In `--json`, the diagnostics follow their check's object, one line each:
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{"type":"check","name":"typecheck","status":"failed","exit_code":2,"elapsed_ms":1412,"log":"/.../typecheck.log","eta_ms":0,"overdue":false,"cached_from":null,"tail":["> app@1.0.0 typecheck","> tsc --noEmit","..."]}
|
|
309
|
+
{"type":"diagnostic","check":"typecheck","file":"src/a.ts","line":1,"col":14,"rule":"TS2322","message":"Type 'number' is not assignable to type 'string'."}
|
|
310
|
+
{"type":"diagnostic","check":"typecheck","file":"src/a.ts","line":2,"col":26,"rule":"TS2304","message":"Cannot find name 'missing'."}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
`line`, `col`, and `rule` are `null` when the tool has none. When a check has more than 20 diagnostics, its object has `more_diagnostics`, the number left out.
|
|
314
|
+
|
|
315
|
+
### depug evidence and rerun
|
|
316
|
+
|
|
317
|
+
When the vitest run uses [depug](https://www.npmjs.com/package/@meganemura/depug)'s reporter, depug prints an evidence file and a rerun command for each failed test. firstlight attaches both to that test's diagnostic, as two lines under it:
|
|
318
|
+
|
|
319
|
+
```text
|
|
320
|
+
test failed (exit 1, 3.2s)
|
|
321
|
+
test/names.test.ts:6:10 quotes a word with a space AssertionError: Expected values to be strictly equal: "'a b'" !== 'a b'
|
|
322
|
+
evidence: /.../tmp/depug/run-20261012-101024-1110/001-quotes-a-word-with-a-space.json
|
|
323
|
+
rerun: npx depug rerun -- npx vitest run "test/names.test.ts" -t "^quotes a word with a space$"
|
|
324
|
+
log: /.../test.log
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
In `--json`, the diagnostic line gets `evidence` (the absolute path) and `rerun` (the command) as two more fields. A diagnostic without them has neither field.
|
|
328
|
+
|
|
329
|
+
firstlight matches by the test file and the full test name in depug's rerun command, against the file and the name in vitest's `FAIL` header. vitest joins the names with ` > ` and depug with a space, so two tests can look the same (`a > b c` and `a b > c`). When two failed tests, or two depug failures, have the same file and name, or when depug printed nothing for a test, firstlight attaches nothing to it. A log without depug lines gives the same diagnostics as before. This applies everywhere diagnostics show: `run`, `results`, `results --new`, and `follow`.
|
|
330
|
+
|
|
331
|
+
## Reused passes
|
|
332
|
+
|
|
333
|
+
When a check already passed in a kept run on the same working tree, with the same command, `firstlight run` does not run it again. It reports the earlier pass:
|
|
334
|
+
|
|
335
|
+
```text
|
|
336
|
+
test passed (cached from run 01K..., 10.4s originally)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
If every selected check is reused, `run` starts no check and exits 0 at once: `passed 4 checks in 0.2s (4 cached, run 01K...)`. A check that is `after` a reused check starts right away. This saves the slow checks when an agent runs again after changing nothing, or after a failure in a different check.
|
|
340
|
+
|
|
341
|
+
firstlight compares a cache key. The key of a check is the SHA-256 of:
|
|
342
|
+
|
|
343
|
+
- the tree key: the git tree hash of the working tree, tracked and untracked files, without ignored files. firstlight computes it with `git add -A` and `git write-tree` against a temporary index of its own, so the real index, the staged changes, and the refs do not change. The blobs of changed files go into the object store as loose objects, which `git gc` removes later like any other unreferenced object;
|
|
344
|
+
- the check's `run` command;
|
|
345
|
+
- the keys of its `after` checks.
|
|
346
|
+
|
|
347
|
+
The name of a check and its `timeoutSec` are not part of the key. `firstlight.json` is part of the tree unless it is ignored, so an edit to it runs every check again.
|
|
348
|
+
|
|
349
|
+
Only a `passed` result is reused. A `failed`, `timed_out`, `cancelled`, `skipped`, or `unknown` check runs again. firstlight finds a pass in the kept runs, newest first, so a pruned run stops being a source. A pass that was itself reused names the run that really ran the check, with that run's time, so a chain of unchanged runs keeps one origin.
|
|
350
|
+
|
|
351
|
+
The limits:
|
|
352
|
+
|
|
353
|
+
- Environment variables are not part of the key. A check whose result depends on one gives the earlier result after that variable changes.
|
|
354
|
+
- Files outside the repository, ignored files (for example `node_modules/`), the installed tools, and the state of a network service are not part of the key.
|
|
355
|
+
- The tree is hashed once, when the run starts. An edit made while a check runs is not in the key of that run.
|
|
356
|
+
- A directory that is not the root of a git repository has no tree key, and nothing is reused there. A git failure has the same effect.
|
|
357
|
+
|
|
358
|
+
In these cases, run `firstlight run --no-cache`. It runs every selected check, and still records the keys, so a later run can reuse its passes.
|
|
359
|
+
|
|
360
|
+
## CPU contention
|
|
361
|
+
|
|
362
|
+
firstlight starts every check at once and has no concurrency limit. Test runners such as vitest and `node --test` already spread one check over every core, so checks that run together share the CPU: each one takes longer than it does alone, and the whole run takes at least the checks' total CPU time divided by the number of cores. Measured on a 4-CPU machine, with Node 24.21.0, three runs each, times in seconds:
|
|
363
|
+
|
|
364
|
+
| Repository | Checks | Each alone (sum) | Together | Slowest check alone, together |
|
|
365
|
+
| --- | --- | --- | --- | --- |
|
|
366
|
+
| firstlight | typecheck, test, archstrict | 35.1 | 34.7 | test 30.9, 34.2 |
|
|
367
|
+
| finderscope | typecheck, test, archstrict | 62.8 | 59.4 | test 58.4, 58.9 |
|
|
368
|
+
| polylinedb | typecheck, test, check:architecture | 197.8 | 178.9 | test 132.0, 178.4 |
|
|
369
|
+
|
|
370
|
+
In polylinedb, test used about 2.9 cores and check:architecture about 3.8 while each ran alone, so together they need about 160 seconds of the 4 cores at best; 179 is close to that, and 20 seconds less than one after the other. A limit on how many checks run at once would not make the run shorter, because the total CPU time is the same; it would only move time from one check to another. The failure that arrives first still arrives first: typecheck finished in 3 to 6 seconds in every run, against about 2 alone. (finderscope's test had the same two failures on that machine in every run, alone and together.)
|
|
371
|
+
|
|
372
|
+
To see one heavy check's result sooner at the cost of a longer run, start another heavy check after it with `after`. In polylinedb, putting check:architecture after test would let test report at about 132 seconds instead of 178, and the run would end at about 196 instead of 179.
|
|
373
|
+
|
|
374
|
+
## Statuses and output
|
|
375
|
+
|
|
376
|
+
A check is `pending`, `running`, `passed`, `failed`, `timed_out`, `cancelled`, or `skipped`. A check is `unknown`, with `orphan: true`, when its supervisor process is gone before the check finished. firstlight does not invent an exit code for it. A check killed by a signal has the exit code 128 plus the signal number.
|
|
377
|
+
|
|
378
|
+
The default output is short plain text with no colors. `--json` prints JSON Lines: one object per check, then one summary:
|
|
379
|
+
|
|
380
|
+
```json
|
|
381
|
+
{"type":"check","name":"lint","status":"failed","exit_code":1,"elapsed_ms":205,"log":"/.../lint.log","eta_ms":0,"overdue":false,"cached_from":null,"tail":["src/a.ts:3:1 unused variable x"]}
|
|
382
|
+
{"type":"check","name":"test","status":"running","exit_code":null,"elapsed_ms":null,"log":"/.../test.log","eta_ms":8012,"overdue":false,"cached_from":null}
|
|
383
|
+
{"type":"check","name":"build","status":"passed","exit_code":0,"elapsed_ms":10412,"log":"/.../build.log","eta_ms":0,"overdue":false,"cached_from":"01K..."}
|
|
384
|
+
{"type":"summary","run":"01K...","failed":["lint"],"running":["test"],"passed":["build"],"cancelled":[],"next":"firstlight results --wait","eta_ms":8012}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
`failed` in the summary holds the checks that are `failed`, `timed_out`, `skipped`, or `unknown`. `running` holds `pending` and `running` checks. `next` is the command to run next, or `null`: `firstlight results --wait` while a check is still running, and `firstlight run --failed` once every check is done and one did not pass. `eta_ms` on a check is the estimated milliseconds it has left: `0` for a finished check, and `null` when there is no estimate. `overdue` is `true` when a running check has run longer than its usual duration; its `eta_ms` is then `0`. `cached_from` is the id of the run that ran a reused check, or `null`. That run may be pruned since. For a reused check, `exit_code` and `elapsed_ms` come from that run, and its log holds one line that names it. `eta_ms` in the summary is the estimate for the whole run, `0` when nothing is left to run, or `null`.
|
|
388
|
+
|
|
389
|
+
## Exit codes
|
|
390
|
+
|
|
391
|
+
| Command | 0 | 1 |
|
|
392
|
+
| --- | --- | --- |
|
|
393
|
+
| `run` | every check passed | any check did not pass |
|
|
394
|
+
| `results`, `follow` | no check is `failed`, `timed_out`, `skipped`, `cancelled`, or `unknown` | at least one is |
|
|
395
|
+
| `results --new` | no failure is new, and no check is `pending`, `running`, `cancelled`, or `unknown` | a failure is new, or a check proved nothing yet |
|
|
396
|
+
| `stop` | the run stopped, or nothing was running | firstlight itself failed |
|
|
397
|
+
|
|
398
|
+
When firstlight itself fails (bad config, unknown command, no run yet), it exits 1 and prints one JSON object on stderr:
|
|
399
|
+
|
|
400
|
+
```json
|
|
401
|
+
{"error":"invalid firstlight.json: check \"b\" is after unknown check \"nope\"","do":"fix /repo/firstlight.json, then rerun the command"}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Do what `do` says.
|
|
405
|
+
|
|
406
|
+
## State
|
|
407
|
+
|
|
408
|
+
firstlight never writes into the repository it checks. State lives under:
|
|
409
|
+
|
|
410
|
+
```text
|
|
411
|
+
${XDG_STATE_HOME:-~/.local/state}/firstlight/<repo-key>/
|
|
412
|
+
current the latest run id
|
|
413
|
+
runs/<run-id>/run.json the run: root, supervisor pid, start time, checks,
|
|
414
|
+
cache keys, and the passes it reused
|
|
415
|
+
runs/<run-id>/events.jsonl every status change, appended by the supervisor
|
|
416
|
+
runs/<run-id>/<check>.log stdout and stderr of one check
|
|
417
|
+
runs/<run-id>/supervisor.log stderr of the supervisor itself
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
The log file name of a check is its name with each character outside `[A-Za-z0-9._-]` replaced by `_`, cut to 100 characters: `lint:js` writes `lint_js.log`, and `bundle exec rspec` writes `bundle_exec_rspec.log`. The name in the output and in `--json` stays as written. When two checks of one run would share a file name, ignoring case, or a check would take `supervisor.log`, one of them gets `-2`, `-3`, and so on; a name that is already a safe file name keeps its own file. The `log` field in `--json` always gives the real path.
|
|
421
|
+
|
|
422
|
+
`<repo-key>` is the first 16 hex characters of the SHA-256 of the repository root path. Run ids sort by start time. firstlight keeps the newest 20 runs.
|
|
423
|
+
|
|
424
|
+
Each run has one detached supervisor process. It records the reused passes, starts the other checks, waits on them, enforces timeouts, and writes `events.jsonl`. There is no long-lived daemon. Each check runs in its own process group, so a stop or a timeout reaches every process the check started.
|
|
425
|
+
|
|
426
|
+
## Non-goals for v0.1
|
|
427
|
+
|
|
428
|
+
- Diagnostics come only from the four parsers above. Any other tool shows its log tail as it is.
|
|
429
|
+
- No cache beyond reused passes. Earlier runs feed the time-left estimate, the passes a run reuses, and the comparison of `results --new`, and nothing else.
|
|
430
|
+
- No daemon, no queue, and no watch mode.
|
|
431
|
+
|
|
432
|
+
## Development
|
|
433
|
+
|
|
434
|
+
```sh
|
|
435
|
+
npm ci
|
|
436
|
+
npm test # vitest, with Hegel property tests
|
|
437
|
+
npm run typecheck # tsc --noEmit; Node runs the sources directly
|
|
438
|
+
npm run archstrict # module boundaries and the layer order
|
|
439
|
+
npm run mutation # Stryker, with the agent reporter
|
|
440
|
+
npm run build # tsc to dist/, the JavaScript the package ships
|
|
441
|
+
npm pack --dry-run # the tarball's file list; prepack runs the build
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
`test/pack.test.ts` packs the package, installs the tarball into a temporary project, and runs the installed `firstlight` bin, so `npm test` covers the published form too. Releases follow [docs/releasing.md](docs/releasing.md).
|
|
445
|
+
|
|
446
|
+
A failed test prints a depug evidence file and the command that reruns that one test.
|
|
447
|
+
|
|
448
|
+
firstlight checks itself. `firstlight.json` in this repository runs `typecheck`, `test`, `archstrict`, and a `mutation` run scoped to `src/checks.ts` and `src/config.ts`, all in parallel:
|
|
449
|
+
|
|
450
|
+
```sh
|
|
451
|
+
node src/cli.ts run
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## License
|
|
455
|
+
|
|
456
|
+
MIT
|