These are st2's load-bearing guarantees. Every entry names a green proof; changes to lifecycle, materialization, messaging, DING, or presence must preserve them.
| Invariant | Guarantee | Proof |
|---|---|---|
| R11 control-plane replacement safety | Normal stop or SIGKILL of st2 up, atomic binary replacement, and control-plane restart leave live PTY and exec agents usable with the same PID and creation identity. The replacement adopts each survivor exactly once, launches only genuinely missing work, and never duplicates a survivor; only explicit teardown kills an agent. |
tests/nomad_survival.rs::normal_stop_and_binary_replacement_adopt_exec_unchanged_without_duplicate; tests/nomad_survival.rs::forced_kill_and_binary_replacement_adopt_exec_unchanged_without_duplicate; tests/nomad_survival.rs::normal_stop_and_binary_replacement_adopt_pty_unchanged_without_duplicate; tests/nomad_survival.rs::forced_kill_and_binary_replacement_adopt_pty_unchanged_without_duplicate; tests/nomad_survival.rs::explicit_teardown_kills_exec_but_plain_stop_does_not; tests/nomad_survival.rs::explicit_teardown_kills_pty_but_plain_stop_does_not |
| Runner-owned task identity | Every launched PTY and exec task receives ST_AGENT=<resolved-host>.<identity> from reconciliation. Missing values are injected, exact authored matches are accepted, conflicts refuse before workspace or runner mutation, and restart/replay preserves the value. Every managed PTY carries the actor path, while only the canonical agent PTY carries the agent compatibility role. |
tests/run.rs::runner_owned_identity_injects_compact_and_explicit_task_omissions_and_accepts_a_match; tests/run.rs::runner_owned_identity_conflict_refuses_before_materialization_or_runner_access; tests/run.rs::runner_owned_identity_metadata_is_form_equivalent_and_role_scoped; tests/run.rs::runner_owned_identity_is_rederived_for_dead_task_replay; src/run/tests.rs::build_run_command_persists_the_complete_managed_environment_before_the_command |
| R23 fail-closed diagnostic inventory | st2 tasks --json joins a semantically stable desired-catalog observation to exact PTY and conservatively proven exec generations without writes or reconciliation. Task absence intent, typed agent desired state/rationale, and the owning supervisor's park decision remain distinct: a believable park and structured recovery argv carrying the exact canonical catalog and selected host accompany the unmodified runtime observation as a complete known fault, while an unbelievable marker is indeterminate. Missing roots are positively absent without creation; declaration drift, malformed, unreadable, duplicate, PID-reused, or incomplete evidence makes the typed envelope incomplete and exits non-zero. The observational boundary does not serialize catalog writers or authorize cutover. |
src/task_inventory.rs::stable_wire_shape_maps_pty_exec_explicit_default_and_ignores_foreign_host; src/task_inventory.rs::complete_missing_runtime_is_absent_but_incomplete_is_indeterminate; src/exec_backend.rs::legacy_plain_pid_is_observed_without_rewriting_it; src/exec_backend.rs::record_older_than_current_process_generation_cannot_be_promoted; src/task_inventory.rs::a_parked_task_reports_its_fault_alongside_a_truthful_runtime_state; src/task_inventory.rs::an_unbelievable_park_marker_makes_the_envelope_incomplete; tests/task_inventory_cli.rs::projected_recovery_targets_its_exact_catalog_and_host_despite_ambient_defaults; tests/task_inventory_cli.rs::suspended_agent_projects_task_absence_and_agent_rationale_separately; tests/task_inventory_cli.rs |
| Transport-decoupled lifecycle | Each task is isolated from a supervisor/transport process-group or cgroup cascade. | tests/transport_isolation.rs; tests/transport_isolation_macos.rs |
| Clean exec teardown | Killing an exec task reaps its whole process group. | tests/exec_backend.rs::exec_kill_reaps_the_whole_process_group_not_just_the_leader |
| Bounded restart diagnostics | Relaunching an exec task preserves the just-finished log as one prior generation while bounding retained diagnostics to current plus prior. Final retirement removes the PID and both logs. | tests/exec_backend.rs::exec_restart_reap_keeps_bounded_diagnostics_and_final_remove_cleans_them; tests/run.rs::up_once_finally_removes_dead_retired_tasks_without_restarting_them |
| Derived companion lifecycle | A generated DING or launched stream starts only with an eligible canonical agent, is suppressed or stopped while that target is held, suspended, unavailable, retired, or terminally parked, and remains coupled without changing explicit sibling task behavior. A stream lowers its authored adapter launch directly, parks and surfaces independently, and neither makes an otherwise empty agent runnable nor claims a delivery transport. Stream authoring is serialized, authority-scoped, source-preserving, and fail-closed for Nix ownership and invalid declarations. Healthy compact startup launches companions in one pass, and targeted reconciliation holds a missing generated companion rather than broadening to its agent. | crates/agent-spec/tests/discovery.rs::streams_are_typed_and_only_launched_streams_lower_to_derived_exec_tasks; crates/agent-spec/tests/discovery.rs::stream_names_launches_and_task_collisions_fail_closed; src/agent_author.rs::stream_add_supports_external_command_and_argv_and_external_remove_is_idempotent; src/agent_author.rs::stream_authoring_enforces_authority_nix_ownership_and_canonical_validation; tests/run.rs::fresh_compact_agent_launches_with_its_derived_ding; tests/run.rs::held_adopt_only_compact_agent_stops_its_live_derived_ding; tests/run.rs::parked_compact_agent_stops_its_live_derived_ding; tests/run.rs::retired_compact_agent_stops_agent_and_derived_ding; tests/run.rs::selected_missing_derived_ding_is_held_without_broadening_to_its_agent; tests/run.rs::fresh_compact_agent_launches_with_its_derived_stream; tests/run.rs::retired_compact_agent_stops_agent_and_derived_stream; tests/run.rs::suspended_compact_agent_stops_its_derived_stream_without_touching_a_sibling; tests/run.rs::held_adopt_only_compact_agent_stops_its_live_derived_stream; tests/run.rs::a_crash_looping_stream_parks_and_surfaces_without_disturbing_its_agent; tests/run.rs::parked_compact_agent_stops_its_live_derived_stream; tests/run.rs::selected_missing_derived_stream_is_held_without_broadening_to_its_agent; tests/run.rs::a_launched_stream_alone_is_rejected_before_reconciliation; tests/run.rs::a_stream_does_not_claim_a_delivery_transport |
| Exactly-once-safe native bus | Messages use stable <unix-ms>-<rand6>.md files. An archive filename is a durable receipt that shadows and cleans restored inbox replicas and makes repeated archive cleanup idempotent. |
src/message.rs::filename_grammar; src/message.rs::archive_receipt_suppresses_and_idempotently_cleans_a_restored_inbox_copy; tests/message.rs |
| Durable native delivery ownership | Each native harness keeps one monotone evidence chain per inbox filename in delivery-ledger.json. Ownership is persisted before the transport call, so a crash between attempt and evidence reopens as an ambiguous attempt that holds instead of re-sending. Evidence only advances, and only to a phase the harness can actually observe: Codex proves transport acceptance and consumption, OpenCode transport acceptance and durable read-back, and a record claiming any other phase is refused. Only an authoritative negative receipt re-authorizes a retry. Settlement stays with the recipient: an entry is released only on a phase its profile releases, and is otherwise reconciled away when the message leaves the unread set. A foreign, malformed, or self-inconsistent record quarantines delivery without rewriting it, and the persisted ledger is owner-only with no temp residue. A delivery attempt an earlier release left behind is carried forward rather than re-sent: the pre-ledger delivery-state.json record is translated exactly once, when no ledger file exists, into an asserted phase that holds the delivery and authorizes no transport until this build observes evidence of its own. |
src/delivery_ledger.rs::tests::begin_persists_attempted_before_transport; src/delivery_ledger.rs::tests::positive_evidence_never_downgrades; src/delivery_ledger.rs::tests::profiles_accept_only_evidence_their_harness_can_produce; src/delivery_ledger.rs::tests::negative_receipt_is_the_only_retry_authority; src/delivery_ledger.rs::tests::opencode_persistence_holds_until_archive; src/delivery_ledger.rs::tests::foreign_or_malformed_state_quarantines_without_rewriting; src/delivery_ledger.rs::tests::every_entry_must_validate_its_own_correlation; src/delivery_ledger.rs::tests::the_persisted_ledger_is_owner_only_and_leaves_no_temp_residue; src/delivery_ledger.rs::tests::an_asserted_phase_suppresses_a_duplicate_and_authorizes_no_transport; src/migrations/delivery_state/mod.rs::tests::a_carried_forward_attempt_is_an_assertion_that_authorizes_no_transport; src/migrations/delivery_state/mod.rs::tests::translation_happens_once_and_leaves_the_old_record_in_place; src/migrations/delivery_state/mod.rs::tests::deletion_trigger_absent_old_record_makes_this_module_a_no_op |
| Idempotent service requests | A declared non-agent service principal publishes one exact JSON request per caller-supplied idempotency key to a canonical Agent Spec inbox. Concurrent or crash-replayed publication reuses the reserved filename; conflicting key reuse fails. The typed reply routes to the principal's canonical inbox without an Agent Spec identity or orphan mailbox. | tests/request_cli.rs::stable_request_key_atomically_deduplicates_one_canonical_agent_message; tests/request_cli.rs::concurrent_replays_publish_exactly_one_request; tests/request_cli.rs::typed_reply_routes_to_the_principal_and_status_is_a_tagged_json_union; tests/request_cli.rs::request_api_rejects_agent_impersonation_and_unknown_flat_principals |
| Bounded idempotent stream ingress | A running agent accepts events only for a declared stream. Within the retained 128-receipt ring, concurrent or crash-replayed (stream, event-id) publication reuses one canonical filename, conflicting content fails, and supersession archives only the matching keyed predecessor or the stream-wide head through ordinary archive semantics. State remains bounded and honestly treats an identity evicted from the ring as new without searching inbox or archive history. Events do not write the Sent ledger and DING marks them as stream work. |
tests/event_e2e.rs::stable_event_identity_publishes_exactly_one_canonical_message; tests/event_e2e.rs::concurrent_replays_publish_exactly_one_event; tests/event_e2e.rs::conflicting_reuse_and_undeclared_or_suspended_ingress_fail_closed; tests/event_e2e.rs::supersede_collapses_only_the_matching_key_and_preserves_archive_receipts; tests/event_e2e.rs::keyless_supersede_replaces_the_stream_wide_head; tests/event_e2e.rs::crash_replay_honors_an_archive_receipt_and_never_restores_the_inbox_copy; tests/event_e2e.rs::subject_frontmatter_injection_is_refused_before_any_write; tests/event_e2e.rs::stream_state_is_bounded_and_forgets_only_beyond_its_honest_horizon; tests/event_e2e.rs::event_emit_cli_returns_a_stable_json_receipt_and_ding_marks_the_record; tests/stream_authoring_cli.rs::a_direct_adapter_launch_executes_the_exact_event_cli_contract; tests/run.rs::suspend_and_resume_relaunch_the_agent_and_stream_together |
| Fail-closed observed native DING | Each unread message becomes one normalized [DING] frame. Fresh delivery records ownership, then preserves the one combined bracketed-paste, 0.5 second delay, and Return transaction. PTY and Return success are transport only: Delivered additionally requires adapter classification of the expected notice text in a submitted-prompt or queued-message pattern while the lowest live composer is empty or an accepted idle placeholder. Retry never re-pastes and may send one bare Return only after two adjacent RetainedSafe observations. A maintained adapter's positive NotRetained observation releases only an already archived staged head; unread, blocked, timed-out, errored, unknown, and unrecognized states retain ownership and later FIFO work remains blocked. Ownership prevents duplicate paste across command failures, receipt ambiguity, archive races, and restart adoption without letting a vanished archived head block FIFO indefinitely. Startup backlog otherwise becomes one generic recovery DING; new arrivals remain FIFO; busy delivers immediately; only fresh dnd defers. |
src/ding/tests.rs::poke_text_normalizes_and_bounds_untrusted_fields; src/ding/tests.rs::malicious_controls_cannot_escape_the_single_paste_frame; src/ding/tests.rs::pty_delivery_uses_face607_delay_order_and_seconds; src/ding/tests.rs::maintained_composer_classifiers_require_exact_idle_state; src/ding/tests.rs::successful_transport_with_retained_or_unproven_pixels_is_not_delivered; src/ding/tests.rs::ambiguous_transport_receipt_and_retry_errors_retain_staged_ownership; src/ding/tests.rs::adapter_recognized_notice_with_an_empty_live_composer_is_a_positive_receipt; src/ding/tests.rs::staged_retry_submits_only_retained_safe_and_requires_a_receipt; src/ding/tests.rs::staged_retry_keeps_unproven_and_retained_blocked_owned; src/ding/tests.rs::staged_ownership_survives_archive_and_never_repastes; src/ding/tests.rs::archived_not_retained_releases_fifo_without_repasting_owned_notice; src/ding/tests.rs::unread_not_retained_keeps_fifo_ownership_without_repasting; src/ding/tests.rs::pty_commands_have_a_real_outer_timeout; src/ding/tests.rs::session_watch_has_startup_grace_debounce_and_live_reset; src/ding/tests.rs::new_arrivals_is_fifo_and_archive_receipts_prevent_reding; src/ding/tests.rs::pending_delivery_ignores_busy_but_respects_fresh_dnd_archive_and_retry; src/ding/tests.rs::startup_recovery_notice_retries_in_memory; src/ding/tests.rs::startup_backlog_gets_one_generic_recovery_then_new_arrivals_poke |
| Mutation-only filesystem wakeups | Supervisor and DING filesystem watchers ignore read/open access events and wake early only for create, modify, rename, or remove events. Their own catalog and inbox reads therefore cannot bypass the bounded timer cadence or form a Linux inotify CPU loop. | src/watch.rs::only_mutations_wake_watch_loops; src/watch.rs::linux_reads_are_silent_but_real_mutations_wake; src/ding/tests.rs::idle_ding_does_not_spin_on_its_own_inbox_reads; src/run/tests.rs::idle_supervisor_does_not_spin_on_its_own_catalog_reads |
| Prompt catalog convergence | A resident catalog supervisor observes cooperative transaction commits through a constant-cost catalog-generation watcher and authorized direct Agent Spec publication through an independent declaration watcher. Both channels coalesce into the serialized loop without waiting for the periodic audit interval; failure of one leaves the other and the timer fallback available. Runtime/control noise remains excluded. | src/watch.rs::catalog_generation_commit_wakes_catalog_watch; src/watch.rs::atomic_agent_bundle_publication_wakes_production_shaped_catalog; src/run/tests.rs::supervisor_wakes_and_launches_a_new_direct_declaration; src/run/tests.rs::failed_watch_installation_keeps_supervisor_on_timer_cadence |
| Bounded DING PTY probe churn | An unsafe or active composer retains its FIFO notice but deferred delivery retries use a bounded backoff, so each inbox poll cannot spawn another short-lived PTY probe. | src/ding/tests.rs::deferred_delivery_backoff_bounds_short_lived_pty_attempts |
| Agent-declared presence discipline | The shipped bus contract requires agents to declare busy before executing work, use available only while yielding or ready, and reserve dnd for an explicit hold. Both native harnesses materialize that contract. Busy remains observable but does not suppress DING; fresh dnd is the only delivery gate. |
tests/native_only.rs::clean_path_executes_the_maintained_native_authoring_guide; src/ding/tests.rs::pending_delivery_ignores_busy_but_respects_fresh_dnd_archive_and_retry |
| Stable roster JSON | st2 agents --json [--enrich] preserves field names, order, null handling, presence, typed desired state and rationale, the retirement compatibility projection, opaque declared Resource descriptors, origin-timed activity, inbox counts, and the appended immutable id, mutable address, nullable busAddress, observedState, driverDiagnostic, and context objects. Declared presence, desired lifecycle, observed harness state, native-driver degradation, and harness context are independent axes: none is derived from another; a missing observed or context record is null, while missing diagnostic evidence is explicitly absent, never healthy. |
src/agents.rs::agents_json_has_stable_wire_shape; src/agents.rs::agents_json_preserves_opaque_declared_resource_descriptors; src/agents.rs::observed_state_joins_declared_presence_without_touching_either; src/agents.rs::driver_diagnostic_wire_exposes_failure_and_evidence_age_without_identity_payloads; src/agents.rs::context_is_a_fourth_axis_that_survives_an_indeterminate_observed_state; tests/status_agents.rs::roster_json_and_human_output_distinguish_retirement_from_presence; tests/status_agents.rs::roster_keeps_presence_separate_from_suspended_desired_state; tests/status_agents.rs::roster_uses_version_1_origin_time_for_last_activity; tests/status_agents.rs::roster_joins_a_real_context_record_independently_of_observed_state |
| Agent-declared presence | Refresh preserves non-DND declared status and advances the version 1 heartbeat. A missing status starts as available. Legacy DND migrates without renewing its hold. Version 1 DND is not refreshed. Stale, malformed, or implausibly future heartbeats read as unknown. The outer Codex, Claude, and pi session wrappers own a five-minute heartbeat while their provider remains alive. |
src/status.rs::refresh_preserves_value_and_changes_heartbeat_bytes; src/status.rs::refresh_upgrades_legacy_dnd_without_renewing_the_hold; src/status.rs::refresh_missing_writes_available_default; src/status.rs::version_1_staleness_and_future_skew_are_bounded; src/status.rs::malformed_versioned_record_is_unknown_without_mtime_fallback; src/claude_session.rs::idle_provider_refreshes_presence_without_mcp_input; src/pi_session.rs::idle_pi_provider_refreshes_presence_without_channel_input; src/codex_app_server/tests.rs::inbox_fallback_does_not_write_a_fifteen_second_presence_heartbeat |
| Scoped delivery-input wakeups | Native delivery pumps watch only their inputs: the agent's resources/inbox subtree and its status file. Runtime records written beside them by the pump's own process group — presence temp siblings, the harness-state and harness-context records with their locks and staged siblings, stream state — never wake delivery, so a producer that writes on every turn boundary cannot pump its own delivery loop. |
src/watch.rs::delivery_watcher_ignores_runtime_records_but_wakes_on_inbox_and_status |
| Observed harness state discipline | The harness-state record is written only by the owning session's driver processes — wrapper, channel, or hooks — serialized by a cross-process lock and coalesced against the on-disk record, atomically and byte-distinct on every write that lands, with freshness from its embedded timestamp and never file mtime; restating an unchanged state touches the record only when the refresh cadence is due. unknown is derived, never written: staleness, future skew, malformation, an unsupported schema, and a provably dead pty session each read as unknown with a distinct reason; an indeterminate liveness probe downgrades nothing; a missing record is no observation rather than unknown; no absence derives a definite state. A writer that loses sight of its harness stops heartbeating instead of refreshing a state it cannot see, and a predecessor session's record is never re-stamped. A reaped provider yields a terminal ended record carrying its real exit — written before the stop path's SIGKILL escalation and rewritten from the escalation cover when a grace-window reap observes the real status, proven against the real wrapper binaries of both stop implementations — and never a live state. |
src/harness_state.rs::unknown_state_is_derived_and_cannot_be_written; src/harness_state.rs::malformed_record_is_unknown_without_mtime_fallback; src/harness_state.rs::staleness_and_future_skew_derive_unknown_with_distinct_reasons; src/harness_state.rs::a_dead_session_reads_unknown_even_while_fresh_but_ended_survives; src/harness_state.rs::every_landed_write_is_byte_distinct_and_fresh_restatements_do_not_write; src/harness_state.rs::a_chatty_producer_restating_its_state_causes_zero_writes; src/harness_state.rs::concurrent_writers_defer_to_the_on_disk_record_not_their_cache; src/harness_state.rs::a_predecessor_sessions_record_is_never_heartbeat_eligible; src/harness_state.rs::missing_record_reads_as_none_not_unknown; src/codex_app_server/tests.rs::pump_publishes_observations_and_stops_heartbeating_on_evidence_loss; src/claude_session.rs::a_provider_killed_mid_turn_reads_ended_rather_than_active; src/claude_session.rs::a_clean_provider_exit_writes_the_terminal_record; tests/harness_state_teardown.rs::stop_escalation_writes_the_terminal_record_before_sigkill; tests/harness_state_teardown.rs::opencode_stop_escalation_writes_the_cover_record_before_sigkill; tests/harness_state_teardown.rs::opencode_graceful_stop_records_the_real_reaped_exit |
| Harness context discipline | The numeric harness-context record is a sibling of harness-state and shares none of its ownership machinery: incarnation is provenance and never a fence, and a straggler's write lands rather than being refused. A reading is written only when it enters a different bucket of usedPercent, when a compaction edge arrives, when proven Claude account-window exhaustion changes, or when the record is older than the heartbeat, so a chatty producer cannot inflate the write rate; Codex account-window occupancy does not classify availability without its separate credit metadata. Every landed write is byte-distinct, and freshness comes from the record's own observedAtMs, never file mtime. A withheld value is carried as null and never fabricated from zero, the previous reading, or a division st2 could have done itself, and a reading above the window is carried unclamped. There is no unknown on this axis: past the horizon the reading is returned marked stale with its age, surviving every derivation the categorical record makes, while an unparseable record, a foreign schema, an uninterpretable harness, and an untrustworthy clock each read as absent. The relaunch claim removes the record, and Doctor's high-reading and stale-record lines are advisory in both directions and never change its exit status. Where a producer ships, its numerator is the harness's own and is pinned to the build it was measured on: pi's tokens is the last assistant message's totalTokens and omp's is its prompt figure, so neither harness's arithmetic can be published under the other's tag, and a harness bump that changes a number's meaning without changing its shape fails a fixture rather than shipping. A harness that positively reports it does not know its own occupancy is forwarded as withheld in the same write as the compaction edge that emptied it, a compaction edge whose harness names no reason yields unknown rather than an invented word, and a durable count the producer cannot read narrows to st2 counting edges rather than losing them. |
src/harness_context.rs::a_reading_inside_the_written_bucket_does_not_write_and_a_crossing_does; src/harness_context.rs::claude_rate_limit_exhaustion_and_reset_crossings_land_inside_one_usage_bucket; src/harness_context.rs::codex_account_window_exhaustion_does_not_prove_the_runtime_is_blocked; src/harness_context.rs::a_withheld_percent_has_no_bucket_and_only_a_compaction_or_heartbeat_writes; src/harness_context.rs::a_record_older_than_the_heartbeat_is_rewritten_and_every_landed_write_is_byte_distinct; src/harness_context.rs::a_compaction_always_lands_with_its_trigger_and_may_carry_a_durable_count; src/harness_context.rs::withheld_values_are_null_and_are_never_fabricated; src/harness_context.rs::a_reading_above_the_window_is_carried_unclamped; src/harness_context.rs::a_stale_reading_is_returned_with_its_age_rather_than_derived_away; src/harness_context.rs::freshness_comes_from_the_record_bytes_and_never_from_file_mtime; src/harness_context.rs::additive_fields_decode_but_foreign_schema_and_harness_read_as_nothing; src/harness_context.rs::a_record_beyond_the_future_skew_bound_reads_as_nothing; src/harness_context.rs::a_straggler_lands_and_is_visible_as_provenance_rather_than_being_refused; src/harness_state.rs::the_relaunch_claim_removes_the_harness_context_record; src/agents.rs::context_is_a_fourth_axis_that_survives_an_indeterminate_observed_state; tests/doctor.rs::harness_context_doctor_lines_are_advisory_and_never_change_the_exit_status; src/pi_channel.rs::the_pi_0_84_2_fixture_pins_total_tokens_as_the_numerator; src/pi_channel.rs::the_omp_18_0_9_fixture_pins_prompt_input_as_the_numerator; src/pi_channel.rs::a_pi_compaction_withholds_the_reading_it_emptied_in_the_same_write; src/pi_channel.rs::an_omp_compaction_yields_unknown_because_the_event_names_no_reason; src/pi_channel.rs::an_unreadable_durable_count_degrades_to_counting_edges_not_to_losing_them; src/pi_channel.rs::context_frames_decode_conservatively_or_not_at_all; src/pi_channel.rs::the_measured_pi_release_is_the_one_the_extension_gate_pins; src/omp_session.rs::the_measured_context_builds_are_admitted_by_this_gate |
| Replicated-path discipline | st2 pins the exact driver-record names it expects the replication transport's include list to carry — harness-state and harness-context — and both readers derive their paths from that list, so a rename cannot silently stop replication in a repository that does not own the list. A harness-context write stages in the catalog control plane, outside the complete agents identity namespace, after validating canonical ancestry and same-filesystem atomic rename. Current-catalog identity walkers overlook only an exact legacy .harness-context.tmp-<numeric-pid>-<numeric-counter> regular non-symlink host child and leave it untouched; directories, symlinks, special files, generic dotfiles, near misses, and prepared topology remain strict. harness-state still stages beside itself, pending its own change; the shared write helper takes the staging directory as an argument precisely because the two answer this differently. |
src/harness_context.rs::the_replicated_driver_record_names_are_pinned; src/harness_context.rs::writes_stage_in_catalog_control_and_clean_up_after_success_or_failure; src/catalog_transaction.rs::current_projection_ignores_and_preserves_an_exact_legacy_staging_file; src/catalog_transaction.rs::current_projection_rejects_legacy_shape_type_confusion_and_near_misses; src/message.rs::transition_addressability_ignores_and_preserves_exact_legacy_staging_files; src/message.rs::transition_addressability_rejects_legacy_type_confusion_and_near_misses; tests/catalog_apply.rs::snapshot_ignores_and_preserves_an_exact_legacy_harness_context_staging_file |
| Status-line slot chaining | Claude's statusLine is a single slot whose winning declaration replaces the others outright, and .claude/settings.local.json — the file st2 materializes — wins over every other settings file. So st2's entry there is a tee: it records the reading and then invokes the operator's own renderer, resolved from $ST_CLAUDE_STATUSLINE_RENDERER and then ~/.claude/statusline-renderer.json, first hit wins and never both. Where no renderer resolves, and wherever a resolved renderer fails, the tee writes NOTHING to stdout and puts its diagnostic on stderr: the payload is machine JSON, so echoing it into the slot is strictly worse for the operator than a blank row. Recording is unaffected by which arm runs. |
tests/claude_statusline.rs::the_tee_records_the_reading_and_hands_the_same_payload_to_the_env_renderer; tests/claude_statusline.rs::the_operator_file_supplies_the_renderer_when_no_variable_does; tests/claude_statusline.rs::the_variable_wins_over_the_file_and_the_file_is_never_also_run; tests/claude_statusline.rs::with_no_renderer_the_tee_renders_nothing_rather_than_the_raw_payload; tests/claude_statusline.rs::a_recording_failure_still_renders_the_status_line; tests/claude_statusline.rs::a_renderer_that_exits_non_zero_leaves_stdout_empty; tests/claude_statusline.rs::a_renderer_file_that_is_not_executable_leaves_stdout_empty; tests/claude_statusline.rs::without_st2_on_path_the_script_drains_stdin_and_renders_nothing; tests/claude_statusline.rs::the_rendered_registration_names_the_chaining_tee_and_carries_the_whole_slot |
| Version-pinned producer arithmetic | Every harness-context producer publishes the number its own harness means by that number, and each one is pinned to the exact build the meaning was measured on: Claude's integer percent over total_input_tokens and context_window_size (2.1.250), Codex's window with the 12,000 baseline subtracted from both sides (codex-cli 0.151.0), pi's last assistant totalTokens and omp's prompt-only input (0.84.2 and 18.0.9), and OpenCode's last non-summary assistant total over the providers window (1.18.25). The failure this bounds is a harness bump that changes what a key means without changing its shape, which no type gate and no round-trip assertion can see, so each fixture decodes a verbatim capture, asserts the version literally, and asserts the wrong numerator the same capture would yield. |
src/claude_session.rs::a_mid_session_statusline_payload_yields_claudes_own_triple; src/claude_session.rs::a_pre_turn_statusline_payload_withholds_rather_than_reporting_zero; src/codex_app_server/tests.rs::codex_context_recomputes_the_captured_reading_and_pins_its_verified_version; src/pi_channel.rs::the_pi_0_84_2_fixture_pins_total_tokens_as_the_numerator; src/pi_channel.rs::the_omp_18_0_9_fixture_pins_prompt_input_as_the_numerator; src/opencode_session.rs::captured_opencode_turns_publish_the_assistant_total_over_the_providers_window |
| Native-driver diagnostics are bounded and recoverable | One additive-tolerant st2.driver-diagnostic.v1 snapshot exposes the earliest failing native-driver boundary through closed driver/stage/reason/source/support vocabulary and origin-timed evidence age. Absent, malformed, foreign, unknown, and wrongly paired records never read healthy. Stage recovery clears only its own failure and removes the record after final recovery without changing prompt submission, retry, read-back, or archive semantics. A rejected provider credential is published on its own providerAuth stage from the harness's own typed turn result — never from provider prose, and never from an auth-class flag a capacity, policy, or retry class also sets — outranking the delivery and read-back symptoms it causes while the four producer gates outrank it; only a turn that reached its ordinary end clears it, and no credential knowledge or remedy text enters st2. Roster and Doctor share the typed reader/repair policy — Doctor advising on absence only for a driver that publishes a boundary result on every launch; telemetry labels contain only bounded driver/stage/reason/source/support/outcome values, never versions, identities, prompts, messages, or paths. |
src/driver_diagnostic.rs::every_stage_reason_and_source_has_bounded_wire_vocabulary; src/driver_diagnostic.rs::additive_fields_decode_but_malformed_foreign_and_unknown_records_are_indeterminate; src/driver_diagnostic.rs::recovery_clears_only_its_stage_and_reveals_the_next_failure; src/driver_diagnostic.rs::a_credential_rejection_is_evidence_only_from_a_typed_turn_result; src/driver_diagnostic.rs::a_rejected_credential_outranks_its_symptoms_but_not_the_producer_gates; src/claude_session.rs::stop_failure_classifies_only_the_credential_class_as_provider_auth; src/claude_session.rs::a_rejected_claude_credential_stands_until_a_turn_reaches_its_ordinary_end; src/codex_app_server/tests.rs::a_rejected_codex_credential_reads_provider_auth_while_a_quota_failure_does_not; src/codex_app_server/tests.rs::protocol_schema_gate_requires_the_distinct_credential_and_quota_error_words; src/opencode_session.rs::delivery_and_read_back_boundaries_publish_and_clear_diagnostics_without_changing_retry; src/metrics.rs::driver_diagnostic_metric_attributes_are_exactly_the_bounded_axes; tests/doctor.rs::native_driver_diagnostic_roster_and_doctor_agree_and_recovery_clears; src/pi_channel.rs::only_omps_own_credential_class_becomes_provider_auth; src/pi_channel.rs::a_rejected_omp_credential_stands_until_a_turn_reaches_its_ordinary_end; src/pi_channel.rs::the_pi_channel_publishes_no_credential_verdict; tests/doctor.rs::a_rejected_provider_credential_advises_while_absence_stays_silent |
| Retirement health | A retired declaration is healthy only after every declared task ID is absent. Any live or dead declared task record reports incomplete retirement; retired declarations do not require presence. Live declarations retain their existing task and presence checks. | tests/doctor.rs::retired_declaration_is_healthy_when_tasks_and_presence_are_absent; tests/doctor.rs::retired_declaration_is_unhealthy_while_a_declared_task_is_alive; tests/doctor.rs::retired_declaration_is_unhealthy_while_a_dead_task_record_remains |
| Suspension health | A suspended declaration is healthy when no declared task is live and every retained dead record is explicitly keep-pinned. It requires no presence, but this weaker result never proves retirement. Resume preserves ordinary keep and adopt-only policy. | tests/doctor.rs::suspended_declaration_is_healthy_when_tasks_are_absent_without_presence; tests/doctor.rs::suspended_declaration_distinguishes_live_dead_keep_and_dead_nonkeep; tests/reconcile.rs::resuming_uses_ordinary_reconcile_and_does_not_override_keep |
| Archival leaves the live catalog | st2 catalog archive moves a retired identity's whole directory — declaration and resources/ byte-identical — under the catalog control plane's .st2/archive, where discovery structurally cannot reach it, and records one tombstone row in st2 catalog graph --json. Eligibility fails closed: a running or suspended declaration, any surviving live or dead declared task record, and any remaining declaration that still names the identity as supervisor each refuse before anything moves, and --dry-run proves the plan without creating the archive root. An archived directory with no readable tombstone is unexplained control-plane state that makes the graph incomplete. st2 catalog unarchive is the exact reverse move. |
tests/catalog_archive.rs::archive_moves_a_retired_seat_out_of_discovery_with_its_resources_byte_identical; tests/catalog_archive.rs::archive_refuses_a_running_or_suspended_declaration; tests/catalog_archive.rs::archive_refuses_while_any_declared_task_record_survives; tests/catalog_archive.rs::archive_refuses_an_identity_another_declaration_still_names_as_supervisor; tests/catalog_archive.rs::dry_run_reports_the_plan_and_changes_nothing; tests/catalog_archive.rs::an_archived_directory_without_a_tombstone_makes_the_graph_incomplete; tests/catalog_archive.rs::unarchive_restores_the_identity_byte_identically_and_clears_its_tombstone |
| Retirement archives itself after its grace period | Each st2 up reconcile pass archives the local seats whose retirement outlived the catalog's archive-after (default 7d; "0" disables the step), through the same fail-closed eligibility as the verb and at most 25 seats per pass. It never queues for the authoring lock: a contended lock skips the step. The grace period is measured from the supervisor's first observation of the retirement, recorded under .st2/retired-observed.json and never in the spec, and that ledger is reconciled to exactly the currently retired seats — so un-retiring drops the row and re-retiring serves a fresh grace period. |
tests/supervisor_auto_archive.rs::a_pass_records_a_fresh_retirement_and_archives_nothing; tests/supervisor_auto_archive.rs::a_retirement_older_than_the_grace_period_is_archived_with_a_tombstone; tests/supervisor_auto_archive.rs::archive_after_zero_disables_the_pass_entirely; tests/supervisor_auto_archive.rs::a_pass_archives_at_most_twenty_five_seats_and_drains_the_rest_next_pass; tests/supervisor_auto_archive.rs::a_contended_authoring_lock_skips_the_pass_instead_of_blocking_it; tests/supervisor_auto_archive.rs::un_retiring_a_seat_inside_the_grace_period_restarts_its_clock |
| Crash loops surface | A task parked by a fail-mode restart policy notifies its supervisor once over the bus. | tests/run.rs::surface_crash_loop_notifies_the_supervisor_over_the_bus |
| Parked tasks are visible and individually recoverable | A parked task is reported alongside an unmodified runtime observation as a complete known fault; only an unbelievable marker fails closed. Park markers and unpark requests share the exact canonical catalog-folder plus host ownership scope, and the projected recovery argv carries both axes, so same-host supervisors cannot see, delete, consume, or advertise recovery into each other's channels even for the same task ID. A projected park whose supervisor generation is gone is positively not parked. An explicit per-task unpark clears that task's park and spent budget so it is launchable again and stays recovered past interval, releases no other parked task, restarts no healthy peer, and restores the agent's derived DING. |
src/flapping.rs::unpark_restores_a_launchable_task_not_just_a_cleared_flag; src/flapping.rs::unpark_is_per_task_and_reports_whether_it_changed_anything; src/park.rs::same_host_supervisors_isolate_markers_and_requests_by_catalog; src/park.rs::a_marker_from_a_dead_supervisor_reads_as_not_parked; src/park.rs::published_parks_are_readable_and_clear_when_the_task_recovers; src/park.rs::an_unbelievable_marker_is_indeterminate_not_absent; src/park.rs::a_request_is_consumed_exactly_once; src/task_inventory.rs::a_parked_task_reports_its_fault_alongside_a_truthful_runtime_state; src/task_inventory.rs::an_unbelievable_park_marker_makes_the_envelope_incomplete; tests/task_inventory_cli.rs::projected_recovery_targets_its_exact_catalog_and_host_despite_ambient_defaults; tests/run.rs::an_operator_recovers_one_parked_task_without_disturbing_a_healthy_peer; tests/run.rs::an_unpark_request_for_a_task_that_is_not_parked_says_so |
| Tracked workspaces fail closed | Materialization simulates content operations before writing and refuses a real change to any Git-tracked target. Byte-identical tracked, untracked, and non-Git targets retain useful behavior. | tests/materialize.rs::every_content_directive_refuses_to_change_a_tracked_target_before_any_write; tests/materialize.rs::byte_identical_tracked_target_is_allowed_without_modification; tests/materialize.rs::untracked_and_non_git_targets_remain_materializable |
| Native flat root | Without an authored override, catalog tasks, eval messaging, shell helpers, and DING all use the catalog itself as ST_ROOT; no nested bus directory is synthesized. |
src/eval_run.rs::bus_root_expands_st_root_else_defaults; tests/eval_run_e2e.rs::st2_eval_runs_a_benign_folder_to_a_pass_verdict; tests/pty.rs |
| Resource observation is state-first, atomic, and fenced | ABI-3 periodic publication and demanded Published results reuse one bounded Publication payload and one host acceptance, digest, relevance, typed-fact, and catch-up core; the host never trusts a runtime digest or observation timestamp. Demand reaches only a resident runtime that explicitly declares capability "demand". Every Observe carries a positive watermark and the exact owner, binding, and registration, and exactly one matching Unchanged, Failed, or Published atomic result closes it. One outstanding dispatch plus one latest trailing watermark coalesces bursts without losing in-flight arrivals. Backpressure retains queued demand, replacement fences stale output, restart and provider failure settle honestly, and client disconnect or wait expiry never cancels accepted work. |
tests/resource_profile_supervisor_e2e.rs::production_demand_jobs_coalesce_queue_disconnect_and_fence_generation; tests/resource_profile_supervisor_e2e.rs::durable_admission_retains_the_256_request_boundary_without_a_runtime; tests/resource_profile_supervisor_e2e.rs::production_component_preserves_resync_filter_catch_up_and_scope_isolation; tests/resource_profile_supervisor_e2e.rs::supervisor_spawns_vista_capability_and_preserves_stable_snapshot; tests/agent_resource.rs::refresh_cli_reports_exact_receipts_and_wait_expiry_keeps_the_request; src/resource_observe.rs::tests::receipt_evidence_shape_matches_atomic_results |
| Atomic resource proposal publication | Every changed resource publication is one host-owned compare-and-swap fenced by binding generation, state revision, and prior carrier digest. A persistent cross-process lock admits at most one proposal from the same prior. The content-derived proposal ID binds the accepted carrier digest and semantic outbox envelope; the durable intent becomes eligible only with the exact canonical carrier, then folds into one authoritative catch-up state. A pre-carrier crash exposes old state, a post-carrier crash catches up on restart, and retry after a lost acknowledgement returns the durable receipt without another transition. Ordinary reconciliation fails closed on out-of-band divergence after a committed intent; only an explicit generation-advance recovery may re-adopt the canonical carrier or its absence while invalidating the old intent and fence. | src/resource_profile.rs::tests::atomic_publication_fences_races_and_survives_crash_restarts; src/resource_profile.rs::tests::generation_advance_explicitly_recovers_diverged_or_missing_carrier |
| Pass progress is independent of publication | A reconcile pass completes whether or not resync publication is making progress. Publication runs on its own thread, so a pass's per-seat watch-set handshakes never wait for a catalog lock, a stream lock, or a refused publication. One publication is outstanding per subscription at a time, and a queued publication is dropped when its subscription is deactivated or removed by a refresh, so nothing is published to a seat the pass has already decided receives no events. | src/run/tests.rs::reconcile_pass_completes_while_a_resync_publication_is_blocked; src/resync/tests.rs::a_flush_never_hands_off_a_subscription_whose_publication_is_outstanding; src/resync/tests.rs::deactivation_drops_only_that_recipients_queued_publication; src/resync/tests.rs::a_refresh_drops_a_queued_publication_for_a_subscription_it_removed |
| Terminal refusals are classified, not retried | A refused resync publication is classified by what could admit it later. A recipient that is declared but not running parks its reservation: it is attempted once, captures and schedules nothing while parked, and re-arms with its exact reserved bytes when a refresh carries that recipient again. An ambiguous recipient, a foreign owning host, and an undeclared stream are permanently refused: the reservation is dropped and the carrier baseline advances so the same transition is not re-captured. Everything else, including an absent declaration, stays retryable. | src/resync/tests.rs::a_not_running_recipient_parks_its_reservation_and_is_attempted_once; src/resync/tests.rs::a_parked_reservation_re_arms_and_replays_when_its_recipient_runs_again; src/resync/tests.rs::a_permanently_refused_reservation_is_dropped_and_not_recaptured; src/resync/tests.rs::refusals_are_classified_by_what_could_admit_them_later |
| Unbindable session sockets fail at admission | Host-scoped validation rejects a pty task whose session socket path would exceed the portable 104-byte sun_path bound, because pty refuses that bind and the task can then never spawn. The bound is derived from the pty root resolved for the selected host rather than a fixed identity length, only that host's pty tasks are judged against it, and the diagnostic names the resolved path and the byte overage. A park whose cause is that same unbindable path does not advise st2 unpark, which would relaunch into the identical failure. |
tests/validate.rs::an_unbindable_session_socket_path_is_rejected_at_admission; tests/validate.rs::another_hosts_long_identity_is_not_judged_against_this_hosts_pty_root; tests/validate.rs::a_long_exec_task_id_is_not_a_socket_path_issue; src/run/tests.rs::session_socket_overage_is_derived_from_the_resolved_root; tests/run.rs::a_structurally_unrecoverable_park_does_not_advise_unpark |
| Marker-matched declaration authority | Every st2 write path that rewrites a declaration carrying meta { managed-by "nix" } admits it only from a caller asserting exactly the marker that declaration carries: the lifecycle verb and the compare-and-swap agent publish both refuse a mismatched marker, an unmarked subject, an unresolvable multi-marker declaration, and an empty or padded assertion before any write, while presentation, address, stream, and Resource authoring refuse a Nix-owned declaration outright. Publication reads that authority from the incumbent, so creating a declaration and republishing byte-identical bytes stay open — neither replaces bytes another writer owns. A marker-matched edit rewrites nothing but the lifecycle line and is admitted through the same full-catalog gate as a compare-and-swap publication of the same bytes, so it cannot commit a declaration the catalog would reject. |
src/agent_author.rs::marker_matched_lifecycle_authority_is_exact_and_source_preserving; src/agent_author.rs::marker_matched_retirement_refuses_a_candidate_admission_would_reject; tests/agent_desired_state.rs::cli_managed_by_authority_retires_a_projected_seat_and_refuses_every_inexact_claim; tests/agent_publish.rs::publication_honours_the_incumbent_ownership_marker; tests/agent_publish.rs::publication_refuses_an_unresolvable_owner_and_repairs_unreadable_bytes |
| Proof references resolve | Every qualified test named in this table exists in its named source file, so stale invariant claims fail the suite instead of silently surviving a refactor. | tests/invariants.rs::qualified_proof_references_resolve |