Operation bets

An operation runs for months. Every tick of a lead’s heartbeat is a fresh agent, every chief session starts anew, and each of them sees the same “no revenue yet” and is its own judge of whether to change course. Left alone, an operator drifts: a niche widens a little each week, a threshold slips, a quiet agent decides the plan was wrong. A bet is the commitment device against that drift.

A bet is one business decision an owner would take — a niche, a positioning, a channel, a product line, a price — written down with the dates it is reviewed on and the numbers it is judged by. The engine holds it:

  • when to look again — the checkpoints in the file, checked by a daily engine pass;
  • by what rule — the expectations written for each checkpoint, judged by code against the KPI history, never by an agent’s reading of its own progress;
  • who may change course — only an owner, on a decision card; every decision pins the approved version, and an edit made outside a decision is caught and judged by the pinned version;
  • who looks from outside — a reviewer with no memory of the operation, at every checkpoint, free to check the rows under any number itself.

Bets are opt-in: a brain with no bets/ directory runs as before, and the daily pass still records the KPI history (kpis.md).

Is it a bet?

  • A bet is a decision whose “stop” is an owner’s business decision. A few a year, not a few a week.
  • Not a bet: a client’s, a campaign’s or an order’s goal. Those are rows of the feature’s own ledger (state.md), kept by its lead. A bet per client would put a dozen cards a month in front of an owner. If your ledger wants the same threshold grammar, import it — verdict, parseThreshold and meetsThreshold from @omnislash-ai/types/bet are pure functions with no clock and no card.
  • Not a bet: a tactic. Hooks, formats, copy, timing — that is the self-evolving loop’s work (self-evolving.md). The loop improves tactics inside a bet; it may never touch the bet.

The file

features/<feature>/bets/<id>.md — the id is the file name, unique across the brain (and not note: a card’s answer form keeps its note under that key — a note.md is refused at load). A feature may hold several bets. A bet that spans features lives in the feature that owns the strategy and lists the others under features:. Closing a bet changes its status; the file stays where it is, so a feature’s bets/ directory is also the record of what was tried.

# features/growth/bets/founder-podcasts-us.md
---
thesis: US founder podcasts pay $500/month for editing, and cold email brings them
status: active               # active | won | killed
since: 2026-12-01            # absent = not started; an owner's start decision sets it
features: [growth, delivery] # optional — whose agents are held to it; default the owning feature
checkpoints:                 # offsets from since (+Nd / +Nw) or dates; the last one is the final
  - at: +6w
    expect: { replies-28d: '>= 3' }
  - at: +13w
    expect: { replies-28d: '>= 6', paid-pilots-total: '>= 2' }
  - at: +26w
    expect: { revenue-30d: '>= 1500' }
    kill:   { revenue-30d: '< 300', sends-30d: '>= 400' }   # all keys must hold: kill only on enough volume
tripwires:                   # the only early exit
  - { kpi: spend-30d, above: 200 }
  - { kpi: yt-reach-7d, below: 1000, for: 3 }               # for = N readings in a row
frozen:                      # concrete values tactics run on — read them through bet_get
  niche: US founder podcasts
  offer: $500/mo, 4 episodes
  channel: cold email
