Installing a wheel built from this package puts a single novel console-script
onto PATH. novel is a multiplexer: it dispatches to a state subgroup that
reads and mutates the harness state, plus four leaf verbs:
novel state— read and mutate the harness state (with its own subcommands).novel done— evaluate the done predicate.novel compile— regenerate the compiled manuscript.novel desloppify— report prose tics.novel wordcount— report per-chapter and cumulative word counts.
novel wordcount reports per-chapter and cumulative word counts (roadmap task
6.1.1). It is read-only: it reads the chapter drafts and state.toml,
derives the report, and writes nothing on any path. The report carries, per
chapter, the drafted words, the percentage of the chapter target, and the delta
against that target; and cumulatively, the drafted total, the percentage of the
novel target, which of the 30%, 50%, and 80% knitting-gate triggers the drafted
ratio has reached, and the distance in words to the next gate. Past the final
(80%) gate the next-gate threshold and distance are reported as null rather
than a negative number. The triggers are derived from the drafted ratio; they
are distinct from the recorded [gates.knitting] flags, which also record that
the knitting pass was integrated. novel wordcount derives the geometry from
the drafts and never reads, claims, or rewrites the recorded gate flags.
In v1 novel wordcount takes no per-chapter flag; the report always covers the
whole manuscript with per-chapter detail. It exits 0 on a report and 3 on a
state or input fault — a missing or unparseable state.toml, an absent
working/, or an unreadable or undecodable draft. An unknown --option is the
shared exit-2 usage channel. Each subcommand drives its real checker.
novel compile regenerates working/manuscript/compiled.md by concatenating
the chapter drafts in zero-padded chapter-index order (chapter-01/draft.md,
chapter-02/draft.md, …), joined by one fixed separator. The order is taken
from the [chapters] manifest, not from the directory listing, so identical
drafts always produce a byte-identical compiled.md: it is deterministic and
idempotent, and a second run over unchanged drafts rewrites nothing observable.
The working directory is the fixed working/ directory relative to the current
directory. When the [chapters] manifest is absent or empty there is no
authoritative ordering, so novel compile writes nothing and exits 3 (the
state/input code in the shared exit-code table below): plan the chapters first.
Any other state or input fault — a missing or unparseable state.toml, or an
unreadable draft — likewise exits 3.
novel compile --check is the read-only counterpart (roadmap task 4.1.2). It
reports whether compiled.md is the ordered concatenation of the present
chapter drafts and writes nothing on any path. It exits 0 when the
compile is current and 4 (an actionable finding) when compiled.md is stale
or absent, so the harness knows to regenerate it; an absent or empty
[chapters] manifest still exits 3. The verdict is read from the one shared
routine the novel done compile_consistent clause also uses, so
novel compile --check and that clause agree on every tree about whether
compiled.md is current.
novel state now has its first real subcommand, novel state check (roadmap
task 2.1.2). It validates the state coherence invariants of
./working/state.toml and writes nothing. The working directory is the fixed
working/ directory relative to the current directory; there is no
--working-dir flag. By default it prints a one-line JSON envelope on standard
output; pass the global --human flag (novel state --human check) for a
readable rendering instead.
novel state check uses the shared exit-code table:
0— every checked invariant holds;result.violationsis empty.4— one or more invariants are violated; the breached invariant names appear inresult.violationsfor the agent to adjudicate.3—./working/state.tomlis missing or unparseable (the state-error channel).
As of roadmap task 2.3.2 novel state check is disk-aware: it validates
the pure-state invariants decidable from state.toml alone and the
disk-evidence invariants that compare state.toml against the working/ tree.
When a disk-evidence invariant is violated it also attaches a
result.reconciliation describing the repair a stale tree implies (the action —
recount, refuse, complete-pending-turn, or rollback-pending-turn — and
the discrepancy names); check still writes nothing on any path.
The pure-state names that can appear in result.violations are:
phase-in-enum— the current phase is not one of the known workflow phases.completed-prefix— the completed-phase list is not the in-order run of phases before the current one (a phase is missing or out of order).by-chapter-sum— the per-chapter word counts do not add up to the recorded current total.consecutive-clean-within-target— the consecutive-clean-pass counter is negative or above its configured convergence target.convergence-target-at-least-one— the convergence target is below one.consecutive-clean-within-drafted— the consecutive-clean-pass counter claims more clean chapters than have actually been drafted.cursor-coherent— the drafting cursor (chapter, scene, beat) is negative or points past the chapters in the manifest.gate-ratio-consistent— a knitting gate is set true or false in a way that disagrees with the drafted-word ratio against its threshold.
The disk-evidence names compare the recorded state against the working/ tree:
manifest-disk-bijection— the chapter manifest and the on-diskchapter-NN/directories are not in one-to-one correspondence. During drafting this relaxes to a subset rule (ADR 009):checkaccepts a tree whose on-disk chapters are a subset of the manifest, so a manifest entry that has no directory yet does not flag while you are still drafting. A directory with no manifest entry (an orphan) or a gap in the manifest still flags in every phase, and the exact one-to-one correspondence is enforced again atfinal-passanddone.done-flag-without-draft— a chapter carries adone.flagbeside an empty or absentdraft.md.compiled-matches-drafts—compiled.mdis not the ordered concatenation of the present drafts.pending-turn-cleared—state.tomlrecords an uncleared[pending_turn](a torn multi-file turn).cursor-plan-present— a non-zero scene or beat cursor has no on-diskscenes.md/beats.mdplan for its chapter.word-counts-match-drafts— the recorded per-chapter[word_counts]table disagrees, on a chapter both sides record, with the words actually on disk (a stale done-claim, or a realdone.flagover a draft the table under-counts).word-counts-cover-drafts— the recorded[word_counts].by_chapterkey set diverges from the drafts: the table omits a drafted chapter, or carries an entry for a chapter the manifest never declared.novel state reconcilerepairs it with the same recount that repairsword-counts-match-drafts, re-keying the table off the manifest so the missing key is supplied and any orphan key dropped.
For example, while you draft chapter by chapter the manifest holds every
planned chapter but only the drafted-so-far directories exist on disk. With a
manifest of chapters 1 to 3 and only chapter-01/ and chapter-02/ written,
novel state check exits 0 during drafting — the on-disk set is an honest
subset of the manifest. Add a stray chapter-09/ the manifest never declares
and the same check exits 4 on manifest-disk-bijection. Advance the
project to final-pass without writing chapter-03/ and check exits 4
again, because every planned chapter must exist on disk before the final
compile.
novel state also exposes three write subcommands that mutate the project —
init, set-cursor, and advance-phase (roadmap task 2.2.2). Every one of
them honours the same validate-before-persist, write-nothing-on-refusal
contract: each derives the state its arguments would produce, checks it against
the coherence invariants above, and writes atomically only when the result is
coherent. A refusal exits 3 (the state-error channel), names the breached
invariant on standard error, and changes nothing on disk, so the prior
state.toml is left byte-for-byte intact. A missing or unparseable
state.toml is itself an exit-3 refusal. The sections below describe only
each subcommand's own arguments and the extra refusals specific to it; the
shared contract holds for all three.
novel state init bootstraps a fresh project: it creates the working/
directory skeleton — the characters/, world/, reader/, plan/,
manuscript/, and reviews/ subdirectories plus an empty log.md — and
writes a coherent initial state.toml. It takes --title (the novel title),
--slug (the project slug), and --target-word-count (the target word count,
defaulting to 80000). To protect a live project, init creates but never
overwrites: when working/state.toml already exists it refuses with exit 3
rather than clobbering it, so re-running init over an initialized project is
safe.
novel state set-cursor moves the drafting cursor. It takes three integer
options — --chapter, --scene (default 0), and --beat (default 0) —
and records them as the [drafting] cursor. Per the shared contract it refuses
(the cursor-coherent invariant) when the cursor is incoherent: a chapter past
the end of the manifest, or a scene or beat set while the cursor names no
chapter.
novel state advance-phase takes no arguments and always advances
phase.current to the immediate next workflow phase, appending the phase it
leaves to phase.completed. Because it can only ever step to the successor, a
phase skip cannot be requested; it instead refuses (exit 3) when advancing
from the terminal done phase (which has no successor) and when advancing into
drafting with an empty chapter manifest.
novel state recount re-derives the word counts from the chapter drafts, so
you never type a word count by hand. It reads each chapter's
working/manuscript/chapter-NN/draft.md, counts its words, and rewrites
[word_counts].current and [word_counts].by_chapter to match what is
actually on disk (current is the sum of the per-chapter counts). It is
idempotent: running it twice over unchanged drafts leaves state.toml
byte-for-byte identical. Like the other write subcommands it writes nothing on
refusal (exit 3) — a missing or unparseable state.toml, an unreadable
draft, or a recount that would leave the state incoherent each leaves the prior
file untouched. One incoherence is worth calling out: a recount refuses when it
would move the drafted ratio across a knitting-gate threshold (30%, 50%, or
80%) the recorded gates do not yet reflect — recount re-derives the word counts
only and never flips a gate. The refusal names the crossed threshold and, once
you have integrated and logged the pending knitting pass, points you at
novel state set-gate (described below) as the remedy.
novel state set-chapters (roadmap task 2.2.3) populates the [chapters]
manifest from your chapter plan — the one sanctioned way a planned chapter
reaches [chapters] (never a hand edit). You pass the plan as a single JSON
array argument:
novel state set-chapters --chapters '[
{"number": 1, "slug": "the-summons", "title": "The Summons", "target_words": 3200},
{"number": 2, "slug": "the-road", "title": "The Road", "target_words": 2800}
]'
On success (exit 0) it writes [chapters] in ascending number order, creates
the on-disk working/manuscript/chapter-NN/ directories, appends a log.md
receipt, and returns the written chapters in result.chapters; a follow-up
novel state check then exits 0 because the manifest and the directories are
in bijection. It refuses with exit 3 — writing nothing — when the plan is
incoherent (numbers not contiguous from 1, a duplicate number or slug, a
non-positive target, or an empty plan) or when [chapters] is already
populated (set-chapters is a one-shot populate, not an editor; re-planning
is a separate concern). A malformed --chapters argument — JSON that does not
parse, or a missing or wrong-typed field — is a usage error and exits 2. If
the command is interrupted mid-write, recover by running
novel state reconcile (which completes the torn turn by creating the missing
chapter directories), never by re-running set-chapters or editing the tree by
hand.
novel state set-gate (roadmap task 2.2.4) asserts the knitting-circle gate
flags and the final-pass gate to the value the state mandates, the one
sanctioned way to record a gate (never a hand edit). It takes an optional flag
per gate: --knitting-30/--no-knitting-30, --knitting-50/
--no-knitting-50, --knitting-80/--no-knitting-80, and --final/
--no-final; an omitted flag leaves that gate untouched.
novel state set-gate --knitting-30
A knitting gate is coherent only when it matches the drafted ratio (the gate
fires once drafting crosses 30%, 50%, or 80% of target). So
novel state set-gate --knitting-30 succeeds only once drafting has crossed
30% of target: it is the repair for a gate that lags its ratio (for example
after a recount moved the ratio past 30% but done_30 is still off).
Asserting a gate true below its threshold, or false once it has crossed,
contradicts the ratio and is refused with exit 3, writing nothing; from a
coherent state the assertion is an idempotent no-op (exit 0). The final-pass
gate has no ratio binding, so --final is accepted on any coherent tree. A
set-gate with no flag is a usage error and exits 2.
novel state complete-final-pass (roadmap task 2.2.4) is the named,
argument-free verb for the common final-pass flip — it sets
gates.final.final_pass_complete true and is idempotent (set-gate --final is
the general form). It exits 0 on any coherent tree; a follow-up
novel state check stays coherent.
novel state complete-final-pass
novel state set-fangirl (roadmap task 2.2.4) records the fangirl pass's last
chapter (drafting.fangirl.last_chapter_passed). It takes --last-chapter k
(an integer); 0 means no pass yet.
novel state set-fangirl --last-chapter 6
It refuses with exit 3 — writing nothing — when k is outside
[0, number-of-manifest-chapters] (a fangirl pass cannot have run on a chapter
the manifest does not contain). A non-integer --last-chapter is a usage error
and exits 2.
novel state set-critic-pass (roadmap task 2.2.4) records the critic pass
number (drafting.critic.pass). It takes --pass p (an integer, numbered from
1).
novel state set-critic-pass --pass 2
It refuses a --pass below 1 with exit 3, writing nothing. A non-integer
--pass is a usage error and exits 2. Record every one of these fields by
running its command, never by editing state.toml by hand.
novel state reconcile (roadmap task 2.3.2) carries out the repair
novel state check reports when state.toml has drifted from the on-disk
manuscript — the recovery routine you used to run by hand, now run as code. It
re-derives the reconciliation from disk independently (it never trusts a
payload from check), then:
- when the
[word_counts]table is stale against the drafts — whether on a shared chapter's count or on theby_chapterkey set (a missing or orphan entry) — it rewrites[word_counts]from the drafts (a recount) and exits0; - when
state.tomlleft an uncleared[pending_turn], it completes or rolls the torn turn back (it never fabricates a draft or adone.flag) and exits0. This includes a tornset-chaptersturn — a populated manifest with one or morechapter-NN/directories still missing:reconcilecompletes it by creating the missing, manifest-derived directories and clearing the record (ADR 008); - when
log.mdis absent beside a presentstate.toml— the partial-initbootstrap, where a crash struck betweeninit's two writes (state.tomlfirst,log.mdsecond) and re-runninginitrefuses — it recreates an emptylog.md, appends a recovery receipt, and exits0(roadmap task 2.3.4); - when disk contradicts itself — a
done.flagbeside an empty draft, acompiled.mdreferencing absent content, a non-bijective manifest, or a plan-less cursor — it refuses: it writes no state change and exits4for you to adjudicate.
The recreated log.md is empty save for the recovery receipt: the
log-present detector fires solely on log.md absence and cannot tell a clean
partial-init crash from a later loss of a populated log, so RECREATE_LOG
always restores a fresh, empty file and exits 0. Prior receipts are not
recoverable by this repair; if you need them back, restore log.md from a
backup before reconciling.
Every repair or refusal is logged as a recovery receipt appended to
working/log.md, and reconcile removes no file under working/. It is
idempotent: running it twice over an already-reconciled tree is a no-op that
leaves state.toml byte-for-byte unchanged. A repair that would cross a
knitting gate the recorded gates do not reflect is refused (exit 3) rather
than silently mis-repaired, because integrating a knitting pass is your
judgement, not a deterministic recompute.
result.violations is the checker's read shape: it belongs to
novel state check alone. The write subcommands (init, set-cursor,
advance-phase, recount, reconcile, set-chapters, set-gate,
complete-final-pass, set-fangirl, set-critic-pass) instead report what
they changed in result — set-cursor returns the cursor it set,
advance-phase returns the {from, to} transition, recount returns the
{current, by_chapter} counts it wrote, reconcile returns the
{action, discrepancies, detail} it enacted (plus the written counts for a
recount), set-chapters returns the {chapters} it wrote, and the
gate/drafting mutators return the {gates} or {drafting} keys they set — so
do not expect a violations key from a write.
novel desloppify reports prose tics (roadmap task 5.1.2). It reads the
chapter drafts under ./working/, scans them against a versioned rule pack —
the §6 high-frequency-offender table shipped with the package by default — and
reports a per-rule finding without editing the manuscript or touching
state.toml (it is a detect-only checker). By default it scans the whole
manuscript (every chapter in the [chapters] manifest); pass --chapter N to
scan a single chapter, or --pack PATH to use a different rule pack. A second
pack, ai-isms.toml, ships with the package and flags lexical AI-isms
("load-bearing", "a testament to"); it is opt-in, selected with
--pack novel_ralph_skill/rulepack/packs/ai-isms.toml. A single run scans
exactly one pack; combining both packs in one invocation is not yet supported.
Like novel state check novel desloppify prints a one-line JSON envelope by
default and a readable rendering under the global --human flag.
Pass --ledger PATH to enforce a per-novel device ledger instead of the
rule-pack scan. A device ledger rations a book's signature devices — a
recurring image, a key phrase, a bookend line — naming each device a regex
pattern and a ration (max_count, allowed_chapters,
retired_after_chapter, or reserved_for_chapter). The ledger is your own
file (it is not shipped with the package); write it into working/ and point
--ledger at it. The current spend is recomputed from the chapter drafts on
disk on every run, so the ledger cannot drift from the manuscript: removing a
use from a draft and re-running drops the finding with no ledger edit. The
ledger rations across the whole manuscript, so --ledger cannot be
combined with --chapter; the combination is a usage error. An over-ration
device appears in result.violations, exactly as an over-threshold rule does.
novel desloppify uses the shared exit-code table:
0— every rule is within threshold (or every device is within ration);result.violationsis empty, andresult.findingsis empty too — a clean pass carries only the over-threshold findings, of which there are none.4— one or more rules exceed threshold (or one or more devices exceed their ration); the offending ids appear inresult.violationsfor the agent to adjudicate, and each finding'srule_id/device_id(the canonical slug theviolationslist references),phrase/pattern(the authored pattern source — the regex that names the offender, not a literal matched span), hit count, and per-{chapter, line}matches are inresult.findings. Rule findings also carrythresholdand per-page density; device findings carry the ration kind, its bound, and the offending chapters for a window breach.2— a usage error:--chapter Nnames a chapter absent from the manifest,--packpoints at a rule pack whose content is malformed,--ledgerpoints at a ledger whose content is malformed, or--ledgeris combined with--chapter.3— a state or input error:./working/state.tomlis missing or unparseable, a chapter draft is unreadable, or--pack/--ledgerpoints at an absent or undecodable file.
A per-page rule reports density as hits per page_words tokens, and a partial
page still counts: the scanned text is divided by page_words as a float
rather than rounded up to a whole page. On a short or near-empty draft — an
early chapter, or a --chapter N scan of a chapter barely begun — the scanned
text is a fraction of one page, so a single offending hit extrapolates to a
high per-page density and can trip the threshold. This is design-correct: the
density measures the rate the tic appears at, not the raw count, so a draft
that is one tenth of a page with one hit is reported at the same rate as a full
page with ten. Do not be surprised when a short chapter trips a per-page rule
on one hit; re-scan once the chapter is fuller to read the settled rate.
novel done evaluates the done predicate (roadmap task 3.1.1): it answers "is
the novel finished?" deterministically, so the harness can check it every turn
with one call. It reads ./working/state.toml and the working/ tree,
evaluates six done clauses against disk, and writes nothing on any path (it is
a read-only checker). It takes no arguments. Like the other checkers it prints
a one-line JSON envelope by default and a readable rendering under the global
--human flag.
The result reports each clause as a boolean, so an operator sees exactly
which conditions are unmet:
phase_is_done—state.phase.currenthas reached the terminaldonephase.final_pass_complete— the final-pass gate ([gates.final]) is set.all_chapters_flagged— every manifest chapter has an on-diskdone.flag.knitting_gates_passed— all three knitting gate booleans are true and all threeworking/reviews/knitting-{30,50,80}.mdreviews are present.compile_consistent—working/manuscript/compiled.mdis present and its content is the ordered concatenation of the chapter drafts. An absent compile, or a present-but-stale one that no longer matches the drafts, fails the clause.no_unresolved_blockers— no chapter'scritic-notes.mdcarries an unresolved BLOCKER (a line beginningBLOCKERwithout a[resolved]marker).
novel done uses the shared exit-code table:
0— every clause holds; the novel is done.1— a drafting clause is unmet (the benign "not yet done" the harness loops on), alone or alongside a stale compile;messagesnames the unmet clauses. A sole failure caused by an absentcompiled.mdalso exits1(an absent compile is not a regenerable stale one).3— a state or input error:./working/state.tomlis missing or unparseable, or a chapter artefact (such ascritic-notes.md,compiled.md, or adraft.md) is unreadable.4— every clause holds exceptcompile_consistent, andcompiled.mdis present: the manuscript is otherwise complete and the only obstacle is a stale compile, which the harness regenerates (matchingnovel compile --check).
Stale-compile handling. compile_consistent checks the compile content:
it recomputes the ordered concatenation of the present drafts and compares it
byte-for-byte against compiled.md, so a present-but-stale compile — one that
no longer matches the drafts even if its header count and word total coincide —
is caught. When that stale compile is the only unmet clause, novel done
exits 4 (the actionable finding above) rather than looping at 1.