A local-first memory and autopilot enhancer for AI agents.
AI agents forget everything between sessions, lose the thread of work they started, and keep no honest record of what they actually did. MindOS fixes that with one small Python toolkit backed by a single local SQLite database:
- Durable semantic memory — retain facts and decisions locally, recall them in any later session. FTS5-backed search, fully offline, inside the audit chain — no external memory service.
- Autopilot execution truth — tasks with leases, dependencies, approvals, receipts, and a tamper-evident audit history, so completed work carries evidence instead of claims.
- Temporal facts — what is true now versus what was true before: ownership changes, priority shifts, superseded decisions, expiring blockers.
- Session-start context packs — one bounded, digest-sealed pack injected on the first turn of a new session, so an agent resumes instead of restarting.
- Cross-agent handoffs — provider-neutral envelopes with acknowledgment, SLAs, and fleet-wide inbox views for multi-agent work.
- Recovery and portability — sealed inventories, dry-run-first migration, rollback, snapshots, integrity checks, and health doctoring that never mutates what it inspects.
- Bounded autonomy — model-bound, time-limited continuation grants with a
cron-driven nanny tick that repairs, escalates, and reports four readable
states (
all_clear,working,hit_snag,decision_needed).
Everything runs locally on the Python standard library — no third-party packages, no server to deploy. Source-available under FSL-1.1-MIT and MIT after the two-year change date.
git clone https://github.com/idea-torx/mindOS.git && cd mindOS
export MINDOS_HOME="$HOME/.hermes/mindos" # any writable path works
python3 autopilot.py init # create the home + schema
python3 ops.py doctor # health-check it
python3 autopilot.py create --project demo --title "First task"
python3 autopilot.py memory-retain --text "We chose SQLite for execution truth"Point your agent harness at the same home and every new session starts with its memory intact. INSTALL.md covers migration from an existing Autopilot installation.
MindOS is a combinatorial system: eight orthogonal durable dimensions (execution truth × semantic memory × temporal facts × session/context × agents/handoffs × evidence/receipts × recovery/rollback × policy/provenance) whose combinations produce capabilities — safe ownership, evidence-backed completion, current-vs-historical truth, resumable multi-agent work, portable brain with reversion, enforceable definition of done. Git + Markdown can store the data; MindOS makes concurrency, atomic transitions, retrieval, policy gates, receipts, temporal state, and recovery operational and queryable.
Visual explanations (all render on GitHub):
- ARCHITECTURE.md — system layers & data flow, task/lease/receipt lifecycle, memory/context retrieval path, migration & rollback path, capability matrix, plus a fact table mapping every claim to source + test.
- docs/COMBINATORIAL-ARCHITECTURE.md — the dimension → combination → capability derivation and why "just use Git" misses the point.
Core invariants: single SQLite authority, fail closed, evidence over claims, dry-run first, source immutability.
Project: MindOS
Release: v0.5.0
Role: memory + autopilot enhancer for local AI agents
Primary surface: Hermes Agent (harness-agnostic by design)
Execution core: SQLite-backed autopilot
Semantic brain: local in-database memories store (memory-fts-v1)
Fallback: preserved prior Autopilot installation
The schema names that end in -v1 elsewhere in this document are artifact-format versions, not the project generation.
MindOS coordinates durable work and evidence. It does not silently deploy, merge, send external messages, submit applications, or cross a human approval seam. Autonomous continuation is explicit, model-bound, time-limited, and receipt-backed.
The autopilot layer adds the missing impulse layer for complex work. A bounded nanny tick can recover stale leases, detect findings, run capped approved repairs, escalate decisions, and emit an audited state report. It is cron-driven rather than a resident daemon, so every tick is bounded and independently observable.
Tasks may declare an autonomy level and exact model/provider binding:
python3 "$A" declare <task-id> \
--model opencode/x-preview-f-free \
--autonomy-level L1 \
--granted-by leo \
--grant-hours 24
python3 "$O" nanny --dry-run
python3 "$O" nanny --max-repairs 2The tick is assembled from three inspectable commands, each usable on its own:
python3 "$O" sense # typed, content-hashed findings across doctor,
# verify-chain, recall-stale, and activity sweeps
python3 "$O" repair-list # the tier-0 repair policies eligible to run
python3 "$O" fts-rebuild --dry-run # rebuild a drifted full-text indexsense is read-only: it reports findings and never mutates state.
repair-list shows which bounded repairs the nanny is permitted to apply, and
fts-rebuild is one of them — dry-run first, like every other write path here.
The nanny reports four compact impulse states:
all_clear nothing needs attention
working work is active or progressing
hit_snag bounded work remains blocked or needs correction
decision_needed a human approval or breaker decision is required
Persistent findings are carried forward by digest instead of being narrated as repeated status spam. A task's model, autonomy grant, recap, receipts, and audit events remain tied together for later continuation.
The managed botmail layer adds a provider-neutral envelope for Hermes, DSH, OpenCode, Codex, Claude Code, and future harnesses. It validates peer allowlists, capability epochs, profile scope, content class, replay budgets, and provenance before accepting bot-originated messages. Delivery is idempotent and produces accepted, rejected, duplicate, expired, or failed receipts. Bot chat remains distinct from user relay, handoffs, and task receipts; bounded bot-chat context is profile-scoped and redacted.
The live layer is installed with explicit follow-up gates for Hermes peer-DM delivery, gateway roster reconciliation, semantic bot-chat synchronization, and the cross-profile end-to-end sentinel.
When enabled in the Hermes configuration, MindOS supplies one bounded context pack on the first turn of a new session. It can include relevant session history, temporal facts, handoffs, receipts, and semantic memory with provenance. Continuation turns receive no repeated injection.
mindos_bridge:
context_pack: true
context_pack_max_bytes: 4096
context_pack_seconds: 15
hooks:
pre_llm_call:
- command: $HOME/.hermes/mindos/mindos_gateway_hook.py
timeout: 15The pack is ephemeral and bounded. It does not rewrite the system prompt,
create synthetic user messages, cross profiles, or bypass redaction. Set
HERMES_MINDOS_CONTEXT=off for an end-to-end opt-out. Use
python3 mindos_context_pack.py sentinel --json to exercise the disposable
proof path without touching live homes.
This release is a registry, memory, and continuation layer. It does not deploy, merge, send external messages, submit applications, or run arbitrary agent commands.
A=~/.hermes/autopilot/autopilot.py
python3 "$A" init
python3 "$A" create --project Trove --title "Example task" --priority P1 --next-action "Inspect evidence"
python3 "$A" list
python3 "$A" claim <task-id> --owner hermes --minutes 30
python3 "$A" heartbeat <task-id> --owner hermes --note "Running verification"
python3 "$A" receipt <task-id> --kind verification --payload '{"tests":"pass","health":200}'
python3 "$A" update <task-id> --status waiting_for_user --next-action "Leo review"
python3 "$A" complete <task-id> --owner hermes --note "tests pass" # requires live lease
python3 "$A" fail <task-id> --owner codex --reason "tests red" # failure + retry budget/backoff
python3 "$A" release <task-id> --owner hermes # live-lease holder only
python3 "$A" renew <task-id> --owner hermes --minutes 45 # extend a live lease
python3 "$A" leases [--all] [--owner hermes] # fleet-wide lease view
python3 "$A" transfer <task-id> --from-owner hermes --to-agent codex # reassign a live lease
python3 "$A" resume <task-id> --agent codex # idempotent killed-session recovery
python3 "$A" cancel <task-id> --owner leo --reason "obsolete" # rejected on foreign leases
python3 "$A" defer <task-id> --owner hermes --until "2026-08-22T09:00:00Z" # park out of dispatch
python3 "$A" tag <task-id> --tag autopilot-safe # attach capability/scope tags (repeatable)
python3 "$A" untag <task-id> --tag client:trove # remove one tag
python3 "$A" next --claim --owner codex --tag autopilot-safe # tag-scoped dispatch
python3 "$A" plan [--project P] [--tag T] # parallel dispatch-wave schedule
python3 "$A" show <task-id> # task detail + receipts + audit trail + dependencies
python3 "$A" search "deploy" # substring search over task text fields
python3 "$A" search "audit" --status queued --project Trove --priority P1
python3 "$A" session-scan --root ~/library-session-store # redacted dry-run inventory
python3 "$O" brain-inventory --out brain.json # end-to-end redacted brain manifest (read-only)
python3 "$O" brain-inventory-check brain.json # verify a sealed brain manifest
python3 "$A" fact-assert --subject service-auth --predicate uses --object postgres-14 --source codex
python3 "$A" facts --subject service-auth # query currently-valid triples
python3 "$A" search-facts postgres --rank # BM25 retrieval over the fact graph
python3 "$A" metrics # JSON observability snapshot
python3 "$A" verify-chain # recompute the audit hash chain; report tampering
python3 "$A" events --action claimed --limit 20 # query the global audit stream
python3 "$A" events --entity-id <task-id> --since 2026-08-21T00:00:00Z --verify
python3 "$A" dashboardEvery lease acquisition bumps a monotonic lease_epoch on the task and
surfaces it in the claim / next --claim output. A holder can pass its epoch
back on mutations so a stale process is rejected even when the owner name still
matches (e.g. its lease expired, was recovered, and reacquired by the same
owner):
python3 "$A" claim <task-id> --owner hermes --minutes 30 # → lease_epoch: 1
python3 "$A" heartbeat <task-id> --owner hermes --epoch 1 # ok while epoch matches
python3 "$A" complete <task-id> --owner hermes --epoch 1 # fenced completion
python3 "$A" release <task-id> --owner hermes --epoch 1 # fenced releaseA mismatch fails with lease superseded (held epoch N, current M); reclaim ….
Passing no --epoch preserves the previous behavior, so existing callers keep
working; new agents should always fence with the epoch they were issued.
A lease holder can hand a task back to the queue without consuming retry budget (unlike stale-lease recovery):
python3 "$A" release <task-id> --owner hermes # live-lease holder onlyOnly the current holder of a live lease may release; foreign, expired, and
terminal states are rejected. The transition is audited as lease_released.
A holder can extend its lease without changing task status (unlike heartbeat,
which forces running and a fixed 15-minute window) — useful for long-running
work that outlives the original claim window:
python3 "$A" renew <task-id> --owner hermes --minutes 45 # extend from now
python3 "$A" renew <task-id> --owner hermes --epoch 1 # fenced renewalrenew keeps the task's current status and preserves the fencing epoch, so a
renewal never invalidates a holder's token; a superseded holder passing a stale
--epoch is still rejected. Only the holder of a live lease may renew;
foreign or expired leases are rejected. The transition is audited as
lease_renewed.
Operators can see every held lease at a glance:
python3 "$A" leases # live leases, soonest expiry first
python3 "$A" leases --all # include expired-but-still-held (recovery candidates)
python3 "$A" leases --owner hermes # filter by holderEach entry carries task_id, project, title, status, priority,
owner, lease_expires_at, lease_epoch, a live flag, and
seconds_remaining. Default output hides expired-held leases so it answers
"what is active right now"; --all answers "what will recover sweep".
Leases prevent two agents from taking the same task, but not two agents
editing the same checkout. A seam is the shared filesystem/VCS resource where
concurrent agents physically collide: an identical non-empty worktree path,
or the same branch name within the same project (the same branch across
different projects is a different repository, so it is not a conflict).
claim refuses a task whose seam is held by another live lease:
python3 "$A" claim wt-2 --owner codex
# seam conflict: wt-1 holds worktree '/srv/wt' (owner hermes); complete/release the holder first or pass --force
python3 "$A" claim wt-2 --owner codex --force # deliberate override (e.g. holder is you, read-only pass)The refusal is audited as claim_refused_seam (kind-level conflict list, no
lease left behind), and dispatch participates too: next --claim skips
seam-conflicted candidates and picks the best unconflicted task instead of
failing after picking — --explain reports them as seam_conflict skips with
the holding tasks. Set worktree/branch via update; empty values are
never seams.
blocked-by answers "what holds this task?"; impact answers the mirror
question: "what is waiting on this task?" It walks the dependency DAG downward
and reports every transitive dependent with its depth, live status, and a
settled flag (completed/cancelled work no longer cares), plus a summary that
answers "what happens if I block, defer, or cancel this?":
python3 "$A" impact <task-id>
# { "impacted": 5, "open": 4, "by_status": {"queued": 3, "running": 1, ...},
# "dependents": [{"id": ..., "depth": 1, "status": ..., "settled": 0}, ...] }Completion feeds back downstream: complete now reports newly_unblocked —
the queued direct dependents whose dependencies it just satisfied — in both
its output and the audited completed event, so an agent finishing a hub
knows exactly which work it freed (and the audit trail records it).
Dispatch can also schedule by graph shape. next --prefer-unblocking adds a
critical-path tie-break: within one effective priority tier and deadline
class, candidates are ordered by descending count of queued direct dependents
(surfaced as unblocks on every pick and under --explain with
unblock_scheduling: true). Finishing a hub frees more of the graph than
finishing a leaf — but priority, deadlines, and aging fairness are never
overridden; without the flag, ordering is byte-for-byte unchanged.
python3 "$A" next --claim --owner codex --prefer-unblockingblocked-by looks up from one task and impact looks down from one task;
critical-path answers the fleet-level question: what is the longest chain of
still-unfinished prerequisite work? Its length is the minimum number of
sequential dispatch waves needed to drain the open graph — no schedule can
finish faster — and its members are the bottleneck chain: slipping on any of
them slips everything behind it.
python3 "$A" critical-path # whole fleet
python3 "$A" critical-path --project Trove # one project's graph
# { "length": 3, "path": [{"id": ..., "title": ..., "status": ..., "priority": ...}, ...],
# "open_tasks": 12, "by_status": {"queued": 9, "running": 2, "blocked": 1} }Design properties:
- Open graph only: completed/cancelled tasks leave the graph entirely; a
missing prerequisite referenced by a live edge appears as a
missingnode, because it blocks dispatch exactly like a real task. - Deterministic: ties at every step break to the lexicographically smallest id, so identical state yields an identical path.
- Composable: pair it with
next --prefer-unblockingto actually work the chain — the path names what matters, unblocking tie-breaks help drain it. - Observable:
metricsreportscritical_path_lengthfleet-wide.
critical-path says how many serial waves the open graph needs; plan says
which tasks go in each wave. Wave 1 is every ready task (in-flight work
included, shown with its live status), each later wave is what the previous
waves unblock, and anything that can never start inside the requested scope is
reported under unschedulable with its blockers instead of being silently
dropped:
python3 "$A" plan # whole fleet
python3 "$A" plan --project Trove # one project's graph
python3 "$A" plan --tag autopilot-safe # one capability scope
# { "waves": [{"wave": 1, "tasks": [{"id": ..., "title": ..., "status": ..., "priority": ...}, ...]},
# {"wave": 2, "tasks": [...]}],
# "unschedulable": [{"id": ..., "blocked_by": [{"id": ..., "status": "missing"}]}],
# "waves_total": 2, "scheduled_tasks": 5, "open_tasks": 6 }Design properties:
- Deterministic: within a wave, tasks order by dispatch preference —
priority rank, then earliest deadline (undated last), then oldest-created,
then id — so identical state yields an identical schedule and re-running
plandiffs to nothing. - Honest about scope: a prerequisite outside the plan (a missing id, or a
live task in another project under
--project) can never be scheduled here, so it and everything downstream of it land inunschedulablewith blocker ids and statuses. - Read-only simulation: runtime guards that depend on live state (seam conflicts, recovery backoff, deferral windows) still apply at claim time and are deliberately not folded into the waves.
- Composable:
waves_totalequalscritical-path'slengthfor the same scope — useplanto size parallel capacity per wave and work the waves withnext --claim.
When work moves from one agent to another, ownership moves with it. transfer
atomically reassigns a live lease — only the current holder of a live lease
may transfer, the fencing epoch bumps so the old holder's token is invalidated
immediately, and status is preserved:
python3 "$A" transfer <task-id> --from-owner codex --to-owner claude-code --minutes 45
python3 "$A" transfer <task-id> --from-owner codex --to-owner claude-code --epoch 3 # fencedThe transition is audited as lease_transferred with both owners and both
epochs. Terminal tasks, foreign holders, expired leases, and same-owner
transfers are rejected.
resume is the one-call recovery half of the handoff protocol: a killed or
fresh session recreates itself from durable state in moments, instead of hand-
orchestrating claim + recall. It applies the same guards as claim (terminal,
blocked, and dep-blocked tasks are rejected with their reasons), then:
- live lease held by the caller → no mutation; returns the sealed recall
bundle with
action: already_held(callingresumetwice is safe); - expired or absent lease → claimed atomically for the caller, honoring
per-owner caps (
action: claimed); - live lease held by someone else → rejected; ask them to
transfer.
python3 "$A" resume <task-id> --agent codex [--budget 6000] [--related 5]The response embeds the full recall bundle (task header, deps, live handoff,
notes, lease state, latest receipts) sealed with the deterministic digest, and
every resume is audited as session_resumed carrying the agent, action, and
digest — so recovery is idempotent, observable, and provably context-fresh.
Tasks carry a structured memory of provenance-tagged notes. This is the retrieval substrate agents share across task boundaries:
python3 "$A" note <task-id> --kind fact --content "API rate limit is 60/min" --source hermes
python3 "$A" note <task-id> --kind constraint --content "MUST NOT deploy on Friday" --pinned
python3 "$A" notes <task-id> # live notes, oldest first
python3 "$A" notes <task-id> --all # include superseded history
python3 "$A" supersede-note <note-id> --content "rate limit raised to 120/min"
python3 "$A" context <task-id> --budget 4000 # prompt-ready pack within a char budget
python3 "$A" search-notes "rate limit" --project Trove --kind factDesign properties:
- Kinds:
fact,decision,observation,evidence,constraint. - Provenance: every note records its
source(agent/operator) and timestamp. - Deduplication: exact duplicate content on the same task returns the
existing note (
deduplicated: true) instead of growing the store; a duplicate add of pinned content promotes the existing note to pinned. - Near-duplicate guard: rephrased restatements (high token-Jaccard overlap,
default ≥ 0.8, tunable via
AUTOPILOT_NEAR_DUP_THRESHOLD) are still stored but flagged: the response carriessimilar_towith note ids and similarity scores, and the auditednote_addedevent recordssimilar_notes, so shared memory does not silently accumulate near-identical facts. - Pinning:
--pinnedmarks a note as critical. Pinned notes pack first incontext(and survive tight budgets that drop unpinned notes), and the pin survives supersession so temporal fact chains stay protected. - Temporal facts:
supersede-noteatomically retires an old note and links it to its replacement (superseded_by); superseded notes are hidden from default views but retained for audit. - TTL (note lifetimes):
--ttl-hours Ngives a fact a lifetime. Past itsexpires_at, an unpinned note retires: it is excluded from context packs, recall bundles, related-note candidates, andsearch-notes(counted in the pack asnotes_expired_excludedrather than dropped silently; search can surface retirees with--include-expired). Pinned notes are immortal by design — an expired pin still packs but carriesexpired: trueso the agent knows a fresh supersede is due; silently dropping a critical constraint is exactly the failure mode TTL must not introduce. Re-adding a retired note's exact content revives it (revived: true, audited asnote_ttl_refreshed) and restates its lifetime — omitting--ttl-hoursmakes it immortal again. A superseding note is fresh: it inherits no expiry unless--ttl-hoursis passed explicitly.metricsreportsnotes_expired_liveandops.py notes-expiredlists every expired live note fleet-wide with the action it needs (revivefor retirees,supersedefor expired pins). - Context budgets:
contextpacks a task summary header, unsatisfied dependencies, then live notes pinned-first (oldest→newest within each group) within a character budget. It reportsused_chars,truncated, pack counts, andnotes_pinned_packedso callers can assemble prompts deterministically. - Retrieval:
search-notesdoes keyword search over live note content with--kindplus task-level--project/--statusfilters via a join. - Lineage:
note-history <note-id>walks a temporal fact chain end to end (oldest predecessor → newest live successor), so agents can reconstruct how a fact evolved without manual--allarchaeology. - Handoff lineage:
handoff-history <handoff-id>is the same walk for the resume point: given any link in a task's supersession chain, it reconstructs the full sequence oldest → newest, showing how the handoff (owner, objective, evidence) evolved across agents.
metrics reports notes_total, notes_superseded, notes_pinned_live,
notes_expired_live, and notes_consolidated_total; ops.py doctor checks
for orphaned notes and dangling supersession links.
The note command's near-duplicate guard only flags rephrased restatements;
over weeks of agent traffic, shared memory still accumulates near-identical
facts that all consume context budget and all surface in retrieval.
ops.py consolidate finishes the job: live notes on each task are clustered by
token-Jaccard similarity (the same measure as the guard), and every
non-canonical member is superseded into its cluster's canonical note — the
pinned note when a cluster has one, else the newest:
python3 ops.py consolidate --dry-run # preview clusters without mutating
python3 ops.py consolidate # merge fleet-wide
python3 ops.py consolidate --task <task-id> # scope to one task
python3 ops.py consolidate --threshold 0.7 # looser matching (default: 0.8 / env)Design properties:
- Deterministic clustering: notes are processed oldest→newest; each joins the first cluster whose canonical note it matches at or above the threshold, else founds its own cluster. Same rows always yield the same plan.
- Pin-aware canonical choice: a pinned note beats an unpinned one for survival regardless of age, so critical constraints never dissolve into a newer paraphrase; among equals the newest wins.
- History-preserving: losers are superseded (never deleted) — they point at
their canonical note via
superseded_by, sonote-historystill reconstructs how a fact was restated, and audit retains every step. - Audited + observable: each merge records a
note_consolidatedevent carrying the kept note id and similarity;metricsreportsnotes_consolidated_total. - Idempotent: consolidated notes leave the live set, so repeated passes
find nothing new; concurrent supersedes lose safely against the
WHERE superseded_by=''guard. - Retired notes excluded: expired unpinned notes are already invisible to packs and retrieval, so consolidation never resurrects them.
The same dedup discipline that keeps shared memory clean applies to the work
queue itself: two open tasks describing the same work split agent effort across
two seams, both surface in dispatch, and neither inherits the other's context.
Task creation now flags this at the source — when a new task's
title+description text overlaps an open (non-terminal) same-project task at or
above the near-duplicate threshold, the response carries similar_open_tasks
and the audited created event records similar_open_tasks, so provenance
shows the collision was visible from birth:
python3 autopilot.py similar <task-id> # triage: what restates this task?
python3 autopilot.py similar <task-id> --threshold 0.7 # looser matching
python3 ops.py dup-tasks # fleet-wide cluster sweep
python3 ops.py dup-tasks --threshold 0.7 --dry-run # (read-only either way)Design properties:
- Open tasks only: settled work is history, not a collision — completed, failed, and cancelled tasks never count as duplicates.
- Same-project only: by the seam rule, the same title under a different project is a different checkout and never conflicts.
- Informational, never blocking: creation is never refused; agents and
operators decide whether to cancel a duplicate or fold it into the canonical
task via
dep. - Read-only fleet sweep: unlike notes, tasks cannot be auto-superseded —
merging them is a lifecycle decision — so
dup-tasksclusters with the same greedy token-Jaccard algorithm asconsolidate(canonical = oldest) and reports each cluster with a suggested action instead of mutating. - Deterministic: similarity descending, then id; same rows always yield the same clusters.
The contract is explicit: credentials, private tokens, and raw personal data
never enter shared memory. The write path now enforces it instead of trusting
agent discipline. note, supersede-note, and handoff scan their content
(objective + list fields for handoffs) with shape detectors — AWS access keys,
GitHub / OpenAI-style / Slack / Google tokens, private-key blocks, bearer
headers, and generic password:/api_key= assignments — and refuse to store
credential-shaped content:
python3 autopilot.py note t1 --content 'key is AKIA...' # blocked (audited secret_blocked)
python3 autopilot.py note t1 --content 'key is AKIA...' --redact # stored as [REDACTED:aws_access_key]
python3 autopilot.py handoff t1 ... --allow-secret # verbatim override, auditedDesign properties:
- Kind-only reporting: findings are named by pattern kind, never by value — errors, output, and audit payloads never echo the secret itself.
- Low false positives: the generic assignment pattern requires a digit in
the value, so prose like
fencing token: lease_epoch chainpasses while real secrets (which almost always mix digits in) still trip it. - Three audited outcomes:
secret_blocked(default),secret_redacted(--redactstores a[REDACTED:<kind>]copy),secret_allowed(--allow-secretoverride) — every escape hatch leaves a trail. - Fleet sweep:
ops.py secret-scanfinds credential-shaped content already sitting in live notes/handoffs (legacy rows or overrides) with the same detector; read-only, remediation is a history-preserving supersede.--allincludes superseded rows. - Observable:
metricsreportssecrets_blocked_total,secrets_redacted_total, andsecrets_allowed_total.
Any agent — Hermes, Claude Code, Codex, OpenCode — can publish a durable,
structured handoff on a task. The latest live handoff is the authoritative
resume point: a killed or fresh session reconstructs its working context from
handoff-current (or the context pack) in moments, with no vendor-specific
format:
python3 "$A" handoff <task-id> --from-agent codex --to-agent claude-code \
--status running --objective "implement retry path" \
--evidence "tests pass locally" --constraint "no new dependencies" \
--decision "use exponential backoff" --file src/retry.py \
--commit abc1234 --next-action "open PR" --risk "flaky integration test"
python3 "$A" handoff-current <task-id> # recovery point for a new session
python3 "$A" ack <task-id> --agent claude-code # accept an inbound handoff
python3 "$A" handoff-inbox --agent claude-code [--unacked-only] # fleet-wide inbound view
python3 "$A" handoffs <task-id> # live handoff
python3 "$A" handoffs <task-id> --all # full temporal chainDesign properties:
- Complete carrier: every handoff records source agent, target agent, work status, objective, verified evidence, constraints, decisions, files/commit, next actions, risks, and a timestamp — the fields an incoming agent needs before acting.
- Temporal: recording a new handoff atomically supersedes the previous
live one (
superseded_bylink); superseded handoffs stay queryable via--all, so context evolution is auditable. A self-report is still not execution truth — pair handoffs with receipts for verified evidence. - Recall provenance: pass
--recall-digest <sha256>(the digest from a priorrecall) to attach proof of the exact context the handoff was written against;complete --recall-digestdoes the same for completions. The digest is stored on the record and in its audit event, andmetricsreportshandoffs_with_recall_proof. Digests are validated as 64-char hex. - Deduplicated: an identical live payload returns the existing handoff
(
deduplicated: true) instead of growing the store; every write is provenance-tagged in the hash-chained audit ledger (handoff_recorded,handoff_deduplicated). - Context-budget aware:
contextpacks the live handoff immediately after the task header (before notes), so the resume point survives tight budgets; output reportshandoff/handoff_packed.showsurfaces it too. - Privacy boundary: handoff payloads are plain operator/agent text — never copy credentials, tokens, raw personal data, or unrelated client context into them.
- Consistency-checked:
ops.py doctordetects orphaned handoffs, dangling supersession links, and invariant violations (more than one live handoff per task); snapshots and archives carry thehandoffstable.
Per-task commands answer "what is the state of this task"; handoff-inbox
answers the question an incoming agent actually starts from — "what work was
handed to me across the whole fleet?":
python3 "$A" handoff-inbox --agent claude-code
python3 "$A" handoff-inbox --agent claude-code --project Trove --limit 10Only live (non-superseded) handoffs whose to_agent matches are listed, so
when a handoff is superseded by one addressed to someone else, the task leaves
the previous recipient's inbox automatically. Each item joins the task's live
state — title, status, priority, lease owner/liveness — plus the handoff's
from_agent, objective, commit, recall digest, and timestamp, so an agent can
triage its inbound work without a follow-up show per task. The natural loop
is: handoff-inbox → resume <task-id> --agent me → act → publish a receipt.
The inbox surfaces inbound work; ack records that the recipient has
accepted it, closing the loop between "handed to" and "picked up by":
python3 "$A" ack <task-id> --agent claude-code
python3 "$A" ack <task-id> --agent claude-code --recall-digest <sha256> # tie acceptance to recalled contextDesign properties:
- Addressed-only: only the agent the live handoff is addressed to may ack it; a foreign agent is rejected.
- Idempotent: re-acking returns the existing acknowledgment
(
already_acked: true) instead of duplicating state. - Reset by supersession: recording a new handoff clears acceptance — a reassigned or updated handoff must be picked up again by its new recipient.
- Provenance: an optional
--recall-digestties the acceptance to proof of the context pack the recipient recalled before accepting. - Audited: every first ack records a
handoff_acknowledgedevent in the hash chain;handoff-current,show, andhandoffs --allsurfaceacked_by/acked_at. - Triage-aware: inbox items carry
acked/acked_at, andhandoff-inbox --unacked-onlyrestricts the view to work not yet picked up. - Observable:
metricsreportshandoffs_acked_total, andops.py handoff-checkflags an addressed live handoff older than the ack SLA (--ack-sla-hours, default 24) that was never acknowledged asstale_unacknowledged.
The handoff protocol makes promises — an objective, a recipient, evidence or
next actions, and recall provenance when --recall-digest is cited.
ops.py handoff-check is the read-only enforcement sweep that turns violations
into observable problems instead of silent drift:
python3 ops.py handoff-check # lint every live handoff fleet-wide
python3 ops.py handoff-check --task <task-id> # scope to one taskReported reasons:
unaddressed— noto_agent, so no inbox will ever surface itmissing_objective— the resume point has no stated goalsparse_no_evidence_or_next_actions— a self-report without proofunproven_recall_digest— the cited digest never appears in the audited recall stream (context_recalledorsession_resumed; a resume digest is first-class provenance) — fabricated or mistyped citationolder_than_latest_recall— genuinely recalled, but a newer audited recall for the task exists since, so the handoff may rest on stale contextterminal_task_handoff— live handoff on a completed/failed/cancelled taskstale_unacknowledged— addressed live handoff older than the ack SLA (--ack-sla-hours, default 24) that its recipient never acknowledged
Provenance is checked against the audit ledger, not recomputed digests, so
routine lease renewals and the handoff's own recording never false-positive;
fine-grained freshness against current state remains recall-verify's job.
context --related N turns the pack into cross-task RAG: up to N live notes
from other tasks whose content matches this task's title/description/
next_action are appended after the task's own notes, within the same character
budget. Each related note carries its source task (task_id,
via_task_title) and an FTS relevance score, so agents see prior knowledge
from sibling work without manual searching:
python3 "$A" context <task-id> --budget 4000 --related 5
python3 "$A" context <task-id> --budget 4000 --related 5 --related-scope global
python3 "$A" recall <task-id> --agent opencode # session bootstrap: pack + lease + receipts + digestDesign properties:
- Recall-oriented ranking: candidate tokens are OR-combined through the FTS5 index and ordered by BM25, so the strongest matches pack first even when only one token overlaps.
- Provenance: every related note is tagged with the task it came from; the task's own notes are never duplicated into the related section.
- Budget-honest: related notes consume the same budget as own notes and
report
related_requested/related_matched/related_packed;truncatedflips when anything was dropped. - Scope: default
--related-scope projectrestricts candidates to the task's project;globalsearches all projects. - Graceful fallback: on non-FTS builds it degrades to any-token substring
matching (same shape, minus
score); tokenless task text yields zero related matches instead of an error.
--related-handoffs N extends the same idea to the handoff protocol itself:
up to N live handoffs on other tasks whose objective/status/agents match the
task's text are packed after related notes, within the same budget. Where
related notes carry prior knowledge, related handoffs carry neighboring
resume points — the decisions and commit refs of sibling work an agent would
otherwise rediscover:
python3 "$A" context <task-id> --budget 6000 --related 5 --related-handoffs 3Each entry carries task_id, via_task_title, from_agent/to_agent,
status, objective, and commit_ref; the pack reports
related_handoffs_requested / _matched / _packed. The flag is opt-in and
digest-gated: packs built without it stay byte-identical to the legacy shape,
and a recall made with it only verifies when recomputed with the same value.
Candidates are matched through the FTS index but ordered deterministically
(created_at DESC, rowid) with no relevance score emitted — BM25 scores drift
whenever any handoff joins the index, which would falsely stale every sealed
digest. Superseded handoffs are never candidates; the task's own handoffs are
excluded (its own live handoff already appears in the bundle).
search-handoffs is fleet-wide keyword retrieval over the protocol: "what
decided/did work like this before?" Live handoffs are searched by default;
--all includes superseded ones (tagged with superseded_by). Filters:
--task, --from-agent, --to-agent, --project. With --rank (and an
FTS5-capable SQLite) results are BM25-ranked over
objective/status/from_agent/to_agent via a dedicated handoffs_fts index and
carry a score; otherwise substring LIKE matching with identical output shape
minus the score. Every row joins its task's project/title so hits are
triageable without a follow-up show:
python3 "$A" search-handoffs "postgres pool" --rank --project Trove
python3 "$A" search-handoffs "migration" --from-agent codex --allrecall is the one-call pre-action ritual the handoff protocol requires: every
agent recalls the relevant context pack before acting. It bundles everything
in context --related (task header, unsatisfied deps, live handoff,
pinned-first notes, cross-task related notes) plus:
- Lease awareness: current lease owner/expiry/fencing epoch, whether it is
live, and
held_by_callerwhen--agentmatches — so an agent knows if it must claim before editing. - Latest receipts: the 3 most recent receipts with parsed payloads.
- Sealed digest: a deterministic SHA-256 over the durable context (the
recall timestamp is excluded), so identical state yields an identical,
referenceable digest. Any state change moves it. A second
core_digestexcludes the live-handoff section: an agent recalls first and records its handoff afterwards, so its own handoff must not count as drift against its own citation — note/receipt/lease/dep drift still moves both digests.
Each recall is audited as a context_recalled event carrying the agent, the
digest, the core digest, and the bundle parameters (budget, related count,
scope) — so downstream sweeps can recompute the digest exactly as it was
recalled. resume audits its bundle the same way (session_resumed), making a
resume digest first-class recall provenance for handoffs and completions. A
self-report is never execution truth without a receipt; a digest ties the two
together.
recall-verify closes the loop: pass a previously recalled digest and it
recomputes the current bundle (same algorithm, no audit write) and reports
fresh: true when nothing durable has changed since that recall — notes,
handoffs, lease state, receipts, deps all match. A stale result carries the
new current_digest so the agent can re-recall before acting. Handoffs and
completions cite the digest they acted on via --recall-digest, making stale
context detectable after the fact.
python3 "$A" recall <task-id> --agent codex --budget 6000 --related 5
python3 "$A" recall-verify <task-id> --digest <sha256> --agent codex --budget 6000 --related 5
python3 "$A" recall-diff <task-id> --digest <sha256>
python3 "$A" events --action context_recalled --entity-id <task-id>
python3 ops.py recall-stale # fleet sweep: which live handoffs cite drifted context?recall-verify answers "is my context still fresh?" with a boolean;
recall-diff answers the follow-up an agent actually acts on — "what
changed?". Every recall, resume, and next --claim --recall now records a
compact per-section manifest of the bundle alongside its digest in the audited
event (never hashed into the digest itself, so digests stay byte-compatible
with pre-manifest recalls). Given a cited digest, recall-diff looks up that
event, recomputes the current bundle exactly as it was originally recalled
(recorded budget/related/scope/rerank parameters), and diffs section by
section:
task— status/priority/due_at/next_action/blocked_reason field movesdependencies— satisfied vs newly-added prerequisite idshandoff— the live resume point was recorded or superseded (from/to)notes— added/removed note ids plus pinned/expired flag flipsrelated_notes— cross-task retrieval candidates that appeared or leftlease— owner/epoch/expiry/liveness changes (from/to)receipts— evidence receipts posted or rotated out of the top 3
The result carries fresh, unchanged, changes, and sections_changed.
A digest with no audited provenance reports unproven_recall_digest; events
recorded before manifests existed degrade to the plain fresh/stale verdict
(legacy_event: true) instead of guessing. Exit code stays 0 either way.
recall after next --claim is two round trips for what is one decision. With
--recall, dispatch embeds the full sealed recall bundle in the claim response
and audits it as context_recalled — one call takes work AND proves which
context it was taken against:
python3 "$A" next --claim --owner codex --recall --budget 8000 --related 5The agent defaults to the claiming --owner; --budget, --related, and
--related-scope tune the bundle exactly like recall. The response carries
recall (the bundle) plus recall_digest, which is first-class provenance:
it passes handoff-check, can be cited by handoff --recall-digest /
complete --recall-digest, and is fresh per recall-verify. Without
--recall the output shape is unchanged; --recall without --claim is
rejected.
recall-verify answers freshness for one task and one digest the caller
already holds; ops.py recall-stale answers the operator question across the
whole fleet. For every live handoff citing a --recall-digest, it recomputes
the task's current recall bundle exactly as it was originally recalled (the
audited event stores the bundle parameters) and compares digests:
fresh— the cited recall's core context still matches current durable statestale— notes, receipts, lease state, or deps moved since; the item carries the recomputedcurrent_digestso the next agent can re-recall before actingunproven_recall_digest— no audited recall/resume ever produced the digestunknown_recall_params— proven by a legacy pre-parameter-capture event; freshness cannot be recomputed exactly
Read-only: reports problems, never mutates.
Search can run through SQLite FTS5 (stdlib — no external dependency) instead of
substring matching. Pass --rank to search-notes or search for BM25-ranked
results; each hit carries a score (more negative = more relevant):
python3 "$A" search-notes "postgres pool" --rank --kind fact --project Trove
python3 "$A" search "rate limit" --rank --status queuedDesign properties:
- Always in sync: the
notes_fts/tasks_ftsindexes are external-content tables maintained by triggers on every insert/update/delete, including snapshot restores — no separate reindex step. - Graceful fallback: on SQLite builds without FTS5 the indexes are skipped
entirely and
--rankdegrades silently to the substring path (same output shape, minusscore). Tokenless queries return[]rather than erroring. - Conjunctive semantics: multi-token queries match documents containing all tokens; superseded notes are excluded from ranked note search.
- Drift detection:
ops.py doctorcompares indexed vs source row counts and reportsfts_index_driftif they ever diverge.
Pure BM25 is blind to time: a perfectly-matched note from months ago outranks a fresh one, and stale facts are exactly what agents must not pack first. Retrieval commands accept an opt-in hybrid re-scoring pass — lexical match × recency decay + pinned bonus:
python3 "$A" search-notes "postgres pool" --rank --rerank --recency-half-life-hours 24
python3 "$A" context <task-id> --related 5 --rerank
python3 "$A" recall <task-id> --agent codex --related 5 --rerank --pinned-boost 0.5
python3 "$A" next --claim --recall --owner codex --rerankDesign properties:
- Hybrid score: the best BM25 match in the candidate set normalizes to
1.0 (LIKE-fallback rows count as 1.0 — that path is already newest-first),
multiplied by an exponential recency decay (
--recency-half-life-hours, default 168 = one week), plus a flat--pinned-boost(default 0.5) for pinned notes. Each row carries itsrank_score; results sort by it, ties newest-first. - Deterministic digests: note ages are floored to whole hours before the decay is applied, so a recall bundle's scores — and therefore its sealed digest — are stable within the hour instead of drifting on every recomputation. Identical state still yields an identical digest.
- Provenance-preserving: when a recall/resume uses
--rerank, the half-life and boost are recorded in the auditedcontext_recalled/session_resumedevent, soops.py recall-stalerecomputes cited digests exactly as originally recalled; events recorded before this feature recompute unchanged (rerank off). - Opt-in and shape-stable: without
--rerank, every command's output (and digest behavior) is byte-identical to the pre-rerank semantics.
By default an owner may hold unlimited live leases. Set a cap to stop one agent from hogging dispatch:
python3 "$A" claim <task-id> --owner hermes --minutes 30 --max-active 4
python3 "$A" next --claim --owner hermes --max-active 4
export AUTOPILOT_MAX_ACTIVE_PER_OWNER=4 # default for every claim/nextThe cap is enforced atomically inside the lease acquire statement, so
concurrent dispatchers cannot race past it. When an owner is at capacity the
claim fails with owner '<name>' at lease capacity (n/max); completing or
releasing a lease frees capacity immediately. metrics reports
active_leases_by_owner for observability.
Tasks can carry an optional UTC deadline. Dispatch honors it: within a
priority class, the earliest deadline is picked first and undated tasks sort
last. Overdue non-terminal tasks surface in metrics (overdue_tasks,
due_within_24h), can be listed with list --overdue, and are flagged
[OVERDUE …] on the dashboard:
python3 "$A" create --project Trove --title "Renew cert" --due-at "2026-09-01T17:00:00Z"
python3 "$A" update <task-id> --due-at "2026-09-02T09:00:00+02:00" # reschedule (normalized to UTC)
python3 "$A" update <task-id> --due-at "" # clear the deadline
python3 "$A" list --overdueTimestamps accept any ISO 8601 form (naive values are assumed UTC) and are
stored normalized; invalid timestamps are rejected. context includes
due_at in the task summary so agents see deadlines in their prompt pack.
A queued task can be parked out of dispatch until a future instant — useful
for "retry after the deploy window", rate-limited external calls, or scheduled
follow-ups. Unlike block, no reason or lifecycle change is involved; the
task stays queued and simply is not dispatched until its time arrives:
python3 "$A" defer <task-id> --owner hermes --until "2026-08-22T09:00:00Z"
python3 "$A" defer <task-id> --owner hermes --until "" # clear the deferral
python3 "$A" create --project Trove --title "Follow up" --not-before "2026-08-25T00:00:00Z"
python3 "$A" update <task-id> --not-before "2026-08-23T12:00:00+02:00"next skips deferred tasks (reason deferred_until with the not_before
timestamp under --explain) exactly like recovery backoff, while an explicit
claim remains allowed as a deliberate operator override. metrics reports
the count as queued_deferred. Every defer/clear is audited with the owner
and previous status.
Static priority ordering can starve old low-priority work when fresh P0/P1
tasks keep arriving. next therefore applies a virtual priority boost at
dispatch time: a queued task that has waited --aging-minutes (default 360)
per level since creation is promoted one effective level, up to
--aging-boost levels (default 2). A P3 that has waited 12+ hours dispatches
like a P1 without its stored priority ever being mutated:
python3 "$A" next --explain # defaults: 360 min/level, max boost 2
python3 "$A" next --aging-minutes 120 --aging-boost 3 # more aggressive fairness
python3 "$A" next --aging-minutes 0 # strict static ordering (old behavior)With --explain, a boosted pick reports effective_priority and
priority_boost. Within one effective tier the longest-waiting task wins
(oldest created_at first), so equal-priority work drains FIFO instead of
last-touched-first.
Tags are a lightweight vocabulary on tasks — autopilot-safe, client:trove,
infra — that turn dispatch policy into data instead of per-agent prompts.
An operator marks what each task is allowed for, and every agent constrains
itself with the same flag shape across Hermes, Claude Code, Codex, and
OpenCode:
python3 "$A" create --project Infra --title "rotate logs" --tag autopilot-safe
python3 "$A" tag t-123 --tag autopilot-safe --tag client:trove # idempotent, audited
python3 "$A" untag t-123 --tag client:trove # audited; absent tag fails
python3 "$A" next --claim --owner codex --tag autopilot-safe # scoped dispatch
python3 "$A" list --tag autopilot-safe # triage filter
python3 "$A" search "logs" --tag autopilot-safe # search filterTag-scoped dispatch is a hard filter: an agent constrained to
--tag autopilot-safe never receives untagged or differently-tagged work,
even when that work outranks it — an empty scope dispatches nothing rather
than leaking other work. Tags are validated to lowercase
[a-z0-9:_./-] (max 64 chars), which keeps them safe inside the JSON-array
LIKE filter and stable as CLI flags. Every task output exposes tags as a
JSON array, and tagging is audited (task_tagged/task_untagged) like all
state changes.
Task tags put dispatch policy on the tasks; project policies put it on the
projects. The same policies/<project>.yaml files that gate merge/deploy
readiness can now also gate dispatch itself, so a project's rules hold no
matter which agent claims the work:
# policies/client-trove.yaml
dispatch_requires_tag: client:trove # only tagged work is dispatchable here
max_wip_per_owner: 2 # an owner holds at most 2 live leases heredispatch_requires_tag—nextskips the project's untagged tasks (policy_missing_tag, with the required tag, under--explain) and a directclaimrefuses until the task is tagged.--forceis the deliberate override; the override is recorded in theclaimedaudit event aspolicy_overrides, so forced past-the-gate claims leave provenance.max_wip_per_owner— counts an owner's live leases within that project (the global--max-activecap stays independent). At cap,next --claimskips the project's candidates (policy_wip_cap, with the held ids) and picks the best task elsewhere instead of failing after the pick — a multi-project dispatcher is steered toward work it may actually take. A directclaimat cap refuses with the held ids;--forceoverrides.
Both gates run before the lease is acquired, refusals are audited as
claim_refused_policy on their own connection (gate kind + held ids, never
resurrected by the rolled-back transaction), and metrics reports
claims_refused_by_policy fleet-wide. Policy-less projects behave exactly as
before; deleting a policy file reopens dispatch immediately. Like the seam
guard, plan remains a read-only simulation — policy enforcement happens at
dispatch/claim time against live state.
A P0 task is useless if its P3 prerequisite never gets dispatched. next
therefore walks the dependency DAG in reverse to a fixpoint: every queued
prerequisite inherits the urgency of its dependents, so if a P0 task depends on
a P2 which depends on a P3, all three dispatch at P0 urgency. Stored priorities
are never mutated — inheritance is a dispatch-time view, composable with aging
(the better of the two effective levels wins). Terminal dependents confer
nothing (their chain is already satisfied), and cycle-checked edges guarantee
the fixpoint terminates. With --explain, an inherited pick reports
effective_priority and inherited_via (the nearest dependent that conferred
the urgency):
python3 "$A" next --explain # inherited_via shows which dependent made this urgentPriority flows upstream through the dep DAG; evidence should flow downstream.
When a prerequisite completes, its live handoff and latest sealed receipt are
the verified proof of what upstream produced — yet only unsatisfied
dependencies surface in context packs, so an agent picking up downstream work
starts blind to what it is building on. --dep-context N (on context,
recall, recall-verify, resume, and next --claim --recall) packs up to N
completed direct prerequisites into the bundle, each with its id, title, live
handoff (the resume point) and latest receipt (sealed evidence, payload
included), within the same character budget as everything else:
python3 "$A" recall <task-id> --agent codex --dep-context 3
python3 "$A" next --claim --owner codex --recall --dep-context 3Design properties:
- Opt-in and digest-sealed: every output key is present only when the flag
is used, so packs built without it stay byte-identical (and
digest-compatible) to the legacy shape. Using the flag moves the sealed
digest;
recall-verifyis fresh only under identical parameters. - Provenance-complete: the flag value is recorded in the audited
context_recalled/session_resumedpayload, soops.py recall-stalerecomputes cited digests exactly andrecall-diffreports adep_contextsection (prerequisite evidence added/removed) like any other section. - Budget-honest: each entry costs its real serialized size; under a tight
budget entries drop out and
truncatedflags it rather than lying. - Deterministic: prerequisites appear in dependency-edge creation order, so identical state yields identical bundles.
Operators and agents can park work with a reason without cancelling it:
python3 "$A" block <task-id> --owner leo --reason "waiting on credentials"
python3 "$A" unblock <task-id> --owner leo # requeues, clears the reason
python3 "$A" blocked-by <task-id> # transitive blockers, depth-taggedDesign properties:
- Lease-safe:
blocknever overrides a foreign or expired lease; blocking a task you hold a live lease on releases that lease so blocked tasks cannot look active to recovery or dispatch. - Audited: transitions are recorded as
blocked(withprevious_status) andunblockedevents in the hash chain. - Claim guard: claiming a blocked task is rejected with its reason —
deliberate migration from earlier behavior where blocked tasks were directly
claimable; call
unblockfirst. - DAG visibility:
blocked-bywalks all transitive prerequisites via a recursive CTE, reporting each blocker'sdepth(direct deps at 1), live status, title, andsatisfiedflag, plus a top-levelblockedboolean. - Reverse edges:
shownow includesdependents— the tasks waiting on this one — so operators can see what completing a task unblocks.
next --explain reports how many queued candidates were considered and why
each skipped candidate was not picked (unsatisfied_dependencies with the
blocking ids). Without the flag the output shape is unchanged:
python3 "$A" next --project Trove --explain
# → { "task": null, "considered": 3, "skipped": [{"task_id": "t2",
# "reason": "unsatisfied_dependencies", "blocked_by": ["t1"]}] }search does a substring match across id, project, title,
description, next_action, and blocked_reason, with optional
--status, --project, and --priority filters. Results use the same
priority-then-recency ordering as list.
Tasks can declare dependencies on other tasks. A task with an incomplete
dependency cannot be claimed, and next skips it when dispatching:
python3 "$A" create --project Trove --title "Dependent task" --depends-on <prereq-id>
python3 "$A" dep <task-id> <prereq-id> # add a dependency edge (cycles rejected)
python3 "$A" dep-remove <task-id> <prereq-id> # remove a mistaken edge (audited)
python3 "$A" next # highest-priority queued task whose deps are completed
python3 "$A" next --project Trove --claim --owner hermes --minutes 30dep-remove corrects a mistaken dep / create --depends-on call: the edge is
deleted, the removal is audited as dependency_removed, and the dependent task
becomes claimable immediately. Removing a non-existent edge (or naming a task
that does not exist) is rejected.
Tasks are not immutable: update can also edit identity fields, so a typo or a
re-prioritization does not force a create/cancel round trip:
python3 "$A" update <task-id> --title "Corrected title"
python3 "$A" update <task-id> --description "Fuller description" --priority P1
python3 "$A" update <task-id> --project RenamedProjectInvalid priorities are rejected by the CLI, and every change is recorded in the task's audit trail with the new values.
next orders by priority (P0 first), then oldest-created. With --claim, the
picked task's lease is acquired atomically in the same step, so concurrent
dispatchers can never double-claim. metrics reports
queued_blocked_by_deps for tasks waiting on prerequisites.
O=~/.hermes/autopilot/ops.py
python3 "$O" recover --max-retries 3 # requeue stale leases; fail tasks past retry budget
python3 "$O" recover --dry-run # preview what the next pass would do, mutating nothing
python3 "$O" approval approve <task-id> --by leo
python3 "$O" policy <project> <action> # check user-approval policy for an action
python3 "$O" processes # list active agent processes (read-only)
python3 "$O" github # GitHub integration surface (read-only)
python3 "$O" sentry # Sentry triage surface (read-only)
python3 "$O" morning # morning brief
python3 "$O" snapshot # consistent JSON export of all tables, sealed with a SHA-256
python3 "$O" snapshot-check <file> # verify a snapshot's integrity hash (exit 1 on tampering)
python3 "$O" snapshot-restore <file> --force # rebuild the database from a verified snapshot
python3 "$O" archive --before "2026-09-01T00:00:00Z" # seal + remove terminal tasks
python3 "$O" archive --before "..." --dry-run # preview without mutating
python3 "$O" archive-check <file> # verify an archive's integrity hash
python3 "$O" archive-restore <file> [--force] # re-import archived tasks
python3 "$O" notes-expired # list live notes past their TTL (read-only)
python3 "$O" consolidate [--task ID] [--dry-run] # merge near-duplicate notes into canonical facts
python3 "$O" onboard --inventory inv.json [--apply] [--probe] # one-command installer: init + import + doctor + protocol proofsnapshot writes an atomic, autopilot-snapshot-v1 JSON document (default
under ~/.hermes/autopilot/backups/) containing every table plus a self-hash,
giving a point-in-time backup for disaster recovery without touching the live
database. snapshot-check recomputes the hash and exits non-zero if the file
was modified.
snapshot-restore closes the recovery loop: it verifies the snapshot's
integrity hash before touching anything, refuses to overwrite a non-empty
database unless --force is passed, reloads every table in one transaction,
then re-checks foreign-key consistency and the audit hash chain (exiting
non-zero if either fails). Restores preserve audit-event ordering and lease
state exactly as snapshotted.
recover consumes one unit of each task's retry budget per pass. Tasks whose
retry budget is exhausted (retry_count > max-retries, default 3) transition to
failed with reason max lease retries exceeded instead of looping forever.
--dry-run reports would_recover / would_fail without touching state,
making it safe to run from monitoring cron before committing to a real pass.
A task whose lease just went stale is requeued by recover, but redispatching
it instantly lets a repeatedly failing task hot-loop through its retry budget.
Recovered tasks therefore enter a deterministic exponential cooldown:
recover_after = now + backoff_base * 2^(retry_count-1) seconds (default base
60s, capped at 3600s via --backoff-cap; --backoff-base 0 disables the
cooldown entirely for the old instant-redispatch behavior):
python3 "$O" recover --backoff-base 60 --backoff-cap 3600
python3 "$O" recover --dry-run # previews the cooldown per task in "backoff"Design properties:
- Dispatch-level enforcement:
nextnever picks a queued task whoserecover_afteris in the future;next --explainreports those candidates with reasonrecovery_backoffand their deadline. - Explicit override preserved: a direct
claimof a cooling-down task is still allowed as a deliberate operator action, and any successful lease acquisition clears the cooldown. - Audited: the
lease_recoveredaudit event records the appliedrecover_after. - Observable:
metricsreportstasks_in_backoff.
complete records success, but an agent that attempted the work and could not
finish had no first-class path — generic update --status failed silently lost
the attempt. fail is its counterpart:
python3 "$A" fail <task-id> --owner codex --reason "tests red after rebase"
python3 "$A" fail <task-id> --owner codex --reason "unrecoverable" --no-retry
python3 "$A" fail <task-id> --owner codex --max-retries 5 --backoff-base 120 --backoff-cap 7200Design properties:
- Lease-gated: only the current holder of a live lease may record failure,
fenced by
--epochexactly likecomplete; terminal tasks are final. - Shared retry budget: each failure bumps
retry_count, the same counter stale-lease recovery consumes, so an agent's failures and recoveries draw from one budget. Whileretry_count <= --max-retries(default 3) the task returns toqueued. - Backoff, not hot-looping: the task re-enters dispatch under the same
deterministic exponential cooldown as recovery —
recover_after = now + backoff_base * 2^(retry_count-1)seconds (default base 60s, capped at 3600s; base 0 disables).nextskips cooling-down tasks (reasonrecovery_backoffunder--explain); a direct claim stays allowed as a deliberate override and any lease acquisition clears the cooldown. - Terminal escalation: with the budget exhausted or
--no-retry, the task goes terminallyfailedwith the reason preserved inblocked_reason, and the response namesdependents_stranded— direct non-terminal dependents the permanent failure froze — so an operator can cancel or re-plan them. - Audited & observable:
task_failed(retry scheduled) ortask_failed_terminalin the hash chain;metricsreportsfailures_retried_total/failures_terminal_total.
Dispatch orders by priority then earliest deadline, but a stale P3 task that
misses its deadline keeps losing dispatch races to fresh P2 work forever.
ops.py escalate is the SLA sweep: every non-terminal task whose due_at has
passed climbs exactly one priority level per pass (P3→P2→P1→P0), so repeated
passes converge an ignored overdue task toward the front of the queue:
python3 "$O" escalate # bump all overdue non-terminal tasks one level
python3 "$O" escalate --dry-run # preview the bumps without mutatingDesign properties:
- Convergent, not jumpy: one level per pass keeps operator intent visible
in the audit trail instead of slamming everything to P0 at once; tasks
already at P0 are reported as
already_p0rather than being silently stuck. - Terminal-safe: completed/failed/cancelled tasks are never escalated even when overdue.
- Audited: each bump records a
priority_escalatedevent with the old and new priority, the deadline, andreason: overduein the hash chain.
Terminal tasks accumulate forever without a retention path. ops.py archive
consolidates them: every completed / failed / cancelled task with
updated_at <= --before is sealed into an atomic, self-hash-verified
autopilot-archive-v1 JSON document (default under
~/.hermes/autopilot/backups/), then removed from the live database together
with its dependencies, heartbeats, receipts, notes, and receipt files:
python3 "$O" archive --before "2026-09-01T00:00:00Z" --dry-run # preview ids + counts
python3 "$O" archive --before "2026-09-01T00:00:00Z" --out /path/archive.json
python3 "$O" archive-check <archive.json> # exit 1 if the file was tampered with
python3 "$O" archive-restore <archive.json> # re-import; --force replaces collisionsDesign properties:
- Seal-then-destroy: the archive file is written and fsynced before any row is deleted; deletion happens in one transaction in child-first order.
- Dependency guard: archiving refuses while any live task still depends on a terminal candidate, so dispatch prerequisites can never be archived out from under queued work.
- Append-only audit: audit events are retained in the live database (the
hash chain must stay verifiable) but are counted and copied into the archive
for reference.
verify-chainremainsokafter an archive pass. - FTS-consistent: deletions and restores fire the external-content triggers, so ranked search never surfaces (or misses) archived notes.
- Restorable:
archive-restoreverifies integrity first, refuses task-id collisions unless--force, reinserts rows in FK order, recreates receipt files atomically with0600permissions, and re-checks foreign keys.
Archives move retired fleet history; work orders move live work. A work order is the provider-neutral unit of cross-boundary recovery: one task's full execution state — the task row, dependency edges in both directions, complete note and handoff history, receipts with their sealed files, the heartbeat, and every temporal fact provenance-linked to the task (validity windows intact) — sealed under a single sha256 so any Autopilot home can verify integrity before importing:
python3 "$O" export-task t1 --out /tmp/t1.json # sealed autopilot-workorder-v1
python3 "$O" import-task /tmp/t1.json --dry-run # seal check + merge preview
python3 "$O" import-task /tmp/t1.json # merge into this homeDesign properties:
- Tamper-evident: the sha256 seal covers every exported row and receipt
file;
import-taskrefuses a mutated file before touching the database, and--forcedoes not bypass the seal. - Lease sanitization: an imported task can never arrive still leased —
claimed/running/waiting_for_agentreset toqueuedwith lease fields cleared, because the previous owner does not exist in this home. - Idempotent recovery: an identical re-import deduplicates (audited) instead
of duplicating; a changed export refuses without
--force, and--forcemerges rather than clobbers (local child rows are preserved, only new rows are inserted). - Dependency-aware: dependency edges are inserted only when both endpoints
exist locally; dangling ones are reported in
skipped_deps, never silently dropped. Import prerequisite tasks first to carry the full graph. - Fact graph travels with provenance: facts whose
task_idpoints at the exported task are carried with validity windows byte-intact and deduplicate by fact id on re-import; work orders sealed before the fact graph simply carry no facts key and import unchanged. - Privacy boundary on both ends: the same secret guard that protects
shared-memory writes scans the whole document at export and again at
import (so an
--allow-secretoverride at the source cannot leak credentials into this home unnoticed). Default is refuse;--redacttransfers[REDACTED:<kind>]copies; every decision is audited kind-only. - Atomic writes: files are written via fsync + atomic rename with
0600permissions; receipt files are restored only when absent locally.
completeenforces claim-before-complete: only the current holder of a live lease may complete a task. Unleased, foreign-held, or expired leases are rejected.cancelis an operator transition that tolerates an unleased task but never overrides a foreign or expired lease.block/unblockpark and requeue work with audited reasons; blocked tasks cannot be claimed until unblocked (see "Blocking & unblocking").- Any
update --statusto a terminal state (completed,failed,cancelled) releases the held lease so terminal tasks cannot look active.
events queries the global hash-chained audit ledger — not just the per-task
trail from show — with entity/action filters, an ISO 8601 time window, a
limit, and optional inline chain verification:
python3 "$A" events --action claimed --limit 20
python3 "$A" events --entity-id <task-id> --since "2026-08-21T00:00:00Z" --until "2026-08-22T00:00:00Z"
python3 "$A" events --limit 100 --verify # also recompute the chain in the same callResults are newest-first and carry parsed payload objects. total_matching
and truncated report how the limit clipped the result set; invalid
timestamps are rejected with the offending flag named in the error.
Every audit event is linked into a SHA-256 hash chain (prev_hash, hash).
Existing databases are migrated and backfilled automatically on first use.
autopilot.py verify-chain recomputes the chain and reports any
hash_mismatch or broken_link, giving tamper-evident history. ops.py doctor runs the same check as part of a broader consistency sweep.
A hash chain is tamper-evident for modification but blind to tail
truncation: deleting the newest events leaves every remaining link perfectly
valid. Checkpoints close that gap by pinning the chain head — last event id,
head hash, and total count — into a self-hash-sealed
autopilot-checkpoint-v1 file:
O=~/.hermes/autopilot/ops.py
python3 "$O" checkpoint # seal the current head (default under backups/)
python3 "$O" checkpoint-check <file> # verify the seal + containment in the live chain
python3 "$A" verify-chain --checkpoint <file> # chain recompute + checkpoint pin in one callDesign properties:
- Divergence is proof: a missing pinned event (
chain_truncated), a changed head hash (checkpoint_head_mismatch), or a shrunken event count (events_removed_since_checkpoint) each prove history was deleted or rewritten after the checkpoint was sealed. Growth past the checkpoint is normal operation and never flagged. - Seal-then-compare: the checkpoint file carries its own integrity hash; a modified file is refused outright rather than trusted.
- Doctor-integrated:
ops.py doctorvalidates everycheckpoint-*.jsonunderbackups/against the live ledger on every sweep, so a stale operator checkpoint turns truncation into a routine finding.
python3 "$O" doctor # orphan deps, receipt index/file drift, audit chain, stale leases, note integrity, checkpoint pinsqueued → claimed → running → waiting_for_user → completed
↘ waiting_for_review
↘ blocked
Terminal states are completed, failed, and cancelled.
Receipts are stored in receipts/ and indexed in SQLite. Every new receipt is
integrity-sealed: the row carries a file_hash (sha256 of the exact file
bytes, also printed by the receipt command), and ops.py doctor re-verifies
each sealed file so silent corruption or tampering surfaces as a
receipt_file_hash_mismatch problem. Rows created before sealing
(file_hash='') are skipped by the check. A completed engineering task should carry evidence such as:
- test/typecheck result
- commit SHA
- PR URL
- CI result
- deployment URL
- health-check result
- approval record
A self-report is never execution truth without a receipt. complete accepts
repeatable --receipt <id> flags citing integrity-sealed receipts on the
same task; unknown ids and other tasks' receipts are refused, and the cited
ids are recorded in the audited completed event so provenance survives in
the chain. Omitting the flag keeps the legacy shape byte-compatible.
Two observability paths make unverified completions visible instead of aspirational:
metricsreportscompletions_without_receipt— completed tasks with zero receipts (bare agent claims);ops.py unverified-completionssweeps the fleet read-only for both bare claims (no_receipts) and completions whose cited evidence later vanished (evidence_receipt_missing, e.g. deleted rows or partial restore).
Project policies can gate side-effectful readiness promises: with
policies/<project>.yaml containing merge_requires_user: true (or
deploy_requires_user), update --status ready_to_merge|ready_to_deploy
refuses until --approved-by <name> names who accepted it. The approver and
gate kind are recorded in the audited updated event. Re-stating the current
status is not a transition and stays ungated; projects without a policy file
behave exactly as before.
Evidence citations on completion are stronger when the acceptance criteria are
declared up front. Tasks carry an optional requires_receipts list — repeatable
--requires-receipt <kind> at create, or update --requires-receipt to set
or revise it later (a single empty string clears it deliberately). Kinds use
the same restricted token charset as tags so they survive CLI flags, dispatch
data, and sweep output across every agent adapter.
complete then enforces the definition of done as data: until at least one
receipt of every required kind exists on the task, completion refuses with
the missing kinds named, and the refusal is audited as
completion_blocked_evidence on its own connection (gate kind — never erased
by the rolled-back transaction). Once every kind is satisfied, the completion
carries required_evidence_met: true. Observability mirrors the gate:
metricsreportstasks_missing_required_evidence— open work whose acceptance criteria are not yet satisfiable — andcompletions_blocked_by_evidence, the fleet-wide count of gate refusals.
Tasks without requirements behave exactly as before; the legacy completion shape is unchanged apart from the new field when requirements exist.
brain-inventory is the end-to-end, dry-run-first counterpart to
migrate-inventory: where the migration inventory discovers sources under a
root, the brain inventory reads the known durable surfaces of a whole MindOS
installation and seals one honest manifest about them. Every source carries an
epistemic role so downstream stages can treat each kind differently instead of
flattening the brain into one authority:
| source | role | what is recorded |
|---|---|---|
autopilot (state.db + receipts) |
execution truth | integrity verdict, row counts, receipt-file count |
temporal (temporal.db) |
temporal facts | integrity + entity/relation/event counts; absent if none |
memories (state.db) |
semantic memory | engine tag plus live/retracted memory counts; shares the control-plane file, inventoried as its own epistemic source |
claude_sync (claude-memory-sync.json) |
sync metadata | entry count, sha256 |
claude_memory (~/.claude/projects/*/memory) |
human archive | per-file checksums across all projects |
sessions (raw store + control-plane cache) |
session cache | file checksums plus derived row counts |
profiles, skills, cron |
definitions | checksummed inventories; cron job count parsed from jobs.json |
python3 "$O" brain-inventory --out brain.json # defaults: live homes
python3 "$O" brain-inventory-check brain.json # verify the seal before trusting itDesign rules, enforced and tested:
- Dry-run-first / read-only by construction: databases open with read-only URIs, and a full-fixture hash comparison proves nothing scanned is ever mutated — including on fail-closed runs. Since the Hindsight probe was retired this command makes no outbound network call at all, so an offline machine inventories the full brain with nothing degraded by reachability.
- Redaction: values never enter the manifest. Text files are secret- scanned with findings reported by kind only; every recorded statistic is a stable count, never a timestamp or latency figure.
- Sealed & reproducible: versioned
mindos-brain-inventory-v1documents, sha256-sealed withcreated_atoutside the digest, so unchanged sources re-seal byte-identically and interrupted installs resume against the same manifest; tampering any field makesbrain-inventory-checkrefuse it. The manifest itself is the audit artifact: because the live Autopilot home is a source, this end-to-end inventory does not append even a digest-only bookkeeping event to it. - Honest degradation: corruption or ambiguity fails closed naming exact
blockers (garbage
jobs.json, a non-SQLitetemporal.db); an absent optional sidecar and an absent sync file are recorded asabsent/unavailablewithout blocking. - Bounded & atomic: file-count caps keep giant trees from stalling the
sweep (overflow reported, never silent); manifests are written atomically
with
0600permissions.
brain-import consumes only a sealed brain inventory and requires an explicit
absolute new home. It is a separate layer from migrate-import: execution
truth stays at the existing SQLite migration boundary, while this command
brings across the non-execution brain surfaces. Semantic memory is not among
them: it lives in the control-plane database and moves with the execution-truth
import, so there is never a second semantic authority to reconcile.
python3 "$O" brain-import --inventory brain.json --target /absolute/new/mindos
python3 "$O" brain-import --inventory brain.json --target /absolute/new/mindos --apply --redactThe default command is a no-write plan. Under --apply, it first re-hashes
every selected file from the inventory, refuses source drift or a different
pre-existing target file, then creates files atomically with 0600 mode. It
imports temporal.db, Claude sync metadata, Claude memory archives, portable
raw session-store files, profile/config declarations, skill SKILL.md
declarations, and cron/jobs.json; derived session cache artifacts, profile
runtime state, non-definition skill support/cache trees, and non-definition
cron artifacts are explicitly quarantined rather than guessed at. Every copied artifact has source and target
checksums in provenance/brain-import-<inventory-sha>.json, and an applied run
writes a sealed mindos-brain-import-v1 report under migrations/ (or --out).
Credential-shaped text is quarantined by default. --redact copies only a
redacted derivative; non-text secret-bearing sources remain quarantined. The
quarantine records identities, checksums, kinds, and reasons — never values.
Re-running the same inventory verifies the already-created bytes and is a
no-op; it never replaces local destination data.
brain-import writes no service binding. The retired Hindsight binding file
(bindings/hindsight-shared-bank.json) is gone along with the service it
pointed at; the memories source is planned as
external_execution_import_required, because semantic memory travels inside
the control-plane database rather than being copied out as a second
authority.
Bringing a new machine (or a new Autopilot home) up starts with honest
discovery, not import. migrate-inventory walks an explicit --root — it
never guesses at live homes or touches a source — and classifies the durable
sources the migration model cares about: Autopilot SQLite databases
(execution truth), legacy Hindsight banks (importable via memory-import), Hermes homes
(profiles/skills/cron/ownership), Obsidian vaults (optional human archive),
and unrecognized SQLite files. Every source is checksummed file-by-file,
Autopilot databases get a read-only integrity check plus row counts, and all
scanned text is run through the same secret detector as the shared-memory
write path — findings are reported by kind only, so the manifest can be
shared without leaking credentials.
The output is a versioned, sha256-sealed autopilot-migration-inventory-v1
manifest carrying an ordered migration plan (control plane first, then
Hindsight, Hermes registration, Obsidian archive) and is audited as
migration_inventory_sealed in this home:
python3 "$O" migrate-inventory --root /Volumes/old-machine --out inventory.json
python3 "$O" migrate-inventory-check inventory.json # verify the seal before trusting itDesign rules, enforced and tested:
- Read-only: sources are opened with SQLite read-only URIs; symlinks are never followed; a full-fixture hash comparison proves discovery mutates nothing.
- Fail-closed: any corrupted database (
integrity_check != ok, unreadable bytes) or ambiguous source (valid SQLite without the Autopilot schema) marks the inventoryfail_closedand exits non-zero, naming every blocked source — ambiguity must be resolved by an operator before any import runs. - Deterministic: identical trees produce byte-identical manifests modulo
created_at(sorted traversal, plan-rank ordering), so interrupted migrations resume against the same plan. - Bounded: file-count and per-file size caps keep giant vaults from stalling the sweep; cap overflow is reported in the manifest, not silent.
- Sealed manifests: tampering any field breaks the sha256 seal and
migrate-inventory-checkrefuses the document; manifests are written atomically with0600permissions.
migrate-import turns a sealed stage-one inventory into an actual import of
Autopilot execution truth — tasks, dependency edges, receipts, notes, and
handoffs — into this home. It binds to the inventory (--inventory +
--source-id), refuses fail-closed manifests, re-verifies both the manifest
seal and the source database checksum (so drift since discovery is caught
before any data moves), opens the source read-only, and merges every table in
one transaction with INSERT OR IGNORE on natural keys:
python3 "$O" migrate-import --inventory inventory.json --source-id src-… # dry-run plan
python3 "$O" migrate-import --inventory inventory.json --source-id src-… --apply # do itDesign rules, enforced and tested:
- Dry-run first: without
--applynothing is written; the plan reports per-table source/already-present counts, sanitized tasks, secret kinds, skipped disposable heartbeats, and whether the target would need--relink-audit. - Idempotent: an identical re-run deduplicates to nothing (
deduplicated: true) — interrupted migrations resume by simply re-running; audit events already absorbed by a prior import are recognized by content twin and never double-inserted. - Lease sanitization: tasks that arrive mid-flight (
claimed,running,waiting_for_agent) are reset toqueuedwith leases cleared, because the prior owner does not exist here; heartbeats are disposable liveness cache and are deliberately not carried. - FK-orphan refusal: before either dry-run or apply, the source is checked
with
PRAGMA foreign_key_check. Any dangling receipt, heartbeat, or other foreign-key row is named and refused; the importer never weakens constraints, invents tasks, attaches evidence by guess, or repairs the source in place. Resolve the source separately through an approved remediation path, then re-seal the inventory. - Audit-chain integrity: a fresh home imports the source chain verbatim;
a home with genuine history refuses to merge a foreign chain (that would
break tamper evidence) unless
--relink-auditexplicitly relinks the combined ledger. Stage-one's own bookkeeping does not make a home "lived-in", so the canonical init → inventory → import flow needs no flags. - Secret boundary: notes/handoffs/receipts/facts pass through the same
guard as the shared-memory write path — credential-shaped content is refused
by default,
--redactimports[REDACTED:<kind>]copies (their source receipt files are withheld rather than reintroducing the secret),--allow-secretoverrides audited. Fact tokens cannot be credential-shaped by construction, but their free-formsourcefield is operator text and is scanned like any other note. - Fact graph crosses with execution truth: the temporal fact graph imports idempotently by fact id with validity windows intact; a source home whose schema predates the facts table is legacy shape — it classifies healthy and imports cleanly with zero facts instead of refusing.
- Restore verification: receipt files reappear byte-exactly, each checked
against its sealed
file_hash; a post-apply health report (integrity check, FK violations introduced by the import, audit-chain problems, per-table coverage) exits non-zero on any problem. Pre-existing target conditions (e.g. intentionally dangling dep edges) are baselined before the import and reported separately instead of falsely failing the migration. - Sealed result:
--outwrites an sha256-sealedautopilot-migration-result-v1document for operator records — which now doubles as the rollback journal (see next section).
Every migrate-import --apply --out now seals a rollback journal inside
the result document: exactly the rows it inserted per table (with each row's
content hash at insert time), the audit event ids it merged, and the receipt
files it restored. migrate-rollback consumes that journal to undo the
import precisely — not "delete everything that looks migrated", but delete
exactly what that apply added:
python3 "$O" migrate-rollback migration-result.json # dry-run plan
python3 "$O" migrate-rollback migration-result.json --apply # undo itDesign rules, enforced and tested:
- Dry-run first: without
--applynothing is written; the plan lists rows it would remove, what is already gone, audit events and receipt files in scope, plus any drift or local dependents that would demand--force. - Fail-closed on changed truth: a row whose content hash differs from its
insert-time journal hash has become live local execution truth since the
import — rolling it back would destroy someone's newer work, so the
rollback refuses naming the drifted rows. Local rows that were never
imported but depend on an imported task (a note an agent wrote afterwards,
a dep edge added locally, a fact whose soft provenance would dangle) block
the same way — facts have no FK cascade by design, so under
--forcethey are deleted explicitly rather than left pointing at removed tasks.--forceis the explicit override: drifted rows and dependents cascade away with the task, and the auditedmigration_rollback_appliedevent records that it was forced. - Chain integrity: removed audit events leave no gaps in tamper evidence
— the ledger is relinked into a continuous chain and must verify
(
verify-chain) before the rollback reports success. - Receipt files: deleted only while they still match their sealed hash; a locally changed file is kept and reported as withheld rather than destroyed.
- Idempotent: re-running against an already-rolled-back home deduplicates to nothing; the sealed document itself is verified before use, so a tampered or hand-edited result doc is refused; documents from imports run before journals existed are refused with a clear regeneration hint.
The full installer lifecycle is therefore: init → migrate-inventory →
migrate-import --apply --out → (verify) → optional migrate-rollback,
each stage dry-run-first, sealed, and independently re-runnable.
The installer's front door: one command that takes a sealed stage-one inventory and turns a fresh machine into a verified working Autopilot home — no manual file choreography:
python3 "$O" onboard --inventory inventory.json # plan + verify, nothing imported
python3 "$O" onboard --inventory inventory.json --apply # import + doctor
python3 "$O" onboard --inventory inventory.json --apply --probe # + cross-agent protocol proof
python3 "$O" onboard --inventory inventory.json --apply --out report.json # sealed onboarding reportStages run in order and any failure stops the run before later stages, exiting non-zero naming what failed:
- preflight — python/sqlite versions, FTS5 availability (recorded as a warning when absent: ranked retrieval degrades to LIKE), home writability.
- init_control_plane — idempotent
init; a bare directory is enough. - select_source — verifies the manifest seal, refuses fail-closed
inventories outright, auto-selects the single healthy
autopilot_sqlitesource; several candidates without an explicit--source-idis ambiguous and refuses listing them. - import_plan — the migrate-import dry-run always runs first, so the
report shows exactly what would move before anything does. Secret policy
flags (
--redact,--allow-secret) and--relink-auditforward to the import unchanged. - import_apply (only under
--apply) — the real import; its sealed rollback journal lands in<home>/migrations/, so every onboard is undoable by the stage-three tooling. - doctor — the full consistency sweep must come back clean or onboarding fails closed.
- protocol_probe (
--probe, requires--apply) — execution proof of the contract's cross-agent requirement: two distinct identities (hermeshands off,codexpicks up) exercise handoff → recall → ack → resume → receipt → complete against a dedicatedonboard-probetask through the real CLI.
Everything is idempotent, so an interrupted onboarding resumes by running the
same command again: init re-runs harmlessly, imports deduplicate to nothing,
the probe skips when its task has already settled. Dry-run writes nothing but
its own bookkeeping audit events (the same convention as migrate-inventory,
and such bookkeeping never makes a home "lived-in" for a later import) — which
is why --probe refuses to run without --apply: a probe mutates by design.
With --out, a sha256-sealed autopilot-onboarding-v1 report (0600, seal
excluding only created_at) records every stage for audit; the stdout summary
is a compact view of it.
Agents' raw conversation transcripts are the freshest record of what was actually tried and decided — but they live in vendor-specific stores outside the control plane. The session-ingestion adapter indexes them into a disposable, rebuildable cache and exposes them through the same retrieval and context-pack protocol as notes and handoffs:
python3 "$A" session-scan --root ~/library-session-store # redacted dry-run inventory
python3 "$A" session-ingest --source claude-code --root ~/.../proj-a \
--project Auth --apply --redact # incremental index
python3 "$A" search-sessions "redirect cookie" --role assistant --rank
python3 "$A" sessions-prune --older-than 30d # bounded retention pass over the session cache
python3 "$A" context <task-id> --related-sessions 3 # packs matching snippetsDesign properties:
- Read-only by construction: source stores are opened for reading only, never mutated; symlinked files/directories are never followed, so a planted link cannot smuggle unrelated private context into shared memory.
- Cache, not truth: ingested rows are derived data keyed by each file's sha256 — a changed transcript re-indexes atomically (delete+insert), an unchanged one costs a single hash read, and an interrupted run resumes by re-running. Raw conversation is never execution truth.
- Redacted inventory first:
session-scan/ dry-runsession-ingestreport counts and kind-only secret findings; message content never leaves the process before an explicit--apply. - Secret guard: credential-shaped content is refused by default;
--redactstores[REDACTED:<kind>]copies;--allow-secretis audited. The guard gates what a run would write — unchanged files already settled at their original ingest never block later incremental runs. - Honest format handling: Claude Code-style JSONL (
type/message/ timestamp) and generic{role, content, timestamp}lines are recognized; tool calls/results and unknown roles are counted and skipped, malformed lines counted, files with zero recognizable messages reported asunsupported— unstable formats degrade into a report, not garbage. - Provenance on every message:
source,profile,project, session id, sequence, role, and timestamp travel with every hit in search results and context packs. - Context-pack integration:
--related-sessions N(oncontext,recall,recall-verify,resume,next) appends bounded snippets of ingested messages matching the task's text after dep context, under the same budget and flag-gated digest rules as related handoffs — packs built without the flag stay byte-identical. Ordering is deterministic (at DESC, session, seq), so digests stay stable until real context drift. - Observability & hygiene:
metricsreportssessions_indexed/session_messages_indexed;ops.py doctorincludes the session FTS index in its fts5vocab drift sweep.
The cache is disposable by design, so it gets a matching retention pass — bounded, planned first, and rebuildable:
python3 "$A" sessions-prune --older-than 30d # read-only dry-run plan
python3 "$A" sessions-prune --older-than 30d \
--source claude-code --project Auth --apply # bounded pruneDesign properties:
- Bounded by construction:
--older-than(a relative ageNd/Nh/Nmor an absolute ISO timestamp) is mandatory — no filter combination can ever express "delete everything". A session's age is its last message time, falling back to ingest time;--source/--profile/--projectonly narrow the scope. - Dry-run plan first: the default run reports exact candidates with per-source counts (sessions, messages, bytes) and touches nothing.
- Honest apply: one transaction deletes only cache rows; the FTS triggers
keep the search index in sync and source transcript files are external
stores that are never touched. Each affected source gets a single audited
session_prunedevent with its counts; a zero-candidate apply audits nothing. - Rebuildable: pruning is never data loss — re-running
session-ingestrestores pruned sessions from their sources, which is also the recovery path for an over-eager prune.metricsreportssessions_pruned_total.
Notes are task-scoped prose; the fact graph is the machine-queryable layer of
the temporal sidecar: subject predicate object triples — service-auth uses postgres-14, deploy-api reads redis-cache — that any agent asserts with
provenance and retracts (or lets expire) as the world changes. Validity
windows make time a first-class query dimension: retrieval packs only what is
currently true, while closed windows stay queryable as history:
python3 "$A" fact-assert --subject service-auth --predicate uses --object postgres-14 \
--source codex --task t1 --valid-hours 48 # provenance + validity window
python3 "$A" fact-assert ... # identical live triple -> deduplicated
python3 "$A" fact-retract <fact-id> --reason "migrated to postgres-16"
python3 "$A" facts --subject service-auth # currently-valid triples only
python3 "$A" facts --all # include closed windows (live flag)
python3 "$A" search-facts postgres --rank # BM25 over subject/predicate/object/sourceDesign properties:
- Restricted tokens: subject/predicate/object use the tag charset
(
[a-z0-9:_./-]), so facts are safe as CLI flags and LIKE filters across every agent adapter — and credential shapes cannot survive the charset, so the privacy boundary is structural rather than pattern-based. - Deduplication: an identical triple still inside its validity window
deduplicates to the existing row (audited
fact_deduplicated) instead of growing the store; re-asserting after expiry records a fresh row and keeps the old window as history. - Provenance: every fact carries its asserting agent (
--source) and an optional originating task (--task, must exist). The reference is a deliberate soft link: archiving a task detaches provenance instead of destroying fleet knowledge, andops.py doctorflags any dangling ref as evidence of out-of-band surgery. - Context-pack integration:
--related-facts N(oncontext,recall,recall-verify,resume, andnext --claim --recall) packs up to N currently-valid facts matching the task's text after dep context and sessions, within the same budget. Flag-gated keys keep legacy packs byte-identical and digest-compatible; the flag value travels in the audited recall/resume provenance soops.py recall-stalerecomputes exactly, andrecall-diffreports arelated_factssection. - Deterministic retrieval: pack candidates order by
valid_from DESC, idwith no relevance score emitted — BM25 scores drift whenever any fact joins the index, which would falsely stale sealed digests without real drift. - Observable:
metricsreportsfacts_total/facts_live/facts_closed;ops.py doctorincludesfacts_ftsin its FTS drift sweep; snapshots carry thefactstable.
MindOS is source-available under the Functional Source License 1.1 with an MIT future license (FSL-1.1-MIT). You may use, copy, modify, and redistribute it today for any non-competing purpose (internal use, non-commercial education/research, professional services); it becomes fully MIT-licensed two years after each release. This is not OSI open source during the restricted period.
- README.md — what MindOS is, command surface, design rules (this document)
- ARCHITECTURE.md — layers, invariants, diagrams, fact table
- docs/COMBINATORIAL-ARCHITECTURE.md — the combinatorial capability model
- INSTALL.md — install + migration from an existing home
- CONTRIBUTING.md — workflow, gates, commit style
- SECURITY.md — safety boundary, data boundary, secret guard
- ROADMAP.md — shipped vs. next vs. non-goals
- CHANGELOG.md — notable changes
- STABILITY.md — audited fixed issues / accepted risks
- RELEASE.md — release policy: no public release yet; FSL-1.1-MIT terms
- LICENSE — FSL-1.1-MIT with automatic MIT future license
A full runtime audit (see STABILITY.md for the complete report) fixed the
concurrency and false-success seams it found:
- Guarded lease mutations —
complete,fail, andreleasenow land only while the lease is exactly as checked (AND lease_owner=? AND lease_expires_at>?); a lease stolen between an agent's read and write turns the write into alease changed since checkrefusal instead of clobbering the new owner's claim.ops.py recoverandescalatesweep with the same guard and report mid-sweep losses under a new additiveskippedkey. - Sealed approval receipts —
ops.py approvalwrites hash-sealed receipt files likereceiptdoes, so approvals no longer poisondoctorwith permanentreceipt_file_missingfindings. - Real FTS drift detection —
COUNT(*)over an external-content FTS5 table reads through to the content table and cannot see index drift;doctornow compares each index (notes_fts,tasks_fts, and the previously uncheckedhandoffs_fts) against its true inverted-index contents viafts5vocab, naming missing/stale rowids, with a count fallback when fts5vocab is absent. - Smaller seams — duplicate
create --idrefuses cleanly instead of raising IntegrityError;tag/untagare compare-and-swap so concurrent writers cannot drop each other's tags; malformed or missing migration-result documents fail with clean messages;ops.py policyresolves policies underHERMES_AUTOPILOT_HOMEinstead of a hardcoded live-home path;ops.pyis importable (__main__-guarded) for in-process testing.
The runtime resolves its home in a strict precedence order, so the new MindOS
home can be selected without ever touching ~/.hermes/autopilot:
HERMES_AUTOPILOT_HOMEenv var (unchanged; always wins)- An explicit selector file —
~/.hermes/autopilot-home-selector.json(override withAUTOPILOT_HOME_SELECTOR), written atomically0600byops.py home-selectand removed byops.py home-deselect --apply - The immutable rollback default
~/.hermes/autopilot
Nothing writes the selector implicitly: a missing, unreadable, or malformed selector degrades silently to the default rather than failing the runtime. A selector pointing at a directory that no longer exists is ignored.
O=~/.hermes/mindos/ops.py
python3 "$O" home-doctor --home ~/.hermes/mindos # read-only health sweep of an explicit home
python3 "$O" home-show # which home is active and why
python3 "$O" home-select --home ~/.hermes/mindos # verify-then-select (refuses an unhealthy home)
python3 "$O" home-deselect # read-only dry-run plan
python3 "$O" home-deselect --apply # one-command rollback to ~/.hermes/autopilothome-select refuses to point at the rollback default itself and runs a
read-only doctor sweep first (audit chain, sealed checkpoints under
<home>/backups, stale leases) — selection fails closed naming every problem,
so a default switch only ever follows verified health. Deselecting is the
one-command rollback; the old home's data is never mutated by any of this.
Semantic memory behind the same shape as sessions/facts, and behind the same
retrieval machinery: the memories table in the control-plane database with an
FTS5 index over it. Retrieval is deterministic token matching — no model, no
embeddings, no network. Nothing outside the process is consulted.
This replaced an external Hindsight adapter (a bank.jsonl file read
out-of-band, plus a provider HTTP service on port 8888). Moving the store
in-database removed a class of failure the old design invited: cross-context
bleed from a shared external bank, torn JSONL lines, retains that landed on
disk but not in the audit chain, and sealed pack digests silently invalidated
by an out-of-band file edit. See "Memory engine migration" below for the
one-shot import path off a legacy bank.
- Recall (
--related-semantic N) oncontext,recall,recall-verify,resume, andnext --claimpacks up to N matching memories under the same budget as other sections, each row carrying its own engine tag (memory-fts-v1) so staleness detection covers the semantic section. Ordering is deterministic (created_at DESC, thenid ASC) so digests are exactly recomputable, and — like related facts and handoffs — no relevance score is emitted, because BM25 drifts whenever any row joins the index and would falsely stale sealed digests. Retracted memories never pack. - Scope is enforced, not preferred. With
--related-scope project(the default) a pack sees that project's memories plus project-less fleet-wide ones, and nothing else. A scoped recall that matches nothing returns nothing; it does not fall back to unscoped results. - Empty is healthy: with nothing retained the flag is a no-op and
ops.py doctorreportsmemory_storeas a note (status: empty), never a problem. - Retain (
memory-retain) writes the row and its audit event in one transaction, behind the same secret guard as notes (--redactstores placeholders,--allow-secretoverrides, both audited). Memory ids are content-addressed per project, so retaining the same fact twice is an idempotent no-op rather than a duplicate. - Retract (
memory-forget <id>) stops a memory packing without deleting it, so the audit chain still explains why a pack that once carried it no longer does.--superseded-by <id>points at the memory that replaced it. - Inspect (
memory-list) is the read-only view of the store: filter by--project,--query, and--allto include retracted rows.
python3 "$A" memory-retain --text "auth sessions expire after 30 days" \
--kind decision --project Auth --tag auth
python3 "$A" memory-list --project Auth
python3 "$A" context <task-id> --related-semantic 5
python3 "$A" memory-forget mem-18da4972e0ff92c8A legacy Hindsight bank.jsonl imports in one command. The source file is
opened read-only and never mutated; malformed lines are counted and skipped,
created_at is preserved so imported memories keep their place in temporal
order, and the same secret guard as memory-retain applies. Content
addressing makes a re-run a no-op, so an interrupted import is simply re-run.
python3 "$A" memory-import ~/.hermes/hindsight/bank.jsonl # dry run
python3 "$A" memory-import ~/.hermes/hindsight/bank.jsonl --applyThe hourly control tower should read this registry and report task changes. Existing project-specific policies should be added under policies/ before allowing automatic side effects.