Skip to content

Composition Graphs

JSON files that wire stages together. Noether type-checks the entire graph before execution; you can dry-run any composition via noether run --dry-run <file>.

Graph format

{
  "description": "What this composition does",
  "version": "0.1.0",
  "root": { "op": "Sequential", "stages": [ ... ] }
}

Operators we use

Op Purpose
Stage Call a single stage by hash ID
Sequential Pipe each stage's output into the next stage's input
Let Run named bindings; the body sees the outer scope plus each binding's output as a nested field under the binding name
Parallel Run branches concurrently; outputs nested under branch names

We don't use Branch, Fanout, Merge, Retry, or Const in caloron's compositions today — they're available if a future composition needs them.

Compositions in this repo

File Status Purpose
full_cycle.json Live The phase pipeline: design → architect → dev → review → flatten. End-to-end planning.
sprint_plan.json Live Architect + dev only (no review). Lighter alternative for one-shot planning.
sprint_tick_core.json Live Pure per-tick loop, stateless
sprint_tick_stateful.json Live KV-wrapped sprint_tick_core: load state → tick → save
kickoff.json Aspirational One-shot DAG generation + Gitea issue creation
retro.json Aspirational Feedback collection + KPI computation + report
spawn_agent.json Aspirational Tiny wrapper around the shell's /spawn endpoint
sprint_tick.json Deprecated Original v0.1 stub; references stage IDs that haven't been valid since Noether v0.3

The aspirational ones are reference-quality JSON for what their respective compositions could look like once register_stages.sh and the relevant stages are wired up against your local Noether store.

Resolved-ID files (gitignored)

register_stages.sh and register_phases.sh write compositions/full_cycle_resolved.json and stage_ids.json (gitignored) with the SHA-256 hashes Noether assigned at registration time. Other compositions that want to reference these stages can either be generated similarly or pull from stage_ids.json.

Threading data through a Let chain — the key pattern

Each Let binding's output appears in the body's scope as a nested field, not flattened. So if poll: github_poll_events outputs {events, polled_at}, the body sees {...outer, poll: {events, polled_at}}. The next stage that wants events at the top level needs a tiny reshape stage to project it out:

# stages/sprint/project_poll_to_eval.py
def execute(input):
    return {
        "state": input["state"],
        "events": input["poll"]["events"],
        "stall_threshold_m": input["stall_threshold_m"],
    }

This is what stages/sprint/project_*.py and the build_tick_output stage do — small typed reshape stages between Let bindings and Sequential steps. They keep each domain stage's input contract narrow (no per-composition shape pollution) while making data flow explicit in the graph itself.

We considered three alternatives — pass-through fields on every domain stage, a generic untyped projection operator, and a monolithic sprint_tick_core single stage — and rejected each for reasons recorded in the v0.4.0 commit message.

Type checking

noether run --dry-run compositions/full_cycle_resolved.json

The output's type_check.input field tells you the exact shape the composition requires from its caller. Use this to design your caller's input record before wiring anything up.