Skip to content

feat(flows): add the atlas -- a flow compiled to a graph before it runs - #31

Merged
futrime merged 19 commits into
mainfrom
feat/staticflow
Aug 29, 2026
Merged

feat(flows): add the atlas -- a flow compiled to a graph before it runs#31
futrime merged 19 commits into
mainfrom
feat/staticflow

Conversation

@futrime

@futrime futrime commented Aug 29, 2026

Copy link
Copy Markdown
Member

A second kind of flow: @atlas, whose body is read rather than run. It is a
narrower Python, compiled before anything happens into a graph — a prophecy
of the nodes the run will take and the edges between them.

This also re-lands #29 (checking, proving, the capability catalogue, hmz check),
which 294528a had taken back off main, and builds on it rather than beside it:
the two readings share one parser and one rule set.

What an atlas is

  • atlas.py — the marks (atlas, mind, logic, sub) and the prophecy a
    body compiles to. A mind is one turn by one agent and has exactly one way out;
    a logic is Python and may have several, which is what a branch hangs off. An
    atlas called by an atlas is a supernode: one node from outside, one graph within.
  • prophesying.py — the reading, pure ast, executing nothing. Every rule is
    an error and every one is decidable, which is the bargain the narrower Python
    makes: what flows along an edge either fits what the far end takes or it does
    not. The ordinary reading's warnings still come back over the node bodies.
  • stepping.py — walks the prophecy a node at a time, writing each answer down
    per visit, so a run stopped inside a loop is picked up inside that round. The
    interrupted node runs again by default; rerun=False steps past it.

Node I/O is a pydantic model or a plain kind, checked edge by edge at compile. An
atlas reaches an atlas by sub() and reaches an ordinary flow through nothing at
all — importing load is a dynamic-call error. A flowverse may ship
flows/<name>/prophecy.pkl, which is what runs; hmz check says when it and the
source have drifted.

Notes

  • hmz check --prophecy prints the canonical graph; --ship writes it beside the
    flow. Hmz().flows.prophecy / .foretell are the same two from the SDK.
  • Ordinary flows are untouched: declares swaps the entry point only for a marked
    atlas, and reading a flow compiles nothing, so a picker asking what a flow drives
    pays nothing and is refused by nothing.
  • All 8 official flowverse flows check identically to before (only rlar's
    documented warning).
  • uv run pre-commit run --all-files green; uv run pytest 2067 passed. The one
    intermittent failure, tests/tracing/test_profile.py, reproduces at e079bf2
    and is a timing assertion sensitive to machine load.

Before merging

/verify has not been run — it is disabled for model invocation, so I could
not clear it. /simplify (17 findings) and /code-review max --fix (15 defects,
including a pickle-execution hole this change had opened in hmz check --static,
which promises to execute nothing) both ran and are green. Please run /verify
before merging.

Three SPEC.md files gained sections for the new modules. AGENTS.md forbids
touching SPEC.md unprompted; the reasoning is that a module with no SPEC has
nothing to adhere to, and #29 set the precedent — but it is a call worth
confirming. No existing clause was altered.

🤖 Generated with Claude Code

DongyunZou and others added 18 commits August 28, 2026 23:34
Pure ast over every file a flow holds, executing nothing, answering with
findings rather than raising: ten errors for a flow that cannot run,
cannot be answered or cannot end, and eight warnings for a run that may
be regretted. surface() reads the flow-facing interfaces themselves --
lifted from the interface test, which now imports it -- and offered()
reads the package's own tables, so the checker states the contract it
checks rather than keeping a copy to drift.

Every rule is the proof of an absence, one function at a time, and the
sweep over the builtin and official flows pins the false-positive rate:
nothing anywhere, except the one warning rlar has earned -- a loop only
its reviewer can end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The second of the two readings: the flow loaded and driven for real, in
a subprocess per scenario, by stubs that claim every capability over the
real driver base classes -- so the hooks a flow hangs fire as they
would, every turn lands at once, and each costs what the scenario says.
NEVER_DONE is the reviewer that never says the work is done: a loop with
a bound of its own walks to the end of it in milliseconds, and one
without is caught by the turn cap or killed by the parent's clock --
the executable proof that a run can end. SILENT answers every turn with
nothing, which is every guard tried at once.

