One lit / dark light for any build, test or check.
Report the result of anything that has an exit code — a shell script, a cron job, Jenkins, GitLab, a health check — or let light-report run a public repo's tests for you. Read it back as JSON, a one-word file, or a README badge.
npx -y -p github:fitzyracing1/light-report light-check fitzyracing1/light-reportLive board: 8 lights, 1 lit · source on GitHub (MIT)
What lit and dark mean
A light is one bit. It is active-low, like a hardware signal line: the "everything is fine" state is the zero.
0exit code 0, or
state: "lit", or GitHub conclusion success1anything else: nonzero exit, cancelled, timeout, or no report in time
- Exit code 0 is lit. Any other exit code is dark. There is no third state.
- Stale is dark. If a light gets no report for 24 hours (
LIGHT_TTL_SECONDS), it reads dark with"stale": true. A name nobody ever reported is dark and stale. - GitHub's words are mapped once at the edge (
success→ lit, anything else → dark). After that, only lit and dark. - Eight lights pack into one active-low byte, MSB first:
●●●●○○●●→00001100→12.
The full rule lives in the lit-dark spec (fitzyracing1/lit-dark).
Two ways to use it
1. Report from anywhere
Your build runs wherever it already runs. Wrap the command with light-run, or send one curl with the exit code. light-report keeps the light.
2. Let the server run your repo
Point light-check at a public GitHub repo. light-report clones it, runs its tests, streams the log, and turns the exit code into a light.
30-second quickstart
You need Node 18 or newer. Nothing to install: npx fetches the CLI from GitHub.
Check a repo (server runs its tests)
npx -y -p github:fitzyracing1/light-report light-check owner/repoYou see ◌ running…, the live log, then ● lit or ○ dark. Exit code: 0 for lit, 1 for dark, 2 if the run could not start (that is not a light).
Read a repo's GitHub light instead
npx -y -p github:fitzyracing1/light-report light-check owner/repo --githubReads the newest completed push run on GitHub Actions and maps it once: success → lit, anything else → dark.
Report your own command
npx -y -p github:fitzyracing1/light-report light-run my-backup -- ./backup.shThe first report for a new name prints a write token once. Save it (export LIGHT_TOKEN_MY_BACKUP=lr_…); later reports for that name need it.
Report from anywhere
light-run <name> -- <command> runs your command, reports its exit code, and exits with the same code, so your script or CI step behaves exactly as before.
| env | meaning |
|---|---|
LIGHT_REPORT_URL | server (default https://light-report-production.up.railway.app) |
LIGHT_TOKEN_<NAME> or LIGHT_TOKEN | write token for the light (name upper-cased, other characters become _) |
LIGHT_SOURCE | label shown on the board (default light-run@<hostname>) |
Shell
export LIGHT_TOKEN_DEPLOY=lr_...
npx -y -p github:fitzyracing1/light-report light-run deploy -- ./deploy.shcurl (anything with an exit code)
./deploy.sh; curl -fsS -X POST https://light-report-production.up.railway.app/lights/deploy \
-H "Authorization: Bearer $LIGHT_TOKEN" -H 'Content-Type: application/json' \
-d "{\"exitCode\": $?, \"source\": \"shell\"}"cron
0 3 * * * /opt/backup.sh; curl -fsS -X POST https://light-report-production.up.railway.app/lights/backup -H "Authorization: Bearer lr_..." -H 'Content-Type: application/json' -d "{\"exitCode\": $?, \"source\": \"cron\"}"Jenkins (declarative pipeline)
post {
success { sh 'curl -fsS -X POST $LIGHT_REPORT_URL/lights/$JOB_NAME -H "Authorization: Bearer $LIGHT_TOKEN" -H "Content-Type: application/json" -d \'{"state":"lit","source":"jenkins"}\'' }
unsuccessful { sh 'curl -fsS -X POST $LIGHT_REPORT_URL/lights/$JOB_NAME -H "Authorization: Bearer $LIGHT_TOKEN" -H "Content-Type: application/json" -d \'{"state":"dark","source":"jenkins"}\'' }
}GitLab CI
test:
script:
- npx -y -p github:fitzyracing1/light-report light-run $CI_PROJECT_NAME -- npm test
variables:
LIGHT_REPORT_URL: https://light-report-production.up.railway.app # put LIGHT_TOKEN in masked CI/CD variablesHealth check (every 5 minutes)
*/5 * * * * curl -fsS https://example.com/health >/dev/null; curl -fsS -X POST https://light-report-production.up.railway.app/lights/example-health -H "Authorization: Bearer lr_..." -H 'Content-Type: application/json' -d "{\"exitCode\": $?, \"source\": \"healthcheck\"}"If the checker itself stops, the light goes dark on its own once it is stale. That is the point of the TTL.
Read a light back
curl -s https://light-report-production.up.railway.app/lights/deploy # JSON
curl -s https://light-report-production.up.railway.app/lights/deploy/LIGHT # one word: lit or dark
[ "$(curl -s https://light-report-production.up.railway.app/lights/deploy/LIGHT)" = lit ] && echo "go ahead"Let the server run your repo
- Make sure the repo is public on GitHub and its tests can run from a clean clone.
- Optional: add a AI agentslight.yml to say exactly what to run. Otherwise the runner detects it.
- Run
light-check:
npx -y -p github:fitzyracing1/light-report light-check owner/repo
npx -y -p github:fitzyracing1/light-report light-check owner/repo --ref v1.2.0
npx -y -p github:fitzyracing1/light-report light-check owner/repo --cmd 'npm ci && npm run lint && npm test'The result is saved as a light named owner--repo (the slash becomes two dashes), source light-report-runner, so it shows on the board and as a badge.
Same thing with curl
curl -s -X POST https://light-report-production.up.railway.app/runs -H 'Content-Type: application/json' \
-d '{"repo":"owner/repo","ref":"main","command":"npm test"}'
# {"id":"9f2c...","status":"queued","log":"/runs/9f2c.../log",...}
curl -N https://light-report-production.up.railway.app/runs/9f2c.../log # live log until the run ends
curl -s https://light-report-production.up.railway.app/runs/9f2c... # {"status":"done","light":"lit","bit":0,"exitCode":0,...}For AI agents (Claude Code, VS Code / Copilot, Cursor)
Make your coding agent run the light by itself, in the terminal, before anything goes to GitHub — and keep fixing while it is dark. One command sets it up:
cd your-repo
npx -y -p github:fitzyracing1/light-report light-check init # Claude Code + VS Code + Cursor + AGENTS.md
npx -y -p github:fitzyracing1/light-report light-check init --pre-push # also block git push while darkIt only adds to your files: JSON settings are merged, markdown gets one marked block (<!-- light-check:start -->), and running it again changes nothing. A JSON file with comments is left alone and listed as skipped. Commit what it writes.
Check a local folder
npx -y -p github:fitzyracing1/light-report light-check . --local # run the tests here, nothing uploaded, report the light
npx -y -p github:fitzyracing1/light-report light-check . # upload the working tree and run it on the server
npx -y -p github:fitzyracing1/light-report light-check ../app --name my-app --cmd 'npm test'--localruns the detected command (same detection and light.yml) on your machine and reports the lightlocal--<folder>. Add--no-reportto keep it entirely offline.- Without
--local, the folder is packed and sent toPOST /runs/upload. It respects.gitignoreand always leaves out.git,node_modules,.env*,*.pem,*.key, SSH keys,.npmrc,.netrcand similar. Over 20 MB total is refused. Same runner limits as repo runs. - Write tokens are saved in
~/.config/light-report/tokens.json, so repeat reports just work.
Claude Code
- Run
light-check init. It adds a Stop hook to.claude/settings.jsonand a note toCLAUDE.md:
{
"hooks": {
"Stop": [
{ "hooks": [ { "type": "command",
"command": "npx -y -p github:fitzyracing1/light-report light-check --hook claude-stop",
"timeout": 900 } ] }
]
}
}- When Claude finishes a turn, the hook runs the tests locally. Lit: Claude stops as usual. Dark: the hook answers
{"decision":"block","reason":"…last log lines…"}, which tells Claude Code not to stop, and Claude goes back to fixing with the log in front of it. - To avoid an endless loop it lets Claude stop after 8 dark rounds in one session, with a note for you.
- Optional:
light-check init --post-editalso adds aPostToolUsehook onEdit|Write|MultiEditthat shows Claude the dark log right after an edit (it can't undo the edit). It runs the tests on every edit, so use it only for fast test suites. - Check it with
/hooksinside Claude Code.
VS Code (Copilot agent mode)
light-check initadds a light-check task to.vscode/tasks.json(Terminal → Run Task → light-check;--folder-openruns it whenever the folder opens) and a block in.github/copilot-instructions.mdtelling Copilot to runlight-check . --localbefore it says it is done, and to keep fixing until it is lit.- Copilot agent mode reads
.github/copilot-instructions.md(andAGENTS.md) on its own. Allow it to run terminal commands so it can call the check.
Cursor
light-check initwrites.cursor/rules/light-check.mdc(alwaysApply: true) and a block inAGENTS.mdwith the same instruction.- Cursor's agent picks up the rule in every chat; let it run terminal commands.
git pre-push hook (any editor, any agent)
npx -y -p github:fitzyracing1/light-report light-check init --pre-push
git push # runs light-check . --local first; dark → push blocked
git push --no-verify # skip it onceInstruction files (Copilot, Cursor, AGENTS.md) ask the agent to run the check; the agent can still ignore them. The Claude Code Stop hook and the git pre-push hook actually enforce it.
Add a light.yml
Put light.yml (or light.yaml or .light) in the repo root. One shell command per run: line, run in order from the repo root. The first command that exits nonzero stops the run and the light is dark. # starts a comment; other lines are ignored.
# light.yml
run: npm ci
run: npm run lint
run: npm testAdd a badge to your README
For a repo checked by the runner, NAME is owner--repo:
[](https://light-report-production.up.railway.app/lights/owner--repo)Example:
API reference
| method & path | body / auth | gives |
|---|---|---|
POST /lights/:name | {"exitCode":0} or {"state":"lit"|"dark"}, source, optional detail. Authorization: Bearer <token> after the first report. | 201 + one-time token on first report, 200 after, 401 on wrong token |
POST /lights/:name/github | {"repo":"owner/repo"}, same token rule | light from the newest completed push run (502 if GitHub can't be read) |
GET /lights/:name | — | {name, light, bit, source, detail, reportedAt, stale} |
GET /lights/:name/LIGHT | — | plain text lit or dark |
GET /lights/:name/badge.svg | — | SVG badge |
GET /lights | — | all lights as JSON |
POST /runs | {"repo":"owner/repo","ref"?,"command"?}, no auth, rate limited | 202 {id, status:"queued", log, …}; 429 over the limit; 503 queue full |
POST /runs/upload | gzip'd JSON {"name","command"?,"files":[{"path","mode","data":base64}]}; up to 20 MB of files; same rate limit. Bearer token after the first upload for a name | 202 {id, status, lightName, …} + one-time token for a new name; 401 wrong token; 409 name taken by a repo run |
GET /runs/:id | — | {status, light, bit, exitCode, command, detected, timedOut, error, queuedAt, startedAt, finishedAt, seconds, lightName} |
GET /runs/:id/log | — | chunked plain text, streamed live until the run ends |
GET /runs | — | recent runs (newest first) |
GET /board | — | HTML board of all lights |
GET /health | — | {ok, persistent, ttlSeconds} |
Light names: letters, digits, . _ -, up to 100 characters. Tokens are stored only as SHA-256 hashes.
What the runner runs
If you don't give a command, the runner takes the first match:
- light.yml / light.yaml / .light — your
run:lines. - package.json with a
testscript —npm ci(lockfile) ornpm install, thennpm test. - Cargo.toml —
cargo test, only if a Rust toolchain is installed. The hosted runner has none, so these read dark with the reason in the log. - pyproject.toml / setup.py / requirements.txt — fresh venv,
pip installrequirements and the project, thenpytest. - Makefile with a
test:target —make test.
Nothing found → dark, and the log says why. Toolchains on the hosted runner: Node 22 + npm, Python 3, git, make.
Trials: one stable URL, flip to a lit merge
Try a branch before it ships. light-check trial makes a copy of your repo with the branch merged in (branch trial/<id>), gets it a lit / dark light from the runner, and deploys the copy as its own preview. A small router, light-site, keeps one stable URL pointing at the live copy. Flip it to the trial when the trial is lit; flip back any time — the live copy never stopped, so rollback is instant.
light-check trial owner/repo --merge my-branch # or --merge pr:12
light-check flip repo <trial-id> # refused while the trial is dark (--force overrides)
light-check flip repo live # rollback
light-check site repo # where does the stable URL point?
Router API: GET /sites/:name (public) and POST /sites/:name/flip {"to":"<id>|live"} (admin token). At most 2 trials per site; the oldest one is cleaned up (preview service, branch, router entry). v1 is for the operator's own repos only — it needs Railway and GitHub credentials. Stable URL for this site: light-site-production.up.railway.app. Design: docs/TRIALS.md.
Limits and safety — honestly
The runner is a convenience, not a full sandbox. Only point it at code you would be fine running on a small shared box.
- Public GitHub repos only for repo runs. Clones are anonymous; private repos don't clone. Folder uploads (
light-check .) send your files to this server — use--localfor private code. Refs must be branches or tags (shallow clone), not commit SHAs. - Each job runs as an unprivileged user in a fresh temp directory that is wiped afterwards, with an environment built from scratch: no server variables, Railway settings or tokens reach it, and it can't read the light store.
- Jobs share the container's kernel, network, CPU and memory. There is no network isolation and no per-job memory limit.
- 10-minute hard cap per job; the whole process group is killed and the light is dark.
- One job at a time, up to 10 waiting. 6 runs per hour per IP.
- Logs are capped at 1 MB and kept in memory only for the last 100 runs; a restart forgets them. Lights are persisted.
- Anyone can run any public repo, which writes the
owner--repolight. Running with a custom--cmdalso writes it. Use other names for lights you report yourself.
FAQ
Is this replacing GitHub Actions?
No. light-report is a small runner plus a scoreboard. GitHub Actions is one optional source of lights (light-check --github, POST /lights/:name/github). Use it next to whatever CI you already have — or for the jobs that have no CI at all, like cron scripts and health checks.
Does my code leave my machine with the AI-agent setup?
No. The hooks and instructions use light-check . --local: tests run on your machine and only the light (lit/dark, exit code, command) is reported. Add --no-report to send nothing at all.
Why is lit 0 and dark 1?
Because exit code 0 means "it worked" everywhere in Unix, and the lit-dark spec keeps that: active-low, lit = 0, dark = 1. A missing signal reads as 1 — dark — which is the safe default.
Why is my light dark when nothing ran?
No report within the TTL means stale, and stale means dark. Report again, or raise LIGHT_TTL_SECONDS on your own server.
I lost my write token.
Tokens are shown once and stored hashed, so they can't be recovered. Use a new light name.
Can I run my own server?
Yes. node server.mjs with Node 18+, no dependencies. Set DATA_DIR to persist lights. The repo has a Dockerfile; this instance runs on Railway with a volume at /data.
Does a GitHub API error make a light dark?
No. Per the spec, "could not read GitHub" is an error (HTTP 502 / exit 2), not a dark light.
Live board
| name | light | bit | source | |
|---|---|---|---|---|
| ○ | demo-dark | dark | 1 | light-run@grok-bot-vm-359303082 |
| ○ | demo-lit | dark | 1 | light-run@grok-bot-vm-359303082 |
| ○ | fitzyracing1--light-demo-dark | dark | 1 | light-report-runner |
| ● | fitzyracing1--light-report | lit | 0 | light-report-runner |
| ○ | fitzyracing1--light-report--trial-268c21 | dark | 1 | light-report-runner |
| ○ | local--dark-folder | dark | 1 | light-check --local@grok-bot-vm-359303082 |
| ○ | local--demo-repo | dark | 1 | light-check --local@grok-bot-vm-359303082 |
| ○ | local--lit-folder | dark | 1 | light-check --local@grok-bot-vm-359303082 |
Also: Darks — dark lights found in popular GitHub repos by the dark crawler.