supersedes: podcast-eu       # optional — a pivot is a close plus a new bet naming the old one
---
Why this bet. The assumptions ("20 Dec – 5 Jan is dead", "mailboxes warm up
for three weeks"), the base rates with their sources. The body is pinned with
the frontmatter, so the reviewer reads it as approved. The outcome and the
lesson are appended when the bet closes.
KeyWhat it means
thesisrequiredthe decision, in one sentence
statusrequiredactive, won (it worked — the bottleneck moved elsewhere) or killed
since—the start date. Offsets count from it, so extending a bet is moving since. Absent = not started: the bet has no calendar, no tripwires and no stale checks until a revision sets it
featuresown featurethe features whose agents carry the bet in their context
checkpointsrequired, ≥ 1in calendar order. at: +6w, +10d or 2027-11-27. Each names expect, kill or both — KPI id → threshold
tripwires[]{ kpi, above | below, for? } — exactly one bound, strict (above: 200 trips at 201); for (default 1) readings in a row past it
frozen{}any keys and values: what the tactics read instead of their own copy
supersedes—the id of the bet this one replaces

A threshold is an operator and a number, quoted: '>= 3', '> 0.5', '<= 40', '< 300'. The frontmatter is strict: an unknown key, a threshold that does not parse, a checkpoint with neither expect nor kill, or checkpoints out of order fail the file — the doctor’s bets check names it, and the pass skips it.

Why the fields are shaped so:

  • expect on every checkpoint is the plan’s curve. Seasonality and warm-up go here — a January checkpoint simply expects less. The engine does not model seasons; your curve does.
  • kill only where written, with the volume floor as one more key. An intermediate checkpoint without kill never kills. kill: { revenue-30d: '< 300', sends-30d: '>= 400' } kills only when the low revenue comes with enough sending to mean something.
  • Offsets, not dates, because the start waits on someone else’s queue (a store review, a mailbox warm-up, an account unban). An absolute date is fine for a real deadline (“before Black Friday”).
  • frozen holds values, not words. “The niche is frozen” in prose holds nothing; a lead-search tool that reads frozen.niche through bet_get (an agent) or findApprovedBet (code, below) cannot drift from it. A value that lives outside the brain (a price in the billing system) is guarded by a tripwire on a KPI that would show it moved.
  • Assumptions live in the body, which is pinned — the reviewer knows about the warm-up and August before reading the numbers.

The KPIs a bet reads

Every id under expect, kill and tripwires is a KPI the brain declares (defineProject({ kpis }), kpis.md). The engine samples every KPI once a day into its KPI history, and a checkpoint is judged on the newest reading at or before it. So:

  • Use rolling or closed windows — revenue-30d, replies-28d, never “this month so far”. On the 5th of a month a month-to-date number is five days of data judged as a month. The engine cannot see what read computes; this rule is yours, and the reviewer checks it.
  • A slow metric declares matures — the days after a start before it means anything (a cohort’s churn, 30-day retention). A checkpoint reading it earlier than since + matures is refused.
  • Final checkpoints read money from a rail — Stripe, a partner API, a bank — not from tables your own agents write. Say where the number comes from in the KPI’s description.
  • A KPI definition is pinned with the bet. Each decision records a hash of the read source and period of every KPI the bet reads. A read is brain code that reaches production through your gates — which check that it builds, not that it measures the same thing — with no owner merging it (contract.md § 2.1). A redefinition (revenue net → gross) shows as an edit outside a decision (below).
  • The span fits the action log. Decisions are pinned in the action log (audit.retentionDays, default 365 days). A bet whose final checkpoint is further from since than that is refused — shorten it or raise the retention (manifest_set_retention, an owner’s approval).

The verdict

At a checkpoint the engine computes the verdict from the readings — a pure function, the same one you can import:

Intermediate checkpointFinal checkpoint
every expect meton_trackpersevere
every kill key metkill (only where written)kill
neitherbehindpivot_zone
a reading it needs is missing, null or not matureinsufficientinsufficient

A written kill that holds wins over everything else; a kill that cannot be judged yet leaves the answer insufficient. Beside the checkpoint, every day:

  • tripped — a tripwire’s last for readings are all past its bound. A null reading breaks the run — missing data is the next finding’s.
  • stale — the last bets.staleAfter readings (default 3) of a KPI the bet reads are all null: a ban, a broken sync, an expired token.

A below tripwire has no warm-up of its own — it can trip during a ramp. Set its bound to what the operation already does on day one, or leave it for a later revision of the bet.

The daily pass

One engine pass a day, at MANIFEST bets.schedule (cron in your locale.timezone, default 0 6 * * * — before the 07:45 brief). It shows on the schedule as the engine’s Bets and KPI samples pass; schedule_overview gives its next run and its last (bet:pass.completed). One run:

  1. samples every KPI into the KPI history;
  2. finds the version each bet is judged by — its latest pin. A file or a KPI definition changed since is an edit outside a decision, judged by the pinned version — a pinned file deleted or moved too: the pass finds the bets from their pins, not only from the files. An active bet no owner ever approved (a file written with file tools, a bet the brain shipped before, one whose last pin the action log’s retention reaped) is not judged: it goes on the card to be approved or rejected. The engine never approves a bet by itself;
  3. once a bet has started (since set and reached), checks its tripwires and stale KPIs on the readings taken since it started, and the checkpoint due: the newest one on or before today that no decision settled yet — as the approved version now reads it: a revision that changed that checkpoint’s expect / kill and kept its date, or a closed bet reopened, makes it due again — and whose bet is on no open card. Older undecided checkpoints are passed over; a checkpoint is judged on the readings as of the end of its own day, so one judged late reads what was known then. Only readings of the KPI definitions the bet was approved with count — a KPI redefined since reads as missing until an owner accepts the new definition;
  4. asks the reviewer at every due checkpoint;
  5. decides on an owner’s card.

A checkpoint is due until a decision names it, so a run that failed half-way is finished by the next one; no state of its own. Don’t add a heartbeat of your own to review a bet — the pass is that heartbeat.

The reviewer

At every due checkpoint — on_track included, because made-up progress is green, not red — the engine runs the system:bet-reviewer role once, with no memory and read-only capabilities (db:read, runtime:read). It is given the bet as approved (the pinned frontmatter and body), the KPI series, the open engine notices and the closed bets of the same features. It is not given the operation’s own story: no daily log, no MEMORY.md, no lead’s report.

It may query your tables itself — a “paused” customer who churned, a lead row nobody sent to — and answers agree or dissent, evidence: verified (it checked rows behind the readings) or unverified, a recommendation and, for a pivot or a kill, the best alternative course it can name.

Its model is its role’s: smartest by default. Pick another — a model of another family than the one running the operation is a fair choice — by writing modelRef in your copy of the role (role_update).

An owner’s card

The bet request (human-input.md) goes to the chief’s bot, in the primary owner’s DM. One card per pass, covering every bet the run touched: one bet asks with buttons, several ask one choice each (a form on the dashboard). Each bet on it says why it is there — a checkpoint, a tripwire, a stale KPI, an edit outside a decision, a bet no owner approved, a proposal — with the verdict, its conditions against the readings, the reviewer’s opinion and, for a change, the diff. New thresholds always arrive as a diff: an agent cannot lower its own bar in silence.

No card for an intermediate checkpoint that is on_track — or insufficient only on a kill the plan did not expect to read yet — when the reviewer agrees on verified evidence and nothing is tripped or stale. The pass records that checkpoint itself, and the next daily brief says “checkpoint X of bet Y passed, the reviewer agrees” (brief_digest’s bets.passed). A final checkpoint always goes to an owner.

On the card forAnswersWhat each does
a checkpoint, a tripwire, a stale KPIContinue · Revise · Close: won · Close: killedContinue keeps the course; Revise tells the chief to draft a new version through bet_propose — the course holds until that is approved (a tripwire or stale KPI answered with either stays quiet while it holds). Close writes the status and pins it. The final checkpoint takes no Continue: past it a bet has no calendar left — Revise (a later checkpoint) or Close
an edit outside a decisionAccept the edit · Revert to approvedAccept pins the file as it is, with the KPI definitions as they are now. Revert writes the approved version back — byte for byte
a file edited outside a decision that no longer loads, or is gone (deleted, moved)Revert to approvedRevert writes the approved version back
a bet no owner approvedApprove · RejectApprove pins the file as it stands — the bet holds the course from then on. Reject records the no: the bet stays unjudged and off the card until its file changes
a proposalApprove · RejectApprove writes the proposed file and pins it; Reject records the no

Every answer but an approval (Approve, Accept) keeps the KPI definitions the bet was approved with: a KPI redefined outside a decision stays redefined — and unjudged — until an owner accepts it. An approval of a version reading a KPI the brain does not declare is not applied (recorded as skipped, the chief told why): its readings could never be judged.

Only a person answers a bet card — an agent never decides its own course, even one holding hitl:write, and Boss mode does not either. A reply in words reaches the chief as a chat message and decides nothing; closing the card decides nothing either — the next pass brings the bet back.

A bet waits on one card at a time: while it is on a card — pending, or answered and not yet applied — the pass does not judge it again and bet_propose refuses it. The check and the raise hold one lock, so a proposal made while the pass waits on its reviewer leaves that bet off the pass’s card; and a checkpoint the pass passed is recorded only while the version it judged is still the approved one.

After a decision the engine writes the file (compare-and-swap: only over the version the card was raised against, else the decision is recorded as skipped and the chief is told why), records the pin and sends one message to the chief in its DM with the primary owner saying what was decided and what to do next. That message is how a decision on a bet wakes the chief.

A kill never pauses the feature. It closes the bet. Winding down acquisition and spend is the chief’s and the lead’s work after the decision; the clients, orders and refunds already taken are served as before. No bet is ever killed automatically, even when the reviewer agrees.

Changing course

An owner may change course at any time. When one asks, the chief drafts it:

ToolCapabilityWhat it does
bet_listbet:readEvery bet as approved (filter by feature, status): thesis, status, next checkpoint, approved (whether an owner approved a version), editedOutsideDecision, the last decision. Closed bets are the record of what was tried; files that do not load are listed under errors
bet_getbet:readOne bet whole, as approved: the calendar, tripwires, frozen, the body; the file as it stands when it differs (fileText — not the course), KPIs redefined since the pin, its decisions, the latest reading of each KPI, the card it waits on. With no approved version, approved: false and frozen: null
bet_proposebet:writeA new bet, a revision (new thresholds, a later checkpoint or since, a start) or a close outside a checkpoint. Takes the whole new file text and a reason. Never writes: the text is checked as the engine would load it, then goes to an owner as a card with the diff against the approved version (an earlier edit outside a decision shows in it). Answers the card id and the closed bets of the same features

A pivot is a close plus a new bet with supersedes. bet_propose refuses a text that does not load, a KPI the brain does not declare, a checkpoint before a KPI matures, a span past the action log’s retention, an id another feature already holds, a supersedes naming no bet, a text identical to the approved version, and a bet already on an open card.

Every decision is an action-log row against the bet file (audit_list, actions bet.open, bet.checkpoint, bet.revise, bet.close, bet.accept, bet.revert pin a version; bet.reject and bet.skip record a no and a decision that could not apply).

Reading a bet from code

A tactic in code — a pipeline, a tool, a hook’s gate — reads the values it runs on from the approved version, never from the file:

import { betFilePath } from '@omnislash-ai/config/bets';
import { findApprovedBet } from '@omnislash-ai/infra';

const bet = await findApprovedBet(betFilePath('growth', 'founder-podcasts-us'));
if (bet === null) return; // no owner approved it — nothing runs on it
const niche = bet.spec.frozen.niche;

findApprovedBet answers { pin, spec, body } of the latest pin, or null while no version is approved.

Edits outside a decision

Any .md in the brain is runtime content: an agent can rewrite a bet file with its harness’s file tools, and the edit reaches production without a gate. The engine cannot stop the write. It catches it after:

  • every decision pins the file’s text and its KPIs’ definition hashes;
  • the daily pass compares the file and the live KPI definitions with the pin; a difference is judged by the pinned version and goes on the card as “changed outside a decision — accept or revert”, with the diff;
  • bet_list / bet_get say editedOutsideDecision meanwhile.

bet_get, findApprovedBet and the agents’ block all answer the approved version, so an edit reaches no tactic before an owner accepts it; bet_get shows the edited file beside it as fileText. A bet file nobody approved holds no course at all: it is not judged, not in any agent’s block, and frozen is null until an owner approves it.

Revert restores the bet file, not a KPI: a redefined KPI is fixed in kpis.ts, then accepted.

What agents see

The engine puts the bets into the context of the agents they hold, every turn — a block <operation-bets>, re-sent only when a bet changes and whole after a compaction. It needs no role setting and no feature context (contexts.md) — the engine renders it:

  • the chief sees every active bet as one line: thesis, day N of M, the next checkpoint’s expect and kill against the latest readings;
  • an agent of a feature a bet lists (its lead included) sees the whole bet: thesis, day N of M, the next checkpoint, frozen, and “Course frozen until : a doubt goes into a note for that checkpoint or into bet_propose — do not change course”;
  • both read the line that an owner may change course at any time, and that the answer to “let’s change it” is bet_propose, never “it is frozen”.

The block shows the approved version — also while its file is edited or no longer loads; a bet no owner approved is not in it. A pinned file that is gone leaves the block until an owner reverts it (the next pass asks). Don’t copy a bet into a feature context or FEATURE.md: the map points at the bet, the bet holds the thesis and the numbers.

The self-evolving loop’s proposer is not given the block: a cycle analyses one entity’s runs, and its edits cannot reach a bet anyway (below). Keep the tactics it changes on frozen values, and don’t grade an entity on a proxy that pulls against the bet’s KPIs — a format graded on views alone will widen a niche the bet holds.

What the loop may not touch

**/bets/ is in the engine floor of the self-evolving loop (self-evolving.md § What the loop may not touch): no cycle may write a bet, whatever your tiers say. A cycle that rewrote the thesis or lowered a threshold would grade itself against a bar it moved.

Values you can tune

ValueWhereDefault
When the pass runsconstitution/MANIFEST.yaml § bets.schedule (cron, locale.timezone)0 6 * * *
How many null readings in a row make a KPI staleconstitution/MANIFEST.yaml § bets.staleAfter3
When a slow KPI starts to mean somethingmatures on the KPI (defineKpi)at once
The reviewer’s model and instructionsyour copy of constitution/Agents/system/role-bet-reviewer.md (role_update)smartest
How long decisions are keptconstitution/MANIFEST.yaml § audit.retentionDays (manifest_set_retention)365 days

Every threshold, checkpoint and tripwire is the bet’s own — the engine ships no number for them.

Checking it

  • omnislash doctor --check bets (doctor_check at runtime) loads every bet and warns on a file that does not load, a KPI the brain does not declare, a checkpoint before a KPI matures, a span past the audit retention, and a supersedes naming no bet.
  • bet_list shows which bets are judged by an approved version and which wait on a card; schedule_overview shows the pass’s last run.
  • The bets engine skill (skills/engine/bets/) walks an agent through writing one; find-and-validate-an-offer leads from an owner’s goal to the first bet.

Screens from Sixty Seconds Studio, a fictional demo studio, running on the real dashboard.