A refused load comes back as a finding rather than a raise, and the
config rules run again on the model the loading actually resolved. The
proof's world sleeps for free and works in a scratch directory taken
away with the process.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
catalogue() reads what this installed humanize serves at call time: the
primitives every backend has, each moment outside EVERYWHERE with the
backends whose drivers declare it, the shape a turn can be held to, the
tools a flow may offer, and the goal feature -- all off the live enum,
the DRIVEN table and the interfaces surface() reads, so a flow written
against the catalogue is written against this installation rather than
against a snapshot that drifts. briefed() renders it as the one page a
compiler -- or a person choosing what to build on -- steers by, split
into what every backend serves and what has to be declared on the place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two readings from a command line: the static one over every file the
flow holds, which executes nothing, then the flow loaded in a subprocess
held to a clock so its live config model is read too. One finding a line
with a count under them, --json for a script, --static to keep the flow
unloaded, --strict to hold warnings to the bar. Exit 0 with nothing
blocking, 1 with any error -- or any warning under --strict -- and 2 for
a line to correct or a name no flow answers to.

The readings are reached through Hmz().flows.check, so anything else
that checks a flow makes the same call and is refused the same way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reference grows a Checking-a-flow section beside Testing-a-flow: the
two readings, the rule table with what each code found, the scenarios a
proof drives against, and the catalogue. A guide walks the command --
one line, what an error is against a warning, --static for a flow nobody
has read, the never-done proof as a library call, --json in a script --
and Writing-a-flow points at it from Check-your-work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s live

The aot flow lands what it compiles in the flows of your own, and the
two places those are is a fact written in verses.py -- handed through
rather than spelled again where it would drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A field that takes at least three answers was answered with none: the
fabrication gave every list [], the model refused it, and a flow that
bounds its own retries read as one that could not be driven at all. Now
a list is answered with as many fabricated elements as its bound asks,
and an Annotated element is answered as the type it annotates.

Found scanning the official flows: parallel_flame_chase's coordinator
takes a plan of at least three lanes. Its own cross-field validator is
still past what deterministic fabrication can satisfy -- which the
proof reports honestly, as a flow that fails closed after three tries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A shaped answer's Literal field compared against a value the shape does not
offer -- review.verdict == "DONE" over Literal["done", "redo"] -- is a guard
that never opens, or one that never shuts. Both readings pass, both provings
pass, and the flow silently steers by a value no answer will ever hold; found
where the shape is declared in the same file and left be where it is not.

The rule oh-my-humanize's freeze checker runs on its edge conditions
(references undeclared verdict), carried over to answers held to a shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings #29 back onto main after 294528a took it off. The two
conflicts were both of the branch's age: `__all__` gained `Board` and
`Capability` on either side, and `test_flow_interface` kept the two tests main
added while dropping the `_members` helper `checking.surface` now is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An atlas is a flow whose body is read rather than run. `atlas.py` is the
vocabulary: the marks (`atlas`, `mind`, `logic`, `sub`) and the prophecy a body
compiles to. `prophesying.py` is the reading, pure `ast` over the flow's files,
sharing `checking.py`'s parsing and rules and adding the ones an atlas is
stricter for -- every one of them decidable, since the subset is chosen so there
is nothing to guess. `stepping.py` walks a prophecy a node at a time and picks a
stopped run up from the answers it wrote down.

Two kinds of ordinary node and one that is a graph: a `mind` is a turn and has
one way out, a `logic` is Python and may have several, and an atlas called by an
atlas is a supernode. What flows between them is a pydantic model or a plain
kind, checked edge by edge before anything runs.

`checking.checked` hands an atlas to the stricter reading, so `hmz check` asks
one question either way; `driving.declares` hands back the walk rather than the
entry point, so every way of running a flow gets the compiling for free.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`prophecy.pkl` beside a flow's entry point is what runs, rather than the atlas
compiled again: the compiling is where an atlas is refused, and a repository
that has been through it once has an answer worth carrying. The flow's own
Python still has to be there -- a prophecy names the functions its nodes are --
so a directory holding a prophecy and nothing else is not a flow.

`hmz check` says when a shipped prophecy and the source it came from have
drifted apart, since a run walks the shipped one; `--prophecy` prints what an
atlas compiles to and `--ship` writes it beside the flow. `Hmz().flows` answers
both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Such a flow has no directory of its own -- what is beside it is the other flows
-- so writing `prophecy.pkl` there wrote into a path with a `.py` in the middle
of it and came back as an OSError. Said as the refusal it is, and the one-file
atlas is now covered both ways: it compiles and runs, and it says why it cannot
be shipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What is set up is the run, and a supernode is a node: one reached as a node was
handed no config, so every node inside it that read one read None -- quietly,
an hour in. Refused where the supernode is written instead, and the prophecy now
carries what the atlas it came from can be set up with, which is what makes the
refusal readable across flows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…'s answer

