Conversation
FM_HARNESS_RE matched the bare command name "claude", but Claude Code names its shared background daemon and the daemon's pooled workers "claude" too. The daemon is parented to init, is shared by every session of the user, and outlives all of them, so when a hook or tool call was hosted by a pooled spare the ancestry walk resolved the daemon's pid as that session's identity. That broke identity in both directions. A session-start fm-lock.sh call hosted by a pooled spare published the daemon's pid into state/.lock, so two different sessions each verified ownership of a lock the other wrote, and because the daemon never dies no later session could see that lock go stale: fm-lock.sh refused acquisition permanently and the home stayed read-only. With a daemon pid in the lock, fm-claude-stop-autoarm.sh found ownership false but the recorded owner alive, so it exited 0 on every Stop for the whole session and nothing routinely armed the watcher. The walk now classifies process shapes through one shared owner, fm_harness_session_match, used by both the ancestry walk and the holder-liveness check so they cannot disagree about the same pid. The daemon (matched by its "daemon" subcommand, not by one --origin value), bg-pty-host, and bg-spare are rejected as non-session shapes. Claude Code's versioned executable is now recognized as a session, so resumed, forked, and app-hosted sessions resolve their own per-session pid instead of the daemon-owned pty host above them. Infrastructure met before any session match is skipped and the walk continues, which preserves the nested bg-spare chain fixed in kunchenguid#1206; infrastructure met after a match ends the contiguous run. When every candidate is infrastructure the walk resolves nothing and every caller fails closed, because a pooled worker's ancestry and inherited environment both describe the daemon and carry no evidence of the session that claimed it. A lock recorded against infrastructure is now a reclaimable owner, so an already-wedged home recovers through the existing stale-owner path. Every caller of the shared predicates was reviewed: all six call sites in fm-claude-stop-autoarm.sh and fm-lock.sh were affected and are fixed by this change. fm-harness.sh answers harness kind rather than session identity and is unaffected; fm-sessionstart-nudge.sh classifies no shape and was exposed only through lock content.
…oned exec and fail-closed inertness
FM_HARNESS_RE matched the bare command name "claude", but Claude Code names its shared background daemon and the daemon's pooled workers "claude" too. The daemon is parented to init, is shared by every session of the user, and outlives all of them, so when a hook or tool call was hosted by a pooled spare the ancestry walk resolved the daemon's pid as that session's identity. That broke identity in both directions. A session-start fm-lock.sh call hosted by a pooled spare published the daemon's pid into state/.lock, so two different sessions each verified ownership of a lock the other wrote, and because the daemon never dies no later session could see that lock go stale: fm-lock.sh refused acquisition permanently and the home stayed read-only. With a daemon pid in the lock, fm-claude-stop-autoarm.sh found ownership false but the recorded owner alive, so it exited 0 on every Stop for the whole session and nothing routinely armed the watcher. The walk now classifies process shapes through one shared owner, fm_harness_session_match, used by both the ancestry walk and the holder-liveness check so they cannot disagree about the same pid. The daemon (matched by its "daemon" subcommand, not by one --origin value), bg-pty-host, and bg-spare are rejected as non-session shapes. Claude Code's versioned executable is now recognized as a session, so resumed, forked, and app-hosted sessions resolve their own per-session pid instead of the daemon-owned pty host above them. Infrastructure met before any session match is skipped and the walk continues, which preserves the nested bg-spare chain fixed in kunchenguid#1206; infrastructure met after a match ends the contiguous run. When every candidate is infrastructure the walk resolves nothing and every caller fails closed, because a pooled worker's ancestry and inherited environment both describe the daemon and carry no evidence of the session that claimed it. A lock recorded against infrastructure is now a reclaimable owner, so an already-wedged home recovers through the existing stale-owner path. Every caller of the shared predicates was reviewed: all six call sites in fm-claude-stop-autoarm.sh and fm-lock.sh were affected and are fixed by this change. fm-harness.sh answers harness kind rather than session identity and is unaffected; fm-sessionstart-nudge.sh classifies no shape and was exposed only through lock content.
…oned exec and fail-closed inertness
A Claude Code mid-conversation fork (observed live 2026-07-29 16:51, Claude Code 2.1.220) replaces the session process: a new pid carrying a new --session-id continues the working conversation while the pre-fork pid stays alive as an idle interactive process. The lock still recorded the pre-fork pid, so the working session failed fm_session_lock_owned_by_self forever, the Stop auto-arm stayed correctly-but-silently inert, and nothing noticed that supervision was down: the working session neither owned its home nor demoted. Identity now has two layers. state/.lock keeps the owning pid; a new state/.lock-session sidecar records the owning harness SESSION id when a trusted source proves it - the Stop-payload session_id hint exported by the auto-arm, or the CLAUDE_CODE_SESSION_ID/CLAUDE_PID env pair Claude Code plants in every tool shell, accepted only when CLAUDE_PID names exactly the ancestry-resolved pid so a daemon-inherited or outer-session environment can never leak in. Command-line text is never an identity source because prompts are argv-visible. fm_session_lock_relation classifies a non-owned holder as self, same-session, live-other, or stale. A same-session holder is the same logical session in a replaced process, so fm-lock.sh re-keys the pid and the auto-arm recovers through the existing fm-lock.sh delegation, after the unchanged AFK and need gates. A live-other holder is never armed over, reclaimed, inherited, or forced - but the auto-arm no longer goes silent: once per distinct holder it wakes the model (exit 2, deduped via state/.claude-autoarm-foreign-lock) with the real diagnosis, and the fm-lock.sh refusal now names the holder's pid, session, start time, and terminal, plus the exact recovery action when this process's own argv shows it was forked from the recorded session. Dead-holder reclaim, daemon-shape rejection, lock-refused read-only, and the no-force rule are unchanged; non-Claude harnesses resolve no session id and keep the exact pid-only contract. Also fixes a latent test-hermeticity bug in the cherry-picked shared-daemon reclaim test: a single-command fake-claude body was tail-exec-collapsed by bash, so the ancestry walk escaped to whatever real session hosted the test run (it only passed locally because a real Claude session sat above the runner; CI has none).
…key scope honestly Independent review confirmed against the verification data that Claude Code's --fork-session mints the successor a NEW session id (cfaf5775 -> 42ed4142 in docs/verification/supervision.md), so same-session re-keying covers only successions that KEEP their session id; the fork successor's path is the loud foreign-owner notice while the pre-fork process lives, then the ordinary stale reclaim the moment it exits. That split is deliberate - fork lineage cannot distinguish an abandoned live holder from an idle working one, so automatic takeover from any live different-session holder would break the never-inherit boundary - but the lib, hook, and doc wording previously blurred the two legs. State the coverage split explicitly in fm-session-lock-lib.sh, fm-lock.sh, fm-claude-stop-autoarm.sh, docs/watcher-continuity.md, and the verification record, and add the missing end-to-end fork test: sidecar holds the pre-fork id, the Stop payload proves a different id, the hook emits exactly the foreign notice naming the holder and its session while mutating nothing, and the firing after the pre-fork process exits reclaims, re-keys to the successor's own id, and arms. Also make the two foreign-owner tests hermetic: their single-command fake-claude bodies were tail-exec-collapsed by bash, so the hook's ancestry escaped to whatever real session hosted the run; in CI, where no real session exists, classification would have failed closed and the tests would have failed.
…ession re-key Third-review TOCTOU findings, dispositioned by evidence. A same-session re-key rewrites the sidecar with a byte-identical value, so skip the clear-then-rewrite entirely in that case: the recorded identity is then never even transiently absent, which removes both the crash window that degraded a re-keyed lock to pid-only and the false foreign-owner notice a concurrent auto-arm firing could emit while the sidecar was cleared. Every other acquisition keeps the clear-first order deliberately: the reviewer-proposed alternative (retain the old sidecar until the new one lands) would let a crash leave the previous owner's identity beside the new owner's pid, and a relaunch successor of that PREVIOUS session could then prove same-session against a live foreign holder and steal the lock - clear-first makes every crash degrade toward pid-only, the safe direction. A sidecar write failure now also warns on stderr with the concrete consequence (a replacement process for this session cannot take over until this process exits) instead of degrading silently, and the lib header notes that alternating re-keys between two live incarnations of one session stay inside that session and settle once the superseded incarnation idles.
…n efforts Add an agent-only effort-maps skill owning a wayfinder-style orientation map convention at data/maps/<effort-slug>.md: Destination, Notes, a Decisions-so-far index over authoritative decision records, fog-of-war Not-yet-specified, and Out-of-scope, adapted from the MIT-licensed wayfinder skill in mattpocock/skills with attribution. Wire the convention in with pointer-sized edits only: a decision-hold-lifecycle operating-sequence step to append newly resolved decisions to an active map, a bearings line surfacing each active map's Destination and newest decisions, one AGENTS.md section 13 trigger line, and the docs audience inventory entry.
…dd regression test
…c busy-state contract Rebasing onto kunchenguid#1327 (semantic lifecycle state) removed fm-watch's rendered busy-regex selection; the only rendered fallback left is Grok-scoped inside bin/fm-busy-lib.sh. Standalone Hermes has no trusted semantic busy source, so a Hermes task classifies unknown and its ASCII cancel row stays a delivery-only signature in the shared tmux matcher, mirroring Kimi's demotion. The watcher test now asserts that truth instead of the pre-kunchenguid#1327 rendered-row recognition, and the adapter doc records the contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Effort maps: wayfinder-style orientation maps
Session-lock fork-handover fix (panel-hardened)
Hermes worker adapter (verified)
…solved to the fork's reworked versions
* fix: preserve Calm boat continuity across working periods (kunchenguid#1356) * fix(calm): resume working boat from frozen column across runs Keep one extension-owned boat animation for the Pi session so settling freezes column and direction, the next working period resumes there without hidden-time jumps, and only a fresh session resets to the left edge. * no-mistakes(review): Freeze Calm boat from last rendered state * no-mistakes(document): Document Calm boat continuity contract * feat(board): captain-facing kanban board generated from live fleet state Add bin/fm-board.sh, a renderer over the canonical fm-fleet-snapshot.sh JSON contract plus the hand-maintained effort maps under data/maps/. It writes a self-contained Lavish-reviewable HTML board with the ordered pipeline columns Decide / Queued / Building / Review / Landed: captain holds and open keyed decisions land in Decide with approval panels (radio options plus a free-text override, one queued Lavish prompt per submit), queued rows surface blockers, building cards show the assigned harness/model/effort, review cards link the recorded PR, and Done rows land in Landed. Cards are draggable; a drop queues a move order. All interactions queue prompts - nothing on the board mutates fleet state. Approval panels render as siblings after each card's todo list, never inside it, avoiding the details-in-ul clipping bug the hand-built prototype hit; a test enforces that structurally on the output. Expose the recorded model and effort meta fields on fleet-snapshot task rows (additive to schema fm-fleet-snapshot.v1) so the board never re-parses state files. Document the board in docs/board.md (classified operator-current) with a toolbelt row in docs/scripts.md. * no-mistakes(document): docs: count fm-board.sh among fleet-snapshot renderers --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Co-authored-by: Louis Condevaux <louiscondevaux@Louiss-MacBook-Pro.local>
DaisyUI's .badge has a fixed height and does not wrap, so a card's metadata badge whose text runs past one line has that text clipped. The effort-map card's fog: and out of scope: badges hit this today, but any long .meta-row badge can. Add a stylesheet override in the generated board that lets .meta-row badges grow and wrap. Presentation only: truncation logic and badge markup are unchanged, and badges outside .meta-row keep their default fixed height.
Author
|
Opened here by mistake - this change targets our own fork and was never intended as an upstream contribution. Closing; apologies for the noise. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Fix vertically clipped badge text in the kanban board generated by bin/fm-board.sh. The board renders card metadata as DaisyUI badges inside .meta-row; DaisyUI's .badge has a fixed height and does not wrap, so a badge whose text runs longer than one line has its text clipped. A real browser layout audit of a generated board reported two severe clipped-text failures (vertical, 6.5px overflow, viewport 1152): the 'fog:' and 'out of scope:' badges on an effort-map card in the top band. Their text is long by nature - the generator already truncates it with an ellipsis and even truncated it exceeds one badge line. The captain asked to fix the CLASS of defect (any long .meta-row badge), not just those two spans.
Deliberate decisions, so a reviewer reading only the diff has the context:
Test coverage: tests/fm-board.test.sh gains an assertion inside the existing render test that extracts the emitted .meta-row .badge rule from the GENERATED HTML (the executable interface, per this repo's rule that tests must never assert implementation-source bytes) and checks height:auto and white-space:normal, so the defect cannot silently return. It matches the file's existing assert_contains conventions rather than adding a new runner.
Verification already performed: the full tests/fm-board.test.sh suite passes (4/4). A board generated from live fleet data was opened in a real browser and all 60 .meta-row badges measured - zero clipped; the previously-clipped fog badge now renders 31px tall against 29px of content, wrapping to two lines. docs/board.md was checked and documents no styling behavior, so per the task it was correctly left untouched.
Two established environment facts: (1) local lint cannot run on this machine - the pinned shellcheck is linux-x86_64 only and this is Apple Silicon macOS; bin/fm-lint.sh correctly refuses. (2) origin is the captain's own fork Lcxiv/firstmate with push access, and that fork has no CI configured, so no GitHub Actions run will ever appear for this PR.
What Changed
.meta-row .badgerule to the inline stylesheet emitted bybin/fm-board.sh(height:auto; min-height:1.1rem; white-space:normal; text-align:left; line-height:1.3;plus small vertical padding), so card metadata longer than one line wraps instead of being vertically clipped by DaisyUI's fixed-height, no-wrap.badge. The selector is deliberately scoped to.meta-rowso header count badges keep their current rendering, and the rule is inline so the board stays a single self-contained HTML file.fog:andout of scope:text on effort-map cards exceeds one badge line, so this is a presentation-layer fix on top of truncation, not a replacement for it.tests/fm-board.test.shto extract the emitted.meta-row .badgerule from the generated HTML and assertheight:autoandwhite-space:normal, and documented the invariant in thebin/fm-board.shheader comment beside the existing clipping-bug note. The full board suite passes 4/4, and a browser layout audit of a generated board measured zero clipped.meta-rowbadges after the change.Risk Assessment
✅ Low: A single presentation-only CSS rule tightly scoped to
.meta-row .badge, with no logic, markup, or data-path changes, plus a generated-HTML regression assertion that fails correctly if the rule is removed.Testing
Ran the targeted suite
tests/fm-board.test.sh(4/4 pass) and verified its new assertion actually catches the defect by removing the CSS rule and watching it fail. For end-user evidence I generated a real board with long effort-map metadata, produced a pre-fix baseline of the identical HTML minus the one new rule, and opened both in Chrome: before the fix 1-2.meta-rowbadges clip with 8px of vertical overflow depending on viewport, after the fix zero of 12 clip at 1600, 1152, and 500px, with the fog badge growing to 31px and wrapping to two fully readable lines. The 1600px pair also shows a ticket card'sstage:badge — not one of the two originally reported spans — going from clipped to wrapped, which is the direct evidence that the class of defect is fixed rather than the two instances. Header count badges measured an unchanged 24px fixed height throughout, and the rendered output still shows the ellipsis truncation, so the scoping and out-of-scope constraints both hold. Screenshots and the rendered board are in the evidence directory; the worktree is clean and temp fixtures were removed./var/folders/gp/phywrkfd3kj2jp2f764xmldw0000gn/T/no-mistakes-evidence/01KYWFCVGYZXGXP8SP4JH2HFAJ/01-before-clipped-badges.png)/var/folders/gp/phywrkfd3kj2jp2f764xmldw0000gn/T/no-mistakes-evidence/01KYWFCVGYZXGXP8SP4JH2HFAJ/02-after-wrapped-badges.png)/var/folders/gp/phywrkfd3kj2jp2f764xmldw0000gn/T/no-mistakes-evidence/01KYWFCVGYZXGXP8SP4JH2HFAJ/03-before-1600-ticket-badge-clipped.png)/var/folders/gp/phywrkfd3kj2jp2f764xmldw0000gn/T/no-mistakes-evidence/01KYWFCVGYZXGXP8SP4JH2HFAJ/04-after-1600-ticket-badge-wrapped.png)Evidence: Generated board HTML used for the post-fix measurements (self-contained, no added assets)
Evidence: Browser clipping measurements across three viewports (scrollHeight - clientHeight over every .meta-row .badge)
Evidence: New assertion fails when the fix is removed (non-vacuity proof)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
🔧 **Rebase** - 1 issue found → auto-fixed ✅
.agents/skills/afk/SKILL.md- branch carries 20 commit(s) that exist on your local main branch but were never pushed to origin/main; rebasing would bundle this unrelated work (59 file(s)) into the PR:Push main to origin, or rebase your branch onto origin/main, before gating.
🔧 Fix applied.
✅ Re-checked - no issues remain.
bin/fm-board.sh:428- bin/fm-board.sh:428 —white-space:normalonly breaks at whitespace, so a.meta-rowbadge whose text is a single unbreakable token wider than the card (e.g. a URL landing in an effort-map fog/out-of-scope item) would overflow horizontally rather than wrap. No current badge source produces such a token (fog/out-of-scope are joined with "; ",stage:is prose, repo/kind/crew/blocked-by are short), so this is not a reachable defect today — addingoverflow-wrap:anywhereto the same rule would close the residual case if it ever appears. Flagged as an observation only; it does not affect the reported clipping class.✅ **Test** - passed
✅ No issues found.
bash tests/fm-board.test.sh— full board suite, 4/4 passNon-vacuity check: deleted the.meta-row .badgeline from bin/fm-board.sh, re-ranbash tests/fm-board.test.sh→not ok - meta badges must grow past one line (missing: 'height:auto'), thengit checkout bin/fm-board.shGenerated a board viaFM_HOME=<fixture> bin/fm-board.sh --out after.htmlfrom a fixture whose effort map has longNot yet specified/Out of scopeentries, sofog:andout of scope:truncate to more than one badge lineBuilt the pre-fix baseline by stripping only the.meta-row .badgerule from the same generated HTML (grep -v→ before.html; diff confirms a 1-line delta)Opened both files in real Chrome viachrome-devtools-axi open file://…and measured every.meta-row .badgewithscrollHeight - clientHeightat viewports 1600, 1152, and 500 — before: 1/2/2 clipped at +8px; after: 0/0/0Measuredheader .badgecomputed height in every before/after pair (24px in all cases) to confirm the fix does not restyle badges outside.meta-rowCapturedchrome-devtools-axi screenshotbefore/after pairs at 1152 (effort-map fog and out-of-scope badges) and 1600 (a ticket card's longstage:badge)✅ **Document** - passed
✅ No issues found.
⏭️ **Lint** - skipped
✅ **Push** - passed
✅ No issues found.