Mova0.6 pre-release⌕
☰
home / docs / tools / mova-whale — abstract Whale workers

mova-whale — abstract Whale workers

🐋 Whales are among Mova's animals (Rust on Mova): giants that write big articles on special missions a deployment gives them through its own prompt and specification; the 🐙 Octopus that assigns their work is a large language model (LLM). See the sea animals of Mova.

A Whale turns a pinned, immutable task queue into versioned bundles. For each task it:

  1. extracts a bounded source context;
  2. renders a pinned prompt template;
  3. makes at most one teacher call through mova-teacher (Codex CLI, no fallback);
  4. validates the reply against an output contract supplied by configuration;
  5. runs the configured length and originality gates;
  6. publishes an immutable bundle with receipts.

The crate also provides pool and swarm scheduling, status, pause/stop markers, a bounded agent-session lane and the Crab ExternalRoleHandler.

The public code knows no concrete task domain. Prompt texts, output schemas, required fields, length and originality parameters, source-kind choices, model selection, queues and deployment paths come from configuration files given at run time. A deployment keeps them outside this crate. Nothing in the crate depends on private code.

Build and run

cargo build --manifest-path tools/whale/Cargo.toml --offline --locked
cargo test  --manifest-path tools/whale/Cargo.toml --offline --locked

The binary is mova-whale; every subcommand takes one absolute path to a JSON configuration file (mova-whale <subcommand> CONFIG [--once]):

commandeffect
whale-prepare CONFIGpins items, extracts contexts, renders prompts and writes worker configs. No teacher call.
whale CONFIG [--once]runs one disjoint worker. Makes at most one teacher call per task.
whale-status CONFIGreads receipts and worker status
whale-project-lengths CONFIGtrims retained length-gate quarantines to a whole paragraph or sentence. No teacher call.
whale-swarm CONFIG [--once]runs the persistent multi-lane manager
whale-swarm-status CONFIGreads the swarm database and actor status
whale-discover CONFIGpreviews the default queue-file discovery. Writes nothing.
whale-research CONFIGruns one bounded agent session in an isolated Git worktree
whale-research-status CONFIGreads an agent job's report or reservation
crab CONFIGserves Crab with WhaleRoles installed
serve CONFIG DEPUTY_CONFIG SOCKETthe Whales as an English-speaking animal on a private socket (see «In English»). STOP in the socket directory ends it
ask SOCKET ID [urgent]sends arbitrary English from stdin; prints the English answer, then the JSON reply
paper CONFIG [--no-terminal]one Whale reads one paper once, writes its skeleton description, asks its terminal (see «Reading papers»)
learn CONFIG [--no-terminal]one Whale learns on one paper round after round until its own description passes the gates (see «Learning on a paper»)
learn CONFIG --replay ROOT...the same with a replay terminal: answers recorded in earlier state roots or paper run roots, by question; nothing is asked (measurement)
learn-report STATE_ROOT...per paper: rounds, first/best/last words, rounds without gain, questions by kind, terminal calls, facts kept per answer, stop (read-only)
extraction-check TEXT...a paper text's extraction quality against the default limits; WHALE_PROSE=1 adds the prose words Mova reads
batch-candidates CONFIGwhat the selection could take now (read-only, nothing recorded): with eprint_root, the library's e-prints, those in the catalog and those with an open licence; eligible papers; refusals by reason
drafts-audit OUT ROOT...the drafts yardstick (see «The drafts yardstick»): defect classes of every round under state roots, all rounds and each paper's latest; deterministic
drafts-recompose OUT [--map LIVE=SNAPSHOT]... LEARN_CONFIG...each paper's draft written again by the current writer from its text and a knowledge snapshot, in the round layout (no learner, no terminal)
batch-plan CONFIGcloses every finished assignment and gives each free Whale its next paper (one batch each); writes the learners' configurations
batch CONFIG [--no-terminal]the rolling runner (one per run root): each Whale with its own terminal takes its next paper as soon as its paper is final; collects review-only teaching candidates; --no-terminal: one dry pass
epoch CONFIG [--epoch N] [--doc ID] [--no-terminal]one epoch of texts per document: Mova's long cited text from the paper and its neighbourhood, then the Whale's terminal writes from the given sources by path (see «Epochs of texts»); --no-terminal: Mova only
epoch-status CONFIG [--epoch N]every document's state in an epoch (read-only)

In English

mova-whale serve CONFIG DEPUTY_CONFIG SOCKET runs the Whales as a persistent animal, like Sea Spider. A prompt is arbitrary English plus an optional priority (urgent or normal); there is no command language. Mova's native deputy (mova_octopus::Deputy) reads it: Mova's annotator, the world tree reader and the configured capabilities. Sources reach a capability only as typed arguments bound through the parse: an absolute URL or a catalog identifier (slot kind id, a hyphen-joined lowercase slug with a letter and a digit such as smith-2024-tree-parsing; see Crab's deputy planner (mova-octopus), «Typed arguments»). The answer is English realized deterministically (answer.rs over mova_octopus::realize). No language model writes or interprets the answer.

Capabilities (handler whale, parameter operation; vocabulary in deputy.example.json):

operationslotexample promptsanswer
describesources: 1–8 URLs or ids"Describe this paper: smith-2024-tree-parsing.", "Please survey ID.", "Survey this paper: URL.", "Could you summarize ID?"queued at once: «I have queued 1 paper for description (octopus-r1-0): ID (“Title”). I am starting on it now.» Already described, waiting, failed before, or not in the catalog: said, nothing queued
count—"How many papers have you described?"«I have described 12 papers. 1 paper is being described now and 2 waiting.» plus failed and local-only attempts
statussource: optional"What is the status of ID?", "Tell me the status of URL.", "Are you busy?"«ID is waiting; 1 request is ahead of it.», «I am describing ID now (since …).», «I described ID at …: 4,512 words. The bundle is in …», a failed attempt with its reason
showsource: one"Show the description of ID.", "Show me the summary of URL."«My description of ID (“Title”) has 4,512 words; I wrote it at …. It begins: “…”. The full text is …/description.md. The bundle is in ….»
pause / resume—"Pause the Whales.", "Resume the Whales."writes or removes PAUSE in the run root: no new description starts; running ones may finish

Refused (nothing is queued; the deputy keeps a review candidate and the answer says «I did not act on this request because …»): negation ("Do not describe ID."), conditions ("If you can, describe ID."), unsupported verbs ("Delete ID.", "Stop the Whales." — stopping is Crab's), a missing or surplus source ("Describe the paper.", two sources to show), a source outside the requested action's parse ("Describe ID because ID2 has a copy."), declarative narration, and questions about an acting operation ("Is ID described?", "Are the Whales paused?", "Have the Whales resumed?"): only count, status and show (declared observes) answer questions. A sentence-initial "Survey ID." read as a noun is repaired to the imperative, and an identifier after a colon ("Survey this paper: ID.") belongs to the request.

How it works:

Reading papers

mova-whale paper CONFIG runs one Whale on one paper and asks its private terminal (mova-terminal, tools/terminal); --no-terminal journals the questions without asking. mova-whale paper-check CONFIG... validates several Whale configs side by side. The model is found through WORLD_EN_MODEL (or en.bin).

The private terminal interface (terminal.rs)

pub trait PrivateTerminal {
    fn animal(&self) -> &str;                               // == the Whale's name
    fn ask(&mut self, q: &Question) -> Result<Answer, AskError>;
}
Question { id, kind: "term" | "sentence", subject, text /* includes context */, context: Vec<String> /* <= 3 sentences */ }
Answer   { text, elapsed_ms, terminal_id: Option<String> /* mova-terminal question id */ }
AskError::Limit(reason) | Timeout(reason) | Failed(reason) | Unavailable

A Whale config names its terminal as "terminal": {"profile": ABSOLUTE_PROFILE_JSON, "poll_ms": 2000}: a mova-terminal profile (mova-terminal) whose animal is the Whale itself. run refuses a terminal of another animal, and holds ROOT/holder.lock of the profile for the whole run (TerminalLease), so a second Whale cannot attach; mova-terminal's own terminal.lock serializes each question. paper-check refuses configs that share a Whale, run root, profile file, session, owner marker or terminal root. Which model the session runs is the holder's command line (crab-tmux-worker … -- claude --model opus … by default, Codex with a gpt- model where configured), not the Whale's.

MovaTerminal is the real implementation, a thin adapter: terminal::ask(profile, text) sends the question (refused, with nothing sent, unless the pane is idle and owned, and while an earlier question is unanswered), then it polls terminal::answer every poll_ms and reads the screen through terminal::observe. A usage or rate-limit message on the screen (LIMIT_TEXT) returns Limit at once; answer_timeout_seconds without an answer returns Timeout. In both cases the question stays pending in the terminal's journal, is never re-sent, and the Whale asks nothing more. An answer counts only when its file exists and the pane is idle again. NoTerminal (dry run) and FakeTerminal (tests) start and ask nothing.

Learning on a paper

mova-whale learn CONFIG (schema mova-shark-learn-v1) keeps one Whale on one paper, round after round, until its own description passes every gate. The words come from Mova's reading and from what the Whale has learned; never from padding, never from the terminal's text.

State (the Whale's private state_root, kept across papers and batches): knowledge/items.jsonl (retained facts with provenance), knowledge/asked.jsonl (every question key ever sent, answered or not), knowledge/answers/SHA.txt (answer texts, kept only for the overlap gate), and per paper papers/<id>/journal.jsonl, status.json (learning, passed or stopped with its reason, first start, deadline) and round-NNN/ (description.txt, draft.json with every sentence's basis, gates.json, questions.jsonl, teaching.jsonl, metrics.json). learner.lock allows one learner per Whale; the terminal lease one Whale per terminal.

A round: read (once per invocation) → (topic corpus: seed) → write with all retained knowledge (and the pod's) → gates → (review hints when the round gained little, or before a pass) → pass: stop. Otherwise at most max_questions questions (4) about gaps never asked before: unknown terms the Whale has no facts for (half the slots, acronyms first), a part it could not restate at all, the longest prose sentences it could not restate. Each is asked once through its own terminal; Mova re-reads the answer and retains only typed facts (knowledge.rs): an acronym's expansion, «X is a Y», «X means Y», «X does Y» (object phrases of at most 5 words, below the overlap gate's 6), and for sentence and part questions subject–verb–object readings whose every word occurs in the source. Acronyms the paper defines itself («Grammatical Error Correction (GEC)») are understood natively and never asked. The next round writes with the new knowledge.

One call per round (batched, default): the round's questions go to the terminal as one numbered request asking for answers in shapes Mova's answer reader keeps, built from the source's own words: «X stands for …» (an abbreviation), «X is a …», «X does …» for a term, «In the paper's words: …» followed by simple subject–verb–object sentences that reuse the sentence's words for a sentence or a part. The answer is split by number (knowledge::split_answer) and each part read for its own question; every question key is marked asked on its own. Facts that only name a category («Goldberg is a surname», «the label»: knowledge::bare) are flagged for review (review.jsonl of the pod or the store), never kept or written.

The pod (pod, a shared directory for all learners of a batch; pod.rs): one store keyed by normalized term with provenance per Whale, paper, item and answer. A learner publishes its term knowledge and asked terms at its start and after every round, claims a term (under pod.lock) before asking it, and asks nothing another Whale has claimed or knows; the writer explains with its own items first, then the pod's (one sentence per fact relation, each bound to its item), and the overlap gate covers every answer behind the pod's facts.

Review hints (review, review.rs): when a round gains fewer than min_gain_words (150) words over the best earlier draft, and once before a draft passes, the Whale's own terminal receives the description and the text of the parts it describes (requests of at most max_bytes, 100,000 bytes, wrappers included; parts that do not fit go to further requests) and answers with hint lines MISSING: part | what, WRONG: quote | why, UNCLEAR: quote | why, REPHRASE: part | how, ORDER: advice. Hints are the paper's working material: papers/<id>/hints/round-NNN.json and .md, indexed by (description hash, paper hash) in index.jsonl, so the same pair is never asked twice. They guide the Whale's own questions (terms and sentences a missing/wrong/unclear hint mentions are asked first; journal hints lists what they chose and what was left aside — rephrase and order hints, since the writer restates in the paper's order); their text never enters a description: hint answers are kept with the terminal's answers, so the overlap gate refuses any copy.

Topic corpus (topic_corpus, off by default; topic.rs): the frozen mova-topic-corpus binary (tools/topic-corpus; a child process, since that crate depends on this one) seeds term facts from the paper's closed topic corpus before each round's questions — the corpus explains before the terminal is asked; the learner releases learner.lock for the seed only and stops (lock_lost) if it cannot take it back — and writes round-NNN/cited.md, the round's description with citations and a bibliography (public refuses closed sources). Journal seeded, cited. A child's failure is journaled and never stops learning.

Stops: passed; limit, timeout or failed at the terminal (no retry; a later invocation continues with new questions only — a sent question is never sent again); no_terminal (--no-terminal: questions journaled, not asked); nothing_to_ask; stalled (stall_rounds, 3, rounds in a row that gained no word over the best earlier draft after a round that learned no fact the writer can use); lock_lost; stop (stop_file present before a round — a batch sets its run root's STOP; not final, the next invocation continues); round_limit (max_rounds, 24 by default); deadline (hours from the first round, 12 by default, or deadline_unix if earlier). A paper stopped nothing_to_ask, stalled or round_limit under the current writer version is not reopened (journal not_reopened). Another stopped paper assigned again with a later deadline (a batch's retry) is reopened: the new deadline, max_rounds counted from there, journal reopened. import names earlier paper run roots of the same Whale: their answered questions become knowledge, every question they sent counts as asked.

The writer (clause.rs, compose.rs). Whole clauses from Mova's UD tree instead of skeletons: determiners, prepositions, auxiliaries and negation stay; coordinated verbs, side clauses and subject relative clauses become sentences of their own with their shared subject or antecedent; the authors' «we / our / us» becomes «the authors / their / them»; bracketed citations are left out; «X was made by Y» becomes «Y made X» where the verb form does not change. A clause may share at most 12 words in a row with its source: adverbial clauses and obliques are moved (fronted or put last) before optional modifiers are left out; negation is never left out; a clause that cannot be brought under the cap is not used. «X not only does A, but (can) also B» keeps both halves (the «but» half the parse hangs as an adverbial clause is the second half of the coordination; «not only» is left out of the first, never «only» alone). Table and figure captions are restated as «Table N shows …»; a sentence whose whole parse yields nothing is tried by its comma segments. The description: an opening (title, size, parts, topics), per part a lead that connects it to the previous one with its size and topics, its restated clauses in source order, each term explained right after its first use from the Whale's knowledge (««GEC» is short for Grammatical Error Correction.», «I understand «UAS» as the percentage of words.»), sentences learned through a question, the numbers the part reports, and a closing. Standalone title-case lines between prose open parts (PDF subsection titles).

Gates (gates.rs; each has a red control in its tests): words (min..=max), near_duplicates (Jaccard of content-word sets ≥ 0.8), verbatim (≤ 12 tokens in a row shared with anywhere in the paper; a structural sentence's «…» quotation of a title is exempt), answer_overlap (no 6-token sequence of any terminal answer, unless the paper has it), traceability (every content word belongs to the sentence's basis — its source sentences, its knowledge item, or the counts and titles it states — or to the fixed frame vocabulary compose::FRAME; a term may appear in another form of the same normalized term), structure_share (structural sentences ≤ 20 % of the words), grammar (every restated sentence, parsed again by Mova on its own, is a whole clause: clause::grammatical refuses a bare subordinated «-ing» form at the end — «…, while maintaining.» —, a participle after a determiner read as the verb — «This architecture all the previously derived subtrees» —, a plural noun read as the verb — «A number of parsing steps linear in …» —, a sentence without a finite verb by parse, tag or dictionary, and a closing function word; the writer applies it before it keeps a sentence). Parameters live in the configuration (gates).

The writer restates admitted source clauses and retained facts. Output length depends on how much of the source the reader covers and on the configured bounds; asking the terminal does not guarantee a passing description. When a description is short or ungrammatical, inspect its rejection diagnostics and source-bound mappings: Mova's parser gives some long scientific sentences a wrong root, formulas and tables are not restated, and modifiers left out for the verbatim cap shorten clauses.

Tests (cargo test, model required where they read): pod::tests (one term, one question across the pod; bare facts to review), learn::tests (one call per round; stalled with a red control at a high stall_rounds; an asked-out paper not reopened; review hints cached, stored, used for questions, kept out of descriptions, with a copied hint failing the overlap gate; the replay terminal), review::tests, topic::tests, batch::tests (rolling assignment with a scripted learner: a passed Whale takes the next paper at once while a failed one keeps its own; a failed paper given up at its deadline; a usage limit pausing every Whale; one runner per root, STOP, a restart continuing the open assignment; with the real learner and a fake terminal a restarted runner asks nothing again; selection by relevance and length, never alphabetical; broken extractions refused once, red with the limits off; topic corpus built, seeded with the lock released and cited, with a fake binary), reader::filter_tests (furniture, names, repair, extraction, each with sentences that must stay), knowledge::norm_tests, clause::correlative_tests and gates::tests::grammar_gate_refuses_fragments (red on the ma-2018 fragments). Negative controls, red then restored: the grammar gate always passing, the pod claim ignored, the stall off, the extraction check off; a terminal failure counted as final, no pod pause, no backoff, and assignment only when the whole pod is free (the old batch rule), each red on the rolling tests.

Tests: learn::tests (rounds retain and reuse knowledge and never ask twice across invocations; a limit stops cleanly; a low bound passes without a question; a past deadline starts no round; two runs identical), gates::tests (every gate red on its control), batch::tests, knowledge::tests, clause::tests, and tests/terminal_adapter.rs (an answer counts only when the terminal is idle again). Negative controls, red then restored: ignoring the asked set (a term asked three times), answers pasted into the draft, the adapter returning on the answer file alone.

The drafts yardstick

mova-whale drafts-audit OUT ROOT... (drafts.rs) reads every papers/*/round-*/draft.json under the roots (interrupted rounds left out) and counts, per class, over all rounds and over each paper's latest round: garbled (a line-end hyphen break such as «immigra-tion» or «immigration-the», a phrase repeated right after itself, a sentence Mova's parse does not cover), clipped (the grammar gate's fragment marks, a bare noun phrase fronted by a comma before a clause of its own, a «how/that» clause ending on a bare verb), gloss_repeated (a term explained twice in one description), gloss_conflicting (two definitions of a non-acronym with no content word in common), gloss_non_term (an adjective, adverb or word fragment explained) and near_duplicate (content similarity ≥ 0.6). (Resemblance as in broder-1997, text reuse as in clough-2002.) Each class reports examples, reasons and per-paper counts. The classes are heuristics independent of the writer's own gates, so text the writer admitted can show red; they do not judge meaning. Two runs write identical reports.

mova-whale drafts-recompose OUT --map LIVE=SNAPSHOT ... LEARN_CONFIG... writes each recorded paper again with the current writer from its pinned text and a copy of the knowledge (the Whale's store, then the pod's items), under OUT/<state>/papers/<paper>/round-recomposed/, so a change to the reader or writer is measured on the same papers with the same knowledge, with no learner.

Measured 2026-11-06 on the 220 papers of the Owl batches recomposed from one knowledge snapshot, writer-2026-11-04d -> writer-2026-11-06a: gloss_repeated 1,438 -> 0, gloss_conflicting 267 -> 0, gloss_non_term 1,105 -> 9, clipped 205 -> 174, garbled 524 -> 523, near_duplicate 974 -> 984 (restated sentences 64,277 -> 64,054, explanations 5,120 -> 2,335). On eight e-print papers with the same knowledge: $ in descriptions 581 -> 0, gloss_repeated 62 -> 0, gloss_non_term 46 -> 0, clipped 3 -> 2, garbled 6 -> 5.

Sources

Library ids are folders of the paper library; works marked «outside» have no card yet (docs/usage.tsv).

Epochs of texts

mova-whale epoch CONFIG [--epoch N] [--doc ID] [--no-terminal] (schema mova-whale-epoch-v1, epoch.rs) gives every document a growing set of texts. An epoch is just an increasing number; no epoch is special. Per document and epoch N:

  1. Mova writes (compose::compose_epoch, version compose::EPOCH_WRITER): first the description of the original exactly as learn writes it (compose), then what the document's neighbourhood says about its key terms (its most frequent nouns, its acronyms, the terms Mova did not know). The neighbourhood is the document's own texts of all earlier epochs (consultant texts, older and web descriptions, Mova's own) and the nearest documents of the global world, nearest first; each text is read whole by Mova's reader. Neighbour sentences are taken in rounds — every text's most relevant sentence, then every text's second — so the nearest texts all speak before one dominates; a sentence must speak of mova.min_relevance (2) key terms. Each clause passes the writer's guard (at most verbatim_max tokens in a row shared with the paper or any neighbour text except Mova's own earlier texts, no terminal-answer overlap, the grammar gate) and the near-duplicate check; nothing is padded. Every sentence ends with its citations ([n]) and the text closes with ## References (number, entry, citation id). Gates: mova.words (min..=max), verbatim, near duplicates, every sentence cited and every citation resolved, structure share. A text below min is recorded mova_short (other gate failures mova_gates), not registered, and nothing is asked for that document.
  2. The text is registered in the document's dossier (dossier.rs, kind mova).
  3. The Whale's own terminal writes (unless --no-terminal or no sol step): one question from a pinned prompt template ({{SOURCES}}, {{MIN_WORDS}}, {{MAX_WORDS}} exactly once; {{TITLE}}, {{DOC}}, {{EPOCH}}, {{WHALE}} optional; example: tools/whale/epoch-sol.example.md) listing numbered sources by path, never pasted: [1] the original, [2] Mova's text of this epoch, then the document's earlier texts (newest epoch first) and the key text of each nearest document (its latest consultant text, else its original; sol.neighbour_texts, 12). With stage_inputs (default) the files are copied into the terminal's root (inputs/<doc>/e<N>/), which every terminal sandbox may read. Only hard limits cut the list (max_inputs 64, max_input_bytes 8 MB); what is left out is journaled. The answer must cite only the given numbers (claims::check: a citation or reference entry outside them, or no citation at all, refuses the text) and have sol.words words; a refused text stays in the run root (sol-answer.md) and is never registered.
  4. The terminal's text is registered as kind sol, never public. Its sentences become claims (claims.rs), each followed to the sentences of the sources it cites: sourced (a cited source holds a sentence with content-word Jaccard ≥ min_support, 0.2), weak, uncited. With topic_corpus the text is also imported into the paper's closed topic corpus (sol-import, sol-claims), where its claims are followed into the library.
  5. Stop rule: an epoch whose terminal text adds no sourced claim earlier epochs lacked and is not longer than the best earlier one marks the document saturated (a record, not an error); later epochs skip it.

Dossier (dossier, rows {id, doc, epoch, kind: original|mova|sol|site-old|web, source, licence, licence_class, public_ok, sha256, words, created_unix, inputs, citations: {count, unresolved}}, files <doc>/texts/<epoch>-<kind>-<n>.md): {"kind": "world", "executable", "config"} uses the dossier store of the global world's tool through its CLI (dossier list|text|add CONFIG …; its originals may be compressed, so texts are read through it); {"kind": "files", "root"} keeps this module's own files (texts.jsonl appended under texts.lock, each file written once). Either way the same text of the same epoch and kind is registered once, the original is registered as epoch 0 when missing, and generated texts are never public_ok (a store that marks one public is an error).

Neighbourhood (world, the integration point with the global world, queried at the epoch being written, so it sees the texts of earlier epochs): {"kind": "world", "executable", "config", "radius", "max_docs"} runs the world tool's world neighbourhood CONFIG DOC --radius R --max-docs N --epoch E (documents, distances, visible texts), world related (edge kinds of the direct neighbours) and world neighbourhood … --content (every sentence of every visible text, rebuilt as one text per id); the document itself comes back at distance 0, and its texts other than the original are its own material. topic_corpus reads a paper's closed topic corpus (graph.json order: references, similar works, same authors, citing works; library texts from its registry); fixture reads DIR/<doc>.json ({"documents": [{doc, title, relations, distance, licence, public_ok, texts: [{id, kind, epoch, path}]}]}); none. Without a distance-0 entry the document's own texts come from the dossier. The driver does not build the world: before epoch N the world tool's world build --epoch N must have read the texts of earlier epochs.

Configuration (unknown fields refused, absolute paths): whale, run_root, epoch, documents ({"from": "batches", "path", "batches": [N…], "whales": [labels]} — the papers batches assigned, in their order; {"from": "catalog", …} — the batch selection batch::candidates over a pinned catalog, never alphabetical; {"from": "list", "items"}), max_documents (5), dossier, world, mova {words, gates, knowledge (a Whale state root whose facts explain terms), min_relevance}, optional sol {terminal, prompt {path, sha256}, words, stage_inputs, max_inputs, max_input_bytes, neighbour_texts, min_support} and topic_corpus.

State and resumption. run_root/epoch-N/journal.jsonl (fsynced) and per document run_root/epoch-N/docs/<doc>/ (status.json written before every step, draft.json, gates.json, mova.md, sol-prompt.md, sol-inputs.json, sol-answer.md, claims.jsonl); run_root/docs/<doc>/state.json (per epoch: words, new sourced claims; saturated_at) and claims.keys. A registered Mova text is not written again; sol_asking without an answer becomes uncertain and is never re-sent. A terminal limit, timeout or failure stops the run (sol_limit, sol_timeout, sol_failed; final for that document's epoch: the question may be pending in the terminal); nothing is retried. epoch.lock allows one driver per run root; a live run holds the terminal's holder.lock for the whole run, like a batch learner, so the two never share it.

Tests (epoch::tests, claims::tests, dossier::tests; fake terminal, fixture neighbourhood): the length gate keeps a short text out and asks nothing (control: a low minimum registers and asks); a citation to an unknown source is refused; a consultant text is never public (control: an original is); a resumed epoch writes and asks nothing again; sol_asking becomes uncertain without a question; a limit stops the run and is final; epoch 2 reads and cites epoch 1 and saturates on the same answer (control: a longer answer does not); unknown fields are refused; two runs write the same text; the world tool (a fake CLI) gives the dossier and the neighbourhood, and Mova cites the neighbour and the document's own older description. Negative controls, red then restored: accepting unknown citations, ignoring the document's own texts from the world, re-asking after sol_asking, generated texts public, saturation off.

Batches

mova-whale batch-plan CONFIG and mova-whale batch CONFIG [--no-terminal] (schema mova-shark-batch-v1) run a pod of Whales over papers with rolling assignment: each Whale works on its own paper, and as soon as that paper is final it takes the next one; no Whale waits for another.

Specification contract (spec, schema mova-shark-spec-v1)

A run configuration must contain "spec": {"path": ABSOLUTE, "sha256": HEX}. There is no built-in specification. The specification file is JSON with unknown fields rejected:

A word is a whitespace-delimited token containing at least one Unicode alphanumeric character. With project_oversized, a reply above max is first cut to its last whole paragraph, else sentence, whose count is within bounds; without such a boundary, or below min, it is a length-gate quarantine. A changed length rule needs a new profile identifier. Old editions keep their receipts and are never reinterpreted.

Run configuration (whale, whale-prepare, whale-status, whale-project-lengths)

Required fields:

Optional fields:

The run identity hashes the configuration together with the specification and template hashes. A changed specification, template, item or teacher therefore needs a new run root. A changed template or specification after preparation is refused before any call.

Swarm configuration (whale-swarm)

Required fields:

Optional fields are estimated_job_ms, poll_seconds, discovery_poll_seconds, discovery_limit, pending_per_worker and storage_floor_bytes.

The default discovery reads candidates {path, sha256}: a pinned JSONL queue of {key, lane, payload} rows, consumed from a durable cursor. An embedding binary can supply another Discovery through swarm::run_with.

Scheduling picks the lane with the largest deficit between its target share and its effective worker time, counted in milliseconds. In-flight jobs and unmeasured uncertain jobs are charged the estimate. Ties keep the configured lane order. With two lanes this is exactly the rule "choose the second lane if its time is below share × total". This is a long-run target, not an instantaneous guarantee.

The two roles launch different children:

Agent configuration (whale-research)

Required fields:

Optional fields are reference_bindings, scope_bindings, scope_manifest_equals, sidecar_suffixes, allowed_write_files together with write_file_rule, staged_guard, source_coverage, reference_metadata_only, reference_gap, task, timeout_seconds, max_retained_worktrees, retain_successful_worktree, target_dir and storage_floor_bytes.

The admission rules:

Crab

mova_whale::WhaleRoles implements mova_crab::angel::ExternalRoleHandler. Construct it with WhaleRoles::new(), or get an Arc with WhaleRoles::handler(), and install it with mova_crab::angel::serve_with_roles(config, WhaleRoles::handler()).

The pool reads Crab's run_root, whale_config and whale_executable. The swarm start reads whale_swarm_config and whale_swarm_executable.

Compatibility identifiers

Existing configurations, run roots and pinned specifications may carry shark names, so the engine accepts them as aliases of the whale ones; new files are written with the whale names.

The format identifiers are fixed: the schemas mova-shark-spec-v1, mova-shark-run-v1, mova-shark-paper-v1, mova-shark-learn-v1, mova-shark-batch-v1 and mova-shark-pool-v1, the pool owner marker prefix mova-shark-pool:, its shark_root field and the Crab pool directory shark-pools. They are format names, not display names.

Invariants

Checks

cargo test: the unit tests above plus the service tests (animal.rs: priority orders waiting work and never preempts; duplicates, failed sources and unknown sources queue nothing; PAUSE blocks admission; a started request without a receipt is recorded uncertain and not relaunched after a restart; a second service on the run root is refused), the realizer tests (answer.rs) and the length gate on a fixture specification with 4,000..5,000-word bounds (3,999 and 5,001 refused, 4,000 and 5,000 admitted, 5,002 with a sentence end projected to 5,000, 5,001 without one refused). MOVA_WHALE_SPEC_CHECK=PATH[:SHA256] also runs that gate against a deployment specification, and MOVA_WHALE_ANIMAL_CHECK=CONFIG:DEPUTY validates a service configuration and its catalog pin; both report a skip when unset.

English (tests/english.rs, with MOVA_OCTOPUS_TEST_MODEL=$MOVA_DATA/download/0/0.4/en.bin; a skip without it): 18 prompts map to the expected capabilities and arguments and 10 refusals fail at the expected stage with a review candidate. The served test runs the real mova-whale child for each request against a fake Codex executable and a fixture specification with 4,000..5,000-word bounds: a status question is answered within 1.5 s while a description runs; refused prompts queue nothing; an urgent request starts after the running one and before an earlier normal one; 4,500 words completes, 3,999 words and 5,001 words without a sentence end are quarantined; count, status, show, pause, resume and a repeated request id are answered as expected.

Negative controls, each turned red and then restored: arrival-only ordering (priority test red); recovery ignoring started lines, so an attempted request would wait again (restart test red); length bounds widened by one word (bounds test red); dropping "not"/"if" from the deputy's scope check (planner and served tests red: "Do not describe …" queued work).

Terminal adapter (tests/terminal_adapter.rs, tmux required; a fake bash LLM in a real tmux server on a private socket, never a model): the adapter returns the answer file; a usage-limit screen returns Limit within seconds and a following question is refused as a replay; a full Whale run asks through its own terminal and keeps the answer out of the description. Negative control: ignoring the limit text makes the limit test wait out the timeout (red).

Paper reading: a fixture paper with a fake terminal: the draft is journaled before the first question; the answer never reaches the description; a usage limit ends consulting; references are not read; teaching records carry licence admissibility; configs sharing a terminal are refused. Negative controls, red then restored: appending answers to the description; ignoring the limit (asking on); a lease that does not lock.

Limits

This page in Markdown → https://md.mova.elamur.ai/free/en/docs/tools/whale/