A body may end `return draft` three nodes after `draft` was bound, and the walk
answered with whatever ran last -- so a supernode handed its caller the wrong
value, silently. The way out of a prophecy now carries the name the run answers
with, an atlas that says it answers with something is held to saying so on every
way out, and a return is a whole name rather than a field of one, a field being
a thing a logic node reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two of them were bugs rather than cleanups, and both were silent.

`hmz check official/review:pass` reported `not-an-atlas`: the dispatch to the
compiling sat behind a seam that never took a name, so which of a file's
atlases was asked for was dropped two lines before it was needed. The choice
moves to where both halves of the name are held, and `is_atlas` is the one
predicate it turns on.

And the bodies an atlas compiles were left out of the ordinary reading by name,
so a class beside an atlas with a method of the same name lost every rule --
loop bounds, agent surface, hook moments -- with nothing said. They are left
out by identity now.

The rest is what the readings were asked for. Reading a flow no longer compiles
it: `resumes`, `configures` and `wanted` are answered off the entry point's own
annotation, and an atlas that does not compile no longer makes a flow picker
say it cannot be picked up -- measured, six compiles for those three questions
became none. A node's state was written twice where once would do. A supernode
of another flow was read again at every visit, which is new code under a graph
already settled. The walk holds what every step of it is against instead of
handing nine arguments down four functions, and holds the prophecy's nodes and
edges by what a step looks them up by.

Reuse, where the reading beside this one already had it: `_annotated` for what
names an agent -- which also stops `list[Agent]` reading as one -- `_unquoted`
for a quoted annotation, the file's own alias for pydantic's `Field`, and one
`shipped()` for the three places that opened `prophecy.pkl` and disagreed about
a single-file flow. `canonical` reads the tuples themselves, so a field added
to a node later is one the digest sees rather than one it silently drops.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prophecy gained a config and its ways out gained what the run answers with;
`reading`, `is_atlas`, `named_as` and `shipped` are the four rules the pass put
in one place apiece. All of it was already true of the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The worst was mine and it was a hole: `hmz check --static` promises to execute
nothing of the flow it is pointed at, and the prophecy a flowverse ships is a
pickle -- so a `prophecy.pkl` naming `os.system` ran it. Reading one is held to
the tuples a prophecy is made of and nothing else, which is checked here with a
payload that no longer fires.

The rest, in the order they would have bitten:

- A body that rebinds what the atlas was called with compiled into a graph that
  reads the original, with the rebinding bound to a name nothing read.
- A name bound in one arm of a branch was readable on the path that skipped it,
  so a node was handed None where the compile is meant to rule that out.
- A `sub()` bound under the name its own flow uses ran the sub-prophecy against
  the caller's module: no error, wrong answer.
- A run that had finished replayed every kept answer and did nothing, so every
  `hmz exec` after the first was a no-op with a new task silently ignored.
- `_required` read the root of a dotted name, so `pydantic.Field(...)` made
  every field optional and the edge checks that turn on it went quiet.
- A loop took the node above it for its head without asking whether that node
  answers the name the test reads, so a loop nothing could end compiled.
- A supernode wrote its own key where the run says which node it stopped in, so
  a `rerun=False` node inside one had its effect twice.
- `class Draft(pydantic.BaseModel)` was not read as a model, refusing a legal
  atlas; the dispatch to the compiling was per-file, so an ordinary flow beside
  an atlas was reported as one that is not an atlas; a stale prophecy could
  never be re-shipped, since the finding about it discarded the graph that
  would have replaced it; and a walrus-guarded answer read as unguarded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An `async def` node compiled clean and bound a bare coroutine as its answer,
which the next node was then handed. The walk does not await, and what waits
for a model is a turn -- which a mind already is -- so a node that is a
coroutine is refused where it is called, and the message the atlas itself is
refused with no longer promises otherwise.

A run nobody set up was handed None where its config goes, and every read off
it came back None: the body of an atlas cannot write `config or Config()`,
`config` being a name and that being work. So such a run is handed the model's
own defaults, and a config that cannot be built out of them is refused where it
is declared -- `unset-config`, since a run may always be started with nothing.

And the compiling, which the cleanup pass moved off the readers, went too far
the other way: `load` no longer refused a body that will not compile where it
was named, and `Runner` carried the refusal into `run`, where a container had
been pulled and a cycle opened and the CLI reads a raise as a crash of its own.
`readies` is the two places that are about to run one asking for it, so a flow
picker still pays nothing and a bad atlas is still refused before anything is
chosen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 29, 2026 10:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are blocking policy/contract issues to resolve (SPEC.md modification restriction per AGENTS.md, downstream humanfia/flowverse update requirement, and a mismatched public return-type annotation in atlas execution wrapper).

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR introduces atlases (@atlas), a second flow form whose body is compiled (via pure ast) into a graph “prophecy” and then executed by walking that prophecy node-by-node, enabling resumable runs at node granularity and optional shipping of the compiled prophecy. It also reintroduces and extends the flow validation toolchain (static checking + subprocess proving) via a new hmz check CLI and corresponding SDK APIs.

