How Nabu gets built with maximum unattended automation, minimum Fable-tier spend, and explicit human approval gates. Ratified 2026-07-03 (§9); this is the operating process, amended in place since — the "proposal" voice below is the original document's, kept for the record. Since 2026-09 the GENERAL process doctrine (working agreements, board machinery, the full-board slate ritual) is maintained in the owner's dev-loop tooling and applies here in full; this document records Nabu's project specifics, deltas, and history.
Nabu's own ground rules (CLAUDE.md) accidentally specify a near-perfect autonomous dev loop:
- TDD is mandatory → every packet has a machine-checkable definition of done (
rake testgreen). - No network in tests → the loop can verify everything locally, deterministically, forever.
- Shared adapter conformance suite → new adapters are graded by an existing oracle, not by judgment.
- "Small diffs" rule → the unit of work is already PR-sized; packets fall out of the docs naturally.
rake lint→ objective style oracle, no bikeshedding.
The loop's job is therefore mostly dispatch and verify, not judge — which is exactly what lets cheaper models do most of the work.
Principle: Fable writes contracts and judges; Opus writes code that has a pattern to follow; the test suite is the real gatekeeper regardless of who wrote the code.
| Tier | Used for | Rationale |
|---|---|---|
| Fable | Adapter contract + conformance suite design; loader idempotency/revision/withdrawal semantics; URN minting policy; first parser of each family (sets the pattern); phase-gate reviews of the whole phase diff; adjudicating packets the loop marked blocked |
Mistakes here are expensive and cross-cutting; everything downstream copies these patterns |
| Opus | Scaffolding; CLI commands; store/query implementation against a Fable-approved schema; every second-and-later adapter or parser in a known family; SyncRunner/reports; test-writing for well-specified behavior; doc updates; lint cleanups and mechanical code changes | Pattern-following work with an existing reference implementation and an oracle to grade it |
| Sonnet | Non-coding chores only: fixture READMEs, worklog/backlog housekeeping, summarizing run reports, drafting fixture shopping lists | Cheap and adequate for prose/bookkeeping; never writes code |
Only Fable and Opus write code. Sonnet/Haiku are restricted to simple non-coding tasks. Heuristic for tagging a coding packet: first-of-kind → Fable; everything else → Opus. When in doubt, tag Opus and rely on the escalation rule (§4) — a wrongly-tagged packet fails verification and gets bumped up, costing one retry, not a bad foundation.
The backlog lives at docs/backlog.md — a flat, human-editable list of packets
(HISTORICAL through ~P52: since then the live coordination state moved to the
gitignored .docs/ — the per-phase plan doc .docs/p<N>-plan.md, the work
queue .docs/work-queue.md, and the decision register
.docs/decision-register.md; docs/backlog.md and docs/worklog.md stand as
the early record):
## P1-04 · Loader: upsert, content hashing, revisions [tier: fable] [status: ready] [deps: P1-03]
Goal: Loader upserts passages by urn; unchanged content skipped via content_sha256;
changed content bumps revision and journals old hash; upstream deletion → withdrawn.
Acceptance: idempotency test (load fixture twice, counts/revisions unchanged);
revision-bump test; withdrawal test; rake test + rake lint green.Statuses: ready → in-progress → done | blocked (with a reason line). The executing session updates its own packet's status and appends one line to docs/worklog.md (date, packet, commit SHA, notes). The backlog is the loop's entire coordination state — no external tracker, survives any session dying.
Each iteration, regardless of execution vehicle (§5):
- Pick the first
readypacket whose deps aredone. - Dispatch at the packet's tier (session model or
Agentmodel override). - Implement TDD: failing test first, then code, then refactor. CLAUDE.md rules apply in full (no assertion-weakening, no opportunistic refactors, no invented upstream formats).
- Verify:
rake test+rake lintgreen, then a/code-review(medium) pass; fix findings. Conventions earned in Phases 5–6 (each caught real bugs green suites missed):- Live smoke at acceptance — for any packet touching real data or the outside world, the orchestrator runs at least one real invocation against the live corpus/upstream before accepting the packet (mocked tests can't see real bytes, real filenames, real rankings, or CI's environment).
- Census first — a defect-class packet starts by classifying EVERY instance from the quarantine/provenance journal, then fixes classes; unfixed classes are reported per class — honesty over recovery count.
- Frozen-URN/text is standing acceptance language for any parser or normalization change: documents that parsed cleanly before must re-parse byte-identical (URNs and text), verified read-only against the live catalog before commit; quarantined documents are unconstrained. Search-form changes instead ride a full rebuild (content hashes cover text_normalized — a parse-only sync would read as a revision storm).
- No orphaned tails: opening phase-N+1 ALWAYS starts with
git cherry main origin/phase-N— commits pushed to a phase branch after its PR merged are silently absent from main and every later branch (bitten twice: the P17 backlog block, and the liv/define fixes whose absence let a pre-fix resync withdraw a whole shelf, 2026-07-14). Any tail commits are cherry-picked before the first packet dispatches. - Owner-consideration output goes to
.docs/, neverdocs/(owner policy, 2026-07-14): every survey, scouting report, strategy brief, or design-for-consideration an agent produces targets the gitignored.docs/directory — the repo is public and these are owner-facing working documents, not publications. Publishing anything intodocs/is the owner's explicit decision, packet by packet. Dispatch mechanics: survey/scout agents run in the MAIN tree (gitignored files never cross a worktree merge); worktree agents deliver code + backlog/worklog only, and route any consideration material through their final report for the orchestrator to file under.docs/. Loop docs that ARE the public record (backlog, worklog, architecture, shipped-feature docs) stay indocs/as before.
- Commit on the current phase branch (
phase-N), imperative message referencing the packet ID. Update backlog + worklog. - Escalate on failure: two failed attempts at a packet → mark
blockedwith a diagnosis, move to the next packet. Never thrash.blockedpackets are adjudicated by Fable at the next gate (or sooner if everything else is blocked → stop and notify the owner). - Phase gate (all phase packets done/blocked): Fable reviews the entire phase diff against
docs/architecture.md, checks the doc is still truthful (updates it if implementation deviated — per CLAUDE.md), resolves blocked packets, updatesREADME.md— the user-facing document describing the capabilities and commands implemented up to this point (honest about what doesn't work yet; a newcomer reading only the README should know exactly whatbin/nabucan do today) — then opens a PRphase-N → main. The owner reviews and merges the PR — this is the standing human approval gate. The gate turn ends by arming the owner's attention alarm (sticky mode — see the global convention in~/.claude/CLAUDE.md), as does any blocked state needing guidance. The next phase's packets are elaborated in detail only after the merge.
Stage A (Phases 0–2): Fable-led sessions, semi-attended.
A Claude Code session on Fable does the design-heavy packets itself and delegates tier: opus packets to Opus subagents via the Agent tool. Owner is around intermittently; this is where trust in the loop is built and where Fable spend is genuinely justified anyway.
Stage B (Phases 3–4): assembly line, unattended.
The pattern library exists; now it's a dozen similar adapters. Run the loop as an Opus main session (interactive /loop, or headless claude -p --model opus per packet from a small driver script) that spawns Fable subagents only for gate reviews and blocked-packet adjudication. Fresh context per packet prevents drift; the backlog file carries all state between packets.
Cloud scheduled agents are deliberately not proposed for v1: fixtures and real syncs are local-machine concerns and the project is local-first. Revisit if Stage B proves itself.
The principle: inside the sandbox, full freedom — the boundary itself is hard. Permissions are set to minimize nagging for anything that can't damage the box or leak credentials.
Freely allowed, no prompts:
- All file operations inside the repo, plus free writing/experimentation in the project scratch space (
tmp/inside the repo, gitignored) and the session scratchpad. rake test/rake lint/rake lint:fix,bin/nabucommands,bundle install/bundle execagainst the project Gemfile,git add/commit/branch/checkouton non-mainbranches.- Online research: web search and fetching docs/specs/upstream format references.
- Experimenting with external APIs (upstream corpus endpoints, IIIF manifests, etc.) — exploratory calls to understand formats are fine; only bulk corpus fetches follow the fixture/sync procedure (§8).
Hard boundary (explicit owner permission, every time):
- Anything outside the repo on this machine — dotfiles, global git config, system settings, other projects.
- Keys, auth, credentials, tokens — never touched, period. API keys the loop needs are provided by the owner via env/config; the loop uses them but never creates, moves, or modifies them.
- Pushes to
main. (Phase branches: per the push policy decided in §9.) - Installing software outside the project (brew, global gems, system Ruby changes). New gems in the Gemfile still follow the CLAUDE.md ask-first rule.
Loop discipline:
- Two-strike rule (§4) bounds wasted spend on any one packet.
- The loop never marks its own phase done — a phase ends at a human-merged PR, full stop.
Every literal in code — a limit, a window multiplier, a hand-frozen
enumeration, a lazy initialization — is a census claim about the corpus
at authoring time. Expansion phases falsify these claims by
succeeding, and the expensive failures are the ones that keep producing
correct-looking output: a clean short list, a tidy "no matches", a card
with no hint of what it omitted. (Specimens, all P34: Define's fetch-time
5-shelf cap silently hiding tls-words once a Han headword matched 11
dictionaries; the limit×10 FTS inner window emptying pages under composed
filters; the ten frozen --parallel regexes; the lazy stdlib require
dying hours into a wave.)
The suite cannot catch this class — characterization tests are loyal to era-bound behavior and pin it as correct. Only measurement against the live corpus does. Standing rules:
- Cap at render, never at fetch — a fetch-time cap destroys the count that honesty needs.
- Every truncating or empty-returning surface adopts the honesty vocabulary: "… N more" tails, "page may be incomplete" hints, skip-by-rule census notes. Silence that looks like completeness is a defect, not a default.
- Era-bound constants carry their justification: a
# census: <number>, <date>comment beside the literal, so future invalidation is a greppable review question, not an archaeology dig. - After every expansion wave, schedule the three-pass audit as consolidation work: mechanical inventory of assumption sites → per-site recalibration against the live catalog (blame the era, re-measure, classify OK / HARDEN / GENERALIZE / RETIRE) → convert the class into gate-checked invariants. Gate runbooks with EXPECT lines against real data are the recalibration instrument — keep writing them.
- Gate checklist line (P35-6, standing): at every phase gate run
bundle exec rake census:check(presence of the markers — also enforced inside the suite) AND re-diff the RECORDED census numbers against the live catalog (sqlite3 "file:db/catalog.sqlite3?mode=ro") — a stamped number that has drifted materially is a review question for exactly the constant beside it; sites stamped while an index was mid-rebuild (the stamp says so) are re-measured first. - Local gate duties (standing, 2026-08-28): the owner's untracked protocol defines further duties that run at every phase gate; their definition and state live outside the public tree by design (public-surface hygiene, 2026-08-30).
The full slate ritual — board census first, explicit ADOPT/HOLD/KILL
dispositions with ages, the three-hold forcing rule, gaps before
candidates, periodic archaeology, the slate format — is canonical in
the owner's dev-loop tooling (its boards doctrine) and BINDING here.
Nabu's deltas: the board files live under gitignored .docs/
(work-queue.md · decision-register.md · p-plan.md · inbox/); the
gap axes are corpus breadth · enrichment depth · reader surfaces ·
publication/outward · operational hygiene · structural layers,
scored from measured state (nabu health, the census SSOT, sister
last-commit dates); and the census instruments are nabu health plus
two-way sister staleness across the family (CLAUDE.md §Sister
projects). A slate is a read product, never a recall product; the
owner names the phase.
Build order follows docs/02-sources.md synthesis (1 → 2 → 4 → 3/5 → 10 …). Packet lists below are the plan; each phase's packets get their full Goal/Acceptance elaboration at the previous phase's gate.
Phase 0 — Scaffold (Opus throughout; Fable reviews the result at the gate)
- P0-1 Gemfile, Rakefile, RuboCop config, Minitest + WebMock harness (HTTP blocked globally)
- P0-2
bin/nabuThor skeleton,config/nabu.ymlloading,--version - P0-3 GitHub Actions CI:
rake test+rake linton every PR (the loop's external, un-gameable oracle) - P0-4
Nabu::Errorhierarchy,Nabu::Shell.run,Nabu::Normalize.nfcwith encoding regression-test scaffolding
Phase 1 — Core domain (Fable-heavy: this is the foundation everything copies)
- P1-1 Value objects:
Passage,DocumentRef,SourceManifest,Document[fable] - P1-2
Nabu::Adapterbase class + contract + conformance suite skeleton [fable] - P1-3 Store: Sequel migrations for the catalog schema (architecture §5), models [fable design → opus implement]
- P1-4 Loader: upsert-by-URN, content hashing, revision journal, withdrawal [fable]
- P1-5
nabu rebuild+ in-memory-SQLite store tests + idempotency tests [opus] - P1-6
config/sources.ymlregistry +runstable +nabu status[opus]
Phase 2 — Reference adapter (Perseus) (the pattern-setter)
- P2-1 Perseus fixtures: acquisition plan → owner approval → loop fetches (§8)
- P2-2
EpidocParser, SAX-based, standalone + tests [fable — hardest parser, defines the family pattern] - P2-3 Perseus adapter composing EpidocParser + conformance suite pass [opus]
- P2-4
SyncRunner,FetchReport/LoadReport, >20% withdrawal circuit breaker [opus, fable review] - P2-5 First real
bin/nabu sync perseus-greek+ eyeball check — human
Phase 3 — Family expansion (Opus assembly line; fixtures batch-provisioned up front)
- First1KGreek adapter (EpiDoc reuse — "nearly free") ·
ConlluParser+ UD adapter ·ProielParser+ PROIEL adapter · TOROT adapter (PROIEL reuse) · Papyri.info adapter (EpiDoc reuse)
Phase 4 — Query surface (Opus; Fable reviews normalization/search-form rules)
- FTS5 external-content table +
nabu search·nabu show/nabu export(plain/JSONL/CoNLL-U) · golden-query smoke tests (test/golden/) ·nabu verify
Phase 5+ — Enrichment, ad-hoc/HTR pipeline — deliberately unplanned here; involves API keys, local sidecars, and human review by design. Planned at the Phase 4 gate.
Tests need real trimmed upstream samples. Acquisition is automated but plan-gated:
- At each phase gate, the loop builds a fixture acquisition plan: per source — exact URLs/repo paths, what to trim and why, expected sizes, target
test/fixtures/<source>/layout, license note, README template. - The owner approves the plan (per phase, one approval covering all that phase's sources).
- The loop then executes the fetches itself, trims, writes the fixture READMEs (retrieval date + URL per CLAUDE.md), and commits — staying strictly within the approved list; anything unexpected (moved URLs, format surprises) goes back on the plan for re-approval rather than being improvised.
- A phase never starts with an unapproved fixture plan; the gate is where approval happens, so fixtures never block mid-loop.
Real full syncs (bin/nabu sync <source> against complete upstream corpora) remain human-initiated per CLAUDE.md — they're bulk downloads and eyeball-verification events, not test infrastructure.
The owner decides the release moment — the loop does all the technical
work around it. A release happens when the owner names a version at a
gate ("cut 1.2.0 after this merges"); that sign-off is the owner's ONLY
step. Everything else — the CITATION.cff and Nabu::VERSION bumps, the
release-notes distillation, the site news announcement (obligatory,
past-dated, verified visible on the live /news/ page — a release is not
done unannounced), the tag, the GitHub release, the Zenodo DOI, the
post-release journal sync — is the orchestrator's, run without handing
the owner commands. The mechanical steps live in ops §12; this section
is the division of labor.
- Plan approved with amendments incorporated (§2 only Fable/Opus write code; §6 sandbox-freedom guardrails; §8 plan-gated automated fixture acquisition).
- Push policy: the loop pushes
phase-Nbranches and opens PRs onarvicco/nabu;mainstays owner-merged. - Stage A attendance: Phase 0 runs interactively (Fable orchestrating, Opus implementing);
/loopfrom Phase 1 onward. - In effect: the phase plan doc (
.docs/p<N>-plan.md) carries the elaborated packets (docs/backlog.md carried them through ~P52),.claude/settings.jsoncarries the permission profile, work proceeds onphase-Nbranches — and independent packets dispatch as parallel agents in worktrees off the phase branch (routine practice since P78), with the loop integrating the merges.
When a packet stops at an owner approval gate (fixture plan, design pick), the orchestrator immediately dispatches the next dependency-free backlog packet rather than idling. Discipline: the gated agent may hold uncommitted edits in the shared working tree (the Phase A convention), so the parallel packet runs in an isolated worktree (or with strict file-disjointness, including backlog/worklog) and merges at review. Reviews remain sequential; the parallelism is in the agents' work, not in the orchestrator's acceptance. Owner verdicts are relayed to the paused agent the moment they arrive, regardless of what else is running.