Skip to content

fix(calm): share the Pi working-ship widget slot with standalone Calm - #1854

Open
ironerumi wants to merge 2 commits into
kunchenguid:mainfrom
ironerumi:fm/firstmate-calm-upstream-pr-v1
Open

fix(calm): share the Pi working-ship widget slot with standalone Calm#1854
ironerumi wants to merge 2 commits into
kunchenguid:mainfrom
ironerumi:fm/firstmate-calm-upstream-pr-v1

Conversation

@ironerumi

Copy link
Copy Markdown

Intent

The developer wanted the already-validated Firstmate Calm widget de-duplication fix contributed upstream to kunchenguid/firstmate:main as a narrow pull request, under strict process constraints: read the current upstream contribution standards (AGENTS.md, CONTRIBUTING.md, the no-mistakes-required workflow, the coding-guidelines skill, and two scout reports) before touching anything; branch from freshly fetched upstream/main rather than the fork's history; and apply only the exact two-file patch from fork PR #17 (pointing Calm's widget key at the standalone Calm's shared calm-working-ship slot with a contract comment, plus a focused regression assertion in tests/fm-calm-pi-extension.test.sh), importing no unrelated fork commits or fork-private files. Delivery had to go through upstream's required no-mistakes pipeline with ironerumi/firstmate as push-only fork and kunchenguid/firstmate:main as PR base, without --yes and without merging. A hard constraint was commit identity: only a provably captain-owned, GitHub-attributable identity (ironerumi 39033099+ironerumi@users.noreply.github.com) was acceptable, with no agent co-author, and the unexplained sim sim@example.com pipeline identity was forbidden — the developer wanted the agent to stop and escalate rather than invent or rewrite identity. Mid-run the developer approved captain decisions to fix two review findings (add the smallest ungated constant-parity regression so CI enforces the shared key, and one concise docs/calm.md sentence on dual-install single-boat behavior) while keeping the shared-slot midrun-toggle finding as informational, to drop the scope-widening .gitignore entry, and to strip sim-authored commits — culminating in a read-only investigation of whether worktree-scoped git config could expose the captain identity to the pipeline, and then approval of a temporary repository identity switch for one authorized replacement validation run.

What Changed

  • CALM_WORKING_SHIP_WIDGET_KEY moves from the private firstmate-calm-working-ship slot to calm-working-ship, the key the standalone Pi Calm extension already uses. Pi replaces widgets per key, so a session that loads both Calms now renders one boat instead of two; either extension alone renders as before. A contract comment records that the slot must be renamed in both implementations together.
  • tests/fm-calm-pi-extension.test.sh gains test_working_ship_widget_key_parity, a source-level assertion on the literal key that runs with no Pi installed, alongside the same check inside the working-ship fixture (which skips where Pi is absent, including CI). Review confirmed the ungated half is all this repo can enforce, since the standalone implementation lives outside this tree.
  • docs/calm.md states the dual-install consequence: one boat, not two.

Test reported a pre-existing test_interactive_terminal_e2e failure in the same file that reproduces on base commit fb368dc and is untouched here.

Risk Assessment

✅ Low: A one-line widget-key change with no stale references anywhere in the repo, backed by an ungated regression assertion that actually executes in CI and a single accurate docs sentence; the only residual concern is the deliberately accepted cross-extension slot-sharing tradeoff, which cannot regress single-install behavior.

Testing

Ran the single targeted suite tests/fm-calm-pi-extension.test.sh (twice) plus a base-commit baseline of the same suite, and a CI-shaped run with Pi, node and tmux removed from the environment to prove the new ungated parity assertion still executes where every Pi fixture skips; a mutation of the widget key made that assertion fail with its intended contract message, confirming it is a real guard. For product-level evidence I built a dual-install harness that launches real Pi 0.83.0 sessions in tmux with Calm on, the Firstmate Calm extension, and a stand-in for the standalone Pi Calm extension that claims the same working-row slot, and captured the live working row mid-run: the tracked code shows exactly one boat, the pre-fix private key in the same harness shows two stacked boats, and a Firstmate-Calm-only session is unchanged. Those captures and the new docs sentence are rendered into an HTML page and a screenshot. Everything related to the intent passes; the only failure, the live interactive /export e2e, reproduces identically on the base commit and traces to stale test expectations against the newer locally installed Pi rather than to this change.

  • Evidence: Working-row captures: before (2 boats) / after (1 boat) / solo, plus the docs/calm.md sentence (local file: /var/folders/l8/tyd57dc53v3938ttkwmw5kgr0000gp/T/no-mistakes-evidence/01KZCWGBSGYZRX5CJRXTJY6EQ9/dual-calm-evidence.png)
