# TOL Handbook

*TOL - the **Total Orchestration Language**. The reference for reading and writing TOL directly, by hand or with an AI - the same spec Topolog's own authoring models read.*

> **Authoring.** Write TOL however suits you: visually in the IDE, or directly - including with an AI. Point any model (Claude, GPT, Gemini) at this handbook, describe your goal, and paste the result into Expert Mode (Shift+E in the plan IDE). The engine validates every construct before it lands, so a plan that clears the validator runs identically whether a human, the IDE, or your own AI wrote it. Your TOL is the source of truth - the canvas, schedule, and forecasts are all views computed from it. (Declared field values survive editing; only comments and exact formatting are normalised - see "What round-trips byte-stable, what doesn't" below.)

---

## What problem TOL solves

You're planning a piece of work - a product launch, a research programme, a renovation, a hiring round, a financial year. It has tasks, deadlines, costs, dependencies, decisions you haven't taken yet, and outcomes you can't predict with certainty. You want to know how long it'll take, how much it'll cost, how likely it is to succeed, and what the best version of the decisions you're sweeping looks like.

TOL is the language you write the plan in. The Topolog engine reads it and gives you back a probability distribution over outcomes - cost, completion time, success probability, account-balance trajectory - along with a Pareto frontier across the decisions you've declared variable. The thing that makes TOL different from "just a planning tool" is that the language is shaped so the engine can answer those questions *analytically*, without ever running an unbounded simulation.

---

## Vocabulary

A few terms recur throughout. Skim once, refer back as needed.

- **Plan** - the structural document. A single `plan "..." { ... }` block declaring tasks, agents, deliverables, decisions, milestones, edges, and outcomes.
- **Task** - a leaf unit of work. Has an agent, a duration estimate, optional cost, optional effects on plan state.
- **Outcome** - a named result a task can produce (boolean / scalar / categorical). The engine tracks its distribution.
- **Agent** - who or what does a task. Internal / external / AI / (rare) universe. May be an individual, a pool, or a joint group. The literal `@open` is a runtime placeholder - the scheduler picks the cheapest available qualified member of an implicit pool determined by the task's area / role constraints.
- **Deliverable** - a typed object tasks consume, produce, or transform. Lives in the `deliverable_registry` (owned by the plan) or `unowned_deliverable_registry` (acquired from outside).
- **Decision** - a variable the engine sweeps over. Either continuous (gridded) or discrete (enumerated); see Decisions for the classification rule.
- **Monte Carlo iteration** - one stochastic run of the plan with all random draws realised. The engine runs many of them (typically thousands) to build the outcome distribution.
- **Pareto cell** - one point in the decision-space the engine sweeps. Continuous decisions are gridded; discrete decisions enumerate their cardinality. Each cell gets its own batch of Monte Carlo iterations.
- **Sentinel** - a terminal node of the plan-DAG. Tagged `success`, `failure`, or `partial`.
- **Edge** - a directed connection between nodes, optionally carrying a deliverable or a gate predicate.
- **Effect** - a declarative state delta applied atomically when a task completes (`mutate`, `transition`, `produce`, `consume`, `release`, plus the population verbs).
- **TOLScript** - the small total expression language embedded inside TOL at let / derived / when / produces / function-body positions.

---

## Totality, briefly

Every well-formed TOL plan terminates with a defined result in bounded time. You don't engineer for termination - the language guarantees it. Structurally: discrete decisions are capped at cardinality 32 per axis, deliverable instance counts come from literal integers or bounded decisions, user-defined references form acyclic dependency graphs (you can't define a thing in terms of itself, directly or transitively), evolution between events is closed-form (linear or exponential - no Euler stepping), and TOLScript cannot recurse, cannot use unbounded control flow (the lexer rejects `while`, `do`, `loop`, `goto`, `break`, `continue` and 22 other keywords as identifiers), and runs under a per-evaluation instruction budget.

If you find yourself reaching for a construct the language doesn't allow, that's usually a sign the model belongs in a solver, not in TOL.

---

## Plan shape

A minimal plan - goal, decisions to sweep, Pareto objectives, an outcome, a deliverable, an agent, a task, a sentinel, an edge:

```tol
plan "My Plan" {
  version: "0.1"

  // 1. Goal - one-line description of what the plan is for.
  goal { name: "Ship the thing" }

  // 2. Decisions - variables the engine sweeps over.
  decisions {
    budget:    money in [10000gbp, 50000gbp]
    promo:     boolean
    office:    enum[small, medium, large]
    headcount: integer in [1, 5]
  }

  // 3. Pareto objectives - what we're optimising.
  pareto {
    minimize: account.balance.trajectory.drawdown
    maximize: account.balance.trajectory.last
  }

  // 4. Outcomes - named results tasks can produce.
  outcome menu_done: boolean

  // 5. Deliverable registry - typed objects tasks operate on.
  deliverable_registry {
    Account {
      chained_type: chain
      parent_id: null
      properties { balance: money = 80000gbp }
      evolution  { balance: exponential(0.04 per 1y) }
      initial_instances: 1 { }
    }
  }

  // 6. Agents - who does the work.
  agent alice { type: internal }

  // 7. Tasks - leaf units of work.
  task design_menu "Design the menu" {
    agent: alice
    estimate: 4h
    cost: 200gbp
    produces: [menu_done]
  }

  // 8. Sentinel + edge - terminal node and the task's path to it.
  sentinel done { end_state: success }
  edge e1 design_menu -> done { carries: null }
}
```

### Available Pareto objectives

Dotted paths in the `pareto` block name derived plan quantities the engine computes per Monte Carlo iteration and aggregates across the iteration set. The headline ones:

| Dotted path | What it measures |
|---|---|
| `account.balance.trajectory.drawdown` | Running maximum drawdown of an `Account` deliverable's balance over the plan timeline. Use this to optimise against worst-case cash position. |
| `account.balance.trajectory.last` | Terminal balance at the plan's end. Use for "how much do we keep?". |
| `account.balance.trajectory.min` / `.max` | Running min / max of the balance over the plan timeline. |
| `plan.duration.mean` / `.p50` / `.p95` | Total plan duration (mean / median / 95th percentile across iterations). Use to optimise against completion-time risk. |
| `plan.cost.mean` / `.p95` | Total plan cost; same percentile shapes. |
| `p_success` / `p_partial` / `p_failure` | The probability the plan reaches a success / partial / failure **sentinel** (the terminal verdict) across iterations - the headline "P(success)" objective. A bare name, no aggregator. Distinct from `outcome.<id>.p_true` (one outcome's resolution): `p_success` rolls up the whole terminal-gate partition, so it is the right `maximize:` target when several outcomes compose into the verdict. |
| `outcome.<id>.p_true` | For a single boolean outcome - the probability it resolves true across iterations. |
| `outcome.<id>.mean` / `.p50` / `.p95` | For scalar outcomes - moments of the realised distribution. |

The general shape is `<noun-path>.<aggregator>`, where the noun-path resolves to a deliverable property, a plan-level quantity, or an outcome, and the aggregator is one of `mean`, `p50`, `p95`, `min`, `max`, `last`, `drawdown`, `p_true` (boolean outcomes only). Not every aggregator is meaningful on every noun; the IDE surfaces a picker.

**Noun-path naming.** Dotted paths use the **lowercased deliverable-type name** as the head when the plan declares a single instance of that type (e.g. `account.balance.…` for the `Account` registry entry with `initial_instances: 1`). The literal prefix `outcome.<id>.…` introduces an outcome path regardless of the outcome's own naming convention. `plan.…` is a built-in prefix for plan-level aggregates (`plan.duration.…`, `plan.cost.…`). Multi-instance deliverables and per-instance aggregates use a richer path shape the IDE composes for you; you can write them by hand too.

---

## Plan - top-level fields

| Field | Required | Notes |
|---|---|---|
| `"..."` quoted goal text | yes | Quoted string right after `plan`. |
| `version: "..."` | no | Author-supplied version tag. |
| `team_id: <ident>` | no | Identifier of the owning team. |
| `mode: design` / `mode: execution` | no | Lifecycle (see below). |
| `start_date: <ISO timestamp>` | no | Plan anchor, e.g. `start_date: 2026-01-01T00:00:00Z` (a full timestamp, not a bare date). Needed for absolute-deadline enforcement (below) and for any `start_at:` clock anchor. |
| `goal { name; description; status; deadline: <date> }` | no | Embedded goal; `deadline:` is the plan-wide target and the anchor for the `goal.deadline` shorthand. |

**Deadlines are advisory but checked.** A `deadline:` on a milestone (or the goal) does not hard-stop the plan, but once `start_date` is set the engine compares each milestone's modelled earliest-finish to its deadline and raises **HW-5 (deadline at risk)** when it is more likely than not to miss. Without `start_date` an absolute deadline cannot be mapped onto the schedule, so it stays purely decorative - set `start_date` if you want deadlines enforced.

### Plan lifecycle: design vs execution

A plan moves through two phases.

In **`mode: design`** the plan-DAG is allowed to contain cycles at non-leaf levels - for example, a `Decision` milestone with an outgoing "feedback" edge to an earlier `Research` milestone, modelling a real-world loop. Cycles between leaf-level tasks are still rejected, but the higher-level loops let you express the plan's causal structure honestly while you're still authoring. Monte Carlo, critical path, and Pareto sweeps cannot run in design mode.

In **`mode: execution`** all cycles have been resolved - either wrapped as `iteration` blocks (a cycle expanded into a bounded sequence) or removed. The leaf-DAG is acyclic and the engine can run.

Omitting `mode` is the legacy behaviour: equivalent to execution-strict (no permissive design-mode semantics). Mode lives on the plan rather than per-node so the engine has one consistent invariant set to enforce.

**Execution anchors the prior in reality.** Once a plan is executing, the engine keeps an append-only event log beside it and conditions every forecast on what has actually happened, so the spectrum is the distribution *given the past*, not the prior from t=0. You do not author this; it is captured on the /execute page. Marking a task done is treated as an observation that moves it across the **execution frontier** (past vs future): a mandatory gate collects the realised values that completion resolves - the decisions it enacted, its actual spend + time, and the realised outcomes / deliverables / hires / gate outcomes - which then pin the Money, Pareto, and forecast charts to the left of "now". You write the prior (this handbook); execution writes the facts. (Engine detail: the realisations live on each task's `observations` block; see `tol-spec.md` SI-59.)

---

## Decisions

A `decisions { ... }` block enumerates variables the Pareto sweep explores. Every decision row has the shape `<name>: <type> [in [<lo>, <hi>]]`. The type determines whether it's continuous (gridded) or discrete (enumerated).

```tol
decisions {
  starting_balance: money   in [1000gbp, 10000gbp]   // continuous
  growth_rate:      float   in [0.0, 0.2]            // continuous
  team_size:        integer in [1, 30]               // discrete (cardinality 30)
  run_promo:        boolean                          // discrete (cardinality 2)
  office_size:      enum[small, medium, large]       // discrete (cardinality 3)
}
```

The classification rule:

| Surface | Classification | Constraint |
|---|---|---|
| `money in [a, b]` | continuous | gridded at configurable resolution per axis |
| `float in [a, b]` | continuous | gridded |
| `integer in [a, b]` | **discrete** iff `(b - a + 1) ≤ 32`; **rejected at parse** otherwise | the engine enumerates every value |
| `boolean` | discrete (cardinality 2) | - |
| `enum[v1, v2, ...]` | discrete (cardinality = list length); **rejected at parse** if length > 32 | - |

There is no "continuous integer" surface today - every integer decision is bounded, and bounded integers with cardinality > 32 are rejected as combinatorial-optimisation territory (route those to a solver). If you genuinely want a real-valued sweep on what's logically an integer, declare it as `float in [a, b]` and round at the read site.

Continuous decisions are admitted only in value-expression positions (cost expressions, arithmetic, `let` right-hand sides). Discrete decisions are admitted everywhere - gate predicates, iteration generators, effect `when` clauses, distribution parameters - because within a Pareto cell their value is fixed and the engine's variance-reduction tricks still hold.

A free-variable reference in expression positions is written `?<name>` - e.g. `cost: ?starting_balance`.

**Coupling a decision to the forecast (decision → outcome prior).** A decision that only feeds task *costs* moves the money axis but not P(success) - so the optimiser just minimises spend and the frontier collapses to "always pick the cheapest". To give a decision real agency, layer a new outcome prior on it with `conditional_overrides` on the outcome: a `when:` gate over a discrete decision raises (or lowers) that outcome's `default_prior` inside the Pareto cells where it fires. Now "invest more" buys a higher success probability, and the sweep returns a genuine spend-vs-success trade-off.

```tol
decisions { gtm: enum[lean, balanced, aggressive] }

outcome pmf_strong: boolean {
  default_prior: 0.45                                  // lean GTM
  conditional_overrides: [
    { when: gtm = balanced;   apply: { default_prior: 0.65 } }
    { when: gtm = aggressive; apply: { default_prior: 0.85 } }
  ]
}
```

The coupling is applied per Pareto cell, so it shows in the **frontier** (the sweep), not in the single unconditional spectrum you see before sweeping.

**Coupling a decision to cost (decision → drawdown).** The complement to the outcome-prior link: give the decision a `cost_scale` clause so each value scales every task cost. Without it, the value that lifts P(success) is free and strictly dominates - a trivial frontier; with it, "invest more" genuinely costs more and you get a real cost-vs-return trade-off.

```tol
decisions {
  gtm: enum[lean, balanced, aggressive] cost_scale { lean: 0.8, balanced: 1.0, aggressive: 1.4 }
}
```

Now both axes move: aggressive GTM lifts P(success) and profit (via the prior coupling above) AND raises drawdown (via the cost scale), so the optimiser trades return against cash risk. The multipliers must be positive and their keys must be the decision's values (SI-44d). A boolean decision takes `cost_scale { true: 1.4, false: 1.0 }`.

---

## Pareto block

Declares the multi-objective optimisation surface. Plan-root only.

```tol
pareto {
  minimize: account.balance.trajectory.drawdown
  maximize: account.balance.trajectory.last
}
```

The engine returns the non-dominated frontier across all Pareto cells. There is no single "optimal" plan - the frontier is the answer; the operator picks a point matching their risk preference.

---

## Outcomes

```tol
outcome menu_done:    boolean
outcome cost_overrun: scalar { default_prior: { distribution: normal, mean: 0, std: 100 } }
outcome decision:     categorical { yes, no, defer }
```

Three types: `boolean`, `scalar`, `categorical { v1, v2, ... }`.

**`default_prior`** is the distribution the engine falls back to when the outcome is referenced (typically by a downstream gate predicate) on a Monte Carlo iteration where no producing task fired. The most common reason this happens is a gate cutting off the branch that would have produced the outcome - for example, an early-failure milestone short-circuiting the rest of the plan. Without a prior, the engine has to treat the outcome as undefined; with a prior, it samples from the declared distribution and keeps going. Required on scalar / categorical outcomes referenced from gates that may be reached without producers; optional on booleans (default: false).

Outcomes are produced by tasks two ways:

- **Bare list** - `task t { ... produces: [outcome_id, ...] }`. Use this for outcomes whose value is determined by the task firing at all (typical for booleans: "did the milestone happen?").
- **Explicit block** - `task t { ... produces { name: expr, ... } }`. Use this when the outcome's value is *computed* (typical for scalars: "how many signups did this campaign produce?").

A task may use one form or the other, not both. Rule of thumb: bare list for boolean flags, explicit block for value bindings.

**Reading an outcome - two shapes.** Inside a gate or an effect `when` clause, an outcome is referenced as `<outcome_id>.value` and reads the *current Monte Carlo iteration's* realisation (e.g. `gate: verified.value = true`). Inside a `pareto { ... }` dotted path, `outcome.<id>.<aggregator>` reads a *cross-iteration aggregate* (e.g. `maximize: outcome.menu_done.p_true`). The first lives inside one sample path; the second summarises across many.

---

## Deliverable registry

Deliverables are the typed objects tasks consume and produce. Two flavours:

- **`deliverable_registry { ... }`** - types the plan owns and constructs.
- **`unowned_deliverable_registry { ... }`** - types the plan acquires from outside (book a venue, hire a consultant) and may release. Same entry shape; semantically distinct.

Every deliverable entry has a `chained_type`:

- **`chain`** - an ordered state machine. Instances move through ordered states declared by *child* entries: the parent type (`chained_type: chain`) holds the identity; its children (`chained_type: state_set` with `parent_id: <parent>`) declare the legal states. Example: `Lease` (chain, parent_id: null) with child `LeaseState` (state_set, parent_id: Lease, predicates: `[draft, signed, archived]`).
- **`state_set`** - unordered set membership. Instances tag with any combination of states declared in `predicates: [...]` directly on the entry. Example: a `Customer` is `new`, `paying`, or `churned`.

The `predicates:` field is meaningful on state_set entries; on chain entries it's normally absent (the states live on the child state_set entry instead). A state_set entry **may omit `predicates:` entirely** when the type has no meaningful states - the entry then behaves as a populated typed bag whose instances carry only their properties. Use this shape for types you create-and-count but never tag (e.g. `Engineer { chained_type: state_set; initial_instances: 3 }` in the hiring idiom below).

```tol
deliverable_registry {
  // A money-typed chain with no states (just numeric properties).
  Account {
    chained_type: chain
    parent_id: null
    properties { balance: money = 80000gbp }
    derived {
      annual_interest := "balance * 0.04"
    }
    evolution { balance: exponential(0.04 per 1y) }
    initial_instances: 1 { }
  }

  // A state_set type with three declared states.
  Customer {
    chained_type: state_set
    parent_id: null
    predicates: [new, paying, churned]
    properties {
      monthly_rate: money
      signup_time:  time
    }
    derived {
      ltv          := "monthly_rate * 24"
      age_days     := "now() - signup_time"
      is_long_term := "age_days > 365"
    }
  }
}
```

**Properties** declare typed numeric attributes with optional defaults. Types: `money`, `count<T>`, `duration`, `time`, `int`, `float`, plus any author-declared quantity type.

**Derived** properties are pure TOLScript expressions evaluated lazily at the read site. They can reference primary properties and other derived properties on the same instance; they cannot mutate, cannot reference other instances, and the dependency graph must be acyclic.

**Evolution** laws specify continuous-time integration between events. Two closed-form families: `linear(<rate> [per <duration>])` and `exponential(<rate> [per <duration>])`. Zero exponential rates and non-positive `per` durations are rejected - a constant-value property is declared `linear(0)`.

**`initial_instances: <N> { <bindings> }`** seeds `N` instances of the type at plan start. The count must be a literal integer (no `?varname`, no `dist(...)`) - it's structural plan setup, not stochastic content.

**Abstract types + polymorphic queries.** A deliverable type may declare one or more parents via `is`. The parent can be either a concrete `deliverable_type` or an `abstract_type` - the same shape but with no instances of its own.

```tol
deliverable_registry {
  abstract_type Employee {
    properties { monthly_salary: money, hire_date: time }
  }

  deliverable_type Engineer is Employee, Agent {
    chained_type: state_set
    predicates:   [hired, productive, terminated]
    threads:      [coding, review]
    properties    { specialty: enum<frontend, backend, ml> }
  }
}
```

Queries against an abstract type are polymorphic: `query(Employee)` matches every concrete `is Employee` subtype (Engineer, SalesRep, Designer). Predicates inside the `where` clause can only reference fields declared on the queried type - to access subtype-specific fields, narrow with `as`: `(e as Engineer).specialty`.

Multi-parent is allowed, but parents' property declarations must be compatible: two parents may declare a same-named property only if it has the same type. There is no method-resolution order - only **property-contract intersection** - so diamonds are impossible. Use multi-parent when an entity belongs to two cross-cutting categories at once (an Engineer is both an HR-tracked Employee and a schedulable Agent; a SessionGuitarist is both a SessionCandidate and an Agent).

### Shareability - runtime contention over a deliverable

A deliverable entry may declare `shareability: N` to cap how many tasks may simultaneously occupy it. Positive integer, ≥ 1. Omit it (the default) for the overwhelming majority of deliverables - documents, decisions, sign-offs, software artefacts - where the underlying medium is effectively infinitely shareable and the question of contention doesn't arise. Declare it when the deliverable represents a single physical object, a venue, or a finite-stock resource that runtime tasks would queue for.

```tol
deliverable_registry {
  guitar     { chained_type: state_set; shareability: 1; predicates: [tuned] }       // one guitar
  workbench  { chained_type: state_set; shareability: 3; predicates: [setup] }       // three stations
  design_doc { chained_type: state_set; predicates: [drafted] }                      // omit → ∞
}
```

`shareability` pairs with a per-edge `(capacity: K)` annotation on each `consumes:` / `uses:` reference (see Tasks below). The two together let you express resources that admit *partial* occupancy - a workbench with 3 stations where one task needs 2 stations and another needs 2, so they can't both be in-flight. With both annotations omitted, plans behave as before: unconstrained shareability, unit capacity, no contention check.

The runtime check fires at **scheduler pickup**, not at plan validation: when you try to pick up a task, Topolog sums every active claim (`{in-flight ∪ pickup-candidate}`) against the deliverable's `shareability`; if it would overflow, the pickup is blocked with a message naming the contended deliverable. Plan-time validation only checks structural well-formedness (SI-68) - there is no "you forgot to declare shareability" warning because the unconstrained default is what most authors actually want and the check is non-vacuous only at the moment of contention. Cross-goal contention is by **pure type-name match** (a `workbench` in goal A contends with a `workbench` in goal B); conflicting declarations across goals resolve to the **MIN** (the most-restrictive author wins).

Forbidden on `chained_type: pool` - pool's `initial_count` plus `produce: N` / `consume: N` effects already encode fungible stock-and-flow; layering shareability on top would be redundant. For "three workbench stations" use a non-pool deliverable with `shareability: 3` and per-task `(capacity: K)`, not a pool.

---

## Agents

```tol
agent alice {
  type: internal
  kind: individual
  availability: schedule {
    mon { 0900-1700 }
    tue { 0900-1700 }
    wed { 0900-1300 }
  }
  cv: 0.2                      // coefficient of variation on duration / cost
}

agent design_pair {
  type: internal
  kind: joint
  constituents: [alice, bob]   // a fixed set who all participate
}

agent eng_team {
  type: internal
  kind: pool
  members: [alice, bob, carol] // any one of them, scheduled by availability
}

agent gpt {
  type: ai
  cost_per_call:     0.02usd
  cost_per_token:    0.00001usd
  inference_latency: 3s
}
```

`type:` is one of `internal`, `external`, `ai`, `universe`. `type: ai` agents support the three AI cost/latency fields. `kind:` distinguishes how multiple humans are combined: `individual` (one person), `joint` (a fixed group, scheduled by everyone's availability), `pool` (any one of N, scheduled by the first available).

**Threads decide what runs in parallel.** Each agent has one default execution lane (its *thread*), capacity one: two tasks owned by the same agent **serialise** on that thread even when they are structurally independent in the DAG. This is why a solo founder's plan is mostly sequential no matter how the tasks branch - one person, one thread - and why piling work onto a single agent is the usual cause of a plan that "takes forever". To model genuine parallel lanes for one agent, declare named threads (`agent dev { threads: [feature, hotfix] }`) and place a task on one with `thread: hotfix`. A `universe` agent has *unbounded* concurrency (the world does many things at once), so its iteration instances can overlap - see `concurrent: true` on iterations. Getting threads right is what separates an honest schedule from a fantasy where everything happens at once.

Pool agents may also be declared with the compact `pool` form:

```tol
pool eng_team {
  type: internal
  members: [alice, bob, carol]
}
```

The compact form is sugar; the IDE may emit either form on round-trip.

---

## Tasks

```tol
task design_menu "Design the menu" {
  agent: alice                              // a declared agent, or @open
  estimate: 4h                              // lognormal duration with the agent's cv
  cost: 200gbp                              // overrides agent default
  area: design                              // calibration bucket
  description: "..."
  consumes: [draft_lease]                   // owned-deliverable handles consumed at task start
  produces: [menu_done]                     // bare-list outcome producer
  precondition: design_brief_signed         // a gate that must resolve true
  acquires: [printer]                       // unowned deliverables acquired
  releases: [printer]                       // unowned deliverables released
  rationale: { confidence: HIGH }           // optional provenance metadata
  effects { /* see below */ }
}
```

Or with the explicit produces form:

```tol
task survey_pass "Survey" {
  agent: alice
  estimate: 4h
  cost: 200gbp
  produces { signups: 50, lifespan: 30d }
}
```

A task uses `produces:` (bare list) OR `produces { ... }` (explicit block) - not both.

**Atomicity: keep leaf tasks small (the 4-hour cap).** A task run by a human agent (`type: internal` / `external`) must have a mean `estimate` of **at most 4 hours** - the validator rejects anything larger (SI-2 / SI-70). A leaf task is one sitting of focused work, not a phase. Anything bigger is decomposed: a multi-day effort becomes a **milestone** of ≤4h tasks; a repeated unit (twelve partner onboardings, eight deals) becomes an **iteration**. `universe` and `ai` agents are exempt - a six-week procurement wait or a single model call is one modelled event. This cap is what keeps estimates calibratable and the schedule honest, and it is the most common authoring error - decompose first, size tasks second.

**Per-task `cv` (uncertainty).** Append `cv <n>` to an estimate to set that task's coefficient of variation - the spread of its lognormal duration - overriding the agent default: `estimate: 4h cv 0.5`. Calibrate it: routine, well-trodden work is **cv 0.2-0.3**; normal delivery **0.4-0.5**; novel, externally-blocked, or first-time work (a new acquisition channel, a regulator's response, launch day) **0.7-1.5**. One uniform cv across every task is a calibration smell - vary it by how much you actually know.

**Owned vs unowned deliverables.** `consumes: [...]` / `produces: [...]` move *owned* deliverables - types declared in `deliverable_registry`, which the plan constructs and destroys. `acquires: [...]` / `releases: [...]` move *unowned* deliverables - types declared in `unowned_deliverable_registry`, which the plan books from the outside world and gives back. The two pairs are surface-distinct because they have different costs (acquiring an external venue debits the account; producing an owned `Customer` does not).

**`uses: [h]`** is a **non-destructive** deliverable reference. Like `consumes:`, the task can't pick up until the handle exists; unlike `consumes:`, the deliverable is *not* destroyed at task completion - it persists for downstream tasks. Use `uses:` for tasks that read a deliverable's properties (peer review reading a Paper, an audition reading a SessionCandidate without "consuming" them) or that need a non-consumable resource (a guitar used during recording, not destroyed).

**Per-edge capacity `(capacity: K)`**. Any `consumes:` / `uses:` reference may carry an optional `(capacity: K)` postfix declaring how many units of the referenced deliverable's `shareability` budget this task claims for its entire pickup-to-done window. Positive integer ≥ 1; defaults to 1 when omitted (the common case - "I need one of these"). Pairs with the deliverable-side `shareability:` field (see Deliverable registry above) to model resources that admit *partial* occupancy.

```tol
deliverable_registry {
  workbench { chained_type: state_set; shareability: 3; predicates: [setup] }
}

task t_assemble { agent: a; estimate: 30min; consumes: [workbench(capacity: 2)] }
task t_finish   { agent: a; estimate: 30min; consumes: [workbench(capacity: 2)] }
task t_inspect  { agent: a; estimate: 30min; uses:     [workbench(capacity: 2)] }
```

With a budget of 3 and three tasks claiming 2 apiece, the scheduler admits any one alone but never any pair (`2 + 2 > 3`). Both `consumes:` and `uses:` references contribute to the claim sum - the consume-vs-use distinction is whether the deliverable persists after the task, not whether it occupies capacity during the task's window. The capacity postfix can also wrap inline property-match shorthand and `all(...)` queries - anywhere a reference is legal, `(capacity: K)` is too.

**`start_at: <time>`** pins a task to a wall-clock time. The task fires at `max(start_at, dependency_ready, thread_free)` - `start_at` is a lower bound, not a hard pin if upstream dependencies aren't ready yet. Use for fixed-time events: a wedding ceremony at 4pm regardless of preparation finish, a conference talk at a published slot, a market open at 9am. `start_at` must evaluate to a time at or after plan start (load-time check).

**Inline property-match shorthand.** Any deliverable-ref position (`consumes:`, `uses:`, gate predicates) accepts the inline form `<Type> { field: value, ... }` as sugar for `query(Type).where(t => t.field == value && ...).first()`:

```tol
task SubmitPaper "Submit" {
  consumes: Paper { state: draft }     // sugar
  // equivalent: consumes: query(Paper).where(p => p.state == draft).first()
  agent: pi
  duration: 2w
  effect: transition Paper to state submitted
}
```

The validator type-checks each `field: value` pair against the named type's registry entry. Useful for picking a single instance by a small set of property constraints without writing the full query.

**`agent: @open`** leaves the agent unbound. The scheduler resolves it at run time to the cheapest available qualified member of an implicit pool determined by the task's `area:` and any other declared constraints. Use this when the plan doesn't care *who* does the task as long as someone qualified is free.

### Effects

Tasks may declare an `effects { ... }` block holding declarative state deltas applied atomically at task completion. Ten verbs (counting `destruct` and `destruct into ...` as one verb with an optional `into` clause); every one admits an optional `when "<tolscript-source>"` guard.

The example below shows each verb in isolation - `kitchen`, `menu_v1`, `staff_x`, `alive_robots`, etc. are placeholder handle names. In a real plan they'd be declared deliverables in the registry or outcomes referenced by id.

```tol
effects {
  // the four essentials
  mutate kitchen.headcount   by 5
  transition menu_v1         to state signed_off when "review_passed"
  produce  menu_done
  consume  menu_v1
  release  staff_x

  // population / construction verbs
  construct Robot from chassis, battery { hp: 100, speed: 5 }
  construct_many Robot count: 5 from blueprints { hp: 100 }
  produce_many   Widget count: 12 { weight: 2 }
  destruct       old_unit
  destruct       old_unit into Scrap[3] { grade: 1 }
  transition_sample alive_robots count: 3 to state decommissioned
}
```

Effects within a block are an unordered set - two effects must not write the same `<handle>.<property>`. The `when` guard runs at task completion; if the predicate is false the entire effect is a no-op.

**`transition` vs `transition_sample`.** `transition <handle> to state <s>` moves a single named handle into state `<s>`. `transition_sample <set> count: <N> to state <s>` randomly draws `N` instances from a population type (e.g. all `Customer` instances currently in `paying`) and moves all of them to `<s>` together. Use the singular form when you have one specific instance in hand (an order, a lease, a contract); use the sample form when you're modelling churn or attrition over a cohort.

**Task-header vs effect-verb forms.** Two surfaces, related operations, differing in *when they apply* - and the spelling differs in a way that's easy to miss: the **header** form is plural-with-colon (`consumes: [h]`, `produces: [outcome]`, `acquires: [h]`, `releases: [h]`); the **effect-verb** form is singular-no-colon (`consume h`, `produce o`, `release h`).

- The header form takes effect at task **start** - `consumes:` reads the handle at start; `produces:` declares the task as the structural producer of the outcome (the value still resolves at completion).
- The effect-verb form takes effect at task **completion**, alongside every other effect in the block.

For most authoring, the two surfaces are equivalent: the deliverable-flow check treats them as cross-satisfying - the effect-block form satisfies the header form and vice versa. Both surfaces exist for historical reasons and the validator unifies them. Declaring the same handle on both surfaces of the same task is redundant but not an error - the validator merges them. The IDE emits the header form for the simple cases and the effect-block form when there's a `when` guard or other effect-block context.

**What runs at sample time (Phases 4-6 + A-E engine work, 2026-05).** The spectrum sampler maintains a per-MC-iteration **live handle world** that reflects every instance currently alive. Each event (`produce_many`, `construct`, `consume`, `release`, `transition`, `mutate`, …) updates this world at task completion. Author-visible consequences:

1. **`exists(EntityQuery {...})`, `count(...)`, `any(...)`, `all(...)`** gates evaluate against the live world. A gate like `count(EntityQuery { target: Paper }) >= 3` correctly fires only when three Papers are alive at the moment the edge is evaluated.
2. **Implicit producer-consumer ordering.** A task with `consumes: [X]` automatically depends on tasks that produce X - even with no explicit edge. The consumer's earliest-start is raised to the earliest producer's completion. Types with `initial_instances` declared skip this wait (supply assumed at t=0). Implicit edges that would close a cycle with the static DAG are skipped (the consumer fires without the wait - that scenario almost certainly indicates an authoring mistake the validator should catch first).
3. **Chain refs disambiguate instances.** `@track.composition.arranged` matches the oldest live track instance currently in state `arranged` (the *leaf* step is the state filter; intermediate steps describe registry-side hierarchy and aren't consulted at runtime). `@Paper[1]` picks the second matching instance after FIFO ordering. `Paper { state: draft, rank: 1 }` (inline property-match) narrows by multiple property pairs.
4. **Cardinality on `consumes:` is honoured.** `consumes: [Paper [3, 3]]` destroys 3 Paper instances per task fire (lower-bound convention). When fewer instances are available, the task fires with partial supply and emits `RT-CARDINALITY-NOT-MET`.
5. **`mutate <handle>.<property> by <value>` accumulates.** The instance's property is set to `(existing ?? 0) + value` at task completion. Downstream gates reading the property see the updated value. Numeric properties only - non-numeric `mutate` is a future extension.
6. **`acquire` / `release` lifecycle for unowned deliverables.** Types declared in `unowned_deliverable_registry { ... }` (alongside the owned `deliverable_registry`) follow check-out semantics: `acquire` marks the instance with an internal `_busy: true` flag; `release` clears it. Authors can query busy/free via `any(EntityQuery { target: Tool }, _busy = true)`. The flag survives across tasks in the same MC sample.
7. **Runtime issues surface in `spectrum.runtime_issues`.** When a consume finds no match, a mutate targets a missing instance, or any other silent no-op fires, the sampler emits an `RT-*` code aggregated across samples. The spectrum still returns successfully; these are advisory signals for the author. See the runtime-issue table below the SI table.

---

## Milestones

Concrete milestones group child items:

```tol
milestone MenuLaunch "Menu launch" {
  description: "Stages of getting the new menu out"
  deadline: 2026-09-01
  area: design

  task design_menu "Design" { ... }
  task print_menu  "Print"   { ... }
}
```

**Container edges need a leaf edge underneath (transmission).** When you connect two milestones at the container level (`edge A -> B`), you must also author a **leaf cross-edge** from a task inside A to a task inside B - the milestone-zoom view and the task-zoom view of the flow have to agree. Skipping it triggers SI-26 / SI-73 (a container edge with no leaf flow, or leaf flow with no container edge); a milestone whose children carry no transmitted edge triggers SI-75 / SI-76 / SI-77 (orphan / dead-end / unreachable tasks). Practically: when you split work into a milestone and wire it to its neighbours, route one real task→task edge across each boundary - the container edge is the bare summary, the leaf edge carries the gate. The IDE's Quick Fix synthesises these mechanically, but authoring them keeps the two zoom levels honest.

Parametric milestones are templates with type and/or value parameters, instantiated at `use` sites:

```tol
milestone Pipeline<Input, Output> (depth: int) "Pipeline template" {
  description: "Generic pipeline"
  task t1 "Process" { ... }
}

// call site (legal inside another milestone body):
use Pipeline<RawData, ProcessedData>("3")
```

Type args are bareword identifiers (deliverable type names or built-ins). Value args are quoted TOLScript expressions. The `use` graph is acyclic - a parametric milestone cannot instantiate itself directly or transitively.

---

## Iterations

Iterations express bounded repetition over a generator. The canonical form is the `over:` field; `max_count` caps the number of expansions and is mandatory.

```tol
iteration weekly "Weekly review" {
  over: schedule(every 1w, start 2026-01-05)
  max_count: 26
  template: milestone Review { ... }
}

iteration onboard "Onboard each existing customer" {
  over: query({ target: Customer, filters: [{ field: state, op: =, value: new }] })
  max_count: 200
  template: milestone OnboardingCohort { ... }
}

iteration customer_arrivals "Customers arrive over Q1" {
  over: arrivals({ distribution: poisson, rate: 5.0 })
  max_count: 1500
  template: milestone NewCustomer { ... }
}

iteration revisions "Revise the draft until it passes review" {
  over: until(draft_accepted = true)
  max_count: 8
  template: milestone Revision { ... }
}

iteration songs "Five tracks on the EP" {
  over: range(5)
  max_count: 5
  template: milestone TrackProduction { ... }
}
```

Five generator forms:

- **`range(N)`** - N independent expansions, indexed 0..N-1. Static - unfolds at plan time. (Distinct from the TOLScript builtin `range(lo, hi)`, which produces a list. Same name, different namespaces; the iteration-generator form takes one arg, the builtin takes two.)
- **`until(<gate_expr>)`** - keep expanding until the gate evaluates true. Bounded by `max_count`.
- **`query(<entity_query>)`** - one expansion per matching deliverable instance, evaluated at plan time. Use to iterate over an existing population (every `Customer` in `state == new`, every `Lease` past its deadline).
- **`arrivals({ distribution: poisson, rate: <r> })`** - temporal: Poisson event stream with rate `r` per unit time. Each Monte Carlo iteration draws its own arrival sequence within the plan's time window. Use to model customer arrivals, support tickets, equipment failures.
- **`schedule(every <duration> [, start <date>])`** - temporal: wall-clock periodic. Fires every `<duration>` from the start date. Use for monthly reviews, daily standups, quarterly board meetings.

Static generators (`range`, `until`, `query`) let the engine pre-compute the unfolded DAG once and reuse it across every Monte Carlo iteration - sample paths share topology, which is cheaper. Temporal generators (`arrivals`, `schedule`) are re-evaluated per iteration because the event stream varies.

**`concurrent: true` - parallel instances.** By default a `range` iteration's instances share their agent's single thread, so they *serialise* in the schedule even though they are structurally independent. That is right for a person (one founder cannot onboard twelve partners simultaneously) but wrong for a `universe` agent whose instances genuinely overlap - eight enterprise procurement cycles, N council reviews, N market-response delays all run in parallel in the real world. Add `concurrent: true` and each instance's **`universe`-agent** tasks get their own lane, so the plan span stops growing linearly in the instance count. It only re-threads universe-agent work; tasks owned by people stay serial, so the flag can never pretend one person is in eight places at once.

```tol
iteration enterprise_deals "Run N enterprise deals in parallel" {
  over: range(8)
  max_count: 8
  concurrent: true                 // the six-week procurement waits (a universe agent) overlap
  template: milestone Deal { ... }
}
```

---

## Edges, gates, sentinels

```tol
outcome verified: boolean

agent alice { type: internal }

milestone Launch "Launch milestone" {
  deadline: 2026-09-01

  task t1 "Setup"  { agent: alice; estimate: 1h }
  task t2 "Verify" { agent: alice; estimate: 30min; produces: [verified] }
}

sentinel done { end_state: success }   // success | failure | partial

edge e1 t1 -> t2   { carries: null }
edge e2 t2 -> done {
  carries: null
  gate: verified.value = true and t1.completed_at < Launch.deadline
}
```

Gates are pure boolean predicates over plan state. Admitted: outcome value comparisons (`<outcome_id>.value > 0.5`, `verified.value = true`), task lifecycle timestamps (`<task_id>.started_at`, `<task_id>.completed_at`), milestone deadlines (`<milestone_id>.deadline`), iteration tick predicates, and boolean combinators (`and`, `or`, `not`). Forbidden: effects, mutation, anything with a side effect - the validator enforces gate purity.

**Every leaf task reaches a sentinel (SI-10), and the terminal gates must partition.** Each outdegree-zero task must edge to a sentinel, so every path ends in a `success` / `partial` / `failure` verdict - plans are outcome-complete by construction. When several edges fan into the terminal sentinels over the same outcomes, their gates must be **disjoint and exhaustive**: exactly one fires on every realisation, or the forecast double-counts (overlap) or loses probability mass (gap). The canonical pattern composes boolean signals - e.g. with `pmf`, `profit`, `retain`:

```tol
edge e_ok   work -> s_success { gate: pmf = true and profit = true and retain = true }
edge e_mid  work -> s_partial { gate: pmf = true and (profit = false or retain = false) }
edge e_bad  work -> s_failure { gate: pmf = false }
```

These three gates are mutually exclusive and cover every combination, so P(success) + P(partial) + P(failure) = 1. Reach for this composition rather than a conditional `produces` over a categorical verdict (effect-value positions want numbers, not category labels). To make a decision move the forecast, layer the signals' priors on it with `conditional_overrides` (see Decisions). SI-9-G restricts sentinel-incoming gates to boolean / categorical outcome predicates precisely so this disjointness stays statically checkable.

---

## Plan-level `let` - the TOLScript bridge

```tol
let annual_growth = "0.04"
let monthly_burn  = "10000 * (1 + annual_growth)"
let cohort_size   = "if (run_promo) { 100 } else { 50 }"
```

Every `let` body is a quoted TOLScript source string. The TOL parser is non-evaluating - it preserves the source verbatim; the TOLScript evaluator runs the script at evaluation time against the surrounding plan state.

Use `let` for anything you want to compute once and reference from many places (a shared growth rate, a derived cohort size, a normalised metric).

---

## Named procedures

```tol
function add(a: int, b: int) -> int = "a + b"
function double(x: int) -> int       = "add(x, x)"
function ltv(monthly: money, months: int) -> money = "monthly * months"
```

Body is quoted TOLScript source. The call graph is acyclic (no direct or transitive self-call); TOLScript's runtime `RECURSION_BLOCKED` provides defence in depth.

---

## TOLScript essentials

TOLScript is the inner total expression language. Embedded inside TOL at:

- `let <name> = "<source>"` (plan root)
- `derived { p := "<source>" }` (deliverable_type)
- `produces { name: <expr> }` (explicit task produces - restricted to numeric / cost / duration literals; richer expressions live in `let` and are referenced by name)
- `effect when "<source>"` (any effect verb)
- `function f(...) -> T = "<source>"` (plan root)

Inline TOL effect and binding positions still admit the constant shapes (`5`, `30d`, `500gbp`) directly without quoting; reach for TOLScript when you need expression-level computation.

### Lexical features

Numeric: `42`, `-3.14`, `1.5e6`. Duration: `30min`, `4h`, `2d`, `12w`, `1y`. Money: `500gbp`, `0.02usd`. Percent: `50%`. Strings: `"hi"` (double-quoted, standard escapes). Booleans: `true`, `false`. Null: `null`.

### Expressions

```tolscript
// arithmetic
1 + 2 * 3
(a - b) / c

// comparisons (non-chaining - parenthesise compound forms)
x > 0
y >= z

// boolean
not (a and b) or c

// if-then-else
if (x > 0) { x } else { -x }

// multi-branch if/elif/else (sugar - lowers to nested ternaries)
if (score >= 6) { accept }
elif (score >= 4) { revise }
else { reject }

// records and field access
let r = { a: 1, b: 2 }
r.a

// lists and indexing
let xs = [1, 2, 3]
xs[0]

// fold over a list
fold (acc, 0, x in xs) { acc + x }

// for as statement
for (x in xs) { x * 2 }

// pattern match (literal + binding patterns)
match x {
  0 => "zero"
  n => "non-zero"
}

// chain-walk on records - extend / is / as
let signed = extend(draft_lease, "Lease") { signer: "alice" }
signed is Lease         // → true
signed as Lease         // → signed (or throws TYPE_MISMATCH)
```

**`extend` / `is` / `as`** are the chain-walk operators. `extend(<parent>, "<TypeName>") { ... }` produces a new record that merges the parent's fields with the bindings you supply and tags it with the named type. `<value> is <TypeName>` is a runtime type check (returns a bool); `<value> as <TypeName>` is the same check but throws `TYPE_MISMATCH` on failure and returns the value on success. Use these when you're moving an instance one step along its declared state machine - e.g., turning a `LeaseState.draft` record into a `LeaseState.signed` record by extending it with the signing-event fields.

### Functions

```tolscript
fn add(a: int, b: int) -> int { a + b }
fn double(x: int) -> int { add(x, x) }
double(7)             // → 14
```

Functions cannot recurse (direct or transitive).

### Builtins (partial list)

`abs`, `min`, `max`, `int_to_float`, `float_to_int`, `string_length`, `list_length`, `range(lo, hi)`, `random(...)`, `now()`. The distribution family names - `normal`, `lognormal`, `uniform`, `beta`, `gamma`, `exponential`, `triangular`, `bernoulli`, `categorical`, `poisson`, `mixture` - appear as the first argument of `random(...)` inside TOLScript or `dist(...)` inside TOL.

**`now()`** returns the simulated current time at the call site's evaluation point - *not* wall-clock time. The exact moment depends on where you call it: inside a task body let it's the task's start time on this Monte Carlo iteration; inside an effect parameter (or an effect `when` clause) it's the task's completion time; inside a derived property on a deliverable it's the read site's evaluation time; inside a gate predicate it's the upstream task's completion event time. Across all positions the rule is the same - `now()` is the time of the surrounding evaluation event on the current sample path. Banned in evolution-law rates (those must reduce to compile-time constants) and in plan-root constant `let`s.

**Cross-task time references.** Inside an expression you can reference another task's recorded time via `<task_id>.done_time()` (completion time on the current MC iteration) or `<task_id>.pickup_time()` (start time). The referenced task must be upstream in the DAG - forward references to a downstream task's time are a load-time error (a temporal cycle would let a task see its own future). Use this to drive rate curves, conditional effects, and Pareto objectives that depend on the timing of named milestones:

```tolscript
// RSVP arrival rate as a curve over time-since-invitation
let weeks_since_invite = (plan_t() - SendInvitations.done_time()) / 1w
let arrival_rate       = rsvp_curve(weeks_since_invite)
```

**Time helpers.** `day_of_week(t)` returns 1-7 (Mon-Sun); `month(t)` returns 1-12; `hour(t)` returns 0-23; `is_weekend(t)` returns a bool. Pure functions over `time` values. Use in gate predicates, derived properties, and TOLScript bodies for calendar-aware scheduling outside of arrival processes (which already accept piecewise-constant day-of-week rate schedules).

**Query slicing operators.** Method-chain queries support `.take(n)`, `.skip(n)`, and `.flat_map(λ)` alongside the existing `.where`, `.map`, `.order_by`, `.first`, `.last`, `.count`, `.sum`, `.min`, `.max`. Use `.take(n)` to limit to the first n results, `.skip(n)` to drop the first n, and `.flat_map` to flatten nested collections (e.g. all tranches across all funded grants). `n` must be a literal integer or a bounded decision.

```tolscript
// Best 3 positive experiments by creation time
query(Experiment)
  .where(e => e.result == positive)
  .order_by(e => e.created_at)
  .take(3)

// All tranches across all funded grants (flatten one level)
query(Grant)
  .where(g => g.state == funded)
  .flat_map(g => g.tranches)
```

### Distributional draws - recommended forms

Inside TOL itself (estimate / cost / property binding positions), use the inline `dist(<family>, <params>)` form:

```tol
task survey_pass "Survey" {
  agent: alice
  estimate: dist(lognormal, mean: 4h, cv: 0.3)
  cost:     dist(lognormal, mean: 200gbp, cv: 0.2)
  produces { signups: dist(normal, mean: 50, std: 10) }
}
```

This is what the IDE emits and what every example in this handbook uses.

Inside TOLScript expressions (let / derived / function / when bodies), `dist(...)` is not available as a function call - use the equivalent `random(<family>, ...)` builtin instead. The two are semantically identical; the surface differs because one is parsed by the TOL parser and the other by the TOLScript parser.

---

## Areas

`area:` is an optional tag on tasks, milestones, deliverable entries, and other items. Used for the Bayesian calibration multiplier. Free-form identifiers like `design`, `procurement`, `qa`.

---

## Rationale and provenance

Most authoring-relevant items admit an optional `rationale: { ... }` block holding LLM-provenance metadata (confidence, sources, search_used). The IDE renders this; it does not affect plan execution.

---

## Common authoring idioms

### Money trajectories

```tol
deliverable_registry {
  Account {
    chained_type: chain
    parent_id: null
    initial_balance: 150000                       // the Pareto/drawdown budget - REQUIRED
    properties { balance: money = 150000gbp }      // the mutable balance effects move
    evolution  { balance: exponential(0.04 per 1y) }
    initial_instances: 1 { }
  }
}

task pay_salaries "Monthly salary run" {
  agent: cfo
  estimate: 1h
  effects {
    mutate Account.balance by -50000gbp
  }
}
```

**Declare BOTH `initial_balance:` and `properties { balance: money }`.** They are different fields serving different layers, and the money model only works with both: `initial_balance` is the starting cash the Pareto money pass reads (omit it and the budget is **0** - `drawdown` and the cash trajectory all read zero, silently), while `properties { balance: money }` is the live balance that `mutate Account.balance` and gate predicates (`Account.balance < X`) actually read - omit it and every `mutate Account.balance` is a runtime no-op (`RT-MUTATE-NO-MATCH`, which `validate()` reports as 0 errors). Set both to the same starting figure.

The Pareto-objective dotted name `account.balance.trajectory.drawdown` is the running maximum drawdown across Monte Carlo iterations.

**Costs debit, revenue credits.** The money model folds in BOTH task **`cost:`** debits and explicit **`mutate Account.balance by +<amount>`** credits at each task's completion, so `account.balance.trajectory.*` (drawdown, end balance, insolvency) and `profit_mean` reflect real revenue-minus-burn - a plan that books its income is not mistaken for pure burn. (`total_cost` stays burn-only; it is spend, not net.)

**Tie revenue to outcomes with a `when` guard.** Prefer `mutate Account.balance by 30000gbp when "deal_won = true"` over an unconditional credit. The money pass counts a guarded credit at its EXPECTED value (`amount x P(deal_won)`), and because that probability is read from the outcome prior - which a decision's `conditional_overrides` shift - the revenue MOVES with your decisions. That is what makes the Pareto profit frontier non-degenerate: aggressive GTM raises P(pmf) -> raises expected revenue -> raises profit, instead of "every deal closes". The pass resolves simple guards (a boolean outcome, `= true` / `= false`, and `and`-conjunctions of them) and literal amounts; an `or` / `not` guard or a `dist(...)` amount is skipped (counted as zero) rather than mis-counted, so keep revenue guards simple and express "did we hit the target?" as a boolean outcome (e.g. `reached_profit`) gated on the work.

**Gate a sentinel on solvency (insolvency → failure, an *earned* forecast).** A sentinel-incoming edge may gate directly on a derived balance quantity: `edge convergence -> s_bankrupt { gate: account.balance.trajectory.last < 0gbp }`, with the success sentinel taking the complement (`... -> s_profitable { gate: account.balance.trajectory.last >= 0gbp }`). The spectrum replays each Monte-Carlo sample's cash position and fires the gated sentinel from THAT balance, so `P(failure)` carries the plan's real revenue-vs-burn risk - and the engine never overrides which sentinel fires: the sentinel stays ground truth, you simply opt in by authoring the gate. Available quantities are `account.balance.trajectory.last` (end balance), `.drawdown` (running max drawdown), and `.min` (lowest point). This `account.balance.*` comparison is the ONLY scalar a sentinel gate may reference - every other sentinel gate must still be a boolean/categorical decider. Because the comparison is evaluated per-sample (not over the assignment-enumeration space), a complementary `< X` / `>= X` pair on the same quantity is a valid disjoint partition, so `SI-9` / `SI-9-G` accept it.

### Churning cohort

```tol
deliverable_registry {
  Customer {
    chained_type: state_set
    parent_id: null
    predicates: [new, paying, churned]
    properties { monthly_rate: money = 50gbp }
    initial_instances: 100 { }
  }
}

iteration monthly "Monthly cycle" {
  over: schedule(every 30d)
  max_count: 24
  template: milestone CustomerCycle {
    task churn_step "Churn 10% of paying" {
      agent: ops
      estimate: 1h
      effects {
        transition_sample Customer count: 10 to state churned
          when "monthly_rate > 0"
      }
    }
  }
}
```

### Hiring decision

```tol
decisions {
  team_size: integer in [3, 10]
}

deliverable_registry {
  Engineer {
    chained_type: state_set
    parent_id: null
    initial_instances: 3 { }
  }
}

task hire_round "Hiring round" {
  agent: hr
  estimate: 2w
  effects {
    construct_many Engineer count: ?team_size from candidate_pool { }
  }
}
```

The Pareto sweep enumerates each `team_size ∈ {3, 4, 5, 6, 7, 8, 9, 10}` as its own cell.

---

## Validator codes you may see in the IDE

| Code | What it means |
|---|---|
| `SI-1` | Plan-DAG well-formedness - leaf-level acyclicity (in execution mode). |
| `SI-2` / `SI-70` | Task atomicity - a human-agent (`internal` / `external`) task's mean `estimate` must be ≤ 4h. Decompose larger work into a milestone or iteration; `universe` / `ai` agents are exempt. |
| `SI-10` | Sentinel reachability - every outdegree-zero task must edge to a sentinel; no dangling leaf work. |
| `SI-13` | Edge cardinality compatibility - what an edge carries matches both endpoints' declared types. |
| `SI-14` | Edge target is reachable from the source via the gate predicate. |
| `SI-23` | Gate-query cycle prevention - a gate predicate cannot depend on a node downstream of the gate. |
| `SI-9-G` | Sentinel gate shape - edges into a sentinel may only gate on boolean / categorical `outcome_predicate`s (combined via `and` / `or` / `not`), or be ungated. Lift count / time / scalar conditions into a producing task that emits a categorical bucket so SI-9's rollup-disjointness check has a complete decision procedure on the edges that decide which sentinel fires. |
| `SI-26` | Mode-aware milestone-edge transmission rules. |
| `SI-31` | Gate purity - gates have no Effect-class terms. |
| `SI-32` | Evolution-law well-formedness - finite rate, non-zero exponential, strictly-positive `per` duration, one law per property. |
| `SI-33` | Effect disjointness - two effects in a task must not write the same `<handle>.<property>`. |
| `SI-34` | Produces / `effects { produce }` agreement - the validator unifies the header and effect-verb surfaces; neither may contradict the other within the same task (e.g. `produces: [x]` on the header and `consume x` in `effects { }` is rejected). Same-handle on both surfaces is redundant but not an error. |
| `SI-36` | `fold` first-argument is a handle-set expression (not a scalar literal). |
| `SI-40` | Parametric milestone resolution + arity + call-graph acyclicity. |
| `SI-41` | Time-window query well-formedness - `at_time_window(t_start ≤ t_end)`, `state_held_for(_, d ≥ 0)`. |
| `SI-42` | Named-procedure call graph is acyclic. |
| `SI-43` | Derived-property dependency graph on a deliverable type is acyclic. |
| `SI-44` | Decision-position rules - continuous decisions only in expression positions; discrete admitted everywhere. |
| `SI-55` | Subset-transition well-formedness - `transition_sample` count non-negative; `target_state` in the type's declared state set. |
| `SI-56` | Effect `when` clause is a pure boolean TOLScript expression. |
| `SI-57` | Discrete-decision cardinality ≤ 32 per axis. |
| `SI-58` | Every consumed deliverable handle has a matching releaser somewhere in the plan. |
| `SI-72` | This plan participates in a cross-workspace dependency cycle. Every plan in your portfolio is internally acyclic, but cross-workspace edges close a loop across the union DAG so the meta-scheduler cannot find a schedule. The federation-level meta-scheduler is the authority; the IDE surfaces SI-72 in the Problems drawer so it agrees with the orange "1 ERROR" pill on the /plan list card. Fix by removing or inverting a cross-plan edge, or by moving one of the involved plans to Design (which removes it from the active pool). |
| `SI-73` | Bottom-up edge transmission - the dual of SI-26. If a Task `t_a` inside container `A` has an edge to anything in container `B`, then a container-level edge `A → B` must also be authored. Catches **under-authoring**: leaf-level flow exists but the collapsed-milestone view of the plan doesn't show it. The milestone-zoom view and the leaf-zoom view of the plan must agree about flow. Iteration sources are exempt (same as SI-26). Hard in execution mode, warning (HW-73) in design mode. **Auto-fix:** the IDE Problems drawer surfaces a "Quick Fix" button on every HW-73 / SI-73 row that mechanically synthesises every missing container edge - each synthesized edge carries the OR of the backing leaf-edge gates so SI-9 (sentinel mutual-exclusion) sees the same firing surface at the milestone-zoom view as at the leaf-zoom view. See `lib/tol/quickFix.ts::computeHW73Fixes` for the IDE auto-fix and `lib/tol/migrations/hw73UnderAuthoredEdges.ts::migratePlanForHW73` for the one-shot data-backfill that applies the same patch to plans authored before SI-73 landed. |
| `HW-74` | Per-level Miller's Law cap (advisory). Every container in the plan hierarchy (plan root, each Milestone, each Iteration template) has at most 7 non-sentinel direct children. Sentinels are exempt; Iterations count as one child at their parent level. Fix by re-decomposing the offending container into 2–7 sub-containers grouped by theme. Promotes the AI-authoring 1-7 guideline to a structural advisory. Always a non-blocking HW-74 warning in BOTH design and execution - an over-broad sibling set is a cognitive-load smell, not a correctness defect, so it never blocks Resolve & Promote / Activate. (The synthesised terminal-partition join lands at plan root and can itself tip a 7-child plan to 8.) |
| `SI-75` | Transmit-down completeness - incident form (legacy). When a Milestone has external (non-sentinel) edges - either container-level inbound/outbound or cross-container leaf edges - every direct Task child of that milestone must be incident on at least one leaf-level edge. Catches the fully-orphan shape (indeg=0 AND outdeg=0); the directional companions SI-76 / SI-77 catch the dead-end / unreachable shapes. This promotes the authoring-algorithm Transmit op (`authoring-algorithm.md` §5) to a structural invariant: when a container is decomposed via Split → Transmit → WireSiblings, every child becomes the endpoint of at least one transmitted leaf edge per active direction. Skipping Transmit produces orphan children - tasks that visualise as floating cards on the IDE canvas because no leaf edge incident on them carries the parent's flow down. Tasks at plan root, tasks inside iteration bodies (per-tick semantics), and tasks inside truly-isolated milestones (no external flow) are exempt. **Auto-fix:** the one-shot data-backfill `lib/tol/migrations/si75TransmitDown.ts::migratePlanForSI75` mechanically synthesises transmit-down leaf edges via a five-priority source-selection ladder (existing inbound transmit pattern → existing outbound → container-level inbound → container-level outbound → any cross-container leaf flow); the same backfill simultaneously satisfies SI-76 and SI-77 by adding both inbound and outbound edges as needed. Hard in execution mode, warning (HW-75) in design mode. |
| `SI-76` | Outdegree completeness - directional companion to SI-75. For every Task `t` whose containing milestone has at least one external outbound *non-sentinel* edge, `t` must have outdeg ≥ 1 at the leaf level - i.e. there must be at least one leaf edge starting from `t`. Catches the dead-end shape (indeg>0 AND outdeg=0): a task that satisfies SI-75's incident requirement but renders as a stub-tailed card with no arrow leaving it, because the Transmit-down outbound step was skipped during decomposition. Sentinel destinations are excluded from the trigger (SI-10 already enforces task-to-sentinel reachability). Exemptions and auto-fix are the same as SI-75 - the migration adds an outbound leaf edge per affected task via priorities 2 / 4 / 5'. Hard in execution mode, warning (HW-76) in design mode. |
| `SI-77` | Indegree completeness - directional companion to SI-75. For every Task `t` whose containing milestone has at least one external inbound edge, `t` must have indeg ≥ 1 at the leaf level - i.e. there must be at least one leaf edge ending at `t`. Catches the unreachable shape (indeg=0 AND outdeg>0): a task that satisfies SI-75's incident requirement but renders with no inbound arrow and can never be triggered in the leaf-level DAG, because the Transmit-down inbound step was skipped during decomposition. Exemptions and auto-fix are the same as SI-75 - the migration adds an inbound leaf edge per affected task via priorities 1 / 3 / 5. Hard in execution mode, warning (HW-77) in design mode. |
| `SI-IPM-1` / `SI-IPM-2` | Inline property-match (`<Type> { field: value }` in `consumes:` / `uses:`) - type must exist; fields must be declared on the type or its parents (or the special `state`). |
| `SI-TTR-1` / `SI-TTR-2` / `SI-TTR-3` | Cross-task time ref (`<task_id>.done_time()` / `.pickup_time()`) - referenced task must exist; cannot reference self; cannot reference downstream. Walks `precondition`, `effects`, and `start_at`. |
| `SI-MP-1` / `SI-MP-2` / `SI-MP-3` | Multi-parent `is X, Y` - every parent must be declared; same-named property contracts across parents must have compatible types; the `is` graph must be acyclic. |
| `SI-Q-1` / `SI-Q-2` | `EntityQuery.take` / `.skip` must be non-negative literal integers. |
| `SI-SA-1` / `SI-SA-2` | `task.start_at` consistency - `SI-SA-1` flags `start_at` resolving to before `plan.start_date` (negative minutes); `SI-SA-2` flags absolute-timestamp `start_at` with no `plan.start_date` to anchor it. |
| `HW-5` | Deadline at risk (advisory) - a milestone's modelled earliest-finish is more likely than not to miss its `deadline:`. Only fires when `plan.start_date` is set (the absolute deadline needs the anchor). |
| `SI-LT-1` | Uses-after-consume - a task with `uses: [X]` whose ancestor on every DAG path has `consumes: [X]`. The handle is destroyed before the using task can read it. Refined by inline-property-match disjointness: filters like `Paper { state: draft }` vs `Paper { state: submitted }` target disjoint instances and don't trip the rule. |
| `SCRIPT-PARSE` | Embedded TOLScript source (in a `let` / `derived` / `function` / `when`) failed to parse. |
| `HW-*` | Non-blocking health warning. Plan still runs; the IDE flags the issue. |

## Runtime issues you may see on a spectrum

In addition to `SI-*` (load-time) and `HW-*` (health), the spectrum sampler emits `RT-*` issues when it silently no-ops a handle-mutation event at sample time. Issues land in `spectrum.runtime_issues` aggregated by `(code, task_id)` with a `count` recording how many MC samples observed the issue. The spectrum still computes; these are best-effort signals that something the author wrote did not fire as intended.

| Code | Severity | Fires when |
|---|---|---|
| `RT-CONSUME-NO-MATCH` | error | Destructive op (`consume` / `destruct` / `construct from inputs`) found no live instance. Often "over-consume" - a prior task consumed the only available instance, or no producer fired. |
| `RT-RELEASE-NO-MATCH` | warning | `release` found no live instance (the handle was never acquired or already released). |
| `RT-ACQUIRE-NO-MATCH` | warning | `acquire` found no live instance to acquire. |
| `RT-TRANSITION-NO-MATCH` | warning | `transition` target handle does not exist. |
| `RT-MUTATE-NO-MATCH` | warning | `mutate <handle>.<property> by <value>` target handle does not exist. |
| `RT-CARDINALITY-NOT-MET` | warning | A consume like `[Paper [3, 3]]` needed 3 instances but found fewer; the task fires with partial supply. |
| `RT-NO-PRODUCER` | warning | A consumer's wait-for-handle floor had no reachable producer for a consumed type with no initial supply. Effectively the consume can't be satisfied this sample. |

A plan must pass all `SI` checks before the engine runs. `HW` codes are advisory.

---

## Direct-editing notes

- **Statement separators.** TOL accepts both newlines and `;` between fields inside a block - `agent: alice; estimate: 1h` on one line is equivalent to the same fields on two lines. The renderer prefers newlines for top-level fields and `;` for the compact one-line declarations the IDE emits (`agent alice { type: internal; kind: individual }`). Both round-trip.
- **Identifiers** are case-sensitive `[A-Za-z_][A-Za-z0-9_]*`. Type names by convention start uppercase; agents / tasks / outcomes lowercase.
- **Comments** are `//` line comments and `/* ... */` block comments.
- **Quoted strings** use double quotes with standard escapes (`\"`, `\\`, `\n`).
- **Quoted TOLScript source** uses the same string conventions. Multi-line scripts are written on one line with `\n` escapes, or by concatenating adjacent string literals.
- **`schema: { ... }` on deliverable entries** is a legacy v3 JSON-Schema-shaped block predating the `properties { }` form. The parser still admits it for back-compat, but the IDE emits `properties { }` exclusively; ignore `schema:` on read and prefer `properties { }` on write.
- **What round-trips byte-stable, what doesn't.** All *declared field values* (every `<name>: <value>` field, every block body, every nested AST node) survive parse → render → parse identically. *Comments and whitespace formatting* don't - `//` line comments and `/* */` blocks are stripped at parse time and not re-emitted; the renderer canonicalises indentation, integer-vs-float trailing zeros, and trailing-comma placement. If you need a comment to survive, put it in a `description:` or `rationale:` field instead.

---

## What TOL is not

- **Not Turing-complete.** This is the central design choice. If you need unbounded computation, the model belongs in a different system.
- **Not a general DSL for combinatorial optimisation.** Discrete decisions are capped at cardinality 32 per axis. Route job-shop or large-permutation problems to a solver and import the result as a fixed input.
- **Not a temporal logic.** Time-window queries (`at_time`, `state_held_for`) read from the executor's event log; they don't model speculative futures.

---

## Where to go next

The Topolog IDE is the supported authoring path. Use it. If you find a TOL construct the IDE doesn't surface and you've reached for direct editing, that's a UI gap worth reporting.
