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-report

Live 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.

●lit = 0
exit code 0, or state: "lit", or GitHub conclusion success
○dark = 1
anything else: nonzero exit, cancelled, timeout, or no report in time

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.

How to report →

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.

How to run →

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/repo

You 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 --github

Reads 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.sh

The 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.

envmeaning
LIGHT_REPORT_URLserver (default https://light-report-production.up.railway.app)
LIGHT_TOKEN_<NAME> or LIGHT_TOKENwrite token for the light (name upper-cased, other characters become _)
LIGHT_SOURCElabel 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.sh

curl (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 variables

Health 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

  1. Make sure the repo is public on GitHub and its tests can run from a clean clone.
  2. Optional: add a AI agentslight.yml to say exactly what to run. Otherwise the runner detects it.
  3. 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 dark

It 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'

Claude Code

  1. Run light-check init. It adds a Stop hook to .claude/settings.json and a note to CLAUDE.md:
{
  "hooks": {
    "Stop": [
      { "hooks": [ { "type": "command",
        "command": "npx -y -p github:fitzyracing1/light-report light-check --hook claude-stop",
        "timeout": 900 } ] }
    ]
  }
}
  1. 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.
  2. To avoid an endless loop it lets Claude stop after 8 dark rounds in one session, with a note for you.
  3. Optional: light-check init --post-edit also adds a PostToolUse hook on Edit|Write|MultiEdit that 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.
  4. Check it with /hooks inside Claude Code.

VS Code (Copilot agent mode)

  1. light-check init adds a light-check task to .vscode/tasks.json (Terminal → Run Task → light-check; --folder-open runs it whenever the folder opens) and a block in .github/copilot-instructions.md telling Copilot to run light-check . --local before it says it is done, and to keep fixing until it is lit.
  2. Copilot agent mode reads .github/copilot-instructions.md (and AGENTS.md) on its own. Allow it to run terminal commands so it can call the check.

Cursor

  1. light-check init writes .cursor/rules/light-check.mdc (alwaysApply: true) and a block in AGENTS.md with the same instruction.
  2. 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 once

Instruction 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 test

Add a badge to your README

![light](https://light-report-production.up.railway.app/lights/NAME/badge.svg)

For a repo checked by the runner, NAME is owner--repo:

[![light](https://light-report-production.up.railway.app/lights/owner--repo/badge.svg)](https://light-report-production.up.railway.app/lights/owner--repo)

Example: light-report light demo light

API reference

method & pathbody / authgives
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 rulelight 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 limited202 {id, status:"queued", log, …}; 429 over the limit; 503 queue full
POST /runs/uploadgzip'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 name202 {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:

  1. light.yml / light.yaml / .light — your run: lines.
  2. package.json with a test script — npm ci (lockfile) or npm install, then npm test.
  3. 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.
  4. pyproject.toml / setup.py / requirements.txt — fresh venv, pip install requirements and the project, then pytest.
  5. 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.

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

namelightbitsource
○demo-darkdark1light-run@grok-bot-vm-359303082
○demo-litdark1light-run@grok-bot-vm-359303082
○fitzyracing1--light-demo-darkdark1light-report-runner
●fitzyracing1--light-reportlit0light-report-runner
○fitzyracing1--light-report--trial-268c21dark1light-report-runner
○local--dark-folderdark1light-check --local@grok-bot-vm-359303082
○local--demo-repodark1light-check --local@grok-bot-vm-359303082
○local--lit-folderdark1light-check --local@grok-bot-vm-359303082

Full board → · JSON · recent runs

Also: Darks — dark lights found in popular GitHub repos by the dark crawler.