Skip to content

fix(dashboard): wake firstmate when the Admiral writes a note on a card - #39

Merged
joliverMI merged 3 commits into
mainfrom
fm/fm-admiral-note-wakes-firstmate
Sep 8, 2026
Merged

joliverMI merged 3 commits into
mainfrom
fm/fm-admiral-note-wakes-firstmate

Conversation

@joliverMI

Copy link
Copy Markdown
Owner

Intent

Bug: on 2026-09-07 the Admiral wrote "is this still needing attention?" on a board card as a communication note and nobody answered for 2.6 hours, because the board only wakes firstmate when he approves a plan, never when he writes a note.

Fix: in the note handler (bin/fleet-dashboard/server/store.py add_note), when the note's author is "admiral", publish one durable wake through the existing bin/fleet-dashboard/server/wake.py path (publish_check_wake, which calls fm_wake_append) - the same publisher the approval path already uses, not a hand-written queue append. The wake is shaped like the approval wake: check: dashboard-note <card-id> - he wrote on the card; read and answer it. Agent-authored notes must publish nothing. A wake-publish failure must never fail the note write - the note is his either way - and mirrors exactly how the approval path (_publish_approval_wake) handles the same failure: log loudly to stderr and record an audit finding, but never raise back to the caller.

Also updated the trigger documentation so this wake is discoverable: .agents/skills/fleet-dashboard/SKILL.md now documents that a note on the communication tab wakes firstmate with check: dashboard-note <card-id>, mirroring the approval wake documentation already there, and AGENTS.md section 13's one-line fleet-dashboard entry now also names the dashboard-note wake meaning (the Admiral wrote on that card; answer on the card in that turn).

Tests added to tests/fm-dashboard.test.sh, following the existing approval-wake test pattern exactly:

  • an admiral note publishes exactly one durable check wake record naming the card
  • an agent note publishes no wake record
  • a note is still saved (verified via the HTTP API returning success and the text appearing in show) even when the wake queue directory is made unwritable, proving publish failure never costs the note

Constraints observed: never write the wake queue directly - only through wake.py's existing publish_check_wake/fm_wake_append path; never touch the live systemd-managed board process; this is firstmate's own shared tracked material (bin/, .agents/skills/, AGENTS.md, tests/) so firstmate-coding-guidelines applies - ran bin/fm-lint.sh (shellcheck clean) and bin/fm-doc-audience-check.sh (clean) before considering the work done.

What Changed

  • Store.add_note in bin/fleet-dashboard/server/store.py now calls a new _publish_note_wake when the note's author is admiral, publishing one durable check: dashboard-note <card-id> - he wrote on the card; read and answer it record through the existing publish_check_wake path in wake.py after the note commits. Agent-authored notes publish nothing. A WakePublishError never fails the note write: it is logged to stderr and recorded as a note-wake-unpublished audit finding on the card, mirroring _publish_approval_wake.
  • Documented the new wake where it is discoverable: the fleet-dashboard skill's communication-tab entry, the fleet-dashboard line in AGENTS.md section 13, the note command's --author usage in bin/fm-dashboard.sh, and a new "A note he writes wakes firstmate the same way" paragraph plus the discrepancy-log description in docs/dashboard.md. wake.py's module and function docstrings now describe both the approval and note wakes.
  • Added three tests to tests/fm-dashboard.test.sh following the approval-wake pattern: an admiral note publishes exactly one wake record naming the card, an agent note publishes no wake record, and a note posted via the HTTP API is still saved (HTTP 201, text visible in show) with a note-wake-unpublished finding recorded when the state directory is made unwritable.

Risk Assessment

✅ Low: The change is a small, well-bounded addition that reuses the existing approval-wake publisher and failure handling verbatim, both prior-round fixes are verified present and correct in the current code, the UI's note write sends author "admiral" so the reported failure sequence is covered, and the tests assert observable persisted state through the public HTTP and CLI surfaces.

Testing

