orchestrator.yaml lives at the repository root and declares the
schedules for periodic role dispatches, gate-less
"sweeps" that are not tied to a pipeline run. The first (and currently
only) scheduled role is the Historian, which periodically reconciles
documentation with run artifacts. This file is committed project policy,
not per-instance configuration: any orchestrator instance pointed at the
same repo derives the same sweeps.
Where and how it is read
The v1 orchestrator reads orchestrator.yaml from the
default-branch tip, never from a checkout and never
from the current branch. On each tick, the scheduler calls
this.git.show(defaultBranch, 'orchestrator.yaml'). If the
file is absent, the scheduler returns an empty schedule list. Parse
errors are logged, never guessed at. That is the bounce rule applied to
config.
Key: schedules
A map of role name → schedule entry. Each entry has one required key and two optional keys:
| Key | Required | Type | Purpose |
|---|---|---|---|
every |
yes | string | Interval between sweeps: <n>d | <n>h | <n>m. Sub-daily intervals clamp to one sweep per day (the slug is date-grained). Example: 7d. A missing or invalid value is a parse error and the role is skipped. |
cost_limit_usd |
no | number | absent | Pre-flight cap for one sweep dispatch. Absent → no cost-cap check at all (the SB rule is skipped when cost_limit_usd is null). When present, a role estimate from the registry exceeding this cap is a config defect — the scheduler skips rather than dispatching (rule SB). |
enabled |
no | boolean | Whether sweeps for this role are active. Default true. Disabled → rest (rule S0). |
Interval grammar
Intervals are parsed by parseEvery() in
schedule.ts. The grammar is
^(\d+)([dhm])$: a positive integer followed by one of
d, h, or m (days, hours, minutes).
The input is trimmed first. The function returns milliseconds,
or null when the value does not match the grammar or the integer
is zero, which the scheduler treats as a parse error.
Example
schedules:
historian:
every: 7d
cost_limit_usd: 5
enabled: true
Sweep lifecycle
A sweep is a gate-less mini-run: runs/<role>-<date>/
on branch run/<role>-<date>, seeded with a
sweep.yaml marker. It carries no state.yaml on
purpose: the gate engine and frontend recognize runs by
state.yaml, so sweeps stay out of the derivation table
entirely. The human surface is the branch itself: review the docs-delta
and doc edits, and merge to approve (the P4 principle). The merged marker
on the default branch makes the next sweep's interval derivable.
The marker carries these fields:
| Field | Purpose |
|---|---|
sweep, role | The sweep slug and the scheduled role |
every, at | The interval in force and the dispatch time (ISO-8601) |
covering_since | The point the previous merged sweep covered up to; null on a first sweep |
adapter, model, cost_limit_usd | What ran and under what cap |
tokens_in, tokens_out, cost_usd | Real usage, written by the closing commit; null until then |
failed | Present only when the dispatch did not complete, with the truncated error |
Sweep derivation rules (S0–S4+SB)
The scheduler applies one rule per schedule entry per tick; each rule
has a test in test/schedule.test.ts:
| Rule | Condition | Action |
|---|---|---|
| S0 | Schedule disabled | Rest — no sweep for this role |
| S1 | A sweep branch for this role is open (unmerged) | Rest — one in flight or awaiting human review per role |
| S2 | Interval since the last merged sweep has not elapsed | Rest — not yet due |
| S3 | Today's sweep branch already exists | Rest — duplicate guard; sub-daily intervals clamp to daily |
| SB | Role estimate exceeds the schedule's cost cap | Skip + warn — a config defect, not a dispatch |
| S4 | Otherwise (none of the above) | Dispatch the sweep |
S1 and S3 read local branches and remote-tracking refs
(refs/remotes/*/run/<name>) alike, and the caller
fetches with --prune first, so a sweep open only on origin
still counts as open.
The dispatch itself follows commit-then-launch: the intent commit seeds
the sweep branch with its marker, created from ZERO_OID so two
instances racing the same schedule resolve at the ref, and the loser rests.
After the agent completes, a closing commit records real usage into the
marker.