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,parseThresholdandmeetsThresholdfrom@omnislash-ai/types/betare 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.
| Key | What it means | |
|---|---|---|
thesis | required | the decision, in one sentence |
status | required | active, 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 |
features | own feature | the features whose agents carry the bet in their context |
checkpoints | required, ≥ 1 | in 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:
expecton 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.killonly where written, with the volume floor as one more key. An intermediate checkpoint withoutkillnever 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”).
frozenholds values, not words. “The niche is frozen” in prose holds nothing; a lead-search tool that readsfrozen.nichethroughbet_get(an agent) orfindApprovedBet(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 whatreadcomputes; 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 thansince + maturesis 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
readsource andperiodof every KPI the bet reads. Areadis 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 fromsincethan 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 checkpoint | Final checkpoint | |
|---|---|---|
every expect met | on_track | persevere |
every kill key met | kill (only where written) | kill |
| neither | behind | pivot_zone |
a reading it needs is missing, null or not mature | insufficient | insufficient |
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
forreadings are all past its bound. Anullreading breaks the run — missing data is the next finding’s. - stale — the last
bets.staleAfterreadings (default 3) of a KPI the bet reads are allnull: 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:
- samples every KPI into the KPI history;
- 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;
- once a bet has started (
sinceset 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’sexpect/killand 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; - asks the reviewer at every due checkpoint;
- 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 for | Answers | What each does |
|---|---|---|
| a checkpoint, a tripwire, a stale KPI | Continue · Revise · Close: won · Close: killed | Continue 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 decision | Accept the edit · Revert to approved | Accept 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 approved | Revert writes the approved version back |
| a bet no owner approved | Approve · Reject | Approve 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 proposal | Approve · Reject | Approve 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:
| Tool | Capability | What it does |
|---|---|---|
bet_list | bet:read | Every 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_get | bet:read | One 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_propose | bet:write | A 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_getsayeditedOutsideDecisionmeanwhile.
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
expectandkillagainst 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
| Value | Where | Default |
|---|---|---|
| When the pass runs | constitution/MANIFEST.yaml § bets.schedule (cron, locale.timezone) | 0 6 * * * |
How many null readings in a row make a KPI stale | constitution/MANIFEST.yaml § bets.staleAfter | 3 |
| When a slow KPI starts to mean something | matures on the KPI (defineKpi) | at once |
| The reviewer’s model and instructions | your copy of constitution/Agents/system/role-bet-reviewer.md (role_update) | smartest |
| How long decisions are kept | constitution/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_checkat 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 asupersedesnaming no bet.bet_listshows which bets are judged by an approved version and which wait on a card;schedule_overviewshows the pass’s last run.- The
betsengine skill (skills/engine/bets/) walks an agent through writing one;find-and-validate-an-offerleads from an owner’s goal to the first bet.