Skip to content

Latest commit

 

History

History
258 lines (209 loc) · 19.8 KB

File metadata and controls

258 lines (209 loc) · 19.8 KB

Dev Loop Plan — driving Nabu's implementation with Claude models

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.

1. Why this project fits an unattended loop

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 test green).
  • 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.

2. Model tiering policy

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.

3. Work packets and the backlog

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.

4. Loop mechanics

Each iteration, regardless of execution vehicle (§5):

  1. Pick the first ready packet whose deps are done.
  2. Dispatch at the packet's tier (session model or Agent model override).
  3. 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).
  4. Verify: rake test + rake lint green, 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/, never docs/ (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 into docs/ 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 in docs/ as before.
  5. Commit on the current phase branch (phase-N), imperative message referencing the packet ID. Update backlog + worklog.
  6. Escalate on failure: two failed attempts at a packet → mark blocked with a diagnosis, move to the next packet. Never thrash. blocked packets are adjudicated by Fable at the next gate (or sooner if everything else is blocked → stop and notify the owner).
  7. 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, updates README.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 what bin/nabu can do today) — then opens a PR phase-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.

5. Execution vehicles — two stages

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.

6. Guardrails

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/nabu commands, bundle install/bundle exec against the project Gemfile, git add/commit/branch/checkout on non-main branches.
  • 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.

6b. Recalibration after growth waves (owner-ratified 2026-07-20, the P34-gate lesson)

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:

  1. Cap at render, never at fetch — a fetch-time cap destroys the count that honesty needs.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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).

6c. The phase slate — full-board candidacy (owner-ruled 2026-09-12)

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.

7. Phase & packet breakdown

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/nabu Thor skeleton, config/nabu.yml loading, --version
  • P0-3 GitHub Actions CI: rake test + rake lint on every PR (the loop's external, un-gameable oracle)
  • P0-4 Nabu::Error hierarchy, Nabu::Shell.run, Nabu::Normalize.nfc with 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::Adapter base 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.yml registry + runs table + 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.

8. Fixture acquisition (approve the plan, then the loop fetches)

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.

8b. Releases (owner ruling 2026-07-22)

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.

9. Decisions (approved by owner, 2026-07-03)

  1. Plan approved with amendments incorporated (§2 only Fable/Opus write code; §6 sandbox-freedom guardrails; §8 plan-gated automated fixture acquisition).
  2. Push policy: the loop pushes phase-N branches and opens PRs on arvicco/nabu; main stays owner-merged.
  3. Stage A attendance: Phase 0 runs interactively (Fable orchestrating, Opus implementing); /loop from Phase 1 onward.
  4. In effect: the phase plan doc (.docs/p<N>-plan.md) carries the elaborated packets (docs/backlog.md carried them through ~P52), .claude/settings.json carries the permission profile, work proceeds on phase-N branches — and independent packets dispatch as parallel agents in worktrees off the phase branch (routine practice since P78), with the loop integrating the merges.

§4 addendum — gate waits don't block the loop (owner, 2026-07-11)

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.