Imported from dc-devs/lightsout (
plugin/skills/plan/SKILL.md). Install upstream withnpx skills add dc-devs/lightsout --skill plan. Copyright stays with the author.
lightsout: plan
This skill is the interactive conductor, not the engine. All determinism —
fact verification, the draft↔structural-lint loop, grading — lives in the
lightsout plan … subcommands as deterministic code. This skill only conducts
the human dialogue the engine cannot (Elicitation, Grill, gap resolution) and
relays typed results. Do not add gates, retries, caps, or contract parsing
here. The one branch you make is reading the typed passed verdict from
grade.json.
Resolve the plugin root once from this loaded skill's absolute path: it is two
directories above this SKILL.md. In Claude Code, ${CLAUDE_PLUGIN_ROOT} may
provide the same path; do not assume that variable exists in Codex skill shell
calls. Use the resolved absolute path wherever <plugin-root> appears below.
Confirm <plugin-root>/dist/cli.mjs exists; otherwise stop and tell the user to
reinstall the plugin or run pnpm bundle.
Question format
Pick the shape from what the answer is. Before writing the question, ask: does answering it mean inventing a name or a short phrase the code or the user will see — a value, a state, a field, a flag, a message? Two or more of them: draft the real names as a table under Options, one row each, first column the name, second column a short description of what that thing is. Exactly one: write the drafted wording out inline, in full, rather than describing it. Any other question stays prose. The labeled parts below apply either way — this test only decides whether the names get written down or talked about, and the 1–3 sentence target counts sentences, not table rows.
Every question this skill puts to the user — Elicitation batches, Grill escalations, Dedup findings, Converge gaps — uses this labeled four-part shape, in this order:
Context: what the question is about and why it matters, in everyday words. Write for someone who has not read the plan or the code — never assume they know the plan's internals. State the problem the question decides — in everyday words — before naming any options.
Question: the question itself, one sentence.
Options: the answers to choose between, one per line, each opening with a
bracketed number and its name — (1) <name>: … — then what it wins and what it
costs. The number is there so the user can reply with the digit alone; the name
is what makes the list readable to someone who skipped the paragraphs above.
When an option carries risk, say what goes wrong if it fails and what catches
it.
Recommendation: the option you recommend, named by its number, and the one-line why — so a reply of just that number resolves it.
Presentation. Each labeled part is its own short paragraph — bold label, blank line between parts. No bullet dashes on the labels; the blank lines are what keep the block readable.
Extra parts are welcome when needed. If something the user must know fits none of the four labels (a safety note, a cost, a deadline effect), add another bold-labeled paragraph rather than forcing it in or leaving it out.
Plain language, always. No jargon. Never use an internal name — a file, symbol, subcommand, or engine term — without saying what it means in everyday words. If the reader would need to open a file to answer, the question is not ready to ask.
A label reads like a well-named variable. Someone who skips straight to
the options knows what each one is from the label alone — nothing borrowed
from the paragraphs above it or from its place in the list. needs-a-human
passes; "Not true" and "the third one" do not. When the question is about
which action to take, name each option by what it does ("copy the file each
run", "keep the first copy"). When the question asks you to invent a name,
the drafted name itself is the label. An internal name never appears in a
label, even one explained earlier in the question — the label names what
the option does in everyday words (checker-per-plan-file, not
fourth-lens).
Keep each part short. Aim for 1–3 plain sentences per label. When a question outgrows that, treat it as a sign it is really two questions — split it.
Durable question delivery. A pending decision is the deliverable for that turn. Put every complete four-part question block in the final response that waits for the user's answer. Never put the full block in commentary and then summarize or repeat only its Question in the final response; commentary may report progress, but must not contain a decision the user needs to answer.
The final response may carry at most 2 full-format questions. Truly trivial yes/no items may share one combined block instead of getting a block each. Grill escalations are stricter: one question at a time, always.
Never put a question to the user through an option-picker tool — the kind that shows a list of one-line choices to select from. Every question in this phase is written out in that final response, in the shape above. A picker's labels cannot carry a Context, an Options list, or a drafted table, so what it saves in typing it takes out of the user's ability to answer.
Settled decisions
Settled means settled — never re-ask it. Before putting any question to the user — an Elicitation batch, an Approaches fork, a Grill escalation, a Dedup finding, a Converge gap — check whether the answer is already on the record. If it is, take the recorded answer, and do not surface the question. The user sat through that decision once; asking again spends their attention on work already done and invites them to contradict themselves.
By the time the plan is drafted, settled decisions live in three places, and all three count:
| Where | Holds |
|---|---|
.lightsout/plans/<name>/brainstorm-decisions.json |
what was settled with the user in the brainstorm before this session |
.lightsout/plans/<name>/decisions.json |
what was settled earlier in this plan session |
the drafted plan's ## Decision Log |
a rendering of the rows of both, composed by the engine — read a settled answer here, never write one |
Record first, refresh, then edit the plan. The engine composes the
## Decision Log from the two record files, so a row written into a plan file
by hand is overwritten the next time it runs. Every decision made after the
draft — a grill answer, a dedup resolution, a converge resolution, a veto — is
appended to decisions.json first, then the log is refreshed with:
node "<plugin-root>/dist/cli.mjs" plan sync-decisions --name <name>
Only then edit the plan content the answer changes — implementation detail,
acceptance rows, constraints. Never write a Decision Log row by hand. The
steps below call this the sync command.
Name the phases a decision concerns. For a phased plan, a row that
resolves a finding carries "phases" naming the finding's phase file, plus
any other phase file the answer changes. A revision row that repeats a
question names the phases its new answer concerns; the engine also covers the
phases the row it replaces named. A decision that names its phases lets the
next re-grade read those phases and the phases connected to them, not the
whole plan. The step 2 glossary says when to leave the field out.
A ticket's ## Decisions lines are the same kind of record: outcomes the
user settled before shaping began. Elicitation harvests them into
decisions.json (step 2), and from then on this section governs them like
any other row. A ticket's acceptance criteria are not decisions — they
are floors, never ceilings, and a criterion's silence about a case decides
nothing.
A settled question is dropped, not answered again. Do not append a
Decision Log row for it and do not mirror one into decisions.json — the
row it would duplicate is already there. Where a step distinguishes answering
a question yourself from putting it to the user, a settled question is
neither; it never enters that routing at all.
Re-open a settled decision only for a contradiction you can name in a
specific file and line. A preference for a different approach is not a
contradiction, and neither is a later step wanting a different answer than an
earlier one gave. A re-opened decision is asked in the Question format, with
the contradicting file:line stated in the Context.
A re-opened decision keeps both records. Record the corrected answer as a
new row in decisions.json, repeating the original row's question
text verbatim, with a rationale naming the file:line and saying which row
it supersedes. The repeated question text is what marks the supersession: the
engine's renderer marks every earlier row sharing that question as superseded
by the later one, so the corrected answer is the binding one while both rows
stay in the log. Run the sync command afterwards. Never edit
brainstorm-decisions.json — brainstorm owns it, and both rows belong in the
log.
The user's latest explicit instruction outranks every record. When what they
say now contradicts a settled row, follow them. Record the new answer as a new
row repeating the original row's question text verbatim, with a rationale
naming the instruction and saying which row it supersedes, then run the sync
command. This sits beside the file:line re-open rule above, not in place of it:
that rule is about what you may re-open on your own, and this one is about what
the user has already decided to change.
On a ticket holding several plans, an earlier plan's records are context, not
this plan's settled rows. Read them for what was built and why, and do not
harvest their rows into this plan's decisions.json. The ticket's own
## Decisions lines still bind every plan of the ticket until they are
explicitly revised. Where this plan's direction conflicts with what an earlier
plan settled, surface it to the user rather than deciding it quietly. A change to
a plan whose implementation is already finished belongs in this plan — never
in an edit to that earlier one.
This narrows what gets asked, not how hard a step pushes. Every question that is not already settled is still asked, at whatever intensity its step calls for. Dropping a settled question is not a licence to drop a hard one.
Steps
0. Name the plan. With a ticket, <name> is the plan's address — the
ticket's branch, a slash and the plan's id. Read what the ticket already holds
first:
node "<plugin-root>/dist/cli.mjs" ticket show --name <ticket-branch>
Then continue the lowest-numbered plan still at planning, unless the user says
this is a separate plan — in which case add one:
node "<plugin-root>/dist/cli.mjs" ticket add-plan --name <ticket-branch> --slug <slug> [--title <title>]
and take the address it prints on its last line. What that command refuses, and
why, is the ticket-workflow skill's ### Adding a plan. Two questions come
first, in the Question format: in single-plan mode with plan 001 already there,
whether to switch the ticket to multiple-plan mode; and on a ticket folder still
holding files from before ticket records, whether to run lightsout ticket adopt. Declining the second is fine — planning then carries on in that folder as
it stands.
With no ticket, derive a kebab <name> from the
request (e.g. "add a rate-limit banner" → rate-limit-banner), and rename the
folder to the ticket's branch when the ticket is filed — see the
ticket-workflow skill's ## Plan folder section for what a rename also has to
update, and when it is too late to do one. When the request is a rough-notes
file path (given by the user, or a /brainstorm handoff), read it before
anything else; when it already lives under the plans directory, take <name>
from the path segments below that directory instead of deriving a new one. Also read
.lightsout/plans/<name>/brainstorm-decisions.json when it exists — its rows
are decisions already settled with the user. Absent → nothing changes; that is
the normal path for a plan that started from a direct request. This read is not
the only one: the file may arrive during step 1, because plan verify-facts
fetches the brainstorm the ticket carries. Step 2's Honor the brainstorm hand-off bullet reads the folder again after that command has run rather than
trusting the answer here.
Once <name> is settled, establish the plan's own worktree as the very first
shell command:
node "<plugin-root>/dist/cli.mjs" plan workspace --name <name>
Read the absolute path it prints on its last line, and do every later step from
that directory — reading source, authoring facts.json, and every
lightsout plan … call. The plan folder already in this checkout is copied
into the tree, so the brainstorm files read above are there too; pass any
rough-notes path as an absolute one, since it lives in the checkout you started
from. A nonzero exit is the end of the session — report the sentence it printed
and stop, never carry on in the launching checkout. The command is safe to
re-run: a session already standing in the tree is answered the same path.
For a plan address, the tree is the ticket branch's, so a later plan of the ticket continues in the tree its earlier plans used and is researched against the implementation already on that branch. While a live implementation run holds that tree, the command refuses and names the run: report its sentence and stop.
Surface any discrepancy between the ticket text and the user's current direction
per the ticket-workflow skill's ## Keeping the body true — and never hold this
session up waiting on a ticket edit.
1. Explore (in-context) + verify. Explore the codebase yourself: read the
files the request touches, follow the integration points, and note real
signatures. For a feature spanning many packages/layers, optionally fan out
read-only Explore subagents for breadth — either way YOU author the facts, and
only from paths you actually confirmed by reading them. Author
.lightsout/plans/<name>/facts.json. Write this exact shape (the engine
hard-parses it):
{
"request": "<the feature request>",
"areas": [
{
"area": "<what this area covers>",
"affectedPackages": ["<repo-relative package dir>"],
"filesToModify": [{ "path": "<repo-relative>", "role": "<one line>" }],
"patternsToMirror": [{ "path": "<repo-relative>", "takeaway": "<what to take>" }],
"integrationPoints": [{ "name": "<symbol>", "signature": "<real signature>", "at": "<file:line>" }],
"scripts": [{ "key": "<package.json script key>", "command": "<what it runs>" }],
"namingConvention": "<one line>"
}
]
}
Then run:
node "<plugin-root>/dist/cli.mjs" plan verify-facts --name <name> [--notes "<path>"]
It also fetches this plan's own brainstorm from the ticket —
brainstorm-notes.md, plus brainstorm-decisions.json when that brainstorm
settled anything, so its absence is ordinary rather than a fault — into
.lightsout/plans/<name>/ before it reads anything, so a fresh worktree has
them without the folder having travelled.
Pass --notes when the request came from a rough-notes file — the engine
freezes a copy at .lightsout/plans/<name>/brainstorm-notes.md as the plan's first
artifact. Write-once: an existing snapshot is never overwritten, so re-running
verify-facts never clobbers it (a /brainstorm-authored brainstorm-notes.md is already
home — whether it was written here or just fetched from the ticket — and is
simply kept).
It deterministically checks every claimed path/script on disk and stamps the
verification into facts.json. Relay the summary; fix any genuinely wrong path
in facts.json and re-run verify-facts, and carry remaining missing-path
warnings into Elicitation.
- While exploring, deliberately check each settled brainstorm decision against
the code you are reading. The value of the hand-off is that the plan trusts
these rows without asking, and trust that is never verified is a guess. Note
any conflict with the exact
file:line.
2. Elicitation — drain the user's conscious knowledge (interactive):
- Scope check first. Before any detail question, judge the request's
size: one plan, one phased plan, or several independent plans. When it is a
genuine fork, ask in the Question format; when the request is several
independent plans, say so, agree which to plan now, and record the split as
a decisions row. This check aims the interview — the engine's
plan draftstill makes the single-versus-phased estimate on its own. - Collect global constraints. Ask once, early, whether any project-wide
rules bind this work (for example "no new dependencies", "the public API
stays frozen"). Record each as its own decisions row whose
questionbegins exactlyGlobal constraint:— the drafted plan's Global Constraints section is built from these rows. None stated → no rows; the section will read "None". Constraints already recorded as brainstorm rows carry their ownGlobal constraint:prefix and flow through untouched — ask only for rules not already settled. - Harvest the session first. If the feature was discussed in this
conversation before the skill was invoked, record each decision the user
already made as a decisions row (
Source = "Elicitation") before asking anything. Those rows are settled — see Settled decisions. - Harvest the ticket. When the work traces to a ticket, record
each line of its
## Decisionsas a decisions row (Source = "Elicitation",assumption: false) before asking anything — those are settled. Treat its## Open questionsas part of the interview's agenda. A "no" the user settles to one of them is a decisions row like any other, and a rejected idea never becomes a new ticket. - Honor the brainstorm hand-off. The rows in
brainstorm-decisions.jsonare decisions already settled with the user, and Settled decisions governs them — never re-asked, re-opened only for a contradiction at a namedfile:line. Two things are particular to this step:- A row re-opened here is recorded with
source: "Elicitation". This matters most forGlobal constraint:rows, where the live row alone becomes a binding bullet. - Do not copy brainstorm rows into
decisions.json;plan draftreads both files and merges them.
- A row re-opened here is recorded with
- Ask in the Question format above — at most 2 full-format questions per
message. Resolve the decision tree branch by branch, reflect each answer
back to converge on a shared understanding. Never ask what the codebase can
answer — read it (or re-explore in-context, update facts.json, and re-run
plan verify-facts) instead. - Continue until the user is tapped out and aligned — their bound, not yours.
- Alignment checkpoint. Close by stating back, in plain words: the goal, the design shape, and the kinds of implementation detail you will decide yourself from here (best practice only). The user's explicit confirmation licenses Grill's self-answer routing (step 5); without it, every grill question escalates to the user. A brainstorm hand-off does not stand in for this checkpoint: the plan reads the code after brainstorm ended and may surface things brainstorm could not have known, so the licence to self-answer is still earned here.
- Author
.lightsout/plans/<name>/decisions.json. Write this exact shape (the engine hard-parses it; a wrong field name blocks drafting):{ "planName": "<name>", "decisions": [ { "source": "Elicitation", "question": "<q>", "options": "<A / B>", "choice": "<chosen>", "rationale": "<one line>", "assumption": false }, { "source": "Converge", "question": "<q>", "options": "<A / B>", "choice": "<chosen>", "rationale": "<one line>", "assumption": false, "phases": ["phase2-<slug>.md"] } ] }sourceis exactly"Elicitation"|"Grill"|"Dedup"|"Converge";optionsis a string;assumptionis a bool. Mark a choice made without user confirmation as an assumption.phasesis optional: a list of the phase-file basenames the decision concerns. Leave it out when the decision reaches the whole plan, when its reach is not known, on rows written before the plan is drafted (phase files do not exist yet, so no Elicitation or Approaches row carries it), and onGlobal constraint:rows, which always reach the whole plan. Never write an empty list.
3. Approaches — settle the design shape before drafting (interactive,
conditional). Run this step only when the design shape is not already settled
— by the session discussion, the Elicitation answers, or a brainstorm decision
naming the chosen approach; Settled decisions is the
test. When it is settled, say so in one line ("Design shape settled during
Elicitation — skipping approaches", or "Approach settled during brainstorm —
skipping approaches") and move on — never skip silently. Present 2–3 genuinely
different approaches in the Question format: Context states the design problem
in everyday words, Question asks which to build, Options gives each approach
with its wins and costs, Recommendation names one by number with the one-line
why. Record the
chosen approach as a decisions row (Source = "Elicitation") before drafting.
4. Draft. Run:
node "<plugin-root>/dist/cli.mjs" plan draft --name <name>
Pass --scope single|phased only to override the engine's estimate. On
facts error → re-explore in-context, correct facts.json, re-run
plan verify-facts, then re-draft. On
remaining structural issue(s) → relay them. On success → note the written
plan.md path.
A phased draft runs in two stages: one agent authors overview.md, the engine
checks the phase breakdown it declares against the created-file ceiling, and
then one agent per declared phase authors its phase<N>-<slug>.md
concurrently. So structural issue(s) on a phased plan may name the overview's
phase breakdown rather than a phase file: a phase that creates more files
than one implementing agent may. The fix there is to resplit the phases — edit
the overview's ## Phases table and its ## Phase Declarations to spread the
creates across more phases — and re-run plan draft.
5. Grill — push past conscious knowledge against the drafted plan (interactive, unbounded):
- Relentless: generate the full stream of edge-case questions against the draft — grilling intensity never drops. Routing decides who answers each question, never whether it gets asked; the settled check below is the one thing that removes a question, and it runs before routing. Explore the codebase instead of asking whenever possible.
- Drop a question the record already answers. Check each generated
question against the brainstorm rows,
decisions.jsonand the draft's Decision Log — see Settled decisions — before routing it. A settled question is neither escalated nor self-answered: it is dropped, with no new Decision Log row, because the answer is already logged. The rest of the stream is unaffected — this removes repeats, not rigour. - Route every question before surfacing it. Escalating to the user is the default — self-answer is the single exception, allowed only when ALL of these hold: the user confirmed the alignment checkpoint (step 2); the answer follows directly from the established goal, direction, and architecture; and no defensible reading of the user's intent gives a different answer. Fail any one → escalate. (Typical escalations: a genuine fork, anything that could bend the plan's direction, a question with two defensible answers — illustrative, never a filter.) When in doubt, escalate.
- Self-answered → append the row to
decisions.jsonwith"source": "Grill","assumption": trueand a rationale ending in(self-answered), run the sync command, then fold the answer intoplan.mdvia Edit. Do not surface it live. - Escalated → one question at a time, in the Question format (one
full labeled block per message — never two). After each answer, append the
decisions.jsonrow with"source": "Grill", run the sync command, and immediately fold the answer intoplan.mdvia Edit. Do not batch edits to the end. - On a phased plan, either kind of Grill row carries
"phases"naming the phase files the answer changes, and a row that re-asks a question names the phases its new answer concerns — see Name the phases a decision concerns under Settled decisions. - Continue until the user says stop — do not self-terminate. Self-answering a question never counts as stopping.
- The grill also interrogates the ledger. With
plan.contracton, the plan carries an## Acceptance Teststable and a## Prose Fileslist, and three questions belong in the stream like any other: an acceptance criterion with no row, a row whose named test could not fail if the feature were never built, and a prose file that a test could have stated after all. Each finding lands where it belongs — adecisions.jsonrow followed by the sync command, or a new ledger row. - Assumption digest. When the user stops, list every self-answered
question with its chosen answer. A veto re-opens that question as an
escalation — fold the corrected answer into
plan.mdbefore moving on.
6. Dedup Review — resolve prior-art duplication (interactive). This is the last shaping of the plan; after it the plan is complete and Grade only verifies. Run:
node "<plugin-root>/dist/cli.mjs" plan dedup --name <name>
Read .lightsout/plans/<name>/dedup.json. Detection and judgment are the
subcommand's; you only conduct the review and apply the chosen edits.
findingsempty → nothing to review; go to Grade.- A finding whose resolution the record already carries is not surfaced — see Settled decisions. The subcommand re-detects an overlap every run, so a resolution chosen on an earlier pass comes back as a finding; apply the resolution already recorded and say in one line that it was settled, rather than asking again.
findingspresent → surface each remaining finding in the Question format (at most 2 per message): Context says in plain words what the plan wants to build and what already exists that overlaps — never bare symbol names; Question asks which to pick; Options summarizes the resolutions to choose between; Recommendation is the judge'srecommendationin plain words. Get the user's choice per finding or offer auto-accept (apply everyrecommendation, showing a summary first). Append onedecisions.jsonrow with"source": "Dedup"per resolution — on a phased plan with"phases"naming the finding'sphasefile, plus any other phase file the resolution changes — and run the sync command once, then apply each chosen resolution toplan.mdvia Edit:- reuse → drop the Files-to-Create entry; wire the plan's usage to the existing symbol.
- extend → add a Files-to-Modify entry for the existing symbol.
- extract → add the shared file to Files-to-Create at
suggestedLocation, plus a Files-to-Modify entry permigrateCallers. - defer → leave the entry; record the accepted duplication in
## Prior Art(logged debt). - distinct → record the justification in
## Prior Art.
- Each finding carries the
phaseit was planned in — the plan file's basename. Apply the resolution to that file, not toplan.md. "complete": falsemeans a judge failed or hit the rate-limit wall. The findings present are real, but the scan is partial — resolve them, then re-runplan dedupbefore moving on.
7. Grade + converge. Run:
node "<plugin-root>/dist/cli.mjs" plan grade --name <name>
Read .lightsout/plans/<name>/grade.json:
"passed": trueand"complete": true→ go to handoff."passed": falsewithgaps→ surface only the blocking gaps: the ones whoseoutcomeisneeds-a-humanorunjudged. Put each in the Question format (at most 2 per message, recommended-first), grouped by the gap'sphase. Resolve each by appending adecisions.jsonrow with"source": "Converge"— on a phased plan with"phases"naming the gap'sphasefile, plus any other phase file the answer changes — running the sync command, then editing the plan file the gap'sphasenames in place via Edit —plan.mdfor a single plan, thatphase<N>-<slug>.mdfor a phased one. Then re-runplan grade. Repeat untilpassedor the user calls it. Do NOT re-runplan draft— a re-draft regenerates the plan files and would clobber the Grill edits already folded in.- A blocking gap whose answer the record already carries is not surfaced — see Settled decisions. A re-grade re-reads the plan from scratch and can raise a gap over something settled in Elicitation, Grill or Dedup. Resolve it from the recorded answer, note in one line that it was already settled and where, and re-grade.
- Everything else the pass found is still in
grade.json, in full, for the user or a later agent to read. Nothing was dropped; it was weighed and found not to need them. - Every pass — including one that did not finish — is also appended as one JSON
line to
.lightsout/plans/<name>/grade-history.jsonl.grade.jsonis still the latest pass and still the only file to branch on; the history is there for the user, or for an agent asked to look, to see how a plan's grade moved across re-grades and which finding kept coming back. Nothing reads it automatically. - Every gap carries an
outcomesaying who has to settle it:needs-a-human— a person has to decide this one. These are the questions.agent-can-decide— the implementing agent can settle it on its own, andagentDecisionsays what it would decide. Not a question.already-answered— the answer is already in the plan or the code, andanswerAtsays where. Not a question.unjudged— nobody weighed this one, so it blocks until someone does.
- An
unjudgedgap is a different question from aneeds-a-humanone, and must not be dressed as the same thing. Surface it in the Question format like any other blocking gap, so it is never silently dropped — and in the same block, say plainly that it blocks because nobody weighed it, not because the plan is thin, and quote itsunjudgedReason. Say that re-grading will not retry that judge: a re-grade re-runs every reader and comes back with a fresh set of findings, so this exact one may simply not reappear, and there is no way to re-judge a single finding. Leave the choice with the user: answer it into the plan, or let it go. Do not recommend a re-grade as the remedy — it reads like a retry and is not one. - Every gap also carries the
lensthat found it (surface,wiring,decisions) — three differently-briefed checkers read every phase, so two gaps with the same text and different lenses are two lenses agreeing, not noise. - While resolving, re-check a single phase you just edited with
lightsout plan grade --name <name> --phase <n>— three checkers and about three minutes instead of a full pass. The final grade before handoff is always a full run with no--phase. - A grade whose
"complete"is false, or whosephasesCheckeddoes not list every phase file in the plan folder, is not a clean bill whatever its verdict. Its gaps are real and worth fixing, but the unlisted phases were not looked at — say so to the user and re-grade once the fixes are in. (overview.mdis deliberately never gap-checked and never appears inphasesChecked; it is checked deterministically instead.) - A pass whose
incompleteReasonnames blocking structural findings ran no semantic reader at all. Itsgapslist is empty because nobody looked, not because the plan is clean. Fix the structural findings and re-grade before reading anything into it. scopesays how far the pass reached. Only afullpass can bepassed; afocusedpass is a repair check over the edited phases and the phases they reach, and is always"complete": false. The engine chooses the scope itself and runs the full review automatically once a focused pass clears — there is no flag to pass and nothing extra to run.- A blocking gap carrying a
findingIdis a finding the plan has seen before. Its record lives in.lightsout/plans/<name>/grade-memory.json, which the engine owns: never edit it, and never treat a finding's absence from a later pass as it being resolved. A record closes only when the plan states the answer and the engine's re-verification judge cites where. - When a re-grade reports that a recorded passing full review still covers the
current inputs, nothing was re-run and that grade is current. Deleting
grade-memory.jsonforces a new baseline. structuralfindings present (rare) → apply each finding's exactfixto the plan file named by itsphase, via Edit, then re-grade. A finding printed asnoterather than⚠is advisory: information for you and the user, not work to do.- With
plan.contracton,grade.jsonalso carriesweightsandphasesLight. Each plan file is weighed from its own counts — files created, packages touched, whether it has a pattern to mirror — and only a heavy file gets the reader fleet. A file named inphasesLightwas graded by the deterministic lint and the ledger check alone, and its entry inweightssays why. That is not a thinner grade of the same kind: it is the mechanical half, and the grill and the ledger are what carry the rest. - Reading a typed field to decide what to display is not a gate. What blocks is
decided in the engine and arrives as
passed; you never recompute it.
8. Handoff. When the work traces to a ticket, take it to ready-to-implement first, in this order:
node "<plugin-root>/dist/cli.mjs" plan publish --name <name>
node "<plugin-root>/dist/cli.mjs" ticket-state --ref <ticket> --planning-status planning-complete --tracker-status ready
Publish first, so the durable plan is on the ticket before anything claims the
ticket is ready to build. A successful publish is also what moves this plan from
planning to ready on the ticket's record. Between the two commands, drain the
ticket's ## Open questions of every line the shaping answered. Then run
ticket-state. A nonzero exit from either command is a stop: report the exact
failure and do not print the handoff line below, because a ticket another machine
cannot recover is not ready for anyone. The rule behind the order is the
ticket-workflow skill's ### Publish when the ticket is ready to implement, not at close section.
On a multiple-plan ticket, add one line to the handoff: the ticket stays open
until the user files a ship request with lightsout ticket request-ship, and the
ticket-workflow skill's ### Ship requests says what that request has to name.
Never file one yourself — the user decides the finish line.
With no ticket, skip both commands.
Then relay the final grade and:
Next: run the `implement` skill with .lightsout/plans/<name>
The same line works for both shapes — the engine reads the folder: an
overview.md runs every phase in order, otherwise the folder's plan.md runs
on its own. To run a single phase of a phased plan by itself, pass that phase
file instead: run the implement skill with
.lightsout/plans/<name>/phase1-<slug>.md as the plan and
.lightsout/plans/<name>/overview.md as its overview.
List any decisions left unresolved. The grade is
advisory — the implement skill runs whatever plan it is given.