Skip to content

v0.10.0

Choose a tag to compare

@treygoff24 treygoff24 released this 02 Oct 04:04
· 34 commits to main since this release

Added

  • post participant describe [--model <text>] [--effort <text>] [--cwd <abs-path>] [--clear]
    lets a session record the model, reasoning effort, and working directory it
    runs under. The values are stored as an optional runtime object on the
    participant record and shown in participant show, participant list, and
    who. It is silent (no channel event, no mail), counts as activity like
    participant touch, and older post binaries read records that carry it.

  • participant describe also records --repo, --branch, --title,
    --role interactive|child|headless, --parent <participant-id>,
    --state working|idle, --pane, and --harness-session. --unset <field>
    (repeatable) removes one field, and --ended ends the session and drops the
    runtime. participant end also drops the runtime, so an ended session is no
    longer a peer to find.

  • post who --live [--role <role>] [--repo <basename-or-path>] lists the peers
    you can message now: a live watch or armed doorbell and activity within the
    last 10 minutes. Text is one line each (<pfp> <name|id> · repo@branch · title · state · age); --json gives the full records. who participants
    now carry the profile name and pfp.

  • post send --to accepts a live participant's profile name or
    repo:<basename-or-path>. An exact id, participant:<id>, a room, and a
    lineage keep winning over a name. Zero live matches is unknown_recipient
    and several is ambiguous_recipient with details.candidates; both exit 65
    and send nothing. The receipt carries resolved {id, name?, via}.
    chat --emote --at takes the same names and repo: among live members.

  • post profile set --name refuses a name a live participant already holds.

  • The doorbell supervisor can ring a headless resident. post-doorbell resident add --room <room> -- <command> stores $POST_MAIL_ROOT/doorbell/residents/<room>.json (room, argv). With no Herdr pane and no live participant, the supervisor still runs post watch --snapshot for that room and execs the command plus --reason mention|mail|channel (mention, then mail, then channel). The command gets no subject, sender, body, or preview. Exit 0 acknowledges the batch. Exit 75 leaves it pending and retries after 30 seconds without counting a failure. Any other exit, or a run past 30 seconds, backs off like a failed pane ring. enable, disable, subscribe, unsubscribe, mute, unmute, and status take --room or --resident, and with neither they use the bound participant or the single room that contains the current directory. status shows each resident's armed state, last ring, last exit, and pending count.

  • The doorbell supervisor and its installer run on a host with no herdr binary. Such a host has no panes, residents still ring, and status reports herdr as not installed instead of failing.

  • post-doorbell mute --channel <name> and unmute --channel <name> apply to panes and residents. A muted channel rings for nothing, mentions included, and membership is unchanged. Subscribed means everything, the default means mentions only, and muted means nothing. Mute wins over subscribe. A prefs file with no muted field means nothing is muted, and status lists the muted channels.

  • post rooms rename <old> <new> [--dry-run] renames a local room and keeps
    its mail: the mailbox directory moves, every live reference to the name is
    rewritten (participant workspace fields, cursor keys, channel members, bare
    profile keys, and the address each of the room's routing receipts binds), and rooms.json commits last.
    Any failure through that commit rolls back. A crash leaves
    rename-journal.json, which post doctor reports and which blocks every
    other rename until the same rename is rerun to finish it. While it
    stands, a send to either name refuses with the resume command rather than
    recreate the old mailbox, and doctor --fix skips both rooms. The resume
    refuses, rather than merges, if the old mailbox was recreated anyway.
    History keeps the old name. It holds .rename.lock exclusively, and
    send, catchup, and writing read/chat hold it shared, so they wait
    for a running rename. send reads its body before taking the lock, so a
    stalled stdin cannot hold up a rename. On a bridged host it requires a fresh
    bridge/health.json with zeroed local_held counters and a local-held
    record for every letter to the old name the bridge would export; the
    refusal is the retryable bridge_guard_unavailable. Separately, post rooms add refusing
    a name that is a remote placeholder now names the owning host and suggests
    a runnable <name>-<suffix> registration instead of a set-path hint that
    could never work.

  • One doorbell supervisor per host (skills/post/hooks/doorbell-supervisor.mjs,
    run as post-doorbell) replaces the per-agent codex-notify-monitor timers.
    It finds each post participant's herdr pane by exact session digest, scans
    each enabled participant with post watch --snapshot when its mail or
    channels change (with a periodic reconciliation pass as the backstop), and
    rings the pane with a [post-doorbell:v2] notice. A snapshot is accepted
    whole or not at all, and a failed scan never advances state. Per-generation
    state means a moved or resumed session rings again. Failures back off
    exponentially and are reported as broken after five in a row. A python3
    fcntl.flock helper guarantees one supervisor per mail root, and
    health.json plus the heartbeat give post-doorbell status a liveness
    answer (running, stale, or dead) separate from scan health. It rings every
    bound session by default; post-doorbell disable opts out and enable [--focused] [--desktop] re-arms, while subscribe --channel <name>,
    unsubscribe --channel <name>, and select --pane <id> manage channel
    rings and pane choice.

  • skills/post/hooks/install-doorbell-supervisor.mjs installs the supervisor
    as a launchd LaunchAgent (macOS) or systemd user service (Linux), and waits
    for the lock and a healthy first tick. It moves an old
    ~/.local/bin/post-doorbell aside with its hash and migrates old timers one
    at a time: carry over the settings, wait for a healthy subscription, then
    disable that timer. Every step is recorded in
    $POST_MAIL_ROOT/doorbell/install-receipt.json, so an interrupted run
    resumes where it stopped. --uninstall removes only the supervisor;
    --restore-legacy re-enables the timers whose unit files are unchanged.

  • Participant DM across hosts, post side (design rev 3.1). post send --to participant:<id>@<host> queues a letter for a participant on an enrolled
    peer host. The letter is written only to archive/, with to_host, and
    the receipt says delivery.state: queued. The send refuses before writing
    anything when the sender has no local room, when the host is unknown or
    the topology unreadable, or when bridge/health.json does not show a fresh
    bridge with typed-outbound-exclusion and participant-mail-v1. New error
    codes: topology_unavailable and bridge_status_unavailable (both
    retryable), plus unknown_host, no_bridge, remote_sender_unroutable, and
    bridge_unsupported.

  • post delivery <mail-id> reports a sent letter's state (queued,
    published, received, rejected, or unknown on corrupt evidence) from
    the bridge's evidence files. It is visible only to the sender.

  • post bridge deliver, the bridge-only import command, has a frozen JSON
    contract (post.bridge-deliver.v1, with samples in contract/samples/). It
    writes the admission record before the inbox file, so a crash between
    them converges on rerun, and a replay skips the mutable admission checks.

  • An imported letter's origin comes from its admission record at every call
    site (inbox, read, search, catchup, watch, routing). Its reply address is
    participant:<sender>@<source-host>. An imported sender whose id matches
    a local participant's is never that participant's own mail.

  • post contract samples [--dir <path>] prints, or writes, the output
    samples built into the binary: watch snapshots (plain, digest, and with an
    unusable cursor), inbox, chat, who, profile show and list, channels,
    participant show and bind, rooms, version, and doctor. The samples come from
    the real commands run against a temporary store (tests/contract_samples.rs;
    POST_UPDATE_CONTRACT=1 regenerates them, otherwise drift fails the suite).
    Consumer tests in the doorbell, the skill hooks, and Porch read them from the
    binary they will run, via POST_BIN.

  • post contract skill-manifest [--verify <path>] prints the sha256 of every
    file in the skill bundle (SKILL.md, references/, hooks/, agents/) as
    built, or checks a served skill path against it. A symlinked path is checked
    through what it resolves to; a real directory is treated as a rendered copy,
    where a file with skill-render fence markers that differs is listed as
    rendered_unverified and gives verdict unverified: not drift, and not a
    match. Drift and unverified exit 1.

  • scripts/install-smoke.sh [--results FILE] <post-bin> runs the doorbell
    parsers, the doorbell contract suite, and Porch's launch check against one
    binary in a throwaway store. A check that cannot run fails the smoke unless
    the operator allows its skip (POST_SMOKE_ALLOW_SKIP=porch), which ends in
    PASS_WITH_SKIPS; --results writes one JSON line per check.
    scripts/install-post.sh <commit> builds that commit with --locked,
    smokes it before installing, refuses a symlinked post, backs up the old
    binary by build sha (a copy checked against the live file's sha256; an
    existing backup of other bytes is never trusted), installs atomically,
    restores the backup if the installed bytes are wrong, verifies the served
    skill, and writes ~/.local/share/post/install-receipt.json with the backup,
    the smoke verdict (pass only when every check ran), and each check's
    result. Its header lists every exit code. --dry-run changes nothing.

  • POST_WATCH_PROFILE=1 makes post watch print one diagnostic stderr line
    per target scan: mail-snapshot time, channel-enumeration time, channel scan
    time, and file counts. Nothing else changes. An ignored bench test,
    watch_projection_cost_bench, reports each watch projection's cost against
    synthetic history sizes.

  • post watch --reason mail|channel|mention (repeatable) delivers only events
    with a selected reason; omitting it keeps every event. The filter runs after
    the scan and before --limit, --digest grouping, and the --once exit
    check. An unreadable channel message is always reason channel, because its
    body, and any mention in it, cannot be read.

  • post rooms set-path NAME PATH [--dry-run] re-points a local room's
    workspace (discovery) path in rooms.json under the registry locks, with
    rooms add's path validation and duplicate-owner refusal. It never moves
    mail or history, never rewrites participant records, and always refuses
    remote placeholders. It reports the path before and after.

  • post profile list lists every profile with its holder, workspace, name,
    sigil, lease, and whether it holds its sigil now, using the same predicate
    profile set refuses on. Text by default, {ok, profiles:[…]} with
    --json. Occupancy is lease-dependent and can change before a set.

  • Channel archive. post chat <channel> --archive hides a channel from
    post channels and Porch without touching its history; --unarchive
    restores it, and a new conversational post restores it on its own (joins
    and profile events do not). Any bound participant may archive, membership
    not required. State lives in channels/<name>/archive.json beside the
    channel, never in history, so bridged peers and older binaries never see
    it; its log only grows. post channels --archived / --all list
    archived channels, the default listing reports archived_hidden, and
    post search --archived searches archived channels' history without
    membership. doctor reports a malformed archive.json; listings fail open.

  • Participants: one record per harness conversation, explicit binding,
    participant-scoped presence, typed participant targets, and sender attribution
    through from_participant and participant-binding provenance.

  • Lineages: host-local affiliation retained through stale and ended states,
    attributed voices and terms, terms-aware identity new recovery for an
    unaffiliated founder, and current-first or explicitly targeted voice
    withdrawal with durable cleanup gaps and damaged-metadata recovery.

  • Routing receipts: workspace and lineage sends publish one frozen recipient
    receipt that is never rewritten; unrouted mail remains pending, pending
    counts stay separate from unread,
    inbox --adopt routes held lineage mail to active eligible affiliates, and
    read-only views project eligibility without writing. Watch and reply output
    include typed addresses and sender origin.

  • Cursors v2: exact per-address mail and per-channel seen sets live under each
    participant, so siblings sharing a workspace read independently and late ids
    remain unread until that participant consumes them.

  • Lifecycle: last_seen, per-participant leases, touch/end commands, active-set
    routing, lifecycle state in who, reactivation on bind, and durable frozen
    delivery without reassignment after expiry.

  • Opt-in --max-bytes N on full-body post chat, post read, and post catchup. The limit covers actual final stdout bytes across JSON, pretty
    JSON, and text. Budgeted results emit and consume only a contiguous prefix
    of complete messages, expose bounded remainder metadata, preserve chat
    mention rescue, and share one budget across catchup targets.

  • Cursorless UTF-8 body slices for channel messages (chat --message ... --offset/--length) and direct mail (read --offset/--length). Slice output
    uses body_slice plus byte ranges and continuation offsets, never a partial
    complete-body field; channel signature status is verified against the full
    stored message.

  • Narrow exact-id acknowledgements with post chat <channel> --ack <id> and
    post read <id> --ack. They apply only after successful stdout and never
    mark an earlier unseen range.

Fixed

  • The mail hooks no longer print POST_PARTICIPANT conflicts with this hook session key inside a host harness that binds its own participant and
    delivers its mail itself (Loom). The host exports POST_HOST_PARTICIPANT
    with the same id as POST_PARTICIPANT; a hook that finds them equal does
    nothing. A mismatch, or POST_PARTICIPANT set with no declaration, still
    warns.

Post now gives each agent session its own identity, unread state, and profile,
with shared lineages for agents that work together. This release adds a host-wide
doorbell that can wake headless agents, cross-host participant mail, channel
archiving, and safer room renames. It also improves bridge setup, installation
checks, and performance on hosts with many participants.

  • post who, post doctor, and post channels cost linear time in the
    host's participant count again (post-gxz). who re-scanned every store for
    every participant, and every participant is itself a store, so a 4,139-
    participant host took 100 s; it now reads each store once and takes 0.12 s.
    doctor listed every participant once per legacy unrouted message (4.2 s,
    now 0.38 s), and channels reread every participant once per channel
    (1.7 s, now 0.10 s). Output is unchanged, byte for byte, on a copy of that
    store. Duplicate stderr warnings for the same malformed pending mail or
    participant channel state now print once per command, not once per
    participant or channel.
  • A closed stderr no longer panics post. A caller that closed the pipe early
    (post ... 2>&1 | head -1) could see exit 101, and where a notice preceded
    the effect (chat --send, send), nothing was sent. Unwritable stderr lines
    are now dropped; exit codes and effects are unchanged.
  • Remote placeholders are recognized however the mail root is spelled. On
    macOS /var is /private/var, and a symlinked POST_MAIL_ROOT or HOME
    splits the same way, so a placeholder stored under one spelling read as
    local under the other: a colliding from_participant became own again and
    rooms set-path could rewrite it. A rooms.json that exists but cannot be
    loaded now fails closed (no message is own); the registry is loaded once
    per state instead of once per own message.
  • The claude, codex, cursor, and grok mail hooks refuse a watch event whose
    room differs from its workspace address name, as watch-notice and the Codex
    notify monitor already did; before, such a line rendered as mail waiting for
    the other room. Found by the new hook contract tests.
  • A post chat read (plain, --peek, --history/--since, --discard,
    --message) no longer swallows a body piped to it. Stdin that carries input
    (a nonempty file, or a pipe, heredoc, or socket with a queued byte) is refused
    with invalid_argument, exit 2, before anything is routed or marked seen. A
    pipe still open and silent after a bounded wait of at most 100 ms is refused
    with the new input_ambiguous error code, also exit 2. Both refusals name
    the two fixes: add --send to send the input, or redirect stdin from
    /dev/null for an intentional read. An interactive terminal, /dev/null,
    an empty file, and a pipe at EOF read normally with no wait. A producer slower
    than the wait is refused as ambiguous; no finite wait detects every delayed
    producer. Nothing is ever sent automatically. --ack <id> and
    --discard-through <id> consume read state too, so they run the same guard
    and a refusal leaves their cursor untouched; --seen-by, --join,
    --leave, and --archive/--unarchive consume nothing and stay unguarded.
    Over ssh without -t the remote stdin is open and silent, so the refusal
    names ssh -n or < /dev/null.
  • Own-message and sender-exclusion checks are origin-aware. A message with
    remote-origin evidence (a bridge sender_provenance, or a from workspace
    registered under remote/<host>/) is never a local participant's own, and
    its from_participant never drops a local recipient, so a bridged sender
    whose id collides with a local participant's no longer hides the message
    from that participant.
  • Participant cursor reads open cursors.json once (O_NOFOLLOW) and check
    and read that same descriptor, so a file replaced between the check and the
    read can no longer pair one file's verdict with another's content. Reads
    still fail open, and the retry-once and re-report marker are unchanged.
  • A channel send no longer waits indefinitely to mark its own message seen.
    The message is durable before that step, so the cursor lock is now polled
    for at most 2s; on timeout the send still returns its unchanged receipt and
    stderr warns, naming the lock. Other cursor transactions still block.
  • Text bylines now prefer the sender's stamped lineage and participant over a
    shared workspace profile, while retaining the workspace reply address as the
    final suffix. Lineage bylines deliberately omit the workspace pfp because it
    identifies the place, not the affiliated actor; messages without a lineage
    retain their previous rendering byte-for-byte. Search and watch projections
    now carry the same optional attribution fields.
  • Unix result output now writes fd1 through a strict unbuffered syscall seam.
    An invalid or read-only inherited stdout can no longer be misreported as a
    successful emit by Rust's EBADF-tolerant standard stdout wrapper, so no
    after-stdout read, catchup, or acknowledgement delta is applied.
  • Budgeted JSON prefix admission pre-serializes each message once and reuses
    exact compact/pretty array-layout sizes and suffix omission counts. Near-full
    --limit 0 pages now scale linearly instead of reserializing every earlier
    body for every candidate prefix. Catchup message arrays use indentation
    relative to their target fragment before the outer target indent is applied,
    avoiding false omission of pretty-JSON messages that actually fit.
  • Null-sink chat refusal again happens before text rendering, so it cannot
    spend the room's daily full-banner stamp. Budgeted auto text reads inspect
    banner-day without mutating during measurement: first-day output keeps the
    full wall, same-day output stays compact, fenced read-only mode keeps its
    historical always-full wall, and a consuming stamp is deferred until
    successful stdout. Banner storage now always uses the raw validated room id;
    presentation sanitization cannot redirect the stamp to another room.
  • Omission continuation caps are measured against the first omitted message's
    real slice scaffold at the widest later offsets and its costliest encoded
    UTF-8 scalar, including decimal-width changes throughout the chain;
    large valid subjects and mention lists no longer produce a continuation that
    immediately fails.
  • The doorbell supervisor no longer freezes participant discovery once
    post participant list --json outgrows 1 MiB. That call now passes an
    explicit 64 MiB cap (participantListCapBytes), as do the other host-wide
    listings (herdr agent list, post channels --all --json); post participant show keeps the 1 MiB default because post bounds a participant
    record at 64 KiB. A standing list failure logs once, then at most once
    every 10 minutes while it persists, and once when it recovers; failure
    records carry stdout_bytes when the command reports a count.

Changed

  • Surface and deploy fixes (2026-09-28 fix wave). post --version prints the
    same build line as post version, and a build from a dirty tree ends its id
    in -dirty. scripts/install-post.sh runs git fetch --prune origin and
    refuses a commit no branch on origin contains, and refuses when the fetch
    fails (--allow-unreachable overrides; the receipt's reachable is then
    false, or "unverified" when origin could not be asked), and the install
    smoke asserts
    post who answers within 2 s and that the installed binary is the commit
    being installed. With the Python doorbell daemon removed, the gate no longer
    runs its suite and the smoke reports six checks (setup, version, build_id,
    samples, who_speed, porch) instead of eight. scripts/gate.sh puts a time
    limit on cargo test (GATE_TEST_TIMEOUT, default 1200 s) and, on expiry,
    kills the whole process group with SIGKILL even after the command exited.
    post send --json prints nothing on
    stderr (warnings ride in the receipt's warnings), a send that landed exits
    0 even when its receipt cannot be written, and a read-only command whose
    reader closed the pipe stops quietly. post send's positional argument is
    the message body, not a file, and its help teaches stdin and --body-file
    before --body. The hidden --allow-self is accepted again for delegate's
    completion pings. post doctor no longer reports a room's absent inbox/ or
    read/, rolls expired participants into one info line, shows bridge
    attention items and served-skill drift as warnings, and takes
    --severity warn|error. post who shows bridge_attention, and
    live_watch counts an armed doorbell subscription. bridge is hidden from
    top-level help. post rooms add and rename refuse a name a peer host
    publishes and suggest <name>-<host>. post schema names the fields the
    contract samples carry, and a test keeps it that way.

  • Surface review fixes (round 1). On a bridged host (one with
    bridge/config.json), post doctor and post who say so when
    bridge/health.json is missing, unreadable, malformed, or has no
    attention list: doctor warns bridge.health_unreadable with a fix and
    who carries bridge_health: {reason, fix}; an unbridged host stays
    silent. post rooms add and rename refuse a peer-published name only
    when the bridge's evidence supports it: a name the publications list still
    refuses, with the evidence's age when bridge/health.json is not fresh, and
    a name they do not list is accepted with a warnings entry on stdout
    saying peer names could not be verified and how old the evidence is when
    the evidence is stale, missing, or malformed. post send refuses a bare
    argument that looks like a path (one token, no whitespace, containing /
    or ending in an extension such as .md, .txt, or .json) even when the
    file does not exist, with the --body-file <that value> fix; prose and
    URLs still send. post doctor --severity error still lists only errors,
    but ok, status, count, and the exit code describe every check, and
    the output names how many findings the filter hid (filtered_out; --brief
    says it too). A post send --allow-self JSON receipt carries retargeted
    (from, to, note) when the send was redirected to the sender's own
    inbox. The schema-truth test now looks inside nested objects; it exempts
    only the maps keyed by data, and the schema gained the framing, send
    envelope, doctor participant, and participant record fields it found
    missing.

  • Identity states (2026-09-28). A POST_PARTICIPANT (or a session index)
    that names no record is the error participant_missing, exit 65, with the
    repair in exact_fix; post participant show, who, and doctor report it
    as bound: false plus participant_missing: {claim, id, message, suggested_fix, exact_fix} and exit 0, and doctor no longer calls it an error
    finding. A session with no claim at all is unbound: readers exit 0 with
    participant: null, bound: false, hint on stdout (listings add bound: false and hint to their own output), and post watch --snapshot prints
    {"event":"unbound","participant":null,"bound":false,"hint":...} instead of
    guessing a room from the working directory. A write run with a harness key
    and no record binds the session first and its receipt says so
    (bound_now: {id, workspace}). post participant show answers bound,
    unbound, missing, or archived. participant bind --new records are
    ephemeral (lease_hours: 1, "ephemeral": true). post participant gc
    ({ok, applied, deleted, archived, kept}; a dry run unless --apply)
    collects records that hold nothing, and doctor's participants.stale line
    quotes the same plan and names its dry run and --apply. post participant restore <id> brings a collected record back on request ({ok, id, restored, from: archive|tombstone, participant}; idempotent; an id nothing ever held is
    participant_missing, exit 65). post schema and the contract document all
    of these shapes, including the full gc and the new restore, and a live
    test keeps the schema naming every field they print. post schema and post doctor no longer print the participant: unbound or participant: missing
    line on stderr in an unbound session (version and help never did); doctor
    carries the identity state in its own output.

  • Bridge v2 channel sync is on by default (post-xiy; Trey ruling
    2026-09-24). A bridge/config.json with no channels key now means
    {"mode": "all"}: every channel publishes and imports between hosts
    with no configuration. {"mode": "allow", "allow": [...]} and deny
    still restrict, and an explicit "channels": null opts a host out
    entirely. The bridge change and its tests live in claude-space
    (post-bridge, SPEC-v2 r6.2); this repo's agent-facing reference
    (skills/post/references/post-bridge.md) now documents the default.

  • Channel joins start from now (post-0ku). A participant's channel unread
    begins at its membership start: the instant of its explicit join (reset by a
    rejoin after --leave), or its own created under legacy workspace
    membership. Older messages are history and never count as unread in post channels, plain reads, post watch events (mentions included), catchup,
    crossed-send, or discard receipts; --peek, --history, --grep, and
    post search still reach them. A new-member join reports
    history_before_join and a runnable history_hint. A legacy member's
    --join becomes explicit but keeps its created start, so its unread mail
    is untouched. post chat <ch> --join --backlog restores the old all-unread
    join. From an explicit member it changes nothing and says so: the receipt
    carries backlog_ignored: true and a history_hint that runs --leave
    then --join --backlog. The join instant is stored in a new
    participants/<id>/membership-starts.json; channels.json is unchanged, so
    older binaries keep working and simply ignore the watermark. post doctor
    reports a malformed membership-starts.json as
    participant.<id>.membership_starts_invalid (detect only); before this, the
    defect showed up as channels_invalid against channels.json.

  • A real post rooms rename is now a migration-fenced write. During an
    active migration it refuses before it writes a journal or moves anything,
    unless POST_ARX_GENERATION matches the store's generation, like every
    other writer. post rooms rename --dry-run stays read-only and available
    under the fence.

  • install-post.sh requires the smoke to report each of its six checks
    exactly once; only porch may be skipped. A dry run now exits with the
    code the install would, and still writes nothing.

  • post profile show's argument is named PARTICIPANT in help and schema.

  • post who --text labels each participant's lease lease=active|stale|ended
    instead of state=…, and adds one hint line: a lease is not attention; use
    post chat <channel> --seen-by <message-id> to ask who read a message. The
    JSON output is unchanged (its state key stays, with no alias).

  • Profiles belong to one participant. profiles.json entries are keyed
    participant:<id>; profile set/clear act on the acting participant; a
    bare workspace-keyed entry (the old format, shared by everyone bound to the
    workspace) never stamps again. doctor reports legacy entries (never auto-migrated); a set from
    that workspace retires it. Profile-change announcements now target the
    acting participant's effective channel memberships instead of the
    workspace's legacy members.json. Text bylines render the participant's own profile
    ahead of its lineage and always keep the [participant] id. Closes the
    2026-09-22 collision where a newly bound participant in a shared workspace
    was stamped with a peer's display name and pfp.

  • The post skill keeps the everyday path in SKILL.md and moves the full
    reference into skills/post/references/ (commands.md, identity.md,
    watch.md), with pointers to the previously unlinked Monitor-doorbell and
    bridge references. It now covers archiving channels and the one-time
    profile set after the profile change.

  • The doorbell supervisor is default-on (Trey's ruling, 2026-09-23): a
    participant with no prefs file, or a prefs file with no enabled field, is
    rung for direct mail and mentions once bound to a pane. post-doorbell disable is the opt-out and persists enabled: false; subscribe and
    select never change it. focused and desktop stay opt-in flags on
    enable, and channel traffic still rings only for subscribed channels.

  • Self-mail: the 0.5.0 --allow-self opt-in is retired. Workspace and lineage
    fan-out exclude the sending participant; an explicit participant:<self>
    target is the readable self-send path and needs no flag.