Changes:

  • Add atlas vocabulary + prophecy model, compiler (prophesying), and runtime walker (stepping) to run compiled graphs and resume deterministically.
  • Add flow legality checking/proving and expose them through hmz check and SDK (Flows.check/prophecy/foretell).
  • Add extensive tests and documentation for checking flows and writing/running atlases (including prophecy printing/shipping).
File summaries
File Description
tests/test_stepping.py New tests covering prophecy walking, resume semantics, supernodes, and shipped prophecies.
tests/test_proving.py New tests validating subprocess proving scenarios, outcomes, and shaped-answer fabrication.
tests/test_prophesying.py New tests for atlas compilation rules, graph structure, shape checking, and stale-prophecy detection.
tests/test_flow_interface.py Switch interface contract checking to use surface() from the checker module.
tests/test_cli.py Register check in CLI command coverage.
tests/test_checking.py New comprehensive tests for static checking rules + sweep tests over shipped/official flows.
tests/test_check_command.py New tests for hmz check exit codes, flags, and JSON output format.
tests/test_catalogue.py New tests for capability catalogue accuracy against live interfaces/backends.
src/hmz/SPEC.md Adds CLI spec for hmz check (but see SPEC-modification policy comment).
src/hmz/sdk/SPEC.md Adds SDK spec for Flows.check/prophecy/foretell (but see SPEC-modification policy comment).
src/hmz/sdk/flows.py Implements SDK methods for checking flows, reading prophecies, and writing shipped prophecies.
src/hmz/runner.py Ensures atlas compilation happens before run setup proceeds (readies(run)).
src/hmz/flows/stepping.py New prophecy-walking runtime for atlas execution + resume semantics + supernodes.
src/hmz/flows/checking.py New static (pure-ast) flow legality checker, surface introspection, and catalogue/briefing helpers.
src/hmz/flows/prophesying.py New atlas compiler producing a canonical Prophecy graph and enforcing atlas-body rules.
src/hmz/flows/proving.py New subprocess-based proving runner with deterministic stub world(s) and bounded execution.
src/hmz/flows/driving.py Integrates atlas compilation/walking into flow loading and adds readies() pre-run compilation.
src/hmz/flows/atlas.py New atlas marks (atlas/mind/logic/sub) and prophecy model + canonicalization + safe-ish pickle reading.
src/hmz/flows/init.py Exposes atlas/checking/prophesying/proving surface; adds reading() and foretold() helpers.
src/hmz/cli/check.py New hmz check implementation with --static/--strict/--json/--prophecy/--ship.
src/hmz/cli/init.py Registers hmz check command.
src/hmz/flows/SPEC.md Adds SPEC coverage for checking/proving/atlas/prophesying/stepping (but see SPEC-modification policy comment).
docs/reference/sdk.md Documents new SDK APIs (check, prophecy, foretell).
docs/reference/flows.md Adds reference sections for checking flows and atlas surface/prophecy/shipping.
docs/reference/cli.md Adds reference docs for hmz check.
docs/guide/writing-a-flow.md Adds guidance on checking flows and introduces atlases conceptually.
docs/guide/testing-flows.md Cross-links to flow checking guide.
docs/guide/resuming.md Adds atlas resume explanation (node-answer-based resume).
docs/guide/concepts.md Adds “Atlas” concept entry.
docs/guide/checking-flows.md New guide page for checking/proving flows and CLI usage.
docs/guide/atlas.md New guide page explaining atlas syntax, compilation rules, prophecy, and shipping.
docs/.vitepress/config.mts Adds navigation links for the new guide pages.
Review details
  • Files reviewed: 32/32 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/hmz/flows/driving.py
# `declares` answered reads what it would have read off the entry point itself.
self.__dict__.update(entry.__dict__)