Ran the targeted dashboard test file at the target commit (all pass, including the three new note-wake cases), reproduced the bug by running the same new tests against the base commit (fails: admiral note publishes no wake), and performed a manual end-to-end pass against a real board process showing the wake record in firstmate's queue, firstmate's own drain reading it back, agent notes staying silent, and the unwritable-queue case saving the note while logging loudly to stderr and the board's discrepancy log (captured in a CLI transcript and a screenshot of the rendered board). No failures or missing evidence.

Evidence: CLI/API transcript: admiral note wake, drain read-back, agent silence, unwritable-queue failure path

Source: CLI/API transcript: admiral note wake, drain read-back, agent silence, unwritable-queue failure path

# Admiral note wakes firstmate - end-to-end CLI transcript

Fresh temporary FM_HOME, real board process started through bin/fm-dashboard.sh, driven only via the CLI and HTTP API.

`` `
$ /home/joliv/.no-mistakes/worktrees/4cc5c0885385/01M2086S42GW8BK4Z4ZCY1NF9C/bin/fm-dashboard.sh start
fleet dashboard started (pid 2824766) - http://127.0.0.1:38707/  api reachable at http://127.0.0.1:38707/api/health  log: /tmp/fm-note-e2e.fXlJyB/state/dashboard.log
$ fm-dashboard.sh add ...  -> card board-card-the-admiral-asks-about-ows7

## 1. The Admiral writes a communication note (the reported bug scenario)
$ fm-dashboard.sh note board-card-the-admiral-asks-about-ows7 --tab communication --author admiral --text 'is this still needing attention?'
board-card-the-admiral-asks-about-ows7: note added to communication

$ cat $FM_HOME/state/.wake-queue   (tab-separated; firstmate's durable wake queue)
1788863050 <TAB> 1 <TAB> check <TAB> dashboard-note:board-card-the-admiral-asks-about-ows7 <TAB> check: dashboard-note board-card-the-admiral-asks-about-ows7 - he wrote on the card; read and answer it

## 2. firstmate's own wake drain (bin/fm-wake-drain.sh) reads it back
$ FM_HOME=<copy> bin/fm-wake-drain.sh
1788863050	1	check	dashboard-note:board-card-the-admiral-asks-about-ows7	check: dashboard-note board-card-the-admiral-asks-about-ows7 - he wrote on the card; read and answer it
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 1 --recovery-generation 2825763.1788863050.veQEKp

## 3. An agent-authored note publishes nothing
$ fm-dashboard.sh note board-card-the-admiral-asks-about-ows7 --tab communication --author agent --text 'routine update'
board-card-the-admiral-asks-about-ows7: note added to communication
$ grep -c 'dashboard-note:board-card-the-admiral-asks-about-ows7' $FM_HOME/state/.wake-queue
1
(still exactly one record: the admiral's; the agent note added none)

## 4. Wake queue made unwritable: the note still saves, failure is loud
$ chmod 000 $FM_HOME/state
$ curl -X POST /api/tasks/board-card-the-admiral-asks-about-ows7/notes -d '{"tab":"communication","author":"admiral","text":"still there?"}'
{
  "id": "board-card-the-admiral-asks-about-ows7",
  "communication_notes": [
    {
      "id": 1,
      "task_id": "board-card-the-admiral-asks-about-ows7",
      "tab": "communication",
      "author": "admiral",
      "text": "is this still needing attention?",
      "link_url": null,
      "link_label": null,
      "created_at": "2026-09-08T10:24:10Z"
    },
    {
      "id": 2,
      "task_id": "board-card-the-admiral-asks-about-ows7",
      "tab": "communication",
      "author": "agent",
      "text": "routine update",
      "link_url": null,
      "link_label": null,
      "created_at": "2026-09-08T10:24:10Z"
    },
    {
      "id": 3,
      "task_id": "board-card-the-admiral-asks-about-ows7",
      "tab": "communication",
      "author": "admiral",
      "text": "still there?",
      "link_url": null,
      "link_label": null,
      "created_at": "2026-09-08T10:24:10Z"
    }
  ]
}
HTTP 201
$ chmod 755 $FM_HOME/state

$ fm-dashboard.sh show board-card-the-admiral-asks-about-ows7
id:       board-card-the-admiral-asks-about-ows7
title:    Board card the Admiral asks about
status:   not_started
captain:  firstmate
agent:    
starred:  false

--- prompt ---
verify the note wake

--- communication ---
[admiral 2026-09-08T10:24:10Z] is this still needing attention?
[agent 2026-09-08T10:24:10Z] routine update
[admiral 2026-09-08T10:24:10Z] still there?

$ grep dashboard-note $FM_HOME/state/.wake-queue   (no second admiral record: the publish really failed)
1

$ cat $FM_HOME/state/dashboard.log   (server stderr - the loud failure)
fleet dashboard listening on http://127.0.0.1:38707  (db: /tmp/fm-note-e2e.fXlJyB/data/dashboard.db)
dashboard: NOTE NOT ANNOUNCED for board-card-the-admiral-asks-about-ows7 - he wrote on the card but firstmate was not woken: firstmate's wake-queue writer did not finish within 5s. Read and answer it by hand.

$ curl /api/audit/status | jq .log   (the board's own discrepancy log)
[
  {
    "id": 1,
    "task_id": "board-card-the-admiral-asks-about-ows7",
    "kind": "error",
    "text": "he wrote on this card, but firstmate could not be notified (firstmate's wake-queue writer did not finish within 5s) - this card needs reading and answering by hand",
    "key": "note-wake-unpublished",
    "occurrences": 1,
    "created_at": "2026-09-08T10:24:15Z",
    "last_seen_at": "2026-09-08T10:24:15Z"
  }
]
`` `
- Evidence: [Screenshot: board page showing the note-wake-unpublished discrepancy-log row](https://github.com/joliverMI/firstmate/blob/9a02924822f47cd075b5af0cff0e4ae179976f0c/.no-mistakes/evidence/fm/fm-admiral-note-wakes-firstmate/board-with-note-wake-finding.png)
Evidence: Base-commit reproduction: new tests fail before the fix

Source: Base-commit reproduction: new tests fail before the fix

not ok - his note published no wake record for the card he wrote on

ok - server-status reports the running, reachable server
ok - add/list/show round-trip works through the CLI
ok - status, title, and captain updates persist through the CLI
ok - testing and review are separate, independently settable statuses
ok - waiting status carries its target card and reason
ok - interpretation tab stays absent until something real is recorded
ok - link policy rejects GitHub/PR and local-only links, accepts a real one
ok - needs-action status carries a reason and sorts above every other status
ok - needs-action refuses a missing or report-shaped reason, on both status and add, and the server enforces both independently of the CLI
ok - no path - either spelling, a blank reason, or PATCH - can put a card in needs-action without a real ask
ok - needs-attention is accepted as an input alias for needs-action, enforced the same way, and never emitted
ok - needs-review refuses a missing, blank, or absent plan on every writer, and accepts one the card already holds
ok - an approval binds to the verbatim plan it was given for, is refused against any other text, and never drifts onto an edited plan
ok - his approval takes the card out of needs_review to not_started, with the transition and its reason on the card's own history
ok - a refused or stale approval moves nothing and wakes nobody, and an approval on a card that has moved on records consent without dragging it back
ok - an approval publishes one durable check record through firstmate's own writer, and firstmate's own drain reads it back
not ok - his note published no wake record for the card he wrote on
Evidence: Wake record published by an admiral note (from the transcript)
1788863050 1 check dashboard-note:board-card-the-admiral-asks-about-ows7 check: dashboard-note board-card-the-admiral-asks-about-ows7 - he wrote on the card; read and answer it

$ FM_HOME=<copy> bin/fm-wake-drain.sh
1788863050 1 check dashboard-note:board-card-the-admiral-asks-about-ows7 check: dashboard-note board-card-the-admiral-asks-about-ows7 - he wrote on the card; read and answer it
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 1 ...
Evidence: tests/fm-dashboard.test.sh at target commit

Source: tests/fm-dashboard.test.sh at target commit

ok - server-status reports the running, reachable server
ok - add/list/show round-trip works through the CLI
ok - status, title, and captain updates persist through the CLI
ok - testing and review are separate, independently settable statuses
ok - waiting status carries its target card and reason
ok - interpretation tab stays absent until something real is recorded
ok - link policy rejects GitHub/PR and local-only links, accepts a real one
ok - needs-action status carries a reason and sorts above every other status
ok - needs-action refuses a missing or report-shaped reason, on both status and add, and the server enforces both independently of the CLI
ok - no path - either spelling, a blank reason, or PATCH - can put a card in needs-action without a real ask
ok - needs-attention is accepted as an input alias for needs-action, enforced the same way, and never emitted
ok - needs-review refuses a missing, blank, or absent plan on every writer, and accepts one the card already holds
ok - an approval binds to the verbatim plan it was given for, is refused against any other text, and never drifts onto an edited plan
ok - his approval takes the card out of needs_review to not_started, with the transition and its reason on the card's own history
ok - a refused or stale approval moves nothing and wakes nobody, and an approval on a card that has moved on records consent without dragging it back
ok - an approval publishes one durable check record through firstmate's own writer, and firstmate's own drain reads it back
ok - an admiral note publishes exactly one durable check wake record naming the card
ok - an agent note publishes no wake record
ok - a note is saved, and the failure is written down, even when the wake queue could not be written to
ok - a card's recommended plan and his approval of it survive the status change, so the fleet can still read its authority while acting
ok - status refuses a --plan for every status but needs-review, exactly as add does
ok - a plan can only be created by the path that shows him the approval box, while a card already through it still accepts a correction
ok - correcting a plan on a card that has left needs-review still breaks the binding and keeps both texts readable
ok - a plan is stored under one normalization, so approving it exactly as displayed binds cleanly while different wording still reads stale
ok - plan refuses an unquoted multi-word plan instead of recording its first word
ok - needs-action sorts above needs-review, and both sort above ordinary work
ok - the needs-attention split migrates every card to needs-action, keeping its reason, notes, history, and blocked-age, and runs once
ok - a report phrase buried mid-clause does not refuse a genuine ask
ok - add refuses a --reason no status but needs-action can carry, instead of dropping it
ok - the catch, miss, and false-positive rates documented for the reason guard are the ones it actually achieves
ok - audit-log, audit-run, and audit-interval work end to end
ok - invalid input fails loudly with a non-zero exit, not a silent success
ok - --help prints the header through its final exit-code block
ok - every dashboard call is bounded, so a board that never answers fails fast instead of hanging
ok - a zero timeout override is refused and replaced by the default, so the bound cannot be handed back
ok - a board-answered missing id and an unreachable board are distinguishable by exit code
ok - the testing-to-review split migration converts only pre-existing testing cards, and runs at most once
ok - a start that cannot bind leaves the migration pending, and stop waits for the port
ok - restart brings the board back from a crash, from stopped, and from healthy
ok - the lifecycle commands refuse a recycled pid instead of signalling it
ok - starring persists and delete requires --confirm
ok - the CLI, the server and the page the browser loads all name the same captains
ok - start defaults its bind host to the address recorded in config/dashboard-url, and an explicit FM_DASHBOARD_HOST still wins
ok - start keeps waiting for a slow but healthy bind instead of killing it
ok - start refuses an address something else already answers on, leaving it alone and no pidfile behind
ok - start stops the process and fails loudly, naming the failure, when the API never answers after it comes up

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 3 issues found → auto-fixed ✅
  • ⚠️ tests/fm-dashboard.test.sh:1448 - test_a_note_is_saved_even_when_the_wake_cannot_be_published relies on chmod 000 $FM_HOME/state to make the wake publish fail, but never asserts the failure actually happened (no check that the wake queue lacks a dashboard-note:&lt;id&gt; record or that a note-wake-unpublished audit finding was recorded). When the suite runs as root, chmod 000 is ignored, the wake publishes normally, and the test passes without exercising the failure path it claims to prove. The repo already has a convention for this (tests/fm-dashboard-card-link.test.sh:1740 skips permission-based cases under root with an explicit pass &#34;skipped ...&#34;). Add the same [ &#34;$(id -u)&#34; -ne 0 ] || { pass ...; return 0; } guard and, after restoring the directory, assert the queue has no dashboard-note:$id line (and/or that the card's audit finding with key note-wake-unpublished exists) so the test distinguishes 'saved despite failure' from 'saved, nothing failed'. Note also this path costs ~5s per run because fm_lock_acquire_wait spins until wake.py's PUBLISH_TIMEOUT_SECONDS kills it - acceptable, but worth a comment so nobody mistakes it for a hang.
  • ℹ️ bin/fleet-dashboard/server/wake.py:5 - Two prose statements are now false after this change: the module docstring of bin/fleet-dashboard/server/wake.py still says nothing the board records reaches firstmate 'except his approval, which is the one board write that hands work BACK to the fleet', and docs/dashboard.md's discrepancy-log paragraph (around line 326) lists 'an approval whose wake to firstmate could not be published' as the only wake-failure row shape, while the new note-wake-unpublished finding now also lands there. SKILL.md and AGENTS.md were updated per intent; these two were not. Both are one-line fixes.
  • ℹ️ bin/fleet-dashboard/server/store.py:924 - _publish_note_wake is a near-verbatim copy of _publish_approval_wake (try publish_check_wake / catch WakePublishError / stderr headline / record_audit_finding guarded by a broad except). A small shared helper taking (key, payload, headline, audit_text, audit_key) would collapse both to a few lines each and guarantee the two failure paths stay identical, which is the invariant the intent asks for. The intent explicitly chose mirroring, so this is a follow-up refactor, not a blocker.

🔧 Fix: assert note-wake failure in test; refresh stale wake docs
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-dashboard.test.sh at target commit 16c008c: all cases pass including the three new note-wake tests (test_his_note_leaves_firstmate_a_wake_record, test_an_agent_note_publishes_no_wake, test_a_note_is_saved_even_when_the_wake_cannot_be_published)
  • Regression reproduction: extracted base commit ddc4b6c to a temp dir, overlaid the target commit's tests/fm-dashboard.test.sh, ran it; fails with not ok - his note published no wake record for the card he wrote on
  • Manual E2E in a fresh temp FM_HOME with a real board started via bin/fm-dashboard.sh start: fm-dashboard.sh note &lt;id&gt; --tab communication --author admiral then inspected $FM_HOME/state/.wake-queue (one 5-field check record, payload check: dashboard-note &lt;id&gt; - he wrote on the card; read and answer it)
  • Manual E2E: copied the queue into a scratch home and ran bin/fm-wake-drain.sh; it presented the dashboard-note record and printed WAKE_ACK_REQUIRED
  • Manual E2E: fm-dashboard.sh note ... --author agent left the queue at exactly one dashboard-note record
  • Manual E2E: chmod 000 $FM_HOME/state, POST /api/tasks/<id>/notes as admiral -> HTTP 201 with the note in the response; fm-dashboard.sh show lists the note; no new queue record; state/dashboard.log contains NOTE NOT ANNOUNCED; /api/audit/status log holds a note-wake-unpublished error finding naming the card
  • Headless Chrome screenshot of the running board page showing the discrepancy-log row for the unpublished note wake
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

jolivertx and others added 3 commits September 8, 2026 06:12
A note he writes in the communication tab used to sit unread until a
session happened to look, the same way an unwoken approval did before.
Publish a `check: dashboard-note <card-id>` wake through the existing
approval publisher when the note's author is admiral; agent-authored
notes publish nothing, and a wake-publish failure never costs him the
note itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZEpZ1SxCyt5n9ChfaMcEX
@joliverMI
joliverMI merged commit 2677ac0 into main Sep 8, 2026
13 checks passed
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.

2 participants