By example
Find the sentence closest to what you mean and take the line beside it. Everything here is a one-liner from the presets or the checks; the pages after this one explain how they are built.
Tools are named by string below so each line stands alone. Where the tool is in scope, pass the tool itself instead —
requireBefore(restartService, queryMetrics) — and a rename becomes a compile error rather than a rule that silently
stops firing. Propositions has the argument.
Ordering
Section titled “Ordering”“It must not do X until Y has come back.”
Presets.requireBefore('restartService', 'queryMetrics')The second argument is read as a result, not a call: an agent that fired the query and reached for the restart while it was still running has not read anything.
“Once it has done X, it must never do Y.”
Presets.neverAfter('closeIncident', 'restartService')“X must never happen at all.”
Patterns.absence(P.toolCall('purgeLogs'))“Every X must be answered within four events.”
Patterns.response(P.toolCall('pageOncall'), P.toolResult('pageOncall'), { within: 4 })A window is required, because an unbounded “eventually” cannot be decided on a run that has not ended.
People
Section titled “People”“A person has to approve this step.”
Presets.requireHumanBefore(P.stepStart('applyRemediation'), { mode: 'suspend' })What counts as an approval is a run.resume — the run’s own lifecycle, which no tool call can fake.
Volume
Section titled “Volume”“At most eight tool calls in a run.”
Checks.callBudget({ max: 8 })“At most two searches.”
Checks.callBudget({ tool: 'search', max: 2 })“Stop it repeating itself.”
Checks.loop({ n: 3 }) // the same call, same arguments, three times in a rowChecks.duplicateCall({ window: 5 }) // the same call again within five eventsRepetition is keyed on the call’s arguments, which is what makes “the same call again” mean something.
Payloads
Section titled “Payloads”“Only when the amount is over a thousand.”
Presets.requireHumanBefore(P.toolCall('refund', { amount: P.gt(1000) }), { mode: 'suspend' })Every proposition takes an optional payload matcher. Propositions has the full vocabulary — comparison, membership, regular expressions, presence, and dotted paths into nested objects.
Assembling a spec
Section titled “Assembling a spec”import { Checks, Presets } from '@rhysbalevicius/ltl-core'import type { MonitorSpec } from '@rhysbalevicius/ltl-core'
export const SPEC: MonitorSpec = { rules: [ Presets.requireBefore('restartService', 'queryMetrics', { id: 'SEC-01', mode: 'deny' }), Presets.neverAfter('closeIncident', 'restartService', { id: 'LIFE-01' }), ], checks: [Checks.callBudget({ max: 8 })],}Rules and checks are reported identically — same verdict shape, same modes, same place in a summary. The split exists because counting is not a temporal property, not because the two are treated differently.
When none of these fit
Section titled “When none of these fit”Reach down a layer. Patterns is the specification literature’s vocabulary, and combinators is the operators themselves. Each layer is implemented by calling the one below it, so dropping down never means leaving the system.