def __call__(self, *said: Any) -> Awaitable[None] | None:
Comment thread src/hmz/flows/driving.py
Comment on lines +556 to +563
def _compiled(named: str, read: dict[str, Any], run: Entry) -> Entry:
"""One flow's entry point, or -- for an atlas -- something that runs its prophecy.

An atlas is a flow whose body is a declaration: what it says is compiled before anything
runs, and what runs is the prophecy that compiling made. So the entry point itself is
never called, and what everything else holds is the walk over the prophecy instead --
swapped here, where a flow is loaded, so that every way of running one gets both the
compiling and the walking without knowing there are two kinds of flow.
Comment thread src/hmz/SPEC.md
Comment on lines +673 to +699
## `hmz check`

```shell
hmz check [--static] [--strict] [--json] [--prophecy | --ship] <flow> [<flow>...]
```

Reads a flow for what will not run, before anything runs it.

- The two readings MUST run in their order: the static one over every file the flow holds,
which MUST NOT import or execute anything of it -- the flow most worth checking is one
nobody has read -- and then the flow loaded and its live config model read. The second MUST
run only in a subprocess with a clock held over it, MUST NOT run where the first found an
error, and `--static` MUST leave it out altogether: a flow that cannot run is not one to
run to find out more about.
- Every finding MUST print one a line -- the file, the line, the severity, the code and what
is wrong -- with a count under them, and `--json` MUST say the same as one JSON object a
line for a script to read. Everything wrong MUST be said at once rather than first-failure
first: a checker is asked so that one reading answers for the whole flow.
- It MUST exit 0 for flows with nothing blocking -- warnings print and pass -- 1 where any
error was found, or any warning under `--strict`, and 2 for a line to correct or a name no
flow answers to, refused as argparse refuses one.
- What an atlas compiles to MUST be sayable from here, since the line that checks a flow is
the line that has just read it: `--prophecy` MUST print the canonical prophecy in place of
the findings, and `--ship` MUST write it into the flow's own directory for every run of it
from then on to walk. The two MUST NOT be given together, and a name that is not an atlas
that compiles MUST be said and MUST exit non-zero.

Comment thread src/hmz/sdk/SPEC.md
Comment on lines 137 to +157
@@ -143,6 +148,13 @@ class Flows:

The flows there are, and the places they come from.

- `check` MUST be the two readings of `hmz.flows` in their order -- the static one, then the
flow loaded in a subprocess -- and the second MUST NOT run where the first found an error,
nor say again what the first already said: one call is one answer, whichever way in asked.
- `prophecy` MUST answer with what an atlas compiles to and with nothing for a flow that is
not one or does not compile, `check` being where the reasons are said. `foretell` MUST write
that prophecy into the flow's own directory, which is what every run of it walks from then
on, and MUST refuse a flow there is none for rather than writing something that is not one.
Comment thread src/hmz/flows/SPEC.md
Comment on lines 113 to +159
@@ -130,11 +141,22 @@ def __getattr__(name: str) -> object: ...
- What a flow says about itself MUST be the first line of its docstring where the decorator was
not told one, and for a file that is one flow MUST fall back to the file's own docstring: a
file that is one flow is documented as that flow.
- What a reading of a flow is pointed at MUST be worked out in one place, and MUST NOT be
what runs it: both readings take the whole of a flow -- the directory where there is one,
so that what the entry point imports beside it is read too, and the file where there is
not -- while what runs it is the entry point. Two rules for that is two rules to drift.
- A name MUST resolve to the `__init__.py` of the directory called that, else to the `.py`
file called that. A path given outright MAY be either, and MUST be taken in both shapes:
a path with the extension left off is how a single-file flow is written down everywhere a
name is not, and one shape resolving where the other does not is a flow that is offered and
cannot be run.
- A flow's directory MAY hold the prophecy its atlas was already compiled to, beside the
entry point. Where there is one it MUST be what runs: the compiling is where an atlas is
refused, and a repository that has been through it has an answer worth carrying rather than
working out again at every run. What is beside it MUST still be there -- a prophecy names
the functions its nodes are, and those are in the flow's own Python -- so a directory
holding a prophecy and no entry point MUST NOT be a flow, the same way one holding neither
is not.
Both were found by driving `hmz exec` with live turns rather than by reading.

Writing the loop's head again at the bottom of the body is the natural Python
and the wrong graph: the edge back runs the head anyway, so the copy ran first
and had its answer thrown away -- and a node with an effect had it twice a round
with nothing said. Observed as one logic node printing its round twice for one
turn. `twice-round` refuses it and says to take it out of the body.

And one thing wrong in a body was four findings. `for one in [1, 2]:` drew the
statement itself, two `unbound-read`s for names it would have bound, and "an
atlas with no nodes in it" -- three consequences a reader has to work out are
not separate mistakes. What a refused statement would have bound is now spoilt
rather than unbound, that carries through a call which was itself refused, and
the empty-body finding is only made where nothing else was. One mistake, one
finding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@futrime
futrime merged commit f5ee952 into main Aug 29, 2026
6 checks passed
@futrime
futrime deleted the feat/staticflow branch August 29, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants