Conversation
added 7 commits
September 27, 2026 08:03
The pinnace, the crowsnest phone channel, queues its orders through `fm-inbox.sh note` like the voice handover does, but nothing in the record said who spoke. `note --source <token>` now sets the `source` header that `receipts` already returns (default `text`, `say` keeps `voice`, the phone passes `pinnace`), and each repeatable `--meta <key>=<value>` adds one provenance header through the header slot `say` already fills, so a phone order carries its tailnet node and login. Tokens are validated and a meta key may not reuse a fixed header name, so no caller can forge the id, time, or source of a note.
…innace exists The away-posture record always said `reach_channels: none` and the entry announcement always denied a phone channel, because none existed. The pinnace, the crowsnest phone channel, now exists and rides the captain-note plane, so a home that opts in with `config/pinnace` (first line the captain's tailnet login) records `reach_channels: pinnace` and announces that orders from the phone reach the first mate while away. Validation accepts `none|pinnace`, a standing record keeps the channel it was written with, and a version 2 record carrying `none` still validates, so a live away window is unaffected by the upgrade. The reach lines in the afk skill, the architecture doc, the launch header, and the AGENTS.md away stub now say the recorded channel instead of denying a phone.
A note the pinnace queues carries `source=pinnace`, but nothing told the first mate how to treat it: while away, a note arrives as an operational digest, and an inbox note is otherwise answered only "when it needs a durable answer". The afk skill's "While away" section and the AGENTS.md inbox-note rule now say a pinnace note is the captain speaking from the phone: act on it with ordinary chat authority, keep the away posture, confirm first through a further note for anything destructive, irreversible, or security-sensitive, and always answer it with `fm-inbox.sh reply` in one short section 9 outcome message before acknowledging it. Prose only; the crowsnest smoke script and a manual end-to-end after install are the executable proof.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Ship the three small firstmate-repo plumbing changes that finish the pinnace (the crowsnest mobile feature the captain merged as PR #2). These make firstmate's own message plane and away mode aware of the pinnace phone channel. The full design is in the approved plan at
~/Dev/firstmate/data/crowsnest-mobile-plan-g1/report.mdunder R3 and steps P1-P3; implement exactly those three, no more.P1 - note provenance in the captain-note plane. In
bin/fm-inbox.sh, addnote --source <token>(validated[A-Za-z0-9._-]+, default staystext) and a repeatable--meta <key>=<value>, written through the existingextraslot, so a note can readsource=pinnacewithnode=<tailnet machine>andlogin=<tailnet login>;receiptsalready returnssource. Update the script header and--help, and the onedocs/voice-relay.mdline thatsaykeepsvoice.P2 - the pinnace as the recorded reach channel. In
bin/fm-afk-contract.sh, when a new opt-in flagconfig/pinnaceexists in the home, recordreach_channels: pinnaceand print a matching reach-announced sentence at away entry (todaynoneis written and required by validation); validation acceptsnone|pinnace; a version-2 record withnonestill validates. Registerconfig/pinnaceindocs/configuration.md(first line the captain's tailnet login; presence turns the channel on), and update the away skill anddocs/architecture.mdreach lines.P3 - how the first mate treats a pinnace note. In
.agents/skills/afk/SKILL.mdandAGENTS.md's captain-inbox-note handling, state that asource=pinnacenote is the captain speaking: act on it with ordinary chat authority, keep the away posture, confirm-first for the never-set (destructive, irreversible, security-sensitive), and always answer it withbin/fm-inbox.sh reply <id>in one short section-9 outcome message before acknowledging.Out of scope: any crowsnest-repo change (that shipped in PR #2), the Android client, and anything beyond these three changes.
What Changed
bin/fm-inbox.sh notegains--source <token>, validated as[A-Za-z0-9._-]+and defaulting totext. It also gains a repeatable--meta <key>=<value>, written through the existingextraheader slot. A meta key must pass the same token rule and cannot reuse a fixed header name (id,at,source,announce_marker,request_id), and its value must be non-empty with no line breaks. This lets the pinnace queue a note withsource=pinnace,node=andlogin=, andreceiptsreturns the source. The script header, the shared usage/--helptext anddocs/voice-relay.mdnow describe these flags.bin/fm-afk-contract.shchecks for an opt-inconfig/pinnaceflag when it writes a record (the path can be overridden withFM_CONFIG_OVERRIDE):reach_channels: pinnace, a pinnace reach sentence, and the phrase "the pinnace is the reach channel" in the entry announcement and read-back. Without it, the record is hold-for-return only, as before.none|pinnace.fm-afk-launch.shand thefm-session-start.shAFK digest no longer hard-code "hold-for-return only".docs/configuration.mdregistersconfig/pinnace: its first line is the captain's tailnet login, it is local, gitignored and not inherited by secondmate homes, and a standing record keeps the channel it was written with. TheAGENTS.mdconfig listing and the reach lines indocs/architecture.mdare updated to match..agents/skills/afk/SKILL.mdaway skill andAGENTS.md's captain-inbox-note handling now say to read a note'ssourcefromreceipts. Asource=pinnacenote counts as the captain speaking: the first mate acts on it with ordinary chat authority, keeps the away posture, and asks for confirmation first on anything destructive, irreversible or security-sensitive. It always answers withbin/fm-inbox.sh reply <id>in one short section-9 outcome message before acknowledging the note. Tests intests/fm-inbox.test.shandtests/fm-afk-contract.test.shcover the new flags and the pinnace reach channel.🤖 Generated with Claude Code
Risk Assessment
✅ Low: Three small, well-bounded plumbing changes whose concrete input traces (a pinnace note with meta headers through write and receipts; a pinnace-flag record through write, validate, refresh, replace, readback, and announcement) produce correct results, with every extra edit on the branch authorized by an earlier recorded decision and no crowsnest or Android scope touched.
Testing
Baseline: bin/fm-test-run.sh on tests/fm-inbox.test.sh and tests/fm-afk-contract.test.sh both passed. Live: two driver scripts exercised bin/fm-inbox.sh note/receipts/reply/drain and bin/fm-afk-launch.sh enter/stop plus bin/fm-afk-contract.sh field/validate/readback against disposable lab homes with and without config/pinnace, including adversarial inputs; bin/fm-session-start.sh was run against a pinnace lab home to check the AFK digest line; a throwaway Claude primary was launched on a private tmux socket with FM_HOME set to a marked lab home, driven through /afk, two externally queued pinnace notes delivered by the real away daemon, and an unmarked return message, with pane transcripts, durable inbox state, and the primary's own command log captured. The lab primary hit the account's Fable usage limit mid-run and was switched to Opus 5.5 to avoid spending paid credits. All driven scenarios passed; the lab, its tmux server, and its daemon were torn down and the worktree is clean. tests/fm-afk-launch.test.sh was not run because it creates sessions on the default tmux server, which the runbook forbids touching; its enter-path assertion was covered by the P2 driver.
Evidence: P1 live transcript: note provenance, receipts, replay, list/drain, adversarial refusals, reply and ack
Source: P1 live transcript: note provenance, receipts, replay, list/drain, adversarial refusals, reply and ack
Evidence: P1 driver script (reproducible)
Source: P1 driver script (reproducible)
Evidence: P2 live transcript: pinnace and no-flag entries, refresh, replacement, flag removal, unknown channel, v1 record, return
Source: P2 live transcript: pinnace and no-flag entries, refresh, replacement, flag removal, unknown channel, v1 record, return
Evidence: P2 driver script (reproducible)
Source: P2 driver script (reproducible)
Evidence: P2 pinnace entry announcement as printed by fm-afk-launch.sh enter
Evidence: P2 session-start AFK subsection on a pinnace lab home
Source: P2 session-start AFK subsection on a pinnace lab home
present - away posture recorded at 2026-09-27T06:17:21Z (bin/fm-afk-contract.sh readback for the mandate and the reach channel); no daemon runs, the ordinary supervision session continues.Evidence: P3 lab Claude primary pane: /afk entry and the relayed pinnace announcement
Source: P3 lab Claude primary pane: /afk entry and the relayed pinnace announcement
Evidence: P3 reply the first mate sent to the phone for the destructive order
Evidence: P3 queued pinnace notes (JSON returned by fm-inbox.sh note)
Source: P3 queued pinnace notes (JSON returned by fm-inbox.sh note)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ **Review** - passed
✅ No issues found.
bin/fm-inbox.sh:248-fm-inbox.sh note --source --json nopequeues a note withsource=--json(exit 0, human output) instead of a usage error, because a leading dash satisfies the intent's token rule[A-Za-z0-9._-]+. This matches the existing--request-idconvention and is not a scenario failure against the stated intent, but a source token can never legitimately begin with--, so the author may want valid_source to refuse a leading dash. Observed in p1-inbox-provenance-transcript.txt section 5.bin/fm-test-run.sh tests/fm-inbox.test.sh tests/fm-afk-contract.test.sh(baseline, both pass; log in baseline-targeted-tests.log)bash p1-inbox-provenance-driver.shagainst a lab home:fm-inbox.sh note --source pinnace --meta node=... --meta login=... --request-id pinnace:<id> --json, stdin body variant, plain note,list,drain,receipts,reply,drain --ack, and 18 malformed --source/--meta inputsbash p2-afk-reach-channel-driver.shagainst lab homes:fm-afk-launch.sh enterwith and without config/pinnace, refresh, replacement,stop,fm-afk-contract.sh field/validate/readback, flag removal, a corrupted reach_channels value, an empty flag file, a directory named pinnace, and a hand-written version 1 recordFM_HOME=<pinnace lab> bin/fm-session-start.shand inspection of its AFK subsectionLab Claude primary ontmux -L fm-labwithFM_HOME=<marked lab home>:/afk answer phone orders while I am out; hold anything you are unsure of, then two pinnace notes queued from outside withfm-inbox.sh note --source pinnace ..., delivered by the real away daemon as record-backed doorbells, then the unmarked return messageVerification of the lab's durable state after each note:fm-inbox.sh receipts, the reply records under state/inbox/.replies, the handled note, the operational-inbox escalation record, the away record, and the archived record after returnExtraction of the lab primary's Bash commands from its own Claude Code session transcript to confirm it ranfm-inbox.sh receipts,reply <id>, anddrain --ack <id>in that order✅ **Document** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.