Evidence: Same evidence as a rendered HTML page (color-faithful tmux panes)
<!doctype html>
<html><head><meta charset="utf-8"><title>Firstmate Calm shared working-ship slot</title>
<style>
  body { background:#11141a; color:#dcdfe4; font-family:-apple-system,Segoe UI,sans-serif; margin:0; padding:28px 32px; }
  h1 { font-size:20px; margin:0 0 4px; }
  .sub { color:#8d94a1; font-size:13px; margin:0 0 24px; }
  .card { margin-bottom:26px; }
  h2 { font-size:14px; font-weight:600; margin:0 0 4px; color:#dcdfe4; }
  h2 code { font-size:13px; color:#e5c07b; }
  .badge { color:#11141a; font-size:11px; font-weight:700; padding:2px 8px; border-radius:10px; margin-left:8px; }
  .caption { color:#8d94a1; font-size:12px; margin:0 0 8px; }
  pre.pane { background:#1b1f27; color:#dcdfe4; font-family:"SF Mono",Menlo,monospace;
    font-size:11.5px; line-height:1.32; padding:12px 14px; border-radius:8px; margin:0;
    border:1px solid #2a3040; overflow:hidden; white-space:pre; }
</style></head><body>
<h1>Pi Calm working-row widget slot — live tmux pane captures</h1>
<p class="sub">Real <code>pi 0.83.0</code> sessions, Calm on, deterministic offline provider, 100&times;30 tmux pane, captured mid-run.</p>

    <section class="card">
      <h2>BEFORE — pre-fix private slot <code>firstmate-calm-working-ship</code> <span class="badge" style="background:#e06c75">2 boats</span></h2>
      <p class="caption">Both Calms loaded: two boats stacked in the working row.</p>
      <pre class="pane"><span style="color:#dcdfe4;background:#1b1f27">
 </span><span style="color:#8abeb7;background:#1b1f27;font-weight:700">pi</span><span style="color:#666666;background:#1b1f27"> v0.83.0</span><span style="color:#dcdfe4;background:#1b1f27">
 </span><span style="color:#666666;background:#1b1f27">escape</span><span style="color:#808080;background:#1b1f27"> interrupt · </span><span style="color:#666666;background:#1b1f27">ctrl+c/ctrl+d</span><span style="color:#808080;background:#1b1f27"> clear/exit · </span><span style="color:#666666;background:#1b1f27">/</span><span style="color:#808080;background:#1b1f27"> commands · </span><span style="color:#666666;background:#1b1f27">!</span><span style="color:#808080;background:#1b1f27"> bash · </span><span style="color:#666666;background:#1b1f27">ctrl+o</span><span style="color:#808080;background:#1b1f27"> more</span><span style="color:#dcdfe4;background:#1b1f27">
 </span><span style="color:#666666;background:#1b1f27">Press ctrl+o to show full startup help and loaded resources.</span><span style="color:#dcdfe4;background:#1b1f27">

 </span><span style="color:#666666;background:#1b1f27">Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.</span><span style="color:#dcdfe4;background:#1b1f27">

</span><span style="color:#f0c674;background:#1b1f27">[Extensions]</span><span style="color:#dcdfe4;background:#1b1f27">
</span><span style="color:#666666;background:#1b1f27">  fm-calm.ts, slow-provider.ts, standalone-calm.ts</span><span style="color:#dcdfe4;background:#1b1f27">


 </span><span style="color:#ffff00;background:#1b1f27">Warning: tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys
</span><span style="color:#dcdfe4;background:#1b1f27"> </span><span style="color:#ffff00;background:#1b1f27">on` to ~/.tmux.conf and restart tmux.</span><span style="color:#dcdfe4;background:#1b1f27">

</span><span style="color:#dcdfe4;background:#343541">
 </span><span style="color:#d4d4d4;background:#343541">sail on</span><span style="color:#dcdfe4;background:#343541">


</span><span style="color:#dcdfe4;background:#1b1f27"> DUAL_CAL

  </span><span style="color:#e5c07b;background:#1b1f27">&lt;|</span><span style="color:#dcdfe4;background:#1b1f27">
</span><span style="color:#61afef;background:#1b1f27">-</span><span style="color:#e5c07b;background:#1b1f27">\__/</span><span style="color:#61afef;background:#1b1f27">~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~
</span><span style="color:#dcdfe4;background:#1b1f27">       </span><span style="color:#e5c07b;background:#1b1f27">&lt;|</span><span style="color:#dcdfe4;background:#1b1f27">
</span><span style="color:#61afef;background:#1b1f27">~~~~~~</span><span style="color:#e5c07b;background:#1b1f27">\__/</span><span style="color:#61afef;background:#1b1f27">~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</span><span style="color:#505050;background:#1b1f27">────────────────────────────────────────────────────────────────────────────────────────────────────
</span><span style="color:#1b1f27;background:#dcdfe4"> </span><span style="color:#dcdfe4;background:#1b1f27">
</span><span style="color:#505050;background:#1b1f27">────────────────────────────────────────────────────────────────────────────────────────────────────
</span><span style="color:#666666;background:#1b1f27">/private/var/folders/l8/tyd57dc53v3938ttkwmw5kgr0000gp/T/dual-calm-private.LVL0w1/project (master)</span><span style="color:#dcdfe4;background:#1b1f27">
</span><span style="color:#666666;background:#1b1f27">0.0%/4.1k (auto)                                                                    (dual-calm) slow

</span></pre>
    </section>
    <section class="card">
      <h2>AFTER — shared slot <code>calm-working-ship</code> (this change) <span class="badge" style="background:#98c379">1 boat</span></h2>
      <p class="caption">Both Calms loaded: Pi replaces per key, so exactly one boat renders.</p>
      <pre class="pane"><span style="color:#dcdfe4;background:#1b1f27">
 </span><span style="color:#8abeb7;background:#1b1f27;font-weight:700">pi</span><span style="color:#666666;background:#1b1f27"> v0.83.0</span><span style="color:#dcdfe4;background:#1b1f27">
 </span><span style="color:#666666;background:#1b1f27">escape</span><span style="color:#808080;background:#1b1f27"> interrupt · </span><span style="color:#666666;background:#1b1f27">ctrl+c/ctrl+d</span><span style="color:#808080;background:#1b1f27"> clear/exit · </span><span style="color:#666666;background:#1b1f27">/</span><span style="color:#808080;background:#1b1f27"> commands · </span><span style="color:#666666;background:#1b1f27">!</span><span style="color:#808080;background:#1b1f27"> bash · </span><span style="color:#666666;background:#1b1f27">ctrl+o</span><span style="color:#808080;background:#1b1f27"> more</span><span style="color:#dcdfe4;background:#1b1f27">
 </span><span style="color:#666666;background:#1b1f27">Press ctrl+o to show full startup help and loaded resources.</span><span style="color:#dcdfe4;background:#1b1f27">

 </span><span style="color:#666666;background:#1b1f27">Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.</span><span style="color:#dcdfe4;background:#1b1f27">

</span><span style="color:#f0c674;background:#1b1f27">[Extensions]</span><span style="color:#dcdfe4;background:#1b1f27">
</span><span style="color:#666666;background:#1b1f27">  fm-calm.ts, slow-provider.ts, standalone-calm.ts</span><span style="color:#dcdfe4;background:#1b1f27">


 </span><span style="color:#ffff00;background:#1b1f27">Warning: tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys
</span><span style="color:#dcdfe4;background:#1b1f27"> </span><span style="color:#ffff00;background:#1b1f27">on` to ~/.tmux.conf and restart tmux.</span><span style="color:#dcdfe4;background:#1b1f27">

</span><span style="color:#dcdfe4;background:#343541">
 </span><span style="color:#d4d4d4;background:#343541">sail on</span><span style="color:#dcdfe4;background:#343541">


</span><span style="color:#dcdfe4;background:#1b1f27"> DUAL_CAL

       </span><span style="color:#e5c07b;background:#1b1f27">&lt;|</span><span style="color:#dcdfe4;background:#1b1f27">
</span><span style="color:#61afef;background:#1b1f27">~~~~~~</span><span style="color:#e5c07b;background:#1b1f27">\__/</span><span style="color:#61afef;background:#1b1f27">~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</span><span style="color:#505050;background:#1b1f27">────────────────────────────────────────────────────────────────────────────────────────────────────
</span><span style="color:#1b1f27;background:#dcdfe4"> </span><span style="color:#dcdfe4;background:#1b1f27">
</span><span style="color:#505050;background:#1b1f27">────────────────────────────────────────────────────────────────────────────────────────────────────
</span><span style="color:#666666;background:#1b1f27">/private/var/folders/l8/tyd57dc53v3938ttkwmw5kgr0000gp/T/dual-calm-shared.xlCBFB/project (master)</span><span style="color:#dcdfe4;background:#1b1f27">
</span><span style="color:#666666;background:#1b1f27">0.0%/4.1k (auto)                                                                    (dual-calm) slow



</span></pre>
    </section>
    <section class="card">
      <h2>AFTER — Firstmate Calm alone, shared slot <span class="badge" style="background:#98c379">1 boat</span></h2>
      <p class="caption">Single-install session is unchanged: its own one boat.</p>
      <pre class="pane"><span style="color:#dcdfe4;background:#1b1f27">
 </span><span style="color:#8abeb7;background:#1b1f27;font-weight:700">pi</span><span style="color:#666666;background:#1b1f27"> v0.83.0</span><span style="color:#dcdfe4;background:#1b1f27">
 </span><span style="color:#666666;background:#1b1f27">escape</span><span style="color:#808080;background:#1b1f27"> interrupt · </span><span style="color:#666666;background:#1b1f27">ctrl+c/ctrl+d</span><span style="color:#808080;background:#1b1f27"> clear/exit · </span><span style="color:#666666;background:#1b1f27">/</span><span style="color:#808080;background:#1b1f27"> commands · </span><span style="color:#666666;background:#1b1f27">!</span><span style="color:#808080;background:#1b1f27"> bash · </span><span style="color:#666666;background:#1b1f27">ctrl+o</span><span style="color:#808080;background:#1b1f27"> more</span><span style="color:#dcdfe4;background:#1b1f27">
 </span><span style="color:#666666;background:#1b1f27">Press ctrl+o to show full startup help and loaded resources.</span><span style="color:#dcdfe4;background:#1b1f27">

 </span><span style="color:#666666;background:#1b1f27">Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.</span><span style="color:#dcdfe4;background:#1b1f27">

</span><span style="color:#f0c674;background:#1b1f27">[Extensions]</span><span style="color:#dcdfe4;background:#1b1f27">
</span><span style="color:#666666;background:#1b1f27">  fm-calm.ts, slow-provider.ts</span><span style="color:#dcdfe4;background:#1b1f27">


 </span><span style="color:#ffff00;background:#1b1f27">Warning: tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys
</span><span style="color:#dcdfe4;background:#1b1f27"> </span><span style="color:#ffff00;background:#1b1f27">on` to ~/.tmux.conf and restart tmux.</span><span style="color:#dcdfe4;background:#1b1f27">

</span><span style="color:#dcdfe4;background:#343541">
 </span><span style="color:#d4d4d4;background:#343541">sail on</span><span style="color:#dcdfe4;background:#343541">


</span><span style="color:#dcdfe4;background:#1b1f27"> DUAL_CAL

  </span><span style="color:#e5c07b;background:#1b1f27">&lt;|</span><span style="color:#dcdfe4;background:#1b1f27">
</span><span style="color:#61afef;background:#1b1f27">~</span><span style="color:#e5c07b;background:#1b1f27">\__/</span><span style="color:#61afef;background:#1b1f27">-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~
</span><span style="color:#505050;background:#1b1f27">────────────────────────────────────────────────────────────────────────────────────────────────────
</span><span style="color:#1b1f27;background:#dcdfe4"> </span><span style="color:#dcdfe4;background:#1b1f27">
</span><span style="color:#505050;background:#1b1f27">────────────────────────────────────────────────────────────────────────────────────────────────────
</span><span style="color:#666666;background:#1b1f27">/private/var/folders/l8/tyd57dc53v3938ttkwmw5kgr0000gp/T/dual-calm-solo.y4NzKk/project (master)</span><span style="color:#dcdfe4;background:#1b1f27">
</span><span style="color:#666666;background:#1b1f27">0.0%/4.1k (auto)                                                                    (dual-calm) slow



</span></pre>
    </section>
    <section class="card">
      <h2>docs/calm.md — captain-facing contract sentence added by this change</h2>
      <p class="caption">Renders in the Calm mode doc directly under the working-row paragraph.</p>
      <pre class="pane" style="white-space:pre-wrap">
While Calm is active and an agent run is under way, Calm hides Pi&#x27;s built-in `Working...` row and shows a small two-row animated boat in its place, and no separate Calm status row is added.
<span style="background:#2d4a33;color:#c8f0d2">The boat claims the same Pi working-row widget slot as the standalone Pi Calm extension, so a session that loads both Calms shows one boat rather than two.</span>
The water fills the usable width in standard ANSI blue and the complete boat is standard ANSI yellow.</pre>
    </section>
</body></html>
Evidence: Dual-install working row — pre-fix private key vs shared key (live Pi 0.83.0 tmux panes)
$ ./dual-calm-e2e.sh private # CALM_WORKING_SHIP_WIDGET_KEY = "firstmate-calm-working-ship"
VARIANT=private BOAT_ROWS=2
21: <|
22:-\__/~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~
23: <|
24:~~~~~~\__/~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

$ ./dual-calm-e2e.sh shared # CALM_WORKING_SHIP_WIDGET_KEY = "calm-working-ship" (this change)
VARIANT=shared BOAT_ROWS=1
21: <|
22:~~~~~~\__/~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

$ ./dual-calm-e2e.sh solo # Firstmate Calm alone, shared key
VARIANT=solo BOAT_ROWS=1
21: <|
22:~\__/-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~~-~~
Evidence: CI-shaped run: every Pi fixture skips, the new key guard still enforces the contract
$ env -i PATH=/usr/bin:/bin:/usr/sbin:/sbin FM_PI_PACKAGE_DIR=/nonexistent bash bin/fm-test-run.sh tests/fm-calm-pi-extension.test.sh
skip: node or npm not found for Pi calm renderer test
skip: pi or tmux not found for Pi Calm hidden-block geometry E2E
ok - Firstmate Calm's working-row widget key stays on the shared standalone Calm slot
skip: node or npm not found for Pi Calm working-ship test
skip: pi or tmux not found for Pi calm interactive E2E
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=1

# same command after mutating the key to "firstmate-calm-working-ship":
not ok - Firstmate Calm must keep the working-row widget key "calm-working-ship" so it shares the standalone Calm slot and dual-install sessions render one boat
FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0
Evidence: Dual-install e2e harness used to produce the captures
#!/usr/bin/env bash
# Dual-install end-to-end evidence: run a real Pi session in tmux with BOTH the
# Firstmate Calm extension and a stand-in for the standalone Pi Calm extension
# (which installs its working-row boat under the "calm-working-ship" widget key),
# then capture the live working row.
#
#   variant "shared"  -> tracked source (fix applied): expect ONE boat
#   variant "private" -> pre-fix key "firstmate-calm-working-ship": expect TWO boats
set -u

ROOT=${ROOT:?ROOT (repo worktree) required}
EVID=${EVID:?EVID (evidence dir) required}
VARIANT=${1:?variant: shared|private}

WORK=$(mktemp -d "${TMPDIR:-/tmp}/dual-calm-$VARIANT.XXXXXX")
project="$WORK/project"
home="$WORK/home"
config="$WORK/config"
SOCK="dual-calm-$VARIANT-$$"
SESSION="dual-calm"

cleanup() {
  tmux -L "$SOCK" kill-server 2>/dev/null || true
  rm -rf "$WORK"
}
trap cleanup EXIT

mkdir -p "$project/.pi/extensions/lib" "$home/config" "$config"
git -C "$project" init -q
printf '# dual-calm\n' >"$project/README.md"
git -C "$project" add README.md
git -C "$project" -c user.name='Firstmate Tests' -c user.email='tests@example.invalid' commit -qm initial

cp "$ROOT/.pi/extensions/fm-calm.ts" "$project/.pi/extensions/fm-calm.ts"
for lib in fm-calm-assistant-layout fm-calm-operational-user-layout fm-calm-visibility fm-calm-working-ship fm-operational-input; do
  cp "$ROOT/.pi/extensions/lib/$lib.ts" "$project/.pi/extensions/lib/$lib.ts"
done

if [ "$VARIANT" = "private" ]; then
  # Reproduce the pre-fix state: Firstmate Calm claims its own private slot.
  sed -i '' 's/CALM_WORKING_SHIP_WIDGET_KEY = "calm-working-ship"/CALM_WORKING_SHIP_WIDGET_KEY = "firstmate-calm-working-ship"/' \
    "$project/.pi/extensions/lib/fm-calm-working-ship.ts"
fi
grep -n 'CALM_WORKING_SHIP_WIDGET_KEY =' "$project/.pi/extensions/lib/fm-calm-working-ship.ts"

printf 'on\n' >"$home/config/calm"
printf '%s\n' '{"hideThinkingBlock":true,"terminal":{"clearOnShrink":false}}' >"$config/settings.json"

# Stand-in for the standalone Pi Calm extension: same working-row slot, same
# two-row boat presentation, independent implementation.
cat >"$project/standalone-calm.ts" <<'TS'
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

const WIDGET_KEY = "calm-working-ship";
const BLUE = "�[34m";
const YELLOW = "�[33m";
const RESET = "�[39m";

export default function (pi: ExtensionAPI): void {
  const factory = (tui: any) => {
    let phase = 0;
    let disposed = false;
    const timer = setInterval(() => {
      if (disposed) return;
      phase = (phase + 1) % 4;
      tui.requestRender();
    }, 240);
    timer.unref?.();
    return {
      render: (width: number) => {
        if (disposed || width <= 0) return [];
        const column = 6;
        const wave = (n: number) => `${BLUE}${"~".repeat(Math.max(0, n))}${RESET}`;
        return [
          " ".repeat(column + 1) + `${YELLOW}<|${RESET}`,
          wave(column) + `${YELLOW}\\__/${RESET}` + wave(width - column - 4),
        ];
      },
      invalidate: () => {},
      dispose: () => {
        disposed = true;
        clearInterval(timer);
      },
    };
  };
  pi.on("agent_start", (_event: unknown, ctx: any) => {
    ctx.ui.setWidget(WIDGET_KEY, factory);
    ctx.ui.setWorkingVisible(false);
  });
  pi.on("agent_settled", (_event: unknown, ctx: any) => {
    ctx.ui.setWidget(WIDGET_KEY, undefined);
    ctx.ui.setWorkingVisible(true);
  });
}
TS

# Deterministic offline provider that streams slowly, so the working row stays on
# screen long enough to photograph.
cat >"$project/slow-provider.ts" <<'TS'
import { createFauxCore, fauxAssistantMessage, fauxText } from "@earendil-works/pi-ai";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI): void {
  const faux = createFauxCore({
    api: "dual-calm-api",
    provider: "dual-calm",
    models: [{
      id: "slow",
      name: "Dual Calm evidence model",
      reasoning: false,
      input: ["text"],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow: 4096,
      maxTokens: 512,
    }],
    tokensPerSecond: 2,
    tokenSize: { min: 1, max: 1 },
  });
  faux.setResponses([
    fauxAssistantMessage([
      fauxText("DUAL_CALM_DONE steady as she goes one two three four five six seven eight"),
    ]),
  ]);
  pi.registerProvider("dual-calm", {
    baseUrl: "http://127.0.0.1/unused",
    apiKey: "test-only",
    api: faux.api,
    models: faux.models,
    streamSimple: faux.streamSimple,
  });
  pi.registerCommand("dual-calm", {
    description: "Select the deterministic dual-Calm evidence model.",
    handler: async (_args: unknown, ctx: any) => {
      const model = ctx.modelRegistry.find("dual-calm", "slow");
      if (!model || !(await pi.setModel(model))) throw new Error("dual-calm model unavailable");
    },
  });
}
TS

# variant "solo": only Firstmate Calm is loaded, proving the shared key leaves a
# single-install session rendering exactly its own one boat.
STANDALONE="-e ./standalone-calm.ts"
[ "$VARIANT" = "solo" ] && STANDALONE=""

capture() { tmux -L "$SOCK" capture-pane -p -t "$SESSION" >"$1" 2>/dev/null; }
capture_ansi() { tmux -L "$SOCK" capture-pane -e -p -t "$SESSION" >"$1" 2>/dev/null; }
wait_for() {
  local text=$1 f="$WORK/wait.txt" i=0
  while [ "$i" -lt 300 ]; do
    capture "$f" || true
    grep -Fq "$text" "$f" 2>/dev/null && return 0
    sleep 0.05
    i=$((i + 1))
  done
  return 1
}

tmux -L "$SOCK" new-session -d -s "$SESSION" -x 100 -y 30 \
  "cd '$project' && env FM_HOME='$home' PI_CODING_AGENT_DIR='$config' PI_OFFLINE=1 pi --approve --no-context-files --no-skills --no-prompt-templates --no-extensions -e ./.pi/extensions/fm-calm.ts $STANDALONE -e ./slow-provider.ts; sleep 5"

wait_for "fm-calm.ts" || { echo "FAIL: pi never reached the composer"; capture /dev/stdout; exit 1; }
tmux -L "$SOCK" send-keys -t "$SESSION" -l '/dual-calm'
tmux -L "$SOCK" send-keys -t "$SESSION" Enter
sleep 0.4
tmux -L "$SOCK" send-keys -t "$SESSION" -l 'sail on'
tmux -L "$SOCK" send-keys -t "$SESSION" Enter

# Wait for the boat to appear, then hold a frame.
i=0
while [ "$i" -lt 200 ]; do
  capture "$WORK/live.txt" || true
  grep -Fq '\__/' "$WORK/live.txt" && break
  sleep 0.05
  i=$((i + 1))
done
sleep 1.2
capture "$EVID/working-row-$VARIANT.txt"
capture_ansi "$EVID/working-row-$VARIANT.ansi"

boats=$(grep -c -F '\__/' "$EVID/working-row-$VARIANT.txt" || true)
echo "VARIANT=$VARIANT BOAT_ROWS=$boats"
echo "--- live working row ---"
grep -n -F -e '\__/' -e '<|' -e '|>' -e 'Working' "$EVID/working-row-$VARIANT.txt" || true

tmux -L "$SOCK" send-keys -t "$SESSION" -l '/quit'
tmux -L "$SOCK" send-keys -t "$SESSION" Enter
sleep 0.3
Evidence: Raw tmux pane captures (plain + ANSI)

 pi v0.83.0
 escape interrupt · ctrl+c/ctrl+d clear/exit · / commands · ! bash · ctrl+o more
 Press ctrl+o to show full startup help and loaded resources.

 Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.

[Extensions]
  fm-calm.ts, slow-provider.ts, standalone-calm.ts


 Warning: tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys
 on` to ~/.tmux.conf and restart tmux.


 sail on


 DUAL_CAL

       <|
~~~~~~\__/~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
────────────────────────────────────────────────────────────────────────────────────────────────────

────────────────────────────────────────────────────────────────────────────────────────────────────
/private/var/folders/l8/tyd57dc53v3938ttkwmw5kgr0000gp/T/dual-calm-shared.xlCBFB/project (master)
0.0%/4.1k (auto)                                                                    (dual-calm) slow
- Outcome: ⚠️ 1 warning across 1 run (13m56s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ℹ️ .pi/extensions/lib/fm-calm-working-ship.ts:56 - Sharing the slot makes widget removal cross-extension, not just widget installation. In .pi/extensions/fm-calm.ts:145-153 a true->false transition calls ui.setWidget("calm-working-ship", undefined) and ui.setWorkingVisible(true). In a dual-install session that clears whatever occupies the shared slot, so toggling Firstmate Calm off mid-run (registerCommand handler at fm-calm.ts:452) or agent_settled (line 436) removes the standalone Calm's boat and restores Pi's stock working row until the other extension's next transition. The mirror case is symmetric: if the standalone clears the slot, Firstmate's cached workingShipShown stays true, so it will not re-install its boat until the next agent_start. Both windows are bounded by the remaining run and neither regresses single-install behavior, which is the tradeoff the shared-slot contract comment deliberately accepts; recording it only so the accepted scope is explicit.
  • ℹ️ tests/fm-calm-pi-extension.test.sh:1954 - The new parity assertion (and its fixture twin at line 2021) can only pin this repo's half of a two-implementation contract; the standalone Pi Calm extension is not present in this tree (no in-repo calm implementation outside .pi/extensions/), so a rename on that side silently restores the duplicate-boat defect with this suite still green. The source comment's "rename the slot in both implementations together" is the only available enforcement, which is the correct honest bound for a repo that does not own the other artifact. No in-repo action possible.
⚠️ **Test** - 1 warning
  • ⚠️ tests/fm-calm-pi-extension.test.sh:3261 - Pre-existing, unrelated failure in the same test file: test_interactive_terminal_e2e fails at /export did not complete while calm mode was on. It fails identically on the base commit fb368dc (verified by checking out the base versions of the three touched files and re-running), so this change did not introduce it. Direct probe of the same flow against the locally installed Pi 0.83.0 shows the product is fine: /export under Calm does write its HTML artifact (279KB), but M-s no longer submits the composer (Enter does) and the Session exported to: &lt;path&gt; status text never appears in the pane. The suite documents verification against Pi 0.81.1/0.82.0, so this reads as test staleness against the newer locally installed Pi rather than a Calm defect; it also self-skips wherever Pi/tmux are absent, so it does not gate CI. I left it untouched because fixing it would widen this deliberately narrow two-file upstream patch.
  • bin/fm-test-run.sh tests/fm-calm-pi-extension.test.sh (run twice; the new test_working_ship_widget_key_parity and the working-ship fixture with its in-fixture key check both pass)
  • Baseline comparison: git checkout fb368dc -- .pi/extensions/lib/fm-calm-working-ship.ts tests/fm-calm-pi-extension.test.sh docs/calm.md &amp;&amp; bin/fm-test-run.sh tests/fm-calm-pi-extension.test.sh (then restored to 7dcdd89) — same /export failure at base
  • CI-shaped run with no Pi/node/tmux: env -i PATH=/usr/bin:/bin:/usr/sbin:/sbin FM_PI_PACKAGE_DIR=/nonexistent bash bin/fm-test-run.sh tests/fm-calm-pi-extension.test.sh — every Pi-dependent fixture skips, the widget-key parity check still runs and passes
  • Mutation check of the new guard: sed the key to firstmate-calm-working-ship, re-run the same CI-shaped command — fails with the intended contract message, then restored the file
  • Dual-install end-to-end in real Pi 0.83.0 + tmux: dual-calm-e2e.sh shared (Firstmate Calm + standalone-Calm stand-in, tracked source) → 1 boat row
  • dual-calm-e2e.sh private (same harness, key reverted to the pre-fix firstmate-calm-working-ship in the fixture copy only) → 2 boat rows
  • dual-calm-e2e.sh solo (Firstmate Calm alone, shared key) → 1 boat, single-install rendering unchanged
  • Manual probe of the pre-existing failure: drove /export &lt;path&gt; in a live Calm session with M-s and with Enter, confirmed the exported HTML file is written on Pi 0.83.0 while the pane status text never appears
  • Rendered the three ANSI pane captures plus the new docs/calm.md sentence to HTML and screenshotted with headless Chrome
⚠️ **Document** - 1 info
  • ℹ️ .pi/extensions/lib/fm-calm-working-ship.ts:51 - Judgment call, left as-is: the cross-repo rename contract ("rename the slot in both Calm implementations together or dual-install duplicates the boat") is owned solely by the code comment at .pi/extensions/lib/fm-calm-working-ship.ts:51-55 and enforced by two assertions in tests/fm-calm-pi-extension.test.sh (ungated source-level parity check plus the Pi-gated fixture check). The maintainer-architecture section docs/calm-mode-feasibility.md 'Calm working presentation' does not restate it. Per the placement policy, code comments own external constraints and safety invariants, so adding a prose copy there would create a second location to keep in sync; docs/calm.md:7 already carries the user-visible consequence. No edit made.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Firstmate Calm and the user-global standalone Pi Calm both install an
animated working-ship widget during agent runs. Each claimed its own Pi
widget key, so a session loading both (the main Firstmate home) rendered
two boats. Pi replaces widgets under one key, so claiming the shared
"calm-working-ship" slot keeps dual-install sessions to a single boat
while a Firstmate-only session is unchanged.

Pins the shared slot contract in the working-ship module test so the key
cannot silently diverge again.
…al-install

The key-parity assertion inside the Pi fixture only runs where the
@earendil-works/pi-coding-agent package is installed, so CI never
exercised it. Add a source-level twin that needs nothing but the
tracked file, and note in docs/calm.md that the boat shares the
standalone Pi Calm working-row widget slot.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant