This document owns the version-scoped feasibility evidence, Pi transcript taxonomy, and supported-API boundaries for Firstmate calm mode.
calm.md owns the current user-facing /calm usage and limitation contract.
A qualifying implementation must auto-load from the trusted project, persist the toggle choice for the effective Firstmate home across session starts and resumes, keep working activity visible, emit no Calm status row, redraw already-rendered controllable rows, remove supported hidden rows without gaps, restore ordinary rendering, and leave delivery, tool execution, model context, session storage, export and share operation, diagnostics, and expansion state unchanged. The governing presentation policy allows genuine original user prompts, genuine user-facing assistant text, and working activity. Working activity may be presented through the harness's stock row or through a supported Calm-owned drawing, but Calm must leave the stock row untouched whenever Calm is off. Changing persisted context to remove hidden content, filtering provider context, patching installed harness code, or claiming coverage outside a supported renderer does not satisfy that boundary.
calm.md owns the current Pi compatibility contract.
Pi 0.81.1 was installed when Calm was first built, and Pi 0.82.0 was the later reverification target.
The inspected Pi CHANGELOG shows no relevant presentation API introduced at either version, so those versions remain verification evidence rather than compatibility bounds.
The exported classes used by the adapters (AssistantMessageComponent and InteractiveMode) are undocumented internals with no stated version guarantee.
tests/fm-calm-pi-extension.test.sh records the installed Pi version as evidence without gating on it and covers both newer synthetic versions and an unavailable adapter seam.
This host tracks Pi latest, so the version the evidence is pinned to moves; the 2026-09-07 record owns the currently pinned version and the renderer comparison behind it.
calm.md owns the current user-facing collision behavior and limitation.
Inspection of Pi 0.80.10 and 0.82.0 established that extensions override a built-in tool by registering the same name, the first registered extension wins the complete ToolDefinition without merging, and Pi exposes no unregister operation.
Pi loads project-local extensions before global or CLI-configured extensions, so Firstmate's tracked Calm extension previously won those collisions even when its persisted preference was off.
The losing definition's execution and render functions are both discarded, so unconditionally registering Calm's wrappers would replace another extension's same-named tool rather than changing presentation alone.
Pi's getAllTools() exposes tool metadata and source identity but not the executable or rendering functions needed to wrap another extension's full definition.
It is also usable for reliable collision detection only after extension binding, which makes it suitable for the first same-session /calm activation but not for synchronous extension loading.
Deferring registration to session_start is not an equivalent path: Pi constructs restored tool rows from an earlier tool-registry snapshot during reload, new-session, fork, and session switching, so those rows retain the definition captured before session_start.
tests/fm-calm-pi-extension.test.sh covers the resulting split contract: no load-time claims while Calm is off, synchronous claims while it is already on, collision-checked first activation with a warning, preservation of a contested tool's execution, and the non-retroactive bound for rows rendered before first activation.
The Pi version installed at the time was verified on 2026-07-22.
$ pi --version
0.81.1
The pre-cleanup reproduction used a real isolated Pi TUI at 180 columns by 44 rows with the tracked Calm and watcher extensions, an isolated FM_HOME, and a live home-owned watcher cycle.
The model called fm_watch_arm_pi, the real tool returned watcher: started Pi extension arm child 1, and a done: status write caused the watcher extension to inject FIRSTMATE WATCHER WAKE: signal: ... followed by the stable drain instruction.
With Calm off, the captured transcript contained the genuine user prompt, the full watcher tool shell, the synthetic user-role wake, four collapsed Thinking... labels, built-in tool rows from wake handling, and the final assistant response.
With the pre-cleanup implementation's Calm mode on, the existing seven built-in tool rows disappeared, but the watcher tool shell, synthetic wake, and all four Thinking... labels remained.
The final screenshot-scale regression reproduced the same transcript after the cleanup and verified that Calm removed those remaining controlled rows while retaining the genuine prompt, a watcher-shaped genuine near-miss prompt, and the genuine assistant responses.
The original proven comparison path was a built-in text tool.
Calm owned both of that tool's supported renderer slots and switched its shell to renderShell: "self", so returning empty components removed the complete row and setToolsExpanded redrew existing tool components.
Adding supported empty renderer slots to a scratch copy of fm_watch_arm_pi likewise removed its row while the real watcher still started and the model still returned PROBE_COMPLETE.
Legacy synthetic presentation entries use CustomEntryComponent, whose host adds spacing only when its renderer returns content, so an undefined Calm renderer result removes the complete row and can later restore it through the ordinary expansion redraw.
The later duplicate-turn evidence below supersedes custom-message rerouting as an acceptable implementation for current operational input.
Hidden-block height regression
The 2026-07-23 end-user-aligned reproduction used the installed Pi 0.81.1 TUI at 100 columns by 44 rows, an isolated project and FM_HOME, the real /skill:ahoy command path, and a deterministic provider that produced five thinking-bearing read calls, five tool results, final hidden thinking, and a visible final response.
With Calm on and Pi's thinking display collapsed, the completed turn left 14 empty rows between the visible collapsed [skill] ahoy content row and the first final assistant row.
With Calm off, the same sequence rendered all six Thinking... labels and all five read rows instead of an empty field.
A controlled baseline containing only the skill row and final response had two standard visible-row separators.
Adding one final thinking block increased that gap from two rows to four, while adding a tool call without a result or a completed tool call and result left it at two.
Removing only all six thinking blocks from the failing persisted session left all five tool calls and results intact and reduced the gap from 14 rows to the two-row baseline.
Enabling Pi's terminal.clearOnShrink on the unchanged failing session left the gap at 14 rows, which rules out stale terminal allocation as the cause.
The initiating trigger was a non-empty thinking block in an assistant message that Pi rendered through AssistantMessageComponent.
The masking condition was the combination of Calm being active and Pi's thinking display being collapsed, because Calm replaced the visible label with an empty string while Calm off or explicit thinking expansion filled those rows with visible content.
The visible symptom was the large empty vertical field between the intentionally visible collapsed skill row and final assistant response.
The earliest divergent layout path was AssistantMessageComponent.updateContent, before terminal differential rendering or tool-result composition.
Pi computed hasVisibleContent from the original thinking data and added a leading Spacer before applying the hidden-thinking presentation.
Pi then styled the empty label before constructing Text, so the resulting ANSI-only string occupied one rendered row, and a thinking block followed by assistant text also added its ordinary inter-block spacer.
Each thinking-only tool turn therefore retained two empty rows, while the final thinking-plus-text turn retained two extra rows beyond the final response's normal leading separator.
The proven tool path diverged through ToolExecutionComponent, where the Calm self-render shell returned zero lines for both call and result slots and contributed no residual height.
The smallest counterfactual was the thinking-only removal from the same persisted session, which preserved the skill, tools, results, final response, session ordering, and terminal settings while eliminating every unwanted row.
The single-thinking, tool-call-only, tool-result, Calm-off, and clearOnShrink controls deliberately sought disconfirming evidence and isolated collapsed thinking layout from skill, tool, result, and terminal-cache candidates.
PR 927 made Calm persistent and described controlled rows as gapless while retaining a documented unsupported boundary for collapsed-thinking spacing.
PR 936 removed the unsafe operational-input reroute and preserved legacy zero-height entries but did not change assistant-message layout.
The fix installs one idempotent presentation adapter, verified on Pi 0.81.1 through 0.82.0, on the exported AssistantMessageComponent.updateContent method.
The adapter probes for that exact method and, per the compatibility contract, degrades independently with a diagnostic rather than gating on a version number.
Only while Calm is active and Pi has collapsed thinking does the adapter pass a shallow thinking-free presentation copy into Pi's ordinary layout calculation, then retain the original message on the component for invalidation and thinking expansion.
The persisted assistant message, provider context, tool execution, export data, and expansion history remain unchanged.
Collapsed thinking-only assistant messages now render zero rows, thinking before visible assistant text adds no spacing beyond the text-only baseline, and expanding thinking still renders the original reasoning.
The disconfirming checks deliberately retain supported boundaries. An arbitrary third-party custom tool and a built-in read image remain visible because Pi exposes neither a global tool renderer nor image-row control. Expanded thinking remains visible by design, while re-collapsing it returns to zero-height Calm presentation. Ordinary user-role near misses remain visible, including quoted current markers, ASCII-only labels, unrelated text before a marker, unrelated text after U+2063, and image-bearing input.
The captain-visible regression reproduced three consecutive times in a persisted Pi session under ~/.pi/agent/sessions/.
Assistant bb83873b was followed by hidden custom input 9d087b52 and distinct duplicate assistant f4232aa3.
Assistant 3a388d8c was followed by adjacent hidden custom inputs e1914f28 and cfdefb09 and distinct duplicate assistant 47c81eeb.
Distinct provider response identifiers and signatures prove separate model turns rather than duplicate TUI paint.
The initiating trigger was pi.sendUserMessage(..., { deliverAs: "followUp" }) from the watcher or turn-end adapter after a captain-facing response.
The exposure condition was Calm's loaded input handler from commit 6db3b09, which ran whether the persisted toggle was on or off, returned handled, replaced the user message with pi.sendMessage, and triggered a nested custom-message turn.
The visible symptom was a second assistant row repeating the prior captain answer.
The earliest persisted divergence was the operational entry type: Calm loaded produced custom_message with role custom before provider conversion, while Calm absent produced a normal message with role user.
The earliest lifecycle divergence was that the replacement path bypassed Pi's normal user-prompt processing after the input event.
A native deterministic Pi TUI reproduction on landed PR 927 produced CAPTAIN_VISIBLE_ANSWER twice with Calm loaded and explicitly on, and produced the same duplicate with Calm loaded and explicitly off.
The same exact typed notification with Calm absent produced one captain answer followed by MONITOR_NOTIFICATION_HANDLED.
Removing only the input reroute from a scratch copy while leaving Calm loaded and on produced the same proven result and restored the operational entry to role user.
This is the smallest counterfactual and proves extension loading, not the active toggle, was the required exposure condition.
The extension-absent success path is evidence against an independent Pi-core duplicate-turn cause for the same sequence, but it does not claim Pi core could never contain a separate duplication bug.
PR 936 removed Calm's semantic input handler and custom-message delivery path because Pi 0.81.1 exposes no supported ordinary-user renderer and that replacement duplicated model turns.
That correction preserved current operational input as an exact ordinary user-role message with its ordering and authority unchanged, but deliberately left the row visible until a presentation-only boundary was proven.
Legacy firstmate-synthetic-input-presentation entries remained renderable so existing sessions preserved their stored presentation and zero-height hidden-row behavior.
The 2026-07-23 end-user-aligned reproduction used the installed Pi 0.81.1 TUI at 160 columns by 36 rows, the tracked Calm extension persisted on, an isolated home and session directory, and a deterministic in-process provider.
The injected user message began with exact U+2063 plus FIRSTMATE_OP: and carried the watcher status path from the durable captain screenshot followed by the blank line and stable drain instruction.
The exact U+2063 bytes, both payload lines, user role, and ordering survived live delivery and process restart.
The provider observed one matching user message, returned OPERATIONAL_PROCESSED occurrences=1, and the session contained one matching user entry and one matching assistant entry.
The failing viewport rendered the operational input as a five-cell-high user box on rows 1 through 5 and placed the assistant text on row 7 after Pi's normal assistant separator. The same persisted session reproduced those coordinates after restart. Calm off rendered the same user component geometry, proving the active toggle had no presentation effect on this path. The initiating trigger was the exact watcher-generated user message. The exposure condition was PR 936's safe ordinary-user delivery path combined with the absence of a user-row presentation adapter, not marker loss, event-source drift, failed classification, persistence, replay, or duplicate delivery. The visible symptom was the complete two-line synthetic user box and its five rows of terminal height.
The earliest meaningful layout divergence from proven hidden presentation entries was InteractiveMode.addMessageToChat.
Its ordinary-user branch added a leading Spacer when applicable and then a UserMessageComponent, whose Box contributes vertical padding around the three Markdown lines.
The legacy custom-entry path instead checks renderer content before mounting a transcript child, and the completed assistant-thinking fix removes hidden thinking before assistant layout.
Those behaviors have different owners and remain separate.
The smallest counterfactual returned only from the transcript owner's ordinary-user branch for that exact watcher input. The real Pi viewport moved the unchanged assistant text from row 7 to row 2, rendered no operational text, and still persisted one exact user entry and one exact response. The leading cause would have been falsified if the row or height remained, the provider lost or duplicated the message, or the persisted role or bytes changed. None occurred.
The fix installs a separate idempotent presentation adapter, verified on Pi 0.81.1 through 0.82.0, on the exported InteractiveMode.addMessageToChat method.
The adapter probes for that exact method and, per the compatibility contract, degrades independently with a diagnostic rather than gating on a version number.
It delegates current recognition to bin/fm-operational-input.sh, adds only the evidence-backed bare-U+2063 Supervisor escalate ( presentation compatibility shape, mounts a UserMessageComponent subclass that preserves Pi's stock row plus leading spacer while Calm is off, and returns zero rendered lines while Calm is on.
It never intercepts the input event, rewrites the message, changes its role, filters model context, or changes session data.
Messages containing an image are left on Pi's ordinary path even when their text equals an operational envelope because Firstmate's authoritative producers are text-only.
A native exact-watcher run and its process-restart replay kept the neighboring assistant text at the two-row visible-only spacing while retaining one exact user entry and one processing response.
An adjacent two-notification run retained the same two-row neighboring-assistant coordinates, proving both operational components contributed zero height.
Calm off, an absent Calm preference, and an absent Calm extension retained ordinary rows.
The current exact marker and the narrow bare-U+2063 Supervisor escalate ( compatibility shape hid under Calm, while quoted markers, ASCII FIRSTMATE_OP: without U+2063, ordinary text before the current marker, unrelated text after U+2063, and image-bearing input remained visible.
Calm replaces Pi's stock working row with a small animated boat while Calm is on and one logical agent run is active.
This path uses only public extension API and patches nothing: ExtensionUIContext.setWorkingVisible(false) hides the stock row, and setWidget() installs a temporary component factory above the editor.
Pi's documented custom working-indicator frames are static and width-blind, so they cannot own responsive geometry; a widget component receives render(width) and can.
Within Firstmate Calm, .pi/extensions/fm-calm.ts remains the sole owner of its presentation choice and the only caller of setWorkingVisible(), while .pi/extensions/lib/fm-calm-working-ship.ts owns Pi's ANSI painting and the widget over the sprite geometry, bounce track, cadences, and freeze/resume state in .claude/mods/firstmate-calm/lib/fm-calm-working-ship-sprite.ts, the harness-neutral core the Claude Code mod also draws from (reached from the Pi tree through a tracked symlink, because Claude Code refuses a hooks-module import from outside the plugin folder).
Visibility follows agent_start through agent_settled rather than turns or tool calls.
Pi emits agent_settled from a finally block once a run will not continue automatically, so retries, automatic continuations, queued follow-ups, and compaction inside one run never remove the boat, while settle, abort, and failure all reach the same cleanup.
Repeated agent_start events inside one run are idempotent, and both Calm extensions use the shared calm-working-ship widget key, so Pi disposes the previous component before installing a replacement under that key and when it clears extension widgets, while Firstmate Calm's disposal signal prevents its cleanup from clearing a standalone Calm widget that replaced its own widget.
Pi's above-editor widget container reserves one spacer row whether or not a widget is present, so removing the boat leaves no residual blank row.
The sprite is two rows when the usable width admits the complete hull: an asymmetric three-cell ◿│◣ sail centered over a five-cell ╲▁▁▁╱ hull that sits inside the water row rather than adding a third row.
The sail is the same in both travel directions, and its one-cell quarter triangle keeps the left sail visibly smaller than the full right sail.
The hull's three inner cells are zero-height water glyphs, so the swell reads as continuous beneath the boat instead of being interrupted by it.
Direction reverses the moment the boat lands on an endpoint, so the endpoint frame itself already carries the new heading and the trough follows the next boat movement without a discontinuity.
The water row fills the complete supplied width, the track is recomputed and clamped from that width on every frame so a resize cannot wrap or strand the boat offscreen, and widths too narrow for the hull fall back to a deterministic single row.
One scheduler drives two linked cadences.
Every tick advances the wave by one quarter-cell, and only every fourth tick moves the boat one whole cell, so at a 220ms tick the swell advances one cell per 880ms boat step and the boat stays phase-locked inside the same trough.
Ticks rather than wall-clock timestamps drive every state change, so tests seek animation time exactly, and disposing the widget stops both cadences together.
The water is the lower half of the bottom-aligned one-cell bars that Pi Dictation uses for its level history, ▁▂▃▄, so advancing the phase never changes visible width, adds a row, or moves the hull column.
The swell is a deterministic field of smoothstep half-waves whose lengths vary between nine and thirteen cells from a fixed hash, surrounding a broad zero-height trough five cells either side of the hull center, so the boat never rides a crest and the surface still avoids a mechanical fixed period.
Colors are standard ANSI foreground codes rather than theme lookups: every water cell is blue whatever its height, so the swell reads through glyph height alone rather than a crest-versus-trough color split, and the whole boat, both sail halves, the mast, and the complete hull including its zero-height interior, is one yellow, with no bright variant, 256-color, or RGB escape. Each colored run is closed with a default-foreground reset so styling cannot bleed into the sail row's padding, neighbouring UI, or a later frame, and geometry is always computed from visible cells rather than escape bytes.
The presentation is TUI-only and visual-only. It adds no session entry, transcript row, model context, or export or share content, and its widget takes no keyboard input, so editor focus and Escape abort are unchanged. Compaction and retry loaders remain stock because Pi exposes no supported replacement for them.
.pi/extensions/lib/fm-calm-visibility.ts owns only the allowlist-style transcript presentation policy.
bin/fm-operational-input.sh owns current cross-language operational-input construction and parsing, while the thin Pi adapter lives at .pi/extensions/lib/fm-operational-input.ts.
Only genuine-user-prompt, genuine-agent-response, and working-status are policy-visible.
Every other audited class is policy-hidden when Pi exposes a supported presentation boundary, but semantic input is never transformed to enforce that preference.
The home-local persistence schema is owned by docs/configuration.md.
On Pi, current session-start, watcher, turn-end guard, away supervisor, and launch-brief inputs use their versioned U+2063 static envelopes.
The established leading [fm-from-firstmate] plus U+2063 routing carrier remains current so running secondmate charters remain compatible.
Claude-bound typed away escalations and launch briefs instead use the record-backed carrier owned by bin/fm-operational-input.sh; its replay limit is described in calm.md.
Calm classifies only at Pi's transcript-presentation owner through the canonical parser and never replaces, reorders, or weakens those messages.
The session-start nudge already originates as a non-displayed custom message, so it remains on that existing path while retaining model context and session persistence.
Legacy Calm custom entries and messages remain in existing session artifacts, and their presentation entry still uses the supported zero-height renderer while active.
Toggling Calm cycles tool expansion and restores its original value, which rebuilds controllable rows and leaves final Ctrl+O state unchanged.
Returning from stock export rendering instead invalidates only the tool rows Calm currently presents: Pi 0.83.0 made every expansion change emit its own status line, and Pi coalesces consecutive status lines, so an expansion cycle there overwrote the Session exported to: confirmation the export had just printed.
Exported and shared HTML retain genuine user prompts, genuine assistant responses, current operational user messages, ordinary tool rendering, and the complete session artifact.
Serialized session data and Pi 0.81.1's sidebar tree also retain legacy hidden operational custom messages.
Every tool registered or supplied by Firstmate under .pi/extensions has this disposition:
| Tool | Registration surface | Calm disposition |
|---|---|---|
read, bash, edit, write, grep, find, ls |
Calm wrappers for Pi's seven main-session built-ins | Their call and text-result shells hide while Calm is active; ordinary and stock export rendering delegate to Pi's original renderers. |
fm_watch_arm_pi |
Main-session custom tool in fm-primary-pi-watch.ts |
Its complete self-rendered shell hides while Calm is active and returns unchanged when Calm is off or stock export rendering is active. |
fm_branch_outcomes |
Main-session custom tool in fm-branch-supervision.ts |
Its complete self-rendered shell hides while Calm is active; when visible, the self-renderer reconstructs Pi's ordinary boxed fallback shell, matches Pi's collapsed or expanded call-argument header, and probes Pi's rendered stock fallback to preserve that installed surface's collapsed or all-line result policy plus expanded state, while stock export rendering deliberately falls through to Pi's structured fallback. |
fm_branch_processed |
Main-session custom tool in fm-branch-supervision.ts |
Its complete self-rendered shell hides while Calm is active, exactly like fm_branch_outcomes; when visible, the self-renderer preserves Pi's call-argument header around the one-line acknowledgement result, while stock export rendering deliberately falls through to Pi's structured fallback. |
fm_branch_report |
Branch-session custom tool supplied directly to createAgentSession |
It runs only in the headless supervision session and has no main-session ToolExecutionComponent; successful execution writes the outcome store and delivers a routine note or exact captain entry through the separately audited delivery path, so the tool cannot emit a dump-shaped row in the captain's transcript. |
branch-local read built-in |
Branch-session built-in enabled through createAgentSession |
It runs only in the headless supervision session and has no main-session ToolExecutionComponent, so its file output cannot emit a row in the captain's transcript. |
branch-local bash override |
Branch-session replacement supplied directly to createAgentSession |
It runs only in the headless supervision session and has no main-session ToolExecutionComponent, so its command output cannot emit a row in the captain's transcript. |
No other .pi/extensions file registers or supplies a tool. Commands, lifecycle handlers, custom message renderers, and presentation adapters are not tool registrations and remain covered by the transcript taxonomy below.
The taxonomy was derived from Pi 0.81.1's installed public declarations, documentation, examples, interactive-mode.js, and its exported component implementations.
The test fixture enumerates every class below through the centralized policy, and the interactive fixture exercises the screenshot classes, current user-role operational input, and legacy synthetic presentation entries.
| Policy class | Pi transcript path | Calm result (baseline verified on Pi 0.81.1 through 0.82.0; newer evidence noted per row) |
|---|---|---|
genuine-user-prompt |
UserMessageComponent |
Visible, including every tested operational near miss. |
genuine-agent-response |
Assistant text in AssistantMessageComponent |
Visible. |
assistant-working-note |
Assistant text in an AssistantMessageComponent message the model did not end its response with, identified by its own stopReason of toolUse, or of length with tool calls present |
Each settled text block follows the cross-harness preservation contract in calm.md; hidden blocks are removed from the shallow presentation copy before layout, a toolUse message carrying only short narration occupies zero rows (verified on Pi 0.84.1), and a still-streaming pending message is never filtered. |
assistant-thinking |
Thinking content in AssistantMessageComponent |
Collapsed reasoning is removed from the shallow presentation copy before layout and occupies zero rows; explicit expansion renders the original reasoning. |
assistant-tool-call |
ToolExecutionComponent |
Seven built-ins, fm_watch_arm_pi, fm_branch_outcomes, and fm_branch_processed hidden; other arbitrary custom tools remain an unsupported boundary. |
tool-result |
ToolExecutionComponent |
Text results for the controlled tools hidden; other arbitrary custom results remain an unsupported boundary. |
tool-image |
Image children appended outside tool renderer slots | Unsupported boundary; remains visible. |
user-bash |
BashExecutionComponent for ! and !! |
Unsupported boundary; remains visible. |
skill-invocation |
SkillInvocationMessageComponent plus parsed user text |
Unsupported boundary; remains visible. |
custom-message |
CustomMessageComponent when display is true |
The session-start nudge and legacy Calm context messages use display: false; arbitrary extension messages remain an unsupported boundary. |
custom-entry |
CustomEntryComponent with a registered renderer |
Legacy Calm presentation entries rebuild to zero children without a residual spacer and restore through ordinary expansion redraw when mounted; arbitrary extension entries remain an unsupported boundary. |
compaction-summary |
CompactionSummaryMessageComponent |
Unsupported boundary; remains visible. |
branch-summary |
BranchSummaryMessageComponent |
Unsupported boundary; remains visible. |
working-status |
WorkingStatusIndicator, or the Calm working-ship widget while Calm is active |
Always visible. Calm off leaves Pi's stock row untouched; Calm on hides that row for the duration of one logical agent run and renders the working ship instead. |
command-status |
Interactive command result and status rows | Calm emits no enable notice, but generic Pi command rows remain an unsupported boundary. |
system-notice |
showStatus, showError, compaction, retry, and startup warning rows |
Unsupported boundary; remains visible. |
cache-notice |
Non-persisted cache-miss Text row |
Unsupported boundary; remains visible. |
project-trust-warning |
Non-persisted startup Text row |
Unsupported boundary; remains visible. |
synthetic-user |
Firstmate extension sendUserMessage, terminal-injected input, Firstmate-generated Pi positional brief, or the already non-displayed session-start nudge |
Canonically classified text-only operational user messages stay ordinary semantic user messages but render through the zero-height adapter under Calm; legacy entries stay gaplessly controllable, and the session-start nudge retains its existing non-displayed custom-message path. |
synthetic-assistant |
No authoritative Firstmate source found | Policy-hidden, but Pi exposes no generic assistant-role renderer. |
unknown |
Future or unclassified transcript component | Policy-hidden, but no generic renderer exists; never claimed as covered. |
The installed extension API has no supported global transcript filter, user-message renderer, assistant-message renderer, chat-container API, or generic custom-tool wrapper.
Pi 0.81.1 through 0.82.0, Pi 0.84.4, and Pi 0.85.1 export AssistantMessageComponent and InteractiveMode, so Calm uses separate idempotent, API-probed adapters for assistant thinking layout and the complete operational-user transcript row while leaving all message data and non-Calm rendering unchanged; see the compatibility contract for how a future Pi lacking one of those exports is handled.
General component replacement, ANSI cursor erasure, provider-context mutation, and installed-file patching remain rejected as unsupported or preservation-breaking workarounds.
The original five-harness inspection was performed on 2026-07-22, with every integration surface rechecked and Pi reverified at 0.81.1 on 2026-07-23 for the latest Calm presentation change.
$ claude --version
2.1.218 (Claude Code)
$ codex --version
codex-cli 0.144.6
$ opencode --version
1.17.18
$ pi --version
0.81.1
$ grok --version
grok 0.2.106 (bde89716f679)
| Harness | Conclusion | Evidence |
|---|---|---|
| Claude Code 2.1.272 (superseding the 2.1.218 row, which found no transcript-row renderer in project hooks or the plugin CLI) | Feasible through the early-access Claude Code mods surface (function hooks), default-off behind CLAUDE_CODE_ENABLE_FUNCTION_HOOKS, and initially shipped with the plugin name firstmate-calm (now fm; see calm.md). |
A ui.render hook draws per-component transcript rows and the working row, $.ui.invalidate redraws the transcript, and $.ui.blit animates a Raster; the 2026-09-15 record owns the spike-verified working animation, gapless hiding and retroactive redraw of tool, narration, and operational rows, the persisted per-home toggle, and the three bounded gaps: an early-access API that may change, main-screen scrollback keeping pre-toggle copies, and 256-color Raster paint. |
| Codex CLI 0.144.6 | Not feasible through the inspected supported project surface. | The tracked hooks expose session, pre-tool, and stop handling, while the plugin and feature inventories expose no TUI tool-row renderer or transcript redraw control. |
| OpenCode 1.17.18 | Not feasible without violating the preservation boundary. | Plugins expose events and tool execution hooks, not a built-in transcript-row renderer; same-name tool replacement changes execution rather than presentation alone. |
| Pi (verified 0.81.1 through 0.82.0) | Partially feasible with two API-probed exported-class adapters. | Public APIs control working visibility, collapsed labels, known tool slots, custom entries, and expansion redraws; exported assistant and interactive-mode classes provide the collapsed-thinking and operational-user layout boundaries, gated on the exact method's presence rather than a version number, while generic user, tool, and status filtering remains unavailable. |
| Grok CLI 0.2.106 | Not feasible through the inspected supported project surface. | Project hooks expose lifecycle and tool interception, while the plugin CLI exposes no row-renderer contract; --minimal changes the whole screen mode rather than selected transcript rows. |
These conclusions are deliberately limited to the named versions and supported surfaces. They do not claim that a harness can never add the missing renderer API, and the Claude Code row is the first that changed for exactly that reason. For the duplicate-turn fix and the latest presentation change, the launch templates for Claude, Codex, OpenCode, Pi, and Grok and the watcher, turn-end, session-start, away-supervisor, and from-firstmate producers were re-inspected. The canonical encoder and every non-Pi delivery path remain unchanged, and the tmux, Herdr, Zellij, Orca, cmux, and Paseo runtime surfaces continue to transport the same input selected by the harness adapter. Pi's Calm implementation changed only to consume the shared sprite core, while the new Claude Code mod changes drawings only; every producer and non-Pi transport remains unchanged.
On Pi 0.87.1 with Calm persisted on, a Firstmate watcher notification sent while a tool held the turn was listed under the running turn as Follow-up: FIRSTMATE_OP: v1 watcher: ..., identical to Calm off.
Pressing Escape moved that raw text into the editor and removed it from Pi's queue, and the session recorded no delivery of it, so a captain who cleared the editor lost the notification.
The initiating trigger was a notification queued during a run.
The exposure condition was that Pi draws queued input in InteractiveMode.updatePendingMessagesDisplay and restores it through restoreQueuedMessagesToEditor, a path separate from the addMessageToChat path the operational-user adapter covers.
The visible symptom was the listed row and, after Escape, the raw text in the editor.
Hiding the listed row alone would turn the Escape path into the defect issue #1588 describes: stock restore joins the whole queue into the editor, so a hidden notification would reappear as raw text.
Keeping it queued across the restore needs the session's already-expanded queueing entry points (_queueSteer and _queueFollowUp) and, for the delivery below, clearQueue, waitForIdle, sendUserMessage, and isIdle.
Those live on the session instance reached through InteractiveMode.session, so they are checked per session before the first row is hidden rather than at extension load.
A counterfactual built from the closed PR #1620 adapter hid the row and kept the notification out of the editor, but Pi 0.87.1's AgentSession._runAgentPrompt stops continuing once an abort was requested, so the kept follow-up stayed queued until the captain's next prompt while the adapter announced a new turn.
The shipped adapter therefore starts that turn itself once the aborted run settles: it takes the first queued message out, sends it with sendUserMessage, and puts the rest back behind it in Pi's delivery order.
Navigating the session tree during a run takes the same path without an abort flag, restoring the queue and then calling session.abort(), so the adapter waits for every restore that kept a notification and starts the turn only if the session is then idle with messages still queued.
Pi starts navigateTree in the same microtask run that resumes from that abort and marks the session busy before its first await, so the adapter yields one macrotask after each idle wait and waits again while the session is busy, which starts the turn on the navigated branch instead of racing the navigation on the abandoned one.
The same real-Pi reproduction then delivered the notification exactly once in a new turn, returned a queued captain message to the editor, and left Calm off stock.
tests/fm-calm-pi-extension.test.sh compares wrapped and stock renderers and verifies all seven built-ins plus fm_watch_arm_pi; its rendered HTML export check accepts either omission or default-hidden hook rows for legacy synthetic messages while rejecting visible leakage.
tests/fm-pi-branch-extension.test.sh verifies both fm_branch_outcomes and fm_branch_processed call headers against pre-0.99 and 0.99+ Pi stock rendering, plus Calm toggling, capability-probed all-line versus collapsed stock result output, exact expanded output, and export rendering for outcomes.
Together they exercise redraw of already-rendered tool, thinking, current operational-user, and legacy synthetic rows, and cover every policy class.
It covers persisted preference restoration across every session-start reason and a real restart, proves the working-ship presentation and Calm-off stock Working... row through a delayed deterministic provider, asserts no Calm status row, verifies operational messages remain exact ordinary user-role session entries and complete exports, and drives genuine 100 by 44, 160 by 36, and 180 by 44 terminal fixtures.
A native deterministic /skill:ahoy turn produces thinking, tool-call, and tool-result blocks, asserts that the collapsed skill-to-final gap equals the two-row visible-only baseline, expands and re-collapses original thinking, restores Calm-off rendering, verifies persisted hidden history, and repeats the geometry assertion after restart with terminal.clearOnShrink explicitly off.
The operational provider path covers Calm loaded on, loaded off, default preference, extension absent, exact watcher delivery, narrow bare-marker legacy input, persisted restart replay, a genuine captain prompt, and adjacent notifications coalesced into one intended processing turn.
It asserts one persisted and rendered captain answer, exact user-role operational envelopes in order, no replacement custom messages, one processing result, zero operational transcript rows, and the two-row neighboring-assistant geometry for live, adjacent, and restart paths.
Quoted current markers, ASCII-only labels, ordinary text before a marker, unrelated U+2063 placement, and image-bearing input remain visible in component and native transcript checks.
Queued-row coverage drives Pi's real listing and restore methods over a stand-in session for each capability-check branch, including a hidden row kept when the classifier cannot answer again and a refused continuation that re-queues instead of dropping, and repeats Escape in a real Pi TUI with Calm on, with a captain message queued beside the notification, and with Calm off.
tests/fm-calm-pi-queue-retention-live-e2e.test.sh is the default-on, token-free guard that probes a running Pi session for every member the check requires and fails naming the installed Pi version.
tests/fm-pi-primary-live-e2e.test.sh also proves the working ship replaces the built-in Working... row while Calm is active on the credentialed provider path, and that it clears when the run settles, before continuing its ordinary watcher lifecycle.
tests/fm-pi-primary-types.test.sh owns the strict production-consumer typecheck and its dependency contract.
tests/fm-calm-claude-mod.test.sh needs no Claude Code binary: it proves the mod is one hooks module with no command, skill, agent, or classic hook path around its opt-in, that Pi's working ship renders byte-for-byte the shared sprite core painted in ANSI at every width and step, that the Raster packing lays that frame out exactly, that the mod resolves its home like Pi, that its live and restored working-note classifiers enforce the visibility boundaries calm.md owns, and that its operational-input classifier agrees with bin/fm-operational-input.sh on a corpus the shell owner itself encodes plus legacy shapes and near misses.
tests/fm-calm-claude-mod-plugin.test.sh runs wherever claude is installed without spending a model turn: strict claude plugin validate on the folder and on the .claude/skills auto-load path, then the mod's own claude plugin test suites, which drive the hooks module in the engine's host against a mocked clock, environment, file system, and drawing surface.
tests/fm-calm-claude-mod-live-e2e.test.sh is the opt-in credentialed guard in a real Claude Code TUI under tmux: flag off is a complete no-op with the preference already on, flag on shows the moving boat, hides tool and operational rows, toggles and persists through /calm, and claude --continue restores the hidden rows.
The relevant commands are:
tests/fm-calm-pi-extension.test.sh
tests/fm-pi-branch-extension.test.sh
FM_PI_LIVE_E2E=1 tests/fm-pi-primary-live-e2e.test.sh
tests/fm-pi-primary-types.test.sh
tests/fm-calm-pi-queue-retention-live-e2e.test.sh
tests/fm-calm-claude-mod.test.sh
tests/fm-calm-claude-mod-plugin.test.sh
FM_CLAUDE_CALM_LIVE_E2E=1 tests/fm-calm-claude-mod-live-e2e.test.shThe deterministic provider preserves the complete real Pi TUI rendering path without using credentials. The credentialed live regression remains opt-in and was not required because this change does not alter watcher delivery or provider integration.
$ pi --version
0.81.1
$ tests/fm-calm-pi-extension.test.sh
ok - Pi calm extension is presentation-only with one persisted visibility choice, no Calm status row, native working visibility, supported redraw controls, and the Firstmate watcher-tool integration
ok - Pi calm resolves its persistent home independently of Pi's launch directory
ok - Pi calm centralizes transcript visibility, preserves execution/export data, keeps native working visible, and persists its choice across session starts
ok - Pi operational follow-up E2E processes exact user-role notifications once while Calm hides current and adjacent rows, Calm off and absent render them, and restart preserves semantics
ok - Pi Calm native /skill:ahoy geometry keeps every collapsed thinking and tool block at zero height while preserving expansion, history, restart, and Calm-off rendering
ok - Pi calm native E2E keeps Working and captain turns visible, hides exact operational user rows without changing persistence, restores them Calm-off, survives restart, and preserves export plus Ctrl+O behavior
$ tests/fm-pi-primary-types.test.sh
ok - tracked Pi extensions pass strict no-emit typecheck against Pi 0.81.1
$ bin/fm-lint.sh
fm-lint.sh: ShellCheck 0.11.0 (pinned 0.11.0)
$ bin/fm-test-run.sh --changed --base origin/main
FM_TEST_SUMMARY total=38 failed=0 skipped_gate=7 duration_ms=166881
FM_TEST_SUMMARY_FAMILY family=live-harness-optin count=7 duration_ms=192 failed=0
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=31 duration_ms=165384 failed=0
$ tests/fm-pi-primary-live-e2e.test.sh
skip: set FM_PI_LIVE_E2E=1 to run the isolated interactive Pi regression
Pi 0.82.0 preserved both API-probed presentation seams and every deterministic Calm TUI guarantee. The globally installed declaration package remained 0.81.1, so the strict typecheck continued to cover that earlier declaration-evidence version while the real CLI exercised 0.82.0.
$ pi --version
0.82.0
$ tests/fm-calm-pi-extension.test.sh
ok - Pi calm extension is presentation-only with one persisted visibility choice, no Calm status row, native working visibility, supported redraw controls, and the Firstmate watcher-tool integration
ok - Pi calm resolves its persistent home independently of Pi's launch directory
ok - Pi calm centralizes transcript visibility, preserves execution/export data, keeps native working visible, and persists its choice across session starts
ok - Pi operational follow-up E2E processes exact user-role notifications once while Calm hides current and adjacent rows, Calm off and absent render them, and restart preserves semantics
ok - Pi Calm native /skill:ahoy geometry keeps every collapsed thinking and tool block at zero height while preserving expansion, history, restart, and Calm-off rendering
ok - Pi calm native E2E keeps Working and captain turns visible, hides exact operational user rows without changing persistence, restores them Calm-off, survives restart, and preserves export plus Ctrl+O behavior
$ tests/fm-pi-primary-types.test.sh
ok - tracked Pi extensions pass strict no-emit typecheck against Pi 0.81.1
This record captures the first working-presentation implementation and is retained as pipeline history. Its same-orientation sail, theme-derived colors, and single-cadence motion were all replaced later the same day; the revision record at the end of this document owns current behavior.
The working ship was verified against the installed Pi 0.82.0 CLI with a deterministic in-process provider and no credentials.
The globally installed declaration package remained 0.81.1, so the strict typecheck continued to cover that declaration-evidence version while the real CLI exercised 0.82.0.
The real-TUI regression captures two frames at different hull columns, resizes the same running TUI, asserts the reflowed water row equals the new width on a single wave row, types into the editor while the animation runs, aborts with Escape, and then proves Pi's stock Working... row returns with Calm off.
$ pi --version
0.82.0
$ tests/fm-calm-pi-extension.test.sh
ok - Pi calm resolves its persistent home independently of Pi's launch directory
ok - Pi calm compatibility evidence never rejects a Pi version for being newer than 0.82.0, and still fails closed on a missing or malformed version
ok - a missing collapsed-thinking presentation API degrades only that Calm adapter with a clear skip reason, while the rest of Calm still registers
ok - missing Pi presentation class exports reach the independent adapter degradation path
ok - Pi calm centralizes transcript visibility, preserves execution/export data, keeps Pi's stock working row visible while no run is active, and persists its choice across session starts
ok - Pi operational follow-up E2E processes exact user-role notifications once while Calm hides current and adjacent rows, Calm off and absent render them, and restart preserves semantics
ok - Pi Calm native /skill:ahoy geometry keeps every collapsed thinking and tool block at zero height while preserving expansion, history, restart, and Calm-off rendering
ok - Pi Calm working ship renders an exact two-row full-width sprite, clamps every resize, bounces at both edges, falls back deterministically when narrow, and installs and removes one timer-owning widget across starts, settle, abort, failure, shutdown, reload, replacement, and Calm toggles
ok - Pi calm native E2E replaces the stock working row with a moving, resize-clamped working ship that clears on abort, keeps captain turns visible, hides exact operational user rows without changing persistence, restores stock rendering Calm-off, survives restart, and preserves export plus Ctrl+O behavior
$ tests/fm-pi-primary-types.test.sh
ok - tracked Pi extensions pass strict no-emit typecheck against Pi 0.81.1
$ bin/fm-lint.sh
fm-lint.sh: ShellCheck 0.11.0 (pinned 0.11.0)
$ bin/fm-doc-audience-check.sh
fm-doc-audience-check: ok surfaces=57 local_links=160
$ bin/fm-test-run.sh --changed --base origin/main
FM_TEST_SUMMARY total=32 failed=0 skipped_gate=7 duration_ms=196009
FM_TEST_SUMMARY_FAMILY family=live-harness-optin count=7 duration_ms=202 failed=0
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=25 duration_ms=194670 failed=0
One rendered frame at 120 columns, with Pi's stock working row hidden and the boat directly above the editor:
|>
\__/~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The same run after resizing that TUI to 64 columns, showing the waves refilled to the new width on one row with the boat still on screen:
|>
~~~~~~~~~~~~~~~~~~~~~~~~\__/~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Colors at that time were confirmed from an escape-preserving capture as theme-derived entries; the revision below replaced them with standard ANSI blue and yellow.
Pressing Escape during a run left Operation aborted with no boat and no residual blank row, and toggling Calm off restored Pi's stock ⠴ Working... row on the next run.
The revision replaced the single-cadence, theme-colored, same-orientation sprite with a slower boat over independently animated water, standard ANSI colors, and a directional mainsail. It was verified against the installed Pi 0.82.0 CLI with a deterministic in-process provider and no credentials.
$ pi --version
0.82.0
$ tests/fm-pi-primary-types.test.sh
ok - tracked Pi extensions pass strict no-emit typecheck against Pi 0.81.1
$ bin/fm-lint.sh
fm-lint.sh: ShellCheck 0.11.0 (pinned 0.11.0)
$ bin/fm-doc-audience-check.sh
fm-doc-audience-check: ok surfaces=57 local_links=163
$ bin/fm-test-run.sh --changed --base origin/main
FM_TEST_SUMMARY total=32 failed=0 skipped_gate=7 duration_ms=386738
FM_TEST_SUMMARY_FAMILY family=live-harness-optin count=7 duration_ms=257 failed=0
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=25 duration_ms=383010 failed=0
Real Pi TUI observations from the isolated deterministic trial at 100 columns. The hull column held steady across consecutive samples while the water pattern shifted, then advanced about one column every 880ms, which separates the two cadences:
hull_col=12 water=~-~~~-~~~-~\__/~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~
hull_col=12 water=~~~-~~~-~~~\__/-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-
hull_col=13 water=~~-~~~-~~~-~\__/~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~
hull_col=16 (about 2.6s later)
An escape-preserving capture confirmed standard ANSI foreground codes only, blue water and yellow boat, with a default-foreground reset closing each run:
^[[34m~~~-~~~-~~~-~~~^[[33m\__/^[[34m-~~~-~~~-~~~-~~~-...
^[[33m<|^[[39m
Resizing the same running TUI to 12 columns shortened the track enough to observe both reversals, each already showing the heading it was about to travel:
left-heading : |> over ~-~~~-~~\__/
right-heading: <| over \__/~~-~~~-~
At 3 columns the sprite fell back to a single exact-width row, <|~.
Escape aborted the run leaving Operation aborted, no boat, and no stale sprite rows, and the trial exited 0 after deleting its temporary state.
Pi 0.83.0 added a status line to every tool-expansion change, which silently broke the /export confirmation under Calm on Pi 0.83.0 and newer.
Pi appends Session exported to: <path> through showStatus, which updates the previous status line in place whenever two status messages arrive back to back with nothing else added to the chat.
Calm's post-export redraw cycled tool expansion on the macrotask right after that, so both of its expansion status lines coalesced over the confirmation and left no record of where the export landed.
Calm now invalidates only the tool rows it presents and requests the redraw through setStatus, neither of which appends to the transcript.
Pi source evidence, from the installed release's own changelog and interactive mode:
$ pi --version
0.84.1
CHANGELOG.md, 0.83.0 "Fixed":
- Added a status line when the tool output expansion is toggled ([#7180](https://github.com/earendil-works/pi/issues/7180)).
interactive-mode setToolsExpanded:
setToolsExpanded(expanded) {
if (expanded === this.toolOutputExpanded)
return;
...
this.showStatus(`Tool output: ${expanded ? "expanded" : "collapsed"}`);
}
The regression is pinned by the real-terminal /export case in tests/fm-calm-pi-extension.test.sh, which now asserts the confirmation is still on screen after Calm's redraw has settled and that the redraw restored every Calm-hidden row.
Reverting only the extension fix fails that assertion deterministically rather than racing the roughly 50ms window the confirmation used to survive:
not ok - Calm's post-export repaint overwrote Pi's export confirmation (missing: 'Session exported to: .../calm-export.html')
$ tests/fm-calm-pi-extension.test.sh
ok - Pi calm resolves its persistent home independently of Pi's launch directory
ok - Pi calm compatibility evidence never rejects a Pi version for being newer than 0.82.0, and still fails closed on a missing or malformed version
ok - a missing collapsed-thinking presentation API degrades only that Calm adapter with a clear skip reason, while the rest of Calm still registers
ok - missing Pi presentation class exports reach the independent adapter degradation path
ok - Calm registers none of its 7 built-in tool wrappers at load while config/calm is off, and all 7 synchronously at load while config/calm is on
ok - Calm's first same-session /calm activation claims every uncontested built-in, leaves a foreign bash tool fully intact and callable, warns prominently and logs the contested name, and only rows constructed before that activation - the documented bound - fail to retroactively collapse
ok - Pi calm centralizes transcript visibility, preserves execution/export data, keeps Pi's stock working row visible while no run is active, and persists its choice across session starts
ok - Pi calm on collapses mid-turn assistant working notes to zero height while Calm off keeps them, leaves streaming, truncated-final, and genuine final replies untouched, never mutates the messages, ignores every /calm argument, and restores a legacy persisted max as ordinary Calm on
ok - Pi operational follow-up E2E processes exact user-role notifications once while Calm hides current and adjacent rows, Calm off and absent render them, and restart preserves semantics
ok - Pi Calm native /skill:ahoy geometry keeps every collapsed thinking and tool block at zero height while preserving expansion, history, restart, and Calm-off rendering
ok - Pi Calm working ship moves on a slow independent cadence over faster fixed-cell blue water, paints the complete boat standard yellow with balanced resets, keeps ANSI-stripped width exact, flips the directional sail on the exact bounce at both edges and every width, clamps visible and hidden resizes, falls back deterministically when narrow, freezes and resumes column/direction across settle/start without hidden-time jumps or duplicate timers, resets only on a fresh session, and installs and removes one scheduler-owning widget across starts, settle, abort, failure, shutdown, reload, replacement, and Calm toggles while leaving Calm-off visibility untouched
ok - Pi calm native E2E replaces the stock working row with a moving, resize-clamped working ship that freezes and resumes across two working periods in one Pi session, clears on abort, keeps captain turns visible, hides exact operational user rows without changing persistence, restores stock rendering Calm-off, survives restart, and preserves export plus Ctrl+O behavior
$ tests/fm-pi-primary-types.test.sh
ok - tracked Pi extensions pass strict no-emit typecheck against Pi 0.80.10
$ bin/fm-lint.sh
fm-lint.sh: ShellCheck 0.11.0 (pinned 0.11.0)
$ bin/fm-doc-audience-check.sh
fm-doc-audience-check: ok surfaces=68 local_links=253
$ bin/fm-test-run.sh --changed --base origin/main
FM_TEST_SUMMARY total=46 failed=0 skipped_gate=16 duration_ms=279390
FM_TEST_SUMMARY_FAMILY family=live-harness-optin count=16 duration_ms=431 failed=0
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=30 duration_ms=277700 failed=0
Pi 0.84.4's stock ToolExecutionComponent collapses a text result longer than ten lines, adds Pi's expansion hint, and renders every line when expanded, while the previously verified Pi 0.81.1 stock fallback renders every line in both states.
The fm_branch_outcomes self-renderer now probes the installed component's rendered capability once rather than branching on a version number, then applies that discovered preview policy while preserving Pi's exact expanded result.
Calm still hides the complete row while active, restores the probed stock behavior when turned off, and delegates stock HTML export rendering to Pi.
The real installed-package comparison and the portable legacy-capability case are both executable through:
bin/fm-test-run.sh tests/fm-pi-branch-extension.test.shObserved against installed @earendil-works/pi-coding-agent 0.84.4:
ok - fm_branch_outcomes hides through ToolExecutionComponent while Calm-off and HTML export stay stock
ok - the installed Pi still bounds the picker's list and ranks its search
FM_TEST_END 2026-08-29T01:01:30Z tests/fm-pi-branch-extension.test.sh exit=0 duration_ms=22418 gate_skip=false
The real renderer comparison exercised twelve outcome lines and reported collapsed and expanded parity with Pi stock, zero visible rows under Calm, restored stock parity after toggling Calm off, and delegated stock HTML export fallback.
This host tracks Pi latest, so the version this contract's evidence is pinned to moves.
The renderer and lifecycle evidence below was taken against installed @earendil-works/pi-coding-agent 0.85.1 with @earendil-works/pi-server 0.85.0 also installed globally.
Calm's rendered rows are unchanged across 0.84.4, 0.85.0, and 0.85.1.
FM_PI_PACKAGE_DIR points tests/fm-calm-pi-extension.test.sh at an isolated install, so each comparison ran against its own temporary dependency tree and never mutated the globally installed packages.
$ pi --version
0.85.1
$ npm ls -g --depth 0 @earendil-works/pi-coding-agent @earendil-works/pi-server
├── @earendil-works/pi-coding-agent@0.85.1
└── @earendil-works/pi-server@0.85.0
$ FM_PI_PACKAGE_DIR=<pi 0.84.4> tests/fm-calm-pi-extension.test.sh
ok - Pi calm centralizes transcript visibility, preserves execution/export data, keeps Pi's stock working row visible while no run is active, and persists its choice across session starts
$ FM_PI_PACKAGE_DIR=<pi 0.85.0> tests/fm-calm-pi-extension.test.sh
ok - Pi calm centralizes transcript visibility, preserves execution/export data, keeps Pi's stock working row visible while no run is active, and persists its choice across session starts
$ FM_PI_PACKAGE_DIR=<pi 0.85.1> tests/fm-calm-pi-extension.test.sh
ok - Pi calm centralizes transcript visibility, preserves execution/export data, keeps Pi's stock working row visible while no run is active, and persists its choice across session starts
Reaching that parity on 0.85 took one contract adaptation, landed earlier in 85ad5e7.
Pi 0.84 and older silently substituted a built-in's stock definition when a ToolExecutionComponent was constructed without one, so the calm-off equivalence baseline could be built definition-less and still read as stock.
Pi 0.85 removed that substitution, so the definition-less baseline renders Pi's generic text fallback instead - which is what produced read collapsed rendering changed while calm mode was off.
The renderer change was real, and it was the contract's baseline that had to adapt, not Calm's wrappers: the wrapped rows matched Pi stock before and after.
tests/fm-calm-pi-extension.test.sh now builds each baseline from the real stock tool-definition factories that dist/core/tools/index.js exports, calling the built-in's own factory with process.cwd(), which reads as stock on 0.84.4 and on 0.85.x alike and no longer depends on the removed substitution.
Pi 0.85.0 alone requires a package it does not declare.
Its dist/experimental/server.js statically imports @earendil-works/pi-server, which is absent from 0.85.0's dependencies, peerDependencies, and optionalDependencies, so a clean install of 0.85.0 on its own cannot load Pi's interactive mode at all:
Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@earendil-works/pi-server' imported from
.../node_modules/@earendil-works/pi-coding-agent/dist/experimental/server.js
Installing @earendil-works/pi-server@0.85.0 beside it restores the identical Calm rendering, and 0.85.1 no longer reaches that import.
That packaging gap is a separate installation defect, not the renderer change above: it stops Pi from loading at all rather than altering any rendered row.
The could not render calm-mode HTML export DOM failure was a headless-Chrome start-up failure, not a change in Pi's export shape.
It appeared in exactly one of the thirteen most recent CI runs, and that run installed the same Pi 0.85.1 as the runs immediately before and after it, which both passed.
The render step is a vendor-tool step: the assertions that follow it are what protect the Calm conversation boundary.
The failure later reproduced deterministically against Google Chrome for Testing 151.0.7922.34, whose first-run initialization never completes when Chrome is pointed at a brand-new --user-data-dir: the browser and its renderers start, but --dump-dom never returns, so every bounded attempt times out with no bytes.
The render step now gives each attempt a private HOME (with XDG_CONFIG_HOME and XDG_CACHE_HOME beneath it) instead of an explicit --user-data-dir on Linux and every other non-Darwin system, because Chrome creates and initializes its own profile there and renders the same document in about a second, while removing that HOME still gives every attempt a private profile.
macOS derives its profile directory from ~/Library regardless of HOME, so Darwin keeps the explicit --user-data-dir that was this file's original isolation.
It still retries a bounded number of Chrome start-ups and, when every attempt fails, reports the Chrome binary, its version, the installed Pi version, each attempt's exit status, whether that attempt was timed out, and Chrome's own stderr, so the next occurrence is diagnosable from the CI log alone.
test_export_dom_render_guard in the same script pins that behavior with real processes and no browser.
The complete Calm suite against installed Pi 0.85.1, with FM_CHROME_BIN naming the Chrome the render step used:
$ FM_CHROME_BIN=<chrome> tests/fm-calm-pi-extension.test.sh
ok - Pi calm resolves its persistent home independently of Pi's launch directory
ok - Pi calm compatibility evidence never rejects a Pi version for being newer than 0.82.0, and still fails closed on a missing or malformed version
ok - a missing collapsed-thinking presentation API degrades only that Calm adapter with a clear skip reason, while the rest of Calm still registers
ok - missing Pi presentation class exports reach the independent adapter degradation path
ok - Calm registers none of its 7 built-in tool wrappers at load while config/calm is off, and all 7 synchronously at load while config/calm is on
ok - Calm's first same-session /calm activation claims every uncontested built-in, leaves a foreign bash tool fully intact and callable, warns prominently and logs the contested name, and only rows constructed before that activation - the documented bound - fail to retroactively collapse
ok - Pi calm centralizes transcript visibility, preserves execution/export data, keeps Pi's stock working row visible while no run is active, and persists its choice across session starts
ok - Pi calm on collapses mid-turn assistant working notes to zero height while Calm off keeps them, leaves streaming, truncated-final, and genuine final replies untouched, never mutates the messages, ignores every /calm argument, and restores a legacy persisted max as ordinary Calm on
ok - Pi operational follow-up E2E processes exact user-role notifications once while Calm hides current and adjacent rows, Calm off and absent render them, and restart preserves semantics
ok - Pi Calm native /skill:ahoy geometry keeps every collapsed thinking and tool block at zero height while preserving expansion, history, restart, and Calm-off rendering
ok - Pi Calm working ship moves on a slow independent cadence over faster fixed-cell blue water, paints the complete boat standard yellow with balanced resets, keeps ANSI-stripped width exact, flips the directional sail on the exact bounce at both edges and every width, clamps visible and hidden resizes, falls back deterministically when narrow, freezes and resumes column/direction across settle/start without hidden-time jumps or duplicate timers, resets only on a fresh session, and installs and removes one scheduler-owning widget across starts, settle, abort, failure, shutdown, reload, replacement, and Calm toggles while leaving Calm-off visibility untouched
ok - the rendered-export-DOM guard renders in one pass, retries a bounded number of Chrome start-up failures, and reports the Chrome binary, Chrome version, Pi version, exit status, and Chrome diagnostic when every attempt fails
ok - Pi calm native E2E replaces the stock working row with a moving, resize-clamped working ship that freezes and resumes across two working periods in one Pi session, clears on abort, keeps captain turns visible, hides exact operational user rows without changing persistence, restores stock rendering Calm-off, survives restart, and preserves export plus Ctrl+O behavior
The queued-row adapter was verified on Linux 7.0.0 x86_64, Node v22.23.1, and tmux against the globally installed @earendil-works/pi-coding-agent 0.87.1, with TypeScript 7.0.2 installed only for the typecheck.
Every Pi run used a scratch home, project, agent directory, and session directory with a local faux provider, so no model request left the machine.
pi --version
tests/fm-calm-pi-queue-retention-live-e2e.test.sh
tests/fm-calm-pi-extension.test.sh
tests/fm-pi-primary-types.test.sh0.87.1
ok - Pi 0.87.1 exposes every queue-retention member Calm preflights before hiding queued Firstmate rows
ok - Calm hides queued Firstmate rows only on a session that can keep them, keeps hidden ones out of the editor on Escape, delivers them once in order, and leaves unsupported sessions and Calm off stock
ok - Pi 0.87.1 with Calm on keeps a queued Firstmate notification unlisted, out of the editor on Escape, and delivers it once in a new announced turn, while Calm off stays stock
ok - tracked Pi extensions pass strict no-emit typecheck against Pi 0.87.1
The rest of tests/fm-calm-pi-extension.test.sh passed unchanged in the same run.
With a member name the running session does not have added to the adapter's required list, the live guard failed as designed:
not ok - Pi 0.87.1 lacks the queue-retention capability Calm needs to hide queued Firstmate rows: session._queueNotARealMember
With the queued-row adapter left uninstalled, the real-Pi Escape case failed on the listed notification, Pi Calm listed a queued Firstmate notification.
Claude Code 2.1.272 exposes exactly the capability the 2026-07-22 row found missing, through its early-access "Claude Mods" surface, whose engineering primitive is the function hook: a plugin whose behavior lives in one hooks module exporting register(on, options), hooking dotted engine events as ($, e, next) middleware, with ui.render drawing per-component transcript rows and the working row, $.ui.invalidate("ui.render") redrawing every hooked drawing, and $.ui.blit repainting a mounted Raster without a render pass.
The surface is default-off: hooks modules load only when the tengu_plugin_hooks_modules rollout flag or the CLAUDE_CODE_ENABLE_FUNCTION_HOOKS environment variable turns them on, never under safe mode, disableAllHooks, or a managed-hooks-only policy, and only after workspace trust is accepted.
The generated declarations (/plugin-types) carry the header "EARLY ACCESS: this surface may change between releases without notice", and the public proposal invites testing behind that variable while the feature is not yet in the public docs or CHANGELOG.
The feasibility spike (scout fm-claude-mods-calm-sailboat-s1, whose private report holds the raw captures) and the shipped firstmate-calm mod both use only that documented-in-binary plugin API; the shipped mod also checks that CLAUDE_CODE_ENABLE_FUNCTION_HOOKS is exactly 1 before any preference read, transcript read, timer, command registration, or drawing change, so loading its module through the rollout flag alone remains a complete no-op. Nothing patches installed Claude Code code, and no prompt, tool, or session event is rewritten.
$ claude --version
2.1.272 (Claude Code)
$ tmux -V
tmux 3.6a
| Surface | Can it own the working indicator? | Can it hide or redraw transcript rows? | Evidence |
|---|---|---|---|
Mods, ui.render |
Yes: the Spinner component (word, message, mode, requestId the agent id, e.viewport.columns), replaced by a Raster repainted through $.ui.blit at the frame rate. |
Yes: UserMessage, AssistantMessage (one text block), ToolUse, ToolResult, ToolGroup, CommandOutput, TurnDuration, and more, each rewrite changing the drawing and leaving the stored message alone; $.ui.invalidate("ui.render") redraws every instance the plugin may draw. |
The declarations' RenderComponent, RenderPropsOf, UiBlitArgs, and RasterProps, the spike captures below, and the shipped mod's tests. |
statusLine command |
No: it renders in the footer, its input has no turn-running field, and it refreshes at most once per second. | No. | Binary settings schema and the status-line docs; not spiked. |
Spinner settings (spinnerVerbs, spinnerTipsEnabled, prefersReducedMotion) |
No: text and tips only, no frames or hiding. | No. | Binary settings schema. |
/focus view mode |
No. | Coarse only: the stock "prompt, summary, and response" view, fullscreen only, not a per-row policy. | Binary command source. |
| Classic settings hooks | No. | No: decision, context, system message, and terminal-sequence outputs only. | Unchanged from the 2026-07-22 record. |
Every capture came from real Claude Code 2.1.272 TUIs under tmux at 160 by 44 cells, driven by Haiku, with an isolated FM_HOME and the inherited session markers stripped.
- The stock
✽ Verb… (Ns · tokens)row is absent while the boat draws in its place; over 23 working frames at 0.4s spacing the hull advanced one column every 0.8s to 0.9s (the 880ms cadence), the water row changed on every frame (the quarter-cell swell), the water width was exactly 158 (the 160-cell viewport minus the transcript's 2-cell margin), and the sail stayed one column right of the hull. - When the turn settled the boat was gone with no residual row, on both the fullscreen (
CLAUDE_CODE_NO_FLICKER=1) and main-screen (CLAUDE_CODE_NO_FLICKER=0) layouts. - Resizing the running TUI 160 to 64 to 12 to 160 columns reflowed the boat to 62, 10, and 158 cells of water within one frame of each resize settling, with the track clamped and direction flipping at the narrow edges.
- A narrated two-tool turn drawn with Calm off redrew after
/calmwith only the prompt and the final reply, at the same single-row spacing as a turn that never used tools; toggling off restored the narration, theBash(...)row, and theRead 1 filegroup, and toggling on hid them again. - An exact watcher-shaped operational input typed at idle drew no user row while the genuine prompt that followed stayed visible, and session storage held it as one ordinary user entry with its exact U+2063 bytes, answered once.
- The first Calm-on turn's storage held both tool uses, both results, its text, and its thinking blocks intact.
- Launching with
config/calmalreadyonstarted Calm on, and after/exitandclaude --continuethe restored tool rows and operational row stayed hidden from the first frame; the first spike build failed that, because restored rows drew before itssession.startloaded the preference, which is why every hook of the shipped mod awaits one cached load. - A trusted project folder holding only
.claude/skills/<mod>(a symlink to the plugin) loaded the mod with no launch flag once the flag was on, logginghooks module <name> loaded (worker, environment 1, tier user); without the flag the same folder loggedhooks modules not loaded: rollout flag (tengu_plugin_hooks_modules) is off. - Each render dispatch settled well under 3ms in the debug log.
An escape-preserving capture of the boat from the spike, taken before the palette was unified on 2026-09-15 and so still showing a cyan crest and a red sail half, shows the Raster's RGB quantized to 256-color escapes; the shipped mod paints Claude Code's own theme colors through the same quantization, the spinner blue of the active family for every water cell (#93a5ff dark, #5769f7 light) and the Claude orange of the stock spinner (#d77757) for the whole boat, choosing the family from the theme setting's prefix at load and on every theme change, with the light set as the both-readable fallback for auto, custom, missing, or unreadable values, while the Pi extension keeps standard ANSI blue and yellow:
\x1b[38;5;184m◿│\x1b[38;5;167m◣\x1b[39m
\x1b[38;5;69m▁▁▁\x1b[38;5;184m╲\x1b[38;5;69m▁▁▁\x1b[38;5;184m╱\x1b[38;5;69m▁▁▁▁▁▂▂▂\x1b[38;5;38m▃▃▄▄▄▄▄▃▃▃\x1b[38;5;69m▂▂
| Requirement | Result on Claude Code 2.1.272 |
|---|---|
| Auto-load from the trusted project | Met, behind the flag: the project's .claude/skills/<mod> entry, a symlink or directory, is adopted as a <mod>@skills-dir plugin after trust; dot-prefixed entries are skipped, and hooks-module imports must resolve physically inside the plugin folder. |
| Persist the toggle for the effective home across starts and resumes | Met: the same config/calm file and values as Pi, resolved the same way; the plugin API's $.fs.write is a plain write rather than Pi's temp-plus-rename. |
| Keep working activity visible | Met: the boat draws in place of Spinner on every working frame on both layouts. |
| Emit no Calm status row | Met: /calm answers with a transient toast and no output row. |
| Redraw already-rendered controllable rows | Met through $.ui.invalidate("ui.render"), with the main-screen scrollback caveat below. |
| Remove supported hidden rows without gaps | Met: zero-height display: "none" boxes; spacing equals the no-tool baseline. |
| Restore ordinary rendering when off | Met: hooks return next(e); the stock rows and stock spinner return. |
| Leave delivery, tool execution, model context, session storage, and export unchanged | Met for storage and context; only ui.render rewrites drawings and no other event is hooked for effect. |
| Collapsed thinking | Not needed: no thinking row appears in the default view, and there is no thinking drawing to hook elsewhere. |
| Arbitrary third-party rows | Better than Pi: ToolUse, ToolResult, and ToolGroup hooks see every tool, built-in, MCP, or plugin, with no same-name override collision. |
- The whole surface is early access and default-off, and its API may change between releases without notice; the real TUI behavior is verified on Claude Code 2.1.272, the plugin compatibility guard also passes on 2.1.273, the mod refuses nothing newer, and
tests/fm-calm-claude-mod-plugin.test.shis the check that says when a newer Claude Code stops accepting it. - On the main-screen (non-fullscreen) layout a toggle redraws the live screen by clearing and reprinting the whole conversation, and the terminal's own scrollback keeps the previous rendering above it; the fullscreen layout has no such stale copy.
- The Raster paints RGB through a quantized palette, so the boat renders as 256-color escapes rather than Pi's standard 16-color ANSI codes.
Three further observations, recorded so they are not read as failures: the ctrl+o detailed transcript view keeps its per-message timestamp and model headers where hidden assistant rows sat, because those headers are not a render component; on 2.1.272 the /calm toggle's answer was a transient toast under the prompt (firstmate-calm: Calm on) that expired within a few seconds and never became a transcript row; and the engine logs one benign debug-level warning at load, options requested but its manifest declares no userConfig, for every hooks module whose manifest declares no configuration fields, which an empty userConfig object does not silence.
.claude/mods/firstmate-calm holds the plugin: its manifest, hooks/hooks.json naming the one module, hooks/register.ts (the only file that touches $), and pure libraries the tests drive under Node: the sprite core both harnesses share, the Raster packing, the presentation policy, and a port of bin/fm-operational-input.sh's classify guarded by a corpus parity test.
.agents/skills/firstmate-calm is a symlink to it, so the project's .claude/skills scan adopts it, and it carries no SKILL.md so other harnesses' skill loaders see nothing.
The mod declares no command file, skill, agent, or classic hook; its function-hooks handlers independently require the exact environment opt-in before /calm registration or any other side effect, including when Claude Code loads the module through its rollout flag.
Working-note and preserved-reply keys are recorded from turn.step per text block and seeded from $.session.messages() for a restored transcript, with calm.md owning the exact Claude Code visibility contract.
$ CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude plugin validate --strict .claude/mods/firstmate-calm
❯ ./register.ts hooks: session.start, command.run{command=calm}, config.set{key=theme}, turn.step, ui.render{component=Spinner}, ui.render{component=ToolUse}, ui.render{component=ToolResult}, ui.render{component=ToolGroup}, ui.render{component=UserMessage}, ui.render{component=AssistantMessage}
❯ ./register.ts calls: $.clock.every (via load), $.command.register, $.config.list (via readTheme), $.env.get (via isActivated, load), $.fs.read (via readPreference), $.fs.write, $.session.messages (via load), $.ui.blit (via repaintShip), $.ui.invalidate, $.ui.resolve, $.ui.toast
❯ ./register.ts env writes: nothing
❯ ./register.ts env reads: CLAUDE_CODE_ENABLE_FUNCTION_HOOKS, FM_CONFIG_OVERRIDE, FM_HOME, FM_ROOT_OVERRIDE
✔ Validation passed
$ CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude plugin test .claude/mods/firstmate-calm
40 pass
0 fail
Ran 40 tests across 2 files.
$ bin/fm-test-run.sh tests/fm-calm-claude-mod.test.sh
ok - the Calm mod is one hooks module, linked into the project's auto-load path, with no command, skill, agent, or classic hook path that bypasses its exact opt-in
ok - the Pi working ship renders byte-for-byte the shared sprite core's frame painted in standard ANSI, at every width, cadence step, freeze, clamp, and reset
ok - the Raster packing lays the shared frame out row-major with the sprite's palette, plain padding, default backgrounds, BMP glyphs, clipping, and a standard base64 encoding
ok - the Calm policy resolves the shared preference exactly as Pi does, reads on, max, and off as Pi does, and shares Pi's 240-character-or-newline preservation behavior while classifying working notes by stop reason, tool use, and restored transcript shape
ok - the mod's operational-input classifier agrees with bin/fm-operational-input.sh on all 77 corpus cases: every current kind the owner encodes, every legacy shape, and every near miss
$ bin/fm-test-run.sh tests/fm-calm-pi-extension.test.sh
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=68438
The Pi suite above ran against the extracted sprite core with every one of its thirteen cases green, including the working-ship geometry and the interactive TUI case, which is the evidence that the extraction left Pi's drawing unchanged.
Later the same day the installed Claude Code auto-updated to 2.1.273, and tests/fm-calm-claude-mod-plugin.test.sh passed there as well: strict validation accepts the mod from both paths, including the theme hook and configuration read shown above, and the plugin-kit suites pass with the theme cases added.
The opt-in live guard, run on this host against the installed Claude Code 2.1.272 with tmux 3.6a and Haiku, through the shipped .claude/skills auto-load path, an isolated project and FM_HOME, and the preference already on before the flag-off session:
$ FM_CLAUDE_CALM_LIVE_E2E=1 tests/fm-calm-claude-mod-live-e2e.test.sh
ok - Claude Code 2.1.272 (Claude Code) with the flag unset: no hooks module, no /calm, stock working row, stock tool rows, preference on ignored
ok - Claude Code 2.1.272 (Claude Code) with the flag on: the mod auto-loads from .claude/skills, /calm exists, the sailboat replaces and moves in the working row, tool and operational rows draw at zero height, /calm restores and re-hides them while persisting the shared preference
ok - Claude Code 2.1.272 (Claude Code) resumes the transcript with Calm's hidden rows still hidden and the preference intact
$ bin/fm-test-run.sh tests/fm-calm-claude-mod-plugin.test.sh
ok - Claude Code 2.1.272 (Claude Code) validates the Calm mod strictly at its folder and its auto-load path, hooking exactly the working row, tool, user, and assistant drawings and /calm
ok - Claude Code 2.1.272 (Claude Code) runs the Calm mod's plugin test suites clean: persisted toggle, hidden rows, working notes, and the clock-driven working ship
The flag-off session's settled screen, with the preference on on disk, drew Claude Code's own rows exactly as a session without the mod does:
❯ Run this exact bash command with the Bash tool: sleep 5; cat notes.txt Then reply with one short sentence naming the three words.
Ran 1 shell command
⏺ The three words are alpha, beta, and gamma.
✻ Sautéed for 8s · done 11:07 AM
Claude Code 2.1.280 removes invisible characters, U+2063 included, from every submitted prompt, whether typed, pasted, or passed as the launch prompt.
A typed operational envelope first shows Removed 1 invisible character · review and press Enter to send, and the next Enter stores it as plain FIRSTMATE_OP: ... text that no consumer can tell apart from a human message.
No setting or environment variable turns the removal off.
For the current delivery and presentation contracts, see fm-operational-input.sh and calm.md.
2.1.280 also logs the module load as hooks module firstmate-calm@<source> loaded (@skills-dir for the project auto-load path), so the live guard matches either form.
Observed on 2.1.280 with the flag on, beyond the live guard:
$ claude --version
2.1.280 (Claude Code)
$ bash tests/fm-calm-claude-mod.test.sh
ok - the mod's operational-input classifier agrees with bin/fm-operational-input.sh on all 77 corpus cases: every current kind the owner encodes, every legacy shape, and every near miss
ok - the mod's doorbell port agrees with bin/fm-operational-input.sh doorbell-kind on all 28 cases: every record the owner writes and every unbacked or malformed near miss
$ bash tests/fm-calm-claude-mod-plugin.test.sh
ok - Claude Code 2.1.280 (Claude Code) validates the Calm mod strictly at its folder and its auto-load path, hooking exactly the working row, tool, user, and assistant drawings and /calm
ok - Claude Code 2.1.280 (Claude Code) runs the Calm mod's plugin test suites clean: persisted toggle, hidden rows, working notes, and the clock-driven working ship
The live guard in its current form is recorded on 2.1.282 in the next section.
The failure was reproduced end to end on the installed Claude Code 2.1.282 in a disposable lab home and project on a private tmux socket, never touching the default tmux server or any real home.
- Typed path:
tmux send-keys -lofFIRSTMATE_OP: v1 away-supervisor: Supervisor escalate <test events>, then Enter, left the composer showingRemoved 1 invisible character · review and press Enter to send; a second Enter submitted it, and the stored session transcript heldFIRSTMATE_OP: v1 away-supervisor: Supervisor escalate ...with no U+2063 byte. - Launch-prompt path: launching
claudewith the encoded launch-brief envelope as the prompt argument printedRemoved 1 invisible character from the launch prompt before sending it; the stored transcript row kept the brief text but no U+2063. - With the record-backed doorbell: the away-mode daemon's
inject_msgdelivered the doorbell to the real Claude pane as a composer-visible ASCII line only, and the live guard passed.
$ claude --version
2.1.282 (Claude Code)
$ FM_CLAUDE_CALM_LIVE_E2E=1 bash tests/fm-calm-claude-mod-live-e2e.test.sh
ok - Claude Code 2.1.282 (Claude Code) with the flag unset: no hooks module, no /calm, stock working row, stock tool rows, preference on ignored
ok - Claude Code 2.1.282 (Claude Code) with the flag on: the mod auto-loads from .claude/skills, /calm exists, the sailboat replaces and moves in the working row, tool rows and the record-backed operational doorbell draw at zero height, /calm restores and re-hides them while persisting the shared preference
ok - Claude Code 2.1.282 (Claude Code) resumes the transcript with Calm's hidden rows still hidden and the preference intact
The mod's supervision notes were verified on the installed Claude Code 2.1.283 in disposable lab homes and projects on private tmux sockets, with the outcome store written by the real bin/fm-branch-outcome.sh.
$.ui.logdraws each note as its own system-notice row: a gray⏺bullet, then the mod's name, then the text, for example⏺ firstmate-calm: ⚓ [seq 1] fm-quiet-hold-for-return-landing-r1: PR https://..., wrapped at the terminal width.- The note is stored in the session transcript as a display-only entry,
{"type":"system","subtype":"informational","content":"firstmate-calm: ⚓ [seq 2] fm-live-b: LIVE_REPLAY_CAPTAIN still open","level":"notice",...}, andclaude --continuerestores it. The 2.1.274 plugin declarations say only that the line is not sent to the model, so the mod records how far each session has shown the store in its plugin store and replays only newer outcomes on resume. - A Haiku turn asked to quote every sailboat or anchor line in the conversation quoted none of the notes on screen, so they did not reach the model.
- Every rejected
$.fs.reador$.fs.statis logged as[ERROR]in the debug log, so the mod checks$.fs.existsfirst for the files it polls.
$ claude --version
2.1.283 (Claude Code)
$ bash tests/fm-calm-claude-mod-plugin.test.sh
ok - Claude Code 2.1.283 (Claude Code) validates the Calm mod strictly at its folder and its auto-load path, hooking exactly the working row, tool, user, and assistant drawings and /calm, and logging supervision notes
ok - Claude Code 2.1.283 (Claude Code) runs the Calm mod's plugin test suites clean: persisted toggle, hidden rows, working notes, the clock-driven working ship, and supervision notes
$ FM_CLAUDE_CALM_LIVE_E2E=1 bash tests/fm-calm-claude-mod-live-e2e.test.sh
ok - Claude Code 2.1.283 (Claude Code) with the flag unset: no hooks module, no /calm, stock working row, stock tool rows, preference on ignored
ok - Claude Code 2.1.283 (Claude Code) with the flag on: the mod auto-loads from .claude/skills, /calm exists, the sailboat replaces and moves in the working row, tool rows and the record-backed operational doorbell draw at zero height, /calm restores and re-hides them while persisting the shared preference
ok - Claude Code 2.1.283 (Claude Code) resumes the transcript with Calm's hidden rows still hidden and the preference intact
ok - Claude Code 2.1.283 (Claude Code) with Calm off shows the supervision notes: the session-start anchor for an unprocessed captain outcome, a sailboat for a new routine outcome, an anchor for a new captain outcome, and the latch-trip note, skipping processed and silent outcomes, moving no store marker, never reaching the model, and on resume showing each anchor once
The label in front of each supervision note is Claude Code's, not the mod's, so the plugin is named fm to keep it short.
- The mod hands
$.ui.logthe glyph-first line, as the debug log shows:[DEBUG] [firstmate-calm] $.ui.log: ⚓ [seq 1] fm-repro-a: REPRO_CAPTAIN open. - Claude Code 2.1.284 turns every transcript
$.ui.logline into a system-notice entry whose content is<plugin name>: <text>, after theui.loghook chain has run;UiLogOptionsoffers onlyto: "transcript" | "debug", noui.rendercomponent draws that row, and no other$call appends a transcript row. - With the manifest named
fm, the row draws as⏺ fm: ⚓ [seq 1] fm-repro-a: REPRO_CAPTAIN open, is stored as"content":"fm: ⚓ [seq 1] ...", and the module loads ashooks module fm@skills-dir loaded; the folders keep theirfirstmate-calmnames, whichclaude plugin validate --strictaccepts. $.storelives in one file per plugin id under Claude Code's configuration directory (plugins/store/fm_skills-dir-<hash>.json), so the rename starts an empty store and a session resumed across it replays its still-due notes once.
$ claude --version
2.1.284 (Claude Code)
$ bash tests/fm-calm-claude-mod-plugin.test.sh
ok - Claude Code 2.1.284 (Claude Code) validates the Calm mod strictly at its folder and its auto-load path, hooking exactly the working row, tool, user, and assistant drawings and /calm, and logging supervision notes
ok - Claude Code 2.1.284 (Claude Code) runs the Calm mod's plugin test suites clean: persisted toggle, hidden rows, working notes, the clock-driven working ship, and supervision notes
$ FM_CLAUDE_CALM_LIVE_E2E=1 bash tests/fm-calm-claude-mod-live-e2e.test.sh
ok - Claude Code 2.1.284 (Claude Code) with the flag unset: no hooks module, no /calm, stock working row, stock tool rows, preference on ignored
ok - Claude Code 2.1.284 (Claude Code) with the flag on: the mod auto-loads from .claude/skills, /calm exists, the sailboat replaces and moves in the working row, tool rows and the record-backed operational doorbell draw at zero height, /calm restores and re-hides them while persisting the shared preference
ok - Claude Code 2.1.284 (Claude Code) resumes the transcript with Calm's hidden rows still hidden and the preference intact
ok - Claude Code 2.1.284 (Claude Code) with Calm off shows the supervision notes: the session-start anchor for an unprocessed captain outcome, a sailboat for a new routine outcome, an anchor for a new captain outcome, and the latch-trip note, each behind the fm: label, skipping processed and silent outcomes, moving no store marker, never reaching the model, and on resume showing each anchor once