Product requirement ID:
REQ-P-PRODUCT-001The retained ATM product surface consists ofsend,list,read,ack,clear,log,doctor,teams, andmembers, backed by a singleton daemon runtime and SQLite source-of-truth for mail and roster state in the current daemon architecture.
Satisfied by:
- intentionally undecomposed product requirement; this governs overall retained product scope rather than a single crate-local obligation
The product is a local command-line tool named atm.
The current target architecture uses a tightly-bounded singleton daemon runtime for same-host local IPC, mail routing, and native-agent notification. ATM command behavior remains the user-facing surface. The prior custom cross-host transport is superseded; Phase AI defines the replacement HTTPS/TCP adapter.
Phase-AA simplification direction:
- the daemon remains part of the product, but it must return to the original thin-router role
- concrete SQLite construction moves to a dedicated
atm-runtimecrate AA.2lands that composition root and moves production runtime/store assembly there; later AA sprints finish the doctor split, delete remaining daemon SQLite leaks, and relock the boundary permanently- the daemon must not know that the current durable adapter is SQLite
- direct local diagnostics such as store-openability and baseline SQLite health may be answered without routing through the daemon
- the supported 1.2 SQLite bootstrap/migration contract is the current
message_id-based durable schema only; abandoned pre-production identity scaffolding such aslegacy_message_idis not an accepted runtime shape - no repair path is offered for abandoned pre-production databases carrying
only a
legacy_message_idcolumn; operators with such databases must discard them and allow ATM to initialize a fresh schema atm doctornow uses that direct local path for config/store diagnostics throughatm-runtime; daemon routing remains only for daemon-owned runtime state- each subsystem owns its own diagnostic trait and backend-specific diagnosis
- top-level doctor code aggregates subsystem findings and daemon-owned runtime state, but must not reimplement backend-specific diagnosis logic
MailStoreandRosterStoreremain the primary storage-neutral capability traits during this simplification lineMailStoreDoctor,RosterStoreDoctor, andConfigDoctorare the explicit subsystem doctor traits used by that aggregate-only health model- later SQLite-backed and Claude-JSON-backed implementations may satisfy that same behavior-named trait family rather than forcing backend-shaped parallel trait trees
Phase-AC supersession note:
- the Phase
AAsimplification-line store traits above are transitional only - Phase
ACreplacesMailStorewith canonicalMessageStore, converges the roster seam intoRosterStore, and leaves task storage out of scope for the initial sharedatm-storagecontract - speculative task-store code was deleted in
AC.6rather than preserved as a compatibility line - if task storage is approved later, the first canonical implementation starts from Claude-code task schema plus Pydantic validation; SQLite may only sync to that canonical model afterward
- after Phase
AC,MailStore/RosterStoreare historical transition names; a later approved task-storage phase would reintroduce a fresh canonical design instead of reviving speculative transition scaffolding
Phase-AX amendment (2026-09-04): superseded. Task storage is approved in
Phase AX (phase plan §2). Its canonical model is ADR-062's daemon-owned,
message-derived Rust state machine in atm-storage and
atm-storage-rusqlite; the Claude-code-schema-plus-Pydantic direction is
withdrawn because task state derives from messages already persisted by the
daemon, the write path has no Python, and a Claude Code task list is a
per-session harness artifact rather than a cross-host record. The AC.6
deletion stands: ADR-062 revives none of that scaffolding.
Phase-AX.6 amendment (2026-09-05): lead notification and escalation are part
of the retained task surface. atm-daemon is the reserved daemon actor and
cannot be added to or renamed in the roster. Configured daemon and per-team
escalation recipients are additive fan-out destinations, with a per-tick cap
of eight.
Phase-BA amendment (2026-09-11): escalation is one ordinary message to the
roster lead and to every configured escalation recipient, resolved
independently. An open task escalates once when its reminder count reaches 10
and nudging then stops until the task changes state (start or close) or is
reassigned or reopened — a change in the assignee's
runtime state alone does not resume it; a Blocked or Offline assignee
escalates once per episode and receives zero nudges. See Section 15.4.
The retained product surface is:
atm sendatm listatm readatm ackatm clearatm logatm doctoratm teamsatm members
Approved additive CLI feature for the Phase Y line:
atm help
Approved additive CLI feature for the Phase AX line:
atm escalation add|remove|list
Approved additive CLI feature for the Phase BA line:
atm task assign|close|move|list|events
The system must preserve the retained command behavior unless these requirements explicitly retire or change it.
The system uses structured logging through sc-observability.
Schema ownership references:
- Claude Code-native message schema:
claude-code-message-schema.md - ATM additive/interpreted message schema:
atm-message-schema.md - legacy ATM read-compatibility schema:
legacy-atm-message-schema.md(historical only; Phase U removed itsmetadata.atmcoverage from the active compatibility design) sc-observabilityschema ownership pointer:sc-observability-schema.md- ATM-owned error-code registry:
atm-error-codes.md - schema enforcement models:
tools/schema_models/claude_code_message_schema.pyandtools/schema_models/atm_message_schema.py - historical/read-compatibility schema record only:
tools/schema_models/legacy_atm_message_schema.py
Documentation organization is defined in
documentation-guidelines.md.
Top-level product docs in docs/ remain the product source of truth.
Crate-local ownership docs live under:
docs/atm/requirements.mddocs/atm/architecture.mddocs/atm-core/requirements.mddocs/atm-core/architecture.mddocs/atm-daemon/requirements.mddocs/atm-daemon/architecture.mddocs/atm-runtime/requirements.mddocs/atm-runtime/architecture.mddocs/atm-rusqlite/requirements.mddocs/atm-rusqlite/architecture.mddocs/atm-core/boundaries.mddocs/atm-daemon/boundaries.mddocs/atm-runtime/boundaries.mddocs/atm-rusqlite/boundaries.mddocs/atm/boundaries.md
During the cleanup/restructure phase, product requirements stay here while crate-local ownership is moved out of this file into the crate directories.
Phase-Q supersession note:
- earlier daemon-free assumptions in this file are historical requirements from the prior rewrite line
- for mail/runtime architecture, the current authoritative direction is Section 21
Phase-R redesign note:
- Phase R hardens the architecture by making crate-local boundary records part of the enforceable contract before new implementation work proceeds
- Phase R planning and CI may depend on
sc-lintas an external tool dependency;sc-lintis not part of the ATM product surface even when its verification model constrains Phase R gates
Phase-AI portability note:
- ATM daemon functionality is first-class on Windows as well as Unix-like hosts
- the authoritative local access contract is Unix HTTP/UDS or loopback TCP and Windows loopback TCP; peer access is HTTPS/TCP
- the canonical daemon interface is documented in
./atm-daemon/http-api.mdand ADR-033 - route-specific schemas, HTTP status codes, and the OpenAPI artifact are owned by that contract rather than restated piecemeal across product docs
- the current daemon HTTP resource surface covers:
sendackthrough the send-shaped acknowledge requestreadcleardoctor- daemon heartbeat/runtime liveness exchange
- S.5 planning adds
atm listas a distinct CLI surface; S.7 owns the implementation line that refines the current queue-query packet mapping instead of assuming the old multi-messagereadresponse shape is still the final product contract - retained
log,teams, andmembersremain outside the daemon request/response packet family in the current Phase S line - Phase S planning is tracked in
plan-phase-S.md - durable ATM state is one host-scoped SQLite database at
~/.atm/db/mail.db; the daemon is the only writer, while direct read-only SQLite consumers remain an allowed system integration path - the thin-client extension surface should center on
sendandreceiveover the shared ATM protocol, while the retained CLI may continue to exposeackas a user-facing workflow - Phase U removed the active
metadata.atmnamespace from the approved compatibility schema; the authoritative schema isatm-message-schema.mdand the planning record isplan-phase-U.md
Product requirement ID:
REQ-P-SCOPE-001The rewrite retains the documented command surface and migrates ATM mail/runtime ownership from filesystem JSON plus mailbox locks to SQLite plus a singleton daemon without intentionally removing retained functionality.
Satisfied by:
-
intentionally undecomposed product requirement; this governs overall rewrite scope and is enforced across the workspace rather than by one crate-local ID
-
REQ-P-RUNTIME-001Production ATM commands must connect to the daemon and auto-start it when absent. -
REQ-P-RUNTIME-002Daemon singleton is ATM daemon requirement#1: exactly oneatm-daemonprocess may exist per OS account per host. (Rand, 2026-09-08.) A container is its own host for this requirement. (Rand, 2026-09-08.) Daemons serving separate OS accounts on one physical machine are not a violation. (Rand, 2026-09-08.) No code path may intentionally or accidentally allow a second daemon for the same OS account to reach serving state. -
REQ-P-RUNTIME-003Daemon singleton enforcement per OS account per host must use multiple guard layers. (Rand, 2026-09-08.)- a pre-spawn launch gate that serializes daemon creation attempts
- a daemon-side startup gate that refuses serving state when ownership is already held
- a static lint/CI gate that rejects test-only or ad hoc daemon launch patterns No test, tool, or alternate CLI path is exempt from these guards.
Required behavior across
REQ-P-RUNTIME-001throughREQ-P-RUNTIME-003:- the production CLI/runtime path first attempts to connect to an already-running daemon
- if the daemon is not running, the production CLI/runtime path auto-starts it and retries once
- if daemon auto-start still fails, ATM must fail clearly with recovery guidance
- no production path may silently bypass the daemon by talking directly to SQLite or inbox files
- every daemon launch path is subordinate to
REQ-P-RUNTIME-002andREQ-P-RUNTIME-003
-
REQ-P-RUNTIME-004The supported same-host topology is one OS-user-scoped daemon, one OS-user-scoped SQLite database, and one OS-user-scoped retained log root serving multiple ATM workspaces with differentATM_HOMEvalues.Required behavior:
- distinct workspaces on the same host may carry different
ATM_HOMEvalues while still sharing:- one daemon singleton
- one durable SQLite root
- one retained observability root
- concurrent same-host
send,read, andacktraffic from different workspaces must not leak mailbox, roster, or retained-log state across team/workspace boundaries - release evidence for this topology must include at least one shared-host
smoke lane that proves two or more workspaces share one daemon/database/log
root while concurrent
send/read/acktraffic succeeds - any release claim that ATM is production-ready for
10+same-host workspaces must cite explicit accepted evidence for that10+topology in the readiness record; absent that evidence, the release must not claim the broader10+same-host scale target
- distinct workspaces on the same host may carry different
-
REQ-P-RUNTIME-005Same-host daemon timeout handling must preserve an explicit retry-safety contract for side-effecting commands.Required behavior:
- the daemon may continue running accepted request work after a caller-side local IPC deadline expires
- read-only commands may continue to use retryable same-host timeout failures
- side-effecting commands that exceed the caller-visible deadline after dispatch begins must not return a generic retry-safe timeout surface
- same-host side-effecting timeout failures must return a distinct machine-readable error code that means "the command may have executed"
- recovery guidance for that distinct timeout must tell callers to inspect mailbox or service-side effects before retrying
-
REQ-P-RUNTIME-006Every standardatm-daemonlaunch path—including the shared CLI/graft auto-start path, LaunchAgent/launchd plists, and equivalent OS-native service launchers—must sanitize its inherited environment beforeatm-daemonstarts. It must stripATM_TEAM,ATM_IDENTITY, andATM_ENVIRONMENTbefore exec (for example,/usr/bin/env -uon supported Unix launchers or the platform equivalent), so the daemon never receives ambient caller-identity variables at process start.Required behavior:
- every standard launch entry point applies the same three removals before daemon exec; the CLI/graft auto-start path is not exempt
- sanitation is pre-exec defense in depth, not a daemon runtime self-check
and not a replacement for
REQ-CORE-CONFIG-001's prohibition on daemon ambient identity/team fallback - daemon production code must not read, default from, or report these three values as caller context; resolved caller identity/team arrive only in typed request data from the invoking CLI/graft process
-
REQ-P-DAEMON-PARTITION-001Phase R daemon cleanup work must use one explicit daemon-private partition map so ownership, review scope, and later lint enforcement do not depend on ad hoc file boundaries. -
REQ-P-DAEMON-LIFECYCLE-001Daemon lifecycle and singleton teardown rules must define a positive safe-order contract:- keep stable host-wide lock file paths for
launch.lockandowner.lockon every supported operating system - clear or invalidate owner-visible ownership metadata while the live advisory lock is still held
- release the live advisory lock only after the owner metadata is no longer published as current
- if cleanup cannot complete safely, fail closed rather than publishing an ambiguous ownership state
- keep stable host-wide lock file paths for
-
REQ-P-DAEMON-SWITCH-001Thedaemon-switchcontrol plane must provide a typed, temporary managed-service launch overlay for the selected matched ATM CLI/daemon pair.Required behavior:
- it may select only
mutual-tlsorplaintext-testpeer-wire security; raw daemon arguments, environment selection, durable plaintext settings, alternate endpoints/roots, and generic service-editing interfaces are forbidden - before any service/configuration mutation it must validate the explicitly selected CLI/daemon pair and named managed service, capture the accepted original launch specification, and atomically persist owner-only recovery state with service/pair identity, requested mode, original and overlay digests, and transaction phase
- it must follow the bounded transaction
capture -> stop proof -> typed overlay -> paired start/doctor proof -> exact restore -> paired normal start/doctor proof; a crash or failure must preserve enough state for an explicit recovery command rather than guessing or silently repairing - an incomplete overlay session blocks normal pair switching/restart and a second overlay session until explicit recovery validates and restores the captured original configuration; ambiguous, missing, changed, or unsupported service configuration fails closed before an unsafe mutation
- adapters must preserve platform-native ownership: an owned macOS
LaunchAgent plist copy or an owned systemd
--userdrop-in. They must not rewrite an unknown source configuration or start a direct child daemon as fallback. Windows has no temporary-launch adapter: the managed daemon is a per-user scheduled task andtemporary-launchfails closed (ADR-053 amendment 2026-09-05) - after successful restoration, the same selected pair starts through the ordinary managed service without the temporary argument and doctor proves normal mTLS/default state; selected-pair and applicable signing gates remain mandatory for every lifecycle-changing operation
- it may select only
-
REQ-P-DAEMON-SWITCH-002daemon-switchswitches one matched CLI/daemon pair in exactly three ordinary situations:switch --release <X.Y.Z|latest>selects the platform's stable published release without caller-supplied binary paths;switch --prerelease <X.Y.Z|latest>selects a GitHub prerelease Release, checksum-verifies and stages its manifest- declared host archive, applies the platform's required signing, and leaves the Homebrew formula unchanged; andswitch --worktree <path>selects a dogfooding worktree build. Both release selectors resolve through the GitHub Releases API and fail explicitly when offline. A worktree switch requires an exactprerelease/vX.Y.Ztag on its HEAD, whereX.Y.Zequals the workspace and both release binaries;--bumpmay run the patch++ prerelease-tag helper and release build first. The tag prevents speculative code from reporting a released version. Each mode verifies the pair, switches selectors, restarts the one managed daemon, and ends withstatus --doctoron macOS, Linux, and Windows. Raw--cli/--daemonpaths remain only for fixtures and Colima; a pair outside a platform release root that reports a published version is refused unless the caller explicitly opts out. All other daemon-switch ceremony is deferred to a later plan (Rand, 2026-09-05 and 2026-09-09; ADR-053 amendments; issue #1350).- evidence may expose redacted service metadata, pair identity, mode, digests, phase durations, and recovery outcome, but never private keys, certificate contents, raw trust records, or primary-database state
- implementation is outside
atm-http-runtime, the canonical HTTP/router/ persistence path, direct-peer connector, and benchmark timed profile; it must launch only the Tokio/Axum daemon target and must not modify the frozen synchronous legacy daemon
This requirement is governed by ADR-053 and composes with ADR-026, ADR-047, ADR-052,
REQ-P-BENCHMARK-001, andREQ-CORE-TRANSPORT-002B1.Herdr entry-management addendum (AY.5): the same operator control plane additionally exposes exactly
herdr-entry install,remove, andstatus [--repair]. This is an explicit, independent transaction; ordinaryswitch,restart,restore, and daemon startup never invoke it. The command reads Herdr configuration and its ordered endpoint list only from nativeatm doctor --json:configured: trueis required for install,falseis a safe refusal, and null, missing, malformed, or nonzero doctor output isHERDR_DOCTOR_UNREADABLE(exit 4), never a Python fallback.Herdr restart-coordination addendum (AY.6):
restart --restart-herdr [<default-or-session>]is a distinct, explicit operator action. It selects exactly one endpoint from native doctor data and the AY.5 owned-entry projection; an omitted selector is valid only for one configured endpoint. Socket-path, foreign/missing, or journal-active entries fail closed. When the doctor reports that the installed client is newer than the running server andcapabilities.live_handoffis exactly true, it invokes the selected scopedherdr server live-handoff; otherwise it emits a pane-loss warning and requires--stop-herdr-panesbefore scopedserver stopand entry-owned relaunch. It never invokesherdr update, starts or restarts ATM, takes ownership of Herdr, or runs implicitly during switch, restore, or ordinary restart. Every command and the whole operation are deadline-bound; a fresh native doctor read must report the selected endpointokbefore success. The single JSON envelope uses exit 0/3/4. Exact refusals areHERDR_NOT_CONFIGURED,HERDR_DOCTOR_UNREADABLE,HERDR_RESTART_ENDPOINT_REQUIRED,HERDR_RESTART_ENDPOINT_UNKNOWN,HERDR_RESTART_SOCKET_PATH,HERDR_RESTART_PANES_ACK_REQUIRED,HERDR_RESTART_NO_LIVE_HANDOFF,HERDR_RESTART_TIMEOUT, andHERDR_RESTART_VERIFY_TIMEOUT; an Herdr/entry failure isHERDR_RESTART_HERDR_FAILED. Before an ordinary ATM restart mutates its managed service, daemon-switch re-reads doctor and refuses allclient_server_mismatchendpoints withHERDR_RESTART_ENDPOINTS_PENDING. Default and named sessions receive deterministic per-user native entry identifiers; an endpoint configured with explicit socket-path provenance is externally owned and is refused. Every owned object carriesmanaged-by=atm daemon-switchand a canonical-render digest.Install journals
planned -> written -> registered -> verifieddurably before each mutation, atomically writes the owned object, registers it with the native per-user manager, verifies marker/digest/registration, then completes the journal. Remove verifies ownership first and unregisters then deletes only a marker-bearing, digest-matching object. An incomplete journal blocks install/remove;status --repaireither completes verified registration or unregisters and removes the marker-bearing partial object. Foreign collisions, digest mismatch, Windows account/session mismatch, and ambiguity fail closed without overwrite or deletion. Every result is exactly one stdout JSON object withok,code,message,remedy, andentries; success exits 0, safe refusals exit 3, and operational failures exit 4. -
REQ-P-DAEMON-DISPATCHER-001Request work accepted by the daemon must remain tracked by runtime-owned drain accounting until it finishes or is cancelled. Detached untracked request execution is forbidden even when the transport remains single-request-per-connection. -
REQ-P-DAEMON-LANES-001Background daemon lanes must use rollback-safe startup and shutdown sequencing:- partial start failure must stop every lane already started
- shutdown must attempt every lane cleanup path before final ownership release
- partial lane failure must not leave the runtime in ambiguous ownership state
-
REQ-P-PLATFORM-001ATM1.0supports macOS, Linux, and Windows as first-class operating systems for the retained product surface. -
REQ-P-PLATFORM-002Feature parity across supported operating systems is a release requirement, not a best-effort goal.Required behavior:
- every retained ATM feature required for
1.0must work on every supported operating system - daemon functionality must not be considered "supported" on an operating
system when the result is compile-only support,
daemon_unavailablestubs, or documentation that instructs users to switch operating systems - implementation differences by operating system are allowed only behind documented product and crate-local boundaries and must preserve the same observable product behavior, error semantics, and test obligations
- a feature that lacks one supported-operating-system implementation is incomplete and must not be documented as production-ready
- peer authority canonicalization is platform-identical. DNS/DDNS is the
portable baseline; a
.localauthority is available on any supported OS only when that OS has an mDNS resolver. When mDNS is unavailable (including a Windows installation without the facility enabled), resolution fails closed with actionable recovery to use DNS/DDNS or enable mDNS; it must not fall back to a durable or inferred IP alias
- every retained ATM feature required for
-
REQ-P-PLATFORM-003The published release archive set is manifest-declared (release/publish-artifacts.toml).Required behavior:
- the archive set MUST include x86_64 and aarch64 Linux (gnu), x86_64 and aarch64 macOS, and x86_64 Windows
- every target is built natively on a matching runner (no cross-linking)
- every target is carried by the same packaging path and produces checksummed archives (sha256) alongside every other target
- adding a target is a manifest change plus docs, never a workflow fork
- one binary:
atm - one primary library:
atm-core - SQLite-backed ATM mail source of truth
- SQLite-backed team roster source of truth
- singleton daemon runtime
- Phase AI target daemon API: Unix HTTP over local UDS and loopback TCP, Windows HTTP over loopback TCP, and HTTPS over TCP for remote peers. AI.1 intentionally retains the pre-migration local IPC baseline; the local HTTP target becomes live in AI.6 and the remote HTTPS target in AI.9.
- Claude-compatible JSONL inbox ingress and export
- configuration resolution
- caller identity resolution through explicit CLI override or invoking-shell
ATM_IDENTITY - file-reference policy handling for
send --file - origin-inbox merge / ingest compatibility for Claude-owned inbox files
- ATM-owned read/ack/clear/task state in SQLite
- daemon and per-team escalation-recipient state in SQLite
- lead and escalation-recipient notifications for repeated or blocked tasks
- structured logging through
sc-observability - log query and follow through
sc-observability - local diagnostics through
atm doctor - the Phase
AXadditive CLI featureatm escalation - local team discovery and recovery through
atm teams - local roster verification through
atm members - native agent/plugin notification interface
- the retained local team recovery surface:
atm teamsatm membersatm teams add-memberatm teams backupatm teams restore
- task metadata carried in the mail envelope
- JSON output mode
- human-readable output mode
- long-lived durable remote-delivery queueing
- broad plugin host framework beyond the ATM agent notification/runtime interface
- CI monitoring
- TUI and MCP features
- routine daemon process spawning as a correctness test strategy
- a test-only daemon launch path
- manual daemon-start discipline as a product requirement
- production CLI auto-start when the daemon is absent is in scope under
REQ-P-RUNTIME-001
- production CLI auto-start when the daemon is absent is in scope under
atm statusin the initial rewrite- separate
atm tailcommand in the initial rewrite - team lifecycle management outside the retained local recovery surface
(
atm teams,atm members,atm teams add-member,atm teams backup,atm teams restore)
Product requirement ID:
-
REQ-P-RELEASE-001The1.0retained-surface release must replace the previously publishedagent-team-mailCLI/core distribution channels from this repo without requiring downstream users to adopt new crate identities. -
REQ-P-RELEASE-002Channel parity for the replacement release is limited to the historical release channels that actually existed for the old repo: crates.io, GitHub Releases, and Homebrew. -
REQ-P-RELEASE-003Crate/package identity continuity must be preserved by publishing the retained CLI/core replacement under the legacy package namesagent-team-mailandagent-team-mail-corewhile keeping the installed CLI binary nameatm. -
REQ-P-RELEASE-004Superseded in part by ADR-050. ATM owns its release manifest, declared artifacts and destinations, and release evidence. The shared release-process implementation — workflows, actions, helpers, publisher/channel-agent prompts, bootstrap, and tests — is owned only bysc-publishand installed into ATM byte-for-byte through its canonical installer. ATM must repair a shared defect upstream rather than locally modifying a synchronized shared file. -
REQ-P-RELEASE-005Windows installation must be first-class for1.0without requiring Rust tooling or manual archive extraction;wingetis therefore a required additional release channel even though it was not part of the historicalagent-team-mailrelease system. -
REQ-P-RELEASE-006Release prerequisites that depend on account-level distribution infrastructure must be made explicit in the repo-owned release plan before1.0release automation is considered complete. -
REQ-P-RELEASE-007ATM release identifiers must be strict SemVer. The project supports opt-in prerelease builds such as1.3.2-beta.1and1.3.2-alpha.1; prereleases are never the default customer channel. -
REQ-P-RELEASE-008Once immutable releases are enabled, every future ATM GitHub release must satisfy the fail-closed integrity contract in the phase-bc plan: complete draft-first assembly; exact equality among the gated source, build checkout, tag, and release receipt; asset digest agreement withchecksums.txt; immutable tag/assets; verify-only same-tag retries; and production execution from the trusted workflow ref and qualifiedsc-publishrevision. An indeterminate setting, permission, source, digest, or receipt result must stop before the next tag, registry, or channel mutation. Post-publication receipt and attestation verification gate downstream promotion, not initial release publication.
Required behavior:
- the
1.0release must publish the retained CLI and core crates under the legacy crates.io package names:agent-team-mailagent-team-mail-core
- the
atmbinary name remains the installed CLI entrypoint - the release channels that were already part of the historical
agent-team-mailrelease system and must be replaced from this repo are:- crates.io
- GitHub Releases
- Homebrew
wingetis not a historical release channel foragent-team-mail, but it is a required new1.0release channel so normal Windows users can install ATM without Rust tooling or manual zip handling- Homebrew release automation depends on the existing
randlee/homebrew-taptap and requiresHOMEBREW_TAP_TOKENto be configured inatm-coreGitHub secrets before the release workflow can update formulas from this repo wingetrelease automation uses therandleenamespace with package IDrandlee.agent-team-mail- the first
wingetrelease requires a one-time manual manifest submission tomicrosoft/winget-pkgs; after that initial submission, later releases may be automated from this repo - automated
wingetrelease wiring requires a dedicatedWINGET_GITHUB_TOKENrepo secret WINGET_GITHUB_TOKENmust be a PAT with permission to create branches / PRs against therandlee/winget-pkgsfork used by the release workflow- release readiness proof for
wingetmust validate successful submission or manifest update dispatch; it cannot require same-day installability because Microsoft review introduces a normal 1-2 day publication lag - the normal Homebrew
atmformula tracks stable releases only; prereleases are published, when approved, through an explicit opt-inatm-betaformula in the project-owned tap - immutable-release enablement occurs only after every reachable stable, manual, prerelease, and channel writer is compatible and the runtime has an approved least-privilege credential that can read the repository setting
- pre-enablement releases remain historical mutable records and must not be rewritten to simulate the future invariant
Product requirement ID:
REQ-P-HTTP-COMPAT-001The daemon HTTP API has a strict, independently declared SemVer version. Product release versions identify builds only; they are never the CLI-to-daemon compatibility gate.
Required behavior:
/v{major}/atmand the HTTP API's declared SemVer have the same major; different major versions fail before a write with a typed compatibility error- the compatibility preflight compares the explicit CLI/daemon schema version
and HTTP API major, not
atmoratm-daemonproduct release strings - an additive endpoint, optional JSON field, response field, or error detail increments the HTTP API minor version and must preserve successful communication for clients and servers sharing the same major; patch versions are corrective only and do not add or remove contract elements
- requests accept omitted additive fields with documented defaults and servers ignore unknown additive fields; an operation requiring a new capability must declare that requirement rather than relying on a minor-version mismatch
- OpenAPI, generated clients, and compatibility tests are the authoritative proof of this contract
Product requirement ID:
REQ-P-CONTRACT-001External path/config/store/observability contracts must match the documented retained ATM behavior for the active architecture line.
Satisfied by:
REQ-CORE-CONFIG-001for home/path/config resolution aspectsREQ-CORE-RUNTIME-001for durable mail/roster store ownership aspectsREQ-CORE-INGEST-001for config ingest and historical Claude inbox ingest compatibility aspectsREQ-CORE-MAILBOX-001for persisted mailbox atomicity plus historical Claude inbox write/read compatibility aspectsREQ-ATM-OBS-001for CLI observability bootstrap/integration aspectsREQ-CORE-OBS-001for ATM observability boundary/query-model aspects
Workspace/config resolution order:
ATM_HOMEwhen set and non-empty- OS home directory
Runtime-root rule:
- under planned ADR-026, neither the invocation directory nor
ATM_HOMEis a selector for daemon socket, lock, or SQLite durable-state paths; those derive from one OS-userHostRuntimeScope - retained logs follow their host-scoped root under ADR-011 (or explicit
ATM_LOG_DIR), independently of workspaceATM_HOME - the invocation directory and
ATM_HOMEremain workspace/config discovery inputs only
Required workspace/config paths:
{ATM_HOME}/.claude{ATM_HOME}/.claude/teams{ATM_HOME}/.claude/teams/{team}{ATM_HOME}/.claude/teams/{team}/config.json{ATM_HOME}/.claude/teams/{team}/inboxes/{agent}.json{ATM_HOME}/.config/atm/config.toml{ATM_HOME}/.config/atm/state.json{ATM_HOME}/.config/atm/share/{team}/
Required host-runtime paths:
- the canonical endpoint,
launch.lock, andowner.lockderive only fromHostRuntimeScope.runtime_root - the one SQLite durable database derives only from
HostRuntimeScope.durable_state_root - the retained log file derives only from the ADR-011 host log root unless
ATM_LOG_DIRexplicitly overrides it
Product requirement IDs:
REQ-SEC-001All user-supplied team and agent name segments must be validated before path construction.REQ-SEC-002JSON number normalization must not allocate unbounded memory.REQ-DURABILITY-001Atomic-write temp file names must be globally unique within a process.
Required behavior:
- valid team/agent path-segment characters are limited to:
- alphanumeric
- hyphen
- underscore
- period
- team/agent segments must reject:
- empty strings
- path separators
..sequences- consecutive periods
- leading periods
- platform-specific path escapes that could break out of the intended ATM home subtree
- validation must happen before any path construction in address parsing or home/path helpers
Product requirement ID:
REQ-CORE-IDENTITY-CHAT-001ATM must support an optional chat-id as an independent component of a sender or recipient identity.
Required behavior:
- canonical address grammar is
<agent>[:<chat-id>]@<team>[.<host>] agent[:<chat-id>]is the canonical agent-identity grammar used wherever a full team/host address is not required; thereforeagent:XXXmeans agentagentwith chat-idXXX- CLI address composition is equivalent to canonical text: base agent plus
--team <team>yields logicalagent@team; adding--chat-id XXXyields logicalagent:XXX@team, before a single normalization to the structured address - for
atm send,--host <host>qualifies the resolved recipient asagent@team.host; it is equivalent to spelling that host in the recipient address, passes through ADR-040 trusted-host canonicalization, and supplying both forms with different canonical hosts fails before daemon dispatch - caller chat-id resolution is ordered: qualified
--as <agent>:<chat-id>, then--chat-id, thenATM_CHAT_ID, then a chat-id embedded inATM_IDENTITY=<agent>:<chat-id>, then no chat-id. An explicit unqualified--as <agent>is a complete caller override and selects no chat-id. agent,team, andchat-iduse the safe segment alphabet already required above; only the address parser interprets:and.delimiters- storage keeps nullable source and destination chat-id columns rather than concatenating a chat-id into an agent-name column
- reads render a present source chat-id in
fromasagent:chat-id; writes, nudges, replies, and acknowledgements preserve the full destination address agentwithout a chat-id,agent:chat-a, andagent:chat-bare distinct identities for inbox visibility and owner-only mutationsatm read --agent <agent>searches that agent across all chat IDs;atm read --agent <agent> --chat <chat-id>narrows to one chat identity- chat-id is not a daemon session, transport-session, or message-thread field
- JSON number normalization must cap exponent-driven string expansion at 64 characters
- if exponent expansion would exceed 64 characters, ATM must:
- return the original raw numeric string unchanged
- emit a structured warning using
AtmErrorCode::WarningMalformedAtmFieldIgnored
- atomic persistence helpers must use temp-file names that are unique for each write attempt targeting the same destination path from the same process
- timestamp-only temp-file suffixes are not sufficient for the durability contract because rapid same-process writes can collide
Per-team layout:
{ATM_HOME}/.claude/teams/{team}/config.json{ATM_HOME}/.claude/teams/{team}/inboxes/{agent}.json- optional origin inbox files:
{ATM_HOME}/.claude/teams/{team}/inboxes/{agent}.{origin}.json
The rewrite retains origin-file merge behavior for read and wait paths because it is part of the current file-based mail surface and does not require the daemon.
Historical shared inbox file-container rule:
- the prior Claude inbox container at
{ATM_HOME}/.claude/teams/{team}/inboxes/{agent}.jsonused one top-level JSON array of inbox messages - that
.jsonarray shape is historical-only afterADR-019; it is not a live production compatibility path for accepted send/read behavior - any retained repair/rebuild or salvage handling for malformed Claude inbox JSON exists only for historical compatibility tooling and must not redefine current runtime requirements
Product requirement ID:
REQ-P-SCHEMA-001ATM must preserve explicit ownership boundaries between Claude Code-native message schema, legacy ATM compatibility schema, and forward ATM metadata schema.
Satisfied by:
REQ-CORE-MAILBOX-001for persisted inbox read/write compatibilityREQ-CORE-WORKFLOW-001for ATM workflow semantics layered onto compatible message representations
Required rules:
-
Claude Code-native message schema is owned by Claude Code
-
ATM must not redefine Claude-native fields as if ATM owned them
-
ATM read must accept:
- Claude Code-native messages
- legal ATM additive derivative messages, including historical top-level
additive compatibility records and tolerated
metadata.atmderivatives
-
no normal ATM runtime/query path may depend on ATM-owned machine-state reads from Claude JSON
-
no
metadata.atmnamespace may survive in active compatibility output -
retained
message_idis the ULID text form of the one logical ATM message identity -
ATM-owned workflow, delete/close, expiry, sender-projection, and repair state must live in SQLite-owned state, not in shared JSON
-
write-path validation may reject wrong-format ATM-owned compatibility fields with descriptive errors
-
read-path validation failure for additive ATM fields must trigger warning + degradation logic rather than failing the overall message read
-
a separate ATM-native inbox remains deferred; on the earlier compatibility line, the shared inbox remained compatibility-only
REQ-P-SCHEMA-001is owned by: -
legacy-atm-message-schema.md(historical only; itsmetadata.atmcoverage was superseded and removed from the active compatibility design in Phase U) -
atm-core/design/dedup-metadata-schema.md§2.2 and §3.3 for forward ATM alert-field placement and sender-side dedup semantics
Product requirement ID:
REQ-P-FILEIO-001Every live file operation must declare file ownership, mutation class, and the single commit path used for persistence.
Required rules:
- every live file path must be classified as one of:
- Claude-owned
- ATM-owned
- shared/de-facto interoperable
- ownership determines whether ATM is allowed to treat the file as writable source-of-truth state
- ATM-owned machine state must have one documented write path per file family
- ad hoc write logic at leaf call sites is prohibited for live shared state
Operation classes:
read_only- no lock acquisition
- no temp-file write
- no persistence side effect
read_possible_write- initial unlocked read is allowed
- if the read determines no change is needed, return without locking
- if the read determines a change is needed, the operation must enter the shared write-commit path before persisting anything
read_modify_write- mutation is expected
- persistence must still flow through the shared write-commit path
Shared write-commit path requirements:
- the mutation plan must be computed from a concrete input snapshot
- before replacing the live file, ATM must prove source freshness by either:
- compare-and-swap against the exact snapshot identity/content that was read, or
- lock, reread current state, recompute the mutation from the fresh state, then commit
read -> mutate -> lock -> blind renameis not a valid write path- every successful commit of shared mutable structured state must use the documented atomic replacement helper family
Source-of-truth guardrails:
- ATM must not rely on full-file rewrite of Claude-owned files as the long-term source of truth for ATM-local workflow state
- if ATM-local semantics need durability independent of compatibility exports, that state must live in the ATM-owned SQLite store
- when a legacy compatibility path still rewrites a non-ATM-owned shared file, the requirements and architecture docs must call out the limitation
Configuration resolution order:
- CLI flags
- environment variables
- repo-local
.atm.toml - global
{ATM_HOME}/.config/atm/config.toml - defaults
Required config fields:
- default team for config/bootstrap flows that explicitly consume ATM config defaults; it is not a runtime caller-team fallback for commands governed by the caller-context matrix
Supported optional config fields:
[atm].team_members[[atm.post_send_hooks]][atm].aliasesis retired (below, andREQ-ROSTER-NAME-007/008): atm ignores it except for the doctor pane-alias diagnostic
Runtime caller-context rules:
- repo-local
.atm.toml[atm].identityand the legacy top-levelidentitykey are not valid runtime identity fallback for the retained multi-agent ATM model - repo-local
.atm.toml[atm].default_teamis not a valid runtime caller team fallback for commands that require caller context - the authoritative command-by-command caller-context matrix is
docs/requirements.md§4.1 - runtime identity must come from:
- explicit command override when supported
ATM_IDENTITY
- runtime caller chat-id must come from the caller-context precedence in
docs/requirements.md§4.1;ATM_CHAT_IDis the environment-level source and a qualifiedATM_IDENTITYis its lower-precedence fallback - runtime caller team for commands that require it must come from:
- explicit command override when supported
ATM_TEAM
- caller-owned CLI commands must resolve identity before daemon dispatch; if no valid identity exists, the CLI must fail locally and must not contact the daemon
- caller-owned CLI commands must resolve required caller team before daemon dispatch; if no valid required caller team exists, the CLI must fail locally and must not contact the daemon
- daemon-backed caller-owned request DTOs must carry resolved caller identity as required request data
- daemon-backed caller-owned request DTOs must carry resolved caller team as required request data when the command requires caller team
- the daemon must not consult hook files, repo-local config, roster state, or
daemon ambient
ATM_IDENTITY/ATM_TEAMto fill missing caller context - obsolete config identity fields (
[atm].identityand legacy top-levelidentity) may remain temporarily for migration, but ATM must ignore them for runtime identity resolution andatm doctormust flag them for removal .atm.tomlmay define[atm].team_membersas the baseline team roster that should always be present inconfig.json.atm.toml[atm].aliasesis retired. Rand (2026-09-07): ".atm.toml is ONLY used by hmux and 'atm doctor' to display a warning if alias is not consistent. NOTHING else in atm uses .atm.toml alias." Member aliases live only in the roster (REQ-ROSTER-NAME-002); atm-core reads no alias table from.atm.toml; the only.atm.tomlalias use inatmis the doctor pane-alias consistency warning (REQ-ROSTER-NAME-008)..atm.tomlmay define one or more[[atm.post_send_hooks]]rules for best-effort recipient-scoped post-send automation- retired
[atm].post_send_hook,[atm].post_send_hook_senders,[atm].post_send_hook_recipients, and[atm].post_send_hook_memberskeys must be rejected with migration guidance directing operators to[[atm.post_send_hooks]] - config sections outside ATM-owned config, such as
[rmux]or future[scmux], are not ATM runtime config and must be ignored byatm-core, with one diagnostic-only exception:atm doctormay read[[rmux.windows.panes]].aliasfrom the.atm.tomldiscovered from its caller cwd and compare it with the durable alias of the named pane member for that caller'sATM_TEAM - that doctor comparison is validation only: it never supplies an alias to
member creation, identity or recipient resolution, sends, or writes; it
does not widen under
--all-teams, and a missing or unparsable.atm.tomlskips the comparison without failing doctor
Product requirement ID:
REQ-P-CONFIG-HEALTH-001Persisted ATM config and team JSON loading must recover at the narrowest safe scope and report precise diagnostics when recovery is not safe.
Satisfied by:
REQ-CORE-CONFIG-003for config/team schema recovery and diagnostic policyREQ-CORE-SEND-001for send-time missing-config fallback and repair notification policyREQ-CORE-MAILBOX-001for mailbox record skip behavior
Required persisted-data classes:
compatibility-recoverablerecord-invaliddocument-invalidmissing-document
Required handling policy:
- compatibility-only schema drift may be recovered with documented, deterministic defaults
- malformed records inside a larger persisted collection should be skipped or quarantined individually when the rest of the document remains trustworthy
- malformed current-Claude root arrays must salvage segmentable valid message objects and emit explicit degraded warnings whenever localized recovery is possible
- malformed root documents or invalid root structure with no segmentable valid message units must fail with structured errors rather than guessed repairs
- missing persisted team config is a distinct
missing-documentcondition and must not be collapsed into generic parse corruption - identity and routing semantics must never be fabricated to keep a command running
Required diagnostics:
- failure class when known
- file path
- entity scope when known, such as member name or collection entry
- field name when known
- parser detail, including line and column when available
- recovery guidance when operator action is required
Operator examples and safe repair guidance live in
persisted-data-repair.md.
Rand (2026-09-07), the whole section in one line: "This really is a simple
UX abstraction. Both names work user facing, everything under the hood
used member-name except herdr which uses unique-name." Every requirement
below is that sentence spelled out: the alias is a user-facing token,
substituted for the member name at the CLI edge; storage, wire, mailbox,
audit and routing see only the member name; Herdr alone sees
unique_name = alias ?? member name, which must be unique across the
database.
Product requirement IDs: REQ-ROSTER-NAME-001 through REQ-ROSTER-NAME-010.
Source rulings (Rand, 2026-09-07, verbatim): "the requirement comes from herdr agent name MUST be unique which means herdr agent name must be unique on atm database."; "herdr agent name = alias. if alias is null/empty, alias would be equal to member name"; "this allows us to have the same name on different teams (we should guarantee uniqueness of names per team already) by simply adding an alias for the conflicting name."; "we call this 'alias' because using the alias for cross-team messaging has value independent of herdr."; "so basically, there should be a query across all team roster for 'unique-name' which would return alias ?? name."; "if the list of unique-name collides with a proposed alias ?? name, add member must fail"; "the alias should never be used in database." (scoped later the same day by Rand: "alias MUST be in database AND in immutable roster in RAM"; the earlier sentence applies to message, ack, audit and task-state rows only, see REQ-ROSTER-NAME-009); "alias would be acceptable at all user/agent facing interfaces and would immediately be replaced at the ingress interface."
Definitions:
-
canonical member name: the roster
agent_name; the only name stored in any non-roster row -
alias: the optional roster attribute
metadata_json["alias"]; an empty or whitespace-only alias is the same as no alias -
effective name (the Herdr agent name): the alias when present, otherwise the canonical member name;
effective(member) = alias ?? agent_name -
REQ-ROSTER-NAME-001Canonical member names are unique within a team.add-memberrejects a canonical name already present in the same team. -
REQ-ROSTER-NAME-002Effective names are unique across the whole ATM database: for any two distinct roster rows in any teams,effective(a) != effective(b). The check covers every combination: canonical vs canonical, canonical vs alias, alias vs canonical, alias vs alias. It applies to members of every backend, not only Herdr members, because the backend may change later. -
REQ-ROSTER-NAME-003The same canonical name may exist in several teams when the effective names differ. The first member of a name in the database needs no alias; a later member of that name in another team must carry an alias, or an alias must already be on the earlier member. The rejection error names the conflicting team and member and states the--aliasremedy. -
REQ-ROSTER-NAME-004Uniqueness is enforced inside the roster store's write transaction on every write path (add-member,set-memberincluding alias change and alias clear, restore/import, daemon and HTTP member mutation), not only in the CLI. Two concurrent writers of the same effective name: exactly one succeeds. Removing a member or deleting a team frees its effective name. -
REQ-ROSTER-NAME-005Grammar: canonical names and aliases follow the ATM segment rules (ASCII letters, digits,-,_; non-empty). A member whose backend is Herdr additionally requires its effective name to satisfy Herdr's live-agent grammar[a-z][a-z0-9_-]{0,31}; this is validated when the member is added, when its alias changes, and when its backend becomes Herdr. Comparison is exact (case-sensitive); ATM does not fold case. -
REQ-ROSTER-NAME-006Every Herdr call for a member (prompt, get, wait, list matching, presence probe) targets the effective name, never the canonical name directly when an alias exists. Logs and doctor output show both (member = team/agent,herdr_agent = <effective>).- Socket transport and doctor endpoint reporting address roster session
defaultthrough Herdr's default server socket; every other session name usessessions/<name>/herdr.sock, unless explicit[herdr] socket_pathtakes precedence.
- Socket transport and doctor endpoint reporting address roster session
-
REQ-ROSTER-NAME-007Ingress replacement: an alias is accepted wherever a member name is accepted (send recipient,--as,ATM_IDENTITY, read and peek filters, ack,set-member/remove-memberarguments) and is replaced by the canonical name at daemon ingress (REQ-ROSTER-NAME-010), before validation, self-send checks, mailbox lookup, routing, audit and persistence. Resolution order: a canonical name in the addressed team wins (Rand, 2026-09-07: "atm send bobwould send bob based on ATM_TEAM just like today"); then the roster alias..atm.tomlis not an input (see §3.3). Because aliases are unique database-wide, a bare alias with no@teamresolves to its member in any team (cross-team addressing by alias alone). An unknown name falls through to the existing canonical parse/lookup error unchanged. An ambient activity observation attested to an alias is dropped on replacement. -
REQ-ROSTER-NAME-008.atm.tomlpanealiaskeys are spawner input only. Rand (2026-09-07): ".atm.toml alias is ONLY read in doctor as a diagnostic message IF alias in roster != alias in .atm.toml".atm doctoremits one diagnostic line per pane whose alias differs from (or is missing in) the roster alias for the caller's team (§3.3); nothing else inatmreads a.atm.tomlalias. -
REQ-ROSTER-NAME-009Upgrade. Rand (2026-09-07): "where we will run into issues are when upgrade occurs. if non-unique names show up in database, hmux spawn will reject a new member when its add-member call reuses a colliding effective name, so that should force the team to be re-constructed before it can actually go live in herdr." No migration rewrites, renames, aliases, or deletes existing roster rows, and opening a database that already holds duplicate effective names must not fail. On the next roster write, enforcement is delta-scoped: only effective names changed or added by that write are checked against the database-wide namespace. Untouched legacy collision rows remain readable and writable, andatm doctorreports them as findings. A write that creates a new collision fails with theREQ-ROSTER-NAME-003error naming every conflicting(team, member)pair and the--aliasremedy; a write that repairs a colliding member, removes it, or changes only metadata while its effective name is unchanged succeeds. This lets operators repair legacy collisions one member at a time before a team goes live in Herdr. (2026-09-09) Delta-scoped enforcement is the retained ruling for upgrade compatibility: pre-existing collisions are doctor findings, not a blanket write lock.hmux sessionandhmux launchreuse existing roster rows and apply aliases throughupdate-member; onlyhmux spawnregisters a new member throughadd-member. -
Rand (2026-09-07) on why persistence is canonical-only: "by always using the non-alias name when writing to database, we avoid missing things on query. i.e. team-lead-alias becomes team-lead when written to database".
-
REQ-ROSTER-NAME-010Alias parity at every CLI argument. Rand (2026-09-07): "From any cli command accepting team-member name, alias must be allowed AND substituted before sending over wire. i.e. atm send team-lead-alias || atm send team-lead-alias@team or any args i.e. --as team-lead-alias, --from team-lead-alias ..."; "basically if all prompts are written for either member name or alias, it will work the same". Everyatmargument, option, or environment variable that names a team member (positional recipients,name@teamforms,--as,--from,--to, member arguments ofatm teams, nudge and doctor targets, chat-id and qualified-identity forms) accepts the alias. The substitution point is daemon ingress, not the CLI. Rand (2026-09-07): "I would probably allow the daemon to do the replacement. cli doesn't need to query for alias before sending. alias would be in immutable roster, so replacement on ingress to the daemon is the logical single point to translate." The CLI passes the token through unchanged; the daemon's request ingress (HTTP runtime, peer receive) replaces every member-name field against the in-memory roster before validation, self-send checks, mailbox lookup, routing, audit and persistence. The substitution point is the existing check, not a new one. Rand (2026-09-07): "the substitution point should be the point where team-member is checked against immutable roster in RAM already."; "It should simply change to instead of returning a bool/enum (member-valid), it would return (member-valid, member-name)". That membership check returns the canonical member name alongside validity, and every caller uses the returned canonical name from there on. A prompt or script written with aliases and the same prompt written with canonical names produce identical daemon-side handling and identical persisted rows. Cross-host: Rand (2026-09-07): "alias@team.host works". The sending daemon forwards the alias unchanged for a remote team; the receiving daemon's ingress substitutes the canonical name, so persisted rows are canonical on both hosts.
The full permutation matrix and its test mapping live in
docs/plans/phase-ay/herdr-naming-test-matrix.md.
The system must resolve Claude settings for file-reference policy checks.
Resolution order:
- explicit settings path override when provided internally
- repo-local
.claude/settings.local.json - repo-local
.claude/settings.json - global
{ATM_HOME}/.claude/settings.json
ATM depends on sc-observability as the shared logging/query/health substrate.
The shared surface ATM integrates against must support:
- structured log emission
- historical query of retained records
- follow/tail of new matching records
- filtering by severity
- filtering by structured key/value fields
- filtering by time window
- limit/order controls
- health reporting for the logging runtime
The current shared repo now exposes those generic capabilities. ATM must integrate with them directly rather than preserving a local tracing-only adapter.
Required integration rules:
- ATM must not implement a parallel ad hoc log-query engine when shared
sc-observabilityAPIs can own the behavior atm-coremust keep the shared crates behind an ATM-owned injected boundaryatmowns the concrete shared-crate bootstrap and dependency wiring- the active release baseline uses the published
sc-observability = "=1.4.1"crates.io dependency - the same pinned Rust toolchain must be used locally and in CI across ATM and
sc-*repos - the concrete integration work is planned in Phase K of
project-plan.md
Historical note:
OBS-GAP-1is complete as a historical planning artifact and does not remain the gating item for retained observability delivery
Product requirement ID:
REQ-P-IDENTITY-001Caller-context resolution must follow the documented command precedence rules.
Satisfied by:
REQ-CORE-CONFIG-001for caller-context resolution policy
Caller context means:
- caller identity when the command needs caller identity
- caller team when the command needs caller team
Global caller-context rules:
- repo-local
.atm.toml[atm].identityand legacy top-levelidentityare not valid runtime caller identity - repo-local
.atm.toml[atm].default_teamis not valid runtime caller team for commands that require explicit caller context - daemon ambient
ATM_IDENTITY/ATM_TEAMare not valid fallback sources - roster state, hook files, and target-address fields are not valid caller context sources
- if caller context is required and cannot be resolved from the documented sources, the CLI must fail locally before daemon dispatch or retained command execution
- when both an explicit CLI caller-context override and invoking-shell env are present, the explicit CLI override wins
atm doctoris diagnostic and is the explicit exception: it may run without caller identity and without caller team
| Command | Caller identity required | Caller identity may come from | Caller team required | Caller team may come from | Notes |
|---|---|---|---|---|---|
atm send |
Yes | --as, else --chat-id, else ATM_CHAT_ID, else qualified ATM_IDENTITY, else ATM_IDENTITY |
Yes | --team, else ATM_TEAM |
--as/--chat-id select caller context; target recipient/team are not caller context |
atm peek |
Yes | --as, else ATM_IDENTITY |
Yes | --team, else ATM_TEAM |
inspection-only; --from remains a sender filter |
atm read |
Yes | --as, else --chat-id, else ATM_CHAT_ID, else qualified ATM_IDENTITY, else ATM_IDENTITY |
Yes | --team, else ATM_TEAM |
owner-only mutating read path |
atm ack |
Yes | ATM_CHAT_ID, else qualified ATM_IDENTITY, else ATM_IDENTITY |
Yes | --team, else ATM_TEAM |
reply target preserves the received full source address |
atm list |
Yes | --as, else ATM_IDENTITY |
Yes | --team, else ATM_TEAM |
--from is a sender filter, not caller identity |
atm clear |
Yes | ATM_IDENTITY |
Yes | --team, else ATM_TEAM |
owner-only mutating clear path |
atm log |
Yes | ATM_IDENTITY |
Yes | ATM_TEAM |
no explicit caller override surface |
atm members |
Yes | ATM_IDENTITY |
Yes | --team, else ATM_TEAM |
--team scopes the roster being inspected and may also satisfy caller-team requirement |
atm teams |
Yes | ATM_IDENTITY |
Yes | ATM_TEAM |
no explicit override surface |
atm teams add-member |
Yes | ATM_IDENTITY |
Yes | ATM_TEAM |
positional team is the target roster team, not caller team |
atm teams update-member |
Yes | ATM_IDENTITY |
Yes | ATM_TEAM |
positional team is the target roster team, not caller team |
atm teams backup |
Yes | ATM_IDENTITY |
Yes | ATM_TEAM |
positional team is the backup target, not caller team |
atm teams restore |
Yes | ATM_IDENTITY |
Yes | ATM_TEAM |
positional team is the restore target, not caller team |
atm doctor |
No | not required | No | optional --team only |
diagnostic scope is optional; ATM_IDENTITY / ATM_TEAM visibility may be reported but are not required inputs |
atm send --as <agent>[:<chat-id>]is the explicit caller agent/chat override; its caller team still comes from--teamorATM_TEAM- with ambient
ATM_IDENTITY=<agent>,atm send <to> --chat-id <chat-id>is equivalent toatm send <to> --as <agent>:<chat-id>;--chat-idand--asare mutually exclusive and failure to resolve the ambient base agent fails before daemon dispatch - with ambient
ATM_IDENTITY=<agent>,atm read --chat-id <chat-id>is equivalent toatm read --as <agent>:<chat-id>; it resolves the owner mailbox before the existing owner-only read path, and the two flags are mutually exclusive - caller chat-id precedence is exactly:
--as(including its explicit absence of chat-id), then--chat-id, thenATM_CHAT_ID, then a chat-id embedded inATM_IDENTITY, then no chat-id.ATM_CHAT_IDmust be a validChatIdand requiresATM_IDENTITYto supply the base agent; an invalid value fails locally before daemon dispatch. - on mutating
sendand owner-onlyread,--asmust use the same base agent asATM_IDENTITY; a different base agent is an impersonation attempt and fails before daemon dispatch - a chat-qualified recipient is expressed only in canonical
<to>syntax:<agent>:<chat-id>@<team>[.<host>] --fromonsendis not an accepted caller-identity override--fromonread/listis a sender filter only--asis accepted onsendandreadfor the caller agent/chat override, and on inspection-only surfaces such aspeekandlist- any command without an explicit caller override surface must rely on the invoking shell when caller context is required
atm doctormay inspectATM_IDENTITYvisibility and team override behavior, but it must not treat hook files, repo-local config identity/team, or daemon ambient identity/team as command caller context
If command identity cannot be determined where required, the CLI must fail with
a structured recovery-oriented error before daemon dispatch. An obsolete config
identity field may be reported as a diagnostic, but it does not count as
command identity.
Product requirement ID:
REQ-P-ADDRESS-001Address resolution must support the documented local-team and CLI-only cross-host convenience forms and precedence rules.
The canonical precedence, trusted-host matching, completion exclusions, and error/recovery contract are defined by ADR-040. This requirement intentionally does not re-derive those rules; implementations must follow ADR-040.
Supported address forms:
agentagent@teamagent@team.hostagent@host(CLI same-team cross-host shorthand only)
ADR-040 defines this order: explicit agent@team.host, exact known
agent@team, same-team agent@host shorthand, then the existing bare-agent
team/default resolution. It also defines .local-only completion,
ASCII-case-insensitive host comparison, canonical-host persistence, and the
fail-closed structured recovery for unknown or ambiguous authorities. An
explicit @team suffix takes precedence over --team.
--host <host> uses the same CLI-only trusted-authority canonicalization as
an inline host. Before HTTP dispatch, either form becomes the existing
fully-qualified agent@team.canonical-host request; it adds no daemon-side
resolution path, HTTP/wire field, storage field, or graft/native-tool API.
Aliases are resolved after splitting agent@team, so only the agent token is
rewritten.
Alias rules:
- aliases are accepted as ATM-owned input shorthand only
- recipient aliases must resolve to canonical member names before validation, self-send checks, and mailbox lookup
- sender aliases may be accepted on input, but canonical sender identity remains the routing and validation identity
- same-team messages keep current canonical sender projection behavior
- the persisted
fromandtoof every message, acknowledgement, audit and task-state row carry canonical member names only, for same-team and cross-team messages alike. The alias itself is stored: Rand (2026-09-07): "alias MUST be in database AND in immutable roster in RAM". It lives in the roster row and its RAM mirror only; message, ack, audit and task-state rows carry the canonical name (Rand: "the alias should never be used in database" refers to those rows) - roster aliases (§3.3.2) are the only alias source;
.atm.toml[atm].aliasesis retired (§3.3)
Post-send-hook rules:
- ATM always has one shipped default post-send path in the installed binary: the built-in in-process delivery path
[[atm.post_send_hooks]]is the supported external override shape for post-send behavior- each rule binds exactly one
recipientselector and onecommandargv recipientmust be either one concrete team member name or*- multiple matching rules may run for a single send, in config order
- retired
[atm].post_send_hook,[atm].post_send_hook_senders,[atm].post_send_hook_recipients, and[atm].post_send_hook_memberskeys must fail with migration-oriented guidance to[[atm.post_send_hooks]] - a relative hook path must resolve from the directory containing the
discovered
.atm.toml - bare executable names such as
bash,python3, ortmuxmust use normalPATHresolution - the hook must execute with the config-root directory as its working directory
- recipient non-match is expected behavior and must be silent
- the hook inherits the process environment and also receives one ATM-owned
JSON payload in
ATM_POST_SEND - the
ATM_POST_SENDpayload must contain:fromsenderrecipientteammessage_iddescriptiontask_idas a string; it may be empty when no task is associatedrequires_ackis_ack- optional
tofor compatibility - optional
recipient_pane_idwhen ATM has an authoritative pane mapping for the recipient
- Current runtime addition:
is_ackis part of the retained hook payload contract for the daemon-owned send/ack runtime path so hook implementations can distinguishatm sendfromatm ackwithout inspecting message text - any retained built-in
atm internal-nudgehelper must not reuseATM_POST_SENDas its control contract; it consumes a separate resolvedATM_INTERNAL_NUDGEenvelope carrying the canonical event, sink target, resolved template kind, and resolved template body or explicit disabled state - the post-send hook must run after successful non-
dry-runatm send - the post-send hook must also run after successful
atm ack, using the reply message as the hook subject is_ackmust befalseforatm sendandtrueforatm ack- hook configuration lookup must use the sender's authoritative ATM roster
home_dirmetadata rather than the caller's live process working directory - if no matching external
[[atm.post_send_hooks]]rule is configured, ATM must still attempt the shipped built-in in-process post-send path - the built-in shipped nudge path must support exactly eleven named template
cases:
deliverydelivery_ackqueuequeue_ackacknowledgetask_queuedtask_readytask_remindertask_startedtask_completetask_closedNudgeKindselects the delivery or queue family; a task-linked message selects the kind named by itstask_transition.
- the default built-in acknowledge nudge shapes are intentionally compact:
<atm kind="ack" from="..." message-id="..."/>
- teams may override any subset of those eleven built-in template bodies through
host-scoped, team-keyed ATM-managed override rows resolved through the
storage-neutral
NudgeTemplateOverrideStorecontract - built-in precedence is:
- matching external
[[atm.post_send_hooks]]command - resolved team override row for the selected template kind
- built-in product default template body for that kind when no row exists
- matching external
- template lifecycle is explicit:
- no row => product default
- override row => stored non-empty template body
- disabled row => no built-in nudge emission
- clear/reset => row deletion back to product default
- empty-string template bodies are invalid and must not be used as a hidden disable signal
- example payload:
{ "from": "arch-ctm@atm-dev", "sender": "arch-ctm", "recipient": "recipient", "team": "atm-dev", "message_id": "...", "description": "review failing smoke lane", "task_id": "", "requires_ack": false, "is_ack": false, "recipient_pane_id": "%1" } - the hook may optionally emit one structured result object on stdout for ATM to parse as post-send diagnostics
- the structured hook-result object must support:
levelmessage- optional
fields
- supported hook-result levels are:
debuginfowarnerror
- missing stdout, empty stdout, oversized stdout, or invalid hook-result schema must not fail the send or convert a successful hook execution into a command error
- when a valid hook-result object is returned, ATM must log it with the declared level and preserve any structured fields
- when a hook is configured, ATM must emit enough diagnostics to explain whether the hook ran or failed, including the sender, recipient, and matched hook recipient selector
Product requirement ID:
REQ-P-SEND-001atm sendmust satisfy the documented send contract.
Satisfied by:
REQ-ATM-CMD-001for CLI entry, parsing, and dispatch aspectsREQ-ATM-OUT-001for human-readable and JSON output aspectsREQ-CORE-CONFIG-002for address resolution and target-validation aspectsREQ-CORE-SEND-001for send-time missing-config fallback and repair notification behaviorREQ-CORE-MAILBOX-001for message creation, duplicate suppression, and atomic mailbox mutation aspects
Write one message into one target inbox.
- positional target:
agent,agent@team,agent@team.host, or the ADR-040 same-team shorthandagent@host - optional positional message text
--team <name>--file <path>--stdin--summary <text>--json--dry-run--from <name>--requires-ack--task-id <id>--task-complete(requires--task-id)
Retired from the current implementation:
--offline-action- this flag exists only to cooperate with daemon-backed liveness checks and is not retained
- resolve sender identity using the defined precedence
- if sender identity cannot be resolved from
--fromor invoking-shellATM_IDENTITY, fail before daemon dispatch - resolve recipient address using the defined precedence
- resolve aliases before mailbox lookup
- when a cross-team alias-oriented sender is projected into
from, also persist the canonical sender identity in SQLite-owned state and use it for validation, self-send checks, routing, and audit behavior - reject canonical same-team self-addressed sends before any persistence or
--dry-runsuccess reporting only when the resolved destination has no host;atm send --host <host>is a second way to produce that qualified destination, and every syntactically valid host-qualified destination continues to the ordinary host-routing contract - verify target team existence and target agent membership as part of address
resolution before mailbox path selection, except for the documented
missing-documentfallback in §6.3.1 - generate summary when not explicitly provided
- enter the atomic append boundary before final inbox mutation
- validate message text inside the atomic append boundary
- generate message id for ATM-authored messages inside the atomic append boundary
- create inbox file if absent inside the atomic append boundary
- preserve duplicate-suppression behavior for message ids inside the atomic append boundary
- append atomically to the inbox file
- support dry-run without mutation
- support sender-controlled ack-required messages
- support optional task metadata on sent messages
- reject retired
post_send_hook_membersconfig with actionable migration guidance before send execution proceeds - reject retired flat post-send-hook config keys with actionable migration guidance before send execution proceeds
- run matching
[[atm.post_send_hooks]]rules only after successful non-dry-runsends - match rules only by resolved recipient identity
- support
recipient = "*"wildcard matching for all recipients - execute all matching post-send-hook rules in config order
- if no matching external rule exists, execute the built-in in-process post-send path instead of silently skipping post-send emission
- support an optional structured hook result on stdout so hook scripts can report post-send outcomes such as nudges, no-op conditions, and operator errors without relying on stderr scraping
- emit structured diagnostics for hook-rule evaluation and actionable warnings only when a configured hook execution fails
- if a configured recipient exposes post-send behavior and no emission occurs, ATM must either emit the post-send effect or surface a sender-visible warning
- treat
post_send_hookfailure or timeout as best-effort diagnostics only; it must not roll back or fail an already-successful send - write a non-null
message_idon every ATM-authored message message_idis the retained ULID form of the one logical ATM message identity
message_id is required on every message written by atm send.
message_id is optional in the persisted schema (§14.1) only to support
legacy messages written by older clients, but atm send never omits it.
Recipients use message_id for:
- duplicate suppression
- read-time duplicate collapse
- acknowledgement targeting
When team config.json is missing, atm send may still proceed only when:
- the resolved team directory exists
- the target inbox path already exists
- no team, agent, or routing identity must be guessed
When atm send uses this fallback, it must:
- surface an actionable warning to the sender that delivery used inbox fallback because team config is missing
- keep the original delivery path best-effort and non-interactive
- send a best-effort repair notification to
team-leadwhen that recipient can be resolved without guesswork - deduplicate repeated repair notifications for the same unresolved missing-team config condition so inboxes do not accumulate hundreds of identical messages
When team config.json is malformed rather than missing:
atm sendmust fail with a structured configuration error- malformed config must not silently degrade into missing-config fallback
Exactly one message source must be used:
- positional message text
--stdin--file
--file behavior:
- verify the file exists
- apply the file-access policy
- if allowed, send a file-reference message body
- if not allowed, copy the file into the team share directory and rewrite the message body to reference the share copy
If positional message text is combined with --file, preserve the current two-part body shape:
<message text>
File reference: <path or share copy>
--requires-ack means the message must enter the pending-ack queue at write time.
Required behavior:
- write the message with
read = false - set
pendingAckAtto the send timestamp inside the atomic append boundary - do not wait for a later read to create the ack obligation
--task-id <id> attaches task metadata to the message envelope.
Required behavior:
- persist
taskId - reject blank task ids
- acknowledgement MUST NOT read, gate on, or change task state
If --task-id is present:
- treat the message as task-linked mail
- task-linked mail never requires acknowledgement; readiness is signalled by
task_ready, and the assignee starts the task withatm task start - reject
--requires-ackbecause it conflicts with--task-id
--task-complete closes the task named by --task-id with outcome
completed; it requires --task-id and carries the mandatory completion
report. It is the alias of atm task close <id> completed.
Required behavior:
- the assigner or assignee MUST be able to close the task
- the report message MUST be delivered before the close is applied
- closing an already-complete task MUST deliver the message and report the condition to the caller
- closing a task id that never existed MUST fail without writing a message
atm send <agent> --task-id <id>MUST be the alias ofatm task assign
Human output must include:
- recipient
- sender
- delivery result
JSON output must include:
action = "send"teamagentoutcomemessage_idrequires_acktask_idtask_completewhen a completion was requested
Dry-run JSON output must include:
action = "send"agentteammessagedry_run = truerequires_acktask_idtask_completewhen a completion was requested
('queue' here = the mailbox/query surface, unrelated to queue-kind nudges)
Product requirement IDs:
REQ-P-LIST-001atm listmust satisfy the bounded queue/search contract.REQ-P-READ-001atm readmust satisfy the documented single-message selection, mutation, and wait contract.
Satisfied by:
REQ-ATM-CMD-001for CLI entry, parsing, and dispatch aspectsREQ-ATM-OUT-001for human-readable and JSON output aspectsREQ-CORE-CONFIG-002for target-validation aspectsREQ-CORE-LIST-001for bounded metadata search, row shaping, and shared filter semanticsREQ-CORE-MAILBOX-001for merged inbox load/persist aspectsREQ-CORE-WORKFLOW-001for classification, queue selection, and legal transition aspects
Queue inspection is split into three commands:
atm listfinds messages without returning full message bodiesatm peekopens one full message without mutationatm readopens one full message with owner-only mutation
The split exists so ATM can keep default queue inspection bounded even when SQLite-backed mailbox history grows without a practical fixed upper bound.
Shared queue filters:
- optional target:
agentoragent@team --team <name>--from <name>--since <iso8601>--task <task-id>--contains <text>--unread--pending-ack--all
Shared command options:
--json
Inspection-only command option:
--as <name>onatm listandatm peek
Shared rules:
- all three commands must default to the caller's own inbox when no target agent is provided
- all three commands must resolve identity and target address using the defined precedence
atm listandatm peekmay resolve caller identity from--as, else the invoking-shellATM_IDENTITYatm readresolves caller identity from--as, else--chat-id, elseATM_CHAT_ID, else qualified invoking-shellATM_IDENTITY, else invoking- shellATM_IDENTITY; its existing owner-only mutation rule applies to the resolved full identity- if required caller identity cannot be resolved from the documented source for that command, fail before daemon dispatch
--as <name>changes caller identity resolution onsend, owner-onlyread, and inspection-only commands, never message matching; mutating commands require the base-agent match specified in the command matrix--jsonchanges output format only and is not a message-selection filter- all three commands must verify target team exists
atm listandatm peekmust verify an explicit target agent exists in the team config before inspection proceedsatm readmust reject explicit cross-agent mailbox targets on the mutating path and may only operate on the caller's own mailbox- all three commands must support the same semantic message filters even when their output shapes differ
--containsmust search both summary text and full message body text- on metadata-backed read/list/peek paths,
--containsmust stay full-body correct without widening the bounded metadata query into an eager full-body scan - all three commands must preserve origin-inbox visibility when bridge remotes are configured
Legacy atm read flag migration:
--unread-onlyis a deprecated alias for--unread--pending-ack-onlyis a deprecated alias for--pending-ack--historyis a deprecated alias for--all--since-last-seenremains accepted as an explicit restatement of the default seen-state filter- deprecation warnings must direct operators to the new flag names
Additional supported flags:
--limit <n>--tasks [--member <name>]lists durable task rows for the selected team--task-events <task-id> [--member <name>]lists append-only audit rows for one task
--tasks and --task-events are mutually exclusive and each conflicts with
every mailbox filter, including --task; --member is only valid with one of
those task-ledger surfaces. Their JSON results are bare arrays of TaskRow or
TaskEventRow, respectively, rather than the mailbox-list envelope.
Required behavior:
- load the mailbox/query surface through a bounded metadata-first query path
- query logical current messages rather than superseded predecessors
- return compact rows only, not full message bodies
- when
--containsis present, apply sender/timestamp/task and logical-current filtering on bounded metadata rows first, then reload durable body text only for surviving summary-miss candidates that still need a body check - support the canonical row fields:
message_idsummaryfromtimestampreadpending_acktask_id, withnullin JSON output when the logical message is not task-linked
- sort newest-first before limiting
- compute bucket/count summaries through bounded summary queries rather than by materializing every full message body for operator-facing response shaping
- perform no read-state or ack-state mutation
- keep default output bounded to actionable/head results rather than materializing full history by default
Additional supported flags:
--message-id <id>--timeout <seconds>--since-last-seen--no-since-last-seen--as <name>
Required behavior:
- return exactly one full message
- perform no read-state, seen-state, or ack-state mutation
- when
--message-id <id>is present, resolve that exact message when present - collapse successor/update chains to their terminal node before selector-based matching so superseded predecessors do not appear as separate current messages
- when
--task <task-id>is present, find task-linked messages, collapse each successor chain to its terminal node, then select the most recent logical current message - when selectors such as
--task,--from,--since,--contains,--unread, or--pending-ackmatch multiple messages, return the most recent match - when
--containsis present on the metadata-backed path, selector correctness must be preserved by checking bounded summary text first and reloading durable body text only for surviving summary-miss candidates - when multiple matches exist, include:
selected_message_idmatch_countadditional_match_count
match_countis the total number of logical current-message matches after all filters and successor-chain collapse are appliedadditional_match_countismatch_count - 1for a successful peek- when no selector is provided, return the most recent unread actionable
- when no selector is provided, prioritize pending-ack messages ahead of unread messages that do not require acknowledgement
- support optional wait mode with timeout
Additional supported flags:
--message-id <id>--timeout <seconds>--since-last-seen--no-since-last-seen
Required behavior:
- return exactly one full message
- offer the selected message's legal owner-visible seen/read transition to the supervised non-blocking handoff when a message is selected
- when
--message-id <id>is present, resolve that exact message when present - collapse successor/update chains to their terminal node before selector-based matching so superseded predecessors do not appear as separate current messages
- when
--task <task-id>is present, find task-linked messages, collapse each successor chain to its terminal node, then select the most recent logical current message - when selectors such as
--task,--from,--since,--contains,--unread, or--pending-ackmatch multiple messages, return the most recent match - when
--containsis present on the metadata-backed path, selector correctness must be preserved by checking bounded summary text first and reloading durable body text only for surviving summary-miss candidates - when multiple matches exist, include:
selected_message_idmatch_countadditional_match_count
match_countis the total number of logical current-message matches after all filters and successor-chain collapse are appliedadditional_match_countismatch_count - 1for a successful read- when no selector is provided, return the most recent unread actionable message
- when no selector is provided, prioritize pending-ack messages ahead of unread messages that do not require acknowledgement
- support optional wait mode with timeout
- offer the selected message's legal read-axis transition to the supervised non-blocking state handoff without awaiting durable application
- route the accepted transition to the authoritative ATM store; retained origin inbox files are compatibility inputs rather than the read mutation destination
- when a read-side transition is accepted, the returned
messagepayload andselected_message_idmust still refer to that same selected message;atm readmust not accept a transition for one message and then silently swap the output payload to a different unread message bucket_countsin the read outcome describe the reader-lane snapshot. A read-side transition accepted by the non-blocking handoff MAY become visible later; consumers requiring durable visibility use a boundedatm listpoll as specified in §7.12.
- load messages from the merged inbox surface
- deduplicate entries by
message_idbefore bucket selection and output rendering - classify each message into the read axis, the ack axis, and a derived message class
- map the derived message class into display buckets
When multiple inbox entries share the same non-null message_id, queue
inspection must display only the most recent entry. Earlier duplicates are
silently suppressed.
Deduplication order:
- compare entries by
message_id - keep the newest entry by message timestamp
- when timestamps are equal, keep the later record encountered in inbox order
- do not emit suppressed duplicates in either human or JSON output
The shared queue model exposes three display buckets:
unreadpending_ackhistory
Bucket mapping from the derived message class:
Unread->unreadPendingAck->pending_ackRead->historyAcknowledged->history
The display buckets are a presentation contract. They are not the canonical two-axis model.
Default queue inspection behavior:
atm listreturns a bounded actionable/head view- bare
atm peekreturns one selected actionable message without mutation - bare
atm readreturns one selected actionable message --allis the explicit full-surface override and may be slower
Seen-state is enabled by default unless --no-since-last-seen is set.
--since-last-seen explicitly enables the default watermark filter. When set
explicitly, it behaves the same as the default. If both --since-last-seen
and --no-since-last-seen appear, --no-since-last-seen wins.
When seen-state is enabled and a watermark exists:
- unread messages remain eligible even when older than the watermark
- pending-ack messages remain eligible even when older than the watermark
- history messages are filtered by the watermark
On a true first run with no stored watermark:
- the default queue view still shows only actionable messages
- historical messages remain hidden unless
--allis used
--all bypasses seen-state filtering entirely.
If seen-state updates are enabled:
- update the watermark using the latest displayed message timestamp
- do not use non-displayed messages when computing the watermark
--no-update-seen: when this flag is set, messages are read and displayed
normally but the seen-state watermark is not updated after the operation. The
watermark is left unchanged regardless of which messages were displayed.
--since <iso8601> filters to messages whose timestamp field is greater
than or equal to the given ISO 8601 datetime. It filters by message timestamp,
not by the seen-state watermark. It may be combined with seen-state filtering;
both constraints apply independently.
--from <name> is a sender filter: it restricts matched messages to those
sent by the named agent. It does not override the caller's identity.
When --timeout <seconds> is set on atm peek or atm read:
- establish the read selection baseline after actor resolution, inbox loading, workflow classification, and filter application
- if the requested selection already contains an eligible message at wait start, return immediately without blocking
- otherwise block until a newly arrived message becomes eligible for the requested read selection, or until the timeout expires
- re-run the normal selection over the updated merged inbox surface once a new eligible message arrives
- preserve the same sender, timestamp, seen-state, and selection filters during the wait
Timeout success condition:
- either the initial selection is already non-empty, or at least one message that was not eligible at wait start becomes eligible before the timeout expires
Timeout failure condition:
- the initial selection is empty and no newly eligible message arrives before the timeout expires
Peek mutation rule:
atm peeknever mutates mailbox state
Read mutation rules:
- any selected
atm readmessage's legal read/seen transition is offered to the supervised non-blocking handoff;mutation_applied = truereports acceptance, not durableread = truevisibility atm readmust never create a new pending-ack obligation on display- displaying a message never promotes acknowledgement state
- only sender-owned durable
requires_ackintent may createpending_ack_at - only explicit
atm ackhandling may clear pending acknowledgement intoacknowledged_at - when a selected message already requires acknowledgement, it remains pending-ack after display
- when a selected message does not require acknowledgement, it remains
NoAckRequiredafter display - required transition on read of a normal unread message:
(Unread, NoAckRequired) -> (Read, NoAckRequired)
- required transition on read of an ack-required unread message:
(Unread, PendingAck) -> (Read, PendingAck)
- no additional ack-axis mutation happens when:
- the message is
NoAckRequired - the message is already
PendingAck - the message is already
Acknowledged - the message is already
Read
- the message is
atm list human-readable output must remain metadata-only.
atm list JSON output must include:
action = "list"teamagentmessagescountbucket_counts
Every list row must include:
message_idsummaryfromtimestampreadpending_acktask_id(nullwhen the logical message is not task-linked)
atm read JSON output must include:
action = "read"teamagentmessageselected_message_idmatch_countadditional_match_countbucket_counts
atm peek JSON output must include the same shape as atm read, except:
action = "peek"mutation_applied = false
When mutation_applied = true and message is present:
message.message_idandselected_message_idmust identify the same durable message- it means the read/seen transition was accepted by the supervised, non-blocking read-state handoff; it does not mean that transition is durable or visible in this response
message.readandbucket_countsare the reader-lane snapshot and MAY still show the pre-handoff state. Both bareatm read --jsonandatm read --json --message-id <id>use the same acceptance semantics.- consumers requiring durable visibility MUST poll
atm list --jsonwith a bounded deadline. A handoff overflow or process exit leaves the message unread/unseen and re-presented;atm doctorreports handoff degradation. - the read-side mutation contract is distinct from
atm ack; read may mark a message read after the handoff drains, but only ack clearspending_ack_atand setsacknowledged_at
Human-readable atm peek and atm read output must render one message body
only. When additional matches exist, they must state that more matches were
found and direct the operator to atm list for metadata inspection instead of
emitting additional full bodies.
The following requirements make the AV mailbox-read cutover observable and non-regressible. They apply to the daemon's mailbox read-family operations, regardless of whether the caller is the CLI, an HTTP client, or a graft adapter.
R-READ-CONC-1Mailbox read-family operations (read,peek,list,doctor, andquery) MUST be serviced concurrently by a bounded read-only reader lane. They MUST NOT share a concurrency bound with, or be ordered behind, any write or housekeeping lane. A write-lane permit MUST NOT be required to select or load read response data.R-READ-CONC-2Read deadlines MUST be enforced cancellably. A request that cannot acquire reader capacity before its deadline MUST fail explicitly with a saturation or deadline outcome; it MUST NOT remain in an indefinite queue. Reader capacity MUST be reclaimed when a cancelled request stops.R-STATE-RACE-1Durable primary message records MUST be immutable after admission. Mutable read, acknowledgement, and seen state is race-tolerant: a read racing a state change MAY return either value. No mailbox-read requirement may demand read-your-writes, snapshot pinning, or reader/writer fencing.R-STATE-HANDOFF-1Read-flow read/seen state transitions MUST be submitted to a supervised, bounded, non-blocking in-process handoff. That handoff owns writer admission and retry; the read path MUST NOT await the writer lane. A transition MAY be lost only when the handoff buffer overflows or the process exits. Buffer overflow MUST be counted and surfaced byatm doctor. In either loss case the behavior MUST be fail-safe: the affected message remains unread/unseen and is re-presented on a subsequent read; it MUST NOT be hidden or discarded. Its lifecycle MUST include readiness-gated startup, a monitored supervisor task, and an atomicUnavailablestate that rejects new handoffs while preserving the buffered work if the task faults. The supervisor MUST restart within a bounded budget. Restart exhaustion or permanent writer failure MUST fail the runtime closed.R-STATE-RACE-1governs observation only and MUST NOT authorize discarding a transition.
The handoff behavior is a product decision recorded in ADR-059. The ADR records permanent drop and synchronous write-through as rejected alternatives and their operator consequences.
Caller-owned --file and template source paths are a write-admission concern
but MUST NOT consume Tokio worker capacity or reader-lane capacity while their
filesystem policy is evaluated.
R-WRITE-PREFLIGHT-1File and template source preparation MUST run through a bounded blocking admission of capacity two. Inline bodies do not require blocking admission.R-WRITE-PREFLIGHT-2Source preparation MUST observe the request deadline. A request unable to start or finish preflight in time MUST fail closed before durable admission; it MUST NOT persist a partial message.R-WRITE-PREFLIGHT-3A timed-out source-preflight job retains its permit until its blocking work exits. This bounds permanently stalled filesystem operations to two, leaves read-family work schedulable, and forbids treating an abandoned response as a successful write.R-WRITE-PREFLIGHT-4Every source-preflight job that outlives its deadline MUST be counted and surfaced throughatm doctorfor the daemon lifetime.
Product requirement ID:
REQ-P-ACK-001atm ackmust satisfy the documented acknowledgement contract.
Satisfied by:
REQ-ATM-CMD-001for CLI entry, parsing, and dispatch aspectsREQ-ATM-OUT-001for human-readable and JSON output aspectsREQ-CORE-MAILBOX-001for atomic ack persistence and reply append aspectsREQ-CORE-WORKFLOW-001for pending-ack eligibility and acknowledgement transition aspects
Acknowledge a pending-ack message in the caller's own inbox and send a visible reply to the original sender.
- positional
message-id - positional reply text
--team <name>--json
- resolve the caller's own inbox using invoking-shell
ATM_IDENTITY - fail before daemon dispatch if caller identity is unavailable
- locate the target message in the merged inbox surface
- require the target message to be in the pending-ack ack state
- persist the ack transition back to the physical inbox file that owns the source message when the merged inbox surface includes origin inbox files
- atomically:
- set
read = true - remove
pendingAckAt - set
acknowledgedAt - append a reply message to the original sender's inbox unless the acknowledged pending-ack message is an unqualified same-agent/same-team historical self-addressed message
- set
- preserve
acknowledgesMessageIdon the emitted reply - hardcode
requires_ack = falseon the emitted reply - do not allow an acknowledgement reply to request acknowledgement itself
- reject duplicate acknowledgement of an already acknowledged message
- run matching
[[atm.post_send_hooks]]rules after a successful ack, using the reply message as the hook subject - when the pending-ack message is an unqualified same-agent/same-team historical self-addressed message, mark it acknowledged, suppress reply emission, and report the suppression explicitly in the ack output contract. A host-qualified source is never this suppression case: it produces the ordinary canonical ACK reply write.
Phase R continuation semantics:
- one successful acknowledgement clears the chain-level acknowledgement obligation for the current terminal message and all of its ancestors
- if a later update arrives on an already acknowledged ack-required chain, the chain becomes pending again until the new terminal message is acknowledged
-
REQ-P-THREAD-001ATM message update chains must be strictly linear.Required behavior:
- each message may have at most one direct successor
- each successor references exactly one predecessor
- no branching successor graph is permitted
- the terminal node in the chain is the effective current instruction or state for normal reads
-
REQ-P-THREAD-002Only the original sender may update a message chain.Required behavior:
- only the root/original sender may append successors
- recipients and third parties must not add
add-detailsorsupersedeupdates to another sender's chain
-
REQ-P-THREAD-003ATM supports exactly two successor modes for non-ephemeral chains:add-detailssupersede
Required behavior:
- compatibility/export payloads carry successor metadata with
parentMessageIdandthreadMode add-detailsappends missing context while preserving the prior message as valid historical contextsupersedereplaces the prior message as the effective current instruction- logical-current selection keeps the terminal message id for both modes
- terminal
add-detailspreserves still-valid predecessor context in the effective current body used for matching and display - terminal
supersedeuses only the replacement body as the effective current instruction - if a successor arrives after the predecessor was already read, the successor still produces a new nudge so the current effective instruction is visible
-
REQ-P-THREAD-004Ack is a chain-level importance property.Required behavior:
- a chain is either ack-required or not ack-required
- the root/original message establishes that ack class
- successors inherit the existing chain ack class and must not flip it
- one acknowledgement clears the chain up to the then-current terminal node
- if a later successor arrives on an ack-required chain after that acknowledgement, the chain becomes pending again
- parent messages must not remain separately actionable for acknowledgement once a successor exists
-
REQ-P-THREAD-005Ephemeral messages are standalone, time-bounded records.Required behavior:
- ephemeral messages expire by time only, using SQLite-owned
expires_at - compatibility/export payloads carry ephemeral expiry with
expiresAt - no product behavior may depend on first-read deletion semantics
- periodic daemon cleanup deletes expired ephemeral rows
- ephemeral messages are not updatable
- ephemeral messages may not be parents or children in successor chains
- once read, an ephemeral message becomes hidden from normal reads but
remains visible through
--view-alluntilexpires_at
- ephemeral messages expire by time only, using SQLite-owned
-
REQ-CORE-MAILBOX-UNIFIEDMutable mailbox/runtime state must be owned by one canonical SQLite table,mail_message_states.Required behavior:
mail_messagesremains the immutable message-content tablemail_message_statesis the only canonical owner for mutable mailbox state:readpending_ack_atacknowledged_atexpires_atdeleted_atupdated_at
- the retired split-state model (
mail_visibility_statesplusack_state) must not be reintroduced under old or new names - normal mailbox queries must hide rows with
deleted_at - deleted rows may surface only through explicit admin/diagnostic paths
- time-bounded ephemeral retention uses
expires_atfrommail_message_states, not a field onmail_messages
JSON output must include:
action = "ack"teamagentmessage_idreply_dispositionkind = "sent"withreply_message_idandreply_targetwhen a reply message was emittedkind = "suppressed_self_ack"only when the historical pending-ack message was unqualified same-agent/same-team and no reply was emitted
reply_text(validated reply body; retained even when self-ack suppression prevents reply emission)task_id(optional String, present when the source message hastaskId)warnings(array of strings, omitted when empty)
Product requirement ID:
REQ-P-CLEAR-001atm clearmust satisfy the documented clear contract and preserve pending-ack protection.
Satisfied by:
REQ-ATM-CMD-001for CLI entry, parsing, and dispatch aspectsREQ-ATM-OUT-001for human-readable and JSON output aspectsREQ-CORE-CONFIG-002for target-validation aspectsREQ-CORE-MAILBOX-001for clear-set persistence aspectsREQ-CORE-WORKFLOW-001for clear-eligibility and pending-ack protection aspects
Remove non-actionable messages from one inbox without touching actionable work.
- optional target agent:
agentoragent@team --team <name>--older-than <duration>--idle-only--dry-run--json
- default to the caller's own inbox when no target agent is provided
- resolve the target inbox using the retained address and identity rules
- fail before daemon dispatch if caller identity is unavailable
- compute clear eligibility from the merged inbox surface
- persist removals back to the physical inbox file that owns each removed message when origin inbox files are present in the merged surface
Default clear behavior removes only clearable messages:
(Read, NoAckRequired)(Read, Acknowledged)
Clear must never remove:
(Unread, NoAckRequired)(Unread, PendingAck)(Read, PendingAck)
Additional rules:
--idle-onlynarrows removal to idle-notification messages only--older-thanfurther filters the clearable set by message timestamp age- dry-run returns the computed removal set without mutation
- clearing must preserve unknown fields on messages that remain
JSON output must include:
action = "clear"teamagentremoved_totalremaining_total- removal counters by class
Product requirement ID:
REQ-P-LOG-001atm logmust satisfy the documented shared-observability query/follow contract.
Satisfied by:
REQ-ATM-CMD-001for CLI entry, parsing, and dispatch aspectsREQ-ATM-OUT-001for record rendering/output aspectsREQ-ATM-OBS-001for CLI observability bootstrap/injection aspectsREQ-CORE-LOG-001for core query/follow/filter behavior aspectsREQ-CORE-OBS-001for ATM event/query-model aspects
Inspect ATM observability records through shared sc-observability query/follow APIs.
atm log replaces the old daemon-log viewing model. It must not depend on daemon-owned log files, daemon status, or tmux fallback behavior.
--tail--level <trace|debug|info|warn|error>--match <key=value>repeatable--since <iso8601|duration>--limit <n>--json
Deferred from the current source repo:
- direct
--fileselection of arbitrary ATM log files - separate
atm tailcommand
- query existing ATM records through the injected observability port over
sc-observability - support follow mode through the same adapter
- support filtering by level
- support filtering by structured key/value fields
- support filtering by time window
- support limit/order controls for non-tail mode
- default to snapshot mode when
--tailis not set - return snapshot results newest-first before applying output limits
- return followed records in arrival order while
--tailis active - use the built-in shared file-backed retained log store as the authoritative query/follow source
The retained ATM event vocabulary must include enough structure to filter on:
- command
- team
- actor
- target
- outcome
- error class
This ATM field set is ATM-owned even when the underlying query/follow/filter mechanics are shared in sc-observability.
Human output must show one record per line with enough information to understand:
- timestamp
- severity
- source/service
- event name or message
- important ATM fields when present
JSON output must emit structured records suitable for machine filtering and test assertions.
Each JSON record must expose at least:
- timestamp
- severity
- source or service
- event name
- ATM structured fields map
Product requirement ID:
REQ-P-DOCTOR-001atm doctormust satisfy the documented local diagnostics contract.
Satisfied by:
REQ-ATM-CMD-001for CLI entry, parsing, and dispatch aspectsREQ-ATM-OUT-001for report rendering/output aspectsREQ-ATM-OBS-001for CLI observability bootstrap/injection aspectsREQ-CORE-CONFIG-001for config and identity inspection aspectsREQ-CORE-DOCTOR-001for diagnostic evaluation aspects
Run local ATM diagnostics for the retained ATM runtime.
atm doctor remains a local diagnostics command, but in the current SQLite/daemon architecture
architecture it must also report daemon/runtime availability because normal ATM
mail behavior depends on the singleton daemon being present.
Phase-AA target direction:
atm doctorkeeps daemon/runtime reporting, but daemon routing is not the only legal path for diagnostics- checks that are inherently local and store-backed may run directly from the CLI
- daemon-routed doctor data is additive and exists for daemon-owned runtime state or faster asynchronous answers when the daemon is already live
--team <name>--json
The initial doctor implementation must cover:
- config file discovery and parse health
- effective team resolution
- caller identity/team visibility and optional diagnostic scope behavior
- obsolete config identity drift detection (
[atm].identityand legacy top-levelidentity) - daemon control-socket existence and reachability
- singleton daemon ownership health
- SQLite mail-store path visibility and openability when the current runtime is active
- baseline
[atm].team_memberscoverage againstconfig.json.members - team directory existence
- team config existence and parse health
- inbox directory existence and writability
- stale mailbox lock detection across
~/.claude/teams/*/inboxes/*.lockusing start-of-run and end-of-run snapshots; a lock present in both snapshots is stale and must be reported withATM_WARNING_STALE_MAILBOX_LOCKas a transitional compatibility finding rather than a normal mail-correctness dependency in the current SQLite/daemon architecture ATM_HOME,ATM_TEAM, andATM_IDENTITYoverride visibilitysc-observabilityinitialization health- active shared log path visibility
sc-observabilityquery-health readiness foratm log- open-task reminder thresholds and lead-notification audit outcomes
- blocked runtime episode escalation and retry behavior
- daemon and per-team escalation recipient configuration and effective-source reporting
Caller-context behavior for atm doctor:
atm doctormust not requireATM_IDENTITYatm doctormust not requireATM_TEAMatm doctor --team <name>may narrow diagnostic scope when suppliedatm doctormay report caller-context visibility and invalid override situations diagnostically, but it must not fail solely because caller identity/team are absent
Human output must provide:
- overall status summary
- findings grouped by severity
- full current member roster from
config.json, with baseline[atm].team_membersshown first andteam-leadfirst among that baseline - concrete remediation guidance when the user can act
JSON output must provide:
- summary
- findings
- recommendations
- environment override visibility
- member roster
- observability health snapshot
Each doctor finding must expose at least:
- severity
- code
- message
- remediation when available
The obsolete config-identity finding must use:
ATM_WARNING_IDENTITY_DRIFT
Phase AX.6 doctor findings must use these warning codes and actionable guidance:
ATM_ROSTER_NO_LEAD—assign one lead: atm teams update-member <team> <member> --agent-type leadATM_ROSTER_MULTIPLE_LEADS—keep one lead: atm teams update-member <team> <member> --agent-type <other type>ATM_ROSTER_RESERVED_NAME—rename the member: atm-daemon is reserved for daemon-originated messagesATM_TASK_STALLED—check the assignee or close the task: atm task close <task_id> completed --stdinATM_MEMBER_BLOCKED—<member> is waiting for interactive input; attach to its Herdr agent and answer the prompt
Critical findings must cause a non-zero exit status.
Product requirement ID:
REQ-P-TEAMS-001atm teamsmust satisfy the documented retained local team recovery contract.
Satisfied by:
REQ-ATM-CMD-001for CLI entry, parsing, and dispatch aspectsREQ-ATM-OUT-001for human-readable and JSON output aspectsREQ-CORE-TEAM-001for local team discovery, roster mutation, and backup/restore behavior
Provide the minimum retained local team-recovery surface required for initial release and the documented backup/restore workflow.
The retained teams surface for initial release is:
atm teamsatm teams add-memberatm teams update-memberatm teams remove-memberatm teams backupatm teams restore
The retained surface explicitly does not include broader historical team orchestration commands such as:
spawnjoinresumecleanup
Bare atm teams must:
- list discovered teams under ATM home deterministically
- expose at least team name plus enough summary information, such as member count, to pick a target team for restore or repair work
atm teams add-member must:
- validate that the target team exists
- reject duplicate member names
- persist the new member entry deterministically in team config
- persist the member's durable
home_diron the canonical ATM roster row and project that samehome_dirinto compatibilityconfig.json.members - create any required local inbox state atomically with the roster update
- reject the reserved member name
atm-daemonwithATM_MESSAGE_VALIDATION_FAILED; an existing legacy row may remain and is reported byatm doctorasATM_ROSTER_RESERVED_NAME
atm teams update-member must:
- validate that the target team exists
- validate that the target member already exists
- update existing canonical roster metadata without creating a new member
- accept point updates for the accepted mutable roster metadata:
home_dirharnessagent_typemodelrecipient_pane_id
- reject requests that attempt to use
update-memberas implicit member creation - reject operator attempts to set
cwd,live_cwd, orlaunch_cwd; runtime working location and startup-location logging are not operator-settable throughupdate-member - project the repaired metadata deterministically into compatibility
config.json - preserve unchanged member metadata when a field is not supplied
- reject the reserved member name
atm-daemonwithATM_MESSAGE_VALIDATION_FAILED; an existing legacy row may remain and is reported byatm doctorasATM_ROSTER_RESERVED_NAME
atm teams remove-member must:
- require the caller identity to belong to the target team
- validate that the target team and member exist
- remove exactly the requested member from the canonical roster
- allow removing the last member or the currently authenticated caller
- leave the removed member's inbox state untouched
- support human-readable and pretty-printed JSON output
atm teams backup must:
- create a timestamped snapshot under the ATM team backup area
- capture the current
config.json - capture the ATM-owned
.atm-statetree for workflow compatibility state when present - capture the selected team's durable state from the host-scoped SQLite
database at
~/.atm/db/mail.db - capture team inbox files, excluding transient
*.locksentinels, dotfiles, and restore markers - capture the ATM team task bucket
- report the created backup path in human and JSON output
- not claim to back up the separate Claude Code project task list
atm teams restore must:
- restore from the newest snapshot by default or from an explicit backup path
- support a dry-run mode that reports members, inboxes, and tasks that would be restored
- preserve the current team-lead entry and current
leadSessionId - add only missing non-lead members from the snapshot
- clear runtime-only restored-member fields such as session, activity, and pane state before persisting them
- restore the ATM-owned
.atm-stateworkflow compatibility state from the chosen snapshot when present - restore the selected team's durable state back into the host-scoped SQLite database from the chosen snapshot
- restore non-lead inbox files from the chosen snapshot deterministically
- treat stale inbox
*.locksentinels as transitional compatibility diagnostics rather than a restore correctness gate - restore the ATM team task bucket and recompute
.highwatermarkfrom the maximum restored task id - fail with a structured error when backup material is missing or malformed
- avoid partial restore on validation or snapshot-load failure
Human output must make the performed action and target team clear.
JSON output must include:
actionteam
add-member JSON output must additionally include:
member
update-member JSON output must additionally include:
member
remove-member JSON output must additionally include:
member
backup JSON output must additionally include:
backup_path
restore JSON output must additionally include:
backup_pathmembers_restoredinboxes_restoredtasks_restored
Dry-run restore JSON output must additionally include:
dry_run = truewould_restore_memberswould_restore_inboxeswould_restore_tasks
Product requirement ID:
REQ-P-MEMBERS-001atm membersmust satisfy the documented local roster inspection contract.
Satisfied by:
REQ-ATM-CMD-001for CLI entry, parsing, and dispatch aspectsREQ-ATM-OUT-001for human-readable and JSON output aspectsREQ-CORE-TEAM-001for local roster loading and deterministic projection
List the current local team roster for verification, recovery, and restore follow-up without depending on daemon-only or hook-only state.
--team <name>--json
atm members must:
- resolve the effective team using the retained team-resolution rules
- load the local team roster from canonical ATM roster state
- return a structured error when the team is missing from canonical ATM roster state
- show all rostered members deterministically, with
team-leadfirst when present and remaining members in stable local order - use these names distinctly:
home_dir: durable SQL-backed agent-home directory for the member; for worktree-backed members it preserves the worktree home and the canonical association back to the owning main repolive_cwd: runtime-only working-directory overlay for the invoking ATM member when the active CLI/doctor process can bindATM_IDENTITYto the displayed member; never durable roster metadatalaunch_cwd: startup-only current-directory snapshot emitted to ATM CLI startup logs; never durable roster metadata
- never use bare
cwdwhenlaunch_cwdorlive_cwdis the real meaning - expose currently persisted member metadata that ATM already knows durably,
such as
home_dir, type, harness, model, or pane id, and may overlaylive_cwdfor the invoking member only - not persist
live_cwdorlaunch_cwdas canonical member roster metadata - remain useful without daemon or hook state
Richer runtime state, such as live session or activity data, may be layered on later, but it is not required for the retained local release surface.
Human output must show:
- team name
- one row per member
- enough persisted member detail to verify roster repair or restore outcomes
JSON output must include:
teammembers
Each member object must expose at least:
nameharness: the persisted roster harness using its stable kebab-case spelling (for examplehermesorpython-graft)- persisted local member metadata when present
Product requirement ID:
REQ-P-HELP-001atm helpmust satisfy the documented conceptual-help contract for the daemon + SQLite release line.
Satisfied by:
REQ-ATM-CMD-001for CLI entry, parsing, and dispatch aspectsREQ-ATM-OUT-001for human-readable and JSON output aspects
Provide one ATM-owned conceptual help surface that complements clap-generated
syntax help without duplicating the flag/argument contract already exposed by
--help.
atm help must:
- remain a separate subcommand from clap-generated
atm --help - provide
atm help --list - provide
atm help <topic> - provide
atm help <topic> --json - delegate
atm help <subcommand>to the authoritative clap--helpoutput first, with any ATM-owned prose appended after that output when needed - treat clap output as the single source of truth for command flag documentation
- keep concept topics in one typed topic registry rather than scattered prose fragments
- keep topic output concise and point to installed long-form docs when the topic has an authoritative user-doc file
- keep this Phase
Yslice narrowly on conceptual help plus wording cleanup rather than broadening into general structured JSON-input work
Tier-1 concept topics for the first delivery:
configerrors
Tier-2 concept topics for the first delivery:
hooksidentityskills
Human output must:
- clearly distinguish concept topics from command syntax help
- preserve clap output verbatim when the target is a subcommand
- include the installed-doc pointer when the concept topic has authoritative long-form user docs
JSON output must:
- expose the requested topic or command target
- identify whether the result is:
overviewtopic_listconcept_topiccommand_help
- include the rendered help body in a structured field suitable for agent use
- include the installed-doc pointer when the concept topic has authoritative long-form user docs
Product requirement ID:
REQ-P-USER-DOCS-001ATM must ship a versioned installed end-user document corpus that complementsatm help.
Satisfied by:
- intentionally undecomposed product requirement; this spans repo-owned documentation, release packaging, help rendering, and publisher preflight
Required behavior:
-
the authoritative repo-owned source tree for installed end-user docs is
docs/user-documents/ -
the installed destination is
<install-root>/share/doc/atm/ -
the installed primary entrypoint is
<install-root>/share/doc/atm/README.md -
the default local-install root remains
~/.local/atm/<version>/ -
installed-doc lookup at runtime is derived from the installed
atmbinary location using the executable-relative path../share/doc/atm/ -
runtime state under
~/.atm/must remain distinct from the installed document tree -
ATM_HOMEis the runtime/data root only and must not be used to locate the installed end-user document tree -
atm helpmay stay concise, but it must point users to the installed corpus for long-form operator guidance -
end-user docs must remain operator-facing only:
- no direct SQLite queries
- no direct database edits
- no repo-internal development workflow instructions
-
hook and built-in nudge-template docs must enumerate the exact supported operator surface and variables
-
all user-doc links must be relative so the copied installed tree remains navigable after packaging
-
REQ-P-USER-DOCS-002Installed end-user docs are a release-gated artifact.
Satisfied by:
- intentionally undecomposed product requirement; this spans repo-owned validation and release/publisher automation
Required behavior:
- every file in
docs/user-documents/must carry the accepted metadata header withreviewed_for_release - the release/publisher gate must fail closed when a required user-doc file is missing, stale for the target release version, or structurally invalid
- fenced
json,xml,toml, andbashexamples in the user-doc corpus must be mechanically validated - the same canonical verifier must validate both the repo-owned source tree and the staged/installed copied tree
- phase-close evidence must prove the installed/archive output contains the expected copied corpus
Product requirement ID:
REQ-P-WORKFLOW-001The message/workflow model must satisfy the documented persisted-field, two-axis, and legal-transition rules.REQ-P-TEMPLATE-WORKFLOW-001A decomposed templated message may carry a template-declared, generic workflow snapshot whose scope, state, stage, transition, and optional iteration are resolved at admission and retained immutably. ATM validates structure but must not reserve orchestration vocabulary or infer state from rendered content. The canonical decision is ADR-046.REQ-P-TEMPLATE-TAGS-001Template-declared literal tags, sender/instance tags, and ATM-derived search tags must retain distinct provenance. The admission-time applied-template snapshot is historical truth; an effective tag projection is a deterministic search aid only; derived tags are reproducible from the immutable snapshot rather than caller-writable data. Reserved generated tag prefixes must not be caller-spoofable.REQ-P-WORKFLOW-ANALYTICS-001ATM must make the immutable template workflow snapshot available through its local query surfaces for generic duration and iteration analysis. Optional OpenTelemetry-compatible export is a projection of durable facts and must not alter routing, admission, policy, or security decisions.
Satisfied by:
REQ-CORE-WORKFLOW-001for the canonical two-axis model and legal transitionsREQ-CORE-TEMPLATE-WORKFLOW-001atm-coremust own the transport-neutral declaration validation and pure merged-variable resolution for optional template workflow facts. It must carry opaque bounded scope, state, stage, transition, and iteration values into an immutable admission snapshot; neither it nor any downstream surface may reserve orchestration vocabulary or infer facts from rendered content. The requirement derives from ADR-046 and satisfiesREQ-P-TEMPLATE-WORKFLOW-001.REQ-RUSQLITE-TEMPLATE-WORKFLOW-001atm-storage-rusqlitemust own the additive migration, atomic persistence, and indexed query projection for the already-validated workflow snapshot. Its durable tag columns are limited to existing caller/instance tags, the immutable applied-template tag snapshot, and the atomically computedeffective_tags_jsonsearch projection; derived tags have no independently persisted column. It adds no storage capability trait and never rebuilds history from mutable catalog data. The requirement derives from ADR-046 and satisfiesREQ-P-TEMPLATE-WORKFLOW-001andREQ-P-TEMPLATE-TAGS-001.REQ-CORE-WORKFLOW-ANALYTICS-001atm-coremust expose immutable workflow facts and their tag provenance through local query/projection contracts for generic duration and iteration analysis. Any optional telemetry sink is a non-authoritative read-side projection only: it cannot affect message admission, routing, retry, policy, or security. The requirement derives from ADR-046 and satisfiesREQ-P-WORKFLOW-ANALYTICS-001.
Required fields:
fromtexttimestampread
Optional fields:
source_teamsummarymessage_idtaskIdpendingAckAtacknowledgedAtacknowledgesMessageIdparentMessageIdthreadModemetadata
Unknown fields must be preserved.
For ATM-authored messages:
- ATM machine-readable identity is mandatory
- ATM uses one logical message identity and exports it through
message_idon the shared compatibility surface - ATM service addressing accepts ULID text only
- thread/update metadata uses
parentMessageIdplusthreadMode - time-bounded ephemeral retention uses SQLite-owned
expires_at - ATM-authored machine identifiers must not be null or blank
Legacy or externally imported records may still omit message_id; the rewrite
must preserve such records without inventing synthetic ids during read.
The canonical model has two independent axes.
Read axis:
UnreadRead
Ack axis:
NoAckRequiredPendingAckAcknowledged
Persisted-field classification:
- read axis:
read = false=>Unreadread = true=>Read
- ack axis:
acknowledgedAtpresent =>Acknowledged- else
pendingAckAtpresent =>PendingAck - else =>
NoAckRequired
Derived message class for queue logic:
- ack axis
PendingAck=>PendingAck - else ack axis
Acknowledged=>Acknowledged - else read axis
Unread=>Unread - else =>
Read
The canonical two-axis model is distinct from the read command’s display buckets.
Send normal message
-> (Unread, NoAckRequired)
Send ack-required message
-> (Unread, PendingAck)
Send task-linked message
-> persist taskId
-> (Unread, PendingAck)
Read own inbox with marking enabled, normal unread message
(Unread, NoAckRequired) -> (Read, NoAckRequired)
Read own inbox with marking enabled, ack-required unread message
(Unread, PendingAck) -> (Read, PendingAck)
Peek any inbox
(Unread, NoAckRequired) -> (Unread, NoAckRequired)
(Unread, PendingAck) -> (Unread, PendingAck)
(Read, NoAckRequired) -> (Read, NoAckRequired)
(Read, PendingAck) -> (Read, PendingAck)
(Read, Acknowledged) -> (Read, Acknowledged)
Ack workflow
(Read, PendingAck) -> (Read, Acknowledged)
and emit a reply message that references the original message id
Clear workflow
remove only (Read, NoAckRequired) and (Read, Acknowledged)
Disallowed transitions:
- any transition that makes the read axis move from
Readback toUnread Acknowledged -> PendingAckAcknowledged -> NoAckRequired- clearing a message in
PendingAck - clearing a message with read axis
Unread
The implementation must encode legal transitions in code structure, not only in comments or tests.
Messages with taskId are task-linked messages.
Required rules:
- a task-linked message never requires acknowledgement; readiness is signalled
by the task pass (
task_ready), and start byatm task start - acknowledgement is message hygiene only:
atm ackMUST NOT read, gate on, or change task state, and task admission MUST NOT reject a message ack - every transition, rejection, and reminder is append-only audit data; a same-agent resend of an open task id records no task event and changes no task state; it refreshes only the assignment message linkage (Phase BA design §3.1a); the durable tables and replay contract are defined by ADR-062
Task lifecycle (Phase BA):
- A task MUST exist as exactly one row per
(team, task_id). - Task state MUST be
assigned(queued),active(working), or closed. - An agent MUST hold at most one
activetask; the database MUST enforce this with a unique index on(team, assignee)restricted tostate = 'active'. The index fires when a task starts (the implicitassigned → activemove; rejected asActiveElsewhere), never atatm task assignadmission — any number ofassignedrows may queue behind the active one. - Starting a task MUST be
atm task start <id>by the assignee; it MUST move the task fromassignedtoactive, MUST move it to the head of the queue, and MUST send the assigner a start message. The daemon MUST NOT start a task. - An agent's queue MUST be ordered by
(position, assigned_at, task_id);assigned_atMUST be the time of the current assignment; it is reset only by reassignment or reopen and never by a queue move; a new task's default position MUST be the end of the queue. atm task move <id> --before <other> | --head | --endMUST reposition anassignedtask only;--headMUST place it next up behind the active task, and the active task MUST never be repositioned or preempted.- Closing a task MUST record one typed outcome from
completed | refused | cancelledwith optional reason text, MUST remove the task from the queue, and MUST append a timestamped event. - Reassignment and reopening MUST use
atm task assignon the same id: an open id may be reassigned in place, and a closed id may be reopened; neither operation creates a second row or permits simultaneous assignees. Every transition MUST append exactly onetask_eventsrow under that id;reassignedandreopenedare event kinds, not outcomes. atm taskMUST be the closed subcommand setassign,start,close,move,list,events;atm send <agent> --task-id <id>MUST aliasassignandatm send <assigner> --task-complete --task-id <id>MUST aliasclose <id> completedwith a mandatory report.- Close MUST deliver the report message before applying the close; an already-complete task MUST deliver and inform the caller; a task id that never existed MUST fail without writing a message.
atm task listMUST show the caller's queue in order and--allMUST show every member's queue; agent state shown there MUST be read live from the canonical roster record and MUST NOT be persisted.atm queueMUST deliver as an ephemeral queue item that is a scheduling view over the message, never a task row ortask_eventsentry; it MUST close on read, or on ack whenrequires_ackis set, and MUST be discharged before the next task is nudged.- A refusal MUST release the next queued task; consecutive refusals by one agent MUST escalate instead of continuing to feed.
Nudge invariant (Phase BA):
14. Nudge eligibility MUST consume the exact canonical RuntimeMemberState
from the ephemeral roster record; it MUST NOT consume
PickerMemberStatus, a RuntimeHealth projection, raw Herdr output, or
heartbeat DTOs.
15. Idle with an open task MUST be nudged, no more than once per 60 seconds
per task. The first prompt is task_ready; later prompts are
task_reminder with a rising attempt. Every assignment produces one
task_queued line at write time.
16. Active MUST never be nudged or diverted to another task.
17. Blocked or Offline MUST escalate once per episode and MUST receive
zero nudges.
18. Escalation MUST be one ordinary message to the roster lead (when exactly
one) and to every configured escalation recipient, resolved
independently; when a task's reminder count reaches 10 it MUST escalate
once and nudging MUST stop until the task changes state (start or close) or
is reassigned or reopened; a change in the assignee's
runtime state alone does not resume nudging.
Product requirement ID:
REQ-P-OBS-001ATM observability must satisfy the documented best-effort emit behavior and shared query/follow/health expectations.REQ-P-OBS-002ATM retained logs must use one host-scoped ATM-owned default directory and must not derive that retained location fromATM_HOME.REQ-P-OBS-003ATM retained logging must be non-silent by default for the daemon lifecycle baseline and for every warning/error emitted by ATM subsystems.REQ-P-OBS-004ATM retained-log maintenance must keep daemon success-path observability off the synchronous file-I/O hot path.
Satisfied by:
REQ-ATM-OBS-001for CLI bootstrap/injection aspectsREQ-CORE-LOG-001for ATM log query/follow service aspectsREQ-CORE-DOCTOR-001for observability health reporting aspectsREQ-CORE-OBS-001for ATM event and query-model boundary aspectsREQ-DAEMON-OBS-001andREQ-DAEMON-OBS-002for daemon/runtime retained event-baseline aspects
ATM must emit structured records through sc-observability.
Initial shared integration scope:
sc-observability-typessc-observability
Deferred from the initial retained observability integration:
sc-observesc-observability-otlp
Required ATM event classes:
- command started
- command succeeded
- command failed
- mailbox record skipped
Required ATM event fields:
- command name
- team when known
Required retained-log maintenance behavior:
- successful daemon event emission must spend at most one bounded in-memory handoff on the synchronous path; it must not reopen, append, flush, rotate, or prune retained files inline before returning control to the active daemon request/lifecycle path
- blocking retained-log admission must use the queue-backed
sc-observabilitylogger admission path (Logger::log()); any future non-blocking admission path must useLogger::try_log()and handle explicit queue-full degradation flush()/shutdown()are the only durability barriers for retained-log writes; queue admission alone must not be treated as immediate persistence- retained-log file append, rotation, and pruning must run on background maintenance machinery instead
- if retained-log maintenance falls behind, ATM must degrade explicitly with structured diagnostics rather than silently blocking the daemon success path
- retained-log pruning must use a bounded work budget per maintenance tick and must not rely on an unbounded wall-clock scan
- the retained-log shutdown threshold is configured through
RetainedLogPolicy.writer_shutdown_timeout - actor identity when known
- target identity when known
- task id when known
- result
- error class on failure
- count when applicable
- transition count when applicable
Emission is best-effort:
- logging failures must never block retained command behavior
- command correctness takes priority over observability delivery
Typed observability migration requirement:
- ATM must complete the phased migration from raw observability labels to
validated
ActionName/OutcomeLabelvalues at everyDaemonEventconstruction site and everySubsystemObservability::event()call site. - The current Phase W line intentionally stops short of that full migration. The remaining call-site conversion is tracked work, not optional cleanup.
- The final migration step depends on upstream
sc-observability-typessupport for a validated static-construction helper such asvalidated_static!orconst new_static().
Sink policy:
- the shared file sink is required for retained ATM observability
- default ATM-owned retained logs live at
~/.atm/logs/atm.log.jsonl ATM_LOG_DIRoverrides the exact retained log directory- retained log location is host-scoped and must not derive from
ATM_HOME - ATM-owned retained logs must not default to:
~/logs/~/.claude/logs/.local/share/logs/
- the shared console sink is optional and must remain off by default for normal ATM CLI command execution so command output stays stable
- console logging may be enabled later for explicit local debugging or integration testing
Diagnostic logging rules:
- retained logging must include the daemon lifecycle baseline by default:
- start requested
- startup completed / ready
- shutdown requested
- shutdown completed
- degraded / abnormal-exit signals
- command failures must emit structured failure diagnostics before the CLI exits, even when the command fails before reaching a core service
- degraded recovery paths that intentionally continue, such as malformed-record skips or missing-config fallback warnings, must also emit structured warning diagnostics
- every ATM warning/error diagnostic must carry a stable ATM-owned error code in addition to human-readable text
- command lifecycle failure events must include the stable error code when one is available
- every
warn!/error!event emitted by ATM subsystems must remain present in retained logs by default
atm log and atm doctor are not best-effort features in the same sense:
- they are explicit observability consumers
- if shared query/health APIs are unavailable, they must fail with clear structured errors
Product requirement ID:
REQ-P-ERROR-001Public command failures must satisfy the documented structured error requirements.
Satisfied by:
- intentionally undecomposed product requirement; crate-local error ownership remains derived from command and service requirements rather than a dedicated crate requirement ID in this pass
All user-visible failures must use structured errors with recovery guidance.
Persisted-data failures must preserve parser and entity context when available.
Stable error-code rule:
- every public
AtmErrormust map to a stable ATM-owned error code - ATM warning and error logs must include that code
- CLI bootstrap and argument-validation failures must also be logged with a stable error code before process exit
- the single source of truth for ATM-owned error codes is
atm-error-codes.md
Minimum error categories:
- configuration
- missing document
- address
- identity resolution
- team not found
- agent not found
- mailbox read
- mailbox write
- message validation
- store
- ingest
- export
- transport
- daemon runtime
- daemon singleton
- daemon client
Current runtime required families:
- store:
- SQLite bootstrap/open
- schema/transaction
- busy-timeout / saturation
- ingest:
- replay/import failure
- backpressure/degraded ingest
- export:
- historical Claude compatibility export failure
- re-export/replay failure
- transport:
- local daemon request failure
- remote connect/timeout/protocol failure
- daemon runtime:
- shutdown timeout
- signal/reload failure
- runtime over-capacity
- daemon singleton:
- already-running daemon
- stale-artifact cleanup/release failure
- daemon client:
- daemon unavailable
- daemon health-query timeout
- serialization
- file policy
- wait timeout
- observability emit
- observability query
- observability health
Mutation failures must be fail-safe:
- no partial send writes
- no partial read-mark updates
- no illegal state transitions after failed persistence
Product requirement ID:
REQ-P-RELIABILITY-001The retained command surface must satisfy the documented durability and consistency constraints.
Satisfied by:
-
REQ-CORE-MAILBOX-001for atomicity, duplicate suppression, and mailbox consistency aspects -
mailbox writes must be atomic
-
concurrent appends must not silently lose messages
-
duplicate message ids must not be appended twice
-
read-time duplicate message ids collapse to the newest visible entry
-
corrupt records should be skipped individually when possible
-
persisted config/team schema drift should recover with deterministic defaults when safe
-
missing team config may use only the explicitly documented send fallback behavior
-
persisted config/team records with missing identity or routing-critical fields must fail or be isolated rather than guessed
-
missing inbox files are treated as empty inboxes
-
seen-state races must not corrupt mailbox data
-
observability emission failures must not corrupt command behavior
Product requirement ID:
REQ-P-TEST-001The rewrite must satisfy the documented testing obligations.
Satisfied by:
- intentionally undecomposed product requirement; this governs workspace-level test coverage expectations rather than a single crate-local requirement ID
Because sc-observability is newly introduced into ATM, the rewrite must add explicit test coverage for:
- ATM event emission through the observability port boundary
- best-effort emission failure behavior
- two-axis state classification
- two-axis state transition enforcement
- log query by severity
- log query by structured field match
- log follow/tail behavior
- doctor observability-health reporting
- teams list behavior over the local ATM home
- members list behavior over local team config
- add-member duplicate validation and inbox creation
- backup snapshot completeness
- restore dry-run reporting
- restore preservation of team-lead /
leadSessionId - restore recomputation of
.highwatermarkto the maximum restored task id - retained mail-command correctness when observability emission fails
- clear eligibility behavior
The implementation must include:
atm-coretests for observability port behavior using test doubles- CLI integration tests for
atm log - CLI integration tests for
atm doctor - CLI integration tests for
atm ack - CLI integration tests for
atm clear - CLI integration tests for
atm teams - CLI integration tests for
atm members
Required testing architecture:
-
default test suites and all core correctness tests must not depend on:
- daemon spawn
- socket publication timing
- retry sleeps
- environment mutation races
- auto-start side effects
- unbounded waits
- panic-unsafe shared/global test hooks
-
these patterns are treated as sources of flake and false confidence rather than as acceptable test infrastructure
-
a test that might hang is invalid even if it does not use
thread::sleep(...) -
tests must use bounded waits tied to observable predicates or handshakes
-
REQ-P-SMOKE-001The repository must expose one smoke command family:just smokejust smoke fastjust smoke thorough
Required behavior:
just smoke fastmust prove the clean-room happy path on a new disposable baseline:- daemon bring-up
- team setup
doctoratm sendwithout--requires-ackatm sendwith--requires-ackatm readatm ack- nudge-visible flow
- clean shutdown
just smokemust include thefastlane plus broader retained/admin/ operator coverage and must provide root-cause notes for every deviationjust smoke thoroughmust include thenormallane plus every CLI interface on happy path and common error paths, with explicit PASS/FAIL/ SKIP row output and root-cause notes for every deviationjust smoke thoroughmust also include one real same-hostatm-graftlane that proves:- one graft host session connects to the same daemon used by the CLI lane
- post-send nudge delivery succeeds end-to-end
- unary graft
read,ack, andsendall succeed over the shared daemon contract - the CLI operator can observe the graft-host reply/follow-up effects
just smoke thoroughmust also include one shared-host multi-workspace lane where two or more workspaces use differentATM_HOMEvalues while sharing the same hostHOME, daemon, SQLite database root, and retained log root; that lane must prove:- concurrent
sendtraffic from multiple workspaces succeeds - concurrent
read/acktraffic from multiple workspaces succeeds - no cross-workspace message leakage occurs
- the shared daemon remains healthy until both workspaces finish
- concurrent
-
REQ-P-SMOKE-002Smoke reporting must write:- tracked latest smoke reports:
reports/smoke/smoke-fast.mdreports/smoke/smoke.mdreports/smoke/smoke-thorough.md
- gitignored timestamped smoke reports using the shared
YYYY-MM-DD-HH-MM-SS-*convention - one canonical JSON payload per run that records row verdicts, binary SHA, duration, and pass/fail/skip counts
- tracked latest smoke reports:
-
REQ-P-SMOKE-003Smoke logging must support two modes:- smoke/debug mode may enable detailed lifecycle/send/read/ack/nudge event visibility for retained-log analysis
- ordinary runtime logging must remain quiet enough that routine send/read/ ack success does not clutter normal operator logs
-
REQ-P-BENCHMARK-001Physical performance benchmarks must preserve the interactive OS user's canonical ATM state.Required behavior:
- a physical benchmark runs only as a dedicated, disposable benchmark OS
account whose
HostRuntimeScopeis independently derived for that account; it must not select an alternate root, endpoint, lock, or daemon throughATM_HOME, environment variables, symlinks, a workspace path, or a second daemon for the interactive account - the benchmark runner must refuse before daemon-switch, SQLite open, destructive filesystem work, or daemon start unless the executing account proves its benchmark-account contract and canonical state policy
- the current interactive OS user's
~/.atm/dbtree must never be renamed, replaced, deleted, restored, or used as a physical benchmark fixture - destructive benchmark-account reset or restore requires a completed, verified SQLite-consistent snapshot made with the SQLite backup API; the snapshot manifest records integrity evidence and incomplete snapshots are never restore candidates
- setup, snapshot verification, reset, daemon lifecycle, and post-run restoration occur outside the timed benchmark interval; evidence records their durations separately and invalidates a result if they contaminate a timed sample
atm teams backupis selected-team recovery material, not a whole-host database backup and not authorization for a benchmark to mutate the interactive user's durable SQLite state
This requirement is governed by ADR-052 and composes with ADR-026's one
HostRuntimeScopeper OS user. - a physical benchmark runs only as a dedicated, disposable benchmark OS
account whose
-
REQ-P-COVERAGE-001Coverage reporting must remain separate from ordinary test execution.Required behavior:
- the repository must expose
just test coverage - plain
just testmust not implicitly collect coverage - coverage reporting must write tracked latest reports:
reports/coverage/mac.mdreports/coverage/win.md
- coverage reporting must also write gitignored timestamped reports using the same timestamp convention as smoke reporting
- an explicit local coverage run may overwrite only the tracked latest report for the host platform that executed the run
- the other tracked platform report may remain at its last real result or an explicit placeholder until that platform executes its own coverage run
- Linux tracked-latest coverage artifacts are deferred/unsupported in the current Phase Z line and the coverage runner must fail clearly on Linux rather than silently pretending to produce supported tracked artifacts
- the repository must expose
-
bare
join(),recv(),wait(), or equivalent waits are prohibited in risky runtime/daemon test paths unless completion has already been proven by a bounded synchronization step -
test code must not use or reintroduce the current daemon-spawn pattern by name:
spawn_test_daemonwarm_daemonDaemonGuardATM_DAEMON_BIN- direct
Command::new(...atm-daemon...)
-
there is no approved "test daemon launch" path for ordinary ATM correctness tests
-
the primary test tiers are:
- CLI/composition tests using a fake HTTP application client
- in-process integration tests using the HTTP adapter over the shared request/response contracts
- a narrow daemon-runtime suite for singleton/startup/shutdown/recovery requirements only
-
real daemon process tests, if any, must be isolated to the daemon-runtime suite and must never become the default validation path for CLI or core business correctness
Product requirement ID:
REQ-P-ACCEPTANCE-001The rewrite is complete only when the documented acceptance criteria are met.
Satisfied by:
- intentionally undecomposed product requirement; this defines overall product completion gates rather than a single crate-local obligation
The rewrite is ready when:
atm sendworks through the documented production runtime pathatm readworks through the documented production runtime pathatm ackworks through the documented production runtime pathatm clearworks through the documented production runtime pathatm logworks through sharedsc-observabilityAPIsatm doctorworks as a local diagnostics command with daemon/runtime visibility in the current SQLite/daemon architectureatm teamsprovides the retained local team recovery surfaceatm membersprovides the retained local roster verification surface- retained commands preserve documented behavior, and any current-runtime shape changes are explicit in the requirements and architecture
- workflow-axis classification is correct
- workflow-axis transitions are encoded in implementation structure
- display buckets are derived consistently from the two-axis model
- task-linked messages never require acknowledgement; readiness is signalled by
task_ready, and the assignee starts the task withatm task start - observability integration is exercised by automated tests
- the file-by-file migration plan is complete enough to implement directly
- daemon singleton is enforced as requirement
#1with the documented multi-layer guards - the default test and CI paths contain no banned daemon-spawn helpers or timing-based daemon orchestration patterns
- the lint gate that enforces singleton/test-fidelity rules passes in
just lint
Cross-document invariants that must remain true:
taskIdimplies task-linked mail that never requires acknowledgement; readiness is signalled bytask_ready, and start byatm task start- a displayed message with
mutation_applied = truehas had its legal read/seen transition accepted into the supervised non-blocking handoff; durableread = truevisibility may follow later - pending-ack messages remain actionable until acknowledged
atm clearnever removes unread messagesatm clearnever removes pending-ack messagesatm read --timeoutreturns immediately when the requested selection is already non-empty
Phase M addresses blocking and important findings from the Phase L code review (ARCH-CR-001 through ARCH-CR-004 and associated QA findings) that must be closed before the 1.0 release.
-
REQ-CORE-MAILBOX-LOCK-001All mailbox read-modify-write operations must hold an exclusive advisory file lock for the duration of the operation.Rationale:
append_messageinmailbox/mod.rscurrently reads the full inbox, appends one record in memory, then callsatomic::write_messagesto replace the file. Two concurrent writers can both read the same snapshot and the later rename silently drops the earlier writer's append. This is ARCH-CR-001.Required behavior:
- before entering any read-modify-write section on an inbox file, ATM must acquire an exclusive advisory lock on a well-known lock sentinel derived from the inbox path
- the lock must be held for the full duration of read + modify + atomic replacement, including any durability sync that is part of the shared atomic-write helper boundary
- lock release must happen automatically when the lock guard is dropped (RAII)
- lock acquisition must use a bounded timeout (default 5 seconds) and fail
with a structured
AtmErrorcarryingAtmErrorCode::MailboxLockTimeoutwhen the timeout expires - the lock sentinel path is a transient runtime artifact: ATM writes the owner pid while the lock is held, unlinks the sentinel on guard drop, and must tolerate stale pid-bearing sentinels from crashed processes
- advisory locking is cooperative: only concurrent ATM processes coordinate
- any retained historical Claude inbox tooling must not let the sentinel lock block Claude Code native inbox appends because Claude does not participate in ATM's cooperative lock protocol
-
REQ-CORE-MAILBOX-LOCK-002Mailbox locking must work on macOS, Linux, and Windows without platform-specific feature flags in consuming code.Required behavior:
- on Unix: use
flock(2)exclusive lock on the lock sentinel file descriptor - on Windows: use
LockFileExexclusive lock on the lock sentinel file handle - the public API must present a single
MailboxLockGuardtype that is platform-uniform; platform branching is internal tolock.rs - the
fs2crate is the preferred implementation
- on Unix: use
-
REQ-CORE-MAILBOX-LOCK-003Locks must be per-inbox-file, not per-team or global.Required behavior:
- locking is scoped to a single inbox file path
- two concurrent
atm sendcommands to different recipients must not block each other - the lock sentinel path is
{inbox_path}.lock
-
REQ-CORE-MAILBOX-LOCK-004Every mailbox mutation path must acquire the lock.Required coverage:
append_messagefor both normal send and the missing-config team-lead notice path- workflow state writeback in read, ack, and clear paths
- any future mutation path added to the mailbox layer
Read-only
read_messagescalls with no following writeback do not require locking. -
REQ-CORE-MAILBOX-LOCK-005Multi-source mailbox commands must acquire their final required lock set before any mutating source reread, and must do so in deterministic path order.Rationale:
read,ack, andcleardo not operate on a single inbox file. The executed Phase P design permits unlocked observational snapshots when no mutation is committed from that snapshot, but any state-changing path must reacquire the full deterministic lock set, reload fresh source files under that lock set, recompute the mutation, and then persist. Locking only during the final write step would still allow stale reads and lost updates.Required behavior:
readis aread_possible_writepath: it may take an unlocked observational snapshot of the source inbox set, but if display-state mutation is needed it must re-discover the current source-file set, dedupe duplicate paths, sort the resulting paths deterministically by canonical path string, acquire the full lock set, then reload and recompute under that lock set before persistingackuses an unlocked preflight plus one final superset lock: it may resolve the reply target and candidate source message from an unlocked preflight, but it must acquire the final sorted superset lock plan before the mutating source reread, then re-read and re-validate the pending acknowledgement state under that final lock set before writing either the source or reply mailbox state- mutating
clearis a full-lock-through-persist path: it must acquire the deterministic lock set before its mutating source reread and must hold that lock set through removal computation, mailbox replacement, and workflow-sidecar updates;clear --dry-runremains observational and lock-free - final source-file discovery for a mutating path must use the command's existing requested-inbox plus origin-inbox resolution logic
- legitimately absent inbox paths at discovery time are excluded from the lock set rather than locked speculatively
- source enumeration faults are not treated as absent paths; if origin inbox discovery cannot enumerate the candidate directory completely, the command must fail closed instead of continuing with a partial source set
- for any mutating path, those locks must remain held through the fresh surface computation, state transition, and final writeback
- deterministic ordering must prevent deadlock when two commands contend on the same pair of inbox files in opposite discovery order
- lock acquisition uses one total timeout budget for the full lock set, not a fresh timeout per file
- if any lock in the set cannot be acquired, every previously acquired lock in that attempt must be released immediately and the command must fail without mutating any source inbox from a partially locked snapshot
- partial lock acquisition must never degrade into a best-effort state-changing
command result for
read,ack, orclear - the unlocked observational snapshot used by
read,ack, or dry-runclearmust never be the snapshot from which a later mutating commit is persisted - source discovery for mutating commands must fail closed: if directory enumeration itself fails or if any directory entry in the candidate inbox directory cannot be enumerated reliably, the command must abort before the mutating reread instead of warning and continuing with a partial source set
- if a discovered file disappears or becomes unreadable after lock planning but before or during the under-lock source-file load, the command must fail as a normal operator-actionable file-read error and must not persist any partial state
-
REQ-CORE-MAILBOX-LOCK-006Single-process single-threaded usage must not regress measurably due to lock acquisition.Required behavior:
- uncontended
flockis a single syscall returning immediately; no background threads or polling loops - lock sentinel created lazily on first lock attempt
- uncontended
-
REQ-CORE-MAILBOX-LOCK-007Lock acquisition must distinguish true lock contention from other lock-path I/O failures.Required behavior:
- only retry errors that actually mean "lock currently held by another process" for the current platform/API surface
- if the sentinel file cannot be opened, locked, or queried because of a
non-contention I/O or OS error, fail immediately with
MailboxLockFailedrather than sleeping until the timeout budget expires MailboxLockTimeoutis reserved for genuine contention or equivalent lock-busy conditions- operator recovery guidance must distinguish "wait and retry" from "repair filesystem/permissions state"
-
REQ-CORE-MAILBOX-LOCK-008Stale-lock sweeping must identify rotated lock sentinels conservatively and must evict only verifiable orphaned candidates.Required behavior:
- candidate matching is based on the basename, not the full path
- the accepted sentinel predicate is:
file_name.ends_with(".lock") || file_name.contains(".lock.") - the sweep must not use
path.extension() == "lock"because that misses rotated sentinels such asinbox.json.lock.old - the sweep must not broaden to arbitrary substring matching such as
contains("lock"); non-sentinel files likelocksmith.txtmust not be considered - a matched candidate is evictable only when its contents parse as the
documented
pid[:token]owner record format andprocess_is_alive(pid)returns false - malformed or unreadable candidate contents are treated as non-evictable and must be left in place for explicit operator cleanup instead of speculative deletion
- the sweep is a best-effort stale-artifact cleanup path, not a second lock authority; it must not claim ownership without the existing advisory-lock acquisition succeeding afterward
- Windows rename semantics must not be assumed to match Unix for a live held lock handle; rotated-name sweeping exists to clean up post-crash or externally renamed artifacts, not to coordinate live-lock handoff
Acceptance Criteria:
- positive predicate cases:
inbox.json.lock,inbox.json.lock.old, andinbox.json.lock.replacedare all treated as stale-sentinel candidates - negative predicate cases: malformed or unrelated names such as
inbox.json.lockold,locksmith.txt, andinbox.locksmith.jsonare not treated as stale-sentinel candidates - malformed rotated candidates that do match the filename predicate but do
not contain a parseable
pid[:token]owner record remain in place and are not deleted speculatively
-
REQ-CORE-MAILBOX-LOCK-009Read-only filesystem failures on the mailbox-lock path must surface as a dedicated non-contention diagnostic.Required behavior:
- ATM must classify read-only filesystem errors by raw OS error code rather than treating them as generic permission failures
- the required platform mappings are:
- Linux:
EROFS(30) - macOS:
EROFS(30) - Windows:
ERROR_WRITE_PROTECT(19)
- Linux:
- the same classification helper must be used for lock-path open/create, owner-record truncate/write, and sentinel removal so retry behavior and operator guidance stay consistent
- read-only filesystem errors must not participate in the lock-contention retry loop and must not be retried by sentinel-removal backoff logic
- on every lock-acquisition retry iteration, read-only-filesystem
classification must run before any timeout-budget decision; a classified
EROFS/ERROR_WRITE_PROTECTfailure must never fall through toMailboxLockTimeout - mutation-path failures caused by a read-only filesystem must return
MailboxLockReadOnlyFilesystem/ATM_MAILBOX_LOCK_READ_ONLY_FILESYSTEM, notMailboxLockFailedorMailboxLockTimeout - the structured error message and recovery guidance must include the lock
path plus the specific attempted operation (
open,write owner record, orremove stale sentinel) so operators can distinguish remount/media failures from ACL or contention issues - other non-contention lock-path filesystem failures, including
ENOSPC,EMFILE, andESTALE, remainMailboxLockFailedand are not retried - best-effort drop-time cleanup remains warn-only because the command has already completed, but public sweep or acquisition paths must surface the read-only diagnosis instead of silently suppressing it
Acceptance Criteria:
ATM_TEST_FORCE_LOCK_READONLY_FS=openinjects a synthetic platform-correct read-only-filesystem error into the lock open/create path only; owner-record write and sentinel-removal paths continue to run normallyATM_TEST_FORCE_LOCK_READONLY_FS=write_ownerinjects a synthetic read-only-filesystem error into the owner-record truncate/write path onlyATM_TEST_FORCE_LOCK_READONLY_FS=removeinjects a synthetic read-only-filesystem error into the stale-sentinel removal path only- when the seam is unset or set to any other value, no synthetic read-only filesystem failure is injected
- read-only failures injected through any of the three seam values surface as
MailboxLockReadOnlyFilesystem/ATM_MAILBOX_LOCK_READ_ONLY_FILESYSTEM, never asMailboxLockTimeout
-
REQ-CORE-PERSIST-ATOMIC-001Every shared mutable ATM-owned structured state file must be persisted atomically.Scope:
- live inbox files under
.claude/teams/<team>/inboxes/*.json - team
config.json - ATM-owned task-bucket JSON/state files written during backup/restore flows
.highwatermarkand any equivalent ATM-owned monotonic task-state file- send-alert / restore-progress / similar ATM-owned persisted coordination state when that state is shared across processes or operators
- any future ATM-owned JSON or JSONL file that can be rewritten by more than one ATM process, agent, or operator workflow
Required behavior:
- live-file replacement must use a temp-file + fsync + rename pattern or an equivalent same-filesystem atomic-replacement mechanism
- for files replaced via rename, the helper must fsync the parent directory after the rename whenever the platform allows directory-sync semantics, so successful return means both file contents and name publication are durably committed as far as the host platform can provide
- no live shared structured file may be truncated and rewritten in place
- mailbox locking does not replace atomic persistence; both are required for mailbox files
- temp-file + rename atomicity alone is not a source-unchanged compare-and-swap against non-cooperating writers, so ATM must not claim mailbox rewrite safety for concurrent Claude Code appends
- live inbox files under
-
REQ-CORE-PERSIST-ATOMIC-001AShared mutable file commits must use one of the documented mutation classes and the shared commit protocol.Required behavior:
read_onlypaths must not acquire mailbox/file locksread_possible_writepaths may do an initial unlocked read, but any actual commit must prove source freshness before replacing the live fileread_modify_writepaths must also prove source freshness before replacing the live file- acceptable freshness proofs are limited to:
- compare-and-swap against the exact earlier snapshot, or
- lock, reread current state, recompute, and then commit
- a stale-snapshot rename after late lock acquisition is forbidden even if the rename itself is atomic
Mailbox state is durable SQLite state; send, read, ack, clear, and missing-config notices persist it through the retained mailbox runtime.
-
REQ-CORE-PERSIST-ATOMIC-001BEvery shared mutable file family must have one documented write path and one owning helper boundary.Required behavior:
- mailbox file replacement must go through the mailbox atomic helper family
- shared generic state replacement must go through the shared persistence helper family
- new live structured files must not introduce bespoke
fs::write, truncate-and-rewrite, or ad hoc temp-file logic at individual call sites - if a file family needs special preconditions such as lock ordering or freshness validation, those preconditions must be enforced at the shared helper boundary or a single owner-layer wrapper around it
- the current owner-layer set is:
- mailbox compatibility surface:
mailbox::store::observe_source_files(...)for lock-free snapshots,mailbox::store::with_locked_source_files(...)for shared read/ack/clear lock+reload orchestration, andmailbox::store::commit_mailbox_state(...)/mailbox::store::commit_source_files(...)as the persistence leaf - mailbox state:
the retained SQLite mailbox runtime (
persist_message_record(...)andpersist_message_state(...)); no filesystem workflow sidecar exists - seen-state watermark:
read::seen_state::save_seen_watermark(...) - send-alert state:
send::alert_state::{register_missing_team_config_alert(...), clear_missing_team_config_alert(...), save(...), acquire_lock(...)} - team config:
team_admin::write_team_config(...) - task bucket and
.highwatermark:team_admin::restore::restore_task_state_from_backup(...) - restore marker and restore staging:
team_admin::restore::write_restore_marker(...),team_admin::restore::clear_restore_marker(...),team_admin::restore::prepare_restore_workspace(...), andteam_admin::restore::cleanup_restore_workspace(...)
- mailbox compatibility surface:
- command-layer code must not add a filesystem workflow-state mirror; SQLite remains the sole mailbox-state authority.
-
REQ-CORE-PERSIST-ATOMIC-001CATM must not claim rewrite safety for non-cooperating external writers.Required behavior:
- if a live file can be concurrently changed by a writer outside ATM’s lock
protocol, ATM must document whether that file is:
- read-only from ATM’s perspective, or
- a legacy compatibility surface with known overwrite risk, or
- protected by real freshness validation/CAS
- for Claude-owned inbox files, advisory lock correctness applies only to concurrent ATM writers
- ATM-local workflow state that requires stronger guarantees must move to an ATM-owned source-of-truth path rather than relying on full-file rewrite of the Claude-owned inbox surface
- if a live file can be concurrently changed by a writer outside ATM’s lock
protocol, ATM must document whether that file is:
-
REQ-CORE-PERSIST-ATOMIC-002Phase M must treat atomic persistence as a cross-cutting invariant, not a mailbox-only or restore-only rule.Required behavior:
- when Phase M touches a shared mutable structured file path, the implementation must either route that path through an existing atomic write helper or add one before modifying the file
- new shared mutable JSON/JSONL/state files introduced during Phase M must adopt the same atomic persistence contract immediately rather than deferring to a follow-on cleanup sprint
-
REQ-CORE-PERSIST-ATOMIC-003Atomic persistence helpers must be centralized and reused instead of duplicated ad hoc at call sites.Required behavior:
atm-coremust own the shared atomic persistence primitive used by mailbox, config, task-bucket, highwatermark, and shared coordination writers- mailbox writes continue using the mailbox atomic helper
- team-config writes continue using
write_team_config(...) - task-bucket / highwatermark / shared state writes added or touched by Phase M must use a documented helper with the same temp-file + rename semantics
- the Phase M audit must grep for direct
fs::write,File::create, or equivalent in-place rewrites of live shared mutable structured files and either remove them or document why the path is not in scope
The required shared commit protocol is:
- classify the operation as
read_only,read_possible_write, orread_modify_write - perform any unlocked observational read allowed by that class
- compute whether a write is necessary
- if no write is needed, return without locking
- if a write is needed, enter the owning write path for that file family
- prove source freshness by CAS or by lock + reread + recompute
- write the temp file, fsync, rename, and perform any required directory sync
The intentionally forbidden shape is:
- read old snapshot
- compute mutation from old snapshot
- acquire late lock
- rename blindly over a newer live file
-
REQ-CORE-MAILBOX-TEST-001Phase M follow-up coverage must include deterministic failure-path locking tests in addition to success-path no-deadlock tests.Required behavior:
- add bounded tests for lock contention timeout on the mutation commands that
use mailbox locking; for the follow-up sprint the explicit command coverage
list is
sendfor contention timeout,clearfor fail-closed discovery, andsendfor non-contention lock-error classification - add deterministic coverage for fail-closed source discovery when an origin inbox directory entry cannot be enumerated successfully
- add deterministic coverage for non-contention lock-path failures so they do
not regress into
MailboxLockTimeout
- add bounded tests for lock contention timeout on the mutation commands that
use mailbox locking; for the follow-up sprint the explicit command coverage
list is
-
REQ-CORE-MAILBOX-TEST-002Locking tests must use bounded, non-flaky construction that cannot hang indefinitely.Required behavior:
- use explicit timeout-based synchronization (
recv_timeout,wait_timeout, elapsed-time assertions with bounded slack) rather than open-ended thread joins or sleeps waiting for success - tests for directory-entry enumeration failure must use a deterministic seam or injected enumerator/fault source rather than permission tricks, racing deletes, or environment-sensitive filesystem behavior
- tests for non-contention lock errors must use a deterministic seam or injectable failure source rather than depending on platform-specific errno behavior
- tests that intentionally hold a lock must guarantee teardown via scoped guards/channels even when the assertion path fails
- crash-durability helper tests should verify sequencing and error propagation through deterministic seams; they must not rely on real crash simulation
- forbidden test patterns:
- open-ended polling waiting for "eventual" success
- indefinite
join()or blocking wait with no timeout - sleeps used as the primary correctness mechanism
- race-dependent stress loops expected to pass only "most of the time"
- use explicit timeout-based synchronization (
-
REQ-CORE-RESTORE-ATOMIC-001teams restoremust writeconfig.jsonas the last mutation step, only after all other restore mutations succeed.Rationale: ARCH-CR-002 —
team_admin.rs:372-400copies inboxes, restores tasks, recomputes highwatermark, then writes config. If the process dies between inbox copy and config write, the team has partially restored inbox files that do not match the config roster.Required behavior:
- restore planning and backup validation happen before the marker is written
- config.json is written last, after all inbox copies and task restores succeed
- a
.restore-in-progressmarker file is written to the team directory before mutation begins and removed after config is successfully fsynced - the config-last step must continue using the existing
write_team_config(...)atomic temp-file + rename pattern instead of introducing a second config persistence path - on next
atm teams restore, if a.restore-in-progressmarker exists, warn the operator and recommend re-running the restore atm doctormust check for stale.restore-in-progressmarkers and report them as findings with recovery guidance
-
REQ-CORE-RESTORE-ATOMIC-002Restored inbox files must be staged before being placed in the live inbox directory.Required behavior:
- inbox files from the backup must first be copied to
.restore-staging/inboxes/ - after all staging copies succeed, move staged files to the live inboxes
directory using
fs::renamewhere possible - on staging or move failure, clean up the staging directory and fail without writing config
- if stale staging already exists at restore start, the command must either clean it first or fail with a recovery message; it must never merge old and new staging contents implicitly
- inbox files from the backup must first be copied to
-
REQ-CORE-RESTORE-ATOMIC-003Stale restore-progress markers must have a fixed diagnostics contract.Required behavior:
atm doctormust report stale.restore-in-progressmarkers as warnings- the finding must not become a blocking error by default
- the finding must include recovery guidance telling the operator to rerun
atm teams restoreor remove the marker after manual verification
-
REQ-CORE-ERROR-DISPLAY-001AtmError::Displaymust remain concise and must not emit multi-KB backtrace output.Required behavior:
Displayrenders the human-readable message and recovery text only- captured backtraces remain available via Debug output and a dedicated
accessor on
AtmError
-
REQ-CORE-ERROR-DOC-001Every public function returningAtmResultorResult<_, AtmError>in the explicit Phase M audit inventory must have a# Errorsdocumentation section.Required behavior:
- the Phase M audit inventory must explicitly include:
mailbox/mod.rsmailbox/lock.rsread/mod.rsack/mod.rsclear/mod.rsteam_admin.rsdoctor/mod.rserror.rsconfig/mod.rshome.rssend/mod.rssend/input.rssend/file_policy.rsidentity/mod.rsif the consolidation lands there- any new public atomic/state helper introduced by Phase M
- each
# Errorssection must list theAtmErrorCodevariants the function can return - the implementation must audit the current public API surface instead of relying on a stale hard-coded function count
- the Phase M audit inventory must explicitly include:
-
REQ-CORE-ERROR-RECOVERY-001EveryAtmErrorconstruction site in the explicit Phase M audit inventory that represents an operator-actionable failure must use.with_recovery().Required behavior:
- Phase M must perform a grep-driven audit of remaining bare
AtmError::new(...),AtmError::mailbox_*,AtmError::file_policy(...), and similar operator-actionable construction sites in the explicit Phase M audit inventory - the audit must explicitly include bare operator-actionable sites in:
mailbox/mod.rsmailbox/lock.rsread/mod.rsack/mod.rsclear/mod.rsteam_admin.rsdoctor/mod.rsconfig/mod.rshome.rsaddress.rssend/mod.rssend/input.rssend/file_policy.rsidentity/mod.rsif new operator-facing errors are introduced there- any new M.1/M.2 helper that constructs
AtmError
- permission, timeout, missing-file, malformed-input, lock-contention, and operator-remediable configuration failures are always considered operator-actionable for this audit
- sites already covered by L.7/L.8 recovery work do not need duplicate edits
- internal invariant violations do not require recovery guidance
- Phase M must perform a grep-driven audit of remaining bare
-
REQ-CORE-IDENTITY-CONSOLIDATE-001The duplicatedresolve_actor_identityfunction must be consolidated into a single shared implementation.Required behavior:
- the identical helper currently present in
ack/mod.rs,clear/mod.rs, andread/mod.rsmust be moved toidentity/mod.rsaspub(crate)
- the identical helper currently present in
-
REQ-CORE-CONFIG-DOC-001The deprecated[atm].identityconfig key and legacy top-levelidentitykey must be documented in a# Deprecatedsection in the config module documentation.Required behavior:
- migration guidance: use
ATM_IDENTITYenvironment variable instead - reference
ATM_WARNING_IDENTITY_DRIFTerror code
- migration guidance: use
-
REQ-CORE-PANIC-DOC-001The panic path innormalize_json_numbermust be eliminated and documented.Required behavior:
normalize_json_number(...)must return the raw input string on exponent parse failure or unsupported exponent range instead of panicking- a library function must not panic on potentially untrusted input
The current SQLite/daemon architecture supersedes the mailbox-lock line as the target architecture for ATM
mail correctness. The REQ-CORE-MAILBOX-LOCK-* requirements remain
transitional compatibility constraints only for the interim file-based line.
The release-complete target is elimination of mailbox-lock dependence from ATM
mail correctness.
TaskStore is the sealed, backend-neutral task-ledger capability. SQLite owns
the tasks and append-only task_events tables; MessageWriteOrigin
distinguishes local task-bearing messages from peer receipts so only local
writer admission and atm task commands apply task transitions.
Task ledger shape (Phase BA):
tasksMUST havePRIMARY KEY (team, task_id).tasksMUST carry a unique index on(team, assignee)restricted tostate = 'active'.tasksMUST carry a queuepositioncolumn separate fromassigned_at;assigned_atrecords the current assignment and is reset by reassign/reopen, never by move.- Close MUST record a typed outcome (
completed | refused | cancelled).reassignedandreopenedare same-id event kinds produced byassign. tasksandtask_eventsrows MUST NOT be deleted; a task id that ever existed MUST always resolve.
-
REQ-CORE-RUNTIME-001ATM mail and team roster state must move to SQLite as the authoritative source of truth.Required behavior:
- SQLite is the durable source of truth for:
- message records
- read/unread state
- ack-required / acknowledged state
- clear/delete/message state
- task linkage and task metadata
- team roster
- Claude-owned inbox JSONL files are compatibility ingress/export surfaces, not ATM's authoritative durable mail store
config.jsonbecomes a roster-ingress source, not the durable roster truth
- SQLite is the durable source of truth for:
-
REQ-CORE-STORE-001The SQLite store must use one documented schema contract with stable keys, constraints, and indexes.Required behavior:
- the authoritative schema must define at least:
messages- one unified mutable message-state surface
- one canonical roster/member surface
inbox_ingest
message_keyis the canonical ATM durable message identitymessage_keyformat must be deterministic and typed by source family:atm:<ulid>for ATM-authored durable rowsext:<fingerprint>for imported external rows without ATM ids
- schema constraints must forbid duplicate authoritative identities
- schema changes are contract changes and require explicit user approval plus synchronized requirements, architecture, and boundary doc updates before implementation is accepted
- the schema must document the required lookup indexes for message lookup, task lookup, visibility projection, and ingest dedupe
- the authoritative schema must define at least:
-
REQ-CORE-STORE-002The SQLite store must enforce WAL and explicit transaction policy.Required behavior:
journal_mode = WALforeign_keys = ON- mutating ATM command flows must use explicit transactions
- no production mutating path may rely on implicit per-statement autocommit as the normal correctness model
-
REQ-CORE-STORE-003All database access must use the backend-neutral storage-trait contract.Required behavior:
- only
atm-storage-rusqlitemay importrusqlite, own schema/SQL, or expose concrete SQLite behavior - daemon, core, CLI, graft, and transport code must hold storage traits only
atm-runtimemay assemble selected backend trait objects but must not expose SQLite types or introduce a daemon-specific persistence trait- new backend selection occurs at composition without changing daemon, transport, CLI, or graft source
- replay/outbox/finalizer traits created solely for daemon transport state are forbidden
- only
-
REQ-CORE-INGEST-001Inbox/config ingest must use one owned contract for replay, backpressure, and degradation.Required behavior:
-
ingest must be idempotent
-
historical Claude inbox ingest tooling must accept the prior legal inbox container shape: one top-level JSON array document for each shared
.jsoninbox file -
parseable external rows must not be silently dropped
-
malformed external rows must emit structured diagnostics rather than panic
-
on the earlier compatibility line, legal Claude JSON-array inbox files stayed on the normal supported ingest path; repair/rebuild was reserved for malformed or unsupported mailbox state
-
backlog/slow-ingest conditions must surface through structured diagnostics or health findings rather than dropping records silently
-
roster/config ingest must apply one deterministic last-write-wins policy for replacing roster truth in SQLite
-
REQ-CORE-RUNTIME-003Crash recovery preserves committed local mailbox state. The daemon must not maintain a replay store, remote outbox, or retry state. -
REQ-CORE-RUNTIME-002Each durable roster member must have exactly one canonical live agent state, owned by the runtime's ephemeral master-roster record and never by SQLite.Required behavior:
- live status is runtime-owned state layered on the matching master-roster member in the write-through RAM roster mirror
- SQLite stores canonical roster membership and optional routing metadata,
but not lifecycle state, state revision, observation timestamps, current
process
pid, or session id - the ephemeral record contains one
RuntimeMemberState; latest observation source, typed observation availability,last_observation_attempt_at,last_observed_at,state_changed_at, pid, session, and a monotonically increasingRosterStateRevisionare metadata for that one state, not parallel state authorities - daemon memory caches the current
pidand session as observational metadata, not independent liveness-policy inputs - daemon runtime state includes
last_active_atfor each known active member entry - the shared protocol must expose typed heartbeat request/response DTOs for runtime state updates and observational pid continuity
- SQLite must not own live
last_active_at; it remains daemon-memory-only runtime state - durable roster fields and ephemeral live fields share one logical member record while retaining distinct persistence domains
- roster removal atomically removes its ephemeral state; re-addition starts
at
Unknownwith a new revision sequence - all live-state readers, including nudge scheduling, runtime health,
doctor,
teams, andmembers, read this record or a scoped projection of it; a second global member-state map is forbidden pidis transient runtime state rather than durable roster truth and must not be persisted in SQLite
Phase AJ implemented contract. The following clauses are reconciled with the merged AJ.1–AJ.8 source and named tests in ADR-045's evidence table.
-
REQ-CORE-RUNTIME-004Accepted runtime observations converge on the one ephemeral master-roster state. Runtime health is a projection, and the Section 15.4 nudge invariant is the only workflow policy that may consume the canonical state.Required behavior:
- an authenticated local heartbeat POST (including an external hook's
startup/active, idle, or stop event) and each successful Herdr
agent listpoll update the same in-memory member record - no other ingress or daemon side effect may synthesize a canonical state
update; the pre-cutover
ActivityObservationrequest field remains tolerated as transient compatibility metadata but cannot update this record - accepted runtime ingress order, not client-clock ordering, determines the
current state; every accepted state observation advances the per-member
RosterStateRevisionandlast_observed_at, including same-state evidence, whilestate_changed_atchanges only on a lifecycle edge - each defined session/pid value retains source and timestamp metadata; absent metadata is a no-op and cannot overwrite prior defined metadata; a trusted changed pid/session becomes the current metadata and is retained as diagnostic evidence only
- every actual pid/session mutation, including initial set, emits one structured info audit event with prior/new value, member, source, and time; no-op input emits no mutation event
- the existing heartbeat
pid_changedresponse field remains additive observation metadata: it is true only when a prior defined pid is replaced by a different defined pid; initial pid observation is audited but is not a replacement - heartbeat updates may not clear a defined session; a successful Herdr poll
maps working →
Active, idle/done →Idle, blocked →Blocked, and an unknown or absent member in the poll's covered roster scope →Unknown - a failed or incomplete Herdr poll is not a state observation: it preserves the prior state and revision, marks observation availability unavailable with an attempt timestamp, records a structured refresh diagnostic, and triggers no nudge
- a successful Herdr result is applied as one scoped batch so readers cannot observe a half-updated poll and the runtime does not clone the roster once per member
- state ingress for a member absent from the current master roster must not
auto-create membership; it returns or records existing
ATM_MEMBER_NOT_FOUNDwithout panic or state mutation Unknownmeans no trustworthy state observation;Offlinemeans an explicit heartbeat session-end observation. They are distinct values and must not be substituted for one another- heartbeat activity transitions to
Active,Idle, orOfflineonly from its explicit activity value - each cache member carries
state_changed_at; it changes only on a real lifecycle-state transition and is shown only for defined non-default state; human roster output renders its relative age while structured output keeps the absolute timestamp; repeated same-state evidence never resets it - external hook heartbeat mapping is startup/active →
ActiveToolUse/Active, idle →Idle, and stop →SessionEnded/Offline; ATM consumes, but does not install or emit, those hooks - identity change and malformed/suppressed observation are retained
anomalies, not roster lifecycle state. They must not reject ingress, emit
IdentityConflict, degrade readiness, alter cache eviction, or alter routing, notification, or delivery behavior. A future doctor phase may diagnose them. - local read/write request DTOs carry one optional
ActivityObservation(team, member, optional session/pid) for pre-cutover compatibility; it is never canonical state ingress, and remote HTTPS ingress clears it before shared dispatch - session, pid, and observation metadata must not drive routing, notification, retry, admission, or delivery logic
- the sole state-policy exception is the Section 15.4 nudge invariant: the
nudge path MUST consume the exact canonical
RuntimeMemberStateand MUST NOT consumePickerMemberStatus, aRuntimeHealthprojection, raw Herdr output, or heartbeat DTOs;Idlewith an open task MUST be nudged (rate-limited),ActiveMUST never be nudged or diverted, andBlockedorOfflineMUST escalate once with zero nudges - Herdr polling and heartbeat handlers update state only; neither ingress may inspect queues/tasks or emit a nudge directly
- any further exception requires an explicit requirement, ADR, boundary record, and test; telemetry never enters SQLite, durable roster state, mail rows, or message payloads
- roster-view commands display exact state plus availability/freshness from
the canonical record and may also show state age, defined pid, and shortened
session identifier; JSON preserves raw values. Human output may omit only
default
Unknown/Unobservedwith no session/pid, and must not collapse a missing/unavailable projection intoDead
- an authenticated local heartbeat POST (including an external hook's
startup/active, idle, or stop event) and each successful Herdr
-
REQ-CORE-DAEMON-001ATM must run exactly one daemon per OS account per host in the current architecture runtime. (Rand, 2026-09-08.) A container is its own host for this requirement. (Rand, 2026-09-08.)Required behavior:
- it must be impossible for two active ATM daemons for one OS account to run on one host at the same time (Rand, 2026-09-08.)
- daemon startup must fail deterministically when a live daemon already owns the same OS-account runtime (Rand, 2026-09-08.)
- stale daemon ownership artifacts may be cleaned up only when they are proven stale
- stale cleanup must never allow two live daemons for the same OS account (Rand, 2026-09-08.)
-
REQ-CORE-DAEMON-002The daemon must be a thin runtime wrapper rather than a unique business-logic layer.Required behavior:
- daemon responsibility is limited to runtime orchestration such as:
- transport listeners
- route selection
- live-status cache
- direct post-send emission routing when persistence succeeds
- the daemon must not become the only place where ATM mail semantics are implemented
- daemon responsibility is limited to runtime orchestration such as:
-
REQ-CORE-DAEMON-003Production ATM commands must connect to the daemon and auto-start it when absent.Required behavior:
- production CLI/runtime calls first attempt to connect to an already-running daemon
- if the daemon is absent, the production CLI/runtime path auto-starts it and retries once
- if the daemon remains unavailable after auto-start, ATM must fail with a clear recovery message rather than silently falling back to direct SQLite or inbox-file access
- in-process test harnesses may bypass the daemon only inside explicit test wiring, not in the production path
Satisfies:
REQ-P-RUNTIME-001
-
REQ-CORE-DAEMON-004The daemon must implement one documented graceful shutdown and runtime-control contract.Required behavior:
- each supported host platform must expose one typed graceful-shutdown control path before listeners begin accepting
- each supported host platform must expose one typed bounded reload/rescan control path without releasing singleton ownership
- Unix may satisfy this through
SIGINT/SIGTERM/SIGHUP - Windows may satisfy this through console or service-control equivalents
- graceful shutdown must stop accepts, drain inflight work, checkpoint WAL, and release singleton ownership in order
- Phase R transport remains one request per accepted connection, so the
documented
32per-connection inflight ceiling is satisfied by structure until framed multiplexing is introduced
REQ-DAEMON-PLATFORM-001is theatm-daemoncrate traceability record for the same-host daemon parity requirement carried by:REQ-P-PLATFORM-001REQ-P-PLATFORM-002- the same-host daemon portions of
REQ-CORE-DAEMON-003 - the same-host daemon portions of
REQ-CORE-DAEMON-004
REQ-DAEMON-PLATFORM-002is theatm-daemoncrate traceability record for constraining operating-system differences behind daemon-owned portability boundaries for:REQ-P-PLATFORM-002REQ-CORE-BOUNDARY-001
-
REQ-CORE-BOUNDARY-001Every subsystem must be behind a strict trait boundary for all external I/O.Required behavior:
- only the owning store subsystem may touch SQLite
- only the owning config-ingress subsystem may parse team
config.json - only the owning transport subsystem may touch sockets
- only the owning post-send/advisory subsystem may talk to agent processes
- no business logic may live in I/O adapter code
- no "just this one call site" bypasses are allowed
- I/O-owning boundary traits are sealed by default; opening a boundary for external implementation requires explicit architectural approval
- concrete I/O adapter types and constructors remain private unless a documented boundary contract requires wider visibility
- violation of any ownership rule above is a direct QA failure for the Phase Q implementation line
-
REQ-CORE-BOUNDARY-002Production runtime code must model fallible runtime behavior with discriminated error unions and explicitResultpropagation.Required behavior:
- fallible production paths must prefer typed error enums/unions over panic,
unwrap, orexpect - compile-time-visible error types must remain the primary enforcement mechanism for runtime failure handling
- panic is reserved for invariant corruption or explicitly unreachable code paths, not routine I/O, parse, transport, or store failures
- CLI, daemon, and core service layers must preserve structured error identity when translating between boundaries
- the
AtmErrorCoderegistry must not use wildcard or catch-all variants in place of specific codes - every public
AtmErrorCodemust document one recoverability class - the
AtmErrorCoderegistry is centralized and read-only from the perspective of feature/service code; subsystems consume codes from the registry and do not mint local alternatives - violation of these structured-error rules is a direct QA failure for the current SQLite/daemon implementation line
- fallible production paths must prefer typed error enums/unions over panic,
AK.6 status: REQ-CORE-TRANSPORT-002, -002A, -002B, -002B1, -002C, -002D, -003, -003B, -004, and -005A retain their historical requirement records for traceability, but their transport-specific TLS, authority, and outcome status is superseded by ADR-047. Alias/configuration semantics remain AK.3-owned, direct-delivery semantics remain AK.4-owned, and resend-cache semantics remain AK.5-owned.
-
REQ-CORE-TRANSPORT-001Phase AI must replace the local frame protocol with one HTTP daemon API with local and peer production ingress classes plus one test adapter.Required behavior:
- Unix same-host clients use HTTP over UDS through the shared
atm-daemon-clientfacade and may use HTTP over loopback TCP; consumers such asatm-graftmust not take a directinterprocessdependency. Windows same-host clients use HTTP over loopback TCP only - normal remote peers use HTTPS over TCP; the explicit daemon-only
plaintext-testsmoke profile is governed byREQ-CORE-TRANSPORT-002B1and cannot create a second HTTP route - all production adapters call one HTTP router and the same application handlers
- the stable initial resources are
/v1/atm/messages,/v1/atm/message/{message-id},/v1/atm/message/{message-id}/read, and/v1/atm/doctor; their typed route-specific schemas and methods are the versioned OpenAPI contract - the test adapter exercises the same router/handler contract without a live socket
- HTTP adapters perform decode, authentication, and response translation only; they must not perform SQLite mutation, acknowledgement mutation, recipient routing, or post-send emission
- Windows loopback TCP binds only a loopback address and requires a daemon created owner-readable endpoint record plus a 32-byte base64url local capability; Unix UDS uses owner-only endpoint permissions
- ingress authentication creates
AuthenticatedIngress::Localonly after the local capability or UDS ownership check, andAuthenticatedIngress::Peeronly after mTLS plus allowlist verification; adapters must not infer local/peer status from socket family or address - Unix/Windows parity requires equivalent local HTTP request/response tests: UDS plus loopback TCP on Unix and loopback TCP on Windows
- Unix clients select UDS by default.
ATM_LOCAL_TRANSPORT=tcpis the explicit, observable loopback-TCP parity/diagnostic mode; an unavailable UDS endpoint must fail rather than silently falling back to TCP
- Unix same-host clients use HTTP over UDS through the shared
-
REQ-CORE-TRANSPORT-001BRequest routing must live behind one explicit HTTP router and injectable typed application handlers.Required behavior:
- transport adapters hand authenticated HTTP requests to the router
- the router owns route selection only
- concrete request-family behavior lives in injectable handlers behind the dispatcher
- adding a route must not duplicate an existing handler or require adapter logic beyond decode + dispatch
- any violation of this dispatcher/handler rule is a direct QA failure for the current SQLite/daemon implementation line
-
REQ-CORE-TRANSPORT-001Ais historical only.Phase AD retires filesystem watch/reconcile from the accepted runtime. New transport or daemon work must not preserve or expand that retired subsystem.
- on the earlier compatibility line, the daemon implementation could use a bounded polling watch registry instead of OS-native filesystem subscriptions, and the watch lifecycle remained daemon-owned and long-lived rather than one-shot helper calls
- historical reconcile triggering supported debounce/coalesce so repeated identical requests did not fan out into duplicate import work
R.17had completed this lane as a daemon-owned polling watch registry, an ordered debounce/coalesce reconcile worker, and a queued notifier runtime; those lanes started and stopped only through the daemon composition root- the historical notifier lane used a bounded queue of
64events and failed closed with typed backpressure instead of silently buffering unbounded plugin traffic
-
REQ-CORE-TRANSPORT-002After AI.9, cross-host traffic must be daemon-to-daemon HTTPS only.Required behavior:
- native agent/plugin code talks only to the local daemon
- cross-host delivery happens only between daemons
- agent/member names and team names are path-segment-like identifiers, not free-form labels
- the only allowed characters in agent/member names and team names are ASCII
letters, ASCII digits,
-, and_ - agent/member names and team names must reject:
- path delimiters:
/and\ - traversal forms:
.and.. - reserved address delimiters:
.and: - whitespace
- wildcard or pattern characters that could be interpreted by current or
future parsers, including at minimum
*,?,[and]
- path delimiters:
- the canonical remote-send CLI form is
atm send <agent>@<team>.<host> ...; host qualification is part of the typed address grammar. Per ADR-040,agent@hostand--host <host>are CLI-only same-team aliases that normalize to that exact form before the existing request is constructed; they are not a second route - because team names cannot contain
., the inline form splits at the first.after@; the remainder is the host and may be a DNS name or IP address containing additional periods - one post-write router invokes the receiver hook after a newly persisted
inbound write (peer provenance or an empty destination host); it emits no
second hook for an idempotent duplicate. A host-qualified origin write
retains only the temporary peer wake-up until Phase AM deletion, including
localhostand the daemon's own advertised or bound IP address - local CLI HTTP, host-qualified same-host HTTP, and remote peer HTTP submit the same canonical write resource and request schema; TLS/authentication is adapter work before that resource, never a second write endpoint, persistence path, ACK path, or nudge path
- every canonical write orders idempotent persistence, optional receiver-side acknowledgement mutation, and exactly one post-write router dispatch: persist, then emit the steer nudge; queue-kind nudges defer emission until harness readiness (ADR-054); neither kind ever precedes persistence
- a destination host is consumed as an origin-side routing selector before an authenticated peer request reaches receiver-side routing; source host is durable provenance shown by read/nudge/ack projections
- the canonical local write may persist the sender's immutable outbound message record before post-write routing, but it must not create a local recipient-inbox row for a remote recipient or any remote-delivery queue
- when a host-qualified same-host peer receipt encounters that daemon's own
identical retained origin ULID, storage logs
peer_duplicate_write_skippedwith the ULID, both hosts,same_store_peer_receipt=true,database_write=skipped, anddelivery=continued; it skips the second database write without altering origin destination-host metadata; the duplicate emits neither a receiver hook nor another peer wake-up. A later ACK to that retained record derives its host-qualified reply target from the preserved origin destination metadata and still creates the ordinary canonical ACK write
-
REQ-CORE-TRANSPORT-002ACross-host HTTPS listener, local certificate, and peer-trust configuration must use durable storage-backed state rather than environment variables. SQLite is the initial backend behind that trait.Required behavior:
- the daemon reads enabled bind/advertise interfaces, certificate identity, and trusted peers from durable state
- CLI commands are the sole operator surface for adding, enabling, disabling, replacing, removing, and listing those records
- if no enabled interface rows exist, no cross-host listener binds
- environment variables must not configure cross-host networking or trust
-
REQ-CORE-TRANSPORT-002DA peer authority is one durable registered hostname, HTTPS port, and pinned certificate fingerprint.Required behavior:
- a canonical hostname target exact-matches one registered authority name;
its durable HTTPS port selects the endpoint. CLI input may omit the
terminal
.localonly as specified byREQ-P-ADDRESS-001, and must be canonicalized before request construction - a literal IP target is accepted only when a bounded fresh DNS/mDNS lookup of exactly one registered hostname contains that address
- resolved addresses are not stored in SQLite or another durable alias store
- zero or multiple matching registered names fail closed before TLS or route
- reverse DNS is forbidden; an IP-only registration never authorizes a name
- every registered hostname must be forward-resolvable by the peers that use it through ordinary DNS/DDNS or local-network mDNS. A changing VPN, Wi-Fi, Ethernet, DHCP, or VPS address is updated by the host's normal name-resolution mechanism, never by ATM reverse lookup or a SQLite IP alias
- several account-owned daemons may use the same current host IP only when
each authority has a distinct
(hostname, port)endpoint and certificate pin; an OS bind collision fails closed and must not select another port - trust add, replace, and revoke refresh the one live daemon's verifier atomically without starting a second daemon
- a canonical hostname target exact-matches one registered authority name;
its durable HTTPS port selects the endpoint. CLI input may omit the
terminal
-
REQ-CORE-TRANSPORT-002EOutbound peer dialing must resolve the registered hostname at connect time, dial only routable addresses in a fixed order inside the request budget, and remember a working answer briefly in process memory. Dial order, budget, cache, and diagnostics rules are defined by ADR-060; this requirement fixes the contract and ADR-060 is the design of record. Neither may drift without a superseding ADR.Required behavior:
- the operating-system resolver (DNS, DDNS, or mDNS for
.localnames) is the only address source; ATM performs no reverse lookup and stores no resolved address durably (REQ-CORE-TRANSPORT-002D) - an IPv6 answer that is link-local (
fe80::/10) and carries no interface scope is unroutable and is never dialed; IPv4 answers are dialed before routable IPv6 answers; no more than four addresses are dialed for one connect attempt - each address attempt is bounded by an even share of the remaining request budget across the addresses still untried, so one unresponsive address can never consume the budget a later reachable address needs
- a process-local address cache keyed by the canonical registered hostname
holds a working answer for at least five minutes.
rand-m5,rand-m5.local, and any ASCII-case variant address one cache entry; the.localsuffix never selects a different code path - a valid cache entry is found in well under one millisecond; when the cached addresses no longer connect, the name is resolved again and dialed again inside the same request budget, so a peer whose address changed while moving between wired, Wi-Fi, and VPN networks is reached without an error surfacing to the caller. A re-resolution taking milliseconds is acceptable; an unreachable peer leaves no cache entry behind
- timing contract (non-functional): a dial against cached addresses is bounded by the smaller of half the remaining budget and 500 ms, so with the 3 s server request budget at least about 2 s remains for the fresh lookup and dial. That covers an mDNS answer delayed by retransmits after a network change (about 2 s worst case); only a resolver that never answers, or a peer that is asleep or off the network, produces a caller-visible, named failure
- the dial rules, constants, and cache key normalization are locked by
ADR-060 and enforced by the
peer-dial-seamlint: name resolution and peer TCP dialing exist only inpeer_dial.rs, and the locked constants may change only together with a superseding ADR - the mTLS pooled connector and the
plaintext-testdirect connector (REQ-CORE-TRANSPORT-002B1) apply the same address ordering; there is no wire-security mode that dials an unfiltered answer - a connect failure is logged at
warnwith the peer and every attempted address and reason, and the caller-visible error names the attempted addresses and reasons without exposing message content, tokens, or raw configuration (#904)
- the operating-system resolver (DNS, DDNS, or mDNS for
-
REQ-CORE-TRANSPORT-002BCross-host inbound authorization must use mTLS and a durable deny-by-default exact peer allowlist before routing.Required behavior:
- inbound peers are rejected unless their declared stable host identity, configured HTTPS port, and pinned certificate fingerprint match one enabled record; the TCP source IP is routing information only
- wildcard, prefix/suffix, subnet-derived, and regex trust are forbidden
- rejection happens before router, mailbox, acknowledgement, or roster work
- doctor output must surface listener, certificate, and trust state without exposing private key material
-
REQ-CORE-TRANSPORT-002B1A daemon may run an explicit, process-local plaintext peer-wire profile only for debug/smoke diagnosis.Mode ownership and the layered-stream constraint are defined by ADR-047.
Required behavior:
- default and every normal release invocation use mTLS plus the exact peer allowlist; no TLS, certificate, or allowlist failure may fall back to plaintext
- only
atm-daemon --peer-wire-security plaintext-testenables plaintext; the setting is non-durable, not environment-driven, and a restart without that argument restores mTLS - plaintext-test uses the same HTTP resource,
WriteRequest, router, persistence, and post-write path as mTLS. It must not introduce a plaintext-only message shape, route, nudge, or acknowledgement path - plaintext-test does not authenticate or authorize a peer. A declared source-host is untrusted smoke provenance only and must not be presented as authenticated, used to authorize a recipient, or treated as production trust evidence
- doctor, retained logs, smoke JSON, and XHTML label the active wire-security mode. Plaintext-test evidence never satisfies mTLS/allowlist acceptance criteria
-
REQ-CORE-TRANSPORT-002CSame-host proof must use the ordinary remote-host contract and must not be implemented as a special loopback-only send mode.Required behavior:
- same-host transport proof uses the same daemon peer listener/send path as any other remote host proof
localhostand the host's own advertised or bound IP address are valid ordinary remote-host targets- the required same-host transport proof targets the daemon's advertised or
bound virtual-Ethernet IP over TCP; a
localhostrow is address-grammar coverage only and cannot substitute for that proof - same-host proof must not require a dedicated wire field, request flag, or special-case routing branch outside the normal remote-host classifier
- successful same-host rows do not by themselves authorize second-host release claims
-
REQ-CORE-TRANSPORT-003Cross-host transport owns no delivery state.AK.2 status: the worker-specific clauses below are historical. AK.2 removes the scheduler and every transient peer-work key; AK.4 and AK.5 replace delivery and optional resend semantics with their own requirements.
Required behavior:
- no replay store, outbox, retry queue, deferred receipt, remote acknowledgement state, or duplicate-delivery subsystem may exist
- after local admission, an unavailable peer records the typed asynchronous
peer_delivery_unconfirmedoutcome for the attempted immutable write; it does not relabel the already-returned local admission response as a transport error. A later ordinary canonical attempt reuses that identity - duplicate arrival is idempotent at storage by the existing message ULID;
an identical already-delivered remote duplicate has no side effect, while
the narrow same-host retained-origin receipt defined by
REQ-CORE-TRANSPORT-002logs its skipped write and continues the ordinary inbound recipient nudge without a second database write REQ-CORE-TRANSPORT-003Bis historical and separately authorized future work only; it supplies no scheduler, policy, queue, cursor, configuration, or compatibility path to the current implementation
-
REQ-CORE-TRANSPORT-003AHistorical; superseded byREQ-CORE-TRANSPORT-003B. It records the AI.28 ordered-coordinator contract and is not an active implementation requirement. Phase AK supersedes the remaining worker model.Required behavior:
- durable backend-neutral
PeerSyncPolicy.max_message_ageandmax_batch_messagescontrol the feature; zero age disables it by default and the batch cap defaults to 100 - an operator can enable policy and request a one-shot sync. Automatic work is signalled only by a locally persisted host-qualified write or an unconfirmed peer delivery; ordinary peer success does not create a probe loop or a second scheduler
- storage queries locally persisted outbound records for the exact peer newer than the configured age and returns their original ULID and immutable payload through a storage trait
- every selected record uses the ordinary canonical HTTPS write request; no receiver-specific replay path exists
- no outbox, replay store, retry queue, background monitor, checkpoint, cursor, receipt, retry budget, or per-message delivery state is allowed
- an exact duplicate ULID/payload is a no-op except for the
same-host-retained-origin receipt defined by
REQ-CORE-TRANSPORT-002, which logs a skipped write and continues its inbound nudge without a database write; same ULID with different immutable data returns a typed conflict, logs the discrepancy, preserves the original record, and has no side effect or panic - explicit sync runs one bounded pass through the same per-host coordinator as automatic recovery; it introduces no second transport or write route
- durable backend-neutral
-
REQ-CORE-TRANSPORT-004A remote write succeeds only after the remote daemon accepts the canonical write request.Required behavior:
- local admission alone is not remote success
- a failed HTTPS request may leave the already-persisted immutable local sender record, but creates no remote recipient row, delivery receipt, retry state, or sender-side acknowledgement mutation
- the receiving daemon validates the recipient against its own local roster
in the shared write handler; it never reads or preflights the sender host's
roster. A remote rejection returns the ordinary
AtmErrorresponse and leaves receiver mailbox state unchanged - acknowledgement is an ordinary canonical write with
acknowledges_message_idpopulated; its state transition occurs only in the receiver's shared write handler - the origin-created message ULID and all immutable fields are preserved on
the receiver; exact already-delivered remote duplicates do not repeat a
nudge or acknowledgement transition. The same-host retained-origin
receipt is the narrow exception defined by
REQ-CORE-TRANSPORT-002: it logs the skipped database write and continues the inbound nudge without a second record or peer re-delivery. A conflicting payload for the same ULID is a typed error
-
REQ-CORE-TRANSPORT-005The daemon runtime must use concrete timeout and capacity limits for transport/store/health operations.AK.2 status: the peer-worker deadline, job-cap, DNS, and custom-HTTPS listener clauses below are historical worker-model evidence, not current implementation requirements.
Required behavior:
- every local admission request has one absolute
RequestDeadline; local HTTP, router, dispatcher, validation, SQLite transaction, post-commit signal, and response consume only its remaining budget - after the admission response, a peer worker has one separate absolute
PEER_DELIVERY_WORKER_DEADLINE = 10sfor its full DNS/connect/TLS/request/ response attempt. Neither admission nor worker code may create nested or extended per-leg deadlines - SQLite
busy_timeout:5000ms - ingest batch processing slice:
2s - doctor health query deadline:
3s - max concurrent accepts:
64 - max per-connection inflight requests:
32 - ingest queue depth:
1024 - post-commit work queue depth:
256; global active peer jobs:64; active peer jobs per host:8. These are load/file-descriptor bounds, not FIFO controls; saturation coalesces a host rescan signal and never blocks or drops a committed admission - SQLite handle budget:
1..=4 - live status-cache cap:
4096 - saturation behavior must fail with typed errors or structured degradation, never silent drop
- outbound peer connections resolve/bind per attempted request so ordinary local interface changes do not require daemon restart
- inbound HTTPS listeners bound to wildcard/unspecified local addresses must survive ordinary interface rebinding without daemon restart
- if the configured listener bind address itself changes or disappears, the daemon must require bounded reload/rebind through the documented reload path and must surface degraded status until rebind succeeds
- HTTP request bodies are capped at
1_048_576bytes and rejected before decode; UDS, loopback TCP, and HTTPS shutdown stop accepts then drain or cancel tracked requests within the one documented daemon shutdown deadline
- every local admission request has one absolute
-
REQ-CORE-TRANSPORT-005AA remote write is confirmed only after the peer daemon returns canonical HTTP acceptance.AK.2 status: the asynchronous worker clauses below are historical until AK.4 restores direct delivery. Local admission remains durable and distinct from remote acceptance.
Required behavior:
- origin persistence is observable separately and is never labelled sent
- deadline, disconnect, or failed response after dispatch returns the typed
REMOTE_DELIVERY_UNCONFIRMEDerror, neverDAEMON_UNAVAILABLEwhen the local daemon accepted the request - local admission completes at the SQLite response boundary. Later bounded peer work is runtime-tracked independently of the closed local connection; it is not detached, because the scheduler owns cancellation, concurrency, and observability, but it is not cancelled by the completed admission request
- the possible remote side effect is resolved only by repeating the same immutable ULID through ordinary idempotent write handling
- daemon logs record
write_persisted,peer_delivery_confirmed, orpeer_delivery_unconfirmed; terminal handler/response-write failures are retained structured events
-
REQ-CORE-TRANSPORT-005BThe local daemon must admit and respond to at least 1,000 host-qualifiedsendrequests per second through the public ATM API.AK.2 status: worker signalling, DNS, connection, TLS, and remote receipt clauses below are historical. The local SQLite admission requirement and its isolated evidence remain active.
Required behavior:
- the SQLite transaction that durably persists the immutable origin record is the only synchronous operation on the admission-response path. Once it commits, the daemon returns the typed local admission response; peer-job signalling, DNS, connection, TLS, remote receipt, duplicate handling, acknowledgement source resolution/mutation and the acknowledgement reply insertion are the one atomic SQLite transaction; peer delivery, duplicate handling, nudge, and hook work run asynchronously
- pre-persistence admission validation uses only request syntax, provenance,
and identity rules. The sole post-persistence
PostWriteRouterremains the owner of local-versus-host-qualified routing and consults a daemon-owned reloadable runtime view, never a per-request config-file, peer-config/policy store, outbound-page, DNS, socket/TLS, hook, or nudge. An acknowledgement resolves its source, inserts its immutable reply, and conditionally marks the source acknowledged within one SQLite transaction; it has no application-layer source read before that transaction - the response proves only local admission. Remote acceptance remains the
separate asynchronous outcome defined by
REQ-CORE-TRANSPORT-005A; a local admission response must never claim remote delivery - distinct CLI/API write requests are independent. ATM makes no ordering promise between their delivery attempts, even when they target the same peer. Byte ordering is required only within one HTTP request/response exchange, and an acknowledgement is correlated solely by its immutable message ULID
- post-commit work uses a bounded, non-durable scheduler. It may hold
transient
HostName/message-ULID jobs and in-flight coalescing markers, but no payload, receipt, retry history, delivery result, or durable checkpoint. A restart drops that work and rebuilds eligibility from the immutable SQLite records and enabled peer policy - the throughput requirement applies while the destination peer is
unavailable as well as healthy. Release evidence runs ten consecutive
one-second admission intervals against one release-built daemon under the
dedicated disposable benchmark OS account required by
REQ-P-BENCHMARK-001;ATM_HOMEand any other path/config setting must not select its SQLite root. It must never run against a shared or interactive-production ATM store. Each interval has at least 1,000 accepted requests and responses. Mock routers, direct dispatcher calls, and disabled peer delivery do not satisfy this evidence - Unix release evidence records HTTP/UDS and loopback-TCP results separately; Windows records loopback TCP. Every record names its transport and uses identical public request/response semantics rather than omitting response handling on one transport
- local HTTP framing reads bounded chunks and retains over-read bytes for the next frame. It must not implement header delimiter discovery as one system read per byte
-
REQ-CORE-REPORT-001Durable verification evidence must be published as a deterministic static report site.Required behavior:
site/is the publish root;site/index.htmllinks to the generatedsite/reports/index.html- each recognized report has one HTML entry under
site/reports/and a same-named directory containing its JSON/XHTML evidence - every report envelope declares
schema_version,report_type,generated_at(UTC), a relative report HTML path, and the ADR-044-safe opaquehost_label; the initial report types arebenchmarkandfuzz - the repository report-index command regenerates the reports index after every successful or failed report artifact write; a check mode fails when a recognized envelope, report link, or generated index is stale
- the index groups report types and lists entries newest-first by UTC; benchmark run envelopes aggregate to their one benchmark HTML entry
- transient development views under
artifacts/viewmay link to reports but must not duplicate, copy, or become the source of durable evidence site/artifacts follow ADR-044's public-data classification; raw host, endpoint, identity, path, secret, and message data are rejected before publication
-
REQ-CORE-TRANSPORT-003BHistorical; superseded by Phase AK. It records the retired worker reconciliation contract. AK.2 deletes its policy, scheduler, and observability projection; AK.5 defines any later optional resend cache only after AK.4 proves direct delivery.Required behavior:
- policy selects a bounded send window and batch; zero window disables it
- the non-durable scheduler bounds global and per-host in-flight delivery jobs. It may coalesce duplicate in-flight ULIDs, but it makes no ordering promise across distinct messages and owns no durable delivery state
- a scheduler scan pages eligible exact-peer records through the storage trait and enqueues ordinary canonical writes. It has no batch request shape, recovery-only endpoint, or durable cursor; restart/next signal may safely rediscover an immutable ULID through idempotent recipient storage
- every newly persisted outbound write signals scheduling. A signal arriving during a scan or active work must cause a further eligibility scan before the host is considered idle; no write may be lost between scan completion and idle transition
- the post-write router has one
PeerDeliveryCoordinator::signal_after_persisthandoff for host-qualified writes. It signals bounded background work after local admission and never waits for DNS, a socket, TLS, remote receipt, or another message's delivery - first recovery attempt is no earlier than 60 seconds; later failures use exponential backoff capped at 15 minutes
- each individual peer job consumes the one
PEER_DELIVERY_WORKER_DEADLINE = 10sdefined byREQ-CORE-TRANSPORT-005; the scheduler never extends it with per-leg timeouts - recovery submits original ULIDs through normal HTTPS; no ping, outbox, cursor, receipt, payload cache, per-message attempt state, or alternate write route is allowed
- empty window, policy disable, or peer revoke stops scheduling
- retained events use stable typed codes and distinguish
peer_delivery_scheduled,peer_delivery_attempted,peer_delivery_confirmed,peer_delivery_unconfirmed, and terminalpeer_delivery_expired(eligible record aged beyond the configured window), without body or certificate material.peer_delivery_expiredis observability only: it creates no durable delivery state and cannot change the prior local admission result - doctor exposes a bounded, secret-free per-host link projection: quality, last success/failure, last typed error, next attempt, drain state, and bounded candidate count. It is observability only, not durable delivery state
-
REQ-CORE-COMPAT-001Claude inbox-append runtime behavior and the formercrates/atm-storage-claudebackend are retired from the accepted line.Required behavior:
- no retained production path may use Claude inbox
.jsonor.jsonlfiles for context injection or background ingress - if a retained Claude mailbox compatibility export helper survives temporarily, it must be explicit historical/obsolete-only scaffolding and must not define current send/read/post-send semantics
- the accepted line must not ship the former
atm-storage-claudecrate or its boundary records as a production backend - the shared backend contract remains required after Claude backend retirement; SQLite is one backend implementation and future SQL backend support remains an architectural requirement
- no retained production path may require watcher/reconcile observation of Claude mailbox files
- any surviving Claude mailbox documentation must be clearly historical and must not redefine current send/read semantics
- no retained production path may use Claude inbox
-
REQ-CORE-COMPAT-002Native agent/plugin traffic must use the daemon API, not Claude mailbox JSON.Required behavior:
- native agent/plugin delivery and notification uses the daemon API only
- thin-client surfaces such as graft align to the shared daemon/API contract rather than to a mailbox-JSON transport
-
REQ-CORE-GRAFT-001Graft is a thin embedded daemon client and bounded host-wake transport, not a second mailbox or host conversation subsystem.Required behavior:
- graft
send,read, andackuse the normal daemon API and durable ATM mailbox; graft has no direct SQLite, mailbox-file, or durable nudge queue - a graft receiver has one explicit live owner per canonical receiver identity; competing live activation fails without replacing the current endpoint, and process death permits safe reclaim
- a host session identifier such as ADR-037
ChatIdis preserved as an opaque profile/session binding, never parsed into a second ATM transport or conversation manager - a host integration must deliver a nudge through its documented safe between-tool-call mechanism. A Hermes integration uses non-interrupting steer delivery, not normal user-message ingress
- after host restart/reconnect, a host integration may issue one bounded advisory wake-up derived from durable unread/pending-ack counts. It must not replay mail, mutate mail, create a retry loop, or persist recovery state outside the ATM mailbox
- graft
Nudge taxonomy (Phase AQ). "nudge" is the umbrella term for any post-delivery recipient notification. "steer" (steer nudge) is the immediate kind, emitted right after durable persistence — this is the only kind that existed before Phase AQ, so legacy text below that says plain "nudge" for the immediate case means steer nudge. "queue" (queue nudge) is the deferred kind, introduced by Phase AQ, delivered when the recipient harness is ready. Persistence ordering is unchanged for both kinds: neither kind ever precedes durable persistence.
-
REQ-CORE-COMPAT-003Post-send behavior must use one direct post-persist emitter seam.Required behavior:
atm sendpersists the message to durable ATM stateatm ackpersists the reply to durable ATM state- after successful persistence, ATM emits the steer nudge only when the recipient exposes that capability; queue-kind nudges defer emission until harness readiness (ADR-054), and neither kind ever precedes persistence
- the shipped default post-send path is the built-in in-process implementation
- teams may override any subset of the eleven built-in nudge template bodies
through host-scoped, team-keyed ATM-managed override rows resolved through
the storage-neutral
NudgeTemplateOverrideStorecontract - emission failure must be logged and surfaced as a sender-visible warning
- post-send emission must not redefine send success after persistence
- the authoritative Phase AD release smoke lane for post-send behavior must
prove exactly these closure cases:
- external hook success
- external hook partial failure
- built-in fallback
- override reset-to-default after a prior stored override row
- explicit disable behavior when that retained state is supported
-
REQ-CORE-COMPAT-004Post-send capability resolution must not depend on caller working directory or retired mailbox/config side channels.Required behavior:
- running
atm sendfrom another repository or working directory must not silently change whether post-send emission is attempted - hook configuration lookup must follow the sender's canonical roster
home_dirmetadata
- running
-
authoritative
recipient_pane_id, when known, must come from canonical ATM roster state rather than from rediscovering live pane routing through local mailbox files -
live pane routing for built-in tmux nudge must not depend on committed
.atm.tomltmux_pane_idvalues -
retained repo-local compatibility helpers may use only authoritative
recipient_pane_idpayload/roster data or an explicit operator-provided--pane; they must not revive committed.atm.tomlpane lookup -
REQ-CORE-COMPAT-005NotificationSink, queued notifier runtimes (retired internal worker queue — unrelated to queue-kind nudges), and typed delivery-plan execution are not the governing send-path contract.Required behavior:
- post-send ownership must remain a direct emitter seam on the send/ack path
- if notification logging is retained, it must be a direct append at the event site rather than a daemon worker/runtime subsystem
- no retained send/ack contract may require
DeliveryPlan,ReplyDeliveryPlan, orNotificationSink
-
REQ-CORE-LOCK-RETIRE-001ATM mail correctness must stop depending on mailbox lock artifacts.Required behavior:
- mailbox locks may remain only as transitional compatibility machinery for the interim file-based line
- the current SQLite/daemon architecture must eliminate mailbox-lock dependence from normal ATM mail correctness
- completion of the current architecture requires that stale lock artifacts can no longer wedge normal ATM mail flows
-
REQ-CORE-TEST-RUNTIME-001Core target daemon-runtime behavior must be testable without daemon process spawning.Required behavior:
- daemon spawning is not part of the core test strategy
- core service behavior must be testable in-process
- transport/watch/runtime logic must be testable with fakes or in-process harnesses
- no default test path may depend on daemon process lifecycle to validate ATM mail correctness
- there is no approved test-only daemon launch path for ordinary ATM correctness tests
- ordinary tests must not depend on socket publication timing, retry sleeps, parent-process environment mutation, or auto-start side effects
-
REQ-CORE-OBS-002The target daemon-runtime architecture must keep structured observability first-class at both CLI and daemon boundaries.Required behavior:
- CLI entry, daemon runtime, transport, ingest/export, and service
orchestration must emit structured events through the shared
sc-observabilityboundary - observability wiring must remain layered:
atmowns CLI bootstrap and presentation concernsatm-daemonowns daemon/runtime event emissionatm-coreowns ATM event and error models above the shared boundary- native plugins may emit plugin-local diagnostics, but daemon-owned runtime/transport/store/ingest events must be emitted by the daemon and not delegated to plugin code
- observability must not be implemented as ad hoc println/debug output in production paths
- CLI entry, daemon runtime, transport, ingest/export, and service
orchestration must emit structured events through the shared
-
REQ-CORE-DOCTOR-002The target daemon runtime must expose a daemon health query interface consumable byatm doctor.Required behavior:
atm doctorremains a CLI command- daemon/runtime health information must be obtained through an explicit daemon-facing interface rather than direct CLI inspection of private daemon state
- the health interface must be able to report at least:
- daemon reachability
- daemon liveness and readiness as separate dimensions
- singleton ownership status
- live status-cache summary
- ingest backlog / degraded-ingest state when present
- SQLite open/readiness state
-
REQ-CORE-QA-RUNTIME-001Every QA pass for the current runtime must verify the daemon and boundary invariants.Required behavior:
- impossible to run two active ATM daemons for one OS account on one host (Rand, 2026-09-08.)
- daemon singleton remains per OS account per host rather than socket-path-local (Rand, 2026-09-08.)
- daemon unavailability after one auto-start attempt fails clearly with no hidden direct I/O fallback
- every subsystem performs external I/O only through its owning trait boundary
- production error handling uses typed
Result/error-enum boundaries instead of panic/unwrap for fallible runtime paths - daemon/runtime code remains thin and does not accumulate business logic
- daemon spawning is not the test strategy
- banned daemon-spawn helpers and launch shortcuts are absent from the default test path
- SQLite remains the source of truth for mail and roster
- live agent status remains runtime-owned state
- structured
sc-observabilitycoverage remains present at both CLI and daemon layers - any retained historical Claude compatibility export remains a compatibility projection only and is never the ATM-owned runtime truth
- runtime roster truth remains the canonical ATM roster rather than
config.json config.jsonparsing remains limited to the approved ingress/comparison allowlist rather than generic retained command/runtime access
-
REQ-P-LINT-POSTMORTEM-001Mechanically-detectable postmortem finding families must become repository lint or CI gates rather than recurring QA rediscoveries.Required behavior:
atm-coreis the proving ground for new postmortem lint rules; a rule lands here first, is tuned against the live codebase, and is migrated to standalonesc-lintonly after the rule shape is stable and demonstrably reusable- reusable Rust/static-analysis rules must be implemented against the
embedded
crates/sc-lint-*surface on theatm-corebranch before any upstream migration - ATM-specific repository policy rules may stay as
.just/orscripts/lints when the semantics are tied to ATM-only names, documents, or review process state - the default
just lintpath remains the required development gate for any new postmortem rule that is cheap and deterministic enough for normal local use
Family-specific obligations:
- ungated
std::os::uniximports in production paths must fail the portability lint unless they are already protected by an approved Unix-only boundary #[cfg_attr(not(unix), allow(dead_code))]must not be used as a portability suppressor in production code- duplicated raw semantic literals in non-test Rust code must fail the ATM identity-literal gate unless they come from a canonical constant or an explicit allow-list
- raw
"team-lead"role-name literals are the first mandatory case and must fail everywhere except the canonical role-definition source - fixed
thread::sleep(...)in ordinary Rust test code must fail a test-hygiene gate unless the file or callsite is explicitly part of the narrow daemon-runtime suite - mechanically-detectable unbounded wait patterns in the narrow same-host daemon/runtime suites must move into repository lint or analyzer gates once the rule shape is proven deterministic enough for default local use
PORT-004must reject productionstd::os::uniximports that are not protected by an approved Unix-only boundaryPORT-005must reject#[cfg_attr(not(unix), allow(dead_code))]when used as a portability suppressor in production codeSCB-RUNTIME-001must reject bare productionCondvar::wait(...)SCB-RUNTIME-002must reject productionwait_timeout*calls whoseWaitTimeoutResultis discarded or stored only in underscore bindingsSCB-CONFIG-001must reject production direct teamconfig.jsonroster reads outside the explicit allowlist; duringZ.7the only approved survivor ishydrate_roster_from_team_config_once_at_startup_if_empty(...)withsunset_sprint = "Z.8"SCB-CONFIG-002must reject generic runtimeload_team_config(...)helper use from retained command/runtime paths and boundary adapter chainsSCB-CONFIG-003must reject Claude send paths that consultconfig.jsonbefore the durable ATM write has succeeded- bare
Condvar::wait(...)in non-test production code must fail a runtime liveness gate;wait_timeout(...)andwait_timeout_while(...)remain the required production shapes - triage Turtle records must not report contradictory aggregate and terminal state fields in the same record
- any scoped exclusions for the semantic-literal gate must stay narrow and explicit; they must not exempt ordinary production code wholesale