standing

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

Package standing is the ambient side of v3: the things a conversation leaves behind that keep working after the window is closed — a reminder, a watch, a rule, an overnight job — and the small, honest machinery that wakes them.

THIS FILE IS THE CONTRACT. It was written by hand before any lane started, and every lane codes against it: the core lane fills in the store and the ticker, the session lane supplies the sentinel and the runner and arms the belt tool, the home lane draws items, the errand lane hosts the exchange at home. A field or a method added here mid-build is a change every lane has to hear about, so the shape is deliberately small and deliberately complete.

── THE LAWS ──

  • ONE OBJECT, MANY SHAPES. A reminder, a routine, a watch, a rule, an overnight job and a self-proposed follow-up are all an Item: the person's words, what wakes it (When), what it does (Action), and what bounds it (Rails). The product's variety is in two fields, not in six mechanisms.

  • THE MECHANISMS ARE CLOSED; THE CONDITION IS OPEN. There are five ways an item can be woken (WhenKind) and that list does not grow per feature. But WhenProbe is general: the model writes the probe — any shell command, or any tool on the belt with any arguments, including a tool a connected account brought — and a cheap yes/no judgment (Sentinel) reads its output against the person's words. "Is CI red", "did Priya reply", "is the cert under 14 days" are all probes; none of them is a kind.

  • FILES, NOT A DATABASE. One JSON document per item, written temp+rename under a per-item flock; an append-only daily ledger for the rails; one folder per run. docs/AMBIENT.md Part 4 has the numbers. Every path is answered by this package and nowhere else, so an index could be added behind it later without a caller changing.

  • NOTHING STANDS UNTIL THE PERSON SAYS YES. Store.Create is only ever called after a ratification card was answered yes (a StandingProposal in internal/session). There is no path that arms an item silently.

  • UNATTENDED MEANS WHAT WAS ALREADY ALLOWED. A firing runs under the person's banked approval rules with nobody to ask; anything that would have asked stops the run as "needs your look". This paragraph used to finish "there are no probation counters: the rules are the tenure", and the second half of that is no longer true: Item.CleanRuns counts clean firings in a row, and the rope column reads it (RopeWord). The first half still is, and it is the part that mattered — the counter changes WHAT A PERSON IS TOLD about an item, never what a firing is allowed to do. What is allowed is the banked rules and only ever the banked rules; nothing here widens with a count.

  • QUIET IS THE DESIGN. A check that found nothing rewrites LastChecked and LastCheckLine in the item and writes NO line anywhere else. A run that delivered nothing is reaped after RunKeep.

  • STATUS IS DERIVED, NEVER ASSERTED. Whether the OS timer is installed is whether its definition file still matches byte for byte what this build would write; last wake and next due come from the wake log and the fixed cadence. Nothing shells out to ask.

── WHERE THE BODIES ARE ──

This file is the shape. The work is in store.go (the documents, the item log, the locks), ledger.go (the daily lines the rails are summed from), inbox.go (news for a window that is not open), every.go (the rhythm), tick.go (the pass) and watch.go (the operating system's timer).

Every name a caller holds is declared here, with ONE exception the contract could not carry: Watch is an interface with no way to make one, so watch.go adds NewWatch and the WatchOptions it takes. Nothing else outside this file is reachable.

Index

Constants

View Source
const (
	RunningChecking = "checking"
	RunningFiring   = "firing"
)

The two things a pass can be doing to one item, and the whole of what a marker's What may say. Checking is the look — a probe, a fingerprint, the sentinel's yes-or-no; firing is the work that follows a yes.

View Source
const (
	// OutcomeNeedsYou is a firing that stopped on a question for the person.
	OutcomeNeedsYou = "needs-you"
	// OutcomeFailed is a firing that could not finish.
	OutcomeFailed = "failed"
)

The two outcomes that mean a firing did NOT come back clean, spelled once here because three places now test for them — the clause a card reads ([outcomeClause]), the recorder ([Ticker.fire]) and the trust counter that recorder keeps. They were string literals in each, which is a word spelled three times and therefore a word that will drift.

View Source
const (
	DarwinTickLabel = "ai.agentfield.codeaf.tick"
	LinuxTickTimer  = "codeaf-tick.timer"
)

DarwinTickLabel and LinuxTickTimer are what this machine's own scheduler calls the timer.

THEY ARE EXPORTED BECAUSE THE SETTINGS ROW SAYS THEM OUT LOUD. "codeaf installs a launchd agent" is a sentence nobody can check; the label is what `launchctl list` and `systemctl --user list-timers` answer to, and a person deciding whether to leave background checks on is entitled to the name they would have to type to go and look.

View Source
const (
	// RopeAsksFirst is an item that has been given no licence to act unasked.
	RopeAsksFirst = "asks first"
	// RopeTrusted is an item with a grant that has since fired [TrustAfter]
	// times in a row without needing anybody.
	RopeTrusted = "trusted alone"
)

The words the rope column is allowed to say, and no others.

View Source
const CameTo = "came-to"

CameTo is the one-word file a firing leaves in its run folder saying what that run came to — the same word as Outcome.Kind. The item's own LastOutcome is overwritten by the next firing, so without this nothing on disk would say which of a hundred run folders delivered anything.

View Source
const DefaultPerRunUSD = 5.0

DefaultPerRunUSD is what ONE FIRING of a standing item may spend when nobody named a figure — the probe, the sentinel's judgment and the work itself.

IT IS THE ONE PLACE THIS NUMBER LIVES. It was written out three times once — here in the store that creates a charter, in the proposal that quotes a price to the person, and in the belt tool's own schema — and three copies of one fact is the drift this codebase has a law against. Every reader resolves it from this constant.

Fifteen cents was the old figure and it was a rail rather than a backstop: a cheap look plus a small model's answer and nothing else, so the first standing order anybody wrote that did real work stopped halfway through its first firing. Five dollars is a whole piece of work on a good model, which is what a person who says "keep an eye on this" is actually asking for. The protection that matters is still the machine-wide daily rail plus the max-per-day count, not this.

View Source
const (

	// InboxLockName is the flock file's name inside an inbox folder. It is a
	// dot-name, so no reader that globs the folder's notes ever sees it, and it
	// travels WITH the folder so a symlink alias and a different TEMP directory
	// still resolve to one lock.
	//
	// IT IS EXPORTED BECAUSE IT IS A PERSISTENT RESIDENT OF A SESSION FOLDER. A
	// conversation's own folder is a standing inbox, so the first open of a
	// session leaves this file behind and any caller that asserts on what a
	// session folder holds has to name it rather than guess at it.
	InboxLockName = ".inbox.lock"
)

The boxes on one inbox. They bound a folder so a stuck writer surfaces as visible backpressure rather than growing the folder without limit, and bound one line so an oversized note is refused (and its caller keeps the pending) rather than truncating the reader's scan silently.

View Source
const Interval = 5 * time.Minute

Interval is how often a pass runs, whether a window runs it or the OS timer does. It is the cadence the ratification card quotes for "checked every …".

View Source
const NeedsBaselineLead = "started without a full baseline: the watch could not read everything it matches"

NeedsBaselineLead opens the line a ratifier leaves when a file watch could not have its baseline read whole, so no baseline was set (Store.Arm and ErrBaselineIncomplete). It is a FLAG rather than a failure: the item stands and keeps its ordinary first-reading semantics, but the person is told that the reading it starts from was not established at the yes. It is declared beside NeedsPermissionLead for the same reason — two packages agree on the words, the session writes them and IsBaselineLine recognises them.

View Source
const NeedsPermissionLead = "stopped: it needed your ok to run "

NeedsPermissionLead opens the one line a firing leaves when it stopped because a call needed permission and nobody was there to give it. It is declared here, beside the field, because two packages must agree on it: the session writes it and IsPermissionLine recognises it.

View Source
const OutcomeNothing = "nothing"

OutcomeNothing is the Outcome.Kind of a run that delivered nothing at all: no line, no landing, nothing waiting for the person. It is the ONE outcome whose run folder the sweep may reap after RunKeep, so it is a constant rather than a word spelled twice in two packages.

View Source
const PendingGiveUp = 3

PendingGiveUp is how many passes may fail to settle one delivery intent before the item says so as an explicit Item.NeedsPerson. It bounds a retry that cannot succeed so it surfaces to the person instead of retrying forever.

View Source
const PendingKeep = 8

PendingKeep bounds how many undelivered records one item may carry, so a delivery that keeps failing cannot grow the document without limit. It is generous for the real case — one line at a time — and finite for the bad one.

View Source
const Previous = 5

Previous is how many of an item's last sentinel judgments ride in the next judgment's prompt. It is what stops a declined firing being proposed again every wake forever: the sentinel sees what it said last time and what came of it.

View Source
const ProbeClip = 8 * 1024

ProbeClip bounds what one probe may put in front of the sentinel.

View Source
const RunKeep = 7 * 24 * time.Hour

RunKeep is how long a run that delivered nothing is kept before the sweep reaps it. A run that delivered something — a note, a task landing, a needs-your-look — is kept like any session.

View Source
const RunningFile = "running"

RunningFile is the marker's name inside the item's own folder (Store.RunningPath).

View Source
const Schema = 4

Schema is the newest document version this build reads. Bump it when a field changes meaning; a reader that meets a newer schema than it knows skips the document and says so in the pass. SchemaOf says which version one item is written at.

VERSION 3 IS THE DEFERRED-DELIVERY BARRIER. A document that carries a durable Item.Pending, a task in-flight marker, an armed Item.Fingerprint or an advanced Item.Revision is written at 3, so a build that predates those fields — codeaf, devaf and stageaf share one home, so that build is an ordinary afternoon and not a downgrade — SKIPS it rather than decoding it without those fields and re-marshalling the loss back. Version 2 alone is not safe: a v2 reader knows Action.Isolate but not Pending, and would silently drop an authorized line. Reading 1 and 2 still works; the upgrade to 3 happens on the next write.

THE BARRIER PROTECTS SEMANTICS THAT PREDATE Item.Pending. A newly armed file watch carries its Item.Fingerprint baseline and a bumped Item.Revision BEFORE any delivery intent exists; a baseline reader that decodes it without those fields would re-marshal the loss back and turn "the file changed" into "nothing changed", and its write would reset the revision the guarded write ([Store.saveActive]) relies on. So the fence is on the fields themselves and not only on a pending line.

VERSION 4 IS THE PROBE-RECURRENCE BARRIER, and it is deliberately narrow. A document carrying a Item.Positive identity or a one-shot condition (When.OneShot) is written at 4, so the build that first landed the positive identity at schema 3 — which decoded a document without the field and re-reported an unchanged positive — SKIPS it rather than misreading it. The fence is on the new fields themselves: version 3 cannot guard a field a schema-3 reader already claims to understand.

View Source
const TickWindow = 120 * time.Second

TickWindow bounds ONE pass, wherever the pass is run from. It is generous for a pass that found nothing (a stat per item) and short enough that a wedged probe cannot hold the store's lock against every other window on the machine.

IT IS ONE NUMBER BECAUSE IT IS ONE QUESTION. A window's own goroutine and `codeaf tick` each used to name their own 120 seconds, and Store.Running needs a third reading of the same figure — how long a pass may last is how long a marker may be believed. Three copies of a ceiling is three chances for one of them to move.

View Source
const TrustAfter = 5

TrustAfter is how many clean firings in a row earn an item the top rung. It is exported because it is the denominator a person reads — `earning trust 3/5` — and a page that spelled the 5 itself would be a second answer to a number this package owns.

Variables

View Source
var ErrAlreadyDrained = errors.New("standing: this delivery was already drained")

ErrAlreadyDrained reports that a note's identity was already drained from this inbox, so nothing was appended now. IT IS NOT A FAILURE: the line is already in the person's hands, and the caller that owns a durable intent must treat it as settled — but it must NOT offer the line live or steer the model with it again, because the person has already read it once. [standingRunner.deliver] reads it that way; Deliver returns it instead of a silent nil so a live offer cannot be made over a line the fold already spent.

View Source
var ErrBaselineIncomplete = errors.New("standing: the baseline scan could not read everything it matched, so no baseline was set")

ErrBaselineIncomplete is Store.Arm refusing to invent an Item.Fingerprint from a scan that could not read everything it matched. It is exported because the RATIFIER must see it: the session lane calls Arm when the card is answered and turns this refusal into a visible Item.NeedsPerson rather than letting a truncated scan pass as a baseline nobody was told was not set.

View Source
var ErrHeld = errors.New("standing: another codeaf is ticking")

ErrHeld is Tick's answer when another process holds the lock.

View Source
var ErrNotFound = errors.New("standing: no such item")

ErrNotFound is Get's answer for an id that is not here.

View Source
var ErrWatchOwned = errors.New("background checks belong to another profile or running build; the existing timer was left unchanged")

ErrWatchOwned means implicit setup would take the login's shared timer away from another profile or a live build. Only explicit settings may do that.

Functions

func CostPerRunWord

func CostPerRunWord(spend Spend) string

CostPerRunWord is what one firing of this item costs, in words: "under a cent", "$0.31", or NOTHING AT ALL where nobody measured it.

It is the rate and not the total: Spend.USD over Spend.Fired, which is the only per-firing figure this package can honestly produce. Two things about that are worth a caller knowing, because both make the figure an over-estimate rather than an under-estimate:

  • A CHECK'S MONEY IS IN THE NUMERATOR AND ITS FIRING IS NOT. [Spend.count] adds what a quiet check cost and does not count it as a firing, because a check that came to nothing is not a firing — so an item that looks thirty times to fire twice carries thirty looks' worth of sentinel calls across two firings.
  • THERE IS NO EXACT LAST-FIRING COST. Nothing on the item records what the last firing alone came to, and every exported reader of the daily ledger sums rather than handing back rows, so an average is the honest answer and the only one.

NOTHING FIRED IS NOTHING MEASURED, and so is a run nobody could price: both answer "" rather than "$0.00", by the emptiness law — zero here means nobody could say, never that the work was free.

The word is the FIGURE alone. "a run" is the page's to append, because the same rate reads as "under a cent a run" in a sentence and as a bare cell in a column, and a formatter that decided which was a formatter drawing the page.

func Deliver

func Deliver(sessionDir string, note Note) error

Deliver appends a note to a session's inbox.

A NOTE THAT CARRIES AN IDENTITY IS NOT DELIVERED TWICE, while the identity is inside the drained-identity window ([inboxSeenKeep]). The identity is the Pending record's id, written to the item before the line was carried out; a note whose identity was already delivered and drained is dropped here, so a replay after a restart whose acknowledgement was lost does not put the same line in front of the person again. A note with no identity — every note written before this existed, and every writer that has none — is passed through untouched. This is a durable-inbox guarantee, not a display one: the boundary is stated in Drain.

A DEDUP RECORD THAT CANNOT BE READ IS NOT "NOTHING WAS DRAINED". The missing file is normal and reads as an empty record; a read/scanner failure does not. The note is still appended — a duplicate is the safe direction and a line withheld because a record was corrupt is not — but the failure is RETURNED so the caller that owns the durable intent keeps it and says so. It is never swallowed into a success by absence.

func DeliverProject

func DeliverProject(root, workspace string, note Note) error

DeliverProject appends a note to a project's inbox. It is Deliver with the address worked out, so the two inboxes cannot drift on their line shape.

func ExchangesRoot

func ExchangesRoot(root string) string

ExchangesRoot is where an errand said at home keeps its folder BEFORE anything stands: <root>/exchanges/<session id>/. Home's own `ask here` makes one there so that home never lists it, the session lane reads it to know that a conversation IS an errand, and the sweep reaps the ones that came to nothing after RunKeep.

It takes the root rather than hanging off the store because two of those three callers hold a path and not a store, and opening one to ask a question about a directory would create the directory.

func InboxPath

func InboxPath(sessionDir string) string

InboxPath is the inbox inside a session folder.

func IntervalWords

func IntervalWords() string

IntervalWords is Interval the way a person says it — `5 minutes`.

ONE SOURCE OF TRUTH FOR THE CADENCE. Three sentences a person reads name it — the line the conversation says the first time something stands, the settings row's hint, and the manual — and a figure typed into any of them is a figure that will disagree with the timer the day this constant moves.

func IsBaselineLine

func IsBaselineLine(line string) bool

IsBaselineLine reports whether a line on an item is the baseline-incomplete flag rather than a question the firing put to the person.

func IsPermissionLine added in v0.4.0

func IsPermissionLine(line string) bool

IsPermissionLine reports whether a line on an item is about a permission the firing could not get, rather than a QUESTION it put to the person. It is the ONE predicate for that, asked by the store when a person changes an item and by the surface when it decides which door a row takes, so a line cannot be a permission in one place and a question in another.

IT KNOWS THE OLD SPELLING AS WELL AS THE NEW ONE, and that is not tidiness. Builds before this one put the engine's own refusal on the item verbatim, and those items are on disk now: a watch stuck for days carries `refused in a task: default — nobody to ask` and will carry it until it fires again, which an item that has spent its allowance for the day cannot do. A predicate that knew only the new lead would leave every row that provoked this exactly as it was, which is the one outcome that would make the change pointless.

func LastLookLine

func LastLookLine(item Item, now time.Time) string

LastLookLine is what this item did the last time it looked, in one sentence.

It is the line screen 2f draws under a card, and the whole reason it can be written at all is that the item keeps the three facts a firing leaves behind: Item.LastFired, Item.LastOutcome, and Item.LastCheckLine — the field whose own comment says it exists so *a watch that checked faithfully for thirty mornings and found nothing reads differently from one that never ran*.

THIS IS THE ONE HONEST ROUTE TO THAT SENTENCE. The inbox cannot deliver it: a run that came to nothing writes NO note anywhere, by contract (standing.go's QUIET IS THE DESIGN), so the line the design most wants is precisely the line the inbox refuses to carry. It is composed from the item instead, which every surface already holds.

AN ITEM THAT HAS NEVER BEEN LOOKED AT SAYS NOTHING — the emptiness law, and the same nothing a WhenHold rule always says, since nothing examines a rule and it will never have a look to report.

The item's OWN WORDS lead wherever it has them. `LastCheckLine` is written by the thing that looked — "it was the time you asked for", a sentinel's own sentence, "could not check: …" — and a page that replaced it with a phrase of its own would be throwing away the only account of what was actually seen.

func ParseEvery

func ParseEvery(every string) (func(now time.Time) time.Time, error)

ParseEvery reads a WhenEvery's Every: a five-field cron line, or a Go duration of at least one minute. It answers a function from "now" to the next moment.

The two dialects are told apart by a space, which a duration never has and a cron line always does.

func ProjectInboxDir

func ProjectInboxDir(root, workspace string) string

ProjectInboxDir is the folder one project's inbox sits in.

func ProjectInboxPath

func ProjectInboxPath(root, workspace string) string

ProjectInboxPath is that folder's inbox.jsonl.

func ProjectKey

func ProjectKey(workspace string) string

ProjectKey is the one name a workspace has under [projectsDirName]: the path with its separators turned to dashes, exactly the dumb one-way spelling cmd/codeaf gives a session bucket under v3/projects, so a person who goes looking recognises the folder names from the ones they already know.

IT IS NEVER DECODED AND NEVER JOINED TO A BUCKET. Decoding would be guessing which dashes were separators (internal/session's world.go says so about the bucket), and nothing here reads a bucket name or hands one out — every caller on all three sides holds the workspace path itself, so this is a key and not an address anybody has to reverse.

func RopeWord

func RopeWord(item Item) string

RopeWord is HOW MUCH ROPE this item has, in three rungs: RopeAsksFirst, `earning trust 3/5`, or RopeTrusted. It is the only derivation of that answer in the program, and every surface that draws the column reads it.

── THE RULE, AND WHY IT IS THIS ONE ──

Item.Grant is the only record in the whole program of a person letting a standing item act unattended: one sentence, in their own words, of *what acting on this may do without asking* — "open a pull request but never merge it" — written only when they actually said something like it, and its own documentation finishes the thought: **with none, it may only tell them things**. So a grant is the FLOOR: with none, nothing has been agreed and anything past telling somebody things is theirs to allow.

THIS IS THE OPPOSITE POLARITY TO THE ONE THE PLAN'S PARENTHETICAL ASSUMED. docs/design/home-rethink/LANES.md decision 5 glosses "asks first" as *the item's grant names something it must ask for*, which reads the field backwards: a grant names what it may do WITHOUT asking, so a grant is more rope and not less.

AND ABOVE THE FLOOR THERE ARE TWO RUNGS AND NOT ONE, WHICH IS A DECISION AND NOT A DERIVATION. This function argued at length, and until 2026-08-25 correctly, that there was no third rung — quoting standing.go's own *there are no probation counters: the rules are the tenure*. The owner has since ordered the counter ("follow the exact design please", recorded in docs/design/home-rethink/FIDELITY.md item 6), so the counter exists: an item is trusted alone once Item.CleanRuns reaches TrustAfter, and until then the column says how far along it is. The old argument is gone rather than left standing beside code that contradicts it — a comment arguing against the function under it is worse than no comment at all.

The design's own word is *tenured*. THE WORD SHIPPED IS *trust*, and the reason is not the one FIDELITY gives. That doc says the resident-separation test bans "tenure"; it does not — internal/tui3's manual_test.go bans five phrases and that is not among them, and internal/manual/chat/commands.md already ships the words "tenure after" for a settings row. The true reasons are better ones. First, *tenure* is the RESIDENT's own name for this exact mechanism — config.KeyTenureAfter, internal/resident/tenure.go, a charter earning tenure after so many clean firings — and CLAUDE.md's rule about the two products is that vocabulary does not travel between them, whether or not a test happens to catch a given word. Second, it is an employment term for a thing a person thinks of as trust. FIDELITY records this as a deviation in WORDING ONLY, which is right; only its stated cause needs correcting.

AND THE RESIDENT'S THRESHOLD IS NOT THIS ONE. config.TenureAfterAt is a setting over CHARTERS in that other product; TrustAfter is a constant over standing items in this one. They are the same idea about different objects, so this package does not read that setting — a data package reaching into the other product's configuration to answer a question about its own records would be the assumption-carrying CLAUDE.md forbids.

WHAT IS DELIBERATELY NOT IN THE RULE, because both would make the column say something it does not mean:

  • Item.NeedsPerson is a STATE and not rope. It is set while the latest run is stopped on a question and cleared by the next firing, so an item that walked into something this morning would flip its RUNG twice in a day — and how much rope a thing has is not a thing that changes while you are asleep. It does reset Item.CleanRuns, which is a different claim: the streak starts again, and the column says so by counting from nothing rather than by changing what it means.
  • Item.Exceptions narrow WHERE an item reaches — a workspace it skips, a conversation it stays out of — and not what it may do when it does reach. An item that runs everywhere but one repository has the same rope in the repositories it does run in.

func RunCameToNothing

func RunCameToNothing(runDir string) bool

RunCameToNothing answers whether a run folder's own marker says the run delivered nothing, which is the whole of the sweep's licence over it.

EVERYTHING ELSE ANSWERS FALSE: a run that said something, landed something or is waiting for the person; a marker that cannot be read; and a run with no marker at all. A folder that cannot say what it came to is a folder nobody may remove.

func SchemaOf

func SchemaOf(it Item) int

SchemaOf is the version an item is written at: the oldest one whose readers all keep its meaning.

AN ITEM CARRYING A DURABLE DELIVERY OR A TASK IN-FLIGHT MARKER IS VERSION 3, THE DEFERRED-DELIVERY BARRIER. A build that predates those fields decodes the document without them and would

  • drop a Item.Pending on its next write, losing an authorized line that was never acknowledged, or
  • run a ActionTask whose outcome is unknown, replaying arbitrary edits.

Version 2 alone is not safe (a v2 reader knows Action.Isolate but not Pending), so any item that carries them is written at 3 and an older build SKIPS it rather than decoding the loss back.

A POSITIVE IDENTITY OR A ONE-SHOT CONDITION IS VERSION 4, THE PROBE- RECURRENCE BARRIER. Neither field can ride version 3: the build that introduced the identity was itself a schema-3 reader, so it decodes a schema-3 document without the field and re-reports an unchanged positive. Version 4 is what that reader skips. The deferred fields keep version 3, so the fence is only on the semantics that need it.

EVERY ITEM TOUCHED BY THIS BUILD'S DEFERRED SEMANTICS STAYS VERSION 3, and an item this build has never rewritten keeps the oldest version whose readers still keep its meaning: a plain order created here is written at 1 and an isolated order at 2, so an ordinary reminder an older build created is still read and fired rather than hidden for no reason.

THE VERSION IS COMPUTED AT MARSHAL TIME (see [marshalItem]) so the revision this very write is about to mint is the one tested. An item this build has saved at least twice (Item.Revision > 1) carries the guarded-write generation a baseline reader would silently reset, and an armed file watch carries the baseline Item.Fingerprint it would silently drop, so both are fenced at 3 rather than left at 1 for a reader that would lie about them.

Types

type Action

type Action struct {
	// Isolate runs a task in a separate Git worktree, as shown on its approval
	// card. Permission prose never selects an execution directory.
	Isolate    bool       `json:"isolate,omitempty"`
	Kind       ActionKind `json:"kind"`
	Say        string     `json:"say,omitempty"`
	Brief      string     `json:"brief,omitempty"`
	Acceptance string     `json:"acceptance,omitempty"`
	Model      string     `json:"model,omitempty"`

	// Effort is the rung on the effort ladder this item's firings and its checks
	// ask for (internal/effort), and empty on almost every item.
	//
	// EMPTY IS NOT THE CHEAPEST RUNG, IT IS "NOBODY SAID". An item that says
	// nothing runs at the standing role's own floor — low — because a firing is
	// unattended and repeats forever, and an install dialled deep must not turn
	// every check on the machine into a deep pass. This field is how the one
	// item that genuinely needs thinking says so once and gets it every time.
	Effort string `json:"effort,omitempty"`

	MaxSteps int `json:"maxSteps,omitempty"`
}

Action is what a firing does. Say is read for ActionSay; Brief, Acceptance, Model, Effort and MaxSteps for ActionTask. Either kind may template the probe's evidence into its text with {{evidence}}.

type ActionKind

type ActionKind string

ActionKind is what a firing does.

const (
	// ActionSay delivers one line to the person — into the conversation that
	// asked, and onto home. A reminder, "CI is red", "Priya replied".
	ActionSay ActionKind = "say"
	// ActionTask runs work: the brief is carried out in the item's workspace by
	// a fresh unattended session of its own, bounded by the item's rails, and
	// what it came to is written down with a cost row.
	//
	// IT IS A SESSION AND NOT A WORKTREE, which is the runner's own statement of
	// itself (internal/session's standing_run.go) and worth saying here because
	// this constant used to promise otherwise. A firing is one turn in the
	// project the person pointed it at — it has no branch, no landing to accept,
	// and no hands to give work to.
	ActionTask ActionKind = "task"
)

type Altitude

type Altitude string

Altitude is an item's reach — which work it governs and which surfaces list it. It is decided on the ratification card and it never drifts afterward; widening it is a new card. docs/STANDING-ORDERS.md is the design.

const (
	// AltitudeConversation governs one conversation and dies with it. It
	// requires Origin.SessionID: a reach with no place to reach is an error.
	AltitudeConversation Altitude = "conversation"
	// AltitudeProject governs every conversation and every task in one
	// workspace. IT IS WHAT THE ZERO VALUE MEANS: every item made before
	// altitudes were spelled was workspace-scoped, so an empty altitude reads
	// as this and nothing migrates.
	AltitudeProject Altitude = "project"
	// AltitudeMachine governs everything the person does on this machine. It
	// keeps the existing convention that a machine-wide item's workspace is
	// the person's home.
	AltitudeMachine Altitude = "machine"
)

type Brief

type Brief struct {
	Title  string `json:"title,omitempty"`
	Prompt string `json:"prompt,omitempty"`
}

Brief is the working half of an item: a short title for rows too narrow for a sentence, and the compiled prompt the machinery follows. The person's Words are NEVER rewritten — they are the reason the item exists, and every surface that opens the item shows both halves, words first. An empty Brief reads as the Words themselves. Editing a brief down (narrower, gentler) is free; editing it up (more reach, more action) is a new ratification card — the session lane enforces that law, not this package.

type CardKind

type CardKind string

CardKind is what a proposal card calls this item. It is derived from the when, because that is the fact a person can check: a moment is a reminder, a rhythm is a repeating check, a condition is a watch, and a hold is a rule.

const (
	// CardReminder is one moment. Doing it now is not a smaller version of it.
	CardReminder CardKind = "reminder"
	// CardCheck repeats on a cadence.
	CardCheck CardKind = "check"
	// CardWatch waits on a condition or an event.
	CardWatch CardKind = "watch"
	// CardRule is kept, and never wakes.
	CardRule CardKind = "rule"
)

type Deliverer

type Deliverer interface {
	// Deliver carries out a pending delivery whose identity was already
	// written to the item. It answers the same [Outcome] Runner.Say does.
	Deliver(ctx context.Context, item Item, pending Pending) (Outcome, error)
}

Deliverer is the optional capability a Runner may add when it can make a delivery durable by IDENTITY. When a Runner implements it, the core hands it the Pending record it already wrote to the item, so the same line delivered twice across a restart carries the same identity and the second is deduped rather than repeated.

A RUNNER THAT DOES NOT IMPLEMENT IT STILL WORKS: the core falls back to Runner.Say, which has no identity to dedup on. That fallback is at-least- once for a delivery whose acknowledgement was lost, which is stated plainly and is why strong guarantees require this seam (see the integration contract).

type DrainFile

type DrainFile struct {
	// Notes are the notes read whole from this file, oldest first, EXCLUDING any
	// identity already in the drained-identity record: an identity there was
	// handed over and acknowledged on an earlier drain, so it is retired without
	// being shown again. A READ FAILURE in this very file yields no DrainFile at
	// all (it is kept and reported instead).
	Notes []Note
	// contains filtered or unexported fields
}

DrainFile is one staged inbox file and the acknowledgement that retires it: the notes read from it that the caller has NOT already durably received, and the file those notes came from.

NOTHING IS SPENT UNTIL DrainFile.Ack, AND THE CALLER MUST ACK ONLY AFTER ITS OWN RECEIPT IS DURABLE. The file stays on disk and the drained-identity record stays as it was until then, so a crash between the read and the acknowledgement hands the same notes again — a duplicate the person might have read, never a note nobody was shown. The session lane's durable receipt is the journal line its fold is written as ([durableDelivery] and [sessionFile.recorded]); it acks from the settle callback, which fires only after that line reached the file.

func Drain

func Drain(sessionDir string) ([]DrainFile, error)

Drain reads a session's inbox, oldest first, and answers ONE DrainFile per staged file the caller must acknowledge. An absent inbox is an empty answer and no error.

IT RECOVERS A LEFTOVER .draining FILE DETERMINISTICALLY, even alongside a live inbox: a crash after the rename and before the acknowledgement would otherwise lose the notes for good. It reads every staged file, and a file it could NOT read whole (an open failure, a read failure, or a line too long for the scanner) is KEPT and reported rather than handed over or removed.

IT CONSULTS THE DRAINED-IDENTITY RECORD TO SKIP WHAT WAS ALREADY ACKNOWLEDGED. The record is written only by DrainFile.Ack, which a caller runs only after its own receipt is durable, so an identity there means the line already reached the person through that receipt. Skipping it here is what stops a residual file — an acknowledgement whose removal failed — from replaying forever. An identity NOT in the record is handed over however long its file has sat.

THE GUARANTEE IS A DURABLE AT-LEAST-ONCE HANDOFF, NOT EXACTLY-ONCE DISPLAY. A note acknowledged through DrainFile.Ack was handed to the caller AND recorded as spent; whether the caller then draws, folds or forwards it is outside this package. A crash before the acknowledgement shows the note again. The drained-identity window is [inboxSeenKeep]; beyond it a replay is shown again.

A caller MUST use the notes it is handed even when the error is non-nil: the error says a sibling file could not be read, not that every note is unusable.

func DrainProject

func DrainProject(root, workspace string) ([]DrainFile, error)

DrainProject reads a project's inbox, oldest first — Drain at the project's address, answering the same DrainFile acknowledgements its caller must run.

func (DrainFile) Ack

func (f DrainFile) Ack() error

Ack marks this file's notes as spent and removes the file, under the one inbox flock. It is idempotent: a file already gone is already retired.

THE RECORD GOES DOWN BEFORE THE FILE COMES OFF. If the drained-identity record cannot be read or written, the file is KEPT and the failure rides out: the next drain re-hands the notes rather than losing them. Once the record holds the ids, a removal failure only means the file is read again — and its notes are skipped because they are spent — so it can never be shown twice or lost.

func (DrainFile) Path

func (f DrainFile) Path() string

Path is the staged file this acknowledgement retires. It is the identity a caller dedups repeated drains on — a second Drain before this one is acknowledged returns the same path for the same notes.

type Entry

type Entry struct {
	At     time.Time `json:"at"`
	ItemID string    `json:"item"`
	// Kind is "check" (a probe + sentinel), "say", or "task".
	Kind string  `json:"kind"`
	USD  float64 `json:"usd"`
	// Run is the run folder, for a firing.
	Run string `json:"run,omitempty"`
	// Pending is the delivery identity this firing settled, when it settled
	// one: the identity of the [Pending] intent the line was carried out
	// under. The line is written only AFTER the delivery succeeded, so its
	// presence in the append-only ledger is NATIVE DURABLE FIRING EVIDENCE.
	// [Ticker.settleItem] reads it to settle an intent whose item write was
	// lost — a one-shot whose line already reached the person retires from
	// this evidence instead of being delivered a second time.
	Pending string `json:"pending,omitempty"`
	// Outcome is what the firing came to ([Outcome.Kind]: said, landed,
	// needs-you, failed or nothing). [Item.LastOutcome] is overwritten by the
	// next firing, so this field is the only durable record of what this one
	// outcome was; a settlement reads it back rather than inventing one.
	Outcome string `json:"outcome,omitempty"`
	// Line is the one sentence the firing carried (the sighting line
	// [Ticker.fire] recorded as its check line), which is what the firing wrote
	// into [Item.LastCheckLine] and [Item.Previous]. It is kept here so a
	// settlement of this firing can restore both from the record it left behind
	// rather than from a fresh clock.
	Line string `json:"line,omitempty"`
	// Needs is the one line the firing left waiting for the person
	// ([Item.NeedsPerson]), empty when it left none. A firing that stopped on a
	// question is a REACHED outcome and not a clean success, so the record
	// carries the question itself: a settlement restores it, and derives the
	// clean-run counter from it, rather than reading any line on the ledger as
	// a clean success.
	Needs string `json:"needs,omitempty"`
}

Entry is one line of the daily ledger: one firing, or one sentinel call, with what it cost. The rails are sums over today's lines.

type Exception

type Exception struct {
	Workspace string    `json:"workspace,omitempty"`
	SessionID string    `json:"sessionId,omitempty"`
	At        time.Time `json:"at"`
}

Exception is one place an item deliberately does not reach: a workspace, or a single conversation. Exactly one field is set. Exceptions are made by the person — from the place ("not here"), or from the item's own record pointing at a place — and never by the machinery. Both gestures write the same fact.

type Idle

type Idle func(for_ time.Duration) bool

Idle answers whether the machine is quiet enough for a WhenIdle: no live presence file says busy, and the last person activity anywhere is older than the given duration. The session lane supplies it from the world reader.

type Item

type Item struct {
	Schema int    `json:"schema"`
	ID     string `json:"id"`
	// Words are the person's verbatim sentence. Permanent anchor; every
	// surface leads with it.
	Words string `json:"words"`
	// Workspace is the REAL project root the item belongs to — the resolved git
	// root, an owned work/ directory, or the person's home for a machine-wide
	// item. It is what home groups by and where a task firing runs.
	Workspace string `json:"workspace"`
	Origin    Origin `json:"origin"`
	When      When   `json:"when"`
	Does      Action `json:"does"`
	Rails     Rails  `json:"rails"`
	// Altitude is the item's reach (see [Altitude]); empty reads as project.
	Altitude Altitude `json:"altitude,omitempty"`
	// Brief is the working title and compiled prompt; empty reads as Words.
	Brief Brief `json:"brief,omitempty"`
	// Grant is one sentence of what acting on this item may do without asking,
	// quoted on the card that ratified it. Empty means say-only, which is what
	// every item made before grants were spelled could do.
	Grant string `json:"grant,omitempty"`
	// Exceptions are the places this item deliberately does not reach.
	Exceptions []Exception `json:"exceptions,omitempty"`

	Status  Status    `json:"status"`
	Created time.Time `json:"created"`
	Updated time.Time `json:"updated"`
	// Revision counts this document's writes. It is the generation a pass that
	// has been holding a copy for minutes compares against before it writes:
	// if the number moved, somebody edited, paused or resumed the item while the
	// pass was looking, and THEIR ACT IS NEWER THAN THE PASS'S STALE COPY. A
	// status-only check is not enough — a pause and a resume both leave the
	// status active — so the count is what the guard actually reads.
	Revision int64 `json:"revision,omitempty"`
	// RetiredWhy says why a retired item retired: "fired", "expired",
	// "stopped by you", or the sentence the last failure left.
	RetiredWhy string `json:"retiredWhy,omitempty"`

	// The quiet half. LastChecked and LastCheckLine are what let a watch that
	// checked faithfully for thirty mornings and found nothing read differently
	// from one that never ran.
	LastChecked   time.Time `json:"lastChecked,omitempty"`
	LastCheckLine string    `json:"lastCheckLine,omitempty"`
	NextDue       time.Time `json:"nextDue,omitempty"`
	// Fingerprint is a WhenFile's last reading.
	Fingerprint string `json:"fingerprint,omitempty"`
	// Positive is a WhenProbe's last AFFIRMATIVE reading, as an identity: a
	// bounded digest of the exact evidence the check observed when the sentinel
	// last said yes. It is what bounds an UNCHANGED continuous positive from
	// being reported again, independently of the words the sentinel happens to
	// use on a later check: while the observed reading is byte-for-byte the one
	// already reported, the watch is quiet, and a decided no clears it so a
	// later true is a fresh edge again.
	//
	// IT IS AN IDENTITY AND NOT A REASON. Nothing here reads the model's line,
	// because a model that says "same state already reported" and answers yes in
	// the same breath is exactly the failure this field exists to bound. It is
	// also not the evidence: only the digest is kept, so no probe output and no
	// secret it carried is retained.
	//
	// IT IS EMPTY WHEN THE READING CANNOT CERTIFY ANYTHING. A reading the clip
	// cut short ([ProbeClip]) is a partial view, so a repeated look at it must
	// not be passed off as "the same state"; the identity stays empty and the
	// sentinel keeps deciding. An empty identity is never a suppression.
	Positive string `json:"positive,omitempty"`
	// Previous are the last [Previous] sentinel lines, newest first, each with
	// what came of it.
	Previous []string `json:"previous,omitempty"`
	// Pending are deliveries this item means to make but has not confirmed. A
	// delivery record is written BEFORE the line is carried out and cleared only
	// when the delivery is acknowledged, so a crash between the two leaves the
	// intent on disk and a later pass reconciles it BY IDENTITY instead of
	// guessing. It is bounded by [PendingKeep] and empty on almost every item.
	// A later pass settles it at the top of the walk, before any fresh look, so
	// an advanced prerequisite can never strand it.
	Pending []Pending `json:"pending,omitempty"`
	// TaskInflight, when set, is the durable marker of a task attempt that began
	// but whose outcome is not known: written BEFORE [Runner.Run] and cleared
	// only on a terminal outcome. A later pass refuses to run the task again
	// while it is set and reconciles it as [Item.NeedsPerson] rather than
	// replaying arbitrary edits. It makes NO exactly-once promise about what a
	// task did; it makes replay STOP.
	TaskInflight *TaskInflight `json:"taskInflight,omitempty"`

	// The ledger half, kept on the item for the card; the daily ledger is the
	// truth for the rails.
	Runs        int       `json:"runs"`
	LastFired   time.Time `json:"lastFired,omitempty"`
	LastOutcome string    `json:"lastOutcome,omitempty"`
	LastRun     string    `json:"lastRun,omitempty"`
	SpentUSD    float64   `json:"spentUsd"`
	// NeedsPerson is set while the latest run is stopped waiting on the person,
	// with the one line it is stopped on. Home sorts on it.
	//
	// IT CARRIES TWO DIFFERENT THINGS, and a reader has to know which. One is a
	// QUESTION the firing put to the person, in its own words. The other is the
	// line written when a call was refused for want of somebody to allow it,
	// which opens with [NeedsPermissionLead].
	//
	// The pass writes this field whole on the next firing. The PERSON can put
	// down the permission line by changing the item ([Item.ClearNeedsPerson]),
	// and can never lose a question that way. With only the pass, an item that
	// could not fire again — one that had spent its allowance for the day — kept
	// a row on home saying it needed somebody for as long as that stayed true,
	// with no act of theirs able to put it down.
	NeedsPerson string `json:"needsPerson,omitempty"`
	// CleanRuns is HOW MANY FIRINGS IN A ROW CAME BACK CLEAN — fired with
	// nothing waiting for the person and no failure. It is the count the rope
	// column's middle rung is drawn from ([RopeWord]), and a firing that stops
	// on a question or fails puts it back to nothing.
	//
	// IT IS RECORDED AND NOT DERIVED, and that is a deliberate loss of
	// elegance. Everything else this struct keeps about a firing is the LATEST
	// one — LastOutcome is overwritten every time — so a streak simply is not in
	// the record. The one field that looks like it might do is [Item.Previous],
	// which holds the last few sentinel judgments with `" → " + outcome.Kind`
	// glued on the end; reading a streak out of that would mean splitting
	// model-authored prose on an arrow, and it would silently tie a product
	// threshold to [Previous], whose size exists to bound a PROMPT. Somebody
	// trimming that prompt by two lines would quietly make trust unreachable.
	// So the count is kept at the one site that records a firing
	// ([Ticker.fire]), which is the same place [Item.Runs] is kept.
	//
	// AN ITEM WRITTEN BEFORE THIS FIELD EXISTED DECODES AS ZERO and starts
	// earning trust again. That is the conservative direction and the only
	// honest one: nothing on disk says those firings were clean, and a column
	// that assumed they were would be granting rope nobody measured.
	CleanRuns int `json:"cleanRuns,omitempty"`
}

Item is one standing thing. The top half is what the person agreed to and never changes without another card; the bottom half is the item's own present, rewritten on every check.

func (Item) AppliesTo

func (it Item) AppliesTo(workspace, sessionID string) bool

AppliesTo answers whether this item governs the given place: its reach, minus the places the person kept it out of. An exception beats every altitude.

func (Item) CardKindOf

func (it Item) CardKindOf() CardKind

CardKindOf reports which card this item is. A when this build does not know is read as a watch: it is something to look for, and the card says so.

func (Item) ClearNeedsPerson added in v0.4.0

func (it Item) ClearNeedsPerson() Item

ClearNeedsPerson puts down the line about a permission a firing could not get, on the person's own act of changing the item. The acts are the ones this build has: pausing it, stopping it, and letting it go again. What a run before that could not be allowed to do is no longer news about what this item will do next.

IT LEAVES A QUESTION ALONE, and that is the whole of why it reads the line before clearing it. This field carries two different things. One is a QUESTION the firing actually put to the person, in its own words, which is theirs to answer and which nothing may throw away behind their back — pausing a watch is not answering it. The other is the line this build writes when a call was refused for want of somebody to allow it, which goes stale the moment the item changes. Only the second is put down.

IT IS A CHANGE AND NOT A LOOK. Opening an item and closing it again leaves the row exactly as it was, because nothing about the item moved and the reason it stopped is still true.

func (Item) ExceptedFrom

func (it Item) ExceptedFrom(workspace, sessionID string) bool

ExceptedFrom answers whether the person excepted this item from the place.

func (Item) Glyph

func (it Item) Glyph(running bool) string

Glyph is the one character a row leads with, decided here so every surface agrees: ▲ needs you, ● a pass has it in its hands right now — checking it or firing it — ◦ waiting for its time, ∙ paused or retired. Running is the store's knowledge and not the item's, so it is passed (Store.Running).

func (Item) Level

func (it Item) Level() Altitude

Level is the altitude with the zero value resolved to its meaning.

func (Item) Prompt

func (it Item) Prompt() string

Prompt is the instruction the machinery follows: the compiled brief, or the person's words themselves when nobody compiled one.

func (Item) Reaches

func (it Item) Reaches(workspace, sessionID string) bool

Reaches answers whether this item's altitude covers the given place, WITH EXCEPTIONS IGNORED. It exists because a page drawing its dim "not here" lines asks exactly "would this have applied but for the person keeping it out" — and before it was in the contract, the one caller answered that by copying the item and clearing its exceptions, which is the contract's own arithmetic written a second time. Callers pass what they know; an empty sessionID is a place with no conversation (a task's worktree, a firing).

func (Item) Spends

func (it Item) Spends() bool

Spends answers whether anything about this item can ever cost money, and it is the ONE PLACE that question is decided.

EVERY WAKING KIND CAN. A probe runs, the sentinel judges it, a firing works — all three are billed, which is why Item.Validate refuses one of them with a zero budget. A HOLD CANNOT: nothing wakes it, so nothing about it is ever bought. That is why it alone may carry no rails, and it is why the ratification card draws no cost band for a rule — THE EMPTINESS LAW IS THE OTHER HALF OF THE SENTENCE, and a figure nobody can spend is a figure no surface may print.

func (Item) Title

func (it Item) Title() string

Title is what a row too narrow for a sentence leads with: the brief's title, or the words themselves when nobody wrote one.

func (Item) Validate

func (it Item) Validate() error

Validate is what Store.Create and Store.Save refuse on. It is the whole admission law in one place: words, a workspace, a kind with its fields, an action with its text, and rails that are not zero.

type Judgment

type Judgment struct {
	Item     Item
	Evidence string
	Previous []string
}

Judgment is what the sentinel is asked: the person's words, the hint, the evidence a probe gathered, and what the sentinel said the last few times.

type Note

type Note struct {
	At     time.Time `json:"at"`
	ItemID string    `json:"item"`
	Words  string    `json:"words"`
	// Kind is "said", "landed", "needs-you", or "failed".
	Kind string `json:"kind"`
	Text string `json:"text"`
	// Run is the run folder a person can open for the whole story.
	Run string `json:"run,omitempty"`
	// ID is the delivery identity, when the writer had one: the identity of
	// the [Pending] record the core wrote before the line was carried out.
	// Drain and Peek dedup on it, so a line appended twice across a restart or
	// a torn write reaches the person once. Empty notes — every note written
	// before this field existed, and every writer that has no identity — are
	// passed through untouched.
	ID string `json:"id,omitempty"`
}

Note is one line of news for a conversation: a firing's delivery, a needs-your-look, a failure. It is appended to <session dir>/inbox.jsonl when the conversation's window is not open, and drained into one "while you were away" fold the next time it is.

func PeekProjectInbox

func PeekProjectInbox(root, workspace string) []Note

PeekProjectInbox reads a project's inbox WITHOUT emptying it, oldest first.

It is what a screen calls. Home draws what is waiting every time it redraws, and a read that emptied the file would mean the first draw of a project card consumed the news the conversation was supposed to fold in. Draining is the conversation's act and this is the looking.

type Origin

type Origin struct {
	// SessionID and Transcript name the conversation the card was answered in.
	SessionID  string `json:"sessionId,omitempty"`
	Transcript string `json:"transcript,omitempty"`
	// Exchange is set instead when the item was made from home's own box: the
	// short exchange that produced it is kept under the item's folder
	// ([Store.ExchangeDir]) and not as a project session. Promoting it to a
	// conversation moves the folder and fills SessionID.
	Exchange string `json:"exchange,omitempty"`
	// TaskID is set when a finishing task proposed the item itself.
	TaskID int `json:"taskId,omitempty"`
	// TurnIDs are the turns of the origin session that were about making or
	// changing this item. Home uses them to tell a conversation that was only
	// ever about this item from one that merely contains it.
	TurnIDs []string `json:"turnIds,omitempty"`
	// PinnedModel and OneModel are the RATIFYING CONVERSATION'S MODEL POLICY,
	// frozen at the moment the person said yes. They are on the origin because
	// a firing is that conversation's work done later: a background pass
	// reloads a keyless posture whose tiers, role pins and fallback ladder are
	// the profile's, and without the pin an item made under `--one-model` would
	// quietly route its sentinel and its child work through a different model
	// the person had already promised away. OneModel is whether the promise was
	// in force; PinnedModel is the model it named (the conversation's own), and
	// is the model every TEXT seat of this item must use when it is set.
	//
	// IT IS WRITTEN AT SCHEMA 3 WHEN SET (see [SchemaOf]): a build that predates
	// these fields decodes the document without them and would re-marshal the
	// silent re-routing back, so such an item is fenced rather than left to be
	// read as an ordinary unpinned order.
	PinnedModel string `json:"pinnedModel,omitempty"`
	OneModel    bool   `json:"oneModel,omitempty"`
}

Origin is where an item was asked for. It is provenance and it is the door home opens: "why did I get this?" opens the conversation that made it.

type Outcome

type Outcome struct {
	// Kind is "said", "landed", "needs-you", "failed", or [OutcomeNothing].
	Kind string
	Text string
	USD  float64
	// NeedsPerson is the one line the run stopped on, when Kind is needs-you.
	NeedsPerson string
}

Outcome is what a run came to.

type Pass

type Pass struct {
	At       time.Time
	Examined int
	Checked  int
	Fired    int
	Said     int
	NeedsYou int
	Skipped  int
	Errors   int
	// Tidied is how many remembered lines the consolidation pass moved, which
	// is zero on all but a handful of passes a day (see [Tidy]).
	Tidied int
	// Unread is how many standing documents [Store.List] skipped because they
	// could not be read — a newer schema, a damaged file, a dangling link.
	// UnreadWhy says which and why in ONE bounded line. A pass that skipped a
	// document says so; absence of evidence is not success.
	Unread    int
	UnreadWhy string
	// Notes are one sentence per thing worth saying, for the log.
	Notes []string
}

Pass is what one tick decided, for the wake log and for /status.

type Pending

type Pending struct {
	// ID is the delivery identity, stable across restart, and the thing an
	// inbox dedups on.
	ID string `json:"id"`
	// Kind is [ActionSay] today; it names what was being delivered.
	Kind ActionKind `json:"kind"`
	// Text is the line, already templated, that was meant to be said.
	Text string `json:"text,omitempty"`
	// At is when the intent was written.
	At time.Time `json:"at"`
	// Attempts counts how many passes have tried to settle this intent and
	// failed, so a line that cannot be delivered becomes an explicit
	// [Item.NeedsPerson] rather than retrying forever in silence.
	Attempts int `json:"attempts,omitempty"`
}

Pending is one delivery the item means to make, written to the item before the line is carried out so that a crash cannot lose it and a later pass can reconcile it by identity. It is the durable half of ActionSay. It carries an identity and an attempt count; it makes no exactly-once promise about anything a task may do — Item.TaskInflight is what stops arbitrary replay.

type Probe

type Probe struct {
	Command string          `json:"command,omitempty"`
	Tool    string          `json:"tool,omitempty"`
	Args    json.RawMessage `json:"args,omitempty"`
}

Probe is one look at the world: a shell command in the workspace, OR a belt tool with arguments. Exactly one is set. Output is what the sentinel reads, clipped to ProbeClip bytes from the tail.

type ProbeReading

type ProbeReading struct {
	// Text is the evidence, already clipped from the tail where it was too long.
	Text string
	// Clipped reports that Text is only part of what the probe read.
	Clipped bool
}

ProbeReading is what one look at the world answered: the evidence the sentinel is shown, and whether the runner had to keep only part of what it read.

A CLIPPED READING CANNOT CERTIFY AN UNCHANGED STATE. Two different full readings can share the one tail a clip leaves — a log whose new failure is in the dropped head still wears the same footer — so a look built from a clipped reading carries no identity and a repeat of it is judged afresh rather than suppressed as "the same state". A reading that fits is whole and may be hashed.

type Rails

type Rails struct {
	// PerRunUSD is the most one firing may spend, probe and sentinel included.
	//
	// ZERO IS NO LIMIT, and it always was at the place that enforces it —
	// internal/session's standing_run.go has only ever stopped a firing when
	// `PerRunUSD > 0` — so a person who wants a standing order bounded by
	// nothing but the daily rail writes 0 here and gets exactly that.
	// [Item.Validate] used to refuse that number, which made the enforcement
	// site's own contract unreachable.
	PerRunUSD float64 `json:"perRunUsd"`
	// MaxPerDay is how many times it may fire in one local day.
	MaxPerDay int `json:"maxPerDay"`
	// Expires retires the item at that moment. Zero is never. A WhenAt item
	// expires a day after its moment whatever this says.
	Expires time.Time `json:"expires,omitempty"`
}

Rails bound an item. MaxPerDay is mandatory by construction: Store.Create refuses an item that may fire zero times a day, which is an item that would never fire at all. They stay quiet on the proposal card unless the person named money themselves, because the ordinary promise is the machine-wide daily allowance.

type Runner

type Runner interface {
	// Probe runs the item's probe and answers what it saw, with whether the
	// reader had to cut it short. A CLIPPED READING IS NOT AN IDENTITY (see
	// [ProbeReading]): the runner that cut it must say so, because a tail that
	// fits under the cap is indistinguishable from a whole short reading.
	Probe(ctx context.Context, item Item) (ProbeReading, error)
	// Say delivers one line: into the origin conversation if it is open in
	// this process, else into its inbox, and always onto the item.
	Say(ctx context.Context, item Item, text string) (Outcome, error)
	// Run runs the item's task brief in a fresh headless session at runDir,
	// with the evidence available to the brief, bounded by the item's rails.
	Run(ctx context.Context, item Item, runDir, evidence string) (Outcome, error)
}

Runner is supplied by the session lane. It is how a firing touches the world: a probe run in the item's workspace, a line delivered, a task run headless under the person's banked rules in a fresh session folder at runDir.

type RunningMark

type RunningMark struct {
	PID   int       `json:"pid"`
	Since time.Time `json:"since"`
	// What is [RunningChecking] or [RunningFiring]. A surface says it in those
	// words — "checking now", "firing now" — so it is the person's vocabulary
	// and not a state name.
	What string `json:"what"`
}

RunningMark is what a pass leaves behind while it has one item in its hands: which process is doing it, since when, and which half of a pass it is in. It is the answer Store.Running gives and the whole of what running.go writes.

IT IS A CLAIM ABOUT NOW AND IT IS ALWAYS DOUBTED. A process that was killed mid-firing leaves its marker behind, so every reader treats a dead pid or an age past TickWindow as no marker at all (running.go's markLive).

type Sentinel

type Sentinel func(ctx context.Context, judgment Judgment) (yes bool, line string, usd float64, err error)

Sentinel is one cheap yes/no call. The line is kept on the item and in the log; it is read by the person, so it is one plain sentence.

IT IS THE LEGACY SHAPE and it stays source-compatible on purpose: every adapter and test written before SentinelReading existed keeps compiling and keeps its old meaning. A yes is VerdictYes, a no is VerdictNo, and an error — a refusal, a timeout, a provider that could not answer — is VerdictUnknown rather than an established no.

type SentinelReading

type SentinelReading int

SentinelReading is a sentinel's three-valued answer, and the reason this package stopped speaking in booleans.

FALSE AND UNKNOWN ARE DIFFERENT FACTS. A decided no is the world saying the condition does not hold; a refusal, a timeout, an ambiguous output or a provider hiccup says only that nobody could tell. Collapsing the second into the first would write a negative into the item's own history (Item.Previous) and quietly drop a pending opportunity the person is still waiting on. Only VerdictYes fires; VerdictUnknown leaves the opportunity open and records nothing as decided.

const (
	// VerdictUnknown is "nobody could tell": a refusal, a timeout, an
	// ambiguous answer. It neither fires nor counts as an established no.
	VerdictUnknown SentinelReading = iota
	// VerdictNo is a decided negative.
	VerdictNo
	// VerdictYes is a decided affirmative, and the only verdict that fires.
	VerdictYes
)

type SentinelVerdict

type SentinelVerdict func(ctx context.Context, judgment Judgment) (verdict SentinelReading, line string, usd float64, err error)

SentinelVerdict is the three-valued form of Sentinel. A Ticker that carries one uses it and ignores Sentinel; a Ticker that carries only a Sentinel keeps the legacy two-valued meaning. It is an optional capability, discovered by a field rather than by a type assertion on an interface, so a caller that has not heard of it still builds.

type SkippedDoc

type SkippedDoc struct {
	Name   string
	Reason string
}

SkippedDoc is one document List could not read: its file name and why. It is a diagnostic and not an item; the document stays on disk untouched so an older or newer build that understands it still can.

type Spend

type Spend struct {
	USD   float64
	Fired int
}

Spend is what today's ledger says, for one item or for all.

type Status

type Status string

Status is where an item is in its life. There is no "proposed": a proposal is a card in a conversation, and only a yes makes an item.

const (
	StatusActive  Status = "active"
	StatusPaused  Status = "paused"
	StatusRetired Status = "retired"
)

type Store

type Store struct {
	// contains filtered or unexported fields
}

Root is where everything standing lives: <codeaf home>/v3/standing. Callers pass it in rather than this package reading internal/home, so a test's store is a temp dir and nothing else.

func Open

func Open(root string) (*Store, error)

Open answers the store at root, creating the directory. It holds no handles.

func (*Store) Append

func (s *Store) Append(entry Entry) error

Append writes one entry to today's ledger with O_APPEND.

func (*Store) Applicable

func (s *Store) Applicable(workspace, sessionID string) ([]Item, error)

Applicable is THE ONE RESOLVER: which standing things reach this place, in the order a person reads them. Every seam that asks "what stands over here?" asks this and nothing else, so a conversation, a page and a task's world can never come to three different answers about one item.

IT IS THREE SHELVES AND NOT A SORT KEY. This conversation's own orders lead, then this project's, then the machine's — narrowest first, because the nearest one is the one somebody just made and the one they mean when they say "not here". Within a shelf the most recently touched leads, and that is Item.Updated rather than Created: an order paused, excepted or edited this morning is the one they are thinking about.

ONLY WHAT IS ACTUALLY STANDING. A paused order governs nothing while it is paused and a retired one is over, so neither is here — a list that included them would be saying something is true of this place that is not.

Callers pass what they know; an empty sessionID is a place with no conversation (Item.AppliesTo), which is what a task's worktree and a firing both are.

func (*Store) Arm

func (s *Store) Arm(id string) (Item, error)

Arm captures a WhenFile item's baseline NOW, at ratification, and writes it, so the reading the first tick compares against is the world as it stood when the person said yes rather than the world five minutes later.

IT IS A SEPARATE CALL AND NOT A LINE IN Store.Create ON PURPOSE. Create is passive: it validates a document and writes it, and nothing about a passive write may touch the filesystem the item points at or run a probe. Arming is the ratifier's act — the session lane calls it after the card is answered — and it is what makes "a change between the card and the first wake" a change rather than the baseline. An item that is never armed keeps the old, silent first-reading semantics exactly.

IT READS AND WRITES UNDER THE ITEM'S OWN LOCK, so an edit that lands while the scan runs is not overwritten by the whole document this call holds: the document written is the one on disk with only the fingerprint set. A scan that could not read everything it matched ([fingerprint]'s truncated answer) sets NO baseline and answers [errBaselineIncomplete] — a partial reading passed off as the baseline would make "nothing has changed" a lie.

func (*Store) Create

func (s *Store) Create(item Item) (Item, error)

Create validates, assigns an id when there is none, stamps Created, Updated and Status active, writes the document, and answers the item as written. IT IS ONLY CALLED AFTER A YES.

func (*Store) ExchangeDir

func (s *Store) ExchangeDir(id string) string

ExchangeDir is where a home-made item's origin exchange is kept.

func (*Store) ForWorkspace

func (s *Store) ForWorkspace(workspace string) ([]Item, error)

ForWorkspace is List filtered to one project, the grouping home draws.

func (*Store) Get

func (s *Store) Get(id string) (Item, error)

Get reads one item. A missing id is ErrNotFound.

func (*Store) ItemDir

func (s *Store) ItemDir(id string) string

ItemDir is <root>/<id>/ — the item's own folder: runs/, exchange/, log.

func (*Store) ItemPath

func (s *Store) ItemPath(id string) string

ItemPath is <root>/<id>.json.

func (*Store) LedgerPath

func (s *Store) LedgerPath(day time.Time) string

LedgerPath is the append-only daily ledger the rails are summed from.

func (*Store) List

func (s *Store) List() ([]Item, error)

List reads every item, newest first. An unreadable or newer-schema document is skipped, not fatal.

SKIPPING IS THE POINT. One document written by a build from the future, or one truncated by a full disk, must not be able to stop every other standing thing a person owns from being checked. List keeps the original erased shape; Store.ListChecked is the same walk that also says what it skipped, which is what a pass's wake log needs so that "nothing was examined" is never read as "nothing was wrong".

func (*Store) ListChecked

func (s *Store) ListChecked() ([]Item, []SkippedDoc, error)

ListChecked reads every item newest first and, beside them, reports each document it skipped and the reason. An unreadable or newer-schema document is skipped rather than fatal, but it is NEVER SILENT: the reader that has a wake log (Ticker.Tick) writes the skipped set into it, so a pass is not allowed to pass by the absence of a document it could not read.

func (*Store) LockPath

func (s *Store) LockPath() string

LockPath is the flock one ticker at a time holds. A window takes it for the length of a pass; `codeaf tick` refuses when it is held.

func (*Store) Log

func (s *Store) Log(id, line string) error

Log adds one line to an item's own log: a check that found something, a firing, a pause, a stop. NEVER A LINE PER QUIET CHECK — a watch that looks every five minutes for a year would otherwise leave a hundred thousand lines saying nothing happened.

func (*Store) LogPath

func (s *Store) LogPath(id string) string

LogPath is the item's own one-line-per-event log: checks that found something, firings, pauses. Never a line per quiet check.

func (*Store) NoteNeedsPerson

func (s *Store) NoteNeedsPerson(id, note string) error

NoteNeedsPerson makes a line visible on an item that has none, under the item's own lock, and answers the item as written.

IT NEVER OVERWRITES A QUESTION. A firing's own question is the item's words put to the person; a ratifier that could not establish a file watch's baseline is a lesser fact and must not replace it. An empty field is filled and the document re-marshalled; the revision is bumped because a person's later edit must still be seen as newer than a pass holding a copy from before.

func (*Store) NoteSpend

func (s *Store) NoteSpend(id string, usd float64) error

NoteSpend adds a judged check's cost to the item's lifetime figure, under the item's own lock, without touching anything else on the document.

WHY IT EXISTS BESIDE THE LEDGER. The daily rail is read from the LEDGER, so a paid judgment that could not be decided already counts against the day. The item's own lifetime figure is read from Item.SpentUSD and a card quotes it, and an undecided check writes NOTHING on the item ([Ticker.one]'s stateUndecided) — so before this door the item's figure silently lost every refusal, timeout and ambiguity that was still billed. Adding it HERE, without NextDue, Fingerprint, NeedsPerson, LastChecked or a status change, keeps the undecided look from consuming the opportunity while still recording what it cost. A failed write is returned, so the pass counts it rather than losing it in silence.

func (*Store) Root

func (s *Store) Root() string

Root is the directory the store was opened on.

func (*Store) Running

func (s *Store) Running(id string) (RunningMark, bool)

Running answers whether a pass has this item in its hands at this instant, and what it is doing with it.

FALSE IS THE ANSWER TO EVERY DOUBT: no marker, a marker that cannot be read, a marker with nothing to say, a marker whose process is gone, and a marker older than one pass may last. A surface draws `●` on a true and nothing at all on a false, so every uncertainty here is a glyph that stays still.

func (*Store) RunningPath

func (s *Store) RunningPath(id string) string

RunningPath is the marker's place: <root>/<id>/running, inside the item's own folder beside its runs and its log. It is NOT a `.json` beside the document, because Store.List reads that directory by suffix and a second document shape there would be one more thing every reader has to skip.

func (*Store) RunsDir

func (s *Store) RunsDir(id string) string

RunsDir is where an item's firings live, one session folder each, numbered. They live here and NOT under v3/projects so home never scans them.

func (*Store) RunsSince

func (s *Store) RunsSince(from time.Time) (map[string]Spend, error)

RunsSince sums the ledger from a moment until now, PER ITEM: how many times each thing fired, and what it spent doing so. It is what a card means by `3 runs this week · $0.04`.

ONE WALK ANSWERS EVERY ITEM, and that is the whole reason it answers a map rather than one item's figure. The ledger is one file per local day, so a surface asking item by item would open the same seven files once per item on every card it draws; here they are read once and the caller sums whichever ids its subject owns.

A day with no file is a day on which nothing fired, which is not a failure — the same reading Store.Today takes of the same absence. A day whose file cannot be read at all IS reported, because a total silently missing a day is a rail quoting a number that is too small.

The moment is inclusive and entries before it are skipped: the day file it lands in holds the hours on either side of it.

func (*Store) Save

func (s *Store) Save(item Item) error

Save rewrites one item's document, temp+rename under its flock, and stamps Updated. It validates first.

IT BUMPS Item.Revision FROM THE DOCUMENT ON DISK, read inside the same lock as the write. That is what makes a person's edit, pause or resume visible to a pass that is holding a copy from before it ([Store.saveActive] refuses to write over a revision it did not read).

A WHOLE-DOCUMENT WRITE TAKES THE RUNTIME STATE FROM DISK, EXACTLY. This door is the person's — a pause, a stop, an edit, a resume — and the row a surface hands back was drawn before the ticker committed or settled the delivery intents (Item.Pending) and the task's in-flight marker (Item.TaskInflight). The document on disk is read INSIDE this lock and BOTH fields are copied from it verbatim: an intent or marker the ticker committed after the row was read is CARRIED rather than dropped, and one the ticker has since settled is NOT RESURRECTED from the stale row. A union of the two was wrong in the second direction: an intent the ticker had already settled came back from the person's copy and blocked the item until they answered a line they had already seen.

NEITHER FIELD IS THIS DOOR'S TO AUTHOR. No production caller of Store.Save sets them — every one is a surface act on an existing row (a pause, a stop, an edit, an exception, an origin move) — so the disk copy is the only truth there is. The ticker commits and settles both through its own guarded write ([Store.saveActive]), which is the one internal runtime door.

func (*Store) SetStandingEffort

func (s *Store) SetStandingEffort(id string, rung effort.Rung) error

SetStandingEffort sets how hard one item's firings and its checks think, and is the door a surface calls to move that rung.

It is a read-modify-write under the item's own flock rather than a field on a whole item somebody hands back, for the reason Store.Save takes a whole document and this does not: a surface holding an item it read a minute ago would write back the check results, the spend and the next-due that the ticker has moved since, and quietly undo a firing.

The rung is validated against the ladder here, so a word nothing can parse is refused at the door instead of landing on disk and reading back as absence forever. Absence itself IS settable — "off" and "" both clear the field — and clearing it puts the item back on the standing role's own floor.

func (*Store) Today

func (s *Store) Today(itemID string, now time.Time) (Spend, error)

Today sums today's ledger. An empty itemID sums everything, which is what the daily rail reads.

func (*Store) WakeLogPath

func (s *Store) WakeLogPath() string

WakeLogPath is where every pass writes one line, and where "last wake" is read from.

type TaskInflight

type TaskInflight struct {
	// RunDir is the run folder the attempt was given, so the person can open it.
	RunDir string `json:"runDir,omitempty"`
	// Started is when the attempt began.
	Started time.Time `json:"started"`
	// Attempts counts attempts that reached this marker, so a task that keeps
	// dying can be surfaced rather than retried forever.
	Attempts int `json:"attempts,omitempty"`
}

TaskInflight is the durable marker of a task attempt with no known outcome, written before Runner.Run and cleared only when the attempt reaches one. It exists so a crash or an error leaves a fact on disk that a later pass can refuse to replay: a task edits the world, and nothing here can make a second run idempotent, so the second run must not happen at all.

type Ticker

type Ticker struct {
	Store    *Store
	Sentinel Sentinel
	Runner   Runner
	Idle     Idle
	// Tidy is the consolidation pass over what is remembered, run once at the
	// end of a pass and only when the session lane supplied one.
	Tidy Tidy
	// SentinelVerdict, when set, is the three-valued judgment and replaces the
	// legacy Sentinel. Nil means the legacy [Sentinel] is used, if it is set.
	SentinelVerdict SentinelVerdict
	// DailyRailUSD is the ceiling on everything standing spends in one day,
	// from settings. Zero is no rail, which the card says out loud.
	DailyRailUSD float64
	// Now is the clock, injectable for tests.
	Now func() time.Time
}

Ticker runs passes. One is built per process that may tick — a window, or `codeaf tick` — and Ticker.Tick is what both call.

func (*Ticker) Tick

func (t *Ticker) Tick(ctx context.Context) (Pass, error)

Tick runs one pass: take the lock (or decline), walk every active item, wake the due ones, judge, fire within the rails, write the wake log, release. It never blocks on a person and never runs past ctx.

type Tidied

type Tidied struct {
	Merged     int
	Superseded int
	USD        float64
}

Tidied is what one consolidation pass over what is remembered came to (internal/session's memory_consolidate.go): how many lines were merged into one clearer line, how many were retired in favour of one that replaced them, and what the single call cost.

It is declared HERE rather than in the session lane because the two things a pass owes the person about it — the money on the day's rail and the line in the wake log — are both this package's business.

func (Tidied) Changed

func (t Tidied) Changed() int

Changed is how many remembered lines the pass actually moved. Zero is a pass that read fifty lines and decided every one of them was already right, which is the ordinary answer.

func (Tidied) Line

func (t Tidied) Line() string

Line is the pass's own account of a tidy, THE EMPTINESS LAW APPLIED: a part that is zero is absent rather than printed as a zero, and a pass that changed nothing and spent nothing is no line at all.

type Tidy

type Tidy func(ctx context.Context) (Tidied, error)

Tidy is the one piece of work in a pass that nobody armed: the call that reads what is remembered and answers with the duplicates merged and the replaced lines retired. The session lane supplies it, and a NIL Tidy is the whole of "memory is off on this machine" — a capability that cannot work is absent rather than present and refusing.

type Timer

type Timer struct {
	// contains filtered or unexported fields
}

Timer is the Watch this package installs. It holds no state of its own: everything it answers is read from the filesystem at the moment it is asked.

func NewWatch

func NewWatch(options WatchOptions) (*Timer, error)

NewWatch resolves the defaults and validates the immutable inputs once.

func (*Timer) Drift

func (w *Timer) Drift() (WatchDrift, error)

Drift reads that. A machine with no definition answers a zero value and no error: nothing to repair is not a fault.

func (*Timer) Ensure

func (w *Timer) Ensure(ctx context.Context) error

Ensure provides background checks without taking another owner's timer. It is the implicit first-approval path; Install is an explicit takeover.

func (*Timer) Install

func (w *Timer) Install(ctx context.Context) error

Install is the explicit settings action: write this profile/program pair, including taking over an existing timer. Implicit callers must use Ensure or Repair, which apply ownership under the same interprocess lock.

func (*Timer) Repair

func (w *Timer) Repair(ctx context.Context) (WatchDrift, error)

Repair only restores an existing, repairable timer belonging to this profile. The returned drift names the repair attempted (successful when err is nil), never an earlier reading that could race an explicit off or another profile's install.

func (*Timer) Status

func (w *Timer) Status() (WatchStatus, error)

Status derives everything: installation from the definition's own bytes, the last wake from the wake log's last line, the next check from the cadence.

func (*Timer) Uninstall

func (w *Timer) Uninstall(ctx context.Context) error

Uninstall is the explicit settings action that stops and removes the timer.

type Watch

type Watch interface {
	Ensure(ctx context.Context) error
	Install(ctx context.Context) error
	Uninstall(ctx context.Context) error
	Status() (WatchStatus, error)
}

Watch is the OS timer: a launchd agent or a systemd user timer running `codeaf tick` every Interval. The core lane builds it on internal/watchdog's shape with its own unit names, so it can coexist with v1's.

type WatchDrift

type WatchDrift struct {
	// Present is a definition file on disk, whatever it says.
	Present bool
	// Executable is the program that definition names, or empty when the file
	// could not be parsed for one. It is read for the log line the repair
	// writes; nothing decides on it.
	Executable string
	// Gone is Executable naming a path with no program on it — the build was
	// deleted, or moved and the old path left empty.
	Gone bool
	// Stale is a definition for THIS timer's home that this launch may repair:
	// the program it names is gone, or the bytes are not what this build writes
	// for that pair (an older build wrote them, or somebody edited them).
	//
	// IT IS NEVER TRUE FOR A TIMER THAT IS SOMEBODY ELSE'S: one naming another
	// home, or one naming another program that can still run. Repairing either
	// would be this launch taking the machine's one timer away from a pair that
	// was serving it, which is the flip this law exists to end. Turning the
	// settings row off and on is the one hand that moves it on purpose.
	Stale bool
}

WatchDrift is what Timer.Drift answers: a definition already on this machine, and whether the launch that asked may put it back.

IT IS A SEPARATE READING FROM WatchStatus ON PURPOSE. Status answers the only question a person asks — is anything checking — and a definition pointing at a binary that has been deleted is not checking, so Status says no. This says WHY it said no, which is a different question with exactly one caller: the launch that repairs the drift (Timer.Install rewrites it).

type WatchOptions

type WatchOptions struct {
	Platform   string
	HomeDir    string
	Executable string
	// StateRoot is the home the timer ticks — CODEAF_HOME when set, the
	// login's default otherwise — and the half of the pair a test has to name
	// for a home it is not running under. Empty is this process's own.
	StateRoot string
	UID       int
	Runner    WatchRunner
	// WakeLog is where passes leave their one line each — [Store.WakeLogPath].
	// It is a path rather than a store because the timer has no business
	// reading items, only proof that something woke.
	WakeLog string
	Now     func() time.Time
}

WatchOptions makes every host-specific input injectable, so a test can install a timer for a machine it is not running on. Empty values are filled from the current process by NewWatch.

type WatchRunner

type WatchRunner interface {
	Run(ctx context.Context, name string, args ...string) error
}

WatchRunner is the whole process boundary the timer needs. Tests hand it a recorder; only execRunner reaches os/exec.

type WatchStatus

type WatchStatus struct {
	// Installed means the OS timer's definition on disk matches byte for byte
	// what this build writes.
	Installed bool
	LastWake  time.Time
	NextDue   time.Time
}

WatchStatus is what /status prints, derived and never asserted.

type When

type When struct {
	Kind  WhenKind `json:"kind"`
	Words string   `json:"words,omitempty"`
	// At is the one moment of a WhenAt.
	At time.Time `json:"at,omitempty"`
	// Every is a WhenEvery's rhythm: a five-field cron line ("0 9 * * 1") or
	// a Go duration ("20m", "2h"). [ParseEvery] is the one reader of it.
	Every string `json:"every,omitempty"`
	// Glob is a WhenFile's pattern, relative to the item's workspace.
	Glob string `json:"glob,omitempty"`
	// IdleFor is how quiet the machine must have been for a WhenIdle.
	IdleFor time.Duration `json:"idleFor,omitempty"`
	// Probe is a WhenProbe's look at the world, taken every ProbeEvery.
	Probe      Probe         `json:"probe,omitempty"`
	ProbeEvery time.Duration `json:"probeEvery,omitempty"`
	// Hint is the MODEL'S OWN reading, written at proposal time, of what a yes
	// to this condition would look like in the probe's output: "yes when any
	// run on main shows conclusion=failure". It is a note the sentinel is
	// shown, never the person's criterion: [Item.Words] is what is judged, so
	// a match to the hint does not settle a condition the person phrased more
	// narrowly or more broadly than the guess.
	Hint string `json:"hint,omitempty"`
	// OneShot is a WhenProbe's one-shot intent: the condition watch fires when the
	// condition FIRST turns true, delivers its one line, and retires. It is the
	// compiled difference between "notify me once when ready becomes true" (set)
	// and "tell me every time it is ready" (unset, the rising-edge default).
	//
	// IT IS A COMPILED FIELD, NOT A WORD MATCH. The model reads the sentence
	// once, at proposal time, and this is what it wrote; nothing here scans the
	// person's English on every pass. It rides the approval card and the stored
	// document like every other field, and it is fenced at [Schema] so an older
	// reader skips it rather than silently re-arming a one-shot forever.
	OneShot bool `json:"once,omitempty"`
}

When is what wakes an item. Exactly the fields its Kind names are read; the rest are left empty and never consulted. Words are always kept: they are the person's own cadence or condition, and every surface speaks them back rather than the spec.

func (When) ShortWords

func (w When) ShortWords() string

ShortWords is the cadence a button can carry. A long sentence is cut at a word, because a label that fills the row leaves no room for the other answers.

type WhenKind

type WhenKind string

WhenKind is one of the six shapes of an item — five ways to be woken, and one that never wakes (WhenHold). The list is closed.

const (
	// WhenAt fires once, at a moment, then retires. A reminder is this.
	WhenAt WhenKind = "at"
	// WhenEvery fires on a rhythm — a cron line or an interval — forever.
	WhenEvery WhenKind = "every"
	// WhenFile fires when files matching a glob change (a bounded fingerprint of
	// names, sizes and CONTENTS — not mtimes, so a bare touch is not a change and
	// an equal-size edit is).
	WhenFile WhenKind = "file"
	// WhenIdle fires when the machine has been quiet — no window busy, no
	// task running anywhere — for [When.IdleFor]. "Learn this later" is this.
	WhenIdle WhenKind = "idle"
	// WhenProbe fires when a probe's output, judged by the sentinel against
	// the person's words, says yes. Anything the belt can do is a probe.
	WhenProbe WhenKind = "probe"
	// WhenHold never wakes. A rule, "always use tabs here", "never touch the
	// public API", has no moment, no rhythm and no probe: its whole work is
	// done at birth, riding into the world of every conversation and task it
	// reaches (docs/STANDING-ORDERS.md, the birth seam). The pass walks past
	// it; it cannot fire, so it cannot spend, so it alone needs no rails and
	// no action.
	WhenHold WhenKind = "hold"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL