Lights-out manufacturing: a factory so reliable it runs with the lights off.
lightsout is a deterministic engine for coding agents. It does not make your agent smarter — it makes your agent accountable: mechanical gates, typed contracts, resumable run state, and a supervisor for the exception path. Hand it a plan; it drives your own installed coding agent (Claude Code or Codex) through implement → test → refactor with real verification between every step, and leaves a truthful audit trail on disk.
Status: pre-alpha. Design and decision log: docs/architecture.md.
Frontier models already write good code on the median run. What breaks unattended work is the bad run — and everything here exists for that run:
- Verification can't be sweet-talked. Agents report; subprocesses decide. Every gate is your own repo's commands and an exit code the model can't influence. An agent claiming "tests pass" counts for nothing until the engine has run them.
- Failures are honest and specific. Agent output is validated against
typed contracts at every boundary. A failed run tells you exactly what's
missing and why — in one live run, the executor reported the precise
deliverable it couldn't produce and the exact command that would produce
it, which became config (
agentCommands) instead of a mystery. - Everything leaves evidence. Every gate command lands in
commands.jsonlwith exit code and duration; agent output that fails its contract is persisted verbatim; the manifest snapshots the config that produced the run. A green gate that left no trace is indistinguishable from one that never ran — so no gate runs without a trace. - Crashes, rate limits, and flakes are states, not disasters. Run state
lives on disk, never in a context window: kill the process, hit your
subscription window, catch a flaky test-worker crash —
resumere-enters at the exact step. Red gates get one mechanical re-run before the verdict, because one flaky failure is evidence of nothing. - Changed-file truth is double-entry. What agents report is merged with what git actually observed; work an agent forgot to mention still gets tests, review, and gates.
- Your harness, your subscription, zero credentials. The engine drives the coding-agent CLI you're already logged into. No API keys, no third-party auth, nothing to leak.
- It improves from its own runs. Agents report friction — where the plan, prompts, standards, or environment fought them — and the aggregate drives prompt and standards fixes. The first consumer's phase-one plan surfaced five engine improvements before it shipped.
What lightsout deliberately is not: a smarter agent, a prompt library, or an orchestrator persona. Scaffolding that constrains the model depreciates with every model release; scaffolding that verifies it appreciates.
lightsout implement --plan plans/feature.md
clean-slate gate repo must be green before any agent runs
implement feature-executor agent works from your plan
verify your check + test commands; exit codes, not claims
write-tests one test-writer agent per changed source file, 5 in parallel
verify … + coverage gate
refactor scan gate feeds deterministic findings on the changed files
(baseline-suppressed) to the refactor agent; loops (≤3 passes)
until a pass changes nothing AND the scanner is clean —
surviving gating findings escalate
verify … + coverage gate
format your formatter runs once; gates re-verify after
→ verified diff in your worktree + manifest in .lightsout/runs/<id>/
Changed files flow step to step through the run manifest: after every work step the agent's typed report is merged with a git snapshot of the worktree (agents report what they changed; git reports what actually changed), and the merged list is what the next role receives. An implement step that changes nothing fails instead of passing vacuously. Only JS/TS-family files earn agent turns (test writers, refactor review) — everything else is still tracked and gated, but never costs a model call.
Every gate command the engine runs is logged to
.lightsout/runs/<id>/commands.jsonl — step, group, command, exit code,
duration, plus an output tail on failure — so passing gates leave evidence
too. The CLI prints the fully resolved config at launch and streams progress
live (steps, gate results, agent reports, elapsed time), and the manifest
snapshots the config permanently so every run records which settings
produced it.
Agents are watchable while they work: each invocation's harness event
stream — every tool call, every chat message, the final result — is teed to
.lightsout/runs/<id>/agents/stream-*.jsonl, the full conversation as
on-disk evidence. tail -f it when you want the live play-by-play; the
terminal itself stays on the signal (steps, gates, usage) rather than
echoing hundreds of tool calls. Watching is read-only by design:
course-correction belongs to gates, the supervisor, and escalation — never
to a human whispering mid-step.
Runs also account for what they spend: every agent invocation's token usage
and cost land in the run's agents.jsonl with step provenance, each
invocation is narrated as it completes (implement · usage: in 12 · out 41.2k · cache-read 890k · $0.85), and the manifest carries the run-wide
aggregate. Drivers that report no usage simply leave no ledger — the engine
records what it can prove, never estimates.
Every run ends with a report card computed from that evidence: wall time and gate time, token totals with the cache-read share (the cost-efficiency dial), API-equivalent cost, and a per-step table — tries, time, agent invocations, output tokens, cost, files changed — followed by gate flake re-runs, skipped gates, re-emitted reports, and friction counts by area. Ten runs in, this is the table that tells you which step earns its cost.
One run at a time per repo: implement and resume take a lock
(.lightsout/lock.json) before touching anything, so a second concurrent
invocation fails fast instead of fighting the first over the worktree. A lock
left by a crashed process is detected by pid and stolen automatically, and
lightsout status flags a running run with no live process behind it as
crashed-but-resumable.
Failures retry mechanically, then a read-only supervisor agent decides:
retry with guidance, or escalate to you. Hitting your subscription's rate
limit parks the run (paused-rate-limit) — lightsout resume continues it
from the exact step it stopped at.
- Node 20+
- A logged-in coding agent CLI: Claude Code
(
claude) and/or Codex (codex). lightsout never handles credentials or API keys — it drives your installed CLI, and usage bills to the subscription that CLI is already logged into.
No npm. The committed bundle in the repo is the tool:
git clone git@github.com:dc-devs/lightsout.git
alias lightsout="node $(pwd)/lightsout/plugin/dist/cli.mjs"fixtures/toy-calc is a tiny consumer repo with one unimplemented plan:
cd lightsout/fixtures/toy-calc
node ../../plugin/dist/cli.mjs implement --plan plans/power.md --cwd .Watch the pipeline run (an agent implements power, another writes its tests,
gates verify each stage), then inspect src/, test/, and
.lightsout/runs/<id>/manifest.json. Reset the fixture with
git checkout -- fixtures/toy-calc to run it again.
Add lightsout.config.json at the repo root:
{
"scripts": {
"check": "pnpm typecheck",
"testUnit": "pnpm test",
"testCoverage": "pnpm test:coverage"
}
}That's a complete config: the engine's bundled JS/TS standards load by
default (base docs always; React/TanStack docs join automatically when the
run's packages use those frameworks). Add a standards array only to bring
your own docs — include the token lightsout:code-defaults to stack the
bundled ones alongside them.
| Field | Required | Purpose |
|---|---|---|
scripts.check |
yes | Type/lint gate — full shell command, run per verify step |
scripts.testUnit |
yes | Test gate — full shell command. Runs in gate sets without a coverage run (e.g. the post-implement verify, where new code has no tests yet). |
scripts.testCoverage |
yes | Coverage gate — a full shell command (your command owns the threshold), or the literal false to opt out. On by default: silence is not accepted, skipping the strongest gate must be a decision. Runs at clean-slate and every verify after tests exist, and replaces testUnit in those gate sets — the command must run the unit tests (every mainstream runner's coverage mode does), so the same suites never run twice back-to-back. |
scripts.generate |
no | Opt-in codegen (e.g. prisma generate), run once before every gate set — gates verify, generate mutates, and parallel package gates must never race a generator. Red exit fails the gate set. |
scripts.build |
no | Opt-in build gate, run last in every verify step |
scripts.format |
no | Opt-in formatter, run once at the very end of the pipeline; gates re-verify afterwards |
generated |
no | Path prefixes of generated output (e.g. a Prisma client dir). Real files in your diff, but excluded from changed-file attribution — they never earn agent turns; the source that generates them is the change. |
agentCommands |
no | Command prefixes the implementing agent may run (prefix match, arguments allowed) — for deliverables only a command can produce, e.g. "pnpm --filter api run prisma:migrate:dev:name". Injected into the executor's task as an explicit grant list and relayed to the harness's allowed-tools mechanism. Agents may never verify with these — the engine runs all gates itself. |
packageScripts |
no | Monorepo mode — see below |
packagesDir |
no | Workspace packages directory for monorepo mode (default packages) |
plansDir |
no | Where the planning phase writes the committed plan.md deliverable (default .claude/plans). Resolved flag (--plans) → config → default, then absolutized. Transient planning workspace state (facts.json, decisions.json, grade.json) always lives under gitignored .lightsout/plans/<name>/. |
timeouts.agentMinutes |
no | Ceiling for working agents (executor, test writers, refactorer). Default 60. A hit ceiling is a recorded step failure the run resumes from — never a crash. |
timeouts.supervisorMinutes |
no | Ceiling for the read-only supervisor. Default 15. |
standards |
no | Standards for code-writing agents. Unspecified = the engine's bundled JS/TS defaults load (announced in the run header). false = explicitly none. An array = exactly these: repo-relative markdown files (missing = hard error) and/or the token lightsout:code-defaults to stack the bundled defaults with repo extras. |
testStandards |
no | Same, for the test-writer agent (token: lightsout:test-defaults) |
standardsChannels |
no | Framework channels of the bundled defaults. The base docs always apply; React/Preact and TanStack docs ride along only when the run's scoped packages actually depend on that framework (detected from their package.json — announced in the run log). Set an array to replace detection ([] = base only). A terraform package never pays the React-docs token tax. |
scan.minCloneTokens |
no | Tier-1 clone floor for lightsout scan (default 50) — raise for repos with a noisy short-clone tail |
scan.size |
no | Line-cap overrides for the size detector — defaults { "file": 250, "tsxFile": 300, "function": 80, "hook": 160, "component": 200 }; any subset, e.g. { "tsxFile": 350 }. The same numbers appear in the standards docs, so agents are told the caps the scanner enforces. File caps gate runs; function/hook/component caps go to the refactor agent as judgment items (fix unless a documented exemption applies) and never block. |
driver |
no | claude-code (default) or codex |
model |
no | Model override passed through to the harness |
permissionMode |
no | Harness permission mode for agents (default acceptEdits) |
Everything together — a maximal config (every optional field set):
{
"driver": "claude-code",
"model": "opus",
"permissionMode": "acceptEdits",
"scripts": {
"check": "pnpm typecheck",
"testUnit": "pnpm test",
"testCoverage": "pnpm test:coverage",
"generate": "pnpm prisma:generate",
"build": "pnpm build",
"format": "pnpm format:write"
},
"timeouts": { "agentMinutes": 60, "supervisorMinutes": 15 },
"agentCommands": ["pnpm --filter api run prisma:migrate:dev:name"],
"generated": ["src/generated/", "src/schema.gql"],
"packagesDir": "packages",
"packageScripts": {
"check": "pnpm --filter {package} typecheck",
"testUnit": "pnpm --filter {package} test:unit",
"testCoverage": "pnpm --filter {package} test:coverage",
"build": "pnpm --filter {package} build"
},
"standards": ["lightsout:code-defaults", "docs/our-extra-rules.md"],
"testStandards": ["lightsout:test-defaults"],
"standardsChannels": ["react"],
"scan": {
"minCloneTokens": 70,
"size": { "file": 250, "tsxFile": 300, "function": 80, "hook": 160, "component": 200 }
}
}Whole-repo gates on a monorepo mean an unrelated red package blocks every
run, and the coverage bar applies to the entire repo. packageScripts fixes
both: command templates that run once per affected package, in parallel, with
{package} replaced by that package's package.json name:
{
"packageScripts": {
"check": "pnpm --filter {package} typecheck",
"testUnit": "pnpm --filter {package} test:unit",
"testCoverage": "pnpm --filter {package} test:coverage"
}
}Every packageScripts command must contain {package} — one without it
would run identically for every package and belongs in scripts.* instead
(config validation rejects it).
Not every package in scope has to define every script. When a template's
run <script> names a script a package's package.json doesn't have, that
gate is skipped for that package — announced live
(gate [infra-local] check: skipped (no "check" script)) and recorded in
commands.jsonl with skipped: true, so an infra or docs package pulled
into scope never needs placeholder scripts. A package missing only the
coverage script falls back to its plain test script. Templates the engine
can't read a script name from (no run token) always execute.
The run's package scope resolves through a four-tier chain, so
/implement plan.md needs nothing extra:
-
--packages backend-api,sharedon the CLI — explicit override -
Plan front-matter — precise and authoritative when present:
--- packages: - backend-api --- # Plan: ...
-
Derived from the plan body — concrete
packages/<name>/paths the plan references become the scope (recorded in the manifest and the run report asplan-paths, so a derived scope is never mistaken for a declared one). This is why plans from tools that know nothing about lightsout — plan mode output, hand-written plans — just work. Safe in both directions: a package mentioned only as context merely runs extra gates, and a missed one is caught by scope expansion below. -
Hard error — the plan names no packages at all, which usually means it's too vague to implement anyway.
After the implement step, changed files are the truth: the scope widens
automatically when the agent touches a package the scope missed (never
shrinks). Files outside packagesDir re-activate the whole-repo scripts.*
as a "root group". Tip: use a dependents filter in the templates
(pnpm --filter ...{package}) to also verify packages that depend on the
changed ones — the blast radius lives in your template, not in the engine.
Recommended .gitignore entries: commit the config and standards, not run
state —
.lightsout/runs/
.lightsout/friction.jsonl
.lightsout/lock.json
Then: write a plan (a markdown file stating goal, files, and what's out of
scope) and run it. The repo must be green first — the clean-slate gate refuses
a broken baseline. For phased work, pass the high-level plan with
--overview — it rides along as context while the phase plan stays
authoritative for scope.
| Command | Purpose |
|---|---|
lightsout implement --plan <path> [--overview <path>] [--packages <a,b>] [--cwd <path>] [--skip-refactor] |
Run the pipeline on a plan (optionally with an overview plan as context and a package-scope override) |
lightsout resume --run <id> [--cwd <path>] |
Continue a parked/failed/crashed run from its last incomplete step |
lightsout status [--cwd <path>] |
List runs and their states |
lightsout doctor [--cwd <path>] |
Read-only audit of the repo against every assumption the engine and standards make — config validity, harness binary, gitignored run state, scoped-gate script coverage, Jest mock-cleanup flags, generated paths, gate binaries. Each warn prints the exact fix; the doctor never edits anything (repo-wide changes like clearMocks: true are yours to apply and verify). Exit 1 only on a hard fail. |
lightsout scan [--cwd <path>] [--path <subdir>] [--all] [--baseline] |
Read-only structural detector suite: duplicate export names (synonym-aware; conversion opposites like hexToRgb/rgbToHex and component+route pairs are exempt), token-level clones (jscpd; floor tunable via config scan.minCloneTokens, default 50), functions with identical bodies after identifier normalization (uses the repo's own TypeScript — resolved from the root or any workspace package), size thresholds from the standards' numeric tables, one-export-per-file/structure lint (framework dot-suffixes like .model/.dto count as matching filenames), dead-export candidates. Test files are exempt from duplication tiers — assertion literals are contract-pinning, not copy-paste. --baseline writes lightsout.scan-baseline.json at the repo root — the explicit act of accepting the current findings as existing debt; commit it (it's the reviewable debt ledger, like phpstan-baseline.neon). With a baseline present, scans report only NEW findings; --all shows everything, --baseline again refreshes the ledger. Plain scans never write it. Typed findings persist to .lightsout/scan.json (the future remediation pipeline's work-list). Always exits 0 — it reports, gates decide. |
lightsout plan verify-facts --name <n> [--cwd <path>] |
Planning phase, stage 1: the conducting session explores in-context and authors .lightsout/plans/<n>/facts.json ({ request, areas }); this subcommand — no agent — deterministically verifies every claimed path/script on disk and stamps the verification block into it. Usually driven by the /plan skill, not run by hand. |
lightsout plan draft --name <n> [--scope single|phased] [--plans <dir>] [--cwd <path>] |
Planning stage 2: a plan-writer agent authors plan.md from the verified facts + the session's decisions.json; a code structural-lint loop re-drafts until the plan is structurally clean. Writes to plansDir (committed plan output). |
lightsout plan grade --name <n> [--plans <dir>] [--cwd <path>] |
Planning stage 3: read-only detector — deterministic structural re-check + a gap-check agent → .lightsout/plans/<n>/grade.json with a typed passed verdict (A only when structure is clean AND no decision-gaps remain). The /plan skill reads this to converge; the grade is advisory to /implement. |
lightsout friction [--cwd <path>] |
Show accumulated friction reports from agents |
lightsout improve --engine <lightsout-repo> [--cwd <path>] |
Run the self-improvement loop (below) |
Exit code 0 = run passed. Non-zero with state paused-rate-limit or
escalated in the output means the run is waiting for the rate window or for
you — not broken.
Every agent reports friction — moments the plan, its instructions, the
standards, or the environment fought it — and decisions — choices it had to
make where the input was silent — even on successful runs. Both accumulate in
.lightsout/friction.jsonl (a recurring decision means something upstream
should have settled it). lightsout improve feeds the
aggregate plus the agent prompt files to a maintainer agent that turns
systemic patterns into minimal prompt edits in the engine's worktree.
The loop proposes; a human reviews the diff and ships.
The repo doubles as a plugin whose /implement skill is the ignition for the
bundled engine (no logic in the skill — all of it lives in the engine):
/plugin marketplace add dc-devs/lightsout
The plugin flow has not been exercised end-to-end yet — the CLI is the proven path today.
pnpm install
pnpm check # typecheck all packages
pnpm test # engine test suite (node:test, stub drivers only — no agent calls, no network)
pnpm bundle # rebuild plugin/dist/cli.mjs — the bundle is COMMITTED; rebuild + commit with any source changeTests live in packages/engine/tests/, are bundled by esbuild (the engine
imports agent prompts as markdown text, which plain node --test cannot
load), and run against real temp git repos with stubbed drivers.
Conventions and settled decisions: CLAUDE.md and the decision log in docs/architecture.md.